@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.
Files changed (198) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +82 -40
  3. package/dist/adapters/execution/restate.js +2 -2
  4. package/dist/adapters/replication/pgoutput.d.ts +5 -0
  5. package/dist/adapters/replication/pgoutput.js +4 -3
  6. package/dist/admin-openapi.json +185 -686
  7. package/dist/api/components.d.ts +308 -68
  8. package/dist/api/components.js +4 -5
  9. package/dist/api/gate/keys.d.ts +14 -0
  10. package/dist/api/gate/keys.js +59 -0
  11. package/dist/api/gate/routes.d.ts +34 -0
  12. package/dist/api/gate/routes.js +235 -0
  13. package/dist/api/http/define.js +0 -8
  14. package/dist/api/http/routes/access.js +25 -18
  15. package/dist/api/http/routes/tenant.d.ts +2 -1
  16. package/dist/api/http/routes/tenant.js +77 -5
  17. package/dist/api/http/routes/vaults.js +2 -2
  18. package/dist/api/openapi.js +6 -7
  19. package/dist/gates/contract.d.ts +51 -0
  20. package/dist/gates/contract.js +37 -0
  21. package/dist/gates/handler.d.ts +21 -0
  22. package/dist/gates/handler.js +55 -0
  23. package/dist/gates/http-client.d.ts +12 -0
  24. package/dist/gates/http-client.js +160 -0
  25. package/dist/gates/in-process.d.ts +32 -0
  26. package/dist/gates/in-process.js +42 -0
  27. package/dist/gates/inflight.d.ts +39 -0
  28. package/dist/gates/inflight.js +70 -0
  29. package/dist/gates/mcp-handler.d.ts +35 -0
  30. package/dist/gates/mcp-handler.js +142 -0
  31. package/dist/gates/meter.d.ts +24 -0
  32. package/dist/gates/meter.js +165 -0
  33. package/dist/gates/model-gate.d.ts +45 -0
  34. package/dist/gates/model-gate.js +1 -0
  35. package/dist/gates/tenant-vaults.d.ts +64 -0
  36. package/dist/gates/tenant-vaults.js +94 -0
  37. package/dist/gates/tool-calls.d.ts +51 -0
  38. package/dist/gates/tool-calls.js +64 -0
  39. package/dist/gates/tool-client.d.ts +12 -0
  40. package/dist/gates/tool-client.js +201 -0
  41. package/dist/gates/tool-contract.d.ts +116 -0
  42. package/dist/gates/tool-contract.js +64 -0
  43. package/dist/gates/tool-gate.d.ts +45 -0
  44. package/dist/gates/tool-gate.js +10 -0
  45. package/dist/host/admin-api.d.ts +7 -6
  46. package/dist/host/admin-api.js +9 -113
  47. package/dist/host/app.d.ts +1 -1
  48. package/dist/host/app.js +20 -24
  49. package/dist/host/config-for.d.ts +1 -1
  50. package/dist/host/config-for.js +2 -2
  51. package/dist/host/create-host.d.ts +5 -5
  52. package/dist/host/create-host.js +5 -15
  53. package/dist/host/execution.d.ts +20 -18
  54. package/dist/host/execution.js +2 -2
  55. package/dist/host/gates.d.ts +58 -0
  56. package/dist/host/gates.js +127 -0
  57. package/dist/host/http.d.ts +6 -1
  58. package/dist/host/http.js +17 -0
  59. package/dist/host/index.d.ts +1 -1
  60. package/dist/host/main.js +151 -71
  61. package/dist/host/stack-config.d.ts +71 -19
  62. package/dist/host/stack-config.js +192 -18
  63. package/dist/infra/database.d.ts +2 -2
  64. package/dist/infra/execution.d.ts +5 -5
  65. package/dist/infra/execution.js +8 -6
  66. package/dist/infra/index.d.ts +1 -1
  67. package/dist/infra/workers.d.ts +17 -17
  68. package/dist/infra/workers.js +9 -9
  69. package/dist/keys/client.d.ts +9 -0
  70. package/dist/keys/client.js +70 -0
  71. package/dist/keys/contract.d.ts +24 -0
  72. package/dist/keys/contract.js +12 -0
  73. package/dist/keys/keys.d.ts +41 -0
  74. package/dist/keys/keys.js +50 -0
  75. package/dist/mcp/connect.d.ts +43 -3
  76. package/dist/mcp/connect.js +12 -5
  77. package/dist/mcp/pool.d.ts +21 -3
  78. package/dist/mcp/pool.js +15 -2
  79. package/dist/openapi.json +888 -695
  80. package/dist/record/append.d.ts +51 -0
  81. package/dist/record/append.js +20 -0
  82. package/dist/{store/event.js → record/envelope.js} +1 -1
  83. package/dist/record/index.d.ts +11 -0
  84. package/dist/record/index.js +11 -0
  85. package/dist/store/postgres/connect.d.ts +15 -12
  86. package/dist/store/postgres/connect.js +24 -12
  87. package/dist/store/postgres/db.d.ts +16 -0
  88. package/dist/store/postgres/db.js +28 -0
  89. package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
  90. package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
  91. package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
  92. package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
  93. package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
  94. package/dist/store/postgres/migrate.d.ts +68 -0
  95. package/dist/store/postgres/migrate.js +183 -0
  96. package/dist/store/postgres/record-writer.d.ts +4 -0
  97. package/dist/store/postgres/record-writer.js +34 -0
  98. package/dist/store/postgres/record.d.ts +2 -8
  99. package/dist/store/postgres/record.js +50 -54
  100. package/dist/store/postgres/schema.d.ts +3096 -0
  101. package/dist/store/postgres/schema.js +363 -0
  102. package/dist/store/postgres/store.d.ts +2 -4
  103. package/dist/store/postgres/store.js +481 -567
  104. package/dist/store/postgres/tenant.d.ts +56 -0
  105. package/dist/store/postgres/tenant.js +154 -0
  106. package/dist/store/types.d.ts +43 -87
  107. package/dist/store/types.js +0 -1
  108. package/dist/streams/control.d.ts +1 -1
  109. package/dist/streams/control.js +6 -2
  110. package/dist/streams/relay/types.d.ts +2 -1
  111. package/dist/streams/relay/types.js +2 -1
  112. package/dist/streams/types.d.ts +5 -0
  113. package/dist/tenant/advance.d.ts +7 -2
  114. package/dist/tenant/advance.js +95 -16
  115. package/dist/tenant/cause.d.ts +25 -0
  116. package/dist/tenant/cause.js +45 -0
  117. package/dist/tenant/commands.js +4 -2
  118. package/dist/tenant/context.d.ts +26 -3
  119. package/dist/tenant/delivery-token.js +17 -18
  120. package/dist/tenant/delivery.d.ts +1 -1
  121. package/dist/tenant/delivery.js +18 -13
  122. package/dist/tenant/effects.d.ts +15 -1
  123. package/dist/tenant/effects.js +93 -17
  124. package/dist/tenant/ephemeral.d.ts +16 -7
  125. package/dist/tenant/ephemeral.js +45 -111
  126. package/dist/tenant/history.d.ts +64 -0
  127. package/dist/tenant/history.js +120 -0
  128. package/dist/tenant/model-setting.d.ts +2 -2
  129. package/dist/tenant/module.d.ts +14 -28
  130. package/dist/tenant/module.js +131 -271
  131. package/dist/tenant/paths.d.ts +11 -16
  132. package/dist/tenant/paths.js +24 -68
  133. package/dist/tenant/pool.d.ts +1 -1
  134. package/dist/tenant/pool.js +1 -1
  135. package/dist/tenant/principals.d.ts +23 -11
  136. package/dist/tenant/principals.js +61 -49
  137. package/dist/tenant/runtime.d.ts +19 -1
  138. package/dist/tenant/runtime.js +40 -26
  139. package/dist/tenant/scheduler.d.ts +4 -2
  140. package/dist/tenant/scheduler.js +4 -4
  141. package/dist/tenant/session-streams.d.ts +2 -0
  142. package/dist/tenant/session-streams.js +8 -3
  143. package/dist/tenant/status.d.ts +3 -0
  144. package/dist/tenant/status.js +1 -1
  145. package/dist/tenant/store-pg.d.ts +9 -6
  146. package/dist/tenant/store-pg.js +60 -125
  147. package/dist/tenant/streams.d.ts +9 -25
  148. package/dist/tenant/streams.js +7 -22
  149. package/dist/tenant/tokens.js +16 -14
  150. package/dist/tenant/types.d.ts +41 -92
  151. package/dist/tenant/types.js +4 -15
  152. package/dist/tenant/worker.js +4 -4
  153. package/dist/vault/host-model.d.ts +31 -0
  154. package/dist/vault/host-model.js +57 -0
  155. package/dist/vault/service.d.ts +26 -15
  156. package/dist/vault/service.js +70 -109
  157. package/dist/version.d.ts +1 -1
  158. package/dist/version.js +1 -1
  159. package/package.json +9 -5
  160. package/dist/store/memory.d.ts +0 -69
  161. package/dist/store/memory.js +0 -810
  162. package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
  163. package/dist/store/postgres/migrations/001_initial.js +0 -196
  164. package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
  165. package/dist/store/postgres/migrations/002_session_owner.js +0 -13
  166. package/dist/store/postgres/migrations/003_subject_tokens.d.ts +0 -8
  167. package/dist/store/postgres/migrations/003_subject_tokens.js +0 -42
  168. package/dist/store/postgres/migrations/004_publishable_keys.d.ts +0 -6
  169. package/dist/store/postgres/migrations/004_publishable_keys.js +0 -18
  170. package/dist/store/postgres/migrations/005_action_endpoints.d.ts +0 -6
  171. package/dist/store/postgres/migrations/005_action_endpoints.js +0 -32
  172. package/dist/store/postgres/migrations/006_drop_executors.d.ts +0 -11
  173. package/dist/store/postgres/migrations/006_drop_executors.js +0 -32
  174. package/dist/store/postgres/migrations/007_event_time.d.ts +0 -7
  175. package/dist/store/postgres/migrations/007_event_time.js +0 -14
  176. package/dist/store/postgres/migrations/008_durable_streams.d.ts +0 -13
  177. package/dist/store/postgres/migrations/008_durable_streams.js +0 -35
  178. package/dist/store/postgres/migrations/index.d.ts +0 -53
  179. package/dist/store/postgres/migrations/index.js +0 -96
  180. package/dist/store/postgres/migrations/shared/index.d.ts +0 -31
  181. package/dist/store/postgres/migrations/shared/index.js +0 -92
  182. package/dist/store/postgres/names.d.ts +0 -11
  183. package/dist/store/postgres/names.js +0 -42
  184. package/dist/store/postgres/tenants.d.ts +0 -90
  185. package/dist/store/postgres/tenants.js +0 -228
  186. package/dist/streams/relay/memory.d.ts +0 -69
  187. package/dist/streams/relay/memory.js +0 -176
  188. package/dist/tenant/envelope.d.ts +0 -7
  189. package/dist/tenant/envelope.js +0 -22
  190. package/dist/tenant/quarantine-error.d.ts +0 -6
  191. package/dist/tenant/quarantine-error.js +0 -9
  192. package/dist/tenant/quarantine.d.ts +0 -27
  193. package/dist/tenant/quarantine.js +0 -72
  194. package/dist/tenant/store-memory.d.ts +0 -16
  195. package/dist/tenant/store-memory.js +0 -111
  196. /package/dist/{store → record}/cursor.d.ts +0 -0
  197. /package/dist/{store → record}/cursor.js +0 -0
  198. /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,4 @@
