@nylorun/runtime 0.13.0-beta → 0.15.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/CHANGELOG.md +434 -0
  2. package/README.md +137 -41
  3. package/dist/adapters/execution/restate.d.ts +7 -2
  4. package/dist/adapters/execution/restate.js +40 -10
  5. package/dist/adapters/replication/pgoutput.d.ts +21 -0
  6. package/dist/adapters/replication/pgoutput.js +170 -0
  7. package/dist/adapters/streams/s2.js +37 -20
  8. package/dist/admin-openapi.json +570 -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 +8065 -0
  32. package/dist/api/components.js +82 -0
  33. package/dist/api/gate/keys.d.ts +14 -0
  34. package/dist/api/gate/keys.js +59 -0
  35. package/dist/api/gate/routes.d.ts +34 -0
  36. package/dist/api/gate/routes.js +235 -0
  37. package/dist/api/http/app.d.ts +18 -0
  38. package/dist/api/http/app.js +58 -0
  39. package/dist/api/http/body.d.ts +4 -0
  40. package/dist/api/http/body.js +37 -0
  41. package/dist/api/http/define.d.ts +39 -0
  42. package/dist/api/http/define.js +122 -0
  43. package/dist/api/http/respond.d.ts +9 -0
  44. package/dist/api/http/respond.js +44 -0
  45. package/dist/api/http/routes/access.d.ts +24 -0
  46. package/dist/api/http/routes/access.js +266 -0
  47. package/dist/api/http/routes/actions.d.ts +8 -0
  48. package/dist/api/http/routes/actions.js +76 -0
  49. package/dist/api/http/routes/endpoints.d.ts +7 -0
  50. package/dist/api/http/routes/endpoints.js +100 -0
  51. package/dist/api/http/routes/sessions.d.ts +3 -0
  52. package/dist/api/http/routes/sessions.js +205 -0
  53. package/dist/api/http/routes/tenant.d.ts +11 -0
  54. package/dist/api/http/routes/tenant.js +244 -0
  55. package/dist/api/http/routes/vaults.d.ts +8 -0
  56. package/dist/api/http/routes/vaults.js +167 -0
  57. package/dist/api/openapi.d.ts +19 -0
  58. package/dist/api/openapi.js +137 -0
  59. package/dist/api/route.d.ts +12 -0
  60. package/dist/api/route.js +4 -0
  61. package/dist/core/bearer.d.ts +8 -0
  62. package/dist/core/bearer.js +11 -0
  63. package/dist/core/flow-host.d.ts +7 -15
  64. package/dist/core/flow-host.js +16 -41
  65. package/dist/core/sandbox-routes.d.ts +12 -7
  66. package/dist/core/sandbox-routes.js +24 -25
  67. package/dist/execution/memory.d.ts +8 -5
  68. package/dist/execution/memory.js +33 -13
  69. package/dist/execution/types.d.ts +38 -6
  70. package/dist/execution/types.js +12 -3
  71. package/dist/gates/contract.d.ts +51 -0
  72. package/dist/gates/contract.js +37 -0
  73. package/dist/gates/handler.d.ts +21 -0
  74. package/dist/gates/handler.js +55 -0
  75. package/dist/gates/http-client.d.ts +12 -0
  76. package/dist/gates/http-client.js +160 -0
  77. package/dist/gates/in-process.d.ts +32 -0
  78. package/dist/gates/in-process.js +42 -0
  79. package/dist/gates/inflight.d.ts +39 -0
  80. package/dist/gates/inflight.js +70 -0
  81. package/dist/gates/mcp-handler.d.ts +35 -0
  82. package/dist/gates/mcp-handler.js +142 -0
  83. package/dist/gates/meter.d.ts +24 -0
  84. package/dist/gates/meter.js +165 -0
  85. package/dist/gates/model-gate.d.ts +45 -0
  86. package/dist/gates/model-gate.js +1 -0
  87. package/dist/gates/tenant-vaults.d.ts +64 -0
  88. package/dist/gates/tenant-vaults.js +94 -0
  89. package/dist/gates/tool-calls.d.ts +51 -0
  90. package/dist/gates/tool-calls.js +64 -0
  91. package/dist/gates/tool-client.d.ts +12 -0
  92. package/dist/gates/tool-client.js +201 -0
  93. package/dist/gates/tool-contract.d.ts +116 -0
  94. package/dist/gates/tool-contract.js +64 -0
  95. package/dist/gates/tool-gate.d.ts +45 -0
  96. package/dist/gates/tool-gate.js +10 -0
  97. package/dist/host/admin-api.d.ts +23 -0
  98. package/dist/host/admin-api.js +74 -0
  99. package/dist/host/app.d.ts +34 -0
  100. package/dist/host/app.js +181 -0
  101. package/dist/host/config-for.d.ts +3 -1
  102. package/dist/host/config-for.js +3 -2
  103. package/dist/host/config.d.ts +11 -0
  104. package/dist/host/cors.d.ts +27 -0
  105. package/dist/host/cors.js +64 -0
  106. package/dist/host/create-host.d.ts +36 -8
  107. package/dist/host/create-host.js +161 -330
  108. package/dist/host/execution.d.ts +20 -18
  109. package/dist/host/execution.js +2 -2
  110. package/dist/host/gates.d.ts +58 -0
  111. package/dist/host/gates.js +127 -0
  112. package/dist/host/http.d.ts +20 -4
  113. package/dist/host/http.js +81 -23
  114. package/dist/host/index.d.ts +1 -1
  115. package/dist/host/main.js +189 -58
  116. package/dist/host/stack-config.d.ts +91 -19
  117. package/dist/host/stack-config.js +246 -17
  118. package/dist/infra/database.d.ts +2 -2
  119. package/dist/infra/execution.d.ts +5 -5
  120. package/dist/infra/execution.js +8 -6
  121. package/dist/infra/index.d.ts +1 -1
  122. package/dist/infra/workers.d.ts +17 -17
  123. package/dist/infra/workers.js +9 -9
  124. package/dist/keys/client.d.ts +9 -0
  125. package/dist/keys/client.js +70 -0
  126. package/dist/keys/contract.d.ts +24 -0
  127. package/dist/keys/contract.js +12 -0
  128. package/dist/keys/keys.d.ts +41 -0
  129. package/dist/keys/keys.js +50 -0
  130. package/dist/mcp/connect.d.ts +43 -3
  131. package/dist/mcp/connect.js +12 -5
  132. package/dist/mcp/pool.d.ts +39 -3
  133. package/dist/mcp/pool.js +74 -11
  134. package/dist/model/catalog.d.ts +2 -10
  135. package/dist/model/classify.d.ts +13 -0
  136. package/dist/model/classify.js +61 -0
  137. package/dist/model/models.d.ts +10 -1
  138. package/dist/model/models.js +14 -6
  139. package/dist/model/pi-model.d.ts +17 -2
  140. package/dist/model/pi-model.js +366 -222
  141. package/dist/openapi.json +25889 -0
  142. package/dist/record/append.d.ts +51 -0
  143. package/dist/record/append.js +20 -0
  144. package/dist/record/envelope.d.ts +19 -0
  145. package/dist/record/envelope.js +49 -0
  146. package/dist/record/index.d.ts +11 -0
  147. package/dist/record/index.js +11 -0
  148. package/dist/sandbox/manager.d.ts +13 -1
  149. package/dist/sandbox/manager.js +8 -11
  150. package/dist/sandbox/resolve.d.ts +33 -0
  151. package/dist/sandbox/resolve.js +81 -0
  152. package/dist/sandbox/session-sandbox.d.ts +11 -0
  153. package/dist/sandbox/session-sandbox.js +51 -0
  154. package/dist/sandbox/share.d.ts +14 -2
  155. package/dist/sandbox/share.js +33 -3
  156. package/dist/sandbox/tenant-config.d.ts +37 -0
  157. package/dist/sandbox/tenant-config.js +51 -0
  158. package/dist/sandbox/tools.d.ts +7 -9
  159. package/dist/sandbox/tools.js +11 -3
  160. package/dist/sandbox/types.d.ts +8 -2
  161. package/dist/store/postgres/connect.d.ts +25 -9
  162. package/dist/store/postgres/connect.js +27 -10
  163. package/dist/store/postgres/db.d.ts +16 -0
  164. package/dist/store/postgres/db.js +28 -0
  165. package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
  166. package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
  167. package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
  168. package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
  169. package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
  170. package/dist/store/postgres/migrate.d.ts +68 -0
  171. package/dist/store/postgres/migrate.js +183 -0
  172. package/dist/store/postgres/record-writer.d.ts +4 -0
  173. package/dist/store/postgres/record-writer.js +34 -0
  174. package/dist/store/postgres/record.d.ts +5 -0
  175. package/dist/store/postgres/record.js +60 -0
  176. package/dist/store/postgres/schema.d.ts +3096 -0
  177. package/dist/store/postgres/schema.js +363 -0
  178. package/dist/store/postgres/store.d.ts +2 -4
  179. package/dist/store/postgres/store.js +590 -434
  180. package/dist/store/postgres/tenant.d.ts +56 -0
  181. package/dist/store/postgres/tenant.js +154 -0
  182. package/dist/store/types.d.ts +183 -132
  183. package/dist/store/types.js +0 -1
  184. package/dist/streams/basin.d.ts +18 -1
  185. package/dist/streams/basin.js +34 -2
  186. package/dist/streams/control.d.ts +18 -0
  187. package/dist/streams/control.js +33 -0
  188. package/dist/streams/memory.d.ts +3 -1
  189. package/dist/streams/memory.js +14 -8
  190. package/dist/streams/relay/core.d.ts +35 -0
  191. package/dist/streams/relay/core.js +366 -0
  192. package/dist/streams/relay/types.d.ts +78 -0
  193. package/dist/streams/relay/types.js +8 -0
  194. package/dist/streams/types.d.ts +71 -36
  195. package/dist/streams/types.js +18 -53
  196. package/dist/tenant/access-policy.d.ts +30 -0
  197. package/dist/tenant/access-policy.js +52 -0
  198. package/dist/tenant/actions.d.ts +10 -149
  199. package/dist/tenant/actions.js +5 -167
  200. package/dist/tenant/advance.d.ts +7 -2
  201. package/dist/tenant/advance.js +125 -14
  202. package/dist/tenant/auth.d.ts +19 -17
  203. package/dist/tenant/auth.js +90 -93
  204. package/dist/tenant/browser.d.ts +15 -0
  205. package/dist/tenant/browser.js +39 -0
  206. package/dist/tenant/cause.d.ts +25 -0
  207. package/dist/tenant/cause.js +45 -0
  208. package/dist/tenant/commands.d.ts +18 -2
  209. package/dist/tenant/commands.js +111 -115
  210. package/dist/tenant/context.d.ts +101 -21
  211. package/dist/tenant/context.js +14 -10
  212. package/dist/tenant/delivery-token.d.ts +41 -0
  213. package/dist/tenant/delivery-token.js +104 -0
  214. package/dist/tenant/delivery.d.ts +62 -0
  215. package/dist/tenant/delivery.js +481 -0
  216. package/dist/tenant/effects.d.ts +20 -1
  217. package/dist/tenant/effects.js +159 -42
  218. package/dist/tenant/endpoints.d.ts +67 -0
  219. package/dist/tenant/endpoints.js +90 -0
  220. package/dist/tenant/ephemeral.d.ts +22 -7
  221. package/dist/tenant/ephemeral.js +83 -125
  222. package/dist/tenant/history.d.ts +64 -0
  223. package/dist/tenant/history.js +120 -0
  224. package/dist/tenant/http.d.ts +8 -9
  225. package/dist/tenant/http.js +13 -27
  226. package/dist/tenant/jwt.d.ts +16 -0
  227. package/dist/tenant/jwt.js +50 -0
  228. package/dist/tenant/model-setting.d.ts +2 -2
  229. package/dist/tenant/module.d.ts +17 -28
  230. package/dist/tenant/module.js +133 -279
  231. package/dist/tenant/outbound.d.ts +44 -0
  232. package/dist/tenant/outbound.js +127 -0
  233. package/dist/tenant/paths.d.ts +11 -16
  234. package/dist/tenant/paths.js +24 -68
  235. package/dist/tenant/pool.d.ts +1 -3
  236. package/dist/tenant/pool.js +1 -19
  237. package/dist/tenant/principals.d.ts +23 -11
  238. package/dist/tenant/principals.js +61 -49
  239. package/dist/tenant/reset.d.ts +0 -4
  240. package/dist/tenant/reset.js +0 -5
  241. package/dist/tenant/runtime.d.ts +28 -20
  242. package/dist/tenant/runtime.js +95 -64
  243. package/dist/tenant/scheduler.d.ts +10 -3
  244. package/dist/tenant/scheduler.js +9 -5
  245. package/dist/tenant/session-streams.d.ts +127 -0
  246. package/dist/tenant/session-streams.js +404 -0
  247. package/dist/tenant/sessions.d.ts +28 -4
  248. package/dist/tenant/sessions.js +118 -24
  249. package/dist/tenant/signing-keys.d.ts +55 -0
  250. package/dist/tenant/signing-keys.js +172 -0
  251. package/dist/tenant/slim.d.ts +8 -0
  252. package/dist/tenant/slim.js +21 -0
  253. package/dist/tenant/status.d.ts +3 -3
  254. package/dist/tenant/status.js +20 -13
  255. package/dist/tenant/store-pg.d.ts +9 -6
  256. package/dist/tenant/store-pg.js +60 -125
  257. package/dist/tenant/streams.d.ts +62 -85
  258. package/dist/tenant/streams.js +143 -226
  259. package/dist/tenant/subject-limits.d.ts +16 -0
  260. package/dist/tenant/subject-limits.js +45 -0
  261. package/dist/tenant/sweep.d.ts +1 -10
  262. package/dist/tenant/sweep.js +29 -69
  263. package/dist/tenant/tokens.d.ts +14 -0
  264. package/dist/tenant/tokens.js +180 -0
  265. package/dist/tenant/transcript.d.ts +48 -5
  266. package/dist/tenant/transcript.js +58 -1
  267. package/dist/tenant/types.d.ts +69 -98
  268. package/dist/tenant/types.js +4 -15
  269. package/dist/tenant/worker.d.ts +3 -1
  270. package/dist/tenant/worker.js +13 -7
  271. package/dist/vault/host-model.d.ts +31 -0
  272. package/dist/vault/host-model.js +57 -0
  273. package/dist/vault/service.d.ts +44 -17
  274. package/dist/vault/service.js +126 -121
  275. package/dist/version.d.ts +1 -1
  276. package/dist/version.js +1 -1
  277. package/package.json +25 -9
  278. package/dist/core/executors.d.ts +0 -44
  279. package/dist/core/executors.js +0 -95
  280. package/dist/store/memory.d.ts +0 -57
  281. package/dist/store/memory.js +0 -645
  282. package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
  283. package/dist/store/postgres/migrations/001_initial.js +0 -196
  284. package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
  285. package/dist/store/postgres/migrations/002_session_owner.js +0 -13
  286. package/dist/store/postgres/migrations/index.d.ts +0 -49
  287. package/dist/store/postgres/migrations/index.js +0 -76
  288. package/dist/store/postgres/names.d.ts +0 -11
  289. package/dist/store/postgres/names.js +0 -42
  290. package/dist/store/postgres/tenants.d.ts +0 -90
  291. package/dist/store/postgres/tenants.js +0 -214
  292. package/dist/streams/relay.d.ts +0 -37
  293. package/dist/streams/relay.js +0 -254
  294. package/dist/tenant/envelope.d.ts +0 -7
  295. package/dist/tenant/envelope.js +0 -22
  296. package/dist/tenant/live.d.ts +0 -92
  297. package/dist/tenant/live.js +0 -290
  298. package/dist/tenant/quarantine-error.d.ts +0 -6
  299. package/dist/tenant/quarantine-error.js +0 -9
  300. package/dist/tenant/quarantine.d.ts +0 -27
  301. package/dist/tenant/quarantine.js +0 -72
  302. package/dist/tenant/routes-tenant.d.ts +0 -12
  303. package/dist/tenant/routes-tenant.js +0 -137
  304. package/dist/tenant/routes.d.ts +0 -9
  305. package/dist/tenant/routes.js +0 -151
  306. package/dist/tenant/store-memory.d.ts +0 -16
  307. package/dist/tenant/store-memory.js +0 -111
  308. /package/dist/{store → record}/cursor.d.ts +0 -0
  309. /package/dist/{store → record}/cursor.js +0 -0
