unikvs 0.0.9 → 0.0.12

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 (63) hide show
  1. package/README.md +3 -0
  2. package/dist/src/_logger.d.ts.map +1 -1
  3. package/dist/src/_merge-variables.d.ts +13 -0
  4. package/dist/src/_merge-variables.d.ts.map +1 -0
  5. package/dist/src/_merge-variables.js +12 -0
  6. package/dist/src/_storage.d.ts +11 -11
  7. package/dist/src/_storage.d.ts.map +1 -1
  8. package/dist/src/_storage.js +31 -22
  9. package/dist/src/_streams.d.ts +20 -0
  10. package/dist/src/_streams.d.ts.map +1 -0
  11. package/dist/src/_streams.js +36 -0
  12. package/dist/src/_to-value-stream.d.ts.map +1 -1
  13. package/dist/src/_to-value-stream.js +44 -6
  14. package/dist/src/_transformer.d.ts +7 -7
  15. package/dist/src/_transformer.d.ts.map +1 -1
  16. package/dist/src/_transformer.js +12 -12
  17. package/dist/src/_valibot.d.ts +6 -7
  18. package/dist/src/_valibot.d.ts.map +1 -1
  19. package/dist/src/_valibot.js +1 -1
  20. package/dist/src/_value-schemas.d.ts +179 -0
  21. package/dist/src/_value-schemas.d.ts.map +1 -0
  22. package/dist/src/_value-schemas.js +150 -0
  23. package/dist/src/errors.d.ts +218 -27
  24. package/dist/src/errors.d.ts.map +1 -1
  25. package/dist/src/errors.js +102 -47
  26. package/dist/src/index.d.ts +3 -3
  27. package/dist/src/index.d.ts.map +1 -1
  28. package/dist/src/index.js +2 -2
  29. package/dist/src/unikvs-config.d.ts +58 -139
  30. package/dist/src/unikvs-config.d.ts.map +1 -1
  31. package/dist/src/unikvs-config.js +41 -46
  32. package/dist/src/unikvs.d.ts +86 -161
  33. package/dist/src/unikvs.d.ts.map +1 -1
  34. package/dist/src/unikvs.js +374 -171
  35. package/dist/src/utils.types.d.ts +1 -3
  36. package/dist/src/utils.types.d.ts.map +1 -1
  37. package/dist/src/value-stream.types.d.ts +5 -6
  38. package/dist/src/value-stream.types.d.ts.map +1 -1
  39. package/dist/src/variables.types.d.ts +11 -0
  40. package/dist/src/variables.types.d.ts.map +1 -0
  41. package/package.json +26 -25
  42. package/src/_merge-variables.ts +19 -0
  43. package/src/_storage.ts +47 -24
  44. package/src/_streams.ts +47 -0
  45. package/src/_to-value-stream.ts +47 -8
  46. package/src/_transformer.ts +13 -13
  47. package/src/_valibot.ts +8 -6
  48. package/src/_value-schemas.ts +362 -0
  49. package/src/errors.ts +280 -68
  50. package/src/index.ts +2 -3
  51. package/src/unikvs-config.ts +142 -167
  52. package/src/unikvs.ts +523 -285
  53. package/src/utils.types.ts +1 -3
  54. package/src/value-stream.types.ts +5 -6
  55. package/src/variables.types.ts +13 -0
  56. package/dist/src/_merge-context.d.ts +0 -13
  57. package/dist/src/_merge-context.d.ts.map +0 -1
  58. package/dist/src/_merge-context.js +0 -12
  59. package/dist/src/context.types.d.ts +0 -17
  60. package/dist/src/context.types.d.ts.map +0 -1
  61. package/src/_merge-context.ts +0 -19
  62. package/src/context.types.ts +0 -19
  63. /package/dist/src/{context.types.js → variables.types.js} +0 -0
package/src/unikvs.ts CHANGED
@@ -1,32 +1,40 @@
1
- import type { Context, IStorage, IReadableStream } from "@unikvs/core";
1
+ import type { Variables, IStorage, IReadableStream } from "@unikvs/core";
2
2
  import { combineSignals } from "abort-signal-utils";
3
3
  import { type AsyncmuxLock, asyncmux, Asyncmux } from "asyncmux";
4
4
 
5
5
  import logger from "./_logger.js";
6
- import mergeContext from "./_merge-context.js";
6
+ import mergeVariables from "./_merge-variables.js";
7
7
  import UniKvsStorage from "./_storage.js";
8
+ import { isReadableStream } from "./_streams.js";
8
9
  import toValueStream from "./_to-value-stream.js";
9
10
  import UniKvsTransformer from "./_transformer.js";
10
11
  import * as v from "./_valibot.js";
11
- import type { ContextSource } from "./context.types.js";
12
12
  import {
13
+ type KeyNotFoundErrorArgs,
13
14
  KeyNotFoundError,
14
15
  UniKvsIsOpenError,
15
- PluginOperationAggregateError,
16
16
  UniKvsIsNotOpenError,
17
+ PluginOperationAggregateError,
17
18
  } from "./errors.js";
18
19
  import UniKvsConfig, {
19
20
  type Value,
20
21
  type PlainValue,
21
22
  type StreamValue,
23
+ type UniKvsSchema,
22
24
  type KeyValueMapping,
25
+ type ValueSchemaInfo,
26
+ type UniKvsConfigOptions,
23
27
  type $InferPlainValueData,
24
28
  type IUniKvsConfigBuilder,
29
+ type IValueSchemaResolver,
25
30
  type KeyofKeyValueMapping,
31
+ type $InferKeyValueMapping,
32
+ type $InferPlainValueInput,
26
33
  type $InferStreamValueChunkData,
27
34
  } from "./unikvs-config.js";
28
35
  import type { ValueOf } from "./utils.types.js";
29
36
  import type { ValueStream } from "./value-stream.types.js";
37
+ import type { VariablesSource } from "./variables.types.js";
30
38
 
31
39
  // -------------------------------------------------------------------------------------------------
32
40
  //
@@ -35,18 +43,14 @@ import type { ValueStream } from "./value-stream.types.js";
35
43
  // -------------------------------------------------------------------------------------------------
36
44
 
37
45
  /**
38
- * 設定可能な値の型定義です。
39
- *
40
- * @template TValue 値の型定義です。
46
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#value-types)
41
47
  */
42
48
  export type SetValue<TValue extends Value> =
43
- | $InferPlainValueData<TValue>
44
- | (TValue extends StreamValue<infer TChunkData> ? ReadableStream<TChunkData> : never);
49
+ | $InferPlainValueInput<TValue>
50
+ | (TValue extends StreamValue<any, infer TChunkInput> ? ReadableStream<TChunkInput> : never);
45
51
 
46
52
  /**
47
- * プレーンな値を持つキーのみを抽出する型定義です。
48
- *
49
- * @template TKeyValueMapping キーと値のマッピング定義です。
53
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#value-types)
50
54
  */
51
55
  export type KeyofKeyValueMappingHasPlainValue<TKeyValueMapping extends KeyValueMapping> = ValueOf<{
52
56
  [TKey in KeyofKeyValueMapping<TKeyValueMapping>]: PlainValue extends TKeyValueMapping[TKey]
@@ -55,9 +59,7 @@ export type KeyofKeyValueMappingHasPlainValue<TKeyValueMapping extends KeyValueM
55
59
  }>;
56
60
 
57
61
  /**
58
- * ストリーム形式の値を持つキーのみを抽出する型定義です。
59
- *
60
- * @template TKeyValueMapping キーと値のマッピング定義です。
62
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#value-types)
61
63
  */
