target-supabase-sdk 0.2.2 → 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 (168) 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-BH-tbKEj.js → enable-log-persist-B8rg_wXG.js} +41 -8
  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/env/index.d.ts +2 -2
  18. package/dist/node/env/index.d.ts.map +1 -1
  19. package/dist/node/env/init-supabase-from-env.d.ts +1 -1
  20. package/dist/node/env/init-supabase-from-env.d.ts.map +1 -1
  21. package/dist/node/env/load-env.d.ts +15 -1
  22. package/dist/node/env/load-env.d.ts.map +1 -1
  23. package/dist/node/node-runtime.base.d.ts +13 -1
  24. package/dist/node/node-runtime.base.d.ts.map +1 -1
  25. package/dist/node/preload/run-service-preload.d.ts +1 -1
  26. package/dist/node/preload/run-service-preload.d.ts.map +1 -1
  27. package/dist/node/process/create-managed-child-processes.d.ts +12 -0
  28. package/dist/node/process/create-managed-child-processes.d.ts.map +1 -0
  29. package/dist/node/process/index.d.ts +3 -1
  30. package/dist/node/process/index.d.ts.map +1 -1
  31. package/dist/node/process/managed-child-processes.d.ts +15 -2
  32. package/dist/node/process/managed-child-processes.d.ts.map +1 -1
  33. package/dist/node/runtime-state/create-service-runtime-state-store.d.ts +23 -0
  34. package/dist/node/runtime-state/create-service-runtime-state-store.d.ts.map +1 -0
  35. package/dist/node/runtime-state/index.d.ts +4 -0
  36. package/dist/node/runtime-state/index.d.ts.map +1 -0
  37. package/dist/node/runtime-state/service-runtime-state.types.d.ts +57 -0
  38. package/dist/node/runtime-state/service-runtime-state.types.d.ts.map +1 -0
  39. package/dist/node/service-guard/index.d.ts +9 -0
  40. package/dist/node/service-guard/index.d.ts.map +1 -0
  41. package/dist/node/service-guard/register-service-guard-runner.d.ts +9 -0
  42. package/dist/node/service-guard/register-service-guard-runner.d.ts.map +1 -0
  43. package/dist/node/service-guard/run-readiness-gate.d.ts +11 -0
  44. package/dist/node/service-guard/run-readiness-gate.d.ts.map +1 -0
  45. package/dist/node/service-guard/run-service-guard-tick.d.ts +7 -0
  46. package/dist/node/service-guard/run-service-guard-tick.d.ts.map +1 -0
  47. package/dist/node/service-guard/service-guard-node.d.ts +18 -0
  48. package/dist/node/service-guard/service-guard-node.d.ts.map +1 -0
  49. package/dist/node/service-guard/service-guard.interface.d.ts +58 -0
  50. package/dist/node/service-guard/service-guard.interface.d.ts.map +1 -0
  51. package/dist/node/service-guard/worker-spawn-cooldown.d.ts +4 -0
  52. package/dist/node/service-guard/worker-spawn-cooldown.d.ts.map +1 -0
  53. package/dist/node/service-host/index.d.ts +9 -0
  54. package/dist/node/service-host/index.d.ts.map +1 -0
  55. package/dist/node/service-host/registry-slot-guard-step.d.ts +21 -0
  56. package/dist/node/service-host/registry-slot-guard-step.d.ts.map +1 -0
  57. package/dist/node/service-host/service-host.d.ts +36 -0
  58. package/dist/node/service-host/service-host.d.ts.map +1 -0
  59. package/dist/node/service-host/single-process-service.d.ts +21 -0
  60. package/dist/node/service-host/single-process-service.d.ts.map +1 -0
  61. package/dist/node.d.ts +17 -4
  62. package/dist/node.d.ts.map +1 -1
  63. package/dist/node.js +2088 -1495
  64. package/dist/node.js.map +1 -1
  65. package/dist/preload.js +5 -4
  66. package/dist/preload.js.map +1 -1
  67. package/dist/service/config.api.d.ts +143 -0
  68. package/dist/service/config.api.d.ts.map +1 -0
  69. package/dist/service/config.interface.d.ts +22 -0
  70. package/dist/service/config.interface.d.ts.map +1 -0
  71. package/dist/service/index.d.ts +16 -17
  72. package/dist/service/index.d.ts.map +1 -1
  73. package/dist/service/registry-lifecycle.d.ts +43 -0
  74. package/dist/service/registry-lifecycle.d.ts.map +1 -0
  75. package/dist/service/registry.service.d.ts +94 -0
  76. package/dist/service/registry.service.d.ts.map +1 -0
  77. package/dist/service/service-bootstrap.d.ts +28 -0
  78. package/dist/service/service-bootstrap.d.ts.map +1 -0
  79. package/dist/service/service.api.d.ts +160 -0
  80. package/dist/service/service.api.d.ts.map +1 -1
  81. package/dist/service/service.interface.d.ts +32 -1
  82. package/dist/service/service.interface.d.ts.map +1 -1
  83. package/dist/shared/log/index.d.ts +4 -0
  84. package/dist/shared/log/index.d.ts.map +1 -1
  85. package/dist/shared/log/log-batch.d.ts +44 -0
  86. package/dist/shared/log/log-batch.d.ts.map +1 -0
  87. package/dist/shared/log/log-persist-coordinator.d.ts +37 -0
  88. package/dist/shared/log/log-persist-coordinator.d.ts.map +1 -0
  89. package/dist/shared/log/log-persist-flush.d.ts.map +1 -1
  90. package/dist/shared/log/log-persist.config.d.ts +0 -1
  91. package/dist/shared/log/log-persist.config.d.ts.map +1 -1
  92. package/dist/shared/log/log-persist.interface.d.ts +8 -0
  93. package/dist/shared/log/log-persist.interface.d.ts.map +1 -1
  94. package/dist/shared/utils/config-path.utils.d.ts +21 -0
  95. package/dist/shared/utils/config-path.utils.d.ts.map +1 -1
  96. package/dist/shared/utils/get-value-at-path.d.ts +3 -0
  97. package/dist/shared/utils/get-value-at-path.d.ts.map +1 -0
  98. package/dist/task/index.d.ts +2 -0
  99. package/dist/task/index.d.ts.map +1 -1
  100. package/dist/task/task-manager.d.ts +1 -1
  101. package/dist/task/task-manager.d.ts.map +1 -1
  102. package/dist/task/task-post-retry.api.d.ts +24 -0
  103. package/dist/task/task-post-retry.api.d.ts.map +1 -0
  104. package/dist/task/task-repo-context.d.ts +7 -1
  105. package/dist/task/task-repo-context.d.ts.map +1 -1
  106. package/dist/trigger/trigger-manager.d.ts +1 -1
  107. package/dist/trigger/trigger-manager.d.ts.map +1 -1
  108. package/dist/trigger/trigger-node.d.ts +1 -0
  109. package/dist/trigger/trigger-node.d.ts.map +1 -1
  110. package/dist/trigger/trigger.constant.d.ts +1 -1
  111. package/dist/trigger/trigger.constant.d.ts.map +1 -1
  112. package/dist/trigger/trigger.interface.d.ts +10 -0
  113. package/dist/trigger/trigger.interface.d.ts.map +1 -1
  114. package/package.json +4 -2
  115. package/src/browser.ts +98 -4
  116. package/src/extraction/extraction.api.ts +232 -0
  117. package/src/extraction/extraction.interface.ts +45 -0
  118. package/src/list/list.interface.ts +2 -2
  119. package/src/node/env/index.ts +13 -2
  120. package/src/node/env/init-supabase-from-env.ts +1 -1
  121. package/src/node/env/load-env.ts +48 -2
  122. package/src/node/node-runtime.base.ts +37 -6
  123. package/src/node/preload/run-service-preload.ts +3 -2
  124. package/src/node/process/create-managed-child-processes.ts +27 -0
  125. package/src/node/process/index.ts +4 -0
  126. package/src/node/process/managed-child-processes.ts +48 -15
  127. package/src/node/runtime-state/create-service-runtime-state-store.ts +125 -0
  128. package/src/node/runtime-state/index.ts +17 -0
  129. package/src/node/runtime-state/service-runtime-state.types.ts +64 -0
  130. package/src/node/service-guard/index.ts +17 -0
  131. package/src/node/service-guard/register-service-guard-runner.ts +64 -0
  132. package/src/node/service-guard/run-readiness-gate.ts +34 -0
  133. package/src/node/service-guard/run-service-guard-tick.ts +106 -0
  134. package/src/node/service-guard/service-guard-node.ts +52 -0
  135. package/src/node/service-guard/service-guard.interface.ts +64 -0
  136. package/src/node/service-guard/worker-spawn-cooldown.ts +13 -0
  137. package/src/node/service-host/index.ts +30 -0
  138. package/src/node/service-host/registry-slot-guard-step.ts +57 -0
  139. package/src/node/service-host/service-host.ts +168 -0
  140. package/src/node/service-host/single-process-service.ts +58 -0
  141. package/src/node.ts +76 -0
  142. package/src/service/config.api.ts +224 -0
  143. package/src/service/config.interface.ts +25 -0
  144. package/src/service/index.ts +74 -149
  145. package/src/service/registry-lifecycle.ts +144 -0
  146. package/src/service/registry.service.ts +564 -0
  147. package/src/service/service-bootstrap.ts +74 -0
  148. package/src/service/service.api.ts +13 -0
  149. package/src/service/service.interface.ts +36 -1
  150. package/src/shared/log/index.ts +13 -0
  151. package/src/shared/log/log-batch.ts +156 -0
  152. package/src/shared/log/log-persist-coordinator.ts +107 -0
  153. package/src/shared/log/log-persist-flush.ts +3 -3
  154. package/src/shared/log/log-persist.config.ts +0 -2
  155. package/src/shared/log/log-persist.interface.ts +10 -0
  156. package/src/shared/utils/config-path.utils.ts +35 -0
  157. package/src/shared/utils/get-value-at-path.ts +17 -0
  158. package/src/task/index.ts +2 -0
  159. package/src/task/task-manager.ts +10 -4
  160. package/src/task/task-post-retry.api.ts +61 -0
  161. package/src/task/task-repo-context.ts +8 -1
  162. package/src/trigger/trigger-manager.ts +4 -2
  163. package/src/trigger/trigger-node.ts +10 -4
  164. package/src/trigger/trigger.constant.ts +1 -1
  165. package/src/trigger/trigger.interface.ts +10 -0
  166. package/dist/chunks/create-logger-DyH3mZ8p.js.map +0 -1
  167. package/dist/chunks/enable-log-persist-BH-tbKEj.js.map +0 -1
  168. package/dist/chunks/word.interface-DLgRobKV.js.map +0 -1