@@ -1,38 +1,27 @@
1
- import { isQuarantineError, quarantine, TenantBusyError, TenantConflictError } from "./quarantine.js";
2
- import { TenantNotFoundError, TenantUnavailableError, type Logger, type OpenTenantRuntime, type TenantConfig, type TenantModule, type TenantStore } from "./types.js";
1
+ import type { StreamRelayStatus } from "@nylorun/core/contracts";
2
+ import { TenantUnavailableError, type Logger, type TenantHandle, type TenantModule, type TenantOpener } from "./types.js";
3
3
  export interface CreateTenantModuleOptions {
4
- store: TenantStore;
5
- /** Unused: the store derives paths. Accepted so callers can pass the store's options. */
6
- hostRoot?: string;
7
- /** Unused: the store opens Tenants. Accepted so callers can pass the store's options. */
8
- openRuntime?: OpenTenantRuntime;
9
- /** Unused; see `openRuntime`. */
10
- configFor?: (tenantId: string) => TenantConfig;
4
+ /** Opens the Host's Tenant (`store-pg.ts`). */
5
+ open: TenantOpener;
11
6
  logger: Logger;
12
- /** Bound on one Tenant open; a slower open quarantines it with `open-timeout`. */
7
+ /** Bound on opening the Tenant; a slower open fails with `open-timeout`. */
13
8
  openTimeoutMs?: number;
14
9
  /**
15
- * Runs after a Tenant is created in the store, before it opens: creates what lives outside
16
- * it (its Durable Streams basin). A failure is logged; opening the Tenant repairs it.
10
+ * Runs once the Tenant is open, at start or on a later retry: the Host starts what needs the
11
+ * Tenant's id (its stream relay). A failure is logged; the Tenant stays open.
17
12
  */
18
- onCreated?: (tenantId: string) => Promise<void>;
19
- /**
20
- * Runs after a Tenant is removed from the store: removes what lives outside it (its
21
- * Durable Streams basin, its armed sweep). A failure is logged; the Tenant stays deleted.
22
- */
23
- onDeleted?: (tenantId: string) => Promise<void>;
13
+ onOpen?: (handle: TenantHandle) => void | Promise<void>;
14
+ /** This process's stream relay, for the Host aggregate (a Host with S2 runs one). */
15
+ relayStatus?: () => Promise<StreamRelayStatus>;
24
16
  }
