target-supabase-sdk 0.2.12 → 0.2.14

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 (197) hide show
  1. package/dist/browser.d.ts +1 -1
  2. package/dist/browser.d.ts.map +1 -1
  3. package/dist/browser.js +1 -1
  4. package/dist/chunks/enable-C7lFooTG.js +874 -0
  5. package/dist/chunks/enable-C7lFooTG.js.map +1 -0
  6. package/dist/chunks/list.api-BzbTJBsM.js.map +1 -1
  7. package/dist/chunks/{task-queue-H4mpSW8R.js → task-queue-qSnsfiIg.js} +111 -4
  8. package/dist/chunks/task-queue-qSnsfiIg.js.map +1 -0
  9. package/dist/node/env/index.d.ts +1 -1
  10. package/dist/node/env/index.d.ts.map +1 -1
  11. package/dist/node/node-runtime.base.d.ts +14 -1
  12. package/dist/node/node-runtime.base.d.ts.map +1 -1
  13. package/dist/node/node.api.d.ts.map +1 -1
  14. package/dist/node/node.constant.d.ts +3 -0
  15. package/dist/node/node.constant.d.ts.map +1 -0
  16. package/dist/node/node.utils.d.ts +0 -2
  17. package/dist/node/node.utils.d.ts.map +1 -1
  18. package/dist/node/preload/run-service-preload.d.ts.map +1 -1
  19. package/dist/node/preload/service-preload.interface.d.ts +3 -1
  20. package/dist/node/preload/service-preload.interface.d.ts.map +1 -1
  21. package/dist/node/process/managed-child-processes.d.ts.map +1 -1
  22. package/dist/node/readiness/index.d.ts +2 -0
  23. package/dist/node/readiness/index.d.ts.map +1 -1
  24. package/dist/node/{service-guard → readiness}/run-readiness-gate.d.ts +1 -1
  25. package/dist/node/readiness/run-readiness-gate.d.ts.map +1 -0
  26. package/dist/node/readiness/service-ready-gate.d.ts.map +1 -1
  27. package/dist/node/runtime-state/create-service-runtime-state-store.d.ts.map +1 -1
  28. package/dist/node/runtime-state/service-runtime-state.types.d.ts +16 -0
  29. package/dist/node/runtime-state/service-runtime-state.types.d.ts.map +1 -1
  30. package/dist/node/service-guard/index.d.ts +4 -4
  31. package/dist/node/service-guard/index.d.ts.map +1 -1
  32. package/dist/node/service-guard/register-service-guard-runner.d.ts +1 -2
  33. package/dist/node/service-guard/register-service-guard-runner.d.ts.map +1 -1
  34. package/dist/node/service-guard/registry-slot-guard-step.d.ts.map +1 -0
  35. package/dist/node/service-guard/run-service-guard-tick.d.ts.map +1 -1
  36. package/dist/node/service-guard/service-guard-node.d.ts +25 -2
  37. package/dist/node/service-guard/service-guard-node.d.ts.map +1 -1
  38. package/dist/node/service-guard/service-guard.interface.d.ts +25 -10
  39. package/dist/node/service-guard/service-guard.interface.d.ts.map +1 -1
  40. package/dist/node/service-host/claim-registry-slot.d.ts +15 -0
  41. package/dist/node/service-host/claim-registry-slot.d.ts.map +1 -0
  42. package/dist/node/service-host/create-l3-child-launcher.d.ts +38 -0
  43. package/dist/node/service-host/create-l3-child-launcher.d.ts.map +1 -0
  44. package/dist/node/service-host/index.d.ts +2 -4
  45. package/dist/node/service-host/index.d.ts.map +1 -1
  46. package/dist/node/service-host/service-host.d.ts +8 -4
  47. package/dist/node/service-host/service-host.d.ts.map +1 -1
  48. package/dist/node/service-host/single-process-service.d.ts +3 -3
  49. package/dist/node/service-host/single-process-service.d.ts.map +1 -1
  50. package/dist/node.d.ts +17 -10
  51. package/dist/node.d.ts.map +1 -1
  52. package/dist/node.js +927 -119
  53. package/dist/node.js.map +1 -1
  54. package/dist/preload.js +9 -6
  55. package/dist/preload.js.map +1 -1
  56. package/dist/service/index.d.ts +2 -2
  57. package/dist/service/index.d.ts.map +1 -1
  58. package/dist/service/registry.service.d.ts +19 -0
  59. package/dist/service/registry.service.d.ts.map +1 -1
  60. package/dist/shared/log/core/create-logger.d.ts.map +1 -0
  61. package/dist/shared/log/{log-batch.d.ts → core/log-batch.d.ts} +4 -4
  62. package/dist/shared/log/core/log-batch.d.ts.map +1 -0
  63. package/dist/shared/log/core/log-manager.d.ts.map +1 -0
  64. package/dist/shared/log/core/log-min-level.d.ts.map +1 -0
  65. package/dist/shared/log/core/log-scope.d.ts.map +1 -0
  66. package/dist/shared/log/index.d.ts +13 -13
  67. package/dist/shared/log/index.d.ts.map +1 -1
  68. package/dist/shared/log/spool/collector.d.ts +21 -0
  69. package/dist/shared/log/spool/collector.d.ts.map +1 -0
  70. package/dist/shared/log/spool/config.d.ts +7 -0
  71. package/dist/shared/log/spool/config.d.ts.map +1 -0
  72. package/dist/shared/log/spool/coordinator.d.ts +18 -0
  73. package/dist/shared/log/spool/coordinator.d.ts.map +1 -0
  74. package/dist/shared/log/spool/enable.d.ts +23 -0
  75. package/dist/shared/log/spool/enable.d.ts.map +1 -0
  76. package/dist/shared/log/spool/file.d.ts +21 -0
  77. package/dist/shared/log/spool/file.d.ts.map +1 -0
  78. package/dist/shared/log/spool/interface.d.ts +55 -0
  79. package/dist/shared/log/spool/interface.d.ts.map +1 -0
  80. package/dist/shared/log/spool/paths.d.ts +6 -0
  81. package/dist/shared/log/spool/paths.d.ts.map +1 -0
  82. package/dist/shared/log/spool/process-roles.d.ts +17 -0
  83. package/dist/shared/log/spool/process-roles.d.ts.map +1 -0
  84. package/dist/shared/log/spool/register-collect-log-runner.d.ts +15 -0
  85. package/dist/shared/log/spool/register-collect-log-runner.d.ts.map +1 -0
  86. package/dist/shared/log/spool/service-lifecycle.d.ts +13 -0
  87. package/dist/shared/log/spool/service-lifecycle.d.ts.map +1 -0
  88. package/dist/shared/log/spool/writer.d.ts +32 -0
  89. package/dist/shared/log/spool/writer.d.ts.map +1 -0
  90. package/dist/shared/log/{log-persist-batch-id.d.ts → upload/batch-id.d.ts} +3 -3
  91. package/dist/shared/log/upload/batch-id.d.ts.map +1 -0
  92. package/dist/shared/log/upload/env.d.ts +7 -0
  93. package/dist/shared/log/upload/env.d.ts.map +1 -0
  94. package/dist/shared/log/{log-persist-flush.d.ts → upload/flush.d.ts} +3 -3
  95. package/dist/shared/log/upload/flush.d.ts.map +1 -0
  96. package/dist/shared/log/{log-persist-hook.d.ts → upload/hook.d.ts} +2 -2
  97. package/dist/shared/log/upload/hook.d.ts.map +1 -0
  98. package/dist/shared/log/upload/interface.d.ts +33 -0
  99. package/dist/shared/log/upload/interface.d.ts.map +1 -0
  100. package/dist/shared/log/upload/logger.d.ts +22 -0
  101. package/dist/shared/log/upload/logger.d.ts.map +1 -0
  102. package/package.json +1 -1
  103. package/src/browser.ts +1 -0
  104. package/src/node/env/index.ts +0 -4
  105. package/src/node/node-runtime.base.ts +33 -5
  106. package/src/node/node.api.ts +1 -1
  107. package/src/node/node.constant.ts +2 -0
  108. package/src/node/node.utils.ts +2 -2
  109. package/src/node/preload/run-service-preload.ts +11 -6
  110. package/src/node/preload/service-preload.interface.ts +3 -1
  111. package/src/node/process/create-managed-child-processes.ts +1 -1
  112. package/src/node/process/managed-child-processes.ts +8 -1
  113. package/src/node/readiness/index.ts +2 -0
  114. package/src/node/{service-guard → readiness}/run-readiness-gate.ts +2 -2
  115. package/src/node/readiness/service-ready-gate.ts +27 -23
  116. package/src/node/runtime-state/create-service-runtime-state-store.ts +11 -0
  117. package/src/node/runtime-state/service-runtime-state.types.ts +12 -0
  118. package/src/node/service-guard/index.ts +7 -8
  119. package/src/node/service-guard/register-service-guard-runner.ts +0 -2
  120. package/src/node/service-guard/run-service-guard-tick.ts +5 -2
  121. package/src/node/service-guard/service-guard-node.ts +234 -6
  122. package/src/node/service-guard/service-guard.interface.ts +35 -10
  123. package/src/node/service-host/claim-registry-slot.ts +33 -0
  124. package/src/node/service-host/create-l3-child-launcher.ts +157 -0
  125. package/src/node/service-host/index.ts +5 -20
  126. package/src/node/service-host/service-host.ts +33 -21
  127. package/src/node/service-host/single-process-service.ts +10 -20
  128. package/src/node.ts +45 -35
  129. package/src/service/index.ts +3 -0
  130. package/src/service/registry.service.ts +142 -0
  131. package/src/shared/log/README.md +206 -0
  132. package/src/shared/log/{log-batch.ts → core/log-batch.ts} +4 -4
  133. package/src/shared/log/{log-manager.ts → core/log-manager.ts} +3 -3
  134. package/src/shared/log/{log-min-level.ts → core/log-min-level.ts} +1 -1
  135. package/src/shared/log/{log-scope.ts → core/log-scope.ts} +1 -1
  136. package/src/shared/log/index.ts +13 -18
  137. package/src/shared/log/spool/collector.ts +330 -0
  138. package/src/shared/log/spool/config.ts +56 -0
  139. package/src/shared/log/spool/coordinator.ts +55 -0
  140. package/src/shared/log/spool/enable.ts +136 -0
  141. package/src/shared/log/spool/file.ts +75 -0
  142. package/src/shared/log/spool/interface.ts +62 -0
  143. package/src/shared/log/spool/paths.ts +30 -0
  144. package/src/shared/log/spool/process-roles.ts +75 -0
  145. package/src/shared/log/spool/register-collect-log-runner.ts +63 -0
  146. package/src/shared/log/spool/service-lifecycle.ts +35 -0
  147. package/src/shared/log/spool/writer.ts +273 -0
  148. package/src/shared/log/{log-persist-batch-id.ts → upload/batch-id.ts} +2 -2
  149. package/src/shared/log/upload/env.ts +26 -0
  150. package/src/shared/log/{log-persist-flush.ts → upload/flush.ts} +8 -7
  151. package/src/shared/log/{log-persist-hook.ts → upload/hook.ts} +1 -1
  152. package/src/shared/log/upload/interface.ts +38 -0
  153. package/src/shared/log/upload/logger.ts +37 -0
  154. package/dist/chunks/enable-log-persist-Dl55_mv2.js +0 -1674
  155. package/dist/chunks/enable-log-persist-Dl55_mv2.js.map +0 -1
  156. package/dist/chunks/task-queue-H4mpSW8R.js.map +0 -1
  157. package/dist/node/service-guard/run-readiness-gate.d.ts.map +0 -1
  158. package/dist/node/service-host/registry-slot-guard-step.d.ts.map +0 -1
  159. package/dist/shared/log/create-logger.d.ts.map +0 -1
  160. package/dist/shared/log/enable-log-persist.d.ts +0 -28
  161. package/dist/shared/log/enable-log-persist.d.ts.map +0 -1
  162. package/dist/shared/log/log-batch.d.ts.map +0 -1
  163. package/dist/shared/log/log-manager.d.ts.map +0 -1
  164. package/dist/shared/log/log-min-level.d.ts.map +0 -1
  165. package/dist/shared/log/log-persist-batch-id.d.ts.map +0 -1
  166. package/dist/shared/log/log-persist-coordinator.d.ts +0 -37
  167. package/dist/shared/log/log-persist-coordinator.d.ts.map +0 -1
  168. package/dist/shared/log/log-persist-errors.d.ts +0 -4
  169. package/dist/shared/log/log-persist-errors.d.ts.map +0 -1
  170. package/dist/shared/log/log-persist-flush.d.ts.map +0 -1
  171. package/dist/shared/log/log-persist-hook.d.ts.map +0 -1
  172. package/dist/shared/log/log-persist-logger.d.ts +0 -16
  173. package/dist/shared/log/log-persist-logger.d.ts.map +0 -1
  174. package/dist/shared/log/log-persist-registry.d.ts +0 -18
  175. package/dist/shared/log/log-persist-registry.d.ts.map +0 -1
  176. package/dist/shared/log/log-persist.config.d.ts +0 -9
  177. package/dist/shared/log/log-persist.config.d.ts.map +0 -1
  178. package/dist/shared/log/log-persist.d.ts +0 -62
  179. package/dist/shared/log/log-persist.d.ts.map +0 -1
  180. package/dist/shared/log/log-persist.interface.d.ts +0 -132
  181. package/dist/shared/log/log-persist.interface.d.ts.map +0 -1
  182. package/dist/shared/log/log-scope.d.ts.map +0 -1
  183. package/src/shared/log/enable-log-persist.ts +0 -225
  184. package/src/shared/log/log-persist-coordinator.ts +0 -107
  185. package/src/shared/log/log-persist-errors.ts +0 -16
  186. package/src/shared/log/log-persist-logger.ts +0 -61
  187. package/src/shared/log/log-persist-registry.ts +0 -252
  188. package/src/shared/log/log-persist.config.ts +0 -115
  189. package/src/shared/log/log-persist.interface.ts +0 -150
  190. package/src/shared/log/log-persist.ts +0 -822
  191. /package/dist/node/{service-host → service-guard}/registry-slot-guard-step.d.ts +0 -0
  192. /package/dist/shared/log/{create-logger.d.ts → core/create-logger.d.ts} +0 -0
  193. /package/dist/shared/log/{log-manager.d.ts → core/log-manager.d.ts} +0 -0
  194. /package/dist/shared/log/{log-min-level.d.ts → core/log-min-level.d.ts} +0 -0
  195. /package/dist/shared/log/{log-scope.d.ts → core/log-scope.d.ts} +0 -0
  196. /package/src/node/{service-host → service-guard}/registry-slot-guard-step.ts +0 -0
  197. /package/src/shared/log/{create-logger.ts → core/create-logger.ts} +0 -0
