unikvs 0.0.13 → 0.0.14
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/dist/index.d.ts +84 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +103 -68
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/_middleware.ts +5 -5
- package/src/_pipeline-graph.helpers.ts +2 -2
- package/src/_storage.ts +8 -8
- package/src/_streams.schemas.ts +3 -3
- package/src/_unikvs-branch.ts +16 -24
- package/src/_unikvs.schemas.ts +71 -85
- package/src/_value-schemas.ts +7 -8
- package/src/_value-validation.ts +11 -11
- package/src/errors.ts +34 -0
- package/src/index.ts +2 -0
- package/src/unikvs-branch.ts +22 -15
- package/src/unikvs-builder.ts +121 -34
- package/src/unikvs.ts +982 -982
- package/src/unikvs.types.ts +4 -4
- package/src/value-schemas.ts +14 -14
package/src/unikvs.ts
CHANGED
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
StreamArgsSchema,
|
|
34
34
|
VariablesArgsSchema,
|
|
35
35
|
} from "./_unikvs.schemas.js";
|
|
36
|
-
import
|
|
36
|
+
import * as v from "./_valibot.js";
|
|
37
37
|
import type {
|
|
38
38
|
ValueSchemaInfo,
|
|
39
39
|
$InferPlainValueData,
|
|
@@ -182,7 +182,7 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
|
|
|
182
182
|
*/
|
|
183
183
|
public setVars(vars: VariablesInit): this;
|
|
184
184
|
public setVars(...args: unknown[]): this {
|
|
185
|
-
const [vars] = parseInput(VariablesArgsSchema, args);
|
|
185
|
+
const [vars] = v.parseInput(VariablesArgsSchema, args);
|
|
186
186
|
this.#baseVars = vars;
|
|
187
187
|
|
|
188
188
|
return this;
|
|
@@ -193,7 +193,7 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
|
|
|
193
193
|
*/
|
|
194
194
|
public assignVars(vars: VariablesInit): this;
|
|
195
195
|
public assignVars(...args: unknown[]): this {
|
|
196
|
-
const [vars] = parseInput(VariablesArgsSchema, args);
|
|
196
|
+
const [vars] = v.parseInput(VariablesArgsSchema, args);
|
|
197
197
|
|
|
198
198
|
const map = new Map(this.#baseVars);
|
|
199
199
|
|
|
@@ -211,7 +211,7 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
|
|
|
211
211
|
*/
|
|
212
212
|
public defaultsVars(vars: VariablesInit): this;
|
|
213
213
|
public defaultsVars(...args: unknown[]): this {
|
|
214
|
-
const [vars] = parseInput(VariablesArgsSchema, args);
|
|
214
|
+
const [vars] = v.parseInput(VariablesArgsSchema, args);
|
|
215
215
|
|
|
216
216
|
const map = new Map(vars);
|
|
217
217
|
|
|
@@ -224,87 +224,6 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
|
|
|
224
224
|
return this;
|
|
225
225
|
}
|
|
226
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);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return map;
|
|
244
|
-
}
|
|
245
|
-
|
|
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
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* キーに対応する値のスキーマ情報を解決します。
|
|
261
|
-
*
|
|
262
|
-
* @param key 解決するキーです。
|
|
263
|
-
*
|
|
264
|
-
* @returns 対応するスキーマ情報を返します。スキーマ未設定または対応する定義がない場合は `undefined` を返します。
|
|
265
|
-
*/
|
|
266
|
-
#resolveValueSchema(key: string): ValueSchemaInfo | undefined {
|
|
267
|
-
return this.#valueSchemaResolver?.resolve(key);
|
|
268
|
-
}
|
|
269
|
-
|
|
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 };
|
|
280
|
-
|
|
281
|
-
switch (errors.length) {
|
|
282
|
-
case 0: {
|
|
283
|
-
break;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
case 1: {
|
|
287
|
-
const [firstError] = errors;
|
|
288
|
-
|
|
289
|
-
if (firstError !== undefined) {
|
|
290
|
-
args.cause = firstError.reason;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
break;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
default: {
|
|
297
|
-
args.cause = new PluginOperationAggregateError({
|
|
298
|
-
action: "read",
|
|
299
|
-
errors,
|
|
300
|
-
plugin: "storage",
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return new KeyNotFoundError(args);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
227
|
public get isOpen(): boolean {
|
|
309
228
|
return this.#con !== null;
|
|
310
229
|
}
|
|
@@ -315,7 +234,7 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
|
|
|
315
234
|
throw new UniKvsIsOpenError();
|
|
316
235
|
}
|
|
317
236
|
|
|
318
|
-
const [options = {}] = parseInput(OpenArgsSchema, args);
|
|
237
|
+
const [options = {}] = v.parseInput(OpenArgsSchema, args);
|
|
319
238
|
const { vars: varsOption, signal: signalOption } = options;
|
|
320
239
|
|
|
321
240
|
const ac = new AbortController();
|
|
@@ -457,125 +376,10 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
|
|
|
457
376
|
}
|
|
458
377
|
}
|
|
459
378
|
|
|
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
|
-
}
|
|
574
|
-
|
|
575
379
|
public close(options?: CloseOptions): Promise<void>;
|
|
576
380
|
public close(...args: any): Promise<void> {
|
|
577
381
|
try {
|
|
578
|
-
const [options = {}] = parseInput(CloseArgsSchema, args);
|
|
382
|
+
const [options = {}] = v.parseInput(CloseArgsSchema, args);
|
|
579
383
|
const { vars: varsOption, signal = AbortSignal.timeout(10e3) } = options;
|
|
580
384
|
|
|
581
385
|
const operationVars = this.#createOperationVars(varsOption);
|
|
@@ -644,7 +448,7 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
|
|
|
644
448
|
}
|
|
645
449
|
}
|
|
646
450
|
|
|
647
|
-
async [Symbol.asyncDispose](): Promise<void> {
|
|
451
|
+
public async [Symbol.asyncDispose](): Promise<void> {
|
|
648
452
|
if (!this.isOpen) {
|
|
649
453
|
return;
|
|
650
454
|
}
|
|
@@ -654,664 +458,303 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
|
|
|
654
458
|
});
|
|
655
459
|
}
|
|
656
460
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
async
|
|
666
|
-
|
|
667
|
-
|
|
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;
|
|
461
|
+
public set<const TKey extends KeyofKeyValueMapping<TKeyValueMapping>>(
|
|
462
|
+
options: SetOptions<TKeyValueMapping, TKey>,
|
|
463
|
+
): Promise<void>;
|
|
464
|
+
public set<const TKey extends KeyofKeyValueMapping<TKeyValueMapping>>(
|
|
465
|
+
key: TKey,
|
|
466
|
+
value: SetValue<TKeyValueMapping[TKey]>,
|
|
467
|
+
options?: Omit<SetOptions, "key" | "value">,
|
|
468
|
+
): Promise<void>;
|
|
469
|
+
public async set(...args: any): Promise<void> {
|
|
470
|
+
if (this.#con === null) {
|
|
471
|
+
throw new UniKvsIsNotOpenError();
|
|
677
472
|
}
|
|
678
473
|
|
|
679
|
-
const
|
|
680
|
-
|
|
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
|
-
}
|
|
474
|
+
const [options] = v.parseInput(SetArgsSchema, args);
|
|
475
|
+
const { key, vars: varsOption, signal: signalOption } = options;
|
|
691
476
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
store,
|
|
696
|
-
signal,
|
|
697
|
-
});
|
|
698
|
-
await target.leaf.storage.onOtherWriteError(c, error);
|
|
699
|
-
} catch (ex) {
|
|
700
|
-
notifyErrors.push(ex);
|
|
701
|
-
}
|
|
702
|
-
}),
|
|
703
|
-
);
|
|
477
|
+
const valueSchema = this.#resolveValueSchema(key);
|
|
478
|
+
// スキーマ検証はロック取得前に済ませ、無効な入力ではロックを取得しません。
|
|
479
|
+
const value = valueSchema ? parseSetValue(valueSchema, options.value) : options.value;
|
|
704
480
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
}
|
|
481
|
+
const { ac, io } = this.#con;
|
|
482
|
+
const signal = combineSignals([ac.signal, signalOption]);
|
|
708
483
|
|
|
709
|
-
|
|
710
|
-
}
|
|
484
|
+
const store = this.#createOperationStore(varsOption);
|
|
711
485
|
|
|
712
|
-
|
|
713
|
-
|
|
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` コンテキストです。
|
|
722
|
-
*/
|
|
723
|
-
async #use(
|
|
724
|
-
action: "read" | "write" | "delete" | "clear",
|
|
725
|
-
c: IMiddleware.UseContext,
|
|
726
|
-
): Promise<void> {
|
|
727
|
-
throwIfAborted(c.signal);
|
|
486
|
+
// `#setStream` へ所有権を渡す前の失敗では、ソースをロックしたままにしないよう派生ストリームをキャンセルします。
|
|
487
|
+
const stream = isReadableStream(value) ? value : null;
|
|
488
|
+
let ownsStream = stream !== null;
|
|
728
489
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
490
|
+
try {
|
|
491
|
+
using _lock = await asyncmux.readonly(this, signal);
|
|
492
|
+
// ロック待機中に接続状態が変更されていないか再確認します。
|
|
493
|
+
if (this.#con === null) {
|
|
494
|
+
throw new UniKvsIsNotOpenError();
|
|
495
|
+
}
|
|
732
496
|
|
|
733
|
-
|
|
734
|
-
|
|
497
|
+
// 書き込み前の `use` による検証を行います。変異は後続のエンコード・書き込みに引き継がれます。
|
|
498
|
+
const useContext = new SetContext({
|
|
499
|
+
key,
|
|
500
|
+
store,
|
|
501
|
+
signal,
|
|
502
|
+
});
|
|
503
|
+
await this.#use("write", useContext);
|
|
504
|
+
// `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
|
|
505
|
+
throwIfAborted(signal);
|
|
506
|
+
// `use` による変異を操作変数に反映します。
|
|
507
|
+
// ロックはインスタンス (読み取り) からキーの順で取得し、逆順で解放します。
|
|
508
|
+
if (stream !== null) {
|
|
509
|
+
// これ以降の後始末は `#setStream` が所有します。
|
|
510
|
+
ownsStream = false;
|
|
511
|
+
await this.#setStream(io, signal, key, store, stream);
|
|
512
|
+
} else {
|
|
513
|
+
await this.#setPlain(io, signal, key, store, value);
|
|
514
|
+
}
|
|
515
|
+
} catch (ex) {
|
|
516
|
+
if (ownsStream && stream !== null) {
|
|
517
|
+
// 消費されないストリームをキャンセルし、ソースの解放を伝播させます。
|
|
518
|
+
try {
|
|
519
|
+
await stream.cancel(ex);
|
|
520
|
+
} catch {
|
|
521
|
+
// キャンセルに失敗しても元の例外の伝播を優先します。
|
|
522
|
+
}
|
|
735
523
|
}
|
|
736
524
|
|
|
737
|
-
|
|
525
|
+
throw ex;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
738
528
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
529
|
+
public get<const TKey extends KeyofKeyValueMappingHasPlainValue<TKeyValueMapping>>(
|
|
530
|
+
options: GetOptions<TKey>,
|
|
531
|
+
): Promise<$InferPlainValueData<TKeyValueMapping[TKey]>>;
|
|
532
|
+
public get<const TKey extends KeyofKeyValueMappingHasPlainValue<TKeyValueMapping>>(
|
|
533
|
+
key: TKey,
|
|
534
|
+
options?: Omit<GetOptions, "key">,
|
|
535
|
+
): Promise<$InferPlainValueData<TKeyValueMapping[TKey]>>;
|
|
536
|
+
public async get(...args: any): Promise<unknown> {
|
|
537
|
+
if (this.#con === null) {
|
|
538
|
+
throw new UniKvsIsNotOpenError();
|
|
539
|
+
}
|
|
743
540
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
);
|
|
753
|
-
// 呼び出し側が切り離した場合の未処理 rejection を防ぎます。await したときは従来どおり拒否されます。
|
|
754
|
-
// oxlint-disable-next-line promise/prefer-await-to-then
|
|
755
|
-
rejection.catch(() => {});
|
|
541
|
+
const [options] = v.parseInput(GetArgsSchema, args);
|
|
542
|
+
const {
|
|
543
|
+
key,
|
|
544
|
+
vars: varsOption,
|
|
545
|
+
repair = false,
|
|
546
|
+
signal: signalOption,
|
|
547
|
+
throwOnRepairDenied = false,
|
|
548
|
+
} = options;
|
|
756
549
|
|
|
757
|
-
|
|
758
|
-
}
|
|
550
|
+
const valueSchema = this.#resolveValueSchema(key);
|
|
759
551
|
|
|
760
|
-
|
|
552
|
+
const { ac, io } = this.#con;
|
|
553
|
+
const signal = combineSignals([ac.signal, signalOption]);
|
|
761
554
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
555
|
+
const store = this.#createOperationStore(varsOption);
|
|
556
|
+
|
|
557
|
+
using _lock = await asyncmux.readonly(this, signal);
|
|
558
|
+
// ロック待機中に接続状態が変更されていないか再確認します。
|
|
559
|
+
if (this.#con === null) {
|
|
560
|
+
throw new UniKvsIsNotOpenError();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// 読み取り前の `use` による検証を行います。書き戻し要求は `repair` フィールドで伝えます。
|
|
564
|
+
const leafNames = this.#pipeline.leaves.map((leaf) => leaf.name);
|
|
565
|
+
const useContext = new GetContext({
|
|
566
|
+
key,
|
|
567
|
+
store,
|
|
568
|
+
repair: repair
|
|
569
|
+
? {
|
|
570
|
+
enabled: true,
|
|
571
|
+
targets: leafNames as [string, ...string[]],
|
|
772
572
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
573
|
+
: {
|
|
574
|
+
enabled: false,
|
|
575
|
+
targets: [],
|
|
576
|
+
},
|
|
577
|
+
signal,
|
|
578
|
+
});
|
|
579
|
+
await this.#use("read", useContext);
|
|
580
|
+
// `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
|
|
581
|
+
throwIfAborted(signal);
|
|
582
|
+
let data: unknown = undefined;
|
|
583
|
+
let hit: ResolvedLeaf | null = null;
|
|
584
|
+
const missing: PipelineLeaf[] = [];
|
|
585
|
+
const errors: PluginErrorRecord[] = [];
|
|
777
586
|
|
|
778
|
-
|
|
779
|
-
|
|
587
|
+
// 書き戻し時は、読み取りから書き込みまでを同じキーの書き込みと直列化するため、書き込みロックを取得します。
|
|
588
|
+
{
|
|
589
|
+
using _ioLock = await (repair
|
|
590
|
+
? io.lock({
|
|
591
|
+
key,
|
|
592
|
+
signal,
|
|
593
|
+
})
|
|
594
|
+
: io.rLock({
|
|
595
|
+
key,
|
|
596
|
+
signal,
|
|
597
|
+
}));
|
|
598
|
+
// 登録順が読み取りの優先順です。先頭で見つかればそこで終え、失敗したときは次の宛先を試します。
|
|
599
|
+
const resolver = this.#pipeline.createResolver({
|
|
600
|
+
key,
|
|
601
|
+
store,
|
|
602
|
+
signal,
|
|
603
|
+
});
|
|
780
604
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
605
|
+
for (const leaf of this.#pipeline) {
|
|
606
|
+
const resolved = resolver.resolve(leaf);
|
|
607
|
+
const existsContext = new HasContext({
|
|
608
|
+
key: resolved.key,
|
|
609
|
+
store,
|
|
610
|
+
signal,
|
|
611
|
+
});
|
|
612
|
+
const readRepair = repair
|
|
613
|
+
? {
|
|
614
|
+
enabled: true as const,
|
|
615
|
+
targets: leafNames as [string, ...string[]],
|
|
616
|
+
}
|
|
617
|
+
: {
|
|
618
|
+
enabled: false as const,
|
|
619
|
+
targets: [] as const,
|
|
620
|
+
};
|
|
621
|
+
const readContext = new GetContext({
|
|
622
|
+
key: resolved.key,
|
|
623
|
+
store,
|
|
624
|
+
repair: readRepair,
|
|
625
|
+
signal,
|
|
626
|
+
});
|
|
790
627
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
} catch (ex) {
|
|
798
|
-
// 中断による失敗はそのまま投げます。
|
|
799
|
-
if (c.signal.aborted && ex === c.signal.reason) {
|
|
800
|
-
throw ex;
|
|
801
|
-
}
|
|
628
|
+
try {
|
|
629
|
+
if (!(await leaf.storage.exists(existsContext))) {
|
|
630
|
+
missing.push(leaf);
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
802
633
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
634
|
+
data = await leaf.storage.read(readContext);
|
|
635
|
+
hit = resolved;
|
|
636
|
+
break;
|
|
637
|
+
} catch (ex) {
|
|
638
|
+
if (signal.aborted) {
|
|
639
|
+
throw ex;
|
|
640
|
+
}
|
|
811
641
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
642
|
+
errors.push({
|
|
643
|
+
name: leaf.name,
|
|
644
|
+
index: leaf.index,
|
|
645
|
+
reason: ex,
|
|
646
|
+
});
|
|
647
|
+
logger.error`Failed to read from a storage: ${ex}`;
|
|
815
648
|
}
|
|
649
|
+
}
|
|
816
650
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
errors: [
|
|
820
|
-
{
|
|
821
|
-
name: middleware.name,
|
|
822
|
-
index,
|
|
823
|
-
reason: ex,
|
|
824
|
-
},
|
|
825
|
-
],
|
|
826
|
-
plugin: "middleware",
|
|
827
|
-
});
|
|
651
|
+
if (hit === null) {
|
|
652
|
+
throw this.#notFound(key, errors);
|
|
828
653
|
}
|
|
829
|
-
};
|
|
830
|
-
await dispatch(0);
|
|
831
|
-
}
|
|
832
654
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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;
|
|
655
|
+
if (repair) {
|
|
656
|
+
// 後段で見つかったデータをすべてデコードして原データに戻します。
|
|
657
|
+
data = await this.#repairPlain(data, hit, missing, store, key, signal, throwOnRepairDenied);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
903
660
|
|
|
904
|
-
|
|
905
|
-
|
|
661
|
+
if (!repair) {
|
|
662
|
+
// 見つかったストレージ専用の前段パイプラインを逆順に適用して、データをデコードします。
|
|
663
|
+
data = await this.#pipeline.decode(
|
|
664
|
+
hit.leaf,
|
|
906
665
|
{
|
|
907
666
|
key,
|
|
908
667
|
store,
|
|
909
668
|
signal,
|
|
910
669
|
},
|
|
911
|
-
|
|
912
|
-
streams,
|
|
670
|
+
data,
|
|
913
671
|
);
|
|
914
|
-
} catch (ex) {
|
|
915
|
-
await cancelStreams(streams, ex);
|
|
916
|
-
throw ex;
|
|
917
672
|
}
|
|
918
673
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
-
}
|
|
674
|
+
// デコード後のデータを検証します。
|
|
675
|
+
if (valueSchema) {
|
|
676
|
+
data = v.parseOutput(valueSchema.schema, data);
|
|
677
|
+
}
|
|
950
678
|
|
|
951
|
-
|
|
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);
|
|
679
|
+
return data;
|
|
961
680
|
}
|
|
962
681
|
|
|
963
|
-
public
|
|
964
|
-
options:
|
|
965
|
-
): Promise<
|
|
966
|
-
public
|
|
682
|
+
public stream<const TKey extends KeyofKeyValueMappingHasStreamValue<TKeyValueMapping>>(
|
|
683
|
+
options: StreamOptions<TKey>,
|
|
684
|
+
): Promise<IValueStream<$InferStreamValueChunkData<TKeyValueMapping[TKey]>>>;
|
|
685
|
+
public stream<const TKey extends KeyofKeyValueMappingHasStreamValue<TKeyValueMapping>>(
|
|
967
686
|
key: TKey,
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
): Promise<
|
|
971
|
-
public async set(...args: any): Promise<void> {
|
|
687
|
+
options?: Omit<StreamOptions, "key">,
|
|
688
|
+
): Promise<IValueStream<$InferStreamValueChunkData<TKeyValueMapping[TKey]>>>;
|
|
689
|
+
public async stream(...args: any): Promise<IValueStream> {
|
|
972
690
|
if (this.#con === null) {
|
|
973
691
|
throw new UniKvsIsNotOpenError();
|
|
974
692
|
}
|
|
975
693
|
|
|
976
|
-
const [options] = parseInput(
|
|
977
|
-
const {
|
|
694
|
+
const [options] = v.parseInput(StreamArgsSchema, args);
|
|
695
|
+
const {
|
|
696
|
+
key,
|
|
697
|
+
vars: varsOption,
|
|
698
|
+
repair = false,
|
|
699
|
+
signal: signalOption,
|
|
700
|
+
throwOnRepairDenied = false,
|
|
701
|
+
} = options;
|
|
978
702
|
|
|
979
703
|
const valueSchema = this.#resolveValueSchema(key);
|
|
980
|
-
// スキーマ検証はロック取得前に済ませ、無効な入力ではロックを取得しません。
|
|
981
|
-
const value = valueSchema ? parseSetValue(valueSchema, options.value) : options.value;
|
|
982
704
|
|
|
983
705
|
const { ac, io } = this.#con;
|
|
984
706
|
const signal = combineSignals([ac.signal, signalOption]);
|
|
985
707
|
|
|
986
708
|
const store = this.#createOperationStore(varsOption);
|
|
987
709
|
|
|
988
|
-
|
|
989
|
-
const stream = isReadableStream(value) ? value : null;
|
|
990
|
-
let ownsStream = stream !== null;
|
|
991
|
-
|
|
992
|
-
try {
|
|
710
|
+
{
|
|
993
711
|
using _lock = await asyncmux.readonly(this, signal);
|
|
994
712
|
// ロック待機中に接続状態が変更されていないか再確認します。
|
|
995
713
|
if (this.#con === null) {
|
|
996
714
|
throw new UniKvsIsNotOpenError();
|
|
997
715
|
}
|
|
998
716
|
|
|
999
|
-
//
|
|
1000
|
-
const
|
|
717
|
+
// 読み取り前の `use` による検証を行います。書き戻し要求は `repair` フィールドで伝えます。
|
|
718
|
+
const leafNames = this.#pipeline.leaves.map((leaf) => leaf.name);
|
|
719
|
+
const useContext = new StreamContext({
|
|
1001
720
|
key,
|
|
1002
721
|
store,
|
|
722
|
+
repair: repair
|
|
723
|
+
? {
|
|
724
|
+
enabled: true,
|
|
725
|
+
targets: leafNames as [string, ...string[]],
|
|
726
|
+
}
|
|
727
|
+
: {
|
|
728
|
+
enabled: false,
|
|
729
|
+
targets: [],
|
|
730
|
+
},
|
|
1003
731
|
signal,
|
|
1004
732
|
});
|
|
1005
|
-
await this.#use("
|
|
733
|
+
await this.#use("read", useContext);
|
|
1006
734
|
// `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
|
|
1007
735
|
throwIfAborted(signal);
|
|
1008
|
-
//
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
if (ownsStream && stream !== null) {
|
|
1019
|
-
// 消費されないストリームをキャンセルし、ソースの解放を伝播させます。
|
|
1020
|
-
try {
|
|
1021
|
-
await stream.cancel(ex);
|
|
1022
|
-
} catch {
|
|
1023
|
-
// キャンセルに失敗しても元の例外の伝播を優先します。
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
736
|
+
// NONE は getReadable の呼び出し前を表す番兵値です。
|
|
737
|
+
const NONE: any = {};
|
|
738
|
+
let r: IReadableStream = NONE;
|
|
739
|
+
let hit: ResolvedLeaf | null = null;
|
|
740
|
+
const missing: PipelineLeaf[] = [];
|
|
741
|
+
const errors: PluginErrorRecord[] = [];
|
|
742
|
+
const fillBranches: IReadableStream[] = [];
|
|
743
|
+
const fillTasks: Promise<void>[] = [];
|
|
744
|
+
let repairAc: AbortController | null = null;
|
|
745
|
+
let cancelFills: ((reason: unknown) => Promise<void>) | null = null;
|
|
1026
746
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
-
);
|
|
1080
|
-
|
|
1081
|
-
throwIfAborted(signal);
|
|
1082
|
-
|
|
1083
|
-
return raw;
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
public get<const TKey extends KeyofKeyValueMappingHasPlainValue<TKeyValueMapping>>(
|
|
1087
|
-
options: GetOptions<TKey>,
|
|
1088
|
-
): Promise<$InferPlainValueData<TKeyValueMapping[TKey]>>;
|
|
1089
|
-
public get<const TKey extends KeyofKeyValueMappingHasPlainValue<TKeyValueMapping>>(
|
|
1090
|
-
key: TKey,
|
|
1091
|
-
options?: Omit<GetOptions, "key">,
|
|
1092
|
-
): Promise<$InferPlainValueData<TKeyValueMapping[TKey]>>;
|
|
1093
|
-
public async get(...args: any): Promise<unknown> {
|
|
1094
|
-
if (this.#con === null) {
|
|
1095
|
-
throw new UniKvsIsNotOpenError();
|
|
1096
|
-
}
|
|
1097
|
-
|
|
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);
|
|
1108
|
-
|
|
1109
|
-
const { ac, io } = this.#con;
|
|
1110
|
-
const signal = combineSignals([ac.signal, signalOption]);
|
|
1111
|
-
|
|
1112
|
-
const store = this.#createOperationStore(varsOption);
|
|
1113
|
-
|
|
1114
|
-
using _lock = await asyncmux.readonly(this, signal);
|
|
1115
|
-
// ロック待機中に接続状態が変更されていないか再確認します。
|
|
1116
|
-
if (this.#con === null) {
|
|
1117
|
-
throw new UniKvsIsNotOpenError();
|
|
1118
|
-
}
|
|
1119
|
-
|
|
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
|
-
});
|
|
1161
|
-
|
|
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[]],
|
|
1173
|
-
}
|
|
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;
|
|
1189
|
-
}
|
|
1190
|
-
|
|
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}`;
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
if (hit === null) {
|
|
1209
|
-
throw this.#notFound(key, errors);
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
if (repair) {
|
|
1213
|
-
// 後段で見つかったデータをすべてデコードして原データに戻します。
|
|
1214
|
-
data = await this.#repairPlain(data, hit, missing, store, key, signal, throwOnRepairDenied);
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
|
|
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);
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
return data;
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
public stream<const TKey extends KeyofKeyValueMappingHasStreamValue<TKeyValueMapping>>(
|
|
1240
|
-
options: StreamOptions<TKey>,
|
|
1241
|
-
): Promise<IValueStream<$InferStreamValueChunkData<TKeyValueMapping[TKey]>>>;
|
|
1242
|
-
public stream<const TKey extends KeyofKeyValueMappingHasStreamValue<TKeyValueMapping>>(
|
|
1243
|
-
key: TKey,
|
|
1244
|
-
options?: Omit<StreamOptions, "key">,
|
|
1245
|
-
): Promise<IValueStream<$InferStreamValueChunkData<TKeyValueMapping[TKey]>>>;
|
|
1246
|
-
public async stream(...args: any): Promise<IValueStream> {
|
|
1247
|
-
if (this.#con === null) {
|
|
1248
|
-
throw new UniKvsIsNotOpenError();
|
|
1249
|
-
}
|
|
1250
|
-
|
|
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);
|
|
1261
|
-
|
|
1262
|
-
const { ac, io } = this.#con;
|
|
1263
|
-
const signal = combineSignals([ac.signal, signalOption]);
|
|
1264
|
-
|
|
1265
|
-
const store = this.#createOperationStore(varsOption);
|
|
1266
|
-
|
|
1267
|
-
{
|
|
1268
|
-
using _lock = await asyncmux.readonly(this, signal);
|
|
1269
|
-
// ロック待機中に接続状態が変更されていないか再確認します。
|
|
1270
|
-
if (this.#con === null) {
|
|
1271
|
-
throw new UniKvsIsNotOpenError();
|
|
1272
|
-
}
|
|
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 の呼び出し前を表す番兵値です。
|
|
1294
|
-
const NONE: any = {};
|
|
1295
|
-
let r: IReadableStream = NONE;
|
|
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
|
-
}));
|
|
747
|
+
// 書き戻し時は、読み取りから書き込みまでを同じキーの書き込みと直列化するため、書き込みロックを取得します。
|
|
748
|
+
// キーロックは ValueStream の dispose まで保持するため、明示的に解放します。
|
|
749
|
+
const ioLock = await (repair
|
|
750
|
+
? io.lock({
|
|
751
|
+
key,
|
|
752
|
+
signal,
|
|
753
|
+
})
|
|
754
|
+
: io.rLock({
|
|
755
|
+
key,
|
|
756
|
+
signal,
|
|
757
|
+
}));
|
|
1315
758
|
|
|
1316
759
|
try {
|
|
1317
760
|
// 登録順が読み取りの優先順です。先頭で見つかればそこで終え、失敗したときは次の宛先を試します。
|
|
@@ -1515,7 +958,7 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
|
|
|
1515
958
|
r = r.pipeThrough(
|
|
1516
959
|
new TransformStream({
|
|
1517
960
|
transform(chunk, controller): void {
|
|
1518
|
-
controller.enqueue(parseOutput(valueSchema.schema, chunk));
|
|
961
|
+
controller.enqueue(v.parseOutput(valueSchema.schema, chunk));
|
|
1519
962
|
},
|
|
1520
963
|
}),
|
|
1521
964
|
);
|
|
@@ -1591,249 +1034,806 @@ export default class UniKvs<TKeyValueMapping extends KeyValueMapping = KeyValueM
|
|
|
1591
1034
|
} catch (ex) {
|
|
1592
1035
|
await cancelFills?.(ex);
|
|
1593
1036
|
|
|
1594
|
-
if (r !== NONE) {
|
|
1595
|
-
// getReadable の成功後にセットアップで失敗したときは、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放するためキャンセルします。
|
|
1037
|
+
if (r !== NONE) {
|
|
1038
|
+
// getReadable の成功後にセットアップで失敗したときは、ソースストリームが保持するリソース (S3 レスポンスボディなど) を解放するためキャンセルします。
|
|
1039
|
+
try {
|
|
1040
|
+
await r.cancel(ex);
|
|
1041
|
+
} catch {
|
|
1042
|
+
// キャンセルに失敗しても元の例外の伝播を優先します。
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
ioLock.release();
|
|
1047
|
+
|
|
1048
|
+
throw ex;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
public has(options: HasOptions<KeyofKeyValueMapping<TKeyValueMapping>>): Promise<boolean>;
|
|
1054
|
+
public has(
|
|
1055
|
+
key: KeyofKeyValueMapping<TKeyValueMapping>,
|
|
1056
|
+
options?: Omit<HasOptions, "key">,
|
|
1057
|
+
): Promise<boolean>;
|
|
1058
|
+
public async has(...args: any): Promise<boolean> {
|
|
1059
|
+
if (this.#con === null) {
|
|
1060
|
+
throw new UniKvsIsNotOpenError();
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const [options] = v.parseInput(HasArgsSchema, args);
|
|
1064
|
+
const { key, vars: varsOption, signal: signalOption } = options;
|
|
1065
|
+
|
|
1066
|
+
// キーがキースキーマに一致するかを検証します。
|
|
1067
|
+
this.#resolveValueSchema(key);
|
|
1068
|
+
|
|
1069
|
+
const { ac, io } = this.#con;
|
|
1070
|
+
const signal = combineSignals([ac.signal, signalOption]);
|
|
1071
|
+
|
|
1072
|
+
const store = this.#createOperationStore(varsOption);
|
|
1073
|
+
|
|
1074
|
+
using _lock = await asyncmux.readonly(this, signal);
|
|
1075
|
+
// ロック待機中に接続状態が変更されていないか再確認します。
|
|
1076
|
+
if (this.#con === null) {
|
|
1077
|
+
throw new UniKvsIsNotOpenError();
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// 存在確認に先立ち、各ミドルウェアの `use` を実行します。
|
|
1081
|
+
// `use` による vars の変更は後続の解決・走査に引き継がれます。
|
|
1082
|
+
const useContext = new HasContext({
|
|
1083
|
+
key,
|
|
1084
|
+
store,
|
|
1085
|
+
signal,
|
|
1086
|
+
});
|
|
1087
|
+
await this.#use("read", useContext);
|
|
1088
|
+
// `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
|
|
1089
|
+
throwIfAborted(signal);
|
|
1090
|
+
|
|
1091
|
+
using _ioLock = await io.rLock({
|
|
1092
|
+
key,
|
|
1093
|
+
signal,
|
|
1094
|
+
});
|
|
1095
|
+
// 登録順に走査し、いずれかのストレージに存在すれば true を返します。
|
|
1096
|
+
// exists に失敗したときは次のストレージを試します。
|
|
1097
|
+
const errors: PluginErrorRecord[] = [];
|
|
1098
|
+
const resolver = this.#pipeline.createResolver({
|
|
1099
|
+
key,
|
|
1100
|
+
store,
|
|
1101
|
+
signal,
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
for (const leaf of this.#pipeline) {
|
|
1105
|
+
const resolved = resolver.resolve(leaf);
|
|
1106
|
+
|
|
1107
|
+
try {
|
|
1108
|
+
const c = new HasContext({
|
|
1109
|
+
key: resolved.key,
|
|
1110
|
+
store,
|
|
1111
|
+
signal,
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
if (await leaf.storage.exists(c)) {
|
|
1115
|
+
return true;
|
|
1116
|
+
}
|
|
1117
|
+
} catch (ex) {
|
|
1118
|
+
if (signal.aborted) {
|
|
1119
|
+
throw ex;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
errors.push({
|
|
1123
|
+
name: leaf.name,
|
|
1124
|
+
index: leaf.index,
|
|
1125
|
+
reason: ex,
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
logger.error`Failed to check existence in a storage: ${ex}`;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// すべてのストレージで存在を確認できなかったときは結果が不明なため、get() と同様にエラーとして報告します。
|
|
1133
|
+
if (errors.length > 0) {
|
|
1134
|
+
throw this.#notFound(key, errors);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
public delete(options: DeleteOptions<KeyofKeyValueMapping<TKeyValueMapping>>): Promise<void>;
|
|
1141
|
+
public delete(
|
|
1142
|
+
key: KeyofKeyValueMapping<TKeyValueMapping>,
|
|
1143
|
+
options?: Omit<DeleteOptions, "key">,
|
|
1144
|
+
): Promise<void>;
|
|
1145
|
+
public async delete(...args: any): Promise<void> {
|
|
1146
|
+
if (this.#con === null) {
|
|
1147
|
+
throw new UniKvsIsNotOpenError();
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const [options] = v.parseInput(DeleteArgsSchema, args);
|
|
1151
|
+
const { key, vars: varsOption, signal: signalOption } = options;
|
|
1152
|
+
|
|
1153
|
+
// キーがキースキーマに一致するかを検証します。
|
|
1154
|
+
this.#resolveValueSchema(key);
|
|
1155
|
+
|
|
1156
|
+
const { ac, io } = this.#con;
|
|
1157
|
+
const signal = combineSignals([ac.signal, signalOption]);
|
|
1158
|
+
|
|
1159
|
+
const store = this.#createOperationStore(varsOption);
|
|
1160
|
+
|
|
1161
|
+
using _lock = await asyncmux.readonly(this, signal);
|
|
1162
|
+
// ロック待機中に接続状態が変更されていないか再確認します。
|
|
1163
|
+
if (this.#con === null) {
|
|
1164
|
+
throw new UniKvsIsNotOpenError();
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// 削除に先立ち、各ミドルウェアの `use` を実行します。
|
|
1168
|
+
// `use` による vars の変更は後続の解決・削除に引き継がれます。
|
|
1169
|
+
const useContext = new DeleteContext({
|
|
1170
|
+
key,
|
|
1171
|
+
store,
|
|
1172
|
+
signal,
|
|
1173
|
+
});
|
|
1174
|
+
await this.#use("delete", useContext);
|
|
1175
|
+
|
|
1176
|
+
using _ioLock = await io.lock({
|
|
1177
|
+
key,
|
|
1178
|
+
// `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
|
|
1179
|
+
signal,
|
|
1180
|
+
});
|
|
1181
|
+
// 全宛先を並列に削除し、失敗は宛先の index 付きで集約します。
|
|
1182
|
+
const errors: PluginErrorRecord[] = [];
|
|
1183
|
+
const resolver = this.#pipeline.createResolver({
|
|
1184
|
+
key,
|
|
1185
|
+
store,
|
|
1186
|
+
signal,
|
|
1187
|
+
});
|
|
1188
|
+
await Promise.all(
|
|
1189
|
+
this.#pipeline.leaves.map(async (leaf) => {
|
|
1190
|
+
const resolved = resolver.resolve(leaf);
|
|
1191
|
+
const existsContext = new HasContext({
|
|
1192
|
+
key: resolved.key,
|
|
1193
|
+
store,
|
|
1194
|
+
signal,
|
|
1195
|
+
});
|
|
1196
|
+
const deleteContext = new DeleteContext({
|
|
1197
|
+
key: resolved.key,
|
|
1198
|
+
store,
|
|
1199
|
+
signal,
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
try {
|
|
1203
|
+
if (await leaf.storage.exists(existsContext)) {
|
|
1204
|
+
await leaf.storage.delete(deleteContext);
|
|
1205
|
+
}
|
|
1206
|
+
} catch (ex) {
|
|
1207
|
+
errors.push({
|
|
1208
|
+
name: leaf.name,
|
|
1209
|
+
index: leaf.index,
|
|
1210
|
+
reason: ex,
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
}),
|
|
1214
|
+
);
|
|
1215
|
+
|
|
1216
|
+
if (errors.length > 0) {
|
|
1217
|
+
throw new PluginOperationAggregateError({
|
|
1218
|
+
action: "delete",
|
|
1219
|
+
errors,
|
|
1220
|
+
plugin: "storage",
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
public async clear(options?: ClearOptions): Promise<void>;
|
|
1226
|
+
public async clear(...args: any): Promise<void> {
|
|
1227
|
+
if (this.#con === null) {
|
|
1228
|
+
throw new UniKvsIsNotOpenError();
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
const [options = {}] = v.parseInput(ClearArgsSchema, args);
|
|
1232
|
+
const { vars: varsOption, signal: signalOption } = options;
|
|
1233
|
+
|
|
1234
|
+
const { ac, io } = this.#con;
|
|
1235
|
+
const signal = combineSignals([ac.signal, signalOption]);
|
|
1236
|
+
|
|
1237
|
+
const store = this.#createOperationStore(varsOption);
|
|
1238
|
+
|
|
1239
|
+
using _lock = await asyncmux.readonly(this, signal);
|
|
1240
|
+
// ロック待機中に接続状態が変更されていないか再確認します。
|
|
1241
|
+
if (this.#con === null) {
|
|
1242
|
+
throw new UniKvsIsNotOpenError();
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
using _ioLock = await io.lock({ signal });
|
|
1246
|
+
// 全ミドルウェアの `use` を実行します。
|
|
1247
|
+
const useContext = new ClearContext({
|
|
1248
|
+
store,
|
|
1249
|
+
signal,
|
|
1250
|
+
});
|
|
1251
|
+
await this.#use("clear", useContext);
|
|
1252
|
+
// `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
|
|
1253
|
+
throwIfAborted(signal);
|
|
1254
|
+
// 一括削除は全宛先で並列に実行し、失敗は宛先の index 付きで集約します。
|
|
1255
|
+
const errors: PluginErrorRecord[] = [];
|
|
1256
|
+
await Promise.all(
|
|
1257
|
+
this.#pipeline.leaves.map(async (leaf, index) => {
|
|
1258
|
+
try {
|
|
1259
|
+
const c = new ClearContext({
|
|
1260
|
+
store,
|
|
1261
|
+
signal,
|
|
1262
|
+
});
|
|
1263
|
+
await leaf.storage.clear(c);
|
|
1264
|
+
} catch (ex) {
|
|
1265
|
+
errors.push({
|
|
1266
|
+
name: leaf.name,
|
|
1267
|
+
index,
|
|
1268
|
+
reason: ex,
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
}),
|
|
1272
|
+
);
|
|
1273
|
+
|
|
1274
|
+
if (errors.length > 0) {
|
|
1275
|
+
throw new PluginOperationAggregateError({
|
|
1276
|
+
action: "clear",
|
|
1277
|
+
errors,
|
|
1278
|
+
plugin: "storage",
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* 基本変数と操作固有変数を結合した実行時変数マップを作成します。
|
|
1285
|
+
*
|
|
1286
|
+
* @param vars 操作固有の変数です。
|
|
1287
|
+
*
|
|
1288
|
+
* @returns 結合した変数マップを返します。
|
|
1289
|
+
*/
|
|
1290
|
+
#createOperationVars(vars: readonly VariablesEntry[] | undefined): Map<string, unknown> {
|
|
1291
|
+
const map = new Map(this.#baseVars);
|
|
1292
|
+
|
|
1293
|
+
if (vars) {
|
|
1294
|
+
for (const [k, value] of vars) {
|
|
1295
|
+
map.set(k, value);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
return map;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* 操作内で共有される変数ストアを作成します。
|
|
1304
|
+
*
|
|
1305
|
+
* 同一操作内の全コンテキスト (`use`/`encode`/`decode`/ ストレージ) で共有され、下流の `set` が上流の `await next()` 後の `get` に見えます。
|
|
1306
|
+
*
|
|
1307
|
+
* @param vars 操作固有の変数です。
|
|
1308
|
+
*
|
|
1309
|
+
* @returns 共有ストアを返します。
|
|
1310
|
+
*/
|
|
1311
|
+
#createOperationStore(vars: readonly VariablesEntry[] | undefined): VariablesStore {
|
|
1312
|
+
return createVariablesStore([...this.#baseVars, ...(vars ?? [])]);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* キーに対応する値のスキーマ情報を解決します。
|
|
1317
|
+
*
|
|
1318
|
+
* @param key 解決するキーです。
|
|
1319
|
+
*
|
|
1320
|
+
* @returns 対応するスキーマ情報を返します。スキーマ未設定または対応する定義がない場合は `undefined` を返します。
|
|
1321
|
+
*/
|
|
1322
|
+
#resolveValueSchema(key: string): ValueSchemaInfo | undefined {
|
|
1323
|
+
return this.#valueSchemaResolver?.resolve(key);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/**
|
|
1327
|
+
* すべての宛先で読み取りに失敗したときのエラーを組み立てます。
|
|
1328
|
+
*
|
|
1329
|
+
* @param key 読み取ったキーです。
|
|
1330
|
+
* @param errors 収集した失敗です。
|
|
1331
|
+
*
|
|
1332
|
+
* @returns `KeyNotFoundError` を返します。
|
|
1333
|
+
*/
|
|
1334
|
+
#notFound(key: string, errors: readonly PluginErrorRecord[]): KeyNotFoundError {
|
|
1335
|
+
const args: KeyNotFoundErrorArgs = { key };
|
|
1336
|
+
|
|
1337
|
+
switch (errors.length) {
|
|
1338
|
+
case 0: {
|
|
1339
|
+
break;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
case 1: {
|
|
1343
|
+
const [firstError] = errors;
|
|
1344
|
+
|
|
1345
|
+
if (firstError !== undefined) {
|
|
1346
|
+
args.cause = firstError.reason;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
break;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
default: {
|
|
1353
|
+
args.cause = new PluginOperationAggregateError({
|
|
1354
|
+
action: "read",
|
|
1355
|
+
errors,
|
|
1356
|
+
plugin: "storage",
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
return new KeyNotFoundError(args);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
async #close(
|
|
1365
|
+
operationVars: Map<string, unknown>,
|
|
1366
|
+
signal: AbortSignal,
|
|
1367
|
+
con: Connection,
|
|
1368
|
+
progress: { pluginsStarted: boolean },
|
|
1369
|
+
): Promise<void> {
|
|
1370
|
+
using _lock = await asyncmux(this, signal);
|
|
1371
|
+
// ロック待機中に接続状態が変更されていないか再確認します。
|
|
1372
|
+
if (this.#con !== con) {
|
|
1373
|
+
throw new UniKvsIsNotOpenError();
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const { io } = this.#con;
|
|
1377
|
+
using _ioLock = await io.lock({ signal });
|
|
1378
|
+
const closeFns: [
|
|
1379
|
+
() => Promise<void>,
|
|
1380
|
+
() => Promise<void>,
|
|
1381
|
+
plugin: "storage" | "middleware",
|
|
1382
|
+
name: string,
|
|
1383
|
+
index: number,
|
|
1384
|
+
][] = [];
|
|
1385
|
+
|
|
1386
|
+
// ストレージのクローズ処理を登録します。
|
|
1387
|
+
// open と同じ方針でプラグインごとに複製し、並列実行中の変更が互いに見えないようにします。
|
|
1388
|
+
for (const [index, leaf] of this.#pipeline.leaves.entries()) {
|
|
1389
|
+
const createContext = (): CloseContext =>
|
|
1390
|
+
new CloseContext({
|
|
1391
|
+
vars: new Map(operationVars).entries(),
|
|
1392
|
+
signal,
|
|
1393
|
+
});
|
|
1394
|
+
// 再試行は通常 close の失敗 (操作 signal の abort など) に引きずられないよう、新しいタイムアウト signal を使います。
|
|
1395
|
+
const createRetryContext = (): CloseContext =>
|
|
1396
|
+
new CloseContext({
|
|
1397
|
+
vars: new Map(operationVars).entries(),
|
|
1398
|
+
signal: AbortSignal.timeout(10e3),
|
|
1399
|
+
});
|
|
1400
|
+
closeFns.push([
|
|
1401
|
+
(): Promise<void> => leaf.storage.close(createContext()),
|
|
1402
|
+
(): Promise<void> => leaf.storage.retryClose(createRetryContext()),
|
|
1403
|
+
"storage",
|
|
1404
|
+
leaf.name,
|
|
1405
|
+
index,
|
|
1406
|
+
]);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// ミドルウェアのクローズ処理を登録します。
|
|
1410
|
+
for (const [index, plugin] of this.#pipeline.middlewares.entries()) {
|
|
1411
|
+
const createContext = (): CloseContext =>
|
|
1412
|
+
new CloseContext({
|
|
1413
|
+
vars: new Map(operationVars).entries(),
|
|
1414
|
+
signal,
|
|
1415
|
+
});
|
|
1416
|
+
// 再試行は通常 close の失敗 (操作 signal の abort など) に引きずられないよう、新しいタイムアウト signal を使います。
|
|
1417
|
+
const createRetryContext = (): CloseContext =>
|
|
1418
|
+
new CloseContext({
|
|
1419
|
+
vars: new Map(operationVars).entries(),
|
|
1420
|
+
signal: AbortSignal.timeout(10e3),
|
|
1421
|
+
});
|
|
1422
|
+
closeFns.push([
|
|
1423
|
+
(): Promise<void> => plugin.close(createContext()),
|
|
1424
|
+
(): Promise<void> => plugin.retryClose(createRetryContext()),
|
|
1425
|
+
"middleware",
|
|
1426
|
+
plugin.name,
|
|
1427
|
+
index,
|
|
1428
|
+
]);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// すべての処理を並列に実行し、エラーが発生した場合は集約します。
|
|
1432
|
+
progress.pluginsStarted = true;
|
|
1433
|
+
const errors: {
|
|
1434
|
+
name: string;
|
|
1435
|
+
index: number;
|
|
1436
|
+
plugin: "storage" | "middleware";
|
|
1437
|
+
reason: unknown;
|
|
1438
|
+
}[] = [];
|
|
1439
|
+
const retries: (() => Promise<void>)[] = [];
|
|
1440
|
+
await Promise.all(
|
|
1441
|
+
closeFns.map(async ([f, retry, plugin, name, index]) => {
|
|
1442
|
+
try {
|
|
1443
|
+
await f();
|
|
1444
|
+
} catch (ex) {
|
|
1445
|
+
errors.push({
|
|
1446
|
+
name,
|
|
1447
|
+
index,
|
|
1448
|
+
plugin,
|
|
1449
|
+
reason: ex,
|
|
1450
|
+
});
|
|
1451
|
+
// 通常の close で再試行すると、close 失敗後に別の利用者が引き継いだ参照まで解放してしまいます。
|
|
1452
|
+
retries.push(retry);
|
|
1453
|
+
}
|
|
1454
|
+
}),
|
|
1455
|
+
);
|
|
1456
|
+
|
|
1457
|
+
if (errors.length > 0) {
|
|
1458
|
+
// 失敗したプラグインだけを 1 回再試行します。
|
|
1459
|
+
// 成功済みのプラグインを再 close すると、参照カウントが二重に解放され、共有している他の利用者の実体まで閉じてしまうためです。
|
|
1460
|
+
await Promise.all(
|
|
1461
|
+
retries.map(async (f) => {
|
|
1596
1462
|
try {
|
|
1597
|
-
await
|
|
1598
|
-
} catch {
|
|
1599
|
-
|
|
1463
|
+
await f();
|
|
1464
|
+
} catch (ex) {
|
|
1465
|
+
logger.error`Failed to retry closing a plugin: ${ex}`;
|
|
1600
1466
|
}
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
ioLock.release();
|
|
1467
|
+
}),
|
|
1468
|
+
);
|
|
1604
1469
|
|
|
1605
|
-
|
|
1606
|
-
|
|
1470
|
+
throw new PluginOperationAggregateError({
|
|
1471
|
+
action: "close",
|
|
1472
|
+
errors,
|
|
1473
|
+
});
|
|
1607
1474
|
}
|
|
1475
|
+
|
|
1476
|
+
this.#con = null;
|
|
1608
1477
|
}
|
|
1609
1478
|
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1479
|
+
/**
|
|
1480
|
+
* 書き込み失敗の集約と、成功した宛先への通知を行います。
|
|
1481
|
+
*
|
|
1482
|
+
* @param targets 書き込みを試みた対象です。
|
|
1483
|
+
* @param failed 失敗した葉の集合です。
|
|
1484
|
+
* @param errors 集約する失敗です。
|
|
1485
|
+
* @param signal 中断シグナルです。
|
|
1486
|
+
*/
|
|
1487
|
+
async #finishWrites(
|
|
1488
|
+
targets: readonly {
|
|
1489
|
+
readonly key: string;
|
|
1490
|
+
readonly leaf: PipelineLeaf;
|
|
1491
|
+
}[],
|
|
1492
|
+
store: VariablesStore,
|
|
1493
|
+
failed: ReadonlySet<PipelineLeaf>,
|
|
1494
|
+
errors: PluginErrorRecord[],
|
|
1495
|
+
signal: AbortSignal,
|
|
1496
|
+
): Promise<void> {
|
|
1497
|
+
if (errors.length === 0) {
|
|
1498
|
+
return;
|
|
1618
1499
|
}
|
|
1619
1500
|
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1501
|
+
const error = new PluginOperationAggregateError({
|
|
1502
|
+
action: "write",
|
|
1503
|
+
errors,
|
|
1504
|
+
plugin: "storage",
|
|
1505
|
+
});
|
|
1506
|
+
const notifyErrors: unknown[] = [];
|
|
1507
|
+
await Promise.all(
|
|
1508
|
+
targets.map(async (target) => {
|
|
1509
|
+
// 失敗した宛先を除いたすべての宛先へ通知します。通知に失敗しても元の集約エラーは置き換えません。
|
|
1510
|
+
if (failed.has(target.leaf)) {
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1628
1513
|
|
|
1629
|
-
|
|
1514
|
+
try {
|
|
1515
|
+
const c = new SetContext({
|
|
1516
|
+
key: target.key,
|
|
1517
|
+
store,
|
|
1518
|
+
signal,
|
|
1519
|
+
});
|
|
1520
|
+
await target.leaf.storage.onOtherWriteError(c, error);
|
|
1521
|
+
} catch (ex) {
|
|
1522
|
+
notifyErrors.push(ex);
|
|
1523
|
+
}
|
|
1524
|
+
}),
|
|
1525
|
+
);
|
|
1630
1526
|
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
if (this.#con === null) {
|
|
1634
|
-
throw new UniKvsIsNotOpenError();
|
|
1527
|
+
if (notifyErrors.length > 0) {
|
|
1528
|
+
logger.error(new AggregateError(notifyErrors, "Failed to handle error"));
|
|
1635
1529
|
}
|
|
1636
1530
|
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
const useContext = new HasContext({
|
|
1640
|
-
key,
|
|
1641
|
-
store,
|
|
1642
|
-
signal,
|
|
1643
|
-
});
|
|
1644
|
-
await this.#use("read", useContext);
|
|
1645
|
-
// `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
|
|
1646
|
-
throwIfAborted(signal);
|
|
1531
|
+
throw error;
|
|
1532
|
+
}
|
|
1647
1533
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1534
|
+
/**
|
|
1535
|
+
* ミドルウェアの `use` をオニオン実行し、失敗時は集約して投げます。
|
|
1536
|
+
*
|
|
1537
|
+
* 同一コンテキストオブジェクトを全ミドルウェアで共有し、`next` で下流をネスト実行するため、下流の `set` が上流の `await next()` 後の `get` に見えます。
|
|
1538
|
+
* `encode`/`decode` と同じく `next()` の呼び出し規約を検証し、違反したときは操作を実行せず失敗させます (fail-close)。
|
|
1539
|
+
* いずれかのミドルウェアが投げたときは即時に確定し、後段は実行しません (fail-fast)。
|
|
1540
|
+
* 失敗は集約エラー (単一件) として投げます。
|
|
1541
|
+
*
|
|
1542
|
+
* @param action 集約エラーに付与する操作名です。
|
|
1543
|
+
* @param c 共有の `use` コンテキストです。
|
|
1544
|
+
*/
|
|
1545
|
+
async #use(
|
|
1546
|
+
action: "read" | "write" | "delete" | "clear",
|
|
1547
|
+
c: IMiddleware.UseContext,
|
|
1548
|
+
): Promise<void> {
|
|
1549
|
+
throwIfAborted(c.signal);
|
|
1660
1550
|
|
|
1661
|
-
|
|
1662
|
-
|
|
1551
|
+
const middlewares = this.#pipeline.middlewares;
|
|
1552
|
+
const dispatch = async (index: number): Promise<void> => {
|
|
1553
|
+
const maybeMiddleware = middlewares[index];
|
|
1663
1554
|
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
store,
|
|
1668
|
-
signal,
|
|
1669
|
-
});
|
|
1555
|
+
if (maybeMiddleware === undefined) {
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1670
1558
|
|
|
1671
|
-
|
|
1672
|
-
return true;
|
|
1673
|
-
}
|
|
1674
|
-
} catch (ex) {
|
|
1675
|
-
if (signal.aborted) {
|
|
1676
|
-
throw ex;
|
|
1677
|
-
}
|
|
1559
|
+
const middleware: UniKvsMiddleware = maybeMiddleware;
|
|
1678
1560
|
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
});
|
|
1561
|
+
let nextCalled = false;
|
|
1562
|
+
let nextSettled = false;
|
|
1563
|
+
let downstreamError: unknown;
|
|
1564
|
+
let hasDownstreamError = false;
|
|
1684
1565
|
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1566
|
+
function next(): Promise<void> {
|
|
1567
|
+
if (nextCalled) {
|
|
1568
|
+
// 2 回目以降の `next()` は下流を実行せず拒否だけを返します。
|
|
1569
|
+
const rejection = Promise.reject(
|
|
1570
|
+
new MiddlewareNextCalledMultipleTimesError({
|
|
1571
|
+
name: middleware.name,
|
|
1572
|
+
operation: "use",
|
|
1573
|
+
}),
|
|
1574
|
+
);
|
|
1575
|
+
// 呼び出し側が切り離した場合の未処理 rejection を防ぎます。await したときは従来どおり拒否されます。
|
|
1576
|
+
// oxlint-disable-next-line promise/prefer-await-to-then
|
|
1577
|
+
rejection.catch(() => {});
|
|
1688
1578
|
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
throw this.#notFound(key, errors);
|
|
1692
|
-
}
|
|
1579
|
+
return rejection;
|
|
1580
|
+
}
|
|
1693
1581
|
|
|
1694
|
-
|
|
1695
|
-
}
|
|
1582
|
+
nextCalled = true;
|
|
1696
1583
|
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1584
|
+
const promise = Promise.try(async () => {
|
|
1585
|
+
try {
|
|
1586
|
+
await dispatch(index + 1);
|
|
1587
|
+
} catch (ex) {
|
|
1588
|
+
// 下流の失敗を親自身の失敗と取り違えないよう、由来を記録します。
|
|
1589
|
+
downstreamError = ex;
|
|
1590
|
+
hasDownstreamError = true;
|
|
1591
|
+
throw ex;
|
|
1592
|
+
} finally {
|
|
1593
|
+
nextSettled = true;
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
// 呼び出し側が切り離した場合の未処理 rejection を防ぎます。await したときは従来どおり拒否されます。
|
|
1597
|
+
// oxlint-disable-next-line promise/prefer-await-to-then
|
|
1598
|
+
promise.catch(() => {});
|
|
1706
1599
|
|
|
1707
|
-
|
|
1708
|
-
|
|
1600
|
+
return promise;
|
|
1601
|
+
}
|
|
1709
1602
|
|
|
1710
|
-
|
|
1711
|
-
|
|
1603
|
+
try {
|
|
1604
|
+
await middleware.use(c, next);
|
|
1605
|
+
throwIfAborted(c.signal);
|
|
1606
|
+
if (!nextCalled) {
|
|
1607
|
+
throw new MiddlewareNextNotCalledError({
|
|
1608
|
+
name: middleware.name,
|
|
1609
|
+
operation: "use",
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1712
1612
|
|
|
1713
|
-
|
|
1714
|
-
|
|
1613
|
+
if (!nextSettled) {
|
|
1614
|
+
throw new MiddlewareNextNotAwaitedError({
|
|
1615
|
+
name: middleware.name,
|
|
1616
|
+
operation: "use",
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
} catch (ex) {
|
|
1620
|
+
// 中断による失敗はそのまま投げます。
|
|
1621
|
+
if (c.signal.aborted && ex === c.signal.reason) {
|
|
1622
|
+
throw ex;
|
|
1623
|
+
}
|
|
1715
1624
|
|
|
1716
|
-
|
|
1625
|
+
// 規約違反はプログラミングエラーであり、集約せずに投げます。
|
|
1626
|
+
if (
|
|
1627
|
+
ex instanceof MiddlewareNextNotCalledError ||
|
|
1628
|
+
ex instanceof MiddlewareNextNotAwaitedError ||
|
|
1629
|
+
ex instanceof MiddlewareNextCalledMultipleTimesError
|
|
1630
|
+
) {
|
|
1631
|
+
throw ex;
|
|
1632
|
+
}
|
|
1717
1633
|
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
}
|
|
1634
|
+
// `next()` から伝播した下流の失敗は再集約せず、そのまま投げます。
|
|
1635
|
+
if (hasDownstreamError && ex === downstreamError) {
|
|
1636
|
+
throw ex;
|
|
1637
|
+
}
|
|
1723
1638
|
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1639
|
+
throw new PluginOperationAggregateError({
|
|
1640
|
+
action,
|
|
1641
|
+
errors: [
|
|
1642
|
+
{
|
|
1643
|
+
name: middleware.name,
|
|
1644
|
+
index,
|
|
1645
|
+
reason: ex,
|
|
1646
|
+
},
|
|
1647
|
+
],
|
|
1648
|
+
plugin: "middleware",
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
await dispatch(0);
|
|
1653
|
+
}
|
|
1732
1654
|
|
|
1655
|
+
/**
|
|
1656
|
+
* プレーン値の並列書き込みを行います。
|
|
1657
|
+
*
|
|
1658
|
+
* @param io 多重化インスタンスです。
|
|
1659
|
+
* @param signal 中断シグナルです。
|
|
1660
|
+
* @param key 書き込みキーです。
|
|
1661
|
+
* @param store 操作内で共有される変数ストアです。
|
|
1662
|
+
* @param value 書き込む値です.
|
|
1663
|
+
*/
|
|
1664
|
+
async #setPlain(
|
|
1665
|
+
io: Asyncmux,
|
|
1666
|
+
signal: AbortSignal,
|
|
1667
|
+
key: string,
|
|
1668
|
+
store: VariablesStore,
|
|
1669
|
+
value: unknown,
|
|
1670
|
+
): Promise<void> {
|
|
1671
|
+
const targets = await this.#pipeline.encode(
|
|
1672
|
+
{
|
|
1673
|
+
key,
|
|
1674
|
+
store,
|
|
1675
|
+
signal,
|
|
1676
|
+
},
|
|
1677
|
+
value,
|
|
1678
|
+
);
|
|
1733
1679
|
using _ioLock = await io.lock({
|
|
1734
1680
|
key,
|
|
1735
|
-
// `use` の完了直後に中断を確認し、後続処理での検出に依存しないようにします。
|
|
1736
1681
|
signal,
|
|
1737
1682
|
});
|
|
1738
|
-
// 全宛先を並列に削除し、失敗は宛先の index 付きで集約します。
|
|
1739
1683
|
const errors: PluginErrorRecord[] = [];
|
|
1740
|
-
const
|
|
1741
|
-
key,
|
|
1742
|
-
store,
|
|
1743
|
-
signal,
|
|
1744
|
-
});
|
|
1684
|
+
const failed = new Set<PipelineLeaf>();
|
|
1745
1685
|
await Promise.all(
|
|
1746
|
-
|
|
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
|
-
|
|
1686
|
+
targets.map(async (target) => {
|
|
1759
1687
|
try {
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1688
|
+
const c = new SetContext({
|
|
1689
|
+
key: target.key,
|
|
1690
|
+
store,
|
|
1691
|
+
signal,
|
|
1692
|
+
});
|
|
1693
|
+
await target.leaf.storage.write(c, target.data);
|
|
1763
1694
|
} catch (ex) {
|
|
1764
1695
|
errors.push({
|
|
1765
|
-
name: leaf.name,
|
|
1766
|
-
index: leaf.index,
|
|
1696
|
+
name: target.leaf.name,
|
|
1697
|
+
index: target.leaf.index,
|
|
1767
1698
|
reason: ex,
|
|
1768
1699
|
});
|
|
1700
|
+
failed.add(target.leaf);
|
|
1769
1701
|
}
|
|
1770
1702
|
}),
|
|
1771
1703
|
);
|
|
1772
|
-
|
|
1773
|
-
if (errors.length > 0) {
|
|
1774
|
-
throw new PluginOperationAggregateError({
|
|
1775
|
-
action: "delete",
|
|
1776
|
-
errors,
|
|
1777
|
-
plugin: "storage",
|
|
1778
|
-
});
|
|
1779
|
-
}
|
|
1704
|
+
await this.#finishWrites(targets, store, failed, errors, signal);
|
|
1780
1705
|
}
|
|
1781
1706
|
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1707
|
+
/**
|
|
1708
|
+
* ストリーム値の並列書き込みを行います。
|
|
1709
|
+
*
|
|
1710
|
+
* @param io 多重化インスタンスです。
|
|
1711
|
+
* @param signal 中断シグナルです。
|
|
1712
|
+
* @param key 書き込みキーです。
|
|
1713
|
+
* @param store 操作内で共有される変数ストアです。
|
|
1714
|
+
* @param value 書き込むストリームです。
|
|
1715
|
+
*/
|
|
1716
|
+
async #setStream(
|
|
1717
|
+
io: Asyncmux,
|
|
1718
|
+
signal: AbortSignal,
|
|
1719
|
+
key: string,
|
|
1720
|
+
store: VariablesStore,
|
|
1721
|
+
value: IReadableStream,
|
|
1722
|
+
): Promise<void> {
|
|
1723
|
+
const streams = new Set<IReadableStream>([value]);
|
|
1724
|
+
let tree;
|
|
1795
1725
|
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1726
|
+
try {
|
|
1727
|
+
tree = await this.#pipeline.buildEncodeTree(
|
|
1728
|
+
{
|
|
1729
|
+
key,
|
|
1730
|
+
store,
|
|
1731
|
+
signal,
|
|
1732
|
+
},
|
|
1733
|
+
value,
|
|
1734
|
+
streams,
|
|
1735
|
+
);
|
|
1736
|
+
} catch (ex) {
|
|
1737
|
+
await cancelStreams(streams, ex);
|
|
1738
|
+
throw ex;
|
|
1800
1739
|
}
|
|
1801
1740
|
|
|
1802
|
-
using _ioLock = await io
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1741
|
+
using _ioLock = await io
|
|
1742
|
+
.lock({
|
|
1743
|
+
key,
|
|
1744
|
+
signal,
|
|
1745
|
+
})
|
|
1746
|
+
.catch(async (ex) => {
|
|
1747
|
+
// キーロックの待機中に中断されたときも、構築済みのストリームをキャンセルします。
|
|
1748
|
+
// ソースストリームをロックしたまま残さないためです。
|
|
1749
|
+
await cancelStreams(streams, ex);
|
|
1750
|
+
throw ex;
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1812
1753
|
const errors: PluginErrorRecord[] = [];
|
|
1754
|
+
const failed = new Set<PipelineLeaf>();
|
|
1813
1755
|
await Promise.all(
|
|
1814
|
-
|
|
1756
|
+
tree.targets.map(async (target) => {
|
|
1815
1757
|
try {
|
|
1816
|
-
const c = new
|
|
1758
|
+
const c = new SetContext({
|
|
1759
|
+
key: target.key,
|
|
1817
1760
|
store,
|
|
1818
1761
|
signal,
|
|
1819
1762
|
});
|
|
1820
|
-
await leaf.storage.
|
|
1763
|
+
const w = await target.leaf.storage.getWritable(c);
|
|
1764
|
+
await target.stream.pipeTo(w, { signal });
|
|
1821
1765
|
} catch (ex) {
|
|
1766
|
+
// tee で分岐したストリームは、片方の分岐をキャンセルするまで他方が読み取ったチャンクを保持し続けるため、失敗時に放棄した分岐は確実にキャンセルします。
|
|
1767
|
+
try {
|
|
1768
|
+
await target.stream.cancel(ex);
|
|
1769
|
+
} catch {
|
|
1770
|
+
// キャンセルに失敗してもエラー集約を優先します。
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1822
1773
|
errors.push({
|
|
1823
|
-
name: leaf.name,
|
|
1824
|
-
index,
|
|
1774
|
+
name: target.leaf.name,
|
|
1775
|
+
index: target.leaf.index,
|
|
1825
1776
|
reason: ex,
|
|
1826
1777
|
});
|
|
1778
|
+
failed.add(target.leaf);
|
|
1827
1779
|
}
|
|
1828
1780
|
}),
|
|
1829
1781
|
);
|
|
1782
|
+
await this.#finishWrites(tree.targets, store, failed, errors, signal);
|
|
1783
|
+
}
|
|
1830
1784
|
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1785
|
+
/**
|
|
1786
|
+
* 読み取り元のチェーン全体を逆順にデコードし、書き戻し対象向けに再エンコードして書き込みます。
|
|
1787
|
+
*
|
|
1788
|
+
* @param data 読み取ったデータです。
|
|
1789
|
+
* @param hit 読み取り元の葉です。
|
|
1790
|
+
* @param missing 読み取りに失敗または不在だった葉です。
|
|
1791
|
+
* @param store 操作内で共有される変数ストアです。
|
|
1792
|
+
* @param key キーです。
|
|
1793
|
+
* @param signal 中断シグナルです。
|
|
1794
|
+
* @param throwOnRepairDenied 書き戻し拒否をエラーとして投げるかどうかです。
|
|
1795
|
+
*
|
|
1796
|
+
* @returns デコード済みの原データを返します。
|
|
1797
|
+
*/
|
|
1798
|
+
async #repairPlain(
|
|
1799
|
+
data: unknown,
|
|
1800
|
+
hit: ResolvedLeaf,
|
|
1801
|
+
missing: readonly PipelineLeaf[],
|
|
1802
|
+
store: VariablesStore,
|
|
1803
|
+
key: string,
|
|
1804
|
+
signal: AbortSignal,
|
|
1805
|
+
throwOnRepairDenied: boolean,
|
|
1806
|
+
): Promise<unknown> {
|
|
1807
|
+
const raw = await this.#pipeline.decode(
|
|
1808
|
+
hit.leaf,
|
|
1809
|
+
{
|
|
1810
|
+
key,
|
|
1811
|
+
store,
|
|
1812
|
+
signal,
|
|
1813
|
+
},
|
|
1814
|
+
data,
|
|
1815
|
+
);
|
|
1816
|
+
await this.#pipeline.repair(
|
|
1817
|
+
{
|
|
1818
|
+
key,
|
|
1819
|
+
store,
|
|
1820
|
+
signal,
|
|
1821
|
+
},
|
|
1822
|
+
missing,
|
|
1823
|
+
throwOnRepairDenied,
|
|
1824
|
+
raw,
|
|
1825
|
+
async (leaf, writeKey, input) => {
|
|
1826
|
+
const c = new SetContext({
|
|
1827
|
+
key: writeKey,
|
|
1828
|
+
store,
|
|
1829
|
+
signal,
|
|
1830
|
+
});
|
|
1831
|
+
await leaf.storage.write(c, input);
|
|
1832
|
+
},
|
|
1833
|
+
);
|
|
1834
|
+
|
|
1835
|
+
throwIfAborted(signal);
|
|
1836
|
+
|
|
1837
|
+
return raw;
|
|
1838
1838
|
}
|
|
1839
1839
|
}
|