unikvs 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +2 -2
  2. package/dist/index.d.ts +1553 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +4032 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +34 -34
  7. package/src/_contexts.ts +1089 -0
  8. package/src/_logger.ts +5 -3
  9. package/src/_middleware.ts +207 -0
  10. package/src/_pipeline-graph.helpers.ts +183 -0
  11. package/src/_pipeline-graph.ts +1307 -0
  12. package/src/_pipeline-graph.types.ts +303 -0
  13. package/src/_pipeline-leaf-resolver.ts +70 -0
  14. package/src/_plugin-lifecycle.ts +201 -0
  15. package/src/_storage.ts +169 -115
  16. package/src/_streams.schemas.ts +21 -0
  17. package/src/_streams.ts +32 -6
  18. package/src/_to-value-stream.ts +22 -11
  19. package/src/_types.ts +237 -0
  20. package/src/_unikvs-branch.ts +94 -0
  21. package/src/_unikvs.schemas.ts +451 -0
  22. package/src/_valibot.ts +17 -6
  23. package/src/_value-schemas.ts +36 -206
  24. package/src/_value-validation.ts +57 -0
  25. package/src/errors.ts +391 -148
  26. package/src/index.ts +46 -25
  27. package/src/{value-stream.types.ts → types.ts} +1 -1
  28. package/src/unikvs-branch.ts +445 -0
  29. package/src/unikvs-builder.ts +706 -0
  30. package/src/unikvs.ts +1423 -1007
  31. package/src/unikvs.types.ts +194 -0
  32. package/src/value-schemas.ts +98 -0
  33. package/dist/src/_logger.d.ts +0 -3
  34. package/dist/src/_logger.d.ts.map +0 -1
  35. package/dist/src/_logger.js +0 -3
  36. package/dist/src/_merge-variables.d.ts +0 -13
  37. package/dist/src/_merge-variables.d.ts.map +0 -1
  38. package/dist/src/_merge-variables.js +0 -12
  39. package/dist/src/_storage.d.ts +0 -18
  40. package/dist/src/_storage.d.ts.map +0 -1
  41. package/dist/src/_storage.js +0 -103
  42. package/dist/src/_streams.d.ts +0 -20
  43. package/dist/src/_streams.d.ts.map +0 -1
  44. package/dist/src/_streams.js +0 -36
  45. package/dist/src/_to-value-stream.d.ts +0 -12
  46. package/dist/src/_to-value-stream.d.ts.map +0 -1
  47. package/dist/src/_to-value-stream.js +0 -102
  48. package/dist/src/_transformer.d.ts +0 -13
  49. package/dist/src/_transformer.d.ts.map +0 -1
  50. package/dist/src/_transformer.js +0 -60
  51. package/dist/src/_valibot.d.ts +0 -15
  52. package/dist/src/_valibot.d.ts.map +0 -1
  53. package/dist/src/_valibot.js +0 -23
  54. package/dist/src/_value-schemas.d.ts +0 -179
  55. package/dist/src/_value-schemas.d.ts.map +0 -1
  56. package/dist/src/_value-schemas.js +0 -150
  57. package/dist/src/errors.d.ts +0 -306
  58. package/dist/src/errors.d.ts.map +0 -1
  59. package/dist/src/errors.js +0 -234
  60. package/dist/src/index.d.ts +0 -10
  61. package/dist/src/index.d.ts.map +0 -1
  62. package/dist/src/index.js +0 -3
  63. package/dist/src/unikvs-config.d.ts +0 -174
  64. package/dist/src/unikvs-config.d.ts.map +0 -1
  65. package/dist/src/unikvs-config.js +0 -100
  66. package/dist/src/unikvs.d.ts +0 -272
  67. package/dist/src/unikvs.d.ts.map +0 -1
  68. package/dist/src/unikvs.js +0 -928
  69. package/dist/src/utils.types.d.ts +0 -5
  70. package/dist/src/utils.types.d.ts.map +0 -1
  71. package/dist/src/utils.types.js +0 -1
  72. package/dist/src/value-stream.types.d.ts +0 -10
  73. package/dist/src/value-stream.types.d.ts.map +0 -1
  74. package/dist/src/value-stream.types.js +0 -1
  75. package/dist/src/variables.types.d.ts +0 -11
  76. package/dist/src/variables.types.d.ts.map +0 -1
  77. package/dist/src/variables.types.js +0 -1
  78. package/src/_merge-variables.ts +0 -19
  79. package/src/_transformer.ts +0 -89
  80. package/src/unikvs-config.ts +0 -401
  81. package/src/utils.types.ts +0 -4
  82. package/src/variables.types.ts +0 -13
package/src/unikvs.ts CHANGED
@@ -1,397 +1,72 @@
1
- import type { Variables, IStorage, IReadableStream } from "@unikvs/core";
2
- import { combineSignals } from "abort-signal-utils";
3
- import { type AsyncmuxLock, asyncmux, Asyncmux } from "asyncmux";
4
-
5
- import logger from "./_logger.js";
6
- import mergeVariables from "./_merge-variables.js";
7
- import UniKvsStorage from "./_storage.js";
8
- import { isReadableStream } from "./_streams.js";
1
+ import { type IMiddleware, type IReadableStream, RepairNotAllowedError } from "@unikvs/core";
2
+ import { combineSignals, throwIfAborted } from "abort-signal-utils";
3
+ import { type AsyncmuxLock, Asyncmux, asyncmux } from "asyncmux";
4
+
5
+ import {
6
+ type VariablesInit,
7
+ type VariablesEntry,
8
+ type VariablesStore,
9
+ GetContext,
10
+ HasContext,
11
+ SetContext,
12
+ OpenContext,
13
+ ClearContext,
14
+ CloseContext,
15
+ DeleteContext,
16
+ StreamContext,
17
+ createVariablesStore,
18
+ } from "./_contexts.js";
19
+ import { logger, repairLogger } from "./_logger.js";
20
+ import type UniKvsMiddleware from "./_middleware.js";
21
+ import type { PipelineGraph } from "./_pipeline-graph.js";
22
+ import type { PipelineLeaf, ResolvedLeaf, StreamEncodeTarget } from "./_pipeline-graph.types.js";
23
+ import { cancelStreams, isReadableStream } from "./_streams.js";
9
24
  import toValueStream from "./_to-value-stream.js";
10
- import UniKvsTransformer from "./_transformer.js";
11
- import * as v from "./_valibot.js";
25
+ import {
26
+ GetArgsSchema,
27
+ HasArgsSchema,
28
+ SetArgsSchema,
29
+ OpenArgsSchema,
30
+ ClearArgsSchema,
31
+ CloseArgsSchema,
32
+ DeleteArgsSchema,
33
+ StreamArgsSchema,
34
+ VariablesArgsSchema,
35
+ } from "./_unikvs.schemas.js";
36
+ import { parseInput, parseOutput } from "./_valibot.js";
37
+ import type {
38
+ ValueSchemaInfo,
39
+ $InferPlainValueData,
40
+ IValueSchemaResolver,
41
+ $InferStreamValueChunkData,
42
+ } from "./_value-schemas.js";
43
+ import { parseSetValue } from "./_value-validation.js";
12
44
  import {
13
45
  type KeyNotFoundErrorArgs,
14
46
  KeyNotFoundError,
15
47
  UniKvsIsOpenError,
16
48
  UniKvsIsNotOpenError,
49
+ MiddlewareNextNotCalledError,
50
+ MiddlewareNextNotAwaitedError,
17
51
  PluginOperationAggregateError,
52
+ MiddlewareNextCalledMultipleTimesError,
18
53
  } from "./errors.js";
