@signaldb/core 2.0.0-beta.1 → 2.0.0-beta.3

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.
@@ -120,7 +120,7 @@
120
120
  ]
121
121
  },
122
122
  "src/createIndexProvider.ts": {
123
- "file": "index.cjs30.js",
123
+ "file": "index.cjs33.js",
124
124
  "name": "createIndexProvider",
125
125
  "src": "src/createIndexProvider.ts"
126
126
  },
@@ -205,7 +205,7 @@
205
205
  ]
206
206
  },
207
207
  "src/utils/intersection.ts": {
208
- "file": "index.cjs31.js",
208
+ "file": "index.cjs30.js",
209
209
  "name": "utils/intersection",
210
210
  "src": "src/utils/intersection.ts"
211
211
  },
@@ -215,7 +215,7 @@
215
215
  "src": "src/utils/isEqual.ts"
216
216
  },
217
217
  "src/utils/isFieldExpression.ts": {
218
- "file": "index.cjs33.js",
218
+ "file": "index.cjs31.js",
219
219
  "name": "utils/isFieldExpression",
220
220
  "src": "src/utils/isFieldExpression.ts"
221
221
  },
@@ -27,7 +27,7 @@ export default class AsyncDataAdapter implements DataAdapter {
27
27
  private collectionIndices;
28
28
  private queries;
29
29
  constructor(options: AsyncDataAdapterOptions);
30
- createCollectionBackend<T extends BaseItem<I>, I = any, U = T>(collection: Collection<T, I, U>, indices: string[]): CollectionBackend<T, I>;
30
+ createCollectionBackend<T extends BaseItem<I>, I = any, E extends BaseItem = T, U = E>(collection: Collection<T, I, E, U>, indices: string[]): CollectionBackend<T, I>;
31
31
  private setupStorage;
32
32
  private ensureStorageAdapter;
33
33
  /**
@@ -71,7 +71,7 @@ export default class AutoFetchDataAdapter implements DataAdapter {
71
71
  private idRefCounts;
72
72
  private autoloadIds;
73
73
  constructor(options: AutoFetchDataAdapterOptions);
74
- createCollectionBackend<T extends BaseItem<I>, I = any, U = T>(collection: Collection<T, I, U>, indices: string[]): CollectionBackend<T, I>;
74
+ createCollectionBackend<T extends BaseItem<I>, I = any, E extends BaseItem = T, U = E>(collection: Collection<T, I, E, U>, indices: string[]): CollectionBackend<T, I>;
75
75
  private forceRefetchAll;
76
76
  private fetchAndIngest;
77
77
  private purgeSelector;
@@ -15,7 +15,7 @@ export interface CursorOptions<T extends BaseItem, U = T, Async extends boolean
15
15
  * Represents a cursor for querying and observing a filtered, sorted, and transformed
16
16
  * subset of items from a collection. Supports reactivity and field tracking.
17
17
  * @template T - The type of the items in the collection.
18
- * @template U - The transformed item type after applying transformations (default is T).
18
+ * @template U - The transformed item type after applying transform (default is T).
19
19
  */
20
20
  export default class Cursor<T extends BaseItem, U = T, Async extends boolean = false> {
21
21
  private observer;
@@ -37,6 +37,7 @@ export default class Cursor<T extends BaseItem, U = T, Async extends boolean = f
37
37
  * @param options.limit - The maximum number of items to return in the result set.
38
38
  * @param options.reactive - A reactivity adapter to enable observing changes in the cursor's result set.
39
39
  * @param options.fieldTracking - A boolean to enable fine-grained field tracking for reactivity.
40
+ * @param options.transformAll - A function that will be able to solve the n+1 problem
40
41
  */
41
42
  constructor(getItems: Async extends true ? () => Promise<T[]> : () => T[], options?: CursorOptions<T, U, Async>);
42
43
  private addGetters;
@@ -102,7 +103,7 @@ export default class Cursor<T extends BaseItem, U = T, Async extends boolean = f
102
103
  * @param skipInitial - A boolean indicating whether to skip the initial notification of the current result set.
103
104
  * @returns A function to stop observing changes.
104
105
  */
105
- observeChanges(callbacks: ObserveCallbacks<U>, skipInitial?: boolean): () => void;
106
+ observeChanges(callbacks: ObserveCallbacks<T>, skipInitial?: boolean): () => void;
106
107
  /**
107
108
  * Forces the cursor to re-evaluate its result set by re-fetching items
108
109
  * from the collection. This is useful when the underlying data or query
@@ -6,12 +6,12 @@ import type DataAdapter from '../DataAdapter';
6
6
  import type { QueryOptions } from '../DataAdapter';
7
7
  import type StorageAdapter from '../types/StorageAdapter';
8
8
  import Cursor from './Cursor';
9
- import type { BaseItem, FindOptions, Transform } from './types';
10
- export type { BaseItem, Transform, SortSpecifier, FieldSpecifier, FindOptions } from './types';
9
+ import type { BaseItem, FindOptions, Transform, TransformAll } from './types';
10
+ export type { BaseItem, Transform, TransformAll, SortSpecifier, FieldSpecifier, FindOptions } from './types';
11
11
  export type { CursorOptions } from './Cursor';
12
12
  export type { ObserveCallbacks } from './Observer';
13
13
  export { default as createIndex } from '../createIndex';
14
- export interface CollectionOptions<T extends BaseItem<I>, I, U = T> {
14
+ export interface CollectionOptions<T extends BaseItem<I>, I, E extends BaseItem = T, U = E> {
15
15
  /**
16
16
  * @deprecated Use new constructor parameters instead.
17
17
  */
@@ -22,19 +22,20 @@ export interface CollectionOptions<T extends BaseItem<I>, I, U = T> {
22
22
  persistence?: StorageAdapter<T, I>;
23
23
  primaryKeyGenerator?: (item: Omit<T, 'id'>) => I;
24
24
  reactivity?: ReactivityAdapter;
25
- transform?: Transform<T, U>;
25
+ transform?: Transform<E, U>;
26
+ transformAll?: TransformAll<T, E>;
26
27
  indices?: string[];
27
28
  enableDebugMode?: boolean;
28
29
  fieldTracking?: boolean;
29
30
  }
30
- interface CollectionEvents<T extends BaseItem, U = T> {
31
+ interface CollectionEvents<T extends BaseItem, E extends BaseItem = T, U = E> {
31
32
  'added': (item: T) => void;
32
33
  'changed': (item: T, modifier: Modifier<T>) => void;
33
34
  'removed': (item: T) => void;
34
35
  'observer.created': <O extends QueryOptions<T>>(selector?: Selector<T>, options?: O) => void;
35
36
  'observer.disposed': <O extends QueryOptions<T>>(selector?: Selector<T>, options?: O) => void;
36
37
  'getItems': (selector: Selector<T> | undefined) => void;
37
- 'find': <O extends FindOptions<T, Async>, Async extends boolean>(selector: Selector<T> | undefined, options: O | undefined, cursor: Cursor<T, U, Async>) => void;
38
+ 'find': <O extends FindOptions<T, Async>, Async extends boolean>(selector: Selector<T> | undefined, options: O | undefined, cursor: Cursor<E, U, Async>) => void;
38
39
  'findOne': <O extends QueryOptions<T>>(selector: Selector<T>, options: O | undefined, item: U | undefined) => void;
39
40
  'insert': (item: Omit<T, 'id'> & Partial<Pick<T, 'id'>>) => void;
40
41
  'updateOne': (selector: Selector<T>, modifier: Modifier<T>) => void;
@@ -44,7 +45,7 @@ interface CollectionEvents<T extends BaseItem, U = T> {
44
45
  'removeMany': (selector: Selector<T>) => void;
45
46
  'validate': (item: T) => void;
46
47
  '_debug.getItems': (callstack: string, selector: Selector<T> | undefined, measuredTime: number) => void;
47
- '_debug.find': <O extends FindOptions<T, Async>, Async extends boolean>(callstack: string, selector: Selector<T> | undefined, options: O | undefined, cursor: Cursor<T, U, Async>) => void;
48
+ '_debug.find': <O extends FindOptions<T, Async>, Async extends boolean>(callstack: string, selector: Selector<T> | undefined, options: O | undefined, cursor: Cursor<E, U, Async>) => void;
48
49
  '_debug.findOne': <O extends FindOptions<T, Async>, Async extends boolean>(callstack: string, selector: Selector<T>, options: O | undefined, item: U | undefined) => void;
49
50
  '_debug.insert': (callstack: string, item: Omit<T, 'id'> & Partial<Pick<T, 'id'>>) => void;
50
51
  '_debug.updateOne': (callstack: string, selector: Selector<T>, modifier: Modifier<T>) => void;
@@ -61,14 +62,14 @@ interface CollectionEvents<T extends BaseItem, U = T> {
61
62
  * @template I - The type of the unique identifier for the items.
62
63
  * @template U - The transformed item type after applying transformations (default is T).
63
64
  */
64
- export default class Collection<T extends BaseItem<I> = BaseItem, I = any, U = T> extends EventEmitter<CollectionEvents<T, U>> {
65
+ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, E extends BaseItem = T, U = E> extends EventEmitter<CollectionEvents<T, E, U>> {
65
66
  private static collections;
66
67
  private static debugMode;
67
68
  private static batchOperationInProgress;
68
69
  private static fieldTracking;
69
70
  private static onCreationCallbacks;
70
71
  private static onDisposeCallbacks;
71
- static getCollections(): Collection<any, any, any>[];
72
+ static getCollections(): Collection<any, any, any, any>[];
72
73
  static onCreation(callback: (collection: Collection<any>) => void): void;
73
74
  static onDispose(callback: (collection: Collection<any>) => void): void;
74
75
  /**
@@ -120,9 +121,10 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, U = T
120
121
  * @param options.indices - An array of index providers for optimized querying.
121
122
  * @param options.enableDebugMode - A boolean to enable or disable debug mode.
122
123
  * @param options.fieldTracking - A boolean to enable or disable field tracking by default.
124
+ * @param options.transformAll - A function that will be able to solve the n+1 problem
123
125
  */
124
- constructor(options?: CollectionOptions<T, I, U>);
125
- constructor(name: string, dataAdapter: DataAdapter, options?: CollectionOptions<T, I, U>);
126
+ constructor(options?: CollectionOptions<T, I, E, U>);
127
+ constructor(name: string, dataAdapter: DataAdapter, options?: CollectionOptions<T, I, E, U>);
126
128
  isBatchOperationInProgress(): boolean;
127
129
  /**
128
130
  * Checks whether the collection is currently performing a pull operation
@@ -184,6 +186,7 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, U = T
184
186
  private profile;
185
187
  private executeInDebugMode;
186
188
  private transform;
189
+ private transformAll;
187
190
  private getItem;
188
191
  private getItems;
189
192
  private withPushState;
@@ -202,7 +205,7 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, U = T
202
205
  * @param [options] - Options for the find operation, such as limit and sort.
203
206
  * @returns A cursor to fetch and observe the matching items.
204
207
  */
205
- find<Async extends boolean = false, O extends FindOptions<T, Async> = FindOptions<T, Async>>(selector?: Selector<T>, options?: O): Cursor<T, U, Async>;
208
+ find<Async extends boolean = false, O extends FindOptions<T, Async> = FindOptions<T, Async>>(selector?: Selector<T>, options?: O): Cursor<E, U, Async>;
206
209
  /**
207
210
  * Finds a single item in the collection based on a selector and optional options.
208
211
  * ⚡️ this function is reactive!
@@ -4,6 +4,7 @@ export type BaseItem<I = any> = {
4
4
  id: I;
5
5
  } & Record<string, any>;
6
6
  export type Transform<T, U = T> = ((document: T) => U) | null | undefined;
7
+ export type TransformAll<T extends BaseItem, O extends BaseItem = T> = ((items: T[], fields: FieldSpecifier<O> | undefined) => O[]) | null | undefined;
7
8
  export type SortSpecifier<T> = {
8
9
  [P in keyof T]?: -1 | 1;
9
10
  } & Record<string, -1 | 1>;
@@ -12,6 +12,7 @@ export interface QueryOptions<T extends BaseItem> {
12
12
  /** Dictionary of fields to return or exclude. */
13
13
  fields?: FieldSpecifier<T> | undefined;
14
14
  }
15
+ export type StateChangeCallback = (state: 'active' | 'complete' | 'error') => void;
15
16
  export interface CollectionBackend<T extends BaseItem<I>, I> {
16
17
  insert(item: T): Promise<T>;
17
18
  updateOne(selector: Selector<T>, modifier: Modifier<T>): Promise<T[]>;
@@ -25,10 +26,10 @@ export interface CollectionBackend<T extends BaseItem<I>, I> {
25
26
  getQueryError<O extends QueryOptions<T>>(selector: Selector<T>, options: O): Error | null;
26
27
  getQueryResult<O extends QueryOptions<T>>(selector: Selector<T>, options: O): T[];
27
28
  executeQuery<O extends QueryOptions<T>>(selector: Selector<T>, options: O): Promise<T[]>;
28
- onQueryStateChange<O extends QueryOptions<T>>(selector: Selector<T>, options: O, callback: (state: 'active' | 'complete' | 'error') => void): () => void;
29
+ onQueryStateChange<O extends QueryOptions<T>>(selector: Selector<T>, options: O, callback: StateChangeCallback): () => void;
29
30
  dispose(): Promise<void>;
30
31
  isReady(): Promise<void>;
31
32
  }
32
33
  export default interface DataAdapter {
33
- createCollectionBackend<T extends BaseItem<I>, I = any, U = T>(collection: Collection<T, I, U>, indices: string[]): CollectionBackend<T, I>;
34
+ createCollectionBackend<T extends BaseItem<I>, I = any, E extends BaseItem = T, U = E>(collection: Collection<T, I, E, U>, indices: string[]): CollectionBackend<T, I>;
34
35
  }
@@ -28,6 +28,6 @@ export default class DefaultDataAdapter implements DataAdapter {
28
28
  private flushQueuedQueryUpdates;
29
29
  private executeAndCacheQuery;
30
30
  private updateQueries;
31
- createCollectionBackend<T extends BaseItem<I>, I = any, U = T>(collection: Collection<T, I, U>, indices: string[]): CollectionBackend<T, I>;
31
+ createCollectionBackend<T extends BaseItem<I>, I = any, E extends BaseItem = T, U = E>(collection: Collection<T, I, E, U>, indices: string[]): CollectionBackend<T, I>;
32
32
  }
33
33
  export {};
@@ -20,6 +20,6 @@ export default class WorkerDataAdapter implements DataAdapter {
20
20
  private exec;
21
21
  private enqueueBatched;
22
22
  private updateQuery;
23
- createCollectionBackend<T extends BaseItem<I>, I = any, U = T>(collection: Collection<T, I, U>, indices?: string[]): CollectionBackend<T, I>;
23
+ createCollectionBackend<T extends BaseItem<I>, I = any, E extends BaseItem = T, U = E>(collection: Collection<T, I, E, U>, indices: string[]): CollectionBackend<T, I>;
24
24
  }
25
25
  export {};
@@ -324,7 +324,10 @@ class DefaultDataAdapter {
324
324
  getQueryResult: (selector, options) => {
325
325
  const isQueryActive = this.activeQueries.get(collection.name)?.has(queryId(selector, options));
326
326
  if (isQueryActive) {
327
- return this.cachedQueryResults.get(collection.name)?.get(queryId(selector, options)) ?? [];
327
+ const results = this.cachedQueryResults.get(collection.name)?.get(queryId(selector, options));
328
+ if (!results)
329
+ throw new Error("Cached query results are not defined!");
330
+ return results;
328
331
  }
329
332
  return this.executeQuery(collection, selector, options);
330
333
  },
@@ -142,8 +142,8 @@ class AsyncDataAdapter {
142
142
  const storage = this.storageAdapters.get(collectionName);
143
143
  if (!storage)
144
144
  throw new Error(`No persistence adapter for collection ${collectionName}`);
145
- await Promise.all(indices.map((index) => storage.createIndex(index)));
146
145
  await storage.setup();
146
+ await Promise.all(indices.map((index) => storage.createIndex(index)));
147
147
  }
148
148
  ensureStorageAdapter(name) {
149
149
  if (this.storageAdapters.has(name))
@@ -327,8 +327,8 @@ class AsyncDataAdapter {
327
327
  const storage = this.storageAdapters.get(collectionName);
328
328
  if (!storage)
329
329
  throw new Error(`No persistence adapter for collection ${collectionName}`);
330
- const existing = await this.executeQuery(collectionName, { id: newItem.id }, { limit: 1 });
331
- if (existing.length > 0)
330
+ const existingItems = await storage.readIds([newItem.id]);
331
+ if (existingItems.length > 0)
332
332
  throw new Error(`Item with id ${String(newItem.id)} already exists`);
333
333
  await storage.insert([newItem]);
334
334
  await this.checkQueryUpdates(collectionName, [newItem]);
@@ -91,13 +91,15 @@ class WorkerDataAdapter {
91
91
  state: "active",
92
92
  error: null,
93
93
  items: [],
94
+ stateChangeCallbacks: [],
95
+ eventHandler: existing?.eventHandler,
94
96
  ...existing,
95
97
  ...update
96
98
  };
97
99
  collectionQueries.set(id, newState);
98
100
  this.queries[collectionName] = collectionQueries;
99
101
  }
100
- createCollectionBackend(collection, indices = []) {
102
+ createCollectionBackend(collection, indices) {
101
103
  this.queries[collection.name] = /* @__PURE__ */ new Map();
102
104
  void this.exec("registerCollection", collection.name, indices);
103
105
  this.collectionReady.set(collection.name, this.exec("isReady", collection.name));
@@ -125,24 +127,6 @@ class WorkerDataAdapter {
125
127
  registerQuery: (selector, options) => {
126
128
  this.updateQuery(collection.name, { selector, options }, { state: "active", error: null, items: [] });
127
129
  void this.exec("registerQuery", collection.name, selector, options);
128
- },
129
- unregisterQuery: (selector, options) => {
130
- this.queries[collection.name]?.delete(queryId(selector, options));
131
- void this.exec("unregisterQuery", collection.name, selector, options);
132
- },
133
- getQueryState: (selector, options) => {
134
- const query = this.queries[collection.name]?.get(queryId(selector, options));
135
- return query?.state || "active";
136
- },
137
- getQueryError: (selector, options) => {
138
- const query = this.queries[collection.name]?.get(queryId(selector, options));
139
- return query?.error || null;
140
- },
141
- getQueryResult: (selector, options) => {
142
- const query = this.queries[collection.name]?.get(queryId(selector, options));
143
- return query?.items || [];
144
- },
145
- onQueryStateChange: (selector, options, callback) => {
146
130
  const handler = (event) => {
147
131
  const { type, data, workerId, error } = event.data;
148
132
  if (type !== "queryUpdate")
@@ -161,11 +145,49 @@ class WorkerDataAdapter {
161
145
  selector: responseSelector,
162
146
  options: responseOptions
163
147
  }, { state, error, items });
164
- callback(state);
148
+ const query = this.queries[collection.name]?.get(queryId(selector, options));
149
+ if (!query)
150
+ return;
151
+ query.stateChangeCallbacks.forEach((callback) => callback(state));
165
152
  };
166
153
  this.worker.addEventListener("message", handler);
154
+ this.updateQuery(collection.name, { selector, options }, { eventHandler: handler });
155
+ },
156
+ unregisterQuery: (selector, options) => {
157
+ const qid = queryId(selector, options);
158
+ const query = this.queries[collection.name]?.get(qid);
159
+ if (query?.eventHandler) {
160
+ this.worker.removeEventListener("message", query.eventHandler);
161
+ }
162
+ this.queries[collection.name]?.delete(qid);
163
+ void this.exec("unregisterQuery", collection.name, selector, options);
164
+ },
165
+ getQueryState: (selector, options) => {
166
+ const query = this.queries[collection.name]?.get(queryId(selector, options));
167
+ return query?.state || "active";
168
+ },
169
+ getQueryError: (selector, options) => {
170
+ const query = this.queries[collection.name]?.get(queryId(selector, options));
171
+ return query?.error || null;
172
+ },
173
+ getQueryResult: (selector, options) => {
174
+ const query = this.queries[collection.name]?.get(queryId(selector, options));
175
+ return query?.items || [];
176
+ },
177
+ onQueryStateChange: (selector, options, callback) => {
178
+ this.updateQuery(collection.name, { selector, options }, {
179
+ stateChangeCallbacks: [
180
+ ...this.queries[collection.name]?.get(queryId(selector, options))?.stateChangeCallbacks || [],
181
+ callback
182
+ ]
183
+ });
167
184
  return () => {
168
- this.worker.removeEventListener("message", handler);
185
+ const currentCallbacks = this.queries[collection.name]?.get(queryId(selector, options))?.stateChangeCallbacks;
186
+ if (!currentCallbacks)
187
+ throw new Error("State change callbacks are not defined!");
188
+ this.updateQuery(collection.name, { selector, options }, {
189
+ stateChangeCallbacks: currentCallbacks.filter((existingCallback) => existingCallback !== callback)
190
+ });
169
191
  };
170
192
  },
171
193
  executeQuery: (selector, options) => this.exec("executeQuery", collection.name, selector, options),
@@ -156,6 +156,8 @@ class WorkerDataAdapterHost {
156
156
  }
157
157
  }
158
158
  async executeQuery(collectionName, selector, options) {
159
+ if (selector === null)
160
+ return [];
159
161
  const items = await this.queryItems(collectionName, selector || {});
160
162
  const { sort, skip, limit, fields } = options || {};
161
163
  const sorted = sort ? sortItems(items, sort) : items;
@@ -249,7 +251,7 @@ class WorkerDataAdapterHost {
249
251
  const existingItems = await this.executeQuery(collectionName, { id: { $in: input.map((i) => i[0].id) } });
250
252
  const result = input.map(([item]) => {
251
253
  if (item.id == null)
252
- throw new Error("Item must have an id");
254
+ return new Error("Item must have an id");
253
255
  if (existingItems.some((existing) => existing.id === item.id)) {
254
256
  return new Error(`Item with id ${item.id} already exists`);
255
257
  }
@@ -53,7 +53,9 @@ class Observer {
53
53
  runChecks(getItems) {
54
54
  const result = getItems();
55
55
  if (result instanceof Promise) {
56
- void result.then((newItems) => this.checkItems(newItems));
56
+ result.then((newItems) => this.checkItems(newItems)).catch((error) => {
57
+ console.error("Error while asynchronously querying items", error);
58
+ });
57
59
  } else {
58
60
  this.checkItems(result);
59
61
  }
@@ -36,7 +36,6 @@ function clone(value) {
36
36
  function deepClone(object) {
37
37
  if (typeof structuredClone === "function")
38
38
  return structuredClone(object);
39
- /* istanbul ignore next -- @preserve */
40
39
  return clone(object);
41
40
  }
42
41
  exports.clone = clone;
@@ -28,6 +28,7 @@ class Cursor {
28
28
  * @param options.limit - The maximum number of items to return in the result set.
29
29
  * @param options.reactive - A reactivity adapter to enable observing changes in the cursor's result set.
30
30
  * @param options.fieldTracking - A boolean to enable fine-grained field tracking for reactivity.
31
+ * @param options.transformAll - A function that will be able to solve the n+1 problem
31
32
  */
32
33
  constructor(getItems, options) {
33
34
  this.getItems = getItems;
@@ -1,7 +1,16 @@
1
1
  "use strict";
2
+ function isEmptyOptions(options) {
3
+ if (options == null)
4
+ return true;
5
+ if (typeof options !== "object")
6
+ return false;
7
+ if (Array.isArray(options))
8
+ return false;
9
+ return Object.keys(options).length === 0;
10
+ }
2
11
  function queryId(selector, options) {
3
12
  const selectorId = JSON.stringify(selector);
4
- const optionsId = options == null ? -1 : JSON.stringify(options);
13
+ const optionsId = isEmptyOptions(options) ? -1 : JSON.stringify(options);
5
14
  return `${selectorId}:${optionsId}`;
6
15
  }
7
16
  module.exports = queryId;
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- const createIndexProvider = require("./index.cjs30.js");
2
+ const createIndexProvider = require("./index.cjs33.js");
3
3
  const get = require("./index.cjs10.js");
4
4
  const getMatchingKeys = require("./index.cjs26.js");
5
5
  const serializeValue = require("./index.cjs11.js");
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
- const intersection = require("./index.cjs31.js");
3
+ const intersection = require("./index.cjs30.js");
4
4
  function getMergedIndexInfo(queryFunctions, selector) {
5
5
  return queryFunctions.reduce((memoOrPromise, queryFunction) => {
6
6
  const resultOrPromise = queryFunction(selector);
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- const isFieldExpression = require("./index.cjs33.js");
2
+ const isFieldExpression = require("./index.cjs31.js");
3
3
  const serializeValue = require("./index.cjs11.js");
4
4
  function getMatchingKeys(field, selector) {
5
5
  const result = { include: null, exclude: null };
@@ -194,6 +194,11 @@ class Collection extends EventEmitter {
194
194
  return item;
195
195
  return this.options.transform(item);
196
196
  }
197
+ transformAll(items, fields) {
198
+ if (!this.options.transformAll)
199
+ return items;
200
+ return this.options.transformAll(deepClone.default(items), fields);
201
+ }
197
202
  getItem(selector, options) {
198
203
  const itemsOrPromise = this.getItems(selector, { ...options, limit: 1 });
199
204
  if (itemsOrPromise instanceof Promise) {
@@ -254,7 +259,17 @@ class Collection extends EventEmitter {
254
259
  throw new Error("Collection is disposed");
255
260
  if (selector !== void 0 && (!selector || typeof selector !== "object"))
256
261
  throw new Error("Invalid selector");
257
- const cursor = new Cursor.default((() => this.getItems(selector, options || {})), {
262
+ const getTransformedItems = () => {
263
+ const itemsOrPromise = this.getItems(selector, options || {});
264
+ if (itemsOrPromise instanceof Promise) {
265
+ return itemsOrPromise.then((items2) => {
266
+ return this.transformAll(items2, options?.fields);
267
+ });
268
+ }
269
+ const items = itemsOrPromise;
270
+ return this.transformAll(items, options?.fields);
271
+ };
272
+ const cursor = new Cursor.default(getTransformedItems, {
258
273
  reactive: this.options.reactivity,
259
274
  fieldTracking: this.fieldTracking,
260
275
  ...options,
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
- function createIndexProvider(definition) {
3
- return definition;
2
+ function intersection(...arrays) {
3
+ if (arrays.length === 0)
4
+ return [];
5
+ return [...new Set(arrays.reduce((a, b) => a.filter((c) => b.includes(c))))];
4
6
  }
5
- module.exports = createIndexProvider;
7
+ module.exports = intersection;
@@ -1,7 +1,42 @@
1
1
  "use strict";
2
- function intersection(...arrays) {
3
- if (arrays.length === 0)
4
- return [];
5
- return [...new Set(arrays.reduce((a, b) => a.filter((c) => b.includes(c))))];
2
+ const expressionKeys = /* @__PURE__ */ new Set([
3
+ "$eq",
4
+ "$gt",
5
+ "$gte",
6
+ "$lt",
7
+ "$lte",
8
+ "$in",
9
+ "$nin",
10
+ "$ne",
11
+ "$exists",
12
+ "$not",
13
+ "$expr",
14
+ "$jsonSchema",
15
+ "$mod",
16
+ "$regex",
17
+ "$options",
18
+ "$text",
19
+ "$where",
20
+ "$all",
21
+ "$elemMatch",
22
+ "$size",
23
+ "$bitsAllClear",
24
+ "$bitsAllSet",
25
+ "$bitsAnyClear",
26
+ "$bitsAnySet"
27
+ ]);
28
+ function isFieldExpression(expression) {
29
+ if (typeof expression !== "object" || expression == null) {
30
+ return false;
31
+ }
32
+ const keys = Object.keys(expression);
33
+ if (keys.length === 0) {
34
+ return false;
35
+ }
36
+ const hasInvalidKeys = keys.some((key) => !expressionKeys.has(key));
37
+ if (hasInvalidKeys)
38
+ return false;
39
+ const hasValidKeys = keys.every((key) => expressionKeys.has(key));
40
+ return hasValidKeys;
6
41
  }
7
- module.exports = intersection;
42
+ module.exports = isFieldExpression;
@@ -1,42 +1,5 @@
1
1
  "use strict";
2
- const expressionKeys = /* @__PURE__ */ new Set([
3
- "$eq",
4
- "$gt",
5
- "$gte",
6
- "$lt",
7
- "$lte",
8
- "$in",
9
- "$nin",
10
- "$ne",
11
- "$exists",
12
- "$not",
13
- "$expr",
14
- "$jsonSchema",
15
- "$mod",
16
- "$regex",
17
- "$options",
18
- "$text",
19
- "$where",
20
- "$all",
21
- "$elemMatch",
22
- "$size",
23
- "$bitsAllClear",
24
- "$bitsAllSet",
25
- "$bitsAnyClear",
26
- "$bitsAnySet"
27
- ]);
28
- function isFieldExpression(expression) {
29
- if (typeof expression !== "object" || expression == null) {
30
- return false;
31
- }
32
- const keys = Object.keys(expression);
33
- if (keys.length === 0) {
34
- return false;
35
- }
36
- const hasInvalidKeys = keys.some((key) => !expressionKeys.has(key));
37
- if (hasInvalidKeys)
38
- return false;
39
- const hasValidKeys = keys.every((key) => expressionKeys.has(key));
40
- return hasValidKeys;
2
+ function createIndexProvider(definition) {
3
+ return definition;
41
4
  }
42
- module.exports = isFieldExpression;
5
+ module.exports = createIndexProvider;
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export type { default as ReactivityAdapter } from './types/ReactivityAdapter';
2
2
  export type { default as StorageAdapter, Changeset, } from './types/StorageAdapter';
3
3
  export type { default as Selector } from './types/Selector';
4
4
  export type { default as Modifier } from './types/Modifier';
5
- export type { BaseItem, ObserveCallbacks, CursorOptions, Transform, SortSpecifier, FieldSpecifier, FindOptions, CollectionOptions, } from './Collection';
5
+ export type { BaseItem, ObserveCallbacks, CursorOptions, Transform, TransformAll, SortSpecifier, FieldSpecifier, FindOptions, CollectionOptions, } from './Collection';
6
6
  export type { default as DataAdapter } from './DataAdapter';
7
7
  export { default as Cursor } from './Collection/Cursor';
8
8
  export { default as Collection } from './Collection';
package/dist/index12.mjs CHANGED
@@ -323,7 +323,10 @@ class DefaultDataAdapter {
323
323
  getQueryResult: (selector, options) => {
324
324
  const isQueryActive = this.activeQueries.get(collection.name)?.has(queryId(selector, options));
325
325
  if (isQueryActive) {
326
- return this.cachedQueryResults.get(collection.name)?.get(queryId(selector, options)) ?? [];
326
+ const results = this.cachedQueryResults.get(collection.name)?.get(queryId(selector, options));
327
+ if (!results)
328
+ throw new Error("Cached query results are not defined!");
329
+ return results;
327
330
  }
328
331
  return this.executeQuery(collection, selector, options);
329
332
  },
package/dist/index13.mjs CHANGED
@@ -141,8 +141,8 @@ class AsyncDataAdapter {
141
141
  const storage = this.storageAdapters.get(collectionName);
142
142
  if (!storage)
143
143
  throw new Error(`No persistence adapter for collection ${collectionName}`);
144
- await Promise.all(indices.map((index) => storage.createIndex(index)));
145
144
  await storage.setup();
145
+ await Promise.all(indices.map((index) => storage.createIndex(index)));
146
146
  }
147
147
  ensureStorageAdapter(name) {
148
148
  if (this.storageAdapters.has(name))
@@ -326,8 +326,8 @@ class AsyncDataAdapter {
326
326
  const storage = this.storageAdapters.get(collectionName);
327
327
  if (!storage)
328
328
  throw new Error(`No persistence adapter for collection ${collectionName}`);
329
- const existing = await this.executeQuery(collectionName, { id: newItem.id }, { limit: 1 });
330
- if (existing.length > 0)
329
+ const existingItems = await storage.readIds([newItem.id]);
330
+ if (existingItems.length > 0)
331
331
  throw new Error(`Item with id ${String(newItem.id)} already exists`);
332
332
  await storage.insert([newItem]);
333
333
  await this.checkQueryUpdates(collectionName, [newItem]);
package/dist/index14.mjs CHANGED
@@ -90,13 +90,15 @@ class WorkerDataAdapter {
90
90
  state: "active",
91
91
  error: null,
92
92
  items: [],
93
+ stateChangeCallbacks: [],
94
+ eventHandler: existing?.eventHandler,
93
95
  ...existing,
94
96
  ...update
95
97
  };
96
98
  collectionQueries.set(id, newState);
97
99
  this.queries[collectionName] = collectionQueries;
98
100
  }
99
- createCollectionBackend(collection, indices = []) {
101
+ createCollectionBackend(collection, indices) {
100
102
  this.queries[collection.name] = /* @__PURE__ */ new Map();
101
103
  void this.exec("registerCollection", collection.name, indices);
102
104
  this.collectionReady.set(collection.name, this.exec("isReady", collection.name));
@@ -124,24 +126,6 @@ class WorkerDataAdapter {
124
126
  registerQuery: (selector, options) => {
125
127
  this.updateQuery(collection.name, { selector, options }, { state: "active", error: null, items: [] });
126
128
  void this.exec("registerQuery", collection.name, selector, options);
127
- },
128
- unregisterQuery: (selector, options) => {
129
- this.queries[collection.name]?.delete(queryId(selector, options));
130
- void this.exec("unregisterQuery", collection.name, selector, options);
131
- },
132
- getQueryState: (selector, options) => {
133
- const query = this.queries[collection.name]?.get(queryId(selector, options));
134
- return query?.state || "active";
135
- },
136
- getQueryError: (selector, options) => {
137
- const query = this.queries[collection.name]?.get(queryId(selector, options));
138
- return query?.error || null;
139
- },
140
- getQueryResult: (selector, options) => {
141
- const query = this.queries[collection.name]?.get(queryId(selector, options));
142
- return query?.items || [];
143
- },
144
- onQueryStateChange: (selector, options, callback) => {
145
129
  const handler = (event) => {
146
130
  const { type, data, workerId, error } = event.data;
147
131
  if (type !== "queryUpdate")
@@ -160,11 +144,49 @@ class WorkerDataAdapter {
160
144
  selector: responseSelector,
161
145
  options: responseOptions
162
146
  }, { state, error, items });
163
- callback(state);
147
+ const query = this.queries[collection.name]?.get(queryId(selector, options));
148
+ if (!query)
149
+ return;
150
+ query.stateChangeCallbacks.forEach((callback) => callback(state));
164
151
  };
165
152
  this.worker.addEventListener("message", handler);
153
+ this.updateQuery(collection.name, { selector, options }, { eventHandler: handler });
154
+ },
155
+ unregisterQuery: (selector, options) => {
156
+ const qid = queryId(selector, options);
157
+ const query = this.queries[collection.name]?.get(qid);
158
+ if (query?.eventHandler) {
159
+ this.worker.removeEventListener("message", query.eventHandler);
160
+ }
161
+ this.queries[collection.name]?.delete(qid);
162
+ void this.exec("unregisterQuery", collection.name, selector, options);
163
+ },
164
+ getQueryState: (selector, options) => {
165
+ const query = this.queries[collection.name]?.get(queryId(selector, options));
166
+ return query?.state || "active";
167
+ },
168
+ getQueryError: (selector, options) => {
169
+ const query = this.queries[collection.name]?.get(queryId(selector, options));
170
+ return query?.error || null;
171
+ },
172
+ getQueryResult: (selector, options) => {
173
+ const query = this.queries[collection.name]?.get(queryId(selector, options));
174
+ return query?.items || [];
175
+ },
176
+ onQueryStateChange: (selector, options, callback) => {
177
+ this.updateQuery(collection.name, { selector, options }, {
178
+ stateChangeCallbacks: [
179
+ ...this.queries[collection.name]?.get(queryId(selector, options))?.stateChangeCallbacks || [],
180
+ callback
181
+ ]
182
+ });
166
183
  return () => {
167
- this.worker.removeEventListener("message", handler);
184
+ const currentCallbacks = this.queries[collection.name]?.get(queryId(selector, options))?.stateChangeCallbacks;
185
+ if (!currentCallbacks)
186
+ throw new Error("State change callbacks are not defined!");
187
+ this.updateQuery(collection.name, { selector, options }, {
188
+ stateChangeCallbacks: currentCallbacks.filter((existingCallback) => existingCallback !== callback)
189
+ });
168
190
  };
169
191
  },
170
192
  executeQuery: (selector, options) => this.exec("executeQuery", collection.name, selector, options),
package/dist/index15.mjs CHANGED
@@ -155,6 +155,8 @@ class WorkerDataAdapterHost {
155
155
  }
156
156
  }
157
157
  async executeQuery(collectionName, selector, options) {
158
+ if (selector === null)
159
+ return [];
158
160
  const items = await this.queryItems(collectionName, selector || {});
159
161
  const { sort, skip, limit, fields } = options || {};
160
162
  const sorted = sort ? sortItems(items, sort) : items;
@@ -248,7 +250,7 @@ class WorkerDataAdapterHost {
248
250
  const existingItems = await this.executeQuery(collectionName, { id: { $in: input.map((i) => i[0].id) } });
249
251
  const result = input.map(([item]) => {
250
252
  if (item.id == null)
251
- throw new Error("Item must have an id");
253
+ return new Error("Item must have an id");
252
254
  if (existingItems.some((existing) => existing.id === item.id)) {
253
255
  return new Error(`Item with id ${item.id} already exists`);
254
256
  }
package/dist/index17.mjs CHANGED
@@ -52,7 +52,9 @@ class Observer {
52
52
  runChecks(getItems) {
53
53
  const result = getItems();
54
54
  if (result instanceof Promise) {
55
- void result.then((newItems) => this.checkItems(newItems));
55
+ result.then((newItems) => this.checkItems(newItems)).catch((error) => {
56
+ console.error("Error while asynchronously querying items", error);
57
+ });
56
58
  } else {
57
59
  this.checkItems(result);
58
60
  }
package/dist/index19.mjs CHANGED
@@ -34,7 +34,6 @@ function clone(value) {
34
34
  function deepClone(object) {
35
35
  if (typeof structuredClone === "function")
36
36
  return structuredClone(object);
37
- /* istanbul ignore next -- @preserve */
38
37
  return clone(object);
39
38
  }
40
39
  export {
package/dist/index2.mjs CHANGED
@@ -26,6 +26,7 @@ class Cursor {
26
26
  * @param options.limit - The maximum number of items to return in the result set.
27
27
  * @param options.reactive - A reactivity adapter to enable observing changes in the cursor's result set.
28
28
  * @param options.fieldTracking - A boolean to enable fine-grained field tracking for reactivity.
29
+ * @param options.transformAll - A function that will be able to solve the n+1 problem
29
30
  */
30
31
  constructor(getItems, options) {
31
32
  this.getItems = getItems;
package/dist/index20.mjs CHANGED
@@ -1,6 +1,15 @@
1
+ function isEmptyOptions(options) {
2
+ if (options == null)
3
+ return true;
4
+ if (typeof options !== "object")
5
+ return false;
6
+ if (Array.isArray(options))
7
+ return false;
8
+ return Object.keys(options).length === 0;
9
+ }
1
10
  function queryId(selector, options) {
2
11
  const selectorId = JSON.stringify(selector);
3
- const optionsId = options == null ? -1 : JSON.stringify(options);
12
+ const optionsId = isEmptyOptions(options) ? -1 : JSON.stringify(options);
4
13
  return `${selectorId}:${optionsId}`;
5
14
  }
6
15
  export {
package/dist/index21.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import createIndexProvider from "./index30.mjs";
1
+ import createIndexProvider from "./index33.mjs";
2
2
  import get from "./index10.mjs";
3
3
  import getMatchingKeys from "./index26.mjs";
4
4
  import serializeValue from "./index11.mjs";
package/dist/index22.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import intersection from "./index31.mjs";
1
+ import intersection from "./index30.mjs";
2
2
  function getMergedIndexInfo(queryFunctions, selector) {
3
3
  return queryFunctions.reduce((memoOrPromise, queryFunction) => {
4
4
  const resultOrPromise = queryFunction(selector);
package/dist/index26.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import isFieldExpression from "./index33.mjs";
1
+ import isFieldExpression from "./index31.mjs";
2
2
  import serializeValue from "./index11.mjs";
3
3
  function getMatchingKeys(field, selector) {
4
4
  const result = { include: null, exclude: null };
package/dist/index3.mjs CHANGED
@@ -192,6 +192,11 @@ class Collection extends EventEmitter {
192
192
  return item;
193
193
  return this.options.transform(item);
194
194
  }
195
+ transformAll(items, fields) {
196
+ if (!this.options.transformAll)
197
+ return items;
198
+ return this.options.transformAll(deepClone(items), fields);
199
+ }
195
200
  getItem(selector, options) {
196
201
  const itemsOrPromise = this.getItems(selector, { ...options, limit: 1 });
197
202
  if (itemsOrPromise instanceof Promise) {
@@ -252,7 +257,17 @@ class Collection extends EventEmitter {
252
257
  throw new Error("Collection is disposed");
253
258
  if (selector !== void 0 && (!selector || typeof selector !== "object"))
254
259
  throw new Error("Invalid selector");
255
- const cursor = new Cursor((() => this.getItems(selector, options || {})), {
260
+ const getTransformedItems = () => {
261
+ const itemsOrPromise = this.getItems(selector, options || {});
262
+ if (itemsOrPromise instanceof Promise) {
263
+ return itemsOrPromise.then((items2) => {
264
+ return this.transformAll(items2, options?.fields);
265
+ });
266
+ }
267
+ const items = itemsOrPromise;
268
+ return this.transformAll(items, options?.fields);
269
+ };
270
+ const cursor = new Cursor(getTransformedItems, {
256
271
  reactive: this.options.reactivity,
257
272
  fieldTracking: this.fieldTracking,
258
273
  ...options,
package/dist/index30.mjs CHANGED
@@ -1,6 +1,8 @@
1
- function createIndexProvider(definition) {
2
- return definition;
1
+ function intersection(...arrays) {
2
+ if (arrays.length === 0)
3
+ return [];
4
+ return [...new Set(arrays.reduce((a, b) => a.filter((c) => b.includes(c))))];
3
5
  }
4
6
  export {
5
- createIndexProvider as default
7
+ intersection as default
6
8
  };
package/dist/index31.mjs CHANGED
@@ -1,8 +1,43 @@
1
- function intersection(...arrays) {
2
- if (arrays.length === 0)
3
- return [];
4
- return [...new Set(arrays.reduce((a, b) => a.filter((c) => b.includes(c))))];
1
+ const expressionKeys = /* @__PURE__ */ new Set([
2
+ "$eq",
3
+ "$gt",
4
+ "$gte",
5
+ "$lt",
6
+ "$lte",
7
+ "$in",
8
+ "$nin",
9
+ "$ne",
10
+ "$exists",
11
+ "$not",
12
+ "$expr",
13
+ "$jsonSchema",
14
+ "$mod",
15
+ "$regex",
16
+ "$options",
17
+ "$text",
18
+ "$where",
19
+ "$all",
20
+ "$elemMatch",
21
+ "$size",
22
+ "$bitsAllClear",
23
+ "$bitsAllSet",
24
+ "$bitsAnyClear",
25
+ "$bitsAnySet"
26
+ ]);
27
+ function isFieldExpression(expression) {
28
+ if (typeof expression !== "object" || expression == null) {
29
+ return false;
30
+ }
31
+ const keys = Object.keys(expression);
32
+ if (keys.length === 0) {
33
+ return false;
34
+ }
35
+ const hasInvalidKeys = keys.some((key) => !expressionKeys.has(key));
36
+ if (hasInvalidKeys)
37
+ return false;
38
+ const hasValidKeys = keys.every((key) => expressionKeys.has(key));
39
+ return hasValidKeys;
5
40
  }
6
41
  export {
7
- intersection as default
42
+ isFieldExpression as default
8
43
  };
package/dist/index33.mjs CHANGED
@@ -1,43 +1,6 @@
1
- const expressionKeys = /* @__PURE__ */ new Set([
2
- "$eq",
3
- "$gt",
4
- "$gte",
5
- "$lt",
6
- "$lte",
7
- "$in",
8
- "$nin",
9
- "$ne",
10
- "$exists",
11
- "$not",
12
- "$expr",
13
- "$jsonSchema",
14
- "$mod",
15
- "$regex",
16
- "$options",
17
- "$text",
18
- "$where",
19
- "$all",
20
- "$elemMatch",
21
- "$size",
22
- "$bitsAllClear",
23
- "$bitsAllSet",
24
- "$bitsAnyClear",
25
- "$bitsAnySet"
26
- ]);
27
- function isFieldExpression(expression) {
28
- if (typeof expression !== "object" || expression == null) {
29
- return false;
30
- }
31
- const keys = Object.keys(expression);
32
- if (keys.length === 0) {
33
- return false;
34
- }
35
- const hasInvalidKeys = keys.some((key) => !expressionKeys.has(key));
36
- if (hasInvalidKeys)
37
- return false;
38
- const hasValidKeys = keys.every((key) => expressionKeys.has(key));
39
- return hasValidKeys;
1
+ function createIndexProvider(definition) {
2
+ return definition;
40
3
  }
41
4
  export {
42
- isFieldExpression as default
5
+ createIndexProvider as default
43
6
  };
@@ -4,7 +4,7 @@
4
4
  * @template T - The type of the value to clone.
5
5
  * @param value - The value to deep clone.
6
6
  * @returns A deep copy of the provided value.
7
- * @throws An error if the value is a function, as cloning functions is not supported.
7
+ * @throws {Error} An error if the value is a function, as cloning functions is not supported.
8
8
  */
9
9
  export declare function clone<T>(value: T): T;
10
10
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaldb/core",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0-beta.3",
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",
@@ -55,9 +55,9 @@
55
55
  ],
56
56
  "dependencies": {
57
57
  "fast-sort": "^3.4.1",
58
- "mingo": "^6.5.1"
58
+ "mingo": "^7.1.1"
59
59
  },
60
60
  "devDependencies": {
61
- "zod": "^4.0.14"
61
+ "zod": "^4.3.5"
62
62
  }
63
63
  }