@ixo/oracle-runtime-workers 0.2.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/README.md +110 -0
- package/package.json +77 -0
- package/src/attachments/classify.ts +114 -0
- package/src/attachments/content-blocks.ts +51 -0
- package/src/attachments/download.ts +235 -0
- package/src/attachments/extract.ts +241 -0
- package/src/attachments/index.ts +84 -0
- package/src/attachments/magic.ts +133 -0
- package/src/attachments/pipeline.ts +409 -0
- package/src/attachments/retention.ts +205 -0
- package/src/attachments/route.ts +41 -0
- package/src/attachments/sandbox-archive.ts +90 -0
- package/src/attachments/sanitize.ts +35 -0
- package/src/attachments/types.ts +86 -0
- package/src/attachments/view.ts +107 -0
- package/src/core/env.ts +200 -0
- package/src/core/index.ts +513 -0
- package/src/core/llm.ts +842 -0
- package/src/core/main-agent-types.ts +123 -0
- package/src/core/main-agent.ts +399 -0
- package/src/core/manifest.ts +365 -0
- package/src/core/meta-tools.ts +232 -0
- package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
- package/src/core/middlewares/capability-gate.ts +76 -0
- package/src/core/middlewares/index.ts +18 -0
- package/src/core/middlewares/page-context.ts +98 -0
- package/src/core/middlewares/safety-guardrail.ts +117 -0
- package/src/core/middlewares/summarization.ts +99 -0
- package/src/core/middlewares/tool-repetition-guard.ts +125 -0
- package/src/core/middlewares/tool-validation.ts +83 -0
- package/src/core/middlewares/work-status.ts +57 -0
- package/src/core/openrouter-pricing.ts +124 -0
- package/src/core/plugin-loader.ts +192 -0
- package/src/core/plugins/index.ts +6 -0
- package/src/core/plugins/skills/index.ts +8 -0
- package/src/core/plugins/skills/skills-tools.ts +356 -0
- package/src/core/plugins/skills/skills.plugin.ts +113 -0
- package/src/core/plugins/weather/index.ts +8 -0
- package/src/core/plugins/weather/weather-client.ts +153 -0
- package/src/core/plugins/weather/weather-middleware.ts +27 -0
- package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
- package/src/core/plugins/weather/weather-tools.ts +103 -0
- package/src/core/plugins/weather/weather.plugin.ts +227 -0
- package/src/core/prompt-composer.ts +469 -0
- package/src/core/registries.ts +642 -0
- package/src/core/runtime-context.ts +619 -0
- package/src/core/state.ts +108 -0
- package/src/core/sub-agent-fallback.ts +187 -0
- package/src/core/subagent-as-tool.ts +322 -0
- package/src/core/template.ts +245 -0
- package/src/core/utils.ts +246 -0
- package/src/core/wrap-plugin-tool.ts +100 -0
- package/src/do/ambient.ts +266 -0
- package/src/do/contracts.ts +562 -0
- package/src/do/debug-timers.ts +120 -0
- package/src/do/fresh-stub.ts +32 -0
- package/src/do/gateway-retry.ts +73 -0
- package/src/do/secrets-adapter.ts +32 -0
- package/src/do/size-alerts.ts +76 -0
- package/src/do/sse-stream.ts +472 -0
- package/src/do/transcript.ts +192 -0
- package/src/do/turn-body.ts +84 -0
- package/src/do/turn-metadata.ts +146 -0
- package/src/do/ucan-service.ts +339 -0
- package/src/do/user-oracle-do.ts +3035 -0
- package/src/gateway-worker.ts +82 -0
- package/src/index.ts +237 -0
- package/src/llm/byo-adapter.ts +59 -0
- package/src/llm/byo-catalog.ts +520 -0
- package/src/llm/byo-client.ts +280 -0
- package/src/llm/byo-routes.ts +255 -0
- package/src/llm/byo-service.ts +836 -0
- package/src/llm/chatgpt-oauth.ts +430 -0
- package/src/llm/provider-error.ts +283 -0
- package/src/matrix/gateway-do.ts +918 -0
- package/src/matrix/ingest.ts +209 -0
- package/src/matrix/replay-format.ts +34 -0
- package/src/matrix/reply-chain.ts +109 -0
- package/src/matrix/room-state-codec.ts +115 -0
- package/src/matrix/user-homeserver.ts +106 -0
- package/src/matrix/work-status.ts +294 -0
- package/src/memory/session-history-indexer.ts +328 -0
- package/src/owner-store/ixo-vfs-store.ts +570 -0
- package/src/owner-store/matrix-media-store.ts +136 -0
- package/src/owner-store/migrating-store.ts +185 -0
- package/src/owner-store/owner-copy-errors.ts +175 -0
- package/src/owner-store/retry.ts +45 -0
- package/src/owner-store/tus-upload.ts +333 -0
- package/src/owner-store/types.ts +0 -0
- package/src/plugin-api/define-plugin.ts +45 -0
- package/src/plugin-api/index.ts +56 -0
- package/src/plugin-api/oracle-plugin.ts +145 -0
- package/src/plugin-api/tool-helper.ts +82 -0
- package/src/plugin-api/types.ts +816 -0
- package/src/plugin-api/ucan-errors.ts +16 -0
- package/src/plugins/agui/agui-agent.ts +115 -0
- package/src/plugins/agui/agui.plugin.ts +148 -0
- package/src/plugins/agui/index.ts +2 -0
- package/src/plugins/attachments/attachments.plugin.ts +54 -0
- package/src/plugins/attachments/index.ts +6 -0
- package/src/plugins/attachments/view-attachment-tool.ts +91 -0
- package/src/plugins/base64.ts +26 -0
- package/src/plugins/composio/composio-tools.ts +365 -0
- package/src/plugins/composio/composio-ucan.ts +43 -0
- package/src/plugins/composio/composio.plugin.ts +214 -0
- package/src/plugins/composio/index.ts +15 -0
- package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
- package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
- package/src/plugins/domain-indexer/index.ts +3 -0
- package/src/plugins/editor/blocknote-bridge.ts +120 -0
- package/src/plugins/editor/content-session.ts +331 -0
- package/src/plugins/editor/content-tools.ts +686 -0
- package/src/plugins/editor/document-model.ts +0 -0
- package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
- package/src/plugins/editor/editor-agent.ts +34 -0
- package/src/plugins/editor/editor-config.ts +64 -0
- package/src/plugins/editor/editor-mx.ts +128 -0
- package/src/plugins/editor/editor.plugin.ts +143 -0
- package/src/plugins/editor/failures.ts +154 -0
- package/src/plugins/editor/index.ts +31 -0
- package/src/plugins/editor/jsdom-shim.ts +55 -0
- package/src/plugins/editor/prompts.ts +59 -0
- package/src/plugins/editor/prop-policy.ts +147 -0
- package/src/plugins/editor/provider.ts +386 -0
- package/src/plugins/editor/room-membership.ts +95 -0
- package/src/plugins/editor/standalone-editor-tool.ts +200 -0
- package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
- package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
- package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
- package/src/plugins/firecrawl/index.ts +15 -0
- package/src/plugins/flows/action-metadata.ts +3505 -0
- package/src/plugins/flows/actions.ts +190 -0
- package/src/plugins/flows/edit.ts +314 -0
- package/src/plugins/flows/errors.ts +38 -0
- package/src/plugins/flows/explain.ts +43 -0
- package/src/plugins/flows/flow-doc.ts +111 -0
- package/src/plugins/flows/flows.plugin.ts +129 -0
- package/src/plugins/flows/forms.ts +330 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/linkage.ts +147 -0
- package/src/plugins/flows/port-types.ts +53 -0
- package/src/plugins/flows/prompts.ts +65 -0
- package/src/plugins/flows/read.ts +392 -0
- package/src/plugins/flows/references.ts +66 -0
- package/src/plugins/flows/test-support.ts +165 -0
- package/src/plugins/flows/tools/authoring.ts +516 -0
- package/src/plugins/flows/tools/discovery.ts +119 -0
- package/src/plugins/flows/tools/forms.ts +142 -0
- package/src/plugins/flows/tools/inspect.ts +165 -0
- package/src/plugins/flows/tools/linkage.ts +117 -0
- package/src/plugins/flows/tools/settings.ts +184 -0
- package/src/plugins/flows/translator.ts +328 -0
- package/src/plugins/flows/types.ts +210 -0
- package/src/plugins/flows/ydoc-helpers.ts +543 -0
- package/src/plugins/index.ts +70 -0
- package/src/plugins/mcp-call-timeout.ts +38 -0
- package/src/plugins/mcp-tool-adapter.ts +87 -0
- package/src/plugins/memory/index.ts +17 -0
- package/src/plugins/memory/memory-tools.ts +463 -0
- package/src/plugins/memory/memory-ucan.ts +51 -0
- package/src/plugins/memory/memory.plugin.ts +122 -0
- package/src/plugins/memory/types.ts +147 -0
- package/src/plugins/portal/action-log.ts +34 -0
- package/src/plugins/portal/index.ts +2 -0
- package/src/plugins/portal/portal.plugin.ts +203 -0
- package/src/plugins/sandbox/index.ts +34 -0
- package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
- package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
- package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
- package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
- package/src/plugins/tasks/index.ts +17 -0
- package/src/plugins/tasks/manifest.ts +81 -0
- package/src/plugins/tasks/middleware.ts +219 -0
- package/src/plugins/tasks/tasks-tools.ts +414 -0
- package/src/plugins/tasks/tasks.plugin.ts +74 -0
- package/src/plugins/ucan-failure.ts +133 -0
- package/src/plugins/user-preferences/index.ts +22 -0
- package/src/plugins/user-preferences/schema.ts +31 -0
- package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
- package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
- package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
- package/src/plugins/vfs/index.ts +40 -0
- package/src/plugins/vfs/vfs-auth.ts +56 -0
- package/src/plugins/vfs/vfs-client.ts +655 -0
- package/src/plugins/vfs/vfs-content.ts +181 -0
- package/src/plugins/vfs/vfs-errors.ts +163 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
- package/src/plugins/vfs/vfs-tools.ts +635 -0
- package/src/plugins/vfs/vfs.plugin.ts +201 -0
- package/src/realtime/frontend-call-registry.ts +163 -0
- package/src/realtime/realtime-endpoint.ts +619 -0
- package/src/realtime/session-socket-hub.ts +249 -0
- package/src/realtime/socket-io-codec.ts +187 -0
- package/src/secrets/jwe.ts +74 -0
- package/src/secrets/pin-cipher.ts +43 -0
- package/src/secrets/secrets-service.ts +319 -0
- package/src/shell/app.ts +564 -0
- package/src/shell/auth.ts +290 -0
- package/src/sqlite/blob-codec.ts +100 -0
- package/src/sqlite/blob-compactor.ts +146 -0
- package/src/sqlite/cache-config.ts +45 -0
- package/src/sqlite/database.ts +643 -0
- package/src/sqlite/do-vfs.ts +1485 -0
- package/src/sqlite/index.ts +61 -0
- package/src/sqlite/serialization.ts +370 -0
- package/src/sqlite/sessions-store.ts +311 -0
- package/src/sqlite/sqlite-saver.ts +919 -0
- package/src/sqlite/vacuum-policy.ts +77 -0
- package/src/sqlite/wa-sqlite-loader.ts +123 -0
- package/src/sqlite/wasm.d.ts +10 -0
- package/src/tasks/schedule.ts +174 -0
- package/src/tasks/scheduler.ts +753 -0
- package/src/tasks/spec.ts +125 -0
- package/src/tasks/store.ts +351 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default owner store: **IXO VFS is the system of record**; the user's
|
|
3
|
+
* Matrix room media is only a read-only *legacy* source, checked when VFS has
|
|
4
|
+
* no file yet so users coming from the Node runtime keep their history.
|
|
5
|
+
*
|
|
6
|
+
* load(): VFS first. If absent there but present in Matrix media, import
|
|
7
|
+
* the legacy file AND write it to VFS immediately — from that
|
|
8
|
+
* moment the VFS copy is authoritative and Matrix is never
|
|
9
|
+
* consulted again (head() sees the VFS file).
|
|
10
|
+
* save(): VFS, and only VFS. Nothing is ever written to Matrix media any
|
|
11
|
+
* more: a failed VFS write (no `ixo:filesystem` delegation yet, a
|
|
12
|
+
* VFS outage) propagates, and the object keeps its working copy
|
|
13
|
+
* dirty in Durable Object storage and retries.
|
|
14
|
+
* head(): VFS if a file exists there, else legacy.
|
|
15
|
+
* remove(): both (the user asked to be forgotten).
|
|
16
|
+
* removeLegacyCopy(etag): once the VFS copy is confirmed (its etag matches
|
|
17
|
+
* what the caller just uploaded or loaded), redact the legacy
|
|
18
|
+
* Matrix copy so no second copy of the user's history lingers.
|
|
19
|
+
*/
|
|
20
|
+
import { VfsNoDelegationError } from './ixo-vfs-store';
|
|
21
|
+
import {
|
|
22
|
+
bytesOfStream,
|
|
23
|
+
snapshotOfBytes,
|
|
24
|
+
streamOfBytes,
|
|
25
|
+
type FileSnapshot,
|
|
26
|
+
type OwnerCopy,
|
|
27
|
+
type OwnerStore,
|
|
28
|
+
type SaveResult,
|
|
29
|
+
} from './types';
|
|
30
|
+
|
|
31
|
+
export interface MigratingOwnerStoreOptions {
|
|
32
|
+
/** System of record (IXO VFS). */
|
|
33
|
+
primary: OwnerStore;
|
|
34
|
+
/** Read-mostly legacy source (Matrix room media). */
|
|
35
|
+
legacy: OwnerStore;
|
|
36
|
+
log?: (level: 'log' | 'warn' | 'error', message: string) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class MigratingOwnerStore implements OwnerStore {
|
|
40
|
+
readonly kind = 'vfs' as const;
|
|
41
|
+
private readonly primary: OwnerStore;
|
|
42
|
+
private readonly legacy: OwnerStore;
|
|
43
|
+
private readonly log: (
|
|
44
|
+
level: 'log' | 'warn' | 'error',
|
|
45
|
+
message: string,
|
|
46
|
+
) => void;
|
|
47
|
+
|
|
48
|
+
constructor(opts: MigratingOwnerStoreOptions) {
|
|
49
|
+
this.primary = opts.primary;
|
|
50
|
+
this.legacy = opts.legacy;
|
|
51
|
+
this.log =
|
|
52
|
+
opts.log ??
|
|
53
|
+
((level, message) => {
|
|
54
|
+
// eslint-disable-next-line no-console -- console IS the logger on Workers
|
|
55
|
+
console[level](message);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async load(): Promise<OwnerCopy | null> {
|
|
60
|
+
// "Not on VFS yet" (no ixo:filesystem delegation deposited) means *no VFS
|
|
61
|
+
// copy* — fall through to the legacy source so the user's boot works and
|
|
62
|
+
// their Matrix history stays reachable. Any OTHER failure (store/VFS
|
|
63
|
+
// outage) still throws: silently serving a stale legacy copy to a user
|
|
64
|
+
// whose system of record IS the VFS would fork their history.
|
|
65
|
+
let fromVfs: OwnerCopy | null;
|
|
66
|
+
let noDelegation: VfsNoDelegationError | null = null;
|
|
67
|
+
try {
|
|
68
|
+
fromVfs = await this.primary.load();
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (!(err instanceof VfsNoDelegationError)) throw err;
|
|
71
|
+
this.log(
|
|
72
|
+
'warn',
|
|
73
|
+
`[owner-store] ${err.message} — this user is not on VFS yet; checking legacy Matrix media`,
|
|
74
|
+
);
|
|
75
|
+
noDelegation = err;
|
|
76
|
+
fromVfs = null;
|
|
77
|
+
}
|
|
78
|
+
if (fromVfs) return fromVfs;
|
|
79
|
+
|
|
80
|
+
const fromLegacy = await this.legacy.load().catch((err) => {
|
|
81
|
+
this.log(
|
|
82
|
+
'warn',
|
|
83
|
+
`[owner-store] legacy Matrix load failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
84
|
+
);
|
|
85
|
+
return null;
|
|
86
|
+
});
|
|
87
|
+
if (!fromLegacy) {
|
|
88
|
+
// Without a delegation VFS could not be READ, so "no legacy copy" is
|
|
89
|
+
// not "no file": an existing user whose grant lapsed would otherwise
|
|
90
|
+
// start with an empty history (and later flush it over the real
|
|
91
|
+
// copy). Only a successful VFS listing may say the user has no file.
|
|
92
|
+
if (noDelegation) throw noDelegation;
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// One-time migration: the legacy file becomes the first VFS version. The
|
|
97
|
+
// legacy copy is already in memory (gateway RPCs hand over bytes), so
|
|
98
|
+
// re-opening it for the VFS upload costs nothing extra.
|
|
99
|
+
const legacyBytes = await bytesOfStream(fromLegacy.stream);
|
|
100
|
+
try {
|
|
101
|
+
const { etag } = await this.primary.save(snapshotOfBytes(legacyBytes));
|
|
102
|
+
this.log(
|
|
103
|
+
'log',
|
|
104
|
+
`[owner-store] migrated ${legacyBytes.byteLength} bytes from Matrix media to VFS (${etag})`,
|
|
105
|
+
);
|
|
106
|
+
return { stream: streamOfBytes(legacyBytes), etag };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
this.log(
|
|
109
|
+
'warn',
|
|
110
|
+
`[owner-store] VFS migration write failed — serving the legacy Matrix copy for now: ${err instanceof Error ? err.message : String(err)}`,
|
|
111
|
+
);
|
|
112
|
+
return { stream: streamOfBytes(legacyBytes), etag: fromLegacy.etag };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Write to the system of record. Every failure propagates — including
|
|
118
|
+
* "not on VFS yet" (no delegation) — so the object keeps the copy dirty
|
|
119
|
+
* and retries; Matrix media is never written.
|
|
120
|
+
*/
|
|
121
|
+
async save(snapshot: FileSnapshot): Promise<SaveResult> {
|
|
122
|
+
return this.primary.save(snapshot);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Delete the legacy Matrix copy once VFS is confirmed to hold the file:
|
|
127
|
+
* the VFS `head()` etag must equal `expectedEtag` (the etag the caller just
|
|
128
|
+
* uploaded or loaded), otherwise nothing is touched. Returns true when a
|
|
129
|
+
* legacy copy existed and was removed.
|
|
130
|
+
*/
|
|
131
|
+
async removeLegacyCopy(expectedEtag: string): Promise<boolean> {
|
|
132
|
+
const legacyHead = await this.legacy.head();
|
|
133
|
+
if (!legacyHead) return false;
|
|
134
|
+
const vfsHead = await this.primary.head();
|
|
135
|
+
if (!vfsHead || vfsHead.etag !== expectedEtag) {
|
|
136
|
+
this.log(
|
|
137
|
+
'warn',
|
|
138
|
+
`[owner-store] legacy Matrix copy kept: VFS copy not confirmed (have ${vfsHead?.etag ?? 'none'}, expected ${expectedEtag})`,
|
|
139
|
+
);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
await this.legacy.remove('Migrated to IXO VFS');
|
|
143
|
+
this.log(
|
|
144
|
+
'log',
|
|
145
|
+
`[owner-store] removed the legacy Matrix copy (${legacyHead.etag}); VFS ${vfsHead.etag} is the only copy now`,
|
|
146
|
+
);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async head(): Promise<{ etag: string } | null> {
|
|
151
|
+
// Errors PROPAGATE: a caller must be able to tell "the file is absent"
|
|
152
|
+
// (null) from "could not check" (throw). Swallowing a VFS error to null
|
|
153
|
+
// here would let a boot read a transient outage as an upstream deletion.
|
|
154
|
+
// Only fall through to legacy when VFS positively has no file.
|
|
155
|
+
const vfs = await this.primary.head();
|
|
156
|
+
if (vfs) return vfs;
|
|
157
|
+
return this.legacy.head().catch(() => null);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The legacy Matrix copy regardless of what VFS holds (see `OwnerStore.loadLegacy`). */
|
|
161
|
+
async loadLegacy(): Promise<OwnerCopy | null> {
|
|
162
|
+
return this.legacy.load().catch((err) => {
|
|
163
|
+
this.log(
|
|
164
|
+
'warn',
|
|
165
|
+
`[owner-store] legacy Matrix load failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
166
|
+
);
|
|
167
|
+
return null;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async remove(): Promise<void> {
|
|
172
|
+
await this.primary.remove().catch((err) => {
|
|
173
|
+
this.log(
|
|
174
|
+
'warn',
|
|
175
|
+
`[owner-store] VFS remove failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
await this.legacy.remove().catch((err) => {
|
|
179
|
+
this.log(
|
|
180
|
+
'warn',
|
|
181
|
+
`[owner-store] legacy Matrix remove failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a user's owner copy could not be loaded on a cold boot, classified so
|
|
3
|
+
* the object retries only what can succeed on its own and the shell can
|
|
4
|
+
* answer with an honest status code:
|
|
5
|
+
*
|
|
6
|
+
* - `NO_VFS_DELEGATION` (403, not retryable): the user has not deposited an
|
|
7
|
+
* `ixo:filesystem` delegation for this oracle. Retrying cannot help; the
|
|
8
|
+
* client must deposit the grant and try again.
|
|
9
|
+
* - `VFS_AUTH_FAILED` (403, not retryable): the VFS rejected the oracle's
|
|
10
|
+
* credentials (401/403). Something is misconfigured; retrying cannot help.
|
|
11
|
+
* - `OWNER_COPY_UNAVAILABLE` (503, retryable): the UCAN store or the VFS was
|
|
12
|
+
* unreachable or failing (network, 5xx, 429, timeout). The object already
|
|
13
|
+
* retried with backoff; the next request tries again from scratch.
|
|
14
|
+
*
|
|
15
|
+
* In every case nothing was written — the boot is not memoised on failure.
|
|
16
|
+
*/
|
|
17
|
+
import {
|
|
18
|
+
VfsNoDelegationError,
|
|
19
|
+
VfsRequestError,
|
|
20
|
+
VfsStoreUnavailableError,
|
|
21
|
+
} from './ixo-vfs-store';
|
|
22
|
+
|
|
23
|
+
export type OwnerCopyErrorCode =
|
|
24
|
+
| 'NO_VFS_DELEGATION'
|
|
25
|
+
| 'VFS_AUTH_FAILED'
|
|
26
|
+
| 'OWNER_COPY_UNAVAILABLE';
|
|
27
|
+
|
|
28
|
+
/** Backoff between cold-boot load attempts: three retries, ~7 s in total. */
|
|
29
|
+
export const OWNER_COPY_LOAD_RETRY_DELAYS_MS: readonly number[] = [
|
|
30
|
+
1_000, 2_000, 4_000,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export class OwnerCopyUnavailableError extends Error {
|
|
34
|
+
readonly code: OwnerCopyErrorCode;
|
|
35
|
+
|
|
36
|
+
readonly httpStatus: 403 | 503;
|
|
37
|
+
|
|
38
|
+
readonly retryable: boolean;
|
|
39
|
+
|
|
40
|
+
readonly userDid: string;
|
|
41
|
+
|
|
42
|
+
constructor(userDid: string, cause: unknown, attempts: number) {
|
|
43
|
+
const code = classifyOwnerCopyError(cause);
|
|
44
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
45
|
+
super(messageFor(code, detail, attempts));
|
|
46
|
+
this.name = 'OwnerCopyUnavailableError';
|
|
47
|
+
this.code = code;
|
|
48
|
+
this.httpStatus = code === 'OWNER_COPY_UNAVAILABLE' ? 503 : 403;
|
|
49
|
+
this.retryable = code === 'OWNER_COPY_UNAVAILABLE';
|
|
50
|
+
this.userDid = userDid;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Whether a failed load may succeed if simply tried again shortly. */
|
|
55
|
+
export function isRetryableOwnerCopyError(error: unknown): boolean {
|
|
56
|
+
return classifyOwnerCopyError(error) === 'OWNER_COPY_UNAVAILABLE';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function classifyOwnerCopyError(error: unknown): OwnerCopyErrorCode {
|
|
60
|
+
if (error instanceof VfsNoDelegationError) return 'NO_VFS_DELEGATION';
|
|
61
|
+
if (
|
|
62
|
+
error instanceof VfsRequestError &&
|
|
63
|
+
(error.status === 401 || error.status === 403)
|
|
64
|
+
)
|
|
65
|
+
return 'VFS_AUTH_FAILED';
|
|
66
|
+
return 'OWNER_COPY_UNAVAILABLE';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function messageFor(
|
|
70
|
+
code: OwnerCopyErrorCode,
|
|
71
|
+
detail: string,
|
|
72
|
+
attempts: number,
|
|
73
|
+
): string {
|
|
74
|
+
switch (code) {
|
|
75
|
+
case 'NO_VFS_DELEGATION':
|
|
76
|
+
return `Your oracle data cannot be loaded because this oracle has no file-storage permission from you yet. Deposit an ixo:filesystem delegation for it, then try again. Nothing was changed. (${detail})`;
|
|
77
|
+
case 'VFS_AUTH_FAILED':
|
|
78
|
+
return `Your oracle data cannot be loaded because your file storage rejected the oracle's credentials. Nothing was changed. (${detail})`;
|
|
79
|
+
default:
|
|
80
|
+
return `Your oracle data could not be loaded from your file storage after ${attempts} attempt(s). Nothing was changed — please try again in a moment. (${detail})`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Re-exported so the shell can map the transient store failure too.
|
|
85
|
+
export { VfsStoreUnavailableError };
|
|
86
|
+
|
|
87
|
+
/** What the shell needs to answer a request whose owner copy could not load. */
|
|
88
|
+
export interface OwnerCopyFailure {
|
|
89
|
+
code: OwnerCopyErrorCode;
|
|
90
|
+
httpStatus: 403 | 503;
|
|
91
|
+
retryable: boolean;
|
|
92
|
+
message: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const RPC_ENVELOPE_TAG = 'owner-copy';
|
|
96
|
+
|
|
97
|
+
interface RpcEnvelope extends OwnerCopyFailure {
|
|
98
|
+
oracleError: typeof RPC_ENVELOPE_TAG;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Durable Object RPC serialises a thrown error as a plain `Error` carrying
|
|
103
|
+
* only its message: the class, `name` and custom fields are gone on the
|
|
104
|
+
* caller's side. The user object therefore throws the failure as a JSON
|
|
105
|
+
* envelope in the message, and the shell / turn handler read it back with
|
|
106
|
+
* `parseOwnerCopyFailure`. Inside the object `instanceof` still works.
|
|
107
|
+
*/
|
|
108
|
+
export function toRpcError(failure: OwnerCopyUnavailableError): Error {
|
|
109
|
+
const envelope: RpcEnvelope = {
|
|
110
|
+
oracleError: RPC_ENVELOPE_TAG,
|
|
111
|
+
code: failure.code,
|
|
112
|
+
httpStatus: failure.httpStatus,
|
|
113
|
+
retryable: failure.retryable,
|
|
114
|
+
message: failure.message,
|
|
115
|
+
};
|
|
116
|
+
const err = new Error(JSON.stringify(envelope));
|
|
117
|
+
err.name = failure.name;
|
|
118
|
+
return err;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isEnvelope(value: unknown): value is RpcEnvelope {
|
|
122
|
+
return (
|
|
123
|
+
typeof value === 'object' &&
|
|
124
|
+
value !== null &&
|
|
125
|
+
(value as { oracleError?: unknown }).oracleError === RPC_ENVELOPE_TAG &&
|
|
126
|
+
typeof (value as { code?: unknown }).code === 'string' &&
|
|
127
|
+
typeof (value as { message?: unknown }).message === 'string'
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Recover the typed failure from anything the object may have thrown: the
|
|
133
|
+
* class itself (same isolate), its RPC envelope (across the stub), or the
|
|
134
|
+
* raw store errors when they escape before classification. `null` for
|
|
135
|
+
* everything else (a genuine 500).
|
|
136
|
+
*/
|
|
137
|
+
export function parseOwnerCopyFailure(error: unknown): OwnerCopyFailure | null {
|
|
138
|
+
if (error instanceof OwnerCopyUnavailableError) {
|
|
139
|
+
return {
|
|
140
|
+
code: error.code,
|
|
141
|
+
httpStatus: error.httpStatus,
|
|
142
|
+
retryable: error.retryable,
|
|
143
|
+
message: error.message,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (!(error instanceof Error)) return null;
|
|
147
|
+
if (error.message.startsWith('{')) {
|
|
148
|
+
try {
|
|
149
|
+
const parsed: unknown = JSON.parse(error.message);
|
|
150
|
+
if (isEnvelope(parsed)) {
|
|
151
|
+
const { code, httpStatus, retryable, message } = parsed;
|
|
152
|
+
return { code, httpStatus, retryable, message };
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// not an envelope
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (error.name === 'VfsNoDelegationError') {
|
|
159
|
+
return {
|
|
160
|
+
code: 'NO_VFS_DELEGATION',
|
|
161
|
+
httpStatus: 403,
|
|
162
|
+
retryable: false,
|
|
163
|
+
message: error.message,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (error.name === 'VfsStoreUnavailableError') {
|
|
167
|
+
return {
|
|
168
|
+
code: 'OWNER_COPY_UNAVAILABLE',
|
|
169
|
+
httpStatus: 503,
|
|
170
|
+
retryable: true,
|
|
171
|
+
message: error.message,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded retry with fixed backoff for owner-store requests. A flush that
|
|
3
|
+
* still fails after these attempts is left for the next alarm tick — the
|
|
4
|
+
* working copy stays dirty, nothing is lost.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_RETRY_DELAYS_MS: readonly number[] = [
|
|
8
|
+
2_000, 5_000, 15_000,
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export interface RetryOptions {
|
|
12
|
+
/** Delay before retry n (length = number of retries). */
|
|
13
|
+
delaysMs?: readonly number[];
|
|
14
|
+
isRetryable: (error: unknown) => boolean;
|
|
15
|
+
sleep?: (ms: number) => Promise<void>;
|
|
16
|
+
onRetry?: (error: unknown, attempt: number, delayMs: number) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function withRetry<T>(
|
|
20
|
+
op: (attempt: number) => Promise<T>,
|
|
21
|
+
options: RetryOptions,
|
|
22
|
+
): Promise<T> {
|
|
23
|
+
const delays = options.delaysMs ?? DEFAULT_RETRY_DELAYS_MS;
|
|
24
|
+
const sleep =
|
|
25
|
+
options.sleep ??
|
|
26
|
+
((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
|
|
27
|
+
for (let attempt = 0; ; attempt++) {
|
|
28
|
+
try {
|
|
29
|
+
return await op(attempt);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
const delay = delays[attempt];
|
|
32
|
+
if (delay === undefined || !options.isRetryable(error)) throw error;
|
|
33
|
+
options.onRetry?.(error, attempt + 1, delay);
|
|
34
|
+
await sleep(delay);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Network-level failures (`fetch` rejections, aborts) are always retryable. */
|
|
40
|
+
export function isNetworkError(error: unknown): boolean {
|
|
41
|
+
if (!(error instanceof Error)) return false;
|
|
42
|
+
if (error.name === 'AbortError' || error.name === 'TimeoutError') return true;
|
|
43
|
+
// workerd/undici surface connection failures as TypeError('fetch failed' | 'Network connection lost' | ...).
|
|
44
|
+
return error.name === 'TypeError';
|
|
45
|
+
}
|