@nylorun/runtime 0.13.0-beta → 0.14.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/CHANGELOG.md +330 -0
  2. package/README.md +69 -15
  3. package/dist/adapters/execution/restate.d.ts +7 -2
  4. package/dist/adapters/execution/restate.js +38 -8
  5. package/dist/adapters/replication/pgoutput.d.ts +16 -0
  6. package/dist/adapters/replication/pgoutput.js +169 -0
  7. package/dist/adapters/streams/s2.js +37 -20
  8. package/dist/admin-openapi.json +1071 -0
  9. package/dist/api/a2a/card.d.ts +15 -0
  10. package/dist/api/a2a/card.js +24 -0
  11. package/dist/api/a2a/endpoint.d.ts +8 -0
  12. package/dist/api/a2a/endpoint.js +81 -0
  13. package/dist/api/a2a/protocol.d.ts +144 -0
  14. package/dist/api/a2a/protocol.js +188 -0
  15. package/dist/api/a2a/routes.d.ts +19 -0
  16. package/dist/api/a2a/routes.js +289 -0
  17. package/dist/api/a2a/tasks.d.ts +50 -0
  18. package/dist/api/a2a/tasks.js +186 -0
  19. package/dist/api/ag-ui/endpoint.d.ts +8 -0
  20. package/dist/api/ag-ui/endpoint.js +126 -0
  21. package/dist/api/ag-ui/history.d.ts +7 -0
  22. package/dist/api/ag-ui/history.js +60 -0
  23. package/dist/api/ag-ui/routes.d.ts +32 -0
  24. package/dist/api/ag-ui/routes.js +305 -0
  25. package/dist/api/ag-ui/session-id.d.ts +1 -0
  26. package/dist/api/ag-ui/session-id.js +12 -0
  27. package/dist/api/ag-ui/sse.d.ts +9 -0
  28. package/dist/api/ag-ui/sse.js +7 -0
  29. package/dist/api/ag-ui/translate.d.ts +38 -0
  30. package/dist/api/ag-ui/translate.js +222 -0
  31. package/dist/api/components.d.ts +7825 -0
  32. package/dist/api/components.js +83 -0
  33. package/dist/api/http/app.d.ts +18 -0
  34. package/dist/api/http/app.js +58 -0
  35. package/dist/api/http/body.d.ts +4 -0
  36. package/dist/api/http/body.js +37 -0
  37. package/dist/api/http/define.d.ts +39 -0
  38. package/dist/api/http/define.js +130 -0
  39. package/dist/api/http/respond.d.ts +9 -0
  40. package/dist/api/http/respond.js +44 -0
  41. package/dist/api/http/routes/access.d.ts +24 -0
  42. package/dist/api/http/routes/access.js +259 -0
  43. package/dist/api/http/routes/actions.d.ts +8 -0
  44. package/dist/api/http/routes/actions.js +76 -0
  45. package/dist/api/http/routes/endpoints.d.ts +7 -0
  46. package/dist/api/http/routes/endpoints.js +100 -0
  47. package/dist/api/http/routes/sessions.d.ts +3 -0
  48. package/dist/api/http/routes/sessions.js +205 -0
  49. package/dist/api/http/routes/tenant.d.ts +10 -0
  50. package/dist/api/http/routes/tenant.js +172 -0
  51. package/dist/api/http/routes/vaults.d.ts +8 -0
  52. package/dist/api/http/routes/vaults.js +167 -0
  53. package/dist/api/openapi.d.ts +19 -0
  54. package/dist/api/openapi.js +138 -0
  55. package/dist/api/route.d.ts +12 -0
  56. package/dist/api/route.js +4 -0
  57. package/dist/core/bearer.d.ts +8 -0
  58. package/dist/core/bearer.js +11 -0
  59. package/dist/core/flow-host.d.ts +7 -15
  60. package/dist/core/flow-host.js +16 -41
  61. package/dist/core/sandbox-routes.d.ts +12 -7
  62. package/dist/core/sandbox-routes.js +24 -25
  63. package/dist/execution/memory.d.ts +8 -5
  64. package/dist/execution/memory.js +33 -13
  65. package/dist/execution/types.d.ts +38 -6
  66. package/dist/execution/types.js +12 -3
  67. package/dist/host/admin-api.d.ts +22 -0
  68. package/dist/host/admin-api.js +178 -0
  69. package/dist/host/app.d.ts +34 -0
  70. package/dist/host/app.js +185 -0
  71. package/dist/host/config-for.d.ts +2 -0
  72. package/dist/host/config-for.js +1 -0
  73. package/dist/host/config.d.ts +11 -0
  74. package/dist/host/cors.d.ts +27 -0
  75. package/dist/host/cors.js +64 -0
  76. package/dist/host/create-host.d.ts +31 -3
  77. package/dist/host/create-host.js +168 -327
  78. package/dist/host/http.d.ts +14 -3
  79. package/dist/host/http.js +64 -23
  80. package/dist/host/main.js +55 -4
  81. package/dist/host/stack-config.d.ts +20 -0
  82. package/dist/host/stack-config.js +55 -0
  83. package/dist/mcp/pool.d.ts +19 -1
  84. package/dist/mcp/pool.js +60 -10
  85. package/dist/model/catalog.d.ts +2 -10
  86. package/dist/model/classify.d.ts +13 -0
  87. package/dist/model/classify.js +61 -0
  88. package/dist/model/models.d.ts +10 -1
  89. package/dist/model/models.js +14 -6
  90. package/dist/model/pi-model.d.ts +17 -2
  91. package/dist/model/pi-model.js +366 -222
  92. package/dist/openapi.json +25696 -0
  93. package/dist/sandbox/manager.d.ts +13 -1
  94. package/dist/sandbox/manager.js +8 -11
  95. package/dist/sandbox/resolve.d.ts +33 -0
  96. package/dist/sandbox/resolve.js +81 -0
  97. package/dist/sandbox/session-sandbox.d.ts +11 -0
  98. package/dist/sandbox/session-sandbox.js +51 -0
  99. package/dist/sandbox/share.d.ts +14 -2
  100. package/dist/sandbox/share.js +33 -3
  101. package/dist/sandbox/tenant-config.d.ts +37 -0
  102. package/dist/sandbox/tenant-config.js +51 -0
  103. package/dist/sandbox/tools.d.ts +7 -9
  104. package/dist/sandbox/tools.js +11 -3
  105. package/dist/sandbox/types.d.ts +8 -2
  106. package/dist/store/event.d.ts +19 -0
  107. package/dist/store/event.js +49 -0
  108. package/dist/store/memory.d.ts +20 -8
  109. package/dist/store/memory.js +265 -100
  110. package/dist/store/postgres/connect.d.ts +13 -0
  111. package/dist/store/postgres/connect.js +5 -0
  112. package/dist/store/postgres/migrations/003_subject_tokens.d.ts +8 -0
  113. package/dist/store/postgres/migrations/003_subject_tokens.js +42 -0
  114. package/dist/store/postgres/migrations/004_publishable_keys.d.ts +6 -0
  115. package/dist/store/postgres/migrations/004_publishable_keys.js +18 -0
  116. package/dist/store/postgres/migrations/005_action_endpoints.d.ts +6 -0
  117. package/dist/store/postgres/migrations/005_action_endpoints.js +32 -0
  118. package/dist/store/postgres/migrations/006_drop_executors.d.ts +11 -0
  119. package/dist/store/postgres/migrations/006_drop_executors.js +32 -0
  120. package/dist/store/postgres/migrations/007_event_time.d.ts +7 -0
  121. package/dist/store/postgres/migrations/007_event_time.js +14 -0
  122. package/dist/store/postgres/migrations/008_durable_streams.d.ts +13 -0
  123. package/dist/store/postgres/migrations/008_durable_streams.js +35 -0
  124. package/dist/store/postgres/migrations/index.d.ts +5 -1
  125. package/dist/store/postgres/migrations/index.js +22 -2
  126. package/dist/store/postgres/migrations/shared/index.d.ts +31 -0
  127. package/dist/store/postgres/migrations/shared/index.js +92 -0
  128. package/dist/store/postgres/record.d.ts +11 -0
  129. package/dist/store/postgres/record.js +64 -0
  130. package/dist/store/postgres/store.js +337 -95
  131. package/dist/store/postgres/tenants.js +14 -0
  132. package/dist/store/types.d.ts +176 -81
  133. package/dist/streams/basin.d.ts +18 -1
  134. package/dist/streams/basin.js +34 -2
  135. package/dist/streams/control.d.ts +18 -0
  136. package/dist/streams/control.js +29 -0
  137. package/dist/streams/memory.d.ts +3 -1
  138. package/dist/streams/memory.js +14 -8
  139. package/dist/streams/relay/core.d.ts +35 -0
  140. package/dist/streams/relay/core.js +366 -0
  141. package/dist/streams/relay/memory.d.ts +69 -0
  142. package/dist/streams/relay/memory.js +176 -0
  143. package/dist/streams/relay/types.d.ts +77 -0
  144. package/dist/streams/relay/types.js +7 -0
  145. package/dist/streams/types.d.ts +66 -36
  146. package/dist/streams/types.js +18 -53
  147. package/dist/tenant/access-policy.d.ts +30 -0
  148. package/dist/tenant/access-policy.js +52 -0
  149. package/dist/tenant/actions.d.ts +10 -149
  150. package/dist/tenant/actions.js +5 -167
  151. package/dist/tenant/advance.js +32 -0
  152. package/dist/tenant/auth.d.ts +19 -17
  153. package/dist/tenant/auth.js +90 -93
  154. package/dist/tenant/browser.d.ts +15 -0
  155. package/dist/tenant/browser.js +39 -0
  156. package/dist/tenant/commands.d.ts +18 -2
  157. package/dist/tenant/commands.js +107 -113
  158. package/dist/tenant/context.d.ts +75 -18
  159. package/dist/tenant/context.js +14 -10
  160. package/dist/tenant/delivery-token.d.ts +41 -0
  161. package/dist/tenant/delivery-token.js +105 -0
  162. package/dist/tenant/delivery.d.ts +62 -0
  163. package/dist/tenant/delivery.js +476 -0
  164. package/dist/tenant/effects.d.ts +5 -0
  165. package/dist/tenant/effects.js +78 -37
  166. package/dist/tenant/endpoints.d.ts +67 -0
  167. package/dist/tenant/endpoints.js +90 -0
  168. package/dist/tenant/ephemeral.d.ts +6 -0
  169. package/dist/tenant/ephemeral.js +60 -36
  170. package/dist/tenant/http.d.ts +8 -9
  171. package/dist/tenant/http.js +13 -27
  172. package/dist/tenant/jwt.d.ts +16 -0
  173. package/dist/tenant/jwt.js +50 -0
  174. package/dist/tenant/module.d.ts +3 -0
  175. package/dist/tenant/module.js +6 -12
  176. package/dist/tenant/outbound.d.ts +44 -0
  177. package/dist/tenant/outbound.js +127 -0
  178. package/dist/tenant/pool.d.ts +0 -2
  179. package/dist/tenant/pool.js +0 -18
  180. package/dist/tenant/reset.d.ts +0 -4
  181. package/dist/tenant/reset.js +0 -5
  182. package/dist/tenant/runtime.d.ts +10 -20
  183. package/dist/tenant/runtime.js +59 -42
  184. package/dist/tenant/scheduler.d.ts +7 -2
  185. package/dist/tenant/scheduler.js +7 -3
  186. package/dist/tenant/session-streams.d.ts +125 -0
  187. package/dist/tenant/session-streams.js +399 -0
  188. package/dist/tenant/sessions.d.ts +28 -4
  189. package/dist/tenant/sessions.js +118 -24
  190. package/dist/tenant/signing-keys.d.ts +55 -0
  191. package/dist/tenant/signing-keys.js +172 -0
  192. package/dist/tenant/slim.d.ts +8 -0
  193. package/dist/tenant/slim.js +21 -0
  194. package/dist/tenant/status.d.ts +0 -3
  195. package/dist/tenant/status.js +19 -12
  196. package/dist/tenant/streams.d.ts +60 -67
  197. package/dist/tenant/streams.js +140 -208
  198. package/dist/tenant/subject-limits.d.ts +16 -0
  199. package/dist/tenant/subject-limits.js +45 -0
  200. package/dist/tenant/sweep.d.ts +1 -10
  201. package/dist/tenant/sweep.js +29 -69
  202. package/dist/tenant/tokens.d.ts +14 -0
  203. package/dist/tenant/tokens.js +178 -0
  204. package/dist/tenant/transcript.d.ts +48 -5
  205. package/dist/tenant/transcript.js +58 -1
  206. package/dist/tenant/types.d.ts +29 -7
  207. package/dist/tenant/worker.d.ts +3 -1
  208. package/dist/tenant/worker.js +9 -3
  209. package/dist/vault/service.d.ts +18 -2
  210. package/dist/vault/service.js +57 -13
  211. package/dist/version.d.ts +1 -1
  212. package/dist/version.js +1 -1
  213. package/package.json +20 -8
  214. package/dist/core/executors.d.ts +0 -44
  215. package/dist/core/executors.js +0 -95
  216. package/dist/streams/relay.d.ts +0 -37
  217. package/dist/streams/relay.js +0 -254
  218. package/dist/tenant/live.d.ts +0 -92
  219. package/dist/tenant/live.js +0 -290
  220. package/dist/tenant/routes-tenant.d.ts +0 -12
  221. package/dist/tenant/routes-tenant.js +0 -137
  222. package/dist/tenant/routes.d.ts +0 -9
  223. package/dist/tenant/routes.js +0 -151
