@signaldb/core 2.0.0-beta.11 → 2.0.0-beta.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/dist/index31.mjs CHANGED
@@ -1,191 +1,32 @@
1
- import randomId from "./index8.mjs";
2
- import queryId from "./index22.mjs";
3
- import batchOnNextTick from "./index30.mjs";
4
- //#region src/WorkerDataAdapter.ts
5
- var WorkerDataAdapter = class {
6
- worker;
7
- options;
8
- id;
9
- isDisposed = false;
10
- workerReady;
11
- log = () => {};
12
- collectionReady = /* @__PURE__ */ new Map();
13
- batchExecutionHelpers = /* @__PURE__ */ new Map();
14
- queries = {};
15
- constructor(worker, options) {
16
- this.worker = worker;
17
- this.options = options;
18
- this.id = this.options.id || "default-worker-data-adapter";
19
- if (this.options.log) this.log = this.options.log;
20
- this.workerReady = new Promise((resolve, reject) => {
21
- const timeoutId = setTimeout(() => {
22
- reject(/* @__PURE__ */ new Error("WorkerDataAdapter initialization timed out"));
23
- }, 5e3);
24
- const handleMessage = (event) => {
25
- const { type, workerId } = event.data;
26
- if (workerId !== this.id) return;
27
- if (type === "ready") {
28
- resolve();
29
- clearTimeout(timeoutId);
30
- this.worker.removeEventListener("message", handleMessage);
31
- }
32
- };
33
- this.worker.addEventListener("message", handleMessage);
34
- });
35
- }
36
- async exec(method, collectionName, ...args) {
37
- await this.workerReady;
38
- if (method !== "isReady") {
39
- const collectionReady = this.collectionReady.get(collectionName);
40
- if (!collectionReady) throw new Error(`Collection "${collectionName}" is not registered in WorkerDataAdapter`);
41
- await collectionReady;
42
- }
43
- if (this.isDisposed) throw new Error("WorkerDataAdapter is disposed");
44
- return new Promise((resolve, reject) => {
45
- const messageId = randomId();
46
- const handleMessage = (event) => {
47
- const { id, workerId, type, data, error } = event.data;
48
- if (workerId !== this.id) return;
49
- if (type !== "response") return;
50
- if (id !== messageId) return;
51
- this.log(method, "result", data ?? error);
52
- if (error) reject(error);
53
- else resolve(data);
54
- this.worker.removeEventListener("message", handleMessage);
55
- };
56
- this.worker.addEventListener("message", handleMessage);
57
- this.worker.postMessage({
58
- id: messageId,
59
- workerId: this.id,
60
- method,
61
- args: [collectionName, ...args]
62
- });
63
- });
64
- }
65
- enqueueBatched(collectionName, method, args) {
66
- const helper = this.batchExecutionHelpers.get(collectionName);
67
- if (!helper) throw new Error(`Collection "${collectionName}" is not registered in WorkerDataAdapter`);
68
- return helper.enqueue(method, args);
69
- }
70
- updateQuery(collectionName, query, update) {
71
- const id = queryId(query.selector, query.options);
72
- const collectionQueries = this.queries[collectionName];
73
- if (!collectionQueries) return;
74
- const existing = collectionQueries.get(id);
75
- const newState = {
76
- state: "active",
77
- error: null,
78
- items: [],
79
- stateChangeCallbacks: [],
80
- eventHandler: existing?.eventHandler,
81
- ...existing,
82
- ...update
83
- };
84
- collectionQueries.set(id, newState);
85
- this.queries[collectionName] = collectionQueries;
86
- }
87
- createCollectionBackend(collection, indices) {
88
- this.queries[collection.name] = /* @__PURE__ */ new Map();
89
- this.exec("registerCollection", collection.name, indices);
90
- this.collectionReady.set(collection.name, this.exec("isReady", collection.name));
91
- this.batchExecutionHelpers.set(collection.name, batchOnNextTick(async (method, args) => this.exec(method, collection.name, args)));
1
+ import match from "./index18.mjs";
2
+ import project from "./index21.mjs";
3
+ import sortItems from "./index23.mjs";
4
+ //#region src/utils/applyQueryOptions.ts
5
+ /**
6
+ * Filters, sorts, paginates and projects a plain in-memory array the same way
7
+ * DefaultDataAdapter and WorkerDataAdapterHost apply a selector/QueryOptions
8
+ * pair to their stored items. Used to re-derive a query's result locally after
9
+ * a write, without asking the backing store again.
10
+ * @template T - The type of the items.
11
+ * @param items - The items to filter, sort, paginate and project.
12
+ * @param selector - The selector to match items against.
13
+ * @param options - Sort, skip, limit and field projection options.
14
+ * @returns The resulting items.
15
+ */
16
+ function applyQueryOptions(items, selector, options) {
17
+ const matched = selector == null ? [] : items.filter((item) => match(item, selector));
18
+ const { sort, skip, limit, fields } = options || {};
19
+ const sorted = sort ? sortItems(matched, sort) : matched;
20
+ const skipped = skip ? sorted.slice(skip) : sorted;
21
+ const limited = limit ? skipped.slice(0, limit) : skipped;
22
+ const idExcluded = fields && fields.id === 0;
23
+ return limited.map((item) => {
24
+ if (!fields) return item;
92
25
  return {
93
- insert: async (item) => {
94
- return this.enqueueBatched(collection.name, "insert", [item]);
95
- },
96
- updateOne: async (selector, modifier) => {
97
- return this.enqueueBatched(collection.name, "updateOne", [selector, modifier]);
98
- },
99
- updateMany: async (selector, modifier) => {
100
- return this.enqueueBatched(collection.name, "updateMany", [selector, modifier]);
101
- },
102
- replaceOne: async (selector, replacement) => {
103
- return this.enqueueBatched(collection.name, "replaceOne", [selector, replacement]);
104
- },
105
- removeOne: async (selector) => {
106
- return this.enqueueBatched(collection.name, "removeOne", [selector]);
107
- },
108
- removeMany: async (selector) => {
109
- return this.enqueueBatched(collection.name, "removeMany", [selector]);
110
- },
111
- registerQuery: (selector, options) => {
112
- this.updateQuery(collection.name, {
113
- selector,
114
- options
115
- }, {
116
- state: "active",
117
- error: null,
118
- items: []
119
- });
120
- this.exec("registerQuery", collection.name, selector, options);
121
- const handler = (event) => {
122
- const { type, data, workerId, error } = event.data;
123
- if (type !== "queryUpdate") return;
124
- if (data == null) return;
125
- const { collectionName, selector: responseSelector, options: responseOptions, state, items } = data;
126
- if (workerId !== this.id) return;
127
- if (collectionName !== collection.name) return;
128
- if (queryId(responseSelector, responseOptions) !== queryId(selector, options)) return;
129
- this.log("queryUpdate", responseSelector, responseOptions, state, data ?? error);
130
- this.updateQuery(collection.name, {
131
- selector: responseSelector,
132
- options: responseOptions
133
- }, {
134
- state,
135
- error,
136
- items
137
- });
138
- const query = this.queries[collection.name]?.get(queryId(selector, options));
139
- if (!query) return;
140
- query.stateChangeCallbacks.forEach((callback) => callback(state));
141
- };
142
- this.worker.addEventListener("message", handler);
143
- this.updateQuery(collection.name, {
144
- selector,
145
- options
146
- }, { eventHandler: handler });
147
- },
148
- unregisterQuery: (selector, options) => {
149
- const qid = queryId(selector, options);
150
- const query = this.queries[collection.name]?.get(qid);
151
- if (query?.eventHandler) this.worker.removeEventListener("message", query.eventHandler);
152
- this.queries[collection.name]?.delete(qid);
153
- this.exec("unregisterQuery", collection.name, selector, options);
154
- },
155
- getQueryState: (selector, options) => {
156
- return (this.queries[collection.name]?.get(queryId(selector, options)))?.state || "active";
157
- },
158
- getQueryError: (selector, options) => {
159
- return (this.queries[collection.name]?.get(queryId(selector, options)))?.error || null;
160
- },
161
- getQueryResult: (selector, options) => {
162
- return (this.queries[collection.name]?.get(queryId(selector, options)))?.items || [];
163
- },
164
- onQueryStateChange: (selector, options, callback) => {
165
- this.updateQuery(collection.name, {
166
- selector,
167
- options
168
- }, { stateChangeCallbacks: [...this.queries[collection.name]?.get(queryId(selector, options))?.stateChangeCallbacks || [], callback] });
169
- return () => {
170
- const currentCallbacks = this.queries[collection.name]?.get(queryId(selector, options))?.stateChangeCallbacks;
171
- if (!currentCallbacks) return;
172
- this.updateQuery(collection.name, {
173
- selector,
174
- options
175
- }, { stateChangeCallbacks: currentCallbacks.filter((existingCallback) => existingCallback !== callback) });
176
- };
177
- },
178
- executeQuery: (selector, options) => this.exec("executeQuery", collection.name, selector, options),
179
- dispose: async () => {
180
- await this.exec("unregisterCollection", collection.name);
181
- this.isDisposed = true;
182
- this.worker.terminate?.();
183
- },
184
- isReady: async () => {
185
- await this.exec("isReady", collection.name);
186
- }
26
+ ...idExcluded ? {} : { id: item.id },
27
+ ...project(item, fields)
187
28
  };
188
- }
189
- };
29
+ });
30
+ }
190
31
  //#endregion
