dl-once 0.0.1 → 0.0.2
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/LICENSE +21 -0
- package/README.md +128 -28
- package/dist/_bytes-builder.d.ts +34 -0
- package/dist/_bytes-builder.d.ts.map +1 -0
- package/dist/_bytes-builder.js +48 -0
- package/dist/cache-storage.d.ts +74 -0
- package/dist/cache-storage.d.ts.map +1 -0
- package/dist/cache-storage.js +1 -0
- package/dist/dl-once.d.ts +242 -0
- package/dist/dl-once.d.ts.map +1 -0
- package/dist/dl-once.js +245 -0
- package/dist/hash-verification-hook.d.ts +46 -0
- package/dist/hash-verification-hook.d.ts.map +1 -0
- package/dist/hash-verification-hook.js +97 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/indexeddb-cache-storage.d.ts +51 -0
- package/dist/indexeddb-cache-storage.d.ts.map +1 -0
- package/dist/indexeddb-cache-storage.js +573 -0
- package/dist/md5-verification-hook.d.ts +15 -0
- package/dist/md5-verification-hook.d.ts.map +1 -0
- package/dist/md5-verification-hook.js +17 -0
- package/dist/memory-cache-storage.d.ts +41 -0
- package/dist/memory-cache-storage.d.ts.map +1 -0
- package/dist/memory-cache-storage.js +290 -0
- package/dist/node-fs-cache-storage.d.ts +63 -0
- package/dist/node-fs-cache-storage.d.ts.map +1 -0
- package/dist/node-fs-cache-storage.js +565 -0
- package/dist/sha256-verification-hook.d.ts +15 -0
- package/dist/sha256-verification-hook.d.ts.map +1 -0
- package/dist/sha256-verification-hook.js +17 -0
- package/package.json +46 -7
- package/src/_bytes-builder.ts +66 -0
- package/src/cache-storage.ts +86 -0
- package/src/dl-once.ts +554 -0
- package/src/hash-verification-hook.ts +98 -0
- package/src/index.ts +28 -0
- package/src/indexeddb-cache-storage.ts +535 -0
- package/src/md5-verification-hook.ts +18 -0
- package/src/memory-cache-storage.ts +361 -0
- package/src/node-fs-cache-storage.ts +511 -0
- package/src/sha256-verification-hook.ts +18 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { IHasher } from "hash-wasm";
|
|
2
|
+
import type { ChunkReadHandlerArgs, IHook } from "./dl-once.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ダウンロードされたデータのハッシュ値を検証するためのフッククラスです。
|
|
6
|
+
*
|
|
7
|
+
* `hash-wasm` ライブラリーを使用して、ストリーム読み込みに合わせて逐次的にハッシュ計算を行い、完了時に期待されるハッシュ値と一致するかを判定します。
|
|
8
|
+
*/
|
|
9
|
+
export default class HashVerificationHook implements IHook {
|
|
10
|
+
/**
|
|
11
|
+
* ハッシュ計算を行うオブジェクトを作成する関数です。
|
|
12
|
+
*/
|
|
13
|
+
#createHahser: () => Promise<IHasher>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ハッシュ計算を行うための WebAssembly インスタンスを保持するプロパティーです。
|
|
17
|
+
*/
|
|
18
|
+
#hasher: IHasher | null = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 比較対象となる期待されるハッシュ値(小文字)を保持する読み取り専用プロパティーです。
|
|
22
|
+
*/
|
|
23
|
+
readonly #expectedHash: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* HashVerificationHook の新しいインスタンスを作成します。
|
|
27
|
+
*
|
|
28
|
+
* @param createHahser ハッシュ計算を行うオブジェクトを作成する関数です。
|
|
29
|
+
* @param expectedHash 比較対象となる 16 進数形式の期待されるハッシュ値です。
|
|
30
|
+
*/
|
|
31
|
+
public constructor(createHahser: () => Promise<IHasher>, expectedHash: string) {
|
|
32
|
+
this.#createHahser = createHahser;
|
|
33
|
+
// 比較時の揺らぎをなくすため、入力されたハッシュ値を小文字に正規化して保存します。
|
|
34
|
+
this.#expectedHash = expectedHash.toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* ダウンロード開始前に呼び出される非同期メソッドです。
|
|
39
|
+
*
|
|
40
|
+
* ハッシュ計算用の WebAssembly インスタンスを作成し、初期化処理を行います。
|
|
41
|
+
*
|
|
42
|
+
* @returns インスタンスの準備が完了した際に解決される Promise です。
|
|
43
|
+
*/
|
|
44
|
+
public async onBeforeDownload(): Promise<void> {
|
|
45
|
+
if (this.#hasher) {
|
|
46
|
+
throw new Error("Hasher is already initialized");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// hash-wasm ライブラリーを使用してハッシュ計算用の WASM インスタンスを作成します。
|
|
50
|
+
this.#hasher = await this.#createHahser();
|
|
51
|
+
|
|
52
|
+
// ハッシュ計算の内部状態をリセットし、新しいストリームの受け入れ準備を整えます。
|
|
53
|
+
this.#hasher.init();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* データチャンクが読み込まれるたびに呼び出されるメソッドです。
|
|
58
|
+
*
|
|
59
|
+
* 取得したチャンクデータをハッシュ計算機に送り、中間状態を更新します。
|
|
60
|
+
*
|
|
61
|
+
* @param args チャンクデータを含むハンドラー引数です。
|
|
62
|
+
*/
|
|
63
|
+
public onChunkRead(args: Pick<ChunkReadHandlerArgs, "chunkData">): void {
|
|
64
|
+
const { chunkData } = args;
|
|
65
|
+
|
|
66
|
+
// 初期化済みのハッシュ計算機へバイナリーデータを渡します。
|
|
67
|
+
// update メソッドにより、メモリー効率を保ちながら逐次計算が行われます。
|
|
68
|
+
this.#hasher!.update(chunkData);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* ダウンロードストリームが正常に終了した際に呼び出されるメソッドです。
|
|
73
|
+
*
|
|
74
|
+
* 最終的なハッシュ値を算出し、期待される値と異なる場合はエラーを投げます。
|
|
75
|
+
*/
|
|
76
|
+
public onClose(): void {
|
|
77
|
+
// これまでに蓄積されたデータから最終的なダイジェスト(16 進数文字列)を作成します。
|
|
78
|
+
const finalHash = this.#hasher!.digest();
|
|
79
|
+
|
|
80
|
+
// 算出したハッシュと、コンストラクターで受け取った期待値を比較します。
|
|
81
|
+
if (finalHash !== this.#expectedHash) {
|
|
82
|
+
// データの整合性が保たれていないため、詳細な情報を添えてエラーを投げます。
|
|
83
|
+
throw new Error(`Hash mismatch! Expected: ${this.#expectedHash}, but got: ${finalHash}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.#hasher = null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* ダウンロード中にエラーが発生した際に呼び出されるメソッドです。
|
|
91
|
+
*
|
|
92
|
+
* 保持しているハッシュ計算機のインスタンスを破棄し、リソースを解放します。
|
|
93
|
+
*/
|
|
94
|
+
public onError(): void {
|
|
95
|
+
// 計算途中の状態を破棄するため、プロパティーを null にリセットします。
|
|
96
|
+
this.#hasher = null;
|
|
97
|
+
}
|
|
98
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// メイン
|
|
2
|
+
|
|
3
|
+
export type * from "./dl-once.js";
|
|
4
|
+
export { default, default as downloadOnce } from "./dl-once.js";
|
|
5
|
+
|
|
6
|
+
// フック
|
|
7
|
+
|
|
8
|
+
export type * from "./hash-verification-hook.js";
|
|
9
|
+
export { default as HashVerificationHook } from "./hash-verification-hook.js";
|
|
10
|
+
|
|
11
|
+
export type * from "./md5-verification-hook.js";
|
|
12
|
+
export { default as Md5VerificationHook } from "./md5-verification-hook.js";
|
|
13
|
+
|
|
14
|
+
export type * from "./sha256-verification-hook.js";
|
|
15
|
+
export { default as Sha256VerificationHook } from "./sha256-verification-hook.js";
|
|
16
|
+
|
|
17
|
+
// キャッシュストレージ
|
|
18
|
+
|
|
19
|
+
export type * from "./cache-storage.js";
|
|
20
|
+
|
|
21
|
+
export type * from "./indexeddb-cache-storage.js";
|
|
22
|
+
export { default as IndexedDbCacheStorage } from "./indexeddb-cache-storage.js";
|
|
23
|
+
|
|
24
|
+
export type * from "./memory-cache-storage.js";
|
|
25
|
+
export { default as MemoryCacheStorage } from "./memory-cache-storage.js";
|
|
26
|
+
|
|
27
|
+
export type * from "./node-fs-cache-storage.js";
|
|
28
|
+
export { default as NodeFsCacheStorage } from "./node-fs-cache-storage.js";
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { Asyncmux, asyncmux } from "asyncmux";
|
|
2
|
+
import { type IDBPDatabase, openDB } from "idb";
|
|
3
|
+
import { tryCaptureStackTrace } from "try-capture-stack-trace";
|
|
4
|
+
import type { ClearOptions, CloseOptions, ICacheStorage, OpenOptions } from "./cache-storage.js";
|
|
5
|
+
import type { GetReaderArgs, GetWriterArgs, ICacheHandle, IWriter } from "./dl-once.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* キャッシュのメタデータ情報を定義する型です。
|
|
9
|
+
*/
|
|
10
|
+
type Metadata = {
|
|
11
|
+
/**
|
|
12
|
+
* 分割されたチャンクの総数です。
|
|
13
|
+
*/
|
|
14
|
+
chunkCount: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* キャッシュを一意に識別するためのキーの型です。
|
|
19
|
+
*/
|
|
20
|
+
type CacheKey = string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* IndexedDB のオブジェクトストア構造を定義するスキーマ型です。
|
|
24
|
+
*/
|
|
25
|
+
type IdbSchema = {
|
|
26
|
+
/**
|
|
27
|
+
* バイナリーデータをチャンク単位で格納するストアです。
|
|
28
|
+
*
|
|
29
|
+
* キーは `${CacheKey}:${number}` の形式です。
|
|
30
|
+
*/
|
|
31
|
+
buff: {
|
|
32
|
+
key: `${CacheKey}:${number}`;
|
|
33
|
+
value: Uint8Array<ArrayBuffer>;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* 各キャッシュキーに対応するメタデータを格納するストアです。
|
|
37
|
+
*/
|
|
38
|
+
meta: {
|
|
39
|
+
key: CacheKey;
|
|
40
|
+
value: Metadata;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 型定義された IndexedDB データベースのインスタンス型です。
|
|
46
|
+
*/
|
|
47
|
+
type Idb = IDBPDatabase<IdbSchema>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* IndexedDB を開くためのユーティリティー関数です。
|
|
51
|
+
*/
|
|
52
|
+
const openIdb = openDB<IdbSchema>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 中断理由が設定されていないことを示すための初期値シンボルです。
|
|
56
|
+
*/
|
|
57
|
+
const NONE = Symbol();
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 指定された非同期破棄処理を実行する AsyncDisposable オブジェクトを作成します。
|
|
61
|
+
*
|
|
62
|
+
* @param onAsyncDispose 破棄時に実行される非同期関数です。
|
|
63
|
+
* @returns 非同期破棄インターフェースを実装したオブジェクトです。
|
|
64
|
+
*/
|
|
65
|
+
function defer(onAsyncDispose: () => Promise<void>): AsyncDisposable {
|
|
66
|
+
return { [Symbol.asyncDispose]: onAsyncDispose };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* IndexedDB に対してキャッシュデータの書き込みを行うクラスです。
|
|
71
|
+
*/
|
|
72
|
+
class IndexedDbCacheWriter implements IWriter {
|
|
73
|
+
/**
|
|
74
|
+
* ストレージ本体への参照です。
|
|
75
|
+
*/
|
|
76
|
+
readonly #storage: IndexedDbCacheStorage;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 使用する IndexedDB インスタンスです。
|
|
80
|
+
*/
|
|
81
|
+
readonly #db: Idb;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 対象となるキャッシュキーです。
|
|
85
|
+
*/
|
|
86
|
+
readonly #cacheKey: string;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 中断された際の理由です。
|
|
90
|
+
*/
|
|
91
|
+
#reason: unknown;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* ライターが閉じられているかどうかを示すフラグです。
|
|
95
|
+
*/
|
|
96
|
+
#isClosed: boolean;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* チャンクのカウンターです。
|
|
100
|
+
*/
|
|
101
|
+
#chunkCounter: number;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* IndexedDbCacheWriter のインスタンスを初期化します。
|
|
105
|
+
*
|
|
106
|
+
* @param storage 親となるストレージインスタンスです。
|
|
107
|
+
* @param db 操作対象のデータベースです。
|
|
108
|
+
* @param cacheKey キャッシュの識別キーです。
|
|
109
|
+
*/
|
|
110
|
+
constructor(storage: IndexedDbCacheStorage, db: Idb, cacheKey: string) {
|
|
111
|
+
this.#storage = storage;
|
|
112
|
+
this.#db = db;
|
|
113
|
+
this.#cacheKey = cacheKey;
|
|
114
|
+
this.#reason = NONE;
|
|
115
|
+
this.#isClosed = false;
|
|
116
|
+
this.#chunkCounter = 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* インスタンスの状態が有効であるか確認します。
|
|
121
|
+
*
|
|
122
|
+
* ストレージが閉じられている、またはライターが閉じられている場合にエラーを投げます。
|
|
123
|
+
*/
|
|
124
|
+
#assertOk(): void {
|
|
125
|
+
// 親ストレージの稼働状態を確認します。
|
|
126
|
+
if (!this.#storage.isOpen) {
|
|
127
|
+
const error = new Error("IndexedDbCacheStorage is closed");
|
|
128
|
+
tryCaptureStackTrace(error, this.#assertOk);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// このライター自体の状態を確認します。
|
|
133
|
+
if (this.#isClosed) {
|
|
134
|
+
// 正常に閉じられたのか、エラーで中断されたのかを判定します。
|
|
135
|
+
if (this.#reason === NONE) {
|
|
136
|
+
const error = new Error("IndexedDbCacheWriter is closed");
|
|
137
|
+
tryCaptureStackTrace(error, this.#assertOk);
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 中断理由がある場合は、その理由(エラー等)をそのまま投げます。
|
|
142
|
+
throw this.#reason;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* チャンクデータを IndexedDB に書き込みます。
|
|
148
|
+
*
|
|
149
|
+
* @param chunkData 書き込むバイナリーデータです。
|
|
150
|
+
*/
|
|
151
|
+
@asyncmux
|
|
152
|
+
public async write(chunkData: Uint8Array<ArrayBuffer>): Promise<void> {
|
|
153
|
+
this.#assertOk();
|
|
154
|
+
|
|
155
|
+
// インデックスをインクリメントしながら、各チャンクを個別のキーで保存します。
|
|
156
|
+
const index = this.#chunkCounter;
|
|
157
|
+
await this.#db.put("buff", chunkData, `${this.#cacheKey}:${index}`);
|
|
158
|
+
this.#chunkCounter += 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 書き込みを完了し、メタデータを保存してライターを閉じます。
|
|
163
|
+
*/
|
|
164
|
+
@asyncmux
|
|
165
|
+
public async close(): Promise<void> {
|
|
166
|
+
this.#assertOk();
|
|
167
|
+
|
|
168
|
+
// これまでに書き込んだチャンクの総数をメタデータとして保存します。
|
|
169
|
+
const chunkCount = this.#chunkCounter;
|
|
170
|
+
const meta: Metadata = {
|
|
171
|
+
chunkCount,
|
|
172
|
+
};
|
|
173
|
+
await this.#db.put("meta", meta, this.#cacheKey);
|
|
174
|
+
|
|
175
|
+
this.#isClosed = true;
|
|
176
|
+
this.#chunkCounter = 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 書き込みを中断し、これまでに書き込んだ一時的なチャンクデータを削除します。
|
|
181
|
+
*
|
|
182
|
+
* @param reason 中断した理由です。
|
|
183
|
+
*/
|
|
184
|
+
@asyncmux
|
|
185
|
+
public async abort(reason: unknown): Promise<void> {
|
|
186
|
+
this.#assertOk();
|
|
187
|
+
|
|
188
|
+
const chunkCount = this.#chunkCounter;
|
|
189
|
+
|
|
190
|
+
this.#reason = reason;
|
|
191
|
+
this.#isClosed = true;
|
|
192
|
+
this.#chunkCounter = 0;
|
|
193
|
+
|
|
194
|
+
// 書き込まれたチャンクがない場合は削除処理をスキップします。
|
|
195
|
+
if (chunkCount === 0) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 0 から chunkCount - 1 までの範囲キーを作成し、一括削除します。
|
|
200
|
+
const range = IDBKeyRange.bound(
|
|
201
|
+
`${this.#cacheKey}:0`,
|
|
202
|
+
`${this.#cacheKey}:${chunkCount - 1}`,
|
|
203
|
+
);
|
|
204
|
+
await this.#db.delete("buff", range);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* キャッシュされたデータへのアクセス(読み取り・書き込み)を管理するハンドルクラスです。
|
|
210
|
+
*/
|
|
211
|
+
class IndexedDbCacheHandle implements ICacheHandle {
|
|
212
|
+
/**
|
|
213
|
+
* 親ストレージへの参照です。
|
|
214
|
+
*/
|
|
215
|
+
readonly #storage: IndexedDbCacheStorage;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* データベースインスタンスです。
|
|
219
|
+
*/
|
|
220
|
+
readonly #db: Idb;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 排他制御用のミューテックスです。
|
|
224
|
+
*/
|
|
225
|
+
readonly #mux: Asyncmux;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* キャッシュキーです。
|
|
229
|
+
*/
|
|
230
|
+
readonly #cacheKey: string;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* IndexedDbCacheHandle のインスタンスを初期化します。
|
|
234
|
+
*
|
|
235
|
+
* @param storage ストレージインスタンスです。
|
|
236
|
+
* @param db データベースです。
|
|
237
|
+
* @param mux 同期用オブジェクトです。
|
|
238
|
+
* @param cacheKey キーです。
|
|
239
|
+
*/
|
|
240
|
+
public constructor(storage: IndexedDbCacheStorage, db: Idb, mux: Asyncmux, cacheKey: string) {
|
|
241
|
+
this.#storage = storage;
|
|
242
|
+
this.#db = db;
|
|
243
|
+
this.#mux = mux;
|
|
244
|
+
this.#cacheKey = cacheKey;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 親ストレージの状態を確認します。
|
|
249
|
+
*/
|
|
250
|
+
#assertOk(): void {
|
|
251
|
+
if (!this.#storage.isOpen) {
|
|
252
|
+
const error = new Error("IndexedDbCacheStorage is closed");
|
|
253
|
+
tryCaptureStackTrace(error, this.#assertOk);
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* キャッシュデータを読み取るためのジェネレーターを取得します。
|
|
260
|
+
*
|
|
261
|
+
* @param args 読み取りオプション(Signal 等)です。
|
|
262
|
+
* @returns キャッシュが存在する場合は AsyncGenerator、存在しない場合は null を返します。
|
|
263
|
+
*/
|
|
264
|
+
public async getReader(args: GetReaderArgs): Promise<AsyncGenerator<any, void, unknown> | null> {
|
|
265
|
+
const { signal } = args;
|
|
266
|
+
|
|
267
|
+
// 読み取りロックを取得します。
|
|
268
|
+
using _1 = await this.#mux.rLock({ signal });
|
|
269
|
+
|
|
270
|
+
this.#assertOk();
|
|
271
|
+
|
|
272
|
+
// メタデータを取得するために読み取り専用トランザクションを開始します。
|
|
273
|
+
const tx = this.#db.transaction(["meta"], "readonly");
|
|
274
|
+
|
|
275
|
+
// 外部からの中断シグナルをトランザクションの中断に連携させます。
|
|
276
|
+
signal?.addEventListener("abort", tx.abort, { once: true });
|
|
277
|
+
await using _2 = defer(async () => {
|
|
278
|
+
signal?.removeEventListener("abort", tx.abort);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const metaStore = tx.objectStore("meta");
|
|
282
|
+
const meta = await metaStore.get(this.#cacheKey);
|
|
283
|
+
|
|
284
|
+
// キャッシュ(メタデータ)が存在しない場合は null を返して終了します。
|
|
285
|
+
if (!meta) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const db = this.#db;
|
|
290
|
+
const storage = this.#storage;
|
|
291
|
+
const cacheKey = this.#cacheKey;
|
|
292
|
+
const { chunkCount } = meta;
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 実際のチャンク読み取りを行う内部非同期ジェネレーターです。
|
|
296
|
+
*/
|
|
297
|
+
async function* createReader() {
|
|
298
|
+
// チャンク取得用のトランザクションを開始します。
|
|
299
|
+
const tx = db.transaction(["buff"], "readonly");
|
|
300
|
+
const buffStore = tx.objectStore("buff");
|
|
301
|
+
|
|
302
|
+
for (let index = 0; index < chunkCount; index++) {
|
|
303
|
+
// 読み取りの途中でストレージが閉じられていないか毎ステップ確認します。
|
|
304
|
+
if (!storage.isOpen) {
|
|
305
|
+
throw new Error("IndexedDbCacheStorage is closed");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const chunk = await buffStore.get(`${cacheKey}:${index}`);
|
|
309
|
+
if (!chunk) {
|
|
310
|
+
throw new Error(`Missing chunk at index ${index}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
yield chunk;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return createReader();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* キャッシュにデータを書き込むためのライターを取得します。
|
|
322
|
+
*
|
|
323
|
+
* @param args 書き込みオプションです。
|
|
324
|
+
* @returns IWriter インターフェースを実装したライターインスタンスです。
|
|
325
|
+
*/
|
|
326
|
+
public async getWriter(args: GetWriterArgs): Promise<IWriter> {
|
|
327
|
+
const { signal } = args;
|
|
328
|
+
|
|
329
|
+
// 書き込みの排他制御のためにロックを取得します。
|
|
330
|
+
using _1 = await this.#mux.rLock({ signal });
|
|
331
|
+
|
|
332
|
+
this.#assertOk();
|
|
333
|
+
|
|
334
|
+
return new IndexedDbCacheWriter(this.#storage, this.#db, this.#cacheKey);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* IndexedDB をバックエンドとしたキャッシュストレージの実装クラスです。
|
|
340
|
+
*/
|
|
341
|
+
export default class IndexedDbCacheStorage implements ICacheStorage, AsyncDisposable {
|
|
342
|
+
/**
|
|
343
|
+
* データベース接続インスタンスです。閉じられているときは null になります。
|
|
344
|
+
*/
|
|
345
|
+
#db: Idb | null;
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* クラス全体での非同期操作の整合性を保つためのミューテックスです。
|
|
349
|
+
*/
|
|
350
|
+
readonly #mux: Asyncmux;
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* 使用する IndexedDB のデータベース名です。
|
|
354
|
+
*/
|
|
355
|
+
readonly #dbName: string;
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* IndexedDbCacheStorage のインスタンスを初期化します。
|
|
359
|
+
*
|
|
360
|
+
* @param dbName データベース名です。指定されない場合はデフォルト名が使用されます。
|
|
361
|
+
*/
|
|
362
|
+
public constructor(dbName: string | undefined) {
|
|
363
|
+
this.#db = null;
|
|
364
|
+
this.#mux = new Asyncmux();
|
|
365
|
+
this.#dbName = dbName ?? "dl-once";
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* 接続が有効であるか確認します。
|
|
370
|
+
*/
|
|
371
|
+
#assertOk(): void {
|
|
372
|
+
if (!this.isOpen) {
|
|
373
|
+
const error = new Error("IndexedDbCacheStorage is closed");
|
|
374
|
+
tryCaptureStackTrace(error, this.#assertOk);
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* ストレージが開いているかどうかを返します。
|
|
381
|
+
*/
|
|
382
|
+
public get isOpen(): boolean {
|
|
383
|
+
return !!this.#db;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* データベースを開き、初期化を行います。
|
|
388
|
+
*
|
|
389
|
+
* @param options オープン時のオプション(Signal 等)です。
|
|
390
|
+
*/
|
|
391
|
+
public async open(options: OpenOptions | undefined = {}): Promise<void> {
|
|
392
|
+
const { signal } = options;
|
|
393
|
+
|
|
394
|
+
// 二重オープンを防止するためロックを取得します。
|
|
395
|
+
using _ = await this.#mux.lock({ signal });
|
|
396
|
+
|
|
397
|
+
if (this.#db) {
|
|
398
|
+
throw new Error("Storage is already open");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// データベースを開き、必要に応じてオブジェクトストアを作成します。
|
|
402
|
+
this.#db = await openIdb(this.#dbName, 1, {
|
|
403
|
+
upgrade(db) {
|
|
404
|
+
if (!db.objectStoreNames.contains("meta")) {
|
|
405
|
+
db.createObjectStore("meta");
|
|
406
|
+
}
|
|
407
|
+
if (!db.objectStoreNames.contains("buff")) {
|
|
408
|
+
db.createObjectStore("buff");
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* データベース接続を閉じます。
|
|
416
|
+
*
|
|
417
|
+
* @param options クローズ時のオプションです。
|
|
418
|
+
*/
|
|
419
|
+
public async close(options: CloseOptions | undefined = {}): Promise<void> {
|
|
420
|
+
const { signal } = options;
|
|
421
|
+
|
|
422
|
+
using _ = await this.#mux.lock({ signal });
|
|
423
|
+
|
|
424
|
+
this.#assertOk();
|
|
425
|
+
|
|
426
|
+
this.#db!.close();
|
|
427
|
+
this.#db = null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* `using` 構文等で利用される非同期破棄メソッドです。
|
|
432
|
+
*
|
|
433
|
+
* 接続が開いている場合は閉じます。
|
|
434
|
+
*/
|
|
435
|
+
public async [Symbol.asyncDispose](): Promise<void> {
|
|
436
|
+
using _ = await this.#mux.lock();
|
|
437
|
+
|
|
438
|
+
this.#db?.close();
|
|
439
|
+
this.#db = null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* キャッシュされたデータを削除します。
|
|
444
|
+
*
|
|
445
|
+
* 引数の組み合わせにより、特定のキーの削除または全削除を行います。
|
|
446
|
+
*/
|
|
447
|
+
public clear(
|
|
448
|
+
cacheKey?: string | undefined,
|
|
449
|
+
options?: Omit<ClearOptions, "cacheKey"> | undefined,
|
|
450
|
+
): Promise<void>;
|
|
451
|
+
|
|
452
|
+
public clear(options?: ClearOptions | undefined): Promise<void>;
|
|
453
|
+
|
|
454
|
+
public async clear(
|
|
455
|
+
cacheKeyOrOptions?: string | ClearOptions | undefined,
|
|
456
|
+
options: Omit<ClearOptions, "cacheKey"> | undefined = {},
|
|
457
|
+
): Promise<void> {
|
|
458
|
+
// 引数の型に応じてオプションを正規化します。
|
|
459
|
+
const {
|
|
460
|
+
signal,
|
|
461
|
+
cacheKey,
|
|
462
|
+
} = typeof cacheKeyOrOptions === "object"
|
|
463
|
+
? cacheKeyOrOptions
|
|
464
|
+
: {
|
|
465
|
+
...options,
|
|
466
|
+
cacheKey: cacheKeyOrOptions,
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// 削除操作の整合性を保つためロックを取得します。
|
|
470
|
+
using _1 = await this.#mux.lock({ signal });
|
|
471
|
+
|
|
472
|
+
this.#assertOk();
|
|
473
|
+
|
|
474
|
+
// 読み書きトランザクションを開始します。
|
|
475
|
+
const tx = this.#db!.transaction(["buff", "meta"], "readwrite");
|
|
476
|
+
signal?.addEventListener("abort", tx.abort, { once: true });
|
|
477
|
+
|
|
478
|
+
// トランザクション完了の待機とイベントリスナーの解除を行います。
|
|
479
|
+
await using _2 = defer(async () => {
|
|
480
|
+
try {
|
|
481
|
+
await tx.done;
|
|
482
|
+
} finally {
|
|
483
|
+
signal?.removeEventListener("abort", tx.abort);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
if (cacheKey !== undefined) {
|
|
488
|
+
// 特定のキャッシュキーのみを削除する場合のロジックです。
|
|
489
|
+
const metaStore = tx.objectStore("meta");
|
|
490
|
+
const meta = await metaStore.get(cacheKey);
|
|
491
|
+
if (!meta) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// まずメタデータを削除します。
|
|
496
|
+
await metaStore.delete(cacheKey);
|
|
497
|
+
|
|
498
|
+
const { chunkCount } = meta;
|
|
499
|
+
const buffStore = tx.objectStore("buff");
|
|
500
|
+
|
|
501
|
+
// チャンク数に応じて削除戦略を切り替えます。
|
|
502
|
+
if (chunkCount === 0) {
|
|
503
|
+
// 削除するバッファーはありません。
|
|
504
|
+
} else if (chunkCount === 1) {
|
|
505
|
+
// チャンクが 1 つだけなら直接削除します。
|
|
506
|
+
await buffStore.delete(`${cacheKey}:0`);
|
|
507
|
+
} else {
|
|
508
|
+
// チャンクが複数あるなら、範囲指定で一括削除します。
|
|
509
|
+
const keyRange = IDBKeyRange.bound(
|
|
510
|
+
`${cacheKey}:0`,
|
|
511
|
+
`${cacheKey}:${chunkCount - 1}`,
|
|
512
|
+
);
|
|
513
|
+
await buffStore.delete(keyRange);
|
|
514
|
+
}
|
|
515
|
+
} else {
|
|
516
|
+
// キャッシュキーが指定されていない場合は、すべてのストアの内容を消去します。
|
|
517
|
+
const metaStore = tx.objectStore("meta");
|
|
518
|
+
await metaStore.clear();
|
|
519
|
+
const buffStore = tx.objectStore("buff");
|
|
520
|
+
await buffStore.clear();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* 指定したキーに対するキャッシュ操作ハンドルを作成します。
|
|
526
|
+
*
|
|
527
|
+
* @param key キャッシュキーです。
|
|
528
|
+
* @returns キャッシュハンドルインスタンスです。
|
|
529
|
+
*/
|
|
530
|
+
public createCacheHandle(key: string): ICacheHandle {
|
|
531
|
+
this.#assertOk();
|
|
532
|
+
|
|
533
|
+
return new IndexedDbCacheHandle(this, this.#db!, this.#mux, key);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createMD5 } from "hash-wasm";
|
|
2
|
+
import HashVerificationHook from "./hash-verification-hook.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ダウンロードされたデータの MD5 ハッシュ値を検証するためのフッククラスです。
|
|
6
|
+
*
|
|
7
|
+
* `hash-wasm` ライブラリーを使用して、ストリーム読み込みに合わせて逐次的にハッシュ計算を行い、完了時に期待されるハッシュ値と一致するかを判定します。
|
|
8
|
+
*/
|
|
9
|
+
export default class Md5VerificationHook extends HashVerificationHook {
|
|
10
|
+
/**
|
|
11
|
+
* Md5VerificationHook の新しいインスタンスを生成します。
|
|
12
|
+
*
|
|
13
|
+
* @param expectedHash 比較対象となる 16 進数形式の期待されるハッシュ値です。
|
|
14
|
+
*/
|
|
15
|
+
public constructor(expectedHash: string) {
|
|
16
|
+
super(createMD5, expectedHash);
|
|
17
|
+
}
|
|
18
|
+
}
|