unikvs 0.0.10 → 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 (77) hide show
  1. package/README.md +3 -0
  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 +35 -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 +173 -96
  16. package/src/_streams.schemas.ts +21 -0
  17. package/src/_streams.ts +73 -0
  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 -3
  23. package/src/_value-schemas.ts +192 -0
  24. package/src/_value-validation.ts +57 -0
  25. package/src/errors.ts +446 -252
  26. package/src/index.ts +46 -26
  27. package/src/types.ts +13 -0
  28. package/src/unikvs-branch.ts +445 -0
  29. package/src/unikvs-builder.ts +706 -0
  30. package/src/unikvs.ts +1456 -971
  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-context.d.ts +0 -13
  37. package/dist/src/_merge-context.d.ts.map +0 -1
  38. package/dist/src/_merge-context.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 -94
  42. package/dist/src/_to-value-stream.d.ts +0 -12
  43. package/dist/src/_to-value-stream.d.ts.map +0 -1
  44. package/dist/src/_to-value-stream.js +0 -102
  45. package/dist/src/_transformer.d.ts +0 -13
  46. package/dist/src/_transformer.d.ts.map +0 -1
  47. package/dist/src/_transformer.js +0 -60
  48. package/dist/src/_valibot.d.ts +0 -15
  49. package/dist/src/_valibot.d.ts.map +0 -1
  50. package/dist/src/_valibot.js +0 -23
  51. package/dist/src/context.types.d.ts +0 -17
  52. package/dist/src/context.types.d.ts.map +0 -1
  53. package/dist/src/context.types.js +0 -1
  54. package/dist/src/errors.d.ts +0 -340
  55. package/dist/src/errors.d.ts.map +0 -1
  56. package/dist/src/errors.js +0 -250
  57. package/dist/src/index.d.ts +0 -10
  58. package/dist/src/index.d.ts.map +0 -1
  59. package/dist/src/index.js +0 -3
  60. package/dist/src/unikvs-config.d.ts +0 -255
  61. package/dist/src/unikvs-config.d.ts.map +0 -1
  62. package/dist/src/unikvs-config.js +0 -105
  63. package/dist/src/unikvs.d.ts +0 -354
  64. package/dist/src/unikvs.d.ts.map +0 -1
  65. package/dist/src/unikvs.js +0 -867
  66. package/dist/src/utils.types.d.ts +0 -7
  67. package/dist/src/utils.types.d.ts.map +0 -1
  68. package/dist/src/utils.types.js +0 -1
  69. package/dist/src/value-stream.types.d.ts +0 -16
  70. package/dist/src/value-stream.types.d.ts.map +0 -1
  71. package/dist/src/value-stream.types.js +0 -1
  72. package/src/_merge-context.ts +0 -19
  73. package/src/_transformer.ts +0 -89
  74. package/src/context.types.ts +0 -19
  75. package/src/unikvs-config.ts +0 -426
  76. package/src/utils.types.ts +0 -6
  77. package/src/value-stream.types.ts +0 -19
package/src/unikvs.ts CHANGED
@@ -1,677 +1,590 @@
1
- import type { Context, IStorage, IReadableStream } from "@unikvs/core";
2
- import { combineSignals } from "abort-signal-utils";
3
- import { type AsyncmuxLock, asyncmux, Asyncmux } from "asyncmux";
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
4
 
5
- import logger from "./_logger.js";
6
- import mergeContext from "./_merge-context.js";
7
- import UniKvsStorage from "./_storage.js";
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";
8
24
  import toValueStream from "./_to-value-stream.js";
9
- import UniKvsTransformer from "./_transformer.js";
10
- import * as v from "./_valibot.js";
11
- import type { ContextSource } from "./context.types.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
- PluginOperationAggregateError,
17
48
  UniKvsIsNotOpenError,
49
+ MiddlewareNextNotCalledError,
50
+ MiddlewareNextNotAwaitedError,
51
+ PluginOperationAggregateError,
52
+ MiddlewareNextCalledMultipleTimesError,
18
53
  } from "./errors.js";
19
- import UniKvsConfig, {
20
- type Value,
21
- type PlainValue,
22
- type StreamValue,
23
- type KeyValueMapping,
24
- type $InferPlainValueData,
25
- type IUniKvsConfigBuilder,
26
- type KeyofKeyValueMapping,
27
- type $InferStreamValueChunkData,
28
- } from "./unikvs-config.js";
29
- import type { ValueOf } from "./utils.types.js";
30
- import type { ValueStream } from "./value-stream.types.js";
31
-
32
- // -------------------------------------------------------------------------------------------------
33
- //
34
- // ユーティリテー
35
- //
36
- // -------------------------------------------------------------------------------------------------
37
-
38
- /**
39
- * 設定可能な値の型定義です。
40
- *
41
- * @template TValue 値の型定義です。
42
- */
43
- export type SetValue<TValue extends Value> =
44
- | $InferPlainValueData<TValue>
45
- | (TValue extends StreamValue<infer TChunkData> ? ReadableStream<TChunkData> : never);
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";
46
70
 
47
71
  /**
48
- * プレーンな値を持つキーのみを抽出する型定義です。
49
- *
50
- * @template TKeyValueMapping キーと値のマッピング定義です。
51
- */
52
- export type KeyofKeyValueMappingHasPlainValue<TKeyValueMapping extends KeyValueMapping> = ValueOf<{
53
- [TKey in KeyofKeyValueMapping<TKeyValueMapping>]: PlainValue extends TKeyValueMapping[TKey]
54
- ? TKey
55
- : never;
56
- }>;
57
-
58
- /**
59
- * ストリーム形式の値を持つキーのみを抽出する型定義です。
60
- *
61
- * @template TKeyValueMapping キーと値のマッピング定義です。
72
+ * ストリームの読み取りロックを自動解放するための FinalizationRegistry です。
62
73
  */
