transport-io 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +28 -0
  3. package/dist/adapter.d.ts +69 -0
  4. package/dist/adapter.d.ts.map +1 -0
  5. package/dist/adapter.js +51 -0
  6. package/dist/adapter.js.map +1 -0
  7. package/dist/client.d.ts +60 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +162 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/codec.d.ts +5 -0
  12. package/dist/codec.d.ts.map +1 -0
  13. package/dist/codec.js +47 -0
  14. package/dist/codec.js.map +1 -0
  15. package/dist/contract.d.ts +79 -0
  16. package/dist/contract.d.ts.map +1 -0
  17. package/dist/contract.js +54 -0
  18. package/dist/contract.js.map +1 -0
  19. package/dist/datagram.d.ts +29 -0
  20. package/dist/datagram.d.ts.map +1 -0
  21. package/dist/datagram.js +92 -0
  22. package/dist/datagram.js.map +1 -0
  23. package/dist/errors.d.ts +8 -0
  24. package/dist/errors.d.ts.map +1 -0
  25. package/dist/errors.js +11 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/framer.d.ts +21 -0
  28. package/dist/framer.d.ts.map +1 -0
  29. package/dist/framer.js +110 -0
  30. package/dist/framer.js.map +1 -0
  31. package/dist/handshake.d.ts +34 -0
  32. package/dist/handshake.d.ts.map +1 -0
  33. package/dist/handshake.js +79 -0
  34. package/dist/handshake.js.map +1 -0
  35. package/dist/hub.d.ts +43 -0
  36. package/dist/hub.d.ts.map +1 -0
  37. package/dist/hub.js +149 -0
  38. package/dist/hub.js.map +1 -0
  39. package/dist/index.d.ts +19 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +18 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/origin.d.ts +18 -0
  44. package/dist/origin.d.ts.map +1 -0
  45. package/dist/origin.js +72 -0
  46. package/dist/origin.js.map +1 -0
  47. package/dist/protocol.d.ts +79 -0
  48. package/dist/protocol.d.ts.map +1 -0
  49. package/dist/protocol.js +76 -0
  50. package/dist/protocol.js.map +1 -0
  51. package/dist/queue.d.ts +45 -0
  52. package/dist/queue.d.ts.map +1 -0
  53. package/dist/queue.js +94 -0
  54. package/dist/queue.js.map +1 -0
  55. package/dist/server.d.ts +45 -0
  56. package/dist/server.d.ts.map +1 -0
  57. package/dist/server.js +127 -0
  58. package/dist/server.js.map +1 -0
  59. package/dist/session.d.ts +94 -0
  60. package/dist/session.d.ts.map +1 -0
  61. package/dist/session.js +645 -0
  62. package/dist/session.js.map +1 -0
  63. package/dist/testing/hostile-adapter.d.ts +44 -0
  64. package/dist/testing/hostile-adapter.d.ts.map +1 -0
  65. package/dist/testing/hostile-adapter.js +116 -0
  66. package/dist/testing/hostile-adapter.js.map +1 -0
  67. package/dist/testing/index.d.ts +8 -0
  68. package/dist/testing/index.d.ts.map +1 -0
  69. package/dist/testing/index.js +8 -0
  70. package/dist/testing/index.js.map +1 -0
  71. package/dist/transport/browser.d.ts +8 -0
  72. package/dist/transport/browser.d.ts.map +1 -0
  73. package/dist/transport/browser.js +89 -0
  74. package/dist/transport/browser.js.map +1 -0
  75. package/dist/transport/fails.node.d.ts +33 -0
  76. package/dist/transport/fails.node.d.ts.map +1 -0
  77. package/dist/transport/fails.node.js +184 -0
  78. package/dist/transport/fails.node.js.map +1 -0
  79. package/dist/transport/loopback.d.ts +11 -0
  80. package/dist/transport/loopback.d.ts.map +1 -0
  81. package/dist/transport/loopback.js +98 -0
  82. package/dist/transport/loopback.js.map +1 -0
  83. package/dist/transport/moq.node.d.ts +19 -0
  84. package/dist/transport/moq.node.d.ts.map +1 -0
  85. package/dist/transport/moq.node.js +211 -0
  86. package/dist/transport/moq.node.js.map +1 -0
  87. package/dist/transport/parity-suite.d.ts +40 -0
  88. package/dist/transport/parity-suite.d.ts.map +1 -0
  89. package/dist/transport/parity-suite.js +133 -0
  90. package/dist/transport/parity-suite.js.map +1 -0
  91. package/dist/transport/types.d.ts +36 -0
  92. package/dist/transport/types.d.ts.map +1 -0
  93. package/dist/transport/types.js +2 -0
  94. package/dist/transport/types.js.map +1 -0
  95. package/dist/transport/unreliable.d.ts +37 -0
  96. package/dist/transport/unreliable.d.ts.map +1 -0
  97. package/dist/transport/unreliable.js +59 -0
  98. package/dist/transport/unreliable.js.map +1 -0
  99. package/package.json +55 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 #V0ID <hey@v0id.me>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # transport-io
