unikvs 0.0.12 → 0.0.13

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.
Files changed (82) hide show
  1. package/README.md +2 -2
  2. package/dist/index.d.ts +1553 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +4032 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +34 -34
  7. package/src/_contexts.ts +1089 -0
  8. package/src/_logger.ts +5 -3
  9. package/src/_middleware.ts +207 -0
  10. package/src/_pipeline-graph.helpers.ts +183 -0
  11. package/src/_pipeline-graph.ts +1307 -0
  12. package/src/_pipeline-graph.types.ts +303 -0
  13. package/src/_pipeline-leaf-resolver.ts +70 -0
  14. package/src/_plugin-lifecycle.ts +201 -0
  15. package/src/_storage.ts +169 -115
  16. package/src/_streams.schemas.ts +21 -0
  17. package/src/_streams.ts +32 -6
  18. package/src/_to-value-stream.ts +22 -11
  19. package/src/_types.ts +237 -0
  20. package/src/_unikvs-branch.ts +94 -0
  21. package/src/_unikvs.schemas.ts +451 -0
  22. package/src/_valibot.ts +17 -6
  23. package/src/_value-schemas.ts +36 -206
  24. package/src/_value-validation.ts +57 -0
  25. package/src/errors.ts +391 -148
  26. package/src/index.ts +46 -25
  27. package/src/{value-stream.types.ts → types.ts} +1 -1
  28. package/src/unikvs-branch.ts +445 -0
  29. package/src/unikvs-builder.ts +706 -0
  30. package/src/unikvs.ts +1423 -1007
  31. package/src/unikvs.types.ts +194 -0
  32. package/src/value-schemas.ts +98 -0
  33. package/dist/src/_logger.d.ts +0 -3
  34. package/dist/src/_logger.d.ts.map +0 -1
  35. package/dist/src/_logger.js +0 -3
  36. package/dist/src/_merge-variables.d.ts +0 -13
  37. package/dist/src/_merge-variables.d.ts.map +0 -1
  38. package/dist/src/_merge-variables.js +0 -12
  39. package/dist/src/_storage.d.ts +0 -18
  40. package/dist/src/_storage.d.ts.map +0 -1
  41. package/dist/src/_storage.js +0 -103
  42. package/dist/src/_streams.d.ts +0 -20
  43. package/dist/src/_streams.d.ts.map +0 -1
  44. package/dist/src/_streams.js +0 -36
  45. package/dist/src/_to-value-stream.d.ts +0 -12
  46. package/dist/src/_to-value-stream.d.ts.map +0 -1
  47. package/dist/src/_to-value-stream.js +0 -102
  48. package/dist/src/_transformer.d.ts +0 -13
  49. package/dist/src/_transformer.d.ts.map +0 -1
  50. package/dist/src/_transformer.js +0 -60
  51. package/dist/src/_valibot.d.ts +0 -15
  52. package/dist/src/_valibot.d.ts.map +0 -1
  53. package/dist/src/_valibot.js +0 -23
  54. package/dist/src/_value-schemas.d.ts +0 -179
  55. package/dist/src/_value-schemas.d.ts.map +0 -1
  56. package/dist/src/_value-schemas.js +0 -150
  57. package/dist/src/errors.d.ts +0 -306
  58. package/dist/src/errors.d.ts.map +0 -1
  59. package/dist/src/errors.js +0 -234
  60. package/dist/src/index.d.ts +0 -10
  61. package/dist/src/index.d.ts.map +0 -1
  62. package/dist/src/index.js +0 -3
  63. package/dist/src/unikvs-config.d.ts +0 -174
  64. package/dist/src/unikvs-config.d.ts.map +0 -1
  65. package/dist/src/unikvs-config.js +0 -100
  66. package/dist/src/unikvs.d.ts +0 -272
  67. package/dist/src/unikvs.d.ts.map +0 -1
  68. package/dist/src/unikvs.js +0 -928
  69. package/dist/src/utils.types.d.ts +0 -5
  70. package/dist/src/utils.types.d.ts.map +0 -1
  71. package/dist/src/utils.types.js +0 -1
  72. package/dist/src/value-stream.types.d.ts +0 -10
  73. package/dist/src/value-stream.types.d.ts.map +0 -1
  74. package/dist/src/value-stream.types.js +0 -1
  75. package/dist/src/variables.types.d.ts +0 -11
  76. package/dist/src/variables.types.d.ts.map +0 -1
  77. package/dist/src/variables.types.js +0 -1
  78. package/src/_merge-variables.ts +0 -19
  79. package/src/_transformer.ts +0 -89
  80. package/src/unikvs-config.ts +0 -401
  81. package/src/utils.types.ts +0 -4
  82. package/src/variables.types.ts +0 -13
