@unikvs/indexeddb 0.0.12 → 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/README.md CHANGED
@@ -1,3 +1,3 @@
1
1
  # @unikvs/indexeddb
2
2
 
3
- [Documentation](https://tai-kun.github.io/unikvs/packages/indexeddb)
3
+ [Document](https://tai-kun.github.io/unikvs/)
@@ -0,0 +1,102 @@
1
+ import { IStorage } from "@unikvs/core";
2
+ //#region ../src/indexeddb.d.ts
3
+ /**
4
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
5
+ */
6
+ type IndexeddbOptions = {};
7
+ /**
8
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
9
+ */
10
+ declare class Indexeddb implements IStorage {
11
+ #private;
12
+ /**
13
+ * ストレージ名です。常に `Indexeddb` を返します。
14
+ */
15
+ readonly name: string;
16
+ /**
17
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
18
+ */
19
+ constructor(dbName?: string, storeName?: string, _options?: IndexeddbOptions);
20
+ /**
21
+ * ストレージが開いているかどうかを返します。
22
+ *
23
+ * @returns データベース接続があれば true を返します。
24
+ */
25
+ get isOpen(): boolean;
26
+ /**
27
+ * データベースを開きます。並行した呼び出しは一つの接続処理に束ねます。
28
+ *
29
+ * @param c 中断を知らせるシグナルを持つコンテキストです。
30
+ *
31
+ * @returns 完了を知らせるプロミスです。
32
+ */
33
+ open(c: IStorage.OpenContext): Promise<void>;
34
+ /**
35
+ * データベース接続を閉じます。
36
+ *
37
+ * @param c 中断を知らせるシグナルを持つコンテキストです。
38
+ *
39
+ * @returns 完了を知らせるプロミスです。
40
+ */
41
+ close(c: IStorage.CloseContext): Promise<void>;
42
+ /**
43
+ * 指定したキーに値を保存します。Blob は中間形式に変換して保存します。
44
+ *
45
+ * @param c 書き込み対象のキーを持つコンテキストです。
46
+ * @param data 保存する値です。
47
+ *
48
+ * @returns 完了を知らせるプロミスです。
49
+ */
50
+ write(c: IStorage.WriteContext, data: any): Promise<void>;
51
+ /**
52
+ * 指定したキーの値を読み出します。中間形式は Blob または File に戻します。
53
+ *
54
+ * @param c 読み出すキーを持つコンテキストです。
55
+ *
56
+ * @returns 保存されている値です。
57
+ */
58
+ read(c: IStorage.ReadContext): Promise<any>;
59
+ /**
60
+ * 指定したキーが存在するかどうかを返します。
61
+ *
62
+ * @param c 確認するキーを持つコンテキストです。
63
+ *
64
+ * @returns キーが存在すれば true を返します。
65
+ */
66
+ exists(c: IStorage.ExistsContext): Promise<boolean>;
67
+ /**
68
+ * 指定したキーを削除します。
69
+ *
70
+ * @param c 削除するキーを持つコンテキストです。
71
+ *
72
+ * @returns 完了を知らせるプロミスです。
73
+ */
74
+ delete(c: IStorage.DeleteContext): Promise<void>;
75
+ /**
76
+ * オブジェクトストアのすべての値を削除します。
77
+ *
78
+ * @param c 中断を知らせるシグナルを持つコンテキストです。
79
+ *
80
+ * @returns 完了を知らせるプロミスです。
81
+ */
82
+ clear(c: IStorage.ClearContext): Promise<void>;
83
+ /**
84
+ * Uint8Array を書き込むためのストリームを作成します。close 時にまとめて保存します。
85
+ *
86
+ * @param c 書き込み対象のキーを持つコンテキストです。
87
+ *
88
+ * @returns Uint8Array を受け付ける書き込みストリームです。
89
+ */
90
+ getWritable(c: IStorage.GetWritableContext): WritableStream<Uint8Array>;
91
+ /**
92
+ * 保存済みの値を読み出すストリームを作成します。
93
+ *
94
+ * @param c 読み出すキーを持つコンテキストです。
95
+ *
96
+ * @returns Uint8Array を送出する読み取りストリームです。
97
+ */
98
+ getReadable(c: IStorage.GetReadableContext): ReadableStream<Uint8Array>;
99
+ }
100
+ //#endregion
101
+ export { Indexeddb, type IndexeddbOptions };
102
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/indexeddb.ts"],"mappings":";;;;;KAMY;;;;cAkGS,qBAAqB;;;;;WAIxB;;;;EA2BhB,YACE,iBACA,oBACA,WAAU;;;;;;MAcD;;;;;;;;EAWX,KAAkB,GAAG,SAAS,cAAc;;;;;;;;EAqD5C,MAAmB,GAAG,SAAS,eAAe;;;;;;;;;EAsB9C,MAAmB,GAAG,SAAS,cAAc,YAAY;;;;;;;;EA0BzD,KAAkB,GAAG,SAAS,cAAc;;;;;;;;EAoC5C,OAAoB,GAAG,SAAS,gBAAgB;;;;;;;;EAoBhD,OAAoB,GAAG,SAAS,gBAAgB;;;;;;;;EAgBhD,MAAmB,GAAG,SAAS,eAAe;;;;;;;;EAgB9C,YAAmB,GAAG,SAAS,qBAAqB,eAAe;;;;;;;;EAyCnE,YAAmB,GAAG,SAAS,qBAAqB,eAAe"}
package/dist/index.js ADDED
@@ -0,0 +1,304 @@
1
+ import { openDB } from "idb";
2
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/checkPrivateRedeclaration.js
3
+ function _checkPrivateRedeclaration(e, t) {
4
+ if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object");
5
+ }
6
+ //#endregion
7
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/classPrivateMethodInitSpec.js
8
+ function _classPrivateMethodInitSpec(e, a) {
9
+ _checkPrivateRedeclaration(e, a), a.add(e);
10
+ }
11
+ //#endregion
12
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/classPrivateFieldInitSpec.js
13
+ function _classPrivateFieldInitSpec(e, t, a) {
14
+ _checkPrivateRedeclaration(e, t), t.set(e, a);
15
+ }
16
+ //#endregion
17
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/assertClassBrand.js
18
+ function _assertClassBrand(e, t, n) {
19
+ if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n;
20
+ throw new TypeError("Private element is not present on this object");
21
+ }
22
+ //#endregion
23
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/classPrivateFieldSet2.js
24
+ function _classPrivateFieldSet2(s, a, r) {
25
+ return s.set(_assertClassBrand(s, a), r), r;
26
+ }
27
+ //#endregion
28
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/classPrivateFieldGet2.js
29
+ function _classPrivateFieldGet2(s, a) {
30
+ return s.get(_assertClassBrand(s, a));
31
+ }
32
+ //#endregion
33
+ //#region ../src/indexeddb.ts
34
+ /**
35
+ * 保存した中間形式かどうかを判定します。
36
+ *
37
+ * @param value 判定する値です。
38
+ *
39
+ * @returns 中間形式であれば true を返します。
40
+ */
41
+ function isStoredBlob(value) {
42
+ if (typeof value !== "object" || value === null) return false;
43
+ return value["unikvsBlob"] === true;
44
+ }
45
+ /**
46
+ * Blob を IndexedDB に保存できる中間形式に変換します。File の名前と更新時刻も保持します。
47
+ *
48
+ * @param signal 中断を知らせるシグナルです。
49
+ * @param data 変換する Blob です。
50
+ *
51
+ * @returns 中間形式に変換した値です。
52
+ */
53
+ async function toStoredBlob(signal, data) {
54
+ const buffer = await data.arrayBuffer();
55
+ signal.throwIfAborted();
56
+ const isFile = typeof File !== "undefined" && data instanceof File;
57
+ return {
58
+ name: isFile ? data.name : "",
59
+ type: data.type,
60
+ buffer,
61
+ unikvsBlob: true,
62
+ unikvsFile: isFile,
63
+ lastModified: isFile ? data.lastModified : 0
64
+ };
65
+ }
66
+ /**
67
+ * 中間形式を Blob または File に戻します。
68
+ *
69
+ * @param stored 中間形式の値です。
70
+ *
71
+ * @returns 復元した Blob または File です。
72
+ */
73
+ function fromStoredBlob(stored) {
74
+ if (stored.unikvsFile) return new File([stored.buffer], stored.name, {
75
+ type: stored.type,
76
+ lastModified: stored.lastModified
77
+ });
78
+ return new Blob([stored.buffer], { type: stored.type });
79
+ }
80
+ var _db = /* @__PURE__ */ new WeakMap();
81
+ var _opening = /* @__PURE__ */ new WeakMap();
82
+ var _dbName = /* @__PURE__ */ new WeakMap();
83
+ var _storeName = /* @__PURE__ */ new WeakMap();
84
+ var _Indexeddb_brand = /* @__PURE__ */ new WeakSet();
85
+ /**
86
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
87
+ */
88
+ var Indexeddb = class {
89
+ /**
90
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
91
+ */
92
+ constructor(dbName = "unikvs_db", storeName = "kvs_store", _options = {}) {
93
+ _classPrivateMethodInitSpec(this, _Indexeddb_brand);
94
+ _classPrivateFieldInitSpec(this, _db, void 0);
95
+ _classPrivateFieldInitSpec(this, _opening, void 0);
96
+ _classPrivateFieldInitSpec(this, _dbName, void 0);
97
+ _classPrivateFieldInitSpec(this, _storeName, void 0);
98
+ this.name = "Indexeddb";
99
+ _classPrivateFieldSet2(_db, this, null);
100
+ _classPrivateFieldSet2(_opening, this, null);
101
+ _classPrivateFieldSet2(_dbName, this, dbName);
102
+ _classPrivateFieldSet2(_storeName, this, storeName);
103
+ }
104
+ /**
105
+ * ストレージが開いているかどうかを返します。
106
+ *
107
+ * @returns データベース接続があれば true を返します。
108
+ */
109
+ get isOpen() {
110
+ return _classPrivateFieldGet2(_db, this) !== null;
111
+ }
112
+ /**
113
+ * データベースを開きます。並行した呼び出しは一つの接続処理に束ねます。
114
+ *
115
+ * @param c 中断を知らせるシグナルを持つコンテキストです。
116
+ *
117
+ * @returns 完了を知らせるプロミスです。
118
+ */
119
+ async open(c) {
120
+ c.signal.throwIfAborted();
121
+ if (_classPrivateFieldGet2(_db, this) !== null) return;
122
+ if (_classPrivateFieldGet2(_opening, this) !== null) {
123
+ await _classPrivateFieldGet2(_opening, this);
124
+ c.signal.throwIfAborted();
125
+ return;
126
+ }
127
+ const opening = openDB(_classPrivateFieldGet2(_dbName, this), 1, { upgrade: (upgraded) => {
128
+ if (!upgraded.objectStoreNames.contains(_classPrivateFieldGet2(_storeName, this))) upgraded.createObjectStore(_classPrivateFieldGet2(_storeName, this));
129
+ } });
130
+ _classPrivateFieldSet2(_opening, this, opening);
131
+ let opened = null;
132
+ try {
133
+ opened = await opening;
134
+ c.signal.throwIfAborted();
135
+ _classPrivateFieldSet2(_db, this, opened);
136
+ opened = null;
137
+ } catch (ex) {
138
+ if (opened !== null) try {
139
+ opened.close();
140
+ } catch {}
141
+ throw ex;
142
+ } finally {
143
+ _classPrivateFieldSet2(_opening, this, null);
144
+ }
145
+ }
146
+ /**
147
+ * データベース接続を閉じます。
148
+ *
149
+ * @param c 中断を知らせるシグナルを持つコンテキストです。
150
+ *
151
+ * @returns 完了を知らせるプロミスです。
152
+ */
153
+ async close(c) {
154
+ c.signal.throwIfAborted();
155
+ const db = _classPrivateFieldGet2(_db, this);
156
+ if (db === null) throw new TypeError("Indexeddb is not open. Call open() first.");
157
+ await Promise.resolve();
158
+ db.close();
159
+ _classPrivateFieldSet2(_db, this, null);
160
+ }
161
+ /**
162
+ * 指定したキーに値を保存します。Blob は中間形式に変換して保存します。
163
+ *
164
+ * @param c 書き込み対象のキーを持つコンテキストです。
165
+ * @param data 保存する値です。
166
+ *
167
+ * @returns 完了を知らせるプロミスです。
168
+ */
169
+ async write(c, data) {
170
+ c.signal.throwIfAborted();
171
+ const db = _assertClassBrand(_Indexeddb_brand, this, _getDb).call(this);
172
+ if (typeof Blob !== "undefined" && data instanceof Blob) {
173
+ const stored = await toStoredBlob(c.signal, data);
174
+ await db.put(_classPrivateFieldGet2(_storeName, this), stored, c.key);
175
+ c.signal.throwIfAborted();
176
+ return;
177
+ }
178
+ await db.put(_classPrivateFieldGet2(_storeName, this), data, c.key);
179
+ c.signal.throwIfAborted();
180
+ }
181
+ /**
182
+ * 指定したキーの値を読み出します。中間形式は Blob または File に戻します。
183
+ *
184
+ * @param c 読み出すキーを持つコンテキストです。
185
+ *
186
+ * @returns 保存されている値です。
187
+ */
188
+ async read(c) {
189
+ c.signal.throwIfAborted();
190
+ const db = _assertClassBrand(_Indexeddb_brand, this, _getDb).call(this);
191
+ const value = await db.get(_classPrivateFieldGet2(_storeName, this), c.key);
192
+ c.signal.throwIfAborted();
193
+ if (isStoredBlob(value)) return fromStoredBlob(value);
194
+ if (value === void 0) {
195
+ const count = await db.count(_classPrivateFieldGet2(_storeName, this), c.key);
196
+ c.signal.throwIfAborted();
197
+ if (count === 0) throw new DOMException("A requested file or directory could not be found at the time an operation was processed.", "NotFoundError");
198
+ }
199
+ return value;
200
+ }
201
+ /**
202
+ * 指定したキーが存在するかどうかを返します。
203
+ *
204
+ * @param c 確認するキーを持つコンテキストです。
205
+ *
206
+ * @returns キーが存在すれば true を返します。
207
+ */
208
+ async exists(c) {
209
+ c.signal.throwIfAborted();
210
+ const count = await _assertClassBrand(_Indexeddb_brand, this, _getDb).call(this).count(_classPrivateFieldGet2(_storeName, this), c.key);
211
+ c.signal.throwIfAborted();
212
+ return count > 0;
213
+ }
214
+ /**
215
+ * 指定したキーを削除します。
216
+ *
217
+ * @param c 削除するキーを持つコンテキストです。
218
+ *
219
+ * @returns 完了を知らせるプロミスです。
220
+ */
221
+ async delete(c) {
222
+ c.signal.throwIfAborted();
223
+ await _assertClassBrand(_Indexeddb_brand, this, _getDb).call(this).delete(_classPrivateFieldGet2(_storeName, this), c.key);
224
+ c.signal.throwIfAborted();
225
+ }
226
+ /**
227
+ * オブジェクトストアのすべての値を削除します。
228
+ *
229
+ * @param c 中断を知らせるシグナルを持つコンテキストです。
230
+ *
231
+ * @returns 完了を知らせるプロミスです。
232
+ */
233
+ async clear(c) {
234
+ c.signal.throwIfAborted();
235
+ await _assertClassBrand(_Indexeddb_brand, this, _getDb).call(this).clear(_classPrivateFieldGet2(_storeName, this));
236
+ c.signal.throwIfAborted();
237
+ }
238
+ /**
239
+ * Uint8Array を書き込むためのストリームを作成します。close 時にまとめて保存します。
240
+ *
241
+ * @param c 書き込み対象のキーを持つコンテキストです。
242
+ *
243
+ * @returns Uint8Array を受け付ける書き込みストリームです。
244
+ */
245
+ getWritable(c) {
246
+ c.signal.throwIfAborted();
247
+ const chunks = [];
248
+ return new WritableStream({
249
+ abort: () => {
250
+ chunks.length = 0;
251
+ },
252
+ close: async () => {
253
+ c.signal.throwIfAborted();
254
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
255
+ const merged = new Uint8Array(totalLength);
256
+ let offset = 0;
257
+ for (const chunk of chunks) {
258
+ merged.set(chunk, offset);
259
+ offset += chunk.length;
260
+ }
261
+ await _assertClassBrand(_Indexeddb_brand, this, _getDb).call(this).put(_classPrivateFieldGet2(_storeName, this), merged, c.key);
262
+ c.signal.throwIfAborted();
263
+ },
264
+ write: (chunk) => {
265
+ c.signal.throwIfAborted();
266
+ chunks.push(chunk);
267
+ }
268
+ });
269
+ }
270
+ /**
271
+ * 保存済みの値を読み出すストリームを作成します。
272
+ *
273
+ * @param c 読み出すキーを持つコンテキストです。
274
+ *
275
+ * @returns Uint8Array を送出する読み取りストリームです。
276
+ */
277
+ getReadable(c) {
278
+ c.signal.throwIfAborted();
279
+ return new ReadableStream({ pull: async (controller) => {
280
+ try {
281
+ const data = await this.read(c);
282
+ c.signal.throwIfAborted();
283
+ controller.enqueue(data);
284
+ controller.close();
285
+ } catch (ex) {
286
+ controller.error(ex);
287
+ }
288
+ } });
289
+ }
290
+ };
291
+ /**
292
+ * 開いているデータベースを返します。未オープン時はエラーを投げます。
293
+ *
294
+ * @returns 開いているデータベースです。
295
+ */
296
+ function _getDb() {
297
+ const db = _classPrivateFieldGet2(_db, this);
298
+ if (db === null) throw new TypeError("Indexeddb is not open. Call open() first.");
299
+ return db;
300
+ }
301
+ //#endregion
302
+ export { Indexeddb };
303
+
304
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/indexeddb.ts"],"sourcesContent":["import type { IStorage } from \"@unikvs/core\";\nimport { type IDBPDatabase, openDB } from \"idb\";\n\n/**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)\n */\nexport type IndexeddbOptions = {};\n\n/**\n * WebKit で Blob を直接保存できない場合があるため、中間形式で保持します。\n */\ntype StoredBlob = {\n /**\n * File のときに保持するファイル名です。\n */\n readonly name: string;\n\n /**\n * 元の Blob の MIME 種別です。\n */\n readonly type: string;\n\n /**\n * Blob のバイト列です。\n */\n readonly buffer: ArrayBuffer;\n\n /**\n * 中間形式であることを示す目印です。\n */\n readonly unikvsBlob: true;\n\n /**\n * File 由来かどうかを示す目印です。\n */\n readonly unikvsFile: boolean;\n\n /**\n * File のときに保持する更新時刻です。\n */\n readonly lastModified: number;\n};\n\n/**\n * 保存した中間形式かどうかを判定します。\n *\n * @param value 判定する値です。\n *\n * @returns 中間形式であれば true を返します。\n */\nfunction isStoredBlob(value: unknown): value is StoredBlob {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n\n return (value as Record<string, unknown>)[\"unikvsBlob\"] === true;\n}\n\n/**\n * Blob を IndexedDB に保存できる中間形式に変換します。File の名前と更新時刻も保持します。\n *\n * @param signal 中断を知らせるシグナルです。\n * @param data 変換する Blob です。\n *\n * @returns 中間形式に変換した値です。\n */\nasync function toStoredBlob(signal: AbortSignal, data: Blob): Promise<StoredBlob> {\n const buffer = await data.arrayBuffer();\n\n signal.throwIfAborted();\n\n const isFile = typeof File !== \"undefined\" && data instanceof File;\n\n return {\n name: isFile ? data.name : \"\",\n type: data.type,\n buffer,\n unikvsBlob: true,\n unikvsFile: isFile,\n lastModified: isFile ? data.lastModified : 0,\n };\n}\n\n/**\n * 中間形式を Blob または File に戻します。\n *\n * @param stored 中間形式の値です。\n *\n * @returns 復元した Blob または File です。\n */\nfunction fromStoredBlob(stored: StoredBlob): Blob {\n if (stored.unikvsFile) {\n return new File([stored.buffer], stored.name, {\n type: stored.type,\n lastModified: stored.lastModified,\n });\n }\n\n return new Blob([stored.buffer], { type: stored.type });\n}\n\n/**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)\n */\nexport default class Indexeddb implements IStorage {\n /**\n * ストレージ名です。常に `Indexeddb` を返します。\n */\n public readonly name: string;\n\n /**\n * IndexedDB のデータベースインスタンスを保持します。\n *\n * ストレージがオープンされるまで null です。\n */\n #db: IDBPDatabase | null;\n\n /**\n * 並行 open を束ねるための進行中 Promise です。\n */\n #opening: Promise<IDBPDatabase> | null;\n\n /**\n * 使用するデータベース名です。\n */\n readonly #dbName: string;\n\n /**\n * 使用するオブジェクトストア名です。\n */\n readonly #storeName: string;\n\n /**\n * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)\n */\n public constructor(\n dbName = \"unikvs_db\",\n storeName = \"kvs_store\",\n _options: IndexeddbOptions = {},\n ) {\n this.name = \"Indexeddb\";\n this.#db = null;\n this.#opening = null;\n this.#dbName = dbName;\n this.#storeName = storeName;\n }\n\n /**\n * ストレージが開いているかどうかを返します。\n *\n * @returns データベース接続があれば true を返します。\n */\n public get isOpen(): boolean {\n return this.#db !== null;\n }\n\n /**\n * データベースを開きます。並行した呼び出しは一つの接続処理に束ねます。\n *\n * @param c 中断を知らせるシグナルを持つコンテキストです。\n *\n * @returns 完了を知らせるプロミスです。\n */\n public async open(c: IStorage.OpenContext): Promise<void> {\n c.signal.throwIfAborted();\n\n if (this.#db !== null) {\n return;\n }\n\n if (this.#opening !== null) {\n await this.#opening;\n c.signal.throwIfAborted();\n\n return;\n }\n\n const opening = openDB(this.#dbName, 1, {\n upgrade: (upgraded) => {\n // オブジェクトストアが存在しない場合は作成します。\n if (!upgraded.objectStoreNames.contains(this.#storeName)) {\n upgraded.createObjectStore(this.#storeName);\n }\n },\n });\n this.#opening = opening;\n\n let opened: IDBPDatabase | null = null;\n\n try {\n opened = await opening;\n c.signal.throwIfAborted();\n this.#db = opened;\n opened = null;\n } catch (ex) {\n if (opened !== null) {\n try {\n opened.close();\n } catch {\n // 閉鎖時の失敗は無視します。\n }\n }\n\n throw ex;\n } finally {\n this.#opening = null;\n }\n }\n\n /**\n * データベース接続を閉じます。\n *\n * @param c 中断を知らせるシグナルを持つコンテキストです。\n *\n * @returns 完了を知らせるプロミスです。\n */\n public async close(c: IStorage.CloseContext): Promise<void> {\n c.signal.throwIfAborted();\n\n const db = this.#db;\n\n if (db === null) {\n throw new TypeError(\"Indexeddb is not open. Call open() first.\");\n }\n\n await Promise.resolve();\n db.close();\n this.#db = null;\n }\n\n /**\n * 指定したキーに値を保存します。Blob は中間形式に変換して保存します。\n *\n * @param c 書き込み対象のキーを持つコンテキストです。\n * @param data 保存する値です。\n *\n * @returns 完了を知らせるプロミスです。\n */\n public async write(c: IStorage.WriteContext, data: any): Promise<void> {\n c.signal.throwIfAborted();\n\n const db = this.#getDb();\n\n if (typeof Blob !== \"undefined\" && data instanceof Blob) {\n const stored = await toStoredBlob(c.signal, data);\n await db.put(this.#storeName, stored, c.key);\n\n c.signal.throwIfAborted();\n\n return;\n }\n\n await db.put(this.#storeName, data, c.key);\n\n c.signal.throwIfAborted();\n }\n\n /**\n * 指定したキーの値を読み出します。中間形式は Blob または File に戻します。\n *\n * @param c 読み出すキーを持つコンテキストです。\n *\n * @returns 保存されている値です。\n */\n public async read(c: IStorage.ReadContext): Promise<any> {\n c.signal.throwIfAborted();\n\n const db = this.#getDb();\n const value = await db.get(this.#storeName, c.key);\n\n c.signal.throwIfAborted();\n\n if (isStoredBlob(value)) {\n return fromStoredBlob(value);\n }\n\n if (value === undefined) {\n const count = await db.count(this.#storeName, c.key);\n\n c.signal.throwIfAborted();\n\n // 存在しない場合は DOMException (NotFoundError) を投げます。\n if (count === 0) {\n throw new DOMException(\n \"A requested file or directory could not be found at the time an operation was processed.\",\n \"NotFoundError\",\n );\n }\n }\n\n return value;\n }\n\n /**\n * 指定したキーが存在するかどうかを返します。\n *\n * @param c 確認するキーを持つコンテキストです。\n *\n * @returns キーが存在すれば true を返します。\n */\n public async exists(c: IStorage.ExistsContext): Promise<boolean> {\n c.signal.throwIfAborted();\n\n const db = this.#getDb();\n\n // 件数が 1 であれば存在し、0 であれば存在しません。\n const count = await db.count(this.#storeName, c.key);\n\n c.signal.throwIfAborted();\n\n return count > 0;\n }\n\n /**\n * 指定したキーを削除します。\n *\n * @param c 削除するキーを持つコンテキストです。\n *\n * @returns 完了を知らせるプロミスです。\n */\n public async delete(c: IStorage.DeleteContext): Promise<void> {\n c.signal.throwIfAborted();\n\n const db = this.#getDb();\n await db.delete(this.#storeName, c.key);\n\n c.signal.throwIfAborted();\n }\n\n /**\n * オブジェクトストアのすべての値を削除します。\n *\n * @param c 中断を知らせるシグナルを持つコンテキストです。\n *\n * @returns 完了を知らせるプロミスです。\n */\n public async clear(c: IStorage.ClearContext): Promise<void> {\n c.signal.throwIfAborted();\n\n const db = this.#getDb();\n await db.clear(this.#storeName);\n\n c.signal.throwIfAborted();\n }\n\n /**\n * Uint8Array を書き込むためのストリームを作成します。close 時にまとめて保存します。\n *\n * @param c 書き込み対象のキーを持つコンテキストです。\n *\n * @returns Uint8Array を受け付ける書き込みストリームです。\n */\n public getWritable(c: IStorage.GetWritableContext): WritableStream<Uint8Array> {\n c.signal.throwIfAborted();\n\n const chunks: Uint8Array[] = [];\n\n return new WritableStream<Uint8Array>({\n abort: (): void => {\n chunks.length = 0;\n },\n close: async (): Promise<void> => {\n // 保存は close のときだけ行うため、中断時はここまでのチャンクを破棄できます。\n c.signal.throwIfAborted();\n\n const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);\n const merged = new Uint8Array(totalLength);\n let offset = 0;\n\n for (const chunk of chunks) {\n merged.set(chunk, offset);\n offset += chunk.length;\n }\n\n const db = this.#getDb();\n await db.put(this.#storeName, merged, c.key);\n\n c.signal.throwIfAborted();\n },\n write: (chunk): void => {\n c.signal.throwIfAborted();\n chunks.push(chunk);\n },\n });\n }\n\n /**\n * 保存済みの値を読み出すストリームを作成します。\n *\n * @param c 読み出すキーを持つコンテキストです。\n *\n * @returns Uint8Array を送出する読み取りストリームです。\n */\n public getReadable(c: IStorage.GetReadableContext): ReadableStream<Uint8Array> {\n c.signal.throwIfAborted();\n\n return new ReadableStream<Uint8Array>({\n pull: async (controller: ReadableStreamDefaultController<Uint8Array>): Promise<void> => {\n try {\n const data = await this.read(c as unknown as IStorage.ReadContext);\n\n c.signal.throwIfAborted();\n controller.enqueue(data as Uint8Array);\n controller.close();\n } catch (ex) {\n controller.error(ex);\n }\n },\n });\n }\n\n /**\n * 開いているデータベースを返します。未オープン時はエラーを投げます。\n *\n * @returns 開いているデータベースです。\n */\n #getDb(): IDBPDatabase {\n const db = this.#db;\n\n if (db === null) {\n throw new TypeError(\"Indexeddb is not open. Call open() first.\");\n }\n\n return db;\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,49 +1,44 @@
1
1
  {
2
2
  "name": "@unikvs/indexeddb",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "Storage plugin that stores arbitrary data in the browser IndexedDB.",
5
- "homepage": "https://github.com/tai-kun/unikvs",
5
+ "homepage": "https://tai-kun.github.io/unikvs/",
6
6
  "license": "MIT",
7
- "author": "tai-kun",
8
7
  "repository": {
9
- "url": "https://github.com/tai-kun/unikvs"
8
+ "url": "https://github.com/tai-kun/unikvs",
9
+ "directory": "packages/indexeddb"
10
10
  },
11
11
  "files": [
12
12
  "dist",
13
13
  "src",
14
14
  "package.json",
15
- "README.md",
16
- "LICENSE"
15
+ "LICENSE",
16
+ "README.md"
17
17
  ],
18
18
  "type": "module",
19
19
  "sideEffects": false,
20
- "main": "./dist/src/index.js",
21
- "types": "./dist/src/index.d.ts",
22
20
  "exports": {
23
21
  ".": {
24
- "types": "./dist/src/index.d.ts",
25
- "default": "./dist/src/index.js"
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./dist/index.js"
26
24
  }
27
25
  },
28
26
  "dependencies": {
29
- "@unikvs/core": "0.0.12",
30
- "idb": "8.0.3"
27
+ "@unikvs/core": "0.0.14",
28
+ "idb": "^8.0.4"
31
29
  },
32
30
  "devDependencies": {
33
- "@tsconfig/node24": "24.0.5",
34
- "@tsconfig/strictest": "2.0.8",
35
- "@types/node": "24.13.6",
36
- "@vitest/browser": "4.1.11",
37
- "@vitest/browser-playwright": "4.1.11",
38
- "npm-check-updates": "20.0.2",
39
- "playwright": "1.63.0",
40
- "vite": "8.3.0",
41
- "vitest": "4.1.11"
31
+ "@unikvs/testing": "0.0.0",
32
+ "@vitest/browser-playwright": "5.0.5",
33
+ "@vitest/coverage-istanbul": "5.0.5",
34
+ "playwright": "1.64.1",
35
+ "vite-plus": "1.3.0"
42
36
  },
43
37
  "peerDependencies": {
44
- "@logtape/logtape": "2.0.0"
38
+ "@logtape/logtape": "^2.0.0"
45
39
  },
40
+ "readme": "README.md",
46
41
  "scripts": {
47
- "build": "tsc --project ./.config/tsconfig.build.json"
42
+ "build": "vp -C .config pack"
48
43
  }
49
44
  }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export type * from "./indexeddb.js";
2
+ export { default as Indexeddb } from "./indexeddb.js";
package/src/indexeddb.ts CHANGED
@@ -1,164 +1,426 @@
1
1
  import type { IStorage } from "@unikvs/core";
2
- import { openDB, type IDBPDatabase } from "idb";
2
+ import { type IDBPDatabase, openDB } from "idb";
3
+
4
+ /**
5
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
6
+ */
7
+ export type IndexeddbOptions = {};
8
+
9
+ /**
10
+ * WebKit で Blob を直接保存できない場合があるため、中間形式で保持します。
11
+ */
12
+ type StoredBlob = {
13
+ /**
14
+ * File のときに保持するファイル名です。
15
+ */
16
+ readonly name: string;
17
+
18
+ /**
19
+ * 元の Blob の MIME 種別です。
20
+ */
21
+ readonly type: string;
22
+
23
+ /**
24
+ * Blob のバイト列です。
25
+ */
26
+ readonly buffer: ArrayBuffer;
27
+
28
+ /**
29
+ * 中間形式であることを示す目印です。
30
+ */
31
+ readonly unikvsBlob: true;
32
+
33
+ /**
34
+ * File 由来かどうかを示す目印です。
35
+ */
36
+ readonly unikvsFile: boolean;
37
+
38
+ /**
39
+ * File のときに保持する更新時刻です。
40
+ */
41
+ readonly lastModified: number;
42
+ };
43
+
44
+ /**
45
+ * 保存した中間形式かどうかを判定します。
46
+ *
47
+ * @param value 判定する値です。
48
+ *
49
+ * @returns 中間形式であれば true を返します。
50
+ */
51
+ function isStoredBlob(value: unknown): value is StoredBlob {
52
+ if (typeof value !== "object" || value === null) {
53
+ return false;
54
+ }
55
+
56
+ return (value as Record<string, unknown>)["unikvsBlob"] === true;
57
+ }
58
+
59
+ /**
60
+ * Blob を IndexedDB に保存できる中間形式に変換します。File の名前と更新時刻も保持します。
61
+ *
62
+ * @param signal 中断を知らせるシグナルです。
63
+ * @param data 変換する Blob です。
64
+ *
65
+ * @returns 中間形式に変換した値です。
66
+ */
67
+ async function toStoredBlob(signal: AbortSignal, data: Blob): Promise<StoredBlob> {
68
+ const buffer = await data.arrayBuffer();
69
+
70
+ signal.throwIfAborted();
71
+
72
+ const isFile = typeof File !== "undefined" && data instanceof File;
73
+
74
+ return {
75
+ name: isFile ? data.name : "",
76
+ type: data.type,
77
+ buffer,
78
+ unikvsBlob: true,
79
+ unikvsFile: isFile,
80
+ lastModified: isFile ? data.lastModified : 0,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * 中間形式を Blob または File に戻します。
86
+ *
87
+ * @param stored 中間形式の値です。
88
+ *
89
+ * @returns 復元した Blob または File です。
90
+ */
91
+ function fromStoredBlob(stored: StoredBlob): Blob {
92
+ if (stored.unikvsFile) {
93
+ return new File([stored.buffer], stored.name, {
94
+ type: stored.type,
95
+ lastModified: stored.lastModified,
96
+ });
97
+ }
98
+
99
+ return new Blob([stored.buffer], { type: stored.type });
100
+ }
3
101
 
4
102
  /**
5
103
  * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
6
104
  */
7
105
  export default class Indexeddb implements IStorage {
106
+ /**
107
+ * ストレージ名です。常に `Indexeddb` を返します。
108
+ */
109
+ public readonly name: string;
110
+
8
111
  /**
9
112
  * IndexedDB のデータベースインスタンスを保持します。
10
113
  *
11
114
  * ストレージがオープンされるまで null です。
12
115
  */
13
- private db: IDBPDatabase | null;
116
+ #db: IDBPDatabase | null;
14
117
 
15
118
  /**
16
- * 使用するデータベース名です。
119
+ * 並行 open を束ねるための進行中 Promise です。
17
120
  */
18
- private readonly dbName: string;
121
+ #opening: Promise<IDBPDatabase> | null;
19
122
 
20
123
  /**
21
- * 使用するオブジェクトストア名です。
124
+ * 使用するデータベース名です。
22
125
  */
23
- private readonly storeName: string;
126
+ readonly #dbName: string;
24
127
 
25
128
  /**
26
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
129
+ * 使用するオブジェクトストア名です。
27
130
  */
28
- public readonly name: string;
131
+ readonly #storeName: string;
29
132
 
30
133
  /**
31
134
  * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
32
135
  */
33
- public constructor(dbName: string = "unikvs_db", storeName: string = "kvs_store") {
136
+ public constructor(
137
+ dbName = "unikvs_db",
138
+ storeName = "kvs_store",
139
+ _options: IndexeddbOptions = {},
140
+ ) {
34
141
  this.name = "Indexeddb";
35
- this.db = null;
36
- this.dbName = dbName;
37
- this.storeName = storeName;
142
+ this.#db = null;
143
+ this.#opening = null;
144
+ this.#dbName = dbName;
145
+ this.#storeName = storeName;
38
146
  }
39
147
 
40
148
  /**
41
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
149
+ * ストレージが開いているかどうかを返します。
150
+ *
151
+ * @returns データベース接続があれば true を返します。
42
152
  */
43
153
  public get isOpen(): boolean {
44
- return this.db !== null;
154
+ return this.#db !== null;
45
155
  }
46
156
 
47
157
  /**
48
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
158
+ * データベースを開きます。並行した呼び出しは一つの接続処理に束ねます。
159
+ *
160
+ * @param c 中断を知らせるシグナルを持つコンテキストです。
161
+ *
162
+ * @returns 完了を知らせるプロミスです。
49
163
  */
50
- public async open(): Promise<void> {
51
- if (this.db) {
164
+ public async open(c: IStorage.OpenContext): Promise<void> {
165
+ c.signal.throwIfAborted();
166
+
167
+ if (this.#db !== null) {
168
+ return;
169
+ }
170
+
171
+ if (this.#opening !== null) {
172
+ await this.#opening;
173
+ c.signal.throwIfAborted();
174
+
52
175
  return;
53
176
  }
54
177
 
55
- this.db = await openDB(this.dbName, 1, {
56
- upgrade: (db) => {
57
- // オブジェクトストアが存在しない場合は作成します
58
- if (!db.objectStoreNames.contains(this.storeName)) {
59
- db.createObjectStore(this.storeName);
178
+ const opening = openDB(this.#dbName, 1, {
179
+ upgrade: (upgraded) => {
180
+ // オブジェクトストアが存在しない場合は作成します。
181
+ if (!upgraded.objectStoreNames.contains(this.#storeName)) {
182
+ upgraded.createObjectStore(this.#storeName);
60
183
  }
61
184
  },
62
185
  });
186
+ this.#opening = opening;
187
+
188
+ let opened: IDBPDatabase | null = null;
189
+
190
+ try {
191
+ opened = await opening;
192
+ c.signal.throwIfAborted();
193
+ this.#db = opened;
194
+ opened = null;
195
+ } catch (ex) {
196
+ if (opened !== null) {
197
+ try {
198
+ opened.close();
199
+ } catch {
200
+ // 閉鎖時の失敗は無視します。
201
+ }
202
+ }
203
+
204
+ throw ex;
205
+ } finally {
206
+ this.#opening = null;
207
+ }
63
208
  }
64
209
 
65
210
  /**
66
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
211
+ * データベース接続を閉じます。
212
+ *
213
+ * @param c 中断を知らせるシグナルを持つコンテキストです。
214
+ *
215
+ * @returns 完了を知らせるプロミスです。
67
216
  */
68
- public async close(): Promise<void> {
69
- this.db!.close();
70
- this.db = null;
217
+ public async close(c: IStorage.CloseContext): Promise<void> {
218
+ c.signal.throwIfAborted();
219
+
220
+ const db = this.#db;
221
+
222
+ if (db === null) {
223
+ throw new TypeError("Indexeddb is not open. Call open() first.");
224
+ }
225
+
226
+ await Promise.resolve();
227
+ db.close();
228
+ this.#db = null;
71
229
  }
72
230
 
73
231
  /**
74
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#data)
232
+ * 指定したキーに値を保存します。Blob は中間形式に変換して保存します。
233
+ *
234
+ * @param c 書き込み対象のキーを持つコンテキストです。
235
+ * @param data 保存する値です。
236
+ *
237
+ * @returns 完了を知らせるプロミスです。
75
238
  */
76
- public async write(args: Pick<IStorage.WriteArgs<any>, "key" | "data">): Promise<void> {
77
- const { key, data } = args;
78
- await this.db!.put(this.storeName, data, key);
239
+ public async write(c: IStorage.WriteContext, data: any): Promise<void> {
240
+ c.signal.throwIfAborted();
241
+
242
+ const db = this.#getDb();
243
+
244
+ if (typeof Blob !== "undefined" && data instanceof Blob) {
245
+ const stored = await toStoredBlob(c.signal, data);
246
+ await db.put(this.#storeName, stored, c.key);
247
+
248
+ c.signal.throwIfAborted();
249
+
250
+ return;
251
+ }
252
+
253
+ await db.put(this.#storeName, data, c.key);
254
+
255
+ c.signal.throwIfAborted();
79
256
  }
80
257
 
81
258
  /**
82
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#data)
259
+ * 指定したキーの値を読み出します。中間形式は Blob または File に戻します。
260
+ *
261
+ * @param c 読み出すキーを持つコンテキストです。
262
+ *
263
+ * @returns 保存されている値です。
83
264
  */
84
- public async read(args: Pick<IStorage.ReadArgs, "key">): Promise<any> {
85
- const { key } = args;
86
- // Opfs の挙動に合わせて、存在しない場合は DOMException (NotFoundError) を投げます
87
- if (!(await this.exists({ key }))) {
88
- throw new DOMException(
89
- `A requested file or directory could not be found at the time an operation was processed.`,
90
- "NotFoundError",
91
- );
265
+ public async read(c: IStorage.ReadContext): Promise<any> {
266
+ c.signal.throwIfAborted();
267
+
268
+ const db = this.#getDb();
269
+ const value = await db.get(this.#storeName, c.key);
270
+
271
+ c.signal.throwIfAborted();
272
+
273
+ if (isStoredBlob(value)) {
274
+ return fromStoredBlob(value);
275
+ }
276
+
277
+ if (value === undefined) {
278
+ const count = await db.count(this.#storeName, c.key);
279
+
280
+ c.signal.throwIfAborted();
281
+
282
+ // 存在しない場合は DOMException (NotFoundError) を投げます。
283
+ if (count === 0) {
284
+ throw new DOMException(
285
+ "A requested file or directory could not be found at the time an operation was processed.",
286
+ "NotFoundError",
287
+ );
288
+ }
92
289
  }
93
290
 
94
- return await this.db!.get(this.storeName, key);
291
+ return value;
95
292
  }
96
293
 
97
294
  /**
98
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
295
+ * 指定したキーが存在するかどうかを返します。
296
+ *
297
+ * @param c 確認するキーを持つコンテキストです。
298
+ *
299
+ * @returns キーが存在すれば true を返します。
99
300
  */
100
- public async exists(args: Pick<IStorage.ExistsArgs, "key">): Promise<boolean> {
101
- const { key } = args;
301
+ public async exists(c: IStorage.ExistsContext): Promise<boolean> {
302
+ c.signal.throwIfAborted();
303
+
304
+ const db = this.#getDb();
305
+
306
+ // 件数が 1 であれば存在し、0 であれば存在しません。
307
+ const count = await db.count(this.#storeName, c.key);
308
+
309
+ c.signal.throwIfAborted();
102
310
 
103
- // count() はキーが存在すれば 1 を、存在しなければ 0 を返します
104
- const count = await this.db!.count(this.storeName, key);
105
311
  return count > 0;
106
312
  }
107
313
 
108
314
  /**
109
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
315
+ * 指定したキーを削除します。
316
+ *
317
+ * @param c 削除するキーを持つコンテキストです。
318
+ *
319
+ * @returns 完了を知らせるプロミスです。
110
320
  */
111
- public async delete(args: Pick<IStorage.DeleteArgs, "key">): Promise<void> {
112
- const { key } = args;
321
+ public async delete(c: IStorage.DeleteContext): Promise<void> {
322
+ c.signal.throwIfAborted();
113
323
 
114
- await this.db!.delete(this.storeName, key);
324
+ const db = this.#getDb();
325
+ await db.delete(this.#storeName, c.key);
326
+
327
+ c.signal.throwIfAborted();
115
328
  }
116
329
 
117
330
  /**
118
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
331
+ * オブジェクトストアのすべての値を削除します。
332
+ *
333
+ * @param c 中断を知らせるシグナルを持つコンテキストです。
334
+ *
335
+ * @returns 完了を知らせるプロミスです。
119
336
  */
120
- public async clear(): Promise<void> {
121
- await this.db!.clear(this.storeName);
337
+ public async clear(c: IStorage.ClearContext): Promise<void> {
338
+ c.signal.throwIfAborted();
339
+
340
+ const db = this.#getDb();
341
+ await db.clear(this.#storeName);
342
+
343
+ c.signal.throwIfAborted();
122
344
  }
123
345
 
124
346
  /**
125
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#data)
347
+ * Uint8Array を書き込むためのストリームを作成します。close 時にまとめて保存します。
348
+ *
349
+ * @param c 書き込み対象のキーを持つコンテキストです。
350
+ *
351
+ * @returns Uint8Array を受け付ける書き込みストリームです。
126
352
  */
127
- public getWritable(args: Pick<IStorage.GetWritableArgs, "key">): WritableStream<Uint8Array> {
128
- const { key } = args;
353
+ public getWritable(c: IStorage.GetWritableContext): WritableStream<Uint8Array> {
354
+ c.signal.throwIfAborted();
355
+
129
356
  const chunks: Uint8Array[] = [];
130
357
 
131
- return new WritableStream({
132
- write: (chunk) => {
133
- chunks.push(chunk);
358
+ return new WritableStream<Uint8Array>({
359
+ abort: (): void => {
360
+ chunks.length = 0;
134
361
  },
135
- close: async () => {
136
- const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
137
- const result = new Uint8Array(totalLength);
362
+ close: async (): Promise<void> => {
363
+ // 保存は close のときだけ行うため、中断時はここまでのチャンクを破棄できます。
364
+ c.signal.throwIfAborted();
365
+
366
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
367
+ const merged = new Uint8Array(totalLength);
138
368
  let offset = 0;
139
369
 
140
370
  for (const chunk of chunks) {
141
- result.set(chunk, offset);
371
+ merged.set(chunk, offset);
142
372
  offset += chunk.length;
143
373
  }
144
374
 
145
- await this.db!.put(this.storeName, result, key);
375
+ const db = this.#getDb();
376
+ await db.put(this.#storeName, merged, c.key);
377
+
378
+ c.signal.throwIfAborted();
379
+ },
380
+ write: (chunk): void => {
381
+ c.signal.throwIfAborted();
382
+ chunks.push(chunk);
146
383
  },
147
384
  });
148
385
  }
149
386
 
150
387
  /**
151
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#data)
388
+ * 保存済みの値を読み出すストリームを作成します。
389
+ *
390
+ * @param c 読み出すキーを持つコンテキストです。
391
+ *
392
+ * @returns Uint8Array を送出する読み取りストリームです。
152
393
  */
153
- public getReadable(args: Pick<IStorage.GetReadableArgs, "key">): ReadableStream<Uint8Array> {
154
- const { key } = args;
394
+ public getReadable(c: IStorage.GetReadableContext): ReadableStream<Uint8Array> {
395
+ c.signal.throwIfAborted();
155
396
 
156
- return new ReadableStream({
157
- pull: async (controller) => {
158
- const data = await this.read({ key });
159
- controller.enqueue(data);
160
- controller.close();
397
+ return new ReadableStream<Uint8Array>({
398
+ pull: async (controller: ReadableStreamDefaultController<Uint8Array>): Promise<void> => {
399
+ try {
400
+ const data = await this.read(c as unknown as IStorage.ReadContext);
401
+
402
+ c.signal.throwIfAborted();
403
+ controller.enqueue(data as Uint8Array);
404
+ controller.close();
405
+ } catch (ex) {
406
+ controller.error(ex);
407
+ }
161
408
  },
162
409
  });
163
410
  }
411
+
412
+ /**
413
+ * 開いているデータベースを返します。未オープン時はエラーを投げます。
414
+ *
415
+ * @returns 開いているデータベースです。
416
+ */
417
+ #getDb(): IDBPDatabase {
418
+ const db = this.#db;
419
+
420
+ if (db === null) {
421
+ throw new TypeError("Indexeddb is not open. Call open() first.");
422
+ }
423
+
424
+ return db;
425
+ }
164
426
  }
@@ -1,69 +0,0 @@
1
- import type { IStorage } from "@unikvs/core";
2
- /**
3
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
4
- */
5
- export default class Indexeddb implements IStorage {
6
- /**
7
- * IndexedDB のデータベースインスタンスを保持します。
8
- *
9
- * ストレージがオープンされるまで null です。
10
- */
11
- private db;
12
- /**
13
- * 使用するデータベース名です。
14
- */
15
- private readonly dbName;
16
- /**
17
- * 使用するオブジェクトストア名です。
18
- */
19
- private readonly storeName;
20
- /**
21
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
22
- */
23
- readonly name: string;
24
- /**
25
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
26
- */
27
- constructor(dbName?: string, storeName?: string);
28
- /**
29
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
30
- */
31
- get isOpen(): boolean;
32
- /**
33
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
34
- */
35
- open(): Promise<void>;
36
- /**
37
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
38
- */
39
- close(): Promise<void>;
40
- /**
41
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#data)
42
- */
43
- write(args: Pick<IStorage.WriteArgs<any>, "key" | "data">): Promise<void>;
44
- /**
45
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#data)
46
- */
47
- read(args: Pick<IStorage.ReadArgs, "key">): Promise<any>;
48
- /**
49
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
50
- */
51
- exists(args: Pick<IStorage.ExistsArgs, "key">): Promise<boolean>;
52
- /**
53
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
54
- */
55
- delete(args: Pick<IStorage.DeleteArgs, "key">): Promise<void>;
56
- /**
57
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
58
- */
59
- clear(): Promise<void>;
60
- /**
61
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#data)
62
- */
63
- getWritable(args: Pick<IStorage.GetWritableArgs, "key">): WritableStream<Uint8Array>;
64
- /**
65
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#data)
66
- */
67
- getReadable(args: Pick<IStorage.GetReadableArgs, "key">): ReadableStream<Uint8Array>;
68
- }
69
- //# sourceMappingURL=indexeddb.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"indexeddb.d.ts","sourceRoot":"","sources":["../../src/indexeddb.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAG7C;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,SAAU,YAAW,QAAQ;IAChD;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAsB;IAEhC;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IAEnC;;OAEG;IACH,SAAgB,IAAI,EAAE,MAAM,CAAC;IAE7B;;OAEG;IACH,YAAmB,MAAM,GAAE,MAAoB,EAAE,SAAS,GAAE,MAAoB,EAK/E;IAED;;OAEG;IACH,IAAW,MAAM,IAAI,OAAO,CAE3B;IAED;;OAEG;IACU,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAajC;IAED;;OAEG;IACU,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAGlC;IAED;;OAEG;IACU,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAGrF;IAED;;OAEG;IACU,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAWpE;IAED;;OAEG;IACU,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAM5E;IAED;;OAEG;IACU,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAIzE;IAED;;OAEG;IACU,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAElC;IAED;;OAEG;IACI,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,eAAe,EAAE,KAAK,CAAC,GAAG,cAAc,CAAC,UAAU,CAAC,CAqB1F;IAED;;OAEG;IACI,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,eAAe,EAAE,KAAK,CAAC,GAAG,cAAc,CAAC,UAAU,CAAC,CAU1F;CACF"}
@@ -1,119 +0,0 @@
1
- import { openDB } from "idb";
2
- /**
3
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
4
- */
5
- export default class Indexeddb {
6
- /**
7
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
8
- */
9
- constructor(dbName = "unikvs_db", storeName = "kvs_store") {
10
- this.name = "Indexeddb";
11
- this.db = null;
12
- this.dbName = dbName;
13
- this.storeName = storeName;
14
- }
15
- /**
16
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
17
- */
18
- get isOpen() {
19
- return this.db !== null;
20
- }
21
- /**
22
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
23
- */
24
- async open() {
25
- if (this.db) {
26
- return;
27
- }
28
- this.db = await openDB(this.dbName, 1, {
29
- upgrade: (db) => {
30
- // オブジェクトストアが存在しない場合は作成します
31
- if (!db.objectStoreNames.contains(this.storeName)) {
32
- db.createObjectStore(this.storeName);
33
- }
34
- },
35
- });
36
- }
37
- /**
38
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
39
- */
40
- async close() {
41
- this.db.close();
42
- this.db = null;
43
- }
44
- /**
45
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#data)
46
- */
47
- async write(args) {
48
- const { key, data } = args;
49
- await this.db.put(this.storeName, data, key);
50
- }
51
- /**
52
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#data)
53
- */
54
- async read(args) {
55
- const { key } = args;
56
- // Opfs の挙動に合わせて、存在しない場合は DOMException (NotFoundError) を投げます
57
- if (!(await this.exists({ key }))) {
58
- throw new DOMException(`A requested file or directory could not be found at the time an operation was processed.`, "NotFoundError");
59
- }
60
- return await this.db.get(this.storeName, key);
61
- }
62
- /**
63
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
64
- */
65
- async exists(args) {
66
- const { key } = args;
67
- // count() はキーが存在すれば 1 を、存在しなければ 0 を返します
68
- const count = await this.db.count(this.storeName, key);
69
- return count > 0;
70
- }
71
- /**
72
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
73
- */
74
- async delete(args) {
75
- const { key } = args;
76
- await this.db.delete(this.storeName, key);
77
- }
78
- /**
79
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
80
- */
81
- async clear() {
82
- await this.db.clear(this.storeName);
83
- }
84
- /**
85
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#data)
86
- */
87
- getWritable(args) {
88
- const { key } = args;
89
- const chunks = [];
90
- return new WritableStream({
91
- write: (chunk) => {
92
- chunks.push(chunk);
93
- },
94
- close: async () => {
95
- const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
96
- const result = new Uint8Array(totalLength);
97
- let offset = 0;
98
- for (const chunk of chunks) {
99
- result.set(chunk, offset);
100
- offset += chunk.length;
101
- }
102
- await this.db.put(this.storeName, result, key);
103
- },
104
- });
105
- }
106
- /**
107
- * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#data)
108
- */
109
- getReadable(args) {
110
- const { key } = args;
111
- return new ReadableStream({
112
- pull: async (controller) => {
113
- const data = await this.read({ key });
114
- controller.enqueue(data);
115
- controller.close();
116
- },
117
- });
118
- }
119
- }