63
- export type KeyofKeyValueMappingHasStreamValue<TKeyValueMapping extends KeyValueMapping> = ValueOf<{
64
- [TKey in KeyofKeyValueMapping<TKeyValueMapping>]: StreamValue extends TKeyValueMapping[TKey]
65
- ? TKey
66
- : never;
67
- }>;
68
-
69
- // -------------------------------------------------------------------------------------------------
70
- //
71
- // スキーマ
72
- //
73
- // -------------------------------------------------------------------------------------------------
74
-
75
- const ContextKeySchema = v.string();
76
-
77
- // 配列形式の context も正当な入力であるため、record スキーマより先に判定する必要があります。
78
- // record スキーマは配列にもマッチして数値キーのオブジェクトに変換してしまうため、配列形式を先に処理しないと mergeContext での配列としてのマージが行われません。
79
- const ContextSourceSchema = v.union([
80
- v.array(v.tuple([ContextKeySchema, v.unknown()])),
81
- v.record(v.any(), v.unknown()),
82
- ]);
83
-
84
- const OpenOptionsSchema = v.object({
85
- /**
86
- * 処理の中断を通知するためのシグナルです。
87
- */
88
- signal: v.optional(v.instance(AbortSignal)),
89
-
90
- /**
91
- * 実行時のコンテキスト情報です。
92
- */
93
- context: v.optional(ContextSourceSchema),
94
- });
95
-
96
- const OpenArgsSchema = v.tuple([v.optional(OpenOptionsSchema)]);
74
+ const ioLockRegistry =
75
+ typeof FinalizationRegistry !== "function"
76
+ ? null
77
+ : new FinalizationRegistry<IoLockRegistryHeld>((held) => {
78
+ if (!held.lock.released) {
79
+ held.lock.release();
80
+ }
97
81
 
98
- const CloseOptionsSchema = v.object({
99
- /**
100
- * 処理の中断を通知するためのシグナルです。
101
- */
102
- signal: v.optional(v.instance(AbortSignal)),
82
+ // GC で回収された ValueStream のロックを解放し、ソースのリソースも破棄します。
83
+ // dispose は valueStream を参照しないため、GC を妨げません。失敗したときはログに残すだけにします。
84
+ // oxlint-disable-next-line promise/prefer-await-to-then
85
+ held.dispose().catch((ex) => {
86
+ logger.error`Failed to dispose value stream: ${ex}`;
87
+ });
88
+ });
103
89
 
90
+ type IoLockRegistryHeld = {
104
91
  /**
105
- * 実行時のコンテキスト情報です。
106
- */
107
- context: v.optional(ContextSourceSchema),
108
- });
109
-
110
- const CloseArgsSchema = v.tuple([v.optional(CloseOptionsSchema)]);
111
-
112
- const SetOptionsSchema = v.object({
113
- key: v.string(),
114
- value: v.unknown(),
115
- signal: v.optional(v.instance(AbortSignal)),
116
- context: v.optional(ContextSourceSchema),
117
- });
118
-
119
- const SetArgsSchema = v.union([
120
- v.tuple([SetOptionsSchema]),
121
- v.pipe(
122
- v.tuple([
123
- SetOptionsSchema.entries.key,
124
- SetOptionsSchema.entries.value,
125
- v.optional(v.omit(SetOptionsSchema, ["key", "value"])),
126
- ]),
127
- v.transform(([key, value, options]) => [{ ...options, key, value }]),
128
- ),
129
- ]);
130
-
131
- const GetOptionsSchema = v.object({
132
- key: v.string(),
133
- signal: v.optional(v.instance(AbortSignal)),
134
- context: v.optional(ContextSourceSchema),
135
- });
136
-
137
- const GetArgsSchema = v.union([
138
- v.tuple([GetOptionsSchema]),
139
- v.pipe(
140
- v.tuple([GetOptionsSchema.entries.key, v.optional(v.omit(GetOptionsSchema, ["key"]))]),
141
- v.transform(([key, options]) => [{ ...options, key }]),
142
- ),
143
- ]);
144
-
145
- const StreamOptionsSchema = v.object({
146
- key: v.string(),
147
- signal: v.optional(v.instance(AbortSignal)),
148
- context: v.optional(ContextSourceSchema),
149
- });
150
-
151
- const StreamArgsSchema = v.union([
152
- v.tuple([StreamOptionsSchema]),
153
- v.pipe(
154
- v.tuple([StreamOptionsSchema.entries.key, v.optional(v.omit(StreamOptionsSchema, ["key"]))]),
155
- v.transform(([key, options]) => [{ ...options, key }]),
156
- ),
157
- ]);
158
-
159
- const HasOptionsSchema = v.object({
160
- key: v.string(),
161
- signal: v.optional(v.instance(AbortSignal)),
162
- context: v.optional(ContextSourceSchema),
163
- });
164
-
165
- const HasArgsSchema = v.union([
166
- v.tuple([HasOptionsSchema]),
167
- v.pipe(
168
- v.tuple([HasOptionsSchema.entries.key, v.optional(v.omit(HasOptionsSchema, ["key"]))]),
169
- v.transform(([key, options]) => [{ ...options, key }]),
170
- ),
171
- ]);
172
-
173
- const DeleteOptionsSchema = v.object({
174
- key: v.string(),
175
- signal: v.optional(v.instance(AbortSignal)),
176
- context: v.optional(ContextSourceSchema),
177
- });
178
-
179
- const DeleteArgsSchema = v.union([
180
- v.tuple([DeleteOptionsSchema]),
181
- v.pipe(
182
- v.tuple([DeleteOptionsSchema.entries.key, v.optional(v.omit(DeleteOptionsSchema, ["key"]))]),
183
- v.transform(([key, options]) => [{ ...options, key }]),
184
- ),
185
- ]);
186
-
187
- const ClearOptionsSchema = v.object({
188
- /**
189
- * 処理の中断を通知するためのシグナルです。
92
+ * stream 操作で取得したキーの読み取りロックです。
190
93
  */
191
- signal: v.optional(v.instance(AbortSignal)),
94
+ readonly lock: AsyncmuxLock;
192
95
 
193
96
  /**
194
- * 実行時のコンテキスト情報です。
97
+ * {@linkcode ValueStream.dispose} です。ソースストリームのキャンセルとロック解放を行います。
195
98
  */
196
- context: v.optional(ContextSourceSchema),
197
- });
198
-
199
- const ClearArgsSchema = v.tuple([v.optional(ClearOptionsSchema)]);
200
-
201
- // -------------------------------------------------------------------------------------------------
202
- //
203
- // 型定義
204
- //
205
- // -------------------------------------------------------------------------------------------------
206
-
207
- /**
208
- * オープン操作時のオプションです。
209
- */
210
- export type OpenOptions = v.InferInput<typeof OpenOptionsSchema>;
211
-
212
- /**
213
- * クローズ操作時のオプションです。
214
- */
215
- export type CloseOptions = v.InferInput<typeof CloseOptionsSchema>;
99
+ readonly dispose: () => Promise<void>;
100
+ };
216
101
 
217
102
  /**
218
- * 保存操作時のオプションです。
219
- *
220
- * @template TKeyValueMapping キーと値のマッピング定義です。
221
- * @template TKey 対象となるキーの型です。
103
+ * 接続状態を管理する内部オブジェクトです。
222
104
  */
