toiljs 0.0.66 → 0.0.68

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 (111) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +63 -61
  3. package/build/backend/.tsbuildinfo +1 -1
  4. package/build/cli/.tsbuildinfo +1 -1
  5. package/build/cli/index.js +13 -1
  6. package/build/client/.tsbuildinfo +1 -1
  7. package/build/client/index.d.ts +2 -0
  8. package/build/client/index.js +1 -0
  9. package/build/client/rpc.js +21 -1
  10. package/build/client/stream/client.d.ts +11 -0
  11. package/build/client/stream/client.js +59 -0
  12. package/build/compiler/.tsbuildinfo +1 -1
  13. package/build/compiler/config.d.ts +2 -0
  14. package/build/compiler/config.js +9 -7
  15. package/build/compiler/index.d.ts +1 -0
  16. package/build/compiler/index.js +22 -6
  17. package/build/compiler/toil-docs.generated.js +3 -3
  18. package/build/devserver/.tsbuildinfo +1 -1
  19. package/build/devserver/daemon/index.js +4 -3
  20. package/build/devserver/daemon/runtime.d.ts +13 -0
  21. package/build/devserver/daemon/runtime.js +29 -0
  22. package/build/devserver/db/catalog.js +8 -12
  23. package/build/devserver/db/database.d.ts +1 -0
  24. package/build/devserver/db/database.js +10 -0
  25. package/build/devserver/db/derives.d.ts +7 -0
  26. package/build/devserver/db/derives.js +94 -0
  27. package/build/devserver/db/index.d.ts +1 -0
  28. package/build/devserver/db/index.js +1 -0
  29. package/build/devserver/db/types.d.ts +1 -0
  30. package/build/devserver/db/types.js +1 -0
  31. package/build/devserver/http/proxy.d.ts +5 -1
  32. package/build/devserver/http/proxy.js +39 -36
  33. package/build/devserver/http/runtime.d.ts +62 -0
  34. package/build/devserver/http/runtime.js +194 -0
  35. package/build/devserver/index.d.ts +2 -0
  36. package/build/devserver/index.js +1 -0
  37. package/build/devserver/production-ipc.d.ts +50 -0
  38. package/build/devserver/production-ipc.js +21 -0
  39. package/build/devserver/production-worker.d.ts +1 -0
  40. package/build/devserver/production-worker.js +73 -0
  41. package/build/devserver/production.d.ts +35 -0
  42. package/build/devserver/production.js +502 -0
  43. package/build/devserver/runtime/module.d.ts +5 -0
  44. package/build/devserver/runtime/module.js +47 -1
  45. package/build/devserver/server.d.ts +1 -0
  46. package/build/devserver/server.js +32 -145
  47. package/build/devserver/ssr.d.ts +2 -0
  48. package/build/devserver/ssr.js +19 -2
  49. package/build/devserver/stream/catalog.d.ts +20 -0
  50. package/build/devserver/stream/catalog.js +54 -0
  51. package/build/devserver/stream/host.d.ts +9 -0
  52. package/build/devserver/stream/host.js +15 -0
  53. package/build/devserver/stream/index.d.ts +37 -0
  54. package/build/devserver/stream/index.js +220 -0
  55. package/build/devserver/stream/manager.d.ts +34 -0
  56. package/build/devserver/stream/manager.js +103 -0
  57. package/build/devserver/stream/router.d.ts +25 -0
  58. package/build/devserver/stream/router.js +64 -0
  59. package/build/devserver/stream/wire.d.ts +5 -0
  60. package/build/devserver/stream/wire.js +33 -0
  61. package/build/devserver/stream/ws.d.ts +18 -0
  62. package/build/devserver/stream/ws.js +46 -0
  63. package/build/devserver/wasm/surface.d.ts +1 -1
  64. package/build/devserver/wasm/surface.js +1 -1
  65. package/docs/cli.md +3 -1
  66. package/docs/getting-started.md +7 -7
  67. package/docs/tiers.md +15 -9
  68. package/examples/basic/server/routes/Guestbook.ts +38 -13
  69. package/package.json +2 -2
  70. package/src/cli/index.ts +14 -1
  71. package/src/client/index.ts +2 -0
  72. package/src/client/rpc.ts +25 -1
  73. package/src/client/stream/client.ts +107 -0
  74. package/src/compiler/config.ts +15 -7
  75. package/src/compiler/index.ts +43 -18
  76. package/src/compiler/toil-docs.generated.ts +3 -3
  77. package/src/devserver/daemon/index.ts +7 -7
  78. package/src/devserver/daemon/runtime.ts +48 -0
  79. package/src/devserver/db/catalog.ts +9 -13
  80. package/src/devserver/db/database.ts +14 -0
  81. package/src/devserver/db/derives.ts +121 -0
  82. package/src/devserver/db/index.ts +1 -0
  83. package/src/devserver/db/types.ts +6 -0
  84. package/src/devserver/http/proxy.ts +53 -39
  85. package/src/devserver/http/runtime.ts +287 -0
  86. package/src/devserver/index.ts +2 -0
  87. package/src/devserver/production-ipc.ts +63 -0
  88. package/src/devserver/production-worker.ts +83 -0
  89. package/src/devserver/production.ts +706 -0
  90. package/src/devserver/runtime/module.ts +95 -1
  91. package/src/devserver/server.ts +52 -201
  92. package/src/devserver/ssr.ts +23 -3
  93. package/src/devserver/stream/catalog.ts +106 -0
  94. package/src/devserver/stream/host.ts +42 -0
  95. package/src/devserver/stream/index.ts +308 -0
  96. package/src/devserver/stream/manager.ts +163 -0
  97. package/src/devserver/stream/router.ts +101 -0
  98. package/src/devserver/stream/wire.ts +58 -0
  99. package/src/devserver/stream/ws.ts +76 -0
  100. package/src/devserver/wasm/surface.ts +5 -7
  101. package/test/built-ssr.test.ts +98 -0
  102. package/test/daemon-build.test.ts +15 -7
  103. package/test/daemon-catalog.test.ts +17 -8
  104. package/test/devserver-database.test.ts +8 -8
  105. package/test/devserver.test.ts +20 -4
  106. package/test/example-guestbook.test.ts +8 -5
  107. package/test/fixtures/stream-echo.ts +26 -0
  108. package/test/fixtures/stream-gate.ts +24 -0
  109. package/test/fixtures/stream-trap.ts +18 -0
  110. package/test/rpc-bignum-wire.test.ts +8 -8
  111. package/test/stream-emulation.test.ts +394 -0