25
17
  /**
26
- * Deep Tenant module (§8): discovery, create, resolve, list, delete and summarize over an
27
- * injected `TenantStore`.
18
+ * Deep Tenant module (§8): the Host's one Tenant over an injected opener (tenancy.md §5).
28
19
  *
29
- * Handles open on demand and are cached (architecture §8.2). The first call that needs a
30
- * Tenant (`resolve`, `status`, `worker`, `delete`) opens it once, even when several ask at
31
- * the same time; a failed open is cached as a quarantine, and a missing Tenant is not
32
- * cached. A failure outside the Tenant (`TenantUnavailableError`: the database is down, its
33
- * sweep could not be armed) is thrown to the caller and not cached either, so the next use
34
- * tries again. A Tenant being deleted resolves as not found, so a late request or Worker
35
- * call cannot reopen it while its storage is removed.
20
+ * `start` opens it once, even when requests or Worker calls ask at the same time. An open that
21
+ * fails for a reason in the Tenant (a `TenantOpenError`, or a timeout) is kept as the Tenant's
22
+ * cause: the Host is not ready, `/v1/admin/status` names it, requests get the opaque 404.
23
+ * One that fails outside the Tenant (`TenantUnavailableError`) is retried in the background
24
+ * with backoff, and by the next request, which gets 503 meanwhile.
36
25
  */
