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.
- package/LICENSE +21 -0
- package/README.md +28 -0
- package/dist/adapter.d.ts +69 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +51 -0
- package/dist/adapter.js.map +1 -0
- package/dist/client.d.ts +60 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +162 -0
- package/dist/client.js.map +1 -0
- package/dist/codec.d.ts +5 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +47 -0
- package/dist/codec.js.map +1 -0
- package/dist/contract.d.ts +79 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +54 -0
- package/dist/contract.js.map +1 -0
- package/dist/datagram.d.ts +29 -0
- package/dist/datagram.d.ts.map +1 -0
- package/dist/datagram.js +92 -0
- package/dist/datagram.js.map +1 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +11 -0
- package/dist/errors.js.map +1 -0
- package/dist/framer.d.ts +21 -0
- package/dist/framer.d.ts.map +1 -0
- package/dist/framer.js +110 -0
- package/dist/framer.js.map +1 -0
- package/dist/handshake.d.ts +34 -0
- package/dist/handshake.d.ts.map +1 -0
- package/dist/handshake.js +79 -0
- package/dist/handshake.js.map +1 -0
- package/dist/hub.d.ts +43 -0
- package/dist/hub.d.ts.map +1 -0
- package/dist/hub.js +149 -0
- package/dist/hub.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/origin.d.ts +18 -0
- package/dist/origin.d.ts.map +1 -0
- package/dist/origin.js +72 -0
- package/dist/origin.js.map +1 -0
- package/dist/protocol.d.ts +79 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +76 -0
- package/dist/protocol.js.map +1 -0
- package/dist/queue.d.ts +45 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +94 -0
- package/dist/queue.js.map +1 -0
- package/dist/server.d.ts +45 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +127 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +94 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +645 -0
- package/dist/session.js.map +1 -0
- package/dist/testing/hostile-adapter.d.ts +44 -0
- package/dist/testing/hostile-adapter.d.ts.map +1 -0
- package/dist/testing/hostile-adapter.js +116 -0
- package/dist/testing/hostile-adapter.js.map +1 -0
- package/dist/testing/index.d.ts +8 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +8 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/transport/browser.d.ts +8 -0
- package/dist/transport/browser.d.ts.map +1 -0
- package/dist/transport/browser.js +89 -0
- package/dist/transport/browser.js.map +1 -0
- package/dist/transport/fails.node.d.ts +33 -0
- package/dist/transport/fails.node.d.ts.map +1 -0
- package/dist/transport/fails.node.js +184 -0
- package/dist/transport/fails.node.js.map +1 -0
- package/dist/transport/loopback.d.ts +11 -0
- package/dist/transport/loopback.d.ts.map +1 -0
- package/dist/transport/loopback.js +98 -0
- package/dist/transport/loopback.js.map +1 -0
- package/dist/transport/moq.node.d.ts +19 -0
- package/dist/transport/moq.node.d.ts.map +1 -0
- package/dist/transport/moq.node.js +211 -0
- package/dist/transport/moq.node.js.map +1 -0
- package/dist/transport/parity-suite.d.ts +40 -0
- package/dist/transport/parity-suite.d.ts.map +1 -0
- package/dist/transport/parity-suite.js +133 -0
- package/dist/transport/parity-suite.js.map +1 -0
- package/dist/transport/types.d.ts +36 -0
- package/dist/transport/types.d.ts.map +1 -0
- package/dist/transport/types.js +2 -0
- package/dist/transport/types.js.map +1 -0
- package/dist/transport/unreliable.d.ts +37 -0
- package/dist/transport/unreliable.d.ts.map +1 -0
- package/dist/transport/unreliable.js +59 -0
- package/dist/transport/unreliable.js.map +1 -0
- 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"}
|
package/dist/adapter.js
ADDED
|
@@ -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"}
|
package/dist/client.d.ts
ADDED
|
@@ -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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"}
|
package/dist/codec.d.ts
ADDED
|
@@ -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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"}
|
package/dist/contract.js
ADDED
|
@@ -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"}
|