@zap-proto/web 0.1.0
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 +142 -0
- package/dist/auth.d.ts +22 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +4 -0
- package/dist/auth.js.map +1 -0
- package/dist/client.d.ts +50 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +105 -0
- package/dist/client.js.map +1 -0
- package/dist/conn.d.ts +60 -0
- package/dist/conn.d.ts.map +1 -0
- package/dist/conn.js +145 -0
- package/dist/conn.js.map +1 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +27 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +35 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +101 -0
- package/dist/server.js.map +1 -0
- package/dist/transport.d.ts +46 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +140 -0
- package/dist/transport.js.map +1 -0
- package/package.json +76 -0
- package/src/auth.ts +25 -0
- package/src/client.ts +165 -0
- package/src/conn.ts +172 -0
- package/src/errors.ts +30 -0
- package/src/index.ts +36 -0
- package/src/server.ts +146 -0
- package/src/transport.ts +173 -0
package/src/transport.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
2
|
+
// See the file LICENSE for licensing terms.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* transport.ts — an isomorphic WebSocket transport for ZAP RPC frames.
|
|
6
|
+
*
|
|
7
|
+
* The foundation runtime (@zap-proto/zap) frames a call as a single ZAP envelope
|
|
8
|
+
* (envelope.ts buildRequest / buildResponse). Over TCP, @zap-proto/zap's ZapClient
|
|
9
|
+
* length-prefixes each envelope to recover message boundaries. WebSocket binary
|
|
10
|
+
* frames already give us message boundaries for free, so the framing here is
|
|
11
|
+
* one ZAP envelope = one WS binary message — no extra length prefix.
|
|
12
|
+
*
|
|
13
|
+
* A single internal impl (makeWsTransport) feature-detects whether it is given
|
|
14
|
+
* the `ws` package WebSocket (Node, post-upgrade) or the browser's native
|
|
15
|
+
* WebSocket, and normalises both to the same {@link WsTransport} shape. Bytes
|
|
16
|
+
* in arrive as ArrayBuffer or Uint8Array (or, on Node `ws`, a Buffer) and are
|
|
17
|
+
* normalised to Uint8Array; bytes out are written as Uint8Array.
|
|
18
|
+
*
|
|
19
|
+
* Cap'n Proto-style text frames are a protocol violation: ZAP is binary-only.
|
|
20
|
+
* A received text frame closes the connection with WebSocket close code 1003
|
|
21
|
+
* (Unsupported Data).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** WebSocket close code for a non-binary (text) frame — Unsupported Data. */
|
|
25
|
+
export const WS_CLOSE_UNSUPPORTED = 1003;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* WsTransport is the byte pipe a ZAP RPC connection rides on. It is duplex and
|
|
29
|
+
* message-oriented: every {@link send} ships exactly one ZAP envelope, and the
|
|
30
|
+
* {@link onMessage} callback fires once per inbound envelope.
|
|
31
|
+
*/
|
|
32
|
+
export interface WsTransport {
|
|
33
|
+
/** Ship one ZAP envelope as a single binary WebSocket message. */
|
|
34
|
+
send(bytes: Uint8Array): void;
|
|
35
|
+
/** Register the inbound-envelope handler. At most one is active. */
|
|
36
|
+
onMessage(handler: (bytes: Uint8Array) => void): void;
|
|
37
|
+
/** Register the close handler (fires once, on either side closing). */
|
|
38
|
+
onClose(handler: (code: number, reason: string) => void): void;
|
|
39
|
+
/** Close the underlying socket. */
|
|
40
|
+
close(code?: number, reason?: string): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The subset of the WebSocket API this transport relies on, in either env. */
|
|
44
|
+
interface SocketLike {
|
|
45
|
+
binaryType?: string;
|
|
46
|
+
send(data: Uint8Array | ArrayBufferView | ArrayBuffer): void;
|
|
47
|
+
close(code?: number, reason?: string): void;
|
|
48
|
+
addEventListener?(type: string, listener: (ev: unknown) => void): void;
|
|
49
|
+
on?(type: string, listener: (...args: unknown[]) => void): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Normalise any WS payload shape to a Uint8Array, or null for a text frame. */
|
|
53
|
+
function toBytes(data: unknown): Uint8Array | null {
|
|
54
|
+
if (data instanceof Uint8Array) return data;
|
|
55
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
56
|
+
if (ArrayBuffer.isView(data)) {
|
|
57
|
+
const v = data as ArrayBufferView;
|
|
58
|
+
return new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
|
|
59
|
+
}
|
|
60
|
+
// Node `ws` can hand back an array of Buffers when fragmented; coalesce.
|
|
61
|
+
if (Array.isArray(data)) {
|
|
62
|
+
const parts = data.map(toBytes);
|
|
63
|
+
if (parts.some((p) => p === null)) return null;
|
|
64
|
+
const total = parts.reduce((n, p) => n + (p as Uint8Array).byteLength, 0);
|
|
65
|
+
const out = new Uint8Array(total);
|
|
66
|
+
let off = 0;
|
|
67
|
+
for (const p of parts) {
|
|
68
|
+
out.set(p as Uint8Array, off);
|
|
69
|
+
off += (p as Uint8Array).byteLength;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
// A string payload is a text frame — a protocol violation for ZAP.
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Wrap a socket (browser native WebSocket or Node `ws` WebSocket) as a
|
|
79
|
+
* {@link WsTransport}. Branches on whether the socket exposes the browser
|
|
80
|
+
* `addEventListener` API or the Node `ws` `on(...)` emitter API.
|
|
81
|
+
*/
|
|
82
|
+
function makeWsTransport(socket: SocketLike): WsTransport {
|
|
83
|
+
// Ask for ArrayBuffer payloads where the API allows it (browser + `ws`),
|
|
84
|
+
// so toBytes has a fast path and never sees a Blob.
|
|
85
|
+
if ("binaryType" in socket) socket.binaryType = "arraybuffer";
|
|
86
|
+
|
|
87
|
+
let onMsg: (bytes: Uint8Array) => void = () => {};
|
|
88
|
+
let onClose: (code: number, reason: string) => void = () => {};
|
|
89
|
+
let closed = false;
|
|
90
|
+
|
|
91
|
+
const handleData = (data: unknown): void => {
|
|
92
|
+
const bytes = toBytes(data);
|
|
93
|
+
if (bytes === null) {
|
|
94
|
+
// Text frame (or unrecognised payload): reject per ZAP binary-only rule.
|
|
95
|
+
if (!closed) {
|
|
96
|
+
closed = true;
|
|
97
|
+
socket.close(WS_CLOSE_UNSUPPORTED, "zap: binary frames only");
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
onMsg(bytes);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const handleClose = (code: number, reason: string): void => {
|
|
105
|
+
if (closed) return;
|
|
106
|
+
closed = true;
|
|
107
|
+
onClose(code, reason);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (typeof socket.addEventListener === "function") {
|
|
111
|
+
// Browser native WebSocket.
|
|
112
|
+
socket.addEventListener("message", (ev: unknown) =>
|
|
113
|
+
handleData((ev as MessageEvent).data),
|
|
114
|
+
);
|
|
115
|
+
socket.addEventListener("close", (ev: unknown) => {
|
|
116
|
+
const ce = ev as CloseEvent;
|
|
117
|
+
handleClose(ce.code ?? 1006, ce.reason ?? "");
|
|
118
|
+
});
|
|
119
|
+
socket.addEventListener("error", () => handleClose(1006, "error"));
|
|
120
|
+
} else if (typeof socket.on === "function") {
|
|
121
|
+
// Node `ws` WebSocket.
|
|
122
|
+
socket.on("message", (data: unknown, isBinary?: unknown) => {
|
|
123
|
+
// `ws` passes (data, isBinary); a false isBinary is a text frame.
|
|
124
|
+
if (isBinary === false) {
|
|
125
|
+
handleData("text");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
handleData(data);
|
|
129
|
+
});
|
|
130
|
+
socket.on("close", (code: unknown, reason: unknown) =>
|
|
131
|
+
handleClose(
|
|
132
|
+
typeof code === "number" ? code : 1006,
|
|
133
|
+
reason ? String(reason) : "",
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
socket.on("error", () => handleClose(1006, "error"));
|
|
137
|
+
} else {
|
|
138
|
+
throw new TypeError("zap-web: unsupported WebSocket implementation");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
send(bytes: Uint8Array): void {
|
|
143
|
+
if (closed) throw new Error("zap-web: transport closed");
|
|
144
|
+
socket.send(bytes);
|
|
145
|
+
},
|
|
146
|
+
onMessage(handler: (bytes: Uint8Array) => void): void {
|
|
147
|
+
onMsg = handler;
|
|
148
|
+
},
|
|
149
|
+
onClose(handler: (code: number, reason: string) => void): void {
|
|
150
|
+
onClose = handler;
|
|
151
|
+
},
|
|
152
|
+
close(code?: number, reason?: string): void {
|
|
153
|
+
if (closed) return;
|
|
154
|
+
closed = true;
|
|
155
|
+
socket.close(code, reason);
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Wrap a Node `ws` package WebSocket (after the HTTP upgrade completes) as a
|
|
162
|
+
* {@link WsTransport}. Server side.
|
|
163
|
+
*/
|
|
164
|
+
export function nodeWsTransport(ws: unknown): WsTransport {
|
|
165
|
+
return makeWsTransport(ws as SocketLike);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Wrap the browser's native WebSocket as a {@link WsTransport}. Client side.
|
|
170
|
+
*/
|
|
171
|
+
export function browserWsTransport(ws: unknown): WsTransport {
|
|
172
|
+
return makeWsTransport(ws as SocketLike);
|
|
173
|
+
}
|