37
26
  export declare function createTenantModule(options: CreateTenantModuleOptions): TenantModule;
38
- export { TenantBusyError, TenantConflictError, TenantNotFoundError, TenantUnavailableError, isQuarantineError, quarantine, };
27
+ export { TenantUnavailableError };
@@ -1,322 +1,176 @@
1
- import { isTenantId } from "@nylorun/core/compatibility";
2
- import { POSTGRES_SCHEMA_VERSION } from "../store/postgres/migrations/index.js";
3
- import { envelopeNow } from "./envelope.js";
1
+ import { openError, TenantOpenError } from "./cause.js";
4
2
  import { TimeoutError, withTimeout } from "./pool.js";
5
- import { asQuarantine, isQuarantineError, quarantine, TenantBusyError, TenantConflictError, } from "./quarantine.js";
6
- import { TenantNotFoundError, TenantUnavailableError, } from "./types.js";
3
+ import { TenantUnavailableError, } from "./types.js";
7
4
  const OPEN_TIMEOUT_MS = 30_000;
8
- function hasLiveWork(summary) {
9
- return (summary.runningSessions > 0 ||
10
- summary.connectedExecutors > 0 ||
11
- summary.pendingActions > 0);
12
- }
13
- function toAdmin(id, entry, envelope) {
14
- if (entry?.kind === "open") {
15
- return {
16
- id,
17
- name: entry.handle.envelope.name,
18
- state: "open",
19
- envelope: entry.handle.envelope,
20
- };
21
- }
22
- return {
23
- id,
24
- name: envelope?.name ?? null,
25
- state: entry ? "quarantined" : "open",
26
- envelope,
27
- };
28
- }
29
- /** The quarantine an open failure carries, from either quarantine error type. */
30
- function quarantineOf(error) {
31
- const carried = error?.quarantine;
32
- if (carried && typeof carried === "object" && "code" in carried)
33
- return carried;
34
- return asQuarantine(error);
35
- }
5
+ /** Retries of an open that failed outside the Tenant: from 1 s, doubling, at most 30 s apart. */
6
+ const RETRY_FIRST_MS = 1_000;
7
+ const RETRY_MAX_MS = 30_000;
36
8
  /**
37
- * Deep Tenant module (§8): discovery, create, resolve, list, delete and summarize over an
38
- * injected `TenantStore`.
9
+ * Deep Tenant module (§8): the Host's one Tenant over an injected opener (tenancy.md §5).
39
10
  *
40
- * Handles open on demand and are cached (architecture §8.2). The first call that needs a
41
- * Tenant (`resolve`, `status`, `worker`, `delete`) opens it once, even when several ask at
42
- * the same time; a failed open is cached as a quarantine, and a missing Tenant is not
43
- * cached. A failure outside the Tenant (`TenantUnavailableError`: the database is down, its
44
- * sweep could not be armed) is thrown to the caller and not cached either, so the next use
45
- * tries again. A Tenant being deleted resolves as not found, so a late request or Worker
46
- * call cannot reopen it while its storage is removed.
11
+ * `start` opens it once, even when requests or Worker calls ask at the same time. An open that
12
+ * fails for a reason in the Tenant (a `TenantOpenError`, or a timeout) is kept as the Tenant's
13
+ * cause: the Host is not ready, `/v1/admin/status` names it, requests get the opaque 404.
14
+ * One that fails outside the Tenant (`TenantUnavailableError`) is retried in the background
15
+ * with backoff, and by the next request, which gets 503 meanwhile.
47
16
  */
