@ixo/oracle-runtime-workers 0.37.1 → 0.37.2

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.37.1",
3
+ "version": "0.37.2",
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": {
@@ -724,6 +724,8 @@ export interface StorageStatus {
724
724
  uploadedGeneration?: number;
725
725
  lastChecksum?: string;
726
726
  flushFailures: number;
727
+ /** Set while the owner copy waits for a delegation instead of retrying (reason, since). */
728
+ flushWaitsForDelegation: { reason: string; since: number } | null;
727
729
  lastVacuumAt?: number;
728
730
  /** True once the user's legacy Matrix copy has been removed or found absent. */
729
731
  legacyCleared?: boolean;
@@ -9,7 +9,9 @@
9
9
  * dirty copy alone. Two exceptions upload right away: a copy about to be
10
10
  * evicted (the wipe must never drop unsaved turns) and a copy marked dirty
11
11
  * before the deadline existed (older objects; one upload, then the
12
- * debounce applies).
12
+ * debounce applies). A copy waiting for a delegation is not uploaded at
13
+ * all: without one the owner store refuses it, and the next delegation
14
+ * the object receives uploads it (`meta:flushWaitsForDelegation`).
13
15
  */
14
16
  export interface FlushScheduleInput {
15
17
  /** The object holds writes the owner store has not seen (and a database is open). */
@@ -19,10 +21,12 @@ export interface FlushScheduleInput {
19
21
  now: number;
20
22
  /** The idle-eviction path is about to wipe the working copy. */
21
23
  idle: boolean;
24
+ /** The last upload failed for want of a delegation; none has arrived since. */
25
+ waitingForDelegation?: boolean;
22
26
  }
23
27
 
24
28
  export type FlushScheduleDecision =
25
- | { action: 'skip'; reason: 'clean' }
29
+ | { action: 'skip'; reason: 'clean' | 'waiting-for-delegation' }
26
30
  | { action: 'flush'; reason: 'due' | 'idle' | 'no-deadline' }
27
31
  | { action: 'wait'; at: number };
28
32
 
@@ -31,6 +35,8 @@ export const FLUSH_DUE_SLACK_MS = 1000;
31
35
 
32
36
  export function decideFlush(input: FlushScheduleInput): FlushScheduleDecision {
33
37
  if (!input.dirty) return { action: 'skip', reason: 'clean' };
38
+ if (input.waitingForDelegation)
39
+ return { action: 'skip', reason: 'waiting-for-delegation' };
34
40
  if (input.idle) return { action: 'flush', reason: 'idle' };
35
41
  if (typeof input.flushAt !== 'number' || !Number.isFinite(input.flushAt))
36
42
  return { action: 'flush', reason: 'no-deadline' };
@@ -264,6 +264,7 @@ import {
264
264
  import {
265
265
  IxoVfsOwnerStore,
266
266
  VFS_DEFAULT_BASE_URLS,
267
+ VfsNoDelegationError,
267
268
  } from '../owner-store/ixo-vfs-store';
268
269
  import type { OwnerStore } from '../owner-store/types';
269
270
  import {
@@ -731,6 +732,14 @@ interface MatrixUserIdCache {
731
732
  }
732
733
  /** Consecutive flush failures (cleared on success). */
733
734
  const META_FLUSH_FAILURES = 'meta:flushFailures';
735
+ /**
736
+ * Set when an upload failed because the user has no delegation to this
737
+ * oracle that reaches the owner copy (`VfsNoDelegationError`): the copy then
738
+ * waits for one instead of retrying every `FLUSH_RETRY_DELAY_MS`, and the
739
+ * next delegation (`POST /delegation`, a request's header, the room state)
740
+ * uploads it. Holds the reason and when the wait began.
741
+ */
742
+ const META_FLUSH_WAITS_FOR_DELEGATION = 'meta:flushWaitsForDelegation';
734
743
  const META_LAST_VACUUM = 'meta:lastVacuumAt';
735
744
  /** When the last R2 page-tier eviction pass ran (see `tierTick`). */
736
745
  const META_LAST_TIER_PASS = 'meta:lastTierPassAt';
@@ -1473,9 +1482,24 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
1473
1482
  private async onDelegationReplaced(): Promise<void> {
1474
1483
  const failures =
1475
1484
  (await this.ctx.storage.get<number>(META_FLUSH_FAILURES)) ?? 0;
1476
- if (failures === 0) return;
1477
- await this.ctx.storage.delete(META_FLUSH_FAILURES);
1478
- if (!this.db) return;
1485
+ const waiting = await this.ctx.storage.get<unknown>(
1486
+ META_FLUSH_WAITS_FOR_DELEGATION,
1487
+ );
1488
+ if (failures === 0 && waiting === undefined) return;
1489
+ await this.ctx.storage.delete([
1490
+ META_FLUSH_FAILURES,
1491
+ META_FLUSH_WAITS_FOR_DELEGATION,
1492
+ ]);
1493
+ if (waiting !== undefined)
1494
+ console.log(
1495
+ `[user-do] ${this.userDid ?? 'user'} has a delegation again — uploading the owner copy that waited for it`,
1496
+ );
1497
+ // Not booted yet (a delegation adopted before boot): the alarm boots
1498
+ // the object and serves the upload at once.
1499
+ if (!this.db) {
1500
+ if (waiting !== undefined) await this.scheduleFlushRetry(Date.now());
1501
+ return;
1502
+ }
1479
1503
  if (
1480
1504
  !this.dirty &&
1481
1505
  (await this.ctx.storage.get<boolean>(META_DIRTY)) === true
@@ -1563,6 +1587,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
1563
1587
  console.log(
1564
1588
  `[user-do] delegation for ${userDid} loaded from the deposited room state (${held ? 'the held copy was inside the margin' : 'header-less turn'})`,
1565
1589
  );
1590
+ await this.onDelegationReplaced();
1566
1591
  }
1567
1592
  return;
1568
1593
  }
@@ -2458,6 +2483,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
2458
2483
  META_FLUSH_AT,
2459
2484
  META_UPLOADED_GEN,
2460
2485
  META_FLUSH_FAILURES,
2486
+ META_FLUSH_WAITS_FOR_DELEGATION,
2461
2487
  META_LAST_VACUUM,
2462
2488
  ]);
2463
2489
  this.flushAt = null;
@@ -2777,6 +2803,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
2777
2803
  flushAt: await this.ctx.storage.get<number>(META_FLUSH_AT),
2778
2804
  now,
2779
2805
  idle,
2806
+ waitingForDelegation:
2807
+ (await this.ctx.storage.get<unknown>(
2808
+ META_FLUSH_WAITS_FOR_DELEGATION,
2809
+ )) !== undefined,
2780
2810
  });
