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