191
- export { WorkerDataAdapter as default };
32
+ export { applyQueryOptions as default };
@@ -1,21 +1,311 @@
1
- //#region src/utils/compact.ts
2
- /**
3
- * Checks if a value is truthy.
4
- * @template T - The type of the value.
5
- * @param value - The value to check.
6
- * @returns A boolean indicating if the value is truthy.
7
- */
8
- function truthy(value) {
9
- return !!value;
10
- }
11
- /**
12
- * Filters out falsy values (`false`, `''`, `0`, `null`, `undefined`) from an array.
13
- * @template T - The type of the elements in the array.
14
- * @param array - The array to filter.
15
- * @returns A new array containing only the truthy values from the input array.
16
- */
17
- function compact(array) {
18
- return array.filter(truthy);
19
- }
1
+ const require_randomId = require("./index8.cjs.js");
2
+ const require_deepClone = require("./index17.cjs.js");
3
+ const require_match = require("./index18.cjs.js");
4
+ const require_modify = require("./index19.cjs.js");
5
+ const require_queryId = require("./index22.cjs.js");
6
+ const require_batchOnNextTick = require("./index30.cjs.js");
7
+ const require_applyQueryOptions = require("./index31.cjs.js");
8
+ //#region src/WorkerDataAdapter.ts
9
+ var WorkerDataAdapter = class {
10
+ worker;
11
+ options;
12
+ id;
13
+ isDisposed = false;
14
+ workerReady;
15
+ log = () => {};
16
+ collectionReady = /* @__PURE__ */ new Map();
17
+ batchExecutionHelpers = /* @__PURE__ */ new Map();
18
+ queries = {};
19
+ pendingWrites = /* @__PURE__ */ new Map();
20
+ pendingWriteSeq = 0;
21
+ constructor(worker, options) {
22
+ this.worker = worker;
23
+ this.options = options;
24
+ this.id = this.options.id || "default-worker-data-adapter";
25
+ if (this.options.log) this.log = this.options.log;
26
+ this.workerReady = new Promise((resolve, reject) => {
27
+ const timeoutId = setTimeout(() => {
28
+ reject(/* @__PURE__ */ new Error("WorkerDataAdapter initialization timed out"));
29
+ }, 5e3);
30
+ const handleMessage = (event) => {
31
+ const { type, workerId } = event.data;
32
+ if (workerId !== this.id) return;
33
+ if (type === "ready") {
34
+ resolve();
35
+ clearTimeout(timeoutId);
36
+ this.worker.removeEventListener("message", handleMessage);
37
+ }
38
+ };
39
+ this.worker.addEventListener("message", handleMessage);
40
+ });
41
+ }
42
+ async exec(method, collectionName, ...args) {
43
+ await this.workerReady;
44
+ if (method !== "isReady") {
45
+ const collectionReady = this.collectionReady.get(collectionName);
46
+ if (!collectionReady) throw new Error(`Collection "${collectionName}" is not registered in WorkerDataAdapter`);
47
+ await collectionReady;
48
+ }
49
+ if (this.isDisposed) throw new Error("WorkerDataAdapter is disposed");
50
+ return new Promise((resolve, reject) => {
51
+ const messageId = require_randomId.default();
52
+ const handleMessage = (event) => {
53
+ const { id, workerId, type, data, error } = event.data;
54
+ if (workerId !== this.id) return;
55
+ if (type !== "response") return;
56
+ if (id !== messageId) return;
57
+ this.log(method, "result", data ?? error);
58
+ if (error) reject(error);
59
+ else resolve(data);
60
+ this.worker.removeEventListener("message", handleMessage);
61
+ };
62
+ this.worker.addEventListener("message", handleMessage);
63
+ this.worker.postMessage({
64
+ id: messageId,
65
+ workerId: this.id,
66
+ method,
67
+ args: [collectionName, ...args]
68
+ });
69
+ });
70
+ }
71
+ observableItems(collectionName) {
72
+ const byId = /* @__PURE__ */ new Map();
73
+ this.queries[collectionName]?.forEach((query) => {
74
+ this.mergePendingWrites(collectionName, query.items).forEach((item) => {
75
+ byId.set(item.id, item);
76
+ });
77
+ });
78
+ return [...byId.values()];
79
+ }
80
+ mergePendingWrites(collectionName, items) {
81
+ const pending = this.pendingWrites.get(collectionName);
82
+ if (!pending || pending.size === 0) return items;
83
+ const merged = new Map(items.map((item) => [item.id, item]));
84
+ [...pending.entries()].sort(([a], [b]) => a - b).forEach(([, write]) => {
85
+ write.upserts.forEach((item, id) => merged.set(id, item));
86
+ write.deletes.forEach((id) => merged.delete(id));
87
+ });
88
+ return [...merged.values()];
89
+ }
90
+ /**
91
+ * Registers a write's effect locally and notifies every active query it
92
+ * touches, then returns a function that drops it again once the write
93
+ * settles.
94
+ * @param collectionName - The collection the write targets.
95
+ * @param upserts - Items inserted or updated by the write.
96
+ * @param deletes - Ids removed by the write.
97
+ * @returns A function that drops the pending write and re-notifies.
98
+ */
99
+ applyPendingWrite(collectionName, upserts, deletes) {
100
+ if (upserts.length === 0 && deletes.length === 0) return () => {};
101
+ const seq = this.pendingWriteSeq += 1;
102
+ const pending = this.pendingWrites.get(collectionName) ?? /* @__PURE__ */ new Map();
103
+ pending.set(seq, {
104
+ upserts: new Map(upserts.map((item) => [item.id, item])),
105
+ deletes: new Set(deletes)
106
+ });
107
+ this.pendingWrites.set(collectionName, pending);
108
+ const affectedIds = new Set([...upserts.map((item) => item.id), ...deletes]);
109
+ this.notifyAffectedQueries(collectionName, upserts, affectedIds);
110
+ return () => {
111
+ const current = this.pendingWrites.get(collectionName);
112
+ if (!current) return;
113
+ current.delete(seq);
114
+ if (current.size === 0) this.pendingWrites.delete(collectionName);
115
+ this.notifyAffectedQueries(collectionName, upserts, affectedIds);
116
+ };
117
+ }
118
+ notifyAffectedQueries(collectionName, upserts, affectedIds) {
119
+ this.queries[collectionName]?.forEach((query) => {
120
+ const wasHolding = query.items.some((item) => affectedIds.has(item.id));
121
+ const nowMatches = upserts.some((item) => query.selector != null && require_match.default(item, query.selector));
122
+ if (!wasHolding && !nowMatches) return;
123
+ query.stateChangeCallbacks.forEach((callback) => callback(query.state));
124
+ });
125
+ }
126
+ matchObservableItems(collectionName, selector, onlyFirst) {
127
+ if (selector == null) return [];
128
+ const matches = this.observableItems(collectionName).filter((item) => require_match.default(item, selector));
129
+ return onlyFirst ? matches.slice(0, 1) : matches;
130
+ }
131
+ resolveUpdate(collectionName, selector, modifier, onlyFirst) {
132
+ const { $setOnInsert, ...restModifier } = modifier;
133
+ const upserts = [];
134
+ const deletes = [];
135
+ this.matchObservableItems(collectionName, selector, onlyFirst).forEach((item) => {
136
+ const modifiedItem = require_modify.default(require_deepClone.default(item), restModifier);
137
+ upserts.push(modifiedItem);
138
+ if (modifiedItem.id !== item.id) deletes.push(item.id);
139
+ });
140
+ return {
141
+ upserts,
142
+ deletes
143
+ };
144
+ }
145
+ resolveRemoval(collectionName, selector, onlyFirst) {
146
+ return this.matchObservableItems(collectionName, selector, onlyFirst).map((item) => item.id);
147
+ }
148
+ resolveReplacement(item, replacement) {
149
+ const modifiedItem = {
150
+ ...replacement,
151
+ id: replacement.id ?? item.id
152
+ };
153
+ return {
154
+ upserts: [modifiedItem],
155
+ deletes: modifiedItem.id === item.id ? [] : [item.id]
156
+ };
157
+ }
158
+ async withPendingWrite(collectionName, delta, run) {
159
+ const dropPendingWrite = this.applyPendingWrite(collectionName, delta.upserts, delta.deletes);
160
+ try {
161
+ return await run();
162
+ } finally {
163
+ dropPendingWrite();
164
+ }
165
+ }
166
+ enqueueBatched(collectionName, method, args) {
167
+ const helper = this.batchExecutionHelpers.get(collectionName);
168
+ if (!helper) throw new Error(`Collection "${collectionName}" is not registered in WorkerDataAdapter`);
169
+ return helper.enqueue(method, args);
170
+ }
171
+ updateQuery(collectionName, query, update) {
172
+ const id = require_queryId.default(query.selector, query.options);
173
+ const collectionQueries = this.queries[collectionName];
174
+ if (!collectionQueries) return;
175
+ const existing = collectionQueries.get(id);
176
+ const newState = {
177
+ selector: query.selector,
178
+ options: query.options,
179
+ state: "active",
180
+ error: null,
181
+ items: [],
182
+ stateChangeCallbacks: [],
183
+ eventHandler: existing?.eventHandler,
184
+ ...existing,
185
+ ...update
186
+ };
187
+ collectionQueries.set(id, newState);
188
+ this.queries[collectionName] = collectionQueries;
189
+ }
190
+ createCollectionBackend(collection, indices) {
191
+ this.queries[collection.name] = /* @__PURE__ */ new Map();
192
+ this.exec("registerCollection", collection.name, indices);
193
+ this.collectionReady.set(collection.name, this.exec("isReady", collection.name));
194
+ this.batchExecutionHelpers.set(collection.name, require_batchOnNextTick.default(async (method, args) => this.exec(method, collection.name, args)));
195
+ return {
196
+ insert: async (item) => {
197
+ return this.withPendingWrite(collection.name, {
198
+ upserts: [item],
199
+ deletes: []
200
+ }, () => this.enqueueBatched(collection.name, "insert", [item]));
201
+ },
202
+ updateOne: async (selector, modifier) => {
203
+ return this.withPendingWrite(collection.name, this.resolveUpdate(collection.name, selector, modifier, true), () => this.enqueueBatched(collection.name, "updateOne", [selector, modifier]));
204
+ },
205
+ updateMany: async (selector, modifier) => {
206
+ return this.withPendingWrite(collection.name, this.resolveUpdate(collection.name, selector, modifier, false), () => this.enqueueBatched(collection.name, "updateMany", [selector, modifier]));
207
+ },
208
+ replaceOne: async (selector, replacement) => {
209
+ const [item] = this.matchObservableItems(collection.name, selector, true);
210
+ return this.withPendingWrite(collection.name, item == null ? {
211
+ upserts: [],
212
+ deletes: []
213
+ } : this.resolveReplacement(item, replacement), () => this.enqueueBatched(collection.name, "replaceOne", [selector, replacement]));
214
+ },
215
+ removeOne: async (selector) => {
216
+ return this.withPendingWrite(collection.name, {
217
+ upserts: [],
218
+ deletes: this.resolveRemoval(collection.name, selector, true)
219
+ }, () => this.enqueueBatched(collection.name, "removeOne", [selector]));
220
+ },
221
+ removeMany: async (selector) => {
222
+ return this.withPendingWrite(collection.name, {
223
+ upserts: [],
224
+ deletes: this.resolveRemoval(collection.name, selector, false)
225
+ }, () => this.enqueueBatched(collection.name, "removeMany", [selector]));
226
+ },
227
+ registerQuery: (selector, options) => {
228
+ this.updateQuery(collection.name, {
229
+ selector,
230
+ options
231
+ }, {
232
+ state: "active",
233
+ error: null,
234
+ items: []
235
+ });
236
+ this.exec("registerQuery", collection.name, selector, options);
237
+ const handler = (event) => {
238
+ const { type, data, workerId, error } = event.data;
239
+ if (type !== "queryUpdate") return;
240
+ if (data == null) return;
241
+ const { collectionName, selector: responseSelector, options: responseOptions, state, items } = data;
242
+ if (workerId !== this.id) return;
243
+ if (collectionName !== collection.name) return;
244
+ if (require_queryId.default(responseSelector, responseOptions) !== require_queryId.default(selector, options)) return;
245
+ this.log("queryUpdate", responseSelector, responseOptions, state, data ?? error);
246
+ this.updateQuery(collection.name, {
247
+ selector: responseSelector,
248
+ options: responseOptions
249
+ }, {
250
+ state,
251
+ error,
252
+ items
253
+ });
254
+ const query = this.queries[collection.name]?.get(require_queryId.default(selector, options));
255
+ if (!query) return;
256
+ query.stateChangeCallbacks.forEach((callback) => callback(state));
257
+ };
258
+ this.worker.addEventListener("message", handler);
259
+ this.updateQuery(collection.name, {
260
+ selector,
261
+ options
262
+ }, { eventHandler: handler });
263
+ },
264
+ unregisterQuery: (selector, options) => {
265
+ const qid = require_queryId.default(selector, options);
266
+ const query = this.queries[collection.name]?.get(qid);
267
+ if (query?.eventHandler) this.worker.removeEventListener("message", query.eventHandler);
268
+ this.queries[collection.name]?.delete(qid);
269
+ this.exec("unregisterQuery", collection.name, selector, options);
270
+ },
271
+ getQueryState: (selector, options) => {
272
+ return (this.queries[collection.name]?.get(require_queryId.default(selector, options)))?.state || "active";
273
+ },
274
+ getQueryError: (selector, options) => {
275
+ return (this.queries[collection.name]?.get(require_queryId.default(selector, options)))?.error || null;
276
+ },
277
+ getQueryResult: (selector, options) => {
278
+ const query = this.queries[collection.name]?.get(require_queryId.default(selector, options));
279
+ if (!query) return [];
280
+ const pending = this.pendingWrites.get(collection.name);
281
+ if (!pending || pending.size === 0) return query.items;
282
+ return require_applyQueryOptions.default(this.mergePendingWrites(collection.name, query.items), selector, options);
283
+ },
284
+ onQueryStateChange: (selector, options, callback) => {
285
+ this.updateQuery(collection.name, {
286
+ selector,
287
+ options
288
+ }, { stateChangeCallbacks: [...this.queries[collection.name]?.get(require_queryId.default(selector, options))?.stateChangeCallbacks || [], callback] });
289
+ return () => {
290
+ const currentCallbacks = this.queries[collection.name]?.get(require_queryId.default(selector, options))?.stateChangeCallbacks;
291
+ if (!currentCallbacks) return;
292
+ this.updateQuery(collection.name, {
293
+ selector,
294
+ options
295
+ }, { stateChangeCallbacks: currentCallbacks.filter((existingCallback) => existingCallback !== callback) });
296
+ };
297
+ },
298
+ executeQuery: (selector, options) => this.exec("executeQuery", collection.name, selector, options),
299
+ dispose: async () => {
300
+ await this.exec("unregisterCollection", collection.name);
301
+ this.isDisposed = true;
302
+ this.worker.terminate?.();
303
+ },
304
+ isReady: async () => {
305
+ await this.exec("isReady", collection.name);
306
+ }
307
+ };
308
+ }
309
+ };
20
310
  //#endregion
21
- exports.default = compact;
311
+ exports.default = WorkerDataAdapter;