@objectstack/core 17.0.0-rc.0 → 17.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3295 -0
- package/dist/index.cjs +659 -101
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +454 -19
- package/dist/index.d.ts +454 -19
- package/dist/index.js +642 -97
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.cjs
CHANGED
|
@@ -40,6 +40,8 @@ __export(index_exports, {
|
|
|
40
40
|
DependencyResolver: () => DependencyResolver,
|
|
41
41
|
HotReloadManager: () => HotReloadManager,
|
|
42
42
|
LiteKernel: () => LiteKernel,
|
|
43
|
+
MigrationJournalRefusal: () => MigrationJournalRefusal,
|
|
44
|
+
MigrationPlanRegistry: () => MigrationPlanRegistry,
|
|
43
45
|
NamespaceResolver: () => NamespaceResolver,
|
|
44
46
|
ObjectKernel: () => ObjectKernel,
|
|
45
47
|
ObjectKernelBase: () => ObjectKernelBase,
|
|
@@ -62,6 +64,7 @@ __export(index_exports, {
|
|
|
62
64
|
ServiceLifecycle: () => ServiceLifecycle,
|
|
63
65
|
UnknownFilterTokenError: () => UnknownFilterTokenError,
|
|
64
66
|
UnresolvedFilterTokenError: () => UnresolvedFilterTokenError,
|
|
67
|
+
assertInitServiceRequirements: () => assertInitServiceRequirements,
|
|
65
68
|
bucketKeyToCalendarRange: () => bucketKeyToCalendarRange,
|
|
66
69
|
buildPermissionsFromGrants: () => buildPermissionsFromGrants,
|
|
67
70
|
bulkWrite: () => bulkWrite,
|
|
@@ -80,33 +83,45 @@ __export(index_exports, {
|
|
|
80
83
|
deepMerge: () => deepMerge,
|
|
81
84
|
defaultIsTransientError: () => defaultIsTransientError,
|
|
82
85
|
derivePosture: () => derivePosture,
|
|
86
|
+
describeInitOrderFault: () => describeInitOrderFault,
|
|
87
|
+
engineCanRollBack: () => engineCanRollBack,
|
|
83
88
|
evaluateAuthGate: () => evaluateAuthGate,
|
|
84
89
|
extractApiKey: () => extractApiKey,
|
|
85
90
|
filterTokenContextFrom: () => filterTokenContextFrom,
|
|
91
|
+
findInterruptedRuns: () => findInterruptedRuns,
|
|
86
92
|
generateApiKey: () => generateApiKey,
|
|
87
93
|
generateEd25519KeyPair: () => generateEd25519KeyPair,
|
|
88
94
|
getEnv: () => getEnv,
|
|
89
95
|
getMemoryUsage: () => getMemoryUsage,
|
|
90
96
|
hashApiKey: () => hashApiKey,
|
|
97
|
+
hashMigrationPlan: () => hashMigrationPlan,
|
|
91
98
|
isAuthGateAllowlisted: () => isAuthGateAllowlisted,
|
|
92
99
|
isExpired: () => isExpired,
|
|
93
100
|
isGrantActive: () => isGrantActive,
|
|
94
101
|
isGrantExpired: () => isGrantExpired,
|
|
95
102
|
isNode: () => isNode,
|
|
103
|
+
nextUtcCalendarDay: () => import_data.nextUtcCalendarDay,
|
|
96
104
|
parseScopes: () => parseScopes,
|
|
97
105
|
parseSignature: () => parseSignature,
|
|
106
|
+
planChunks: () => planChunks,
|
|
98
107
|
postureVisibleRows: () => postureVisibleRows,
|
|
99
108
|
readAuthoredTranslationLayer: () => readAuthoredTranslationLayer,
|
|
109
|
+
readRunJournal: () => readRunJournal,
|
|
100
110
|
resolveApiKeyPrincipal: () => resolveApiKeyPrincipal,
|
|
101
111
|
resolveAuthzContext: () => resolveAuthzContext,
|
|
102
112
|
resolveFilterToken: () => resolveFilterToken,
|
|
103
113
|
resolveFilterTokens: () => resolveFilterTokens,
|
|
104
114
|
resolveLocale: () => resolveLocale,
|
|
105
115
|
resolveLocalizationContext: () => resolveLocalizationContext,
|
|
116
|
+
resolvePluginOrder: () => resolvePluginOrder,
|
|
106
117
|
resolveUserAuthzGrants: () => resolveUserAuthzGrants,
|
|
118
|
+
resumeMigrationJournal: () => resumeMigrationJournal,
|
|
119
|
+
runMigrationJournal: () => runMigrationJournal,
|
|
107
120
|
safeExit: () => safeExit,
|
|
108
121
|
shouldDenyAnonymous: () => shouldDenyAnonymous,
|
|
109
122
|
signPayload: () => signPayload,
|
|
123
|
+
utcInstantMs: () => import_data.utcInstantMs,
|
|
124
|
+
validateInitServiceContract: () => validateInitServiceContract,
|
|
110
125
|
verifyPayload: () => verifyPayload,
|
|
111
126
|
verifyPlatformSignature: () => verifyPlatformSignature,
|
|
112
127
|
verifyPluginArtifact: () => verifyPluginArtifact,
|
|
@@ -117,6 +132,89 @@ __export(index_exports, {
|
|
|
117
132
|
});
|
|
118
133
|
module.exports = __toCommonJS(index_exports);
|
|
119
134
|
|
|
135
|
+
// src/plugin-order.ts
|
|
136
|
+
function resolvePluginOrder(plugins) {
|
|
137
|
+
const resolved = [];
|
|
138
|
+
const visited = /* @__PURE__ */ new Set();
|
|
139
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
140
|
+
const visit = (pluginName) => {
|
|
141
|
+
if (visited.has(pluginName)) return;
|
|
142
|
+
if (visiting.has(pluginName)) {
|
|
143
|
+
throw new Error(`[Kernel] Circular dependency detected: ${pluginName}`);
|
|
144
|
+
}
|
|
145
|
+
const plugin = plugins.get(pluginName);
|
|
146
|
+
if (!plugin) {
|
|
147
|
+
throw new Error(`[Kernel] Plugin '${pluginName}' not found`);
|
|
148
|
+
}
|
|
149
|
+
visiting.add(pluginName);
|
|
150
|
+
for (const dep of plugin.dependencies ?? []) {
|
|
151
|
+
if (!plugins.has(dep)) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`[Kernel] Dependency '${dep}' not found for plugin '${pluginName}'`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
visit(dep);
|
|
157
|
+
}
|
|
158
|
+
for (const dep of plugin.optionalDependencies ?? []) {
|
|
159
|
+
if (plugins.has(dep)) visit(dep);
|
|
160
|
+
}
|
|
161
|
+
visiting.delete(pluginName);
|
|
162
|
+
visited.add(pluginName);
|
|
163
|
+
resolved.push(plugin);
|
|
164
|
+
};
|
|
165
|
+
for (const pluginName of plugins.keys()) {
|
|
166
|
+
visit(pluginName);
|
|
167
|
+
}
|
|
168
|
+
return resolved;
|
|
169
|
+
}
|
|
170
|
+
function validateInitServiceContract(ordered, isServiceRegistered) {
|
|
171
|
+
const providerSlot = /* @__PURE__ */ new Map();
|
|
172
|
+
ordered.forEach((plugin, slot) => {
|
|
173
|
+
for (const service of plugin.providesServices ?? []) {
|
|
174
|
+
if (!providerSlot.has(service)) {
|
|
175
|
+
providerSlot.set(service, { plugin: plugin.name, slot });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
const violations = [];
|
|
180
|
+
ordered.forEach((plugin, slot) => {
|
|
181
|
+
for (const service of plugin.requiresServices ?? []) {
|
|
182
|
+
if (isServiceRegistered(service)) continue;
|
|
183
|
+
const provider = providerSlot.get(service);
|
|
184
|
+
if (provider && provider.slot > slot) {
|
|
185
|
+
violations.push(
|
|
186
|
+
`'${plugin.name}' requires service '${service}' during init, but '${service}' is provided by '${provider.plugin}', which initializes later (slot ${provider.slot} vs ${slot}). Registration order is not a contract \u2014 declare '${provider.plugin}' in '${plugin.name}'.dependencies (hard) or .optionalDependencies (order-if-present) so the kernel hoists it.`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
if (violations.length > 0) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`[Kernel] Plugin ordering contract violated (#4131):
|
|
194
|
+
- ${violations.join("\n - ")}`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function describeInitOrderFault(currentlyInitializing, plugins, serviceName) {
|
|
199
|
+
if (!currentlyInitializing) return "";
|
|
200
|
+
let providerHint = "";
|
|
201
|
+
for (const plugin of plugins) {
|
|
202
|
+
if (plugin.providesServices?.includes(serviceName)) {
|
|
203
|
+
providerHint = ` '${serviceName}' is provided by composed plugin '${plugin.name}', which has not initialized yet \u2014 declare it in the requiring plugin's dependencies/optionalDependencies.`;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return ` (while plugin '${currentlyInitializing}' was initializing \u2014 a composition/ordering fault, #4131.${providerHint})`;
|
|
208
|
+
}
|
|
209
|
+
function assertInitServiceRequirements(plugin, isServiceRegistered) {
|
|
210
|
+
for (const service of plugin.requiresServices ?? []) {
|
|
211
|
+
if (isServiceRegistered(service)) continue;
|
|
212
|
+
throw new Error(
|
|
213
|
+
`[Kernel] Plugin '${plugin.name}' requires service '${service}' at init, but no such service is registered at this point of the boot. No composed plugin that initializes earlier provides it \u2014 compose a provider (and, if it initializes later without declaring '${service}' in providesServices, order it ahead via this plugin's dependencies/optionalDependencies) (#4131).`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
120
218
|
// src/kernel-base.ts
|
|
121
219
|
var ObjectKernelBase = class {
|
|
122
220
|
constructor(logger) {
|
|
@@ -167,7 +265,9 @@ var ObjectKernelBase = class {
|
|
|
167
265
|
if (this.services instanceof Map) {
|
|
168
266
|
const service = this.services.get(name);
|
|
169
267
|
if (!service) {
|
|
170
|
-
throw new Error(
|
|
268
|
+
throw new Error(
|
|
269
|
+
`[Kernel] Service '${name}' not found${this.describeInitOrderFault(name)}`
|
|
270
|
+
);
|
|
171
271
|
}
|
|
172
272
|
return service;
|
|
173
273
|
} else {
|
|
@@ -218,40 +318,37 @@ var ObjectKernelBase = class {
|
|
|
218
318
|
};
|
|
219
319
|
}
|
|
220
320
|
/**
|
|
221
|
-
* Resolve plugin dependencies using topological sort
|
|
321
|
+
* Resolve plugin dependencies using topological sort — `dependencies`
|
|
322
|
+
* hard, `optionalDependencies` order-if-present (ADR-0116, #4131). One
|
|
323
|
+
* implementation shared with ObjectKernel via `plugin-order.ts`.
|
|
222
324
|
* @returns Ordered list of plugins (dependencies first)
|
|
223
325
|
*/
|
|
224
326
|
resolveDependencies() {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
};
|
|
251
|
-
for (const pluginName of this.plugins.keys()) {
|
|
252
|
-
visit(pluginName);
|
|
253
|
-
}
|
|
254
|
-
return resolved;
|
|
327
|
+
return resolvePluginOrder(this.plugins);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Whether a service is registered on this kernel right now. Backs the
|
|
331
|
+
* init-service contract checks (#4131).
|
|
332
|
+
*/
|
|
333
|
+
hasRegisteredService(name) {
|
|
334
|
+
return this.services.has(name);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Pre-Phase-1 ordering validation (ADR-0116, #4131): a plugin whose
|
|
338
|
+
* `requiresServices` names a service provided only by a LATER plugin is
|
|
339
|
+
* a named boot error before any init side effects.
|
|
340
|
+
*/
|
|
341
|
+
validateInitServices(ordered) {
|
|
342
|
+
validateInitServiceContract(ordered, (name) => this.hasRegisteredService(name));
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* When a getService miss happens while a plugin's init() is running,
|
|
346
|
+
* append the structural diagnosis (#4131): which plugin was initializing,
|
|
347
|
+
* and — when a composed plugin declares the service — who provides it.
|
|
348
|
+
* Empty string outside Phase 1, so non-boot messages stay unchanged.
|
|
349
|
+
*/
|
|
350
|
+
describeInitOrderFault(serviceName) {
|
|
351
|
+
return describeInitOrderFault(this.currentlyInitializing, this.plugins.values(), serviceName);
|
|
255
352
|
}
|
|
256
353
|
/**
|
|
257
354
|
* Run plugin init phase
|
|
@@ -260,12 +357,16 @@ var ObjectKernelBase = class {
|
|
|
260
357
|
async runPluginInit(plugin) {
|
|
261
358
|
const pluginName = plugin.name;
|
|
262
359
|
this.logger.info(`Initializing plugin: ${pluginName}`);
|
|
360
|
+
assertInitServiceRequirements(plugin, (name) => this.hasRegisteredService(name));
|
|
361
|
+
this.currentlyInitializing = pluginName;
|
|
263
362
|
try {
|
|
264
363
|
await plugin.init(this.context);
|
|
265
364
|
this.logger.info(`Plugin initialized: ${pluginName}`);
|
|
266
365
|
} catch (error) {
|
|
267
366
|
this.logger.error(`Plugin init failed: ${pluginName}`, error);
|
|
268
367
|
throw error;
|
|
368
|
+
} finally {
|
|
369
|
+
this.currentlyInitializing = void 0;
|
|
269
370
|
}
|
|
270
371
|
}
|
|
271
372
|
/**
|
|
@@ -1112,7 +1213,11 @@ function createMemoryCache() {
|
|
|
1112
1213
|
let hits = 0;
|
|
1113
1214
|
let misses = 0;
|
|
1114
1215
|
return {
|
|
1115
|
-
|
|
1216
|
+
__serviceInfo: {
|
|
1217
|
+
status: "degraded",
|
|
1218
|
+
handlerReady: false,
|
|
1219
|
+
message: "In-process Map cache \u2014 not shared across instances, lost on restart. Register a cache plugin (e.g. Redis) for a real one."
|
|
1220
|
+
},
|
|
1116
1221
|
_serviceName: "cache",
|
|
1117
1222
|
async get(key) {
|
|
1118
1223
|
const entry = store.get(key);
|
|
@@ -1147,7 +1252,11 @@ function createMemoryQueue() {
|
|
|
1147
1252
|
const handlers = /* @__PURE__ */ new Map();
|
|
1148
1253
|
let msgId = 0;
|
|
1149
1254
|
return {
|
|
1150
|
-
|
|
1255
|
+
__serviceInfo: {
|
|
1256
|
+
status: "degraded",
|
|
1257
|
+
handlerReady: false,
|
|
1258
|
+
message: "Synchronous in-process delivery \u2014 no durability, retry, or cross-instance fan-out. Register a queue plugin (e.g. BullMQ) for a real one."
|
|
1259
|
+
},
|
|
1151
1260
|
_serviceName: "queue",
|
|
1152
1261
|
async publish(queue, data) {
|
|
1153
1262
|
const id = `fallback-msg-${++msgId}`;
|
|
@@ -1174,7 +1283,11 @@ function createMemoryQueue() {
|
|
|
1174
1283
|
function createMemoryJob() {
|
|
1175
1284
|
const jobs = /* @__PURE__ */ new Map();
|
|
1176
1285
|
return {
|
|
1177
|
-
|
|
1286
|
+
__serviceInfo: {
|
|
1287
|
+
status: "degraded",
|
|
1288
|
+
handlerReady: false,
|
|
1289
|
+
message: "In-process job registry \u2014 trigger() runs handlers, but scheduled jobs never fire on their own (no timer). Register a job plugin (e.g. Agenda) for real scheduling."
|
|
1290
|
+
},
|
|
1178
1291
|
_serviceName: "job",
|
|
1179
1292
|
async schedule(name, schedule, handler) {
|
|
1180
1293
|
jobs.set(name, { schedule, handler });
|
|
@@ -1253,7 +1366,15 @@ function createMemoryI18n() {
|
|
|
1253
1366
|
return void 0;
|
|
1254
1367
|
}
|
|
1255
1368
|
return {
|
|
1256
|
-
|
|
1369
|
+
// [#4058] `degraded` (ADR-0076 D12): translations, locale fallback and
|
|
1370
|
+
// interpolation are all real — what is missing is persistence and the
|
|
1371
|
+
// authoring surface service-i18n adds. `handlerReady` left at the
|
|
1372
|
+
// `degraded` default (true): the dispatcher's `/i18n` domain does serve
|
|
1373
|
+
// this implementation.
|
|
1374
|
+
__serviceInfo: {
|
|
1375
|
+
status: "degraded",
|
|
1376
|
+
message: "In-memory translations \u2014 real lookup and locale fallback, but nothing is persisted. Register I18nServicePlugin from @objectstack/service-i18n for the full implementation."
|
|
1377
|
+
},
|
|
1257
1378
|
_serviceName: "i18n",
|
|
1258
1379
|
t(key, locale, params) {
|
|
1259
1380
|
const data = resolveTranslations(locale) ?? mergedLocale(defaultLocale);
|
|
@@ -1310,7 +1431,14 @@ function createMemoryMetadata() {
|
|
|
1310
1431
|
return map;
|
|
1311
1432
|
}
|
|
1312
1433
|
return {
|
|
1313
|
-
|
|
1434
|
+
// [#4058] `degraded` (ADR-0076 D12): the registry is real — everything
|
|
1435
|
+
// registered is listable and readable back — it simply never reaches disk
|
|
1436
|
+
// or a database. `handlerReady` keeps the `degraded` default (true): the
|
|
1437
|
+
// dispatcher's `/meta` domain serves this implementation.
|
|
1438
|
+
__serviceInfo: {
|
|
1439
|
+
status: "degraded",
|
|
1440
|
+
message: "In-memory metadata registry \u2014 real reads and writes, no persistence (lost on restart). Register MetadataPlugin for a persisted registry."
|
|
1441
|
+
},
|
|
1314
1442
|
_serviceName: "metadata",
|
|
1315
1443
|
async register(type, name, data) {
|
|
1316
1444
|
getTypeMap(type).set(name, data);
|
|
@@ -1526,24 +1654,12 @@ var ObjectKernel = class {
|
|
|
1526
1654
|
this.services.set(name, loaderService);
|
|
1527
1655
|
return loaderService;
|
|
1528
1656
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
});
|
|
1534
|
-
throw new Error(`Service '${name}' is async - use await`);
|
|
1535
|
-
}
|
|
1536
|
-
return service2;
|
|
1537
|
-
} catch (error) {
|
|
1538
|
-
if (error.message?.includes("is async")) {
|
|
1539
|
-
throw error;
|
|
1540
|
-
}
|
|
1541
|
-
const isNotFoundError = error.message === `Service '${name}' not found`;
|
|
1542
|
-
if (!isNotFoundError) {
|
|
1543
|
-
throw error;
|
|
1544
|
-
}
|
|
1545
|
-
throw new Error(`[Kernel] Service '${name}' not found`);
|
|
1657
|
+
if (!this.pluginLoader.hasService(name)) {
|
|
1658
|
+
throw new Error(
|
|
1659
|
+
`[Kernel] Service '${name}' not found${this.describeInitOrderFault(name)}`
|
|
1660
|
+
);
|
|
1546
1661
|
}
|
|
1662
|
+
throw new Error(`Service '${name}' is async - use await`);
|
|
1547
1663
|
},
|
|
1548
1664
|
replaceService: (name, implementation) => {
|
|
1549
1665
|
const hasService = this.services.has(name) || this.pluginLoader.hasService(name);
|
|
@@ -1702,6 +1818,7 @@ var ObjectKernel = class {
|
|
|
1702
1818
|
this.logger.warn("Circular service dependencies detected:", { cycles });
|
|
1703
1819
|
}
|
|
1704
1820
|
const orderedPlugins = this.resolveDependencies();
|
|
1821
|
+
validateInitServiceContract(orderedPlugins, (name) => this.hasAnyService(name));
|
|
1705
1822
|
this.logger.info("Phase 1: Init plugins");
|
|
1706
1823
|
for (const plugin of orderedPlugins) {
|
|
1707
1824
|
await this.initPluginWithTimeout(plugin);
|
|
@@ -1847,13 +1964,70 @@ var ObjectKernel = class {
|
|
|
1847
1964
|
async initPluginWithTimeout(plugin) {
|
|
1848
1965
|
const timeout = plugin.startupTimeout || this.config.defaultStartupTimeout;
|
|
1849
1966
|
this.logger.debug(`Init: ${plugin.name}`, { plugin: plugin.name });
|
|
1850
|
-
|
|
1967
|
+
assertInitServiceRequirements(plugin, (name) => this.hasAnyService(name));
|
|
1968
|
+
this.currentlyInitializing = plugin.name;
|
|
1969
|
+
try {
|
|
1970
|
+
await this.raceStartupTimeout(
|
|
1971
|
+
plugin.init(this.context),
|
|
1972
|
+
timeout,
|
|
1973
|
+
`Plugin ${plugin.name} init timeout after ${timeout}ms`
|
|
1974
|
+
);
|
|
1975
|
+
} finally {
|
|
1976
|
+
this.currentlyInitializing = void 0;
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
/**
|
|
1980
|
+
* Race a plugin lifecycle hook against its startup-timeout guard, and
|
|
1981
|
+
* reclaim the guard the moment the race settles (#4813).
|
|
1982
|
+
*
|
|
1983
|
+
* The guard used to be armed and then abandoned: when the plugin won the
|
|
1984
|
+
* race, its `setTimeout` stayed ref'd in the event loop for the full
|
|
1985
|
+
* `startupTimeout`, so every process idled that long after its work was
|
|
1986
|
+
* done. One `os migrate` finished in 3s and then sat for 120s
|
|
1987
|
+
* (`ObjectQLPlugin.startupTimeout`), held open by 8 orphaned guards — one
|
|
1988
|
+
* per init plus one per start.
|
|
1989
|
+
*
|
|
1990
|
+
* Clearing on settle rather than `unref()`-ing at arm time is deliberate.
|
|
1991
|
+
* An unref'd guard also stops pinning the loop, but it stops being a guard
|
|
1992
|
+
* as well: if the hook never settles and nothing else keeps the loop alive,
|
|
1993
|
+
* Node exits before the timer can fire and the timeout is never reported.
|
|
1994
|
+
* The guard has to stay ref'd exactly as long as the race is undecided,
|
|
1995
|
+
* which is what `clearTimeout` in a `finally` expresses.
|
|
1996
|
+
*
|
|
1997
|
+
* `operation` is widened to `T | PromiseLike<T>` because the Plugin
|
|
1998
|
+
* contract permits a synchronous hook (`init`/`start` return
|
|
1999
|
+
* `void | Promise<void>`); such a hook wins the race immediately and the
|
|
2000
|
+
* guard is reclaimed on the same turn.
|
|
2001
|
+
*/
|
|
2002
|
+
async raceStartupTimeout(operation, timeout, message) {
|
|
2003
|
+
let guard;
|
|
1851
2004
|
const timeoutPromise = new Promise((_, reject) => {
|
|
1852
|
-
setTimeout(() => {
|
|
1853
|
-
reject(new Error(
|
|
2005
|
+
guard = setTimeout(() => {
|
|
2006
|
+
reject(new Error(message));
|
|
1854
2007
|
}, timeout);
|
|
1855
2008
|
});
|
|
1856
|
-
|
|
2009
|
+
try {
|
|
2010
|
+
return await Promise.race([operation, timeoutPromise]);
|
|
2011
|
+
} finally {
|
|
2012
|
+
clearTimeout(guard);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* Whether a service is resolvable on this kernel right now — direct
|
|
2017
|
+
* registration or a loader-registered factory. Backs the init-service
|
|
2018
|
+
* contract checks (#4131).
|
|
2019
|
+
*/
|
|
2020
|
+
hasAnyService(name) {
|
|
2021
|
+
return this.services.has(name) || this.pluginLoader.hasService(name);
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* When a getService miss happens while a plugin's init() is running,
|
|
2025
|
+
* append the structural diagnosis (#4131): which plugin was initializing,
|
|
2026
|
+
* and — when a composed plugin declares the service — who provides it.
|
|
2027
|
+
* Empty string outside Phase 1, so non-boot messages stay unchanged.
|
|
2028
|
+
*/
|
|
2029
|
+
describeInitOrderFault(serviceName) {
|
|
2030
|
+
return describeInitOrderFault(this.currentlyInitializing, this.plugins.values(), serviceName);
|
|
1857
2031
|
}
|
|
1858
2032
|
async startPluginWithTimeout(plugin) {
|
|
1859
2033
|
if (!plugin.start) {
|
|
@@ -1863,13 +2037,11 @@ var ObjectKernel = class {
|
|
|
1863
2037
|
const startTime = Date.now();
|
|
1864
2038
|
this.logger.debug(`Start: ${plugin.name}`, { plugin: plugin.name });
|
|
1865
2039
|
try {
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
});
|
|
1872
|
-
await Promise.race([startPromise, timeoutPromise]);
|
|
2040
|
+
await this.raceStartupTimeout(
|
|
2041
|
+
plugin.start(this.context),
|
|
2042
|
+
timeout,
|
|
2043
|
+
`Plugin ${plugin.name} start timeout after ${timeout}ms`
|
|
2044
|
+
);
|
|
1873
2045
|
const duration = Date.now() - startTime;
|
|
1874
2046
|
this.startedPlugins.add(plugin.name);
|
|
1875
2047
|
this.pluginStartTimes.set(plugin.name, duration);
|
|
@@ -1927,35 +2099,13 @@ var ObjectKernel = class {
|
|
|
1927
2099
|
}
|
|
1928
2100
|
}
|
|
1929
2101
|
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Topological order over `dependencies` (hard) + `optionalDependencies`
|
|
2104
|
+
* (order-if-present) — ADR-0116, #4131. One implementation shared with
|
|
2105
|
+
* LiteKernel via `plugin-order.ts`.
|
|
2106
|
+
*/
|
|
1930
2107
|
resolveDependencies() {
|
|
1931
|
-
|
|
1932
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1933
|
-
const visiting = /* @__PURE__ */ new Set();
|
|
1934
|
-
const visit = (pluginName) => {
|
|
1935
|
-
if (visited.has(pluginName)) return;
|
|
1936
|
-
if (visiting.has(pluginName)) {
|
|
1937
|
-
throw new Error(`[Kernel] Circular dependency detected: ${pluginName}`);
|
|
1938
|
-
}
|
|
1939
|
-
const plugin = this.plugins.get(pluginName);
|
|
1940
|
-
if (!plugin) {
|
|
1941
|
-
throw new Error(`[Kernel] Plugin '${pluginName}' not found`);
|
|
1942
|
-
}
|
|
1943
|
-
visiting.add(pluginName);
|
|
1944
|
-
const deps = plugin.dependencies || [];
|
|
1945
|
-
for (const dep of deps) {
|
|
1946
|
-
if (!this.plugins.has(dep)) {
|
|
1947
|
-
throw new Error(`[Kernel] Dependency '${dep}' not found for plugin '${pluginName}'`);
|
|
1948
|
-
}
|
|
1949
|
-
visit(dep);
|
|
1950
|
-
}
|
|
1951
|
-
visiting.delete(pluginName);
|
|
1952
|
-
visited.add(pluginName);
|
|
1953
|
-
resolved.push(plugin);
|
|
1954
|
-
};
|
|
1955
|
-
for (const pluginName of this.plugins.keys()) {
|
|
1956
|
-
visit(pluginName);
|
|
1957
|
-
}
|
|
1958
|
-
return resolved;
|
|
2108
|
+
return resolvePluginOrder(this.plugins);
|
|
1959
2109
|
}
|
|
1960
2110
|
registerShutdownSignals() {
|
|
1961
2111
|
const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
|
|
@@ -2021,6 +2171,7 @@ var LiteKernel = class extends ObjectKernelBase {
|
|
|
2021
2171
|
this.state = "initializing";
|
|
2022
2172
|
this.logger.info("Bootstrap started");
|
|
2023
2173
|
const orderedPlugins = this.resolveDependencies();
|
|
2174
|
+
this.validateInitServices(orderedPlugins);
|
|
2024
2175
|
this.logger.info("Phase 1: Init plugins");
|
|
2025
2176
|
for (const plugin of orderedPlugins) {
|
|
2026
2177
|
await this.runPluginInit(plugin);
|
|
@@ -2621,6 +2772,11 @@ function createApiRegistryPlugin(config = {}) {
|
|
|
2621
2772
|
} = config;
|
|
2622
2773
|
return {
|
|
2623
2774
|
name: "com.objectstack.core.api-registry",
|
|
2775
|
+
/**
|
|
2776
|
+
* Services init() registers on every path (ADR-0116, #4131) — lets the
|
|
2777
|
+
* kernel name this plugin when a consumer requires one before it inits.
|
|
2778
|
+
*/
|
|
2779
|
+
providesServices: ["api-registry"],
|
|
2624
2780
|
type: "standard",
|
|
2625
2781
|
version: "1.0.0",
|
|
2626
2782
|
init: async (ctx) => {
|
|
@@ -4683,14 +4839,13 @@ function evaluateAuthGate(sessionUser, path) {
|
|
|
4683
4839
|
|
|
4684
4840
|
// src/security/anonymous-deny.ts
|
|
4685
4841
|
var ANONYMOUS_DENY_STATUS = 401;
|
|
4686
|
-
var ANONYMOUS_DENY_CODE = "
|
|
4842
|
+
var ANONYMOUS_DENY_CODE = "UNAUTHENTICATED";
|
|
4687
4843
|
var ANONYMOUS_DENY_MESSAGE = "Authentication is required to access this endpoint.";
|
|
4688
4844
|
var ANONYMOUS_DENY_BODY = {
|
|
4689
4845
|
error: ANONYMOUS_DENY_CODE,
|
|
4690
4846
|
message: ANONYMOUS_DENY_MESSAGE
|
|
4691
4847
|
};
|
|
4692
4848
|
function shouldDenyAnonymous(input) {
|
|
4693
|
-
if (!input.requireAuth) return false;
|
|
4694
4849
|
if (typeof input.method === "string" && input.method.toUpperCase() === "OPTIONS") {
|
|
4695
4850
|
return false;
|
|
4696
4851
|
}
|
|
@@ -4702,6 +4857,7 @@ function shouldDenyAnonymous(input) {
|
|
|
4702
4857
|
}
|
|
4703
4858
|
|
|
4704
4859
|
// src/utils/datetime.ts
|
|
4860
|
+
var import_data = require("@objectstack/spec/data");
|
|
4705
4861
|
function calendarPartsInTz(d, tz) {
|
|
4706
4862
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
4707
4863
|
timeZone: tz,
|
|
@@ -4942,8 +5098,395 @@ async function bulkWrite(rows, opts) {
|
|
|
4942
5098
|
return results;
|
|
4943
5099
|
}
|
|
4944
5100
|
|
|
5101
|
+
// src/utils/migration-journal.ts
|
|
5102
|
+
var import_node_crypto3 = require("crypto");
|
|
5103
|
+
var import_system3 = require("@objectstack/spec/system");
|
|
5104
|
+
var SYSTEM_CTX = { isSystem: true };
|
|
5105
|
+
var DEFAULT_CHUNK_SIZE = 200;
|
|
5106
|
+
function engineCanRollBack(engine) {
|
|
5107
|
+
const e = engine;
|
|
5108
|
+
if (typeof e?.transaction !== "function") return false;
|
|
5109
|
+
const defaultDriverName = e.getDefaultDriverName?.();
|
|
5110
|
+
const defaultDriver = defaultDriverName ? e.getDriverByName?.(defaultDriverName) : void 0;
|
|
5111
|
+
return !defaultDriver || typeof defaultDriver.beginTransaction === "function";
|
|
5112
|
+
}
|
|
5113
|
+
var MigrationPlanRegistry = class {
|
|
5114
|
+
constructor() {
|
|
5115
|
+
this.plans = /* @__PURE__ */ new Map();
|
|
5116
|
+
}
|
|
5117
|
+
register(plan) {
|
|
5118
|
+
this.plans.set(plan.id, plan);
|
|
5119
|
+
}
|
|
5120
|
+
get(planId) {
|
|
5121
|
+
return this.plans.get(planId);
|
|
5122
|
+
}
|
|
5123
|
+
list() {
|
|
5124
|
+
return [...this.plans.values()];
|
|
5125
|
+
}
|
|
5126
|
+
};
|
|
5127
|
+
var MigrationJournalRefusal = class extends Error {
|
|
5128
|
+
constructor(code, message) {
|
|
5129
|
+
super(message);
|
|
5130
|
+
this.name = "MigrationJournalRefusal";
|
|
5131
|
+
this.code = code;
|
|
5132
|
+
}
|
|
5133
|
+
};
|
|
5134
|
+
function planChunks(plan, rowCounts, chunkSize = plan.chunkSize ?? DEFAULT_CHUNK_SIZE) {
|
|
5135
|
+
const size = Math.max(1, chunkSize);
|
|
5136
|
+
const chunks = [];
|
|
5137
|
+
plan.steps.forEach((step, stepIndex) => {
|
|
5138
|
+
const total = rowCounts[stepIndex] ?? 0;
|
|
5139
|
+
for (let offset = 0; offset < total; offset += size) {
|
|
5140
|
+
chunks.push({
|
|
5141
|
+
index: chunks.length,
|
|
5142
|
+
stepIndex,
|
|
5143
|
+
stepName: step.name,
|
|
5144
|
+
offset,
|
|
5145
|
+
length: Math.min(size, total - offset)
|
|
5146
|
+
});
|
|
5147
|
+
}
|
|
5148
|
+
});
|
|
5149
|
+
return chunks;
|
|
5150
|
+
}
|
|
5151
|
+
function hashMigrationPlan(plan, chunks) {
|
|
5152
|
+
const shape = JSON.stringify({
|
|
5153
|
+
id: plan.id,
|
|
5154
|
+
steps: plan.steps.map((s) => s.name),
|
|
5155
|
+
chunks: chunks.map((c) => [c.stepIndex, c.offset, c.length])
|
|
5156
|
+
});
|
|
5157
|
+
return (0, import_node_crypto3.createHash)("sha256").update(shape, "utf8").digest("hex").slice(0, 32);
|
|
5158
|
+
}
|
|
5159
|
+
async function appendEvent(engine, event, execContext) {
|
|
5160
|
+
await engine.insert(
|
|
5161
|
+
import_system3.MIGRATION_JOURNAL_OBJECT,
|
|
5162
|
+
{ ...event, created_at: event.created_at ?? (/* @__PURE__ */ new Date()).toISOString() },
|
|
5163
|
+
{ context: execContext ?? { ...SYSTEM_CTX } }
|
|
5164
|
+
);
|
|
5165
|
+
}
|
|
5166
|
+
async function readRunJournal(engine, runId) {
|
|
5167
|
+
const rows = await engine.find(
|
|
5168
|
+
import_system3.MIGRATION_JOURNAL_OBJECT,
|
|
5169
|
+
{ where: { run_id: runId } },
|
|
5170
|
+
{ context: { ...SYSTEM_CTX } }
|
|
5171
|
+
);
|
|
5172
|
+
return [...rows ?? []].sort((a, b) => Number(a.seq) - Number(b.seq));
|
|
5173
|
+
}
|
|
5174
|
+
function chunkSetOf(events, kind) {
|
|
5175
|
+
const out = /* @__PURE__ */ new Set();
|
|
5176
|
+
for (const e of events) {
|
|
5177
|
+
if (e.kind === kind && typeof e.chunk_index === "number") out.add(e.chunk_index);
|
|
5178
|
+
}
|
|
5179
|
+
return out;
|
|
5180
|
+
}
|
|
5181
|
+
async function findInterruptedRuns(engine) {
|
|
5182
|
+
const started = await engine.find(
|
|
5183
|
+
import_system3.MIGRATION_JOURNAL_OBJECT,
|
|
5184
|
+
{ where: { kind: "run_started" } },
|
|
5185
|
+
{ context: { ...SYSTEM_CTX } }
|
|
5186
|
+
);
|
|
5187
|
+
const out = [];
|
|
5188
|
+
for (const start of started ?? []) {
|
|
5189
|
+
const events = await readRunJournal(engine, start.run_id);
|
|
5190
|
+
if (events.some((e) => e.kind === "run_done")) continue;
|
|
5191
|
+
const committed = chunkSetOf(events, "chunk_done");
|
|
5192
|
+
const compensated = chunkSetOf(events, "compensated");
|
|
5193
|
+
const outstanding = [...committed].filter((i) => !compensated.has(i));
|
|
5194
|
+
if (events.some((e) => e.kind === "run_failed") && outstanding.length === 0) continue;
|
|
5195
|
+
const unknown = [...chunkSetOf(events, "chunk_started")].filter((i) => !committed.has(i));
|
|
5196
|
+
let planId = start.run_id;
|
|
5197
|
+
try {
|
|
5198
|
+
planId = start.detail ? JSON.parse(start.detail).planId ?? start.run_id : start.run_id;
|
|
5199
|
+
} catch {
|
|
5200
|
+
}
|
|
5201
|
+
out.push({
|
|
5202
|
+
runId: start.run_id,
|
|
5203
|
+
planId,
|
|
5204
|
+
planHash: start.plan_hash ?? "",
|
|
5205
|
+
migrationId: start.migration_id,
|
|
5206
|
+
startedAt: start.created_at,
|
|
5207
|
+
committedChunks: [...committed].sort((a, b) => a - b),
|
|
5208
|
+
unknownChunks: unknown.sort((a, b) => a - b),
|
|
5209
|
+
compensatedChunks: [...compensated].sort((a, b) => a - b)
|
|
5210
|
+
});
|
|
5211
|
+
}
|
|
5212
|
+
return out;
|
|
5213
|
+
}
|
|
5214
|
+
async function loadPlan(engine, plan, chunkSize) {
|
|
5215
|
+
const rowsByStep = [];
|
|
5216
|
+
for (const step of plan.steps) rowsByStep.push(await step.load(engine) ?? []);
|
|
5217
|
+
const chunks = planChunks(plan, rowsByStep.map((r) => r.length), chunkSize ?? plan.chunkSize);
|
|
5218
|
+
return { chunks, planHash: hashMigrationPlan(plan, chunks), rowsByStep };
|
|
5219
|
+
}
|
|
5220
|
+
async function runMigrationJournal(engine, plan, options = {}) {
|
|
5221
|
+
const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
5222
|
+
if (!engineCanRollBack(engine)) {
|
|
5223
|
+
throw new MigrationJournalRefusal(
|
|
5224
|
+
"NOT_IMPLEMENTED",
|
|
5225
|
+
`Migration plan '${plan.id}' requires engine transaction support; this runtime cannot roll back. The journal's chunk_done markers would not mean "committed", so the run is refused rather than started.`
|
|
5226
|
+
);
|
|
5227
|
+
}
|
|
5228
|
+
const { chunks, planHash, rowsByStep } = await loadPlan(engine, plan, options.chunkSize);
|
|
5229
|
+
const resuming = Boolean(options.runId);
|
|
5230
|
+
const runId = options.runId ?? (0, import_node_crypto3.randomUUID)();
|
|
5231
|
+
let events = [];
|
|
5232
|
+
let seq = 0;
|
|
5233
|
+
let committed = /* @__PURE__ */ new Set();
|
|
5234
|
+
let compensated = /* @__PURE__ */ new Set();
|
|
5235
|
+
const attemptsByChunk = /* @__PURE__ */ new Map();
|
|
5236
|
+
if (resuming) {
|
|
5237
|
+
events = await readRunJournal(engine, runId);
|
|
5238
|
+
if (events.length === 0) {
|
|
5239
|
+
throw new MigrationJournalRefusal("NO_SUCH_RUN", `No journal rows for run '${runId}'.`);
|
|
5240
|
+
}
|
|
5241
|
+
const start = events.find((e) => e.kind === "run_started");
|
|
5242
|
+
if (start?.plan_hash && start.plan_hash !== planHash) {
|
|
5243
|
+
throw new MigrationJournalRefusal(
|
|
5244
|
+
"PLAN_CHANGED",
|
|
5245
|
+
`Refusing to resume run '${runId}': plan hash ${planHash} does not match the journal's ${start.plan_hash}. The chunk boundaries recorded in the journal describe a different plan.`
|
|
5246
|
+
);
|
|
5247
|
+
}
|
|
5248
|
+
if (events.some((e) => e.kind === "run_done")) {
|
|
5249
|
+
return {
|
|
5250
|
+
runId,
|
|
5251
|
+
status: "completed",
|
|
5252
|
+
chunksTotal: chunks.length,
|
|
5253
|
+
chunksCommitted: chunkSetOf(events, "chunk_done").size,
|
|
5254
|
+
chunksCompensated: chunkSetOf(events, "compensated").size,
|
|
5255
|
+
planHash
|
|
5256
|
+
};
|
|
5257
|
+
}
|
|
5258
|
+
seq = events.reduce((m, e) => Math.max(m, Number(e.seq) + 1), 0);
|
|
5259
|
+
committed = chunkSetOf(events, "chunk_done");
|
|
5260
|
+
compensated = chunkSetOf(events, "compensated");
|
|
5261
|
+
for (const e of events) {
|
|
5262
|
+
if (e.kind === "chunk_started" && typeof e.chunk_index === "number") {
|
|
5263
|
+
attemptsByChunk.set(e.chunk_index, (attemptsByChunk.get(e.chunk_index) ?? 0) + 1);
|
|
5264
|
+
}
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
for (const step of plan.steps) {
|
|
5268
|
+
if (!step.preflight) continue;
|
|
5269
|
+
try {
|
|
5270
|
+
await step.preflight(engine);
|
|
5271
|
+
} catch (err) {
|
|
5272
|
+
throw new MigrationJournalRefusal(
|
|
5273
|
+
"PREFLIGHT_FAILED",
|
|
5274
|
+
`Migration plan '${plan.id}' refused: preflight for step '${step.name}' failed: ${errText(err)}`
|
|
5275
|
+
);
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
if (plan.onCrash === "compensate") {
|
|
5279
|
+
const missing = plan.steps.filter((s) => !s.compensate).map((s) => s.name);
|
|
5280
|
+
if (missing.length > 0) {
|
|
5281
|
+
throw new MigrationJournalRefusal(
|
|
5282
|
+
"NOT_COMPENSABLE",
|
|
5283
|
+
`Migration plan '${plan.id}' declares onCrash: 'compensate' but step(s) ${missing.join(", ")} declare no compensate().`
|
|
5284
|
+
);
|
|
5285
|
+
}
|
|
5286
|
+
}
|
|
5287
|
+
const rowsOf = (c) => rowsByStep[c.stepIndex].slice(c.offset, c.offset + c.length);
|
|
5288
|
+
const next = () => seq++;
|
|
5289
|
+
if (!resuming) {
|
|
5290
|
+
await appendEvent(engine, {
|
|
5291
|
+
run_id: runId,
|
|
5292
|
+
seq: next(),
|
|
5293
|
+
kind: "run_started",
|
|
5294
|
+
plan_hash: planHash,
|
|
5295
|
+
migration_id: plan.migrationId,
|
|
5296
|
+
created_at: now(),
|
|
5297
|
+
detail: JSON.stringify({
|
|
5298
|
+
planId: plan.id,
|
|
5299
|
+
onCrash: plan.onCrash ?? "resume",
|
|
5300
|
+
chunks: chunks.map((c) => ({ i: c.index, step: c.stepName, offset: c.offset, length: c.length }))
|
|
5301
|
+
})
|
|
5302
|
+
});
|
|
5303
|
+
}
|
|
5304
|
+
if (resuming && plan.onCrash === "compensate") {
|
|
5305
|
+
return await unwind(engine, plan, {
|
|
5306
|
+
runId,
|
|
5307
|
+
planHash,
|
|
5308
|
+
chunks,
|
|
5309
|
+
rowsOf,
|
|
5310
|
+
next,
|
|
5311
|
+
now,
|
|
5312
|
+
committed,
|
|
5313
|
+
compensated,
|
|
5314
|
+
chunksTotal: chunks.length,
|
|
5315
|
+
cause: new Error(`run '${runId}' rediscovered after interruption; plan policy is compensate`)
|
|
5316
|
+
});
|
|
5317
|
+
}
|
|
5318
|
+
for (const chunk of chunks) {
|
|
5319
|
+
if (committed.has(chunk.index)) continue;
|
|
5320
|
+
const attempt = (attemptsByChunk.get(chunk.index) ?? 0) + 1;
|
|
5321
|
+
attemptsByChunk.set(chunk.index, attempt);
|
|
5322
|
+
const step = plan.steps[chunk.stepIndex];
|
|
5323
|
+
const rows = rowsOf(chunk);
|
|
5324
|
+
await appendEvent(engine, {
|
|
5325
|
+
run_id: runId,
|
|
5326
|
+
seq: next(),
|
|
5327
|
+
kind: "chunk_started",
|
|
5328
|
+
chunk_index: chunk.index,
|
|
5329
|
+
attempt,
|
|
5330
|
+
migration_id: plan.migrationId,
|
|
5331
|
+
created_at: now()
|
|
5332
|
+
});
|
|
5333
|
+
try {
|
|
5334
|
+
await engine.transaction(async (trxCtx) => {
|
|
5335
|
+
await step.forward(rows, { runId, chunkIndex: chunk.index, attempt, context: trxCtx }, engine);
|
|
5336
|
+
await appendEvent(
|
|
5337
|
+
engine,
|
|
5338
|
+
{
|
|
5339
|
+
run_id: runId,
|
|
5340
|
+
seq: next(),
|
|
5341
|
+
kind: "chunk_done",
|
|
5342
|
+
chunk_index: chunk.index,
|
|
5343
|
+
attempt,
|
|
5344
|
+
migration_id: plan.migrationId,
|
|
5345
|
+
created_at: now()
|
|
5346
|
+
},
|
|
5347
|
+
trxCtx
|
|
5348
|
+
);
|
|
5349
|
+
}, { ...SYSTEM_CTX });
|
|
5350
|
+
committed.add(chunk.index);
|
|
5351
|
+
} catch (err) {
|
|
5352
|
+
return await unwind(engine, plan, {
|
|
5353
|
+
runId,
|
|
5354
|
+
planHash,
|
|
5355
|
+
chunks,
|
|
5356
|
+
rowsOf,
|
|
5357
|
+
next,
|
|
5358
|
+
now,
|
|
5359
|
+
committed,
|
|
5360
|
+
compensated,
|
|
5361
|
+
chunksTotal: chunks.length,
|
|
5362
|
+
cause: err
|
|
5363
|
+
});
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
5366
|
+
await appendEvent(engine, {
|
|
5367
|
+
run_id: runId,
|
|
5368
|
+
seq: next(),
|
|
5369
|
+
kind: "run_done",
|
|
5370
|
+
migration_id: plan.migrationId,
|
|
5371
|
+
created_at: now()
|
|
5372
|
+
});
|
|
5373
|
+
return {
|
|
5374
|
+
runId,
|
|
5375
|
+
status: "completed",
|
|
5376
|
+
chunksTotal: chunks.length,
|
|
5377
|
+
chunksCommitted: committed.size,
|
|
5378
|
+
chunksCompensated: compensated.size,
|
|
5379
|
+
planHash
|
|
5380
|
+
};
|
|
5381
|
+
}
|
|
5382
|
+
async function unwind(engine, plan, a) {
|
|
5383
|
+
const order = [...a.committed].sort((x, y) => y - x);
|
|
5384
|
+
for (const index of order) {
|
|
5385
|
+
if (a.compensated.has(index)) continue;
|
|
5386
|
+
const chunk = a.chunks[index];
|
|
5387
|
+
const step = plan.steps[chunk.stepIndex];
|
|
5388
|
+
if (!step.compensate) {
|
|
5389
|
+
await appendEvent(engine, {
|
|
5390
|
+
run_id: a.runId,
|
|
5391
|
+
seq: a.next(),
|
|
5392
|
+
kind: "run_failed",
|
|
5393
|
+
chunk_index: index,
|
|
5394
|
+
migration_id: plan.migrationId,
|
|
5395
|
+
created_at: a.now(),
|
|
5396
|
+
detail: JSON.stringify({
|
|
5397
|
+
phase: "compensate",
|
|
5398
|
+
reason: "step declares no compensate()",
|
|
5399
|
+
step: step.name,
|
|
5400
|
+
cause: errText(a.cause)
|
|
5401
|
+
})
|
|
5402
|
+
});
|
|
5403
|
+
return {
|
|
5404
|
+
runId: a.runId,
|
|
5405
|
+
status: "failed",
|
|
5406
|
+
chunksTotal: a.chunksTotal,
|
|
5407
|
+
chunksCommitted: a.committed.size,
|
|
5408
|
+
chunksCompensated: a.compensated.size,
|
|
5409
|
+
planHash: a.planHash,
|
|
5410
|
+
error: a.cause
|
|
5411
|
+
};
|
|
5412
|
+
}
|
|
5413
|
+
const attempt = 1;
|
|
5414
|
+
try {
|
|
5415
|
+
await engine.transaction(async (trxCtx) => {
|
|
5416
|
+
await step.compensate(a.rowsOf(chunk), { runId: a.runId, chunkIndex: index, attempt, context: trxCtx }, engine);
|
|
5417
|
+
await appendEvent(
|
|
5418
|
+
engine,
|
|
5419
|
+
{
|
|
5420
|
+
run_id: a.runId,
|
|
5421
|
+
seq: a.next(),
|
|
5422
|
+
kind: "compensated",
|
|
5423
|
+
chunk_index: index,
|
|
5424
|
+
attempt,
|
|
5425
|
+
migration_id: plan.migrationId,
|
|
5426
|
+
created_at: a.now()
|
|
5427
|
+
},
|
|
5428
|
+
trxCtx
|
|
5429
|
+
);
|
|
5430
|
+
}, { ...SYSTEM_CTX });
|
|
5431
|
+
a.compensated.add(index);
|
|
5432
|
+
} catch (err) {
|
|
5433
|
+
await appendEvent(engine, {
|
|
5434
|
+
run_id: a.runId,
|
|
5435
|
+
seq: a.next(),
|
|
5436
|
+
kind: "run_failed",
|
|
5437
|
+
chunk_index: index,
|
|
5438
|
+
migration_id: plan.migrationId,
|
|
5439
|
+
created_at: a.now(),
|
|
5440
|
+
detail: JSON.stringify({
|
|
5441
|
+
phase: "compensate",
|
|
5442
|
+
step: step.name,
|
|
5443
|
+
error: errText(err),
|
|
5444
|
+
cause: errText(a.cause)
|
|
5445
|
+
})
|
|
5446
|
+
});
|
|
5447
|
+
return {
|
|
5448
|
+
runId: a.runId,
|
|
5449
|
+
status: "failed",
|
|
5450
|
+
chunksTotal: a.chunksTotal,
|
|
5451
|
+
chunksCommitted: a.committed.size,
|
|
5452
|
+
chunksCompensated: a.compensated.size,
|
|
5453
|
+
planHash: a.planHash,
|
|
5454
|
+
error: err
|
|
5455
|
+
};
|
|
5456
|
+
}
|
|
5457
|
+
}
|
|
5458
|
+
await appendEvent(engine, {
|
|
5459
|
+
run_id: a.runId,
|
|
5460
|
+
seq: a.next(),
|
|
5461
|
+
kind: "run_failed",
|
|
5462
|
+
migration_id: plan.migrationId,
|
|
5463
|
+
created_at: a.now(),
|
|
5464
|
+
detail: JSON.stringify({ phase: "forward", error: errText(a.cause), compensated: [...a.compensated].sort((x, y) => x - y) })
|
|
5465
|
+
});
|
|
5466
|
+
return {
|
|
5467
|
+
runId: a.runId,
|
|
5468
|
+
status: "compensated",
|
|
5469
|
+
chunksTotal: a.chunksTotal,
|
|
5470
|
+
chunksCommitted: a.committed.size,
|
|
5471
|
+
chunksCompensated: a.compensated.size,
|
|
5472
|
+
planHash: a.planHash,
|
|
5473
|
+
error: a.cause
|
|
5474
|
+
};
|
|
5475
|
+
}
|
|
5476
|
+
async function resumeMigrationJournal(engine, plan, runId, options = {}) {
|
|
5477
|
+
return runMigrationJournal(engine, plan, { ...options, runId });
|
|
5478
|
+
}
|
|
5479
|
+
function errText(err) {
|
|
5480
|
+
if (err instanceof Error) return err.message;
|
|
5481
|
+
try {
|
|
5482
|
+
return String(err);
|
|
5483
|
+
} catch {
|
|
5484
|
+
return "<unprintable error>";
|
|
5485
|
+
}
|
|
5486
|
+
}
|
|
5487
|
+
|
|
4945
5488
|
// src/utils/filter-tokens.ts
|
|
4946
|
-
var
|
|
5489
|
+
var import_data2 = require("@objectstack/spec/data");
|
|
4947
5490
|
var UnknownFilterTokenError = class extends Error {
|
|
4948
5491
|
constructor(token, suggestion) {
|
|
4949
5492
|
super(
|
|
@@ -5094,7 +5637,7 @@ function resolveFilterToken(token, ctx = {}) {
|
|
|
5094
5637
|
}
|
|
5095
5638
|
const period = resolvePeriodToken(token, today);
|
|
5096
5639
|
if (period !== void 0) return period;
|
|
5097
|
-
const param = (0,
|
|
5640
|
+
const param = (0, import_data2.parseDateMacroParam)(token);
|
|
5098
5641
|
if (param) {
|
|
5099
5642
|
const sign = param.direction === "ago" ? -1 : 1;
|
|
5100
5643
|
if (param.unit === "minute" || param.unit === "hour") {
|
|
@@ -5105,7 +5648,7 @@ function resolveFilterToken(token, ctx = {}) {
|
|
|
5105
5648
|
return void 0;
|
|
5106
5649
|
}
|
|
5107
5650
|
function hasFilterToken(node) {
|
|
5108
|
-
if (typeof node === "string") return (0,
|
|
5651
|
+
if (typeof node === "string") return (0, import_data2.classifyFilterToken)(node) !== null;
|
|
5109
5652
|
if (Array.isArray(node)) return node.some(hasFilterToken);
|
|
5110
5653
|
if (node && typeof node === "object" && !(node instanceof Date)) {
|
|
5111
5654
|
return Object.values(node).some(hasFilterToken);
|
|
@@ -5118,7 +5661,7 @@ function resolveFilterTokens(filter, ctx = {}) {
|
|
|
5118
5661
|
const pinned = { ...ctx, now: ctx.now ?? /* @__PURE__ */ new Date() };
|
|
5119
5662
|
const walk = (node) => {
|
|
5120
5663
|
if (typeof node === "string") {
|
|
5121
|
-
const cls = (0,
|
|
5664
|
+
const cls = (0, import_data2.classifyFilterToken)(node);
|
|
5122
5665
|
if (!cls) return node;
|
|
5123
5666
|
if (cls.kind === "unknown") throw new UnknownFilterTokenError(cls.token, cls.suggestion);
|
|
5124
5667
|
const resolved = resolveFilterToken(cls.token, pinned);
|
|
@@ -5388,7 +5931,7 @@ var PluginHealthMonitor = class {
|
|
|
5388
5931
|
};
|
|
5389
5932
|
|
|
5390
5933
|
// src/hot-reload.ts
|
|
5391
|
-
var
|
|
5934
|
+
var import_node_crypto4 = require("crypto");
|
|
5392
5935
|
var generateUUID = () => {
|
|
5393
5936
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
5394
5937
|
return crypto.randomUUID();
|
|
@@ -5483,7 +6026,7 @@ var PluginStateManager = class {
|
|
|
5483
6026
|
*/
|
|
5484
6027
|
calculateChecksum(state) {
|
|
5485
6028
|
const stateStr = JSON.stringify(state);
|
|
5486
|
-
return (0,
|
|
6029
|
+
return (0, import_node_crypto4.createHash)("sha256").update(stateStr).digest("hex");
|
|
5487
6030
|
}
|
|
5488
6031
|
/**
|
|
5489
6032
|
* Shutdown state manager
|
|
@@ -6070,6 +6613,8 @@ var NamespaceResolver = class {
|
|
|
6070
6613
|
DependencyResolver,
|
|
6071
6614
|
HotReloadManager,
|
|
6072
6615
|
LiteKernel,
|
|
6616
|
+
MigrationJournalRefusal,
|
|
6617
|
+
MigrationPlanRegistry,
|
|
6073
6618
|
NamespaceResolver,
|
|
6074
6619
|
ObjectKernel,
|
|
6075
6620
|
ObjectKernelBase,
|
|
@@ -6092,6 +6637,7 @@ var NamespaceResolver = class {
|
|
|
6092
6637
|
ServiceLifecycle,
|
|
6093
6638
|
UnknownFilterTokenError,
|
|
6094
6639
|
UnresolvedFilterTokenError,
|
|
6640
|
+
assertInitServiceRequirements,
|
|
6095
6641
|
bucketKeyToCalendarRange,
|
|
6096
6642
|
buildPermissionsFromGrants,
|
|
6097
6643
|
bulkWrite,
|
|
@@ -6110,33 +6656,45 @@ var NamespaceResolver = class {
|
|
|
6110
6656
|
deepMerge,
|
|
6111
6657
|
defaultIsTransientError,
|
|
6112
6658
|
derivePosture,
|
|
6659
|
+
describeInitOrderFault,
|
|
6660
|
+
engineCanRollBack,
|
|
6113
6661
|
evaluateAuthGate,
|
|
6114
6662
|
extractApiKey,
|
|
6115
6663
|
filterTokenContextFrom,
|
|
6664
|
+
findInterruptedRuns,
|
|
6116
6665
|
generateApiKey,
|
|
6117
6666
|
generateEd25519KeyPair,
|
|
6118
6667
|
getEnv,
|
|
6119
6668
|
getMemoryUsage,
|
|
6120
6669
|
hashApiKey,
|
|
6670
|
+
hashMigrationPlan,
|
|
6121
6671
|
isAuthGateAllowlisted,
|
|
6122
6672
|
isExpired,
|
|
6123
6673
|
isGrantActive,
|
|
6124
6674
|
isGrantExpired,
|
|
6125
6675
|
isNode,
|
|
6676
|
+
nextUtcCalendarDay,
|
|
6126
6677
|
parseScopes,
|
|
6127
6678
|
parseSignature,
|
|
6679
|
+
planChunks,
|
|
6128
6680
|
postureVisibleRows,
|
|
6129
6681
|
readAuthoredTranslationLayer,
|
|
6682
|
+
readRunJournal,
|
|
6130
6683
|
resolveApiKeyPrincipal,
|
|
6131
6684
|
resolveAuthzContext,
|
|
6132
6685
|
resolveFilterToken,
|
|
6133
6686
|
resolveFilterTokens,
|
|
6134
6687
|
resolveLocale,
|
|
6135
6688
|
resolveLocalizationContext,
|
|
6689
|
+
resolvePluginOrder,
|
|
6136
6690
|
resolveUserAuthzGrants,
|
|
6691
|
+
resumeMigrationJournal,
|
|
6692
|
+
runMigrationJournal,
|
|
6137
6693
|
safeExit,
|
|
6138
6694
|
shouldDenyAnonymous,
|
|
6139
6695
|
signPayload,
|
|
6696
|
+
utcInstantMs,
|
|
6697
|
+
validateInitServiceContract,
|
|
6140
6698
|
verifyPayload,
|
|
6141
6699
|
verifyPlatformSignature,
|
|
6142
6700
|
verifyPluginArtifact,
|