@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.
Files changed (49) hide show
  1. package/assets/helps/error-recovery.md +85 -0
  2. package/assets/helps/google-calendar-collection.md +41 -18
  3. package/dist/collection/core/schemaZ.d.ts +10 -1
  4. package/dist/collection/registry/server/index.cjs +2 -2
  5. package/dist/collection/registry/server/index.js +2 -2
  6. package/dist/collection/server/index.cjs +2 -2
  7. package/dist/collection/server/index.js +2 -2
  8. package/dist/collection-watchers/index.cjs +2 -2
  9. package/dist/collection-watchers/index.js +2 -2
  10. package/dist/{discovery-_fjsluLx.js → discovery-B4CZvrXR.js} +11 -3
  11. package/dist/{discovery-_fjsluLx.js.map → discovery-B4CZvrXR.js.map} +1 -1
  12. package/dist/{discovery-DYcR83qx.cjs → discovery-NRy3tyUA.cjs} +11 -3
  13. package/dist/{discovery-DYcR83qx.cjs.map → discovery-NRy3tyUA.cjs.map} +1 -1
  14. package/dist/feeds/server/index.cjs +2 -2
  15. package/dist/feeds/server/index.js +2 -2
  16. package/dist/google/calendar.d.ts +9 -0
  17. package/dist/google/calendarLock.d.ts +16 -0
  18. package/dist/google/calendarPushState.d.ts +1 -1
  19. package/dist/google/collectionProjection.d.ts +18 -0
  20. package/dist/google/collectionPush.d.ts +62 -0
  21. package/dist/google/collectionSync.d.ts +51 -23
  22. package/dist/google/index.cjs +833 -567
  23. package/dist/google/index.cjs.map +1 -1
  24. package/dist/google/index.d.ts +4 -2
  25. package/dist/google/index.js +825 -568
  26. package/dist/google/index.js.map +1 -1
  27. package/dist/google/pushPlan.d.ts +1 -1
  28. package/dist/remote-host/health.d.ts +13 -0
  29. package/dist/remote-host/index.cjs +11 -30
  30. package/dist/remote-host/index.d.ts +2 -0
  31. package/dist/remote-host/index.js +2 -24
  32. package/dist/remote-host/server/index.cjs +223 -8
  33. package/dist/remote-host/server/index.cjs.map +1 -1
  34. package/dist/remote-host/server/index.d.ts +4 -0
  35. package/dist/remote-host/server/index.js +214 -5
  36. package/dist/remote-host/server/index.js.map +1 -1
  37. package/dist/remote-host/server/presenceProbe.d.ts +23 -0
  38. package/dist/remote-host/server/resilientRunner.d.ts +24 -0
  39. package/dist/remote-host-D_BRFHcI.js +36 -0
  40. package/dist/remote-host-D_BRFHcI.js.map +1 -0
  41. package/dist/remote-host-DkDVxNim.cjs +95 -0
  42. package/dist/remote-host-DkDVxNim.cjs.map +1 -0
  43. package/dist/{server-l4TZ5Lbf.cjs → server-7U-3DE2e.cjs} +2 -2
  44. package/dist/{server-l4TZ5Lbf.cjs.map → server-7U-3DE2e.cjs.map} +1 -1
  45. package/dist/{server-BlIrtGKl.js → server-DdyIvFhz.js} +2 -2
  46. package/dist/{server-BlIrtGKl.js.map → server-DdyIvFhz.js.map} +1 -1
  47. package/package.json +5 -5
  48. package/dist/remote-host/index.cjs.map +0 -1
  49. 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
- let firebase_firestore = require("firebase/firestore");
3
- //#region src/remote-host/index.ts
4
- /** Widen a JSON-shaped handler payload to the channel's `JsonObject`.
5
- *
6
- * Exists so the `Jsonify` reasoning above lives in ONE place. Before this,
7
- * eight remote-host handlers each carried their own `as unknown as JsonObject`
8
- * with the justification re-argued in eight slightly different comments —
9
- * which is how a rule stops being reviewable. */
10
- var toJsonObject = (payload) => payload;
11
- var isExpired = (command, now) => typeof command.expiresAt === "number" && now >= command.expiresAt;
12
- var byCreatedAt = (left, right) => (left.createdAt ?? 0) - (right.createdAt ?? 0);
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 { collection, doc } from "firebase/firestore";
2
- //#region src/remote-host/index.ts
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 require_remote_host_index = require("../index.cjs");
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 (require_remote_host_index.isExpired(command, now)) {
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) => require_remote_host_index.byCreatedAt(left.command, right.command)).forEach(({ ref, command }) => {
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 = require_remote_host_index.hostDoc(firestore, channel);
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
- ...require_remote_host_index.buildHostPresence(channel, handlers, online),
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)(require_remote_host_index.commandsCollection(firestore, channel), (0, firebase_firestore.where)("status", "==", "queued")), {
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