223
- export type SetOptions<
224
- TKeyValueMapping extends KeyValueMapping = KeyValueMapping,
225
- TKey extends KeyofKeyValueMapping<TKeyValueMapping> = KeyofKeyValueMapping<TKeyValueMapping>,
226
- > = {
227
- /**
228
- * 操作対象を識別するためのキーです。
229
- */
230
- readonly key: TKey;
231
-
232
- /**
233
- * 保存する値です。
234
- */
235
- readonly value: SetValue<TKeyValueMapping[TKey]>;
236
-
105
+ type Connection = {
237
106
  /**
238
- * 処理の中断を通知するためのシグナルです。
107
+ * 接続全体を管理する AbortController です。
239
108
  */
240
- readonly signal?: AbortSignal | undefined;
109
+ readonly ac: AbortController;
241
110
 
242
111
  /**
243
- * 実行時のコンテキスト情報です。
112
+ * I/O の多重化を制御する Asyncmux インスタンスです。
244
113
  */
245
- readonly context?: ContextSource | undefined;
114
+ readonly io: Asyncmux;
246
115
  };
247
116
 
248
117
  /**
249
- * 取得操作時のオプションです。
250
- *
251
- * @template TKey 対象となるキーの型です。
118
+ * プラグイン操作の失敗を集約するための 1 件分の記録です。
252
119
  */
253
- export type GetOptions<TKey = IStorage.Key> = {
120
+ type PluginErrorRecord = {
254
121
  /**
255
- * 操作対象を識別するためのキーです。
122
+ * 失敗したプラグインの名前です。
256
123
  */
257
- readonly key: TKey;
124
+ readonly name: string;
258
125
 
259
126
  /**
260
- * 処理の中断を通知するためのシグナルです。
127
+ * 宛先またはプラグインの登録順 (0 起点) です。
261
128
  */
262
- readonly signal?: AbortSignal | undefined;
129
+ readonly index: number;
263
130
 
264
131
  /**
265
- * 実行時のコンテキスト情報です。
132
+ * 失敗の理由です。
266
133
  */
267
- readonly context?: ContextSource | undefined;
134
+ readonly reason: unknown;
268
135
  };
269
136
 
270
137
  /**
271
- * ストリーム取得操作時のオプションです。
272
- *
273
- * @template TKey 対象となるキーの型です。
138
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#client-operations)
274
139
  */
275
- export type StreamOptions<TKey = IStorage.Key> = {
140
+ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueMapping> {
276
141
  /**
277
- * 操作対象を識別するためのキーです。
142
+ * 現在の接続状態です。非接続時は null となります。
278
143
  */
279
- readonly key: TKey;
144
+ #con: Connection | null;
280
145
 
281
- /**
282
- * 処理の中断を通知するためのシグナルです。
283
- */
284
- readonly signal?: AbortSignal | undefined;
146
+ readonly #acSet: Set<AbortController>;
285
147
 
286
148
  /**
287
- * 実行時のコンテキスト情報です。
149
+ * 基本となる実行変数情報です。
150
+ *
151
+ * コンストラクターで受け取った初期値をエントリー列で保持します。操作ごとに複写して使用します。
288
152
  */
289
- readonly context?: ContextSource | undefined;
290
- };
153
+ #baseVars: readonly VariablesEntry[];
291
154
 
292
- /**
293
- * 存在確認操作時のオプションです。
294
- *
295
- * @template TKey 対象となるキーの型です。
296
- */
297
- export type HasOptions<TKey = IStorage.Key> = {
298
155
  /**
299
- * 操作対象を識別するためのキーです。
156
+ * コンパイル済みのパイプライン・グラフです。
300
157
  */
301
- readonly key: TKey;
158
+ readonly #pipeline: PipelineGraph;
302
159
 
303
160
  /**
304
- * 処理の中断を通知するためのシグナルです。
161
+ * キーに対応する値のスキーマ情報を解決するリゾルバーです。スキーマ未設定時は null となります。
305
162
  */
306
- readonly signal?: AbortSignal | undefined;
163
+ readonly #valueSchemaResolver: IValueSchemaResolver | null;
307
164
 
308
165
  /**
309
- * 実行時のコンテキスト情報です。
166
+ * @internal
310
167
  */
311
- readonly context?: ContextSource | undefined;
312
- };
168
+ public constructor(
169
+ vars: readonly VariablesEntry[],
170
+ pipeline: PipelineGraph,
171
+ valueSchemaResolver: IValueSchemaResolver | null = null,
172
+ ) {
173
+ this.#con = null;
174
+ this.#acSet = new Set();
175
+ this.#baseVars = vars;
176
+ this.#pipeline = pipeline;
177
+ this.#valueSchemaResolver = valueSchemaResolver;
178
+ }
313
179
 
314
- /**
315
- * 削除操作時のオプションです。
316
- *
317
- * @template TKey 対象となるキーの型です。
318
- */
319
- export type DeleteOptions<TKey = IStorage.Key> = {
320
180
  /**
321
- * 操作対象を識別するためのキーです。
181
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#base-vars-set-vars)
322
182
  */
323
- readonly key: TKey;
183
+ public setVars(vars: VariablesInit): this;
184
+ public setVars(...args: unknown[]): this {
185
+ const [vars] = parseInput(VariablesArgsSchema, args);
186
+ this.#baseVars = vars;
324
187
 
325
- /**
326
- * 処理の中断を通知するためのシグナルです。
327
- */
328
- readonly signal?: AbortSignal | undefined;
188
+ return this;
189
+ }
329
190
 
330
191
  /**
331
- * 実行時のコンテキスト情報です。
192
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#base-vars-assign-vars)
332
193
  */
333
- readonly context?: ContextSource | undefined;
334
- };
194
+ public assignVars(vars: VariablesInit): this;
195
+ public assignVars(...args: unknown[]): this {
196
+ const [vars] = parseInput(VariablesArgsSchema, args);
335
197
 
336
- /**
337
- * 全削除操作時のオプションです。
338
- */
339
- export type ClearOptions = v.InferInput<typeof ClearOptionsSchema>;
198
+ const map = new Map(this.#baseVars);
340
199
 
341
- // -------------------------------------------------------------------------------------------------
342
- //
343
- // UniKvs
344
- //
345
- // -------------------------------------------------------------------------------------------------
200
+ for (const [name, value] of vars) {
201
+ map.set(name, value);
202
+ }
346
203
 
347
- /**
348
- * ストリームの読み取りロックを自動解放するための FinalizationRegistry です。
349
- */
350
- const ioLockRegistry =
351
- typeof FinalizationRegistry !== "function"
352
- ? null
353
- : new FinalizationRegistry<IoLockRegistryHeld>((held) => {
354
- if (!held.lock.released) {
355
- held.lock.release();
356
- }
204
+ this.#baseVars = [...map];
357
205
 
358
- // valueStream が読み取られないまま GC されたとき、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放するために破棄します。
359
- // dispose は valueStream 自身を参照しないため、これを保持しても valueStream のガベージコレクションを妨げません。
360
- // 失敗しても対処できないので無視します。
361
- held.dispose().catch((ex) => {
362
- logger.error`Failed to dispose value stream: ${ex}`;
363
- });
364
- });
206
+ return this;
207
+ }
365
208
 
366
- type IoLockRegistryHeld = {
367
209
  /**
368
- * stream 操作で取得したキーの読み取りロックです。
210
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#base-vars-defaults-vars)
369
211
  */
370
- readonly lock: AsyncmuxLock;
212
+ public defaultsVars(vars: VariablesInit): this;
213
+ public defaultsVars(...args: unknown[]): this {
214
+ const [vars] = parseInput(VariablesArgsSchema, args);
371
215
 
372
- /**
373
- * {@linkcode ValueStream.dispose} です。ソースストリームのキャンセルとロック解放を行います。
374
- */
375
- readonly dispose: () => Promise<void>;
376
- };
216
+ const map = new Map(vars);
377
217
 
378
- /**
379
- * 接続状態を管理する内部オブジェクトです。
380
- */
381
- type Connection = {
382
- /**
383
- * 接続全体を管理する AbortController です。
384
- */
385
- readonly ac: AbortController;
218
+ for (const [name, value] of this.#baseVars) {
219
+ map.set(name, value);
220
+ }
221
+
222
+ this.#baseVars = [...map];
223
+
224
+ return this;
225
+ }
386
226
 
387
227
  /**
388
- * I/O の多重化を制御する Asyncmux インスタンスです。
228
+ * 基本変数と操作固有変数を結合した実行時変数マップを作成します。
229
+ *
230
+ * @param vars 操作固有の変数です。
231
+ *
232
+ * @returns 結合した変数マップを返します。
389
233
  */
390
- readonly io: Asyncmux;
391
- };
234
+ #createOperationVars(vars: readonly VariablesEntry[] | undefined): Map<string, unknown> {
235
+ const map = new Map(this.#baseVars);
236
+
237
+ if (vars) {
238
+ for (const [k, v] of vars) {
239
+ map.set(k, v);
240
+ }
241
+ }
242
+
243
+ return map;
244
+ }
392
245
 
393
- /**
394
- * UniKvs メインクラスです。
395
- *
396
- * 複数のストレージとトランスフォーマーを統合して KVS 操作を提供します。
397
- *
398
- * @template TKeyValueMapping キーと値のマッピング定義です。
399
- */
400
- export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueMapping> {
401
246
  /**
402
- * UniKvs の設定ビルダーを作成します。
247
+ * 操作内で共有される変数ストアを作成します。
403
248
  *
404
- * @template TKeyValueMapping マッピング定義です。
405
- * @returns 設定ビルダーのインスタンスです。
406
- * @example
407
- * ```typescript
408
- * import { Compression } from "@unikvs/compression";
409
- * import { FileSystem } from "@unikvs/fs.node";
410
- * import { UniKvs, type Value } from "unikvs";
249
+ * 同一操作内の全コンテキスト (`use`/`encode`/`decode`/ ストレージ) で共有され、下流の `set` が上流の `await next()` 後の `get` に見えます。
411
250
  *
412
- * const kvs = UniKvs.config<{
413
- * foo: Value<Uint8Array<ArrayBuffer>>;
414
- * }>()
415
- * .appendTransformer(new Compression("gzip"))
416
- * .appendStorage(new FileSystem(".tmp"))
417
- * .create();
251
+ * @param vars 操作固有の変数です。
418
252
  *
419
- * await kvs.open();
253
+ * @returns 共有ストアを返します。
254
+ */
255
+ #createOperationStore(vars: readonly VariablesEntry[] | undefined): VariablesStore {
256
+ return createVariablesStore([...this.#baseVars, ...(vars ?? [])]);
257
+ }
258
+
259
+ /**
260
+ * キーに対応する値のスキーマ情報を解決します。
420
261
  *
421
- * await kvs.set("foo", Uint8Array.from([0, 1, 2]));
262
+ * @param key 解決するキーです。
422
263
  *
423
- * await kvs.close();
424
- * ```
264
+ * @returns 対応するスキーマ情報を返します。スキーマ未設定または対応する定義がない場合は `undefined` を返します。
425
265
  */
426
- public static config<
427
- TKeyValueMapping extends KeyValueMapping,
428
- >(): IUniKvsConfigBuilder<TKeyValueMapping> {
429
- return new UniKvsConfig(this);
266
+ #resolveValueSchema(key: string): ValueSchemaInfo | undefined {
267
+ return this.#valueSchemaResolver?.resolve(key);
430
268
  }
431
269
 
432
270
  /**
433
- * 現在の接続状態です。非接続時は null となります。
271
+ * すべての宛先で読み取りに失敗したときのエラーを組み立てます。
272
+ *
273
+ * @param key 読み取ったキーです。
274
+ * @param errors 収集した失敗です。
275
+ *
276
+ * @returns `KeyNotFoundError` を返します。
434
277
  */
435
- #con: Connection | null;
278
+ #notFound(key: string, errors: readonly PluginErrorRecord[]): KeyNotFoundError {
279
+ const args: KeyNotFoundErrorArgs = { key };
436
280
 
437
- readonly #acSet: Set<AbortController>;
281
+ switch (errors.length) {
282
+ case 0: {
283
+ break;
284
+ }
438
285
 
439
- /**
440
- * 基本となる実行コンテキスト情報です。
441
- */
442
- readonly #context: Readonly<Context>;
286
+ case 1: {
287
+ const [firstError] = errors;
443
288
 
444
- /**
445
- * データの永続化先となるストレージのリストです。
446
- */
447
- readonly #destinations: readonly [UniKvsStorage, ...UniKvsStorage[]];
289
+ if (firstError !== undefined) {
290
+ args.cause = firstError.reason;
291
+ }
448
292
 
449
- /**
450
- * データの変換を行うトランスフォーマーのリストです。
451
- */
452
- readonly #transformers: readonly UniKvsTransformer[];
293
+ break;
294
+ }
453
295
 
454
- /**
455
- * インスタンスを初期化します。
456
- *
457
- * @internal UniKvs の設定ビルダー経由で使用します。
458
- * @param context コンテキストです。
459
- * @param destinations ストレージのリストです。
460
- * @param transformers トランスフォーマーのリストです。
461
- */
462
- public constructor(
463
- context: Readonly<Context>,
464
- destinations: readonly [UniKvsStorage, ...UniKvsStorage[]],
465
- transformers: readonly UniKvsTransformer[],
466
- ) {
467
- this.#con = null;
468
- this.#acSet = new Set();
469
- this.#context = { ...context };
470
- this.#destinations = destinations;
471
- this.#transformers = transformers;
296
+ default: {
297
+ args.cause = new PluginOperationAggregateError({
298
+ action: "read",
299
+ errors,
300
+ plugin: "storage",
301
+ });
302
+ }
303
+ }
304
+
305
+ return new KeyNotFoundError(args);
472
306
  }
473
307
 
474
- /**
475
- * UniKvs が利用可能であるかを確認します。
476
- *
477
- * @returns 利用可能である場合は true、そうでない場合は false を返します。
478
- */
479
308
  public get isOpen(): boolean {
480
309
  return this.#con !== null;
481
310
  }
482
311
 
483
- /**
484
- * すべてのストレージとトランスフォーマーを初期化します。
485
- *
486
- * @param options オープン時のオプションです。
487
- * @returns 完了を通知する Promise です。
488
- */
489
312
  public open(options?: OpenOptions): Promise<void>;
490
-
491
313
  public async open(...args: any): Promise<void> {
492
314
  if (this.#con !== null) {
493
315
  throw new UniKvsIsOpenError();
494
316
  }
495
317
 
496
- const [options = {}] = v.parseInput(OpenArgsSchema, args);
497
- const { signal: signalOption, context: contextOption } = options;
318
+ const [options = {}] = parseInput(OpenArgsSchema, args);
319
+ const { vars: varsOption, signal: signalOption } = options;
498
320
 
499
321
  const ac = new AbortController();
500
322
  const signal = combineSignals([ac.signal, signalOption]);
501
323
 
502
- const context = mergeContext(this.#context, contextOption);
503
- context["unikvs:action"] = "open";
504
-
505
- signal.throwIfAborted();
506
-
324
+ const operationVars = this.#createOperationVars(varsOption);
325
+ throwIfAborted(signal);
507
326
  this.#acSet.add(ac);
508
327
 
509
328
  const dispose: (() => Promise<void>)[] = [];
510
329
 
511
- const lock = await asyncmux(this, signal);
512
- try {
513
- if (this.#con !== null) {
514
- throw new UniKvsIsOpenError();
515
- }
516
-
517
- const openFns: [() => Promise<void>, plugin: "storage" | "transformer"][] = [];
518
-
519
- // 各ストレージのオープン処理をリストに追加します。
520
- for (const storage of this.#destinations) {
521
- openFns.push([
522
- async () => {
523
- await storage.open(context, signal);
524
-
525
- dispose.push(async () => {
526
- try {
527
- await storage.close(context, signal);
528
- } catch (ex) {
529
- logger.error`Failed to close storage: ${ex}`;
530
- }
531
- });
532
- },
533
- "storage",
534
- ]);
535
- }
536
-
537
- // 各トランスフォーマーのオープン処理をリストに追加します。
538
- for (const transformer of this.#transformers) {
539
- openFns.push([
540
- async () => {
541
- await transformer.open(context, signal);
542
-
543
- dispose.push(async () => {
544
- try {
545
- await transformer.close(context, signal);
546
- } catch (ex) {
547
- logger.error`Failed to close transformer: ${ex}`;
548
- }
549
- });
550
- },
551
- "transformer",
552
- ]);
553
- }
554
-
555
- // すべての処理を並列に実行し、エラーが発生した場合は集約します。
556
- const errors: { plugin: "storage" | "transformer"; reason: unknown }[] = [];
557
- await Promise.all(
558
- openFns.map(async ([f, plugin]) => {
559
- try {
560
- await f();
561
- } catch (reason) {
562
- errors.push({ plugin, reason });
563
- }
564
- }),
565
- );
566
- if (errors.length > 0) {
567
- // 中断以外の失敗が混在しない場合は、abort 理由が集約エラーに埋もれないようにそのまま投げます。
568
- if (signal.aborted && errors.every((error) => error.reason === signal.reason)) {
569
- throw signal.reason;
570
- }
571
-
572
- throw new PluginOperationAggregateError({ action: "open", errors });
573
- }
574
-
575
- // すべてのプラグインのオープンが成功した後も、待機中に中断されていないか最終確認します。
576
- // 中断済みのシグナルを持つ接続を作成すると、isOpen が true でありながら以降のすべての操作が即座に失敗する壊れた状態になります。
577
- signal.throwIfAborted();
578
-
579
- this.#con = {
580
- ac,
581
- io: new Asyncmux(),
582
- };
583
- } catch (ex) {
584
- if (dispose.length > 0) {
585
- await Promise.all(
586
- dispose.map(async (f) => {
587
- await f();
588
- }),
589
- );
590
- }
591
-
592
- throw ex;
593
- } finally {
594
- this.#acSet.delete(ac);
595
- lock.release();
596
- }
597
- }
598
-
599
- async #close(context: Context, signal: AbortSignal, con: Connection): Promise<void> {
600
- const lock = await asyncmux(this, signal);
601
330
  try {
602
- // ロック待機中に接続状態が変更されていないか再確認します。
603
- if (this.#con !== con) {
604
- throw new UniKvsIsNotOpenError();
605
- }
331
+ using _lock = await asyncmux(this, signal);
606
332
 
607
- const { io } = this.#con;
608
- const lock = await io.lock({ signal });
609
333
  try {
610
- const closeFns: [() => Promise<void>, plugin: "storage" | "transformer"][] = [];
334
+ if (this.#con !== null) {
335
+ throw new UniKvsIsOpenError();
336
+ }
611
337
 
612
- // ストレージのクローズ処理を登録します。
613
- for (const plugin of this.#destinations) {
614
- closeFns.push([
615
- async () => {
616
- await plugin.close(context, 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
+ });
617
366
  },
618
367
  "storage",
368
+ leaf.name,
369
+ index,
619
370
  ]);
620
371
  }
621
372
 
622
- // トランスフォーマーのクローズ処理を登録します。
623
- for (const plugin of this.#transformers) {
624
- closeFns.push([
625
- async () => {
626
- await plugin.close(context, 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
+ });
627
395
  },
628
- "transformer",
396
+ "middleware",
397
+ middleware.name,
398
+ index,
629
399
  ]);
630
400
  }
631
401
 
632
402
  // すべての処理を並列に実行し、エラーが発生した場合は集約します。
633
- const errors: { plugin: "storage" | "transformer"; reason: unknown }[] = [];
403
+ const errors: {
404
+ name: string;
405
+ index: number;
406
+ plugin: "storage" | "middleware";
407
+ reason: unknown;
408
+ }[] = [];
634
409
  await Promise.all(
635
- closeFns.map(async ([f, plugin]) => {
410
+ openFns.map(async ([f, plugin, name, index]) => {
636
411
  try {
637
412
  await f();
638
- } catch (reason) {
639
- errors.push({ plugin, reason });
413
+ } catch (ex) {
414
+ errors.push({
415
+ name,
416
+ index,
417
+ plugin,
418
+ reason: ex,
419
+ });
640
420
  }
641
421
  }),
642
422
  );
423
+
643
424
  if (errors.length > 0) {
644
- 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
+ );
645
451
  }
646
- } finally {
647
- lock.release();
648
- }
649
452
 
650
- this.#con = null;
453
+ throw ex;
454
+ }
651
455
  } finally {
652
- lock.release();
456
+ this.#acSet.delete(ac);
653
457
  }
654
458
  }
655
459
 
656
- /**
657
- * ストレージをクローズします。
658
- *
659
- * @param options クローズ時のオプションです。
660
- * @returns 完了を通知する Promise です。
661
- */
662
- 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
+ }
663
574
 
575
+ public close(options?: CloseOptions): Promise<void>;
664
576
  public close(...args: any): Promise<void> {
665
577
  try {
666
- const [options = {}] = v.parseInput(CloseArgsSchema, args);
667
- const { signal = AbortSignal.timeout(10e3), context: contextOption } = options;
578
+ const [options = {}] = parseInput(CloseArgsSchema, args);
579
+ const { vars: varsOption, signal = AbortSignal.timeout(10e3) } = options;
668
580
 
669
- const context = mergeContext(this.#context, contextOption);
670
- context["unikvs:action"] = "close";
581
+ const operationVars = this.#createOperationVars(varsOption);
671
582
 
672
583
  if (this.#con === null) {
673
584
  const acArr = [...this.#acSet];
585
+
674
586
  this.#acSet.clear();
587
+
675
588
  for (const ac of acArr) {
676
589
  if (!ac.signal.aborted) {
677
590
  ac.abort(new UniKvsIsNotOpenError());
@@ -684,45 +597,53 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
684
597
  const con = this.#con;
685
598
  const { ac } = con;
686
599
  const acArr = [ac, ...this.#acSet];
600
+
687
601
  this.#acSet.clear();
688
- for (const ac of acArr) {
689
- if (!ac.signal.aborted) {
690
- ac.abort(new UniKvsIsNotOpenError());
602
+
603
+ for (const targetAc of acArr) {
604
+ if (!targetAc.signal.aborted) {
605
+ targetAc.abort(new UniKvsIsNotOpenError());
691
606
  }
692
607
  }
693
608
 
694
- return this.#close(context, signal, con).catch(async (ex) => {
695
- // #close が失敗した場合、コネクションの AbortController は既に abort 済みであり、以降の操作がすべて即座に失敗する壊れた状態になります。
696
- // そこで接続を破棄して 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 の一貫した状態に戻します。
697
614
  if (this.#con === con) {
698
615
  this.#con = null;
699
616
 
700
- const disposeSignal = AbortSignal.timeout(10e3);
701
- await Promise.all(
702
- [...this.#destinations, ...this.#transformers].map(async (plugin) => {
703
- try {
704
- await plugin.close(context, disposeSignal);
705
- } catch (reason) {
706
- logger.error`Failed to close plugin after close failure: ${reason}`;
707
- }
708
- }),
709
- );
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) => {
626
+ try {
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}`;
634
+ }
635
+ }),
636
+ );
637
+ }
710
638
  }
711
639
 
712
- throw ex;
640
+ throw closeError;
713
641
  });
714
642
  } catch (ex) {
715
643
  return Promise.reject(ex);
716
644
  }
717
645
  }
718
646
 
719
- /**
720
- * UniKvs が開いている場合にクローズします。
721
- *
722
- * `await using` 構文による自動クローズに使用されます。
723
- *
724
- * @returns 完了を通知する Promise です。
725
- */
726
647
  async [Symbol.asyncDispose](): Promise<void> {
727
648
  if (!this.isOpen) {
728
649
  return;
@@ -734,391 +655,933 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
734
655
  }
735
656
 
736
657
  /**
737
- * 指定したオプションで値を保存します。
658
+ * 書き込み失敗の集約と、成功した宛先への通知を行います。
738
659
  *
739
- * @template TKey 対象キーの型定義です。
740
- * @param options セット操作のオプション一式です。
741
- * @returns 完了を通知する Promise です。
660
+ * @param targets 書き込みを試みた対象です。
661
+ * @param failed 失敗した葉の集合です。
662
+ * @param errors 集約する失敗です。
663
+ * @param signal 中断シグナルです。
742
664
  */
743
- public set<const TKey extends KeyofKeyValueMapping<TKeyValueMapping>>(
744
- options: SetOptions<TKeyValueMapping, TKey>,
745
- ): 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
+ }
746
711
 
747
712
  /**
748
- * 指定したキーに値を保存します。
713
+ * ミドルウェアの `use` をオニオン実行し、失敗時は集約して投げます。
749
714
  *
750
- * @template TKey 対象キーの型定義です。
751
- * @param key 操作対象を識別するためのキーです。
752
- * @param value 保存する値です。
753
- * @param options 追加のオプションです。
754
- * @returns 完了を通知する Promise です。
715
+ * 同一コンテキストオブジェクトを全ミドルウェアで共有し、`next` で下流をネスト実行するため、下流の `set` が上流の `await next()` 後の `get` に見えます。
716
+ * `encode`/`decode` と同じく `next()` の呼び出し規約を検証し、違反したときは操作を実行せず失敗させます (fail-close)。
717
+ * いずれかのミドルウェアが投げたときは即時に確定し、後段は実行しません (fail-fast)。
718
+ * 失敗は集約エラー (単一件) として投げます。
719
+ *
720
+ * @param action 集約エラーに付与する操作名です。
721
+ * @param c 共有の `use` コンテキストです。
755
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>;
756
966
  public set<const TKey extends KeyofKeyValueMapping<TKeyValueMapping>>(
757
967
  key: TKey,
758
968
  value: SetValue<TKeyValueMapping[TKey]>,
759
969
  options?: Omit<SetOptions, "key" | "value">,
760
970
  ): Promise<void>;
761
-
762
971
  public async set(...args: any): Promise<void> {
763
972
  if (this.#con === null) {
764
973
  throw new UniKvsIsNotOpenError();
765
974
  }
766
975
 
767
- const [options] = v.parseInput(SetArgsSchema, args);
768
- const { key, value, signal: signalOption, context: contextOption } = options;
976
+ const [options] = parseInput(SetArgsSchema, args);
977
+ const { key, vars: varsOption, signal: signalOption } = options;
978
+
979
+ const valueSchema = this.#resolveValueSchema(key);
980
+ // スキーマ検証はロック取得前に済ませ、無効な入力ではロックを取得しません。
981
+ const value = valueSchema ? parseSetValue(valueSchema, options.value) : options.value;
769
982
 
770
983
  const { ac, io } = this.#con;
771
984
  const signal = combineSignals([ac.signal, signalOption]);
772
985
 
773
- const context = mergeContext(this.#context, contextOption);
774
- context["unikvs:action"] = "set";
775
- context["unikvs:key"] = key;
986
+ const store = this.#createOperationStore(varsOption);
987
+
988
+ // `#setStream` へ所有権を渡す前の失敗では、ソースをロックしたままにしないよう派生ストリームをキャンセルします。
989
+ const stream = isReadableStream(value) ? value : null;
990
+ let ownsStream = stream !== null;
776
991
 
777
- const lock = await asyncmux(this, signal);
778
992
  try {
993
+ using _lock = await asyncmux.readonly(this, signal);
779
994
  // ロック待機中に接続状態が変更されていないか再確認します。
780
995
  if (this.#con === null) {
781
996
  throw new UniKvsIsNotOpenError();
782
997
  }
783
998
 
784
- const errors: { reason: unknown }[] = [];
785
- const errorStorageSet = new Set<UniKvsStorage>();
786
- if (value instanceof ReadableStream) {
787
- // 各トランスフォーマーを通してデータをエンコードストリームへ変換します。
788
- let data = value;
789
- try {
790
- for (const transformer of this.#transformers) {
791
- const e = await transformer.getEncodable(context, signal);
792
- data = data.pipeThrough(e);
793
- }
794
- } catch (ex) {
795
- // チェーンの構築中に失敗した場合、パイプが保持するソースストリームのリソースを解放するためにキャンセルします。
796
- // キャンセルしないとソースストリームはロックされたままリークします。
797
- try {
798
- await data.cancel(ex);
799
- } catch {
800
- // キャンセルに失敗しても元の例外の伝播を優先します。
801
- }
802
-
803
- throw ex;
804
- }
805
-
806
- const lock = await io.lock({ key, signal });
807
- try {
808
- await Promise.all(
809
- this.#destinations.map(async (storage, i) => {
810
- // tee ブランチはキャンセルされるまで健康なブランチの読み取り分のチャンクを保持し続けるため、失敗時に放棄したブランチを確実にキャンセルできるよう参照を保持します。
811
- let branch: ReadableStream | null = null;
812
- try {
813
- // 複数のストレージがある場合は、tee メソッドを使用してストリームを分岐させます。
814
- let r = data;
815
- if (i !== this.#destinations.length - 1) {
816
- [r, data] = data.tee();
817
- }
818
- branch = r;
819
-
820
- const w = await storage.getWritable(context, signal, key);
821
- await r.pipeTo(w, { signal });
822
- } catch (reason) {
823
- if (branch !== null) {
824
- try {
825
- await branch.cancel(reason);
826
- } catch {
827
- // キャンセルに失敗してもエラー集約を優先します。
828
- }
829
- }
830
-
831
- errors.push({ reason });
832
- errorStorageSet.add(storage);
833
- }
834
- }),
835
- );
836
- } finally {
837
- lock.release();
838
- }
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);
839
1014
  } else {
840
- // 登録されているトランスフォーマーを順番に適用してデータをエンコードします。
841
- let data = value;
842
- for (const transformer of this.#transformers) {
843
- data = await transformer.encode(context, signal, data);
844
- }
845
-
846
- 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
+ // 消費されないストリームをキャンセルし、ソースの解放を伝播させます。
847
1020
  try {
848
- await Promise.all(
849
- this.#destinations.map(async (storage) => {
850
- try {
851
- await storage.write(context, signal, key, data);
852
- } catch (reason) {
853
- errors.push({ reason });
854
- errorStorageSet.add(storage);
855
- }
856
- }),
857
- );
858
- } finally {
859
- lock.release();
1021
+ await stream.cancel(ex);
1022
+ } catch {
1023
+ // キャンセルに失敗しても元の例外の伝播を優先します。
860
1024
  }
861
1025
  }
862
1026
 
863
- // エラーが発生した場合は集約します。
864
- if (errors.length > 0) {
865
- const error = new PluginOperationAggregateError({
866
- plugin: "storage",
867
- action: "write",
868
- errors,
869
- });
870
- {
871
- const errors: unknown[] = [];
872
- await Promise.all(
873
- this.#destinations.map(async (storage) => {
874
- if (errorStorageSet.has(storage)) {
875
- return;
876
- }
877
-
878
- try {
879
- await storage.onOtherWriteError(context, signal, key, error);
880
- } catch (ex) {
881
- errors.push(ex);
882
- }
883
- }),
884
- );
885
- if (errors.length > 0) {
886
- logger.error(new AggregateError(errors, "Failed to handle error"));
887
- }
888
- }
889
-
890
- throw error;
891
- }
892
- } finally {
893
- lock.release();
1027
+ throw ex;
894
1028
  }
895
1029
  }
896
1030
 
897
1031
  /**
898
- * 指定したオプションで値を取得します。
1032
+ * 読み取り元のチェーン全体を逆順にデコードし、書き戻し対象向けに再エンコードして書き込みます。
1033
+ *
1034
+ * @param data 読み取ったデータです。
1035
+ * @param hit 読み取り元の葉です。
1036
+ * @param missing 読み取りに失敗または不在だった葉です。
1037
+ * @param store 操作内で共有される変数ストアです。
1038
+ * @param key キーです。
1039
+ * @param signal 中断シグナルです。
1040
+ * @param throwOnRepairDenied 書き戻し拒否をエラーとして投げるかどうかです。
899
1041
  *
900
- * @template TKey 対象キーの型定義です。
901
- * @param options 取得操作のオプション一式です。
902
- * @returns 取得した値を返します。
1042
+ * @returns デコード済みの原データを返します。
903
1043
  */
1044
+ async #repairPlain(
1045
+ data: unknown,
1046
+ hit: ResolvedLeaf,
1047
+ missing: readonly PipelineLeaf[],
1048
+ store: VariablesStore,
1049
+ key: string,
1050
+ signal: AbortSignal,
1051
+ throwOnRepairDenied: boolean,
1052
+ ): Promise<unknown> {
1053
+ const raw = await this.#pipeline.decode(
1054
+ hit.leaf,
1055
+ {
1056
+ key,
1057
+ store,
1058
+ signal,
1059
+ },
1060
+ data,
1061
+ );
1062
+ await this.#pipeline.repair(
1063
+ {
1064
+ key,
1065
+ store,
1066
+ signal,
1067
+ },
1068
+ missing,
1069
+ throwOnRepairDenied,
1070
+ raw,
1071
+ async (leaf, writeKey, input) => {
1072
+ const c = new SetContext({
1073
+ key: writeKey,
1074
+ store,
1075
+ signal,
1076
+ });
1077
+ await leaf.storage.write(c, input);
1078
+ },
1079
+ );
1080
+
1081
+ throwIfAborted(signal);
1082
+
1083
+ return raw;
1084
+ }
1085
+
904
1086
  public get<const TKey extends KeyofKeyValueMappingHasPlainValue<TKeyValueMapping>>(
905
1087
  options: GetOptions<TKey>,
906
1088
  ): Promise<$InferPlainValueData<TKeyValueMapping[TKey]>>;
907
-
908
- /**
909
- * 指定したキーから値を取得します。
910
- *
911
- * @template TKey 対象キーの型定義です。
912
- * @param key 操作対象を識別するためのキーです。
913
- * @param options 追加のオプションです。
914
- * @returns 取得した値を返します。
915
- */
916
1089
  public get<const TKey extends KeyofKeyValueMappingHasPlainValue<TKeyValueMapping>>(
917
1090
  key: TKey,
918
1091
  options?: Omit<GetOptions, "key">,
919
1092
  ): Promise<$InferPlainValueData<TKeyValueMapping[TKey]>>;
920
-
921
1093
  public async get(...args: any): Promise<unknown> {
922
1094
  if (this.#con === null) {
923
1095
  throw new UniKvsIsNotOpenError();
924
1096
  }
925
1097
 
926
- const [options] = v.parseInput(GetArgsSchema, args);
927
- const { key, signal: signalOption, context: contextOption } = 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;
1106
+
1107
+ const valueSchema = this.#resolveValueSchema(key);
928
1108
 
929
1109
  const { ac, io } = this.#con;
930
1110
  const signal = combineSignals([ac.signal, signalOption]);
931
1111
 
932
- const context = mergeContext(this.#context, contextOption);
933
- context["unikvs:action"] = "get";
934
- context["unikvs:key"] = key;
1112
+ const store = this.#createOperationStore(varsOption);
935
1113
 
936
- const lock = await asyncmux.readonly(this, signal);
937
- try {
938
- // ロック待機中に接続状態が変更されていないか再確認します。
939
- if (this.#con === null) {
940
- throw new UniKvsIsNotOpenError();
941
- }
1114
+ using _lock = await asyncmux.readonly(this, signal);
1115
+ // ロック待機中に接続状態が変更されていないか再確認します。
1116
+ if (this.#con === null) {
1117
+ throw new UniKvsIsNotOpenError();
1118
+ }
942
1119
 
943
- const NONE = {};
944
- let data: any = NONE;
945
- 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
+ });
946
1161
 
947
- const lock = await io.rLock({ key, signal });
948
- try {
949
- // 各ストレージを巡回し、最初に見つかったデータを取得します。
950
- // あるストレージの読み取りに失敗しても、他のストレージからデータを取得できるようにフォールバックします。
951
- for (const storage of this.#destinations) {
952
- try {
953
- if (!(await storage.exists(context, signal, key))) {
954
- continue;
955
- }
956
- data = await storage.read(context, signal, key);
957
- break;
958
- } catch (ex) {
959
- if (signal.aborted) {
960
- 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[]],
961
1173
  }
962
- errors.push({ reason: ex });
963
- 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;
964
1189
  }
965
- }
966
- } finally {
967
- lock.release();
968
- }
969
1190
 
970
- if (data === NONE) {
971
- const args: KeyNotFoundErrorArgs = { key };
972
- switch (errors.length) {
973
- case 0:
974
- break;
975
- case 1:
976
- args.cause = errors[0]!.reason;
977
- break;
978
- default:
979
- args.cause = new PluginOperationAggregateError({
980
- plugin: "storage",
981
- action: "read",
982
- errors,
983
- });
1191
+ data = await leaf.storage.read(readContext);
1192
+ hit = resolved;
1193
+ break;
1194
+ } catch (ex) {
1195
+ if (signal.aborted) {
1196
+ throw ex;
1197
+ }
1198
+
1199
+ errors.push({
1200
+ name: leaf.name,
1201
+ index: leaf.index,
1202
+ reason: ex,
1203
+ });
1204
+ logger.error`Failed to read from a storage: ${ex}`;
984
1205
  }
1206
+ }
985
1207
 
986
- throw new KeyNotFoundError(args);
1208
+ if (hit === null) {
1209
+ throw this.#notFound(key, errors);
987
1210
  }
988
1211
 
989
- // トランスフォーマーを逆順に適用してデータをデコードします。
990
- if (this.#transformers.length > 0) {
991
- for (const transformer of this.#transformers.toReversed()) {
992
- data = await transformer.decode(context, signal, data);
993
- }
1212
+ if (repair) {
1213
+ // 後段で見つかったデータをすべてデコードして原データに戻します。
1214
+ data = await this.#repairPlain(data, hit, missing, store, key, signal, throwOnRepairDenied);
994
1215
  }
1216
+ }
995
1217
 
996
- return data;
997
- } finally {
998
- 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
+ );
999
1229
  }
