@signaldb/core 2.0.0-beta.14 → 2.0.0-beta.15

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.
@@ -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
@@ -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(callback) {
55
- Collection.batchOperationInProgress = true;
56
- const execute = () => Collection.collections.reduce((memo, collection) => () => {
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(callback) {
55
- Collection.batchOperationInProgress = true;
56
- const execute = () => Collection.collections.reduce((memo, collection) => () => {
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;
@@ -482,7 +482,7 @@ var WorkerDataAdapter = class WorkerDataAdapter {
482
482
  this.worker.terminate?.();
483
483
  },
484
484
  isReady: async () => {
485
- await this.exec("isReady", collection.name);
485
+ await this.collectionReady.get(collection.name);
486
486
  }
487
487
  };
488
488
  }
package/dist/index34.mjs CHANGED
@@ -482,7 +482,7 @@ var WorkerDataAdapter = class WorkerDataAdapter {
482
482
  this.worker.terminate?.();
483
483
  },
484
484
  isReady: async () => {
485
- await this.exec("isReady", collection.name);
485
+ await this.collectionReady.get(collection.name);
486
486
  }
487
487
  };
488
488
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaldb/core",
3
- "version": "2.0.0-beta.14",
3
+ "version": "2.0.0-beta.15",
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",