@unikvs/indexeddb 0.0.10 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @unikvs/indexeddb
2
+
3
+ [Document](https://tai-kun.github.io/unikvs/)
@@ -0,0 +1,30 @@
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
+ readonly name: string;
13
+ /**
14
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
15
+ */
16
+ constructor(dbName?: string, storeName?: string, _options?: IndexeddbOptions);
17
+ get isOpen(): boolean;
18
+ open(c: IStorage.OpenContext): Promise<void>;
19
+ close(c: IStorage.CloseContext): Promise<void>;
20
+ write(c: IStorage.WriteContext, data: any): Promise<void>;
21
+ read(c: IStorage.ReadContext): Promise<any>;
22
+ exists(c: IStorage.ExistsContext): Promise<boolean>;
23
+ delete(c: IStorage.DeleteContext): Promise<void>;
24
+ clear(c: IStorage.ClearContext): Promise<void>;
25
+ getWritable(c: IStorage.GetWritableContext): WritableStream<Uint8Array>;
26
+ getReadable(c: IStorage.GetReadableContext): ReadableStream<Uint8Array>;
27
+ }
28
+ //#endregion
29
+ export { Indexeddb, type IndexeddbOptions };
30
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/indexeddb.ts"],"mappings":";;;;;KAMY;;;;cAuDS,qBAAqB;;WAuBxB;;;;EAKhB,YACE,iBACA,oBACA,WAAU;MASD;EAgBX,KAAkB,GAAG,SAAS,cAAc;EA8C5C,MAAmB,GAAG,SAAS,eAAe;EAc9C,MAAmB,GAAG,SAAS,cAAc,YAAY;EAmBzD,KAAkB,GAAG,SAAS,cAAc;EA6B5C,OAAoB,GAAG,SAAS,gBAAgB;EAahD,OAAoB,GAAG,SAAS,gBAAgB;EAShD,MAAmB,GAAG,SAAS,eAAe;EAS9C,YAAmB,GAAG,SAAS,qBAAqB,eAAe;EAkCnE,YAAmB,GAAG,SAAS,qBAAqB,eAAe"}
package/dist/index.js ADDED
@@ -0,0 +1,208 @@
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
+ function isStoredBlob(value) {
35
+ if (typeof value !== "object" || value === null) return false;
36
+ return value["unikvsBlob"] === true;
37
+ }
38
+ async function toStoredBlob(signal, data) {
39
+ const buffer = await data.arrayBuffer();
40
+ signal.throwIfAborted();
41
+ const isFile = typeof File !== "undefined" && data instanceof File;
42
+ return {
43
+ name: isFile ? data.name : "",
44
+ type: data.type,
45
+ buffer,
46
+ unikvsBlob: true,
47
+ unikvsFile: isFile,
48
+ lastModified: isFile ? data.lastModified : 0
49
+ };
50
+ }
51
+ function fromStoredBlob(stored) {
52
+ if (stored.unikvsFile) return new File([stored.buffer], stored.name, {
53
+ type: stored.type,
54
+ lastModified: stored.lastModified
55
+ });
56
+ return new Blob([stored.buffer], { type: stored.type });
57
+ }
58
+ var _db = /* @__PURE__ */ new WeakMap();
59
+ var _opening = /* @__PURE__ */ new WeakMap();
60
+ var _dbName = /* @__PURE__ */ new WeakMap();
61
+ var _storeName = /* @__PURE__ */ new WeakMap();
62
+ var _Indexeddb_brand = /* @__PURE__ */ new WeakSet();
63
+ /**
64
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
65
+ */
66
+ var Indexeddb = class {
67
+ /**
68
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
69
+ */
70
+ constructor(dbName = "unikvs_db", storeName = "kvs_store", _options = {}) {
71
+ _classPrivateMethodInitSpec(this, _Indexeddb_brand);
72
+ _classPrivateFieldInitSpec(this, _db, void 0);
73
+ _classPrivateFieldInitSpec(this, _opening, void 0);
74
+ _classPrivateFieldInitSpec(this, _dbName, void 0);
75
+ _classPrivateFieldInitSpec(this, _storeName, void 0);
76
+ this.name = "Indexeddb";
77
+ _classPrivateFieldSet2(_db, this, null);
78
+ _classPrivateFieldSet2(_opening, this, null);
79
+ _classPrivateFieldSet2(_dbName, this, dbName);
80
+ _classPrivateFieldSet2(_storeName, this, storeName);
81
+ }
82
+ get isOpen() {
83
+ return _classPrivateFieldGet2(_db, this) !== null;
84
+ }
85
+ async open(c) {
86
+ c.signal.throwIfAborted();
87
+ if (_classPrivateFieldGet2(_db, this) !== null) return;
88
+ if (_classPrivateFieldGet2(_opening, this) !== null) {
89
+ await _classPrivateFieldGet2(_opening, this);
90
+ c.signal.throwIfAborted();
91
+ return;
92
+ }
93
+ const opening = openDB(_classPrivateFieldGet2(_dbName, this), 1, { upgrade: (upgraded) => {
94
+ if (!upgraded.objectStoreNames.contains(_classPrivateFieldGet2(_storeName, this))) upgraded.createObjectStore(_classPrivateFieldGet2(_storeName, this));
95
+ } });
96
+ _classPrivateFieldSet2(_opening, this, opening);
97
+ let opened = null;
98
+ try {
99
+ opened = await opening;
100
+ c.signal.throwIfAborted();
101
+ _classPrivateFieldSet2(_db, this, opened);
102
+ opened = null;
103
+ } catch (ex) {
104
+ if (opened !== null) try {
105
+ opened.close();
106
+ } catch {}
107
+ throw ex;
108
+ } finally {
109
+ _classPrivateFieldSet2(_opening, this, null);
110
+ }
111
+ }
112
+ async close(c) {
113
+ c.signal.throwIfAborted();
114
+ const db = _classPrivateFieldGet2(_db, this);
115
+ if (db === null) throw new TypeError("Indexeddb is not open. Call open() first.");
116
+ await Promise.resolve();
117
+ db.close();
118
+ _classPrivateFieldSet2(_db, this, null);
119
+ }
120
+ async write(c, data) {
121
+ c.signal.throwIfAborted();
122
+ const db = _assertClassBrand(_Indexeddb_brand, this, _getDb).call(this);
123
+ if (typeof Blob !== "undefined" && data instanceof Blob) {
124
+ const stored = await toStoredBlob(c.signal, data);
125
+ await db.put(_classPrivateFieldGet2(_storeName, this), stored, c.key);
126
+ c.signal.throwIfAborted();
127
+ return;
128
+ }
129
+ await db.put(_classPrivateFieldGet2(_storeName, this), data, c.key);
130
+ c.signal.throwIfAborted();
131
+ }
132
+ async read(c) {
133
+ c.signal.throwIfAborted();
134
+ const db = _assertClassBrand(_Indexeddb_brand, this, _getDb).call(this);
135
+ const value = await db.get(_classPrivateFieldGet2(_storeName, this), c.key);
136
+ c.signal.throwIfAborted();
137
+ if (isStoredBlob(value)) return fromStoredBlob(value);
138
+ if (value === void 0) {
139
+ const count = await db.count(_classPrivateFieldGet2(_storeName, this), c.key);
140
+ c.signal.throwIfAborted();
141
+ if (count === 0) throw new DOMException("A requested file or directory could not be found at the time an operation was processed.", "NotFoundError");
142
+ }
143
+ return value;
144
+ }
145
+ async exists(c) {
146
+ c.signal.throwIfAborted();
147
+ const count = await _assertClassBrand(_Indexeddb_brand, this, _getDb).call(this).count(_classPrivateFieldGet2(_storeName, this), c.key);
148
+ c.signal.throwIfAborted();
149
+ return count > 0;
150
+ }
151
+ async delete(c) {
152
+ c.signal.throwIfAborted();
153
+ await _assertClassBrand(_Indexeddb_brand, this, _getDb).call(this).delete(_classPrivateFieldGet2(_storeName, this), c.key);
154
+ c.signal.throwIfAborted();
155
+ }
156
+ async clear(c) {
157
+ c.signal.throwIfAborted();
158
+ await _assertClassBrand(_Indexeddb_brand, this, _getDb).call(this).clear(_classPrivateFieldGet2(_storeName, this));
159
+ c.signal.throwIfAborted();
160
+ }
161
+ getWritable(c) {
162
+ c.signal.throwIfAborted();
163
+ const chunks = [];
164
+ return new WritableStream({
165
+ abort: () => {
166
+ chunks.length = 0;
167
+ },
168
+ close: async () => {
169
+ c.signal.throwIfAborted();
170
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
171
+ const merged = new Uint8Array(totalLength);
172
+ let offset = 0;
173
+ for (const chunk of chunks) {
174
+ merged.set(chunk, offset);
175
+ offset += chunk.length;
176
+ }
177
+ await _assertClassBrand(_Indexeddb_brand, this, _getDb).call(this).put(_classPrivateFieldGet2(_storeName, this), merged, c.key);
178
+ c.signal.throwIfAborted();
179
+ },
180
+ write: (chunk) => {
181
+ c.signal.throwIfAborted();
182
+ chunks.push(chunk);
183
+ }
184
+ });
185
+ }
186
+ getReadable(c) {
187
+ c.signal.throwIfAborted();
188
+ return new ReadableStream({ pull: async (controller) => {
189
+ try {
190
+ const data = await this.read(c);
191
+ c.signal.throwIfAborted();
192
+ controller.enqueue(data);
193
+ controller.close();
194
+ } catch (ex) {
195
+ controller.error(ex);
196
+ }
197
+ } });
198
+ }
199
+ };
200
+ function _getDb() {
201
+ const db = _classPrivateFieldGet2(_db, this);
202
+ if (db === null) throw new TypeError("Indexeddb is not open. Call open() first.");
203
+ return db;
204
+ }
205
+ //#endregion
206
+ export { Indexeddb };
207
+
208
+ //# 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// WebKit で `Blob` を直接保存できない場合があるため、中間形式で保持します。\ntype StoredBlob = {\n readonly name: string;\n readonly type: string;\n readonly buffer: ArrayBuffer;\n readonly unikvsBlob: true;\n readonly unikvsFile: boolean;\n readonly lastModified: number;\n};\n\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// `Blob` を IndexedDB に保存できる中間形式に変換します。\n// `File` の名前と更新時刻も保持します。\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// 中間形式を `Blob` または `File` に戻します。\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 * ストレージがオープンされるまで 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 public readonly name: 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 public get isOpen(): boolean {\n return this.#db !== null;\n }\n\n // 開いているデータベースを返します。\n // 未オープン時は `TypeError` を投げます。\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 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 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 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 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 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 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 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 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 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"],"mappings":"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"}
package/package.json CHANGED
@@ -1,48 +1,44 @@
1
1
  {
2
2
  "name": "@unikvs/indexeddb",
3
- "version": "0.0.10",
4
- "description": "",
5
- "homepage": "https://github.com/tai-kun/unikvs",
3
+ "version": "0.0.13",
4
+ "description": "Storage plugin that stores arbitrary data in the browser IndexedDB.",
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
- "LICENSE"
15
+ "LICENSE",
16
+ "README.md"
16
17
  ],
