@deepseek-ai/dsh-client-modules 0.1.7-alpha.1 → 0.1.7-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/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: c99293b1f7a26a86279c4d71d9d6f41c6452b13e
6
- README.zh.md: f6057204e1a039cecb824a3c355f4d7b906abac9
5
+ README.md: 3388eb2ae421b303df6709f0bfc656fe67e3a866
6
+ README.zh.md: 3cbc600ee5ed211640070773efb72f58b91d4669
package/README.md CHANGED
@@ -35,7 +35,7 @@ A browser plugin package declares `dsh.client` in its `package.json` with `platf
35
35
 
36
36
  ### What the browser loads
37
37
 
38
- The application combo scripts carry only each plugin's `client.js` entry and register those factories once during boot; module bodies remain lazy and run only at first import or materialization. A source `import()` split by tsdown compiles to `require.async("./client.<name>.js")`; its versioned sibling script arrives only when that expression runs. Rows that share a combo URL share one in-flight script task. HMR switches one changed row to its revisioned one-resource combo URL. `<id>/client` and the bare id resolve to the same exports, because a plugin bundle is its package's client half.
38
+ The application combo scripts carry only each plugin's `client.js` entry and register those factories once during boot; module bodies remain lazy and run only at first import or materialization. A source `import()` split by tsdown compiles to `require.async("./client.<name>.js")`; its versioned sibling script arrives only when that expression runs. Rows that share a combo URL share one in-flight script task. A combo whose `<script>` fails to load is requested once more; a combo that loads without registering a row is never re-executed, because the batch script registers its packages in sequence and a replay would stop at the first duplicate registration. In both cases each still-missing row then loads its own one-resource combo URL, so one failed batch costs at most three requests per missing row and leaves the rows it did register untouched. The module system records the last import failure per row (transport, registration, dependency cascade, or factory execution); the Web boot audit reports that text per entry. HMR switches one changed row to its revisioned one-resource combo URL. `<id>/client` and the bare id resolve to the same exports, because a plugin bundle is its package's client half.
39
39
 
40
40
  ### Live plugin composition
41
41
 
package/README.zh.md CHANGED
@@ -35,7 +35,7 @@ kind: "package-reference"
35
35
 
36
36
  ### 浏览器加载什么
37
37
 
38
- application combo 脚本只携带每个插件的 `client.js` 入口,并在启动时仅注册一次这些 factory;模块主体仍保持惰性,只在首次 import 或物化时运行。经 tsdown 拆分的源码 `import()` 会编译为 `require.async("./client.<name>.js")`;只有执行该表达式时,对应的带版本同级脚本才会到达。共享 combo URL 的 row 共用一个进行中的脚本任务。HMR(热模块替换)会让一条发生变化的 row 改用带 revision 的单资源 combo URL。`<id>/client` 与裸 id 解析到同一组导出,因为插件 bundle 就是其包的客户端半侧。
38
+ application combo 脚本只携带每个插件的 `client.js` 入口,并在启动时仅注册一次这些 factory;模块主体仍保持惰性,只在首次 import 或物化时运行。经 tsdown 拆分的源码 `import()` 会编译为 `require.async("./client.<name>.js")`;只有执行该表达式时,对应的带版本同级脚本才会到达。共享 combo URL 的 row 共用一个进行中的脚本任务。`<script>` 加载失败的 combo 会再请求一次;加载成功但没有注册某条 row 的 combo 绝不会重新执行,因为批量脚本按顺序注册各个包,重放会在第一个重复注册处停止。两种情况下,每条仍缺失的 row 随后加载自己的单资源 combo URL,因此一个失败的 batch 对每条缺失 row 最多花费三次请求,且不影响它已经注册的 row。模块系统按 row 记录最后一次 import 失败(传输、注册、依赖级联或 factory 执行);Web 启动审计按条目报告该文本。HMR(热模块替换)会让一条发生变化的 row 改用带 revision 的单资源 combo URL。`<id>/client` 与裸 id 解析到同一组导出,因为插件 bundle 就是其包的客户端半侧。
39
39
 
40
40
  ### 插件动态组合
41
41
 
package/lib/client.js CHANGED
@@ -468,6 +468,10 @@ window.__ModuleLoader__.load({
468
468
  return url.replace(/([?&]rev=)[^&#]*/, `$1${encodeURIComponent(rev)}`);
469
469
  }
470
470
  const CLIENT_CHUNK = /^client\.[A-Za-z0-9][A-Za-z0-9._-]*\.js$/;
471
+ /** The message of a thrown value: an Error's message, anything else stringified. */
472
+ function describeError(error) {
473
+ return error instanceof Error ? error.message : String(error);
474
+ }
471
475
  /** Internal module-table key for one package-local chunk. */
472
476
  function chunkId(ownerId, fileName) {
473
477
  return `${ownerId}/${fileName}`;
@@ -517,6 +521,12 @@ window.__ModuleLoader__.load({
517
521
  materializing = /* @__PURE__ */ new Set();
518
522
  graphRows = /* @__PURE__ */ new Map();
519
523
  loadBundle;
524
+ /** Last import or prefetch failure per graph row, cleared by a later success or invalidation. */
525
+ importErrors = /* @__PURE__ */ new Map();
526
+ /** Batch URLs whose transport or execution already failed; rows still missing from them go straight to their one-resource URL. */
527
+ failedBundleUrls = /* @__PURE__ */ new Set();
528
+ /** Every URL whose script has executed once; a batch among them is never requested again. */
529
+ executedBundleUrls = /* @__PURE__ */ new Set();
520
530
  /**
521
531
  * Build the module system over the parsed boot rows.
522
532
  * @param options - Parsed graph, platform seed, bootstrap module, registration facade, and transport.
@@ -569,23 +579,66 @@ window.__ModuleLoader__.load({
569
579
  rev: this.reloadTargets.get(ownerId)?.rev ?? this.graphRows.get(ownerId)?.rev
570
580
  });
571
581
  }
572
- /** Load one graph row so its factory is registered (idempotent per in-flight arrival). */
573
- arrive(row) {
574
- const { id } = row;
575
- if (this.loadCache.has(id) || this.factories.has(id)) return Promise.resolve();
576
- const reload = this.reloadTargets.get(id);
577
- const url = reload?.url ?? row.initialUrl;
582
+ /** Run one bundle transport per URL; every row waiting on the same URL shares the in-flight request. */
583
+ loadShared(url) {
578
584
  let transport = this.pendingArrival.get(url);
579
585
  if (transport === void 0) {
580
- transport = this.loadBundle(url).finally(() => {
586
+ transport = this.loadBundle(url).then(() => {
587
+ this.executedBundleUrls.add(url);
588
+ }).finally(() => {
581
589
  this.pendingArrival.delete(url);
582
590
  });
583
591
  this.pendingArrival.set(url, transport);
584
592
  }
585
- return transport.then(() => {
586
- if (!this.factories.has(id)) throw new Error(`client-modules: bundle ${url} loaded without registering "${id}" via __ModuleLoader__.load`);
587
- if (reload !== void 0 && this.reloadTargets.get(id) === reload) this.reloadTargets.delete(id);
588
- });
593
+ return transport;
594
+ }
595
+ /**
596
+ * Load one graph row so its factory is registered (idempotent per in-flight
597
+ * arrival). A batch script is one classic script that registers every
598
+ * package in sequence, and {@link register} rejects a second registration,
599
+ * so the two failure kinds differ: a transport failure (`error` event, nothing
600
+ * executed) is retried once on the same URL; a script that loaded without
601
+ * registering this row (a parse error registered nothing, or a runtime throw
602
+ * stopped it after registering others) is never re-executed, because a replay
603
+ * would stop again at the first duplicate registration; that holds even when
604
+ * the row that first imports from the batch is one it did register, because
605
+ * every executed batch URL is remembered. Either way the row then falls back
606
+ * to its own one-resource URL, which the Host serves for every package, so one
607
+ * failed batch costs at most three requests per missing row and never fails
608
+ * the rows that were registered.
609
+ */
610
+ async arrive(row) {
611
+ const { id } = row;
612
+ if (this.loadCache.has(id) || this.factories.has(id)) return;
613
+ const reload = this.reloadTargets.get(id);
614
+ const preferred = reload?.url ?? row.initialUrl;
615
+ const fallback = reload === void 0 && row.url !== preferred ? row.url : void 0;
616
+ const failures = [];
617
+ const attempt = async (url) => {
618
+ try {
619
+ await this.loadShared(url);
620
+ } catch (error) {
621
+ failures.push(`${url}: ${describeError(error)}`);
622
+ return "transport-failed";
623
+ }
624
+ if (this.factories.has(id)) return "registered";
625
+ failures.push(`${url}: loaded without registering "${id}" via __ModuleLoader__.load`);
626
+ return "not-registered";
627
+ };
628
+ let outcome = "transport-failed";
629
+ if (this.failedBundleUrls.has(preferred)) failures.push(`${preferred}: skipped after an earlier failure of this bundle`);
630
+ else if (fallback !== void 0 && this.executedBundleUrls.has(preferred)) {
631
+ failures.push(`${preferred}: already executed without registering "${id}"`);
632
+ outcome = "not-registered";
633
+ this.failedBundleUrls.add(preferred);
634
+ } else {
635
+ outcome = await attempt(preferred);
636
+ if (outcome === "transport-failed") outcome = await attempt(preferred);
637
+ if (outcome !== "registered" && fallback !== void 0) this.failedBundleUrls.add(preferred);
638
+ }
639
+ if (outcome !== "registered" && fallback !== void 0) outcome = await attempt(fallback);
640
+ if (outcome !== "registered") throw new Error(`client-modules: could not load "${id}": ${failures.join("; ")}`);
641
+ if (reload !== void 0 && this.reloadTargets.get(id) === reload) this.reloadTargets.delete(id);
589
642
  }
590
643
  /** Register each injected package and unresolved dynamic request before its consumer. */
591
644
  async arriveGraphRow(row, open = [], visited = /* @__PURE__ */ new Set()) {
@@ -598,14 +651,22 @@ window.__ModuleLoader__.load({
598
651
  const id = stripClientSuffix(request);
599
652
  if (this.seed.has(request) || this.loadCache.has(id)) continue;
600
653
  const dependency = this.graphRows.get(id);
601
- if (dependency !== void 0) await this.arriveGraphRow(dependency, next, visited);
654
+ if (dependency !== void 0) await this.arriveDependency(row.id, dependency, next, visited);
602
655
  }
603
656
  for (const packageName of row.inject) {
604
657
  const dependency = this.graphRows.get(packageName);
605
- if (dependency !== void 0) await this.arriveGraphRow(dependency, [], visited);
658
+ if (dependency !== void 0) await this.arriveDependency(row.id, dependency, [], visited);
606
659
  }
607
660
  await this.arrive(row);
608
661
  }
662
+ /** Arrive one dependency, naming the consumer it failed for so a cascade reads as a chain, not as 44 unrelated failures. */
663
+ async arriveDependency(consumerId, dependency, open, visited) {
664
+ try {
665
+ await this.arriveGraphRow(dependency, open, visited);
666
+ } catch (error) {
667
+ throw new Error(`client-modules: "${consumerId}" not loaded because dependency "${dependency.id}" failed: ${describeError(error)}`, { cause: error });
668
+ }
669
+ }
609
670
  /** Materialize a registered factory (synchronous; memoized in loadCache). */
610
671
  materialize(id, ownerId = id) {
611
672
  const existing = this.loadCache.get(id);
@@ -685,16 +746,40 @@ window.__ModuleLoader__.load({
685
746
  const existing = this.loadCache.get(id);
686
747
  if (existing !== void 0) return existing.exports;
687
748
  const row = this.graphRows.get(id);
688
- if (row !== void 0) await this.arriveGraphRow(row);
689
- else if (!this.factories.has(id)) throw new Error(`client-modules: cannot resolve "${specifier}" — not a seed word, not a materialized module, and not a row in the boot graph (the runtime mirror of the bundle purity gate)`);
690
- return this.materialize(id).exports;
749
+ if (row === void 0) {
750
+ if (this.factories.has(id)) return this.materialize(id).exports;
751
+ throw new Error(`client-modules: cannot resolve "${specifier}" — not a seed word, not a materialized module, and not a row in the boot graph (the runtime mirror of the bundle purity gate)`);
752
+ }
753
+ return this.recordingImportError(id, async () => {
754
+ await this.arriveGraphRow(row);
755
+ return this.materialize(id).exports;
756
+ });
691
757
  }
692
758
  async prefetch(id) {
693
759
  const normalized = stripClientSuffix(id);
694
760
  if (this.loadCache.has(normalized)) return;
695
761
  const row = this.graphRows.get(normalized);
696
762
  if (row === void 0) throw new Error(`client-modules: prefetch("${id}") — not a graph entry`);
697
- await this.arriveGraphRow(row);
763
+ await this.recordingImportError(normalized, () => this.arriveGraphRow(row));
764
+ }
765
+ importError(id) {
766
+ return this.importErrors.get(stripClientSuffix(id));
767
+ }
768
+ /**
769
+ * Run one graph-row operation, recording its failure for the boot audit and
770
+ * clearing the record on success. Arrival and materialization both run in
771
+ * here, so a factory that throws is recorded as well as a bundle that never
772
+ * arrived; the Loader only sees a missing fiber either way.
773
+ */
774
+ async recordingImportError(id, operation) {
775
+ try {
776
+ const result = await operation();
777
+ this.importErrors.delete(id);
778
+ return result;
779
+ } catch (error) {
780
+ this.importErrors.set(id, error instanceof Error ? error : new Error(describeError(error)));
781
+ throw error;
782
+ }
698
783
  }
699
784
  /** Refresh descriptors and unowned factory revisions before any entry imports its dependencies. */
700
785
  updateManifest(manifest, managed) {
@@ -739,6 +824,7 @@ window.__ModuleLoader__.load({
739
824
  invalidate(id, rev) {
740
825
  const normalized = stripClientSuffix(id);
741
826
  if (this.bootstrapIds.has(normalized)) return;
827
+ this.importErrors.delete(normalized);
742
828
  this.generations.set(normalized, (this.generations.get(normalized) ?? 0) + 1);
743
829
  const row = this.graphRows.get(normalized);
744
830
  if (row !== void 0) {
@@ -92,7 +92,7 @@ export interface WebBootGraph {
92
92
  export interface BootModuleRow {
93
93
  /** Entry name == package name (module-table key). */
94
94
  id: string;
95
- /** Revisioned single-resource combo reference used after HMR invalidation. */
95
+ /** Revisioned single-resource combo reference: the fallback when the row's batch fails and the reload target after HMR invalidation. */
96
96
  url: string;
97
97
  /** Content-addressed combo reference used before the first HMR invalidation. */
98
98
  initialUrl: string;
@@ -266,6 +266,15 @@ export interface ClientModuleLoader {
266
266
  * @param id - graph entry name.
267
267
  */
268
268
  prefetch(id: string): Promise<void>;
269
+ /**
270
+ * The last failure of {@link import} or {@link prefetch} for one graph row:
271
+ * transport, registration, dependency cascade, or factory execution. Cleared
272
+ * by a later success and by {@link invalidate}. The boot audit reads it to
273
+ * report why a Loader entry has no fiber.
274
+ * @param id - graph entry name.
275
+ * @returns the recorded failure, or `undefined` when the row never failed or succeeded since.
276
+ */
277
+ importError(id: string): Error | undefined;
269
278
  /**
270
279
  * Full reset of one non-bootstrap package: drop its entry and chunk factories
271
280
  * and materialized records so the next prefetch/import loads its one-resource
@@ -25,6 +25,12 @@ export declare class ClientModuleSystem implements ClientModuleLoader {
25
25
  private readonly materializing;
26
26
  private readonly graphRows;
27
27
  private readonly loadBundle;
28
+ /** Last import or prefetch failure per graph row, cleared by a later success or invalidation. */
29
+ private readonly importErrors;
30
+ /** Batch URLs whose transport or execution already failed; rows still missing from them go straight to their one-resource URL. */
31
+ private readonly failedBundleUrls;
32
+ /** Every URL whose script has executed once; a batch among them is never requested again. */
33
+ private readonly executedBundleUrls;
28
34
  /**
29
35
  * Build the module system over the parsed boot rows.
30
36
  * @param options - Parsed graph, platform seed, bootstrap module, registration facade, and transport.
@@ -32,10 +38,28 @@ export declare class ClientModuleSystem implements ClientModuleLoader {
32
38
  constructor(options: ClientModuleSystemOptions);
33
39
  /** Register one bundle factory, rejecting a script that executes twice without invalidation. */
34
40
  private register;
35
- /** Load one graph row so its factory is registered (idempotent per in-flight arrival). */
41
+ /** Run one bundle transport per URL; every row waiting on the same URL shares the in-flight request. */
42
+ private loadShared;
43
+ /**
44
+ * Load one graph row so its factory is registered (idempotent per in-flight
45
+ * arrival). A batch script is one classic script that registers every
46
+ * package in sequence, and {@link register} rejects a second registration,
47
+ * so the two failure kinds differ: a transport failure (`error` event, nothing
48
+ * executed) is retried once on the same URL; a script that loaded without
49
+ * registering this row (a parse error registered nothing, or a runtime throw
50
+ * stopped it after registering others) is never re-executed, because a replay
51
+ * would stop again at the first duplicate registration; that holds even when
52
+ * the row that first imports from the batch is one it did register, because
53
+ * every executed batch URL is remembered. Either way the row then falls back
54
+ * to its own one-resource URL, which the Host serves for every package, so one
55
+ * failed batch costs at most three requests per missing row and never fails
56
+ * the rows that were registered.
57
+ */
36
58
  private arrive;
37
59
  /** Register each injected package and unresolved dynamic request before its consumer. */
38
60
  private arriveGraphRow;
61
+ /** Arrive one dependency, naming the consumer it failed for so a cascade reads as a chain, not as 44 unrelated failures. */
62
+ private arriveDependency;
39
63
  /** Materialize a registered factory (synchronous; memoized in loadCache). */
40
64
  private materialize;
41
65
  /** Build the synchronous module-table require and its asynchronous chunk operation. */
@@ -44,6 +68,14 @@ export declare class ClientModuleSystem implements ClientModuleLoader {
44
68
  private importChunk;
45
69
  import(specifier: string): Promise<unknown>;
46
70
  prefetch(id: string): Promise<void>;
71
+ importError(id: string): Error | undefined;
72
+ /**
73
+ * Run one graph-row operation, recording its failure for the boot audit and
74
+ * clearing the record on success. Arrival and materialization both run in
75
+ * here, so a factory that throws is recorded as well as a bundle that never
76
+ * arrived; the Loader only sees a missing fiber either way.
77
+ */
78
+ private recordingImportError;
47
79
  /** Refresh descriptors and unowned factory revisions before any entry imports its dependencies. */
48
80
  private updateManifest;
49
81
  /** Retain live Loader modules and their transitive requests before evicting unreferenced graph records. */
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.7-alpha.1",
4
+ "version": "0.1.7-alpha.2",
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/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"
41
+ "@deepseek-ai/cordis-plugin-loader": "~1.0.5",
42
+ "@deepseek-ai/dsh-host-webserver": "0.1.7-alpha.2",
43
+ "@deepseek-ai/cordis": "~4.0.4",
44
+ "@deepseek-ai/dsh-package-manifest": "0.1.7-alpha.2",
45
+ "@deepseek-ai/dsh-client-store": "0.1.7-alpha.2",
46
+ "@deepseek-ai/dsh-invariants": "0.1.7-alpha.2"
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.3"
55
+ "@deepseek-ai/cordis": "~4.0.4"
56
56
  },
57
57
  "scripts": {
58
58
  "bundle": "tsdown",