19
- import UniKvsConfig, {
20
- type Value,
21
- type PlainValue,
22
- type StreamValue,
23
- type UniKvsSchema,
24
- type KeyValueMapping,
25
- type ValueSchemaInfo,
26
- type UniKvsConfigOptions,
27
- type $InferPlainValueData,
28
- type IUniKvsConfigBuilder,
29
- type IValueSchemaResolver,
30
- type KeyofKeyValueMapping,
31
- type $InferKeyValueMapping,
32
- type $InferPlainValueInput,
33
- type $InferStreamValueChunkData,
34
- } from "./unikvs-config.js";
35
- import type { ValueOf } from "./utils.types.js";
36
- import type { ValueStream } from "./value-stream.types.js";
37
- import type { VariablesSource } from "./variables.types.js";
38
-
39
- // -------------------------------------------------------------------------------------------------
40
- //
41
- // ユーティリテー
42
- //
43
- // -------------------------------------------------------------------------------------------------
44
-
45
- /**
46
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#value-types)
47
- */
48
- export type SetValue<TValue extends Value> =
49
- | $InferPlainValueInput<TValue>
50
- | (TValue extends StreamValue<any, infer TChunkInput> ? ReadableStream<TChunkInput> : never);
51
-
52
- /**
53
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#value-types)
54
- */
55
- export type KeyofKeyValueMappingHasPlainValue<TKeyValueMapping extends KeyValueMapping> = ValueOf<{
56
- [TKey in KeyofKeyValueMapping<TKeyValueMapping>]: PlainValue extends TKeyValueMapping[TKey]
57
- ? TKey
58
- : never;
59
- }>;
60
-
61
- /**
62
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#value-types)
63
- */
64
- export type KeyofKeyValueMappingHasStreamValue<TKeyValueMapping extends KeyValueMapping> = ValueOf<{
65
- [TKey in KeyofKeyValueMapping<TKeyValueMapping>]: StreamValue extends TKeyValueMapping[TKey]
66
- ? TKey
67
- : never;
68
- }>;
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
-
129
- // -------------------------------------------------------------------------------------------------
130
- //
131
- // スキーマ
132
- //
133
- // -------------------------------------------------------------------------------------------------
134
-
135
- const VariablesKeySchema = v.string();
136
-
137
- // 配列形式の vars も正当な入力であるため、record スキーマより先に判定する必要があります。
138
- // record スキーマは配列にもマッチして数値キーのオブジェクトに変換してしまうため、配列形式を先に処理しないと mergeVariables での配列としてのマージが行われません。
139
- const VariablesSourceSchema = v.union([
140
- v.array(v.tuple([VariablesKeySchema, v.unknown()])),
141
- v.record(v.any(), v.unknown()),
142
- ]);
143
-
144
- const OpenOptionsSchema = v.object({
145
- /**
146
- * 処理の中断を通知するためのシグナルです。
147
- */
148
- signal: v.optional(v.instance(AbortSignal)),
149
-
150
- /**
151
- * 実行時の変数です。
152
- */
153
- vars: v.optional(VariablesSourceSchema),
154
- });
155
-
156
- const OpenArgsSchema = v.tuple([v.optional(OpenOptionsSchema)]);
157
-
158
- const CloseOptionsSchema = v.object({
159
- /**
160
- * 処理の中断を通知するためのシグナルです。
161
- */
162
- signal: v.optional(v.instance(AbortSignal)),
163
-
164
- /**
165
- * 実行時の変数です。
166
- */
167
- vars: v.optional(VariablesSourceSchema),
168
- });
169
-
170
- const CloseArgsSchema = v.tuple([v.optional(CloseOptionsSchema)]);
171
-
172
- const SetOptionsSchema = v.object({
173
- key: v.string(),
174
- value: v.unknown(),
175
- signal: v.optional(v.instance(AbortSignal)),
176
- vars: v.optional(VariablesSourceSchema),
177
- });
178
-
179
- const SetArgsSchema = v.union([
180
- v.tuple([SetOptionsSchema]),
181
- v.pipe(
182
- v.tuple([
183
- SetOptionsSchema.entries.key,
184
- SetOptionsSchema.entries.value,
185
- v.optional(v.omit(SetOptionsSchema, ["key", "value"])),
186
- ]),
187
- v.transform(([key, value, options]) => [{ ...options, key, value }]),
188
- ),
189
- ]);
190
-
191
- const GetOptionsSchema = v.object({
192
- key: v.string(),
193
- signal: v.optional(v.instance(AbortSignal)),
194
- vars: v.optional(VariablesSourceSchema),
195
- });
196
-
197
- const GetArgsSchema = v.union([
198
- v.tuple([GetOptionsSchema]),
199
- v.pipe(
200
- v.tuple([GetOptionsSchema.entries.key, v.optional(v.omit(GetOptionsSchema, ["key"]))]),
201
- v.transform(([key, options]) => [{ ...options, key }]),
202
- ),
203
- ]);
204
-
205
- const StreamOptionsSchema = v.object({
206
- key: v.string(),
207
- signal: v.optional(v.instance(AbortSignal)),
208
- vars: v.optional(VariablesSourceSchema),
209
- });
210
-
211
- const StreamArgsSchema = v.union([
212
- v.tuple([StreamOptionsSchema]),
213
- v.pipe(
214
- v.tuple([StreamOptionsSchema.entries.key, v.optional(v.omit(StreamOptionsSchema, ["key"]))]),
215
- v.transform(([key, options]) => [{ ...options, key }]),
216
- ),
217
- ]);
218
-
219
- const HasOptionsSchema = v.object({
220
- key: v.string(),
221
- signal: v.optional(v.instance(AbortSignal)),
222
- vars: v.optional(VariablesSourceSchema),
223
- });
224
-
225
- const HasArgsSchema = v.union([
226
- v.tuple([HasOptionsSchema]),
227
- v.pipe(
228
- v.tuple([HasOptionsSchema.entries.key, v.optional(v.omit(HasOptionsSchema, ["key"]))]),
229
- v.transform(([key, options]) => [{ ...options, key }]),
230
- ),
231
- ]);
232
-
233
- const DeleteOptionsSchema = v.object({
234
- key: v.string(),
235
- signal: v.optional(v.instance(AbortSignal)),
236
- vars: v.optional(VariablesSourceSchema),
237
- });
238
-
239
- const DeleteArgsSchema = v.union([
240
- v.tuple([DeleteOptionsSchema]),
241
- v.pipe(
242
- v.tuple([DeleteOptionsSchema.entries.key, v.optional(v.omit(DeleteOptionsSchema, ["key"]))]),
243
- v.transform(([key, options]) => [{ ...options, key }]),
244
- ),
245
- ]);
246
-
247
- const ClearOptionsSchema = v.object({
248
- /**
249
- * 処理の中断を通知するためのシグナルです。
250
- */
251
- signal: v.optional(v.instance(AbortSignal)),
252
-
253
- /**
254
- * 実行時の変数です。
255
- */
256
- vars: v.optional(VariablesSourceSchema),
257
- });
258
-
259
- const ClearArgsSchema = v.tuple([v.optional(ClearOptionsSchema)]);
260
-
261
- // -------------------------------------------------------------------------------------------------
262
- //
263
- // 型定義
264
- //
265
- // -------------------------------------------------------------------------------------------------
266
-
267
- /**
268
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
269
- */
270
- export type OpenOptions = v.InferInput<typeof OpenOptionsSchema>;
271
-
272
- /**
273
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
274
- */
275
- export type CloseOptions = v.InferInput<typeof CloseOptionsSchema>;
276
-
277
- /**
278
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
279
- */
280
- export type SetOptions<
281
- TKeyValueMapping extends KeyValueMapping = KeyValueMapping,
282
- TKey extends KeyofKeyValueMapping<TKeyValueMapping> = KeyofKeyValueMapping<TKeyValueMapping>,
283
- > = {
284
- /**
285
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
286
- */
287
- readonly key: TKey;
288
-
289
- /**
290
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
291
- */
292
- readonly value: SetValue<TKeyValueMapping[TKey]>;
293
-
294
- /**
295
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
296
- */
297
- readonly signal?: AbortSignal | undefined;
298
-
299
- /**
300
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
301
- */
302
- readonly vars?: VariablesSource | undefined;
303
- };
304
-
305
- /**
306
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
307
- */
308
- export type GetOptions<TKey = IStorage.Key> = {
309
- /**
310
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
311
- */
312
- readonly key: TKey;
313
-
314
- /**
315
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
316
- */
317
- readonly signal?: AbortSignal | undefined;
318
-
319
- /**
320
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
321
- */
322
- readonly vars?: VariablesSource | undefined;
323
- };
324
-
325
- /**
326
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#value-stream)
327
- */
328
- export type StreamOptions<TKey = IStorage.Key> = {
329
- /**
330
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#value-stream)
331
- */
332
- readonly key: TKey;
333
-
334
- /**
335
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
336
- */
337
- readonly signal?: AbortSignal | undefined;
338
-
339
- /**
340
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
341
- */
342
- readonly vars?: VariablesSource | undefined;
343
- };
344
-
345
- /**
346
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
347
- */
348
- export type HasOptions<TKey = IStorage.Key> = {
349
- /**
350
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
351
- */
352
- readonly key: TKey;
353
-
354
- /**
355
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
356
- */
357
- readonly signal?: AbortSignal | undefined;
358
-
359
- /**
360
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
361
- */
362
- readonly vars?: VariablesSource | undefined;
363
- };
364
-
365
- /**
366
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
367
- */
368
- export type DeleteOptions<TKey = IStorage.Key> = {
369
- /**
370
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
371
- */
372
- readonly key: TKey;
373
-
374
- /**
375
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
376
- */
377
- readonly signal?: AbortSignal | undefined;
378
-
379
- /**
380
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#variables-and-cancellation)
381
- */
382
- readonly vars?: VariablesSource | undefined;
383
- };
384
-
385
- /**
386
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
387
- */
388
- export type ClearOptions = v.InferInput<typeof ClearOptionsSchema>;
389
-
390
- // -------------------------------------------------------------------------------------------------
391
- //
392
- // UniKvs
393
- //
394
- // -------------------------------------------------------------------------------------------------
54
+ import type { IValueStream } from "./types.js";
55
+ import type { KeyValueMapping } from "./unikvs-branch.js";
56
+ import type {
57
+ SetValue,
58
+ GetOptions,
59
+ HasOptions,
60
+ SetOptions,
61
+ OpenOptions,
62
+ ClearOptions,
63
+ CloseOptions,
64
+ DeleteOptions,
65
+ StreamOptions,
66
+ KeyofKeyValueMapping,
67
+ KeyofKeyValueMappingHasPlainValue,
68
+ KeyofKeyValueMappingHasStreamValue,
69
+ } from "./unikvs.types.js";
395
70
 
396
71
  /**
397
72
  * ストリームの読み取りロックを自動解放するための FinalizationRegistry です。
@@ -404,9 +79,9 @@ const ioLockRegistry =
404
79
  held.lock.release();
405
80
  }
406
81
 
407
- // valueStream が読み取られないまま GC されたとき、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放するために破棄します。
408
- // dispose は valueStream 自身を参照しないため、これを保持しても valueStream のガベージコレクションを妨げません。
409
- // 失敗しても対処できないので無視します。
82
+ // GC で回収された ValueStream のロックを解放し、ソースのリソースも破棄します。
83
+ // dispose は valueStream を参照しないため、GC を妨げません。失敗したときはログに残すだけにします。
84
+ // oxlint-disable-next-line promise/prefer-await-to-then
410
85
  held.dispose().catch((ex) => {
411
86
  logger.error`Failed to dispose value stream: ${ex}`;
412
87
  });
@@ -440,47 +115,29 @@ type Connection = {
440
115
  };
441
116
 
442
117
  /**
443
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#config-builder)
118
+ * プラグイン操作の失敗を集約するための 1 件分の記録です。
444
119
  */
