@rivetkit/engine-runner 25.7.1-rc.1
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/.turbo/turbo-build.log +23 -0
- package/.turbo/turbo-check-types.log +5 -0
- package/.turbo/turbo-test.log +5537 -0
- package/benches/actor-lifecycle.bench.ts +190 -0
- package/benches/utils.ts +143 -0
- package/dist/mod.cjs +2044 -0
- package/dist/mod.cjs.map +1 -0
- package/dist/mod.d.cts +67 -0
- package/dist/mod.d.ts +67 -0
- package/dist/mod.js +2044 -0
- package/dist/mod.js.map +1 -0
- package/package.json +38 -0
- package/src/log.ts +11 -0
- package/src/mod.ts +1354 -0
- package/src/tunnel.ts +841 -0
- package/src/utils.ts +31 -0
- package/src/websocket-tunnel-adapter.ts +486 -0
- package/src/websocket.ts +43 -0
- package/tests/lifecycle.test.ts +596 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +4 -0
- package/turbo.json +4 -0
- package/vitest.config.ts +17 -0
package/dist/mod.js
ADDED
|
@@ -0,0 +1,2044 @@
|
|
|
1
|
+
// src/mod.ts
|
|
2
|
+
import WebSocket3 from "ws";
|
|
3
|
+
|
|
4
|
+
// src/log.ts
|
|
5
|
+
var LOGGER = void 0;
|
|
6
|
+
function setLogger(logger2) {
|
|
7
|
+
LOGGER = logger2;
|
|
8
|
+
}
|
|
9
|
+
function logger() {
|
|
10
|
+
return LOGGER;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/websocket.ts
|
|
14
|
+
var webSocketPromise = null;
|
|
15
|
+
async function importWebSocket() {
|
|
16
|
+
if (webSocketPromise !== null) {
|
|
17
|
+
return webSocketPromise;
|
|
18
|
+
}
|
|
19
|
+
webSocketPromise = (async () => {
|
|
20
|
+
var _a, _b, _c;
|
|
21
|
+
let _WebSocket;
|
|
22
|
+
if (typeof WebSocket !== "undefined") {
|
|
23
|
+
_WebSocket = WebSocket;
|
|
24
|
+
(_a = logger()) == null ? void 0 : _a.debug({ msg: "using native websocket" });
|
|
25
|
+
} else {
|
|
26
|
+
try {
|
|
27
|
+
const ws = await import("ws");
|
|
28
|
+
_WebSocket = ws.default;
|
|
29
|
+
(_b = logger()) == null ? void 0 : _b.debug({ msg: "using websocket from npm" });
|
|
30
|
+
} catch {
|
|
31
|
+
_WebSocket = class MockWebSocket {
|
|
32
|
+
constructor() {
|
|
33
|
+
throw new Error(
|
|
34
|
+
'WebSocket support requires installing the "ws" peer dependency.'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
(_c = logger()) == null ? void 0 : _c.debug({ msg: "using mock websocket" });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return _WebSocket;
|
|
42
|
+
})();
|
|
43
|
+
return webSocketPromise;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/mod.ts
|
|
47
|
+
import * as protocol from "@rivetkit/engine-runner-protocol";
|
|
48
|
+
|
|
49
|
+
// src/utils.ts
|
|
50
|
+
function unreachable(x) {
|
|
51
|
+
throw `Unreachable: ${x}`;
|
|
52
|
+
}
|
|
53
|
+
function calculateBackoff(attempt, options = {}) {
|
|
54
|
+
const {
|
|
55
|
+
initialDelay = 1e3,
|
|
56
|
+
maxDelay = 3e4,
|
|
57
|
+
multiplier = 2,
|
|
58
|
+
jitter = true
|
|
59
|
+
} = options;
|
|
60
|
+
let delay = Math.min(initialDelay * Math.pow(multiplier, attempt), maxDelay);
|
|
61
|
+
if (jitter) {
|
|
62
|
+
delay = delay * (1 + Math.random() * 0.25);
|
|
63
|
+
}
|
|
64
|
+
return Math.floor(delay);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/tunnel.ts
|
|
68
|
+
import WebSocket2 from "ws";
|
|
69
|
+
import * as tunnel from "@rivetkit/engine-tunnel-protocol";
|
|
70
|
+
|
|
71
|
+
// src/websocket-tunnel-adapter.ts
|
|
72
|
+
var WebSocketTunnelAdapter = class {
|
|
73
|
+
#webSocketId;
|
|
74
|
+
#readyState = 0;
|
|
75
|
+
// CONNECTING
|
|
76
|
+
#eventListeners = /* @__PURE__ */ new Map();
|
|
77
|
+
#onopen = null;
|
|
78
|
+
#onclose = null;
|
|
79
|
+
#onerror = null;
|
|
80
|
+
#onmessage = null;
|
|
81
|
+
#bufferedAmount = 0;
|
|
82
|
+
#binaryType = "nodebuffer";
|
|
83
|
+
#extensions = "";
|
|
84
|
+
#protocol = "";
|
|
85
|
+
#url = "";
|
|
86
|
+
#sendCallback;
|
|
87
|
+
#closeCallback;
|
|
88
|
+
// Event buffering for events fired before listeners are attached
|
|
89
|
+
#bufferedEvents = [];
|
|
90
|
+
constructor(webSocketId, sendCallback, closeCallback) {
|
|
91
|
+
this.#webSocketId = webSocketId;
|
|
92
|
+
this.#sendCallback = sendCallback;
|
|
93
|
+
this.#closeCallback = closeCallback;
|
|
94
|
+
}
|
|
95
|
+
get readyState() {
|
|
96
|
+
return this.#readyState;
|
|
97
|
+
}
|
|
98
|
+
get bufferedAmount() {
|
|
99
|
+
return this.#bufferedAmount;
|
|
100
|
+
}
|
|
101
|
+
get binaryType() {
|
|
102
|
+
return this.#binaryType;
|
|
103
|
+
}
|
|
104
|
+
set binaryType(value) {
|
|
105
|
+
if (value === "nodebuffer" || value === "arraybuffer" || value === "blob") {
|
|
106
|
+
this.#binaryType = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
get extensions() {
|
|
110
|
+
return this.#extensions;
|
|
111
|
+
}
|
|
112
|
+
get protocol() {
|
|
113
|
+
return this.#protocol;
|
|
114
|
+
}
|
|
115
|
+
get url() {
|
|
116
|
+
return this.#url;
|
|
117
|
+
}
|
|
118
|
+
get onopen() {
|
|
119
|
+
return this.#onopen;
|
|
120
|
+
}
|
|
121
|
+
set onopen(value) {
|
|
122
|
+
this.#onopen = value;
|
|
123
|
+
if (value) {
|
|
124
|
+
this.#flushBufferedEvents("open");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
get onclose() {
|
|
128
|
+
return this.#onclose;
|
|
129
|
+
}
|
|
130
|
+
set onclose(value) {
|
|
131
|
+
this.#onclose = value;
|
|
132
|
+
if (value) {
|
|
133
|
+
this.#flushBufferedEvents("close");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
get onerror() {
|
|
137
|
+
return this.#onerror;
|
|
138
|
+
}
|
|
139
|
+
set onerror(value) {
|
|
140
|
+
this.#onerror = value;
|
|
141
|
+
if (value) {
|
|
142
|
+
this.#flushBufferedEvents("error");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
get onmessage() {
|
|
146
|
+
return this.#onmessage;
|
|
147
|
+
}
|
|
148
|
+
set onmessage(value) {
|
|
149
|
+
this.#onmessage = value;
|
|
150
|
+
if (value) {
|
|
151
|
+
this.#flushBufferedEvents("message");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
send(data) {
|
|
155
|
+
if (this.#readyState !== 1) {
|
|
156
|
+
throw new Error("WebSocket is not open");
|
|
157
|
+
}
|
|
158
|
+
let isBinary = false;
|
|
159
|
+
let messageData;
|
|
160
|
+
if (typeof data === "string") {
|
|
161
|
+
messageData = data;
|
|
162
|
+
} else if (data instanceof ArrayBuffer) {
|
|
163
|
+
isBinary = true;
|
|
164
|
+
messageData = data;
|
|
165
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
166
|
+
isBinary = true;
|
|
167
|
+
const view = data;
|
|
168
|
+
if (view.buffer instanceof SharedArrayBuffer) {
|
|
169
|
+
const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
|
170
|
+
messageData = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
171
|
+
} else {
|
|
172
|
+
messageData = view.buffer.slice(
|
|
173
|
+
view.byteOffset,
|
|
174
|
+
view.byteOffset + view.byteLength
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
} else if (data instanceof Blob) {
|
|
178
|
+
throw new Error("Blob sending not implemented in tunnel adapter");
|
|
179
|
+
} else if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {
|
|
180
|
+
isBinary = true;
|
|
181
|
+
const buf = data;
|
|
182
|
+
if (buf.buffer instanceof SharedArrayBuffer) {
|
|
183
|
+
const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
184
|
+
messageData = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
185
|
+
} else {
|
|
186
|
+
messageData = buf.buffer.slice(
|
|
187
|
+
buf.byteOffset,
|
|
188
|
+
buf.byteOffset + buf.byteLength
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
throw new Error("Invalid data type");
|
|
193
|
+
}
|
|
194
|
+
this.#sendCallback(messageData, isBinary);
|
|
195
|
+
}
|
|
196
|
+
close(code, reason) {
|
|
197
|
+
if (this.#readyState === 2 || // CLOSING
|
|
198
|
+
this.#readyState === 3) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.#readyState = 2;
|
|
202
|
+
this.#closeCallback(code, reason);
|
|
203
|
+
this.#readyState = 3;
|
|
204
|
+
const closeEvent = {
|
|
205
|
+
wasClean: true,
|
|
206
|
+
code: code || 1e3,
|
|
207
|
+
reason: reason || "",
|
|
208
|
+
type: "close",
|
|
209
|
+
target: this
|
|
210
|
+
};
|
|
211
|
+
this.#fireEvent("close", closeEvent);
|
|
212
|
+
}
|
|
213
|
+
addEventListener(type, listener, options) {
|
|
214
|
+
if (typeof listener === "function") {
|
|
215
|
+
let listeners = this.#eventListeners.get(type);
|
|
216
|
+
if (!listeners) {
|
|
217
|
+
listeners = /* @__PURE__ */ new Set();
|
|
218
|
+
this.#eventListeners.set(type, listeners);
|
|
219
|
+
}
|
|
220
|
+
listeners.add(listener);
|
|
221
|
+
this.#flushBufferedEvents(type);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
removeEventListener(type, listener, options) {
|
|
225
|
+
if (typeof listener === "function") {
|
|
226
|
+
const listeners = this.#eventListeners.get(type);
|
|
227
|
+
if (listeners) {
|
|
228
|
+
listeners.delete(listener);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
dispatchEvent(event) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
#fireEvent(type, event) {
|
|
236
|
+
var _a, _b, _c, _d, _e;
|
|
237
|
+
const listeners = this.#eventListeners.get(type);
|
|
238
|
+
let hasListeners = false;
|
|
239
|
+
if (listeners && listeners.size > 0) {
|
|
240
|
+
hasListeners = true;
|
|
241
|
+
for (const listener of listeners) {
|
|
242
|
+
try {
|
|
243
|
+
listener.call(this, event);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
(_a = logger()) == null ? void 0 : _a.error({ msg: "error in websocket event listener", error, type });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
switch (type) {
|
|
250
|
+
case "open":
|
|
251
|
+
if (this.#onopen) {
|
|
252
|
+
hasListeners = true;
|
|
253
|
+
try {
|
|
254
|
+
this.#onopen.call(this, event);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
(_b = logger()) == null ? void 0 : _b.error({ msg: "error in onopen handler", error });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
break;
|
|
260
|
+
case "close":
|
|
261
|
+
if (this.#onclose) {
|
|
262
|
+
hasListeners = true;
|
|
263
|
+
try {
|
|
264
|
+
this.#onclose.call(this, event);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
(_c = logger()) == null ? void 0 : _c.error({ msg: "error in onclose handler", error });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
case "error":
|
|
271
|
+
if (this.#onerror) {
|
|
272
|
+
hasListeners = true;
|
|
273
|
+
try {
|
|
274
|
+
this.#onerror.call(this, event);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
(_d = logger()) == null ? void 0 : _d.error({ msg: "error in onerror handler", error });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
280
|
+
case "message":
|
|
281
|
+
if (this.#onmessage) {
|
|
282
|
+
hasListeners = true;
|
|
283
|
+
try {
|
|
284
|
+
this.#onmessage.call(this, event);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
(_e = logger()) == null ? void 0 : _e.error({ msg: "error in onmessage handler", error });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
if (!hasListeners) {
|
|
292
|
+
this.#bufferedEvents.push({ type, event });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
#flushBufferedEvents(type) {
|
|
296
|
+
var _a, _b, _c, _d, _e;
|
|
297
|
+
const eventsToFlush = this.#bufferedEvents.filter(
|
|
298
|
+
(buffered) => buffered.type === type
|
|
299
|
+
);
|
|
300
|
+
this.#bufferedEvents = this.#bufferedEvents.filter(
|
|
301
|
+
(buffered) => buffered.type !== type
|
|
302
|
+
);
|
|
303
|
+
for (const { event } of eventsToFlush) {
|
|
304
|
+
const listeners = this.#eventListeners.get(type);
|
|
305
|
+
if (listeners) {
|
|
306
|
+
for (const listener of listeners) {
|
|
307
|
+
try {
|
|
308
|
+
listener.call(this, event);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
(_a = logger()) == null ? void 0 : _a.error({
|
|
311
|
+
msg: "error in websocket event listener",
|
|
312
|
+
error,
|
|
313
|
+
type
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
switch (type) {
|
|
319
|
+
case "open":
|
|
320
|
+
if (this.#onopen) {
|
|
321
|
+
try {
|
|
322
|
+
this.#onopen.call(this, event);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
(_b = logger()) == null ? void 0 : _b.error({ msg: "error in onopen handler", error });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
break;
|
|
328
|
+
case "close":
|
|
329
|
+
if (this.#onclose) {
|
|
330
|
+
try {
|
|
331
|
+
this.#onclose.call(this, event);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
(_c = logger()) == null ? void 0 : _c.error({ msg: "error in onclose handler", error });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
case "error":
|
|
338
|
+
if (this.#onerror) {
|
|
339
|
+
try {
|
|
340
|
+
this.#onerror.call(this, event);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
(_d = logger()) == null ? void 0 : _d.error({ msg: "error in onerror handler", error });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
break;
|
|
346
|
+
case "message":
|
|
347
|
+
if (this.#onmessage) {
|
|
348
|
+
try {
|
|
349
|
+
this.#onmessage.call(this, event);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
(_e = logger()) == null ? void 0 : _e.error({ msg: "error in onmessage handler", error });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Internal methods called by the Tunnel class
|
|
359
|
+
_handleOpen() {
|
|
360
|
+
if (this.#readyState !== 0) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
this.#readyState = 1;
|
|
364
|
+
const event = {
|
|
365
|
+
type: "open",
|
|
366
|
+
target: this
|
|
367
|
+
};
|
|
368
|
+
this.#fireEvent("open", event);
|
|
369
|
+
}
|
|
370
|
+
_handleMessage(data, isBinary) {
|
|
371
|
+
if (this.#readyState !== 1) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
let messageData;
|
|
375
|
+
if (isBinary) {
|
|
376
|
+
if (this.#binaryType === "nodebuffer") {
|
|
377
|
+
messageData = Buffer.from(data);
|
|
378
|
+
} else if (this.#binaryType === "arraybuffer") {
|
|
379
|
+
if (data instanceof Uint8Array) {
|
|
380
|
+
messageData = data.buffer.slice(
|
|
381
|
+
data.byteOffset,
|
|
382
|
+
data.byteOffset + data.byteLength
|
|
383
|
+
);
|
|
384
|
+
} else {
|
|
385
|
+
messageData = data;
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
throw new Error("Blob binaryType not supported in tunnel adapter");
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
messageData = data;
|
|
392
|
+
}
|
|
393
|
+
const event = {
|
|
394
|
+
data: messageData,
|
|
395
|
+
type: "message",
|
|
396
|
+
target: this
|
|
397
|
+
};
|
|
398
|
+
this.#fireEvent("message", event);
|
|
399
|
+
}
|
|
400
|
+
_handleClose(code, reason) {
|
|
401
|
+
if (this.#readyState === 3) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
this.#readyState = 3;
|
|
405
|
+
const event = {
|
|
406
|
+
wasClean: true,
|
|
407
|
+
code: code || 1e3,
|
|
408
|
+
reason: reason || "",
|
|
409
|
+
type: "close",
|
|
410
|
+
target: this
|
|
411
|
+
};
|
|
412
|
+
this.#fireEvent("close", event);
|
|
413
|
+
}
|
|
414
|
+
_handleError(error) {
|
|
415
|
+
const event = {
|
|
416
|
+
type: "error",
|
|
417
|
+
target: this,
|
|
418
|
+
error
|
|
419
|
+
};
|
|
420
|
+
this.#fireEvent("error", event);
|
|
421
|
+
}
|
|
422
|
+
// WebSocket constants for compatibility
|
|
423
|
+
static CONNECTING = 0;
|
|
424
|
+
static OPEN = 1;
|
|
425
|
+
static CLOSING = 2;
|
|
426
|
+
static CLOSED = 3;
|
|
427
|
+
// Instance constants
|
|
428
|
+
CONNECTING = 0;
|
|
429
|
+
OPEN = 1;
|
|
430
|
+
CLOSING = 2;
|
|
431
|
+
CLOSED = 3;
|
|
432
|
+
// Additional methods for compatibility
|
|
433
|
+
ping(data, mask, cb) {
|
|
434
|
+
if (cb) cb(new Error("Ping not supported in tunnel adapter"));
|
|
435
|
+
}
|
|
436
|
+
pong(data, mask, cb) {
|
|
437
|
+
if (cb) cb(new Error("Pong not supported in tunnel adapter"));
|
|
438
|
+
}
|
|
439
|
+
terminate() {
|
|
440
|
+
this.#readyState = 3;
|
|
441
|
+
this.#closeCallback(1006, "Abnormal Closure");
|
|
442
|
+
const event = {
|
|
443
|
+
wasClean: false,
|
|
444
|
+
code: 1006,
|
|
445
|
+
reason: "Abnormal Closure",
|
|
446
|
+
type: "close",
|
|
447
|
+
target: this
|
|
448
|
+
};
|
|
449
|
+
this.#fireEvent("close", event);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// src/tunnel.ts
|
|
454
|
+
import { v4 as uuidv4 } from "uuid";
|
|
455
|
+
var GC_INTERVAL = 6e4;
|
|
456
|
+
var MESSAGE_ACK_TIMEOUT = 5e3;
|
|
457
|
+
var Tunnel = class {
|
|
458
|
+
#pegboardTunnelUrl;
|
|
459
|
+
#runner;
|
|
460
|
+
#tunnelWs;
|
|
461
|
+
#shutdown = false;
|
|
462
|
+
#reconnectTimeout;
|
|
463
|
+
#reconnectAttempt = 0;
|
|
464
|
+
#actorPendingRequests = /* @__PURE__ */ new Map();
|
|
465
|
+
#actorWebSockets = /* @__PURE__ */ new Map();
|
|
466
|
+
#pendingMessages = /* @__PURE__ */ new Map();
|
|
467
|
+
#gcInterval;
|
|
468
|
+
#callbacks;
|
|
469
|
+
constructor(runner, pegboardTunnelUrl, callbacks) {
|
|
470
|
+
this.#pegboardTunnelUrl = pegboardTunnelUrl;
|
|
471
|
+
this.#runner = runner;
|
|
472
|
+
this.#callbacks = callbacks;
|
|
473
|
+
}
|
|
474
|
+
start() {
|
|
475
|
+
var _a;
|
|
476
|
+
if (((_a = this.#tunnelWs) == null ? void 0 : _a.readyState) === WebSocket2.OPEN) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
this.#connect();
|
|
480
|
+
this.#startGarbageCollector();
|
|
481
|
+
}
|
|
482
|
+
shutdown() {
|
|
483
|
+
this.#shutdown = true;
|
|
484
|
+
if (this.#reconnectTimeout) {
|
|
485
|
+
clearTimeout(this.#reconnectTimeout);
|
|
486
|
+
this.#reconnectTimeout = void 0;
|
|
487
|
+
}
|
|
488
|
+
if (this.#gcInterval) {
|
|
489
|
+
clearInterval(this.#gcInterval);
|
|
490
|
+
this.#gcInterval = void 0;
|
|
491
|
+
}
|
|
492
|
+
if (this.#tunnelWs) {
|
|
493
|
+
this.#tunnelWs.close();
|
|
494
|
+
this.#tunnelWs = void 0;
|
|
495
|
+
}
|
|
496
|
+
for (const [_, request] of this.#actorPendingRequests) {
|
|
497
|
+
request.reject(new Error("Tunnel shutting down"));
|
|
498
|
+
}
|
|
499
|
+
this.#actorPendingRequests.clear();
|
|
500
|
+
for (const [_, ws] of this.#actorWebSockets) {
|
|
501
|
+
ws.close();
|
|
502
|
+
}
|
|
503
|
+
this.#actorWebSockets.clear();
|
|
504
|
+
}
|
|
505
|
+
#sendMessage(requestId, messageKind) {
|
|
506
|
+
if (!this.#tunnelWs || this.#tunnelWs.readyState !== WebSocket2.OPEN) {
|
|
507
|
+
console.warn("Cannot send tunnel message, WebSocket not connected");
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const messageId = generateUuidBuffer();
|
|
511
|
+
const requestIdStr = bufferToString(requestId);
|
|
512
|
+
this.#pendingMessages.set(bufferToString(messageId), {
|
|
513
|
+
sentAt: Date.now(),
|
|
514
|
+
requestIdStr
|
|
515
|
+
});
|
|
516
|
+
const message = {
|
|
517
|
+
requestId,
|
|
518
|
+
messageId,
|
|
519
|
+
messageKind
|
|
520
|
+
};
|
|
521
|
+
const encoded = tunnel.encodeRunnerMessage(message);
|
|
522
|
+
this.#tunnelWs.send(encoded);
|
|
523
|
+
}
|
|
524
|
+
#sendAck(requestId, messageId) {
|
|
525
|
+
if (!this.#tunnelWs || this.#tunnelWs.readyState !== WebSocket2.OPEN) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const message = {
|
|
529
|
+
requestId,
|
|
530
|
+
messageId,
|
|
531
|
+
messageKind: { tag: "Ack", val: null }
|
|
532
|
+
};
|
|
533
|
+
const encoded = tunnel.encodeRunnerMessage(message);
|
|
534
|
+
this.#tunnelWs.send(encoded);
|
|
535
|
+
}
|
|
536
|
+
#startGarbageCollector() {
|
|
537
|
+
if (this.#gcInterval) {
|
|
538
|
+
clearInterval(this.#gcInterval);
|
|
539
|
+
}
|
|
540
|
+
this.#gcInterval = setInterval(() => {
|
|
541
|
+
this.#gc();
|
|
542
|
+
}, GC_INTERVAL);
|
|
543
|
+
}
|
|
544
|
+
#gc() {
|
|
545
|
+
const now = Date.now();
|
|
546
|
+
const messagesToDelete = [];
|
|
547
|
+
for (const [messageId, pendingMessage] of this.#pendingMessages) {
|
|
548
|
+
if (now - pendingMessage.sentAt > MESSAGE_ACK_TIMEOUT) {
|
|
549
|
+
messagesToDelete.push(messageId);
|
|
550
|
+
const requestIdStr = pendingMessage.requestIdStr;
|
|
551
|
+
const pendingRequest = this.#actorPendingRequests.get(requestIdStr);
|
|
552
|
+
if (pendingRequest) {
|
|
553
|
+
pendingRequest.reject(
|
|
554
|
+
new Error("Message acknowledgment timeout")
|
|
555
|
+
);
|
|
556
|
+
if (pendingRequest.streamController) {
|
|
557
|
+
pendingRequest.streamController.error(
|
|
558
|
+
new Error("Message acknowledgment timeout")
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
this.#actorPendingRequests.delete(requestIdStr);
|
|
562
|
+
}
|
|
563
|
+
const webSocket = this.#actorWebSockets.get(requestIdStr);
|
|
564
|
+
if (webSocket) {
|
|
565
|
+
webSocket.close(1e3, "Message acknowledgment timeout");
|
|
566
|
+
this.#actorWebSockets.delete(requestIdStr);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
for (const messageId of messagesToDelete) {
|
|
571
|
+
this.#pendingMessages.delete(messageId);
|
|
572
|
+
console.warn(`Purged unacked message: ${messageId}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
unregisterActor(actor) {
|
|
576
|
+
const actorId = actor.actorId;
|
|
577
|
+
for (const requestId of actor.requests) {
|
|
578
|
+
const pending = this.#actorPendingRequests.get(requestId);
|
|
579
|
+
if (pending) {
|
|
580
|
+
pending.reject(new Error(`Actor ${actorId} stopped`));
|
|
581
|
+
this.#actorPendingRequests.delete(requestId);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
actor.requests.clear();
|
|
585
|
+
for (const webSocketId of actor.webSockets) {
|
|
586
|
+
const ws = this.#actorWebSockets.get(webSocketId);
|
|
587
|
+
if (ws) {
|
|
588
|
+
ws.close(1e3, "Actor stopped");
|
|
589
|
+
this.#actorWebSockets.delete(webSocketId);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
actor.webSockets.clear();
|
|
593
|
+
}
|
|
594
|
+
async #fetch(actorId, request) {
|
|
595
|
+
var _a;
|
|
596
|
+
if (!this.#runner.hasActor(actorId)) {
|
|
597
|
+
(_a = logger()) == null ? void 0 : _a.warn({
|
|
598
|
+
msg: "ignoring request for unknown actor",
|
|
599
|
+
actorId
|
|
600
|
+
});
|
|
601
|
+
return new Response("Actor not found", { status: 404 });
|
|
602
|
+
}
|
|
603
|
+
const fetchHandler = this.#runner.config.fetch(actorId, request);
|
|
604
|
+
if (!fetchHandler) {
|
|
605
|
+
return new Response("Not Implemented", { status: 501 });
|
|
606
|
+
}
|
|
607
|
+
return fetchHandler;
|
|
608
|
+
}
|
|
609
|
+
#connect() {
|
|
610
|
+
var _a;
|
|
611
|
+
if (this.#shutdown) return;
|
|
612
|
+
try {
|
|
613
|
+
this.#tunnelWs = new WebSocket2(this.#pegboardTunnelUrl, {
|
|
614
|
+
headers: {
|
|
615
|
+
"x-rivet-target": "tunnel"
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
this.#tunnelWs.binaryType = "arraybuffer";
|
|
619
|
+
this.#tunnelWs.addEventListener("open", () => {
|
|
620
|
+
this.#reconnectAttempt = 0;
|
|
621
|
+
if (this.#reconnectTimeout) {
|
|
622
|
+
clearTimeout(this.#reconnectTimeout);
|
|
623
|
+
this.#reconnectTimeout = void 0;
|
|
624
|
+
}
|
|
625
|
+
this.#callbacks.onConnected();
|
|
626
|
+
});
|
|
627
|
+
this.#tunnelWs.addEventListener("message", async (event) => {
|
|
628
|
+
var _a2;
|
|
629
|
+
try {
|
|
630
|
+
await this.#handleMessage(event.data);
|
|
631
|
+
} catch (error) {
|
|
632
|
+
(_a2 = logger()) == null ? void 0 : _a2.error({
|
|
633
|
+
msg: "error handling tunnel message",
|
|
634
|
+
error
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
this.#tunnelWs.addEventListener("error", (event) => {
|
|
639
|
+
var _a2;
|
|
640
|
+
(_a2 = logger()) == null ? void 0 : _a2.error({ msg: "tunnel websocket error", event });
|
|
641
|
+
});
|
|
642
|
+
this.#tunnelWs.addEventListener("close", () => {
|
|
643
|
+
this.#callbacks.onDisconnected();
|
|
644
|
+
if (!this.#shutdown) {
|
|
645
|
+
this.#scheduleReconnect();
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
} catch (error) {
|
|
649
|
+
(_a = logger()) == null ? void 0 : _a.error({ msg: "failed to connect tunnel", error });
|
|
650
|
+
if (!this.#shutdown) {
|
|
651
|
+
this.#scheduleReconnect();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
#scheduleReconnect() {
|
|
656
|
+
if (this.#shutdown) return;
|
|
657
|
+
const delay = calculateBackoff(this.#reconnectAttempt, {
|
|
658
|
+
initialDelay: 1e3,
|
|
659
|
+
maxDelay: 3e4,
|
|
660
|
+
multiplier: 2,
|
|
661
|
+
jitter: true
|
|
662
|
+
});
|
|
663
|
+
this.#reconnectAttempt++;
|
|
664
|
+
this.#reconnectTimeout = setTimeout(() => {
|
|
665
|
+
this.#connect();
|
|
666
|
+
}, delay);
|
|
667
|
+
}
|
|
668
|
+
async #handleMessage(data) {
|
|
669
|
+
const message = tunnel.decodeRunnerMessage(new Uint8Array(data));
|
|
670
|
+
if (message.messageKind.tag === "Ack") {
|
|
671
|
+
const msgIdStr = bufferToString(message.messageId);
|
|
672
|
+
const pending = this.#pendingMessages.get(msgIdStr);
|
|
673
|
+
if (pending) {
|
|
674
|
+
this.#pendingMessages.delete(msgIdStr);
|
|
675
|
+
}
|
|
676
|
+
} else {
|
|
677
|
+
this.#sendAck(message.requestId, message.messageId);
|
|
678
|
+
switch (message.messageKind.tag) {
|
|
679
|
+
case "ToServerRequestStart":
|
|
680
|
+
await this.#handleRequestStart(
|
|
681
|
+
message.requestId,
|
|
682
|
+
message.messageKind.val
|
|
683
|
+
);
|
|
684
|
+
break;
|
|
685
|
+
case "ToServerRequestChunk":
|
|
686
|
+
await this.#handleRequestChunk(
|
|
687
|
+
message.requestId,
|
|
688
|
+
message.messageKind.val
|
|
689
|
+
);
|
|
690
|
+
break;
|
|
691
|
+
case "ToServerRequestAbort":
|
|
692
|
+
await this.#handleRequestAbort(message.requestId);
|
|
693
|
+
break;
|
|
694
|
+
case "ToServerWebSocketOpen":
|
|
695
|
+
await this.#handleWebSocketOpen(
|
|
696
|
+
message.requestId,
|
|
697
|
+
message.messageKind.val
|
|
698
|
+
);
|
|
699
|
+
break;
|
|
700
|
+
case "ToServerWebSocketMessage":
|
|
701
|
+
await this.#handleWebSocketMessage(
|
|
702
|
+
message.requestId,
|
|
703
|
+
message.messageKind.val
|
|
704
|
+
);
|
|
705
|
+
break;
|
|
706
|
+
case "ToServerWebSocketClose":
|
|
707
|
+
await this.#handleWebSocketClose(
|
|
708
|
+
message.requestId,
|
|
709
|
+
message.messageKind.val
|
|
710
|
+
);
|
|
711
|
+
break;
|
|
712
|
+
case "ToClientResponseStart":
|
|
713
|
+
this.#handleResponseStart(
|
|
714
|
+
message.requestId,
|
|
715
|
+
message.messageKind.val
|
|
716
|
+
);
|
|
717
|
+
break;
|
|
718
|
+
case "ToClientResponseChunk":
|
|
719
|
+
this.#handleResponseChunk(
|
|
720
|
+
message.requestId,
|
|
721
|
+
message.messageKind.val
|
|
722
|
+
);
|
|
723
|
+
break;
|
|
724
|
+
case "ToClientResponseAbort":
|
|
725
|
+
this.#handleResponseAbort(message.requestId);
|
|
726
|
+
break;
|
|
727
|
+
case "ToClientWebSocketOpen":
|
|
728
|
+
this.#handleWebSocketOpenResponse(
|
|
729
|
+
message.requestId,
|
|
730
|
+
message.messageKind.val
|
|
731
|
+
);
|
|
732
|
+
break;
|
|
733
|
+
case "ToClientWebSocketMessage":
|
|
734
|
+
this.#handleWebSocketMessageResponse(
|
|
735
|
+
message.requestId,
|
|
736
|
+
message.messageKind.val
|
|
737
|
+
);
|
|
738
|
+
break;
|
|
739
|
+
case "ToClientWebSocketClose":
|
|
740
|
+
this.#handleWebSocketCloseResponse(
|
|
741
|
+
message.requestId,
|
|
742
|
+
message.messageKind.val
|
|
743
|
+
);
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
async #handleRequestStart(requestId, req) {
|
|
749
|
+
var _a;
|
|
750
|
+
const requestIdStr = bufferToString(requestId);
|
|
751
|
+
const actor = this.#runner.getActor(req.actorId);
|
|
752
|
+
if (actor) {
|
|
753
|
+
actor.requests.add(requestIdStr);
|
|
754
|
+
}
|
|
755
|
+
try {
|
|
756
|
+
const headers = new Headers();
|
|
757
|
+
for (const [key, value] of req.headers) {
|
|
758
|
+
headers.append(key, value);
|
|
759
|
+
}
|
|
760
|
+
const request = new Request(`http://localhost${req.path}`, {
|
|
761
|
+
method: req.method,
|
|
762
|
+
headers,
|
|
763
|
+
body: req.body ? new Uint8Array(req.body) : void 0
|
|
764
|
+
});
|
|
765
|
+
if (req.stream) {
|
|
766
|
+
const stream = new ReadableStream({
|
|
767
|
+
start: (controller) => {
|
|
768
|
+
const existing = this.#actorPendingRequests.get(requestIdStr);
|
|
769
|
+
if (existing) {
|
|
770
|
+
existing.streamController = controller;
|
|
771
|
+
existing.actorId = req.actorId;
|
|
772
|
+
} else {
|
|
773
|
+
this.#actorPendingRequests.set(requestIdStr, {
|
|
774
|
+
resolve: () => {
|
|
775
|
+
},
|
|
776
|
+
reject: () => {
|
|
777
|
+
},
|
|
778
|
+
streamController: controller,
|
|
779
|
+
actorId: req.actorId
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
const streamingRequest = new Request(request, {
|
|
785
|
+
body: stream,
|
|
786
|
+
duplex: "half"
|
|
787
|
+
});
|
|
788
|
+
const response = await this.#fetch(
|
|
789
|
+
req.actorId,
|
|
790
|
+
streamingRequest
|
|
791
|
+
);
|
|
792
|
+
await this.#sendResponse(requestId, response);
|
|
793
|
+
} else {
|
|
794
|
+
const response = await this.#fetch(req.actorId, request);
|
|
795
|
+
await this.#sendResponse(requestId, response);
|
|
796
|
+
}
|
|
797
|
+
} catch (error) {
|
|
798
|
+
(_a = logger()) == null ? void 0 : _a.error({ msg: "error handling request", error });
|
|
799
|
+
this.#sendResponseError(requestId, 500, "Internal Server Error");
|
|
800
|
+
} finally {
|
|
801
|
+
const actor2 = this.#runner.getActor(req.actorId);
|
|
802
|
+
if (actor2) {
|
|
803
|
+
actor2.requests.delete(requestIdStr);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
async #handleRequestChunk(requestId, chunk) {
|
|
808
|
+
const requestIdStr = bufferToString(requestId);
|
|
809
|
+
const pending = this.#actorPendingRequests.get(requestIdStr);
|
|
810
|
+
if (pending == null ? void 0 : pending.streamController) {
|
|
811
|
+
pending.streamController.enqueue(new Uint8Array(chunk.body));
|
|
812
|
+
if (chunk.finish) {
|
|
813
|
+
pending.streamController.close();
|
|
814
|
+
this.#actorPendingRequests.delete(requestIdStr);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
async #handleRequestAbort(requestId) {
|
|
819
|
+
const requestIdStr = bufferToString(requestId);
|
|
820
|
+
const pending = this.#actorPendingRequests.get(requestIdStr);
|
|
821
|
+
if (pending == null ? void 0 : pending.streamController) {
|
|
822
|
+
pending.streamController.error(new Error("Request aborted"));
|
|
823
|
+
}
|
|
824
|
+
this.#actorPendingRequests.delete(requestIdStr);
|
|
825
|
+
}
|
|
826
|
+
async #sendResponse(requestId, response) {
|
|
827
|
+
const body = response.body ? await response.arrayBuffer() : null;
|
|
828
|
+
const headers = /* @__PURE__ */ new Map();
|
|
829
|
+
response.headers.forEach((value, key) => {
|
|
830
|
+
headers.set(key, value);
|
|
831
|
+
});
|
|
832
|
+
if (body && !headers.has("content-length")) {
|
|
833
|
+
headers.set("content-length", String(body.byteLength));
|
|
834
|
+
}
|
|
835
|
+
this.#sendMessage(requestId, {
|
|
836
|
+
tag: "ToClientResponseStart",
|
|
837
|
+
val: {
|
|
838
|
+
status: response.status,
|
|
839
|
+
headers,
|
|
840
|
+
body: body || null,
|
|
841
|
+
stream: false
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
#sendResponseError(requestId, status, message) {
|
|
846
|
+
const headers = /* @__PURE__ */ new Map();
|
|
847
|
+
headers.set("content-type", "text/plain");
|
|
848
|
+
this.#sendMessage(requestId, {
|
|
849
|
+
tag: "ToClientResponseStart",
|
|
850
|
+
val: {
|
|
851
|
+
status,
|
|
852
|
+
headers,
|
|
853
|
+
body: new TextEncoder().encode(message).buffer,
|
|
854
|
+
stream: false
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
async #handleWebSocketOpen(requestId, open) {
|
|
859
|
+
var _a, _b, _c;
|
|
860
|
+
const webSocketId = bufferToString(requestId);
|
|
861
|
+
const actor = this.#runner.getActor(open.actorId);
|
|
862
|
+
if (!actor) {
|
|
863
|
+
(_a = logger()) == null ? void 0 : _a.warn({
|
|
864
|
+
msg: "ignoring websocket for unknown actor",
|
|
865
|
+
actorId: open.actorId
|
|
866
|
+
});
|
|
867
|
+
this.#sendMessage(requestId, {
|
|
868
|
+
tag: "ToClientWebSocketClose",
|
|
869
|
+
val: {
|
|
870
|
+
code: 1011,
|
|
871
|
+
reason: "Actor not found"
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const websocketHandler = this.#runner.config.websocket;
|
|
877
|
+
if (!websocketHandler) {
|
|
878
|
+
console.error("No websocket handler configured for tunnel");
|
|
879
|
+
(_b = logger()) == null ? void 0 : _b.error({
|
|
880
|
+
msg: "no websocket handler configured for tunnel"
|
|
881
|
+
});
|
|
882
|
+
this.#sendMessage(requestId, {
|
|
883
|
+
tag: "ToClientWebSocketClose",
|
|
884
|
+
val: {
|
|
885
|
+
code: 1011,
|
|
886
|
+
reason: "Not Implemented"
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (actor) {
|
|
892
|
+
actor.webSockets.add(webSocketId);
|
|
893
|
+
}
|
|
894
|
+
try {
|
|
895
|
+
const adapter = new WebSocketTunnelAdapter(
|
|
896
|
+
webSocketId,
|
|
897
|
+
(data, isBinary) => {
|
|
898
|
+
const dataBuffer = typeof data === "string" ? new TextEncoder().encode(data).buffer : data;
|
|
899
|
+
this.#sendMessage(requestId, {
|
|
900
|
+
tag: "ToClientWebSocketMessage",
|
|
901
|
+
val: {
|
|
902
|
+
data: dataBuffer,
|
|
903
|
+
binary: isBinary
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
},
|
|
907
|
+
(code, reason) => {
|
|
908
|
+
this.#sendMessage(requestId, {
|
|
909
|
+
tag: "ToClientWebSocketClose",
|
|
910
|
+
val: {
|
|
911
|
+
code: code || null,
|
|
912
|
+
reason: reason || null
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
this.#actorWebSockets.delete(webSocketId);
|
|
916
|
+
if (actor) {
|
|
917
|
+
actor.webSockets.delete(webSocketId);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
);
|
|
921
|
+
this.#actorWebSockets.set(webSocketId, adapter);
|
|
922
|
+
this.#sendMessage(requestId, {
|
|
923
|
+
tag: "ToClientWebSocketOpen",
|
|
924
|
+
val: null
|
|
925
|
+
});
|
|
926
|
+
adapter._handleOpen();
|
|
927
|
+
const headerInit = {};
|
|
928
|
+
if (open.headers) {
|
|
929
|
+
for (const [k, v] of open.headers) {
|
|
930
|
+
headerInit[k] = v;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
headerInit["Upgrade"] = "websocket";
|
|
934
|
+
headerInit["Connection"] = "Upgrade";
|
|
935
|
+
const request = new Request(`http://localhost${open.path}`, {
|
|
936
|
+
method: "GET",
|
|
937
|
+
headers: headerInit
|
|
938
|
+
});
|
|
939
|
+
await websocketHandler(open.actorId, adapter, request);
|
|
940
|
+
} catch (error) {
|
|
941
|
+
(_c = logger()) == null ? void 0 : _c.error({ msg: "error handling websocket open", error });
|
|
942
|
+
this.#sendMessage(requestId, {
|
|
943
|
+
tag: "ToClientWebSocketClose",
|
|
944
|
+
val: {
|
|
945
|
+
code: 1011,
|
|
946
|
+
reason: "Server Error"
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
this.#actorWebSockets.delete(webSocketId);
|
|
950
|
+
if (actor) {
|
|
951
|
+
actor.webSockets.delete(webSocketId);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
async #handleWebSocketMessage(requestId, msg) {
|
|
956
|
+
const webSocketId = bufferToString(requestId);
|
|
957
|
+
const adapter = this.#actorWebSockets.get(webSocketId);
|
|
958
|
+
if (adapter) {
|
|
959
|
+
const data = msg.binary ? new Uint8Array(msg.data) : new TextDecoder().decode(new Uint8Array(msg.data));
|
|
960
|
+
adapter._handleMessage(data, msg.binary);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
async #handleWebSocketClose(requestId, close) {
|
|
964
|
+
const webSocketId = bufferToString(requestId);
|
|
965
|
+
const adapter = this.#actorWebSockets.get(webSocketId);
|
|
966
|
+
if (adapter) {
|
|
967
|
+
adapter._handleClose(
|
|
968
|
+
close.code || void 0,
|
|
969
|
+
close.reason || void 0
|
|
970
|
+
);
|
|
971
|
+
this.#actorWebSockets.delete(webSocketId);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
#handleResponseStart(requestId, resp) {
|
|
975
|
+
var _a;
|
|
976
|
+
const requestIdStr = bufferToString(requestId);
|
|
977
|
+
const pending = this.#actorPendingRequests.get(requestIdStr);
|
|
978
|
+
if (!pending) {
|
|
979
|
+
(_a = logger()) == null ? void 0 : _a.warn({
|
|
980
|
+
msg: "received response for unknown request",
|
|
981
|
+
requestId: requestIdStr
|
|
982
|
+
});
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
const headers = new Headers();
|
|
986
|
+
for (const [key, value] of resp.headers) {
|
|
987
|
+
headers.append(key, value);
|
|
988
|
+
}
|
|
989
|
+
if (resp.stream) {
|
|
990
|
+
const stream = new ReadableStream({
|
|
991
|
+
start: (controller) => {
|
|
992
|
+
pending.streamController = controller;
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
const response = new Response(stream, {
|
|
996
|
+
status: resp.status,
|
|
997
|
+
headers
|
|
998
|
+
});
|
|
999
|
+
pending.resolve(response);
|
|
1000
|
+
} else {
|
|
1001
|
+
const body = resp.body ? new Uint8Array(resp.body) : null;
|
|
1002
|
+
const response = new Response(body, {
|
|
1003
|
+
status: resp.status,
|
|
1004
|
+
headers
|
|
1005
|
+
});
|
|
1006
|
+
pending.resolve(response);
|
|
1007
|
+
this.#actorPendingRequests.delete(requestIdStr);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
#handleResponseChunk(requestId, chunk) {
|
|
1011
|
+
const requestIdStr = bufferToString(requestId);
|
|
1012
|
+
const pending = this.#actorPendingRequests.get(requestIdStr);
|
|
1013
|
+
if (pending == null ? void 0 : pending.streamController) {
|
|
1014
|
+
pending.streamController.enqueue(new Uint8Array(chunk.body));
|
|
1015
|
+
if (chunk.finish) {
|
|
1016
|
+
pending.streamController.close();
|
|
1017
|
+
this.#actorPendingRequests.delete(requestIdStr);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
#handleResponseAbort(requestId) {
|
|
1022
|
+
const requestIdStr = bufferToString(requestId);
|
|
1023
|
+
const pending = this.#actorPendingRequests.get(requestIdStr);
|
|
1024
|
+
if (pending == null ? void 0 : pending.streamController) {
|
|
1025
|
+
pending.streamController.error(new Error("Response aborted"));
|
|
1026
|
+
}
|
|
1027
|
+
this.#actorPendingRequests.delete(requestIdStr);
|
|
1028
|
+
}
|
|
1029
|
+
#handleWebSocketOpenResponse(requestId, open) {
|
|
1030
|
+
const webSocketId = bufferToString(requestId);
|
|
1031
|
+
const adapter = this.#actorWebSockets.get(webSocketId);
|
|
1032
|
+
if (adapter) {
|
|
1033
|
+
adapter._handleOpen();
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
#handleWebSocketMessageResponse(requestId, msg) {
|
|
1037
|
+
const webSocketId = bufferToString(requestId);
|
|
1038
|
+
const adapter = this.#actorWebSockets.get(webSocketId);
|
|
1039
|
+
if (adapter) {
|
|
1040
|
+
const data = msg.binary ? new Uint8Array(msg.data) : new TextDecoder().decode(new Uint8Array(msg.data));
|
|
1041
|
+
adapter._handleMessage(data, msg.binary);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
#handleWebSocketCloseResponse(requestId, close) {
|
|
1045
|
+
const webSocketId = bufferToString(requestId);
|
|
1046
|
+
const adapter = this.#actorWebSockets.get(webSocketId);
|
|
1047
|
+
if (adapter) {
|
|
1048
|
+
adapter._handleClose(
|
|
1049
|
+
close.code || void 0,
|
|
1050
|
+
close.reason || void 0
|
|
1051
|
+
);
|
|
1052
|
+
this.#actorWebSockets.delete(webSocketId);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
function bufferToString(buffer) {
|
|
1057
|
+
return Buffer.from(buffer).toString("base64");
|
|
1058
|
+
}
|
|
1059
|
+
function generateUuidBuffer() {
|
|
1060
|
+
const buffer = new Uint8Array(16);
|
|
1061
|
+
uuidv4(void 0, buffer);
|
|
1062
|
+
return buffer.buffer;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/mod.ts
|
|
1066
|
+
var KV_EXPIRE = 3e4;
|
|
1067
|
+
var Runner = class {
|
|
1068
|
+
#config;
|
|
1069
|
+
get config() {
|
|
1070
|
+
return this.#config;
|
|
1071
|
+
}
|
|
1072
|
+
#actors = /* @__PURE__ */ new Map();
|
|
1073
|
+
#actorWebSockets = /* @__PURE__ */ new Map();
|
|
1074
|
+
// WebSocket
|
|
1075
|
+
#pegboardWebSocket;
|
|
1076
|
+
runnerId;
|
|
1077
|
+
#lastCommandIdx = -1;
|
|
1078
|
+
#pingLoop;
|
|
1079
|
+
#nextEventIdx = 0n;
|
|
1080
|
+
#started = false;
|
|
1081
|
+
#shutdown = false;
|
|
1082
|
+
#reconnectAttempt = 0;
|
|
1083
|
+
#reconnectTimeout;
|
|
1084
|
+
// Runner lost threshold management
|
|
1085
|
+
#runnerLostThreshold;
|
|
1086
|
+
#runnerLostTimeout;
|
|
1087
|
+
// Event storage for resending
|
|
1088
|
+
#eventHistory = [];
|
|
1089
|
+
#eventPruneInterval;
|
|
1090
|
+
// Command acknowledgment
|
|
1091
|
+
#ackInterval;
|
|
1092
|
+
// KV operations
|
|
1093
|
+
#nextRequestId = 0;
|
|
1094
|
+
#kvRequests = /* @__PURE__ */ new Map();
|
|
1095
|
+
#kvCleanupInterval;
|
|
1096
|
+
// Tunnel for HTTP/WebSocket forwarding
|
|
1097
|
+
#tunnel;
|
|
1098
|
+
constructor(config) {
|
|
1099
|
+
this.#config = config;
|
|
1100
|
+
if (this.#config.logger) setLogger(this.#config.logger);
|
|
1101
|
+
this.#eventPruneInterval = setInterval(() => {
|
|
1102
|
+
this.#pruneOldEvents();
|
|
1103
|
+
}, 6e4);
|
|
1104
|
+
this.#kvCleanupInterval = setInterval(() => {
|
|
1105
|
+
this.#cleanupOldKvRequests();
|
|
1106
|
+
}, 15e3);
|
|
1107
|
+
}
|
|
1108
|
+
// MARK: Manage actors
|
|
1109
|
+
sleepActor(actorId, generation) {
|
|
1110
|
+
const actor = this.getActor(actorId, generation);
|
|
1111
|
+
if (!actor) return;
|
|
1112
|
+
this.#sendActorIntent(actorId, actor.generation, "sleep");
|
|
1113
|
+
}
|
|
1114
|
+
async stopActor(actorId, generation) {
|
|
1115
|
+
const actor = this.#removeActor(actorId, generation);
|
|
1116
|
+
if (!actor) return;
|
|
1117
|
+
if (this.#tunnel) {
|
|
1118
|
+
this.#tunnel.unregisterActor(actor);
|
|
1119
|
+
}
|
|
1120
|
+
try {
|
|
1121
|
+
await this.#config.onActorStop(actorId, actor.generation);
|
|
1122
|
+
} catch (err) {
|
|
1123
|
+
console.error(`Error in onActorStop for actor ${actorId}:`, err);
|
|
1124
|
+
}
|
|
1125
|
+
this.#sendActorStateUpdate(actorId, actor.generation, "stopped");
|
|
1126
|
+
this.#config.onActorStop(actorId, actor.generation).catch((err) => {
|
|
1127
|
+
var _a;
|
|
1128
|
+
(_a = logger()) == null ? void 0 : _a.error({
|
|
1129
|
+
msg: "error in onactorstop for actor",
|
|
1130
|
+
actorId,
|
|
1131
|
+
err
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
#stopAllActors() {
|
|
1136
|
+
var _a;
|
|
1137
|
+
(_a = logger()) == null ? void 0 : _a.info(
|
|
1138
|
+
"stopping all actors due to runner lost threshold exceeded"
|
|
1139
|
+
);
|
|
1140
|
+
const actorIds = Array.from(this.#actors.keys());
|
|
1141
|
+
for (const actorId of actorIds) {
|
|
1142
|
+
this.stopActor(actorId);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
getActor(actorId, generation) {
|
|
1146
|
+
var _a, _b;
|
|
1147
|
+
const actor = this.#actors.get(actorId);
|
|
1148
|
+
if (!actor) {
|
|
1149
|
+
(_a = logger()) == null ? void 0 : _a.error({ msg: "actor not found", actorId });
|
|
1150
|
+
return void 0;
|
|
1151
|
+
}
|
|
1152
|
+
if (generation !== void 0 && actor.generation !== generation) {
|
|
1153
|
+
(_b = logger()) == null ? void 0 : _b.error({
|
|
1154
|
+
msg: "actor generation mismatch",
|
|
1155
|
+
actorId,
|
|
1156
|
+
generation
|
|
1157
|
+
});
|
|
1158
|
+
return void 0;
|
|
1159
|
+
}
|
|
1160
|
+
return actor;
|
|
1161
|
+
}
|
|
1162
|
+
hasActor(actorId, generation) {
|
|
1163
|
+
const actor = this.#actors.get(actorId);
|
|
1164
|
+
return !!actor && (generation === void 0 || actor.generation === generation);
|
|
1165
|
+
}
|
|
1166
|
+
#removeActor(actorId, generation) {
|
|
1167
|
+
var _a, _b, _c;
|
|
1168
|
+
const actor = this.#actors.get(actorId);
|
|
1169
|
+
if (!actor) {
|
|
1170
|
+
(_a = logger()) == null ? void 0 : _a.error({ msg: "actor not found", actorId });
|
|
1171
|
+
return void 0;
|
|
1172
|
+
}
|
|
1173
|
+
if (generation !== void 0 && actor.generation !== generation) {
|
|
1174
|
+
(_b = logger()) == null ? void 0 : _b.error({
|
|
1175
|
+
msg: "actor generation mismatch",
|
|
1176
|
+
actorId,
|
|
1177
|
+
generation
|
|
1178
|
+
});
|
|
1179
|
+
return void 0;
|
|
1180
|
+
}
|
|
1181
|
+
this.#actors.delete(actorId);
|
|
1182
|
+
const actorWebSockets = this.#actorWebSockets.get(actorId);
|
|
1183
|
+
if (actorWebSockets) {
|
|
1184
|
+
for (const ws of actorWebSockets) {
|
|
1185
|
+
try {
|
|
1186
|
+
ws.close(1e3, "Actor stopped");
|
|
1187
|
+
} catch (err) {
|
|
1188
|
+
(_c = logger()) == null ? void 0 : _c.error({
|
|
1189
|
+
msg: "error closing websocket for actor",
|
|
1190
|
+
actorId,
|
|
1191
|
+
err
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
this.#actorWebSockets.delete(actorId);
|
|
1196
|
+
}
|
|
1197
|
+
return actor;
|
|
1198
|
+
}
|
|
1199
|
+
// MARK: Start
|
|
1200
|
+
async start() {
|
|
1201
|
+
var _a;
|
|
1202
|
+
if (this.#started) throw new Error("Cannot call runner.start twice");
|
|
1203
|
+
this.#started = true;
|
|
1204
|
+
(_a = logger()) == null ? void 0 : _a.info("starting runner");
|
|
1205
|
+
try {
|
|
1206
|
+
await this.#openTunnelAndWait();
|
|
1207
|
+
await this.#openPegboardWebSocket();
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
this.#started = false;
|
|
1210
|
+
throw error;
|
|
1211
|
+
}
|
|
1212
|
+
if (!this.#config.noAutoShutdown) {
|
|
1213
|
+
process.on("SIGTERM", this.shutdown.bind(this, false, true));
|
|
1214
|
+
process.on("SIGINT", this.shutdown.bind(this, false, true));
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
// MARK: Shutdown
|
|
1218
|
+
async shutdown(immediate, exit = false) {
|
|
1219
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1220
|
+
(_a = logger()) == null ? void 0 : _a.info({ msg: "starting shutdown...", immediate });
|
|
1221
|
+
this.#shutdown = true;
|
|
1222
|
+
if (this.#reconnectTimeout) {
|
|
1223
|
+
clearTimeout(this.#reconnectTimeout);
|
|
1224
|
+
this.#reconnectTimeout = void 0;
|
|
1225
|
+
}
|
|
1226
|
+
if (this.#runnerLostTimeout) {
|
|
1227
|
+
clearTimeout(this.#runnerLostTimeout);
|
|
1228
|
+
this.#runnerLostTimeout = void 0;
|
|
1229
|
+
}
|
|
1230
|
+
if (this.#pingLoop) {
|
|
1231
|
+
clearInterval(this.#pingLoop);
|
|
1232
|
+
this.#pingLoop = void 0;
|
|
1233
|
+
}
|
|
1234
|
+
if (this.#ackInterval) {
|
|
1235
|
+
clearInterval(this.#ackInterval);
|
|
1236
|
+
this.#ackInterval = void 0;
|
|
1237
|
+
}
|
|
1238
|
+
if (this.#eventPruneInterval) {
|
|
1239
|
+
clearInterval(this.#eventPruneInterval);
|
|
1240
|
+
this.#eventPruneInterval = void 0;
|
|
1241
|
+
}
|
|
1242
|
+
if (this.#kvCleanupInterval) {
|
|
1243
|
+
clearInterval(this.#kvCleanupInterval);
|
|
1244
|
+
this.#kvCleanupInterval = void 0;
|
|
1245
|
+
}
|
|
1246
|
+
for (const request of this.#kvRequests.values()) {
|
|
1247
|
+
request.reject(
|
|
1248
|
+
new Error("WebSocket connection closed during shutdown")
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
this.#kvRequests.clear();
|
|
1252
|
+
if (this.#pegboardWebSocket && this.#pegboardWebSocket.readyState === WebSocket3.OPEN) {
|
|
1253
|
+
const pegboardWebSocket = this.#pegboardWebSocket;
|
|
1254
|
+
if (immediate) {
|
|
1255
|
+
pegboardWebSocket.close(1e3, "Stopping");
|
|
1256
|
+
} else {
|
|
1257
|
+
try {
|
|
1258
|
+
(_b = logger()) == null ? void 0 : _b.info({
|
|
1259
|
+
msg: "sending stopping message",
|
|
1260
|
+
readyState: pegboardWebSocket.readyState
|
|
1261
|
+
});
|
|
1262
|
+
const encoded = protocol.encodeToServer({
|
|
1263
|
+
tag: "ToServerStopping",
|
|
1264
|
+
val: null
|
|
1265
|
+
});
|
|
1266
|
+
if (this.#pegboardWebSocket && this.#pegboardWebSocket.readyState === WebSocket3.OPEN) {
|
|
1267
|
+
this.#pegboardWebSocket.send(encoded);
|
|
1268
|
+
} else {
|
|
1269
|
+
(_c = logger()) == null ? void 0 : _c.error(
|
|
1270
|
+
"WebSocket not available or not open for sending data"
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
const closePromise = new Promise((resolve) => {
|
|
1274
|
+
if (!pegboardWebSocket)
|
|
1275
|
+
throw new Error("missing pegboardWebSocket");
|
|
1276
|
+
pegboardWebSocket.addEventListener("close", (ev) => {
|
|
1277
|
+
var _a2;
|
|
1278
|
+
(_a2 = logger()) == null ? void 0 : _a2.info({
|
|
1279
|
+
msg: "connection closed",
|
|
1280
|
+
code: ev.code,
|
|
1281
|
+
reason: ev.reason.toString()
|
|
1282
|
+
});
|
|
1283
|
+
resolve();
|
|
1284
|
+
});
|
|
1285
|
+
});
|
|
1286
|
+
(_d = logger()) == null ? void 0 : _d.info("closing WebSocket");
|
|
1287
|
+
pegboardWebSocket.close(1e3, "Stopping");
|
|
1288
|
+
await closePromise;
|
|
1289
|
+
(_e = logger()) == null ? void 0 : _e.info("websocket shutdown completed");
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
(_f = logger()) == null ? void 0 : _f.error({
|
|
1292
|
+
msg: "error during websocket shutdown:",
|
|
1293
|
+
error
|
|
1294
|
+
});
|
|
1295
|
+
pegboardWebSocket.close();
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
} else {
|
|
1299
|
+
(_g = logger()) == null ? void 0 : _g.warn("no runner WebSocket to shutdown or already closed");
|
|
1300
|
+
}
|
|
1301
|
+
if (this.#tunnel) {
|
|
1302
|
+
this.#tunnel.shutdown();
|
|
1303
|
+
(_h = logger()) == null ? void 0 : _h.info("tunnel shutdown completed");
|
|
1304
|
+
}
|
|
1305
|
+
if (exit) process.exit(0);
|
|
1306
|
+
this.#config.onShutdown();
|
|
1307
|
+
}
|
|
1308
|
+
// MARK: Networking
|
|
1309
|
+
get pegboardUrl() {
|
|
1310
|
+
const endpoint = this.#config.pegboardEndpoint || this.#config.endpoint;
|
|
1311
|
+
const wsEndpoint = endpoint.replace("http://", "ws://").replace("https://", "wss://");
|
|
1312
|
+
return `${wsEndpoint}?protocol_version=1&namespace=${encodeURIComponent(this.#config.namespace)}&runner_key=${encodeURIComponent(this.#config.runnerKey)}`;
|
|
1313
|
+
}
|
|
1314
|
+
get pegboardTunnelUrl() {
|
|
1315
|
+
const endpoint = this.#config.pegboardRelayEndpoint || this.#config.pegboardEndpoint || this.#config.endpoint;
|
|
1316
|
+
const wsEndpoint = endpoint.replace("http://", "ws://").replace("https://", "wss://");
|
|
1317
|
+
return `${wsEndpoint}?protocol_version=1&namespace=${encodeURIComponent(this.#config.namespace)}&runner_name=${encodeURIComponent(this.#config.runnerName)}&runner_key=${encodeURIComponent(this.#config.runnerKey)}`;
|
|
1318
|
+
}
|
|
1319
|
+
async #openTunnelAndWait() {
|
|
1320
|
+
return new Promise((resolve, reject) => {
|
|
1321
|
+
var _a, _b, _c;
|
|
1322
|
+
const url = this.pegboardTunnelUrl;
|
|
1323
|
+
(_a = logger()) == null ? void 0 : _a.info({ msg: "opening tunnel to:", url });
|
|
1324
|
+
(_b = logger()) == null ? void 0 : _b.info({
|
|
1325
|
+
msg: "current runner id:",
|
|
1326
|
+
runnerId: this.runnerId || "none"
|
|
1327
|
+
});
|
|
1328
|
+
(_c = logger()) == null ? void 0 : _c.info({
|
|
1329
|
+
msg: "active actors count:",
|
|
1330
|
+
actors: this.#actors.size
|
|
1331
|
+
});
|
|
1332
|
+
let connected = false;
|
|
1333
|
+
this.#tunnel = new Tunnel(this, url, {
|
|
1334
|
+
onConnected: () => {
|
|
1335
|
+
var _a2;
|
|
1336
|
+
if (!connected) {
|
|
1337
|
+
connected = true;
|
|
1338
|
+
(_a2 = logger()) == null ? void 0 : _a2.info("tunnel connected");
|
|
1339
|
+
resolve();
|
|
1340
|
+
}
|
|
1341
|
+
},
|
|
1342
|
+
onDisconnected: () => {
|
|
1343
|
+
if (!connected) {
|
|
1344
|
+
reject(new Error("Tunnel connection failed"));
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
this.#tunnel.start();
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
// MARK: Runner protocol
|
|
1352
|
+
async #openPegboardWebSocket() {
|
|
1353
|
+
const WS = await importWebSocket();
|
|
1354
|
+
const ws = new WS(this.pegboardUrl, {
|
|
1355
|
+
headers: {
|
|
1356
|
+
"x-rivet-target": "runner-ws"
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
this.#pegboardWebSocket = ws;
|
|
1360
|
+
ws.addEventListener("open", () => {
|
|
1361
|
+
var _a;
|
|
1362
|
+
(_a = logger()) == null ? void 0 : _a.info("Connected");
|
|
1363
|
+
this.#reconnectAttempt = 0;
|
|
1364
|
+
if (this.#reconnectTimeout) {
|
|
1365
|
+
clearTimeout(this.#reconnectTimeout);
|
|
1366
|
+
this.#reconnectTimeout = void 0;
|
|
1367
|
+
}
|
|
1368
|
+
if (this.#runnerLostTimeout) {
|
|
1369
|
+
clearTimeout(this.#runnerLostTimeout);
|
|
1370
|
+
this.#runnerLostTimeout = void 0;
|
|
1371
|
+
}
|
|
1372
|
+
const init = {
|
|
1373
|
+
name: this.#config.runnerName,
|
|
1374
|
+
version: this.#config.version,
|
|
1375
|
+
totalSlots: this.#config.totalSlots,
|
|
1376
|
+
lastCommandIdx: this.#lastCommandIdx >= 0 ? BigInt(this.#lastCommandIdx) : null,
|
|
1377
|
+
prepopulateActorNames: new Map(
|
|
1378
|
+
Object.entries(this.#config.prepopulateActorNames).map(
|
|
1379
|
+
([name, data]) => [
|
|
1380
|
+
name,
|
|
1381
|
+
{ metadata: JSON.stringify(data.metadata) }
|
|
1382
|
+
]
|
|
1383
|
+
)
|
|
1384
|
+
),
|
|
1385
|
+
metadata: JSON.stringify(this.#config.metadata)
|
|
1386
|
+
};
|
|
1387
|
+
this.#sendToServer({
|
|
1388
|
+
tag: "ToServerInit",
|
|
1389
|
+
val: init
|
|
1390
|
+
});
|
|
1391
|
+
this.#processUnsentKvRequests();
|
|
1392
|
+
const pingInterval = 1e3;
|
|
1393
|
+
const pingLoop = setInterval(() => {
|
|
1394
|
+
var _a2;
|
|
1395
|
+
if (ws.readyState === WebSocket3.OPEN) {
|
|
1396
|
+
this.#sendToServer({
|
|
1397
|
+
tag: "ToServerPing",
|
|
1398
|
+
val: {
|
|
1399
|
+
ts: BigInt(Date.now())
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
} else {
|
|
1403
|
+
clearInterval(pingLoop);
|
|
1404
|
+
(_a2 = logger()) == null ? void 0 : _a2.info("WebSocket not open, stopping ping loop");
|
|
1405
|
+
}
|
|
1406
|
+
}, pingInterval);
|
|
1407
|
+
this.#pingLoop = pingLoop;
|
|
1408
|
+
const ackInterval = 5 * 60 * 1e3;
|
|
1409
|
+
const ackLoop = setInterval(() => {
|
|
1410
|
+
var _a2;
|
|
1411
|
+
if (ws.readyState === WebSocket3.OPEN) {
|
|
1412
|
+
this.#sendCommandAcknowledgment();
|
|
1413
|
+
} else {
|
|
1414
|
+
clearInterval(ackLoop);
|
|
1415
|
+
(_a2 = logger()) == null ? void 0 : _a2.info("WebSocket not open, stopping ack loop");
|
|
1416
|
+
}
|
|
1417
|
+
}, ackInterval);
|
|
1418
|
+
this.#ackInterval = ackLoop;
|
|
1419
|
+
});
|
|
1420
|
+
ws.addEventListener("message", async (ev) => {
|
|
1421
|
+
var _a, _b;
|
|
1422
|
+
let buf;
|
|
1423
|
+
if (ev.data instanceof Blob) {
|
|
1424
|
+
buf = new Uint8Array(await ev.data.arrayBuffer());
|
|
1425
|
+
} else if (Buffer.isBuffer(ev.data)) {
|
|
1426
|
+
buf = new Uint8Array(ev.data);
|
|
1427
|
+
} else {
|
|
1428
|
+
throw new Error("expected binary data, got " + typeof ev.data);
|
|
1429
|
+
}
|
|
1430
|
+
const message = protocol.decodeToClient(buf);
|
|
1431
|
+
if (message.tag === "ToClientInit") {
|
|
1432
|
+
const init = message.val;
|
|
1433
|
+
const hadRunnerId = !!this.runnerId;
|
|
1434
|
+
this.runnerId = init.runnerId;
|
|
1435
|
+
this.#runnerLostThreshold = ((_a = init.metadata) == null ? void 0 : _a.runnerLostThreshold) ? Number(init.metadata.runnerLostThreshold) : void 0;
|
|
1436
|
+
(_b = logger()) == null ? void 0 : _b.info({
|
|
1437
|
+
msg: "received init",
|
|
1438
|
+
runnerId: init.runnerId,
|
|
1439
|
+
lastEventIdx: init.lastEventIdx,
|
|
1440
|
+
runnerLostThreshold: this.#runnerLostThreshold
|
|
1441
|
+
});
|
|
1442
|
+
this.#resendUnacknowledgedEvents(init.lastEventIdx);
|
|
1443
|
+
this.#config.onConnected();
|
|
1444
|
+
} else if (message.tag === "ToClientCommands") {
|
|
1445
|
+
const commands = message.val;
|
|
1446
|
+
this.#handleCommands(commands);
|
|
1447
|
+
} else if (message.tag === "ToClientAckEvents") {
|
|
1448
|
+
throw new Error("TODO");
|
|
1449
|
+
} else if (message.tag === "ToClientKvResponse") {
|
|
1450
|
+
const kvResponse = message.val;
|
|
1451
|
+
this.#handleKvResponse(kvResponse);
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
ws.addEventListener("error", (ev) => {
|
|
1455
|
+
var _a;
|
|
1456
|
+
(_a = logger()) == null ? void 0 : _a.error("WebSocket error:", ev.error);
|
|
1457
|
+
});
|
|
1458
|
+
ws.addEventListener("close", (ev) => {
|
|
1459
|
+
var _a, _b;
|
|
1460
|
+
(_a = logger()) == null ? void 0 : _a.info({
|
|
1461
|
+
msg: "connection closed",
|
|
1462
|
+
code: ev.code,
|
|
1463
|
+
reason: ev.reason.toString()
|
|
1464
|
+
});
|
|
1465
|
+
this.#config.onDisconnected();
|
|
1466
|
+
if (this.#pingLoop) {
|
|
1467
|
+
clearInterval(this.#pingLoop);
|
|
1468
|
+
this.#pingLoop = void 0;
|
|
1469
|
+
}
|
|
1470
|
+
if (this.#ackInterval) {
|
|
1471
|
+
clearInterval(this.#ackInterval);
|
|
1472
|
+
this.#ackInterval = void 0;
|
|
1473
|
+
}
|
|
1474
|
+
if (!this.#shutdown) {
|
|
1475
|
+
if (this.#runnerLostThreshold && this.#runnerLostThreshold > 0) {
|
|
1476
|
+
(_b = logger()) == null ? void 0 : _b.info({
|
|
1477
|
+
msg: "starting runner lost timeout",
|
|
1478
|
+
seconds: this.#runnerLostThreshold / 1e3
|
|
1479
|
+
});
|
|
1480
|
+
this.#runnerLostTimeout = setTimeout(() => {
|
|
1481
|
+
this.#stopAllActors();
|
|
1482
|
+
}, this.#runnerLostThreshold);
|
|
1483
|
+
}
|
|
1484
|
+
this.#scheduleReconnect();
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
#handleCommands(commands) {
|
|
1489
|
+
var _a, _b;
|
|
1490
|
+
(_a = logger()) == null ? void 0 : _a.info({
|
|
1491
|
+
msg: "received commands",
|
|
1492
|
+
commandCount: commands.length
|
|
1493
|
+
});
|
|
1494
|
+
for (const commandWrapper of commands) {
|
|
1495
|
+
(_b = logger()) == null ? void 0 : _b.info({ msg: "received command", commandWrapper });
|
|
1496
|
+
if (commandWrapper.inner.tag === "CommandStartActor") {
|
|
1497
|
+
this.#handleCommandStartActor(commandWrapper);
|
|
1498
|
+
} else if (commandWrapper.inner.tag === "CommandStopActor") {
|
|
1499
|
+
this.#handleCommandStopActor(commandWrapper);
|
|
1500
|
+
}
|
|
1501
|
+
this.#lastCommandIdx = Number(commandWrapper.index);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
#handleCommandStartActor(commandWrapper) {
|
|
1505
|
+
const startCommand = commandWrapper.inner.val;
|
|
1506
|
+
const actorId = startCommand.actorId;
|
|
1507
|
+
const generation = startCommand.generation;
|
|
1508
|
+
const config = startCommand.config;
|
|
1509
|
+
const actorConfig = {
|
|
1510
|
+
name: config.name,
|
|
1511
|
+
key: config.key,
|
|
1512
|
+
createTs: config.createTs,
|
|
1513
|
+
input: config.input ? new Uint8Array(config.input) : null
|
|
1514
|
+
};
|
|
1515
|
+
const instance = {
|
|
1516
|
+
actorId,
|
|
1517
|
+
generation,
|
|
1518
|
+
config: actorConfig,
|
|
1519
|
+
requests: /* @__PURE__ */ new Set(),
|
|
1520
|
+
webSockets: /* @__PURE__ */ new Set()
|
|
1521
|
+
};
|
|
1522
|
+
this.#actors.set(actorId, instance);
|
|
1523
|
+
this.#sendActorStateUpdate(actorId, generation, "running");
|
|
1524
|
+
this.#config.onActorStart(actorId, generation, actorConfig).catch((err) => {
|
|
1525
|
+
var _a;
|
|
1526
|
+
(_a = logger()) == null ? void 0 : _a.error({
|
|
1527
|
+
msg: "error in onactorstart for actor",
|
|
1528
|
+
actorId,
|
|
1529
|
+
err
|
|
1530
|
+
});
|
|
1531
|
+
this.stopActor(actorId, generation);
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
#handleCommandStopActor(commandWrapper) {
|
|
1535
|
+
const stopCommand = commandWrapper.inner.val;
|
|
1536
|
+
const actorId = stopCommand.actorId;
|
|
1537
|
+
const generation = stopCommand.generation;
|
|
1538
|
+
this.stopActor(actorId, generation);
|
|
1539
|
+
}
|
|
1540
|
+
#sendActorIntent(actorId, generation, intentType) {
|
|
1541
|
+
var _a, _b;
|
|
1542
|
+
if (this.#shutdown) {
|
|
1543
|
+
(_a = logger()) == null ? void 0 : _a.warn("Runner is shut down, cannot send actor intent");
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
let actorIntent;
|
|
1547
|
+
if (intentType === "sleep") {
|
|
1548
|
+
actorIntent = { tag: "ActorIntentSleep", val: null };
|
|
1549
|
+
} else if (intentType === "stop") {
|
|
1550
|
+
actorIntent = {
|
|
1551
|
+
tag: "ActorIntentStop",
|
|
1552
|
+
val: null
|
|
1553
|
+
};
|
|
1554
|
+
} else {
|
|
1555
|
+
unreachable(intentType);
|
|
1556
|
+
}
|
|
1557
|
+
const intentEvent = {
|
|
1558
|
+
actorId,
|
|
1559
|
+
generation,
|
|
1560
|
+
intent: actorIntent
|
|
1561
|
+
};
|
|
1562
|
+
const eventIndex = this.#nextEventIdx++;
|
|
1563
|
+
const eventWrapper = {
|
|
1564
|
+
index: eventIndex,
|
|
1565
|
+
inner: {
|
|
1566
|
+
tag: "EventActorIntent",
|
|
1567
|
+
val: intentEvent
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
this.#eventHistory.push({
|
|
1571
|
+
event: eventWrapper,
|
|
1572
|
+
timestamp: Date.now()
|
|
1573
|
+
});
|
|
1574
|
+
(_b = logger()) == null ? void 0 : _b.info({
|
|
1575
|
+
msg: "sending event to server",
|
|
1576
|
+
index: eventWrapper.index,
|
|
1577
|
+
tag: eventWrapper.inner.tag,
|
|
1578
|
+
val: eventWrapper.inner.val
|
|
1579
|
+
});
|
|
1580
|
+
this.#sendToServer({
|
|
1581
|
+
tag: "ToServerEvents",
|
|
1582
|
+
val: [eventWrapper]
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
#sendActorStateUpdate(actorId, generation, stateType) {
|
|
1586
|
+
var _a, _b;
|
|
1587
|
+
if (this.#shutdown) {
|
|
1588
|
+
(_a = logger()) == null ? void 0 : _a.warn(
|
|
1589
|
+
"Runner is shut down, cannot send actor state update"
|
|
1590
|
+
);
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
let actorState;
|
|
1594
|
+
if (stateType === "running") {
|
|
1595
|
+
actorState = { tag: "ActorStateRunning", val: null };
|
|
1596
|
+
} else if (stateType === "stopped") {
|
|
1597
|
+
actorState = {
|
|
1598
|
+
tag: "ActorStateStopped",
|
|
1599
|
+
val: {
|
|
1600
|
+
code: protocol.StopCode.Ok,
|
|
1601
|
+
message: "hello"
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
} else {
|
|
1605
|
+
unreachable(stateType);
|
|
1606
|
+
}
|
|
1607
|
+
const stateUpdateEvent = {
|
|
1608
|
+
actorId,
|
|
1609
|
+
generation,
|
|
1610
|
+
state: actorState
|
|
1611
|
+
};
|
|
1612
|
+
const eventIndex = this.#nextEventIdx++;
|
|
1613
|
+
const eventWrapper = {
|
|
1614
|
+
index: eventIndex,
|
|
1615
|
+
inner: {
|
|
1616
|
+
tag: "EventActorStateUpdate",
|
|
1617
|
+
val: stateUpdateEvent
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1620
|
+
this.#eventHistory.push({
|
|
1621
|
+
event: eventWrapper,
|
|
1622
|
+
timestamp: Date.now()
|
|
1623
|
+
});
|
|
1624
|
+
(_b = logger()) == null ? void 0 : _b.info({
|
|
1625
|
+
msg: "sending event to server",
|
|
1626
|
+
index: eventWrapper.index,
|
|
1627
|
+
tag: eventWrapper.inner.tag,
|
|
1628
|
+
val: eventWrapper.inner.val
|
|
1629
|
+
});
|
|
1630
|
+
this.#sendToServer({
|
|
1631
|
+
tag: "ToServerEvents",
|
|
1632
|
+
val: [eventWrapper]
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
#sendCommandAcknowledgment() {
|
|
1636
|
+
var _a;
|
|
1637
|
+
if (this.#shutdown) {
|
|
1638
|
+
(_a = logger()) == null ? void 0 : _a.warn(
|
|
1639
|
+
"Runner is shut down, cannot send command acknowledgment"
|
|
1640
|
+
);
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
if (this.#lastCommandIdx < 0) {
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
this.#sendToServer({
|
|
1647
|
+
tag: "ToServerAckCommands",
|
|
1648
|
+
val: {
|
|
1649
|
+
lastCommandIdx: BigInt(this.#lastCommandIdx)
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
#handleKvResponse(response) {
|
|
1654
|
+
var _a, _b;
|
|
1655
|
+
const requestId = response.requestId;
|
|
1656
|
+
const request = this.#kvRequests.get(requestId);
|
|
1657
|
+
if (!request) {
|
|
1658
|
+
const msg = "received kv response for unknown request id";
|
|
1659
|
+
if (logger()) {
|
|
1660
|
+
(_a = logger()) == null ? void 0 : _a.error({ msg, requestId });
|
|
1661
|
+
} else {
|
|
1662
|
+
(_b = logger()) == null ? void 0 : _b.error({ msg, requestId });
|
|
1663
|
+
}
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
this.#kvRequests.delete(requestId);
|
|
1667
|
+
if (response.data.tag === "KvErrorResponse") {
|
|
1668
|
+
request.reject(
|
|
1669
|
+
new Error(response.data.val.message || "Unknown KV error")
|
|
1670
|
+
);
|
|
1671
|
+
} else {
|
|
1672
|
+
request.resolve(response.data.val);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
#parseGetResponseSimple(response, requestedKeys) {
|
|
1676
|
+
const responseKeys = [];
|
|
1677
|
+
const responseValues = [];
|
|
1678
|
+
for (const key of response.keys) {
|
|
1679
|
+
responseKeys.push(new Uint8Array(key));
|
|
1680
|
+
}
|
|
1681
|
+
for (const value of response.values) {
|
|
1682
|
+
responseValues.push(new Uint8Array(value));
|
|
1683
|
+
}
|
|
1684
|
+
const result = [];
|
|
1685
|
+
for (const requestedKey of requestedKeys) {
|
|
1686
|
+
let found = false;
|
|
1687
|
+
for (let i = 0; i < responseKeys.length; i++) {
|
|
1688
|
+
if (this.#keysEqual(requestedKey, responseKeys[i])) {
|
|
1689
|
+
result.push(responseValues[i]);
|
|
1690
|
+
found = true;
|
|
1691
|
+
break;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
if (!found) {
|
|
1695
|
+
result.push(null);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
return result;
|
|
1699
|
+
}
|
|
1700
|
+
#keysEqual(key1, key2) {
|
|
1701
|
+
if (key1.length !== key2.length) return false;
|
|
1702
|
+
for (let i = 0; i < key1.length; i++) {
|
|
1703
|
+
if (key1[i] !== key2[i]) return false;
|
|
1704
|
+
}
|
|
1705
|
+
return true;
|
|
1706
|
+
}
|
|
1707
|
+
//#parseGetResponse(response: protocol.KvGetResponse) {
|
|
1708
|
+
// const keys: string[] = [];
|
|
1709
|
+
// const values: Uint8Array[] = [];
|
|
1710
|
+
// const metadata: { version: Uint8Array; createTs: bigint }[] = [];
|
|
1711
|
+
//
|
|
1712
|
+
// for (const key of response.keys) {
|
|
1713
|
+
// keys.push(new TextDecoder().decode(key));
|
|
1714
|
+
// }
|
|
1715
|
+
//
|
|
1716
|
+
// for (const value of response.values) {
|
|
1717
|
+
// values.push(new Uint8Array(value));
|
|
1718
|
+
// }
|
|
1719
|
+
//
|
|
1720
|
+
// for (const meta of response.metadata) {
|
|
1721
|
+
// metadata.push({
|
|
1722
|
+
// version: new Uint8Array(meta.version),
|
|
1723
|
+
// createTs: meta.createTs,
|
|
1724
|
+
// });
|
|
1725
|
+
// }
|
|
1726
|
+
//
|
|
1727
|
+
// return { keys, values, metadata };
|
|
1728
|
+
//}
|
|
1729
|
+
#parseListResponseSimple(response) {
|
|
1730
|
+
const result = [];
|
|
1731
|
+
for (let i = 0; i < response.keys.length; i++) {
|
|
1732
|
+
const key = response.keys[i];
|
|
1733
|
+
const value = response.values[i];
|
|
1734
|
+
if (key && value) {
|
|
1735
|
+
const keyBytes = new Uint8Array(key);
|
|
1736
|
+
const valueBytes = new Uint8Array(value);
|
|
1737
|
+
result.push([keyBytes, valueBytes]);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
return result;
|
|
1741
|
+
}
|
|
1742
|
+
//#parseListResponse(response: protocol.KvListResponse) {
|
|
1743
|
+
// const keys: string[] = [];
|
|
1744
|
+
// const values: Uint8Array[] = [];
|
|
1745
|
+
// const metadata: { version: Uint8Array; createTs: bigint }[] = [];
|
|
1746
|
+
//
|
|
1747
|
+
// for (const key of response.keys) {
|
|
1748
|
+
// keys.push(new TextDecoder().decode(key));
|
|
1749
|
+
// }
|
|
1750
|
+
//
|
|
1751
|
+
// for (const value of response.values) {
|
|
1752
|
+
// values.push(new Uint8Array(value));
|
|
1753
|
+
// }
|
|
1754
|
+
//
|
|
1755
|
+
// for (const meta of response.metadata) {
|
|
1756
|
+
// metadata.push({
|
|
1757
|
+
// version: new Uint8Array(meta.version),
|
|
1758
|
+
// createTs: meta.createTs,
|
|
1759
|
+
// });
|
|
1760
|
+
// }
|
|
1761
|
+
//
|
|
1762
|
+
// return { keys, values, metadata };
|
|
1763
|
+
//}
|
|
1764
|
+
// MARK: KV Operations
|
|
1765
|
+
async kvGet(actorId, keys) {
|
|
1766
|
+
const kvKeys = keys.map(
|
|
1767
|
+
(key) => key.buffer.slice(
|
|
1768
|
+
key.byteOffset,
|
|
1769
|
+
key.byteOffset + key.byteLength
|
|
1770
|
+
)
|
|
1771
|
+
);
|
|
1772
|
+
const requestData = {
|
|
1773
|
+
tag: "KvGetRequest",
|
|
1774
|
+
val: { keys: kvKeys }
|
|
1775
|
+
};
|
|
1776
|
+
const response = await this.#sendKvRequest(actorId, requestData);
|
|
1777
|
+
return this.#parseGetResponseSimple(response, keys);
|
|
1778
|
+
}
|
|
1779
|
+
async kvListAll(actorId, options) {
|
|
1780
|
+
const requestData = {
|
|
1781
|
+
tag: "KvListRequest",
|
|
1782
|
+
val: {
|
|
1783
|
+
query: { tag: "KvListAllQuery", val: null },
|
|
1784
|
+
reverse: (options == null ? void 0 : options.reverse) || null,
|
|
1785
|
+
limit: (options == null ? void 0 : options.limit) !== void 0 ? BigInt(options.limit) : null
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
const response = await this.#sendKvRequest(actorId, requestData);
|
|
1789
|
+
return this.#parseListResponseSimple(response);
|
|
1790
|
+
}
|
|
1791
|
+
async kvListRange(actorId, start, end, exclusive, options) {
|
|
1792
|
+
const startKey = start.buffer.slice(
|
|
1793
|
+
start.byteOffset,
|
|
1794
|
+
start.byteOffset + start.byteLength
|
|
1795
|
+
);
|
|
1796
|
+
const endKey = end.buffer.slice(
|
|
1797
|
+
end.byteOffset,
|
|
1798
|
+
end.byteOffset + end.byteLength
|
|
1799
|
+
);
|
|
1800
|
+
const requestData = {
|
|
1801
|
+
tag: "KvListRequest",
|
|
1802
|
+
val: {
|
|
1803
|
+
query: {
|
|
1804
|
+
tag: "KvListRangeQuery",
|
|
1805
|
+
val: {
|
|
1806
|
+
start: startKey,
|
|
1807
|
+
end: endKey,
|
|
1808
|
+
exclusive: exclusive || false
|
|
1809
|
+
}
|
|
1810
|
+
},
|
|
1811
|
+
reverse: (options == null ? void 0 : options.reverse) || null,
|
|
1812
|
+
limit: (options == null ? void 0 : options.limit) !== void 0 ? BigInt(options.limit) : null
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1815
|
+
const response = await this.#sendKvRequest(actorId, requestData);
|
|
1816
|
+
return this.#parseListResponseSimple(response);
|
|
1817
|
+
}
|
|
1818
|
+
async kvListPrefix(actorId, prefix, options) {
|
|
1819
|
+
const prefixKey = prefix.buffer.slice(
|
|
1820
|
+
prefix.byteOffset,
|
|
1821
|
+
prefix.byteOffset + prefix.byteLength
|
|
1822
|
+
);
|
|
1823
|
+
const requestData = {
|
|
1824
|
+
tag: "KvListRequest",
|
|
1825
|
+
val: {
|
|
1826
|
+
query: {
|
|
1827
|
+
tag: "KvListPrefixQuery",
|
|
1828
|
+
val: { key: prefixKey }
|
|
1829
|
+
},
|
|
1830
|
+
reverse: (options == null ? void 0 : options.reverse) || null,
|
|
1831
|
+
limit: (options == null ? void 0 : options.limit) !== void 0 ? BigInt(options.limit) : null
|
|
1832
|
+
}
|
|
1833
|
+
};
|
|
1834
|
+
const response = await this.#sendKvRequest(actorId, requestData);
|
|
1835
|
+
return this.#parseListResponseSimple(response);
|
|
1836
|
+
}
|
|
1837
|
+
async kvPut(actorId, entries) {
|
|
1838
|
+
const keys = entries.map(
|
|
1839
|
+
([key, _value]) => key.buffer.slice(
|
|
1840
|
+
key.byteOffset,
|
|
1841
|
+
key.byteOffset + key.byteLength
|
|
1842
|
+
)
|
|
1843
|
+
);
|
|
1844
|
+
const values = entries.map(
|
|
1845
|
+
([_key, value]) => value.buffer.slice(
|
|
1846
|
+
value.byteOffset,
|
|
1847
|
+
value.byteOffset + value.byteLength
|
|
1848
|
+
)
|
|
1849
|
+
);
|
|
1850
|
+
const requestData = {
|
|
1851
|
+
tag: "KvPutRequest",
|
|
1852
|
+
val: { keys, values }
|
|
1853
|
+
};
|
|
1854
|
+
await this.#sendKvRequest(actorId, requestData);
|
|
1855
|
+
}
|
|
1856
|
+
async kvDelete(actorId, keys) {
|
|
1857
|
+
const kvKeys = keys.map(
|
|
1858
|
+
(key) => key.buffer.slice(
|
|
1859
|
+
key.byteOffset,
|
|
1860
|
+
key.byteOffset + key.byteLength
|
|
1861
|
+
)
|
|
1862
|
+
);
|
|
1863
|
+
const requestData = {
|
|
1864
|
+
tag: "KvDeleteRequest",
|
|
1865
|
+
val: { keys: kvKeys }
|
|
1866
|
+
};
|
|
1867
|
+
await this.#sendKvRequest(actorId, requestData);
|
|
1868
|
+
}
|
|
1869
|
+
async kvDrop(actorId) {
|
|
1870
|
+
const requestData = {
|
|
1871
|
+
tag: "KvDropRequest",
|
|
1872
|
+
val: null
|
|
1873
|
+
};
|
|
1874
|
+
await this.#sendKvRequest(actorId, requestData);
|
|
1875
|
+
}
|
|
1876
|
+
// MARK: Alarm Operations
|
|
1877
|
+
setAlarm(actorId, alarmTs, generation) {
|
|
1878
|
+
const actor = this.getActor(actorId, generation);
|
|
1879
|
+
if (!actor) return;
|
|
1880
|
+
if (this.#shutdown) {
|
|
1881
|
+
console.warn("Runner is shut down, cannot set alarm");
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
const alarmEvent = {
|
|
1885
|
+
actorId,
|
|
1886
|
+
generation: actor.generation,
|
|
1887
|
+
alarmTs: alarmTs !== null ? BigInt(alarmTs) : null
|
|
1888
|
+
};
|
|
1889
|
+
const eventIndex = this.#nextEventIdx++;
|
|
1890
|
+
const eventWrapper = {
|
|
1891
|
+
index: eventIndex,
|
|
1892
|
+
inner: {
|
|
1893
|
+
tag: "EventActorSetAlarm",
|
|
1894
|
+
val: alarmEvent
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1897
|
+
this.#eventHistory.push({
|
|
1898
|
+
event: eventWrapper,
|
|
1899
|
+
timestamp: Date.now()
|
|
1900
|
+
});
|
|
1901
|
+
this.#sendToServer({
|
|
1902
|
+
tag: "ToServerEvents",
|
|
1903
|
+
val: [eventWrapper]
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
clearAlarm(actorId, generation) {
|
|
1907
|
+
this.setAlarm(actorId, null, generation);
|
|
1908
|
+
}
|
|
1909
|
+
#sendKvRequest(actorId, requestData) {
|
|
1910
|
+
return new Promise((resolve, reject) => {
|
|
1911
|
+
if (this.#shutdown) {
|
|
1912
|
+
reject(new Error("Runner is shut down"));
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
const requestId = this.#nextRequestId++;
|
|
1916
|
+
const isConnected = this.#pegboardWebSocket && this.#pegboardWebSocket.readyState === WebSocket3.OPEN;
|
|
1917
|
+
const requestEntry = {
|
|
1918
|
+
actorId,
|
|
1919
|
+
data: requestData,
|
|
1920
|
+
resolve,
|
|
1921
|
+
reject,
|
|
1922
|
+
sent: false,
|
|
1923
|
+
timestamp: Date.now()
|
|
1924
|
+
};
|
|
1925
|
+
this.#kvRequests.set(requestId, requestEntry);
|
|
1926
|
+
if (isConnected) {
|
|
1927
|
+
this.#sendSingleKvRequest(requestId);
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
#sendSingleKvRequest(requestId) {
|
|
1932
|
+
const request = this.#kvRequests.get(requestId);
|
|
1933
|
+
if (!request || request.sent) return;
|
|
1934
|
+
try {
|
|
1935
|
+
const kvRequest = {
|
|
1936
|
+
actorId: request.actorId,
|
|
1937
|
+
requestId,
|
|
1938
|
+
data: request.data
|
|
1939
|
+
};
|
|
1940
|
+
this.#sendToServer({
|
|
1941
|
+
tag: "ToServerKvRequest",
|
|
1942
|
+
val: kvRequest
|
|
1943
|
+
});
|
|
1944
|
+
request.sent = true;
|
|
1945
|
+
request.timestamp = Date.now();
|
|
1946
|
+
} catch (error) {
|
|
1947
|
+
this.#kvRequests.delete(requestId);
|
|
1948
|
+
request.reject(error);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
#processUnsentKvRequests() {
|
|
1952
|
+
if (!this.#pegboardWebSocket || this.#pegboardWebSocket.readyState !== WebSocket3.OPEN) {
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
let processedCount = 0;
|
|
1956
|
+
for (const [requestId, request] of this.#kvRequests.entries()) {
|
|
1957
|
+
if (!request.sent) {
|
|
1958
|
+
this.#sendSingleKvRequest(requestId);
|
|
1959
|
+
processedCount++;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
if (processedCount > 0) {
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
#sendToServer(message) {
|
|
1966
|
+
var _a, _b;
|
|
1967
|
+
if (this.#shutdown) {
|
|
1968
|
+
(_a = logger()) == null ? void 0 : _a.warn(
|
|
1969
|
+
"Runner is shut down, cannot send message to server"
|
|
1970
|
+
);
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
const encoded = protocol.encodeToServer(message);
|
|
1974
|
+
if (this.#pegboardWebSocket && this.#pegboardWebSocket.readyState === WebSocket3.OPEN) {
|
|
1975
|
+
this.#pegboardWebSocket.send(encoded);
|
|
1976
|
+
} else {
|
|
1977
|
+
(_b = logger()) == null ? void 0 : _b.error(
|
|
1978
|
+
"WebSocket not available or not open for sending data"
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
#scheduleReconnect() {
|
|
1983
|
+
if (this.#shutdown) {
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
const delay = calculateBackoff(this.#reconnectAttempt, {
|
|
1987
|
+
initialDelay: 1e3,
|
|
1988
|
+
maxDelay: 3e4,
|
|
1989
|
+
multiplier: 2,
|
|
1990
|
+
jitter: true
|
|
1991
|
+
});
|
|
1992
|
+
this.#reconnectTimeout = setTimeout(async () => {
|
|
1993
|
+
if (!this.#shutdown) {
|
|
1994
|
+
this.#reconnectAttempt++;
|
|
1995
|
+
await this.#openPegboardWebSocket();
|
|
1996
|
+
}
|
|
1997
|
+
}, delay);
|
|
1998
|
+
}
|
|
1999
|
+
#resendUnacknowledgedEvents(lastEventIdx) {
|
|
2000
|
+
const eventsToResend = this.#eventHistory.filter(
|
|
2001
|
+
(item) => item.event.index > lastEventIdx
|
|
2002
|
+
);
|
|
2003
|
+
if (eventsToResend.length === 0) return;
|
|
2004
|
+
const events = eventsToResend.map((item) => item.event);
|
|
2005
|
+
this.#sendToServer({
|
|
2006
|
+
tag: "ToServerEvents",
|
|
2007
|
+
val: events
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
// TODO(RVT-4986): Prune when server acks events instead of based on old events
|
|
2011
|
+
#pruneOldEvents() {
|
|
2012
|
+
const fiveMinutesAgo = Date.now() - 5 * 60 * 1e3;
|
|
2013
|
+
const originalLength = this.#eventHistory.length;
|
|
2014
|
+
this.#eventHistory = this.#eventHistory.filter(
|
|
2015
|
+
(item) => item.timestamp > fiveMinutesAgo
|
|
2016
|
+
);
|
|
2017
|
+
const prunedCount = originalLength - this.#eventHistory.length;
|
|
2018
|
+
if (prunedCount > 0) {
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
#cleanupOldKvRequests() {
|
|
2022
|
+
const thirtySecondsAgo = Date.now() - KV_EXPIRE;
|
|
2023
|
+
const toDelete = [];
|
|
2024
|
+
for (const [requestId, request] of this.#kvRequests.entries()) {
|
|
2025
|
+
if (request.timestamp < thirtySecondsAgo) {
|
|
2026
|
+
request.reject(
|
|
2027
|
+
new Error(
|
|
2028
|
+
"KV request timed out waiting for WebSocket connection"
|
|
2029
|
+
)
|
|
2030
|
+
);
|
|
2031
|
+
toDelete.push(requestId);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
for (const requestId of toDelete) {
|
|
2035
|
+
this.#kvRequests.delete(requestId);
|
|
2036
|
+
}
|
|
2037
|
+
if (toDelete.length > 0) {
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
};
|
|
2041
|
+
export {
|
|
2042
|
+
Runner
|
|
2043
|
+
};
|
|
2044
|
+
//# sourceMappingURL=mod.js.map
|