@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 +1 -1
- package/src/do/contracts.ts +2 -0
- package/src/do/flush-schedule.ts +8 -2
- package/src/do/user-oracle-do.ts +81 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ixo/oracle-runtime-workers",
|
|
3
|
-
"version": "0.37.
|
|
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": {
|
package/src/do/contracts.ts
CHANGED
|
@@ -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;
|
package/src/do/flush-schedule.ts
CHANGED
|
@@ -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' };
|
package/src/do/user-oracle-do.ts
CHANGED
|
@@ -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
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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
|
-
|
|
2791
|
-
|
|
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
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
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(
|
|
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,
|