@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.
Files changed (2) hide show
  1. package/lib/index.js +848 -163
  2. 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/include/src/index.ts
104
- const JsExpr = new yaml.Type("tag:yaml.org,2002:js", {
105
- kind: "scalar",
106
- resolve: (data) => typeof data === "string",
107
- construct: (data) => ({ __jsExpr: data }),
108
- predicate: isJsExpr,
109
- represent: (data) => data["__jsExpr"]
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
- * The entry-list YAML dialect: `!!js` scalars round-trip as expression nodes
113
- * the Loader evaluates at entry activation. Exported so config tooling
114
- * (`dsh --dump-config`) parses and prints exactly the dialect this include
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
- const entryListSchema = yaml.JSON_SCHEMA.extend(JsExpr);
118
- const schema = entryListSchema;
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
- * Apply patch lists to an entry list — THE patch semantics of this include,
133
- * shared by mounting (`applyPatches`) and offline config tooling
134
- * (`dsh --dump-config`) so a dump can never drift from what boots. The input
135
- * is never mutated: patching shared entry objects would bake earlier patch
136
- * values into the cached parse, so repeated application (config hot-reloads)
137
- * could never revert a removed or changed patch. Inserted entries are indexed
138
- * as they are added, so a later patch in the same list can target a row an
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 applyEntryPatches(data, patches, warn) {
146
- if (!patches?.length) return [...data];
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(...insert);
171
- } else data.push(...insert);
172
- buildMap(insert);
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
- /** Loader entry tree backed by a YAML or JSON file. */
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
- readonly;
203
- content;
204
- data;
205
- writeTask;
206
- pendingWrite;
207
- writeQueue = Promise.resolve();
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 (!supported.has(ext)) throw new Error(`extension "${ext}" not supported`);
215
- this.type = writable[ext];
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
- ctx.on("internal/update", (config, _, next) => {
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
- this.root.update(this.applyPatches(this.data, config.patches)).catch((error) => {
222
- this.ctx.logger.warn("config update at %C failed", this.filename);
223
- this.ctx.logger.warn(error);
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.read();
758
+ await this._read();
262
759
  } catch (error) {
263
- if (error?.code !== "ENOENT") throw error;
264
- if (this.config.initial) {
265
- await this._writeFile(this.config.initial);
266
- await this.read(true);
267
- } else throw new Error(`config file not found: ${this.filename}`);
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.root.update(this.applyPatches(this.data));
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.flushWrite();
778
+ await this.flush();
275
779
  } finally {
276
780
  this.root.stop();
277
- await this.flushWrite();
278
781
  }
279
782
  }
280
783
  /**
281
- * Re-read the file and refresh child entries when content changed. An
282
- * unreadable or unparsable file logs a warning and keeps the last good
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 refresh() {
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
- if (!await this.read()) return;
288
- await this.root.update(this.applyPatches(this.data));
846
+ content = await readFile(this.filename, "utf8");
289
847
  } catch (error) {
290
- this.ctx.logger.warn("config reload at %C failed; keeping the running tree", this.filename);
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 _writeFile(config) {
295
- if (this.readonly) throw new Error(`cannot overwrite readonly config`);
296
- if (this.type === "application/yaml") this.content = yaml.dump(config, { schema });
297
- else if (this.type === "application/json") this.content = JSON.stringify(config, null, 2);
298
- await writeFile(this.filename + ".tmp", this.content);
299
- for (let retry = 0;; retry++) try {
300
- await rename(this.filename + ".tmp", this.filename);
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
- if (!retryableWriteError(error) || retry >= WRITE_RETRY_LIMIT) throw error;
304
- await setTimeout$1((retry + 1) * WRITE_RETRY_DELAY_MS);
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
- writeFile(config) {
308
- clearTimeout(this.writeTask);
309
- this.pendingWrite = config;
310
- this.writeTask = setTimeout(() => {
311
- this.flushWrite();
312
- }, 0);
313
- }
314
- flushWrite() {
315
- clearTimeout(this.writeTask);
316
- this.writeTask = void 0;
317
- const config = this.pendingWrite;
318
- this.pendingWrite = void 0;
319
- if (config === void 0) return this.writeQueue;
320
- const run = this.writeQueue.then(() => this._writeFile(config), () => this._writeFile(config));
321
- this.writeQueue = run;
322
- run.catch((error) => {
323
- this.ctx.root.logger?.("loader").warn("failed to write config file %C", this.filename);
324
- this.ctx.root.logger?.("loader").warn(error);
325
- });
326
- return run;
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
- /** Schedule a write of the current root entry data. */
329
- write() {
330
- this.context.emit("loader/config-update");
331
- return this.writeFile(this.root.data);
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 applyEntryPatches([], structuredClone(layers.flat()), (message, ...args) => {
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(applyEntryPatches(data, config.patches, patchWarning(ctx)), pathToFileURL(filename).href) ? `its included file ${filename} reaches an incompatible plugin, and that file is never rewritten` : void 0;
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, applyEntryPatches([], patches, patchWarning(ctx)), parentURL, binName);
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 = applyEntryPatches(rows, patches, warn(path));
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
- const results = await Promise.allSettled(previousFibers.map(({ fiber }) => fiber.await()));
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(applyEntryPatches([], patches, () => {}));
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 applyEntryPatches(base, structuredClone(layers.slice(0, count).flatMap((layer) => layer.patches)), (message, ...args) => {
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-alpha.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-alpha.2",
36
- "@deepseek-ai/dsh-official-bundles": "0.2.2-alpha.2",
37
- "@deepseek-ai/dsh-package-manifest": "0.2.2-alpha.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.6-alpha.1",
47
- "@deepseek-ai/cordis-plugin-include": "~1.0.11-alpha.1",
48
- "@deepseek-ai/cordis-plugin-loader": "~1.0.7-alpha.1",
49
- "@deepseek-ai/dsh-launch-environment": "0.2.2-alpha.2",
50
- "@deepseek-ai/dsh-home-paths": "0.2.2-alpha.2",
51
- "@deepseek-ai/cordis": "~4.0.6-alpha.1"
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.18.6-alpha.1",
55
- "@deepseek-ai/cordis-plugin-group": "~1.0.6-alpha.1",
56
- "@deepseek-ai/cordis-plugin-include": "~1.0.11-alpha.1",
57
- "@deepseek-ai/cordis-plugin-loader": "~1.0.7-alpha.1",
58
- "@deepseek-ai/cordis-plugin-timer": "~1.1.8-alpha.1",
59
- "@deepseek-ai/dsh-launch-environment": "0.2.2-alpha.2",
60
- "@deepseek-ai/dsh-home-paths": "0.2.2-alpha.2",
61
- "@deepseek-ai/dsh-system-prompt": "0.2.2-alpha.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.6-alpha.1"
64
+ "@deepseek-ai/cordis": "~4.1.0"
65
65
  }
66
66
  }