@wcstack/state 1.31.0 → 1.32.0

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/dist/index.esm.js CHANGED
@@ -1,3 +1,60 @@
1
+ // ===========================================================================
2
+ // AUTO-GENERATED FILE - DO NOT EDIT.
3
+ // Generated from /protocol/ssr-snapshot.ts by scripts/sync-protocol-types.mjs.
4
+ // Run `node scripts/sync-protocol-types.mjs` after editing the source.
5
+ // ===========================================================================
6
+ // ssr-snapshot protocol — how the SSR renderer asks whoever owns reactive
7
+ // state to build hydration snapshots (<wcs-ssr>) as a final pass, after every
8
+ // DOM inserter (router route content, late custom elements) has settled.
9
+ //
10
+ // Without this, the snapshot is built inside <wcs-state>'s connectedCallback
11
+ // and races DOM inserted by other packages: whether a route's structural
12
+ // templates make it into the snapshot depends on document order and state's
13
+ // load mechanism (docs/ssr-router-design.md §5).
14
+ //
15
+ // The provider (@wcstack/state) installs itself on a well-known global symbol
16
+ // at bootstrap. The renderer (@wcstack/server) looks the builder up after
17
+ // running bootstraps: if present it announces orchestration by setting
18
+ // `data-wcs-server="orchestrated"` on the document element BEFORE parsing, and
19
+ // calls build() right before serialization. The provider keeps its inline
20
+ // per-element fallback whenever the attribute value is anything else, so:
21
+ // - old renderer + new provider -> inline build, yesterday's behavior
22
+ // - new renderer + old provider -> no builder found, attribute stays "",
23
+ // the old provider builds inline as before
24
+ // - new renderer + new provider -> orchestrated: snapshots are built last
25
+ // and therefore always see settled DOM
26
+ //
27
+ // The symbol (rather than a package import) also pins the builder to the state
28
+ // copy that actually runs on the page — its module-scoped fragment registries
29
+ // are the ones the snapshot must read.
30
+ //
31
+ // SINGLE SOURCE OF TRUTH: edit only this file (/protocol/ssr-snapshot.ts), then
32
+ // run `node scripts/sync-protocol-types.mjs` to regenerate the per-package
33
+ // copies (packages/<pkg>/src/protocol/ssrSnapshot.ts). Those copies are
34
+ // generated — do not edit them.
35
+ /**
36
+ * Global key the snapshot builder installs itself under. `Symbol.for` so
37
+ * independently loaded copies of this file (state's and server's) still agree.
38
+ */
39
+ const SSR_SNAPSHOT_BUILDER_KEY = Symbol.for("wcstack.ssr.snapshotBuilder");
40
+ /**
41
+ * `data-wcs-server` attribute value announcing that the renderer will call the
42
+ * builder as a final pass. Providers must skip their inline per-element build
43
+ * when they see this value, and keep it for any other value (including "").
44
+ */
45
+ const SSR_ORCHESTRATED_VALUE = "orchestrated";
46
+
47
+ /**
48
+ * サーバー主導スナップショット(orchestrated)の判定
49
+ * (docs/ssr-router-design.md §5)。renderToString が snapshot builder を
50
+ * 見つけたときだけ `data-wcs-server="orchestrated"` を宣言する — 値が他の
51
+ * もの(旧 server の "" を含む)なら inline 生成が従来どおり働く。
52
+ * inSsr と同じ理由でキャッシュしない。
53
+ */
54
+ function isOrchestratedSsr() {
55
+ const html = document.documentElement;
56
+ return html ? html.getAttribute('data-wcs-server') === SSR_ORCHESTRATED_VALUE : false;
57
+ }
1
58
  function inSsr() {
2
59
  // キャッシュしない: SSR モードはプロセスの属性ではなく「現在の document」の
3
60
  // 属性。@wcstack/server はグローバル document を差し替えてサーバーレンダリング
@@ -1142,16 +1199,33 @@ const fix = (options) => {
1142
1199
  /**
1143
1200
  * Locale number filter - formats number according to locale.
1144
1201
  *
1202
+ * ロケール依存フィルタ(`locale` / `date` / `time` / `datetime`)は
1203
+ * **明示引数だけを構築時に確定し、既定の `config.locale` は適用のたびに読む**。
1204
+ *
1205
+ * 以前は `options?.[0] ?? config.locale` を返り値の関数の**外**で解決していた。
1206
+ * フィルタ関数はバインド構築時に一度だけ作られるので、これはロケールを
1207
+ * クロージャに焼き込むことを意味する。`config.locale` の確定がバインド構築より
1208
+ * 遅れると、それ以降どう直しても「同じページの中で日付だけ既定ロケール」が
1209
+ * 永続し、しかも `config.locale` は依存グラフに載らないので再描画で回復もしない。
1210
+ * 症状(日付だけ英語)は原因(起動順序)から遠く、追いにくい。
1211
+ *
1212
+ * 適用のたびに読めば、少なくとも**再適用されたバインドは回復する**。ロケールは
1213
+ * 起動時に確定する前提(docs/i18n-design.md D1)なので通常この差は現れず、
1214
+ * これは順序事故から復帰できるようにするための保険である。
1215
+ *
1216
+ * 明示引数(`|date(ja-JP)`)は構築時に固定でよい — バインド式の一部であり、
1217
+ * 実行中に変わらない。
1218
+ *
1145
1219
  * @param options - Array with locale string as first element (default: config.locale)
1146
1220
  * @returns Filter function that returns localized number string
1147
1221
  */
1148
1222
  const locale = (options) => {
1149
- const opt = options?.[0] ?? config.locale;
1223
+ const explicit = options?.[0];
1150
1224
  return (value) => {
1151
1225
  if (typeof value !== 'number') {
1152
1226
  valueMustBeNumber('locale');
1153
1227
  }
1154
- return value.toLocaleString(opt);
1228
+ return value.toLocaleString(explicit ?? config.locale);
1155
1229
  };
1156
1230
  };
1157
1231
  /**
@@ -1464,12 +1538,13 @@ const truncate = (options) => {
1464
1538
  * @returns Filter function that returns date string
1465
1539
  */
1466
1540
  const date = (options) => {
1467
- const opt = options?.[0] ?? config.locale;
1541
+ // 既定ロケールは適用のたびに読む(`locale` フィルタの注記を参照)
1542
+ const explicit = options?.[0];
1468
1543
  return (value) => {
1469
1544
  if (!(value instanceof Date)) {
1470
1545
  valueMustBeDate('date');
1471
1546
  }
1472
- return value.toLocaleDateString(opt);
1547
+ return value.toLocaleDateString(explicit ?? config.locale);
1473
1548
  };
1474
1549
  };
1475
1550
  /**
@@ -1479,12 +1554,13 @@ const date = (options) => {
1479
1554
  * @returns Filter function that returns time string
1480
1555
  */
1481
1556
  const time = (options) => {
1482
- const opt = options?.[0] ?? config.locale;
1557
+ // 既定ロケールは適用のたびに読む(`locale` フィルタの注記を参照)
1558
+ const explicit = options?.[0];
1483
1559
  return (value) => {
1484
1560
  if (!(value instanceof Date)) {
1485
1561
  valueMustBeDate('time');
1486
1562
  }
1487
- return value.toLocaleTimeString(opt);
1563
+ return value.toLocaleTimeString(explicit ?? config.locale);
1488
1564
  };
1489
1565
  };
1490
1566
  /**
@@ -1494,12 +1570,13 @@ const time = (options) => {
1494
1570
  * @returns Filter function that returns datetime string
1495
1571
  */
1496
1572
  const datetime = (options) => {
1497
- const opt = options?.[0] ?? config.locale;
1573
+ // 既定ロケールは適用のたびに読む(`locale` フィルタの注記を参照)
1574
+ const explicit = options?.[0];
1498
1575
  return (value) => {
1499
1576
  if (!(value instanceof Date)) {
1500
1577
  valueMustBeDate('datetime');
1501
1578
  }
1502
- return value.toLocaleString(opt);
1579
+ return value.toLocaleString(explicit ?? config.locale);
1503
1580
  };
1504
1581
  };
1505
1582
  /**
@@ -4568,6 +4645,16 @@ function addInterestedSession(node, session) {
4568
4645
  }
4569
4646
  interestedSessionsByNode.set(node, new Set([current, session]));
4570
4647
  }
4648
+ /**
4649
+ * このノードに既にバインドが張られているか。
4650
+ *
4651
+ * binder プロトコル(`bind()`)の冪等判定に使う。`remember` が binding ごとに
4652
+ * `addInterestedSession(binding.replaceNode, …)` を呼ぶので、バインド済みノードは
4653
+ * 必ずこの台帳に載っている。新しい台帳を足さずに済むぶん、二重管理の齟齬が無い。
4654
+ */
4655
+ function hasInterestedSession(node) {
4656
+ return interestedSessionsByNode.has(node);
4657
+ }
4571
4658
  function forEachInterestedSession(node, callback) {
4572
4659
  const current = interestedSessionsByNode.get(node);
4573
4660
  if (typeof current === "undefined")
@@ -6452,8 +6539,23 @@ class Content {
6452
6539
  let anchor = targetNode;
6453
6540
  for (const node of this._movableNodes()) {
6454
6541
  if (anchor.nextSibling !== node) {
6542
+ // moveBefore も childList mutation record を出すため、マークは両分岐の前
6455
6543
  markObserverSkipOnAdd(node);
6456
- parentNode.insertBefore(node, anchor.nextSibling);
6544
+ // moveBefore は取り外しを伴わない移動 — 接続済み行の reorder で
6545
+ // フォーカス・iframe・アニメーション状態を保存する(docs/a11y-design.md §4-1)。
6546
+ // この 1 文は 4 つのノード状態を共有する: (a) 接続済み reorder、
6547
+ // (b) clone フラグメント由来(root 違い)、(c) プール/unmount 済み(親なし)、
6548
+ // (d) バッチフラグメント内。moveBefore は「同一ツリー・親あり」を要求し
6549
+ // (b)(c)(d) では HierarchyRequestError を投げるため、same-parent ガード
6550
+ // (同 root かつ親が非 null の同時証明 = フォーカス保存が意味を持つ (a) と
6551
+ // 正確に一致)は外せない。ガードを外す「簡略化」をしてはならない。
6552
+ const mover = parentNode;
6553
+ if (node.parentNode === parentNode && typeof mover.moveBefore === "function") {
6554
+ mover.moveBefore(node, anchor.nextSibling);
6555
+ }
6556
+ else {
6557
+ parentNode.insertBefore(node, anchor.nextSibling);
6558
+ }
6457
6559
  }
6458
6560
  anchor = node;
6459
6561
  }
@@ -6665,6 +6767,157 @@ function createContent(bindingInfo) {
6665
6767
  return content;
6666
6768
  }
6667
6769
 
6770
+ // ===========================================================================
6771
+ // AUTO-GENERATED FILE - DO NOT EDIT.
6772
+ // Generated from /protocol/transition-runner.ts by scripts/sync-protocol-types.mjs.
6773
+ // Run `node scripts/sync-protocol-types.mjs` after editing the source.
6774
+ // ===========================================================================
6775
+ // transition-runner protocol — how a package that mutates the DOM hands that
6776
+ // mutation to whoever is arbitrating view transitions on the page.
6777
+ //
6778
+ // @wcstack/state and @wcstack/router must not depend on @wcstack/view-transition
6779
+ // (zero runtime dependencies, independently publishable), so the arbiter installs
6780
+ // itself on a well-known global symbol and the participants look it up lazily.
6781
+ // No arbiter installed means the mutation is invoked directly, synchronously —
6782
+ // byte-for-byte the behavior these packages had before the protocol existed.
6783
+ //
6784
+ // docs/view-transition-design.md §4 is the normative description.
6785
+ //
6786
+ // SINGLE SOURCE OF TRUTH: edit only this file (/protocol/transition-runner.ts), then run
6787
+ // `node scripts/sync-protocol-types.mjs` to regenerate the per-package copies
6788
+ // (packages/<pkg>/src/protocol/transitionRunner.ts). Those copies are generated — do not edit them.
6789
+ /**
6790
+ * Global key the arbiter installs itself under. `Symbol.for` so independently
6791
+ * loaded copies of this file (two CDN bundles on one page) still agree.
6792
+ */
6793
+ const TRANSITION_RUNNER_KEY = Symbol.for("wcstack.transition-runner");
6794
+ /**
6795
+ * The installed arbiter, or null when there is none, it speaks a version this
6796
+ * reader does not, or it does not accept this participant.
6797
+ *
6798
+ * Looked up on every call rather than cached: the tag can be added, removed, or
6799
+ * reconfigured at any point in a page's life, and a stale cache would either
6800
+ * animate what the author just switched off or miss what they switched on.
6801
+ */
6802
+ function getTransitionRunner(source) {
6803
+ const candidate = globalThis[TRANSITION_RUNNER_KEY];
6804
+ if (candidate === undefined || candidate === null)
6805
+ return null;
6806
+ if (candidate.protocol !== "wcs-transition-runner")
6807
+ return null;
6808
+ if (typeof candidate.version !== "number" || candidate.version < 1)
6809
+ return null;
6810
+ if (typeof candidate.run !== "function")
6811
+ return null;
6812
+ if (typeof candidate.accepts !== "function" || !candidate.accepts(source))
6813
+ return null;
6814
+ return candidate;
6815
+ }
6816
+ /**
6817
+ * Run `mutate` under the installed arbiter, or directly when there is none.
6818
+ *
6819
+ * Returns `undefined` in the no-arbiter case instead of a resolved promise: the
6820
+ * state drain calls this on every batch, and awaiting is a caller's choice, not
6821
+ * an allocation the common path should pay for. `await` accepts both.
6822
+ */
6823
+ function runTransition(source, mutate, types) {
6824
+ const runner = getTransitionRunner(source);
6825
+ if (runner === null) {
6826
+ mutate();
6827
+ return undefined;
6828
+ }
6829
+ return runner.run(mutate, { source, types });
6830
+ }
6831
+
6832
+ /** Elements that already carry a generated name (never renamed). */
6833
+ const namedElements = new WeakSet();
6834
+ /**
6835
+ * The generated-name ledger is per *document*, not per module instance.
6836
+ *
6837
+ * `view-transition-name` has to be unique across the whole document: the moment
6838
+ * two elements share one, the browser aborts the transition outright. A
6839
+ * module-scope counter breaks that as soon as `@wcstack/state` is loaded twice on
6840
+ * one page (two CDN bundles), because both copies would start minting
6841
+ * `wcs-row-1`. The transition-runner key is a `Symbol.for` for exactly this
6842
+ * reason, and the counter needs the same protection.
6843
+ *
6844
+ * Sharing the cap is right for the same reason: the cost a cap exists to bound —
6845
+ * one snapshot group per named element — is a document-wide cost, not a
6846
+ * per-bundle one.
6847
+ */
6848
+ const NAMING_LEDGER_KEY = Symbol.for("wcstack.state.view-transition-naming");
6849
+ function getLedger() {
6850
+ const slot = globalThis;
6851
+ return (slot[NAMING_LEDGER_KEY] ??= { counter: 0, assigned: 0, warned: false });
6852
+ }
6853
+ /**
6854
+ * The active auto-naming policy, or null when names are the author's business
6855
+ * (the default) — one arbiter lookup per structural apply, not per row.
6856
+ */
6857
+ function getAutoNaming() {
6858
+ const runner = getTransitionRunner("state");
6859
+ if (runner === null || runner.naming !== "auto") {
6860
+ return null;
6861
+ }
6862
+ return { limit: runner.namingLimit };
6863
+ }
6864
+ function firstElementOf(content) {
6865
+ const first = content.firstNode;
6866
+ if (first === null) {
6867
+ return null;
6868
+ }
6869
+ const last = content.lastNode;
6870
+ for (let node = first; node !== null; node = node.nextSibling) {
6871
+ if (node.nodeType === Node.ELEMENT_NODE) {
6872
+ return node;
6873
+ }
6874
+ if (node === last) {
6875
+ break;
6876
+ }
6877
+ }
6878
+ return null;
6879
+ }
6880
+ /**
6881
+ * Give this content's first element a unique name plus a class for group
6882
+ * styling, unless it already has one or the cap has been reached.
6883
+ *
6884
+ * The cap exists because every named element becomes its own snapshot group; a
6885
+ * few hundred of them make a transition visibly slow. Past it naming stops and
6886
+ * says so once — silently degrading would leave the author wondering why only
6887
+ * the first part of a list animates.
6888
+ */
6889
+ function applyTransitionName(content, kind, naming) {
6890
+ const element = firstElementOf(content);
6891
+ if (element === null || namedElements.has(element)) {
6892
+ return;
6893
+ }
6894
+ // A node without `style` (anything outside HTMLElement / SVGElement) cannot
6895
+ // carry a name. Bail before touching the ledger: consuming the cap and marking
6896
+ // the element as named would burn a slot for a name that was never written,
6897
+ // and leave that element permanently ineligible.
6898
+ const style = element.style;
6899
+ if (style === undefined) {
6900
+ return;
6901
+ }
6902
+ const ledger = getLedger();
6903
+ if (ledger.assigned >= naming.limit) {
6904
+ if (!ledger.warned) {
6905
+ ledger.warned = true;
6906
+ console.warn(`[@wcstack/state] auto view-transition-name limit (${naming.limit}) reached; ` +
6907
+ "further elements are left unnamed. Raise naming-limit on <wcs-view-transition>, " +
6908
+ 'or switch to naming="manual" and name only what should morph.');
6909
+ }
6910
+ return;
6911
+ }
6912
+ namedElements.add(element);
6913
+ ledger.assigned += 1;
6914
+ ledger.counter += 1;
6915
+ style.setProperty("view-transition-name", `wcs-${kind}-${ledger.counter}`);
6916
+ // Group handle for CSS (`::view-transition-group(*.wcs-row)`). Ignored by
6917
+ // engines that predate view-transition-class, which costs nothing.
6918
+ style.setProperty("view-transition-class", `wcs-${kind}`);
6919
+ }
6920
+
6668
6921
  const lastNodeByNode = new WeakMap();
6669
6922
  const contentByListIndexByNode = new WeakMap();
6670
6923
  const pooledContentsByNode = new WeakMap();
@@ -6832,6 +7085,10 @@ function applyChangeToFor(bindingInfo, context, newValue) {
6832
7085
  setRootNodeByFragment(fragment, context.rootNode);
6833
7086
  }
6834
7087
  const ssrMode = inSsr();
7088
+ // 自動命名ポリシーは行ごとではなく apply ごとに 1 回だけ引く
7089
+ // (docs/view-transition-design.md §6)。既定の manual では null で、
7090
+ // 以降の行ループは分岐 1 つ分しか増えない。
7091
+ const autoNaming = getAutoNaming();
6835
7092
  const uuid = bindingInfo.uuid ?? '';
6836
7093
  // 追加行ごとの WeakMap 解決を避けるためプール配列も 1 回だけ引く(プールの配列
6837
7094
  // 実体は setPooledContent が一度作ったら不変なので、delete ループ後の参照で安定)
@@ -6875,6 +7132,9 @@ function applyChangeToFor(bindingInfo, context, newValue) {
6875
7132
  }
6876
7133
  // コンテントを活性化
6877
7134
  activateContent(content, loopContext, context);
7135
+ if (autoNaming !== null) {
7136
+ applyTransitionName(content, "row", autoNaming);
7137
+ }
6878
7138
  });
6879
7139
  if (typeof content === 'undefined') {
6880
7140
  raiseError(`Content not found for ListIndex: ${index.index} at path "${listPathInfo.path}"`);
@@ -6974,6 +7234,11 @@ function applyChangeToIf(bindingInfo, context, rawNewValue) {
6974
7234
  }
6975
7235
  const loopContext = getLoopContextByNode(bindingInfo.node);
6976
7236
  activateContent(content, loopContext, context);
7237
+ // 自動命名(docs/view-transition-design.md §6)。manual(既定)では null。
7238
+ const autoNaming = getAutoNaming();
7239
+ if (autoNaming !== null) {
7240
+ applyTransitionName(content, "branch", autoNaming);
7241
+ }
6977
7242
  }
6978
7243
  }
6979
7244
 
@@ -7509,12 +7774,47 @@ function missingRootPathMessage(stateName, path, target, declaredPaths) {
7509
7774
  * 決まるので、噛み合わないことは常にプログラマのミス。
7510
7775
  */
7511
7776
  function indexArityMessage(api, path, wildcardCount, actual) {
7777
+ // `$getAll` / `$setAll` の添字は前方一致の接頭辞なので上限、`$resolve` だけが厳密一致
7778
+ // (docs/state-set-all-design.md §4)。
7512
7779
  const requirement = api === "$resolve"
7513
7780
  ? `exactly ${wildcardCount}`
7514
7781
  : `at most ${wildcardCount}`;
7515
7782
  return `[wcs/index-arity] ${api}("${path}") requires ${requirement} index(es) ` +
7516
7783
  `("*" appears ${wildcardCount} time(s) in the path) but got ${actual}.${LINT_HINT}`;
7517
7784
  }
7785
+ /**
7786
+ * `$getAll(path)`(添字省略)の既定値はループ文脈の添字 `[$1..$n]` だが、それを
7787
+ * 敷けるのは path と文脈がワイルドカード連鎖を共有している場合だけ。共有ゼロなのに
7788
+ * 文脈が添字を持っている場合、黙って全展開に倒すと「文脈で絞られている」という
7789
+ * 書き手の期待と食い違い、異なる文脈の添字の流用とも区別が付かないため throw する。
7790
+ *
7791
+ * 実行時の評価文脈に依存する(`$setAll` の spread 長と同種)ので lint へは誘導しない。
7792
+ */
7793
+ function getAllContextMismatchMessage(path, contextPath) {
7794
+ return `$getAll("${path}") was called without indexes inside the loop context of ` +
7795
+ `"${contextPath}", but the path shares no wildcard level with that context, ` +
7796
+ `so the context indexes ($1..$n) do not apply. ` +
7797
+ `Pass indexes explicitly ([] expands every level).`;
7798
+ }
7799
+ /**
7800
+ * `$setAll(path, indexes, values, { spread: true })` の配列長がマッチ件数と噛み合わない。
7801
+ *
7802
+ * 静的には件数が分からない(実行時のリスト長に依存する)ので lint へは誘導しない。
7803
+ * 黙って切り詰める/余りを捨てると誤配が通ってしまうため throw する
7804
+ * (docs/state-set-all-design.md §3-3)。
7805
+ */
7806
+ function setAllSpreadArityMessage(path, matched, actual) {
7807
+ return `$setAll("${path}", …, { spread: true }) requires the values array to have ` +
7808
+ `exactly one entry per matched address (matched ${matched}) but got ${actual}. ` +
7809
+ `Did the list change between $getAll and $setAll?`;
7810
+ }
7811
+ /**
7812
+ * `$setAll` の値と `options` の組み合わせが意味を成さない。
7813
+ * (docs/state-set-all-design.md §3-1)
7814
+ */
7815
+ function setAllValueKindMessage(path, reason) {
7816
+ return `$setAll("${path}") ${reason}`;
7817
+ }
7518
7818
  /**
7519
7819
  * ワイルドカードを解決するループ文脈が足りない(=パスの階数 > スコープの階数)。
7520
7820
  *
@@ -8453,7 +8753,7 @@ async function buildBindings(root) {
8453
8753
  }
8454
8754
  }
8455
8755
 
8456
- var version = "1.31.0";
8756
+ var version = "1.32.0";
8457
8757
  var pkg = {
8458
8758
  version: version};
8459
8759
 
@@ -9176,6 +9476,221 @@ async function hydrateBindings(root) {
9176
9476
  return true;
9177
9477
  }
9178
9478
 
9479
+ // ===========================================================================
9480
+ // AUTO-GENERATED FILE - DO NOT EDIT.
9481
+ // Generated from /protocol/binder.ts by scripts/sync-protocol-types.mjs.
9482
+ // Run `node scripts/sync-protocol-types.mjs` after editing the source.
9483
+ // ===========================================================================
9484
+ // binder protocol — how a package that inserts DOM hands those nodes to whoever
9485
+ // owns data bindings on the page.
9486
+ //
9487
+ // The dual of transition-runner: that one hands a *mutation* to whoever animates
9488
+ // it, this one hands *new nodes* to whoever binds them.
9489
+ //
9490
+ // A `data-wcs` binding exists only for nodes @wcstack/state walked when it built
9491
+ // its bindings. Nodes that arrive later — the content of a route that was not
9492
+ // active at that moment, a <wcs-head> child reflected into <head> — were never
9493
+ // walked, so their bindings silently do nothing, however often they are inserted.
9494
+ // @wcstack/router must not depend on @wcstack/state (zero runtime dependencies,
9495
+ // independently publishable), so state installs a binder on a well-known global
9496
+ // symbol and inserters look it up lazily.
9497
+ //
9498
+ // No binder installed means nothing happens — byte-for-byte the behavior these
9499
+ // packages had before the protocol existed.
9500
+ //
9501
+ // docs/binder-protocol-design.md is the normative description.
9502
+ //
9503
+ // SINGLE SOURCE OF TRUTH: edit only this file (/protocol/binder.ts), then run
9504
+ // `node scripts/sync-protocol-types.mjs` to regenerate the per-package copies
9505
+ // (packages/<pkg>/src/protocol/binder.ts). Those copies are generated — do not edit them.
9506
+ /**
9507
+ * Global key the binder installs itself under. `Symbol.for` so independently
9508
+ * loaded copies of this file (two CDN bundles on one page) still agree.
9509
+ */
9510
+ const BINDER_KEY = Symbol.for("wcstack.binder");
9511
+ /**
9512
+ * The installed binder, or null when there is none or it speaks a version this
9513
+ * reader does not.
9514
+ *
9515
+ * Looked up on every call rather than cached, for the same reason
9516
+ * transition-runner does: the page's composition can change at any point, and a
9517
+ * stale cache would keep calling into a binder that is no longer there.
9518
+ */
9519
+ function getBinder() {
9520
+ const candidate = globalThis[BINDER_KEY];
9521
+ if (candidate === undefined || candidate === null)
9522
+ return null;
9523
+ if (candidate.protocol !== "wcs-binder")
9524
+ return null;
9525
+ if (typeof candidate.version !== "number" || candidate.version < 1)
9526
+ return null;
9527
+ if (typeof candidate.bind !== "function")
9528
+ return null;
9529
+ return candidate;
9530
+ }
9531
+ /**
9532
+ * Subtrees offered before a binder existed, and the set of everything a binder
9533
+ * has taken. Both live on global symbols so that independently loaded copies of
9534
+ * this file — the router's and state's — share one queue.
9535
+ *
9536
+ * The queue is needed because of load order: the router's auto bundle runs
9537
+ * before state's, so `<wcs-head>` reflects its children into `<head>` while
9538
+ * there is still nothing to bind them. Offering them to a binder that arrives
9539
+ * later is the difference between working and silently blank.
9540
+ */
9541
+ const PENDING_KEY = Symbol.for("wcstack.binder.pending");
9542
+ const TAKEN_KEY = Symbol.for("wcstack.binder.taken");
9543
+ function pendingQueue() {
9544
+ const globals = globalThis;
9545
+ let queue = globals[PENDING_KEY];
9546
+ if (queue === undefined) {
9547
+ queue = [];
9548
+ globals[PENDING_KEY] = queue;
9549
+ }
9550
+ return queue;
9551
+ }
9552
+ function takenSet() {
9553
+ const globals = globalThis;
9554
+ let taken = globals[TAKEN_KEY];
9555
+ if (taken === undefined) {
9556
+ taken = new WeakSet();
9557
+ globals[TAKEN_KEY] = taken;
9558
+ }
9559
+ return taken;
9560
+ }
9561
+ /**
9562
+ * Bind everything offered before this binder existed. Called by the binder right
9563
+ * after it installs itself.
9564
+ */
9565
+ function flushPendingBinds() {
9566
+ const binder = getBinder();
9567
+ if (binder === null)
9568
+ return;
9569
+ const queue = pendingQueue();
9570
+ if (queue.length === 0)
9571
+ return;
9572
+ const pending = queue.splice(0, queue.length);
9573
+ const taken = takenSet();
9574
+ for (const subtree of pending) {
9575
+ taken.add(subtree);
9576
+ binder.bind(subtree);
9577
+ }
9578
+ }
9579
+
9580
+ /**
9581
+ * binder プロトコルの提供側(docs/binder-protocol-design.md)。
9582
+ *
9583
+ * `buildBindings` は起動時に `document.body` を 1 回走査するだけなので、そのとき
9584
+ * document に居なかったノードのバインドは存在しない。router が後から差し込む
9585
+ * ルート内容や `<wcs-head>` のクローンがこれに当たり、書いたバインドが黙って
9586
+ * 何もしない状態になっていた。`bind()` はその取りこぼしを 1 サブツリー分だけ
9587
+ * 埋める。
9588
+ *
9589
+ * **走査を勝手に広げない。** MutationObserver が見た全追加ノードを走査する形に
9590
+ * すると、バインドを 1 個も持たない挿入(大多数)にコストが乗り、さらに
9591
+ * `innerHTML` で入れた外部由来の DOM が `data-wcs` を発火させることになる。
9592
+ * ここで束ねるのは**明示的に渡されたものだけ**である。
9593
+ */
9594
+ const BIND_ATTRIBUTE_SELECTOR = () => `[${config.bindAttributeName}]`;
9595
+ /**
9596
+ * このサブツリーは既にバインド済みか。
9597
+ *
9598
+ * ルート内容は「起動時に active だったので全部バインド済み」か「一度も走査されて
9599
+ * いないので全部未バインド」のどちらかで、途中の状態を取らない。したがって
9600
+ * **宣言を持つ最初のノード 1 個**を見れば足りる。全ノードを走査して判定するのは
9601
+ * 同じ結論により高いコストを払うだけになる。
9602
+ */
9603
+ function alreadyBound(subtree) {
9604
+ if (hasInterestedSession(subtree)) {
9605
+ return true;
9606
+ }
9607
+ if (!isElement(subtree)) {
9608
+ return false;
9609
+ }
9610
+ if (subtree.hasAttribute(config.bindAttributeName)) {
9611
+ // 属性を持つのに台帳に居ない = 未バインド
9612
+ return false;
9613
+ }
9614
+ const first = subtree.querySelector(BIND_ATTRIBUTE_SELECTOR());
9615
+ return first !== null && hasInterestedSession(first);
9616
+ }
9617
+ function isElement(node) {
9618
+ return node.nodeType === 1;
9619
+ }
9620
+ function bindNow(subtree) {
9621
+ if (alreadyBound(subtree)) {
9622
+ return;
9623
+ }
9624
+ convertMustacheToComments(subtree);
9625
+ collectStructuralFragments(subtree.getRootNode(), subtree);
9626
+ // `getSubscriberNodes` の TreeWalker は**ルート自身を返さない**。`buildBindings` は
9627
+ // `document.body` を渡すので今まで問題にならなかったが、ここには宣言をルートに
9628
+ // 持つノードが来る(`<wcs-head>` が head へ入れる `<title data-wcs="…">`)。
9629
+ // そのときだけ親から走査して、ルートを走査範囲に含める。兄弟の重複登録は
9630
+ // `registeredNodeSet` が弾くので、余計なバインドは生まれない。
9631
+ // 親は Element とは限らない(ShadowRoot 直下なら DocumentFragment、head 直下なら
9632
+ // Element)。`parentElement` だと前者で null になり、ルートを含められない。
9633
+ const declaresOnRoot = subtree.hasAttribute(config.bindAttributeName);
9634
+ const parent = subtree.parentNode;
9635
+ const canWalkFromParent = parent !== null
9636
+ && (parent.nodeType === 1 || parent.nodeType === 9 || parent.nodeType === 11);
9637
+ const walkRoot = declaresOnRoot && canWalkFromParent
9638
+ ? parent
9639
+ : subtree;
9640
+ initializeBindings(walkRoot, null);
9641
+ }
9642
+ /**
9643
+ * 初期バインド構築より前に差し出されたサブツリー。
9644
+ *
9645
+ * `<wcs-head>` は `connectedCallback` の中でクローンを head へ入れるので、
9646
+ * state / router のどちらを先に読み込んでも「まだ構築が終わっていない」時点で
9647
+ * bind を求めてくる。そこで同期に束ねても `<wcs-state>` の登録が済んでおらず、
9648
+ * バインドは state を見つけられない。**構築の完了を唯一の合図にする。**
9649
+ */
9650
+ const beforeFirstBuild = [];
9651
+ function bind(subtree) {
9652
+ if (!isElement(subtree) || alreadyBound(subtree)) {
9653
+ return;
9654
+ }
9655
+ if (!areBindingsBuilt(subtree.getRootNode())) {
9656
+ beforeFirstBuild.push(subtree);
9657
+ return;
9658
+ }
9659
+ bindNow(subtree);
9660
+ }
9661
+ /**
9662
+ * 初期バインド構築の完了時に呼ぶ(stateElementByName.ts)。binder が居ない時点で
9663
+ * 差し出された分(プロトコルの保留キュー)と、居たが早すぎた分をまとめて束ねる。
9664
+ */
9665
+ function drainPendingBinds() {
9666
+ const pending = beforeFirstBuild.splice(0, beforeFirstBuild.length);
9667
+ for (const subtree of pending) {
9668
+ bindNow(subtree);
9669
+ }
9670
+ flushPendingBinds();
9671
+ }
9672
+ const binder = {
9673
+ protocol: "wcs-binder",
9674
+ version: 1,
9675
+ bind,
9676
+ };
9677
+ /**
9678
+ * グローバル symbol へ自分を載せる。`bootstrapState` から呼ぶ。
9679
+ *
9680
+ * 既に別のコピーが載っているなら譲る。1 ページに 2 つの state バンドルが載る構成
9681
+ * (CDN の取り違え)で、後から読まれた側が先客を追い出すと、先客がバインドした
9682
+ * ノードの台帳と食い違う。
9683
+ */
9684
+ function registerBinder() {
9685
+ const globals = globalThis;
9686
+ if (globals[BINDER_KEY] === undefined) {
9687
+ globals[BINDER_KEY] = binder;
9688
+ }
9689
+ // ここでは引き取らない。`<wcs-state>` の登録は connectedCallback の await より
9690
+ // 後なので、この時点ではまだ state が居ない。保留分は初期バインド構築の完了時に
9691
+ // 流す(stateElementByName.ts)。そこが「state が確実に居る」最初の瞬間である。
9692
+ }
9693
+
9179
9694
  const stateElementByNameByNode = new WeakMap();
9180
9695
  const bindingsReadyByNode = new WeakMap();
9181
9696
  // devtools 用の列挙可能な登録簿(protocol §4.1 — 唯一の常時 ON 台帳)。
@@ -9198,6 +9713,25 @@ function getStateElementByName(rootNode, name) {
9198
9713
  function getBindingsReady(rootNode) {
9199
9714
  return bindingsReadyByNode.get(rootNode) ?? Promise.resolve();
9200
9715
  }
9716
+ const bindingsBuiltRoots = new WeakSet();
9717
+ /**
9718
+ * この rootNode の初期バインド構築が完了しているか。
9719
+ *
9720
+ * binder プロトコル(`bind()`)が使う。router の `<wcs-head>` はクローンを
9721
+ * `connectedCallback` の中で head へ入れるので、**state が最初の走査を終える前**に
9722
+ * bind を求めてくる。そこで同期に束ねても state 要素の初期化が済んでおらず、
9723
+ * 結果は空のままになる。完了までは binder 側で保留する。
9724
+ *
9725
+ * 「まだ登録も済んでいない」と「もう構築が終わった」を取り違えないよう、判定は
9726
+ * 完了の側で持つ。<wcs-state> の登録は connectedCallback の await より後に起きるので、
9727
+ * 「エントリの有無」で進行中かを測ると読み込み順によって逆の答えを返す。
9728
+ */
9729
+ function areBindingsBuilt(rootNode) {
9730
+ return bindingsBuiltRoots.has(rootNode);
9731
+ }
9732
+ function markBindingsBuilt(rootNode) {
9733
+ bindingsBuiltRoots.add(rootNode);
9734
+ }
9201
9735
  function setStateElementByName(rootNode, name, element) {
9202
9736
  let stateElementByName = stateElementByNameByNode.get(rootNode);
9203
9737
  if (element === null) {
@@ -9250,6 +9784,11 @@ function setStateElementByName(rootNode, name, element) {
9250
9784
  else {
9251
9785
  await buildBindings(rootNode);
9252
9786
  }
9787
+ markBindingsBuilt(rootNode);
9788
+ // binder が居ない時点で差し出されたサブツリーを引き取る。ここが
9789
+ // 「state が確実に居る」最初の瞬間で、router の auto バンドルが
9790
+ // state のそれより先に走る順序を吸収できる唯一の場所である。
9791
+ drainPendingBinds();
9253
9792
  resolve();
9254
9793
  }
9255
9794
  catch (error) {
@@ -9264,6 +9803,11 @@ function setStateElementByName(rootNode, name, element) {
9264
9803
  queueMicrotask(async () => {
9265
9804
  try {
9266
9805
  await buildBindings(rootNode);
9806
+ markBindingsBuilt(rootNode);
9807
+ // binder が居ない時点で差し出されたサブツリーを引き取る。ここが
9808
+ // 「state が確実に居る」最初の瞬間で、router の auto バンドルが
9809
+ // state のそれより先に走る順序を吸収できる唯一の場所である。
9810
+ drainPendingBinds();
9267
9811
  resolve();
9268
9812
  }
9269
9813
  catch (error) {
@@ -9373,6 +9917,17 @@ function notifyUpdateBatchListeners(batch) {
9373
9917
  registered.listener(batch);
9374
9918
  }
9375
9919
  }
9920
+ /**
9921
+ * 遷移越しの適用が失敗したときの報告。
9922
+ *
9923
+ * 遷移の中では例外を同期的に呼び出し元へ投げ返せない。今日の drain は
9924
+ * queueMicrotask の中で throw する = uncaught として観測されるので、それと同じ
9925
+ * 「loud に出す」挙動へ揃える。握り潰すと `$updatedCallback` の throw が黙って
9926
+ * 消える(README の 3 層表が定める伝播の契約が破れる)。
9927
+ */
9928
+ function reportDeferredApplyFailure(error) {
9929
+ queueMicrotask(() => { throw error; });
9930
+ }
9376
9931
  class Updater {
9377
9932
  _queueUpdateRecords = [];
9378
9933
  constructor() {
@@ -9481,12 +10036,34 @@ class Updater {
9481
10036
  // 限られるが、そのとき drain フックまで道連れにすると「機構間の順序は固定」
9482
10037
  // (README の 3 層表)が黙って破れる。例外は握らない = 伝播は維持する。
9483
10038
  try {
9484
- // context が無い場合は従来どおり 1 引数で呼ぶ(呼び出し契約の互換維持)
9485
- if (propagationContextByBinding.size > 0) {
9486
- applyChangeFromBindings(processBindings, propagationContextByBinding);
10039
+ const applyBindings = () => {
10040
+ // context が無い場合は従来どおり 1 引数で呼ぶ(呼び出し契約の互換維持)
10041
+ if (propagationContextByBinding.size > 0) {
10042
+ applyChangeFromBindings(processBindings, propagationContextByBinding);
10043
+ }
10044
+ else {
10045
+ applyChangeFromBindings(processBindings);
10046
+ }
10047
+ };
10048
+ // View transition 参加点(docs/view-transition-design.md §7.2)。arbiter が
10049
+ // 居なければ runTransition はその場で applyBindings を呼び、undefined を返す
10050
+ // = 従来と完全に同じ同期適用。SSR では遷移そのものを持たない(G5)。
10051
+ //
10052
+ // 適用する binding が 0 本のバッチは arbiter へ渡さない。書き込みはバインドの
10053
+ // 有無に関わらず enqueue される(setByAddress)ため、headless なパス
10054
+ // (`$watch` 専用・`$streams` の内部状態・リスト置換の中間アドレス)への
10055
+ // 書き込みだけでもここへ到達する。それでページ全体をスナップショットするのは
10056
+ // 無駄なだけでなく、既定の mode="latest" では「アニメーションすべき DOM 変更が
10057
+ // 無い遷移」が実行中の本物の遷移をスキップしてしまう(ルート遷移が毎回途中で
10058
+ // 切れる/active が空撃ちで振動する)。
10059
+ if (inSsr() || processBindings.length === 0) {
10060
+ applyBindings();
9487
10061
  }
9488
10062
  else {
9489
- applyChangeFromBindings(processBindings);
10063
+ const pending = runTransition("state", applyBindings);
10064
+ if (pending !== undefined) {
10065
+ pending.catch(reportDeferredApplyFailure);
10066
+ }
9490
10067
  }
9491
10068
  }
9492
10069
  finally {
@@ -9881,6 +10458,58 @@ function registerDevtoolsSource() {
9881
10458
  getOrCreateHookRegistry().register(source);
9882
10459
  }
9883
10460
 
10461
+ /**
10462
+ * ssr-snapshot プロトコルの提供側(docs/ssr-router-design.md §5)。
10463
+ *
10464
+ * `<wcs-ssr>` スナップショットを document 全体に対する最終パスとして生成する。
10465
+ * connectedCallback 内の inline 生成は「その時点の DOM」しか見えず、router が
10466
+ * 後から挿入するルート内容の構造テンプレートを取り逃がすレースがあった
10467
+ * (state のロード方式と文書順に依存)。renderToString が全要素の完了と
10468
+ * バインディング構築の後にこれを呼ぶことで、スナップショットは常に確定後の
10469
+ * DOM を見る。
10470
+ *
10471
+ * 複数 `enable-ssr` state の意味論は inline 生成と同一に保つ(文書順に生成・
10472
+ * fragment レジストリはモジュール共有・props store は生成ごとにクリア)。
10473
+ * その整理は本プロトコルの範囲外の既存挙動として引き継ぐ。
10474
+ */
10475
+ function buildSsrDocument(root) {
10476
+ const stateTag = config.tagNames.state;
10477
+ const ssrTag = config.tagNames.ssr;
10478
+ const stateElements = root.querySelectorAll(`${stateTag}[enable-ssr]`);
10479
+ for (const stateEl of stateElements) {
10480
+ const name = stateEl.getAttribute("name") || "default";
10481
+ // 既に直前へ生成済み(旧 server との組み合わせで inline 生成された等)なら
10482
+ // 何もしない — build() は冪等でなければならない(プロトコル契約)
10483
+ const prev = stateEl.previousElementSibling;
10484
+ if (prev !== null &&
10485
+ prev.tagName.toLowerCase() === ssrTag &&
10486
+ (prev.getAttribute("name") || "default") === name) {
10487
+ continue;
10488
+ }
10489
+ const ssrEl = document.createElement(ssrTag);
10490
+ ssrEl.setAttribute("name", name);
10491
+ ssrEl.setAttribute("version", VERSION);
10492
+ Ssr.buildContent(ssrEl, Ssr.extractStateData(stateEl));
10493
+ stateEl.parentNode?.insertBefore(ssrEl, stateEl);
10494
+ }
10495
+ }
10496
+ const builder = {
10497
+ protocol: "wcs-ssr-snapshot",
10498
+ version: 1,
10499
+ build: buildSsrDocument,
10500
+ };
10501
+ /**
10502
+ * グローバル symbol へ自分を載せる。`bootstrapState` から呼ぶ。
10503
+ * binder(registerBinder)と同じ規範 — 既に別のコピーが載っているなら譲る
10504
+ * (そのコピーのレジストリが、そのページの正本だからである)。
10505
+ */
10506
+ function registerSsrSnapshotBuilder() {
10507
+ const globals = globalThis;
10508
+ if (globals[SSR_SNAPSHOT_BUILDER_KEY] === undefined) {
10509
+ globals[SSR_SNAPSHOT_BUILDER_KEY] = builder;
10510
+ }
10511
+ }
10512
+
9884
10513
  const CSP_GUIDE = "https://github.com/wcstack/wcstack/blob/main/docs/csp.md";
9885
10514
  /**
9886
10515
  * インライン `<script>` の評価失敗を、原因の分かるメッセージに変換する。
@@ -9961,9 +10590,26 @@ async function loadFromJsonFile(url) {
9961
10590
  }
9962
10591
  }
9963
10592
 
10593
+ /**
10594
+ * `src` の値を **document の base URL** に対して解決する。
10595
+ *
10596
+ * `import(url)` の相対解決は「import を書いたモジュール」を基準にする。ここは
10597
+ * `@wcstack/state` の中なので、素の `import(url)` は `<wcs-state src>` を
10598
+ * **state パッケージの所在**から解決してしまう。同一オリジンに置いたページでは
10599
+ * たまたま一致して見えるが、CDN 一発(`https://esm.run/@wcstack/state/auto`)で
10600
+ * 読み込んだ瞬間に `src="/app.js"` が CDN 側の URL を指して 404 になる。
10601
+ *
10602
+ * `src` は HTML 属性なので、正しい基準は document の base URL である
10603
+ * (`src="*.json"` 側は `fetch` がそう解決しており、同じ属性が形式によって
10604
+ * 違う基準で解決されていた)。絶対 URL・`data:`・`blob:` は URL 解決で
10605
+ * そのまま素通りするため、既存の使い方は影響を受けない。
10606
+ */
10607
+ function resolveAgainstDocument(url) {
10608
+ return new URL(url, document.baseURI).href;
10609
+ }
9964
10610
  async function loadFromScriptFile(url) {
9965
10611
  try {
9966
- const module = await import(/* @vite-ignore */ url);
10612
+ const module = await import(/* @vite-ignore */ resolveAgainstDocument(url));
9967
10613
  return module.default || {};
9968
10614
  }
9969
10615
  catch (e) {
@@ -12176,6 +12822,41 @@ function disconnectedCallback(target, _prop, receiver, _handler) {
12176
12822
  }
12177
12823
  }
12178
12824
 
12825
+ /**
12826
+ * getContextListIndex.ts
12827
+ *
12828
+ * Stateの内部APIとして、現在のプロパティ参照スコープにおける
12829
+ * 指定したstructuredPath(ワイルドカード付きプロパティパス)に対応する
12830
+ * リストインデックス(IListIndex)を取得する関数です。
12831
+ *
12832
+ * 主な役割:
12833
+ * - handlerの最後にアクセスされたAddressから、指定パスに対応するリストインデックスを取得
12834
+ * - ワイルドカード階層に対応し、多重ループやネストした配列バインディングにも利用可能
12835
+ *
12836
+ * 設計ポイント:
12837
+ * - 直近のプロパティ参照情報を取得
12838
+ * - info.indexByWildcardPathからstructuredPathのインデックスを特定
12839
+ * - listIndex.at(index)で該当階層のリストインデックスを取得
12840
+ * - パスが一致しない場合や参照が存在しない場合はnullを返す
12841
+ */
12842
+ function getContextListIndex(handler, structuredPath) {
12843
+ if (handler.addressStackLength === 0) {
12844
+ return null;
12845
+ }
12846
+ const address = handler.lastAddressStack;
12847
+ if (address === null) {
12848
+ return null;
12849
+ }
12850
+ const index = address.pathInfo.indexByWildcardPath[structuredPath];
12851
+ if (typeof index === "undefined") {
12852
+ return null;
12853
+ }
12854
+ if (address.listIndex === null) {
12855
+ return null;
12856
+ }
12857
+ return listIndexAtWildcard(address.listIndex, index, address.pathInfo.wildcardCount);
12858
+ }
12859
+
12179
12860
  const cacheEntryByAbsoluteStateAddress = new WeakMap();
12180
12861
  function getCacheEntryByAbsoluteStateAddress(address) {
12181
12862
  return cacheEntryByAbsoluteStateAddress.get(address) ?? null;
@@ -12463,38 +13144,112 @@ function getByAddress(target, address, receiver, handler) {
12463
13144
  }
12464
13145
 
12465
13146
  /**
12466
- * getContextListIndex.ts
13147
+ * wildcardIndexes.ts
12467
13148
  *
12468
- * Stateの内部APIとして、現在のプロパティ参照スコープにおける
12469
- * 指定したstructuredPath(ワイルドカード付きプロパティパス)に対応する
12470
- * リストインデックス(IListIndex)を取得する関数です。
13149
+ * ワイルドカードを含むパスから「解決済み添字タプルの集合」を列挙する共有走査。
13150
+ * `$getAll`(読み)と `$setAll`(書き)が**同じ展開規則・同じ順序**で動くための単一の正本
13151
+ * (docs/state-set-all-design.md §6-1)。
12471
13152
  *
12472
- * 主な役割:
12473
- * - handlerの最後にアクセスされたAddressから、指定パスに対応するリストインデックスを取得
12474
- * - ワイルドカード階層に対応し、多重ループやネストした配列バインディングにも利用可能
13153
+ * 添字は**前方一致の接頭辞**で、足りない分は「その階層を全部展開する」という意味を持つ
13154
+ * (README の `$getAll("scores.*", [])` がこれ)。返るタプルは常にワイルドカードの本数と
13155
+ * 同じ長さになるので、そのまま `$resolve` の厳密一致な添字として使える。
12475
13156
  *
12476
- * 設計ポイント:
12477
- * - 直近のプロパティ参照情報を取得
12478
- * - info.indexByWildcardPathからstructuredPathのインデックスを特定
12479
- * - listIndex.at(index)で該当階層のリストインデックスを取得
12480
- * - パスが一致しない場合や参照が存在しない場合はnullを返す
13157
+ * 順序は**深さ優先・添字昇順**(ネストは添字タプルの辞書順)で決定的。
13158
+ * `$getAll(p, i)` の戻り順と `$setAll(p, i, …)` の適用順が一致する根拠がこれであり、
13159
+ * `$setAll` の `{ spread: true }` 形はこの順序に乗っている。
13160
+ *
13161
+ * Throws: LIST-201(インデックス未解決)、BIND-201(ワイルドカード情報不整合)
12481
13162
  */
12482
- function getContextListIndex(handler, structuredPath) {
12483
- if (handler.addressStackLength === 0) {
12484
- return null;
12485
- }
12486
- const address = handler.lastAddressStack;
12487
- if (address === null) {
12488
- return null;
12489
- }
12490
- const index = address.pathInfo.indexByWildcardPath[structuredPath];
12491
- if (typeof index === "undefined") {
12492
- return null;
12493
- }
12494
- if (address.listIndex === null) {
12495
- return null;
13163
+ /**
13164
+ * 各ワイルドカード階層で最後に観測したリスト値。**次の読みの差分基準**であり、
13165
+ * ListIndex の同一性を跨いで保つために使う。
13166
+ *
13167
+ * 所有権は読み(`$getAll`)側にある。書き(`$setAll`)はこの走査を借りるだけで
13168
+ * 記録を更新しない(`commitDiffBaseline: false`。設計 §6-2)。
13169
+ */
13170
+ // ToDo: IAbsoluteStateAddressに変更する
13171
+ const lastValueByListAddress = new WeakMap();
13172
+ /**
13173
+ * `pathInfo` のワイルドカードを `indexes`(前方一致の接頭辞)で絞り込みつつ展開し、
13174
+ * マッチする添字タプルを列挙する。
13175
+ *
13176
+ * 添字の本数検査(上限)は呼び出し側の責務 — API 名を診断メッセージに出すため。
13177
+ */
13178
+ function collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, options) {
13179
+ const newValueByAddress = new Map();
13180
+ const walkWildcardPattern = (wildcardParentPathInfos, wildcardIndexPos, listIndex, indexes, indexPos, parentIndexes, results) => {
13181
+ const wildcardParentPathInfo = wildcardParentPathInfos[wildcardIndexPos] ?? null;
13182
+ if (wildcardParentPathInfo === null) {
13183
+ results.push(parentIndexes);
13184
+ return;
13185
+ }
13186
+ const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
13187
+ const oldValue = lastValueByListAddress.get(wildcardAddress);
13188
+ const newValue = getByAddress(target, wildcardAddress, receiver, handler);
13189
+ const listDiff = createListDiff(getListParentListIndex(handler.stateElement, listIndex), oldValue, newValue);
13190
+ const listIndexes = listDiff.newIndexes;
13191
+ const index = indexes[indexPos] ?? null;
13192
+ newValueByAddress.set(wildcardAddress, newValue);
13193
+ if (index === null) {
13194
+ for (let i = 0; i < listIndexes.length; i++) {
13195
+ const listIndex = listIndexes[i];
13196
+ walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
13197
+ }
13198
+ }
13199
+ else {
13200
+ // 範囲外 index はリスト自体の不在と別原因なので index を含める
13201
+ // (docs/state-bind-component-nested-for-design.md §8.4)
13202
+ const listIndex = listIndexes[index] ??
13203
+ raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
13204
+ if ((wildcardIndexPos + 1) < wildcardParentPathInfos.length) {
13205
+ walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
13206
+ }
13207
+ else {
13208
+ // 最終ワイルドカード層まで到達しているので、結果を確定
13209
+ results.push(parentIndexes.concat(listIndex.index));
13210
+ }
13211
+ }
13212
+ };
13213
+ const resultIndexes = [];
13214
+ walkWildcardPattern(pathInfo.wildcardParentPathInfos, 0, null, indexes, 0, [], resultIndexes);
13215
+ if (options.commitDiffBaseline) {
13216
+ for (const [address, newValue] of newValueByAddress.entries()) {
13217
+ lastValueByListAddress.set(address, newValue);
13218
+ }
12496
13219
  }
12497
- return listIndexAtWildcard(address.listIndex, index, address.pathInfo.wildcardCount);
13220
+ return resultIndexes;
13221
+ }
13222
+
13223
+ /**
13224
+ * getListIndexByIndexes.ts
13225
+ *
13226
+ * 解決済みの添字タプル(ワイルドカード 1 段につき 1 個)から、対応する ListIndex を
13227
+ * **正本レジストリ**(listIndexesByList)経由で引き当てる。
13228
+ *
13229
+ * `$resolve` と `$setAll` の共有部分。列挙側(wildcardIndexes.ts)が走査中に生成した
13230
+ * ListIndex をそのまま書き込み先にせず、ここで引き直すことで、binding が使っている
13231
+ * ListIndex と同一の同一性に載る(docs/state-set-all-design.md §6-2)。
13232
+ *
13233
+ * 添字の本数がワイルドカードの本数と一致していることは呼び出し側の責務。
13234
+ */
13235
+ function getListIndexByIndexes(target, receiver, handler, pathInfo, indexes) {
13236
+ // ワイルドカード階層ごとにListIndexを解決していく
13237
+ let listIndex = null;
13238
+ for (let i = 0; i < pathInfo.wildcardParentPathInfos.length; i++) {
13239
+ const wildcardParentPathInfo = pathInfo.wildcardParentPathInfos[i];
13240
+ const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
13241
+ const tmpValue = getByAddress(target, wildcardAddress, receiver, handler);
13242
+ const listIndexes = getListIndexesByList(tmpValue);
13243
+ if (listIndexes == null) {
13244
+ raiseError(`ListIndexes not found: ${wildcardParentPathInfo.path}`);
13245
+ }
13246
+ const index = indexes[i];
13247
+ // 範囲外 index はリスト自体の不在と別原因なので index を含める
13248
+ // (docs/state-bind-component-nested-for-design.md §8.4)
13249
+ listIndex = listIndexes[index] ??
13250
+ raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
13251
+ }
13252
+ return listIndex;
12498
13253
  }
12499
13254
 
12500
13255
  /**
@@ -13500,22 +14255,8 @@ function resolve(target, _prop, receiver, handler) {
13500
14255
  if (indexes.length !== pathInfo.wildcardParentPathInfos.length) {
13501
14256
  raiseError(indexArityMessage("$resolve", path, pathInfo.wildcardParentPathInfos.length, indexes.length));
13502
14257
  }
13503
- // ワイルドカード階層ごとにListIndexを解決していく
13504
- let listIndex = null;
13505
- for (let i = 0; i < pathInfo.wildcardParentPathInfos.length; i++) {
13506
- const wildcardParentPathInfo = pathInfo.wildcardParentPathInfos[i];
13507
- const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
13508
- const tmpValue = getByAddress(target, wildcardAddress, receiver, handler);
13509
- const listIndexes = getListIndexesByList(tmpValue);
13510
- if (listIndexes == null) {
13511
- raiseError(`ListIndexes not found: ${wildcardParentPathInfo.path}`);
13512
- }
13513
- const index = indexes[i];
13514
- // 範囲外 index はリスト自体の不在と別原因なので index を含める
13515
- // (docs/state-bind-component-nested-for-design.md §8.4)
13516
- listIndex = listIndexes[index] ??
13517
- raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
13518
- }
14258
+ // ワイルドカード階層ごとにListIndexを解決していく(`$setAll` と共有)
14259
+ const listIndex = getListIndexByIndexes(target, receiver, handler, pathInfo, indexes);
13519
14260
  // ToDo:WritableかReadonlyかを判定して適切なメソッドを呼び出す
13520
14261
  const address = createStateAddress(pathInfo, listIndex);
13521
14262
  const hasSetValue = typeof value !== "undefined";
@@ -13532,14 +14273,18 @@ function resolve(target, _prop, receiver, handler) {
13532
14273
  * getAllReadonly
13533
14274
  *
13534
14275
  * ワイルドカードを含む State パスから、対象となる全要素を配列で取得する。
13535
- * Throws: LIST-201(インデックス未解決)、BIND-201(ワイルドカード情報不整合)
14276
+ * 走査そのものは `$setAll` と共有する(wildcardIndexes.ts)。
14277
+ *
14278
+ * `indexes` 省略時の既定はループ文脈の添字 `[$1..$n]`。正確には「path と文脈が
14279
+ * 共有するワイルドカード連鎖の分だけ文脈の添字を接頭辞として敷く」(整合最長接頭辞)。
14280
+ * 共有が無いのに文脈が添字を持つ場合は throw する — 異なる文脈の添字は流用しない。
14281
+ *
14282
+ * Throws: LIST-201(インデックス未解決)、BIND-201(ワイルドカード情報不整合)、
14283
+ * 添字本数超過(wcs/index-arity)、省略時の文脈不整合(getAllContextMismatchMessage)
13536
14284
  */
13537
- // ToDo: IAbsoluteStateAddressに変更する
13538
- const lastValueByListAddress = new WeakMap();
13539
14285
  function getAll(target, prop, receiver, handler) {
13540
14286
  const resolveFn = resolve(target, prop, receiver, handler);
13541
14287
  return (path, indexes) => {
13542
- const newValueByAddress = new Map();
13543
14288
  const pathInfo = getPathInfo(path);
13544
14289
  if (handler.addressStackLength > 0) {
13545
14290
  const lastInfo = handler.lastAddressStack?.pathInfo ?? null;
@@ -13560,60 +14305,39 @@ function getAll(target, prop, receiver, handler) {
13560
14305
  raiseError(indexArityMessage("$getAll", path, pathInfo.wildcardParentPathInfos.length, indexes.length));
13561
14306
  }
13562
14307
  if (typeof indexes === "undefined") {
13563
- for (let i = 0; i < pathInfo.wildcardParentPathInfos.length; i++) {
13564
- const wildcardPattern = pathInfo.wildcardParentPathInfos[i];
13565
- const listIndex = getContextListIndex(handler, wildcardPattern.path);
14308
+ // 省略時の既定はループ文脈の添字 `[$1..$n]`。ただし敷けるのは path と文脈が
14309
+ // **共有するワイルドカード連鎖**の分だけなので、path のワイルドカードを
14310
+ // 内側(最深)から探し、最初に文脈にヒットした階層の scoped indexes を接頭辞にする。
14311
+ // ワイルドカードパスの序数はパス文字列自身の `*` の本数で決まるため、深い側が
14312
+ // ヒットすれば浅い側は必ず含まれ、これが整合する最長の接頭辞になる。文脈が
14313
+ // path より深い分は自然に切り詰められ、導出した接頭辞は path のワイルドカード
14314
+ // 本数を超えないので、上の本数検査には掛けない。
14315
+ for (let i = pathInfo.wildcardPaths.length - 1; i >= 0; i--) {
14316
+ const listIndex = getContextListIndex(handler, pathInfo.wildcardPaths[i]);
13566
14317
  if (listIndex) {
13567
14318
  indexes = getScopedIndexes(listIndex, listIndex.length - getBaseDepth(handler.stateElement));
13568
14319
  break;
13569
14320
  }
13570
14321
  }
13571
14322
  if (typeof indexes === "undefined") {
14323
+ // 共有ゼロ。文脈が自スコープの添字を実際に持っているなら、既定の `[...$n]` は
14324
+ // **異なる文脈の添字の流用(混入)**になるため、黙って全展開へ倒さず throw する。
14325
+ // 文脈そのものが無い(トップレベル getter / メソッド直下)なら全展開が既定。
14326
+ const lastAddress = handler.addressStackLength > 0 ? handler.lastAddressStack : null;
14327
+ const contextListIndex = lastAddress?.listIndex ?? null;
14328
+ if (pathInfo.wildcardCount > 0 && lastAddress !== null && contextListIndex !== null &&
14329
+ contextListIndex.length - getBaseDepth(handler.stateElement) > 0) {
14330
+ raiseError(getAllContextMismatchMessage(path, lastAddress.pathInfo.path));
14331
+ }
13572
14332
  indexes = [];
13573
14333
  }
13574
14334
  }
13575
- const walkWildcardPattern = (wildcardParentPathInfos, wildcardIndexPos, listIndex, indexes, indexPos, parentIndexes, results) => {
13576
- const wildcardParentPathInfo = wildcardParentPathInfos[wildcardIndexPos] ?? null;
13577
- if (wildcardParentPathInfo === null) {
13578
- results.push(parentIndexes);
13579
- return;
13580
- }
13581
- const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
13582
- const oldValue = lastValueByListAddress.get(wildcardAddress);
13583
- const newValue = getByAddress(target, wildcardAddress, receiver, handler);
13584
- const listDiff = createListDiff(getListParentListIndex(handler.stateElement, listIndex), oldValue, newValue);
13585
- const listIndexes = listDiff.newIndexes;
13586
- const index = indexes[indexPos] ?? null;
13587
- newValueByAddress.set(wildcardAddress, newValue);
13588
- if (index === null) {
13589
- for (let i = 0; i < listIndexes.length; i++) {
13590
- const listIndex = listIndexes[i];
13591
- walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
13592
- }
13593
- }
13594
- else {
13595
- // 範囲外 index はリスト自体の不在と別原因なので index を含める
13596
- // (docs/state-bind-component-nested-for-design.md §8.4)
13597
- const listIndex = listIndexes[index] ??
13598
- raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
13599
- if ((wildcardIndexPos + 1) < wildcardParentPathInfos.length) {
13600
- walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
13601
- }
13602
- else {
13603
- // 最終ワイルドカード層まで到達しているので、結果を確定
13604
- results.push(parentIndexes.concat(listIndex.index));
13605
- }
13606
- }
13607
- };
13608
- const resultIndexes = [];
13609
- walkWildcardPattern(pathInfo.wildcardParentPathInfos, 0, null, indexes, 0, [], resultIndexes);
14335
+ // 読みなので差分基準を更新する(`$setAll` は更新しない。設計 §6-2)
14336
+ const resultIndexes = collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, { commitDiffBaseline: true });
13610
14337
  const resultValues = [];
13611
14338
  for (let i = 0; i < resultIndexes.length; i++) {
13612
14339
  resultValues.push(resolveFn(pathInfo.path, resultIndexes[i]));
13613
14340
  }
13614
- for (const [address, newValue] of newValueByAddress.entries()) {
13615
- lastValueByListAddress.set(address, newValue);
13616
- }
13617
14341
  return resultValues;
13618
14342
  };
13619
14343
  }
@@ -13697,6 +14421,83 @@ function postUpdate(target, _prop, receiver, handler) {
13697
14421
  };
13698
14422
  }
13699
14423
 
14424
+ /**
14425
+ * setAll.ts
14426
+ *
14427
+ * ワイルドカードを含む State パスにマッチする**全アドレスへ一括で書き込む**。
14428
+ * `$getAll`(読み)の対称形(docs/state-set-all-design.md)。
14429
+ *
14430
+ * 存在理由は糖衣ではなく「**リスト全置換の回避**」(設計 §1-1)。
14431
+ * `this.users = this.users.map(...)` は配列を作り直すので ListIndex・行 getter
14432
+ * キャッシュ・差分描画がまとめて作り直しになる。`$setAll` は意味としては一括更新、
14433
+ * 実体は in-place な個別書き込みで、同じことを差分に載せたまま行う。
14434
+ *
14435
+ * 3 つの形(設計 §2):
14436
+ * - ブロードキャスト `$setAll(path, indexes, value)`
14437
+ * - mapper(第一級) `$setAll(path, indexes, (current, ...indexes) => next)`
14438
+ * - spread `$setAll(path, indexes, values, { spread: true })`
14439
+ */
14440
+ function setAll(target, _prop, receiver, handler) {
14441
+ return (path, indexes, value, options) => {
14442
+ const pathInfo = getPathInfo(path);
14443
+ // 書き込み API に暗黙の文脈依存は持たせない。`for` の中で `[]` と書けば
14444
+ // 「現在行」ではなく「全行」を意味する(設計 §4-1)。
14445
+ if (!Array.isArray(indexes)) {
14446
+ raiseError(setAllValueKindMessage(path, "requires an explicit indexes array (pass [] to expand every level)."));
14447
+ }
14448
+ // 添字は前方一致の接頭辞なので不足は正当。超過だけを弾く(`$getAll` と同じ規則)。
14449
+ if (indexes.length > pathInfo.wildcardParentPathInfos.length) {
14450
+ raiseError(indexArityMessage("$setAll", path, pathInfo.wildcardParentPathInfos.length, indexes.length));
14451
+ }
14452
+ const spread = options?.spread === true;
14453
+ const isMapper = typeof value === "function";
14454
+ if (spread && isMapper) {
14455
+ raiseError(setAllValueKindMessage(path, "cannot combine { spread: true } with a mapper function."));
14456
+ }
14457
+ if (spread && !Array.isArray(value)) {
14458
+ raiseError(setAllValueKindMessage(path, "requires an array as the value when { spread: true } is set."));
14459
+ }
14460
+ // --- 第 1 相: 書き込み先を全部確定する(設計 §6) ---
14461
+ // 走査しながら書くと書き込みが ListIndex 集合を動かしうる。
14462
+ // 差分基準(lastValueByListAddress)は読みの持ち物なので commit しない(§6-2)。
14463
+ const resultIndexes = collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, { commitDiffBaseline: false });
14464
+ if (spread && value.length !== resultIndexes.length) {
14465
+ raiseError(setAllSpreadArityMessage(path, resultIndexes.length, value.length));
14466
+ }
14467
+ const addresses = [];
14468
+ for (let i = 0; i < resultIndexes.length; i++) {
14469
+ const listIndex = getListIndexByIndexes(target, receiver, handler, pathInfo, resultIndexes[i]);
14470
+ addresses.push(createStateAddress(pathInfo, listIndex));
14471
+ }
14472
+ // --- 第 2 相: 確定したアドレスにだけ書く ---
14473
+ let written = 0;
14474
+ for (let i = 0; i < addresses.length; i++) {
14475
+ const address = addresses[i];
14476
+ let nextValue;
14477
+ if (isMapper) {
14478
+ // 現在値は書く直前に読む。先行する書き込みが getter 経由で他行に及ぶ場合、
14479
+ // mapper が見るべきなのは最新値。
14480
+ const currentValue = getByAddress(target, address, receiver, handler);
14481
+ nextValue = value(currentValue, ...resultIndexes[i]);
14482
+ }
14483
+ else if (spread) {
14484
+ nextValue = value[i];
14485
+ }
14486
+ else {
14487
+ nextValue = value;
14488
+ }
14489
+ // undefined は常にスキップ(設計 §5)。mapper の return 忘れで全行を潰さないため、
14490
+ // かつ「この行は変えない」を表現できるようにするため。クリアは null。
14491
+ if (typeof nextValue === "undefined") {
14492
+ continue;
14493
+ }
14494
+ setByAddress(target, address, nextValue, receiver, handler);
14495
+ written++;
14496
+ }
14497
+ return written;
14498
+ };
14499
+ }
14500
+
13700
14501
  /**
13701
14502
  * trackDependency.ts
13702
14503
  *
@@ -13871,7 +14672,7 @@ function setLoopContext(handler, loopContext, callback) {
13871
14672
  * StateClassのProxyトラップとして、プロパティアクセス時の値取得処理を担う関数(get)の実装です。
13872
14673
  *
13873
14674
  * 主な役割:
13874
- * - 文字列プロパティの場合、特殊プロパティ($1〜、$stateElement, $getAll, $postUpdate,
14675
+ * - 文字列プロパティの場合、特殊プロパティ($1〜、$stateElement, $getAll, $setAll, $postUpdate,
13875
14676
  * $resolve, $trackDependency, $command, $streamStatus, $streamError)に応じた値やAPIを返却
13876
14677
  * - 通常のプロパティはgetResolvedPathInfoでパス情報を解決し、getListIndexでリストインデックスを取得
13877
14678
  * - getByRefで構造化パス・リストインデックスに対応した値を取得
@@ -13934,6 +14735,11 @@ function get(target, prop, receiver, handler) {
13934
14735
  return getAll(target, prop, receiver, handler)(path, indexes);
13935
14736
  };
13936
14737
  }
14738
+ case "$setAll": {
14739
+ return (path, indexes, value, options) => {
14740
+ return setAll(target, prop, receiver, handler)(path, indexes, value, options);
14741
+ };
14742
+ }
13937
14743
  case "$postUpdate": {
13938
14744
  return (path) => {
13939
14745
  return postUpdate(target, prop, receiver, handler)(path);
@@ -14896,8 +15702,12 @@ class State extends HTMLElementBase {
14896
15702
  if (!this.hasAttribute('enable-ssr') || inSsr()) {
14897
15703
  await this._callStateConnectedCallback();
14898
15704
  }
14899
- // サーバーモード + enable-ssr: バインディング完了後に <wcs-ssr> を生成
14900
- if (inSsr() && this.hasAttribute('enable-ssr')) {
15705
+ // サーバーモード + enable-ssr: バインディング完了後に <wcs-ssr> を生成。
15706
+ // orchestrated(サーバー主導の最終パス、docs/ssr-router-design.md §5)では
15707
+ // 生成しない — renderToString が全要素の完了後にまとめて生成するため。
15708
+ // ここで生成すると、router 等が後から挿入した内容の構造テンプレートを
15709
+ // 取り逃がすレースがある(state のロード方式と文書順に依存)
15710
+ if (inSsr() && this.hasAttribute('enable-ssr') && !isOrchestratedSsr()) {
14901
15711
  try {
14902
15712
  await getBindingsReady(this.rootNode);
14903
15713
  const name = this.getAttribute('name') || 'default';
@@ -15199,11 +16009,63 @@ function registerComponents(registry = customElements) {
15199
16009
  }
15200
16010
  }
15201
16011
 
16012
+ /**
16013
+ * `<html lang>` を既定ロケールとして採る。
16014
+ *
16015
+ * ロケール依存フィルタ(`locale` / `date` / `time` / `datetime`)は `config.locale`
16016
+ * を読むが、それを設定できる公開の入口は `bootstrapState({ locale })` しかない。
16017
+ * 一方 `auto` エントリは `bootstrapState()` を引数なしで呼ぶため、CDN 一発
16018
+ * (`<script src=".../@wcstack/state/auto">`)で読み込んだページには**ロケールを
16019
+ * 渡す口が無かった**。auto バンドルは SRI のため自己完結で、別途 `@wcstack/state`
16020
+ * を import して `bootstrapState` を呼んでも別インスタンスになり効かない。
16021
+ *
16022
+ * `<html lang>` はページのロケールを書く HTML 標準の場所であり、SSR ではサーバーが、
16023
+ * 静的ページでは head のスニペットが DOM 解析前に書く。そこを既定にすると
16024
+ * **ロケールの正本が 1 つになり**、「設定を早く呼ぶ」という守りにくい順序の約束が
16025
+ * 「`<html lang>` が state のロードより前にある」という構造的な保証に変わる。
16026
+ *
16027
+ * 明示指定(`bootstrapState({ locale })`)が常に優先する。
16028
+ */
16029
+ function localeFromDocument() {
16030
+ const lang = document.documentElement?.lang;
16031
+ if (!lang) {
16032
+ return undefined;
16033
+ }
16034
+ try {
16035
+ // 妥当な BCP-47 タグでなければ Intl が RangeError を投げる。不正な lang を
16036
+ // そのまま採ると、これまで既定 'en' で動いていたページのフィルタが実行時に
16037
+ // 落ちる。既定へ落として警告するほうが、黙って壊すより回復しやすい。
16038
+ Intl.getCanonicalLocales(lang);
16039
+ return lang;
16040
+ }
16041
+ catch {
16042
+ console.warn(`[@wcstack/state] <html lang="${lang}"> is not a valid BCP-47 language tag. ` +
16043
+ `Falling back to the default locale for filters.`);
16044
+ return undefined;
16045
+ }
16046
+ }
16047
+ function resolveConfig(config) {
16048
+ if (typeof config?.locale === "string") {
16049
+ return config;
16050
+ }
16051
+ const locale = localeFromDocument();
16052
+ if (locale === undefined) {
16053
+ return config;
16054
+ }
16055
+ return { ...config, locale };
16056
+ }
15202
16057
  function bootstrapState(config, registry) {
15203
- if (config) {
15204
- setConfig(config);
16058
+ const resolved = resolveConfig(config);
16059
+ if (resolved) {
16060
+ setConfig(resolved);
15205
16061
  }
15206
16062
  registerComponents(registry);
16063
+ // binder プロトコルの提供(docs/binder-protocol-design.md)。router が後から
16064
+ // 差し込むノードをバインドできるようにする。登録は冪等。
16065
+ registerBinder();
16066
+ // ssr-snapshot プロトコルの提供(docs/ssr-router-design.md §5)。renderToString が
16067
+ // <wcs-ssr> 生成をサーバー主導の最終パスへ回せるようにする。登録は冪等。
16068
+ registerSsrSnapshotBuilder();
15207
16069
  // DevTools Hook Protocol への source 登録(SSR では no-op・冪等)
15208
16070
  registerDevtoolsSource();
15209
16071
  }