@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
package/dist/binary.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LinkConnection } from './link/index.js';
|
|
2
|
+
export type OutgoingBinary = {
|
|
3
|
+
channelId: string;
|
|
4
|
+
/** Settles once every byte is handed to the link, or rejects if the channel fails. */
|
|
5
|
+
written: Promise<void>;
|
|
6
|
+
abort: (reason?: string) => void;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Opens a link channel and starts writing `data` into it. The channel id travels in an
|
|
10
|
+
* RPC message; the peer reads the bytes with `receiveBinary`. The link sends the channel's
|
|
11
|
+
* OPEN before any later message, so the peer always knows the id by the time it asks.
|
|
12
|
+
*/
|
|
13
|
+
export declare const sendBinary: (link: LinkConnection, data: Uint8Array) => Promise<OutgoingBinary>;
|
|
14
|
+
/** Accepts the channel the peer opened with `sendBinary` and reads it to the end. */
|
|
15
|
+
export declare const receiveBinary: (link: LinkConnection, channelId: string) => Promise<Uint8Array>;
|
|
16
|
+
//# sourceMappingURL=binary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binary.d.ts","sourceRoot":"","sources":["../src/binary.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEnE,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,sFAAsF;IACtF,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,UAAU,GACrB,MAAM,cAAc,EACpB,MAAM,UAAU,KACf,OAAO,CAAC,cAAc,CAgBxB,CAAC;AA6BF,qFAAqF;AACrF,eAAO,MAAM,aAAa,GACxB,MAAM,cAAc,EACpB,WAAW,MAAM,KAChB,OAAO,CAAC,UAAU,CAOpB,CAAC"}
|
package/dist/binary.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opens a link channel and starts writing `data` into it. The channel id travels in an
|
|
3
|
+
* RPC message; the peer reads the bytes with `receiveBinary`. The link sends the channel's
|
|
4
|
+
* OPEN before any later message, so the peer always knows the id by the time it asks.
|
|
5
|
+
*/
|
|
6
|
+
export const sendBinary = async (link, data) => {
|
|
7
|
+
const channel = await link.openChannel({
|
|
8
|
+
signal: new AbortController().signal,
|
|
9
|
+
});
|
|
10
|
+
const written = (async () => {
|
|
11
|
+
const writer = channel.writable.getWriter();
|
|
12
|
+
await writer.write(data);
|
|
13
|
+
await writer.close();
|
|
14
|
+
})();
|
|
15
|
+
return {
|
|
16
|
+
channelId: channel.id,
|
|
17
|
+
written,
|
|
18
|
+
abort: (reason) => channel.abort(reason),
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
const readAll = async (channel) => {
|
|
22
|
+
const reader = channel.readable.getReader();
|
|
23
|
+
const chunks = [];
|
|
24
|
+
let size = 0;
|
|
25
|
+
for (;;) {
|
|
26
|
+
const { done, value } = await reader.read();
|
|
27
|
+
if (done) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
chunks.push(value);
|
|
31
|
+
size += value.length;
|
|
32
|
+
}
|
|
33
|
+
const bytes = new Uint8Array(size);
|
|
34
|
+
let offset = 0;
|
|
35
|
+
for (const chunk of chunks) {
|
|
36
|
+
bytes.set(chunk, offset);
|
|
37
|
+
offset += chunk.length;
|
|
38
|
+
}
|
|
39
|
+
return bytes;
|
|
40
|
+
};
|
|
41
|
+
/** Accepts the channel the peer opened with `sendBinary` and reads it to the end. */
|
|
42
|
+
export const receiveBinary = async (link, channelId) => {
|
|
43
|
+
const channel = link.acceptChannel(channelId);
|
|
44
|
+
// Nothing flows back to the sender; ending this direction lets the channel free its slot.
|
|
45
|
+
void channel.writable.close().catch(() => undefined);
|
|
46
|
+
return readAll(channel);
|
|
47
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Clock, Logger } from "./ports.js";
|
|
2
|
+
import { type ResetReason } from "./envelope.js";
|
|
3
|
+
import type { BulkMessage, BulkSource, Scheduler } from "./scheduler.js";
|
|
4
|
+
export type LinkChannelErrorReason = "aborted" | "invalid" | "not-accepted" | "stalled" | "closed" | "stopped";
|
|
5
|
+
export declare class LinkChannelError extends Error {
|
|
6
|
+
readonly reason: LinkChannelErrorReason;
|
|
7
|
+
constructor(reason: LinkChannelErrorReason, message?: string);
|
|
8
|
+
}
|
|
9
|
+
export interface LinkChannel {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly readable: ReadableStream<Uint8Array>;
|
|
12
|
+
readonly writable: WritableStream<Uint8Array>;
|
|
13
|
+
abort(reason?: string): void;
|
|
14
|
+
readonly credit: number;
|
|
15
|
+
whenCredit(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export interface ChannelHost {
|
|
18
|
+
readonly clock: Clock;
|
|
19
|
+
readonly logger: Logger;
|
|
20
|
+
readonly scheduler: Scheduler;
|
|
21
|
+
unsentBytes(): number;
|
|
22
|
+
channelDone(channel: Channel): void;
|
|
23
|
+
}
|
|
24
|
+
export declare class Channel implements BulkSource {
|
|
25
|
+
#private;
|
|
26
|
+
readonly number: number;
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly ownedByUs: boolean;
|
|
29
|
+
accepted: boolean;
|
|
30
|
+
readonly view: LinkChannel;
|
|
31
|
+
constructor(host: ChannelHost, number: number, id: string, ownedByUs: boolean);
|
|
32
|
+
get hasNext(): boolean;
|
|
33
|
+
get credit(): number;
|
|
34
|
+
markAccepted(): void;
|
|
35
|
+
takeNext(): BulkMessage | undefined;
|
|
36
|
+
transportMoved(): void;
|
|
37
|
+
onData(payload: Uint8Array): void;
|
|
38
|
+
onEnd(): void;
|
|
39
|
+
onStop(): void;
|
|
40
|
+
onCredit(amount: number | undefined): void;
|
|
41
|
+
onReset(reason: ResetReason | undefined): void;
|
|
42
|
+
closeWith(error: LinkChannelError): void;
|
|
43
|
+
abort(reason?: string): void;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/link/channel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAe,MAAM,YAAY,CAAC;AAC7D,OAAO,EASL,KAAK,WAAW,EACjB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAOzE,MAAM,MAAM,sBAAsB,GAC9B,SAAS,GACT,SAAS,GACT,cAAc,GACd,SAAS,GACT,QAAQ,GACR,SAAS,CAAC;AAEd,qBAAa,gBAAiB,SAAQ,KAAK;IAEvC,QAAQ,CAAC,MAAM,EAAE,sBAAsB;gBAA9B,MAAM,EAAE,sBAAsB,EACvC,OAAO,CAAC,EAAE,MAAM;CAKnB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IAC9C,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,WAAW,IAAI,MAAM,CAAC;IACtB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACrC;AAaD,qBAAa,OAAQ,YAAW,UAAU;;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;gBAkCf,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO;IAwB7E,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,MAAM,IAAI,MAAM,CAGnB;IAED,YAAY,IAAI,IAAI;IAQpB,QAAQ,IAAI,WAAW,GAAG,SAAS;IAwBnC,cAAc,IAAI,IAAI;IAItB,MAAM,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAcjC,KAAK,IAAI,IAAI;IAab,MAAM,IAAI,IAAI;IAUd,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAW1C,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI;IAW9C,SAAS,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAMxC,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;CA0O7B"}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { CHANNEL_WINDOW, ENVELOPE, KIB, MAX_DATA_BYTES, encodeCredit, encodeEnvelope, encodeAscii, encodeReset, } from "./envelope.js";
|
|
2
|
+
import { LinkByteLengthQueuingStrategy, LinkReadableStream, LinkWritableStream, } from "./streams.js";
|
|
3
|
+
export class LinkChannelError extends Error {
|
|
4
|
+
reason;
|
|
5
|
+
constructor(reason, message) {
|
|
6
|
+
super(message ?? reason);
|
|
7
|
+
this.reason = reason;
|
|
8
|
+
this.name = "LinkChannelError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const ACCEPT_WAIT_MS = 30_000;
|
|
12
|
+
const STALL_WAIT_MS = 60_000;
|
|
13
|
+
const WRITER_BUFFER = 64 * KIB;
|
|
14
|
+
export class Channel {
|
|
15
|
+
number;
|
|
16
|
+
id;
|
|
17
|
+
ownedByUs;
|
|
18
|
+
accepted;
|
|
19
|
+
view;
|
|
20
|
+
#host;
|
|
21
|
+
#failure;
|
|
22
|
+
#done = false;
|
|
23
|
+
#openHanded;
|
|
24
|
+
#queue = [];
|
|
25
|
+
#queuedBytes = 0;
|
|
26
|
+
#allowance = CHANNEL_WINDOW;
|
|
27
|
+
#handed = 0;
|
|
28
|
+
#inFlight = 0;
|
|
29
|
+
#endQueued = false;
|
|
30
|
+
#endHanded = false;
|
|
31
|
+
#stoppedByPeer = false;
|
|
32
|
+
#pendingWrite;
|
|
33
|
+
#creditWaiters = [];
|
|
34
|
+
#stallTimer;
|
|
35
|
+
#acceptTimer;
|
|
36
|
+
#received = 0;
|
|
37
|
+
#granted = 0;
|
|
38
|
+
#readTotal = 0;
|
|
39
|
+
#inQueue = [];
|
|
40
|
+
#inEnded = false;
|
|
41
|
+
#stopSent = false;
|
|
42
|
+
#wantRead = false;
|
|
43
|
+
#readableController;
|
|
44
|
+
#writableController;
|
|
45
|
+
#resetQueued = false;
|
|
46
|
+
#resetHanded = false;
|
|
47
|
+
#resetReceived = false;
|
|
48
|
+
constructor(host, number, id, ownedByUs) {
|
|
49
|
+
this.#host = host;
|
|
50
|
+
this.number = number;
|
|
51
|
+
this.id = id;
|
|
52
|
+
this.ownedByUs = ownedByUs;
|
|
53
|
+
this.accepted = ownedByUs;
|
|
54
|
+
this.#openHanded = !ownedByUs;
|
|
55
|
+
this.view = this.#buildView();
|
|
56
|
+
if (ownedByUs) {
|
|
57
|
+
host.scheduler.binary(encodeEnvelope(ENVELOPE.OPEN, number, encodeAscii(id)), () => {
|
|
58
|
+
this.#openHanded = true;
|
|
59
|
+
if (this.hasNext)
|
|
60
|
+
host.scheduler.addBulk(this);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
this.#acceptTimer = host.clock.setTimer(ACCEPT_WAIT_MS, () => {
|
|
65
|
+
this.#acceptTimer = undefined;
|
|
66
|
+
this.#fail(new LinkChannelError("not-accepted"), "not-accepted");
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
get hasNext() {
|
|
71
|
+
return this.#queue.length > 0;
|
|
72
|
+
}
|
|
73
|
+
get credit() {
|
|
74
|
+
if (this.#failure !== undefined || this.#endQueued || this.#stoppedByPeer)
|
|
75
|
+
return 0;
|
|
76
|
+
return Math.max(0, this.#allowance - this.#handed - this.#queuedBytes);
|
|
77
|
+
}
|
|
78
|
+
markAccepted() {
|
|
79
|
+
this.accepted = true;
|
|
80
|
+
if (this.#acceptTimer !== undefined) {
|
|
81
|
+
this.#host.clock.cancel(this.#acceptTimer);
|
|
82
|
+
this.#acceptTimer = undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
takeNext() {
|
|
86
|
+
const item = this.#queue.shift();
|
|
87
|
+
if (item === undefined)
|
|
88
|
+
return undefined;
|
|
89
|
+
return {
|
|
90
|
+
bytes: item.bytes,
|
|
91
|
+
onHanded: () => {
|
|
92
|
+
if (item.end) {
|
|
93
|
+
this.#endHanded = true;
|
|
94
|
+
this.#checkDone();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.#queuedBytes -= item.payload;
|
|
98
|
+
this.#handed += item.payload;
|
|
99
|
+
this.#inFlight += 1;
|
|
100
|
+
this.#updateStall();
|
|
101
|
+
},
|
|
102
|
+
onWritten: () => {
|
|
103
|
+
if (item.end)
|
|
104
|
+
return;
|
|
105
|
+
this.#inFlight -= 1;
|
|
106
|
+
this.#updateStall();
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
transportMoved() {
|
|
111
|
+
this.#updateStall();
|
|
112
|
+
}
|
|
113
|
+
onData(payload) {
|
|
114
|
+
if (this.#failure !== undefined)
|
|
115
|
+
return;
|
|
116
|
+
if (this.#inEnded)
|
|
117
|
+
return this.#invalid("DATA after the direction ended");
|
|
118
|
+
if (this.#stopSent)
|
|
119
|
+
return;
|
|
120
|
+
if (payload.length > MAX_DATA_BYTES)
|
|
121
|
+
return this.#invalid("DATA over 64 KiB");
|
|
122
|
+
if (this.#received + payload.length > CHANNEL_WINDOW + this.#granted) {
|
|
123
|
+
return this.#invalid("DATA past the allowed bytes");
|
|
124
|
+
}
|
|
125
|
+
if (payload.length === 0)
|
|
126
|
+
return;
|
|
127
|
+
this.#received += payload.length;
|
|
128
|
+
this.#inQueue.push(payload);
|
|
129
|
+
this.#deliver();
|
|
130
|
+
}
|
|
131
|
+
onEnd() {
|
|
132
|
+
if (this.#failure !== undefined) {
|
|
133
|
+
// why: the peer's END still ends its direction after this side failed; the slot frees once both are done.
|
|
134
|
+
this.#inEnded = true;
|
|
135
|
+
this.#checkDone();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (this.#inEnded)
|
|
139
|
+
return this.#invalid("END after the direction ended");
|
|
140
|
+
this.#inEnded = true;
|
|
141
|
+
this.#deliver();
|
|
142
|
+
this.#checkDone();
|
|
143
|
+
}
|
|
144
|
+
onStop() {
|
|
145
|
+
if (this.#failure !== undefined)
|
|
146
|
+
return;
|
|
147
|
+
if (this.#stoppedByPeer)
|
|
148
|
+
return this.#invalid("a second STOP for one direction");
|
|
149
|
+
this.#dropQueuedData();
|
|
150
|
+
if (this.#endQueued)
|
|
151
|
+
return;
|
|
152
|
+
this.#stoppedByPeer = true;
|
|
153
|
+
this.#queueEnd();
|
|
154
|
+
this.#failOutgoing(new LinkChannelError("stopped"));
|
|
155
|
+
}
|
|
156
|
+
onCredit(amount) {
|
|
157
|
+
if (this.#failure !== undefined)
|
|
158
|
+
return;
|
|
159
|
+
if (amount === undefined || amount < 1)
|
|
160
|
+
return this.#invalid("a CREDIT of 0");
|
|
161
|
+
const taken = this.#handed + this.#queuedBytes;
|
|
162
|
+
if (this.#allowance + amount > taken + CHANNEL_WINDOW) {
|
|
163
|
+
return this.#invalid("credit beyond 4 MiB outstanding");
|
|
164
|
+
}
|
|
165
|
+
this.#allowance += amount;
|
|
166
|
+
this.#wakeWriters();
|
|
167
|
+
}
|
|
168
|
+
onReset(reason) {
|
|
169
|
+
this.#resetReceived = true;
|
|
170
|
+
if (this.#failure === undefined) {
|
|
171
|
+
this.#fail(new LinkChannelError(reason ?? "aborted"), this.#resetQueued ? undefined : "aborted");
|
|
172
|
+
}
|
|
173
|
+
this.#checkDone();
|
|
174
|
+
}
|
|
175
|
+
closeWith(error) {
|
|
176
|
+
this.#resetQueued = true;
|
|
177
|
+
this.#done = true;
|
|
178
|
+
this.#fail(error, undefined);
|
|
179
|
+
}
|
|
180
|
+
abort(reason) {
|
|
181
|
+
if (this.#failure !== undefined)
|
|
182
|
+
return;
|
|
183
|
+
this.#fail(new LinkChannelError("aborted", reason), "aborted");
|
|
184
|
+
}
|
|
185
|
+
#invalid(rule) {
|
|
186
|
+
this.#host.logger.warn(`link: channel ${this.id} broke a rule: ${rule}`);
|
|
187
|
+
this.#fail(new LinkChannelError("invalid", rule), "invalid");
|
|
188
|
+
}
|
|
189
|
+
#fail(error, send) {
|
|
190
|
+
if (this.#failure !== undefined)
|
|
191
|
+
return;
|
|
192
|
+
this.#failure = error;
|
|
193
|
+
this.#clearTimers();
|
|
194
|
+
this.#host.scheduler.removeBulk(this);
|
|
195
|
+
this.#queue = [];
|
|
196
|
+
this.#queuedBytes = 0;
|
|
197
|
+
this.#inQueue = [];
|
|
198
|
+
this.#wantRead = false;
|
|
199
|
+
this.#failOutgoing(error);
|
|
200
|
+
try {
|
|
201
|
+
this.#readableController?.error(error);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// why: the readable may already be closed or cancelled, and a stream cannot be errored twice.
|
|
205
|
+
}
|
|
206
|
+
if (send !== undefined && !this.#resetQueued) {
|
|
207
|
+
this.#resetQueued = true;
|
|
208
|
+
this.#host.scheduler.binary(encodeReset(this.number, send), () => {
|
|
209
|
+
this.#resetHanded = true;
|
|
210
|
+
this.#checkDone();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
this.#checkDone();
|
|
214
|
+
}
|
|
215
|
+
#failOutgoing(error) {
|
|
216
|
+
this.#clearStall();
|
|
217
|
+
const pending = this.#pendingWrite;
|
|
218
|
+
this.#pendingWrite = undefined;
|
|
219
|
+
pending?.reject(error);
|
|
220
|
+
for (const waiter of this.#creditWaiters.splice(0))
|
|
221
|
+
waiter.reject(error);
|
|
222
|
+
try {
|
|
223
|
+
this.#writableController?.error(error);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// why: the writable may already be closed or errored.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
#checkDone() {
|
|
230
|
+
if (this.#done)
|
|
231
|
+
return;
|
|
232
|
+
const ours = this.#resetHanded || this.#endHanded;
|
|
233
|
+
const theirs = this.#resetReceived || this.#inEnded;
|
|
234
|
+
if (!ours || !theirs)
|
|
235
|
+
return;
|
|
236
|
+
this.#done = true;
|
|
237
|
+
this.#clearTimers();
|
|
238
|
+
this.#host.scheduler.removeBulk(this);
|
|
239
|
+
this.#host.channelDone(this);
|
|
240
|
+
}
|
|
241
|
+
#clearTimers() {
|
|
242
|
+
this.#clearStall();
|
|
243
|
+
if (this.#acceptTimer !== undefined) {
|
|
244
|
+
this.#host.clock.cancel(this.#acceptTimer);
|
|
245
|
+
this.#acceptTimer = undefined;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
#dropQueuedData() {
|
|
249
|
+
this.#queue = this.#queue.filter((item) => item.end);
|
|
250
|
+
this.#queuedBytes = 0;
|
|
251
|
+
if (this.#queue.length === 0)
|
|
252
|
+
this.#host.scheduler.removeBulk(this);
|
|
253
|
+
}
|
|
254
|
+
#queueEnd() {
|
|
255
|
+
this.#endQueued = true;
|
|
256
|
+
this.#queue.push({ bytes: encodeEnvelope(ENVELOPE.END, this.number), end: true, payload: 0 });
|
|
257
|
+
if (this.#openHanded)
|
|
258
|
+
this.#host.scheduler.addBulk(this);
|
|
259
|
+
}
|
|
260
|
+
#take(chunk) {
|
|
261
|
+
for (let offset = 0; offset < chunk.length; offset += MAX_DATA_BYTES) {
|
|
262
|
+
const piece = chunk.subarray(offset, Math.min(chunk.length, offset + MAX_DATA_BYTES));
|
|
263
|
+
this.#queue.push({
|
|
264
|
+
bytes: encodeEnvelope(ENVELOPE.DATA, this.number, piece),
|
|
265
|
+
end: false,
|
|
266
|
+
payload: piece.length,
|
|
267
|
+
});
|
|
268
|
+
this.#queuedBytes += piece.length;
|
|
269
|
+
}
|
|
270
|
+
if (this.#openHanded)
|
|
271
|
+
this.#host.scheduler.addBulk(this);
|
|
272
|
+
}
|
|
273
|
+
#wakeWriters() {
|
|
274
|
+
const pending = this.#pendingWrite;
|
|
275
|
+
this.#pendingWrite = undefined;
|
|
276
|
+
pending?.resolve();
|
|
277
|
+
for (const waiter of this.#creditWaiters.splice(0))
|
|
278
|
+
waiter.resolve();
|
|
279
|
+
this.#updateStall();
|
|
280
|
+
}
|
|
281
|
+
#updateStall() {
|
|
282
|
+
const waiting = this.#pendingWrite !== undefined || this.#creditWaiters.length > 0;
|
|
283
|
+
const peerHoldsEverything = this.#queuedBytes === 0 && (this.#host.unsentBytes() === 0 || this.#inFlight === 0);
|
|
284
|
+
if (!waiting || !peerHoldsEverything || this.#failure !== undefined) {
|
|
285
|
+
this.#clearStall();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (this.#stallTimer !== undefined)
|
|
289
|
+
return;
|
|
290
|
+
this.#stallTimer = this.#host.clock.setTimer(STALL_WAIT_MS, () => {
|
|
291
|
+
this.#stallTimer = undefined;
|
|
292
|
+
this.#fail(new LinkChannelError("stalled"), "stalled");
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
#clearStall() {
|
|
296
|
+
if (this.#stallTimer === undefined)
|
|
297
|
+
return;
|
|
298
|
+
this.#host.clock.cancel(this.#stallTimer);
|
|
299
|
+
this.#stallTimer = undefined;
|
|
300
|
+
}
|
|
301
|
+
#deliver() {
|
|
302
|
+
const controller = this.#readableController;
|
|
303
|
+
if (controller === undefined || !this.#wantRead || this.#stopSent)
|
|
304
|
+
return;
|
|
305
|
+
const chunk = this.#inQueue.shift();
|
|
306
|
+
if (chunk !== undefined) {
|
|
307
|
+
this.#wantRead = false;
|
|
308
|
+
this.#readTotal += chunk.length;
|
|
309
|
+
controller.enqueue(chunk);
|
|
310
|
+
this.#grant();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (this.#inEnded) {
|
|
314
|
+
this.#wantRead = false;
|
|
315
|
+
controller.close();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
#grant() {
|
|
319
|
+
const unread = this.#readTotal - this.#granted;
|
|
320
|
+
if (unread <= 0 || this.#inEnded)
|
|
321
|
+
return;
|
|
322
|
+
if (unread < 64 * KIB && this.#inQueue.length > 0)
|
|
323
|
+
return;
|
|
324
|
+
this.#granted += unread;
|
|
325
|
+
this.#host.scheduler.binary(encodeCredit(this.number, unread));
|
|
326
|
+
}
|
|
327
|
+
#cancelReadable() {
|
|
328
|
+
if (this.#stopSent || this.#failure !== undefined)
|
|
329
|
+
return;
|
|
330
|
+
this.#inQueue = [];
|
|
331
|
+
this.#wantRead = false;
|
|
332
|
+
if (this.#inEnded)
|
|
333
|
+
return;
|
|
334
|
+
this.#stopSent = true;
|
|
335
|
+
this.#host.scheduler.binary(encodeEnvelope(ENVELOPE.STOP, this.number));
|
|
336
|
+
}
|
|
337
|
+
#buildView() {
|
|
338
|
+
const readable = new LinkReadableStream({
|
|
339
|
+
start: (controller) => {
|
|
340
|
+
this.#readableController = controller;
|
|
341
|
+
},
|
|
342
|
+
pull: () => {
|
|
343
|
+
this.#wantRead = true;
|
|
344
|
+
this.#deliver();
|
|
345
|
+
},
|
|
346
|
+
cancel: () => this.#cancelReadable(),
|
|
347
|
+
}, { highWaterMark: 0 });
|
|
348
|
+
const writable = new LinkWritableStream({
|
|
349
|
+
start: (controller) => {
|
|
350
|
+
this.#writableController = controller;
|
|
351
|
+
},
|
|
352
|
+
write: (chunk) => this.#write(chunk),
|
|
353
|
+
close: () => {
|
|
354
|
+
if (this.#failure !== undefined)
|
|
355
|
+
throw this.#failure;
|
|
356
|
+
if (this.#endQueued)
|
|
357
|
+
return;
|
|
358
|
+
this.#queueEnd();
|
|
359
|
+
const error = new LinkChannelError("closed", "the channel's sending direction ended");
|
|
360
|
+
for (const waiter of this.#creditWaiters.splice(0))
|
|
361
|
+
waiter.reject(error);
|
|
362
|
+
this.#updateStall();
|
|
363
|
+
},
|
|
364
|
+
abort: (reason) => {
|
|
365
|
+
this.abort(reason === undefined ? undefined : String(reason));
|
|
366
|
+
},
|
|
367
|
+
}, new LinkByteLengthQueuingStrategy({ highWaterMark: WRITER_BUFFER }));
|
|
368
|
+
const currentCredit = () => this.credit;
|
|
369
|
+
return {
|
|
370
|
+
abort: (reason) => this.abort(reason),
|
|
371
|
+
get credit() {
|
|
372
|
+
return currentCredit();
|
|
373
|
+
},
|
|
374
|
+
id: this.id,
|
|
375
|
+
readable,
|
|
376
|
+
whenCredit: () => this.#whenCredit(),
|
|
377
|
+
writable,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
async #write(chunk) {
|
|
381
|
+
let offset = 0;
|
|
382
|
+
while (offset < chunk.length) {
|
|
383
|
+
if (this.#failure !== undefined)
|
|
384
|
+
throw this.#failure;
|
|
385
|
+
if (this.#stoppedByPeer)
|
|
386
|
+
throw new LinkChannelError("stopped");
|
|
387
|
+
const n = Math.min(this.credit, chunk.length - offset);
|
|
388
|
+
if (n > 0) {
|
|
389
|
+
this.#take(chunk.subarray(offset, offset + n));
|
|
390
|
+
offset += n;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
await new Promise((resolve, reject) => {
|
|
394
|
+
this.#pendingWrite = { reject, resolve };
|
|
395
|
+
this.#updateStall();
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
#whenCredit() {
|
|
400
|
+
if (this.#failure !== undefined)
|
|
401
|
+
return Promise.reject(this.#failure);
|
|
402
|
+
if (this.#stoppedByPeer)
|
|
403
|
+
return Promise.reject(new LinkChannelError("stopped"));
|
|
404
|
+
if (this.#endQueued) {
|
|
405
|
+
return Promise.reject(new LinkChannelError("closed", "the channel's sending direction ended"));
|
|
406
|
+
}
|
|
407
|
+
if (this.credit > 0)
|
|
408
|
+
return Promise.resolve();
|
|
409
|
+
return new Promise((resolve, reject) => {
|
|
410
|
+
this.#creditWaiters.push({ reject, resolve });
|
|
411
|
+
this.#updateStall();
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const KIB = 1024;
|
|
2
|
+
export declare const MAX_DATA_BYTES: number;
|
|
3
|
+
export declare const CHANNEL_WINDOW: number;
|
|
4
|
+
export declare const MAX_CHANNEL_NUMBER = 4294967295;
|
|
5
|
+
export declare const ENVELOPE: {
|
|
6
|
+
readonly OPEN: 1;
|
|
7
|
+
readonly DATA: 2;
|
|
8
|
+
readonly END: 3;
|
|
9
|
+
readonly STOP: 4;
|
|
10
|
+
readonly CREDIT: 5;
|
|
11
|
+
readonly RESET: 6;
|
|
12
|
+
};
|
|
13
|
+
export type EnvelopeType = (typeof ENVELOPE)[keyof typeof ENVELOPE];
|
|
14
|
+
declare const RESET_CODES: {
|
|
15
|
+
readonly aborted: 1;
|
|
16
|
+
readonly stalled: 2;
|
|
17
|
+
readonly "not-accepted": 3;
|
|
18
|
+
readonly invalid: 4;
|
|
19
|
+
};
|
|
20
|
+
export type ResetReason = keyof typeof RESET_CODES;
|
|
21
|
+
export interface ParsedEnvelope {
|
|
22
|
+
readonly type: number;
|
|
23
|
+
readonly number: number;
|
|
24
|
+
readonly payload: Uint8Array;
|
|
25
|
+
}
|
|
26
|
+
export declare function encodeEnvelope(type: EnvelopeType, number: number, payload?: Uint8Array): Uint8Array;
|
|
27
|
+
export declare function encodeCredit(number: number, amount: number): Uint8Array;
|
|
28
|
+
export declare function encodeReset(number: number, reason: ResetReason): Uint8Array;
|
|
29
|
+
export declare function parseEnvelope(bytes: Uint8Array): ParsedEnvelope | undefined;
|
|
30
|
+
export declare function isEnvelopeType(type: number): type is EnvelopeType;
|
|
31
|
+
export declare function parseCredit(payload: Uint8Array): number | undefined;
|
|
32
|
+
export declare function parseResetReason(payload: Uint8Array): ResetReason | undefined;
|
|
33
|
+
export declare function utf8Length(text: string): number;
|
|
34
|
+
export declare function isChannelId(id: string): boolean;
|
|
35
|
+
export declare function parseChannelId(payload: Uint8Array): string | undefined;
|
|
36
|
+
export declare function encodeAscii(text: string): Uint8Array;
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=envelope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope.d.ts","sourceRoot":"","sources":["../../src/link/envelope.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,GAAG,OAAO,CAAC;AACxB,eAAO,MAAM,cAAc,QAAW,CAAC;AACvC,eAAO,MAAM,cAAc,QAAgB,CAAC;AAC5C,eAAO,MAAM,kBAAkB,aAAa,CAAC;AAE7C,eAAO,MAAM,QAAQ;;;;;;;CAAsE,CAAC;AAC5F,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC,MAAM,OAAO,QAAQ,CAAC,CAAC;AAEpE,QAAA,MAAM,WAAW;;;;;CAAqE,CAAC;AACvF,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,WAAW,CAAC;AAKnD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;CAC9B;AAED,wBAAgB,cAAc,CAC5B,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,UAA8B,GACtC,UAAU,CAMZ;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAIvE;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,UAAU,CAE3E;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,cAAc,GAAG,SAAS,CAI3E;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,YAAY,CAEjE;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAGnE;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,UAAU,GAAG,WAAW,GAAG,SAAS,CAM7E;AAGD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAgB/C;AAMD,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAGtE;AAID,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAIpD"}
|
|
@@ -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
|