17
18
  "type": "module",
18
19
  "sideEffects": false,
19
- "main": "./dist/src/index.js",
20
- "types": "./dist/src/index.d.ts",
21
20
  "exports": {
22
21
  ".": {
23
- "types": "./dist/src/index.d.ts",
24
- "default": "./dist/src/index.js"
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./dist/index.js"
25
24
  }
26
25
  },
27
26
  "dependencies": {
28
- "@unikvs/core": "0.0.10",
29
- "idb": "8.0.3"
27
+ "@unikvs/core": "0.0.13",
28
+ "idb": "^8.0.4"
30
29
  },
31
30
  "devDependencies": {
32
- "@tsconfig/node24": "24.0.5",
33
- "@tsconfig/strictest": "2.0.8",
34
- "@types/node": "24.13.3",
35
- "@vitest/browser": "4.1.10",
36
- "@vitest/browser-playwright": "4.1.10",
37
- "npm-check-updates": "20.0.2",
38
- "playwright": "1.62.1",
39
- "vite": "8.2.1",
40
- "vitest": "4.1.10"
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"
41
36
  },
42
37
  "peerDependencies": {
43
- "@logtape/logtape": "2.0.0"
38
+ "@logtape/logtape": "^2.0.0"
44
39
  },
40
+ "readme": "README.md",
45
41
  "scripts": {
46
- "build": "tsc --project ./.config/tsconfig.build.json"
42
+ "build": "vp -C .config pack"
47
43
  }