1230
+
1231
+ // デコード後のデータを検証します。
1232
+ if (valueSchema) {
1233
+ data = parseOutput(valueSchema.schema, data);
1234
+ }
1235
+
1236
+ return data;
1000
1237
  }
1001
1238
 
1002
- /**
1003
- * 指定したオプションでストリームを取得します。
1004
- *
1005
- * @template TKey 対象キーの型定義です。
1006
- * @param options ストリーム取得操作のオプション一式です。
1007
- * @returns 取得したストリームを返します。
1008
- */
1009
1239
  public stream<const TKey extends KeyofKeyValueMappingHasStreamValue<TKeyValueMapping>>(
1010
1240
  options: StreamOptions<TKey>,
1011
- ): Promise<ValueStream<$InferStreamValueChunkData<TKeyValueMapping[TKey]>>>;
1012
-
1013
- /**
1014
- * 指定したキーからストリームを取得します。
1015
- *
1016
- * @template TKey 対象キーの型定義です。
1017
- * @param key 操作対象を識別するためのキーです。
1018
- * @param options 追加のオプションです。
1019
- * @returns 取得したストリームを返します。
1020
- */
1241
+ ): Promise<IValueStream<$InferStreamValueChunkData<TKeyValueMapping[TKey]>>>;
1021
1242
  public stream<const TKey extends KeyofKeyValueMappingHasStreamValue<TKeyValueMapping>>(
1022
1243
  key: TKey,
1023
1244
  options?: Omit<StreamOptions, "key">,
1024
- ): Promise<ValueStream<$InferStreamValueChunkData<TKeyValueMapping[TKey]>>>;
1025
-
1026
- public async stream(...args: any): Promise<ValueStream> {
1245
+ ): Promise<IValueStream<$InferStreamValueChunkData<TKeyValueMapping[TKey]>>>;
1246
+ public async stream(...args: any): Promise<IValueStream> {
1027
1247
  if (this.#con === null) {
1028
1248
  throw new UniKvsIsNotOpenError();
1029
1249
  }
1030
1250
 
1031
- const [options] = v.parseInput(StreamArgsSchema, args);
1032
- const { key, signal: signalOption, context: contextOption } = 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;
1259
+
1260
+ const valueSchema = this.#resolveValueSchema(key);
1033
1261
 
1034
1262
  const { ac, io } = this.#con;
1035
1263
  const signal = combineSignals([ac.signal, signalOption]);
1036
1264
 
1037
- const context = mergeContext(this.#context, contextOption);
1038
- context["unikvs:action"] = "stream";
1039
- context["unikvs:key"] = key;
1265
+ const store = this.#createOperationStore(varsOption);
1040
1266
 
1041
- const lock = await asyncmux.readonly(this, signal);
1042
- try {
1267
+ {
1268
+ using _lock = await asyncmux.readonly(this, signal);
1043
1269
  // ロック待機中に接続状態が変更されていないか再確認します。
1044
1270
  if (this.#con === null) {
1045
1271
  throw new UniKvsIsNotOpenError();
1046
1272
  }
1047
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 の呼び出し前を表す番兵値です。
1048
1294
  const NONE: any = {};
1049
1295
  let r: IReadableStream = NONE;
1050
- 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
+ }));
1051
1315
 
1052
- const lock = await io.rLock({ key, signal });
1053
1316
  try {
1054
- // 各ストレージを巡回し、最初に見つかったデータを取得します。
1055
- // あるストレージの読み取りに失敗しても、他のストレージからデータを取得できるようにフォールバックします。
1056
- for (const storage 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
+
1057
1347
  try {
1058
- if (!(await storage.exists(context, signal, key))) {
1348
+ if (!(await leaf.storage.exists(existsContext))) {
1349
+ missing.push(leaf);
1059
1350
  continue;
1060
1351
  }
1061
1352
 
1062
- r = await storage.getReadable(context, signal, key);
1353
+ r = await leaf.storage.getReadable(readableContext);
1354
+ hit = resolved;
1063
1355
  break;
1064
1356
  } catch (ex) {
1065
1357
  if (signal.aborted) {
1066
1358
  throw ex;
1067
1359
  }
1068
1360
 
1069
- errors.push({ reason: ex });
1361
+ errors.push({
1362
+ name: leaf.name,
1363
+ index: leaf.index,
1364
+ reason: ex,
1365
+ });
1070
1366
  logger.error`Failed to read from a storage: ${ex}`;
1071
1367
  }
1072
1368
  }
1073
1369
 
1074
- if (r === NONE) {
1075
- const args: KeyNotFoundErrorArgs = { key };
1076
- switch (errors.length) {
1077
- case 0:
1078
- break;
1079
- case 1:
1080
- args.cause = errors[0]!.reason;
1081
- break;
1082
- default:
1083
- args.cause = new PluginOperationAggregateError({
1084
- plugin: "storage",
1085
- action: "read",
1086
- errors,
1087
- });
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);
1464
+ }
1465
+
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;
1504
+
1505
+ for (const target of tree.targets) {
1506
+ fill(target);
1507
+ }
1088
1508
  }
1089
1509
 
1090
- throw new KeyNotFoundError(args);
1510
+ throwIfAborted(signal);
1091
1511
  }
1092
1512
 
1093
- // トランスフォーマーを逆順に適用し、デコード用トランスフォームを連結します。
1094
- for (const transformer of this.#transformers.toReversed()) {
1095
- const d = await transformer.getDecodable(context, signal);
1096
- r = r.pipeThrough(d);
1513
+ // デコード後のチャンクを検証します。
1514
+ if (valueSchema) {
1515
+ r = r.pipeThrough(
1516
+ new TransformStream({
1517
+ transform(chunk, controller): void {
1518
+ controller.enqueue(parseOutput(valueSchema.schema, chunk));
1519
+ },
1520
+ }),
1521
+ );
1097
1522
  }
1098
1523
 
1099
- if (!ioLockRegistry) {
1100
- 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 {
1101
1553
  try {
1102
- lock.release();
1103
- } catch {}
1104
- });
1554
+ ioLock.release();
1555
+ } catch {
1556
+ // 解放済みでも dispose を失敗させません。
1557
+ }
1558
+ }
1559
+ };
1560
+
1561
+ if (!ioLockRegistry) {
1562
+ return toValueStream(r, dispose, beforeDispose);
1105
1563
  }