62
64
  export type KeyofKeyValueMappingHasStreamValue<TKeyValueMapping extends KeyValueMapping> = ValueOf<{
63
65
  [TKey in KeyofKeyValueMapping<TKeyValueMapping>]: StreamValue extends TKeyValueMapping[TKey]
@@ -65,17 +67,78 @@ export type KeyofKeyValueMappingHasStreamValue<TKeyValueMapping extends KeyValue
65
67
  : never;
66
68
  }>;
67
69
 
70
+ // -------------------------------------------------------------------------------------------------
71
+ //
72
+ // 値の検証
73
+ //
74
+ // -------------------------------------------------------------------------------------------------
75
+
76
+ /**
77
+ * プレーン値専用のスキーマに ReadableStream が渡されたことを報告するためのスキーマです。
78
+ */
79
+ const PlainValueInputSchema = v.pipe(
80
+ v.unknown(),
81
+ v.check(
82
+ (input) => !isReadableStream(input),
83
+ "Expected a plain value, but received a ReadableStream",
84
+ ),
85
+ );
86
+
87
+ /**
88
+ * ストリーム専用のスキーマにプレーン値が渡されたことを報告するためのスキーマです。
89
+ */
90
+ const StreamValueInputSchema = v.pipe(
91
+ v.unknown(),
92
+ v.check(isReadableStream, "Expected a ReadableStream, but received a plain value"),
93
+ );
94
+
95
+ /**
96
+ * スキーマ情報に基づいて `set` の入力値を検証します。
97
+ *
98
+ * プレーン値はその場で検証し、ストリームはチャンクごとに検証するストリームに変換します。
99
+ *
100
+ * @param info キーに対応するスキーマ情報です。
101
+ * @param value 検証する入力値です。
102
+ * @returns 検証済みの入力値を返します。
103
+ */
104
+ function parseSetValue(info: ValueSchemaInfo, value: unknown): unknown {
105
+ if (isReadableStream(value)) {
106
+ if (info.kind === "plain") {
107
+ // 必ず検証に失敗するため、InvalidInputError が投げられます。
108
+ v.parseInput(PlainValueInputSchema, value);
109
+ }
110
+
111
+ // 入力チャンクを検証し、変換後の値を後続のパイプラインへ流します。
112
+ return value.pipeThrough(
113
+ new TransformStream({
114
+ transform(chunk, controller) {
115
+ controller.enqueue(v.parseInput(info.schema, chunk));
116
+ },
117
+ }),
118
+ );
119
+ }
120
+
121
+ if (info.kind === "stream") {
122
+ // 必ず検証に失敗するため、InvalidInputError が投げられます。
123
+ v.parseInput(StreamValueInputSchema, value);
124
+ }
125
+
126
+ return v.parseInput(info.schema, value);
127
+ }
128
+
68
129
  // -------------------------------------------------------------------------------------------------
69
130
  //
70
131
  // スキーマ
71
132
  //
72
133
  // -------------------------------------------------------------------------------------------------
73
134
 
74
- const ContextKeySchema = v.string();
135
+ const VariablesKeySchema = v.string();
75
136
 