@@ -1,17 +1,35 @@
1
- import type { LoggerWithScope } from "../../shared/log";
1
+ import { createLogger, type LoggerWithScope } from "../../shared/log";
2
+ import { registerCollectLogRunner } from "../../shared/log/spool/register-collect-log-runner";
3
+ import { getErrorMessage } from "../../shared/utils/error.utils";
2
4
  import { TriggerNode } from "../../trigger/trigger-node";
5
+ import type { NodeLoopContext } from "../node-runtime.base";
6
+ import { pollUntil } from "../readiness/poll-until";
7
+ import { runReadinessGate } from "../readiness/run-readiness-gate";
8
+ import type { GuardRuntimeSlice } from "../runtime-state/service-runtime-state.types";
3
9
  import { registerServiceGuardRunner } from "./register-service-guard-runner";
4
- import { runReadinessGate } from "./run-readiness-gate";
5
10
  import type { ServiceGuardNodeOptions } from "./service-guard.interface";
11
+ import { markWorkerSpawned } from "./worker-spawn-cooldown";
12
+
13
+ const SILENT_BACKOFF_MS = [15_000, 30_000, 60_000, 120_000, 240_000, 300_000] as const;
14
+ const SILENT_EVENT_LIMIT = 32;
15
+ const DEFAULT_BUSINESS_READY_TIMEOUT_MS = 180_000;
6
16
 
