target-supabase-sdk 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) 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-BL2FaGQp.js} +10 -8
  6. package/dist/chunks/enable-log-persist-BL2FaGQp.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-Mw7bTSqs.js} +1191 -97
  10. package/dist/chunks/word.interface-Mw7bTSqs.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/fs/json-state-store.d.ts.map +1 -1
  18. package/dist/node/fs/runtime-state-path.d.ts +17 -0
  19. package/dist/node/fs/runtime-state-path.d.ts.map +1 -0
  20. package/dist/node/fs/sharded-json-state-store.d.ts +19 -0
  21. package/dist/node/fs/sharded-json-state-store.d.ts.map +1 -0
  22. package/dist/node/node-runtime.base.d.ts +13 -1
  23. package/dist/node/node-runtime.base.d.ts.map +1 -1
  24. package/dist/node/process/create-managed-child-processes.d.ts +12 -0
  25. package/dist/node/process/create-managed-child-processes.d.ts.map +1 -0
  26. package/dist/node/process/index.d.ts +3 -1
  27. package/dist/node/process/index.d.ts.map +1 -1
  28. package/dist/node/process/managed-child-processes.d.ts +15 -2
  29. package/dist/node/process/managed-child-processes.d.ts.map +1 -1
  30. package/dist/node/runtime-state/create-service-runtime-state-store.d.ts +27 -0
  31. package/dist/node/runtime-state/create-service-runtime-state-store.d.ts.map +1 -0
  32. package/dist/node/runtime-state/index.d.ts +4 -0
  33. package/dist/node/runtime-state/index.d.ts.map +1 -0
  34. package/dist/node/runtime-state/service-runtime-state.types.d.ts +57 -0
  35. package/dist/node/runtime-state/service-runtime-state.types.d.ts.map +1 -0
  36. package/dist/node/service-guard/index.d.ts +9 -0
  37. package/dist/node/service-guard/index.d.ts.map +1 -0
  38. package/dist/node/service-guard/register-service-guard-runner.d.ts +9 -0
  39. package/dist/node/service-guard/register-service-guard-runner.d.ts.map +1 -0
  40. package/dist/node/service-guard/run-readiness-gate.d.ts +11 -0
  41. package/dist/node/service-guard/run-readiness-gate.d.ts.map +1 -0
  42. package/dist/node/service-guard/run-service-guard-tick.d.ts +7 -0
  43. package/dist/node/service-guard/run-service-guard-tick.d.ts.map +1 -0
  44. package/dist/node/service-guard/service-guard-node.d.ts +18 -0
  45. package/dist/node/service-guard/service-guard-node.d.ts.map +1 -0
  46. package/dist/node/service-guard/service-guard.interface.d.ts +58 -0
  47. package/dist/node/service-guard/service-guard.interface.d.ts.map +1 -0
  48. package/dist/node/service-guard/worker-spawn-cooldown.d.ts +4 -0
  49. package/dist/node/service-guard/worker-spawn-cooldown.d.ts.map +1 -0
  50. package/dist/node/service-host/index.d.ts +9 -0
  51. package/dist/node/service-host/index.d.ts.map +1 -0
  52. package/dist/node/service-host/registry-slot-guard-step.d.ts +21 -0
  53. package/dist/node/service-host/registry-slot-guard-step.d.ts.map +1 -0
  54. package/dist/node/service-host/service-host.d.ts +36 -0
  55. package/dist/node/service-host/service-host.d.ts.map +1 -0
  56. package/dist/node/service-host/single-process-service.d.ts +21 -0
  57. package/dist/node/service-host/single-process-service.d.ts.map +1 -0
  58. package/dist/node.d.ts +15 -2
  59. package/dist/node.d.ts.map +1 -1
  60. package/dist/node.js +2221 -1485
  61. package/dist/node.js.map +1 -1
  62. package/dist/preload.js +3 -3
  63. package/dist/service/config.api.d.ts +143 -0
  64. package/dist/service/config.api.d.ts.map +1 -0
  65. package/dist/service/config.interface.d.ts +22 -0
  66. package/dist/service/config.interface.d.ts.map +1 -0
  67. package/dist/service/index.d.ts +16 -17
  68. package/dist/service/index.d.ts.map +1 -1
  69. package/dist/service/registry-lifecycle.d.ts +43 -0
  70. package/dist/service/registry-lifecycle.d.ts.map +1 -0
  71. package/dist/service/registry.service.d.ts +117 -0
  72. package/dist/service/registry.service.d.ts.map +1 -0
  73. package/dist/service/service-bootstrap.d.ts +28 -0
  74. package/dist/service/service-bootstrap.d.ts.map +1 -0
  75. package/dist/service/service.api.d.ts +160 -0
  76. package/dist/service/service.api.d.ts.map +1 -1
  77. package/dist/service/service.interface.d.ts +32 -1
  78. package/dist/service/service.interface.d.ts.map +1 -1
  79. package/dist/shared/log/index.d.ts +4 -0
  80. package/dist/shared/log/index.d.ts.map +1 -1
  81. package/dist/shared/log/log-batch.d.ts +44 -0
  82. package/dist/shared/log/log-batch.d.ts.map +1 -0
  83. package/dist/shared/log/log-persist-coordinator.d.ts +37 -0
  84. package/dist/shared/log/log-persist-coordinator.d.ts.map +1 -0
  85. package/dist/shared/log/log-persist-flush.d.ts.map +1 -1
  86. package/dist/shared/log/log-persist.config.d.ts +0 -1
  87. package/dist/shared/log/log-persist.config.d.ts.map +1 -1
  88. package/dist/shared/log/log-persist.interface.d.ts +8 -0
  89. package/dist/shared/log/log-persist.interface.d.ts.map +1 -1
  90. package/dist/shared/utils/config-path.utils.d.ts +21 -0
  91. package/dist/shared/utils/config-path.utils.d.ts.map +1 -1
  92. package/dist/shared/utils/get-value-at-path.d.ts +3 -0
  93. package/dist/shared/utils/get-value-at-path.d.ts.map +1 -0
  94. package/dist/task/index.d.ts +2 -0
  95. package/dist/task/index.d.ts.map +1 -1
  96. package/dist/task/task-manager.d.ts +1 -1
  97. package/dist/task/task-manager.d.ts.map +1 -1
  98. package/dist/task/task-post-retry.api.d.ts +24 -0
  99. package/dist/task/task-post-retry.api.d.ts.map +1 -0
  100. package/dist/task/task-repo-context.d.ts +7 -1
  101. package/dist/task/task-repo-context.d.ts.map +1 -1
  102. package/dist/trigger/trigger-manager.d.ts +1 -1
  103. package/dist/trigger/trigger-manager.d.ts.map +1 -1
  104. package/dist/trigger/trigger-node.d.ts +1 -0
  105. package/dist/trigger/trigger-node.d.ts.map +1 -1
  106. package/dist/trigger/trigger.constant.d.ts +1 -1
  107. package/dist/trigger/trigger.constant.d.ts.map +1 -1
  108. package/dist/trigger/trigger.interface.d.ts +10 -0
  109. package/dist/trigger/trigger.interface.d.ts.map +1 -1
  110. package/package.json +4 -2
  111. package/src/browser.ts +102 -4
  112. package/src/extraction/extraction.api.ts +232 -0
  113. package/src/extraction/extraction.interface.ts +45 -0
  114. package/src/list/list.interface.ts +2 -2
  115. package/src/node/fs/json-state-store.ts +5 -2
  116. package/src/node/fs/runtime-state-path.ts +35 -0
  117. package/src/node/fs/sharded-json-state-store.ts +157 -0
  118. package/src/node/node-runtime.base.ts +37 -6
  119. package/src/node/process/create-managed-child-processes.ts +27 -0
  120. package/src/node/process/index.ts +4 -0
  121. package/src/node/process/managed-child-processes.ts +48 -15
  122. package/src/node/runtime-state/create-service-runtime-state-store.ts +125 -0
  123. package/src/node/runtime-state/index.ts +17 -0
  124. package/src/node/runtime-state/service-runtime-state.types.ts +64 -0
  125. package/src/node/service-guard/index.ts +17 -0
  126. package/src/node/service-guard/register-service-guard-runner.ts +64 -0
  127. package/src/node/service-guard/run-readiness-gate.ts +34 -0
  128. package/src/node/service-guard/run-service-guard-tick.ts +106 -0
  129. package/src/node/service-guard/service-guard-node.ts +52 -0
  130. package/src/node/service-guard/service-guard.interface.ts +64 -0
  131. package/src/node/service-guard/worker-spawn-cooldown.ts +13 -0
  132. package/src/node/service-host/index.ts +30 -0
  133. package/src/node/service-host/registry-slot-guard-step.ts +57 -0
  134. package/src/node/service-host/service-host.ts +168 -0
  135. package/src/node/service-host/single-process-service.ts +58 -0
  136. package/src/node.ts +69 -0
  137. package/src/service/config.api.ts +224 -0
  138. package/src/service/config.interface.ts +25 -0
  139. package/src/service/index.ts +78 -149
  140. package/src/service/registry-lifecycle.ts +144 -0
  141. package/src/service/registry.service.ts +726 -0
  142. package/src/service/service-bootstrap.ts +74 -0
  143. package/src/service/service.api.ts +13 -0
  144. package/src/service/service.interface.ts +36 -1
  145. package/src/shared/log/index.ts +13 -0
  146. package/src/shared/log/log-batch.ts +156 -0
  147. package/src/shared/log/log-persist-coordinator.ts +107 -0
  148. package/src/shared/log/log-persist-flush.ts +3 -3
  149. package/src/shared/log/log-persist.config.ts +0 -2
  150. package/src/shared/log/log-persist.interface.ts +10 -0
  151. package/src/shared/utils/config-path.utils.ts +35 -0
  152. package/src/shared/utils/get-value-at-path.ts +17 -0
  153. package/src/task/index.ts +2 -0
  154. package/src/task/task-manager.ts +10 -4
  155. package/src/task/task-post-retry.api.ts +61 -0
  156. package/src/task/task-repo-context.ts +8 -1
  157. package/src/trigger/trigger-manager.ts +4 -2
  158. package/src/trigger/trigger-node.ts +10 -4
  159. package/src/trigger/trigger.constant.ts +1 -1
  160. package/src/trigger/trigger.interface.ts +10 -0
  161. package/dist/chunks/create-logger-DyH3mZ8p.js.map +0 -1
  162. package/dist/chunks/enable-log-persist-C7GJo_Nc.js.map +0 -1
  163. package/dist/chunks/word.interface-DLgRobKV.js.map +0 -1