48
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,8 +1,63 @@
1
1
  import type { IStorage } from "@unikvs/core";
2
- import { openDB, type IDBPDatabase } from "idb";
2
+ import { type IDBPDatabase, openDB } from "idb";
3
3
 
4
4
  /**
5
- * ブラウザーの IndexedDB を永続化先として使用するストレージクラスです。
5
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
6
+ */
7
+ export type IndexeddbOptions = {};
8
+
9
+ // WebKit で `Blob` を直接保存できない場合があるため、中間形式で保持します。
10
+ type StoredBlob = {
11
+ readonly name: string;
12
+ readonly type: string;
13
+ readonly buffer: ArrayBuffer;
14
+ readonly unikvsBlob: true;
15
+ readonly unikvsFile: boolean;
16
+ readonly lastModified: number;
17
+ };
18
+
19
+ // 保存した中間形式かどうかを判定します。
20
+ function isStoredBlob(value: unknown): value is StoredBlob {
21
+ if (typeof value !== "object" || value === null) {
22
+ return false;
23
+ }
24
+
25
+ return (value as Record<string, unknown>)["unikvsBlob"] === true;
26
+ }
27
+
28
+ // `Blob` を IndexedDB に保存できる中間形式に変換します。
29
+ // `File` の名前と更新時刻も保持します。
30
+ async function toStoredBlob(signal: AbortSignal, data: Blob): Promise<StoredBlob> {
31
+ const buffer = await data.arrayBuffer();
32
+
33
+ signal.throwIfAborted();
34
+
35
+ const isFile = typeof File !== "undefined" && data instanceof File;
36
+
37
+ return {
38
+ name: isFile ? data.name : "",
39
+ type: data.type,
40
+ buffer,
41
+ unikvsBlob: true,
42
+ unikvsFile: isFile,
43
+ lastModified: isFile ? data.lastModified : 0,
44
+ };
45
+ }
46
+
47
+ // 中間形式を `Blob` または `File` に戻します。
48
+ function fromStoredBlob(stored: StoredBlob): Blob {
49
+ if (stored.unikvsFile) {
50
+ return new File([stored.buffer], stored.name, {
51
+ type: stored.type,
52
+ lastModified: stored.lastModified,
53
+ });
54
+ }
55
+
56
+ return new Blob([stored.buffer], { type: stored.type });
57
+ }
58
+
59
+ /**
60
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
6
61
  */
