From 625d8ae13e7ddc140539777a2c1c64567054d7dc Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Wed, 12 Aug 2026 10:59:21 -0400 Subject: [PATCH] deltic: bump pin to 0.1.0-pre.g078aa15; A10 rename (ComponentException, {kind, value}) Coordinated family migration for deltic amendment A10 (breaking): WitError -> ComponentException (isWitError -> isComponentException) and variant-family discriminants {tag, val} -> {kind, value} (payloadless cases still omit the payload property; case names kebab-case verbatim). Pin moves to the first prerelease whose @deltic/protocol dependency (0.2.0) carries the rename. jco-era surfaces keep jco conventions. Gates: deltic-impl check+test 13/13; driver-ct deltic loopback 37/37; pin gate green --- conformance/driver-ct/deltic/README.md | 2 +- .../driver-ct/deltic/browser/deno.json | 8 +- .../driver-ct/deltic/browser/deno.lock | 38 ++++--- conformance/driver-ct/deltic/deno.json | 12 +-- conformance/driver-ct/deltic/deno.lock | 48 +++++---- conformance/driver-ct/deltic/run.ts | 2 +- conformance/driver-ct/deltic/signaling.ts | 8 +- deltic-impl/README.md | 4 +- deltic-impl/deno.json | 4 +- deltic-impl/deno.lock | 15 ++- deltic-impl/src/types.ts | 26 ++--- deltic-impl/src/webrtc.ts | 102 +++++++++--------- deltic-impl/tests/webrtc_test.ts | 84 +++++++-------- 13 files changed, 188 insertions(+), 165 deletions(-) diff --git a/conformance/driver-ct/deltic/README.md b/conformance/driver-ct/deltic/README.md index 60aa6be..cdaf90f 100644 --- a/conformance/driver-ct/deltic/README.md +++ b/conformance/driver-ct/deltic/README.md @@ -50,7 +50,7 @@ gate (`../../../scripts/check-deltic-pin.sh`): the SAME `@deltic/runtime/embedder` version (the module-identity constraint: deltic's `wasi-shims` imports that specifier by bare name internally, so every config resolving it must agree, or the embedder - module loads twice and `instanceof WitError` stops holding across + module loads twice and `instanceof ComponentException` stops holding across the boundary). - [`browser/deno.json`](browser/deno.json) — the SAME pins again, with the npm WebRTC backends stubbed out (never executed in a page). diff --git a/conformance/driver-ct/deltic/browser/deno.json b/conformance/driver-ct/deltic/browser/deno.json index dd1036e..75ebb90 100644 --- a/conformance/driver-ct/deltic/browser/deno.json +++ b/conformance/driver-ct/deltic/browser/deno.json @@ -2,10 +2,10 @@ "//": "The browser-bundle import map: identical deltic pins to ../deno.json, with the npm WebRTC backends mapped to ./stub.ts (never executed in a page; see stub.ts). deltic ships as exact-pinned JSR prereleases; the pin gate (`just deltic-check`) asserts one version repo-wide.", "minimumDependencyAge": { "age": "P1D", "exclude": ["jsr:@deltic/*"] }, "imports": { - "@deltic/ct-runner": "jsr:@deltic/ct-runner@0.1.0-pre.ga67ee83", - "@deltic/runtime/embedder": "jsr:@deltic/runtime@0.1.0-pre.ga67ee83/embedder", - "@deltic/runtime/shim": "jsr:@deltic/runtime@0.1.0-pre.ga67ee83/shim", - "@deltic/wasi-shims": "jsr:@deltic/wasi-shims@0.1.0-pre.ga67ee83", + "@deltic/ct-runner": "jsr:@deltic/ct-runner@0.1.0-pre.g078aa15", + "@deltic/runtime/embedder": "jsr:@deltic/runtime@0.1.0-pre.g078aa15/embedder", + "@deltic/runtime/shim": "jsr:@deltic/runtime@0.1.0-pre.g078aa15/shim", + "@deltic/wasi-shims": "jsr:@deltic/wasi-shims@0.1.0-pre.g078aa15", "node-datachannel": "./stub.ts", "node-datachannel/polyfill": "./stub.ts", "werift": "./stub.ts" diff --git a/conformance/driver-ct/deltic/browser/deno.lock b/conformance/driver-ct/deltic/browser/deno.lock index c1e189c..0c39843 100644 --- a/conformance/driver-ct/deltic/browser/deno.lock +++ b/conformance/driver-ct/deltic/browser/deno.lock @@ -1,33 +1,41 @@ { "version": "5", "specifiers": { - "jsr:@deltic/ct-runner@0.1.0-pre.ga67ee83": "0.1.0-pre.ga67ee83", - "jsr:@deltic/runtime@0.1.0-pre.ga67ee83": "0.1.0-pre.ga67ee83", - "jsr:@deltic/runtime@~0.1.0-pre.ga67ee83": "0.1.0-pre.ga67ee83", - "jsr:@deltic/wasi-shims@0.1.0-pre.ga67ee83": "0.1.0-pre.ga67ee83" + "jsr:@deltic/ct-runner@0.1.0-pre.g078aa15": "0.1.0-pre.g078aa15", + "jsr:@deltic/protocol@0.2": "0.2.0", + "jsr:@deltic/runtime@0.1.0-pre.g078aa15": "0.1.0-pre.g078aa15", + "jsr:@deltic/runtime@~0.1.0-pre.g078aa15": "0.1.0-pre.g078aa15", + "jsr:@deltic/wasi-shims@0.1.0-pre.g078aa15": "0.1.0-pre.g078aa15" }, "jsr": { - "@deltic/ct-runner@0.1.0-pre.ga67ee83": { - "integrity": "d78e96eba54e53dabe7c1de5a67782aad755362824d741913ab4d36941ebfbcc", + "@deltic/ct-runner@0.1.0-pre.g078aa15": { + "integrity": "d777a53e6be337d999a80de030628e1226ae7649ca62e49dd444e58d0973e6be", "dependencies": [ - "jsr:@deltic/runtime@~0.1.0-pre.ga67ee83" + "jsr:@deltic/runtime@~0.1.0-pre.g078aa15" ] }, - "@deltic/runtime@0.1.0-pre.ga67ee83": { - "integrity": "2a2b0949031747a2340dc186299e654ea9f361ae39ac6bd633019940682db4ed" + "@deltic/protocol@0.2.0": { + "integrity": "028be6a3623c5e910598aa7a199209b85e8931ae484ac7f6638d610ddb0e19fa" }, - "@deltic/wasi-shims@0.1.0-pre.ga67ee83": { - "integrity": "a5754e65d50873e675695a51279989e22b3771e31f16a84a09feb15905c574a7", + "@deltic/runtime@0.1.0-pre.g078aa15": { + "integrity": "5e64f8dc6d32190ecd394d6ad9881eb0fa97b6598b1e7f2ef1d52cff4674287c", "dependencies": [ - "jsr:@deltic/runtime@~0.1.0-pre.ga67ee83" + "jsr:@deltic/protocol" + ] + }, + "@deltic/wasi-shims@0.1.0-pre.g078aa15": { + "integrity": "fba2081f804eaaccf95e31cdd634fd85a2d724f6be7f62578ddc26aef03524a1", + "dependencies": [ + "jsr:@deltic/protocol", + "jsr:@deltic/runtime@~0.1.0-pre.g078aa15" ] } }, "workspace": { "dependencies": [ - "jsr:@deltic/ct-runner@0.1.0-pre.ga67ee83", - "jsr:@deltic/runtime@0.1.0-pre.ga67ee83", - "jsr:@deltic/wasi-shims@0.1.0-pre.ga67ee83" + "jsr:@deltic/ct-runner@0.1.0-pre.g078aa15", + "jsr:@deltic/runtime@0.1.0-pre.g078aa15", + "jsr:@deltic/wasi-shims@0.1.0-pre.g078aa15" ] } } diff --git a/conformance/driver-ct/deltic/deno.json b/conformance/driver-ct/deltic/deno.json index 2cb2124..b6d29cc 100644 --- a/conformance/driver-ct/deltic/deno.json +++ b/conformance/driver-ct/deltic/deno.json @@ -1,13 +1,13 @@ { - "//": "MODULE-IDENTITY CONSTRAINT: deltic's wasi-shims module imports @deltic/runtime/embedder by bare specifier internally. Every config in this repo (this file AND deltic-impl/deno.json AND browser/deno.json) must map that specifier to the IDENTICAL exact-pinned JSR version, or the embedder module loads twice and `instanceof WitError` stops holding across the module boundary. deltic ships as exact-pinned JSR prereleases: the hash names the upstream commit; the lock carries integrity and --frozen enforces it; the pin gate (`just deltic-check`) asserts one version repo-wide. The npm mappings mirror deltic-impl/deno.json: the host module's bare `node-datachannel/polyfill` / `werift` specifiers resolve against THIS config when run.ts is the entry module.", + "//": "MODULE-IDENTITY CONSTRAINT: deltic's wasi-shims module imports @deltic/runtime/embedder by bare specifier internally. Every config in this repo (this file AND deltic-impl/deno.json AND browser/deno.json) must map that specifier to the IDENTICAL exact-pinned JSR version, or the embedder module loads twice and `instanceof ComponentException` stops holding across the module boundary. deltic ships as exact-pinned JSR prereleases: the hash names the upstream commit; the lock carries integrity and --frozen enforces it; the pin gate (`just deltic-check`) asserts one version repo-wide. The npm mappings mirror deltic-impl/deno.json: the host module's bare `node-datachannel/polyfill` / `werift` specifiers resolve against THIS config when run.ts is the entry module.", "nodeModulesDir": "auto", "minimumDependencyAge": { "age": "P1D", "exclude": ["jsr:@deltic/*"] }, "imports": { - "@deltic/ct-runner": "jsr:@deltic/ct-runner@0.1.0-pre.ga67ee83", - "@deltic/runtime/embedder": "jsr:@deltic/runtime@0.1.0-pre.ga67ee83/embedder", - "@deltic/runtime/shim": "jsr:@deltic/runtime@0.1.0-pre.ga67ee83/shim", - "@deltic/wasi-shims": "jsr:@deltic/wasi-shims@0.1.0-pre.ga67ee83", - "@deltic/translator": "jsr:@deltic/translator@0.1.0-pre.ga67ee83", + "@deltic/ct-runner": "jsr:@deltic/ct-runner@0.1.0-pre.g078aa15", + "@deltic/runtime/embedder": "jsr:@deltic/runtime@0.1.0-pre.g078aa15/embedder", + "@deltic/runtime/shim": "jsr:@deltic/runtime@0.1.0-pre.g078aa15/shim", + "@deltic/wasi-shims": "jsr:@deltic/wasi-shims@0.1.0-pre.g078aa15", + "@deltic/translator": "jsr:@deltic/translator@0.1.0-pre.g078aa15", "node-datachannel": "npm:node-datachannel@0.32.3", "node-datachannel/polyfill": "npm:node-datachannel@0.32.3/polyfill", "werift": "npm:werift@0.22.2" diff --git a/conformance/driver-ct/deltic/deno.lock b/conformance/driver-ct/deltic/deno.lock index 00bc2bb..7f733e3 100644 --- a/conformance/driver-ct/deltic/deno.lock +++ b/conformance/driver-ct/deltic/deno.lock @@ -1,35 +1,43 @@ { "version": "5", "specifiers": { - "jsr:@deltic/ct-runner@0.1.0-pre.ga67ee83": "0.1.0-pre.ga67ee83", - "jsr:@deltic/runtime@0.1.0-pre.ga67ee83": "0.1.0-pre.ga67ee83", - "jsr:@deltic/runtime@~0.1.0-pre.ga67ee83": "0.1.0-pre.ga67ee83", - "jsr:@deltic/translator@0.1.0-pre.ga67ee83": "0.1.0-pre.ga67ee83", - "jsr:@deltic/wasi-shims@0.1.0-pre.ga67ee83": "0.1.0-pre.ga67ee83", + "jsr:@deltic/ct-runner@0.1.0-pre.g078aa15": "0.1.0-pre.g078aa15", + "jsr:@deltic/protocol@0.2": "0.2.0", + "jsr:@deltic/runtime@0.1.0-pre.g078aa15": "0.1.0-pre.g078aa15", + "jsr:@deltic/runtime@~0.1.0-pre.g078aa15": "0.1.0-pre.g078aa15", + "jsr:@deltic/translator@0.1.0-pre.g078aa15": "0.1.0-pre.g078aa15", + "jsr:@deltic/wasi-shims@0.1.0-pre.g078aa15": "0.1.0-pre.g078aa15", "npm:node-datachannel@0.32.3": "0.32.3", "npm:playwright-core@^1.61.1": "1.62.1", "npm:werift@0.22.2": "0.22.2" }, "jsr": { - "@deltic/ct-runner@0.1.0-pre.ga67ee83": { - "integrity": "d78e96eba54e53dabe7c1de5a67782aad755362824d741913ab4d36941ebfbcc", + "@deltic/ct-runner@0.1.0-pre.g078aa15": { + "integrity": "d777a53e6be337d999a80de030628e1226ae7649ca62e49dd444e58d0973e6be", "dependencies": [ - "jsr:@deltic/runtime@~0.1.0-pre.ga67ee83" + "jsr:@deltic/runtime@~0.1.0-pre.g078aa15" ] }, - "@deltic/runtime@0.1.0-pre.ga67ee83": { - "integrity": "2a2b0949031747a2340dc186299e654ea9f361ae39ac6bd633019940682db4ed" + "@deltic/protocol@0.2.0": { + "integrity": "028be6a3623c5e910598aa7a199209b85e8931ae484ac7f6638d610ddb0e19fa" }, - "@deltic/translator@0.1.0-pre.ga67ee83": { - "integrity": "e4f1bb219e56b62262b0c9665e3f6646aa13883b2cfbb9d6306f69d19974d412", + "@deltic/runtime@0.1.0-pre.g078aa15": { + "integrity": "5e64f8dc6d32190ecd394d6ad9881eb0fa97b6598b1e7f2ef1d52cff4674287c", "dependencies": [ - "jsr:@deltic/runtime@~0.1.0-pre.ga67ee83" + "jsr:@deltic/protocol" ] }, - "@deltic/wasi-shims@0.1.0-pre.ga67ee83": { - "integrity": "a5754e65d50873e675695a51279989e22b3771e31f16a84a09feb15905c574a7", + "@deltic/translator@0.1.0-pre.g078aa15": { + "integrity": "7125f99ac46af4a69f1c432def2b7ea4be63509c3a7ebae83537357615ec33ef", "dependencies": [ - "jsr:@deltic/runtime@~0.1.0-pre.ga67ee83" + "jsr:@deltic/runtime@~0.1.0-pre.g078aa15" + ] + }, + "@deltic/wasi-shims@0.1.0-pre.g078aa15": { + "integrity": "fba2081f804eaaccf95e31cdd634fd85a2d724f6be7f62578ddc26aef03524a1", + "dependencies": [ + "jsr:@deltic/protocol", + "jsr:@deltic/runtime@~0.1.0-pre.g078aa15" ] } }, @@ -605,10 +613,10 @@ }, "workspace": { "dependencies": [ - "jsr:@deltic/ct-runner@0.1.0-pre.ga67ee83", - "jsr:@deltic/runtime@0.1.0-pre.ga67ee83", - "jsr:@deltic/translator@0.1.0-pre.ga67ee83", - "jsr:@deltic/wasi-shims@0.1.0-pre.ga67ee83", + "jsr:@deltic/ct-runner@0.1.0-pre.g078aa15", + "jsr:@deltic/runtime@0.1.0-pre.g078aa15", + "jsr:@deltic/translator@0.1.0-pre.g078aa15", + "jsr:@deltic/wasi-shims@0.1.0-pre.g078aa15", "npm:node-datachannel@0.32.3", "npm:werift@0.22.2" ], diff --git a/conformance/driver-ct/deltic/run.ts b/conformance/driver-ct/deltic/run.ts index 4c10ce3..05ddcd3 100644 --- a/conformance/driver-ct/deltic/run.ts +++ b/conformance/driver-ct/deltic/run.ts @@ -23,7 +23,7 @@ // `@deltic/runtime/embedder` by bare specifier internally; this leg's // `deno.json` AND `deltic-impl/deno.json` (AND `browser/deno.json`) must // map that specifier to the IDENTICAL pinned JSR version, or the embedder -// module loads twice and `instanceof WitError` stops holding across the +// module loads twice and `instanceof ComponentException` stops holding across the // module boundary. import { Translator } from "@deltic/runtime/shim"; diff --git a/conformance/driver-ct/deltic/signaling.ts b/conformance/driver-ct/deltic/signaling.ts index 7109531..345bf0e 100644 --- a/conformance/driver-ct/deltic/signaling.ts +++ b/conformance/driver-ct/deltic/signaling.ts @@ -3,14 +3,14 @@ // `conformance-signalingd` (see `conformance/signaling/PROTOCOL.md`). The // deltic analogue of the retired jco `signaling.js` (see git history) — same endpoints, same long-poll // discipline; only the boundary conventions differ (`throw new -// WitError({ tag, val })` rather than a bare `{ tag, val }` payload, per +// ComponentException({ kind, value })` rather than a bare `{ tag, val }` payload, per // deltic's contracts/embedder-api.md §"Error model"). // // Blob payloads are opaque here; the conformance guest owns the encoding. // Failures are thrown as the WIT `error` variant's `other` case, which the // runtime lifts into the `result<_, error>` the mailbox interface declares. -import { WitError } from "@deltic/runtime/embedder"; +import { ComponentException } from "@deltic/runtime/embedder"; /** The mailbox interface's WIT id (conformance/wit/deps/conformance-signaling). */ export const MAILBOX_INTERFACE = "conformance:signaling/mailbox@0.1.0"; @@ -118,8 +118,8 @@ export class Session { } /** Map a host-side mailbox failure to the guest-visible `error.other`. */ -function mailboxError(detail: string): WitError { - return new WitError({ tag: "other", val: `mailbox: ${detail}` }); +function mailboxError(detail: string): ComponentException { + return new ComponentException({ kind: "other", value: `mailbox: ${detail}` }); } /** diff --git a/deltic-impl/README.md b/deltic-impl/README.md index 3288763..c310ea7 100644 --- a/deltic-impl/README.md +++ b/deltic-impl/README.md @@ -5,7 +5,7 @@ port of the browser-first reference host `jco-impl/webrtc.js` **at commit 65bc15b** (retired with the jco legs; `git show 65bc15b:jco-impl/webrtc.js`): the same behavioral reference host, rewritten over deltic's embedder API (typed -`Stream` / `ReadableStream` rather than jco streams, and `WitError` +`Stream` / `ReadableStream` rather than jco streams, and `ComponentException` throws rather than `throw { tag, val }`). It was developed as deltic's own `ports/webrtc` reference-host port and is upstreamed here per [lann/deltic#14](https://github.com/lann/deltic/issues/14); the WIT @@ -44,7 +44,7 @@ byte-identical with the one in [`conformance/driver-ct/deltic/deno.json`](../conformance/driver-ct/deltic/deno.json) (and its `browser/deno.json`): deltic's `wasi-shims` imports that specifier by bare name internally, so two divergent mappings load the -embedder module twice and `instanceof WitError` stops holding across +embedder module twice and `instanceof ComponentException` stops holding across the module boundary. The bump procedure lives in [`conformance/driver-ct/deltic/README.md`](../conformance/driver-ct/deltic/README.md); `just deltic-check` asserts all three configs agree on one version diff --git a/deltic-impl/deno.json b/deltic-impl/deno.json index 99ee36a..de2b5c7 100644 --- a/deltic-impl/deno.json +++ b/deltic-impl/deno.json @@ -2,11 +2,11 @@ "name": "@polymorph/webrtc-datachannels-deltic", "version": "0.0.0", "exports": "./src/webrtc.ts", - "//": "MODULE-IDENTITY CONSTRAINT: deltic's wasi-shims module imports @deltic/runtime/embedder by bare specifier internally. Every config in this repo (this file AND conformance/driver-ct/deltic/deno.json AND conformance/driver-ct/deltic/browser/deno.json) must map that specifier to the IDENTICAL exact-pinned JSR version, or the embedder module loads twice and `instanceof WitError` stops holding across the module boundary. deltic ships as exact-pinned JSR prereleases: the hash in the version names the upstream commit; the lock carries integrity and --frozen enforces it; the pin gate (`just deltic-check`) asserts one version repo-wide.", + "//": "MODULE-IDENTITY CONSTRAINT: deltic's wasi-shims module imports @deltic/runtime/embedder by bare specifier internally. Every config in this repo (this file AND conformance/driver-ct/deltic/deno.json AND conformance/driver-ct/deltic/browser/deno.json) must map that specifier to the IDENTICAL exact-pinned JSR version, or the embedder module loads twice and `instanceof ComponentException` stops holding across the module boundary. deltic ships as exact-pinned JSR prereleases: the hash in the version names the upstream commit; the lock carries integrity and --frozen enforces it; the pin gate (`just deltic-check`) asserts one version repo-wide.", "nodeModulesDir": "auto", "minimumDependencyAge": { "age": "P1D", "exclude": ["jsr:@deltic/*"] }, "imports": { - "@deltic/runtime/embedder": "jsr:@deltic/runtime@0.1.0-pre.ga67ee83/embedder", + "@deltic/runtime/embedder": "jsr:@deltic/runtime@0.1.0-pre.g078aa15/embedder", "node-datachannel": "npm:node-datachannel@0.32.3", "node-datachannel/polyfill": "npm:node-datachannel@0.32.3/polyfill", "werift": "npm:werift@0.22.2" diff --git a/deltic-impl/deno.lock b/deltic-impl/deno.lock index 364d3f0..853dd91 100644 --- a/deltic-impl/deno.lock +++ b/deltic-impl/deno.lock @@ -1,15 +1,22 @@ { "version": "5", "specifiers": { - "jsr:@deltic/runtime@0.1.0-pre.ga67ee83": "0.1.0-pre.ga67ee83", + "jsr:@deltic/protocol@0.2": "0.2.0", + "jsr:@deltic/runtime@0.1.0-pre.g078aa15": "0.1.0-pre.g078aa15", "jsr:@std/assert@1": "1.0.19", "jsr:@std/internal@^1.0.12": "1.0.14", "npm:node-datachannel@0.32.3": "0.32.3", "npm:werift@0.22.2": "0.22.2" }, "jsr": { - "@deltic/runtime@0.1.0-pre.ga67ee83": { - "integrity": "2a2b0949031747a2340dc186299e654ea9f361ae39ac6bd633019940682db4ed" + "@deltic/protocol@0.2.0": { + "integrity": "028be6a3623c5e910598aa7a199209b85e8931ae484ac7f6638d610ddb0e19fa" + }, + "@deltic/runtime@0.1.0-pre.g078aa15": { + "integrity": "5e64f8dc6d32190ecd394d6ad9881eb0fa97b6598b1e7f2ef1d52cff4674287c", + "dependencies": [ + "jsr:@deltic/protocol" + ] }, "@std/assert@1.0.19": { "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e", @@ -589,7 +596,7 @@ }, "workspace": { "dependencies": [ - "jsr:@deltic/runtime@0.1.0-pre.ga67ee83", + "jsr:@deltic/runtime@0.1.0-pre.g078aa15", "npm:node-datachannel@0.32.3", "npm:werift@0.22.2" ] diff --git a/deltic-impl/src/types.ts b/deltic-impl/src/types.ts index 0c1a6a1..7034ac7 100644 --- a/deltic-impl/src/types.ts +++ b/deltic-impl/src/types.ts @@ -3,32 +3,32 @@ // // Authority: wit/webrtc.wit `interface types` (polymorph-webrtc-datachannels, // read-only reference). Enums are kebab-case string literal unions; variants -// are `{ tag, val? }`; records are plain camelCase objects. +// are `{ kind, value? }`; records are plain camelCase objects. import type { Stream, StreamSource } from "@deltic/runtime/embedder"; // --- error ----------------------------------------------------------------- export type WebrtcError = - | { tag: "closed" } - | { tag: "timed-out" } - | { tag: "invalid-signaling"; val: string } - | { tag: "receiving-via-stream" } - | { tag: "receive-buffer-overflow" } - | { tag: "other"; val: string }; + | { kind: "closed" } + | { kind: "timed-out" } + | { kind: "invalid-signaling"; value: string } + | { kind: "receiving-via-stream" } + | { kind: "receive-buffer-overflow" } + | { kind: "other"; value: string }; // --- message ----------------------------------------------------------------- export type Message = - | { tag: "binary"; val: Uint8Array } - | { tag: "string"; val: string }; + | { kind: "binary"; value: Uint8Array } + | { kind: "string"; value: string }; export const Message = { binary(bytes: Uint8Array): Message { - return { tag: "binary", val: bytes }; + return { kind: "binary", value: bytes }; }, string(text: string): Message { - return { tag: "string", val: text }; + return { kind: "string", value: text }; }, }; @@ -84,8 +84,8 @@ export interface IceCandidate { // --- config-error ------------------------------------------------------------------ export type ConfigError = - | { tag: "not-supported" } - | { tag: "invalid"; val: string }; + | { kind: "not-supported" } + | { kind: "invalid"; value: string }; // --- ice-server -------------------------------------------------------------------- diff --git a/deltic-impl/src/webrtc.ts b/deltic-impl/src/webrtc.ts index f1e5a61..62e68a9 100644 --- a/deltic-impl/src/webrtc.ts +++ b/deltic-impl/src/webrtc.ts @@ -17,14 +17,14 @@ // // the retired jco host | this port // -------------------------------------+------------------------------------ -// `throw { tag, val }` (bare payload) | `throw new WitError({ tag, val })` +// `throw { tag, val }` (bare payload) | `throw new ComponentException({ kind, value })` // jco `Stream` (`read({count})`) | `Stream` / `ReadableStream` // `jco --map` module wiring | `webrtcImports()` record fragment // module-level setters | same setters (see below) // -// - thrown bare `{ tag, val }` payloads become `throw new WitError(payload)` +// - thrown bare `{ kind, value }` payloads become `throw new ComponentException(payload)` // (contracts/embedder-api.md §"Error model" — "Host import with -// result": throw new WitError(payload) for err). +// result": throw new ComponentException(payload) for err). // - jco `Stream`/`ReadableStream` params/results become the runtime's real // `Stream` (consumed, e.g. `send-via-stream`'s guest-provided // messages) / `ReadableStream` (produced, e.g. `receive-via-stream`'s @@ -32,10 +32,10 @@ // a `stream` is expected). Imported from // `@deltic/runtime/embedder` (pinned in this package's `deno.json` to // the exact release URL every deltic-facing module in this repository -// shares), NOT reimplemented locally: `WitError` is a plain branded +// shares), NOT reimplemented locally: `ComponentException` is a plain branded // class with no `Store` involvement, so a local clone would produce a // second class identity and every `throw` from this port would fail -// `instanceof WitError` at a real component boundary — silently +// `instanceof ComponentException` at a real component boundary — silently // becoming an unbranded-throw trap instead of a guest-visible err. // `deno.json` documents the module-identity constraint. // - the inbound buffer bound stays a module-level setter @@ -44,7 +44,7 @@ // per the `data-channel` resource's doc comment), so there is no // guest-facing shape to convert. -import { Stream, type StreamSource, WitError } from "@deltic/runtime/embedder"; +import { Stream, type StreamSource, ComponentException } from "@deltic/runtime/embedder"; import type { ConfigError, ConnectionState, @@ -255,13 +255,13 @@ export class PeerConnectionConfig { setIceServers(servers: IceServer[]): void { for (const server of servers) { if (!server.urls.length) { - throw new WitError({ tag: "invalid", val: "ice-server has no urls" }); + throw new ComponentException({ kind: "invalid", value: "ice-server has no urls" }); } for (const url of server.urls) { if (!/^(stun|stuns|turn|turns):/.test(url)) { - throw new WitError({ - tag: "invalid", - val: `ice-server url ${JSON.stringify(url)} has no stun:/stuns:/turn:/turns: scheme`, + throw new ComponentException({ + kind: "invalid", + value: `ice-server url ${JSON.stringify(url)} has no stun:/stuns:/turn:/turns: scheme`, }); } } @@ -336,13 +336,13 @@ function incomingQueue(channel: { return; } const message: Message = typeof data === "string" - ? { tag: "string", val: data } - : { tag: "binary", val: new Uint8Array(data) }; + ? { kind: "string", value: data } + : { kind: "binary", value: new Uint8Array(data) }; push(message, size); }); const endError = (): WebrtcError => - overflowed ? { tag: "receive-buffer-overflow" } : { tag: "closed" }; + overflowed ? { kind: "receive-buffer-overflow" } : { kind: "closed" }; let endTimer: ReturnType | undefined; const armEnd = () => { if (endTimer !== undefined) clearTimeout(endTimer); @@ -372,12 +372,12 @@ function incomingQueue(channel: { buffered -= size; return Promise.resolve(message); } - if (overflowed) return Promise.reject(new WitError({ tag: "receive-buffer-overflow" })); - if (closed) return Promise.reject(new WitError({ tag: "closed" })); + if (overflowed) return Promise.reject(new ComponentException({ kind: "receive-buffer-overflow" })); + if (closed) return Promise.reject(new ComponentException({ kind: "closed" })); return new Promise((resolve, reject) => { waiters.push({ resolve, - reject: (e) => reject(new WitError(e)), + reject: (e) => reject(new ComponentException(e)), }); }); }, @@ -391,7 +391,7 @@ function incomingQueue(channel: { messages.length = 0; buffered = 0; closed = true; - while (waiters.length) waiters.shift()!.reject({ tag: "closed" }); + while (waiters.length) waiters.shift()!.reject({ kind: "closed" }); }, }; } @@ -430,20 +430,20 @@ export class DataChannel { // `"open"`), unlike a synchronous local latch. Gate on the local flag // first so this port's `close()` is observed synchronously regardless of // backend timing. - if (this.#localClosed) throw new WitError({ tag: "closed" }); + if (this.#localClosed) throw new ComponentException({ kind: "closed" }); await this.#waitOpen(); await this.#waitForDrain(); try { - this.#channel.send(message.val); + this.#channel.send(message.value); } catch { - throw new WitError({ tag: "closed" }); + throw new ComponentException({ kind: "closed" }); } } async receive(): Promise { - if (this.#localClosed) throw new WitError({ tag: "closed" }); + if (this.#localClosed) throw new ComponentException({ kind: "closed" }); if (this.#streamClaimed) { - throw new WitError({ tag: "receiving-via-stream" }); + throw new ComponentException({ kind: "receiving-via-stream" }); } return this.#incoming.next(); } @@ -466,21 +466,21 @@ export class DataChannel { const bytes = await collectByteStream(item.data); if (bytes.length !== item.length) { throw { - tag: "other", - val: `stream-message payload was ${bytes.length} bytes but length declared ${item.length}`, + kind: "other", + value: `stream-message payload was ${bytes.length} bytes but length declared ${item.length}`, } satisfies WebrtcError; } const message: Message = item.kind === "string" - ? { tag: "string", val: new TextDecoder().decode(bytes) } - : { tag: "binary", val: bytes }; + ? { kind: "string", value: new TextDecoder().decode(bytes) } + : { kind: "binary", value: bytes }; await this.send(message); sent += 1n; } } catch (error) { - const payload: WebrtcError = error instanceof WitError + const payload: WebrtcError = error instanceof ComponentException ? (error.payload as WebrtcError) - : (isWebrtcError(error) ? error : { tag: "closed" }); - throw new WitError({ error: payload, sent }); + : (isWebrtcError(error) ? error : { kind: "closed" }); + throw new ComponentException({ error: payload, sent }); } } @@ -492,13 +492,13 @@ export class DataChannel { * expected — the runtime lowers it; this port never drives a `Store`. */ receiveViaStream(): ReadableStream { - if (this.#localClosed) throw new WitError({ tag: "closed" }); + if (this.#localClosed) throw new ComponentException({ kind: "closed" }); if (this.#streamClaimed) { - throw new WitError({ tag: "receiving-via-stream" }); + throw new ComponentException({ kind: "receiving-via-stream" }); } this.#streamClaimed = true; const incoming = this.#incoming; - incoming.rejectWaiters({ tag: "receiving-via-stream" }); + incoming.rejectWaiters({ kind: "receiving-via-stream" }); return new ReadableStream({ async pull(controller) { let message: Message; @@ -510,11 +510,11 @@ export class DataChannel { controller.close(); return; } - const bytes = message.tag === "string" - ? new TextEncoder().encode(message.val) - : message.val; + const bytes = message.kind === "string" + ? new TextEncoder().encode(message.value) + : message.value; controller.enqueue({ - kind: message.tag, + kind: message.kind, length: bytes.length, data: bytesToReadable(bytes) as StreamSource, }); @@ -527,18 +527,18 @@ export class DataChannel { const channel = this.#channel; if (channel.readyState === "open") return Promise.resolve(); if (channel.readyState === "closing" || channel.readyState === "closed") { - return Promise.reject(new WitError({ tag: "closed" })); + return Promise.reject(new ComponentException({ kind: "closed" })); } return new Promise((resolve, reject) => { channel.addEventListener("open", () => resolve(), { once: true }); channel.addEventListener( "close", - () => reject(new WitError({ tag: "closed" })), + () => reject(new ComponentException({ kind: "closed" })), { once: true }, ); channel.addEventListener( "error", - () => reject(new WitError({ tag: "closed" })), + () => reject(new ComponentException({ kind: "closed" })), { once: true }, ); }); @@ -636,7 +636,7 @@ export class DataChannel { } function isWebrtcError(v: unknown): v is WebrtcError { - return typeof v === "object" && v !== null && typeof (v as { tag?: unknown }).tag === "string"; + return typeof v === "object" && v !== null && typeof (v as { kind?: unknown }).kind === "string"; } // --- peer-connection ------------------------------------------------------------ @@ -773,7 +773,7 @@ export class PeerConnection { this.#closed || this.#failed || this.#isFailedNow() || this.#pc.connectionState === "closed" ) { - throw new WitError({ tag: "closed" }); + throw new ComponentException({ kind: "closed" }); } } @@ -798,7 +798,7 @@ export class PeerConnection { this.#ownedWrappers.add(wrapper); return wrapper; } catch (err) { - throw new WitError({ tag: "other", val: String(err) }); + throw new ComponentException({ kind: "other", value: String(err) }); } } @@ -823,7 +823,7 @@ export class PeerConnection { const offer = await this.#pc.createOffer(); return { kind: "offer", sdp: offer.sdp }; } catch (err) { - throw new WitError({ tag: "other", val: String(err) }); + throw new ComponentException({ kind: "other", value: String(err) }); } } @@ -833,7 +833,7 @@ export class PeerConnection { const answer = await this.#pc.createAnswer(); return { kind: "answer", sdp: answer.sdp }; } catch (err) { - throw new WitError({ tag: "other", val: String(err) }); + throw new ComponentException({ kind: "other", value: String(err) }); } } @@ -842,7 +842,7 @@ export class PeerConnection { try { await this.#pc.setLocalDescription({ type: description.kind, sdp: description.sdp }); } catch (err) { - throw new WitError({ tag: "invalid-signaling", val: String(err) }); + throw new ComponentException({ kind: "invalid-signaling", value: String(err) }); } } @@ -851,7 +851,7 @@ export class PeerConnection { try { await this.#pc.setRemoteDescription({ type: description.kind, sdp: description.sdp }); } catch (err) { - throw new WitError({ tag: "invalid-signaling", val: String(err) }); + throw new ComponentException({ kind: "invalid-signaling", value: String(err) }); } } @@ -876,7 +876,7 @@ export class PeerConnection { sdpMLineIndex: candidate.sdpMlineIndex ?? null, }); } catch (err) { - throw new WitError({ tag: "invalid-signaling", val: String(err) }); + throw new ComponentException({ kind: "invalid-signaling", value: String(err) }); } } @@ -910,11 +910,11 @@ export class PeerConnection { if (this.#isConnectedNow()) this.#everConnected = true; if (this.#everConnected) return; - if (this.#closed || isFailed()) throw new WitError({ tag: "closed" }); + if (this.#closed || isFailed()) throw new ComponentException({ kind: "closed" }); await new Promise((resolve, reject) => { const timer = setTimeout(() => { cleanup(); - reject(new WitError({ tag: "timed-out" })); + reject(new ComponentException({ kind: "timed-out" })); }, CONNECT_TIMEOUT_MS); const check = () => { if (this.#isConnectedNow()) { @@ -923,12 +923,12 @@ export class PeerConnection { resolve(); } else if (isFailed()) { cleanup(); - reject(new WitError({ tag: "closed" })); + reject(new ComponentException({ kind: "closed" })); } }; const onClose = () => { cleanup(); - reject(new WitError({ tag: "closed" })); + reject(new ComponentException({ kind: "closed" })); }; const cleanup = () => { clearTimeout(timer); diff --git a/deltic-impl/tests/webrtc_test.ts b/deltic-impl/tests/webrtc_test.ts index 2fad338..714666a 100644 --- a/deltic-impl/tests/webrtc_test.ts +++ b/deltic-impl/tests/webrtc_test.ts @@ -21,7 +21,7 @@ import { resetMaxInboundBufferBytes, setMaxInboundBufferBytes, } from "../src/webrtc.ts"; -import { WitError } from "@deltic/runtime/embedder"; +import { ComponentException } from "@deltic/runtime/embedder"; import type { IceCandidate, Message, WebrtcError } from "../src/types.ts"; const NO_SANITIZE = { sanitizeResources: false, sanitizeOps: false }; @@ -96,13 +96,13 @@ Deno.test("loopback: text echo both directions", NO_SANITIZE, async () => { const chA = a.createDataChannel(options); const chB = await firstIncoming(b); - await chA.send({ tag: "string", val: "hello from a" }); + await chA.send({ kind: "string", value: "hello from a" }); const gotAtB = await chB.receive(); - assertEquals(gotAtB, { tag: "string", val: "hello from a" }); + assertEquals(gotAtB, { kind: "string", value: "hello from a" }); - await chB.send({ tag: "string", val: "hello from b" }); + await chB.send({ kind: "string", value: "hello from b" }); const gotAtA = await chA.receive(); - assertEquals(gotAtA, { tag: "string", val: "hello from b" }); + assertEquals(gotAtA, { kind: "string", value: "hello from b" }); } finally { a.close(); b.close(); @@ -117,13 +117,13 @@ Deno.test("loopback: binary echo + message-boundary preservation", NO_SANITIZE, const msg1 = new Uint8Array([1, 2, 3]); const msg2 = new Uint8Array([4, 5]); - await chA.send({ tag: "binary", val: msg1 }); - await chA.send({ tag: "binary", val: msg2 }); + await chA.send({ kind: "binary", value: msg1 }); + await chA.send({ kind: "binary", value: msg2 }); const got1 = await chB.receive(); const got2 = await chB.receive(); - assertEquals(got1, { tag: "binary", val: msg1 }); - assertEquals(got2, { tag: "binary", val: msg2 }); + assertEquals(got1, { kind: "binary", value: msg1 }); + assertEquals(got2, { kind: "binary", value: msg2 }); } finally { a.close(); b.close(); @@ -141,8 +141,8 @@ Deno.test("loopback: unordered/maxRetransmits options accepted", NO_SANITIZE, as const chA = a.createDataChannel(options); const chB = await firstIncoming(b); - await chA.send({ tag: "string", val: "unordered ok" }); - assertEquals(await chB.receive(), { tag: "string", val: "unordered ok" }); + await chA.send({ kind: "string", value: "unordered ok" }); + assertEquals(await chB.receive(), { kind: "string", value: "unordered ok" }); } finally { a.close(); b.close(); @@ -162,21 +162,21 @@ Deno.test("loopback: receive-via-stream consumes a burst", NO_SANITIZE, async () const bytes = await collectU8(sm.data as unknown as AsyncIterable); received.push( sm.kind === "string" - ? { tag: "string", val: new TextDecoder().decode(bytes) } - : { tag: "binary", val: bytes }, + ? { kind: "string", value: new TextDecoder().decode(bytes) } + : { kind: "binary", value: bytes }, ); if (received.length === 3) return; } })(); for (let i = 0; i < 3; i++) { - await chA.send({ tag: "string", val: `msg-${i}` }); + await chA.send({ kind: "string", value: `msg-${i}` }); } await streamDone; assertEquals(received, [ - { tag: "string", val: "msg-0" }, - { tag: "string", val: "msg-1" }, - { tag: "string", val: "msg-2" }, + { kind: "string", value: "msg-0" }, + { kind: "string", value: "msg-1" }, + { kind: "string", value: "msg-2" }, ]); } finally { a.close(); @@ -208,8 +208,8 @@ Deno.test("loopback: single-use violation -> receiving-via-stream error", NO_SAN void chA; // keep `a`'s channel referenced for symmetry/documentation chB.receiveViaStream(); - const err = await assertRejects(() => chB.receive(), WitError); - assertEquals((err as WitError).payload, { tag: "receiving-via-stream" }); + const err = await assertRejects(() => chB.receive(), ComponentException); + assertEquals((err as ComponentException).payload, { kind: "receiving-via-stream" }); // A second `receiveViaStream` call after the first also violates the // once-only rule (thrown synchronously, per the WIT contract). @@ -219,8 +219,8 @@ Deno.test("loopback: single-use violation -> receiving-via-stream error", NO_SAN } catch (e) { threw = e; } - assert(threw instanceof WitError); - assertEquals((threw as WitError).payload, { tag: "receiving-via-stream" }); + assert(threw instanceof ComponentException); + assertEquals((threw as ComponentException).payload, { kind: "receiving-via-stream" }); } finally { a.close(); b.close(); @@ -240,7 +240,7 @@ Deno.test("loopback: inbound-buffer overflow -> overflow-close semantics", NO_SA // and the overflow-close fires on the sender or receiver's channel. for (let i = 0; i < 20; i++) { try { - await chA.send({ tag: "string", val: `0123456789-${i}` }); + await chA.send({ kind: "string", value: `0123456789-${i}` }); } catch { break; // sender side observed the close once b's channel closed. } @@ -256,9 +256,9 @@ Deno.test("loopback: inbound-buffer overflow -> overflow-close semantics", NO_SA try { await chB.receive(); } catch (e) { - assert(e instanceof WitError); - assertEquals((e as WitError).payload, { - tag: "receive-buffer-overflow", + assert(e instanceof ComponentException); + assertEquals((e as ComponentException).payload, { + kind: "receive-buffer-overflow", }); overflowed = true; break; @@ -280,8 +280,8 @@ Deno.test("loopback: close propagation + post-close error cases", NO_SANITIZE, a const chB = await firstIncoming(b); chA.close(); - const err = await assertRejects(() => chA.send({ tag: "string", val: "x" }), WitError); - assertEquals((err as WitError).payload, { tag: "closed" }); + const err = await assertRejects(() => chA.send({ kind: "string", value: "x" }), ComponentException); + assertEquals((err as ComponentException).payload, { kind: "closed" }); // The peer observes the remote close too (eventually `receive` fails). let sawClosed = false; @@ -289,7 +289,7 @@ Deno.test("loopback: close propagation + post-close error cases", NO_SANITIZE, a try { await chB.receive(); } catch (e) { - assert(e instanceof WitError); + assert(e instanceof ComponentException); sawClosed = true; break; } @@ -298,8 +298,8 @@ Deno.test("loopback: close propagation + post-close error cases", NO_SANITIZE, a a.close(); b.close(); - const connErr = await assertRejects(() => a.createOffer(), WitError); - assertEquals((connErr as WitError).payload, { tag: "closed" }); + const connErr = await assertRejects(() => a.createOffer(), ComponentException); + assertEquals((connErr as ComponentException).payload, { kind: "closed" }); }); Deno.test( @@ -317,17 +317,17 @@ Deno.test( // the backend transitions the native readyState. a.close(); const errA = await assertRejects( - () => chA.send({ tag: "string", val: "after-close" }), - WitError, + () => chA.send({ kind: "string", value: "after-close" }), + ComponentException, ); - assertEquals((errA as WitError).payload, { tag: "closed" }); + assertEquals((errA as ComponentException).payload, { kind: "closed" }); b.close(); const errB = await assertRejects( - () => chB.send({ tag: "string", val: "after-close" }), - WitError, + () => chB.send({ kind: "string", value: "after-close" }), + ComponentException, ); - assertEquals((errB as WitError).payload, { tag: "closed" }); + assertEquals((errB as ComponentException).payload, { kind: "closed" }); }, ); @@ -380,13 +380,13 @@ Deno.test("remote close dispatched before a delivered message: payload still win const got = await parked; assertEquals(got, { - tag: "binary", - val: new Uint8Array([7, 7, 7, 7, 7, 7, 7, 7]), + kind: "binary", + value: new Uint8Array([7, 7, 7, 7, 7, 7, 7, 7]), } as Message); // After the drain window the implied close reaches readers as `closed`. - const err = await assertRejects(() => dc.receive(), WitError); - assertEquals((err.payload as WebrtcError).tag, "closed"); + const err = await assertRejects(() => dc.receive(), ComponentException); + assertEquals((err.payload as WebrtcError).kind, "closed"); }); // The sender half of issue #154: Chromium can DISCARD payload still in the @@ -421,8 +421,8 @@ Deno.test("close with buffered payload defers the reset until the queue drains", dc.close(); // Locally the close is immediate (WIT contract)... await assertRejects( - () => dc.send({ tag: "binary", val: new Uint8Array([1]) }), - WitError, + () => dc.send({ kind: "binary", value: new Uint8Array([1]) }), + ComponentException, ); // ...but the wire-level reset waits for the transport queue. assertEquals(nativeCloseCalls, 0);