@nylorun/runtime 0.10.0-beta → 0.12.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 (194) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +53 -51
  3. package/dist/adapters/execution/restate.d.ts +130 -0
  4. package/dist/adapters/execution/restate.js +442 -0
  5. package/dist/adapters/sandbox/virtual.js +1 -1
  6. package/dist/adapters/streams/s2.d.ts +18 -0
  7. package/dist/adapters/streams/s2.js +361 -0
  8. package/dist/core/executors.d.ts +4 -1
  9. package/dist/core/executors.js +20 -1
  10. package/dist/core/flow-host.d.ts +93 -132
  11. package/dist/core/flow-host.js +282 -547
  12. package/dist/core/sandbox-routes.d.ts +13 -4
  13. package/dist/core/sandbox-routes.js +14 -12
  14. package/dist/execution/memory.d.ts +54 -0
  15. package/dist/execution/memory.js +217 -0
  16. package/dist/execution/types.d.ts +125 -0
  17. package/dist/execution/types.js +52 -0
  18. package/dist/host/config-for.d.ts +2 -1
  19. package/dist/host/config-for.js +3 -21
  20. package/dist/host/config.d.ts +0 -8
  21. package/dist/host/create-host.d.ts +36 -1
  22. package/dist/host/create-host.js +51 -24
  23. package/dist/host/execution.d.ts +61 -0
  24. package/dist/host/execution.js +69 -0
  25. package/dist/host/http.d.ts +4 -0
  26. package/dist/host/http.js +6 -0
  27. package/dist/host/index.d.ts +3 -1
  28. package/dist/host/index.js +2 -0
  29. package/dist/host/main.js +106 -47
  30. package/dist/host/stack-config.d.ts +72 -0
  31. package/dist/host/stack-config.js +193 -0
  32. package/dist/index.d.ts +1 -1
  33. package/dist/infra/database.d.ts +16 -0
  34. package/dist/infra/database.js +31 -0
  35. package/dist/infra/execution.d.ts +39 -0
  36. package/dist/infra/execution.js +68 -0
  37. package/dist/infra/index.d.ts +42 -0
  38. package/dist/infra/index.js +56 -0
  39. package/dist/infra/readiness.d.ts +33 -0
  40. package/dist/infra/readiness.js +53 -0
  41. package/dist/infra/streams.d.ts +17 -0
  42. package/dist/infra/streams.js +33 -0
  43. package/dist/infra/workers.d.ts +39 -0
  44. package/dist/infra/workers.js +31 -0
  45. package/dist/model/pi-model.d.ts +2 -2
  46. package/dist/model/pi-model.js +5 -5
  47. package/dist/sandbox/manager.d.ts +26 -8
  48. package/dist/sandbox/manager.js +95 -47
  49. package/dist/sandbox/select.d.ts +1 -1
  50. package/dist/sandbox/select.js +4 -5
  51. package/dist/sandbox/types.d.ts +2 -2
  52. package/dist/store/canonical.d.ts +5 -0
  53. package/dist/store/canonical.js +17 -0
  54. package/dist/store/cursor.d.ts +9 -0
  55. package/dist/store/cursor.js +18 -0
  56. package/dist/store/memory.d.ts +57 -0
  57. package/dist/store/memory.js +645 -0
  58. package/dist/store/ownership.d.ts +19 -0
  59. package/dist/store/ownership.js +33 -0
  60. package/dist/store/postgres/connect.d.ts +25 -0
  61. package/dist/store/postgres/connect.js +25 -0
  62. package/dist/store/postgres/locking.d.ts +16 -0
  63. package/dist/store/postgres/locking.js +22 -0
  64. package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
  65. package/dist/store/postgres/migrations/001_initial.js +196 -0
  66. package/dist/store/postgres/migrations/002_session_owner.d.ts +6 -0
  67. package/dist/store/postgres/migrations/002_session_owner.js +13 -0
  68. package/dist/store/postgres/migrations/index.d.ts +49 -0
  69. package/dist/store/postgres/migrations/index.js +76 -0
  70. package/dist/store/postgres/names.d.ts +11 -0
  71. package/dist/store/postgres/names.js +42 -0
  72. package/dist/store/postgres/store.d.ts +11 -0
  73. package/dist/store/postgres/store.js +797 -0
  74. package/dist/store/postgres/tenants.d.ts +90 -0
  75. package/dist/store/postgres/tenants.js +214 -0
  76. package/dist/store/types.d.ts +442 -0
  77. package/dist/store/types.js +10 -0
  78. package/dist/streams/basin.d.ts +23 -0
  79. package/dist/streams/basin.js +51 -0
  80. package/dist/streams/memory.d.ts +25 -0
  81. package/dist/streams/memory.js +124 -0
  82. package/dist/streams/relay.d.ts +37 -0
  83. package/dist/streams/relay.js +254 -0
  84. package/dist/streams/types.d.ts +115 -0
  85. package/dist/streams/types.js +86 -0
  86. package/dist/tenant/actions.d.ts +153 -0
  87. package/dist/tenant/actions.js +191 -0
  88. package/dist/tenant/advance.d.ts +15 -0
  89. package/dist/tenant/advance.js +439 -0
  90. package/dist/tenant/auth.d.ts +32 -0
  91. package/dist/tenant/auth.js +169 -0
  92. package/dist/tenant/commands.d.ts +3 -0
  93. package/dist/tenant/commands.js +393 -0
  94. package/dist/tenant/context.d.ts +147 -0
  95. package/dist/tenant/context.js +48 -0
  96. package/dist/tenant/effects.d.ts +20 -0
  97. package/dist/tenant/effects.js +564 -0
  98. package/dist/tenant/envelope.d.ts +0 -3
  99. package/dist/tenant/envelope.js +2 -34
  100. package/dist/tenant/ephemeral.d.ts +15 -4
  101. package/dist/tenant/ephemeral.js +106 -38
  102. package/dist/tenant/http.d.ts +38 -0
  103. package/dist/tenant/http.js +53 -0
  104. package/dist/tenant/live.d.ts +92 -0
  105. package/dist/tenant/live.js +290 -0
  106. package/dist/tenant/model-setting.d.ts +16 -0
  107. package/dist/tenant/model-setting.js +12 -0
  108. package/dist/tenant/module.d.ts +29 -8
  109. package/dist/tenant/module.js +165 -158
  110. package/dist/tenant/paths.d.ts +12 -8
  111. package/dist/tenant/paths.js +38 -12
  112. package/dist/tenant/principals.d.ts +12 -13
  113. package/dist/tenant/principals.js +50 -38
  114. package/dist/tenant/quarantine-error.d.ts +1 -1
  115. package/dist/tenant/quarantine-error.js +2 -8
  116. package/dist/tenant/quarantine.d.ts +0 -8
  117. package/dist/tenant/quarantine.js +8 -30
  118. package/dist/tenant/reset.d.ts +7 -6
  119. package/dist/tenant/reset.js +7 -51
  120. package/dist/tenant/routes-tenant.d.ts +12 -0
  121. package/dist/tenant/routes-tenant.js +137 -0
  122. package/dist/tenant/routes.d.ts +9 -0
  123. package/dist/tenant/routes.js +151 -0
  124. package/dist/tenant/runtime.d.ts +59 -58
  125. package/dist/tenant/runtime.js +243 -2201
  126. package/dist/tenant/scheduler.d.ts +25 -0
  127. package/dist/tenant/scheduler.js +84 -0
  128. package/dist/tenant/session.d.ts +60 -0
  129. package/dist/tenant/session.js +94 -0
  130. package/dist/tenant/sessions.d.ts +40 -0
  131. package/dist/tenant/sessions.js +163 -0
  132. package/dist/tenant/status.d.ts +13 -4
  133. package/dist/tenant/status.js +76 -41
  134. package/dist/tenant/store-memory.d.ts +9 -12
  135. package/dist/tenant/store-memory.js +79 -68
  136. package/dist/tenant/store-pg.d.ts +15 -0
  137. package/dist/tenant/store-pg.js +140 -0
  138. package/dist/tenant/streams.d.ts +126 -0
  139. package/dist/tenant/streams.js +419 -0
  140. package/dist/tenant/sweep.d.ts +15 -0
  141. package/dist/tenant/sweep.js +122 -0
  142. package/dist/tenant/transcript.d.ts +104 -0
  143. package/dist/tenant/transcript.js +58 -0
  144. package/dist/tenant/types.d.ts +101 -15
  145. package/dist/tenant/types.js +23 -1
  146. package/dist/tenant/worker.d.ts +84 -0
  147. package/dist/tenant/worker.js +189 -0
  148. package/dist/vault/service.d.ts +44 -31
  149. package/dist/vault/service.js +454 -360
  150. package/dist/version.d.ts +1 -1
  151. package/dist/version.js +1 -1
  152. package/package.json +15 -14
  153. package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
  154. package/dist/adapters/sandbox/microsandbox.js +0 -238
  155. package/dist/core/store.d.ts +0 -32
  156. package/dist/core/store.js +0 -143
  157. package/dist/launcher/commands.d.ts +0 -24
  158. package/dist/launcher/commands.js +0 -260
  159. package/dist/launcher/ensure-config.d.ts +0 -16
  160. package/dist/launcher/ensure-config.js +0 -79
  161. package/dist/launcher/errors.d.ts +0 -12
  162. package/dist/launcher/errors.js +0 -22
  163. package/dist/launcher/host-config.d.ts +0 -17
  164. package/dist/launcher/host-config.js +0 -64
  165. package/dist/launcher/host-state.d.ts +0 -17
  166. package/dist/launcher/host-state.js +0 -61
  167. package/dist/launcher/lifecycle.d.ts +0 -66
  168. package/dist/launcher/lifecycle.js +0 -407
  169. package/dist/launcher/locks.d.ts +0 -26
  170. package/dist/launcher/locks.js +0 -117
  171. package/dist/launcher/logs.d.ts +0 -18
  172. package/dist/launcher/logs.js +0 -117
  173. package/dist/launcher/main.d.ts +0 -2
  174. package/dist/launcher/main.js +0 -66
  175. package/dist/launcher/output.d.ts +0 -11
  176. package/dist/launcher/output.js +0 -68
  177. package/dist/launcher/paths.d.ts +0 -20
  178. package/dist/launcher/paths.js +0 -36
  179. package/dist/launcher/probe.d.ts +0 -23
  180. package/dist/launcher/probe.js +0 -72
  181. package/dist/launcher/protocol.d.ts +0 -62
  182. package/dist/launcher/protocol.js +0 -1
  183. package/dist/launcher/schema-guard.d.ts +0 -6
  184. package/dist/launcher/schema-guard.js +0 -36
  185. package/dist/launcher/spawn.d.ts +0 -22
  186. package/dist/launcher/spawn.js +0 -85
  187. package/dist/launcher/status.d.ts +0 -6
  188. package/dist/launcher/status.js +0 -65
  189. package/dist/tenant/migration.d.ts +0 -16
  190. package/dist/tenant/migration.js +0 -116
  191. package/dist/tenant/schema.d.ts +0 -23
  192. package/dist/tenant/schema.js +0 -151
  193. package/dist/tenant/store-fs.d.ts +0 -16
  194. package/dist/tenant/store-fs.js +0 -205
