@nylorun/runtime 0.10.0-beta → 0.11.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +53 -51
- package/dist/adapters/execution/restate.d.ts +130 -0
- package/dist/adapters/execution/restate.js +442 -0
- package/dist/adapters/sandbox/virtual.js +1 -1
- package/dist/adapters/streams/s2.d.ts +18 -0
- package/dist/adapters/streams/s2.js +361 -0
- package/dist/core/executors.d.ts +4 -1
- package/dist/core/executors.js +20 -1
- package/dist/core/flow-host.d.ts +93 -132
- package/dist/core/flow-host.js +282 -547
- package/dist/core/sandbox-routes.d.ts +8 -3
- package/dist/core/sandbox-routes.js +10 -10
- package/dist/execution/memory.d.ts +54 -0
- package/dist/execution/memory.js +217 -0
- package/dist/execution/types.d.ts +125 -0
- package/dist/execution/types.js +52 -0
- package/dist/host/config-for.d.ts +2 -1
- package/dist/host/config-for.js +3 -21
- package/dist/host/config.d.ts +0 -8
- package/dist/host/create-host.d.ts +36 -1
- package/dist/host/create-host.js +48 -24
- package/dist/host/execution.d.ts +61 -0
- package/dist/host/execution.js +69 -0
- package/dist/host/http.d.ts +4 -0
- package/dist/host/http.js +6 -0
- package/dist/host/index.d.ts +3 -1
- package/dist/host/index.js +2 -0
- package/dist/host/main.js +106 -47
- package/dist/host/stack-config.d.ts +72 -0
- package/dist/host/stack-config.js +193 -0
- package/dist/index.d.ts +1 -1
- package/dist/infra/database.d.ts +16 -0
- package/dist/infra/database.js +31 -0
- package/dist/infra/execution.d.ts +39 -0
- package/dist/infra/execution.js +68 -0
- package/dist/infra/index.d.ts +42 -0
- package/dist/infra/index.js +56 -0
- package/dist/infra/readiness.d.ts +33 -0
- package/dist/infra/readiness.js +53 -0
- package/dist/infra/streams.d.ts +17 -0
- package/dist/infra/streams.js +33 -0
- package/dist/infra/workers.d.ts +39 -0
- package/dist/infra/workers.js +31 -0
- package/dist/model/pi-model.d.ts +2 -2
- package/dist/model/pi-model.js +5 -5
- package/dist/sandbox/manager.d.ts +26 -8
- package/dist/sandbox/manager.js +95 -47
- package/dist/sandbox/select.d.ts +1 -1
- package/dist/sandbox/select.js +4 -5
- package/dist/sandbox/types.d.ts +2 -2
- package/dist/store/canonical.d.ts +5 -0
- package/dist/store/canonical.js +17 -0
- package/dist/store/cursor.d.ts +9 -0
- package/dist/store/cursor.js +18 -0
- package/dist/store/memory.d.ts +57 -0
- package/dist/store/memory.js +643 -0
- package/dist/store/ownership.d.ts +19 -0
- package/dist/store/ownership.js +33 -0
- package/dist/store/postgres/connect.d.ts +25 -0
- package/dist/store/postgres/connect.js +25 -0
- package/dist/store/postgres/locking.d.ts +16 -0
- package/dist/store/postgres/locking.js +22 -0
- package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
- package/dist/store/postgres/migrations/001_initial.js +196 -0
- package/dist/store/postgres/migrations/index.d.ts +49 -0
- package/dist/store/postgres/migrations/index.js +75 -0
- package/dist/store/postgres/names.d.ts +11 -0
- package/dist/store/postgres/names.js +42 -0
- package/dist/store/postgres/store.d.ts +11 -0
- package/dist/store/postgres/store.js +790 -0
- package/dist/store/postgres/tenants.d.ts +90 -0
- package/dist/store/postgres/tenants.js +207 -0
- package/dist/store/types.d.ts +440 -0
- package/dist/store/types.js +10 -0
- package/dist/streams/basin.d.ts +23 -0
- package/dist/streams/basin.js +51 -0
- package/dist/streams/memory.d.ts +25 -0
- package/dist/streams/memory.js +124 -0
- package/dist/streams/relay.d.ts +37 -0
- package/dist/streams/relay.js +254 -0
- package/dist/streams/types.d.ts +115 -0
- package/dist/streams/types.js +86 -0
- package/dist/tenant/actions.d.ts +153 -0
- package/dist/tenant/actions.js +191 -0
- package/dist/tenant/advance.d.ts +15 -0
- package/dist/tenant/advance.js +439 -0
- package/dist/tenant/auth.d.ts +11 -0
- package/dist/tenant/auth.js +34 -0
- package/dist/tenant/commands.d.ts +3 -0
- package/dist/tenant/commands.js +384 -0
- package/dist/tenant/context.d.ts +140 -0
- package/dist/tenant/context.js +40 -0
- package/dist/tenant/effects.d.ts +20 -0
- package/dist/tenant/effects.js +539 -0
- package/dist/tenant/envelope.d.ts +0 -3
- package/dist/tenant/envelope.js +2 -34
- package/dist/tenant/ephemeral.d.ts +15 -4
- package/dist/tenant/ephemeral.js +106 -38
- package/dist/tenant/http.d.ts +32 -0
- package/dist/tenant/http.js +51 -0
- package/dist/tenant/live.d.ts +92 -0
- package/dist/tenant/live.js +290 -0
- package/dist/tenant/model-setting.d.ts +16 -0
- package/dist/tenant/model-setting.js +12 -0
- package/dist/tenant/module.d.ts +29 -8
- package/dist/tenant/module.js +162 -158
- package/dist/tenant/paths.d.ts +12 -8
- package/dist/tenant/paths.js +38 -12
- package/dist/tenant/principals.d.ts +9 -13
- package/dist/tenant/principals.js +31 -38
- package/dist/tenant/quarantine-error.d.ts +1 -1
- package/dist/tenant/quarantine-error.js +2 -8
- package/dist/tenant/quarantine.d.ts +0 -8
- package/dist/tenant/quarantine.js +8 -30
- package/dist/tenant/reset.d.ts +7 -6
- package/dist/tenant/reset.js +7 -51
- package/dist/tenant/routes-tenant.d.ts +12 -0
- package/dist/tenant/routes-tenant.js +126 -0
- package/dist/tenant/routes.d.ts +9 -0
- package/dist/tenant/routes.js +142 -0
- package/dist/tenant/runtime.d.ts +59 -58
- package/dist/tenant/runtime.js +243 -2201
- package/dist/tenant/scheduler.d.ts +25 -0
- package/dist/tenant/scheduler.js +84 -0
- package/dist/tenant/session.d.ts +60 -0
- package/dist/tenant/session.js +94 -0
- package/dist/tenant/sessions.d.ts +34 -0
- package/dist/tenant/sessions.js +149 -0
- package/dist/tenant/status.d.ts +13 -4
- package/dist/tenant/status.js +76 -41
- package/dist/tenant/store-memory.d.ts +9 -12
- package/dist/tenant/store-memory.js +79 -68
- package/dist/tenant/store-pg.d.ts +15 -0
- package/dist/tenant/store-pg.js +140 -0
- package/dist/tenant/streams.d.ts +126 -0
- package/dist/tenant/streams.js +419 -0
- package/dist/tenant/sweep.d.ts +15 -0
- package/dist/tenant/sweep.js +122 -0
- package/dist/tenant/types.d.ts +95 -15
- package/dist/tenant/types.js +23 -1
- package/dist/tenant/worker.d.ts +84 -0
- package/dist/tenant/worker.js +189 -0
- package/dist/vault/service.d.ts +37 -31
- package/dist/vault/service.js +439 -359
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -13
- package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
- package/dist/adapters/sandbox/microsandbox.js +0 -238
- package/dist/core/store.d.ts +0 -32
- package/dist/core/store.js +0 -143
- package/dist/launcher/commands.d.ts +0 -24
- package/dist/launcher/commands.js +0 -260
- package/dist/launcher/ensure-config.d.ts +0 -16
- package/dist/launcher/ensure-config.js +0 -79
- package/dist/launcher/errors.d.ts +0 -12
- package/dist/launcher/errors.js +0 -22
- package/dist/launcher/host-config.d.ts +0 -17
- package/dist/launcher/host-config.js +0 -64
- package/dist/launcher/host-state.d.ts +0 -17
- package/dist/launcher/host-state.js +0 -61
- package/dist/launcher/lifecycle.d.ts +0 -66
- package/dist/launcher/lifecycle.js +0 -407
- package/dist/launcher/locks.d.ts +0 -26
- package/dist/launcher/locks.js +0 -117
- package/dist/launcher/logs.d.ts +0 -18
- package/dist/launcher/logs.js +0 -117
- package/dist/launcher/main.d.ts +0 -2
- package/dist/launcher/main.js +0 -66
- package/dist/launcher/output.d.ts +0 -11
- package/dist/launcher/output.js +0 -68
- package/dist/launcher/paths.d.ts +0 -20
- package/dist/launcher/paths.js +0 -36
- package/dist/launcher/probe.d.ts +0 -23
- package/dist/launcher/probe.js +0 -72
- package/dist/launcher/protocol.d.ts +0 -62
- package/dist/launcher/protocol.js +0 -1
- package/dist/launcher/schema-guard.d.ts +0 -6
- package/dist/launcher/schema-guard.js +0 -36
- package/dist/launcher/spawn.d.ts +0 -22
- package/dist/launcher/spawn.js +0 -85
- package/dist/launcher/status.d.ts +0 -6
- package/dist/launcher/status.js +0 -65
- package/dist/tenant/migration.d.ts +0 -16
- package/dist/tenant/migration.js +0 -116
- package/dist/tenant/schema.d.ts +0 -23
- package/dist/tenant/schema.js +0 -151
- package/dist/tenant/store-fs.d.ts +0 -16
- package/dist/tenant/store-fs.js +0 -205
|
@@ -0,0 +1,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,15 @@ 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
|
+
: {}),
|
|
146
155
|
};
|
|
156
|
+
// The store records its own schema version.
|
|
147
157
|
const envelope = envelopeNow({
|
|
148
158
|
id: input.tenantId,
|
|
149
159
|
name: input.name,
|
|
150
|
-
schemaVersion:
|
|
160
|
+
schemaVersion: POSTGRES_SCHEMA_VERSION,
|
|
151
161
|
});
|
|
152
162
|
let outcome;
|
|
153
163
|
try {
|
|
@@ -162,53 +172,63 @@ export function createTenantModule(options) {
|
|
|
162
172
|
if (!matches)
|
|
163
173
|
throw new TenantConflictError();
|
|
164
174
|
const existing = await store.readEnvelope(input.tenantId);
|
|
165
|
-
|
|
166
|
-
if (!entry || entry.kind !== "open") {
|
|
167
|
-
await openOne(input.tenantId);
|
|
168
|
-
}
|
|
175
|
+
await load(input.tenantId).catch(() => undefined);
|
|
169
176
|
return { envelope: existing, created: false };
|
|
170
177
|
}
|
|
171
|
-
await
|
|
172
|
-
|
|
178
|
+
await options.onCreated?.(input.tenantId).catch((error) => logger.warn("tenant create step failed; open repairs it", {
|
|
179
|
+
tenantId: input.tenantId,
|
|
180
|
+
message: error instanceof Error ? error.message : String(error),
|
|
181
|
+
}));
|
|
182
|
+
const opened = await load(input.tenantId).catch(() => undefined);
|
|
173
183
|
if (opened?.kind === "open") {
|
|
174
184
|
return { envelope: opened.handle.envelope, created: true };
|
|
175
185
|
}
|
|
176
|
-
// Created
|
|
177
|
-
return {
|
|
186
|
+
// Created but failed to open: report the stored envelope.
|
|
187
|
+
return {
|
|
188
|
+
envelope: await store
|
|
189
|
+
.readEnvelope(input.tenantId)
|
|
190
|
+
.catch(() => envelope),
|
|
191
|
+
created: true,
|
|
192
|
+
};
|
|
178
193
|
},
|
|
179
194
|
async list() {
|
|
180
195
|
const ids = new Set(await store.enumerate());
|
|
181
196
|
for (const id of entries.keys())
|
|
182
197
|
ids.add(id);
|
|
183
198
|
const result = [];
|
|
184
|
-
for (const id of ids) {
|
|
185
|
-
if (!isTenantId(id))
|
|
199
|
+
for (const id of [...ids].sort()) {
|
|
200
|
+
if (!isTenantId(id) || deleting.has(id))
|
|
186
201
|
continue;
|
|
187
202
|
let entry = entries.get(id);
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
203
|
+
if (entry?.kind === "open") {
|
|
204
|
+
result.push(toAdmin(id, entry, null));
|
|
205
|
+
continue;
|
|
191
206
|
}
|
|
192
207
|
let envelope = null;
|
|
193
208
|
try {
|
|
194
209
|
envelope = await store.readEnvelope(id);
|
|
195
210
|
if (envelope.id !== id) {
|
|
196
|
-
|
|
197
|
-
|
|
211
|
+
entry = {
|
|
212
|
+
kind: "quarantined",
|
|
213
|
+
quarantine: quarantine("envelope-invalid", `Tenant envelope id ${envelope.id} does not match ${id}`, { tenantId: id }).toQuarantine(),
|
|
214
|
+
};
|
|
198
215
|
envelope = null;
|
|
199
216
|
}
|
|
200
217
|
}
|
|
201
218
|
catch (error) {
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
219
|
+
// Deleted since it was enumerated.
|
|
220
|
+
if (error instanceof TenantNotFoundError)
|
|
221
|
+
continue;
|
|
222
|
+
if (error instanceof TenantUnavailableError)
|
|
223
|
+
throw error;
|
|
224
|
+
// Listed as quarantined; only opening it caches a quarantine.
|
|
225
|
+
entry ??= {
|
|
226
|
+
kind: "quarantined",
|
|
227
|
+
quarantine: quarantineOf(error) ??
|
|
228
|
+
quarantine("envelope-invalid", "Tenant envelope is unreadable", {
|
|
229
|
+
tenantId: id,
|
|
230
|
+
}).toQuarantine(),
|
|
231
|
+
};
|
|
212
232
|
}
|
|
213
233
|
result.push(toAdmin(id, entry, envelope));
|
|
214
234
|
}
|
|
@@ -217,99 +237,83 @@ export function createTenantModule(options) {
|
|
|
217
237
|
async status(id) {
|
|
218
238
|
if (!isTenantId(id))
|
|
219
239
|
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
|
-
}
|
|
240
|
+
const entry = await load(id);
|
|
232
241
|
if (!entry)
|
|
233
242
|
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;
|
|
243
|
+
if (entry.kind === "open")
|
|
244
|
+
return toAdmin(id, entry, null);
|
|
245
|
+
const envelope = await store.readEnvelope(id).catch(() => null);
|
|
246
|
+
return { ...toAdmin(id, entry, envelope), quarantine: entry.quarantine };
|
|
249
247
|
},
|
|
250
248
|
async delete(id, activeWork) {
|
|
251
249
|
if (!isTenantId(id))
|
|
252
250
|
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();
|
|
251
|
+
if (deleting.has(id))
|
|
252
|
+
throw new TenantBusyError("Tenant is being deleted");
|
|
253
|
+
const entry = await load(id);
|
|
254
|
+
if (!entry)
|
|
255
|
+
throw new TenantNotFoundError(id);
|
|
256
|
+
if (entry.kind === "open") {
|
|
257
|
+
const summary = await entry.handle.summary();
|
|
271
258
|
if (activeWork === "refuse" && hasLiveWork(summary)) {
|
|
272
259
|
throw new TenantBusyError();
|
|
273
260
|
}
|
|
274
261
|
if (activeWork === "drain" || activeWork === "cancel") {
|
|
275
262
|
await entry.handle.drain(activeWork);
|
|
276
263
|
}
|
|
277
|
-
// Sandbox prefix cleanup is owned by TenantHandle.close() (WS-A).
|
|
278
|
-
await entry.handle.close();
|
|
279
264
|
}
|
|
280
|
-
|
|
281
|
-
|
|
265
|
+
deleting.add(id);
|
|
266
|
+
try {
|
|
267
|
+
entries.delete(id);
|
|
268
|
+
// Sandbox prefix cleanup is owned by TenantHandle.close().
|
|
269
|
+
if (entry.kind === "open")
|
|
270
|
+
await entry.handle.close();
|
|
271
|
+
await store.trash(id, new Date());
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
deleting.delete(id);
|
|
275
|
+
}
|
|
282
276
|
logger.info("tenant deleted", { tenantId: id, activeWork });
|
|
277
|
+
await options.onDeleted?.(id).catch((error) => logger.warn("tenant delete cleanup failed", {
|
|
278
|
+
tenantId: id,
|
|
279
|
+
message: error instanceof Error ? error.message : String(error),
|
|
280
|
+
}));
|
|
283
281
|
},
|
|
284
|
-
summarize() {
|
|
282
|
+
async summarize() {
|
|
285
283
|
let runningSessions = 0;
|
|
286
284
|
let connectedExecutors = 0;
|
|
287
285
|
let pendingActions = 0;
|
|
288
286
|
let uncertainEffects = 0;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const s = entry.handle.summary();
|
|
287
|
+
let outboxDepth;
|
|
288
|
+
let relayLagMs;
|
|
289
|
+
for (const s of await Promise.all(openEntries().map((e) => e.handle.summary()))) {
|
|
293
290
|
runningSessions += s.runningSessions;
|
|
294
291
|
connectedExecutors += s.connectedExecutors;
|
|
295
292
|
pendingActions += s.pendingActions;
|
|
296
293
|
uncertainEffects += s.uncertainEffects;
|
|
294
|
+
if (s.outboxDepth !== undefined)
|
|
295
|
+
outboxDepth = (outboxDepth ?? 0) + s.outboxDepth;
|
|
296
|
+
if (s.relayLagMs !== undefined)
|
|
297
|
+
relayLagMs = Math.max(relayLagMs ?? 0, s.relayLagMs);
|
|
297
298
|
}
|
|
298
299
|
return {
|
|
299
300
|
runningSessions,
|
|
300
301
|
connectedExecutors,
|
|
301
302
|
pendingActions,
|
|
302
303
|
uncertainEffects,
|
|
304
|
+
...(outboxDepth !== undefined ? { outboxDepth } : {}),
|
|
305
|
+
...(relayLagMs !== undefined ? { relayLagMs } : {}),
|
|
303
306
|
};
|
|
304
307
|
},
|
|
305
308
|
async close() {
|
|
306
309
|
closed = true;
|
|
307
310
|
started = false;
|
|
308
|
-
|
|
309
|
-
|
|
311
|
+
await Promise.allSettled([...opening.values()]);
|
|
312
|
+
const open = openEntries();
|
|
310
313
|
entries.clear();
|
|
314
|
+
await Promise.all(open.map((e) => e.handle.close().catch(() => undefined)));
|
|
311
315
|
},
|
|
312
316
|
};
|
|
313
317
|
return module;
|
|
314
318
|
}
|
|
315
|
-
export { TenantBusyError, TenantConflictError, isQuarantineError, quarantine, };
|
|
319
|
+
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
|
+
}
|