@signaldb/svelte 2.0.0-beta.2 → 2.0.0-beta.21
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/base/core/src/AsyncDataAdapter.d.ts +112 -0
- package/dist/base/core/src/AsyncDataAdapter.js +549 -0
- package/dist/base/core/src/AutoFetchDataAdapter.d.ts +112 -0
- package/dist/base/core/src/AutoFetchDataAdapter.js +612 -0
- package/dist/base/core/src/Collection/Cursor.d.ts +165 -0
- package/dist/base/core/src/Collection/Cursor.js +352 -0
- package/dist/base/core/src/Collection/Observer.d.ts +95 -0
- package/dist/base/core/src/Collection/Observer.js +227 -0
- package/dist/base/core/src/Collection/index.d.ts +341 -0
- package/dist/base/core/src/Collection/index.js +761 -0
- package/dist/base/core/src/Collection/types.d.ts +28 -0
- package/dist/base/core/src/Collection/types.js +2 -0
- package/dist/base/core/src/DataAdapter.d.ts +54 -0
- package/dist/base/core/src/DataAdapter.js +2 -0
- package/dist/base/core/src/DefaultDataAdapter.d.ts +35 -0
- package/dist/base/core/src/DefaultDataAdapter.js +408 -0
- package/dist/base/core/src/WorkerDataAdapter.d.ts +79 -0
- package/dist/base/core/src/WorkerDataAdapter.js +749 -0
- package/dist/base/core/src/WorkerDataAdapterHost.d.ts +64 -0
- package/dist/base/core/src/WorkerDataAdapterHost.js +430 -0
- package/dist/base/core/src/createIndex.d.ts +7 -0
- package/dist/base/core/src/createIndex.js +120 -0
- package/dist/base/core/src/createIndexProvider.d.ts +8 -0
- package/dist/base/core/src/createIndexProvider.js +11 -0
- package/dist/base/core/src/createReactivityAdapter.d.ts +8 -0
- package/dist/base/core/src/createReactivityAdapter.js +11 -0
- package/dist/base/core/src/createStorageAdapter.d.ts +9 -0
- package/dist/base/core/src/createStorageAdapter.js +11 -0
- package/dist/base/core/src/getIndexInfo.d.ts +39 -0
- package/dist/base/core/src/getIndexInfo.js +188 -0
- package/dist/base/core/src/index.d.ts +26 -0
- package/dist/base/core/src/index.js +39 -0
- package/dist/base/core/src/types/Dependency.d.ts +4 -0
- package/dist/base/core/src/types/Dependency.js +2 -0
- package/dist/base/core/src/types/IndexProvider.d.ts +26 -0
- package/dist/base/core/src/types/IndexProvider.js +2 -0
- package/dist/base/core/src/types/Modifier.d.ts +46 -0
- package/dist/base/core/src/types/Modifier.js +3 -0
- package/dist/base/core/src/types/ReactivityAdapter.d.ts +6 -0
- package/dist/base/core/src/types/ReactivityAdapter.js +2 -0
- package/dist/base/core/src/types/Selector.d.ts +46 -0
- package/dist/base/core/src/types/Selector.js +3 -0
- package/dist/base/core/src/types/Signal.d.ts +4 -0
- package/dist/base/core/src/types/Signal.js +2 -0
- package/dist/base/core/src/types/StorageAdapter.d.ts +28 -0
- package/dist/base/core/src/types/StorageAdapter.js +2 -0
- package/dist/base/core/src/utils/EventEmitter.d.ts +71 -0
- package/dist/base/core/src/utils/EventEmitter.js +144 -0
- package/dist/base/core/src/utils/applyQueryOptions.d.ts +15 -0
- package/dist/base/core/src/utils/applyQueryOptions.js +36 -0
- package/dist/base/core/src/utils/batchOnNextTick.d.ts +16 -0
- package/dist/base/core/src/utils/batchOnNextTick.js +72 -0
- package/dist/base/core/src/utils/compact.d.ts +9 -0
- package/dist/base/core/src/utils/compact.js +21 -0
- package/dist/base/core/src/utils/createSignal.d.ts +14 -0
- package/dist/base/core/src/utils/createSignal.js +38 -0
- package/dist/base/core/src/utils/deepClone.d.ts +17 -0
- package/dist/base/core/src/utils/deepClone.js +68 -0
- package/dist/base/core/src/utils/get.d.ts +9 -0
- package/dist/base/core/src/utils/get.js +27 -0
- package/dist/base/core/src/utils/getMatchingKeys.d.ts +19 -0
- package/dist/base/core/src/utils/getMatchingKeys.js +49 -0
- package/dist/base/core/src/utils/idIndexQuery.d.ts +20 -0
- package/dist/base/core/src/utils/idIndexQuery.js +49 -0
- package/dist/base/core/src/utils/incrementalQueryUpdate.d.ts +62 -0
- package/dist/base/core/src/utils/incrementalQueryUpdate.js +197 -0
- package/dist/base/core/src/utils/intersection.d.ts +9 -0
- package/dist/base/core/src/utils/intersection.js +16 -0
- package/dist/base/core/src/utils/isEqual.d.ts +14 -0
- package/dist/base/core/src/utils/isEqual.js +43 -0
- package/dist/base/core/src/utils/isFieldExpression.d.ts +11 -0
- package/dist/base/core/src/utils/isFieldExpression.js +52 -0
- package/dist/base/core/src/utils/match.d.ts +12 -0
- package/dist/base/core/src/utils/match.js +16 -0
- package/dist/base/core/src/utils/modify.d.ts +14 -0
- package/dist/base/core/src/utils/modify.js +29 -0
- package/dist/base/core/src/utils/project.d.ts +15 -0
- package/dist/base/core/src/utils/project.js +43 -0
- package/dist/base/core/src/utils/projectItems.d.ts +12 -0
- package/dist/base/core/src/utils/projectItems.js +25 -0
- package/dist/base/core/src/utils/queryDelta.d.ts +83 -0
- package/dist/base/core/src/utils/queryDelta.js +231 -0
- package/dist/base/core/src/utils/queryId.d.ts +9 -0
- package/dist/base/core/src/utils/queryId.js +61 -0
- package/dist/base/core/src/utils/randomId.d.ts +7 -0
- package/dist/base/core/src/utils/randomId.js +12 -0
- package/dist/base/core/src/utils/reactiveOrAsync.d.ts +59 -0
- package/dist/base/core/src/utils/reactiveOrAsync.js +93 -0
- package/dist/base/core/src/utils/serializeValue.d.ts +12 -0
- package/dist/base/core/src/utils/serializeValue.js +27 -0
- package/dist/base/core/src/utils/set.d.ts +13 -0
- package/dist/base/core/src/utils/set.js +43 -0
- package/dist/base/core/src/utils/sortItems.d.ts +12 -0
- package/dist/base/core/src/utils/sortItems.js +23 -0
- package/dist/base/core/src/utils/storageIndexQuery.d.ts +20 -0
- package/dist/base/core/src/utils/storageIndexQuery.js +75 -0
- package/dist/base/core/src/utils/uniqueBy.d.ts +10 -0
- package/dist/base/core/src/utils/uniqueBy.js +19 -0
- package/dist/index.cjs +38 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +38 -0
- package/dist/index.mjs.map +1 -0
- package/dist/reactivity-adapters/svelte/index.svelte.d.ts +10 -0
- package/dist/reactivity-adapters/svelte/index.svelte.js +36 -0
- package/package.json +3 -3
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createIndex = void 0;
|
|
7
|
+
const EventEmitter_1 = __importDefault(require("../utils/EventEmitter"));
|
|
8
|
+
const createSignal_1 = __importDefault(require("../utils/createSignal"));
|
|
9
|
+
const randomId_1 = __importDefault(require("../utils/randomId"));
|
|
10
|
+
const DefaultDataAdapter_1 = __importDefault(require("../DefaultDataAdapter"));
|
|
11
|
+
const modify_1 = __importDefault(require("../utils/modify"));
|
|
12
|
+
const deepClone_1 = __importDefault(require("../utils/deepClone"));
|
|
13
|
+
const queryId_1 = __importDefault(require("../utils/queryId"));
|
|
14
|
+
const Cursor_1 = __importDefault(require("./Cursor"));
|
|
15
|
+
var createIndex_1 = require("../createIndex");
|
|
16
|
+
Object.defineProperty(exports, "createIndex", { enumerable: true, get: function () { return __importDefault(createIndex_1).default; } });
|
|
17
|
+
/**
|
|
18
|
+
* Represents a collection of data items with support for in-memory operations,
|
|
19
|
+
* persistence, reactivity, and event-based notifications. The collection provides
|
|
20
|
+
* CRUD operations, observer patterns, and batch operations.
|
|
21
|
+
* @template T - The type of the items stored in the collection.
|
|
22
|
+
* @template I - The type of the unique identifier for the items.
|
|
23
|
+
* @template U - The transformed item type after applying transformations (default is T).
|
|
24
|
+
*/
|
|
25
|
+
class Collection extends EventEmitter_1.default {
|
|
26
|
+
static collections = [];
|
|
27
|
+
static debugMode = false;
|
|
28
|
+
static batchOperationInProgress = false;
|
|
29
|
+
static fieldTracking = false;
|
|
30
|
+
static onCreationCallbacks = [];
|
|
31
|
+
static onDisposeCallbacks = [];
|
|
32
|
+
// How many rows a live query may hold before it is reported as expensive.
|
|
33
|
+
// `null` disables the check, which is the default: it costs a stack capture
|
|
34
|
+
// per registered query, which is worth paying while developing and not in
|
|
35
|
+
// production. `enableDebugMode()` turns it on.
|
|
36
|
+
static largeQueryWarningThreshold = null;
|
|
37
|
+
static reportedLargeQueries = new Set();
|
|
38
|
+
static getCollections() {
|
|
39
|
+
return Collection.collections;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Reports live queries whose result is larger than `rows`, once each, with
|
|
43
|
+
* the stack that registered them.
|
|
44
|
+
*
|
|
45
|
+
* A reactive query is re-evaluated whenever the data under it changes, and
|
|
46
|
+
* one registered from a long-lived place — a navigation bar, a provider
|
|
47
|
+
* near the root — keeps that cost for the lifetime of the application. There
|
|
48
|
+
* is otherwise nothing to see: the query works, and its price is only
|
|
49
|
+
* visible as an application that has grown slow. Finding one such query in a
|
|
50
|
+
* real app took a purpose-built profiler and the better part of a day.
|
|
51
|
+
* @param rows - Result size to report above, or `null` to switch the check off.
|
|
52
|
+
*/
|
|
53
|
+
static reportLargeQueries(rows) {
|
|
54
|
+
Collection.largeQueryWarningThreshold = rows;
|
|
55
|
+
if (rows == null)
|
|
56
|
+
Collection.reportedLargeQueries.clear();
|
|
57
|
+
}
|
|
58
|
+
static onCreation(callback) {
|
|
59
|
+
Collection.onCreationCallbacks.push(callback);
|
|
60
|
+
}
|
|
61
|
+
static onDispose(callback) {
|
|
62
|
+
Collection.onDisposeCallbacks.push(callback);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Enables debug mode for all collections.
|
|
66
|
+
*/
|
|
67
|
+
static enableDebugMode = () => {
|
|
68
|
+
Collection.debugMode = true;
|
|
69
|
+
// A query large enough to matter is exactly the kind of thing debug mode
|
|
70
|
+
// exists to surface, and it is invisible otherwise. Call
|
|
71
|
+
// `reportLargeQueries()` afterwards to pick a different threshold or turn
|
|
72
|
+
// it off again.
|
|
73
|
+
if (Collection.largeQueryWarningThreshold == null)
|
|
74
|
+
Collection.reportLargeQueries(500);
|
|
75
|
+
Collection.collections.forEach((collection) => {
|
|
76
|
+
collection.setDebugMode(true);
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Enables field tracking for all collections.
|
|
81
|
+
* @param enable - A boolean indicating whether to enable field tracking.
|
|
82
|
+
*/
|
|
83
|
+
static setFieldTracking = (enable) => {
|
|
84
|
+
Collection.fieldTracking = enable;
|
|
85
|
+
Collection.collections.forEach((collection) => {
|
|
86
|
+
collection.setFieldTracking(enable);
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
static batch(collectionsOrCallback, maybeCallback) {
|
|
90
|
+
const scoped = Array.isArray(collectionsOrCallback);
|
|
91
|
+
const callback = (scoped ? maybeCallback : collectionsOrCallback);
|
|
92
|
+
if (typeof callback !== 'function')
|
|
93
|
+
throw new TypeError('Collection.batch requires a callback');
|
|
94
|
+
const collections = scoped ? collectionsOrCallback : Collection.collections;
|
|
95
|
+
// Only a batch that really covers every collection may claim the global
|
|
96
|
+
// flag; a scoped one must not make unrelated collections report themselves
|
|
97
|
+
// as batching through `isBatchOperationInProgress()`.
|
|
98
|
+
if (!scoped)
|
|
99
|
+
Collection.batchOperationInProgress = true;
|
|
100
|
+
const execute = () => collections.reduce((memo, collection) => () => {
|
|
101
|
+
return collection.batch(memo);
|
|
102
|
+
}, callback)();
|
|
103
|
+
const afterBatch = () => {
|
|
104
|
+
if (!scoped)
|
|
105
|
+
Collection.batchOperationInProgress = false;
|
|
106
|
+
};
|
|
107
|
+
let maybePromise;
|
|
108
|
+
try {
|
|
109
|
+
maybePromise = execute();
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
// A synchronously throwing callback must not leave the batch flag
|
|
113
|
+
// stuck at `true` — that would defer every requery forever (see the
|
|
114
|
+
// rejection branch below).
|
|
115
|
+
afterBatch();
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
119
|
+
return maybePromise
|
|
120
|
+
.then(() => afterBatch(), (error) => {
|
|
121
|
+
// Rejections need the same cleanup as fulfillment — otherwise
|
|
122
|
+
// `batchOperationInProgress` stays `true` and all deferred
|
|
123
|
+
// requeries are never flushed, silently freezing reactivity.
|
|
124
|
+
afterBatch();
|
|
125
|
+
throw error;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
afterBatch();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
name;
|
|
133
|
+
backend;
|
|
134
|
+
options;
|
|
135
|
+
isPullingSignal;
|
|
136
|
+
isPushingSignal;
|
|
137
|
+
readySignal;
|
|
138
|
+
debugMode;
|
|
139
|
+
batchOperationInProgress = false;
|
|
140
|
+
isDisposed = false;
|
|
141
|
+
postBatchCallbacks = new Set();
|
|
142
|
+
fieldTracking = false;
|
|
143
|
+
queryListenersMap = new Map();
|
|
144
|
+
// Which registered queries have delivered an outcome at least once, backing
|
|
145
|
+
// `Cursor#isLoading()`. Kept here rather than on a cursor because a cursor is
|
|
146
|
+
// rebuilt on every reactive re-run, and rather than in the data adapters
|
|
147
|
+
// because deriving it from the state they already publish needs no change to
|
|
148
|
+
// the `CollectionBackend` contract. Keyed and dropped exactly like
|
|
149
|
+
// `queryListenersMap`, so a query that gets unregistered starts out pending
|
|
150
|
+
// again — which is correct, since the adapter re-executes it on the next
|
|
151
|
+
// registration.
|
|
152
|
+
settledQueriesSet = new Set();
|
|
153
|
+
constructor(nameOrOptions, maybeDataAdapter, maybeOptions) {
|
|
154
|
+
super();
|
|
155
|
+
const name = typeof nameOrOptions === 'string'
|
|
156
|
+
? nameOrOptions
|
|
157
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
158
|
+
: nameOrOptions?.name || `${this.constructor.name}-${(0, randomId_1.default)()}`;
|
|
159
|
+
const options = typeof nameOrOptions === 'string'
|
|
160
|
+
? maybeOptions || {}
|
|
161
|
+
: nameOrOptions || {};
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
163
|
+
const persistence = options.persistence;
|
|
164
|
+
const dataAdapter = maybeDataAdapter || new DefaultDataAdapter_1.default({
|
|
165
|
+
...persistence ? { storage: () => persistence } : {},
|
|
166
|
+
});
|
|
167
|
+
Collection.collections.push(this);
|
|
168
|
+
this.name = name;
|
|
169
|
+
this.options = { ...options };
|
|
170
|
+
this.fieldTracking = this.options.fieldTracking ?? Collection.fieldTracking;
|
|
171
|
+
this.debugMode = this.options.enableDebugMode ?? Collection.debugMode;
|
|
172
|
+
this.isPullingSignal = (0, createSignal_1.default)(this.options.reactivity, false);
|
|
173
|
+
this.isPushingSignal = (0, createSignal_1.default)(this.options.reactivity, false);
|
|
174
|
+
this.readySignal = (0, createSignal_1.default)(this.options.reactivity, false);
|
|
175
|
+
this.backend = dataAdapter.createCollectionBackend(this, this.options.indices ?? []);
|
|
176
|
+
void this.backend.isReady()
|
|
177
|
+
.then(() => {
|
|
178
|
+
this.readySignal.set(true);
|
|
179
|
+
})
|
|
180
|
+
.catch(() => { });
|
|
181
|
+
Collection.onCreationCallbacks.forEach(callback => callback(this));
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Reports a live query the first time its result is found to be larger than
|
|
185
|
+
* the configured threshold. Once per query, because it re-runs on every
|
|
186
|
+
* write and a warning per write would be its own performance problem.
|
|
187
|
+
* @param selector - The query's selector.
|
|
188
|
+
* @param options - The query's options.
|
|
189
|
+
* @param registrationStack - Where the query was registered, if captured.
|
|
190
|
+
*/
|
|
191
|
+
reportIfLargeQuery(selector, options, registrationStack) {
|
|
192
|
+
const threshold = Collection.largeQueryWarningThreshold;
|
|
193
|
+
if (threshold == null)
|
|
194
|
+
return;
|
|
195
|
+
const id = `${this.name}:${(0, queryId_1.default)(selector, options)}`;
|
|
196
|
+
if (Collection.reportedLargeQueries.has(id))
|
|
197
|
+
return;
|
|
198
|
+
const rows = this.backend.getQueryResult(selector, options || {}).length;
|
|
199
|
+
if (rows <= threshold)
|
|
200
|
+
return;
|
|
201
|
+
Collection.reportedLargeQueries.add(id);
|
|
202
|
+
// The selector's *keys*, never its values: the shape is what identifies the
|
|
203
|
+
// problem — an empty one means the query holds the whole collection — and
|
|
204
|
+
// the values would put user data into a log.
|
|
205
|
+
const keys = selector && typeof selector === 'object' ? Object.keys(selector) : [];
|
|
206
|
+
// eslint-disable-next-line no-console
|
|
207
|
+
console.warn(`[SignalDB] Live query on "${this.name}" holds ${rows} rows `
|
|
208
|
+
+ `with selector {${keys.join(', ')}}. It is re-evaluated on every write to this `
|
|
209
|
+
+ `collection, for as long as it stays registered. ${registrationStack ?? ''}`);
|
|
210
|
+
}
|
|
211
|
+
isBatchOperationInProgress() {
|
|
212
|
+
return Collection.batchOperationInProgress || this.batchOperationInProgress;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Checks whether the collection is currently performing a pull operation
|
|
216
|
+
* ⚡️ this function is reactive!
|
|
217
|
+
* (loading data from the persistence adapter).
|
|
218
|
+
* @returns A boolean indicating if the collection is in the process of pulling data.
|
|
219
|
+
*/
|
|
220
|
+
isPulling() {
|
|
221
|
+
return this.isPullingSignal.get() ?? false;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Checks whether the collection is currently performing a push operation
|
|
225
|
+
* ⚡️ this function is reactive!
|
|
226
|
+
* (saving data to the persistence adapter).
|
|
227
|
+
* @returns A boolean indicating if the collection is in the process of pushing data.
|
|
228
|
+
*/
|
|
229
|
+
isPushing() {
|
|
230
|
+
return this.isPushingSignal.get() ?? false;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Checks whether the collection is currently performing either a pull or push operation,
|
|
234
|
+
* ⚡️ this function is reactive!
|
|
235
|
+
* indicating that it is loading or saving data.
|
|
236
|
+
* @returns A boolean indicating if the collection is in the process of loading or saving data.
|
|
237
|
+
*/
|
|
238
|
+
isLoading() {
|
|
239
|
+
const isPulling = this.isPulling();
|
|
240
|
+
const isPushing = this.isPushing();
|
|
241
|
+
return isPulling || isPushing;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Retrieves the current debug mode status of the collection.
|
|
245
|
+
* @returns A boolean indicating whether debug mode is enabled for the collection.
|
|
246
|
+
*/
|
|
247
|
+
getDebugMode() {
|
|
248
|
+
return this.debugMode;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Enables or disables debug mode for the collection.
|
|
252
|
+
* When debug mode is enabled, additional debugging information and events are emitted.
|
|
253
|
+
* @param enable - A boolean indicating whether to enable (`true`) or disable (`false`) debug mode.
|
|
254
|
+
*/
|
|
255
|
+
setDebugMode(enable) {
|
|
256
|
+
this.debugMode = enable;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Enables or disables field tracking for the collection.
|
|
260
|
+
* @param enable - A boolean indicating whether to enable (`true`) or disable (`false`) field tracking.
|
|
261
|
+
*/
|
|
262
|
+
setFieldTracking(enable) {
|
|
263
|
+
this.fieldTracking = enable;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Resolves when the persistence adapter finished initializing
|
|
267
|
+
* and the collection is ready to be used.
|
|
268
|
+
* @returns A promise that resolves when the collection is ready.
|
|
269
|
+
* @example
|
|
270
|
+
* ```ts
|
|
271
|
+
* const collection = new Collection({
|
|
272
|
+
* persistence: // ...
|
|
273
|
+
* })
|
|
274
|
+
* await collection.isReady()
|
|
275
|
+
*
|
|
276
|
+
* collection.insert({ name: 'Item 1' })
|
|
277
|
+
*/
|
|
278
|
+
async ready() {
|
|
279
|
+
return this.backend.isReady();
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Checks if the collection is ready.
|
|
283
|
+
* ⚡️ this function is reactive!
|
|
284
|
+
* @returns A boolean indicating whether the collection is ready.
|
|
285
|
+
*/
|
|
286
|
+
isReady() {
|
|
287
|
+
return this.readySignal.get() ?? false;
|
|
288
|
+
}
|
|
289
|
+
profile(fn, measureFunction) {
|
|
290
|
+
if (!this.debugMode)
|
|
291
|
+
return fn();
|
|
292
|
+
const startTime = performance.now();
|
|
293
|
+
const handleProfileEnd = (result) => {
|
|
294
|
+
const endTime = performance.now();
|
|
295
|
+
measureFunction(endTime - startTime);
|
|
296
|
+
return result;
|
|
297
|
+
};
|
|
298
|
+
const maybePromise = fn();
|
|
299
|
+
return maybePromise instanceof Promise
|
|
300
|
+
? maybePromise.then(handleProfileEnd)
|
|
301
|
+
: handleProfileEnd(maybePromise);
|
|
302
|
+
}
|
|
303
|
+
executeInDebugMode(fn) {
|
|
304
|
+
if (!this.debugMode)
|
|
305
|
+
return;
|
|
306
|
+
// eslint-disable-next-line unicorn/error-message
|
|
307
|
+
const callstack = new Error().stack || '';
|
|
308
|
+
fn(callstack);
|
|
309
|
+
}
|
|
310
|
+
transform(item) {
|
|
311
|
+
if (!this.options.transform)
|
|
312
|
+
return item;
|
|
313
|
+
return this.options.transform(item);
|
|
314
|
+
}
|
|
315
|
+
transformAll(items, fields) {
|
|
316
|
+
if (!this.options.transformAll)
|
|
317
|
+
return items;
|
|
318
|
+
return this.options.transformAll((0, deepClone_1.default)(items), fields);
|
|
319
|
+
}
|
|
320
|
+
getItem(selector, options) {
|
|
321
|
+
const itemsOrPromise = this.getItems(selector, { ...options, limit: 1 });
|
|
322
|
+
if (itemsOrPromise instanceof Promise) {
|
|
323
|
+
return itemsOrPromise.then((items) => {
|
|
324
|
+
return items[0] || undefined;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return itemsOrPromise[0];
|
|
328
|
+
}
|
|
329
|
+
getItems(selector, options) {
|
|
330
|
+
this.emit('getItems', selector);
|
|
331
|
+
return this.profile(() => {
|
|
332
|
+
if (!options?.async)
|
|
333
|
+
return this.backend.getQueryResult(selector, options);
|
|
334
|
+
this.isPullingSignal.set(true);
|
|
335
|
+
return this.backend.executeQuery(selector, options)
|
|
336
|
+
.finally(() => {
|
|
337
|
+
this.isPullingSignal.set(false);
|
|
338
|
+
});
|
|
339
|
+
}, measuredTime => this.executeInDebugMode(callstack => this.emit('_debug.getItems', callstack, selector, measuredTime)));
|
|
340
|
+
}
|
|
341
|
+
async withPushState(asyncFunction) {
|
|
342
|
+
this.isPushingSignal.set(true);
|
|
343
|
+
try {
|
|
344
|
+
return await asyncFunction();
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
this.isPushingSignal.set(false);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
queryListeners(query, listeners) {
|
|
351
|
+
const id = (0, queryId_1.default)(query.selector, query.options);
|
|
352
|
+
if (listeners != null) {
|
|
353
|
+
return this.queryListenersMap.set(id, listeners);
|
|
354
|
+
}
|
|
355
|
+
return this.queryListenersMap.get(id) ?? 0;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Disposes the collection, unregisters persistence adapters, clears memory, and
|
|
359
|
+
* cleans up all resources used by the collection.
|
|
360
|
+
* @returns A promise that resolves when the collection is disposed.
|
|
361
|
+
*/
|
|
362
|
+
async dispose() {
|
|
363
|
+
await this.backend.dispose();
|
|
364
|
+
this.isDisposed = true;
|
|
365
|
+
this.removeAllListeners();
|
|
366
|
+
Collection.collections = Collection.collections.filter(collection => collection !== this);
|
|
367
|
+
Collection.onDisposeCallbacks.forEach(callback => callback(this));
|
|
368
|
+
}
|
|
369
|
+
find(selector = {}, options) {
|
|
370
|
+
if (this.isDisposed)
|
|
371
|
+
throw new Error('Collection is disposed');
|
|
372
|
+
if (selector !== undefined && (!selector || typeof selector !== 'object'))
|
|
373
|
+
throw new Error('Invalid selector');
|
|
374
|
+
const getTransformedItems = () => {
|
|
375
|
+
const itemsOrPromise = this.getItems(selector, options || {});
|
|
376
|
+
if (itemsOrPromise instanceof Promise) {
|
|
377
|
+
return itemsOrPromise.then((items) => {
|
|
378
|
+
return this.transformAll(items, options?.fields);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
const items = itemsOrPromise;
|
|
382
|
+
return this.transformAll(items, options?.fields);
|
|
383
|
+
};
|
|
384
|
+
const cursor = new Cursor_1.default(getTransformedItems, {
|
|
385
|
+
reactive: this.options.reactivity,
|
|
386
|
+
fieldTracking: this.fieldTracking,
|
|
387
|
+
...options,
|
|
388
|
+
transform: this.transform.bind(this),
|
|
389
|
+
queryState: {
|
|
390
|
+
hasSettled: () => {
|
|
391
|
+
if (this.settledQueriesSet.has((0, queryId_1.default)(selector, options)))
|
|
392
|
+
return true;
|
|
393
|
+
// An adapter that answers synchronously reports `'complete'` from
|
|
394
|
+
// the start, so a cursor over one is never in a loading state.
|
|
395
|
+
const state = this.backend.getQueryState(selector, options || {});
|
|
396
|
+
return state === 'complete' || state === 'error';
|
|
397
|
+
},
|
|
398
|
+
// The latch is set by the very callback that notifies, so a cursor
|
|
399
|
+
// can never be woken to read a state that has not been recorded yet,
|
|
400
|
+
// whatever order the backend runs its subscribers in.
|
|
401
|
+
onSettled: callback => this.backend.onQueryStateChange(selector, options || {}, (state) => {
|
|
402
|
+
if (state !== 'complete' && state !== 'error')
|
|
403
|
+
return;
|
|
404
|
+
this.settledQueriesSet.add((0, queryId_1.default)(selector, options));
|
|
405
|
+
callback();
|
|
406
|
+
}),
|
|
407
|
+
},
|
|
408
|
+
bindEvents: (requery, applyDelta) => {
|
|
409
|
+
const handleRequery = () => {
|
|
410
|
+
if (this.batchOperationInProgress) {
|
|
411
|
+
this.postBatchCallbacks.add(requery);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
requery();
|
|
415
|
+
};
|
|
416
|
+
// A `transformAll` sits between the backend's result and what the cursor holds, and it
|
|
417
|
+
// is free to produce anything at all — so a delta describing the backend's result says
|
|
418
|
+
// nothing about the cursor's. Those collections keep comparing.
|
|
419
|
+
const canApplyDeltas = !this.options.transformAll && !options?.async;
|
|
420
|
+
// register query if not yet registered
|
|
421
|
+
const listeners = this.queryListeners({ selector, options });
|
|
422
|
+
const didRegister = listeners === 0;
|
|
423
|
+
if (didRegister)
|
|
424
|
+
this.backend.registerQuery(selector, options || {});
|
|
425
|
+
this.queryListeners({ selector, options }, listeners + 1);
|
|
426
|
+
// Captured at registration, not at completion: by the time the result
|
|
427
|
+
// arrives the stack is the adapter's, and the only useful thing to
|
|
428
|
+
// report is where the query was asked for.
|
|
429
|
+
const registrationStack = didRegister && Collection.largeQueryWarningThreshold != null
|
|
430
|
+
? new Error('query registered here').stack
|
|
431
|
+
: undefined;
|
|
432
|
+
// A synchronous adapter already holds the result here and never
|
|
433
|
+
// reports `'complete'`, so the check has to happen at both points.
|
|
434
|
+
// Reporting is once per query, which makes the overlap harmless.
|
|
435
|
+
if (didRegister)
|
|
436
|
+
this.reportIfLargeQuery(selector, options, registrationStack);
|
|
437
|
+
const queryStateChangeCleanup = this.backend.onQueryStateChange(selector, options || {}, (state, delta) => {
|
|
438
|
+
// A failed query never reaches `'complete'`, so the cursor keeps
|
|
439
|
+
// serving its neutral empty value — indistinguishable from "no
|
|
440
|
+
// data" for anyone reading it. Surfacing the failure as an event
|
|
441
|
+
// is the only way a consumer can tell the difference. Requerying
|
|
442
|
+
// here would be pointless (the backend result is still empty)
|
|
443
|
+
// and risks a loop, so it deliberately does not.
|
|
444
|
+
if (state === 'error') {
|
|
445
|
+
const queryError = this.backend.getQueryError(selector, options || {})
|
|
446
|
+
|| new Error(`Query on "${this.name}" failed`);
|
|
447
|
+
this.emit('query.error', queryError, selector, options);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (state !== 'complete')
|
|
451
|
+
return;
|
|
452
|
+
this.reportIfLargeQuery(selector, options, registrationStack);
|
|
453
|
+
// Inside a batch the update is deferred to the end of it, by which point this delta
|
|
454
|
+
// is one of several and no longer describes the whole change — so the batch always
|
|
455
|
+
// ends in a comparison.
|
|
456
|
+
if (delta != null && canApplyDeltas && !this.batchOperationInProgress) {
|
|
457
|
+
applyDelta(delta);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
handleRequery();
|
|
461
|
+
});
|
|
462
|
+
this.emit('observer.created', selector, options);
|
|
463
|
+
return () => {
|
|
464
|
+
// Use queueMicrotask instead of setTimeout to avoid race conditions
|
|
465
|
+
// while still allowing batching of quick register/unregister calls
|
|
466
|
+
queueMicrotask(() => {
|
|
467
|
+
// unregister query if no more listeners
|
|
468
|
+
const newListeners = Math.max(0, this.queryListeners({ selector, options }) - 1);
|
|
469
|
+
// The count decides, and only the count. Asking additionally whether *this*
|
|
470
|
+
// observer was the one that registered leaks the query permanently: a rerun that
|
|
471
|
+
// creates its replacement before the old one's cleanup runs — which the microtask
|
|
472
|
+
// above deliberately allows — hands the count to an observer for which
|
|
473
|
+
// `didRegister` is false, and when that one is disposed the count reaches zero with
|
|
474
|
+
// nobody left who is allowed to act on it. The backend keeps the query registered
|
|
475
|
+
// and maintains its result on every write for the rest of the session, while
|
|
476
|
+
// `queryListeners` reads zero, so the next observer registers it a second time and
|
|
477
|
+
// is answered with the whole result again.
|
|
478
|
+
//
|
|
479
|
+
// The race that guard was written for is already covered here: an observer that
|
|
480
|
+
// registered in the meantime has incremented the count, so `newListeners` is not
|
|
481
|
+
// zero and nothing is unregistered.
|
|
482
|
+
if (newListeners === 0) {
|
|
483
|
+
this.backend.unregisterQuery(selector, options || {});
|
|
484
|
+
this.settledQueriesSet.delete((0, queryId_1.default)(selector, options));
|
|
485
|
+
}
|
|
486
|
+
this.queryListeners({ selector, options }, newListeners);
|
|
487
|
+
queryStateChangeCleanup();
|
|
488
|
+
this.emit('observer.disposed', selector, options);
|
|
489
|
+
});
|
|
490
|
+
};
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
this.emit('find', selector, options, cursor);
|
|
494
|
+
this.executeInDebugMode(callstack => this.emit('_debug.find', callstack, selector, options, cursor));
|
|
495
|
+
return cursor;
|
|
496
|
+
}
|
|
497
|
+
findOne(selector, options) {
|
|
498
|
+
if (this.isDisposed)
|
|
499
|
+
throw new Error('Collection is disposed');
|
|
500
|
+
const cursor = this.find(selector, {
|
|
501
|
+
limit: 1,
|
|
502
|
+
...options,
|
|
503
|
+
});
|
|
504
|
+
const handleItems = (items) => {
|
|
505
|
+
const returnValue = items[0] || undefined;
|
|
506
|
+
this.emit('findOne', selector, options, returnValue);
|
|
507
|
+
this.executeInDebugMode(callstack => this.emit('_debug.findOne', callstack, selector, options, returnValue));
|
|
508
|
+
return returnValue;
|
|
509
|
+
};
|
|
510
|
+
const maybePromise = cursor.fetch();
|
|
511
|
+
return (maybePromise instanceof Promise
|
|
512
|
+
? maybePromise.then(handleItems)
|
|
513
|
+
: handleItems(maybePromise));
|
|
514
|
+
}
|
|
515
|
+
batch(callback) {
|
|
516
|
+
if (this.batchOperationInProgress)
|
|
517
|
+
return callback();
|
|
518
|
+
this.batchOperationInProgress = true;
|
|
519
|
+
const afterBatch = () => {
|
|
520
|
+
this.batchOperationInProgress = false;
|
|
521
|
+
this.postBatchCallbacks.forEach(callback_ => callback_());
|
|
522
|
+
this.postBatchCallbacks.clear();
|
|
523
|
+
};
|
|
524
|
+
let maybePromise;
|
|
525
|
+
try {
|
|
526
|
+
maybePromise = callback();
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
// A synchronously throwing callback must not leave the batch flag
|
|
530
|
+
// stuck at `true` — that would defer every requery forever (see the
|
|
531
|
+
// rejection branch below).
|
|
532
|
+
afterBatch();
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
536
|
+
return maybePromise.then(() => afterBatch(), (error) => {
|
|
537
|
+
// Rejections need the same cleanup as fulfillment — otherwise
|
|
538
|
+
// `batchOperationInProgress` stays `true`, deferred post-batch
|
|
539
|
+
// callbacks (e.g. reactive requeries) are never flushed and
|
|
540
|
+
// reactivity silently freezes for the rest of the session.
|
|
541
|
+
afterBatch();
|
|
542
|
+
throw error;
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
afterBatch();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
onPostBatch(callback) {
|
|
550
|
+
if (this.isDisposed)
|
|
551
|
+
throw new Error('Collection is disposed');
|
|
552
|
+
if (this.batchOperationInProgress) {
|
|
553
|
+
this.postBatchCallbacks.add(callback);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
return callback();
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Inserts a single item into the collection. Generates a unique ID if not provided.
|
|
560
|
+
* @param item - The item to insert.
|
|
561
|
+
* @returns The ID of the inserted item.
|
|
562
|
+
* @throws {Error} If the collection is disposed or the item has an invalid ID.
|
|
563
|
+
*/
|
|
564
|
+
async insert(item) {
|
|
565
|
+
if (this.isDisposed)
|
|
566
|
+
throw new Error('Collection is disposed');
|
|
567
|
+
if (!item)
|
|
568
|
+
throw new Error('Invalid item');
|
|
569
|
+
const primaryKeyGenerator = this.options.primaryKeyGenerator ?? randomId_1.default;
|
|
570
|
+
const itemWithId = {
|
|
571
|
+
id: primaryKeyGenerator(item),
|
|
572
|
+
...item,
|
|
573
|
+
};
|
|
574
|
+
this.emit('validate', itemWithId);
|
|
575
|
+
const newItem = await this.withPushState(() => this.backend.insert(itemWithId));
|
|
576
|
+
this.emit('added', newItem);
|
|
577
|
+
this.emit('insert', newItem);
|
|
578
|
+
this.executeInDebugMode(callstack => this.emit('_debug.insert', callstack, newItem));
|
|
579
|
+
return newItem.id;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Inserts multiple items into the collection. Generates unique IDs for items if not provided.
|
|
583
|
+
* @param items - The items to insert.
|
|
584
|
+
* @returns An array of IDs of the inserted items.
|
|
585
|
+
* @throws {Error} If the collection is disposed or the items are invalid.
|
|
586
|
+
*/
|
|
587
|
+
async insertMany(items) {
|
|
588
|
+
if (this.isDisposed)
|
|
589
|
+
throw new Error('Collection is disposed');
|
|
590
|
+
if (!items)
|
|
591
|
+
throw new Error('Invalid items');
|
|
592
|
+
if (items.length === 0)
|
|
593
|
+
return [];
|
|
594
|
+
const ids = [];
|
|
595
|
+
await this.batch(async () => {
|
|
596
|
+
await Promise.all(items.map(async (item) => {
|
|
597
|
+
ids.push(await this.insert(item));
|
|
598
|
+
}));
|
|
599
|
+
});
|
|
600
|
+
return ids;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Updates a single item in the collection that matches the given selector.
|
|
604
|
+
* @param selector - The criteria to select the item to update.
|
|
605
|
+
* @param modifier - The modifications to apply to the item.
|
|
606
|
+
* @param [options] - Optional settings for the update operation.
|
|
607
|
+
* @param [options.upsert] - If `true`, creates a new item if no item matches the selector.
|
|
608
|
+
* @returns The number of items updated (0 or 1).
|
|
609
|
+
* @throws {Error} If the collection is disposed or invalid arguments are provided.
|
|
610
|
+
*/
|
|
611
|
+
async updateOne(selector, modifier, options) {
|
|
612
|
+
if (this.isDisposed)
|
|
613
|
+
throw new Error('Collection is disposed');
|
|
614
|
+
if (!selector)
|
|
615
|
+
throw new Error('Invalid selector');
|
|
616
|
+
if (!modifier)
|
|
617
|
+
throw new Error('Invalid modifier');
|
|
618
|
+
const { $setOnInsert, ...restModifier } = modifier;
|
|
619
|
+
// Reading the item back before writing it is worth a round trip to the data layer only when
|
|
620
|
+
// something is waiting to inspect it: a validator gets to refuse the write, and it can only do
|
|
621
|
+
// that beforehand. Otherwise the backend's own answer says everything there is to know — what
|
|
622
|
+
// it returns is what changed, and an empty answer is what turns an upsert into an insert.
|
|
623
|
+
if (this.listenerCount('validate') > 0) {
|
|
624
|
+
const item = await this.getItem(selector, { async: true });
|
|
625
|
+
if (item != null)
|
|
626
|
+
this.emit('validate', (0, modify_1.default)((0, deepClone_1.default)(item), restModifier));
|
|
627
|
+
}
|
|
628
|
+
const changes = await this.withPushState(() => this.backend.updateOne(selector, modifier));
|
|
629
|
+
if (changes.length === 0) {
|
|
630
|
+
if (!options?.upsert)
|
|
631
|
+
return 0; // no item found, and upsert is not enabled
|
|
632
|
+
const newItem = (0, modify_1.default)({}, {
|
|
633
|
+
...restModifier,
|
|
634
|
+
$set: {
|
|
635
|
+
...$setOnInsert,
|
|
636
|
+
...restModifier.$set,
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
await this.insert(newItem);
|
|
640
|
+
return 1;
|
|
641
|
+
}
|
|
642
|
+
changes.forEach(item => this.emit('changed', item, restModifier));
|
|
643
|
+
this.emit('updateOne', selector, modifier);
|
|
644
|
+
this.executeInDebugMode(callstack => this.emit('_debug.updateOne', callstack, selector, modifier));
|
|
645
|
+
return changes.length;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Updates multiple items in the collection that match the given selector.
|
|
649
|
+
* @param selector - The criteria to select the items to update.
|
|
650
|
+
* @param modifier - The modifications to apply to the items.
|
|
651
|
+
* @param [options] - Optional settings for the update operation.
|
|
652
|
+
* @param [options.upsert] - If `true`, creates new items if no items match the selector.
|
|
653
|
+
* @returns The number of items updated.
|
|
654
|
+
* @throws {Error} If the collection is disposed or invalid arguments are provided.
|
|
655
|
+
*/
|
|
656
|
+
async updateMany(selector, modifier, options) {
|
|
657
|
+
if (this.isDisposed)
|
|
658
|
+
throw new Error('Collection is disposed');
|
|
659
|
+
if (!selector)
|
|
660
|
+
throw new Error('Invalid selector');
|
|
661
|
+
if (!modifier)
|
|
662
|
+
throw new Error('Invalid modifier');
|
|
663
|
+
const { $setOnInsert, ...restModifier } = modifier;
|
|
664
|
+
// See `updateOne`: the items are only fetched up front for the sake of a validator.
|
|
665
|
+
if (this.listenerCount('validate') > 0) {
|
|
666
|
+
const items = await this.getItems(selector, { async: true });
|
|
667
|
+
items.forEach((item) => {
|
|
668
|
+
this.emit('validate', (0, modify_1.default)((0, deepClone_1.default)(item), restModifier));
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
const changes = await this.withPushState(() => this.backend.updateMany(selector, modifier));
|
|
672
|
+
if (changes.length === 0) {
|
|
673
|
+
if (!options?.upsert)
|
|
674
|
+
return 0; // no items found, and upsert is not enabled
|
|
675
|
+
const newItem = (0, modify_1.default)({}, {
|
|
676
|
+
...restModifier,
|
|
677
|
+
$set: {
|
|
678
|
+
...$setOnInsert,
|
|
679
|
+
...restModifier.$set,
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
await this.insert(newItem);
|
|
683
|
+
return 1;
|
|
684
|
+
}
|
|
685
|
+
changes.forEach((item) => {
|
|
686
|
+
this.emit('changed', item, restModifier);
|
|
687
|
+
});
|
|
688
|
+
this.emit('updateMany', selector, modifier);
|
|
689
|
+
this.executeInDebugMode(callstack => this.emit('_debug.updateMany', callstack, selector, modifier));
|
|
690
|
+
return changes.length;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Replaces a single item in the collection that matches the given selector.
|
|
694
|
+
* @param selector - The criteria to select the item to replace.
|
|
695
|
+
* @param replacement - The item to replace the selected item with.
|
|
696
|
+
* @param [options] - Optional settings for the replace operation.
|
|
697
|
+
* @param [options.upsert] - If `true`, creates a new item if no item matches the selector.
|
|
698
|
+
* @returns The number of items replaced (0 or 1).
|
|
699
|
+
* @throws {Error} If the collection is disposed or invalid arguments are provided.
|
|
700
|
+
*/
|
|
701
|
+
async replaceOne(selector, replacement, options) {
|
|
702
|
+
if (this.isDisposed)
|
|
703
|
+
throw new Error('Collection is disposed');
|
|
704
|
+
if (!selector)
|
|
705
|
+
throw new Error('Invalid selector');
|
|
706
|
+
// See `updateOne`: the item is only fetched up front for the sake of a validator.
|
|
707
|
+
if (this.listenerCount('validate') > 0) {
|
|
708
|
+
const item = await this.getItem(selector, { async: true });
|
|
709
|
+
if (item != null)
|
|
710
|
+
this.emit('validate', { id: item.id, ...replacement });
|
|
711
|
+
}
|
|
712
|
+
const changes = await this.withPushState(() => this.backend.replaceOne(selector, replacement));
|
|
713
|
+
if (changes.length === 0) {
|
|
714
|
+
if (!options?.upsert)
|
|
715
|
+
return 0; // no item found, and upsert is not enabled
|
|
716
|
+
await this.insert(replacement);
|
|
717
|
+
return 1;
|
|
718
|
+
}
|
|
719
|
+
changes.forEach(item => this.emit('changed', item, replacement));
|
|
720
|
+
this.emit('replaceOne', selector, replacement);
|
|
721
|
+
this.executeInDebugMode(callstack => this.emit('_debug.replaceOne', callstack, selector, replacement));
|
|
722
|
+
return changes.length;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Removes a single item from the collection that matches the given selector.
|
|
726
|
+
* @param selector - The criteria to select the item to remove.
|
|
727
|
+
* @returns The number of items removed (0 or 1).
|
|
728
|
+
* @throws {Error} If the collection is disposed or invalid arguments are provided.
|
|
729
|
+
*/
|
|
730
|
+
async removeOne(selector) {
|
|
731
|
+
if (this.isDisposed)
|
|
732
|
+
throw new Error('Collection is disposed');
|
|
733
|
+
if (!selector)
|
|
734
|
+
throw new Error('Invalid selector');
|
|
735
|
+
const removedItems = await this.withPushState(() => this.backend.removeOne(selector));
|
|
736
|
+
this.emit('removed', removedItems[0]);
|
|
737
|
+
this.emit('removeOne', selector);
|
|
738
|
+
this.executeInDebugMode(callstack => this.emit('_debug.removeOne', callstack, selector));
|
|
739
|
+
return removedItems.length;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Removes multiple items from the collection that match the given selector.
|
|
743
|
+
* @param selector - The criteria to select the items to remove.
|
|
744
|
+
* @returns The number of items removed.
|
|
745
|
+
* @throws {Error} If the collection is disposed or invalid arguments are provided.
|
|
746
|
+
*/
|
|
747
|
+
async removeMany(selector) {
|
|
748
|
+
if (this.isDisposed)
|
|
749
|
+
throw new Error('Collection is disposed');
|
|
750
|
+
if (!selector)
|
|
751
|
+
throw new Error('Invalid selector');
|
|
752
|
+
const removedItems = await this.withPushState(() => this.backend.removeMany(selector));
|
|
753
|
+
removedItems.forEach((item) => {
|
|
754
|
+
this.emit('removed', item);
|
|
755
|
+
});
|
|
756
|
+
this.emit('removeMany', selector);
|
|
757
|
+
this.executeInDebugMode(callstack => this.emit('_debug.removeMany', callstack, selector));
|
|
758
|
+
return removedItems.length;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
exports.default = Collection;
|