@mulmoclaude/core 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/remote-host/health.d.ts +13 -0
- package/dist/remote-host/index.cjs +11 -30
- package/dist/remote-host/index.d.ts +2 -0
- package/dist/remote-host/index.js +2 -24
- package/dist/remote-host/server/firestoreSafeResult.d.ts +3 -1
- package/dist/remote-host/server/hostRunner.d.ts +4 -1
- package/dist/remote-host/server/index.cjs +333 -39
- package/dist/remote-host/server/index.cjs.map +1 -1
- package/dist/remote-host/server/index.d.ts +6 -1
- package/dist/remote-host/server/index.js +321 -37
- package/dist/remote-host/server/index.js.map +1 -1
- package/dist/remote-host/server/presenceBeat.d.ts +18 -0
- package/dist/remote-host/server/presenceProbe.d.ts +23 -0
- package/dist/remote-host/server/resilientRunner.d.ts +24 -0
- package/dist/remote-host-D_BRFHcI.js +36 -0
- package/dist/remote-host-D_BRFHcI.js.map +1 -0
- package/dist/remote-host-DkDVxNim.cjs +95 -0
- package/dist/remote-host-DkDVxNim.cjs.map +1 -0
- package/package.json +2 -2
- package/dist/remote-host/index.cjs.map +0 -1
- package/dist/remote-host/index.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as errorMessage } from "../../dist-Cwk0e12G.js";
|
|
2
|
-
import {
|
|
3
|
-
import { deleteDoc, getFirestore, onSnapshot, query, runTransaction, serverTimestamp, setDoc, updateDoc, where } from "firebase/firestore";
|
|
2
|
+
import { a as hostDoc, i as commandsCollection, n as buildHostPresence, o as isExpired, r as byCreatedAt } from "../../remote-host-D_BRFHcI.js";
|
|
3
|
+
import { deleteDoc, getDocFromServer, getFirestore, onSnapshot, query, runTransaction, serverTimestamp, setDoc, updateDoc, where } from "firebase/firestore";
|
|
4
4
|
import { GoogleAuthProvider, getAuth, initializeAuth, signInWithCredential, signOut } from "firebase/auth";
|
|
5
5
|
import { deleteApp, initializeApp } from "firebase/app";
|
|
6
6
|
import { getStorage } from "firebase/storage";
|
|
@@ -9,25 +9,36 @@ var isPlainObject = (value) => {
|
|
|
9
9
|
return proto === Object.prototype || proto === null;
|
|
10
10
|
};
|
|
11
11
|
var isWalkable = (value) => typeof value === "object" && value !== null && (Array.isArray(value) || isPlainObject(value));
|
|
12
|
-
|
|
13
|
-
var
|
|
12
|
+
var childPath = (prefix, key) => prefix ? `${prefix}.${key}` : String(key);
|
|
13
|
+
var withCycleGuard = (value, ancestors, path, walk) => {
|
|
14
|
+
if (ancestors.has(value)) throw new Error(`circular reference at ${path || "(root)"} — Firestore cannot serialise it`);
|
|
15
|
+
ancestors.add(value);
|
|
16
|
+
try {
|
|
17
|
+
return walk();
|
|
18
|
+
} finally {
|
|
19
|
+
ancestors.delete(value);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var pathsIn = (value, prefix, ancestors) => {
|
|
14
23
|
if (value === void 0) return [prefix || "(root)"];
|
|
15
24
|
if (!isWalkable(value)) return [];
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
return withCycleGuard(value, ancestors, prefix, () => Array.isArray(value) ? Array.from(value).flatMap((item, index) => pathsIn(item, childPath(prefix, index), ancestors)) : Object.entries(value).flatMap(([key, item]) => pathsIn(item, childPath(prefix, key), ancestors)));
|
|
26
|
+
};
|
|
27
|
+
/** Every path holding `undefined`, in `a.b.0.c` form. Empty when the value is safe to write.
|
|
28
|
+
* Throws on a circular reply, naming the path it closed the loop at. */
|
|
29
|
+
var undefinedPaths = (value, prefix = "") => pathsIn(value, prefix, /* @__PURE__ */ new WeakSet());
|
|
30
|
+
var strippedIn = (value, prefix, ancestors) => {
|
|
31
|
+
if (value === void 0) return null;
|
|
32
|
+
if (!isWalkable(value)) return value;
|
|
33
|
+
return withCycleGuard(value, ancestors, prefix, () => Array.isArray(value) ? Array.from(value, (item, index) => strippedIn(item, childPath(prefix, index), ancestors)) : Object.fromEntries(Object.entries(value).flatMap(([key, item]) => item === void 0 ? [] : [[key, strippedIn(item, childPath(prefix, key), ancestors)]])));
|
|
19
34
|
};
|
|
20
35
|
/**
|
|
21
36
|
* The same value with every `undefined` removed — object keys dropped, array holes
|
|
22
37
|
* turned into `null` so the surrounding indexes still line up with what the sender
|
|
23
38
|
* meant. Returns `unknown` because a stripped object is no longer the input type.
|
|
39
|
+
* Throws on a circular reply, same as `undefinedPaths`.
|
|
24
40
|
*/
|
|
25
|
-
var stripUndefined = (value) =>
|
|
26
|
-
if (value === void 0) return null;
|
|
27
|
-
if (!isWalkable(value)) return value;
|
|
28
|
-
if (Array.isArray(value)) return Array.from(value, (item) => stripUndefined(item));
|
|
29
|
-
return Object.fromEntries(Object.entries(value).flatMap(([key, item]) => item === void 0 ? [] : [[key, stripUndefined(item)]]));
|
|
30
|
-
};
|
|
41
|
+
var stripUndefined = (value) => strippedIn(value, "", /* @__PURE__ */ new WeakSet());
|
|
31
42
|
/** Does `path` match `pattern`, where `*` stands for exactly one segment?
|
|
32
43
|
* Segment-wise rather than by regex: no escaping of user-supplied dots, and no
|
|
33
44
|
* pattern can turn into a catastrophic backtrack. */
|
|
@@ -49,6 +60,42 @@ var matchesPathPattern = (path, pattern) => {
|
|
|
49
60
|
*/
|
|
50
61
|
var unexpectedPaths = (paths, expected = []) => paths.filter((path) => !expected.some((pattern) => matchesPathPattern(path, pattern)));
|
|
51
62
|
//#endregion
|
|
63
|
+
//#region src/remote-host/server/presenceBeat.ts
|
|
64
|
+
var PRESENCE_STALE_BEATS = 3;
|
|
65
|
+
var errorText$1 = (error) => error instanceof Error ? error.message : String(error);
|
|
66
|
+
var notify = (report, onThrow) => {
|
|
67
|
+
try {
|
|
68
|
+
report();
|
|
69
|
+
} catch (error) {
|
|
70
|
+
onThrow?.(error);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var createPresenceBeat = (deps) => {
|
|
74
|
+
const now = deps.now ?? Date.now;
|
|
75
|
+
const state = { lastAckMs: now() };
|
|
76
|
+
const announce = (online) => {
|
|
77
|
+
const fail = (error) => notify(() => deps.onError(errorText$1(error)));
|
|
78
|
+
const ack = () => notify(() => {
|
|
79
|
+
state.lastAckMs = now();
|
|
80
|
+
});
|
|
81
|
+
notify(() => {
|
|
82
|
+
deps.write(online).then(ack, fail);
|
|
83
|
+
}, fail);
|
|
84
|
+
};
|
|
85
|
+
const beat = () => {
|
|
86
|
+
const silentMs = now() - state.lastAckMs;
|
|
87
|
+
if (silentMs >= deps.staleAfterMs) {
|
|
88
|
+
notify(() => deps.onStale(silentMs));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
announce(true);
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
announce,
|
|
95
|
+
beat
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
//#endregion
|
|
52
99
|
//#region src/remote-host/server/hostRunner.ts
|
|
53
100
|
var DEFAULT_HEARTBEAT_MS = 6e4;
|
|
54
101
|
var TRANSIENT_LISTEN_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
@@ -57,6 +104,7 @@ var TRANSIENT_LISTEN_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
57
104
|
"deadline-exceeded",
|
|
58
105
|
"internal",
|
|
59
106
|
"resource-exhausted",
|
|
107
|
+
"unauthenticated",
|
|
60
108
|
"unavailable"
|
|
61
109
|
]);
|
|
62
110
|
var isRecord$1 = (value) => typeof value === "object" && value !== null;
|
|
@@ -64,6 +112,8 @@ var listenErrorCode = (error) => isRecord$1(error) && typeof error.code === "str
|
|
|
64
112
|
var classifyListenerError = (error) => TRANSIENT_LISTEN_ERROR_CODES.has(listenErrorCode(error)) ? "transient" : "fatal";
|
|
65
113
|
var BASE_LISTEN_RETRY_MS = 1e3;
|
|
66
114
|
var MAX_LISTEN_RETRY_MS = 3e4;
|
|
115
|
+
var LISTEN_RETRY_WINDOW_MS = 5 * 6e4;
|
|
116
|
+
var shouldGiveUpListening = (downSinceMs, now, windowMs = LISTEN_RETRY_WINDOW_MS) => now - downSinceMs >= windowMs;
|
|
67
117
|
var backoffDelayMs = (attempt) => Math.min(MAX_LISTEN_RETRY_MS, BASE_LISTEN_RETRY_MS * 2 ** attempt);
|
|
68
118
|
var noop$2 = () => void 0;
|
|
69
119
|
var writeError = (ref, code, message) => updateDoc(ref, {
|
|
@@ -187,7 +237,9 @@ function handleListenError(run, error) {
|
|
|
187
237
|
message: error.message
|
|
188
238
|
});
|
|
189
239
|
if (run.stopped) return;
|
|
190
|
-
|
|
240
|
+
const now = Date.now();
|
|
241
|
+
run.downSinceMs ??= now;
|
|
242
|
+
if (classifyListenerError(error) === "fatal" || shouldGiveUpListening(run.downSinceMs, now)) {
|
|
191
243
|
run.goOffline();
|
|
192
244
|
return;
|
|
193
245
|
}
|
|
@@ -198,6 +250,7 @@ function subscribeCommands(run) {
|
|
|
198
250
|
if (run.stopped) return;
|
|
199
251
|
run.unsubscribe = onSnapshot(run.queuedCommands, (snapshot) => {
|
|
200
252
|
run.attempt = 0;
|
|
253
|
+
run.downSinceMs = null;
|
|
201
254
|
dispatchAddedCommands(run.ctx, snapshot);
|
|
202
255
|
}, (error) => handleListenError(run, error));
|
|
203
256
|
}
|
|
@@ -209,7 +262,8 @@ var listenForCommands = (queuedCommands, ctx, goOffline) => {
|
|
|
209
262
|
stopped: false,
|
|
210
263
|
unsubscribe: noop$2,
|
|
211
264
|
retryTimer: null,
|
|
212
|
-
attempt: 0
|
|
265
|
+
attempt: 0,
|
|
266
|
+
downSinceMs: null
|
|
213
267
|
};
|
|
214
268
|
subscribeCommands(run);
|
|
215
269
|
return () => {
|
|
@@ -218,36 +272,265 @@ var listenForCommands = (queuedCommands, ctx, goOffline) => {
|
|
|
218
272
|
run.unsubscribe();
|
|
219
273
|
};
|
|
220
274
|
};
|
|
221
|
-
var
|
|
275
|
+
var heartbeatMs = (options) => options.heartbeatMs ?? 6e4;
|
|
276
|
+
var presenceStaleAfterMs = (options = {}) => heartbeatMs(options) * 3;
|
|
277
|
+
var buildPresenceBeat = (firestore, channel, handlers, options, onStale) => {
|
|
222
278
|
const presence = hostDoc(firestore, channel);
|
|
223
|
-
const
|
|
224
|
-
|
|
279
|
+
const report = (message) => options.onEvent?.({
|
|
280
|
+
phase: "error",
|
|
281
|
+
method: "presence",
|
|
282
|
+
message
|
|
283
|
+
});
|
|
284
|
+
return createPresenceBeat({
|
|
285
|
+
write: (online) => setDoc(presence, {
|
|
225
286
|
...buildHostPresence(channel, handlers, online),
|
|
226
287
|
updatedAt: serverTimestamp()
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
288
|
+
}),
|
|
289
|
+
onError: (message) => report(`presence write failed: ${message}`),
|
|
290
|
+
onStale: (silentMs) => {
|
|
291
|
+
report(`no presence write acknowledged for ${Math.round(silentMs / 1e3)}s — the remote cannot see this host`);
|
|
292
|
+
onStale();
|
|
293
|
+
},
|
|
294
|
+
staleAfterMs: presenceStaleAfterMs(options)
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
var shutDown = (run, presence) => {
|
|
298
|
+
if (run.closed) return;
|
|
299
|
+
run.closed = true;
|
|
300
|
+
if (run.beat) clearInterval(run.beat);
|
|
301
|
+
run.beat = null;
|
|
302
|
+
presence.announce(false);
|
|
303
|
+
run.stopListening();
|
|
304
|
+
};
|
|
305
|
+
var startHostRunner = (firestore, channel, handlers, options = {}) => {
|
|
306
|
+
const run = {
|
|
307
|
+
beat: null,
|
|
308
|
+
stopListening: noop$2,
|
|
309
|
+
closed: false
|
|
231
310
|
};
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
311
|
+
const presence = buildPresenceBeat(firestore, channel, handlers, options, goOffline);
|
|
312
|
+
function goOffline() {
|
|
313
|
+
if (run.closed) return;
|
|
314
|
+
shutDown(run, presence);
|
|
315
|
+
options.onClosed?.();
|
|
316
|
+
}
|
|
317
|
+
presence.announce(true);
|
|
318
|
+
run.beat = setInterval(presence.beat, heartbeatMs(options));
|
|
319
|
+
run.stopListening = listenForCommands(query(commandsCollection(firestore, channel), where("status", "==", "queued")), {
|
|
236
320
|
firestore,
|
|
237
321
|
handlers,
|
|
238
322
|
options,
|
|
239
323
|
uid: channel.uid
|
|
324
|
+
}, goOffline);
|
|
325
|
+
return () => shutDown(run, presence);
|
|
326
|
+
};
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/remote-host/server/resilientRunner.ts
|
|
329
|
+
var ONE_SECOND_MS$1 = 1e3;
|
|
330
|
+
var ONE_MINUTE_MS = 60 * ONE_SECOND_MS$1;
|
|
331
|
+
var RECONNECT_BASE_MS = ONE_SECOND_MS$1;
|
|
332
|
+
var RECONNECT_MAX_MS = ONE_MINUTE_MS;
|
|
333
|
+
var SETTLE_MS = ONE_MINUTE_MS;
|
|
334
|
+
var GIVE_UP_MS = 5 * ONE_MINUTE_MS;
|
|
335
|
+
var PROBE_INTERVAL_MS = 90 * ONE_SECOND_MS$1;
|
|
336
|
+
var reconnectDelayMs = (attempt) => Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** attempt);
|
|
337
|
+
var errorText = (error) => error instanceof Error ? error.message : String(error);
|
|
338
|
+
var scheduleWithTimeout = (task, delayMs) => {
|
|
339
|
+
const timer = setTimeout(task, delayMs);
|
|
340
|
+
return () => clearTimeout(timer);
|
|
341
|
+
};
|
|
342
|
+
var setState = (ctx, next) => {
|
|
343
|
+
ctx.state = next;
|
|
344
|
+
try {
|
|
345
|
+
ctx.deps.onHealth?.({
|
|
346
|
+
state: next,
|
|
347
|
+
lastError: ctx.lastError,
|
|
348
|
+
changedAt: ctx.now()
|
|
349
|
+
});
|
|
350
|
+
} catch (error) {
|
|
351
|
+
ctx.deps.log.warn(`host runner health observer threw: ${errorText(error)}`);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
var clearTimer = (ctx) => {
|
|
355
|
+
ctx.cancelTimer?.();
|
|
356
|
+
ctx.cancelTimer = null;
|
|
357
|
+
};
|
|
358
|
+
var clearProbe = (ctx) => {
|
|
359
|
+
ctx.cancelProbe?.();
|
|
360
|
+
ctx.cancelProbe = null;
|
|
361
|
+
};
|
|
362
|
+
function scheduleProbe(ctx) {
|
|
363
|
+
if (ctx.stopped || !ctx.deps.checkAlive) return;
|
|
364
|
+
clearProbe(ctx);
|
|
365
|
+
ctx.cancelProbe = ctx.schedule(() => void runProbe(ctx), PROBE_INTERVAL_MS);
|
|
366
|
+
}
|
|
367
|
+
var STALE_REASON = "presence went stale — the phone can no longer see this host";
|
|
368
|
+
async function askAlive(ctx) {
|
|
369
|
+
try {
|
|
370
|
+
return {
|
|
371
|
+
alive: await ctx.deps.checkAlive?.() ?? null,
|
|
372
|
+
reason: STALE_REASON
|
|
373
|
+
};
|
|
374
|
+
} catch (error) {
|
|
375
|
+
return {
|
|
376
|
+
alive: false,
|
|
377
|
+
reason: `presence probe failed: ${errorText(error)}`
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async function runProbe(ctx) {
|
|
382
|
+
ctx.cancelProbe = null;
|
|
383
|
+
if (ctx.stopped || ctx.state !== "online") return;
|
|
384
|
+
const answer = await askAlive(ctx);
|
|
385
|
+
if (ctx.stopped || ctx.state !== "online") return;
|
|
386
|
+
if (answer.alive === false) {
|
|
387
|
+
ctx.lastError = answer.reason;
|
|
388
|
+
ctx.deps.log.warn(`host runner ${answer.reason}`);
|
|
389
|
+
onUnderlyingClosed(ctx);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
scheduleProbe(ctx);
|
|
393
|
+
}
|
|
394
|
+
var releaseUnderlying = (ctx) => {
|
|
395
|
+
const stop = ctx.stopUnderlying;
|
|
396
|
+
ctx.stopUnderlying = null;
|
|
397
|
+
try {
|
|
398
|
+
stop?.();
|
|
399
|
+
} catch (error) {
|
|
400
|
+
ctx.deps.log.warn(`host runner teardown failed: ${errorText(error)}`);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
var markRecovered = (ctx) => {
|
|
404
|
+
ctx.downSinceMs = null;
|
|
405
|
+
ctx.attempt = 0;
|
|
406
|
+
ctx.lastError = null;
|
|
407
|
+
if (ctx.state === "online") return;
|
|
408
|
+
ctx.deps.log.info("host runner re-subscribed");
|
|
409
|
+
setState(ctx, "online");
|
|
410
|
+
scheduleProbe(ctx);
|
|
411
|
+
};
|
|
412
|
+
async function settle(ctx) {
|
|
413
|
+
ctx.cancelTimer = null;
|
|
414
|
+
if (ctx.stopped) return;
|
|
415
|
+
const answer = await askAlive(ctx);
|
|
416
|
+
if (ctx.stopped) return;
|
|
417
|
+
if (answer.alive === false) {
|
|
418
|
+
ctx.lastError = answer.reason;
|
|
419
|
+
ctx.deps.log.warn(`host runner is up but still unreachable — ${answer.reason}`);
|
|
420
|
+
onUnderlyingClosed(ctx);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
markRecovered(ctx);
|
|
424
|
+
}
|
|
425
|
+
var giveUp = (ctx) => {
|
|
426
|
+
ctx.stopped = true;
|
|
427
|
+
clearProbe(ctx);
|
|
428
|
+
ctx.deps.log.warn(`host runner stayed down for ${Math.round(GIVE_UP_MS / ONE_SECOND_MS$1)}s, giving up (${ctx.lastError ?? "no error reported"})`);
|
|
429
|
+
setState(ctx, "offline");
|
|
430
|
+
ctx.deps.options.onClosed?.();
|
|
431
|
+
};
|
|
432
|
+
function scheduleRelaunch(ctx) {
|
|
433
|
+
const delayMs = reconnectDelayMs(ctx.attempt);
|
|
434
|
+
ctx.attempt += 1;
|
|
435
|
+
ctx.deps.log.warn(`host runner closed (${ctx.lastError ?? "no error reported"}); re-subscribing in ${Math.round(delayMs / ONE_SECOND_MS$1)}s`);
|
|
436
|
+
if (ctx.state !== "reconnecting") setState(ctx, "reconnecting");
|
|
437
|
+
ctx.cancelTimer = ctx.schedule(() => launch(ctx), delayMs);
|
|
438
|
+
}
|
|
439
|
+
function onUnderlyingClosed(ctx) {
|
|
440
|
+
if (ctx.stopped) return;
|
|
441
|
+
releaseUnderlying(ctx);
|
|
442
|
+
clearTimer(ctx);
|
|
443
|
+
clearProbe(ctx);
|
|
444
|
+
ctx.downSinceMs ??= ctx.now();
|
|
445
|
+
if (ctx.now() - ctx.downSinceMs >= GIVE_UP_MS) giveUp(ctx);
|
|
446
|
+
else scheduleRelaunch(ctx);
|
|
447
|
+
}
|
|
448
|
+
var CHANNEL_METHODS = /* @__PURE__ */ new Set(["listen", "presence"]);
|
|
449
|
+
var runnerOptions = (ctx) => ({
|
|
450
|
+
...ctx.deps.options,
|
|
451
|
+
onEvent: (event) => {
|
|
452
|
+
if (event.phase === "error") {
|
|
453
|
+
const text = `${event.method}: ${event.message ?? "no detail"}`;
|
|
454
|
+
if (CHANNEL_METHODS.has(event.method)) ctx.lastError = text;
|
|
455
|
+
ctx.deps.log.warn(`host runner event error — ${text}`);
|
|
456
|
+
}
|
|
457
|
+
ctx.deps.options.onEvent?.(event);
|
|
458
|
+
},
|
|
459
|
+
onClosed: () => onUnderlyingClosed(ctx)
|
|
460
|
+
});
|
|
461
|
+
function launch(ctx) {
|
|
462
|
+
ctx.cancelTimer = null;
|
|
463
|
+
if (ctx.stopped) return;
|
|
464
|
+
try {
|
|
465
|
+
ctx.stopUnderlying = ctx.deps.start(runnerOptions(ctx));
|
|
466
|
+
} catch (error) {
|
|
467
|
+
ctx.lastError = errorText(error);
|
|
468
|
+
onUnderlyingClosed(ctx);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
ctx.cancelTimer = ctx.schedule(() => void settle(ctx), SETTLE_MS);
|
|
472
|
+
}
|
|
473
|
+
function startResilientHostRunner(deps) {
|
|
474
|
+
const ctx = {
|
|
475
|
+
deps,
|
|
476
|
+
schedule: deps.schedule ?? scheduleWithTimeout,
|
|
477
|
+
now: deps.now ?? Date.now,
|
|
478
|
+
stopUnderlying: null,
|
|
479
|
+
cancelTimer: null,
|
|
480
|
+
attempt: 0,
|
|
481
|
+
downSinceMs: null,
|
|
482
|
+
lastError: null,
|
|
483
|
+
state: "online",
|
|
484
|
+
stopped: false,
|
|
485
|
+
cancelProbe: null
|
|
240
486
|
};
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
options.onClosed?.();
|
|
245
|
-
};
|
|
246
|
-
const stopListening = listenForCommands(queuedCommands, ctx, goOffline);
|
|
487
|
+
setState(ctx, "online");
|
|
488
|
+
launch(ctx);
|
|
489
|
+
scheduleProbe(ctx);
|
|
247
490
|
return () => {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
491
|
+
ctx.stopped = true;
|
|
492
|
+
clearTimer(ctx);
|
|
493
|
+
clearProbe(ctx);
|
|
494
|
+
releaseUnderlying(ctx);
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
//#endregion
|
|
498
|
+
//#region src/remote-host/server/presenceProbe.ts
|
|
499
|
+
var ONE_SECOND_MS = 1e3;
|
|
500
|
+
var PRESENCE_STALE_MS = presenceStaleAfterMs();
|
|
501
|
+
var PROBE_TIMEOUT_MS = 30 * ONE_SECOND_MS;
|
|
502
|
+
var hasToMillis = (value) => "toMillis" in value && typeof Reflect.get(value, "toMillis") === "function";
|
|
503
|
+
var asMillis = (value) => {
|
|
504
|
+
if (typeof value === "number") return value;
|
|
505
|
+
if (typeof value === "object" && value !== null && hasToMillis(value)) return value.toMillis();
|
|
506
|
+
return null;
|
|
507
|
+
};
|
|
508
|
+
/** Judge a presence document that was read successfully. Exported for its own test:
|
|
509
|
+
* the freshness rule is the part worth pinning, and it needs no Firestore. */
|
|
510
|
+
var presenceIsFresh = (data, now, staleAfterMs = PRESENCE_STALE_MS) => {
|
|
511
|
+
if (!data) return null;
|
|
512
|
+
const updatedAt = asMillis(data.updatedAt);
|
|
513
|
+
if (updatedAt === null) return null;
|
|
514
|
+
if (data.online === false) return null;
|
|
515
|
+
return now - updatedAt < staleAfterMs;
|
|
516
|
+
};
|
|
517
|
+
/** Reject once `timeoutMs` has passed, so a stalled read answers instead of hanging. */
|
|
518
|
+
var withTimeout = async (work, timeoutMs) => {
|
|
519
|
+
const timer = { handle: null };
|
|
520
|
+
const deadline = new Promise((_, reject) => {
|
|
521
|
+
timer.handle = setTimeout(() => reject(/* @__PURE__ */ new Error(`presence read did not answer within ${Math.round(timeoutMs / ONE_SECOND_MS)}s`)), timeoutMs);
|
|
522
|
+
});
|
|
523
|
+
try {
|
|
524
|
+
return await Promise.race([work, deadline]);
|
|
525
|
+
} finally {
|
|
526
|
+
if (timer.handle) clearTimeout(timer.handle);
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
/** Reads the host's own presence document from the server and reports whether it is fresh. */
|
|
530
|
+
var createPresenceProbe = (deps) => {
|
|
531
|
+
const now = deps.now ?? Date.now;
|
|
532
|
+
return async () => {
|
|
533
|
+
return presenceIsFresh((await withTimeout(getDocFromServer(hostDoc(deps.firestore(), deps.channel)), deps.timeoutMs ?? PROBE_TIMEOUT_MS)).data(), now(), deps.staleAfterMs ?? PRESENCE_STALE_MS);
|
|
251
534
|
};
|
|
252
535
|
};
|
|
253
536
|
//#endregion
|
|
@@ -258,6 +541,7 @@ var silentLogger = {
|
|
|
258
541
|
warn: noop$1,
|
|
259
542
|
debug: noop$1
|
|
260
543
|
};
|
|
544
|
+
var eventText = (event) => event.message ? `${event.phase} ${event.method} — ${event.message}` : `${event.phase} ${event.method}`;
|
|
261
545
|
var createRemoteHost = (deps) => {
|
|
262
546
|
const log = deps.log ?? silentLogger;
|
|
263
547
|
let stopRunner = null;
|
|
@@ -283,7 +567,7 @@ var createRemoteHost = (deps) => {
|
|
|
283
567
|
uid,
|
|
284
568
|
hostId: deps.hostId
|
|
285
569
|
}, deps.handlers, {
|
|
286
|
-
onEvent: (event) => log.debug(`host event: ${event
|
|
570
|
+
onEvent: (event) => log.debug(`host event: ${eventText(event)}`),
|
|
287
571
|
onExpire: deps.onExpire,
|
|
288
572
|
onClosed: () => {
|
|
289
573
|
if (stopRunner === runner) {
|
|
@@ -480,6 +764,6 @@ var createRemoteHostSession = (config) => {
|
|
|
480
764
|
};
|
|
481
765
|
};
|
|
482
766
|
//#endregion
|
|
483
|
-
export { createHostSessionPersistence, createRemoteHost, createRemoteHostAuth, createRemoteHostFirebase, createRemoteHostSession, isSeedableBlob, startHostRunner, stripUndefined, undefinedPaths, unexpectedPaths };
|
|
767
|
+
export { DEFAULT_HEARTBEAT_MS, LISTEN_RETRY_WINDOW_MS, PRESENCE_STALE_BEATS, PRESENCE_STALE_MS, createHostSessionPersistence, createPresenceProbe, createRemoteHost, createRemoteHostAuth, createRemoteHostFirebase, createRemoteHostSession, isSeedableBlob, presenceIsFresh, presenceStaleAfterMs, reconnectDelayMs, startHostRunner, startResilientHostRunner, stripUndefined, undefinedPaths, unexpectedPaths, withTimeout };
|
|
484
768
|
|
|
485
769
|
//# sourceMappingURL=index.js.map
|