@react-native-harness/bridge 1.5.0 → 1.7.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.
Files changed (104) hide show
  1. package/dist/app-liveness.d.ts +33 -0
  2. package/dist/app-liveness.d.ts.map +1 -0
  3. package/dist/app-liveness.js +57 -0
  4. package/dist/binary.d.ts +16 -0
  5. package/dist/binary.d.ts.map +1 -0
  6. package/dist/binary.js +47 -0
  7. package/dist/client.d.ts +6 -4
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +112 -105
  10. package/dist/connection-check.d.ts +22 -0
  11. package/dist/connection-check.d.ts.map +1 -0
  12. package/dist/connection-check.js +30 -0
  13. package/dist/errors.d.ts +6 -2
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +12 -4
  16. package/dist/image-snapshot.d.ts +2 -2
  17. package/dist/image-snapshot.d.ts.map +1 -1
  18. package/dist/image-snapshot.js +21 -8
  19. package/dist/index.d.ts +0 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +0 -2
  22. package/dist/js-block-budget-policy.d.ts +31 -0
  23. package/dist/js-block-budget-policy.d.ts.map +1 -0
  24. package/dist/js-block-budget-policy.js +60 -0
  25. package/dist/js-watchdog.d.ts +31 -0
  26. package/dist/js-watchdog.d.ts.map +1 -0
  27. package/dist/js-watchdog.js +46 -0
  28. package/dist/link/channel.d.ts +45 -0
  29. package/dist/link/channel.d.ts.map +1 -0
  30. package/dist/link/channel.js +414 -0
  31. package/dist/link/envelope.d.ts +38 -0
  32. package/dist/link/envelope.d.ts.map +1 -0
  33. package/dist/link/envelope.js +90 -0
  34. package/dist/link/index.d.ts +22 -0
  35. package/dist/link/index.d.ts.map +1 -0
  36. package/dist/link/index.js +271 -0
  37. package/dist/link/ports.d.ts +31 -0
  38. package/dist/link/ports.d.ts.map +1 -0
  39. package/dist/link/ports.js +7 -0
  40. package/dist/link/scheduler.d.ts +21 -0
  41. package/dist/link/scheduler.d.ts.map +1 -0
  42. package/dist/link/scheduler.js +116 -0
  43. package/dist/link/standard-websocket.d.ts +36 -0
  44. package/dist/link/standard-websocket.d.ts.map +1 -0
  45. package/dist/link/standard-websocket.js +214 -0
  46. package/dist/link/streams.d.ts +4 -0
  47. package/dist/link/streams.d.ts.map +1 -0
  48. package/dist/link/streams.js +15 -0
  49. package/dist/protocol.d.ts +10 -5
  50. package/dist/protocol.d.ts.map +1 -1
  51. package/dist/protocol.js +2 -2
  52. package/dist/rpc-peer.d.ts +3 -1
  53. package/dist/rpc-peer.d.ts.map +1 -1
  54. package/dist/rpc-peer.js +2 -2
  55. package/dist/server.d.ts +4 -0
  56. package/dist/server.d.ts.map +1 -1
  57. package/dist/server.js +71 -56
  58. package/dist/shared/test-runner.d.ts +11 -0
  59. package/dist/shared/test-runner.d.ts.map +1 -1
  60. package/dist/shared.d.ts +5 -1
  61. package/dist/shared.d.ts.map +1 -1
  62. package/package.json +4 -3
  63. package/src/app-liveness.ts +103 -0
  64. package/src/binary.ts +74 -0
  65. package/src/client.ts +172 -170
  66. package/src/connection-check.ts +60 -0
  67. package/src/errors.ts +16 -5
  68. package/src/image-snapshot.ts +31 -9
  69. package/src/index.ts +0 -2
  70. package/src/js-block-budget-policy.ts +90 -0
  71. package/src/js-watchdog.ts +78 -0
  72. package/src/link/README.md +43 -0
  73. package/src/link/channel.ts +473 -0
  74. package/src/link/envelope.ts +110 -0
  75. package/src/link/index.ts +321 -0
  76. package/src/link/ports.ts +42 -0
  77. package/src/link/scheduler.ts +132 -0
  78. package/src/link/standard-websocket.ts +251 -0
  79. package/src/link/streams.ts +34 -0
  80. package/src/protocol.ts +14 -9
  81. package/src/rpc-peer.ts +6 -3
  82. package/src/server.ts +99 -67
  83. package/src/shared/test-runner.ts +12 -0
  84. package/src/shared.ts +6 -1
  85. package/dist/binary-transfer.d.ts +0 -17
  86. package/dist/binary-transfer.d.ts.map +0 -1
  87. package/dist/binary-transfer.js +0 -60
  88. package/dist/heartbeat.d.ts +0 -13
  89. package/dist/heartbeat.d.ts.map +0 -1
  90. package/dist/heartbeat.js +0 -49
  91. package/dist/transport.d.ts +0 -22
  92. package/dist/transport.d.ts.map +0 -1
  93. package/dist/transport.js +0 -22
  94. package/dist/websocket-client-transport.d.ts +0 -3
  95. package/dist/websocket-client-transport.d.ts.map +0 -1
  96. package/dist/websocket-client-transport.js +0 -45
  97. package/dist/websocket-server-transport.d.ts +0 -4
  98. package/dist/websocket-server-transport.d.ts.map +0 -1
  99. package/dist/websocket-server-transport.js +0 -43
  100. package/src/binary-transfer.ts +0 -79
  101. package/src/heartbeat.ts +0 -67
  102. package/src/transport.ts +0 -47
  103. package/src/websocket-client-transport.ts +0 -85
  104. package/src/websocket-server-transport.ts +0 -54
