@wcstack/state 1.26.0 → 1.27.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 +100 -8
- package/README.md +103 -7
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +25 -0
- package/dist/index.esm.js +1209 -122
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.esm.js +159 -1
- package/dist/wcs-manifest.json +83 -0
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -82,47 +82,6 @@ function setConfig(partialConfig) {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
const bindingPromiseByNode = new WeakMap();
|
|
86
|
-
// resolve 済みマーク。エントリ未生成のまま resolve されたノードは、後から
|
|
87
|
-
// wait された時に「生成して即 resolve」で追いつく。
|
|
88
|
-
const resolvedNodes = new WeakSet();
|
|
89
|
-
let id$1 = 0;
|
|
90
|
-
function getInitializeBindingPromiseByNode(node) {
|
|
91
|
-
let bindingPromise = bindingPromiseByNode.get(node) || null;
|
|
92
|
-
if (bindingPromise !== null) {
|
|
93
|
-
return bindingPromise;
|
|
94
|
-
}
|
|
95
|
-
let resolveFn = undefined;
|
|
96
|
-
const promise = new Promise((resolve) => {
|
|
97
|
-
resolveFn = resolve;
|
|
98
|
-
});
|
|
99
|
-
bindingPromise = {
|
|
100
|
-
id: ++id$1,
|
|
101
|
-
promise,
|
|
102
|
-
resolve: resolveFn
|
|
103
|
-
};
|
|
104
|
-
bindingPromiseByNode.set(node, bindingPromise);
|
|
105
|
-
if (resolvedNodes.has(node)) {
|
|
106
|
-
bindingPromise.resolve();
|
|
107
|
-
}
|
|
108
|
-
return bindingPromise;
|
|
109
|
-
}
|
|
110
|
-
async function waitInitializeBinding(node) {
|
|
111
|
-
const bindingPromise = getInitializeBindingPromiseByNode(node);
|
|
112
|
-
await bindingPromise.promise;
|
|
113
|
-
}
|
|
114
|
-
function resolveInitializedBinding(node) {
|
|
115
|
-
// ホットパス: リスト行では全 subscriber ノードがここを通るが、await する消費者
|
|
116
|
-
// (boundComponent / shadowRoot host)はほぼ居ない。既存エントリが無ければ
|
|
117
|
-
// Promise+closure を生成せず resolve 済みマークだけ残す(15 万個級の割り当て削減)。
|
|
118
|
-
const existing = bindingPromiseByNode.get(node);
|
|
119
|
-
if (typeof existing !== "undefined") {
|
|
120
|
-
existing.resolve();
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
resolvedNodes.add(node);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
85
|
const DELIMITER = '.';
|
|
127
86
|
const WILDCARD = '*';
|
|
128
87
|
const MAX_WILDCARD_DEPTH = 128;
|
|
@@ -130,6 +89,23 @@ const MAX_LOOP_DEPTH = 128;
|
|
|
130
89
|
// 因果伝播(Phase 3)の 1 transaction あたり hop 上限。超過分の未処理 record は
|
|
131
90
|
// quarantine し(適用済みの値は戻さない)、updater から例外は投げない。
|
|
132
91
|
const MAX_PROPAGATION_HOPS = 32;
|
|
92
|
+
// `$watch` ハンドラ起点の書き込み連鎖の打ち切り深さ(docs/state-watch-hook-design.md §7-2)。
|
|
93
|
+
// watch ハンドラ内の書き込みは新しい microtask バッチを作るため MAX_PROPAGATION_HOPS の
|
|
94
|
+
// ガードが効かず、書き込み先が動的なので `$streams` のような静的な自己依存検出もできない。
|
|
95
|
+
// 値は MAX_PROPAGATION_HOPS と同値だが、別の打ち切り機構なので定数は共有しない。
|
|
96
|
+
const MAX_WATCH_CHAIN_DEPTH = 32;
|
|
97
|
+
// updater の drain 終了リスナーの実行順(昇順に呼ばれる。設計書 §3-2 層 1)。
|
|
98
|
+
// watch が先なのは、watch ハンドラの書き込みが同じバッチの stream restart 判定に
|
|
99
|
+
// 影響しないようにするため(watch → restart の一方向)。import 順に順序を持たせると
|
|
100
|
+
// 無関係な import 整理で静かに壊れるため、明示的な優先度で固定する。
|
|
101
|
+
//
|
|
102
|
+
// devtools が最も先なのは、`state:update-batch` が「そのバッチに何が載ったか」の
|
|
103
|
+
// 観測であり、watch / restart の副作用が乗る前の生の集合を報告すべきだから。
|
|
104
|
+
// 既定値 0 のまま暗黙に先頭へ入るのに任せず、意図として定数で固定する
|
|
105
|
+
// (docs/devtools-hook-protocol.md §4.3)。
|
|
106
|
+
const DEVTOOLS_LISTENER_PRIORITY = 0;
|
|
107
|
+
const WATCH_LISTENER_PRIORITY = 10;
|
|
108
|
+
const STREAM_LISTENER_PRIORITY = 20;
|
|
133
109
|
// data-wcs バインディング構文 `[prop][#mod]: [path][@state][|filter...]` の区切り文字(単一正本)。
|
|
134
110
|
// これらは「死守の壁(構文契約)」であり値は不変。manifest.syntax.delimiters で公開される。
|
|
135
111
|
const BINDING_SEPARATOR = ';'; // 複数バインディングの区切り
|
|
@@ -164,11 +140,53 @@ const STATE_COMMAND_NAMESPACE_NAME = "$command";
|
|
|
164
140
|
const STATE_EVENT_TOKENS_NAME = "$eventTokens";
|
|
165
141
|
const STATE_ON_NAME = "$on";
|
|
166
142
|
const STATE_STREAMS_NAME = "$streams";
|
|
143
|
+
const STATE_WATCH_NAME = "$watch";
|
|
167
144
|
const STATE_LIST_KEYS_NAME = "$listKeys";
|
|
168
145
|
const STATE_STREAM_STATUS_NAMESPACE_NAME = "$streamStatus";
|
|
169
146
|
const STATE_STREAM_ERROR_NAMESPACE_NAME = "$streamError";
|
|
170
147
|
const DCC_DEFINITION_ATTRIBUTE = "data-wc-definition";
|
|
171
148
|
|
|
149
|
+
const bindingPromiseByNode = new WeakMap();
|
|
150
|
+
// resolve 済みマーク。エントリ未生成のまま resolve されたノードは、後から
|
|
151
|
+
// wait された時に「生成して即 resolve」で追いつく。
|
|
152
|
+
const resolvedNodes = new WeakSet();
|
|
153
|
+
let id$1 = 0;
|
|
154
|
+
function getInitializeBindingPromiseByNode(node) {
|
|
155
|
+
let bindingPromise = bindingPromiseByNode.get(node) || null;
|
|
156
|
+
if (bindingPromise !== null) {
|
|
157
|
+
return bindingPromise;
|
|
158
|
+
}
|
|
159
|
+
let resolveFn = undefined;
|
|
160
|
+
const promise = new Promise((resolve) => {
|
|
161
|
+
resolveFn = resolve;
|
|
162
|
+
});
|
|
163
|
+
bindingPromise = {
|
|
164
|
+
id: ++id$1,
|
|
165
|
+
promise,
|
|
166
|
+
resolve: resolveFn
|
|
167
|
+
};
|
|
168
|
+
bindingPromiseByNode.set(node, bindingPromise);
|
|
169
|
+
if (resolvedNodes.has(node)) {
|
|
170
|
+
bindingPromise.resolve();
|
|
171
|
+
}
|
|
172
|
+
return bindingPromise;
|
|
173
|
+
}
|
|
174
|
+
async function waitInitializeBinding(node) {
|
|
175
|
+
const bindingPromise = getInitializeBindingPromiseByNode(node);
|
|
176
|
+
await bindingPromise.promise;
|
|
177
|
+
}
|
|
178
|
+
function resolveInitializedBinding(node) {
|
|
179
|
+
// ホットパス: リスト行では全 subscriber ノードがここを通るが、await する消費者
|
|
180
|
+
// (boundComponent / shadowRoot host)はほぼ居ない。既存エントリが無ければ
|
|
181
|
+
// Promise+closure を生成せず resolve 済みマークだけ残す(15 万個級の割り当て削減)。
|
|
182
|
+
const existing = bindingPromiseByNode.get(node);
|
|
183
|
+
if (typeof existing !== "undefined") {
|
|
184
|
+
existing.resolve();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
resolvedNodes.add(node);
|
|
188
|
+
}
|
|
189
|
+
|
|
172
190
|
const _cache$4 = new Map();
|
|
173
191
|
let id = 0;
|
|
174
192
|
function getPathInfo(path) {
|
|
@@ -683,6 +701,15 @@ function valueMustBeBoolean(fnName) {
|
|
|
683
701
|
function valueMustBeDate(fnName) {
|
|
684
702
|
raiseError(`filter ${fnName} requires a date value`);
|
|
685
703
|
}
|
|
704
|
+
/**
|
|
705
|
+
* Throws error when filter requires array value but non-array provided.
|
|
706
|
+
*
|
|
707
|
+
* @param fnName - Name of the filter function
|
|
708
|
+
* @returns Never returns (always throws)
|
|
709
|
+
*/
|
|
710
|
+
function valueMustBeArray(fnName) {
|
|
711
|
+
raiseError(`filter ${fnName} requires an array value`);
|
|
712
|
+
}
|
|
686
713
|
|
|
687
714
|
/**
|
|
688
715
|
* builtinFilters.ts
|
|
@@ -695,7 +722,7 @@ function valueMustBeDate(fnName) {
|
|
|
695
722
|
* - Designed for common use as both input and output filters
|
|
696
723
|
*
|
|
697
724
|
* Design points:
|
|
698
|
-
* - Comprehensive coverage of diverse filters: eq, ne, lt, gt, inc, fix, locale, uc, lc, cap, trim, slice, pad, int, float, round, date, time, ymd, falsy, truthy, defaults, boolean, number, string, null, etc.
|
|
725
|
+
* - Comprehensive coverage of diverse filters: eq, ne, lt, gt, inc, abs, clamp, fix, locale, uc, lc, cap, trim, slice, pad, truncate, join, int, float, round, percent, unit, date, time, ymd, hms, falsy, truthy, defaults, boolean, number, string, null, etc.
|
|
699
726
|
* - Rich type checking and error handling for option values
|
|
700
727
|
* - Centralized management of filter functions with FilterWithOptions type, easy to extend
|
|
701
728
|
* - Dynamic retrieval of filter functions from filter names and options via builtinFilterFn
|
|
@@ -928,6 +955,48 @@ const mod = (options) => {
|
|
|
928
955
|
return value % Number(opt);
|
|
929
956
|
};
|
|
930
957
|
};
|
|
958
|
+
/**
|
|
959
|
+
* Absolute value filter - returns the magnitude of a number.
|
|
960
|
+
*
|
|
961
|
+
* @param options - Unused
|
|
962
|
+
* @returns Filter function that returns the absolute value
|
|
963
|
+
*/
|
|
964
|
+
const abs = (_options) => {
|
|
965
|
+
return (value) => {
|
|
966
|
+
if (typeof value !== 'number') {
|
|
967
|
+
valueMustBeNumber('abs');
|
|
968
|
+
}
|
|
969
|
+
return Math.abs(value);
|
|
970
|
+
};
|
|
971
|
+
};
|
|
972
|
+
/**
|
|
973
|
+
* Clamp filter - constrains a number to the inclusive range [min, max].
|
|
974
|
+
*
|
|
975
|
+
* Saturating conversion in the same family as round/floor/ceil, so it stays on
|
|
976
|
+
* the wire rather than in state. Pairs with `unit` for style bindings:
|
|
977
|
+
* `style.width: ratio|clamp(0,1)|percent(0)`.
|
|
978
|
+
*
|
|
979
|
+
* @param options - Array with minimum as first element and maximum as second (both required)
|
|
980
|
+
* @returns Filter function that returns the clamped number
|
|
981
|
+
*/
|
|
982
|
+
const clamp = (options) => {
|
|
983
|
+
const opt1 = options?.[0] ?? optionsRequired('clamp');
|
|
984
|
+
if (!validateNumberString(opt1)) {
|
|
985
|
+
optionMustBeNumber('clamp');
|
|
986
|
+
}
|
|
987
|
+
const opt2 = options?.[1] ?? optionsRequired('clamp');
|
|
988
|
+
if (!validateNumberString(opt2)) {
|
|
989
|
+
optionMustBeNumber('clamp');
|
|
990
|
+
}
|
|
991
|
+
const min = Number(opt1);
|
|
992
|
+
const max = Number(opt2);
|
|
993
|
+
return (value) => {
|
|
994
|
+
if (typeof value !== 'number') {
|
|
995
|
+
valueMustBeNumber('clamp');
|
|
996
|
+
}
|
|
997
|
+
return Math.min(Math.max(value, min), max);
|
|
998
|
+
};
|
|
999
|
+
};
|
|
931
1000
|
/**
|
|
932
1001
|
* Fixed decimal filter - formats number to fixed decimal places.
|
|
933
1002
|
*
|
|
@@ -1194,6 +1263,76 @@ const percent = (options) => {
|
|
|
1194
1263
|
return `${(value * 100).toFixed(Number(opt))}%`;
|
|
1195
1264
|
};
|
|
1196
1265
|
};
|
|
1266
|
+
/**
|
|
1267
|
+
* Unit filter - appends a CSS unit (or any suffix) to the value.
|
|
1268
|
+
*
|
|
1269
|
+
* A number alone does nothing in CSS, so without this the unit has to be built in
|
|
1270
|
+
* state — which drags presentation into the source of truth, and in the worst case
|
|
1271
|
+
* forces a whole derived array just to carry `"42%"` strings.
|
|
1272
|
+
* `style.height: samples.*.cpu|clamp(0,100)|fix(0)|unit(%)` keeps it on the wire.
|
|
1273
|
+
*
|
|
1274
|
+
* Accepts strings as well as numbers **on purpose**: the useful chains run through
|
|
1275
|
+
* `fix` / `percent`, which already return strings. Rejecting non-numbers here would
|
|
1276
|
+
* break exactly the combination this filter exists for.
|
|
1277
|
+
*
|
|
1278
|
+
* `null` / `undefined` pass through untouched rather than becoming `"undefinedpx"`,
|
|
1279
|
+
* so the binding layer's "undefined skips the write, null clears" semantics survive.
|
|
1280
|
+
*
|
|
1281
|
+
* @param options - Array with the unit/suffix as first element (required)
|
|
1282
|
+
* @returns Filter function that returns the value with the unit appended
|
|
1283
|
+
*/
|
|
1284
|
+
const unit = (options) => {
|
|
1285
|
+
const opt = options?.[0] ?? optionsRequired('unit');
|
|
1286
|
+
return (value) => {
|
|
1287
|
+
if (value === null || typeof value === 'undefined') {
|
|
1288
|
+
return value;
|
|
1289
|
+
}
|
|
1290
|
+
return String(value) + opt;
|
|
1291
|
+
};
|
|
1292
|
+
};
|
|
1293
|
+
/**
|
|
1294
|
+
* Join filter - joins array elements into a string.
|
|
1295
|
+
*
|
|
1296
|
+
* The default separator is `", "` rather than `","`: a bare comma is what `String()`
|
|
1297
|
+
* already produces without any filter, so defaulting to it would make `|join` a no-op.
|
|
1298
|
+
*
|
|
1299
|
+
* @param options - Array with separator as first element (default: ', ')
|
|
1300
|
+
* @returns Filter function that returns the joined string
|
|
1301
|
+
*/
|
|
1302
|
+
const join = (options) => {
|
|
1303
|
+
const opt = options?.[0] ?? ', ';
|
|
1304
|
+
return (value) => {
|
|
1305
|
+
if (!Array.isArray(value)) {
|
|
1306
|
+
valueMustBeArray('join');
|
|
1307
|
+
}
|
|
1308
|
+
return value.join(opt);
|
|
1309
|
+
};
|
|
1310
|
+
};
|
|
1311
|
+
/**
|
|
1312
|
+
* Truncate filter - shortens a string and appends an ellipsis.
|
|
1313
|
+
*
|
|
1314
|
+
* The length option counts **kept characters**, not the total including the suffix,
|
|
1315
|
+
* matching the existing `slice(0, n)` reading. A string at or below the limit is
|
|
1316
|
+
* returned untouched (no suffix).
|
|
1317
|
+
*
|
|
1318
|
+
* @param options - Array with max kept length as first element and suffix as second (default: '…')
|
|
1319
|
+
* @returns Filter function that returns the truncated string
|
|
1320
|
+
*/
|
|
1321
|
+
const truncate = (options) => {
|
|
1322
|
+
const opt1 = options?.[0] ?? optionsRequired('truncate');
|
|
1323
|
+
if (!validateNumberString(opt1)) {
|
|
1324
|
+
optionMustBeNumber('truncate');
|
|
1325
|
+
}
|
|
1326
|
+
const maxLength = Number(opt1);
|
|
1327
|
+
const suffix = options?.[1] ?? '…';
|
|
1328
|
+
return (value) => {
|
|
1329
|
+
const v = String(value);
|
|
1330
|
+
if (v.length <= maxLength) {
|
|
1331
|
+
return v;
|
|
1332
|
+
}
|
|
1333
|
+
return v.slice(0, maxLength) + suffix;
|
|
1334
|
+
};
|
|
1335
|
+
};
|
|
1197
1336
|
/**
|
|
1198
1337
|
* Date filter - formats Date object as localized date string.
|
|
1199
1338
|
*
|
|
@@ -1257,6 +1396,27 @@ const ymd = (options) => {
|
|
|
1257
1396
|
return `${year}${opt}${month}${opt}${day}`;
|
|
1258
1397
|
};
|
|
1259
1398
|
};
|
|
1399
|
+
/**
|
|
1400
|
+
* Hour-Minute-Second filter - formats Date object as HH:MM:SS string.
|
|
1401
|
+
*
|
|
1402
|
+
* The counterpart of `ymd`: a fixed, zero-padded, locale-independent rendering with a
|
|
1403
|
+
* configurable separator, for when `time` (locale-formatted) is not stable enough.
|
|
1404
|
+
*
|
|
1405
|
+
* @param options - Array with separator string as first element (default: ':')
|
|
1406
|
+
* @returns Filter function that returns formatted time string
|
|
1407
|
+
*/
|
|
1408
|
+
const hms = (options) => {
|
|
1409
|
+
const opt = options?.[0] ?? ':';
|
|
1410
|
+
return (value) => {
|
|
1411
|
+
if (!(value instanceof Date)) {
|
|
1412
|
+
valueMustBeDate('hms');
|
|
1413
|
+
}
|
|
1414
|
+
const hours = value.getHours().toString().padStart(2, '0');
|
|
1415
|
+
const minutes = value.getMinutes().toString().padStart(2, '0');
|
|
1416
|
+
const seconds = value.getSeconds().toString().padStart(2, '0');
|
|
1417
|
+
return `${hours}${opt}${minutes}${opt}${seconds}`;
|
|
1418
|
+
};
|
|
1419
|
+
};
|
|
1260
1420
|
/**
|
|
1261
1421
|
* Falsy filter - checks if value is falsy.
|
|
1262
1422
|
*
|
|
@@ -1347,6 +1507,8 @@ const builtinFilters = {
|
|
|
1347
1507
|
"mul": mul,
|
|
1348
1508
|
"div": div,
|
|
1349
1509
|
"mod": mod,
|
|
1510
|
+
"abs": abs,
|
|
1511
|
+
"clamp": clamp,
|
|
1350
1512
|
"fix": fix,
|
|
1351
1513
|
"locale": locale,
|
|
1352
1514
|
"uc": uc,
|
|
@@ -1358,16 +1520,20 @@ const builtinFilters = {
|
|
|
1358
1520
|
"pad": pad,
|
|
1359
1521
|
"rep": rep,
|
|
1360
1522
|
"rev": rev,
|
|
1523
|
+
"truncate": truncate,
|
|
1524
|
+
"join": join,
|
|
1361
1525
|
"int": int,
|
|
1362
1526
|
"float": float,
|
|
1363
1527
|
"round": round,
|
|
1364
1528
|
"floor": floor,
|
|
1365
1529
|
"ceil": ceil,
|
|
1366
1530
|
"percent": percent,
|
|
1531
|
+
"unit": unit,
|
|
1367
1532
|
"date": date,
|
|
1368
1533
|
"time": time,
|
|
1369
1534
|
"datetime": datetime,
|
|
1370
1535
|
"ymd": ymd,
|
|
1536
|
+
"hms": hms,
|
|
1371
1537
|
"falsy": falsy,
|
|
1372
1538
|
"truthy": truthy,
|
|
1373
1539
|
"defaults": defaults,
|
|
@@ -1397,11 +1563,44 @@ const builtinFilterFn = (name, options) => (filters) => {
|
|
|
1397
1563
|
return filter(options);
|
|
1398
1564
|
};
|
|
1399
1565
|
|
|
1566
|
+
/**
|
|
1567
|
+
* フィルタ引数リストのパース。`filter(a, b)` の `a, b` 部分を受け取る。
|
|
1568
|
+
*
|
|
1569
|
+
* トリムの規則は「**クォートの外側だけ**」。`fix( 2 )` のような書き癖を吸収するために
|
|
1570
|
+
* 素の引数は前後をトリムするが、クォートは「ここは literal」という宣言なので中身の
|
|
1571
|
+
* 空白は残す。両方まとめてトリムしていたため `pad(5, ' ')` が空文字パディング
|
|
1572
|
+
* (=無変化)に化けており、空白区切りの `join(' / ')` も指定できなかった。
|
|
1573
|
+
*/
|
|
1574
|
+
/** 引数 1 つを確定する。クォート由来の文字が入った範囲より外側だけをトリムする。 */
|
|
1575
|
+
function finalizeArg(text, firstQuoteStart, lastQuoteEnd) {
|
|
1576
|
+
// 先頭側: 最初のクォート文字より前だけが削れる(クォートが無ければ全体が対象)
|
|
1577
|
+
const startLimit = firstQuoteStart === -1 ? text.length : firstQuoteStart;
|
|
1578
|
+
let start = 0;
|
|
1579
|
+
while (start < startLimit && /\s/.test(text[start])) {
|
|
1580
|
+
start++;
|
|
1581
|
+
}
|
|
1582
|
+
// 末尾側: 最後のクォート文字より後ろだけが削れる(クォートが無ければ全体が対象)
|
|
1583
|
+
const endLimit = lastQuoteEnd === -1 ? 0 : lastQuoteEnd;
|
|
1584
|
+
let end = text.length;
|
|
1585
|
+
while (end > endLimit && /\s/.test(text[end - 1])) {
|
|
1586
|
+
end--;
|
|
1587
|
+
}
|
|
1588
|
+
return text.slice(start, end);
|
|
1589
|
+
}
|
|
1400
1590
|
function parseFilterArgs(argsText) {
|
|
1401
1591
|
const args = [];
|
|
1402
1592
|
let current = '';
|
|
1403
1593
|
let inQuote = null;
|
|
1404
1594
|
let hasQuote = false;
|
|
1595
|
+
let firstQuoteStart = -1;
|
|
1596
|
+
let lastQuoteEnd = -1;
|
|
1597
|
+
const flush = () => {
|
|
1598
|
+
args.push(finalizeArg(current, firstQuoteStart, lastQuoteEnd));
|
|
1599
|
+
current = '';
|
|
1600
|
+
hasQuote = false;
|
|
1601
|
+
firstQuoteStart = -1;
|
|
1602
|
+
lastQuoteEnd = -1;
|
|
1603
|
+
};
|
|
1405
1604
|
for (let i = 0; i < argsText.length; i++) {
|
|
1406
1605
|
const char = argsText[i];
|
|
1407
1606
|
if (inQuote) {
|
|
@@ -1409,7 +1608,11 @@ function parseFilterArgs(argsText) {
|
|
|
1409
1608
|
inQuote = null;
|
|
1410
1609
|
}
|
|
1411
1610
|
else {
|
|
1611
|
+
if (firstQuoteStart === -1) {
|
|
1612
|
+
firstQuoteStart = current.length;
|
|
1613
|
+
}
|
|
1412
1614
|
current += char;
|
|
1615
|
+
lastQuoteEnd = current.length;
|
|
1413
1616
|
}
|
|
1414
1617
|
}
|
|
1415
1618
|
else if (char === '"' || char === "'") {
|
|
@@ -1417,15 +1620,13 @@ function parseFilterArgs(argsText) {
|
|
|
1417
1620
|
hasQuote = true;
|
|
1418
1621
|
}
|
|
1419
1622
|
else if (char === ',') {
|
|
1420
|
-
|
|
1421
|
-
current = '';
|
|
1422
|
-
hasQuote = false;
|
|
1623
|
+
flush();
|
|
1423
1624
|
}
|
|
1424
1625
|
else {
|
|
1425
1626
|
current += char;
|
|
1426
1627
|
}
|
|
1427
1628
|
}
|
|
1428
|
-
const last = current
|
|
1629
|
+
const last = finalizeArg(current, firstQuoteStart, lastQuoteEnd);
|
|
1429
1630
|
if (last || hasQuote) {
|
|
1430
1631
|
args.push(last);
|
|
1431
1632
|
}
|
|
@@ -1778,13 +1979,97 @@ function getParseBindTextResults(node) {
|
|
|
1778
1979
|
return [];
|
|
1779
1980
|
}
|
|
1780
1981
|
|
|
1982
|
+
/**
|
|
1983
|
+
* bindings/lightDomComponentScope.ts — Light DOM の mapped `bind-component` を
|
|
1984
|
+
* 「ホストとは別のバインディングスコープ」として扱うための判定
|
|
1985
|
+
* (docs/architecture-hardening/15-state-component-mechanism-consistency.md §1.13)。
|
|
1986
|
+
*
|
|
1987
|
+
* Shadow DOM 形では、コンポーネントの `<wcs-state>` が**別 rootNode** にいることで
|
|
1988
|
+
* 2 つのことが同時に成立している。
|
|
1989
|
+
*
|
|
1990
|
+
* 1. ホスト root の `waitForStateInitialize` の走査集合に入らない
|
|
1991
|
+
* 2. 子スコープのバインディングがホストとは別の `buildBindings` パスで処理される
|
|
1992
|
+
*
|
|
1993
|
+
* Light DOM では両方が失われる。1 が失われると、
|
|
1994
|
+
* 「ホストの `waitForStateInitialize` が子 state を待つ →
|
|
1995
|
+
* 子 state は自分を束ねるホスト binding を待つ →
|
|
1996
|
+
* その binding を作る `initializeBindings` は `waitForStateInitialize` の後」
|
|
1997
|
+
* という循環になり、初期化が永久に解決しない。2 が失われると、子スコープの
|
|
1998
|
+
* `@name` 参照がホストと同じパスで解決されてしまい、子 state の名前登録より
|
|
1999
|
+
* 先に評価される。
|
|
2000
|
+
*
|
|
2001
|
+
* このモジュールはその 2 つを明示的に復元するための判定だけを持つ。
|
|
2002
|
+
*
|
|
2003
|
+
* **plain(ホストからバインドしない state 注入)は対象外**であることに注意。
|
|
2004
|
+
* plain は `waitInitializeBinding` を通らないので循環せず、従来どおりホストと
|
|
2005
|
+
* 同じパスで初期化して問題ない。ここで一律に切り出すと、成立している plain 形が
|
|
2006
|
+
* 「子 state の登録前に `@name` を解決する」形に退行する。
|
|
2007
|
+
*/
|
|
2008
|
+
/** `<wcs-state bind-component>` が Light DOM の mapped 形(=別スコープ扱い)か。 */
|
|
2009
|
+
function isLightDomMappedStateElement(stateElement) {
|
|
2010
|
+
if (!stateElement.hasAttribute("bind-component")) {
|
|
2011
|
+
return false;
|
|
2012
|
+
}
|
|
2013
|
+
const parentNode = stateElement.parentNode;
|
|
2014
|
+
// Shadow DOM 形では parentNode が ShadowRoot になる(かつホスト root の
|
|
2015
|
+
// querySelectorAll にはそもそも出てこない)
|
|
2016
|
+
if (!(parentNode instanceof Element)) {
|
|
2017
|
+
return false;
|
|
2018
|
+
}
|
|
2019
|
+
// ホストからバインドされていなければ plain。従来どおりの扱いに任せる
|
|
2020
|
+
return parentNode.hasAttribute(config.bindAttributeName);
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* `root` の内側にある Light DOM mapped コンポーネント要素を集める。
|
|
2024
|
+
*
|
|
2025
|
+
* `root` 自身は**含めない**。子スコープが自分のパスとして
|
|
2026
|
+
* `initializeBindings(componentElement)` を呼ぶとき、その要素自身まで prune すると
|
|
2027
|
+
* 何も初期化されなくなるため。
|
|
2028
|
+
*/
|
|
2029
|
+
function findNestedLightDomComponents(root) {
|
|
2030
|
+
const components = [];
|
|
2031
|
+
const stateElements = root.querySelectorAll(`${config.tagNames.state}[bind-component]`);
|
|
2032
|
+
for (const stateElement of stateElements) {
|
|
2033
|
+
if (!isLightDomMappedStateElement(stateElement)) {
|
|
2034
|
+
continue;
|
|
2035
|
+
}
|
|
2036
|
+
const component = stateElement.parentNode;
|
|
2037
|
+
if (component === root) {
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
components.push(component);
|
|
2041
|
+
}
|
|
2042
|
+
return components;
|
|
2043
|
+
}
|
|
2044
|
+
/** `node` が、いずれかのコンポーネント要素の**真の**子孫か。 */
|
|
2045
|
+
function isInsideAnyComponent(node, components) {
|
|
2046
|
+
for (let i = 0; i < components.length; i++) {
|
|
2047
|
+
const component = components[i];
|
|
2048
|
+
if (component !== node && component.contains(node)) {
|
|
2049
|
+
return true;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
return false;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
1781
2055
|
/**
|
|
1782
2056
|
* data-wcs 属性または埋め込みノード<!--{{}}-->を持つノードをすべて取得する
|
|
2057
|
+
*
|
|
2058
|
+
* Light DOM の mapped コンポーネントの**内側**は除外する(§1.13)。そのサブツリーの
|
|
2059
|
+
* `@name` 参照は、コンポーネント側の state が名前登録を済ませてからでないと解決できず、
|
|
2060
|
+
* ホストと同じパスで拾うと登録前に評価されてしまう。除外したぶんは、その state が
|
|
2061
|
+
* 初期化を終えた時点で自分のスコープとして `initializeBindings(componentElement)` を
|
|
2062
|
+
* 呼び直す(Shadow DOM 形で rootNode ごとにパスが分かれるのと同じ形にする)。
|
|
2063
|
+
*
|
|
2064
|
+
* コンポーネント要素**自身**は除外しない。ホスト側の `data-wcs`(`state.msg: user.name`)
|
|
2065
|
+
* はホストのスコープに属し、それが張られることで子側の待ちが解ける。
|
|
2066
|
+
*
|
|
1783
2067
|
* @param root
|
|
1784
2068
|
* @returns
|
|
1785
2069
|
*/
|
|
1786
2070
|
function getSubscriberNodes(root) {
|
|
1787
2071
|
const subscriberNodes = [];
|
|
2072
|
+
const nestedComponents = findNestedLightDomComponents(root);
|
|
1788
2073
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT, {
|
|
1789
2074
|
acceptNode(node) {
|
|
1790
2075
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
@@ -1803,7 +2088,14 @@ function getSubscriberNodes(root) {
|
|
|
1803
2088
|
}
|
|
1804
2089
|
});
|
|
1805
2090
|
while (walker.nextNode()) {
|
|
1806
|
-
|
|
2091
|
+
const node = walker.currentNode;
|
|
2092
|
+
// TreeWalker の acceptNode は「自分は拾うが子孫は辿らない」を表現できないため、
|
|
2093
|
+
// コンポーネント要素自身を拾ったうえで、その子孫をここで落とす。
|
|
2094
|
+
// nestedComponents が空(=圧倒的多数)のときは contains 走査ごと発生しない。
|
|
2095
|
+
if (nestedComponents.length > 0 && isInsideAnyComponent(node, nestedComponents)) {
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
subscriberNodes.push(node);
|
|
1807
2099
|
}
|
|
1808
2100
|
return subscriberNodes;
|
|
1809
2101
|
}
|
|
@@ -2642,12 +2934,12 @@ class EventToken extends Token {
|
|
|
2642
2934
|
}
|
|
2643
2935
|
}
|
|
2644
2936
|
|
|
2645
|
-
const registryByStateElement$
|
|
2937
|
+
const registryByStateElement$3 = new WeakMap();
|
|
2646
2938
|
function getOrCreateEventToken(stateElement, name) {
|
|
2647
|
-
let registry = registryByStateElement$
|
|
2939
|
+
let registry = registryByStateElement$3.get(stateElement);
|
|
2648
2940
|
if (typeof registry === "undefined") {
|
|
2649
2941
|
registry = new Map();
|
|
2650
|
-
registryByStateElement$
|
|
2942
|
+
registryByStateElement$3.set(stateElement, registry);
|
|
2651
2943
|
}
|
|
2652
2944
|
let token = registry.get(name);
|
|
2653
2945
|
if (typeof token === "undefined") {
|
|
@@ -2657,7 +2949,7 @@ function getOrCreateEventToken(stateElement, name) {
|
|
|
2657
2949
|
return token;
|
|
2658
2950
|
}
|
|
2659
2951
|
function clearEventTokenRegistry(stateElement) {
|
|
2660
|
-
registryByStateElement$
|
|
2952
|
+
registryByStateElement$3.delete(stateElement);
|
|
2661
2953
|
}
|
|
2662
2954
|
|
|
2663
2955
|
/**
|
|
@@ -3401,6 +3693,42 @@ function detachTwowayEventHandler(binding) {
|
|
|
3401
3693
|
}
|
|
3402
3694
|
}
|
|
3403
3695
|
|
|
3696
|
+
const stateElementByWebComponent = new WeakMap();
|
|
3697
|
+
function setStateElementByWebComponent(webComponent, stateName, stateElement) {
|
|
3698
|
+
let stateMap = stateElementByWebComponent.get(webComponent);
|
|
3699
|
+
if (!stateMap) {
|
|
3700
|
+
stateMap = new Map();
|
|
3701
|
+
stateElementByWebComponent.set(webComponent, stateMap);
|
|
3702
|
+
}
|
|
3703
|
+
stateMap.set(stateName, stateElement);
|
|
3704
|
+
}
|
|
3705
|
+
function getStateElementByWebComponent(webComponent, stateName) {
|
|
3706
|
+
const stateMap = stateElementByWebComponent.get(webComponent);
|
|
3707
|
+
if (!stateMap) {
|
|
3708
|
+
return null;
|
|
3709
|
+
}
|
|
3710
|
+
return stateMap.get(stateName) ?? null;
|
|
3711
|
+
}
|
|
3712
|
+
/**
|
|
3713
|
+
* コンポーネントが mapped されている「1 つ外のスコープ」の state 要素。
|
|
3714
|
+
* `buildPrimaryMappingRule` がプライマリ規則から記録する
|
|
3715
|
+
* (規則の outer 側が属する state 要素 = 値の正本を持つスコープそのもの)。
|
|
3716
|
+
*
|
|
3717
|
+
* 用途は Δ(base listIndex)の境界越え合成(§1.12)。`getLoopContextByNode` は
|
|
3718
|
+
* `parentNode` しか辿らず shadow 境界を越えないため、Δ を外へ引き継ぐには
|
|
3719
|
+
* 「1 つ外のスコープ」への明示的なリンクが要る。
|
|
3720
|
+
*
|
|
3721
|
+
* この台帳を MappingRule ではなくここに置くのは循環参照を避けるため
|
|
3722
|
+
* (baseListIndex → MappingRule → BindingSession → outerListPath → baseListIndex)。
|
|
3723
|
+
*/
|
|
3724
|
+
const outerStateElementByWebComponent = new WeakMap();
|
|
3725
|
+
function setOuterStateElementByWebComponent(webComponent, stateElement) {
|
|
3726
|
+
outerStateElementByWebComponent.set(webComponent, stateElement);
|
|
3727
|
+
}
|
|
3728
|
+
function getOuterStateElementByWebComponent(webComponent) {
|
|
3729
|
+
return outerStateElementByWebComponent.get(webComponent) ?? null;
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3404
3732
|
/**
|
|
3405
3733
|
* webComponent/baseListIndex.ts
|
|
3406
3734
|
*
|
|
@@ -3423,19 +3751,49 @@ function detachTwowayEventHandler(binding) {
|
|
|
3423
3751
|
* ホットパスに walk は載らない。
|
|
3424
3752
|
*/
|
|
3425
3753
|
function getBaseListIndex(stateElement) {
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3754
|
+
let current = stateElement;
|
|
3755
|
+
for (;;) {
|
|
3756
|
+
if (current == null || current.hasMappedComponentState !== true) {
|
|
3757
|
+
return null;
|
|
3758
|
+
}
|
|
3759
|
+
const component = current.boundComponent;
|
|
3760
|
+
if (component == null) {
|
|
3761
|
+
return null;
|
|
3762
|
+
}
|
|
3763
|
+
const listIndex = getLoopContextByNode(component)?.listIndex;
|
|
3764
|
+
if (listIndex != null) {
|
|
3765
|
+
return listIndex;
|
|
3766
|
+
}
|
|
3767
|
+
// このスコープには囲むループが無い。コンポーネントがさらに別の mapped な
|
|
3768
|
+
// コンポーネントの shadow の中にいるなら、Δ は外側スコープから引き継ぐ(§1.12)。
|
|
3769
|
+
//
|
|
3770
|
+
// `getLoopContextByNode` は `parentNode` しか辿らず、ShadowRoot の parentNode は
|
|
3771
|
+
// null なので shadow 境界で必ず止まる。境界 1 枚なら外側は素の文書スコープで
|
|
3772
|
+
// Δ=0 が正しいが、2 枚重なっていると中間スコープの Δ が丸ごと落ちて
|
|
3773
|
+
// 子の listIndex が正本スコープより浅い arity で作られる。
|
|
3774
|
+
//
|
|
3775
|
+
// 外側が mapped でない(=値の正本がそのスコープにある)なら、そこから先の
|
|
3776
|
+
// ループはこの子のリストとは無関係なので次の周回の先頭ガードで止まる。
|
|
3777
|
+
current = getOuterStateElementByWebComponent(component);
|
|
3432
3778
|
}
|
|
3433
|
-
return getLoopContextByNode(component)?.listIndex ?? null;
|
|
3434
3779
|
}
|
|
3435
3780
|
/** base の段数 Δ。base が無ければ 0。 */
|
|
3436
3781
|
function getBaseDepth(stateElement) {
|
|
3437
3782
|
return getBaseListIndex(stateElement)?.length ?? 0;
|
|
3438
3783
|
}
|
|
3784
|
+
/**
|
|
3785
|
+
* そのスコープでそのパスに実際に使われる listIndex の arity。
|
|
3786
|
+
*
|
|
3787
|
+
* パス自身のワイルドカード段数に、そのスコープが外側のループの内側にいる分(Δ)を
|
|
3788
|
+
* 足したもの。`items.*` は子スコープから見れば 1 段でも、そのスコープが Δ=1 の位置に
|
|
3789
|
+
* あれば台帳の listIndex は arity 2 になる(§1.10)。
|
|
3790
|
+
*
|
|
3791
|
+
* **境界を跨ぐ照合はこの実 arity どうしで行うこと**(§1.12)。片側だけ Δ を足すと、
|
|
3792
|
+
* 境界が 2 枚以上あるときに中間スコープの Δ を二重計上して不一致になる。
|
|
3793
|
+
*/
|
|
3794
|
+
function getScopeArity(stateElement, pathInfo) {
|
|
3795
|
+
return pathInfo.wildcardCount + getBaseDepth(stateElement);
|
|
3796
|
+
}
|
|
3439
3797
|
/**
|
|
3440
3798
|
* リストの行を生成するときの親 listIndex。
|
|
3441
3799
|
*
|
|
@@ -3451,23 +3809,6 @@ function getListParentListIndex(stateElement, containerListIndex) {
|
|
|
3451
3809
|
return containerListIndex ?? getBaseListIndex(stateElement);
|
|
3452
3810
|
}
|
|
3453
3811
|
|
|
3454
|
-
const stateElementByWebComponent = new WeakMap();
|
|
3455
|
-
function setStateElementByWebComponent(webComponent, stateName, stateElement) {
|
|
3456
|
-
let stateMap = stateElementByWebComponent.get(webComponent);
|
|
3457
|
-
if (!stateMap) {
|
|
3458
|
-
stateMap = new Map();
|
|
3459
|
-
stateElementByWebComponent.set(webComponent, stateMap);
|
|
3460
|
-
}
|
|
3461
|
-
stateMap.set(stateName, stateElement);
|
|
3462
|
-
}
|
|
3463
|
-
function getStateElementByWebComponent(webComponent, stateName) {
|
|
3464
|
-
const stateMap = stateElementByWebComponent.get(webComponent);
|
|
3465
|
-
if (!stateMap) {
|
|
3466
|
-
return null;
|
|
3467
|
-
}
|
|
3468
|
-
return stateMap.get(stateName) ?? null;
|
|
3469
|
-
}
|
|
3470
|
-
|
|
3471
3812
|
const innerMappingByElement = new WeakMap();
|
|
3472
3813
|
const outerMappingByElement = new WeakMap();
|
|
3473
3814
|
const primaryMappingRuleSetByElement = new WeakMap();
|
|
@@ -3503,6 +3844,10 @@ function buildPrimaryMappingRule(webComponent, stateName, bindings) {
|
|
|
3503
3844
|
primaryBindingByMappingRule.set(mappingRule, binding);
|
|
3504
3845
|
innerMappingRule.set(innerAbsPathInfo, outerAbsPathInfo);
|
|
3505
3846
|
outerMappingRule.set(outerAbsPathInfo, innerAbsPathInfo);
|
|
3847
|
+
// 1 つ外のスコープへのリンク。Δ の境界越え合成(§1.12)が引く。
|
|
3848
|
+
// プライマリ規則はすべて同じホスト要素の data-wcs 由来なので、どの規則から
|
|
3849
|
+
// 採っても同じスコープを指す。
|
|
3850
|
+
setOuterStateElementByWebComponent(webComponent, outerAbsPathInfo.stateElement);
|
|
3506
3851
|
}
|
|
3507
3852
|
innerMappingByElement.set(webComponent, innerMappingRule);
|
|
3508
3853
|
outerMappingByElement.set(webComponent, outerMappingRule);
|
|
@@ -3728,12 +4073,56 @@ function getOuterRowPathInfo(innerStateElement, innerPathInfo) {
|
|
|
3728
4073
|
if (innerPathInfo.wildcardCount === 0) {
|
|
3729
4074
|
return null;
|
|
3730
4075
|
}
|
|
4076
|
+
return stepOuterRowPathInfo(innerStateElement, innerPathInfo);
|
|
4077
|
+
}
|
|
4078
|
+
/**
|
|
4079
|
+
* `getOuterRowPathInfo` の 2 段目以降。境界が 2 枚以上重なっている(コンポーネントの
|
|
4080
|
+
* shadow の中にさらに mapped な `bind-component` がある)場合、値の正本は 1 つ外では
|
|
4081
|
+
* なく**最も外のスコープ**にある。1 段目だけに相乗りしていると、中間スコープは
|
|
4082
|
+
* 素通しで自分の行バインディングを持たないため、正本スコープ起点の行フィールド
|
|
4083
|
+
* 書き込みを購読する者が誰もいなくなる(§1.11)。
|
|
4084
|
+
*
|
|
4085
|
+
* 1 段目が成立したときだけ呼ばれる(=mapped な行バインディング限定)ので、
|
|
4086
|
+
* 通常のリストはこの walk を一切踏まない。返り値は 2 段目以降が無ければ `null` で、
|
|
4087
|
+
* 圧倒的多数である深さ 1 の行では配列を確保しない。
|
|
4088
|
+
*
|
|
4089
|
+
* 各段で必ず外側の state 要素へ進む(`resolveOuterAbsolutePathInfo` は
|
|
4090
|
+
* `boundComponent` の属するスコープを返す=DOM 上の真の祖先)ので停止する。
|
|
4091
|
+
* `propagateListPathToOuterState` の外向き伝播と同じ論拠。
|
|
4092
|
+
*/
|
|
4093
|
+
function getOuterRowPathInfosBeyond(firstOuterAbsPathInfo) {
|
|
4094
|
+
let rest = null;
|
|
4095
|
+
let stateElement = firstOuterAbsPathInfo.stateElement;
|
|
4096
|
+
let pathInfo = firstOuterAbsPathInfo.pathInfo;
|
|
4097
|
+
for (;;) {
|
|
4098
|
+
const outerAbsPathInfo = stepOuterRowPathInfo(stateElement, pathInfo);
|
|
4099
|
+
if (outerAbsPathInfo === null) {
|
|
4100
|
+
return rest;
|
|
4101
|
+
}
|
|
4102
|
+
(rest ??= []).push(outerAbsPathInfo);
|
|
4103
|
+
stateElement = outerAbsPathInfo.stateElement;
|
|
4104
|
+
pathInfo = outerAbsPathInfo.pathInfo;
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
/**
|
|
4108
|
+
* 境界 1 枚分の外向き解決。成立条件の判定を含む。
|
|
4109
|
+
*
|
|
4110
|
+
* 判定は**両側の実 arity(`getScopeArity` = パスの段数 + そのスコープの Δ)が
|
|
4111
|
+
* 一致すること**。相乗り登録は子の listIndex をそのまま鍵に使うので、外側スコープが
|
|
4112
|
+
* その arity で台帳を引けなければ意味がない。
|
|
4113
|
+
*
|
|
4114
|
+
* 境界 1 枚なら外側は Δ=0 なので、これは従来の `Δ + innerW === outerW` と同値。
|
|
4115
|
+
* 2 枚以上あるときに外側の Δ を数えないと、中間スコープの Δ を二重計上して
|
|
4116
|
+
* 成立するはずの段を落とす(§1.12)。
|
|
4117
|
+
*/
|
|
4118
|
+
function stepOuterRowPathInfo(innerStateElement, innerPathInfo) {
|
|
3731
4119
|
const outerAbsPathInfo = resolveOuterAbsolutePathInfo(innerStateElement, innerPathInfo);
|
|
3732
4120
|
if (outerAbsPathInfo === null || outerAbsPathInfo.stateElement === innerStateElement) {
|
|
3733
4121
|
return null;
|
|
3734
4122
|
}
|
|
3735
|
-
const
|
|
3736
|
-
|
|
4123
|
+
const innerArity = getScopeArity(innerStateElement, innerPathInfo);
|
|
4124
|
+
const outerArity = getScopeArity(outerAbsPathInfo.stateElement, outerAbsPathInfo.pathInfo);
|
|
4125
|
+
if (innerArity !== outerArity) {
|
|
3737
4126
|
return null;
|
|
3738
4127
|
}
|
|
3739
4128
|
return outerAbsPathInfo;
|
|
@@ -4552,6 +4941,7 @@ class BindingSession {
|
|
|
4552
4941
|
patternPathInfo: null,
|
|
4553
4942
|
patternListIndex: null,
|
|
4554
4943
|
outerPatternPathInfo: null,
|
|
4944
|
+
outerPatternPathInfosRest: null,
|
|
4555
4945
|
pendingDefinitions: 0,
|
|
4556
4946
|
initialPolicy: slot.policy,
|
|
4557
4947
|
resolvedAuthority: slot.authority,
|
|
@@ -4668,6 +5058,7 @@ class BindingSession {
|
|
|
4668
5058
|
patternPathInfo: null,
|
|
4669
5059
|
patternListIndex: null,
|
|
4670
5060
|
outerPatternPathInfo: null,
|
|
5061
|
+
outerPatternPathInfosRest: null,
|
|
4671
5062
|
pendingDefinitions: 0,
|
|
4672
5063
|
initialPolicy: null,
|
|
4673
5064
|
resolvedAuthority: null,
|
|
@@ -4884,6 +5275,17 @@ class BindingSession {
|
|
|
4884
5275
|
if (outerPathInfo !== null) {
|
|
4885
5276
|
addBindingByPattern(outerPathInfo, listIndex, binding);
|
|
4886
5277
|
record.outerPatternPathInfo = outerPathInfo;
|
|
5278
|
+
// 境界が 2 枚以上重なっていると、値の正本は 1 つ外ではなく最も外のスコープに
|
|
5279
|
+
// ある。中間スコープは配列を素通しするだけで自分の行バインディングを持たない
|
|
5280
|
+
// ため、1 段目だけでは正本スコープ起点の行フィールド書き込みが誰にも届かない
|
|
5281
|
+
// (§1.11)。成立する段すべてに載せる。
|
|
5282
|
+
const restPathInfos = getOuterRowPathInfosBeyond(outerPathInfo);
|
|
5283
|
+
if (restPathInfos !== null) {
|
|
5284
|
+
for (let i = 0; i < restPathInfos.length; i++) {
|
|
5285
|
+
addBindingByPattern(restPathInfos[i], listIndex, binding);
|
|
5286
|
+
}
|
|
5287
|
+
record.outerPatternPathInfosRest = restPathInfos;
|
|
5288
|
+
}
|
|
4887
5289
|
}
|
|
4888
5290
|
}
|
|
4889
5291
|
else {
|
|
@@ -4951,6 +5353,19 @@ class BindingSession {
|
|
|
4951
5353
|
}
|
|
4952
5354
|
record.outerPatternPathInfo = null;
|
|
4953
5355
|
}
|
|
5356
|
+
// 3 段目以降(§1.11)。各段も互いに独立した資源なので 1 つずつ守る
|
|
5357
|
+
if (record.outerPatternPathInfosRest !== null) {
|
|
5358
|
+
const restPathInfos = record.outerPatternPathInfosRest;
|
|
5359
|
+
for (let i = 0; i < restPathInfos.length; i++) {
|
|
5360
|
+
try {
|
|
5361
|
+
removeBindingByPattern(restPathInfos[i], record.patternListIndex, binding);
|
|
5362
|
+
}
|
|
5363
|
+
catch {
|
|
5364
|
+
// Cleanup is best-effort.
|
|
5365
|
+
}
|
|
5366
|
+
}
|
|
5367
|
+
record.outerPatternPathInfosRest = null;
|
|
5368
|
+
}
|
|
4954
5369
|
try {
|
|
4955
5370
|
removeBindingByPattern(record.patternPathInfo, record.patternListIndex, binding);
|
|
4956
5371
|
record.patternPathInfo = null;
|
|
@@ -7453,9 +7868,17 @@ function _getFragmentInfo(rootNode, fragment, parseBindingTextResult, forPath) {
|
|
|
7453
7868
|
}
|
|
7454
7869
|
function collectStructuralFragments(rootNode, walkRoot, forPath) {
|
|
7455
7870
|
const elseKeyword = config.commentElsePrefix;
|
|
7871
|
+
// Light DOM の mapped コンポーネントの内側は、その子スコープが自分で処理する(§1.13)。
|
|
7872
|
+
// fragment info は rootNode + state 名で登録されるため、ホストのパスでここを拾うと
|
|
7873
|
+
// コンポーネント側の state がまだ名前登録を済ませておらず解決に失敗する。
|
|
7874
|
+
// コンポーネント要素自身は template ではないので、REJECT でサブツリーごと落として問題ない。
|
|
7875
|
+
const nestedComponents = findNestedLightDomComponents(walkRoot);
|
|
7456
7876
|
const walker = document.createTreeWalker(walkRoot, NodeFilter.SHOW_ELEMENT, {
|
|
7457
7877
|
acceptNode(node) {
|
|
7458
7878
|
const element = node;
|
|
7879
|
+
if (nestedComponents.length > 0 && nestedComponents.indexOf(element) !== -1) {
|
|
7880
|
+
return NodeFilter.FILTER_REJECT;
|
|
7881
|
+
}
|
|
7459
7882
|
if (element.tagName.toLowerCase() === 'template') {
|
|
7460
7883
|
const bindText = element.getAttribute(config.bindAttributeName) || '';
|
|
7461
7884
|
if (bindText.length > 0) {
|
|
@@ -7576,6 +7999,13 @@ async function waitForStateInitialize(root) {
|
|
|
7576
7999
|
const promises = [];
|
|
7577
8000
|
await customElements.whenDefined(config.tagNames.state);
|
|
7578
8001
|
for (const element of elements) {
|
|
8002
|
+
// Light DOM の mapped コンポーネントの state は待たない。それはこの root の
|
|
8003
|
+
// バインディングが張られてからでないと初期化できず(自分を束ねるホスト binding を
|
|
8004
|
+
// 待つ)、ここで待つと循環する(§1.13)。Shadow DOM 形では別 rootNode にいるので
|
|
8005
|
+
// そもそもこの集合に現れず、plain 形は循環しないので従来どおり待つ。
|
|
8006
|
+
if (isLightDomMappedStateElement(element)) {
|
|
8007
|
+
continue;
|
|
8008
|
+
}
|
|
7579
8009
|
const stateElement = element;
|
|
7580
8010
|
promises.push(stateElement.initializePromise);
|
|
7581
8011
|
}
|
|
@@ -7606,7 +8036,7 @@ async function buildBindings(root) {
|
|
|
7606
8036
|
}
|
|
7607
8037
|
}
|
|
7608
8038
|
|
|
7609
|
-
var version = "1.
|
|
8039
|
+
var version = "1.27.0";
|
|
7610
8040
|
var pkg = {
|
|
7611
8041
|
version: version};
|
|
7612
8042
|
|
|
@@ -8441,27 +8871,89 @@ function setStateElementByName(rootNode, name, element) {
|
|
|
8441
8871
|
}
|
|
8442
8872
|
}
|
|
8443
8873
|
|
|
8444
|
-
|
|
8874
|
+
/**
|
|
8875
|
+
* watch/chainDepth.ts
|
|
8876
|
+
*
|
|
8877
|
+
* `$watch` ハンドラ起点の書き込み連鎖の深さを数える台帳
|
|
8878
|
+
* (docs/state-watch-hook-design.md §7-2)。
|
|
8879
|
+
*
|
|
8880
|
+
* watch ハンドラ内の書き込みは新しい microtask バッチを作るため、伝播 context の
|
|
8881
|
+
* hop 上限(MAX_PROPAGATION_HOPS)のガードが効かない。かつ書き込み先が動的なので、
|
|
8882
|
+
* `$streams` のような「宣言時の自己依存検出」も使えない。よって実行時に数える。
|
|
8883
|
+
*
|
|
8884
|
+
* updater(enqueue 側)と watchRuntime(発火側)の両方から参照されるため、
|
|
8885
|
+
* **依存ゼロの葉モジュール**にして循環 import を避ける(devtools/sink.ts と同じ方針)。
|
|
8886
|
+
*
|
|
8887
|
+
* 数え方: ハンドラ実行中に enqueue が起きたときだけ「次のバッチはこの連鎖の続き」と
|
|
8888
|
+
* マークする。ハンドラが何も書かなければ次のバッチは深さ 0 に戻るので、利用者操作が
|
|
8889
|
+
* 何度続いても深さは伸びない。
|
|
8890
|
+
*/
|
|
8891
|
+
/** ハンドラ実行中に立つ「今の連鎖の深さ + 1」。0 なら watch 起点ではない */
|
|
8892
|
+
let firingDepth = 0;
|
|
8893
|
+
/** 次に drain されるバッチの深さ */
|
|
8894
|
+
let pendingDepth = 0;
|
|
8895
|
+
/** watch の発火フェーズ開始(watchRuntime 専用) */
|
|
8896
|
+
function beginWatchFiring(depth) {
|
|
8897
|
+
firingDepth = depth + 1;
|
|
8898
|
+
}
|
|
8899
|
+
/** watch の発火フェーズ終了(watchRuntime 専用。必ず finally で呼ぶ) */
|
|
8900
|
+
function endWatchFiring() {
|
|
8901
|
+
firingDepth = 0;
|
|
8902
|
+
}
|
|
8903
|
+
/**
|
|
8904
|
+
* 書き込みの enqueue を記録する(updater 専用)。
|
|
8905
|
+
* ハンドラ実行中でなければ何もしない = 通常の書き込みに深さは付かない。
|
|
8906
|
+
*/
|
|
8907
|
+
function noteEnqueueForWatchChain() {
|
|
8908
|
+
if (firingDepth > pendingDepth) {
|
|
8909
|
+
pendingDepth = firingDepth;
|
|
8910
|
+
}
|
|
8911
|
+
}
|
|
8912
|
+
/** 次バッチの深さを消費する(watchRuntime 専用。読んだらリセット) */
|
|
8913
|
+
function consumeWatchChainDepth() {
|
|
8914
|
+
const depth = pendingDepth;
|
|
8915
|
+
pendingDepth = 0;
|
|
8916
|
+
return depth;
|
|
8917
|
+
}
|
|
8918
|
+
|
|
8919
|
+
const updateBatchListeners = [];
|
|
8445
8920
|
/**
|
|
8446
8921
|
* drain 終了リスナーを登録する。
|
|
8922
|
+
*
|
|
8923
|
+
* `priority` の昇順に呼ばれる(同値は登録順)。機構間の実行順序
|
|
8924
|
+
* (`$watch` → `$streams` restart、docs/state-watch-hook-design.md §3-2 層 1)は
|
|
8925
|
+
* この優先度で固定する — import 順に順序を持たせると、無関係な import 整理で
|
|
8926
|
+
* 静かに壊れるため。定数は define.ts の `*_LISTENER_PRIORITY` を使うこと。
|
|
8447
8927
|
*/
|
|
8448
|
-
function registerUpdateBatchListener(listener) {
|
|
8449
|
-
|
|
8928
|
+
function registerUpdateBatchListener(listener, priority = 0) {
|
|
8929
|
+
// 挿入ソート: 同値優先度の中では登録順を保つ(find は最初の「より大きい」要素を指す)
|
|
8930
|
+
const index = updateBatchListeners.findIndex((registered) => registered.priority > priority);
|
|
8931
|
+
const entry = { listener, priority };
|
|
8932
|
+
if (index === -1) {
|
|
8933
|
+
updateBatchListeners.push(entry);
|
|
8934
|
+
}
|
|
8935
|
+
else {
|
|
8936
|
+
updateBatchListeners.splice(index, 0, entry);
|
|
8937
|
+
}
|
|
8450
8938
|
}
|
|
8451
8939
|
/**
|
|
8452
8940
|
* drain 終了リスナーを解除する(テスト間の分離用)。
|
|
8453
8941
|
*/
|
|
8454
8942
|
function unregisterUpdateBatchListener(listener) {
|
|
8455
|
-
updateBatchListeners.
|
|
8943
|
+
const index = updateBatchListeners.findIndex((registered) => registered.listener === listener);
|
|
8944
|
+
if (index !== -1) {
|
|
8945
|
+
updateBatchListeners.splice(index, 1);
|
|
8946
|
+
}
|
|
8456
8947
|
}
|
|
8457
8948
|
/**
|
|
8458
|
-
* 全リスナーに drain
|
|
8949
|
+
* 全リスナーに drain のバッチを優先度順で通知する。
|
|
8459
8950
|
* リスナーの throw は握りつぶさない(内部バグの隠蔽防止)。
|
|
8460
|
-
* stream 側リスナーが entry ごとに自前で try/catch する契約(設計書 §3-2)。
|
|
8951
|
+
* stream / watch 側リスナーが entry ごとに自前で try/catch する契約(設計書 §3-2)。
|
|
8461
8952
|
*/
|
|
8462
8953
|
function notifyUpdateBatchListeners(batch) {
|
|
8463
|
-
|
|
8464
|
-
|
|
8954
|
+
// 反復中の register / unregister(ハンドラ内の切断・再 set)に耐えるためコピーする
|
|
8955
|
+
for (const registered of updateBatchListeners.slice()) {
|
|
8956
|
+
registered.listener(batch);
|
|
8465
8957
|
}
|
|
8466
8958
|
}
|
|
8467
8959
|
class Updater {
|
|
@@ -8469,6 +8961,9 @@ class Updater {
|
|
|
8469
8961
|
constructor() {
|
|
8470
8962
|
}
|
|
8471
8963
|
enqueueAbsoluteAddress(absoluteAddress, context = null) {
|
|
8964
|
+
// `$watch` ハンドラ実行中の書き込みだけを連鎖としてマークする(watch/chainDepth.ts)。
|
|
8965
|
+
// ハンドラ実行中でなければ即 return する葉モジュール呼び出し 1 個のコスト。
|
|
8966
|
+
noteEnqueueForWatchChain();
|
|
8472
8967
|
const requireStartProcess = this._queueUpdateRecords.length === 0;
|
|
8473
8968
|
this._queueUpdateRecords.push({ absoluteAddress, context });
|
|
8474
8969
|
if (requireStartProcess) {
|
|
@@ -8695,7 +9190,9 @@ function setSink(sink) {
|
|
|
8695
9190
|
setDevtoolsSink(sink);
|
|
8696
9191
|
const isActive = sink !== null;
|
|
8697
9192
|
if (isActive && !wasActive) {
|
|
8698
|
-
|
|
9193
|
+
// `$watch` / `$streams` restart より先に流す(protocol §4.3)。優先度を省略しても
|
|
9194
|
+
// 既定 0 で結果は同じだが、それは偶然なので定数で意図を固定する。
|
|
9195
|
+
registerUpdateBatchListener(onUpdateBatch, DEVTOOLS_LISTENER_PRIORITY);
|
|
8699
9196
|
}
|
|
8700
9197
|
else if (!isActive && wasActive) {
|
|
8701
9198
|
unregisterUpdateBatchListener(onUpdateBatch);
|
|
@@ -9012,12 +9509,12 @@ function processCommandTokensDeclaration(state) {
|
|
|
9012
9509
|
return names;
|
|
9013
9510
|
}
|
|
9014
9511
|
|
|
9015
|
-
const registryByStateElement$
|
|
9512
|
+
const registryByStateElement$2 = new WeakMap();
|
|
9016
9513
|
function getOrCreateCommandToken(stateElement, name) {
|
|
9017
|
-
let registry = registryByStateElement$
|
|
9514
|
+
let registry = registryByStateElement$2.get(stateElement);
|
|
9018
9515
|
if (typeof registry === "undefined") {
|
|
9019
9516
|
registry = new Map();
|
|
9020
|
-
registryByStateElement$
|
|
9517
|
+
registryByStateElement$2.set(stateElement, registry);
|
|
9021
9518
|
}
|
|
9022
9519
|
let token = registry.get(name);
|
|
9023
9520
|
if (typeof token === "undefined") {
|
|
@@ -9027,7 +9524,7 @@ function getOrCreateCommandToken(stateElement, name) {
|
|
|
9027
9524
|
return token;
|
|
9028
9525
|
}
|
|
9029
9526
|
function clearCommandTokenRegistry(stateElement) {
|
|
9030
|
-
registryByStateElement$
|
|
9527
|
+
registryByStateElement$2.delete(stateElement);
|
|
9031
9528
|
}
|
|
9032
9529
|
|
|
9033
9530
|
/**
|
|
@@ -9262,24 +9759,24 @@ function invalidateLastNotified(stateElement, name) {
|
|
|
9262
9759
|
* 設計書 §3-2 の「未接続(disconnect 済み)の stateElement の entry は restart
|
|
9263
9760
|
* しない」はこの不変条件で担保される。
|
|
9264
9761
|
*/
|
|
9265
|
-
const activeStateElements = new Set();
|
|
9762
|
+
const activeStateElements$1 = new Set();
|
|
9266
9763
|
/**
|
|
9267
9764
|
* 起動中 stateElement として登録する(startStreams 専用。不変条件はモジュールヘッダ参照)。
|
|
9268
9765
|
*/
|
|
9269
9766
|
function addActiveStateElement(stateElement) {
|
|
9270
|
-
activeStateElements.add(stateElement);
|
|
9767
|
+
activeStateElements$1.add(stateElement);
|
|
9271
9768
|
}
|
|
9272
9769
|
/**
|
|
9273
9770
|
* 起動中 stateElement から外す(abortAllStreams / clearStreamRegistry 専用)。
|
|
9274
9771
|
*/
|
|
9275
9772
|
function deleteActiveStateElement(stateElement) {
|
|
9276
|
-
activeStateElements.delete(stateElement);
|
|
9773
|
+
activeStateElements$1.delete(stateElement);
|
|
9277
9774
|
}
|
|
9278
9775
|
/**
|
|
9279
9776
|
* 起動中 stateElement を列挙する(drain リスナーの交差判定用)。
|
|
9280
9777
|
*/
|
|
9281
9778
|
function getActiveStateElements() {
|
|
9282
|
-
return activeStateElements;
|
|
9779
|
+
return activeStateElements$1;
|
|
9283
9780
|
}
|
|
9284
9781
|
|
|
9285
9782
|
/**
|
|
@@ -9292,18 +9789,18 @@ function getActiveStateElements() {
|
|
|
9292
9789
|
* - disconnect 時は abortAllStreams(abort のみ・registry 保持)、
|
|
9293
9790
|
* `_state` 再 set 時のみ clearStreamRegistry(abort + 全削除)。
|
|
9294
9791
|
*/
|
|
9295
|
-
const registryByStateElement = new WeakMap();
|
|
9792
|
+
const registryByStateElement$1 = new WeakMap();
|
|
9296
9793
|
/**
|
|
9297
9794
|
* stream entry 群を置換登録する(`_state` セッターからの再構築で丸ごと差し替える)。
|
|
9298
9795
|
*/
|
|
9299
9796
|
function setStreamEntries(stateElement, entries) {
|
|
9300
|
-
registryByStateElement.set(stateElement, entries);
|
|
9797
|
+
registryByStateElement$1.set(stateElement, entries);
|
|
9301
9798
|
}
|
|
9302
9799
|
/**
|
|
9303
9800
|
* 登録済みの stream entry 群を返す。未登録なら空 Map を返す(registry への登録はしない)。
|
|
9304
9801
|
*/
|
|
9305
9802
|
function getStreamEntries(stateElement) {
|
|
9306
|
-
return registryByStateElement.get(stateElement) ?? new Map();
|
|
9803
|
+
return registryByStateElement$1.get(stateElement) ?? new Map();
|
|
9307
9804
|
}
|
|
9308
9805
|
/**
|
|
9309
9806
|
* 全 stream を abort して idle に戻す(設計書 §5-1)。registry は保持する。
|
|
@@ -9323,7 +9820,7 @@ function abortAllStreams(stateElement) {
|
|
|
9323
9820
|
// 設計書 §3-2。add 側は startStreams — stream/activeStateElements.ts の
|
|
9324
9821
|
// リーク防止不変条件を参照)。registry の有無に関わらず必ず外す。
|
|
9325
9822
|
deleteActiveStateElement(stateElement);
|
|
9326
|
-
const entries = registryByStateElement.get(stateElement);
|
|
9823
|
+
const entries = registryByStateElement$1.get(stateElement);
|
|
9327
9824
|
if (typeof entries === "undefined") {
|
|
9328
9825
|
return;
|
|
9329
9826
|
}
|
|
@@ -9343,7 +9840,7 @@ function clearStreamRegistry(stateElement) {
|
|
|
9343
9840
|
// abortAllStreams が既に delete 済みだが、「clear = 全削除でも必ず restart 対象から
|
|
9344
9841
|
// 外れる」不変条件を将来の abortAllStreams の変更から独立に保証するため明示的に呼ぶ。
|
|
9345
9842
|
deleteActiveStateElement(stateElement);
|
|
9346
|
-
registryByStateElement.delete(stateElement);
|
|
9843
|
+
registryByStateElement$1.delete(stateElement);
|
|
9347
9844
|
}
|
|
9348
9845
|
|
|
9349
9846
|
/**
|
|
@@ -10069,7 +10566,501 @@ function restartStreamsOnUpdateBatch(batch) {
|
|
|
10069
10566
|
}
|
|
10070
10567
|
}
|
|
10071
10568
|
}
|
|
10072
|
-
|
|
10569
|
+
// 優先度で `$watch` の後に固定する(設計書 §3-2 層 1)。import 順には依存しない。
|
|
10570
|
+
registerUpdateBatchListener(restartStreamsOnUpdateBatch, STREAM_LISTENER_PRIORITY);
|
|
10571
|
+
|
|
10572
|
+
/**
|
|
10573
|
+
* watch/watchRegistry.ts
|
|
10574
|
+
*
|
|
10575
|
+
* `$watch` の registry と、drain リスナーの走査元になる「発火対象の stateElement 集合」
|
|
10576
|
+
* (docs/state-watch-hook-design.md §9)。
|
|
10577
|
+
*
|
|
10578
|
+
* `$streams` は registry(delete 側)と runtime(add 側)が相互に依存するため
|
|
10579
|
+
* active 集合を stream/activeStateElements.ts へ切り出しているが、`$watch` は
|
|
10580
|
+
* **add が State のライフサイクル側、delete が registry 側**で、runtime は読むだけの
|
|
10581
|
+
* 一方向依存になる。よって循環せず、1 モジュールにまとめられる。
|
|
10582
|
+
*
|
|
10583
|
+
* リーク防止の不変条件(strong Set が切断済み要素の GC を妨げないための連動):
|
|
10584
|
+
* - add は `startWatch`(`State.connectedCallback` の $connectedCallback 完了後、および
|
|
10585
|
+
* 接続中の `_state` 再 set)だけが行い、**宣言が 1 つも無い stateElement は入れない**。
|
|
10586
|
+
* - delete は `deactivateWatch`(disconnectedCallback)/`clearWatchRegistry`(`_state`
|
|
10587
|
+
* 再 set)だけが行う。
|
|
10588
|
+
* どちらの経路も必ずここを通るため「Set に居る = 接続中かつ宣言済み」が保たれる。
|
|
10589
|
+
* この「宣言済み」の側が崩れると、`$watch` 未使用アプリの drain にも収集ループが乗る
|
|
10590
|
+
* (ゼロコスト契約、docs/state-watch-hook-design.md §10)。
|
|
10591
|
+
*/
|
|
10592
|
+
const registryByStateElement = new WeakMap();
|
|
10593
|
+
const activeStateElements = new Set();
|
|
10594
|
+
/**
|
|
10595
|
+
* 未登録時に返す共有の空 Map。
|
|
10596
|
+
*
|
|
10597
|
+
* ここで毎回 `new Map()` すると、drain の収集ループが「バッチのアドレス 1 個につき
|
|
10598
|
+
* Map を 1 個」アロケートすることになる(発火対象だが宣言を持たない stateElement を
|
|
10599
|
+
* 通る経路)。読み出ししかしない返り値なので 1 個を使い回す。
|
|
10600
|
+
*/
|
|
10601
|
+
const EMPTY_ENTRIES = new Map();
|
|
10602
|
+
/**
|
|
10603
|
+
* watch entry 群を置換登録する(`_state` セッターからの再構築で丸ごと差し替える)。
|
|
10604
|
+
*/
|
|
10605
|
+
function setWatchEntries(stateElement, entries) {
|
|
10606
|
+
registryByStateElement.set(stateElement, entries);
|
|
10607
|
+
}
|
|
10608
|
+
/**
|
|
10609
|
+
* 登録済みの watch entry 群を返す。未登録なら共有の空 Map を返す
|
|
10610
|
+
* (registry への登録はしない。返り値は読み出し専用)。
|
|
10611
|
+
*/
|
|
10612
|
+
function getWatchEntries(stateElement) {
|
|
10613
|
+
return registryByStateElement.get(stateElement) ?? EMPTY_ENTRIES;
|
|
10614
|
+
}
|
|
10615
|
+
/**
|
|
10616
|
+
* 発火対象として登録する(`startWatch` 専用。不変条件はモジュールヘッダ参照)。
|
|
10617
|
+
*/
|
|
10618
|
+
function addActiveWatchStateElement(stateElement) {
|
|
10619
|
+
activeStateElements.add(stateElement);
|
|
10620
|
+
}
|
|
10621
|
+
/**
|
|
10622
|
+
* 発火対象を列挙する(drain リスナーの early return 判定用)。
|
|
10623
|
+
*/
|
|
10624
|
+
function getActiveWatchStateElements() {
|
|
10625
|
+
return activeStateElements;
|
|
10626
|
+
}
|
|
10627
|
+
/**
|
|
10628
|
+
* 発火対象から外す(切断時)。**registry は保持する。**
|
|
10629
|
+
*
|
|
10630
|
+
* `$streams` の abortAllStreams と同じ二段構えで、切断は「発火しなくなる」だけにする。
|
|
10631
|
+
* registry まで捨てると、再接続(connectedCallback → startWatch)で宣言を作り直す経路が
|
|
10632
|
+
* 無い(`_state` セッターは初回ロード時にしか走らない)ため、watch が二度と発火しない。
|
|
10633
|
+
*/
|
|
10634
|
+
function deactivateWatch(stateElement) {
|
|
10635
|
+
activeStateElements.delete(stateElement);
|
|
10636
|
+
}
|
|
10637
|
+
/**
|
|
10638
|
+
* registry から削除し、発火対象からも外す(`_state` 再 set 時の再配線用)。
|
|
10639
|
+
*/
|
|
10640
|
+
function clearWatchRegistry(stateElement) {
|
|
10641
|
+
activeStateElements.delete(stateElement);
|
|
10642
|
+
registryByStateElement.delete(stateElement);
|
|
10643
|
+
}
|
|
10644
|
+
|
|
10645
|
+
/**
|
|
10646
|
+
* watch/processWatchDeclaration.ts
|
|
10647
|
+
*
|
|
10648
|
+
* `$watch: { "<path>": (cur, prev, ...indexes) => void }` 宣言マップを解析し、
|
|
10649
|
+
* IWatchEntry を構築して watchRegistry に一括登録する
|
|
10650
|
+
* (docs/state-watch-hook-design.md §2-2 / §8)。
|
|
10651
|
+
*
|
|
10652
|
+
* `$streams` の processStreamsDeclaration と対称だが、**キーが宣言名ではなくパス**である
|
|
10653
|
+
* ぶん検証が異なる(`.` / `*` を許可し、代わりにパスとしての妥当性を見る)。
|
|
10654
|
+
*
|
|
10655
|
+
* 依存グラフ登録(§8)がこの関数の要点:
|
|
10656
|
+
* `setPathInfo` は BindingSession(= DOM バインディング登録)からしか呼ばれないため、
|
|
10657
|
+
* 静的依存グラフに載るのは「バインドされたパス」だけである。watch を宣言しただけでは
|
|
10658
|
+
* walkDependency がそのパスを知らず、`items` への代入で `items.*.price` がバッチに載らない
|
|
10659
|
+
* = ハンドラが黙って一度も発火しない。宣言時に自分で登録することでこれを塞ぐ。
|
|
10660
|
+
*
|
|
10661
|
+
* 呼び出しは stateElement の `_pathSet` クリア後・getterPaths 確定後であること
|
|
10662
|
+
* (State の `_state` セッターが順序を保証する)。
|
|
10663
|
+
*/
|
|
10664
|
+
/**
|
|
10665
|
+
* `$watch` 宣言を registry へ反映し、監視対象パスの集合を返す。
|
|
10666
|
+
*
|
|
10667
|
+
* 宣言が無い(または空)なら **null** を返す。呼び出し側(State)はこれを
|
|
10668
|
+
* `watchPaths` に保持し、setByAddress のホットパスは `!== null` の分岐 1 個で
|
|
10669
|
+
* 抜けられる(ゼロコスト契約、§10)。
|
|
10670
|
+
*/
|
|
10671
|
+
function processWatchDeclaration(stateElement, state) {
|
|
10672
|
+
const declared = state[STATE_WATCH_NAME];
|
|
10673
|
+
if (typeof declared === "undefined") {
|
|
10674
|
+
return null;
|
|
10675
|
+
}
|
|
10676
|
+
if (typeof declared !== "object" || declared === null) {
|
|
10677
|
+
raiseError(`${STATE_WATCH_NAME} must be an object mapping state paths to handler functions.`);
|
|
10678
|
+
}
|
|
10679
|
+
const entries = new Map();
|
|
10680
|
+
const paths = new Set();
|
|
10681
|
+
let order = 0;
|
|
10682
|
+
for (const [path, handler] of Object.entries(declared)) {
|
|
10683
|
+
if (typeof handler !== "function") {
|
|
10684
|
+
raiseError(`${STATE_WATCH_NAME} entry "${path}" must be a function.`);
|
|
10685
|
+
}
|
|
10686
|
+
if (path.length === 0) {
|
|
10687
|
+
raiseError(`${STATE_WATCH_NAME} entry name must be a non-empty state path.`);
|
|
10688
|
+
}
|
|
10689
|
+
if (path.startsWith("$")) {
|
|
10690
|
+
raiseError(`${STATE_WATCH_NAME} entry "${path}" must not start with "$" (reserved namespace).`);
|
|
10691
|
+
}
|
|
10692
|
+
// 越境 watch は不採用(設計 D8)。他 state のアドレスは発火対象にしないため、
|
|
10693
|
+
// `@stateName` 付きのパスは受け取った時点で落とす(黙って発火しないより良い)。
|
|
10694
|
+
if (path.includes(STATE_NAME_SEPARATOR)) {
|
|
10695
|
+
raiseError(`${STATE_WATCH_NAME} entry "${path}" must not target another state ("${STATE_NAME_SEPARATOR}" is not allowed); watch only paths of its own state.`);
|
|
10696
|
+
}
|
|
10697
|
+
// Object.prototype の継承名は `path in state` 系の判定を汚すため一律拒否する
|
|
10698
|
+
// (processStreamsDeclaration と同じ防衛線)。
|
|
10699
|
+
if (path in Object.prototype) {
|
|
10700
|
+
raiseError(`${STATE_WATCH_NAME} entry "${path}" must not be a property name inherited from Object.prototype (e.g. "__proto__", "constructor").`);
|
|
10701
|
+
}
|
|
10702
|
+
const pathInfo = getPathInfo(path);
|
|
10703
|
+
// 空セグメント("a..b" / 先頭・末尾の ".")は getPathInfo が黙って受理してしまうため、
|
|
10704
|
+
// ここで落とす。放置すると解決不能なアドレスを依存グラフへ登録することになる。
|
|
10705
|
+
for (const segment of pathInfo.segments) {
|
|
10706
|
+
if (segment.length === 0) {
|
|
10707
|
+
raiseError(`${STATE_WATCH_NAME} entry "${path}" has an empty path segment.`);
|
|
10708
|
+
}
|
|
10709
|
+
}
|
|
10710
|
+
if (pathInfo.wildcardCount > MAX_WILDCARD_DEPTH) {
|
|
10711
|
+
raiseError(`${STATE_WATCH_NAME} entry "${path}" exceeds the maximum wildcard depth (${MAX_WILDCARD_DEPTH}).`);
|
|
10712
|
+
}
|
|
10713
|
+
entries.set(path, {
|
|
10714
|
+
path,
|
|
10715
|
+
pathInfo,
|
|
10716
|
+
handler: handler,
|
|
10717
|
+
order: order++,
|
|
10718
|
+
});
|
|
10719
|
+
paths.add(path);
|
|
10720
|
+
// 依存グラフ登録(§8)。"for" 以外の bindingType は親 → 子の staticDependency
|
|
10721
|
+
// チェーンを生やすだけで listPaths / elementPaths を触らない(State.setPathInfo 参照)。
|
|
10722
|
+
stateElement.setPathInfo(path, "prop");
|
|
10723
|
+
}
|
|
10724
|
+
setWatchEntries(stateElement, entries);
|
|
10725
|
+
return paths.size > 0 ? paths : null;
|
|
10726
|
+
}
|
|
10727
|
+
|
|
10728
|
+
/**
|
|
10729
|
+
* watch/computedSnapshots.ts
|
|
10730
|
+
*
|
|
10731
|
+
* watch 対象の computed(getter)の「前回評価値」台帳
|
|
10732
|
+
* (docs/state-watch-hook-design.md §5)。
|
|
10733
|
+
*
|
|
10734
|
+
* getter は `setByAddress` を通らないので、スカラ書き込み用の旧値台帳
|
|
10735
|
+
* (watch/prevValues.ts)には載らない。`prev` を渡すには前回の評価値を
|
|
10736
|
+
* **バッチを跨いで**保持する必要があり、こちらは drain ごとにクリアしない。
|
|
10737
|
+
*
|
|
10738
|
+
* 寿命は stateElement 単位(WeakMap)。`_state` 再 set では宣言ごと作り直すため
|
|
10739
|
+
* 破棄する。切断では破棄しない —— 再接続時の初回評価が上書きするので、
|
|
10740
|
+
* 残っていても害がなく、registry を保持する扱いとも揃う。
|
|
10741
|
+
*/
|
|
10742
|
+
const snapshotsByStateElement = new WeakMap();
|
|
10743
|
+
function getComputedSnapshot(stateElement, absAddress) {
|
|
10744
|
+
return snapshotsByStateElement.get(stateElement)?.get(absAddress);
|
|
10745
|
+
}
|
|
10746
|
+
function setComputedSnapshot(stateElement, absAddress, value) {
|
|
10747
|
+
let snapshots = snapshotsByStateElement.get(stateElement);
|
|
10748
|
+
if (typeof snapshots === "undefined") {
|
|
10749
|
+
snapshots = new Map();
|
|
10750
|
+
snapshotsByStateElement.set(stateElement, snapshots);
|
|
10751
|
+
}
|
|
10752
|
+
snapshots.set(absAddress, value);
|
|
10753
|
+
}
|
|
10754
|
+
/** `_state` 再 set で宣言ごと作り直すときに破棄する */
|
|
10755
|
+
function clearComputedSnapshots(stateElement) {
|
|
10756
|
+
snapshotsByStateElement.delete(stateElement);
|
|
10757
|
+
}
|
|
10758
|
+
|
|
10759
|
+
/**
|
|
10760
|
+
* watch/prevValues.ts
|
|
10761
|
+
*
|
|
10762
|
+
* `$watch` ハンドラへ渡す `prev`(バッチ開始時点の値)の台帳
|
|
10763
|
+
* (docs/state-watch-hook-design.md §4-1)。
|
|
10764
|
+
*
|
|
10765
|
+
* 記録するのは **watch 宣言済みパスへの書き込みだけ**、かつ **バッチ内で最初の 1 回だけ**
|
|
10766
|
+
* (first-write-wins)。したがって `prev` は「そのバッチが始まる前の値」、`cur` は
|
|
10767
|
+
* drain 時点の確定値になる。同一バッチ内の中間値は観測できない(§3-4)。
|
|
10768
|
+
*
|
|
10769
|
+
* 値の出どころは same-value guard が既に読んでいる旧値であり、watch のために
|
|
10770
|
+
* 追加の getByAddress は行わない(§10)。その帰結として:
|
|
10771
|
+
* - 参照型(object / array)は guard が素通しするので `prev` は undefined
|
|
10772
|
+
* - `config.sameValueGuard = false` でも undefined
|
|
10773
|
+
* - `$postUpdate` / stream の status 通知は setByAddress を通らないので undefined
|
|
10774
|
+
*
|
|
10775
|
+
* 台帳は drain 終端(watchRuntime)でクリアされる。drain の外で書き込まれた分が
|
|
10776
|
+
* 次のバッチへ持ち越されることはない。
|
|
10777
|
+
*/
|
|
10778
|
+
const prevValueByAbsoluteStateAddress = new Map();
|
|
10779
|
+
/**
|
|
10780
|
+
* バッチ内で最初の書き込みのときだけ旧値を記録する(first-write-wins)。
|
|
10781
|
+
*/
|
|
10782
|
+
function recordPrevValue(absAddress, oldValue) {
|
|
10783
|
+
if (prevValueByAbsoluteStateAddress.has(absAddress)) {
|
|
10784
|
+
return;
|
|
10785
|
+
}
|
|
10786
|
+
prevValueByAbsoluteStateAddress.set(absAddress, oldValue);
|
|
10787
|
+
}
|
|
10788
|
+
/**
|
|
10789
|
+
* 記録済みの旧値を返す。記録が無ければ undefined(§4-1 の「prev を保証しない」経路)。
|
|
10790
|
+
*/
|
|
10791
|
+
function getPrevValue(absAddress) {
|
|
10792
|
+
return prevValueByAbsoluteStateAddress.get(absAddress);
|
|
10793
|
+
}
|
|
10794
|
+
/**
|
|
10795
|
+
* 台帳をクリアする(drain 終端で必ず呼ぶ)。
|
|
10796
|
+
*/
|
|
10797
|
+
function clearPrevValues() {
|
|
10798
|
+
prevValueByAbsoluteStateAddress.clear();
|
|
10799
|
+
}
|
|
10800
|
+
|
|
10801
|
+
/**
|
|
10802
|
+
* watch/watchRuntime.ts
|
|
10803
|
+
*
|
|
10804
|
+
* `$watch` の発火(docs/state-watch-hook-design.md §3 / §7)。
|
|
10805
|
+
*
|
|
10806
|
+
* updater の drain 終了フックに 1 つだけリスナーを登録し、バッチに載った絶対アドレスと
|
|
10807
|
+
* 宣言済み watch パスを突き合わせて発火する。**binding の有無に関係なくバッチへ載る**ので、
|
|
10808
|
+
* これが headless 購読の実体になる(binding 駆動の `$updatedCallback` との違い)。
|
|
10809
|
+
*
|
|
10810
|
+
* 実行順序(設計書 §3-2):
|
|
10811
|
+
* - 機構間は優先度で固定(`$updatedCallback` → `$watch` → `$streams` restart)。
|
|
10812
|
+
* `$updatedCallback` が先なのは binding 適用ループの内側で呼ばれる構造的必然。
|
|
10813
|
+
* - watch ハンドラ間は `$watch` の宣言順(entry.order)。利用者が順序に意思を持てる唯一の層。
|
|
10814
|
+
* - 同一パスの複数行は indexes 昇順。
|
|
10815
|
+
*
|
|
10816
|
+
* 収集と発火を 2 相に分ける理由:
|
|
10817
|
+
* 1. ハンドラ内の書き込みが registry / active 集合を同期的に変えうる(`_state` 再 set・切断)
|
|
10818
|
+
* ため、発火直前に live 再チェックが要る(`$streams` の restart hits と同型)。
|
|
10819
|
+
* 2. 上記の順序規約のために hits をソートする必要がある(バッチの反復順は enqueue 順)。
|
|
10820
|
+
*/
|
|
10821
|
+
/**
|
|
10822
|
+
* この stateElement の `$watch` を有効化する(`State.connectedCallback` /接続中の
|
|
10823
|
+
* `_state` 再 set から呼ばれる。無効化は `clearWatchRegistry`)。
|
|
10824
|
+
*
|
|
10825
|
+
* `addActiveWatchStateElement` の薄いラッパではなく、**State が runtime を import する
|
|
10826
|
+
* 経路をここに一本化する**意味がある: drain リスナーの登録はこのモジュールの
|
|
10827
|
+
* 初期化副作用なので、registry だけを import すると発火機構ごと落ちる。
|
|
10828
|
+
* `$streams` の `startStreams` と対称の位置づけ。
|
|
10829
|
+
*
|
|
10830
|
+
* **宣言が 1 つも無ければ active 集合に入れない**(`startStreams` の
|
|
10831
|
+
* `entries.size === 0` early return と同型)。ここを無条件にすると active 集合が
|
|
10832
|
+
* 「接続中の全 `<wcs-state>`」になり、`fireWatchOnUpdateBatch` の early return が
|
|
10833
|
+
* 実アプリで効かなくなる = `$watch` 未使用アプリの drain にも収集ループが乗る
|
|
10834
|
+
* (ゼロコスト契約、設計書 §10 / 実装計画 P16)。
|
|
10835
|
+
*/
|
|
10836
|
+
function startWatch(stateElement) {
|
|
10837
|
+
if (getWatchEntries(stateElement).size === 0) {
|
|
10838
|
+
return;
|
|
10839
|
+
}
|
|
10840
|
+
addActiveWatchStateElement(stateElement);
|
|
10841
|
+
primeComputedWatches(stateElement);
|
|
10842
|
+
}
|
|
10843
|
+
/**
|
|
10844
|
+
* watch 対象の computed(getter)を 1 回評価する(設計書 §5-2 の eager 化、C-3)。
|
|
10845
|
+
*
|
|
10846
|
+
* これが要るのは、getter の依存(dynamicDependency)が**評価時にしか張られない**ため。
|
|
10847
|
+
* 一度も評価されていない getter は依存グラフに載らず、依存の書き込みが walkDependency で
|
|
10848
|
+
* そのパスへ到達しないので、バッチにも載らず watch が永久に発火しない。ここで 1 回
|
|
10849
|
+
* 読むことで依存が張られ、同時に `prev` の初期スナップショットが埋まる。
|
|
10850
|
+
*
|
|
10851
|
+
* **これが「watch した getter は lazy でなくなる」の実体**であり、設計書 §5-2 で
|
|
10852
|
+
* 規範として明記している副作用(毎バッチ評価・例外の表面化・依存の再登録)の起点。
|
|
10853
|
+
*
|
|
10854
|
+
* ワイルドカードを含む getter パス(`items.*.tax` など)は対象外: 初回評価に行ごとの
|
|
10855
|
+
* indexes が要り、全行評価は宣言しただけでリスト全体を舐めることになる。この形は
|
|
10856
|
+
* 「DOM にバインドされていれば発火する」ままとし、§5-3 に制約として書く。
|
|
10857
|
+
*/
|
|
10858
|
+
function primeComputedWatches(stateElement) {
|
|
10859
|
+
// 宣言が 1 つ以上あることは startWatch が保証済み
|
|
10860
|
+
const targets = [];
|
|
10861
|
+
for (const entry of getWatchEntries(stateElement).values()) {
|
|
10862
|
+
if (isScalarComputed(stateElement, entry)) {
|
|
10863
|
+
targets.push(entry);
|
|
10864
|
+
}
|
|
10865
|
+
}
|
|
10866
|
+
if (targets.length === 0) {
|
|
10867
|
+
// getter を watch していないなら createState ごと省く(宣言の大半はこちら)
|
|
10868
|
+
return;
|
|
10869
|
+
}
|
|
10870
|
+
stateElement.createState("readonly", (state) => {
|
|
10871
|
+
for (const entry of targets) {
|
|
10872
|
+
try {
|
|
10873
|
+
setComputedSnapshot(stateElement, absoluteAddressOf(stateElement, entry), state[entry.path]);
|
|
10874
|
+
}
|
|
10875
|
+
catch (e) {
|
|
10876
|
+
// 初回評価の throw は接続を巻き添えにしない(発火時と同じ隔離方針、§7-1)
|
|
10877
|
+
reportWatchError(stateElement, entry.path, "prime", e);
|
|
10878
|
+
}
|
|
10879
|
+
}
|
|
10880
|
+
});
|
|
10881
|
+
}
|
|
10882
|
+
/** ワイルドカードを含まない watch パスの絶対アドレス(listIndex は常に null) */
|
|
10883
|
+
function absoluteAddressOf(stateElement, entry) {
|
|
10884
|
+
return createAbsoluteStateAddress(getAbsolutePathInfo(stateElement, entry.pathInfo), null);
|
|
10885
|
+
}
|
|
10886
|
+
/**
|
|
10887
|
+
* 前回評価値のスナップショット台帳(computedSnapshots)に載せる entry か。
|
|
10888
|
+
*
|
|
10889
|
+
* ワイルドカードを含む getter を**除く**のが要点。除かないと台帳のキーが行ごとの
|
|
10890
|
+
* 絶対アドレス(= listIndex を強参照)になり、prune 経路が `_state` 再 set しか
|
|
10891
|
+
* 無いため、行が入れ替わり続けるページで単調増加する(リスト置換 5 回で 2→10 件を実測)。
|
|
10892
|
+
* そもそもワイルドカード getter は eager 化の対象外(設計書 §5-3)なので、
|
|
10893
|
+
* 「初回評価もしない・前回値も持たない」で primeComputedWatches と対称になる。
|
|
10894
|
+
* この形の `prev` は常に undefined(getter は setByAddress を通らない)。
|
|
10895
|
+
*/
|
|
10896
|
+
function isScalarComputed(stateElement, entry) {
|
|
10897
|
+
return entry.pathInfo.wildcardCount === 0 && stateElement.getterPaths.has(entry.path);
|
|
10898
|
+
}
|
|
10899
|
+
/**
|
|
10900
|
+
* throw を報告する(設計書 §7-1)。
|
|
10901
|
+
*
|
|
10902
|
+
* `console.error` だけだと **devtools からは「静かに握られた失敗」が見えない**。
|
|
10903
|
+
* watch は drain フックを `$streams` と共有しており、例外を watch 側で閉じるのが
|
|
10904
|
+
* 前提なので、閉じた事実をここで観測可能にしておく必要がある。
|
|
10905
|
+
* イベント生成は必ず `devtoolsSink !== null` の内側で行う(sink のコスト規範)。
|
|
10906
|
+
*/
|
|
10907
|
+
const WATCH_ERROR_SUBJECT = {
|
|
10908
|
+
prime: "initial evaluation of",
|
|
10909
|
+
evaluate: "evaluation of",
|
|
10910
|
+
handler: "handler for",
|
|
10911
|
+
};
|
|
10912
|
+
function reportWatchError(stateElement, path, phase, error) {
|
|
10913
|
+
console.error(`[@wcstack/state] $watch ${WATCH_ERROR_SUBJECT[phase]} "${path}" threw.`, error);
|
|
10914
|
+
if (devtoolsSink !== null) {
|
|
10915
|
+
devtoolsSink({
|
|
10916
|
+
type: "state:watch-error",
|
|
10917
|
+
phase,
|
|
10918
|
+
stateName: stateElement.name,
|
|
10919
|
+
path,
|
|
10920
|
+
error,
|
|
10921
|
+
});
|
|
10922
|
+
}
|
|
10923
|
+
}
|
|
10924
|
+
function fireWatchOnUpdateBatch(batch) {
|
|
10925
|
+
const activeStateElements = getActiveWatchStateElements();
|
|
10926
|
+
try {
|
|
10927
|
+
if (activeStateElements.size === 0) {
|
|
10928
|
+
// watch 未使用アプリの drain に配列・イテレータ割り当てのコストを載せない。
|
|
10929
|
+
// ここも finally を通す: 宣言済みの state が切断されている間(active からは
|
|
10930
|
+
// 外れるが watchPaths は残る)の書き込みで台帳に旧値が積まれるため、
|
|
10931
|
+
// クリアを早期 return の外に置くと次のバッチどころか永久に残る。
|
|
10932
|
+
return;
|
|
10933
|
+
}
|
|
10934
|
+
const depth = consumeWatchChainDepth();
|
|
10935
|
+
if (depth > MAX_WATCH_CHAIN_DEPTH) {
|
|
10936
|
+
// 打ち切るのは watch の発火のみ。値と binding 適用は巻き戻さない
|
|
10937
|
+
// (伝播 hop 上限超過時の quarantine と同じ姿勢、§7-2)。
|
|
10938
|
+
const paths = Array.from(batch, (absAddress) => absAddress.absolutePathInfo.pathInfo.path);
|
|
10939
|
+
console.error(`[@wcstack/state] $watch chain depth limit exceeded; watch handlers for this batch were skipped.`, { maxDepth: MAX_WATCH_CHAIN_DEPTH, paths });
|
|
10940
|
+
if (devtoolsSink !== null) {
|
|
10941
|
+
devtoolsSink({ type: "state:watch-chain-limit", maxDepth: MAX_WATCH_CHAIN_DEPTH, paths });
|
|
10942
|
+
}
|
|
10943
|
+
return;
|
|
10944
|
+
}
|
|
10945
|
+
// --- 収集フェーズ ---
|
|
10946
|
+
const hits = [];
|
|
10947
|
+
for (const absAddress of batch) {
|
|
10948
|
+
// stateName 文字列ではなく stateElement 参照で引く。AbsolutePathInfo は
|
|
10949
|
+
// stateElement 単位でキャッシュされるので、同名 state が複数の rootNode に
|
|
10950
|
+
// 居ても取り違えない(address/AbsolutePathInfo.ts)。他 state のアドレスは
|
|
10951
|
+
// ここで自然に落ちる = 越境しない(設計 D8)。
|
|
10952
|
+
const stateElement = absAddress.absolutePathInfo.stateElement;
|
|
10953
|
+
if (!activeStateElements.has(stateElement)) {
|
|
10954
|
+
continue;
|
|
10955
|
+
}
|
|
10956
|
+
const entry = getWatchEntries(stateElement).get(absAddress.absolutePathInfo.pathInfo.path);
|
|
10957
|
+
if (typeof entry === "undefined") {
|
|
10958
|
+
continue;
|
|
10959
|
+
}
|
|
10960
|
+
let indexes = [];
|
|
10961
|
+
if (entry.pathInfo.wildcardCount > 0) {
|
|
10962
|
+
if (absAddress.listIndex === null) {
|
|
10963
|
+
// ワイルドカードパスなのに行が特定できないヒット(リストの依存展開で載る
|
|
10964
|
+
// 中間アドレス等)。indexes を空のまま発火すると cur の解決($resolve)が
|
|
10965
|
+
// 「indexes 不足」で throw し、例外隔離に落ちて console.error だけが残る。
|
|
10966
|
+
// 行が定まらない以上ハンドラに渡せる意味が無いので、収集段階で落とす。
|
|
10967
|
+
continue;
|
|
10968
|
+
}
|
|
10969
|
+
indexes = getScopedIndexes(absAddress.listIndex, entry.pathInfo.wildcardCount);
|
|
10970
|
+
}
|
|
10971
|
+
hits.push({ stateElement, entry, absAddress, indexes });
|
|
10972
|
+
}
|
|
10973
|
+
if (hits.length === 0) {
|
|
10974
|
+
return;
|
|
10975
|
+
}
|
|
10976
|
+
hits.sort(compareHits);
|
|
10977
|
+
// --- 発火フェーズ ---
|
|
10978
|
+
beginWatchFiring(depth);
|
|
10979
|
+
try {
|
|
10980
|
+
for (const hit of hits) {
|
|
10981
|
+
// 先行ハンドラが同期的に切断や `_state` 再 set を行い得るため、発火直前に
|
|
10982
|
+
// 「まだ active か」「entry が現行 registry のものか」を再確認する。
|
|
10983
|
+
if (!activeStateElements.has(hit.stateElement) ||
|
|
10984
|
+
getWatchEntries(hit.stateElement).get(hit.entry.path) !== hit.entry) {
|
|
10985
|
+
continue;
|
|
10986
|
+
}
|
|
10987
|
+
fireOne(hit);
|
|
10988
|
+
}
|
|
10989
|
+
}
|
|
10990
|
+
finally {
|
|
10991
|
+
endWatchFiring();
|
|
10992
|
+
}
|
|
10993
|
+
}
|
|
10994
|
+
finally {
|
|
10995
|
+
// 旧値台帳はこの drain 限りのもの。次のバッチへ持ち越さない(§4-1)。
|
|
10996
|
+
clearPrevValues();
|
|
10997
|
+
}
|
|
10998
|
+
}
|
|
10999
|
+
/**
|
|
11000
|
+
* 層 2(宣言順)→ 層 3(indexes 昇順)の順に比較する(設計書 §3-3)。
|
|
11001
|
+
*/
|
|
11002
|
+
function compareHits(a, b) {
|
|
11003
|
+
if (a.entry.order !== b.entry.order) {
|
|
11004
|
+
return a.entry.order - b.entry.order;
|
|
11005
|
+
}
|
|
11006
|
+
const length = Math.min(a.indexes.length, b.indexes.length);
|
|
11007
|
+
for (let i = 0; i < length; i++) {
|
|
11008
|
+
if (a.indexes[i] !== b.indexes[i]) {
|
|
11009
|
+
return a.indexes[i] - b.indexes[i];
|
|
11010
|
+
}
|
|
11011
|
+
}
|
|
11012
|
+
return a.indexes.length - b.indexes.length;
|
|
11013
|
+
}
|
|
11014
|
+
/**
|
|
11015
|
+
* ハンドラ 1 つを発火する。**例外はここで閉じる**(設計書 §7-1)。
|
|
11016
|
+
*
|
|
11017
|
+
* drain リスナーの throw は握りつぶさない契約(updater.ts)なので、watch 側で捕まえないと
|
|
11018
|
+
* 1 つのユーザー例外が他の watch と `$streams` の restart を巻き添えにする。
|
|
11019
|
+
* `$connectedCallback` / `$updatedCallback` の loud fail とは意図的に異なる扱い。
|
|
11020
|
+
*
|
|
11021
|
+
* 報告は throw 元で分ける: `cur` の解決(watch した getter の強制評価 = §5-2 の副作用 b)と
|
|
11022
|
+
* ハンドラ本体では原因も直し方も違うため、同じ文言に丸めない。
|
|
11023
|
+
*/
|
|
11024
|
+
function fireOne(hit) {
|
|
11025
|
+
const { stateElement, entry, absAddress, indexes } = hit;
|
|
11026
|
+
// スカラ getter は setByAddress を通らないので旧値台帳に載らない。前回評価値の
|
|
11027
|
+
// スナップショット(バッチを跨いで生きる別台帳)から prev を取る(§5-2)。
|
|
11028
|
+
const isComputed = isScalarComputed(stateElement, entry);
|
|
11029
|
+
try {
|
|
11030
|
+
stateElement.createState("writable", (state) => {
|
|
11031
|
+
let cur;
|
|
11032
|
+
try {
|
|
11033
|
+
// 強制評価はここ。dirty なら再計算され、その結果が cur になる
|
|
11034
|
+
cur = readCurrentValue(state, entry, indexes);
|
|
11035
|
+
}
|
|
11036
|
+
catch (e) {
|
|
11037
|
+
// cur が得られない以上ハンドラは呼べない。次の hit へ進む
|
|
11038
|
+
reportWatchError(stateElement, entry.path, "evaluate", e);
|
|
11039
|
+
return;
|
|
11040
|
+
}
|
|
11041
|
+
const prev = isComputed ? getComputedSnapshot(stateElement, absAddress) : getPrevValue(absAddress);
|
|
11042
|
+
if (isComputed) {
|
|
11043
|
+
// ハンドラ本体が throw しても次回の prev は「今回の評価値」であるべきなので、
|
|
11044
|
+
// handler 呼び出しより前に更新する
|
|
11045
|
+
setComputedSnapshot(stateElement, absAddress, cur);
|
|
11046
|
+
}
|
|
11047
|
+
entry.handler.call(state, cur, prev, ...indexes);
|
|
11048
|
+
});
|
|
11049
|
+
}
|
|
11050
|
+
catch (e) {
|
|
11051
|
+
reportWatchError(stateElement, entry.path, "handler", e);
|
|
11052
|
+
}
|
|
11053
|
+
}
|
|
11054
|
+
function readCurrentValue(state, entry, indexes) {
|
|
11055
|
+
if (entry.pathInfo.wildcardCount === 0) {
|
|
11056
|
+
return state[entry.path];
|
|
11057
|
+
}
|
|
11058
|
+
// ワイルドカードを含むパスは素の読みでは解決できない。getScopedIndexes が返した列は
|
|
11059
|
+
// そのまま $resolve の引数として使える(list/wildcardLevel.ts の往復契約)。
|
|
11060
|
+
return state.$resolve(entry.path, indexes);
|
|
11061
|
+
}
|
|
11062
|
+
// 優先度で `$streams` の restart より先に固定する(設計書 §3-2 層 1)。import 順には依存しない。
|
|
11063
|
+
registerUpdateBatchListener(fireWatchOnUpdateBatch, WATCH_LISTENER_PRIORITY);
|
|
10073
11064
|
|
|
10074
11065
|
function getterFn(name) {
|
|
10075
11066
|
return function () {
|
|
@@ -11270,13 +12261,22 @@ function selectExpansionIndexes(context, sourcePath, _lastValue, _newValue, list
|
|
|
11270
12261
|
const EMPTY_PATH_INFOS = [];
|
|
11271
12262
|
/**
|
|
11272
12263
|
* 位置だけが変わった行(movedRows)で展開すべきパス群を求める。
|
|
11273
|
-
* `${listPath}.*`
|
|
12264
|
+
* `${listPath}.*` 配下にある、$1 等を読んだ実績のある getter
|
|
11274
12265
|
* (indexDependentGetterPaths)だけを返す。行の同一性・listIndex は保たれ
|
|
11275
12266
|
* index 以外の入力が不変なので、index を読まない getter / 値パスは再評価不要。
|
|
11276
12267
|
* 戻り値:
|
|
11277
12268
|
* - IPathInfo[](空可): この各パスだけを行の listIndex で展開する
|
|
11278
12269
|
* - null: ネストしたワイルドカード配下に index 依存 getter がある
|
|
11279
12270
|
* (listIndex の階数が合わず個別展開できない)→ 呼び出し側で行全体展開に倒す
|
|
12271
|
+
*
|
|
12272
|
+
* 配下判定は staticMap の subtree 走査ではなく indexDependentGetterPaths 側の
|
|
12273
|
+
* プレフィックス照合で行う。静的依存グラフは `State.setPathInfo` が
|
|
12274
|
+
* 「バインドされたパスから親方向へ」張るため、DOM にバインドされていない中間
|
|
12275
|
+
* getter(`.label` だけを描画し `.rank` は `.label` からしか読まれない綴り)は
|
|
12276
|
+
* subtree に現れない。そこを走査すると index 依存 getter を取りこぼし、
|
|
12277
|
+
* 「index を読む getter が subtree に無い=位置のみ変わった行の値は不変」という
|
|
12278
|
+
* 呼び出し側の判断が偽になって、移動行の getter が古い値のまま残る。
|
|
12279
|
+
* この集合は $1 を読んだ getter の数しか持たないので、走査コストも subtree より小さい。
|
|
11280
12280
|
*/
|
|
11281
12281
|
function getMovedRowExpansionPaths(context, wildcardPath, depPathInfo) {
|
|
11282
12282
|
const indexGetters = context.stateElement.indexDependentGetterPaths;
|
|
@@ -11284,26 +12284,16 @@ function getMovedRowExpansionPaths(context, wildcardPath, depPathInfo) {
|
|
|
11284
12284
|
return EMPTY_PATH_INFOS;
|
|
11285
12285
|
}
|
|
11286
12286
|
let result = null;
|
|
11287
|
-
const
|
|
11288
|
-
const
|
|
11289
|
-
|
|
11290
|
-
|
|
11291
|
-
if (indexGetters.has(path)) {
|
|
11292
|
-
const pathInfo = getPathInfo(path);
|
|
11293
|
-
if (pathInfo.wildcardCount !== depPathInfo.wildcardCount) {
|
|
11294
|
-
return null;
|
|
11295
|
-
}
|
|
11296
|
-
(result ??= []).push(pathInfo);
|
|
12287
|
+
const prefix = wildcardPath + DELIMITER;
|
|
12288
|
+
for (const path of indexGetters) {
|
|
12289
|
+
if (path !== wildcardPath && !path.startsWith(prefix)) {
|
|
12290
|
+
continue;
|
|
11297
12291
|
}
|
|
11298
|
-
const
|
|
11299
|
-
if (
|
|
11300
|
-
|
|
11301
|
-
if (!seen.has(child)) {
|
|
11302
|
-
seen.add(child);
|
|
11303
|
-
queue.push(child);
|
|
11304
|
-
}
|
|
11305
|
-
}
|
|
12292
|
+
const pathInfo = getPathInfo(path);
|
|
12293
|
+
if (pathInfo.wildcardCount !== depPathInfo.wildcardCount) {
|
|
12294
|
+
return null;
|
|
11306
12295
|
}
|
|
12296
|
+
(result ??= []).push(pathInfo);
|
|
11307
12297
|
}
|
|
11308
12298
|
return result ?? EMPTY_PATH_INFOS;
|
|
11309
12299
|
}
|
|
@@ -11531,6 +12521,21 @@ function walkDependency(stateName, stateElement, startAddress, staticDependency,
|
|
|
11531
12521
|
* - finallyで必ず更新情報を登録し、再描画や依存解決に利用
|
|
11532
12522
|
* - getter/setter経由のスコープ切り替えも考慮した設計
|
|
11533
12523
|
*/
|
|
12524
|
+
/**
|
|
12525
|
+
* `$watch` の `prev` 台帳へ旧値を記録する(docs/state-watch-hook-design.md §4-1)。
|
|
12526
|
+
*
|
|
12527
|
+
* same-value guard が既に読んだ旧値だけを使い、watch のための追加読みはしない。
|
|
12528
|
+
* `$watch` 未宣言時のコストは `watchPaths` の null 判定 1 個に収める(§10)。
|
|
12529
|
+
*/
|
|
12530
|
+
function recordWatchPrevValue(stateElement, path, absAddress, oldValue, hasOldValue) {
|
|
12531
|
+
const watchPaths = stateElement.watchPaths;
|
|
12532
|
+
if (watchPaths == null || !hasOldValue) {
|
|
12533
|
+
return;
|
|
12534
|
+
}
|
|
12535
|
+
if (watchPaths.has(path)) {
|
|
12536
|
+
recordPrevValue(absAddress, oldValue);
|
|
12537
|
+
}
|
|
12538
|
+
}
|
|
11534
12539
|
// Phase 3: 書き込み時点の因果 context を update record に付与する。
|
|
11535
12540
|
// binding 経由の書き込みは呼び出し元の dynamic scope から context を引き継ぎ、
|
|
11536
12541
|
// binding 外からの API update は新しい transaction を開始する(設計書 §4 規則 1)。
|
|
@@ -11764,6 +12769,7 @@ function setByAddressCore(target, address, value, receiver, handler, keyedMergeP
|
|
|
11764
12769
|
hasOldValue: devHasOldValue,
|
|
11765
12770
|
});
|
|
11766
12771
|
}
|
|
12772
|
+
recordWatchPrevValue(stateElement, path, absAddress, devOldValue, devHasOldValue);
|
|
11767
12773
|
try {
|
|
11768
12774
|
if (key === undefined) {
|
|
11769
12775
|
raiseError(`address.listIndex?.index is undefined path: ${path}`);
|
|
@@ -11814,6 +12820,7 @@ function setByAddressCore(target, address, value, receiver, handler, keyedMergeP
|
|
|
11814
12820
|
hasOldValue: devHasOldValue,
|
|
11815
12821
|
});
|
|
11816
12822
|
}
|
|
12823
|
+
recordWatchPrevValue(stateElement, path, absAddress, devOldValue, devHasOldValue);
|
|
11817
12824
|
try {
|
|
11818
12825
|
if (isSwappable) {
|
|
11819
12826
|
return _setByAddressWithSwap(target, address, absAddress, value, receiver, handler, keyedMergePath);
|
|
@@ -12558,14 +13565,19 @@ class InnerStateProxyHandler {
|
|
|
12558
13565
|
*/
|
|
12559
13566
|
_outerLoopContext(innerPathInfo, outerAbsPathInfo) {
|
|
12560
13567
|
const outerWildcardCount = outerAbsPathInfo.pathInfo.wildcardCount;
|
|
13568
|
+
// 段数の照合は外側スコープの**実 arity**(パスの段数 + そのスコープの Δ)で行う。
|
|
13569
|
+
// 境界 1 枚なら外側は Δ=0 で従来と同値、2 枚以上あるときに中間スコープの Δ を
|
|
13570
|
+
// 数えないと候補が両方とも外れて loopContext が null になる(§1.12)。
|
|
13571
|
+
// 添字(wildcardPaths)に使うのは Δ を含まない段数のままであることに注意。
|
|
13572
|
+
const outerArity = getScopeArity(outerAbsPathInfo.stateElement, outerAbsPathInfo.pathInfo);
|
|
12561
13573
|
const nodeLoopContext = getLoopContextByNode(this._webComponent);
|
|
12562
|
-
if (nodeLoopContext !== null && nodeLoopContext.listIndex.length ===
|
|
13574
|
+
if (nodeLoopContext !== null && nodeLoopContext.listIndex.length === outerArity) {
|
|
12563
13575
|
return nodeLoopContext;
|
|
12564
13576
|
}
|
|
12565
13577
|
if (outerWildcardCount > 0) {
|
|
12566
13578
|
const address = getCrossBoundaryAddress(this._innerStateElement, innerPathInfo.path);
|
|
12567
13579
|
const listIndex = address?.listIndex ?? null;
|
|
12568
|
-
if (listIndex !== null && listIndex.length ===
|
|
13580
|
+
if (listIndex !== null && listIndex.length === outerArity) {
|
|
12569
13581
|
const outerWildcardPath = outerAbsPathInfo.pathInfo.wildcardPaths[outerWildcardCount - 1];
|
|
12570
13582
|
return createStateAddress(getPathInfo(outerWildcardPath), listIndex);
|
|
12571
13583
|
}
|
|
@@ -12837,6 +13849,8 @@ class State extends HTMLElementBase {
|
|
|
12837
13849
|
_dynamicDependency = new Map();
|
|
12838
13850
|
_staticDependency = new Map();
|
|
12839
13851
|
_pathSet = new Set();
|
|
13852
|
+
// `$watch` 宣言の監視対象パス。宣言が無ければ null(setByAddress のゼロコスト契約)
|
|
13853
|
+
_watchPaths = null;
|
|
12840
13854
|
_version = 0;
|
|
12841
13855
|
_rootNode = null;
|
|
12842
13856
|
_boundComponent = null;
|
|
@@ -12916,10 +13930,21 @@ class State extends HTMLElementBase {
|
|
|
12916
13930
|
// $listKeys: 宣言が無ければ null のままで、setByAddress のキー突合経路には
|
|
12917
13931
|
// 一切入らない(docs/state-list-key-design.md §7-1)。再 set で必ず置き換える。
|
|
12918
13932
|
this._listKeys = processListKeysDeclaration(value);
|
|
13933
|
+
// $watch: 旧宣言のハンドラが残らないよう registry を落としてから新宣言を解析する。
|
|
13934
|
+
// _pathSet.clear() の後であること(依存グラフ登録をやり直す必要がある、
|
|
13935
|
+
// docs/state-watch-hook-design.md §8)。宣言が無ければ watchPaths は null で、
|
|
13936
|
+
// setByAddress の旧値キャプチャには一切入らない(§10 のゼロコスト契約)。
|
|
13937
|
+
clearWatchRegistry(this);
|
|
13938
|
+
// computed の前回評価値も宣言と寿命を共にする(旧宣言の値を新しい watch の
|
|
13939
|
+
// prev として渡さない)。切断では消さない — 再接続の初回評価が上書きする。
|
|
13940
|
+
clearComputedSnapshots(this);
|
|
13941
|
+
this._watchPaths = processWatchDeclaration(this, value);
|
|
12919
13942
|
// 接続中の再 set(S13)は新宣言で即再起動する。
|
|
12920
13943
|
// 初回(_initialize 中)は _initialized が false なのでここでは起動されず、
|
|
12921
13944
|
// connectedCallback 側の startStreams($connectedCallback 完了後)が担う。
|
|
12922
13945
|
if (this._initialized && this._rootNode !== null && !inSsr()) {
|
|
13946
|
+
// watch は stream より先に有効化する(stream の起動時書き込みを観測できるように)
|
|
13947
|
+
startWatch(this);
|
|
12923
13948
|
startStreams(this);
|
|
12924
13949
|
// $connectedCallback 実行中の再 set(setInitialState)では、ここで新宣言が
|
|
12925
13950
|
// 起動済みのため connectedCallback 末尾の startStreams を skip させる。
|
|
@@ -13057,6 +14082,36 @@ class State extends HTMLElementBase {
|
|
|
13057
14082
|
bindWebComponent(this, this._boundComponent, this._boundComponentStateProp, state);
|
|
13058
14083
|
}
|
|
13059
14084
|
}
|
|
14085
|
+
/**
|
|
14086
|
+
* Light DOM の mapped コンポーネントが、自分のサブツリーのバインディングを張る(§1.13)。
|
|
14087
|
+
*
|
|
14088
|
+
* Shadow DOM 形では子スコープが別 rootNode にあり、`setStateElementByName` の初回登録から
|
|
14089
|
+
* その root ぶんの `buildBindings` が別パスとして起動する。Light DOM ではホストと同じ root に
|
|
14090
|
+
* いるためそのパスが存在せず、かといってホストのパスに混ぜると `@name` の解決が
|
|
14091
|
+
* この要素の名前登録より先に来てしまう。そこで `getSubscriberNodes` がホスト側の走査から
|
|
14092
|
+
* このサブツリーを外し、名前登録が済んだここで同じことを自前で行う。
|
|
14093
|
+
*
|
|
14094
|
+
* `{{ }}` の変換だけはホストのパスが root 全体に対して済ませている(純粋にテキスト操作で
|
|
14095
|
+
* state に依存しないため)。構造フラグメントの収集は fragment info を rootNode + state 名で
|
|
14096
|
+
* 登録するので state 依存であり、ホストのパスからは外してここで走らせる。
|
|
14097
|
+
*
|
|
14098
|
+
* ループ文脈を null で渡すのは Shadow DOM 形(`initializeBindings(shadowRoot, null)`)と
|
|
14099
|
+
* 揃えるため —— 子孫の `getLoopContextByNode` はコンポーネント要素まで遡って
|
|
14100
|
+
* 親スコープの行を見つける。
|
|
14101
|
+
*/
|
|
14102
|
+
_initializeLightDomComponentScope() {
|
|
14103
|
+
const component = this._boundComponent;
|
|
14104
|
+
if (component === null || this.parentNode !== component) {
|
|
14105
|
+
// Shadow DOM 形(parentNode が ShadowRoot)は対象外
|
|
14106
|
+
return;
|
|
14107
|
+
}
|
|
14108
|
+
if (!component.hasAttribute(config.bindAttributeName)) {
|
|
14109
|
+
// plain 形はホストのパスに含まれたままなので、ここで張ると二重になる
|
|
14110
|
+
return;
|
|
14111
|
+
}
|
|
14112
|
+
collectStructuralFragments(this._rootNode, component);
|
|
14113
|
+
initializeBindings(component, null);
|
|
14114
|
+
}
|
|
13060
14115
|
/**
|
|
13061
14116
|
* mapped な `bind-component` が切断 → 再接続したときに、束ねているパスを読み直させる(§1.9)。
|
|
13062
14117
|
*
|
|
@@ -13158,6 +14213,9 @@ class State extends HTMLElementBase {
|
|
|
13158
14213
|
await this._initializeBindWebComponent();
|
|
13159
14214
|
await this._initialize();
|
|
13160
14215
|
this._initialized = true;
|
|
14216
|
+
// 名前登録(_initialize の末尾)が済んだこの時点でなければ、子スコープの
|
|
14217
|
+
// `@name` 参照が解決できない(§1.13)
|
|
14218
|
+
this._initializeLightDomComponentScope();
|
|
13161
14219
|
this._resolveInitialize?.();
|
|
13162
14220
|
}
|
|
13163
14221
|
else if (!this._dcc && getStateElementByName(this._rootNode, this._name) !== this) {
|
|
@@ -13209,6 +14267,19 @@ class State extends HTMLElementBase {
|
|
|
13209
14267
|
// _streamsStartedGeneration ガード: $connectedCallback 内の setInitialState
|
|
13210
14268
|
// (接続中の再 set)で _state セッター側が新宣言を起動済みの場合は skip する
|
|
13211
14269
|
// (skip しないと同一 connect サイクルで source が 2 回起動する、設計書 §2-3)。
|
|
14270
|
+
// $watch の有効化($connectedCallback 完了後 = 初期化中の書き込みは購読対象外)。
|
|
14271
|
+
// ガードは startStreams と同じ理由で必要(await 中の切断・再接続)。SSR では
|
|
14272
|
+
// 走らせない — ハンドラの副作用がサーバとクライアントで二重に実行されるため
|
|
14273
|
+
// (docs/state-watch-hook-design.md §11)。
|
|
14274
|
+
// startStreams より先に呼ぶ: stream の起動時書き込み(initial リセット・status 遷移)は
|
|
14275
|
+
// watch から観測できるべきで、逆向きは要らない。
|
|
14276
|
+
// 再入不要: 接続中の _state 再 set は _state セッター側で startWatch 済みだが、
|
|
14277
|
+
// startWatch は Set への add で冪等なので $streams のような世代ガードは要らない。
|
|
14278
|
+
if (!inSsr() &&
|
|
14279
|
+
this._rootNode !== null &&
|
|
14280
|
+
connectGeneration === this._connectGeneration) {
|
|
14281
|
+
startWatch(this);
|
|
14282
|
+
}
|
|
13212
14283
|
if (!inSsr() &&
|
|
13213
14284
|
this._rootNode !== null &&
|
|
13214
14285
|
connectGeneration === this._connectGeneration &&
|
|
@@ -13238,6 +14309,10 @@ class State extends HTMLElementBase {
|
|
|
13238
14309
|
// registry は残るため再接続後の初回アクセスで同内容の proxy が再生成される)。
|
|
13239
14310
|
abortAllStreams(this);
|
|
13240
14311
|
clearStreamNamespace(this);
|
|
14312
|
+
// watch は発火対象から外すだけで registry は保持する(stream の abortAllStreams と
|
|
14313
|
+
// 同じ二段構え、設計書 §9)。registry まで捨てると、_state セッターが再度走らない
|
|
14314
|
+
// 再接続で宣言を作り直せず watch が二度と発火しない。
|
|
14315
|
+
deactivateWatch(this);
|
|
13241
14316
|
this._rootNode = null;
|
|
13242
14317
|
}
|
|
13243
14318
|
}
|
|
@@ -13257,6 +14332,9 @@ class State extends HTMLElementBase {
|
|
|
13257
14332
|
get listKeys() {
|
|
13258
14333
|
return this._listKeys;
|
|
13259
14334
|
}
|
|
14335
|
+
get watchPaths() {
|
|
14336
|
+
return this._watchPaths;
|
|
14337
|
+
}
|
|
13260
14338
|
get elementPaths() {
|
|
13261
14339
|
return this._elementPaths;
|
|
13262
14340
|
}
|
|
@@ -13568,6 +14646,8 @@ const builtinFilterMeta = {
|
|
|
13568
14646
|
mul: { description: "乗算", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
|
|
13569
14647
|
div: { description: "除算", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
|
|
13570
14648
|
mod: { description: "剰余", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
|
|
14649
|
+
abs: { description: "絶対値", hasArgs: false, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 0 },
|
|
14650
|
+
clamp: { description: "範囲内に丸める (min,max)", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 2, maxArgs: 2, argTypes: ["number", "number"] },
|
|
13571
14651
|
// 数値フォーマット
|
|
13572
14652
|
fix: { description: "固定小数点表記", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
|
|
13573
14653
|
locale: { description: "ロケール形式で数値フォーマット", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["string"] },
|
|
@@ -13581,6 +14661,8 @@ const builtinFilterMeta = {
|
|
|
13581
14661
|
pad: { description: "パディング (length[,char])", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
|
|
13582
14662
|
rep: { description: "繰り返し (count)", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
|
|
13583
14663
|
rev: { description: "文字順を反転", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
|
|
14664
|
+
truncate: { description: "切り詰めて省略記号 (length[,suffix])", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
|
|
14665
|
+
join: { description: "配列を連結 ([separator])", hasArgs: true, resultType: "string", acceptTypes: ["array"], minArgs: 0, maxArgs: 1, argTypes: ["string"] },
|
|
13584
14666
|
// 数値パース・丸め
|
|
13585
14667
|
int: { description: "整数にパース", hasArgs: false, resultType: "number", acceptTypes: ["string", "number"], minArgs: 0, maxArgs: 0 },
|
|
13586
14668
|
float: { description: "浮動小数点数にパース", hasArgs: false, resultType: "number", acceptTypes: ["string", "number"], minArgs: 0, maxArgs: 0 },
|
|
@@ -13588,11 +14670,15 @@ const builtinFilterMeta = {
|
|
|
13588
14670
|
floor: { description: "切り下げ", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
|
|
13589
14671
|
ceil: { description: "切り上げ", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
|
|
13590
14672
|
percent: { description: "パーセンテージ形式", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
|
|
14673
|
+
// number だけでなく string も受ける。実用チェーンは fix / percent の後ろに繋がり、
|
|
14674
|
+
// それらは既に string を返すため(builtinFilters.ts の unit を参照)
|
|
14675
|
+
unit: { description: "単位(接尾辞)を付加", hasArgs: true, resultType: "string", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["string"] },
|
|
13591
14676
|
// 日付・時刻
|
|
13592
14677
|
date: { description: "ロケール形式の日付", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
|
|
13593
14678
|
time: { description: "ロケール形式の時刻", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
|
|
13594
14679
|
datetime: { description: "ロケール形式の日時", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
|
|
13595
14680
|
ymd: { description: "YYYY-MM-DD 形式", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
|
|
14681
|
+
hms: { description: "HH:MM:SS 形式", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
|
|
13596
14682
|
// 真偽値・変換
|
|
13597
14683
|
falsy: { description: "偽値か判定", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
|
|
13598
14684
|
truthy: { description: "真値か判定", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
|
|
@@ -13654,6 +14740,7 @@ function getWcsManifest() {
|
|
|
13654
14740
|
STATE_EVENT_TOKENS_NAME,
|
|
13655
14741
|
STATE_ON_NAME,
|
|
13656
14742
|
STATE_STREAMS_NAME,
|
|
14743
|
+
STATE_WATCH_NAME,
|
|
13657
14744
|
STATE_LIST_KEYS_NAME,
|
|
13658
14745
|
STATE_STREAM_STATUS_NAMESPACE_NAME,
|
|
13659
14746
|
STATE_STREAM_ERROR_NAMESPACE_NAME,
|