@nylorun/runtime 0.12.0-beta → 0.14.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 +361 -0
- package/README.md +69 -15
- package/dist/adapters/execution/restate.d.ts +7 -2
- package/dist/adapters/execution/restate.js +38 -8
- package/dist/adapters/replication/pgoutput.d.ts +16 -0
- package/dist/adapters/replication/pgoutput.js +169 -0
- package/dist/adapters/streams/s2.js +37 -20
- package/dist/admin-openapi.json +1071 -0
- package/dist/api/a2a/card.d.ts +15 -0
- package/dist/api/a2a/card.js +24 -0
- package/dist/api/a2a/endpoint.d.ts +8 -0
- package/dist/api/a2a/endpoint.js +81 -0
- package/dist/api/a2a/protocol.d.ts +144 -0
- package/dist/api/a2a/protocol.js +188 -0
- package/dist/api/a2a/routes.d.ts +19 -0
- package/dist/api/a2a/routes.js +289 -0
- package/dist/api/a2a/tasks.d.ts +50 -0
- package/dist/api/a2a/tasks.js +186 -0
- package/dist/api/ag-ui/endpoint.d.ts +8 -0
- package/dist/api/ag-ui/endpoint.js +126 -0
- package/dist/api/ag-ui/history.d.ts +7 -0
- package/dist/api/ag-ui/history.js +60 -0
- package/dist/api/ag-ui/routes.d.ts +32 -0
- package/dist/api/ag-ui/routes.js +305 -0
- package/dist/api/ag-ui/session-id.d.ts +1 -0
- package/dist/api/ag-ui/session-id.js +12 -0
- package/dist/api/ag-ui/sse.d.ts +9 -0
- package/dist/api/ag-ui/sse.js +7 -0
- package/dist/api/ag-ui/translate.d.ts +38 -0
- package/dist/api/ag-ui/translate.js +222 -0
- package/dist/api/components.d.ts +7825 -0
- package/dist/api/components.js +83 -0
- package/dist/api/http/app.d.ts +18 -0
- package/dist/api/http/app.js +58 -0
- package/dist/api/http/body.d.ts +4 -0
- package/dist/api/http/body.js +37 -0
- package/dist/api/http/define.d.ts +39 -0
- package/dist/api/http/define.js +130 -0
- package/dist/api/http/respond.d.ts +9 -0
- package/dist/api/http/respond.js +44 -0
- package/dist/api/http/routes/access.d.ts +24 -0
- package/dist/api/http/routes/access.js +259 -0
- package/dist/api/http/routes/actions.d.ts +8 -0
- package/dist/api/http/routes/actions.js +76 -0
- package/dist/api/http/routes/endpoints.d.ts +7 -0
- package/dist/api/http/routes/endpoints.js +100 -0
- package/dist/api/http/routes/sessions.d.ts +3 -0
- package/dist/api/http/routes/sessions.js +205 -0
- package/dist/api/http/routes/tenant.d.ts +10 -0
- package/dist/api/http/routes/tenant.js +172 -0
- package/dist/api/http/routes/vaults.d.ts +8 -0
- package/dist/api/http/routes/vaults.js +167 -0
- package/dist/api/openapi.d.ts +19 -0
- package/dist/api/openapi.js +138 -0
- package/dist/api/route.d.ts +12 -0
- package/dist/api/route.js +4 -0
- package/dist/core/bearer.d.ts +8 -0
- package/dist/core/bearer.js +11 -0
- package/dist/core/flow-host.d.ts +7 -15
- package/dist/core/flow-host.js +19 -41
- package/dist/core/sandbox-routes.d.ts +12 -7
- package/dist/core/sandbox-routes.js +24 -25
- package/dist/execution/memory.d.ts +8 -5
- package/dist/execution/memory.js +33 -13
- package/dist/execution/types.d.ts +38 -6
- package/dist/execution/types.js +12 -3
- package/dist/host/admin-api.d.ts +22 -0
- package/dist/host/admin-api.js +178 -0
- package/dist/host/app.d.ts +34 -0
- package/dist/host/app.js +185 -0
- package/dist/host/config-for.d.ts +2 -0
- package/dist/host/config-for.js +1 -0
- package/dist/host/config.d.ts +11 -0
- package/dist/host/cors.d.ts +27 -0
- package/dist/host/cors.js +64 -0
- package/dist/host/create-host.d.ts +31 -3
- package/dist/host/create-host.js +168 -327
- package/dist/host/http.d.ts +14 -3
- package/dist/host/http.js +64 -23
- package/dist/host/main.js +55 -4
- package/dist/host/stack-config.d.ts +20 -0
- package/dist/host/stack-config.js +55 -0
- package/dist/mcp/pool.d.ts +19 -1
- package/dist/mcp/pool.js +60 -10
- package/dist/model/catalog.d.ts +2 -10
- package/dist/model/classify.d.ts +13 -0
- package/dist/model/classify.js +61 -0
- package/dist/model/models.d.ts +10 -1
- package/dist/model/models.js +14 -6
- package/dist/model/pi-model.d.ts +17 -2
- package/dist/model/pi-model.js +366 -222
- package/dist/openapi.json +25696 -0
- package/dist/sandbox/manager.d.ts +13 -1
- package/dist/sandbox/manager.js +8 -11
- package/dist/sandbox/resolve.d.ts +33 -0
- package/dist/sandbox/resolve.js +81 -0
- package/dist/sandbox/session-sandbox.d.ts +11 -0
- package/dist/sandbox/session-sandbox.js +51 -0
- package/dist/sandbox/share.d.ts +14 -2
- package/dist/sandbox/share.js +33 -3
- package/dist/sandbox/tenant-config.d.ts +37 -0
- package/dist/sandbox/tenant-config.js +51 -0
- package/dist/sandbox/tools.d.ts +7 -9
- package/dist/sandbox/tools.js +11 -3
- package/dist/sandbox/types.d.ts +8 -2
- package/dist/store/event.d.ts +19 -0
- package/dist/store/event.js +49 -0
- package/dist/store/memory.d.ts +20 -8
- package/dist/store/memory.js +265 -100
- package/dist/store/postgres/connect.d.ts +13 -0
- package/dist/store/postgres/connect.js +5 -0
- package/dist/store/postgres/migrations/003_subject_tokens.d.ts +8 -0
- package/dist/store/postgres/migrations/003_subject_tokens.js +42 -0
- package/dist/store/postgres/migrations/004_publishable_keys.d.ts +6 -0
- package/dist/store/postgres/migrations/004_publishable_keys.js +18 -0
- package/dist/store/postgres/migrations/005_action_endpoints.d.ts +6 -0
- package/dist/store/postgres/migrations/005_action_endpoints.js +32 -0
- package/dist/store/postgres/migrations/006_drop_executors.d.ts +11 -0
- package/dist/store/postgres/migrations/006_drop_executors.js +32 -0
- package/dist/store/postgres/migrations/007_event_time.d.ts +7 -0
- package/dist/store/postgres/migrations/007_event_time.js +14 -0
- package/dist/store/postgres/migrations/008_durable_streams.d.ts +13 -0
- package/dist/store/postgres/migrations/008_durable_streams.js +35 -0
- package/dist/store/postgres/migrations/index.d.ts +5 -1
- package/dist/store/postgres/migrations/index.js +22 -2
- package/dist/store/postgres/migrations/shared/index.d.ts +31 -0
- package/dist/store/postgres/migrations/shared/index.js +92 -0
- package/dist/store/postgres/record.d.ts +11 -0
- package/dist/store/postgres/record.js +64 -0
- package/dist/store/postgres/store.js +337 -95
- package/dist/store/postgres/tenants.js +14 -0
- package/dist/store/types.d.ts +176 -81
- package/dist/streams/basin.d.ts +18 -1
- package/dist/streams/basin.js +34 -2
- package/dist/streams/control.d.ts +18 -0
- package/dist/streams/control.js +29 -0
- package/dist/streams/memory.d.ts +3 -1
- package/dist/streams/memory.js +14 -8
- package/dist/streams/relay/core.d.ts +35 -0
- package/dist/streams/relay/core.js +366 -0
- package/dist/streams/relay/memory.d.ts +69 -0
- package/dist/streams/relay/memory.js +176 -0
- package/dist/streams/relay/types.d.ts +77 -0
- package/dist/streams/relay/types.js +7 -0
- package/dist/streams/types.d.ts +66 -36
- package/dist/streams/types.js +18 -53
- package/dist/tenant/access-policy.d.ts +30 -0
- package/dist/tenant/access-policy.js +52 -0
- package/dist/tenant/actions.d.ts +10 -149
- package/dist/tenant/actions.js +5 -167
- package/dist/tenant/advance.js +32 -0
- package/dist/tenant/auth.d.ts +19 -17
- package/dist/tenant/auth.js +90 -93
- package/dist/tenant/browser.d.ts +15 -0
- package/dist/tenant/browser.js +39 -0
- package/dist/tenant/commands.d.ts +18 -2
- package/dist/tenant/commands.js +121 -129
- package/dist/tenant/context.d.ts +75 -18
- package/dist/tenant/context.js +14 -10
- package/dist/tenant/delivery-token.d.ts +41 -0
- package/dist/tenant/delivery-token.js +105 -0
- package/dist/tenant/delivery.d.ts +62 -0
- package/dist/tenant/delivery.js +476 -0
- package/dist/tenant/effects.d.ts +5 -0
- package/dist/tenant/effects.js +138 -48
- package/dist/tenant/endpoints.d.ts +67 -0
- package/dist/tenant/endpoints.js +90 -0
- package/dist/tenant/ephemeral.d.ts +6 -0
- package/dist/tenant/ephemeral.js +60 -36
- package/dist/tenant/http.d.ts +8 -9
- package/dist/tenant/http.js +13 -27
- package/dist/tenant/jwt.d.ts +16 -0
- package/dist/tenant/jwt.js +50 -0
- package/dist/tenant/module.d.ts +3 -0
- package/dist/tenant/module.js +6 -12
- package/dist/tenant/outbound.d.ts +44 -0
- package/dist/tenant/outbound.js +127 -0
- package/dist/tenant/pool.d.ts +0 -2
- package/dist/tenant/pool.js +0 -18
- package/dist/tenant/reset.d.ts +0 -4
- package/dist/tenant/reset.js +0 -5
- package/dist/tenant/runtime.d.ts +10 -20
- package/dist/tenant/runtime.js +59 -42
- package/dist/tenant/scheduler.d.ts +7 -2
- package/dist/tenant/scheduler.js +7 -3
- package/dist/tenant/session-streams.d.ts +125 -0
- package/dist/tenant/session-streams.js +399 -0
- package/dist/tenant/sessions.d.ts +28 -4
- package/dist/tenant/sessions.js +118 -24
- package/dist/tenant/signing-keys.d.ts +55 -0
- package/dist/tenant/signing-keys.js +172 -0
- package/dist/tenant/slim.d.ts +8 -0
- package/dist/tenant/slim.js +21 -0
- package/dist/tenant/status.d.ts +0 -3
- package/dist/tenant/status.js +19 -12
- package/dist/tenant/streams.d.ts +60 -67
- package/dist/tenant/streams.js +140 -208
- package/dist/tenant/subject-limits.d.ts +16 -0
- package/dist/tenant/subject-limits.js +45 -0
- package/dist/tenant/sweep.d.ts +1 -10
- package/dist/tenant/sweep.js +29 -69
- package/dist/tenant/tokens.d.ts +14 -0
- package/dist/tenant/tokens.js +178 -0
- package/dist/tenant/transcript.d.ts +48 -5
- package/dist/tenant/transcript.js +58 -1
- package/dist/tenant/types.d.ts +29 -7
- package/dist/tenant/worker.d.ts +3 -1
- package/dist/tenant/worker.js +9 -3
- package/dist/vault/service.d.ts +18 -2
- package/dist/vault/service.js +57 -13
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +20 -8
- package/dist/core/executors.d.ts +0 -44
- package/dist/core/executors.js +0 -95
- package/dist/streams/relay.d.ts +0 -37
- package/dist/streams/relay.js +0 -254
- package/dist/tenant/live.d.ts +0 -92
- package/dist/tenant/live.js +0 -290
- package/dist/tenant/routes-tenant.d.ts +0 -12
- package/dist/tenant/routes-tenant.js +0 -137
- package/dist/tenant/routes.d.ts +0 -9
- package/dist/tenant/routes.js +0 -151
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* ends. To stay deadlock-free:
|
|
20
20
|
*
|
|
21
21
|
* 1. A session-scoped transaction locks its session row before writing any
|
|
22
|
-
* other row of that session (effects, actions, checkpoints,
|
|
22
|
+
* other row of that session (effects, actions, checkpoints, its record rows
|
|
23
|
+
* and log head).
|
|
23
24
|
* 2. A transaction that touches several sessions locks them in ascending id
|
|
24
25
|
* order, with `lockSessions` (`./locking.ts`), before writing any of them.
|
|
25
26
|
*
|
|
@@ -29,7 +30,7 @@
|
|
|
29
30
|
*
|
|
30
31
|
* ## Values
|
|
31
32
|
*
|
|
32
|
-
* Document and
|
|
33
|
+
* Document and event bodies are `json`, stored as the text `JSON.stringify`
|
|
33
34
|
* wrote, so every string round-trips, including U+0000 and unpaired
|
|
34
35
|
* surrogates that `jsonb` rejects (see `migrations/001_initial.ts`), and key
|
|
35
36
|
* order is kept. Ids and ISO timestamps compared as text use `COLLATE "C"`.
|
|
@@ -37,10 +38,16 @@
|
|
|
37
38
|
*/
|
|
38
39
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
39
40
|
import { randomUUID } from "node:crypto";
|
|
41
|
+
import { buildEvent } from "../event.js";
|
|
40
42
|
import { encodeCursor } from "../cursor.js";
|
|
41
43
|
import { OwnershipLostError } from "../ownership.js";
|
|
42
44
|
import { POSTGRES_SCHEMA_VERSION, readSchemaVersion } from "./migrations/index.js";
|
|
43
45
|
import { assertIdentifier, tenantSchemaName } from "./names.js";
|
|
46
|
+
import { STREAMS_SCHEMA } from "./migrations/shared/index.js";
|
|
47
|
+
import { createPostgresRecordReader } from "./record.js";
|
|
48
|
+
/** The shared record (Durable Streams §6): every Tenant's events and log heads. */
|
|
49
|
+
const SESSION_EVENTS = `${STREAMS_SCHEMA}.session_events`;
|
|
50
|
+
const LOG_HEADS = `${STREAMS_SCHEMA}.session_log_heads`;
|
|
44
51
|
export function createPostgresSessionStore(options) {
|
|
45
52
|
return new PostgresSessionStore(options);
|
|
46
53
|
}
|
|
@@ -94,11 +101,10 @@ class PostgresSessionStore {
|
|
|
94
101
|
finally {
|
|
95
102
|
this.inflight.delete(run);
|
|
96
103
|
}
|
|
97
|
-
if (t.events.length > 0
|
|
104
|
+
if (t.events.length > 0) {
|
|
98
105
|
const commit = {
|
|
99
106
|
events: t.events,
|
|
100
|
-
|
|
101
|
-
workAvailable: t.workAvailable,
|
|
107
|
+
generations: t.generations,
|
|
102
108
|
};
|
|
103
109
|
for (const listener of [...this.listeners]) {
|
|
104
110
|
try {
|
|
@@ -123,6 +129,9 @@ class PostgresSessionStore {
|
|
|
123
129
|
this.listeners.add(listener);
|
|
124
130
|
return () => this.listeners.delete(listener);
|
|
125
131
|
}
|
|
132
|
+
record() {
|
|
133
|
+
return createPostgresRecordReader(this.sql, { tenantId: this.tenantId });
|
|
134
|
+
}
|
|
126
135
|
async health() {
|
|
127
136
|
const expectedSchemaVersion = this.schemaVersion;
|
|
128
137
|
if (this.closed)
|
|
@@ -166,13 +175,23 @@ function toJson(value) {
|
|
|
166
175
|
}
|
|
167
176
|
const bytes = (value) => Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
168
177
|
const fromBytes = (value) => new Uint8Array(value);
|
|
169
|
-
function
|
|
178
|
+
function endpointRow(row) {
|
|
179
|
+
const optional = (key, value) => value === null ? {} : { [key]: value };
|
|
170
180
|
return {
|
|
171
181
|
agentId: row.agent_id,
|
|
172
|
-
|
|
182
|
+
url: row.url,
|
|
173
183
|
implementationVersion: row.implementation_version,
|
|
174
|
-
...(
|
|
175
|
-
|
|
184
|
+
...optional("manifestHash", row.manifest_hash),
|
|
185
|
+
timeoutMs: row.timeout_ms,
|
|
186
|
+
maxConcurrent: row.max_concurrent,
|
|
187
|
+
...optional("principalId", row.principal_id),
|
|
188
|
+
...optional("lastDeliveryAt", row.last_delivery_at),
|
|
189
|
+
...optional("lastSuccessAt", row.last_success_at),
|
|
190
|
+
...optional("lastErrorCode", row.last_error_code),
|
|
191
|
+
...optional("lastErrorMessage", row.last_error_message),
|
|
192
|
+
consecutiveFailures: row.consecutive_failures,
|
|
193
|
+
...optional("servedImplementationVersion", row.served_implementation_version),
|
|
194
|
+
...optional("servedManifestHash", row.served_manifest_hash),
|
|
176
195
|
createdAt: row.created_at,
|
|
177
196
|
updatedAt: row.updated_at,
|
|
178
197
|
};
|
|
@@ -186,6 +205,38 @@ function principalRow(row) {
|
|
|
186
205
|
createdAt: row.created_at,
|
|
187
206
|
};
|
|
188
207
|
}
|
|
208
|
+
function signingKeyRow(row) {
|
|
209
|
+
return {
|
|
210
|
+
id: row.id,
|
|
211
|
+
state: row.state,
|
|
212
|
+
alg: row.alg,
|
|
213
|
+
publicJwk: row.public_jwk,
|
|
214
|
+
createdAt: row.created_at,
|
|
215
|
+
activatedAt: row.activated_at,
|
|
216
|
+
retiredAt: row.retired_at,
|
|
217
|
+
revokedAt: row.revoked_at,
|
|
218
|
+
kekId: row.kek_id,
|
|
219
|
+
nonce: fromBytes(row.nonce),
|
|
220
|
+
ciphertext: fromBytes(row.ciphertext),
|
|
221
|
+
wrappedDek: fromBytes(row.wrapped_dek),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function publishableKeyRow(row) {
|
|
225
|
+
return {
|
|
226
|
+
id: row.id,
|
|
227
|
+
key: row.key,
|
|
228
|
+
name: row.name,
|
|
229
|
+
originsJson: row.origins_json,
|
|
230
|
+
createdAt: row.created_at,
|
|
231
|
+
revokedAt: row.revoked_at,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/** The time column a signing key's new state stamps. */
|
|
235
|
+
const SIGNING_KEY_STAMP = {
|
|
236
|
+
current: "activated_at",
|
|
237
|
+
previous: "retired_at",
|
|
238
|
+
revoked: "revoked_at",
|
|
239
|
+
};
|
|
189
240
|
function vaultRow(row) {
|
|
190
241
|
return {
|
|
191
242
|
id: row.id,
|
|
@@ -244,7 +295,6 @@ const SESSION_TABLES = [
|
|
|
244
295
|
"effects",
|
|
245
296
|
"actions",
|
|
246
297
|
"links",
|
|
247
|
-
"outbox",
|
|
248
298
|
];
|
|
249
299
|
class PostgresTx {
|
|
250
300
|
sql;
|
|
@@ -253,9 +303,8 @@ class PostgresTx {
|
|
|
253
303
|
now;
|
|
254
304
|
closed = false;
|
|
255
305
|
events = [];
|
|
256
|
-
|
|
306
|
+
generations = [];
|
|
257
307
|
callbacks = [];
|
|
258
|
-
workAvailable = false;
|
|
259
308
|
constructor(sql, schema, tenantId, now) {
|
|
260
309
|
this.sql = sql;
|
|
261
310
|
this.schema = schema;
|
|
@@ -295,10 +344,9 @@ class PostgresTx {
|
|
|
295
344
|
}
|
|
296
345
|
async delete(table, id) {
|
|
297
346
|
this.check();
|
|
347
|
+
// A session's record rows and log head stay: a session created again with this id
|
|
348
|
+
// continues its log (per-session record deletion is deferred, Durable Streams §15).
|
|
298
349
|
await this.sql `DELETE FROM ${this.t(table)} WHERE id = ${id}`;
|
|
299
|
-
// The session's stream is abandoned with it (a new incarnation starts at 0).
|
|
300
|
-
if (table === "sessions")
|
|
301
|
-
await this.sql `DELETE FROM ${this.t("outbox")} WHERE session_id = ${id}`;
|
|
302
350
|
}
|
|
303
351
|
async sessionRows(where, tail) {
|
|
304
352
|
const sql = this.sql;
|
|
@@ -319,39 +367,43 @@ class PostgresTx {
|
|
|
319
367
|
async event(sessionId, turnId, type, payload) {
|
|
320
368
|
this.check();
|
|
321
369
|
const sql = this.sql;
|
|
322
|
-
// The
|
|
323
|
-
const [
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
RETURNING next_event_seq - 1 AS seq, stream_incarnation AS incarnation`;
|
|
327
|
-
if (!row)
|
|
370
|
+
// The session row lock orders the session's events; the log head allocates under it.
|
|
371
|
+
const [session] = await sql `
|
|
372
|
+
SELECT epoch FROM ${this.t("sessions")} WHERE id = ${sessionId} FOR UPDATE`;
|
|
373
|
+
if (!session)
|
|
328
374
|
throw new Error(`Session ${sessionId} not found`);
|
|
375
|
+
const [row] = await sql `
|
|
376
|
+
INSERT INTO ${sql(LOG_HEADS)} (tenant_id, session_id, generation, head)
|
|
377
|
+
VALUES (
|
|
378
|
+
${this.tenantId}, ${sessionId},
|
|
379
|
+
coalesce((SELECT basin_generation FROM ${this.t("tenant")}), 0), 1)
|
|
380
|
+
ON CONFLICT (tenant_id, session_id)
|
|
381
|
+
DO UPDATE SET head = ${sql(LOG_HEADS)}.head + 1
|
|
382
|
+
RETURNING head - 1 AS seq, generation`;
|
|
329
383
|
const seq = Number(row.seq);
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
sessionId,
|
|
384
|
+
const generation = Number(row.generation);
|
|
385
|
+
const event = buildEvent({
|
|
333
386
|
tenantId: this.tenantId,
|
|
387
|
+
sessionId,
|
|
334
388
|
turnId,
|
|
335
|
-
|
|
336
|
-
|
|
389
|
+
seq,
|
|
390
|
+
epoch: Number(session.epoch),
|
|
391
|
+
time: this.now(),
|
|
337
392
|
type,
|
|
338
393
|
payload,
|
|
339
|
-
})
|
|
394
|
+
});
|
|
340
395
|
await sql `
|
|
341
|
-
INSERT INTO ${
|
|
342
|
-
VALUES (${sessionId}, ${seq}, ${
|
|
396
|
+
INSERT INTO ${sql(SESSION_EVENTS)} (tenant_id, session_id, seq, generation, type, body)
|
|
397
|
+
VALUES (${this.tenantId}, ${sessionId}, ${seq}, ${generation}, ${type},
|
|
398
|
+
${JSON.stringify(event)}::text::json)`;
|
|
343
399
|
this.events.push(event);
|
|
344
|
-
this.
|
|
400
|
+
this.generations.push(generation);
|
|
345
401
|
return structuredClone(event);
|
|
346
402
|
}
|
|
347
403
|
afterCommit(fn) {
|
|
348
404
|
this.check();
|
|
349
405
|
this.callbacks.push(fn);
|
|
350
406
|
}
|
|
351
|
-
signalWork() {
|
|
352
|
-
this.check();
|
|
353
|
-
this.workAvailable = true;
|
|
354
|
-
}
|
|
355
407
|
// --- ownership -----------------------------------------------------------
|
|
356
408
|
async takeOwnership(sessionId, claim) {
|
|
357
409
|
this.check();
|
|
@@ -443,22 +495,39 @@ class PostgresTx {
|
|
|
443
495
|
this.check();
|
|
444
496
|
return this.bodies("sandboxes");
|
|
445
497
|
}
|
|
446
|
-
async
|
|
498
|
+
async pendingActions(agentId) {
|
|
447
499
|
this.check();
|
|
448
500
|
const rows = await this.sql `
|
|
449
501
|
SELECT body FROM ${this.t("actions")}
|
|
450
|
-
WHERE
|
|
451
|
-
|
|
452
|
-
|
|
502
|
+
WHERE agent_id = ${agentId} AND status = 'pending'
|
|
503
|
+
ORDER BY id`;
|
|
504
|
+
return rows.map((row) => row.body);
|
|
505
|
+
}
|
|
506
|
+
async deliveringCount(agentId) {
|
|
507
|
+
this.check();
|
|
508
|
+
const [row] = await this.sql `
|
|
509
|
+
SELECT count(*)::int AS n FROM ${this.t("actions")}
|
|
510
|
+
WHERE agent_id = ${agentId} AND status = 'delivering'`;
|
|
511
|
+
return row.n;
|
|
512
|
+
}
|
|
513
|
+
async pendingActionsWithEndpoint(limit) {
|
|
514
|
+
this.check();
|
|
515
|
+
const rows = await this.sql `
|
|
516
|
+
SELECT a.body FROM ${this.t("actions")} a
|
|
517
|
+
JOIN ${this.t("endpoints")} e ON e.agent_id = a.agent_id
|
|
518
|
+
WHERE a.status = 'pending'
|
|
519
|
+
ORDER BY a.id
|
|
453
520
|
LIMIT ${limit}`;
|
|
454
521
|
return rows.map((row) => row.body);
|
|
455
522
|
}
|
|
456
|
-
async
|
|
523
|
+
async expiredDeliveries(now, limit) {
|
|
457
524
|
this.check();
|
|
458
525
|
const rows = await this.sql `
|
|
459
526
|
SELECT body FROM ${this.t("actions")}
|
|
460
|
-
WHERE
|
|
461
|
-
|
|
527
|
+
WHERE status = 'delivering' AND deadline_at IS NOT NULL
|
|
528
|
+
AND deadline_at::timestamptz <= ${now}
|
|
529
|
+
ORDER BY deadline_at::timestamptz, id
|
|
530
|
+
LIMIT ${limit}`;
|
|
462
531
|
return rows.map((row) => row.body);
|
|
463
532
|
}
|
|
464
533
|
async actionsForSession(sessionId, filter = {}) {
|
|
@@ -529,7 +598,7 @@ class PostgresTx {
|
|
|
529
598
|
SELECT
|
|
530
599
|
(SELECT count(*) FROM ${this.t("sessions")})::int AS sessions,
|
|
531
600
|
(SELECT count(*) FROM ${this.t("sessions")} WHERE status = ANY(${OPEN_SESSION}))::int AS running,
|
|
532
|
-
(SELECT count(*) FROM ${this.t("actions")} WHERE status IN ('pending', '
|
|
601
|
+
(SELECT count(*) FROM ${this.t("actions")} WHERE status IN ('pending', 'delivering'))::int AS actions,
|
|
533
602
|
(SELECT count(*) FROM ${this.t("effects")} WHERE status = 'uncertain')::int AS uncertain,
|
|
534
603
|
(SELECT count(*) FROM ${this.t("sandboxes")})::int AS sandboxes,
|
|
535
604
|
(SELECT count(*) FROM ${this.t("definitions")})::int AS definitions`;
|
|
@@ -542,78 +611,96 @@ class PostgresTx {
|
|
|
542
611
|
definitions: row.definitions,
|
|
543
612
|
};
|
|
544
613
|
}
|
|
545
|
-
// ---
|
|
546
|
-
async
|
|
547
|
-
this.check();
|
|
548
|
-
const sql = this.sql;
|
|
549
|
-
const rows = await sql `
|
|
550
|
-
SELECT session_id, seq, body FROM ${this.t("outbox")}
|
|
551
|
-
${filter.sessionId === undefined ? sql `` : sql `WHERE session_id = ${filter.sessionId}`}
|
|
552
|
-
ORDER BY session_id, seq
|
|
553
|
-
LIMIT ${limit}`;
|
|
554
|
-
return rows.map((row) => ({
|
|
555
|
-
sessionId: row.session_id,
|
|
556
|
-
seq: Number(row.seq),
|
|
557
|
-
event: row.body,
|
|
558
|
-
}));
|
|
559
|
-
}
|
|
560
|
-
async deleteOutbox(sessionId, throughSeq, incarnation) {
|
|
561
|
-
this.check();
|
|
562
|
-
const sql = this.sql;
|
|
563
|
-
// One statement: the session and the rows are read from the same snapshot.
|
|
564
|
-
const result = await sql `
|
|
565
|
-
DELETE FROM ${this.t("outbox")}
|
|
566
|
-
WHERE session_id = ${sessionId} AND seq <= ${throughSeq}
|
|
567
|
-
${incarnation === undefined
|
|
568
|
-
? sql ``
|
|
569
|
-
: sql `AND EXISTS (
|
|
570
|
-
SELECT 1 FROM ${this.t("sessions")} WHERE id = ${sessionId}
|
|
571
|
-
AND stream_incarnation IS NOT DISTINCT FROM ${incarnation})`}`;
|
|
572
|
-
return result.count;
|
|
573
|
-
}
|
|
574
|
-
async outboxStats() {
|
|
614
|
+
// --- basin generations --------------------------------------------------
|
|
615
|
+
async basinGenerations() {
|
|
575
616
|
this.check();
|
|
576
617
|
const [row] = await this.sql `
|
|
577
|
-
SELECT
|
|
578
|
-
|
|
618
|
+
SELECT basin_generation, retired_generations FROM ${this.t("tenant")}`;
|
|
619
|
+
// A schema without its Tenant row (store tests) is at generation 0.
|
|
620
|
+
if (!row)
|
|
621
|
+
return { current: 0, retired: [] };
|
|
579
622
|
return {
|
|
580
|
-
|
|
581
|
-
|
|
623
|
+
current: Number(row.basin_generation),
|
|
624
|
+
retired: row.retired_generations.map(Number),
|
|
582
625
|
};
|
|
583
626
|
}
|
|
584
|
-
|
|
585
|
-
|
|
627
|
+
async forgetRetiredGeneration(generation) {
|
|
628
|
+
this.check();
|
|
629
|
+
await this.sql `
|
|
630
|
+
UPDATE ${this.t("tenant")}
|
|
631
|
+
SET retired_generations = array_remove(retired_generations, ${generation}::int)`;
|
|
632
|
+
}
|
|
633
|
+
// --- Action endpoints -----------------------------------------------------
|
|
634
|
+
async listEndpoints() {
|
|
586
635
|
this.check();
|
|
587
|
-
const rows = await this.sql `SELECT * FROM ${this.t("
|
|
588
|
-
return rows.map(
|
|
636
|
+
const rows = await this.sql `SELECT * FROM ${this.t("endpoints")} ORDER BY agent_id`;
|
|
637
|
+
return rows.map(endpointRow);
|
|
589
638
|
}
|
|
590
|
-
async
|
|
639
|
+
async getEndpoint(agentId) {
|
|
591
640
|
this.check();
|
|
592
641
|
const [row] = await this.sql `
|
|
593
|
-
SELECT * FROM ${this.t("
|
|
594
|
-
return row &&
|
|
642
|
+
SELECT * FROM ${this.t("endpoints")} WHERE agent_id = ${agentId}`;
|
|
643
|
+
return row && endpointRow(row);
|
|
595
644
|
}
|
|
596
|
-
async
|
|
645
|
+
async putEndpoint(row) {
|
|
597
646
|
this.check();
|
|
647
|
+
// Health belongs to a URL: a new URL starts with none.
|
|
598
648
|
await this.sql `
|
|
599
|
-
INSERT INTO ${this.t("
|
|
600
|
-
agent_id,
|
|
601
|
-
principal_id, created_at, updated_at
|
|
649
|
+
INSERT INTO ${this.t("endpoints")} AS e (
|
|
650
|
+
agent_id, url, implementation_version, manifest_hash, timeout_ms,
|
|
651
|
+
max_concurrent, principal_id, consecutive_failures, created_at, updated_at
|
|
602
652
|
) VALUES (
|
|
603
|
-
${row.agentId}, ${row.
|
|
604
|
-
${row.
|
|
653
|
+
${row.agentId}, ${row.url}, ${row.implementationVersion}, ${row.manifestHash ?? null},
|
|
654
|
+
${row.timeoutMs}, ${row.maxConcurrent}, ${row.principalId ?? null}, 0,
|
|
605
655
|
${row.updatedAt}, ${row.updatedAt}
|
|
606
656
|
)
|
|
607
657
|
ON CONFLICT (agent_id) DO UPDATE SET
|
|
608
|
-
|
|
658
|
+
url = excluded.url,
|
|
609
659
|
implementation_version = excluded.implementation_version,
|
|
610
660
|
manifest_hash = excluded.manifest_hash,
|
|
661
|
+
timeout_ms = excluded.timeout_ms,
|
|
662
|
+
max_concurrent = excluded.max_concurrent,
|
|
611
663
|
principal_id = excluded.principal_id,
|
|
612
|
-
updated_at = excluded.updated_at
|
|
664
|
+
updated_at = excluded.updated_at,
|
|
665
|
+
last_delivery_at = CASE WHEN e.url = excluded.url THEN e.last_delivery_at END,
|
|
666
|
+
last_success_at = CASE WHEN e.url = excluded.url THEN e.last_success_at END,
|
|
667
|
+
last_error_code = CASE WHEN e.url = excluded.url THEN e.last_error_code END,
|
|
668
|
+
last_error_message = CASE WHEN e.url = excluded.url THEN e.last_error_message END,
|
|
669
|
+
consecutive_failures =
|
|
670
|
+
CASE WHEN e.url = excluded.url THEN e.consecutive_failures ELSE 0 END,
|
|
671
|
+
served_implementation_version =
|
|
672
|
+
CASE WHEN e.url = excluded.url THEN e.served_implementation_version END,
|
|
673
|
+
served_manifest_hash =
|
|
674
|
+
CASE WHEN e.url = excluded.url THEN e.served_manifest_hash END`;
|
|
675
|
+
}
|
|
676
|
+
async deleteEndpoint(agentId) {
|
|
677
|
+
this.check();
|
|
678
|
+
await this.sql `DELETE FROM ${this.t("endpoints")} WHERE agent_id = ${agentId}`;
|
|
613
679
|
}
|
|
614
|
-
async
|
|
680
|
+
async recordEndpointHealth(agentId, update) {
|
|
615
681
|
this.check();
|
|
616
|
-
|
|
682
|
+
const table = this.t("endpoints");
|
|
683
|
+
switch (update.kind) {
|
|
684
|
+
case "success":
|
|
685
|
+
await this.sql `
|
|
686
|
+
UPDATE ${table} SET last_delivery_at = ${update.at}, last_success_at = ${update.at},
|
|
687
|
+
consecutive_failures = 0, last_error_code = NULL, last_error_message = NULL
|
|
688
|
+
WHERE agent_id = ${agentId}`;
|
|
689
|
+
return;
|
|
690
|
+
case "failure":
|
|
691
|
+
await this.sql `
|
|
692
|
+
UPDATE ${table} SET last_delivery_at = ${update.at}, last_error_code = ${update.code},
|
|
693
|
+
last_error_message = ${update.message},
|
|
694
|
+
consecutive_failures = consecutive_failures + 1
|
|
695
|
+
WHERE agent_id = ${agentId}`;
|
|
696
|
+
return;
|
|
697
|
+
case "served":
|
|
698
|
+
await this.sql `
|
|
699
|
+
UPDATE ${table} SET served_implementation_version = ${update.implementationVersion},
|
|
700
|
+
served_manifest_hash = ${update.manifestHash ?? null}
|
|
701
|
+
WHERE agent_id = ${agentId}`;
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
617
704
|
}
|
|
618
705
|
// --- principals ----------------------------------------------------------
|
|
619
706
|
async insertPrincipal(row) {
|
|
@@ -766,6 +853,151 @@ class PostgresTx {
|
|
|
766
853
|
INSERT INTO ${this.t("vault_idempotency")} (id, body_hash, response)
|
|
767
854
|
VALUES (${row.id}, ${row.bodyHash}, ${row.response})`;
|
|
768
855
|
}
|
|
856
|
+
// --- subject tokens ------------------------------------------------------
|
|
857
|
+
async lockSigningKeys() {
|
|
858
|
+
this.check();
|
|
859
|
+
const key = `${this.schema}.signing_keys`;
|
|
860
|
+
await this.sql `SELECT pg_advisory_xact_lock(hashtext(${key}))`;
|
|
861
|
+
}
|
|
862
|
+
async insertSigningKey(row) {
|
|
863
|
+
this.check();
|
|
864
|
+
await this.sql `
|
|
865
|
+
INSERT INTO ${this.t("signing_keys")} (
|
|
866
|
+
id, state, alg, public_jwk, kek_id, nonce, ciphertext, wrapped_dek,
|
|
867
|
+
created_at, activated_at, retired_at, revoked_at
|
|
868
|
+
) VALUES (
|
|
869
|
+
${row.id}, ${row.state}, ${row.alg}, ${row.publicJwk}, ${row.kekId},
|
|
870
|
+
${bytes(row.nonce)}, ${bytes(row.ciphertext)}, ${bytes(row.wrappedDek)},
|
|
871
|
+
${row.createdAt}, ${row.activatedAt}, ${row.retiredAt}, ${row.revokedAt}
|
|
872
|
+
)`;
|
|
873
|
+
}
|
|
874
|
+
async signingKey(id) {
|
|
875
|
+
this.check();
|
|
876
|
+
const [row] = await this.sql `
|
|
877
|
+
SELECT * FROM ${this.t("signing_keys")} WHERE id = ${id}`;
|
|
878
|
+
return row && signingKeyRow(row);
|
|
879
|
+
}
|
|
880
|
+
async signingKeys(states) {
|
|
881
|
+
this.check();
|
|
882
|
+
const sql = this.sql;
|
|
883
|
+
const rows = await sql `
|
|
884
|
+
SELECT * FROM ${this.t("signing_keys")}
|
|
885
|
+
${states === undefined ? sql `` : sql `WHERE state = ANY(${states})`}
|
|
886
|
+
ORDER BY created_at, id`;
|
|
887
|
+
return rows.map(signingKeyRow);
|
|
888
|
+
}
|
|
889
|
+
async setSigningKeyState(id, from, to, at) {
|
|
890
|
+
this.check();
|
|
891
|
+
const sql = this.sql;
|
|
892
|
+
const column = SIGNING_KEY_STAMP[to];
|
|
893
|
+
const rows = await sql `
|
|
894
|
+
UPDATE ${this.t("signing_keys")}
|
|
895
|
+
SET state = ${to}${column ? sql `, ${sql(column)} = ${at}` : sql ``}
|
|
896
|
+
WHERE id = ${id} AND state = ${from}
|
|
897
|
+
RETURNING id`;
|
|
898
|
+
return rows.length === 1;
|
|
899
|
+
}
|
|
900
|
+
async countSigningKeys() {
|
|
901
|
+
this.check();
|
|
902
|
+
const [row] = await this.sql `
|
|
903
|
+
SELECT count(*)::int AS n FROM ${this.t("signing_keys")}`;
|
|
904
|
+
return row.n;
|
|
905
|
+
}
|
|
906
|
+
async subjectEpoch(subject) {
|
|
907
|
+
this.check();
|
|
908
|
+
const [row] = await this.sql `
|
|
909
|
+
SELECT epoch FROM ${this.t("subject_epochs")} WHERE subject = ${subject}`;
|
|
910
|
+
return row ? Number(row.epoch) : 0;
|
|
911
|
+
}
|
|
912
|
+
async subjectEpochs(subjects) {
|
|
913
|
+
this.check();
|
|
914
|
+
if (subjects.length === 0)
|
|
915
|
+
return new Map();
|
|
916
|
+
const rows = await this.sql `
|
|
917
|
+
SELECT subject, epoch FROM ${this.t("subject_epochs")}
|
|
918
|
+
WHERE subject = ANY(${subjects})`;
|
|
919
|
+
return new Map(rows.map((row) => [row.subject, Number(row.epoch)]));
|
|
920
|
+
}
|
|
921
|
+
async bumpSubjectEpoch(subject, at) {
|
|
922
|
+
this.check();
|
|
923
|
+
const [row] = await this.sql `
|
|
924
|
+
INSERT INTO ${this.t("subject_epochs")} (subject, epoch, revoked_at)
|
|
925
|
+
VALUES (${subject}, 1, ${at})
|
|
926
|
+
ON CONFLICT (subject) DO UPDATE
|
|
927
|
+
SET epoch = ${this.t("subject_epochs")}.epoch + 1, revoked_at = excluded.revoked_at
|
|
928
|
+
RETURNING epoch`;
|
|
929
|
+
return Number(row.epoch);
|
|
930
|
+
}
|
|
931
|
+
async lockSubjectUsage(initial) {
|
|
932
|
+
this.check();
|
|
933
|
+
await this.sql `
|
|
934
|
+
INSERT INTO ${this.t("subject_usage")} (subject, turn_tokens, refilled_at)
|
|
935
|
+
VALUES (${initial.subject}, ${initial.turnTokens}, ${initial.refilledAt})
|
|
936
|
+
ON CONFLICT (subject) DO NOTHING`;
|
|
937
|
+
const [row] = await this.sql `
|
|
938
|
+
SELECT subject, turn_tokens, refilled_at FROM ${this.t("subject_usage")}
|
|
939
|
+
WHERE subject = ${initial.subject} FOR UPDATE`;
|
|
940
|
+
return {
|
|
941
|
+
subject: row.subject,
|
|
942
|
+
turnTokens: Number(row.turn_tokens),
|
|
943
|
+
refilledAt: row.refilled_at,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
async putSubjectUsage(row) {
|
|
947
|
+
this.check();
|
|
948
|
+
await this.sql `
|
|
949
|
+
INSERT INTO ${this.t("subject_usage")} (subject, turn_tokens, refilled_at)
|
|
950
|
+
VALUES (${row.subject}, ${row.turnTokens}, ${row.refilledAt})
|
|
951
|
+
ON CONFLICT (subject) DO UPDATE
|
|
952
|
+
SET turn_tokens = excluded.turn_tokens, refilled_at = excluded.refilled_at`;
|
|
953
|
+
}
|
|
954
|
+
async countOwnerSessions(ownerUserId, statuses) {
|
|
955
|
+
this.check();
|
|
956
|
+
const [row] = await this.sql `
|
|
957
|
+
SELECT count(*)::int AS n FROM ${this.t("sessions")}
|
|
958
|
+
WHERE owner_user_id = ${ownerUserId} AND status = ANY(${statuses})`;
|
|
959
|
+
return row.n;
|
|
960
|
+
}
|
|
961
|
+
// --- publishable keys ----------------------------------------------------
|
|
962
|
+
async insertPublishableKey(row) {
|
|
963
|
+
this.check();
|
|
964
|
+
await this.sql `
|
|
965
|
+
INSERT INTO ${this.t("publishable_keys")} (id, key, name, origins_json, created_at, revoked_at)
|
|
966
|
+
VALUES (${row.id}, ${row.key}, ${row.name}, ${row.originsJson}, ${row.createdAt}, ${row.revokedAt})`;
|
|
967
|
+
}
|
|
968
|
+
async publishableKeyByKey(key) {
|
|
969
|
+
this.check();
|
|
970
|
+
const [row] = await this.sql `
|
|
971
|
+
SELECT * FROM ${this.t("publishable_keys")} WHERE key = ${key}`;
|
|
972
|
+
return row && publishableKeyRow(row);
|
|
973
|
+
}
|
|
974
|
+
async publishableKey(id) {
|
|
975
|
+
this.check();
|
|
976
|
+
const [row] = await this.sql `
|
|
977
|
+
SELECT * FROM ${this.t("publishable_keys")} WHERE id = ${id}`;
|
|
978
|
+
return row && publishableKeyRow(row);
|
|
979
|
+
}
|
|
980
|
+
async publishableKeys() {
|
|
981
|
+
this.check();
|
|
982
|
+
const rows = await this.sql `
|
|
983
|
+
SELECT * FROM ${this.t("publishable_keys")} ORDER BY created_at, id`;
|
|
984
|
+
return rows.map(publishableKeyRow);
|
|
985
|
+
}
|
|
986
|
+
async updatePublishableKey(id, patch) {
|
|
987
|
+
this.check();
|
|
988
|
+
const sql = this.sql;
|
|
989
|
+
const sets = [
|
|
990
|
+
...(patch.originsJson === undefined ? [] : [sql `origins_json = ${patch.originsJson}`]),
|
|
991
|
+
...(patch.revokedAt === undefined ? [] : [sql `revoked_at = ${patch.revokedAt}`]),
|
|
992
|
+
];
|
|
993
|
+
if (sets.length === 0)
|
|
994
|
+
return (await this.publishableKey(id)) !== undefined;
|
|
995
|
+
const rows = await sql `
|
|
996
|
+
UPDATE ${this.t("publishable_keys")}
|
|
997
|
+
SET ${sets.reduce((all, next) => sql `${all}, ${next}`)}
|
|
998
|
+
WHERE id = ${id} RETURNING id`;
|
|
999
|
+
return rows.length === 1;
|
|
1000
|
+
}
|
|
769
1001
|
// --- settings ------------------------------------------------------------
|
|
770
1002
|
async getSetting(key) {
|
|
771
1003
|
this.check();
|
|
@@ -783,14 +1015,24 @@ class PostgresTx {
|
|
|
783
1015
|
async reset(scope) {
|
|
784
1016
|
this.check();
|
|
785
1017
|
const sql = this.sql;
|
|
786
|
-
if (scope === "sessions" || scope === "all")
|
|
1018
|
+
if (scope === "sessions" || scope === "all") {
|
|
787
1019
|
for (const table of SESSION_TABLES)
|
|
788
1020
|
await sql `DELETE FROM ${this.t(table)}`;
|
|
1021
|
+
await sql `DELETE FROM ${this.t("subject_usage")}`;
|
|
1022
|
+
// The record goes with the sessions, and the Tenant moves to a new basin: the ids it
|
|
1023
|
+
// frees start again in an empty one (Durable Streams §8.1).
|
|
1024
|
+
await sql `DELETE FROM ${sql(SESSION_EVENTS)} WHERE tenant_id = ${this.tenantId}`;
|
|
1025
|
+
await sql `DELETE FROM ${sql(LOG_HEADS)} WHERE tenant_id = ${this.tenantId}`;
|
|
1026
|
+
await sql `
|
|
1027
|
+
UPDATE ${this.t("tenant")}
|
|
1028
|
+
SET retired_generations = array_append(retired_generations, basin_generation),
|
|
1029
|
+
basin_generation = basin_generation + 1`;
|
|
1030
|
+
}
|
|
789
1031
|
if (scope === "sandboxes" || scope === "all")
|
|
790
1032
|
await sql `DELETE FROM ${this.t("sandboxes")}`;
|
|
791
1033
|
if (scope === "all") {
|
|
792
1034
|
await sql `DELETE FROM ${this.t("definitions")}`;
|
|
793
|
-
await sql `DELETE FROM ${this.t("
|
|
1035
|
+
await sql `DELETE FROM ${this.t("endpoints")}`;
|
|
794
1036
|
await sql `DELETE FROM ${this.t("vaults")} WHERE scope <> 'host'`;
|
|
795
1037
|
}
|
|
796
1038
|
}
|
|
@@ -5,6 +5,7 @@ import { TenantConflictError, asQuarantine, quarantine, } from "../../tenant/qua
|
|
|
5
5
|
import { MIGRATIONS, lockSchema, migrateSchemaInTx, readSchemaVersion, schemaExists, } from "./migrations/index.js";
|
|
6
6
|
import { TENANT_SCHEMA_PREFIX, tenantIdFromSchema, tenantSchemaName } from "./names.js";
|
|
7
7
|
import { createPostgresSessionStore } from "./store.js";
|
|
8
|
+
import { STREAMS_SCHEMA, migrateStreamsSchema } from "./migrations/shared/index.js";
|
|
8
9
|
/**
|
|
9
10
|
* Whether Postgres rejected a statement (a SQLSTATE), rather than the connection or the
|
|
10
11
|
* server failing (classes 08, 53, 57, and the driver's own connection errors). Only the
|
|
@@ -19,6 +20,12 @@ function isStatementError(error) {
|
|
|
19
20
|
export function createPostgresTenantCatalog(options) {
|
|
20
21
|
const { sql } = options;
|
|
21
22
|
const now = options.now ?? (() => new Date());
|
|
23
|
+
// The shared record (`nylorun_streams`) exists before any Tenant is created or opened.
|
|
24
|
+
let streamsReady;
|
|
25
|
+
const ensureStreams = () => (streamsReady ??= migrateStreamsSchema(sql).catch((error) => {
|
|
26
|
+
streamsReady = undefined;
|
|
27
|
+
throw error;
|
|
28
|
+
}));
|
|
22
29
|
const migrations = options.migrations ?? MIGRATIONS;
|
|
23
30
|
const latest = migrations.length;
|
|
24
31
|
async function readEnvelopeWith(q, id) {
|
|
@@ -104,6 +111,7 @@ export function createPostgresTenantCatalog(options) {
|
|
|
104
111
|
return readEnvelopeWith(sql, id);
|
|
105
112
|
},
|
|
106
113
|
async createTenant({ envelope, principals }) {
|
|
114
|
+
await ensureStreams();
|
|
107
115
|
const input = TenantEnvelopeSchema.parse(envelope);
|
|
108
116
|
const schema = tenantSchemaName(input.id);
|
|
109
117
|
const outcome = await sql.begin(async (tx) => {
|
|
@@ -145,6 +153,7 @@ export function createPostgresTenantCatalog(options) {
|
|
|
145
153
|
async openTenant(id, storeOptions = {}) {
|
|
146
154
|
if (!isTenantId(id))
|
|
147
155
|
return { status: "not-found" };
|
|
156
|
+
await ensureStreams();
|
|
148
157
|
const schema = tenantSchemaName(id);
|
|
149
158
|
const version = await readSchemaVersion(sql, schema);
|
|
150
159
|
if (version === undefined)
|
|
@@ -206,6 +215,11 @@ export function createPostgresTenantCatalog(options) {
|
|
|
206
215
|
if (!(await schemaExists(tx, schema)))
|
|
207
216
|
return false;
|
|
208
217
|
await tx `DROP SCHEMA ${tx(schema)} CASCADE`;
|
|
218
|
+
// The Tenant's share of the record goes with it (Durable Streams §15).
|
|
219
|
+
if ((await schemaExists(tx, STREAMS_SCHEMA))) {
|
|
220
|
+
await tx `DELETE FROM ${tx(`${STREAMS_SCHEMA}.session_events`)} WHERE tenant_id = ${id}`;
|
|
221
|
+
await tx `DELETE FROM ${tx(`${STREAMS_SCHEMA}.session_log_heads`)} WHERE tenant_id = ${id}`;
|
|
222
|
+
}
|
|
209
223
|
return true;
|
|
210
224
|
});
|
|
211
225
|
},
|