unikvs 0.0.13 → 0.0.14

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/src/unikvs.ts CHANGED
@@ -33,7 +33,7 @@ import {
33
33
  StreamArgsSchema,
34
34
  VariablesArgsSchema,
35
35
  } from "./_unikvs.schemas.js";
36
- import { parseInput, parseOutput } from "./_valibot.js";
36
+ import * as v from "./_valibot.js";
37
37
  import type {
38
38
  ValueSchemaInfo,
39
39
  $InferPlainValueData,
@@ -182,7 +182,7 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
182
182
  */
183
183
  public setVars(vars: VariablesInit): this;
184
184
  public setVars(...args: unknown[]): this {
185
- const [vars] = parseInput(VariablesArgsSchema, args);
185
+ const [vars] = v.parseInput(VariablesArgsSchema, args);
186
186
  this.#baseVars = vars;
187
187
 
188
188
  return this;
@@ -193,7 +193,7 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
193
193
  */
194
194
  public assignVars(vars: VariablesInit): this;
195
195
  public assignVars(...args: unknown[]): this {
196
- const [vars] = parseInput(VariablesArgsSchema, args);
196
+ const [vars] = v.parseInput(VariablesArgsSchema, args);
197
197
 
198
198
  const map = new Map(this.#baseVars);
199
199
 
@@ -211,7 +211,7 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
211
211
  */
212
212
  public defaultsVars(vars: VariablesInit): this;
213
213
  public defaultsVars(...args: unknown[]): this {
214
- const [vars] = parseInput(VariablesArgsSchema, args);
214
+ const [vars] = v.parseInput(VariablesArgsSchema, args);
215
215
 
216
216
  const map = new Map(vars);
217
217
 
@@ -224,87 +224,6 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
224
224
  return this;
225
225
  }
226
226
 
227
- /**
228
- * 基本変数と操作固有変数を結合した実行時変数マップを作成します。
229
- *
230
- * @param vars 操作固有の変数です。
231
- *
232
- * @returns 結合した変数マップを返します。
233
- */
234
- #createOperationVars(vars: readonly VariablesEntry[] | undefined): Map<string, unknown> {
235
- const map = new Map(this.#baseVars);
236
-
237
- if (vars) {
238
- for (const [k, v] of vars) {
239
- map.set(k, v);
240
- }
241
- }
242
-
243
- return map;
244
- }
245
-
246
- /**
247
- * 操作内で共有される変数ストアを作成します。
248
- *
249
- * 同一操作内の全コンテキスト (`use`/`encode`/`decode`/ ストレージ) で共有され、下流の `set` が上流の `await next()` 後の `get` に見えます。
250
- *
251
- * @param vars 操作固有の変数です。
252
- *
253
- * @returns 共有ストアを返します。
254
- */
255
- #createOperationStore(vars: readonly VariablesEntry[] | undefined): VariablesStore {
256
- return createVariablesStore([...this.#baseVars, ...(vars ?? [])]);
257
- }
258
-
259
- /**
260
- * キーに対応する値のスキーマ情報を解決します。
261
- *
262
- * @param key 解決するキーです。
263
- *
264
- * @returns 対応するスキーマ情報を返します。スキーマ未設定または対応する定義がない場合は `undefined` を返します。
265
- */
266
- #resolveValueSchema(key: string): ValueSchemaInfo | undefined {
267
- return this.#valueSchemaResolver?.resolve(key);
268
- }
269
-
270
- /**
271
- * すべての宛先で読み取りに失敗したときのエラーを組み立てます。
272
- *
273
- * @param key 読み取ったキーです。
274
- * @param errors 収集した失敗です。
275
- *
276
- * @returns `KeyNotFoundError` を返します。
277
- */
278
- #notFound(key: string, errors: readonly PluginErrorRecord[]): KeyNotFoundError {
279
- const args: KeyNotFoundErrorArgs = { key };
280
-
281
- switch (errors.length) {
282
- case 0: {
283
- break;
284
- }
285
-
286
- case 1: {
287
- const [firstError] = errors;
288
-
289
- if (firstError !== undefined) {
290
- args.cause = firstError.reason;
291
- }
292
-
293
- break;
294
- }
295
-
296
- default: {
297
- args.cause = new PluginOperationAggregateError({
298
- action: "read",
299
- errors,
300
- plugin: "storage",
301
- });
302
- }
303
- }
304
-
305
- return new KeyNotFoundError(args);
306
- }
307
-
308
227
  public get isOpen(): boolean {
309
228
  return this.#con !== null;
310
229
  }
@@ -315,7 +234,7 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
315
234
  throw new UniKvsIsOpenError();
316
235
  }
317
236
 
318
- const [options = {}] = parseInput(OpenArgsSchema, args);
237
+ const [options = {}] = v.parseInput(OpenArgsSchema, args);
319
238
  const { vars: varsOption, signal: signalOption } = options;
320
239
 
321
240
  const ac = new AbortController();
@@ -457,125 +376,10 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
457
376
  }
458
377
  }
459
378
 
460
- async #close(
461
- operationVars: Map<string, unknown>,
462
- signal: AbortSignal,
463
- con: Connection,
464
- progress: { pluginsStarted: boolean },
465
- ): Promise<void> {
466
- using _lock = await asyncmux(this, signal);
467
- // ロック待機中に接続状態が変更されていないか再確認します。
468
- if (this.#con !== con) {
469
- throw new UniKvsIsNotOpenError();
470
- }
471
-
472
- const { io } = this.#con;
473
- using _ioLock = await io.lock({ signal });
474
- const closeFns: [
475
- () => Promise<void>,
476
- () => Promise<void>,
477
- plugin: "storage" | "middleware",
478
- name: string,
479
- index: number,
480
- ][] = [];
481
-
482
- // ストレージのクローズ処理を登録します。
483
- // open と同じ方針でプラグインごとに複製し、並列実行中の変更が互いに見えないようにします。
484
- for (const [index, leaf] of this.#pipeline.leaves.entries()) {
485
- const createContext = (): CloseContext =>
486
- new CloseContext({
487
- vars: new Map(operationVars).entries(),
488
- signal,
489
- });
490
- // 再試行は通常 close の失敗 (操作 signal の abort など) に引きずられないよう、新しいタイムアウト signal を使います。
491
- const createRetryContext = (): CloseContext =>
492
- new CloseContext({
493
- vars: new Map(operationVars).entries(),
494
- signal: AbortSignal.timeout(10e3),
495
- });
496
- closeFns.push([
497
- (): Promise<void> => leaf.storage.close(createContext()),
498
- (): Promise<void> => leaf.storage.retryClose(createRetryContext()),
499
- "storage",
500
- leaf.name,
501
- index,
502
- ]);
503
- }
504
-
505
- // ミドルウェアのクローズ処理を登録します。
506
- for (const [index, plugin] of this.#pipeline.middlewares.entries()) {
507
- const createContext = (): CloseContext =>
508
- new CloseContext({
509
- vars: new Map(operationVars).entries(),
510
- signal,
511
- });
512
- // 再試行は通常 close の失敗 (操作 signal の abort など) に引きずられないよう、新しいタイムアウト signal を使います。
513
- const createRetryContext = (): CloseContext =>
514
- new CloseContext({
515
- vars: new Map(operationVars).entries(),
516
- signal: AbortSignal.timeout(10e3),
517
- });
518
- closeFns.push([
519
- (): Promise<void> => plugin.close(createContext()),
520
- (): Promise<void> => plugin.retryClose(createRetryContext()),
521
- "middleware",
522
- plugin.name,
523
- index,
524
- ]);
525
- }
526
-
527
- // すべての処理を並列に実行し、エラーが発生した場合は集約します。
528
- progress.pluginsStarted = true;
529
- const errors: {
530
- name: string;
531
- index: number;
532
- plugin: "storage" | "middleware";
533
- reason: unknown;
534
- }[] = [];
535
- const retries: (() => Promise<void>)[] = [];
536
- await Promise.all(
537
- closeFns.map(async ([f, retry, plugin, name, index]) => {
538
- try {
539
- await f();
540
- } catch (ex) {
541
- errors.push({
542
- name,
543
- index,
544
- plugin,
545
- reason: ex,
546
- });
547
- // 通常の close で再試行すると、close 失敗後に別の利用者が引き継いだ参照まで解放してしまいます。
548
- retries.push(retry);
549
- }
550
- }),
551
- );
552
-
553
- if (errors.length > 0) {
554
- // 失敗したプラグインだけを 1 回再試行します。
555
- // 成功済みのプラグインを再 close すると、参照カウントが二重に解放され、共有している他の利用者の実体まで閉じてしまうためです。
556
- await Promise.all(
557
- retries.map(async (f) => {
558
- try {
559
- await f();
560
- } catch (ex) {
561
- logger.error`Failed to retry closing a plugin: ${ex}`;
562
- }
563
- }),
564
- );
565
-
566
- throw new PluginOperationAggregateError({
567
- action: "close",
568
- errors,
569
- });
570
- }
571
-
572
- this.#con = null;
573
- }
574
-
575
379
  public close(options?: CloseOptions): Promise<void>;
576
380
  public close(...args: any): Promise<void> {
577
381
  try {
578
- const [options = {}] = parseInput(CloseArgsSchema, args);
382
+ const [options = {}] = v.parseInput(CloseArgsSchema, args);
579
383
  const { vars: varsOption, signal = AbortSignal.timeout(10e3) } = options;
580
384
 
581
385
  const operationVars = this.#createOperationVars(varsOption);
@@ -644,7 +448,7 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
644
448
  }
645
449
  }
646
450
 
647
- async [Symbol.asyncDispose](): Promise<void> {
451
+ public async [Symbol.asyncDispose](): Promise<void> {
648
452
  if (!this.isOpen) {
649
453
  return;
650
454
  }
@@ -654,664 +458,303 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
654
458
  });
655
459
  }
656
460
 
