@deepseek-ai/dsh-client-modules 0.1.6-alpha.2 → 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/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/modules/README.md
5
- README.md: 5b751268430b716e7a0af24f8e25aaf2f865d079
6
- README.zh.md: 4152d64ca9dde60ab1aeb76c98508d8d86fcf54e
5
+ README.md: c99293b1f7a26a86279c4d71d9d6f41c6452b13e
6
+ README.zh.md: f6057204e1a039cecb824a3c355f4d7b906abac9
package/README.md CHANGED
@@ -71,7 +71,7 @@ Executing a plugin bundle only registers its factory; every module-body side eff
71
71
 
72
72
  The Node half scans incrementally per package — no full-rescan path. Every `internal/plugin` emission marks the fiber's entry name dirty; a microtask flush reconciles each dirty name against the live loader entries, and the activation pass seeds the same dirty set and flushes synchronously, so first scan and steady state share one implementation. Package metadata is cached per Loader specifier and owning-tree base URL until restart, while the resolved manifest package name identifies the browser module. Distinct active Loader sources resolving to one package name are rejected; removing the conflict promotes the remaining source without requiring its fiber to restart. Bundle content changes reach the graph only through `rebuilt()` (the HMR hook).
73
73
 
74
- The Node half snapshots each `client.js` entry before publication and creates combo descriptors without building response bodies. It groups resources into `/plugins/??...&rev=...` combo URLs, with one bootstrap combo for the modules row and one or more application combos for the other rows; each phase is partitioned before a URL exceeds 3 KiB. A script body is combined once on its first `GET` and ends with its map URL. The corresponding map files are read, validated, and combined separately on the first map `GET`; `HEAD` materializes neither body. The Host does not scan or preload sibling chunks: an exact `/plugins/<package>/client.<name>.js?rev=<rev>` request reads and caches that script, and its map remains uncomputed until the map URL is requested. Every combo or chunk map is Indexed Source Map v3 and uses an authored section when available or an identity section for the packaged bundle. Initial per-plugin revisions use process nonces. During development, the shared preset stamps `client.js` after every package output is written; HMR derives the next revision from the entry bytes and that completed-build stamp, so a chunk-only rebuild changes the owner revision without a Host-side chunk scan. Combo revisions derive from the ordered row revisions. Advertised combo responses and requested chunk responses remain immutable across unrelated graph recomposition, and an unknown resource or revision returns 404.
74
+ The Node half snapshots each `client.js` entry before publication and creates combo descriptors without building response bodies. It groups resources into combo route keys (`/plugins/??...&rev=...`), with one bootstrap combo for the modules row and one or more application combos for the other rows; each phase is partitioned before a URL exceeds 3 KiB. The graph and batch descriptors carry the app-directory-relative form of the same key (`plugins/??...&rev=...`), which a browser document resolves under its own mount, while the response table stays keyed by the absolute route; the source-map trailer is resolved against the combo script's own directory. A script body is combined once on its first `GET` and ends with its map reference. The corresponding map files are read, validated, and combined separately on the first map `GET`; `HEAD` materializes neither body. The Host does not scan or preload sibling chunks: an exact `/plugins/<package>/client.<name>.js?rev=<rev>` request reads and caches that script, and its map remains uncomputed until the map URL is requested. The chunk reference the browser derives from its owner row is document-relative as well (`plugins/<package>/client.<name>.js?rev=<rev>`), and a chunk script's map trailer is the bare `client.<name>.js.map?rev=<rev>` name, which resolves against the script's own directory. Every combo or chunk map is Indexed Source Map v3 and uses an authored section when available or an identity section for the packaged bundle. Initial publication and HMR derive per-plugin revisions from the entry's `mtimeMs`, `ctimeMs`, and size, without hashing artifact contents. Status-change time distinguishes rewrites that preserve mtime and size. Unchanged artifacts therefore retain their revisions across Host restarts, so SSE reconnection does not replace their browser plugins. The shared preset stamps `client.js` after every package output is written, so a chunk-only rebuild changes the owner revision without a Host-side chunk scan. Combo revisions derive from the ordered row revisions. Advertised combo responses and requested chunk responses remain immutable across unrelated graph recomposition, and an unknown resource or revision returns 404.
75
75
 
76
76
  ### Boot manifest injection
77
77
 
@@ -127,6 +127,7 @@ None; this package neither assembles nor sends a provider request.
127
127
 
