@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.
- package/__tests__/src/Reactor.test.js +97 -21
- package/__tests__/src/utils/PersistedObject.test.ts +383 -0
- package/dist/commonjs/InMemoryStorage.d.ts +11 -0
- package/dist/commonjs/InMemoryStorage.d.ts.map +1 -0
- package/dist/commonjs/InMemoryStorage.js +48 -0
- package/dist/commonjs/InMemoryStorage.js.map +1 -0
- package/dist/commonjs/IndexedDBStorage.d.ts +10 -3
- package/dist/commonjs/IndexedDBStorage.d.ts.map +1 -1
- package/dist/commonjs/IndexedDBStorage.js +254 -8
- package/dist/commonjs/IndexedDBStorage.js.map +1 -1
- package/dist/commonjs/Reactor.d.ts +7 -16
- package/dist/commonjs/Reactor.d.ts.map +1 -1
- package/dist/commonjs/Reactor.js +199 -149
- package/dist/commonjs/Reactor.js.map +1 -1
- package/dist/commonjs/SyncTable.d.ts +3 -1
- package/dist/commonjs/SyncTable.d.ts.map +1 -1
- package/dist/commonjs/SyncTable.js +194 -168
- package/dist/commonjs/SyncTable.js.map +1 -1
- package/dist/commonjs/index.d.ts +2 -1
- package/dist/commonjs/index.d.ts.map +1 -1
- package/dist/commonjs/index.js +3 -1
- package/dist/commonjs/index.js.map +1 -1
- package/dist/commonjs/utils/PersistedObject.d.ts +73 -29
- package/dist/commonjs/utils/PersistedObject.d.ts.map +1 -1
- package/dist/commonjs/utils/PersistedObject.js +366 -77
- package/dist/commonjs/utils/PersistedObject.js.map +1 -1
- package/dist/esm/InMemoryStorage.d.ts +11 -0
- package/dist/esm/InMemoryStorage.d.ts.map +1 -0
- package/dist/esm/InMemoryStorage.js +45 -0
- package/dist/esm/InMemoryStorage.js.map +1 -0
- package/dist/esm/IndexedDBStorage.d.ts +10 -3
- package/dist/esm/IndexedDBStorage.d.ts.map +1 -1
- package/dist/esm/IndexedDBStorage.js +254 -8
- package/dist/esm/IndexedDBStorage.js.map +1 -1
- package/dist/esm/Reactor.d.ts +7 -16
- package/dist/esm/Reactor.d.ts.map +1 -1
- package/dist/esm/Reactor.js +199 -149
- package/dist/esm/Reactor.js.map +1 -1
- package/dist/esm/SyncTable.d.ts +3 -1
- package/dist/esm/SyncTable.d.ts.map +1 -1
- package/dist/esm/SyncTable.js +194 -168
- package/dist/esm/SyncTable.js.map +1 -1
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +4 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/utils/PersistedObject.d.ts +73 -29
- package/dist/esm/utils/PersistedObject.d.ts.map +1 -1
- package/dist/esm/utils/PersistedObject.js +364 -76
- package/dist/esm/utils/PersistedObject.js.map +1 -1
- package/dist/standalone/index.js +1978 -1567
- package/dist/standalone/index.umd.cjs +3 -3
- package/package.json +2 -2
- package/src/InMemoryStorage.ts +34 -0
- package/src/IndexedDBStorage.ts +281 -9
- package/src/Reactor.js +212 -184
- package/src/SyncTable.ts +220 -191
- package/src/index.ts +8 -0
- package/src/utils/PersistedObject.ts +480 -109
- package/src/InMemoryStorage.js +0 -14
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
|
|
16
16
|
// Uses `requestIdleCallback` if available, otherwise calls the
|
|
17
17
|
// callback immediately
|
|
18
|
+
|
|
19
|
+
import { create } from 'mutative';
|
|
20
|
+
import type { Logger } from './log.ts';
|
|
21
|
+
|
|
18
22
|
function safeIdleCallback(cb, timeout: number) {
|
|
19
23
|
if (typeof requestIdleCallback === 'undefined') {
|
|
20
24
|
cb();
|
|
@@ -23,115 +27,313 @@ function safeIdleCallback(cb, timeout: number) {
|
|
|
23
27
|
}
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
30
|
+
export const META_KEY = '__meta';
|
|
31
|
+
|
|
32
|
+
export type ObjectMeta = { createdAt: number; updatedAt: number; size: number };
|
|
33
|
+
|
|
34
|
+
export type Meta<K extends string> = {
|
|
35
|
+
objects: Record<K, ObjectMeta>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type StorageInterfaceStoreName = 'kv' | 'querySubs' | 'syncSubs';
|
|
39
|
+
|
|
40
|
+
export abstract class StorageInterface {
|
|
41
|
+
constructor(appId: string, storeName: StorageInterfaceStoreName) {}
|
|
42
|
+
abstract getItem(key: string): Promise<any>;
|
|
43
|
+
abstract removeItem(key: string): Promise<void>;
|
|
44
|
+
abstract multiSet(keyValuePairs: Array<[string, any]>): Promise<void>;
|
|
45
|
+
abstract getAllKeys(): Promise<string[]>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type GCOpts = {
|
|
49
|
+
maxSize: number;
|
|
50
|
+
maxAgeMs: number;
|
|
51
|
+
maxEntries: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type Opts<K, T, SerializedT> = {
|
|
55
|
+
persister: StorageInterface;
|
|
56
|
+
/**
|
|
57
|
+
* Merges data from storage with in-memory value on load.
|
|
58
|
+
* The value returned from merge will become the current value.
|
|
59
|
+
*/
|
|
60
|
+
merge: (
|
|
61
|
+
key: K,
|
|
62
|
+
fromStorage: T | null | undefined,
|
|
63
|
+
inMemoryValue: T | null | undefined,
|
|
64
|
+
) => T;
|
|
65
|
+
serialize: (key: K, input: T) => SerializedT;
|
|
66
|
+
parse: (key: K, value: SerializedT) => T;
|
|
67
|
+
objectSize: (x: SerializedT) => number;
|
|
68
|
+
logger: Logger;
|
|
69
|
+
gc: GCOpts | null | undefined;
|
|
70
|
+
saveThrottleMs?: number | null | undefined;
|
|
71
|
+
idleCallbackMaxWaitMs?: number | null | undefined;
|
|
72
|
+
preloadEntryCount?: number | null | undefined;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export class PersistedObject<K extends string, T, SerializedT> {
|
|
76
|
+
currentValue: Record<K, T>;
|
|
77
|
+
private _subs = [];
|
|
78
|
+
private _persister: StorageInterface;
|
|
79
|
+
private _merge: (key: K, fromStorage: T, inMemoryValue: T) => T;
|
|
80
|
+
private serialize: (key: K, input: T) => SerializedT;
|
|
81
|
+
private parse: (key: K, value: SerializedT) => T;
|
|
82
|
+
private _saveThrottleMs: number;
|
|
83
|
+
private _idleCallbackMaxWaitMs: number;
|
|
84
|
+
private _nextSave: null | NodeJS.Timeout = null;
|
|
85
|
+
private _nextGc: null | NodeJS.Timeout = null;
|
|
86
|
+
private _pendingSaveKeys: Set<K> = new Set();
|
|
87
|
+
private _loadedKeys: Set<K> = new Set();
|
|
88
|
+
private _loadingKeys: Record<K, Promise<T>>;
|
|
89
|
+
private _objectSize: (serializedObject: SerializedT) => number;
|
|
90
|
+
private _log: Logger;
|
|
91
|
+
|
|
92
|
+
onKeyLoaded: (key: string) => void | null | undefined;
|
|
93
|
+
private _version = 0;
|
|
94
|
+
private _meta: {
|
|
95
|
+
isLoading: boolean;
|
|
96
|
+
onLoadCbs: Array<() => void>;
|
|
97
|
+
value: null | Meta<K>;
|
|
98
|
+
error: null | Error;
|
|
99
|
+
attempts: number;
|
|
100
|
+
loadingPromise?: Promise<Meta<K>> | null | undefined;
|
|
101
|
+
} = {
|
|
102
|
+
isLoading: true,
|
|
103
|
+
onLoadCbs: [],
|
|
104
|
+
value: null,
|
|
105
|
+
error: null,
|
|
106
|
+
attempts: 0,
|
|
107
|
+
};
|
|
108
|
+
private _gcOpts: GCOpts;
|
|
109
|
+
|
|
110
|
+
constructor(opts: Opts<K, T, SerializedT>) {
|
|
111
|
+
this._persister = opts.persister;
|
|
112
|
+
this._merge = opts.merge;
|
|
113
|
+
this.serialize = opts.serialize;
|
|
114
|
+
this.parse = opts.parse;
|
|
115
|
+
this._objectSize = opts.objectSize;
|
|
116
|
+
this._log = opts.logger;
|
|
117
|
+
this._saveThrottleMs = opts.saveThrottleMs ?? 100;
|
|
118
|
+
this._idleCallbackMaxWaitMs = opts.idleCallbackMaxWaitMs ?? 1000;
|
|
119
|
+
this._gcOpts = opts.gc;
|
|
120
|
+
this.currentValue = {} as Record<K, T>;
|
|
121
|
+
this._loadedKeys = new Set();
|
|
122
|
+
this._loadingKeys = {} as Record<K, Promise<T>>;
|
|
123
|
+
this._initMeta();
|
|
124
|
+
if (opts.preloadEntryCount) {
|
|
125
|
+
this._preloadEntries(opts.preloadEntryCount);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private async _initMeta() {
|
|
130
|
+
if (this._meta.loadingPromise) {
|
|
131
|
+
await this._meta.loadingPromise;
|
|
132
|
+
}
|
|
80
133
|
try {
|
|
81
|
-
|
|
134
|
+
const p = this._persister.getItem(META_KEY);
|
|
135
|
+
this._meta.loadingPromise = p;
|
|
136
|
+
const v = await p;
|
|
137
|
+
this._meta.isLoading = false;
|
|
138
|
+
this._meta.error = null;
|
|
139
|
+
this._meta.loadingPromise = null;
|
|
140
|
+
this._meta.attempts = 0;
|
|
141
|
+
const existingObjects = this._meta.value?.objects ?? {};
|
|
142
|
+
const value = v ?? {};
|
|
143
|
+
const objects = value.objects ?? {};
|
|
144
|
+
// Merge the values from storage with in-memory values
|
|
145
|
+
this._meta.value = {
|
|
146
|
+
...value,
|
|
147
|
+
objects: { ...existingObjects, ...objects },
|
|
148
|
+
} as Meta<K>;
|
|
82
149
|
} catch (e) {
|
|
83
|
-
|
|
84
|
-
|
|
150
|
+
this._meta.error = e;
|
|
151
|
+
this._meta.attempts++;
|
|
152
|
+
this._meta.loadingPromise = null;
|
|
85
153
|
}
|
|
86
154
|
}
|
|
87
155
|
|
|
88
|
-
async
|
|
89
|
-
|
|
90
|
-
|
|
156
|
+
private async _getMeta(): Promise<Meta<K>> {
|
|
157
|
+
if (this._meta.value) {
|
|
158
|
+
return this._meta.value;
|
|
159
|
+
}
|
|
160
|
+
if (this._meta.loadingPromise) {
|
|
161
|
+
await this._meta.loadingPromise;
|
|
162
|
+
return this._meta.value;
|
|
163
|
+
}
|
|
164
|
+
this._initMeta();
|
|
165
|
+
await this._meta.loadingPromise;
|
|
166
|
+
return this._meta.value;
|
|
167
|
+
}
|
|
91
168
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
169
|
+
private async _refreshMeta(): Promise<Meta<K>> {
|
|
170
|
+
await this._initMeta();
|
|
171
|
+
return this._meta.value;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private async _preloadEntries(n: number) {
|
|
175
|
+
const meta = await this.waitForMetaToLoad();
|
|
176
|
+
const entries = Object.entries(meta.objects) as Array<[K, ObjectMeta]>;
|
|
177
|
+
entries.sort(([_k_a, a_meta], [_k_b, b_meta]) => {
|
|
178
|
+
return b_meta.updatedAt - a_meta.updatedAt;
|
|
179
|
+
});
|
|
180
|
+
for (const [k] of entries.slice(0, n)) {
|
|
181
|
+
this._loadKey(k);
|
|
95
182
|
}
|
|
96
|
-
|
|
97
|
-
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async _getFromStorage(key: K) {
|
|
186
|
+
try {
|
|
187
|
+
const data = await this._persister.getItem(key);
|
|
188
|
+
if (!data) {
|
|
189
|
+
return data;
|
|
190
|
+
}
|
|
191
|
+
const parsed = this.parse(key, data as SerializedT);
|
|
192
|
+
return parsed;
|
|
193
|
+
} catch (e) {
|
|
194
|
+
console.error(`Unable to read from storage for key=${key}`, e);
|
|
195
|
+
return null;
|
|
98
196
|
}
|
|
99
197
|
}
|
|
100
198
|
|
|
101
|
-
async
|
|
102
|
-
if (
|
|
103
|
-
return;
|
|
199
|
+
public async waitForKeyToLoad(k: K) {
|
|
200
|
+
if (this._loadedKeys.has(k)) {
|
|
201
|
+
return this.currentValue[k];
|
|
104
202
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
});
|
|
108
|
-
await loadedPromise;
|
|
203
|
+
await (this._loadingKeys[k] || this._loadKey(k));
|
|
204
|
+
return this.currentValue[k];
|
|
109
205
|
}
|
|
110
206
|
|
|
111
|
-
|
|
112
|
-
|
|
207
|
+
// Used for tests
|
|
208
|
+
public async waitForMetaToLoad() {
|
|
209
|
+
return this._getMeta();
|
|
113
210
|
}
|
|
114
211
|
|
|
115
|
-
|
|
116
|
-
|
|
212
|
+
// Unloads the key so that it can be garbage collected, but does not
|
|
213
|
+
// delete it. Removes the key from currentValue.
|
|
214
|
+
public unloadKey(k: K) {
|
|
215
|
+
this._loadedKeys.delete(k);
|
|
216
|
+
delete this._loadingKeys[k];
|
|
217
|
+
delete this.currentValue[k];
|
|
117
218
|
}
|
|
118
219
|
|
|
119
|
-
async
|
|
120
|
-
if (
|
|
121
|
-
|
|
220
|
+
private async _loadKey(k: K) {
|
|
221
|
+
if (this._loadedKeys.has(k) || k in this._loadingKeys) return;
|
|
222
|
+
const p = this._getFromStorage(k);
|
|
223
|
+
this._loadingKeys[k] = p;
|
|
224
|
+
const value = await p;
|
|
225
|
+
delete this._loadingKeys[k];
|
|
226
|
+
this._loadedKeys.add(k);
|
|
227
|
+
|
|
228
|
+
if (value) {
|
|
229
|
+
const merged = this._merge(k, value, this.currentValue[k]);
|
|
230
|
+
if (merged) {
|
|
231
|
+
this.currentValue[k] = merged;
|
|
232
|
+
}
|
|
122
233
|
}
|
|
123
|
-
|
|
124
|
-
this._pendingSaveCbs.push(resolve);
|
|
125
|
-
});
|
|
126
|
-
await syncedPromise;
|
|
234
|
+
this.onKeyLoaded && this.onKeyLoaded(k);
|
|
127
235
|
}
|
|
128
236
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
237
|
+
// Returns a promise with a number so that we can wait for flush
|
|
238
|
+
// to finish in the tests. The number is the number of operations
|
|
239
|
+
// it performed, but it's mostly there so that typescript will warn
|
|
240
|
+
// us if we forget to retun the promise from the function.
|
|
241
|
+
private _writeToStorage(opts?: {
|
|
242
|
+
skipGc?: boolean | null | undefined;
|
|
243
|
+
attempts?: number | null | undefined;
|
|
244
|
+
}): Promise<number> {
|
|
245
|
+
const promises: Promise<number>[] = [];
|
|
246
|
+
const skipGc = opts?.skipGc;
|
|
247
|
+
if (this._meta.isLoading) {
|
|
248
|
+
// Wait for meta to load and try again, give it a delay so that
|
|
249
|
+
// we don't spend too much time retrying
|
|
250
|
+
const p: Promise<number> = new Promise((resolve, reject) => {
|
|
251
|
+
setTimeout(
|
|
252
|
+
() =>
|
|
253
|
+
this._enqueuePersist(
|
|
254
|
+
opts
|
|
255
|
+
? { ...opts, attempts: (opts.attempts || 0) + 1 }
|
|
256
|
+
: { attempts: 1 },
|
|
257
|
+
)
|
|
258
|
+
.then(resolve)
|
|
259
|
+
.catch(reject),
|
|
260
|
+
10 + (opts?.attempts ?? 0) * 1000,
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
promises.push(p);
|
|
264
|
+
return Promise.all(promises).then((vs) =>
|
|
265
|
+
vs.reduce((acc, x) => acc + x, 0),
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
const metaValue = this._meta.value;
|
|
269
|
+
if (!metaValue) {
|
|
270
|
+
// If it's not loading and we don't have the data, then there
|
|
271
|
+
// must be an error and we're not going to be able to save until
|
|
272
|
+
// the error is resolved elsewhere.
|
|
273
|
+
return Promise.resolve(0);
|
|
274
|
+
}
|
|
275
|
+
const keysToDelete = [];
|
|
276
|
+
const keysToUpdate = [];
|
|
277
|
+
for (const k of this._pendingSaveKeys) {
|
|
278
|
+
if (!(k in this.currentValue)) {
|
|
279
|
+
keysToDelete.push(k);
|
|
280
|
+
delete metaValue.objects[k];
|
|
281
|
+
} else {
|
|
282
|
+
keysToUpdate.push(k);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (const k of keysToDelete) {
|
|
287
|
+
const p = this._persister.removeItem(k);
|
|
288
|
+
promises.push(p.then(() => 1));
|
|
289
|
+
this._loadedKeys.delete(k);
|
|
290
|
+
this._pendingSaveKeys.delete(k);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const keysToLoad = [];
|
|
294
|
+
|
|
295
|
+
const kvPairs: Array<[string, any]> = [[META_KEY, metaValue]];
|
|
296
|
+
const metaObjects: Meta<K>['objects'] =
|
|
297
|
+
metaValue.objects ?? ({} as Meta<K>['objects']);
|
|
298
|
+
metaValue.objects = metaObjects;
|
|
299
|
+
for (const k of keysToUpdate) {
|
|
300
|
+
if (this._loadedKeys.has(k)) {
|
|
301
|
+
const serializedV = this.serialize(k, this.currentValue[k]);
|
|
302
|
+
kvPairs.push([k, serializedV]);
|
|
303
|
+
const size = this._objectSize(serializedV);
|
|
304
|
+
const m = metaObjects[k] ?? {
|
|
305
|
+
createdAt: Date.now(),
|
|
306
|
+
updatedAt: Date.now(),
|
|
307
|
+
size,
|
|
308
|
+
};
|
|
309
|
+
m.updatedAt = Date.now();
|
|
310
|
+
m.size = size;
|
|
311
|
+
metaObjects[k] = m;
|
|
312
|
+
this._pendingSaveKeys.delete(k);
|
|
313
|
+
} else {
|
|
314
|
+
keysToLoad.push(k);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const p = this._persister.multiSet(kvPairs);
|
|
319
|
+
promises.push(p.then(() => 1));
|
|
320
|
+
|
|
321
|
+
// For the keys that haven't loaded, load the key then try
|
|
322
|
+
// persisting again. We don't want to do any async work here
|
|
323
|
+
// or else we might end up saving older copies of the data to
|
|
324
|
+
// the store.
|
|
325
|
+
for (const k of keysToLoad) {
|
|
326
|
+
const p = this._loadKey(k).then(() => this._enqueuePersist(opts));
|
|
327
|
+
promises.push(p);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!skipGc) {
|
|
331
|
+
this.gc();
|
|
133
332
|
}
|
|
134
|
-
|
|
333
|
+
|
|
334
|
+
return Promise.all(promises).then((vs) => {
|
|
335
|
+
return vs.reduce((acc, x) => acc + x, 0);
|
|
336
|
+
});
|
|
135
337
|
}
|
|
136
338
|
|
|
137
339
|
async flush() {
|
|
@@ -139,39 +341,208 @@ export class PersistedObject<T> {
|
|
|
139
341
|
return;
|
|
140
342
|
}
|
|
141
343
|
clearTimeout(this._nextSave);
|
|
142
|
-
this.
|
|
344
|
+
this._nextSave = null;
|
|
345
|
+
const p = this._writeToStorage();
|
|
346
|
+
return p;
|
|
143
347
|
}
|
|
144
348
|
|
|
145
|
-
|
|
146
|
-
if (this.
|
|
147
|
-
|
|
148
|
-
|
|
349
|
+
private async _gc() {
|
|
350
|
+
if (!this._gcOpts) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const keys = new Set(await this._persister.getAllKeys());
|
|
354
|
+
keys.delete(META_KEY);
|
|
355
|
+
|
|
356
|
+
// Keys we can't delete
|
|
357
|
+
const sacredKeys = new Set(Object.keys(this.currentValue));
|
|
358
|
+
for (const k of Object.keys(this._loadingKeys)) {
|
|
359
|
+
sacredKeys.add(k);
|
|
360
|
+
}
|
|
361
|
+
for (const k of this._loadedKeys) {
|
|
362
|
+
sacredKeys.add(k);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Refresh meta from the store so that we're less likely to
|
|
366
|
+
// clobber data from other tabs
|
|
367
|
+
const meta = await this._refreshMeta();
|
|
368
|
+
if (!meta) {
|
|
369
|
+
this._log.info('Could not gc because we were not able to load meta');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const promises = [];
|
|
374
|
+
|
|
375
|
+
const deets = {
|
|
376
|
+
gcOpts: this._gcOpts,
|
|
377
|
+
keys,
|
|
378
|
+
sacredKeys,
|
|
379
|
+
removed: [],
|
|
380
|
+
metaRemoved: [],
|
|
381
|
+
removedMissingCount: 0,
|
|
382
|
+
removedOldCount: 0,
|
|
383
|
+
removedThresholdCount: 0,
|
|
384
|
+
removedSizeCount: 0,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// First, remove all keys we don't know about
|
|
388
|
+
for (const key of keys) {
|
|
389
|
+
if (sacredKeys.has(key) || key in meta.objects) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
this._log.info('Lost track of key in meta', key);
|
|
393
|
+
promises.push(this._persister.removeItem(key));
|
|
394
|
+
deets.removed.push(key);
|
|
395
|
+
deets.removedMissingCount++;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Remove anything over the max age
|
|
399
|
+
const now = Date.now();
|
|
400
|
+
for (const [k, m] of Object.entries(meta.objects)) {
|
|
401
|
+
if (
|
|
402
|
+
!sacredKeys.has(k) &&
|
|
403
|
+
(m as ObjectMeta).updatedAt < now - this._gcOpts.maxAgeMs
|
|
404
|
+
) {
|
|
405
|
+
promises.push(this._persister.removeItem(k));
|
|
406
|
+
delete meta.objects[k];
|
|
407
|
+
deets.removed.push(k);
|
|
408
|
+
deets.removedOldCount++;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Keep queries under max queries
|
|
413
|
+
const maxEntries = Object.entries(meta.objects) as Array<[K, ObjectMeta]>;
|
|
414
|
+
maxEntries.sort(([_k_a, a_meta], [_k_b, b_meta]) => {
|
|
415
|
+
return a_meta.updatedAt - b_meta.updatedAt;
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const deletableMaxEntries = maxEntries.filter(([x]) => !sacredKeys.has(x));
|
|
419
|
+
if (maxEntries.length > this._gcOpts.maxEntries) {
|
|
420
|
+
for (const [k] of deletableMaxEntries.slice(
|
|
421
|
+
0,
|
|
422
|
+
maxEntries.length - this._gcOpts.maxEntries,
|
|
423
|
+
)) {
|
|
424
|
+
promises.push(this._persister.removeItem(k));
|
|
425
|
+
delete meta.objects[k];
|
|
426
|
+
deets.removed.push(k);
|
|
427
|
+
deets.removedThresholdCount++;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Remove oldest entries until we are under max size
|
|
432
|
+
const delEntries = Object.entries(meta.objects) as Array<[K, ObjectMeta]>;
|
|
433
|
+
delEntries.sort(([_k_a, a_meta], [_k_b, b_meta]) => {
|
|
434
|
+
return a_meta.updatedAt - b_meta.updatedAt;
|
|
435
|
+
});
|
|
436
|
+
const deletableDelEntries = delEntries.filter(([x]) => !sacredKeys.has(x));
|
|
437
|
+
|
|
438
|
+
let currentSize = delEntries.reduce((acc, [_k, m]) => {
|
|
439
|
+
return acc + m.size;
|
|
440
|
+
}, 0);
|
|
441
|
+
|
|
442
|
+
while (
|
|
443
|
+
currentSize > 0 &&
|
|
444
|
+
currentSize > this._gcOpts.maxSize &&
|
|
445
|
+
deletableDelEntries.length
|
|
446
|
+
) {
|
|
447
|
+
const [[k, m]] = deletableDelEntries.splice(0, 1);
|
|
448
|
+
currentSize -= m.size;
|
|
449
|
+
promises.push(this._persister.removeItem(k));
|
|
450
|
+
delete meta.objects[k];
|
|
451
|
+
deets.removed.push(k);
|
|
452
|
+
deets.removedSizeCount++;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Update meta to remove keys that are no longer in the store
|
|
456
|
+
for (const k of Object.keys(meta.objects)) {
|
|
457
|
+
if (!keys.has(k) && !sacredKeys.has(k)) {
|
|
458
|
+
delete meta.objects[k];
|
|
149
459
|
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (deets.removed.length || deets.metaRemoved.length) {
|
|
463
|
+
// Trigger a flush of the meta
|
|
464
|
+
promises.push(this._enqueuePersist({ skipGc: true }));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
this._log.info('Completed GC', deets);
|
|
468
|
+
|
|
469
|
+
await Promise.all(promises);
|
|
470
|
+
return deets;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Schedules a GC to run in one minute (unless it is already scheduled)
|
|
474
|
+
gc() {
|
|
475
|
+
if (this._nextGc) {
|
|
150
476
|
return;
|
|
151
477
|
}
|
|
152
|
-
this.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
478
|
+
this._nextGc = setTimeout(
|
|
479
|
+
() => {
|
|
480
|
+
safeIdleCallback(() => {
|
|
481
|
+
this._nextGc = null;
|
|
482
|
+
this._gc();
|
|
483
|
+
}, 30 * 1000);
|
|
484
|
+
},
|
|
485
|
+
// 1 minute + some jitter to keep multiple tabs from running at same time
|
|
486
|
+
1000 * 60 + Math.random() * 500,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private _enqueuePersist(opts?: {
|
|
491
|
+
skipGc?: boolean | null | undefined;
|
|
492
|
+
attempts?: number | null | undefined;
|
|
493
|
+
}): Promise<number> {
|
|
494
|
+
return new Promise((resolve, reject) => {
|
|
495
|
+
if (this._nextSave) {
|
|
496
|
+
resolve(0);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
this._nextSave = setTimeout(() => {
|
|
501
|
+
safeIdleCallback(() => {
|
|
502
|
+
this._nextSave = null;
|
|
503
|
+
this._writeToStorage(opts).then(resolve).catch(reject);
|
|
504
|
+
}, this._idleCallbackMaxWaitMs);
|
|
505
|
+
}, this._saveThrottleMs);
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
version() {
|
|
510
|
+
return this._version;
|
|
158
511
|
}
|
|
159
512
|
|
|
160
|
-
|
|
513
|
+
// Takes a function that updates the store in place.
|
|
514
|
+
// Uses `mutative` to get a list of keys that were changed
|
|
515
|
+
// so that we know which entries we need to persist to the store.
|
|
516
|
+
public updateInPlace(f: (prev: Record<string, T>) => void) {
|
|
161
517
|
this._version++;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
518
|
+
const [state, patches] = create(this.currentValue, f, {
|
|
519
|
+
enablePatches: true,
|
|
520
|
+
});
|
|
521
|
+
for (const patch of patches) {
|
|
522
|
+
const k = patch.path[0];
|
|
523
|
+
if (k && typeof k === 'string') {
|
|
524
|
+
this._pendingSaveKeys.add(k as K);
|
|
525
|
+
if (!this._loadedKeys.has(k as K)) {
|
|
526
|
+
this._loadKey(k as K);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
for (const k of Object.keys(state)) {
|
|
532
|
+
if (!state[k]) {
|
|
533
|
+
debugger;
|
|
534
|
+
}
|
|
167
535
|
}
|
|
168
|
-
|
|
169
|
-
|
|
536
|
+
|
|
537
|
+
this.currentValue = state;
|
|
538
|
+
this._enqueuePersist();
|
|
539
|
+
for (const cb of this._subs) {
|
|
540
|
+
cb(this.currentValue);
|
|
170
541
|
}
|
|
171
|
-
return
|
|
542
|
+
return state;
|
|
172
543
|
}
|
|
173
544
|
|
|
174
|
-
subscribe(cb) {
|
|
545
|
+
public subscribe(cb: (value: Record<K, T>) => void) {
|
|
175
546
|
this._subs.push(cb);
|
|
176
547
|
cb(this.currentValue);
|
|
177
548
|
|
package/src/InMemoryStorage.js
DELETED