657
- /**
658
- * 書き込み失敗の集約と、成功した宛先への通知を行います。
659
- *
660
- * @param targets 書き込みを試みた対象です。
661
- * @param failed 失敗した葉の集合です。
662
- * @param errors 集約する失敗です。
663
- * @param signal 中断シグナルです。
664
- */
665
- async #finishWrites(
666
- targets: readonly {
667
- readonly key: string;
668
- readonly leaf: PipelineLeaf;
669
- }[],
670
- store: VariablesStore,
671
- failed: ReadonlySet<PipelineLeaf>,
672
- errors: PluginErrorRecord[],
673
- signal: AbortSignal,
674
- ): Promise<void> {
675
- if (errors.length === 0) {
676
- return;
461
+ public set<const TKey extends KeyofKeyValueMapping<TKeyValueMapping>>(
462
+ options: SetOptions<TKeyValueMapping, TKey>,
463
+ ): Promise<void>;
464
+ public set<const TKey extends KeyofKeyValueMapping<TKeyValueMapping>>(
465
+ key: TKey,
466
+ value: SetValue<TKeyValueMapping[TKey]>,
467
+ options?: Omit<SetOptions, "key" | "value">,
468
+ ): Promise<void>;
469
+ public async set(...args: any): Promise<void> {
470
+ if (this.#con === null) {
471
+ throw new UniKvsIsNotOpenError();
677
472
  }
678
473
 
679
- const error = new PluginOperationAggregateError({
680
- action: "write",
681
- errors,
682
- plugin: "storage",
683
- });
684
- const notifyErrors: unknown[] = [];
685
- await Promise.all(
686
- targets.map(async (target) => {
687
- // 失敗した宛先を除いたすべての宛先へ通知します。通知に失敗しても元の集約エラーは置き換えません。
688
- if (failed.has(target.leaf)) {
689
- return;
690
- }
474
+ const [options] = v.parseInput(SetArgsSchema, args);
475
+ const { key, vars: varsOption, signal: signalOption } = options;
691
476
 
692
- try {
693
- const c = new SetContext({
694
- key: target.key,
695
- store,
696
- signal,
697
- });
698
- await target.leaf.storage.onOtherWriteError(c, error);
699
- } catch (ex) {
700
- notifyErrors.push(ex);
701
- }
702
- }),
703
- );
477
+ const valueSchema = this.#resolveValueSchema(key);
478
+ // スキーマ検証はロック取得前に済ませ、無効な入力ではロックを取得しません。
479
+ const value = valueSchema ? parseSetValue(valueSchema, options.value) : options.value;
704
480
 
705
- if (notifyErrors.length > 0) {
706
- logger.error(new AggregateError(notifyErrors, "Failed to handle error"));
707
- }
481
+ const { ac, io } = this.#con;
482
+ const signal = combineSignals([ac.signal, signalOption]);
708
483
 
709
- throw error;
710
- }
484
+ const store = this.#createOperationStore(varsOption);
711
485
 
712
- /**
713
- * ミドルウェアの `use` をオニオン実行し、失敗時は集約して投げます。
714
- *
715
- * 同一コンテキストオブジェクトを全ミドルウェアで共有し、`next` で下流をネスト実行するため、下流の `set` が上流の `await next()` 後の `get` に見えます。
716
- * `encode`/`decode` と同じく `next()` の呼び出し規約を検証し、違反したときは操作を実行せず失敗させます (fail-close)。
717
- * いずれかのミドルウェアが投げたときは即時に確定し、後段は実行しません (fail-fast)。
718
- * 失敗は集約エラー (単一件) として投げます。
719
- *
720
- * @param action 集約エラーに付与する操作名です。
721
- * @param c 共有の `use` コンテキストです。
722
- */
723
- async #use(
724
- action: "read" | "write" | "delete" | "clear",
725
- c: IMiddleware.UseContext,
726
- ): Promise<void> {
727
- throwIfAborted(c.signal);
486
+ // `#setStream` へ所有権を渡す前の失敗では、ソースをロックしたままにしないよう派生ストリームをキャンセルします。
487
+ const stream = isReadableStream(value) ? value : null;
488
+ let ownsStream = stream !== null;
728
489
 
729
- const middlewares = this.#pipeline.middlewares;
730
- const dispatch = async (index: number): Promise<void> => {
731
- const maybeMiddleware = middlewares[index];
490
+ try {
491
+ using _lock = await asyncmux.readonly(this, signal);
492
+ // ロック待機中に接続状態が変更されていないか再確認します。
493
+ if (this.#con === null) {
494
+ throw new UniKvsIsNotOpenError();
495
+ }
732
496
 
733
- if (maybeMiddleware === undefined) {
734
- return;
497
+ // 書き込み前の `use` による検証を行います。変異は後続のエンコード・書き込みに引き継がれます。
498
+ const useContext = new SetContext({
499
+ key,
500
+ store,
501
+ signal,
502
+ });
503
+ await this.#use("write", useContext);
504
+ // `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
505
+ throwIfAborted(signal);
506
+ // `use` による変異を操作変数に反映します。
507
+ // ロックはインスタンス (読み取り) からキーの順で取得し、逆順で解放します。
508
+ if (stream !== null) {
509
+ // これ以降の後始末は `#setStream` が所有します。
510
+ ownsStream = false;
511
+ await this.#setStream(io, signal, key, store, stream);
512
+ } else {
513
+ await this.#setPlain(io, signal, key, store, value);
514
+ }
515
+ } catch (ex) {
516
+ if (ownsStream && stream !== null) {
517
+ // 消費されないストリームをキャンセルし、ソースの解放を伝播させます。
518
+ try {
519
+ await stream.cancel(ex);
520
+ } catch {
521
+ // キャンセルに失敗しても元の例外の伝播を優先します。
522
+ }
735
523
  }
736
524
 
737
- const middleware: UniKvsMiddleware = maybeMiddleware;
525
+ throw ex;
526
+ }
527
+ }
738
528
 
739
- let nextCalled = false;
740
- let nextSettled = false;
741
- let downstreamError: unknown;
742
- let hasDownstreamError = false;
529
+ public get<const TKey extends KeyofKeyValueMappingHasPlainValue<TKeyValueMapping>>(
530
+ options: GetOptions<TKey>,
531
+ ): Promise<$InferPlainValueData<TKeyValueMapping[TKey]>>;
532
+ public get<const TKey extends KeyofKeyValueMappingHasPlainValue<TKeyValueMapping>>(
533
+ key: TKey,
534
+ options?: Omit<GetOptions, "key">,
535
+ ): Promise<$InferPlainValueData<TKeyValueMapping[TKey]>>;
536
+ public async get(...args: any): Promise<unknown> {
537
+ if (this.#con === null) {
538
+ throw new UniKvsIsNotOpenError();
539
+ }
743
540
 
744
- function next(): Promise<void> {
745
- if (nextCalled) {
746
- // 2 回目以降の `next()` は下流を実行せず拒否だけを返します。
747
- const rejection = Promise.reject(
748
- new MiddlewareNextCalledMultipleTimesError({
749
- name: middleware.name,
750
- operation: "use",
751
- }),
752
- );
753
- // 呼び出し側が切り離した場合の未処理 rejection を防ぎます。await したときは従来どおり拒否されます。
754
- // oxlint-disable-next-line promise/prefer-await-to-then
755
- rejection.catch(() => {});
541
+ const [options] = v.parseInput(GetArgsSchema, args);
542
+ const {
543
+ key,
544
+ vars: varsOption,
545
+ repair = false,
546
+ signal: signalOption,
547
+ throwOnRepairDenied = false,
548
+ } = options;
756
549
 
757
- return rejection;
758
- }
550
+ const valueSchema = this.#resolveValueSchema(key);
759
551
 
760
- nextCalled = true;
552
+ const { ac, io } = this.#con;
553
+ const signal = combineSignals([ac.signal, signalOption]);
761
554
 
762
- const promise = Promise.try(async () => {
763
- try {
764
- await dispatch(index + 1);
765
- } catch (ex) {
766
- // 下流の失敗を親自身の失敗と取り違えないよう、由来を記録します。
767
- downstreamError = ex;
768
- hasDownstreamError = true;
769
- throw ex;
770
- } finally {
771
- nextSettled = true;
555
+ const store = this.#createOperationStore(varsOption);
556
+
557
+ using _lock = await asyncmux.readonly(this, signal);
558
+ // ロック待機中に接続状態が変更されていないか再確認します。
559
+ if (this.#con === null) {
560
+ throw new UniKvsIsNotOpenError();
561
+ }
562
+
563
+ // 読み取り前の `use` による検証を行います。書き戻し要求は `repair` フィールドで伝えます。
564
+ const leafNames = this.#pipeline.leaves.map((leaf) => leaf.name);
565
+ const useContext = new GetContext({
566
+ key,
567
+ store,
568
+ repair: repair
569
+ ? {
570
+ enabled: true,
571
+ targets: leafNames as [string, ...string[]],
772
572
  }
773
- });
774
- // 呼び出し側が切り離した場合の未処理 rejection を防ぎます。await したときは従来どおり拒否されます。
775
- // oxlint-disable-next-line promise/prefer-await-to-then
776
- promise.catch(() => {});
573
+ : {
574
+ enabled: false,
575
+ targets: [],
576
+ },
577
+ signal,
578
+ });
579
+ await this.#use("read", useContext);
580
+ // `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
581
+ throwIfAborted(signal);
582
+ let data: unknown = undefined;
583
+ let hit: ResolvedLeaf | null = null;
584
+ const missing: PipelineLeaf[] = [];
585
+ const errors: PluginErrorRecord[] = [];
777
586
 
778
- return promise;
779
- }
587
+ // 書き戻し時は、読み取りから書き込みまでを同じキーの書き込みと直列化するため、書き込みロックを取得します。
588
+ {
589
+ using _ioLock = await (repair
590
+ ? io.lock({
591
+ key,
592
+ signal,
593
+ })
594
+ : io.rLock({
595
+ key,
596
+ signal,
597
+ }));
598
+ // 登録順が読み取りの優先順です。先頭で見つかればそこで終え、失敗したときは次の宛先を試します。
599
+ const resolver = this.#pipeline.createResolver({
600
+ key,
601
+ store,
602
+ signal,
603
+ });
780
604
 
781
- try {
782
- await middleware.use(c, next);
783
- throwIfAborted(c.signal);
784
- if (!nextCalled) {
785
- throw new MiddlewareNextNotCalledError({
786
- name: middleware.name,
787
- operation: "use",
788
- });
789
- }
605
+ for (const leaf of this.#pipeline) {
606
+ const resolved = resolver.resolve(leaf);
607
+ const existsContext = new HasContext({
608
+ key: resolved.key,
609
+ store,
610
+ signal,
611
+ });
612
+ const readRepair = repair
613
+ ? {
614
+ enabled: true as const,
615
+ targets: leafNames as [string, ...string[]],
616
+ }
617
+ : {
618
+ enabled: false as const,
619
+ targets: [] as const,
620
+ };
621
+ const readContext = new GetContext({
622
+ key: resolved.key,
623
+ store,
624
+ repair: readRepair,
625
+ signal,
626
+ });
790
627
 
791
- if (!nextSettled) {
792
- throw new MiddlewareNextNotAwaitedError({
793
- name: middleware.name,
794
- operation: "use",
795
- });
796
- }
797
- } catch (ex) {
798
- // 中断による失敗はそのまま投げます。
799
- if (c.signal.aborted && ex === c.signal.reason) {
800
- throw ex;
801
- }
628
+ try {
629
+ if (!(await leaf.storage.exists(existsContext))) {
630
+ missing.push(leaf);
631
+ continue;
632
+ }
802
633
 
803
- // 規約違反はプログラミングエラーであり、集約せずに投げます。
804
- if (
805
- ex instanceof MiddlewareNextNotCalledError ||
806
- ex instanceof MiddlewareNextNotAwaitedError ||
807
- ex instanceof MiddlewareNextCalledMultipleTimesError
808
- ) {
809
- throw ex;
810
- }
634
+ data = await leaf.storage.read(readContext);
635
+ hit = resolved;
636
+ break;
637
+ } catch (ex) {
638
+ if (signal.aborted) {
639
+ throw ex;
640
+ }
811
641
 
812
- // `next()` から伝播した下流の失敗は再集約せず、そのまま投げます。
813
- if (hasDownstreamError && ex === downstreamError) {
814
- throw ex;
642
+ errors.push({
643
+ name: leaf.name,
644
+ index: leaf.index,
645
+ reason: ex,
646
+ });
647
+ logger.error`Failed to read from a storage: ${ex}`;
815
648
  }
649
+ }
816
650
 
817
- throw new PluginOperationAggregateError({
818
- action,
819
- errors: [
820
- {
821
- name: middleware.name,
822
- index,
823
- reason: ex,
824
- },
825
- ],
826
- plugin: "middleware",
827
- });
651
+ if (hit === null) {
652
+ throw this.#notFound(key, errors);
828
653
  }
829
- };
830
- await dispatch(0);
831
- }
832
654
 
833
- /**
834
- * プレーン値の並列書き込みを行います。
835
- *
836
- * @param io 多重化インスタンスです。
837
- * @param signal 中断シグナルです。
838
- * @param key 書き込みキーです。
839
- * @param store 操作内で共有される変数ストアです。
840
- * @param value 書き込む値です.
841
- */
842
- async #setPlain(
843
- io: Asyncmux,
844
- signal: AbortSignal,
845
- key: string,
846
- store: VariablesStore,
847
- value: unknown,
848
- ): Promise<void> {
849
- const targets = await this.#pipeline.encode(
850
- {
851
- key,
852
- store,
853
- signal,
854
- },
855
- value,
856
- );
857
- using _ioLock = await io.lock({
858
- key,
859
- signal,
860
- });
861
- const errors: PluginErrorRecord[] = [];
862
- const failed = new Set<PipelineLeaf>();
863
- await Promise.all(
864
- targets.map(async (target) => {
865
- try {
866
- const c = new SetContext({
867
- key: target.key,
868
- store,
869
- signal,
870
- });
871
- await target.leaf.storage.write(c, target.data);
872
- } catch (ex) {
873
- errors.push({
874
- name: target.leaf.name,
875
- index: target.leaf.index,
876
- reason: ex,
877
- });
878
- failed.add(target.leaf);
879
- }
880
- }),
881
- );
882
- await this.#finishWrites(targets, store, failed, errors, signal);
883
- }
884
-
885
- /**
886
- * ストリーム値の並列書き込みを行います。
887
- *
888
- * @param io 多重化インスタンスです。
889
- * @param signal 中断シグナルです。
890
- * @param key 書き込みキーです。
891
- * @param store 操作内で共有される変数ストアです。
892
- * @param value 書き込むストリームです。
893
- */
894
- async #setStream(
895
- io: Asyncmux,
896
- signal: AbortSignal,
897
- key: string,
898
- store: VariablesStore,
899
- value: IReadableStream,
900
- ): Promise<void> {
901
- const streams = new Set<IReadableStream>([value]);
902
- let tree;
655
+ if (repair) {
656
+ // 後段で見つかったデータをすべてデコードして原データに戻します。
657
+ data = await this.#repairPlain(data, hit, missing, store, key, signal, throwOnRepairDenied);
658
+ }
659
+ }
903
660
 
904
- try {
905
- tree = await this.#pipeline.buildEncodeTree(
661
+ if (!repair) {
662
+ // 見つかったストレージ専用の前段パイプラインを逆順に適用して、データをデコードします。
663
+ data = await this.#pipeline.decode(
664
+ hit.leaf,
906
665
  {
907
666
  key,
908
667
  store,
909
668
  signal,
910
669
  },
911
- value,
912
- streams,
670
+ data,
913
671
  );
914
- } catch (ex) {
915
- await cancelStreams(streams, ex);
916
- throw ex;
917
672
  }
918
673
 
919
- using _ioLock = await io
920
- .lock({
921
- key,
922
- signal,
923
- })
924
- .catch(async (ex) => {
925
- // キーロックの待機中に中断されたときも、構築済みのストリームをキャンセルします。
926
- // ソースストリームをロックしたまま残さないためです。
927
- await cancelStreams(streams, ex);
928
- throw ex;
929
- });
930
-
931
- const errors: PluginErrorRecord[] = [];
932
- const failed = new Set<PipelineLeaf>();
933
- await Promise.all(
934
- tree.targets.map(async (target) => {
935
- try {
936
- const c = new SetContext({
937
- key: target.key,
938
- store,
939
- signal,
940
- });
941
- const w = await target.leaf.storage.getWritable(c);
942
- await target.stream.pipeTo(w, { signal });
943
- } catch (ex) {
944
- // tee で分岐したストリームは、片方の分岐をキャンセルするまで他方が読み取ったチャンクを保持し続けるため、失敗時に放棄した分岐は確実にキャンセルします。
945
- try {
946
- await target.stream.cancel(ex);
947
- } catch {
948
- // キャンセルに失敗してもエラー集約を優先します。
949
- }
674
+ // デコード後のデータを検証します。
675
+ if (valueSchema) {
676
+ data = v.parseOutput(valueSchema.schema, data);
677
+ }
950
678
 
951
- errors.push({
952
- name: target.leaf.name,
953
- index: target.leaf.index,
954
- reason: ex,
955
- });
956
- failed.add(target.leaf);
957
- }
958
- }),
959
- );
960
- await this.#finishWrites(tree.targets, store, failed, errors, signal);
679
+ return data;
961
680
  }