@@ -1,19 +1,16 @@
1
- import type { TenantEnvelope } from "@nylorun/core/contracts";
2
- import type { BootstrapPrincipal, Logger, OpenTenantRuntime, TenantConfig, TenantStore } from "./types.js";
1
+ import type { SessionStore } from "../store/types.js";
2
+ import { type Logger, type OpenTenantRuntime, type TenantConfig, type TenantStore } from "./types.js";
3
3
  export interface MemoryTenantStoreOptions {
4
+ /** When set, each Tenant's directory under this Host root lives as long as the Tenant. */
5
+ hostRoot?: string;
4
6
  openRuntime: OpenTenantRuntime;
5
7
  configFor: (tenantId: string) => TenantConfig;
6
8
  logger?: Logger;
7
- /** Schema version recorded for new tenants (default TENANT_SCHEMA_VERSION). */
8
- schemaVersion?: number;
9
9
  }
10
- /**
11
- * In-memory TenantStore for tests. No filesystem; openRuntime is still injected
12
- * so conformance can exercise the same module path as the FS adapter.
13
- */
14
- export declare function createMemoryTenantStore(options: MemoryTenantStoreOptions): TenantStore & {
15
- /** Test helper: insert a raw record without opening. */
16
- seed(envelope: TenantEnvelope, bootstrap: BootstrapPrincipal): void;
17
- /** Test helper: current live ids. */
10
+ export type MemoryTenantStore = TenantStore & {
11
+ /** Test helper: current Tenant ids. */
18
12
  ids(): string[];
13
+ /** Test helper: a Session Store on a Tenant's data, as another process would open it. */
14
+ sessionStore(id: string): SessionStore;
19
15
  };
16
+ export declare function createMemoryTenantStore(options: MemoryTenantStoreOptions): MemoryTenantStore;
@@ -1,100 +1,111 @@
1
+ /**
2
+ * An in-memory Tenant store for tests: each Tenant is a `MemoryStoreData` (its Session
3
+ * Store's data) and an envelope. It behaves like the Postgres store (`store-pg.ts`): the
4
+ * Tenant directory under the Host root, when one is given, is created with the Tenant and
5
+ * removed with it, and `open` hands the Tenant Runtime a fresh Session Store on the Tenant's
6
+ * data. Not a supported profile.
7
+ */
8
+ import { mkdirSync, rmSync } from "node:fs";
1
9
  import { isTenantId } from "@nylorun/core/compatibility";
2
- import { TENANT_SCHEMA_VERSION } from "./schema.js";
10
+ import { MEMORY_SCHEMA_VERSION, MemorySessionStore, MemoryStoreData, } from "../store/memory.js";
3
11
  import { quarantine } from "./quarantine.js";
4
12
  import { parseEnvelope } from "./envelope.js";
5
- /**
6
- * In-memory TenantStore for tests. No filesystem; openRuntime is still injected
7
- * so conformance can exercise the same module path as the FS adapter.
8
- */
13
+ import { tenantPaths } from "./paths.js";
14
+ import { bootstrapPrincipal, bootstrapPrincipalMatches } from "./principals.js";
15
+ import { TenantNotFoundError, } from "./types.js";
9
16
  export function createMemoryTenantStore(options) {
10
17
  const records = new Map();
11
- const schemaVersion = options.schemaVersion ?? TENANT_SCHEMA_VERSION;
12
- const store = {
13
- seed(envelope, bootstrap) {
14
- const env = parseEnvelope(envelope, envelope.id);
15
- records.set(env.id, {
16
- envelope: env,
17
- bootstrap: { ...bootstrap },
18
- schemaVersion: env.schemaVersion,
18
+ function sessionStore(id, data) {
19
+ return new MemorySessionStore({
20
+ tenantId: id,
21
+ onError: (error) => options.logger?.error("post-commit step failed", {
22
+ message: error instanceof Error ? error.message : String(error),
23
+ }),
24
+ }, data);
25
+ }
26
+ function makeDirectory(id) {
27
+ if (!options.hostRoot)
28
+ return;
29
+ const p = tenantPaths(options.hostRoot, id);
30
+ mkdirSync(p.root, { recursive: true, mode: 0o700 });
31
+ for (const dir of [p.home, p.tmp, p.sandboxes, p.pluginData, p.logs])
32
+ mkdirSync(dir, { recursive: true });
33
+ }
34
+ function removeDirectory(id) {
35
+ if (options.hostRoot)
36
+ rmSync(tenantPaths(options.hostRoot, id).root, {
37
+ recursive: true,
38
+ force: true,
19
39
  });
20
- },
40
+ }
41
+ async function withStore(id, data, fn) {
42
+ const store = sessionStore(id, data);
43
+ try {
44
+ return await fn(store);
45
+ }
46
+ finally {
47
+ await store.close();
48
+ }
49
+ }
50
+ return {
21
51
  ids() {
22
- return [...records.keys()].filter((id) => !records.get(id)?.trashed);
52
+ return [...records.keys()];
53
+ },
54
+ sessionStore(id) {
55
+ const record = records.get(id);
56
+ if (!record)
57
+ throw new TenantNotFoundError(id);
58
+ return sessionStore(id, record.data);
23
59
  },
24
60
  async enumerate() {
25
- return [...records.entries()]
26
- .filter(([, r]) => !r.trashed)
27
- .map(([id]) => id)
28
- .filter(isTenantId);
61
+ return [...records.keys()].filter(isTenantId).sort();
29
62
  },
30
63
  async readEnvelope(id) {
31
64
  const record = records.get(id);
32
- if (!record || record.trashed) {
33
- throw quarantine("envelope-invalid", "tenant envelope not found", {
34
- tenantId: id,
35
- });
36
- }
65
+ if (!record)
66
+ throw new TenantNotFoundError(id);
37
67
  return parseEnvelope(record.envelope, id);
38
68
  },
39
69
  async create(envelope, bootstrap) {
40
- const env = parseEnvelope(envelope, envelope.id);
41
- const existing = records.get(env.id);
42
- if (existing && !existing.trashed)
70
+ const parsed = parseEnvelope({ ...envelope, schemaVersion: MEMORY_SCHEMA_VERSION }, envelope.id);
71
+ if (records.has(parsed.id))
43
72
  return "exists";
44
- records.set(env.id, {
45
- envelope: env,
46
- bootstrap: { ...bootstrap },
47
- schemaVersion: env.schemaVersion || schemaVersion,
48
- });
73
+ makeDirectory(parsed.id);
74
+ const data = new MemoryStoreData();
75
+ await withStore(parsed.id, data, (store) => store.tx((t) => bootstrapPrincipal(t, bootstrap)));
76
+ records.set(parsed.id, { envelope: parsed, data });
49
77
  return "created";
50
78
  },
51
79
  async bootstrapMatches(id, bootstrap) {
52
80
  const record = records.get(id);
53
- if (!record || record.trashed)
81
+ if (!record)
54
82
  return false;
55
- return (record.bootstrap.principalId === bootstrap.principalId &&
56
- record.bootstrap.credentialHash === bootstrap.credentialHash &&
57
- record.bootstrap.idempotencyKey === bootstrap.idempotencyKey);
83
+ return withStore(id, record.data, (store) => store.tx((t) => bootstrapPrincipalMatches(t, bootstrap)));
58
84
  },
59
85
  async open(id) {
60
86
  const record = records.get(id);
61
- if (!record || record.trashed) {
62
- throw quarantine("open-failed", `Tenant ${id} not found`, {
63
- tenantId: id,
64
- });
87
+ if (!record)
88
+ throw new TenantNotFoundError(id);
89
+ if (record.envelope.schemaVersion > MEMORY_SCHEMA_VERSION)
90
+ throw quarantine("schema-too-new", `Tenant schema version ${record.envelope.schemaVersion} is newer than this Runtime's ${MEMORY_SCHEMA_VERSION}`, { tenantId: id });
91
+ makeDirectory(id);
92
+ const store = sessionStore(id, record.data);
93
+ try {
94
+ return await options.openRuntime({ ...options.configFor(id), tenantId: id }, { store, envelope: record.envelope });
65
95
  }
66
- if (record.schemaVersion > TENANT_SCHEMA_VERSION) {
67
- throw quarantine("schema-too-new", `Tenant schema version ${record.schemaVersion} is newer than Host ${TENANT_SCHEMA_VERSION}`, { tenantId: id });
96
+ catch (error) {
97
+ await store.close().catch(() => undefined);
98
+ throw error;
68
99
  }
69
- const config = options.configFor(id);
70
- const opened = await options.openRuntime({
71
- ...config,
72
- tenantId: id,
73
- });
74
- const handle = {
75
- ...opened,
76
- envelope: record.envelope,
77
- handle: opened.handle.bind(opened),
78
- summary: opened.summary.bind(opened),
79
- drain: opened.drain.bind(opened),
80
- close: opened.close.bind(opened),
81
- };
82
- record.handle = handle;
83
- return handle;
84
100
  },
85
- async trash(id, _now) {
86
- const record = records.get(id);
87
- if (!record)
88
- return;
89
- if (record.handle) {
90
- await record.handle.close().catch(() => undefined);
91
- }
92
- record.trashed = true;
93
- record.handle = undefined;
101
+ async trash(id) {
102
+ records.delete(id);
103
+ removeDirectory(id);
94
104
  },
95
105
  async removePartial(id) {
96
- records.delete(id);
106
+ if (records.has(id))
107
+ return;
108
+ removeDirectory(id);
97
109
  },
98
110
  };
99
- return store;
100
111
  }
@@ -0,0 +1,15 @@
1
+ import type { PostgresClient } from "../store/postgres/connect.js";
2
+ import type { Migration } from "../store/postgres/migrations/index.js";
3
+ import { type Logger, type OpenTenantRuntime, type TenantConfig, type TenantStore } from "./types.js";
4
+ export interface PostgresTenantStoreOptions {
5
+ hostRoot: string;
6
+ /** The Host's pool. The store never ends it. */
7
+ sql: PostgresClient;
8
+ /** Opens the Tenant Runtime on a store the catalog opened; the Runtime then owns it. */
9
+ openRuntime: OpenTenantRuntime;
10
+ configFor: (tenantId: string) => TenantConfig;
11
+ logger?: Logger;
12
+ /** Tests only: the migrations this Runtime knows. */
13
+ migrations?: readonly Migration[];
14
+ }
15
+ export declare function createPostgresTenantStore(options: PostgresTenantStoreOptions): TenantStore;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * The Tenant store over the Postgres Tenant catalog (architecture §8.1, §8.2).
3
+ *
4
+ * A Tenant exists when its schema `tenant_<id>` exists (`store/postgres/tenants.ts`); the
5
+ * envelope is a row in it, so this store keeps no registry. What stays on the Host root is
6
+ * the Tenant directory `tenants/<id>/`: the vault key (`vault-kek`), `plugin-data/`, `logs/`,
7
+ * and the private `home/`, `tmp/` and `sandboxes/`. It is created with the Tenant and removed
8
+ * with it; nothing in it says whether the Tenant exists.
9
+ *
10
+ * - `create` makes the directory, then creates, migrates and bootstraps the schema in one
11
+ * transaction. A schema that already exists is `exists`; the module compares its bootstrap.
12
+ * - `open` migrates an older schema forward and opens the Tenant Runtime on the schema's
13
+ * Session Store. A missing schema is `TenantNotFoundError`; a schema newer than this
14
+ * Runtime, a failed migration, an unreadable envelope or vault ciphertext without its key
15
+ * throws that quarantine. A failure outside the Tenant (Postgres unreachable, its sweep
16
+ * not armed) is `TenantUnavailableError`, which the module does not cache.
17
+ * - `trash` drops the schema and removes the directory. There is no `trash/` copy: the key
18
+ * is useless without the schema.
19
+ */
20
+ import { mkdirSync, rmSync } from "node:fs";
21
+ import { tenantSchemaName } from "../store/postgres/names.js";
22
+ import { createPostgresTenantCatalog } from "../store/postgres/tenants.js";
23
+ import { tenantPaths } from "./paths.js";
24
+ import { QuarantineError, TenantConflictError } from "./quarantine.js";
25
+ import { TenantNotFoundError, TenantUnavailableError, } from "./types.js";
26
+ /** Repair instructions for a Postgres Tenant, naming its schema. */
27
+ function repairFor(code, tenantId) {
28
+ const status = `nylo tenant status ${tenantId}`;
29
+ const schema = tenantSchemaName(tenantId);
30
+ switch (code) {
31
+ case "schema-too-new":
32
+ return `${status} — schema ${schema} was migrated by a newer Runtime; run that version or newer`;
33
+ case "migration-failed":
34
+ return `${status} — inspect the Runtime log for the failed migration of ${schema}, fix it, then restart the Runtime`;
35
+ case "envelope-invalid":
36
+ return `${status} — the tenant table of schema ${schema} is missing or invalid; restore the schema from a Postgres backup`;
37
+ case "kek-missing":
38
+ return `${status} — restore vault-kek under the Tenant directory (ciphertext cannot be opened without it)`;
39
+ default:
40
+ return `${status} — inspect the Runtime log and Postgres, then restart the Runtime`;
41
+ }
42
+ }
43
+ function unavailable(tenantId, cause) {
44
+ return cause instanceof TenantUnavailableError
45
+ ? cause
46
+ : new TenantUnavailableError(tenantId, { cause });
47
+ }
48
+ function quarantined(reason, tenantId) {
49
+ return new QuarantineError({ ...reason, repair: repairFor(reason.code, tenantId) });
50
+ }
51
+ export function createPostgresTenantStore(options) {
52
+ const catalog = createPostgresTenantCatalog({
53
+ sql: options.sql,
54
+ ...(options.migrations ? { migrations: options.migrations } : {}),
55
+ onError: (error) => options.logger?.error("post-commit step failed", {
56
+ message: error instanceof Error ? error.message : String(error),
57
+ }),
58
+ });
59
+ const paths = (id) => tenantPaths(options.hostRoot, id);
60
+ function makeDirectory(id) {
61
+ const p = paths(id);
62
+ mkdirSync(p.root, { recursive: true, mode: 0o700 });
63
+ for (const dir of [p.home, p.tmp, p.sandboxes, p.pluginData, p.logs])
64
+ mkdirSync(dir, { recursive: true });
65
+ }
66
+ async function readEnvelope(id) {
67
+ try {
68
+ return await catalog.readEnvelope(id);
69
+ }
70
+ catch (error) {
71
+ if (!(error instanceof QuarantineError))
72
+ throw unavailable(id, error);
73
+ // Dropped since it was listed (deleted by another process).
74
+ if (!(await catalog.tenantExists(id).catch(() => true)))
75
+ throw new TenantNotFoundError(id);
76
+ throw quarantined(error.toQuarantine(), id);
77
+ }
78
+ }
79
+ return {
80
+ enumerate: () => catalog.listTenantIds(),
81
+ readEnvelope,
82
+ async create(envelope, bootstrap) {
83
+ makeDirectory(envelope.id);
84
+ try {
85
+ const result = await catalog.createTenant({ envelope, principals: bootstrap });
86
+ return result.status;
87
+ }
88
+ catch (error) {
89
+ // An existing Tenant with other bootstrap material; the module reports the conflict.
90
+ if (error instanceof TenantConflictError)
91
+ return "exists";
92
+ throw error;
93
+ }
94
+ },
95
+ bootstrapMatches: (id, bootstrap) => catalog.bootstrapMatches(id, bootstrap),
96
+ async open(id) {
97
+ const result = await catalog
98
+ .openTenant(id)
99
+ .catch((error) => {
100
+ throw unavailable(id, error);
101
+ });
102
+ if (result.status === "not-found")
103
+ throw new TenantNotFoundError(id);
104
+ if (result.status === "quarantined")
105
+ throw quarantined(result.reason, id);
106
+ if (result.migrated.from !== result.migrated.to)
107
+ options.logger?.info("tenant schema migrated", {
108
+ tenantId: id,
109
+ from: result.migrated.from,
110
+ to: result.migrated.to,
111
+ });
112
+ try {
113
+ makeDirectory(id);
114
+ const config = options.configFor(id);
115
+ return await options.openRuntime({ ...config, tenantId: id }, { store: result.store, envelope: result.envelope });
116
+ }
117
+ catch (error) {
118
+ await result.store.close().catch(() => undefined);
119
+ // The Runtime's own quarantines (`kek-missing`) get this store's repair wording.
120
+ const reason = error?.quarantine;
121
+ if (reason)
122
+ throw quarantined(reason, id);
123
+ if (error instanceof QuarantineError)
124
+ throw error;
125
+ // Anything else failed outside the Tenant (Postgres, arming its sweep in Restate):
126
+ // the next use tries again rather than quarantining it.
127
+ throw unavailable(id, error);
128
+ }
129
+ },
130
+ async trash(id) {
131
+ await catalog.deleteTenant(id);
132
+ rmSync(paths(id).root, { recursive: true, force: true });
133
+ },
134
+ async removePartial(id) {
135
+ if (await catalog.tenantExists(id))
136
+ return;
137
+ rmSync(paths(id).root, { recursive: true, force: true });
138
+ },
139
+ };
140
+ }
@@ -0,0 +1,126 @@
1
+ import type { SessionStore } from "../store/types.js";
2
+ import { type Relay } from "../streams/relay.js";
3
+ import { type DurableStreams } from "../streams/types.js";
4
+ import type { TenantContext } from "./context.js";
5
+ export interface WireStreamsOptions {
6
+ tenantId: string;
7
+ store: SessionStore;
8
+ streams: DurableStreams;
9
+ /** Close `streams` with the wiring (streams created for this Tenant alone). */
10
+ ownsStreams?: boolean;
11
+ /** An existing relay to use instead of creating one; the caller closes it. */
12
+ relay?: Relay;
13
+ }
14
+ /** The state of the Tenant's basin as this process last saw it. */
15
+ export interface BasinStatus {
16
+ /** `ensureTenant` succeeded and no append has failed since. */
17
+ ready: boolean;
18
+ /** Failed `ensureTenant` attempts since the basin was last ready. */
19
+ failures: number;
20
+ /** The last failure's message, cleared once the basin is ready. */
21
+ lastError: string | null;
22
+ }
23
+ /** What one `collectSessionStreams` pass did. */
24
+ export interface CollectResult {
25
+ /** Abandoned session streams deleted. */
26
+ deleted: number;
27
+ /** Session streams of existing sessions, kept. */
28
+ kept: number;
29
+ /** Deletions that failed; the collection stays pending. */
30
+ failed: number;
31
+ }
32
+ /** The handles `wireStreams` returns (also kept on `ctx.live.wiring`). */
33
+ export interface StreamsWiring {
34
+ readonly streams: DurableStreams;
35
+ readonly relay: Relay;
36
+ /** The basin's state (`streamsStatus`). */
37
+ basin(): BasinStatus;
38
+ /** Appends leftover outbox rows, at most `limit`; resolves with the number relayed. */
39
+ drain(limit?: number): Promise<number>;
40
+ /**
41
+ * Deletes abandoned session streams when a collection is pending; undefined when none was.
42
+ * One pass at a time.
43
+ */
44
+ collect(): Promise<CollectResult | undefined>;
45
+ /** Stops the readers, waits for in-flight relays, and closes what `wireStreams` created. */
46
+ close(): Promise<void>;
47
+ }
48
+ /** The Tenant setting that holds a pending collection's token ("" when none is pending). */
49
+ export declare const COLLECT_SETTING = "streams.collect";
50
+ /**
51
+ * Wires the Tenant to Durable Streams: checks the basin, creates the relay (unless given),
52
+ * starts the work and control readers, and records the handles on `ctx.live.wiring`. Call it
53
+ * once, after `ctx` is built and before the Tenant serves requests.
54
+ */
55
+ export declare function wireStreams(ctx: TenantContext, options: WireStreamsOptions): Promise<StreamsWiring>;
56
+ /** The Tenant sweep's outbox recovery: appends leftover rows, at most `limit`. */
57
+ export declare function drainOutbox(ctx: TenantContext, limit?: number): Promise<number>;
58
+ /**
59
+ * Appends `session.cancel` for `sessionId` to `tenant/control`, so the process running its
60
+ * advance aborts it. Call it from `t.afterCommit` after a cancel commits. A lost signal costs
61
+ * latency only (the advance checks the Session Store before every effect), so failures are
62
+ * logged, never thrown.
63
+ */
64
+ export declare function signalSessionCancel(ctx: TenantContext, sessionId: string): void;
65
+ /**
66
+ * Before a reset that deletes sessions: records a pending collection of their streams in the
67
+ * Tenant settings, so it is retried by the sweep even if this process stops before it runs.
68
+ */
69
+ export declare function requestStreamCollection(ctx: TenantContext): Promise<void>;
70
+ /**
71
+ * After a reset deleted sessions: tells every process with the Tenant open to check its
72
+ * session feeds (`sessions.reset`), and deletes the abandoned streams now, best effort.
73
+ * Failures are logged; the sweep retries the collection.
74
+ */
75
+ export declare function sessionStreamsAbandoned(ctx: TenantContext): void;
76
+ /**
77
+ * Deletes the Tenant's session streams that no session uses any more: streams of deleted
78
+ * sessions and of earlier incarnations. Runs only while a collection is pending
79
+ * (`requestStreamCollection`), and clears it once every deletion succeeded, unless another
80
+ * reset requested a new one meanwhile.
81
+ *
82
+ * Streams are listed before their sessions are read, so a session created meanwhile keeps
83
+ * its stream: its row committed before the relay created the stream.
84
+ */
85
+ export declare function collectSessionStreams(ctx: TenantContext, streams: DurableStreams): Promise<CollectResult | undefined>;
86
+ /** Tenant status of the streams seam (for `GET /v1/tenant` and the Admin API). */
87
+ export interface StreamsStatus {
88
+ /** The streams' service answered a probe (`probeStreams`) within the timeout. */
89
+ reachable: boolean;
90
+ basin: BasinStatus;
91
+ outbox: {
92
+ /** Events committed but not yet in their streams. */
93
+ depth: number;
94
+ /** Age of the oldest of them, or null when the outbox is empty. */
95
+ oldestAgeMs: number | null;
96
+ };
97
+ /** How far the relay is behind: the oldest unrelayed event's age, 0 when none is waiting. */
98
+ relayLagMs: number;
99
+ /** Abandoned session streams are waiting to be deleted. */
100
+ collectionPending: boolean;
101
+ }
102
+ /**
103
+ * The streams seam's status for an open Tenant: S2 reachability, the basin, outbox depth and
104
+ * relay lag. Reads the Session Store and probes the streams (at most `probeTimeoutMs`).
105
+ */
106
+ export declare function streamsStatus(ctx: TenantContext, options?: {
107
+ probeTimeoutMs?: number;
108
+ now?: () => number;
109
+ }): Promise<StreamsStatus>;
110
+ /**
111
+ * Creates a new Tenant's basin. Call it when the Tenant is created, after its schema commits;
112
+ * retries transient failures, then throws. A failure need not fail the creation: opening the
113
+ * Tenant repairs a missing basin.
114
+ */
115
+ export declare function createTenantStreams(streams: DurableStreams, tenantId: string, options?: {
116
+ attempts?: number;
117
+ }): Promise<void>;
118
+ /**
119
+ * Deletes a Tenant's basin and every stream in it. Call it when the Tenant is deleted (never
120
+ * on reset). Idempotent; retries transient failures, then throws.
121
+ */
122
+ export declare function deleteTenantStreams(streams: DurableStreams, tenantId: string, options?: {
123
+ attempts?: number;
124
+ }): Promise<void>;
125
+ /** Close: stops the readers and relay; the observers were ended by `endAllStreams`. */
126
+ export declare function closeStreams(ctx: TenantContext): Promise<void>;