target-supabase-sdk 0.2.3 → 0.2.4

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.
Files changed (155) hide show
  1. package/README.md +24 -18
  2. package/dist/browser.d.ts +11 -4
  3. package/dist/browser.d.ts.map +1 -1
  4. package/dist/browser.js +3 -3
  5. package/dist/chunks/{enable-log-persist-C7GJo_Nc.js → enable-log-persist-B8rg_wXG.js} +5 -6
  6. package/dist/chunks/enable-log-persist-B8rg_wXG.js.map +1 -0
  7. package/dist/chunks/{create-logger-DyH3mZ8p.js → list.api-Ab9v8yOg.js} +66 -57
  8. package/dist/chunks/list.api-Ab9v8yOg.js.map +1 -0
  9. package/dist/chunks/{word.interface-DLgRobKV.js → word.interface-B_fbVFOW.js} +1078 -97
  10. package/dist/chunks/word.interface-B_fbVFOW.js.map +1 -0
  11. package/dist/extraction/extraction.api.d.ts +117 -0
  12. package/dist/extraction/extraction.api.d.ts.map +1 -0
  13. package/dist/extraction/extraction.interface.d.ts +43 -0
  14. package/dist/extraction/extraction.interface.d.ts.map +1 -0
  15. package/dist/list/list.interface.d.ts +2 -2
  16. package/dist/list/list.interface.d.ts.map +1 -1
  17. package/dist/node/node-runtime.base.d.ts +13 -1
  18. package/dist/node/node-runtime.base.d.ts.map +1 -1
  19. package/dist/node/process/create-managed-child-processes.d.ts +12 -0
  20. package/dist/node/process/create-managed-child-processes.d.ts.map +1 -0
  21. package/dist/node/process/index.d.ts +3 -1
  22. package/dist/node/process/index.d.ts.map +1 -1
  23. package/dist/node/process/managed-child-processes.d.ts +15 -2
  24. package/dist/node/process/managed-child-processes.d.ts.map +1 -1
  25. package/dist/node/runtime-state/create-service-runtime-state-store.d.ts +23 -0
  26. package/dist/node/runtime-state/create-service-runtime-state-store.d.ts.map +1 -0
  27. package/dist/node/runtime-state/index.d.ts +4 -0
  28. package/dist/node/runtime-state/index.d.ts.map +1 -0
  29. package/dist/node/runtime-state/service-runtime-state.types.d.ts +57 -0
  30. package/dist/node/runtime-state/service-runtime-state.types.d.ts.map +1 -0
  31. package/dist/node/service-guard/index.d.ts +9 -0
  32. package/dist/node/service-guard/index.d.ts.map +1 -0
  33. package/dist/node/service-guard/register-service-guard-runner.d.ts +9 -0
  34. package/dist/node/service-guard/register-service-guard-runner.d.ts.map +1 -0
  35. package/dist/node/service-guard/run-readiness-gate.d.ts +11 -0
  36. package/dist/node/service-guard/run-readiness-gate.d.ts.map +1 -0
  37. package/dist/node/service-guard/run-service-guard-tick.d.ts +7 -0
  38. package/dist/node/service-guard/run-service-guard-tick.d.ts.map +1 -0
  39. package/dist/node/service-guard/service-guard-node.d.ts +18 -0
  40. package/dist/node/service-guard/service-guard-node.d.ts.map +1 -0
  41. package/dist/node/service-guard/service-guard.interface.d.ts +58 -0
  42. package/dist/node/service-guard/service-guard.interface.d.ts.map +1 -0
  43. package/dist/node/service-guard/worker-spawn-cooldown.d.ts +4 -0
  44. package/dist/node/service-guard/worker-spawn-cooldown.d.ts.map +1 -0
  45. package/dist/node/service-host/index.d.ts +9 -0
  46. package/dist/node/service-host/index.d.ts.map +1 -0
  47. package/dist/node/service-host/registry-slot-guard-step.d.ts +21 -0
  48. package/dist/node/service-host/registry-slot-guard-step.d.ts.map +1 -0
  49. package/dist/node/service-host/service-host.d.ts +36 -0
  50. package/dist/node/service-host/service-host.d.ts.map +1 -0
  51. package/dist/node/service-host/single-process-service.d.ts +21 -0
  52. package/dist/node/service-host/single-process-service.d.ts.map +1 -0
  53. package/dist/node.d.ts +15 -2
  54. package/dist/node.d.ts.map +1 -1
  55. package/dist/node.js +2088 -1495
  56. package/dist/node.js.map +1 -1
  57. package/dist/preload.js +3 -3
  58. package/dist/service/config.api.d.ts +143 -0
  59. package/dist/service/config.api.d.ts.map +1 -0
  60. package/dist/service/config.interface.d.ts +22 -0
  61. package/dist/service/config.interface.d.ts.map +1 -0
  62. package/dist/service/index.d.ts +16 -17
  63. package/dist/service/index.d.ts.map +1 -1
  64. package/dist/service/registry-lifecycle.d.ts +43 -0
  65. package/dist/service/registry-lifecycle.d.ts.map +1 -0
  66. package/dist/service/registry.service.d.ts +94 -0
  67. package/dist/service/registry.service.d.ts.map +1 -0
  68. package/dist/service/service-bootstrap.d.ts +28 -0
  69. package/dist/service/service-bootstrap.d.ts.map +1 -0
  70. package/dist/service/service.api.d.ts +160 -0
  71. package/dist/service/service.api.d.ts.map +1 -1
  72. package/dist/service/service.interface.d.ts +32 -1
  73. package/dist/service/service.interface.d.ts.map +1 -1
  74. package/dist/shared/log/index.d.ts +4 -0
  75. package/dist/shared/log/index.d.ts.map +1 -1
  76. package/dist/shared/log/log-batch.d.ts +44 -0
  77. package/dist/shared/log/log-batch.d.ts.map +1 -0
  78. package/dist/shared/log/log-persist-coordinator.d.ts +37 -0
  79. package/dist/shared/log/log-persist-coordinator.d.ts.map +1 -0
  80. package/dist/shared/log/log-persist-flush.d.ts.map +1 -1
  81. package/dist/shared/log/log-persist.config.d.ts +0 -1
  82. package/dist/shared/log/log-persist.config.d.ts.map +1 -1
  83. package/dist/shared/log/log-persist.interface.d.ts +8 -0
  84. package/dist/shared/log/log-persist.interface.d.ts.map +1 -1
  85. package/dist/shared/utils/config-path.utils.d.ts +21 -0
  86. package/dist/shared/utils/config-path.utils.d.ts.map +1 -1
  87. package/dist/shared/utils/get-value-at-path.d.ts +3 -0
  88. package/dist/shared/utils/get-value-at-path.d.ts.map +1 -0
  89. package/dist/task/index.d.ts +2 -0
  90. package/dist/task/index.d.ts.map +1 -1
  91. package/dist/task/task-manager.d.ts +1 -1
  92. package/dist/task/task-manager.d.ts.map +1 -1
  93. package/dist/task/task-post-retry.api.d.ts +24 -0
  94. package/dist/task/task-post-retry.api.d.ts.map +1 -0
  95. package/dist/task/task-repo-context.d.ts +7 -1
  96. package/dist/task/task-repo-context.d.ts.map +1 -1
  97. package/dist/trigger/trigger-manager.d.ts +1 -1
  98. package/dist/trigger/trigger-manager.d.ts.map +1 -1
  99. package/dist/trigger/trigger-node.d.ts +1 -0
  100. package/dist/trigger/trigger-node.d.ts.map +1 -1
  101. package/dist/trigger/trigger.constant.d.ts +1 -1
  102. package/dist/trigger/trigger.constant.d.ts.map +1 -1
  103. package/dist/trigger/trigger.interface.d.ts +10 -0
  104. package/dist/trigger/trigger.interface.d.ts.map +1 -1
  105. package/package.json +4 -2
  106. package/src/browser.ts +98 -4
  107. package/src/extraction/extraction.api.ts +232 -0
  108. package/src/extraction/extraction.interface.ts +45 -0
  109. package/src/list/list.interface.ts +2 -2
  110. package/src/node/node-runtime.base.ts +37 -6
  111. package/src/node/process/create-managed-child-processes.ts +27 -0
  112. package/src/node/process/index.ts +4 -0
  113. package/src/node/process/managed-child-processes.ts +48 -15
  114. package/src/node/runtime-state/create-service-runtime-state-store.ts +125 -0
  115. package/src/node/runtime-state/index.ts +17 -0
  116. package/src/node/runtime-state/service-runtime-state.types.ts +64 -0
  117. package/src/node/service-guard/index.ts +17 -0
  118. package/src/node/service-guard/register-service-guard-runner.ts +64 -0
  119. package/src/node/service-guard/run-readiness-gate.ts +34 -0
  120. package/src/node/service-guard/run-service-guard-tick.ts +106 -0
  121. package/src/node/service-guard/service-guard-node.ts +52 -0
  122. package/src/node/service-guard/service-guard.interface.ts +64 -0
  123. package/src/node/service-guard/worker-spawn-cooldown.ts +13 -0
  124. package/src/node/service-host/index.ts +30 -0
  125. package/src/node/service-host/registry-slot-guard-step.ts +57 -0
  126. package/src/node/service-host/service-host.ts +168 -0
  127. package/src/node/service-host/single-process-service.ts +58 -0
  128. package/src/node.ts +69 -0
  129. package/src/service/config.api.ts +224 -0
  130. package/src/service/config.interface.ts +25 -0
  131. package/src/service/index.ts +74 -149
  132. package/src/service/registry-lifecycle.ts +144 -0
  133. package/src/service/registry.service.ts +564 -0
  134. package/src/service/service-bootstrap.ts +74 -0
  135. package/src/service/service.api.ts +13 -0
  136. package/src/service/service.interface.ts +36 -1
  137. package/src/shared/log/index.ts +13 -0
  138. package/src/shared/log/log-batch.ts +156 -0
  139. package/src/shared/log/log-persist-coordinator.ts +107 -0
  140. package/src/shared/log/log-persist-flush.ts +3 -3
  141. package/src/shared/log/log-persist.config.ts +0 -2
  142. package/src/shared/log/log-persist.interface.ts +10 -0
  143. package/src/shared/utils/config-path.utils.ts +35 -0
  144. package/src/shared/utils/get-value-at-path.ts +17 -0
  145. package/src/task/index.ts +2 -0
  146. package/src/task/task-manager.ts +10 -4
  147. package/src/task/task-post-retry.api.ts +61 -0
  148. package/src/task/task-repo-context.ts +8 -1
  149. package/src/trigger/trigger-manager.ts +4 -2
  150. package/src/trigger/trigger-node.ts +10 -4
  151. package/src/trigger/trigger.constant.ts +1 -1
  152. package/src/trigger/trigger.interface.ts +10 -0
  153. package/dist/chunks/create-logger-DyH3mZ8p.js.map +0 -1
  154. package/dist/chunks/enable-log-persist-C7GJo_Nc.js.map +0 -1
  155. package/dist/chunks/word.interface-DLgRobKV.js.map +0 -1
