unikvs 0.0.9 → 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/dist/src/_logger.d.ts.map +1 -1
- package/dist/src/_storage.d.ts.map +1 -1
- package/dist/src/_to-value-stream.d.ts.map +1 -1
- package/dist/src/_to-value-stream.js +44 -6
- package/dist/src/_transformer.d.ts.map +1 -1
- package/dist/src/_valibot.d.ts +4 -5
- package/dist/src/_valibot.d.ts.map +1 -1
- package/dist/src/errors.d.ts +249 -24
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +107 -36
- package/dist/src/unikvs-config.d.ts.map +1 -1
- package/dist/src/unikvs-config.js +2 -2
- package/dist/src/unikvs.d.ts +10 -3
- package/dist/src/unikvs.d.ts.map +1 -1
- package/dist/src/unikvs.js +174 -32
- package/dist/src/value-stream.types.d.ts +6 -1
- package/dist/src/value-stream.types.d.ts.map +1 -1
- package/package.json +24 -24
- package/src/_to-value-stream.ts +47 -8
- package/src/_valibot.ts +4 -5
- package/src/errors.ts +316 -55
- package/src/unikvs-config.ts +2 -2
- package/src/unikvs.ts +199 -30
- package/src/value-stream.types.ts +6 -1
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<
|
|
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
|
|
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,
|
|
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
|
-
|
|
732
|
-
const
|
|
733
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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(
|
|
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
|
-
|
|
1050
|
-
|
|
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
|
-
|
|
13
|
+
/**
|
|
14
|
+
* ストリームを閉じ、関連するリソースを解放します。
|
|
15
|
+
*
|
|
16
|
+
* @returns リソースの解放が完了したときに解決される Promise です。
|
|
17
|
+
*/
|
|
18
|
+
dispose: (this: void) => Promise<void>;
|
|
14
19
|
}
|