@@ -0,0 +1,726 @@
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 interface ReleaseSystemRegistrySlotsInput {
55
+ /** Logical service keys whose ACTIVE slots should be forced to EMPTY (e.g. `watch-service`). */
56
+ serviceValues: string[];
57
+ traceId?: string;
58
+ maxAttempts?: number;
59
+ }
60
+
61
+ export interface ReleasedRegistrySlot {
62
+ serviceValue: string;
63
+ serviceId: string;
64
+ slotIndex: number;
65
+ }
66
+
67
+ export interface ReleaseSystemRegistrySlotsOutcome {
68
+ config: Config;
69
+ released: ReleasedRegistrySlot[];
70
+ /** Requested `serviceValues` with no ACTIVE slot released (already EMPTY or never bound). */
71
+ unchangedServiceValues: string[];
72
+ }
73
+
74
+ export class ServiceRegistryError extends Error {
75
+ constructor(
76
+ message: string,
77
+ readonly code:
78
+ | "REGISTRY_CONFIG_NOT_FOUND"
79
+ | "SERVICE_NOT_DECLARED"
80
+ | "SERVICE_SLOTS_FULL"
81
+ | "REGISTRY_SLOT_LOST"
82
+ | "REGISTRY_UPDATE_FAILED",
83
+ ) {
84
+ super(message);
85
+ this.name = "ServiceRegistryError";
86
+ }
87
+ }
88
+
89
+ function asRecord(value: unknown): Record<string, unknown> | null {
90
+ return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : null;
91
+ }
92
+
93
+ function asFiniteNumber(value: unknown): number | null {
94
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
95
+ }
96
+
97
+ function isServiceSlotStatus(value: unknown): value is ServiceSlotStatus {
98
+ return value === ServiceSlotStatus.EMPTY || value === ServiceSlotStatus.ACTIVE;
99
+ }
100
+
101
+ /** Defensive decode of one {@link ServiceSlot} from Config `details.objects`. */
102
+ export function parseServiceSlot(value: unknown): ServiceSlot | null {
103
+ const source = asRecord(value);
104
+ if (source == null) {
105
+ return null;
106
+ }
107
+
108
+ const serviceValue = typeof source.serviceValue === "string" ? source.serviceValue : null;
109
+ const status = source.status;
110
+ if (serviceValue == null || !isServiceSlotStatus(status)) {
111
+ return null;
112
+ }
113
+
114
+ let serviceId: string | null = null;
115
+ if (source.serviceId != null) {
116
+ if (typeof source.serviceId !== "string" || source.serviceId === "") {
117
+ return null;
118
+ }
119
+ serviceId = source.serviceId;
120
+ }
121
+
122
+ if (status === ServiceSlotStatus.EMPTY) {
123
+ serviceId = null;
124
+ } else if (serviceId == null) {
125
+ return null;
126
+ }
127
+
128
+ return { serviceValue, serviceId, status };
129
+ }
130
+
131
+ export function parseServiceSlots(config: Config): ServiceSlot[] {
132
+ const slots: ServiceSlot[] = [];
133
+ for (const object of config.details.objects) {
134
+ const slot = parseServiceSlot(object);
135
+ if (slot != null) {
136
+ slots.push(slot);
137
+ }
138
+ }
139
+ return slots;
140
+ }
141
+
142
+ /** Parse `details.meta.revision` from the system registry Config (defaults to 0). */
143
+ function parseRegistryRevision(rawMeta: unknown): number {
144
+ const source = asRecord(rawMeta);
145
+ const revision = asFiniteNumber(source?.revision);
146
+ return revision ?? 0;
147
+ }
148
+
149
+ function nextRegistryMeta(nextRevision: number): RegistryConfigMeta {
150
+ return { revision: nextRevision };
151
+ }
152
+
153
+ function lockOnRegistryRevision(revision: number) {
154
+ return [{ field: "details->meta->>revision", operator: "eq" as const, value: String(revision) }];
155
+ }
156
+
157
+ function sleep(ms: number): Promise<void> {
158
+ return new Promise((resolve) => setTimeout(resolve, ms));
159
+ }
160
+
161
+ async function loadSystemRegistryConfig(): Promise<Config> {
162
+ const response = await getConfig({ value: TARGET_SYSTEM_REGISTRY_KEY });
163
+ if (response.data == null) {
164
+ throw new ServiceRegistryError(
165
+ `System registry config not found (value=${TARGET_SYSTEM_REGISTRY_KEY})`,
166
+ "REGISTRY_CONFIG_NOT_FOUND",
167
+ );
168
+ }
169
+ return response.data;
170
+ }
171
+
172
+ async function resolveServiceById(serviceId: string): Promise<Service | null> {
173
+ const response = await getService({ id: serviceId });
174
+ return response.data ?? null;
175
+ }
176
+
177
+ /**
178
+ * Load the global system registry Config and resolve each ACTIVE slot's Service row.
179
+ * Read-only — does not mutate slots or heartbeats.
180
+ */
181
+ export async function getTargetSystemRegistry(): Promise<TargetSystemRegistryView> {
182
+ const config = await loadSystemRegistryConfig();
183
+ const slots = parseServiceSlots(config);
184
+
185
+ const views = await Promise.all(
186
+ slots.map(async (slot): Promise<TargetSystemRegistrySlotView> => {
187
+ if (slot.serviceId == null) {
188
+ return { slot, service: null };
189
+ }
190
+ const service = await resolveServiceById(slot.serviceId);
191
+ return { slot, service };
192
+ }),
193
+ );
194
+
195
+ return { config, slots: views };
196
+ }
197
+
198
+ function findFirstEmptySlot(slots: ServiceSlot[], serviceValue: string): number {
199
+ return slots.findIndex((slot) => slot.serviceValue === serviceValue && slot.status === ServiceSlotStatus.EMPTY);
200
+ }
201
+
202
+ function countSlotsForValue(
203
+ slots: ServiceSlot[],
204
+ serviceValue: string,
205
+ ): { declared: number; active: number; empty: number } {
206
+ let declared = 0;
207
+ let active = 0;
208
+ let empty = 0;
209
+ for (const slot of slots) {
210
+ if (slot.serviceValue !== serviceValue) {
211
+ continue;
212
+ }
213
+ declared++;
214
+ if (slot.status === ServiceSlotStatus.ACTIVE) {
215
+ active++;
216
+ } else if (slot.status === ServiceSlotStatus.EMPTY) {
217
+ empty++;
218
+ }
219
+ }
220
+ return { declared, active, empty };
221
+ }
222
+
223
+ /**
224
+ * Preflight before `postService` — fail fast when registry capacity is full.
225
+ * Prevents orphan Service rows and child process spawn when no slot is available.
226
+ */
227
+ export async function assertRegistrySlotAvailable(serviceValue: string): Promise<void> {
228
+ const config = await loadSystemRegistryConfig();
229
+ const slots = parseServiceSlots(config);
230
+ const counts = countSlotsForValue(slots, serviceValue);
231
+
232
+ if (counts.declared === 0) {
233
+ throw new ServiceRegistryError(
234
+ `Service ${serviceValue} is not declared in system registry config`,
235
+ "SERVICE_NOT_DECLARED",
236
+ );
237
+ }
238
+
239
+ if (counts.empty === 0) {
240
+ throw new ServiceRegistryError(
241
+ `No EMPTY slot for ${serviceValue} — declared=${counts.declared}, active=${counts.active}. Refusing startup.`,
242
+ "SERVICE_SLOTS_FULL",
243
+ );
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Runtime guard: this process's Service instance must still own an ACTIVE registry slot.
249
+ * Supports N>1 replicas — checks whether `serviceId` appears in any ACTIVE slot for
250
+ * `serviceValue`, not merely the first ACTIVE slot.
251
+ */
252
+ export async function assertRegistrySlotOwner(input: { serviceValue: string; serviceId: string }): Promise<void> {
253
+ const config = await loadSystemRegistryConfig();
254
+ const slots = parseServiceSlots(config);
255
+ const ownsSlot = slots.some(
256
+ (slot) =>
257
+ slot.serviceValue === input.serviceValue &&
258
+ slot.status === ServiceSlotStatus.ACTIVE &&
259
+ slot.serviceId === input.serviceId,
260
+ );
261
+ if (ownsSlot) {
262
+ return;
263
+ }
264
+ const activeId = slots.find(
265
+ (slot) =>
266
+ slot.serviceValue === input.serviceValue &&
267
+ slot.status === ServiceSlotStatus.ACTIVE &&
268
+ slot.serviceId != null,
269
+ )?.serviceId;
270
+ throw new ServiceRegistryError(
271
+ `Registry slot for ${input.serviceValue} is not owned by ${input.serviceId} (active=${activeId ?? "none"})`,
272
+ "REGISTRY_SLOT_LOST",
273
+ );
274
+ }
275
+
276
+ function isServiceValueDeclared(slots: ServiceSlot[], serviceValue: string): boolean {
277
+ return slots.some((slot) => slot.serviceValue === serviceValue);
278
+ }
279
+
280
+ function claimSlot(slots: ServiceSlot[], slotIndex: number, serviceId: string): ServiceSlot[] {
281
+ const next = [...slots];
282
+ next[slotIndex] = {
283
+ ...next[slotIndex],
284
+ serviceId,
285
+ status: ServiceSlotStatus.ACTIVE,
286
+ };
287
+ return next;
288
+ }
289
+
290
+ async function persistSlotClaim(params: { configId: string; revision: number; slots: ServiceSlot[] }): Promise<void> {
291
+ const { configId, revision, slots } = params;
292
+
293
+ await updateTargetDetails<Config, ConfigDetails>({
294
+ id: configId,
295
+ optimisticLockFilterList: lockOnRegistryRevision(revision),
296
+ updateFn: (details) => ({
297
+ ...details,
298
+ meta: nextRegistryMeta(revision + 1),
299
+ objects: slots,
300
+ }),
301
+ });
302
+ }
303
+
304
+ function releaseOwnedSlot(slots: ServiceSlot[], serviceId: string): ServiceSlot[] | null {
305
+ const slotIndex = slots.findIndex(
306
+ (slot) => slot.serviceId === serviceId && slot.status === ServiceSlotStatus.ACTIVE,
307
+ );
308
+ if (slotIndex < 0) {
309
+ return null;
310
+ }
311
+
312
+ const next = [...slots];
313
+ next[slotIndex] = {
314
+ ...next[slotIndex],
315
+ serviceId: null,
316
+ status: ServiceSlotStatus.EMPTY,
317
+ };
318
+ return next;
319
+ }
320
+
321
+ function releaseActiveSlotsForServiceValues(
322
+ slots: ServiceSlot[],
323
+ serviceValueSet: ReadonlySet<string>,
324
+ ): { nextSlots: ServiceSlot[]; released: ReleasedRegistrySlot[] } {
325
+ const nextSlots = [...slots];
326
+ const released: ReleasedRegistrySlot[] = [];
327
+
328
+ for (let slotIndex = 0; slotIndex < nextSlots.length; slotIndex += 1) {
329
+ const slot = nextSlots[slotIndex];
330
+ if (
331
+ !serviceValueSet.has(slot.serviceValue) ||
332
+ slot.status !== ServiceSlotStatus.ACTIVE ||
333
+ slot.serviceId == null
334
+ ) {
335
+ continue;
336
+ }
337
+
338
+ released.push({
339
+ serviceValue: slot.serviceValue,
340
+ serviceId: slot.serviceId,
341
+ slotIndex,
342
+ });
343
+ nextSlots[slotIndex] = {
344
+ ...slot,
345
+ serviceId: null,
346
+ status: ServiceSlotStatus.EMPTY,
347
+ };
348
+ }
349
+
350
+ return { nextSlots, released };
351
+ }
352
+
353
+ /**
354
+ * Force-release ACTIVE registry slots for the given logical service keys (`ACTIVE → EMPTY`).
355
+ * Ops/dev recovery when a process crashed without {@link unregisterService}.
356
+ * Does not delete Service rows or mutate `details.runtime`.
357
+ */
358
+ export async function releaseSystemRegistrySlots(
359
+ input: ReleaseSystemRegistrySlotsInput,
360
+ ): Promise<ReleaseSystemRegistrySlotsOutcome> {
361
+ const serviceValues = [
362
+ ...new Set(input.serviceValues.map((value) => value.trim()).filter((value) => value !== "")),
363
+ ];
364
+ if (serviceValues.length === 0) {
365
+ throw new Error("[releaseSystemRegistrySlots] serviceValues must not be empty");
366
+ }
367
+
368
+ const serviceValueSet = new Set(serviceValues);
369
+ const maxAttempts = input.maxAttempts ?? DEFAULT_REGISTER_RETRY_ATTEMPTS;
370
+ const logger = createLogger({
371
+ module: "releaseSystemRegistrySlots",
372
+ traceId: input.traceId,
373
+ labels: { serviceValues: serviceValues.join(",") },
374
+ });
375
+
376
+ logger.info("开始强制释放 registry 槽位", {
377
+ topic: LOG_TOPIC_REGISTRY,
378
+ data: { serviceValues },
379
+ });
380
+
381
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
382
+ try {
383
+ const config = await loadSystemRegistryConfig();
384
+ const revision = parseRegistryRevision(config.details.meta);
385
+ const slots = parseServiceSlots(config);
386
+
387
+ for (const serviceValue of serviceValues) {
388
+ if (!isServiceValueDeclared(slots, serviceValue)) {
389
+ throw new ServiceRegistryError(
390
+ `Service ${serviceValue} is not declared in system registry config`,
391
+ "SERVICE_NOT_DECLARED",
392
+ );
393
+ }
394
+ }
395
+
396
+ const { nextSlots, released } = releaseActiveSlotsForServiceValues(slots, serviceValueSet);
397
+ const releasedValueSet = new Set(released.map((entry) => entry.serviceValue));
398
+ const unchangedServiceValues = serviceValues.filter((serviceValue) => !releasedValueSet.has(serviceValue));
399
+
400
+ if (released.length === 0) {
401
+ logger.info("无 ACTIVE 槽位需要释放", {
402
+ topic: LOG_TOPIC_REGISTRY,
403
+ data: { serviceValues, attempt },
404
+ });
405
+ return { config, released, unchangedServiceValues: serviceValues };
406
+ }
407
+
408
+ await persistSlotClaim({
409
+ configId: config.id,
410
+ revision,
411
+ slots: nextSlots,
412
+ });
413
+
414
+ const updated = await loadSystemRegistryConfig();
415
+ logger.info("槽位强制释放成功", {
416
+ topic: LOG_TOPIC_REGISTRY,
417
+ data: {
418
+ released,
419
+ unchangedServiceValues,
420
+ attempt,
421
+ revision: revision + 1,
422
+ },
423
+ });
424
+
425
+ return {
426
+ config: updated,
427
+ released,
428
+ unchangedServiceValues,
429
+ };
430
+ } catch (error) {
431
+ if (error instanceof ServiceRegistryError) {
432
+ logger.error("槽位强制释放被拒绝", {
433
+ topic: LOG_TOPIC_REGISTRY,
434
+ data: { serviceValues, code: error.code, message: error.message },
435
+ });
436
+ throw error;
437
+ }
438
+
439
+ if (isOptimisticLockError(error) && attempt < maxAttempts) {
440
+ logger.warn("registry 乐观锁冲突,重试", {
441
+ topic: LOG_TOPIC_REGISTRY,
442
+ data: { serviceValues, attempt, maxAttempts },
443
+ });
444
+ await sleep(50 * attempt);
445
+ continue;
446
+ }
447
+
448
+ const message = error instanceof Error ? error.message : String(error);
449
+ logger.error("槽位强制释放失败", {
450
+ topic: LOG_TOPIC_REGISTRY,
451
+ data: { serviceValues, attempt, message },
452
+ });
453
+ throw new ServiceRegistryError(message, "REGISTRY_UPDATE_FAILED");
454
+ }
455
+ }
456
+
457
+ throw new ServiceRegistryError(
458
+ "[releaseSystemRegistrySlots] Optimistic lock retries exhausted",
459
+ "REGISTRY_UPDATE_FAILED",
460
+ );
461
+ }
462
+
463
+ /**
464
+ * Claim one EMPTY slot for a running Service instance (`EMPTY → ACTIVE` only).
465
+ *
466
+ * - Each startup should `postService` a **new** instance row; do not reuse prior Service ids.
467
+ * - Declared capacity = count of {@link ServiceSlot} rows sharing `service.value`
468
+ * - No idempotent skip — if all slots are ACTIVE (e.g. crashed prior instance), throws
469
+ * `SERVICE_SLOTS_FULL` until monitor/ops releases the slot
470
+ * - Does not write heartbeats; guard maintains {@link ServiceDetails.runtime}
471
+ * - Graceful release: {@link unregisterService}; crash recovery: future monitor
472
+ */
473
+ export async function registerService(input: RegisterServiceInput): Promise<void> {
474
+ const { service } = input;
475
+ const serviceValue = service.value;
476
+ const maxAttempts = input.maxAttempts ?? DEFAULT_REGISTER_RETRY_ATTEMPTS;
477
+ const logger = createLogger({
478
+ module: "registerService",
479
+ traceId: input.traceId,
480
+ labels: { serviceValue, serviceId: service.id },
481
+ });
482
+
483
+ logger.info("开始认领系统 registry 槽位", {
484
+ topic: LOG_TOPIC_REGISTRY,
485
+ data: { serviceValue, serviceId: service.id },
486
+ });
487
+
488
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
489
+ try {
490
+ const config = await loadSystemRegistryConfig();
491
+ const revision = parseRegistryRevision(config.details.meta);
492
+ const slots = parseServiceSlots(config);
493
+
494
+ if (!isServiceValueDeclared(slots, serviceValue)) {
495
+ throw new ServiceRegistryError(
496
+ `Service ${serviceValue} is not declared in system registry config`,
497
+ "SERVICE_NOT_DECLARED",
498
+ );
499
+ }
500
+
501
+ const emptyIndex = findFirstEmptySlot(slots, serviceValue);
502
+ if (emptyIndex < 0) {
503
+ throw new ServiceRegistryError(
504
+ `No EMPTY slot for ${serviceValue} — declared capacity is full`,
505
+ "SERVICE_SLOTS_FULL",
506
+ );
507
+ }
508
+
509
+ const nextSlots = claimSlot(slots, emptyIndex, service.id);
510
+ await persistSlotClaim({
511
+ configId: config.id,
512
+ revision,
513
+ slots: nextSlots,
514
+ });
515
+
516
+ logger.info("槽位认领成功", {
517
+ topic: LOG_TOPIC_REGISTRY,
518
+ data: {
519
+ serviceValue,
520
+ serviceId: service.id,
521
+ slotIndex: emptyIndex,
522
+ attempt,
523
+ revision: revision + 1,
524
+ },
525
+ });
526
+ return;
527
+ } catch (error) {
528
+ if (error instanceof ServiceRegistryError) {
529
+ logger.error("槽位认领被拒绝", {
530
+ topic: LOG_TOPIC_REGISTRY,
531
+ data: { serviceValue, serviceId: service.id, code: error.code, message: error.message },
532
+ });
533
+ throw error;
534
+ }
535
+
536
+ if (isOptimisticLockError(error) && attempt < maxAttempts) {
537
+ logger.warn("registry 乐观锁冲突,重试", {
538
+ topic: LOG_TOPIC_REGISTRY,
539
+ data: { serviceValue, serviceId: service.id, attempt, maxAttempts },
540
+ });
541
+ await sleep(50 * attempt);
542
+ continue;
543
+ }
544
+
545
+ const message = error instanceof Error ? error.message : String(error);
546
+ logger.error("槽位认领失败", {
547
+ topic: LOG_TOPIC_REGISTRY,
548
+ data: { serviceValue, serviceId: service.id, attempt, message },
549
+ });
550
+ throw new ServiceRegistryError(message, "REGISTRY_UPDATE_FAILED");
551
+ }
552
+ }
553
+
554
+ throw new ServiceRegistryError(
555
+ `Failed to register ${serviceValue} after ${maxAttempts} attempts`,
556
+ "REGISTRY_UPDATE_FAILED",
557
+ );
558
+ }
559
+
560
+ /** Resolve the ACTIVE registry slot id for a declared `serviceValue`. */
561
+ export async function resolveActiveRegistryServiceId(serviceValue: string): Promise<string | null> {
562
+ const registry = await getTargetSystemRegistry();
563
+ for (const { slot } of registry.slots) {
564
+ if (slot.serviceValue === serviceValue && slot.status === ServiceSlotStatus.ACTIVE && slot.serviceId != null) {
565
+ return slot.serviceId;
566
+ }
567
+ }
568
+ return null;
569
+ }
570
+
571
+ function nodeToServiceNodeSnapshot(node: Node): ServiceNodeSnapshot {
572
+ return {
573
+ nodeId: node.id,
574
+ status: node.details.status,
575
+ lastHeartBeat: node.details.lastHeartBeat,
576
+ };
577
+ }
578
+
579
+ export interface PatchServiceRuntimeInput {
580
+ serviceValue: string;
581
+ /** Prefer explicit id (N>1 replicas); falls back to first ACTIVE slot when omitted. */
582
+ serviceId?: string;
583
+ nodes: readonly Node[];
584
+ lastHeartBeat: number;
585
+ }
586
+
587
+ /** Roll up Node liveness into `Service.details.runtime` (service guard only). */
588
+ export async function patchServiceRuntime(input: PatchServiceRuntimeInput): Promise<void> {
589
+ const serviceId = input.serviceId ?? (await resolveActiveRegistryServiceId(input.serviceValue));
590
+ if (serviceId == null) {
591
+ return;
592
+ }
593
+
594
+ const nodes = input.nodes.map(nodeToServiceNodeSnapshot);
595
+
596
+ await updateTargetDetails<Service, ServiceDetails>({
597
+ id: serviceId,
598
+ updateFn: (details) => ({
599
+ ...details,
600
+ runtime: {
601
+ lastHeartBeat: input.lastHeartBeat,
602
+ nodes,
603
+ },
604
+ }),
605
+ });
606
+ }
607
+
608
+ /**
609
+ * Claim a registry slot at L3 startup — propagates {@link ServiceRegistryError} (e.g. `SERVICE_SLOTS_FULL`).
610
+ */
611
+ export async function registerServiceAtStartup(input: RegisterServiceInput): Promise<void> {
612
+ await registerService(input);
613
+ }
614
+
615
+ /**
616
+ * Release the ACTIVE slot owned by this Service instance (`ACTIVE → EMPTY` only).
617
+ *
618
+ * - Idempotent when no ACTIVE slot is bound to `service.id`
619
+ * - Does not mutate the Service row or `details.runtime`
620
+ * - Does not release slots owned by other instances
621
+ */
622
+ export async function unregisterService(input: RegisterServiceInput): Promise<void> {
623
+ const { service } = input;
624
+ const serviceValue = service.value;
625
+ const maxAttempts = input.maxAttempts ?? DEFAULT_REGISTER_RETRY_ATTEMPTS;
626
+ const logger = createLogger({
627
+ module: "unregisterService",
628
+ traceId: input.traceId,
629
+ labels: { serviceValue, serviceId: service.id },
630
+ });
631
+
632
+ logger.info("开始释放系统 registry 槽位", {
633
+ topic: LOG_TOPIC_REGISTRY,
634
+ data: { serviceValue, serviceId: service.id },
635
+ });
636
+
637
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
638
+ try {
639
+ const config = await loadSystemRegistryConfig();
640
+ const revision = parseRegistryRevision(config.details.meta);
641
+ const slots = parseServiceSlots(config);
642
+ const nextSlots = releaseOwnedSlot(slots, service.id);
643
+
644
+ if (nextSlots == null) {
645
+ logger.info("无绑定槽位,跳过释放", {
646
+ topic: LOG_TOPIC_REGISTRY,
647
+ data: { serviceValue, serviceId: service.id, attempt },
648
+ });
649
+ return;
650
+ }
651
+
652
+ await persistSlotClaim({
653
+ configId: config.id,
654
+ revision,
655
+ slots: nextSlots,
656
+ });
657
+
658
+ logger.info("槽位释放成功", {
659
+ topic: LOG_TOPIC_REGISTRY,
660
+ data: {
661
+ serviceValue,
662
+ serviceId: service.id,
663
+ attempt,
664
+ revision: revision + 1,
665
+ },
666
+ });
667
+ return;
668
+ } catch (error) {
669
+ if (error instanceof ServiceRegistryError) {
670
+ logger.error("槽位释放被拒绝", {
671
+ topic: LOG_TOPIC_REGISTRY,
672
+ data: { serviceValue, serviceId: service.id, code: error.code, message: error.message },
673
+ });
674
+ throw error;
675
+ }
676
+
677
+ if (isOptimisticLockError(error) && attempt < maxAttempts) {
678
+ logger.warn("registry 乐观锁冲突,重试", {
679
+ topic: LOG_TOPIC_REGISTRY,
680
+ data: { serviceValue, serviceId: service.id, attempt, maxAttempts },
681
+ });
682
+ await sleep(50 * attempt);
683
+ continue;
684
+ }
685
+
686
+ const message = error instanceof Error ? error.message : String(error);
687
+ logger.error("槽位释放失败", {
688
+ topic: LOG_TOPIC_REGISTRY,
689
+ data: { serviceValue, serviceId: service.id, attempt, message },
690
+ });
691
+ throw new ServiceRegistryError(message, "REGISTRY_UPDATE_FAILED");
692
+ }
693
+ }
694
+
695
+ throw new ServiceRegistryError(
696
+ `Failed to unregister ${serviceValue} after ${maxAttempts} attempts`,
697
+ "REGISTRY_UPDATE_FAILED",
698
+ );
699
+ }
700
+
701
+ /**
702
+ * Best-effort slot release for process shutdown — never throws.
703
+ */
704
+ export async function unregisterServiceAtShutdown(input: RegisterServiceInput): Promise<void> {
705
+ const logger = createLogger({
706
+ module: "unregisterServiceAtShutdown",
707
+ traceId: input.traceId,
708
+ labels: { serviceValue: input.service.value, serviceId: input.service.id },
709
+ });
710
+
711
+ try {
712
+ await unregisterService(input);
713
+ } catch (error) {
714
+ const message = error instanceof Error ? error.message : String(error);
715
+ const code = error instanceof ServiceRegistryError ? error.code : undefined;
716
+ logger.warn("shutdown 释放 registry 槽位失败(best-effort)", {
717
+ topic: LOG_TOPIC_REGISTRY,
718
+ data: {
719
+ serviceValue: input.service.value,
720
+ serviceId: input.service.id,
721
+ code,
722
+ message,
723
+ },
724
+ });
725
+ }
726
+ }