7
17
  /**
8
- * L3 guard process TriggerNode: readiness gate, TaskNode worker spawn owner,
18
+ * L3 guard process TriggerNode: readiness gate, business-node spawn owner,
9
19
  * and {@link registerServiceGuardRunner} tick (registry slot + liveness + runtime).
10
20
  *
21
+ * Consecutive heartbeat failure enters silent mode (no process exit). Recovery
22
+ * respawns business nodes; leaving silent requires heartbeat + all nodes ready.
23
+ *
11
24
  * Use {@link ServiceGuardNode.create} so the guard runner is registered before `start()`.
12
25
  */
13
26
  class ServiceGuardNode extends TriggerNode {
14
27
  private readonly options: ServiceGuardNodeOptions;
28
+ private readonly businessReadyTimeoutMs: number;
29
+ private mode: GuardRuntimeSlice["mode"] = "healthy";
30
+ private silentBackoffIndex = 0;
31
+ private silentEvents: GuardRuntimeSlice["silentEvents"] = [];
32
+ private recoverPromise: Promise<void> | null = null;
15
33
 
16
34
  private constructor(options: ServiceGuardNodeOptions) {
17
35
  super({
@@ -19,18 +37,56 @@ class ServiceGuardNode extends TriggerNode {
19
37
  beforeProcessExit: options.beforeProcessExit,
20
38
  });
21
39
  this.options = options;
40
+ this.businessReadyTimeoutMs = options.businessReadyTimeoutMs ?? DEFAULT_BUSINESS_READY_TIMEOUT_MS;
22
41
  }
23
42
 
24
43
  /** Register the guard runner and return a ready-to-start node. */
25
44
  static create(options: ServiceGuardNodeOptions): ServiceGuardNode {
26
45
  registerServiceGuardRunner({
46
+ ...options.guardRunner,
27
47
  serviceValue: options.serviceValue,
28
48
  logTopic: options.logTopic,
29
- ...options.guardRunner,
49
+ spawnWorker: options.spawnBusinessNodes,
50
+ });
51
+ registerCollectLogRunner({
52
+ serviceValue: options.serviceValue,
53
+ getServiceId: options.guardRunner.getServiceId,
30
54
  });
31
55
  return new ServiceGuardNode(options);
32
56
  }
33
57
 
58
+ protected isSilenced(): boolean {
59
+ return this.mode !== "healthy";
60
+ }
61
+
62
+ protected getLoopIntervalMs(): number {
63
+ if (this.mode === "silent") {
64
+ return SILENT_BACKOFF_MS[this.silentBackoffIndex] ?? 300_000;
65
+ }
66
+ return super.getLoopIntervalMs();
67
+ }
68
+
69
+ protected async onHealthCheckFailed(_ctx: NodeLoopContext): Promise<void> {
70
+ if (this.mode !== "healthy") {
71
+ this.bumpSilentBackoff();
72
+ await this.persistGuard({
73
+ lastDecision: "silent_heartbeat_fail",
74
+ silentConsecutiveFailures: this.silentConsecutiveFailures(),
75
+ silentBackoffMs: this.getLoopIntervalMs(),
76
+ silentLastHeartbeatAt: new Date().toISOString(),
77
+ });
78
+ return;
79
+ }
80
+ await this.enterSilent("heartbeat:consecutive-failures");
81
+ }
82
+
83
+ protected async onHeartbeatRecovered(ctx: NodeLoopContext): Promise<void> {
84
+ if (this.mode !== "silent") {
85
+ return;
86
+ }
87
+ await this.recoverFromSilent(ctx);
88
+ }
89
+
34
90
  protected async onBeforeRegisterNode(nodeLogger: LoggerWithScope): Promise<void> {
35
91
  await runReadinessGate({
36
92
  checks: this.options.readinessChecks,
@@ -41,11 +97,183 @@ class ServiceGuardNode extends TriggerNode {
41
97
 
42
98
  await super.onBeforeRegisterNode(nodeLogger);
43
99
 
44
- nodeLogger.info("Spawning TaskNode worker (single spawn owner)", {
100
+ nodeLogger.info("Spawning business nodes", {
45
101
  topic: this.options.logTopic ?? "guard",
46
102
  });
47
- await this.options.spawnWorker("guard:readiness-passed");
103
+ await this.options.spawnBusinessNodes("guard:readiness-passed");
104
+ markWorkerSpawned();
48
105
  this.options.onWorkerSpawned?.();
106
+
107
+ await this.waitUntilBusinessReady();
108
+ await this.persistGuard({
109
+ mode: "healthy",
110
+ lastDecision: "bootstrap_ready",
111
+ });
112
+ }
113
+
114
+ protected async runLoopSteps(ctx: NodeLoopContext, heartbeatOk: boolean): Promise<void> {
115
+ await super.runLoopSteps(ctx, heartbeatOk);
116
+ if (this.mode !== "healthy" || !heartbeatOk) {
117
+ return;
118
+ }
119
+ // Idempotent — respawn exited children without entering silent (worker stale is the runner's job).
120
+ try {
121
+ await this.options.spawnBusinessNodes("guard:ensure-business");
122
+ } catch (error) {
123
+ this.createGuardLogger().warn("ensure-business spawn failed", {
124
+ topic: this.options.logTopic ?? "guard",
125
+ data: { error: getErrorMessage(error) },
126
+ });
127
+ }
128
+ }
129
+
130
+ private silentConsecutiveFailures(): number {
131
+ return this.silentBackoffIndex + 1;
132
+ }
133
+
134
+ private bumpSilentBackoff(): void {
135
+ if (this.silentBackoffIndex < SILENT_BACKOFF_MS.length - 1) {
136
+ this.silentBackoffIndex += 1;
137
+ }
138
+ }
139
+
140
+ private async enterSilent(reason: string): Promise<void> {
141
+ this.mode = "silent";
142
+ this.silentBackoffIndex = 0;
143
+ const enteredAt = new Date().toISOString();
144
+ this.pushEvent("enter", reason);
145
+ await this.persistGuard({
146
+ mode: "silent",
147
+ lastDecision: `silent:${reason}`,
148
+ silentEnteredAt: enteredAt,
149
+ silentLastHeartbeatAt: enteredAt,
150
+ silentBackoffMs: SILENT_BACKOFF_MS[0],
151
+ silentConsecutiveFailures: 1,
152
+ silentRecoveryAttempt: 0,
153
+ silentEvents: this.silentEvents,
154
+ });
155
+
156
+ const logger = this.createGuardLogger();
157
+ logger.warn("Entering silent mode — service unavailable until heartbeat + business nodes recover", {
158
+ topic: this.options.logTopic ?? "guard",
159
+ data: { reason },
160
+ });
161
+
162
+ try {
163
+ await this.options.stopBusinessNodes();
164
+ } catch (error) {
165
+ logger.warn("stopBusinessNodes failed (best-effort)", {
166
+ topic: this.options.logTopic ?? "guard",
167
+ data: { error: getErrorMessage(error) },
168
+ });
169
+ }
170
+ }
171
+
172
+ private async recoverFromSilent(ctx: NodeLoopContext): Promise<void> {
173
+ if (this.recoverPromise != null) {
174
+ return this.recoverPromise;
175
+ }
176
+ this.recoverPromise = this.runRecover(ctx).finally(() => {
177
+ this.recoverPromise = null;
178
+ });
179
+ return this.recoverPromise;
180
+ }
181
+
182
+ private async runRecover(ctx: NodeLoopContext): Promise<void> {
183
+ this.mode = "recovering";
184
+ this.pushEvent("recover_start", ctx.loopTraceId);
185
+ await this.persistGuard({
186
+ mode: "recovering",
187
+ lastDecision: "recovering",
188
+ silentLastHeartbeatAt: new Date().toISOString(),
189
+ silentRecoveryAttempt: this.silentEvents.filter((event) => event.type === "recover_start").length,
190
+ silentEvents: this.silentEvents,
191
+ });
192
+
193
+ const logger = this.createGuardLogger();
194
+ logger.info("Heartbeat restored — recovering business nodes", {
195
+ topic: this.options.logTopic ?? "guard",
196
+ });
197
+
198
+ try {
199
+ await this.options.stopBusinessNodes();
200
+ } catch (error) {
201
+ logger.warn("stopBusinessNodes failed before respawn", {
202
+ topic: this.options.logTopic ?? "guard",
203
+ data: { error: getErrorMessage(error) },
204
+ });
205
+ }
206
+
207
+ try {
208
+ await this.options.spawnBusinessNodes("guard:silent-recover");
209
+ markWorkerSpawned();
210
+ await this.waitUntilBusinessReady();
211
+ } catch (error) {
212
+ this.pushEvent("recover_fail", getErrorMessage(error));
213
+ this.mode = "silent";
214
+ this.bumpSilentBackoff();
215
+ await this.persistGuard({
216
+ mode: "silent",
217
+ lastDecision: "recover_fail",
218
+ silentBackoffMs: this.getLoopIntervalMs(),
219
+ silentEvents: this.silentEvents,
220
+ });
221
+ logger.warn("Recovery failed — returning to silent", {
222
+ topic: this.options.logTopic ?? "guard",
223
+ data: { error: getErrorMessage(error) },
224
+ });
225
+ try {
226
+ await this.options.stopBusinessNodes();
227
+ } catch {
228
+ // best-effort — leftover children are stopped again on the next recover
229
+ }
230
+ return;
231
+ }
232
+
233
+ this.mode = "healthy";
234
+ this.silentBackoffIndex = 0;
235
+ this.pushEvent("recover_ok");
236
+ await this.persistGuard({
237
+ mode: "healthy",
238
+ lastDecision: "recover_ok",
239
+ silentEnteredAt: null,
240
+ silentBackoffMs: 0,
241
+ silentConsecutiveFailures: 0,
242
+ silentEvents: this.silentEvents,
243
+ });
244
+ logger.success("Left silent mode — business nodes ready", {
245
+ topic: this.options.logTopic ?? "guard",
246
+ });
247
+ }
248
+
249
+ private async waitUntilBusinessReady(): Promise<void> {
250
+ try {
251
+ await pollUntil({
252
+ timeoutMs: this.businessReadyTimeoutMs,
253
+ intervalMs: 500,
254
+ until: () => this.options.isBusinessReady(),
255
+ });
256
+ } catch {
257
+ throw new Error(`Business nodes not ready within ${this.businessReadyTimeoutMs}ms`);
258
+ }
259
+ }
260
+
261
+ private pushEvent(type: string, detail?: string): void {
262
+ this.silentEvents = [...this.silentEvents, { at: new Date().toISOString(), type, detail }].slice(
263
+ -SILENT_EVENT_LIMIT,
264
+ );
265
+ }
266
+
267
+ private async persistGuard(patch: Partial<GuardRuntimeSlice>): Promise<void> {
268
+ try {
269
+ await this.options.guardRunner.onGuardPatch(patch);
270
+ } catch {
271
+ // best-effort — disk/runtime state must not take down the guard loop
272
+ }
273
+ }
274
+
275
+ private createGuardLogger(): LoggerWithScope {
276
+ return createLogger({ module: "service-guard" });
49
277
  }
50
278
  }
51
279
 
@@ -1,8 +1,8 @@
1
- import type { Node } from "../../node/node.interface";
2
1
  import type { RegistrySlotRuntimeState } from "../../service/registry-lifecycle";
3
2
  import type { LoggerWithScope } from "../../shared/log";
4
- import type { TriggerNodeOptions } from "../../trigger/trigger.interface";
3
+ import type { Node } from "../node.interface";
5
4
  import type { ReadinessCheck, ReadinessReport } from "../readiness/readiness.types";
5
+ import type { GuardRuntimeSlice } from "../runtime-state/service-runtime-state.types";
6
6
 
7
7
  /** Default TriggerManager runner key for {@link registerServiceGuardRunner}. */
8
8
  export const SERVICE_GUARD_RUNNER_KEY = "service-guard";
@@ -45,20 +45,45 @@ export interface RegisterServiceGuardRunnerOptions {
45
45
  onRegistryPatch: (
46
46
  patch: Pick<RegistrySlotRuntimeState, "slotOwned" | "lastSlotCheckAt" | "lastSlotCheckError">,
47
47
  ) => Promise<void>;
48
- onGuardPatch: (patch: { nodeId: string; lastCheckAt: string; lastDecision?: string }) => Promise<void>;
48
+ onGuardPatch: (patch: Partial<GuardRuntimeSlice>) => Promise<void>;
49
49
  onDecision?: (decision: string) => Promise<void>;
50
50
  }
51
51
 
52
- export interface ServiceGuardNodeOptions extends TriggerNodeOptions {
52
+ export interface ServiceGuardNodeOptions {
53
53
  serviceValue: string;
54
54
  logTopic?: string;
55
55
  readinessChecks: ReadinessCheck[];
56
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>;
57
+ guardRunner: Omit<RegisterServiceGuardRunnerOptions, "serviceValue" | "logTopic" | "runnerKey" | "spawnWorker">;
58
+ /**
59
+ * Idempotent spawn of all business nodes (scheduler + worker, etc.).
60
+ * Used at bootstrap, silent recovery, and healthy-mode TaskNode respawn.
61
+ */
62
+ spawnBusinessNodes: (reason: string) => Promise<void>;
63
+ /** Stop business nodes only (never the guard process). */
64
+ stopBusinessNodes: () => Promise<void>;
65
+ /** Local runtime gate: all business nodes registered and ready. */
66
+ isBusinessReady: () => Promise<boolean>;
67
+ /** Timeout for {@link isBusinessReady} during bootstrap and recovery. Default 180s. */
68
+ businessReadyTimeoutMs?: number;
69
+ /** Extra hook after the initial business spawn (cooldown is recorded automatically). */
63
70
  onWorkerSpawned?: () => void;
71
+ beforeProcessExit?: () => void | Promise<void>;
72
+ }
73
+
74
+ /** True when the guard slice is serving traffic (`mode` omitted = legacy healthy). */
75
+ export function isGuardAvailable(guard: Pick<GuardRuntimeSlice, "mode">): boolean {
76
+ return guard.mode == null || guard.mode === "healthy";
77
+ }
78
+
79
+ /**
80
+ * Seconds for `Retry-After` when the guard is not serving traffic.
81
+ * `null` when {@link isGuardAvailable} is true.
82
+ */
83
+ export function guardRetryAfterSec(guard: Pick<GuardRuntimeSlice, "mode" | "silentBackoffMs">): number | null {
84
+ if (isGuardAvailable(guard)) {
85
+ return null;
86
+ }
87
+ const backoffMs = guard.silentBackoffMs > 0 ? guard.silentBackoffMs : 15_000;
88
+ return Math.max(1, Math.ceil(backoffMs / 1000));
64
89
  }
@@ -0,0 +1,33 @@
1
+ import { ServiceRegistryError } from "../../service/registry.service";
2
+ import { claimServiceRegistrySlot, type ServiceRegistrySession } from "../../service/registry-lifecycle";
3
+ import type { Service } from "../../service/service.interface";
4
+ import type { LoggerWithScope } from "../../shared/log";
5
+
6
+ export interface ClaimRegistrySlotOrExitInput {
7
+ serviceValue: string;
8
+ createInstance: () => Promise<Service>;
9
+ logger: LoggerWithScope;
10
+ logTopic: string;
11
+ }
12
+
13
+ /**
14
+ * Claim a registry slot, or `process.exit(1)` when no EMPTY slot remains.
15
+ * Other errors propagate.
16
+ */
17
+ export async function claimRegistrySlotOrExit(input: ClaimRegistrySlotOrExitInput): Promise<ServiceRegistrySession> {
18
+ try {
19
+ return await claimServiceRegistrySlot({
20
+ serviceValue: input.serviceValue,
21
+ createInstance: input.createInstance,
22
+ });
23
+ } catch (error) {
24
+ if (error instanceof ServiceRegistryError && error.code === "SERVICE_SLOTS_FULL") {
25
+ input.logger.critical("No EMPTY registry slot — refusing to start", {
26
+ topic: input.logTopic,
27
+ data: { serviceValue: input.serviceValue, code: error.code },
28
+ });
29
+ process.exit(1);
30
+ }
31
+ throw error;
32
+ }
33
+ }
@@ -0,0 +1,157 @@
1
+ import type { ChildProcess } from "node:child_process";
2
+ import type { ManagedChildProcesses } from "../process/managed-child-processes";
3
+ import type {
4
+ GuardRuntimeSlice,
5
+ SchedulerRuntimeSlice,
6
+ WorkerRuntimeSlice,
7
+ } from "../runtime-state/service-runtime-state.types";
8
+
9
+ const WORKER_SCRIPT = "./dist/processes/worker.js";
10
+ const GUARD_SCRIPT = "./dist/processes/guard.js";
11
+ const SCHEDULER_SCRIPT = "./dist/processes/scheduler.js";
12
+
13
+ export interface L3ChildLauncherRuntimeStore {
14
+ readRuntimeState(): Promise<{
15
+ worker: WorkerRuntimeSlice;
16
+ scheduler: SchedulerRuntimeSlice;
17
+ guard: GuardRuntimeSlice;
18
+ }>;
19
+ writeRuntimeState(patch: {
20
+ worker?: Partial<WorkerRuntimeSlice>;
21
+ scheduler?: Partial<SchedulerRuntimeSlice>;
22
+ guard?: Partial<GuardRuntimeSlice>;
23
+ }): Promise<unknown>;
24
+ }
25
+
26
+ export interface CreateL3ChildLauncherOptions {
27
+ childProcesses: ManagedChildProcesses;
28
+ readRuntimeState: L3ChildLauncherRuntimeStore["readRuntimeState"];
29
+ writeRuntimeState: L3ChildLauncherRuntimeStore["writeRuntimeState"];
30
+ /** Merged onto the child env. Default: `{ LOG_PERSIST_PROCESS: label }`. */
31
+ spawnEnv?: (label: string) => Record<string, string>;
32
+ }
33
+
34
+ export interface L3ChildLauncher {
35
+ spawnGuard: () => ChildProcess;
36
+ spawnTaskWorker: (reason: string) => Promise<ChildProcess>;
37
+ spawnScheduler: (reason: string) => Promise<ChildProcess>;
38
+ spawnBusinessNodes: (reason: string) => Promise<void>;
39
+ stopBusinessNodes: () => Promise<void>;
40
+ isBusinessReady: () => Promise<boolean>;
41
+ stopChildProcesses: () => Promise<void>;
42
+ }
43
+
44
+ function defaultSpawnEnv(label: string): Record<string, string> {
45
+ return { LOG_PERSIST_PROCESS: label };
46
+ }
47
+
48
+ /**
49
+ * Guard-owned scheduler + worker spawn/stop, plus main-owned Guard spawn.
50
+ * `stopBusinessNodes` must run in the Guard process (that instance's children only).
51
+ * `stopChildProcesses` is for main shutdown (`extraPids` for Guard-spawned PIDs).
52
+ */
53
+ export function createL3ChildLauncher(options: CreateL3ChildLauncherOptions): L3ChildLauncher {
54
+ const { childProcesses, readRuntimeState, writeRuntimeState } = options;
55
+ const spawnEnv = options.spawnEnv ?? defaultSpawnEnv;
56
+
57
+ function nowIso(): string {
58
+ return new Date().toISOString();
59
+ }
60
+
61
+ async function spawnTaskWorker(reason: string): Promise<ChildProcess> {
62
+ const { child, created } = childProcesses.spawn("worker", WORKER_SCRIPT, {
63
+ env: spawnEnv("worker"),
64
+ });
65
+ if (!created) {
66
+ return child;
67
+ }
68
+
69
+ const current = await readRuntimeState();
70
+ await writeRuntimeState({
71
+ worker: {
72
+ pid: child.pid ?? null,
73
+ spawnedAt: nowIso(),
74
+ ready: false,
75
+ registeredTasks: [],
76
+ readyAt: null,
77
+ },
78
+ guard: {
79
+ lastSpawnAt: nowIso(),
80
+ lastDecision: reason,
81
+ spawnCount: current.guard.spawnCount + 1,
82
+ },
83
+ });
84
+
85
+ return child;
86
+ }
87
+
88
+ function spawnGuard(): ChildProcess {
89
+ return childProcesses.spawn("guard", GUARD_SCRIPT, { env: spawnEnv("guard") }).child;
90
+ }
91
+
92
+ async function spawnScheduler(reason: string): Promise<ChildProcess> {
93
+ const { child, created } = childProcesses.spawn("scheduler", SCHEDULER_SCRIPT, {
94
+ env: spawnEnv("scheduler"),
95
+ });
96
+ if (!created) {
97
+ return child;
98
+ }
99
+
100
+ await writeRuntimeState({
101
+ scheduler: {
102
+ pid: child.pid ?? null,
103
+ spawnedAt: nowIso(),
104
+ ready: false,
105
+ readyAt: null,
106
+ },
107
+ guard: {
108
+ lastDecision: reason,
109
+ },
110
+ });
111
+
112
+ return child;
113
+ }
114
+
115
+ async function spawnBusinessNodes(reason: string): Promise<void> {
116
+ await spawnScheduler(reason);
117
+ await spawnTaskWorker(reason);
118
+ }
119
+
120
+ async function stopBusinessNodes(): Promise<void> {
121
+ await childProcesses.stopAll();
122
+ await writeRuntimeState({
123
+ worker: {
124
+ pid: null,
125
+ ready: false,
126
+ readyAt: null,
127
+ registeredTasks: [],
128
+ },
129
+ scheduler: {
130
+ pid: null,
131
+ ready: false,
132
+ readyAt: null,
133
+ },
134
+ });
135
+ }
136
+
137
+ async function isBusinessReady(): Promise<boolean> {
138
+ const state = await readRuntimeState();
139
+ return state.worker.ready && state.scheduler.ready;
140
+ }
141
+
142
+ async function stopChildProcesses(): Promise<void> {
143
+ const state = await readRuntimeState();
144
+ const extraPids = [state.worker.pid, state.scheduler.pid].filter((pid): pid is number => pid != null);
145
+ await childProcesses.stopAll({ extraPids });
146
+ }
147
+
148
+ return {
149
+ spawnGuard,
150
+ spawnTaskWorker,
151
+ spawnScheduler,
152
+ spawnBusinessNodes,
153
+ stopBusinessNodes,
154
+ isBusinessReady,
155
+ stopChildProcesses,
156
+ };
157
+ }
@@ -1,24 +1,9 @@
1
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";
2
+ CreateL3ChildLauncherOptions,
3
+ L3ChildLauncher,
4
+ L3ChildLauncherRuntimeStore,
5
+ } from "./create-l3-child-launcher";
6
+ export { createL3ChildLauncher } from "./create-l3-child-launcher";
22
7
  export type {
23
8
  ServiceHost,
24
9
  ServiceHostClosable,
@@ -1,7 +1,12 @@
1
- import { createLogger, type Service, ServiceRegistryError } from "../../browser";
2
- import { claimServiceRegistrySlot, type ServiceRegistrySession } from "../../service/registry-lifecycle";
3
- import type { LoggerWithScope } from "../../shared/log";
1
+ import { ServiceRegistryError } from "../../service/registry.service";
2
+ import type { ServiceRegistrySession } from "../../service/registry-lifecycle";
3
+ import type { Service } from "../../service/service.interface";
4
+ import { createLogger, type LoggerWithScope } from "../../shared/log";
5
+ import { LOG_SPOOL_SERVICE_ID_ENV } from "../../shared/log/spool/config";
6
+ import { ensureLogSpoolFromEnv } from "../../shared/log/spool/enable";
7
+ import { logSpoolEnabledFromEnv } from "../../shared/log/upload/env";
4
8
  import type { ManagedChildProcesses } from "../process/managed-child-processes";
9
+ import { claimRegistrySlotOrExit } from "./claim-registry-slot";
5
10
 
6
11
  export interface ServiceHostClosable {
7
12
  close: () => Promise<void>;
@@ -10,6 +15,8 @@ export interface ServiceHostClosable {
10
15
  export interface ServiceHostContext {
11
16
  service: Service;
12
17
  session: ServiceRegistrySession;
18
+ /** Registry instance id — same as `service.id`; children inherit via spawn env injection. */
19
+ serviceId: string;
13
20
  }
14
21
 
15
22
  export interface ServiceHostOptions {
@@ -23,9 +30,11 @@ export interface ServiceHostOptions {
23
30
  prepare?: () => Promise<void>;
24
31
  createInstance: () => Promise<Service | { service: Service; baseUrl?: string }>;
25
32
  onRegistryClaimed?: (ctx: ServiceHostContext) => Promise<void>;
26
- startSupervisors?: () => void | Promise<void>;
33
+ startSupervisors?: (ctx: ServiceHostContext) => void | Promise<void>;
27
34
  waitUntilReady?: () => Promise<void>;
28
35
  startServer?: () => Promise<ServiceHostClosable>;
36
+ /** When true (default), enable file log spool for main after registry claim. */
37
+ enableLogSpoolAfterClaim?: boolean;
29
38
  /** Stop child processes and other local resources (not registry release). */
30
39
  onShutdown?: (signal: string) => Promise<void>;
31
40
  }
@@ -105,28 +114,31 @@ export function createServiceHost(options: ServiceHostOptions): ServiceHost {
105
114
  await options.prepare();
106
115
  }
107
116
 
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
- }
117
+ session = await claimRegistrySlotOrExit({
118
+ serviceValue: options.serviceValue,
119
+ createInstance: async () => unwrapService(await options.createInstance()),
120
+ logger,
121
+ logTopic,
122
+ });
123
+
124
+ const hostCtx: ServiceHostContext = { service: session.service, session, serviceId: session.service.id };
123
125
 
124
126
  if (options.onRegistryClaimed != null) {
125
- await options.onRegistryClaimed({ service: session.service, session });
127
+ await options.onRegistryClaimed(hostCtx);
128
+ }
129
+
130
+ const enableSpool = options.enableLogSpoolAfterClaim ?? true;
131
+ if (enableSpool && logSpoolEnabledFromEnv()) {
132
+ process.env[LOG_SPOOL_SERVICE_ID_ENV] = session.service.id;
133
+ await ensureLogSpoolFromEnv({
134
+ serviceId: session.service.id,
135
+ serviceValue: options.serviceValue,
136
+ processRole: "main",
137
+ });
126
138
  }
127
139
 
128
140
  if (options.startSupervisors != null) {
129
- await options.startSupervisors();
141
+ await options.startSupervisors(hostCtx);
130
142
  }
131
143
 
132
144
  if (options.waitUntilReady != null) {