7
62
  export default class Indexeddb implements IStorage {
8
63
  /**
@@ -10,180 +65,243 @@ export default class Indexeddb implements IStorage {
10
65
  *
11
66
  * ストレージがオープンされるまで null です。
12
67
  */
13
- private db: IDBPDatabase | null;
68
+ #db: IDBPDatabase | null;
14
69
 
15
70
  /**
16
- * 使用するデータベース名です。
71
+ * 並行 open を束ねるための進行中 Promise です。
17
72
  */
18
- private readonly dbName: string;
73
+ #opening: Promise<IDBPDatabase> | null;
19
74
 
20
75
  /**
21
- * 使用するオブジェクトストア名です。
76
+ * 使用するデータベース名です。
22
77
  */
23
- private readonly storeName: string;
78
+ readonly #dbName: string;
24
79
 
25
80
  /**
26
- * ストレージの名前です。デバッグメッセージなどに使用されます。
81
+ * 使用するオブジェクトストア名です。
27
82
  */
83
+ readonly #storeName: string;
84
+
28
85
  public readonly name: string;
29
86
 
30
87
  /**
31
- * Indexeddb インスタンスを初期化します。
32
- *
33
- * @param dbName データベース名です。デフォルトは `"unikvs_db"` です。
34
- * @param storeName データを保存するオブジェクトストア名です。デフォルトは `"kvs_store"` です。
88
+ * [API Reference](https://tai-kun.github.io/unikvs/packages/indexeddb#usage)
35
89
  */
36
- public constructor(dbName: string = "unikvs_db", storeName: string = "kvs_store") {
90
+ public constructor(
91
+ dbName = "unikvs_db",
92
+ storeName = "kvs_store",
93
+ _options: IndexeddbOptions = {},
94
+ ) {
37
95
  this.name = "Indexeddb";
38
- this.db = null;
39
- this.dbName = dbName;
40
- this.storeName = storeName;
96
+ this.#db = null;
97
+ this.#opening = null;
98
+ this.#dbName = dbName;
99
+ this.#storeName = storeName;
41
100
  }
42
101
 
43
- /** ストレージが現在利用可能な状態であるかを示します。 */
44
102
  public get isOpen(): boolean {
45
- return this.db !== null;
103
+ return this.#db !== null;
46
104
  }
47
105
 
48
- /**
49
- * ストレージをオープンし、IndexedDB データベースへの接続を確立します。
50
- *
51
- * 既にオープンされている場合は何も行いません。
52
- */
53
- public async open(): Promise<void> {
54
- if (this.db) {
106
+ // 開いているデータベースを返します。
107
+ // 未オープン時は `TypeError` を投げます。
108
+ #getDb(): IDBPDatabase {
109
+ const db = this.#db;
110
+
111
+ if (db === null) {
112
+ throw new TypeError("Indexeddb is not open. Call open() first.");
113
+ }
114
+
115
+ return db;
116
+ }
117
+
118
+ public async open(c: IStorage.OpenContext): Promise<void> {
119
+ c.signal.throwIfAborted();
120
+
121
+ if (this.#db !== null) {
55
122
  return;
56
123
  }
57
124
 
58
- this.db = await openDB(this.dbName, 1, {
59
- upgrade: (db) => {
60
- // オブジェクトストアが存在しない場合は作成します
61
- if (!db.objectStoreNames.contains(this.storeName)) {
62
- db.createObjectStore(this.storeName);
125
+ if (this.#opening !== null) {
126
+ await this.#opening;
127
+ c.signal.throwIfAborted();
128
+
129
+ return;
130
+ }
131
+
132
+ const opening = openDB(this.#dbName, 1, {
133
+ upgrade: (upgraded) => {
134
+ // オブジェクトストアが存在しない場合は作成します。
135
+ if (!upgraded.objectStoreNames.contains(this.#storeName)) {
136
+ upgraded.createObjectStore(this.#storeName);
63
137
  }
64
138
  },
65
139
  });
140
+ this.#opening = opening;
141
+
142
+ let opened: IDBPDatabase | null = null;
143
+
144
+ try {
145
+ opened = await opening;
146
+ c.signal.throwIfAborted();
147
+ this.#db = opened;
148
+ opened = null;
149
+ } catch (ex) {
150
+ if (opened !== null) {
151
+ try {
152
+ opened.close();
153
+ } catch {
154
+ // 閉鎖時の失敗は無視します。
155
+ }
156
+ }
157
+
158
+ throw ex;
159
+ } finally {
160
+ this.#opening = null;
161
+ }
66
162
  }
67
163
 
68
- /**
69
- * ストレージを安全にクローズします。
70
- *
71
- * データベース接続を閉じ、内部状態を初期化します。
72
- */
73
- public async close(): Promise<void> {
74
- this.db!.close();
75
- this.db = null;
164
+ public async close(c: IStorage.CloseContext): Promise<void> {
165
+ c.signal.throwIfAborted();
166
+
167
+ const db = this.#db;
168
+
169
+ if (db === null) {
170
+ throw new TypeError("Indexeddb is not open. Call open() first.");
171
+ }
172
+
173
+ await Promise.resolve();
174
+ db.close();
175
+ this.#db = null;
76
176
  }
77
177
 
78
- /**
79
- * 指定されたデータを、対応するキーでストレージに保存します。
80
- *
81
- * @param args.key 保存先のキーです。
82
- * @param args.data 保存するデータです。
83
- */
84
- public async write(args: Pick<IStorage.WriteArgs<any>, "key" | "data">): Promise<void> {
85
- const { key, data } = args;
86
- await this.db!.put(this.storeName, data, key);
178
+ public async write(c: IStorage.WriteContext, data: any): Promise<void> {
179
+ c.signal.throwIfAborted();
180
+
181
+ const db = this.#getDb();
182
+
183
+ if (typeof Blob !== "undefined" && data instanceof Blob) {
184
+ const stored = await toStoredBlob(c.signal, data);
185
+ await db.put(this.#storeName, stored, c.key);
186
+
187
+ c.signal.throwIfAborted();
188
+
189
+ return;
190
+ }
191
+
192
+ await db.put(this.#storeName, data, c.key);
193
+
194
+ c.signal.throwIfAborted();
87
195
  }
88
196
 
89
- /**
90
- * 指定されたキーに対応するデータをストレージから取得します。
91
- *
92
- * @param args.key 取得元のキーです。
93
- * @returns キーに対応するデータです。存在しない場合は DOMException (NotFoundError) を投げます。
94
- */
95
- public async read(args: Pick<IStorage.ReadArgs, "key">): Promise<any> {
96
- const { key } = args;
97
- // Opfs の挙動に合わせて、存在しない場合は DOMException (NotFoundError) を投げます
98
- if (!(await this.exists({ key }))) {
99
- throw new DOMException(
100
- `A requested file or directory could not be found at the time an operation was processed.`,
101
- "NotFoundError",
102
- );
197
+ public async read(c: IStorage.ReadContext): Promise<any> {
198
+ c.signal.throwIfAborted();
199
+
200
+ const db = this.#getDb();
201
+ const value = await db.get(this.#storeName, c.key);
202
+
203
+ c.signal.throwIfAborted();
204
+
205
+ if (isStoredBlob(value)) {
206
+ return fromStoredBlob(value);
207
+ }
208
+
209
+ if (value === undefined) {
210
+ const count = await db.count(this.#storeName, c.key);
211
+
212
+ c.signal.throwIfAborted();
213
+
214
+ // 存在しない場合は DOMException (NotFoundError) を投げます。
215
+ if (count === 0) {
216
+ throw new DOMException(
217
+ "A requested file or directory could not be found at the time an operation was processed.",
218
+ "NotFoundError",
219
+ );
220
+ }
103
221
  }
104
222
 
105
- return await this.db!.get(this.storeName, key);
223
+ return value;
106
224
  }
107
225
 
108
- /**
109
- * 指定されたキーに対応するデータがストレージ内に存在するかを確認します。
110
- *
111
- * @param args.key 確認するキーです。
112
- * @returns キーに対応するデータが存在する場合は true、それ以外は false です。
113
- */
114
- public async exists(args: Pick<IStorage.ExistsArgs, "key">): Promise<boolean> {
115
- const { key } = args;
226
+ public async exists(c: IStorage.ExistsContext): Promise<boolean> {
227
+ c.signal.throwIfAborted();
228
+
229
+ const db = this.#getDb();
230
+
231
+ // 件数が 1 であれば存在し、0 であれば存在しません。
232
+ const count = await db.count(this.#storeName, c.key);
233
+
234
+ c.signal.throwIfAborted();
116
235
 
117
- // count() はキーが存在すれば 1 を、存在しなければ 0 を返します
118
- const count = await this.db!.count(this.storeName, key);
119
236
  return count > 0;
120
237
  }
121
238
 
122
- /**
123
- * 指定されたキーに対応するデータをストレージから削除します。
124
- *
125
- * @param args.key 削除するキーです。
126
- */
127
- public async delete(args: Pick<IStorage.DeleteArgs, "key">): Promise<void> {
128
- const { key } = args;
239
+ public async delete(c: IStorage.DeleteContext): Promise<void> {
240
+ c.signal.throwIfAborted();
241
+
242
+ const db = this.#getDb();
243
+ await db.delete(this.#storeName, c.key);
129
244
 
130
- await this.db!.delete(this.storeName, key);
245
+ c.signal.throwIfAborted();
131
246
  }
132
247
 
133
- /**
134
- * ストレージ内のすべてのデータを完全に消去します。
135
- */
136
- public async clear(): Promise<void> {
137
- await this.db!.clear(this.storeName);
248
+ public async clear(c: IStorage.ClearContext): Promise<void> {
249
+ c.signal.throwIfAborted();
250
+
251
+ const db = this.#getDb();
252
+ await db.clear(this.#storeName);
253
+
254
+ c.signal.throwIfAborted();
138
255
  }
139
256
 
140
- /**
141
- * 指定されたキーに対応する書き込み可能なストリームを取得します。
142
- *
143
- * IndexedDB にはネイティブなストリームがないため、書き込まれたチャンクをメモリー上に保持し、ストリームがクローズされたときにまとめて保存します。
144
- *
145
- * @param args.key 書き込み先のキーです。
146
- * @returns 書き込み可能なストリームです。
147
- */
148
- public getWritable(args: Pick<IStorage.GetWritableArgs, "key">): WritableStream<Uint8Array> {
149
- const { key } = args;
257
+ public getWritable(c: IStorage.GetWritableContext): WritableStream<Uint8Array> {
258
+ c.signal.throwIfAborted();
259
+
150
260
  const chunks: Uint8Array[] = [];
151
261
 
152
- return new WritableStream({
153
- write: (chunk) => {
154
- chunks.push(chunk);
262
+ return new WritableStream<Uint8Array>({
263
+ abort: (): void => {
264
+ chunks.length = 0;
155
265
  },
156
- close: async () => {
157
- const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
158
- const result = new Uint8Array(totalLength);
266
+ close: async (): Promise<void> => {
267
+ // 保存は close のときだけ行うため、中断時はここまでのチャンクを破棄できます。
268
+ c.signal.throwIfAborted();
269
+
270
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
271
+ const merged = new Uint8Array(totalLength);
159
272
  let offset = 0;
160
273
 
161
274
  for (const chunk of chunks) {
162
- result.set(chunk, offset);
275
+ merged.set(chunk, offset);
163
276
  offset += chunk.length;
164
277
  }
165
278
 
166
- await this.db!.put(this.storeName, result, key);
279
+ const db = this.#getDb();
280
+ await db.put(this.#storeName, merged, c.key);
281
+
282
+ c.signal.throwIfAborted();
283
+ },
284
+ write: (chunk): void => {
285
+ c.signal.throwIfAborted();
286
+ chunks.push(chunk);
167
287
  },
168
288
  });
169
289
  }
170
290
 
171
- /**
172
- * 指定されたキーに対応する読み取り可能なストリームを取得します。
173
- *
174
- * IndexedDB にはネイティブなストリームがないため、データ全体をメモリーにロードしてから単一チャンクとしてストリームで送出します。
175
- *
176
- * @param args.key 読み取り元のキーです。
177
- * @returns 読み取り可能なストリームです。
178
- */
179
- public getReadable(args: Pick<IStorage.GetReadableArgs, "key">): ReadableStream<Uint8Array> {
180
- const { key } = args;
181
-
182
- return new ReadableStream({
183
- pull: async (controller) => {
184
- const data = await this.read({ key });
185
- controller.enqueue(data);
186
- controller.close();
291
+ public getReadable(c: IStorage.GetReadableContext): ReadableStream<Uint8Array> {
292
+ c.signal.throwIfAborted();
293
+
294
+ return new ReadableStream<Uint8Array>({
295
+ pull: async (controller: ReadableStreamDefaultController<Uint8Array>): Promise<void> => {
296
+ try {
297
+ const data = await this.read(c as unknown as IStorage.ReadContext);
298
+
299
+ c.signal.throwIfAborted();
300
+ controller.enqueue(data as Uint8Array);
301
+ controller.close();
302
+ } catch (ex) {
303
+ controller.error(ex);
304
+ }
187
305
  },
188
306
  });
189
307
  }
@@ -1,95 +0,0 @@
1
- import type { IStorage } from "@unikvs/core";
2
- /**
3
- * ブラウザーの IndexedDB を永続化先として使用するストレージクラスです。
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
- * ストレージの名前です。デバッグメッセージなどに使用されます。
22
- */
23
- readonly name: string;
24
- /**
25
- * Indexeddb インスタンスを初期化します。
26
- *
27
- * @param dbName データベース名です。デフォルトは `"unikvs_db"` です。
28
- * @param storeName データを保存するオブジェクトストア名です。デフォルトは `"kvs_store"` です。
29
- */
30
- constructor(dbName?: string, storeName?: string);
31
- /** ストレージが現在利用可能な状態であるかを示します。 */
32
- get isOpen(): boolean;
33
- /**
34
- * ストレージをオープンし、IndexedDB データベースへの接続を確立します。
35
- *
36
- * 既にオープンされている場合は何も行いません。
37
- */
38
- open(): Promise<void>;
39
- /**
40
- * ストレージを安全にクローズします。
41
- *
42
- * データベース接続を閉じ、内部状態を初期化します。
43
- */
44
- close(): Promise<void>;
45
- /**
46
- * 指定されたデータを、対応するキーでストレージに保存します。
47
- *
48
- * @param args.key 保存先のキーです。
49
- * @param args.data 保存するデータです。
50
- */
51
- write(args: Pick<IStorage.WriteArgs<any>, "key" | "data">): Promise<void>;
52
- /**
53
- * 指定されたキーに対応するデータをストレージから取得します。
54
- *
55
- * @param args.key 取得元のキーです。
56
- * @returns キーに対応するデータです。存在しない場合は DOMException (NotFoundError) を投げます。
57
- */
58
- read(args: Pick<IStorage.ReadArgs, "key">): Promise<any>;
59
- /**
60
- * 指定されたキーに対応するデータがストレージ内に存在するかを確認します。
61
- *
62
- * @param args.key 確認するキーです。
63
- * @returns キーに対応するデータが存在する場合は true、それ以外は false です。
64
- */
65
- exists(args: Pick<IStorage.ExistsArgs, "key">): Promise<boolean>;
66
- /**
67
- * 指定されたキーに対応するデータをストレージから削除します。
68
- *
69
- * @param args.key 削除するキーです。
70
- */
71
- delete(args: Pick<IStorage.DeleteArgs, "key">): Promise<void>;
72
- /**
73
- * ストレージ内のすべてのデータを完全に消去します。
74
- */
75
- clear(): Promise<void>;
76
- /**
77
- * 指定されたキーに対応する書き込み可能なストリームを取得します。
78
- *
79
- * IndexedDB にはネイティブなストリームがないため、書き込まれたチャンクをメモリー上に保持し、ストリームがクローズされたときにまとめて保存します。
80
- *
81
- * @param args.key 書き込み先のキーです。
82
- * @returns 書き込み可能なストリームです。
83
- */
84
- getWritable(args: Pick<IStorage.GetWritableArgs, "key">): WritableStream<Uint8Array>;
85
- /**
86
- * 指定されたキーに対応する読み取り可能なストリームを取得します。
87
- *
88
- * IndexedDB にはネイティブなストリームがないため、データ全体をメモリーにロードしてから単一チャンクとしてストリームで送出します。
89
- *
90
- * @param args.key 読み取り元のキーです。
91
- * @returns 読み取り可能なストリームです。
92
- */
93
- getReadable(args: Pick<IStorage.GetReadableArgs, "key">): ReadableStream<Uint8Array>;
94
- }
95
- //# 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;;;;;OAKG;IACH,YAAmB,MAAM,GAAE,MAAoB,EAAE,SAAS,GAAE,MAAoB,EAK/E;IAED,gCAAgC;IAChC,IAAW,MAAM,IAAI,OAAO,CAE3B;IAED;;;;OAIG;IACU,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAajC;IAED;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAGlC;IAED;;;;;OAKG;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;;;;;OAKG;IACU,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAWpE;IAED;;;;;OAKG;IACU,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAM5E;IAED;;;;OAIG;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;;;;;;;OAOG;IACI,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,eAAe,EAAE,KAAK,CAAC,GAAG,cAAc,CAAC,UAAU,CAAC,CAqB1F;IAED;;;;;;;OAOG;IACI,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,eAAe,EAAE,KAAK,CAAC,GAAG,cAAc,CAAC,UAAU,CAAC,CAU1F;CACF"}
@@ -1,145 +0,0 @@
1
- import { openDB } from "idb";
2
- /**
3
- * ブラウザーの IndexedDB を永続化先として使用するストレージクラスです。
4
- */
5
- export default class Indexeddb {
6
- /**
7
- * Indexeddb インスタンスを初期化します。
8
- *
9
- * @param dbName データベース名です。デフォルトは `"unikvs_db"` です。
10
- * @param storeName データを保存するオブジェクトストア名です。デフォルトは `"kvs_store"` です。
11
- */
12
- constructor(dbName = "unikvs_db", storeName = "kvs_store") {
13
- this.name = "Indexeddb";
14
- this.db = null;
15
- this.dbName = dbName;
16
- this.storeName = storeName;
17
- }
18
- /** ストレージが現在利用可能な状態であるかを示します。 */
19
- get isOpen() {
20
- return this.db !== null;
21
- }
22
- /**
23
- * ストレージをオープンし、IndexedDB データベースへの接続を確立します。
24
- *
25
- * 既にオープンされている場合は何も行いません。
26
- */
27
- async open() {
28
- if (this.db) {
29
- return;
30
- }
31
- this.db = await openDB(this.dbName, 1, {
32
- upgrade: (db) => {
33
- // オブジェクトストアが存在しない場合は作成します
34
- if (!db.objectStoreNames.contains(this.storeName)) {
35
- db.createObjectStore(this.storeName);
36
- }
37
- },
38
- });
39
- }
40
- /**
41
- * ストレージを安全にクローズします。
42
- *
43
- * データベース接続を閉じ、内部状態を初期化します。
44
- */
45
- async close() {
46
- this.db.close();
47
- this.db = null;
48
- }
49
- /**
50
- * 指定されたデータを、対応するキーでストレージに保存します。
51
- *
52
- * @param args.key 保存先のキーです。
53
- * @param args.data 保存するデータです。
54
- */
55
- async write(args) {
56
- const { key, data } = args;
57
- await this.db.put(this.storeName, data, key);
58
- }
59
- /**
60
- * 指定されたキーに対応するデータをストレージから取得します。
61
- *
62
- * @param args.key 取得元のキーです。
63
- * @returns キーに対応するデータです。存在しない場合は DOMException (NotFoundError) を投げます。
64
- */
65
- async read(args) {
66
- const { key } = args;
67
- // Opfs の挙動に合わせて、存在しない場合は DOMException (NotFoundError) を投げます
68
- if (!(await this.exists({ key }))) {
69
- throw new DOMException(`A requested file or directory could not be found at the time an operation was processed.`, "NotFoundError");
70
- }
71
- return await this.db.get(this.storeName, key);
72
- }
73
- /**
74
- * 指定されたキーに対応するデータがストレージ内に存在するかを確認します。
75
- *
76
- * @param args.key 確認するキーです。
77
- * @returns キーに対応するデータが存在する場合は true、それ以外は false です。
78
- */
79
- async exists(args) {
80
- const { key } = args;
81
- // count() はキーが存在すれば 1 を、存在しなければ 0 を返します
82
- const count = await this.db.count(this.storeName, key);
83
- return count > 0;
84
- }
85
- /**
86
- * 指定されたキーに対応するデータをストレージから削除します。
87
- *
88
- * @param args.key 削除するキーです。
89
- */
90
- async delete(args) {
91
- const { key } = args;
92
- await this.db.delete(this.storeName, key);
93
- }
94
- /**
95
- * ストレージ内のすべてのデータを完全に消去します。
96
- */
97
- async clear() {
98
- await this.db.clear(this.storeName);
99
- }
100
- /**
101
- * 指定されたキーに対応する書き込み可能なストリームを取得します。
102
- *
103
- * IndexedDB にはネイティブなストリームがないため、書き込まれたチャンクをメモリー上に保持し、ストリームがクローズされたときにまとめて保存します。
104
- *
105
- * @param args.key 書き込み先のキーです。
106
- * @returns 書き込み可能なストリームです。
107
- */
108
- getWritable(args) {
109
- const { key } = args;
110
- const chunks = [];
111
- return new WritableStream({
112
- write: (chunk) => {
113
- chunks.push(chunk);
114
- },
115
- close: async () => {
116
- const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
117
- const result = new Uint8Array(totalLength);
118
- let offset = 0;
119
- for (const chunk of chunks) {
120
- result.set(chunk, offset);
121
- offset += chunk.length;
122
- }
123
- await this.db.put(this.storeName, result, key);
124
- },
125
- });
126
- }
127
- /**
128
- * 指定されたキーに対応する読み取り可能なストリームを取得します。
129
- *
130
- * IndexedDB にはネイティブなストリームがないため、データ全体をメモリーにロードしてから単一チャンクとしてストリームで送出します。
131
- *
132
- * @param args.key 読み取り元のキーです。
133
- * @returns 読み取り可能なストリームです。
134
- */
135
- getReadable(args) {
136
- const { key } = args;
137
- return new ReadableStream({
138
- pull: async (controller) => {
139
- const data = await this.read({ key });
140
- controller.enqueue(data);
141
- controller.close();
142
- },
143
- });
144
- }
145
- }