@deepseek-ai/dsh-client-modules 0.1.5-rc.2 → 0.1.6-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -50,6 +50,43 @@ window.__ModuleLoader__.load({
50
50
  return value;
51
51
  }
52
52
  /**
53
+ * Narrow an unknown parsed JSON value to the `dsh.client` declaration. Shared
54
+ * by the node half's Loader scan and the roster generator, so both read a
55
+ * package's browser declaration through one validator.
56
+ * @param pkgName - package name used as the diagnostic prefix.
57
+ * @param value - the raw `dsh.client` field of the package manifest.
58
+ * @returns the validated declaration, or undefined when the field is absent.
59
+ * @throws {Error} when the field is present but any member is malformed.
60
+ */
61
+ function parseDshClient(pkgName, value) {
62
+ if (value === void 0) return void 0;
63
+ if (typeof value !== "object" || value === null) throw new Error(`client-modules: ${pkgName} has a non-object dsh.client declaration`);
64
+ const decl = value;
65
+ if (typeof decl.platform !== "string") throw new Error(`client-modules: ${pkgName} dsh.client.platform must be a string`);
66
+ const inject = optionalStringArray(pkgName, "dsh.client.inject", decl.inject);
67
+ const external = optionalStringArray(pkgName, "dsh.client.external", decl.external);
68
+ if (decl.immediately !== void 0 && typeof decl.immediately !== "boolean") throw new Error(`client-modules: ${pkgName} dsh.client.immediately must be a boolean`);
69
+ return {
70
+ platform: decl.platform,
71
+ ...inject !== void 0 ? { inject } : {},
72
+ ...external !== void 0 ? { external } : {},
73
+ ...decl.immediately !== void 0 ? { immediately: decl.immediately } : {}
74
+ };
75
+ }
76
+ /**
77
+ * The bare package-root specifier `specifier` names, or undefined for a subpath, a path, or any scheme-qualified
78
+ * specifier (`cordis:` builtins, `node:` modules, URLs).
79
+ * @param specifier - Loader row name.
80
+ * @returns the package name, or undefined.
81
+ */
82
+ function exactPackageSpecifier(specifier) {
83
+ if (specifier.startsWith("@")) {
84
+ const parts = specifier.split("/");
85
+ return parts.length === 2 && parts.every(Boolean) ? specifier : void 0;
86
+ }
87
+ return specifier.length > 0 && !specifier.includes("/") && !specifier.includes(":") ? specifier : void 0;
88
+ }
89
+ /**
53
90
  * Normalize a module specifier onto the graph row that owns it: a plugin bundle
54
91
  * IS its package's client half, so `<id>/client` (the exports subpath external
55
92
  * bundles emit) and the bare package name resolve to the same exports. Both the
@@ -135,6 +172,274 @@ window.__ModuleLoader__.load({
135
172
  };
136
173
  }
137
174
  //#endregion
175
+ //#region lib/types/client/entry-lifecycle.js
176
+ /**
177
+ * Release a runtime before clearing its entry fiber so Loader refresh can import new code.
178
+ * Registry deletion prevents Loader from treating replacement as a user disable.
179
+ * @param entry - Entry retained for code replacement.
180
+ */
181
+ async function tearDownEntryFiber(entry) {
182
+ const fiber = entry.fiber;
183
+ if (fiber === void 0) return;
184
+ const runtime = fiber.runtime;
185
+ /* v8 ignore next -- Loader entries own plugin fibers; only the root context has a null runtime. */
186
+ if (runtime !== null) entry.ctx.registry.delete(runtime.callback);
187
+ while (fiber.inertia !== void 0) await fiber.inertia;
188
+ delete entry.fiber;
189
+ }
190
+ /**
191
+ * Remove styles after their plugin's effect cleanup has settled.
192
+ * @param id - Package whose factory owns the style tags.
193
+ */
194
+ function removeOwnedStyles(id) {
195
+ if (typeof document === "undefined") return;
196
+ for (const el of document.querySelectorAll("style[data-plugin]")) if (el.getAttribute("data-plugin") === id) el.remove();
197
+ }
198
+ //#endregion
199
+ //#region lib/types/client/entries.js
200
+ /** Numeric values mirror Cordis's const enum, which bundle loaders cannot import as a runtime object. */
201
+ const ACTIVE = 2;
202
+ const FAILED = 3;
203
+ /** Revisions and requests identify desired code; URLs only select its immutable delivery resource. */
204
+ function entryTargets(manifest) {
205
+ return JSON.stringify(manifest.modules.map((row) => [
206
+ row.id,
207
+ row.rev,
208
+ row.inject,
209
+ row.external
210
+ ]));
211
+ }
212
+ /** Manages only entries created from the Host manifest; other Loader contributors retain ownership. */
213
+ var ClientEntries = class {
214
+ modules;
215
+ index;
216
+ /** Stable observable consumed by page diagnostics through the renderer's injected hook. */
217
+ state = {
218
+ getSnapshot: () => this.snapshot,
219
+ subscribe: (listener) => {
220
+ this.listeners.add(listener);
221
+ return () => {
222
+ this.listeners.delete(listener);
223
+ };
224
+ }
225
+ };
226
+ snapshot = {
227
+ syncing: false,
228
+ failures: []
229
+ };
230
+ listeners = /* @__PURE__ */ new Set();
231
+ managed = /* @__PURE__ */ new Map();
232
+ revisions = /* @__PURE__ */ new Map();
233
+ loader;
234
+ queue = Promise.resolve();
235
+ desired;
236
+ generation = 0;
237
+ stopped = false;
238
+ /**
239
+ * Construct the page controller before Cordis boot.
240
+ * @param modules - Module arrival and materialization owner.
241
+ * @param index - Private descriptor replacement and unused-module cleanup.
242
+ */
243
+ constructor(modules, index) {
244
+ this.modules = modules;
245
+ this.index = index;
246
+ this.desired = modules.manifest;
247
+ }
248
+ /**
249
+ * Create the initial roster and retain its entry identities for subsequent reconciliation.
250
+ * @param loader - Page Loader, already configured with the module system.
251
+ * @param manifest - Initial roster audited by the boot caller.
252
+ * @returns after initial entries and their activation settle; boot owns its activation audit.
253
+ */
254
+ start(loader, manifest) {
255
+ if (this.loader !== void 0) throw new Error("client-modules: entries already started");
256
+ this.loader = loader;
257
+ this.desired = manifest;
258
+ loader.ctx.effect(() => () => {
259
+ this.stopped = true;
260
+ this.generation++;
261
+ return this.queue;
262
+ }, "client-modules: entry reconciliation");
263
+ return this.enqueue(async () => {
264
+ await Promise.all(this.desired.plugins.map(async ({ id }) => {
265
+ await this.create(loader, id);
266
+ }));
267
+ await loader.await();
268
+ for (const row of this.modules.manifest.modules) this.revisions.set(row.id, row.rev);
269
+ });
270
+ }
271
+ /**
272
+ * Validate and apply the latest full Host graph. Changed targets cancel obsolete mounts; identical targets share pending loads.
273
+ * @param graph - JSON-decoded graph received from the Host.
274
+ * @returns after the queued reconciliation; per-package failures remain available in {@link state}.
275
+ */
276
+ sync(graph) {
277
+ const manifest = parseBootManifest(graph);
278
+ if (entryTargets(manifest) !== entryTargets(this.desired)) this.generation++;
279
+ this.desired = manifest;
280
+ const generation = this.generation;
281
+ return this.enqueue(() => this.reconcile(generation));
282
+ }
283
+ /**
284
+ * Retry failed entries against the latest graph, including an unchanged revision.
285
+ * @returns after retry settlement, with remaining errors in {@link state}.
286
+ */
287
+ retry() {
288
+ const generation = ++this.generation;
289
+ return this.enqueue(() => this.reconcile(generation));
290
+ }
291
+ /**
292
+ * Replace one entry's code in the same queue as graph updates; duplicate revisions are ignored.
293
+ * Entries missing after a failed import are reconciled; bootstrap replacement fails before teardown.
294
+ * @param id - Package id from a rebuilt frame.
295
+ * @param rev - Opaque revision selecting the rebuilt artifact.
296
+ * @returns after queued work; replacement errors reject, while per-package reconciliation errors remain in {@link state}.
297
+ */
298
+ reload(id, rev) {
299
+ this.desired = {
300
+ ...this.desired,
301
+ modules: this.desired.modules.map((row) => row.id === id ? {
302
+ ...row,
303
+ rev
304
+ } : row)
305
+ };
306
+ return this.enqueue(async () => {
307
+ const desired = this.desired.modules.find((row) => row.id === id);
308
+ if (this.stopped || desired === void 0) return;
309
+ const entry = this.managed.get(id);
310
+ if (entry === void 0) {
311
+ this.modules.invalidate(id, desired.rev);
312
+ removeOwnedStyles(id);
313
+ await this.reconcile(this.generation);
314
+ return;
315
+ }
316
+ if (this.revisions.get(id) === rev) return;
317
+ this.publish({
318
+ syncing: true,
319
+ failures: this.snapshot.failures.filter((failure) => failure.id !== id)
320
+ });
321
+ await this.replace(entry, id, rev, this.generation);
322
+ this.publish({
323
+ syncing: false,
324
+ failures: this.snapshot.failures
325
+ });
326
+ }, id);
327
+ }
328
+ publish(snapshot) {
329
+ this.snapshot = snapshot;
330
+ for (const listener of [...this.listeners]) try {
331
+ listener();
332
+ } catch (error) {
333
+ console.error("client-modules: synchronization subscriber failed", error);
334
+ }
335
+ }
336
+ enqueue(task, subject = "graph") {
337
+ const run = this.queue.then(task);
338
+ this.queue = run.then(() => void 0, (error) => {
339
+ this.publish({
340
+ syncing: false,
341
+ failures: [...this.snapshot.failures.filter((failure) => failure.id !== subject), {
342
+ id: subject,
343
+ message: String(error)
344
+ }]
345
+ });
346
+ });
347
+ return run;
348
+ }
349
+ current(generation) {
350
+ return !this.stopped && generation === this.generation;
351
+ }
352
+ /** Keep ownership even when Loader rejects a module's plugin exports after inserting its entry. */
353
+ async create(loader, id) {
354
+ const options = { name: id };
355
+ const entryId = loader.ensureId(options);
356
+ try {
357
+ await loader.create(options);
358
+ } finally {
359
+ this.managed.set(id, loader.resolve(entryId));
360
+ }
361
+ }
362
+ async replace(entry, id, rev, generation) {
363
+ this.index.invalidateForReplacement(id, rev);
364
+ await this.modules.prefetch(id);
365
+ if (!this.current(generation)) return;
366
+ await tearDownEntryFiber(entry);
367
+ removeOwnedStyles(id);
368
+ if (!this.current(generation)) return;
369
+ await this.modules.import(id, "", {});
370
+ if (!this.current(generation)) return;
371
+ await entry.refresh();
372
+ await entry.fiber?.await();
373
+ if (entry.fiber === void 0) throw new Error(`client-modules: ${id} import failed (see console)`);
374
+ this.revisions.set(id, rev);
375
+ }
376
+ async reconcile(generation) {
377
+ if (!this.current(generation)) return;
378
+ const loader = this.loader;
379
+ if (loader === void 0) throw new Error("client-modules: entries have not started");
380
+ const manifest = this.desired;
381
+ this.publish({
382
+ syncing: true,
383
+ failures: []
384
+ });
385
+ const failures = [];
386
+ this.index.update(manifest, this.managed.keys());
387
+ const wanted = new Set(manifest.plugins.map((row) => row.id));
388
+ for (const [id, entry] of this.managed) {
389
+ if (wanted.has(id)) continue;
390
+ const fiber = entry.fiber;
391
+ loader.remove(entry.id);
392
+ this.managed.delete(id);
393
+ this.revisions.delete(id);
394
+ while (fiber?.inertia !== void 0) await fiber.inertia;
395
+ }
396
+ for (const row of manifest.modules) {
397
+ if (!this.current(generation)) break;
398
+ try {
399
+ const entry = this.managed.get(row.id);
400
+ if (entry === void 0) {
401
+ await this.modules.prefetch(row.id);
402
+ if (!this.current(generation)) break;
403
+ await this.modules.import(row.id, "", {});
404
+ if (!this.current(generation)) break;
405
+ await this.create(loader, row.id);
406
+ this.revisions.set(row.id, row.rev);
407
+ } else if (this.revisions.get(row.id) !== row.rev) await this.replace(entry, row.id, row.rev, generation);
408
+ else if (entry.fiber === void 0) await this.replace(entry, row.id, row.rev, generation);
409
+ else if (entry.fiber.state === FAILED) entry.fiber.update(entry.options.config);
410
+ } catch (error) {
411
+ failures.push({
412
+ id: row.id,
413
+ message: String(error)
414
+ });
415
+ }
416
+ }
417
+ await loader.await();
418
+ for (const [id, entry] of this.managed) {
419
+ if (failures.some((failure) => failure.id === id)) continue;
420
+ if (entry.fiber?.state === ACTIVE) continue;
421
+ try {
422
+ if (entry.fiber === void 0) throw new Error(`client-modules: ${id} import failed (see console)`);
423
+ await entry.fiber.await();
424
+ failures.push({
425
+ id,
426
+ message: `client-modules: ${id} is waiting for activation`
427
+ });
428
+ } catch (error) {
429
+ failures.push({
430
+ id,
431
+ message: String(error)
432
+ });
433
+ }
434
+ }
435
+ this.index.prune([...loader.entries()].map((entry) => entry.options.name));
436
+ if (this.current(generation)) this.publish({
437
+ syncing: false,
438
+ failures
439
+ });
440
+ }
441
+ };
442
+ //#endregion
138
443
  //#region lib/types/client/system.js
139
444
  /**
140
445
  * ClientModuleSystem — the implementation behind the {@link ClientModuleLoader}
@@ -162,6 +467,19 @@ window.__ModuleLoader__.load({
162
467
  if (!/[?&]rev=[^&#]*/.test(url)) throw new Error(`client-modules: bundle URL ${url} has no revision`);