@@ -0,0 +1,16 @@
1
+ import type { ChangeSource } from "../../streams/relay/types.js";
2
+ export interface PgoutputSourceOptions {
3
+ /** A Postgres URL whose role may replicate (`REPLICATION`) and read `nylorun_streams`. */
4
+ connectionString: string;
5
+ /** Default `nylorun_stream_relay`. */
6
+ slot?: string;
7
+ /** Default `nylorun_stream_relay`. */
8
+ publication?: string;
9
+ /** The schema of the record tables. Default `nylorun_streams`. */
10
+ schema?: string;
11
+ /** Wait between attempts to take the slot. Default 5 s. */
12
+ retryMs?: number;
13
+ log?: (message: string, fields?: Record<string, unknown>) => void;
14
+ }
15
+ export declare const DEFAULT_RELAY_SLOT = "nylorun_stream_relay";
16
+ export declare function createPgoutputSource(options: PgoutputSourceOptions): ChangeSource;
@@ -0,0 +1,169 @@
1
+ /**
2
+ * The stream relay's change source on Postgres logical replication (Durable Streams §7.1,
3
+ * §7.4, §7.6): a persistent `pgoutput` slot on the `nylorun_stream_relay` publication, read
4
+ * with `pg-logical-replication`. The only file importing it (with `pg`, which it needs).
5
+ *
6
+ * - **One reader per slot.** Postgres lets one connection hold a slot, so every Runtime
7
+ * process can run a source and exactly one is active; the others retry every few seconds
8
+ * and take over when it goes away. The slot is the leader election.
9
+ * - **Acknowledged only on request.** The library acknowledges the last position it
10
+ * *received* on a timer; that is turned off (`timeoutSeconds: 0`) and keepalives are
11
+ * answered by the relay, which acknowledges only what S2 has.
12
+ * - **Always from the confirmed position.** Each attempt is a new connection that starts at
13
+ * `0/0`, which Postgres reads as the slot's confirmed position, so a reconnect replays
14
+ * whatever was received but not acknowledged.
15
+ * - **Fresh slots.** A missing slot is created; a slot Postgres invalidated
16
+ * (`max_slot_wal_keep_size`) is dropped and created again. Either way the relay must
17
+ * reconcile the record with S2; that is recorded in `relay_slots` until `reconciled()`, so a
18
+ * crash during reconciliation, or another process winning the slot, does not lose it.
19
+ */
20
+ import pg from "pg";
21
+ import { LogicalReplicationService, PgoutputPlugin } from "pg-logical-replication";
22
+ export const DEFAULT_RELAY_SLOT = "nylorun_stream_relay";
23
+ export function createPgoutputSource(options) {
24
+ const slot = options.slot ?? DEFAULT_RELAY_SLOT;
25
+ const publication = options.publication ?? "nylorun_stream_relay";
26
+ const schema = options.schema ?? "nylorun_streams";
27
+ const retryMs = options.retryMs ?? 5000;
28
+ const log = options.log ?? (() => { });
29
+ const pool = new pg.Pool({
30
+ connectionString: options.connectionString,
31
+ max: 2,
32
+ application_name: "nylorun-stream-relay",
33
+ });
34
+ let service;
35
+ let stopped = false;
36
+ let running;
37
+ let wake;
38
+ /** Creates the slot when missing or lost; true when the record must be reconciled. */
39
+ async function prepareSlot() {
40
+ const client = await pool.connect();
41
+ try {
42
+ const { rows } = await client.query("SELECT wal_status, active FROM pg_replication_slots WHERE slot_name = $1", [slot]);
43
+ const existing = rows[0];
44
+ if (existing?.wal_status === "lost" && !existing.active) {
45
+ log("replication slot was invalidated; creating it again", { slot });
46
+ await client.query("SELECT pg_drop_replication_slot($1)", [slot]);
47
+ }
48
+ if (!existing || existing.wal_status === "lost") {
49
+ try {
50
+ await client.query("SELECT pg_create_logical_replication_slot($1, 'pgoutput')", [slot]);
51
+ log("replication slot created", { slot });
52
+ await client.query(`INSERT INTO ${schema}.relay_slots (slot_name, reconcile_pending) VALUES ($1, true)
53
+ ON CONFLICT (slot_name) DO UPDATE SET reconcile_pending = true`, [slot]);
54
+ }
55
+ catch (error) {
56
+ // Another process created it first; its row says whether to reconcile.
57
+ if (error.code !== "42710")
58
+ throw error;
59
+ }
60
+ }
61
+ const pending = await client.query(`SELECT reconcile_pending FROM ${schema}.relay_slots WHERE slot_name = $1`, [slot]);
62
+ // A slot created before this table knew it: reconcile once to be sure.
63
+ return pending.rows[0]?.reconcile_pending ?? true;
64
+ }
65
+ finally {
66
+ client.release();
67
+ }
68
+ }
69
+ async function attempt(handlers) {
70
+ const fresh = await prepareSlot();
71
+ // `stop()` during `prepareSlot` had no service to stop: do not start one it cannot.
72
+ if (stopped)
73
+ return;
74
+ const current = new LogicalReplicationService({ connectionString: options.connectionString, application_name: "nylorun-stream-relay" }, { acknowledge: { auto: false, timeoutSeconds: 0 }, flowControl: { enabled: false } });
75
+ service = current;
76
+ let decoding;
77
+ current.on("start", () => handlers.onActive({ fresh }));
78
+ current.on("data", (_lsn, message) => {
79
+ switch (message.tag) {
80
+ case "begin":
81
+ decoding = { rows: [] };
82
+ break;
83
+ case "insert":
84
+ if (decoding &&
85
+ message.relation?.schema === schema &&
86
+ message.relation?.name === "session_events")
87
+ decoding.rows.push(rowOf(message.new));
88
+ break;
89
+ case "commit": {
90
+ const tx = { endLsn: message.commitEndLsn, rows: decoding?.rows ?? [] };
91
+ decoding = undefined;
92
+ // A transaction with no record rows still moves the slot once acknowledged.
93
+ handlers.onTx(tx);
94
+ break;
95
+ }
96
+ }
97
+ });
98
+ current.on("heartbeat", (lsn, _timestamp, shouldRespond) => {
99
+ // Mid-transaction, the server's position may be past rows not yet delivered.
100
+ if (!shouldRespond || decoding)
101
+ return;
102
+ const position = handlers.onKeepalive(lsn);
103
+ if (position)
104
+ void current.acknowledge(position).catch(() => { });
105
+ });
106
+ current.on("error", () => { });
107
+ const plugin = new PgoutputPlugin({ protoVersion: 1, publicationNames: [publication] });
108
+ // Resolves when replication ends, rejects when it fails; `0/0` = the slot's confirmed position.
109
+ await current.subscribe(plugin, slot, "0/00000000");
110
+ }
111
+ async function run(handlers) {
112
+ while (!stopped) {
113
+ let failure;
114
+ try {
115
+ await attempt(handlers);
116
+ }
117
+ catch (error) {
118
+ failure = error;
119
+ }
120
+ if (stopped)
121
+ break;
122
+ const busy = failure?.code === "55006";
123
+ // Another process holds the slot: it is the relay. Anything else: report and retry.
124
+ handlers.onInactive(busy ? undefined : failure ?? new Error("replication ended"));
125
+ await new Promise((resolve) => {
126
+ wake = resolve;
127
+ setTimeout(resolve, retryMs).unref?.();
128
+ });
129
+ wake = undefined;
130
+ }
131
+ }
132
+ return {
133
+ start(handlers) {
134
+ if (running)
135
+ throw new Error("The change source is already started");
136
+ running = run(handlers);
137
+ },
138
+ acknowledge(lsn) {
139
+ void service?.acknowledge(lsn).catch(() => { });
140
+ },
141
+ async reconciled() {
142
+ await pool.query(`UPDATE ${schema}.relay_slots SET reconcile_pending = false WHERE slot_name = $1`, [
143
+ slot,
144
+ ]);
145
+ },
146
+ async lag() {
147
+ const { rows } = await pool.query(`SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)::text AS lag
148
+ FROM pg_replication_slots WHERE slot_name = $1`, [slot]);
149
+ return rows[0]?.lag == null ? undefined : Number(rows[0].lag);
150
+ },
151
+ async stop() {
152
+ stopped = true;
153
+ wake?.();
154
+ await service?.stop().catch(() => undefined);
155
+ await running?.catch(() => undefined);
156
+ await pool.end().catch(() => undefined);
157
+ },
158
+ };
159
+ }
160
+ /** A decoded `session_events` row (`pg` parses `json`; `bigint` arrives as text). */
161
+ function rowOf(values) {
162
+ return {
163
+ tenantId: values.tenant_id,
164
+ sessionId: values.session_id,
165
+ seq: Number(values.seq),
166
+ generation: Number(values.generation),
167
+ body: typeof values.body === "string" ? JSON.parse(values.body) : values.body,
168
+ };
169
+ }
@@ -5,12 +5,12 @@
5
5
  *