48
17
  export function createTenantModule(options) {
49
- const { store, logger, openTimeoutMs = OPEN_TIMEOUT_MS } = options;
50
- const entries = new Map();
51
- const opening = new Map();
52
- const deleting = new Set();
53
- let started = false;
54
- let closed = false;
55
- function rememberQuarantine(id, q) {
56
- const entry = { kind: "quarantined", quarantine: q };
57
- entries.set(id, entry);
58
- logger.warn("tenant quarantined", {
59
- tenantId: id,
60
- code: q.code,
61
- repair: q.repair,
62
- });
63
- return entry;
64
- }
65
- async function openEntry(id) {
18
+ const { logger, openTimeoutMs = OPEN_TIMEOUT_MS } = options;
19
+ let state = { kind: "idle" };
20
+ let opening;
21
+ let retry;
22
+ let nextDelayMs = RETRY_FIRST_MS;
23
+ const current = () => state;
24
+ async function openOnce() {
66
25
  let handle;
67
26
  try {
68
- handle = await withTimeout(store.open(id), openTimeoutMs, (late) => {
27
+ handle = await withTimeout(options.open(), openTimeoutMs, (late) => {
69
28
  void late.then((h) => h.close()).catch(() => undefined);
70
29
  });
71
30
  }
72
31
  catch (error) {
73
- if (error instanceof TenantNotFoundError)
74
- return undefined;
75
- if (closed || deleting.has(id))
76
- return undefined;
32
+ if (current().kind === "closed")
33
+ return;
77
34
  if (error instanceof TenantUnavailableError) {
78
- // Not the Tenant's fault: not cached, so the next use opens it again.
35
+ state = { kind: "unavailable", error };
79
36
  logger.warn("tenant unavailable", {
80
- tenantId: id,
81
37
  message: error.cause instanceof Error ? error.cause.message : String(error.cause),
38
+ retryMs: nextDelayMs,
82
39
  });
83
- throw error;
40
+ scheduleRetry();
41
+ return;
84
42
  }
85
- if (error instanceof TimeoutError)
86
- return rememberQuarantine(id, quarantine("open-timeout", `open timed out after ${openTimeoutMs}ms`, {
87
- tenantId: id,
88
- }).toQuarantine());
89
- return rememberQuarantine(id, quarantineOf(error) ??
90
- quarantine("open-failed", error instanceof Error ? error.message : "open failed", { tenantId: id }).toQuarantine());
43
+ const failure = error instanceof TenantOpenError
44
+ ? error
45
+ : error instanceof TimeoutError
46
+ ? openError("open-timeout", `Opening the Tenant timed out after ${openTimeoutMs}ms`)
47
+ : openError("open-failed", error instanceof Error ? error.message : "Opening the Tenant failed");
48
+ const failed = {
49
+ kind: "failed",
50
+ cause: failure.toCause(),
51
+ envelope: failure.envelope ?? null,
52
+ };
53
+ state = failed;
54
+ logger.error("tenant not opened", {
55
+ ...(failed.envelope ? { tenantId: failed.envelope.id } : {}),
56
+ code: failed.cause.code,
57
+ message: failed.cause.message,
58
+ repair: failed.cause.repair,
59
+ });
60
+ return;
91
61
  }
92
- if (closed || deleting.has(id)) {
62
+ if (current().kind === "closed") {
93
63
  await handle.close().catch(() => undefined);
94
- return undefined;
64
+ return;
95
65
  }
96
- const entry = { kind: "open", handle };
97
- entries.set(id, entry);
98
- return entry;
99
- }
100
- /** The cached entry, or the result of opening the Tenant once. */
101
- async function load(id) {
102
- if (deleting.has(id))
103
- return undefined;
104
- const cached = entries.get(id);
105
- if (cached)
106
- return cached;
107
- if (closed)
108
- return undefined;
109
- let pending = opening.get(id);
110
- if (!pending) {
111
- pending = openEntry(id).finally(() => opening.delete(id));
112
- opening.set(id, pending);
66
+ state = { kind: "open", handle };
67
+ nextDelayMs = RETRY_FIRST_MS;
68
+ logger.info("tenant opened", { tenantId: handle.envelope.id });
69
+ try {
70
+ await options.onOpen?.(handle);
113
71
  }
114
- return pending;
72
+ catch (error) {
73
+ logger.warn("tenant open step failed", {
74
+ tenantId: handle.envelope.id,
75
+ message: error instanceof Error ? error.message : String(error),
76
+ });
77
+ }
78
+ }
79
+ /** Opens the Tenant unless it is open, failed or closed; one open at a time. */
80
+ function load() {
81
+ const { kind } = current();
82
+ if (kind !== "idle" && kind !== "unavailable")
83
+ return Promise.resolve();
84
+ clearTimeout(retry);
85
+ retry = undefined;
86
+ opening ??= openOnce().finally(() => {
87
+ opening = undefined;
88
+ });
89
+ return opening;
90
+ }
91
+ function scheduleRetry() {
92
+ if (retry)
93
+ return;
94
+ const delayMs = nextDelayMs;
95
+ nextDelayMs = Math.min(delayMs * 2, RETRY_MAX_MS);
96
+ retry = setTimeout(() => {
97
+ retry = undefined;
98
+ void load();
99
+ }, delayMs);
100
+ retry.unref();
115
101
  }
116
- const openEntries = () => [...entries.values()].filter((e) => e.kind === "open");
117
102
  const module = {
118
- get started() {
119
- return started;
103
+ get ready() {
104
+ return current().kind === "open";
120
105
  },
121
106
  async start() {
122
- if (closed)
107
+ if (current().kind === "closed")
123
108
  throw new Error("Tenant module is closed");
124
- const ids = await store.enumerate();
125
- started = true;
126
- logger.info("tenant module started", { tenants: ids.length });
109
+ await load();
127
110
  },
128
- async resolve(id) {
129
- if (!isTenantId(id))
130
- return { kind: "not-found" };
131
- const entry = await load(id);
132
- if (!entry)
133
- return { kind: "not-found" };
134
- if (entry.kind === "open")
135
- return { kind: "open", handle: entry.handle };
136
- return { kind: "quarantined", quarantine: entry.quarantine };
111
+ async resolve() {
112
+ await load();
113
+ const now = current();
114
+ switch (now.kind) {
115
+ case "open":
116
+ return { kind: "open", handle: now.handle };
117
+ case "failed":
118
+ return { kind: "unavailable", cause: now.cause };
119
+ case "unavailable":
120
+ throw now.error;
121
+ default:
122
+ return { kind: "unavailable" };
123
+ }
137
124
  },
138
125
  async worker(id) {
139
- if (!isTenantId(id))
140
- return undefined;
141
- const entry = await load(id);
142
- return entry?.kind === "open" ? entry.handle.worker : undefined;
126
+ await load();
127
+ const now = current();
128
+ return now.kind === "open" && now.handle.envelope.id === id
129
+ ? now.handle.worker
130
+ : undefined;
143
131
  },
144
- async create(input) {
145
- if (!isTenantId(input.tenantId)) {
146
- throw new Error(`Invalid tenant id: ${input.tenantId}`);
147
- }
148
- const bootstrap = {
149
- principalId: input.principalId,
150
- credentialHash: input.credentialHash,
151
- idempotencyKey: input.idempotencyKey,
152
- ...(input.studioCredentialHash
153
- ? { studioCredentialHash: input.studioCredentialHash }
154
- : {}),
155
- ...(input.derivedPrincipals?.length
156
- ? { derivedPrincipals: input.derivedPrincipals }
157
- : {}),
158
- };
159
- // The store records its own schema version.
160
- const envelope = envelopeNow({
161
- id: input.tenantId,
162
- name: input.name,
163
- schemaVersion: POSTGRES_SCHEMA_VERSION,
164
- });
165
- let outcome;
166
- try {
167
- outcome = await store.create(envelope, bootstrap);
168
- }
169
- catch (error) {
170
- await store.removePartial(input.tenantId).catch(() => undefined);
171
- throw error;
172
- }
173
- if (outcome === "exists") {
174
- const matches = await store.bootstrapMatches(input.tenantId, bootstrap);
175
- if (!matches)
176
- throw new TenantConflictError();
177
- const existing = await store.readEnvelope(input.tenantId);
178
- await load(input.tenantId).catch(() => undefined);
179
- return { envelope: existing, created: false };
180
- }
181
- await options.onCreated?.(input.tenantId).catch((error) => logger.warn("tenant create step failed; open repairs it", {
182
- tenantId: input.tenantId,
183
- message: error instanceof Error ? error.message : String(error),
184
- }));
185
- const opened = await load(input.tenantId).catch(() => undefined);
186
- if (opened?.kind === "open") {
187
- return { envelope: opened.handle.envelope, created: true };
188
- }
189
- // Created but failed to open: report the stored envelope.
190
- return {
191
- envelope: await store
192
- .readEnvelope(input.tenantId)
193
- .catch(() => envelope),
194
- created: true,
195
- };
196
- },
197
- async list() {
198
- const ids = new Set(await store.enumerate());
199
- for (const id of entries.keys())
200
- ids.add(id);
201
- const result = [];
202
- for (const id of [...ids].sort()) {
203
- if (!isTenantId(id) || deleting.has(id))
204
- continue;
205
- let entry = entries.get(id);
206
- if (entry?.kind === "open") {
207
- result.push(toAdmin(id, entry, null));
208
- continue;
209
- }
210
- let envelope = null;
211
- try {
212
- envelope = await store.readEnvelope(id);
213
- if (envelope.id !== id) {
214
- entry = {
215
- kind: "quarantined",
216
- quarantine: quarantine("envelope-invalid", `Tenant envelope id ${envelope.id} does not match ${id}`, { tenantId: id }).toQuarantine(),
217
- };
218
- envelope = null;
219
- }
132
+ tenant() {
133
+ const now = current();
134
+ switch (now.kind) {
135
+ case "open": {
136
+ const { envelope } = now.handle;
137
+ return { id: envelope.id, name: envelope.name, state: "open", envelope };
220
138
  }
221
- catch (error) {
222
- // Deleted since it was enumerated.
223
- if (error instanceof TenantNotFoundError)
224
- continue;
225
- if (error instanceof TenantUnavailableError)
226
- throw error;
227
- // Listed as quarantined; only opening it caches a quarantine.
228
- entry ??= {
229
- kind: "quarantined",
230
- quarantine: quarantineOf(error) ??
231
- quarantine("envelope-invalid", "Tenant envelope is unreadable", {
232
- tenantId: id,
233
- }).toQuarantine(),
139
+ case "failed":
140
+ return {
141
+ id: now.envelope?.id ?? null,
142
+ name: now.envelope?.name ?? null,
143
+ state: "unavailable",
144
+ envelope: now.envelope,
145
+ cause: now.cause,
234
146
  };
235
- }
236
- result.push(toAdmin(id, entry, envelope));
237
- }
238
- return result;
239
- },
240
- async status(id) {
241
- if (!isTenantId(id))
242
- return undefined;
243
- const entry = await load(id);
244
- if (!entry)
245
- return undefined;
246
- if (entry.kind === "open")
247
- return toAdmin(id, entry, null);
248
- const envelope = await store.readEnvelope(id).catch(() => null);
249
- return { ...toAdmin(id, entry, envelope), quarantine: entry.quarantine };
250
- },
251
- async delete(id, activeWork) {
252
- if (!isTenantId(id))
253
- throw new Error(`Invalid tenant id: ${id}`);
254
- if (deleting.has(id))
255
- throw new TenantBusyError("Tenant is being deleted");
256
- const entry = await load(id);
257
- if (!entry)
258
- throw new TenantNotFoundError(id);
259
- if (entry.kind === "open") {
260
- const summary = await entry.handle.summary();
261
- if (activeWork === "refuse" && hasLiveWork(summary)) {
262
- throw new TenantBusyError();
263
- }
264
- if (activeWork === "drain" || activeWork === "cancel") {
265
- await entry.handle.drain(activeWork);
266
- }
267
- }
268
- deleting.add(id);
269
- try {
270
- entries.delete(id);
271
- // Sandbox prefix cleanup is owned by TenantHandle.close().
272
- if (entry.kind === "open")
273
- await entry.handle.close();
274
- await store.trash(id, new Date());
275
- }
276
- finally {
277
- deleting.delete(id);
147
+ default:
148
+ return { id: null, name: null, state: "unavailable", envelope: null };
278
149
  }
279
- logger.info("tenant deleted", { tenantId: id, activeWork });
280
- await options.onDeleted?.(id).catch((error) => logger.warn("tenant delete cleanup failed", {
281
- tenantId: id,
282
- message: error instanceof Error ? error.message : String(error),
283
- }));
284
150
  },
285
151
  async summarize() {
286
- let runningSessions = 0;
287
- let connectedExecutors = 0;
288
- let pendingActions = 0;
289
- let uncertainEffects = 0;
290
- let outboxDepth;
291
- let relayLagMs;
292
- for (const s of await Promise.all(openEntries().map((e) => e.handle.summary()))) {
293
- runningSessions += s.runningSessions;
294
- connectedExecutors += s.connectedExecutors;
295
- pendingActions += s.pendingActions;
296
- uncertainEffects += s.uncertainEffects;
297
- if (s.outboxDepth !== undefined)
298
- outboxDepth = (outboxDepth ?? 0) + s.outboxDepth;
299
- if (s.relayLagMs !== undefined)
300
- relayLagMs = Math.max(relayLagMs ?? 0, s.relayLagMs);
301
- }
152
+ const now = current();
153
+ const summary = now.kind === "open" ? await now.handle.summary() : undefined;
154
+ const relay = await options.relayStatus?.().catch(() => undefined);
302
155
  return {
303
- runningSessions,
304
- connectedExecutors,
305
- pendingActions,
306
- uncertainEffects,
307
- ...(outboxDepth !== undefined ? { outboxDepth } : {}),
308
- ...(relayLagMs !== undefined ? { relayLagMs } : {}),
156
+ runningSessions: summary?.runningSessions ?? 0,
157
+ inFlightDeliveries: summary?.inFlightDeliveries ?? 0,
158
+ pendingActions: summary?.pendingActions ?? 0,
159
+ uncertainEffects: summary?.uncertainEffects ?? 0,
160
+ ...(relay ? { relay } : {}),
309
161
  };
310
162
  },
311
163
  async close() {
312
- closed = true;
313
- started = false;
314
- await Promise.allSettled([...opening.values()]);
315
- const open = openEntries();
316
- entries.clear();
317
- await Promise.all(open.map((e) => e.handle.close().catch(() => undefined)));
164
+ const previous = current();
165
+ state = { kind: "closed" };
166
+ clearTimeout(retry);
167
+ retry = undefined;
168
+ // An open in progress sees `closed` and closes what it opened.
169
+ await opening?.catch(() => undefined);
170
+ if (previous.kind === "open")
171
+ await previous.handle.close().catch(() => undefined);
318
172
  },
319
173
  };
320
174
  return module;
321
175
  }
322
- export { TenantBusyError, TenantConflictError, TenantNotFoundError, TenantUnavailableError, isQuarantineError, quarantine, };
176
+ export { TenantUnavailableError };
@@ -0,0 +1,44 @@
1
+ import { type IncomingHttpHeaders } from "node:http";
2
+ /** How the Host lets the Runtime call developer URLs (Host settings, `TenantConfig.delivery`). */
3
+ export interface OutboundPolicy {
4
+ /** `http` URLs as well as `https`. Default true (OSS). */
5
+ allowHttp?: boolean;
6
+ /** Private, loopback and link-local addresses. Default `allow` (OSS); Cloud refuses. */
7
+ privateAddresses?: "allow" | "refuse";
8
+ /**
9
+ * `docker-host`: the Runtime runs in the local stack's container, so `localhost`,
10
+ * `127.0.0.1` and `[::1]` mean the machine that runs Docker (`host.docker.internal`).
11
+ */
12
+ loopback?: "docker-host";
13
+ }
14
+ export type OutboundResult = {
15
+ kind: "response";
16
+ status: number;
17
+ headers: IncomingHttpHeaders;
18
+ body: Buffer;
19
+ }
20
+ /** The answer was larger than `maxResponseBytes`. */
21
+ | {
22
+ kind: "too_large";
23
+ status: number;
24
+ } | {
25
+ kind: "not_sent";
26
+ code: string;
27
+ message: string;
28
+ } | {
29
+ kind: "lost";
30
+ code: string;
31
+ message: string;
32
+ };
33
+ /** The largest answer read from an endpoint. */
34
+ export declare const MAX_RESPONSE_BYTES: number;
35
+ /** True when `address` is private, loopback, link-local or unspecified (IPv4-mapped included). */
36
+ export declare function isPrivateAddress(address: string): boolean;
37
+ /** Why the Host refuses `url`, or `undefined` when it may be called. */
38
+ export declare function refusal(url: URL, policy: OutboundPolicy): string | undefined;
39
+ /** POSTs `body` to `url` under `policy`. Never throws; `signal` aborts the request. */
40
+ export declare function post(target: string, body: string, headers: Record<string, string>, options: {
41
+ policy: OutboundPolicy;
42
+ signal: AbortSignal;
43
+ maxResponseBytes?: number;
44
+ }): Promise<OutboundResult>;