unikvs 0.0.8 → 0.0.10

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
@@ -10,6 +10,7 @@ import UniKvsTransformer from "./_transformer.js";
10
10
  import * as v from "./_valibot.js";
11
11
  import type { ContextSource } from "./context.types.js";
12
12
  import {
13
+ type KeyNotFoundErrorArgs,
13
14
  KeyNotFoundError,
14
15
  UniKvsIsOpenError,
15
16
  PluginOperationAggregateError,
@@ -73,9 +74,11 @@ export type KeyofKeyValueMappingHasStreamValue<TKeyValueMapping extends KeyValue
73
74
 
74
75
  const ContextKeySchema = v.string();
75
76
 
77
+ // 配列形式の context も正当な入力であるため、record スキーマより先に判定する必要があります。
78
+ // record スキーマは配列にもマッチして数値キーのオブジェクトに変換してしまうため、配列形式を先に処理しないと mergeContext での配列としてのマージが行われません。
76
79
  const ContextSourceSchema = v.union([
77
- v.record(v.any(), v.unknown()),
78
80
  v.array(v.tuple([ContextKeySchema, v.unknown()])),
81
+ v.record(v.any(), v.unknown()),
79
82
  ]);
80
83
 
81
84
  const OpenOptionsSchema = v.object({
@@ -347,12 +350,31 @@ export type ClearOptions = v.InferInput<typeof ClearOptionsSchema>;
347
350
  const ioLockRegistry =
348
351
  typeof FinalizationRegistry !== "function"
349
352
  ? null
350
- : new FinalizationRegistry<AsyncmuxLock>((lock) => {
351
- if (!lock.released) {
352
- lock.release();
353
+ : new FinalizationRegistry<IoLockRegistryHeld>((held) => {
354
+ if (!held.lock.released) {
355
+ held.lock.release();
353
356
  }
357
+
358
+ // valueStream が読み取られないまま GC されたとき、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放するために破棄します。
359
+ // dispose は valueStream 自身を参照しないため、これを保持しても valueStream のガベージコレクションを妨げません。
360
+ // 失敗しても対処できないので無視します。
361
+ held.dispose().catch((ex) => {
362
+ logger.error`Failed to dispose value stream: ${ex}`;
363
+ });
354
364
  });
355
365
 
366
+ type IoLockRegistryHeld = {
367
+ /**
368
+ * stream 操作で取得したキーの読み取りロックです。
369
+ */
370
+ readonly lock: AsyncmuxLock;
371
+
372
+ /**
373
+ * {@linkcode ValueStream.dispose} です。ソースストリームのキャンセルとロック解放を行います。
374
+ */
375
+ readonly dispose: () => Promise<void>;
376
+ };
377
+
356
378
  /**
357
379
  * 接続状態を管理する内部オブジェクトです。
358
380
  */
@@ -542,9 +564,18 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
542
564
  }),
543
565
  );
544
566
  if (errors.length > 0) {
567
+ // 中断以外の失敗が混在しない場合は、abort 理由が集約エラーに埋もれないようにそのまま投げます。
568
+ if (signal.aborted && errors.every((error) => error.reason === signal.reason)) {
569
+ throw signal.reason;
570
+ }
571
+
545
572
  throw new PluginOperationAggregateError({ action: "open", errors });
546
573
  }
547
574
 
575
+ // すべてのプラグインのオープンが成功した後も、待機中に中断されていないか最終確認します。
576
+ // 中断済みのシグナルを持つ接続を作成すると、isOpen が true でありながら以降のすべての操作が即座に失敗する壊れた状態になります。
577
+ signal.throwIfAborted();
578
+
548
579
  this.#con = {
549
580
  ac,
550
581
  io: new Asyncmux(),
@@ -650,7 +681,8 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
650
681
  throw new UniKvsIsNotOpenError();
651
682
  }
652
683
 
653
- const { ac } = this.#con;
684
+ const con = this.#con;
685
+ const { ac } = con;
654
686
  const acArr = [ac, ...this.#acSet];
655
687
  this.#acSet.clear();
656
688
  for (const ac of acArr) {
@@ -659,12 +691,38 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
659
691
  }
660
692
  }
661
693
 
662
- return this.#close(context, signal, this.#con);
694
+ return this.#close(context, signal, con).catch(async (ex) => {
695
+ // #close が失敗した場合、コネクションの AbortController は既に abort 済みであり、以降の操作がすべて即座に失敗する壊れた状態になります。
696
+ // そこで接続を破棄して isOpen=false の一貫した状態にし、ベストエフォートでプラグインのクローズ処理を実行します。
697
+ if (this.#con === con) {
698
+ this.#con = null;
699
+
700
+ const disposeSignal = AbortSignal.timeout(10e3);
701
+ await Promise.all(
702
+ [...this.#destinations, ...this.#transformers].map(async (plugin) => {
703
+ try {
704
+ await plugin.close(context, disposeSignal);
705
+ } catch (reason) {
706
+ logger.error`Failed to close plugin after close failure: ${reason}`;
707
+ }
708
+ }),
709
+ );
710
+ }
711
+
712
+ throw ex;
713
+ });
663
714
  } catch (ex) {
664
715
  return Promise.reject(ex);
665
716
  }
