@signaldb/core 2.0.0-beta.14 → 2.0.0-beta.16
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/Collection/index.d.ts +40 -0
- package/dist/index28.cjs.js +49 -4
- package/dist/index28.mjs +49 -4
- package/dist/index34.cjs.js +16 -3
- package/dist/index34.mjs +16 -3
- package/package.json +1 -1
|
@@ -74,7 +74,22 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, E ext
|
|
|
74
74
|
private static fieldTracking;
|
|
75
75
|
private static onCreationCallbacks;
|
|
76
76
|
private static onDisposeCallbacks;
|
|
77
|
+
private static largeQueryWarningThreshold;
|
|
78
|
+
private static reportedLargeQueries;
|
|
77
79
|
static getCollections(): Collection<any, any, any, any>[];
|
|
80
|
+
/**
|
|
81
|
+
* Reports live queries whose result is larger than `rows`, once each, with
|
|
82
|
+
* the stack that registered them.
|
|
83
|
+
*
|
|
84
|
+
* A reactive query is re-evaluated whenever the data under it changes, and
|
|
85
|
+
* one registered from a long-lived place — a navigation bar, a provider
|
|
86
|
+
* near the root — keeps that cost for the lifetime of the application. There
|
|
87
|
+
* is otherwise nothing to see: the query works, and its price is only
|
|
88
|
+
* visible as an application that has grown slow. Finding one such query in a
|
|
89
|
+
* real app took a purpose-built profiler and the better part of a day.
|
|
90
|
+
* @param rows - Result size to report above, or `null` to switch the check off.
|
|
91
|
+
*/
|
|
92
|
+
static reportLargeQueries(rows: number | null): void;
|
|
78
93
|
static onCreation(callback: (collection: Collection<any>) => void): void;
|
|
79
94
|
static onDispose(callback: (collection: Collection<any>) => void): void;
|
|
80
95
|
/**
|
|
@@ -92,11 +107,27 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, E ext
|
|
|
92
107
|
* This improves performance by avoiding repetitive index recalculations and
|
|
93
108
|
* provides atomicity for the batch of operations.
|
|
94
109
|
* Supports both synchronous and asynchronous callbacks.
|
|
110
|
+
*
|
|
111
|
+
* **Without a `collections` argument this affects every collection in the
|
|
112
|
+
* process, not only the ones being written to.** Each of them defers every
|
|
113
|
+
* live query's requery until the batch ends. That is what makes a batch
|
|
114
|
+
* cheap for a handful of writes belonging to one event, and what makes it
|
|
115
|
+
* dangerous around a loop whose length is data-dependent: while it is open
|
|
116
|
+
* nothing anywhere updates, and everything deferred is flushed at once when
|
|
117
|
+
* it closes. One application wrapped a sync of roughly 1,100 records this
|
|
118
|
+
* way and its screens stopped resolving their data for the whole drain.
|
|
119
|
+
*
|
|
120
|
+
* Pass the collections being written to whenever that scope is known — it is
|
|
121
|
+
* both cheaper and safer. `Collection.batch([logs, versions], () => …)`
|
|
122
|
+
* defers those two and leaves everything else live.
|
|
123
|
+
* @param collections - The collections to batch. Omit to batch all of them.
|
|
95
124
|
* @param callback - The batch operation to execute.
|
|
96
125
|
* @returns A promise if the callback returns a promise, otherwise `void`.
|
|
97
126
|
*/
|
|
98
127
|
static batch<ReturnType>(callback: () => Promise<ReturnType>): Promise<void>;
|
|
99
128
|
static batch<ReturnType>(callback: () => ReturnType): void;
|
|
129
|
+
static batch<ReturnType>(collections: Collection<any, any, any, any>[], callback: () => Promise<ReturnType>): Promise<void>;
|
|
130
|
+
static batch<ReturnType>(collections: Collection<any, any, any, any>[], callback: () => ReturnType): void;
|
|
100
131
|
readonly name: string;
|
|
101
132
|
private backend;
|
|
102
133
|
private options;
|
|
@@ -131,6 +162,15 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, E ext
|
|
|
131
162
|
*/
|
|
132
163
|
constructor(options?: CollectionOptions<T, I, E, U>);
|
|
133
164
|
constructor(name: string, dataAdapter: DataAdapter, options?: CollectionOptions<T, I, E, U>);
|
|
165
|
+
/**
|
|
166
|
+
* Reports a live query the first time its result is found to be larger than
|
|
167
|
+
* the configured threshold. Once per query, because it re-runs on every
|
|
168
|
+
* write and a warning per write would be its own performance problem.
|
|
169
|
+
* @param selector - The query's selector.
|
|
170
|
+
* @param options - The query's options.
|
|
171
|
+
* @param registrationStack - Where the query was registered, if captured.
|
|
172
|
+
*/
|
|
173
|
+
private reportIfLargeQuery;
|
|
134
174
|
isBatchOperationInProgress(): boolean;
|
|
135
175
|
/**
|
|
136
176
|
* Checks whether the collection is currently performing a pull operation
|
package/dist/index28.cjs.js
CHANGED
|
@@ -23,9 +23,27 @@ var Collection = class Collection extends require_EventEmitter.default {
|
|
|
23
23
|
static fieldTracking = false;
|
|
24
24
|
static onCreationCallbacks = [];
|
|
25
25
|
static onDisposeCallbacks = [];
|
|
26
|
+
static largeQueryWarningThreshold = null;
|
|
27
|
+
static reportedLargeQueries = /* @__PURE__ */ new Set();
|
|
26
28
|
static getCollections() {
|
|
27
29
|
return Collection.collections;
|
|
28
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Reports live queries whose result is larger than `rows`, once each, with
|
|
33
|
+
* the stack that registered them.
|
|
34
|
+
*
|
|
35
|
+
* A reactive query is re-evaluated whenever the data under it changes, and
|
|
36
|
+
* one registered from a long-lived place — a navigation bar, a provider
|
|
37
|
+
* near the root — keeps that cost for the lifetime of the application. There
|
|
38
|
+
* is otherwise nothing to see: the query works, and its price is only
|
|
39
|
+
* visible as an application that has grown slow. Finding one such query in a
|
|
40
|
+
* real app took a purpose-built profiler and the better part of a day.
|
|
41
|
+
* @param rows - Result size to report above, or `null` to switch the check off.
|
|
42
|
+
*/
|
|
43
|
+
static reportLargeQueries(rows) {
|
|
44
|
+
Collection.largeQueryWarningThreshold = rows;
|
|
45
|
+
if (rows == null) Collection.reportedLargeQueries.clear();
|
|
46
|
+
}
|
|
29
47
|
static onCreation(callback) {
|
|
30
48
|
Collection.onCreationCallbacks.push(callback);
|
|
31
49
|
}
|
|
@@ -37,6 +55,7 @@ var Collection = class Collection extends require_EventEmitter.default {
|
|
|
37
55
|
*/
|
|
38
56
|
static enableDebugMode = () => {
|
|
39
57
|
Collection.debugMode = true;
|
|
58
|
+
if (Collection.largeQueryWarningThreshold == null) Collection.reportLargeQueries(500);
|
|
40
59
|
Collection.collections.forEach((collection) => {
|
|
41
60
|
collection.setDebugMode(true);
|
|
42
61
|
});
|
|
@@ -51,13 +70,17 @@ var Collection = class Collection extends require_EventEmitter.default {
|
|
|
51
70
|
collection.setFieldTracking(enable);
|
|
52
71
|
});
|
|
53
72
|
};
|
|
54
|
-
static batch(
|
|
55
|
-
|
|
56
|
-
const
|
|
73
|
+
static batch(collectionsOrCallback, maybeCallback) {
|
|
74
|
+
const scoped = Array.isArray(collectionsOrCallback);
|
|
75
|
+
const callback = scoped ? maybeCallback : collectionsOrCallback;
|
|
76
|
+
if (typeof callback !== "function") throw new TypeError("Collection.batch requires a callback");
|
|
77
|
+
const collections = scoped ? collectionsOrCallback : Collection.collections;
|
|
78
|
+
if (!scoped) Collection.batchOperationInProgress = true;
|
|
79
|
+
const execute = () => collections.reduce((memo, collection) => () => {
|
|
57
80
|
return collection.batch(memo);
|
|
58
81
|
}, callback)();
|
|
59
82
|
const afterBatch = () => {
|
|
60
|
-
Collection.batchOperationInProgress = false;
|
|
83
|
+
if (!scoped) Collection.batchOperationInProgress = false;
|
|
61
84
|
};
|
|
62
85
|
let maybePromise;
|
|
63
86
|
try {
|
|
@@ -105,6 +128,25 @@ var Collection = class Collection extends require_EventEmitter.default {
|
|
|
105
128
|
}).catch(() => {});
|
|
106
129
|
Collection.onCreationCallbacks.forEach((callback) => callback(this));
|
|
107
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Reports a live query the first time its result is found to be larger than
|
|
133
|
+
* the configured threshold. Once per query, because it re-runs on every
|
|
134
|
+
* write and a warning per write would be its own performance problem.
|
|
135
|
+
* @param selector - The query's selector.
|
|
136
|
+
* @param options - The query's options.
|
|
137
|
+
* @param registrationStack - Where the query was registered, if captured.
|
|
138
|
+
*/
|
|
139
|
+
reportIfLargeQuery(selector, options, registrationStack) {
|
|
140
|
+
const threshold = Collection.largeQueryWarningThreshold;
|
|
141
|
+
if (threshold == null) return;
|
|
142
|
+
const id = `${this.name}:${require_queryId.default(selector, options)}`;
|
|
143
|
+
if (Collection.reportedLargeQueries.has(id)) return;
|
|
144
|
+
const rows = this.backend.getQueryResult(selector, options || {}).length;
|
|
145
|
+
if (rows <= threshold) return;
|
|
146
|
+
Collection.reportedLargeQueries.add(id);
|
|
147
|
+
const keys = selector && typeof selector === "object" ? Object.keys(selector) : [];
|
|
148
|
+
console.warn(`[SignalDB] Live query on "${this.name}" holds ${rows} rows with selector {${keys.join(", ")}}. It is re-evaluated on every write to this collection, for as long as it stays registered. ${registrationStack ?? ""}`);
|
|
149
|
+
}
|
|
108
150
|
isBatchOperationInProgress() {
|
|
109
151
|
return Collection.batchOperationInProgress || this.batchOperationInProgress;
|
|
110
152
|
}
|
|
@@ -297,6 +339,8 @@ var Collection = class Collection extends require_EventEmitter.default {
|
|
|
297
339
|
selector,
|
|
298
340
|
options
|
|
299
341
|
}, listeners + 1);
|
|
342
|
+
const registrationStack = didRegister && Collection.largeQueryWarningThreshold != null ? (/* @__PURE__ */ new Error("query registered here")).stack : void 0;
|
|
343
|
+
if (didRegister) this.reportIfLargeQuery(selector, options, registrationStack);
|
|
300
344
|
const queryStateChangeCleanup = this.backend.onQueryStateChange(selector, options || {}, (state, delta) => {
|
|
301
345
|
if (state === "error") {
|
|
302
346
|
const queryError = this.backend.getQueryError(selector, options || {}) || /* @__PURE__ */ new Error(`Query on "${this.name}" failed`);
|
|
@@ -304,6 +348,7 @@ var Collection = class Collection extends require_EventEmitter.default {
|
|
|
304
348
|
return;
|
|
305
349
|
}
|
|
306
350
|
if (state !== "complete") return;
|
|
351
|
+
this.reportIfLargeQuery(selector, options, registrationStack);
|
|
307
352
|
if (delta != null && canApplyDeltas && !this.batchOperationInProgress) {
|
|
308
353
|
applyDelta(delta);
|
|
309
354
|
return;
|
package/dist/index28.mjs
CHANGED
|
@@ -23,9 +23,27 @@ var Collection = class Collection extends EventEmitter {
|
|
|
23
23
|
static fieldTracking = false;
|
|
24
24
|
static onCreationCallbacks = [];
|
|
25
25
|
static onDisposeCallbacks = [];
|
|
26
|
+
static largeQueryWarningThreshold = null;
|
|
27
|
+
static reportedLargeQueries = /* @__PURE__ */ new Set();
|
|
26
28
|
static getCollections() {
|
|
27
29
|
return Collection.collections;
|
|
28
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Reports live queries whose result is larger than `rows`, once each, with
|
|
33
|
+
* the stack that registered them.
|
|
34
|
+
*
|
|
35
|
+
* A reactive query is re-evaluated whenever the data under it changes, and
|
|
36
|
+
* one registered from a long-lived place — a navigation bar, a provider
|
|
37
|
+
* near the root — keeps that cost for the lifetime of the application. There
|
|
38
|
+
* is otherwise nothing to see: the query works, and its price is only
|
|
39
|
+
* visible as an application that has grown slow. Finding one such query in a
|
|
40
|
+
* real app took a purpose-built profiler and the better part of a day.
|
|
41
|
+
* @param rows - Result size to report above, or `null` to switch the check off.
|
|
42
|
+
*/
|
|
43
|
+
static reportLargeQueries(rows) {
|
|
44
|
+
Collection.largeQueryWarningThreshold = rows;
|
|
45
|
+
if (rows == null) Collection.reportedLargeQueries.clear();
|
|
46
|
+
}
|
|
29
47
|
static onCreation(callback) {
|
|
30
48
|
Collection.onCreationCallbacks.push(callback);
|
|
31
49
|
}
|
|
@@ -37,6 +55,7 @@ var Collection = class Collection extends EventEmitter {
|
|
|
37
55
|
*/
|
|
38
56
|
static enableDebugMode = () => {
|
|
39
57
|
Collection.debugMode = true;
|
|
58
|
+
if (Collection.largeQueryWarningThreshold == null) Collection.reportLargeQueries(500);
|
|
40
59
|
Collection.collections.forEach((collection) => {
|
|
41
60
|
collection.setDebugMode(true);
|
|
42
61
|
});
|
|
@@ -51,13 +70,17 @@ var Collection = class Collection extends EventEmitter {
|
|
|
51
70
|
collection.setFieldTracking(enable);
|
|
52
71
|
});
|
|
53
72
|
};
|
|
54
|
-
static batch(
|
|
55
|
-
|
|
56
|
-
const
|
|
73
|
+
static batch(collectionsOrCallback, maybeCallback) {
|
|
74
|
+
const scoped = Array.isArray(collectionsOrCallback);
|
|
75
|
+
const callback = scoped ? maybeCallback : collectionsOrCallback;
|
|
76
|
+
if (typeof callback !== "function") throw new TypeError("Collection.batch requires a callback");
|
|
77
|
+
const collections = scoped ? collectionsOrCallback : Collection.collections;
|
|
78
|
+
if (!scoped) Collection.batchOperationInProgress = true;
|
|
79
|
+
const execute = () => collections.reduce((memo, collection) => () => {
|
|
57
80
|
return collection.batch(memo);
|
|
58
81
|
}, callback)();
|
|
59
82
|
const afterBatch = () => {
|
|
60
|
-
Collection.batchOperationInProgress = false;
|
|
83
|
+
if (!scoped) Collection.batchOperationInProgress = false;
|
|
61
84
|
};
|
|
62
85
|
let maybePromise;
|
|
63
86
|
try {
|
|
@@ -105,6 +128,25 @@ var Collection = class Collection extends EventEmitter {
|
|
|
105
128
|
}).catch(() => {});
|
|
106
129
|
Collection.onCreationCallbacks.forEach((callback) => callback(this));
|
|
107
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Reports a live query the first time its result is found to be larger than
|
|
133
|
+
* the configured threshold. Once per query, because it re-runs on every
|
|
134
|
+
* write and a warning per write would be its own performance problem.
|
|
135
|
+
* @param selector - The query's selector.
|
|
136
|
+
* @param options - The query's options.
|
|
137
|
+
* @param registrationStack - Where the query was registered, if captured.
|
|
138
|
+
*/
|
|
139
|
+
reportIfLargeQuery(selector, options, registrationStack) {
|
|
140
|
+
const threshold = Collection.largeQueryWarningThreshold;
|
|
141
|
+
if (threshold == null) return;
|
|
142
|
+
const id = `${this.name}:${queryId(selector, options)}`;
|
|
143
|
+
if (Collection.reportedLargeQueries.has(id)) return;
|
|
144
|
+
const rows = this.backend.getQueryResult(selector, options || {}).length;
|
|
145
|
+
if (rows <= threshold) return;
|
|
146
|
+
Collection.reportedLargeQueries.add(id);
|
|
147
|
+
const keys = selector && typeof selector === "object" ? Object.keys(selector) : [];
|
|
148
|
+
console.warn(`[SignalDB] Live query on "${this.name}" holds ${rows} rows with selector {${keys.join(", ")}}. It is re-evaluated on every write to this collection, for as long as it stays registered. ${registrationStack ?? ""}`);
|
|
149
|
+
}
|
|
108
150
|
isBatchOperationInProgress() {
|
|
109
151
|
return Collection.batchOperationInProgress || this.batchOperationInProgress;
|
|
110
152
|
}
|
|
@@ -297,6 +339,8 @@ var Collection = class Collection extends EventEmitter {
|
|
|
297
339
|
selector,
|
|
298
340
|
options
|
|
299
341
|
}, listeners + 1);
|
|
342
|
+
const registrationStack = didRegister && Collection.largeQueryWarningThreshold != null ? (/* @__PURE__ */ new Error("query registered here")).stack : void 0;
|
|
343
|
+
if (didRegister) this.reportIfLargeQuery(selector, options, registrationStack);
|
|
300
344
|
const queryStateChangeCleanup = this.backend.onQueryStateChange(selector, options || {}, (state, delta) => {
|
|
301
345
|
if (state === "error") {
|
|
302
346
|
const queryError = this.backend.getQueryError(selector, options || {}) || /* @__PURE__ */ new Error(`Query on "${this.name}" failed`);
|
|
@@ -304,6 +348,7 @@ var Collection = class Collection extends EventEmitter {
|
|
|
304
348
|
return;
|
|
305
349
|
}
|
|
306
350
|
if (state !== "complete") return;
|
|
351
|
+
this.reportIfLargeQuery(selector, options, registrationStack);
|
|
307
352
|
if (delta != null && canApplyDeltas && !this.batchOperationInProgress) {
|
|
308
353
|
applyDelta(delta);
|
|
309
354
|
return;
|
package/dist/index34.cjs.js
CHANGED
|
@@ -93,8 +93,21 @@ var WorkerDataAdapter = class WorkerDataAdapter {
|
|
|
93
93
|
let nextItems = items;
|
|
94
94
|
let deltaToPublish;
|
|
95
95
|
if (delta != null) {
|
|
96
|
-
|
|
97
|
-
if (require_queryDelta.isEmptyQueryDelta(delta))
|
|
96
|
+
const canApply = require_queryDelta.canApplyQueryDelta(query.items, delta);
|
|
97
|
+
if (!canApply || require_queryDelta.isEmptyQueryDelta(delta)) {
|
|
98
|
+
if (state === query.state) return;
|
|
99
|
+
this.updateQuery(collectionName, {
|
|
100
|
+
selector: query.selector,
|
|
101
|
+
options: query.options
|
|
102
|
+
}, {
|
|
103
|
+
state,
|
|
104
|
+
error
|
|
105
|
+
});
|
|
106
|
+
const settled = collectionQueries.get(id);
|
|
107
|
+
if (!settled) return;
|
|
108
|
+
settled.stateChangeCallbacks.forEach((callback) => require_queryDelta.callWithDelta(callback, state, canApply ? delta : void 0));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
98
111
|
const servedBefore = this.flattenPendingWrites(collectionName) == null ? null : this.servedResult(collectionName, query);
|
|
99
112
|
nextItems = require_queryDelta.applyQueryDelta(query.items, delta);
|
|
100
113
|
if (servedBefore == null) deltaToPublish = delta;
|
|
@@ -482,7 +495,7 @@ var WorkerDataAdapter = class WorkerDataAdapter {
|
|
|
482
495
|
this.worker.terminate?.();
|
|
483
496
|
},
|
|
484
497
|
isReady: async () => {
|
|
485
|
-
await this.
|
|
498
|
+
await this.collectionReady.get(collection.name);
|
|
486
499
|
}
|
|
487
500
|
};
|
|
488
501
|
}
|
package/dist/index34.mjs
CHANGED
|
@@ -93,8 +93,21 @@ var WorkerDataAdapter = class WorkerDataAdapter {
|
|
|
93
93
|
let nextItems = items;
|
|
94
94
|
let deltaToPublish;
|
|
95
95
|
if (delta != null) {
|
|
96
|
-
|
|
97
|
-
if (isEmptyQueryDelta(delta))
|
|
96
|
+
const canApply = canApplyQueryDelta(query.items, delta);
|
|
97
|
+
if (!canApply || isEmptyQueryDelta(delta)) {
|
|
98
|
+
if (state === query.state) return;
|
|
99
|
+
this.updateQuery(collectionName, {
|
|
100
|
+
selector: query.selector,
|
|
101
|
+
options: query.options
|
|
102
|
+
}, {
|
|
103
|
+
state,
|
|
104
|
+
error
|
|
105
|
+
});
|
|
106
|
+
const settled = collectionQueries.get(id);
|
|
107
|
+
if (!settled) return;
|
|
108
|
+
settled.stateChangeCallbacks.forEach((callback) => callWithDelta(callback, state, canApply ? delta : void 0));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
98
111
|
const servedBefore = this.flattenPendingWrites(collectionName) == null ? null : this.servedResult(collectionName, query);
|
|
99
112
|
nextItems = applyQueryDelta(query.items, delta);
|
|
100
113
|
if (servedBefore == null) deltaToPublish = delta;
|
|
@@ -482,7 +495,7 @@ var WorkerDataAdapter = class WorkerDataAdapter {
|
|
|
482
495
|
this.worker.terminate?.();
|
|
483
496
|
},
|
|
484
497
|
isReady: async () => {
|
|
485
|
-
await this.
|
|
498
|
+
await this.collectionReady.get(collection.name);
|
|
486
499
|
}
|
|
487
500
|
};
|
|
488
501
|
}
|
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.16",
|
|
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",
|