@mulmoclaude/core 1.9.0 → 1.11.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/assets/helps/error-recovery.md +85 -0
- package/assets/helps/google-calendar-collection.md +41 -18
- package/dist/collection/core/schemaZ.d.ts +10 -1
- package/dist/collection/registry/server/index.cjs +2 -2
- package/dist/collection/registry/server/index.js +2 -2
- package/dist/collection/server/index.cjs +2 -2
- package/dist/collection/server/index.js +2 -2
- package/dist/collection-watchers/index.cjs +2 -2
- package/dist/collection-watchers/index.js +2 -2
- package/dist/{discovery-_fjsluLx.js → discovery-B4CZvrXR.js} +11 -3
- package/dist/{discovery-_fjsluLx.js.map → discovery-B4CZvrXR.js.map} +1 -1
- package/dist/{discovery-DYcR83qx.cjs → discovery-NRy3tyUA.cjs} +11 -3
- package/dist/{discovery-DYcR83qx.cjs.map → discovery-NRy3tyUA.cjs.map} +1 -1
- package/dist/feeds/server/index.cjs +2 -2
- package/dist/feeds/server/index.js +2 -2
- package/dist/google/calendar.d.ts +9 -0
- package/dist/google/calendarLock.d.ts +16 -0
- package/dist/google/calendarPushState.d.ts +1 -1
- package/dist/google/collectionProjection.d.ts +18 -0
- package/dist/google/collectionPush.d.ts +62 -0
- package/dist/google/collectionSync.d.ts +51 -23
- package/dist/google/index.cjs +833 -567
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.d.ts +4 -2
- package/dist/google/index.js +825 -568
- package/dist/google/index.js.map +1 -1
- package/dist/google/pushPlan.d.ts +1 -1
- 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/index.cjs +223 -8
- package/dist/remote-host/server/index.cjs.map +1 -1
- package/dist/remote-host/server/index.d.ts +4 -0
- package/dist/remote-host/server/index.js +214 -5
- package/dist/remote-host/server/index.js.map +1 -1
- 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/dist/{server-l4TZ5Lbf.cjs → server-7U-3DE2e.cjs} +2 -2
- package/dist/{server-l4TZ5Lbf.cjs.map → server-7U-3DE2e.cjs.map} +1 -1
- package/dist/{server-BlIrtGKl.js → server-DdyIvFhz.js} +2 -2
- package/dist/{server-BlIrtGKl.js.map → server-DdyIvFhz.js.map} +1 -1
- package/package.json +5 -5
- package/dist/remote-host/index.cjs.map +0 -1
- package/dist/remote-host/index.js.map +0 -1
|
@@ -4,7 +4,7 @@ import { ShadowEvent } from './calendarPushState.js';
|
|
|
4
4
|
* read-only, so a record column mapped to either is ignored here rather than
|
|
5
5
|
* rejected — the mapping was authored for the pull, and a push has no business
|
|
6
6
|
* invalidating it. */
|
|
7
|
-
export declare const PUSHABLE_SOURCE_FIELDS: readonly ["summary", "start", "end", "colorId"];
|
|
7
|
+
export declare const PUSHABLE_SOURCE_FIELDS: readonly ["summary", "start", "end", "colorId", "description", "location"];
|
|
8
8
|
export type PushableSourceField = (typeof PUSHABLE_SOURCE_FIELDS)[number];
|
|
9
9
|
/** The subset of a schema's `map` a push can act on: collection field → event field. */
|
|
10
10
|
export declare function pushableMap(map: Record<string, string>): Record<string, PushableSourceField>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const RUNNER_HEALTH_STATES: readonly ["online", "reconnecting", "offline"];
|
|
2
|
+
export type RunnerHealthState = (typeof RUNNER_HEALTH_STATES)[number];
|
|
3
|
+
export interface RunnerHealth {
|
|
4
|
+
state: RunnerHealthState;
|
|
5
|
+
/** Last channel error seen, for the popover and the log. Null before the first one. */
|
|
6
|
+
lastError: string | null;
|
|
7
|
+
/** ms epoch of the last state change, so the UI can say how long it has been down. */
|
|
8
|
+
changedAt: number;
|
|
9
|
+
}
|
|
10
|
+
export declare const isRunnerHealthState: (value: unknown) => value is RunnerHealthState;
|
|
11
|
+
/** Narrows a parsed HTTP payload. The client renders whatever this accepts, so a
|
|
12
|
+
* half-shaped health has to read as "no health reported" rather than as a state. */
|
|
13
|
+
export declare const isRunnerHealth: (value: unknown) => value is RunnerHealth;
|
|
@@ -1,31 +1,12 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
var REMOTE_HOST_PROTOCOL_VERSION = 2;
|
|
14
|
-
var buildHostPresence = (channel, handlers, online) => ({
|
|
15
|
-
online,
|
|
16
|
-
hostId: channel.hostId,
|
|
17
|
-
protocolVersion: 2,
|
|
18
|
-
capabilities: Object.keys(handlers)
|
|
19
|
-
});
|
|
20
|
-
var commandsCollection = (firestore, channel) => (0, firebase_firestore.collection)(firestore, "users", channel.uid, "hosts", channel.hostId, "commands");
|
|
21
|
-
var hostDoc = (firestore, channel) => (0, firebase_firestore.doc)(firestore, "users", channel.uid, "hosts", channel.hostId);
|
|
22
|
-
//#endregion
|
|
23
|
-
exports.REMOTE_HOST_PROTOCOL_VERSION = REMOTE_HOST_PROTOCOL_VERSION;
|
|
24
|
-
exports.buildHostPresence = buildHostPresence;
|
|
25
|
-
exports.byCreatedAt = byCreatedAt;
|
|
26
|
-
exports.commandsCollection = commandsCollection;
|
|
27
|
-
exports.hostDoc = hostDoc;
|
|
28
|
-
exports.isExpired = isExpired;
|
|
29
|
-
exports.toJsonObject = toJsonObject;
|
|
30
|
-
|
|
31
|
-
//# sourceMappingURL=index.cjs.map
|
|
2
|
+
const require_remote_host = require("../remote-host-DkDVxNim.cjs");
|
|
3
|
+
exports.REMOTE_HOST_PROTOCOL_VERSION = require_remote_host.REMOTE_HOST_PROTOCOL_VERSION;
|
|
4
|
+
exports.RUNNER_HEALTH_STATES = require_remote_host.RUNNER_HEALTH_STATES;
|
|
5
|
+
exports.buildHostPresence = require_remote_host.buildHostPresence;
|
|
6
|
+
exports.byCreatedAt = require_remote_host.byCreatedAt;
|
|
7
|
+
exports.commandsCollection = require_remote_host.commandsCollection;
|
|
8
|
+
exports.hostDoc = require_remote_host.hostDoc;
|
|
9
|
+
exports.isExpired = require_remote_host.isExpired;
|
|
10
|
+
exports.isRunnerHealth = require_remote_host.isRunnerHealth;
|
|
11
|
+
exports.isRunnerHealthState = require_remote_host.isRunnerHealthState;
|
|
12
|
+
exports.toJsonObject = require_remote_host.toJsonObject;
|
|
@@ -58,3 +58,5 @@ export interface HostPresence {
|
|
|
58
58
|
export declare const buildHostPresence: (channel: Channel, handlers: CommandHandlers, online: boolean) => HostPresence;
|
|
59
59
|
export declare const commandsCollection: (firestore: Firestore, channel: Channel) => CollectionReference<DocumentData>;
|
|
60
60
|
export declare const hostDoc: (firestore: Firestore, channel: Channel) => DocumentReference<DocumentData>;
|
|
61
|
+
export { RUNNER_HEALTH_STATES, isRunnerHealth, isRunnerHealthState } from './health.js';
|
|
62
|
+
export type { RunnerHealth, RunnerHealthState } from './health.js';
|
|
@@ -1,24 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
/** Widen a JSON-shaped handler payload to the channel's `JsonObject`.
|
|
4
|
-
*
|
|
5
|
-
* Exists so the `Jsonify` reasoning above lives in ONE place. Before this,
|
|
6
|
-
* eight remote-host handlers each carried their own `as unknown as JsonObject`
|
|
7
|
-
* with the justification re-argued in eight slightly different comments —
|
|
8
|
-
* which is how a rule stops being reviewable. */
|
|
9
|
-
var toJsonObject = (payload) => payload;
|
|
10
|
-
var isExpired = (command, now) => typeof command.expiresAt === "number" && now >= command.expiresAt;
|
|
11
|
-
var byCreatedAt = (left, right) => (left.createdAt ?? 0) - (right.createdAt ?? 0);
|
|
12
|
-
var REMOTE_HOST_PROTOCOL_VERSION = 2;
|
|
13
|
-
var buildHostPresence = (channel, handlers, online) => ({
|
|
14
|
-
online,
|
|
15
|
-
hostId: channel.hostId,
|
|
16
|
-
protocolVersion: 2,
|
|
17
|
-
capabilities: Object.keys(handlers)
|
|
18
|
-
});
|
|
19
|
-
var commandsCollection = (firestore, channel) => collection(firestore, "users", channel.uid, "hosts", channel.hostId, "commands");
|
|
20
|
-
var hostDoc = (firestore, channel) => doc(firestore, "users", channel.uid, "hosts", channel.hostId);
|
|
21
|
-
//#endregion
|
|
22
|
-
export { REMOTE_HOST_PROTOCOL_VERSION, buildHostPresence, byCreatedAt, commandsCollection, hostDoc, isExpired, toJsonObject };
|
|
23
|
-
|
|
24
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
import { a as hostDoc, c as RUNNER_HEALTH_STATES, i as commandsCollection, l as isRunnerHealth, n as buildHostPresence, o as isExpired, r as byCreatedAt, s as toJsonObject, t as REMOTE_HOST_PROTOCOL_VERSION, u as isRunnerHealthState } from "../remote-host-D_BRFHcI.js";
|
|
2
|
+
export { REMOTE_HOST_PROTOCOL_VERSION, RUNNER_HEALTH_STATES, buildHostPresence, byCreatedAt, commandsCollection, hostDoc, isExpired, isRunnerHealth, isRunnerHealthState, toJsonObject };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_dist = require("../../dist-pWpC-b04.cjs");
|
|
3
|
-
const
|
|
3
|
+
const require_remote_host = require("../../remote-host-DkDVxNim.cjs");
|
|
4
4
|
let firebase_firestore = require("firebase/firestore");
|
|
5
5
|
let firebase_auth = require("firebase/auth");
|
|
6
6
|
let firebase_app = require("firebase/app");
|
|
@@ -63,7 +63,7 @@ var unexpectedPaths = (paths, expected = []) => paths.filter((path) => !expected
|
|
|
63
63
|
//#endregion
|
|
64
64
|
//#region src/remote-host/server/presenceBeat.ts
|
|
65
65
|
var PRESENCE_STALE_BEATS = 3;
|
|
66
|
-
var errorText = (error) => error instanceof Error ? error.message : String(error);
|
|
66
|
+
var errorText$1 = (error) => error instanceof Error ? error.message : String(error);
|
|
67
67
|
var notify = (report, onThrow) => {
|
|
68
68
|
try {
|
|
69
69
|
report();
|
|
@@ -75,7 +75,7 @@ var createPresenceBeat = (deps) => {
|
|
|
75
75
|
const now = deps.now ?? Date.now;
|
|
76
76
|
const state = { lastAckMs: now() };
|
|
77
77
|
const announce = (online) => {
|
|
78
|
-
const fail = (error) => notify(() => deps.onError(errorText(error)));
|
|
78
|
+
const fail = (error) => notify(() => deps.onError(errorText$1(error)));
|
|
79
79
|
const ack = () => notify(() => {
|
|
80
80
|
state.lastAckMs = now();
|
|
81
81
|
});
|
|
@@ -196,7 +196,7 @@ var expireCommand = async (ref, command, options, uid) => {
|
|
|
196
196
|
};
|
|
197
197
|
var processCommand = async (ctx, ref, command, now) => {
|
|
198
198
|
const { handlers, options } = ctx;
|
|
199
|
-
if (
|
|
199
|
+
if (require_remote_host.isExpired(command, now)) {
|
|
200
200
|
await expireCommand(ref, command, options, ctx.uid);
|
|
201
201
|
return;
|
|
202
202
|
}
|
|
@@ -223,7 +223,7 @@ var dispatchAddedCommands = (ctx, snapshot) => {
|
|
|
223
223
|
snapshot.docChanges().filter((change) => change.type === "added").map((change) => ({
|
|
224
224
|
ref: change.doc.ref,
|
|
225
225
|
command: change.doc.data()
|
|
226
|
-
})).sort((left, right) =>
|
|
226
|
+
})).sort((left, right) => require_remote_host.byCreatedAt(left.command, right.command)).forEach(({ ref, command }) => {
|
|
227
227
|
processCommand(ctx, ref, command, now).catch(noop$2);
|
|
228
228
|
});
|
|
229
229
|
};
|
|
@@ -276,7 +276,7 @@ var listenForCommands = (queuedCommands, ctx, goOffline) => {
|
|
|
276
276
|
var heartbeatMs = (options) => options.heartbeatMs ?? 6e4;
|
|
277
277
|
var presenceStaleAfterMs = (options = {}) => heartbeatMs(options) * 3;
|
|
278
278
|
var buildPresenceBeat = (firestore, channel, handlers, options, onStale) => {
|
|
279
|
-
const presence =
|
|
279
|
+
const presence = require_remote_host.hostDoc(firestore, channel);
|
|
280
280
|
const report = (message) => options.onEvent?.({
|
|
281
281
|
phase: "error",
|
|
282
282
|
method: "presence",
|
|
@@ -284,7 +284,7 @@ var buildPresenceBeat = (firestore, channel, handlers, options, onStale) => {
|
|
|
284
284
|
});
|
|
285
285
|
return createPresenceBeat({
|
|
286
286
|
write: (online) => (0, firebase_firestore.setDoc)(presence, {
|
|
287
|
-
...
|
|
287
|
+
...require_remote_host.buildHostPresence(channel, handlers, online),
|
|
288
288
|
updatedAt: (0, firebase_firestore.serverTimestamp)()
|
|
289
289
|
}),
|
|
290
290
|
onError: (message) => report(`presence write failed: ${message}`),
|
|
@@ -317,7 +317,7 @@ var startHostRunner = (firestore, channel, handlers, options = {}) => {
|
|
|
317
317
|
}
|
|
318
318
|
presence.announce(true);
|
|
319
319
|
run.beat = setInterval(presence.beat, heartbeatMs(options));
|
|
320
|
-
run.stopListening = listenForCommands((0, firebase_firestore.query)(
|
|
320
|
+
run.stopListening = listenForCommands((0, firebase_firestore.query)(require_remote_host.commandsCollection(firestore, channel), (0, firebase_firestore.where)("status", "==", "queued")), {
|
|
321
321
|
firestore,
|
|
322
322
|
handlers,
|
|
323
323
|
options,
|
|
@@ -326,6 +326,215 @@ var startHostRunner = (firestore, channel, handlers, options = {}) => {
|
|
|
326
326
|
return () => shutDown(run, presence);
|
|
327
327
|
};
|
|
328
328
|
//#endregion
|
|
329
|
+
//#region src/remote-host/server/resilientRunner.ts
|
|
330
|
+
var ONE_SECOND_MS$1 = 1e3;
|
|
331
|
+
var ONE_MINUTE_MS = 60 * ONE_SECOND_MS$1;
|
|
332
|
+
var RECONNECT_BASE_MS = ONE_SECOND_MS$1;
|
|
333
|
+
var RECONNECT_MAX_MS = ONE_MINUTE_MS;
|
|
334
|
+
var SETTLE_MS = ONE_MINUTE_MS;
|
|
335
|
+
var GIVE_UP_MS = 5 * ONE_MINUTE_MS;
|
|
336
|
+
var PROBE_INTERVAL_MS = 90 * ONE_SECOND_MS$1;
|
|
337
|
+
var reconnectDelayMs = (attempt) => Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** attempt);
|
|
338
|
+
var errorText = (error) => error instanceof Error ? error.message : String(error);
|
|
339
|
+
var scheduleWithTimeout = (task, delayMs) => {
|
|
340
|
+
const timer = setTimeout(task, delayMs);
|
|
341
|
+
return () => clearTimeout(timer);
|
|
342
|
+
};
|
|
343
|
+
var setState = (ctx, next) => {
|
|
344
|
+
ctx.state = next;
|
|
345
|
+
try {
|
|
346
|
+
ctx.deps.onHealth?.({
|
|
347
|
+
state: next,
|
|
348
|
+
lastError: ctx.lastError,
|
|
349
|
+
changedAt: ctx.now()
|
|
350
|
+
});
|
|
351
|
+
} catch (error) {
|
|
352
|
+
ctx.deps.log.warn(`host runner health observer threw: ${errorText(error)}`);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
var clearTimer = (ctx) => {
|
|
356
|
+
ctx.cancelTimer?.();
|
|
357
|
+
ctx.cancelTimer = null;
|
|
358
|
+
};
|
|
359
|
+
var clearProbe = (ctx) => {
|
|
360
|
+
ctx.cancelProbe?.();
|
|
361
|
+
ctx.cancelProbe = null;
|
|
362
|
+
};
|
|
363
|
+
function scheduleProbe(ctx) {
|
|
364
|
+
if (ctx.stopped || !ctx.deps.checkAlive) return;
|
|
365
|
+
clearProbe(ctx);
|
|
366
|
+
ctx.cancelProbe = ctx.schedule(() => void runProbe(ctx), PROBE_INTERVAL_MS);
|
|
367
|
+
}
|
|
368
|
+
var STALE_REASON = "presence went stale — the phone can no longer see this host";
|
|
369
|
+
async function askAlive(ctx) {
|
|
370
|
+
try {
|
|
371
|
+
return {
|
|
372
|
+
alive: await ctx.deps.checkAlive?.() ?? null,
|
|
373
|
+
reason: STALE_REASON
|
|
374
|
+
};
|
|
375
|
+
} catch (error) {
|
|
376
|
+
return {
|
|
377
|
+
alive: false,
|
|
378
|
+
reason: `presence probe failed: ${errorText(error)}`
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async function runProbe(ctx) {
|
|
383
|
+
ctx.cancelProbe = null;
|
|
384
|
+
if (ctx.stopped || ctx.state !== "online") return;
|
|
385
|
+
const answer = await askAlive(ctx);
|
|
386
|
+
if (ctx.stopped || ctx.state !== "online") return;
|
|
387
|
+
if (answer.alive === false) {
|
|
388
|
+
ctx.lastError = answer.reason;
|
|
389
|
+
ctx.deps.log.warn(`host runner ${answer.reason}`);
|
|
390
|
+
onUnderlyingClosed(ctx);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
scheduleProbe(ctx);
|
|
394
|
+
}
|
|
395
|
+
var releaseUnderlying = (ctx) => {
|
|
396
|
+
const stop = ctx.stopUnderlying;
|
|
397
|
+
ctx.stopUnderlying = null;
|
|
398
|
+
try {
|
|
399
|
+
stop?.();
|
|
400
|
+
} catch (error) {
|
|
401
|
+
ctx.deps.log.warn(`host runner teardown failed: ${errorText(error)}`);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
var markRecovered = (ctx) => {
|
|
405
|
+
ctx.downSinceMs = null;
|
|
406
|
+
ctx.attempt = 0;
|
|
407
|
+
ctx.lastError = null;
|
|
408
|
+
if (ctx.state === "online") return;
|
|
409
|
+
ctx.deps.log.info("host runner re-subscribed");
|
|
410
|
+
setState(ctx, "online");
|
|
411
|
+
scheduleProbe(ctx);
|
|
412
|
+
};
|
|
413
|
+
async function settle(ctx) {
|
|
414
|
+
ctx.cancelTimer = null;
|
|
415
|
+
if (ctx.stopped) return;
|
|
416
|
+
const answer = await askAlive(ctx);
|
|
417
|
+
if (ctx.stopped) return;
|
|
418
|
+
if (answer.alive === false) {
|
|
419
|
+
ctx.lastError = answer.reason;
|
|
420
|
+
ctx.deps.log.warn(`host runner is up but still unreachable — ${answer.reason}`);
|
|
421
|
+
onUnderlyingClosed(ctx);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
markRecovered(ctx);
|
|
425
|
+
}
|
|
426
|
+
var giveUp = (ctx) => {
|
|
427
|
+
ctx.stopped = true;
|
|
428
|
+
clearProbe(ctx);
|
|
429
|
+
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"})`);
|
|
430
|
+
setState(ctx, "offline");
|
|
431
|
+
ctx.deps.options.onClosed?.();
|
|
432
|
+
};
|
|
433
|
+
function scheduleRelaunch(ctx) {
|
|
434
|
+
const delayMs = reconnectDelayMs(ctx.attempt);
|
|
435
|
+
ctx.attempt += 1;
|
|
436
|
+
ctx.deps.log.warn(`host runner closed (${ctx.lastError ?? "no error reported"}); re-subscribing in ${Math.round(delayMs / ONE_SECOND_MS$1)}s`);
|
|
437
|
+
if (ctx.state !== "reconnecting") setState(ctx, "reconnecting");
|
|
438
|
+
ctx.cancelTimer = ctx.schedule(() => launch(ctx), delayMs);
|
|
439
|
+
}
|
|
440
|
+
function onUnderlyingClosed(ctx) {
|
|
441
|
+
if (ctx.stopped) return;
|
|
442
|
+
releaseUnderlying(ctx);
|
|
443
|
+
clearTimer(ctx);
|
|
444
|
+
clearProbe(ctx);
|
|
445
|
+
ctx.downSinceMs ??= ctx.now();
|
|
446
|
+
if (ctx.now() - ctx.downSinceMs >= GIVE_UP_MS) giveUp(ctx);
|
|
447
|
+
else scheduleRelaunch(ctx);
|
|
448
|
+
}
|
|
449
|
+
var CHANNEL_METHODS = /* @__PURE__ */ new Set(["listen", "presence"]);
|
|
450
|
+
var runnerOptions = (ctx) => ({
|
|
451
|
+
...ctx.deps.options,
|
|
452
|
+
onEvent: (event) => {
|
|
453
|
+
if (event.phase === "error") {
|
|
454
|
+
const text = `${event.method}: ${event.message ?? "no detail"}`;
|
|
455
|
+
if (CHANNEL_METHODS.has(event.method)) ctx.lastError = text;
|
|
456
|
+
ctx.deps.log.warn(`host runner event error — ${text}`);
|
|
457
|
+
}
|
|
458
|
+
ctx.deps.options.onEvent?.(event);
|
|
459
|
+
},
|
|
460
|
+
onClosed: () => onUnderlyingClosed(ctx)
|
|
461
|
+
});
|
|
462
|
+
function launch(ctx) {
|
|
463
|
+
ctx.cancelTimer = null;
|
|
464
|
+
if (ctx.stopped) return;
|
|
465
|
+
try {
|
|
466
|
+
ctx.stopUnderlying = ctx.deps.start(runnerOptions(ctx));
|
|
467
|
+
} catch (error) {
|
|
468
|
+
ctx.lastError = errorText(error);
|
|
469
|
+
onUnderlyingClosed(ctx);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
ctx.cancelTimer = ctx.schedule(() => void settle(ctx), SETTLE_MS);
|
|
473
|
+
}
|
|
474
|
+
function startResilientHostRunner(deps) {
|
|
475
|
+
const ctx = {
|
|
476
|
+
deps,
|
|
477
|
+
schedule: deps.schedule ?? scheduleWithTimeout,
|
|
478
|
+
now: deps.now ?? Date.now,
|
|
479
|
+
stopUnderlying: null,
|
|
480
|
+
cancelTimer: null,
|
|
481
|
+
attempt: 0,
|
|
482
|
+
downSinceMs: null,
|
|
483
|
+
lastError: null,
|
|
484
|
+
state: "online",
|
|
485
|
+
stopped: false,
|
|
486
|
+
cancelProbe: null
|
|
487
|
+
};
|
|
488
|
+
setState(ctx, "online");
|
|
489
|
+
launch(ctx);
|
|
490
|
+
scheduleProbe(ctx);
|
|
491
|
+
return () => {
|
|
492
|
+
ctx.stopped = true;
|
|
493
|
+
clearTimer(ctx);
|
|
494
|
+
clearProbe(ctx);
|
|
495
|
+
releaseUnderlying(ctx);
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
//#endregion
|
|
499
|
+
//#region src/remote-host/server/presenceProbe.ts
|
|
500
|
+
var ONE_SECOND_MS = 1e3;
|
|
501
|
+
var PRESENCE_STALE_MS = presenceStaleAfterMs();
|
|
502
|
+
var PROBE_TIMEOUT_MS = 30 * ONE_SECOND_MS;
|
|
503
|
+
var hasToMillis = (value) => "toMillis" in value && typeof Reflect.get(value, "toMillis") === "function";
|
|
504
|
+
var asMillis = (value) => {
|
|
505
|
+
if (typeof value === "number") return value;
|
|
506
|
+
if (typeof value === "object" && value !== null && hasToMillis(value)) return value.toMillis();
|
|
507
|
+
return null;
|
|
508
|
+
};
|
|
509
|
+
/** Judge a presence document that was read successfully. Exported for its own test:
|
|
510
|
+
* the freshness rule is the part worth pinning, and it needs no Firestore. */
|
|
511
|
+
var presenceIsFresh = (data, now, staleAfterMs = PRESENCE_STALE_MS) => {
|
|
512
|
+
if (!data) return null;
|
|
513
|
+
const updatedAt = asMillis(data.updatedAt);
|
|
514
|
+
if (updatedAt === null) return null;
|
|
515
|
+
if (data.online === false) return null;
|
|
516
|
+
return now - updatedAt < staleAfterMs;
|
|
517
|
+
};
|
|
518
|
+
/** Reject once `timeoutMs` has passed, so a stalled read answers instead of hanging. */
|
|
519
|
+
var withTimeout = async (work, timeoutMs) => {
|
|
520
|
+
const timer = { handle: null };
|
|
521
|
+
const deadline = new Promise((_, reject) => {
|
|
522
|
+
timer.handle = setTimeout(() => reject(/* @__PURE__ */ new Error(`presence read did not answer within ${Math.round(timeoutMs / ONE_SECOND_MS)}s`)), timeoutMs);
|
|
523
|
+
});
|
|
524
|
+
try {
|
|
525
|
+
return await Promise.race([work, deadline]);
|
|
526
|
+
} finally {
|
|
527
|
+
if (timer.handle) clearTimeout(timer.handle);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
/** Reads the host's own presence document from the server and reports whether it is fresh. */
|
|
531
|
+
var createPresenceProbe = (deps) => {
|
|
532
|
+
const now = deps.now ?? Date.now;
|
|
533
|
+
return async () => {
|
|
534
|
+
return presenceIsFresh((await withTimeout((0, firebase_firestore.getDocFromServer)(require_remote_host.hostDoc(deps.firestore(), deps.channel)), deps.timeoutMs ?? PROBE_TIMEOUT_MS)).data(), now(), deps.staleAfterMs ?? PRESENCE_STALE_MS);
|
|
535
|
+
};
|
|
536
|
+
};
|
|
537
|
+
//#endregion
|
|
329
538
|
//#region src/remote-host/server/lifecycle.ts
|
|
330
539
|
var noop$1 = () => void 0;
|
|
331
540
|
var silentLogger = {
|
|
@@ -559,16 +768,22 @@ var createRemoteHostSession = (config) => {
|
|
|
559
768
|
exports.DEFAULT_HEARTBEAT_MS = DEFAULT_HEARTBEAT_MS;
|
|
560
769
|
exports.LISTEN_RETRY_WINDOW_MS = LISTEN_RETRY_WINDOW_MS;
|
|
561
770
|
exports.PRESENCE_STALE_BEATS = PRESENCE_STALE_BEATS;
|
|
771
|
+
exports.PRESENCE_STALE_MS = PRESENCE_STALE_MS;
|
|
562
772
|
exports.createHostSessionPersistence = createHostSessionPersistence;
|
|
773
|
+
exports.createPresenceProbe = createPresenceProbe;
|
|
563
774
|
exports.createRemoteHost = createRemoteHost;
|
|
564
775
|
exports.createRemoteHostAuth = createRemoteHostAuth;
|
|
565
776
|
exports.createRemoteHostFirebase = createRemoteHostFirebase;
|
|
566
777
|
exports.createRemoteHostSession = createRemoteHostSession;
|
|
567
778
|
exports.isSeedableBlob = isSeedableBlob;
|
|
779
|
+
exports.presenceIsFresh = presenceIsFresh;
|
|
568
780
|
exports.presenceStaleAfterMs = presenceStaleAfterMs;
|
|
781
|
+
exports.reconnectDelayMs = reconnectDelayMs;
|
|
569
782
|
exports.startHostRunner = startHostRunner;
|
|
783
|
+
exports.startResilientHostRunner = startResilientHostRunner;
|
|
570
784
|
exports.stripUndefined = stripUndefined;
|
|
571
785
|
exports.undefinedPaths = undefinedPaths;
|
|
572
786
|
exports.unexpectedPaths = unexpectedPaths;
|
|
787
|
+
exports.withTimeout = withTimeout;
|
|
573
788
|
|
|
574
789
|
//# sourceMappingURL=index.cjs.map
|