128
128
  These limits define what the module system does not do. They are current package constraints, not a task backlog.
129
129
 
130
+ - **Metadata-based revisions** — revisions identify filesystem generations, not content equality. Metadata-only changes can reload a plugin; changes invisible in mtime, ctime, and size cannot be distinguished.
130
131
  - **Flat module graph by design** — every bundle is one module node whose edges point only at table leaves; the interface (`loadCache`/`edges`/`invalidate`) already supports a general module graph, so the externalization granularity can change without an interface change.
131
132
  - **Bootstrap and code replacement limits** — the page retains its modules bootstrap and static platform identities. Removing or replacing the bootstrap requires a page reload; live replacement requests report a page-local error while retaining its fiber and exports; replacing package code and all existing consumers is outside ordinary enable/disable synchronization.
132
133
  - **Lazy delivery retains requested bodies** — the Host holds each bundle and lazy response plan; a script or map body remains cached after its first `GET`, and HMR additionally retains one prior startup generation. Memory grows only for response bodies that clients request while preserving one-generation race tolerance.
package/README.zh.md CHANGED
@@ -71,7 +71,7 @@ application combo 脚本只携带每个插件的 `client.js` 入口,并在启
71
71
 
72
72
  Node 半侧逐包增量扫描——没有全量重扫路径。每次发出 `internal/plugin` 事件时,系统都会把该 fiber 的 entry 名标脏;微任务 flush 会把每个脏名与当前 loader 条目对账,激活 pass 会初始化同一个脏集合并同步 flush,因此首次扫描与稳态共用同一实现。包元数据按 Loader specifier 与所属 tree base URL 缓存至重启,解析出的 manifest(元数据清单)包名作为浏览器模块身份。若不同的 active Loader source 解析到同一包名,组合会失败;移除冲突来源后,剩余来源无需重启 fiber 即可接替。bundle 内容变更只能通过 `rebuilt()`(HMR 钩子)进入图。
73
73
 
74
- Node 半侧会在发布前快照每个 `client.js` 入口,并在不构建响应 body 的情况下创建 combo descriptor。它把资源分组到 `/plugins/??...&rev=...` combo URL:modules row 使用一个 bootstrap combo,其余 row 使用一个或多个 application combo;每个阶段都会在 URL 超过 3 KiB 之前分区。脚本 body 在首次 `GET` 时只组合一次,并以对应 map URL 结尾;map 文件则在首次 map `GET` 时单独读取、校验并组合,`HEAD` 不会物化任一 body。Host 不扫描也不预加载同级 chunk:精确的 `/plugins/<package>/client.<name>.js?rev=<rev>` 请求会读取并缓存该脚本,其 map 仍会等到 map URL 被请求后才计算。每个 combo 或 chunk map 都是 Indexed Source Map v3,并在可用时使用作者提供的 section,否则为已打包 bundle 生成 identity section。初始逐插件 revision 使用进程 nonce。开发期间,共享预设会在该包所有输出写完后标记 `client.js`;HMR 从入口字节和该构建完成标记派生下一 revision,因此仅 chunk 发生重建也会更换 owner revision,无需 Host 扫描 chunk。combo revision 从有序 row revision 派生。已公告的 combo 响应与已请求的 chunk 响应会跨无关图重组保持不可变;未知资源或 revision 返回 404。
74
+ Node 半侧会在发布前快照每个 `client.js` 入口,并在不构建响应 body 的情况下创建 combo descriptor。它把资源分组为 combo 路由键(`/plugins/??...&rev=...`):modules row 使用一个 bootstrap combo,其余 row 使用一个或多个 application combo;每个阶段都会在 URL 超过 3 KiB 之前分区。启动图与批次描述符携带同一路由键的应用目录相对形式(`plugins/??...&rev=...`),浏览器文档会按其自身挂载解析它,而响应表仍以绝对路由为键;source-map trailer 则相对该 combo 脚本自身目录解析。脚本 body 在首次 `GET` 时只组合一次,并以对应的 map 引用结尾;map 文件则在首次 map `GET` 时单独读取、校验并组合,`HEAD` 不会物化任一 body。Host 不扫描也不预加载同级 chunk:精确的 `/plugins/<package>/client.<name>.js?rev=<rev>` 请求会读取并缓存该脚本,其 map 仍会等到 map URL 被请求后才计算。浏览器从所属 row 派生出该 chunk 引用,因此它同样是文档相对形式(`plugins/<package>/client.<name>.js?rev=<rev>`);chunk 脚本携带的 map trailer 则是裸文件名 `client.<name>.js.map?rev=<rev>`,由该脚本自身目录解析。每个 combo 或 chunk map 都是 Indexed Source Map v3,并在可用时使用作者提供的 section,否则为已打包 bundle 生成 identity section。首次发布与 HMR 都从入口的 `mtimeMs`、`ctimeMs` 和大小派生逐插件 revision,不对产物内容求哈希。状态变更时间用于区分保留 mtime 和大小的重写。未变化的产物因此会跨 Host 重启保持 revision,SSE 重连不会替换其浏览器插件。共享预设会在该包所有输出写完后标记 `client.js`,因此仅 chunk 发生重建也会更换 owner revision,无需 Host 扫描 chunk。combo revision 从有序 row revision 派生。已公告的 combo 响应与已请求的 chunk 响应会跨无关图重组保持不可变;未知资源或 revision 返回 404。
75
75
 
