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.
- package/README.md +2 -2
- package/dist/index.d.ts +1553 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4032 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -34
- package/src/_contexts.ts +1089 -0
- package/src/_logger.ts +5 -3
- package/src/_middleware.ts +207 -0
- package/src/_pipeline-graph.helpers.ts +183 -0
- package/src/_pipeline-graph.ts +1307 -0
- package/src/_pipeline-graph.types.ts +303 -0
- package/src/_pipeline-leaf-resolver.ts +70 -0
- package/src/_plugin-lifecycle.ts +201 -0
- package/src/_storage.ts +169 -115
- package/src/_streams.schemas.ts +21 -0
- package/src/_streams.ts +32 -6
- package/src/_to-value-stream.ts +22 -11
- package/src/_types.ts +237 -0
- package/src/_unikvs-branch.ts +94 -0
- package/src/_unikvs.schemas.ts +451 -0
- package/src/_valibot.ts +17 -6
- package/src/_value-schemas.ts +36 -206
- package/src/_value-validation.ts +57 -0
- package/src/errors.ts +391 -148
- package/src/index.ts +46 -25
- package/src/{value-stream.types.ts → types.ts} +1 -1
- package/src/unikvs-branch.ts +445 -0
- package/src/unikvs-builder.ts +706 -0
- package/src/unikvs.ts +1423 -1007
- package/src/unikvs.types.ts +194 -0
- package/src/value-schemas.ts +98 -0
- package/dist/src/_logger.d.ts +0 -3
- package/dist/src/_logger.d.ts.map +0 -1
- package/dist/src/_logger.js +0 -3
- package/dist/src/_merge-variables.d.ts +0 -13
- package/dist/src/_merge-variables.d.ts.map +0 -1
- package/dist/src/_merge-variables.js +0 -12
- package/dist/src/_storage.d.ts +0 -18
- package/dist/src/_storage.d.ts.map +0 -1
- package/dist/src/_storage.js +0 -103
- package/dist/src/_streams.d.ts +0 -20
- package/dist/src/_streams.d.ts.map +0 -1
- package/dist/src/_streams.js +0 -36
- package/dist/src/_to-value-stream.d.ts +0 -12
- package/dist/src/_to-value-stream.d.ts.map +0 -1
- package/dist/src/_to-value-stream.js +0 -102
- package/dist/src/_transformer.d.ts +0 -13
- package/dist/src/_transformer.d.ts.map +0 -1
- package/dist/src/_transformer.js +0 -60
- package/dist/src/_valibot.d.ts +0 -15
- package/dist/src/_valibot.d.ts.map +0 -1
- package/dist/src/_valibot.js +0 -23
- package/dist/src/_value-schemas.d.ts +0 -179
- package/dist/src/_value-schemas.d.ts.map +0 -1
- package/dist/src/_value-schemas.js +0 -150
- package/dist/src/errors.d.ts +0 -306
- package/dist/src/errors.d.ts.map +0 -1
- package/dist/src/errors.js +0 -234
- package/dist/src/index.d.ts +0 -10
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -3
- package/dist/src/unikvs-config.d.ts +0 -174
- package/dist/src/unikvs-config.d.ts.map +0 -1
- package/dist/src/unikvs-config.js +0 -100
- package/dist/src/unikvs.d.ts +0 -272
- package/dist/src/unikvs.d.ts.map +0 -1
- package/dist/src/unikvs.js +0 -928
- package/dist/src/utils.types.d.ts +0 -5
- package/dist/src/utils.types.d.ts.map +0 -1
- package/dist/src/utils.types.js +0 -1
- package/dist/src/value-stream.types.d.ts +0 -10
- package/dist/src/value-stream.types.d.ts.map +0 -1
- package/dist/src/value-stream.types.js +0 -1
- package/dist/src/variables.types.d.ts +0 -11
- package/dist/src/variables.types.d.ts.map +0 -1
- package/dist/src/variables.types.js +0 -1
- package/src/_merge-variables.ts +0 -19
- package/src/_transformer.ts +0 -89
- package/src/unikvs-config.ts +0 -401
- package/src/utils.types.ts +0 -4
- package/src/variables.types.ts +0 -13
package/src/unikvs.ts
CHANGED
|
@@ -1,397 +1,72 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { combineSignals } from "abort-signal-utils";
|
|
3
|
-
import { type AsyncmuxLock,
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
11
|
-
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} from "./unikvs
|
|
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
|
-
//
|
|
408
|
-
// dispose は 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
|
-
*
|
|
118
|
+
* プラグイン操作の失敗を集約するための 1 件分の記録です。
|
|
444
119
|
*/
|
|
445
|
-
|
|
120
|
+
type PluginErrorRecord = {
|
|
121
|
+
/**
|
|
122
|
+
* 失敗したプラグインの名前です。
|
|
123
|
+
*/
|
|
124
|
+
readonly name: string;
|
|
125
|
+
|
|
446
126
|
/**
|
|
447
|
-
*
|
|
127
|
+
* 宛先またはプラグインの登録順 (0 起点) です。
|
|
448
128
|
*/
|
|
449
|
-
readonly
|
|
129
|
+
readonly index: number;
|
|
450
130
|
|
|
451
131
|
/**
|
|
452
|
-
*
|
|
132
|
+
* 失敗の理由です。
|
|
453
133
|
*/
|
|
454
|
-
readonly
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* データの永続化先となるストレージと前段パイプラインのリストです。
|
|
498
|
-
*/
|
|
499
|
-
readonly #destinations: readonly [UniKvsDestination, ...UniKvsDestination[]];
|
|
153
|
+
#baseVars: readonly VariablesEntry[];
|
|
500
154
|
|
|
501
155
|
/**
|
|
502
|
-
*
|
|
156
|
+
* コンパイル済みのパイプライン・グラフです。
|
|
503
157
|
*/
|
|
504
|
-
readonly #
|
|
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
|
-
*
|
|
166
|
+
* @internal
|
|
513
167
|
*/
|
|
514
168
|
public constructor(
|
|
515
|
-
vars:
|
|
516
|
-
|
|
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.#
|
|
523
|
-
this.#
|
|
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
|
-
|
|
535
|
-
|
|
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#
|
|
192
|
+
* [API Reference](https://tai-kun.github.io/unikvs/packages/unikvs#base-vars-assign-vars)
|
|
547
193
|
*/
|
|
548
|
-
public
|
|
194
|
+
public assignVars(vars: VariablesInit): this;
|
|
195
|
+
public assignVars(...args: unknown[]): this {
|
|
196
|
+
const [vars] = parseInput(VariablesArgsSchema, args);
|
|
549
197
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
556
|
-
const { signal: signalOption, vars: varsOption } = options;
|
|
204
|
+
this.#baseVars = [...map];
|
|
557
205
|
|
|
558
|
-
|
|
559
|
-
|
|
206
|
+
return this;
|
|
207
|
+
}
|
|
560
208
|
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
216
|
+
const map = new Map(vars);
|
|
565
217
|
|
|
566
|
-
this.#
|
|
218
|
+
for (const [name, value] of this.#baseVars) {
|
|
219
|
+
map.set(name, value);
|
|
220
|
+
}
|
|
567
221
|
|
|
568
|
-
|
|
222
|
+
this.#baseVars = [...map];
|
|
569
223
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
243
|
+
return map;
|
|
244
|
+
}
|
|
577
245
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
293
|
+
break;
|
|
632
294
|
}
|
|
633
295
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
|
|
659
|
-
|
|
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
|
-
|
|
334
|
+
if (this.#con !== null) {
|
|
335
|
+
throw new UniKvsIsOpenError();
|
|
336
|
+
}
|
|
670
337
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
"
|
|
396
|
+
"middleware",
|
|
397
|
+
middleware.name,
|
|
398
|
+
index,
|
|
688
399
|
]);
|
|
689
400
|
}
|
|
690
401
|
|
|
691
402
|
// すべての処理を並列に実行し、エラーが発生した場合は集約します。
|
|
692
|
-
const errors: {
|
|
403
|
+
const errors: {
|
|
404
|
+
name: string;
|
|
405
|
+
index: number;
|
|
406
|
+
plugin: "storage" | "middleware";
|
|
407
|
+
reason: unknown;
|
|
408
|
+
}[] = [];
|
|
693
409
|
await Promise.all(
|
|
694
|
-
|
|
410
|
+
openFns.map(async ([f, plugin, name, index]) => {
|
|
695
411
|
try {
|
|
696
412
|
await f();
|
|
697
|
-
} catch (
|
|
698
|
-
errors.push({
|
|
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
|
-
|
|
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
|
-
|
|
453
|
+
throw ex;
|
|
454
|
+
}
|
|
710
455
|
} finally {
|
|
711
|
-
|
|
456
|
+
this.#acSet.delete(ac);
|
|
712
457
|
}
|
|
713
458
|
}
|
|
714
459
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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 = {}] =
|
|
723
|
-
const { signal = AbortSignal.timeout(10e3)
|
|
578
|
+
const [options = {}] = parseInput(CloseArgsSchema, args);
|
|
579
|
+
const { vars: varsOption, signal = AbortSignal.timeout(10e3) } = options;
|
|
724
580
|
|
|
725
|
-
const
|
|
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
|
-
|
|
745
|
-
|
|
746
|
-
|
|
602
|
+
|
|
603
|
+
for (const targetAc of acArr) {
|
|
604
|
+
if (!targetAc.signal.aborted) {
|
|
605
|
+
targetAc.abort(new UniKvsIsNotOpenError());
|
|
747
606
|
}
|
|
748
607
|
}
|
|
749
608
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
|
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
|
-
*
|
|
658
|
+
* 書き込み失敗の集約と、成功した宛先への通知を行います。
|
|
659
|
+
*
|
|
660
|
+
* @param targets 書き込みを試みた対象です。
|
|
661
|
+
* @param failed 失敗した葉の集合です。
|
|
662
|
+
* @param errors 集約する失敗です。
|
|
663
|
+
* @param signal 中断シグナルです。
|
|
792
664
|
*/
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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
|
-
*
|
|
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] =
|
|
812
|
-
const { key,
|
|
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
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
832
|
-
const
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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
|
|
925
|
-
|
|
926
|
-
|
|
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
|
-
|
|
941
|
-
|
|
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
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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] =
|
|
994
|
-
const {
|
|
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
|
|
1002
|
-
vars["unikvs:action"] = "get";
|
|
1003
|
-
vars["unikvs:key"] = key;
|
|
1112
|
+
const store = this.#createOperationStore(varsOption);
|
|
1004
1113
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
}
|
|
1114
|
+
using _lock = await asyncmux.readonly(this, signal);
|
|
1115
|
+
// ロック待機中に接続状態が変更されていないか再確認します。
|
|
1116
|
+
if (this.#con === null) {
|
|
1117
|
+
throw new UniKvsIsNotOpenError();
|
|
1118
|
+
}
|
|
1011
1119
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
-
|
|
1034
|
-
|
|
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
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1062
|
-
data = await transformer.decode(vars, signal, data);
|
|
1208
|
+
if (hit === null) {
|
|
1209
|
+
throw this.#notFound(key, errors);
|
|
1063
1210
|
}
|
|
1064
1211
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
data =
|
|
1212
|
+
if (repair) {
|
|
1213
|
+
// 後段で見つかったデータをすべてデコードして原データに戻します。
|
|
1214
|
+
data = await this.#repairPlain(data, hit, missing, store, key, signal, throwOnRepairDenied);
|
|
1068
1215
|
}
|
|
1216
|
+
}
|
|
1069
1217
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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<
|
|
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<
|
|
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] =
|
|
1097
|
-
const {
|
|
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
|
|
1105
|
-
vars["unikvs:action"] = "stream";
|
|
1106
|
-
vars["unikvs:key"] = key;
|
|
1265
|
+
const store = this.#createOperationStore(varsOption);
|
|
1107
1266
|
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
|
1118
|
-
const
|
|
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
|
-
|
|
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
|
|
1348
|
+
if (!(await leaf.storage.exists(existsContext))) {
|
|
1349
|
+
missing.push(leaf);
|
|
1127
1350
|
continue;
|
|
1128
1351
|
}
|
|
1129
1352
|
|
|
1130
|
-
r = await
|
|
1131
|
-
|
|
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({
|
|
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 (
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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(
|
|
1517
|
+
transform(chunk, controller): void {
|
|
1518
|
+
controller.enqueue(parseOutput(valueSchema.schema, chunk));
|
|
1174
1519
|
},
|
|
1175
1520
|
}),
|
|
1176
1521
|
);
|
|
1177
1522
|
}
|
|
1178
1523
|
|
|
1179
|
-
|
|
1180
|
-
|
|
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
|
-
|
|
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(
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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
|
|
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
|
|
1595
|
+
// getReadable の成功後にセットアップで失敗したときは、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放するためキャンセルします。
|
|
1211
1596
|
try {
|
|
1212
1597
|
await r.cancel(ex);
|
|
1213
|
-
} catch {
|
|
1598
|
+
} catch {
|
|
1599
|
+
// キャンセルに失敗しても元の例外の伝播を優先します。
|
|
1600
|
+
}
|
|
1214
1601
|
}
|
|
1215
1602
|
|
|
1216
|
-
|
|
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] =
|
|
1243
|
-
const { key,
|
|
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
|
|
1252
|
-
vars["unikvs:action"] = "has";
|
|
1253
|
-
vars["unikvs:key"] = key;
|
|
1629
|
+
const store = this.#createOperationStore(varsOption);
|
|
1254
1630
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
}
|
|
1631
|
+
using _lock = await asyncmux.readonly(this, signal);
|
|
1632
|
+
// ロック待機中に接続状態が変更されていないか再確認します。
|
|
1633
|
+
if (this.#con === null) {
|
|
1634
|
+
throw new UniKvsIsNotOpenError();
|
|
1635
|
+
}
|
|
1261
1636
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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] =
|
|
1327
|
-
const { key,
|
|
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
|
|
1336
|
-
vars["unikvs:action"] = "delete";
|
|
1337
|
-
vars["unikvs:key"] = key;
|
|
1716
|
+
const store = this.#createOperationStore(varsOption);
|
|
1338
1717
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
}
|
|
1718
|
+
using _lock = await asyncmux.readonly(this, signal);
|
|
1719
|
+
// ロック待機中に接続状態が変更されていないか再確認します。
|
|
1720
|
+
if (this.#con === null) {
|
|
1721
|
+
throw new UniKvsIsNotOpenError();
|
|
1722
|
+
}
|
|
1345
1723
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
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
|
-
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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 = {}] =
|
|
1384
|
-
const {
|
|
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
|
|
1390
|
-
vars["unikvs:action"] = "clear";
|
|
1794
|
+
const store = this.#createOperationStore(varsOption);
|
|
1391
1795
|
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
}
|
|
1796
|
+
using _lock = await asyncmux.readonly(this, signal);
|
|
1797
|
+
// ロック待機中に接続状態が変更されていないか再確認します。
|
|
1798
|
+
if (this.#con === null) {
|
|
1799
|
+
throw new UniKvsIsNotOpenError();
|
|
1800
|
+
}
|
|
1398
1801
|
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
-
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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
|
}
|