@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/index.js CHANGED
@@ -50,6 +50,43 @@ function optionalStringArray(subject, field, value) {
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
@@ -71,7 +108,7 @@ function stripClientSuffix(spec) {
71
108
  * combo scripts plus their source maps,
72
109
  * contributes the registration facade, application preloads, bootstrap scripts,
73
110
  * and graph to the webserver's index injection table, and provides the
74
- * `clientModuleHost` service (the HMR node half's registration/notification
111
+ * `clientModules` service (the HMR node half's registration/notification
75
112
  * face).
76
113
  *
77
114
  * Scanning is incremental per package — there is no full-rescan code path.
@@ -128,30 +165,8 @@ const COMBO_REVISION_PLACEHOLDER = "0".repeat(HASH_REVISION_LENGTH);
128
165
  const SOURCE_MAP_TRAILER = /(?:\r?\n)?\/\/# sourceMappingURL=[^\r\n]*(?:\r?\n)?$/;
129
166
  /** Debugger source name appended to page bundles in the WebWorker image. */
130
167
  const SOURCE_URL_TRAILER = /(?:\r?\n)?\/\/# sourceURL=([^\r\n]+)(?:\r?\n)?$/;
131
- /** Return a bare package-root specifier, excluding package subpaths and path-like entries. */
132
- function exactPackageSpecifier(specifier) {
133
- if (specifier.startsWith("@")) {
134
- const parts = specifier.split("/");
135
- return parts.length === 2 && parts.every(Boolean) ? specifier : void 0;
136
- }
137
- return specifier.length > 0 && !specifier.includes("/") ? specifier : void 0;
138
- }
139
- /** Narrow an unknown parsed JSON value to the `dsh.client` declaration, throwing on malformed fields. */
140
- function parseDshClient(pkgName, value) {
141
- if (value === void 0) return void 0;
142
- if (typeof value !== "object" || value === null) throw new Error(`client-modules: ${pkgName} has a non-object dsh.client declaration`);
143
- const decl = value;
144
- if (typeof decl.platform !== "string") throw new Error(`client-modules: ${pkgName} dsh.client.platform must be a string`);
145
- const inject = optionalStringArray(pkgName, "dsh.client.inject", decl.inject);
146
- const external = optionalStringArray(pkgName, "dsh.client.external", decl.external);
147
- if (decl.immediately !== void 0 && typeof decl.immediately !== "boolean") throw new Error(`client-modules: ${pkgName} dsh.client.immediately must be a boolean`);
148
- return {
149
- platform: decl.platform,
150
- ...inject !== void 0 ? { inject } : {},
151
- ...external !== void 0 ? { external } : {},
152
- ...decl.immediately !== void 0 ? { immediately: decl.immediately } : {}
153
- };
154
- }
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$/;
155
170
  /** Resolve `exports["./client"]` to a relative path, accepting the string and one-level conditional forms. */
156
171
  function clientExportOf(pkgName, exportsField) {
157
172
  if (typeof exportsField !== "object" || exportsField === null) return void 0;
@@ -174,14 +189,18 @@ function framedHash(domain, parts) {
174
189
  for (const part of parts) hash.update(`${String(part.byteLength)}:`).update(part);
175
190
  return hash.digest("hex").slice(0, HASH_REVISION_LENGTH);
176
191
  }
177
- /** Hash every artifact input served after HMR observes one plugin change. */
178
- function artifactRevision(bundle, sourceMap) {
179
- return framedHash("plugin-artifact", sourceMap === void 0 ? [bundle] : [bundle, sourceMap.body]);
192
+ /** Hash one completed build generation observed through its entry artifact. */
193
+ function artifactRevision(bundle, baseline) {
194
+ return framedHash("plugin-artifact", [bundle, Buffer.from(String(baseline.mtimeMs))]);
180
195
  }
181
196
  /** Address one ordered plugin-file list through the shared combo route. */
182
197
  function comboUrl(ids, rev, sourceMap = false) {
183
198
  return `/plugins/??${ids.map((id) => `${id}/client.js${sourceMap ? ".map" : ""}`).join(",")}&rev=${rev}`;
184
199
  }
200
+ /** Address one package-local chunk through the same revision as its entry. */
201
+ function chunkUrl(id, fileName, rev, sourceMap = false) {
202
+ return `/plugins/${id}/${fileName}${sourceMap ? ".map" : ""}?rev=${rev}`;
203
+ }
185
204
  /** Measure the longer map-form URL used to partition a startup resource list. */
186
205
  function projectedComboUrlBytes(records) {
187
206
  return Buffer.byteLength(comboUrl(records.map((record) => record.entry.id), COMBO_REVISION_PLACEHOLDER, true));
@@ -205,12 +224,12 @@ function partitionComboRecords(records) {
205
224
  return chunks;
206
225
  }
207
226
  /** Remove bundle-local debug directives and retain their stable generated-file name. */
208
- function comboSource(record) {
209
- let source = record.bundle.toString("utf8");
227
+ function prepareSource(resource) {
228
+ let source = resource.bundle.toString("utf8");
210
229
  const sourceUrl = SOURCE_URL_TRAILER.exec(source)?.[1];
211
230
  source = source.replace(SOURCE_URL_TRAILER, "").replace(SOURCE_MAP_TRAILER, "");
212
231
  if (!source.endsWith("\n")) source += "\n";
213
- const fallbackSource = sourceUrl === void 0 ? `/plugins/${record.entry.id}/client.js` : /^(?:[A-Za-z][A-Za-z\d+.-]*:|\/)/.test(sourceUrl) ? sourceUrl : `/${sourceUrl}`;
232
+ const fallbackSource = sourceUrl === void 0 ? `/plugins/${resource.id}/${resource.fileName}` : /^(?:[A-Za-z][A-Za-z\d+.-]*:|\/)/.test(sourceUrl) ? sourceUrl : `/${sourceUrl}`;
214
233
  return {
215
234
  source,
216
235
  fallbackSource
@@ -220,8 +239,8 @@ function comboSource(record) {
220
239
  function comboScript(input, sourceMapUrl) {
221
240
  return Buffer.from(sourceMapUrl === void 0 ? input : `${input}//# sourceMappingURL=${sourceMapUrl}\n`);
222
241
  }
223
- /** Parse an optional source-map artifact; missing maps do not prevent plugin execution. */
224
- function sourceMapSnapshot(clientPath) {
242
+ /** Read and parse an optional source map when its combo-map endpoint is requested. */
243
+ function readSourceMap(clientPath) {
225
244
  let body;
226
245
  try {
227
246
  body = readFileSync(`${clientPath}.map`);
@@ -232,10 +251,7 @@ function sourceMapSnapshot(clientPath) {
232
251
  const value = JSON.parse(body.toString("utf8"));
233
252
  const parsed = typeof value === "object" && value !== null ? value : void 0;
234
253
  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`);
235
- return {
236
- body,
237
- parsed
238
- };
254
+ return parsed;
239
255
  }
240
256
  /** Count generated lines while assembling indexed-map section offsets. */
241
257
  function newlineCount(value) {
@@ -244,13 +260,10 @@ function newlineCount(value) {
244
260
  return count;
245
261
  }
246
262
  /** Resolve section sources against their original per-plugin map URL before combo relocation. */
247
- function comboSectionMap(record) {
248
- const original = record.sourceMap?.parsed;
249
- /* v8 ignore next -- callers add sections only for records with a source map. */
250
- if (original === void 0) throw new Error(`client-modules: source map missing for ${record.entry.id}`);
263
+ function comboSectionMap(resource, original) {
251
264
  const sourcePaths = original.sources;
252
265
  const sourceRoot = typeof original.sourceRoot === "string" ? original.sourceRoot : "";
253
- const base = new URL(`/plugins/${record.entry.id}/client.js.map`, "http://dsh.invalid");
266
+ const base = new URL(`/plugins/${resource.id}/client.js.map`, "http://dsh.invalid");
254
267
  const relocated = sourcePaths.map((source) => {
255
268
  const separator = sourceRoot !== "" && !sourceRoot.endsWith("/") && !source.startsWith("/") ? "/" : "";
256
269
  const resolved = new URL(`${sourceRoot}${separator}${source}`, base);
@@ -274,14 +287,35 @@ function identitySectionMap(source, sourceUrl) {
274
287
  mappings
275
288
  };
276
289
  }
277
- /** Concatenate one or more factory registrations and compose their maps as indexed sections. */
278
- function buildCombo(records, revision) {
290
+ /** Run one producer in the first requester's microtask, not off-thread, and share its settlement. */
291
+ function lazyBody(produce) {
292
+ let result;
293
+ return () => {
294
+ result ??= Promise.resolve().then(produce);
295
+ return result;
296
+ };
297
+ }
298
+ /** Derive one combo revision from the ordered immutable row revisions. */
299
+ function comboRevision(resources) {
300
+ return framedHash("combo", resources.flatMap((resource) => [Buffer.from(resource.id), Buffer.from(resource.rev)]));
301
+ }
302
+ /** Concatenate one or more factory registrations without reading or composing source maps. */
303
+ function buildComboScript(resources, sourceMapUrl) {
279
304
  let source = "";
305
+ for (const resource of resources) source += `${prepareSource(resource).source};\n`;
306
+ return comboScript(source, sourceMapUrl);
307
+ }
308
+ /** Compose one indexed map from source-map files read only for this request. */
309
+ function buildComboSourceMap(resources, sourceMapOf, fileName = "client.js") {
280
310
  const sections = [];
281
311
  let line = 0;
282
- for (const record of records) {
283
- const prepared = comboSource(record);
284
- const section = record.sourceMap === void 0 ? identitySectionMap(prepared.source, prepared.fallbackSource) : comboSectionMap(record);
312
+ for (const resource of resources) {
313
+ const prepared = prepareSource(resource);
314
+ const sourceMap = sourceMapOf(resource.clientPath);
315
+ let section = identitySectionMap(prepared.source, prepared.fallbackSource);
316
+ if (sourceMap !== void 0) try {
317
+ section = comboSectionMap(resource, sourceMap);
318
+ } catch {}
285
319
  sections.push({
286
320
  offset: {
287
321
  line,
@@ -289,32 +323,39 @@ function buildCombo(records, revision) {
289
323
  },
290
324
  map: section
291
325
  });
292
- const bundle = `${prepared.source};\n`;
293
- source += bundle;
294
- line += newlineCount(bundle);
326
+ line += newlineCount(`${prepared.source};\n`);
295
327
  }
296
- const sourceMap = Buffer.from(`${JSON.stringify({
328
+ return Buffer.from(`${JSON.stringify({
297
329
  version: 3,
298
- file: "client.js",
330
+ file: fileName,
299
331
  sections
300
332
  })}\n`);
301
- const sourceBytes = Buffer.from(source);
302
- const rev = revision ?? framedHash("combo", [sourceBytes, sourceMap]);
303
- const entries = records.map((record) => record.entry.id);
333
+ }
334
+ /** Describe one combo and defer its executable and debug payloads independently. */
335
+ function buildCombo(records, sourceMapOf, revision) {
336
+ const resources = records.map((record) => ({
337
+ id: record.entry.id,
338
+ rev: record.entry.rev,
339
+ clientPath: record.meta.clientPath,
340
+ fileName: "client.js",
341
+ bundle: record.bundle
342
+ }));
343
+ const rev = revision ?? comboRevision(resources);
344
+ const entries = resources.map((resource) => resource.id);
304
345
  const url = comboUrl(entries, rev);
305
346
  const sourceMapUrl = comboUrl(entries, rev, true);
306
347
  return {
307
348
  url,
308
349
  rev,
309
350
  entries,
310
- script: comboScript(source, sourceMapUrl),
311
- sourceMap,
312
- sourceMapUrl
351
+ sourceMapUrl,
352
+ scriptBody: lazyBody(() => buildComboScript(resources, sourceMapUrl)),
353
+ sourceMapBody: lazyBody(() => buildComboSourceMap(resources, sourceMapOf))
313
354
  };
314
355
  }
315
356
  /** Add initial-load scheduling metadata to a combo artifact. */
316
- function buildBatch(phase, records) {
317
- const artifact = buildCombo(records);
357
+ function buildBatch(phase, records, sourceMapOf) {
358
+ const artifact = buildCombo(records, sourceMapOf);
318
359
  return {
319
360
  ...artifact,
320
361
  descriptor: {
@@ -455,6 +496,7 @@ var ClientModuleRegistry = class extends Service {
455
496
  composed;
456
497
  /**
457
498
  * Build the service: subscribe, seed, and run the activation flush.
499
+ * Bundle routes follow the optional Web carrier's injected lifecycle.
458
500
  * @param ctx - plugin context carrying Loader and an optional Web carrier.
459
501
  */
460
502
  constructor(ctx) {
@@ -484,8 +526,7 @@ var ClientModuleRegistry = class extends Service {
484
526
  handler: this.serveBundle
485
527
  }), "client-modules: bundle route");
486
528
  };
487
- if (ctx.get("webServer") === void 0) ctx.inject(["webServer"], registerWebCarrier);
488
- else registerWebCarrier(ctx);
529
+ ctx.inject(["webServer"], registerWebCarrier);
489
530
  ctx.on("webserver/index-inject", (table) => {
490
531
  table.push(...bootInjections(this.composed));
491
532
  });
@@ -508,12 +549,13 @@ var ClientModuleRegistry = class extends Service {
508
549
  /**
509
550
  * Serve an advertised revisioned bundle or source map without a Web server.
510
551
  * Unknown URLs return 404, unsupported methods return 405, and `HEAD`
511
- * returns the same immutable headers without a body.
552
+ * returns the same immutable headers without materializing a body. Each body
553
+ * is built once on its first `GET`; script construction never reads maps.
512
554
  * @param request - shell-carrier request for a `/plugins` resource.
513
555
  * @returns the exact response also exposed by the optional Web route.
514
556
  */
515
- fetchBundle(request) {
516
- const resource = this.bundleResource(request.method, request.url);
557
+ async fetchBundle(request) {
558
+ const resource = await this.bundleResource(request.method, request.url);
517
559
  const body = resource.body === void 0 ? null : Uint8Array.from(resource.body);
518
560
  return new Response(body, {
519
561
  status: resource.status,
@@ -533,8 +575,8 @@ var ClientModuleRegistry = class extends Service {
533
575
  return baseline === void 0 ? void 0 : { ...baseline };
534
576
  }
535
577
  /**
536
- * Re-hash one bundle (the HMR watch's registration hook — the only entry
537
- * point through which bundle content changes reach the graph).
578
+ * Publish one completed bundle generation (the HMR watch's registration
579
+ * hook — the only entry point through which build changes reach the graph).
538
580
  * @param id - entry id (package name).
539
581
  * @returns the new rev, or undefined for an unknown id.
540
582
  */
@@ -543,14 +585,11 @@ var ClientModuleRegistry = class extends Service {
543
585
  if (record === void 0) return void 0;
544
586
  const baseline = this.captureArtifactBaseline(record.meta.clientPath);
545
587
  const bundle = readFileSync(record.meta.clientPath);
546
- const sourceMap = this.readSourceMapSnapshot(record.meta.clientPath);
547
- const rev = artifactRevision(bundle, sourceMap);
588
+ const rev = artifactRevision(bundle, baseline);
548
589
  record.baseline = baseline;
549
590
  if (rev === record.entry.rev) return rev;
550
591
  record.entry = graphRow(id, rev, record.meta);
551
592
  record.bundle = bundle;
552
- if (sourceMap === void 0) delete record.sourceMap;
553
- else record.sourceMap = sourceMap;
554
593
  this.composed = this.compose();
555
594
  for (const notify of this.rebuildListeners) try {
556
595
  notify(id, rev);
@@ -589,31 +628,32 @@ var ClientModuleRegistry = class extends Service {
589
628
  const bootstrapIds = new Set(bootstrap.map((record) => record.entry.id));
590
629
  const application = entries.filter((entry) => !bootstrapIds.has(entry.id)).map((entry) => this.table.get(entry.id)).filter((record) => record !== void 0);
591
630
  const artifacts = [];
592
- for (const records of partitionComboRecords(bootstrap)) artifacts.push(buildBatch("bootstrap", records));
593
- for (const records of partitionComboRecords(application)) artifacts.push(buildBatch("application", records));
631
+ for (const records of partitionComboRecords(bootstrap)) artifacts.push(buildBatch("bootstrap", records, this.readSourceMap));
632
+ for (const records of partitionComboRecords(application)) artifacts.push(buildBatch("application", records, this.readSourceMap));
594
633
  const batchResponses = /* @__PURE__ */ new Map();
595
634
  for (const artifact of artifacts) {
596
- batchResponses.set(artifact.descriptor.url, {
597
- body: artifact.script,
635
+ batchResponses.set(artifact.descriptor.url, this.responses.get(artifact.descriptor.url) ?? {
636
+ body: artifact.scriptBody,
598
637
  contentType: "text/javascript; charset=utf-8"
599
638
  });
600
- batchResponses.set(artifact.sourceMapUrl, {
601
- body: artifact.sourceMap,
639
+ batchResponses.set(artifact.sourceMapUrl, this.responses.get(artifact.sourceMapUrl) ?? {
640
+ body: artifact.sourceMapBody,
602
641
  contentType: "application/json; charset=utf-8"
603
642
  });
604
643
  }
605
644
  const responses = new Map(batchResponses);
606
645
  for (const record of this.table.values()) {
607
- const artifact = buildCombo([record], record.entry.rev);
608
- responses.set(artifact.url, {
609
- body: artifact.script,
646
+ const artifact = buildCombo([record], this.readSourceMap, record.entry.rev);
647
+ responses.set(artifact.url, responses.get(artifact.url) ?? this.responses.get(artifact.url) ?? {
648
+ body: artifact.scriptBody,
610
649
  contentType: "text/javascript; charset=utf-8"
611
650
  });
612
- responses.set(artifact.sourceMapUrl, {
613
- body: artifact.sourceMap,
651
+ responses.set(artifact.sourceMapUrl, responses.get(artifact.sourceMapUrl) ?? this.responses.get(artifact.sourceMapUrl) ?? {
652
+ body: artifact.sourceMapBody,
614
653
  contentType: "application/json; charset=utf-8"
615
654
  });
616
655
  }
656
+ for (const [resourceUrl, response] of this.responses) if (this.chunkRequest(new URL(resourceUrl, "http://x")) !== void 0) responses.set(resourceUrl, response);
617
657
  this.previousBatchResponses = this.batchResponses;
618
658
  this.batchResponses = batchResponses;
619
659
  this.responses = responses;
@@ -741,7 +781,7 @@ var ClientModuleRegistry = class extends Service {
741
781
  return `${this.initialRevisionNonce}-${String(this.nextInitialRevision++)}`;
742
782
  }
743
783
  /**
744
- * Read the activation-time bundle and optional source-map snapshots.
784
+ * Read the activation-time bundle snapshot.
745
785
  * @param pkgName - package that declares the client bundle.
746
786
  * @param clientPath - absolute path of the built client artifact.
747
787
  * @returns the immutable bytes plus the pre-read filesystem baseline.
@@ -750,27 +790,24 @@ var ClientModuleRegistry = class extends Service {
750
790
  initialBundleSnapshot(pkgName, clientPath) {
751
791
  try {
752
792
  const baseline = this.captureArtifactBaseline(clientPath);
753
- const bundle = readFileSync(clientPath);
754
- const sourceMap = this.readSourceMapSnapshot(clientPath);
755
793
  return {
756
- bundle,
757
- baseline,
758
- ...sourceMap === void 0 ? {} : { sourceMap }
794
+ bundle: readFileSync(clientPath),
795
+ baseline
759
796
  };
760
797
  } catch (error) {
761
798
  if (error.code !== "ENOENT") throw error;
762
799
  throw new MissingClientBundleError(pkgName, clientPath, error);
763
800
  }
764
801
  }
765
- /** Treat a missing, torn, or malformed development map as an identity-mapped artifact revision. */
766
- readSourceMapSnapshot(clientPath) {
802
+ /** Treat a missing, torn, or malformed development map as an identity section. */
803
+ readSourceMap = (clientPath) => {
767
804
  try {
768
- return sourceMapSnapshot(clientPath);
805
+ return readSourceMap(clientPath);
769
806
  } catch (error) {
770
807
  this.ctx.logger.warn(error);
771
808
  return;
772
809
  }
773
- }
810
+ };
774
811
  /** Reconcile one entry name against the live Loader sources. @returns whether the table changed. */
775
812
  processOne(entryName, onError) {
776
813
  const nextSources = /* @__PURE__ */ new Map();
@@ -828,8 +865,7 @@ var ClientModuleRegistry = class extends Service {
828
865
  sourceKey: source.sourceKey,
829
866
  meta: source.meta,
830
867
  bundle: snapshot.bundle,
831
- baseline: snapshot.baseline,
832
- ...snapshot.sourceMap === void 0 ? {} : { sourceMap: snapshot.sourceMap }
868
+ baseline: snapshot.baseline
833
869
  });
834
870
  return true;
835
871
  }
@@ -854,24 +890,68 @@ var ClientModuleRegistry = class extends Service {
854
890
  this.composed = composed;
855
891
  this.notifyGraphChanged();
856
892
  }
857
- bundleResource(method, url) {
893
+ /** Match an exact current-revision package-local chunk URL without reading its file. */
894
+ chunkRequest(requestUrl) {
895
+ const resourceUrl = `${requestUrl.pathname}${requestUrl.search}`;
896
+ for (const record of this.table.values()) {
897
+ const prefix = `/plugins/${record.entry.id}/`;
898
+ if (!requestUrl.pathname.startsWith(prefix)) continue;
899
+ const requested = requestUrl.pathname.slice(prefix.length);
900
+ const sourceMap = requested.endsWith(".map");
901
+ const fileName = sourceMap ? requested.slice(0, -4) : requested;
902
+ if (!CLIENT_CHUNK.test(fileName)) return void 0;
903
+ if (resourceUrl !== chunkUrl(record.entry.id, fileName, record.entry.rev, sourceMap)) return void 0;
904
+ return {
905
+ record,
906
+ fileName,
907
+ sourceMap,
908
+ resourceUrl
909
+ };
910
+ }
911
+ }
912
+ /** Build a package-local chunk response only when its URL is requested. */
913
+ chunkResponse(requestUrl) {
914
+ const request = this.chunkRequest(requestUrl);
915
+ if (request === void 0) return void 0;
916
+ const { record, fileName, sourceMap, resourceUrl } = request;
917
+ const clientPath = join(dirname(record.meta.clientPath), fileName);
918
+ if (!existsSync(clientPath)) return void 0;
919
+ const sourceMapUrl = chunkUrl(record.entry.id, fileName, record.entry.rev, true);
920
+ const resource = () => ({
921
+ id: record.entry.id,
922
+ rev: record.entry.rev,
923
+ clientPath,
924
+ fileName,
925
+ bundle: readFileSync(clientPath)
926
+ });
927
+ const response = sourceMap ? {
928
+ body: lazyBody(() => buildComboSourceMap([resource()], this.readSourceMap, fileName)),
929
+ contentType: "application/json; charset=utf-8"
930
+ } : {
931
+ body: lazyBody(() => buildComboScript([resource()], sourceMapUrl)),
932
+ contentType: "text/javascript; charset=utf-8"
933
+ };
934
+ this.responses.set(resourceUrl, response);
935
+ return response;
936
+ }
937
+ async bundleResource(method, url) {
858
938
  if (method !== "GET" && method !== "HEAD") return { status: 405 };
859
939
  const requestUrl = new URL(url, "http://x");
860
940
  const resourceUrl = `${requestUrl.pathname}${requestUrl.search}`;
861
- const response = this.responses.get(resourceUrl) ?? this.previousBatchResponses.get(resourceUrl);
941
+ const response = this.responses.get(resourceUrl) ?? this.previousBatchResponses.get(resourceUrl) ?? this.chunkResponse(requestUrl);
862
942
  if (response !== void 0) return {
863
943
  status: 200,
864
944
  headers: {
865
945
  "content-type": response.contentType,
866
946
  "cache-control": IMMUTABLE_CACHE
867
947
  },
868
- ...method === "HEAD" ? {} : { body: response.body }
948
+ ...method === "HEAD" ? {} : { body: await response.body() }
869
949
  };
870
950
  return { status: 404 };
871
951
  }
872
- serveBundle = (req, res) => {
952
+ serveBundle = async (req, res) => {
873
953
  /* v8 ignore next -- `?? '/'` arm: node:http always sets url on server requests. */
874
- const response = this.bundleResource(req.method, req.url ?? "/");
954
+ const response = await this.bundleResource(req.method, req.url ?? "/");
875
955
  res.writeHead(response.status, response.headers);
876
956
  res.end(response.body);
877
957
  };
@@ -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
@@ -6,24 +6,28 @@
6
6
  * parser-preloads this ordinary client bundle into the pending registration
7
7
  * queue. The HTML-installed loader facade materializes this bundle and calls
8
8
  * its bootstrap export, which constructs the system and retains the same
9
- * exports for this package's graph row. The plugin face only enrolls that
10
- * pre-existing instance by providing it as `ctx.modules`.
9
+ * exports for this package's graph row. The plugin face enrolls the module
10
+ * system attached to its own Loader as `ctx.modules`.
11
11
  * @module @deepseek-ai/dsh-client-modules/client
12
12
  */
13
13
  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 { parseBootManifest, stripClientSuffix } from './manifest.ts';
17
+ export { tearDownEntryFiber } from './entry-lifecycle.ts';
18
+ export type { ClientEntries, ClientEntryState } from './entries.ts';
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
  /**
20
22
  * Build the live module system from the HTML facade's materialized modules bundle.
21
23
  * @param target - Stable registration facade whose pending queue becomes the live sink.
22
24
  * @param bootstrapModule - This bundle's id and already-materialized exports.
23
25
  * @param options - Raw boot graph, platform seed, and optional bundle transport.
24
- * @returns The created module system, also published for this package's Cordis plugin face.
26
+ * @returns The created module system.
25
27
  */
26
28
  export declare function createClientModuleSystem(target: ClientModuleLoaderTarget, bootstrapModule: ClientBootstrapModule, options: ClientModuleCreateOptions): ClientModuleSystem;
29
+ /** Required service: the Loader whose internal module system this plugin publishes. */
30
+ export declare const inject: string[];
27
31
  /**
28
32
  * Enroll the kernel-built module system as `ctx.modules`.
29
33
  * @param ctx - client root context.