@wcstack/state 1.8.6 → 1.9.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/dist/auto.js CHANGED
@@ -1,3 +1,3 @@
1
- import { bootstrapState } from "./index.esm.js";
2
-
3
- await bootstrapState();
1
+ import { bootstrapState } from "./index.esm.js";
2
+
3
+ await bootstrapState();
package/dist/auto.min.js CHANGED
@@ -1,3 +1,3 @@
1
- import { bootstrapState } from "./index.esm.min.js";
2
-
3
- await bootstrapState();
1
+ import { bootstrapState } from "./index.esm.min.js";
2
+
3
+ await bootstrapState();
package/dist/index.d.ts CHANGED
@@ -133,14 +133,14 @@ type IsAny<T> = 0 extends (1 & T) ? true : false;
133
133
  * T がドットパス再帰の対象となる「プレーンなデータオブジェクト」かどうかを判定する。
134
134
  * プリミティブ、組み込みオブジェクト (Date, Map 等)、関数、配列、any は除外。
135
135
  */
136
- type IsPlainObject<T> = IsAny<T> extends true ? false : T extends string | number | boolean | null | undefined | symbol | bigint | Function | Date | RegExp | Error | Map<any, any> | Set<any> | WeakMap<any, any> | WeakSet<any> | Promise<any> | readonly any[] ? false : T extends Record<string, any> ? true : false;
136
+ type IsPlainObject<T> = IsAny<T> extends true ? false : T extends string | number | boolean | null | undefined | symbol | bigint | ((...args: any[]) => any) | Date | RegExp | Error | Map<any, any> | Set<any> | WeakMap<any, any> | WeakSet<any> | Promise<any> | readonly any[] ? false : T extends Record<string, any> ? true : false;
137
137
  /**
138
138
  * T のキーのうち、関数でないもの(データプロパティ・computed getter)を抽出する。
139
139
  * メソッド(イベントハンドラ等)はドットパスの対象外。
140
140
  * any 型のプロパティは除外せず保持する。
141
141
  */
142
142
  type DataKeys<T> = {
143
- [K in keyof T & string]: IsAny<T[K]> extends true ? K : T[K] extends Function ? never : K;
143
+ [K in keyof T & string]: IsAny<T[K]> extends true ? K : T[K] extends (...args: any[]) => any ? never : K;
144
144
  }[keyof T & string];