1106
1564
 
1107
1565
  const unregisterToken = {};
1108
- const valueStream = toValueStream(r, async () => {
1109
- try {
1110
- ioLockRegistry.unregister(unregisterToken);
1111
- } catch {}
1112
- try {
1113
- lock.release();
1114
- } catch {}
1115
- });
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
+ );
1116
1579
 
1117
- // valueStream が GC されるタイミングでストリームが終了していなければロックを自動解放するとともに、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放できるように dispose を記録します。
1580
+ // valueStream が GC される時点でストリームが終わっていなければロックを自動解放します。あわせて、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放できるよう dispose を記録します。
1118
1581
  ioLockRegistry.register(
1119
1582
  valueStream,
1120
1583
  {
1121
- lock,
1584
+ lock: ioLock,
1122
1585
  dispose: valueStream.dispose,
1123
1586
  },
1124
1587
  unregisterToken,
@@ -1126,229 +1589,251 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
1126
1589
 
1127
1590
  return valueStream;
1128
1591
  } catch (ex) {
1592
+ await cancelFills?.(ex);
1593
+
1129
1594
  if (r !== NONE) {
1130
- // getReadable 成功後のセットアップ中に失敗した場合、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放するためにキャンセルします。
1595
+ // getReadable の成功後にセットアップで失敗したときは、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放するためキャンセルします。
1131
1596
  try {
1132
1597
  await r.cancel(ex);
1133
- } catch {}
1598
+ } catch {
1599
+ // キャンセルに失敗しても元の例外の伝播を優先します。
1600
+ }
1134
1601
  }