@@ -0,0 +1,1307 @@
1
+ import { type IReadableStream, RepairNotAllowedError } from "@unikvs/core";
2
+
3
+ import {
4
+ DecodeContext,
5
+ EncodeContext,
6
+ DecodeStreamContext,
7
+ EncodeStreamContext,
8
+ } from "./_contexts.js";
9
+ import { logger, repairLogger } from "./_logger.js";
10
+ import type UniKvsMiddleware from "./_middleware.js";
11
+ import {
12
+ applyMapKey,
13
+ createEntries,
14
+ activeLeavesOf,
15
+ freezeDestination,
16
+ requireDestination,
17
+ requireStreamDestination,
18
+ } from "./_pipeline-graph.helpers.js";
19
+ import type {
20
+ MutableNode,
21
+ EncodeTarget,
22
+ PipelineLeaf,
23
+ PipelineNode,
24
+ PipelineEntry,
25
+ OperationScope,
26
+ StreamEncodeTree,
27
+ StreamEncodeTarget,
28
+ PipelineDestination,
29
+ BuildEncodeTreeOptions,
30
+ } from "./_pipeline-graph.types.js";
31
+ import { LeafResolver } from "./_pipeline-leaf-resolver.js";
32
+ import { cancelStreams } from "./_streams.js";
33
+ import {
34
+ MiddlewareNextNotCalledError,
35
+ MiddlewareNextNotAwaitedError,
36
+ MiddlewareDestinationFrozenError,
37
+ MiddlewareDestinationMissingError,
38
+ MiddlewareNextCalledMultipleTimesError,
39
+ } from "./errors.js";
40
+
41
+ /**
42
+ * 節を前順で列挙するための再帰ヘルパーです。
43
+ *
44
+ * @param node 列挙を開始する節です。
45
+ *
46
+ * @yields 前順の節です。
47
+ */
48
+ function* visit(node: PipelineNode): IterableIterator<PipelineNode> {
49
+ yield node;
50
+ for (const entry of node.entries) {
51
+ if (entry.kind === "node") {
52
+ yield* visit(entry.node);
53
+ }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * 直通区間をまとめて葉名簿と全葉を確定します。
59
+ *
60
+ * @param node 確定する節です。
61
+ */
62
+ function finalize(node: MutableNode): void {
63
+ // 直通の節は子の葉名簿と全葉を共有します。
64
+ if (node.leaves.length === 0 && node.children.length === 1) {
65
+ const [child] = node.children;
66
+
67
+ if (child === undefined) {
68
+ throw new Error("直通区間の子節が存在しません。");
69
+ }
70
+
71
+ finalize(child);
72
+ node.entries = [
73
+ {
74
+ kind: "node",
75
+ node: child,
76
+ },
77
+ ];
78
+ node.allLeaves = child.allLeaves;
79
+ node.leafNames = child.leafNames;
80
+ } else {
81
+ const leaves = [...node.leaves];
82
+
83
+ for (const child of node.children) {
84
+ finalize(child);
85
+ leaves.push(...child.allLeaves);
86
+ }
87
+
88
+ leaves.sort((a, b) => a.index - b.index);
89
+
90
+ const [singleLeaf] = leaves;
91
+
92
+ node.entries = createEntries(node.leaves, node.children);
93
+ node.allLeaves = leaves;
94
+ if (leaves.length === 1 && singleLeaf !== undefined) {
95
+ node.leafNames = singleLeaf.nameNames;
96
+ } else {
97
+ node.leafNames = Object.freeze(leaves.map((leaf) => leaf.name));
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * コンパイル済みのパイプライン・グラフです。
104
+ *
105
+ * 宛先の変換チェーンをミドルウェアのインスタンス同一性で共有する根付き木として保持し、操作ごとの走査・エンコード・書き戻し・ストリーム構築を提供します。
106
+ *
107
+ * 不変条件 :
108
+ * - 葉(`leaves`)と各節の項目(`entries`)は宛先の登録順です。
109
+ * - 名簿(`leafNames` / `nameNames`)は凍結された配列で、直通区間では参照を共有します。
110
+ * - 共有節は 1 操作内で 1 回だけ `encode`/`encodeStream`/`mapKey` を実行します。分岐点ではプレーン値のエンコード結果を共有し、ストリームは tee で分岐します。
111
+ * - 書き戻しの拒否は、走査中と書き込み中の 2 経路で分類します。いずれも即時に確定します。
112
+ */
113
+
114
+ export class PipelineGraph {
115
+ /**
116
+ * 全葉です。
117
+ * 登録順に保持します。
118
+ */
119
+ readonly #leaves: readonly PipelineLeaf[];
120
+
121
+ /**
122
+ * 根直下の葉と節を登録順に列挙した静的な項目列です。
123
+ */
124
+ readonly #entries: readonly PipelineEntry[];
125
+
126
+ /**
127
+ * `open`/`close`/`use` の対象となるミドルウェアです。初出順に保持します。
128
+ */
129
+ readonly #middlewares: readonly UniKvsMiddleware[];
130
+
131
+ /**
132
+ * 全葉数が 1 件かどうかです。
133
+ */
134
+ readonly #isSingleLeaf: boolean;
135
+
136
+ private constructor(
137
+ leaves: readonly PipelineLeaf[],
138
+ entries: readonly PipelineEntry[],
139
+ middlewares: readonly UniKvsMiddleware[],
140
+ ) {
141
+ this.#leaves = leaves;
142
+ this.#entries = entries;
143
+ this.#middlewares = middlewares;
144
+ this.#isSingleLeaf = leaves.length === 1;
145
+ }
146
+
147
+ /**
148
+ * 宛先定義の列を trie にコンパイルします。
149
+ *
150
+ * ミドルウェアはインスタンス同一性で共有します。同一インスタンスが複数の節に現れても、`open`/`close`/`use` の対象には初出の 1 件だけを保持します。
151
+ *
152
+ * @param destinations コンパイルする宛先定義です。
153
+ *
154
+ * @returns コンパイル済みのパイプライン・グラフを返します。
155
+ */
156
+ public static compile(destinations: readonly PipelineDestination[]): PipelineGraph {
157
+ const rootLeaves: PipelineLeaf[] = [];
158
+ const rootChildren: MutableNode[] = [];
159
+ const allLeaves: PipelineLeaf[] = [];
160
+ const middlewares: UniKvsMiddleware[] = [];
161
+ const seenMiddlewares = new Set<UniKvsMiddleware>();
162
+
163
+ for (const dest of destinations) {
164
+ // 葉の index は登録順のもとです。名簿・書き込み順・エラー識別の基準になります。
165
+ const index = allLeaves.length;
166
+ const name = dest.storage.name;
167
+ const path: PipelineNode[] = [];
168
+ let parent: MutableNode | null = null;
169
+ let canRepair = false;
170
+
171
+ let siblings = rootChildren;
172
+
173
+ for (const middleware of dest.middlewares) {
174
+ // 共有はインスタンス同一性だけで判定し、同構造でも別インスタンスは共有しません。
175
+ let node = siblings.find((candidate) => candidate.middleware === middleware);
176
+
177
+ if (node === undefined) {
178
+ node = {
179
+ leaves: [],
180
+ entries: [],
181
+ children: [],
182
+ allLeaves: [],
183
+ leafNames: [],
184
+ middleware,
185
+ };
186
+ siblings.push(node);
187
+
188
+ // open/close/use の重複実行を避けるため、初出のインスタンスだけを保持します。
189
+ if (!seenMiddlewares.has(middleware)) {
190
+ seenMiddlewares.add(middleware);
191
+ middlewares.push(middleware);
192
+ }
193
+ }
194
+ // canRepair は経路上の AllowRepair の有無の論理和で、葉ごとに確定します。
195
+ if (middleware.name === "AllowRepair") {
196
+ canRepair = true;
197
+ }
198
+
199
+ path.push(node);
200
+ parent = node;
201
+ siblings = node.children;
202
+ }
203
+
204
+ const leaf: PipelineLeaf = {
205
+ name,
206
+ path,
207
+ index,
208
+ storage: dest.storage,
209
+ canRepair,
210
+ nameNames: Object.freeze([name]),
211
+ };
212
+
213
+ if (parent === null) {
214
+ rootLeaves.push(leaf);
215
+ } else {
216
+ parent.leaves.push(leaf);
217
+ }
218
+
219
+ allLeaves.push(leaf);
220
+ }
221
+
222
+ for (const child of rootChildren) {
223
+ finalize(child);
224
+ }
225
+
226
+ let rootEntries: readonly PipelineEntry[];
227
+ let rootAllLeaves: readonly PipelineLeaf[];
228
+
229
+ if (rootLeaves.length === 0 && rootChildren.length === 1) {
230
+ const [child] = rootChildren;
231
+
232
+ if (child === undefined) {
233
+ throw new Error("根直下の子節が存在しません。");
234
+ }
235
+
236
+ rootEntries = [
237
+ {
238
+ kind: "node",
239
+ node: child,
240
+ },
241
+ ];
242
+ rootAllLeaves = child.allLeaves;
243
+ } else {
244
+ const collected = [...rootLeaves];
245
+
246
+ for (const child of rootChildren) {
247
+ collected.push(...child.allLeaves);
248
+ }
249
+
250
+ rootEntries = createEntries(rootLeaves, rootChildren);
251
+ rootAllLeaves = collected.toSorted((a, b) => a.index - b.index);
252
+ }
253
+
254
+ return new PipelineGraph(rootAllLeaves, rootEntries, middlewares);
255
+ }
256
+
257
+ /**
258
+ * 全葉です。登録順に保持します。
259
+ */
260
+ public get leaves(): readonly PipelineLeaf[] {
261
+ return this.#leaves;
262
+ }
263
+
264
+ /**
265
+ * `open`/`close`/`use` の対象となるミドルウェアです。
266
+ * 初出順・重複なしです。
267
+ */
268
+ public get middlewares(): readonly UniKvsMiddleware[] {
269
+ return this.#middlewares;
270
+ }
271
+
272
+ /**
273
+ * 全葉数が 1 件かどうかです。
274
+ */
275
+ public get isSingleLeaf(): boolean {
276
+ return this.#isSingleLeaf;
277
+ }
278
+
279
+ /**
280
+ * 葉数です。
281
+ */
282
+ public get size(): number {
283
+ return this.#leaves.length;
284
+ }
285
+
286
+ /**
287
+ * 葉を登録順に列挙します。
288
+ */
289
+ public [Symbol.iterator](): IterableIterator<PipelineLeaf> {
290
+ return this.#leaves[Symbol.iterator]();
291
+ }
292
+
293
+ /**
294
+ * 節を根から前順 (親が子より前、兄弟は登録順) に列挙します。
295
+ *
296
+ * 構造の確認・保守用です。操作の走査には使用しません。
297
+ *
298
+ * @yields 前順の節です。
299
+ */
300
+ public *nodes(): IterableIterator<PipelineNode> {
301
+ for (const entry of this.#entries) {
302
+ if (entry.kind === "node") {
303
+ yield* visit(entry.node);
304
+ }
305
+ }
306
+ }
307
+
308
+ /**
309
+ * 単一操作のスコープでキー写像をメモ化するリゾルバーを生成します。
310
+ *
311
+ * 新 `mapKey` は同期的であるため、リゾルバーも同期的です。共有節の写像は操作内で 1 回だけ実行されます。
312
+ *
313
+ * @param key 根のキー (論理キー) です。
314
+ * @param vars 根の実行時変数です。
315
+ *
316
+ * @returns 葉リゾルバーを返します。
317
+ */
318
+ public createResolver(scope: OperationScope): LeafResolver {
319
+ return new LeafResolver(scope);
320
+ }
321
+
322
+ /**
323
+ * 葉の経路のミドルウェアを逆順に適用してデータをデコードします。
324
+ *
325
+ * 各段の前後で中断を確認します。
326
+ *
327
+ * @param leaf デコード元の葉です。
328
+ * @param scope 操作共通の実行時情報です。`key` は論理キーです。
329
+ * @param data デコードするデータです。
330
+ *
331
+ * @returns デコード済みのデータを返します。
332
+ */
333
+ public async decode(leaf: PipelineLeaf, scope: OperationScope, data: unknown): Promise<unknown> {
334
+ // 経路を逆順 (葉→根) にネスト実行し、下流 (根側) の `set` が上流 (葉側) の `await next()` 後に見えるようにします。
335
+ // データは外側から内側へ鎖状に渡し、最終結果を外側へ伝播させます。
336
+ const reversed = leaf.path.toReversed();
337
+
338
+ async function run(index: number, input: unknown): Promise<unknown> {
339
+ const maybeNode = reversed[index];
340
+
341
+ if (maybeNode === undefined) {
342
+ return input;
343
+ }
344
+
345
+ const node: PipelineNode = maybeNode;
346
+
347
+ const ctx = new DecodeContext({
348
+ key: scope.key,
349
+ store: scope.store,
350
+ signal: scope.signal,
351
+ source: input,
352
+ });
353
+ let downstreamResult: unknown;
354
+ let hasDownstream = false;
355
+ let nextCalled = false;
356
+ let nextSettled = false;
357
+
358
+ function next(): Promise<void> {
359
+ if (nextCalled) {
360
+ return Promise.reject(
361
+ new MiddlewareNextCalledMultipleTimesError({
362
+ name: node.middleware.name,
363
+ operation: "decode",
364
+ }),
365
+ );
366
+ }
367
+
368
+ nextCalled = true;
369
+
370
+ // next() の実行後、即座に destination の確認と凍結を行います。
371
+ let decoded: unknown;
372
+
373
+ try {
374
+ decoded = requireDestination(node.middleware, ctx, "decode");
375
+ freezeDestination(ctx);
376
+ } catch (ex) {
377
+ return Promise.reject(ex);
378
+ }
379
+
380
+ return Promise.try(async () => {
381
+ try {
382
+ downstreamResult = await run(index + 1, decoded);
383
+ hasDownstream = true;
384
+ } finally {
385
+ nextSettled = true;
386
+ }
387
+ });
388
+ }
389
+
390
+ await node.middleware.decode(ctx, next);
391
+
392
+ scope.signal.throwIfAborted();
393
+ if (!nextCalled) {
394
+ throw new MiddlewareNextNotCalledError({
395
+ name: node.middleware.name,
396
+ operation: "decode",
397
+ });
398
+ }
399
+
400
+ if (!nextSettled) {
401
+ throw new MiddlewareNextNotAwaitedError({
402
+ name: node.middleware.name,
403
+ operation: "decode",
404
+ });
405
+ }
406
+
407
+ const destination = requireDestination(node.middleware, ctx, "decode");
408
+
409
+ // 下流が実行されたときはその最終結果を返し、そうでなければ自らの出力を返す。
410
+ // (下流の失敗を握り潰したミドルウェアでは自らの出力が最終となる。)。
411
+ if (hasDownstream) {
412
+ return downstreamResult;
413
+ }
414
+
415
+ return destination;
416
+ }
417
+
418
+ return run(0, data);
419
+ }
420
+
421
+ /**
422
+ * 葉の経路のミドルウェアを逆順に `pipeThrough` してストリームをデコードします。
423
+ *
424
+ * @param leaf デコード元の葉です。
425
+ * @param scope 操作共通の実行時情報です。`key` は論理キーです。
426
+ * @param source 入力ストリームです。
427
+ *
428
+ * @returns デコード済みのストリームを返します。
429
+ */
430
+ public async decodeStream(
431
+ leaf: PipelineLeaf,
432
+ scope: OperationScope,
433
+ source: IReadableStream,
434
+ ): Promise<IReadableStream> {
435
+ // 途中で失敗したときは構築済みのパイプを解放し、ソースストリームがロックされたまま残らないようにします。
436
+ const pipes: IReadableStream[] = [];
437
+
438
+ try {
439
+ const reversed = leaf.path.toReversed();
440
+
441
+ async function run(index: number, input: IReadableStream): Promise<IReadableStream> {
442
+ const maybeNode = reversed[index];
443
+
444
+ if (maybeNode === undefined) {
445
+ return input;
446
+ }
447
+
448
+ const node: PipelineNode = maybeNode;
449
+
450
+ const ctx = new DecodeStreamContext({
451
+ key: scope.key,
452
+ store: scope.store,
453
+ signal: scope.signal,
454
+ source: input,
455
+ });
456
+ let downstreamResult: IReadableStream | undefined;
457
+ let nextCalled = false;
458
+ let nextSettled = false;
459
+
460
+ function next(): Promise<void> {
461
+ if (nextCalled) {
462
+ return Promise.reject(
463
+ new MiddlewareNextCalledMultipleTimesError({
464
+ name: node.middleware.name,
465
+ operation: "decode",
466
+ }),
467
+ );
468
+ }
469
+
470
+ nextCalled = true;
471
+
472
+ // next() の実行後、即座に destination の確認と凍結を行います。
473
+ let decoded: IReadableStream;
474
+
475
+ try {
476
+ decoded = requireStreamDestination(node.middleware, ctx, "decode");
477
+ freezeDestination(ctx);
478
+ } catch (ex) {
479
+ return Promise.reject(ex);
480
+ }
481
+
482
+ return Promise.try(async () => {
483
+ try {
484
+ downstreamResult = await run(index + 1, decoded);
485
+ } finally {
486
+ nextSettled = true;
487
+ }
488
+ });
489
+ }
490
+
491
+ try {
492
+ await node.middleware.decodeStream(ctx, next);
493
+ scope.signal.throwIfAborted();
494
+ if (!nextCalled) {
495
+ throw new MiddlewareNextNotCalledError({
496
+ name: node.middleware.name,
497
+ operation: "decode",
498
+ });
499
+ }
500
+
501
+ if (!nextSettled) {
502
+ throw new MiddlewareNextNotAwaitedError({
503
+ name: node.middleware.name,
504
+ operation: "decode",
505
+ });
506
+ }
507
+
508
+ const output = requireStreamDestination(node.middleware, ctx, "decode");
509
+ pipes.push(output);
510
+
511
+ if (downstreamResult !== undefined) {
512
+ return downstreamResult;
513
+ }
514
+
515
+ return output;
516
+ } catch (ex) {
517
+ // ミドルウェアが設定した destination は `pipes` に未登録のことがあるため、投げる前に解放対象へ加えます。
518
+ // `source.pipeThrough(...)` で入力をロックした出力は、ここで収集しないと呼び出し側から解放できません。
519
+ // abort 判定より前に加えることで、next() 完了後の abort でもロックした出力を解放できます。
520
+ if (ctx.destination !== undefined) {
521
+ pipes.push(ctx.destination);
522
+ }
523
+
524
+ throw ex;
525
+ }
526
+ }
527
+
528
+ return await run(0, source);
529
+ } catch (ex) {
530
+ await cancelStreams(pipes, ex);
531
+ throw ex;
532
+ }
533
+ }
534
+
535
+ /**
536
+ * 共有節を 1 回だけ実行しながら、全葉向けのプレーン値をエンコードします。
537
+ *
538
+ * エンコード中の失敗は生の例外として投げます。
539
+ *
540
+ * @param scope 操作共通の実行時情報です。`key` は論理キーです。
541
+ * @param data エンコードするデータです。
542
+ *
543
+ * @returns 登録順のエンコード結果を返します。
544
+ */
545
+ public async encode(scope: OperationScope, data: unknown): Promise<EncodeTarget[]> {
546
+ // 共有節は 1 回だけ encode し、得られた値は全分岐で参照を共有します(複製しません)。
547
+ // オニオン実行により、下流の `set` が上流の `await next()` 後の `get` に見えます (共有ストアのため)。
548
+ const targets: EncodeTarget[] = [];
549
+
550
+ async function walk(node: PipelineNode, input: unknown, parentKey: string): Promise<void> {
551
+ scope.signal.throwIfAborted();
552
+
553
+ // 親コンテキストを共有ストアで作成し、`next` 内で子の入力として `destination` を参照します。
554
+ // ミドルウェアは `destination` を設定してから `next` を呼ぶため、`next` 実行時点では親出力が確定しています。
555
+ const ctx = new EncodeContext({
556
+ key: scope.key,
557
+ store: scope.store,
558
+ signal: scope.signal,
559
+ source: input,
560
+ });
561
+ let nodeKey = parentKey;
562
+ let nextCalled = false;
563
+ let nextSettled = false;
564
+
565
+ function next(): Promise<void> {
566
+ if (nextCalled) {
567
+ return Promise.reject(
568
+ new MiddlewareNextCalledMultipleTimesError({
569
+ name: node.middleware.name,
570
+ operation: "encode",
571
+ }),
572
+ );
573
+ }
574
+
575
+ nextCalled = true;
576
+
577
+ // next() の実行後、即座に destination の確認と凍結を行います。
578
+ let encoded: unknown;
579
+
580
+ try {
581
+ encoded = requireDestination(node.middleware, ctx, "encode");
582
+ freezeDestination(ctx);
583
+ } catch (ex) {
584
+ return Promise.reject(ex);
585
+ }
586
+
587
+ return Promise.try(async () => {
588
+ try {
589
+ // `mapKey` は親出力確定後に、共有ストアで適用します。
590
+ nodeKey = applyMapKey(node.middleware, parentKey, scope);
591
+ for (const entry of node.entries) {
592
+ if (entry.kind === "node") {
593
+ await walk(entry.node, encoded, nodeKey);
594
+ } else {
595
+ targets.push({
596
+ key: nodeKey,
597
+ data: encoded,
598
+ leaf: entry.leaf,
599
+ });
600
+ }
601
+ }
602
+ } finally {
603
+ nextSettled = true;
604
+ }
605
+ });
606
+ }
607
+
608
+ await node.middleware.encode(ctx, next);
609
+
610
+ scope.signal.throwIfAborted();
611
+ if (!nextCalled) {
612
+ throw new MiddlewareNextNotCalledError({
613
+ name: node.middleware.name,
614
+ operation: "encode",
615
+ });
616
+ }
617
+
618
+ if (!nextSettled) {
619
+ throw new MiddlewareNextNotAwaitedError({
620
+ name: node.middleware.name,
621
+ operation: "encode",
622
+ });
623
+ }
624
+ }
625
+
626
+ for (const entry of this.#entries) {
627
+ if (entry.kind === "node") {
628
+ await walk(entry.node, data, scope.key);
629
+ } else {
630
+ targets.push({
631
+ key: scope.key,
632
+ data,
633
+ leaf: entry.leaf,
634
+ });
635
+ }
636
+ }
637
+
638
+ // 登録順に整列し、並列書き込みの開始順を安定させます。
639
+ targets.sort((a, b) => a.leaf.index - b.leaf.index);
640
+
641
+ return targets;
642
+ }
643
+
644
+ /**
645
+ * 書き戻し対象の葉へ、共有節を 1 回だけ実行しながら再エンコードして書き込みます。
646
+ *
647
+ * 葉への書き込みは並列に開始し、対象単位の拒否は記録だけして他の対象の開始を止めません。
648
+ * 走査中に確定した拒否は即座に投げます。書き込み中に判明した拒否も、応答のない他の書き込みを待たずに確定させます。
649
+ * 拒否がなければ全タスクの終了を待ちます。
650
+ *
651
+ * @param scope 操作共通の実行時情報です。`key` は論理キーです。
652
+ * @param targets 書き戻し対象の葉です。登録順に渡します。
653
+ * @param throwOnRepairDenied 書き戻し拒否をエラーとして投げるかどうかです。
654
+ * @param data デコード済みの原データです。
655
+ * @param write 葉への書き込み処理です。
656
+ */
657
+ public async repair(
658
+ scope: OperationScope,
659
+ targets: readonly PipelineLeaf[],
660
+ throwOnRepairDenied: boolean,
661
+ data: unknown,
662
+ write: (leaf: PipelineLeaf, key: string, data: unknown) => Promise<void>,
663
+ ): Promise<void> {
664
+ if (targets.length === 0) {
665
+ return;
666
+ }
667
+
668
+ const targetSet = new Set(targets);
669
+ const isTarget = (leaf: PipelineLeaf): boolean => targetSet.has(leaf);
670
+ const writeTasks: Promise<void>[] = [];
671
+ // 拒否は 2 経路です。走査中の拒否は即時に確定するため、書き込みの待機で get を止めません。
672
+ let traversalDenial: RepairNotAllowedError | null = null;
673
+ // 書き込み中の拒否は 1 件目のシグナルで確定し、応答のない他タスクを待ちません。
674
+ const writeDenials: RepairNotAllowedError[] = [];
675
+ const writeDenialSignal = Promise.withResolvers<void>();
676
+
677
+ // 葉書き込みは待機せず開始し、各タスクが自身の失敗を処理して allSettled で確定します。
678
+ const writeLeaf = (leaf: PipelineLeaf, input: unknown, writeKey: string): void => {
679
+ writeTasks.push(
680
+ (async (): Promise<void> => {
681
+ try {
682
+ await write(leaf, writeKey, input);
683
+ } catch (ex) {
684
+ // 意図的な中断時はエラーではありません。
685
+ if (scope.signal.aborted) {
686
+ return;
687
+ }
688
+
689
+ if (ex instanceof RepairNotAllowedError) {
690
+ repairLogger.warn`Failed to repair a storage: ${ex}`;
691
+
692
+ if (throwOnRepairDenied) {
693
+ writeDenials.push(
694
+ new RepairNotAllowedError({
695
+ cause: ex,
696
+ names: [leaf.name],
697
+ }),
698
+ );
699
+ // 他の書き込みの完了を待たずに拒否を確定させます。resolve は冪等です。
700
+ writeDenialSignal.resolve();
701
+ }
702
+
703
+ return;
704
+ }
705
+
706
+ logger.error`Failed to repair a storage: ${ex}`;
707
+ }
708
+ })(),
709
+ );
710
+ };
711
+
712
+ const recordTraversalDenial = (denied: RepairNotAllowedError): void => {
713
+ traversalDenial ??= denied;
714
+ };
715
+
716
+ const skipDenied = (leaf: PipelineLeaf): void => {
717
+ // 意図的な中断時はエラーではありません。
718
+ if (scope.signal.aborted) {
719
+ return;
720
+ }
721
+
722
+ repairLogger.warn`Skipped repair for a storage without AllowRepair: ${leaf.name}`;
723
+
724
+ if (throwOnRepairDenied) {
725
+ recordTraversalDenial(new RepairNotAllowedError({ names: [leaf.name] }));
726
+ }
727
+ };
728
+
729
+ const walk = async (node: PipelineNode, input: unknown, parentKey: string): Promise<void> => {
730
+ const activeLeaves = activeLeavesOf(node, isTarget);
731
+
732
+ const [firstLeaf] = activeLeaves;
733
+
734
+ if (firstLeaf === undefined) {
735
+ return;
736
+ }
737
+
738
+ const shared = activeLeaves.length >= 2;
739
+
740
+ // 単一対象の節でも許可があれば再エンコードを行います。
741
+ // 許可がなければ書き込みに到達しないため、副作用のないスキップに正規化しています。
742
+ // ミドルウェアを実行せず、下流へそのまま伝播します。
743
+ if (!(shared || firstLeaf.canRepair)) {
744
+ scope.signal.throwIfAborted();
745
+
746
+ for (const entry of node.entries) {
747
+ if (entry.kind === "node") {
748
+ await walk(entry.node, input, parentKey);
749
+ } else {
750
+ if (!isTarget(entry.leaf)) {
751
+ continue;
752
+ }
753
+
754
+ if (!entry.leaf.canRepair) {
755
+ skipDenied(entry.leaf);
756
+ continue;
757
+ }
758
+
759
+ writeLeaf(entry.leaf, input, parentKey);
760
+ }
761
+ }
762
+
763
+ return;
764
+ }
765
+
766
+ scope.signal.throwIfAborted();
767
+
768
+ // オニオン実行により、下流の `set` が上流の `await next()` 後の `get` に見えます (共有ストアのため)。
769
+ // 子走査は `next()` 内で行い、親ミドルウェアの後段としてネスト実行します。
770
+ const ctx = new EncodeContext({
771
+ key: scope.key,
772
+ store: scope.store,
773
+ signal: scope.signal,
774
+ source: input,
775
+ });
776
+ let nodeKey = parentKey;
777
+ let nextCalled = false;
778
+ let nextSettled = false;
779
+ let downstreamStarted = false;
780
+
781
+ function next(): Promise<void> {
782
+ if (nextCalled) {
783
+ return Promise.reject(
784
+ new MiddlewareNextCalledMultipleTimesError({
785
+ name: node.middleware.name,
786
+ operation: "encode",
787
+ }),
788
+ );
789
+ }
790
+
791
+ nextCalled = true;
792
+
793
+ // next() の実行後、即座に destination の確認と凍結を行います。
794
+ let encoded: unknown;
795
+
796
+ try {
797
+ encoded = requireDestination(node.middleware, ctx, "encode");
798
+ freezeDestination(ctx);
799
+ } catch (ex) {
800
+ return Promise.reject(ex);
801
+ }
802
+
803
+ return Promise.try(async () => {
804
+ try {
805
+ // `mapKey` は親出力確定後に、共有ストアで適用します。
806
+ // ここでの失敗は節固有のセットアップ失敗であり、下流開始前として扱います。
807
+ nodeKey = applyMapKey(node.middleware, parentKey, scope);
808
+ scope.signal.throwIfAborted();
809
+ downstreamStarted = true;
810
+
811
+ for (const entry of node.entries) {
812
+ if (entry.kind === "node") {
813
+ await walk(entry.node, encoded, nodeKey);
814
+ } else {
815
+ if (!isTarget(entry.leaf)) {
816
+ continue;
817
+ }
818
+
819
+ if (!entry.leaf.canRepair) {
820
+ skipDenied(entry.leaf);
821
+ continue;
822
+ }
823
+
824
+ writeLeaf(entry.leaf, encoded, nodeKey);
825
+ }
826
+ }
827
+ } finally {
828
+ nextSettled = true;
829
+ }
830
+ });
831
+ }
832
+
833
+ try {
834
+ await node.middleware.encode(ctx, next);
835
+ scope.signal.throwIfAborted();
836
+ if (!nextCalled) {
837
+ throw new MiddlewareNextNotCalledError({
838
+ name: node.middleware.name,
839
+ operation: "encode",
840
+ });
841
+ }
842
+
843
+ if (!nextSettled) {
844
+ throw new MiddlewareNextNotAwaitedError({
845
+ name: node.middleware.name,
846
+ operation: "encode",
847
+ });
848
+ }
849
+ } catch (ex) {
850
+ // 意図的な中断時は中断理由で失敗します。
851
+ if (scope.signal.aborted) {
852
+ if (shared) {
853
+ throw ex;
854
+ }
855
+
856
+ return;
857
+ }
858
+ // 規約違反はプログラミングエラーであり、握り潰さずに投げます。
859
+ if (
860
+ ex instanceof MiddlewareNextNotCalledError ||
861
+ ex instanceof MiddlewareNextNotAwaitedError ||
862
+ ex instanceof MiddlewareNextCalledMultipleTimesError ||
863
+ ex instanceof MiddlewareDestinationMissingError ||
864
+ ex instanceof MiddlewareDestinationFrozenError
865
+ ) {
866
+ throw ex;
867
+ }
868
+ // 下流開始後の失敗は子節で確定済み (単一拒否は記録、共有拒否は投げ) です。
869
+ // repair は補助処理のため、拒否以外の失敗は記録に留め、読み取りは成功させます。
870
+ if (downstreamStarted) {
871
+ if (ex instanceof RepairNotAllowedError) {
872
+ if (throwOnRepairDenied) {
873
+ throw ex;
874
+ }
875
+
876
+ // 拒否をエラーにしない指定では、下流開始前と同じく警告に留めて読み取りを成功させます。
877
+ repairLogger.warn`Failed to repair a storage: ${ex}`;
878
+
879
+ return;
880
+ }
881
+
882
+ logger.error`Failed to repair a storage: ${ex}`;
883
+
884
+ return;
885
+ }
886
+
887
+ if (ex instanceof RepairNotAllowedError) {
888
+ repairLogger.warn`Failed to repair a storage: ${ex}`;
889
+
890
+ if (throwOnRepairDenied) {
891
+ const denied = new RepairNotAllowedError({
892
+ cause: ex,
893
+ names: activeLeaves.map((leaf) => leaf.name),
894
+ });
895
+ // 複数対象が共有する節の拒否は、旧実装同様に書き込み開始前に確定させます。
896
+ if (shared) {
897
+ throw denied;
898
+ }
899
+
900
+ recordTraversalDenial(denied);
901
+ }
902
+ } else {
903
+ logger.error`Failed to repair a storage: ${ex}`;
904
+ }
905
+ }
906
+ };
907
+
908
+ for (const entry of this.#entries) {
909
+ if (entry.kind === "node") {
910
+ await walk(entry.node, data, scope.key);
911
+ } else {
912
+ if (!isTarget(entry.leaf)) {
913
+ continue;
914
+ }
915
+
916
+ if (!entry.leaf.canRepair) {
917
+ skipDenied(entry.leaf);
918
+ continue;
919
+ }
920
+
921
+ writeLeaf(entry.leaf, data, scope.key);
922
+ }
923
+ }
924
+
925
+ // 走査中の拒否は開始済みの書き込みを待たずに投げます。タスクは自身で例外を処理します。
926
+ if (traversalDenial !== null) {
927
+ const denial: RepairNotAllowedError = traversalDenial;
928
+
929
+ throw denial;
930
+ }
931
+
932
+ // 書き込み中の拒否はシグナルで確定し、拒否がなければ全タスクの終了を待ちます。
933
+ await Promise.race([Promise.allSettled(writeTasks), writeDenialSignal.promise]);
934
+
935
+ const [firstDenial] = writeDenials;
936
+
937
+ if (firstDenial !== undefined) {
938
+ throw firstDenial;
939
+ }
940
+ }
941
+
942
+ /**
943
+ * 共有節を 1 回だけ実行しながらストリームエンコード木を構築します。
944
+ *
945
+ * 新 Middleware の `encodeStream` は `c.source.pipeThrough(...)` で変換し `c.destination` に設定します。
946
+ * 共有節の実行は 1 回だけであり、分岐点では tee で入力を分けます。
947
+ * 計画段階の失敗は何も構築せずに伝播するため、呼び出し側が `streams` のソースをキャンセルします。
948
+ * tee とパイプの構築後の失敗は、`streams` に収集したストリーム (分割した読み取り側を含む) を内部でキャンセルしてから伝播します。
949
+ * 呼び出し側はどちらの失敗でも `streams` をキャンセルできます。
950
+ *
951
+ * @param scope 操作共通の実行時情報です。`key` は論理キーです。
952
+ * @param source 入力ストリームです。
953
+ * @param streams 構築したストリームを収集する集合です。呼び出し側がソースを事前に追加します。
954
+ * @param options 構築オプションです。
955
+ *
956
+ * @returns 構築結果を返します。
957
+ */
958
+ public async buildEncodeTree(
959
+ scope: OperationScope,
960
+ source: IReadableStream,
961
+ streams: Set<IReadableStream>,
962
+ options: BuildEncodeTreeOptions = {},
963
+ ): Promise<StreamEncodeTree> {
964
+ const { isActive, onNodeError } = options;
965
+ const targets: StreamEncodeTarget[] = [];
966
+ let readStream = source;
967
+
968
+ // 段階 1 は共有節の単一実行と tee 分岐です。対象外の部分木は作りません。
969
+ // 旧 `planNode` + `buildGraph` の 2 段階を、新 `encodeStream` の直接実行に統合しています。
970
+ // オニオン実行により、下流の `set` が上流の `await next()` 後の `get` に見えます (共有ストアのため)。
971
+ // 子走査と tee 分岐は `next()` 内で行い、親ミドルウェアの後段としてネスト実行します。
972
+ const walk = async (
973
+ node: PipelineNode,
974
+ input: IReadableStream,
975
+ parentKey: string,
976
+ ): Promise<{
977
+ readonly key: string;
978
+ readonly output: IReadableStream;
979
+ } | null> => {
980
+ const activeLeaves = isActive === undefined ? node.allLeaves : activeLeavesOf(node, isActive);
981
+
982
+ if (activeLeaves.length === 0) {
983
+ return null;
984
+ }
985
+
986
+ scope.signal.throwIfAborted();
987
+
988
+ const ctx = new EncodeStreamContext({
989
+ key: scope.key,
990
+ store: scope.store,
991
+ signal: scope.signal,
992
+ source: input,
993
+ });
994
+ let nodeKey: string = parentKey;
995
+ let output: IReadableStream | undefined;
996
+ let nextCalled = false;
997
+ let nextSettled = false;
998
+ let downstreamStarted = false;
999
+ let downstreamError: unknown;
1000
+ let hasDownstreamError = false;
1001
+ let emptyBranches = false;
1002
+
1003
+ function next(): Promise<void> {
1004
+ if (nextCalled) {
1005
+ return Promise.reject(
1006
+ new MiddlewareNextCalledMultipleTimesError({
1007
+ name: node.middleware.name,
1008
+ operation: "encode",
1009
+ }),
1010
+ );
1011
+ }
1012
+
1013
+ nextCalled = true;
1014
+
1015
+ // next() の実行後、即座に destination の確認と凍結を行います。
1016
+ let encoded: IReadableStream;
1017
+
1018
+ try {
1019
+ encoded = requireStreamDestination(node.middleware, ctx, "encode");
1020
+ freezeDestination(ctx);
1021
+ } catch (ex) {
1022
+ return Promise.reject(ex);
1023
+ }
1024
+
1025
+ return Promise.try(async () => {
1026
+ try {
1027
+ // pipeThrough で入力をロックした出力は、以降の失敗でも解放できるよう先に収集します。
1028
+ output = encoded;
1029
+ streams.add(output);
1030
+
1031
+ // `mapKey` は親出力確定後に、共有ストアで適用します。
1032
+ // ここでの失敗は節固有のセットアップ失敗であり、下流開始前として扱います。
1033
+ nodeKey = applyMapKey(node.middleware, parentKey, scope);
1034
+ scope.signal.throwIfAborted();
1035
+ downstreamStarted = true;
1036
+
1037
+ // 直下の分岐を登録順に tee で割り当てます。最後の分岐は tee せず残りを使います。
1038
+ const branchEntries = node.entries.filter((entry) => {
1039
+ if (entry.kind === "leaf") {
1040
+ return isActive === undefined || isActive(entry.leaf);
1041
+ }
1042
+
1043
+ const leaves = entry.node.allLeaves;
1044
+
1045
+ return isActive === undefined || leaves.some(isActive);
1046
+ });
1047
+
1048
+ if (branchEntries.length === 0) {
1049
+ emptyBranches = true;
1050
+
1051
+ return;
1052
+ }
1053
+
1054
+ const assigned: IReadableStream[] = [];
1055
+ let current = output;
1056
+
1057
+ for (let i = 0; i < branchEntries.length; i++) {
1058
+ if (i < branchEntries.length - 1) {
1059
+ const [branch, rest] = current.tee();
1060
+ streams.add(branch);
1061
+ streams.add(rest);
1062
+ assigned.push(branch);
1063
+ current = rest;
1064
+ } else {
1065
+ assigned.push(current);
1066
+ }
1067
+ }
1068
+
1069
+ for (let i = 0; i < branchEntries.length; i++) {
1070
+ const entry = branchEntries[i];
1071
+ const stream = assigned[i];
1072
+
1073
+ if (entry === undefined || stream === undefined) {
1074
+ throw new Error("分岐の項目またはストリームが存在しません。");
1075
+ }
1076
+
1077
+ if (entry.kind === "leaf") {
1078
+ targets.push({
1079
+ key: nodeKey,
1080
+ leaf: entry.leaf,
1081
+ stream,
1082
+ });
1083
+ } else {
1084
+ const child = await walk(entry.node, stream, nodeKey);
1085
+
1086
+ if (child === null) {
1087
+ // スキップされた部分木の分岐は解放します。tee の cancel は兄弟分岐の cancel/drain まで解決しないため、開始だけして構築を止めません (未処理 rejection も残しません)。
1088
+ // oxlint-disable-next-line promise/prefer-await-to-then
1089
+ Promise.try(() => stream.cancel(new Error("Skipped branch"))).catch(() => {});
1090
+ }
1091
+ }
1092
+ }
1093
+ } catch (ex) {
1094
+ // 下流のセットアップ失敗を親の後処理失敗と取り違えないよう、由来を記録します。
1095
+ downstreamError = ex;
1096
+ hasDownstreamError = true;
1097
+ throw ex;
1098
+ } finally {
1099
+ nextSettled = true;
1100
+ }
1101
+ });
1102
+ }
1103
+
1104
+ try {
1105
+ await node.middleware.encodeStream(ctx, next);
1106
+ scope.signal.throwIfAborted();
1107
+ if (!nextCalled) {
1108
+ throw new MiddlewareNextNotCalledError({
1109
+ name: node.middleware.name,
1110
+ operation: "encode",
1111
+ });
1112
+ }
1113
+
1114
+ if (!nextSettled) {
1115
+ throw new MiddlewareNextNotAwaitedError({
1116
+ name: node.middleware.name,
1117
+ operation: "encode",
1118
+ });
1119
+ }
1120
+ } catch (ex) {
1121
+ // next() を呼ぶ前に設定された destination は `streams` に未登録のため、投げる前に解放対象へ加えます。
1122
+ // pipeThrough で入力をロックした出力は、呼び出し側の `streams` のキャンセルでしか解放できません。
1123
+ // abort 判定・規約違反判定より前に加えることで、どちらの失敗でもロックした出力を解放できます。
1124
+ if (ctx.destination !== undefined) {
1125
+ streams.add(ctx.destination);
1126
+ }
1127
+
1128
+ // 意図的な中断時は中断理由で失敗します。
1129
+ if (scope.signal.aborted) {
1130
+ throw ex;
1131
+ }
1132
+ // 規約違反はプログラミングエラーであり、`onNodeError` を経由せずに投げます。
1133
+ if (
1134
+ ex instanceof MiddlewareNextNotCalledError ||
1135
+ ex instanceof MiddlewareNextNotAwaitedError ||
1136
+ ex instanceof MiddlewareNextCalledMultipleTimesError ||
1137
+ ex instanceof MiddlewareDestinationMissingError ||
1138
+ ex instanceof MiddlewareDestinationFrozenError
1139
+ ) {
1140
+ throw ex;
1141
+ }
1142
+
1143
+ // 下流開始後の失敗は子節で確定済みのため、そのまま伝播し、根直下で `onNodeError` に分類させます。
1144
+ if (downstreamStarted) {
1145
+ // 子節のセットアップ失敗は、親の後処理失敗ではないため repair でも握り潰しません。
1146
+ if (hasDownstreamError && ex === downstreamError) {
1147
+ throw ex;
1148
+ }
1149
+
1150
+ // repair は補助処理のため、下流開始後の拒否以外の失敗は記録に留め、読み取りは成功させます。
1151
+ if (onNodeError !== undefined) {
1152
+ if (ex instanceof RepairNotAllowedError) {
1153
+ if (onNodeError(ex, activeLeaves)) {
1154
+ // 書き戻しの分岐は既に構築済みのため、そのまま読み取りを続けます。
1155
+ return output === undefined
1156
+ ? null
1157
+ : {
1158
+ key: nodeKey,
1159
+ output,
1160
+ };
1161
+ }
1162
+ }
1163
+
1164
+ logger.error`Failed to repair a storage: ${ex}`;
1165
+
1166
+ return output === undefined
1167
+ ? null
1168
+ : {
1169
+ key: nodeKey,
1170
+ output,
1171
+ };
1172
+ }
1173
+
1174
+ throw ex;
1175
+ }
1176
+
1177
+ if (onNodeError !== undefined && onNodeError(ex, activeLeaves)) {
1178
+ // 構築済みの出力は親の入力を pipeThrough でロックしており、親側からの解放は失敗するため、出力側からキャンセルを伝播させます。
1179
+ // next() 前の失敗で output が未確定のときは、設定済みの destination を直接キャンセルします。
1180
+ // tee の cancel は兄弟分岐の cancel/drain まで解決しないため、開始だけして構築を止めません (未処理 rejection も残しません)。
1181
+ const abandoned = output ?? ctx.destination;
1182
+
1183
+ if (abandoned !== undefined) {
1184
+ // oxlint-disable-next-line promise/prefer-await-to-then
1185
+ Promise.try(() => abandoned.cancel(ex)).catch(() => {});
1186
+ }
1187
+
1188
+ return null;
1189
+ }
1190
+
1191
+ throw ex;
1192
+ }
1193
+
1194
+ if (emptyBranches || output === undefined) {
1195
+ return null;
1196
+ }
1197
+
1198
+ return {
1199
+ key: nodeKey,
1200
+ output,
1201
+ };
1202
+ };
1203
+
1204
+ try {
1205
+ // 読み取り側は不変に保ち、書き戻し側だけを木へ通します。
1206
+ let rootInput = source;
1207
+
1208
+ if (options.splitRead === true) {
1209
+ const [repairInput, read] = source.tee();
1210
+ streams.add(repairInput);
1211
+ streams.add(read);
1212
+ rootInput = repairInput;
1213
+ readStream = read;
1214
+ }
1215
+
1216
+ // 根直下の分岐を登録順に tee で割り当てます。
1217
+ const rootActives = this.#leaves.filter((leaf) => isActive === undefined || isActive(leaf));
1218
+
1219
+ if (rootActives.length === 0 && this.#entries.length === 0) {
1220
+ return {
1221
+ targets,
1222
+ readStream,
1223
+ };
1224
+ }
1225
+
1226
+ const rootBranchEntries = this.#entries.filter((entry) => {
1227
+ if (entry.kind === "leaf") {
1228
+ return isActive === undefined || isActive(entry.leaf);
1229
+ }
1230
+
1231
+ return isActive === undefined || entry.node.allLeaves.some(isActive);
1232
+ });
1233
+
1234
+ if (rootBranchEntries.length > 0) {
1235
+ const assigned: IReadableStream[] = [];
1236
+ let current = rootInput;
1237
+
1238
+ for (let i = 0; i < rootBranchEntries.length; i++) {
1239
+ if (i < rootBranchEntries.length - 1) {
1240
+ const [branch, rest] = current.tee();
1241
+ streams.add(branch);
1242
+ streams.add(rest);
1243
+ assigned.push(branch);
1244
+ current = rest;
1245
+ } else {
1246
+ assigned.push(current);
1247
+ }
1248
+ }
1249
+
1250
+ for (let i = 0; i < rootBranchEntries.length; i++) {
1251
+ const entry = rootBranchEntries[i];
1252
+ const stream = assigned[i];
1253
+
1254
+ if (entry === undefined || stream === undefined) {
1255
+ throw new Error("分岐の項目またはストリームが存在しません。");
1256
+ }
1257
+
1258
+ if (entry.kind === "leaf") {
1259
+ if (isActive === undefined || isActive(entry.leaf)) {
1260
+ targets.push({
1261
+ key: scope.key,
1262
+ leaf: entry.leaf,
1263
+ stream,
1264
+ });
1265
+ }
1266
+ } else {
1267
+ try {
1268
+ const child = await walk(entry.node, stream, scope.key);
1269
+
1270
+ if (child === null) {
1271
+ // tee の cancel は兄弟分岐の cancel/drain まで解決しないため、開始だけして構築を止めません (未処理 rejection も残しません)。
1272
+ // oxlint-disable-next-line promise/prefer-await-to-then
1273
+ Promise.try(() => stream.cancel(new Error("Skipped branch"))).catch(() => {});
1274
+ }
1275
+ } catch (ex) {
1276
+ if (onNodeError) {
1277
+ const leaves = entry.node.allLeaves.filter(
1278
+ (leaf) => isActive === undefined || isActive(leaf),
1279
+ );
1280
+
1281
+ if (onNodeError(ex, leaves)) {
1282
+ // tee の cancel は兄弟分岐の cancel/drain まで解決しないため、開始だけして構築を止めません (未処理 rejection も残しません)。
1283
+ // oxlint-disable-next-line promise/prefer-await-to-then
1284
+ Promise.try(() => stream.cancel(ex)).catch(() => {});
1285
+ continue;
1286
+ }
1287
+ }
1288
+
1289
+ throw ex;
1290
+ }
1291
+ }
1292
+ }
1293
+ }
1294
+ } catch (ex) {
1295
+ // 構築途中で失敗した場合は、tee の両分岐を含む構築済みストリームを解放してから伝播します。
1296
+ await cancelStreams(streams, ex);
1297
+ throw ex;
1298
+ }
1299
+
1300
+ targets.sort((a, b) => a.leaf.index - b.leaf.index);
1301
+
1302
+ return {
1303
+ targets,
1304
+ readStream,
1305
+ };
1306
+ }
1307
+ }