76
76
  ### 启动 manifest 注入
77
77
 
@@ -127,6 +127,7 @@ bundle 路由随注入的 `webServer` 生命周期注册:服务就绪时注册
127
127
 
128
128
  这些限制说明模块系统不做什么。它们是当前包约束,不是任务积压。
129
129
 
130
+ - **基于元数据的 revision**——revision 标识文件系统代际,而非内容相等性。仅元数据变化也可能重载插件;mtime、ctime 和大小都无法反映的变化无法区分。
130
131
  - **有意采用扁平模块图**——每个 bundle 是一个模块节点,其边只指向表中的叶节点;接口(`loadCache`/`edges`/`invalidate`)已经支持通用模块图,因此可以改变 externalization 粒度而不更改接口。
131
132
  - **Bootstrap 与代码替换限制**——页面保留 modules bootstrap 和静态平台模块的身份。移除或替换 bootstrap 需要刷新页面;动态替换请求会报告页面本地错误,并保留其 fiber 与导出;替换包代码及其所有现有消费者不属于普通启停同步。
132
133
  - **惰性提供会保留已请求的 body**——Host 在内存中保留每个 bundle 与惰性响应计划;脚本或 map body 在首次 `GET` 后保留缓存,HMR 还会保留上一代启动响应。内存仅随客户端实际请求的响应 body 增长,同时保留一代竞态容忍。
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";
@@ -179,31 +179,58 @@ function clientExportOf(pkgName, exportsField) {
179
179
  }
180
180
  throw new Error(`client-modules: ${pkgName} exports["./client"] must be a string or an object with a string default`);
181
181
  }
182
- /** sha1 content hash shortened to 12 hex chars (combo / graph / rebuilt-artifact rev). */
182
+ /** sha1 metadata hash shortened to 12 hex chars. */
183
183
  function shortHash(input) {
184
184
  return createHash("sha1").update(input).digest("hex").slice(0, HASH_REVISION_LENGTH);
185
185
  }
186
186
  /** Hash several response fields without allowing bytes to move across field boundaries. */
187
187
  function framedHash(domain, parts) {
188
188
  const hash = createHash("sha1").update(domain).update("\0");
189
- 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);
190
190
  return hash.digest("hex").slice(0, HASH_REVISION_LENGTH);
191
191
  }
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))]);
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
+ ]);
195
199
  }
196
- /** 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. */
197
207
  function comboUrl(ids, rev, sourceMap = false) {
198
- return `/plugins/??${ids.map((id) => `${id}/client.js${sourceMap ? ".map" : ""}`).join(",")}&rev=${rev}`;
208
+ return `${PLUGIN_ROUTE}/${comboSearch(ids, rev, sourceMap)}`;
199
209
  }
200
- /** Address one package-local chunk through the same revision as its entry. */
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. */
201
220
  function chunkUrl(id, fileName, rev, sourceMap = false) {
202
- return `/plugins/${id}/${fileName}${sourceMap ? ".map" : ""}?rev=${rev}`;
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}`;
203
230
  }
204
- /** 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. */
205
232
  function projectedComboUrlBytes(records) {
206
- 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));
207
234
  }
208
235
  /** Partition one phase in graph order without allowing a generated URL above the protocol limit. */
209
236
  function partitionComboRecords(records) {
@@ -235,7 +262,7 @@ function prepareSource(resource) {
235
262
  fallbackSource
236
263
  };
237
264
  }
238
- /** 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. */
239
266
  function comboScript(input, sourceMapUrl) {
240
267
  return Buffer.from(sourceMapUrl === void 0 ? input : `${input}//# sourceMappingURL=${sourceMapUrl}\n`);
