@react-native-harness/bridge 1.4.1 → 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.
- package/dist/binary.d.ts +16 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +47 -0
- package/dist/link/channel.d.ts +45 -0
- package/dist/link/channel.d.ts.map +1 -0
- package/dist/link/channel.js +414 -0
- package/dist/link/envelope.d.ts +38 -0
- package/dist/link/envelope.d.ts.map +1 -0
- package/dist/link/envelope.js +90 -0
- package/dist/link/index.d.ts +22 -0
- package/dist/link/index.d.ts.map +1 -0
- package/dist/link/index.js +271 -0
- package/dist/link/ports.d.ts +31 -0
- package/dist/link/ports.d.ts.map +1 -0
- package/dist/link/ports.js +7 -0
- package/dist/link/scheduler.d.ts +21 -0
- package/dist/link/scheduler.d.ts.map +1 -0
- package/dist/link/scheduler.js +116 -0
- package/dist/link/standard-websocket.d.ts +36 -0
- package/dist/link/standard-websocket.d.ts.map +1 -0
- package/dist/link/standard-websocket.js +214 -0
- package/dist/link/streams.d.ts +4 -0
- package/dist/link/streams.d.ts.map +1 -0
- package/dist/link/streams.js +15 -0
- package/dist/shared.d.ts +1 -1
- package/dist/shared.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/shared.ts +1 -1
|
@@ -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"}
|