@wcstack/state 1.8.5 → 1.9.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/README.ja.md +38 -8
- package/README.md +144 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.esm.js +255 -9
- package/dist/index.esm.js.map +1 -1
- package/dist/index.esm.min.js +1 -1
- package/dist/index.esm.min.js.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -59,7 +59,7 @@ function setConfig(partialConfig) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
var version$1 = "1.
|
|
62
|
+
var version$1 = "1.9.0";
|
|
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 $commandToken or $commandTokens declaration).`);
|
|
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 経由で内容をクローン
|
|
@@ -6025,6 +6261,9 @@ function get(target, prop, receiver, handler) {
|
|
|
6025
6261
|
return trackDependency(target, prop, receiver, handler)(path);
|
|
6026
6262
|
};
|
|
6027
6263
|
}
|
|
6264
|
+
case STATE_COMMAND_NAMESPACE_NAME: {
|
|
6265
|
+
return getCommandNamespace(handler.stateElement);
|
|
6266
|
+
}
|
|
6028
6267
|
}
|
|
6029
6268
|
}
|
|
6030
6269
|
else {
|
|
@@ -6630,6 +6869,7 @@ class State extends HTMLElement {
|
|
|
6630
6869
|
_boundComponent = null;
|
|
6631
6870
|
_boundComponentStateProp = null;
|
|
6632
6871
|
_bindableEventMap = {};
|
|
6872
|
+
_commandTokenNames = new Set();
|
|
6633
6873
|
constructor() {
|
|
6634
6874
|
super();
|
|
6635
6875
|
this._initializePromise = new Promise((resolve) => {
|
|
@@ -6652,6 +6892,7 @@ class State extends HTMLElement {
|
|
|
6652
6892
|
return this.__state;
|
|
6653
6893
|
}
|
|
6654
6894
|
set _state(value) {
|
|
6895
|
+
this._commandTokenNames = processCommandTokensDeclaration(value);
|
|
6655
6896
|
this.__state = value;
|
|
6656
6897
|
this._listPaths.clear();
|
|
6657
6898
|
this._elementPaths.clear();
|
|
@@ -6864,6 +7105,8 @@ class State extends HTMLElement {
|
|
|
6864
7105
|
if (this._rootNode !== null) {
|
|
6865
7106
|
this._callStateDisconnectedCallback();
|
|
6866
7107
|
setStateElementByName(this.rootNode, this._name, null);
|
|
7108
|
+
clearCommandTokenRegistry(this);
|
|
7109
|
+
clearCommandNamespace(this);
|
|
6867
7110
|
this._rootNode = null;
|
|
6868
7111
|
}
|
|
6869
7112
|
}
|
|
@@ -6909,6 +7152,9 @@ class State extends HTMLElement {
|
|
|
6909
7152
|
get bindableEventMap() {
|
|
6910
7153
|
return this._bindableEventMap;
|
|
6911
7154
|
}
|
|
7155
|
+
get commandTokenNames() {
|
|
7156
|
+
return this._commandTokenNames;
|
|
7157
|
+
}
|
|
6912
7158
|
setBindableEventMap(map) {
|
|
6913
7159
|
this._bindableEventMap = map;
|
|
6914
7160
|
}
|