145
145
  /**
146
146
  * 型 T から生成される全てのドットパスの union。
package/dist/index.esm.js CHANGED
@@ -59,7 +59,7 @@ function setConfig(partialConfig) {
59
59
  }
60
60
  }
61
61
 
62
- var version$1 = "1.8.6";
62
+ var version$1 = "1.9.1";
63
63
  var pkg = {
64
64
  version: version$1};
65
65
 
@@ -187,6 +187,8 @@ const STATE_DISCONNECTED_CALLBACK_NAME = "$disconnectedCallback";
187
187
  const STATE_UPDATED_CALLBACK_NAME = "$updatedCallback";
188
188
  const WEBCOMPONENT_STATE_READY_CALLBACK_NAME = "$stateReadyCallback";
189
189
  const STATE_BINDABLES_NAME = "$bindables";
190
+ const STATE_COMMAND_TOKENS_NAME = "$commandTokens";
191
+ const STATE_COMMAND_NAMESPACE_NAME = "$command";
190
192
  const DCC_DEFINITION_ATTRIBUTE = "data-wc-definition";
191
193
 
192
194
  const _cache$4 = new Map();
@@ -1988,6 +1990,123 @@ function applyChangeToClass(binding, _context, newValue) {
1988
1990
  element.classList.toggle(className, newValue);
1989
1991
  }
1990
1992
 
1993
+ // _subscribers は Set のため挿入順を保持する。
1994
+ // emit() は subscribe() された順に呼び出され、戻り値配列も同じ順序で返る。
1995
+ class CommandToken {
1996
+ _name;
1997
+ _subscribers = new Set();
1998
+ constructor(name) {
1999
+ this._name = name;
2000
+ }
2001
+ get name() {
2002
+ return this._name;
2003
+ }
2004
+ get size() {
2005
+ return this._subscribers.size;
2006
+ }
2007
+ subscribe(fn) {
2008
+ this._subscribers.add(fn);
2009
+ return () => {
2010
+ this._subscribers.delete(fn);
2011
+ };
2012
+ }
2013
+ unsubscribe(fn) {
2014
+ return this._subscribers.delete(fn);
2015
+ }
2016
+ emit(...args) {
2017
+ const results = [];
2018
+ for (const fn of this._subscribers) {
2019
+ results.push(fn(...args));
2020
+ }
2021
+ return results;
2022
+ }
2023
+ }
2024
+ function isCommandToken(value) {
2025
+ return value instanceof CommandToken;
2026
+ }
2027
+
2028
+ /**
2029
+ * command.<methodName>: <commandToken-path> バインディングの適用ハンドラ。
2030
+ *
2031
+ * subscribe lifecycle:
2032
+ * - 同一 binding に同じ token が再評価された場合は no-op。
2033
+ * - 異なる token が来た場合は古い subscription を解除し、新しい token に subscribe し直す。
2034
+ * 旧解除は新しい binding 妥当性検証(methodName・wcBindable.commands チェック)を
2035
+ * 通過した後に行うため、再評価が validation で失敗しても旧購読は温存される(fail-fast)。
2036
+ * - element は WeakRef で保持し、subscriber 経由で element を強参照しないようにする。
2037
+ * これにより、element が DOM から消えた後に subscriber が token._subscribers に
2038
+ * 残っていても element 本体は GC 可能。
2039
+ * - emit 時に下記いずれかなら自動で subscription を破棄する(lazy purge):
2040
+ * - WeakRef.deref() が undefined(element が既に GC 済み)
2041
+ * - element.isConnected が false(DOM から取り外されている)
2042
+ *
2043
+ * 既知の制約:
2044
+ * - emit が来なければ stale subscriber は token に残り続ける(要素が GC されても subscriber 関数自体は残る)。
2045
+ * state インスタンスが disconnect されたタイミングで registry ごとクリアされるため、最終的には解放される。
2046
+ * element ライフサイクルに直接フックする手段が現状の binding 機構に無いため、能動的な purge は将来課題。
2047
+ */
2048
+ const subscribedBindings = new WeakMap();
2049
+ function getWcBindable(element) {
2050
+ const customTagName = getCustomElement(element);
2051
+ if (customTagName === null) {
2052
+ return null;
2053
+ }
2054
+ const customClass = customElements.get(customTagName);
2055
+ if (typeof customClass === "undefined") {
2056
+ raiseError(`Custom element <${customTagName}> is not defined for command binding.`);
2057
+ }
2058
+ const bindable = customClass.wcBindable;
2059
+ if (bindable?.protocol === "wc-bindable" && bindable?.version === 1) {
2060
+ return bindable;
2061
+ }
2062
+ return null;
2063
+ }
2064
+ function applyChangeToCommand(binding, _context, newValue) {
2065
+ if (!isCommandToken(newValue)) {
2066
+ raiseError(`command binding requires a CommandToken value (use $command.<tokenName> with a name declared in $commandTokens).`);
2067
+ }
2068
+ const token = newValue;
2069
+ const existing = subscribedBindings.get(binding);
2070
+ if (existing && existing.token === token) {
2071
+ return;
2072
+ }
2073
+ // 新しい binding 妥当性検証は、旧 subscription を解除する前に通す(fail-fast)。
2074
+ const element = binding.node;
2075
+ const methodName = binding.propSegments[1];
2076
+ if (typeof methodName !== "string" || methodName.length === 0) {
2077
+ raiseError(`command binding requires a method name (e.g., "command.fetch").`);
2078
+ }
2079
+ const bindable = getWcBindable(element);
2080
+ if (bindable === null) {
2081
+ raiseError(`command binding requires a wc-bindable custom element. <${element.tagName.toLowerCase()}> is not wc-bindable.`);
2082
+ }
2083
+ if (!Array.isArray(bindable.commands) || !bindable.commands.includes(methodName)) {
2084
+ raiseError(`Command "${methodName}" is not declared in wcBindable.commands of <${element.tagName.toLowerCase()}>.`);
2085
+ }
2086
+ // ここまで来たら旧解除して新 subscribe に切り替える。
2087
+ if (existing) {
2088
+ existing.unsubscribe();
2089
+ subscribedBindings.delete(binding);
2090
+ }
2091
+ const elementRef = new WeakRef(element);
2092
+ let unsubscribe = null;
2093
+ const subscriber = (...args) => {
2094
+ const el = elementRef.deref();
2095
+ if (!el || !el.isConnected) {
2096
+ unsubscribe?.();
2097
+ subscribedBindings.delete(binding);
2098
+ return undefined;
2099
+ }
2100
+ const method = el[methodName];
2101
+ if (typeof method !== "function") {
2102
+ raiseError(`Method "${methodName}" is not a function on <${el.tagName.toLowerCase()}>.`);
2103
+ }
2104
+ return Reflect.apply(method, el, args);
2105
+ };
2106
+ unsubscribe = token.subscribe(subscriber);
2107
+ subscribedBindings.set(binding, { token, unsubscribe, elementRef });
2108
+ }
2109
+
1991
2110
  const _cache$1 = new WeakMap();