@@ -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 logger?: Pick<LoggerWithScope, "info" | "warn" | "error">;
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
- const tracked = [...this.byLabel.values()].filter((child) => isChildProcessRunning(child));
121
+ this.suppressCriticalExit = true;
122
+ try {
123
+ const tracked = [...this.byLabel.values()].filter((child) => isChildProcessRunning(child));
94
124
 
95
- await Promise.all(tracked.map((child) => this.stopChild(child)));
125
+ await Promise.all(tracked.map((child) => this.stopChild(child)));
96
126
 
97
- const extraPids = options?.extraPids ?? [];
98
- for (const pid of extraPids) {
99
- if (pid == null || tracked.some((child) => child.pid === pid)) {
100
- continue;
101
- }
102
- try {
103
- process.kill(pid, "SIGTERM");
104
- } catch {
105
- // already exited
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 { createJsonFileStateStore, type JsonFileStateStore, type JsonStatePatch } from "../fs/json-state-store";
3
+ import type {
4
+ DefaultExtraRuntimeSlices,
5
+ GuardRuntimeSlice,
6
+ ReadinessRuntimeSlice,
7
+ SchedulerRuntimeSlice,
8
+ ServiceRuntimeExtraSlicePatch,
9
+ ServiceRuntimeNestedKeys,
10
+ ServiceRuntimeState,
11
+ WorkerRuntimeSlice,
12
+ } from "./service-runtime-state.types";
13
+
14
+ const EPOCH_ISO = new Date(0).toISOString();
15
+
16
+ export interface CreateServiceRuntimeStateStoreOptions<
17
+ TExtraSlices extends Record<string, object> = DefaultExtraRuntimeSlices,
18
+ > {
19
+ filePath: string;
20
+ /** Default values for service-specific top-level nested slices. */
21
+ extraDefaults?: TExtraSlices;
22
+ }
23
+
24
+ export interface FinishRunnerTickOptions<TExtraSlices extends Record<string, object> = DefaultExtraRuntimeSlices> {
25
+ nowMs: number;
26
+ lastFired: Record<string, string>;
27
+ postedCount?: number;
28
+ /** Patch extra slices in the same atomic write as the scheduler tick. */
29
+ extra?: ServiceRuntimeExtraSlicePatch<TExtraSlices>;
30
+ }
31
+
32
+ export interface ServiceRuntimeStateStore<TExtraSlices extends Record<string, object> = DefaultExtraRuntimeSlices> {
33
+ readRuntimeState(): Promise<ServiceRuntimeState<TExtraSlices>>;
34
+ writeRuntimeState(
35
+ patch: JsonStatePatch<ServiceRuntimeState<TExtraSlices>, ServiceRuntimeNestedKeys<TExtraSlices>>,
36
+ ): Promise<ServiceRuntimeState<TExtraSlices>>;
37
+ resetRuntimeStateForStartup(): Promise<ServiceRuntimeState<TExtraSlices>>;
38
+ finishRunnerTick(options: FinishRunnerTickOptions<TExtraSlices>): Promise<void>;
39
+ }
40
+
41
+ function createDefaultServiceRuntimeState<TExtraSlices extends Record<string, object>>(options: {
42
+ extraDefaults?: TExtraSlices;
43
+ }): ServiceRuntimeState<TExtraSlices> {
44
+ const readiness: ReadinessRuntimeSlice = {
45
+ status: "pending",
46
+ checkedAt: null,
47
+ message: null,
48
+ checks: [],
49
+ };
50
+ const guard: GuardRuntimeSlice = {
51
+ nodeId: null,
52
+ lastCheckAt: null,
53
+ lastDecision: null,
54
+ lastSpawnAt: null,
55
+ spawnCount: 0,
56
+ };
57
+ const scheduler: SchedulerRuntimeSlice = {
58
+ lastTickAt: null,
59
+ lastPostedCount: 0,
60
+ lastFired: {},
61
+ };
62
+ const worker: WorkerRuntimeSlice = {
63
+ pid: null,
64
+ spawnedAt: null,
65
+ ready: false,
66
+ registeredTasks: [],
67
+ readyAt: null,
68
+ };
69
+
70
+ return {
71
+ updatedAt: EPOCH_ISO,
72
+ readiness,
73
+ guard,
74
+ scheduler,
75
+ worker,
76
+ registry: { ...EMPTY_REGISTRY_SLOT_RUNTIME_STATE },
77
+ ...(options.extraDefaults ?? ({} as TExtraSlices)),
78
+ };
79
+ }
80
+
81
+ /** JSON file-backed service runtime state (L3 blueprint) with typed core slices and extensions. */
82
+ export function createServiceRuntimeStateStore<TExtraSlices extends Record<string, object> = DefaultExtraRuntimeSlices>(
83
+ options: CreateServiceRuntimeStateStoreOptions<TExtraSlices>,
84
+ ): ServiceRuntimeStateStore<TExtraSlices> {
85
+ type State = ServiceRuntimeState<TExtraSlices>;
86
+ type NK = ServiceRuntimeNestedKeys<TExtraSlices>;
87
+
88
+ const defaultState = createDefaultServiceRuntimeState(options);
89
+ const nestedKeys = [
90
+ "readiness",
91
+ "guard",
92
+ "scheduler",
93
+ "worker",
94
+ "registry",
95
+ ...(Object.keys(options.extraDefaults ?? {}) as (keyof TExtraSlices & string)[]),
96
+ ] as NK[];
97
+
98
+ let store: JsonFileStateStore<State, NK> | undefined;
99
+
100
+ function instance(): JsonFileStateStore<State, NK> {
101
+ store ??= createJsonFileStateStore<State, NK>({
102
+ filePath: options.filePath,
103
+ defaultState,
104
+ nestedKeys,
105
+ updatedAtKey: "updatedAt",
106
+ });
107
+ return store;
108
+ }
109
+
110
+ return {
111
+ readRuntimeState: () => instance().read(),
112
+ writeRuntimeState: (patch) => instance().write(patch),
113
+ resetRuntimeStateForStartup: () => instance().reset(),
114
+ finishRunnerTick: async ({ nowMs, lastFired, postedCount, extra }) => {
115
+ await instance().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";
@@ -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
+ }