6
6
  * ## Layout
7
7
  *
8
- * - One basin per Tenant, named by `tenantBasinName` (`streams/basin.ts`):
9
- * `<basinPrefix>tn-<ulid>` for a Tenant id `tn_<ulid>`.
10
- * - The basin is created with `createStreamOnAppend`, so `sessions/<id>/<incarnation>`,
11
- * `tenant/work` and `tenant/control` come into existence on their first
12
- * append. Its default stream config sets infinite retention, which session
13
- * streams keep; the work and control streams are created with a one-day
8
+ * - One basin per Tenant basin generation, named by `tenantBasinName` (`streams/basin.ts`):
9
+ * `<basinPrefix>tn-<ulid>` for a Tenant id `tn_<ulid>`, `…-<g base36>` for generation g.
10
+ * - The basin is created with `createStreamOnAppend` and `createStreamOnRead`, so
11
+ * `sessions/<id>` and `tenant/control` come into existence on first use. Its
12
+ * default stream config sets infinite retention, which session streams
13
+ * keep; the control stream is created with a one-day
14
14
  * age-based retention instead (signals are latency hints, not history).
15
15
  * - Bodies are JSON text in string records. Record timestamps are S2 arrival
16
16
  * times.
@@ -27,7 +27,7 @@
27
27
  * explicit `{ infinite: {} }` retention to keep session history; this adapter
