@nylorun/runtime 0.14.0-beta → 0.15.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/README.md +75 -33
- package/dist/adapters/execution/restate.js +2 -2
- package/dist/adapters/replication/pgoutput.d.ts +5 -0
- package/dist/adapters/replication/pgoutput.js +4 -3
- package/dist/admin-openapi.json +184 -685
- package/dist/api/components.d.ts +308 -68
- package/dist/api/components.js +4 -5
- package/dist/api/gate/keys.d.ts +14 -0
- package/dist/api/gate/keys.js +59 -0
- package/dist/api/gate/routes.d.ts +34 -0
- package/dist/api/gate/routes.js +235 -0
- package/dist/api/http/define.js +0 -8
- package/dist/api/http/routes/access.js +25 -18
- package/dist/api/http/routes/tenant.d.ts +2 -1
- package/dist/api/http/routes/tenant.js +77 -5
- package/dist/api/http/routes/vaults.js +2 -2
- package/dist/api/openapi.js +4 -5
- package/dist/gates/contract.d.ts +51 -0
- package/dist/gates/contract.js +37 -0
- package/dist/gates/handler.d.ts +21 -0
- package/dist/gates/handler.js +55 -0
- package/dist/gates/http-client.d.ts +12 -0
- package/dist/gates/http-client.js +160 -0
- package/dist/gates/in-process.d.ts +32 -0
- package/dist/gates/in-process.js +42 -0
- package/dist/gates/inflight.d.ts +39 -0
- package/dist/gates/inflight.js +70 -0
- package/dist/gates/mcp-handler.d.ts +35 -0
- package/dist/gates/mcp-handler.js +142 -0
- package/dist/gates/meter.d.ts +24 -0
- package/dist/gates/meter.js +165 -0
- package/dist/gates/model-gate.d.ts +45 -0
- package/dist/gates/model-gate.js +1 -0
- package/dist/gates/tenant-vaults.d.ts +64 -0
- package/dist/gates/tenant-vaults.js +94 -0
- package/dist/gates/tool-calls.d.ts +51 -0
- package/dist/gates/tool-calls.js +64 -0
- package/dist/gates/tool-client.d.ts +12 -0
- package/dist/gates/tool-client.js +201 -0
- package/dist/gates/tool-contract.d.ts +116 -0
- package/dist/gates/tool-contract.js +64 -0
- package/dist/gates/tool-gate.d.ts +45 -0
- package/dist/gates/tool-gate.js +10 -0
- package/dist/host/admin-api.d.ts +7 -6
- package/dist/host/admin-api.js +9 -113
- package/dist/host/app.d.ts +1 -1
- package/dist/host/app.js +20 -24
- package/dist/host/config-for.d.ts +1 -1
- package/dist/host/config-for.js +2 -2
- package/dist/host/create-host.d.ts +5 -5
- package/dist/host/create-host.js +5 -15
- package/dist/host/execution.d.ts +20 -18
- package/dist/host/execution.js +2 -2
- package/dist/host/gates.d.ts +58 -0
- package/dist/host/gates.js +127 -0
- package/dist/host/http.d.ts +6 -1
- package/dist/host/http.js +17 -0
- package/dist/host/index.d.ts +1 -1
- package/dist/host/main.js +151 -71
- package/dist/host/stack-config.d.ts +71 -19
- package/dist/host/stack-config.js +192 -18
- package/dist/infra/database.d.ts +2 -2
- package/dist/infra/execution.d.ts +5 -5
- package/dist/infra/execution.js +8 -6
- package/dist/infra/index.d.ts +1 -1
- package/dist/infra/workers.d.ts +17 -17
- package/dist/infra/workers.js +9 -9
- package/dist/keys/client.d.ts +9 -0
- package/dist/keys/client.js +70 -0
- package/dist/keys/contract.d.ts +24 -0
- package/dist/keys/contract.js +12 -0
- package/dist/keys/keys.d.ts +41 -0
- package/dist/keys/keys.js +50 -0
- package/dist/mcp/connect.d.ts +43 -3
- package/dist/mcp/connect.js +12 -5
- package/dist/mcp/pool.d.ts +21 -3
- package/dist/mcp/pool.js +15 -2
- package/dist/openapi.json +887 -694
- package/dist/record/append.d.ts +51 -0
- package/dist/record/append.js +20 -0
- package/dist/{store/event.js → record/envelope.js} +1 -1
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +11 -0
- package/dist/store/postgres/connect.d.ts +15 -12
- package/dist/store/postgres/connect.js +24 -12
- package/dist/store/postgres/db.d.ts +16 -0
- package/dist/store/postgres/db.js +28 -0
- package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
- package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
- package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
- package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
- package/dist/store/postgres/migrate.d.ts +68 -0
- package/dist/store/postgres/migrate.js +183 -0
- package/dist/store/postgres/record-writer.d.ts +4 -0
- package/dist/store/postgres/record-writer.js +34 -0
- package/dist/store/postgres/record.d.ts +2 -8
- package/dist/store/postgres/record.js +50 -54
- package/dist/store/postgres/schema.d.ts +3096 -0
- package/dist/store/postgres/schema.js +363 -0
- package/dist/store/postgres/store.d.ts +2 -4
- package/dist/store/postgres/store.js +481 -567
- package/dist/store/postgres/tenant.d.ts +56 -0
- package/dist/store/postgres/tenant.js +154 -0
- package/dist/store/types.d.ts +43 -87
- package/dist/store/types.js +0 -1
- package/dist/streams/control.d.ts +1 -1
- package/dist/streams/control.js +6 -2
- package/dist/streams/relay/types.d.ts +2 -1
- package/dist/streams/relay/types.js +2 -1
- package/dist/streams/types.d.ts +5 -0
- package/dist/tenant/advance.d.ts +7 -2
- package/dist/tenant/advance.js +95 -16
- package/dist/tenant/cause.d.ts +25 -0
- package/dist/tenant/cause.js +45 -0
- package/dist/tenant/commands.js +4 -2
- package/dist/tenant/context.d.ts +26 -3
- package/dist/tenant/delivery-token.js +17 -18
- package/dist/tenant/delivery.d.ts +1 -1
- package/dist/tenant/delivery.js +18 -13
- package/dist/tenant/effects.d.ts +15 -1
- package/dist/tenant/effects.js +93 -17
- package/dist/tenant/ephemeral.d.ts +16 -7
- package/dist/tenant/ephemeral.js +45 -111
- package/dist/tenant/history.d.ts +64 -0
- package/dist/tenant/history.js +120 -0
- package/dist/tenant/model-setting.d.ts +2 -2
- package/dist/tenant/module.d.ts +14 -28
- package/dist/tenant/module.js +131 -271
- package/dist/tenant/paths.d.ts +11 -16
- package/dist/tenant/paths.js +24 -68
- package/dist/tenant/pool.d.ts +1 -1
- package/dist/tenant/pool.js +1 -1
- package/dist/tenant/principals.d.ts +23 -11
- package/dist/tenant/principals.js +61 -49
- package/dist/tenant/runtime.d.ts +19 -1
- package/dist/tenant/runtime.js +40 -26
- package/dist/tenant/scheduler.d.ts +4 -2
- package/dist/tenant/scheduler.js +4 -4
- package/dist/tenant/session-streams.d.ts +2 -0
- package/dist/tenant/session-streams.js +8 -3
- package/dist/tenant/status.d.ts +3 -0
- package/dist/tenant/status.js +1 -1
- package/dist/tenant/store-pg.d.ts +9 -6
- package/dist/tenant/store-pg.js +60 -125
- package/dist/tenant/streams.d.ts +9 -25
- package/dist/tenant/streams.js +7 -22
- package/dist/tenant/tokens.js +16 -14
- package/dist/tenant/types.d.ts +41 -92
- package/dist/tenant/types.js +4 -15
- package/dist/tenant/worker.js +4 -4
- package/dist/vault/host-model.d.ts +31 -0
- package/dist/vault/host-model.js +57 -0
- package/dist/vault/service.d.ts +26 -15
- package/dist/vault/service.js +70 -109
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -5
- package/dist/store/memory.d.ts +0 -69
- package/dist/store/memory.js +0 -810
- package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
- package/dist/store/postgres/migrations/001_initial.js +0 -196
- package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
- package/dist/store/postgres/migrations/002_session_owner.js +0 -13
- package/dist/store/postgres/migrations/003_subject_tokens.d.ts +0 -8
- package/dist/store/postgres/migrations/003_subject_tokens.js +0 -42
- package/dist/store/postgres/migrations/004_publishable_keys.d.ts +0 -6
- package/dist/store/postgres/migrations/004_publishable_keys.js +0 -18
- package/dist/store/postgres/migrations/005_action_endpoints.d.ts +0 -6
- package/dist/store/postgres/migrations/005_action_endpoints.js +0 -32
- package/dist/store/postgres/migrations/006_drop_executors.d.ts +0 -11
- package/dist/store/postgres/migrations/006_drop_executors.js +0 -32
- package/dist/store/postgres/migrations/007_event_time.d.ts +0 -7
- package/dist/store/postgres/migrations/007_event_time.js +0 -14
- package/dist/store/postgres/migrations/008_durable_streams.d.ts +0 -13
- package/dist/store/postgres/migrations/008_durable_streams.js +0 -35
- package/dist/store/postgres/migrations/index.d.ts +0 -53
- package/dist/store/postgres/migrations/index.js +0 -96
- package/dist/store/postgres/migrations/shared/index.d.ts +0 -31
- package/dist/store/postgres/migrations/shared/index.js +0 -92
- package/dist/store/postgres/names.d.ts +0 -11
- package/dist/store/postgres/names.js +0 -42
- package/dist/store/postgres/tenants.d.ts +0 -90
- package/dist/store/postgres/tenants.js +0 -228
- package/dist/streams/relay/memory.d.ts +0 -69
- package/dist/streams/relay/memory.js +0 -176
- package/dist/tenant/envelope.d.ts +0 -7
- package/dist/tenant/envelope.js +0 -22
- package/dist/tenant/quarantine-error.d.ts +0 -6
- package/dist/tenant/quarantine-error.js +0 -9
- package/dist/tenant/quarantine.d.ts +0 -27
- package/dist/tenant/quarantine.js +0 -72
- package/dist/tenant/store-memory.d.ts +0 -16
- package/dist/tenant/store-memory.js +0 -111
- /package/dist/{store → record}/cursor.d.ts +0 -0
- /package/dist/{store → record}/cursor.js +0 -0
- /package/dist/{store/event.d.ts → record/envelope.d.ts} +0 -0
package/dist/tenant/module.d.ts
CHANGED
|
@@ -1,41 +1,27 @@
|
|
|
1
1
|
import type { StreamRelayStatus } from "@nylorun/core/contracts";
|
|
2
|
-
import {
|
|
3
|
-
import { TenantNotFoundError, TenantUnavailableError, type Logger, type OpenTenantRuntime, type TenantConfig, type TenantModule, type TenantStore } from "./types.js";
|
|
2
|
+
import { TenantUnavailableError, type Logger, type TenantHandle, type TenantModule, type TenantOpener } from "./types.js";
|
|
4
3
|
export interface CreateTenantModuleOptions {
|
|
5
|
-
store
|
|
6
|
-
|
|
7
|
-
hostRoot?: string;
|
|
8
|
-
/** Unused: the store opens Tenants. Accepted so callers can pass the store's options. */
|
|
9
|
-
openRuntime?: OpenTenantRuntime;
|
|
10
|
-
/** Unused; see `openRuntime`. */
|
|
11
|
-
configFor?: (tenantId: string) => TenantConfig;
|
|
4
|
+
/** Opens the Host's Tenant (`store-pg.ts`). */
|
|
5
|
+
open: TenantOpener;
|
|
12
6
|
logger: Logger;
|
|
13
|
-
/** Bound on
|
|
7
|
+
/** Bound on opening the Tenant; a slower open fails with `open-timeout`. */
|
|
14
8
|
openTimeoutMs?: number;
|
|
15
9
|
/**
|
|
16
|
-
* Runs
|
|
17
|
-
*
|
|
10
|
+
* Runs once the Tenant is open, at start or on a later retry: the Host starts what needs the
|
|
11
|
+
* Tenant's id (its stream relay). A failure is logged; the Tenant stays open.
|
|
18
12
|
*/
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Runs after a Tenant is removed from the store: removes what lives outside it (its
|
|
22
|
-
* Durable Streams basin, its armed sweep). A failure is logged; the Tenant stays deleted.
|
|
23
|
-
*/
|
|
24
|
-
onDeleted?: (tenantId: string) => Promise<void>;
|
|
13
|
+
onOpen?: (handle: TenantHandle) => void | Promise<void>;
|
|
25
14
|
/** This process's stream relay, for the Host aggregate (a Host with S2 runs one). */
|
|
26
15
|
relayStatus?: () => Promise<StreamRelayStatus>;
|
|
27
16
|
}
|
|
28
17
|
/**
|
|
29
|
-
* Deep Tenant module (§8):
|
|
30
|
-
* injected `TenantStore`.
|
|
18
|
+
* Deep Tenant module (§8): the Host's one Tenant over an injected opener (tenancy.md §5).
|
|
31
19
|
*
|
|
32
|
-
*
|
|
33
|
-
* Tenant (
|
|
34
|
-
* the
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* tries again. A Tenant being deleted resolves as not found, so a late request or Worker
|
|
38
|
-
* call cannot reopen it while its storage is removed.
|
|
20
|
+
* `start` opens it once, even when requests or Worker calls ask at the same time. An open that
|
|
21
|
+
* fails for a reason in the Tenant (a `TenantOpenError`, or a timeout) is kept as the Tenant's
|
|
22
|
+
* cause: the Host is not ready, `/v1/admin/status` names it, requests get the opaque 404.
|
|
23
|
+
* One that fails outside the Tenant (`TenantUnavailableError`) is retried in the background
|
|
24
|
+
* with backoff, and by the next request, which gets 503 meanwhile.
|
|
39
25
|
*/
|
|
40
26
|
export declare function createTenantModule(options: CreateTenantModuleOptions): TenantModule;
|
|
41
|
-
export {
|
|
27
|
+
export { TenantUnavailableError };
|
package/dist/tenant/module.js
CHANGED
|
@@ -1,316 +1,176 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { POSTGRES_SCHEMA_VERSION } from "../store/postgres/migrations/index.js";
|
|
3
|
-
import { envelopeNow } from "./envelope.js";
|
|
1
|
+
import { openError, TenantOpenError } from "./cause.js";
|
|
4
2
|
import { TimeoutError, withTimeout } from "./pool.js";
|
|
5
|
-
import {
|
|
6
|
-
import { TenantNotFoundError, TenantUnavailableError, } from "./types.js";
|
|
3
|
+
import { TenantUnavailableError, } from "./types.js";
|
|
7
4
|
const OPEN_TIMEOUT_MS = 30_000;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
summary.pendingActions > 0);
|
|
12
|
-
}
|
|
13
|
-
function toAdmin(id, entry, envelope) {
|
|
14
|
-
if (entry?.kind === "open") {
|
|
15
|
-
return {
|
|
16
|
-
id,
|
|
17
|
-
name: entry.handle.envelope.name,
|
|
18
|
-
state: "open",
|
|
19
|
-
envelope: entry.handle.envelope,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
return {
|
|
23
|
-
id,
|
|
24
|
-
name: envelope?.name ?? null,
|
|
25
|
-
state: entry ? "quarantined" : "open",
|
|
26
|
-
envelope,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
/** The quarantine an open failure carries, from either quarantine error type. */
|
|
30
|
-
function quarantineOf(error) {
|
|
31
|
-
const carried = error?.quarantine;
|
|
32
|
-
if (carried && typeof carried === "object" && "code" in carried)
|
|
33
|
-
return carried;
|
|
34
|
-
return asQuarantine(error);
|
|
35
|
-
}
|
|
5
|
+
/** Retries of an open that failed outside the Tenant: from 1 s, doubling, at most 30 s apart. */
|
|
6
|
+
const RETRY_FIRST_MS = 1_000;
|
|
7
|
+
const RETRY_MAX_MS = 30_000;
|
|
36
8
|
/**
|
|
37
|
-
* Deep Tenant module (§8):
|
|
38
|
-
* injected `TenantStore`.
|
|
9
|
+
* Deep Tenant module (§8): the Host's one Tenant over an injected opener (tenancy.md §5).
|
|
39
10
|
*
|
|
40
|
-
*
|
|
41
|
-
* Tenant (
|
|
42
|
-
* the
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* tries again. A Tenant being deleted resolves as not found, so a late request or Worker
|
|
46
|
-
* call cannot reopen it while its storage is removed.
|
|
11
|
+
* `start` opens it once, even when requests or Worker calls ask at the same time. An open that
|
|
12
|
+
* fails for a reason in the Tenant (a `TenantOpenError`, or a timeout) is kept as the Tenant's
|
|
13
|
+
* cause: the Host is not ready, `/v1/admin/status` names it, requests get the opaque 404.
|
|
14
|
+
* One that fails outside the Tenant (`TenantUnavailableError`) is retried in the background
|
|
15
|
+
* with backoff, and by the next request, which gets 503 meanwhile.
|
|
47
16
|
*/
|
|
48
17
|
export function createTenantModule(options) {
|
|
49
|
-
const {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
let
|
|
54
|
-
|
|
55
|
-
function
|
|
56
|
-
const entry = { kind: "quarantined", quarantine: q };
|
|
57
|
-
entries.set(id, entry);
|
|
58
|
-
logger.warn("tenant quarantined", {
|
|
59
|
-
tenantId: id,
|
|
60
|
-
code: q.code,
|
|
61
|
-
repair: q.repair,
|
|
62
|
-
});
|
|
63
|
-
return entry;
|
|
64
|
-
}
|
|
65
|
-
async function openEntry(id) {
|
|
18
|
+
const { logger, openTimeoutMs = OPEN_TIMEOUT_MS } = options;
|
|
19
|
+
let state = { kind: "idle" };
|
|
20
|
+
let opening;
|
|
21
|
+
let retry;
|
|
22
|
+
let nextDelayMs = RETRY_FIRST_MS;
|
|
23
|
+
const current = () => state;
|
|
24
|
+
async function openOnce() {
|
|
66
25
|
let handle;
|
|
67
26
|
try {
|
|
68
|
-
handle = await withTimeout(
|
|
27
|
+
handle = await withTimeout(options.open(), openTimeoutMs, (late) => {
|
|
69
28
|
void late.then((h) => h.close()).catch(() => undefined);
|
|
70
29
|
});
|
|
71
30
|
}
|
|
72
31
|
catch (error) {
|
|
73
|
-
if (
|
|
74
|
-
return
|
|
75
|
-
if (closed || deleting.has(id))
|
|
76
|
-
return undefined;
|
|
32
|
+
if (current().kind === "closed")
|
|
33
|
+
return;
|
|
77
34
|
if (error instanceof TenantUnavailableError) {
|
|
78
|
-
|
|
35
|
+
state = { kind: "unavailable", error };
|
|
79
36
|
logger.warn("tenant unavailable", {
|
|
80
|
-
tenantId: id,
|
|
81
37
|
message: error.cause instanceof Error ? error.cause.message : String(error.cause),
|
|
38
|
+
retryMs: nextDelayMs,
|
|
82
39
|
});
|
|
83
|
-
|
|
40
|
+
scheduleRetry();
|
|
41
|
+
return;
|
|
84
42
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
43
|
+
const failure = error instanceof TenantOpenError
|
|
44
|
+
? error
|
|
45
|
+
: error instanceof TimeoutError
|
|
46
|
+
? openError("open-timeout", `Opening the Tenant timed out after ${openTimeoutMs}ms`)
|
|
47
|
+
: openError("open-failed", error instanceof Error ? error.message : "Opening the Tenant failed");
|
|
48
|
+
const failed = {
|
|
49
|
+
kind: "failed",
|
|
50
|
+
cause: failure.toCause(),
|
|
51
|
+
envelope: failure.envelope ?? null,
|
|
52
|
+
};
|
|
53
|
+
state = failed;
|
|
54
|
+
logger.error("tenant not opened", {
|
|
55
|
+
...(failed.envelope ? { tenantId: failed.envelope.id } : {}),
|
|
56
|
+
code: failed.cause.code,
|
|
57
|
+
message: failed.cause.message,
|
|
58
|
+
repair: failed.cause.repair,
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
91
61
|
}
|
|
92
|
-
if (
|
|
62
|
+
if (current().kind === "closed") {
|
|
93
63
|
await handle.close().catch(() => undefined);
|
|
94
|
-
return
|
|
64
|
+
return;
|
|
95
65
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
async function load(id) {
|
|
102
|
-
if (deleting.has(id))
|
|
103
|
-
return undefined;
|
|
104
|
-
const cached = entries.get(id);
|
|
105
|
-
if (cached)
|
|
106
|
-
return cached;
|
|
107
|
-
if (closed)
|
|
108
|
-
return undefined;
|
|
109
|
-
let pending = opening.get(id);
|
|
110
|
-
if (!pending) {
|
|
111
|
-
pending = openEntry(id).finally(() => opening.delete(id));
|
|
112
|
-
opening.set(id, pending);
|
|
66
|
+
state = { kind: "open", handle };
|
|
67
|
+
nextDelayMs = RETRY_FIRST_MS;
|
|
68
|
+
logger.info("tenant opened", { tenantId: handle.envelope.id });
|
|
69
|
+
try {
|
|
70
|
+
await options.onOpen?.(handle);
|
|
113
71
|
}
|
|
114
|
-
|
|
72
|
+
catch (error) {
|
|
73
|
+
logger.warn("tenant open step failed", {
|
|
74
|
+
tenantId: handle.envelope.id,
|
|
75
|
+
message: error instanceof Error ? error.message : String(error),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Opens the Tenant unless it is open, failed or closed; one open at a time. */
|
|
80
|
+
function load() {
|
|
81
|
+
const { kind } = current();
|
|
82
|
+
if (kind !== "idle" && kind !== "unavailable")
|
|
83
|
+
return Promise.resolve();
|
|
84
|
+
clearTimeout(retry);
|
|
85
|
+
retry = undefined;
|
|
86
|
+
opening ??= openOnce().finally(() => {
|
|
87
|
+
opening = undefined;
|
|
88
|
+
});
|
|
89
|
+
return opening;
|
|
90
|
+
}
|
|
91
|
+
function scheduleRetry() {
|
|
92
|
+
if (retry)
|
|
93
|
+
return;
|
|
94
|
+
const delayMs = nextDelayMs;
|
|
95
|
+
nextDelayMs = Math.min(delayMs * 2, RETRY_MAX_MS);
|
|
96
|
+
retry = setTimeout(() => {
|
|
97
|
+
retry = undefined;
|
|
98
|
+
void load();
|
|
99
|
+
}, delayMs);
|
|
100
|
+
retry.unref();
|
|
115
101
|
}
|
|
116
|
-
const openEntries = () => [...entries.values()].filter((e) => e.kind === "open");
|
|
117
102
|
const module = {
|
|
118
|
-
get
|
|
119
|
-
return
|
|
103
|
+
get ready() {
|
|
104
|
+
return current().kind === "open";
|
|
120
105
|
},
|
|
121
106
|
async start() {
|
|
122
|
-
if (closed)
|
|
107
|
+
if (current().kind === "closed")
|
|
123
108
|
throw new Error("Tenant module is closed");
|
|
124
|
-
|
|
125
|
-
started = true;
|
|
126
|
-
logger.info("tenant module started", { tenants: ids.length });
|
|
109
|
+
await load();
|
|
127
110
|
},
|
|
128
|
-
async resolve(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
111
|
+
async resolve() {
|
|
112
|
+
await load();
|
|
113
|
+
const now = current();
|
|
114
|
+
switch (now.kind) {
|
|
115
|
+
case "open":
|
|
116
|
+
return { kind: "open", handle: now.handle };
|
|
117
|
+
case "failed":
|
|
118
|
+
return { kind: "unavailable", cause: now.cause };
|
|
119
|
+
case "unavailable":
|
|
120
|
+
throw now.error;
|
|
121
|
+
default:
|
|
122
|
+
return { kind: "unavailable" };
|
|
123
|
+
}
|
|
137
124
|
},
|
|
138
125
|
async worker(id) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
126
|
+
await load();
|
|
127
|
+
const now = current();
|
|
128
|
+
return now.kind === "open" && now.handle.envelope.id === id
|
|
129
|
+
? now.handle.worker
|
|
130
|
+
: undefined;
|
|
143
131
|
},
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
credentialHash: input.credentialHash,
|
|
151
|
-
idempotencyKey: input.idempotencyKey,
|
|
152
|
-
...(input.studioCredentialHash
|
|
153
|
-
? { studioCredentialHash: input.studioCredentialHash }
|
|
154
|
-
: {}),
|
|
155
|
-
...(input.derivedPrincipals?.length
|
|
156
|
-
? { derivedPrincipals: input.derivedPrincipals }
|
|
157
|
-
: {}),
|
|
158
|
-
};
|
|
159
|
-
// The store records its own schema version.
|
|
160
|
-
const envelope = envelopeNow({
|
|
161
|
-
id: input.tenantId,
|
|
162
|
-
name: input.name,
|
|
163
|
-
schemaVersion: POSTGRES_SCHEMA_VERSION,
|
|
164
|
-
});
|
|
165
|
-
let outcome;
|
|
166
|
-
try {
|
|
167
|
-
outcome = await store.create(envelope, bootstrap);
|
|
168
|
-
}
|
|
169
|
-
catch (error) {
|
|
170
|
-
await store.removePartial(input.tenantId).catch(() => undefined);
|
|
171
|
-
throw error;
|
|
172
|
-
}
|
|
173
|
-
if (outcome === "exists") {
|
|
174
|
-
const matches = await store.bootstrapMatches(input.tenantId, bootstrap);
|
|
175
|
-
if (!matches)
|
|
176
|
-
throw new TenantConflictError();
|
|
177
|
-
const existing = await store.readEnvelope(input.tenantId);
|
|
178
|
-
await load(input.tenantId).catch(() => undefined);
|
|
179
|
-
return { envelope: existing, created: false };
|
|
180
|
-
}
|
|
181
|
-
await options.onCreated?.(input.tenantId).catch((error) => logger.warn("tenant create step failed; open repairs it", {
|
|
182
|
-
tenantId: input.tenantId,
|
|
183
|
-
message: error instanceof Error ? error.message : String(error),
|
|
184
|
-
}));
|
|
185
|
-
const opened = await load(input.tenantId).catch(() => undefined);
|
|
186
|
-
if (opened?.kind === "open") {
|
|
187
|
-
return { envelope: opened.handle.envelope, created: true };
|
|
188
|
-
}
|
|
189
|
-
// Created but failed to open: report the stored envelope.
|
|
190
|
-
return {
|
|
191
|
-
envelope: await store
|
|
192
|
-
.readEnvelope(input.tenantId)
|
|
193
|
-
.catch(() => envelope),
|
|
194
|
-
created: true,
|
|
195
|
-
};
|
|
196
|
-
},
|
|
197
|
-
async list() {
|
|
198
|
-
const ids = new Set(await store.enumerate());
|
|
199
|
-
for (const id of entries.keys())
|
|
200
|
-
ids.add(id);
|
|
201
|
-
const result = [];
|
|
202
|
-
for (const id of [...ids].sort()) {
|
|
203
|
-
if (!isTenantId(id) || deleting.has(id))
|
|
204
|
-
continue;
|
|
205
|
-
let entry = entries.get(id);
|
|
206
|
-
if (entry?.kind === "open") {
|
|
207
|
-
result.push(toAdmin(id, entry, null));
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
let envelope = null;
|
|
211
|
-
try {
|
|
212
|
-
envelope = await store.readEnvelope(id);
|
|
213
|
-
if (envelope.id !== id) {
|
|
214
|
-
entry = {
|
|
215
|
-
kind: "quarantined",
|
|
216
|
-
quarantine: quarantine("envelope-invalid", `Tenant envelope id ${envelope.id} does not match ${id}`, { tenantId: id }).toQuarantine(),
|
|
217
|
-
};
|
|
218
|
-
envelope = null;
|
|
219
|
-
}
|
|
132
|
+
tenant() {
|
|
133
|
+
const now = current();
|
|
134
|
+
switch (now.kind) {
|
|
135
|
+
case "open": {
|
|
136
|
+
const { envelope } = now.handle;
|
|
137
|
+
return { id: envelope.id, name: envelope.name, state: "open", envelope };
|
|
220
138
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
entry ??= {
|
|
229
|
-
kind: "quarantined",
|
|
230
|
-
quarantine: quarantineOf(error) ??
|
|
231
|
-
quarantine("envelope-invalid", "Tenant envelope is unreadable", {
|
|
232
|
-
tenantId: id,
|
|
233
|
-
}).toQuarantine(),
|
|
139
|
+
case "failed":
|
|
140
|
+
return {
|
|
141
|
+
id: now.envelope?.id ?? null,
|
|
142
|
+
name: now.envelope?.name ?? null,
|
|
143
|
+
state: "unavailable",
|
|
144
|
+
envelope: now.envelope,
|
|
145
|
+
cause: now.cause,
|
|
234
146
|
};
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
return result;
|
|
239
|
-
},
|
|
240
|
-
async status(id) {
|
|
241
|
-
if (!isTenantId(id))
|
|
242
|
-
return undefined;
|
|
243
|
-
const entry = await load(id);
|
|
244
|
-
if (!entry)
|
|
245
|
-
return undefined;
|
|
246
|
-
if (entry.kind === "open")
|
|
247
|
-
return toAdmin(id, entry, null);
|
|
248
|
-
const envelope = await store.readEnvelope(id).catch(() => null);
|
|
249
|
-
return { ...toAdmin(id, entry, envelope), quarantine: entry.quarantine };
|
|
250
|
-
},
|
|
251
|
-
async delete(id, activeWork) {
|
|
252
|
-
if (!isTenantId(id))
|
|
253
|
-
throw new Error(`Invalid tenant id: ${id}`);
|
|
254
|
-
if (deleting.has(id))
|
|
255
|
-
throw new TenantBusyError("Tenant is being deleted");
|
|
256
|
-
const entry = await load(id);
|
|
257
|
-
if (!entry)
|
|
258
|
-
throw new TenantNotFoundError(id);
|
|
259
|
-
if (entry.kind === "open") {
|
|
260
|
-
const summary = await entry.handle.summary();
|
|
261
|
-
if (activeWork === "refuse" && hasLiveWork(summary)) {
|
|
262
|
-
throw new TenantBusyError();
|
|
263
|
-
}
|
|
264
|
-
if (activeWork === "drain" || activeWork === "cancel") {
|
|
265
|
-
await entry.handle.drain(activeWork);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
deleting.add(id);
|
|
269
|
-
try {
|
|
270
|
-
entries.delete(id);
|
|
271
|
-
// Sandbox prefix cleanup is owned by TenantHandle.close().
|
|
272
|
-
if (entry.kind === "open")
|
|
273
|
-
await entry.handle.close();
|
|
274
|
-
await store.trash(id, new Date());
|
|
275
|
-
}
|
|
276
|
-
finally {
|
|
277
|
-
deleting.delete(id);
|
|
147
|
+
default:
|
|
148
|
+
return { id: null, name: null, state: "unavailable", envelope: null };
|
|
278
149
|
}
|
|
279
|
-
logger.info("tenant deleted", { tenantId: id, activeWork });
|
|
280
|
-
await options.onDeleted?.(id).catch((error) => logger.warn("tenant delete cleanup failed", {
|
|
281
|
-
tenantId: id,
|
|
282
|
-
message: error instanceof Error ? error.message : String(error),
|
|
283
|
-
}));
|
|
284
150
|
},
|
|
285
151
|
async summarize() {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
let pendingActions = 0;
|
|
289
|
-
let uncertainEffects = 0;
|
|
290
|
-
for (const s of await Promise.all(openEntries().map((e) => e.handle.summary()))) {
|
|
291
|
-
runningSessions += s.runningSessions;
|
|
292
|
-
inFlightDeliveries += s.inFlightDeliveries;
|
|
293
|
-
pendingActions += s.pendingActions;
|
|
294
|
-
uncertainEffects += s.uncertainEffects;
|
|
295
|
-
}
|
|
152
|
+
const now = current();
|
|
153
|
+
const summary = now.kind === "open" ? await now.handle.summary() : undefined;
|
|
296
154
|
const relay = await options.relayStatus?.().catch(() => undefined);
|
|
297
155
|
return {
|
|
298
|
-
runningSessions,
|
|
299
|
-
inFlightDeliveries,
|
|
300
|
-
pendingActions,
|
|
301
|
-
uncertainEffects,
|
|
156
|
+
runningSessions: summary?.runningSessions ?? 0,
|
|
157
|
+
inFlightDeliveries: summary?.inFlightDeliveries ?? 0,
|
|
158
|
+
pendingActions: summary?.pendingActions ?? 0,
|
|
159
|
+
uncertainEffects: summary?.uncertainEffects ?? 0,
|
|
302
160
|
...(relay ? { relay } : {}),
|
|
303
161
|
};
|
|
304
162
|
},
|
|
305
163
|
async close() {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
await
|
|
164
|
+
const previous = current();
|
|
165
|
+
state = { kind: "closed" };
|
|
166
|
+
clearTimeout(retry);
|
|
167
|
+
retry = undefined;
|
|
168
|
+
// An open in progress sees `closed` and closes what it opened.
|
|
169
|
+
await opening?.catch(() => undefined);
|
|
170
|
+
if (previous.kind === "open")
|
|
171
|
+
await previous.handle.close().catch(() => undefined);
|
|
312
172
|
},
|
|
313
173
|
};
|
|
314
174
|
return module;
|
|
315
175
|
}
|
|
316
|
-
export {
|
|
176
|
+
export { TenantUnavailableError };
|
package/dist/tenant/paths.d.ts
CHANGED
|
@@ -1,28 +1,23 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TenantPaths } from "./types.js";
|
|
2
2
|
export interface HostPaths {
|
|
3
3
|
root: string;
|
|
4
4
|
config: string;
|
|
5
5
|
credentials: string;
|
|
6
6
|
home: string;
|
|
7
7
|
tmp: string;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/** The Tenant directory (`tenantPaths`). */
|
|
9
|
+
tenant: string;
|
|
10
10
|
}
|
|
11
11
|
/** Host layout under NYLORUN_HOME / ~/.nylorun. */
|
|
12
12
|
export declare function hostPaths(hostRoot: string): HostPaths;
|
|
13
13
|
/**
|
|
14
|
-
* Absolute Tenant directory
|
|
15
|
-
*
|
|
14
|
+
* Absolute paths of the Tenant directory, `<hostRoot>/tenant/`: a Host serves one Tenant
|
|
15
|
+
* (tenancy.md §1), so the directory has no id segment. Asserts that it stays inside the Host
|
|
16
|
+
* root after realpath.
|
|
16
17
|
*
|
|
17
|
-
* The directory holds what stays on the Host (the Tenant's data is its
|
|
18
|
-
* `
|
|
18
|
+
* The directory holds what stays on the Host (the Tenant's data is its database): `home/`,
|
|
19
|
+
* `tmp/`, `sandboxes/`, `plugin-data/`, `logs/tenant.log`. The vault key is the one exception:
|
|
20
|
+
* it lives beside the Tenant directory, in `<hostRoot>/keys/vault-kek`, which the local stack
|
|
21
|
+
* mounts only into the gateway (F4.2), never into the runtime container.
|
|
19
22
|
*/
|
|
20
|
-
export declare function tenantPaths(hostRoot: string
|
|
21
|
-
/**
|
|
22
|
-
* Moves every Tenant directory that holds a `tenant.sqlite` to `trash/`, logging each.
|
|
23
|
-
*
|
|
24
|
-
* Those are beta Tenants from the SQLite Runtime. They are not migrated (recreate them); a
|
|
25
|
-
* Postgres Tenant's directory never holds that file. Moving them at Host start keeps a new
|
|
26
|
-
* Postgres Tenant with the same id from sharing the old directory. Returns the moved ids.
|
|
27
|
-
*/
|
|
28
|
-
export declare function trashSqliteTenants(hostRoot: string, logger: Logger): string[];
|
|
23
|
+
export declare function tenantPaths(hostRoot: string): TenantPaths;
|