@nylorun/runtime 0.10.0-beta → 0.12.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -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 +13 -4
- package/dist/core/sandbox-routes.js +14 -12
- 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 +51 -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 +645 -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/002_session_owner.d.ts +6 -0
- package/dist/store/postgres/migrations/002_session_owner.js +13 -0
- package/dist/store/postgres/migrations/index.d.ts +49 -0
- package/dist/store/postgres/migrations/index.js +76 -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 +797 -0
- package/dist/store/postgres/tenants.d.ts +90 -0
- package/dist/store/postgres/tenants.js +214 -0
- package/dist/store/types.d.ts +442 -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 +32 -0
- package/dist/tenant/auth.js +169 -0
- package/dist/tenant/commands.d.ts +3 -0
- package/dist/tenant/commands.js +393 -0
- package/dist/tenant/context.d.ts +147 -0
- package/dist/tenant/context.js +48 -0
- package/dist/tenant/effects.d.ts +20 -0
- package/dist/tenant/effects.js +564 -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 +38 -0
- package/dist/tenant/http.js +53 -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 +165 -158
- package/dist/tenant/paths.d.ts +12 -8
- package/dist/tenant/paths.js +38 -12
- package/dist/tenant/principals.d.ts +12 -13
- package/dist/tenant/principals.js +50 -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 +137 -0
- package/dist/tenant/routes.d.ts +9 -0
- package/dist/tenant/routes.js +151 -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 +40 -0
- package/dist/tenant/sessions.js +163 -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/transcript.d.ts +104 -0
- package/dist/tenant/transcript.js +58 -0
- package/dist/tenant/types.d.ts +101 -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 +44 -31
- package/dist/vault/service.js +454 -360
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +15 -14
- 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
|
@@ -10,11 +10,16 @@ export type SandboxRouteSession = SessionSandboxRef & {
|
|
|
10
10
|
readonly activeTurnId: string | null;
|
|
11
11
|
readonly status: string;
|
|
12
12
|
};
|
|
13
|
+
/** A session together with a synchronous lookup over its sandbox owner chain. */
|
|
14
|
+
export type SandboxRouteRead = {
|
|
15
|
+
readonly session: SandboxRouteSession;
|
|
16
|
+
readonly lookup: (id: string) => SessionSandboxRef | undefined;
|
|
17
|
+
};
|
|
13
18
|
export type SandboxRouteDeps = {
|
|
14
19
|
readonly sandbox: SandboxManager;
|
|
15
|
-
|
|
16
|
-
readonly
|
|
17
|
-
readonly getAction: (id: string) => Action | undefined
|
|
20
|
+
/** The session (rejects when missing) and its owner chain, read in one transaction. */
|
|
21
|
+
readonly session: (id: string) => Promise<SandboxRouteRead>;
|
|
22
|
+
readonly getAction: (id: string) => Promise<Action | undefined>;
|
|
18
23
|
};
|
|
19
24
|
/**
|
|
20
25
|
* `POST /v1/sessions/:id/sandbox/:tool` — application principal.
|
|
@@ -32,4 +37,8 @@ export declare function validateSandboxAttach(body: {
|
|
|
32
37
|
sandbox?: {
|
|
33
38
|
session: string;
|
|
34
39
|
};
|
|
35
|
-
}, newManifest: SessionSandboxRef["manifest"], lookup: (id: string) => SessionSandboxRef | undefined
|
|
40
|
+
}, newManifest: SessionSandboxRef["manifest"], lookup: (id: string) => SessionSandboxRef | undefined,
|
|
41
|
+
/** Acting for a subject: another owner's session is the 404 of a missing one. */
|
|
42
|
+
options?: {
|
|
43
|
+
opaque?: boolean;
|
|
44
|
+
}): string | undefined;
|
|
@@ -34,9 +34,9 @@ function toolInput(body) {
|
|
|
34
34
|
const { claimId: _c, generation: _g, requestId: _r, ...input } = record;
|
|
35
35
|
return input;
|
|
36
36
|
}
|
|
37
|
-
async function runTool(deps, session, tool, input, signal) {
|
|
38
|
-
const spec = resolveSpec(session,
|
|
39
|
-
const ownerId = owningSandboxSessionId(session,
|
|
37
|
+
async function runTool(deps, { session, lookup }, tool, input, signal) {
|
|
38
|
+
const spec = resolveSpec(session, lookup);
|
|
39
|
+
const ownerId = owningSandboxSessionId(session, lookup);
|
|
40
40
|
const ref = {
|
|
41
41
|
id: ownerId,
|
|
42
42
|
activeTurnId: session.activeTurnId,
|
|
@@ -49,11 +49,11 @@ async function runTool(deps, session, tool, input, signal) {
|
|
|
49
49
|
* Refused while the session has an active agent turn.
|
|
50
50
|
*/
|
|
51
51
|
export async function handleSessionSandboxTool(deps, sessionId, toolName, body, signal) {
|
|
52
|
-
const
|
|
53
|
-
if (session.activeTurnId)
|
|
52
|
+
const read = await deps.session(sessionId);
|
|
53
|
+
if (read.session.activeTurnId)
|
|
54
54
|
throw new SandboxRouteError(409, "Sandbox is unavailable while the session has an active agent turn");
|
|
55
55
|
const tool = parseTool(toolName);
|
|
56
|
-
return runTool(deps,
|
|
56
|
+
return runTool(deps, read, tool, toolInput(body), signal);
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
59
|
* `POST /v1/actions/:id/sandbox/:tool` — executor, authorized by the live claim.
|
|
@@ -69,7 +69,7 @@ export async function handleActionSandboxTool(deps, actionId, toolName, body, si
|
|
|
69
69
|
throw new SandboxRouteError(400, "claimId is required");
|
|
70
70
|
if (typeof generation !== "number" || !Number.isInteger(generation) || generation < 1)
|
|
71
71
|
throw new SandboxRouteError(400, "generation must be a positive integer");
|
|
72
|
-
const action = deps.getAction(actionId);
|
|
72
|
+
const action = await deps.getAction(actionId);
|
|
73
73
|
if (!action)
|
|
74
74
|
throw new SandboxRouteError(404, "Action not found");
|
|
75
75
|
if (action.status !== "claimed" ||
|
|
@@ -78,19 +78,21 @@ export async function handleActionSandboxTool(deps, actionId, toolName, body, si
|
|
|
78
78
|
!action.leaseExpiresAt ||
|
|
79
79
|
Date.parse(action.leaseExpiresAt) <= Date.now())
|
|
80
80
|
throw new SandboxRouteError(409, "Stale or expired claim");
|
|
81
|
-
const
|
|
82
|
-
if (session.activeTurnId !== action.turnId)
|
|
81
|
+
const read = await deps.session(action.sessionId);
|
|
82
|
+
if (read.session.activeTurnId !== action.turnId)
|
|
83
83
|
throw new SandboxRouteError(409, "Action unavailable");
|
|
84
84
|
const tool = parseTool(toolName);
|
|
85
|
-
return runTool(deps,
|
|
85
|
+
return runTool(deps, read, tool, toolInput(body), signal);
|
|
86
86
|
}
|
|
87
87
|
/** Validate PutSession.sandbox attach: same owner, identical specs. */
|
|
88
|
-
export function validateSandboxAttach(body, newManifest, lookup
|
|
88
|
+
export function validateSandboxAttach(body, newManifest, lookup,
|
|
89
|
+
/** Acting for a subject: another owner's session is the 404 of a missing one. */
|
|
90
|
+
options = {}) {
|
|
89
91
|
const share = body.sandbox?.session;
|
|
90
92
|
if (!share)
|
|
91
93
|
return undefined;
|
|
92
94
|
const owner = lookup(share);
|
|
93
|
-
if (!owner)
|
|
95
|
+
if (!owner || (options.opaque && owner.ownerUserId !== body.ownerUserId))
|
|
94
96
|
throw new SandboxRouteError(404, "Sandbox session not found");
|
|
95
97
|
if (owner.ownerUserId !== body.ownerUserId)
|
|
96
98
|
throw new SandboxRouteError(403, "Sandbox session must belong to the same owner");
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type DurableExecution, type Wake, type WorkerHandlers } from "./types.js";
|
|
2
|
+
export interface MemoryExecutionOptions {
|
|
3
|
+
/** Delay between sweep passes of one Tenant. Default 5000. */
|
|
4
|
+
sweepIntervalMs?: number;
|
|
5
|
+
/** First retry delay after `advance` throws; doubles per attempt. Default 50. */
|
|
6
|
+
retryDelayMs?: number;
|
|
7
|
+
/** Attempts before giving up on a throwing `advance` and reporting it. Default 5. */
|
|
8
|
+
maxAttempts?: number;
|
|
9
|
+
/** How long a dedupe key merges repeated wakes. Default 24 hours. */
|
|
10
|
+
dedupeRetentionMs?: number;
|
|
11
|
+
/** Receives handler failures that are not retried. */
|
|
12
|
+
onError?: (error: unknown) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* In-process `DurableExecution`. Not durable: wakes, timers and sweeps live in
|
|
16
|
+
* memory. It keeps the seam's ordering guarantees within one process, which is
|
|
17
|
+
* what unit tests and the Runtime before Restate (Wave 2) need.
|
|
18
|
+
*/
|
|
19
|
+
export declare class MemoryExecution implements DurableExecution {
|
|
20
|
+
private handlers?;
|
|
21
|
+
private readonly keys;
|
|
22
|
+
/** Dedupe keys seen, with the time each was first seen; insertion order is age order. */
|
|
23
|
+
private readonly seen;
|
|
24
|
+
private readonly timers;
|
|
25
|
+
private readonly sweeps;
|
|
26
|
+
private readonly delayed;
|
|
27
|
+
private readonly inflight;
|
|
28
|
+
private readonly sweepIntervalMs;
|
|
29
|
+
private readonly retryDelayMs;
|
|
30
|
+
private readonly maxAttempts;
|
|
31
|
+
private readonly dedupeRetentionMs;
|
|
32
|
+
private readonly onError;
|
|
33
|
+
constructor(options?: MemoryExecutionOptions);
|
|
34
|
+
wake(tenantId: string, sessionId: string, wake: Wake): Promise<void>;
|
|
35
|
+
timer(tenantId: string, key: string, at: Date): Promise<void>;
|
|
36
|
+
armSweep(tenantId: string): Promise<void>;
|
|
37
|
+
disarmSweep(tenantId: string): Promise<void>;
|
|
38
|
+
start(handlers: WorkerHandlers): Promise<void>;
|
|
39
|
+
stop(): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Resolves once no advance is running or queued and no busy or retry
|
|
42
|
+
* re-wake is pending. Timers and sweeps are not waited for. Test helper.
|
|
43
|
+
*/
|
|
44
|
+
idle(): Promise<void>;
|
|
45
|
+
private enqueue;
|
|
46
|
+
private drain;
|
|
47
|
+
/** Drops dedupe keys older than the retention window. */
|
|
48
|
+
private forgetDedupeKeys;
|
|
49
|
+
private advanceOnce;
|
|
50
|
+
private armTimer;
|
|
51
|
+
private scheduleSweep;
|
|
52
|
+
private later;
|
|
53
|
+
private track;
|
|
54
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { WAKE_REASONS, sessionKey, } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* In-process `DurableExecution`. Not durable: wakes, timers and sweeps live in
|
|
4
|
+
* memory. It keeps the seam's ordering guarantees within one process, which is
|
|
5
|
+
* what unit tests and the Runtime before Restate (Wave 2) need.
|
|
6
|
+
*/
|
|
7
|
+
export class MemoryExecution {
|
|
8
|
+
handlers;
|
|
9
|
+
keys = new Map();
|
|
10
|
+
/** Dedupe keys seen, with the time each was first seen; insertion order is age order. */
|
|
11
|
+
seen = new Map();
|
|
12
|
+
timers = new Map();
|
|
13
|
+
sweeps = new Map();
|
|
14
|
+
delayed = new Set();
|
|
15
|
+
inflight = new Set();
|
|
16
|
+
sweepIntervalMs;
|
|
17
|
+
retryDelayMs;
|
|
18
|
+
maxAttempts;
|
|
19
|
+
dedupeRetentionMs;
|
|
20
|
+
onError;
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this.sweepIntervalMs = options.sweepIntervalMs ?? 5000;
|
|
23
|
+
this.retryDelayMs = options.retryDelayMs ?? 50;
|
|
24
|
+
this.maxAttempts = options.maxAttempts ?? 5;
|
|
25
|
+
this.dedupeRetentionMs = options.dedupeRetentionMs ?? 24 * 60 * 60_000;
|
|
26
|
+
this.onError =
|
|
27
|
+
options.onError ??
|
|
28
|
+
((error) => queueMicrotask(() => {
|
|
29
|
+
throw error;
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
async wake(tenantId, sessionId, wake) {
|
|
33
|
+
if (!WAKE_REASONS.includes(wake.reason))
|
|
34
|
+
throw new Error(`Unknown wake reason: ${String(wake.reason)}`);
|
|
35
|
+
const key = sessionKey(tenantId, sessionId);
|
|
36
|
+
if (wake.dedupeKey !== undefined) {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
this.forgetDedupeKeys(now);
|
|
39
|
+
const dedupe = `${key}\u0000${wake.dedupeKey}`;
|
|
40
|
+
if (this.seen.has(dedupe))
|
|
41
|
+
return;
|
|
42
|
+
this.seen.set(dedupe, now);
|
|
43
|
+
}
|
|
44
|
+
this.enqueue(tenantId, sessionId);
|
|
45
|
+
}
|
|
46
|
+
async timer(tenantId, key, at) {
|
|
47
|
+
const id = `${tenantId}\u0000${key}`;
|
|
48
|
+
const existing = this.timers.get(id);
|
|
49
|
+
if (existing?.handle)
|
|
50
|
+
clearTimeout(existing.handle);
|
|
51
|
+
const timer = { tenantId, key, at: at.getTime() };
|
|
52
|
+
this.timers.set(id, timer);
|
|
53
|
+
if (this.handlers)
|
|
54
|
+
this.armTimer(id, timer);
|
|
55
|
+
}
|
|
56
|
+
async armSweep(tenantId) {
|
|
57
|
+
if (this.sweeps.has(tenantId))
|
|
58
|
+
return;
|
|
59
|
+
const sweep = { running: false };
|
|
60
|
+
this.sweeps.set(tenantId, sweep);
|
|
61
|
+
if (this.handlers)
|
|
62
|
+
this.scheduleSweep(tenantId, sweep, 0);
|
|
63
|
+
}
|
|
64
|
+
async disarmSweep(tenantId) {
|
|
65
|
+
const sweep = this.sweeps.get(tenantId);
|
|
66
|
+
if (sweep?.handle)
|
|
67
|
+
clearTimeout(sweep.handle);
|
|
68
|
+
this.sweeps.delete(tenantId);
|
|
69
|
+
}
|
|
70
|
+
async start(handlers) {
|
|
71
|
+
if (this.handlers)
|
|
72
|
+
throw new Error("DurableExecution already started");
|
|
73
|
+
this.handlers = handlers;
|
|
74
|
+
for (const state of this.keys.values())
|
|
75
|
+
if (state.queued && !state.running)
|
|
76
|
+
this.drain(state);
|
|
77
|
+
for (const [id, timer] of this.timers)
|
|
78
|
+
this.armTimer(id, timer);
|
|
79
|
+
for (const [tenantId, sweep] of this.sweeps)
|
|
80
|
+
this.scheduleSweep(tenantId, sweep, 0);
|
|
81
|
+
}
|
|
82
|
+
async stop() {
|
|
83
|
+
this.handlers = undefined;
|
|
84
|
+
for (const handle of this.delayed)
|
|
85
|
+
clearTimeout(handle);
|
|
86
|
+
this.delayed.clear();
|
|
87
|
+
for (const timer of this.timers.values())
|
|
88
|
+
if (timer.handle)
|
|
89
|
+
clearTimeout(timer.handle);
|
|
90
|
+
for (const sweep of this.sweeps.values())
|
|
91
|
+
if (sweep.handle)
|
|
92
|
+
clearTimeout(sweep.handle);
|
|
93
|
+
for (const state of this.keys.values())
|
|
94
|
+
state.controller?.abort();
|
|
95
|
+
while (this.inflight.size > 0)
|
|
96
|
+
await Promise.allSettled(this.inflight);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolves once no advance is running or queued and no busy or retry
|
|
100
|
+
* re-wake is pending. Timers and sweeps are not waited for. Test helper.
|
|
101
|
+
*/
|
|
102
|
+
async idle() {
|
|
103
|
+
while (this.delayed.size > 0 ||
|
|
104
|
+
this.inflight.size > 0 ||
|
|
105
|
+
[...this.keys.values()].some((s) => s.running || (s.queued && this.handlers)))
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
107
|
+
}
|
|
108
|
+
enqueue(tenantId, sessionId) {
|
|
109
|
+
const key = sessionKey(tenantId, sessionId);
|
|
110
|
+
let state = this.keys.get(key);
|
|
111
|
+
if (!state)
|
|
112
|
+
this.keys.set(key, (state = {
|
|
113
|
+
tenantId,
|
|
114
|
+
sessionId,
|
|
115
|
+
running: false,
|
|
116
|
+
queued: false,
|
|
117
|
+
attempts: 0,
|
|
118
|
+
}));
|
|
119
|
+
state.queued = true;
|
|
120
|
+
if (!state.running && this.handlers)
|
|
121
|
+
this.drain(state);
|
|
122
|
+
}
|
|
123
|
+
drain(state) {
|
|
124
|
+
state.running = true;
|
|
125
|
+
this.track((async () => {
|
|
126
|
+
try {
|
|
127
|
+
while (state.queued && this.handlers) {
|
|
128
|
+
state.queued = false;
|
|
129
|
+
await this.advanceOnce(state, this.handlers);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
state.running = false;
|
|
134
|
+
// Idle keys hold nothing (a retry keeps its attempt count); forget them so
|
|
135
|
+
// long-lived processes stay small.
|
|
136
|
+
const key = sessionKey(state.tenantId, state.sessionId);
|
|
137
|
+
if (!state.queued && state.attempts === 0 && this.keys.get(key) === state)
|
|
138
|
+
this.keys.delete(key);
|
|
139
|
+
}
|
|
140
|
+
})());
|
|
141
|
+
}
|
|
142
|
+
/** Drops dedupe keys older than the retention window. */
|
|
143
|
+
forgetDedupeKeys(now) {
|
|
144
|
+
for (const [dedupe, seenAt] of this.seen) {
|
|
145
|
+
if (now - seenAt < this.dedupeRetentionMs)
|
|
146
|
+
break;
|
|
147
|
+
this.seen.delete(dedupe);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async advanceOnce(state, handlers) {
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
state.controller = controller;
|
|
153
|
+
try {
|
|
154
|
+
const result = await handlers.advance(state.tenantId, state.sessionId, controller.signal);
|
|
155
|
+
state.attempts = 0;
|
|
156
|
+
if (result.status === "busy")
|
|
157
|
+
this.later(result.retryAfterMs, () => this.enqueue(state.tenantId, state.sessionId));
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
state.attempts += 1;
|
|
161
|
+
if (state.attempts >= this.maxAttempts || controller.signal.aborted) {
|
|
162
|
+
state.attempts = 0;
|
|
163
|
+
this.onError(error);
|
|
164
|
+
}
|
|
165
|
+
else
|
|
166
|
+
this.later(this.retryDelayMs * 2 ** (state.attempts - 1), () => this.enqueue(state.tenantId, state.sessionId));
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
state.controller = undefined;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
armTimer(id, timer) {
|
|
173
|
+
timer.handle = setTimeout(() => {
|
|
174
|
+
if (this.timers.get(id) !== timer)
|
|
175
|
+
return;
|
|
176
|
+
this.timers.delete(id);
|
|
177
|
+
const handlers = this.handlers;
|
|
178
|
+
if (!handlers)
|
|
179
|
+
return;
|
|
180
|
+
if (!handlers.fire) {
|
|
181
|
+
this.onError(new Error("WorkerHandlers.fire is required for timers"));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this.track(handlers.fire(timer.tenantId, timer.key).catch(this.onError));
|
|
185
|
+
}, Math.max(0, timer.at - Date.now()));
|
|
186
|
+
}
|
|
187
|
+
scheduleSweep(tenantId, sweep, delay) {
|
|
188
|
+
if (sweep.running || sweep.handle)
|
|
189
|
+
return;
|
|
190
|
+
sweep.handle = setTimeout(() => {
|
|
191
|
+
sweep.handle = undefined;
|
|
192
|
+
const handlers = this.handlers;
|
|
193
|
+
if (!handlers || this.sweeps.get(tenantId) !== sweep)
|
|
194
|
+
return;
|
|
195
|
+
sweep.running = true;
|
|
196
|
+
this.track(handlers
|
|
197
|
+
.sweep(tenantId)
|
|
198
|
+
.catch(this.onError)
|
|
199
|
+
.finally(() => {
|
|
200
|
+
sweep.running = false;
|
|
201
|
+
if (this.handlers && this.sweeps.get(tenantId) === sweep)
|
|
202
|
+
this.scheduleSweep(tenantId, sweep, this.sweepIntervalMs);
|
|
203
|
+
}));
|
|
204
|
+
}, delay);
|
|
205
|
+
}
|
|
206
|
+
later(ms, fn) {
|
|
207
|
+
const handle = setTimeout(() => {
|
|
208
|
+
this.delayed.delete(handle);
|
|
209
|
+
fn();
|
|
210
|
+
}, Math.max(0, ms));
|
|
211
|
+
this.delayed.add(handle);
|
|
212
|
+
}
|
|
213
|
+
track(promise) {
|
|
214
|
+
this.inflight.add(promise);
|
|
215
|
+
void promise.finally(() => this.inflight.delete(promise));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Durable Session Execution seam (architecture §12.3).
|
|
3
|
+
*
|
|
4
|
+
* Durable Session Execution decides when to look at a session again. It never
|
|
5
|
+
* holds session state: every outcome lives in the Session Store, and `advance`
|
|
6
|
+
* is safe to repeat (§10.1). Restate is the supported implementation
|
|
7
|
+
* (`adapters/execution/restate.ts`); `execution/memory.ts` is the in-process
|
|
8
|
+
* implementation used by unit tests, `startEphemeralRuntime` and a Host
|
|
9
|
+
* started without Restate endpoints.
|
|
10
|
+
*
|
|
11
|
+
* ## Guarantees every implementation keeps
|
|
12
|
+
*
|
|
13
|
+
* - **One advance per key at a time.** The key is `<tenantId>:<sessionId>`.
|
|
14
|
+
* Two `advance` calls for one key never overlap, on one process or many.
|
|
15
|
+
* Different keys may run concurrently.
|
|
16
|
+
* - **At least once after a wake.** When `wake` resolves, an `advance` for the
|
|
17
|
+
* key will start after that point (possibly merged with other wakes). Wakes
|
|
18
|
+
* are sent after the Session Store transaction that caused them commits
|
|
19
|
+
* (`Tx.afterCommit`), never inside it.
|
|
20
|
+
* - **Dedupe.** Wakes carrying the same `dedupeKey` for the same key cause at
|
|
21
|
+
* most one advance for that cause (within the implementation's retention
|
|
22
|
+
* window, at least 24 hours for Restate).
|
|
23
|
+
* - **Busy re-wake.** An advance that returns `busy` is re-run for the same key
|
|
24
|
+
* after `retryAfterMs`.
|
|
25
|
+
* - **Retries on infrastructure errors.** A handler that throws is retried with
|
|
26
|
+
* backoff. Session outcomes never throw: a turn that fails, pauses, waits or
|
|
27
|
+
* becomes `uncertain` is settled in the Session Store and `advance` returns
|
|
28
|
+
* `done`.
|
|
29
|
+
* - **Sweeps.** `armSweep` arms one self-re-arming sweep per Tenant; arming an
|
|
30
|
+
* armed Tenant is a no-op. The sweep expires claims, re-wakes orphaned
|
|
31
|
+
* `runnable` sessions and relays outbox rows left behind.
|
|
32
|
+
*/
|
|
33
|
+
/** Why a session is woken (§12.3, "Where wakes come from"). */
|
|
34
|
+
export type WakeReason =
|
|
35
|
+
/** Session commands. */
|
|
36
|
+
"message" | "approve" | "respond"
|
|
37
|
+
/** An executor posted an Action result. */
|
|
38
|
+
| "action_result"
|
|
39
|
+
/** A linked agent turn completed, failed or was cancelled. */
|
|
40
|
+
| "linked"
|
|
41
|
+
/** Queued flow effects, pending agent effects reconciled. */
|
|
42
|
+
| "flow"
|
|
43
|
+
/** The Tenant sweep and `busy` re-wakes. */
|
|
44
|
+
| "recover";
|
|
45
|
+
export declare const WAKE_REASONS: readonly WakeReason[];
|
|
46
|
+
export interface Wake {
|
|
47
|
+
reason: WakeReason;
|
|
48
|
+
/**
|
|
49
|
+
* Merges repeated wakes for the same cause, e.g. the command's idempotency
|
|
50
|
+
* key. Scoped to the session key. Absent means never deduped.
|
|
51
|
+
*/
|
|
52
|
+
dedupeKey?: string;
|
|
53
|
+
}
|
|
54
|
+
export type AdvanceResult = {
|
|
55
|
+
status: "done";
|
|
56
|
+
}
|
|
57
|
+
/** Another Worker holds a live lease; run again for this key after `retryAfterMs`. */
|
|
58
|
+
| {
|
|
59
|
+
status: "busy";
|
|
60
|
+
retryAfterMs: number;
|
|
61
|
+
};
|
|
62
|
+
/** What a Worker runs when Durable Session Execution calls it. */
|
|
63
|
+
export interface WorkerHandlers {
|
|
64
|
+
/**
|
|
65
|
+
* Advances one session (§10.5). `signal` aborts when the Worker stops or the
|
|
66
|
+
* session is cancelled through the control stream. Throw only on
|
|
67
|
+
* infrastructure errors.
|
|
68
|
+
*/
|
|
69
|
+
advance(tenantId: string, sessionId: string, signal: AbortSignal): Promise<AdvanceResult>;
|
|
70
|
+
/** One pass of the Tenant sweep. Re-armed by the implementation afterwards. */
|
|
71
|
+
sweep(tenantId: string): Promise<void>;
|
|
72
|
+
/** A durable timer set with `timer` fired. Required when `timer` is used. */
|
|
73
|
+
fire?(tenantId: string, key: string): Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
export interface DurableExecution {
|
|
76
|
+
/** At-least-once. `dedupeKey` merges repeated wakes for the same cause. */
|
|
77
|
+
wake(tenantId: string, sessionId: string, wake: Wake): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Durable one-shot timer that calls `WorkerHandlers.fire(tenantId, key)` at
|
|
80
|
+
* or after `at`. Setting a key again replaces the earlier time where the
|
|
81
|
+
* implementation can; `fire` must be idempotent either way.
|
|
82
|
+
*/
|
|
83
|
+
timer(tenantId: string, key: string, at: Date): Promise<void>;
|
|
84
|
+
/** Arms the Tenant's self-re-arming sweep. Idempotent. */
|
|
85
|
+
armSweep(tenantId: string): Promise<void>;
|
|
86
|
+
/** Stops re-arming the Tenant's sweep (Tenant deleted). A pass already running finishes. */
|
|
87
|
+
disarmSweep(tenantId: string): Promise<void>;
|
|
88
|
+
/** Starts delivering to `handlers` (serving the Worker endpoint, for Restate). */
|
|
89
|
+
start(handlers: WorkerHandlers): Promise<void>;
|
|
90
|
+
/** Stops delivering, aborts running advances' signals and waits for them. */
|
|
91
|
+
stop(): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Resolves when the backing service answers, rejects otherwise (readiness,
|
|
94
|
+
* `infra/execution.ts`). Absent for in-process implementations.
|
|
95
|
+
*/
|
|
96
|
+
probe?(signal: AbortSignal): Promise<void>;
|
|
97
|
+
/**
|
|
98
|
+
* Invocations for `tenantId` that need an operator: paused after exhausting
|
|
99
|
+
* retries, or backing off after failures (Tenant status). Absent for
|
|
100
|
+
* in-process implementations.
|
|
101
|
+
*/
|
|
102
|
+
stuckInvocations?(tenantId: string): Promise<StuckInvocation[]>;
|
|
103
|
+
}
|
|
104
|
+
/** An invocation that needs an operator (`DurableExecution.stuckInvocations`). */
|
|
105
|
+
export interface StuckInvocation {
|
|
106
|
+
id: string;
|
|
107
|
+
/** `paused` or `backing-off`. */
|
|
108
|
+
status: string;
|
|
109
|
+
/** Service name without any prefix, e.g. `NylorunSession`. */
|
|
110
|
+
service: string;
|
|
111
|
+
handler: string;
|
|
112
|
+
/** Object key: `<tenantId>:<sessionId>`, `<tenantId>` or `<tenantId>:<timer key>`. */
|
|
113
|
+
key: string;
|
|
114
|
+
tenantId?: string;
|
|
115
|
+
retryCount: number;
|
|
116
|
+
lastFailure?: string;
|
|
117
|
+
modifiedAt?: string;
|
|
118
|
+
}
|
|
119
|
+
/** The key one advance at a time is serialized on. */
|
|
120
|
+
export declare function sessionKey(tenantId: string, sessionId: string): string;
|
|
121
|
+
/** Splits a key made by `sessionKey`. Tenant ids never contain `:`. */
|
|
122
|
+
export declare function parseSessionKey(key: string): {
|
|
123
|
+
tenantId: string;
|
|
124
|
+
sessionId: string;
|
|
125
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Durable Session Execution seam (architecture §12.3).
|
|
3
|
+
*
|
|
4
|
+
* Durable Session Execution decides when to look at a session again. It never
|
|
5
|
+
* holds session state: every outcome lives in the Session Store, and `advance`
|
|
6
|
+
* is safe to repeat (§10.1). Restate is the supported implementation
|
|
7
|
+
* (`adapters/execution/restate.ts`); `execution/memory.ts` is the in-process
|
|
8
|
+
* implementation used by unit tests, `startEphemeralRuntime` and a Host
|
|
9
|
+
* started without Restate endpoints.
|
|
10
|
+
*
|
|
11
|
+
* ## Guarantees every implementation keeps
|
|
12
|
+
*
|
|
13
|
+
* - **One advance per key at a time.** The key is `<tenantId>:<sessionId>`.
|
|
14
|
+
* Two `advance` calls for one key never overlap, on one process or many.
|
|
15
|
+
* Different keys may run concurrently.
|
|
16
|
+
* - **At least once after a wake.** When `wake` resolves, an `advance` for the
|
|
17
|
+
* key will start after that point (possibly merged with other wakes). Wakes
|
|
18
|
+
* are sent after the Session Store transaction that caused them commits
|
|
19
|
+
* (`Tx.afterCommit`), never inside it.
|
|
20
|
+
* - **Dedupe.** Wakes carrying the same `dedupeKey` for the same key cause at
|
|
21
|
+
* most one advance for that cause (within the implementation's retention
|
|
22
|
+
* window, at least 24 hours for Restate).
|
|
23
|
+
* - **Busy re-wake.** An advance that returns `busy` is re-run for the same key
|
|
24
|
+
* after `retryAfterMs`.
|
|
25
|
+
* - **Retries on infrastructure errors.** A handler that throws is retried with
|
|
26
|
+
* backoff. Session outcomes never throw: a turn that fails, pauses, waits or
|
|
27
|
+
* becomes `uncertain` is settled in the Session Store and `advance` returns
|
|
28
|
+
* `done`.
|
|
29
|
+
* - **Sweeps.** `armSweep` arms one self-re-arming sweep per Tenant; arming an
|
|
30
|
+
* armed Tenant is a no-op. The sweep expires claims, re-wakes orphaned
|
|
31
|
+
* `runnable` sessions and relays outbox rows left behind.
|
|
32
|
+
*/
|
|
33
|
+
export const WAKE_REASONS = [
|
|
34
|
+
"message",
|
|
35
|
+
"approve",
|
|
36
|
+
"respond",
|
|
37
|
+
"action_result",
|
|
38
|
+
"linked",
|
|
39
|
+
"flow",
|
|
40
|
+
"recover",
|
|
41
|
+
];
|
|
42
|
+
/** The key one advance at a time is serialized on. */
|
|
43
|
+
export function sessionKey(tenantId, sessionId) {
|
|
44
|
+
return `${tenantId}:${sessionId}`;
|
|
45
|
+
}
|
|
46
|
+
/** Splits a key made by `sessionKey`. Tenant ids never contain `:`. */
|
|
47
|
+
export function parseSessionKey(key) {
|
|
48
|
+
const at = key.indexOf(":");
|
|
49
|
+
if (at <= 0 || at === key.length - 1)
|
|
50
|
+
throw new Error(`Invalid session key: ${key}`);
|
|
51
|
+
return { tenantId: key.slice(0, at), sessionId: key.slice(at + 1) };
|
|
52
|
+
}
|
|
@@ -2,7 +2,8 @@ import type { HostConfigFile } from "./config.js";
|
|
|
2
2
|
import type { Logger, TenantConfig } from "../tenant/types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Builds a TenantConfig for `openTenantRuntime`.
|
|
5
|
-
*
|
|
5
|
+
* The sandbox backend here is the Host default; a Tenant's seeded `sandbox.backend`
|
|
6
|
+
* setting overrides it when the Tenant opens (I1, A18).
|
|
6
7
|
*/
|
|
7
8
|
export declare function configForFactory(options: {
|
|
8
9
|
hostRoot: string;
|
package/dist/host/config-for.js
CHANGED
|
@@ -1,33 +1,15 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { DatabaseSync } from "node:sqlite";
|
|
3
1
|
import { tenantPaths } from "../tenant/paths.js";
|
|
4
|
-
import { readTenantConfig } from "../tenant/schema.js";
|
|
5
2
|
import { tenantChildEnvironment } from "./environment.js";
|
|
6
3
|
/**
|
|
7
4
|
* Builds a TenantConfig for `openTenantRuntime`.
|
|
8
|
-
*
|
|
5
|
+
* The sandbox backend here is the Host default; a Tenant's seeded `sandbox.backend`
|
|
6
|
+
* setting overrides it when the Tenant opens (I1, A18).
|
|
9
7
|
*/
|
|
10
8
|
export function configForFactory(options) {
|
|
11
9
|
const { baseline } = options;
|
|
12
10
|
return (id) => {
|
|
13
11
|
const tenant = tenantPaths(options.hostRoot, id);
|
|
14
|
-
|
|
15
|
-
if (existsSync(tenant.database)) {
|
|
16
|
-
try {
|
|
17
|
-
const db = new DatabaseSync(tenant.database, { readOnly: true });
|
|
18
|
-
try {
|
|
19
|
-
const seeded = readTenantConfig(db).sandboxBackend;
|
|
20
|
-
if (seeded)
|
|
21
|
-
sandboxBackend = seeded;
|
|
22
|
-
}
|
|
23
|
-
finally {
|
|
24
|
-
db.close();
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
/* unreadable — keep default */
|
|
29
|
-
}
|
|
30
|
-
}
|
|
12
|
+
const sandboxBackend = "auto";
|
|
31
13
|
return {
|
|
32
14
|
tenantId: id,
|
|
33
15
|
mode: options.mode ?? "shared",
|
package/dist/host/config.d.ts
CHANGED
|
@@ -10,14 +10,6 @@ export interface HostConfigFile {
|
|
|
10
10
|
nodeExtraCaCerts?: string;
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
-
/** host-state.json — process only */
|
|
14
|
-
export interface HostStateFile {
|
|
15
|
-
pid: number;
|
|
16
|
-
startedAt: string;
|
|
17
|
-
version: string;
|
|
18
|
-
entry: string;
|
|
19
|
-
url: string;
|
|
20
|
-
}
|
|
21
13
|
/** host-credentials.json, mode 0600 */
|
|
22
14
|
export interface HostCredentialsFile {
|
|
23
15
|
adminKey: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Logger, type TenantModule } from "../tenant/types.js";
|
|
2
2
|
import type { HostConfigFile, HostCredentialsFile } from "./config.js";
|
|
3
|
+
import type { ContainerListen } from "./stack-config.js";
|
|
3
4
|
export interface CreateHostOptions {
|
|
4
5
|
hostRoot: string;
|
|
5
6
|
module: TenantModule;
|
|
@@ -10,10 +11,44 @@ export interface CreateHostOptions {
|
|
|
10
11
|
coreVersion: string;
|
|
11
12
|
/** Process id reported by `/health` and admin host status. Defaults to `process.pid`. */
|
|
12
13
|
pid?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Container mode: bind this address and port instead of host.json's, and
|
|
16
|
+
* accept only `allowedHosts` in the `Host` check (replacing the
|
|
17
|
+
* loopback-only rule). host.json then describes the client-facing address.
|
|
18
|
+
*/
|
|
19
|
+
listen?: ContainerListen;
|
|
20
|
+
/**
|
|
21
|
+
* The client-facing URL `/v1/admin/status` reports as `host.url`
|
|
22
|
+
* (`NYLORUN_PUBLIC_URL`). Defaults to the bound address, which in container
|
|
23
|
+
* mode is `http://0.0.0.0:4000`.
|
|
24
|
+
*/
|
|
25
|
+
publicUrl?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Infrastructure readiness (`infra/readiness.ts`). `/ready` adds its checks
|
|
28
|
+
* and answers 503 while it reports not ok. Default: listener and discovery
|
|
29
|
+
* only.
|
|
30
|
+
*/
|
|
31
|
+
readiness?: () => Promise<{
|
|
32
|
+
ok: boolean;
|
|
33
|
+
checks: Record<string, boolean>;
|
|
34
|
+
}>;
|
|
35
|
+
/**
|
|
36
|
+
* Shutdown steps around closing the Tenants. `close()` runs them whatever asked for it
|
|
37
|
+
* (SIGTERM in `host/main.ts`, `POST /v1/admin/host/shutdown`): the listener stops, then
|
|
38
|
+
* `beforeTenants` (stop the Worker so no advance starts on a closing Tenant), the Tenants
|
|
39
|
+
* close, then `afterTenants` (end the infrastructure clients). A failing step is logged
|
|
40
|
+
* and shutdown goes on.
|
|
41
|
+
*/
|
|
42
|
+
shutdown?: {
|
|
43
|
+
beforeTenants?(): Promise<void>;
|
|
44
|
+
afterTenants?(): Promise<void>;
|
|
45
|
+
};
|
|
13
46
|
}
|
|
14
47
|
export interface HostServer {
|
|
15
48
|
listen(): Promise<void>;
|
|
16
49
|
close(): Promise<void>;
|
|
50
|
+
/** Settles once `close()` has finished, whatever called it. */
|
|
51
|
+
readonly closed: Promise<void>;
|
|
17
52
|
readonly url: string;
|
|
18
53
|
}
|
|
19
54
|
/** Constant-time comparison of SHA-256 digests of the presented and stored admin keys. */
|