1135
1602
 
1136
- lock.release();
1603
+ ioLock.release();
1604
+
1137
1605
  throw ex;
1138
1606
  }
1139
- } finally {
1140
- lock.release();
1141
1607
  }
1142
1608
  }
1143
1609
 
1144
- /**
1145
- * 指定したオプションでキーの存在を確認します。
1146
- *
1147
- * @param options 存在確認操作のオプションです。
1148
- * @returns 存在する場合は true、そうでない場合は false を返します。
1149
- */
1150
1610
  public has(options: HasOptions<KeyofKeyValueMapping<TKeyValueMapping>>): Promise<boolean>;
1151
-
1152
- /**
1153
- * 指定したキーが存在するかを確認します。
1154
- *
1155
- * @param key 操作対象を識別するためのキーです。
1156
- * @param options 追加のオプションです。
1157
- * @returns 存在する場合は true、そうでない場合は false を返します。
1158
- */
1159
1611
  public has(
1160
1612
  key: KeyofKeyValueMapping<TKeyValueMapping>,
1161
1613
  options?: Omit<HasOptions, "key">,
1162
1614
  ): Promise<boolean>;
1163
-
1164
1615
  public async has(...args: any): Promise<boolean> {
1165
1616
  if (this.#con === null) {
1166
1617
  throw new UniKvsIsNotOpenError();
1167
1618
  }
1168
1619
 
1169
- const [options] = v.parseInput(HasArgsSchema, args);
1170
- const { key, signal: signalOption, context: contextOption } = options;
1620
+ const [options] = parseInput(HasArgsSchema, args);
1621
+ const { key, vars: varsOption, signal: signalOption } = options;
1622
+
1623
+ // キーがキースキーマに一致するかを検証します。
1624
+ this.#resolveValueSchema(key);
1171
1625
 
1172
1626
  const { ac, io } = this.#con;
1173
1627
  const signal = combineSignals([ac.signal, signalOption]);
1174
1628
 
1175
- const context = mergeContext(this.#context, contextOption);
1176
- context["unikvs:action"] = "has";
1177
- context["unikvs:key"] = key;
1629
+ const store = this.#createOperationStore(varsOption);
1178
1630
 
1179
- const lock = await asyncmux.readonly(this, signal);
1180
- try {
1181
- // ロック待機中に接続状態が変更されていないか再確認します。
1182
- if (this.#con === null) {
1183
- throw new UniKvsIsNotOpenError();
1184
- }
1631
+ using _lock = await asyncmux.readonly(this, signal);
1632
+ // ロック待機中に接続状態が変更されていないか再確認します。
1633
+ if (this.#con === null) {
1634
+ throw new UniKvsIsNotOpenError();
1635
+ }
1185
1636
 
1186
- const lock = await io.rLock({ key, signal });
1187
- try {
1188
- // いずれかのストレージに存在すれば true を返します。
1189
- // あるストレージの存在確認に失敗しても、他のストレージで存在を確認できるようにフォールバックします。
1190
- const errors: { reason: unknown }[] = [];
1191
- for (const storage of this.#destinations) {
1192
- try {
1193
- if (await storage.exists(context, signal, key)) {
1194
- return true;
1195
- }
1196
- } catch (ex) {
1197
- if (signal.aborted) {
1198
- throw ex;
1199
- }
1200
- errors.push({ reason: ex });
1201
- logger.error`Failed to check existence in a storage: ${ex}`;
1202
- }
1203
- }
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);
1204
1647
 
1205
- // すべてのストレージで存在を確認できなかった場合は結果が不明のため、get() と同様にエラーとして報告します。
1206
- if (errors.length > 0) {
1207
- const args: KeyNotFoundErrorArgs = { key };
1208
- switch (errors.length) {
1209
- case 1:
1210
- args.cause = errors[0]!.reason;
1211
- break;
1212
- default:
1213
- args.cause = new PluginOperationAggregateError({
1214
- plugin: "storage",
1215
- action: "read",
1216
- errors,
1217
- });
1218
- }
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);
1219
1663
 
1220
- throw new KeyNotFoundError(args);
1664
+ try {
1665
+ const c = new HasContext({
1666
+ key: resolved.key,
1667
+ store,
1668
+ signal,
1669
+ });
1670
+
1671
+ if (await leaf.storage.exists(c)) {
1672
+ return true;
1673
+ }
1674
+ } catch (ex) {
1675
+ if (signal.aborted) {
1676
+ throw ex;
1221
1677
  }
1222
1678
 
1223
- return false;
1224
- } finally {
1225
- 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}`;
1226
1686
  }
1227
- } finally {
1228
- lock.release();
1229
1687
  }
1688
+
1689
+ // すべてのストレージで存在を確認できなかったときは結果が不明なため、get() と同様にエラーとして報告します。
1690
+ if (errors.length > 0) {
1691
+ throw this.#notFound(key, errors);
1692
+ }
1693
+
1694
+ return false;
1230
1695
  }
1231
1696
 
1232
- /**
1233
- * 指定したオプションでキーを削除します。
1234
- *
1235
- * @param options 削除操作のオプションです。
1236
- * @returns 完了を通知する Promise です。
1237
- */
1238
1697
  public delete(options: DeleteOptions<KeyofKeyValueMapping<TKeyValueMapping>>): Promise<void>;
1239
-
1240
- /**
1241
- * 指定したキーを削除します。
1242
- *
1243
- * @param key 操作対象を識別するためのキーです。
1244
- * @param options 追加のオプションです。
1245
- * @returns 完了を通知する Promise です。
1246
- */
1247
1698
  public delete(
1248
1699
  key: KeyofKeyValueMapping<TKeyValueMapping>,
1249
1700
  options?: Omit<DeleteOptions, "key">,
1250
1701
  ): Promise<void>;
1251
-
1252
1702
  public async delete(...args: any): Promise<void> {
1253
1703
  if (this.#con === null) {
1254
1704
  throw new UniKvsIsNotOpenError();
1255
1705
  }
1256
1706
 
1257
- const [options] = v.parseInput(DeleteArgsSchema, args);
1258
- const { key, signal: signalOption, context: contextOption } = options;
1707
+ const [options] = parseInput(DeleteArgsSchema, args);
1708
+ const { key, vars: varsOption, signal: signalOption } = options;
1709
+
1710
+ // キーがキースキーマに一致するかを検証します。
1711
+ this.#resolveValueSchema(key);
1259
1712
 
1260
1713
  const { ac, io } = this.#con;
1261
1714
  const signal = combineSignals([ac.signal, signalOption]);
1262
1715
 
1263
- const context = mergeContext(this.#context, contextOption);
1264
- context["unikvs:action"] = "delete";
1265
- context["unikvs:key"] = key;
1716
+ const store = this.#createOperationStore(varsOption);
1266
1717
 
1267
- const lock = await asyncmux(this, signal);
1268
- try {
1269
- // ロック待機中に接続状態が変更されていないか再確認します。
1270
- if (this.#con === null) {
1271
- throw new UniKvsIsNotOpenError();
1272
- }
1718
+ using _lock = await asyncmux.readonly(this, signal);
1719
+ // ロック待機中に接続状態が変更されていないか再確認します。
1720
+ if (this.#con === null) {
1721
+ throw new UniKvsIsNotOpenError();
1722
+ }
1273
1723
 
1274
- const lock = await io.lock({ key, signal });
1275
- try {
1276
- // すべてのストレージから対象データを削除します。
1277
- // すべての処理を並列に実行し、エラーが発生した場合は集約します。
1278
- const errors: { reason: unknown }[] = [];
1279
- await Promise.all(
1280
- this.#destinations.map(async (storage) => {
1281
- try {
1282
- if (await storage.exists(context, signal, key)) {
1283
- await storage.delete(context, signal, key);
1284
- }
1285
- } catch (reason) {
1286
- errors.push({ reason });
1287
- }
1288
- }),
1289
- );
1290
- if (errors.length > 0) {
1291
- 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
+ });
1292
1769
  }
1293
- } finally {
1294
- lock.release();
1295
- }
1296
- } finally {
1297
- lock.release();
1770
+ }),
1771
+ );
1772
+
1773
+ if (errors.length > 0) {
1774
+ throw new PluginOperationAggregateError({
1775
+ action: "delete",
1776
+ errors,
1777
+ plugin: "storage",
1778
+ });
1298
1779
  }
1299
1780
  }
1300
1781
 
1301
- /**
1302
- * すべてのストレージ内のデータを全削除します。
1303
- *
1304
- * @param options クリア操作のオプションです。
1305
- * @returns 完了を通知する Promise です。
1306
- */
1307
1782
  public async clear(options?: ClearOptions): Promise<void>;
1308
-
1309
1783
  public async clear(...args: any): Promise<void> {
1310
1784
  if (this.#con === null) {
1311
1785
  throw new UniKvsIsNotOpenError();
1312
1786
  }
1313
1787
 
1314
- const [options = {}] = v.parseInput(ClearArgsSchema, args);
1315
- const { signal: signalOption, context: contextOption } = options;
1788
+ const [options = {}] = parseInput(ClearArgsSchema, args);
1789
+ const { vars: varsOption, signal: signalOption } = options;
1316
1790
 
1317
1791
  const { ac, io } = this.#con;
1318
1792
  const signal = combineSignals([ac.signal, signalOption]);
1319
1793
 
1320
- const context = mergeContext(this.#context, contextOption);
1321
- context["unikvs:action"] = "clear";
1794
+ const store = this.#createOperationStore(varsOption);
1322
1795
 
1323
- const lock = await asyncmux(this, signal);
1324
- try {
1325
- // ロック待機中に接続状態が変更されていないか再確認します。
1326
- if (this.#con === null) {
1327
- throw new UniKvsIsNotOpenError();
1328
- }
1796
+ using _lock = await asyncmux.readonly(this, signal);
1797
+ // ロック待機中に接続状態が変更されていないか再確認します。
1798
+ if (this.#con === null) {
1799
+ throw new UniKvsIsNotOpenError();
1800
+ }
1329
1801
 
1330
- const lock = await io.lock({ signal });
1331
- try {
1332
- // すべてのストレージで一括削除を実行します。
1333
- // すべての処理を並列に実行し、エラーが発生した場合は集約します。
1334
- const errors: { reason: unknown }[] = [];
1335
- await Promise.all(
1336
- this.#destinations.map(async (storage) => {
1337
- try {
1338
- await storage.clear(context, signal);
1339
- } catch (reason) {
1340
- errors.push({ reason });
1341
- }
1342
- }),
1343
- );
1344
- if (errors.length > 0) {
1345
- 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
+ });
1346
1827
  }
1347
- } finally {
1348
- lock.release();
1349
- }
1350
- } finally {
1351
- lock.release();
1828
+ }),
1829
+ );
1830
+
1831
+ if (errors.length > 0) {
1832
+ throw new PluginOperationAggregateError({
1833
+ action: "clear",
1834
+ errors,
1835
+ plugin: "storage",
1836
+ });
1352
1837
  }
1353
1838
  }
1354
1839
  }