445
- export type UniKvsDestination = {
120
+ type PluginErrorRecord = {
121
+ /**
122
+ * 失敗したプラグインの名前です。
123
+ */
124
+ readonly name: string;
125
+
446
126
  /**
447
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#config-builder)
127
+ * 宛先またはプラグインの登録順 (0 起点) です。
448
128
  */
449
- readonly storage: UniKvsStorage;
129
+ readonly index: number;
450
130
 
451
131
  /**
452
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#config-builder)
132
+ * 失敗の理由です。
453
133
  */
454
- readonly transformers: readonly UniKvsTransformer[];
134
+ readonly reason: unknown;
455
135
  };
456
136
 
457
137
  /**
458
138
  * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
459
139
  */
460
140
  export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueMapping> {
461
- /**
462
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#config-builder)
463
- */
464
- public static config<
465
- TKeyValueMapping extends KeyValueMapping,
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);
482
- }
483
-
484
141
  /**
485
142
  * 現在の接続状態です。非接続時は null となります。
486
143
  */
@@ -490,18 +147,15 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
490
147
 
491
148
  /**
492
149
  * 基本となる実行変数情報です。
150
+ *
151
+ * コンストラクターで受け取った初期値をエントリー列で保持します。操作ごとに複写して使用します。
493
152
  */
494
- readonly #vars: Readonly<Variables>;
495
-
496
- /**
497
- * データの永続化先となるストレージと前段パイプラインのリストです。
498
- */
499
- readonly #destinations: readonly [UniKvsDestination, ...UniKvsDestination[]];
153
+ #baseVars: readonly VariablesEntry[];
500
154
 
501
155
  /**
502
- * データの変換を行うトランスフォーマーのリストです。open/close の管理用に全件を保持します。
156
+ * コンパイル済みのパイプライン・グラフです。
503
157
  */
504
- readonly #transformers: readonly UniKvsTransformer[];
158
+ readonly #pipeline: PipelineGraph;
505
159
 
506
160
  /**
507
161
  * キーに対応する値のスキーマ情報を解決するリゾルバーです。スキーマ未設定時は null となります。
@@ -509,225 +163,428 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
509
163
  readonly #valueSchemaResolver: IValueSchemaResolver | null;
510
164
 
511
165
  /**
512
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
166
+ * @internal
513
167
  */
514
168
  public constructor(
515
- vars: Readonly<Variables>,
516
- destinations: readonly [UniKvsDestination, ...UniKvsDestination[]],
517
- transformers: readonly UniKvsTransformer[],
169
+ vars: readonly VariablesEntry[],
170
+ pipeline: PipelineGraph,
518
171
  valueSchemaResolver: IValueSchemaResolver | null = null,
519
172
  ) {
520
173
  this.#con = null;
521
174
  this.#acSet = new Set();
522
- this.#vars = { ...vars };
523
- this.#destinations = destinations;
524
- this.#transformers = transformers;
175
+ this.#baseVars = vars;
176
+ this.#pipeline = pipeline;
525
177
  this.#valueSchemaResolver = valueSchemaResolver;
526
178
  }
527
179
 
528
180
  /**
529
- * キーに対応する値のスキーマ情報を解決します。
530
- *
531
- * @param key 解決するキーです。
532
- * @returns 対応するスキーマ情報を返します。スキーマ未設定または対応する定義がない場合は `undefined` を返します。
181
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#base-vars-set-vars)
533
182
  */