2
+
3
+ Real-time apps over WebTransport. Socket.IO's shape, on a transport with multiple streams
4
+ and datagrams, without Socket.IO's mistakes.
5
+
6
+ Framing, length prefixes, buffer accumulation, stream lifecycle and backpressure queues are
7
+ hidden. Reliability is not: an event declares `stream` or `datagram` in the contract, and
8
+ "this message may be dropped" is a property of your data that lives in the type system.
9
+
10
+ **Full documentation, and the limitations you should read before installing, are in the
11
+ [repository README](https://github.com/v0id-user/transport-io#readme).** The short version:
12
+
13
+ - **WebTransport only.** No WebSocket fallback, deliberately — a fallback would silently
14
+ make the datagram lane reliable and ordered, which is a lie about your data.
15
+ - **Chrome and Firefox.** Safari cannot talk to a quiche-backed server and is unsupported.
16
+ - **The server needs a separate native install**, and its Linux prebuild needs glibc 2.38 —
17
+ no default Node `-slim` image has it, and Alpine has no prebuild at all.
18
+ - **Each `call()` leaks ~5.95 KB of server memory**, upstream in the QUIC binding, not in
19
+ this library. `emit` and datagrams are flat.
20
+ - **The protocol is v0 and unstable.** Both sides currently require an exact match.
21
+
22
+ ```bash
23
+ npm install transport-io
24
+ ```
25
+
26
+ A git install does not work — the repository root is a private monorepo package.
27
+
28
+ MIT © #V0ID
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The fan-out boundary. PROTOCOL.md is silent on this by design: it is our contract with
3
+ * a message bus, not with a peer.
4
+ *
5
+ * Rules core obeys (D40): every method is async even in memory, frames cross as bytes and
6
+ * never as live objects, `PeerId` is a stable cross-process string, no node assumes it
7
+ * knows a room's full membership, a frame for a room with no local members is dropped
8
+ * silently rather than erroring, and any method may reject — core degrades rather than
9
+ * crashing.
10
+ */
11
+ export type PeerId = string;
12
+ export type Frame = Uint8Array;
13
+ export type Lane = 'stream' | 'datagram';
14
+ /** The envelope carries the origin node. The frame the peer receives is not these bytes. */
15
+ export interface RemoteEnvelope {
16
+ readonly room: string;
17
+ readonly frame: Frame;
18
+ readonly lane: Lane;
19
+ readonly nodeId: string;
20
+ readonly except?: readonly PeerId[];
21
+ }
22
+ export interface BroadcastOptions {
23
+ readonly lane: Lane;
24
+ readonly except?: readonly PeerId[];
25
+ }
26
+ export interface Adapter {
27
+ /**
28
+ * The node this adapter publishes as. It is the identity stamped into every
29
+ * `RemoteEnvelope`, and therefore the identity core dedupes against.
30
+ *
31
+ * Declared here rather than left to implementations because `Server` also has a
32
+ * `nodeId`, and when the two diverged the dedup guard silently never fired: a node
33
+ * delivered its own broadcast twice, once locally and once back off the bus. Reading
34
+ * both from one place makes that impossible rather than merely unlikely.
35
+ */
36
+ readonly nodeId: string;
37
+ join(room: string, peer: PeerId): Promise<void>;
38
+ leave(room: string, peer: PeerId): Promise<void>;
39
+ broadcast(room: string, frame: Frame, opts: BroadcastOptions): Promise<void>;
40
+ onRemote(cb: (envelope: RemoteEnvelope) => void): void;
41
+ }
42
+ /**
43
+ * Write-only half of the boundary, for a caller that publishes without hosting sessions.
44
+ * Internal in this version (D22): the interface is a design constraint, and a
45
+ * cross-process implementation arrives with the Redis adapter.
46
+ */
47
+ export interface Publisher {
48
+ broadcast(room: string, frame: Frame, opts: BroadcastOptions): Promise<void>;
49
+ }
50
+ /** Ships in core and is the default, so installing this library needs no infrastructure. */
51
+ /**
52
+ * A bus two or more `MemoryAdapter`s can share, so a single process can model several
53
+ * nodes. Without it `MemoryAdapter` is per-instance and two of them cannot hear each
54
+ * other, which is precisely why the cross-node delivery path had never executed a line.
55
+ */
56
+ export declare function memoryBus(): MemoryBus;
57
+ export interface MemoryBus {
58
+ readonly listeners: ((e: RemoteEnvelope) => void)[];
59
+ }
60
+ export declare class MemoryAdapter implements Adapter {
61
+ #private;
62
+ readonly nodeId: string;
63
+ constructor(nodeId: string, bus?: MemoryBus);
64
+ join(room: string, peer: PeerId): Promise<void>;
65
+ leave(room: string, peer: PeerId): Promise<void>;
66
+ broadcast(room: string, frame: Frame, opts: BroadcastOptions): Promise<void>;
67
+ onRemote(cb: (e: RemoteEnvelope) => void): void;
68
+ }
69
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,MAAM,MAAM,GAAG,MAAM,CAAA;AAC3B,MAAM,MAAM,KAAK,GAAG,UAAU,CAAA;AAC9B,MAAM,MAAM,IAAI,GAAG,QAAQ,GAAG,UAAU,CAAA;AAExC,4FAA4F;AAC5F,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACpC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;IACnB,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACpC;AAED,MAAM,WAAW,OAAO;IACtB;;;;;;;;OAQG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/C,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChD,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5E,QAAQ,CAAC,EAAE,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,GAAG,IAAI,CAAA;CACvD;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7E;AAED,4FAA4F;AAC5F;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,SAAS,CAErC;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,KAAK,IAAI,CAAC,EAAE,CAAA;CACpD;AAED,qBAAa,aAAc,YAAW,OAAO;;IAG3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IAEvB,YAAY,MAAM,EAAE,MAAM,EAAE,GAAG,GAAE,SAAuB,EAGvD;IAEK,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOpD;IAEK,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKrD;IAEK,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAWjF;IAED,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,IAAI,GAAG,IAAI,CAE9C;CACF"}
@@ -0,0 +1,51 @@
1
+ /** Ships in core and is the default, so installing this library needs no infrastructure. */
2
+ /**
3
+ * A bus two or more `MemoryAdapter`s can share, so a single process can model several
4
+ * nodes. Without it `MemoryAdapter` is per-instance and two of them cannot hear each
5
+ * other, which is precisely why the cross-node delivery path had never executed a line.
6
+ */
7
+ export function memoryBus() {
8
+ return { listeners: [] };
9
+ }
10
+ export class MemoryAdapter {
11
+ #rooms = new Map();
12
+ #bus;
13
+ nodeId;
14
+ constructor(nodeId, bus = memoryBus()) {
15
+ this.nodeId = nodeId;
16
+ this.#bus = bus;
17
+ }
18
+ async join(room, peer) {
19
+ let set = this.#rooms.get(room);
20
+ if (set === undefined) {
21
+ set = new Set();
22
+ this.#rooms.set(room, set);
23
+ }
24
+ set.add(peer);
25
+ }
26
+ async leave(room, peer) {
27
+ const set = this.#rooms.get(room);
28
+ if (set === undefined)
29
+ return;
30
+ set.delete(peer);
31
+ if (set.size === 0)
32
+ this.#rooms.delete(room);
33
+ }
34
+ async broadcast(room, frame, opts) {
35
+ // A single process still delivers its own publish back, because that is what a real
36
+ // bus does and core must dedupe rather than rely on the adapter suppressing it.
37
+ const envelope = {
38
+ room,
39
+ frame,
40
+ lane: opts.lane,
41
+ nodeId: this.nodeId,
42
+ ...(opts.except === undefined ? {} : { except: opts.except }),
43
+ };
44
+ for (const l of this.#bus.listeners)
45
+ l(envelope);
46
+ }
47
+ onRemote(cb) {
48
+ this.#bus.listeners.push(cb);
49
+ }
50
+ }
51
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAsDA,4FAA4F;AAC5F;;;;GAIG;AACH,MAAM,UAAU,SAAS;IACvB,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,CAAA;AAC1B,CAAC;AAMD,MAAM,OAAO,aAAa;IACf,MAAM,GAAG,IAAI,GAAG,EAAuB,CAAA;IACvC,IAAI,CAAW;IACf,MAAM,CAAQ;IAEvB,YAAY,MAAc,EAAE,GAAG,GAAc,SAAS,EAAE;QACtD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,IAAI,GAAG,GAAG,CAAA;IACjB,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAY,EAAE,IAAY;QACnC,IAAI,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QAC/B,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,GAAG,GAAG,IAAI,GAAG,EAAE,CAAA;YACf,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;QAC5B,CAAC;QACD,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IACf,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAAY,EAAE,IAAY;QACpC,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACjC,IAAI,GAAG,KAAK,SAAS;YAAE,OAAM;QAC7B,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAChB,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC;YAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC9C,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,IAAY,EAAE,KAAY,EAAE,IAAsB;QAChE,oFAAoF;QACpF,gFAAgF;QAChF,MAAM,QAAQ,GAAmB;YAC/B,IAAI;YACJ,KAAK;YACL,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;SAC9D,CAAA;QACD,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS;YAAE,CAAC,CAAC,QAAQ,CAAC,CAAA;IAClD,CAAC;IAED,QAAQ,CAAC,EAA+B;QACtC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAC9B,CAAC;CACF"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Client surface.
3
+ *
4
+ * Constructible without I/O: nothing here touches `window` or `WebTransport` at module
5
+ * scope, so importing this on a server — which Next.js will do — is safe. Feature
6
+ * detection happens inside connect().
7
+ */
8
+ import { type AnyMap, type CallableOf, type Contract } from './contract.ts';
9
+ import { TransportError } from './errors.ts';
10
+ import { type SessionStats } from './session.ts';
11
+ import type { Connection } from './transport/types.ts';
12
+ export type Status = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
13
+ export interface ClientState {
14
+ readonly status: Status;
15
+ readonly sessionId: string | null;
16
+ readonly rooms: readonly string[];
17
+ readonly lastError: TransportError | null;
18
+ }
19
+ export interface ClientOptions<C extends Contract = Contract> {
20
+ /**
21
+ * How long to wait for the peer's handshake before giving up. The deadline covers
22
+ * opening the emit stream as well as the exchange, so a transport that never opens one
23
+ * fails here rather than hanging.
24
+ */
25
+ readonly handshakeDeadlineMs?: number;
26
+ readonly contract: C;
27
+ /** Supplied by the transport seam, so this class never imports a transport. */
28
+ readonly connect: () => Promise<Connection>;
29
+ readonly validateInbound?: boolean;
30
+ /** Clients stamp their own origin on outbound datagrams. */
31
+ readonly origin?: number;
32
+ /** Test seam: how a queued datagram flush is deferred. Defaults to a microtask. */
33
+ readonly scheduleFlush?: (flush: () => void) => void;
34
+ /** Test seam: the clock the TTL is measured against. Defaults to `Date.now`. */
35
+ readonly now?: () => number;
36
+ }
37
+ export declare class Client<M extends AnyMap = AnyMap> {
38
+ #private;
39
+ constructor(opts: ClientOptions);
40
+ /**
41
+ * Referentially stable until something actually changes. Returning a freshly built
42
+ * object on each call makes useSyncExternalStore re-render forever, which is the single
43
+ * most common way this shape is implemented incorrectly.
44
+ */
45
+ getSnapshot(): ClientState;
46
+ subscribe(listener: () => void): () => void;
47
+ /** Idempotent and refcounted: two components sharing a client cannot tear each other
48
+ * down, which matters because React StrictMode mounts twice in development. */
49
+ connect(): Promise<void>;
50
+ disconnect(): void;
51
+ /** The lane comes from the contract, never from this call site. */
52
+ emit<K extends keyof M & string>(event: K, payload: M[K]['payload']): void;
53
+ on<K extends keyof M & string>(event: K, handler: (payload: M[K]['payload']) => void): () => void;
54
+ /** Available only on events declaring `returns`. Aborting resets the QUIC stream. */
55
+ call<K extends CallableOf<M> & string>(event: K, payload: M[K]['payload'], options?: {
56
+ readonly signal?: AbortSignal;
57
+ }): Promise<M[K]['returns']>;
58
+ stats(): SessionStats | undefined;
59
+ }
60
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,KAAK,MAAM,EAAmB,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAA;AAC5F,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAE5C,OAAO,EAAW,KAAK,YAAY,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEtD,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,YAAY,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAA;AAE/E,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,QAAQ,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,CAAA;CAC1C;AAED,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,QAAQ,GAAG,QAAQ;IAC1D;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAErC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAA;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,CAAA;IAC3C,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAA;IAClC,4DAA4D;IAC5D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,mFAAmF;IACnF,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,IAAI,KAAK,IAAI,CAAA;IACpD,gFAAgF;IAChF,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAC5B;AAED,qBAAa,MAAM,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM;;IAc3C,YAAY,IAAI,EAAE,aAAa,EAE9B;IAED;;;;OAIG;IACH,WAAW,IAAI,WAAW,CAEzB;IAED,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAK1C;IAED;oFACgF;IAC1E,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAS7B;IAED,UAAU,IAAI,IAAI,CAQjB;IAED,mEAAmE;IACnE,IAAI,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,CAEzE;IAED,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,EAC3B,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,IAAI,GAC1C,MAAM,IAAI,CAYZ;IAED,qFAAqF;IAC/E,IAAI,CAAC,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,GAAG,MAAM,EACzC,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,EACxB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAC1C,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAE1B;IAED,KAAK,IAAI,YAAY,GAAG,SAAS,CAEhC;CAoFF"}
package/dist/client.js ADDED
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Client surface.
3
+ *
4
+ * Constructible without I/O: nothing here touches `window` or `WebTransport` at module
5
+ * scope, so importing this on a server — which Next.js will do — is safe. Feature
6
+ * detection happens inside connect().
7
+ */
8
+ import { buildEventTable } from './contract.js';
9
+ import { TransportError } from './errors.js';
10
+ import { CloseCode, FrameType } from './protocol.js';
11
+ import { Session } from './session.js';
12
+ export class Client {
13
+ #opts;
14
+ #listeners = new Set();
15
+ #handlers = new Map();
16
+ #session;
17
+ #snapshot = Object.freeze({
18
+ status: 'idle',
19
+ sessionId: null,
20
+ rooms: [],
21
+ lastError: null,
22
+ });
23
+ #refs = 0;
24
+ #connecting;
25
+ constructor(opts) {
26
+ this.#opts = opts;
27
+ }
28
+ /**
29
+ * Referentially stable until something actually changes. Returning a freshly built
30
+ * object on each call makes useSyncExternalStore re-render forever, which is the single
31
+ * most common way this shape is implemented incorrectly.
32
+ */
33
+ getSnapshot() {
34
+ return this.#snapshot;
35
+ }
36
+ subscribe(listener) {
37
+ this.#listeners.add(listener);
38
+ return () => {
39
+ this.#listeners.delete(listener);
40
+ };
41
+ }
42
+ /** Idempotent and refcounted: two components sharing a client cannot tear each other
43
+ * down, which matters because React StrictMode mounts twice in development. */
44
+ async connect() {
45
+ this.#refs++;
46
+ if (this.#connecting === undefined)
47
+ this.#connecting = this.#doConnect();
48
+ try {
49
+ await this.#connecting;
50
+ }
51
+ catch (e) {
52
+ this.#connecting = undefined;
53
+ throw e;
54
+ }
55
+ }
56
+ disconnect() {
57
+ this.#refs = Math.max(0, this.#refs - 1);
58
+ if (this.#refs > 0)
59
+ return;
60
+ this.#patch({ status: 'closing' });
61
+ this.#session?.close(CloseCode.WT_NO_ERROR, 'client disconnect');
62
+ this.#session = undefined;
63
+ this.#connecting = undefined;
64
+ this.#patch({ status: 'closed', sessionId: null, rooms: [] });
65
+ }
66
+ /** The lane comes from the contract, never from this call site. */
67
+ emit(event, payload) {
68
+ this.#requireSession().emit(event, payload);
69
+ }
70
+ on(event, handler) {
71
+ let set = this.#handlers.get(event);
72
+ if (set === undefined) {
73
+ set = new Set();
74
+ this.#handlers.set(event, set);
75
+ }
76
+ set.add(handler);
77
+ const off = this.#session?.on(event, (p) => handler(p));
78
+ return () => {
79
+ set.delete(handler);
80
+ off?.();
81
+ };
82
+ }
83
+ /** Available only on events declaring `returns`. Aborting resets the QUIC stream. */
84
+ async call(event, payload, options) {
85
+ return (await this.#requireSession().call(event, payload, options));
86
+ }
87
+ stats() {
88
+ return this.#session?.stats();
89
+ }
90
+ async #doConnect() {
91
+ this.#patch({ status: 'connecting', lastError: null });
92
+ try {
93
+ const table = await buildEventTable(this.#opts.contract);
94
+ const conn = await this.#opts.connect();
95
+ // Chrome implements neither `requireUnreliable` nor `reliability`, so `undefined`
96
+ // must pass or every session on the dominant browser would be refused. Only an
97
+ // explicit reliable-only is a lie about the datagram lane.
98
+ if (conn.reliability() === 'reliable-only') {
99
+ // §10.2 code 1006. Throwing without closing left the peer holding a session this
100
+ // side had already abandoned, with nothing on the wire to say why.
101
+ conn.close(CloseCode.WT_RELIABILITY_REFUSED, 'reliable-only transport refused');
102
+ throw new TransportError('WT_RELIABILITY_REFUSED', 'the session negotiated reliable-only transport', 'The datagram lane would silently become reliable and ordered. This library refuses rather than lie about your data.');
103
+ }
104
+ const session = new Session(conn, {
105
+ table,
106
+ origin: this.#opts.origin ?? 0x80000001,
107
+ ...(this.#opts.validateInbound === undefined
108
+ ? {}
109
+ : { validateInbound: this.#opts.validateInbound }),
110
+ ...(this.#opts.handshakeDeadlineMs === undefined
111
+ ? {}
112
+ : { handshakeDeadlineMs: this.#opts.handshakeDeadlineMs }),
113
+ ...(this.#opts.scheduleFlush === undefined
114
+ ? {}
115
+ : { scheduleFlush: this.#opts.scheduleFlush }),
116
+ ...(this.#opts.now === undefined ? {} : { now: this.#opts.now }),
117
+ });
118
+ this.#session = session;
119
+ for (const [event, handlers] of this.#handlers) {
120
+ for (const h of handlers)
121
+ session.on(event, (p) => h(p));
122
+ }
123
+ session.onControl((type, body) => this.#onMembership(type, body));
124
+ await session.start();
125
+ this.#patch({ status: 'connected', sessionId: `s-${session.origin}` });
126
+ void conn.closed.then(() => {
127
+ this.#patch({ status: 'closed', sessionId: null, rooms: [] });
128
+ });
129
+ }
130
+ catch (e) {
131
+ const err = e instanceof TransportError
132
+ ? e
133
+ : new TransportError('WT_SESSION_CLOSED', String(e), 'Retry the connection.');
134
+ this.#patch({ status: 'closed', lastError: err });
135
+ throw err;
136
+ }
137
+ }
138
+ /** Rooms are server-authoritative, so membership only ever arrives as a notification. */
139
+ #onMembership(type, body) {
140
+ const room = body.room;
141
+ if (typeof room !== 'string')
142
+ return;
143
+ const rooms = new Set(this.#snapshot.rooms);
144
+ if (type === FrameType.JOIN)
145
+ rooms.add(room);
146
+ else
147
+ rooms.delete(room);
148
+ this.#patch({ rooms: [...rooms].sort() });
149
+ }
150
+ #requireSession() {
151
+ if (this.#session === undefined) {
152
+ throw new TransportError('WT_SESSION_CLOSED', 'not connected', 'Await connect() before emitting.');
153
+ }
154
+ return this.#session;
155
+ }
156
+ #patch(next) {
157
+ this.#snapshot = Object.freeze({ ...this.#snapshot, ...next });
158
+ for (const l of this.#listeners)
159
+ l();
160
+ }
161
+ }
162
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export declare function encodePayload(value: unknown): Uint8Array;
2
+ export declare function decodePayload(bytes: Uint8Array): unknown;
3
+ /** Validate inbound only. The process that produced a payload need not check its own work. */
4
+ export declare function validate(schema: import('./contract.ts').Schema, value: unknown): Promise<unknown>;
5
+ //# sourceMappingURL=codec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../src/codec.ts"],"names":[],"mappings":"AAMA,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,CAmBxD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAoBxD;AAED,8FAA8F;AAC9F,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,OAAO,eAAe,EAAE,MAAM,EACtC,KAAK,EAAE,OAAO,GACb,OAAO,CAAC,OAAO,CAAC,CAgBlB"}
package/dist/codec.js ADDED
@@ -0,0 +1,47 @@
1
+ /** JSON over UTF-8. PROTOCOL.md §5.3 — codec 0x01, the only one this version speaks. */
2
+ import { TransportError } from './errors.js';
3
+ const encoder = new TextEncoder();
4
+ const decoder = new TextDecoder('utf-8', { fatal: true });
5
+ export function encodePayload(value) {
6
+ let json;
7
+ try {
8
+ json = JSON.stringify(value);
9
+ }
10
+ catch (cause) {
11
+ throw new TransportError('WT_VALIDATION_FAILED', `payload is not JSON-serialisable: ${cause.message}`, 'Remove cycles, functions and BigInt from the payload, or declare a codec that supports them.');
12
+ }
13
+ if (json === undefined) {
14
+ throw new TransportError('WT_VALIDATION_FAILED', 'payload serialised to undefined', 'Send null rather than undefined. A zero-length frame is a protocol error.');
15
+ }
16
+ return encoder.encode(json);
17
+ }
18
+ export function decodePayload(bytes) {
19
+ let text;
20
+ try {
21
+ text = decoder.decode(bytes);
22
+ }
23
+ catch {
24
+ throw new TransportError('WT_PROTOCOL_ERROR', 'payload is not valid UTF-8', 'Check the sender is encoding with codec 0x01 (JSON over UTF-8).');
25
+ }
26
+ try {
27
+ return JSON.parse(text);
28
+ }
29
+ catch (cause) {
30
+ throw new TransportError('WT_PROTOCOL_ERROR', `payload is not valid JSON: ${cause.message}`, 'Check the sender is encoding with codec 0x01 (JSON over UTF-8).');
31
+ }
32
+ }
33
+ /** Validate inbound only. The process that produced a payload need not check its own work. */
34
+ export async function validate(schema, value) {
35
+ const result = await schema['~standard'].validate(value);
36
+ if ('issues' in result && result.issues !== undefined) {
37
+ const first = result.issues[0];
38
+ const path = first?.path
39
+ ?.map((p) => (typeof p === 'object' ? String(p.key) : String(p)))
40
+ .join('.');
41
+ throw new TransportError('WT_VALIDATION_FAILED', path
42
+ ? `field '${path}': ${first?.message ?? 'invalid'}`
43
+ : (first?.message ?? 'invalid payload'), 'Fix the payload to match the schema declared in the contract.');
44
+ }
45
+ return result.value;
46
+ }
47
+ //# sourceMappingURL=codec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec.js","sourceRoot":"","sources":["../src/codec.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAE5C,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAA;AACjC,MAAM,OAAO,GAAG,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;AAEzD,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,IAAI,IAAY,CAAA;IAChB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;IAC9B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,cAAc,CACtB,sBAAsB,EACtB,qCAAsC,KAAe,CAAC,OAAO,EAAE,EAC/D,8FAA8F,CAC/F,CAAA;IACH,CAAC;IACD,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,IAAI,cAAc,CACtB,sBAAsB,EACtB,iCAAiC,EACjC,2EAA2E,CAC5E,CAAA;IACH,CAAC;IACD,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;AAC7B,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAiB;IAC7C,IAAI,IAAY,CAAA;IAChB,IAAI,CAAC;QACH,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,cAAc,CACtB,mBAAmB,EACnB,4BAA4B,EAC5B,iEAAiE,CAClE,CAAA;IACH,CAAC;IACD,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACzB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,cAAc,CACtB,mBAAmB,EACnB,8BAA+B,KAAe,CAAC,OAAO,EAAE,EACxD,iEAAiE,CAClE,CAAA;IACH,CAAC;AACH,CAAC;AAED,8FAA8F;AAC9F,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,MAAsC,EACtC,KAAc;IAEd,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;IACxD,IAAI,QAAQ,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACtD,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;QAC9B,MAAM,IAAI,GAAG,KAAK,EAAE,IAAI;YACtB,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;aAChE,IAAI,CAAC,GAAG,CAAC,CAAA;QACZ,MAAM,IAAI,cAAc,CACtB,sBAAsB,EACtB,IAAI;YACF,CAAC,CAAC,UAAU,IAAI,MAAM,KAAK,EAAE,OAAO,IAAI,SAAS,EAAE;YACnD,CAAC,CAAC,CAAC,KAAK,EAAE,OAAO,IAAI,iBAAiB,CAAC,EACzC,+DAA+D,CAChE,CAAA;IACH,CAAC;IACD,OAAQ,MAA6B,CAAC,KAAK,CAAA;AAC7C,CAAC"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The contract is the single source of truth. Reading `contract.ts` in an application
3
+ * tells you every event, payload and lane without reading anything else.
4
+ */
5
+ import type { StandardSchemaV1, StandardTypedV1 } from '@standard-schema/spec';
6
+ export type Lane = 'stream' | 'datagram';
7
+ export type Schema = StandardSchemaV1;
8
+ /**
9
+ * `returns` is meaningful only on the stream lane: a datagram has no response path.
10
+ *
11
+ * The `returns?: never` on the datagram branch is load-bearing, not decoration. Excess
12
+ * property checking against a *union* admits any property present on any member, so
13
+ * `{ lane: 'datagram', payload, returns }` compiled happily, `CallableOf` admitted it, and
14
+ * `call()` served it over a bidirectional stream. A contract that says "may be dropped"
15
+ * produced a guaranteed ordered message with the type system agreeing — a direct violation
16
+ * of D1, the first decision this project made.
17
+ */
18
+ export type EventDef = {
19
+ readonly lane: 'datagram';
20
+ readonly payload: Schema;
21
+ readonly id?: number;
22
+ readonly returns?: never;
23
+ } | {
24
+ readonly lane: 'stream';
25
+ readonly payload: Schema;
26
+ readonly returns?: Schema;
27
+ readonly id?: number;
28
+ };
29
+ export type Contract = Readonly<Record<string, EventDef>>;
30
+ export type Infer<S extends StandardTypedV1> = StandardTypedV1.InferOutput<S>;
31
+ /**
32
+ * The plain payload/returns map every public signature is written against, so that no
33
+ * method hover ever has to print a validator's internal types. See D57 — declaring
34
+ * `interface AppMap extends MapOf<typeof contract> {}` is what keeps hover at 126
35
+ * characters instead of 303.
36
+ */
37
+ export type MapOf<C extends Contract> = {
38
+ readonly [K in keyof C]: {
39
+ readonly payload: Infer<C[K]['payload']>;
40
+ readonly returns: C[K] extends {
41
+ readonly returns: infer R extends Schema;
42
+ } ? Infer<R> : never;
43
+ };
44
+ };
45
+ export interface EventShape {
46
+ readonly payload: unknown;
47
+ readonly returns: unknown;
48
+ }
49
+ export type AnyMap = Readonly<Record<string, EventShape>>;
50
+ /** Events declaring `returns`, and therefore callable. */
51
+ export type CallableOf<M extends AnyMap> = {
52
+ [K in keyof M]: [M[K]['returns']] extends [never] ? never : K;
53
+ }[keyof M];
54
+ export declare function defineContract<const C extends Contract>(contract: C): C;
55
+ /** A types-only schema, for inference without runtime validation. */
56
+ export declare function type$<T>(): StandardSchemaV1<unknown, T>;
57
+ export interface EventEntry {
58
+ readonly name: string;
59
+ readonly id: number;
60
+ readonly lane: Lane;
61
+ readonly def: EventDef;
62
+ }
63
+ export interface EventTable {
64
+ readonly entries: readonly EventEntry[];
65
+ byName(name: string): EventEntry | undefined;
66
+ byId(id: number): EventEntry | undefined;
67
+ /** The `[name, id, lane]` triples the handshake carries. PROTOCOL.md §4.3. */
68
+ wire(): readonly [string, number, Lane][];
69
+ }
70
+ /**
71
+ * PROTOCOL.md §5.4 — the first four bytes of SHA-256 of the event name, big-endian.
72
+ *
73
+ * Async because `crypto.subtle` is, and because writing SHA-256 by hand would mean typing
74
+ * its round constants from memory, which is the one thing D58 forbids. Both `connect()`
75
+ * and `listen()` are already async, so the table is built once at session start.
76
+ */
77
+ export declare function eventIdOf(name: string): Promise<number>;
78
+ export declare function buildEventTable(contract: Contract): Promise<EventTable>;
79
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import { TransportError } from './errors.js';
2
+ import { EVENT_ID_NOT_APPLICABLE } from './protocol.js';
3
+ export function defineContract(contract) {
4
+ return contract;
5
+ }
6
+ /** A types-only schema, for inference without runtime validation. */
7
+ export function type$() {
8
+ return {
9
+ '~standard': {
10
+ version: 1,
11
+ vendor: 'transport-io',
12
+ validate: (value) => ({ value: value }),
13
+ },
14
+ };
15
+ }
16
+ /**
17
+ * PROTOCOL.md §5.4 — the first four bytes of SHA-256 of the event name, big-endian.
18
+ *
19
+ * Async because `crypto.subtle` is, and because writing SHA-256 by hand would mean typing
20
+ * its round constants from memory, which is the one thing D58 forbids. Both `connect()`
21
+ * and `listen()` are already async, so the table is built once at session start.
22
+ */
23
+ export async function eventIdOf(name) {
24
+ const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(name));
25
+ return new DataView(digest).getUint32(0, false);
26
+ }
27
+ export async function buildEventTable(contract) {
28
+ const names = Object.keys(contract).sort();
29
+ const entries = [];
30
+ const byId = new Map();
31
+ const byName = new Map();
32
+ for (const name of names) {
33
+ const def = contract[name];
34
+ const id = def.id ?? (await eventIdOf(name));
35
+ if (id === EVENT_ID_NOT_APPLICABLE) {
36
+ throw new TransportError('WT_CONTRACT_MISMATCH', `event '${name}' resolves to the reserved id 0`, 'Set an explicit `id` on this event. Zero is reserved for frames whose meaning comes from the stream.');
37
+ }
38
+ const clash = byId.get(id);
39
+ if (clash !== undefined) {
40
+ throw new TransportError('WT_CONTRACT_MISMATCH', `events '${clash.name}' and '${name}' both hash to id ${id}`, `Set an explicit \`id\` on one of them, for example { ..., id: ${id + 1} }. Do not rename your events.`);
41
+ }
42
+ const entry = { name, id, lane: def.lane, def };
43
+ entries.push(entry);
44
+ byId.set(id, entry);
45
+ byName.set(name, entry);
46
+ }
47
+ return {
48
+ entries,
49
+ byName: (n) => byName.get(n),
50
+ byId: (i) => byId.get(i),
51
+ wire: () => entries.map((e) => [e.name, e.id, e.lane]),
52
+ };
53
+ }
54
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AA2DvD,MAAM,UAAU,cAAc,CAA2B,QAAW;IAClE,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,KAAK;IACnB,OAAO;QACL,WAAW,EAAE;YACX,OAAO,EAAE,CAAC;YACV,MAAM,EAAE,cAAc;YACtB,QAAQ,EAAE,CAAC,KAAc,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAU,EAAE,CAAC;SACtD;KACF,CAAA;AACH,CAAC;AAmBD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAY;IAC1C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IACpF,OAAO,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;AACjD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,QAAkB;IACtD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAA;IAC1C,MAAM,OAAO,GAAiB,EAAE,CAAA;IAChC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAsB,CAAA;IAC1C,MAAM,MAAM,GAAG,IAAI,GAAG,EAAsB,CAAA;IAE5C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAa,CAAA;QACtC,MAAM,EAAE,GAAG,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC,CAAA;QAC5C,IAAI,EAAE,KAAK,uBAAuB,EAAE,CAAC;YACnC,MAAM,IAAI,cAAc,CACtB,sBAAsB,EACtB,UAAU,IAAI,iCAAiC,EAC/C,sGAAsG,CACvG,CAAA;QACH,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAC1B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,MAAM,IAAI,cAAc,CACtB,sBAAsB,EACtB,WAAW,KAAK,CAAC,IAAI,UAAU,IAAI,qBAAqB,EAAE,EAAE,EAC5D,iEAAiE,EAAE,GAAG,CAAC,gCAAgC,CACxG,CAAA;QACH,CAAC;QACD,MAAM,KAAK,GAAe,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,CAAA;QAC3D,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACnB,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAA;QACnB,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IACzB,CAAC;IAED,OAAO;QACL,OAAO;QACP,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QAC5B,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QACxB,IAAI,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAA2B,CAAC;KACjF,CAAA;AACH,CAAC"}