241
268
  }
@@ -297,7 +324,7 @@ function lazyBody(produce) {
297
324
  }
298
325
  /** Derive one combo revision from the ordered immutable row revisions. */
299
326
  function comboRevision(resources) {
300
- return framedHash("combo", resources.flatMap((resource) => [Buffer.from(resource.id), Buffer.from(resource.rev)]));
327
+ return framedHash("combo", resources.flatMap((resource) => [resource.id, resource.rev]));
301
328
  }
302
329
  /** Concatenate one or more factory registrations without reading or composing source maps. */
303
330
  function buildComboScript(resources, sourceMapUrl) {
@@ -342,14 +369,12 @@ function buildCombo(records, sourceMapOf, revision) {
342
369
  }));
343
370
  const rev = revision ?? comboRevision(resources);
344
371
  const entries = resources.map((resource) => resource.id);
345
- const url = comboUrl(entries, rev);
346
- const sourceMapUrl = comboUrl(entries, rev, true);
347
372
  return {
348
- url,
373
+ url: comboUrl(entries, rev),
349
374
  rev,
350
375
  entries,
351
- sourceMapUrl,
352
- scriptBody: lazyBody(() => buildComboScript(resources, sourceMapUrl)),
376
+ sourceMapUrl: comboUrl(entries, rev, true),
377
+ scriptBody: lazyBody(() => buildComboScript(resources, comboSearch(entries, rev, true))),
353
378
  sourceMapBody: lazyBody(() => buildComboSourceMap(resources, sourceMapOf))
354
379
  };
355
380
  }
@@ -360,17 +385,17 @@ function buildBatch(phase, records, sourceMapOf) {
360
385
  ...artifact,
361
386
  descriptor: {
362
387
  phase,
363
- url: artifact.url,
388
+ url: artifact.url.slice(1),
364
389
  rev: artifact.rev,
365
390
  entries: artifact.entries
366
391
  }
367
392
  };
368
393
  }