534
- #resolveValueSchema(key: string): ValueSchemaInfo | undefined {
535
- return this.#valueSchemaResolver?.resolve(key);
536
- }
183
+ public setVars(vars: VariablesInit): this;
184
+ public setVars(...args: unknown[]): this {
185
+ const [vars] = parseInput(VariablesArgsSchema, args);
186
+ this.#baseVars = vars;
537
187
 
538
- /**
539
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
540
- */
541
- public get isOpen(): boolean {
542
- return this.#con !== null;
188
+ return this;
543
189
  }
544
190
 
545
191
  /**
546
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
192
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#base-vars-assign-vars)
547
193
  */
548
- public open(options?: OpenOptions): Promise<void>;
194
+ public assignVars(vars: VariablesInit): this;
195
+ public assignVars(...args: unknown[]): this {
196
+ const [vars] = parseInput(VariablesArgsSchema, args);
549
197
 
550
- public async open(...args: any): Promise<void> {
551
- if (this.#con !== null) {
552
- throw new UniKvsIsOpenError();
198
+ const map = new Map(this.#baseVars);
199
+
200
+ for (const [name, value] of vars) {
201
+ map.set(name, value);
553
202
  }
554
203
 
555
- const [options = {}] = v.parseInput(OpenArgsSchema, args);
556
- const { signal: signalOption, vars: varsOption } = options;
204
+ this.#baseVars = [...map];
557
205
 
558
- const ac = new AbortController();
559
- const signal = combineSignals([ac.signal, signalOption]);
206
+ return this;
207
+ }
560
208
 
561
- const vars = mergeVariables(this.#vars, varsOption);
562
- vars["unikvs:action"] = "open";
209
+ /**
210
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#base-vars-defaults-vars)
211
+ */
212
+ public defaultsVars(vars: VariablesInit): this;
213
+ public defaultsVars(...args: unknown[]): this {
214
+ const [vars] = parseInput(VariablesArgsSchema, args);
563
215
 
564
- signal.throwIfAborted();
216
+ const map = new Map(vars);
565
217
 
566
- this.#acSet.add(ac);
218
+ for (const [name, value] of this.#baseVars) {
219
+ map.set(name, value);
220
+ }
567
221
 
568
- const dispose: (() => Promise<void>)[] = [];
222
+ this.#baseVars = [...map];
569
223
 
570
- const lock = await asyncmux(this, signal);
571
- try {
572
- if (this.#con !== null) {
573
- throw new UniKvsIsOpenError();
224
+ return this;
225
+ }
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);
574
240
  }
241
+ }
575
242
 
576
- const openFns: [() => Promise<void>, plugin: "storage" | "transformer"][] = [];
243
+ return map;
244
+ }
577
245
 
578
- // 各ストレージのオープン処理をリストに追加します。
579
- for (const { storage } of this.#destinations) {
580
- openFns.push([
581
- async () => {
582
- await storage.open(vars, signal);
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
+ }
583
258
 
584
- dispose.push(async () => {
585
- try {
586
- await storage.close(vars, signal);
587
- } catch (ex) {
588
- logger.error`Failed to close storage: ${ex}`;
589
- }
590
- });
591
- },
592
- "storage",
593
- ]);
594
- }
259
+ /**
260
+ * キーに対応する値のスキーマ情報を解決します。
261
+ *
262
+ * @param key 解決するキーです。
263
+ *
264
+ * @returns 対応するスキーマ情報を返します。スキーマ未設定または対応する定義がない場合は `undefined` を返します。
265
+ */
266
+ #resolveValueSchema(key: string): ValueSchemaInfo | undefined {
267
+ return this.#valueSchemaResolver?.resolve(key);
268
+ }
595
269
 
596
- // 各トランスフォーマーのオープン処理をリストに追加します。
597
- for (const transformer of this.#transformers) {
598
- openFns.push([
599
- async () => {
600
- await transformer.open(vars, signal);
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 };
601
280
 
602
- dispose.push(async () => {
603
- try {
604
- await transformer.close(vars, signal);
605
- } catch (ex) {
606
- logger.error`Failed to close transformer: ${ex}`;
607
- }
608
- });
609
- },
610
- "transformer",
611
- ]);
281
+ switch (errors.length) {
282
+ case 0: {
283
+ break;
612
284
  }
613
285
 
614
- // すべての処理を並列に実行し、エラーが発生した場合は集約します。
615
- const errors: { plugin: "storage" | "transformer"; reason: unknown }[] = [];
616
- await Promise.all(
617
- openFns.map(async ([f, plugin]) => {
618
- try {
619
- await f();
620
- } catch (reason) {
621
- errors.push({ plugin, reason });
622
- }
623
- }),
624
- );
625
- if (errors.length > 0) {
626
- // 中断以外の失敗が混在しない場合は、abort 理由が集約エラーに埋もれないようにそのまま投げます。
627
- if (signal.aborted && errors.every((error) => error.reason === signal.reason)) {
628
- throw signal.reason;
286
+ case 1: {
287
+ const [firstError] = errors;
288
+
289
+ if (firstError !== undefined) {
290
+ args.cause = firstError.reason;
629
291
  }
630
292
 
631
- throw new PluginOperationAggregateError({ action: "open", errors });
293
+ break;
632
294
  }
633
295
 
634
- // すべてのプラグインのオープンが成功した後も、待機中に中断されていないか最終確認します。
635
- // 中断済みのシグナルを持つ接続を作成すると、isOpen が true でありながら以降のすべての操作が即座に失敗する壊れた状態になります。
636
- signal.throwIfAborted();
637
-
638
- this.#con = {
639
- ac,
640
- io: new Asyncmux(),
641
- };
642
- } catch (ex) {
643
- if (dispose.length > 0) {
644
- await Promise.all(
645
- dispose.map(async (f) => {
646
- await f();
647
- }),
648
- );
296
+ default: {
297
+ args.cause = new PluginOperationAggregateError({
298
+ action: "read",
299
+ errors,
300
+ plugin: "storage",
301
+ });
649
302
  }
650
-
651
- throw ex;
652
- } finally {
653
- this.#acSet.delete(ac);
654
- lock.release();
655
303
  }
304
+
305
+ return new KeyNotFoundError(args);
656
306
  }
657
307
 
658
- async #close(vars: Variables, signal: AbortSignal, con: Connection): Promise<void> {
659
- const lock = await asyncmux(this, signal);
308
+ public get isOpen(): boolean {
309
+ return this.#con !== null;
310
+ }
311
+
312
+ public open(options?: OpenOptions): Promise<void>;
313
+ public async open(...args: any): Promise<void> {
314
+ if (this.#con !== null) {
315
+ throw new UniKvsIsOpenError();
316
+ }
317
+
318
+ const [options = {}] = parseInput(OpenArgsSchema, args);
319
+ const { vars: varsOption, signal: signalOption } = options;
320
+
321
+ const ac = new AbortController();
322
+ const signal = combineSignals([ac.signal, signalOption]);
323
+
324
+ const operationVars = this.#createOperationVars(varsOption);
325
+ throwIfAborted(signal);
326
+ this.#acSet.add(ac);
327
+
328
+ const dispose: (() => Promise<void>)[] = [];
329
+
660
330
  try {
661
- // ロック待機中に接続状態が変更されていないか再確認します。
662
- if (this.#con !== con) {
663
- throw new UniKvsIsNotOpenError();
664
- }
331
+ using _lock = await asyncmux(this, signal);
665
332
 
666
- const { io } = this.#con;
667
- const lock = await io.lock({ signal });
668
333
  try {
669
- const closeFns: [() => Promise<void>, plugin: "storage" | "transformer"][] = [];
334
+ if (this.#con !== null) {
335
+ throw new UniKvsIsOpenError();
336
+ }
670
337
 
671
- // ストレージのクローズ処理を登録します。
672
- for (const { storage } of this.#destinations) {
673
- closeFns.push([
674
- async () => {
675
- await storage.close(vars, signal);
338
+ const openFns: [
339
+ () => Promise<void>,
340
+ plugin: "storage" | "middleware",
341
+ name: string,
342
+ index: number,
343
+ ][] = [];
344
+
345
+ // open/close 用の vars はプラグインごとに複製し、並列実行中の変更が他に見えないようにします。
346
+ for (const [index, leaf] of this.#pipeline.leaves.entries()) {
347
+ openFns.push([
348
+ async (): Promise<void> => {
349
+ const pluginVars = new Map(operationVars);
350
+ const c = new OpenContext({
351
+ vars: pluginVars.entries(),
352
+ signal,
353
+ });
354
+ await leaf.storage.open(c);
355
+ dispose.push(async () => {
356
+ try {
357
+ const cc = new CloseContext({
358
+ vars: pluginVars.entries(),
359
+ signal,
360
+ });
361
+ await leaf.storage.close(cc);
362
+ } catch (ex) {
363
+ logger.error`Failed to close storage: ${ex}`;
364
+ }
365
+ });
676
366
  },
677
367
  "storage",
368
+ leaf.name,
369
+ index,
678
370
  ]);
679
371
  }
680
372
 
681
- // トランスフォーマーのクローズ処理を登録します。
682
- for (const plugin of this.#transformers) {
683
- closeFns.push([
684
- async () => {
685
- await plugin.close(vars, signal);
373
+ // 各ミドルウェアのオープン処理をリストに追加します。
374
+ // ストレージと同様にプラグインごとに複製し、並列実行中の変更が互いに見えないようにします。
375
+ for (const [index, middleware] of this.#pipeline.middlewares.entries()) {
376
+ openFns.push([
377
+ async (): Promise<void> => {
378
+ const pluginVars = new Map(operationVars);
379
+ const c = new OpenContext({
380
+ vars: pluginVars.entries(),
381
+ signal,
382
+ });
383
+ await middleware.open(c);
384
+ dispose.push(async () => {
385
+ try {
386
+ const cc = new CloseContext({
387
+ vars: pluginVars.entries(),
388
+ signal,
389
+ });
390
+ await middleware.close(cc);
391
+ } catch (ex) {
392
+ logger.error`Failed to close middleware: ${ex}`;
393
+ }
394
+ });
686
395
  },
687
- "transformer",
396
+ "middleware",
397
+ middleware.name,
398
+ index,
688
399
  ]);
689
400
  }
690
401
 
691
402
  // すべての処理を並列に実行し、エラーが発生した場合は集約します。
692
- const errors: { plugin: "storage" | "transformer"; reason: unknown }[] = [];
403
+ const errors: {
404
+ name: string;
405
+ index: number;
406
+ plugin: "storage" | "middleware";
407
+ reason: unknown;
408
+ }[] = [];
693
409
  await Promise.all(
694
- closeFns.map(async ([f, plugin]) => {
410
+ openFns.map(async ([f, plugin, name, index]) => {
695
411
  try {
696
412
  await f();
697
- } catch (reason) {
698
- errors.push({ plugin, reason });
413
+ } catch (ex) {
414
+ errors.push({
415
+ name,
416
+ index,
417
+ plugin,
418
+ reason: ex,
419
+ });
699
420
  }
700
421
  }),
701
422
  );
423
+
702
424
  if (errors.length > 0) {
703
- throw new PluginOperationAggregateError({ action: "close", errors });
425
+ // 中断以外の失敗が混ざっていないときは、abort 理由が集約エラーに埋もれないようそのまま投げます。
426
+ if (signal.aborted && errors.every((error) => error.reason === signal.reason)) {
427
+ throw signal.reason;
428
+ }
429
+
430
+ throw new PluginOperationAggregateError({
431
+ action: "open",
432
+ errors,
433
+ });
434
+ }
435
+
436
+ // すべてのプラグインのオープンが成功した後も、待機中に中断されていないか最終確認します。
437
+ // 中断済みのシグナルを持つ接続を作ると、isOpen が true なのに以降の操作がすべて即座に失敗する不整合な状態になります。
438
+ throwIfAborted(signal);
439
+
440
+ this.#con = {
441
+ ac,
442
+ io: new Asyncmux(),
443
+ };
444
+ } catch (ex) {
445
+ if (dispose.length > 0) {
446
+ await Promise.all(
447
+ dispose.map(async (f) => {
448
+ await f();
449
+ }),
450
+ );
704
451
  }
705
- } finally {
706
- lock.release();
707
- }
708
452
 
709
- this.#con = null;
453
+ throw ex;
454
+ }
710
455
  } finally {
711
- lock.release();
456
+ this.#acSet.delete(ac);
712
457
  }
713
458
  }
714
459
 
715
- /**
716
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
717
- */
718
- public close(options?: CloseOptions): Promise<void>;
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
+ }
719
574
 
575
+ public close(options?: CloseOptions): Promise<void>;
720
576
  public close(...args: any): Promise<void> {
721
577
  try {
722
- const [options = {}] = v.parseInput(CloseArgsSchema, args);
723
- const { signal = AbortSignal.timeout(10e3), vars: varsOption } = options;
578
+ const [options = {}] = parseInput(CloseArgsSchema, args);
579
+ const { vars: varsOption, signal = AbortSignal.timeout(10e3) } = options;
724
580
 
725
- const vars = mergeVariables(this.#vars, varsOption);
726
- vars["unikvs:action"] = "close";
581
+ const operationVars = this.#createOperationVars(varsOption);
727
582
 
728
583
  if (this.#con === null) {
729
584
  const acArr = [...this.#acSet];
585
+
730
586
  this.#acSet.clear();
587
+
731
588
  for (const ac of acArr) {
732
589
  if (!ac.signal.aborted) {
733
590
  ac.abort(new UniKvsIsNotOpenError());
@@ -740,43 +597,53 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
740
597
  const con = this.#con;
741
598
  const { ac } = con;
742
599
  const acArr = [ac, ...this.#acSet];
600
+
743
601
  this.#acSet.clear();
744
- for (const ac of acArr) {
745
- if (!ac.signal.aborted) {
746
- ac.abort(new UniKvsIsNotOpenError());
602
+
603
+ for (const targetAc of acArr) {
604
+ if (!targetAc.signal.aborted) {
605
+ targetAc.abort(new UniKvsIsNotOpenError());
747
606
  }
748
607
  }
749
608
 
750
- return this.#close(vars, signal, con).catch(async (ex) => {
751
- // #close が失敗した場合、コネクションの AbortController はすでに abort 済みであり、以降の操作がすべて即座に失敗する壊れた状態になります。
752
- // そこで接続を破棄して isOpen=false の一貫した状態にし、ベストエフォートでプラグインのクローズ処理を実行します。
609
+ const progress = { pluginsStarted: false };
610
+
611
+ return this.#close(operationVars, signal, con, progress).catch(async (closeError) => {
612
+ // #close が失敗した場合、コネクションの AbortController はすでに abort 済みで、以降の操作がすべて即座に失敗する不整合な状態になります。
613
+ // そこで接続を破棄して isOpen=false の一貫した状態に戻します。
753
614
  if (this.#con === con) {
754
615
  this.#con = null;
755
616
 
756
- const disposeSignal = AbortSignal.timeout(10e3);
757
- await Promise.all(
758
- [...this.#destinations.map((dest) => dest.storage), ...this.#transformers].map(
759
- async (plugin) => {
617
+ // プラグインの close 段階まで到達していれば、成功分の参照は解放済みで、失敗分は #close が再試行済みです。
618
+ // ここで全プラグインを再度閉じると参照を二重解放してしまうため、close 段階に到達できなかったときだけ破棄します。
619
+ if (!progress.pluginsStarted) {
620
+ const disposeSignal = AbortSignal.timeout(10e3);
621
+ await Promise.all(
622
+ [
623
+ ...this.#pipeline.leaves.map((leaf) => leaf.storage),
624
+ ...this.#pipeline.middlewares,
625
+ ].map(async (plugin) => {
760
626
  try {
761
- await plugin.close(vars, disposeSignal);
762
- } catch (reason) {
763
- logger.error`Failed to close plugin after close failure: ${reason}`;
627
+ const c = new CloseContext({
628
+ vars: new Map(operationVars).entries(),
629
+ signal: disposeSignal,
630
+ });
631
+ await plugin.close(c);
632
+ } catch (disposeError) {
633
+ logger.error`Failed to close plugin after close failure: ${disposeError}`;
764
634
  }
765
- },
766
- ),
767
- );
635
+ }),
636
+ );
637
+ }
768
638
  }
769
639
 
770
- throw ex;
640
+ throw closeError;
771
641
  });
772
642
  } catch (ex) {
773
643
  return Promise.reject(ex);
774
644
  }
775
645
  }
776
646
 
777
- /**
778
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
779
- */
780
647
  async [Symbol.asyncDispose](): Promise<void> {
781
648
  if (!this.isOpen) {
782
649
  return;
@@ -788,417 +655,933 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
788
655
  }
789
656
 
790
657
  /**
791
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
658
+ * 書き込み失敗の集約と、成功した宛先への通知を行います。
659
+ *
660
+ * @param targets 書き込みを試みた対象です。
661
+ * @param failed 失敗した葉の集合です。
662
+ * @param errors 集約する失敗です。
663
+ * @param signal 中断シグナルです。
792
664
  */
793
- public set<const TKey extends KeyofKeyValueMapping<TKeyValueMapping>>(
794
- options: SetOptions<TKeyValueMapping, TKey>,
795
- ): Promise<void>;
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;
677
+ }
678
+
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
+ }
691
+
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
+ );
704
+
705
+ if (notifyErrors.length > 0) {
706
+ logger.error(new AggregateError(notifyErrors, "Failed to handle error"));
707
+ }
708
+
709
+ throw error;
710
+ }
796
711
 
797
712
  /**
798
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
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` コンテキストです。
799
722
  */
723
+ async #use(
724
+ action: "read" | "write" | "delete" | "clear",
725
+ c: IMiddleware.UseContext,
726
+ ): Promise<void> {
727
+ throwIfAborted(c.signal);
728
+
729
+ const middlewares = this.#pipeline.middlewares;
730
+ const dispatch = async (index: number): Promise<void> => {
731
+ const maybeMiddleware = middlewares[index];
732
+
733
+ if (maybeMiddleware === undefined) {
734
+ return;
735
+ }
736
+
737
+ const middleware: UniKvsMiddleware = maybeMiddleware;
738
+
739
+ let nextCalled = false;
740
+ let nextSettled = false;
741
+ let downstreamError: unknown;
742
+ let hasDownstreamError = false;
743
+
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(() => {});
756
+
757
+ return rejection;
758
+ }
759
+
760
+ nextCalled = true;
761
+
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;
772
+ }
773
+ });
774
+ // 呼び出し側が切り離した場合の未処理 rejection を防ぎます。await したときは従来どおり拒否されます。
775
+ // oxlint-disable-next-line promise/prefer-await-to-then
776
+ promise.catch(() => {});
777
+
778
+ return promise;
779
+ }
780
+
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
+ }
790
+
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
+ }
802
+
803
+ // 規約違反はプログラミングエラーであり、集約せずに投げます。
804
+ if (
805
+ ex instanceof MiddlewareNextNotCalledError ||
806
+ ex instanceof MiddlewareNextNotAwaitedError ||
807
+ ex instanceof MiddlewareNextCalledMultipleTimesError
808
+ ) {
809
+ throw ex;
810
+ }
811
+
812
+ // `next()` から伝播した下流の失敗は再集約せず、そのまま投げます。
813
+ if (hasDownstreamError && ex === downstreamError) {
814
+ throw ex;
815
+ }
816
+
817
+ throw new PluginOperationAggregateError({
818
+ action,
819
+ errors: [
820
+ {
821
+ name: middleware.name,
822
+ index,
823
+ reason: ex,
824
+ },
825
+ ],
826
+ plugin: "middleware",
827
+ });
828
+ }
829
+ };
830
+ await dispatch(0);
831
+ }
832
+
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;
903
+
904
+ try {
905
+ tree = await this.#pipeline.buildEncodeTree(
906
+ {
907
+ key,
908
+ store,
909
+ signal,
910
+ },
911
+ value,
912
+ streams,
913
+ );
914
+ } catch (ex) {
915
+ await cancelStreams(streams, ex);
916
+ throw ex;
917
+ }
918
+
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
+ }
950
+
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);
961
+ }
962
+
963
+ public set<const TKey extends KeyofKeyValueMapping<TKeyValueMapping>>(
964
+ options: SetOptions<TKeyValueMapping, TKey>,
965
+ ): Promise<void>;
800
966
  public set<const TKey extends KeyofKeyValueMapping<TKeyValueMapping>>(
801
967
  key: TKey,
802
968
  value: SetValue<TKeyValueMapping[TKey]>,
803
969
  options?: Omit<SetOptions, "key" | "value">,
804
970
  ): Promise<void>;