@@ -139,20 +139,21 @@ export class DaemonHost implements DaemonRuntime {
139
139
  // else the emulator stays off (fail-closed; section 3.3 / 5.1).
140
140
  const surface = parseSurface(bytes);
141
141
  if (surface === 'invalid') {
142
- this.log(pc.red(' ✗ cold artifact toil.surface is corrupt; daemon not started') + '\n');
142
+ this.log(
143
+ pc.red(' ✗ cold artifact toil.surface is corrupt; daemon not started') + '\n',
144
+ );
143
145
  if (this.running) this.stop();
144
146
  this.loadedMtimeMs = mtimeMs;
145
147
  return false;
146
148
  }
147
- if (surface !== 'absent' && surface.targetMode !== 'cold')
149
+ if (surface.targetMode !== 'cold')
148
150
  this.log(
149
151
  pc.yellow(' ! ') +
150
152
  pc.dim('cold slot holds a hot-mode artifact; ignoring daemon emulator') +
151
153
  '\n',
152
154
  );
153
155
  const catalog = parseDaemonCatalog(bytes);
154
- const declaresDaemon =
155
- (surface === 'absent' ? false : surface.flags.daemon) || (catalog?.hasDaemon ?? false);
156
+ const declaresDaemon = surface.flags.daemon || (catalog?.hasDaemon ?? false);
156
157
 
157
158
  // A restart: stop the old box (timers + instance), bump epoch, start fresh.
158
159
  if (this.running) this.stop();
@@ -318,9 +319,8 @@ export class DaemonHost implements DaemonRuntime {
318
319
  const ret = this.exports.scheduled_tick(task.taskIndex); // packed-i64
319
320
  if (ret < 0n)
320
321
  this.log(
321
- pc.yellow(
322
- ` ⏱ @scheduled ${task.name} returned error ${decodeAbiError(ret)}`,
323
- ) + '\n',
322
+ pc.yellow(` ⏱ @scheduled ${task.name} returned error ${decodeAbiError(ret)}`) +
323
+ '\n',
324
324
  );
325
325
  } catch (e) {
326
326
  // A trapped tick does NOT tear down the long-lived daemon box (unlike a
@@ -0,0 +1,48 @@
1
+ import pc from 'picocolors';
2
+
3
+ import { DaemonHost, daemonEmulationEnabled } from './index.js';
4
+ import type { ResolvedDaemonConfig } from './host.js';
5
+
6
+ export interface DaemonRuntimeOptions {
7
+ readonly coldWasmFile?: string;
8
+ readonly nodeMode?: string;
9
+ readonly daemon?: ResolvedDaemonConfig;
10
+ readonly pollMs?: number;
11
+ }
12
+
13
+ export interface RunningDaemonRuntime {
14
+ readonly host: DaemonHost;
15
+ close(): void;
16
+ }
17
+
18
+ /** Starts the shared cold-artifact daemon runtime used by both `dev` and `start`. */
19
+ export function startDaemonRuntime(options: DaemonRuntimeOptions): RunningDaemonRuntime | null {
20
+ const nodeMode = options.nodeMode ?? 'all';
21
+ if (
22
+ options.coldWasmFile === undefined ||
23
+ !daemonEmulationEnabled(nodeMode) ||
24
+ options.daemon === undefined
25
+ ) {
26
+ return null;
27
+ }
28
+
29
+ const host = new DaemonHost(options.coldWasmFile, options.daemon, nodeMode);
30
+ const pollDaemon = (): void => {
31
+ try {
32
+ host.refresh();
33
+ } catch (e) {
34
+ process.stdout.write(pc.red(` x daemon reload failed: ${String(e)}`) + '\n');
35
+ }
36
+ };
37
+ pollDaemon();
38
+ const timer = setInterval(pollDaemon, options.pollMs ?? 500);
39
+ timer.unref?.();
40
+
41
+ return {
42
+ host,
43
+ close: (): void => {
44
+ clearInterval(timer);
45
+ host.close();
46
+ },
47
+ };
48
+ }
@@ -48,7 +48,7 @@ export function parseCatalog(wasm: Buffer): DbCatalogState {
48
48
 
49
49
  const r = new DataReader(sec);
50
50
  const version = r.readU16();
51
- if (!r.ok || (version !== 1 && version !== 2)) return { kind: 'malformed' };
51
+ if (!r.ok || version !== 1) return { kind: 'malformed' };
52
52
  const ndb = r.readU16();
53
53
  for (let d = 0; d < ndb && r.ok; d++) {
54
54
  const db = r.readString();
@@ -63,18 +63,14 @@ export function parseCatalog(wasm: Buffer): DbCatalogState {
63
63
  r.readU32(); // generation
64
64
  const replication = r.readU8(); // emitter order: replication then placement
65
65
  const placement = r.readU8();
66
- let fillMaxWaitMs = DEFAULT_FILL_WAIT_MS;
67
- let fillAllowStale = true;
68
- if (version >= 2) {
69
- fillMaxWaitMs = r.readU32();
70
- const fillAllowStaleByte = r.readU8();
71
- if (
72
- fillMaxWaitMs > MAX_FILL_WAIT_MS ||
73
- (fillAllowStaleByte !== 0 && fillAllowStaleByte !== 1)
74
- )
75
- return { kind: 'malformed' };
76
- fillAllowStale = fillAllowStaleByte === 1;
77
- }
66
+ const fillMaxWaitMs = r.readU32();
67
+ const fillAllowStaleByte = r.readU8();
68
+ if (
69
+ fillMaxWaitMs > MAX_FILL_WAIT_MS ||
70
+ (fillAllowStaleByte !== 0 && fillAllowStaleByte !== 1)
71
+ )
72
+ return { kind: 'malformed' };
73
+ const fillAllowStale = fillAllowStaleByte === 1;
78
74
  const nFields = r.readU16();
79
75
  for (let f = 0; f < nFields; f++) {
80
76
  r.readString(); // field name
@@ -700,6 +700,7 @@ export class DevDatabase {
700
700
  if (outcome.kind === 'unit') {
701
701
  this.store.set(sk, value);
702
702
  this.stampVersion(coll, sk); // stamp the value type's current schema version
703
+ this.recordWrite(db, coll);
703
704
  }
704
705
  this.recordIdemFinish(coll, key, 'C', idem, requestHash, outcome);
705
706
  return this.replayRecordOutcome(db, outcome);
@@ -732,6 +733,7 @@ export class DevDatabase {
732
733
  if (outcome.kind === 'value') {
733
734
  this.store.set(sk, v);
734
735
  this.stampVersion(coll, sk); // a patch rewrites the row at the current version
736
+ this.recordWrite(db, coll);
735
737
  }
736
738
  this.recordIdemFinish(coll, key, 'P', idem, requestHash, outcome);
737
739
  return this.replayRecordOutcome(db, outcome);
@@ -1023,6 +1025,15 @@ export class DevDatabase {
1023
1025
 
1024
1026
  // `delta` is the wasm i64 (a BigInt across the boundary); `BigInt()`
1025
1027
  // normalizes the test's plain-number form too. Saturates like the edge.
1028
+ /** Note a successful write to a SOURCE collection, so the runtime can re-run
1029
+ * the `@derive` materializers that depend on it after this dispatch. A
1030
+ * derive's OWN writes run under FunctionKind=Derive and must never
1031
+ * re-trigger a derive (which would loop), so they are never recorded. */
1032
+ private recordWrite(db: DbDevState, coll: DevCollectionHandle): void {
1033
+ if (db.functionKind === DbFunctionKind.Derive) return;
1034
+ db.writtenCollections.add(coll.name);
1035
+ }
1036
+
1026
1037
  counterAdd(
1027
1038
  ref: MemoryRef,
1028
1039
  db: DbDevState,
@@ -1045,6 +1056,7 @@ export class DevDatabase {
1045
1056
  }
1046
1057
  const sk = storeKey(coll.name, key);
1047
1058
  this.counters.set(sk, satI64((this.counters.get(sk) ?? 0n) + d));
1059
+ this.recordWrite(db, coll);
1048
1060
  return 0;
1049
1061
  }
1050
1062
 
@@ -1086,6 +1098,7 @@ export class DevDatabase {
1086
1098
  log.push(ev);
1087
1099
  (this.eventVersions.get(sk) ?? this.eventVersions.set(sk, []).get(sk)!).push(sv);
1088
1100
  }
1101
+ this.recordWrite(db, coll);
1089
1102
  return 0;
1090
1103
  }
1091
1104
 
@@ -1124,6 +1137,7 @@ export class DevDatabase {
1124
1137
  (this.eventVersions.get(sk) ?? this.eventVersions.set(sk, []).get(sk)!).push(sv);
1125
1138
  }
1126
1139
  seen.add(evid);
1140
+ this.recordWrite(db, coll);
1127
1141
  return 1;
1128
1142
  }
1129
1143
 
@@ -0,0 +1,121 @@
1
+ import { customSection } from '../wasm/sections.js';
2
+
3
+ /**
4
+ * Parses the `toildb.derives` wiring section emitted by toilscript for a
5
+ * `@database` class with `@derive` materializer methods (see the compiler's
6
+ * `buildToilDbDerives`). It maps each derive to its owning `@database` class, so
7
+ * the dev runtime (`runtime/module.ts`) can, after a dispatch writes a source
8
+ * collection, re-run that database's derives under FunctionKind=Derive. Fails
9
+ * closed: any malformed byte yields `[]` (no derive runs) rather than throwing.
10
+ *
11
+ * Section layout (LE), mirroring `buildToilDbDerives`:
12
+ * u16 format_version = 1
13
+ * u16 n_derives
14
+ * per derive: u16 derive_id, str db_name, str method_name (str = u32 len + bytes)
15
+ */
16
+ export interface DeriveEntry {
17
+ readonly deriveId: number;
18
+ readonly dbName: string;
19
+ readonly methodName: string;
20
+ }
21
+
22
+ const SECTION = 'toildb.derives';
23
+ const VERSION = 1;
24
+ const MAX_SECTION_BYTES = 128 * 1024;
25
+ const MAX_DERIVES = 1024;
26
+ const MAX_NAME_BYTES = 1024;
27
+ const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true });
28
+
29
+ export function parseDerives(wasm: Buffer): readonly DeriveEntry[] {
30
+ let section: Buffer | null;
31
+ try {
32
+ section = customSection(wasm, SECTION);
33
+ } catch {
34
+ return [];
35
+ }
36
+ if (section === null) return [];
37
+ if (section.length > MAX_SECTION_BYTES) return [];
38
+
39
+ const r = new Reader(section);
40
+ const version = r.u16();
41
+ if (!r.ok || version !== VERSION) return [];
42
+ const count = r.u16();
43
+ if (!r.ok || count > MAX_DERIVES) return [];
44
+
45
+ const derives: DeriveEntry[] = [];
46
+ for (let i = 0; i < count && r.ok; i++) {
47
+ const deriveId = r.u16();
48
+ const dbName = r.string();
49
+ const methodName = r.string();
50
+ if (!r.ok || dbName.length === 0) return [];
51
+ derives.push({ deriveId, dbName, methodName });
52
+ }
53
+ if (!r.ok || r.remaining() !== 0) return [];
54
+ return derives;
55
+ }
56
+
57
+ /**
58
+ * The derives whose owning `@database` had at least one source collection
59
+ * written during this dispatch. `written` holds "Db/coll" store keys; the
60
+ * database is the prefix before the first `/`. Each affected derive appears at
61
+ * most once (coalescing: many writes to one database run its derives once).
62
+ */
63
+ export function derivesForWrites(
64
+ derives: readonly DeriveEntry[],
65
+ written: ReadonlySet<string>,
66
+ ): readonly DeriveEntry[] {
67
+ if (derives.length === 0 || written.size === 0) return [];
68
+ const dbs = new Set<string>();
69
+ for (const key of written) {
70
+ const slash = key.indexOf('/');
71
+ dbs.add(slash >= 0 ? key.slice(0, slash) : key);
72
+ }
73
+ return derives.filter((d) => dbs.has(d.dbName));
74
+ }
75
+
76
+ class Reader {
77
+ private pos = 0;
78
+ ok = true;
79
+
80
+ constructor(private readonly bytes: Buffer) {}
81
+
82
+ remaining(): number {
83
+ return this.bytes.length - this.pos;
84
+ }
85
+
86
+ u16(): number {
87
+ if (!this.ok || this.pos + 2 > this.bytes.length) {
88
+ this.ok = false;
89
+ return 0;
90
+ }
91
+ const out = this.bytes.readUInt16LE(this.pos);
92
+ this.pos += 2;
93
+ return out;
94
+ }
95
+
96
+ u32(): number {
97
+ if (!this.ok || this.pos + 4 > this.bytes.length) {
98
+ this.ok = false;
99
+ return 0;
100
+ }
101
+ const out = this.bytes.readUInt32LE(this.pos);
102
+ this.pos += 4;
103
+ return out;
104
+ }
105
+
106
+ string(): string {
107
+ const len = this.u32();
108
+ if (!this.ok || len > MAX_NAME_BYTES || this.pos + len > this.bytes.length) {
109
+ this.ok = false;
110
+ return '';
111
+ }
112
+ try {
113
+ const out = UTF8_DECODER.decode(this.bytes.subarray(this.pos, this.pos + len));
114
+ this.pos += len;
115
+ return out;
116
+ } catch {
117
+ this.ok = false;
118
+ return '';
119
+ }
120
+ }
121
+ }
@@ -15,4 +15,5 @@ export {
15
15
  setDbCatalog,
16
16
  } from './database.js';
17
17
  export { parseCatalog } from './catalog.js';
18
+ export { type DeriveEntry, derivesForWrites, parseDerives } from './derives.js';
18
19
  export { CollectionFamily, DbFunctionKind, type DbDevState, freshDbState } from './types.js';
@@ -50,6 +50,11 @@ export interface DbDevState {
50
50
  lastResult: Buffer | null;
51
51
  lastResultVersion: number;
52
52
  functionKind: DbFunctionKind;
53
+ /** Names ("Db/coll") of source collections written during this dispatch, so
54
+ * the runtime can re-run the affected `@derive` materializers afterward.
55
+ * Only populated for non-Derive dispatches (a derive's own writes must not
56
+ * re-trigger it - see `database.ts` `recordWrite`). */
57
+ writtenCollections: Set<string>;
53
58
  }
54
59
 
55
60
  export function freshDbState(): DbDevState {
@@ -58,6 +63,7 @@ export function freshDbState(): DbDevState {
58
63
  lastResult: null,
59
64
  lastResultVersion: -1,
60
65
  functionKind: DbFunctionKind.Job,
66
+ writtenCollections: new Set<string>(),
61
67
  };
62
68
  }
63
69
 
@@ -114,45 +114,59 @@ export function wireWebsocketProxy(app: Server, target: ViteTarget): void {
114
114
  '/*',
115
115
  { message_type: 'Buffer', idle_timeout: 120, max_payload_length: 16 * 1024 * 1024 },
116
116
  (ws: Websocket) => {
117
- const { url, protocol } = ws.context as { url: string; protocol: string };
118
- const upstream = new WebSocket(
119
- `ws://${target.host}:${String(target.port)}${url}`,
120
- protocol ? protocol.split(',').map((p) => p.trim()) : [],
121
- );
122
- upstream.binaryType = 'arraybuffer';
123
-
124
- const pending: (string | Uint8Array<ArrayBuffer>)[] = [];
125
- let open = false;
126
-
127
- upstream.onopen = (): void => {
128
- open = true;
129
- for (const m of pending) upstream.send(m);
130
- pending.length = 0;
131
- };
132
- upstream.onmessage = (event: MessageEvent): void => {
133
- if (typeof event.data === 'string') ws.send(event.data);
134
- else ws.send(Buffer.from(event.data as ArrayBuffer), true);
135
- };
136
- upstream.onclose = (event: CloseEvent): void => {
137
- ws.close(event.code, event.reason);
138
- };
139
- upstream.onerror = (): void => {
140
- ws.close();
141
- };
142
-
143
- ws.on('message', (message: Buffer, isBinary: boolean) => {
144
- const m = toUpstreamMessage(message, isBinary);
145
- if (open) upstream.send(m);
146
- else pending.push(m);
147
- });
148
- ws.on('close', () => {
149
- if (
150
- upstream.readyState === WebSocket.OPEN ||
151
- upstream.readyState === WebSocket.CONNECTING
152
- ) {
153
- upstream.close();
154
- }
155
- });
117
+ pipeToVite(ws, target, ws.context as { url: string; protocol: string });
156
118
  },
157
119
  );
158
120
  }
121
+
122
+ /**
123
+ * Pipe ONE upgraded websocket to the internal Vite HMR server. The verbatim body extracted from
124
+ * {@link wireWebsocketProxy} so the dev STREAM router (doc 08 4.1 `wireStreams`) can reuse it for every
125
+ * NON-stream upgrade while it handles `@stream`-route upgrades itself - HMR stays byte-for-byte
126
+ * unchanged. `ctx` is the upgrade context (`{ url, protocol }`) the upgrade handler stamped.
127
+ */
128
+ export function pipeToVite(
129
+ ws: Websocket,
130
+ target: ViteTarget,
131
+ ctx: { url: string; protocol: string },
132
+ ): void {
133
+ const { url, protocol } = ctx;
134
+ const upstream = new WebSocket(
135
+ `ws://${target.host}:${String(target.port)}${url}`,
136
+ protocol ? protocol.split(',').map((p) => p.trim()) : [],
137
+ );
138
+ upstream.binaryType = 'arraybuffer';
139
+
140
+ const pending: (string | Uint8Array<ArrayBuffer>)[] = [];
141
+ let open = false;
142
+
143
+ upstream.onopen = (): void => {
144
+ open = true;
145
+ for (const m of pending) upstream.send(m);
146
+ pending.length = 0;
147
+ };
148
+ upstream.onmessage = (event: MessageEvent): void => {
149
+ if (typeof event.data === 'string') ws.send(event.data);
150
+ else ws.send(Buffer.from(event.data as ArrayBuffer), true);
151
+ };
152
+ upstream.onclose = (event: CloseEvent): void => {
153
+ ws.close(event.code, event.reason);
154
+ };
155
+ upstream.onerror = (): void => {
156
+ ws.close();
157
+ };
158
+
159
+ ws.on('message', (message: Buffer, isBinary: boolean) => {
160
+ const m = toUpstreamMessage(message, isBinary);
161
+ if (open) upstream.send(m);
162
+ else pending.push(m);
163
+ });
164
+ ws.on('close', () => {
165
+ if (
166
+ upstream.readyState === WebSocket.OPEN ||
167
+ upstream.readyState === WebSocket.CONNECTING
168
+ ) {
169
+ upstream.close();
170
+ }
171
+ });
172
+ }