962
681
 
963
- public set<const TKey extends KeyofKeyValueMapping<TKeyValueMapping>>(
964
- options: SetOptions<TKeyValueMapping, TKey>,
965
- ): Promise<void>;
966
- public set<const TKey extends KeyofKeyValueMapping<TKeyValueMapping>>(
682
+ public stream<const TKey extends KeyofKeyValueMappingHasStreamValue<TKeyValueMapping>>(
683
+ options: StreamOptions<TKey>,
684
+ ): Promise<IValueStream<$InferStreamValueChunkData<TKeyValueMapping[TKey]>>>;
685
+ public stream<const TKey extends KeyofKeyValueMappingHasStreamValue<TKeyValueMapping>>(
967
686
  key: TKey,
968
- value: SetValue<TKeyValueMapping[TKey]>,
969
- options?: Omit<SetOptions, "key" | "value">,
970
- ): Promise<void>;
971
- public async set(...args: any): Promise<void> {
687
+ options?: Omit<StreamOptions, "key">,
688
+ ): Promise<IValueStream<$InferStreamValueChunkData<TKeyValueMapping[TKey]>>>;
689
+ public async stream(...args: any): Promise<IValueStream> {
972
690
  if (this.#con === null) {
973
691
  throw new UniKvsIsNotOpenError();
974
692
  }
975
693
 
976
- const [options] = parseInput(SetArgsSchema, args);
977
- const { key, vars: varsOption, signal: signalOption } = options;
694
+ const [options] = v.parseInput(StreamArgsSchema, args);
695
+ const {
696
+ key,
697
+ vars: varsOption,
698
+ repair = false,
699
+ signal: signalOption,
700
+ throwOnRepairDenied = false,
701
+ } = options;
978
702
 
979
703
  const valueSchema = this.#resolveValueSchema(key);
980
- // スキーマ検証はロック取得前に済ませ、無効な入力ではロックを取得しません。
981
- const value = valueSchema ? parseSetValue(valueSchema, options.value) : options.value;
982
704
 
983
705
  const { ac, io } = this.#con;
984
706
  const signal = combineSignals([ac.signal, signalOption]);
985
707
 
986
708
  const store = this.#createOperationStore(varsOption);
987
709
 
988
- // `#setStream` へ所有権を渡す前の失敗では、ソースをロックしたままにしないよう派生ストリームをキャンセルします。
989
- const stream = isReadableStream(value) ? value : null;
990
- let ownsStream = stream !== null;
991
-
992
- try {
710
+ {
993
711
  using _lock = await asyncmux.readonly(this, signal);
994
712
  // ロック待機中に接続状態が変更されていないか再確認します。
995
713
  if (this.#con === null) {
996
714
  throw new UniKvsIsNotOpenError();
997
715
  }
998
716
 
999
- // 書き込み前の `use` による検証を行います。変異は後続のエンコード・書き込みに引き継がれます。
1000
- const useContext = new SetContext({
717
+ // 読み取り前の `use` による検証を行います。書き戻し要求は `repair` フィールドで伝えます。
718
+ const leafNames = this.#pipeline.leaves.map((leaf) => leaf.name);
719
+ const useContext = new StreamContext({
1001
720
  key,
1002
721
  store,
722
+ repair: repair
723
+ ? {
724
+ enabled: true,
725
+ targets: leafNames as [string, ...string[]],
726
+ }
727
+ : {
728
+ enabled: false,
729
+ targets: [],
730
+ },
1003
731
  signal,
1004
732
  });
1005
- await this.#use("write", useContext);
733
+ await this.#use("read", useContext);
1006
734
  // `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
1007
735
  throwIfAborted(signal);
1008
- // `use` による変異を操作変数に反映します。
1009
- // ロックはインスタンス (読み取り) からキーの順で取得し、逆順で解放します。
1010
- if (stream !== null) {
1011
- // これ以降の後始末は `#setStream` が所有します。
1012
- ownsStream = false;
1013
- await this.#setStream(io, signal, key, store, stream);
1014
- } else {
1015
- await this.#setPlain(io, signal, key, store, value);
1016
- }
1017
- } catch (ex) {
1018
- if (ownsStream && stream !== null) {
1019
- // 消費されないストリームをキャンセルし、ソースの解放を伝播させます。
1020
- try {
1021
- await stream.cancel(ex);
1022
- } catch {
1023
- // キャンセルに失敗しても元の例外の伝播を優先します。
1024
- }
1025
- }
736
+ // NONE は getReadable の呼び出し前を表す番兵値です。
737
+ const NONE: any = {};
738
+ let r: IReadableStream = NONE;
739
+ let hit: ResolvedLeaf | null = null;
740
+ const missing: PipelineLeaf[] = [];
741
+ const errors: PluginErrorRecord[] = [];
742
+ const fillBranches: IReadableStream[] = [];
743
+ const fillTasks: Promise<void>[] = [];
744
+ let repairAc: AbortController | null = null;
745
+ let cancelFills: ((reason: unknown) => Promise<void>) | null = null;
1026
746
 
1027
- throw ex;
1028
- }
1029
- }
1030
-
1031
- /**
1032
- * 読み取り元のチェーン全体を逆順にデコードし、書き戻し対象向けに再エンコードして書き込みます。
1033
- *
1034
- * @param data 読み取ったデータです。
1035
- * @param hit 読み取り元の葉です。
1036
- * @param missing 読み取りに失敗または不在だった葉です。
1037
- * @param store 操作内で共有される変数ストアです。
1038
- * @param key キーです。
1039
- * @param signal 中断シグナルです。
1040
- * @param throwOnRepairDenied 書き戻し拒否をエラーとして投げるかどうかです。
1041
- *
1042
- * @returns デコード済みの原データを返します。
1043
- */
1044
- async #repairPlain(
1045
- data: unknown,
1046
- hit: ResolvedLeaf,
1047
- missing: readonly PipelineLeaf[],
1048
- store: VariablesStore,
1049
- key: string,
1050
- signal: AbortSignal,
1051
- throwOnRepairDenied: boolean,
1052
- ): Promise<unknown> {
1053
- const raw = await this.#pipeline.decode(
1054
- hit.leaf,
1055
- {
1056
- key,
1057
- store,
1058
- signal,
1059
- },
1060
- data,
1061
- );
1062
- await this.#pipeline.repair(
1063
- {
1064
- key,
1065
- store,
1066
- signal,
1067
- },
1068
- missing,
1069
- throwOnRepairDenied,
1070
- raw,
1071
- async (leaf, writeKey, input) => {
1072
- const c = new SetContext({
1073
- key: writeKey,
1074
- store,
1075
- signal,
1076
- });
1077
- await leaf.storage.write(c, input);
1078
- },
1079
- );
1080
-
1081
- throwIfAborted(signal);
1082
-
1083
- return raw;
1084
- }
1085
-
1086
- public get<const TKey extends KeyofKeyValueMappingHasPlainValue<TKeyValueMapping>>(
1087
- options: GetOptions<TKey>,
1088
- ): Promise<$InferPlainValueData<TKeyValueMapping[TKey]>>;
1089
- public get<const TKey extends KeyofKeyValueMappingHasPlainValue<TKeyValueMapping>>(
1090
- key: TKey,
1091
- options?: Omit<GetOptions, "key">,
1092
- ): Promise<$InferPlainValueData<TKeyValueMapping[TKey]>>;
1093
- public async get(...args: any): Promise<unknown> {
1094
- if (this.#con === null) {
1095
- throw new UniKvsIsNotOpenError();
1096
- }
1097
-
1098
- const [options] = parseInput(GetArgsSchema, args);
1099
- const {
1100
- key,
1101
- vars: varsOption,
1102
- repair = false,
1103
- signal: signalOption,
1104
- throwOnRepairDenied = false,
1105
- } = options;
1106
-
1107
- const valueSchema = this.#resolveValueSchema(key);
1108
-
1109
- const { ac, io } = this.#con;
1110
- const signal = combineSignals([ac.signal, signalOption]);
1111
-
1112
- const store = this.#createOperationStore(varsOption);
1113
-
1114
- using _lock = await asyncmux.readonly(this, signal);
1115
- // ロック待機中に接続状態が変更されていないか再確認します。
1116
- if (this.#con === null) {
1117
- throw new UniKvsIsNotOpenError();
1118
- }
1119
-
1120
- // 読み取り前の `use` による検証を行います。書き戻し要求は `repair` フィールドで伝えます。
1121
- const leafNames = this.#pipeline.leaves.map((leaf) => leaf.name);
1122
- const useContext = new GetContext({
1123
- key,
1124
- store,
1125
- repair: repair
1126
- ? {
1127
- enabled: true,
1128
- targets: leafNames as [string, ...string[]],
1129
- }
1130
- : {
1131
- enabled: false,
1132
- targets: [],
1133
- },
1134
- signal,
1135
- });
1136
- await this.#use("read", useContext);
1137
- // `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
1138
- throwIfAborted(signal);
1139
- let data: unknown = undefined;
1140
- let hit: ResolvedLeaf | null = null;
1141
- const missing: PipelineLeaf[] = [];
1142
- const errors: PluginErrorRecord[] = [];
1143
-
1144
- // 書き戻し時は、読み取りから書き込みまでを同じキーの書き込みと直列化するため、書き込みロックを取得します。
1145
- {
1146
- using _ioLock = await (repair
1147
- ? io.lock({
1148
- key,
1149
- signal,
1150
- })
1151
- : io.rLock({
1152
- key,
1153
- signal,
1154
- }));
1155
- // 登録順が読み取りの優先順です。先頭で見つかればそこで終え、失敗したときは次の宛先を試します。
1156
- const resolver = this.#pipeline.createResolver({
1157
- key,
1158
- store,
1159
- signal,
1160
- });
1161
-
1162
- for (const leaf of this.#pipeline) {
1163
- const resolved = resolver.resolve(leaf);
1164
- const existsContext = new HasContext({
1165
- key: resolved.key,
1166
- store,
1167
- signal,
1168
- });
1169
- const readRepair = repair
1170
- ? {
1171
- enabled: true as const,
1172
- targets: leafNames as [string, ...string[]],
1173
- }
1174
- : {
1175
- enabled: false as const,
1176
- targets: [] as const,
1177
- };
1178
- const readContext = new GetContext({
1179
- key: resolved.key,
1180
- store,
1181
- repair: readRepair,
1182
- signal,
1183
- });
1184
-
1185
- try {
1186
- if (!(await leaf.storage.exists(existsContext))) {
1187
- missing.push(leaf);
1188
- continue;
1189
- }
1190
-
1191
- data = await leaf.storage.read(readContext);
1192
- hit = resolved;
1193
- break;
1194
- } catch (ex) {
1195
- if (signal.aborted) {
1196
- throw ex;
1197
- }
1198
-
1199
- errors.push({
1200
- name: leaf.name,
1201
- index: leaf.index,
1202
- reason: ex,
1203
- });
1204
- logger.error`Failed to read from a storage: ${ex}`;
1205
- }
1206
- }
1207
-
1208
- if (hit === null) {
1209
- throw this.#notFound(key, errors);
1210
- }
1211
-
1212
- if (repair) {
1213
- // 後段で見つかったデータをすべてデコードして原データに戻します。
1214
- data = await this.#repairPlain(data, hit, missing, store, key, signal, throwOnRepairDenied);
1215
- }
1216
- }
1217
-
1218
- if (!repair) {
1219
- // 見つかったストレージ専用の前段パイプラインを逆順に適用して、データをデコードします。
1220
- data = await this.#pipeline.decode(
1221
- hit.leaf,
1222
- {
1223
- key,
1224
- store,
1225
- signal,
1226
- },
1227
- data,
1228
- );
1229
- }
1230
-
1231
- // デコード後のデータを検証します。
1232
- if (valueSchema) {
1233
- data = parseOutput(valueSchema.schema, data);
1234
- }
1235
-
1236
- return data;
1237
- }
1238
-
1239
- public stream<const TKey extends KeyofKeyValueMappingHasStreamValue<TKeyValueMapping>>(
1240
- options: StreamOptions<TKey>,
1241
- ): Promise<IValueStream<$InferStreamValueChunkData<TKeyValueMapping[TKey]>>>;
1242
- public stream<const TKey extends KeyofKeyValueMappingHasStreamValue<TKeyValueMapping>>(
1243
- key: TKey,
1244
- options?: Omit<StreamOptions, "key">,
1245
- ): Promise<IValueStream<$InferStreamValueChunkData<TKeyValueMapping[TKey]>>>;
1246
- public async stream(...args: any): Promise<IValueStream> {
1247
- if (this.#con === null) {
1248
- throw new UniKvsIsNotOpenError();
1249
- }
1250
-
1251
- const [options] = parseInput(StreamArgsSchema, args);
1252
- const {
1253
- key,
1254
- vars: varsOption,
1255
- repair = false,
1256
- signal: signalOption,
1257
- throwOnRepairDenied = false,
1258
- } = options;
1259
-
1260
- const valueSchema = this.#resolveValueSchema(key);
1261
-
1262
- const { ac, io } = this.#con;
1263
- const signal = combineSignals([ac.signal, signalOption]);
1264
-
1265
- const store = this.#createOperationStore(varsOption);
1266
-
1267
- {
1268
- using _lock = await asyncmux.readonly(this, signal);
1269
- // ロック待機中に接続状態が変更されていないか再確認します。
1270
- if (this.#con === null) {
1271
- throw new UniKvsIsNotOpenError();
1272
- }
1273
-
1274
- // 読み取り前の `use` による検証を行います。書き戻し要求は `repair` フィールドで伝えます。
1275
- const leafNames = this.#pipeline.leaves.map((leaf) => leaf.name);
1276
- const useContext = new StreamContext({
1277
- key,
1278
- store,
1279
- repair: repair
1280
- ? {
1281
- enabled: true,
1282
- targets: leafNames as [string, ...string[]],
1283
- }
1284
- : {
1285
- enabled: false,
1286
- targets: [],
1287
- },
1288
- signal,
1289
- });
1290
- await this.#use("read", useContext);
1291
- // `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
1292
- throwIfAborted(signal);
1293
- // NONE は getReadable の呼び出し前を表す番兵値です。
1294
- const NONE: any = {};
1295
- let r: IReadableStream = NONE;
1296
- let hit: ResolvedLeaf | null = null;
1297
- const missing: PipelineLeaf[] = [];
1298
- const errors: PluginErrorRecord[] = [];
1299
- const fillBranches: IReadableStream[] = [];
1300
- const fillTasks: Promise<void>[] = [];
1301
- let repairAc: AbortController | null = null;
1302
- let cancelFills: ((reason: unknown) => Promise<void>) | null = null;
1303
-
1304
- // 書き戻し時は、読み取りから書き込みまでを同じキーの書き込みと直列化するため、書き込みロックを取得します。
1305
- // キーロックは ValueStream の dispose まで保持するため、明示的に解放します。
1306
- const ioLock = await (repair
1307
- ? io.lock({
1308
- key,
1309
- signal,
1310
- })
1311
- : io.rLock({
1312
- key,
1313
- signal,
1314
- }));
747
+ // 書き戻し時は、読み取りから書き込みまでを同じキーの書き込みと直列化するため、書き込みロックを取得します。
748
+ // キーロックは ValueStream の dispose まで保持するため、明示的に解放します。
749
+ const ioLock = await (repair
750
+ ? io.lock({
751
+ key,
752
+ signal,
753
+ })
754
+ : io.rLock({
755
+ key,
756
+ signal,
757
+ }));
1315
758
 
1316
759
  try {
1317
760
  // 登録順が読み取りの優先順です。先頭で見つかればそこで終え、失敗したときは次の宛先を試します。
@@ -1515,7 +958,7 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
1515
958
  r = r.pipeThrough(
1516
959
  new TransformStream({
1517
960
  transform(chunk, controller): void {
1518
- controller.enqueue(parseOutput(valueSchema.schema, chunk));
961
+ controller.enqueue(v.parseOutput(valueSchema.schema, chunk));
1519
962
  },
1520
963
  }),
1521
964
  );
@@ -1591,249 +1034,806 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
1591
1034
  } catch (ex) {
1592
1035
  await cancelFills?.(ex);
1593
1036
 
1594
- if (r !== NONE) {
1595
- // getReadable の成功後にセットアップで失敗したときは、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放するためキャンセルします。
1037
+ if (r !== NONE) {
1038
+ // getReadable の成功後にセットアップで失敗したときは、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放するためキャンセルします。
1039
+ try {
1040
+ await r.cancel(ex);
1041
+ } catch {
1042
+ // キャンセルに失敗しても元の例外の伝播を優先します。
1043
+ }
1044
+ }
1045
+
1046
+ ioLock.release();
1047
+
1048
+ throw ex;
1049
+ }
1050
+ }
1051
+ }
1052
+
1053
+ public has(options: HasOptions<KeyofKeyValueMapping<TKeyValueMapping>>): Promise<boolean>;
1054
+ public has(
1055
+ key: KeyofKeyValueMapping<TKeyValueMapping>,
1056
+ options?: Omit<HasOptions, "key">,
1057
+ ): Promise<boolean>;
1058
+ public async has(...args: any): Promise<boolean> {
1059
+ if (this.#con === null) {
1060
+ throw new UniKvsIsNotOpenError();
1061
+ }
1062
+
1063
+ const [options] = v.parseInput(HasArgsSchema, args);
1064
+ const { key, vars: varsOption, signal: signalOption } = options;
1065
+
1066
+ // キーがキースキーマに一致するかを検証します。
1067
+ this.#resolveValueSchema(key);
1068
+
1069
+ const { ac, io } = this.#con;
1070
+ const signal = combineSignals([ac.signal, signalOption]);
1071
+
1072
+ const store = this.#createOperationStore(varsOption);
1073
+
1074
+ using _lock = await asyncmux.readonly(this, signal);
1075
+ // ロック待機中に接続状態が変更されていないか再確認します。
1076
+ if (this.#con === null) {
1077
+ throw new UniKvsIsNotOpenError();
1078
+ }
1079
+
1080
+ // 存在確認に先立ち、各ミドルウェアの `use` を実行します。
1081
+ // `use` による vars の変更は後続の解決・走査に引き継がれます。
1082
+ const useContext = new HasContext({
1083
+ key,
1084
+ store,
1085
+ signal,
1086
+ });
1087
+ await this.#use("read", useContext);
1088
+ // `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
1089
+ throwIfAborted(signal);
1090
+
1091
+ using _ioLock = await io.rLock({
1092
+ key,
1093
+ signal,
1094
+ });
1095
+ // 登録順に走査し、いずれかのストレージに存在すれば true を返します。
1096
+ // exists に失敗したときは次のストレージを試します。
1097
+ const errors: PluginErrorRecord[] = [];
1098
+ const resolver = this.#pipeline.createResolver({
1099
+ key,
1100
+ store,
1101
+ signal,
1102
+ });
1103
+
1104
+ for (const leaf of this.#pipeline) {
1105
+ const resolved = resolver.resolve(leaf);
1106
+
1107
+ try {
1108
+ const c = new HasContext({
1109
+ key: resolved.key,
1110
+ store,
1111
+ signal,
1112
+ });
1113
+
1114
+ if (await leaf.storage.exists(c)) {
1115
+ return true;
1116
+ }
1117
+ } catch (ex) {
1118
+ if (signal.aborted) {
1119
+ throw ex;
1120
+ }
1121
+
1122
+ errors.push({
1123
+ name: leaf.name,
1124
+ index: leaf.index,
1125
+ reason: ex,
1126
+ });
1127
+
1128
+ logger.error`Failed to check existence in a storage: ${ex}`;
1129
+ }
1130
+ }
1131
+
1132
+ // すべてのストレージで存在を確認できなかったときは結果が不明なため、get() と同様にエラーとして報告します。
1133
+ if (errors.length > 0) {
1134
+ throw this.#notFound(key, errors);
1135
+ }
1136
+
1137
+ return false;
1138
+ }
1139
+
1140
+ public delete(options: DeleteOptions<KeyofKeyValueMapping<TKeyValueMapping>>): Promise<void>;
1141
+ public delete(
1142
+ key: KeyofKeyValueMapping<TKeyValueMapping>,
1143
+ options?: Omit<DeleteOptions, "key">,
1144
+ ): Promise<void>;
1145
+ public async delete(...args: any): Promise<void> {
1146
+ if (this.#con === null) {
1147
+ throw new UniKvsIsNotOpenError();
1148
+ }
1149
+
1150
+ const [options] = v.parseInput(DeleteArgsSchema, args);
1151
+ const { key, vars: varsOption, signal: signalOption } = options;
1152
+
1153
+ // キーがキースキーマに一致するかを検証します。
1154
+ this.#resolveValueSchema(key);
1155
+
1156
+ const { ac, io } = this.#con;
1157
+ const signal = combineSignals([ac.signal, signalOption]);
1158
+
1159
+ const store = this.#createOperationStore(varsOption);
1160
+
1161
+ using _lock = await asyncmux.readonly(this, signal);
1162
+ // ロック待機中に接続状態が変更されていないか再確認します。
1163
+ if (this.#con === null) {
1164
+ throw new UniKvsIsNotOpenError();
1165
+ }
1166
+
1167
+ // 削除に先立ち、各ミドルウェアの `use` を実行します。
1168
+ // `use` による vars の変更は後続の解決・削除に引き継がれます。
1169
+ const useContext = new DeleteContext({
1170
+ key,
1171
+ store,
1172
+ signal,
1173
+ });
1174
+ await this.#use("delete", useContext);
1175
+
1176
+ using _ioLock = await io.lock({
1177
+ key,
1178
+ // `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
1179
+ signal,
1180
+ });
1181
+ // 全宛先を並列に削除し、失敗は宛先の index 付きで集約します。
1182
+ const errors: PluginErrorRecord[] = [];
1183
+ const resolver = this.#pipeline.createResolver({
1184
+ key,
1185
+ store,
1186
+ signal,
1187
+ });
1188
+ await Promise.all(
1189
+ this.#pipeline.leaves.map(async (leaf) => {
1190
+ const resolved = resolver.resolve(leaf);
1191
+ const existsContext = new HasContext({
1192
+ key: resolved.key,
1193
+ store,
1194
+ signal,
1195
+ });
1196
+ const deleteContext = new DeleteContext({
1197
+ key: resolved.key,
1198
+ store,
1199
+ signal,
1200
+ });
1201
+
1202
+ try {
1203
+ if (await leaf.storage.exists(existsContext)) {
1204
+ await leaf.storage.delete(deleteContext);
1205
+ }
1206
+ } catch (ex) {
1207
+ errors.push({
1208
+ name: leaf.name,
1209
+ index: leaf.index,
1210
+ reason: ex,
1211
+ });
1212
+ }
1213
+ }),
1214
+ );
1215
+
1216
+ if (errors.length > 0) {
1217
+ throw new PluginOperationAggregateError({
1218
+ action: "delete",
1219
+ errors,
1220
+ plugin: "storage",
1221
+ });
1222
+ }
1223
+ }
1224
+
1225
+ public async clear(options?: ClearOptions): Promise<void>;
1226
+ public async clear(...args: any): Promise<void> {
1227
+ if (this.#con === null) {
1228
+ throw new UniKvsIsNotOpenError();
1229
+ }
1230
+
1231
+ const [options = {}] = v.parseInput(ClearArgsSchema, args);
1232
+ const { vars: varsOption, signal: signalOption } = options;
1233
+
1234
+ const { ac, io } = this.#con;
1235
+ const signal = combineSignals([ac.signal, signalOption]);
1236
+
1237
+ const store = this.#createOperationStore(varsOption);
1238
+
1239
+ using _lock = await asyncmux.readonly(this, signal);
1240
+ // ロック待機中に接続状態が変更されていないか再確認します。
1241
+ if (this.#con === null) {
1242
+ throw new UniKvsIsNotOpenError();
1243
+ }
1244
+
1245
+ using _ioLock = await io.lock({ signal });
1246
+ // 全ミドルウェアの `use` を実行します。
1247
+ const useContext = new ClearContext({
1248
+ store,
1249
+ signal,
1250
+ });
1251
+ await this.#use("clear", useContext);
1252
+ // `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
1253
+ throwIfAborted(signal);
1254
+ // 一括削除は全宛先で並列に実行し、失敗は宛先の index 付きで集約します。
1255
+ const errors: PluginErrorRecord[] = [];
1256
+ await Promise.all(
1257
+ this.#pipeline.leaves.map(async (leaf, index) => {
1258
+ try {
1259
+ const c = new ClearContext({
1260
+ store,
1261
+ signal,
1262
+ });
1263
+ await leaf.storage.clear(c);
1264
+ } catch (ex) {
1265
+ errors.push({
1266
+ name: leaf.name,
1267
+ index,
1268
+ reason: ex,
1269
+ });
1270
+ }
1271
+ }),
1272
+ );
1273
+
1274
+ if (errors.length > 0) {
1275
+ throw new PluginOperationAggregateError({
1276
+ action: "clear",
1277
+ errors,
1278
+ plugin: "storage",
1279
+ });
1280
+ }
1281
+ }
1282
+
1283
+ /**
1284
+ * 基本変数と操作固有変数を結合した実行時変数マップを作成します。
1285
+ *
1286
+ * @param vars 操作固有の変数です。
1287
+ *
1288
+ * @returns 結合した変数マップを返します。
1289
+ */
1290
+ #createOperationVars(vars: readonly VariablesEntry[] | undefined): Map<string, unknown> {
1291
+ const map = new Map(this.#baseVars);
1292
+
1293
+ if (vars) {
1294
+ for (const [k, value] of vars) {
1295
+ map.set(k, value);
1296
+ }
1297
+ }
1298
+
1299
+ return map;
1300
+ }
1301
+
1302
+ /**
1303
+ * 操作内で共有される変数ストアを作成します。
1304
+ *
1305
+ * 同一操作内の全コンテキスト (`use`/`encode`/`decode`/ ストレージ) で共有され、下流の `set` が上流の `await next()` 後の `get` に見えます。
1306
+ *
1307
+ * @param vars 操作固有の変数です。
1308
+ *
1309
+ * @returns 共有ストアを返します。
1310
+ */
1311
+ #createOperationStore(vars: readonly VariablesEntry[] | undefined): VariablesStore {
1312
+ return createVariablesStore([...this.#baseVars, ...(vars ?? [])]);
1313
+ }
1314
+
1315
+ /**
1316
+ * キーに対応する値のスキーマ情報を解決します。
1317
+ *
1318
+ * @param key 解決するキーです。
1319
+ *
1320
+ * @returns 対応するスキーマ情報を返します。スキーマ未設定または対応する定義がない場合は `undefined` を返します。
1321
+ */
1322
+ #resolveValueSchema(key: string): ValueSchemaInfo | undefined {
1323
+ return this.#valueSchemaResolver?.resolve(key);
1324
+ }
1325
+
1326
+ /**
1327
+ * すべての宛先で読み取りに失敗したときのエラーを組み立てます。
1328
+ *
1329
+ * @param key 読み取ったキーです。
1330
+ * @param errors 収集した失敗です。
1331
+ *
1332
+ * @returns `KeyNotFoundError` を返します。
1333
+ */
1334
+ #notFound(key: string, errors: readonly PluginErrorRecord[]): KeyNotFoundError {
1335
+ const args: KeyNotFoundErrorArgs = { key };
1336
+
1337
+ switch (errors.length) {
1338
+ case 0: {
1339
+ break;
1340
+ }
1341
+
1342
+ case 1: {
1343
+ const [firstError] = errors;
1344
+
1345
+ if (firstError !== undefined) {
1346
+ args.cause = firstError.reason;
1347
+ }
1348
+
1349
+ break;
1350
+ }
1351
+
1352
+ default: {
1353
+ args.cause = new PluginOperationAggregateError({
1354
+ action: "read",
1355
+ errors,
1356
+ plugin: "storage",
1357
+ });
1358
+ }
1359
+ }
1360
+
1361
+ return new KeyNotFoundError(args);
1362
+ }
1363
+
1364
+ async #close(
1365
+ operationVars: Map<string, unknown>,
1366
+ signal: AbortSignal,
1367
+ con: Connection,
1368
+ progress: { pluginsStarted: boolean },
1369
+ ): Promise<void> {
1370
+ using _lock = await asyncmux(this, signal);
1371
+ // ロック待機中に接続状態が変更されていないか再確認します。
1372
+ if (this.#con !== con) {
1373
+ throw new UniKvsIsNotOpenError();
1374
+ }
1375
+
1376
+ const { io } = this.#con;
1377
+ using _ioLock = await io.lock({ signal });
1378
+ const closeFns: [
1379
+ () => Promise<void>,
1380
+ () => Promise<void>,
1381
+ plugin: "storage" | "middleware",
1382
+ name: string,
1383
+ index: number,
1384
+ ][] = [];
1385
+
1386
+ // ストレージのクローズ処理を登録します。
1387
+ // open と同じ方針でプラグインごとに複製し、並列実行中の変更が互いに見えないようにします。
1388
+ for (const [index, leaf] of this.#pipeline.leaves.entries()) {
1389
+ const createContext = (): CloseContext =>
1390
+ new CloseContext({
1391
+ vars: new Map(operationVars).entries(),
1392
+ signal,
1393
+ });
1394
+ // 再試行は通常 close の失敗 (操作 signal の abort など) に引きずられないよう、新しいタイムアウト signal を使います。
1395
+ const createRetryContext = (): CloseContext =>
1396
+ new CloseContext({
1397
+ vars: new Map(operationVars).entries(),
1398
+ signal: AbortSignal.timeout(10e3),
1399
+ });
1400
+ closeFns.push([
1401
+ (): Promise<void> => leaf.storage.close(createContext()),
1402
+ (): Promise<void> => leaf.storage.retryClose(createRetryContext()),
1403
+ "storage",
1404
+ leaf.name,
1405
+ index,
1406
+ ]);
1407
+ }
1408
+
1409
+ // ミドルウェアのクローズ処理を登録します。
1410
+ for (const [index, plugin] of this.#pipeline.middlewares.entries()) {
1411
+ const createContext = (): CloseContext =>
1412
+ new CloseContext({
1413
+ vars: new Map(operationVars).entries(),
1414
+ signal,
1415
+ });
1416
+ // 再試行は通常 close の失敗 (操作 signal の abort など) に引きずられないよう、新しいタイムアウト signal を使います。
1417
+ const createRetryContext = (): CloseContext =>
1418
+ new CloseContext({
1419
+ vars: new Map(operationVars).entries(),
1420
+ signal: AbortSignal.timeout(10e3),
1421
+ });
1422
+ closeFns.push([
1423
+ (): Promise<void> => plugin.close(createContext()),
1424
+ (): Promise<void> => plugin.retryClose(createRetryContext()),
1425
+ "middleware",
1426
+ plugin.name,
1427
+ index,
1428
+ ]);
1429
+ }
1430
+
1431
+ // すべての処理を並列に実行し、エラーが発生した場合は集約します。
1432
+ progress.pluginsStarted = true;
1433
+ const errors: {
1434
+ name: string;
1435
+ index: number;
1436
+ plugin: "storage" | "middleware";
1437
+ reason: unknown;
1438
+ }[] = [];
1439
+ const retries: (() => Promise<void>)[] = [];
1440
+ await Promise.all(
1441
+ closeFns.map(async ([f, retry, plugin, name, index]) => {
1442
+ try {
1443
+ await f();
1444
+ } catch (ex) {
1445
+ errors.push({
1446
+ name,
1447
+ index,
1448
+ plugin,
1449
+ reason: ex,
1450
+ });
1451
+ // 通常の close で再試行すると、close 失敗後に別の利用者が引き継いだ参照まで解放してしまいます。
1452
+ retries.push(retry);
1453
+ }
1454
+ }),
1455
+ );
1456
+
1457
+ if (errors.length > 0) {
1458
+ // 失敗したプラグインだけを 1 回再試行します。
1459
+ // 成功済みのプラグインを再 close すると、参照カウントが二重に解放され、共有している他の利用者の実体まで閉じてしまうためです。
1460
+ await Promise.all(
1461
+ retries.map(async (f) => {
1596
1462
  try {
1597
- await r.cancel(ex);
1598
- } catch {
1599
- // キャンセルに失敗しても元の例外の伝播を優先します。
1463
+ await f();
1464
+ } catch (ex) {
1465
+ logger.error`Failed to retry closing a plugin: ${ex}`;
1600
1466
  }
1601
- }
1602
-
1603
- ioLock.release();
1467
+ }),
1468
+ );
1604
1469
 
1605
- throw ex;
1606
- }
1470
+ throw new PluginOperationAggregateError({
1471
+ action: "close",
1472
+ errors,
1473
+ });
1607
1474
  }