805
-
806
971
  public async set(...args: any): Promise<void> {
807
972
  if (this.#con === null) {
808
973
  throw new UniKvsIsNotOpenError();
809
974
  }
810
975
 
811
- const [options] = v.parseInput(SetArgsSchema, args);
812
- const { key, signal: signalOption, vars: varsOption } = options;
976
+ const [options] = parseInput(SetArgsSchema, args);
977
+ const { key, vars: varsOption, signal: signalOption } = options;
813
978
 
814
979
  const valueSchema = this.#resolveValueSchema(key);
980
+ // スキーマ検証はロック取得前に済ませ、無効な入力ではロックを取得しません。
815
981
  const value = valueSchema ? parseSetValue(valueSchema, options.value) : options.value;
816
982
 
817
983
  const { ac, io } = this.#con;
818
984
  const signal = combineSignals([ac.signal, signalOption]);
819
985
 
820
- const vars = mergeVariables(this.#vars, varsOption);
821
- vars["unikvs:action"] = "set";
822
- vars["unikvs:key"] = key;
986
+ const store = this.#createOperationStore(varsOption);
987
+
988
+ // `#setStream` へ所有権を渡す前の失敗では、ソースをロックしたままにしないよう派生ストリームをキャンセルします。
989
+ const stream = isReadableStream(value) ? value : null;
990
+ let ownsStream = stream !== null;
823
991
 
824
- const lock = await asyncmux(this, signal);
825
992
  try {
993
+ using _lock = await asyncmux.readonly(this, signal);
826
994
  // ロック待機中に接続状態が変更されていないか再確認します。
827
995
  if (this.#con === null) {
828
996
  throw new UniKvsIsNotOpenError();
829
997
  }
830
998
 
831
- const errors: { reason: unknown }[] = [];
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;
882
- }
883
-
884
- const lock = await io.lock({ key, signal });
885
- try {
886
- await Promise.all(
887
- this.#destinations.map(async (dest, i) => {
888
- // tee ブランチはキャンセルされるまで健康なブランチの読み取り分のチャンクを保持し続けるため、失敗時に放棄したブランチを確実にキャンセルできるよう参照を保持します。
889
- const branch = branches[i]!;
890
- try {
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
- // キャンセルに失敗してもエラー集約を優先します。
898
- }
899
-
900
- errors.push({ reason });
901
- errorStorageSet.add(dest);
902
- }
903
- }),
904
- );
905
- } finally {
906
- lock.release();
907
- }
999
+ // 書き込み前の `use` による検証を行います。変異は後続のエンコード・書き込みに引き継がれます。
1000
+ const useContext = new SetContext({
1001
+ key,
1002
+ store,
1003
+ signal,
1004
+ });
1005
+ await this.#use("write", useContext);
1006
+ // `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
1007
+ throwIfAborted(signal);
1008
+ // `use` による変異を操作変数に反映します。
1009
+ // ロックはインスタンス (読み取り) からキーの順で取得し、逆順で解放します。
1010
+ if (stream !== null) {
1011
+ // これ以降の後始末は `#setStream` が所有します。
1012
+ ownsStream = false;
1013
+ await this.#setStream(io, signal, key, store, stream);
908
1014
  } else {
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]));
919
- }
920
- const encoded = this.#destinations.map((dest) => prefix[dest.transformers.length]);
921
-
922
- const lock = await io.lock({ key, signal });
1015
+ await this.#setPlain(io, signal, key, store, value);
1016
+ }
1017
+ } catch (ex) {
1018
+ if (ownsStream && stream !== null) {
1019
+ // 消費されないストリームをキャンセルし、ソースの解放を伝播させます。
923
1020
  try {
924
- await Promise.all(
925
- this.#destinations.map(async (dest, i) => {
926
- try {
927
- await dest.storage.write(vars, signal, key, encoded[i]);
928
- } catch (reason) {
929
- errors.push({ reason });
930
- errorStorageSet.add(dest);
931
- }
932
- }),
933
- );
934
- } finally {
935
- lock.release();
1021
+ await stream.cancel(ex);
1022
+ } catch {
1023
+ // キャンセルに失敗しても元の例外の伝播を優先します。
936
1024
  }
937
1025
  }
938
1026
 
939
- // エラーが発生した場合は集約します。
940
- if (errors.length > 0) {
941
- const error = new PluginOperationAggregateError({
942
- plugin: "storage",
943
- action: "write",
944
- errors,
945
- });
946
- {
947
- const errors: unknown[] = [];
948
- await Promise.all(
949
- this.#destinations.map(async (dest) => {
950
- if (errorStorageSet.has(dest)) {
951
- return;
952
- }
1027
+ throw ex;
1028
+ }
1029
+ }
953
1030
 
954
- try {
955
- await dest.storage.onOtherWriteError(vars, signal, key, error);
956
- } catch (ex) {
957
- errors.push(ex);
958
- }
959
- }),
960
- );
961
- if (errors.length > 0) {
962
- logger.error(new AggregateError(errors, "Failed to handle error"));
963
- }
964
- }
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
+ );
965
1080
 
966
- throw error;
967
- }
968
- } finally {
969
- lock.release();
970
- }
1081
+ throwIfAborted(signal);
1082
+
1083
+ return raw;
971
1084
  }
972
1085
 
973
- /**
974
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
975
- */
976
1086
  public get<const TKey extends KeyofKeyValueMappingHasPlainValue<TKeyValueMapping>>(
977
1087
  options: GetOptions<TKey>,
978
1088
  ): Promise<$InferPlainValueData<TKeyValueMapping[TKey]>>;
979
-
980
- /**
981
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
982
- */
983
1089
  public get<const TKey extends KeyofKeyValueMappingHasPlainValue<TKeyValueMapping>>(
984
1090
  key: TKey,
985
1091
  options?: Omit<GetOptions, "key">,
986
1092
  ): Promise<$InferPlainValueData<TKeyValueMapping[TKey]>>;
