@instantdb/core 0.22.57 → 0.22.58

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.
Files changed (60) hide show
  1. package/__tests__/src/Reactor.test.js +97 -21
  2. package/__tests__/src/utils/PersistedObject.test.ts +383 -0
  3. package/dist/commonjs/InMemoryStorage.d.ts +11 -0
  4. package/dist/commonjs/InMemoryStorage.d.ts.map +1 -0
  5. package/dist/commonjs/InMemoryStorage.js +48 -0
  6. package/dist/commonjs/InMemoryStorage.js.map +1 -0
  7. package/dist/commonjs/IndexedDBStorage.d.ts +10 -3
  8. package/dist/commonjs/IndexedDBStorage.d.ts.map +1 -1
  9. package/dist/commonjs/IndexedDBStorage.js +254 -8
  10. package/dist/commonjs/IndexedDBStorage.js.map +1 -1
  11. package/dist/commonjs/Reactor.d.ts +7 -16
  12. package/dist/commonjs/Reactor.d.ts.map +1 -1
  13. package/dist/commonjs/Reactor.js +199 -149
  14. package/dist/commonjs/Reactor.js.map +1 -1
  15. package/dist/commonjs/SyncTable.d.ts +3 -1
  16. package/dist/commonjs/SyncTable.d.ts.map +1 -1
  17. package/dist/commonjs/SyncTable.js +194 -168
  18. package/dist/commonjs/SyncTable.js.map +1 -1
  19. package/dist/commonjs/index.d.ts +2 -1
  20. package/dist/commonjs/index.d.ts.map +1 -1
  21. package/dist/commonjs/index.js +3 -1
  22. package/dist/commonjs/index.js.map +1 -1
  23. package/dist/commonjs/utils/PersistedObject.d.ts +73 -29
  24. package/dist/commonjs/utils/PersistedObject.d.ts.map +1 -1
  25. package/dist/commonjs/utils/PersistedObject.js +366 -77
  26. package/dist/commonjs/utils/PersistedObject.js.map +1 -1
  27. package/dist/esm/InMemoryStorage.d.ts +11 -0
  28. package/dist/esm/InMemoryStorage.d.ts.map +1 -0
  29. package/dist/esm/InMemoryStorage.js +45 -0
  30. package/dist/esm/InMemoryStorage.js.map +1 -0
  31. package/dist/esm/IndexedDBStorage.d.ts +10 -3
  32. package/dist/esm/IndexedDBStorage.d.ts.map +1 -1
  33. package/dist/esm/IndexedDBStorage.js +254 -8
  34. package/dist/esm/IndexedDBStorage.js.map +1 -1
  35. package/dist/esm/Reactor.d.ts +7 -16
  36. package/dist/esm/Reactor.d.ts.map +1 -1
  37. package/dist/esm/Reactor.js +199 -149
  38. package/dist/esm/Reactor.js.map +1 -1
  39. package/dist/esm/SyncTable.d.ts +3 -1
  40. package/dist/esm/SyncTable.d.ts.map +1 -1
  41. package/dist/esm/SyncTable.js +194 -168
  42. package/dist/esm/SyncTable.js.map +1 -1
  43. package/dist/esm/index.d.ts +2 -1
  44. package/dist/esm/index.d.ts.map +1 -1
  45. package/dist/esm/index.js +4 -1
  46. package/dist/esm/index.js.map +1 -1
  47. package/dist/esm/utils/PersistedObject.d.ts +73 -29
  48. package/dist/esm/utils/PersistedObject.d.ts.map +1 -1
  49. package/dist/esm/utils/PersistedObject.js +364 -76
  50. package/dist/esm/utils/PersistedObject.js.map +1 -1
  51. package/dist/standalone/index.js +1978 -1567
  52. package/dist/standalone/index.umd.cjs +3 -3
  53. package/package.json +2 -2
  54. package/src/InMemoryStorage.ts +34 -0
  55. package/src/IndexedDBStorage.ts +281 -9
  56. package/src/Reactor.js +212 -184
  57. package/src/SyncTable.ts +220 -191
  58. package/src/index.ts +8 -0
  59. package/src/utils/PersistedObject.ts +480 -109
  60. package/src/InMemoryStorage.js +0 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instantdb/core",
3
- "version": "v0.22.57",
3
+ "version": "v0.22.58",
4
4
  "description": "Instant's core local abstraction",
5
5
  "homepage": "https://github.com/instantdb/instant/tree/main/client/packages/core",
