From a9dd4015b81d4dddb4a159bcb26eaee78ee95363 Mon Sep 17 00:00:00 2001 From: pgherveou Date: Thu, 14 May 2026 16:15:18 +0100 Subject: [PATCH 1/2] feat(host-api,host-container): scaffold @parity/truapi migration Adds `@parity/truapi` (and `@parity/truapi-host` for `host-container`) as dependencies and introduces directional facade files alongside the existing transport and container code. The existing surfaces remain in place to avoid breaking downstream consumers; the migration is staged in `MIGRATION.md` next to each package. - packages/host-api/src/truapi-transport.ts: thin facade exposing a `@parity/truapi` `TrUApiTransport` built on the legacy `Provider`. - packages/host-container/src/truapi-container.ts: thin facade calling `createTrUApiServer(provider, handlers)` from `@parity/truapi-host`, with "Not Implemented" defaults until each `handleXxx` slot is lifted across. - MIGRATION.md in both packages: punch list for completing the swap (handshake retry, multiplexer removal, legacy codec re-export, etc.). Dependencies are pinned via `file:` paths to a local truapi worktree while `@parity/truapi@0.2.0` is unreleased; switch to the published version once it ships. Refs paritytech/truapi#54. --- package-lock.json | 36 ++++++ packages/host-api/MIGRATION.md | 56 +++++++++ packages/host-api/package.json | 1 + packages/host-api/src/truapi-transport.ts | 110 ++++++++++++++++++ packages/host-container/MIGRATION.md | 54 +++++++++ packages/host-container/package.json | 2 + .../host-container/src/truapi-container.ts | 81 +++++++++++++ 7 files changed, 340 insertions(+) create mode 100644 packages/host-api/MIGRATION.md create mode 100644 packages/host-api/src/truapi-transport.ts create mode 100644 packages/host-container/MIGRATION.md create mode 100644 packages/host-container/src/truapi-container.ts diff --git a/package-lock.json b/package-lock.json index 83520a20..ea305e95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,31 @@ "vitest": "4.1.5" } }, + "../truapi/.claude/worktrees/issue-54-truapi-host/js/packages/truapi": { + "name": "@parity/truapi", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "neverthrow": "^8.2.0", + "scale-ts": "^1.6.1" + }, + "devDependencies": { + "typescript": "^5.7" + } + }, + "../truapi/.claude/worktrees/issue-54-truapi-host/js/packages/truapi-host": { + "name": "@parity/truapi-host", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@parity/truapi": "file:../truapi", + "neverthrow": "^8.2.0", + "scale-ts": "^1.6.1" + }, + "devDependencies": { + "typescript": "^5.7" + } + }, "node_modules/@adobe/css-tools": { "version": "4.4.4", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", @@ -4507,6 +4532,14 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@parity/truapi": { + "resolved": "../truapi/.claude/worktrees/issue-54-truapi-host/js/packages/truapi", + "link": true + }, + "node_modules/@parity/truapi-host": { + "resolved": "../truapi/.claude/worktrees/issue-54-truapi-host/js/packages/truapi-host", + "link": true + }, "node_modules/@pkgr/core": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", @@ -16925,6 +16958,7 @@ "license": "Apache-2.0", "dependencies": { "@novasamatech/scale": "0.7.8", + "@parity/truapi": "file:../../../truapi/.claude/worktrees/issue-54-truapi-host/js/packages/truapi", "nanoevents": "9.1.0", "nanoid": "5.1.9", "neverthrow": "^8.2.0", @@ -16998,6 +17032,8 @@ "dependencies": { "@noble/hashes": "2.2.0", "@novasamatech/host-api": "0.7.8", + "@parity/truapi": "file:../../../truapi/.claude/worktrees/issue-54-truapi-host/js/packages/truapi", + "@parity/truapi-host": "file:../../../truapi/.claude/worktrees/issue-54-truapi-host/js/packages/truapi-host", "@polkadot-api/substrate-client": "^0.7.0", "nanoid": "5.1.9", "neverthrow": "^8.2.0", diff --git a/packages/host-api/MIGRATION.md b/packages/host-api/MIGRATION.md new file mode 100644 index 00000000..c7ed0a08 --- /dev/null +++ b/packages/host-api/MIGRATION.md @@ -0,0 +1,56 @@ +# host-api migration to `@parity/truapi` + +This package is being reworked into a thin compatibility facade over +[`@parity/truapi`][1]. The Rust trait surface in `paritytech/truapi` +(`rust/crates/truapi/src/api/*.rs`) is the single source of truth for wire +ids, payload shapes, and dispatch. + +The work tracking issue is [`paritytech/truapi#54`][issue]. + +[1]: https://github.com/paritytech/truapi/tree/main/js/packages/truapi +[issue]: https://github.com/paritytech/truapi/issues/54 + +## Status + +### Done + +- `@parity/truapi` added to `package.json` dependencies (currently a `file:` + link to the local truapi worktree; switch to the published version once + `@parity/truapi@0.2.0` ships). +- `src/truapi-transport.ts`: directional facade that wraps a legacy + `Provider` and produces a `@parity/truapi` `TrUApiTransport` instance. + +### Not done + +- `src/transport.ts` still owns its own request correlation, subscription + router, frame envelope, and subscription multiplexing. Replace its + internals with calls into the `@parity/truapi` transport returned from + `createTruapiTransportFacade()`. Keep the public `Transport` shape from + `./types.ts` byte-for-byte stable so downstream consumers (`host-papp`, + `product-react-renderer`, `product-bulletin`, `host-worker-sandbox`, + `host-api-wrapper`) do not break. +- Active handshake retry / readiness state currently lives in `isReady()` + inside `transport.ts`. Re-implement on top of the new transport's public + handshake helpers (the auto-response path inside `@parity/truapi` covers + passive handshake; active retry stays here as consumer policy). +- Drop subscription multiplexing (the `activeSubscriptions` map and + `getSubscriptionKey`). `@parity/truapi` exposes 1:1 wire subscriptions, + any consumer that relies on dedup adds its own multiplexer above this + package. +- Drop inbound `handleRequest` / `handleSubscription` from the public + surface, they exist only to support `host-container`, which migrates onto + `@parity/truapi-host` separately. Update `Transport` in `./types.ts`. +- Repoint the per-codec exports in `src/index.ts` (lines 36 onward) to + re-export from `@parity/truapi`'s generated `types.ts`. The legacy class + names (`SigningErr`, `CreateTransactionErr`, `AccountConnectionStatus`, + …) need either a thin alias layer or a one-shot rename in downstream + consumers, the generated equivalents use tagged-union shape + (`{ tag, value }`) rather than `new Err.Variant({...})` calls. +- Delete `src/protocol/v1/*.ts` once every type alias above is repointed. + +### Tests + +- `src/transport.spec.ts` covers subscription multiplexing today, that test + will need to move or get retired alongside the multiplexer. +- Add a 56-method byte-equivalence fixture test against a captured + `@novasamatech/host-api@0.7.7` baseline. diff --git a/packages/host-api/package.json b/packages/host-api/package.json index 53418e2b..919cbacb 100644 --- a/packages/host-api/package.json +++ b/packages/host-api/package.json @@ -23,6 +23,7 @@ ], "dependencies": { "@novasamatech/scale": "0.7.8", + "@parity/truapi": "file:../../../truapi/.claude/worktrees/issue-54-truapi-host/js/packages/truapi", "nanoevents": "9.1.0", "nanoid": "5.1.9", "neverthrow": "^8.2.0", diff --git a/packages/host-api/src/truapi-transport.ts b/packages/host-api/src/truapi-transport.ts new file mode 100644 index 00000000..89232ed4 --- /dev/null +++ b/packages/host-api/src/truapi-transport.ts @@ -0,0 +1,110 @@ +/** + * Facade transport built on `@parity/truapi`. + * + * Replaces the bespoke request correlation, subscription router, and wire + * frame envelope that `transport.ts` currently owns with the equivalent + * primitives from `@parity/truapi`. The public surface intentionally mirrors + * `Transport` from `./types.ts` so consumers of `createHostApi` and the + * factories in `host-container` can switch over without churn. + * + * What this file does NOT do, by design: + * - Subscription multiplexing or dedup. `@parity/truapi` exposes 1:1 wire + * subscriptions, products that need fan-out layer their own. + * - Inbound handler registration (`handleRequest`, `handleSubscription`). + * Hosts should use `@parity/truapi-host`'s typed dispatcher instead, and + * `host-container`'s `createContainer` is being migrated onto it in + * `truapi-container.ts`. + * + * Status: directional. Active handshake retry, connection-status events, and + * the legacy `postMessage`/`listenMessages` low-level helpers still need to + * be re-implemented on top of this. See `MIGRATION.md` for the punch list. + */ + +import type { Provider as TruapiProvider, TrUApiTransport } from '@parity/truapi'; +import { createTransport as createTruapiTransport } from '@parity/truapi'; +import { createNanoEvents } from 'nanoevents'; + +import type { Provider } from './provider.js'; +import type { ConnectionStatus, Logger } from './types.js'; + +/** + * Adapt a legacy `Provider` (postMessage/listenMessages over wire-frame + * codec objects) to the raw byte-level `Provider` shape `@parity/truapi` + * expects. + * + * The legacy provider already speaks SCALE-encoded wire frames, so this is + * a pass-through, message-codec wrapping and unwrapping is now owned by + * `@parity/truapi`'s `encodeWireMessage`/`decodeWireMessage`. + */ +function adaptProvider(provider: Provider): TruapiProvider { + return { + postMessage(bytes) { + // The legacy `Provider` postMessage accepts already-encoded bytes; the + // wrapping `MessageProvider.postMessage(message)` indirection that + // `transport.ts` adds to encode the wire-frame envelope is now + // performed by `@parity/truapi.createTransport`. + provider.postMessage(bytes); + }, + subscribe(callback) { + const unsub = provider.subscribe(callback); + return () => unsub(); + }, + dispose() { + provider.dispose(); + }, + }; +} + +export interface TruapiTransportOptions { + logger?: Logger; +} + +export interface TruapiTransport { + /** + * Underlying `@parity/truapi` transport. Generated client modules accept + * this directly; legacy `host-api` consumers wrap it via the facade + * methods below. + **/ + readonly inner: TrUApiTransport; + + /** + * Subscribe to connection-status changes. Mirrors the legacy + * `onConnectionStatusChange` surface. + **/ + onConnectionStatusChange(callback: (status: ConnectionStatus) => void): VoidFunction; + + /** + * Tear down the transport and detach provider listeners. + **/ + destroy(): void; +} + +/** + * Build a `@parity/truapi` transport bound to the supplied legacy + * `Provider`. Generated client modules (from `@parity/truapi`) accept the + * returned `inner` transport directly. + */ +export function createTruapiTransportFacade( + provider: Provider, + _options: TruapiTransportOptions = {}, +): TruapiTransport { + const status = createNanoEvents<{ + change: (status: ConnectionStatus) => void; + }>(); + let currentStatus: ConnectionStatus = 'connecting'; + + const inner = createTruapiTransport(adaptProvider(provider)); + + return { + inner, + onConnectionStatusChange(callback) { + callback(currentStatus); + return status.on('change', callback); + }, + destroy() { + inner.dispose(); + currentStatus = 'disconnected'; + status.emit('change', currentStatus); + }, + }; +} diff --git a/packages/host-container/MIGRATION.md b/packages/host-container/MIGRATION.md new file mode 100644 index 00000000..5d1b8a9e --- /dev/null +++ b/packages/host-container/MIGRATION.md @@ -0,0 +1,54 @@ +# host-container migration to `@parity/truapi-host` + +This package is being reworked into a thin compatibility facade over +[`@parity/truapi-host`][1]. The Rust trait surface in `paritytech/truapi` +drives both the wire ids and the typed handler interfaces emitted by the +generator. + +The work tracking issue is [`paritytech/truapi#54`][issue]. + +[1]: https://github.com/paritytech/truapi/tree/main/js/packages/truapi-host +[issue]: https://github.com/paritytech/truapi/issues/54 + +## Status + +### Done + +- `@parity/truapi` and `@parity/truapi-host` added to `package.json` + dependencies (currently `file:` links to the local truapi worktree). +- `src/truapi-container.ts`: directional facade that creates a + `@parity/truapi-host` server bound to a `Provider`. Default handlers + reject with "Not Implemented" until each existing `handleXxx` slot is + lifted across. + +### Not done + +- `src/createContainer.ts` still owns: + - 40+ `handleXxx(handler)` slots (the public surface). + - `handleV1Request` / `handleV1Subscription` wrappers around versioned + payload envelopes. + - `makeNotImplementedSlot` / `makePermissionGatedRequestSlot` / + `makeDevicePermissionGatedRequestSlot`, all of which produce + method-specific `Unknown { reason: NOT_IMPLEMENTED }` defaults. + - `handleChainConnection()` (lines ~759–1099): polkadot-api bridge. + This stays inside this package, only its registration call should + change to forward to `@parity/truapi-host`'s typed handlers. +- Replace each `handleXxx` slot's update mechanism with a call into + `@parity/truapi-host`'s typed registration. The slot can remain as the + public API; under the hood it should mutate the handler reference that + `createTrUApiServer(...)` is dispatching against. +- Replace `handleV1Request` / `handleV1Subscription` with the typed + handler signatures from `@parity/truapi-host`'s generated `server.ts`. + The versioned envelope wrap/unwrap is now performed by the generator, + not by hand. +- Drop the dependency on `@novasamatech/host-api` once `host-api` itself + drops its inbound `handleRequest` / `handleSubscription` surface (see + `packages/host-api/MIGRATION.md`). + +### Tests + +- `src/chainConnectionManager.spec.ts` is unchanged scope, must continue + to pass. +- Add an integration test that wires a `@parity/truapi` client and a + `@parity/truapi-host` server through a `MessageChannel` and exercises + every wire method via the container's compat shims. diff --git a/packages/host-container/package.json b/packages/host-container/package.json index 3ce29ffb..fc11ccb5 100644 --- a/packages/host-container/package.json +++ b/packages/host-container/package.json @@ -29,6 +29,8 @@ "polkadot-api": ">=2", "@polkadot-api/substrate-client": "^0.7.0", "@novasamatech/host-api": "0.7.8", + "@parity/truapi": "file:../../../truapi/.claude/worktrees/issue-54-truapi-host/js/packages/truapi", + "@parity/truapi-host": "file:../../../truapi/.claude/worktrees/issue-54-truapi-host/js/packages/truapi-host", "nanoid": "5.1.9", "neverthrow": "^8.2.0" }, diff --git a/packages/host-container/src/truapi-container.ts b/packages/host-container/src/truapi-container.ts new file mode 100644 index 00000000..4f069f3a --- /dev/null +++ b/packages/host-container/src/truapi-container.ts @@ -0,0 +1,81 @@ +/** + * Facade host container built on `@parity/truapi-host`. + * + * Replaces the bespoke per-method dispatch, "Not Implemented" fallbacks, and + * `handleV1Request` / `handleV1Subscription` wrappers in `createContainer.ts` + * with the typed handler registration produced by `@parity/truapi-host`'s + * generator. + * + * The new public surface is structured-by-service, mirroring the Rust trait + * surface in `truapi`. Hosts that consume `createContainer()` today (with + * `handleXxx` slots) keep working through compat shims in + * `createContainer.ts`, which forward to `createTruapiContainer()` under the + * hood. + * + * Status: directional. The handlers map below names the same wire methods + * `createContainer.ts` registers today, but each handler is a stub that + * throws "Not Implemented". The migration plan is to lift each existing + * `handleXxx` slot's behaviour into the matching typed handler, then delete + * `createContainer.ts`'s registration scaffolding. + */ + +import type { Provider as TruapiProvider } from '@parity/truapi'; +import type { TrUApiHostHandlers, TrUApiHostServer } from '@parity/truapi-host'; +import { createTrUApiServer } from '@parity/truapi-host'; + +/** + * Build a stub handler that rejects with a "Not Implemented" error. The + * legacy container produces method-specific error variants for the wire, + * the migration TODO is to replace these stubs with handlers that produce + * the corresponding typed responses. + */ +function unimplemented(method: string) { + return async () => { + throw new Error(`@parity/truapi-host handler not yet wired: ${method}`); + }; +} + +/** + * Stand-in until per-method handlers are lifted across from + * `createContainer.ts`. Replace each entry as the migration progresses. + */ +function defaultHandlers(): TrUApiHostHandlers { + const proxy = new Proxy( + {}, + { + get(_, prop) { + if (prop === 'then') return undefined; + return unimplemented(String(prop)); + }, + }, + ); + return { + account: proxy as TrUApiHostHandlers['account'], + chain: proxy as TrUApiHostHandlers['chain'], + chat: proxy as TrUApiHostHandlers['chat'], + entropy: proxy as TrUApiHostHandlers['entropy'], + jsonRpc: proxy as TrUApiHostHandlers['jsonRpc'], + localStorage: proxy as TrUApiHostHandlers['localStorage'], + payment: proxy as TrUApiHostHandlers['payment'], + permissions: proxy as TrUApiHostHandlers['permissions'], + preimage: proxy as TrUApiHostHandlers['preimage'], + resourceAllocation: proxy as TrUApiHostHandlers['resourceAllocation'], + signing: proxy as TrUApiHostHandlers['signing'], + statementStore: proxy as TrUApiHostHandlers['statementStore'], + system: proxy as TrUApiHostHandlers['system'], + theme: proxy as TrUApiHostHandlers['theme'], + }; +} + +/** + * Attach a host server built on `@parity/truapi-host` to a `Provider`. + * Returns the underlying server handle, callers dispose it when the host + * tears down. + */ +export function createTruapiContainer( + provider: TruapiProvider, + handlers: Partial = {}, +): TrUApiHostServer { + const merged = { ...defaultHandlers(), ...handlers } as TrUApiHostHandlers; + return createTrUApiServer(provider, merged); +} From 22b2edb5891d51f0a011ae4e00b38a3850938e37 Mon Sep 17 00:00:00 2001 From: pgherveou Date: Thu, 14 May 2026 18:20:04 +0100 Subject: [PATCH 2/2] refactor(host-container): drop throwing stubs from truapi facade `@parity/truapi-host` now requires handlers to express every outcome as a typed return value, including unsupported wire versions and missing implementations, and exposes `ObservableLike` as the subscription handler return type (the previous `SubscriptionSink` parameter shape is gone). The `defaultHandlers()` Proxy in this file produced throwing stubs that satisfied the type system via `any` but would have broken the runtime contract: throwing out of a request handler hangs the client, and a throwing subscription handler doesn't return an `ObservableLike` for the dispatcher to subscribe to. Drop the defaults entirely and require the caller to supply a full `TrUApiHostHandlers`. The TypeScript compiler now flags any missing service at the call site, which is the right enforcement point for a migration that lifts each `handleXxx` slot across one at a time. --- packages/host-container/MIGRATION.md | 10 +-- .../host-container/src/truapi-container.ts | 69 ++++--------------- 2 files changed, 18 insertions(+), 61 deletions(-) diff --git a/packages/host-container/MIGRATION.md b/packages/host-container/MIGRATION.md index 5d1b8a9e..2b09f45c 100644 --- a/packages/host-container/MIGRATION.md +++ b/packages/host-container/MIGRATION.md @@ -16,10 +16,12 @@ The work tracking issue is [`paritytech/truapi#54`][issue]. - `@parity/truapi` and `@parity/truapi-host` added to `package.json` dependencies (currently `file:` links to the local truapi worktree). -- `src/truapi-container.ts`: directional facade that creates a - `@parity/truapi-host` server bound to a `Provider`. Default handlers - reject with "Not Implemented" until each existing `handleXxx` slot is - lifted across. +- `src/truapi-container.ts`: directional facade that forwards a fully + populated `TrUApiHostHandlers` to `createTrUApiServer`. The TypeScript + compiler enforces completeness; there are no throwing "Not + Implemented" stubs, the dispatcher contract requires handlers to + express every outcome (including missing implementations) as a typed + return rather than an exception. ### Not done diff --git a/packages/host-container/src/truapi-container.ts b/packages/host-container/src/truapi-container.ts index 4f069f3a..2aed1a86 100644 --- a/packages/host-container/src/truapi-container.ts +++ b/packages/host-container/src/truapi-container.ts @@ -12,70 +12,25 @@ * `createContainer.ts`, which forward to `createTruapiContainer()` under the * hood. * - * Status: directional. The handlers map below names the same wire methods - * `createContainer.ts` registers today, but each handler is a stub that - * throws "Not Implemented". The migration plan is to lift each existing - * `handleXxx` slot's behaviour into the matching typed handler, then delete - * `createContainer.ts`'s registration scaffolding. + * Status: directional. The generated `TrUApiHostHandlers` interface forces + * the caller to supply every service handler (handlers must not throw, per + * the `@parity/truapi-host` contract; every outcome including unsupported + * versions and missing implementations is expressed as a typed return). + * Lifting each existing `handleXxx` slot's behaviour into the matching typed + * handler is the bulk of the remaining migration. */ import type { Provider as TruapiProvider } from '@parity/truapi'; import type { TrUApiHostHandlers, TrUApiHostServer } from '@parity/truapi-host'; import { createTrUApiServer } from '@parity/truapi-host'; -/** - * Build a stub handler that rejects with a "Not Implemented" error. The - * legacy container produces method-specific error variants for the wire, - * the migration TODO is to replace these stubs with handlers that produce - * the corresponding typed responses. - */ -function unimplemented(method: string) { - return async () => { - throw new Error(`@parity/truapi-host handler not yet wired: ${method}`); - }; -} - -/** - * Stand-in until per-method handlers are lifted across from - * `createContainer.ts`. Replace each entry as the migration progresses. - */ -function defaultHandlers(): TrUApiHostHandlers { - const proxy = new Proxy( - {}, - { - get(_, prop) { - if (prop === 'then') return undefined; - return unimplemented(String(prop)); - }, - }, - ); - return { - account: proxy as TrUApiHostHandlers['account'], - chain: proxy as TrUApiHostHandlers['chain'], - chat: proxy as TrUApiHostHandlers['chat'], - entropy: proxy as TrUApiHostHandlers['entropy'], - jsonRpc: proxy as TrUApiHostHandlers['jsonRpc'], - localStorage: proxy as TrUApiHostHandlers['localStorage'], - payment: proxy as TrUApiHostHandlers['payment'], - permissions: proxy as TrUApiHostHandlers['permissions'], - preimage: proxy as TrUApiHostHandlers['preimage'], - resourceAllocation: proxy as TrUApiHostHandlers['resourceAllocation'], - signing: proxy as TrUApiHostHandlers['signing'], - statementStore: proxy as TrUApiHostHandlers['statementStore'], - system: proxy as TrUApiHostHandlers['system'], - theme: proxy as TrUApiHostHandlers['theme'], - }; -} - /** * Attach a host server built on `@parity/truapi-host` to a `Provider`. - * Returns the underlying server handle, callers dispose it when the host - * tears down. + * + * The caller supplies the full `TrUApiHostHandlers` shape. The TypeScript + * compiler enforces completeness, anything missing is flagged at the call + * site rather than papered over with a stub that would throw at runtime. */ -export function createTruapiContainer( - provider: TruapiProvider, - handlers: Partial = {}, -): TrUApiHostServer { - const merged = { ...defaultHandlers(), ...handlers } as TrUApiHostHandlers; - return createTrUApiServer(provider, merged); +export function createTruapiContainer(provider: TruapiProvider, handlers: TrUApiHostHandlers): TrUApiHostServer { + return createTrUApiServer(provider, handlers); }