163
468
  return url.replace(/([?&]rev=)[^&#]*/, `$1${encodeURIComponent(rev)}`);
164
469
  }
470
+ const CLIENT_CHUNK = /^client\.[A-Za-z0-9][A-Za-z0-9._-]*\.js$/;
471
+ /** Internal module-table key for one package-local chunk. */
472
+ function chunkId(ownerId, fileName) {
473
+ return `${ownerId}/${fileName}`;
474
+ }
475
+ /** Resolve a sibling chunk against the package's one-resource URL and current revision. */
476
+ function chunkUrl(row, fileName, rev) {
477
+ const url = atRevision(row.url, rev);
478
+ const resourceStart = url.indexOf("/??");
479
+ const revisionStart = url.indexOf("&rev=", resourceStart + 3);
480
+ if ((resourceStart < 0 || revisionStart < 0 ? void 0 : url.slice(resourceStart + 3, revisionStart)) !== `${row.id}/client.js`) throw new Error(`client-modules: cannot resolve chunk ${JSON.stringify(fileName)} from bundle URL ${url}`);
481
+ return `${url.slice(0, resourceStart)}/${row.id}/${fileName}?${url.slice(revisionStart + 1)}`;
482
+ }
165
483
  /**
166
484
  * Claim and inventory the <style> tags a factory injected during
167
485
  * materialization: preset-emitted tags arrive pre-tagged with data-plugin;
@@ -184,14 +502,17 @@ window.__ModuleLoader__.load({
184
502
  var ClientModuleSystem = class {
185
503
  version = "client";
186
504
  manifest;
505
+ entries;
187
506
  loadCache = /* @__PURE__ */ new Map();
188
507
  seed;
189
508
  factories = /* @__PURE__ */ new Map();
190
509
  bootstrapIds = /* @__PURE__ */ new Set();
191
510
  /** In-flight script transport per URL; every row in one batch shares it. */
192
511
  pendingArrival = /* @__PURE__ */ new Map();
512
+ /** Owner generation captured by in-flight chunk requests and advanced on invalidation. */
513
+ generations = /* @__PURE__ */ new Map();
193
514
  /** Single-resource combo URL selected by HMR after invalidating one row. */
194
- reloadUrls = /* @__PURE__ */ new Map();
515
+ reloadTargets = /* @__PURE__ */ new Map();
195
516
  /** Materialization re-entrancy guard: factory-form CJS cannot deliver partial exports, so a cycle is fatal. */
196
517
  materializing = /* @__PURE__ */ new Set();
197
518
  graphRows = /* @__PURE__ */ new Map();
@@ -202,12 +523,21 @@ window.__ModuleLoader__.load({
202
523
  */
203
524
  constructor(options) {
204
525
  this.manifest = options.manifest;
526
+ this.entries = new ClientEntries(this, {
527
+ update: (manifest, managed) => {
528
+ this.updateManifest(manifest, managed);
529
+ },
530
+ invalidateForReplacement: (id, rev) => {
531
+ if (this.bootstrapIds.has(id)) throw new Error(`client-modules: replacing bootstrap module ${id} requires a page reload`);
532
+ this.invalidate(id, rev);
533
+ },
534
+ prune: (roots) => {
535
+ this.prune(roots);
536
+ }
537
+ });
205
538
  this.seed = new Map(Object.entries(options.staticModules));
206
539
  this.loadBundle = options.loadBundle ?? defaultLoadBundle;
207
- for (const row of options.manifest.modules) {
208
- if (this.graphRows.has(row.id)) throw new Error(`client-modules: duplicate graph entry "${row.id}"`);
209
- this.graphRows.set(row.id, row);
210
- }
540
+ for (const row of options.manifest.modules) this.graphRows.set(row.id, row);
211
541
  const bootstrapId = stripClientSuffix(options.bootstrapModule.id);
212
542
  this.bootstrapIds.add(bootstrapId);
213
543
  this.loadCache.set(bootstrapId, {
@@ -227,16 +557,24 @@ window.__ModuleLoader__.load({
227
557
  }
228
558
  /** Register one bundle factory, rejecting a script that executes twice without invalidation. */
229
559
  register(registration) {
230
- const id = stripClientSuffix(registration.id);
231
- if (this.bootstrapIds.has(id) || this.factories.has(id)) throw new Error(`client-modules: duplicate factory registration for "${registration.id}" (bundle executed twice without invalidate?)`);
232
- this.factories.set(id, registration.factory);
560
+ const ownerId = stripClientSuffix(registration.id);
561
+ if (registration.chunk !== void 0 && !CLIENT_CHUNK.test(registration.chunk)) throw new Error(`client-modules: invalid package-local chunk ${JSON.stringify(registration.chunk)}`);
562
+ const id = registration.chunk === void 0 ? ownerId : chunkId(ownerId, registration.chunk);
563
+ if (this.bootstrapIds.has(id) || this.factories.has(id)) {
564
+ const registrationName = registration.chunk === void 0 ? registration.id : id;
565
+ throw new Error(`client-modules: duplicate factory registration for "${registrationName}" (bundle executed twice without invalidate?)`);
566
+ }
567
+ this.factories.set(id, {
568
+ factory: registration.factory,
569
+ rev: this.reloadTargets.get(ownerId)?.rev ?? this.graphRows.get(ownerId)?.rev
570
+ });
233
571
  }
234
572
  /** Load one graph row so its factory is registered (idempotent per in-flight arrival). */
235
573
  arrive(row) {
236
574
  const { id } = row;
237
575
  if (this.loadCache.has(id) || this.factories.has(id)) return Promise.resolve();
238
- const reloadUrl = this.reloadUrls.get(id);
239
- const url = reloadUrl ?? row.initialUrl;
576
+ const reload = this.reloadTargets.get(id);
577
+ const url = reload?.url ?? row.initialUrl;
240
578
  let transport = this.pendingArrival.get(url);
241
579
  if (transport === void 0) {
242
580
  transport = this.loadBundle(url).finally(() => {
@@ -246,7 +584,7 @@ window.__ModuleLoader__.load({
246
584
  }
247
585
  return transport.then(() => {
248
586
  if (!this.factories.has(id)) throw new Error(`client-modules: bundle ${url} loaded without registering "${id}" via __ModuleLoader__.load`);
249
- if (reloadUrl !== void 0 && this.reloadUrls.get(id) === reloadUrl) this.reloadUrls.delete(id);
587
+ if (reload !== void 0 && this.reloadTargets.get(id) === reload) this.reloadTargets.delete(id);
250
588
  });
251
589
  }
252
590
  /** Register each injected package and unresolved dynamic request before its consumer. */
@@ -269,7 +607,7 @@ window.__ModuleLoader__.load({
269
607
  await this.arrive(row);
270
608
  }
271
609
  /** Materialize a registered factory (synchronous; memoized in loadCache). */
272
- materialize(id) {
610
+ materialize(id, ownerId = id) {
273
611
  const existing = this.loadCache.get(id);
274
612
  if (existing !== void 0) return existing;
275
613
  const registered = this.factories.get(id);
@@ -281,24 +619,22 @@ window.__ModuleLoader__.load({
281
619
  const edges = /* @__PURE__ */ new Set();
282
620
  const record = {
283
621
  id,
284
- exports: registered(this.makeRequire(edges)),
285
- styles: claimStyles(id),
622
+ exports: registered.factory(this.makeRequire(ownerId, edges)),
623
+ styles: claimStyles(ownerId),
286
624
  edges
287
625
  };
288
626
  this.loadCache.set(id, record);
289
627
  return record;
628
+ } catch (error) {
629
+ removeOwnedStyles(ownerId);
630
+ throw error;
290
631
  } finally {
291
632
  this.materializing.delete(id);
292
633
  }
293
634
  }
294
- /**
295
- * The synchronous require answered to factories: seed → memoized record →
296
- * registered factory. Fetching is async and therefore unreachable
297
- * from here; an external dynamic package must have arrived before its
298
- * consumer materializes.
299
- */
300
- makeRequire(edges) {
301
- return (spec) => {
635
+ /** Build the synchronous module-table require and its asynchronous chunk operation. */
636
+ makeRequire(ownerId, edges) {
637
+ const require = (spec) => {
302
638
  edges.add(spec);
303
639
  if (this.seed.has(spec)) return this.seed.get(spec);
304
640
  const id = stripClientSuffix(spec);
@@ -307,6 +643,41 @@ window.__ModuleLoader__.load({
307
643
  if (this.factories.has(id)) return this.materialize(id).exports;
308
644
  throw new Error(`client-modules: require("${spec}") missed the module table — not a platform seed word, not a materialized module, and no registered package factory (a build-time externals drift, or a dynamic dependency that did not arrive)`);
309
645
  };
646
+ require.async = async (spec) => {
647
+ edges.add(spec);
648
+ if (!spec.startsWith("./")) return await this.import(spec);
649
+ const fileName = spec.slice(2);
650
+ if (!CLIENT_CHUNK.test(fileName)) throw new Error(`client-modules: invalid relative chunk request ${JSON.stringify(spec)}`);
651
+ return await this.importChunk(ownerId, fileName);
652
+ };
653
+ return require;
654
+ }
655
+ /** Load, register, and materialize one package-local dynamic chunk. */
656
+ async importChunk(ownerId, fileName) {
657
+ const id = chunkId(ownerId, fileName);
658
+ const existing = this.loadCache.get(id);
659
+ if (existing !== void 0) return existing.exports;
660
+ if (!this.factories.has(id)) {
661
+ const generation = this.generations.get(ownerId) ?? 0;
662
+ const row = this.graphRows.get(ownerId);
663
+ if (row === void 0) throw new Error(`client-modules: chunk owner "${ownerId}" is not a boot graph entry`);
664
+ const url = chunkUrl(row, fileName, this.factories.get(ownerId)?.rev ?? this.reloadTargets.get(ownerId)?.rev ?? row.rev);
665
+ let transport = this.pendingArrival.get(url);
666
+ if (transport === void 0) {
667
+ transport = this.loadBundle(url).finally(() => {
668
+ this.pendingArrival.delete(url);
669
+ });
670
+ this.pendingArrival.set(url, transport);
671
+ }
672
+ await transport;
673
+ if ((this.generations.get(ownerId) ?? 0) !== generation) {
674
+ this.factories.delete(id);
675
+ this.loadCache.delete(id);
676
+ return await this.importChunk(ownerId, fileName);
677
+ }
678
+ if (!this.factories.has(id)) throw new Error(`client-modules: bundle ${url} loaded without registering "${id}" via __ModuleLoader__.load`);
679
+ }
680
+ return this.materialize(id, ownerId).exports;
310
681
  }
311
682
  async import(specifier) {
312
683
  if (this.seed.has(specifier)) return this.seed.get(specifier);
@@ -325,50 +696,101 @@ window.__ModuleLoader__.load({
325
696
  if (row === void 0) throw new Error(`client-modules: prefetch("${id}") — not a graph entry`);
326
697
  await this.arriveGraphRow(row);
327
698
  }
699
+ /** Refresh descriptors and unowned factory revisions before any entry imports its dependencies. */
700
+ updateManifest(manifest, managed) {
701
+ for (const id of this.bootstrapIds) if (this.manifest.modules.some((row) => row.id === id) && !manifest.modules.some((row) => row.id === id)) throw new Error(`client-modules: removing bootstrap module ${id} requires a page reload`);
702
+ const owned = new Set(managed);
703
+ for (const row of manifest.modules) {
704
+ this.graphRows.set(row.id, {
705
+ ...row,
706
+ initialUrl: row.url
707
+ });
708
+ const cachedRevision = this.factories.get(row.id)?.rev ?? this.reloadTargets.get(row.id)?.rev;
709
+ if (!owned.has(row.id) && cachedRevision !== void 0 && cachedRevision !== row.rev) {
710
+ this.invalidate(row.id, row.rev);
711
+ removeOwnedStyles(row.id);
712
+ }
713
+ }
714
+ this.manifest = manifest;
715
+ }
716
+ /** Retain live Loader modules and their transitive requests before evicting unreferenced graph records. */
717
+ prune(roots) {
718
+ const retained = new Set(this.bootstrapIds);
719
+ const visit = (specifier) => {
720
+ const id = stripClientSuffix(specifier);
721
+ if (retained.has(id)) return;
722
+ retained.add(id);
723
+ const row = this.graphRows.get(id);
724
+ for (const request of [
725
+ ...row?.external ?? [],
726
+ ...row?.inject ?? [],
727
+ ...this.loadCache.get(id)?.edges ?? []
728
+ ]) visit(request);
729
+ };
730
+ for (const row of this.manifest.modules) visit(row.id);
731
+ for (const id of roots) visit(id);
732
+ for (const id of this.graphRows.keys()) {
733
+ if (retained.has(id)) continue;
734
+ this.graphRows.delete(id);
735
+ this.invalidate(id);
736
+ removeOwnedStyles(id);
737
+ }
738
+ }
328
739
  invalidate(id, rev) {
329
740
  const normalized = stripClientSuffix(id);
330
741
  if (this.bootstrapIds.has(normalized)) return;
742
+ this.generations.set(normalized, (this.generations.get(normalized) ?? 0) + 1);
331
743
  const row = this.graphRows.get(normalized);
332
- if (row !== void 0) this.reloadUrls.set(normalized, atRevision(row.url, rev ?? row.rev));
333
- else this.reloadUrls.delete(normalized);
334
- this.factories.delete(normalized);
335
- this.loadCache.delete(normalized);
744
+ if (row !== void 0) {
745
+ const revision = rev ?? row.rev;
746
+ this.reloadTargets.set(normalized, {
747
+ url: atRevision(row.url, revision),
748
+ rev: revision
749
+ });
750
+ } else this.reloadTargets.delete(normalized);
751
+ for (const key of this.factories.keys()) if (key === normalized || key.startsWith(`${normalized}/client.`)) this.factories.delete(key);
752
+ for (const key of this.loadCache.keys()) if (key === normalized || key.startsWith(`${normalized}/client.`)) this.loadCache.delete(key);
336
753
  }
337
754
  };
338
755
  //#endregion
339
756
  //#region lib/types/client/index.js
340
- let moduleSystem;
341
757
  /**
342
758
  * Build the live module system from the HTML facade's materialized modules bundle.
343
759
  * @param target - Stable registration facade whose pending queue becomes the live sink.
344
760
  * @param bootstrapModule - This bundle's id and already-materialized exports.
345
761
  * @param options - Raw boot graph, platform seed, and optional bundle transport.
346
- * @returns The created module system, also published for this package's Cordis plugin face.
762
+ * @returns The created module system.
347
763
  */
348
764
  function createClientModuleSystem(target, bootstrapModule, options) {
349
- moduleSystem = new ClientModuleSystem({
765
+ return new ClientModuleSystem({
350
766
  manifest: parseBootManifest(options.boot),
351
767
  staticModules: options.staticModules,
352
768
  registrationTarget: target,
353
769
  bootstrapModule,
354
770
  ...options.loadBundle === void 0 ? {} : { loadBundle: options.loadBundle }
355
771
  });
356
- return moduleSystem;
357
772
  }
773
+ /** Required service: the Loader whose internal module system this plugin publishes. */
774
+ const inject = ["loader"];
358
775
  /**
359
776
  * Enroll the kernel-built module system as `ctx.modules`.
360
777
  * @param ctx - client root context.
361
778
  */
362
779
  function apply(ctx) {
363
- if (moduleSystem === void 0) throw new Error("client-modules: createClientModuleSystem must run before plugin boot");
364
- ctx.reflect.provide("modules", moduleSystem);
780
+ const modules = ctx.loader.internal;
781
+ if (modules?.version !== "client") throw new Error("client-modules: the Loader has no client module system");
782
+ ctx.reflect.provide("modules", modules);
365
783
  }
366
784
  //#endregion
367
785
  exports.ClientModuleSystem = ClientModuleSystem;
368
786
  exports.apply = apply;
369
787
  exports.createClientModuleSystem = createClientModuleSystem;
788
+ exports.exactPackageSpecifier = exactPackageSpecifier;
789
+ exports.inject = inject;
370
790
  exports.parseBootManifest = parseBootManifest;
791
+ exports.parseDshClient = parseDshClient;
371
792
  exports.stripClientSuffix = stripClientSuffix;
793
+ exports.tearDownEntryFiber = tearDownEntryFiber;
372
794
  return module.exports;
373
795
  }
374
796
  });