@deepseek-ai/dsh-app-boot 0.2.2-alpha.2 → 0.2.2-rc.1
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/lib/index.js +848 -163
- package/package.json +19 -19
package/lib/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep, w
|
|
|
7
7
|
import * as yaml from "js-yaml";
|
|
8
8
|
import { load } from "js-yaml";
|
|
9
9
|
import Loader, { EntryGroup, EntryTree, ModuleLoader, isJsExpr } from "@deepseek-ai/cordis-plugin-loader";
|
|
10
|
-
import { access, constants, mkdir, readFile, realpath, rename, writeFile } from "node:fs/promises";
|
|
10
|
+
import { access, constants, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
11
11
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
12
12
|
import Group from "@deepseek-ai/cordis-plugin-group";
|
|
13
13
|
import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
@@ -80,8 +80,8 @@ function captureStartupComposition(ctx) {
|
|
|
80
80
|
const impl = store[key];
|
|
81
81
|
if (impl?.name !== service) continue;
|
|
82
82
|
providers.push({
|
|
83
|
-
entry: impl.fiber.entry?.id ?? "unknown",
|
|
84
|
-
state: stateName(impl.fiber.state),
|
|
83
|
+
entry: impl.ctx.fiber.entry?.id ?? "unknown",
|
|
84
|
+
state: stateName(impl.ctx.fiber.state),
|
|
85
85
|
visibility: key === scope ? "same-scope" : "other-scope"
|
|
86
86
|
});
|
|
87
87
|
}
|
|
@@ -100,51 +100,411 @@ function captureStartupComposition(ctx) {
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
//#endregion
|
|
103
|
-
//#region ../../../vendor/
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
103
|
+
//#region ../../../vendor/cosmokit/src/misc.ts
|
|
104
|
+
/** Return true when a value is `null` or `undefined`. */
|
|
105
|
+
function isNullable(value) {
|
|
106
|
+
return value === null || value === void 0;
|
|
107
|
+
}
|
|
108
|
+
/** Omit selected keys from a shallow object copy. */
|
|
109
|
+
function omit(source, keys) {
|
|
110
|
+
if (!keys) return { ...source };
|
|
111
|
+
const result = { ...source };
|
|
112
|
+
for (const key of keys) Reflect.deleteProperty(result, key);
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region ../../../vendor/cosmokit/src/volatile.ts
|
|
117
|
+
/** Shared config references used by schema validators and plugin runtimes. */
|
|
118
|
+
const write = Symbol.for("cosmokit.volatile.write");
|
|
111
119
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* mounts.
|
|
120
|
+
* Identify references across ESM/CJS copies of the shared library.
|
|
121
|
+
* @param value - a parsed config value.
|
|
122
|
+
* @returns whether the value implements the shared reference protocol.
|
|
116
123
|
*/
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const writable = {
|
|
120
|
-
".json": "application/json",
|
|
121
|
-
".yaml": "application/yaml",
|
|
122
|
-
".yml": "application/yaml"
|
|
123
|
-
};
|
|
124
|
-
const supported = new Set(Object.keys(writable));
|
|
125
|
-
const WRITE_RETRY_LIMIT = 10;
|
|
126
|
-
const WRITE_RETRY_DELAY_MS = 50;
|
|
127
|
-
function retryableWriteError(error) {
|
|
128
|
-
const code = error?.code;
|
|
129
|
-
return code === "EACCES" || code === "EBUSY" || code === "EPERM";
|
|
124
|
+
function isVolatile(value) {
|
|
125
|
+
return typeof value === "object" && value !== null && write in value;
|
|
130
126
|
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region ../../../vendor/cosmokit/src/types.ts
|
|
129
|
+
/** Test values using `instanceof` with a `toStringTag` fallback. */
|
|
130
|
+
function is(type, value) {
|
|
131
|
+
if (arguments.length === 1) return (value) => is(type, value);
|
|
132
|
+
return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
|
|
133
|
+
}
|
|
134
|
+
function isArrayBufferLike(value) {
|
|
135
|
+
return is("ArrayBuffer", value) || is("SharedArrayBuffer", value);
|
|
136
|
+
}
|
|
137
|
+
function isArrayBufferSource(value) {
|
|
138
|
+
return isArrayBufferLike(value) || ArrayBuffer.isView(value);
|
|
139
|
+
}
|
|
140
|
+
let Binary;
|
|
141
|
+
(function(_Binary) {
|
|
142
|
+
_Binary.is = isArrayBufferLike;
|
|
143
|
+
_Binary.isSource = isArrayBufferSource;
|
|
144
|
+
function fromSource(source) {
|
|
145
|
+
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
|
|
146
|
+
else return source;
|
|
147
|
+
}
|
|
148
|
+
_Binary.fromSource = fromSource;
|
|
149
|
+
function toBase64(source) {
|
|
150
|
+
source = fromSource(source);
|
|
151
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("base64");
|
|
152
|
+
let binary = "";
|
|
153
|
+
const bytes = new Uint8Array(source);
|
|
154
|
+
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
|
|
155
|
+
return btoa(binary);
|
|
156
|
+
}
|
|
157
|
+
_Binary.toBase64 = toBase64;
|
|
158
|
+
function fromBase64(source) {
|
|
159
|
+
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
|
|
160
|
+
return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
|
|
161
|
+
}
|
|
162
|
+
_Binary.fromBase64 = fromBase64;
|
|
163
|
+
function toHex(source) {
|
|
164
|
+
source = fromSource(source);
|
|
165
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("hex");
|
|
166
|
+
return Array.from(new Uint8Array(source), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
167
|
+
}
|
|
168
|
+
_Binary.toHex = toHex;
|
|
169
|
+
function fromHex(source) {
|
|
170
|
+
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "hex"));
|
|
171
|
+
const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1);
|
|
172
|
+
const buffer = [];
|
|
173
|
+
for (let i = 0; i < hex.length; i += 2) buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16));
|
|
174
|
+
return Uint8Array.from(buffer).buffer;
|
|
175
|
+
}
|
|
176
|
+
_Binary.fromHex = fromHex;
|
|
177
|
+
})(Binary || (Binary = {}));
|
|
178
|
+
Binary.fromBase64;
|
|
179
|
+
Binary.toBase64;
|
|
180
|
+
Binary.fromHex;
|
|
181
|
+
Binary.toHex;
|
|
131
182
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* earlier patch inserted. A patch that matches nothing warns and is skipped.
|
|
140
|
-
* @param data - the parsed entry list (JSON-safe plain data).
|
|
141
|
-
* @param patches - the patch list to apply, in order.
|
|
142
|
-
* @param warn - sink for skipped-patch diagnostics (printf-style, `%C` = code).
|
|
143
|
-
* @returns a detached entry list with every applicable patch applied.
|
|
183
|
+
* Compare values recursively, treating two volatile references as equal regardless of value.
|
|
184
|
+
* Strict comparison distinguishes null/undefined, treats opaque objects by identity,
|
|
185
|
+
* compares URLs by normalized href, treats array holes as undefined, and considers distinct cyclic structures unequal.
|
|
186
|
+
* @param a - first value.
|
|
187
|
+
* @param b - second value.
|
|
188
|
+
* @param strict - whether to require strict data equality outside volatile references.
|
|
189
|
+
* @returns whether the values compare equal.
|
|
144
190
|
*/
|
|
145
|
-
function
|
|
146
|
-
|
|
191
|
+
function deepEqual(a, b, strict) {
|
|
192
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
193
|
+
function compare(a, b) {
|
|
194
|
+
if (a === b) return true;
|
|
195
|
+
if (isVolatile(a) || isVolatile(b)) return isVolatile(a) && isVolatile(b);
|
|
196
|
+
if (!strict && isNullable(a) && isNullable(b)) return true;
|
|
197
|
+
if (typeof a !== typeof b || typeof a !== "object" || !a || !b) return false;
|
|
198
|
+
if (ancestors.has(a)) return false;
|
|
199
|
+
function check(test, then) {
|
|
200
|
+
return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
|
|
201
|
+
}
|
|
202
|
+
ancestors.add(a);
|
|
203
|
+
try {
|
|
204
|
+
return check(Array.isArray, (a, b) => {
|
|
205
|
+
if (a.length !== b.length) return false;
|
|
206
|
+
for (let index = 0; index < a.length; index++) if (!compare(a[index], b[index])) return false;
|
|
207
|
+
return true;
|
|
208
|
+
}) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("URL"), (a, b) => a.href === b.href) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
|
|
209
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
210
|
+
const viewA = new Uint8Array(a);
|
|
211
|
+
const viewB = new Uint8Array(b);
|
|
212
|
+
for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
|
|
213
|
+
return true;
|
|
214
|
+
}) ?? ((!strict || [a, b].every((value) => Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) && Object.keys({
|
|
215
|
+
...a,
|
|
216
|
+
...b
|
|
217
|
+
}).every((key) => compare(a[key], b[key])));
|
|
218
|
+
} finally {
|
|
219
|
+
ancestors.delete(a);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return compare(a, b);
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region ../../../vendor/cosmokit/src/time.ts
|
|
226
|
+
let Time;
|
|
227
|
+
(function(_Time) {
|
|
228
|
+
_Time.millisecond = 1;
|
|
229
|
+
const second = _Time.second = 1e3;
|
|
230
|
+
const minute = _Time.minute = second * 60;
|
|
231
|
+
const hour = _Time.hour = minute * 60;
|
|
232
|
+
const day = _Time.day = hour * 24;
|
|
233
|
+
const week = _Time.week = day * 7;
|
|
234
|
+
let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
235
|
+
function setTimezoneOffset(offset) {
|
|
236
|
+
timezoneOffset = offset;
|
|
237
|
+
}
|
|
238
|
+
_Time.setTimezoneOffset = setTimezoneOffset;
|
|
239
|
+
function getTimezoneOffset() {
|
|
240
|
+
return timezoneOffset;
|
|
241
|
+
}
|
|
242
|
+
_Time.getTimezoneOffset = getTimezoneOffset;
|
|
243
|
+
function getDateNumber(date = /* @__PURE__ */ new Date(), offset) {
|
|
244
|
+
if (typeof date === "number") date = new Date(date);
|
|
245
|
+
if (offset === void 0) offset = timezoneOffset;
|
|
246
|
+
return Math.floor((date.valueOf() / minute - offset) / 1440);
|
|
247
|
+
}
|
|
248
|
+
_Time.getDateNumber = getDateNumber;
|
|
249
|
+
function fromDateNumber(value, offset) {
|
|
250
|
+
const date = new Date(value * day);
|
|
251
|
+
if (offset === void 0) offset = timezoneOffset;
|
|
252
|
+
return new Date(+date + offset * minute);
|
|
253
|
+
}
|
|
254
|
+
_Time.fromDateNumber = fromDateNumber;
|
|
255
|
+
const numeric = /\d+(?:\.\d+)?/.source;
|
|
256
|
+
const timeRegExp = new RegExp(`^${[
|
|
257
|
+
"w(?:eek(?:s)?)?",
|
|
258
|
+
"d(?:ay(?:s)?)?",
|
|
259
|
+
"h(?:our(?:s)?)?",
|
|
260
|
+
"m(?:in(?:ute)?(?:s)?)?",
|
|
261
|
+
"s(?:ec(?:ond)?(?:s)?)?"
|
|
262
|
+
].map((unit) => `(${numeric}${unit})?`).join("")}$`);
|
|
263
|
+
function parseTime(source) {
|
|
264
|
+
const capture = timeRegExp.exec(source);
|
|
265
|
+
if (!capture) return 0;
|
|
266
|
+
return (parseFloat(capture[1]) * week || 0) + (parseFloat(capture[2]) * day || 0) + (parseFloat(capture[3]) * hour || 0) + (parseFloat(capture[4]) * minute || 0) + (parseFloat(capture[5]) * second || 0);
|
|
267
|
+
}
|
|
268
|
+
_Time.parseTime = parseTime;
|
|
269
|
+
function parseDate(date) {
|
|
270
|
+
const parsed = parseTime(date);
|
|
271
|
+
if (parsed) date = Date.now() + parsed;
|
|
272
|
+
else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).toLocaleDateString()}-${date}`;
|
|
273
|
+
else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).getFullYear()}-${date}`;
|
|
274
|
+
return date ? new Date(date) : /* @__PURE__ */ new Date();
|
|
275
|
+
}
|
|
276
|
+
_Time.parseDate = parseDate;
|
|
277
|
+
function format(ms) {
|
|
278
|
+
const abs = Math.abs(ms);
|
|
279
|
+
if (abs >= day - hour / 2) return Math.round(ms / day) + "d";
|
|
280
|
+
else if (abs >= hour - minute / 2) return Math.round(ms / hour) + "h";
|
|
281
|
+
else if (abs >= minute - second / 2) return Math.round(ms / minute) + "m";
|
|
282
|
+
else if (abs >= second) return Math.round(ms / second) + "s";
|
|
283
|
+
return ms + "ms";
|
|
284
|
+
}
|
|
285
|
+
_Time.format = format;
|
|
286
|
+
function toDigits(source, length = 2) {
|
|
287
|
+
return source.toString().padStart(length, "0");
|
|
288
|
+
}
|
|
289
|
+
_Time.toDigits = toDigits;
|
|
290
|
+
function template(template, time = /* @__PURE__ */ new Date()) {
|
|
291
|
+
return template.replace("yyyy", time.getFullYear().toString()).replace("yy", time.getFullYear().toString().slice(2)).replace("MM", toDigits(time.getMonth() + 1)).replace("dd", toDigits(time.getDate())).replace("hh", toDigits(time.getHours())).replace("mm", toDigits(time.getMinutes())).replace("ss", toDigits(time.getSeconds())).replace("SSS", toDigits(time.getMilliseconds(), 3));
|
|
292
|
+
}
|
|
293
|
+
_Time.template = template;
|
|
294
|
+
})(Time || (Time = {}));
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region ../../../vendor/include/src/journal.ts
|
|
297
|
+
function diffOptions(legacy, options) {
|
|
298
|
+
const changes = {};
|
|
299
|
+
for (const key of new Set([...Object.keys(legacy), ...Object.keys(options)])) {
|
|
300
|
+
if (key === "id") continue;
|
|
301
|
+
if (deepEqual(legacy[key], options[key])) continue;
|
|
302
|
+
changes[key] = options[key];
|
|
303
|
+
}
|
|
304
|
+
return changes;
|
|
305
|
+
}
|
|
306
|
+
/** Fold a later record into an earlier one for the same entry. */
|
|
307
|
+
function mergeRecords(older, newer) {
|
|
308
|
+
if (!older) return newer;
|
|
309
|
+
if (newer.kind === "remove") return older.kind === "upsert" && older.created ? void 0 : newer;
|
|
310
|
+
if (older.kind === "remove") return {
|
|
311
|
+
...newer,
|
|
312
|
+
created: false
|
|
313
|
+
};
|
|
314
|
+
const relocated = newer.parent !== void 0;
|
|
315
|
+
return {
|
|
316
|
+
kind: "upsert",
|
|
317
|
+
created: older.created,
|
|
318
|
+
parent: relocated ? newer.parent : older.parent,
|
|
319
|
+
position: relocated ? newer.position : older.position,
|
|
320
|
+
changes: {
|
|
321
|
+
...older.changes,
|
|
322
|
+
...newer.changes
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function merge(journal, id, record) {
|
|
327
|
+
const merged = mergeRecords(journal.get(id), record);
|
|
328
|
+
if (merged) journal.set(id, merged);
|
|
329
|
+
else journal.delete(id);
|
|
330
|
+
}
|
|
331
|
+
function record$1(journal, change, parent) {
|
|
332
|
+
const { id, options, legacy } = change;
|
|
333
|
+
if (!options) return merge(journal, id, { kind: "remove" });
|
|
334
|
+
const position = change.group.data.indexOf(options);
|
|
335
|
+
if (!legacy) return merge(journal, id, {
|
|
336
|
+
kind: "upsert",
|
|
337
|
+
created: true,
|
|
338
|
+
parent,
|
|
339
|
+
position,
|
|
340
|
+
changes: omit(options, ["id"])
|
|
341
|
+
});
|
|
342
|
+
merge(journal, id, {
|
|
343
|
+
kind: "upsert",
|
|
344
|
+
created: false,
|
|
345
|
+
parent: change.from ? parent : void 0,
|
|
346
|
+
position,
|
|
347
|
+
changes: diffOptions(legacy, options)
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/** Index every entry (including nested group members) by id. */
|
|
351
|
+
function flatten(data, parent = null, result = /* @__PURE__ */ new Map()) {
|
|
352
|
+
data.forEach((options, position) => {
|
|
353
|
+
if (options.id) result.set(options.id, {
|
|
354
|
+
parent,
|
|
355
|
+
position,
|
|
356
|
+
options,
|
|
357
|
+
list: data
|
|
358
|
+
});
|
|
359
|
+
if (options.group && Array.isArray(options.config)) flatten(options.config, options.id ?? null, result);
|
|
360
|
+
});
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
function applyChanges(options, changes, filter) {
|
|
364
|
+
for (const [key, value] of Object.entries(changes)) {
|
|
365
|
+
if (filter && !filter(key)) continue;
|
|
366
|
+
if (value === void 0) delete options[key];
|
|
367
|
+
else options[key] = value;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function detach(data, id) {
|
|
371
|
+
const flat = flatten(data).get(id);
|
|
372
|
+
if (!flat) return;
|
|
373
|
+
flat.list.splice(flat.list.indexOf(flat.options), 1);
|
|
374
|
+
return flat.options;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Insert `options` under `parent` (a group id, or `null` for the root) at
|
|
378
|
+
* `position`, clamped to the group's length. Returns false when the parent
|
|
379
|
+
* group does not exist.
|
|
380
|
+
*/
|
|
381
|
+
function place(data, options, parent, position) {
|
|
382
|
+
let list = data;
|
|
383
|
+
if (parent !== null) {
|
|
384
|
+
const group = flatten(data).get(parent);
|
|
385
|
+
if (!group?.options.group) return false;
|
|
386
|
+
list = group.options.config ??= [];
|
|
387
|
+
}
|
|
388
|
+
list.splice(Math.min(position, list.length), 0, options);
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Overlay a journal onto `data` in place. `filter(id, key)` restricts which
|
|
393
|
+
* changes apply; `key` is `null` when asking about the entry as a whole
|
|
394
|
+
* (creation, removal, relocation).
|
|
395
|
+
*/
|
|
396
|
+
function applyJournal(data, journal, filter = () => true, warn = () => {}) {
|
|
397
|
+
for (const [id, record] of journal) {
|
|
398
|
+
if (!filter(id, null)) continue;
|
|
399
|
+
if (record.kind === "remove") {
|
|
400
|
+
detach(data, id);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const flat = flatten(data).get(id);
|
|
404
|
+
if (!flat) {
|
|
405
|
+
const options = { id };
|
|
406
|
+
applyChanges(options, record.changes, (key) => filter(id, key));
|
|
407
|
+
const parent = record.parent ?? null;
|
|
408
|
+
if (!place(data, options, parent, record.position)) warn("cannot place entry %C: group %C not found", id, parent);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
applyChanges(flat.options, record.changes, (key) => filter(id, key));
|
|
412
|
+
if (record.parent !== void 0 && flat.parent !== record.parent) {
|
|
413
|
+
detach(data, id);
|
|
414
|
+
if (!place(data, flat.options, record.parent, record.position)) {
|
|
415
|
+
warn("cannot move entry %C: group %C not found", id, record.parent);
|
|
416
|
+
place(data, flat.options, flat.parent, flat.position);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return data;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Three-way reconciliation of a journal against a new file state. The file
|
|
424
|
+
* wins every conflict: conflicting keys are dropped from the journal so that
|
|
425
|
+
* the tree follows the file, and each drop is reported.
|
|
426
|
+
*
|
|
427
|
+
* `fileOwned(id, key)` scopes the check to what the file can actually express
|
|
428
|
+
* (patch-owned keys and patch-inserted entries live elsewhere).
|
|
429
|
+
*/
|
|
430
|
+
function reconcile(journal, base, theirs, fileOwned) {
|
|
431
|
+
const conflicts = [];
|
|
432
|
+
for (const [id, record] of journal) {
|
|
433
|
+
if (!fileOwned(id, null)) continue;
|
|
434
|
+
const b = base.get(id);
|
|
435
|
+
const t = theirs.get(id);
|
|
436
|
+
if (record.kind === "remove") {
|
|
437
|
+
if (!t) journal.delete(id);
|
|
438
|
+
else if (!b || !deepEqual(t.options, b.options)) {
|
|
439
|
+
conflicts.push({
|
|
440
|
+
id,
|
|
441
|
+
reason: b ? "modified in file, removed at runtime" : "added in file, removed at runtime"
|
|
442
|
+
});
|
|
443
|
+
journal.delete(id);
|
|
444
|
+
}
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (!t) {
|
|
448
|
+
if (b) {
|
|
449
|
+
conflicts.push({
|
|
450
|
+
id,
|
|
451
|
+
reason: "removed in file, modified at runtime"
|
|
452
|
+
});
|
|
453
|
+
journal.delete(id);
|
|
454
|
+
}
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (record.created) record.created = false;
|
|
458
|
+
for (const key of Object.keys(record.changes)) {
|
|
459
|
+
if (!fileOwned(id, key)) continue;
|
|
460
|
+
const bv = b?.options[key];
|
|
461
|
+
const tv = t.options[key];
|
|
462
|
+
if (deepEqual(bv, tv) || deepEqual(tv, record.changes[key])) continue;
|
|
463
|
+
conflicts.push({
|
|
464
|
+
id,
|
|
465
|
+
reason: `key "${key}" modified both in file and at runtime`
|
|
466
|
+
});
|
|
467
|
+
delete record.changes[key];
|
|
468
|
+
}
|
|
469
|
+
if (record.parent !== void 0 && b && t.parent !== b.parent && t.parent !== record.parent) {
|
|
470
|
+
conflicts.push({
|
|
471
|
+
id,
|
|
472
|
+
reason: "moved both in file and at runtime"
|
|
473
|
+
});
|
|
474
|
+
record.parent = void 0;
|
|
475
|
+
}
|
|
476
|
+
const moved = record.parent !== void 0 && record.parent !== t.parent;
|
|
477
|
+
if (!Object.keys(record.changes).length && !moved) journal.delete(id);
|
|
478
|
+
}
|
|
479
|
+
return conflicts;
|
|
480
|
+
}
|
|
481
|
+
//#endregion
|
|
482
|
+
//#region ../../../vendor/include/src/patch.ts
|
|
483
|
+
function randomId(used) {
|
|
484
|
+
let id;
|
|
485
|
+
do
|
|
486
|
+
id = Math.random().toString(16).slice(2, 10);
|
|
487
|
+
while (used(id));
|
|
488
|
+
return id;
|
|
489
|
+
}
|
|
490
|
+
/** Give every entry (nested groups included) an id, in place. */
|
|
491
|
+
function ensureIds(entries, used) {
|
|
492
|
+
for (const entry of entries) {
|
|
493
|
+
if (!entry.id) entry.id = randomId(used);
|
|
494
|
+
if (entry.group && Array.isArray(entry.config)) ensureIds(entry.config, used);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/** Runtime changes are routed back to an inserted entry by its id, so every insert needs one. */
|
|
498
|
+
function ensureInsertIds(patches, used) {
|
|
499
|
+
for (const patch of patches ?? []) if (patch.insert) ensureIds(patch.insert, used);
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Overlay `patches` onto a fresh clone of `data`. Inserted entries are cloned
|
|
503
|
+
* too, so the tree owns every object it mounts.
|
|
504
|
+
*/
|
|
505
|
+
function applyPatches(data, patches, warn) {
|
|
147
506
|
data = structuredClone(data);
|
|
507
|
+
if (!patches?.length) return data;
|
|
148
508
|
const entryMap = /* @__PURE__ */ new Map();
|
|
149
509
|
const buildMap = (entries) => {
|
|
150
510
|
for (const entry of entries) {
|
|
@@ -156,6 +516,7 @@ function applyEntryPatches(data, patches, warn) {
|
|
|
156
516
|
for (const patch of patches) {
|
|
157
517
|
const { id, insert, name, ...overrides } = patch;
|
|
158
518
|
if (insert) {
|
|
519
|
+
const clone = structuredClone(insert);
|
|
159
520
|
if (id) {
|
|
160
521
|
const target = entryMap.get(id);
|
|
161
522
|
if (!target) {
|
|
@@ -167,9 +528,9 @@ function applyEntryPatches(data, patches, warn) {
|
|
|
167
528
|
continue;
|
|
168
529
|
}
|
|
169
530
|
if (!Array.isArray(target.config)) target.config = [];
|
|
170
|
-
target.config.push(...
|
|
171
|
-
} else data.push(...
|
|
172
|
-
buildMap(
|
|
531
|
+
target.config.push(...clone);
|
|
532
|
+
} else data.push(...clone);
|
|
533
|
+
buildMap(clone);
|
|
173
534
|
continue;
|
|
174
535
|
}
|
|
175
536
|
if (!id) {
|
|
@@ -192,143 +553,478 @@ function applyEntryPatches(data, patches, warn) {
|
|
|
192
553
|
}
|
|
193
554
|
return data;
|
|
194
555
|
}
|
|
195
|
-
|
|
556
|
+
var PatchIndex = class {
|
|
557
|
+
patches;
|
|
558
|
+
keys = /* @__PURE__ */ new Map();
|
|
559
|
+
inserts = /* @__PURE__ */ new Map();
|
|
560
|
+
constructor(patches) {
|
|
561
|
+
this.patches = patches;
|
|
562
|
+
patches.forEach((patch, index) => {
|
|
563
|
+
const { id, insert } = patch;
|
|
564
|
+
if (insert) {
|
|
565
|
+
const walk = (list, parent) => {
|
|
566
|
+
for (const options of list) {
|
|
567
|
+
this.inserts.set(options.id, {
|
|
568
|
+
type: "insert",
|
|
569
|
+
index,
|
|
570
|
+
list,
|
|
571
|
+
options,
|
|
572
|
+
parent
|
|
573
|
+
});
|
|
574
|
+
if (options.group && Array.isArray(options.config)) walk(options.config, options.id);
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
walk(insert, id ?? null);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (!id) return;
|
|
581
|
+
const keys = this.keys.get(id) ?? /* @__PURE__ */ new Map();
|
|
582
|
+
for (const key of Object.keys(omit(patch, [
|
|
583
|
+
"id",
|
|
584
|
+
"insert",
|
|
585
|
+
"name"
|
|
586
|
+
]))) keys.set(key, index);
|
|
587
|
+
this.keys.set(id, keys);
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
/** Owner of the entry as a whole. */
|
|
591
|
+
entry(id) {
|
|
592
|
+
if (id === null) return { type: "file" };
|
|
593
|
+
return this.inserts.get(id) ?? { type: "file" };
|
|
594
|
+
}
|
|
595
|
+
/** Owner of one key of an entry. */
|
|
596
|
+
key(id, key) {
|
|
597
|
+
const insert = this.inserts.get(id);
|
|
598
|
+
if (insert) return insert;
|
|
599
|
+
const index = this.keys.get(id)?.get(key);
|
|
600
|
+
return index === void 0 ? { type: "file" } : {
|
|
601
|
+
type: "patch",
|
|
602
|
+
index
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
fileOwned(id, key) {
|
|
606
|
+
return (key === null ? this.entry(id) : this.key(id, key)).type === "file";
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
/**
|
|
610
|
+
* Route a journal to its owners: file-owned changes are applied to `data`,
|
|
611
|
+
* patch-owned ones to `patches`. Both are mutated in place, so pass clones.
|
|
612
|
+
* Returns whether any patch changed.
|
|
613
|
+
*/
|
|
614
|
+
function routeJournal(journal, data, patches, warn) {
|
|
615
|
+
const index = new PatchIndex(patches);
|
|
616
|
+
let patched = false;
|
|
617
|
+
const insertList = (owner) => owner.options.config ??= [];
|
|
618
|
+
const placeAt = (options, parent, position) => {
|
|
619
|
+
const owner = index.entry(parent);
|
|
620
|
+
if (owner.type === "insert") {
|
|
621
|
+
const list = insertList(owner);
|
|
622
|
+
list.splice(Math.min(position, list.length), 0, options);
|
|
623
|
+
patched = true;
|
|
624
|
+
} else if (!place(data, options, parent, position)) warn("cannot place entry %C: group %C not found", options.id, parent);
|
|
625
|
+
};
|
|
626
|
+
for (const [id, record] of journal) {
|
|
627
|
+
const owner = index.entry(id);
|
|
628
|
+
if (record.kind === "remove") {
|
|
629
|
+
if (owner.type === "insert") {
|
|
630
|
+
owner.list.splice(owner.list.indexOf(owner.options), 1);
|
|
631
|
+
patched = true;
|
|
632
|
+
} else detach(data, id);
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
const current = owner.type === "insert" ? owner : flatten(data).get(id);
|
|
636
|
+
if (!current) {
|
|
637
|
+
const options = { id };
|
|
638
|
+
applyChanges(options, record.changes);
|
|
639
|
+
placeAt(options, record.parent ?? null, record.position);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (record.parent !== void 0 && current.parent !== record.parent) {
|
|
643
|
+
if (owner.type === "insert") {
|
|
644
|
+
owner.list.splice(owner.list.indexOf(owner.options), 1);
|
|
645
|
+
patched = true;
|
|
646
|
+
} else detach(data, id);
|
|
647
|
+
applyChanges(current.options, record.changes);
|
|
648
|
+
placeAt(current.options, record.parent, record.position);
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (owner.type === "insert") {
|
|
652
|
+
applyChanges(current.options, record.changes);
|
|
653
|
+
patched = true;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
const patchChanges = {};
|
|
657
|
+
applyChanges(current.options, record.changes, (key) => {
|
|
658
|
+
const keyOwner = index.key(id, key);
|
|
659
|
+
if (keyOwner.type !== "patch") return true;
|
|
660
|
+
(patchChanges[keyOwner.index] ??= {})[key] = record.changes[key];
|
|
661
|
+
return false;
|
|
662
|
+
});
|
|
663
|
+
for (const [i, changes] of Object.entries(patchChanges)) {
|
|
664
|
+
applyChanges(patches[i], changes);
|
|
665
|
+
patched = true;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return patched;
|
|
669
|
+
}
|
|
670
|
+
//#endregion
|
|
671
|
+
//#region ../../../vendor/include/src/index.ts
|
|
672
|
+
const JsExpr = new yaml.Type("tag:yaml.org,2002:js", {
|
|
673
|
+
kind: "scalar",
|
|
674
|
+
resolve: (data) => typeof data === "string",
|
|
675
|
+
construct: (data) => ({ __jsExpr: data }),
|
|
676
|
+
predicate: isJsExpr,
|
|
677
|
+
represent: (data) => data["__jsExpr"]
|
|
678
|
+
});
|
|
679
|
+
/** YAML dialect preserving child-owned JavaScript expression nodes. */
|
|
680
|
+
const entryListSchema = yaml.JSON_SCHEMA.extend(JsExpr);
|
|
681
|
+
const schema = entryListSchema;
|
|
682
|
+
const types = {
|
|
683
|
+
".json": "application/json",
|
|
684
|
+
".yaml": "application/yaml",
|
|
685
|
+
".yml": "application/yaml"
|
|
686
|
+
};
|
|
687
|
+
var ConfigFileError = class extends Error {
|
|
688
|
+
stage;
|
|
689
|
+
name = "ConfigFileError";
|
|
690
|
+
constructor(stage, path, cause) {
|
|
691
|
+
super(`failed to ${stage} config file ${path}`, { cause });
|
|
692
|
+
this.stage = stage;
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
/** The file changed under us between the pre-check and the rename. */
|
|
696
|
+
var StaleWriteError = class extends Error {
|
|
697
|
+
name = "StaleWriteError";
|
|
698
|
+
};
|
|
699
|
+
function isENOENT(error) {
|
|
700
|
+
return error?.code === "ENOENT";
|
|
701
|
+
}
|
|
702
|
+
const RENAME_RETRIES = 10;
|
|
703
|
+
const RENAME_BACKOFF = 20;
|
|
704
|
+
/**
|
|
705
|
+
* On Windows a rename over a file that another process holds open (typically
|
|
706
|
+
* an antivirus scanning what we just wrote) fails with one of these; the lock
|
|
707
|
+
* is normally gone within milliseconds. A bounded retry is cheap enough to
|
|
708
|
+
* apply on every platform.
|
|
709
|
+
*/
|
|
710
|
+
function isTransientRenameError(error) {
|
|
711
|
+
const code = error?.code;
|
|
712
|
+
return code === "EACCES" || code === "EPERM" || code === "EBUSY";
|
|
713
|
+
}
|
|
196
714
|
var Include = class extends EntryTree {
|
|
197
715
|
config;
|
|
198
716
|
static inject = ["loader"];
|
|
199
717
|
static [EntryGroup.key] = true;
|
|
200
718
|
filename;
|
|
201
719
|
type;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
720
|
+
/** The last file state we read or wrote. */
|
|
721
|
+
cache;
|
|
722
|
+
/** Tree-side changes that have not reached the file yet. */
|
|
723
|
+
journal = /* @__PURE__ */ new Map();
|
|
724
|
+
/** Ids we assigned to entries the file leaves anonymous. */
|
|
725
|
+
anonymous = /* @__PURE__ */ new Map();
|
|
726
|
+
dirtyRead = false;
|
|
727
|
+
dirtyWrite = false;
|
|
728
|
+
_draining;
|
|
729
|
+
/** Only the latest intended tree is ever applied; older ones are skipped. */
|
|
730
|
+
_pendingTree;
|
|
731
|
+
_applyTask;
|
|
732
|
+
_disposed = false;
|
|
733
|
+
_seq = 0;
|
|
734
|
+
persistenceError;
|
|
208
735
|
constructor(ctx, config) {
|
|
209
736
|
super(ctx);
|
|
210
737
|
this.config = config;
|
|
211
738
|
this.enableLogs = config.enableLogs ?? ctx.fiber.entry?.parent.tree.enableLogs ?? false;
|
|
212
739
|
this.filename = fileURLToPath(new URL(this.config.path, this.ctx.baseUrl));
|
|
213
740
|
const ext = extname(this.filename);
|
|
214
|
-
if (!
|
|
215
|
-
this.type =
|
|
216
|
-
this.readonly = !this.type;
|
|
741
|
+
if (!types[ext]) throw new Error(`extension "${ext}" not supported`);
|
|
742
|
+
this.type = types[ext];
|
|
217
743
|
this.ctx.baseUrl = new URL(".", pathToFileURL(this.filename)).href;
|
|
218
|
-
|
|
744
|
+
ensureInsertIds(config.patches, (id) => !!this.store[id]);
|
|
745
|
+
ctx.on("internal/update", (config, next) => {
|
|
219
746
|
if (config.path !== this.config.path) return next();
|
|
220
747
|
this.config = config;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
async checkAccess() {
|
|
228
|
-
if (!this.type) return;
|
|
229
|
-
try {
|
|
230
|
-
await access(this.filename, constants.W_OK);
|
|
231
|
-
} catch {
|
|
232
|
-
this.readonly = true;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
async read(forced = false) {
|
|
236
|
-
const content = await readFile(this.filename, "utf8");
|
|
237
|
-
if (!forced && this.content === content) return false;
|
|
238
|
-
let data;
|
|
239
|
-
if (this.type === "application/yaml") data = yaml.load(content, { schema: entryListSchema });
|
|
240
|
-
else if (this.type === "application/json") data = JSON.parse(content);
|
|
241
|
-
else {
|
|
242
|
-
const module = await import(
|
|
243
|
-
/* @vite-ignore */
|
|
244
|
-
this.filename
|
|
245
|
-
);
|
|
246
|
-
data = module.default || module;
|
|
247
|
-
}
|
|
248
|
-
if (!Array.isArray(data)) throw new TypeError(`config file must be a top-level array of entries: ${this.filename}`);
|
|
249
|
-
this.content = content;
|
|
250
|
-
this.data = data;
|
|
251
|
-
await this.checkAccess();
|
|
252
|
-
return true;
|
|
253
|
-
}
|
|
254
|
-
applyPatches(data, patches = this.config.patches) {
|
|
255
|
-
return applyEntryPatches(data, patches, (message, ...args) => {
|
|
256
|
-
this.ctx.root.logger?.("loader").warn(message, ...args);
|
|
748
|
+
ensureInsertIds(config.patches, (id) => !!this.store[id]);
|
|
749
|
+
this._scheduleApply();
|
|
750
|
+
return this._applyTask;
|
|
257
751
|
});
|
|
258
752
|
}
|
|
753
|
+
warn = (format, ...args) => {
|
|
754
|
+
this.ctx.root.logger?.("loader").warn(format, ...args);
|
|
755
|
+
};
|
|
259
756
|
async *[Service.init]() {
|
|
260
757
|
try {
|
|
261
|
-
await this.
|
|
758
|
+
await this._read();
|
|
262
759
|
} catch (error) {
|
|
263
|
-
if (error
|
|
264
|
-
if (this.config.initial) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
760
|
+
if (!isENOENT(error instanceof ConfigFileError && error.stage === "read" ? error.cause : void 0)) throw error;
|
|
761
|
+
if (!this.config.initial) throw new Error(`config file not found: ${this.filename}`);
|
|
762
|
+
const initial = structuredClone(this.config.initial);
|
|
763
|
+
ensureIds(initial, (id) => !!this.store[id]);
|
|
764
|
+
await this._writeText(this.dump(initial));
|
|
765
|
+
await this._read();
|
|
268
766
|
}
|
|
269
767
|
yield () => this.stop();
|
|
270
|
-
await this.
|
|
768
|
+
await this._applyTask;
|
|
271
769
|
}
|
|
272
770
|
async stop() {
|
|
771
|
+
this._disposed = true;
|
|
772
|
+
this._pendingTree = void 0;
|
|
773
|
+
if (this.journal.size) {
|
|
774
|
+
this.dirtyWrite = true;
|
|
775
|
+
this._drain();
|
|
776
|
+
}
|
|
273
777
|
try {
|
|
274
|
-
await this.
|
|
778
|
+
await this.flush();
|
|
275
779
|
} finally {
|
|
276
780
|
this.root.stop();
|
|
277
|
-
await this.flushWrite();
|
|
278
781
|
}
|
|
279
782
|
}
|
|
280
783
|
/**
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
* tree: a hot-reload of a live app must never take the process down.
|
|
784
|
+
* Resolves once every pending read and write has been processed and the
|
|
785
|
+
* tree reflects the outcome. Retries pending writes and rejects while changes remain unsaved.
|
|
284
786
|
*/
|
|
285
|
-
async
|
|
787
|
+
async flush() {
|
|
788
|
+
if (this.journal.size && !this._draining) {
|
|
789
|
+
this.dirtyWrite = true;
|
|
790
|
+
this._drain();
|
|
791
|
+
}
|
|
792
|
+
while (this._draining || this._applyTask) {
|
|
793
|
+
await this._draining;
|
|
794
|
+
await this._applyTask;
|
|
795
|
+
}
|
|
796
|
+
if (this.journal.size) throw this.persistenceError ?? /* @__PURE__ */ new Error(`config file ${this.filename} has ${this.journal.size} unsaved change(s)`);
|
|
797
|
+
this.persistenceError = void 0;
|
|
798
|
+
}
|
|
799
|
+
/** Re-read the file and reconcile the tree with it. */
|
|
800
|
+
refresh() {
|
|
801
|
+
this.dirtyRead = true;
|
|
802
|
+
this._drain();
|
|
803
|
+
return this.flush();
|
|
804
|
+
}
|
|
805
|
+
/** Called synchronously by the loader once a tree-side change is in place. */
|
|
806
|
+
commit(change) {
|
|
807
|
+
this.context.emit("loader/config-update");
|
|
808
|
+
const parent = change.group === this.root ? null : change.group.ctx.fiber.entry.options.id;
|
|
809
|
+
record$1(this.journal, change, parent);
|
|
810
|
+
this.dirtyWrite = true;
|
|
811
|
+
this._drain();
|
|
812
|
+
}
|
|
813
|
+
_drain() {
|
|
814
|
+
if (this._draining) return;
|
|
815
|
+
this._draining = this._loop().finally(() => {
|
|
816
|
+
this._draining = void 0;
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
async _loop() {
|
|
820
|
+
while (this.dirtyRead || this.dirtyWrite) {
|
|
821
|
+
if (this.dirtyRead) {
|
|
822
|
+
this.dirtyRead = false;
|
|
823
|
+
try {
|
|
824
|
+
await this._read();
|
|
825
|
+
} catch (error) {
|
|
826
|
+
this.persistenceError = error;
|
|
827
|
+
this.ctx.logger.warn(error);
|
|
828
|
+
if (this.dirtyRead) continue;
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (this.dirtyWrite) {
|
|
833
|
+
this.dirtyWrite = false;
|
|
834
|
+
try {
|
|
835
|
+
await this._write();
|
|
836
|
+
} catch (error) {
|
|
837
|
+
this.persistenceError = error;
|
|
838
|
+
this.ctx.logger.error(error);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
async _read() {
|
|
844
|
+
let content;
|
|
286
845
|
try {
|
|
287
|
-
|
|
288
|
-
await this.root.update(this.applyPatches(this.data));
|
|
846
|
+
content = await readFile(this.filename, "utf8");
|
|
289
847
|
} catch (error) {
|
|
290
|
-
|
|
291
|
-
this.ctx.logger.warn(error);
|
|
848
|
+
throw new ConfigFileError("read", this.filename, error);
|
|
292
849
|
}
|
|
850
|
+
if (content === this.cache?.content) return;
|
|
851
|
+
let data;
|
|
852
|
+
try {
|
|
853
|
+
data = this.type === "application/yaml" ? yaml.load(content, { schema }) : JSON.parse(content);
|
|
854
|
+
} catch (error) {
|
|
855
|
+
throw new ConfigFileError("parse", this.filename, error);
|
|
856
|
+
}
|
|
857
|
+
if (!Array.isArray(data)) throw new ConfigFileError("validate", this.filename, /* @__PURE__ */ new TypeError("config file must be a top-level array"));
|
|
858
|
+
this._assignIds(data);
|
|
859
|
+
if (this.cache) {
|
|
860
|
+
const index = new PatchIndex(this.config.patches ?? []);
|
|
861
|
+
const conflicts = reconcile(this.journal, flatten(this.cache.data), flatten(data), (id, key) => index.fileOwned(id, key));
|
|
862
|
+
for (const { id, reason } of conflicts) this.ctx.logger.error("config conflict in %C: entry %C %s; file wins", this.filename, id, reason);
|
|
863
|
+
}
|
|
864
|
+
this.cache = {
|
|
865
|
+
content,
|
|
866
|
+
data
|
|
867
|
+
};
|
|
868
|
+
this._scheduleApply();
|
|
869
|
+
if (this.journal.size) this.dirtyWrite = true;
|
|
293
870
|
}
|
|
294
|
-
async
|
|
295
|
-
if (this.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
871
|
+
async _write() {
|
|
872
|
+
if (!this.cache || !this.journal.size) return;
|
|
873
|
+
const current = await readFile(this.filename, "utf8").catch((error) => {
|
|
874
|
+
if (isENOENT(error)) return void 0;
|
|
875
|
+
throw error;
|
|
876
|
+
});
|
|
877
|
+
if (current === void 0) {
|
|
878
|
+
this.ctx.logger.warn("config file %C is missing, %d pending change(s) kept in memory", this.filename, this.journal.size);
|
|
301
879
|
return;
|
|
880
|
+
}
|
|
881
|
+
if (current !== this.cache.content) {
|
|
882
|
+
this.dirtyRead = true;
|
|
883
|
+
this.dirtyWrite = true;
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const batch = this.journal;
|
|
887
|
+
this.journal = /* @__PURE__ */ new Map();
|
|
888
|
+
const restore = () => {
|
|
889
|
+
for (const [id, record] of this.journal) merge(batch, id, record);
|
|
890
|
+
this.journal = batch;
|
|
891
|
+
};
|
|
892
|
+
const data = structuredClone(this.cache.data);
|
|
893
|
+
const patches = structuredClone(this.config.patches ?? []);
|
|
894
|
+
if (routeJournal(batch, data, patches, this.warn)) {
|
|
895
|
+
this.config = {
|
|
896
|
+
...this.config,
|
|
897
|
+
patches
|
|
898
|
+
};
|
|
899
|
+
if (this.ctx.fiber.entry) try {
|
|
900
|
+
Promise.resolve(this.ctx.fiber.update(this.config)).catch((error) => {
|
|
901
|
+
this.ctx.logger.error(error);
|
|
902
|
+
});
|
|
903
|
+
} catch (error) {
|
|
904
|
+
this.ctx.logger.error(error);
|
|
905
|
+
}
|
|
906
|
+
else this.ctx.logger.warn("patches of %C changed at runtime but cannot be persisted", this.filename);
|
|
907
|
+
}
|
|
908
|
+
if (deepEqual(data, this.cache.data)) {
|
|
909
|
+
this._scheduleApply();
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const text = this.dump(data);
|
|
913
|
+
try {
|
|
914
|
+
await access(this.filename, constants.W_OK);
|
|
915
|
+
} catch {
|
|
916
|
+
restore();
|
|
917
|
+
this.ctx.logger.warn("config file %C is read-only, %d pending change(s) kept in memory", this.filename, this.journal.size);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
try {
|
|
921
|
+
await this._writeText(text, this.cache.content);
|
|
302
922
|
} catch (error) {
|
|
303
|
-
|
|
304
|
-
|
|
923
|
+
restore();
|
|
924
|
+
if (error instanceof StaleWriteError) {
|
|
925
|
+
this.dirtyRead = true;
|
|
926
|
+
this.dirtyWrite = true;
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
this.ctx.logger.warn("failed to write config file %C, %d pending change(s) kept in memory", this.filename, this.journal.size);
|
|
930
|
+
throw error;
|
|
305
931
|
}
|
|
932
|
+
this.cache = {
|
|
933
|
+
content: text,
|
|
934
|
+
data
|
|
935
|
+
};
|
|
936
|
+
this._scheduleApply();
|
|
306
937
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
938
|
+
/**
|
|
939
|
+
* Write through a temp file and a rename, so that no reader ever sees a
|
|
940
|
+
* truncated file. When `expected` is given, the rename (and every retry of
|
|
941
|
+
* it) only goes ahead while the file still holds that content.
|
|
942
|
+
*/
|
|
943
|
+
async _writeText(text, expected) {
|
|
944
|
+
const tmp = join(dirname(this.filename), `.${basename(this.filename)}.${process.pid}.${this._seq++}.tmp`);
|
|
945
|
+
try {
|
|
946
|
+
await writeFile(tmp, text);
|
|
947
|
+
for (let attempt = 0;; attempt++) {
|
|
948
|
+
if (expected !== void 0) {
|
|
949
|
+
if (await readFile(this.filename, "utf8").catch((error) => {
|
|
950
|
+
if (isENOENT(error)) return void 0;
|
|
951
|
+
throw error;
|
|
952
|
+
}) !== expected) throw new StaleWriteError();
|
|
953
|
+
}
|
|
954
|
+
try {
|
|
955
|
+
await rename(tmp, this.filename);
|
|
956
|
+
return;
|
|
957
|
+
} catch (error) {
|
|
958
|
+
if (attempt >= RENAME_RETRIES || !isTransientRenameError(error)) throw error;
|
|
959
|
+
}
|
|
960
|
+
await setTimeout$1(RENAME_BACKOFF * (attempt + 1));
|
|
961
|
+
}
|
|
962
|
+
} catch (error) {
|
|
963
|
+
await rm(tmp, { force: true }).catch(() => {});
|
|
964
|
+
throw error;
|
|
965
|
+
}
|
|
327
966
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
967
|
+
dump(data) {
|
|
968
|
+
return this.type === "application/yaml" ? yaml.dump(data, { schema }) : JSON.stringify(data, null, 2);
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Entries the file leaves anonymous get an id here, before the loader sees
|
|
972
|
+
* them. One that looks exactly like an entry we named on the previous read
|
|
973
|
+
* keeps that id, so editing a neighbor does not restart it; anything else
|
|
974
|
+
* gets a fresh id. Assigned ids stay in memory until the next write.
|
|
975
|
+
*/
|
|
976
|
+
_assignIds(data) {
|
|
977
|
+
const used = /* @__PURE__ */ new Set();
|
|
978
|
+
const pending = [];
|
|
979
|
+
const walk = (entries, parent) => {
|
|
980
|
+
for (const options of entries) {
|
|
981
|
+
if (options.id) used.add(options.id);
|
|
982
|
+
else pending.push({
|
|
983
|
+
options,
|
|
984
|
+
parent
|
|
985
|
+
});
|
|
986
|
+
if (options.group && Array.isArray(options.config)) walk(options.config, options.id ?? null);
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
walk(data, null);
|
|
990
|
+
const previous = this.anonymous;
|
|
991
|
+
this.anonymous = /* @__PURE__ */ new Map();
|
|
992
|
+
for (const { options, parent } of pending) {
|
|
993
|
+
let id;
|
|
994
|
+
for (const [candidate, info] of previous) {
|
|
995
|
+
if (info.parent !== parent || !deepEqual(info.options, options)) continue;
|
|
996
|
+
id = candidate;
|
|
997
|
+
previous.delete(candidate);
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
if (!id) do
|
|
1001
|
+
id = Math.random().toString(16).slice(2, 10);
|
|
1002
|
+
while (used.has(id) || this.store[id]);
|
|
1003
|
+
used.add(id);
|
|
1004
|
+
this.anonymous.set(id, {
|
|
1005
|
+
parent,
|
|
1006
|
+
options: structuredClone(options)
|
|
1007
|
+
});
|
|
1008
|
+
const rest = { ...options };
|
|
1009
|
+
for (const key of Object.keys(options)) delete options[key];
|
|
1010
|
+
Object.assign(options, { id }, rest);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
_scheduleApply() {
|
|
1014
|
+
if (this._disposed || !this.cache) return;
|
|
1015
|
+
const tree = applyPatches(this.cache.data, this.config.patches, this.warn);
|
|
1016
|
+
this._pendingTree = applyJournal(tree, this.journal, void 0, this.warn);
|
|
1017
|
+
this._applyTask ??= (async () => {
|
|
1018
|
+
try {
|
|
1019
|
+
while (this._pendingTree) {
|
|
1020
|
+
const tree = this._pendingTree;
|
|
1021
|
+
this._pendingTree = void 0;
|
|
1022
|
+
await this.root.update(tree);
|
|
1023
|
+
}
|
|
1024
|
+
} finally {
|
|
1025
|
+
this._applyTask = void 0;
|
|
1026
|
+
}
|
|
1027
|
+
})();
|
|
332
1028
|
}
|
|
333
1029
|
};
|
|
334
1030
|
//#endregion
|
|
@@ -1167,7 +1863,7 @@ function loadProfile(binName, name, installAnchor, home = resolveDshHome(), opti
|
|
|
1167
1863
|
* @returns the composed entry list.
|
|
1168
1864
|
*/
|
|
1169
1865
|
function composeEntries(layers, warn = () => {}) {
|
|
1170
|
-
return
|
|
1866
|
+
return applyPatches([], structuredClone(layers.flat()), (message, ...args) => {
|
|
1171
1867
|
let index = 0;
|
|
1172
1868
|
warn(message.replace(/%C/g, () => JSON.stringify(args[index++])));
|
|
1173
1869
|
});
|
|
@@ -2466,7 +3162,7 @@ function preflight(ctx, entries, parentURL, report) {
|
|
|
2466
3162
|
data = config.initial;
|
|
2467
3163
|
}
|
|
2468
3164
|
if (!Array.isArray(data)) return void 0;
|
|
2469
|
-
return check(
|
|
3165
|
+
return check(applyPatches(data, config.patches, patchWarning(ctx)), pathToFileURL(filename).href) ? `its included file ${filename} reaches an incompatible plugin, and that file is never rewritten` : void 0;
|
|
2470
3166
|
} finally {
|
|
2471
3167
|
includes.delete(filename);
|
|
2472
3168
|
}
|
|
@@ -2487,7 +3183,7 @@ function preflight(ctx, entries, parentURL, report) {
|
|
|
2487
3183
|
*/
|
|
2488
3184
|
function prepareProfilePatches(ctx, patches, parentURL, binName = "dsh") {
|
|
2489
3185
|
if (ctx.get("profileContext") === void 0) return patches;
|
|
2490
|
-
const rows = prepareProfileEntries(ctx,
|
|
3186
|
+
const rows = prepareProfileEntries(ctx, applyPatches([], patches, patchWarning(ctx)), parentURL, binName);
|
|
2491
3187
|
return rows.length === 0 ? [] : [{ insert: rows }];
|
|
2492
3188
|
}
|
|
2493
3189
|
//#endregion
|
|
@@ -3413,7 +4109,7 @@ async function collectConfigSchemas(profile, entries, resolution, diagnostics =
|
|
|
3413
4109
|
const rows = entryList(source);
|
|
3414
4110
|
let children;
|
|
3415
4111
|
try {
|
|
3416
|
-
children =
|
|
4112
|
+
children = applyPatches(rows, patches, warn(path));
|
|
3417
4113
|
} catch (error) {
|
|
3418
4114
|
throw new Error(`include patches could not be composed for ${filename}; child declarations are unavailable`, { cause: error });
|
|
3419
4115
|
}
|
|
@@ -3861,10 +4557,7 @@ async function recompose(ctx, patches, binName, options, withResolution) {
|
|
|
3861
4557
|
options: JSON.stringify(failure.entry.options),
|
|
3862
4558
|
diagnostic: failure.entry.fiber?.uid ? reloadDiagnostic(failure) : void 0
|
|
3863
4559
|
}));
|
|
3864
|
-
const previousFibers = [...ctx.loader.entries()].flatMap((row) => row.fiber === void 0 ? [] : [
|
|
3865
|
-
fiber: row.fiber,
|
|
3866
|
-
failed: row.fiber.state === FIBER_FAILED || row.fiber.state === FIBER_DISPOSED
|
|
3867
|
-
}]);
|
|
4560
|
+
const previousFibers = [...ctx.loader.entries()].flatMap((row) => row.fiber === void 0 ? [] : [row.fiber]);
|
|
3868
4561
|
const { patches: previous = [], ...includeConfig } = entry.options.config;
|
|
3869
4562
|
const current = withResolution ? ctx.get("pluginPackages")?.resolution : void 0;
|
|
3870
4563
|
const successor = current instanceof ProfileRuntimeResolution ? await current.computeLatestResolution() : void 0;
|
|
@@ -3903,15 +4596,16 @@ async function recompose(ctx, patches, binName, options, withResolution) {
|
|
|
3903
4596
|
const prepared = prepareProfilePatches(ctx, patches, parentURL, binName);
|
|
3904
4597
|
applying = true;
|
|
3905
4598
|
const retried = [...new Set([...running, ...withResolution ? [...ctx.loader.entries()].filter(importFailed) : []])];
|
|
4599
|
+
const priorData = new Map(retried.map((row) => [row, row.parent.data]));
|
|
3906
4600
|
try {
|
|
3907
4601
|
await entry.update({ config: {
|
|
3908
4602
|
...includeConfig,
|
|
3909
4603
|
patches: prepared
|
|
3910
4604
|
} });
|
|
3911
4605
|
} finally {
|
|
3912
|
-
await resume(ctx, retried);
|
|
4606
|
+
await resume(ctx, retried.filter((row) => row.parent.data === priorData.get(row)));
|
|
3913
4607
|
}
|
|
3914
|
-
|
|
4608
|
+
await Promise.allSettled(previousFibers.map((fiber) => fiber.await()));
|
|
3915
4609
|
await ctx.loader.await();
|
|
3916
4610
|
const failures = await inactiveEntries(ctx);
|
|
3917
4611
|
const introduced = failures.filter((failure) => {
|
|
@@ -3921,7 +4615,6 @@ async function recompose(ctx, patches, binName, options, withResolution) {
|
|
|
3921
4615
|
return !previousFailures.some((previous) => sameFailure(previous, failure));
|
|
3922
4616
|
});
|
|
3923
4617
|
if (introduced.length > 0) throw new Error(activationDiagnostic(binName, introduced, false).trimEnd());
|
|
3924
|
-
for (const [index, result] of results.entries()) if (result.status === "rejected" && !previousFibers[index]?.failed) throw result.reason;
|
|
3925
4618
|
ctx.emit("app-boot/config-reload");
|
|
3926
4619
|
return { warnings: failures.map(reloadDiagnostic) };
|
|
3927
4620
|
} finally {
|
|
@@ -3951,7 +4644,7 @@ function rowIds(patches) {
|
|
|
3951
4644
|
if ((row.group === true || row.name === "cordis:group") && Array.isArray(row.config)) visit(row.config);
|
|
3952
4645
|
}
|
|
3953
4646
|
};
|
|
3954
|
-
visit(
|
|
4647
|
+
visit(applyPatches([], patches, () => {}));
|
|
3955
4648
|
return ids;
|
|
3956
4649
|
}
|
|
3957
4650
|
/** Publish a generation unless it maps exactly what is published, which would only drop the package caches. */
|
|
@@ -4106,7 +4799,7 @@ function renderConfigDump(binName, absoluteConfigPath, layers, warn = (line) =>
|
|
|
4106
4799
|
const baseLabel = basename(absoluteConfigPath);
|
|
4107
4800
|
const base = parsed;
|
|
4108
4801
|
const snapshot = (count, warnings) => {
|
|
4109
|
-
return
|
|
4802
|
+
return applyPatches(base, structuredClone(layers.slice(0, count).flatMap((layer) => layer.patches)), (message, ...args) => {
|
|
4110
4803
|
let index = 0;
|
|
4111
4804
|
warnings.push(message.replace(/%C/g, () => JSON.stringify(args[index++])));
|
|
4112
4805
|
});
|
|
@@ -4583,14 +5276,6 @@ async function boot(binName, absoluteConfigPath, patches, prepare, bareModuleBas
|
|
|
4583
5276
|
try {
|
|
4584
5277
|
ctx.baseUrl = pathToFileURL(dirname(absoluteConfigPath)).href + "/";
|
|
4585
5278
|
ctx.provide("dshHomePath", dshHomePath);
|
|
4586
|
-
ctx.on("internal/update", (_config, _noSave, next) => {
|
|
4587
|
-
Promise.resolve(next()).catch((error) => {
|
|
4588
|
-
ctx.logger.error(error);
|
|
4589
|
-
});
|
|
4590
|
-
}, {
|
|
4591
|
-
global: true,
|
|
4592
|
-
prepend: true
|
|
4593
|
-
});
|
|
4594
5279
|
await ctx.plugin(Loader);
|
|
4595
5280
|
await prepare?.(ctx);
|
|
4596
5281
|
stage = "plugin tree failed to load";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-app-boot",
|
|
3
3
|
"description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence",
|
|
4
|
-
"version": "0.2.2-
|
|
4
|
+
"version": "0.2.2-rc.1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
],
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@deepseek-ai/dsh-atomic-write": "0.2.2-
|
|
36
|
-
"@deepseek-ai/dsh-official-bundles": "0.2.2-
|
|
37
|
-
"@deepseek-ai/dsh-package-manifest": "0.2.2-
|
|
35
|
+
"@deepseek-ai/dsh-atomic-write": "0.2.2-rc.1",
|
|
36
|
+
"@deepseek-ai/dsh-official-bundles": "0.2.2-rc.1",
|
|
37
|
+
"@deepseek-ai/dsh-package-manifest": "0.2.2-rc.1",
|
|
38
38
|
"@eslint-community/regexpp": "^4.12.2",
|
|
39
39
|
"ajv": "^8.20.0",
|
|
40
40
|
"js-yaml": "^4.3.2",
|
|
@@ -43,24 +43,24 @@
|
|
|
43
43
|
"semver": "^7.8.5"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@deepseek-ai/cordis-plugin-group": "~1.0
|
|
47
|
-
"@deepseek-ai/cordis-plugin-include": "~1.0
|
|
48
|
-
"@deepseek-ai/cordis-plugin-loader": "~1.0
|
|
49
|
-
"@deepseek-ai/dsh-launch-environment": "0.2.2-
|
|
50
|
-
"@deepseek-ai/dsh-home-paths": "0.2.2-
|
|
51
|
-
"@deepseek-ai/cordis": "~4.0
|
|
46
|
+
"@deepseek-ai/cordis-plugin-group": "~1.1.0",
|
|
47
|
+
"@deepseek-ai/cordis-plugin-include": "~1.1.0",
|
|
48
|
+
"@deepseek-ai/cordis-plugin-loader": "~1.1.0",
|
|
49
|
+
"@deepseek-ai/dsh-launch-environment": "0.2.2-rc.1",
|
|
50
|
+
"@deepseek-ai/dsh-home-paths": "0.2.2-rc.1",
|
|
51
|
+
"@deepseek-ai/cordis": "~4.1.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@deepseek-ai/schemastery": "~3.
|
|
55
|
-
"@deepseek-ai/cordis-plugin-group": "~1.0
|
|
56
|
-
"@deepseek-ai/cordis-plugin-include": "~1.0
|
|
57
|
-
"@deepseek-ai/cordis-plugin-loader": "~1.0
|
|
58
|
-
"@deepseek-ai/cordis-plugin-timer": "~1.
|
|
59
|
-
"@deepseek-ai/dsh-launch-environment": "0.2.2-
|
|
60
|
-
"@deepseek-ai/dsh-home-paths": "0.2.2-
|
|
61
|
-
"@deepseek-ai/dsh-system-prompt": "0.2.2-
|
|
54
|
+
"@deepseek-ai/schemastery": "~3.19.0",
|
|
55
|
+
"@deepseek-ai/cordis-plugin-group": "~1.1.0",
|
|
56
|
+
"@deepseek-ai/cordis-plugin-include": "~1.1.0",
|
|
57
|
+
"@deepseek-ai/cordis-plugin-loader": "~1.1.0",
|
|
58
|
+
"@deepseek-ai/cordis-plugin-timer": "~1.2.0",
|
|
59
|
+
"@deepseek-ai/dsh-launch-environment": "0.2.2-rc.1",
|
|
60
|
+
"@deepseek-ai/dsh-home-paths": "0.2.2-rc.1",
|
|
61
|
+
"@deepseek-ai/dsh-system-prompt": "0.2.2-rc.1",
|
|
62
62
|
"@types/js-yaml": "^4.0.9",
|
|
63
63
|
"@types/semver": "^7.8.0",
|
|
64
|
-
"@deepseek-ai/cordis": "~4.0
|
|
64
|
+
"@deepseek-ai/cordis": "~4.1.0"
|
|
65
65
|
}
|
|
66
66
|
}
|