@wcstack/state 1.21.7 → 1.22.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 +39 -6
- package/README.md +39 -6
- package/dist/index.esm.js +50 -18
- 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/README.ja.md
CHANGED
|
@@ -344,7 +344,7 @@ property[#modifier]: path[@state][|filter[|filter(args)...]]
|
|
|
344
344
|
|
|
345
345
|
### バインディング authority (`#init=` / `#sync=`)
|
|
346
346
|
|
|
347
|
-
`static wcBindable` を宣言したカスタム要素への prop バインディングは、**authority
|
|
347
|
+
`static wcBindable` を宣言したカスタム要素への prop バインディングは、**authority**(バインディング attach 時の**初期同期**をどちら側が勝つか)を解決します。定常時の方向は authority とは別に、メンバの宣言形状で決まります: output-only メンバは state からの書き込みを恒久的に受け付けず(契約)、双方向メンバは初期同期の勝者と無関係に以後は両方向に流れます。既定 authority はメンバの宣言位置から導出されます(`enableDirectionalInitialSync` で既定 ON):
|
|
348
348
|
|
|
349
349
|
| メンバの宣言位置 | 既定 authority | 効果 |
|
|
350
350
|
|---|---|---|
|
|
@@ -357,12 +357,19 @@ property[#modifier]: path[@state][|filter[|filter(args)...]]
|
|
|
357
357
|
|
|
358
358
|
authority はバインディング単位で `#init=` により上書きできます:
|
|
359
359
|
|
|
360
|
-
| 値 |
|
|
360
|
+
| 値 | 初期同期 | 使える宣言 |
|
|
361
361
|
|---|---|---|
|
|
362
|
-
| `init=state` | state
|
|
363
|
-
| `init=element` |
|
|
362
|
+
| `init=state` | state の値を要素へ書く(双方向の既定) | inputs のみ・双方向 |
|
|
363
|
+
| `init=element` | 要素のスナップショットを state へ入れる — 双方向メンバではその後は通常の双方向に戻る(次の変更から state→element も流れる) | output-only・双方向 |
|
|
364
364
|
| `init=auto` | state スロットが未初期化なら `element`、それ以外は `state` | 双方向 |
|
|
365
|
-
| `init=none` |
|
|
365
|
+
| `init=none` | 初期同期なし — 次の変更から通常どおり流れる(event バインディングはこの値のみ許可) | すべて |
|
|
366
|
+
|
|
367
|
+
`#init=` が決めるのは初期競合の勝者だけです。state→element 書き込みの**恒久**抑止は「メンバが output-only 宣言であること」から来るのであって、修飾子からは来ません。これにより `#init=element`(または `#init=auto`)が **load-before-bind** の宣言的な解になります: 要素が自身の `connectedCallback` で永続値をロード済み(バインディング確立より先)でも state 初期値に潰されず、以後の state 変更は要素へ届きます(例えば `<wcs-storage>` の保存は生き続けます):
|
|
368
|
+
|
|
369
|
+
```html
|
|
370
|
+
<!-- 永続化済みリストが todos を初期化し、以後の todos 代入は保存もされる -->
|
|
371
|
+
<wcs-storage key="todos" type="local" data-wcs="value#init=element: todos"></wcs-storage>
|
|
372
|
+
```
|
|
366
373
|
|
|
367
374
|
`#sync=` は element authority のバインディングで要素スナップショットを**いつ**読むかを制御します:
|
|
368
375
|
|
|
@@ -377,10 +384,13 @@ authority はバインディング単位で `#init=` により上書きできま
|
|
|
377
384
|
<x-widget data-wcs="value#init=element,sync=connect: widget.snapshot"></x-widget>
|
|
378
385
|
```
|
|
379
386
|
|
|
387
|
+
`sync=connect` では、接続時スナップショットが初期競合を解決するまで state→element 書き込みも保留されます。
|
|
388
|
+
|
|
380
389
|
注意:
|
|
381
390
|
|
|
382
391
|
- `enableDirectionalInitialSync: false`(opt-out)のとき `#init=`/`#sync=` を書くと throw します。
|
|
383
392
|
- **1.20 以前からの移行:** output-only メンバに対して state 側に都合のよい初期値(`value: []`、`query: ""` 等)をシードしないでください — 要素側の実初期値(多くは `null`/`undefined`)がシードを置き換えます。シードは要素の実初期値に合わせ、表示用の値は派生 getter で null ガードしてください。
|
|
393
|
+
- **1.21.x まで**、`init=element` / `init=auto` / `init=none` はバインディングの生存期間全体で state→element 書き込みを抑止しており、真に双方向なメンバには使えませんでした。現在は authority は初期同期のみを支配します(`docs/architecture-hardening/09-remediation-design.md` §3.6)。
|
|
384
394
|
|
|
385
395
|
### ラジオボタンバインディング
|
|
386
396
|
|
|
@@ -1485,9 +1495,32 @@ stream の値は普通のプロパティで、コンパニオンの status / err
|
|
|
1485
1495
|
|
|
1486
1496
|
error 時、プロパティは直前の fold 結果を保持し、エラーは `$streamError.<name>` に入ります。`done` / `error` の stream も依存の変化で restart します(再試行 = 依存の叩き直し)。
|
|
1487
1497
|
|
|
1498
|
+
**イベント API の橋渡し** —— 実際の source の多くはコールバック型(`EventSource`・`WebSocket`・DOM イベント)で、async iterable ではありません。標準の `ReadableStream` で包んでください: `start` で enqueue し、`cancel` でリソースを解放します。`AbortSignal` には一切触れません —— restart / 破棄時は runtime が reader を cancel し、parked read を強制解放して `cancel()` まで届けます:
|
|
1499
|
+
|
|
1500
|
+
```js
|
|
1501
|
+
$streams: {
|
|
1502
|
+
metrics: {
|
|
1503
|
+
args: (state) => ({ host: state.host }),
|
|
1504
|
+
source: ({ host }) => {
|
|
1505
|
+
const es = new EventSource(`/api/metrics?host=${host}`);
|
|
1506
|
+
return new ReadableStream({
|
|
1507
|
+
start(controller) {
|
|
1508
|
+
es.addEventListener("metric", (e) => controller.enqueue(JSON.parse(e.data)));
|
|
1509
|
+
},
|
|
1510
|
+
cancel() { es.close(); }, // restart / 破棄時に走る
|
|
1511
|
+
});
|
|
1512
|
+
},
|
|
1513
|
+
fold: (acc, sample) => [...acc, sample].slice(-20),
|
|
1514
|
+
initial: [],
|
|
1515
|
+
},
|
|
1516
|
+
},
|
|
1517
|
+
```
|
|
1518
|
+
|
|
1519
|
+
手書きの async generator も producer 側の制御が要るときは引き続き完全サポートですが、abort 救済は*部分的*です: `signal` を無視して `await` で park した generator は外から強制解放できません。イベント API の橋渡しには `ReadableStream` 形を推奨します。
|
|
1520
|
+
|
|
1488
1521
|
重要な規範:
|
|
1489
1522
|
|
|
1490
|
-
- **協調キャンセル(MUST)** —— `source` は渡された `AbortSignal`
|
|
1523
|
+
- **協調キャンセル(MUST)** —— `source` は渡された `AbortSignal` を必ず監視し、発火したら生産を停止すること。`ReadableStream` の source はこの契約を `cancel()` コールバック経由で自動的に満たします(abort 時に runtime が reader を cancel します)。自前の async iterable だけが `signal` を自分で監視する必要があります。
|
|
1491
1524
|
- **有界 fold** —— 需要は producer に逆流しません(backpressure は明示的に放棄)。無限 / 長寿命ストリームでは latest・count・last-N(`(acc, chunk) => [...acc.slice(-99), chunk]`)・ウィンドウ集計など有界な fold を使うこと。生の全チャンク累積は有限ストリーム限定。
|
|
1492
1525
|
- **`args` は同期** —— Promise を返すとエラー。`args` 内での wildcard 読みも拒否されます。
|
|
1493
1526
|
- **自己依存・相互サイクルの禁止** —— `args` が自 stream の値や status を読むとエラーになります。2 つの stream の相互サイクル(A の `args` が B の値を読み、B の `args` が A の値を読む)は検出されず無限 restart になるため組まないこと。一方向のチェイン(A の値を B の `args` が読む)は正当です。
|
package/README.md
CHANGED
|
@@ -344,7 +344,7 @@ Automatically enabled for:
|
|
|
344
344
|
|
|
345
345
|
### Binding Authority (`#init=` / `#sync=`)
|
|
346
346
|
|
|
347
|
-
For custom elements that declare `static wcBindable`, every prop binding resolves an **authority** — which side
|
|
347
|
+
For custom elements that declare `static wcBindable`, every prop binding resolves an **authority** — which side wins the **initial sync** when the binding attaches. The steady-state direction is decided separately, by the member's declared shape: an output-only member never accepts state writes (a permanent contract), while a two-way member flows both ways after the initial sync regardless of which side won it. The default authority is derived from where the member is declared (on by default via `enableDirectionalInitialSync`):
|
|
348
348
|
|
|
349
349
|
| Member declared in | Default authority | Effect |
|
|
350
350
|
|---|---|---|
|
|
@@ -357,12 +357,19 @@ For custom elements that declare `static wcBindable`, every prop binding resolve
|
|
|
357
357
|
|
|
358
358
|
Override the authority per binding with `#init=`:
|
|
359
359
|
|
|
360
|
-
| Value |
|
|
360
|
+
| Value | Initial sync | Allowed on |
|
|
361
361
|
|---|---|---|
|
|
362
|
-
| `init=state` |
|
|
363
|
-
| `init=element` |
|
|
362
|
+
| `init=state` | The state value is written to the element (two-way default) | inputs-only, two-way |
|
|
363
|
+
| `init=element` | The element's snapshot seeds the state instead — on a two-way member the binding then continues as normal two-way (state→element writes flow from the next change on) | output-only, two-way |
|
|
364
364
|
| `init=auto` | `element` if the state slot is uninitialized, otherwise `state` | two-way |
|
|
365
|
-
| `init=none` | No initial sync (event bindings accept only this value) | any |
|
|
365
|
+
| `init=none` | No initial sync — changes flow normally from the next update (event bindings accept only this value) | any |
|
|
366
|
+
|
|
367
|
+
`#init=` decides only who wins the initial race. The *permanent* suppression of state→element writes comes from the member being declared output-only, never from the modifier. This makes `#init=element` (or `#init=auto`) the declarative fix for **load-before-bind**: an element that loads a persisted value in its own `connectedCallback` — before the binding attaches — is no longer clobbered by the state seed, and later state changes still reach the element (so e.g. `<wcs-storage>` keeps saving):
|
|
368
|
+
|
|
369
|
+
```html
|
|
370
|
+
<!-- The persisted list seeds `todos`; assigning `todos` later still saves. -->
|
|
371
|
+
<wcs-storage key="todos" type="local" data-wcs="value#init=element: todos"></wcs-storage>
|
|
372
|
+
```
|
|
366
373
|
|
|
367
374
|
`#sync=` controls **when** the element snapshot is read for element-authority bindings:
|
|
368
375
|
|
|
@@ -377,10 +384,13 @@ Override the authority per binding with `#init=`:
|
|
|
377
384
|
<x-widget data-wcs="value#init=element,sync=connect: widget.snapshot"></x-widget>
|
|
378
385
|
```
|
|
379
386
|
|
|
387
|
+
With `sync=connect`, state→element writes stay suppressed until the connect snapshot has resolved the initial race.
|
|
388
|
+
|
|
380
389
|
Notes:
|
|
381
390
|
|
|
382
391
|
- With `enableDirectionalInitialSync: false` (opt-out), writing `#init=`/`#sync=` throws.
|
|
383
392
|
- **Migrating from ≤ 1.20:** do not seed state with placeholder values (`value: []`, `query: ""`) for output-only members — the element's real initial value (often `null`/`undefined`) replaces the seed. Match the seed to the element's actual initial value and null-guard display values with a derived getter.
|
|
393
|
+
- **Until 1.21.x**, `init=element` / `init=auto` / `init=none` suppressed state→element writes for the binding's whole lifetime, which made them unusable on genuinely two-way members. Authority now governs only the initial sync (`docs/architecture-hardening/09-remediation-design.md` §3.6).
|
|
384
394
|
|
|
385
395
|
### Radio Binding
|
|
386
396
|
|
|
@@ -1486,9 +1496,32 @@ The stream's value is an ordinary property, and its companion status / error liv
|
|
|
1486
1496
|
|
|
1487
1497
|
On error the property keeps its last folded value and the error lands in `$streamError.<name>`; a `done` or `error` stream restarts when its dependencies change (retrying = re-hitting the dependency).
|
|
1488
1498
|
|
|
1499
|
+
**Bridging an event API** — most real sources are callback-shaped (`EventSource`, `WebSocket`, DOM events), not async iterables. Wrap them in a standard `ReadableStream`: enqueue in `start`, release the resource in `cancel`. You never touch the `AbortSignal` — on restart/dispose the runtime cancels its reader, which force-unwinds a parked read and runs your `cancel()`:
|
|
1500
|
+
|
|
1501
|
+
```js
|
|
1502
|
+
$streams: {
|
|
1503
|
+
metrics: {
|
|
1504
|
+
args: (state) => ({ host: state.host }),
|
|
1505
|
+
source: ({ host }) => {
|
|
1506
|
+
const es = new EventSource(`/api/metrics?host=${host}`);
|
|
1507
|
+
return new ReadableStream({
|
|
1508
|
+
start(controller) {
|
|
1509
|
+
es.addEventListener("metric", (e) => controller.enqueue(JSON.parse(e.data)));
|
|
1510
|
+
},
|
|
1511
|
+
cancel() { es.close(); }, // runs on restart / dispose
|
|
1512
|
+
});
|
|
1513
|
+
},
|
|
1514
|
+
fold: (acc, sample) => [...acc, sample].slice(-20),
|
|
1515
|
+
initial: [],
|
|
1516
|
+
},
|
|
1517
|
+
},
|
|
1518
|
+
```
|
|
1519
|
+
|
|
1520
|
+
Hand-rolled async generators remain fully supported when you need producer-side control, but they are only *partially* rescued on abort: a generator parked in an `await` that ignores `signal` cannot be force-unwound from outside. Prefer the `ReadableStream` form for event-API bridging.
|
|
1521
|
+
|
|
1489
1522
|
Key rules:
|
|
1490
1523
|
|
|
1491
|
-
- **Cooperative cancellation (MUST)** — `source` must observe the passed `AbortSignal` and stop producing when it fires.
|
|
1524
|
+
- **Cooperative cancellation (MUST)** — `source` must observe the passed `AbortSignal` and stop producing when it fires. A `ReadableStream` source satisfies this automatically through its `cancel()` callback (the runtime cancels the reader on abort); only hand-rolled async iterables need to observe `signal` themselves.
|
|
1492
1525
|
- **Bounded fold** — demand never flows back to the producer (backpressure is deliberately abandoned). For infinite / long-lived streams use a bounded fold — latest, count, last-N (`(acc, chunk) => [...acc.slice(-99), chunk]`), windowed aggregates. Raw accumulation of every chunk is for finite streams only.
|
|
1493
1526
|
- **`args` is synchronous** — returning a Promise is an error, and wildcard reads inside `args` are rejected.
|
|
1494
1527
|
- **No self-dependency, no mutual cycles** — `args` reading the stream's own value or status raises an error. Mutual cycles between two streams (A's `args` reads B's value and vice versa) are not detected and restart forever — do not build them. One-way chains (A's value feeding B's `args`) are legitimate.
|
package/dist/index.esm.js
CHANGED
|
@@ -3397,12 +3397,12 @@ function hasInitialSyncModifier(binding) {
|
|
|
3397
3397
|
// 頻出ポリシー(修飾子なしの通常バインディング)の凍結シングルトン。リスト行では
|
|
3398
3398
|
// binding ごとに resolveInitialSyncPolicy が走るため、毎回のオブジェクト割り当てを
|
|
3399
3399
|
// 避ける(record.initialPolicy は読み取り専用でしか使われない)。
|
|
3400
|
-
const STATE_CALL_POLICY = Object.freeze({ authority: "state", syncOn: "call", observable: false });
|
|
3401
|
-
const NONE_CALL_POLICY = Object.freeze({ authority: "none", syncOn: "call", observable: false });
|
|
3400
|
+
const STATE_CALL_POLICY = Object.freeze({ authority: "state", syncOn: "call", observable: false, outputOnly: false });
|
|
3401
|
+
const NONE_CALL_POLICY = Object.freeze({ authority: "none", syncOn: "call", observable: false, outputOnly: false });
|
|
3402
3402
|
function statePolicy(authority, syncOn) {
|
|
3403
3403
|
if (authority === "state" && syncOn === "call")
|
|
3404
3404
|
return STATE_CALL_POLICY;
|
|
3405
|
-
return { authority, syncOn, observable: false };
|
|
3405
|
+
return { authority, syncOn, observable: false, outputOnly: false };
|
|
3406
3406
|
}
|
|
3407
3407
|
function resolveInitialSyncPolicy(binding) {
|
|
3408
3408
|
if (!config.enableDirectionalInitialSync) {
|
|
@@ -3417,7 +3417,7 @@ function resolveInitialSyncPolicy(binding) {
|
|
|
3417
3417
|
if (explicitAuthority !== null && explicitAuthority !== "none") {
|
|
3418
3418
|
raiseError("Event bindings only allow init=none.");
|
|
3419
3419
|
}
|
|
3420
|
-
return syncOn === "call" ? NONE_CALL_POLICY : { authority: "none", syncOn, observable: false };
|
|
3420
|
+
return syncOn === "call" ? NONE_CALL_POLICY : { authority: "none", syncOn, observable: false, outputOnly: false };
|
|
3421
3421
|
}
|
|
3422
3422
|
// command.<name>: $command.<method> は命令的な command-token 配線。bindingType は
|
|
3423
3423
|
// "prop" だが propName ("command.<name>") は wcBindable property ではないため、下の
|
|
@@ -3455,7 +3455,7 @@ function resolveInitialSyncPolicy(binding) {
|
|
|
3455
3455
|
if (syncOn === "connect" && !hasOutput) {
|
|
3456
3456
|
raiseError(`sync=connect requires observable property "${binding.propName}".`);
|
|
3457
3457
|
}
|
|
3458
|
-
return { authority, syncOn, observable: hasOutput };
|
|
3458
|
+
return { authority, syncOn, observable: hasOutput, outputOnly: hasOutput && !hasInput };
|
|
3459
3459
|
}
|
|
3460
3460
|
function isBindingStateInitialized(binding) {
|
|
3461
3461
|
const rootNode = binding.replaceNode.getRootNode();
|
|
@@ -3756,9 +3756,25 @@ class BindingSession {
|
|
|
3756
3756
|
return true;
|
|
3757
3757
|
if (!record.options.registerAddress || record.phase === "waiting-definition")
|
|
3758
3758
|
return true;
|
|
3759
|
+
if (record.phase === "failed")
|
|
3760
|
+
return false;
|
|
3759
3761
|
if (record.phase === "active")
|
|
3760
3762
|
this.settleInitialRecord(record);
|
|
3761
|
-
|
|
3763
|
+
// authority は初期同期のみを支配する(09 §3.6: init=element は「snapshot を
|
|
3764
|
+
// state へ入れる」、init=none は「次の変更から扱う」)。settle 後の最初の相談
|
|
3765
|
+
// = 初期 state sweep / 初回 render / deferred initial apply の選別なので
|
|
3766
|
+
// authority で答え、ここで初期適用を消費する。
|
|
3767
|
+
if (!record.initialApplyDone) {
|
|
3768
|
+
record.initialApplyDone = true;
|
|
3769
|
+
return record.resolvedAuthority === "state";
|
|
3770
|
+
}
|
|
3771
|
+
if (record.resolvedAuthority === "state")
|
|
3772
|
+
return true;
|
|
3773
|
+
// 定常: two-way / input member は authority と無関係に state→element を流す。
|
|
3774
|
+
// 恒久ブロックは (1) output-only member の契約(書き込み無意味・DCC/router の
|
|
3775
|
+
// conformance が依存)と (2) sync=connect の接続 snapshot が未解決の間
|
|
3776
|
+
//(初期競合が未決着のうちは state push が element 初期値を潰しうる)だけ。
|
|
3777
|
+
return !record.outputOnlyMember && !record.observationPending;
|
|
3762
3778
|
}
|
|
3763
3779
|
getRecord(binding) {
|
|
3764
3780
|
const record = recordByBinding.get(binding);
|
|
@@ -4026,6 +4042,8 @@ class BindingSession {
|
|
|
4026
4042
|
initialPolicy: slot.policy,
|
|
4027
4043
|
resolvedAuthority: slot.authority,
|
|
4028
4044
|
initialSettled: true,
|
|
4045
|
+
initialApplyDone: false,
|
|
4046
|
+
outputOnlyMember: slot.policy.outputOnly,
|
|
4029
4047
|
observationPending: false,
|
|
4030
4048
|
eventSequence: 0,
|
|
4031
4049
|
hasProducerValue: false,
|
|
@@ -4079,6 +4097,8 @@ class BindingSession {
|
|
|
4079
4097
|
record.initialPolicy = slot.policy;
|
|
4080
4098
|
record.resolvedAuthority = slot.authority;
|
|
4081
4099
|
record.initialSettled = true;
|
|
4100
|
+
record.initialApplyDone = false;
|
|
4101
|
+
record.outputOnlyMember = slot.policy.outputOnly;
|
|
4082
4102
|
this.records.add(record);
|
|
4083
4103
|
if (slot.isEvent) {
|
|
4084
4104
|
try {
|
|
@@ -4137,6 +4157,8 @@ class BindingSession {
|
|
|
4137
4157
|
initialPolicy: null,
|
|
4138
4158
|
resolvedAuthority: null,
|
|
4139
4159
|
initialSettled: false,
|
|
4160
|
+
initialApplyDone: false,
|
|
4161
|
+
outputOnlyMember: false,
|
|
4140
4162
|
observationPending: false,
|
|
4141
4163
|
eventSequence: 0,
|
|
4142
4164
|
hasProducerValue: false,
|
|
@@ -4265,6 +4287,7 @@ class BindingSession {
|
|
|
4265
4287
|
record.initialPolicy = policy;
|
|
4266
4288
|
record.resolvedAuthority = authority;
|
|
4267
4289
|
record.initialSettled = true;
|
|
4290
|
+
record.outputOnlyMember = policy.outputOnly;
|
|
4268
4291
|
record.phase = "active";
|
|
4269
4292
|
if (!policy.observable)
|
|
4270
4293
|
return;
|
|
@@ -6873,7 +6896,7 @@ async function buildBindings(root) {
|
|
|
6873
6896
|
}
|
|
6874
6897
|
}
|
|
6875
6898
|
|
|
6876
|
-
var version = "1.
|
|
6899
|
+
var version = "1.22.0";
|
|
6877
6900
|
var pkg = {
|
|
6878
6901
|
version: version};
|
|
6879
6902
|
|
|
@@ -8858,7 +8881,11 @@ function traceArgs(stateElement, entry) {
|
|
|
8858
8881
|
* Rescue levels on abort:
|
|
8859
8882
|
* - ReadableStream: FULLY rescued. A parked read() is force-unwound via
|
|
8860
8883
|
* reader.cancel(), which both releases the underlying source and settles the
|
|
8861
|
-
* pending read() so the loop unwinds.
|
|
8884
|
+
* pending read() so the loop unwinds. Anything getReader-bearing is routed
|
|
8885
|
+
* through this path even when it is ALSO natively async-iterable (Node 18+ /
|
|
8886
|
+
* Chrome 124+ / Firefox): the spec serializes asyncIterator.return() behind
|
|
8887
|
+
* the pending next() ([[ongoingPromise]]), so the AsyncIterable rescue below
|
|
8888
|
+
* cannot force-unwind a parked read on a native ReadableStream.
|
|
8862
8889
|
* - AsyncIterable / async generator: PARTIALLY rescued. On abort we call
|
|
8863
8890
|
* iterator.return() to trigger the generator's finally/cleanup. But a parked
|
|
8864
8891
|
* `await` (the producer stalling before its next yield while IGNORING `signal`)
|
|
@@ -8939,19 +8966,24 @@ async function consumeSource(source, args, signal, sink) {
|
|
|
8939
8966
|
}
|
|
8940
8967
|
}
|
|
8941
8968
|
function iterate(produced, signal) {
|
|
8942
|
-
//
|
|
8943
|
-
//
|
|
8944
|
-
//
|
|
8969
|
+
// Anything ReadableStream-shaped (has getReader) takes the reader path FIRST —
|
|
8970
|
+
// even when it is also async-iterable. On runtimes where ReadableStream
|
|
8971
|
+
// implements native async iteration (Node 18+ / Chrome 124+ / Firefox), the
|
|
8972
|
+
// spec serializes `iterator.return()` behind the pending `next()`
|
|
8973
|
+
// ([[ongoingPromise]]), so the AsyncIterable rescue cannot force-unwind a
|
|
8974
|
+
// parked read: abort would hang the consume task until the producer's next
|
|
8975
|
+
// chunk (or forever on a silent stream), leaking the underlying source.
|
|
8976
|
+
// `reader.cancel()` has no such ordering — routing every getReader-bearing
|
|
8977
|
+
// source through it keeps the FULL rescue promised for ReadableStream.
|
|
8978
|
+
// Optional chaining: a null/undefined source return value must fall through to
|
|
8979
|
+
// the explicit TypeError below, not throw an opaque property access error.
|
|
8980
|
+
if (typeof produced?.getReader === "function") {
|
|
8981
|
+
return readableToAsyncIterable(produced, signal);
|
|
8982
|
+
}
|
|
8945
8983
|
if (typeof produced?.[Symbol.asyncIterator] === "function") {
|
|
8946
8984
|
return produced;
|
|
8947
8985
|
}
|
|
8948
|
-
|
|
8949
|
-
// a wrong source value yields a clear error instead of an opaque "getReader is
|
|
8950
|
-
// not a function" from inside the generator.
|
|
8951
|
-
if (typeof produced?.getReader !== "function") {
|
|
8952
|
-
throw new TypeError("[@wcstack/state] $streams: source must return an AsyncIterable or a ReadableStream (got neither).");
|
|
8953
|
-
}
|
|
8954
|
-
return readableToAsyncIterable(produced, signal);
|
|
8986
|
+
throw new TypeError("[@wcstack/state] $streams: source must return an AsyncIterable or a ReadableStream (got neither).");
|
|
8955
8987
|
}
|
|
8956
8988
|
async function* readableToAsyncIterable(stream, signal) {
|
|
8957
8989
|
const reader = stream.getReader();
|