1
+ import type { RecordWriter } from "../../record/index.js";
2
+ import type { Transaction } from "./db.js";
3
+ /** The record writer of transaction `db`. */
4
+ export declare function postgresRecordWriter(db: Transaction): RecordWriter;
@@ -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
- /** Every Tenant's record (the stream relay), or one Tenant's (`tenantId`, its Session Store). */
9
- export declare function createPostgresRecordReader(sql: Sql, options?: {
10
- tenantId?: string;
3
+ export declare function createPostgresRecordReader(sql: Sql, options: {
4
+ tenantId: string;
11
5
  }): RecordReader;
@@ -1,64 +1,60 @@
1
- import { STREAMS_SCHEMA } from "./migrations/shared/index.js";
2
- import { quoteIdentifier, tenantSchemaName } from "./names.js";
3
- /** Every Tenant's record (the stream relay), or one Tenant's (`tenantId`, its Session Store). */
4
- export function createPostgresRecordReader(sql, options = {}) {
5
- const only = options.tenantId;
6
- const events = sql(`${STREAMS_SCHEMA}.session_events`);
7
- const heads = sql(`${STREAMS_SCHEMA}.session_log_heads`);
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
- const rows = await sql `
11
- SELECT seq, generation, body FROM ${events}
12
- WHERE tenant_id = ${tenantId} AND session_id = ${sessionId}
13
- AND seq >= ${from} AND seq < ${to}
14
- ORDER BY seq`;
15
- return rows.map((row) => ({
16
- tenantId,
17
- sessionId,
18
- seq: Number(row.seq),
19
- generation: row.generation,
20
- body: row.body,
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
- const rows = await sql `
25
- SELECT tenant_id, session_id, generation, head FROM ${heads}
26
- WHERE head > 0
27
- ${only === undefined ? sql `` : sql `AND tenant_id = ${only}`}
28
- ${after ? sql `AND (tenant_id, session_id) > (${after.tenantId}, ${after.sessionId})` : sql ``}
29
- ORDER BY tenant_id, session_id
30
- LIMIT ${limit}`;
31
- return rows.map((row) => ({
32
- tenantId: row.tenant_id,
33
- sessionId: row.session_id,
34
- generation: row.generation,
35
- head: Number(row.head),
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
- let schema;
40
- try {
41
- schema = tenantSchemaName(tenantId);
42
- }
43
- catch {
53
+ if (tenantId !== own)
44
54
  return undefined;
45
- }
46
- const [exists] = await sql `
47
- SELECT to_regclass(${`${quoteIdentifier(schema)}.tenant`}) IS NOT NULL AS exists`;
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
  }