28
28
  * sets it as the basin default, and s2-lite honours it (a new stream reports
29
29
  * `infinite`). A `streamConfig` sent with an append that creates the stream
30
- * is honoured too (used for the work and control streams). `deleteOnEmpty`
30
+ * is honoured too (used for the control stream). `deleteOnEmpty`
31
31
  * defaults to disabled (`minAgeSecs: 0`).
32
32
  * - **Stream deletion** is immediate for our purposes: right after `DELETE`,
33
33
  * `checkTail` and reads return 404 `stream_not_found` (new read sessions
@@ -58,9 +58,9 @@
58
58
  * use the SDK's HTTP/2 (s2s) transport, which s2-lite serves over cleartext.
59
59
  */
60
60
  import { AppendInput, AppendRecord, RangeNotSatisfiableError, S2, S2Error, SeqNumMismatchError, } from "@s2-dev/streamstore";
61
- import { tenantBasinName, validateBasinPrefix } from "../../streams/basin.js";
62
- import { CONTROL_STREAM, WORK_STREAM, } from "../../streams/types.js";
63
- /** Retention of the work and control signal streams. */
61
+ import { parseBasin, tenantBasinName, validateBasinPrefix } from "../../streams/basin.js";
62
+ import { CONTROL_STREAM, } from "../../streams/types.js";
63
+ /** Retention of the control signal stream. */
64
64
  const SIGNAL_RETENTION_SECS = 24 * 60 * 60;
65
65
  /** Records per unary read while reading history. */
66
66
  const HISTORY_PAGE = 1000;
@@ -99,7 +99,7 @@ class S2Streams {
99
99
  return AppendRecord.string({ body });
100
100
  }), {
101
101
  ...(options.matchSeq !== undefined ? { matchSeqNum: options.matchSeq } : {}),
102
- ...(stream === WORK_STREAM || stream === CONTROL_STREAM
102
+ ...(stream === CONTROL_STREAM
103
103
  ? { streamConfig: { retentionPolicy: { ageSecs: SIGNAL_RETENTION_SECS } } }
104
104
  : {}),
105
105
  });