@@ -0,0 +1,31 @@
1
+ import type { JsBlockBudget } from './js-watchdog.js';
2
+ import type { BridgeEvents } from './shared.js';
3
+ export type JsBlockBudgetPolicyOptions = {
4
+ /** Budget outside any test, hook or file load. */
5
+ idleMs: number;
6
+ /** Budget while a setup file or test file loads (module code and `describe` bodies). */
7
+ fileLoadMs: number;
8
+ /** Added to a test's or hook's own timeout. */
9
+ graceMs: number;
10
+ };
11
+ export declare const IDLE_BUDGET_LABEL = "between test phases";
12
+ export declare const getIdleBudget: (options: JsBlockBudgetPolicyOptions) => JsBlockBudget;
13
+ export type JsBlockBudgetPolicy = {
14
+ /**
15
+ * Returns the block budget that applies from this event on, or `null` for
16
+ * events that do not change it.
17
+ */
18
+ getBudgetForEvent: (event: BridgeEvents) => JsBlockBudget | null;
19
+ };
20
+ /**
21
+ * Maps runner events to block budgets. Budgets come from timeouts the user
22
+ * already configures, so long work within them never fails a run:
23
+ *
24
+ * - loading a setup file or test file: `fileLoadMs`
25
+ * - a test, including its `beforeEach`/`afterEach` hooks: its timeout + grace
26
+ * - a suite's `beforeAll`/`afterAll` hooks: the hook timeout + grace, from
27
+ * `suite-started` until `suite-finished`, outside the suite's tests
28
+ * - anything else: `idleMs`
29
+ */
30
+ export declare const createJsBlockBudgetPolicy: (options: JsBlockBudgetPolicyOptions) => JsBlockBudgetPolicy;
31
+ //# sourceMappingURL=js-block-budget-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"js-block-budget-policy.d.ts","sourceRoot":"","sources":["../src/js-block-budget-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,MAAM,0BAA0B,GAAG;IACvC,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,wFAAwF;IACxF,UAAU,EAAE,MAAM,CAAC;IACnB,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,iBAAiB,wBAAwB,CAAC;AAEvD,eAAO,MAAM,aAAa,GACxB,SAAS,0BAA0B,KAClC,aAAmE,CAAC;AAEvE,MAAM,MAAM,mBAAmB,GAAG;IAChC;;;OAGG;IACH,iBAAiB,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,aAAa,GAAG,IAAI,CAAC;CAClE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB,GACpC,SAAS,0BAA0B,KAClC,mBAmDF,CAAC"}
@@ -0,0 +1,60 @@
1
+ export const IDLE_BUDGET_LABEL = 'between test phases';
2
+ export const getIdleBudget = (options) => ({ ms: options.idleMs, label: IDLE_BUDGET_LABEL });
3
+ /**
4
+ * Maps runner events to block budgets. Budgets come from timeouts the user
5
+ * already configures, so long work within them never fails a run:
6
+ *
7
+ * - loading a setup file or test file: `fileLoadMs`
8
+ * - a test, including its `beforeEach`/`afterEach` hooks: its timeout + grace
9
+ * - a suite's `beforeAll`/`afterAll` hooks: the hook timeout + grace, from
10
+ * `suite-started` until `suite-finished`, outside the suite's tests
11
+ * - anything else: `idleMs`
12
+ */
13
+ export const createJsBlockBudgetPolicy = (options) => {
14
+ const idle = getIdleBudget(options);
15
+ // Hook budgets of the suites currently running, innermost last.
16
+ const suites = [];
17
+ const outsideTests = () => suites[suites.length - 1] ?? idle;
18
+ return {
19
+ getBudgetForEvent: (event) => {
20
+ switch (event.type) {
21
+ case 'setup-file-bundling-finished':
22
+ return {
23
+ ms: options.fileLoadMs,
24
+ label: `loading setup file ${event.file}`,
25
+ };
26
+ case 'collection-started':
27
+ return { ms: options.fileLoadMs, label: `loading ${event.file}` };
28
+ case 'setup-file-bundling-failed':
29
+ case 'collection-finished':
30
+ case 'file-started':
31
+ case 'file-finished':
32
+ suites.length = 0;
33
+ return idle;
34
+ case 'suite-started':
35
+ suites.push(event.timeout === undefined
36
+ ? outsideTests()
37
+ : {
38
+ ms: event.timeout + options.graceMs,
39
+ label: `beforeAll/afterAll hooks of suite "${event.name}" (${event.timeout}ms timeout + ${options.graceMs}ms grace)`,
40
+ });
41
+ return outsideTests();
42
+ case 'suite-finished':
43
+ suites.pop();
44
+ return outsideTests();
45
+ case 'test-started':
46
+ if (event.timeout === undefined) {
47
+ return null;
48
+ }
49
+ return {
50
+ ms: event.timeout + options.graceMs,
51
+ label: `test "${event.fullName}" (${event.timeout}ms timeout + ${options.graceMs}ms grace)`,
52
+ };
53
+ case 'test-finished':
54
+ return outsideTests();
55
+ default:
56
+ return null;
57
+ }
58
+ },
59
+ };
60
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The longest the app's JS thread may go without answering a `js-probe`, and
3
+ * a label naming what it is busy with, used in the failure message.
4
+ */
5
+ export type JsBlockBudget = {
6
+ ms: number;
7
+ label: string;
8
+ };
9
+ export type JsWatchdog = {
10
+ notifyProbeAck: (id: number) => void;
11
+ /**
12
+ * Replaces the current budget. The new budget covers silence from the
13
+ * later of the last acknowledged probe and the change itself: a block that
14
+ * fitted the previous budget is never judged against a smaller one.
15
+ */
16
+ setBlockBudget: (budget: JsBlockBudget) => void;
17
+ dispose: () => void;
18
+ };
19
+ export declare const DEFAULT_JS_PROBE_INTERVAL_MS = 1000;
20
+ /**
21
+ * Checks that the app's JS thread keeps getting a turn to run. The server
22
+ * sends `js-probe` messages, which only JS can answer, and fails once the
23
+ * thread has been silent for longer than the current block budget.
24
+ */
25
+ export declare const createJsWatchdog: (options: {
26
+ sendProbe: (id: number) => void;
27
+ onUnresponsive: (budget: JsBlockBudget) => void;
28
+ initialBudget: JsBlockBudget;
29
+ probeIntervalMs?: number;
30
+ }) => JsWatchdog;
31
+ //# sourceMappingURL=js-watchdog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"js-watchdog.d.ts","sourceRoot":"","sources":["../src/js-watchdog.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC;;;;OAIG;IACH,cAAc,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAChD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,4BAA4B,OAAQ,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAAI,SAAS;IACxC,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,cAAc,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAChD,aAAa,EAAE,aAAa,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,KAAG,UA6CH,CAAC"}
@@ -0,0 +1,46 @@
1
+ export const DEFAULT_JS_PROBE_INTERVAL_MS = 1_000;
2
+ /**
3
+ * Checks that the app's JS thread keeps getting a turn to run. The server
4
+ * sends `js-probe` messages, which only JS can answer, and fails once the
5
+ * thread has been silent for longer than the current block budget.
6
+ */
7
+ export const createJsWatchdog = (options) => {
8
+ let budget = options.initialBudget;
9
+ let lastAckAt = Date.now();
10
+ let budgetSetAt = lastAckAt;
11
+ let nextProbeId = 1;
12
+ let pendingProbeId = null;
13
+ let disposed = false;
14
+ const dispose = () => {
15
+ if (disposed) {
16
+ return;
17
+ }
18
+ disposed = true;
19
+ clearInterval(interval);
20
+ };
21
+ const interval = setInterval(() => {
22
+ if (Date.now() - Math.max(lastAckAt, budgetSetAt) > budget.ms) {
23
+ dispose();
24
+ options.onUnresponsive(budget);
25
+ return;
26
+ }
27
+ if (pendingProbeId === null) {
28
+ pendingProbeId = nextProbeId++;
29
+ options.sendProbe(pendingProbeId);
30
+ }
31
+ }, options.probeIntervalMs ?? DEFAULT_JS_PROBE_INTERVAL_MS);
32
+ return {
33
+ notifyProbeAck: (id) => {
34
+ if (id !== pendingProbeId) {
35
+ return;
36
+ }
37
+ pendingProbeId = null;
38
+ lastAckAt = Date.now();
39
+ },
40
+ setBlockBudget: (next) => {
41
+ budget = next;
42
+ budgetSetAt = Date.now();
43
+ },
44
+ dispose,
45
+ };
46
+ };
@@ -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"}