987
-
988
1093
  public async get(...args: any): Promise<unknown> {
989
1094
  if (this.#con === null) {
990
1095
  throw new UniKvsIsNotOpenError();
991
1096
  }
992
1097
 
993
- const [options] = v.parseInput(GetArgsSchema, args);
994
- const { key, signal: signalOption, vars: varsOption } = options;
1098
+ const [options] = parseInput(GetArgsSchema, args);
1099
+ const {
1100
+ key,
1101
+ vars: varsOption,
1102
+ repair = false,
1103
+ signal: signalOption,
1104
+ throwOnRepairDenied = false,
1105
+ } = options;
995
1106
 
996
1107
  const valueSchema = this.#resolveValueSchema(key);
997
1108
 
998
1109
  const { ac, io } = this.#con;
999
1110
  const signal = combineSignals([ac.signal, signalOption]);
1000
1111
 
1001
- const vars = mergeVariables(this.#vars, varsOption);
1002
- vars["unikvs:action"] = "get";
1003
- vars["unikvs:key"] = key;
1112
+ const store = this.#createOperationStore(varsOption);
1004
1113
 
1005
- const lock = await asyncmux.readonly(this, signal);
1006
- try {
1007
- // ロック待機中に接続状態が変更されていないか再確認します。
1008
- if (this.#con === null) {
1009
- throw new UniKvsIsNotOpenError();
1010
- }
1114
+ using _lock = await asyncmux.readonly(this, signal);
1115
+ // ロック待機中に接続状態が変更されていないか再確認します。
1116
+ if (this.#con === null) {
1117
+ throw new UniKvsIsNotOpenError();
1118
+ }
1011
1119
 
1012
- const NONE = {};
1013
- let data: any = NONE;
1014
- let transformers: readonly UniKvsTransformer[] = [];
1015
- const errors: { reason: unknown }[] = [];
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
+ });
1016
1161
 
1017
- const lock = await io.rLock({ key, signal });
1018
- try {
1019
- // 各ストレージを巡回し、最初に見つかったデータを取得します。
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;
1028
- break;
1029
- } catch (ex) {
1030
- if (signal.aborted) {
1031
- throw ex;
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[]],
1032
1173
  }
1033
- errors.push({ reason: ex });
1034
- logger.error`Failed to read from a storage: ${ex}`;
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;
1035
1189
  }
1036
- }
1037
- } finally {
1038
- lock.release();
1039
- }
1040
1190
 
1041
- if (data === NONE) {
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
- }
1191
+ data = await leaf.storage.read(readContext);
1192
+ hit = resolved;
1193
+ break;
1194
+ } catch (ex) {
1195
+ if (signal.aborted) {
1196
+ throw ex;
1197
+ }
1056
1198
 
1057
- throw new KeyNotFoundError(args);
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
+ }
1058
1206
  }
1059
1207
 
1060
- // 見つかったストレージ専用の前段パイプラインを逆順に適用してデータをデコードします。
1061
- for (const transformer of transformers.toReversed()) {
1062
- data = await transformer.decode(vars, signal, data);
1208
+ if (hit === null) {
1209
+ throw this.#notFound(key, errors);
1063
1210
  }
1064
1211
 
1065
- // デコード後のデータを検証します。
1066
- if (valueSchema) {
1067
- data = v.parseOutput(valueSchema.schema, data);
1212
+ if (repair) {
1213
+ // 後段で見つかったデータをすべてデコードして原データに戻します。
1214
+ data = await this.#repairPlain(data, hit, missing, store, key, signal, throwOnRepairDenied);
1068
1215
  }
1216
+ }
1069
1217
 
1070
- return data;
1071
- } finally {
1072
- lock.release();
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);
1073
1234
  }
1235
+
1236
+ return data;
1074
1237
  }
1075
1238
 
1076
- /**
1077
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#value-stream)
1078
- */
1079
1239
  public stream<const TKey extends KeyofKeyValueMappingHasStreamValue<TKeyValueMapping>>(
1080
1240
  options: StreamOptions<TKey>,
1081
- ): Promise<ValueStream<$InferStreamValueChunkData<TKeyValueMapping[TKey]>>>;
1082
-
1083
- /**
1084
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#value-stream)
1085
- */
1241
+ ): Promise<IValueStream<$InferStreamValueChunkData<TKeyValueMapping[TKey]>>>;
1086
1242
  public stream<const TKey extends KeyofKeyValueMappingHasStreamValue<TKeyValueMapping>>(
1087
1243
  key: TKey,
1088
1244
  options?: Omit<StreamOptions, "key">,
1089
- ): Promise<ValueStream<$InferStreamValueChunkData<TKeyValueMapping[TKey]>>>;
1090
-
1091
- public async stream(...args: any): Promise<ValueStream> {
1245
+ ): Promise<IValueStream<$InferStreamValueChunkData<TKeyValueMapping[TKey]>>>;
1246
+ public async stream(...args: any): Promise<IValueStream> {
1092
1247
  if (this.#con === null) {
1093
1248
  throw new UniKvsIsNotOpenError();
1094
1249
  }
1095
1250
 
1096
- const [options] = v.parseInput(StreamArgsSchema, args);
1097
- const { key, signal: signalOption, vars: varsOption } = options;
1251
+ const [options] = parseInput(StreamArgsSchema, args);
1252
+ const {
1253
+ key,
1254
+ vars: varsOption,
1255
+ repair = false,
1256
+ signal: signalOption,
1257
+ throwOnRepairDenied = false,
1258
+ } = options;
1098
1259
 
1099
1260
  const valueSchema = this.#resolveValueSchema(key);
1100
1261
 
1101
1262
  const { ac, io } = this.#con;
1102
1263
  const signal = combineSignals([ac.signal, signalOption]);
1103
1264
 
1104
- const vars = mergeVariables(this.#vars, varsOption);
1105
- vars["unikvs:action"] = "stream";
1106
- vars["unikvs:key"] = key;
1265
+ const store = this.#createOperationStore(varsOption);
1107
1266
 
1108
- const lock = await asyncmux.readonly(this, signal);
1109
- try {
1267
+ {
1268
+ using _lock = await asyncmux.readonly(this, signal);
1110
1269
  // ロック待機中に接続状態が変更されていないか再確認します。
1111
1270
  if (this.#con === null) {
1112
1271
  throw new UniKvsIsNotOpenError();
1113
1272
  }
1114
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 の呼び出し前を表す番兵値です。
1115
1294
  const NONE: any = {};
1116
1295
  let r: IReadableStream = NONE;
1117
- let transformers: readonly UniKvsTransformer[] = [];
1118
- const errors: { reason: unknown }[] = [];
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
+ }));
1119
1315
 
1120
- const lock = await io.rLock({ key, signal });
1121
1316
  try {
1122
- // 各ストレージを巡回し、最初に見つかったデータを取得します。
1123
- // あるストレージの読み取りに失敗しても、他のストレージからデータを取得できるようにフォールバックします。
1124
- for (const dest of this.#destinations) {
1317
+ // 登録順が読み取りの優先順です。先頭で見つかればそこで終え、失敗したときは次の宛先を試します。
1318
+ const resolver = this.#pipeline.createResolver({
1319
+ key,
1320
+ store,
1321
+ signal,
1322
+ });
1323
+
1324
+ for (const leaf of this.#pipeline) {
1325
+ const resolved = resolver.resolve(leaf);
1326
+ const existsContext = new HasContext({
1327
+ key: resolved.key,
1328
+ store,
1329
+ signal,
1330
+ });
1331
+ const readableRepair = repair
1332
+ ? {
1333
+ enabled: true as const,
1334
+ targets: leafNames as [string, ...string[]],
1335
+ }
1336
+ : {
1337
+ enabled: false as const,
1338
+ targets: [] as const,
1339
+ };
1340
+ const readableContext = new StreamContext({
1341
+ key: resolved.key,
1342
+ store,
1343
+ repair: readableRepair,
1344
+ signal,
1345
+ });
1346
+
1125
1347
  try {
1126
- if (!(await dest.storage.exists(vars, signal, key))) {
1348
+ if (!(await leaf.storage.exists(existsContext))) {
1349
+ missing.push(leaf);
1127
1350
  continue;
1128
1351
  }
1129
1352
 
1130
- r = await dest.storage.getReadable(vars, signal, key);
1131
- transformers = dest.transformers;
1353
+ r = await leaf.storage.getReadable(readableContext);
1354
+ hit = resolved;
1132
1355
  break;
1133
1356
  } catch (ex) {
1134
1357
  if (signal.aborted) {
1135
1358
  throw ex;
1136
1359
  }
1137
1360
 
1138
- errors.push({ reason: ex });
1361
+ errors.push({
1362
+ name: leaf.name,
1363
+ index: leaf.index,
1364
+ reason: ex,
1365
+ });
1139
1366
  logger.error`Failed to read from a storage: ${ex}`;
1140
1367
  }
1141
1368
  }
1142
1369
 
1143
- if (r === NONE) {
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
- });
1370
+ if (hit === null) {
1371
+ throw this.#notFound(key, errors);
1372
+ }
1373
+
1374
+ // 書き戻しが有効なときだけ、ストリームが途中で破棄されたら進行中の書き戻しも中断できるようにします。
1375
+ repairAc = repair ? new AbortController() : null;
1376
+ const repairSignal = repairAc ? combineSignals([signal, repairAc.signal]) : signal;
1377
+
1378
+ // 書き戻しはできる範囲で行います。失敗してもログに残すだけで、読み取りは続けます。
1379
+ const fill = (target: StreamEncodeTarget): void => {
1380
+ fillBranches.push(target.stream);
1381
+ fillTasks.push(
1382
+ (async (): Promise<void> => {
1383
+ try {
1384
+ const wc = new SetContext({
1385
+ key: target.key,
1386
+ store,
1387
+ signal: repairSignal,
1388
+ });
1389
+ const w = await target.leaf.storage.getWritable(wc);
1390
+ await target.stream.pipeTo(w, { signal: repairSignal });
1391
+ } catch (ex) {
1392
+ // パイプが中断されたときは分岐を解放し、tee が後続のチャンクを保持し続けないようにします。
1393
+ try {
1394
+ await target.stream.cancel(ex);
1395
+ } catch {
1396
+ // 解放に失敗しても書き戻しの失敗処理を優先します。
1397
+ }
1398
+
1399
+ // 意図的な中断時はエラーではありません。
1400
+ if (repairSignal.aborted) {
1401
+ return;
1402
+ }
1403
+
1404
+ if (ex instanceof RepairNotAllowedError) {
1405
+ repairLogger.warn`Failed to repair a storage: ${ex}`;
1406
+
1407
+ return;
1408
+ }
1409
+
1410
+ logger.error`Failed to repair a storage: ${ex}`;
1411
+ }
1412
+ })(),
1413
+ );
1414
+ };
1415
+
1416
+ // セットアップに失敗したときは、構築済みの分岐と進行中の書き戻しを中断します。
1417
+ cancelFills = async (reason: unknown): Promise<void> => {
1418
+ repairAc?.abort(reason);
1419
+ await Promise.all(
1420
+ fillBranches.map(async (branch) => {
1421
+ try {
1422
+ await branch.cancel(reason);
1423
+ } catch {
1424
+ // 解放に失敗しても残りの分岐の解放を優先します。
1425
+ }
1426
+ }),
1427
+ );
1428
+ await Promise.all(fillTasks);
1429
+ };
1430
+
1431
+ // 読み取り元をすべてデコードして原データに戻します。
1432
+ // 書き戻しの分岐は読み取りパスと分離します。
1433
+ r = await this.#pipeline.decodeStream(
1434
+ hit.leaf,
1435
+ {
1436
+ key,
1437
+ store,
1438
+ signal,
1439
+ },
1440
+ r,
1441
+ );
1442
+
1443
+ if (repair) {
1444
+ // 許可のない宛先を先に除外します。throwOnRepairDenied が真のときはここでエラーを確定します。
1445
+ const repairSet = new Set<PipelineLeaf>();
1446
+
1447
+ for (const leaf of missing) {
1448
+ if (!leaf.canRepair) {
1449
+ // 意図的な中断時はエラーではありません。
1450
+ if (signal.aborted) {
1451
+ continue;
1452
+ }
1453
+
1454
+ repairLogger.warn`Skipped repair for a storage without AllowRepair: ${leaf.name}`;
1455
+
1456
+ if (throwOnRepairDenied) {
1457
+ throw new RepairNotAllowedError({ names: [leaf.name] });
1458
+ }
1459
+
1460
+ continue;
1461
+ }
1462
+
1463
+ repairSet.add(leaf);
1157
1464
  }
1158
1465
 
1159
- throw new KeyNotFoundError(args);
1160
- }
1466
+ if (repairSet.size > 0) {
1467
+ // 読み取りパスを変更しないよう、書き戻し側だけを構築して分岐後の読み取り側を返します。
1468
+ const streams = new Set<IReadableStream>([r]);
1469
+ const tree = await this.#pipeline.buildEncodeTree(
1470
+ {
1471
+ key,
1472
+ store,
1473
+ signal,
1474
+ },
1475
+ r,
1476
+ streams,
1477
+ {
1478
+ isActive: (leaf: PipelineLeaf) => repairSet.has(leaf),
1479
+ splitRead: true,
1480
+ onNodeError: (ex: unknown, activeLeaves: readonly PipelineLeaf[]) => {
1481
+ // 意図的な中断時は中断理由で失敗します。
1482
+ throwIfAborted(signal);
1483
+
1484
+ if (ex instanceof RepairNotAllowedError) {
1485
+ repairLogger.warn`Failed to repair a storage: ${ex}`;
1486
+
1487
+ if (throwOnRepairDenied) {
1488
+ throw new RepairNotAllowedError({
1489
+ cause: ex,
1490
+ names: activeLeaves.map((leaf) => leaf.name),
1491
+ });
1492
+ }
1493
+
1494
+ return true;
1495
+ }
1496
+
1497
+ logger.error`Failed to repair a storage: ${ex}`;
1498
+
1499
+ throw ex;
1500
+ },
1501
+ },
1502
+ );
1503
+ r = tree.readStream;
1161
1504
 