1475
+
1476
+ this.#con = null;
1608
1477
  }
1609
1478
 
1610
- public has(options: HasOptions<KeyofKeyValueMapping<TKeyValueMapping>>): Promise<boolean>;
1611
- public has(
1612
- key: KeyofKeyValueMapping<TKeyValueMapping>,
1613
- options?: Omit<HasOptions, "key">,
1614
- ): Promise<boolean>;
1615
- public async has(...args: any): Promise<boolean> {
1616
- if (this.#con === null) {
1617
- throw new UniKvsIsNotOpenError();
1479
+ /**
1480
+ * 書き込み失敗の集約と、成功した宛先への通知を行います。
1481
+ *
1482
+ * @param targets 書き込みを試みた対象です。
1483
+ * @param failed 失敗した葉の集合です。
1484
+ * @param errors 集約する失敗です。
1485
+ * @param signal 中断シグナルです。
1486
+ */
1487
+ async #finishWrites(
1488
+ targets: readonly {
1489
+ readonly key: string;
1490
+ readonly leaf: PipelineLeaf;
1491
+ }[],
1492
+ store: VariablesStore,
1493
+ failed: ReadonlySet<PipelineLeaf>,
1494
+ errors: PluginErrorRecord[],
1495
+ signal: AbortSignal,
1496
+ ): Promise<void> {
1497
+ if (errors.length === 0) {
1498
+ return;
1618
1499
  }
1619
1500
 
1620
- const [options] = parseInput(HasArgsSchema, args);
1621
- const { key, vars: varsOption, signal: signalOption } = options;
1622
-
1623
- // キーがキースキーマに一致するかを検証します。
1624
- this.#resolveValueSchema(key);
1625
-
1626
- const { ac, io } = this.#con;
1627
- const signal = combineSignals([ac.signal, signalOption]);
1501
+ const error = new PluginOperationAggregateError({
1502
+ action: "write",
1503
+ errors,
1504
+ plugin: "storage",
1505
+ });
1506
+ const notifyErrors: unknown[] = [];
1507
+ await Promise.all(
1508
+ targets.map(async (target) => {
1509
+ // 失敗した宛先を除いたすべての宛先へ通知します。通知に失敗しても元の集約エラーは置き換えません。
1510
+ if (failed.has(target.leaf)) {
1511
+ return;
1512
+ }
1628
1513
 
1629
- const store = this.#createOperationStore(varsOption);
1514
+ try {
1515
+ const c = new SetContext({
1516
+ key: target.key,
1517
+ store,
1518
+ signal,
1519
+ });
1520
+ await target.leaf.storage.onOtherWriteError(c, error);
1521
+ } catch (ex) {
1522
+ notifyErrors.push(ex);
1523
+ }
1524
+ }),
1525
+ );
1630
1526
 
1631
- using _lock = await asyncmux.readonly(this, signal);
1632
- // ロック待機中に接続状態が変更されていないか再確認します。
1633
- if (this.#con === null) {
1634
- throw new UniKvsIsNotOpenError();
1527
+ if (notifyErrors.length > 0) {
1528
+ logger.error(new AggregateError(notifyErrors, "Failed to handle error"));
1635
1529
  }
1636
1530
 
1637
- // 存在確認に先立ち、各ミドルウェアの `use` を実行します。
1638
- // `use` による vars の変更は後続の解決・走査に引き継がれます。
1639
- const useContext = new HasContext({
1640
- key,
1641
- store,
1642
- signal,
1643
- });
1644
- await this.#use("read", useContext);
1645
- // `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
1646
- throwIfAborted(signal);
1531
+ throw error;
1532
+ }
1647
1533
 
1648
- using _ioLock = await io.rLock({
1649
- key,
1650
- signal,
1651
- });
1652
- // 登録順に走査し、いずれかのストレージに存在すれば true を返します。
1653
- // exists に失敗したときは次のストレージを試します。
1654
- const errors: PluginErrorRecord[] = [];
1655
- const resolver = this.#pipeline.createResolver({
1656
- key,
1657
- store,
1658
- signal,
1659
- });
1534
+ /**
1535
+ * ミドルウェアの `use` をオニオン実行し、失敗時は集約して投げます。
1536
+ *
1537
+ * 同一コンテキストオブジェクトを全ミドルウェアで共有し、`next` で下流をネスト実行するため、下流の `set` が上流の `await next()` 後の `get` に見えます。
1538
+ * `encode`/`decode` と同じく `next()` の呼び出し規約を検証し、違反したときは操作を実行せず失敗させます (fail-close)。
1539
+ * いずれかのミドルウェアが投げたときは即時に確定し、後段は実行しません (fail-fast)。
1540
+ * 失敗は集約エラー (単一件) として投げます。
1541
+ *
1542
+ * @param action 集約エラーに付与する操作名です。
1543
+ * @param c 共有の `use` コンテキストです。
1544
+ */
1545
+ async #use(
1546
+ action: "read" | "write" | "delete" | "clear",
1547
+ c: IMiddleware.UseContext,
1548
+ ): Promise<void> {
1549
+ throwIfAborted(c.signal);
1660
1550
 
1661
- for (const leaf of this.#pipeline) {
1662
- const resolved = resolver.resolve(leaf);
1551
+ const middlewares = this.#pipeline.middlewares;
1552
+ const dispatch = async (index: number): Promise<void> => {
1553
+ const maybeMiddleware = middlewares[index];
1663
1554
 
1664
- try {
1665
- const c = new HasContext({
1666
- key: resolved.key,
1667
- store,
1668
- signal,
1669
- });
1555
+ if (maybeMiddleware === undefined) {
1556
+ return;
1557
+ }
1670
1558
 
1671
- if (await leaf.storage.exists(c)) {
1672
- return true;
1673
- }
1674
- } catch (ex) {
1675
- if (signal.aborted) {
1676
- throw ex;
1677
- }
1559
+ const middleware: UniKvsMiddleware = maybeMiddleware;
1678
1560
 
1679
- errors.push({
1680
- name: leaf.name,
1681
- index: leaf.index,
1682
- reason: ex,
1683
- });
1561
+ let nextCalled = false;
1562
+ let nextSettled = false;
1563
+ let downstreamError: unknown;
1564
+ let hasDownstreamError = false;
1684
1565
 
1685
- logger.error`Failed to check existence in a storage: ${ex}`;
1686
- }
1687
- }
1566
+ function next(): Promise<void> {
1567
+ if (nextCalled) {
1568
+ // 2 回目以降の `next()` は下流を実行せず拒否だけを返します。
1569
+ const rejection = Promise.reject(
1570
+ new MiddlewareNextCalledMultipleTimesError({
1571
+ name: middleware.name,
1572
+ operation: "use",
1573
+ }),
1574
+ );
1575
+ // 呼び出し側が切り離した場合の未処理 rejection を防ぎます。await したときは従来どおり拒否されます。
1576
+ // oxlint-disable-next-line promise/prefer-await-to-then
1577
+ rejection.catch(() => {});
1688
1578
 
1689
- // すべてのストレージで存在を確認できなかったときは結果が不明なため、get() と同様にエラーとして報告します。
1690
- if (errors.length > 0) {
1691
- throw this.#notFound(key, errors);
1692
- }
1579
+ return rejection;
1580
+ }
1693
1581
 
1694
- return false;
1695
- }
1582
+ nextCalled = true;
1696
1583
 
1697
- public delete(options: DeleteOptions<KeyofKeyValueMapping<TKeyValueMapping>>): Promise<void>;
1698
- public delete(
1699
- key: KeyofKeyValueMapping<TKeyValueMapping>,
1700
- options?: Omit<DeleteOptions, "key">,
1701
- ): Promise<void>;
1702
- public async delete(...args: any): Promise<void> {
1703
- if (this.#con === null) {
1704
- throw new UniKvsIsNotOpenError();
1705
- }
1584
+ const promise = Promise.try(async () => {
1585
+ try {
1586
+ await dispatch(index + 1);
1587
+ } catch (ex) {
1588
+ // 下流の失敗を親自身の失敗と取り違えないよう、由来を記録します。
1589
+ downstreamError = ex;
1590
+ hasDownstreamError = true;
1591
+ throw ex;
1592
+ } finally {
1593
+ nextSettled = true;
1594
+ }
1595
+ });
1596
+ // 呼び出し側が切り離した場合の未処理 rejection を防ぎます。await したときは従来どおり拒否されます。
1597
+ // oxlint-disable-next-line promise/prefer-await-to-then
1598
+ promise.catch(() => {});
1706
1599
 
1707
- const [options] = parseInput(DeleteArgsSchema, args);
1708
- const { key, vars: varsOption, signal: signalOption } = options;
1600
+ return promise;
1601
+ }
1709
1602
 
1710
- // キーがキースキーマに一致するかを検証します。
1711
- this.#resolveValueSchema(key);
1603
+ try {
1604
+ await middleware.use(c, next);
1605
+ throwIfAborted(c.signal);
1606
+ if (!nextCalled) {
1607
+ throw new MiddlewareNextNotCalledError({
1608
+ name: middleware.name,
1609
+ operation: "use",
1610
+ });
1611
+ }
1712
1612
 
1713
- const { ac, io } = this.#con;
1714
- const signal = combineSignals([ac.signal, signalOption]);
1613
+ if (!nextSettled) {
1614
+ throw new MiddlewareNextNotAwaitedError({
1615
+ name: middleware.name,
1616
+ operation: "use",
1617
+ });
1618
+ }
1619
+ } catch (ex) {
1620
+ // 中断による失敗はそのまま投げます。
1621
+ if (c.signal.aborted && ex === c.signal.reason) {
1622
+ throw ex;
1623
+ }
1715
1624
 
1716
- const store = this.#createOperationStore(varsOption);
1625
+ // 規約違反はプログラミングエラーであり、集約せずに投げます。
1626
+ if (
1627
+ ex instanceof MiddlewareNextNotCalledError ||
1628
+ ex instanceof MiddlewareNextNotAwaitedError ||
1629
+ ex instanceof MiddlewareNextCalledMultipleTimesError
1630
+ ) {
1631
+ throw ex;
1632
+ }
1717
1633
 
1718
- using _lock = await asyncmux.readonly(this, signal);
1719
- // ロック待機中に接続状態が変更されていないか再確認します。
1720
- if (this.#con === null) {
1721
- throw new UniKvsIsNotOpenError();
1722
- }
1634
+ // `next()` から伝播した下流の失敗は再集約せず、そのまま投げます。
1635
+ if (hasDownstreamError && ex === downstreamError) {
1636
+ throw ex;
1637
+ }
1723
1638
 
1724
- // 削除に先立ち、各ミドルウェアの `use` を実行します。
1725
- // `use` による vars の変更は後続の解決・削除に引き継がれます。
1726
- const useContext = new DeleteContext({
1727
- key,
1728
- store,
1729
- signal,
1730
- });
1731
- await this.#use("delete", useContext);
1639
+ throw new PluginOperationAggregateError({
1640
+ action,
1641
+ errors: [
1642
+ {
1643
+ name: middleware.name,
1644
+ index,
1645
+ reason: ex,
1646
+ },
1647
+ ],
1648
+ plugin: "middleware",
1649
+ });
1650
+ }
1651
+ };
1652
+ await dispatch(0);
1653
+ }
1732
1654
 