@@ -155,6 +155,9 @@ class S2Streams {
155
155
  basin,
156
156
  config: {
157
157
  createStreamOnAppend: true,
158
+ // A reader that arrives before a session's first event creates the
159
+ // stream and follows it live, instead of polling until it exists.
160
+ createStreamOnRead: true,
158
161
  defaultStreamConfig: { retentionPolicy: { infinite: {} } },
159
162
  },
160
163
  }, { signal: this.closing.signal });
@@ -171,15 +174,24 @@ class S2Streams {
171
174
  }
172
175
  }
173
176
  }
174
- async deleteTenant(tenantId) {
177
+ async deleteTenant(tenantId, options = {}) {
175
178
  this.checkOpen();
176
- try {
177
- await this.s2.basins.delete({ basin: this.basinName(tenantId) }, { signal: this.closing.signal });
178
- }
179
- catch (error) {
180
- if (!isMissing(error))
181
- throw error;
179
+ const basins = [this.basinName(tenantId)];
180
+ // Every later generation's basin too: `<basin>-<g base36>`.
181
+ if (options.allGenerations && parseBasin(tenantId).generation === 0) {
182
+ const later = new RegExp(`^${escapeRegExp(basins[0])}-[0-9a-z]{1,2}$`);
183
+ for await (const info of this.s2.basins.listAll({ prefix: `${basins[0]}-` }, { signal: this.closing.signal }))
184
+ if (later.test(info.name) && info.deletedAt == null)
185
+ basins.push(info.name);
182
186
  }
187
+ for (const basin of basins)
188
+ try {
189
+ await this.s2.basins.delete({ basin }, { signal: this.closing.signal });
190
+ }
191
+ catch (error) {
192
+ if (!isMissing(error))
193
+ throw error;
194
+ }
183
195
  }
184
196
  async deleteStream(tenantId, stream) {
185
197
  this.checkOpen();
@@ -256,7 +268,9 @@ class S2Streams {
256
268
  /**
257
269
  * Follows `stream` from `fromSeq` with S2 read sessions, reopening a session
258
270
  * from the next sequence whenever one ends or fails, until aborted, closed,
259
- * or the Tenant's basin is gone. A missing stream is polled until it exists.
271
+ * or the Tenant's basin is gone. Basins create a stream on read, so a reader
272
+ * that arrives before the first append waits on the live session instead of
273
+ * polling.
260
274
  */
261
275
  async *readLive(tenantId, stream, fromSeq, signal) {
262
276
  const aborted = anySignal(signal, this.closing.signal);
@@ -288,7 +302,7 @@ class S2Streams {
288
302
  if ((isMissing(error) || isCode(error, "stream_deletion_pending")) &&
289
303
  (await this.basinGone(tenantId, aborted)))
290
304
  return;
291
- // Stream not created yet, or S2 unreachable: retry below.
305
+ // S2 unreachable, or a stream being deleted: retry below.
292
306
  }
293
307
  await sleep(delay, aborted);
294
308
  delay = Math.min(delay * 2, RETRY_MAX_MS);
@@ -343,6 +357,9 @@ function isTransactionConflict(error) {
343
357
  function isTenantGone(error) {
344
358
  return isCode(error, "basin_not_found") || isCode(error, "basin_deletion_pending");
345
359
  }
360
+ function escapeRegExp(text) {
361
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
362
+ }
346
363
  function anySignal(...signals) {
347
364
  return AbortSignal.any(signals.filter((s) => s !== undefined));
348
365
  }