@luvio/durable-store-idb 0.128.0

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,23 @@
1
+ # durable-store-idb
2
+
3
+ An implementation of the Luvio DurableStore based on [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API).
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm i @luvio/durable-store-idb
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```js
14
+ import { IndexedDbDurableStore } from '@luvio/durable-store-idb';
15
+
16
+ const durableStore = new IndexedDbDurableStore();
17
+
18
+ const luvio = new Luvio(
19
+ makeDurable(new Environment(new InMemoryStore(), networkAdapter), {
20
+ durableStore,
21
+ })
22
+ );
23
+ ```
@@ -0,0 +1,21 @@
1
+ import type { DurableStore, DurableStoreEntries, DurableStoreOperation, OnDurableStoreChangedListener } from '@luvio/environments';
2
+ export declare class IndexedDbDurableStore implements DurableStore {
3
+ dbs: Map<string, IDBDatabase>;
4
+ listeners: Set<OnDurableStoreChangedListener>;
5
+ private static DB_NAME_PREFIX;
6
+ private buildDbName;
7
+ private getDb;
8
+ getEntries<T>(entryIds: string[], segment: string): Promise<DurableStoreEntries<T> | undefined>;
9
+ getAllEntries<T>(segment: string): Promise<DurableStoreEntries<T> | undefined>;
10
+ setEntries<T>(entries: DurableStoreEntries<T>, segment: string): Promise<void>;
11
+ batchOperations<T>(operations: DurableStoreOperation<T>[]): Promise<void>;
12
+ evictEntries(entryIds: string[], segment: string): Promise<void>;
13
+ registerOnChangedListener(listener: OnDurableStoreChangedListener): () => Promise<void>;
14
+ /**
15
+ * Clears all entries from any of the IndexedDB stores used by this class.
16
+ *
17
+ * NOTE: this method isn't currently part of the DurableStore interface, but still
18
+ * useful to implement.
19
+ */
20
+ clearAllEntries(): Promise<void>;
21
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Helper methods to make working with IDB more promise-based. Also simplifies
3
+ * IDB schema concepts to just having a single "kvp" store per DB.
4
+ */
5
+ function idbTransactionToPromise(transaction) {
6
+ if (transaction === null) {
7
+ return Promise.resolve();
8
+ }
9
+ return new Promise((resolve, reject) => {
10
+ transaction.oncomplete = () => resolve(undefined);
11
+ transaction.onabort = () => reject(transaction.error);
12
+ transaction.onerror = () => reject(transaction.error);
13
+ });
14
+ }
15
+ function idbRequestToPromise(request) {
16
+ return new Promise((resolve, reject) => {
17
+ request.onsuccess = () => resolve(request.result);
18
+ request.onerror = () => reject(request.error);
19
+ });
20
+ }
21
+ /**
22
+ * Checks if the given database exists in indexedDB. If this method cannot determine
23
+ * if a database exists (because the "indexedDB.databases" API isn't implemented) this
24
+ * returns undefined
25
+ */
26
+ async function doesDbExist(dbName) {
27
+ // firefox doesn't support listing all databases (yet),
28
+ // so have to check if API is available
29
+ if (typeof indexedDB.databases === 'undefined') {
30
+ return undefined;
31
+ }
32
+ else {
33
+ const dbs = await indexedDB.databases();
34
+ return dbs.find((db) => db.name === dbName) !== undefined;
35
+ }
36
+ }
37
+ const COMMON_STORE_NAME = 'kvp';
38
+ async function getDb(dbName, createIfNotExist) {
39
+ const exists = await doesDbExist(dbName);
40
+ if (exists === false && !createIfNotExist) {
41
+ return undefined;
42
+ }
43
+ const request = indexedDB.open(dbName);
44
+ request.onupgradeneeded = () => request.result.createObjectStore(COMMON_STORE_NAME);
45
+ return idbRequestToPromise(request);
46
+ }
47
+ // eslint-disable-next-line no-undef
48
+ function getStore(db, transactionMode) {
49
+ return db.transaction(COMMON_STORE_NAME, transactionMode).objectStore(COMMON_STORE_NAME);
50
+ }
51
+ // eslint-disable-next-line no-undef
52
+ function get(keys, db) {
53
+ const store = getStore(db, 'readonly');
54
+ return Promise.all(keys.map((key) => idbRequestToPromise(store.get(key))));
55
+ }
56
+ async function getAll(db) {
57
+ const store = getStore(db, 'readonly');
58
+ const keysPromise = idbRequestToPromise(store.getAllKeys());
59
+ const valuesPromise = idbRequestToPromise(store.getAll());
60
+ const [keys, values] = await Promise.all([keysPromise, valuesPromise]);
61
+ return keys.map((key, index) => ({ key, value: values[index] }));
62
+ }
63
+ function set(entries, db) {
64
+ const store = getStore(db, 'readwrite');
65
+ entries.forEach((entry) => store.put(entry.value, entry.key));
66
+ return idbTransactionToPromise(store.transaction);
67
+ }
68
+ // eslint-disable-next-line no-undef
69
+ function del(keys, db) {
70
+ const store = getStore(db, 'readwrite');
71
+ keys.forEach((key) => store.delete(key));
72
+ return idbTransactionToPromise(store.transaction);
73
+ }
74
+ function clear(db) {
75
+ const store = getStore(db, 'readwrite');
76
+ store.clear();
77
+ return idbTransactionToPromise(store.transaction);
78
+ }
79
+
80
+ // using a type to ensure we use correct segment name without taking runtime dep
81
+ const DEFAULT_SEGMENT_NAME = 'DEFAULT';
82
+ class IndexedDbDurableStore {
83
+ constructor() {
84
+ this.dbs = new Map();
85
+ this.listeners = new Set();
86
+ }
87
+ buildDbName(segment) {
88
+ return `${IndexedDbDurableStore.DB_NAME_PREFIX}${segment}`;
89
+ }
90
+ async getDb(segment, createIfNotExist) {
91
+ const dbName = this.buildDbName(segment);
92
+ // first check if db is memoized
93
+ const memoizedDb = this.dbs.get(dbName);
94
+ if (memoizedDb) {
95
+ return memoizedDb;
96
+ }
97
+ const db = await getDb(dbName, createIfNotExist);
98
+ if (db === undefined) {
99
+ return undefined;
100
+ }
101
+ // memoize the db
102
+ this.dbs.set(dbName, db);
103
+ return db;
104
+ }
105
+ async getEntries(entryIds, segment) {
106
+ const db = await this.getDb(segment, false);
107
+ if (db === undefined) {
108
+ return undefined;
109
+ }
110
+ const rows = await get(entryIds, db);
111
+ const returnEntries = {};
112
+ for (let i = 0, len = rows.length; i < len; i++) {
113
+ const row = rows[i];
114
+ const key = entryIds[i];
115
+ if (row !== undefined) {
116
+ returnEntries[key] = row;
117
+ }
118
+ }
119
+ return returnEntries;
120
+ }
121
+ async getAllEntries(segment) {
122
+ const db = await this.getDb(segment, false);
123
+ if (db === undefined) {
124
+ return undefined;
125
+ }
126
+ const rows = await getAll(db);
127
+ const returnEntries = {};
128
+ for (let i = 0, len = rows.length; i < len; i++) {
129
+ const row = rows[i];
130
+ const { key, value } = row;
131
+ returnEntries[key] = value;
132
+ }
133
+ return returnEntries;
134
+ }
135
+ async setEntries(entries, segment) {
136
+ const db = await this.getDb(segment, true);
137
+ const rows = [];
138
+ const keys = Object.keys(entries);
139
+ for (const key of keys) {
140
+ rows.push({ key, value: entries[key] });
141
+ }
142
+ // we told getDb to createIfNotExist so we know db will be defined
143
+ await set(rows, db);
144
+ this.listeners.forEach((listener) => {
145
+ listener([{ ids: keys, segment, type: 'setEntries' }]);
146
+ });
147
+ }
148
+ async batchOperations(operations) {
149
+ // TODO - this could be optimized to do all evicts and sets in the same
150
+ // call to idb
151
+ for (const operation of operations) {
152
+ if (operation.type === 'evictEntries') {
153
+ await this.evictEntries(operation.ids, operation.segment);
154
+ }
155
+ else if (operation.type === 'setEntries') {
156
+ await this.setEntries(operation.entries, operation.segment);
157
+ }
158
+ }
159
+ }
160
+ async evictEntries(entryIds, segment) {
161
+ const db = await this.getDb(segment, true);
162
+ // we told getDb to createIfNotExist so we know db will be defined
163
+ await del(entryIds, db);
164
+ this.listeners.forEach((listener) => {
165
+ listener([{ ids: entryIds, segment, type: 'evictEntries' }]);
166
+ });
167
+ }
168
+ registerOnChangedListener(listener) {
169
+ this.listeners.add(listener);
170
+ return () => {
171
+ this.listeners.delete(listener);
172
+ return Promise.resolve();
173
+ };
174
+ }
175
+ /**
176
+ * Clears all entries from any of the IndexedDB stores used by this class.
177
+ *
178
+ * NOTE: this method isn't currently part of the DurableStore interface, but still
179
+ * useful to implement.
180
+ */
181
+ async clearAllEntries() {
182
+ const dbsToClear = [];
183
+ // firefox doesn't support listing all databases (yet),
184
+ // so we clear the default segment (it contains all the
185
+ // data)
186
+ if (typeof indexedDB.databases === 'undefined') {
187
+ dbsToClear.push(this.buildDbName(DEFAULT_SEGMENT_NAME));
188
+ }
189
+ else {
190
+ const dbs = await indexedDB.databases();
191
+ dbs.forEach((db) => {
192
+ var _a;
193
+ if (((_a = db.name) === null || _a === void 0 ? void 0 : _a.startsWith(IndexedDbDurableStore.DB_NAME_PREFIX)) === true) {
194
+ dbsToClear.push(db.name);
195
+ }
196
+ });
197
+ }
198
+ await Promise.all(dbsToClear.map((dbName) => getDb(dbName, false).then((db) => {
199
+ if (db !== undefined) {
200
+ clear(db);
201
+ }
202
+ })));
203
+ dbsToClear.forEach((dbName) => this.dbs.delete(dbName));
204
+ }
205
+ }
206
+ IndexedDbDurableStore.DB_NAME_PREFIX = 'durable-store-idb-';
207
+
208
+ export { IndexedDbDurableStore };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Helper methods to make working with IDB more promise-based. Also simplifies
3
+ * IDB schema concepts to just having a single "kvp" store per DB.
4
+ */
5
+ export declare type KeyValue<T = unknown> = {
6
+ key: IDBValidKey;
7
+ value: T;
8
+ };
9
+ export declare function getDb(dbName: string, createIfNotExist: boolean): Promise<IDBDatabase | undefined>;
10
+ export declare function get<T = unknown>(keys: IDBValidKey[], db: IDBDatabase): Promise<T[]>;
11
+ export declare function getAll<T = unknown>(db: IDBDatabase): Promise<KeyValue<T>[]>;
12
+ export declare function set<T = unknown>(entries: KeyValue<T>[], db: IDBDatabase): Promise<void>;
13
+ export declare function del(keys: IDBValidKey[], db: IDBDatabase): Promise<void>;
14
+ export declare function clear(db: IDBDatabase): Promise<void>;
package/dist/main.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { IndexedDbDurableStore } from './IndexedDbDurableStore';
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@luvio/durable-store-idb",
3
+ "version": "0.128.0",
4
+ "description": "IndexedDB-based implementation of the Luvio DurableStore",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/salesforce-experience-platform-emu/luvio.git",
8
+ "directory": "packages/@luvio/durable-store-idb"
9
+ },
10
+ "license": "MIT",
11
+ "main": "dist/durable-store-idb.js",
12
+ "module": "dist/durable-store-idb.js",
13
+ "types": "dist/main.d.ts",
14
+ "files": [
15
+ "dist/"
16
+ ],
17
+ "scripts": {
18
+ "build": "rollup --config rollup.config.js",
19
+ "clean": "rm -rf dist",
20
+ "test": "jest",
21
+ "test:debug": "node --inspect-brk ../../../node_modules/jest/bin/jest.js --config ./jest.config.js --runInBand",
22
+ "test:size": "bundlesize",
23
+ "watch": "yarn build --watch"
24
+ },
25
+ "devDependencies": {
26
+ "@luvio/environments": "0.128.0",
27
+ "fake-indexeddb": "^4.0.1"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "bundlesize": [
33
+ {
34
+ "path": "./dist/durable-store-idb.js",
35
+ "maxSize": "3 kB",
36
+ "compression": "brotli"
37
+ }
38
+ ],
39
+ "nx": {
40
+ "targets": {
41
+ "build": {
42
+ "outputs": [
43
+ "packages/@luvio/durable-store-idb/dist"
44
+ ]
45
+ }
46
+ }
47
+ }
48
+ }