1655
+ /**
1656
+ * プレーン値の並列書き込みを行います。
1657
+ *
1658
+ * @param io 多重化インスタンスです。
1659
+ * @param signal 中断シグナルです。
1660
+ * @param key 書き込みキーです。
1661
+ * @param store 操作内で共有される変数ストアです。
1662
+ * @param value 書き込む値です.
1663
+ */
1664
+ async #setPlain(
1665
+ io: Asyncmux,
1666
+ signal: AbortSignal,
1667
+ key: string,
1668
+ store: VariablesStore,
1669
+ value: unknown,
1670
+ ): Promise<void> {
1671
+ const targets = await this.#pipeline.encode(
1672
+ {
1673
+ key,
1674
+ store,
1675
+ signal,
1676
+ },
1677
+ value,
1678
+ );
1733
1679
  using _ioLock = await io.lock({
1734
1680
  key,
1735
- // `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
1736
1681
  signal,
1737
1682
  });
1738
- // 全宛先を並列に削除し、失敗は宛先の index 付きで集約します。
1739
1683
  const errors: PluginErrorRecord[] = [];
1740
- const resolver = this.#pipeline.createResolver({
1741
- key,
1742
- store,
1743
- signal,
1744
- });
1684
+ const failed = new Set<PipelineLeaf>();
1745
1685
  await Promise.all(
1746
- this.#pipeline.leaves.map(async (leaf) => {
1747
- const resolved = resolver.resolve(leaf);
1748
- const existsContext = new HasContext({
1749
- key: resolved.key,
1750
- store,
1751
- signal,
1752
- });
1753
- const deleteContext = new DeleteContext({
1754
- key: resolved.key,
1755
- store,
1756
- signal,
1757
- });
1758
-
1686
+ targets.map(async (target) => {
1759
1687
  try {
1760
- if (await leaf.storage.exists(existsContext)) {
1761
- await leaf.storage.delete(deleteContext);
1762
- }
1688
+ const c = new SetContext({
1689
+ key: target.key,
1690
+ store,
1691
+ signal,
1692
+ });
1693
+ await target.leaf.storage.write(c, target.data);
1763
1694
  } catch (ex) {
1764
1695
  errors.push({
1765
- name: leaf.name,
1766
- index: leaf.index,
1696
+ name: target.leaf.name,
1697
+ index: target.leaf.index,
1767
1698
  reason: ex,
1768
1699
  });
1700
+ failed.add(target.leaf);
1769
1701
  }
1770
1702
  }),
1771
1703
  );