1992
2111
  const _cacheNullListIndex = new WeakMap();
1993
2112
  class StateAddress {
@@ -3088,6 +3207,7 @@ const applyChangeByFirstSegment = {
3088
3207
  "class": applyChangeToClass,
3089
3208
  "attr": applyChangeToAttribute,
3090
3209
  "style": applyChangeToStyle,
3210
+ "command": applyChangeToCommand,
3091
3211
  };
3092
3212
  const applyChangeByBindingType = {
3093
3213
  "text": applyChangeToText,
@@ -4821,6 +4941,117 @@ function createLoopContextStack() {
4821
4941
  return new LoopContextStack();
4822
4942
  }
4823
4943
 
4944
+ /**
4945
+ * `$commandTokens: ["a", "b", ...]` 配列宣言を解析し、宣言された名前群を Set で返す。
4946
+ *
4947
+ * 注入は行わず、proxy 側で `state.$command.<name>` として token を解決する設計。
4948
+ * (以前の実装は state 直下に各名前の getter を注入していたが、リアクティブ値との
4949
+ * 名前空間衝突を避け識別性を上げるため `$command` ネームスペース集約に切り替え。)
4950
+ *
4951
+ * 対応している宣言形式は **オブジェクトリテラル** のみ。
4952
+ * クラス本体に `static $commandTokens = [...]` を書く形式や、
4953
+ * クラスのプロトタイプ上の同名コマンドの検出は現状サポートしない。
4954
+ */
4955
+ function processCommandTokensDeclaration(state) {
4956
+ const names = new Set();
4957
+ const declared = state[STATE_COMMAND_TOKENS_NAME];
4958
+ if (typeof declared === "undefined") {
4959
+ return names;
4960
+ }
4961
+ if (!Array.isArray(declared)) {
4962
+ raiseError(`${STATE_COMMAND_TOKENS_NAME} must be an array of strings.`);
4963
+ }
4964
+ for (const name of declared) {
4965
+ if (typeof name !== "string" || name.length === 0) {
4966
+ raiseError(`${STATE_COMMAND_TOKENS_NAME} entries must be non-empty strings.`);
4967
+ }
4968
+ if (name === STATE_COMMAND_NAMESPACE_NAME) {
4969
+ raiseError(`${STATE_COMMAND_TOKENS_NAME} entry "${name}" conflicts with the reserved namespace name "${STATE_COMMAND_NAMESPACE_NAME}".`);
4970
+ }
4971
+ if (names.has(name)) {
4972
+ raiseError(`${STATE_COMMAND_TOKENS_NAME} entry "${name}" is duplicated.`);
4973
+ }
4974
+ names.add(name);
4975
+ }
4976
+ return names;
4977
+ }
4978
+
4979
+ const registryByStateElement = new WeakMap();
4980
+ function getOrCreateCommandToken(stateElement, name) {
4981
+ let registry = registryByStateElement.get(stateElement);
4982
+ if (typeof registry === "undefined") {
4983
+ registry = new Map();
4984
+ registryByStateElement.set(stateElement, registry);
4985
+ }
4986
+ let token = registry.get(name);
4987
+ if (typeof token === "undefined") {
4988
+ token = new CommandToken(name);
4989
+ registry.set(name, token);
4990
+ }
4991
+ return token;
4992
+ }
4993
+ function clearCommandTokenRegistry(stateElement) {
4994
+ registryByStateElement.delete(stateElement);
4995
+ }
4996
+
4997
+ /**
4998
+ * `state.$command` でアクセスされる command token の namespace proxy を提供する。
4999
+ *
5000
+ * - state element 単位で memo 化し、同一 stateElement なら同じ proxy が返る。
5001
+ * - 宣言された名前 (`$commandTokens` に列挙されたもの) のみ token を返す。
5002
+ * 宣言外の名前にアクセスした場合は undefined を返す。
5003
+ * (`constructor` / `Symbol.toPrimitive` / `then` など内部システムが触るキーで
5004
+ * 例外を投げないため。typo は subsequent な `.emit()` 呼び出しで TypeError として
5005
+ * 間接的に表面化する。)
5006
+ * - token そのものの memo は `getOrCreateCommandToken` 側に集約されており、
5007
+ * namespace proxy は薄いゲートウェイとして振る舞う。
5008
+ */
5009
+ const namespaceProxyByStateElement = new WeakMap();
5010
+ function getCommandNamespace(stateElement) {
5011
+ const cached = namespaceProxyByStateElement.get(stateElement);
5012
+ if (typeof cached !== "undefined") {
5013
+ return cached;
5014
+ }
5015
+ const proxy = new Proxy(Object.create(null), {
5016
+ get(_target, prop) {
5017
+ if (typeof prop !== "string") {
5018
+ return undefined;
5019
+ }
5020
+ if (!stateElement.commandTokenNames.has(prop)) {
5021
+ return undefined;
5022
+ }
5023
+ return getOrCreateCommandToken(stateElement, prop);
5024
+ },
5025
+ has(_target, prop) {
5026
+ return typeof prop === "string" && stateElement.commandTokenNames.has(prop);
5027
+ },
5028
+ ownKeys() {
5029
+ return Array.from(stateElement.commandTokenNames);
5030
+ },
5031
+ getOwnPropertyDescriptor(_target, prop) {
5032
+ if (typeof prop === "string" && stateElement.commandTokenNames.has(prop)) {
5033
+ return {
5034
+ configurable: true,
5035
+ enumerable: true,
5036
+ value: getOrCreateCommandToken(stateElement, prop),
5037
+ };
5038
+ }
5039
+ return undefined;
5040
+ },
5041
+ set() {
5042
+ raiseError(`$command namespace is read-only; assigning to it is not allowed.`);
5043
+ },
5044
+ deleteProperty() {
5045
+ raiseError(`$command namespace is read-only; deleting from it is not allowed.`);
5046
+ },
5047
+ });
5048
+ namespaceProxyByStateElement.set(stateElement, proxy);
5049
+ return proxy;
5050
+ }
5051
+ function clearCommandNamespace(stateElement) {
5052
+ namespaceProxyByStateElement.delete(stateElement);
5053
+ }
5054
+
4824
5055
  function getterFn(name) {
4825
5056
  return function () {
4826
5057
  const stateEl = this.stateElement;
@@ -4832,7 +5063,8 @@ function getterFn(name) {
4832
5063
  value = state[name];
4833
5064
  });
4834
5065
  }
4835
- catch {
5066
+ catch (e) {
5067
+ console.warn(`[@wcstack/state] DCC getter "${name}" failed:`, e);
4836
5068
  return undefined;
4837
5069
  }
4838
5070
  return value;
@@ -4855,22 +5087,25 @@ function callFn(name, isAsync) {
4855
5087
  return function (...args) {
4856
5088
  const stateEl = this.stateElement;
4857
5089
  if (!stateEl)
4858
- return;
5090
+ return undefined;
4859
5091
  return stateEl.initializePromise.then(() => {
5092
+ let result;
4860
5093
  return stateEl.createStateAsync("writable", async (state) => {
4861
- await state[name](...args);
4862
- });
5094
+ result = await state[name](...args);
5095
+ }).then(() => result);
4863
5096
  });
4864
5097
  };
4865
5098
  }
4866
5099
  return function (...args) {
4867
5100
  const stateEl = this.stateElement;
4868
5101
  if (!stateEl)
4869
- return;
4870
- stateEl.initializePromise.then(() => {
5102
+ return undefined;
5103
+ return stateEl.initializePromise.then(() => {
5104
+ let result;
4871
5105
  stateEl.createState("writable", (state) => {
4872
- state[name](...args);
5106
+ result = state[name](...args);
4873
5107
  });
5108
+ return result;
4874
5109
  });
4875
5110
  };
4876
5111
  }
@@ -4904,7 +5139,8 @@ function defineDCC(hostElement, shadowRoot, state) {
4904
5139
  raiseError(`DCC: "${tagName}" is not a valid custom element name (must contain a hyphen).`);
4905
5140
  }
4906
5141
  if (customElements.get(tagName)) {
4907
- // 既に登録済みならスキップ
5142
+ // 既に登録済みならスキップ(重複定義の検知のため警告は出す)
5143
+ console.warn(`[@wcstack/state] DCC: "${tagName}" is already registered. Skipping redefinition.`);
4908
5144
  return;
4909
5145
  }
4910
5146
  // ShadowRoot は cloneNode 不可のため、template 経由で内容をクローン
@@ -5175,6 +5411,19 @@ function checkDependency(handler, address) {
5175
5411
  * - finallyでキャッシュへの格納を保証
5176
5412
  */
5177
5413
  function _getByAddress(target, address, receiver, handler, stateElement) {
5414
+ if (address.pathInfo.segments[0] === STATE_COMMAND_NAMESPACE_NAME) {
5415
+ // $command 名前空間配下のパスは raw state を持たないため、proxy の get トラップと
5416
+ // 同じ namespace を辿る。1セグメント目は namespace 本体、2セグメント目以降は
5417
+ // namespace 上のキー (= 宣言済み command token 名) を順に走査する。
5418
+ let value = getCommandNamespace(stateElement);
5419
+ for (let i = 1; i < address.pathInfo.segments.length; i++) {
5420
+ if (value == null) {
5421
+ return undefined;
5422
+ }
5423
+ value = Reflect.get(value, address.pathInfo.segments[i]);
5424
+ }
5425
+ return value;
5426
+ }
5178
5427
  if (address.pathInfo.path in target) {
5179
5428
  // getterの中で参照の可能性があるので、addressをプッシュする
5180
5429
  if (stateElement.getterPaths.has(address.pathInfo.path)) {
@@ -6025,6 +6274,9 @@ function get(target, prop, receiver, handler) {
6025
6274
  return trackDependency(target, prop, receiver, handler)(path);
6026
6275
  };
6027
6276
  }
6277
+ case STATE_COMMAND_NAMESPACE_NAME: {
6278
+ return getCommandNamespace(handler.stateElement);
6279
+ }
6028
6280
  }
6029
6281
  }
6030
6282
  else {
@@ -6630,6 +6882,7 @@ class State extends HTMLElement {
6630
6882
  _boundComponent = null;
6631
6883
  _boundComponentStateProp = null;
6632
6884
  _bindableEventMap = {};
6885
+ _commandTokenNames = new Set();
6633
6886
  constructor() {
6634
6887
  super();
6635
6888
  this._initializePromise = new Promise((resolve) => {
@@ -6652,6 +6905,7 @@ class State extends HTMLElement {
6652
6905
  return this.__state;
6653
6906
  }
6654
6907
  set _state(value) {
6908
+ this._commandTokenNames = processCommandTokensDeclaration(value);
6655
6909
  this.__state = value;
6656
6910
  this._listPaths.clear();
6657
6911
  this._elementPaths.clear();
@@ -6864,6 +7118,8 @@ class State extends HTMLElement {
6864
7118
  if (this._rootNode !== null) {
6865
7119
  this._callStateDisconnectedCallback();
6866
7120
  setStateElementByName(this.rootNode, this._name, null);
7121
+ clearCommandTokenRegistry(this);
7122
+ clearCommandNamespace(this);
6867
7123
  this._rootNode = null;
6868
7124
  }
6869
7125
  }
@@ -6909,6 +7165,9 @@ class State extends HTMLElement {
6909
7165
  get bindableEventMap() {
6910
7166
  return this._bindableEventMap;
6911
7167
  }
7168
+ get commandTokenNames() {
7169
+ return this._commandTokenNames;
7170
+ }
6912
7171
  setBindableEventMap(map) {
6913
7172
  this._bindableEventMap = map;
6914
7173
  }