@@ -0,0 +1,564 @@
1
+ /**
2
+ * System registry — declarative service capacity (`target-system-registry` Config)
3
+ * and startup/shutdown slot claims (`registerService` / `unregisterService`).
4
+ *
5
+ * Data planes:
6
+ * - Config `details.objects` — {@link ServiceSlot} desired layout (EMPTY / ACTIVE)
7
+ * - Service rows — runtime instances; guard maintains {@link ServiceDetails.runtime}
8
+ * - Slot release: graceful shutdown via {@link unregisterService}; monitor (later)
9
+ * for crashed / ungraceful exits (stale `Service.details.runtime`)
10
+ */
11
+
12
+ import { isOptimisticLockError, updateTargetDetails } from "../core.api";
13
+ import type { Node } from "../node/node.interface";
14
+ import { createLogger } from "../shared/log";
15
+ import { getConfig } from "./config.api";
16
+ import { type Config, type ConfigDetails, TARGET_SYSTEM_REGISTRY_KEY } from "./config.interface";
17
+ import { getService } from "./service.api";
18
+ import {
19
+ type Service,
20
+ type ServiceDetails,
21
+ type ServiceNodeSnapshot,
22
+ type ServiceSlot,
23
+ ServiceSlotStatus,
24
+ } from "./service.interface";
25
+
26
+ export { TARGET_SYSTEM_REGISTRY_KEY } from "./config.interface";
27
+
28
+ /** `details.meta.revision` on the system registry Config — optimistic-lock token. */
29
+ interface RegistryConfigMeta {
30
+ revision: number;
31
+ }
32
+
33
+ const LOG_TOPIC_REGISTRY = "service-registry";
34
+ const DEFAULT_REGISTER_RETRY_ATTEMPTS = 5;
35
+
36
+ export interface TargetSystemRegistrySlotView {
37
+ slot: ServiceSlot;
38
+ /** Resolved when `slot.serviceId` is set; null for EMPTY slots. */
39
+ service: Service | null;
40
+ }
41
+
42
+ export interface TargetSystemRegistryView {
43
+ config: Config;
44
+ slots: TargetSystemRegistrySlotView[];
45
+ }
46
+
47
+ export interface RegisterServiceInput {
48
+ /** Registered runtime Service row (`category=service`). */
49
+ service: Service;
50
+ traceId?: string;
51
+ maxAttempts?: number;
52
+ }
53
+
54
+ export class ServiceRegistryError extends Error {
55
+ constructor(
56
+ message: string,
57
+ readonly code:
58
+ | "REGISTRY_CONFIG_NOT_FOUND"
59
+ | "SERVICE_NOT_DECLARED"
60
+ | "SERVICE_SLOTS_FULL"
61
+ | "REGISTRY_SLOT_LOST"
62
+ | "REGISTRY_UPDATE_FAILED",
63
+ ) {
64
+ super(message);
65
+ this.name = "ServiceRegistryError";
66
+ }
67
+ }
68
+
69
+ function asRecord(value: unknown): Record<string, unknown> | null {
70
+ return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : null;
71
+ }
72
+
73
+ function asFiniteNumber(value: unknown): number | null {
74
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
75
+ }
76
+
77
+ function isServiceSlotStatus(value: unknown): value is ServiceSlotStatus {
78
+ return value === ServiceSlotStatus.EMPTY || value === ServiceSlotStatus.ACTIVE;
79
+ }
80
+
81
+ /** Defensive decode of one {@link ServiceSlot} from Config `details.objects`. */
82
+ export function parseServiceSlot(value: unknown): ServiceSlot | null {
83
+ const source = asRecord(value);
84
+ if (source == null) {
85
+ return null;
86
+ }
87
+
88
+ const serviceValue = typeof source.serviceValue === "string" ? source.serviceValue : null;
89
+ const status = source.status;
90
+ if (serviceValue == null || !isServiceSlotStatus(status)) {
91
+ return null;
92
+ }
93
+
94
+ let serviceId: string | null = null;
95
+ if (source.serviceId != null) {
96
+ if (typeof source.serviceId !== "string" || source.serviceId === "") {
97
+ return null;
98
+ }
99
+ serviceId = source.serviceId;
100
+ }
101
+
102
+ if (status === ServiceSlotStatus.EMPTY) {
103
+ serviceId = null;
104
+ } else if (serviceId == null) {
105
+ return null;
106
+ }
107
+
108
+ return { serviceValue, serviceId, status };
109
+ }
110
+
111
+ export function parseServiceSlots(config: Config): ServiceSlot[] {
112
+ const slots: ServiceSlot[] = [];
113
+ for (const object of config.details.objects) {
114
+ const slot = parseServiceSlot(object);
115
+ if (slot != null) {
116
+ slots.push(slot);
117
+ }
118
+ }
119
+ return slots;
120
+ }
121
+
122
+ /** Parse `details.meta.revision` from the system registry Config (defaults to 0). */
123
+ function parseRegistryRevision(rawMeta: unknown): number {
124
+ const source = asRecord(rawMeta);
125
+ const revision = asFiniteNumber(source?.revision);
126
+ return revision ?? 0;
127
+ }
128
+
129
+ function nextRegistryMeta(nextRevision: number): RegistryConfigMeta {
130
+ return { revision: nextRevision };
131
+ }
132
+
133
+ function lockOnRegistryRevision(revision: number) {
134
+ return [{ field: "details->meta->>revision", operator: "eq" as const, value: String(revision) }];
135
+ }
136
+
137
+ function sleep(ms: number): Promise<void> {
138
+ return new Promise((resolve) => setTimeout(resolve, ms));
139
+ }
140
+
141
+ async function loadSystemRegistryConfig(): Promise<Config> {
142
+ const response = await getConfig({ value: TARGET_SYSTEM_REGISTRY_KEY });
143
+ if (response.data == null) {
144
+ throw new ServiceRegistryError(
145
+ `System registry config not found (value=${TARGET_SYSTEM_REGISTRY_KEY})`,
146
+ "REGISTRY_CONFIG_NOT_FOUND",
147
+ );
148
+ }
149
+ return response.data;
150
+ }
151
+
152
+ async function resolveServiceById(serviceId: string): Promise<Service | null> {
153
+ const response = await getService({ id: serviceId });
154
+ return response.data ?? null;
155
+ }
156
+
157
+ /**
158
+ * Load the global system registry Config and resolve each ACTIVE slot's Service row.
159
+ * Read-only — does not mutate slots or heartbeats.
160
+ */
161
+ export async function getTargetSystemRegistry(): Promise<TargetSystemRegistryView> {
162
+ const config = await loadSystemRegistryConfig();
163
+ const slots = parseServiceSlots(config);
164
+
165
+ const views = await Promise.all(
166
+ slots.map(async (slot): Promise<TargetSystemRegistrySlotView> => {
167
+ if (slot.serviceId == null) {
168
+ return { slot, service: null };
169
+ }
170
+ const service = await resolveServiceById(slot.serviceId);
171
+ return { slot, service };
172
+ }),
173
+ );
174
+
175
+ return { config, slots: views };
176
+ }
177
+
178
+ function findFirstEmptySlot(slots: ServiceSlot[], serviceValue: string): number {
179
+ return slots.findIndex((slot) => slot.serviceValue === serviceValue && slot.status === ServiceSlotStatus.EMPTY);
180
+ }
181
+
182
+ function countSlotsForValue(
183
+ slots: ServiceSlot[],
184
+ serviceValue: string,
185
+ ): { declared: number; active: number; empty: number } {
186
+ let declared = 0;
187
+ let active = 0;
188
+ let empty = 0;
189
+ for (const slot of slots) {
190
+ if (slot.serviceValue !== serviceValue) {
191
+ continue;
192
+ }
193
+ declared++;
194
+ if (slot.status === ServiceSlotStatus.ACTIVE) {
195
+ active++;
196
+ } else if (slot.status === ServiceSlotStatus.EMPTY) {
197
+ empty++;
198
+ }
199
+ }
200
+ return { declared, active, empty };
201
+ }
202
+
203
+ /**
204
+ * Preflight before `postService` — fail fast when registry capacity is full.
205
+ * Prevents orphan Service rows and child process spawn when no slot is available.
206
+ */
207
+ export async function assertRegistrySlotAvailable(serviceValue: string): Promise<void> {
208
+ const config = await loadSystemRegistryConfig();
209
+ const slots = parseServiceSlots(config);
210
+ const counts = countSlotsForValue(slots, serviceValue);
211
+
212
+ if (counts.declared === 0) {
213
+ throw new ServiceRegistryError(
214
+ `Service ${serviceValue} is not declared in system registry config`,
215
+ "SERVICE_NOT_DECLARED",
216
+ );
217
+ }
218
+
219
+ if (counts.empty === 0) {
220
+ throw new ServiceRegistryError(
221
+ `No EMPTY slot for ${serviceValue} — declared=${counts.declared}, active=${counts.active}. Refusing startup.`,
222
+ "SERVICE_SLOTS_FULL",
223
+ );
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Runtime guard: this process's Service instance must still own an ACTIVE registry slot.
229
+ * Supports N>1 replicas — checks whether `serviceId` appears in any ACTIVE slot for
230
+ * `serviceValue`, not merely the first ACTIVE slot.
231
+ */
232
+ export async function assertRegistrySlotOwner(input: { serviceValue: string; serviceId: string }): Promise<void> {
233
+ const config = await loadSystemRegistryConfig();
234
+ const slots = parseServiceSlots(config);
235
+ const ownsSlot = slots.some(
236
+ (slot) =>
237
+ slot.serviceValue === input.serviceValue &&
238
+ slot.status === ServiceSlotStatus.ACTIVE &&
239
+ slot.serviceId === input.serviceId,
240
+ );
241
+ if (ownsSlot) {
242
+ return;
243
+ }
244
+ const activeId = slots.find(
245
+ (slot) =>
246
+ slot.serviceValue === input.serviceValue &&
247
+ slot.status === ServiceSlotStatus.ACTIVE &&
248
+ slot.serviceId != null,
249
+ )?.serviceId;
250
+ throw new ServiceRegistryError(
251
+ `Registry slot for ${input.serviceValue} is not owned by ${input.serviceId} (active=${activeId ?? "none"})`,
252
+ "REGISTRY_SLOT_LOST",
253
+ );
254
+ }
255
+
256
+ function isServiceValueDeclared(slots: ServiceSlot[], serviceValue: string): boolean {
257
+ return slots.some((slot) => slot.serviceValue === serviceValue);
258
+ }
259
+
260
+ function claimSlot(slots: ServiceSlot[], slotIndex: number, serviceId: string): ServiceSlot[] {
261
+ const next = [...slots];
262
+ next[slotIndex] = {
263
+ ...next[slotIndex],
264
+ serviceId,
265
+ status: ServiceSlotStatus.ACTIVE,
266
+ };
267
+ return next;
268
+ }
269
+
270
+ async function persistSlotClaim(params: { configId: string; revision: number; slots: ServiceSlot[] }): Promise<void> {
271
+ const { configId, revision, slots } = params;
272
+
273
+ await updateTargetDetails<Config, ConfigDetails>({
274
+ id: configId,
275
+ optimisticLockFilterList: lockOnRegistryRevision(revision),
276
+ updateFn: (details) => ({
277
+ ...details,
278
+ meta: nextRegistryMeta(revision + 1),
279
+ objects: slots,
280
+ }),
281
+ });
282
+ }
283
+
284
+ function releaseOwnedSlot(slots: ServiceSlot[], serviceId: string): ServiceSlot[] | null {
285
+ const slotIndex = slots.findIndex(
286
+ (slot) => slot.serviceId === serviceId && slot.status === ServiceSlotStatus.ACTIVE,
287
+ );
288
+ if (slotIndex < 0) {
289
+ return null;
290
+ }
291
+
292
+ const next = [...slots];
293
+ next[slotIndex] = {
294
+ ...next[slotIndex],
295
+ serviceId: null,
296
+ status: ServiceSlotStatus.EMPTY,
297
+ };
298
+ return next;
299
+ }
300
+
301
+ /**
302
+ * Claim one EMPTY slot for a running Service instance (`EMPTY → ACTIVE` only).
303
+ *
304
+ * - Each startup should `postService` a **new** instance row; do not reuse prior Service ids.
305
+ * - Declared capacity = count of {@link ServiceSlot} rows sharing `service.value`
306
+ * - No idempotent skip — if all slots are ACTIVE (e.g. crashed prior instance), throws
307
+ * `SERVICE_SLOTS_FULL` until monitor/ops releases the slot
308
+ * - Does not write heartbeats; guard maintains {@link ServiceDetails.runtime}
309
+ * - Graceful release: {@link unregisterService}; crash recovery: future monitor
310
+ */
311
+ export async function registerService(input: RegisterServiceInput): Promise<void> {
312
+ const { service } = input;
313
+ const serviceValue = service.value;
314
+ const maxAttempts = input.maxAttempts ?? DEFAULT_REGISTER_RETRY_ATTEMPTS;
315
+ const logger = createLogger({
316
+ module: "registerService",
317
+ traceId: input.traceId,
318
+ labels: { serviceValue, serviceId: service.id },
319
+ });
320
+
321
+ logger.info("开始认领系统 registry 槽位", {
322
+ topic: LOG_TOPIC_REGISTRY,
323
+ data: { serviceValue, serviceId: service.id },
324
+ });
325
+
326
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
327
+ try {
328
+ const config = await loadSystemRegistryConfig();
329
+ const revision = parseRegistryRevision(config.details.meta);
330
+ const slots = parseServiceSlots(config);
331
+
332
+ if (!isServiceValueDeclared(slots, serviceValue)) {
333
+ throw new ServiceRegistryError(
334
+ `Service ${serviceValue} is not declared in system registry config`,
335
+ "SERVICE_NOT_DECLARED",
336
+ );
337
+ }
338
+
339
+ const emptyIndex = findFirstEmptySlot(slots, serviceValue);
340
+ if (emptyIndex < 0) {
341
+ throw new ServiceRegistryError(
342
+ `No EMPTY slot for ${serviceValue} — declared capacity is full`,
343
+ "SERVICE_SLOTS_FULL",
344
+ );
345
+ }
346
+
347
+ const nextSlots = claimSlot(slots, emptyIndex, service.id);
348
+ await persistSlotClaim({
349
+ configId: config.id,
350
+ revision,
351
+ slots: nextSlots,
352
+ });
353
+
354
+ logger.info("槽位认领成功", {
355
+ topic: LOG_TOPIC_REGISTRY,
356
+ data: {
357
+ serviceValue,
358
+ serviceId: service.id,
359
+ slotIndex: emptyIndex,
360
+ attempt,
361
+ revision: revision + 1,
362
+ },
363
+ });
364
+ return;
365
+ } catch (error) {
366
+ if (error instanceof ServiceRegistryError) {
367
+ logger.error("槽位认领被拒绝", {
368
+ topic: LOG_TOPIC_REGISTRY,
369
+ data: { serviceValue, serviceId: service.id, code: error.code, message: error.message },
370
+ });
371
+ throw error;
372
+ }
373
+
374
+ if (isOptimisticLockError(error) && attempt < maxAttempts) {
375
+ logger.warn("registry 乐观锁冲突,重试", {
376
+ topic: LOG_TOPIC_REGISTRY,
377
+ data: { serviceValue, serviceId: service.id, attempt, maxAttempts },
378
+ });
379
+ await sleep(50 * attempt);
380
+ continue;
381
+ }
382
+
383
+ const message = error instanceof Error ? error.message : String(error);
384
+ logger.error("槽位认领失败", {
385
+ topic: LOG_TOPIC_REGISTRY,
386
+ data: { serviceValue, serviceId: service.id, attempt, message },
387
+ });
388
+ throw new ServiceRegistryError(message, "REGISTRY_UPDATE_FAILED");
389
+ }
390
+ }
391
+
392
+ throw new ServiceRegistryError(
393
+ `Failed to register ${serviceValue} after ${maxAttempts} attempts`,
394
+ "REGISTRY_UPDATE_FAILED",
395
+ );
396
+ }
397
+
398
+ /** Resolve the ACTIVE registry slot id for a declared `serviceValue`. */
399
+ export async function resolveActiveRegistryServiceId(serviceValue: string): Promise<string | null> {
400
+ const registry = await getTargetSystemRegistry();
401
+ for (const { slot } of registry.slots) {
402
+ if (slot.serviceValue === serviceValue && slot.status === ServiceSlotStatus.ACTIVE && slot.serviceId != null) {
403
+ return slot.serviceId;
404
+ }
405
+ }
406
+ return null;
407
+ }
408
+
409
+ function nodeToServiceNodeSnapshot(node: Node): ServiceNodeSnapshot {
410
+ return {
411
+ nodeId: node.id,
412
+ status: node.details.status,
413
+ lastHeartBeat: node.details.lastHeartBeat,
414
+ };
415
+ }
416
+
417
+ export interface PatchServiceRuntimeInput {
418
+ serviceValue: string;
419
+ /** Prefer explicit id (N>1 replicas); falls back to first ACTIVE slot when omitted. */
420
+ serviceId?: string;
421
+ nodes: readonly Node[];
422
+ lastHeartBeat: number;
423
+ }
424
+
425
+ /** Roll up Node liveness into `Service.details.runtime` (service guard only). */
426
+ export async function patchServiceRuntime(input: PatchServiceRuntimeInput): Promise<void> {
427
+ const serviceId = input.serviceId ?? (await resolveActiveRegistryServiceId(input.serviceValue));
428
+ if (serviceId == null) {
429
+ return;
430
+ }
431
+
432
+ const nodes = input.nodes.map(nodeToServiceNodeSnapshot);
433
+
434
+ await updateTargetDetails<Service, ServiceDetails>({
435
+ id: serviceId,
436
+ updateFn: (details) => ({
437
+ ...details,
438
+ runtime: {
439
+ lastHeartBeat: input.lastHeartBeat,
440
+ nodes,
441
+ },
442
+ }),
443
+ });
444
+ }
445
+
446
+ /**
447
+ * Claim a registry slot at L3 startup — propagates {@link ServiceRegistryError} (e.g. `SERVICE_SLOTS_FULL`).
448
+ */
449
+ export async function registerServiceAtStartup(input: RegisterServiceInput): Promise<void> {
450
+ await registerService(input);
451
+ }
452
+
453
+ /**
454
+ * Release the ACTIVE slot owned by this Service instance (`ACTIVE → EMPTY` only).
455
+ *
456
+ * - Idempotent when no ACTIVE slot is bound to `service.id`
457
+ * - Does not mutate the Service row or `details.runtime`
458
+ * - Does not release slots owned by other instances
459
+ */
460
+ export async function unregisterService(input: RegisterServiceInput): Promise<void> {
461
+ const { service } = input;
462
+ const serviceValue = service.value;
463
+ const maxAttempts = input.maxAttempts ?? DEFAULT_REGISTER_RETRY_ATTEMPTS;
464
+ const logger = createLogger({
465
+ module: "unregisterService",
466
+ traceId: input.traceId,
467
+ labels: { serviceValue, serviceId: service.id },
468
+ });
469
+
470
+ logger.info("开始释放系统 registry 槽位", {
471
+ topic: LOG_TOPIC_REGISTRY,
472
+ data: { serviceValue, serviceId: service.id },
473
+ });
474
+
475
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
476
+ try {
477
+ const config = await loadSystemRegistryConfig();
478
+ const revision = parseRegistryRevision(config.details.meta);
479
+ const slots = parseServiceSlots(config);
480
+ const nextSlots = releaseOwnedSlot(slots, service.id);
481
+
482
+ if (nextSlots == null) {
483
+ logger.info("无绑定槽位,跳过释放", {
484
+ topic: LOG_TOPIC_REGISTRY,
485
+ data: { serviceValue, serviceId: service.id, attempt },
486
+ });
487
+ return;
488
+ }
489
+
490
+ await persistSlotClaim({
491
+ configId: config.id,
492
+ revision,
493
+ slots: nextSlots,
494
+ });
495
+
496
+ logger.info("槽位释放成功", {
497
+ topic: LOG_TOPIC_REGISTRY,
498
+ data: {
499
+ serviceValue,
500
+ serviceId: service.id,
501
+ attempt,
502
+ revision: revision + 1,
503
+ },
504
+ });
505
+ return;
506
+ } catch (error) {
507
+ if (error instanceof ServiceRegistryError) {
508
+ logger.error("槽位释放被拒绝", {
509
+ topic: LOG_TOPIC_REGISTRY,
510
+ data: { serviceValue, serviceId: service.id, code: error.code, message: error.message },
511
+ });
512
+ throw error;
513
+ }
514
+
515
+ if (isOptimisticLockError(error) && attempt < maxAttempts) {
516
+ logger.warn("registry 乐观锁冲突,重试", {
517
+ topic: LOG_TOPIC_REGISTRY,
518
+ data: { serviceValue, serviceId: service.id, attempt, maxAttempts },
519
+ });
520
+ await sleep(50 * attempt);
521
+ continue;
522
+ }
523
+
524
+ const message = error instanceof Error ? error.message : String(error);
525
+ logger.error("槽位释放失败", {
526
+ topic: LOG_TOPIC_REGISTRY,
527
+ data: { serviceValue, serviceId: service.id, attempt, message },
528
+ });
529
+ throw new ServiceRegistryError(message, "REGISTRY_UPDATE_FAILED");
530
+ }
531
+ }
532
+
533
+ throw new ServiceRegistryError(
534
+ `Failed to unregister ${serviceValue} after ${maxAttempts} attempts`,
535
+ "REGISTRY_UPDATE_FAILED",
536
+ );
537
+ }
538
+
539
+ /**
540
+ * Best-effort slot release for process shutdown — never throws.
541
+ */
542
+ export async function unregisterServiceAtShutdown(input: RegisterServiceInput): Promise<void> {
543
+ const logger = createLogger({
544
+ module: "unregisterServiceAtShutdown",
545
+ traceId: input.traceId,
546
+ labels: { serviceValue: input.service.value, serviceId: input.service.id },
547
+ });
548
+
549
+ try {
550
+ await unregisterService(input);
551
+ } catch (error) {
552
+ const message = error instanceof Error ? error.message : String(error);
553
+ const code = error instanceof ServiceRegistryError ? error.code : undefined;
554
+ logger.warn("shutdown 释放 registry 槽位失败(best-effort)", {
555
+ topic: LOG_TOPIC_REGISTRY,
556
+ data: {
557
+ serviceValue: input.service.value,
558
+ serviceId: input.service.id,
559
+ code,
560
+ message,
561
+ },
562
+ });
563
+ }
564
+ }
@@ -0,0 +1,74 @@
1
+ import { createLogger, type LoggerWithScope } from "../shared/log";
2
+ import type { ServiceLifecycle } from "./base.interface";
3
+ import { ServiceLifecycleStatus } from "./base.interface";
4
+ import { postService } from "./service.api";
5
+ import type { Service, ServiceDetails } from "./service.interface";
6
+
7
+ function formatLocalDateYmd(date = new Date()): string {
8
+ const year = date.getFullYear();
9
+ const month = String(date.getMonth() + 1).padStart(2, "0");
10
+ const day = String(date.getDate()).padStart(2, "0");
11
+ return `${year}-${month}-${day}`;
12
+ }
13
+
14
+ /** Default ACTIVE lifecycle for a newly posted Service instance row. */
15
+ export function createActiveServiceLifecycle(activeSince?: string): ServiceLifecycle {
16
+ return {
17
+ status: ServiceLifecycleStatus.ACTIVE,
18
+ activeSince: activeSince ?? formatLocalDateYmd(),
19
+ deprecatedAt: null,
20
+ sunsetAt: null,
21
+ };
22
+ }
23
+
24
+ /** Standard L3 Service `details` — empty apiKeys, manifest v0, ACTIVE lifecycle. */
25
+ export function defaultL3ServiceDetails(options?: { dependencies?: string[] }): ServiceDetails {
26
+ return {
27
+ manifestVersion: 0,
28
+ apiKeys: [],
29
+ dependencies: options?.dependencies ?? [],
30
+ lifecycle: createActiveServiceLifecycle(),
31
+ };
32
+ }
33
+
34
+ export interface PostServiceInstanceOptions {
35
+ name: string;
36
+ value: string;
37
+ tagList?: string[];
38
+ dependencies?: string[];
39
+ logger?: LoggerWithScope;
40
+ /** Log topic for instance-created message. Default `bootstrap`. */
41
+ logTopic?: string;
42
+ }
43
+
44
+ /**
45
+ * Post a new Service instance row (L3 blueprint).
46
+ * Same `value` may exist on many rows; registry slot claim selects the live instance.
47
+ */
48
+ export async function postServiceInstance(options: PostServiceInstanceOptions): Promise<Service> {
49
+ const logger = options.logger ?? createLogger({ module: "service-bootstrap" });
50
+ const logTopic = options.logTopic ?? "bootstrap";
51
+
52
+ const result = await postService({
53
+ name: options.name,
54
+ value: options.value,
55
+ tagList: options.tagList ?? [],
56
+ details: defaultL3ServiceDetails({ dependencies: options.dependencies }),
57
+ });
58
+
59
+ if (!result.success || result.data == null) {
60
+ throw new Error(result.error?.message ?? "postService failed");
61
+ }
62
+
63
+ logger.success("service instance created", {
64
+ topic: logTopic,
65
+ data: { value: options.value, serviceId: result.data.id },
66
+ });
67
+
68
+ return result.data;
69
+ }
70
+
71
+ export interface ServiceBootstrapResult {
72
+ service: Service;
73
+ baseUrl: string;
74
+ }
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { createTarget, getPossibleTarget, getTarget, type QueryFilter, validateWithSchema } from "../core.api";
3
+ import { NodeStatus } from "../node/node.interface";
3
4
  import { type FieldDefinition, type SchemaDefinition, ServiceLifecycleStatus } from "./base.interface";
4
5
  import {
5
6
  type Api,
@@ -94,11 +95,23 @@ export const apiDetailsSchema = z.object({
94
95
  lifecycle: serviceLifecycleSchema,
95
96
  });
96
97
 
98
+ export const serviceNodeSnapshotSchema = z.object({
99
+ nodeId: z.string().trim().min(1),
100
+ status: z.nativeEnum(NodeStatus),
101
+ lastHeartBeat: z.number().int().nonnegative(),
102
+ });
103
+
104
+ export const serviceRuntimeSchema = z.object({
105
+ lastHeartBeat: z.number().int().nonnegative(),
106
+ nodes: z.array(serviceNodeSnapshotSchema).default([]),
107
+ });
108
+
97
109
  export const serviceDetailsSchema = z.object({
98
110
  manifestVersion: z.number().int().nonnegative(),
99
111
  apiKeys: z.array(z.string().trim().min(1)),
100
112
  dependencies: z.array(z.string().trim().min(1)).default([]),
101
113
  lifecycle: serviceLifecycleSchema,
114
+ runtime: serviceRuntimeSchema.optional(),
102
115
  });
103
116
 
104
117
  export const postApiSchema = z.object({