@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.js
CHANGED
|
@@ -10,6 +10,89 @@ var __export = (target, all) => {
|
|
|
10
10
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
// src/plugin-order.ts
|
|
14
|
+
function resolvePluginOrder(plugins) {
|
|
15
|
+
const resolved = [];
|
|
16
|
+
const visited = /* @__PURE__ */ new Set();
|
|
17
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
18
|
+
const visit = (pluginName) => {
|
|
19
|
+
if (visited.has(pluginName)) return;
|
|
20
|
+
if (visiting.has(pluginName)) {
|
|
21
|
+
throw new Error(`[Kernel] Circular dependency detected: ${pluginName}`);
|
|
22
|
+
}
|
|
23
|
+
const plugin = plugins.get(pluginName);
|
|
24
|
+
if (!plugin) {
|
|
25
|
+
throw new Error(`[Kernel] Plugin '${pluginName}' not found`);
|
|
26
|
+
}
|
|
27
|
+
visiting.add(pluginName);
|
|
28
|
+
for (const dep of plugin.dependencies ?? []) {
|
|
29
|
+
if (!plugins.has(dep)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`[Kernel] Dependency '${dep}' not found for plugin '${pluginName}'`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
visit(dep);
|
|
35
|
+
}
|
|
36
|
+
for (const dep of plugin.optionalDependencies ?? []) {
|
|
37
|
+
if (plugins.has(dep)) visit(dep);
|
|
38
|
+
}
|
|
39
|
+
visiting.delete(pluginName);
|
|
40
|
+
visited.add(pluginName);
|
|
41
|
+
resolved.push(plugin);
|
|
42
|
+
};
|
|
43
|
+
for (const pluginName of plugins.keys()) {
|
|
44
|
+
visit(pluginName);
|
|
45
|
+
}
|
|
46
|
+
return resolved;
|
|
47
|
+
}
|
|
48
|
+
function validateInitServiceContract(ordered, isServiceRegistered) {
|
|
49
|
+
const providerSlot = /* @__PURE__ */ new Map();
|
|
50
|
+
ordered.forEach((plugin, slot) => {
|
|
51
|
+
for (const service of plugin.providesServices ?? []) {
|
|
52
|
+
if (!providerSlot.has(service)) {
|
|
53
|
+
providerSlot.set(service, { plugin: plugin.name, slot });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const violations = [];
|
|
58
|
+
ordered.forEach((plugin, slot) => {
|
|
59
|
+
for (const service of plugin.requiresServices ?? []) {
|
|
60
|
+
if (isServiceRegistered(service)) continue;
|
|
61
|
+
const provider = providerSlot.get(service);
|
|
62
|
+
if (provider && provider.slot > slot) {
|
|
63
|
+
violations.push(
|
|
64
|
+
`'${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.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
if (violations.length > 0) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`[Kernel] Plugin ordering contract violated (#4131):
|
|
72
|
+
- ${violations.join("\n - ")}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function describeInitOrderFault(currentlyInitializing, plugins, serviceName) {
|
|
77
|
+
if (!currentlyInitializing) return "";
|
|
78
|
+
let providerHint = "";
|
|
79
|
+
for (const plugin of plugins) {
|
|
80
|
+
if (plugin.providesServices?.includes(serviceName)) {
|
|
81
|
+
providerHint = ` '${serviceName}' is provided by composed plugin '${plugin.name}', which has not initialized yet \u2014 declare it in the requiring plugin's dependencies/optionalDependencies.`;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return ` (while plugin '${currentlyInitializing}' was initializing \u2014 a composition/ordering fault, #4131.${providerHint})`;
|
|
86
|
+
}
|
|
87
|
+
function assertInitServiceRequirements(plugin, isServiceRegistered) {
|
|
88
|
+
for (const service of plugin.requiresServices ?? []) {
|
|
89
|
+
if (isServiceRegistered(service)) continue;
|
|
90
|
+
throw new Error(
|
|
91
|
+
`[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).`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
13
96
|
// src/kernel-base.ts
|
|
14
97
|
var ObjectKernelBase = class {
|
|
15
98
|
constructor(logger) {
|
|
@@ -60,7 +143,9 @@ var ObjectKernelBase = class {
|
|
|
60
143
|
if (this.services instanceof Map) {
|
|
61
144
|
const service = this.services.get(name);
|
|
62
145
|
if (!service) {
|
|
63
|
-
throw new Error(
|
|
146
|
+
throw new Error(
|
|
147
|
+
`[Kernel] Service '${name}' not found${this.describeInitOrderFault(name)}`
|
|
148
|
+
);
|
|
64
149
|
}
|
|
65
150
|
return service;
|
|
66
151
|
} else {
|
|
@@ -111,40 +196,37 @@ var ObjectKernelBase = class {
|
|
|
111
196
|
};
|
|
112
197
|
}
|
|
113
198
|
/**
|
|
114
|
-
* Resolve plugin dependencies using topological sort
|
|
199
|
+
* Resolve plugin dependencies using topological sort — `dependencies`
|
|
200
|
+
* hard, `optionalDependencies` order-if-present (ADR-0116, #4131). One
|
|
201
|
+
* implementation shared with ObjectKernel via `plugin-order.ts`.
|
|
115
202
|
* @returns Ordered list of plugins (dependencies first)
|
|
116
203
|
*/
|
|
117
204
|
resolveDependencies() {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
};
|
|
144
|
-
for (const pluginName of this.plugins.keys()) {
|
|
145
|
-
visit(pluginName);
|
|
146
|
-
}
|
|
147
|
-
return resolved;
|
|
205
|
+
return resolvePluginOrder(this.plugins);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Whether a service is registered on this kernel right now. Backs the
|
|
209
|
+
* init-service contract checks (#4131).
|
|
210
|
+
*/
|
|
211
|
+
hasRegisteredService(name) {
|
|
212
|
+
return this.services.has(name);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Pre-Phase-1 ordering validation (ADR-0116, #4131): a plugin whose
|
|
216
|
+
* `requiresServices` names a service provided only by a LATER plugin is
|
|
217
|
+
* a named boot error before any init side effects.
|
|
218
|
+
*/
|
|
219
|
+
validateInitServices(ordered) {
|
|
220
|
+
validateInitServiceContract(ordered, (name) => this.hasRegisteredService(name));
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* When a getService miss happens while a plugin's init() is running,
|
|
224
|
+
* append the structural diagnosis (#4131): which plugin was initializing,
|
|
225
|
+
* and — when a composed plugin declares the service — who provides it.
|
|
226
|
+
* Empty string outside Phase 1, so non-boot messages stay unchanged.
|
|
227
|
+
*/
|
|
228
|
+
describeInitOrderFault(serviceName) {
|
|
229
|
+
return describeInitOrderFault(this.currentlyInitializing, this.plugins.values(), serviceName);
|
|
148
230
|
}
|
|
149
231
|
/**
|
|
150
232
|
* Run plugin init phase
|
|
@@ -153,12 +235,16 @@ var ObjectKernelBase = class {
|
|
|
153
235
|
async runPluginInit(plugin) {
|
|
154
236
|
const pluginName = plugin.name;
|
|
155
237
|
this.logger.info(`Initializing plugin: ${pluginName}`);
|
|
238
|
+
assertInitServiceRequirements(plugin, (name) => this.hasRegisteredService(name));
|
|
239
|
+
this.currentlyInitializing = pluginName;
|
|
156
240
|
try {
|
|
157
241
|
await plugin.init(this.context);
|
|
158
242
|
this.logger.info(`Plugin initialized: ${pluginName}`);
|
|
159
243
|
} catch (error) {
|
|
160
244
|
this.logger.error(`Plugin init failed: ${pluginName}`, error);
|
|
161
245
|
throw error;
|
|
246
|
+
} finally {
|
|
247
|
+
this.currentlyInitializing = void 0;
|
|
162
248
|
}
|
|
163
249
|
}
|
|
164
250
|
/**
|
|
@@ -1011,7 +1097,11 @@ function createMemoryCache() {
|
|
|
1011
1097
|
let hits = 0;
|
|
1012
1098
|
let misses = 0;
|
|
1013
1099
|
return {
|
|
1014
|
-
|
|
1100
|
+
__serviceInfo: {
|
|
1101
|
+
status: "degraded",
|
|
1102
|
+
handlerReady: false,
|
|
1103
|
+
message: "In-process Map cache \u2014 not shared across instances, lost on restart. Register a cache plugin (e.g. Redis) for a real one."
|
|
1104
|
+
},
|
|
1015
1105
|
_serviceName: "cache",
|
|
1016
1106
|
async get(key) {
|
|
1017
1107
|
const entry = store.get(key);
|
|
@@ -1046,7 +1136,11 @@ function createMemoryQueue() {
|
|
|
1046
1136
|
const handlers = /* @__PURE__ */ new Map();
|
|
1047
1137
|
let msgId = 0;
|
|
1048
1138
|
return {
|
|
1049
|
-
|
|
1139
|
+
__serviceInfo: {
|
|
1140
|
+
status: "degraded",
|
|
1141
|
+
handlerReady: false,
|
|
1142
|
+
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."
|
|
1143
|
+
},
|
|
1050
1144
|
_serviceName: "queue",
|
|
1051
1145
|
async publish(queue, data) {
|
|
1052
1146
|
const id = `fallback-msg-${++msgId}`;
|
|
@@ -1073,7 +1167,11 @@ function createMemoryQueue() {
|
|
|
1073
1167
|
function createMemoryJob() {
|
|
1074
1168
|
const jobs = /* @__PURE__ */ new Map();
|
|
1075
1169
|
return {
|
|
1076
|
-
|
|
1170
|
+
__serviceInfo: {
|
|
1171
|
+
status: "degraded",
|
|
1172
|
+
handlerReady: false,
|
|
1173
|
+
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."
|
|
1174
|
+
},
|
|
1077
1175
|
_serviceName: "job",
|
|
1078
1176
|
async schedule(name, schedule, handler) {
|
|
1079
1177
|
jobs.set(name, { schedule, handler });
|
|
@@ -1152,7 +1250,15 @@ function createMemoryI18n() {
|
|
|
1152
1250
|
return void 0;
|
|
1153
1251
|
}
|
|
1154
1252
|
return {
|
|
1155
|
-
|
|
1253
|
+
// [#4058] `degraded` (ADR-0076 D12): translations, locale fallback and
|
|
1254
|
+
// interpolation are all real — what is missing is persistence and the
|
|
1255
|
+
// authoring surface service-i18n adds. `handlerReady` left at the
|
|
1256
|
+
// `degraded` default (true): the dispatcher's `/i18n` domain does serve
|
|
1257
|
+
// this implementation.
|
|
1258
|
+
__serviceInfo: {
|
|
1259
|
+
status: "degraded",
|
|
1260
|
+
message: "In-memory translations \u2014 real lookup and locale fallback, but nothing is persisted. Register I18nServicePlugin from @objectstack/service-i18n for the full implementation."
|
|
1261
|
+
},
|
|
1156
1262
|
_serviceName: "i18n",
|
|
1157
1263
|
t(key, locale, params) {
|
|
1158
1264
|
const data = resolveTranslations(locale) ?? mergedLocale(defaultLocale);
|
|
@@ -1209,7 +1315,14 @@ function createMemoryMetadata() {
|
|
|
1209
1315
|
return map;
|
|
1210
1316
|
}
|
|
1211
1317
|
return {
|
|
1212
|
-
|
|
1318
|
+
// [#4058] `degraded` (ADR-0076 D12): the registry is real — everything
|
|
1319
|
+
// registered is listable and readable back — it simply never reaches disk
|
|
1320
|
+
// or a database. `handlerReady` keeps the `degraded` default (true): the
|
|
1321
|
+
// dispatcher's `/meta` domain serves this implementation.
|
|
1322
|
+
__serviceInfo: {
|
|
1323
|
+
status: "degraded",
|
|
1324
|
+
message: "In-memory metadata registry \u2014 real reads and writes, no persistence (lost on restart). Register MetadataPlugin for a persisted registry."
|
|
1325
|
+
},
|
|
1213
1326
|
_serviceName: "metadata",
|
|
1214
1327
|
async register(type, name, data) {
|
|
1215
1328
|
getTypeMap(type).set(name, data);
|
|
@@ -1425,24 +1538,12 @@ var ObjectKernel = class {
|
|
|
1425
1538
|
this.services.set(name, loaderService);
|
|
1426
1539
|
return loaderService;
|
|
1427
1540
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
});
|
|
1433
|
-
throw new Error(`Service '${name}' is async - use await`);
|
|
1434
|
-
}
|
|
1435
|
-
return service2;
|
|
1436
|
-
} catch (error) {
|
|
1437
|
-
if (error.message?.includes("is async")) {
|
|
1438
|
-
throw error;
|
|
1439
|
-
}
|
|
1440
|
-
const isNotFoundError = error.message === `Service '${name}' not found`;
|
|
1441
|
-
if (!isNotFoundError) {
|
|
1442
|
-
throw error;
|
|
1443
|
-
}
|
|
1444
|
-
throw new Error(`[Kernel] Service '${name}' not found`);
|
|
1541
|
+
if (!this.pluginLoader.hasService(name)) {
|
|
1542
|
+
throw new Error(
|
|
1543
|
+
`[Kernel] Service '${name}' not found${this.describeInitOrderFault(name)}`
|
|
1544
|
+
);
|
|
1445
1545
|
}
|
|
1546
|
+
throw new Error(`Service '${name}' is async - use await`);
|
|
1446
1547
|
},
|
|
1447
1548
|
replaceService: (name, implementation) => {
|
|
1448
1549
|
const hasService = this.services.has(name) || this.pluginLoader.hasService(name);
|
|
@@ -1601,6 +1702,7 @@ var ObjectKernel = class {
|
|
|
1601
1702
|
this.logger.warn("Circular service dependencies detected:", { cycles });
|
|
1602
1703
|
}
|
|
1603
1704
|
const orderedPlugins = this.resolveDependencies();
|
|
1705
|
+
validateInitServiceContract(orderedPlugins, (name) => this.hasAnyService(name));
|
|
1604
1706
|
this.logger.info("Phase 1: Init plugins");
|
|
1605
1707
|
for (const plugin of orderedPlugins) {
|
|
1606
1708
|
await this.initPluginWithTimeout(plugin);
|
|
@@ -1746,13 +1848,70 @@ var ObjectKernel = class {
|
|
|
1746
1848
|
async initPluginWithTimeout(plugin) {
|
|
1747
1849
|
const timeout = plugin.startupTimeout || this.config.defaultStartupTimeout;
|
|
1748
1850
|
this.logger.debug(`Init: ${plugin.name}`, { plugin: plugin.name });
|
|
1749
|
-
|
|
1851
|
+
assertInitServiceRequirements(plugin, (name) => this.hasAnyService(name));
|
|
1852
|
+
this.currentlyInitializing = plugin.name;
|
|
1853
|
+
try {
|
|
1854
|
+
await this.raceStartupTimeout(
|
|
1855
|
+
plugin.init(this.context),
|
|
1856
|
+
timeout,
|
|
1857
|
+
`Plugin ${plugin.name} init timeout after ${timeout}ms`
|
|
1858
|
+
);
|
|
1859
|
+
} finally {
|
|
1860
|
+
this.currentlyInitializing = void 0;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Race a plugin lifecycle hook against its startup-timeout guard, and
|
|
1865
|
+
* reclaim the guard the moment the race settles (#4813).
|
|
1866
|
+
*
|
|
1867
|
+
* The guard used to be armed and then abandoned: when the plugin won the
|
|
1868
|
+
* race, its `setTimeout` stayed ref'd in the event loop for the full
|
|
1869
|
+
* `startupTimeout`, so every process idled that long after its work was
|
|
1870
|
+
* done. One `os migrate` finished in 3s and then sat for 120s
|
|
1871
|
+
* (`ObjectQLPlugin.startupTimeout`), held open by 8 orphaned guards — one
|
|
1872
|
+
* per init plus one per start.
|
|
1873
|
+
*
|
|
1874
|
+
* Clearing on settle rather than `unref()`-ing at arm time is deliberate.
|
|
1875
|
+
* An unref'd guard also stops pinning the loop, but it stops being a guard
|
|
1876
|
+
* as well: if the hook never settles and nothing else keeps the loop alive,
|
|
1877
|
+
* Node exits before the timer can fire and the timeout is never reported.
|
|
1878
|
+
* The guard has to stay ref'd exactly as long as the race is undecided,
|
|
1879
|
+
* which is what `clearTimeout` in a `finally` expresses.
|
|
1880
|
+
*
|
|
1881
|
+
* `operation` is widened to `T | PromiseLike<T>` because the Plugin
|
|
1882
|
+
* contract permits a synchronous hook (`init`/`start` return
|
|
1883
|
+
* `void | Promise<void>`); such a hook wins the race immediately and the
|
|
1884
|
+
* guard is reclaimed on the same turn.
|
|
1885
|
+
*/
|
|
1886
|
+
async raceStartupTimeout(operation, timeout, message) {
|
|
1887
|
+
let guard;
|
|
1750
1888
|
const timeoutPromise = new Promise((_, reject) => {
|
|
1751
|
-
setTimeout(() => {
|
|
1752
|
-
reject(new Error(
|
|
1889
|
+
guard = setTimeout(() => {
|
|
1890
|
+
reject(new Error(message));
|
|
1753
1891
|
}, timeout);
|
|
1754
1892
|
});
|
|
1755
|
-
|
|
1893
|
+
try {
|
|
1894
|
+
return await Promise.race([operation, timeoutPromise]);
|
|
1895
|
+
} finally {
|
|
1896
|
+
clearTimeout(guard);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* Whether a service is resolvable on this kernel right now — direct
|
|
1901
|
+
* registration or a loader-registered factory. Backs the init-service
|
|
1902
|
+
* contract checks (#4131).
|
|
1903
|
+
*/
|
|
1904
|
+
hasAnyService(name) {
|
|
1905
|
+
return this.services.has(name) || this.pluginLoader.hasService(name);
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* When a getService miss happens while a plugin's init() is running,
|
|
1909
|
+
* append the structural diagnosis (#4131): which plugin was initializing,
|
|
1910
|
+
* and — when a composed plugin declares the service — who provides it.
|
|
1911
|
+
* Empty string outside Phase 1, so non-boot messages stay unchanged.
|
|
1912
|
+
*/
|
|
1913
|
+
describeInitOrderFault(serviceName) {
|
|
1914
|
+
return describeInitOrderFault(this.currentlyInitializing, this.plugins.values(), serviceName);
|
|
1756
1915
|
}
|
|
1757
1916
|
async startPluginWithTimeout(plugin) {
|
|
1758
1917
|
if (!plugin.start) {
|
|
@@ -1762,13 +1921,11 @@ var ObjectKernel = class {
|
|
|
1762
1921
|
const startTime = Date.now();
|
|
1763
1922
|
this.logger.debug(`Start: ${plugin.name}`, { plugin: plugin.name });
|
|
1764
1923
|
try {
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
});
|
|
1771
|
-
await Promise.race([startPromise, timeoutPromise]);
|
|
1924
|
+
await this.raceStartupTimeout(
|
|
1925
|
+
plugin.start(this.context),
|
|
1926
|
+
timeout,
|
|
1927
|
+
`Plugin ${plugin.name} start timeout after ${timeout}ms`
|
|
1928
|
+
);
|
|
1772
1929
|
const duration = Date.now() - startTime;
|
|
1773
1930
|
this.startedPlugins.add(plugin.name);
|
|
1774
1931
|
this.pluginStartTimes.set(plugin.name, duration);
|
|
@@ -1826,35 +1983,13 @@ var ObjectKernel = class {
|
|
|
1826
1983
|
}
|
|
1827
1984
|
}
|
|
1828
1985
|
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Topological order over `dependencies` (hard) + `optionalDependencies`
|
|
1988
|
+
* (order-if-present) — ADR-0116, #4131. One implementation shared with
|
|
1989
|
+
* LiteKernel via `plugin-order.ts`.
|
|
1990
|
+
*/
|
|
1829
1991
|
resolveDependencies() {
|
|
1830
|
-
|
|
1831
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1832
|
-
const visiting = /* @__PURE__ */ new Set();
|
|
1833
|
-
const visit = (pluginName) => {
|
|
1834
|
-
if (visited.has(pluginName)) return;
|
|
1835
|
-
if (visiting.has(pluginName)) {
|
|
1836
|
-
throw new Error(`[Kernel] Circular dependency detected: ${pluginName}`);
|
|
1837
|
-
}
|
|
1838
|
-
const plugin = this.plugins.get(pluginName);
|
|
1839
|
-
if (!plugin) {
|
|
1840
|
-
throw new Error(`[Kernel] Plugin '${pluginName}' not found`);
|
|
1841
|
-
}
|
|
1842
|
-
visiting.add(pluginName);
|
|
1843
|
-
const deps = plugin.dependencies || [];
|
|
1844
|
-
for (const dep of deps) {
|
|
1845
|
-
if (!this.plugins.has(dep)) {
|
|
1846
|
-
throw new Error(`[Kernel] Dependency '${dep}' not found for plugin '${pluginName}'`);
|
|
1847
|
-
}
|
|
1848
|
-
visit(dep);
|
|
1849
|
-
}
|
|
1850
|
-
visiting.delete(pluginName);
|
|
1851
|
-
visited.add(pluginName);
|
|
1852
|
-
resolved.push(plugin);
|
|
1853
|
-
};
|
|
1854
|
-
for (const pluginName of this.plugins.keys()) {
|
|
1855
|
-
visit(pluginName);
|
|
1856
|
-
}
|
|
1857
|
-
return resolved;
|
|
1992
|
+
return resolvePluginOrder(this.plugins);
|
|
1858
1993
|
}
|
|
1859
1994
|
registerShutdownSignals() {
|
|
1860
1995
|
const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
|
|
@@ -1920,6 +2055,7 @@ var LiteKernel = class extends ObjectKernelBase {
|
|
|
1920
2055
|
this.state = "initializing";
|
|
1921
2056
|
this.logger.info("Bootstrap started");
|
|
1922
2057
|
const orderedPlugins = this.resolveDependencies();
|
|
2058
|
+
this.validateInitServices(orderedPlugins);
|
|
1923
2059
|
this.logger.info("Phase 1: Init plugins");
|
|
1924
2060
|
for (const plugin of orderedPlugins) {
|
|
1925
2061
|
await this.runPluginInit(plugin);
|
|
@@ -2520,6 +2656,11 @@ function createApiRegistryPlugin(config = {}) {
|
|
|
2520
2656
|
} = config;
|
|
2521
2657
|
return {
|
|
2522
2658
|
name: "com.objectstack.core.api-registry",
|
|
2659
|
+
/**
|
|
2660
|
+
* Services init() registers on every path (ADR-0116, #4131) — lets the
|
|
2661
|
+
* kernel name this plugin when a consumer requires one before it inits.
|
|
2662
|
+
*/
|
|
2663
|
+
providesServices: ["api-registry"],
|
|
2523
2664
|
type: "standard",
|
|
2524
2665
|
version: "1.0.0",
|
|
2525
2666
|
init: async (ctx) => {
|
|
@@ -4587,14 +4728,13 @@ function evaluateAuthGate(sessionUser, path) {
|
|
|
4587
4728
|
|
|
4588
4729
|
// src/security/anonymous-deny.ts
|
|
4589
4730
|
var ANONYMOUS_DENY_STATUS = 401;
|
|
4590
|
-
var ANONYMOUS_DENY_CODE = "
|
|
4731
|
+
var ANONYMOUS_DENY_CODE = "UNAUTHENTICATED";
|
|
4591
4732
|
var ANONYMOUS_DENY_MESSAGE = "Authentication is required to access this endpoint.";
|
|
4592
4733
|
var ANONYMOUS_DENY_BODY = {
|
|
4593
4734
|
error: ANONYMOUS_DENY_CODE,
|
|
4594
4735
|
message: ANONYMOUS_DENY_MESSAGE
|
|
4595
4736
|
};
|
|
4596
4737
|
function shouldDenyAnonymous(input) {
|
|
4597
|
-
if (!input.requireAuth) return false;
|
|
4598
4738
|
if (typeof input.method === "string" && input.method.toUpperCase() === "OPTIONS") {
|
|
4599
4739
|
return false;
|
|
4600
4740
|
}
|
|
@@ -4606,6 +4746,7 @@ function shouldDenyAnonymous(input) {
|
|
|
4606
4746
|
}
|
|
4607
4747
|
|
|
4608
4748
|
// src/utils/datetime.ts
|
|
4749
|
+
import { nextUtcCalendarDay, utcInstantMs } from "@objectstack/spec/data";
|
|
4609
4750
|
function calendarPartsInTz(d, tz) {
|
|
4610
4751
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
4611
4752
|
timeZone: tz,
|
|
@@ -4846,6 +4987,395 @@ async function bulkWrite(rows, opts) {
|
|
|
4846
4987
|
return results;
|
|
4847
4988
|
}
|
|
4848
4989
|
|
|
4990
|
+
// src/utils/migration-journal.ts
|
|
4991
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
4992
|
+
import {
|
|
4993
|
+
MIGRATION_JOURNAL_OBJECT
|
|
4994
|
+
} from "@objectstack/spec/system";
|
|
4995
|
+
var SYSTEM_CTX = { isSystem: true };
|
|
4996
|
+
var DEFAULT_CHUNK_SIZE = 200;
|
|
4997
|
+
function engineCanRollBack(engine) {
|
|
4998
|
+
const e = engine;
|
|
4999
|
+
if (typeof e?.transaction !== "function") return false;
|
|
5000
|
+
const defaultDriverName = e.getDefaultDriverName?.();
|
|
5001
|
+
const defaultDriver = defaultDriverName ? e.getDriverByName?.(defaultDriverName) : void 0;
|
|
5002
|
+
return !defaultDriver || typeof defaultDriver.beginTransaction === "function";
|
|
5003
|
+
}
|
|
5004
|
+
var MigrationPlanRegistry = class {
|
|
5005
|
+
constructor() {
|
|
5006
|
+
this.plans = /* @__PURE__ */ new Map();
|
|
5007
|
+
}
|
|
5008
|
+
register(plan) {
|
|
5009
|
+
this.plans.set(plan.id, plan);
|
|
5010
|
+
}
|
|
5011
|
+
get(planId) {
|
|
5012
|
+
return this.plans.get(planId);
|
|
5013
|
+
}
|
|
5014
|
+
list() {
|
|
5015
|
+
return [...this.plans.values()];
|
|
5016
|
+
}
|
|
5017
|
+
};
|
|
5018
|
+
var MigrationJournalRefusal = class extends Error {
|
|
5019
|
+
constructor(code, message) {
|
|
5020
|
+
super(message);
|
|
5021
|
+
this.name = "MigrationJournalRefusal";
|
|
5022
|
+
this.code = code;
|
|
5023
|
+
}
|
|
5024
|
+
};
|
|
5025
|
+
function planChunks(plan, rowCounts, chunkSize = plan.chunkSize ?? DEFAULT_CHUNK_SIZE) {
|
|
5026
|
+
const size = Math.max(1, chunkSize);
|
|
5027
|
+
const chunks = [];
|
|
5028
|
+
plan.steps.forEach((step, stepIndex) => {
|
|
5029
|
+
const total = rowCounts[stepIndex] ?? 0;
|
|
5030
|
+
for (let offset = 0; offset < total; offset += size) {
|
|
5031
|
+
chunks.push({
|
|
5032
|
+
index: chunks.length,
|
|
5033
|
+
stepIndex,
|
|
5034
|
+
stepName: step.name,
|
|
5035
|
+
offset,
|
|
5036
|
+
length: Math.min(size, total - offset)
|
|
5037
|
+
});
|
|
5038
|
+
}
|
|
5039
|
+
});
|
|
5040
|
+
return chunks;
|
|
5041
|
+
}
|
|
5042
|
+
function hashMigrationPlan(plan, chunks) {
|
|
5043
|
+
const shape = JSON.stringify({
|
|
5044
|
+
id: plan.id,
|
|
5045
|
+
steps: plan.steps.map((s) => s.name),
|
|
5046
|
+
chunks: chunks.map((c) => [c.stepIndex, c.offset, c.length])
|
|
5047
|
+
});
|
|
5048
|
+
return createHash2("sha256").update(shape, "utf8").digest("hex").slice(0, 32);
|
|
5049
|
+
}
|
|
5050
|
+
async function appendEvent(engine, event, execContext) {
|
|
5051
|
+
await engine.insert(
|
|
5052
|
+
MIGRATION_JOURNAL_OBJECT,
|
|
5053
|
+
{ ...event, created_at: event.created_at ?? (/* @__PURE__ */ new Date()).toISOString() },
|
|
5054
|
+
{ context: execContext ?? { ...SYSTEM_CTX } }
|
|
5055
|
+
);
|
|
5056
|
+
}
|
|
5057
|
+
async function readRunJournal(engine, runId) {
|
|
5058
|
+
const rows = await engine.find(
|
|
5059
|
+
MIGRATION_JOURNAL_OBJECT,
|
|
5060
|
+
{ where: { run_id: runId } },
|
|
5061
|
+
{ context: { ...SYSTEM_CTX } }
|
|
5062
|
+
);
|
|
5063
|
+
return [...rows ?? []].sort((a, b) => Number(a.seq) - Number(b.seq));
|
|
5064
|
+
}
|
|
5065
|
+
function chunkSetOf(events, kind) {
|
|
5066
|
+
const out = /* @__PURE__ */ new Set();
|
|
5067
|
+
for (const e of events) {
|
|
5068
|
+
if (e.kind === kind && typeof e.chunk_index === "number") out.add(e.chunk_index);
|
|
5069
|
+
}
|
|
5070
|
+
return out;
|
|
5071
|
+
}
|
|
5072
|
+
async function findInterruptedRuns(engine) {
|
|
5073
|
+
const started = await engine.find(
|
|
5074
|
+
MIGRATION_JOURNAL_OBJECT,
|
|
5075
|
+
{ where: { kind: "run_started" } },
|
|
5076
|
+
{ context: { ...SYSTEM_CTX } }
|
|
5077
|
+
);
|
|
5078
|
+
const out = [];
|
|
5079
|
+
for (const start of started ?? []) {
|
|
5080
|
+
const events = await readRunJournal(engine, start.run_id);
|
|
5081
|
+
if (events.some((e) => e.kind === "run_done")) continue;
|
|
5082
|
+
const committed = chunkSetOf(events, "chunk_done");
|
|
5083
|
+
const compensated = chunkSetOf(events, "compensated");
|
|
5084
|
+
const outstanding = [...committed].filter((i) => !compensated.has(i));
|
|
5085
|
+
if (events.some((e) => e.kind === "run_failed") && outstanding.length === 0) continue;
|
|
5086
|
+
const unknown = [...chunkSetOf(events, "chunk_started")].filter((i) => !committed.has(i));
|
|
5087
|
+
let planId = start.run_id;
|
|
5088
|
+
try {
|
|
5089
|
+
planId = start.detail ? JSON.parse(start.detail).planId ?? start.run_id : start.run_id;
|
|
5090
|
+
} catch {
|
|
5091
|
+
}
|
|
5092
|
+
out.push({
|
|
5093
|
+
runId: start.run_id,
|
|
5094
|
+
planId,
|
|
5095
|
+
planHash: start.plan_hash ?? "",
|
|
5096
|
+
migrationId: start.migration_id,
|
|
5097
|
+
startedAt: start.created_at,
|
|
5098
|
+
committedChunks: [...committed].sort((a, b) => a - b),
|
|
5099
|
+
unknownChunks: unknown.sort((a, b) => a - b),
|
|
5100
|
+
compensatedChunks: [...compensated].sort((a, b) => a - b)
|
|
5101
|
+
});
|
|
5102
|
+
}
|
|
5103
|
+
return out;
|
|
5104
|
+
}
|
|
5105
|
+
async function loadPlan(engine, plan, chunkSize) {
|
|
5106
|
+
const rowsByStep = [];
|
|
5107
|
+
for (const step of plan.steps) rowsByStep.push(await step.load(engine) ?? []);
|
|
5108
|
+
const chunks = planChunks(plan, rowsByStep.map((r) => r.length), chunkSize ?? plan.chunkSize);
|
|
5109
|
+
return { chunks, planHash: hashMigrationPlan(plan, chunks), rowsByStep };
|
|
5110
|
+
}
|
|
5111
|
+
async function runMigrationJournal(engine, plan, options = {}) {
|
|
5112
|
+
const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
5113
|
+
if (!engineCanRollBack(engine)) {
|
|
5114
|
+
throw new MigrationJournalRefusal(
|
|
5115
|
+
"NOT_IMPLEMENTED",
|
|
5116
|
+
`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.`
|
|
5117
|
+
);
|
|
5118
|
+
}
|
|
5119
|
+
const { chunks, planHash, rowsByStep } = await loadPlan(engine, plan, options.chunkSize);
|
|
5120
|
+
const resuming = Boolean(options.runId);
|
|
5121
|
+
const runId = options.runId ?? randomUUID();
|
|
5122
|
+
let events = [];
|
|
5123
|
+
let seq = 0;
|
|
5124
|
+
let committed = /* @__PURE__ */ new Set();
|
|
5125
|
+
let compensated = /* @__PURE__ */ new Set();
|
|
5126
|
+
const attemptsByChunk = /* @__PURE__ */ new Map();
|
|
5127
|
+
if (resuming) {
|
|
5128
|
+
events = await readRunJournal(engine, runId);
|
|
5129
|
+
if (events.length === 0) {
|
|
5130
|
+
throw new MigrationJournalRefusal("NO_SUCH_RUN", `No journal rows for run '${runId}'.`);
|
|
5131
|
+
}
|
|
5132
|
+
const start = events.find((e) => e.kind === "run_started");
|
|
5133
|
+
if (start?.plan_hash && start.plan_hash !== planHash) {
|
|
5134
|
+
throw new MigrationJournalRefusal(
|
|
5135
|
+
"PLAN_CHANGED",
|
|
5136
|
+
`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.`
|
|
5137
|
+
);
|
|
5138
|
+
}
|
|
5139
|
+
if (events.some((e) => e.kind === "run_done")) {
|
|
5140
|
+
return {
|
|
5141
|
+
runId,
|
|
5142
|
+
status: "completed",
|
|
5143
|
+
chunksTotal: chunks.length,
|
|
5144
|
+
chunksCommitted: chunkSetOf(events, "chunk_done").size,
|
|
5145
|
+
chunksCompensated: chunkSetOf(events, "compensated").size,
|
|
5146
|
+
planHash
|
|
5147
|
+
};
|
|
5148
|
+
}
|
|
5149
|
+
seq = events.reduce((m, e) => Math.max(m, Number(e.seq) + 1), 0);
|
|
5150
|
+
committed = chunkSetOf(events, "chunk_done");
|
|
5151
|
+
compensated = chunkSetOf(events, "compensated");
|
|
5152
|
+
for (const e of events) {
|
|
5153
|
+
if (e.kind === "chunk_started" && typeof e.chunk_index === "number") {
|
|
5154
|
+
attemptsByChunk.set(e.chunk_index, (attemptsByChunk.get(e.chunk_index) ?? 0) + 1);
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
}
|
|
5158
|
+
for (const step of plan.steps) {
|
|
5159
|
+
if (!step.preflight) continue;
|
|
5160
|
+
try {
|
|
5161
|
+
await step.preflight(engine);
|
|
5162
|
+
} catch (err) {
|
|
5163
|
+
throw new MigrationJournalRefusal(
|
|
5164
|
+
"PREFLIGHT_FAILED",
|
|
5165
|
+
`Migration plan '${plan.id}' refused: preflight for step '${step.name}' failed: ${errText(err)}`
|
|
5166
|
+
);
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
if (plan.onCrash === "compensate") {
|
|
5170
|
+
const missing = plan.steps.filter((s) => !s.compensate).map((s) => s.name);
|
|
5171
|
+
if (missing.length > 0) {
|
|
5172
|
+
throw new MigrationJournalRefusal(
|
|
5173
|
+
"NOT_COMPENSABLE",
|
|
5174
|
+
`Migration plan '${plan.id}' declares onCrash: 'compensate' but step(s) ${missing.join(", ")} declare no compensate().`
|
|
5175
|
+
);
|
|
5176
|
+
}
|
|
5177
|
+
}
|
|
5178
|
+
const rowsOf = (c) => rowsByStep[c.stepIndex].slice(c.offset, c.offset + c.length);
|
|
5179
|
+
const next = () => seq++;
|
|
5180
|
+
if (!resuming) {
|
|
5181
|
+
await appendEvent(engine, {
|
|
5182
|
+
run_id: runId,
|
|
5183
|
+
seq: next(),
|
|
5184
|
+
kind: "run_started",
|
|
5185
|
+
plan_hash: planHash,
|
|
5186
|
+
migration_id: plan.migrationId,
|
|
5187
|
+
created_at: now(),
|
|
5188
|
+
detail: JSON.stringify({
|
|
5189
|
+
planId: plan.id,
|
|
5190
|
+
onCrash: plan.onCrash ?? "resume",
|
|
5191
|
+
chunks: chunks.map((c) => ({ i: c.index, step: c.stepName, offset: c.offset, length: c.length }))
|
|
5192
|
+
})
|
|
5193
|
+
});
|
|
5194
|
+
}
|
|
5195
|
+
if (resuming && plan.onCrash === "compensate") {
|
|
5196
|
+
return await unwind(engine, plan, {
|
|
5197
|
+
runId,
|
|
5198
|
+
planHash,
|
|
5199
|
+
chunks,
|
|
5200
|
+
rowsOf,
|
|
5201
|
+
next,
|
|
5202
|
+
now,
|
|
5203
|
+
committed,
|
|
5204
|
+
compensated,
|
|
5205
|
+
chunksTotal: chunks.length,
|
|
5206
|
+
cause: new Error(`run '${runId}' rediscovered after interruption; plan policy is compensate`)
|
|
5207
|
+
});
|
|
5208
|
+
}
|
|
5209
|
+
for (const chunk of chunks) {
|
|
5210
|
+
if (committed.has(chunk.index)) continue;
|
|
5211
|
+
const attempt = (attemptsByChunk.get(chunk.index) ?? 0) + 1;
|
|
5212
|
+
attemptsByChunk.set(chunk.index, attempt);
|
|
5213
|
+
const step = plan.steps[chunk.stepIndex];
|
|
5214
|
+
const rows = rowsOf(chunk);
|
|
5215
|
+
await appendEvent(engine, {
|
|
5216
|
+
run_id: runId,
|
|
5217
|
+
seq: next(),
|
|
5218
|
+
kind: "chunk_started",
|
|
5219
|
+
chunk_index: chunk.index,
|
|
5220
|
+
attempt,
|
|
5221
|
+
migration_id: plan.migrationId,
|
|
5222
|
+
created_at: now()
|
|
5223
|
+
});
|
|
5224
|
+
try {
|
|
5225
|
+
await engine.transaction(async (trxCtx) => {
|
|
5226
|
+
await step.forward(rows, { runId, chunkIndex: chunk.index, attempt, context: trxCtx }, engine);
|
|
5227
|
+
await appendEvent(
|
|
5228
|
+
engine,
|
|
5229
|
+
{
|
|
5230
|
+
run_id: runId,
|
|
5231
|
+
seq: next(),
|
|
5232
|
+
kind: "chunk_done",
|
|
5233
|
+
chunk_index: chunk.index,
|
|
5234
|
+
attempt,
|
|
5235
|
+
migration_id: plan.migrationId,
|
|
5236
|
+
created_at: now()
|
|
5237
|
+
},
|
|
5238
|
+
trxCtx
|
|
5239
|
+
);
|
|
5240
|
+
}, { ...SYSTEM_CTX });
|
|
5241
|
+
committed.add(chunk.index);
|
|
5242
|
+
} catch (err) {
|
|
5243
|
+
return await unwind(engine, plan, {
|
|
5244
|
+
runId,
|
|
5245
|
+
planHash,
|
|
5246
|
+
chunks,
|
|
5247
|
+
rowsOf,
|
|
5248
|
+
next,
|
|
5249
|
+
now,
|
|
5250
|
+
committed,
|
|
5251
|
+
compensated,
|
|
5252
|
+
chunksTotal: chunks.length,
|
|
5253
|
+
cause: err
|
|
5254
|
+
});
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
await appendEvent(engine, {
|
|
5258
|
+
run_id: runId,
|
|
5259
|
+
seq: next(),
|
|
5260
|
+
kind: "run_done",
|
|
5261
|
+
migration_id: plan.migrationId,
|
|
5262
|
+
created_at: now()
|
|
5263
|
+
});
|
|
5264
|
+
return {
|
|
5265
|
+
runId,
|
|
5266
|
+
status: "completed",
|
|
5267
|
+
chunksTotal: chunks.length,
|
|
5268
|
+
chunksCommitted: committed.size,
|
|
5269
|
+
chunksCompensated: compensated.size,
|
|
5270
|
+
planHash
|
|
5271
|
+
};
|
|
5272
|
+
}
|
|
5273
|
+
async function unwind(engine, plan, a) {
|
|
5274
|
+
const order = [...a.committed].sort((x, y) => y - x);
|
|
5275
|
+
for (const index of order) {
|
|
5276
|
+
if (a.compensated.has(index)) continue;
|
|
5277
|
+
const chunk = a.chunks[index];
|
|
5278
|
+
const step = plan.steps[chunk.stepIndex];
|
|
5279
|
+
if (!step.compensate) {
|
|
5280
|
+
await appendEvent(engine, {
|
|
5281
|
+
run_id: a.runId,
|
|
5282
|
+
seq: a.next(),
|
|
5283
|
+
kind: "run_failed",
|
|
5284
|
+
chunk_index: index,
|
|
5285
|
+
migration_id: plan.migrationId,
|
|
5286
|
+
created_at: a.now(),
|
|
5287
|
+
detail: JSON.stringify({
|
|
5288
|
+
phase: "compensate",
|
|
5289
|
+
reason: "step declares no compensate()",
|
|
5290
|
+
step: step.name,
|
|
5291
|
+
cause: errText(a.cause)
|
|
5292
|
+
})
|
|
5293
|
+
});
|
|
5294
|
+
return {
|
|
5295
|
+
runId: a.runId,
|
|
5296
|
+
status: "failed",
|
|
5297
|
+
chunksTotal: a.chunksTotal,
|
|
5298
|
+
chunksCommitted: a.committed.size,
|
|
5299
|
+
chunksCompensated: a.compensated.size,
|
|
5300
|
+
planHash: a.planHash,
|
|
5301
|
+
error: a.cause
|
|
5302
|
+
};
|
|
5303
|
+
}
|
|
5304
|
+
const attempt = 1;
|
|
5305
|
+
try {
|
|
5306
|
+
await engine.transaction(async (trxCtx) => {
|
|
5307
|
+
await step.compensate(a.rowsOf(chunk), { runId: a.runId, chunkIndex: index, attempt, context: trxCtx }, engine);
|
|
5308
|
+
await appendEvent(
|
|
5309
|
+
engine,
|
|
5310
|
+
{
|
|
5311
|
+
run_id: a.runId,
|
|
5312
|
+
seq: a.next(),
|
|
5313
|
+
kind: "compensated",
|
|
5314
|
+
chunk_index: index,
|
|
5315
|
+
attempt,
|
|
5316
|
+
migration_id: plan.migrationId,
|
|
5317
|
+
created_at: a.now()
|
|
5318
|
+
},
|
|
5319
|
+
trxCtx
|
|
5320
|
+
);
|
|
5321
|
+
}, { ...SYSTEM_CTX });
|
|
5322
|
+
a.compensated.add(index);
|
|
5323
|
+
} catch (err) {
|
|
5324
|
+
await appendEvent(engine, {
|
|
5325
|
+
run_id: a.runId,
|
|
5326
|
+
seq: a.next(),
|
|
5327
|
+
kind: "run_failed",
|
|
5328
|
+
chunk_index: index,
|
|
5329
|
+
migration_id: plan.migrationId,
|
|
5330
|
+
created_at: a.now(),
|
|
5331
|
+
detail: JSON.stringify({
|
|
5332
|
+
phase: "compensate",
|
|
5333
|
+
step: step.name,
|
|
5334
|
+
error: errText(err),
|
|
5335
|
+
cause: errText(a.cause)
|
|
5336
|
+
})
|
|
5337
|
+
});
|
|
5338
|
+
return {
|
|
5339
|
+
runId: a.runId,
|
|
5340
|
+
status: "failed",
|
|
5341
|
+
chunksTotal: a.chunksTotal,
|
|
5342
|
+
chunksCommitted: a.committed.size,
|
|
5343
|
+
chunksCompensated: a.compensated.size,
|
|
5344
|
+
planHash: a.planHash,
|
|
5345
|
+
error: err
|
|
5346
|
+
};
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
await appendEvent(engine, {
|
|
5350
|
+
run_id: a.runId,
|
|
5351
|
+
seq: a.next(),
|
|
5352
|
+
kind: "run_failed",
|
|
5353
|
+
migration_id: plan.migrationId,
|
|
5354
|
+
created_at: a.now(),
|
|
5355
|
+
detail: JSON.stringify({ phase: "forward", error: errText(a.cause), compensated: [...a.compensated].sort((x, y) => x - y) })
|
|
5356
|
+
});
|
|
5357
|
+
return {
|
|
5358
|
+
runId: a.runId,
|
|
5359
|
+
status: "compensated",
|
|
5360
|
+
chunksTotal: a.chunksTotal,
|
|
5361
|
+
chunksCommitted: a.committed.size,
|
|
5362
|
+
chunksCompensated: a.compensated.size,
|
|
5363
|
+
planHash: a.planHash,
|
|
5364
|
+
error: a.cause
|
|
5365
|
+
};
|
|
5366
|
+
}
|
|
5367
|
+
async function resumeMigrationJournal(engine, plan, runId, options = {}) {
|
|
5368
|
+
return runMigrationJournal(engine, plan, { ...options, runId });
|
|
5369
|
+
}
|
|
5370
|
+
function errText(err) {
|
|
5371
|
+
if (err instanceof Error) return err.message;
|
|
5372
|
+
try {
|
|
5373
|
+
return String(err);
|
|
5374
|
+
} catch {
|
|
5375
|
+
return "<unprintable error>";
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
|
|
4849
5379
|
// src/utils/filter-tokens.ts
|
|
4850
5380
|
import {
|
|
4851
5381
|
classifyFilterToken,
|
|
@@ -5295,7 +5825,7 @@ var PluginHealthMonitor = class {
|
|
|
5295
5825
|
};
|
|
5296
5826
|
|
|
5297
5827
|
// src/hot-reload.ts
|
|
5298
|
-
import { createHash as
|
|
5828
|
+
import { createHash as createHash3 } from "crypto";
|
|
5299
5829
|
var generateUUID = () => {
|
|
5300
5830
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
5301
5831
|
return crypto.randomUUID();
|
|
@@ -5390,7 +5920,7 @@ var PluginStateManager = class {
|
|
|
5390
5920
|
*/
|
|
5391
5921
|
calculateChecksum(state) {
|
|
5392
5922
|
const stateStr = JSON.stringify(state);
|
|
5393
|
-
return
|
|
5923
|
+
return createHash3("sha256").update(stateStr).digest("hex");
|
|
5394
5924
|
}
|
|
5395
5925
|
/**
|
|
5396
5926
|
* Shutdown state manager
|
|
@@ -5976,6 +6506,8 @@ export {
|
|
|
5976
6506
|
DependencyResolver,
|
|
5977
6507
|
HotReloadManager,
|
|
5978
6508
|
LiteKernel,
|
|
6509
|
+
MigrationJournalRefusal,
|
|
6510
|
+
MigrationPlanRegistry,
|
|
5979
6511
|
NamespaceResolver,
|
|
5980
6512
|
ObjectKernel,
|
|
5981
6513
|
ObjectKernelBase,
|
|
@@ -5998,6 +6530,7 @@ export {
|
|
|
5998
6530
|
ServiceLifecycle,
|
|
5999
6531
|
UnknownFilterTokenError,
|
|
6000
6532
|
UnresolvedFilterTokenError,
|
|
6533
|
+
assertInitServiceRequirements,
|
|
6001
6534
|
bucketKeyToCalendarRange,
|
|
6002
6535
|
buildPermissionsFromGrants,
|
|
6003
6536
|
bulkWrite,
|
|
@@ -6016,33 +6549,45 @@ export {
|
|
|
6016
6549
|
deepMerge,
|
|
6017
6550
|
defaultIsTransientError,
|
|
6018
6551
|
derivePosture,
|
|
6552
|
+
describeInitOrderFault,
|
|
6553
|
+
engineCanRollBack,
|
|
6019
6554
|
evaluateAuthGate,
|
|
6020
6555
|
extractApiKey,
|
|
6021
6556
|
filterTokenContextFrom,
|
|
6557
|
+
findInterruptedRuns,
|
|
6022
6558
|
generateApiKey,
|
|
6023
6559
|
generateEd25519KeyPair,
|
|
6024
6560
|
getEnv,
|
|
6025
6561
|
getMemoryUsage,
|
|
6026
6562
|
hashApiKey,
|
|
6563
|
+
hashMigrationPlan,
|
|
6027
6564
|
isAuthGateAllowlisted,
|
|
6028
6565
|
isExpired,
|
|
6029
6566
|
isGrantActive,
|
|
6030
6567
|
isGrantExpired,
|
|
6031
6568
|
isNode,
|
|
6569
|
+
nextUtcCalendarDay,
|
|
6032
6570
|
parseScopes,
|
|
6033
6571
|
parseSignature,
|
|
6572
|
+
planChunks,
|
|
6034
6573
|
postureVisibleRows,
|
|
6035
6574
|
readAuthoredTranslationLayer,
|
|
6575
|
+
readRunJournal,
|
|
6036
6576
|
resolveApiKeyPrincipal,
|
|
6037
6577
|
resolveAuthzContext,
|
|
6038
6578
|
resolveFilterToken,
|
|
6039
6579
|
resolveFilterTokens,
|
|
6040
6580
|
resolveLocale,
|
|
6041
6581
|
resolveLocalizationContext,
|
|
6582
|
+
resolvePluginOrder,
|
|
6042
6583
|
resolveUserAuthzGrants,
|
|
6584
|
+
resumeMigrationJournal,
|
|
6585
|
+
runMigrationJournal,
|
|
6043
6586
|
safeExit,
|
|
6044
6587
|
shouldDenyAnonymous,
|
|
6045
6588
|
signPayload,
|
|
6589
|
+
utcInstantMs,
|
|
6590
|
+
validateInitServiceContract,
|
|
6046
6591
|
verifyPayload,
|
|
6047
6592
|
verifyPlatformSignature,
|
|
6048
6593
|
verifyPluginArtifact,
|