@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
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const FIXTURE_MODEL_SETTING = "model.fixture";
|
|
2
|
+
/** Whether the Tenant uses the fixture model. */
|
|
3
|
+
export async function usesFixtureModel(t) {
|
|
4
|
+
return (await t.getSetting(FIXTURE_MODEL_SETTING)) === "true";
|
|
5
|
+
}
|
|
6
|
+
/** Seeds the setting if absent. Returns whether it was inserted. */
|
|
7
|
+
export async function seedFixtureModel(t) {
|
|
8
|
+
if ((await t.getSetting(FIXTURE_MODEL_SETTING)) !== undefined)
|
|
9
|
+
return false;
|
|
10
|
+
await t.putSetting(FIXTURE_MODEL_SETTING, "true");
|
|
11
|
+
return true;
|
|
12
|
+
}
|
package/dist/tenant/module.d.ts
CHANGED
|
@@ -1,17 +1,38 @@
|
|
|
1
1
|
import { isQuarantineError, quarantine, TenantBusyError, TenantConflictError } from "./quarantine.js";
|
|
2
|
-
import type
|
|
2
|
+
import { TenantNotFoundError, TenantUnavailableError, type Logger, type OpenTenantRuntime, type TenantConfig, type TenantModule, type TenantStore } from "./types.js";
|
|
3
3
|
export interface CreateTenantModuleOptions {
|
|
4
|
-
hostRoot: string;
|
|
5
4
|
store: TenantStore;
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/** Unused: the store derives paths. Accepted so callers can pass the store's options. */
|
|
6
|
+
hostRoot?: string;
|
|
7
|
+
/** Unused: the store opens Tenants. Accepted so callers can pass the store's options. */
|
|
8
|
+
openRuntime?: OpenTenantRuntime;
|
|
9
|
+
/** Unused; see `openRuntime`. */
|
|
10
|
+
configFor?: (tenantId: string) => TenantConfig;
|
|
8
11
|
logger: Logger;
|
|
9
|
-
|
|
12
|
+
/** Bound on one Tenant open; a slower open quarantines it with `open-timeout`. */
|
|
10
13
|
openTimeoutMs?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Runs after a Tenant is created in the store, before it opens: creates what lives outside
|
|
16
|
+
* it (its Durable Streams basin). A failure is logged; opening the Tenant repairs it.
|
|
17
|
+
*/
|
|
18
|
+
onCreated?: (tenantId: string) => Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Runs after a Tenant is removed from the store: removes what lives outside it (its
|
|
21
|
+
* Durable Streams basin, its armed sweep). A failure is logged; the Tenant stays deleted.
|
|
22
|
+
*/
|
|
23
|
+
onDeleted?: (tenantId: string) => Promise<void>;
|
|
11
24
|
}
|
|
12
25
|
/**
|
|
13
|
-
* Deep Tenant module (§8)
|
|
14
|
-
*
|
|
26
|
+
* Deep Tenant module (§8): discovery, create, resolve, list, delete and summarize over an
|
|
27
|
+
* injected `TenantStore`.
|
|
28
|
+
*
|
|
29
|
+
* Handles open on demand and are cached (architecture §8.2). The first call that needs a
|
|
30
|
+
* Tenant (`resolve`, `status`, `worker`, `delete`) opens it once, even when several ask at
|
|
31
|
+
* the same time; a failed open is cached as a quarantine, and a missing Tenant is not
|
|
32
|
+
* cached. A failure outside the Tenant (`TenantUnavailableError`: the database is down, its
|
|
33
|
+
* sweep could not be armed) is thrown to the caller and not cached either, so the next use
|
|
34
|
+
* tries again. A Tenant being deleted resolves as not found, so a late request or Worker
|
|
35
|
+
* call cannot reopen it while its storage is removed.
|
|
15
36
|
*/
|
|
16
37
|
export declare function createTenantModule(options: CreateTenantModuleOptions): TenantModule;
|
|
17
|
-
export { TenantBusyError, TenantConflictError, isQuarantineError, quarantine, };
|
|
38
|
+
export { TenantBusyError, TenantConflictError, TenantNotFoundError, TenantUnavailableError, isQuarantineError, quarantine, };
|
package/dist/tenant/module.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
1
|
import { isTenantId } from "@nylorun/core/compatibility";
|
|
3
|
-
import {
|
|
2
|
+
import { POSTGRES_SCHEMA_VERSION } from "../store/postgres/migrations/index.js";
|
|
4
3
|
import { envelopeNow } from "./envelope.js";
|
|
5
|
-
import {
|
|
6
|
-
import { mapPool, TimeoutError, withTimeout } from "./pool.js";
|
|
4
|
+
import { TimeoutError, withTimeout } from "./pool.js";
|
|
7
5
|
import { asQuarantine, isQuarantineError, quarantine, TenantBusyError, TenantConflictError, } from "./quarantine.js";
|
|
8
|
-
|
|
6
|
+
import { TenantNotFoundError, TenantUnavailableError, } from "./types.js";
|
|
9
7
|
const OPEN_TIMEOUT_MS = 30_000;
|
|
10
8
|
function hasLiveWork(summary) {
|
|
11
9
|
return (summary.runningSessions > 0 ||
|
|
@@ -13,15 +11,7 @@ function hasLiveWork(summary) {
|
|
|
13
11
|
summary.pendingActions > 0);
|
|
14
12
|
}
|
|
15
13
|
function toAdmin(id, entry, envelope) {
|
|
16
|
-
if (
|
|
17
|
-
return {
|
|
18
|
-
id,
|
|
19
|
-
name: envelope?.name ?? null,
|
|
20
|
-
state: "quarantined",
|
|
21
|
-
envelope,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
if (entry.kind === "open") {
|
|
14
|
+
if (entry?.kind === "open") {
|
|
25
15
|
return {
|
|
26
16
|
id,
|
|
27
17
|
name: entry.handle.envelope.name,
|
|
@@ -32,69 +22,98 @@ function toAdmin(id, entry, envelope) {
|
|
|
32
22
|
return {
|
|
33
23
|
id,
|
|
34
24
|
name: envelope?.name ?? null,
|
|
35
|
-
state: "quarantined",
|
|
25
|
+
state: entry ? "quarantined" : "open",
|
|
36
26
|
envelope,
|
|
37
27
|
};
|
|
38
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
|
+
}
|
|
39
36
|
/**
|
|
40
|
-
* Deep Tenant module (§8)
|
|
41
|
-
*
|
|
37
|
+
* Deep Tenant module (§8): discovery, create, resolve, list, delete and summarize over an
|
|
38
|
+
* injected `TenantStore`.
|
|
39
|
+
*
|
|
40
|
+
* Handles open on demand and are cached (architecture §8.2). The first call that needs a
|
|
41
|
+
* Tenant (`resolve`, `status`, `worker`, `delete`) opens it once, even when several ask at
|
|
42
|
+
* the same time; a failed open is cached as a quarantine, and a missing Tenant is not
|
|
43
|
+
* cached. A failure outside the Tenant (`TenantUnavailableError`: the database is down, its
|
|
44
|
+
* sweep could not be armed) is thrown to the caller and not cached either, so the next use
|
|
45
|
+
* tries again. A Tenant being deleted resolves as not found, so a late request or Worker
|
|
46
|
+
* call cannot reopen it while its storage is removed.
|
|
42
47
|
*/
|
|
43
48
|
export function createTenantModule(options) {
|
|
44
|
-
const {
|
|
45
|
-
// openRuntime / configFor are consumed by the store; kept on options for B1.
|
|
46
|
-
void options.openRuntime;
|
|
47
|
-
void options.configFor;
|
|
49
|
+
const { store, logger, openTimeoutMs = OPEN_TIMEOUT_MS } = options;
|
|
48
50
|
const entries = new Map();
|
|
51
|
+
const opening = new Map();
|
|
52
|
+
const deleting = new Set();
|
|
49
53
|
let started = false;
|
|
50
54
|
let closed = false;
|
|
51
|
-
function rememberOpen(handle) {
|
|
52
|
-
entries.set(handle.envelope.id, { kind: "open", handle });
|
|
53
|
-
}
|
|
54
55
|
function rememberQuarantine(id, q) {
|
|
55
|
-
|
|
56
|
+
const entry = { kind: "quarantined", quarantine: q };
|
|
57
|
+
entries.set(id, entry);
|
|
56
58
|
logger.warn("tenant quarantined", {
|
|
57
59
|
tenantId: id,
|
|
58
60
|
code: q.code,
|
|
59
61
|
repair: q.repair,
|
|
60
62
|
});
|
|
63
|
+
return entry;
|
|
61
64
|
}
|
|
62
|
-
async function
|
|
63
|
-
|
|
64
|
-
if (existing?.kind === "open")
|
|
65
|
-
return;
|
|
65
|
+
async function openEntry(id) {
|
|
66
|
+
let handle;
|
|
66
67
|
try {
|
|
67
|
-
|
|
68
|
-
void late
|
|
69
|
-
.then((h) => h.close())
|
|
70
|
-
.catch(() => undefined);
|
|
68
|
+
handle = await withTimeout(store.open(id), openTimeoutMs, (late) => {
|
|
69
|
+
void late.then((h) => h.close()).catch(() => undefined);
|
|
71
70
|
});
|
|
72
|
-
rememberOpen(handle);
|
|
73
71
|
}
|
|
74
72
|
catch (error) {
|
|
75
|
-
if (error instanceof
|
|
76
|
-
|
|
73
|
+
if (error instanceof TenantNotFoundError)
|
|
74
|
+
return undefined;
|
|
75
|
+
if (closed || deleting.has(id))
|
|
76
|
+
return undefined;
|
|
77
|
+
if (error instanceof TenantUnavailableError) {
|
|
78
|
+
// Not the Tenant's fault: not cached, so the next use opens it again.
|
|
79
|
+
logger.warn("tenant unavailable", {
|
|
77
80
|
tenantId: id,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const q = asQuarantine(error);
|
|
82
|
-
if (q) {
|
|
83
|
-
rememberQuarantine(id, q);
|
|
84
|
-
return;
|
|
81
|
+
message: error.cause instanceof Error ? error.cause.message : String(error.cause),
|
|
82
|
+
});
|
|
83
|
+
throw error;
|
|
85
84
|
}
|
|
86
|
-
|
|
85
|
+
if (error instanceof TimeoutError)
|
|
86
|
+
return rememberQuarantine(id, quarantine("open-timeout", `open timed out after ${openTimeoutMs}ms`, {
|
|
87
|
+
tenantId: id,
|
|
88
|
+
}).toQuarantine());
|
|
89
|
+
return rememberQuarantine(id, quarantineOf(error) ??
|
|
90
|
+
quarantine("open-failed", error instanceof Error ? error.message : "open failed", { tenantId: id }).toQuarantine());
|
|
87
91
|
}
|
|
92
|
+
if (closed || deleting.has(id)) {
|
|
93
|
+
await handle.close().catch(() => undefined);
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const entry = { kind: "open", handle };
|
|
97
|
+
entries.set(id, entry);
|
|
98
|
+
return entry;
|
|
88
99
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const paths = tenantPaths(hostRoot, id);
|
|
93
|
-
if (!existsSync(paths.root))
|
|
100
|
+
/** The cached entry, or the result of opening the Tenant once. */
|
|
101
|
+
async function load(id) {
|
|
102
|
+
if (deleting.has(id))
|
|
94
103
|
return undefined;
|
|
95
|
-
|
|
96
|
-
|
|
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);
|
|
113
|
+
}
|
|
114
|
+
return pending;
|
|
97
115
|
}
|
|
116
|
+
const openEntries = () => [...entries.values()].filter((e) => e.kind === "open");
|
|
98
117
|
const module = {
|
|
99
118
|
get started() {
|
|
100
119
|
return started;
|
|
@@ -103,38 +122,25 @@ export function createTenantModule(options) {
|
|
|
103
122
|
if (closed)
|
|
104
123
|
throw new Error("Tenant module is closed");
|
|
105
124
|
const ids = await store.enumerate();
|
|
106
|
-
await mapPool(ids, openConcurrency, async (id) => {
|
|
107
|
-
await openOne(id);
|
|
108
|
-
});
|
|
109
125
|
started = true;
|
|
110
|
-
logger.info("tenant module started", {
|
|
111
|
-
tenants: ids.length,
|
|
112
|
-
open: [...entries.values()].filter((e) => e.kind === "open").length,
|
|
113
|
-
quarantined: [...entries.values()].filter((e) => e.kind === "quarantined").length,
|
|
114
|
-
});
|
|
126
|
+
logger.info("tenant module started", { tenants: ids.length });
|
|
115
127
|
},
|
|
116
|
-
resolve(id) {
|
|
128
|
+
async resolve(id) {
|
|
117
129
|
if (!isTenantId(id))
|
|
118
130
|
return { kind: "not-found" };
|
|
119
|
-
const entry =
|
|
120
|
-
if (!entry)
|
|
121
|
-
// Sync miss: existence check only (disk I/O allowed on miss). Full open
|
|
122
|
-
// happens on list()/status()/create(); Host sees opaque not-found until then.
|
|
123
|
-
try {
|
|
124
|
-
const paths = tenantPaths(hostRoot, id);
|
|
125
|
-
if (!existsSync(paths.root))
|
|
126
|
-
return { kind: "not-found" };
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
return { kind: "not-found" };
|
|
130
|
-
}
|
|
131
|
+
const entry = await load(id);
|
|
132
|
+
if (!entry)
|
|
131
133
|
return { kind: "not-found" };
|
|
132
|
-
|
|
133
|
-
if (entry.kind === "open") {
|
|
134
|
+
if (entry.kind === "open")
|
|
134
135
|
return { kind: "open", handle: entry.handle };
|
|
135
|
-
}
|
|
136
136
|
return { kind: "quarantined", quarantine: entry.quarantine };
|
|
137
137
|
},
|
|
138
|
+
async worker(id) {
|
|
139
|
+
if (!isTenantId(id))
|
|
140
|
+
return undefined;
|
|
141
|
+
const entry = await load(id);
|
|
142
|
+
return entry?.kind === "open" ? entry.handle.worker : undefined;
|
|
143
|
+
},
|
|
138
144
|
async create(input) {
|
|
139
145
|
if (!isTenantId(input.tenantId)) {
|
|
140
146
|
throw new Error(`Invalid tenant id: ${input.tenantId}`);
|
|
@@ -143,11 +149,18 @@ export function createTenantModule(options) {
|
|
|
143
149
|
principalId: input.principalId,
|
|
144
150
|
credentialHash: input.credentialHash,
|
|
145
151
|
idempotencyKey: input.idempotencyKey,
|
|
152
|
+
...(input.studioCredentialHash
|
|
153
|
+
? { studioCredentialHash: input.studioCredentialHash }
|
|
154
|
+
: {}),
|
|
155
|
+
...(input.derivedPrincipals?.length
|
|
156
|
+
? { derivedPrincipals: input.derivedPrincipals }
|
|
157
|
+
: {}),
|
|
146
158
|
};
|
|
159
|
+
// The store records its own schema version.
|
|
147
160
|
const envelope = envelopeNow({
|
|
148
161
|
id: input.tenantId,
|
|
149
162
|
name: input.name,
|
|
150
|
-
schemaVersion:
|
|
163
|
+
schemaVersion: POSTGRES_SCHEMA_VERSION,
|
|
151
164
|
});
|
|
152
165
|
let outcome;
|
|
153
166
|
try {
|
|
@@ -162,53 +175,63 @@ export function createTenantModule(options) {
|
|
|
162
175
|
if (!matches)
|
|
163
176
|
throw new TenantConflictError();
|
|
164
177
|
const existing = await store.readEnvelope(input.tenantId);
|
|
165
|
-
|
|
166
|
-
if (!entry || entry.kind !== "open") {
|
|
167
|
-
await openOne(input.tenantId);
|
|
168
|
-
}
|
|
178
|
+
await load(input.tenantId).catch(() => undefined);
|
|
169
179
|
return { envelope: existing, created: false };
|
|
170
180
|
}
|
|
171
|
-
await
|
|
172
|
-
|
|
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);
|
|
173
186
|
if (opened?.kind === "open") {
|
|
174
187
|
return { envelope: opened.handle.envelope, created: true };
|
|
175
188
|
}
|
|
176
|
-
// Created
|
|
177
|
-
return {
|
|
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
|
+
};
|
|
178
196
|
},
|
|
179
197
|
async list() {
|
|
180
198
|
const ids = new Set(await store.enumerate());
|
|
181
199
|
for (const id of entries.keys())
|
|
182
200
|
ids.add(id);
|
|
183
201
|
const result = [];
|
|
184
|
-
for (const id of ids) {
|
|
185
|
-
if (!isTenantId(id))
|
|
202
|
+
for (const id of [...ids].sort()) {
|
|
203
|
+
if (!isTenantId(id) || deleting.has(id))
|
|
186
204
|
continue;
|
|
187
205
|
let entry = entries.get(id);
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
206
|
+
if (entry?.kind === "open") {
|
|
207
|
+
result.push(toAdmin(id, entry, null));
|
|
208
|
+
continue;
|
|
191
209
|
}
|
|
192
210
|
let envelope = null;
|
|
193
211
|
try {
|
|
194
212
|
envelope = await store.readEnvelope(id);
|
|
195
213
|
if (envelope.id !== id) {
|
|
196
|
-
|
|
197
|
-
|
|
214
|
+
entry = {
|
|
215
|
+
kind: "quarantined",
|
|
216
|
+
quarantine: quarantine("envelope-invalid", `Tenant envelope id ${envelope.id} does not match ${id}`, { tenantId: id }).toQuarantine(),
|
|
217
|
+
};
|
|
198
218
|
envelope = null;
|
|
199
219
|
}
|
|
200
220
|
}
|
|
201
221
|
catch (error) {
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
222
|
+
// Deleted since it was enumerated.
|
|
223
|
+
if (error instanceof TenantNotFoundError)
|
|
224
|
+
continue;
|
|
225
|
+
if (error instanceof TenantUnavailableError)
|
|
226
|
+
throw error;
|
|
227
|
+
// Listed as quarantined; only opening it caches a quarantine.
|
|
228
|
+
entry ??= {
|
|
229
|
+
kind: "quarantined",
|
|
230
|
+
quarantine: quarantineOf(error) ??
|
|
231
|
+
quarantine("envelope-invalid", "Tenant envelope is unreadable", {
|
|
232
|
+
tenantId: id,
|
|
233
|
+
}).toQuarantine(),
|
|
234
|
+
};
|
|
212
235
|
}
|
|
213
236
|
result.push(toAdmin(id, entry, envelope));
|
|
214
237
|
}
|
|
@@ -217,99 +240,83 @@ export function createTenantModule(options) {
|
|
|
217
240
|
async status(id) {
|
|
218
241
|
if (!isTenantId(id))
|
|
219
242
|
return undefined;
|
|
220
|
-
|
|
221
|
-
if (!entry) {
|
|
222
|
-
entry = await discoverId(id);
|
|
223
|
-
if (!entry) {
|
|
224
|
-
// Not in memory and not on disk.
|
|
225
|
-
const known = (await store.enumerate()).includes(id);
|
|
226
|
-
if (!known)
|
|
227
|
-
return undefined;
|
|
228
|
-
await openOne(id);
|
|
229
|
-
entry = entries.get(id);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
243
|
+
const entry = await load(id);
|
|
232
244
|
if (!entry)
|
|
233
245
|
return undefined;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
? entry.handle.envelope
|
|
239
|
-
: await store.readEnvelope(id);
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
envelope = null;
|
|
243
|
-
}
|
|
244
|
-
const base = toAdmin(id, entry, envelope);
|
|
245
|
-
if (entry.kind === "quarantined") {
|
|
246
|
-
return { ...base, quarantine: entry.quarantine };
|
|
247
|
-
}
|
|
248
|
-
return base;
|
|
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 };
|
|
249
250
|
},
|
|
250
251
|
async delete(id, activeWork) {
|
|
251
252
|
if (!isTenantId(id))
|
|
252
253
|
throw new Error(`Invalid tenant id: ${id}`);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
const ids = await store.enumerate();
|
|
261
|
-
if (!ids.includes(id)) {
|
|
262
|
-
throw quarantine("open-failed", `Tenant ${id} not found`, {
|
|
263
|
-
tenantId: id,
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
await openOne(id);
|
|
267
|
-
entry = entries.get(id);
|
|
268
|
-
}
|
|
269
|
-
if (entry?.kind === "open") {
|
|
270
|
-
const summary = entry.handle.summary();
|
|
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();
|
|
271
261
|
if (activeWork === "refuse" && hasLiveWork(summary)) {
|
|
272
262
|
throw new TenantBusyError();
|
|
273
263
|
}
|
|
274
264
|
if (activeWork === "drain" || activeWork === "cancel") {
|
|
275
265
|
await entry.handle.drain(activeWork);
|
|
276
266
|
}
|
|
277
|
-
// Sandbox prefix cleanup is owned by TenantHandle.close() (WS-A).
|
|
278
|
-
await entry.handle.close();
|
|
279
267
|
}
|
|
280
|
-
|
|
281
|
-
|
|
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);
|
|
278
|
+
}
|
|
282
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
|
+
}));
|
|
283
284
|
},
|
|
284
|
-
summarize() {
|
|
285
|
+
async summarize() {
|
|
285
286
|
let runningSessions = 0;
|
|
286
287
|
let connectedExecutors = 0;
|
|
287
288
|
let pendingActions = 0;
|
|
288
289
|
let uncertainEffects = 0;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const s = entry.handle.summary();
|
|
290
|
+
let outboxDepth;
|
|
291
|
+
let relayLagMs;
|
|
292
|
+
for (const s of await Promise.all(openEntries().map((e) => e.handle.summary()))) {
|
|
293
293
|
runningSessions += s.runningSessions;
|
|
294
294
|
connectedExecutors += s.connectedExecutors;
|
|
295
295
|
pendingActions += s.pendingActions;
|
|
296
296
|
uncertainEffects += s.uncertainEffects;
|
|
297
|
+
if (s.outboxDepth !== undefined)
|
|
298
|
+
outboxDepth = (outboxDepth ?? 0) + s.outboxDepth;
|
|
299
|
+
if (s.relayLagMs !== undefined)
|
|
300
|
+
relayLagMs = Math.max(relayLagMs ?? 0, s.relayLagMs);
|
|
297
301
|
}
|
|
298
302
|
return {
|
|
299
303
|
runningSessions,
|
|
300
304
|
connectedExecutors,
|
|
301
305
|
pendingActions,
|
|
302
306
|
uncertainEffects,
|
|
307
|
+
...(outboxDepth !== undefined ? { outboxDepth } : {}),
|
|
308
|
+
...(relayLagMs !== undefined ? { relayLagMs } : {}),
|
|
303
309
|
};
|
|
304
310
|
},
|
|
305
311
|
async close() {
|
|
306
312
|
closed = true;
|
|
307
313
|
started = false;
|
|
308
|
-
|
|
309
|
-
|
|
314
|
+
await Promise.allSettled([...opening.values()]);
|
|
315
|
+
const open = openEntries();
|
|
310
316
|
entries.clear();
|
|
317
|
+
await Promise.all(open.map((e) => e.handle.close().catch(() => undefined)));
|
|
311
318
|
},
|
|
312
319
|
};
|
|
313
320
|
return module;
|
|
314
321
|
}
|
|
315
|
-
export { TenantBusyError, TenantConflictError, isQuarantineError, quarantine, };
|
|
322
|
+
export { TenantBusyError, TenantConflictError, TenantNotFoundError, TenantUnavailableError, isQuarantineError, quarantine, };
|
package/dist/tenant/paths.d.ts
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
1
|
-
import type { TenantPaths } from "./types.js";
|
|
1
|
+
import type { Logger, TenantPaths } from "./types.js";
|
|
2
2
|
export interface HostPaths {
|
|
3
3
|
root: string;
|
|
4
4
|
config: string;
|
|
5
|
-
state: string;
|
|
6
5
|
credentials: string;
|
|
7
|
-
log: string;
|
|
8
6
|
home: string;
|
|
9
7
|
tmp: string;
|
|
10
|
-
runtime: string;
|
|
11
8
|
tenants: string;
|
|
12
9
|
trash: string;
|
|
13
10
|
}
|
|
14
11
|
/** Host layout under NYLORUN_HOME / ~/.nylorun. */
|
|
15
12
|
export declare function hostPaths(hostRoot: string): HostPaths;
|
|
16
13
|
/**
|
|
17
|
-
* Absolute Tenant paths under `<hostRoot>/tenants/<tenantId>/`.
|
|
14
|
+
* Absolute Tenant directory paths under `<hostRoot>/tenants/<tenantId>/`.
|
|
18
15
|
* Validates the id and asserts containment after realpath.
|
|
19
16
|
*
|
|
20
|
-
*
|
|
21
|
-
* `
|
|
22
|
-
* `home/`, `tmp/`, `.migration/`, `sandboxes/`, `plugin-data/`, `logs/tenant.log`.
|
|
17
|
+
* The directory holds what stays on the Host (the Tenant's data is its Postgres schema):
|
|
18
|
+
* `vault-kek`, `home/`, `tmp/`, `sandboxes/`, `plugin-data/`, `logs/tenant.log`.
|
|
23
19
|
*/
|
|
24
20
|
export declare function tenantPaths(hostRoot: string, tenantId: string): TenantPaths;
|
|
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[];
|
package/dist/tenant/paths.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, realpathSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, realpathSync, renameSync, } from "node:fs";
|
|
2
2
|
import { join, resolve, sep } from "node:path";
|
|
3
3
|
import { isTenantId } from "@nylorun/core/compatibility";
|
|
4
4
|
function ensureDir(path) {
|
|
@@ -17,23 +17,19 @@ export function hostPaths(hostRoot) {
|
|
|
17
17
|
return {
|
|
18
18
|
root,
|
|
19
19
|
config: join(root, "host.json"),
|
|
20
|
-
state: join(root, "host-state.json"),
|
|
21
20
|
credentials: join(root, "host-credentials.json"),
|
|
22
|
-
log: join(root, "runtime.log"),
|
|
23
21
|
home: join(root, "home"),
|
|
24
22
|
tmp: join(root, "tmp"),
|
|
25
|
-
runtime: join(root, "runtime"),
|
|
26
23
|
tenants: join(root, "tenants"),
|
|
27
24
|
trash: join(root, "trash"),
|
|
28
25
|
};
|
|
29
26
|
}
|
|
30
27
|
/**
|
|
31
|
-
* Absolute Tenant paths under `<hostRoot>/tenants/<tenantId>/`.
|
|
28
|
+
* Absolute Tenant directory paths under `<hostRoot>/tenants/<tenantId>/`.
|
|
32
29
|
* Validates the id and asserts containment after realpath.
|
|
33
30
|
*
|
|
34
|
-
*
|
|
35
|
-
* `
|
|
36
|
-
* `home/`, `tmp/`, `.migration/`, `sandboxes/`, `plugin-data/`, `logs/tenant.log`.
|
|
31
|
+
* The directory holds what stays on the Host (the Tenant's data is its Postgres schema):
|
|
32
|
+
* `vault-kek`, `home/`, `tmp/`, `sandboxes/`, `plugin-data/`, `logs/tenant.log`.
|
|
37
33
|
*/
|
|
38
34
|
export function tenantPaths(hostRoot, tenantId) {
|
|
39
35
|
if (!isTenantId(tenantId)) {
|
|
@@ -50,16 +46,46 @@ export function tenantPaths(hostRoot, tenantId) {
|
|
|
50
46
|
}
|
|
51
47
|
return {
|
|
52
48
|
root: resolvedRoot,
|
|
53
|
-
envelope: join(resolvedRoot, "tenant.json"),
|
|
54
|
-
database: join(resolvedRoot, "tenant.sqlite"),
|
|
55
|
-
lock: join(resolvedRoot, ".runtime-lock"),
|
|
56
49
|
kek: join(resolvedRoot, "vault-kek"),
|
|
57
50
|
home: join(resolvedRoot, "home"),
|
|
58
51
|
tmp: join(resolvedRoot, "tmp"),
|
|
59
|
-
migration: join(resolvedRoot, ".migration"),
|
|
60
52
|
sandboxes: join(resolvedRoot, "sandboxes"),
|
|
61
53
|
pluginData: join(resolvedRoot, "plugin-data"),
|
|
62
54
|
logs: join(resolvedRoot, "logs"),
|
|
63
55
|
log: join(resolvedRoot, "logs", "tenant.log"),
|
|
64
56
|
};
|
|
65
57
|
}
|
|
58
|
+
/** The database file of a Tenant from before the Postgres Session Store. */
|
|
59
|
+
const SQLITE_DATABASE = "tenant.sqlite";
|
|
60
|
+
/**
|
|
61
|
+
* Moves every Tenant directory that holds a `tenant.sqlite` to `trash/`, logging each.
|
|
62
|
+
*
|
|
63
|
+
* Those are beta Tenants from the SQLite Runtime. They are not migrated (recreate them); a
|
|
64
|
+
* Postgres Tenant's directory never holds that file. Moving them at Host start keeps a new
|
|
65
|
+
* Postgres Tenant with the same id from sharing the old directory. Returns the moved ids.
|
|
66
|
+
*/
|
|
67
|
+
export function trashSqliteTenants(hostRoot, logger) {
|
|
68
|
+
const host = hostPaths(hostRoot);
|
|
69
|
+
if (!existsSync(host.tenants))
|
|
70
|
+
return [];
|
|
71
|
+
const moved = [];
|
|
72
|
+
const stamp = new Date().toISOString().replaceAll(":", "-");
|
|
73
|
+
for (const entry of readdirSync(host.tenants, { withFileTypes: true })) {
|
|
74
|
+
if (!entry.isDirectory())
|
|
75
|
+
continue;
|
|
76
|
+
const directory = join(host.tenants, entry.name);
|
|
77
|
+
if (!existsSync(join(directory, SQLITE_DATABASE)))
|
|
78
|
+
continue;
|
|
79
|
+
mkdirSync(host.trash, { recursive: true });
|
|
80
|
+
const destination = join(host.trash, `${entry.name}-sqlite-${stamp}`);
|
|
81
|
+
renameSync(directory, destination);
|
|
82
|
+
moved.push(entry.name);
|
|
83
|
+
logger.warn("sqlite_tenant_moved_to_trash", {
|
|
84
|
+
tenant: entry.name,
|
|
85
|
+
from: directory,
|
|
86
|
+
to: destination,
|
|
87
|
+
reason: "SQLite Tenants are not migrated to the Postgres Session Store; recreate the Tenant",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return moved;
|
|
91
|
+
}
|