1162
- // 見つかったストレージ専用の前段パイプラインを逆順に適用し、デコード用トランスフォームを連結します。
1163
- for (const transformer of transformers.toReversed()) {
1164
- const d = await transformer.getDecodable(vars, signal);
1165
- r = r.pipeThrough(d);
1505
+ for (const target of tree.targets) {
1506
+ fill(target);
1507
+ }
1508
+ }
1509
+
1510
+ throwIfAborted(signal);
1166
1511
  }
1167
1512
 
1168
1513
  // デコード後のチャンクを検証します。
1169
1514
  if (valueSchema) {
1170
1515
  r = r.pipeThrough(
1171
1516
  new TransformStream({
1172
- transform(chunk, controller) {
1173
- controller.enqueue(v.parseOutput(valueSchema.schema, chunk));
1517
+ transform(chunk, controller): void {
1518
+ controller.enqueue(parseOutput(valueSchema.schema, chunk));
1174
1519
  },
1175
1520
  }),
1176
1521
  );
1177
1522
  }
1178
1523
 
1179
- if (!ioLockRegistry) {
1180
- return toValueStream(r, async () => {
1524
+ // ストリームを読み切ったかを監視します。読み切ったときは書き戻しの完了を待ち、途中で破棄されたときは書き戻しを中断します。
1525
+ let completed = false;
1526
+
1527
+ if (repairAc) {
1528
+ r = r.pipeThrough(
1529
+ new TransformStream({
1530
+ flush(): void {
1531
+ completed = true;
1532
+ },
1533
+ transform(chunk, controller): void {
1534
+ controller.enqueue(chunk);
1535
+ },
1536
+ }),
1537
+ );
1538
+ }
1539
+
1540
+ // 破棄時は、ソースストリームをキャンセルする前に進行中の書き戻しを中断します。
1541
+ // tee で分岐したストリームはすべての分岐のキャンセルが揃うまで元のキャンセルが完了しないためです。
1542
+ const beforeDispose = (): void => {
1543
+ if (!completed) {
1544
+ repairAc?.abort(signal.reason);
1545
+ }
1546
+ };
1547
+
1548
+ // キーロックは ValueStream の dispose まで保持します。fill の完了を待ってから解放します。
1549
+ const dispose = async (): Promise<void> => {
1550
+ try {
1551
+ await Promise.all(fillTasks);
1552
+ } finally {
1181
1553
  try {
1182
- lock.release();
1183
- } catch {}
1184
- });
1554
+ ioLock.release();
1555
+ } catch {
1556
+ // 解放済みでも dispose を失敗させません。
1557
+ }
1558
+ }
1559
+ };
1560
+
1561
+ if (!ioLockRegistry) {
1562
+ return toValueStream(r, dispose, beforeDispose);
1185
1563
  }
1186
1564
 
1187
1565
  const unregisterToken = {};
1188
- const valueStream = toValueStream(r, async () => {
1189
- try {
1190
- ioLockRegistry.unregister(unregisterToken);
1191
- } catch {}
1192
- try {
1193
- lock.release();
1194
- } catch {}
1195
- });
1566
+ const valueStream = toValueStream(
1567
+ r,
1568
+ async () => {
1569
+ try {
1570
+ ioLockRegistry.unregister(unregisterToken);
1571
+ } catch {
1572
+ // 未登録でも dispose を失敗させません。
1573
+ }
1574
+
1575
+ await dispose();
1576
+ },
1577
+ beforeDispose,
1578
+ );
1196
1579
 
1197
- // valueStream が GC されるタイミングでストリームが終了していなければロックを自動解放するとともに、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放できるように dispose を記録します。
1580
+ // valueStream が GC される時点でストリームが終わっていなければロックを自動解放します。あわせて、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放できるよう dispose を記録します。
1198
1581
  ioLockRegistry.register(
1199
1582
  valueStream,
1200
1583
  {
1201
- lock,
1584
+ lock: ioLock,
1202
1585
  dispose: valueStream.dispose,
1203
1586
  },
1204
1587
  unregisterToken,
@@ -1206,41 +1589,36 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
1206
1589
 
1207
1590
  return valueStream;
1208
1591
  } catch (ex) {
1592
+ await cancelFills?.(ex);
1593
+
1209
1594
  if (r !== NONE) {
1210
- // getReadable 成功後のセットアップ中に失敗した場合、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放するためにキャンセルします。
1595
+ // getReadable の成功後にセットアップで失敗したときは、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放するためキャンセルします。
1211
1596
  try {
1212
1597
  await r.cancel(ex);
1213
- } catch {}
1598
+ } catch {
1599
+ // キャンセルに失敗しても元の例外の伝播を優先します。
1600
+ }
1214
1601
  }
1215
1602
 
1216
- lock.release();
1603
+ ioLock.release();
1604
+
1217
1605
  throw ex;
1218
1606
  }
1219
- } finally {
1220
- lock.release();
1221
1607
  }
1222
1608
  }
1223
1609
 
1224
- /**
1225
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
1226
- */
1227
1610
  public has(options: HasOptions<KeyofKeyValueMapping<TKeyValueMapping>>): Promise<boolean>;
1228
-
1229
- /**
1230
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
1231
- */
1232
1611
  public has(
1233
1612
  key: KeyofKeyValueMapping<TKeyValueMapping>,
1234
1613
  options?: Omit<HasOptions, "key">,
1235
1614
  ): Promise<boolean>;