1772
-
1773
- if (errors.length > 0) {
1774
- throw new PluginOperationAggregateError({
1775
- action: "delete",
1776
- errors,
1777
- plugin: "storage",
1778
- });
1779
- }
1704
+ await this.#finishWrites(targets, store, failed, errors, signal);
1780
1705
  }
1781
1706
 
1782
- public async clear(options?: ClearOptions): Promise<void>;
1783
- public async clear(...args: any): Promise<void> {
1784
- if (this.#con === null) {
1785
- throw new UniKvsIsNotOpenError();
1786
- }
1787
-
1788
- const [options = {}] = parseInput(ClearArgsSchema, args);
1789
- const { vars: varsOption, signal: signalOption } = options;
1790
-
1791
- const { ac, io } = this.#con;
1792
- const signal = combineSignals([ac.signal, signalOption]);
1793
-
1794
- const store = this.#createOperationStore(varsOption);
1707
+ /**
1708
+ * ストリーム値の並列書き込みを行います。
1709
+ *
1710
+ * @param io 多重化インスタンスです。
1711
+ * @param signal 中断シグナルです。
1712
+ * @param key 書き込みキーです。
1713
+ * @param store 操作内で共有される変数ストアです。
1714
+ * @param value 書き込むストリームです。
1715
+ */
1716
+ async #setStream(
1717
+ io: Asyncmux,
1718
+ signal: AbortSignal,
1719
+ key: string,
1720
+ store: VariablesStore,
1721
+ value: IReadableStream,
1722
+ ): Promise<void> {
1723
+ const streams = new Set<IReadableStream>([value]);
1724
+ let tree;
1795
1725
 
1796
- using _lock = await asyncmux.readonly(this, signal);
1797
- // ロック待機中に接続状態が変更されていないか再確認します。
1798
- if (this.#con === null) {
1799
- throw new UniKvsIsNotOpenError();
1726
+ try {
1727
+ tree = await this.#pipeline.buildEncodeTree(
1728
+ {
1729
+ key,
1730
+ store,
1731
+ signal,
1732
+ },
1733
+ value,
1734
+ streams,
1735
+ );
1736
+ } catch (ex) {
1737
+ await cancelStreams(streams, ex);
1738
+ throw ex;
1800
1739
  }
1801
1740
 
1802
- using _ioLock = await io.lock({ signal });
1803
- // 全ミドルウェアの `use` を実行します。
1804
- const useContext = new ClearContext({
1805
- store,
1806
- signal,
1807
- });
1808
- await this.#use("clear", useContext);
1809
- // `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
1810
- throwIfAborted(signal);
1811
- // 一括削除は全宛先で並列に実行し、失敗は宛先の index 付きで集約します。
1741
+ using _ioLock = await io
1742
+ .lock({
1743
+ key,
1744
+ signal,
1745
+ })
1746
+ .catch(async (ex) => {
1747
+ // キーロックの待機中に中断されたときも、構築済みのストリームをキャンセルします。
1748
+ // ソースストリームをロックしたまま残さないためです。
1749
+ await cancelStreams(streams, ex);
1750
+ throw ex;
1751
+ });
1752
+
1812
1753
  const errors: PluginErrorRecord[] = [];
1754
+ const failed = new Set<PipelineLeaf>();
1813
1755
  await Promise.all(
1814
- this.#pipeline.leaves.map(async (leaf, index) => {
1756
+ tree.targets.map(async (target) => {
1815
1757
  try {
1816
- const c = new ClearContext({
1758
+ const c = new SetContext({
1759
+ key: target.key,
1817
1760
  store,
1818
1761
  signal,
1819
1762
  });
1820
- await leaf.storage.clear(c);
1763
+ const w = await target.leaf.storage.getWritable(c);
1764
+ await target.stream.pipeTo(w, { signal });
1821
1765
  } catch (ex) {
1766
+ // tee で分岐したストリームは、片方の分岐をキャンセルするまで他方が読み取ったチャンクを保持し続けるため、失敗時に放棄した分岐は確実にキャンセルします。
1767
+ try {
1768
+ await target.stream.cancel(ex);
1769
+ } catch {
1770
+ // キャンセルに失敗してもエラー集約を優先します。
1771
+ }
1772
+
1822
1773
  errors.push({
1823
- name: leaf.name,
1824
- index,
1774
+ name: target.leaf.name,
1775
+ index: target.leaf.index,
1825
1776
  reason: ex,
1826
1777
  });
1778
+ failed.add(target.leaf);
1827
1779
  }
1828
1780
  }),
1829
1781
  );
1782
+ await this.#finishWrites(tree.targets, store, failed, errors, signal);
1783
+ }
1830
1784
 
1831
- if (errors.length > 0) {
1832
- throw new PluginOperationAggregateError({
1833
- action: "clear",
1834
- errors,
1835
- plugin: "storage",
1836
- });
1837
- }
1785
+ /**
1786
+ * 読み取り元のチェーン全体を逆順にデコードし、書き戻し対象向けに再エンコードして書き込みます。
1787
+ *
1788
+ * @param data 読み取ったデータです。
1789
+ * @param hit 読み取り元の葉です。
1790
+ * @param missing 読み取りに失敗または不在だった葉です。
1791
+ * @param store 操作内で共有される変数ストアです。
1792
+ * @param key キーです。
1793
+ * @param signal 中断シグナルです。
1794
+ * @param throwOnRepairDenied 書き戻し拒否をエラーとして投げるかどうかです。
1795
+ *
1796
+ * @returns デコード済みの原データを返します。
1797
+ */
1798
+ async #repairPlain(
1799
+ data: unknown,
1800
+ hit: ResolvedLeaf,
1801
+ missing: readonly PipelineLeaf[],
1802
+ store: VariablesStore,
1803
+ key: string,
1804
+ signal: AbortSignal,
1805
+ throwOnRepairDenied: boolean,
1806
+ ): Promise<unknown> {
1807
+ const raw = await this.#pipeline.decode(
1808
+ hit.leaf,
1809
+ {
1810
+ key,
1811
+ store,
1812
+ signal,
1813
+ },
1814
+ data,
1815
+ );
1816
+ await this.#pipeline.repair(
1817
+ {
1818
+ key,
1819
+ store,
1820
+ signal,
1821
+ },
1822
+ missing,
1823
+ throwOnRepairDenied,
1824
+ raw,
1825
+ async (leaf, writeKey, input) => {
1826
+ const c = new SetContext({
1827
+ key: writeKey,
1828
+ store,
1829
+ signal,
1830
+ });
1831
+ await leaf.storage.write(c, input);
1832
+ },
1833
+ );
1834
+
1835
+ throwIfAborted(signal);
1836
+
1837
+ return raw;
1838
1838
  }
1839
1839
  }