@deepseek-ai/dsh-client-modules 0.1.6-alpha.1 → 0.1.7-alpha.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
- import { createHash, randomBytes } from "node:crypto";
2
+ import { createHash } from "node:crypto";
3
3
  import { existsSync, readFileSync, statSync } from "node:fs";
4
4
  import { dirname, isAbsolute, join } from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -165,6 +165,8 @@ const COMBO_REVISION_PLACEHOLDER = "0".repeat(HASH_REVISION_LENGTH);
165
165
  const SOURCE_MAP_TRAILER = /(?:\r?\n)?\/\/# sourceMappingURL=[^\r\n]*(?:\r?\n)?$/;
166
166
  /** Debugger source name appended to page bundles in the WebWorker image. */
167
167
  const SOURCE_URL_TRAILER = /(?:\r?\n)?\/\/# sourceURL=([^\r\n]+)(?:\r?\n)?$/;
168
+ /** Published package-local client chunk names accepted by the on-demand route. */
169
+ const CLIENT_CHUNK = /^client\.[A-Za-z0-9][A-Za-z0-9._-]*\.js$/;
168
170
  /** Resolve `exports["./client"]` to a relative path, accepting the string and one-level conditional forms. */
169
171
  function clientExportOf(pkgName, exportsField) {
170
172
  if (typeof exportsField !== "object" || exportsField === null) return void 0;
@@ -177,27 +179,58 @@ function clientExportOf(pkgName, exportsField) {
177
179
  }
178
180
  throw new Error(`client-modules: ${pkgName} exports["./client"] must be a string or an object with a string default`);
179
181
  }
180
- /** sha1 content hash shortened to 12 hex chars (combo / graph / rebuilt-artifact rev). */
182
+ /** sha1 metadata hash shortened to 12 hex chars. */
181
183
  function shortHash(input) {
182
184
  return createHash("sha1").update(input).digest("hex").slice(0, HASH_REVISION_LENGTH);
183
185
  }
184
186
  /** Hash several response fields without allowing bytes to move across field boundaries. */
185
187
  function framedHash(domain, parts) {
186
188
  const hash = createHash("sha1").update(domain).update("\0");
187
- for (const part of parts) hash.update(`${String(part.byteLength)}:`).update(part);
189
+ for (const part of parts) hash.update(`${String(Buffer.byteLength(part))}:`).update(part);
188
190
  return hash.digest("hex").slice(0, HASH_REVISION_LENGTH);
189
191
  }
