@nylorun/runtime 0.10.0-beta → 0.12.0-beta
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/CHANGELOG.md +86 -0
- package/README.md +53 -51
- package/dist/adapters/execution/restate.d.ts +130 -0
- package/dist/adapters/execution/restate.js +442 -0
- package/dist/adapters/sandbox/virtual.js +1 -1
- package/dist/adapters/streams/s2.d.ts +18 -0
- package/dist/adapters/streams/s2.js +361 -0
- package/dist/core/executors.d.ts +4 -1
- package/dist/core/executors.js +20 -1
- package/dist/core/flow-host.d.ts +93 -132
- package/dist/core/flow-host.js +282 -547
- package/dist/core/sandbox-routes.d.ts +13 -4
- package/dist/core/sandbox-routes.js +14 -12
- package/dist/execution/memory.d.ts +54 -0
- package/dist/execution/memory.js +217 -0
- package/dist/execution/types.d.ts +125 -0
- package/dist/execution/types.js +52 -0
- package/dist/host/config-for.d.ts +2 -1
- package/dist/host/config-for.js +3 -21
- package/dist/host/config.d.ts +0 -8
- package/dist/host/create-host.d.ts +36 -1
- package/dist/host/create-host.js +51 -24
- package/dist/host/execution.d.ts +61 -0
- package/dist/host/execution.js +69 -0
- package/dist/host/http.d.ts +4 -0
- package/dist/host/http.js +6 -0
- package/dist/host/index.d.ts +3 -1
- package/dist/host/index.js +2 -0
- package/dist/host/main.js +106 -47
- package/dist/host/stack-config.d.ts +72 -0
- package/dist/host/stack-config.js +193 -0
- package/dist/index.d.ts +1 -1
- package/dist/infra/database.d.ts +16 -0
- package/dist/infra/database.js +31 -0
- package/dist/infra/execution.d.ts +39 -0
- package/dist/infra/execution.js +68 -0
- package/dist/infra/index.d.ts +42 -0
- package/dist/infra/index.js +56 -0
- package/dist/infra/readiness.d.ts +33 -0
- package/dist/infra/readiness.js +53 -0
- package/dist/infra/streams.d.ts +17 -0
- package/dist/infra/streams.js +33 -0
- package/dist/infra/workers.d.ts +39 -0
- package/dist/infra/workers.js +31 -0
- package/dist/model/pi-model.d.ts +2 -2
- package/dist/model/pi-model.js +5 -5
- package/dist/sandbox/manager.d.ts +26 -8
- package/dist/sandbox/manager.js +95 -47
- package/dist/sandbox/select.d.ts +1 -1
- package/dist/sandbox/select.js +4 -5
- package/dist/sandbox/types.d.ts +2 -2
- package/dist/store/canonical.d.ts +5 -0
- package/dist/store/canonical.js +17 -0
- package/dist/store/cursor.d.ts +9 -0
- package/dist/store/cursor.js +18 -0
- package/dist/store/memory.d.ts +57 -0
- package/dist/store/memory.js +645 -0
- package/dist/store/ownership.d.ts +19 -0
- package/dist/store/ownership.js +33 -0
- package/dist/store/postgres/connect.d.ts +25 -0
- package/dist/store/postgres/connect.js +25 -0
- package/dist/store/postgres/locking.d.ts +16 -0
- package/dist/store/postgres/locking.js +22 -0
- package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
- package/dist/store/postgres/migrations/001_initial.js +196 -0
- package/dist/store/postgres/migrations/002_session_owner.d.ts +6 -0
- package/dist/store/postgres/migrations/002_session_owner.js +13 -0
- package/dist/store/postgres/migrations/index.d.ts +49 -0
- package/dist/store/postgres/migrations/index.js +76 -0
- package/dist/store/postgres/names.d.ts +11 -0
- package/dist/store/postgres/names.js +42 -0
- package/dist/store/postgres/store.d.ts +11 -0
- package/dist/store/postgres/store.js +797 -0
- package/dist/store/postgres/tenants.d.ts +90 -0
- package/dist/store/postgres/tenants.js +214 -0
- package/dist/store/types.d.ts +442 -0
- package/dist/store/types.js +10 -0
- package/dist/streams/basin.d.ts +23 -0
- package/dist/streams/basin.js +51 -0
- package/dist/streams/memory.d.ts +25 -0
- package/dist/streams/memory.js +124 -0
- package/dist/streams/relay.d.ts +37 -0
- package/dist/streams/relay.js +254 -0
- package/dist/streams/types.d.ts +115 -0
- package/dist/streams/types.js +86 -0
- package/dist/tenant/actions.d.ts +153 -0
- package/dist/tenant/actions.js +191 -0
- package/dist/tenant/advance.d.ts +15 -0
- package/dist/tenant/advance.js +439 -0
- package/dist/tenant/auth.d.ts +32 -0
- package/dist/tenant/auth.js +169 -0
- package/dist/tenant/commands.d.ts +3 -0
- package/dist/tenant/commands.js +393 -0
- package/dist/tenant/context.d.ts +147 -0
- package/dist/tenant/context.js +48 -0
- package/dist/tenant/effects.d.ts +20 -0
- package/dist/tenant/effects.js +564 -0
- package/dist/tenant/envelope.d.ts +0 -3
- package/dist/tenant/envelope.js +2 -34
- package/dist/tenant/ephemeral.d.ts +15 -4
- package/dist/tenant/ephemeral.js +106 -38
- package/dist/tenant/http.d.ts +38 -0
- package/dist/tenant/http.js +53 -0
- package/dist/tenant/live.d.ts +92 -0
- package/dist/tenant/live.js +290 -0
- package/dist/tenant/model-setting.d.ts +16 -0
- package/dist/tenant/model-setting.js +12 -0
- package/dist/tenant/module.d.ts +29 -8
- package/dist/tenant/module.js +165 -158
- package/dist/tenant/paths.d.ts +12 -8
- package/dist/tenant/paths.js +38 -12
- package/dist/tenant/principals.d.ts +12 -13
- package/dist/tenant/principals.js +50 -38
- package/dist/tenant/quarantine-error.d.ts +1 -1
- package/dist/tenant/quarantine-error.js +2 -8
- package/dist/tenant/quarantine.d.ts +0 -8
- package/dist/tenant/quarantine.js +8 -30
- package/dist/tenant/reset.d.ts +7 -6
- package/dist/tenant/reset.js +7 -51
- package/dist/tenant/routes-tenant.d.ts +12 -0
- package/dist/tenant/routes-tenant.js +137 -0
- package/dist/tenant/routes.d.ts +9 -0
- package/dist/tenant/routes.js +151 -0
- package/dist/tenant/runtime.d.ts +59 -58
- package/dist/tenant/runtime.js +243 -2201
- package/dist/tenant/scheduler.d.ts +25 -0
- package/dist/tenant/scheduler.js +84 -0
- package/dist/tenant/session.d.ts +60 -0
- package/dist/tenant/session.js +94 -0
- package/dist/tenant/sessions.d.ts +40 -0
- package/dist/tenant/sessions.js +163 -0
- package/dist/tenant/status.d.ts +13 -4
- package/dist/tenant/status.js +76 -41
- package/dist/tenant/store-memory.d.ts +9 -12
- package/dist/tenant/store-memory.js +79 -68
- package/dist/tenant/store-pg.d.ts +15 -0
- package/dist/tenant/store-pg.js +140 -0
- package/dist/tenant/streams.d.ts +126 -0
- package/dist/tenant/streams.js +419 -0
- package/dist/tenant/sweep.d.ts +15 -0
- package/dist/tenant/sweep.js +122 -0
- package/dist/tenant/transcript.d.ts +104 -0
- package/dist/tenant/transcript.js +58 -0
- package/dist/tenant/types.d.ts +101 -15
- package/dist/tenant/types.js +23 -1
- package/dist/tenant/worker.d.ts +84 -0
- package/dist/tenant/worker.js +189 -0
- package/dist/vault/service.d.ts +44 -31
- package/dist/vault/service.js +454 -360
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +15 -14
- package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
- package/dist/adapters/sandbox/microsandbox.js +0 -238
- package/dist/core/store.d.ts +0 -32
- package/dist/core/store.js +0 -143
- package/dist/launcher/commands.d.ts +0 -24
- package/dist/launcher/commands.js +0 -260
- package/dist/launcher/ensure-config.d.ts +0 -16
- package/dist/launcher/ensure-config.js +0 -79
- package/dist/launcher/errors.d.ts +0 -12
- package/dist/launcher/errors.js +0 -22
- package/dist/launcher/host-config.d.ts +0 -17
- package/dist/launcher/host-config.js +0 -64
- package/dist/launcher/host-state.d.ts +0 -17
- package/dist/launcher/host-state.js +0 -61
- package/dist/launcher/lifecycle.d.ts +0 -66
- package/dist/launcher/lifecycle.js +0 -407
- package/dist/launcher/locks.d.ts +0 -26
- package/dist/launcher/locks.js +0 -117
- package/dist/launcher/logs.d.ts +0 -18
- package/dist/launcher/logs.js +0 -117
- package/dist/launcher/main.d.ts +0 -2
- package/dist/launcher/main.js +0 -66
- package/dist/launcher/output.d.ts +0 -11
- package/dist/launcher/output.js +0 -68
- package/dist/launcher/paths.d.ts +0 -20
- package/dist/launcher/paths.js +0 -36
- package/dist/launcher/probe.d.ts +0 -23
- package/dist/launcher/probe.js +0 -72
- package/dist/launcher/protocol.d.ts +0 -62
- package/dist/launcher/protocol.js +0 -1
- package/dist/launcher/schema-guard.d.ts +0 -6
- package/dist/launcher/schema-guard.js +0 -36
- package/dist/launcher/spawn.d.ts +0 -22
- package/dist/launcher/spawn.js +0 -85
- package/dist/launcher/status.d.ts +0 -6
- package/dist/launcher/status.js +0 -65
- package/dist/tenant/migration.d.ts +0 -16
- package/dist/tenant/migration.js +0 -116
- package/dist/tenant/schema.d.ts +0 -23
- package/dist/tenant/schema.js +0 -151
- package/dist/tenant/store-fs.d.ts +0 -16
- package/dist/tenant/store-fs.js +0 -205
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { encodeCursor } from "./cursor.js";
|
|
4
|
+
import { OwnershipLostError } from "./ownership.js";
|
|
5
|
+
import { DOC_TABLES, } from "./types.js";
|
|
6
|
+
/** Schema version the fake reports; it has no migrations. */
|
|
7
|
+
export const MEMORY_SCHEMA_VERSION = 1;
|
|
8
|
+
function emptyState() {
|
|
9
|
+
return {
|
|
10
|
+
docs: Object.fromEntries(DOC_TABLES.map((table) => [table, new Map()])),
|
|
11
|
+
sessionMeta: new Map(),
|
|
12
|
+
outbox: new Map(),
|
|
13
|
+
executors: new Map(),
|
|
14
|
+
principals: new Map(),
|
|
15
|
+
vaults: new Map(),
|
|
16
|
+
credentials: new Map(),
|
|
17
|
+
audit: [],
|
|
18
|
+
idempotency: new Map(),
|
|
19
|
+
settings: new Map(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const OWNERSHIP_KEYS = new Set(["owner", "epoch", "ownerExpiresAt"]);
|
|
23
|
+
const OPEN_SESSION = new Set(["running", "runnable"]);
|
|
24
|
+
const byId = (entries) => [...entries].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
25
|
+
const copy = (value) => structuredClone(value);
|
|
26
|
+
/**
|
|
27
|
+
* The data behind in-memory Session Stores: what a database is to its connections. Several
|
|
28
|
+
* `MemorySessionStore`s on one `MemoryStoreData` see each other's commits and serialize their
|
|
29
|
+
* transactions together, as several processes on one Tenant schema would; it outlives them,
|
|
30
|
+
* so a test can close a Tenant and open it again.
|
|
31
|
+
*/
|
|
32
|
+
export class MemoryStoreData {
|
|
33
|
+
/** @internal */ state = emptyState();
|
|
34
|
+
/** @internal */ queue = Promise.resolve();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* In-memory `SessionStore` for unit tests. Not a supported profile.
|
|
38
|
+
*
|
|
39
|
+
* Transactions are serialized (one at a time), each works on a copy of the
|
|
40
|
+
* state and replaces it on commit, so a throw rolls back everything: documents,
|
|
41
|
+
* sequences, outbox rows, `afterCommit` callbacks and `signalWork`. Commit listeners see
|
|
42
|
+
* only this store's commits, as with a Postgres connection.
|
|
43
|
+
*/
|
|
44
|
+
export class MemorySessionStore {
|
|
45
|
+
data;
|
|
46
|
+
tenantId;
|
|
47
|
+
active = new AsyncLocalStorage();
|
|
48
|
+
listeners = new Set();
|
|
49
|
+
now;
|
|
50
|
+
onError;
|
|
51
|
+
closed = false;
|
|
52
|
+
constructor(options,
|
|
53
|
+
/** Shared with other stores on the same data; a fresh, private one by default. */
|
|
54
|
+
data = new MemoryStoreData()) {
|
|
55
|
+
this.data = data;
|
|
56
|
+
this.tenantId = options.tenantId;
|
|
57
|
+
this.now = options.now ?? (() => new Date());
|
|
58
|
+
this.onError =
|
|
59
|
+
options.onError ??
|
|
60
|
+
((error) => queueMicrotask(() => {
|
|
61
|
+
throw error;
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
async tx(fn) {
|
|
65
|
+
if (this.closed)
|
|
66
|
+
throw new Error("SessionStore is closed");
|
|
67
|
+
if (this.active.getStore() === this)
|
|
68
|
+
throw new Error("Nested SessionStore.tx is not allowed");
|
|
69
|
+
const release = await this.acquire();
|
|
70
|
+
let t;
|
|
71
|
+
let result;
|
|
72
|
+
try {
|
|
73
|
+
const working = copy(this.data.state);
|
|
74
|
+
t = new MemoryTx(working, this.tenantId, this.now);
|
|
75
|
+
try {
|
|
76
|
+
result = await this.active.run(this, () => fn(t));
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
t.closed = true;
|
|
80
|
+
}
|
|
81
|
+
this.data.state = working;
|
|
82
|
+
if (t.events.length > 0 || t.workAvailable) {
|
|
83
|
+
const commit = {
|
|
84
|
+
events: t.events,
|
|
85
|
+
incarnations: t.incarnations,
|
|
86
|
+
workAvailable: t.workAvailable,
|
|
87
|
+
};
|
|
88
|
+
for (const listener of this.listeners) {
|
|
89
|
+
try {
|
|
90
|
+
listener(commit);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
this.onError(error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
release();
|
|
100
|
+
}
|
|
101
|
+
for (const callback of t.callbacks) {
|
|
102
|
+
try {
|
|
103
|
+
await callback();
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
this.onError(error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
onCommit(listener) {
|
|
112
|
+
this.listeners.add(listener);
|
|
113
|
+
return () => this.listeners.delete(listener);
|
|
114
|
+
}
|
|
115
|
+
async health() {
|
|
116
|
+
return {
|
|
117
|
+
ok: !this.closed,
|
|
118
|
+
schemaVersion: MEMORY_SCHEMA_VERSION,
|
|
119
|
+
expectedSchemaVersion: MEMORY_SCHEMA_VERSION,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async close() {
|
|
123
|
+
this.closed = true;
|
|
124
|
+
await this.data.queue;
|
|
125
|
+
}
|
|
126
|
+
acquire() {
|
|
127
|
+
let release;
|
|
128
|
+
const next = new Promise((resolve) => (release = resolve));
|
|
129
|
+
const ready = this.data.queue.then(() => release);
|
|
130
|
+
this.data.queue = this.data.queue.then(() => next);
|
|
131
|
+
return ready;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
class MemoryTx {
|
|
135
|
+
s;
|
|
136
|
+
tenantId;
|
|
137
|
+
now;
|
|
138
|
+
closed = false;
|
|
139
|
+
events = [];
|
|
140
|
+
incarnations = [];
|
|
141
|
+
callbacks = [];
|
|
142
|
+
workAvailable = false;
|
|
143
|
+
constructor(s, tenantId, now) {
|
|
144
|
+
this.s = s;
|
|
145
|
+
this.tenantId = tenantId;
|
|
146
|
+
this.now = now;
|
|
147
|
+
}
|
|
148
|
+
check() {
|
|
149
|
+
if (this.closed)
|
|
150
|
+
throw new Error("Tx used after its transaction ended");
|
|
151
|
+
}
|
|
152
|
+
// --- documents -----------------------------------------------------------
|
|
153
|
+
async get(table, id) {
|
|
154
|
+
this.check();
|
|
155
|
+
const raw = this.s.docs[table].get(id);
|
|
156
|
+
if (raw === undefined)
|
|
157
|
+
return undefined;
|
|
158
|
+
return (table === "sessions" ? this.session(id) : JSON.parse(raw));
|
|
159
|
+
}
|
|
160
|
+
async put(table, id, body) {
|
|
161
|
+
this.check();
|
|
162
|
+
let value = body;
|
|
163
|
+
if (table === "sessions" && body && typeof body === "object") {
|
|
164
|
+
value = Object.fromEntries(Object.entries(body).filter(([key]) => !OWNERSHIP_KEYS.has(key)));
|
|
165
|
+
if (!this.s.sessionMeta.has(id))
|
|
166
|
+
this.s.sessionMeta.set(id, {
|
|
167
|
+
owner: null,
|
|
168
|
+
epoch: 0,
|
|
169
|
+
ownerExpiresAt: null,
|
|
170
|
+
nextSeq: 0,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const json = JSON.stringify(value);
|
|
174
|
+
if (json === undefined)
|
|
175
|
+
throw new Error("Document body must be JSON");
|
|
176
|
+
this.s.docs[table].set(id, json);
|
|
177
|
+
}
|
|
178
|
+
async delete(table, id) {
|
|
179
|
+
this.check();
|
|
180
|
+
this.s.docs[table].delete(id);
|
|
181
|
+
if (table === "sessions") {
|
|
182
|
+
this.s.sessionMeta.delete(id);
|
|
183
|
+
this.s.outbox.delete(id);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
session(id) {
|
|
187
|
+
const raw = this.s.docs.sessions.get(id);
|
|
188
|
+
const meta = this.s.sessionMeta.get(id);
|
|
189
|
+
if (raw === undefined || !meta)
|
|
190
|
+
return undefined;
|
|
191
|
+
return {
|
|
192
|
+
...JSON.parse(raw),
|
|
193
|
+
owner: meta.owner,
|
|
194
|
+
epoch: meta.epoch,
|
|
195
|
+
ownerExpiresAt: meta.ownerExpiresAt,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
docs(table) {
|
|
199
|
+
return byId(this.s.docs[table]).map(([id, raw]) => [
|
|
200
|
+
id,
|
|
201
|
+
JSON.parse(raw),
|
|
202
|
+
]);
|
|
203
|
+
}
|
|
204
|
+
sessions() {
|
|
205
|
+
return byId(this.s.docs.sessions).map(([id]) => this.session(id));
|
|
206
|
+
}
|
|
207
|
+
// --- ordering ------------------------------------------------------------
|
|
208
|
+
async lockSession(id) {
|
|
209
|
+
this.check();
|
|
210
|
+
// Transactions are serialized, so the whole store is already locked.
|
|
211
|
+
return this.session(id);
|
|
212
|
+
}
|
|
213
|
+
async event(sessionId, turnId, type, payload) {
|
|
214
|
+
this.check();
|
|
215
|
+
const meta = this.s.sessionMeta.get(sessionId);
|
|
216
|
+
if (!meta)
|
|
217
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
218
|
+
const seq = meta.nextSeq++;
|
|
219
|
+
const event = JSON.parse(JSON.stringify({
|
|
220
|
+
eventId: randomUUID(),
|
|
221
|
+
sessionId,
|
|
222
|
+
tenantId: this.tenantId,
|
|
223
|
+
turnId,
|
|
224
|
+
cursor: encodeCursor(sessionId, seq),
|
|
225
|
+
createdAt: this.now().toISOString(),
|
|
226
|
+
type,
|
|
227
|
+
payload,
|
|
228
|
+
}));
|
|
229
|
+
let rows = this.s.outbox.get(sessionId);
|
|
230
|
+
if (!rows)
|
|
231
|
+
this.s.outbox.set(sessionId, (rows = new Map()));
|
|
232
|
+
rows.set(seq, JSON.stringify(event));
|
|
233
|
+
this.events.push(event);
|
|
234
|
+
const doc = JSON.parse(this.s.docs.sessions.get(sessionId));
|
|
235
|
+
this.incarnations.push(doc.streamIncarnation ?? null);
|
|
236
|
+
return copy(event);
|
|
237
|
+
}
|
|
238
|
+
afterCommit(fn) {
|
|
239
|
+
this.check();
|
|
240
|
+
this.callbacks.push(fn);
|
|
241
|
+
}
|
|
242
|
+
signalWork() {
|
|
243
|
+
this.check();
|
|
244
|
+
this.workAvailable = true;
|
|
245
|
+
}
|
|
246
|
+
// --- ownership -----------------------------------------------------------
|
|
247
|
+
async takeOwnership(sessionId, claim) {
|
|
248
|
+
this.check();
|
|
249
|
+
const meta = this.s.sessionMeta.get(sessionId);
|
|
250
|
+
if (!meta)
|
|
251
|
+
return { status: "missing" };
|
|
252
|
+
if (meta.owner !== null &&
|
|
253
|
+
meta.ownerExpiresAt !== null &&
|
|
254
|
+
Date.parse(meta.ownerExpiresAt) > claim.now.getTime())
|
|
255
|
+
return {
|
|
256
|
+
status: "busy",
|
|
257
|
+
owner: meta.owner,
|
|
258
|
+
ownerExpiresAt: meta.ownerExpiresAt,
|
|
259
|
+
};
|
|
260
|
+
const previous = {
|
|
261
|
+
owner: meta.owner,
|
|
262
|
+
epoch: meta.epoch,
|
|
263
|
+
ownerExpiresAt: meta.ownerExpiresAt,
|
|
264
|
+
};
|
|
265
|
+
meta.epoch += 1;
|
|
266
|
+
meta.owner = claim.owner;
|
|
267
|
+
meta.ownerExpiresAt = new Date(claim.now.getTime() + claim.leaseMs).toISOString();
|
|
268
|
+
return {
|
|
269
|
+
status: "owned",
|
|
270
|
+
epoch: meta.epoch,
|
|
271
|
+
takeover: previous.owner !== null,
|
|
272
|
+
previous,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
async renewOwnership(sessionId, owner, epoch, until) {
|
|
276
|
+
this.check();
|
|
277
|
+
const meta = this.s.sessionMeta.get(sessionId);
|
|
278
|
+
if (!meta || meta.owner !== owner || meta.epoch !== epoch)
|
|
279
|
+
return false;
|
|
280
|
+
meta.ownerExpiresAt = until.toISOString();
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
async releaseOwnership(sessionId, owner, epoch) {
|
|
284
|
+
this.check();
|
|
285
|
+
const meta = this.s.sessionMeta.get(sessionId);
|
|
286
|
+
if (!meta || meta.owner !== owner || meta.epoch !== epoch)
|
|
287
|
+
return false;
|
|
288
|
+
meta.owner = null;
|
|
289
|
+
meta.ownerExpiresAt = null;
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
async assertEpoch(sessionId, epoch) {
|
|
293
|
+
this.check();
|
|
294
|
+
const session = this.session(sessionId);
|
|
295
|
+
if (!session)
|
|
296
|
+
throw new OwnershipLostError(sessionId, epoch, undefined);
|
|
297
|
+
if (session.epoch !== epoch)
|
|
298
|
+
throw new OwnershipLostError(sessionId, epoch, session.epoch);
|
|
299
|
+
return session;
|
|
300
|
+
}
|
|
301
|
+
// --- typed queries -------------------------------------------------------
|
|
302
|
+
async sessionsWithStatus(statuses) {
|
|
303
|
+
this.check();
|
|
304
|
+
return this.sessions().filter((s) => statuses.includes(s.status));
|
|
305
|
+
}
|
|
306
|
+
async orphanedSessions(now, limit) {
|
|
307
|
+
this.check();
|
|
308
|
+
const lease = (s) => s.owner === null || s.ownerExpiresAt === null
|
|
309
|
+
? -Infinity
|
|
310
|
+
: Date.parse(s.ownerExpiresAt);
|
|
311
|
+
return this.sessions()
|
|
312
|
+
.filter((s) => OPEN_SESSION.has(s.status) && lease(s) <= now.getTime())
|
|
313
|
+
.sort((a, b) => lease(a) - lease(b) || (a.id < b.id ? -1 : 1))
|
|
314
|
+
.slice(0, limit);
|
|
315
|
+
}
|
|
316
|
+
async listSessions(filter = {}) {
|
|
317
|
+
this.check();
|
|
318
|
+
return this.sessions().filter((s) => (filter.agentId === undefined || s.agentId === filter.agentId) &&
|
|
319
|
+
(filter.ownerUserId === undefined ||
|
|
320
|
+
s.ownerUserId === filter.ownerUserId));
|
|
321
|
+
}
|
|
322
|
+
async listDefinitions() {
|
|
323
|
+
this.check();
|
|
324
|
+
return this.docs("definitions").map(([, doc]) => doc);
|
|
325
|
+
}
|
|
326
|
+
async listSandboxes() {
|
|
327
|
+
this.check();
|
|
328
|
+
return this.docs("sandboxes").map(([, doc]) => doc);
|
|
329
|
+
}
|
|
330
|
+
actions() {
|
|
331
|
+
return this.docs("actions").map(([, doc]) => doc);
|
|
332
|
+
}
|
|
333
|
+
async expiredClaims(now, limit) {
|
|
334
|
+
this.check();
|
|
335
|
+
return this.actions()
|
|
336
|
+
.filter((a) => a.status === "claimed" &&
|
|
337
|
+
a.leaseExpiresAt !== null &&
|
|
338
|
+
Date.parse(a.leaseExpiresAt) <= now.getTime())
|
|
339
|
+
.sort((a, b) => Date.parse(a.leaseExpiresAt) - Date.parse(b.leaseExpiresAt) ||
|
|
340
|
+
(a.actionId < b.actionId ? -1 : 1))
|
|
341
|
+
.slice(0, limit);
|
|
342
|
+
}
|
|
343
|
+
async pendingActions(agentId) {
|
|
344
|
+
this.check();
|
|
345
|
+
return this.actions().filter((a) => a.status === "pending" && a.agentId === agentId);
|
|
346
|
+
}
|
|
347
|
+
async actionsForSession(sessionId, filter = {}) {
|
|
348
|
+
this.check();
|
|
349
|
+
return this.actions().filter((a) => a.sessionId === sessionId &&
|
|
350
|
+
(filter.turnId === undefined || a.turnId === filter.turnId) &&
|
|
351
|
+
(filter.statuses === undefined || filter.statuses.includes(a.status)));
|
|
352
|
+
}
|
|
353
|
+
async actionsWithStatus(statuses, filter = {}) {
|
|
354
|
+
this.check();
|
|
355
|
+
return this.actions().filter((a) => statuses.includes(a.status) &&
|
|
356
|
+
(filter.kinds === undefined || filter.kinds.includes(a.kind)));
|
|
357
|
+
}
|
|
358
|
+
effects() {
|
|
359
|
+
return this.docs("effects").map(([, doc]) => doc);
|
|
360
|
+
}
|
|
361
|
+
async invokingEffects(sessionId) {
|
|
362
|
+
return this.effectsForSession(sessionId, { statuses: ["invoking"] });
|
|
363
|
+
}
|
|
364
|
+
async effectsForSession(sessionId, filter = {}) {
|
|
365
|
+
this.check();
|
|
366
|
+
return this.effects().filter((e) => e.request?.sessionId === sessionId &&
|
|
367
|
+
(filter.turnId === undefined || e.request.turnId === filter.turnId) &&
|
|
368
|
+
(filter.statuses === undefined || filter.statuses.includes(e.status)));
|
|
369
|
+
}
|
|
370
|
+
async effectsForTurn(sessionId, turnId, statuses) {
|
|
371
|
+
return this.effectsForSession(sessionId, { turnId, statuses });
|
|
372
|
+
}
|
|
373
|
+
async effectsWithStatus(statuses, filter = {}) {
|
|
374
|
+
this.check();
|
|
375
|
+
return this.effects().filter((e) => statuses.includes(e.status) &&
|
|
376
|
+
(filter.kinds === undefined || filter.kinds.includes(e.request?.kind)));
|
|
377
|
+
}
|
|
378
|
+
async linkedSessions(workflowSessionId) {
|
|
379
|
+
this.check();
|
|
380
|
+
const out = [];
|
|
381
|
+
for (const [agentSessionId, link] of this.docs("links")) {
|
|
382
|
+
if (link.workflowSessionId !== workflowSessionId)
|
|
383
|
+
continue;
|
|
384
|
+
const session = this.session(agentSessionId);
|
|
385
|
+
if (session)
|
|
386
|
+
out.push({ agentSessionId, link, session });
|
|
387
|
+
}
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
async counts() {
|
|
391
|
+
this.check();
|
|
392
|
+
const sessions = this.sessions();
|
|
393
|
+
return {
|
|
394
|
+
sessions: sessions.length,
|
|
395
|
+
runningSessions: sessions.filter((s) => OPEN_SESSION.has(s.status))
|
|
396
|
+
.length,
|
|
397
|
+
pendingActions: this.actions().filter((a) => a.status === "pending" || a.status === "claimed").length,
|
|
398
|
+
uncertainEffects: this.effects().filter((e) => e.status === "uncertain")
|
|
399
|
+
.length,
|
|
400
|
+
sandboxes: this.s.docs.sandboxes.size,
|
|
401
|
+
definitions: this.s.docs.definitions.size,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
// --- outbox --------------------------------------------------------------
|
|
405
|
+
async outbox(limit, filter = {}) {
|
|
406
|
+
this.check();
|
|
407
|
+
const rows = [];
|
|
408
|
+
for (const [sessionId, bySeq] of byId(this.s.outbox)) {
|
|
409
|
+
if (filter.sessionId !== undefined && filter.sessionId !== sessionId)
|
|
410
|
+
continue;
|
|
411
|
+
for (const seq of [...bySeq.keys()].sort((a, b) => a - b)) {
|
|
412
|
+
if (rows.length >= limit)
|
|
413
|
+
return rows;
|
|
414
|
+
rows.push({ sessionId, seq, event: JSON.parse(bySeq.get(seq)) });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return rows;
|
|
418
|
+
}
|
|
419
|
+
async deleteOutbox(sessionId, throughSeq, incarnation) {
|
|
420
|
+
this.check();
|
|
421
|
+
const rows = this.s.outbox.get(sessionId);
|
|
422
|
+
if (!rows)
|
|
423
|
+
return 0;
|
|
424
|
+
if (incarnation !== undefined) {
|
|
425
|
+
const doc = this.s.docs.sessions.get(sessionId);
|
|
426
|
+
if (doc === undefined ||
|
|
427
|
+
(JSON.parse(doc).streamIncarnation ?? null) !== incarnation)
|
|
428
|
+
return 0;
|
|
429
|
+
}
|
|
430
|
+
let n = 0;
|
|
431
|
+
for (const seq of [...rows.keys()])
|
|
432
|
+
if (seq <= throughSeq) {
|
|
433
|
+
rows.delete(seq);
|
|
434
|
+
n += 1;
|
|
435
|
+
}
|
|
436
|
+
if (rows.size === 0)
|
|
437
|
+
this.s.outbox.delete(sessionId);
|
|
438
|
+
return n;
|
|
439
|
+
}
|
|
440
|
+
async outboxStats() {
|
|
441
|
+
this.check();
|
|
442
|
+
let depth = 0;
|
|
443
|
+
let oldestCreatedAt = null;
|
|
444
|
+
for (const rows of this.s.outbox.values())
|
|
445
|
+
for (const body of rows.values()) {
|
|
446
|
+
depth += 1;
|
|
447
|
+
const createdAt = JSON.parse(body).createdAt;
|
|
448
|
+
if (oldestCreatedAt === null || createdAt < oldestCreatedAt)
|
|
449
|
+
oldestCreatedAt = createdAt;
|
|
450
|
+
}
|
|
451
|
+
return { depth, oldestCreatedAt };
|
|
452
|
+
}
|
|
453
|
+
// --- executors -----------------------------------------------------------
|
|
454
|
+
async listExecutors() {
|
|
455
|
+
this.check();
|
|
456
|
+
return byId(this.s.executors).map(([, row]) => copy(row));
|
|
457
|
+
}
|
|
458
|
+
async getExecutor(agentId) {
|
|
459
|
+
this.check();
|
|
460
|
+
const row = this.s.executors.get(agentId);
|
|
461
|
+
return row && copy(row);
|
|
462
|
+
}
|
|
463
|
+
async putExecutor(row) {
|
|
464
|
+
this.check();
|
|
465
|
+
for (const other of this.s.executors.values())
|
|
466
|
+
if (other.tokenHash === row.tokenHash && other.agentId !== row.agentId)
|
|
467
|
+
throw new Error("executors.token_hash must be unique");
|
|
468
|
+
const existing = this.s.executors.get(row.agentId);
|
|
469
|
+
this.s.executors.set(row.agentId, {
|
|
470
|
+
...copy(row),
|
|
471
|
+
createdAt: existing?.createdAt ?? row.updatedAt,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
async deleteExecutor(agentId) {
|
|
475
|
+
this.check();
|
|
476
|
+
this.s.executors.delete(agentId);
|
|
477
|
+
}
|
|
478
|
+
// --- principals ----------------------------------------------------------
|
|
479
|
+
async insertPrincipal(row) {
|
|
480
|
+
this.check();
|
|
481
|
+
if (this.s.principals.has(row.id))
|
|
482
|
+
throw new Error("principals.id must be unique");
|
|
483
|
+
for (const other of this.s.principals.values())
|
|
484
|
+
if (other.tokenHash === row.tokenHash)
|
|
485
|
+
throw new Error("principals.token_hash must be unique");
|
|
486
|
+
this.s.principals.set(row.id, copy(row));
|
|
487
|
+
}
|
|
488
|
+
async principalByTokenHash(tokenHash) {
|
|
489
|
+
this.check();
|
|
490
|
+
for (const row of this.s.principals.values())
|
|
491
|
+
if (row.tokenHash === tokenHash)
|
|
492
|
+
return copy(row);
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
495
|
+
async principalById(id) {
|
|
496
|
+
this.check();
|
|
497
|
+
const row = this.s.principals.get(id);
|
|
498
|
+
return row && copy(row);
|
|
499
|
+
}
|
|
500
|
+
async applicationTokenHashes() {
|
|
501
|
+
this.check();
|
|
502
|
+
return byId(this.s.principals)
|
|
503
|
+
.filter(([, row]) => row.role === "application")
|
|
504
|
+
.map(([, row]) => row.tokenHash);
|
|
505
|
+
}
|
|
506
|
+
// --- vault ---------------------------------------------------------------
|
|
507
|
+
async insertVault(row) {
|
|
508
|
+
this.check();
|
|
509
|
+
if (this.s.vaults.has(row.id))
|
|
510
|
+
throw new Error("vaults.id must be unique");
|
|
511
|
+
if (row.scope === "host" &&
|
|
512
|
+
[...this.s.vaults.values()].some((v) => v.scope === "host"))
|
|
513
|
+
throw new Error("Only one host vault is allowed");
|
|
514
|
+
this.s.vaults.set(row.id, copy(row));
|
|
515
|
+
}
|
|
516
|
+
async getVault(id) {
|
|
517
|
+
this.check();
|
|
518
|
+
const row = this.s.vaults.get(id);
|
|
519
|
+
return row && copy(row);
|
|
520
|
+
}
|
|
521
|
+
async vaultsByOwner(ownerUserId) {
|
|
522
|
+
this.check();
|
|
523
|
+
return [...this.s.vaults.values()]
|
|
524
|
+
.filter((v) => v.ownerUserId === ownerUserId && v.scope === "user")
|
|
525
|
+
.sort(byCreated)
|
|
526
|
+
.map(copy);
|
|
527
|
+
}
|
|
528
|
+
async updateVaultMetadata(id, metadataJson) {
|
|
529
|
+
this.check();
|
|
530
|
+
const row = this.s.vaults.get(id);
|
|
531
|
+
if (row)
|
|
532
|
+
row.metadataJson = metadataJson;
|
|
533
|
+
}
|
|
534
|
+
async deleteVault(id) {
|
|
535
|
+
this.check();
|
|
536
|
+
for (const [credentialId, row] of this.s.credentials)
|
|
537
|
+
if (row.vaultId === id)
|
|
538
|
+
this.s.credentials.delete(credentialId);
|
|
539
|
+
this.s.vaults.delete(id);
|
|
540
|
+
}
|
|
541
|
+
async insertCredential(row) {
|
|
542
|
+
this.check();
|
|
543
|
+
if (this.s.credentials.has(row.id))
|
|
544
|
+
throw new Error("vault_credentials.id must be unique");
|
|
545
|
+
if (!this.s.vaults.has(row.vaultId))
|
|
546
|
+
throw new Error("vault_credentials.vault_id references a missing vault");
|
|
547
|
+
this.s.credentials.set(row.id, copy(row));
|
|
548
|
+
}
|
|
549
|
+
async getCredential(id) {
|
|
550
|
+
this.check();
|
|
551
|
+
const row = this.s.credentials.get(id);
|
|
552
|
+
return row && copy(row);
|
|
553
|
+
}
|
|
554
|
+
async credentialsForVault(vaultId, filter = {}) {
|
|
555
|
+
this.check();
|
|
556
|
+
return [...this.s.credentials.values()]
|
|
557
|
+
.filter((c) => c.vaultId === vaultId &&
|
|
558
|
+
(filter.type === undefined || c.type === filter.type))
|
|
559
|
+
.sort(byCreated)
|
|
560
|
+
.map(copy);
|
|
561
|
+
}
|
|
562
|
+
async updateCredential(vaultId, id, patch) {
|
|
563
|
+
this.check();
|
|
564
|
+
const row = this.s.credentials.get(id);
|
|
565
|
+
if (!row || row.vaultId !== vaultId)
|
|
566
|
+
return false;
|
|
567
|
+
const defined = Object.fromEntries(Object.entries(copy(patch)).filter(([, value]) => value !== undefined));
|
|
568
|
+
this.s.credentials.set(id, { ...row, ...defined });
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
async deleteCredential(vaultId, id) {
|
|
572
|
+
this.check();
|
|
573
|
+
const row = this.s.credentials.get(id);
|
|
574
|
+
if (!row || row.vaultId !== vaultId)
|
|
575
|
+
return false;
|
|
576
|
+
this.s.credentials.delete(id);
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
579
|
+
async countCredentials() {
|
|
580
|
+
this.check();
|
|
581
|
+
return this.s.credentials.size;
|
|
582
|
+
}
|
|
583
|
+
async insertVaultAudit(row) {
|
|
584
|
+
this.check();
|
|
585
|
+
this.s.audit.push(copy(row));
|
|
586
|
+
}
|
|
587
|
+
async vaultAudit(filter = {}) {
|
|
588
|
+
this.check();
|
|
589
|
+
return this.s.audit
|
|
590
|
+
.filter((r) => filter.vaultId === undefined || r.vaultId === filter.vaultId)
|
|
591
|
+
.slice(0, filter.limit ?? Infinity)
|
|
592
|
+
.map(copy);
|
|
593
|
+
}
|
|
594
|
+
async getVaultIdempotency(id) {
|
|
595
|
+
this.check();
|
|
596
|
+
const row = this.s.idempotency.get(id);
|
|
597
|
+
return row && copy(row);
|
|
598
|
+
}
|
|
599
|
+
async insertVaultIdempotency(row) {
|
|
600
|
+
this.check();
|
|
601
|
+
if (this.s.idempotency.has(row.id))
|
|
602
|
+
throw new Error("vault_idempotency.id must be unique");
|
|
603
|
+
this.s.idempotency.set(row.id, copy(row));
|
|
604
|
+
}
|
|
605
|
+
// --- settings ------------------------------------------------------------
|
|
606
|
+
async getSetting(key) {
|
|
607
|
+
this.check();
|
|
608
|
+
return this.s.settings.get(key);
|
|
609
|
+
}
|
|
610
|
+
async putSetting(key, value) {
|
|
611
|
+
this.check();
|
|
612
|
+
this.s.settings.set(key, value);
|
|
613
|
+
}
|
|
614
|
+
// --- reset ---------------------------------------------------------------
|
|
615
|
+
async reset(scope) {
|
|
616
|
+
this.check();
|
|
617
|
+
if (scope === "sessions" || scope === "all") {
|
|
618
|
+
for (const table of [
|
|
619
|
+
"sessions",
|
|
620
|
+
"commands",
|
|
621
|
+
"checkpoints",
|
|
622
|
+
"effects",
|
|
623
|
+
"actions",
|
|
624
|
+
"links",
|
|
625
|
+
])
|
|
626
|
+
this.s.docs[table].clear();
|
|
627
|
+
this.s.sessionMeta.clear();
|
|
628
|
+
this.s.outbox.clear();
|
|
629
|
+
}
|
|
630
|
+
if (scope === "sandboxes" || scope === "all")
|
|
631
|
+
this.s.docs.sandboxes.clear();
|
|
632
|
+
if (scope === "all") {
|
|
633
|
+
this.s.docs.definitions.clear();
|
|
634
|
+
this.s.executors.clear();
|
|
635
|
+
for (const vault of [...this.s.vaults.values()])
|
|
636
|
+
if (vault.scope !== "host")
|
|
637
|
+
await this.deleteVault(vault.id);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function byCreated(a, b) {
|
|
642
|
+
if (a.createdAt !== b.createdAt)
|
|
643
|
+
return a.createdAt < b.createdAt ? -1 : 1;
|
|
644
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
645
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SessionDoc, SessionStore, StoredSession, Tx } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* An advance's transaction found the session's epoch changed: another Worker
|
|
4
|
+
* took ownership. The advance aborts with no write (§10.6).
|
|
5
|
+
*/
|
|
6
|
+
export declare class OwnershipLostError extends Error {
|
|
7
|
+
readonly sessionId: string;
|
|
8
|
+
readonly expectedEpoch: number;
|
|
9
|
+
readonly actualEpoch: number | undefined;
|
|
10
|
+
readonly code = "ownership.lost";
|
|
11
|
+
constructor(sessionId: string, expectedEpoch: number, actualEpoch: number | undefined);
|
|
12
|
+
}
|
|
13
|
+
export declare function isOwnershipLost(error: unknown): error is OwnershipLostError;
|
|
14
|
+
/**
|
|
15
|
+
* An epoch-checked transaction: locks the session, checks `epoch`, then runs
|
|
16
|
+
* `fn` with the locked session. A mismatch rejects with `OwnershipLostError`
|
|
17
|
+
* before `fn` runs, so nothing is written.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ownedTx<T, S extends SessionDoc = SessionDoc>(store: SessionStore, sessionId: string, epoch: number, fn: (t: Tx, session: StoredSession<S>) => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An advance's transaction found the session's epoch changed: another Worker
|
|
3
|
+
* took ownership. The advance aborts with no write (§10.6).
|
|
4
|
+
*/
|
|
5
|
+
export class OwnershipLostError extends Error {
|
|
6
|
+
sessionId;
|
|
7
|
+
expectedEpoch;
|
|
8
|
+
actualEpoch;
|
|
9
|
+
code = "ownership.lost";
|
|
10
|
+
constructor(sessionId, expectedEpoch, actualEpoch) {
|
|
11
|
+
super(actualEpoch === undefined
|
|
12
|
+
? `Session ${sessionId} is gone; ownership lost`
|
|
13
|
+
: `Session ${sessionId} is at epoch ${actualEpoch}, not ${expectedEpoch}; ownership lost`);
|
|
14
|
+
this.sessionId = sessionId;
|
|
15
|
+
this.expectedEpoch = expectedEpoch;
|
|
16
|
+
this.actualEpoch = actualEpoch;
|
|
17
|
+
this.name = "OwnershipLostError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function isOwnershipLost(error) {
|
|
21
|
+
return (error instanceof OwnershipLostError ||
|
|
22
|
+
(typeof error === "object" &&
|
|
23
|
+
error !== null &&
|
|
24
|
+
error.code === "ownership.lost"));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* An epoch-checked transaction: locks the session, checks `epoch`, then runs
|
|
28
|
+
* `fn` with the locked session. A mismatch rejects with `OwnershipLostError`
|
|
29
|
+
* before `fn` runs, so nothing is written.
|
|
30
|
+
*/
|
|
31
|
+
export function ownedTx(store, sessionId, epoch, fn) {
|
|
32
|
+
return store.tx(async (t) => fn(t, await t.assertEpoch(sessionId, epoch)));
|
|
33
|
+
}
|