@nylorun/runtime 0.13.0-beta → 0.15.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 +434 -0
- package/README.md +137 -41
- package/dist/adapters/execution/restate.d.ts +7 -2
- package/dist/adapters/execution/restate.js +40 -10
- package/dist/adapters/replication/pgoutput.d.ts +21 -0
- package/dist/adapters/replication/pgoutput.js +170 -0
- package/dist/adapters/streams/s2.js +37 -20
- package/dist/admin-openapi.json +570 -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 +8065 -0
- package/dist/api/components.js +82 -0
- package/dist/api/gate/keys.d.ts +14 -0
- package/dist/api/gate/keys.js +59 -0
- package/dist/api/gate/routes.d.ts +34 -0
- package/dist/api/gate/routes.js +235 -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 +122 -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 +266 -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 +11 -0
- package/dist/api/http/routes/tenant.js +244 -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 +137 -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 +16 -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/gates/contract.d.ts +51 -0
- package/dist/gates/contract.js +37 -0
- package/dist/gates/handler.d.ts +21 -0
- package/dist/gates/handler.js +55 -0
- package/dist/gates/http-client.d.ts +12 -0
- package/dist/gates/http-client.js +160 -0
- package/dist/gates/in-process.d.ts +32 -0
- package/dist/gates/in-process.js +42 -0
- package/dist/gates/inflight.d.ts +39 -0
- package/dist/gates/inflight.js +70 -0
- package/dist/gates/mcp-handler.d.ts +35 -0
- package/dist/gates/mcp-handler.js +142 -0
- package/dist/gates/meter.d.ts +24 -0
- package/dist/gates/meter.js +165 -0
- package/dist/gates/model-gate.d.ts +45 -0
- package/dist/gates/model-gate.js +1 -0
- package/dist/gates/tenant-vaults.d.ts +64 -0
- package/dist/gates/tenant-vaults.js +94 -0
- package/dist/gates/tool-calls.d.ts +51 -0
- package/dist/gates/tool-calls.js +64 -0
- package/dist/gates/tool-client.d.ts +12 -0
- package/dist/gates/tool-client.js +201 -0
- package/dist/gates/tool-contract.d.ts +116 -0
- package/dist/gates/tool-contract.js +64 -0
- package/dist/gates/tool-gate.d.ts +45 -0
- package/dist/gates/tool-gate.js +10 -0
- package/dist/host/admin-api.d.ts +23 -0
- package/dist/host/admin-api.js +74 -0
- package/dist/host/app.d.ts +34 -0
- package/dist/host/app.js +181 -0
- package/dist/host/config-for.d.ts +3 -1
- package/dist/host/config-for.js +3 -2
- 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 +36 -8
- package/dist/host/create-host.js +161 -330
- package/dist/host/execution.d.ts +20 -18
- package/dist/host/execution.js +2 -2
- package/dist/host/gates.d.ts +58 -0
- package/dist/host/gates.js +127 -0
- package/dist/host/http.d.ts +20 -4
- package/dist/host/http.js +81 -23
- package/dist/host/index.d.ts +1 -1
- package/dist/host/main.js +189 -58
- package/dist/host/stack-config.d.ts +91 -19
- package/dist/host/stack-config.js +246 -17
- package/dist/infra/database.d.ts +2 -2
- package/dist/infra/execution.d.ts +5 -5
- package/dist/infra/execution.js +8 -6
- package/dist/infra/index.d.ts +1 -1
- package/dist/infra/workers.d.ts +17 -17
- package/dist/infra/workers.js +9 -9
- package/dist/keys/client.d.ts +9 -0
- package/dist/keys/client.js +70 -0
- package/dist/keys/contract.d.ts +24 -0
- package/dist/keys/contract.js +12 -0
- package/dist/keys/keys.d.ts +41 -0
- package/dist/keys/keys.js +50 -0
- package/dist/mcp/connect.d.ts +43 -3
- package/dist/mcp/connect.js +12 -5
- package/dist/mcp/pool.d.ts +39 -3
- package/dist/mcp/pool.js +74 -11
- 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 +25889 -0
- package/dist/record/append.d.ts +51 -0
- package/dist/record/append.js +20 -0
- package/dist/record/envelope.d.ts +19 -0
- package/dist/record/envelope.js +49 -0
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +11 -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/postgres/connect.d.ts +25 -9
- package/dist/store/postgres/connect.js +27 -10
- package/dist/store/postgres/db.d.ts +16 -0
- package/dist/store/postgres/db.js +28 -0
- package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
- package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
- package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
- package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
- package/dist/store/postgres/migrate.d.ts +68 -0
- package/dist/store/postgres/migrate.js +183 -0
- package/dist/store/postgres/record-writer.d.ts +4 -0
- package/dist/store/postgres/record-writer.js +34 -0
- package/dist/store/postgres/record.d.ts +5 -0
- package/dist/store/postgres/record.js +60 -0
- package/dist/store/postgres/schema.d.ts +3096 -0
- package/dist/store/postgres/schema.js +363 -0
- package/dist/store/postgres/store.d.ts +2 -4
- package/dist/store/postgres/store.js +590 -434
- package/dist/store/postgres/tenant.d.ts +56 -0
- package/dist/store/postgres/tenant.js +154 -0
- package/dist/store/types.d.ts +183 -132
- package/dist/store/types.js +0 -1
- 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 +33 -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/types.d.ts +78 -0
- package/dist/streams/relay/types.js +8 -0
- package/dist/streams/types.d.ts +71 -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.d.ts +7 -2
- package/dist/tenant/advance.js +125 -14
- 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/cause.d.ts +25 -0
- package/dist/tenant/cause.js +45 -0
- package/dist/tenant/commands.d.ts +18 -2
- package/dist/tenant/commands.js +111 -115
- package/dist/tenant/context.d.ts +101 -21
- package/dist/tenant/context.js +14 -10
- package/dist/tenant/delivery-token.d.ts +41 -0
- package/dist/tenant/delivery-token.js +104 -0
- package/dist/tenant/delivery.d.ts +62 -0
- package/dist/tenant/delivery.js +481 -0
- package/dist/tenant/effects.d.ts +20 -1
- package/dist/tenant/effects.js +159 -42
- package/dist/tenant/endpoints.d.ts +67 -0
- package/dist/tenant/endpoints.js +90 -0
- package/dist/tenant/ephemeral.d.ts +22 -7
- package/dist/tenant/ephemeral.js +83 -125
- package/dist/tenant/history.d.ts +64 -0
- package/dist/tenant/history.js +120 -0
- 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/model-setting.d.ts +2 -2
- package/dist/tenant/module.d.ts +17 -28
- package/dist/tenant/module.js +133 -279
- package/dist/tenant/outbound.d.ts +44 -0
- package/dist/tenant/outbound.js +127 -0
- package/dist/tenant/paths.d.ts +11 -16
- package/dist/tenant/paths.js +24 -68
- package/dist/tenant/pool.d.ts +1 -3
- package/dist/tenant/pool.js +1 -19
- package/dist/tenant/principals.d.ts +23 -11
- package/dist/tenant/principals.js +61 -49
- package/dist/tenant/reset.d.ts +0 -4
- package/dist/tenant/reset.js +0 -5
- package/dist/tenant/runtime.d.ts +28 -20
- package/dist/tenant/runtime.js +95 -64
- package/dist/tenant/scheduler.d.ts +10 -3
- package/dist/tenant/scheduler.js +9 -5
- package/dist/tenant/session-streams.d.ts +127 -0
- package/dist/tenant/session-streams.js +404 -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 +3 -3
- package/dist/tenant/status.js +20 -13
- package/dist/tenant/store-pg.d.ts +9 -6
- package/dist/tenant/store-pg.js +60 -125
- package/dist/tenant/streams.d.ts +62 -85
- package/dist/tenant/streams.js +143 -226
- 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 +180 -0
- package/dist/tenant/transcript.d.ts +48 -5
- package/dist/tenant/transcript.js +58 -1
- package/dist/tenant/types.d.ts +69 -98
- package/dist/tenant/types.js +4 -15
- package/dist/tenant/worker.d.ts +3 -1
- package/dist/tenant/worker.js +13 -7
- package/dist/vault/host-model.d.ts +31 -0
- package/dist/vault/host-model.js +57 -0
- package/dist/vault/service.d.ts +44 -17
- package/dist/vault/service.js +126 -121
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +25 -9
- package/dist/core/executors.d.ts +0 -44
- package/dist/core/executors.js +0 -95
- package/dist/store/memory.d.ts +0 -57
- package/dist/store/memory.js +0 -645
- package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
- package/dist/store/postgres/migrations/001_initial.js +0 -196
- package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
- package/dist/store/postgres/migrations/002_session_owner.js +0 -13
- package/dist/store/postgres/migrations/index.d.ts +0 -49
- package/dist/store/postgres/migrations/index.js +0 -76
- package/dist/store/postgres/names.d.ts +0 -11
- package/dist/store/postgres/names.js +0 -42
- package/dist/store/postgres/tenants.d.ts +0 -90
- package/dist/store/postgres/tenants.js +0 -214
- package/dist/streams/relay.d.ts +0 -37
- package/dist/streams/relay.js +0 -254
- package/dist/tenant/envelope.d.ts +0 -7
- package/dist/tenant/envelope.js +0 -22
- package/dist/tenant/live.d.ts +0 -92
- package/dist/tenant/live.js +0 -290
- package/dist/tenant/quarantine-error.d.ts +0 -6
- package/dist/tenant/quarantine-error.js +0 -9
- package/dist/tenant/quarantine.d.ts +0 -27
- package/dist/tenant/quarantine.js +0 -72
- 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
- package/dist/tenant/store-memory.d.ts +0 -16
- package/dist/tenant/store-memory.js +0 -111
- /package/dist/{store → record}/cursor.d.ts +0 -0
- /package/dist/{store → record}/cursor.js +0 -0
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import { isTenantId } from "@nylorun/core/compatibility";
|
|
2
|
-
import { TenantEnvelopeSchema, } from "@nylorun/core/contracts";
|
|
3
|
-
import { STUDIO_PRINCIPAL_ID } from "../../tenant/principals.js";
|
|
4
|
-
import { TenantConflictError, asQuarantine, quarantine, } from "../../tenant/quarantine.js";
|
|
5
|
-
import { MIGRATIONS, lockSchema, migrateSchemaInTx, readSchemaVersion, schemaExists, } from "./migrations/index.js";
|
|
6
|
-
import { TENANT_SCHEMA_PREFIX, tenantIdFromSchema, tenantSchemaName } from "./names.js";
|
|
7
|
-
import { createPostgresSessionStore } from "./store.js";
|
|
8
|
-
/**
|
|
9
|
-
* Whether Postgres rejected a statement (a SQLSTATE), rather than the connection or the
|
|
10
|
-
* server failing (classes 08, 53, 57, and the driver's own connection errors). Only the
|
|
11
|
-
* first says something about the Tenant's schema.
|
|
12
|
-
*/
|
|
13
|
-
function isStatementError(error) {
|
|
14
|
-
const code = error?.code;
|
|
15
|
-
return (typeof code === "string" &&
|
|
16
|
-
/^[0-9A-Z]{5}$/.test(code) &&
|
|
17
|
-
!/^(08|53|57)/.test(code));
|
|
18
|
-
}
|
|
19
|
-
export function createPostgresTenantCatalog(options) {
|
|
20
|
-
const { sql } = options;
|
|
21
|
-
const now = options.now ?? (() => new Date());
|
|
22
|
-
const migrations = options.migrations ?? MIGRATIONS;
|
|
23
|
-
const latest = migrations.length;
|
|
24
|
-
async function readEnvelopeWith(q, id) {
|
|
25
|
-
const schema = tenantSchemaName(id);
|
|
26
|
-
const invalid = (message) => quarantine("envelope-invalid", message, { tenantId: id });
|
|
27
|
-
let rows;
|
|
28
|
-
try {
|
|
29
|
-
rows = await q `
|
|
30
|
-
SELECT id, name, created_at, updated_at, schema_version
|
|
31
|
-
FROM ${q(`${schema}.tenant`)}`;
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
// A missing schema or `tenant` table is the Tenant's problem; anything else
|
|
35
|
-
// (a lost connection) is not, and is thrown as it is.
|
|
36
|
-
const code = error.code;
|
|
37
|
-
if (code !== "42P01" && code !== "3F000")
|
|
38
|
-
throw error;
|
|
39
|
-
throw invalid(`Tenant envelope could not be read: ${error.message}`);
|
|
40
|
-
}
|
|
41
|
-
if (rows.length !== 1)
|
|
42
|
-
throw invalid("Tenant envelope is missing");
|
|
43
|
-
const row = rows[0];
|
|
44
|
-
const parsed = TenantEnvelopeSchema.safeParse({
|
|
45
|
-
id: row.id,
|
|
46
|
-
name: row.name,
|
|
47
|
-
createdAt: row.created_at,
|
|
48
|
-
updatedAt: row.updated_at,
|
|
49
|
-
schemaVersion: row.schema_version,
|
|
50
|
-
});
|
|
51
|
-
if (!parsed.success)
|
|
52
|
-
throw invalid("Tenant envelope is not valid");
|
|
53
|
-
if (parsed.data.id !== id)
|
|
54
|
-
throw invalid(`Tenant envelope id ${parsed.data.id} does not match schema ${schema}`);
|
|
55
|
-
return parsed.data;
|
|
56
|
-
}
|
|
57
|
-
async function bootstrapMatchesWith(q, id, bootstrap) {
|
|
58
|
-
const schema = tenantSchemaName(id);
|
|
59
|
-
const rows = await q `
|
|
60
|
-
SELECT id, token_hash, idempotency_key FROM ${q(`${schema}.principals`)}
|
|
61
|
-
WHERE role = 'application'`;
|
|
62
|
-
const app = rows.find((row) => row.id === bootstrap.principalId);
|
|
63
|
-
if (!app ||
|
|
64
|
-
app.token_hash !== bootstrap.credentialHash ||
|
|
65
|
-
app.idempotency_key !== bootstrap.idempotencyKey)
|
|
66
|
-
return false;
|
|
67
|
-
const studio = rows.find((row) => row.id === STUDIO_PRINCIPAL_ID);
|
|
68
|
-
if (studio?.token_hash !== bootstrap.studioCredentialHash)
|
|
69
|
-
return false;
|
|
70
|
-
return (bootstrap.derivedPrincipals ?? []).every((principal) => rows.find((row) => row.id === principal.id)?.token_hash ===
|
|
71
|
-
principal.credentialHash);
|
|
72
|
-
}
|
|
73
|
-
const catalog = {
|
|
74
|
-
async listTenantIds() {
|
|
75
|
-
const schemas = await sql `
|
|
76
|
-
SELECT nspname FROM pg_namespace
|
|
77
|
-
WHERE starts_with(nspname, ${TENANT_SCHEMA_PREFIX})
|
|
78
|
-
ORDER BY nspname COLLATE "C"`;
|
|
79
|
-
return schemas.flatMap(({ nspname }) => tenantIdFromSchema(nspname) ?? []);
|
|
80
|
-
},
|
|
81
|
-
async tenantExists(id) {
|
|
82
|
-
return isTenantId(id) && schemaExists(sql, tenantSchemaName(id));
|
|
83
|
-
},
|
|
84
|
-
async listTenants() {
|
|
85
|
-
const listings = [];
|
|
86
|
-
for (const id of await catalog.listTenantIds()) {
|
|
87
|
-
try {
|
|
88
|
-
listings.push({ id, envelope: await readEnvelopeWith(sql, id) });
|
|
89
|
-
}
|
|
90
|
-
catch (error) {
|
|
91
|
-
listings.push({
|
|
92
|
-
id,
|
|
93
|
-
envelope: null,
|
|
94
|
-
quarantine: asQuarantine(error) ??
|
|
95
|
-
quarantine("open-failed", "Tenant schema could not be read", {
|
|
96
|
-
tenantId: id,
|
|
97
|
-
}).toQuarantine(),
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return listings;
|
|
102
|
-
},
|
|
103
|
-
readEnvelope(id) {
|
|
104
|
-
return readEnvelopeWith(sql, id);
|
|
105
|
-
},
|
|
106
|
-
async createTenant({ envelope, principals }) {
|
|
107
|
-
const input = TenantEnvelopeSchema.parse(envelope);
|
|
108
|
-
const schema = tenantSchemaName(input.id);
|
|
109
|
-
const outcome = await sql.begin(async (tx) => {
|
|
110
|
-
await lockSchema(tx, schema);
|
|
111
|
-
if (await schemaExists(tx, schema))
|
|
112
|
-
return { created: false };
|
|
113
|
-
await migrateSchemaInTx(tx, schema, migrations);
|
|
114
|
-
const stored = { ...input, schemaVersion: latest };
|
|
115
|
-
await tx `
|
|
116
|
-
INSERT INTO ${tx(`${schema}.tenant`)} (id, name, created_at, updated_at, schema_version)
|
|
117
|
-
VALUES (${stored.id}, ${stored.name}, ${stored.createdAt}, ${stored.updatedAt}, ${stored.schemaVersion})`;
|
|
118
|
-
const createdAt = now().toISOString();
|
|
119
|
-
await tx `
|
|
120
|
-
INSERT INTO ${tx(`${schema}.principals`)} (id, role, token_hash, idempotency_key, created_at)
|
|
121
|
-
VALUES (${principals.principalId}, 'application', ${principals.credentialHash},
|
|
122
|
-
${principals.idempotencyKey}, ${createdAt})`;
|
|
123
|
-
if (principals.studioCredentialHash)
|
|
124
|
-
await tx `
|
|
125
|
-
INSERT INTO ${tx(`${schema}.principals`)} (id, role, token_hash, idempotency_key, created_at)
|
|
126
|
-
VALUES (${STUDIO_PRINCIPAL_ID}, 'application', ${principals.studioCredentialHash},
|
|
127
|
-
NULL, ${createdAt})`;
|
|
128
|
-
for (const derived of principals.derivedPrincipals ?? [])
|
|
129
|
-
await tx `
|
|
130
|
-
INSERT INTO ${tx(`${schema}.principals`)} (id, role, token_hash, idempotency_key, created_at)
|
|
131
|
-
VALUES (${derived.id}, 'application', ${derived.credentialHash}, NULL, ${createdAt})`;
|
|
132
|
-
return { created: true, envelope: stored };
|
|
133
|
-
});
|
|
134
|
-
if (outcome.created)
|
|
135
|
-
return { status: "created", envelope: outcome.envelope };
|
|
136
|
-
if (!(await bootstrapMatchesWith(sql, input.id, principals)))
|
|
137
|
-
throw new TenantConflictError();
|
|
138
|
-
return { status: "exists", envelope: await readEnvelopeWith(sql, input.id) };
|
|
139
|
-
},
|
|
140
|
-
async bootstrapMatches(id, bootstrap) {
|
|
141
|
-
if (!(await schemaExists(sql, tenantSchemaName(id))))
|
|
142
|
-
return false;
|
|
143
|
-
return bootstrapMatchesWith(sql, id, bootstrap);
|
|
144
|
-
},
|
|
145
|
-
async openTenant(id, storeOptions = {}) {
|
|
146
|
-
if (!isTenantId(id))
|
|
147
|
-
return { status: "not-found" };
|
|
148
|
-
const schema = tenantSchemaName(id);
|
|
149
|
-
const version = await readSchemaVersion(sql, schema);
|
|
150
|
-
if (version === undefined)
|
|
151
|
-
return { status: "not-found" };
|
|
152
|
-
if (version > latest)
|
|
153
|
-
return {
|
|
154
|
-
status: "quarantined",
|
|
155
|
-
reason: quarantine("schema-too-new", `Tenant schema version ${version} is newer than this Runtime's ${latest}`, { tenantId: id }).toQuarantine(),
|
|
156
|
-
};
|
|
157
|
-
let migrated = { from: version, to: version };
|
|
158
|
-
if (version < latest) {
|
|
159
|
-
try {
|
|
160
|
-
migrated = await catalog.migrateTenant(id);
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
if (!asQuarantine(error) && !isStatementError(error))
|
|
164
|
-
throw error;
|
|
165
|
-
return {
|
|
166
|
-
status: "quarantined",
|
|
167
|
-
reason: asQuarantine(error) ??
|
|
168
|
-
quarantine("migration-failed", `Tenant schema migration from version ${version} failed: ${error.message}`, { tenantId: id }).toQuarantine(),
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
let envelope;
|
|
173
|
-
try {
|
|
174
|
-
envelope = await readEnvelopeWith(sql, id);
|
|
175
|
-
}
|
|
176
|
-
catch (error) {
|
|
177
|
-
const reason = asQuarantine(error);
|
|
178
|
-
if (!reason)
|
|
179
|
-
throw error;
|
|
180
|
-
return { status: "quarantined", reason };
|
|
181
|
-
}
|
|
182
|
-
const store = createPostgresSessionStore({
|
|
183
|
-
sql,
|
|
184
|
-
tenantId: id,
|
|
185
|
-
schema,
|
|
186
|
-
schemaVersion: latest,
|
|
187
|
-
now: storeOptions.now,
|
|
188
|
-
onError: storeOptions.onError ?? options.onError,
|
|
189
|
-
});
|
|
190
|
-
return { status: "ok", store, envelope, migrated };
|
|
191
|
-
},
|
|
192
|
-
async migrateTenant(id) {
|
|
193
|
-
const schema = tenantSchemaName(id);
|
|
194
|
-
const result = await sql.begin(async (tx) => {
|
|
195
|
-
await lockSchema(tx, schema);
|
|
196
|
-
if (!(await schemaExists(tx, schema)))
|
|
197
|
-
throw new Error(`Tenant ${id} does not exist`);
|
|
198
|
-
return { result: await migrateSchemaInTx(tx, schema, migrations) };
|
|
199
|
-
});
|
|
200
|
-
return result.result;
|
|
201
|
-
},
|
|
202
|
-
async deleteTenant(id) {
|
|
203
|
-
const schema = tenantSchemaName(id);
|
|
204
|
-
return sql.begin(async (tx) => {
|
|
205
|
-
await lockSchema(tx, schema);
|
|
206
|
-
if (!(await schemaExists(tx, schema)))
|
|
207
|
-
return false;
|
|
208
|
-
await tx `DROP SCHEMA ${tx(schema)} CASCADE`;
|
|
209
|
-
return true;
|
|
210
|
-
});
|
|
211
|
-
},
|
|
212
|
-
};
|
|
213
|
-
return catalog;
|
|
214
|
-
}
|
package/dist/streams/relay.d.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { SessionStore } from "../store/types.js";
|
|
2
|
-
import { type DurableStreams } from "./types.js";
|
|
3
|
-
/** A session's outbox starts past the end of its stream: events are missing from the stream. */
|
|
4
|
-
export declare class StreamGapError extends Error {
|
|
5
|
-
constructor(sessionId: string, first: number, tail: number);
|
|
6
|
-
}
|
|
7
|
-
export interface RelayOptions {
|
|
8
|
-
store: SessionStore;
|
|
9
|
-
streams: DurableStreams;
|
|
10
|
-
/** The Tenant whose basin receives the events; must match `store.tenantId`. */
|
|
11
|
-
tenantId: string;
|
|
12
|
-
/** Receives append and delete failures. The rows they concern stay in the outbox. */
|
|
13
|
-
onError?: (error: unknown) => void;
|
|
14
|
-
}
|
|
15
|
-
export interface Relay {
|
|
16
|
-
/**
|
|
17
|
-
* Appends leftover outbox rows (at most `limit`, default 1000) per session in
|
|
18
|
-
* sequence order: the Tenant sweep's recovery after a crash or an S2 outage.
|
|
19
|
-
* Resolves with the number of rows now in S2 and removed from the outbox.
|
|
20
|
-
* Per-session failures go to `onError`; those rows stay.
|
|
21
|
-
*/
|
|
22
|
-
drain(limit?: number): Promise<number>;
|
|
23
|
-
/** Resolves when every relay started so far has settled. */
|
|
24
|
-
idle(): Promise<void>;
|
|
25
|
-
/** Stops listening to commits and waits for in-flight relays. */
|
|
26
|
-
close(): Promise<void>;
|
|
27
|
-
}
|
|
28
|
-
export declare function createRelay(options: RelayOptions): Relay;
|
|
29
|
-
/** Appends a `work_available` signal to `tenant/work`. Not relayed through the outbox. */
|
|
30
|
-
export declare function signalWork(streams: DurableStreams, tenantId: string): Promise<void>;
|
|
31
|
-
/** Appends a `session.cancel` signal to `tenant/control`. Not relayed through the outbox. */
|
|
32
|
-
export declare function signalCancel(streams: DurableStreams, tenantId: string, sessionId: string): Promise<void>;
|
|
33
|
-
/**
|
|
34
|
-
* Appends a `sessions.reset` signal to `tenant/control`: every process with the Tenant open
|
|
35
|
-
* checks its session feeds against the Session Store.
|
|
36
|
-
*/
|
|
37
|
-
export declare function signalSessionsReset(streams: DurableStreams, tenantId: string): Promise<void>;
|
package/dist/streams/relay.js
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The outbox relay (architecture §12.4, "Writing: outbox and relay").
|
|
3
|
-
*
|
|
4
|
-
* The Session Store writes every event to its outbox in the same transaction
|
|
5
|
-
* as the state change. After the commit the relay appends the events to the
|
|
6
|
-
* stream of their session's incarnation (`sessions/<id>/<incarnation>`) with
|
|
7
|
-
* `matchSeq` set to each batch's first sequence, and deletes the rows S2 has:
|
|
8
|
-
*
|
|
9
|
-
* - **The right incarnation.** A committed event comes with the incarnation
|
|
10
|
-
* its session had when the event was allocated (`Commit.incarnations`).
|
|
11
|
-
* Leftover rows (`drain`, and a commit that finds earlier rows still
|
|
12
|
-
* waiting) are read with the session's incarnation in one transaction that
|
|
13
|
-
* locks the session; deleting a session deletes its outbox rows in the same
|
|
14
|
-
* transaction, so those rows belong to the incarnation read. Either way a
|
|
15
|
-
* session deleted and created again with the same id never gets the old
|
|
16
|
-
* incarnation's events, or the reverse.
|
|
17
|
-
* - **ok**: the rows are deleted through the last appended sequence, only
|
|
18
|
-
* while the session still has the incarnation appended to (one statement,
|
|
19
|
-
* `deleteOutbox` with `incarnation`).
|
|
20
|
-
* - **`seq_mismatch` with the tail past the batch start**: an earlier attempt
|
|
21
|
-
* (this process or another) already appended those events. The rows below
|
|
22
|
-
* the tail are deleted, and whatever is left of the batch is appended from
|
|
23
|
-
* the tail.
|
|
24
|
-
* - **`seq_mismatch` with the tail before the batch start**: earlier events of
|
|
25
|
-
* the session are still in the outbox (an earlier append failed), and the
|
|
26
|
-
* session's outbox is relayed in sequence order, which includes this batch.
|
|
27
|
-
* If the outbox's first row is past the tail, events are missing from the
|
|
28
|
-
* stream: the rows stay and the error goes to `onError`.
|
|
29
|
-
* - **append throws** (S2 unreachable, basin missing): the rows stay in the
|
|
30
|
-
* outbox and the error goes to `onError`. The next commit of the session, or
|
|
31
|
-
* the Tenant sweep's `drain`, appends them in order.
|
|
32
|
-
*
|
|
33
|
-
* Conditional appends make this exactly-once and in order per session however
|
|
34
|
-
* many processes relay the same rows. Within one process the relay also runs
|
|
35
|
-
* one relay at a time per session.
|
|
36
|
-
*
|
|
37
|
-
* `work_available`, `session.cancel` and `sessions.reset` are signals, not
|
|
38
|
-
* canonical events: they skip the outbox (`signalWork`, `signalCancel`,
|
|
39
|
-
* `signalSessionsReset`). A commit that called `t.signalWork()` is signalled
|
|
40
|
-
* by the relay after its events.
|
|
41
|
-
*/
|
|
42
|
-
import { decodeCursor } from "../store/cursor.js";
|
|
43
|
-
import { CONTROL_STREAM, WORK_AVAILABLE, WORK_STREAM, streamOfSession, } from "./types.js";
|
|
44
|
-
/** A session's outbox starts past the end of its stream: events are missing from the stream. */
|
|
45
|
-
export class StreamGapError extends Error {
|
|
46
|
-
constructor(sessionId, first, tail) {
|
|
47
|
-
super(`Outbox of session ${sessionId} starts at ${first}, past the end of its stream at ${tail}; events before it are missing`);
|
|
48
|
-
this.name = "StreamGapError";
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
/** At most this many records per append (S2 allows 1000). */
|
|
52
|
-
const MAX_BATCH_RECORDS = 500;
|
|
53
|
-
/** At most this many bytes of JSON per append (S2 allows 1 MiB metered). */
|
|
54
|
-
const MAX_BATCH_BYTES = 768 * 1024;
|
|
55
|
-
/** Outbox rows read per page while relaying one session. */
|
|
56
|
-
const DRAIN_PAGE = 1000;
|
|
57
|
-
export function createRelay(options) {
|
|
58
|
-
const { store, streams, tenantId } = options;
|
|
59
|
-
if (store.tenantId !== tenantId)
|
|
60
|
-
throw new Error(`Relay tenant ${tenantId} does not match store tenant ${store.tenantId}`);
|
|
61
|
-
const onError = options.onError ?? (() => { });
|
|
62
|
-
/** sessionId → tail of that session's relay chain. */
|
|
63
|
-
const chains = new Map();
|
|
64
|
-
const pending = new Set();
|
|
65
|
-
let closed = false;
|
|
66
|
-
/** Runs `task` after every earlier task of the same session. */
|
|
67
|
-
function serialize(sessionId, task) {
|
|
68
|
-
const previous = chains.get(sessionId) ?? Promise.resolve();
|
|
69
|
-
const run = previous.then(task, task);
|
|
70
|
-
const settled = run.then(() => undefined, () => undefined);
|
|
71
|
-
chains.set(sessionId, settled);
|
|
72
|
-
pending.add(settled);
|
|
73
|
-
void settled.then(() => {
|
|
74
|
-
pending.delete(settled);
|
|
75
|
-
if (chains.get(sessionId) === settled)
|
|
76
|
-
chains.delete(sessionId);
|
|
77
|
-
});
|
|
78
|
-
return run;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Appends `rows` (one session, ascending, contiguous) to the stream of `incarnation`
|
|
82
|
-
* with `matchSeq`, and deletes what S2 has while the session still has that incarnation.
|
|
83
|
-
* Throws a `StreamGapError` when the stream's tail is before the first row still to append.
|
|
84
|
-
*/
|
|
85
|
-
async function appendRows(sessionId, incarnation, rows) {
|
|
86
|
-
const stream = streamOfSession({
|
|
87
|
-
id: sessionId,
|
|
88
|
-
...(incarnation === null ? {} : { streamIncarnation: incarnation }),
|
|
89
|
-
});
|
|
90
|
-
const deleteThrough = (seq) => store.tx((t) => t.deleteOutbox(sessionId, seq, incarnation));
|
|
91
|
-
let rest = rows;
|
|
92
|
-
while (rest.length > 0) {
|
|
93
|
-
const batch = takeBatch(rest);
|
|
94
|
-
const first = batch[0].seq;
|
|
95
|
-
const last = batch[batch.length - 1].seq;
|
|
96
|
-
const result = await streams.append(tenantId, stream, batch.map((row) => row.event), { matchSeq: first });
|
|
97
|
-
if (result.status === "ok") {
|
|
98
|
-
await deleteThrough(last);
|
|
99
|
-
rest = rest.slice(batch.length);
|
|
100
|
-
}
|
|
101
|
-
else if (result.tail > first) {
|
|
102
|
-
// Already appended by an earlier attempt: S2 has everything below the tail.
|
|
103
|
-
await deleteThrough(result.tail - 1);
|
|
104
|
-
rest = rest.filter((row) => row.seq >= result.tail);
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
throw new StreamGapError(sessionId, first, result.tail);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Appends the session's outbox rows in sequence order, at most `limit`, reading them with
|
|
113
|
-
* the session's incarnation under its lock. Resolves with the number relayed.
|
|
114
|
-
*/
|
|
115
|
-
async function relaySession(sessionId, limit = Infinity) {
|
|
116
|
-
let relayed = 0;
|
|
117
|
-
while (relayed < limit) {
|
|
118
|
-
const page = Math.min(DRAIN_PAGE, limit - relayed);
|
|
119
|
-
const read = await store.tx(async (t) => {
|
|
120
|
-
const session = await t.lockSession(sessionId);
|
|
121
|
-
// A deleted session took its outbox rows with it.
|
|
122
|
-
if (!session)
|
|
123
|
-
return undefined;
|
|
124
|
-
return {
|
|
125
|
-
incarnation: session.streamIncarnation ?? null,
|
|
126
|
-
rows: await t.outbox(page, { sessionId }),
|
|
127
|
-
};
|
|
128
|
-
});
|
|
129
|
-
if (!read || read.rows.length === 0)
|
|
130
|
-
return relayed;
|
|
131
|
-
const run = contiguous(read.rows);
|
|
132
|
-
await appendRows(sessionId, read.incarnation, run);
|
|
133
|
-
relayed += run.length;
|
|
134
|
-
// A gap inside the outbox: the next page starts at it and reports it.
|
|
135
|
-
if (read.rows.length < page && run.length === read.rows.length)
|
|
136
|
-
return relayed;
|
|
137
|
-
}
|
|
138
|
-
return relayed;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Appends one commit's rows of a session to the stream of the incarnation they were
|
|
142
|
-
* written under. When earlier rows are still in the outbox, relays the whole outbox.
|
|
143
|
-
*/
|
|
144
|
-
async function relayCommitted(sessionId, incarnation, rows) {
|
|
145
|
-
try {
|
|
146
|
-
await appendRows(sessionId, incarnation, contiguous(rows));
|
|
147
|
-
}
|
|
148
|
-
catch (error) {
|
|
149
|
-
if (!(error instanceof StreamGapError))
|
|
150
|
-
throw error;
|
|
151
|
-
await relaySession(sessionId);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
function onCommit(commit) {
|
|
155
|
-
if (closed)
|
|
156
|
-
return;
|
|
157
|
-
/** `sessionId\0incarnation` → the commit's rows of that incarnation. */
|
|
158
|
-
const groups = new Map();
|
|
159
|
-
commit.events.forEach((event, i) => {
|
|
160
|
-
let seq;
|
|
161
|
-
try {
|
|
162
|
-
seq = decodeCursor(event.sessionId, event.cursor);
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
onError(new Error(`Event ${event.eventId} has an invalid cursor; left for the sweep`));
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
const incarnation = commit.incarnations[i] ?? null;
|
|
169
|
-
const key = `${event.sessionId}\u0000${incarnation ?? ""}`;
|
|
170
|
-
let group = groups.get(key);
|
|
171
|
-
if (!group)
|
|
172
|
-
groups.set(key, (group = { sessionId: event.sessionId, incarnation, rows: [] }));
|
|
173
|
-
group.rows.push({ sessionId: event.sessionId, seq, event });
|
|
174
|
-
});
|
|
175
|
-
const relays = [...groups.values()].map(({ sessionId, incarnation, rows }) => {
|
|
176
|
-
rows.sort((a, b) => a.seq - b.seq);
|
|
177
|
-
return serialize(sessionId, () => relayCommitted(sessionId, incarnation, rows)).catch(onError);
|
|
178
|
-
});
|
|
179
|
-
if (commit.workAvailable) {
|
|
180
|
-
const signalled = Promise.all(relays)
|
|
181
|
-
.then(() => signalWork(streams, tenantId))
|
|
182
|
-
.catch(onError);
|
|
183
|
-
pending.add(signalled);
|
|
184
|
-
void signalled.then(() => pending.delete(signalled));
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
const unsubscribe = store.onCommit(onCommit);
|
|
188
|
-
async function idle() {
|
|
189
|
-
while (pending.size > 0)
|
|
190
|
-
await Promise.all([...pending]);
|
|
191
|
-
}
|
|
192
|
-
return {
|
|
193
|
-
async drain(limit = 1000) {
|
|
194
|
-
const rows = await store.tx((t) => t.outbox(limit));
|
|
195
|
-
const bySession = new Map();
|
|
196
|
-
for (const row of rows)
|
|
197
|
-
bySession.set(row.sessionId, (bySession.get(row.sessionId) ?? 0) + 1);
|
|
198
|
-
const counts = await Promise.all([...bySession].map(([sessionId, count]) =>
|
|
199
|
-
// Rows may have been relayed since they were read; the conditional
|
|
200
|
-
// appends skip those.
|
|
201
|
-
serialize(sessionId, () => relaySession(sessionId, count)).catch((error) => {
|
|
202
|
-
onError(error);
|
|
203
|
-
return 0;
|
|
204
|
-
})));
|
|
205
|
-
return counts.reduce((sum, n) => sum + n, 0);
|
|
206
|
-
},
|
|
207
|
-
idle,
|
|
208
|
-
async close() {
|
|
209
|
-
closed = true;
|
|
210
|
-
unsubscribe();
|
|
211
|
-
await idle();
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
/** Appends a `work_available` signal to `tenant/work`. Not relayed through the outbox. */
|
|
216
|
-
export async function signalWork(streams, tenantId) {
|
|
217
|
-
await streams.append(tenantId, WORK_STREAM, [WORK_AVAILABLE]);
|
|
218
|
-
}
|
|
219
|
-
/** Appends a `session.cancel` signal to `tenant/control`. Not relayed through the outbox. */
|
|
220
|
-
export async function signalCancel(streams, tenantId, sessionId) {
|
|
221
|
-
if (!sessionId)
|
|
222
|
-
throw new Error("sessionId is required");
|
|
223
|
-
const signal = { type: "session.cancel", sessionId };
|
|
224
|
-
await streams.append(tenantId, CONTROL_STREAM, [signal]);
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Appends a `sessions.reset` signal to `tenant/control`: every process with the Tenant open
|
|
228
|
-
* checks its session feeds against the Session Store.
|
|
229
|
-
*/
|
|
230
|
-
export async function signalSessionsReset(streams, tenantId) {
|
|
231
|
-
const signal = { type: "sessions.reset" };
|
|
232
|
-
await streams.append(tenantId, CONTROL_STREAM, [signal]);
|
|
233
|
-
}
|
|
234
|
-
// ---------------------------------------------------------------------------
|
|
235
|
-
/** The leading run of `rows` (ascending) without a sequence gap. */
|
|
236
|
-
function contiguous(rows) {
|
|
237
|
-
let n = 1;
|
|
238
|
-
while (n < rows.length && rows[n].seq === rows[n - 1].seq + 1)
|
|
239
|
-
n += 1;
|
|
240
|
-
return rows.slice(0, n);
|
|
241
|
-
}
|
|
242
|
-
/** The leading rows that fit one append. Always at least one row. */
|
|
243
|
-
function takeBatch(rows) {
|
|
244
|
-
let bytes = 0;
|
|
245
|
-
let n = 0;
|
|
246
|
-
while (n < rows.length && n < MAX_BATCH_RECORDS) {
|
|
247
|
-
const size = JSON.stringify(rows[n].event).length + 8;
|
|
248
|
-
if (n > 0 && bytes + size > MAX_BATCH_BYTES)
|
|
249
|
-
break;
|
|
250
|
-
bytes += size;
|
|
251
|
-
n += 1;
|
|
252
|
-
}
|
|
253
|
-
return rows.slice(0, n);
|
|
254
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { type TenantEnvelope } from "@nylorun/core/contracts";
|
|
2
|
-
export declare function parseEnvelope(raw: unknown, expectedId?: string): TenantEnvelope;
|
|
3
|
-
export declare function envelopeNow(input: {
|
|
4
|
-
id: string;
|
|
5
|
-
name: string;
|
|
6
|
-
schemaVersion: number;
|
|
7
|
-
}, now?: Date): TenantEnvelope;
|
package/dist/tenant/envelope.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { TenantEnvelopeSchema, } from "@nylorun/core/contracts";
|
|
2
|
-
import { quarantine } from "./quarantine.js";
|
|
3
|
-
export function parseEnvelope(raw, expectedId) {
|
|
4
|
-
const parsed = TenantEnvelopeSchema.safeParse(raw);
|
|
5
|
-
if (!parsed.success) {
|
|
6
|
-
throw quarantine("envelope-invalid", "the Tenant envelope is missing or invalid", expectedId ? { tenantId: expectedId } : {});
|
|
7
|
-
}
|
|
8
|
-
if (expectedId !== undefined && parsed.data.id !== expectedId) {
|
|
9
|
-
throw quarantine("envelope-invalid", `the Tenant envelope id ${parsed.data.id} does not match ${expectedId}`, { tenantId: expectedId });
|
|
10
|
-
}
|
|
11
|
-
return parsed.data;
|
|
12
|
-
}
|
|
13
|
-
export function envelopeNow(input, now = new Date()) {
|
|
14
|
-
const iso = now.toISOString();
|
|
15
|
-
return {
|
|
16
|
-
id: input.id,
|
|
17
|
-
name: input.name,
|
|
18
|
-
createdAt: iso,
|
|
19
|
-
updatedAt: iso,
|
|
20
|
-
schemaVersion: input.schemaVersion,
|
|
21
|
-
};
|
|
22
|
-
}
|
package/dist/tenant/live.d.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Live delivery over Durable Streams (architecture §12.4, "Reading"): session history
|
|
3
|
-
* (`GET /v1/sessions/:id/items`), session SSE (`GET /v1/sessions/:id/events`) and executor
|
|
4
|
-
* work streams (`GET /v1/executors/connect`).
|
|
5
|
-
*
|
|
6
|
-
* - **Streams.** A session's events are in the stream of its current incarnation
|
|
7
|
-
* (`streamOfSession`, `sessions/<id>/<incarnation>`), resolved from the session on every
|
|
8
|
-
* request; cursors carry only the session id and sequence.
|
|
9
|
-
* - **History** reads the session's stream from the cursor up to the tail seen when the read
|
|
10
|
-
* starts. The `agent` filter runs here, and the response cursor is the last record read. If
|
|
11
|
-
* the streams fail, history answers `503`.
|
|
12
|
-
* - **SSE** shares one stream read per observed session in this process (a `SessionFeed`)
|
|
13
|
-
* among that session's observers. Each observer keeps the next sequence it needs and skips
|
|
14
|
-
* what it already has, so a client resuming from `Last-Event-ID` sees no gap and no
|
|
15
|
-
* duplicate. An observer behind the feed restarts the shared read from its own position.
|
|
16
|
-
* A feed follows one incarnation: it ends once its session is gone or has a new
|
|
17
|
-
* incarnation (a reset, possibly on another node), checked when a client joins, when its
|
|
18
|
-
* read fails, on a `sessions.reset` signal, and periodically (`checkFeeds`).
|
|
19
|
-
* - **Executor streams** receive `work_available` from the `tenant/work` reader that
|
|
20
|
-
* `tenant/streams.ts` runs (one per Tenant per process); discovery stays `GET /v1/actions`.
|
|
21
|
-
*
|
|
22
|
-
* Business code never writes here: events reach streams only through the relay, after commit.
|
|
23
|
-
*/
|
|
24
|
-
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
25
|
-
import type { LiveEvent } from "@nylorun/core/contracts";
|
|
26
|
-
import type { TenantContext } from "./context.js";
|
|
27
|
-
import type { StreamsWiring } from "./streams.js";
|
|
28
|
-
/** One SSE client of a session: the next sequence it needs. */
|
|
29
|
-
interface Observer {
|
|
30
|
-
readonly response: ServerResponse;
|
|
31
|
-
next: number;
|
|
32
|
-
}
|
|
33
|
-
/** The shared stream read of one observed session. */
|
|
34
|
-
interface SessionFeed {
|
|
35
|
-
readonly sessionId: string;
|
|
36
|
-
/** The stream of the session's incarnation when the feed started. */
|
|
37
|
-
readonly stream: string;
|
|
38
|
-
/** The sequence the shared read yields next. Every observer's `next` is at or past it. */
|
|
39
|
-
next: number;
|
|
40
|
-
readonly observers: Set<Observer>;
|
|
41
|
-
/** Aborts the current read; replaced when the read restarts from an earlier sequence. */
|
|
42
|
-
read: AbortController;
|
|
43
|
-
}
|
|
44
|
-
export interface LiveHub {
|
|
45
|
-
/** Set once by `wireStreams`. */
|
|
46
|
-
wiring: StreamsWiring | undefined;
|
|
47
|
-
/** Session id → its shared read and observers. */
|
|
48
|
-
readonly feeds: Map<string, SessionFeed>;
|
|
49
|
-
readonly executorStreams: Map<string, Set<ServerResponse>>;
|
|
50
|
-
}
|
|
51
|
-
export declare function createLiveHub(): LiveHub;
|
|
52
|
-
export declare function send(response: ServerResponse, data: string): void;
|
|
53
|
-
/** Start an SSE response with keepalives; `onClose` runs once the client goes away. */
|
|
54
|
-
export declare function openSse(request: IncomingMessage, response: ServerResponse, onClose: () => void): void;
|
|
55
|
-
/** The resume cursor: `?cursor=` wins over `Last-Event-ID`. */
|
|
56
|
-
export declare function requestCursor(request: IncomingMessage, url: URL): string | undefined;
|
|
57
|
-
/**
|
|
58
|
-
* `GET /v1/sessions/:id/items`: the session stream after the cursor, up to its tail when the
|
|
59
|
-
* read starts. The cursor returned is the last record read (the request's cursor when none
|
|
60
|
-
* was), whatever the `agent` filter kept.
|
|
61
|
-
*/
|
|
62
|
-
export declare function readHistory(ctx: TenantContext, sessionId: string, cursor: string | undefined, agent: string | undefined): Promise<{
|
|
63
|
-
items: LiveEvent[];
|
|
64
|
-
cursor: string | null;
|
|
65
|
-
}>;
|
|
66
|
-
/**
|
|
67
|
-
* `GET /v1/sessions/:id/events`: joins the session's shared read at the client's cursor and
|
|
68
|
-
* follows it until the client leaves, the session is reset or deleted, or the Tenant closes.
|
|
69
|
-
*/
|
|
70
|
-
export declare function streamSessionEvents(ctx: TenantContext, request: IncomingMessage, response: ServerResponse, sessionId: string, cursor: string | undefined): Promise<void>;
|
|
71
|
-
/**
|
|
72
|
-
* Ends every feed in this process whose session is gone or has a new incarnation. Run on a
|
|
73
|
-
* `sessions.reset` signal and periodically, so feeds on nodes other than the one that reset
|
|
74
|
-
* the Tenant end too.
|
|
75
|
-
*/
|
|
76
|
-
export declare function checkFeeds(ctx: TenantContext): Promise<void>;
|
|
77
|
-
/** `GET /v1/executors/connect`: an executor's work stream, primed with one `work_available`. */
|
|
78
|
-
export declare function streamExecutorWork(hub: LiveHub, request: IncomingMessage, response: ServerResponse, tokenHash: string): void;
|
|
79
|
-
/** Called by the `tenant/work` reader for each signal: wake every connected executor. */
|
|
80
|
-
export declare function announceWork(hub: LiveHub): void;
|
|
81
|
-
export declare function executorConnected(hub: LiveHub, tokenHash: string): boolean;
|
|
82
|
-
export declare function connectedExecutorCount(hub: LiveHub): number;
|
|
83
|
-
/** End the streams a token owns (rotation or deregistration). */
|
|
84
|
-
export declare function endExecutorStreams(hub: LiveHub, tokenHash: string): void;
|
|
85
|
-
/** Reset or close: end every session observer and stop every shared read. */
|
|
86
|
-
export declare function clearObservers(hub: LiveHub): void;
|
|
87
|
-
/** Reset: end and forget every executor stream. */
|
|
88
|
-
export declare function clearExecutorStreams(hub: LiveHub): void;
|
|
89
|
-
/** Close: end every executor stream, then every session observer. */
|
|
90
|
-
export declare function endAllStreams(hub: LiveHub): void;
|
|
91
|
-
export declare function sleep(ms: number, signal: AbortSignal): Promise<void>;
|
|
92
|
-
export {};
|