@react-native-harness/bridge 1.5.0 → 1.6.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.
@@ -0,0 +1,90 @@
1
+ export const KIB = 1024;
2
+ export const MAX_DATA_BYTES = 64 * KIB;
3
+ export const CHANNEL_WINDOW = 4 * KIB * KIB;
4
+ export const MAX_CHANNEL_NUMBER = 0xffffffff;
5
+ export const ENVELOPE = { OPEN: 1, DATA: 2, END: 3, STOP: 4, CREDIT: 5, RESET: 6 };
6
+ const RESET_CODES = { aborted: 1, stalled: 2, "not-accepted": 3, invalid: 4 };
7
+ const HEADER_BYTES = 5;
8
+ const CHANNEL_ID = /^[A-Za-z0-9-]{1,32}$/;
9
+ export function encodeEnvelope(type, number, payload = new Uint8Array(0)) {
10
+ const bytes = new Uint8Array(HEADER_BYTES + payload.length);
11
+ bytes[0] = type;
12
+ new DataView(bytes.buffer).setUint32(1, number);
13
+ bytes.set(payload, HEADER_BYTES);
14
+ return bytes;
15
+ }
16
+ export function encodeCredit(number, amount) {
17
+ const payload = new Uint8Array(4);
18
+ new DataView(payload.buffer).setUint32(0, amount);
19
+ return encodeEnvelope(ENVELOPE.CREDIT, number, payload);
20
+ }
21
+ export function encodeReset(number, reason) {
22
+ return encodeEnvelope(ENVELOPE.RESET, number, new Uint8Array([RESET_CODES[reason]]));
23
+ }
24
+ export function parseEnvelope(bytes) {
25
+ if (bytes.length < HEADER_BYTES)
26
+ return undefined;
27
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
28
+ return { number: view.getUint32(1), payload: bytes.slice(HEADER_BYTES), type: bytes[0] ?? 0 };
29
+ }
30
+ export function isEnvelopeType(type) {
31
+ return type >= ENVELOPE.OPEN && type <= ENVELOPE.RESET;
32
+ }
33
+ export function parseCredit(payload) {
34
+ if (payload.length !== 4)
35
+ return undefined;
36
+ return new DataView(payload.buffer, payload.byteOffset, payload.byteLength).getUint32(0);
37
+ }
38
+ export function parseResetReason(payload) {
39
+ const code = payload[0];
40
+ for (const [reason, value] of Object.entries(RESET_CODES)) {
41
+ if (value === code)
42
+ return reason;
43
+ }
44
+ return undefined;
45
+ }
46
+ // why: counted the way Buffer.byteLength counts, so the link needs no Buffer (a browser has none).
47
+ export function utf8Length(text) {
48
+ let bytes = 0;
49
+ for (let at = 0; at < text.length; at += 1) {
50
+ const code = text.charCodeAt(at);
51
+ if (code < 0x80) {
52
+ bytes += 1;
53
+ }
54
+ else if (code < 0x800) {
55
+ bytes += 2;
56
+ }
57
+ else if (code >= 0xd800 && code <= 0xdbff && isLowSurrogate(text.charCodeAt(at + 1))) {
58
+ bytes += 4;
59
+ at += 1;
60
+ }
61
+ else {
62
+ bytes += 3;
63
+ }
64
+ }
65
+ return bytes;
66
+ }
67
+ function isLowSurrogate(code) {
68
+ return code >= 0xdc00 && code <= 0xdfff;
69
+ }
70
+ export function isChannelId(id) {
71
+ return CHANNEL_ID.test(id);
72
+ }
73
+ export function parseChannelId(payload) {
74
+ const id = decodeAscii(payload);
75
+ return isChannelId(id) ? id : undefined;
76
+ }
77
+ // harness: channel ids are ASCII, so the link needs no TextEncoder or TextDecoder (not every
78
+ // Hermes has TextDecoder). A byte over 0x7f decodes to a character the id pattern refuses.
79
+ export function encodeAscii(text) {
80
+ const bytes = new Uint8Array(text.length);
81
+ for (let at = 0; at < text.length; at += 1)
82
+ bytes[at] = text.charCodeAt(at) & 0xff;
83
+ return bytes;
84
+ }
85
+ function decodeAscii(bytes) {
86
+ let text = "";
87
+ for (let at = 0; at < bytes.length; at += 1)
88
+ text += String.fromCharCode(bytes[at] ?? 0);
89
+ return text;
90
+ }
@@ -0,0 +1,22 @@
1
+ import type { Clock, IpcConnection, Logger, UplinkTransportConnection } from "./ports.js";
2
+ import { type LinkChannel } from "./channel.js";
3
+ export { LinkChannelError } from "./channel.js";
4
+ export type { LinkChannel, LinkChannelErrorReason } from "./channel.js";
5
+ export { globalClock } from "./ports.js";
6
+ export type { Clock, IpcConnection, Logger, TimerHandle, UplinkTransportConnection, } from "./ports.js";
7
+ export { connectStandardWebSocket, StandardWebSocketConnection, type StandardWebSocket, type StandardWebSocketDialOptions, type StandardWebSocketOptions, } from "./standard-websocket.js";
8
+ export interface LinkConnection extends IpcConnection {
9
+ openChannel(options: {
10
+ readonly signal: AbortSignal;
11
+ }): Promise<LinkChannel>;
12
+ acceptChannel(id: string): LinkChannel;
13
+ }
14
+ export interface LinkOptions {
15
+ readonly end: "worker" | "gateway";
16
+ readonly clock: Clock;
17
+ readonly logger: Logger;
18
+ readonly firstChannelNumber?: number;
19
+ readonly generateId?: () => string;
20
+ }
21
+ export declare function createLink(transport: UplinkTransportConnection, options: LinkOptions): LinkConnection;
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/link/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAC1F,OAAO,EAA+C,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAc7F,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,YAAY,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACxE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,YAAY,EACV,KAAK,EACL,aAAa,EACb,MAAM,EACN,WAAW,EACX,yBAAyB,GAC1B,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,wBAAwB,EACxB,2BAA2B,EAC3B,KAAK,iBAAiB,EACtB,KAAK,4BAA4B,EACjC,KAAK,wBAAwB,GAC9B,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,cAAe,SAAQ,aAAa;IACnD,WAAW,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC7E,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,CAAC;CACxC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,EAAE,QAAQ,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,MAAM,CAAC;CACpC;AA+QD,wBAAgB,UAAU,CACxB,SAAS,EAAE,yBAAyB,EACpC,OAAO,EAAE,WAAW,GACnB,cAAc,CAEhB"}
@@ -0,0 +1,271 @@
1
+ import { Channel, LinkChannelError } from "./channel.js";
2
+ import { ENVELOPE, MAX_CHANNEL_NUMBER, isChannelId, isEnvelopeType, parseChannelId, parseCredit, parseEnvelope, parseResetReason, } from "./envelope.js";
3
+ import { Scheduler } from "./scheduler.js";
4
+ export { LinkChannelError } from "./channel.js";
5
+ export { globalClock } from "./ports.js";
6
+ export { connectStandardWebSocket, StandardWebSocketConnection, } from "./standard-websocket.js";
7
+ const OPEN_LIMIT = 64;
8
+ class Link {
9
+ clock;
10
+ logger;
11
+ scheduler;
12
+ #transport;
13
+ #ownParity;
14
+ #generateId;
15
+ #channels = new Map();
16
+ #unaccepted = new Map();
17
+ #waiters = [];
18
+ #dataListeners = new Set();
19
+ #closeListeners = new Set();
20
+ #errorListeners = new Set();
21
+ #pendingText = [];
22
+ #nextOwn;
23
+ #highestPeer = 0;
24
+ #ownOpen = 0;
25
+ #peerOpen = 0;
26
+ #closed = false;
27
+ constructor(transport, options) {
28
+ this.clock = options.clock;
29
+ this.logger = options.logger;
30
+ this.#transport = transport;
31
+ this.#ownParity = options.end === "worker" ? 1 : 0;
32
+ this.#generateId = options.generateId ?? randomId;
33
+ this.#nextOwn = options.firstChannelNumber ?? (options.end === "worker" ? 1 : 2);
34
+ this.scheduler = new Scheduler(transport);
35
+ transport.onData((chunk) => this.#onText(chunk));
36
+ transport.onBinary((bytes) => this.#onBinary(bytes));
37
+ transport.onError((error) => {
38
+ for (const listener of this.#errorListeners)
39
+ listener(error);
40
+ });
41
+ transport.onSent(() => {
42
+ for (const channel of this.#channels.values())
43
+ channel.transportMoved();
44
+ });
45
+ transport.onClose(() => this.#onTransportClosed());
46
+ }
47
+ get closed() {
48
+ return this.#closed || this.#transport.closed;
49
+ }
50
+ unsentBytes() {
51
+ return this.#transport.unsentBytes;
52
+ }
53
+ onData(listener) {
54
+ this.#dataListeners.add(listener);
55
+ if (this.#pendingText.length > 0) {
56
+ for (const chunk of this.#pendingText.splice(0))
57
+ listener(chunk);
58
+ }
59
+ return () => {
60
+ this.#dataListeners.delete(listener);
61
+ };
62
+ }
63
+ onClose(listener) {
64
+ if (this.#closed) {
65
+ listener();
66
+ return () => undefined;
67
+ }
68
+ this.#closeListeners.add(listener);
69
+ return () => {
70
+ this.#closeListeners.delete(listener);
71
+ };
72
+ }
73
+ onError(listener) {
74
+ this.#errorListeners.add(listener);
75
+ return () => {
76
+ this.#errorListeners.delete(listener);
77
+ };
78
+ }
79
+ write(contents) {
80
+ return this.scheduler.text(contents);
81
+ }
82
+ async close() {
83
+ await this.#transport.close();
84
+ this.#onTransportClosed();
85
+ }
86
+ openChannel(options) {
87
+ const { signal } = options;
88
+ if (this.#closed)
89
+ return Promise.reject(new LinkChannelError("closed"));
90
+ if (signal.aborted)
91
+ return Promise.reject(signal.reason);
92
+ if (this.#ownOpen < OPEN_LIMIT && this.#waiters.length === 0) {
93
+ try {
94
+ return Promise.resolve(this.#open().view);
95
+ }
96
+ catch (error) {
97
+ return Promise.reject(error);
98
+ }
99
+ }
100
+ return new Promise((resolve, reject) => {
101
+ const onAbort = () => {
102
+ const at = this.#waiters.findIndex((waiter) => waiter.onAbort === onAbort);
103
+ if (at >= 0)
104
+ this.#waiters.splice(at, 1);
105
+ reject(signal.reason);
106
+ };
107
+ signal.addEventListener("abort", onAbort, { once: true });
108
+ this.#waiters.push({ onAbort, reject, resolve, signal });
109
+ });
110
+ }
111
+ acceptChannel(id) {
112
+ const channel = this.#unaccepted.get(id);
113
+ if (channel === undefined) {
114
+ throw new Error(`channel ${id} was not opened by the peer, or was already accepted or ended`);
115
+ }
116
+ this.#unaccepted.delete(id);
117
+ channel.markAccepted();
118
+ return channel.view;
119
+ }
120
+ channelDone(channel) {
121
+ if (this.#channels.get(channel.number) !== channel)
122
+ return;
123
+ this.#forget(channel);
124
+ this.#drainWaiters();
125
+ }
126
+ #forget(channel) {
127
+ this.#channels.delete(channel.number);
128
+ if (this.#unaccepted.get(channel.id) === channel)
129
+ this.#unaccepted.delete(channel.id);
130
+ if (channel.ownedByUs)
131
+ this.#ownOpen -= 1;
132
+ else
133
+ this.#peerOpen -= 1;
134
+ }
135
+ #open() {
136
+ if (this.#nextOwn > MAX_CHANNEL_NUMBER) {
137
+ this.logger.warn("link: this side has used every channel number on the connection");
138
+ throw new Error("the link has no channel numbers left on this connection");
139
+ }
140
+ let id = this.#newId();
141
+ while (this.#channels.size > 0 && [...this.#channels.values()].some((c) => c.id === id)) {
142
+ id = this.#newId();
143
+ }
144
+ const number = this.#nextOwn;
145
+ this.#nextOwn += 2;
146
+ const channel = new Channel(this, number, id, true);
147
+ this.#channels.set(number, channel);
148
+ this.#ownOpen += 1;
149
+ return channel;
150
+ }
151
+ #newId() {
152
+ const id = this.#generateId();
153
+ if (!isChannelId(id)) {
154
+ throw new Error("link: `generateId` must make ids of 1 to 32 characters from [A-Za-z0-9-]");
155
+ }
156
+ return id;
157
+ }
158
+ #drainWaiters() {
159
+ while (this.#waiters.length > 0 && this.#ownOpen < OPEN_LIMIT && !this.#closed) {
160
+ const waiter = this.#waiters.shift();
161
+ if (waiter === undefined)
162
+ break;
163
+ waiter.signal.removeEventListener("abort", waiter.onAbort);
164
+ try {
165
+ waiter.resolve(this.#open().view);
166
+ }
167
+ catch (error) {
168
+ waiter.reject(error);
169
+ }
170
+ }
171
+ }
172
+ #onText(chunk) {
173
+ if (this.#dataListeners.size === 0) {
174
+ this.#pendingText.push(chunk);
175
+ return;
176
+ }
177
+ for (const listener of this.#dataListeners)
178
+ listener(chunk);
179
+ }
180
+ #onTransportClosed() {
181
+ if (this.#closed)
182
+ return;
183
+ this.#closed = true;
184
+ this.scheduler.close();
185
+ for (const waiter of this.#waiters.splice(0)) {
186
+ waiter.signal.removeEventListener("abort", waiter.onAbort);
187
+ waiter.reject(new LinkChannelError("closed"));
188
+ }
189
+ const error = new LinkChannelError("closed");
190
+ for (const channel of this.#channels.values())
191
+ channel.closeWith(error);
192
+ this.#channels.clear();
193
+ this.#unaccepted.clear();
194
+ for (const listener of this.#closeListeners)
195
+ listener();
196
+ }
197
+ #warn(rule) {
198
+ this.logger.warn(`link: dropped a message from the peer: ${rule}`);
199
+ }
200
+ #onBinary(bytes) {
201
+ const envelope = parseEnvelope(bytes);
202
+ if (envelope === undefined)
203
+ return this.#warn("a message too short to read");
204
+ if (!isEnvelopeType(envelope.type))
205
+ return this.#warn("an unknown envelope type");
206
+ if (envelope.type === ENVELOPE.OPEN)
207
+ return this.#onOpen(envelope.number, envelope.payload);
208
+ const channel = this.#channels.get(envelope.number);
209
+ if (channel === undefined) {
210
+ if (!this.#isLate(envelope.number))
211
+ this.#warn("a channel number the peer never opened");
212
+ return;
213
+ }
214
+ this.#dispatch(channel, envelope);
215
+ }
216
+ #dispatch(channel, envelope) {
217
+ switch (envelope.type) {
218
+ case ENVELOPE.DATA:
219
+ return channel.onData(envelope.payload);
220
+ case ENVELOPE.END:
221
+ return channel.onEnd();
222
+ case ENVELOPE.STOP:
223
+ return channel.onStop();
224
+ case ENVELOPE.CREDIT:
225
+ return channel.onCredit(parseCredit(envelope.payload));
226
+ case ENVELOPE.RESET:
227
+ return channel.onReset(parseResetReason(envelope.payload));
228
+ }
229
+ }
230
+ #isLate(number) {
231
+ if (number < 1)
232
+ return false;
233
+ if (number % 2 === this.#ownParity)
234
+ return number < this.#nextOwn;
235
+ return number <= this.#highestPeer;
236
+ }
237
+ #onOpen(number, payload) {
238
+ if (number % 2 === this.#ownParity || number < 1)
239
+ return this.#warn("an OPEN with the wrong parity");
240
+ if (number <= this.#highestPeer) {
241
+ return this.#warn(this.#channels.has(number)
242
+ ? "an OPEN reusing a live number"
243
+ : "an OPEN reusing an ended number");
244
+ }
245
+ if (this.#peerOpen >= OPEN_LIMIT)
246
+ return this.#warn("a 65th OPEN from the peer");
247
+ const id = parseChannelId(payload);
248
+ if (id === undefined)
249
+ return this.#warn("an OPEN with an id outside [A-Za-z0-9-]{1,32}");
250
+ if (this.#unaccepted.has(id))
251
+ return this.#warn("an OPEN reusing an id not yet accepted");
252
+ this.#highestPeer = number;
253
+ const channel = new Channel(this, number, id, false);
254
+ this.#channels.set(number, channel);
255
+ this.#unaccepted.set(id, channel);
256
+ this.#peerOpen += 1;
257
+ }
258
+ }
259
+ function randomId() {
260
+ const source = globalThis.crypto;
261
+ if (typeof source?.randomUUID === "function")
262
+ return source.randomUUID().replace(/-/g, "");
263
+ if (typeof source?.getRandomValues === "function") {
264
+ const bytes = source.getRandomValues(new Uint8Array(16));
265
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
266
+ }
267
+ throw new Error("link: this runtime has no crypto to make channel ids; pass `generateId`");
268
+ }
269
+ export function createLink(transport, options) {
270
+ return new Link(transport, options);
271
+ }
@@ -0,0 +1,31 @@
1
+ export interface TimerHandle {
2
+ readonly _timerHandle?: never;
3
+ }
4
+ export interface Clock {
5
+ now(): number;
6
+ setTimer(delayMs: number, callback: () => void): TimerHandle;
7
+ cancel(handle: TimerHandle): void;
8
+ }
9
+ export declare const globalClock: Clock;
10
+ export interface Logger {
11
+ debug(message: string, fields?: Record<string, unknown>): void;
12
+ info(message: string, fields?: Record<string, unknown>): void;
13
+ warn(message: string, fields?: Record<string, unknown>): void;
14
+ error(message: string, fields?: Record<string, unknown>): void;
15
+ child(module: string): Logger;
16
+ }
17
+ export interface IpcConnection {
18
+ readonly closed: boolean;
19
+ onData(listener: (chunk: string) => void): () => void;
20
+ onClose(listener: () => void): () => void;
21
+ onError(listener: (error: Error) => void): () => void;
22
+ write(contents: string): Promise<void>;
23
+ close(): Promise<void>;
24
+ }
25
+ export interface UplinkTransportConnection extends IpcConnection {
26
+ writeBinary(bytes: Uint8Array): Promise<void>;
27
+ onBinary(listener: (bytes: Uint8Array) => void): () => void;
28
+ readonly unsentBytes: number;
29
+ onSent(listener: () => void): () => void;
30
+ }
31
+ //# sourceMappingURL=ports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ports.d.ts","sourceRoot":"","sources":["../../src/link/ports.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,YAAY,CAAC,EAAE,KAAK,CAAC;CAC/B;AAED,MAAM,WAAW,KAAK;IACpB,GAAG,IAAI,MAAM,CAAC;IACd,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,WAAW,CAAC;IAC7D,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;CACnC;AAED,eAAO,MAAM,WAAW,EAAE,KAIzB,CAAC;AAEF,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC9D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC9D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/D,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACtD,OAAO,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC1C,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACtD,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,yBAA0B,SAAQ,aAAa;IAC9D,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,MAAM,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;CAC1C"}
@@ -0,0 +1,7 @@
1
+ // The parts of simlock's `src/ports` the link uses. Types only, plus `globalClock`,
2
+ // so the link runs unchanged in Node, a browser, and React Native.
3
+ export const globalClock = {
4
+ cancel: (handle) => clearTimeout(handle),
5
+ now: () => Date.now(),
6
+ setTimer: (delayMs, callback) => setTimeout(callback, delayMs),
7
+ };
@@ -0,0 +1,21 @@
1
+ import type { UplinkTransportConnection } from "./ports.js";
2
+ export interface BulkMessage {
3
+ readonly bytes: Uint8Array;
4
+ onHanded(): void;
5
+ onWritten(): void;
6
+ }
7
+ export interface BulkSource {
8
+ takeNext(): BulkMessage | undefined;
9
+ readonly hasNext: boolean;
10
+ }
11
+ export declare class Scheduler {
12
+ #private;
13
+ constructor(transport: UplinkTransportConnection);
14
+ text(text: string): Promise<void>;
15
+ binary(bytes: Uint8Array, onHanded?: () => void): void;
16
+ addBulk(source: BulkSource): void;
17
+ removeBulk(source: BulkSource): void;
18
+ wake(): void;
19
+ close(): void;
20
+ }
21
+ //# sourceMappingURL=scheduler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/link/scheduler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAM5D,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,IAAI,IAAI,CAAC;IACjB,SAAS,IAAI,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,IAAI,WAAW,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAMD,qBAAa,SAAS;;gBAUR,SAAS,EAAE,yBAAyB;IAKhD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQjC,MAAM,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI;IAQtD,OAAO,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAOjC,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAKpC,IAAI,IAAI,IAAI;IASZ,KAAK,IAAI,IAAI;CA0Dd"}
@@ -0,0 +1,116 @@
1
+ import { KIB, utf8Length } from "./envelope.js";
2
+ const UNSENT_MARK = 256 * KIB;
3
+ const PRIORITY_SHARE = 64 * KIB;
4
+ export class Scheduler {
5
+ #transport;
6
+ #priority = [];
7
+ #ring = [];
8
+ #inRing = new Set();
9
+ #prioritySinceBulk = 0;
10
+ #pumping = false;
11
+ #scheduled = false;
12
+ #closed = false;
13
+ constructor(transport) {
14
+ this.#transport = transport;
15
+ transport.onSent(() => this.wake());
16
+ }
17
+ text(text) {
18
+ if (this.#closed)
19
+ return Promise.resolve();
20
+ return new Promise((resolve) => {
21
+ this.#priority.push({ kind: "text", resolve, text });
22
+ this.wake();
23
+ });
24
+ }
25
+ binary(bytes, onHanded) {
26
+ if (this.#closed)
27
+ return;
28
+ this.#priority.push(onHanded === undefined ? { bytes, kind: "binary" } : { bytes, kind: "binary", onHanded });
29
+ this.wake();
30
+ }
31
+ addBulk(source) {
32
+ if (this.#inRing.has(source))
33
+ return;
34
+ this.#inRing.add(source);
35
+ this.#ring.push(source);
36
+ this.wake();
37
+ }
38
+ removeBulk(source) {
39
+ if (!this.#inRing.delete(source))
40
+ return;
41
+ this.#ring.splice(this.#ring.indexOf(source), 1);
42
+ }
43
+ wake() {
44
+ if (this.#scheduled || this.#closed)
45
+ return;
46
+ this.#scheduled = true;
47
+ queueMicrotask(() => {
48
+ this.#scheduled = false;
49
+ this.#pump();
50
+ });
51
+ }
52
+ close() {
53
+ this.#closed = true;
54
+ for (const message of this.#priority.splice(0)) {
55
+ if (message.kind === "text")
56
+ message.resolve();
57
+ }
58
+ this.#ring.length = 0;
59
+ this.#inRing.clear();
60
+ }
61
+ #pump() {
62
+ if (this.#pumping)
63
+ return;
64
+ this.#pumping = true;
65
+ try {
66
+ while (!this.#closed) {
67
+ const canBulk = this.#ring.length > 0 && this.#transport.unsentBytes < UNSENT_MARK;
68
+ const bulkDue = canBulk && this.#prioritySinceBulk >= PRIORITY_SHARE;
69
+ if (this.#priority.length > 0 && !bulkDue) {
70
+ this.#sendPriority();
71
+ }
72
+ else if (canBulk) {
73
+ this.#sendBulk();
74
+ }
75
+ else {
76
+ break;
77
+ }
78
+ }
79
+ }
80
+ finally {
81
+ this.#pumping = false;
82
+ }
83
+ }
84
+ #sendPriority() {
85
+ const message = this.#priority.shift();
86
+ if (message === undefined)
87
+ return;
88
+ if (message.kind === "text") {
89
+ this.#prioritySinceBulk += utf8Length(message.text);
90
+ const done = () => message.resolve();
91
+ this.#transport.write(message.text).then(done, done);
92
+ return;
93
+ }
94
+ this.#prioritySinceBulk += message.bytes.length;
95
+ message.onHanded?.();
96
+ this.#transport.writeBinary(message.bytes).catch(() => undefined);
97
+ }
98
+ #sendBulk() {
99
+ const source = this.#ring.shift();
100
+ if (source === undefined)
101
+ return;
102
+ const message = source.takeNext();
103
+ if (source.hasNext) {
104
+ this.#ring.push(source);
105
+ }
106
+ else {
107
+ this.#inRing.delete(source);
108
+ }
109
+ if (message === undefined)
110
+ return;
111
+ this.#prioritySinceBulk = 0;
112
+ message.onHanded();
113
+ const written = () => message.onWritten();
114
+ this.#transport.writeBinary(message.bytes).then(written, written);
115
+ }
116
+ }
@@ -0,0 +1,36 @@
1
+ import { type Clock, type UplinkTransportConnection } from "./ports.js";
2
+ export { globalClock } from "./ports.js";
3
+ export interface StandardWebSocket {
4
+ binaryType: string;
5
+ readonly bufferedAmount: number | undefined;
6
+ readonly readyState: number;
7
+ send(data: string | ArrayBufferView): void;
8
+ close(code?: number, reason?: string): void;
9
+ addEventListener(type: "open" | "close" | "error" | "message", listener: never): void;
10
+ removeEventListener(type: "open" | "close" | "error" | "message", listener: never): void;
11
+ }
12
+ export interface StandardWebSocketOptions {
13
+ readonly clock?: Clock;
14
+ readonly pollMs?: number;
15
+ }
16
+ export declare class StandardWebSocketConnection implements UplinkTransportConnection {
17
+ #private;
18
+ constructor(socket: StandardWebSocket, options?: StandardWebSocketOptions);
19
+ get closed(): boolean;
20
+ get unsentBytes(): number;
21
+ onData(listener: (chunk: string) => void): () => void;
22
+ onBinary(listener: (bytes: Uint8Array) => void): () => void;
23
+ onClose(listener: () => void): () => void;
24
+ onError(listener: (error: Error) => void): () => void;
25
+ onSent(listener: () => void): () => void;
26
+ write(contents: string): Promise<void>;
27
+ writeBinary(bytes: Uint8Array): Promise<void>;
28
+ close(): Promise<void>;
29
+ }
30
+ export interface StandardWebSocketDialOptions extends StandardWebSocketOptions {
31
+ readonly url: string;
32
+ readonly protocols?: string | string[];
33
+ readonly createSocket?: (url: string, protocols?: string | string[]) => StandardWebSocket;
34
+ }
35
+ export declare function connectStandardWebSocket(options: StandardWebSocketDialOptions): Promise<UplinkTransportConnection>;
36
+ //# sourceMappingURL=standard-websocket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standard-websocket.d.ts","sourceRoot":"","sources":["../../src/link/standard-websocket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,KAAK,EAAoB,KAAK,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAEvG,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IAEnB,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAAC;IAC3C,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,EAAE,QAAQ,EAAE,KAAK,GAAG,IAAI,CAAC;IACtF,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,EAAE,QAAQ,EAAE,KAAK,GAAG,IAAI,CAAC;CAC1F;AAID,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAaD,qBAAa,2BAA4B,YAAW,yBAAyB;;gBAiB/D,MAAM,EAAE,iBAAiB,EAAE,OAAO,GAAE,wBAA6B;IAoB7E,IAAI,MAAM,IAAI,OAAO,CAEpB;IAMD,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAUrD,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI;IAO3D,OAAO,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAWzC,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI;IAOrD,MAAM,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAOxC,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAItC,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAiF7B;AAED,MAAM,WAAW,4BAA6B,SAAQ,wBAAwB;IAC5E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACvC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,iBAAiB,CAAC;CAC3F;AAED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,yBAAyB,CAAC,CA2BpC"}