@nylorun/runtime 0.14.0-beta → 0.15.1-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 +115 -0
- package/README.md +82 -40
- package/dist/adapters/execution/restate.js +2 -2
- package/dist/adapters/replication/pgoutput.d.ts +5 -0
- package/dist/adapters/replication/pgoutput.js +4 -3
- package/dist/admin-openapi.json +185 -686
- package/dist/api/components.d.ts +308 -68
- package/dist/api/components.js +4 -5
- 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/define.js +0 -8
- package/dist/api/http/routes/access.js +25 -18
- package/dist/api/http/routes/tenant.d.ts +2 -1
- package/dist/api/http/routes/tenant.js +77 -5
- package/dist/api/http/routes/vaults.js +2 -2
- package/dist/api/openapi.js +6 -7
- 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 +7 -6
- package/dist/host/admin-api.js +9 -113
- package/dist/host/app.d.ts +1 -1
- package/dist/host/app.js +20 -24
- package/dist/host/config-for.d.ts +1 -1
- package/dist/host/config-for.js +2 -2
- package/dist/host/create-host.d.ts +5 -5
- package/dist/host/create-host.js +5 -15
- 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 +6 -1
- package/dist/host/http.js +17 -0
- package/dist/host/index.d.ts +1 -1
- package/dist/host/main.js +151 -71
- package/dist/host/stack-config.d.ts +71 -19
- package/dist/host/stack-config.js +192 -18
- 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 +21 -3
- package/dist/mcp/pool.js +15 -2
- package/dist/openapi.json +888 -695
- package/dist/record/append.d.ts +51 -0
- package/dist/record/append.js +20 -0
- package/dist/{store/event.js → record/envelope.js} +1 -1
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +11 -0
- package/dist/store/postgres/connect.d.ts +15 -12
- package/dist/store/postgres/connect.js +24 -12
- 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 +2 -8
- package/dist/store/postgres/record.js +50 -54
- 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 +481 -567
- package/dist/store/postgres/tenant.d.ts +56 -0
- package/dist/store/postgres/tenant.js +154 -0
- package/dist/store/types.d.ts +43 -87
- package/dist/store/types.js +0 -1
- package/dist/streams/control.d.ts +1 -1
- package/dist/streams/control.js +6 -2
- package/dist/streams/relay/types.d.ts +2 -1
- package/dist/streams/relay/types.js +2 -1
- package/dist/streams/types.d.ts +5 -0
- package/dist/tenant/advance.d.ts +7 -2
- package/dist/tenant/advance.js +95 -16
- package/dist/tenant/cause.d.ts +25 -0
- package/dist/tenant/cause.js +45 -0
- package/dist/tenant/commands.js +4 -2
- package/dist/tenant/context.d.ts +26 -3
- package/dist/tenant/delivery-token.js +17 -18
- package/dist/tenant/delivery.d.ts +1 -1
- package/dist/tenant/delivery.js +18 -13
- package/dist/tenant/effects.d.ts +15 -1
- package/dist/tenant/effects.js +93 -17
- package/dist/tenant/ephemeral.d.ts +16 -7
- package/dist/tenant/ephemeral.js +45 -111
- package/dist/tenant/history.d.ts +64 -0
- package/dist/tenant/history.js +120 -0
- package/dist/tenant/model-setting.d.ts +2 -2
- package/dist/tenant/module.d.ts +14 -28
- package/dist/tenant/module.js +131 -271
- package/dist/tenant/paths.d.ts +11 -16
- package/dist/tenant/paths.js +24 -68
- package/dist/tenant/pool.d.ts +1 -1
- package/dist/tenant/pool.js +1 -1
- package/dist/tenant/principals.d.ts +23 -11
- package/dist/tenant/principals.js +61 -49
- package/dist/tenant/runtime.d.ts +19 -1
- package/dist/tenant/runtime.js +40 -26
- package/dist/tenant/scheduler.d.ts +4 -2
- package/dist/tenant/scheduler.js +4 -4
- package/dist/tenant/session-streams.d.ts +2 -0
- package/dist/tenant/session-streams.js +8 -3
- package/dist/tenant/status.d.ts +3 -0
- package/dist/tenant/status.js +1 -1
- package/dist/tenant/store-pg.d.ts +9 -6
- package/dist/tenant/store-pg.js +60 -125
- package/dist/tenant/streams.d.ts +9 -25
- package/dist/tenant/streams.js +7 -22
- package/dist/tenant/tokens.js +16 -14
- package/dist/tenant/types.d.ts +41 -92
- package/dist/tenant/types.js +4 -15
- package/dist/tenant/worker.js +4 -4
- package/dist/vault/host-model.d.ts +31 -0
- package/dist/vault/host-model.js +57 -0
- package/dist/vault/service.d.ts +26 -15
- package/dist/vault/service.js +70 -109
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -5
- package/dist/store/memory.d.ts +0 -69
- package/dist/store/memory.js +0 -810
- 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/003_subject_tokens.d.ts +0 -8
- package/dist/store/postgres/migrations/003_subject_tokens.js +0 -42
- package/dist/store/postgres/migrations/004_publishable_keys.d.ts +0 -6
- package/dist/store/postgres/migrations/004_publishable_keys.js +0 -18
- package/dist/store/postgres/migrations/005_action_endpoints.d.ts +0 -6
- package/dist/store/postgres/migrations/005_action_endpoints.js +0 -32
- package/dist/store/postgres/migrations/006_drop_executors.d.ts +0 -11
- package/dist/store/postgres/migrations/006_drop_executors.js +0 -32
- package/dist/store/postgres/migrations/007_event_time.d.ts +0 -7
- package/dist/store/postgres/migrations/007_event_time.js +0 -14
- package/dist/store/postgres/migrations/008_durable_streams.d.ts +0 -13
- package/dist/store/postgres/migrations/008_durable_streams.js +0 -35
- package/dist/store/postgres/migrations/index.d.ts +0 -53
- package/dist/store/postgres/migrations/index.js +0 -96
- package/dist/store/postgres/migrations/shared/index.d.ts +0 -31
- package/dist/store/postgres/migrations/shared/index.js +0 -92
- 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 -228
- package/dist/streams/relay/memory.d.ts +0 -69
- package/dist/streams/relay/memory.js +0 -176
- package/dist/tenant/envelope.d.ts +0 -7
- package/dist/tenant/envelope.js +0 -22
- 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/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
- /package/dist/{store/event.d.ts → record/envelope.d.ts} +0 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The migrations of a Tenant database (session-store.md §3) and the runner that applies them.
|
|
3
|
+
*
|
|
4
|
+
* drizzle-kit generates the migrations from `schema.ts` into `drizzle/` (SQL files and
|
|
5
|
+
* `meta/_journal.json`); custom SQL migrations carry what it does not model (the schemas,
|
|
6
|
+
* `nylorun.doc()`, the relay's publication). The build copies `drizzle/` next to this module
|
|
7
|
+
* in `dist/`, and the Runtime reads it from there.
|
|
8
|
+
*
|
|
9
|
+
* The runner applies them at Host startup, before any service opens the store
|
|
10
|
+
* (`tenant.ts`), in one transaction:
|
|
11
|
+
*
|
|
12
|
+
* 1. It lifts `statement_timeout` for the transaction and takes the database's migration
|
|
13
|
+
* lock (a transaction-level advisory lock), so two processes never migrate at once.
|
|
14
|
+
* 2. It refuses an old layout (`database-layout-old`): `tenant_<id>` schemas, a record keyed
|
|
15
|
+
* by Tenant, or the hand-written migrations' `schema_version` tables of the pre-release
|
|
16
|
+
* builds of one Tenant per database. It never changes such a database.
|
|
17
|
+
* 3. It reads the journal, `nylorun.__drizzle_migrations` (Drizzle's format: `id`, `hash`,
|
|
18
|
+
* `created_at`, so `drizzle-kit migrate` and Drizzle Studio read it too). A journal holding
|
|
19
|
+
* a migration this Runtime does not ship was written by a newer Runtime: `schema-too-new`,
|
|
20
|
+
* and nothing is changed. Drizzle's own migrator does not refuse; this is why the Runtime
|
|
21
|
+
* applies the files itself.
|
|
22
|
+
* 4. It applies every missing migration in order and records each in the journal.
|
|
23
|
+
*
|
|
24
|
+
* The database's schema version is the number of migrations its journal records; this
|
|
25
|
+
* Runtime expects `shippedMigrations().length`.
|
|
26
|
+
*/
|
|
27
|
+
import { readFileSync } from "node:fs";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { sql } from "drizzle-orm";
|
|
30
|
+
import { readMigrationFiles } from "drizzle-orm/migrator";
|
|
31
|
+
import { openError } from "../../tenant/cause.js";
|
|
32
|
+
import { database, driverError } from "./db.js";
|
|
33
|
+
import { STREAMS_SCHEMA, TENANT_SCHEMA } from "./schema.js";
|
|
34
|
+
/** Where the migrations are, next to this module (`src/` in development, `dist/` built). */
|
|
35
|
+
export const MIGRATIONS_FOLDER = fileURLToPath(new URL("./drizzle", import.meta.url));
|
|
36
|
+
/** The journal of applied migrations: `nylorun.__drizzle_migrations`. */
|
|
37
|
+
export const MIGRATIONS_SCHEMA = TENANT_SCHEMA;
|
|
38
|
+
export const MIGRATIONS_TABLE = "__drizzle_migrations";
|
|
39
|
+
const JOURNAL = sql.raw(`"${MIGRATIONS_SCHEMA}"."${MIGRATIONS_TABLE}"`);
|
|
40
|
+
/**
|
|
41
|
+
* What schemas were called when one database held several Tenants (`"tenant_<id>"`). A
|
|
42
|
+
* database that still has one was written by an older Runtime and is refused.
|
|
43
|
+
*/
|
|
44
|
+
export const OLD_TENANT_SCHEMA_PREFIX = "tenant_";
|
|
45
|
+
let shipped;
|
|
46
|
+
/** The migrations this Runtime ships, in order. */
|
|
47
|
+
export function shippedMigrations() {
|
|
48
|
+
if (!shipped) {
|
|
49
|
+
const journal = JSON.parse(readFileSync(`${MIGRATIONS_FOLDER}/meta/_journal.json`, "utf8"));
|
|
50
|
+
shipped = readMigrationFiles({ migrationsFolder: MIGRATIONS_FOLDER }).map((file, index) => ({
|
|
51
|
+
tag: journal.entries[index].tag,
|
|
52
|
+
hash: file.hash,
|
|
53
|
+
when: file.folderMillis,
|
|
54
|
+
statements: file.sql,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
return shipped;
|
|
58
|
+
}
|
|
59
|
+
/** The schema version this Runtime writes and expects: the number of shipped migrations. */
|
|
60
|
+
export function expectedSchemaVersion() {
|
|
61
|
+
return shippedMigrations().length;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Lifts `statement_timeout` for the transaction and takes the database's migration lock
|
|
65
|
+
* until it ends. Waiting for the lock, and the migrations done under it, may outlast the
|
|
66
|
+
* pool's statement timeout; a migration that timed out would fail the Host's readiness.
|
|
67
|
+
*/
|
|
68
|
+
export async function lockMigrations(tx) {
|
|
69
|
+
await tx.execute(sql `SET LOCAL statement_timeout = 0`);
|
|
70
|
+
await tx.execute(sql `SELECT pg_advisory_xact_lock(hashtextextended('nylorun.migrations', 0))`);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Throws `database-layout-old` when the database was written by an older Runtime: one that
|
|
74
|
+
* kept several Tenants in one database (`tenant_<id>` schemas, a record keyed by Tenant), or a
|
|
75
|
+
* pre-release build of one Tenant per database whose hand-written migrations recorded
|
|
76
|
+
* themselves in `schema_version` tables.
|
|
77
|
+
*/
|
|
78
|
+
export async function assertCurrentLayout(q) {
|
|
79
|
+
const [row] = await q.execute(sql `
|
|
80
|
+
SELECT
|
|
81
|
+
EXISTS (
|
|
82
|
+
SELECT 1 FROM pg_namespace WHERE starts_with(nspname, ${OLD_TENANT_SCHEMA_PREFIX})
|
|
83
|
+
) AS tenant_schemas,
|
|
84
|
+
EXISTS (
|
|
85
|
+
SELECT 1 FROM information_schema.columns
|
|
86
|
+
WHERE table_schema = ${STREAMS_SCHEMA} AND table_name = 'session_events'
|
|
87
|
+
AND column_name = 'tenant_id'
|
|
88
|
+
) AS keyed_record,
|
|
89
|
+
to_regclass(${`${TENANT_SCHEMA}.schema_version`}) IS NOT NULL
|
|
90
|
+
OR to_regclass(${`${STREAMS_SCHEMA}.schema_version`}) IS NOT NULL AS versioned`);
|
|
91
|
+
if (row?.tenant_schemas || row?.keyed_record)
|
|
92
|
+
throw openError("database-layout-old", "The database was written by an older Runtime that kept several Tenants in one database " +
|
|
93
|
+
"(tenant_<id> schemas). This release starts fresh with one Tenant per database and " +
|
|
94
|
+
"never changes the old one: point the Runtime at a new database (locally, a new Tenant: " +
|
|
95
|
+
"`nylorun start --tenant <new name>`).");
|
|
96
|
+
if (row?.versioned)
|
|
97
|
+
throw openError("database-layout-old", "The database was created by a pre-release build of one Tenant per database, whose " +
|
|
98
|
+
"migrations this release replaces. It is never changed: point the Runtime at a new " +
|
|
99
|
+
"database (locally, `nylorun reset` and then `nylorun start`).");
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The database's schema version: the number of migrations its journal records, or undefined
|
|
103
|
+
* when it has no journal (a database never migrated).
|
|
104
|
+
*/
|
|
105
|
+
export async function readSchemaVersion(q) {
|
|
106
|
+
const [exists] = await q.execute(sql `
|
|
107
|
+
SELECT to_regclass(${`${MIGRATIONS_SCHEMA}.${MIGRATIONS_TABLE}`}) IS NOT NULL AS journal`);
|
|
108
|
+
if (!exists?.journal)
|
|
109
|
+
return undefined;
|
|
110
|
+
const [row] = await q.execute(sql `SELECT count(*)::int AS n FROM ${JOURNAL}`);
|
|
111
|
+
return row?.n ?? 0;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Applies every migration of `migrations` the journal does not record, in `tx`. Take
|
|
115
|
+
* `lockMigrations` first (`migrateDatabase` does). Throws `schema-too-new` when the journal
|
|
116
|
+
* records a migration `migrations` does not have, and `migration-failed` when Postgres
|
|
117
|
+
* rejects a migration.
|
|
118
|
+
*/
|
|
119
|
+
export async function applyMigrations(tx, migrations = shippedMigrations()) {
|
|
120
|
+
await tx.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.raw(`"${MIGRATIONS_SCHEMA}"`)}`);
|
|
121
|
+
// Drizzle's journal table, as its migrator creates it.
|
|
122
|
+
await tx.execute(sql `
|
|
123
|
+
CREATE TABLE IF NOT EXISTS ${JOURNAL} (
|
|
124
|
+
id SERIAL PRIMARY KEY,
|
|
125
|
+
hash text NOT NULL,
|
|
126
|
+
created_at bigint
|
|
127
|
+
)`);
|
|
128
|
+
const applied = await tx.execute(sql `SELECT hash FROM ${JOURNAL} ORDER BY id`);
|
|
129
|
+
const known = new Set(migrations.map((migration) => migration.hash));
|
|
130
|
+
const unknown = applied.filter((row) => !known.has(row.hash)).length;
|
|
131
|
+
if (unknown > 0)
|
|
132
|
+
throw openError("schema-too-new", `The database has ${unknown} migration${unknown === 1 ? "" : "s"} this Runtime does not ` +
|
|
133
|
+
`know (it is at schema version ${applied.length}; this Runtime's is ${migrations.length}): ` +
|
|
134
|
+
"it was migrated by a newer Runtime");
|
|
135
|
+
applied.forEach((row, index) => {
|
|
136
|
+
if (row.hash !== migrations[index].hash)
|
|
137
|
+
throw openError("migration-failed", `The database's migration ${index + 1} is not this Runtime's ${migrations[index].tag}`);
|
|
138
|
+
});
|
|
139
|
+
for (const migration of migrations.slice(applied.length)) {
|
|
140
|
+
try {
|
|
141
|
+
for (const statement of migration.statements)
|
|
142
|
+
if (statement.trim())
|
|
143
|
+
await tx.execute(sql.raw(statement));
|
|
144
|
+
}
|
|
145
|
+
catch (thrown) {
|
|
146
|
+
const error = driverError(thrown);
|
|
147
|
+
if (!isStatementError(error))
|
|
148
|
+
throw error;
|
|
149
|
+
throw openError("migration-failed", `Migration ${migration.tag} failed: ${error.message}`);
|
|
150
|
+
}
|
|
151
|
+
await tx.execute(sql `
|
|
152
|
+
INSERT INTO ${JOURNAL} (hash, created_at) VALUES (${migration.hash}, ${migration.when})`);
|
|
153
|
+
}
|
|
154
|
+
return { from: applied.length, to: migrations.length };
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Refuses an old layout and applies the missing migrations, in one transaction under the
|
|
158
|
+
* migration lock. For tests and tools; the Host migrates through `openTenantDatabase`.
|
|
159
|
+
*/
|
|
160
|
+
export async function migrateDatabase(pool, migrations = shippedMigrations()) {
|
|
161
|
+
const db = database(pool);
|
|
162
|
+
try {
|
|
163
|
+
return await db.transaction(async (tx) => {
|
|
164
|
+
await lockMigrations(tx);
|
|
165
|
+
await assertCurrentLayout(tx);
|
|
166
|
+
return applyMigrations(tx, migrations);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
throw driverError(error);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Whether Postgres rejected a statement (a SQLSTATE), rather than the connection or the
|
|
175
|
+
* server failing (classes 08, 53, 57, and the driver's own connection errors). Only the
|
|
176
|
+
* first says something about the Tenant's database.
|
|
177
|
+
*/
|
|
178
|
+
export function isStatementError(error) {
|
|
179
|
+
const code = error?.code;
|
|
180
|
+
return (typeof code === "string" &&
|
|
181
|
+
/^[0-9A-Z]{5}$/.test(code) &&
|
|
182
|
+
!/^(08|53|57)/.test(code));
|
|
183
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record's Postgres statements (Durable Streams §6), which the record module runs through
|
|
3
|
+
* `RecordWriter` (`record/append.ts`, blueprint D27): the only inserts into `session_events`
|
|
4
|
+
* and `session_log_heads`. They sit here, behind the driver boundary (session-store.md §3), and
|
|
5
|
+
* run in the store's transaction under the caller's session row lock.
|
|
6
|
+
*/
|
|
7
|
+
import { sql } from "drizzle-orm";
|
|
8
|
+
import { sessionEvents, sessionLogHeads, tenant } from "./schema.js";
|
|
9
|
+
/** The record writer of transaction `db`. */
|
|
10
|
+
export function postgresRecordWriter(db) {
|
|
11
|
+
return {
|
|
12
|
+
async advance(sessionId) {
|
|
13
|
+
const [row] = await db
|
|
14
|
+
.insert(sessionLogHeads)
|
|
15
|
+
.values({
|
|
16
|
+
sessionId,
|
|
17
|
+
generation: sql `coalesce((SELECT ${tenant.basinGeneration} FROM ${tenant}), 0)`,
|
|
18
|
+
head: 1,
|
|
19
|
+
})
|
|
20
|
+
.onConflictDoUpdate({
|
|
21
|
+
target: sessionLogHeads.sessionId,
|
|
22
|
+
set: { head: sql `${sessionLogHeads.head} + 1` },
|
|
23
|
+
})
|
|
24
|
+
.returning({
|
|
25
|
+
seq: sql `${sessionLogHeads.head} - 1`.mapWith(Number),
|
|
26
|
+
generation: sessionLogHeads.generation,
|
|
27
|
+
});
|
|
28
|
+
return row;
|
|
29
|
+
},
|
|
30
|
+
async insert(row) {
|
|
31
|
+
await db.insert(sessionEvents).values(row);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reads the shared record back for the stream relay (Durable Streams §7.2–7.3): a session's
|
|
3
|
-
* rows to refill a gap in S2, every log head to reconcile after a fresh slot, and a Tenant's
|
|
4
|
-
* current basin generation to tell obsolete rows apart. Reads every Tenant's rows by design.
|
|
5
|
-
*/
|
|
6
1
|
import type { Sql } from "postgres";
|
|
7
2
|
import type { RecordReader } from "../../streams/relay/types.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
tenantId?: string;
|
|
3
|
+
export declare function createPostgresRecordReader(sql: Sql, options: {
|
|
4
|
+
tenantId: string;
|
|
11
5
|
}): RecordReader;
|
|
@@ -1,64 +1,60 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Reads the record back for the stream relay (Durable Streams §7.2–7.3): a session's rows to
|
|
3
|
+
* refill a gap in S2, every log head to reconcile after a fresh slot, and the Tenant's current
|
|
4
|
+
* basin generation to tell obsolete rows apart.
|
|
5
|
+
*
|
|
6
|
+
* The database holds one Tenant, so the record has no Tenant column: the reader is made for
|
|
7
|
+
* that Tenant (`tenantId`), fills `RecordRow.tenantId` and `LogHead.tenantId` with it, and
|
|
8
|
+
* reads nothing for any other id.
|
|
9
|
+
*/
|
|
10
|
+
import { and, asc, eq, gt, gte, lt } from "drizzle-orm";
|
|
11
|
+
import { database, driverError } from "./db.js";
|
|
12
|
+
import { sessionEvents, sessionLogHeads, tenant } from "./schema.js";
|
|
13
|
+
export function createPostgresRecordReader(sql, options) {
|
|
14
|
+
const own = options.tenantId;
|
|
15
|
+
const db = database(sql);
|
|
16
|
+
const read = (query) => Promise.resolve(query).catch((error) => {
|
|
17
|
+
throw driverError(error);
|
|
18
|
+
});
|
|
8
19
|
return {
|
|
9
20
|
async readRange(tenantId, sessionId, from, to) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}));
|
|
21
|
+
if (tenantId !== own)
|
|
22
|
+
return [];
|
|
23
|
+
const rows = await read(db
|
|
24
|
+
.select({
|
|
25
|
+
seq: sessionEvents.seq,
|
|
26
|
+
generation: sessionEvents.generation,
|
|
27
|
+
body: sessionEvents.body,
|
|
28
|
+
})
|
|
29
|
+
.from(sessionEvents)
|
|
30
|
+
.where(and(eq(sessionEvents.sessionId, sessionId), gte(sessionEvents.seq, from), lt(sessionEvents.seq, to)))
|
|
31
|
+
.orderBy(asc(sessionEvents.seq)));
|
|
32
|
+
return rows.map((row) => ({ tenantId, sessionId, ...row }));
|
|
22
33
|
},
|
|
23
34
|
async heads(after, limit) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
// Heads are ordered by (Tenant, session): every one of this Tenant's comes after a
|
|
36
|
+
// Tenant id that sorts before it, and none after one that sorts after it.
|
|
37
|
+
if (after && after.tenantId > own)
|
|
38
|
+
return [];
|
|
39
|
+
const from = after?.tenantId === own ? after.sessionId : undefined;
|
|
40
|
+
const rows = await read(db
|
|
41
|
+
.select({
|
|
42
|
+
sessionId: sessionLogHeads.sessionId,
|
|
43
|
+
generation: sessionLogHeads.generation,
|
|
44
|
+
head: sessionLogHeads.head,
|
|
45
|
+
})
|
|
46
|
+
.from(sessionLogHeads)
|
|
47
|
+
.where(and(gt(sessionLogHeads.head, 0), from === undefined ? undefined : gt(sessionLogHeads.sessionId, from)))
|
|
48
|
+
.orderBy(asc(sessionLogHeads.sessionId))
|
|
49
|
+
.limit(limit));
|
|
50
|
+
return rows.map((row) => ({ tenantId: own, ...row }));
|
|
37
51
|
},
|
|
38
52
|
async generation(tenantId) {
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
schema = tenantSchemaName(tenantId);
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
53
|
+
if (tenantId !== own)
|
|
44
54
|
return undefined;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (!exists?.exists)
|
|
49
|
-
return undefined;
|
|
50
|
-
try {
|
|
51
|
-
const [row] = await sql `
|
|
52
|
-
SELECT basin_generation FROM ${sql(`${schema}.tenant`)}`;
|
|
53
|
-
// A schema without its Tenant row (store tests) is at generation 0.
|
|
54
|
-
return row ? Number(row.basin_generation) : 0;
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
// Deleted between the two reads.
|
|
58
|
-
if (error.code === "42P01")
|
|
59
|
-
return undefined;
|
|
60
|
-
throw error;
|
|
61
|
-
}
|
|
55
|
+
const [row] = await read(db.select({ generation: tenant.basinGeneration }).from(tenant));
|
|
56
|
+
// A database whose Tenant row is not written yet is at generation 0.
|
|
57
|
+
return row?.generation ?? 0;
|
|
62
58
|
},
|
|
63
59
|
};
|
|
64
60
|
}
|