@nylorun/runtime 0.10.0-beta → 0.11.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.
- package/CHANGELOG.md +46 -0
- package/README.md +53 -51
- package/dist/adapters/execution/restate.d.ts +130 -0
- package/dist/adapters/execution/restate.js +442 -0
- package/dist/adapters/sandbox/virtual.js +1 -1
- package/dist/adapters/streams/s2.d.ts +18 -0
- package/dist/adapters/streams/s2.js +361 -0
- package/dist/core/executors.d.ts +4 -1
- package/dist/core/executors.js +20 -1
- package/dist/core/flow-host.d.ts +93 -132
- package/dist/core/flow-host.js +282 -547
- package/dist/core/sandbox-routes.d.ts +8 -3
- package/dist/core/sandbox-routes.js +10 -10
- package/dist/execution/memory.d.ts +54 -0
- package/dist/execution/memory.js +217 -0
- package/dist/execution/types.d.ts +125 -0
- package/dist/execution/types.js +52 -0
- package/dist/host/config-for.d.ts +2 -1
- package/dist/host/config-for.js +3 -21
- package/dist/host/config.d.ts +0 -8
- package/dist/host/create-host.d.ts +36 -1
- package/dist/host/create-host.js +48 -24
- package/dist/host/execution.d.ts +61 -0
- package/dist/host/execution.js +69 -0
- package/dist/host/http.d.ts +4 -0
- package/dist/host/http.js +6 -0
- package/dist/host/index.d.ts +3 -1
- package/dist/host/index.js +2 -0
- package/dist/host/main.js +106 -47
- package/dist/host/stack-config.d.ts +72 -0
- package/dist/host/stack-config.js +193 -0
- package/dist/index.d.ts +1 -1
- package/dist/infra/database.d.ts +16 -0
- package/dist/infra/database.js +31 -0
- package/dist/infra/execution.d.ts +39 -0
- package/dist/infra/execution.js +68 -0
- package/dist/infra/index.d.ts +42 -0
- package/dist/infra/index.js +56 -0
- package/dist/infra/readiness.d.ts +33 -0
- package/dist/infra/readiness.js +53 -0
- package/dist/infra/streams.d.ts +17 -0
- package/dist/infra/streams.js +33 -0
- package/dist/infra/workers.d.ts +39 -0
- package/dist/infra/workers.js +31 -0
- package/dist/model/pi-model.d.ts +2 -2
- package/dist/model/pi-model.js +5 -5
- package/dist/sandbox/manager.d.ts +26 -8
- package/dist/sandbox/manager.js +95 -47
- package/dist/sandbox/select.d.ts +1 -1
- package/dist/sandbox/select.js +4 -5
- package/dist/sandbox/types.d.ts +2 -2
- package/dist/store/canonical.d.ts +5 -0
- package/dist/store/canonical.js +17 -0
- package/dist/store/cursor.d.ts +9 -0
- package/dist/store/cursor.js +18 -0
- package/dist/store/memory.d.ts +57 -0
- package/dist/store/memory.js +643 -0
- package/dist/store/ownership.d.ts +19 -0
- package/dist/store/ownership.js +33 -0
- package/dist/store/postgres/connect.d.ts +25 -0
- package/dist/store/postgres/connect.js +25 -0
- package/dist/store/postgres/locking.d.ts +16 -0
- package/dist/store/postgres/locking.js +22 -0
- package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
- package/dist/store/postgres/migrations/001_initial.js +196 -0
- package/dist/store/postgres/migrations/index.d.ts +49 -0
- package/dist/store/postgres/migrations/index.js +75 -0
- package/dist/store/postgres/names.d.ts +11 -0
- package/dist/store/postgres/names.js +42 -0
- package/dist/store/postgres/store.d.ts +11 -0
- package/dist/store/postgres/store.js +790 -0
- package/dist/store/postgres/tenants.d.ts +90 -0
- package/dist/store/postgres/tenants.js +207 -0
- package/dist/store/types.d.ts +440 -0
- package/dist/store/types.js +10 -0
- package/dist/streams/basin.d.ts +23 -0
- package/dist/streams/basin.js +51 -0
- package/dist/streams/memory.d.ts +25 -0
- package/dist/streams/memory.js +124 -0
- package/dist/streams/relay.d.ts +37 -0
- package/dist/streams/relay.js +254 -0
- package/dist/streams/types.d.ts +115 -0
- package/dist/streams/types.js +86 -0
- package/dist/tenant/actions.d.ts +153 -0
- package/dist/tenant/actions.js +191 -0
- package/dist/tenant/advance.d.ts +15 -0
- package/dist/tenant/advance.js +439 -0
- package/dist/tenant/auth.d.ts +11 -0
- package/dist/tenant/auth.js +34 -0
- package/dist/tenant/commands.d.ts +3 -0
- package/dist/tenant/commands.js +384 -0
- package/dist/tenant/context.d.ts +140 -0
- package/dist/tenant/context.js +40 -0
- package/dist/tenant/effects.d.ts +20 -0
- package/dist/tenant/effects.js +539 -0
- package/dist/tenant/envelope.d.ts +0 -3
- package/dist/tenant/envelope.js +2 -34
- package/dist/tenant/ephemeral.d.ts +15 -4
- package/dist/tenant/ephemeral.js +106 -38
- package/dist/tenant/http.d.ts +32 -0
- package/dist/tenant/http.js +51 -0
- package/dist/tenant/live.d.ts +92 -0
- package/dist/tenant/live.js +290 -0
- package/dist/tenant/model-setting.d.ts +16 -0
- package/dist/tenant/model-setting.js +12 -0
- package/dist/tenant/module.d.ts +29 -8
- package/dist/tenant/module.js +162 -158
- package/dist/tenant/paths.d.ts +12 -8
- package/dist/tenant/paths.js +38 -12
- package/dist/tenant/principals.d.ts +9 -13
- package/dist/tenant/principals.js +31 -38
- package/dist/tenant/quarantine-error.d.ts +1 -1
- package/dist/tenant/quarantine-error.js +2 -8
- package/dist/tenant/quarantine.d.ts +0 -8
- package/dist/tenant/quarantine.js +8 -30
- package/dist/tenant/reset.d.ts +7 -6
- package/dist/tenant/reset.js +7 -51
- package/dist/tenant/routes-tenant.d.ts +12 -0
- package/dist/tenant/routes-tenant.js +126 -0
- package/dist/tenant/routes.d.ts +9 -0
- package/dist/tenant/routes.js +142 -0
- package/dist/tenant/runtime.d.ts +59 -58
- package/dist/tenant/runtime.js +243 -2201
- package/dist/tenant/scheduler.d.ts +25 -0
- package/dist/tenant/scheduler.js +84 -0
- package/dist/tenant/session.d.ts +60 -0
- package/dist/tenant/session.js +94 -0
- package/dist/tenant/sessions.d.ts +34 -0
- package/dist/tenant/sessions.js +149 -0
- package/dist/tenant/status.d.ts +13 -4
- package/dist/tenant/status.js +76 -41
- package/dist/tenant/store-memory.d.ts +9 -12
- package/dist/tenant/store-memory.js +79 -68
- package/dist/tenant/store-pg.d.ts +15 -0
- package/dist/tenant/store-pg.js +140 -0
- package/dist/tenant/streams.d.ts +126 -0
- package/dist/tenant/streams.js +419 -0
- package/dist/tenant/sweep.d.ts +15 -0
- package/dist/tenant/sweep.js +122 -0
- package/dist/tenant/types.d.ts +95 -15
- package/dist/tenant/types.js +23 -1
- package/dist/tenant/worker.d.ts +84 -0
- package/dist/tenant/worker.js +189 -0
- package/dist/vault/service.d.ts +37 -31
- package/dist/vault/service.js +439 -359
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -13
- package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
- package/dist/adapters/sandbox/microsandbox.js +0 -238
- package/dist/core/store.d.ts +0 -32
- package/dist/core/store.js +0 -143
- package/dist/launcher/commands.d.ts +0 -24
- package/dist/launcher/commands.js +0 -260
- package/dist/launcher/ensure-config.d.ts +0 -16
- package/dist/launcher/ensure-config.js +0 -79
- package/dist/launcher/errors.d.ts +0 -12
- package/dist/launcher/errors.js +0 -22
- package/dist/launcher/host-config.d.ts +0 -17
- package/dist/launcher/host-config.js +0 -64
- package/dist/launcher/host-state.d.ts +0 -17
- package/dist/launcher/host-state.js +0 -61
- package/dist/launcher/lifecycle.d.ts +0 -66
- package/dist/launcher/lifecycle.js +0 -407
- package/dist/launcher/locks.d.ts +0 -26
- package/dist/launcher/locks.js +0 -117
- package/dist/launcher/logs.d.ts +0 -18
- package/dist/launcher/logs.js +0 -117
- package/dist/launcher/main.d.ts +0 -2
- package/dist/launcher/main.js +0 -66
- package/dist/launcher/output.d.ts +0 -11
- package/dist/launcher/output.js +0 -68
- package/dist/launcher/paths.d.ts +0 -20
- package/dist/launcher/paths.js +0 -36
- package/dist/launcher/probe.d.ts +0 -23
- package/dist/launcher/probe.js +0 -72
- package/dist/launcher/protocol.d.ts +0 -62
- package/dist/launcher/protocol.js +0 -1
- package/dist/launcher/schema-guard.d.ts +0 -6
- package/dist/launcher/schema-guard.js +0 -36
- package/dist/launcher/spawn.d.ts +0 -22
- package/dist/launcher/spawn.js +0 -85
- package/dist/launcher/status.d.ts +0 -6
- package/dist/launcher/status.js +0 -65
- package/dist/tenant/migration.d.ts +0 -16
- package/dist/tenant/migration.js +0 -116
- package/dist/tenant/schema.d.ts +0 -23
- package/dist/tenant/schema.js +0 -151
- package/dist/tenant/store-fs.d.ts +0 -16
- package/dist/tenant/store-fs.js +0 -205
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stack configuration for the Runtime process, parsed from an environment
|
|
3
|
+
* snapshot and argv. Pure: `host/main.ts` passes the process environment and
|
|
4
|
+
* arguments; nothing here reads ambient state.
|
|
5
|
+
*
|
|
6
|
+
* Two listen modes:
|
|
7
|
+
* - **local** (no `NYLORUN_LISTEN_*` / `NYLORUN_ALLOWED_HOSTS`): the Host binds
|
|
8
|
+
* what `host.json` names, loopback only unless `allowNonLoopback`.
|
|
9
|
+
* - **container**: the Host binds `NYLORUN_LISTEN_HOST:NYLORUN_LISTEN_PORT`
|
|
10
|
+
* (default `0.0.0.0:4000`) and accepts only the `Host` headers in
|
|
11
|
+
* `NYLORUN_ALLOWED_HOSTS`, plus the loopback forms of the listen port for
|
|
12
|
+
* probes from inside the container.
|
|
13
|
+
*
|
|
14
|
+
* The Postgres, Restate and S2 endpoints are parsed and validated here;
|
|
15
|
+
* `infra/*` builds the clients from them.
|
|
16
|
+
*/
|
|
17
|
+
export const RUNTIME_ROLES = ["api", "worker", "all"];
|
|
18
|
+
export const DEFAULT_CONTAINER_LISTEN_HOST = "0.0.0.0";
|
|
19
|
+
export const DEFAULT_CONTAINER_LISTEN_PORT = 4000;
|
|
20
|
+
export class StackConfigError extends Error {
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "StackConfigError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function read(env, name) {
|
|
27
|
+
const value = env[name];
|
|
28
|
+
if (value === undefined)
|
|
29
|
+
return undefined;
|
|
30
|
+
const trimmed = value.trim();
|
|
31
|
+
return trimmed === "" ? undefined : trimmed;
|
|
32
|
+
}
|
|
33
|
+
export function parseRole(argv) {
|
|
34
|
+
let role;
|
|
35
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
36
|
+
const arg = argv[index];
|
|
37
|
+
let value;
|
|
38
|
+
if (arg === "--role") {
|
|
39
|
+
value = argv[index + 1];
|
|
40
|
+
index += 1;
|
|
41
|
+
if (value === undefined || value.startsWith("-"))
|
|
42
|
+
throw new StackConfigError("--role requires a value: api, worker or all");
|
|
43
|
+
}
|
|
44
|
+
else if (arg.startsWith("--role=")) {
|
|
45
|
+
value = arg.slice("--role=".length);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
throw new StackConfigError(`Unknown argument ${arg}. Usage: main.js [--role api|worker|all]`);
|
|
49
|
+
}
|
|
50
|
+
if (role !== undefined)
|
|
51
|
+
throw new StackConfigError("--role may only be supplied once");
|
|
52
|
+
if (!RUNTIME_ROLES.includes(value))
|
|
53
|
+
throw new StackConfigError(`Invalid --role ${value}; expected api, worker or all`);
|
|
54
|
+
role = value;
|
|
55
|
+
}
|
|
56
|
+
return role ?? "all";
|
|
57
|
+
}
|
|
58
|
+
function parsePort(name, raw) {
|
|
59
|
+
if (!/^\d+$/.test(raw))
|
|
60
|
+
throw new StackConfigError(`${name} must be a port number; got ${raw}`);
|
|
61
|
+
const port = Number(raw);
|
|
62
|
+
if (port < 1 || port > 65535)
|
|
63
|
+
throw new StackConfigError(`${name} must be between 1 and 65535; got ${raw}`);
|
|
64
|
+
return port;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Normalize one `Host` header allowlist entry to lowercase `name:port`.
|
|
68
|
+
* IPv6 literals must be bracketed (`[::1]:4000`).
|
|
69
|
+
*/
|
|
70
|
+
export function normalizeAllowedHost(name, entry) {
|
|
71
|
+
const value = entry.trim().toLowerCase();
|
|
72
|
+
const match = /^(\[[0-9a-f:.]+\]|[a-z0-9]([a-z0-9.-]*[a-z0-9])?):(\d+)$/.exec(value);
|
|
73
|
+
if (!match)
|
|
74
|
+
throw new StackConfigError(`${name} entry "${entry}" must be host:port (IPv6 in brackets)`);
|
|
75
|
+
parsePort(name, match[3]);
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
function loopbackForms(port) {
|
|
79
|
+
return [`localhost:${port}`, `127.0.0.1:${port}`, `[::1]:${port}`];
|
|
80
|
+
}
|
|
81
|
+
function isLoopbackAddress(host) {
|
|
82
|
+
const value = host.toLowerCase();
|
|
83
|
+
return value === "127.0.0.1" || value === "::1" || value === "localhost";
|
|
84
|
+
}
|
|
85
|
+
function parseUrl(env, name, protocols) {
|
|
86
|
+
const raw = read(env, name);
|
|
87
|
+
if (raw === undefined)
|
|
88
|
+
return undefined;
|
|
89
|
+
let url;
|
|
90
|
+
try {
|
|
91
|
+
url = new URL(raw);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
throw new StackConfigError(`${name} is not a valid URL`);
|
|
95
|
+
}
|
|
96
|
+
if (!protocols.includes(url.protocol))
|
|
97
|
+
throw new StackConfigError(`${name} must use ${protocols.map((p) => p.replace(/:$/, "")).join(" or ")}; got ${url.protocol.replace(/:$/, "")}`);
|
|
98
|
+
return raw;
|
|
99
|
+
}
|
|
100
|
+
function parseListen(env) {
|
|
101
|
+
const rawHost = read(env, "NYLORUN_LISTEN_HOST");
|
|
102
|
+
const rawPort = read(env, "NYLORUN_LISTEN_PORT");
|
|
103
|
+
const rawAllowed = read(env, "NYLORUN_ALLOWED_HOSTS");
|
|
104
|
+
if (rawHost === undefined && rawPort === undefined && rawAllowed === undefined)
|
|
105
|
+
return undefined;
|
|
106
|
+
const host = rawHost ?? DEFAULT_CONTAINER_LISTEN_HOST;
|
|
107
|
+
if (/\s|\//.test(host))
|
|
108
|
+
throw new StackConfigError(`NYLORUN_LISTEN_HOST is not an address: ${host}`);
|
|
109
|
+
const port = rawPort === undefined
|
|
110
|
+
? DEFAULT_CONTAINER_LISTEN_PORT
|
|
111
|
+
: parsePort("NYLORUN_LISTEN_PORT", rawPort);
|
|
112
|
+
const explicit = rawAllowed === undefined
|
|
113
|
+
? []
|
|
114
|
+
: rawAllowed
|
|
115
|
+
.split(",")
|
|
116
|
+
.map((entry) => entry.trim())
|
|
117
|
+
.filter((entry) => entry !== "")
|
|
118
|
+
.map((entry) => normalizeAllowedHost("NYLORUN_ALLOWED_HOSTS", entry));
|
|
119
|
+
if (explicit.length === 0 && !isLoopbackAddress(host))
|
|
120
|
+
throw new StackConfigError(`NYLORUN_ALLOWED_HOSTS is required when NYLORUN_LISTEN_HOST is ${host}: list the Host headers clients send, e.g. runtime:${port},localhost:<published port>`);
|
|
121
|
+
// Loopback forms of the listen port serve probes from inside the container
|
|
122
|
+
// (health checks). A browser on the Docker host never sends them: it sends
|
|
123
|
+
// the published port.
|
|
124
|
+
const allowedHosts = [...new Set([...explicit, ...loopbackForms(port)])];
|
|
125
|
+
return { host, port, allowedHosts };
|
|
126
|
+
}
|
|
127
|
+
const IDENTITY_KEY = /^publickeyv1_[1-9A-HJ-NP-Za-km-z]{32,64}$/;
|
|
128
|
+
function parseIdentityKeys(env) {
|
|
129
|
+
const raw = read(env, "NYLORUN_RESTATE_IDENTITY_KEY");
|
|
130
|
+
if (raw === undefined)
|
|
131
|
+
return undefined;
|
|
132
|
+
const keys = raw
|
|
133
|
+
.split(",")
|
|
134
|
+
.map((key) => key.trim())
|
|
135
|
+
.filter((key) => key !== "");
|
|
136
|
+
for (const key of keys)
|
|
137
|
+
if (!IDENTITY_KEY.test(key))
|
|
138
|
+
throw new StackConfigError("NYLORUN_RESTATE_IDENTITY_KEY must be publickeyv1_<base58 Ed25519 public key>, comma-separated");
|
|
139
|
+
return keys.length > 0 ? keys : undefined;
|
|
140
|
+
}
|
|
141
|
+
/** Parse the stack configuration; throws `StackConfigError` naming the variable. */
|
|
142
|
+
export function parseStackConfig(env, argv) {
|
|
143
|
+
const role = parseRole(argv);
|
|
144
|
+
const listen = parseListen(env);
|
|
145
|
+
const http = ["http:", "https:"];
|
|
146
|
+
const endpoints = {};
|
|
147
|
+
const databaseUrl = parseUrl(env, "NYLORUN_DATABASE_URL", [
|
|
148
|
+
"postgres:",
|
|
149
|
+
"postgresql:",
|
|
150
|
+
]);
|
|
151
|
+
if (databaseUrl)
|
|
152
|
+
endpoints.databaseUrl = databaseUrl;
|
|
153
|
+
const restateIngressUrl = parseUrl(env, "NYLORUN_RESTATE_INGRESS_URL", http);
|
|
154
|
+
if (restateIngressUrl)
|
|
155
|
+
endpoints.restateIngressUrl = restateIngressUrl;
|
|
156
|
+
const restateAdminUrl = parseUrl(env, "NYLORUN_RESTATE_ADMIN_URL", http);
|
|
157
|
+
if (restateAdminUrl)
|
|
158
|
+
endpoints.restateAdminUrl = restateAdminUrl;
|
|
159
|
+
const workerUrl = parseUrl(env, "NYLORUN_WORKER_URL", http);
|
|
160
|
+
if (workerUrl)
|
|
161
|
+
endpoints.workerUrl = workerUrl;
|
|
162
|
+
const s2Endpoint = parseUrl(env, "NYLORUN_S2_ENDPOINT", http);
|
|
163
|
+
if (s2Endpoint)
|
|
164
|
+
endpoints.s2Endpoint = s2Endpoint;
|
|
165
|
+
const s2Token = read(env, "NYLORUN_S2_TOKEN");
|
|
166
|
+
if (s2Token)
|
|
167
|
+
endpoints.s2Token = s2Token;
|
|
168
|
+
const workspaceStoreUrl = parseUrl(env, "NYLORUN_WORKSPACE_STORE_URL", [
|
|
169
|
+
"file:",
|
|
170
|
+
"s3:",
|
|
171
|
+
"http:",
|
|
172
|
+
"https:",
|
|
173
|
+
]);
|
|
174
|
+
if (workspaceStoreUrl)
|
|
175
|
+
endpoints.workspaceStoreUrl = workspaceStoreUrl;
|
|
176
|
+
const restateIdentityKeys = parseIdentityKeys(env);
|
|
177
|
+
if (restateIdentityKeys)
|
|
178
|
+
endpoints.restateIdentityKeys = restateIdentityKeys;
|
|
179
|
+
const publicUrl = parseUrl(env, "NYLORUN_PUBLIC_URL", http)?.replace(/\/+$/, "");
|
|
180
|
+
return {
|
|
181
|
+
role,
|
|
182
|
+
...(listen ? { listen } : {}),
|
|
183
|
+
endpoints,
|
|
184
|
+
...(publicUrl ? { publicUrl } : {}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** Endpoint summary for logs: which endpoints are set, never their values. */
|
|
188
|
+
export function describeEndpoints(endpoints) {
|
|
189
|
+
return Object.entries(endpoints)
|
|
190
|
+
.filter(([, value]) => value !== undefined)
|
|
191
|
+
.map(([key]) => key)
|
|
192
|
+
.sort();
|
|
193
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,4 +7,4 @@ export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageByt
|
|
|
7
7
|
export type { RuntimeModelAdapter } from "./contracts.js";
|
|
8
8
|
export { baselineEnvironment, hostProcessEnvironment, tenantChildEnvironment, } from "./host/environment.js";
|
|
9
9
|
export type { HostProcessPathInputs } from "./host/environment.js";
|
|
10
|
-
export type { HostConfigFile, HostCredentialsFile,
|
|
10
|
+
export type { HostConfigFile, HostCredentialsFile, } from "./host/config.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the Host's Postgres connection pool from the stack configuration
|
|
3
|
+
* (architecture §12.2, §14.5). One pool serves the Tenant catalog and every
|
|
4
|
+
* Tenant schema; the Host ends it at shutdown. The driver stays behind
|
|
5
|
+
* `store/postgres/connect.ts`.
|
|
6
|
+
*/
|
|
7
|
+
import type { StackConfig } from "../host/stack-config.js";
|
|
8
|
+
import { type PostgresClient, type PostgresClientOptions } from "../store/postgres/connect.js";
|
|
9
|
+
export type { PostgresClient };
|
|
10
|
+
/** Throws when `NYLORUN_DATABASE_URL` is unset: there is no in-memory Session Store. */
|
|
11
|
+
export declare function createDatabase(config: Pick<StackConfig, "endpoints">, options?: PostgresClientOptions): PostgresClient;
|
|
12
|
+
/**
|
|
13
|
+
* Readiness probe: resolves when `select 1` answers within `signal`'s
|
|
14
|
+
* lifetime. An aborted probe cancels its query.
|
|
15
|
+
*/
|
|
16
|
+
export declare function probeDatabase(client: PostgresClient, signal: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createPostgresClient, } from "../store/postgres/connect.js";
|
|
2
|
+
/** Throws when `NYLORUN_DATABASE_URL` is unset: there is no in-memory Session Store. */
|
|
3
|
+
export function createDatabase(config, options = {}) {
|
|
4
|
+
const url = config.endpoints.databaseUrl;
|
|
5
|
+
if (!url)
|
|
6
|
+
throw new Error("NYLORUN_DATABASE_URL is required: the Postgres URL of the Session Store");
|
|
7
|
+
return createPostgresClient(url, options);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Readiness probe: resolves when `select 1` answers within `signal`'s
|
|
11
|
+
* lifetime. An aborted probe cancels its query.
|
|
12
|
+
*/
|
|
13
|
+
export async function probeDatabase(client, signal) {
|
|
14
|
+
signal.throwIfAborted();
|
|
15
|
+
const query = client `select 1 as ok`;
|
|
16
|
+
const cancel = () => void query.cancel();
|
|
17
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
18
|
+
try {
|
|
19
|
+
await Promise.race([
|
|
20
|
+
query,
|
|
21
|
+
new Promise((_, reject) => {
|
|
22
|
+
if (signal.aborted)
|
|
23
|
+
reject(signal.reason);
|
|
24
|
+
signal.addEventListener("abort", () => reject(signal.reason), { once: true });
|
|
25
|
+
}),
|
|
26
|
+
]);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
signal.removeEventListener("abort", cancel);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { DurableExecution } from "../execution/types.js";
|
|
2
|
+
import type { RuntimeRole, StackConfig } from "../host/stack-config.js";
|
|
3
|
+
/** Where a Worker serves the Restate endpoint (architecture §14.3). */
|
|
4
|
+
export declare const WORKER_LISTEN: {
|
|
5
|
+
readonly host: "0.0.0.0";
|
|
6
|
+
readonly port: 9080;
|
|
7
|
+
};
|
|
8
|
+
export interface CreateExecutionOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Prepended to Restate service names, so several Runtimes or test runs can
|
|
11
|
+
* share one Restate server. Letters, digits and `_`. Default "".
|
|
12
|
+
*/
|
|
13
|
+
servicePrefix?: string;
|
|
14
|
+
/** Replaces `WORKER_LISTEN` (tests serving several Workers on one machine). */
|
|
15
|
+
workerListen?: {
|
|
16
|
+
host: string;
|
|
17
|
+
port: number;
|
|
18
|
+
};
|
|
19
|
+
/** Receives the Restate SDK's log lines instead of the console. */
|
|
20
|
+
logger?: (level: string, message: string) => void;
|
|
21
|
+
}
|
|
22
|
+
/** Which implementation `createExecution` chooses for a configuration. */
|
|
23
|
+
export declare function executionKind(config: Pick<StackConfig, "endpoints">): "restate" | "memory";
|
|
24
|
+
/**
|
|
25
|
+
* Throws naming the missing variable when the Restate endpoints are
|
|
26
|
+
* incomplete for `role`. The api role never serves the Worker endpoint, so it
|
|
27
|
+
* needs no `NYLORUN_WORKER_URL`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function validateExecutionConfig(config: Pick<StackConfig, "endpoints"> & {
|
|
30
|
+
role?: RuntimeRole;
|
|
31
|
+
}): void;
|
|
32
|
+
export declare function createExecution(config: Pick<StackConfig, "endpoints"> & {
|
|
33
|
+
role?: RuntimeRole;
|
|
34
|
+
}, options?: CreateExecutionOptions): DurableExecution;
|
|
35
|
+
/**
|
|
36
|
+
* Readiness probe: resolves when Restate's admin API and ingress answer
|
|
37
|
+
* within `signal`'s lifetime. The memory execution is always ready.
|
|
38
|
+
*/
|
|
39
|
+
export declare function probeExecution(execution: DurableExecution, signal: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the Host's Durable Session Execution from the stack configuration
|
|
3
|
+
* (architecture §6, §12.3, §14.5).
|
|
4
|
+
*
|
|
5
|
+
* With `NYLORUN_RESTATE_INGRESS_URL` and `NYLORUN_RESTATE_ADMIN_URL` the Host
|
|
6
|
+
* uses Restate:
|
|
7
|
+
*
|
|
8
|
+
* - wakes, timers and sweep arming go through the ingress, so every role can
|
|
9
|
+
* call them;
|
|
10
|
+
* - a Worker (`--role worker|all`) serves the Restate endpoint on
|
|
11
|
+
* `0.0.0.0:9080` and registers `NYLORUN_WORKER_URL` with the admin API when
|
|
12
|
+
* `infra/workers.ts` starts it;
|
|
13
|
+
* - with `NYLORUN_RESTATE_IDENTITY_KEY` the endpoint accepts only requests
|
|
14
|
+
* signed by the Restate server holding the matching private key.
|
|
15
|
+
*
|
|
16
|
+
* Without Restate endpoints it returns the in-process memory execution, which
|
|
17
|
+
* only unit tests and a local development Host use.
|
|
18
|
+
*/
|
|
19
|
+
import { createRestateExecution } from "../adapters/execution/restate.js";
|
|
20
|
+
import { MemoryExecution } from "../execution/memory.js";
|
|
21
|
+
/** Where a Worker serves the Restate endpoint (architecture §14.3). */
|
|
22
|
+
export const WORKER_LISTEN = { host: "0.0.0.0", port: 9080 };
|
|
23
|
+
/** Which implementation `createExecution` chooses for a configuration. */
|
|
24
|
+
export function executionKind(config) {
|
|
25
|
+
const { restateIngressUrl, restateAdminUrl } = config.endpoints;
|
|
26
|
+
return restateIngressUrl || restateAdminUrl ? "restate" : "memory";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Throws naming the missing variable when the Restate endpoints are
|
|
30
|
+
* incomplete for `role`. The api role never serves the Worker endpoint, so it
|
|
31
|
+
* needs no `NYLORUN_WORKER_URL`.
|
|
32
|
+
*/
|
|
33
|
+
export function validateExecutionConfig(config) {
|
|
34
|
+
const { restateIngressUrl, restateAdminUrl, workerUrl, restateIdentityKeys } = config.endpoints;
|
|
35
|
+
if (executionKind(config) === "memory") {
|
|
36
|
+
if (workerUrl || restateIdentityKeys?.length)
|
|
37
|
+
throw new Error(`${workerUrl ? "NYLORUN_WORKER_URL" : "NYLORUN_RESTATE_IDENTITY_KEY"} is set without the Restate endpoints; set NYLORUN_RESTATE_INGRESS_URL and NYLORUN_RESTATE_ADMIN_URL`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!restateIngressUrl)
|
|
41
|
+
throw new Error("NYLORUN_RESTATE_INGRESS_URL is required with NYLORUN_RESTATE_ADMIN_URL");
|
|
42
|
+
if (!restateAdminUrl)
|
|
43
|
+
throw new Error("NYLORUN_RESTATE_ADMIN_URL is required with NYLORUN_RESTATE_INGRESS_URL");
|
|
44
|
+
if ((config.role ?? "all") !== "api" && !workerUrl)
|
|
45
|
+
throw new Error(`NYLORUN_WORKER_URL is required for --role ${config.role ?? "all"}: the Worker endpoint URL Restate calls`);
|
|
46
|
+
}
|
|
47
|
+
export function createExecution(config, options = {}) {
|
|
48
|
+
validateExecutionConfig(config);
|
|
49
|
+
const { restateIngressUrl, restateAdminUrl, workerUrl, restateIdentityKeys } = config.endpoints;
|
|
50
|
+
if (!restateIngressUrl || !restateAdminUrl)
|
|
51
|
+
return new MemoryExecution();
|
|
52
|
+
return createRestateExecution({
|
|
53
|
+
ingressUrl: restateIngressUrl,
|
|
54
|
+
adminUrl: restateAdminUrl,
|
|
55
|
+
workerListen: options.workerListen ?? { ...WORKER_LISTEN },
|
|
56
|
+
...(workerUrl ? { workerAdvertisedUrl: workerUrl } : {}),
|
|
57
|
+
...(restateIdentityKeys?.length ? { identityKeys: [...restateIdentityKeys] } : {}),
|
|
58
|
+
...(options.servicePrefix ? { servicePrefix: options.servicePrefix } : {}),
|
|
59
|
+
...(options.logger ? { logger: options.logger } : {}),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Readiness probe: resolves when Restate's admin API and ingress answer
|
|
64
|
+
* within `signal`'s lifetime. The memory execution is always ready.
|
|
65
|
+
*/
|
|
66
|
+
export async function probeExecution(execution, signal) {
|
|
67
|
+
await execution.probe?.(signal);
|
|
68
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Host's infrastructure clients, built from the stack configuration: the
|
|
3
|
+
* Postgres pool, Durable Session Execution and Durable Streams, each only when
|
|
4
|
+
* its endpoints are configured, plus the readiness that covers them.
|
|
5
|
+
*
|
|
6
|
+
* Building a client connects to nothing: the pool connects on first query,
|
|
7
|
+
* Restate and S2 on first call. Nothing here starts a Worker (see
|
|
8
|
+
* `workers.ts`).
|
|
9
|
+
*/
|
|
10
|
+
import type { DurableExecution } from "../execution/types.js";
|
|
11
|
+
import type { StackConfig } from "../host/stack-config.js";
|
|
12
|
+
import type { DurableStreams } from "../streams/types.js";
|
|
13
|
+
import type { Logger } from "../tenant/types.js";
|
|
14
|
+
import { type PostgresClient } from "./database.js";
|
|
15
|
+
import { type CreateExecutionOptions } from "./execution.js";
|
|
16
|
+
import { type Readiness } from "./readiness.js";
|
|
17
|
+
import { type CreateStreamsOptions } from "./streams.js";
|
|
18
|
+
export { createDatabase, probeDatabase } from "./database.js";
|
|
19
|
+
export { createExecution, probeExecution, WORKER_LISTEN } from "./execution.js";
|
|
20
|
+
export { createReadiness, infraProbes } from "./readiness.js";
|
|
21
|
+
export type { Probe, Readiness, ReadinessReport } from "./readiness.js";
|
|
22
|
+
export { createStreams, probeStreams } from "./streams.js";
|
|
23
|
+
export { servesWorker, startWorker, stopWorker } from "./workers.js";
|
|
24
|
+
export type { WorkerHandle } from "./workers.js";
|
|
25
|
+
export interface Infra {
|
|
26
|
+
database?: PostgresClient;
|
|
27
|
+
execution?: DurableExecution;
|
|
28
|
+
streams?: DurableStreams;
|
|
29
|
+
/** Present when at least one client was built. */
|
|
30
|
+
readiness?: Readiness;
|
|
31
|
+
/** Ends the pool and closes the streams. Does not stop a Worker. */
|
|
32
|
+
close(): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Builds the configured clients. Throws naming the variable when endpoints are
|
|
36
|
+
* incomplete (e.g. a Restate ingress without an admin URL).
|
|
37
|
+
*/
|
|
38
|
+
export declare function createInfra(config: Pick<StackConfig, "role" | "endpoints">, options?: {
|
|
39
|
+
logger?: Logger;
|
|
40
|
+
execution?: CreateExecutionOptions;
|
|
41
|
+
streams?: CreateStreamsOptions;
|
|
42
|
+
}): Infra;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createDatabase } from "./database.js";
|
|
2
|
+
import { createExecution, executionKind } from "./execution.js";
|
|
3
|
+
import { createReadiness, infraProbes } from "./readiness.js";
|
|
4
|
+
import { createStreams, streamsKind } from "./streams.js";
|
|
5
|
+
export { createDatabase, probeDatabase } from "./database.js";
|
|
6
|
+
export { createExecution, probeExecution, WORKER_LISTEN } from "./execution.js";
|
|
7
|
+
export { createReadiness, infraProbes } from "./readiness.js";
|
|
8
|
+
export { createStreams, probeStreams } from "./streams.js";
|
|
9
|
+
export { servesWorker, startWorker, stopWorker } from "./workers.js";
|
|
10
|
+
/**
|
|
11
|
+
* Builds the configured clients. Throws naming the variable when endpoints are
|
|
12
|
+
* incomplete (e.g. a Restate ingress without an admin URL).
|
|
13
|
+
*/
|
|
14
|
+
export function createInfra(config, options = {}) {
|
|
15
|
+
const database = config.endpoints.databaseUrl ? createDatabase(config) : undefined;
|
|
16
|
+
const execution = executionKind(config) === "restate"
|
|
17
|
+
? createExecution(config, options.execution)
|
|
18
|
+
: undefined;
|
|
19
|
+
const streams = streamsKind(config) === "s2" ? createStreams(config, options.streams) : undefined;
|
|
20
|
+
const probes = infraProbes({
|
|
21
|
+
...(database ? { database } : {}),
|
|
22
|
+
...(execution ? { execution } : {}),
|
|
23
|
+
...(streams ? { streams } : {}),
|
|
24
|
+
});
|
|
25
|
+
const readiness = Object.keys(probes).length > 0
|
|
26
|
+
? logTransitions(createReadiness(probes), options.logger)
|
|
27
|
+
: undefined;
|
|
28
|
+
return {
|
|
29
|
+
...(database ? { database } : {}),
|
|
30
|
+
...(execution ? { execution } : {}),
|
|
31
|
+
...(streams ? { streams } : {}),
|
|
32
|
+
...(readiness ? { readiness } : {}),
|
|
33
|
+
async close() {
|
|
34
|
+
await Promise.allSettled([database?.end({ timeout: 5 }), streams?.close()]);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Logs a check when it starts failing and when it recovers, not on every probe. */
|
|
39
|
+
function logTransitions(readiness, logger) {
|
|
40
|
+
if (!logger)
|
|
41
|
+
return readiness;
|
|
42
|
+
const failing = new Set();
|
|
43
|
+
return async () => {
|
|
44
|
+
const report = await readiness();
|
|
45
|
+
for (const [name, ok] of Object.entries(report.checks)) {
|
|
46
|
+
if (!ok && !failing.has(name)) {
|
|
47
|
+
failing.add(name);
|
|
48
|
+
logger.warn("ready_check_failed", { check: name, error: report.errors[name] });
|
|
49
|
+
}
|
|
50
|
+
else if (ok && failing.delete(name)) {
|
|
51
|
+
logger.info("ready_check_recovered", { check: name });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return report;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregate readiness of the Host's infrastructure (architecture §13, §14.3):
|
|
3
|
+
* the Runtime's `/ready` covers Postgres, Restate and S2. s2-lite's image has
|
|
4
|
+
* no health check of its own, so this is the only place its health shows.
|
|
5
|
+
*
|
|
6
|
+
* Every probe runs on each call, in parallel, bounded by one timeout. A probe
|
|
7
|
+
* that throws or outlives the timeout marks its check `false`. Only check
|
|
8
|
+
* names and booleans are reported; the errors stay in the report for logs.
|
|
9
|
+
*/
|
|
10
|
+
import type { DurableExecution } from "../execution/types.js";
|
|
11
|
+
import type { DurableStreams } from "../streams/types.js";
|
|
12
|
+
import { type PostgresClient } from "./database.js";
|
|
13
|
+
/** Resolves when the dependency answers; rejects otherwise. Must honour `signal`. */
|
|
14
|
+
export type Probe = (signal: AbortSignal) => Promise<void>;
|
|
15
|
+
export interface ReadinessReport {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
/** One entry per configured dependency, e.g. `{ postgres, restate, s2 }`. */
|
|
18
|
+
checks: Record<string, boolean>;
|
|
19
|
+
/** Why each failing check failed. Not for unauthenticated responses. */
|
|
20
|
+
errors: Record<string, string>;
|
|
21
|
+
}
|
|
22
|
+
export type Readiness = () => Promise<ReadinessReport>;
|
|
23
|
+
/** Default bound on one readiness pass. Compose probes `/ready` every 2 s with a 5 s timeout. */
|
|
24
|
+
export declare const READINESS_TIMEOUT_MS = 2000;
|
|
25
|
+
export declare function createReadiness(probes: Readonly<Record<string, Probe>>, options?: {
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
}): Readiness;
|
|
28
|
+
/** The standard checks for whichever infrastructure the Host was built with. */
|
|
29
|
+
export declare function infraProbes(infra: {
|
|
30
|
+
database?: PostgresClient;
|
|
31
|
+
execution?: DurableExecution;
|
|
32
|
+
streams?: DurableStreams;
|
|
33
|
+
}): Record<string, Probe>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { probeDatabase } from "./database.js";
|
|
2
|
+
import { probeExecution } from "./execution.js";
|
|
3
|
+
import { probeStreams } from "./streams.js";
|
|
4
|
+
/** Default bound on one readiness pass. Compose probes `/ready` every 2 s with a 5 s timeout. */
|
|
5
|
+
export const READINESS_TIMEOUT_MS = 2000;
|
|
6
|
+
export function createReadiness(probes, options = {}) {
|
|
7
|
+
const timeoutMs = options.timeoutMs ?? READINESS_TIMEOUT_MS;
|
|
8
|
+
return async () => {
|
|
9
|
+
const entries = Object.entries(probes);
|
|
10
|
+
const results = await Promise.all(entries.map(async ([name, probe]) => [name, await runProbe(probe, timeoutMs)]));
|
|
11
|
+
const checks = {};
|
|
12
|
+
const errors = {};
|
|
13
|
+
for (const [name, error] of results) {
|
|
14
|
+
checks[name] = error === undefined;
|
|
15
|
+
if (error !== undefined)
|
|
16
|
+
errors[name] = error;
|
|
17
|
+
}
|
|
18
|
+
return { ok: Object.values(checks).every(Boolean), checks, errors };
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** The standard checks for whichever infrastructure the Host was built with. */
|
|
22
|
+
export function infraProbes(infra) {
|
|
23
|
+
const probes = {};
|
|
24
|
+
const { database, execution, streams } = infra;
|
|
25
|
+
if (database)
|
|
26
|
+
probes.postgres = (signal) => probeDatabase(database, signal);
|
|
27
|
+
if (execution)
|
|
28
|
+
probes.restate = (signal) => probeExecution(execution, signal);
|
|
29
|
+
if (streams)
|
|
30
|
+
probes.s2 = (signal) => probeStreams(streams, signal);
|
|
31
|
+
return probes;
|
|
32
|
+
}
|
|
33
|
+
/** Runs one probe under a timeout; returns its failure message, or undefined when ready. */
|
|
34
|
+
async function runProbe(probe, timeoutMs) {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
let timer;
|
|
37
|
+
const timedOut = new Promise((resolve) => {
|
|
38
|
+
timer = setTimeout(() => {
|
|
39
|
+
controller.abort(new Error(`timed out after ${timeoutMs} ms`));
|
|
40
|
+
resolve(`timed out after ${timeoutMs} ms`);
|
|
41
|
+
}, timeoutMs);
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
return await Promise.race([
|
|
45
|
+
probe(controller.signal).then(() => undefined, (error) => (error instanceof Error ? error.message : String(error))),
|
|
46
|
+
timedOut,
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
controller.abort(new Error("readiness pass finished"));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StackConfig } from "../host/stack-config.js";
|
|
2
|
+
import type { DurableStreams } from "../streams/types.js";
|
|
3
|
+
export interface CreateStreamsOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Prepended to every basin name, so several Hosts or test runs can share one
|
|
6
|
+
* S2 account. See `streams/basin.ts`. Default "".
|
|
7
|
+
*/
|
|
8
|
+
basinPrefix?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Which implementation `createStreams` chooses for a configuration. */
|
|
11
|
+
export declare function streamsKind(config: Pick<StackConfig, "endpoints">): "s2" | "memory";
|
|
12
|
+
export declare function createStreams(config: Pick<StackConfig, "endpoints">, options?: CreateStreamsOptions): DurableStreams;
|
|
13
|
+
/**
|
|
14
|
+
* Readiness probe: resolves when the streams' service answers within
|
|
15
|
+
* `signal`'s lifetime. In-memory streams are always ready.
|
|
16
|
+
*/
|
|
17
|
+
export declare function probeStreams(streams: DurableStreams, signal: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the Host's Durable Streams from the stack configuration
|
|
3
|
+
* (architecture §12.4, §14.5). Only endpoints are configurable, never the kind:
|
|
4
|
+
* with `NYLORUN_S2_ENDPOINT` the Host uses S2 (s2-lite on a developer machine,
|
|
5
|
+
* or the S2 service); without it, the in-memory implementation, which only
|
|
6
|
+
* unit tests and a local development Host use.
|
|
7
|
+
*/
|
|
8
|
+
import { createS2Streams } from "../adapters/streams/s2.js";
|
|
9
|
+
import { MemoryStreams } from "../streams/memory.js";
|
|
10
|
+
/** Which implementation `createStreams` chooses for a configuration. */
|
|
11
|
+
export function streamsKind(config) {
|
|
12
|
+
return config.endpoints.s2Endpoint ? "s2" : "memory";
|
|
13
|
+
}
|
|
14
|
+
export function createStreams(config, options = {}) {
|
|
15
|
+
const { s2Endpoint, s2Token } = config.endpoints;
|
|
16
|
+
if (!s2Endpoint) {
|
|
17
|
+
if (s2Token)
|
|
18
|
+
throw new Error("NYLORUN_S2_TOKEN is set without NYLORUN_S2_ENDPOINT; set the endpoint too");
|
|
19
|
+
return new MemoryStreams();
|
|
20
|
+
}
|
|
21
|
+
return createS2Streams({
|
|
22
|
+
endpoint: s2Endpoint,
|
|
23
|
+
...(s2Token ? { token: s2Token } : {}),
|
|
24
|
+
...(options.basinPrefix ? { basinPrefix: options.basinPrefix } : {}),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Readiness probe: resolves when the streams' service answers within
|
|
29
|
+
* `signal`'s lifetime. In-memory streams are always ready.
|
|
30
|
+
*/
|
|
31
|
+
export async function probeStreams(streams, signal) {
|
|
32
|
+
await streams.probe?.(signal);
|
|
33
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker lifecycle by role (architecture §6).
|
|
3
|
+
*
|
|
4
|
+
* | Role | Serves | `execution.start` |
|
|
5
|
+
* | --- | --- | --- |
|
|
6
|
+
* | `api` | Tenant API, Admin API, SSE | **never**: wakes, timers and sweep arming go through Restate's ingress, which needs no Worker endpoint |
|
|
7
|
+
* | `worker` | the Restate endpoint (`NylorunSession.advance`, `NylorunTenant.sweep`, timers) | yes |
|
|
8
|
+
* | `all` | both (the local stack) | yes |
|
|
9
|
+
*
|
|
10
|
+
* An API node that called `start` would register its own endpoint with
|
|
11
|
+
* Restate, and Restate would send new invocations to it (it routes to the
|
|
12
|
+
* latest registered deployment), so the rule is enforced here rather than left
|
|
13
|
+
* to callers: `startWorker` with role `api` returns a handle that never
|
|
14
|
+
* started anything.
|
|
15
|
+
*/
|
|
16
|
+
import type { DurableExecution, WorkerHandlers } from "../execution/types.js";
|
|
17
|
+
import type { RuntimeRole } from "../host/stack-config.js";
|
|
18
|
+
export interface WorkerHandle {
|
|
19
|
+
readonly role: RuntimeRole;
|
|
20
|
+
/** True when this process serves the Worker endpoint. */
|
|
21
|
+
readonly serving: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Whether a process in `role` serves the Worker endpoint. */
|
|
24
|
+
export declare function servesWorker(role: RuntimeRole): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Starts delivering Restate invocations to `handlers` when `role` is `worker`
|
|
27
|
+
* or `all` (serving and registering the Worker endpoint, for Restate). For
|
|
28
|
+
* `api` it does nothing. Rejects if the endpoint cannot listen or register.
|
|
29
|
+
*/
|
|
30
|
+
export declare function startWorker(input: {
|
|
31
|
+
role: RuntimeRole;
|
|
32
|
+
execution: DurableExecution;
|
|
33
|
+
handlers: WorkerHandlers;
|
|
34
|
+
}): Promise<WorkerHandle>;
|
|
35
|
+
/**
|
|
36
|
+
* Stops a Worker started by `startWorker`: aborts running advances' signals
|
|
37
|
+
* and waits for them. Idempotent; a no-op for the api role.
|
|
38
|
+
*/
|
|
39
|
+
export declare function stopWorker(handle: WorkerHandle): Promise<void>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Whether a process in `role` serves the Worker endpoint. */
|
|
2
|
+
export function servesWorker(role) {
|
|
3
|
+
return role !== "api";
|
|
4
|
+
}
|
|
5
|
+
/** What `stopWorker` needs for a handle that serves the endpoint. */
|
|
6
|
+
const started = new WeakMap();
|
|
7
|
+
/**
|
|
8
|
+
* Starts delivering Restate invocations to `handlers` when `role` is `worker`
|
|
9
|
+
* or `all` (serving and registering the Worker endpoint, for Restate). For
|
|
10
|
+
* `api` it does nothing. Rejects if the endpoint cannot listen or register.
|
|
11
|
+
*/
|
|
12
|
+
export async function startWorker(input) {
|
|
13
|
+
const { role, execution, handlers } = input;
|
|
14
|
+
const handle = Object.freeze({ role, serving: servesWorker(role) });
|
|
15
|
+
if (!handle.serving)
|
|
16
|
+
return handle;
|
|
17
|
+
await execution.start(handlers);
|
|
18
|
+
started.set(handle, { execution });
|
|
19
|
+
return handle;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Stops a Worker started by `startWorker`: aborts running advances' signals
|
|
23
|
+
* and waits for them. Idempotent; a no-op for the api role.
|
|
24
|
+
*/
|
|
25
|
+
export async function stopWorker(handle) {
|
|
26
|
+
const worker = started.get(handle);
|
|
27
|
+
if (!worker)
|
|
28
|
+
return;
|
|
29
|
+
worker.stopped ??= worker.execution.stop();
|
|
30
|
+
await worker.stopped;
|
|
31
|
+
}
|