666
717
  }
667
718
 
719
+ /**
720
+ * UniKvs が開いている場合にクローズします。
721
+ *
722
+ * `await using` 構文による自動クローズに使用されます。
723
+ *
724
+ * @returns 完了を通知する Promise です。
725
+ */
668
726
  async [Symbol.asyncDispose](): Promise<void> {
669
727
  if (!this.isOpen) {
670
728
  return;
@@ -728,25 +786,48 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
728
786
  if (value instanceof ReadableStream) {
729
787
  // 各トランスフォーマーを通してデータをエンコードストリームへ変換します。
730
788
  let data = value;
731
- for (const transformer of this.#transformers) {
732
- const e = await transformer.getEncodable(context, signal);
733
- data = data.pipeThrough(e);
789
+ try {
790
+ for (const transformer of this.#transformers) {
791
+ const e = await transformer.getEncodable(context, signal);
792
+ data = data.pipeThrough(e);
793
+ }
794
+ } catch (ex) {
795
+ // チェーンの構築中に失敗した場合、パイプが保持するソースストリームのリソースを解放するためにキャンセルします。
796
+ // キャンセルしないとソースストリームはロックされたままリークします。
797
+ try {
798
+ await data.cancel(ex);
799
+ } catch {
800
+ // キャンセルに失敗しても元の例外の伝播を優先します。
801
+ }
802
+
803
+ throw ex;
734
804
  }
735
805
 
736
806
  const lock = await io.lock({ key, signal });
737
807
  try {
738
808
  await Promise.all(
739
809
  this.#destinations.map(async (storage, i) => {
810
+ // tee ブランチはキャンセルされるまで健康なブランチの読み取り分のチャンクを保持し続けるため、失敗時に放棄したブランチを確実にキャンセルできるよう参照を保持します。
811
+ let branch: ReadableStream | null = null;
740
812
  try {
741
813
  // 複数のストレージがある場合は、tee メソッドを使用してストリームを分岐させます。
742
814
  let r = data;
743
815
  if (i !== this.#destinations.length - 1) {
744
816
  [r, data] = data.tee();
745
817
  }
818
+ branch = r;
746
819
 
747
820
  const w = await storage.getWritable(context, signal, key);
748
821
  await r.pipeTo(w, { signal });
749
822
  } catch (reason) {
823
+ if (branch !== null) {
824
+ try {
825
+ await branch.cancel(reason);
826
+ } catch {
827
+ // キャンセルに失敗してもエラー集約を優先します。
828
+ }
829
+ }
830
+
750
831
  errors.push({ reason });
751
832
  errorStorageSet.add(storage);
752
833
  }
@@ -861,14 +942,25 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
861
942
 
862
943
  const NONE = {};
863
944
  let data: any = NONE;
945
+ const errors: { reason: unknown }[] = [];
864
946
 
865
947
  const lock = await io.rLock({ key, signal });
866
948
  try {
867
949
  // 各ストレージを巡回し、最初に見つかったデータを取得します。
950
+ // あるストレージの読み取りに失敗しても、他のストレージからデータを取得できるようにフォールバックします。
868
951
  for (const storage of this.#destinations) {
869
- if (await storage.exists(context, signal, key)) {
952
+ try {
953
+ if (!(await storage.exists(context, signal, key))) {
954
+ continue;
955
+ }
870
956
  data = await storage.read(context, signal, key);
871
957
  break;
958
+ } catch (ex) {
959
+ if (signal.aborted) {
960
+ throw ex;
961
+ }
962
+ errors.push({ reason: ex });
963
+ logger.error`Failed to read from a storage: ${ex}`;
872
964
  }
873
965
  }
874
966
  } finally {
@@ -876,7 +968,22 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
876
968
  }
877
969
 
878
970
  if (data === NONE) {
879
- throw new KeyNotFoundError({ key });
971
+ const args: KeyNotFoundErrorArgs = { key };
972
+ switch (errors.length) {
973
+ case 0:
974
+ break;
975
+ case 1:
976
+ args.cause = errors[0]!.reason;
977
+ break;
978
+ default:
979
+ args.cause = new PluginOperationAggregateError({
980
+ plugin: "storage",
981
+ action: "read",
982
+ errors,
983
+ });
984
+ }
985
+
986
+ throw new KeyNotFoundError(args);
880
987
  }
881
988
 
882
989
  // トランスフォーマーを逆順に適用してデータをデコードします。
@@ -931,7 +1038,7 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
931
1038
  context["unikvs:action"] = "stream";
932
1039
  context["unikvs:key"] = key;
933
1040
 
934
- const lock = await asyncmux(this, signal);
1041
+ const lock = await asyncmux.readonly(this, signal);
935
1042
  try {
936
1043
  // ロック待機中に接続状態が変更されていないか再確認します。
937
1044
  if (this.#con === null) {
@@ -940,34 +1047,57 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
940
1047
 
941
1048
  const NONE: any = {};
942
1049
  let r: IReadableStream = NONE;
1050
+ const errors: { reason: unknown }[] = [];
943
1051
 
944
1052
  const lock = await io.rLock({ key, signal });
945
1053
  try {
946
1054
  // 各ストレージを巡回し、最初に見つかったデータを取得します。
1055
+ // あるストレージの読み取りに失敗しても、他のストレージからデータを取得できるようにフォールバックします。
947
1056
  for (const storage of this.#destinations) {
948
- if (await storage.exists(context, signal, key)) {
1057
+ try {
1058
+ if (!(await storage.exists(context, signal, key))) {
1059
+ continue;
1060
+ }
1061
+
949
1062
  r = await storage.getReadable(context, signal, key);
950
1063
  break;
1064
+ } catch (ex) {
1065
+ if (signal.aborted) {
1066
+ throw ex;
1067
+ }
1068
+
1069
+ errors.push({ reason: ex });
1070
+ logger.error`Failed to read from a storage: ${ex}`;
951
1071
  }
952
1072
  }
953
1073
 
954
1074
  if (r === NONE) {
955
- throw new KeyNotFoundError({ key });
1075
+ const args: KeyNotFoundErrorArgs = { key };
1076
+ switch (errors.length) {
1077
+ case 0:
1078
+ break;
1079
+ case 1:
1080
+ args.cause = errors[0]!.reason;
1081
+ break;
1082
+ default:
1083
+ args.cause = new PluginOperationAggregateError({
1084
+ plugin: "storage",
1085
+ action: "read",
1086
+ errors,
1087
+ });
1088
+ }
1089
+
1090
+ throw new KeyNotFoundError(args);
956
1091
  }
957
1092
 
958
1093
  // トランスフォーマーを逆順に適用し、デコード用トランスフォームを連結します。
959
- if (this.#transformers.length > 0) {
960
- for (const transformer of this.#transformers.toReversed()) {
961
- const d = await transformer.getDecodable(context, signal);
962
- r = r.pipeThrough(d);
963
- }
1094
+ for (const transformer of this.#transformers.toReversed()) {
1095
+ const d = await transformer.getDecodable(context, signal);
1096
+ r = r.pipeThrough(d);
964
1097
  }
965
1098
 
966
1099
  if (!ioLockRegistry) {
967
1100
  return toValueStream(r, async () => {
968
- try {
969
- await r.cancel();
970
- } catch {}
971
1101
  try {
972
1102
  lock.release();
973
1103
  } catch {}
@@ -979,19 +1109,30 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
979
1109
  try {
980
1110
  ioLockRegistry.unregister(unregisterToken);
981
1111
  } catch {}
982
- try {
983
- await r.cancel();
984
- } catch {}
985
1112
  try {
986
1113
  lock.release();
987
1114
  } catch {}
988
1115
  });
989
1116
 
990
- // valueStream が GC されるタイミングでストリームが終了していなければロックを自動解放します。
991
- ioLockRegistry.register(valueStream, lock, unregisterToken);
1117
+ // valueStream が GC されるタイミングでストリームが終了していなければロックを自動解放するとともに、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放できるように dispose を記録します。
1118
+ ioLockRegistry.register(
1119
+ valueStream,
1120
+ {
1121
+ lock,
1122
+ dispose: valueStream.dispose,
1123
+ },
1124
+ unregisterToken,
1125
+ );
992
1126
 
993
1127
  return valueStream;
994
1128
  } catch (ex) {
1129
+ if (r !== NONE) {
1130
+ // getReadable 成功後のセットアップ中に失敗した場合、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放するためにキャンセルします。
1131
+ try {
1132
+ await r.cancel(ex);
1133
+ } catch {}
1134
+ }
1135
+
995
1136
  lock.release();
996
1137
  throw ex;
997
1138
  }
@@ -1035,7 +1176,7 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
1035
1176
  context["unikvs:action"] = "has";
1036
1177
  context["unikvs:key"] = key;
1037
1178
 
1038
- const lock = await asyncmux(this, signal);
1179
+ const lock = await asyncmux.readonly(this, signal);
1039
1180
  try {
1040
1181
  // ロック待機中に接続状態が変更されていないか再確認します。
1041
1182
  if (this.#con === null) {
@@ -1045,12 +1186,40 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
1045
1186
  const lock = await io.rLock({ key, signal });
1046
1187
  try {
1047
1188
  // いずれかのストレージに存在すれば true を返します。
1189
+ // あるストレージの存在確認に失敗しても、他のストレージで存在を確認できるようにフォールバックします。
1190
+ const errors: { reason: unknown }[] = [];
1048
1191
  for (const storage of this.#destinations) {
1049
- if (await storage.exists(context, signal, key)) {
1050
- return true;
1192
+ try {
1193
+ if (await storage.exists(context, signal, key)) {
1194
+ return true;
1195
+ }
1196
+ } catch (ex) {
1197
+ if (signal.aborted) {
1198
+ throw ex;
1199
+ }
1200
+ errors.push({ reason: ex });
1201
+ logger.error`Failed to check existence in a storage: ${ex}`;
1051
1202
  }
1052
1203
  }
1053
1204
 
1205
+ // すべてのストレージで存在を確認できなかった場合は結果が不明のため、get() と同様にエラーとして報告します。
1206
+ if (errors.length > 0) {
1207
+ const args: KeyNotFoundErrorArgs = { key };
1208
+ switch (errors.length) {
1209
+ case 1:
1210
+ args.cause = errors[0]!.reason;
1211
+ break;
1212
+ default:
1213
+ args.cause = new PluginOperationAggregateError({
1214
+ plugin: "storage",
1215
+ action: "read",
1216
+ errors,
1217
+ });
1218
+ }
1219
+
1220
+ throw new KeyNotFoundError(args);
1221
+ }
1222
+
1054
1223
  return false;
1055
1224
  } finally {
1056
1225
  lock.release();
@@ -10,5 +10,10 @@ export interface ValueStream<T = any>
10
10
  Omit<ReadableStream, keyof AsyncDisposable | keyof AsyncIterable<unknown>>,
11
11
  AsyncDisposable,
12
12
  AsyncIterable<T, void, unknown> {
13
- dispose(): Promise<void>;
13
+ /**
14
+ * ストリームを閉じ、関連するリソースを解放します。
15
+ *
16
+ * @returns リソースの解放が完了したときに解決される Promise です。
17
+ */
18
+ dispose: (this: void) => Promise<void>;
14
19
  }