1236
-
1237
1615
  public async has(...args: any): Promise<boolean> {
1238
1616
  if (this.#con === null) {
1239
1617
  throw new UniKvsIsNotOpenError();
1240
1618
  }
1241
1619
 
1242
- const [options] = v.parseInput(HasArgsSchema, args);
1243
- const { key, signal: signalOption, vars: varsOption } = options;
1620
+ const [options] = parseInput(HasArgsSchema, args);
1621
+ const { key, vars: varsOption, signal: signalOption } = options;
1244
1622
 
1245
1623
  // キーがキースキーマに一致するかを検証します。
1246
1624
  this.#resolveValueSchema(key);
@@ -1248,83 +1626,86 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
1248
1626
  const { ac, io } = this.#con;
1249
1627
  const signal = combineSignals([ac.signal, signalOption]);
1250
1628
 
1251
- const vars = mergeVariables(this.#vars, varsOption);
1252
- vars["unikvs:action"] = "has";
1253
- vars["unikvs:key"] = key;
1629
+ const store = this.#createOperationStore(varsOption);
1254
1630
 
1255
- const lock = await asyncmux.readonly(this, signal);
1256
- try {
1257
- // ロック待機中に接続状態が変更されていないか再確認します。
1258
- if (this.#con === null) {
1259
- throw new UniKvsIsNotOpenError();
1260
- }
1631
+ using _lock = await asyncmux.readonly(this, signal);
1632
+ // ロック待機中に接続状態が変更されていないか再確認します。
1633
+ if (this.#con === null) {
1634
+ throw new UniKvsIsNotOpenError();
1635
+ }
1261
1636
 
1262
- const lock = await io.rLock({ key, signal });
1263
- try {
1264
- // いずれかのストレージに存在すれば 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}`;
1278
- }
1279
- }
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);
1280
1647
 
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
- }
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
+ });
1660
+
1661
+ for (const leaf of this.#pipeline) {
1662
+ const resolved = resolver.resolve(leaf);
1663
+
1664
+ try {
1665
+ const c = new HasContext({
1666
+ key: resolved.key,
1667
+ store,
1668
+ signal,
1669
+ });
1295
1670
 
1296
- throw new KeyNotFoundError(args);
1671
+ if (await leaf.storage.exists(c)) {
1672
+ return true;
1673
+ }
1674
+ } catch (ex) {
1675
+ if (signal.aborted) {
1676
+ throw ex;
1297
1677
  }
1298
1678
 
1299
- return false;
1300
- } finally {
1301
- lock.release();
1679
+ errors.push({
1680
+ name: leaf.name,
1681
+ index: leaf.index,
1682
+ reason: ex,
1683
+ });
1684
+
1685
+ logger.error`Failed to check existence in a storage: ${ex}`;
1302
1686
  }
1303
- } finally {
1304
- lock.release();
1305
1687
  }
1688
+
1689
+ // すべてのストレージで存在を確認できなかったときは結果が不明なため、get() と同様にエラーとして報告します。
1690
+ if (errors.length > 0) {
1691
+ throw this.#notFound(key, errors);
1692
+ }
1693
+
1694
+ return false;
1306
1695
  }
1307
1696
 
1308
- /**
1309
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
1310
- */
1311
1697
  public delete(options: DeleteOptions<KeyofKeyValueMapping<TKeyValueMapping>>): Promise<void>;
1312
-
1313
- /**
1314
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
1315
- */
1316
1698
  public delete(
1317
1699
  key: KeyofKeyValueMapping<TKeyValueMapping>,
1318
1700
  options?: Omit<DeleteOptions, "key">,
1319
1701
  ): Promise<void>;
1320
-
1321
1702
  public async delete(...args: any): Promise<void> {
1322
1703
  if (this.#con === null) {
1323
1704
  throw new UniKvsIsNotOpenError();
1324
1705
  }
1325
1706
 
1326
- const [options] = v.parseInput(DeleteArgsSchema, args);
1327
- const { key, signal: signalOption, vars: varsOption } = options;
1707
+ const [options] = parseInput(DeleteArgsSchema, args);
1708
+ const { key, vars: varsOption, signal: signalOption } = options;
1328
1709
 
1329
1710
  // キーがキースキーマに一致するかを検証します。
1330
1711
  this.#resolveValueSchema(key);
@@ -1332,92 +1713,127 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
1332
1713
  const { ac, io } = this.#con;
1333
1714
  const signal = combineSignals([ac.signal, signalOption]);
1334
1715
 
1335
- const vars = mergeVariables(this.#vars, varsOption);
1336
- vars["unikvs:action"] = "delete";
1337
- vars["unikvs:key"] = key;
1716
+ const store = this.#createOperationStore(varsOption);
1338
1717
 
1339
- const lock = await asyncmux(this, signal);
1340
- try {
1341
- // ロック待機中に接続状態が変更されていないか再確認します。
1342
- if (this.#con === null) {
1343
- throw new UniKvsIsNotOpenError();
1344
- }
1718
+ using _lock = await asyncmux.readonly(this, signal);
1719
+ // ロック待機中に接続状態が変更されていないか再確認します。
1720
+ if (this.#con === null) {
1721
+ throw new UniKvsIsNotOpenError();
1722
+ }
1345
1723
 
1346
- const lock = await io.lock({ key, signal });
1347
- try {
1348
- // すべてのストレージから対象データを削除します。
1349
- // すべての処理を並列に実行し、エラーが発生した場合は集約します。
1350
- const errors: { reason: unknown }[] = [];
1351
- await Promise.all(
1352
- this.#destinations.map(async ({ storage }) => {
1353
- try {
1354
- if (await storage.exists(vars, signal, key)) {
1355
- await storage.delete(vars, signal, key);
1356
- }
1357
- } catch (reason) {
1358
- errors.push({ reason });
1359
- }
1360
- }),
1361
- );
1362
- if (errors.length > 0) {
1363
- throw new PluginOperationAggregateError({ plugin: "storage", action: "delete", errors });
1724
+ // 削除に先立ち、各ミドルウェアの `use` を実行します。
1725
+ // `use` による vars の変更は後続の解決・削除に引き継がれます。
1726
+ const useContext = new DeleteContext({
1727
+ key,
1728
+ store,
1729
+ signal,
1730
+ });
1731
+ await this.#use("delete", useContext);
1732
+
1733
+ using _ioLock = await io.lock({
1734
+ key,
1735
+ // `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
1736
+ signal,
1737
+ });
1738
+ // 全宛先を並列に削除し、失敗は宛先の index 付きで集約します。
1739
+ const errors: PluginErrorRecord[] = [];
1740
+ const resolver = this.#pipeline.createResolver({
1741
+ key,
1742
+ store,
1743
+ signal,
1744
+ });
1745
+ 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
+
1759
+ try {
1760
+ if (await leaf.storage.exists(existsContext)) {
1761
+ await leaf.storage.delete(deleteContext);
1762
+ }
1763
+ } catch (ex) {
1764
+ errors.push({
1765
+ name: leaf.name,
1766
+ index: leaf.index,
1767
+ reason: ex,
1768
+ });
1364
1769
  }
1365
- } finally {
1366
- lock.release();
1367
- }
1368
- } finally {
1369
- lock.release();
1770
+ }),
1771
+ );
1772
+
1773
+ if (errors.length > 0) {
1774
+ throw new PluginOperationAggregateError({
1775
+ action: "delete",
1776
+ errors,
1777
+ plugin: "storage",
1778
+ });
1370
1779
  }
1371
1780
  }
1372
1781
 
1373
- /**
1374
- * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
1375
- */
1376
1782
  public async clear(options?: ClearOptions): Promise<void>;
1377
-
1378
1783
  public async clear(...args: any): Promise<void> {
1379
1784
  if (this.#con === null) {
1380
1785
  throw new UniKvsIsNotOpenError();
1381
1786
  }
1382
1787
 
1383
- const [options = {}] = v.parseInput(ClearArgsSchema, args);
1384
- const { signal: signalOption, vars: varsOption } = options;
1788
+ const [options = {}] = parseInput(ClearArgsSchema, args);
1789
+ const { vars: varsOption, signal: signalOption } = options;
1385
1790
 
1386
1791
  const { ac, io } = this.#con;
1387
1792
  const signal = combineSignals([ac.signal, signalOption]);
1388
1793
 
1389
- const vars = mergeVariables(this.#vars, varsOption);
1390
- vars["unikvs:action"] = "clear";
1794
+ const store = this.#createOperationStore(varsOption);
1391
1795
 
1392
- const lock = await asyncmux(this, signal);
1393
- try {
1394
- // ロック待機中に接続状態が変更されていないか再確認します。
1395
- if (this.#con === null) {
1396
- throw new UniKvsIsNotOpenError();
1397
- }
1796
+ using _lock = await asyncmux.readonly(this, signal);
1797
+ // ロック待機中に接続状態が変更されていないか再確認します。
1798
+ if (this.#con === null) {
1799
+ throw new UniKvsIsNotOpenError();
1800
+ }
1398
1801
 
1399
- const lock = await io.lock({ signal });
1400
- try {
1401
- // すべてのストレージで一括削除を実行します。
1402
- // すべての処理を並列に実行し、エラーが発生した場合は集約します。
1403
- const errors: { reason: unknown }[] = [];
1404
- await Promise.all(
1405
- this.#destinations.map(async ({ storage }) => {
1406
- try {
1407
- await storage.clear(vars, signal);
1408
- } catch (reason) {
1409
- errors.push({ reason });
1410
- }
1411
- }),
1412
- );
1413
- if (errors.length > 0) {
1414
- throw new PluginOperationAggregateError({ plugin: "storage", action: "clear", errors });
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 付きで集約します。
1812
+ const errors: PluginErrorRecord[] = [];
1813
+ await Promise.all(
1814
+ this.#pipeline.leaves.map(async (leaf, index) => {
1815
+ try {
1816
+ const c = new ClearContext({
1817
+ store,
1818
+ signal,
1819
+ });
1820
+ await leaf.storage.clear(c);
1821
+ } catch (ex) {
1822
+ errors.push({
1823
+ name: leaf.name,
1824
+ index,
1825
+ reason: ex,
1826
+ });
1415
1827
  }
1416
- } finally {
1417
- lock.release();
1418
- }
1419
- } finally {
1420
- lock.release();
1828
+ }),
1829
+ );
1830
+
1831
+ if (errors.length > 0) {
1832
+ throw new PluginOperationAggregateError({
1833
+ action: "clear",
1834
+ errors,
1835
+ plugin: "storage",
1836
+ });
1421
1837
  }
1422
1838
  }
1423
1839
  }