@signaldb/core 2.0.0-beta.21 → 2.0.0-beta.22
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 +3 -3
- package/dist/.vite/manifest.json +61 -59
- package/dist/AsyncDataAdapter.d.ts +15 -2
- package/dist/AutoFetchDataAdapter.d.ts +13 -2
- package/dist/Collection/Observer.d.ts +1 -1
- package/dist/Collection/index.d.ts +11 -12
- package/dist/DataAdapter.d.ts +19 -3
- package/dist/WorkerDataAdapterHost.d.ts +19 -6
- package/dist/index.cjs.js +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +4 -4
- package/dist/index12.cjs.js +1 -1
- package/dist/index12.mjs +1 -1
- package/dist/index15.cjs.js +6 -2
- package/dist/index15.mjs +6 -2
- package/dist/index17.cjs.js +4 -2
- package/dist/index17.mjs +4 -3
- package/dist/index18.cjs.js +1 -0
- package/dist/index18.mjs +1 -1
- package/dist/index28.cjs.js +15 -5
- package/dist/index28.mjs +15 -5
- package/dist/index29.cjs.js +29 -16
- package/dist/index29.mjs +29 -16
- package/dist/index33.cjs.js +17 -49
- package/dist/index33.mjs +17 -49
- package/dist/index34.cjs.js +45 -445
- package/dist/index34.mjs +45 -445
- package/dist/index35.cjs.js +132 -65
- package/dist/index35.mjs +132 -65
- package/dist/index36.cjs.js +385 -531
- package/dist/index36.mjs +386 -533
- package/dist/index37.cjs.js +68 -17
- package/dist/index37.mjs +68 -17
- package/dist/index38.cjs.js +536 -319
- package/dist/index38.mjs +538 -321
- package/dist/index39.cjs.js +301 -466
- package/dist/index39.mjs +301 -466
- package/dist/index40.cjs.js +484 -0
- package/dist/index40.mjs +484 -0
- package/dist/index5.cjs.js +3 -3
- package/dist/index5.mjs +3 -3
- package/dist/index6.cjs.js +1 -0
- package/dist/index6.mjs +1 -1
- package/dist/types/StorageAdapter.d.ts +80 -0
- package/dist/utils/executeStorageQuery.d.ts +28 -0
- package/package.json +1 -1
package/dist/index40.mjs
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import isEqual from "./index2.mjs";
|
|
2
|
+
import deepClone from "./index18.mjs";
|
|
3
|
+
import match from "./index19.mjs";
|
|
4
|
+
import modify from "./index20.mjs";
|
|
5
|
+
import queryId from "./index26.mjs";
|
|
6
|
+
import executeStorageQuery from "./index35.mjs";
|
|
7
|
+
//#region src/AutoFetchDataAdapter.ts
|
|
8
|
+
/**
|
|
9
|
+
* Default merge strategy: shallow spread (right wins)
|
|
10
|
+
* @param a first item
|
|
11
|
+
* @param b second item
|
|
12
|
+
* @returns merged item
|
|
13
|
+
*/
|
|
14
|
+
function defaultMergeItems(a, b) {
|
|
15
|
+
return {
|
|
16
|
+
...a,
|
|
17
|
+
...b
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Generates a stable key for a selector
|
|
22
|
+
* @param selector - the selector
|
|
23
|
+
* @returns the key
|
|
24
|
+
*/
|
|
25
|
+
function selectorId(selector) {
|
|
26
|
+
return JSON.stringify(selector ?? {});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* AutoFetchDataAdapter
|
|
30
|
+
*
|
|
31
|
+
* A DataAdapter that:
|
|
32
|
+
* - Mirrors the CollectionBackend surface (CRUD + query registry + lifecycle)
|
|
33
|
+
* - Executes queries against a provided StorageAdapter (local cache)
|
|
34
|
+
* - On first registration of a selector, auto-fetches from a remote source and
|
|
35
|
+
* ingests the result into storage (upsert), then pushes query result updates
|
|
36
|
+
* - Optionally purges auto-fetched items for a selector once no observers remain
|
|
37
|
+
* - Can subscribe to remote change notifications to re-fetch active selectors
|
|
38
|
+
*
|
|
39
|
+
* IMPORTANT: Purging only ever deletes items that were introduced via the
|
|
40
|
+
* auto-fetch path and are no longer referenced by any active selector. Items
|
|
41
|
+
* inserted through CRUD calls are never purged.
|
|
42
|
+
*/
|
|
43
|
+
var AutoFetchDataAdapter = class {
|
|
44
|
+
options;
|
|
45
|
+
id;
|
|
46
|
+
onError;
|
|
47
|
+
fetchQueryItems;
|
|
48
|
+
mergeItems;
|
|
49
|
+
purgeDelay;
|
|
50
|
+
storageAdapters = /* @__PURE__ */ new Map();
|
|
51
|
+
storageAdapterReady = /* @__PURE__ */ new Map();
|
|
52
|
+
collectionIndices = /* @__PURE__ */ new Map();
|
|
53
|
+
queries = /* @__PURE__ */ new Map();
|
|
54
|
+
activeObservers = /* @__PURE__ */ new Map();
|
|
55
|
+
observerTimeouts = /* @__PURE__ */ new Map();
|
|
56
|
+
selectorIds = /* @__PURE__ */ new Map();
|
|
57
|
+
idRefCounts = /* @__PURE__ */ new Map();
|
|
58
|
+
autoloadIds = /* @__PURE__ */ new Map();
|
|
59
|
+
constructor(options) {
|
|
60
|
+
this.options = options;
|
|
61
|
+
this.id = options.id || "autofetch-data-adapter";
|
|
62
|
+
this.onError = options.onError ?? ((error) => {
|
|
63
|
+
console.error(error);
|
|
64
|
+
});
|
|
65
|
+
this.fetchQueryItems = options.fetchQueryItems;
|
|
66
|
+
this.mergeItems = options.mergeItems ?? defaultMergeItems;
|
|
67
|
+
this.purgeDelay = options.purgeDelay ?? 1e4;
|
|
68
|
+
if (options.registerRemoteChange) options.registerRemoteChange(async () => {
|
|
69
|
+
await this.forceRefetchAll();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
createCollectionBackend(collection, indices) {
|
|
73
|
+
this.collectionIndices.set(collection.name, indices);
|
|
74
|
+
this.queries.set(collection.name, /* @__PURE__ */ new Map());
|
|
75
|
+
this.ensureStorageAdapter(collection.name);
|
|
76
|
+
this.activeObservers.set(collection.name, /* @__PURE__ */ new Map());
|
|
77
|
+
this.observerTimeouts.set(collection.name, /* @__PURE__ */ new Map());
|
|
78
|
+
this.selectorIds.set(collection.name, /* @__PURE__ */ new Map());
|
|
79
|
+
this.idRefCounts.set(collection.name, /* @__PURE__ */ new Map());
|
|
80
|
+
this.autoloadIds.set(collection.name, /* @__PURE__ */ new Set());
|
|
81
|
+
const ready = this.setupStorage(collection.name, indices);
|
|
82
|
+
this.storageAdapterReady.set(collection.name, ready);
|
|
83
|
+
const registerQuery = (selector, options) => {
|
|
84
|
+
const qid = queryId(selector, options);
|
|
85
|
+
const registry = this.queries.get(collection.name);
|
|
86
|
+
if (!registry) throw new Error(`Collection ${collection.name} not initialized!`);
|
|
87
|
+
registry.set(qid, {
|
|
88
|
+
selector,
|
|
89
|
+
options,
|
|
90
|
+
items: [],
|
|
91
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
92
|
+
...registry.get(qid),
|
|
93
|
+
state: "active",
|
|
94
|
+
error: null
|
|
95
|
+
});
|
|
96
|
+
const key = selectorId(selector);
|
|
97
|
+
const perColObservers = this.activeObservers.get(collection.name);
|
|
98
|
+
const current = perColObservers?.get(key)?.count ?? 0;
|
|
99
|
+
perColObservers?.set(key, {
|
|
100
|
+
selector,
|
|
101
|
+
count: current + 1
|
|
102
|
+
});
|
|
103
|
+
const t = this.observerTimeouts.get(collection.name)?.get(key);
|
|
104
|
+
if (t) clearTimeout(t);
|
|
105
|
+
if (current === 0) this.fetchAndIngest(collection.name, selector).catch(this.onError);
|
|
106
|
+
this.fulfillQuery(collection.name, selector, options).catch(this.onError);
|
|
107
|
+
};
|
|
108
|
+
const unregisterQuery = (selector, options) => {
|
|
109
|
+
const qid = queryId(selector, options);
|
|
110
|
+
this.queries.get(collection.name)?.delete(qid);
|
|
111
|
+
const key = selectorId(selector);
|
|
112
|
+
const perColObservers = this.activeObservers.get(collection.name);
|
|
113
|
+
const current = perColObservers?.get(key)?.count ?? 0;
|
|
114
|
+
const remaining = Math.max(0, current - 1);
|
|
115
|
+
if (remaining > 0) {
|
|
116
|
+
perColObservers?.set(key, {
|
|
117
|
+
selector,
|
|
118
|
+
count: remaining
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const doPurge = () => {
|
|
123
|
+
perColObservers?.delete(key);
|
|
124
|
+
this.purgeSelector(collection.name, selector).catch(this.onError);
|
|
125
|
+
};
|
|
126
|
+
if (this.purgeDelay === 0) doPurge();
|
|
127
|
+
else {
|
|
128
|
+
const timeouts = this.observerTimeouts.get(collection.name);
|
|
129
|
+
const t = timeouts?.get(key);
|
|
130
|
+
if (t) clearTimeout(t);
|
|
131
|
+
timeouts?.set(key, setTimeout(doPurge, this.purgeDelay));
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const getQueryState = (selector, options) => {
|
|
135
|
+
return (this.queries.get(collection.name)?.get(queryId(selector, options)))?.state ?? "active";
|
|
136
|
+
};
|
|
137
|
+
const getQueryError = (selector, options) => {
|
|
138
|
+
return (this.queries.get(collection.name)?.get(queryId(selector, options)))?.error ?? null;
|
|
139
|
+
};
|
|
140
|
+
const getQueryResult = (selector, options) => {
|
|
141
|
+
return (this.queries.get(collection.name)?.get(queryId(selector, options)))?.items ?? [];
|
|
142
|
+
};
|
|
143
|
+
const onQueryStateChange = (selector, options, callback) => {
|
|
144
|
+
const qid = queryId(selector, options);
|
|
145
|
+
const registry = this.queries.get(collection.name);
|
|
146
|
+
if (!registry) throw new Error(`Collection ${collection.name} not initialized!`);
|
|
147
|
+
if (!registry.has(qid)) registry.set(qid, {
|
|
148
|
+
selector,
|
|
149
|
+
options,
|
|
150
|
+
state: "active",
|
|
151
|
+
error: null,
|
|
152
|
+
items: [],
|
|
153
|
+
listeners: /* @__PURE__ */ new Set()
|
|
154
|
+
});
|
|
155
|
+
registry.get(qid)?.listeners.add(callback);
|
|
156
|
+
return () => {
|
|
157
|
+
registry.get(qid)?.listeners.delete(callback);
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
insert: async (item) => {
|
|
162
|
+
await ready;
|
|
163
|
+
return await this.insert(collection.name, item);
|
|
164
|
+
},
|
|
165
|
+
updateOne: async (selector, modifier) => {
|
|
166
|
+
await ready;
|
|
167
|
+
return this.updateOne(collection.name, selector, modifier);
|
|
168
|
+
},
|
|
169
|
+
updateMany: async (selector, modifier) => {
|
|
170
|
+
await ready;
|
|
171
|
+
return this.updateMany(collection.name, selector, modifier);
|
|
172
|
+
},
|
|
173
|
+
replaceOne: async (selector, replacement) => {
|
|
174
|
+
await ready;
|
|
175
|
+
return this.replaceOne(collection.name, selector, replacement);
|
|
176
|
+
},
|
|
177
|
+
removeOne: async (selector) => {
|
|
178
|
+
await ready;
|
|
179
|
+
return this.removeOne(collection.name, selector);
|
|
180
|
+
},
|
|
181
|
+
removeMany: async (selector) => {
|
|
182
|
+
await ready;
|
|
183
|
+
return this.removeMany(collection.name, selector);
|
|
184
|
+
},
|
|
185
|
+
registerQuery,
|
|
186
|
+
unregisterQuery,
|
|
187
|
+
getQueryState,
|
|
188
|
+
getQueryError,
|
|
189
|
+
getQueryResult,
|
|
190
|
+
onQueryStateChange,
|
|
191
|
+
executeQuery: async (selector, options) => {
|
|
192
|
+
await ready;
|
|
193
|
+
registerQuery(selector, options);
|
|
194
|
+
await new Promise((resolve) => {
|
|
195
|
+
let stop = () => {};
|
|
196
|
+
stop = onQueryStateChange(selector, options, (state) => {
|
|
197
|
+
if (state === "active") return;
|
|
198
|
+
resolve();
|
|
199
|
+
stop();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
const result = getQueryResult(selector, options);
|
|
203
|
+
unregisterQuery(selector, options);
|
|
204
|
+
return result;
|
|
205
|
+
},
|
|
206
|
+
dispose: async () => {
|
|
207
|
+
this.storageAdapters.delete(collection.name);
|
|
208
|
+
this.queries.delete(collection.name);
|
|
209
|
+
this.collectionIndices.delete(collection.name);
|
|
210
|
+
this.storageAdapterReady.delete(collection.name);
|
|
211
|
+
this.activeObservers.delete(collection.name);
|
|
212
|
+
this.observerTimeouts.delete(collection.name);
|
|
213
|
+
this.selectorIds.delete(collection.name);
|
|
214
|
+
this.idRefCounts.delete(collection.name);
|
|
215
|
+
this.autoloadIds.delete(collection.name);
|
|
216
|
+
},
|
|
217
|
+
isReady: async () => {
|
|
218
|
+
await ready;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
async forceRefetchAll() {
|
|
223
|
+
const tasks = [];
|
|
224
|
+
for (const [collectionName, observers] of this.activeObservers.entries()) for (const { selector, count } of observers.values()) if (count > 0) tasks.push(this.fetchAndIngest(collectionName, selector));
|
|
225
|
+
await Promise.all(tasks);
|
|
226
|
+
}
|
|
227
|
+
async fetchAndIngest(collectionName, selector) {
|
|
228
|
+
this.publishForSelector(collectionName, selector, "active", null);
|
|
229
|
+
try {
|
|
230
|
+
const items = await this.fetchQueryItems(collectionName, selector);
|
|
231
|
+
if (!items || !Array.isArray(items)) throw new Error("AutoFetchDataAdapter: fetchQueryItems must resolve to { items: T[] }");
|
|
232
|
+
const ids = items.map((i) => i.id);
|
|
233
|
+
const selectorKey = selectorId(selector);
|
|
234
|
+
const selMap = this.selectorIds.get(collectionName);
|
|
235
|
+
const previous = selMap?.get(selectorKey) ?? /* @__PURE__ */ new Set();
|
|
236
|
+
ids.forEach((id) => previous.add(id));
|
|
237
|
+
selMap?.set(selectorKey, previous);
|
|
238
|
+
await this.upsertMerged(collectionName, items);
|
|
239
|
+
await this.checkQueryUpdates(collectionName, items);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
this.publishForSelector(collectionName, selector, "error", error);
|
|
242
|
+
this.onError(error);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async purgeSelector(collectionName, selector) {
|
|
246
|
+
const selectorKey = selectorId(selector);
|
|
247
|
+
const selMap = this.selectorIds.get(collectionName);
|
|
248
|
+
const ids = selMap?.get(selectorKey);
|
|
249
|
+
selMap?.delete(selectorKey);
|
|
250
|
+
if (!ids || ids.size === 0) return;
|
|
251
|
+
const referenceMap = this.idRefCounts.get(collectionName);
|
|
252
|
+
const autoload = this.autoloadIds.get(collectionName);
|
|
253
|
+
const toRemove = [];
|
|
254
|
+
for (const id of ids) {
|
|
255
|
+
const current = referenceMap?.get(id) ?? 0;
|
|
256
|
+
const next = Math.max(0, current - 1);
|
|
257
|
+
if (next === 0) {
|
|
258
|
+
referenceMap?.delete(id);
|
|
259
|
+
if (autoload?.has(id)) toRemove.push(id);
|
|
260
|
+
} else referenceMap?.set(id, next);
|
|
261
|
+
}
|
|
262
|
+
if (toRemove.length === 0) return;
|
|
263
|
+
const storage = this.storageAdapters.get(collectionName);
|
|
264
|
+
if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
|
|
265
|
+
const items = await storage.readIds(toRemove);
|
|
266
|
+
if (items.length > 0) {
|
|
267
|
+
await storage.remove(items);
|
|
268
|
+
await this.checkQueryUpdates(collectionName, items);
|
|
269
|
+
}
|
|
270
|
+
toRemove.forEach((id) => autoload?.delete(id));
|
|
271
|
+
}
|
|
272
|
+
async setupStorage(collectionName, indices) {
|
|
273
|
+
const storage = this.storageAdapters.get(collectionName);
|
|
274
|
+
if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
|
|
275
|
+
await storage.setup();
|
|
276
|
+
await Promise.all(indices.map((field) => storage.createIndex(field)));
|
|
277
|
+
}
|
|
278
|
+
ensureStorageAdapter(name) {
|
|
279
|
+
if (this.storageAdapters.has(name)) return;
|
|
280
|
+
const adapter = this.options.storage && this.options.storage(name);
|
|
281
|
+
if (!adapter) return;
|
|
282
|
+
this.storageAdapters.set(name, adapter);
|
|
283
|
+
}
|
|
284
|
+
publishForSelector(collectionName, selector, state, error) {
|
|
285
|
+
const registry = this.queries.get(collectionName);
|
|
286
|
+
if (!registry) return;
|
|
287
|
+
for (const query of registry.values()) {
|
|
288
|
+
if (!isEqual(query.selector, selector)) continue;
|
|
289
|
+
this.publishState(collectionName, queryId(query.selector, query.options), state, error);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
publishState(collectionName, qid, state, error) {
|
|
293
|
+
const query = this.queries.get(collectionName)?.get(qid);
|
|
294
|
+
if (!query) return;
|
|
295
|
+
query.state = state;
|
|
296
|
+
query.error = error;
|
|
297
|
+
for (const callback of query.listeners) try {
|
|
298
|
+
callback(state);
|
|
299
|
+
} catch (error_) {
|
|
300
|
+
this.onError(error_);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
publishResult(collectionName, qid, items) {
|
|
304
|
+
const query = this.queries.get(collectionName)?.get(qid);
|
|
305
|
+
if (!query) return;
|
|
306
|
+
query.items = items;
|
|
307
|
+
this.queries.get(collectionName)?.set(qid, query);
|
|
308
|
+
}
|
|
309
|
+
async fulfillQuery(collectionName, selector, options) {
|
|
310
|
+
const qid = queryId(selector, options);
|
|
311
|
+
const registry = this.queries.get(collectionName);
|
|
312
|
+
if (!registry) throw new Error(`Collection ${collectionName} not initialized!`);
|
|
313
|
+
if (!registry.get(qid)) return;
|
|
314
|
+
this.publishState(collectionName, qid, "active", null);
|
|
315
|
+
try {
|
|
316
|
+
const items = await this.executeQuery(collectionName, selector, options);
|
|
317
|
+
this.publishResult(collectionName, qid, items);
|
|
318
|
+
this.publishState(collectionName, qid, "complete", null);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
this.publishState(collectionName, qid, "error", error);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Reads one query's result from the local storage adapter.
|
|
325
|
+
*
|
|
326
|
+
* Shares `executeStorageQuery` with the other adapters — the projection this
|
|
327
|
+
* used to do by hand was `projectItems` spelled out, so nothing about the
|
|
328
|
+
* result changes.
|
|
329
|
+
* @template T - The type of the items.
|
|
330
|
+
* @template I - The type of the item ids.
|
|
331
|
+
* @param collectionName - The collection to read from.
|
|
332
|
+
* @param selector - The query's selector.
|
|
333
|
+
* @param options - The query's sort, window and projection.
|
|
334
|
+
* @returns The query result.
|
|
335
|
+
*/
|
|
336
|
+
async executeQuery(collectionName, selector, options) {
|
|
337
|
+
const storage = this.storageAdapters.get(collectionName);
|
|
338
|
+
if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
|
|
339
|
+
return executeStorageQuery(storage, this.collectionIndices.get(collectionName) ?? [], selector, options);
|
|
340
|
+
}
|
|
341
|
+
async checkQueryUpdates(collectionName, changedItems) {
|
|
342
|
+
const registry = this.queries.get(collectionName);
|
|
343
|
+
if (!registry) throw new Error(`Collection ${collectionName} not initialized!`);
|
|
344
|
+
if (registry.size === 0) return;
|
|
345
|
+
const affected = [...registry.values()].filter(({ selector }) => changedItems.some((item) => match(item, selector)));
|
|
346
|
+
if (affected.length === 0) return;
|
|
347
|
+
for (const { selector, options } of affected) {
|
|
348
|
+
const qid = queryId(selector, options);
|
|
349
|
+
this.publishState(collectionName, qid, "active", null);
|
|
350
|
+
}
|
|
351
|
+
await Promise.all(affected.map(async ({ selector, options }) => {
|
|
352
|
+
const qid = queryId(selector, options);
|
|
353
|
+
try {
|
|
354
|
+
const items = await this.executeQuery(collectionName, selector, options);
|
|
355
|
+
this.publishResult(collectionName, qid, items);
|
|
356
|
+
this.publishState(collectionName, qid, "complete", null);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
this.publishState(collectionName, qid, "error", error);
|
|
359
|
+
}
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
async insert(collectionName, newItem) {
|
|
363
|
+
const storage = this.storageAdapters.get(collectionName);
|
|
364
|
+
if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
|
|
365
|
+
if ((await this.executeQuery(collectionName, { id: newItem.id }, { limit: 1 })).length > 0) throw new Error(`Item with id ${String(newItem.id)} already exists`);
|
|
366
|
+
await storage.insert([newItem]);
|
|
367
|
+
await this.checkQueryUpdates(collectionName, [newItem]);
|
|
368
|
+
return newItem;
|
|
369
|
+
}
|
|
370
|
+
async updateOne(collectionName, selector, modifier) {
|
|
371
|
+
const storage = this.storageAdapters.get(collectionName);
|
|
372
|
+
if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
|
|
373
|
+
const [item] = await this.executeQuery(collectionName, selector, { limit: 1 });
|
|
374
|
+
const { $setOnInsert, ...rest } = modifier;
|
|
375
|
+
if (item == null) return [];
|
|
376
|
+
const modified = modify(deepClone(item), rest);
|
|
377
|
+
if (item.id !== modified.id) {
|
|
378
|
+
if ((await this.executeQuery(collectionName, { id: modified.id }, { limit: 1 })).length > 0) throw new Error(`Item with id ${String(modified.id)} already exists`);
|
|
379
|
+
}
|
|
380
|
+
await storage.replace([modified]);
|
|
381
|
+
await this.checkQueryUpdates(collectionName, [item, modified]);
|
|
382
|
+
return {
|
|
383
|
+
items: [modified],
|
|
384
|
+
previousItems: [item]
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
async updateMany(collectionName, selector, modifier) {
|
|
388
|
+
const storage = this.storageAdapters.get(collectionName);
|
|
389
|
+
if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
|
|
390
|
+
const items = await this.executeQuery(collectionName, selector);
|
|
391
|
+
if (items.length === 0) return [];
|
|
392
|
+
const { $setOnInsert, ...rest } = modifier;
|
|
393
|
+
const changed = await Promise.all(items.map(async (item) => {
|
|
394
|
+
const modified = modify(deepClone(item), rest);
|
|
395
|
+
if (item.id !== modified.id) {
|
|
396
|
+
if ((await this.executeQuery(collectionName, { id: modified.id }, { limit: 1 })).length > 0) throw new Error(`Item with id ${String(modified.id)} already exists`);
|
|
397
|
+
}
|
|
398
|
+
return modified;
|
|
399
|
+
}));
|
|
400
|
+
await storage.replace(changed);
|
|
401
|
+
await this.checkQueryUpdates(collectionName, [...items, ...changed]);
|
|
402
|
+
return {
|
|
403
|
+
items: changed,
|
|
404
|
+
previousItems: items
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
async replaceOne(collectionName, selector, replacement) {
|
|
408
|
+
const storage = this.storageAdapters.get(collectionName);
|
|
409
|
+
if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
|
|
410
|
+
const [item] = await this.executeQuery(collectionName, selector, { limit: 1 });
|
|
411
|
+
if (item == null) return [];
|
|
412
|
+
const modified = {
|
|
413
|
+
...replacement,
|
|
414
|
+
id: replacement.id ?? item.id
|
|
415
|
+
};
|
|
416
|
+
if (item.id !== modified.id) {
|
|
417
|
+
if ((await this.executeQuery(collectionName, { id: modified.id }, { limit: 1 })).length > 0) throw new Error(`Item with id ${String(modified.id)} already exists`);
|
|
418
|
+
}
|
|
419
|
+
await storage.replace([modified]);
|
|
420
|
+
await this.checkQueryUpdates(collectionName, [item, modified]);
|
|
421
|
+
return {
|
|
422
|
+
items: [modified],
|
|
423
|
+
previousItems: [item]
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
async removeOne(collectionName, selector) {
|
|
427
|
+
const storage = this.storageAdapters.get(collectionName);
|
|
428
|
+
if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
|
|
429
|
+
const [item] = await this.executeQuery(collectionName, selector, { limit: 1 });
|
|
430
|
+
if (item == null) return [];
|
|
431
|
+
await storage.remove([item]);
|
|
432
|
+
await this.checkQueryUpdates(collectionName, [item]);
|
|
433
|
+
return [item];
|
|
434
|
+
}
|
|
435
|
+
async removeMany(collectionName, selector) {
|
|
436
|
+
const storage = this.storageAdapters.get(collectionName);
|
|
437
|
+
if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
|
|
438
|
+
const items = await this.executeQuery(collectionName, selector);
|
|
439
|
+
if (items.length === 0) return [];
|
|
440
|
+
await storage.remove(items);
|
|
441
|
+
await this.checkQueryUpdates(collectionName, items);
|
|
442
|
+
return items;
|
|
443
|
+
}
|
|
444
|
+
async upsertMerged(collectionName, incoming) {
|
|
445
|
+
if (incoming.length === 0) return;
|
|
446
|
+
const storage = this.storageAdapters.get(collectionName);
|
|
447
|
+
if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
|
|
448
|
+
const ids = incoming.map((i) => i.id);
|
|
449
|
+
const existing = await storage.readIds(ids);
|
|
450
|
+
const existingById = new Map(existing.map((it) => [it.id, it]));
|
|
451
|
+
const toInsert = [];
|
|
452
|
+
const toReplace = [];
|
|
453
|
+
const referenceMap = this.idRefCounts.get(collectionName);
|
|
454
|
+
const autoload = this.autoloadIds.get(collectionName);
|
|
455
|
+
for (const item of incoming) {
|
|
456
|
+
const previous = existingById.get(item.id);
|
|
457
|
+
if (previous) toReplace.push(this.mergeItems(previous, item));
|
|
458
|
+
else toInsert.push(item);
|
|
459
|
+
autoload?.add(item.id);
|
|
460
|
+
referenceMap?.set(item.id, (referenceMap?.get(item.id) ?? 0) + 1);
|
|
461
|
+
}
|
|
462
|
+
if (toInsert.length > 0) await storage.insert(toInsert);
|
|
463
|
+
if (toReplace.length > 0) await storage.replace(toReplace);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
/**
|
|
467
|
+
* Usage (example):
|
|
468
|
+
*
|
|
469
|
+
* const adapter = new AutoFetchDataAdapter({
|
|
470
|
+
* storage: (name) => new IndexedDBStorage(name),
|
|
471
|
+
* fetchQueryItems: async (collectionName, selector) => {
|
|
472
|
+
* const res = await fetch(`/api/${collectionName}?q=${encodeURIComponent(JSON.stringify(selector||{}))}`)
|
|
473
|
+
* const items = await res.json()
|
|
474
|
+
* return { items }
|
|
475
|
+
* },
|
|
476
|
+
* registerRemoteChange: (onChange) => subscribeToWS(onChange),
|
|
477
|
+
* mergeItems: (a, b) => ({ ...a, ...b }),
|
|
478
|
+
* purgeDelay: 10_000,
|
|
479
|
+
* })
|
|
480
|
+
*
|
|
481
|
+
* const backend = adapter.createCollectionBackend(myCollection, ['status', 'projectId'])
|
|
482
|
+
*/
|
|
483
|
+
//#endregion
|
|
484
|
+
export { AutoFetchDataAdapter as default };
|
package/dist/index5.cjs.js
CHANGED
|
@@ -103,12 +103,12 @@ var Observer = class {
|
|
|
103
103
|
return;
|
|
104
104
|
}
|
|
105
105
|
const beforeOf = (index) => nextItems[index + 1] || null;
|
|
106
|
-
const previousById = delta.removed.length > 0 && this.hasCallbacks(["removed"]) || delta.changed.length > 0 && this.hasCallbacks(["changedField"]) ? new Map(this.previousItems.map((item) => [item.id, item])) : null;
|
|
106
|
+
const previousById = delta.removed.length > 0 && this.hasCallbacks(["removed"]) || delta.changed.length > 0 && this.hasCallbacks(["changed", "changedField"]) ? new Map(this.previousItems.map((item) => [item.id, item])) : null;
|
|
107
107
|
if (this.hasCallbacks(["changed", "changedField"])) delta.changed.forEach((item) => {
|
|
108
|
-
this.call("changed", item);
|
|
109
|
-
if (!this.hasCallbacks(["changedField"])) return;
|
|
110
108
|
const oldItem = previousById?.get(item.id);
|
|
111
109
|
if (!oldItem) return;
|
|
110
|
+
this.call("changed", item, oldItem);
|
|
111
|
+
if (!this.hasCallbacks(["changedField"])) return;
|
|
112
112
|
require_uniqueBy.default([...Object.keys(item), ...Object.keys(oldItem)], (value) => value).forEach((key) => {
|
|
113
113
|
if (require_isEqual.default(item[key], oldItem[key])) return;
|
|
114
114
|
this.call("changedField", item, key, oldItem[key], item[key]);
|
package/dist/index5.mjs
CHANGED
|
@@ -103,12 +103,12 @@ var Observer = class {
|
|
|
103
103
|
return;
|
|
104
104
|
}
|
|
105
105
|
const beforeOf = (index) => nextItems[index + 1] || null;
|
|
106
|
-
const previousById = delta.removed.length > 0 && this.hasCallbacks(["removed"]) || delta.changed.length > 0 && this.hasCallbacks(["changedField"]) ? new Map(this.previousItems.map((item) => [item.id, item])) : null;
|
|
106
|
+
const previousById = delta.removed.length > 0 && this.hasCallbacks(["removed"]) || delta.changed.length > 0 && this.hasCallbacks(["changed", "changedField"]) ? new Map(this.previousItems.map((item) => [item.id, item])) : null;
|
|
107
107
|
if (this.hasCallbacks(["changed", "changedField"])) delta.changed.forEach((item) => {
|
|
108
|
-
this.call("changed", item);
|
|
109
|
-
if (!this.hasCallbacks(["changedField"])) return;
|
|
110
108
|
const oldItem = previousById?.get(item.id);
|
|
111
109
|
if (!oldItem) return;
|
|
110
|
+
this.call("changed", item, oldItem);
|
|
111
|
+
if (!this.hasCallbacks(["changedField"])) return;
|
|
112
112
|
uniqueBy([...Object.keys(item), ...Object.keys(oldItem)], (value) => value).forEach((key) => {
|
|
113
113
|
if (isEqual(item[key], oldItem[key])) return;
|
|
114
114
|
this.call("changedField", item, key, oldItem[key], item[key]);
|
package/dist/index6.cjs.js
CHANGED
package/dist/index6.mjs
CHANGED
|
@@ -1,8 +1,63 @@
|
|
|
1
|
+
import { BaseItem, FieldSpecifier, SortSpecifier } from '../Collection';
|
|
2
|
+
import { default as Selector } from './Selector';
|
|
1
3
|
export interface Changeset<T> {
|
|
2
4
|
added: T[];
|
|
3
5
|
modified: T[];
|
|
4
6
|
removed: T[];
|
|
5
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* What a caller wants answered, in one piece.
|
|
10
|
+
*
|
|
11
|
+
* The same four options a query carries (`DataAdapter`'s `QueryOptions`) plus
|
|
12
|
+
* the selector, so an adapter that can push some of them down to its backing
|
|
13
|
+
* store sees the whole question rather than a filter at a time.
|
|
14
|
+
*/
|
|
15
|
+
export interface StorageQuery<T extends BaseItem> {
|
|
16
|
+
selector: Selector<T>;
|
|
17
|
+
sort?: SortSpecifier<T> | undefined;
|
|
18
|
+
skip?: number | undefined;
|
|
19
|
+
limit?: number | undefined;
|
|
20
|
+
fields?: FieldSpecifier<T> | undefined;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* What an adapter answered, and how much of the question it actually took on.
|
|
24
|
+
*
|
|
25
|
+
* Every flag defaults to "no": an adapter says only what it did, and the
|
|
26
|
+
* caller does the rest in JavaScript, exactly as it does for an adapter with
|
|
27
|
+
* no `query` at all. That is what makes partial support the normal case rather
|
|
28
|
+
* than a special one — an adapter may translate an equality and decline a
|
|
29
|
+
* `$regex`, or sort but not window, and never has to understand a selector it
|
|
30
|
+
* does not recognise.
|
|
31
|
+
*
|
|
32
|
+
* Three combinations are contradictions rather than choices, and
|
|
33
|
+
* `executeStorageQuery` throws on them rather than returning a wrong result
|
|
34
|
+
* quietly:
|
|
35
|
+
*
|
|
36
|
+
* - `windowed` without having applied the whole selector — the rows it dropped
|
|
37
|
+
* at the window's edge may be rows the caller was going to filter out, so the
|
|
38
|
+
* window is over the wrong set and nothing can repair it.
|
|
39
|
+
* - `windowed` without `sorted`, when a sort was asked for — the same, one step
|
|
40
|
+
* earlier: a window over an unordered set is an arbitrary subset.
|
|
41
|
+
* - `projected` without `sorted`, when the sort is on a field the projection
|
|
42
|
+
* dropped — the caller is then asked to sort by something that is no longer
|
|
43
|
+
* there. This is the trap `incrementalQueryUpdate` already documents.
|
|
44
|
+
*/
|
|
45
|
+
export interface StorageQueryAnswer<T extends BaseItem> {
|
|
46
|
+
items: T[];
|
|
47
|
+
/**
|
|
48
|
+
* The part of the selector the adapter could *not* apply, for the caller to
|
|
49
|
+
* evaluate over `items`. Omit it, or give `{}`, to say the whole selector was
|
|
50
|
+
* applied. This mirrors `getIndexInfo`'s `optimizedSelector`: an adapter
|
|
51
|
+
* narrows as far as it can and hands back what is left.
|
|
52
|
+
*/
|
|
53
|
+
residualSelector?: Selector<T>;
|
|
54
|
+
/** The items are in the requested order. */
|
|
55
|
+
sorted?: boolean;
|
|
56
|
+
/** `skip` and `limit` have already been applied. */
|
|
57
|
+
windowed?: boolean;
|
|
58
|
+
/** The items already carry only the requested fields. */
|
|
59
|
+
projected?: boolean;
|
|
60
|
+
}
|
|
6
61
|
export default interface StorageAdapter<T extends {
|
|
7
62
|
id: I;
|
|
8
63
|
} & Record<string, any>, I> {
|
|
@@ -10,6 +65,28 @@ export default interface StorageAdapter<T extends {
|
|
|
10
65
|
teardown(): Promise<void>;
|
|
11
66
|
readAll(): Promise<T[]>;
|
|
12
67
|
readIds(positions: I[]): Promise<T[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Answers a whole query — predicate, order, window and projection — as far as
|
|
70
|
+
* the backing store can.
|
|
71
|
+
*
|
|
72
|
+
* **Optional, and a pure optimisation.** An adapter that does not implement
|
|
73
|
+
* it is read through `readIds`/`readAll` and filtered, sorted, windowed and
|
|
74
|
+
* projected in JavaScript, which is what every adapter did before this
|
|
75
|
+
* existed and what several of them can only ever do: a store that holds one
|
|
76
|
+
* blob — `localstorage`, `fs`, `opfs` — has no way to answer less than all of
|
|
77
|
+
* it, and implementing this to do the same work behind a new name would buy
|
|
78
|
+
* nothing.
|
|
79
|
+
*
|
|
80
|
+
* It exists because without it there is no way to *express* a bounded read.
|
|
81
|
+
* `limit` costs exactly what no limit costs when the store hands over every
|
|
82
|
+
* matching row first, so nobody writes one, and a query over a table that
|
|
83
|
+
* grows with a user's history stays proportional to that history however
|
|
84
|
+
* carefully its consumer is written.
|
|
85
|
+
*
|
|
86
|
+
* An adapter may always answer less than it was asked; see
|
|
87
|
+
* `StorageQueryAnswer` for what it must not claim.
|
|
88
|
+
*/
|
|
89
|
+
query?(query: StorageQuery<T>): Promise<StorageQueryAnswer<T>>;
|
|
13
90
|
createIndex(field: string): Promise<void>;
|
|
14
91
|
dropIndex(field: string): Promise<void>;
|
|
15
92
|
/**
|
|
@@ -19,6 +96,9 @@ export default interface StorageAdapter<T extends {
|
|
|
19
96
|
* makes `3`, `'3'` and `new Date(...)` comparable as map keys at all. An
|
|
20
97
|
* adapter that stores its backend's own keys instead answers nothing for
|
|
21
98
|
* every non-string field, and everything for a `$ne` on one.
|
|
99
|
+
*
|
|
100
|
+
* Never consulted for a query an adapter answered through `query` itself —
|
|
101
|
+
* that adapter's own store already did the narrowing this index exists for.
|
|
22
102
|
*/
|
|
23
103
|
readIndex(field: string): Promise<Map<string | null, Set<I>>>;
|
|
24
104
|
insert(items: T[]): Promise<void>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BaseItem } from '../Collection';
|
|
2
|
+
import { default as Selector } from '../types/Selector';
|
|
3
|
+
import { default as StorageAdapter, StorageQuery } from '../types/StorageAdapter';
|
|
4
|
+
type QueryShape<T extends BaseItem> = Omit<StorageQuery<T>, 'selector'>;
|
|
5
|
+
/**
|
|
6
|
+
* Runs one query against a storage adapter and returns its finished result.
|
|
7
|
+
*
|
|
8
|
+
* This is the single place a query becomes rows. It used to be three copies — `AsyncDataAdapter`,
|
|
9
|
+
* `AutoFetchDataAdapter` and `WorkerDataAdapterHost` each had the same eight lines — which is how a
|
|
10
|
+
* capability like `StorageAdapter#query` ends up supported in one of them and quietly missing from
|
|
11
|
+
* the other two. The copies had already drifted apart in one respect: each maintained its own
|
|
12
|
+
* primary-key fast path.
|
|
13
|
+
*
|
|
14
|
+
* Two paths, and the second is what every adapter had before:
|
|
15
|
+
*
|
|
16
|
+
* - the adapter answers `query` itself, and this applies whatever it declined;
|
|
17
|
+
* - it does not, and this reads through the index (or the whole store) and filters, sorts, windows
|
|
18
|
+
* and projects here.
|
|
19
|
+
* @template T - The type of the items.
|
|
20
|
+
* @template I - The type of the item ids.
|
|
21
|
+
* @param storageAdapter - The storage adapter to read from.
|
|
22
|
+
* @param indices - The fields this collection has declared indices for.
|
|
23
|
+
* @param selector - The query's selector. `null` matches nothing.
|
|
24
|
+
* @param options - The query's sort, window and projection.
|
|
25
|
+
* @returns The query's result.
|
|
26
|
+
*/
|
|
27
|
+
export default function executeStorageQuery<T extends BaseItem<I>, I = any>(storageAdapter: StorageAdapter<any, any>, indices: string[], selector: Selector<T> | null, options?: QueryShape<T>): Promise<T[]>;
|
|
28
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signaldb/core",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.22",
|
|
4
4
|
"description": "SignalDB is a client-side database that provides a simple MongoDB-like interface to the data with first-class typescript support to achieve an optimistic UI. Data persistence can be achieved by using storage providers that store the data through a JSON interface to places such as localStorage.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "rimraf dist && vite build",
|