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,157 @@
|
|
|
1
|
+
import { mkdir, readFile, stat, unlink } from "node:fs/promises";
|
|
2
|
+
import { createJsonFileStateStore, type JsonStatePatch } from "./json-state-store";
|
|
3
|
+
import { resolveRuntimeStatePaths, runtimeStateShardPath } from "./runtime-state-path";
|
|
4
|
+
|
|
5
|
+
export interface CreateShardedJsonFileStateStoreOptions<T extends object, NK extends keyof T> {
|
|
6
|
+
/** Monolithic legacy path or shard directory root (see {@link resolveRuntimeStatePaths}). */
|
|
7
|
+
filePath: string;
|
|
8
|
+
defaultState: T;
|
|
9
|
+
/** Top-level keys persisted as separate shard files. */
|
|
10
|
+
nestedKeys: NK[];
|
|
11
|
+
updatedAtKey?: keyof T;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
15
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function mergeSlice<T extends object>(defaults: T, key: keyof T, patchValue: unknown): T[keyof T] {
|
|
19
|
+
const base = defaults[key];
|
|
20
|
+
if (isPlainObject(base) && isPlainObject(patchValue)) {
|
|
21
|
+
return { ...base, ...patchValue } as T[keyof T];
|
|
22
|
+
}
|
|
23
|
+
return patchValue as T[keyof T];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* JSON runtime state with **one file per top-level slice** (log-persist registry pattern).
|
|
28
|
+
* Eliminates cross-slice lost updates when guard/scheduler/worker write concurrently.
|
|
29
|
+
*/
|
|
30
|
+
export function createShardedJsonFileStateStore<T extends object, NK extends keyof T>(
|
|
31
|
+
options: CreateShardedJsonFileStateStoreOptions<T, NK>,
|
|
32
|
+
): {
|
|
33
|
+
read(): Promise<T>;
|
|
34
|
+
write(patch: JsonStatePatch<T, NK>): Promise<T>;
|
|
35
|
+
reset(): Promise<T>;
|
|
36
|
+
} {
|
|
37
|
+
const { defaultState, nestedKeys, updatedAtKey } = options;
|
|
38
|
+
const { stateDir, legacyFilePath } = resolveRuntimeStatePaths(options.filePath);
|
|
39
|
+
|
|
40
|
+
type ShardStore = ReturnType<typeof createJsonFileStateStore<object>>;
|
|
41
|
+
const shardStores = new Map<string, ShardStore>();
|
|
42
|
+
|
|
43
|
+
function shardStore(sliceKey: NK): ShardStore {
|
|
44
|
+
const key = String(sliceKey);
|
|
45
|
+
let store = shardStores.get(key);
|
|
46
|
+
if (store == null) {
|
|
47
|
+
store = createJsonFileStateStore({
|
|
48
|
+
filePath: runtimeStateShardPath(stateDir, key),
|
|
49
|
+
defaultState: defaultState[sliceKey] as object,
|
|
50
|
+
});
|
|
51
|
+
shardStores.set(key, store);
|
|
52
|
+
}
|
|
53
|
+
return store;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function readUpdatedAtFromShardMtimes(): Promise<string> {
|
|
57
|
+
let maxMtimeMs = 0;
|
|
58
|
+
for (const sliceKey of nestedKeys) {
|
|
59
|
+
try {
|
|
60
|
+
const shardStat = await stat(runtimeStateShardPath(stateDir, String(sliceKey)));
|
|
61
|
+
maxMtimeMs = Math.max(maxMtimeMs, shardStat.mtimeMs);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (maxMtimeMs <= 0) {
|
|
69
|
+
return defaultState[updatedAtKey as keyof T] as string;
|
|
70
|
+
}
|
|
71
|
+
return new Date(maxMtimeMs).toISOString();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function migrateLegacyMonolithicIfPresent(): Promise<void> {
|
|
75
|
+
if (legacyFilePath == null) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let raw: string;
|
|
80
|
+
try {
|
|
81
|
+
raw = await readFile(legacyFilePath, "utf8");
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let parsed: Partial<T>;
|
|
90
|
+
try {
|
|
91
|
+
parsed = JSON.parse(raw) as Partial<T>;
|
|
92
|
+
} catch {
|
|
93
|
+
await unlink(legacyFilePath).catch(() => undefined);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await mkdir(stateDir, { recursive: true });
|
|
98
|
+
for (const sliceKey of nestedKeys) {
|
|
99
|
+
const slice = parsed[sliceKey];
|
|
100
|
+
if (slice != null) {
|
|
101
|
+
await shardStore(sliceKey).write(slice as object);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await unlink(legacyFilePath).catch(() => undefined);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
async read(): Promise<T> {
|
|
110
|
+
await mkdir(stateDir, { recursive: true });
|
|
111
|
+
await migrateLegacyMonolithicIfPresent();
|
|
112
|
+
|
|
113
|
+
const merged = { ...defaultState } as T;
|
|
114
|
+
for (const sliceKey of nestedKeys) {
|
|
115
|
+
merged[sliceKey] = (await shardStore(sliceKey).read()) as T[NK];
|
|
116
|
+
}
|
|
117
|
+
if (updatedAtKey != null) {
|
|
118
|
+
merged[updatedAtKey] = (await readUpdatedAtFromShardMtimes()) as T[keyof T];
|
|
119
|
+
}
|
|
120
|
+
return merged;
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
async write(patch: JsonStatePatch<T, NK>): Promise<T> {
|
|
124
|
+
await mkdir(stateDir, { recursive: true });
|
|
125
|
+
await migrateLegacyMonolithicIfPresent();
|
|
126
|
+
|
|
127
|
+
for (const sliceKey of nestedKeys) {
|
|
128
|
+
const patchSlice = patch[sliceKey];
|
|
129
|
+
if (patchSlice == null) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const current = await shardStore(sliceKey).read();
|
|
133
|
+
const nextSlice = mergeSlice(defaultState, sliceKey, {
|
|
134
|
+
...(current as object),
|
|
135
|
+
...(patchSlice as object),
|
|
136
|
+
});
|
|
137
|
+
await shardStore(sliceKey).write(nextSlice as object);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return this.read();
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
async reset(): Promise<T> {
|
|
144
|
+
await mkdir(stateDir, { recursive: true });
|
|
145
|
+
|
|
146
|
+
for (const sliceKey of nestedKeys) {
|
|
147
|
+
await shardStore(sliceKey).reset();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (legacyFilePath != null) {
|
|
151
|
+
await unlink(legacyFilePath).catch(() => undefined);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return this.read();
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -21,6 +21,16 @@ export interface NodeLoopContext {
|
|
|
21
21
|
nodeId: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** Shared options for {@link BaseNodeRuntime} and subclasses (e.g. TriggerNode). */
|
|
25
|
+
export interface BaseNodeRuntimeOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Called after node logout (or when the node was never registered),
|
|
28
|
+
* immediately before `process.exit`. Use for best-effort cleanup such as
|
|
29
|
+
* {@link unregisterServiceAtShutdown}. Errors are logged and ignored.
|
|
30
|
+
*/
|
|
31
|
+
beforeProcessExit?: () => void | Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
export const LOG_TOPIC_NODE = "node";
|
|
25
35
|
const LOG_TOPIC_COMMAND = "command";
|
|
26
36
|
const LOG_TOPIC_PROCESS = "process";
|
|
@@ -35,6 +45,7 @@ abstract class BaseNodeRuntime {
|
|
|
35
45
|
private static readonly NODE_ID_PENDING_ASSIGNMENT = "pending-node-id";
|
|
36
46
|
|
|
37
47
|
protected readonly runtimeModule: string;
|
|
48
|
+
private readonly beforeProcessExitHook: (() => void | Promise<void>) | null;
|
|
38
49
|
|
|
39
50
|
private _localNodeId: string | null = null;
|
|
40
51
|
private _isNodeIdLocked = false;
|
|
@@ -63,14 +74,34 @@ abstract class BaseNodeRuntime {
|
|
|
63
74
|
this._isNodeIdLocked = true;
|
|
64
75
|
}
|
|
65
76
|
|
|
66
|
-
protected constructor(runtimeModule: string) {
|
|
77
|
+
protected constructor(runtimeModule: string, options?: BaseNodeRuntimeOptions) {
|
|
67
78
|
this.runtimeModule = runtimeModule;
|
|
79
|
+
this.beforeProcessExitHook = options?.beforeProcessExit ?? null;
|
|
68
80
|
this.isRunning = false;
|
|
69
81
|
this.loopCount = 0;
|
|
70
82
|
this.startupTraceId = logManager.generateTraceId();
|
|
71
83
|
this.startupScope = createScope({ module: runtimeModule, traceId: this.startupTraceId });
|
|
72
84
|
}
|
|
73
85
|
|
|
86
|
+
private async runBeforeProcessExit(logger: LoggerWithScope): Promise<void> {
|
|
87
|
+
if (this.beforeProcessExitHook == null) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
await this.beforeProcessExitHook();
|
|
92
|
+
} catch (error) {
|
|
93
|
+
logger.warn("beforeProcessExit 钩子失败(best-effort)", {
|
|
94
|
+
topic: LOG_TOPIC_PROCESS,
|
|
95
|
+
data: { error: getErrorMessage(error) },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private async exitProcess(code: number, logger: LoggerWithScope): Promise<never> {
|
|
101
|
+
await this.runBeforeProcessExit(logger);
|
|
102
|
+
process.exit(code);
|
|
103
|
+
}
|
|
104
|
+
|
|
74
105
|
/** Hook: task registration, trigger config load, etc. Throw to abort bootstrap. */
|
|
75
106
|
protected abstract onBeforeRegisterNode(logger: LoggerWithScope): Promise<void>;
|
|
76
107
|
|
|
@@ -296,7 +327,7 @@ abstract class BaseNodeRuntime {
|
|
|
296
327
|
}
|
|
297
328
|
|
|
298
329
|
this.isRunning = false;
|
|
299
|
-
this.shutdownPromise = this.runShutdown(options?.trigger).catch((error) => {
|
|
330
|
+
this.shutdownPromise = this.runShutdown(options?.trigger).catch(async (error) => {
|
|
300
331
|
const logger = createLogger({
|
|
301
332
|
scope: patchScope({ scope: this.startupScope, patch: { labels: { nodeId: this.getLogNodeId() } } }),
|
|
302
333
|
});
|
|
@@ -307,7 +338,7 @@ abstract class BaseNodeRuntime {
|
|
|
307
338
|
error: getErrorMessage(error),
|
|
308
339
|
},
|
|
309
340
|
});
|
|
310
|
-
|
|
341
|
+
await this.exitProcess(1, logger);
|
|
311
342
|
});
|
|
312
343
|
return this.shutdownPromise;
|
|
313
344
|
}
|
|
@@ -324,7 +355,7 @@ abstract class BaseNodeRuntime {
|
|
|
324
355
|
|
|
325
356
|
if (this.localNodeId == null) {
|
|
326
357
|
logger.warn("本地節點未註冊,進程退出", { topic: LOG_TOPIC_NODE });
|
|
327
|
-
|
|
358
|
+
await this.exitProcess(0, logger);
|
|
328
359
|
return;
|
|
329
360
|
}
|
|
330
361
|
|
|
@@ -344,7 +375,7 @@ abstract class BaseNodeRuntime {
|
|
|
344
375
|
data: { error: getErrorMessage(error) },
|
|
345
376
|
});
|
|
346
377
|
} finally {
|
|
347
|
-
|
|
378
|
+
await this.exitProcess(0, logger);
|
|
348
379
|
}
|
|
349
380
|
}
|
|
350
381
|
|
|
@@ -391,7 +422,7 @@ abstract class BaseNodeRuntime {
|
|
|
391
422
|
}
|
|
392
423
|
|
|
393
424
|
this.consecutiveHeartbeatFailures = 0;
|
|
394
|
-
logger.
|
|
425
|
+
logger.info("心跳更新成功", {
|
|
395
426
|
topic: LOG_TOPIC_NODE,
|
|
396
427
|
data: { lastHeartBeat: formatHeartbeat(lastHeartBeat) },
|
|
397
428
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createLogger } from "../../shared/log/create-logger";
|
|
2
|
+
import type { ManagedChildProcessesOptions } from "./managed-child-processes";
|
|
3
|
+
import { ManagedChildProcesses } from "./managed-child-processes";
|
|
4
|
+
|
|
5
|
+
export type CreateManagedChildProcessesOptions = Pick<
|
|
6
|
+
ManagedChildProcessesOptions,
|
|
7
|
+
"projectRoot" | "preloadModules" | "useTsx" | "graceMs"
|
|
8
|
+
> & {
|
|
9
|
+
/** Defaults to `createLogger({ module: "launcher" })`. */
|
|
10
|
+
logger?: ManagedChildProcessesOptions["logger"];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const DEFAULT_PRELOAD_MODULES = ["./scripts/preload.mjs"] as const;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Standard L3 service child-process registry: node (not tsx), service preload, launcher logger.
|
|
17
|
+
* Each service keeps a singleton in `src/startup/child-processes.ts`.
|
|
18
|
+
*/
|
|
19
|
+
export function createManagedChildProcesses(options: CreateManagedChildProcessesOptions): ManagedChildProcesses {
|
|
20
|
+
return new ManagedChildProcesses({
|
|
21
|
+
projectRoot: options.projectRoot,
|
|
22
|
+
preloadModules: options.preloadModules ?? DEFAULT_PRELOAD_MODULES,
|
|
23
|
+
useTsx: options.useTsx ?? false,
|
|
24
|
+
graceMs: options.graceMs,
|
|
25
|
+
logger: options.logger ?? createLogger({ module: "launcher" }),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
export type { CreateManagedChildProcessesOptions } from "./create-managed-child-processes";
|
|
2
|
+
export { createManagedChildProcesses } from "./create-managed-child-processes";
|
|
1
3
|
export type {
|
|
4
|
+
CriticalExitHandler,
|
|
2
5
|
ManagedChildProcessesOptions,
|
|
6
|
+
SpawnChildOptions,
|
|
3
7
|
SpawnChildResult,
|
|
4
8
|
StopAllChildrenOptions,
|
|
5
9
|
} from "./managed-child-processes";
|
|
@@ -8,7 +8,13 @@ export interface ManagedChildProcessesOptions {
|
|
|
8
8
|
preloadModules?: readonly string[];
|
|
9
9
|
/** SIGTERM -> wait -> SIGKILL delay (default 5000 ms). */
|
|
10
10
|
graceMs?: number;
|
|
11
|
-
logger?: Pick<LoggerWithScope, "info" | "warn" | "error">;
|
|
11
|
+
logger?: Pick<LoggerWithScope, "info" | "warn" | "error" | "critical">;
|
|
12
|
+
useTsx?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SpawnChildOptions {
|
|
16
|
+
env?: Record<string, string>;
|
|
17
|
+
useTsx?: boolean;
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
export interface SpawnChildResult {
|
|
@@ -22,6 +28,11 @@ export interface StopAllChildrenOptions {
|
|
|
22
28
|
extraPids?: readonly number[];
|
|
23
29
|
}
|
|
24
30
|
|
|
31
|
+
export interface CriticalExitHandler {
|
|
32
|
+
labels: readonly string[];
|
|
33
|
+
onCriticalExit: (label: string, code: number | null, signal: NodeJS.Signals | null) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
/**
|
|
26
37
|
* Process-local registry for tsx child processes (supervisor, worker, etc.).
|
|
27
38
|
* Each OS process has its own instance - do not expect cross-process PID tracking.
|
|
@@ -30,16 +41,24 @@ export class ManagedChildProcesses {
|
|
|
30
41
|
private readonly projectRoot: string;
|
|
31
42
|
private readonly preloadModules: readonly string[];
|
|
32
43
|
private readonly graceMs: number;
|
|
33
|
-
private readonly
|
|
44
|
+
private readonly defaultUseTsx: boolean;
|
|
45
|
+
private readonly logger?: Pick<LoggerWithScope, "info" | "warn" | "error" | "critical">;
|
|
34
46
|
private readonly byLabel = new Map<string, ChildProcess>();
|
|
47
|
+
private criticalExitHandler: CriticalExitHandler | null = null;
|
|
48
|
+
private suppressCriticalExit = false;
|
|
35
49
|
|
|
36
50
|
constructor(options: ManagedChildProcessesOptions) {
|
|
37
51
|
this.projectRoot = options.projectRoot;
|
|
38
52
|
this.preloadModules = options.preloadModules ?? [];
|
|
39
53
|
this.graceMs = options.graceMs ?? 5_000;
|
|
54
|
+
this.defaultUseTsx = options.useTsx ?? true;
|
|
40
55
|
this.logger = options.logger;
|
|
41
56
|
}
|
|
42
57
|
|
|
58
|
+
setCriticalExitHandler(handler: CriticalExitHandler): void {
|
|
59
|
+
this.criticalExitHandler = handler;
|
|
60
|
+
}
|
|
61
|
+
|
|
43
62
|
get(label: string): ChildProcess | null {
|
|
44
63
|
return this.byLabel.get(label) ?? null;
|
|
45
64
|
}
|
|
@@ -55,7 +74,7 @@ export class ManagedChildProcesses {
|
|
|
55
74
|
.map(([label]) => label);
|
|
56
75
|
}
|
|
57
76
|
|
|
58
|
-
spawn(label: string, entryScript: string): SpawnChildResult {
|
|
77
|
+
spawn(label: string, entryScript: string, spawnOptions?: SpawnChildOptions): SpawnChildResult {
|
|
59
78
|
const existing = this.getRunning(label);
|
|
60
79
|
if (existing != null) {
|
|
61
80
|
return { child: existing, created: false };
|
|
@@ -65,14 +84,23 @@ export class ManagedChildProcesses {
|
|
|
65
84
|
projectRoot: this.projectRoot,
|
|
66
85
|
entryScript,
|
|
67
86
|
preloadModules: this.preloadModules,
|
|
87
|
+
useTsx: spawnOptions?.useTsx ?? this.defaultUseTsx,
|
|
88
|
+
env: spawnOptions?.env != null ? { ...process.env, ...spawnOptions.env } : process.env,
|
|
68
89
|
});
|
|
69
90
|
|
|
70
|
-
child.on("exit", (code, signal) => {
|
|
91
|
+
child.on("exit", (code: number | null, signal: NodeJS.Signals | null) => {
|
|
71
92
|
this.logger?.info("child exited", { topic: "process", data: { label, code, signal } });
|
|
72
93
|
const current = this.byLabel.get(label);
|
|
73
94
|
if (current === child) {
|
|
74
95
|
this.byLabel.delete(label);
|
|
75
96
|
}
|
|
97
|
+
if (
|
|
98
|
+
!this.suppressCriticalExit &&
|
|
99
|
+
this.criticalExitHandler != null &&
|
|
100
|
+
this.criticalExitHandler.labels.includes(label)
|
|
101
|
+
) {
|
|
102
|
+
this.criticalExitHandler.onCriticalExit(label, code, signal);
|
|
103
|
+
}
|
|
76
104
|
});
|
|
77
105
|
|
|
78
106
|
this.byLabel.set(label, child);
|
|
@@ -90,20 +118,25 @@ export class ManagedChildProcesses {
|
|
|
90
118
|
}
|
|
91
119
|
|
|
92
120
|
async stopAll(options?: StopAllChildrenOptions): Promise<void> {
|
|
93
|
-
|
|
121
|
+
this.suppressCriticalExit = true;
|
|
122
|
+
try {
|
|
123
|
+
const tracked = [...this.byLabel.values()].filter((child) => isChildProcessRunning(child));
|
|
94
124
|
|
|
95
|
-
|
|
125
|
+
await Promise.all(tracked.map((child) => this.stopChild(child)));
|
|
96
126
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
127
|
+
const extraPids = options?.extraPids ?? [];
|
|
128
|
+
for (const pid of extraPids) {
|
|
129
|
+
if (pid == null || tracked.some((child) => child.pid === pid)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
process.kill(pid, "SIGTERM");
|
|
134
|
+
} catch {
|
|
135
|
+
// already exited
|
|
136
|
+
}
|
|
106
137
|
}
|
|
138
|
+
} finally {
|
|
139
|
+
this.suppressCriticalExit = false;
|
|
107
140
|
}
|
|
108
141
|
}
|
|
109
142
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { EMPTY_REGISTRY_SLOT_RUNTIME_STATE } from "../../service/registry-lifecycle";
|
|
2
|
+
import type { JsonStatePatch } from "../fs/json-state-store";
|
|
3
|
+
import { createShardedJsonFileStateStore } from "../fs/sharded-json-state-store";
|
|
4
|
+
import type {
|
|
5
|
+
DefaultExtraRuntimeSlices,
|
|
6
|
+
GuardRuntimeSlice,
|
|
7
|
+
ReadinessRuntimeSlice,
|
|
8
|
+
SchedulerRuntimeSlice,
|
|
9
|
+
ServiceRuntimeExtraSlicePatch,
|
|
10
|
+
ServiceRuntimeNestedKeys,
|
|
11
|
+
ServiceRuntimeState,
|
|
12
|
+
WorkerRuntimeSlice,
|
|
13
|
+
} from "./service-runtime-state.types";
|
|
14
|
+
|
|
15
|
+
const EPOCH_ISO = new Date(0).toISOString();
|
|
16
|
+
|
|
17
|
+
export interface CreateServiceRuntimeStateStoreOptions<
|
|
18
|
+
TExtraSlices extends Record<string, object> = DefaultExtraRuntimeSlices,
|
|
19
|
+
> {
|
|
20
|
+
/**
|
|
21
|
+
* Monolithic legacy path (`…/state.json`) or shard directory (`…/runtime-state/`).
|
|
22
|
+
* Legacy files migrate automatically to `dirname/state.json → runtime-state/*.json`.
|
|
23
|
+
*/
|
|
24
|
+
filePath: string;
|
|
25
|
+
/** Default values for service-specific top-level nested slices. */
|
|
26
|
+
extraDefaults?: TExtraSlices;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface FinishRunnerTickOptions<TExtraSlices extends Record<string, object> = DefaultExtraRuntimeSlices> {
|
|
30
|
+
nowMs: number;
|
|
31
|
+
lastFired: Record<string, string>;
|
|
32
|
+
postedCount?: number;
|
|
33
|
+
/** Patch extra slices in the same atomic write as the scheduler tick. */
|
|
34
|
+
extra?: ServiceRuntimeExtraSlicePatch<TExtraSlices>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ServiceRuntimeStateStore<TExtraSlices extends Record<string, object> = DefaultExtraRuntimeSlices> {
|
|
38
|
+
readRuntimeState(): Promise<ServiceRuntimeState<TExtraSlices>>;
|
|
39
|
+
writeRuntimeState(
|
|
40
|
+
patch: JsonStatePatch<ServiceRuntimeState<TExtraSlices>, ServiceRuntimeNestedKeys<TExtraSlices>>,
|
|
41
|
+
): Promise<ServiceRuntimeState<TExtraSlices>>;
|
|
42
|
+
resetRuntimeStateForStartup(): Promise<ServiceRuntimeState<TExtraSlices>>;
|
|
43
|
+
finishRunnerTick(options: FinishRunnerTickOptions<TExtraSlices>): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createDefaultServiceRuntimeState<TExtraSlices extends Record<string, object>>(options: {
|
|
47
|
+
extraDefaults?: TExtraSlices;
|
|
48
|
+
}): ServiceRuntimeState<TExtraSlices> {
|
|
49
|
+
const readiness: ReadinessRuntimeSlice = {
|
|
50
|
+
status: "pending",
|
|
51
|
+
checkedAt: null,
|
|
52
|
+
message: null,
|
|
53
|
+
checks: [],
|
|
54
|
+
};
|
|
55
|
+
const guard: GuardRuntimeSlice = {
|
|
56
|
+
nodeId: null,
|
|
57
|
+
lastCheckAt: null,
|
|
58
|
+
lastDecision: null,
|
|
59
|
+
lastSpawnAt: null,
|
|
60
|
+
spawnCount: 0,
|
|
61
|
+
};
|
|
62
|
+
const scheduler: SchedulerRuntimeSlice = {
|
|
63
|
+
lastTickAt: null,
|
|
64
|
+
lastPostedCount: 0,
|
|
65
|
+
lastFired: {},
|
|
66
|
+
};
|
|
67
|
+
const worker: WorkerRuntimeSlice = {
|
|
68
|
+
pid: null,
|
|
69
|
+
spawnedAt: null,
|
|
70
|
+
ready: false,
|
|
71
|
+
registeredTasks: [],
|
|
72
|
+
readyAt: null,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
updatedAt: EPOCH_ISO,
|
|
77
|
+
readiness,
|
|
78
|
+
guard,
|
|
79
|
+
scheduler,
|
|
80
|
+
worker,
|
|
81
|
+
registry: { ...EMPTY_REGISTRY_SLOT_RUNTIME_STATE },
|
|
82
|
+
...(options.extraDefaults ?? ({} as TExtraSlices)),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Sharded JSON runtime state (L3 blueprint) — one file per slice under `runtime-state/`. */
|
|
87
|
+
export function createServiceRuntimeStateStore<TExtraSlices extends Record<string, object> = DefaultExtraRuntimeSlices>(
|
|
88
|
+
options: CreateServiceRuntimeStateStoreOptions<TExtraSlices>,
|
|
89
|
+
): ServiceRuntimeStateStore<TExtraSlices> {
|
|
90
|
+
type State = ServiceRuntimeState<TExtraSlices>;
|
|
91
|
+
type NK = ServiceRuntimeNestedKeys<TExtraSlices>;
|
|
92
|
+
|
|
93
|
+
const defaultState = createDefaultServiceRuntimeState(options);
|
|
94
|
+
const nestedKeys = [
|
|
95
|
+
"readiness",
|
|
96
|
+
"guard",
|
|
97
|
+
"scheduler",
|
|
98
|
+
"worker",
|
|
99
|
+
"registry",
|
|
100
|
+
...(Object.keys(options.extraDefaults ?? {}) as (keyof TExtraSlices & string)[]),
|
|
101
|
+
] as NK[];
|
|
102
|
+
|
|
103
|
+
const store = createShardedJsonFileStateStore<State, NK>({
|
|
104
|
+
filePath: options.filePath,
|
|
105
|
+
defaultState,
|
|
106
|
+
nestedKeys,
|
|
107
|
+
updatedAtKey: "updatedAt",
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
readRuntimeState: () => store.read(),
|
|
112
|
+
writeRuntimeState: (patch) => store.write(patch),
|
|
113
|
+
resetRuntimeStateForStartup: () => store.reset(),
|
|
114
|
+
finishRunnerTick: async ({ nowMs, lastFired, postedCount, extra }) => {
|
|
115
|
+
await store.write({
|
|
116
|
+
scheduler: {
|
|
117
|
+
lastTickAt: new Date(nowMs).toISOString(),
|
|
118
|
+
lastFired,
|
|
119
|
+
...(postedCount != null ? { lastPostedCount: postedCount } : {}),
|
|
120
|
+
},
|
|
121
|
+
...extra,
|
|
122
|
+
} as JsonStatePatch<State, NK>);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
CreateServiceRuntimeStateStoreOptions,
|
|
3
|
+
FinishRunnerTickOptions,
|
|
4
|
+
ServiceRuntimeStateStore,
|
|
5
|
+
} from "./create-service-runtime-state-store";
|
|
6
|
+
export { createServiceRuntimeStateStore } from "./create-service-runtime-state-store";
|
|
7
|
+
export type {
|
|
8
|
+
GuardRuntimeSlice,
|
|
9
|
+
ReadinessRuntimeSlice,
|
|
10
|
+
ReadinessStatus,
|
|
11
|
+
SchedulerRuntimeSlice,
|
|
12
|
+
ServiceRuntimeCoreNestedKey,
|
|
13
|
+
ServiceRuntimeExtraSlicePatch,
|
|
14
|
+
ServiceRuntimeNestedKeys,
|
|
15
|
+
ServiceRuntimeState,
|
|
16
|
+
WorkerRuntimeSlice,
|
|
17
|
+
} from "./service-runtime-state.types";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { RegistrySlotRuntimeState } from "../../service/registry-lifecycle";
|
|
2
|
+
|
|
3
|
+
export type ReadinessStatus = "pending" | "passed" | "failed";
|
|
4
|
+
|
|
5
|
+
export interface ReadinessRuntimeSlice {
|
|
6
|
+
status: ReadinessStatus;
|
|
7
|
+
checkedAt: string | null;
|
|
8
|
+
message: string | null;
|
|
9
|
+
checks: { name: string; ok: boolean; detail?: string }[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GuardRuntimeSlice {
|
|
13
|
+
nodeId: string | null;
|
|
14
|
+
lastCheckAt: string | null;
|
|
15
|
+
lastDecision: string | null;
|
|
16
|
+
lastSpawnAt: string | null;
|
|
17
|
+
spawnCount: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SchedulerRuntimeSlice {
|
|
21
|
+
lastTickAt: string | null;
|
|
22
|
+
lastPostedCount: number;
|
|
23
|
+
lastFired: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface WorkerRuntimeSlice {
|
|
27
|
+
pid: number | null;
|
|
28
|
+
spawnedAt: string | null;
|
|
29
|
+
ready: boolean;
|
|
30
|
+
registeredTasks: string[];
|
|
31
|
+
readyAt: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Default when an L3 service has no service-specific runtime state slices.
|
|
36
|
+
* Use an empty object type (not `Record<string, never>`) so intersecting with core
|
|
37
|
+
* slices does not add a string index signature that collapses property types to `never`.
|
|
38
|
+
*/
|
|
39
|
+
export type DefaultExtraRuntimeSlices = Record<never, never>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Cross-process service runtime state persisted to `state.json`.
|
|
43
|
+
* L3 blueprint: readiness + guard + scheduler + worker + registry.
|
|
44
|
+
* Extend via extra top-level slices only.
|
|
45
|
+
*/
|
|
46
|
+
export type ServiceRuntimeState<TExtraSlices extends Record<string, object> = DefaultExtraRuntimeSlices> = {
|
|
47
|
+
updatedAt: string;
|
|
48
|
+
readiness: ReadinessRuntimeSlice;
|
|
49
|
+
guard: GuardRuntimeSlice;
|
|
50
|
+
scheduler: SchedulerRuntimeSlice;
|
|
51
|
+
worker: WorkerRuntimeSlice;
|
|
52
|
+
registry: RegistrySlotRuntimeState;
|
|
53
|
+
} & TExtraSlices;
|
|
54
|
+
|
|
55
|
+
export type ServiceRuntimeCoreNestedKey = "readiness" | "guard" | "scheduler" | "worker" | "registry";
|
|
56
|
+
|
|
57
|
+
export type ServiceRuntimeNestedKeys<TExtraSlices extends Record<string, object>> =
|
|
58
|
+
| ServiceRuntimeCoreNestedKey
|
|
59
|
+
| (keyof TExtraSlices & string);
|
|
60
|
+
|
|
61
|
+
/** Patch shape for extra top-level slices (one level deep). */
|
|
62
|
+
export type ServiceRuntimeExtraSlicePatch<TExtraSlices extends Record<string, object>> = {
|
|
63
|
+
[K in keyof TExtraSlices]?: TExtraSlices[K] extends object ? Partial<TExtraSlices[K]> : TExtraSlices[K];
|
|
64
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { registerServiceGuardRunner } from "./register-service-guard-runner";
|
|
2
|
+
export type { RunReadinessGateInput } from "./run-readiness-gate";
|
|
3
|
+
export { runReadinessGate } from "./run-readiness-gate";
|
|
4
|
+
export { runServiceGuardTick } from "./run-service-guard-tick";
|
|
5
|
+
export type {
|
|
6
|
+
RegisterServiceGuardRunnerOptions,
|
|
7
|
+
ServiceGuardNodeOptions,
|
|
8
|
+
ServiceGuardTickInput,
|
|
9
|
+
ServiceGuardTickResult,
|
|
10
|
+
} from "./service-guard.interface";
|
|
11
|
+
export { SERVICE_GUARD_RUNNER_KEY } from "./service-guard.interface";
|
|
12
|
+
export { ServiceGuardNode } from "./service-guard-node";
|
|
13
|
+
export {
|
|
14
|
+
getWorkerSpawnCooldownLastAt,
|
|
15
|
+
markWorkerSpawned,
|
|
16
|
+
resetWorkerSpawnCooldownForTests,
|
|
17
|
+
} from "./worker-spawn-cooldown";
|