@ixo/oracle-runtime-workers 0.20.0 → 0.20.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ixo/oracle-runtime-workers",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
4
4
  "description": "QiForge oracle runtime for Cloudflare Workers — Durable Object per user (SQLite in WASM over DO storage), Matrix E2EE gateway Durable Object, Hono HTTP/SSE shell.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -58,8 +58,8 @@
58
58
  "wa-sqlite": "1.0.0",
59
59
  "yjs": "13.6.32",
60
60
  "zod": "^4.4.3",
61
- "@ixo/common": "1.7.0",
62
61
  "@ixo/ixo-transaction": "0.1.0",
62
+ "@ixo/common": "1.7.0",
63
63
  "@ixo/ucan": "2.2.1"
64
64
  },
65
65
  "devDependencies": {
@@ -3253,6 +3253,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3253
3253
  keepGathering();
3254
3254
  return async (onSlow, signal) => {
3255
3255
  if (signal.aborted) return undefined;
3256
+ const waitedFrom = Date.now();
3256
3257
  const timer = setTimeout(onSlow, USER_CONTEXT_PROGRESS_GRACE_MS);
3257
3258
  let onAbort: (() => void) | undefined;
3258
3259
  const aborted = new Promise<undefined>((resolve) => {
@@ -3261,6 +3262,12 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3261
3262
  });
3262
3263
  try {
3263
3264
  const outcome = await Promise.race([pending, aborted]);
3265
+ // One line per first contact, at warn so it stands out when logs
3266
+ // are read at warn and above: what the turn was given and how
3267
+ // long it waited for it.
3268
+ console.warn(
3269
+ `[user-context] first contact room=${input.roomId} session=${input.sessionId} outcome=${outcome?.kind ?? 'aborted'} waited=${Date.now() - waitedFrom}ms`,
3270
+ );
3264
3271
  return outcome?.kind === 'cached' || outcome?.kind === 'fresh'
3265
3272
  ? withEverySlot(outcome.context)
3266
3273
  : undefined;
@@ -557,8 +557,18 @@ type GatherResult = Extract<
557
557
 
558
558
  interface InFlightGather {
559
559
  promise: Promise<GatherResult>;
560
+ /** `now()` when the gather started; a gather older than its request timeout plus grace is treated as lost. */
561
+ startedAt: number;
560
562
  }
561
563
 
564
+ /**
565
+ * Grace past `requestTimeoutMs` before an unsettled gather is given up on.
566
+ * A gather settles within its request timeout; one that has not after this
567
+ * long never will (its request context is gone), and a room must not keep
568
+ * joining it.
569
+ */
570
+ const STALE_GATHER_GRACE_MS = 5_000;
571
+
562
572
  function describe(err: unknown): string {
563
573
  return err instanceof Error ? err.message : String(err);
564
574
  }
@@ -769,7 +779,18 @@ export class UserContextFetcher {
769
779
  }
770
780
 
771
781
  const existing = this.inFlight.get(roomId);
772
- if (existing) return existing;
782
+ if (existing) {
783
+ const ageMs = this.now() - existing.startedAt;
784
+ if (ageMs <= this.requestTimeoutMs + STALE_GATHER_GRACE_MS) {
785
+ return existing;
786
+ }
787
+ // Lost: never settled within its own timeout. Detach it (a late
788
+ // settle must not repopulate the room) and gather again.
789
+ logger.warn(
790
+ `[user-context] gather for room=${roomId} unsettled after ${ageMs}ms; starting a new one`,
791
+ );
792
+ this.inFlight.delete(roomId);
793
+ }
773
794
 
774
795
  if (this.recentlyFailed(roomId)) {
775
796
  logger.log(
@@ -785,6 +806,7 @@ export class UserContextFetcher {
785
806
  }
786
807
 
787
808
  const entry: InFlightGather = {
809
+ startedAt: this.now(),
788
810
  promise: this.gather(engineUrl, roomId).then(
789
811
  (result) => this.settle(roomId, entry, result),
790
812
  (err: unknown) =>