190
- /** Hash every artifact input served after HMR observes one plugin change. */
191
- function artifactRevision(bundle, sourceMap) {
192
- return framedHash("plugin-artifact", sourceMap === void 0 ? [bundle] : [bundle, sourceMap.body]);
192
+ /** Identify an entry's build from filesystem metadata without hashing its contents. */
193
+ function artifactRevision(baseline) {
194
+ return framedHash("plugin-artifact", [
195
+ String(baseline.mtimeMs),
196
+ String(baseline.ctimeMs),
197
+ String(baseline.size)
198
+ ]);
193
199
  }
194
- /** Address one ordered plugin-file list through the shared combo route. */
200
+ /** Absolute route prefix serving every plugin resource. */
201
+ const PLUGIN_ROUTE = "/plugins";
202
+ /** Combo query addressing one ordered plugin-file list. */
203
+ function comboSearch(ids, rev, sourceMap = false) {
204
+ return `??${ids.map((id) => `${id}/client.js${sourceMap ? ".map" : ""}`).join(",")}&rev=${rev}`;
205
+ }
206
+ /** Absolute route URL for one combo resource. */
195
207
  function comboUrl(ids, rev, sourceMap = false) {
196
- return `/plugins/??${ids.map((id) => `${id}/client.js${sourceMap ? ".map" : ""}`).join(",")}&rev=${rev}`;
208
+ return `${PLUGIN_ROUTE}/${comboSearch(ids, rev, sourceMap)}`;
209
+ }
210
+ /**
211
+ * Browser reference to one combo resource: app-owned browser routes are
212
+ * document-relative, so the route key's leading slash is stripped here, at the
213
+ * boundary between the two halves. The rule and its reasons are owned by
214
+ * .agents/notes/implemented/architecture/2026-09-14-web-document-relative-app-routes.md.
215
+ */
216
+ function comboReference(ids, rev, sourceMap = false) {
217
+ return comboUrl(ids, rev, sourceMap).slice(1);
218
+ }
219
+ /** Absolute route URL for one package-local chunk. */
220
+ function chunkUrl(id, fileName, rev, sourceMap = false) {
221
+ return `${PLUGIN_ROUTE}/${id}/${fileName}${sourceMap ? ".map" : ""}?rev=${rev}`;
222
+ }
223
+ /**
224
+ * Source-map reference stamped into one chunk script. A script's map reference
225
+ * resolves against that script's own directory rather than the document, so
226
+ * this is the bare map file name, not the document-relative route.
227
+ */
228
+ function chunkMapReference(fileName, rev) {
229
+ return `${fileName}.map?rev=${rev}`;
197
230
  }
198
- /** Measure the longer map-form URL used to partition a startup resource list. */
231
+ /** Measure the longest browser-facing combo URL used to partition a startup resource list. */
199
232
  function projectedComboUrlBytes(records) {
200
- return Buffer.byteLength(comboUrl(records.map((record) => record.entry.id), COMBO_REVISION_PLACEHOLDER, true));
233
+ return Buffer.byteLength(comboReference(records.map((record) => record.entry.id), COMBO_REVISION_PLACEHOLDER, true));
201
234
  }
202
235
  /** Partition one phase in graph order without allowing a generated URL above the protocol limit. */
203
236
  function partitionComboRecords(records) {
@@ -218,23 +251,23 @@ function partitionComboRecords(records) {
218
251
  return chunks;
219
252
  }
220
253
  /** Remove bundle-local debug directives and retain their stable generated-file name. */
221
- function comboSource(record) {
222
- let source = record.bundle.toString("utf8");
254
+ function prepareSource(resource) {
255
+ let source = resource.bundle.toString("utf8");
223
256
  const sourceUrl = SOURCE_URL_TRAILER.exec(source)?.[1];
224
257
  source = source.replace(SOURCE_URL_TRAILER, "").replace(SOURCE_MAP_TRAILER, "");
225
258
  if (!source.endsWith("\n")) source += "\n";
226
- const fallbackSource = sourceUrl === void 0 ? `/plugins/${record.entry.id}/client.js` : /^(?:[A-Za-z][A-Za-z\d+.-]*:|\/)/.test(sourceUrl) ? sourceUrl : `/${sourceUrl}`;
259
+ const fallbackSource = sourceUrl === void 0 ? `/plugins/${resource.id}/${resource.fileName}` : /^(?:[A-Za-z][A-Za-z\d+.-]*:|\/)/.test(sourceUrl) ? sourceUrl : `/${sourceUrl}`;
227
260
  return {
228
261
  source,
229
262
  fallbackSource
230
263
  };
231
264
  }
232
- /** Stamp a combo script's absolute indexed-map URL onto its executable bytes. */
265
+ /** Stamp a combo script's source-map reference onto its executable bytes. */
233
266
  function comboScript(input, sourceMapUrl) {
234
267
  return Buffer.from(sourceMapUrl === void 0 ? input : `${input}//# sourceMappingURL=${sourceMapUrl}\n`);
235
268
  }
236
- /** Parse an optional source-map artifact; missing maps do not prevent plugin execution. */
237
- function sourceMapSnapshot(clientPath) {
269
+ /** Read and parse an optional source map when its combo-map endpoint is requested. */
270
+ function readSourceMap(clientPath) {
238
271
  let body;
239
272
  try {
240
273
  body = readFileSync(`${clientPath}.map`);
@@ -245,10 +278,7 @@ function sourceMapSnapshot(clientPath) {
245
278
  const value = JSON.parse(body.toString("utf8"));
246
279
  const parsed = typeof value === "object" && value !== null ? value : void 0;
247
280
  if (parsed === void 0 || parsed.version !== 3 || !Array.isArray(parsed.sources) || parsed.sources.some((source) => typeof source !== "string") || !Array.isArray(parsed.names) || parsed.names.some((name) => typeof name !== "string") || typeof parsed.mappings !== "string") throw new Error(`client-modules: ${clientPath}.map is not a regular Source Map v3 object`);
248
- return {
249
- body,
250
- parsed
251
- };
281
+ return parsed;
252
282
  }
253
283
  /** Count generated lines while assembling indexed-map section offsets. */
254
284
  function newlineCount(value) {
@@ -257,13 +287,10 @@ function newlineCount(value) {
257
287
  return count;
258
288
  }
259
289
  /** Resolve section sources against their original per-plugin map URL before combo relocation. */
260
- function comboSectionMap(record) {
261
- const original = record.sourceMap?.parsed;
262
- /* v8 ignore next -- callers add sections only for records with a source map. */
263
- if (original === void 0) throw new Error(`client-modules: source map missing for ${record.entry.id}`);
290
+ function comboSectionMap(resource, original) {
264
291
  const sourcePaths = original.sources;
265
292
  const sourceRoot = typeof original.sourceRoot === "string" ? original.sourceRoot : "";
266
- const base = new URL(`/plugins/${record.entry.id}/client.js.map`, "http://dsh.invalid");
293
+ const base = new URL(`/plugins/${resource.id}/client.js.map`, "http://dsh.invalid");
267
294
  const relocated = sourcePaths.map((source) => {
268
295
  const separator = sourceRoot !== "" && !sourceRoot.endsWith("/") && !source.startsWith("/") ? "/" : "";
269
296
  const resolved = new URL(`${sourceRoot}${separator}${source}`, base);
@@ -287,14 +314,35 @@ function identitySectionMap(source, sourceUrl) {
287
314
  mappings
288
315
  };
289
316
  }
290
- /** Concatenate one or more factory registrations and compose their maps as indexed sections. */
291
- function buildCombo(records, revision) {
317
+ /** Run one producer in the first requester's microtask, not off-thread, and share its settlement. */
318
+ function lazyBody(produce) {
319
+ let result;
320
+ return () => {
321
+ result ??= Promise.resolve().then(produce);
322
+ return result;
323
+ };
324
+ }
325
+ /** Derive one combo revision from the ordered immutable row revisions. */
326
+ function comboRevision(resources) {
327
+ return framedHash("combo", resources.flatMap((resource) => [resource.id, resource.rev]));
328
+ }
329
+ /** Concatenate one or more factory registrations without reading or composing source maps. */
330
+ function buildComboScript(resources, sourceMapUrl) {
292
331
  let source = "";
332
+ for (const resource of resources) source += `${prepareSource(resource).source};\n`;
333
+ return comboScript(source, sourceMapUrl);
334
+ }
335
+ /** Compose one indexed map from source-map files read only for this request. */
336
+ function buildComboSourceMap(resources, sourceMapOf, fileName = "client.js") {
293
337
  const sections = [];
294
338
  let line = 0;
295
- for (const record of records) {
296
- const prepared = comboSource(record);
297
- const section = record.sourceMap === void 0 ? identitySectionMap(prepared.source, prepared.fallbackSource) : comboSectionMap(record);
339
+ for (const resource of resources) {
340
+ const prepared = prepareSource(resource);
341
+ const sourceMap = sourceMapOf(resource.clientPath);
342
+ let section = identitySectionMap(prepared.source, prepared.fallbackSource);
343
+ if (sourceMap !== void 0) try {
344
+ section = comboSectionMap(resource, sourceMap);
345
+ } catch {}
298
346
  sections.push({
299
347
  offset: {
300
348
  line,
@@ -302,47 +350,52 @@ function buildCombo(records, revision) {
302
350
  },
303
351
  map: section
304
352
  });
305
- const bundle = `${prepared.source};\n`;
306
- source += bundle;
307
- line += newlineCount(bundle);
353
+ line += newlineCount(`${prepared.source};\n`);
308
354
  }
309
- const sourceMap = Buffer.from(`${JSON.stringify({
355
+ return Buffer.from(`${JSON.stringify({
310
356
  version: 3,
311
- file: "client.js",
357
+ file: fileName,
312
358
  sections
313
359
  })}\n`);
314
- const sourceBytes = Buffer.from(source);
315
- const rev = revision ?? framedHash("combo", [sourceBytes, sourceMap]);
316
- const entries = records.map((record) => record.entry.id);
317
- const url = comboUrl(entries, rev);
318
- const sourceMapUrl = comboUrl(entries, rev, true);
360
+ }
361
+ /** Describe one combo and defer its executable and debug payloads independently. */
362
+ function buildCombo(records, sourceMapOf, revision) {
363
+ const resources = records.map((record) => ({
364
+ id: record.entry.id,
365
+ rev: record.entry.rev,
366
+ clientPath: record.meta.clientPath,
367
+ fileName: "client.js",
368
+ bundle: record.bundle
369
+ }));
370
+ const rev = revision ?? comboRevision(resources);
371
+ const entries = resources.map((resource) => resource.id);
319
372
  return {
320
- url,
373
+ url: comboUrl(entries, rev),
321
374
  rev,
322
375
  entries,
323
- script: comboScript(source, sourceMapUrl),
324
- sourceMap,
325
- sourceMapUrl
376
+ sourceMapUrl: comboUrl(entries, rev, true),
377
+ scriptBody: lazyBody(() => buildComboScript(resources, comboSearch(entries, rev, true))),
378
+ sourceMapBody: lazyBody(() => buildComboSourceMap(resources, sourceMapOf))
326
379
  };
327
380
  }
328
381
  /** Add initial-load scheduling metadata to a combo artifact. */
329
- function buildBatch(phase, records) {
330
- const artifact = buildCombo(records);
382
+ function buildBatch(phase, records, sourceMapOf) {
383
+ const artifact = buildCombo(records, sourceMapOf);
331
384
  return {
332
385
  ...artifact,
333
386
  descriptor: {
334
387
  phase,
335
- url: artifact.url,
388
+ url: artifact.url.slice(1),
336
389
  rev: artifact.rev,
337
390
  entries: artifact.entries
338
391
  }
339
392
  };
340
393
  }
341
- /** Graph row for one bundle rev (url carries the rev as its cache-busting query). */
394
+ /** Graph row for one bundle rev (the reference carries the rev as its cache-busting query). */
342
395
  function graphRow(id, rev, fields) {
343
396
  return {
344
397
  id,
345
- url: comboUrl([id], rev),
398
+ url: comboReference([id], rev),
346
399
  rev,
347
400
  ...fields.inject !== void 0 ? { inject: fields.inject } : {},
348
401
  ...fields.immediately ? { immediately: true } : {},
@@ -458,8 +511,6 @@ var ClientModuleRegistry = class extends Service {
458
511
  rebuildListeners = /* @__PURE__ */ new Set();
459
512
  graphListeners = /* @__PURE__ */ new Set();
460
513
  dirty = /* @__PURE__ */ new Set();
461
- initialRevisionNonce = randomBytes(8).toString("hex");
462
- nextInitialRevision = 0;
463
514
  responses = /* @__PURE__ */ new Map();
464
515
  batchResponses = /* @__PURE__ */ new Map();
465
516
  /** One prior graph generation covers a request racing the HMR recomposition that replaced its URL. */
@@ -494,7 +545,7 @@ var ClientModuleRegistry = class extends Service {
494
545
  const registerWebCarrier = (webCtx) => {
495
546
  webCtx.effect(() => webCtx.webServer.register({
496
547
  kind: "prefix",
497
- path: "/plugins",
548
+ path: PLUGIN_ROUTE,
498
549
  handler: this.serveBundle
499
550
  }), "client-modules: bundle route");
500
551
  };
@@ -521,12 +572,13 @@ var ClientModuleRegistry = class extends Service {
521
572
  /**
522
573
  * Serve an advertised revisioned bundle or source map without a Web server.
523
574
  * Unknown URLs return 404, unsupported methods return 405, and `HEAD`
524
- * returns the same immutable headers without a body.
575
+ * returns the same immutable headers without materializing a body. Each body
576
+ * is built once on its first `GET`; script construction never reads maps.
525
577
  * @param request - shell-carrier request for a `/plugins` resource.
526
578
  * @returns the exact response also exposed by the optional Web route.
527
579
  */
528
- fetchBundle(request) {
529
- const resource = this.bundleResource(request.method, request.url);
580
+ async fetchBundle(request) {
581
+ const resource = await this.bundleResource(request.method, request.url);
530
582
  const body = resource.body === void 0 ? null : Uint8Array.from(resource.body);
531
583
  return new Response(body, {
532
584
  status: resource.status,
@@ -546,24 +598,22 @@ var ClientModuleRegistry = class extends Service {
546
598
  return baseline === void 0 ? void 0 : { ...baseline };
547
599
  }
548
600
  /**
549
- * Re-hash one bundle (the HMR watch's registration hook — the only entry
550
- * point through which bundle content changes reach the graph).
601
+ * Publish one completed bundle generation (the HMR watch's registration
602
+ * hook — the only entry point through which build changes reach the graph).
603
+ * Unchanged mtime, ctime and size preserve the graph without reading the bundle.
551
604
  * @param id - entry id (package name).
552
- * @returns the new rev, or undefined for an unknown id.
605
+ * @returns the current artifact rev, or undefined for an unknown id.
553
606
  */
554
607
  rebuilt(id) {
555
608
  const record = this.table.get(id);
556
609
  if (record === void 0) return void 0;
557
610
  const baseline = this.captureArtifactBaseline(record.meta.clientPath);
611
+ const rev = artifactRevision(baseline);
612
+ if (rev === record.entry.rev) return rev;
558
613
  const bundle = readFileSync(record.meta.clientPath);
559
- const sourceMap = this.readSourceMapSnapshot(record.meta.clientPath);
560
- const rev = artifactRevision(bundle, sourceMap);
561
614
  record.baseline = baseline;
562
- if (rev === record.entry.rev) return rev;
563
615
  record.entry = graphRow(id, rev, record.meta);
564
616
  record.bundle = bundle;
565
- if (sourceMap === void 0) delete record.sourceMap;
566
- else record.sourceMap = sourceMap;
567
617
  this.composed = this.compose();
568
618
  for (const notify of this.rebuildListeners) try {
569
619
  notify(id, rev);
@@ -574,7 +624,7 @@ var ClientModuleRegistry = class extends Service {
574
624
  return rev;
575
625
  }
576
626
  /**
577
- * Subscribe to bundle rebuilds; fires only when the re-hash changed the rev.
627
+ * Subscribe to bundle rebuilds; fires only when artifact metadata changes the rev.
578
628
  * @param listener - receives the entry id and its new bundle rev.
579
629
  * @returns the unsubscriber.
580
630
  */
@@ -602,31 +652,32 @@ var ClientModuleRegistry = class extends Service {
602
652
  const bootstrapIds = new Set(bootstrap.map((record) => record.entry.id));
603
653
  const application = entries.filter((entry) => !bootstrapIds.has(entry.id)).map((entry) => this.table.get(entry.id)).filter((record) => record !== void 0);
604
654
  const artifacts = [];
605
- for (const records of partitionComboRecords(bootstrap)) artifacts.push(buildBatch("bootstrap", records));
606
- for (const records of partitionComboRecords(application)) artifacts.push(buildBatch("application", records));
655
+ for (const records of partitionComboRecords(bootstrap)) artifacts.push(buildBatch("bootstrap", records, this.readSourceMap));
656
+ for (const records of partitionComboRecords(application)) artifacts.push(buildBatch("application", records, this.readSourceMap));
607
657
  const batchResponses = /* @__PURE__ */ new Map();
608
658
  for (const artifact of artifacts) {
609
- batchResponses.set(artifact.descriptor.url, {
610
- body: artifact.script,
659
+ batchResponses.set(artifact.url, this.responses.get(artifact.url) ?? {
660
+ body: artifact.scriptBody,
611
661
  contentType: "text/javascript; charset=utf-8"
612
662
  });
613
- batchResponses.set(artifact.sourceMapUrl, {
614
- body: artifact.sourceMap,
663
+ batchResponses.set(artifact.sourceMapUrl, this.responses.get(artifact.sourceMapUrl) ?? {
664
+ body: artifact.sourceMapBody,
615
665
  contentType: "application/json; charset=utf-8"
616
666
  });
617
667
  }
618
668
  const responses = new Map(batchResponses);
619
669
  for (const record of this.table.values()) {
620
- const artifact = buildCombo([record], record.entry.rev);
621
- responses.set(artifact.url, {
622
- body: artifact.script,
670
+ const artifact = buildCombo([record], this.readSourceMap, record.entry.rev);
671
+ responses.set(artifact.url, responses.get(artifact.url) ?? this.responses.get(artifact.url) ?? {
672
+ body: artifact.scriptBody,
623
673
  contentType: "text/javascript; charset=utf-8"
624
674
  });
625
- responses.set(artifact.sourceMapUrl, {
626
- body: artifact.sourceMap,
675
+ responses.set(artifact.sourceMapUrl, responses.get(artifact.sourceMapUrl) ?? this.responses.get(artifact.sourceMapUrl) ?? {
676
+ body: artifact.sourceMapBody,
627
677
  contentType: "application/json; charset=utf-8"
628
678
  });
629
679
  }
680
+ for (const [resourceUrl, response] of this.responses) if (this.chunkRequest(new URL(resourceUrl, "http://x")) !== void 0) responses.set(resourceUrl, response);
630
681
  this.previousBatchResponses = this.batchResponses;
631
682
  this.batchResponses = batchResponses;
632
683
  this.responses = responses;
@@ -746,15 +797,12 @@ var ClientModuleRegistry = class extends Service {
746
797
  return {
747
798
  path: clientPath,
748
799
  mtimeMs: bundle.mtimeMs,
800
+ ctimeMs: bundle.ctimeMs,
749
801
  size: bundle.size
750
802
  };
751
803
  }
752
- /** Allocate an opaque initial row revision without inspecting artifact bytes. */
753
- allocateInitialRevision() {
754
- return `${this.initialRevisionNonce}-${String(this.nextInitialRevision++)}`;
755
- }
756
804
  /**
757
- * Read the activation-time bundle and optional source-map snapshots.
805
+ * Read the activation-time bundle snapshot.
758
806
  * @param pkgName - package that declares the client bundle.
759
807
  * @param clientPath - absolute path of the built client artifact.
760
808
  * @returns the immutable bytes plus the pre-read filesystem baseline.
@@ -763,27 +811,24 @@ var ClientModuleRegistry = class extends Service {
763
811
  initialBundleSnapshot(pkgName, clientPath) {
764
812
  try {
765
813
  const baseline = this.captureArtifactBaseline(clientPath);
766
- const bundle = readFileSync(clientPath);
767
- const sourceMap = this.readSourceMapSnapshot(clientPath);
768
814
  return {
769
- bundle,
770
- baseline,
771
- ...sourceMap === void 0 ? {} : { sourceMap }
815
+ bundle: readFileSync(clientPath),
816
+ baseline
772
817
  };
773
818
  } catch (error) {
774
819
  if (error.code !== "ENOENT") throw error;
775
820
  throw new MissingClientBundleError(pkgName, clientPath, error);
776
821
  }
777
822
  }
778
- /** Treat a missing, torn, or malformed development map as an identity-mapped artifact revision. */
779
- readSourceMapSnapshot(clientPath) {
823
+ /** Treat a missing, torn, or malformed development map as an identity section. */
824
+ readSourceMap = (clientPath) => {
780
825
  try {
781
- return sourceMapSnapshot(clientPath);
826
+ return readSourceMap(clientPath);
782
827
  } catch (error) {
783
828
  this.ctx.logger.warn(error);
784
829
  return;
785
830
  }
786
- }
831
+ };
787
832
  /** Reconcile one entry name against the live Loader sources. @returns whether the table changed. */
788
833
  processOne(entryName, onError) {
789
834
  const nextSources = /* @__PURE__ */ new Map();
@@ -834,15 +879,14 @@ var ClientModuleRegistry = class extends Service {
834
879
  if (source === void 0) return this.table.delete(packageName);
835
880
  if (this.table.get(packageName)?.sourceKey === source.sourceKey) return false;
836
881
  const snapshot = this.initialBundleSnapshot(packageName, source.meta.clientPath);
837
- const rev = this.allocateInitialRevision();
882
+ const rev = artifactRevision(snapshot.baseline);
838
883
  this.table.set(packageName, {
839
884
  entry: graphRow(packageName, rev, source.meta),
840
885
  loaderName: source.loaderName,
841
886
  sourceKey: source.sourceKey,
842
887
  meta: source.meta,
843
888
  bundle: snapshot.bundle,
844
- baseline: snapshot.baseline,
845
- ...snapshot.sourceMap === void 0 ? {} : { sourceMap: snapshot.sourceMap }
889
+ baseline: snapshot.baseline
846
890
  });
847
891
  return true;
848
892
  }
@@ -867,24 +911,68 @@ var ClientModuleRegistry = class extends Service {
867
911
  this.composed = composed;
868
912
  this.notifyGraphChanged();
869
913
  }
870
- bundleResource(method, url) {
914
+ /** Match an exact current-revision package-local chunk URL without reading its file. */
915
+ chunkRequest(requestUrl) {
916
+ const resourceUrl = `${requestUrl.pathname}${requestUrl.search}`;
917
+ for (const record of this.table.values()) {
918
+ const prefix = `/plugins/${record.entry.id}/`;
919
+ if (!requestUrl.pathname.startsWith(prefix)) continue;
920
+ const requested = requestUrl.pathname.slice(prefix.length);
921
+ const sourceMap = requested.endsWith(".map");
922
+ const fileName = sourceMap ? requested.slice(0, -4) : requested;
923
+ if (!CLIENT_CHUNK.test(fileName)) return void 0;
924
+ if (resourceUrl !== chunkUrl(record.entry.id, fileName, record.entry.rev, sourceMap)) return void 0;
925
+ return {
926
+ record,
927
+ fileName,
928
+ sourceMap,
929
+ resourceUrl
930
+ };
931
+ }
932
+ }
933
+ /** Build a package-local chunk response only when its URL is requested. */
934
+ chunkResponse(requestUrl) {
935
+ const request = this.chunkRequest(requestUrl);
936
+ if (request === void 0) return void 0;
937
+ const { record, fileName, sourceMap, resourceUrl } = request;
938
+ const clientPath = join(dirname(record.meta.clientPath), fileName);
939
+ if (!existsSync(clientPath)) return void 0;
940
+ const sourceMapUrl = chunkMapReference(fileName, record.entry.rev);
941
+ const resource = () => ({
942
+ id: record.entry.id,
943
+ rev: record.entry.rev,
944
+ clientPath,
945
+ fileName,
946
+ bundle: readFileSync(clientPath)
947
+ });
948
+ const response = sourceMap ? {
949
+ body: lazyBody(() => buildComboSourceMap([resource()], this.readSourceMap, fileName)),
950
+ contentType: "application/json; charset=utf-8"
951
+ } : {
952
+ body: lazyBody(() => buildComboScript([resource()], sourceMapUrl)),
953
+ contentType: "text/javascript; charset=utf-8"
954
+ };
955
+ this.responses.set(resourceUrl, response);
956
+ return response;
957
+ }
958
+ async bundleResource(method, url) {
871
959
  if (method !== "GET" && method !== "HEAD") return { status: 405 };
872
960
  const requestUrl = new URL(url, "http://x");
873
961
  const resourceUrl = `${requestUrl.pathname}${requestUrl.search}`;
874
- const response = this.responses.get(resourceUrl) ?? this.previousBatchResponses.get(resourceUrl);
962
+ const response = this.responses.get(resourceUrl) ?? this.previousBatchResponses.get(resourceUrl) ?? this.chunkResponse(requestUrl);
875
963
  if (response !== void 0) return {
876
964
  status: 200,
877
965
  headers: {
878
966
  "content-type": response.contentType,
879
967
  "cache-control": IMMUTABLE_CACHE
880
968
  },
881
- ...method === "HEAD" ? {} : { body: response.body }
969
+ ...method === "HEAD" ? {} : { body: await response.body() }
882
970
  };
883
971
  return { status: 404 };
884
972
  }
885
- serveBundle = (req, res) => {
973
+ serveBundle = async (req, res) => {
886
974
  /* v8 ignore next -- `?? '/'` arm: node:http always sets url on server requests. */
887
- const response = this.bundleResource(req.method, req.url ?? "/");
975
+ const response = await this.bundleResource(req.method, req.url ?? "/");
888
976
  res.writeHead(response.status, response.headers);
889
977
  res.end(response.body);
890
978
  };
package/lib/invariant.js CHANGED
@@ -11,8 +11,8 @@ const inject = ["invariants"];
11
11
  /**
12
12
  * Owned relation: the node half's boot entry graph must stay self-consistent
13
13
  * — every row must resolve a clientPath under the same id (the
14
- * /plugins/<id>/client.js URL it advertises would otherwise 404 on a browser
15
- * that just received the graph). Checked on every scan trigger (cordis
14
+ * `plugins/<id>/client.js` reference it advertises would otherwise 404 in the
15
+ * browser that just received the graph). Checked on every scan trigger (cordis
16
16
  * 'internal/plugin'): graph() and clientPath() read the same table object,
17
17
  * so the relation holds at any instant — no need to wait out the node half's
18
18
  * own microtask-debounced flush.
@@ -0,0 +1,76 @@
1
+ import type { Loader } from '@deepseek-ai/cordis-plugin-loader';
2
+ import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
3
+ import type { BootManifest, ClientModuleLoader } from './manifest.ts';
4
+ /** Page-local failures do not change the Host's bundle enablement. */
5
+ export interface ClientEntryState {
6
+ /** True while a snapshot, retry or code replacement is being applied. */
7
+ readonly syncing: boolean;
8
+ /** Package ids and errors from the latest reconciliation. */
9
+ readonly failures: readonly {
10
+ readonly id: string;
11
+ readonly message: string;
12
+ }[];
13
+ }
14
+ /** Module-table capabilities used within serialized entry operations. */
15
+ interface ModuleIndex {
16
+ update(manifest: BootManifest, managed: Iterable<string>): void;
17
+ invalidateForReplacement(id: string, rev: string): void;
18
+ prune(roots: Iterable<string>): void;
19
+ }
20
+ /** Manages only entries created from the Host manifest; other Loader contributors retain ownership. */
21
+ export declare class ClientEntries {
22
+ private readonly modules;
23
+ private readonly index;
24
+ /** Stable observable consumed by page diagnostics through the renderer's injected hook. */
25
+ readonly state: ObservableSnapshot<ClientEntryState>;
26
+ private snapshot;
27
+ private readonly listeners;
28
+ private readonly managed;
29
+ private readonly revisions;
30
+ private loader;
31
+ private queue;
32
+ private desired;
33
+ private generation;
34
+ private stopped;
35
+ /**
36
+ * Construct the page controller before Cordis boot.
37
+ * @param modules - Module arrival and materialization owner.
38
+ * @param index - Private descriptor replacement and unused-module cleanup.
39
+ */
40
+ constructor(modules: ClientModuleLoader, index: ModuleIndex);
41
+ /**
42
+ * Create the initial roster and retain its entry identities for subsequent reconciliation.
43
+ * @param loader - Page Loader, already configured with the module system.
44
+ * @param manifest - Initial roster audited by the boot caller.
45
+ * @returns after initial entries and their activation settle; boot owns its activation audit.
46
+ */
47
+ start(loader: Loader, manifest: BootManifest): Promise<void>;
48
+ /**
49
+ * Validate and apply the latest full Host graph. Changed targets cancel obsolete mounts; identical targets share pending loads.
50
+ * @param graph - JSON-decoded graph received from the Host.
51
+ * @returns after the queued reconciliation; per-package failures remain available in {@link state}.
52
+ */
53
+ sync(graph: unknown): Promise<void>;
54
+ /**
55
+ * Retry failed entries against the latest graph, including an unchanged revision.
56
+ * @returns after retry settlement, with remaining errors in {@link state}.
57
+ */
58
+ retry(): Promise<void>;
59
+ /**
60
+ * Replace one entry's code in the same queue as graph updates; duplicate revisions are ignored.
61
+ * Entries missing after a failed import are reconciled; bootstrap replacement fails before teardown.
62
+ * @param id - Package id from a rebuilt frame.
63
+ * @param rev - Opaque revision selecting the rebuilt artifact.
64
+ * @returns after queued work; replacement errors reject, while per-package reconciliation errors remain in {@link state}.
65
+ */
66
+ reload(id: string, rev: string): Promise<void>;
67
+ private publish;
68
+ private enqueue;
69
+ private current;
70
+ /** Keep ownership even when Loader rejects a module's plugin exports after inserting its entry. */
71
+ private create;
72
+ private replace;
73
+ private reconcile;
74
+ }
75
+ export {};
76
+ //# sourceMappingURL=entries.d.ts.map
@@ -0,0 +1,14 @@
1
+ /** Loader lifecycle operations shared by live graph reconciliation and code replacement. */
2
+ import type { Entry } from '@deepseek-ai/cordis-plugin-loader';
3
+ /**
4
+ * Release a runtime before clearing its entry fiber so Loader refresh can import new code.
5
+ * Registry deletion prevents Loader from treating replacement as a user disable.
6
+ * @param entry - Entry retained for code replacement.
7
+ */
8
+ export declare function tearDownEntryFiber(entry: Entry): Promise<void>;
9
+ /**
10
+ * Remove styles after their plugin's effect cleanup has settled.
11
+ * @param id - Package whose factory owns the style tags.
12
+ */
13
+ export declare function removeOwnedStyles(id: string): void;
14
+ //# sourceMappingURL=entry-lifecycle.d.ts.map
@@ -14,6 +14,8 @@ import type { Context } from '@deepseek-ai/cordis';
14
14
  import { ClientModuleSystem } from './system.ts';
15
15
  import type { ClientBootstrapModule, ClientModuleCreateOptions, ClientModuleLoaderTarget } from './manifest.ts';
16
16
  export { ClientModuleSystem };
17
+ export { tearDownEntryFiber } from './entry-lifecycle.ts';
18
+ export type { ClientEntries, ClientEntryState } from './entries.ts';
17
19
  export { exactPackageSpecifier, parseBootManifest, parseDshClient, stripClientSuffix } from './manifest.ts';
18
20
  export type { BootManifest, BootModuleRow, BootPluginRow, ClientBootstrapModule, ClientBundleRegistration, ClientModuleCreateOptions, ClientModuleLoader, ClientModuleLoaderTarget, ClientModuleRecord, ClientModuleSystemOptions, DshWindow, WebBootEntry, WebBootGraph, } from './manifest.ts';
19
21
  /**