76
- const ContextSourceSchema = v.union([
137
+ // 配列形式の vars も正当な入力であるため、record スキーマより先に判定する必要があります。
138
+ // record スキーマは配列にもマッチして数値キーのオブジェクトに変換してしまうため、配列形式を先に処理しないと mergeVariables での配列としてのマージが行われません。
139
+ const VariablesSourceSchema = v.union([
140
+ v.array(v.tuple([VariablesKeySchema, v.unknown()])),
77
141
  v.record(v.any(), v.unknown()),
78
- v.array(v.tuple([ContextKeySchema, v.unknown()])),
79
142
  ]);
80
143
 
81
144
  const OpenOptionsSchema = v.object({
@@ -85,9 +148,9 @@ const OpenOptionsSchema = v.object({
85
148
  signal: v.optional(v.instance(AbortSignal)),
86
149
 
87
150
  /**
88
- * 実行時のコンテキスト情報です。
151
+ * 実行時の変数です。
89
152
  */
90
- context: v.optional(ContextSourceSchema),
153
+ vars: v.optional(VariablesSourceSchema),
91
154
  });
92
155
 
93
156
  const OpenArgsSchema = v.tuple([v.optional(OpenOptionsSchema)]);
@@ -99,9 +162,9 @@ const CloseOptionsSchema = v.object({
99
162
  signal: v.optional(v.instance(AbortSignal)),
100
163
 
101
164
  /**
102
- * 実行時のコンテキスト情報です。
165
+ * 実行時の変数です。
103
166
  */
104
- context: v.optional(ContextSourceSchema),
167
+ vars: v.optional(VariablesSourceSchema),
105
168
  });
106
169
 
107
170
  const CloseArgsSchema = v.tuple([v.optional(CloseOptionsSchema)]);
@@ -110,7 +173,7 @@ const SetOptionsSchema = v.object({
110
173
  key: v.string(),
111
174
  value: v.unknown(),
112
175
  signal: v.optional(v.instance(AbortSignal)),
113
- context: v.optional(ContextSourceSchema),
176
+ vars: v.optional(VariablesSourceSchema),
114
177
  });
115
178
 
116
179
  const SetArgsSchema = v.union([
@@ -128,7 +191,7 @@ const SetArgsSchema = v.union([
128
191
  const GetOptionsSchema = v.object({
129
192
  key: v.string(),
130
193
  signal: v.optional(v.instance(AbortSignal)),
131
- context: v.optional(ContextSourceSchema),
194
+ vars: v.optional(VariablesSourceSchema),
132
195
  });
133
196
 
134
197
  const GetArgsSchema = v.union([
@@ -142,7 +205,7 @@ const GetArgsSchema = v.union([
142
205
  const StreamOptionsSchema = v.object({
143
206
  key: v.string(),
144
207
  signal: v.optional(v.instance(AbortSignal)),
145
- context: v.optional(ContextSourceSchema),
208
+ vars: v.optional(VariablesSourceSchema),
146
209
  });
147
210
 
148
211
  const StreamArgsSchema = v.union([
@@ -156,7 +219,7 @@ const StreamArgsSchema = v.union([
156
219
  const HasOptionsSchema = v.object({
157
220
  key: v.string(),
158
221
  signal: v.optional(v.instance(AbortSignal)),
159
- context: v.optional(ContextSourceSchema),
222
+ vars: v.optional(VariablesSourceSchema),
160
223
  });
161
224
 
162
225
  const HasArgsSchema = v.union([
@@ -170,7 +233,7 @@ const HasArgsSchema = v.union([
170
233
  const DeleteOptionsSchema = v.object({
171
234
  key: v.string(),
172
235
  signal: v.optional(v.instance(AbortSignal)),
173
- context: v.optional(ContextSourceSchema),
236
+ vars: v.optional(VariablesSourceSchema),
174
237
  });
175
238
 
176
239
  const DeleteArgsSchema = v.union([
@@ -188,9 +251,9 @@ const ClearOptionsSchema = v.object({
188
251
  signal: v.optional(v.instance(AbortSignal)),
189
252
 
190
253
  /**
191
- * 実行時のコンテキスト情報です。
254
+ * 実行時の変数です。
192
255
  */
193
- context: v.optional(ContextSourceSchema),
256
+ vars: v.optional(VariablesSourceSchema),
194
257
  });
195
258
 
196
259
  const ClearArgsSchema = v.tuple([v.optional(ClearOptionsSchema)]);
@@ -202,136 +265,125 @@ const ClearArgsSchema = v.tuple([v.optional(ClearOptionsSchema)]);
202
265
  // -------------------------------------------------------------------------------------------------
203
266
 
204
267
  /**
205
- * オープン操作時のオプションです。
268
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
206
269
  */
207
270
  export type OpenOptions = v.InferInput<typeof OpenOptionsSchema>;
208
271
 
209
272
  /**
210
- * クローズ操作時のオプションです。
273
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
211
274
  */
212
275
  export type CloseOptions = v.InferInput<typeof CloseOptionsSchema>;
213
276
 
214
277
  /**
215
- * 保存操作時のオプションです。
216
- *
217
- * @template TKeyValueMapping キーと値のマッピング定義です。
218
- * @template TKey 対象となるキーの型です。
278
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
219
279
  */
220
280
  export type SetOptions<
221
281
  TKeyValueMapping extends KeyValueMapping = KeyValueMapping,
222
282
  TKey extends KeyofKeyValueMapping<TKeyValueMapping> = KeyofKeyValueMapping<TKeyValueMapping>,
223
283
  > = {
224
284
  /**
225
- * 操作対象を識別するためのキーです。
285
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
226
286
  */
227
287
  readonly key: TKey;
228
288
 
229
289
  /**
230
- * 保存する値です。
290
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
231
291
  */
232
292
  readonly value: SetValue<TKeyValueMapping[TKey]>;
233
293
 
234
294
  /**
235
- * 処理の中断を通知するためのシグナルです。
295
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
236
296
  */
237
297
  readonly signal?: AbortSignal | undefined;
238
298
 
239
299
  /**
240
- * 実行時のコンテキスト情報です。
300
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
241
301
  */
242
- readonly context?: ContextSource | undefined;
302
+ readonly vars?: VariablesSource | undefined;
243
303
  };
244
304
 
245
305
  /**
246
- * 取得操作時のオプションです。
247
- *
248
- * @template TKey 対象となるキーの型です。
306
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
249
307
  */
250
308
  export type GetOptions<TKey = IStorage.Key> = {
251
309
  /**
252
- * 操作対象を識別するためのキーです。
310
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
253
311
  */
254
312
  readonly key: TKey;
255
313
 
256
314
  /**
257
- * 処理の中断を通知するためのシグナルです。
315
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
258
316
  */
259
317
  readonly signal?: AbortSignal | undefined;
260
318
 
261
319
  /**
262
- * 実行時のコンテキスト情報です。
320
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
263
321
  */
264
- readonly context?: ContextSource | undefined;
322
+ readonly vars?: VariablesSource | undefined;
265
323
  };
266
324
 
267
325
  /**
268
- * ストリーム取得操作時のオプションです。
269
- *
270
- * @template TKey 対象となるキーの型です。
326
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#value-stream)
271
327
  */
272
328
  export type StreamOptions<TKey = IStorage.Key> = {
273
329
  /**
274
- * 操作対象を識別するためのキーです。
330
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#value-stream)
275
331
  */
276
332
  readonly key: TKey;
277
333
 
278
334
  /**
279
- * 処理の中断を通知するためのシグナルです。
335
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
280
336
  */
281
337
  readonly signal?: AbortSignal | undefined;
282
338
 
283
339
  /**
284
- * 実行時のコンテキスト情報です。
340
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
285
341
  */
286
- readonly context?: ContextSource | undefined;
342
+ readonly vars?: VariablesSource | undefined;
287
343
  };
288
344
 
289
345
  /**
290
- * 存在確認操作時のオプションです。
291
- *
292
- * @template TKey 対象となるキーの型です。
346
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
293
347
  */
294
348
  export type HasOptions<TKey = IStorage.Key> = {
295
349
  /**
296
- * 操作対象を識別するためのキーです。
350
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
297
351
  */
298
352
  readonly key: TKey;
299
353
 
300
354
  /**
301
- * 処理の中断を通知するためのシグナルです。
355
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
302
356
  */
303
357
  readonly signal?: AbortSignal | undefined;
304
358
 
305
359
  /**
306
- * 実行時のコンテキスト情報です。
360
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
307
361
  */
308
- readonly context?: ContextSource | undefined;
362
+ readonly vars?: VariablesSource | undefined;
309
363
  };
310
364
 
311
365
  /**
312
- * 削除操作時のオプションです。
313
- *
314
- * @template TKey 対象となるキーの型です。
366
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
315
367
  */
316
368
  export type DeleteOptions<TKey = IStorage.Key> = {
317
369
  /**
318
- * 操作対象を識別するためのキーです。
370
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
319
371
  */
320
372
  readonly key: TKey;
321
373
 
322
374
  /**
323
- * 処理の中断を通知するためのシグナルです。
375
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
324
376
  */
325
377
  readonly signal?: AbortSignal | undefined;
326
378
 
327
379
  /**
328
- * 実行時のコンテキスト情報です。
380
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
329
381
  */
330
- readonly context?: ContextSource | undefined;
382
+ readonly vars?: VariablesSource | undefined;
331
383
  };
332
384
 
333
385
  /**
334
- * 全削除操作時のオプションです。
386
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
335
387
  */
336
388
  export type ClearOptions = v.InferInput<typeof ClearOptionsSchema>;
337
389
 
@@ -347,12 +399,31 @@ export type ClearOptions = v.InferInput<typeof ClearOptionsSchema>;
347
399
  const ioLockRegistry =
348
400
  typeof FinalizationRegistry !== "function"
349
401
  ? null
350
- : new FinalizationRegistry<AsyncmuxLock>((lock) => {
351
- if (!lock.released) {
352
- lock.release();
402
+ : new FinalizationRegistry<IoLockRegistryHeld>((held) => {
403
+ if (!held.lock.released) {
404
+ held.lock.release();
353
405
  }
406
+
407
+ // valueStream が読み取られないまま GC されたとき、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放するために破棄します。
408
+ // dispose は valueStream 自身を参照しないため、これを保持しても valueStream のガベージコレクションを妨げません。
409
+ // 失敗しても対処できないので無視します。
410
+ held.dispose().catch((ex) => {
411
+ logger.error`Failed to dispose value stream: ${ex}`;
412
+ });
354
413
  });
355
414
 
415
+ type IoLockRegistryHeld = {
416
+ /**
417
+ * stream 操作で取得したキーの読み取りロックです。
418
+ */
419
+ readonly lock: AsyncmuxLock;
420
+
421
+ /**
422
+ * {@linkcode ValueStream.dispose} です。ソースストリームのキャンセルとロック解放を行います。
423
+ */
424
+ readonly dispose: () => Promise<void>;
425
+ };
426
+
356
427
  /**
357
428
  * 接続状態を管理する内部オブジェクトです。
358
429
  */
@@ -369,42 +440,45 @@ type Connection = {
369
440
  };
370
441
 
371
442
  /**
372
- * UniKvs メインクラスです。
373
- *
374
- * 複数のストレージとトランスフォーマーを統合して KVS 操作を提供します。
375
- *
376
- * @template TKeyValueMapping キーと値のマッピング定義です。
443
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#config-builder)
444
+ */
445
+ export type UniKvsDestination = {
446
+ /**
447
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#config-builder)
448
+ */
449
+ readonly storage: UniKvsStorage;
450
+
451
+ /**
452
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#config-builder)
453
+ */
454
+ readonly transformers: readonly UniKvsTransformer[];
455
+ };
456
+
457
+ /**
458
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
377
459
  */
378
460
  export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueMapping> {
379
461
  /**
380
- * UniKvs の設定ビルダーを作成します。
381
- *
382
- * @template TKeyValueMapping マッピング定義です。
383
- * @returns 設定ビルダーのインスタンスです。
384
- * @example
385
- * ```typescript
386
- * import { Compression } from "@unikvs/compression";
387
- * import { FileSystem } from "@unikvs/fs.node";
388
- * import { UniKvs, type Value } from "unikvs";
389
- *
390
- * const kvs = UniKvs.config<{
391
- * foo: Value<Uint8Array<ArrayBuffer>>;
392
- * }>()
393
- * .appendTransformer(new Compression("gzip"))
394
- * .appendStorage(new FileSystem(".tmp"))
395
- * .create();
396
- *
397
- * await kvs.open();
398
- *
399
- * await kvs.set("foo", Uint8Array.from([0, 1, 2]));
400
- *
401
- * await kvs.close();
402
- * ```
462
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#config-builder)
403
463
  */
404
464
  public static config<
405
465
  TKeyValueMapping extends KeyValueMapping,
406
- >(): IUniKvsConfigBuilder<TKeyValueMapping> {
407
- return new UniKvsConfig(this);
466
+ >(): IUniKvsConfigBuilder<TKeyValueMapping>;
467
+
468
+ /**
469
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#config-builder)
470
+ */
471
+ public static config<const TSchema extends UniKvsSchema>(options: {
472
+ readonly schema: TSchema;
473
+ }): IUniKvsConfigBuilder<$InferKeyValueMapping<TSchema>>;
474
+
475
+ /**
476
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#config-builder)
477
+ */
478
+ public static config(options?: UniKvsConfigOptions): IUniKvsConfigBuilder;
479
+
480
+ public static config(options?: UniKvsConfigOptions): IUniKvsConfigBuilder<any> {
481
+ return new UniKvsConfig(this, options);
408
482
  }
409
483
 
410
484
  /**
@@ -415,54 +489,61 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
415
489
  readonly #acSet: Set<AbortController>;
416
490
 
417
491
  /**
418
- * 基本となる実行コンテキスト情報です。
492
+ * 基本となる実行変数情報です。
419
493
  */
420
- readonly #context: Readonly<Context>;
494
+ readonly #vars: Readonly<Variables>;
421
495
 
422
496
  /**
423
- * データの永続化先となるストレージのリストです。
497
+ * データの永続化先となるストレージと前段パイプラインのリストです。
424
498
  */
425
- readonly #destinations: readonly [UniKvsStorage, ...UniKvsStorage[]];
499
+ readonly #destinations: readonly [UniKvsDestination, ...UniKvsDestination[]];
426
500
 
427
501
  /**
428
- * データの変換を行うトランスフォーマーのリストです。
502
+ * データの変換を行うトランスフォーマーのリストです。open/close の管理用に全件を保持します。
429
503
  */
430
504
  readonly #transformers: readonly UniKvsTransformer[];
431
505
 
432
506
  /**
433
- * インスタンスを初期化します。
434
- *
435
- * @internal UniKvs の設定ビルダー経由で使用します。
436
- * @param context コンテキストです。
437
- * @param destinations ストレージのリストです。
438
- * @param transformers トランスフォーマーのリストです。
507
+ * キーに対応する値のスキーマ情報を解決するリゾルバーです。スキーマ未設定時は null となります。
508
+ */
509
+ readonly #valueSchemaResolver: IValueSchemaResolver | null;
510
+
511
+ /**
512
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
439
513
  */
440
514
  public constructor(
441
- context: Readonly<Context>,
442
- destinations: readonly [UniKvsStorage, ...UniKvsStorage[]],
515
+ vars: Readonly<Variables>,
516
+ destinations: readonly [UniKvsDestination, ...UniKvsDestination[]],
443
517
  transformers: readonly UniKvsTransformer[],
518
+ valueSchemaResolver: IValueSchemaResolver | null = null,
444
519
  ) {
445
520
  this.#con = null;
446
521
  this.#acSet = new Set();
447
- this.#context = { ...context };
522
+ this.#vars = { ...vars };
448
523
  this.#destinations = destinations;
449
524
  this.#transformers = transformers;
525
+ this.#valueSchemaResolver = valueSchemaResolver;
450
526
  }
451
527
 
452
528
  /**
453
- * UniKvs が利用可能であるかを確認します。
529
+ * キーに対応する値のスキーマ情報を解決します。
454
530
  *
455
- * @returns 利用可能である場合は true、そうでない場合は false を返します。
531
+ * @param key 解決するキーです。
532
+ * @returns 対応するスキーマ情報を返します。スキーマ未設定または対応する定義がない場合は `undefined` を返します。
533
+ */
534
+ #resolveValueSchema(key: string): ValueSchemaInfo | undefined {
535
+ return this.#valueSchemaResolver?.resolve(key);
536
+ }
537
+
538
+ /**
539
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
456
540
  */
457
541
  public get isOpen(): boolean {
458
542
  return this.#con !== null;
459
543
  }
460
544
 
461
545
  /**
462
- * すべてのストレージとトランスフォーマーを初期化します。
463
- *
464
- * @param options オープン時のオプションです。
465
- * @returns 完了を通知する Promise です。
546
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
466
547
  */
467
548
  public open(options?: OpenOptions): Promise<void>;
468
549
 
@@ -472,13 +553,13 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
472
553
  }
473
554
 
474
555
  const [options = {}] = v.parseInput(OpenArgsSchema, args);
475
- const { signal: signalOption, context: contextOption } = options;
556
+ const { signal: signalOption, vars: varsOption } = options;
476
557
 
477
558
  const ac = new AbortController();
478
559
  const signal = combineSignals([ac.signal, signalOption]);
479
560
 
480
- const context = mergeContext(this.#context, contextOption);
481
- context["unikvs:action"] = "open";
561
+ const vars = mergeVariables(this.#vars, varsOption);
562
+ vars["unikvs:action"] = "open";
482
563
 
483
564
  signal.throwIfAborted();
484
565
 
@@ -495,14 +576,14 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
495
576
  const openFns: [() => Promise<void>, plugin: "storage" | "transformer"][] = [];
496
577
 
497
578
  // 各ストレージのオープン処理をリストに追加します。
498
- for (const storage of this.#destinations) {
579
+ for (const { storage } of this.#destinations) {
499
580
  openFns.push([
500
581
  async () => {
501
- await storage.open(context, signal);
582
+ await storage.open(vars, signal);
502
583
 
503
584
  dispose.push(async () => {
504
585
  try {
505
- await storage.close(context, signal);
586
+ await storage.close(vars, signal);
506
587
  } catch (ex) {
507
588
  logger.error`Failed to close storage: ${ex}`;
508
589
  }
@@ -516,11 +597,11 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
516
597
  for (const transformer of this.#transformers) {
517
598
  openFns.push([
518
599
  async () => {
519
- await transformer.open(context, signal);
600
+ await transformer.open(vars, signal);
520
601
 
521
602
  dispose.push(async () => {
522
603
  try {
523
- await transformer.close(context, signal);
604
+ await transformer.close(vars, signal);
524
605
  } catch (ex) {
525
606
  logger.error`Failed to close transformer: ${ex}`;
526
607
  }
@@ -542,9 +623,18 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
542
623
  }),
543
624
  );
544
625
  if (errors.length > 0) {
626
+ // 中断以外の失敗が混在しない場合は、abort 理由が集約エラーに埋もれないようにそのまま投げます。
627
+ if (signal.aborted && errors.every((error) => error.reason === signal.reason)) {
628
+ throw signal.reason;
629
+ }
630
+
545
631
  throw new PluginOperationAggregateError({ action: "open", errors });
546
632
  }
547
633
 
634
+ // すべてのプラグインのオープンが成功した後も、待機中に中断されていないか最終確認します。
635
+ // 中断済みのシグナルを持つ接続を作成すると、isOpen が true でありながら以降のすべての操作が即座に失敗する壊れた状態になります。
636
+ signal.throwIfAborted();
637
+
548
638
  this.#con = {
549
639
  ac,
550
640
  io: new Asyncmux(),
@@ -565,7 +655,7 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
565
655
  }
566
656
  }
567
657
 
568
- async #close(context: Context, signal: AbortSignal, con: Connection): Promise<void> {
658
+ async #close(vars: Variables, signal: AbortSignal, con: Connection): Promise<void> {
569
659
  const lock = await asyncmux(this, signal);
570
660
  try {
571
661
  // ロック待機中に接続状態が変更されていないか再確認します。
@@ -579,10 +669,10 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
579
669
  const closeFns: [() => Promise<void>, plugin: "storage" | "transformer"][] = [];
580
670
 
581
671
  // ストレージのクローズ処理を登録します。
582
- for (const plugin of this.#destinations) {
672
+ for (const { storage } of this.#destinations) {
583
673
  closeFns.push([
584
674
  async () => {
585
- await plugin.close(context, signal);
675
+ await storage.close(vars, signal);
586
676
  },
587
677
  "storage",
588
678
  ]);
@@ -592,7 +682,7 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
592
682
  for (const plugin of this.#transformers) {
593
683
  closeFns.push([
594
684
  async () => {
595
- await plugin.close(context, signal);
685
+ await plugin.close(vars, signal);
596
686
  },
597
687
  "transformer",
598
688
  ]);
@@ -623,20 +713,17 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
623
713
  }
624
714
 
625
715
  /**
626
- * ストレージをクローズします。
627
- *
628
- * @param options クローズ時のオプションです。
629
- * @returns 完了を通知する Promise です。
716
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
630
717
  */
631
718
  public close(options?: CloseOptions): Promise<void>;
632
719
 
633
720
  public close(...args: any): Promise<void> {
634
721
  try {
635
722
  const [options = {}] = v.parseInput(CloseArgsSchema, args);
636
- const { signal = AbortSignal.timeout(10e3), context: contextOption } = options;
723
+ const { signal = AbortSignal.timeout(10e3), vars: varsOption } = options;
637
724
 
638
- const context = mergeContext(this.#context, contextOption);
639
- context["unikvs:action"] = "close";
725
+ const vars = mergeVariables(this.#vars, varsOption);
726
+ vars["unikvs:action"] = "close";
640
727
 
641
728
  if (this.#con === null) {
642
729
  const acArr = [...this.#acSet];
@@ -650,7 +737,8 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
650
737
  throw new UniKvsIsNotOpenError();
651
738
  }
652
739
 
653
- const { ac } = this.#con;
740
+ const con = this.#con;
741
+ const { ac } = con;
654
742
  const acArr = [ac, ...this.#acSet];
655
743
  this.#acSet.clear();
656
744
  for (const ac of acArr) {
@@ -659,12 +747,36 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
659
747
  }
660
748
  }
661
749
 
662
- return this.#close(context, signal, this.#con);
750
+ return this.#close(vars, signal, con).catch(async (ex) => {
751
+ // #close が失敗した場合、コネクションの AbortController はすでに abort 済みであり、以降の操作がすべて即座に失敗する壊れた状態になります。
752
+ // そこで接続を破棄して isOpen=false の一貫した状態にし、ベストエフォートでプラグインのクローズ処理を実行します。
753
+ if (this.#con === con) {
754
+ this.#con = null;
755
+
756
+ const disposeSignal = AbortSignal.timeout(10e3);
757
+ await Promise.all(
758
+ [...this.#destinations.map((dest) => dest.storage), ...this.#transformers].map(
759
+ async (plugin) => {
760
+ try {
761
+ await plugin.close(vars, disposeSignal);
762
+ } catch (reason) {
763
+ logger.error`Failed to close plugin after close failure: ${reason}`;
764
+ }
765
+ },
766
+ ),
767
+ );
768
+ }
769
+
770
+ throw ex;
771
+ });
663
772
  } catch (ex) {
664
773
  return Promise.reject(ex);
665
774
  }
666
775
  }
667
776
 
777
+ /**
778
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
779
+ */
668
780
  async [Symbol.asyncDispose](): Promise<void> {
669
781
  if (!this.isOpen) {
670
782
  return;
@@ -676,24 +788,14 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
676
788
  }
677
789
 
678
790
  /**
679
- * 指定したオプションで値を保存します。
680
- *
681
- * @template TKey 対象キーの型定義です。
682
- * @param options セット操作のオプション一式です。
683
- * @returns 完了を通知する Promise です。
791
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
684
792
  */
685
793
  public set<const TKey extends KeyofKeyValueMapping<TKeyValueMapping>>(
686
794
  options: SetOptions<TKeyValueMapping, TKey>,
687
795
  ): Promise<void>;
688
796
 
689
797
  /**
690
- * 指定したキーに値を保存します。
691
- *
692
- * @template TKey 対象キーの型定義です。
693
- * @param key 操作対象を識別するためのキーです。
694
- * @param value 保存する値です。
695
- * @param options 追加のオプションです。
696
- * @returns 完了を通知する Promise です。
798
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
697
799
  */
698
800
  public set<const TKey extends KeyofKeyValueMapping<TKeyValueMapping>>(
699
801
  key: TKey,
@@ -707,14 +809,17 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
707
809
  }
708
810
 
709
811
  const [options] = v.parseInput(SetArgsSchema, args);
710
- const { key, value, signal: signalOption, context: contextOption } = options;
812
+ const { key, signal: signalOption, vars: varsOption } = options;
813
+
814
+ const valueSchema = this.#resolveValueSchema(key);
815
+ const value = valueSchema ? parseSetValue(valueSchema, options.value) : options.value;
711
816
 
712
817
  const { ac, io } = this.#con;
713
818
  const signal = combineSignals([ac.signal, signalOption]);
714
819
 
715
- const context = mergeContext(this.#context, contextOption);
716
- context["unikvs:action"] = "set";
717
- context["unikvs:key"] = key;
820
+ const vars = mergeVariables(this.#vars, varsOption);
821
+ vars["unikvs:action"] = "set";
822
+ vars["unikvs:key"] = key;
718
823
 
719
824
  const lock = await asyncmux(this, signal);
720
825
  try {
@@ -724,31 +829,76 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
724
829
  }
725
830
 
726
831
  const errors: { reason: unknown }[] = [];
727
- const errorStorageSet = new Set<UniKvsStorage>();
728
- if (value instanceof ReadableStream) {
729
- // 各トランスフォーマーを通してデータをエンコードストリームへ変換します。
730
- let data = value;
731
- for (const transformer of this.#transformers) {
732
- const e = await transformer.getEncodable(context, signal);
733
- data = data.pipeThrough(e);
832
+ const errorStorageSet = new Set<UniKvsDestination>();
833
+ if (isReadableStream(value)) {
834
+ // 各ストレージ専用の前段パイプラインを通すため、tee で分岐しながらエンコードストリームを構築します。
835
+ // 最長の前段パイプラインを基準にし、ストレージの登録順に分岐点を作ります。
836
+ let longest: readonly UniKvsTransformer[] = [];
837
+ for (const dest of this.#destinations) {
838
+ if (dest.transformers.length > longest.length) {
839
+ longest = dest.transformers;
840
+ }
841
+ }
842
+ let cur: ReadableStream = value;
843
+ const branches: ReadableStream[] = [];
844
+ const built: ReadableStream[] = [];
845
+ try {
846
+ let applied = 0;
847
+ for (let i = 0; i < this.#destinations.length; i++) {
848
+ const dest = this.#destinations[i]!;
849
+ while (applied < dest.transformers.length) {
850
+ const e = await longest[applied]!.getEncodable(vars, signal);
851
+ cur = cur.pipeThrough(e);
852
+ applied++;
853
+ }
854
+ if (i === this.#destinations.length - 1) {
855
+ branches[i] = cur;
856
+ } else {
857
+ const [branch, rest] = cur.tee();
858
+ branches[i] = branch;
859
+ built.push(branch);
860
+ cur = rest;
861
+ }
862
+ }
863
+ } catch (ex) {
864
+ // チェーンの構築中に失敗した場合、パイプが保持するソースストリームのリソースを解放するためにキャンセルします。
865
+ // キャンセルしないとソースストリームはロックされたままリークします。
866
+ try {
867
+ await cur.cancel(ex);
868
+ } catch {
869
+ // キャンセルに失敗しても元の例外の伝播を優先します。
870
+ }
871
+ await Promise.all(
872
+ built.map(async (branch) => {
873
+ try {
874
+ await branch.cancel(ex);
875
+ } catch {
876
+ // キャンセルに失敗しても元の例外の伝播を優先します。
877
+ }
878
+ }),
879
+ );
880
+
881
+ throw ex;
734
882
  }
735
883
 
736
884
  const lock = await io.lock({ key, signal });
737
885
  try {
738
886
  await Promise.all(
739
- this.#destinations.map(async (storage, i) => {
887
+ this.#destinations.map(async (dest, i) => {
888
+ // tee ブランチはキャンセルされるまで健康なブランチの読み取り分のチャンクを保持し続けるため、失敗時に放棄したブランチを確実にキャンセルできるよう参照を保持します。
889
+ const branch = branches[i]!;
740
890
  try {
741
- // 複数のストレージがある場合は、tee メソッドを使用してストリームを分岐させます。
742
- let r = data;
743
- if (i !== this.#destinations.length - 1) {
744
- [r, data] = data.tee();
891
+ const w = await dest.storage.getWritable(vars, signal, key);
892
+ await branch.pipeTo(w, { signal });
893
+ } catch (reason) {
894
+ try {
895
+ await branch.cancel(reason);
896
+ } catch {
897
+ // キャンセルに失敗してもエラー集約を優先します。
745
898
  }
746
899
 
747
- const w = await storage.getWritable(context, signal, key);
748
- await r.pipeTo(w, { signal });
749
- } catch (reason) {
750
900
  errors.push({ reason });
751
- errorStorageSet.add(storage);
901
+ errorStorageSet.add(dest);
752
902
  }
753
903
  }),
754
904
  );
@@ -756,21 +906,28 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
756
906
  lock.release();
757
907
  }
758
908
  } else {
759
- // 登録されているトランスフォーマーを順番に適用してデータをエンコードします。
760
- let data = value;
761
- for (const transformer of this.#transformers) {
762
- data = await transformer.encode(context, signal, data);
909
+ // 各ストレージ専用の前段パイプラインでデータをエンコードします。共有プレフィックスは使い回します。
910
+ let longest: readonly UniKvsTransformer[] = [];
911
+ for (const dest of this.#destinations) {
912
+ if (dest.transformers.length > longest.length) {
913
+ longest = dest.transformers;
914
+ }
915
+ }
916
+ const prefix: unknown[] = [value];
917
+ for (const transformer of longest) {
918
+ prefix.push(await transformer.encode(vars, signal, prefix[prefix.length - 1]));
763
919
  }
920
+ const encoded = this.#destinations.map((dest) => prefix[dest.transformers.length]);
764
921
 
765
922
  const lock = await io.lock({ key, signal });
766
923
  try {
767
924
  await Promise.all(
768
- this.#destinations.map(async (storage) => {
925
+ this.#destinations.map(async (dest, i) => {
769
926
  try {
770
- await storage.write(context, signal, key, data);
927
+ await dest.storage.write(vars, signal, key, encoded[i]);
771
928
  } catch (reason) {
772
929
  errors.push({ reason });
773
- errorStorageSet.add(storage);
930
+ errorStorageSet.add(dest);
774
931
  }
775
932
  }),
776
933
  );
@@ -789,13 +946,13 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
789
946
  {
790
947
  const errors: unknown[] = [];
791
948
  await Promise.all(
792
- this.#destinations.map(async (storage) => {
793
- if (errorStorageSet.has(storage)) {
949
+ this.#destinations.map(async (dest) => {
950
+ if (errorStorageSet.has(dest)) {
794
951
  return;
795
952
  }
796
953
 
797
954
  try {
798
- await storage.onOtherWriteError(context, signal, key, error);
955
+ await dest.storage.onOtherWriteError(vars, signal, key, error);
799
956
  } catch (ex) {
800
957
  errors.push(ex);
801
958
  }
@@ -814,23 +971,14 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
814
971
  }
815
972
 
816
973
  /**
817
- * 指定したオプションで値を取得します。
818
- *
819
- * @template TKey 対象キーの型定義です。
820
- * @param options 取得操作のオプション一式です。
821
- * @returns 取得した値を返します。
974
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
822
975
  */
823
976
  public get<const TKey extends KeyofKeyValueMappingHasPlainValue<TKeyValueMapping>>(
824
977
  options: GetOptions<TKey>,
825
978
  ): Promise<$InferPlainValueData<TKeyValueMapping[TKey]>>;
826
979
 
827
980
  /**
828
- * 指定したキーから値を取得します。
829
- *
830
- * @template TKey 対象キーの型定義です。
831
- * @param key 操作対象を識別するためのキーです。
832
- * @param options 追加のオプションです。
833
- * @returns 取得した値を返します。
981
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
834
982
  */
835
983
  public get<const TKey extends KeyofKeyValueMappingHasPlainValue<TKeyValueMapping>>(
836
984
  key: TKey,
@@ -843,14 +991,16 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
843
991
  }
844
992
 
845
993
  const [options] = v.parseInput(GetArgsSchema, args);
846
- const { key, signal: signalOption, context: contextOption } = options;
994
+ const { key, signal: signalOption, vars: varsOption } = options;
995
+
996
+ const valueSchema = this.#resolveValueSchema(key);
847
997
 
848
998
  const { ac, io } = this.#con;
849
999
  const signal = combineSignals([ac.signal, signalOption]);
850
1000
 
851
- const context = mergeContext(this.#context, contextOption);
852
- context["unikvs:action"] = "get";
853
- context["unikvs:key"] = key;
1001
+ const vars = mergeVariables(this.#vars, varsOption);
1002
+ vars["unikvs:action"] = "get";
1003
+ vars["unikvs:key"] = key;
854
1004
 
855
1005
  const lock = await asyncmux.readonly(this, signal);
856
1006
  try {
@@ -861,14 +1011,27 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
861
1011
 
862
1012
  const NONE = {};
863
1013
  let data: any = NONE;
1014
+ let transformers: readonly UniKvsTransformer[] = [];
1015
+ const errors: { reason: unknown }[] = [];
864
1016
 
865
1017
  const lock = await io.rLock({ key, signal });
866
1018
  try {
867
1019
  // 各ストレージを巡回し、最初に見つかったデータを取得します。
868
- for (const storage of this.#destinations) {
869
- if (await storage.exists(context, signal, key)) {
870
- data = await storage.read(context, signal, key);
1020
+ // あるストレージの読み取りに失敗しても、他のストレージからデータを取得できるようにフォールバックします。
1021
+ for (const dest of this.#destinations) {
1022
+ try {
1023
+ if (!(await dest.storage.exists(vars, signal, key))) {
1024
+ continue;
1025
+ }
1026
+ data = await dest.storage.read(vars, signal, key);
1027
+ transformers = dest.transformers;
871
1028
  break;
1029
+ } catch (ex) {
1030
+ if (signal.aborted) {
1031
+ throw ex;
1032
+ }
1033
+ errors.push({ reason: ex });
1034
+ logger.error`Failed to read from a storage: ${ex}`;
872
1035
  }
873
1036
  }
874
1037
  } finally {
@@ -876,14 +1039,32 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
876
1039
  }
877
1040
 
878
1041
  if (data === NONE) {
879
- throw new KeyNotFoundError({ key });
1042
+ const args: KeyNotFoundErrorArgs = { key };
1043
+ switch (errors.length) {
1044
+ case 0:
1045
+ break;
1046
+ case 1:
1047
+ args.cause = errors[0]!.reason;
1048
+ break;
1049
+ default:
1050
+ args.cause = new PluginOperationAggregateError({
1051
+ plugin: "storage",
1052
+ action: "read",
1053
+ errors,
1054
+ });
1055
+ }
1056
+
1057
+ throw new KeyNotFoundError(args);
880
1058
  }
881
1059
 
882
- // トランスフォーマーを逆順に適用してデータをデコードします。
883
- if (this.#transformers.length > 0) {
884
- for (const transformer of this.#transformers.toReversed()) {
885
- data = await transformer.decode(context, signal, data);
886
- }
1060
+ // 見つかったストレージ専用の前段パイプラインを逆順に適用してデータをデコードします。
1061
+ for (const transformer of transformers.toReversed()) {
1062
+ data = await transformer.decode(vars, signal, data);
1063
+ }
1064
+
1065
+ // デコード後のデータを検証します。
1066
+ if (valueSchema) {
1067
+ data = v.parseOutput(valueSchema.schema, data);
887
1068
  }
888
1069
 
889
1070
  return data;
@@ -893,23 +1074,14 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
893
1074
  }
894
1075
 
895
1076
  /**
896
- * 指定したオプションでストリームを取得します。
897
- *
898
- * @template TKey 対象キーの型定義です。
899
- * @param options ストリーム取得操作のオプション一式です。
900
- * @returns 取得したストリームを返します。
1077
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#value-stream)
901
1078
  */
902
1079
  public stream<const TKey extends KeyofKeyValueMappingHasStreamValue<TKeyValueMapping>>(
903
1080
  options: StreamOptions<TKey>,
904
1081
  ): Promise<ValueStream<$InferStreamValueChunkData<TKeyValueMapping[TKey]>>>;
905
1082
 
906
1083
  /**
907
- * 指定したキーからストリームを取得します。
908
- *
909
- * @template TKey 対象キーの型定義です。
910
- * @param key 操作対象を識別するためのキーです。
911
- * @param options 追加のオプションです。
912
- * @returns 取得したストリームを返します。
1084
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#value-stream)
913
1085
  */
914
1086
  public stream<const TKey extends KeyofKeyValueMappingHasStreamValue<TKeyValueMapping>>(
915
1087
  key: TKey,
@@ -922,16 +1094,18 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
922
1094
  }
923
1095
 
924
1096
  const [options] = v.parseInput(StreamArgsSchema, args);
925
- const { key, signal: signalOption, context: contextOption } = options;
1097
+ const { key, signal: signalOption, vars: varsOption } = options;
1098
+
1099
+ const valueSchema = this.#resolveValueSchema(key);
926
1100
 
927
1101
  const { ac, io } = this.#con;
928
1102
  const signal = combineSignals([ac.signal, signalOption]);
929
1103
 
930
- const context = mergeContext(this.#context, contextOption);
931
- context["unikvs:action"] = "stream";
932
- context["unikvs:key"] = key;
1104
+ const vars = mergeVariables(this.#vars, varsOption);
1105
+ vars["unikvs:action"] = "stream";
1106
+ vars["unikvs:key"] = key;
933
1107
 
934
- const lock = await asyncmux(this, signal);
1108
+ const lock = await asyncmux.readonly(this, signal);
935
1109
  try {
936
1110
  // ロック待機中に接続状態が変更されていないか再確認します。
937
1111
  if (this.#con === null) {
@@ -940,34 +1114,70 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
940
1114
 
941
1115
  const NONE: any = {};
942
1116
  let r: IReadableStream = NONE;
1117
+ let transformers: readonly UniKvsTransformer[] = [];
1118
+ const errors: { reason: unknown }[] = [];
943
1119
 
944
1120
  const lock = await io.rLock({ key, signal });
945
1121
  try {
946
1122
  // 各ストレージを巡回し、最初に見つかったデータを取得します。
947
- for (const storage of this.#destinations) {
948
- if (await storage.exists(context, signal, key)) {
949
- r = await storage.getReadable(context, signal, key);
1123
+ // あるストレージの読み取りに失敗しても、他のストレージからデータを取得できるようにフォールバックします。
1124
+ for (const dest of this.#destinations) {
1125
+ try {
1126
+ if (!(await dest.storage.exists(vars, signal, key))) {
1127
+ continue;
1128
+ }
1129
+
1130
+ r = await dest.storage.getReadable(vars, signal, key);
1131
+ transformers = dest.transformers;
950
1132
  break;
1133
+ } catch (ex) {
1134
+ if (signal.aborted) {
1135
+ throw ex;
1136
+ }
1137
+
1138
+ errors.push({ reason: ex });
1139
+ logger.error`Failed to read from a storage: ${ex}`;
951
1140
  }
952
1141
  }
953
1142
 
954
1143
  if (r === NONE) {
955
- throw new KeyNotFoundError({ key });
1144
+ const args: KeyNotFoundErrorArgs = { key };
1145
+ switch (errors.length) {
1146
+ case 0:
1147
+ break;
1148
+ case 1:
1149
+ args.cause = errors[0]!.reason;
1150
+ break;
1151
+ default:
1152
+ args.cause = new PluginOperationAggregateError({
1153
+ plugin: "storage",
1154
+ action: "read",
1155
+ errors,
1156
+ });
1157
+ }
1158
+
1159
+ throw new KeyNotFoundError(args);
956
1160
  }
957
1161
 
958
- // トランスフォーマーを逆順に適用し、デコード用トランスフォームを連結します。
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
- }
1162
+ // 見つかったストレージ専用の前段パイプラインを逆順に適用し、デコード用トランスフォームを連結します。
1163
+ for (const transformer of transformers.toReversed()) {
1164
+ const d = await transformer.getDecodable(vars, signal);
1165
+ r = r.pipeThrough(d);
1166
+ }
1167
+
1168
+ // デコード後のチャンクを検証します。
1169
+ if (valueSchema) {
1170
+ r = r.pipeThrough(
1171
+ new TransformStream({
1172
+ transform(chunk, controller) {
1173
+ controller.enqueue(v.parseOutput(valueSchema.schema, chunk));
1174
+ },
1175
+ }),
1176
+ );
964
1177
  }
965
1178
 
966
1179
  if (!ioLockRegistry) {
967
1180
  return toValueStream(r, async () => {
968
- try {
969
- await r.cancel();
970
- } catch {}
971
1181
  try {
972
1182
  lock.release();
973
1183
  } catch {}
@@ -979,19 +1189,30 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
979
1189
  try {
980
1190
  ioLockRegistry.unregister(unregisterToken);
981
1191
  } catch {}
982
- try {
983
- await r.cancel();
984
- } catch {}
985
1192
  try {
986
1193
  lock.release();
987
1194
  } catch {}
988
1195
  });
989
1196
 
990
- // valueStream が GC されるタイミングでストリームが終了していなければロックを自動解放します。
991
- ioLockRegistry.register(valueStream, lock, unregisterToken);
1197
+ // valueStream が GC されるタイミングでストリームが終了していなければロックを自動解放するとともに、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放できるように dispose を記録します。
1198
+ ioLockRegistry.register(
1199
+ valueStream,
1200
+ {
1201
+ lock,
1202
+ dispose: valueStream.dispose,
1203
+ },
1204
+ unregisterToken,
1205
+ );
992
1206
 
993
1207
  return valueStream;
994
1208
  } catch (ex) {
1209
+ if (r !== NONE) {
1210
+ // getReadable 成功後のセットアップ中に失敗した場合、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放するためにキャンセルします。
1211
+ try {
1212
+ await r.cancel(ex);
1213
+ } catch {}
1214
+ }
1215
+
995
1216
  lock.release();
996
1217
  throw ex;
997
1218
  }
@@ -1001,19 +1222,12 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
1001
1222
  }
1002
1223
 
1003
1224
  /**
1004
- * 指定したオプションでキーの存在を確認します。
1005
- *
1006
- * @param options 存在確認操作のオプションです。
1007
- * @returns 存在する場合は true、そうでない場合は false を返します。
1225
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
1008
1226
  */
1009
1227
  public has(options: HasOptions<KeyofKeyValueMapping<TKeyValueMapping>>): Promise<boolean>;
1010
1228
 
1011
1229
  /**
1012
- * 指定したキーが存在するかを確認します。
1013
- *
1014
- * @param key 操作対象を識別するためのキーです。
1015
- * @param options 追加のオプションです。
1016
- * @returns 存在する場合は true、そうでない場合は false を返します。
1230
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
1017
1231
  */
1018
1232
  public has(
1019
1233
  key: KeyofKeyValueMapping<TKeyValueMapping>,
@@ -1026,16 +1240,19 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
1026
1240
  }
1027
1241
 
1028
1242
  const [options] = v.parseInput(HasArgsSchema, args);
1029
- const { key, signal: signalOption, context: contextOption } = options;
1243
+ const { key, signal: signalOption, vars: varsOption } = options;
1244
+
1245
+ // キーがキースキーマに一致するかを検証します。
1246
+ this.#resolveValueSchema(key);
1030
1247
 
1031
1248
  const { ac, io } = this.#con;
1032
1249
  const signal = combineSignals([ac.signal, signalOption]);
1033
1250
 
1034
- const context = mergeContext(this.#context, contextOption);
1035
- context["unikvs:action"] = "has";
1036
- context["unikvs:key"] = key;
1251
+ const vars = mergeVariables(this.#vars, varsOption);
1252
+ vars["unikvs:action"] = "has";
1253
+ vars["unikvs:key"] = key;
1037
1254
 
1038
- const lock = await asyncmux(this, signal);
1255
+ const lock = await asyncmux.readonly(this, signal);
1039
1256
  try {
1040
1257
  // ロック待機中に接続状態が変更されていないか再確認します。
1041
1258
  if (this.#con === null) {
@@ -1045,12 +1262,40 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
1045
1262
  const lock = await io.rLock({ key, signal });
1046
1263
  try {
1047
1264
  // いずれかのストレージに存在すれば true を返します。
1048
- for (const storage of this.#destinations) {
1049
- if (await storage.exists(context, signal, key)) {
1050
- return true;
1265
+ // あるストレージの存在確認に失敗しても、他のストレージで存在を確認できるようにフォールバックします。
1266
+ const errors: { reason: unknown }[] = [];
1267
+ for (const { storage } of this.#destinations) {
1268
+ try {
1269
+ if (await storage.exists(vars, signal, key)) {
1270
+ return true;
1271
+ }
1272
+ } catch (ex) {
1273
+ if (signal.aborted) {
1274
+ throw ex;
1275
+ }
1276
+ errors.push({ reason: ex });
1277
+ logger.error`Failed to check existence in a storage: ${ex}`;
1051
1278
  }
1052
1279
  }
1053
1280
 
1281
+ // すべてのストレージで存在を確認できなかった場合は結果が不明のため、get() と同様にエラーとして報告します。
1282
+ if (errors.length > 0) {
1283
+ const args: KeyNotFoundErrorArgs = { key };
1284
+ switch (errors.length) {
1285
+ case 1:
1286
+ args.cause = errors[0]!.reason;
1287
+ break;
1288
+ default:
1289
+ args.cause = new PluginOperationAggregateError({
1290
+ plugin: "storage",
1291
+ action: "read",
1292
+ errors,
1293
+ });
1294
+ }
1295
+
1296
+ throw new KeyNotFoundError(args);
1297
+ }
1298
+
1054
1299
  return false;
1055
1300
  } finally {
1056
1301
  lock.release();
@@ -1061,19 +1306,12 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
1061
1306
  }
1062
1307
 
1063
1308
  /**
1064
- * 指定したオプションでキーを削除します。
1065
- *
1066
- * @param options 削除操作のオプションです。
1067
- * @returns 完了を通知する Promise です。
1309
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
1068
1310
  */
1069
1311
  public delete(options: DeleteOptions<KeyofKeyValueMapping<TKeyValueMapping>>): Promise<void>;
1070
1312
 
1071
1313
  /**
1072
- * 指定したキーを削除します。
1073
- *
1074
- * @param key 操作対象を識別するためのキーです。
1075
- * @param options 追加のオプションです。
1076
- * @returns 完了を通知する Promise です。
1314
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
1077
1315
  */
1078
1316
  public delete(
1079
1317
  key: KeyofKeyValueMapping<TKeyValueMapping>,
@@ -1086,14 +1324,17 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
1086
1324
  }
1087
1325
 
1088
1326
  const [options] = v.parseInput(DeleteArgsSchema, args);
1089
- const { key, signal: signalOption, context: contextOption } = options;
1327
+ const { key, signal: signalOption, vars: varsOption } = options;
1328
+
1329
+ // キーがキースキーマに一致するかを検証します。
1330
+ this.#resolveValueSchema(key);
1090
1331
 
1091
1332
  const { ac, io } = this.#con;
1092
1333
  const signal = combineSignals([ac.signal, signalOption]);
1093
1334
 
1094
- const context = mergeContext(this.#context, contextOption);
1095
- context["unikvs:action"] = "delete";
1096
- context["unikvs:key"] = key;
1335
+ const vars = mergeVariables(this.#vars, varsOption);
1336
+ vars["unikvs:action"] = "delete";
1337
+ vars["unikvs:key"] = key;
1097
1338
 
1098
1339
  const lock = await asyncmux(this, signal);
1099
1340
  try {
@@ -1108,10 +1349,10 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
1108
1349
  // すべての処理を並列に実行し、エラーが発生した場合は集約します。
1109
1350
  const errors: { reason: unknown }[] = [];
1110
1351
  await Promise.all(
1111
- this.#destinations.map(async (storage) => {
1352
+ this.#destinations.map(async ({ storage }) => {
1112
1353
  try {
1113
- if (await storage.exists(context, signal, key)) {
1114
- await storage.delete(context, signal, key);
1354
+ if (await storage.exists(vars, signal, key)) {
1355
+ await storage.delete(vars, signal, key);
1115
1356
  }
1116
1357
  } catch (reason) {
1117
1358
  errors.push({ reason });
@@ -1130,10 +1371,7 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
1130
1371
  }
1131
1372
 
1132
1373
  /**
1133
- * すべてのストレージ内のデータを全削除します。
1134
- *
1135
- * @param options クリア操作のオプションです。
1136
- * @returns 完了を通知する Promise です。
1374
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
1137
1375
  */
1138
1376
  public async clear(options?: ClearOptions): Promise<void>;
1139
1377
 
@@ -1143,13 +1381,13 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
1143
1381
  }
1144
1382
 
1145
1383
  const [options = {}] = v.parseInput(ClearArgsSchema, args);
1146
- const { signal: signalOption, context: contextOption } = options;
1384
+ const { signal: signalOption, vars: varsOption } = options;
1147
1385
 
1148
1386
  const { ac, io } = this.#con;
1149
1387
  const signal = combineSignals([ac.signal, signalOption]);
1150
1388
 
1151
- const context = mergeContext(this.#context, contextOption);
1152
- context["unikvs:action"] = "clear";
1389
+ const vars = mergeVariables(this.#vars, varsOption);
1390
+ vars["unikvs:action"] = "clear";
1153
1391
 
1154
1392
  const lock = await asyncmux(this, signal);
1155
1393
  try {
@@ -1164,9 +1402,9 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
1164
1402
  // すべての処理を並列に実行し、エラーが発生した場合は集約します。
1165
1403
  const errors: { reason: unknown }[] = [];
1166
1404
  await Promise.all(
1167
- this.#destinations.map(async (storage) => {
1405
+ this.#destinations.map(async ({ storage }) => {
1168
1406
  try {
1169
- await storage.clear(context, signal);
1407
+ await storage.clear(vars, signal);
1170
1408
  } catch (reason) {
1171
1409
  errors.push({ reason });
1172
1410
  }