2781
2811
  if (flush.action === 'flush') {
2782
2812
  try {
@@ -2786,9 +2816,12 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
2786
2816
  `[user-do] flush failed for ${this.userDid}: ${err instanceof Error ? err.message : String(err)}`,
2787
2817
  );
2788
2818
  // `flushOnce` arms the retry on an upload failure; anything that
2789
- // failed before the upload gets the same retry window here.
2790
- await this.scheduleFlushRetry(now + FLUSH_RETRY_DELAY_MS);
2791
- deadlines.push(now + FLUSH_RETRY_DELAY_MS);
2819
+ // failed before the upload gets the same retry window here. A
2820
+ // missing delegation is not retried: the copy waits for one.
2821
+ if (!(err instanceof VfsNoDelegationError)) {
2822
+ await this.scheduleFlushRetry(now + FLUSH_RETRY_DELAY_MS);
2823
+ deadlines.push(now + FLUSH_RETRY_DELAY_MS);
2824
+ }
2792
2825
  }
2793
2826
  } else if (flush.action === 'wait') {
2794
2827
  deadlines.push(Math.max(flush.at, now + 1000));
@@ -2931,10 +2964,18 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
2931
2964
  return;
2932
2965
  }
2933
2966
  if (outcome === 'not-current') {
2934
- console.warn(
2935
- `[user-do] ${this.userDid} is idle but the owner copy is not verified current — keeping the working copy`,
2936
- );
2937
- deadlines.push(now + FLUSH_RETRY_DELAY_MS);
2967
+ // Waiting for a delegation, the copy stays until one arrives (the
2968
+ // idle horizon below still wakes the object now and then).
2969
+ if (
2970
+ (await this.ctx.storage.get<unknown>(
2971
+ META_FLUSH_WAITS_FOR_DELEGATION,
2972
+ )) === undefined
2973
+ ) {
2974
+ console.warn(
2975
+ `[user-do] ${this.userDid} is idle but the owner copy is not verified current — keeping the working copy`,
2976
+ );
2977
+ deadlines.push(now + FLUSH_RETRY_DELAY_MS);
2978
+ }
2938
2979
  } else {
2939
2980
  console.log(
2940
2981
  `[user-do] ${this.userDid} became active during the idle check — keeping the working copy`,
@@ -5667,8 +5708,29 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5667
5708
  const failures =
5668
5709
  ((await this.ctx.storage.get<number>(META_FLUSH_FAILURES)) ?? 0) +
5669
5710
  1;
5670
- await this.ctx.storage.put(META_FLUSH_FAILURES, failures);
5671
5711
  const detail = err instanceof Error ? err.message : String(err);
5712
+ if (err instanceof VfsNoDelegationError) {
5713
+ // Nothing to retry until the user deposits a delegation that
5714
+ // reaches the owner copy: say so once, drop the deadline, and
5715
+ // let the next delegation (or the next turn's daily deadline)
5716
+ // upload it.
5717
+ const already = await this.ctx.storage.get<unknown>(
5718
+ META_FLUSH_WAITS_FOR_DELEGATION,
5719
+ );
5720
+ if (already === undefined) {
5721
+ await this.ctx.storage.put(META_FLUSH_WAITS_FOR_DELEGATION, {
5722
+ reason: detail,
5723
+ since: Date.now(),
5724
+ });
5725
+ console.warn(
5726
+ `[user-do] owner copy for ${this.userDid} waits for a delegation — working copy kept, no retry until one arrives: ${detail}`,
5727
+ );
5728
+ }
5729
+ this.flushAt = null;
5730
+ await this.ctx.storage.delete(META_FLUSH_AT);
5731
+ throw err;
5732
+ }
5733
+ await this.ctx.storage.put(META_FLUSH_FAILURES, failures);
5672
5734
  const line = `[user-do] owner-store flush failed (${failures} in a row) for ${this.userDid} — working copy kept dirty, retrying in ${FLUSH_RETRY_DELAY_MS / 60_000} min: ${detail}`;
5673
5735
  if (failures >= FLUSH_FAILURES_ERROR_THRESHOLD) console.error(line);
5674
5736
  else console.warn(line);
@@ -5679,7 +5741,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5679
5741
  await this.ctx.storage.put(META_LAST_CHECKSUM, checksum);
5680
5742
  await this.ctx.storage.put(META_LAST_FLUSH, Date.now());
5681
5743
  await this.ctx.storage.put(META_UPLOADED_GEN, snapshot.generation);
5682
- await this.ctx.storage.delete(META_FLUSH_FAILURES);
5744
+ await this.ctx.storage.delete([
5745
+ META_FLUSH_FAILURES,
5746
+ META_FLUSH_WAITS_FOR_DELEGATION,
5747
+ ]);
5683
5748
  await this.clearDirty();
5684
5749
  console.log(
5685
5750
  `[user-do] flushed ${snapshot.size} bytes (${saved.bytes} sent) to ${store.kind} for ${this.userDid} (${saved.etag})`,
@@ -5917,6 +5982,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5917
5982
  lastChecksum: await this.ctx.storage.get<string>(META_LAST_CHECKSUM),
5918
5983
  flushFailures:
5919
5984
  (await this.ctx.storage.get<number>(META_FLUSH_FAILURES)) ?? 0,
5985
+ flushWaitsForDelegation:
5986
+ (await this.ctx.storage.get<{ reason: string; since: number }>(
5987
+ META_FLUSH_WAITS_FOR_DELEGATION,
5988
+ )) ?? null,
5920
5989
  lastVacuumAt: await this.ctx.storage.get<number>(META_LAST_VACUUM),
5921
5990
  legacyCleared:
5922
5991
  (await this.ctx.storage.get<boolean>(META_LEGACY_CLEARED)) === true,