@nylorun/runtime 0.9.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 +81 -0
- package/README.md +110 -37
- package/dist/adapters/execution/restate.d.ts +130 -0
- package/dist/adapters/execution/restate.js +442 -0
- package/dist/adapters/media.d.ts +2 -2
- package/dist/adapters/media.js +2 -2
- package/dist/adapters/observe.d.ts +2 -1
- package/dist/adapters/observe.js +2 -3
- 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/assets.d.ts +2 -2
- package/dist/assets.js +5 -3
- package/dist/core/executors.d.ts +15 -3
- package/dist/core/executors.js +40 -5
- package/dist/core/flow-host.d.ts +225 -0
- package/dist/core/flow-host.js +518 -0
- package/dist/core/limits.d.ts +40 -0
- package/dist/core/limits.js +76 -0
- package/dist/core/runtime.d.ts +5 -90
- package/dist/core/runtime.js +3 -1447
- package/dist/core/sandbox-routes.d.ts +40 -0
- package/dist/core/sandbox-routes.js +106 -0
- package/dist/core/turn-manifest.d.ts +50 -0
- package/dist/core/turn-manifest.js +66 -0
- 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 +17 -0
- package/dist/host/config-for.js +27 -0
- package/dist/host/config.d.ts +16 -0
- package/dist/host/create-host.d.ts +56 -0
- package/dist/host/create-host.js +420 -0
- package/dist/host/environment.d.ts +13 -0
- package/dist/host/environment.js +53 -0
- package/dist/host/execution.d.ts +61 -0
- package/dist/host/execution.js +69 -0
- package/dist/host/http.d.ts +39 -0
- package/dist/host/http.js +142 -0
- package/dist/host/index.d.ts +8 -0
- package/dist/host/index.js +7 -0
- package/dist/host/logger.d.ts +3 -0
- package/dist/host/logger.js +18 -0
- package/dist/host/main.d.ts +3 -0
- package/dist/host/main.js +201 -0
- package/dist/host/stack-config.d.ts +72 -0
- package/dist/host/stack-config.js +193 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +4 -2
- 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/mcp/connect.d.ts +1 -0
- package/dist/mcp/connect.js +2 -0
- package/dist/mcp/pool.d.ts +3 -1
- package/dist/mcp/pool.js +2 -1
- package/dist/mcp/stdio.d.ts +2 -0
- package/dist/mcp/stdio.js +3 -1
- package/dist/model/auth-store.d.ts +1 -1
- package/dist/model/auth-store.js +2 -2
- package/dist/model/models.d.ts +4 -0
- package/dist/model/models.js +22 -26
- package/dist/model/pi-model.d.ts +2 -2
- package/dist/model/pi-model.js +6 -6
- package/dist/model/settings.d.ts +5 -1
- package/dist/model/settings.js +6 -2
- package/dist/sandbox/manager.d.ts +26 -8
- package/dist/sandbox/manager.js +95 -47
- package/dist/sandbox/select.d.ts +5 -4
- package/dist/sandbox/select.js +18 -13
- package/dist/sandbox/share.d.ts +23 -0
- package/dist/sandbox/share.js +58 -0
- 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 +7 -0
- package/dist/tenant/envelope.js +22 -0
- package/dist/tenant/ephemeral.d.ts +42 -0
- package/dist/tenant/ephemeral.js +224 -0
- 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/logger.d.ts +9 -0
- package/dist/tenant/logger.js +25 -0
- package/dist/tenant/model-setting.d.ts +16 -0
- package/dist/tenant/model-setting.js +12 -0
- package/dist/tenant/module.d.ts +38 -0
- package/dist/tenant/module.js +319 -0
- package/dist/tenant/paths.d.ts +28 -0
- package/dist/tenant/paths.js +91 -0
- package/dist/tenant/pool.d.ts +10 -0
- package/dist/tenant/pool.js +54 -0
- package/dist/tenant/principals.d.ts +13 -0
- package/dist/tenant/principals.js +37 -0
- package/dist/tenant/quarantine-error.d.ts +6 -0
- package/dist/tenant/quarantine-error.js +9 -0
- package/dist/tenant/quarantine.d.ts +27 -0
- package/dist/tenant/quarantine.js +72 -0
- package/dist/tenant/reset.d.ts +22 -0
- package/dist/tenant/reset.js +81 -0
- 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 +81 -0
- package/dist/tenant/runtime.js +298 -0
- 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 +37 -0
- package/dist/tenant/status.js +130 -0
- package/dist/tenant/store-memory.d.ts +16 -0
- package/dist/tenant/store-memory.js +111 -0
- 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 +205 -0
- package/dist/tenant/types.js +23 -0
- package/dist/tenant/worker.d.ts +84 -0
- package/dist/tenant/worker.js +189 -0
- package/dist/vault/kek.d.ts +5 -2
- package/dist/vault/kek.js +7 -10
- 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 +14 -11
- package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
- package/dist/adapters/sandbox/microsandbox.js +0 -238
- package/dist/core/main.js +0 -35
- package/dist/core/store.d.ts +0 -30
- package/dist/core/store.js +0 -202
- /package/dist/{core/main.d.ts → host/config.js} +0 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable Session Execution on Restate (architecture §12.3).
|
|
3
|
+
*
|
|
4
|
+
* The only module that imports the Restate SDK. It serves three virtual
|
|
5
|
+
* objects on the Worker endpoint, all named with `servicePrefix`:
|
|
6
|
+
*
|
|
7
|
+
* - `<prefix>NylorunSession`, key `<tenantId>:<sessionId>`. Its exclusive
|
|
8
|
+
* `advance` handler calls `WorkerHandlers.advance`. It makes no `ctx.run`,
|
|
9
|
+
* keeps no state and uses no awakeables: the only journal entry it can write
|
|
10
|
+
* is the delayed self-send after a `busy` result. Restate's one invocation
|
|
11
|
+
* per key at a time is the "one advance per key" guarantee.
|
|
12
|
+
* - `<prefix>NylorunTenant`, key `<tenantId>`. `arm`, `sweep` and `disarm`
|
|
13
|
+
* keep one self-re-arming sweep chain per Tenant. The chain's generation is
|
|
14
|
+
* kept in the object's state, so arming is idempotent and a disarmed or
|
|
15
|
+
* replaced chain dies at its next link. The state is rebuildable: Workers
|
|
16
|
+
* re-arm every Tenant at startup (§14.8).
|
|
17
|
+
* - `<prefix>NylorunTimer`, key `<tenantId>:<timer key>`. `set` records the
|
|
18
|
+
* latest time and sends a delayed `fire`; a `fire` for an older time is a
|
|
19
|
+
* no-op, which is how setting a key again replaces it.
|
|
20
|
+
*
|
|
21
|
+
* Wakes, timers and sweep arming go through the ingress as one-way sends, so
|
|
22
|
+
* an API node can call them without ever calling `start`.
|
|
23
|
+
*/
|
|
24
|
+
import { createServer } from "node:http2";
|
|
25
|
+
import * as restate from "@restatedev/restate-sdk";
|
|
26
|
+
import { WAKE_REASONS, parseSessionKey, sessionKey, } from "../../execution/types.js";
|
|
27
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
28
|
+
export function createRestateExecution(options) {
|
|
29
|
+
return new RestateExecution(options);
|
|
30
|
+
}
|
|
31
|
+
export class RestateExecution {
|
|
32
|
+
options;
|
|
33
|
+
handlers;
|
|
34
|
+
stopping = false;
|
|
35
|
+
server;
|
|
36
|
+
sessions = new Set();
|
|
37
|
+
controllers = new Set();
|
|
38
|
+
inflight = new Set();
|
|
39
|
+
names;
|
|
40
|
+
ingressUrl;
|
|
41
|
+
adminUrl;
|
|
42
|
+
sweepIntervalMs;
|
|
43
|
+
constructor(options) {
|
|
44
|
+
this.options = options;
|
|
45
|
+
const prefix = options.servicePrefix ?? "";
|
|
46
|
+
if (!/^[A-Za-z0-9_]*$/.test(prefix))
|
|
47
|
+
throw new Error(`Invalid Restate service prefix: ${prefix}`);
|
|
48
|
+
this.names = {
|
|
49
|
+
session: `${prefix}NylorunSession`,
|
|
50
|
+
tenant: `${prefix}NylorunTenant`,
|
|
51
|
+
timer: `${prefix}NylorunTimer`,
|
|
52
|
+
};
|
|
53
|
+
this.ingressUrl = options.ingressUrl.replace(/\/+$/, "");
|
|
54
|
+
this.adminUrl = options.adminUrl.replace(/\/+$/, "");
|
|
55
|
+
this.sweepIntervalMs = options.sweepIntervalMs ?? 5000;
|
|
56
|
+
}
|
|
57
|
+
/** Service names this execution registers, for diagnostics and tests. */
|
|
58
|
+
get serviceNames() {
|
|
59
|
+
return { ...this.names };
|
|
60
|
+
}
|
|
61
|
+
async wake(tenantId, sessionId, wake) {
|
|
62
|
+
if (!WAKE_REASONS.includes(wake.reason))
|
|
63
|
+
throw new Error(`Unknown wake reason: ${String(wake.reason)}`);
|
|
64
|
+
await this.send(this.names.session, sessionKey(tenantId, sessionId), "advance", { reason: wake.reason }, wake.dedupeKey);
|
|
65
|
+
}
|
|
66
|
+
async timer(tenantId, key, at) {
|
|
67
|
+
await this.send(this.names.timer, sessionKey(tenantId, key), "set", {
|
|
68
|
+
at: at.getTime(),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async armSweep(tenantId) {
|
|
72
|
+
await this.send(this.names.tenant, tenantKey(tenantId), "arm", {});
|
|
73
|
+
}
|
|
74
|
+
async disarmSweep(tenantId) {
|
|
75
|
+
await this.send(this.names.tenant, tenantKey(tenantId), "disarm", {});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Readiness: the admin API (`/health`), which `start` registers with, and the
|
|
79
|
+
* ingress (`/restate/health`), which every wake, timer and sweep goes
|
|
80
|
+
* through, both answer.
|
|
81
|
+
*/
|
|
82
|
+
async probe(signal) {
|
|
83
|
+
await Promise.all([`${this.adminUrl}/health`, `${this.ingressUrl}/restate/health`].map(async (url) => {
|
|
84
|
+
const response = await fetch(url, { signal });
|
|
85
|
+
await response.body?.cancel();
|
|
86
|
+
if (!response.ok)
|
|
87
|
+
throw new Error(`Restate health ${response.status} at ${url}`);
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
/** This Runtime's paused or backing-off invocations for `tenantId` (Tenant status). */
|
|
91
|
+
stuckInvocations(tenantId) {
|
|
92
|
+
return listStuckInvocations({
|
|
93
|
+
adminUrl: this.adminUrl,
|
|
94
|
+
servicePrefix: this.options.servicePrefix ?? "",
|
|
95
|
+
tenantId,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async start(handlers) {
|
|
99
|
+
if (this.handlers)
|
|
100
|
+
throw new Error("DurableExecution already started");
|
|
101
|
+
const { workerListen, workerAdvertisedUrl } = this.options;
|
|
102
|
+
if (!workerListen || !workerAdvertisedUrl)
|
|
103
|
+
throw new Error("Restate execution needs workerListen and workerAdvertisedUrl to start");
|
|
104
|
+
this.handlers = handlers;
|
|
105
|
+
this.stopping = false;
|
|
106
|
+
const server = createServer(restate.createEndpointHandler({
|
|
107
|
+
services: this.definitions(),
|
|
108
|
+
...(this.options.identityKeys?.length
|
|
109
|
+
? { identityKeys: this.options.identityKeys }
|
|
110
|
+
: {}),
|
|
111
|
+
...(this.options.logger ? { logger: sdkLogger(this.options.logger) } : {}),
|
|
112
|
+
}));
|
|
113
|
+
server.on("session", (session) => {
|
|
114
|
+
this.sessions.add(session);
|
|
115
|
+
session.once("close", () => this.sessions.delete(session));
|
|
116
|
+
});
|
|
117
|
+
await new Promise((resolve, reject) => {
|
|
118
|
+
server.once("error", reject);
|
|
119
|
+
server.listen(workerListen.port, workerListen.host, () => {
|
|
120
|
+
server.off("error", reject);
|
|
121
|
+
resolve();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
this.server = server;
|
|
125
|
+
try {
|
|
126
|
+
await this.register(workerAdvertisedUrl);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
await this.stop();
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async stop() {
|
|
134
|
+
this.stopping = true;
|
|
135
|
+
for (const controller of this.controllers)
|
|
136
|
+
controller.abort(new Error("Worker stopping"));
|
|
137
|
+
while (this.inflight.size > 0)
|
|
138
|
+
await Promise.allSettled(this.inflight);
|
|
139
|
+
this.handlers = undefined;
|
|
140
|
+
const server = this.server;
|
|
141
|
+
this.server = undefined;
|
|
142
|
+
if (!server)
|
|
143
|
+
return;
|
|
144
|
+
// Let Restate read the responses already written, then drop the
|
|
145
|
+
// connections it keeps open so `close` can finish.
|
|
146
|
+
const closed = new Promise((resolve) => server.close(() => resolve()));
|
|
147
|
+
for (const session of this.sessions)
|
|
148
|
+
session.close();
|
|
149
|
+
const timeout = setTimeout(() => {
|
|
150
|
+
for (const session of this.sessions)
|
|
151
|
+
session.destroy();
|
|
152
|
+
}, 2000);
|
|
153
|
+
await closed;
|
|
154
|
+
clearTimeout(timeout);
|
|
155
|
+
}
|
|
156
|
+
definitions() {
|
|
157
|
+
const names = this.names;
|
|
158
|
+
const serviceOptions = this.serviceOptions();
|
|
159
|
+
const session = restate.object({
|
|
160
|
+
name: names.session,
|
|
161
|
+
handlers: {
|
|
162
|
+
advance: async (ctx, _wake) => {
|
|
163
|
+
const { tenantId, sessionId } = parseSessionKey(ctx.key);
|
|
164
|
+
const result = await this.runAdvance(ctx.request().attemptCompletedSignal, (handlers, signal) => handlers.advance(tenantId, sessionId, signal));
|
|
165
|
+
if (result.status === "busy")
|
|
166
|
+
ctx
|
|
167
|
+
.objectSendClient({ name: names.session }, ctx.key)
|
|
168
|
+
.advance({ reason: "recover" }, restate.rpc.sendOpts({ delay: Math.max(0, result.retryAfterMs) }));
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
options: {
|
|
172
|
+
...serviceOptions,
|
|
173
|
+
idempotencyRetention: Math.max(this.options.dedupeRetentionMs ?? 24 * HOUR_MS, 24 * HOUR_MS),
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
const tenant = restate.object({
|
|
177
|
+
name: names.tenant,
|
|
178
|
+
handlers: {
|
|
179
|
+
arm: async (ctx) => {
|
|
180
|
+
const now = await ctx.date.now();
|
|
181
|
+
const state = await ctx.get("sweep");
|
|
182
|
+
// An armed chain that beat recently is alive; a stale one was lost
|
|
183
|
+
// (killed or purged invocation) and is replaced.
|
|
184
|
+
if (state && now - state.beatAt < this.staleSweepMs())
|
|
185
|
+
return;
|
|
186
|
+
const generation = (state?.generation ?? 0) + 1;
|
|
187
|
+
ctx.set("sweep", { generation, beatAt: now });
|
|
188
|
+
ctx
|
|
189
|
+
.objectSendClient({ name: names.tenant }, ctx.key)
|
|
190
|
+
.sweep({ generation });
|
|
191
|
+
},
|
|
192
|
+
sweep: async (ctx, input) => {
|
|
193
|
+
const state = await ctx.get("sweep");
|
|
194
|
+
if (!state || state.generation !== input.generation)
|
|
195
|
+
return;
|
|
196
|
+
await this.runTracked((handlers) => handlers.sweep(ctx.key));
|
|
197
|
+
ctx.set("sweep", {
|
|
198
|
+
generation: input.generation,
|
|
199
|
+
beatAt: await ctx.date.now(),
|
|
200
|
+
});
|
|
201
|
+
ctx
|
|
202
|
+
.objectSendClient({ name: names.tenant }, ctx.key)
|
|
203
|
+
.sweep({ generation: input.generation }, restate.rpc.sendOpts({ delay: this.sweepIntervalMs }));
|
|
204
|
+
},
|
|
205
|
+
disarm: async (ctx) => {
|
|
206
|
+
ctx.clear("sweep");
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
options: serviceOptions,
|
|
210
|
+
});
|
|
211
|
+
const timer = restate.object({
|
|
212
|
+
name: names.timer,
|
|
213
|
+
handlers: {
|
|
214
|
+
set: async (ctx, input) => {
|
|
215
|
+
ctx.set("at", input.at);
|
|
216
|
+
const now = await ctx.date.now();
|
|
217
|
+
ctx
|
|
218
|
+
.objectSendClient({ name: names.timer }, ctx.key)
|
|
219
|
+
.fire(input, restate.rpc.sendOpts({ delay: Math.max(0, input.at - now) }));
|
|
220
|
+
},
|
|
221
|
+
fire: async (ctx, input) => {
|
|
222
|
+
if ((await ctx.get("at")) !== input.at)
|
|
223
|
+
return;
|
|
224
|
+
const { tenantId, sessionId: key } = parseSessionKey(ctx.key);
|
|
225
|
+
await this.runTracked((handlers) => {
|
|
226
|
+
if (!handlers.fire)
|
|
227
|
+
throw new Error("WorkerHandlers.fire is required for timers");
|
|
228
|
+
return handlers.fire(tenantId, key);
|
|
229
|
+
});
|
|
230
|
+
ctx.clear("at");
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
options: serviceOptions,
|
|
234
|
+
});
|
|
235
|
+
return [session, tenant, timer];
|
|
236
|
+
}
|
|
237
|
+
/** Runs `advance` with a signal aborted by `stop` or by the end of the Restate attempt. */
|
|
238
|
+
async runAdvance(attemptDone, fn) {
|
|
239
|
+
const controller = new AbortController();
|
|
240
|
+
const onAttemptDone = () => controller.abort(new Error("Restate attempt ended"));
|
|
241
|
+
if (attemptDone.aborted)
|
|
242
|
+
onAttemptDone();
|
|
243
|
+
else
|
|
244
|
+
attemptDone.addEventListener("abort", onAttemptDone, { once: true });
|
|
245
|
+
this.controllers.add(controller);
|
|
246
|
+
try {
|
|
247
|
+
return await this.runTracked((handlers) => fn(handlers, controller.signal));
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
this.controllers.delete(controller);
|
|
251
|
+
attemptDone.removeEventListener("abort", onAttemptDone);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** Runs a Worker handler so that `stop` waits for it. Refuses once stopping; Restate retries. */
|
|
255
|
+
runTracked(fn) {
|
|
256
|
+
const handlers = this.handlers;
|
|
257
|
+
if (!handlers || this.stopping)
|
|
258
|
+
return Promise.reject(new Error("Worker is stopping"));
|
|
259
|
+
const run = fn(handlers);
|
|
260
|
+
this.inflight.add(run);
|
|
261
|
+
void run.then(() => this.inflight.delete(run), () => this.inflight.delete(run));
|
|
262
|
+
return run;
|
|
263
|
+
}
|
|
264
|
+
staleSweepMs() {
|
|
265
|
+
return Math.max(this.sweepIntervalMs * 3, 60_000);
|
|
266
|
+
}
|
|
267
|
+
serviceOptions() {
|
|
268
|
+
const timeouts = this.options.timeouts ?? {};
|
|
269
|
+
const retry = this.options.retry ?? {};
|
|
270
|
+
return {
|
|
271
|
+
inactivityTimeout: timeouts.inactivityMs ?? HOUR_MS,
|
|
272
|
+
abortTimeout: timeouts.abortMs ?? HOUR_MS,
|
|
273
|
+
retryPolicy: {
|
|
274
|
+
initialInterval: retry.initialIntervalMs ?? 100,
|
|
275
|
+
maxInterval: retry.maxIntervalMs ?? 30_000,
|
|
276
|
+
exponentiationFactor: retry.exponentiationFactor ?? 2,
|
|
277
|
+
maxAttempts: retry.maxAttempts ?? 70,
|
|
278
|
+
onMaxAttempts: "pause",
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/** One-way send through the ingress. Retries while the ingress is unreachable or overloaded. */
|
|
283
|
+
async send(service, key, handler, body, idempotencyKey) {
|
|
284
|
+
const url = `${this.ingressUrl}/${service}/${encodeURIComponent(key)}/${handler}/send`;
|
|
285
|
+
const headers = { "content-type": "application/json" };
|
|
286
|
+
if (idempotencyKey !== undefined)
|
|
287
|
+
headers["idempotency-key"] = idempotencyKey;
|
|
288
|
+
await withRetry(`Restate send to ${service}/${handler}`, 10_000, async () => {
|
|
289
|
+
const response = await fetch(url, {
|
|
290
|
+
method: "POST",
|
|
291
|
+
headers,
|
|
292
|
+
body: JSON.stringify(body),
|
|
293
|
+
signal: AbortSignal.timeout(10_000),
|
|
294
|
+
});
|
|
295
|
+
if (response.ok) {
|
|
296
|
+
await response.body?.cancel();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const text = await response.text().catch(() => "");
|
|
300
|
+
const error = new Error(`Restate ingress ${response.status} for ${service}/${handler}: ${text}`);
|
|
301
|
+
// A just-registered service can take a moment to reach the ingress.
|
|
302
|
+
throw response.status >= 500 || response.status === 404 || response.status === 429
|
|
303
|
+
? error
|
|
304
|
+
: new PermanentError(error.message);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
/** Registers the Worker endpoint with the admin API, retrying until Restate is up. */
|
|
308
|
+
async register(uri) {
|
|
309
|
+
const force = this.options.forceRegistration ?? true;
|
|
310
|
+
await withRetry("Restate deployment registration", this.options.registrationTimeoutMs ?? 60_000, async () => {
|
|
311
|
+
const response = await fetch(`${this.adminUrl}/deployments`, {
|
|
312
|
+
method: "POST",
|
|
313
|
+
headers: { "content-type": "application/json" },
|
|
314
|
+
body: JSON.stringify({ uri, force }),
|
|
315
|
+
signal: AbortSignal.timeout(30_000),
|
|
316
|
+
});
|
|
317
|
+
if (response.ok) {
|
|
318
|
+
await response.body?.cancel();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const text = await response.text().catch(() => "");
|
|
322
|
+
const message = `Restate admin ${response.status} registering ${uri}: ${text}`;
|
|
323
|
+
// Discovery failures (Restate cannot reach the endpoint yet) come back
|
|
324
|
+
// as 4xx/5xx; keep trying until the deadline.
|
|
325
|
+
throw new Error(message);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Lists invocations of this Runtime's services that need an operator: paused
|
|
331
|
+
* after exhausting retries, or backing off after a failure. Reads Restate's
|
|
332
|
+
* SQL introspection (`POST /query` on the admin API).
|
|
333
|
+
*/
|
|
334
|
+
export async function listStuckInvocations(options) {
|
|
335
|
+
const prefix = options.servicePrefix ?? "";
|
|
336
|
+
if (!/^[A-Za-z0-9_]*$/.test(prefix))
|
|
337
|
+
throw new Error(`Invalid Restate service prefix: ${prefix}`);
|
|
338
|
+
const services = ["NylorunSession", "NylorunTenant", "NylorunTimer"].map((name) => `'${prefix}${name}'`);
|
|
339
|
+
const clauses = [
|
|
340
|
+
`target_service_name IN (${services.join(", ")})`,
|
|
341
|
+
`status IN ('paused', 'backing-off')`,
|
|
342
|
+
];
|
|
343
|
+
if (options.tenantId !== undefined) {
|
|
344
|
+
const tenant = sqlString(options.tenantId);
|
|
345
|
+
clauses.push(`(target_service_key = ${tenant} OR starts_with(target_service_key, ${sqlString(`${options.tenantId}:`)}))`);
|
|
346
|
+
}
|
|
347
|
+
const limit = Math.max(1, Math.min(options.limit ?? 100, 1000));
|
|
348
|
+
const rows = await query(options.adminUrl, "SELECT id, status, target_service_name, target_handler_name, target_service_key, " +
|
|
349
|
+
"retry_count, last_failure, modified_at FROM sys_invocation " +
|
|
350
|
+
`WHERE ${clauses.join(" AND ")} ORDER BY modified_at LIMIT ${limit}`);
|
|
351
|
+
// Restate 1.7 clears `last_failure` and `retry_count` when it pauses an
|
|
352
|
+
// invocation; the failure is kept on the journal's `Paused` event.
|
|
353
|
+
const paused = rows.filter((row) => row.status === "paused").map((row) => String(row.id));
|
|
354
|
+
const pausedFailures = new Map();
|
|
355
|
+
if (paused.length > 0) {
|
|
356
|
+
const events = await query(options.adminUrl, "SELECT id, appended_at, event_json FROM sys_journal_events " +
|
|
357
|
+
`WHERE event_type = 'Paused' AND id IN (${paused.map(sqlString).join(", ")}) ` +
|
|
358
|
+
"ORDER BY appended_at");
|
|
359
|
+
for (const event of events) {
|
|
360
|
+
const message = pausedFailure(event.event_json);
|
|
361
|
+
if (message !== undefined)
|
|
362
|
+
pausedFailures.set(String(event.id), message);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return rows.map((row) => {
|
|
366
|
+
const id = String(row.id);
|
|
367
|
+
const key = String(row.target_service_key ?? "");
|
|
368
|
+
const colon = key.indexOf(":");
|
|
369
|
+
const lastFailure = row.last_failure == null ? pausedFailures.get(id) : String(row.last_failure);
|
|
370
|
+
const modifiedAt = row.modified_at == null ? undefined : String(row.modified_at);
|
|
371
|
+
return {
|
|
372
|
+
id,
|
|
373
|
+
status: String(row.status),
|
|
374
|
+
service: String(row.target_service_name).slice(prefix.length),
|
|
375
|
+
handler: String(row.target_handler_name),
|
|
376
|
+
key,
|
|
377
|
+
...(key ? { tenantId: colon > 0 ? key.slice(0, colon) : key } : {}),
|
|
378
|
+
retryCount: Number(row.retry_count ?? 0),
|
|
379
|
+
...(lastFailure !== undefined ? { lastFailure } : {}),
|
|
380
|
+
...(modifiedAt !== undefined ? { modifiedAt } : {}),
|
|
381
|
+
};
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
async function query(adminUrl, sql) {
|
|
385
|
+
const response = await fetch(`${adminUrl.replace(/\/+$/, "")}/query`, {
|
|
386
|
+
method: "POST",
|
|
387
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
388
|
+
body: JSON.stringify({ query: sql }),
|
|
389
|
+
signal: AbortSignal.timeout(10_000),
|
|
390
|
+
});
|
|
391
|
+
if (!response.ok)
|
|
392
|
+
throw new Error(`Restate query ${response.status}: ${await response.text().catch(() => "")}`);
|
|
393
|
+
return (await response.json()).rows;
|
|
394
|
+
}
|
|
395
|
+
function pausedFailure(eventJson) {
|
|
396
|
+
try {
|
|
397
|
+
const event = JSON.parse(String(eventJson));
|
|
398
|
+
const failure = event.last_failure;
|
|
399
|
+
if (!failure?.error_message)
|
|
400
|
+
return undefined;
|
|
401
|
+
return failure.error_code === undefined
|
|
402
|
+
? failure.error_message
|
|
403
|
+
: `[${failure.error_code}] ${failure.error_message}`;
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function tenantKey(tenantId) {
|
|
410
|
+
if (!tenantId || tenantId.includes(":"))
|
|
411
|
+
throw new Error(`Invalid tenant id: ${tenantId}`);
|
|
412
|
+
return tenantId;
|
|
413
|
+
}
|
|
414
|
+
function sqlString(value) {
|
|
415
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
416
|
+
}
|
|
417
|
+
class PermanentError extends Error {
|
|
418
|
+
}
|
|
419
|
+
async function withRetry(what, timeoutMs, attempt) {
|
|
420
|
+
const deadline = Date.now() + timeoutMs;
|
|
421
|
+
let delay = 100;
|
|
422
|
+
for (;;) {
|
|
423
|
+
try {
|
|
424
|
+
await attempt();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
if (error instanceof PermanentError || Date.now() + delay > deadline)
|
|
429
|
+
throw new Error(`${what} failed: ${error.message}`, {
|
|
430
|
+
cause: error,
|
|
431
|
+
});
|
|
432
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
433
|
+
delay = Math.min(delay * 2, 2000);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function sdkLogger(log) {
|
|
438
|
+
return (meta, message, ...rest) => {
|
|
439
|
+
const parts = [message, ...rest].map((part) => part instanceof Error ? part.stack ?? part.message : String(part));
|
|
440
|
+
log(meta.level, parts.join(" "));
|
|
441
|
+
};
|
|
442
|
+
}
|
package/dist/adapters/media.d.ts
CHANGED
package/dist/adapters/media.js
CHANGED
|
@@ -106,6 +106,6 @@ function omitCreatedAt(asset) {
|
|
|
106
106
|
function safe(value) {
|
|
107
107
|
return value !== "." && value !== ".." && /^[a-zA-Z0-9._-]+$/u.test(value);
|
|
108
108
|
}
|
|
109
|
-
export function localMedia(options
|
|
110
|
-
return new MediaStore(options.root
|
|
109
|
+
export function localMedia(options) {
|
|
110
|
+
return new MediaStore(options.root);
|
|
111
111
|
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
export declare function jsonlObserver(options: {
|
|
3
3
|
readonly agentId: string;
|
|
4
4
|
readonly sessionId: string;
|
|
5
|
-
readonly root
|
|
5
|
+
readonly root: string;
|
|
6
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
6
7
|
}): {
|
|
7
8
|
(event: {
|
|
8
9
|
readonly type: string;
|
package/dist/adapters/observe.js
CHANGED
|
@@ -4,10 +4,9 @@ import { scrub } from "../redact.js";
|
|
|
4
4
|
import { projectSecrets } from "../model/settings.js";
|
|
5
5
|
/** Local JSONL observer. Writes raw engine events beside session durability files. */
|
|
6
6
|
export function jsonlObserver(options) {
|
|
7
|
-
const
|
|
8
|
-
const directory = join(root, safe(options.agentId), safe(options.sessionId));
|
|
7
|
+
const directory = join(options.root, safe(options.agentId), safe(options.sessionId));
|
|
9
8
|
const file = join(directory, "observe.jsonl");
|
|
10
|
-
const secrets = projectSecrets();
|
|
9
|
+
const secrets = projectSecrets(options.root, options.env ?? {});
|
|
11
10
|
return (event) => {
|
|
12
11
|
const line = `${JSON.stringify(scrub(event, secrets))}\n`;
|
|
13
12
|
void mkdir(directory, { recursive: true })
|
|
@@ -36,7 +36,7 @@ export function virtualBackend(options) {
|
|
|
36
36
|
},
|
|
37
37
|
unmet(spec) {
|
|
38
38
|
if (spec.network.suffixes.length > 0)
|
|
39
|
-
return "the virtual backend cannot allow wildcard hosts; list exact host names in network.allow
|
|
39
|
+
return "the virtual backend cannot allow wildcard hosts; list exact host names in network.allow";
|
|
40
40
|
return undefined;
|
|
41
41
|
},
|
|
42
42
|
async open(spec) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type DurableStreams } from "../../streams/types.js";
|
|
2
|
+
export interface S2StreamsOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Base URL serving both S2's account and basin APIs, e.g. `http://s2:80` for
|
|
5
|
+
* s2-lite. Omit to use the S2 service's default endpoints.
|
|
6
|
+
*/
|
|
7
|
+
endpoint?: string;
|
|
8
|
+
/** Access token. s2-lite needs none. */
|
|
9
|
+
token?: string;
|
|
10
|
+
/** Prepended to every basin name (see `streams/basin.ts`). */
|
|
11
|
+
basinPrefix?: string;
|
|
12
|
+
/**
|
|
13
|
+
* How long `ensureTenant` waits for a previous deletion of the same basin to
|
|
14
|
+
* finish. Defaults to two minutes.
|
|
15
|
+
*/
|
|
16
|
+
basinDeletionWaitMs?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function createS2Streams(options?: S2StreamsOptions): DurableStreams;
|