369
- /** 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). */
370
395
  function graphRow(id, rev, fields) {
371
396
  return {
372
397
  id,
373
- url: comboUrl([id], rev),
398
+ url: comboReference([id], rev),
374
399
  rev,
375
400
  ...fields.inject !== void 0 ? { inject: fields.inject } : {},
376
401
  ...fields.immediately ? { immediately: true } : {},
@@ -486,8 +511,6 @@ var ClientModuleRegistry = class extends Service {
486
511
  rebuildListeners = /* @__PURE__ */ new Set();
487
512
  graphListeners = /* @__PURE__ */ new Set();
488
513
  dirty = /* @__PURE__ */ new Set();
489
- initialRevisionNonce = randomBytes(8).toString("hex");
490
- nextInitialRevision = 0;
491
514
  responses = /* @__PURE__ */ new Map();
492
515
  batchResponses = /* @__PURE__ */ new Map();
493
516
  /** One prior graph generation covers a request racing the HMR recomposition that replaced its URL. */
@@ -522,7 +545,7 @@ var ClientModuleRegistry = class extends Service {
522
545
  const registerWebCarrier = (webCtx) => {
523
546
  webCtx.effect(() => webCtx.webServer.register({
524
547
  kind: "prefix",
525
- path: "/plugins",
548
+ path: PLUGIN_ROUTE,
526
549
  handler: this.serveBundle
527
550
  }), "client-modules: bundle route");
528
551
  };
@@ -577,17 +600,18 @@ var ClientModuleRegistry = class extends Service {
577
600
  /**
578
601
  * Publish one completed bundle generation (the HMR watch's registration
579
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.
580
604
  * @param id - entry id (package name).
581
- * @returns the new rev, or undefined for an unknown id.
605
+ * @returns the current artifact rev, or undefined for an unknown id.
582
606
  */
583
607
  rebuilt(id) {
584
608
  const record = this.table.get(id);
585
609
  if (record === void 0) return void 0;
586
610
  const baseline = this.captureArtifactBaseline(record.meta.clientPath);
611
+ const rev = artifactRevision(baseline);
612
+ if (rev === record.entry.rev) return rev;
587
613
  const bundle = readFileSync(record.meta.clientPath);
588
- const rev = artifactRevision(bundle, baseline);
589
614
  record.baseline = baseline;
590
- if (rev === record.entry.rev) return rev;
591
615
  record.entry = graphRow(id, rev, record.meta);
592
616
  record.bundle = bundle;
593
617
  this.composed = this.compose();
@@ -600,7 +624,7 @@ var ClientModuleRegistry = class extends Service {
600
624
  return rev;
601
625
  }
602
626
  /**
603
- * 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.
604
628
  * @param listener - receives the entry id and its new bundle rev.
605
629
  * @returns the unsubscriber.
606
630
  */
@@ -632,7 +656,7 @@ var ClientModuleRegistry = class extends Service {
632
656
  for (const records of partitionComboRecords(application)) artifacts.push(buildBatch("application", records, this.readSourceMap));
633
657
  const batchResponses = /* @__PURE__ */ new Map();
634
658
  for (const artifact of artifacts) {
635
- batchResponses.set(artifact.descriptor.url, this.responses.get(artifact.descriptor.url) ?? {
659
+ batchResponses.set(artifact.url, this.responses.get(artifact.url) ?? {
636
660
  body: artifact.scriptBody,
637
661
  contentType: "text/javascript; charset=utf-8"
638
662
  });
@@ -773,13 +797,10 @@ var ClientModuleRegistry = class extends Service {
773
797
  return {
774
798
  path: clientPath,
775
799
  mtimeMs: bundle.mtimeMs,
800
+ ctimeMs: bundle.ctimeMs,
776
801
  size: bundle.size
777
802
  };
778
803
  }
779
- /** Allocate an opaque initial row revision without inspecting artifact bytes. */
780
- allocateInitialRevision() {
781
- return `${this.initialRevisionNonce}-${String(this.nextInitialRevision++)}`;
782
- }
783
804
  /**
784
805
  * Read the activation-time bundle snapshot.
785
806
  * @param pkgName - package that declares the client bundle.
@@ -858,7 +879,7 @@ var ClientModuleRegistry = class extends Service {
858
879
  if (source === void 0) return this.table.delete(packageName);
859
880
  if (this.table.get(packageName)?.sourceKey === source.sourceKey) return false;
860
881
  const snapshot = this.initialBundleSnapshot(packageName, source.meta.clientPath);
861
- const rev = this.allocateInitialRevision();
882
+ const rev = artifactRevision(snapshot.baseline);
862
883
  this.table.set(packageName, {
863
884
  entry: graphRow(packageName, rev, source.meta),
864
885
  loaderName: source.loaderName,
@@ -916,7 +937,7 @@ var ClientModuleRegistry = class extends Service {
916
937
  const { record, fileName, sourceMap, resourceUrl } = request;
917
938
  const clientPath = join(dirname(record.meta.clientPath), fileName);
918
939
  if (!existsSync(clientPath)) return void 0;
919
- const sourceMapUrl = chunkUrl(record.entry.id, fileName, record.entry.rev, true);
940
+ const sourceMapUrl = chunkMapReference(fileName, record.entry.rev);
920
941
  const resource = () => ({
921
942
  id: record.entry.id,
922
943
  rev: record.entry.rev,
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.
@@ -48,7 +48,10 @@ declare module '@deepseek-ai/cordis' {
48
48
  export interface WebBootEntry {
49
49
  /** Entry name == package name. */
50
50
  id: string;
51
- /** Revisioned single-resource combo endpoint used by HMR. */
51
+ /**
52
+ * Revisioned single-resource combo reference used by HMR. It is relative to
53
+ * the document, so the browser resolves it under whatever mount served the page.
54
+ */
52
55
  url: string;
53
56
  /** Opaque plugin-artifact revision used for HMR cache busting. */
54
57
  rev: string;
@@ -65,7 +68,7 @@ export type WebBootBatchPhase = 'bootstrap' | 'application';
65
68
  export interface WebBootBatch {
66
69
  /** Parser-blocking bootstrap or preloaded application scheduling. */
67
70
  phase: WebBootBatchPhase;
68
- /** Revisioned combo script endpoint. */
71
+ /** Content-addressed combo script reference, document-relative like {@link WebBootEntry.url}. */
69
72
  url: string;
70
73
  /** Revision derived from the ordered entry revisions. */
71
74
  rev: string;
@@ -89,9 +92,9 @@ export interface WebBootGraph {
89
92
  export interface BootModuleRow {
90
93
  /** Entry name == package name (module-table key). */
91
94
  id: string;
92
- /** Revisioned single-resource combo endpoint used after HMR invalidation. */
95
+ /** Revisioned single-resource combo reference used after HMR invalidation. */
93
96
  url: string;
94
- /** Revisioned combo endpoint used before the first HMR invalidation. */
97
+ /** Content-addressed combo reference used before the first HMR invalidation. */
95
98
  initialUrl: string;
96
99
  /** Opaque plugin-artifact revision used after HMR invalidation. */
97
100
  rev: string;
@@ -40,6 +40,8 @@ export interface ClientArtifactBaseline {
40
40
  readonly path: string;
41
41
  /** Bundle modification time in milliseconds. */
42
42
  readonly mtimeMs: number;
43
+ /** Bundle status-change time in milliseconds, including writes that preserve mtime. */
44
+ readonly ctimeMs: number;
43
45
  /** Bundle size in bytes. */
44
46
  readonly size: number;
45
47
  }
@@ -81,8 +83,6 @@ export declare class ClientModuleRegistry extends Service {
81
83
  private readonly rebuildListeners;
82
84
  private readonly graphListeners;
83
85
  private readonly dirty;
84
- private readonly initialRevisionNonce;
85
- private nextInitialRevision;
86
86
  private responses;
87
87
  private batchResponses;
88
88
  /** One prior graph generation covers a request racing the HMR recomposition that replaced its URL. */
@@ -127,12 +127,13 @@ export declare class ClientModuleRegistry extends Service {
127
127
  /**
128
128
  * Publish one completed bundle generation (the HMR watch's registration
129
129
  * hook — the only entry point through which build changes reach the graph).
130
+ * Unchanged mtime, ctime and size preserve the graph without reading the bundle.
130
131
  * @param id - entry id (package name).
131
- * @returns the new rev, or undefined for an unknown id.
132
+ * @returns the current artifact rev, or undefined for an unknown id.
132
133
  */
133
134
  rebuilt(id: string): string | undefined;
134
135
  /**
135
- * Subscribe to bundle rebuilds; fires only when the re-hash changed the rev.
136
+ * Subscribe to bundle rebuilds; fires only when artifact metadata changes the rev.
136
137
  * @param listener - receives the entry id and its new bundle rev.
137
138
  * @returns the unsubscriber.
138
139
  */
@@ -163,8 +164,6 @@ export declare class ClientModuleRegistry extends Service {
163
164
  private sourceKey;
164
165
  /** Capture the bundle stats before reading its bytes. */
165
166
  private captureArtifactBaseline;
166
- /** Allocate an opaque initial row revision without inspecting artifact bytes. */
167
- private allocateInitialRevision;
168
167
  /**
169
168
  * Read the activation-time bundle snapshot.
170
169
  * @param pkgName - package that declares the client bundle.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-modules",
3
3
  "description": "Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dsh.client scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam",
4
- "version": "0.1.6-alpha.2",
4
+ "version": "0.1.7-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -38,12 +38,12 @@
38
38
  },
39
39
  "license": "MIT",
40
40
  "devDependencies": {
41
- "@deepseek-ai/dsh-host-webserver": "^0.1.6-alpha.2",
42
- "@deepseek-ai/dsh-invariants": "^0.1.6-alpha.2",
43
- "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
44
- "@deepseek-ai/cordis": "^4.0.2",
45
- "@deepseek-ai/dsh-package-manifest": "^0.1.6-alpha.2",
46
- "@deepseek-ai/dsh-client-store": "^0.1.6-alpha.2"
41
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.4",
42
+ "@deepseek-ai/dsh-host-webserver": "^0.1.7-alpha.1",
43
+ "@deepseek-ai/dsh-invariants": "^0.1.7-alpha.1",
44
+ "@deepseek-ai/cordis": "^4.0.3",
45
+ "@deepseek-ai/dsh-package-manifest": "^0.1.7-alpha.1",
46
+ "@deepseek-ai/dsh-client-store": "^0.1.7-alpha.1"
47
47
  },
48
48
  "files": [
49
49
  "lib/index.js",
@@ -52,7 +52,7 @@
52
52
  "lib/types/**/*.d.ts"
53
53
  ],
54
54
  "peerDependencies": {
55
- "@deepseek-ai/cordis": "^4.0.2"
55
+ "@deepseek-ai/cordis": "^4.0.3"
56
56
  },
57
57
  "scripts": {
58
58
  "bundle": "tsdown",