6
6
  "repository": {
@@ -53,7 +53,7 @@
53
53
  "dependencies": {
54
54
  "mutative": "^1.0.10",
55
55
  "uuid": "^11.1.0",
56
- "@instantdb/version": "v0.22.57"
56
+ "@instantdb/version": "v0.22.58"
57
57
  },
58
58
  "scripts": {
59
59
  "test": "vitest",
@@ -0,0 +1,34 @@
1
+ import {
2
+ StorageInterface,
3
+ StorageInterfaceStoreName,
4
+ } from './utils/PersistedObject.js';
5
+
6
+ export default class InMemoryStorage extends StorageInterface {
7
+ private store: Map<string, any>;
8
+ constructor(appId: string, dbName: StorageInterfaceStoreName) {
9
+ super(appId, dbName);
10
+ this.store = new Map();
11
+ }
12
+
13
+ async getItem(k) {
14
+ return this.store.get(k) ?? null;
15
+ }
16
+
17
+ async setItem(k, v) {
18
+ this.store.set(k, v);
19
+ }
20
+
21
+ async getAllKeys(): Promise<string[]> {
22
+ return [...this.store.keys()];
23
+ }
24
+
25
+ async multiSet(keyValuePairs: Array<[string, any]>): Promise<void> {
26
+ for (const [k, v] of keyValuePairs) {
27
+ this.setItem(k, v);
28
+ }
29
+ }
30
+
31
+ async removeItem(key: string): Promise<void> {
32
+ this.store.delete(key);
33
+ }
34
+ }
@@ -1,16 +1,206 @@
1
- export default class IndexedDBStorage {
1
+ import {
2
+ Meta,
3
+ META_KEY,
4
+ ObjectMeta,
5
+ StorageInterface,
6
+ StorageInterfaceStoreName,
7
+ } from './utils/PersistedObject.ts';
8
+
9
+ // Any time these are updates to the data format or new stores are added,
10
+ // the version must be updated.
11
+ // onupgradeneeded will be called, which is where you can
12
+ // move objects from one idb to another.
13
+ // We create a new IDB for each version change instead of
14
+ // using their built-in versioning because they have no ability
15
+ // to roll back and if multiple tabs are active, then you'll just
16
+ // be stuck.
17
+ const version = 6;
18
+
19
+ const storeNames = ['kv', 'querySubs', 'syncSubs'] as const;
20
+
21
+ // Check that we're not missing a store name in storeNames
22
+ type MissingStoreNames = Exclude<
23
+ StorageInterfaceStoreName,
24
+ (typeof storeNames)[number]
25
+ >;
26
+ const _exhaustiveCheck: never = null as MissingStoreNames;
27
+
28
+ function logErrorCb(source: string) {
29
+ return function logError(event) {
30
+ console.error('Error in IndexedDB event', { source, event });
31
+ };
32
+ }
33
+
34
+ async function existingDb(name: string): Promise<IDBDatabase | null> {
35
+ return new Promise((resolve) => {
36
+ const request = indexedDB.open(name);
37
+
38
+ request.onerror = (_event) => {
39
+ resolve(null);
40
+ };
41
+
42
+ request.onsuccess = (event) => {
43
+ const target = event.target as IDBOpenDBRequest;
44
+ const db = target.result;
45
+ resolve(db);
46
+ };
47
+
48
+ request.onupgradeneeded = (event) => {
49
+ const target = event.target as IDBOpenDBRequest;
50
+ target.transaction.abort();
51
+ resolve(null);
52
+ };
53
+ });
54
+ }
55
+
56
+ async function upgradeQuerySubs5To6(
57
+ hash: string,
58
+ value: any,
59
+ querySubStore: IDBObjectStore,
60
+ ): Promise<void> {
61
+ const subs =
62
+ // Backwards compatibility for older versions where we JSON.stringified before storing
63
+ typeof value === 'string' ? JSON.parse(value) : value;
64
+ if (!subs) {
65
+ return;
66
+ }
67
+ const putReqs: Set<IDBRequest<IDBValidKey>> = new Set();
68
+ return new Promise((resolve, reject) => {
69
+ const objects = {};
70
+ for (const [hash, v] of Object.entries(subs)) {
71
+ const value = typeof v === 'string' ? JSON.parse(v) : v;
72
+ if (value.lastAccessed) {
73
+ const objectMeta: ObjectMeta = {
74
+ createdAt: value.lastAccessed,
75
+ updatedAt: value.lastAccessed,
76
+ size: value.result?.store?.triples?.length ?? 0,
77
+ };
78
+ objects[hash] = objectMeta;
79
+ }
80
+ const putReq = querySubStore.put(value, hash);
81
+ putReqs.add(putReq);
82
+ }
83
+ const meta: Meta<string> = { objects };
84
+ const metaPutReq = querySubStore.put(meta, META_KEY);
85
+ putReqs.add(metaPutReq);
86
+ for (const r of putReqs) {
87
+ r.onsuccess = () => {
88
+ putReqs.delete(r);
89
+ if (putReqs.size === 0) {
90
+ resolve();
91
+ }
92
+ };
93
+ r.onerror = (event) => {
94
+ logErrorCb(`Move ${hash} to querySubs store failed`);
95
+ reject(event);
96
+ };
97
+ }
98
+ });
99
+ }
100
+
101
+ async function moveKvEntry5To6(
102
+ k: string,
103
+ value: any,
104
+ kvStore: IDBObjectStore,
105
+ ): Promise<void> {
106
+ const request = kvStore.put(value, k);
107
+ return new Promise((resolve, reject) => {
108
+ request.onsuccess = () => resolve();
109
+ request.onerror = (event) => reject(event);
110
+ });
111
+ }
112
+
113
+ async function upgrade5To6(appId: string, v6Db: IDBDatabase): Promise<void> {
114
+ const v5db = await existingDb(`instant_${appId}_5`);
115
+ if (!v5db) {
116
+ return;
117
+ }
118
+
119
+ const data: Array<[string, any]> = await new Promise((resolve, reject) => {
120
+ const v5Tx = v5db.transaction(['kv'], 'readonly');
121
+ const objectStore = v5Tx.objectStore('kv');
122
+ const cursorReq = objectStore.openCursor();
123
+ cursorReq.onerror = (event) => {
124
+ reject(event);
125
+ };
126
+ const data: Array<[string, any]> = [];
127
+ cursorReq.onsuccess = () => {
128
+ const cursor = cursorReq.result;
129
+ if (cursor) {
130
+ const key = cursor.key as string;
131
+ const value = cursor.value;
132
+ data.push([key, value]);
133
+ cursor.continue();
134
+ } else {
135
+ resolve(data);
136
+ }
137
+ };
138
+ cursorReq.onerror = (event) => {
139
+ reject(event);
140
+ };
141
+ });
142
+
143
+ const v6Tx = v6Db.transaction(['kv', 'querySubs'], 'readwrite');
144
+
145
+ const kvStore = v6Tx.objectStore('kv');
146
+ const querySubStore = v6Tx.objectStore('querySubs');
147
+
148
+ const promises = [];
149
+ const kvMeta: Meta<string> = { objects: {} };
150
+ for (const [key, value] of data) {
151
+ switch (key) {
152
+ case 'querySubs': {
153
+ const p = upgradeQuerySubs5To6(key, value, querySubStore);
154
+ promises.push(p);
155
+ break;
156
+ }
157
+ default: {
158
+ const p = moveKvEntry5To6(key as string, value, kvStore);
159
+ promises.push(p);
160
+ const objectMeta: ObjectMeta = {
161
+ createdAt: Date.now(),
162
+ updatedAt: Date.now(),
163
+ size: 0,
164
+ };
165
+ kvMeta.objects[key] = objectMeta;
166
+ break;
167
+ }
168
+ }
169
+ }
170
+ const p = moveKvEntry5To6(META_KEY, kvMeta, kvStore);
171
+ promises.push(p);
172
+ await Promise.all(promises);
173
+ await new Promise((resolve, reject) => {
174
+ v6Tx.oncomplete = (e) => resolve(e);
175
+ v6Tx.onerror = (e) => reject(e);
176
+ v6Tx.onabort = (e) => reject(e);
177
+ });
178
+ }
179
+
180
+ // We create many IndexedDBStorage instances that talk to the same
181
+ // underlying db, but we only get one `onupgradeneeded` event. This holds
182
+ // the upgrade promises so that we wait until upgrade finishes before
183
+ // we start writing.
184
+ const upgradePromises = new Map();
185
+
186
+ export default class IndexedDBStorage extends StorageInterface {
2
187
  dbName: string;
3
- _storeName: string;
188
+ _storeName: StorageInterfaceStoreName;
189
+ _appId: string;
190
+ _prefix: string;
4
191
  _dbPromise: Promise<IDBDatabase>;
5
192
 
6
- constructor(dbName: string) {
7
- this.dbName = dbName;
8
- this._storeName = 'kv';
193
+ constructor(appId: string, storeName: StorageInterfaceStoreName) {
194
+ super(appId, storeName);
195
+ this.dbName = `instant_${appId}_${version}`;
196
+ this._storeName = storeName;
197
+ this._appId = appId;
9
198
  this._dbPromise = this._init();
10
199
  }
11
200
 
12
201
  _init(): Promise<IDBDatabase> {
13
202
  return new Promise((resolve, reject) => {
203
+ let requiresUpgrade = false;
14
204
  const request = indexedDB.open(this.dbName, 1);
15
205
 
16
206
  request.onerror = (event) => {
@@ -19,17 +209,40 @@ export default class IndexedDBStorage {
19
209
 
20
210
  request.onsuccess = (event) => {
21
211
  const target = event.target as IDBOpenDBRequest;
22
- resolve(target.result);
212
+ const db = target.result;
213
+ if (!requiresUpgrade) {
214
+ const p = upgradePromises.get(this.dbName);
215
+ if (!p) {
216
+ resolve(db);
217
+ } else {
218
+ p.then(() => resolve(db)).catch(() => resolve(db));
219
+ }
220
+ } else {
221
+ const p = upgrade5To6(this._appId, db).catch((e) => {
222
+ logErrorCb('Error upgrading store from version 5 to 6.')(e);
223
+ });
224
+ upgradePromises.set(this.dbName, p);
225
+ p.then(() => resolve(db)).catch(() => resolve(db));
226
+ }
23
227
  };
24
228
 
25
229
  request.onupgradeneeded = (event) => {
26
- const target = event.target as IDBOpenDBRequest;
27
- const db = target.result;
28
- db.createObjectStore(this._storeName);
230
+ requiresUpgrade = true;
231
+ this._upgradeStore(event);
29
232
  };
30
233
  });
31
234
  }
32
235
 
236
+ _upgradeStore(event: IDBVersionChangeEvent) {
237
+ const target = event.target as IDBOpenDBRequest;
238
+ const db = target.result;
239
+ for (const storeName of storeNames) {
240
+ if (!db.objectStoreNames.contains(storeName)) {
241
+ db.createObjectStore(storeName);
242
+ }
243
+ }
244
+ }
245
+
33
246
  async getItem(k: string): Promise<any> {
34
247
  const db = await this._dbPromise;
35
248
  return new Promise((resolve, reject) => {
@@ -65,4 +278,63 @@ export default class IndexedDBStorage {
65
278
  };
66
279
  });
67
280
  }
281
+
282
+ // Performs all writes in a transaction so that all succeed or none succeed.
283
+ async multiSet(keyValuePairs: Array<[string, any]>): Promise<void> {
284
+ const db = await this._dbPromise;
285
+ return new Promise((resolve, reject) => {
286
+ const transaction = db.transaction([this._storeName], 'readwrite');
287
+ const objectStore = transaction.objectStore(this._storeName);
288
+ const requests: Set<IDBRequest<IDBValidKey>> = new Set();
289
+ for (const [k, v] of keyValuePairs) {
290
+ const request = objectStore.put(v, k);
291
+ requests.add(request);
292
+ }
293
+
294
+ for (const request of requests) {
295
+ request.onerror = (event) => {
296
+ transaction.abort();
297
+ reject(event);
298
+ };
299
+ request.onsuccess = (_event) => {
300
+ requests.delete(request);
301
+ // Last request to finish resolves the transaction
302
+ if (requests.size === 0) {
303
+ resolve();
304
+ }
305
+ };
306
+ }
307
+ });
308
+ }
309
+
310
+ async removeItem(k: string): Promise<void> {
311
+ const db = await this._dbPromise;
312
+ return new Promise((resolve, reject) => {
313
+ const transaction = db.transaction([this._storeName], 'readwrite');
314
+ const objectStore = transaction.objectStore(this._storeName);
315
+ const request = objectStore.delete(k);
316
+ request.onerror = (event) => {
317
+ reject(event);
318
+ };
319
+
320
+ request.onsuccess = (_event) => {
321
+ resolve();
322
+ };
323
+ });
324
+ }
325
+
326
+ async getAllKeys(): Promise<string[]> {
327
+ const db = await this._dbPromise;
328
+ return new Promise((resolve, reject) => {
329
+ const transaction = db.transaction([this._storeName], 'readonly');
330
+ const objectStore = transaction.objectStore(this._storeName);
331
+ const request = objectStore.getAllKeys();
332
+ request.onerror = (event) => {
333
+ reject(event);
334
+ };
335
+ request.onsuccess = (_event) => {
336
+ resolve(request.result.filter((x) => typeof x === 'string'));
337
+ };
338
+ });
339
+ }
68
340
  }