target-supabase-sdk 0.2.3 → 0.2.5
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/README.md +24 -18
- package/dist/browser.d.ts +11 -4
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +3 -3
- package/dist/chunks/{enable-log-persist-C7GJo_Nc.js → enable-log-persist-BL2FaGQp.js} +10 -8
- package/dist/chunks/enable-log-persist-BL2FaGQp.js.map +1 -0
- package/dist/chunks/{create-logger-DyH3mZ8p.js → list.api-Ab9v8yOg.js} +66 -57
- package/dist/chunks/list.api-Ab9v8yOg.js.map +1 -0
- package/dist/chunks/{word.interface-DLgRobKV.js → word.interface-Mw7bTSqs.js} +1191 -97
- package/dist/chunks/word.interface-Mw7bTSqs.js.map +1 -0
- package/dist/extraction/extraction.api.d.ts +117 -0
- package/dist/extraction/extraction.api.d.ts.map +1 -0
- package/dist/extraction/extraction.interface.d.ts +43 -0
- package/dist/extraction/extraction.interface.d.ts.map +1 -0
- package/dist/list/list.interface.d.ts +2 -2
- package/dist/list/list.interface.d.ts.map +1 -1
- package/dist/node/fs/json-state-store.d.ts.map +1 -1
- package/dist/node/fs/runtime-state-path.d.ts +17 -0
- package/dist/node/fs/runtime-state-path.d.ts.map +1 -0
- package/dist/node/fs/sharded-json-state-store.d.ts +19 -0
- package/dist/node/fs/sharded-json-state-store.d.ts.map +1 -0
- package/dist/node/node-runtime.base.d.ts +13 -1
- package/dist/node/node-runtime.base.d.ts.map +1 -1
- package/dist/node/process/create-managed-child-processes.d.ts +12 -0
- package/dist/node/process/create-managed-child-processes.d.ts.map +1 -0
- package/dist/node/process/index.d.ts +3 -1
- package/dist/node/process/index.d.ts.map +1 -1
- package/dist/node/process/managed-child-processes.d.ts +15 -2
- package/dist/node/process/managed-child-processes.d.ts.map +1 -1
- package/dist/node/runtime-state/create-service-runtime-state-store.d.ts +27 -0
- package/dist/node/runtime-state/create-service-runtime-state-store.d.ts.map +1 -0
- package/dist/node/runtime-state/index.d.ts +4 -0
- package/dist/node/runtime-state/index.d.ts.map +1 -0
- package/dist/node/runtime-state/service-runtime-state.types.d.ts +57 -0
- package/dist/node/runtime-state/service-runtime-state.types.d.ts.map +1 -0
- package/dist/node/service-guard/index.d.ts +9 -0
- package/dist/node/service-guard/index.d.ts.map +1 -0
- package/dist/node/service-guard/register-service-guard-runner.d.ts +9 -0
- package/dist/node/service-guard/register-service-guard-runner.d.ts.map +1 -0
- package/dist/node/service-guard/run-readiness-gate.d.ts +11 -0
- package/dist/node/service-guard/run-readiness-gate.d.ts.map +1 -0
- package/dist/node/service-guard/run-service-guard-tick.d.ts +7 -0
- package/dist/node/service-guard/run-service-guard-tick.d.ts.map +1 -0
- package/dist/node/service-guard/service-guard-node.d.ts +18 -0
- package/dist/node/service-guard/service-guard-node.d.ts.map +1 -0
- package/dist/node/service-guard/service-guard.interface.d.ts +58 -0
- package/dist/node/service-guard/service-guard.interface.d.ts.map +1 -0
- package/dist/node/service-guard/worker-spawn-cooldown.d.ts +4 -0
- package/dist/node/service-guard/worker-spawn-cooldown.d.ts.map +1 -0
- package/dist/node/service-host/index.d.ts +9 -0
- package/dist/node/service-host/index.d.ts.map +1 -0
- package/dist/node/service-host/registry-slot-guard-step.d.ts +21 -0
- package/dist/node/service-host/registry-slot-guard-step.d.ts.map +1 -0
- package/dist/node/service-host/service-host.d.ts +36 -0
- package/dist/node/service-host/service-host.d.ts.map +1 -0
- package/dist/node/service-host/single-process-service.d.ts +21 -0
- package/dist/node/service-host/single-process-service.d.ts.map +1 -0
- package/dist/node.d.ts +15 -2
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +2221 -1485
- package/dist/node.js.map +1 -1
- package/dist/preload.js +3 -3
- package/dist/service/config.api.d.ts +143 -0
- package/dist/service/config.api.d.ts.map +1 -0
- package/dist/service/config.interface.d.ts +22 -0
- package/dist/service/config.interface.d.ts.map +1 -0
- package/dist/service/index.d.ts +16 -17
- package/dist/service/index.d.ts.map +1 -1
- package/dist/service/registry-lifecycle.d.ts +43 -0
- package/dist/service/registry-lifecycle.d.ts.map +1 -0
- package/dist/service/registry.service.d.ts +117 -0
- package/dist/service/registry.service.d.ts.map +1 -0
- package/dist/service/service-bootstrap.d.ts +28 -0
- package/dist/service/service-bootstrap.d.ts.map +1 -0
- package/dist/service/service.api.d.ts +160 -0
- package/dist/service/service.api.d.ts.map +1 -1
- package/dist/service/service.interface.d.ts +32 -1
- package/dist/service/service.interface.d.ts.map +1 -1
- package/dist/shared/log/index.d.ts +4 -0
- package/dist/shared/log/index.d.ts.map +1 -1
- package/dist/shared/log/log-batch.d.ts +44 -0
- package/dist/shared/log/log-batch.d.ts.map +1 -0
- package/dist/shared/log/log-persist-coordinator.d.ts +37 -0
- package/dist/shared/log/log-persist-coordinator.d.ts.map +1 -0
- package/dist/shared/log/log-persist-flush.d.ts.map +1 -1
- package/dist/shared/log/log-persist.config.d.ts +0 -1
- package/dist/shared/log/log-persist.config.d.ts.map +1 -1
- package/dist/shared/log/log-persist.interface.d.ts +8 -0
- package/dist/shared/log/log-persist.interface.d.ts.map +1 -1
- package/dist/shared/utils/config-path.utils.d.ts +21 -0
- package/dist/shared/utils/config-path.utils.d.ts.map +1 -1
- package/dist/shared/utils/get-value-at-path.d.ts +3 -0
- package/dist/shared/utils/get-value-at-path.d.ts.map +1 -0
- package/dist/task/index.d.ts +2 -0
- package/dist/task/index.d.ts.map +1 -1
- package/dist/task/task-manager.d.ts +1 -1
- package/dist/task/task-manager.d.ts.map +1 -1
- package/dist/task/task-post-retry.api.d.ts +24 -0
- package/dist/task/task-post-retry.api.d.ts.map +1 -0
- package/dist/task/task-repo-context.d.ts +7 -1
- package/dist/task/task-repo-context.d.ts.map +1 -1
- package/dist/trigger/trigger-manager.d.ts +1 -1
- package/dist/trigger/trigger-manager.d.ts.map +1 -1
- package/dist/trigger/trigger-node.d.ts +1 -0
- package/dist/trigger/trigger-node.d.ts.map +1 -1
- package/dist/trigger/trigger.constant.d.ts +1 -1
- package/dist/trigger/trigger.constant.d.ts.map +1 -1
- package/dist/trigger/trigger.interface.d.ts +10 -0
- package/dist/trigger/trigger.interface.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/browser.ts +102 -4
- package/src/extraction/extraction.api.ts +232 -0
- package/src/extraction/extraction.interface.ts +45 -0
- package/src/list/list.interface.ts +2 -2
- package/src/node/fs/json-state-store.ts +5 -2
- package/src/node/fs/runtime-state-path.ts +35 -0
- package/src/node/fs/sharded-json-state-store.ts +157 -0
- package/src/node/node-runtime.base.ts +37 -6
- package/src/node/process/create-managed-child-processes.ts +27 -0
- package/src/node/process/index.ts +4 -0
- package/src/node/process/managed-child-processes.ts +48 -15
- package/src/node/runtime-state/create-service-runtime-state-store.ts +125 -0
- package/src/node/runtime-state/index.ts +17 -0
- package/src/node/runtime-state/service-runtime-state.types.ts +64 -0
- package/src/node/service-guard/index.ts +17 -0
- package/src/node/service-guard/register-service-guard-runner.ts +64 -0
- package/src/node/service-guard/run-readiness-gate.ts +34 -0
- package/src/node/service-guard/run-service-guard-tick.ts +106 -0
- package/src/node/service-guard/service-guard-node.ts +52 -0
- package/src/node/service-guard/service-guard.interface.ts +64 -0
- package/src/node/service-guard/worker-spawn-cooldown.ts +13 -0
- package/src/node/service-host/index.ts +30 -0
- package/src/node/service-host/registry-slot-guard-step.ts +57 -0
- package/src/node/service-host/service-host.ts +168 -0
- package/src/node/service-host/single-process-service.ts +58 -0
- package/src/node.ts +69 -0
- package/src/service/config.api.ts +224 -0
- package/src/service/config.interface.ts +25 -0
- package/src/service/index.ts +78 -149
- package/src/service/registry-lifecycle.ts +144 -0
- package/src/service/registry.service.ts +726 -0
- package/src/service/service-bootstrap.ts +74 -0
- package/src/service/service.api.ts +13 -0
- package/src/service/service.interface.ts +36 -1
- package/src/shared/log/index.ts +13 -0
- package/src/shared/log/log-batch.ts +156 -0
- package/src/shared/log/log-persist-coordinator.ts +107 -0
- package/src/shared/log/log-persist-flush.ts +3 -3
- package/src/shared/log/log-persist.config.ts +0 -2
- package/src/shared/log/log-persist.interface.ts +10 -0
- package/src/shared/utils/config-path.utils.ts +35 -0
- package/src/shared/utils/get-value-at-path.ts +17 -0
- package/src/task/index.ts +2 -0
- package/src/task/task-manager.ts +10 -4
- package/src/task/task-post-retry.api.ts +61 -0
- package/src/task/task-repo-context.ts +8 -1
- package/src/trigger/trigger-manager.ts +4 -2
- package/src/trigger/trigger-node.ts +10 -4
- package/src/trigger/trigger.constant.ts +1 -1
- package/src/trigger/trigger.interface.ts +10 -0
- package/dist/chunks/create-logger-DyH3mZ8p.js.map +0 -1
- package/dist/chunks/enable-log-persist-C7GJo_Nc.js.map +0 -1
- package/dist/chunks/word.interface-DLgRobKV.js.map +0 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { TriggerRunnerContext } from "../../trigger/trigger.interface";
|
|
2
|
+
import { TriggerManager } from "../../trigger/trigger-manager";
|
|
3
|
+
import { runServiceGuardTick } from "./run-service-guard-tick";
|
|
4
|
+
import { type RegisterServiceGuardRunnerOptions, SERVICE_GUARD_RUNNER_KEY } from "./service-guard.interface";
|
|
5
|
+
|
|
6
|
+
async function runServiceGuardRunner(
|
|
7
|
+
ctx: TriggerRunnerContext,
|
|
8
|
+
options: RegisterServiceGuardRunnerOptionsWithClock,
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
const { loopTraceId, nodeId, logger } = ctx;
|
|
11
|
+
const nowMs = options.now?.() ?? Date.now();
|
|
12
|
+
const checkedAt = new Date(nowMs).toISOString();
|
|
13
|
+
const serviceId = await options.getServiceId();
|
|
14
|
+
|
|
15
|
+
const result = await runServiceGuardTick({
|
|
16
|
+
serviceValue: options.serviceValue,
|
|
17
|
+
serviceId,
|
|
18
|
+
nodeId,
|
|
19
|
+
loopTraceId,
|
|
20
|
+
logger,
|
|
21
|
+
logTopic: options.logTopic,
|
|
22
|
+
checkedAt,
|
|
23
|
+
nowMs,
|
|
24
|
+
taskNodeStaleMs: options.taskNodeStaleMs,
|
|
25
|
+
workerSpawnCooldownMs: options.workerSpawnCooldownMs,
|
|
26
|
+
maxNodes: options.maxNodes,
|
|
27
|
+
spawnWorker: options.spawnWorker,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (result.registryPatch != null) {
|
|
31
|
+
await options.onRegistryPatch(result.registryPatch);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!result.continueTick) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await options.onGuardPatch({
|
|
39
|
+
nodeId,
|
|
40
|
+
lastCheckAt: checkedAt,
|
|
41
|
+
lastDecision: result.decision,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (options.onDecision != null) {
|
|
45
|
+
await options.onDecision(result.decision);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface RegisterServiceGuardRunnerOptionsWithClock extends RegisterServiceGuardRunnerOptions {
|
|
50
|
+
/** Test hook — defaults to Date.now(). */
|
|
51
|
+
now?: () => number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Register the standard L3 guard runner (registry slot + TaskNode + Service runtime). */
|
|
55
|
+
export function registerServiceGuardRunner(options: RegisterServiceGuardRunnerOptionsWithClock): void {
|
|
56
|
+
TriggerManager.registerRunner({
|
|
57
|
+
key: options.runnerKey ?? SERVICE_GUARD_RUNNER_KEY,
|
|
58
|
+
intervalMs: options.intervalMs,
|
|
59
|
+
initialDelayMs: options.initialDelayMs,
|
|
60
|
+
fn: (ctx) => runServiceGuardRunner(ctx, options),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { SERVICE_GUARD_RUNNER_KEY };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { LoggerWithScope } from "../../shared/log";
|
|
2
|
+
import type { ReadinessCheck, ReadinessReport } from "../readiness/readiness.types";
|
|
3
|
+
import { runReadinessChecks } from "../readiness/run-readiness-checks";
|
|
4
|
+
|
|
5
|
+
export interface RunReadinessGateInput {
|
|
6
|
+
checks: ReadinessCheck[];
|
|
7
|
+
onReport: (report: ReadinessReport) => Promise<void>;
|
|
8
|
+
logger: LoggerWithScope;
|
|
9
|
+
logTopic?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Run readiness checks, persist report via callback, fail-fast when not ok. */
|
|
13
|
+
export async function runReadinessGate(input: RunReadinessGateInput): Promise<ReadinessReport> {
|
|
14
|
+
const { checks, onReport, logger, logTopic = "guard" } = input;
|
|
15
|
+
|
|
16
|
+
logger.info("Running readiness checks", { topic: logTopic });
|
|
17
|
+
const report = await runReadinessChecks(checks);
|
|
18
|
+
await onReport(report);
|
|
19
|
+
|
|
20
|
+
if (!report.ok) {
|
|
21
|
+
logger.error("Readiness checks failed", {
|
|
22
|
+
topic: logTopic,
|
|
23
|
+
data: { message: report.message, checks: report.checks },
|
|
24
|
+
});
|
|
25
|
+
throw new Error(report.message ?? "Readiness checks failed");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
logger.debug("Readiness checks passed", {
|
|
29
|
+
topic: logTopic,
|
|
30
|
+
data: { checks: report.checks.map((check) => check.name) },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return report;
|
|
34
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { CategoryNode, evaluateBusyNodeLiveness, type Node, patchServiceRuntime, scanTargetList } from "../../browser";
|
|
2
|
+
import { applyRegistrySlotGuardStep } from "../service-host/registry-slot-guard-step";
|
|
3
|
+
import type { ServiceGuardTickInput, ServiceGuardTickResult } from "./service-guard.interface";
|
|
4
|
+
import { getWorkerSpawnCooldownLastAt, markWorkerSpawned } from "./worker-spawn-cooldown";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One guard-runner tick: registry slot check, TaskNode liveness, optional worker
|
|
8
|
+
* respawn, Service runtime heartbeat rollup.
|
|
9
|
+
*/
|
|
10
|
+
export async function runServiceGuardTick(input: ServiceGuardTickInput): Promise<ServiceGuardTickResult> {
|
|
11
|
+
const {
|
|
12
|
+
serviceValue,
|
|
13
|
+
serviceId,
|
|
14
|
+
nodeId,
|
|
15
|
+
loopTraceId,
|
|
16
|
+
logger,
|
|
17
|
+
logTopic = "guard",
|
|
18
|
+
checkedAt,
|
|
19
|
+
nowMs,
|
|
20
|
+
taskNodeStaleMs,
|
|
21
|
+
workerSpawnCooldownMs,
|
|
22
|
+
maxNodes = 200,
|
|
23
|
+
spawnWorker,
|
|
24
|
+
getLastWorkerSpawnAt = getWorkerSpawnCooldownLastAt,
|
|
25
|
+
markWorkerSpawned: markWorkerSpawnedAt = markWorkerSpawned,
|
|
26
|
+
} = input;
|
|
27
|
+
|
|
28
|
+
const continueTick = true;
|
|
29
|
+
let registryPatch: ServiceGuardTickResult["registryPatch"];
|
|
30
|
+
let decision = "idle";
|
|
31
|
+
|
|
32
|
+
if (serviceId != null) {
|
|
33
|
+
const guard = await applyRegistrySlotGuardStep({
|
|
34
|
+
serviceValue,
|
|
35
|
+
serviceId,
|
|
36
|
+
checkedAt,
|
|
37
|
+
logger,
|
|
38
|
+
logTopic,
|
|
39
|
+
traceId: loopTraceId,
|
|
40
|
+
});
|
|
41
|
+
registryPatch = guard.registryPatch;
|
|
42
|
+
if (!guard.continueTick) {
|
|
43
|
+
return { continueTick: false, decision: "slot_lost", nodes: [], registryPatch };
|
|
44
|
+
}
|
|
45
|
+
decision = "slot_ok";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { data: nodes = [] } = await scanTargetList<Node>({
|
|
49
|
+
category: CategoryNode.NODE,
|
|
50
|
+
maxRows: maxNodes,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const liveness = evaluateBusyNodeLiveness(nodes, {
|
|
54
|
+
staleMs: taskNodeStaleMs,
|
|
55
|
+
now: nowMs,
|
|
56
|
+
excludeNodeId: nodeId,
|
|
57
|
+
onlyFreshCandidates: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const ageMs = liveness.freshest?.ageMs ?? Number.POSITIVE_INFINITY;
|
|
61
|
+
|
|
62
|
+
if (liveness.healthy) {
|
|
63
|
+
decision = "healthy";
|
|
64
|
+
} else if (nowMs - getLastWorkerSpawnAt() < workerSpawnCooldownMs) {
|
|
65
|
+
decision = "spawn_cooldown";
|
|
66
|
+
logger.warn("TaskNode unavailable but spawn cooldown active", {
|
|
67
|
+
topic: logTopic,
|
|
68
|
+
data: {
|
|
69
|
+
traceId: loopTraceId,
|
|
70
|
+
ageMs,
|
|
71
|
+
staleMs: taskNodeStaleMs,
|
|
72
|
+
cooldownMs: workerSpawnCooldownMs,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
decision = "spawn_worker";
|
|
77
|
+
logger.warn("TaskNode stale — restarting worker", {
|
|
78
|
+
topic: logTopic,
|
|
79
|
+
data: {
|
|
80
|
+
traceId: loopTraceId,
|
|
81
|
+
peerWorkerCount: liveness.busyWorkerCount,
|
|
82
|
+
freshPeerCount: liveness.freshWorkerCount,
|
|
83
|
+
staleMs: taskNodeStaleMs,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
await spawnWorker("guard:restart-unhealthy-tasknode");
|
|
87
|
+
markWorkerSpawnedAt(nowMs);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await patchServiceRuntime({
|
|
92
|
+
serviceValue,
|
|
93
|
+
serviceId: serviceId ?? undefined,
|
|
94
|
+
nodes,
|
|
95
|
+
lastHeartBeat: nowMs,
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
99
|
+
logger.warn("Failed to patch Service runtime rollup", {
|
|
100
|
+
topic: logTopic,
|
|
101
|
+
data: { traceId: loopTraceId, message },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { continueTick, decision, nodes, registryPatch };
|
|
106
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { LoggerWithScope } from "../../shared/log";
|
|
2
|
+
import { TriggerNode } from "../../trigger/trigger-node";
|
|
3
|
+
import { registerServiceGuardRunner } from "./register-service-guard-runner";
|
|
4
|
+
import { runReadinessGate } from "./run-readiness-gate";
|
|
5
|
+
import type { ServiceGuardNodeOptions } from "./service-guard.interface";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* L3 guard process TriggerNode: readiness gate, TaskNode worker spawn owner,
|
|
9
|
+
* and {@link registerServiceGuardRunner} tick (registry slot + liveness + runtime).
|
|
10
|
+
*
|
|
11
|
+
* Use {@link ServiceGuardNode.create} so the guard runner is registered before `start()`.
|
|
12
|
+
*/
|
|
13
|
+
class ServiceGuardNode extends TriggerNode {
|
|
14
|
+
private readonly options: ServiceGuardNodeOptions;
|
|
15
|
+
|
|
16
|
+
private constructor(options: ServiceGuardNodeOptions) {
|
|
17
|
+
super({
|
|
18
|
+
requireRunners: true,
|
|
19
|
+
beforeProcessExit: options.beforeProcessExit,
|
|
20
|
+
});
|
|
21
|
+
this.options = options;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Register the guard runner and return a ready-to-start node. */
|
|
25
|
+
static create(options: ServiceGuardNodeOptions): ServiceGuardNode {
|
|
26
|
+
registerServiceGuardRunner({
|
|
27
|
+
serviceValue: options.serviceValue,
|
|
28
|
+
logTopic: options.logTopic,
|
|
29
|
+
...options.guardRunner,
|
|
30
|
+
});
|
|
31
|
+
return new ServiceGuardNode(options);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected async onBeforeRegisterNode(nodeLogger: LoggerWithScope): Promise<void> {
|
|
35
|
+
await runReadinessGate({
|
|
36
|
+
checks: this.options.readinessChecks,
|
|
37
|
+
onReport: this.options.onReadinessReport,
|
|
38
|
+
logger: nodeLogger,
|
|
39
|
+
logTopic: this.options.logTopic,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await super.onBeforeRegisterNode(nodeLogger);
|
|
43
|
+
|
|
44
|
+
nodeLogger.info("Spawning TaskNode worker (single spawn owner)", {
|
|
45
|
+
topic: this.options.logTopic ?? "guard",
|
|
46
|
+
});
|
|
47
|
+
await this.options.spawnWorker("guard:readiness-passed");
|
|
48
|
+
this.options.onWorkerSpawned?.();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { ServiceGuardNode };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Node } from "../../node/node.interface";
|
|
2
|
+
import type { RegistrySlotRuntimeState } from "../../service/registry-lifecycle";
|
|
3
|
+
import type { LoggerWithScope } from "../../shared/log";
|
|
4
|
+
import type { TriggerNodeOptions } from "../../trigger/trigger.interface";
|
|
5
|
+
import type { ReadinessCheck, ReadinessReport } from "../readiness/readiness.types";
|
|
6
|
+
|
|
7
|
+
/** Default TriggerManager runner key for {@link registerServiceGuardRunner}. */
|
|
8
|
+
export const SERVICE_GUARD_RUNNER_KEY = "service-guard";
|
|
9
|
+
|
|
10
|
+
export interface ServiceGuardTickInput {
|
|
11
|
+
serviceValue: string;
|
|
12
|
+
serviceId: string | null;
|
|
13
|
+
nodeId: string;
|
|
14
|
+
loopTraceId: string;
|
|
15
|
+
logger: LoggerWithScope;
|
|
16
|
+
logTopic?: string;
|
|
17
|
+
checkedAt: string;
|
|
18
|
+
nowMs: number;
|
|
19
|
+
taskNodeStaleMs: number;
|
|
20
|
+
workerSpawnCooldownMs: number;
|
|
21
|
+
maxNodes?: number;
|
|
22
|
+
spawnWorker: (reason: string) => Promise<void>;
|
|
23
|
+
getLastWorkerSpawnAt?: () => number;
|
|
24
|
+
markWorkerSpawned?: (at: number) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ServiceGuardTickResult {
|
|
28
|
+
continueTick: boolean;
|
|
29
|
+
decision: string;
|
|
30
|
+
nodes: Node[];
|
|
31
|
+
registryPatch?: Pick<RegistrySlotRuntimeState, "slotOwned" | "lastSlotCheckAt" | "lastSlotCheckError">;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RegisterServiceGuardRunnerOptions {
|
|
35
|
+
serviceValue: string;
|
|
36
|
+
getServiceId: () => Promise<string | null>;
|
|
37
|
+
logTopic?: string;
|
|
38
|
+
runnerKey?: string;
|
|
39
|
+
intervalMs: number;
|
|
40
|
+
initialDelayMs?: number;
|
|
41
|
+
taskNodeStaleMs: number;
|
|
42
|
+
workerSpawnCooldownMs: number;
|
|
43
|
+
maxNodes?: number;
|
|
44
|
+
spawnWorker: (reason: string) => Promise<void>;
|
|
45
|
+
onRegistryPatch: (
|
|
46
|
+
patch: Pick<RegistrySlotRuntimeState, "slotOwned" | "lastSlotCheckAt" | "lastSlotCheckError">,
|
|
47
|
+
) => Promise<void>;
|
|
48
|
+
onGuardPatch: (patch: { nodeId: string; lastCheckAt: string; lastDecision?: string }) => Promise<void>;
|
|
49
|
+
onDecision?: (decision: string) => Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ServiceGuardNodeOptions extends TriggerNodeOptions {
|
|
53
|
+
serviceValue: string;
|
|
54
|
+
logTopic?: string;
|
|
55
|
+
readinessChecks: ReadinessCheck[];
|
|
56
|
+
onReadinessReport: (report: ReadinessReport) => Promise<void>;
|
|
57
|
+
guardRunner: Omit<
|
|
58
|
+
RegisterServiceGuardRunnerOptions,
|
|
59
|
+
"serviceValue" | "logTopic" | "runnerKey" | "intervalMs" | "initialDelayMs"
|
|
60
|
+
> &
|
|
61
|
+
Pick<RegisterServiceGuardRunnerOptions, "intervalMs" | "initialDelayMs">;
|
|
62
|
+
spawnWorker: (reason: string) => Promise<void>;
|
|
63
|
+
onWorkerSpawned?: () => void;
|
|
64
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
let lastWorkerSpawnAt = 0;
|
|
2
|
+
|
|
3
|
+
export function getWorkerSpawnCooldownLastAt(): number {
|
|
4
|
+
return lastWorkerSpawnAt;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function markWorkerSpawned(at = Date.now()): void {
|
|
8
|
+
lastWorkerSpawnAt = at;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resetWorkerSpawnCooldownForTests(): void {
|
|
12
|
+
lastWorkerSpawnAt = 0;
|
|
13
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
RegisterServiceGuardRunnerOptions,
|
|
3
|
+
RunReadinessGateInput,
|
|
4
|
+
ServiceGuardNodeOptions,
|
|
5
|
+
ServiceGuardTickInput,
|
|
6
|
+
ServiceGuardTickResult,
|
|
7
|
+
} from "../service-guard";
|
|
8
|
+
export {
|
|
9
|
+
getWorkerSpawnCooldownLastAt,
|
|
10
|
+
markWorkerSpawned,
|
|
11
|
+
registerServiceGuardRunner,
|
|
12
|
+
runReadinessGate,
|
|
13
|
+
runServiceGuardTick,
|
|
14
|
+
SERVICE_GUARD_RUNNER_KEY,
|
|
15
|
+
ServiceGuardNode,
|
|
16
|
+
} from "../service-guard";
|
|
17
|
+
export type {
|
|
18
|
+
ApplyRegistrySlotGuardInput,
|
|
19
|
+
ApplyRegistrySlotGuardResult,
|
|
20
|
+
} from "./registry-slot-guard-step";
|
|
21
|
+
export { applyRegistrySlotGuardStep } from "./registry-slot-guard-step";
|
|
22
|
+
export type {
|
|
23
|
+
ServiceHost,
|
|
24
|
+
ServiceHostClosable,
|
|
25
|
+
ServiceHostContext,
|
|
26
|
+
ServiceHostOptions,
|
|
27
|
+
} from "./service-host";
|
|
28
|
+
export { createServiceHost } from "./service-host";
|
|
29
|
+
export type { SingleProcessServiceContext, SingleProcessServiceOptions } from "./single-process-service";
|
|
30
|
+
export { runSingleProcessService } from "./single-process-service";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { registrySlotRuntimePatchFromGuardResult, runRegistrySlotGuardCheck } from "../../service/registry-lifecycle";
|
|
2
|
+
import type { LoggerWithScope } from "../../shared/log";
|
|
3
|
+
|
|
4
|
+
export interface ApplyRegistrySlotGuardInput {
|
|
5
|
+
serviceValue: string;
|
|
6
|
+
serviceId: string;
|
|
7
|
+
checkedAt: string;
|
|
8
|
+
logger: Pick<LoggerWithScope, "critical" | "warn">;
|
|
9
|
+
logTopic?: string;
|
|
10
|
+
traceId?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ApplyRegistrySlotGuardResult {
|
|
14
|
+
/** When false, caller should abort the supervisor tick (slot lost shutdown initiated). */
|
|
15
|
+
continueTick: boolean;
|
|
16
|
+
registryPatch: ReturnType<typeof registrySlotRuntimePatchFromGuardResult>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Run registry slot ownership check and optionally SIGTERM the parent main process.
|
|
21
|
+
* Used by guard / supervisor TriggerNode runners across L3 services.
|
|
22
|
+
*/
|
|
23
|
+
export async function applyRegistrySlotGuardStep(
|
|
24
|
+
input: ApplyRegistrySlotGuardInput,
|
|
25
|
+
): Promise<ApplyRegistrySlotGuardResult> {
|
|
26
|
+
const result = await runRegistrySlotGuardCheck({
|
|
27
|
+
serviceValue: input.serviceValue,
|
|
28
|
+
serviceId: input.serviceId,
|
|
29
|
+
});
|
|
30
|
+
const registryPatch = registrySlotRuntimePatchFromGuardResult(result, input.checkedAt);
|
|
31
|
+
|
|
32
|
+
if (result.shouldShutdown) {
|
|
33
|
+
input.logger.critical("Registry slot lost — shutting down service", {
|
|
34
|
+
topic: input.logTopic ?? "guard",
|
|
35
|
+
data: {
|
|
36
|
+
traceId: input.traceId,
|
|
37
|
+
serviceId: input.serviceId,
|
|
38
|
+
serviceValue: input.serviceValue,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
process.kill(process.ppid, "SIGTERM");
|
|
42
|
+
return { continueTick: false, registryPatch };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (result.error != null) {
|
|
46
|
+
input.logger.warn("Registry slot check failed (transient) — skipping enforcement this tick", {
|
|
47
|
+
topic: input.logTopic ?? "guard",
|
|
48
|
+
data: {
|
|
49
|
+
traceId: input.traceId,
|
|
50
|
+
serviceId: input.serviceId,
|
|
51
|
+
message: result.error,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { continueTick: true, registryPatch };
|
|
57
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { createLogger, type Service, ServiceRegistryError } from "../../browser";
|
|
2
|
+
import { claimServiceRegistrySlot, type ServiceRegistrySession } from "../../service/registry-lifecycle";
|
|
3
|
+
import type { LoggerWithScope } from "../../shared/log";
|
|
4
|
+
import type { ManagedChildProcesses } from "../process/managed-child-processes";
|
|
5
|
+
|
|
6
|
+
export interface ServiceHostClosable {
|
|
7
|
+
close: () => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ServiceHostContext {
|
|
11
|
+
service: Service;
|
|
12
|
+
session: ServiceRegistrySession;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ServiceHostOptions {
|
|
16
|
+
serviceValue: string;
|
|
17
|
+
logger?: LoggerWithScope;
|
|
18
|
+
logTopic?: string;
|
|
19
|
+
childProcesses?: ManagedChildProcesses;
|
|
20
|
+
/** Labels that trigger host shutdown when the child exits unexpectedly. */
|
|
21
|
+
criticalSupervisors?: readonly string[];
|
|
22
|
+
|
|
23
|
+
prepare?: () => Promise<void>;
|
|
24
|
+
createInstance: () => Promise<Service | { service: Service; baseUrl?: string }>;
|
|
25
|
+
onRegistryClaimed?: (ctx: ServiceHostContext) => Promise<void>;
|
|
26
|
+
startSupervisors?: () => void | Promise<void>;
|
|
27
|
+
waitUntilReady?: () => Promise<void>;
|
|
28
|
+
startServer?: () => Promise<ServiceHostClosable>;
|
|
29
|
+
/** Stop child processes and other local resources (not registry release). */
|
|
30
|
+
onShutdown?: (signal: string) => Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_CRITICAL_SUPERVISORS = ["guard", "supervisor"] as const;
|
|
34
|
+
|
|
35
|
+
export interface ServiceHost {
|
|
36
|
+
run: () => Promise<void>;
|
|
37
|
+
requestShutdown: (signal: string, exitCode?: number) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function unwrapService(created: Service | { service: Service; baseUrl?: string }): Service {
|
|
41
|
+
return typeof created === "object" && created != null && "service" in created ? created.service : created;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createServiceHost(options: ServiceHostOptions): ServiceHost {
|
|
45
|
+
const logger = options.logger ?? createLogger({ module: "service-host" });
|
|
46
|
+
const logTopic = options.logTopic ?? "startup";
|
|
47
|
+
const criticalSupervisors = options.criticalSupervisors ?? DEFAULT_CRITICAL_SUPERVISORS;
|
|
48
|
+
|
|
49
|
+
let session: ServiceRegistrySession | null = null;
|
|
50
|
+
let httpServer: ServiceHostClosable | null = null;
|
|
51
|
+
let shuttingDown = false;
|
|
52
|
+
|
|
53
|
+
async function shutdown(signal: string, exitCode = 0): Promise<void> {
|
|
54
|
+
if (shuttingDown) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
shuttingDown = true;
|
|
58
|
+
|
|
59
|
+
logger.info("shutting down", { topic: logTopic, data: { signal, exitCode } });
|
|
60
|
+
|
|
61
|
+
if (httpServer != null) {
|
|
62
|
+
await httpServer.close().catch(() => undefined);
|
|
63
|
+
httpServer = null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (options.onShutdown != null) {
|
|
67
|
+
await options.onShutdown(signal).catch(() => undefined);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (session != null) {
|
|
71
|
+
await session.release();
|
|
72
|
+
session = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
process.exit(exitCode);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function requestShutdown(signal: string, exitCode = 0): void {
|
|
79
|
+
void shutdown(signal, exitCode);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function installProcessHandlers(): void {
|
|
83
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
84
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
85
|
+
|
|
86
|
+
options.childProcesses?.setCriticalExitHandler({
|
|
87
|
+
labels: criticalSupervisors,
|
|
88
|
+
onCriticalExit: (label, code, signal) => {
|
|
89
|
+
if (shuttingDown) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
logger.critical("Critical supervisor exited — shutting down service", {
|
|
93
|
+
topic: logTopic,
|
|
94
|
+
data: { label, code, signal },
|
|
95
|
+
});
|
|
96
|
+
void shutdown("supervisor-exit", 1);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function run(): Promise<void> {
|
|
102
|
+
installProcessHandlers();
|
|
103
|
+
|
|
104
|
+
if (options.prepare != null) {
|
|
105
|
+
await options.prepare();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
session = await claimServiceRegistrySlot({
|
|
110
|
+
serviceValue: options.serviceValue,
|
|
111
|
+
createInstance: async () => unwrapService(await options.createInstance()),
|
|
112
|
+
});
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error instanceof ServiceRegistryError && error.code === "SERVICE_SLOTS_FULL") {
|
|
115
|
+
logger.critical("No EMPTY registry slot — refusing to start", {
|
|
116
|
+
topic: logTopic,
|
|
117
|
+
data: { serviceValue: options.serviceValue, code: error.code },
|
|
118
|
+
});
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (options.onRegistryClaimed != null) {
|
|
125
|
+
await options.onRegistryClaimed({ service: session.service, session });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (options.startSupervisors != null) {
|
|
129
|
+
await options.startSupervisors();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (options.waitUntilReady != null) {
|
|
133
|
+
await options.waitUntilReady();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (options.startServer != null) {
|
|
137
|
+
httpServer = await options.startServer();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function runWithFatalHandling(): Promise<void> {
|
|
142
|
+
try {
|
|
143
|
+
await run();
|
|
144
|
+
} catch (error: unknown) {
|
|
145
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
146
|
+
const code = error instanceof ServiceRegistryError ? error.code : undefined;
|
|
147
|
+
logger.error("fatal", { topic: logTopic, data: { message, code } });
|
|
148
|
+
|
|
149
|
+
if (httpServer != null) {
|
|
150
|
+
await httpServer.close().catch(() => undefined);
|
|
151
|
+
httpServer = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (options.onShutdown != null) {
|
|
155
|
+
await options.onShutdown("fatal").catch(() => undefined);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (session != null) {
|
|
159
|
+
await session.release().catch(() => undefined);
|
|
160
|
+
session = null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { run: runWithFatalHandling, requestShutdown };
|
|
168
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createLogger, type Service, ServiceRegistryError } from "../../browser";
|
|
2
|
+
import { claimServiceRegistrySlot, type ServiceRegistrySession } from "../../service/registry-lifecycle";
|
|
3
|
+
import type { LoggerWithScope } from "../../shared/log";
|
|
4
|
+
|
|
5
|
+
export interface SingleProcessServiceContext {
|
|
6
|
+
service: Service;
|
|
7
|
+
session: ServiceRegistrySession;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SingleProcessServiceOptions {
|
|
11
|
+
serviceValue: string;
|
|
12
|
+
logger?: LoggerWithScope;
|
|
13
|
+
logTopic?: string;
|
|
14
|
+
|
|
15
|
+
prepare?: () => Promise<void>;
|
|
16
|
+
createInstance: () => Promise<Service>;
|
|
17
|
+
run: (ctx: SingleProcessServiceContext) => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Single-process L3 entry (e.g. log-service): claim registry slot then run the service body.
|
|
22
|
+
* Registry release is the caller's responsibility inside `run` (e.g. TriggerNode `beforeProcessExit`).
|
|
23
|
+
*/
|
|
24
|
+
export async function runSingleProcessService(options: SingleProcessServiceOptions): Promise<void> {
|
|
25
|
+
const logger = options.logger ?? createLogger({ module: "single-process-service" });
|
|
26
|
+
const logTopic = options.logTopic ?? "startup";
|
|
27
|
+
|
|
28
|
+
if (options.prepare != null) {
|
|
29
|
+
await options.prepare();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let session: ServiceRegistrySession | null = null;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
session = await claimServiceRegistrySlot({
|
|
36
|
+
serviceValue: options.serviceValue,
|
|
37
|
+
createInstance: options.createInstance,
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error instanceof ServiceRegistryError && error.code === "SERVICE_SLOTS_FULL") {
|
|
41
|
+
logger.critical("No EMPTY registry slot — refusing to start", {
|
|
42
|
+
topic: logTopic,
|
|
43
|
+
data: { serviceValue: options.serviceValue, code: error.code },
|
|
44
|
+
});
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await options.run({ service: session.service, session });
|
|
52
|
+
} catch (error: unknown) {
|
|
53
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
+
logger.error("fatal", { topic: logTopic, data: { message } });
|
|
55
|
+
await session.release().catch(() => undefined);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|