@signaldb/svelte 2.0.0-beta.11 → 2.0.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -281,8 +281,13 @@ class AsyncDataAdapter {
281
281
  return;
282
282
  rec.state = state;
283
283
  rec.error = error;
284
- // notify subscribers
285
- for (const callback of rec.listeners) {
284
+ // Notify over a snapshot, never the live set: a subscriber is free to
285
+ // resubscribe from inside its own callback a reactive scope that reads
286
+ // the query state does exactly that when it re-runs — and `Set` iteration
287
+ // visits entries added while it is still running, so notifying in place
288
+ // turns one state change into an unbounded loop.
289
+ const subscribers = [...rec.listeners];
290
+ for (const callback of subscribers) {
286
291
  try {
287
292
  callback(state);
288
293
  }
@@ -7,9 +7,26 @@ import type { ObserveCallbacks } from './Observer';
7
7
  * @returns A boolean indicating if the current scope is reactive.
8
8
  */
9
9
  export declare function isInReactiveScope(reactivity: ReactivityAdapter | undefined | false): boolean;
10
+ /**
11
+ * Reports whether the query a cursor stands for has produced an outcome yet.
12
+ * Supplied by the collection, which owns the query registration; a cursor
13
+ * built without one simply never reports itself as loading.
14
+ */
15
+ export interface QueryStateAccessor {
16
+ /**
17
+ * Whether the query has settled — completed or failed — at least once
18
+ * since it was registered.
19
+ */
20
+ hasSettled: () => boolean;
21
+ /**
22
+ * Subscribes to the query settling. Returns a cleanup function.
23
+ */
24
+ onSettled: (callback: () => void) => () => void;
25
+ }
10
26
  export interface CursorOptions<T extends BaseItem, U = T, Async extends boolean = false> extends FindOptions<T, Async> {
11
27
  transform?: Transform<T, U>;
12
28
  bindEvents?: (requery: () => void) => () => void;
29
+ queryState?: QueryStateAccessor;
13
30
  }
14
31
  /**
15
32
  * Represents a cursor for querying and observing a filtered, sorted, and transformed
@@ -90,6 +107,31 @@ export default class Cursor<T extends BaseItem, U = T, Async extends boolean = f
90
107
  * @returns The total number of items in the result set.
91
108
  */
92
109
  count(): Async extends true ? Promise<number> : number;
110
+ /**
111
+ * Whether this cursor's query has yet to deliver a first result.
112
+ * ⚡️ this function is reactive!
113
+ *
114
+ * An asynchronous data adapter answers a newly registered query only after a
115
+ * round trip, and serves a neutral empty result until it does — which a
116
+ * consumer cannot otherwise tell apart from "there is nothing to show". This
117
+ * is that distinction, per query rather than per collection, so one screen
118
+ * waiting on its own data says nothing about any other query.
119
+ *
120
+ * Follows the usual `isLoading`/`isFetching` split: it reports "no result
121
+ * yet", not "an execution is in flight". A write that re-runs an
122
+ * already-settled query drives it through `'active'` again while this stays
123
+ * `false`, so a list does not fall back to a loading state every time one of
124
+ * its rows changes.
125
+ *
126
+ * A query that fails counts as settled — the `query.error` event on the
127
+ * collection is what surfaces the failure, and a loading state that never
128
+ * ends is the worse answer. Reading this registers the query if nothing else
129
+ * has, so it cannot wait on something nobody asked for. It is always `false`
130
+ * for an `{ async: true }` cursor, whose `fetch()` awaits the real result
131
+ * anyway, and for a data adapter that answers synchronously.
132
+ * @returns A boolean indicating whether the first result is still pending.
133
+ */
134
+ isLoading(): boolean;
93
135
  /**
94
136
  * Observes changes to the cursor's result set and triggers the specified callbacks
95
137
  * when items are added, removed, or updated. Supports reactivity and transformation.
@@ -79,7 +79,7 @@ class Cursor {
79
79
  return item;
80
80
  return this.options.transform(item);
81
81
  }
82
- depend(changeEvents) {
82
+ depend(changeEvents, bindExtraNotifier) {
83
83
  if (this.options?.async)
84
84
  return;
85
85
  if (!isInReactiveScope(this.options.reactive)) {
@@ -124,6 +124,17 @@ class Cursor {
124
124
  this.options.reactive.onDispose(() => stop(), signal);
125
125
  }
126
126
  this.onCleanup(stop);
127
+ // A notifier that isn't driven by the result set itself — `isLoading()`
128
+ // needs to re-run its scope when the query settles, which is precisely
129
+ // the moment the result set may *not* have changed (an empty query
130
+ // completing produces no diff, so the observer above stays silent).
131
+ if (!bindExtraNotifier)
132
+ return;
133
+ const stopExtraNotifier = bindExtraNotifier(notify);
134
+ if (this.options.reactive.onDispose) {
135
+ this.options.reactive.onDispose(() => stopExtraNotifier(), signal);
136
+ }
137
+ this.onCleanup(stopExtraNotifier);
127
138
  }
128
139
  ensureObserver() {
129
140
  if (!this.observer) {
@@ -247,6 +258,37 @@ class Cursor {
247
258
  ? maybePromise.then(items => items.length)
248
259
  : maybePromise.length);
249
260
  }
261
+ /**
262
+ * Whether this cursor's query has yet to deliver a first result.
263
+ * ⚡️ this function is reactive!
264
+ *
265
+ * An asynchronous data adapter answers a newly registered query only after a
266
+ * round trip, and serves a neutral empty result until it does — which a
267
+ * consumer cannot otherwise tell apart from "there is nothing to show". This
268
+ * is that distinction, per query rather than per collection, so one screen
269
+ * waiting on its own data says nothing about any other query.
270
+ *
271
+ * Follows the usual `isLoading`/`isFetching` split: it reports "no result
272
+ * yet", not "an execution is in flight". A write that re-runs an
273
+ * already-settled query drives it through `'active'` again while this stays
274
+ * `false`, so a list does not fall back to a loading state every time one of
275
+ * its rows changes.
276
+ *
277
+ * A query that fails counts as settled — the `query.error` event on the
278
+ * collection is what surfaces the failure, and a loading state that never
279
+ * ends is the worse answer. Reading this registers the query if nothing else
280
+ * has, so it cannot wait on something nobody asked for. It is always `false`
281
+ * for an `{ async: true }` cursor, whose `fetch()` awaits the real result
282
+ * anyway, and for a data adapter that answers synchronously.
283
+ * @returns A boolean indicating whether the first result is still pending.
284
+ */
285
+ isLoading() {
286
+ const queryState = this.options.queryState;
287
+ if (!queryState || this.options.async)
288
+ return false;
289
+ this.depend({}, notify => queryState.onSettled(notify));
290
+ return !queryState.hasSettled();
291
+ }
250
292
  /**
251
293
  * Observes changes to the cursor's result set and triggers the specified callbacks
252
294
  * when items are added, removed, or updated. Supports reactivity and transformation.
@@ -8,7 +8,7 @@ import type StorageAdapter from '../types/StorageAdapter';
8
8
  import Cursor from './Cursor';
9
9
  import type { AsyncFindOptions, BaseItem, FindOptions, SyncFindOptions, Transform, TransformAll } from './types';
10
10
  export type { AnyFindOptions, AsyncFindOptions, BaseItem, Transform, TransformAll, SortSpecifier, FieldSpecifier, FindOptions, SyncFindOptions, } from './types';
11
- export type { CursorOptions } from './Cursor';
11
+ export type { CursorOptions, QueryStateAccessor } from './Cursor';
12
12
  export type { ObserveCallbacks } from './Observer';
13
13
  export { default as createIndex } from '../createIndex';
14
14
  export interface CollectionOptions<T extends BaseItem<I>, I, E extends BaseItem = T, U = E> {
@@ -110,6 +110,7 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, E ext
110
110
  private postBatchCallbacks;
111
111
  private fieldTracking;
112
112
  private queryListenersMap;
113
+ private settledQueriesSet;
113
114
  /**
114
115
  * Initializes a new instance of the `Collection` class with optional configuration.
115
116
  * Sets up memory, persistence, reactivity, and indices as specified in the options.
@@ -102,6 +102,15 @@ class Collection extends EventEmitter_1.default {
102
102
  postBatchCallbacks = new Set();
103
103
  fieldTracking = false;
104
104
  queryListenersMap = new Map();
105
+ // Which registered queries have delivered an outcome at least once, backing
106
+ // `Cursor#isLoading()`. Kept here rather than on a cursor because a cursor is
107
+ // rebuilt on every reactive re-run, and rather than in the data adapters
108
+ // because deriving it from the state they already publish needs no change to
109
+ // the `CollectionBackend` contract. Keyed and dropped exactly like
110
+ // `queryListenersMap`, so a query that gets unregistered starts out pending
111
+ // again — which is correct, since the adapter re-executes it on the next
112
+ // registration.
113
+ settledQueriesSet = new Set();
105
114
  constructor(nameOrOptions, maybeDataAdapter, maybeOptions) {
106
115
  super();
107
116
  const name = typeof nameOrOptions === 'string'
@@ -310,6 +319,25 @@ class Collection extends EventEmitter_1.default {
310
319
  fieldTracking: this.fieldTracking,
311
320
  ...options,
312
321
  transform: this.transform.bind(this),
322
+ queryState: {
323
+ hasSettled: () => {
324
+ if (this.settledQueriesSet.has((0, queryId_1.default)(selector, options)))
325
+ return true;
326
+ // An adapter that answers synchronously reports `'complete'` from
327
+ // the start, so a cursor over one is never in a loading state.
328
+ const state = this.backend.getQueryState(selector, options || {});
329
+ return state === 'complete' || state === 'error';
330
+ },
331
+ // The latch is set by the very callback that notifies, so a cursor
332
+ // can never be woken to read a state that has not been recorded yet,
333
+ // whatever order the backend runs its subscribers in.
334
+ onSettled: callback => this.backend.onQueryStateChange(selector, options || {}, (state) => {
335
+ if (state !== 'complete' && state !== 'error')
336
+ return;
337
+ this.settledQueriesSet.add((0, queryId_1.default)(selector, options));
338
+ callback();
339
+ }),
340
+ },
313
341
  bindEvents: (requery) => {
314
342
  const handleRequery = () => {
315
343
  if (this.batchOperationInProgress) {
@@ -351,8 +379,10 @@ class Collection extends EventEmitter_1.default {
351
379
  // Only unregister if this observer was the one that registered
352
380
  // This prevents race conditions where a new observer registers
353
381
  // before the old one's cleanup runs
354
- if (newListeners === 0 && didRegister)
382
+ if (newListeners === 0 && didRegister) {
355
383
  this.backend.unregisterQuery(selector, options || {});
384
+ this.settledQueriesSet.delete((0, queryId_1.default)(selector, options));
385
+ }
356
386
  this.queryListeners({ selector, options }, newListeners);
357
387
  queryStateChangeCleanup();
358
388
  this.emit('observer.disposed', selector, options);
@@ -22,8 +22,28 @@ export default class WorkerDataAdapter implements DataAdapter {
22
22
  private collectionReady;
23
23
  private batchExecutionHelpers;
24
24
  private queries;
25
+ private pendingWrites;
26
+ private pendingWriteSeq;
25
27
  constructor(worker: WorkerDataAdapterEndpoint, options: WorkerDataAdapterOptions);
26
28
  private exec;
29
+ private observableItems;
30
+ private mergePendingWrites;
31
+ /**
32
+ * Registers a write's effect locally and notifies every active query it
33
+ * touches, then returns a function that drops it again once the write
34
+ * settles.
35
+ * @param collectionName - The collection the write targets.
36
+ * @param upserts - Items inserted or updated by the write.
37
+ * @param deletes - Ids removed by the write.
38
+ * @returns A function that drops the pending write and re-notifies.
39
+ */
40
+ private applyPendingWrite;
41
+ private notifyAffectedQueries;
42
+ private matchObservableItems;
43
+ private resolveUpdate;
44
+ private resolveRemoval;
45
+ private resolveReplacement;
46
+ private withPendingWrite;
27
47
  private enqueueBatched;
28
48
  private updateQuery;
29
49
  createCollectionBackend<T extends BaseItem<I>, I = any, E extends BaseItem = T, U = E>(collection: Collection<T, I, E, U>, indices: string[]): CollectionBackend<T, I>;
@@ -6,6 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const queryId_1 = __importDefault(require("./utils/queryId"));
7
7
  const randomId_1 = __importDefault(require("./utils/randomId"));
8
8
  const batchOnNextTick_1 = __importDefault(require("./utils/batchOnNextTick"));
9
+ const applyQueryOptions_1 = __importDefault(require("./utils/applyQueryOptions"));
10
+ const match_1 = __importDefault(require("./utils/match"));
11
+ const modify_1 = __importDefault(require("./utils/modify"));
12
+ const deepClone_1 = __importDefault(require("./utils/deepClone"));
9
13
  class WorkerDataAdapter {
10
14
  worker;
11
15
  options;
@@ -16,6 +20,19 @@ class WorkerDataAdapter {
16
20
  collectionReady = new Map();
17
21
  batchExecutionHelpers = new Map();
18
22
  queries = {};
23
+ // Writes that have been issued but not yet confirmed by the worker. Their
24
+ // effect is layered on top of each active query's last authoritative result
25
+ // in `getQueryResult`, so a cursor reflects a write immediately instead of
26
+ // waiting out the postMessage → SQLite write → re-executed query →
27
+ // postMessage round trip. Deliberately scoped to items that active queries
28
+ // already hold: this adapter must not turn into a full in-memory mirror of
29
+ // the backing store, and anything no active query holds isn't observable
30
+ // anyway. Dropped again once the write settles — on success the worker's
31
+ // own `queryUpdate` has already landed (it is posted before the write's
32
+ // response, and message order is preserved), on failure dropping it is the
33
+ // rollback.
34
+ pendingWrites = new Map();
35
+ pendingWriteSeq = 0;
19
36
  constructor(worker, options) {
20
37
  this.worker = worker;
21
38
  this.options = options;
@@ -78,6 +95,117 @@ class WorkerDataAdapter {
78
95
  });
79
96
  });
80
97
  }
98
+ // The items an active query currently holds, deduplicated by id — the only
99
+ // items this adapter knows about, and the set a selector-based write can be
100
+ // resolved against locally.
101
+ observableItems(collectionName) {
102
+ const byId = new Map();
103
+ this.queries[collectionName]?.forEach((query) => {
104
+ this.mergePendingWrites(collectionName, query.items).forEach((item) => {
105
+ byId.set(item.id, item);
106
+ });
107
+ });
108
+ return [...byId.values()];
109
+ }
110
+ mergePendingWrites(collectionName, items) {
111
+ const pending = this.pendingWrites.get(collectionName);
112
+ if (!pending || pending.size === 0)
113
+ return items;
114
+ const merged = new Map(items.map(item => [item.id, item]));
115
+ [...pending.entries()]
116
+ .sort(([a], [b]) => a - b) // eslint-disable-line unicorn/no-array-sort -- unavailable on Hermes
117
+ .forEach(([, write]) => {
118
+ write.upserts.forEach((item, id) => merged.set(id, item));
119
+ write.deletes.forEach(id => merged.delete(id));
120
+ });
121
+ return [...merged.values()];
122
+ }
123
+ /**
124
+ * Registers a write's effect locally and notifies every active query it
125
+ * touches, then returns a function that drops it again once the write
126
+ * settles.
127
+ * @param collectionName - The collection the write targets.
128
+ * @param upserts - Items inserted or updated by the write.
129
+ * @param deletes - Ids removed by the write.
130
+ * @returns A function that drops the pending write and re-notifies.
131
+ */
132
+ applyPendingWrite(collectionName, upserts, deletes) {
133
+ if (upserts.length === 0 && deletes.length === 0)
134
+ return () => { };
135
+ const seq = this.pendingWriteSeq += 1;
136
+ const pending = this.pendingWrites.get(collectionName)
137
+ ?? new Map();
138
+ pending.set(seq, {
139
+ upserts: new Map(upserts.map(item => [item.id, item])),
140
+ deletes: new Set(deletes),
141
+ });
142
+ this.pendingWrites.set(collectionName, pending);
143
+ const affectedIds = new Set([...upserts.map(item => item.id), ...deletes]);
144
+ this.notifyAffectedQueries(collectionName, upserts, affectedIds);
145
+ return () => {
146
+ const current = this.pendingWrites.get(collectionName);
147
+ if (!current)
148
+ return;
149
+ current.delete(seq);
150
+ if (current.size === 0)
151
+ this.pendingWrites.delete(collectionName);
152
+ this.notifyAffectedQueries(collectionName, upserts, affectedIds);
153
+ };
154
+ }
155
+ // Re-runs the state-change callbacks of every query whose result the write
156
+ // can have changed — either because a written item matches its selector, or
157
+ // because it already held one of the affected items (an update that moves an
158
+ // item out of a query, or a removal).
159
+ notifyAffectedQueries(collectionName, upserts, affectedIds) {
160
+ this.queries[collectionName]?.forEach((query) => {
161
+ const wasHolding = query.items.some(item => affectedIds.has(item.id));
162
+ const nowMatches = upserts.some(item => query.selector != null
163
+ && (0, match_1.default)(item, query.selector));
164
+ if (!wasHolding && !nowMatches)
165
+ return;
166
+ query.stateChangeCallbacks.forEach(callback => callback(query.state));
167
+ });
168
+ }
169
+ matchObservableItems(collectionName, selector, onlyFirst) {
170
+ if (selector == null)
171
+ return [];
172
+ const matches = this.observableItems(collectionName).filter(item => (0, match_1.default)(item, selector));
173
+ return onlyFirst ? matches.slice(0, 1) : matches;
174
+ }
175
+ resolveUpdate(collectionName, selector, modifier, onlyFirst) {
176
+ const { $setOnInsert, ...restModifier } = modifier;
177
+ const upserts = [];
178
+ const deletes = [];
179
+ this.matchObservableItems(collectionName, selector, onlyFirst).forEach((item) => {
180
+ const modifiedItem = (0, modify_1.default)((0, deepClone_1.default)(item), restModifier);
181
+ upserts.push(modifiedItem);
182
+ if (modifiedItem.id !== item.id)
183
+ deletes.push(item.id);
184
+ });
185
+ return { upserts, deletes };
186
+ }
187
+ resolveRemoval(collectionName, selector, onlyFirst) {
188
+ return this.matchObservableItems(collectionName, selector, onlyFirst).map(item => item.id);
189
+ }
190
+ resolveReplacement(item, replacement) {
191
+ const modifiedItem = { ...replacement, id: replacement.id ?? item.id };
192
+ return {
193
+ upserts: [modifiedItem],
194
+ deletes: modifiedItem.id === item.id ? [] : [item.id],
195
+ };
196
+ }
197
+ // Makes a write's effect visible to active queries for the duration of the
198
+ // round trip, and drops it again once the worker has spoken — see
199
+ // `pendingWrites`.
200
+ async withPendingWrite(collectionName, delta, run) {
201
+ const dropPendingWrite = this.applyPendingWrite(collectionName, delta.upserts, delta.deletes);
202
+ try {
203
+ return await run();
204
+ }
205
+ finally {
206
+ dropPendingWrite();
207
+ }
208
+ }
81
209
  enqueueBatched(collectionName, method, args) {
82
210
  const helper = this.batchExecutionHelpers.get(collectionName);
83
211
  if (!helper)
@@ -92,6 +220,8 @@ class WorkerDataAdapter {
92
220
  return;
93
221
  const existing = collectionQueries.get(id);
94
222
  const newState = {
223
+ selector: query.selector,
224
+ options: query.options,
95
225
  state: 'active',
96
226
  error: null,
97
227
  items: [],
@@ -110,22 +240,25 @@ class WorkerDataAdapter {
110
240
  this.batchExecutionHelpers.set(collection.name, (0, batchOnNextTick_1.default)(async (method, args) => this.exec(method, collection.name, args)));
111
241
  return {
112
242
  insert: async (item) => {
113
- return this.enqueueBatched(collection.name, 'insert', [item]);
243
+ return this.withPendingWrite(collection.name, { upserts: [item], deletes: [] }, () => this.enqueueBatched(collection.name, 'insert', [item]));
114
244
  },
115
245
  updateOne: async (selector, modifier) => {
116
- return this.enqueueBatched(collection.name, 'updateOne', [selector, modifier]);
246
+ return this.withPendingWrite(collection.name, this.resolveUpdate(collection.name, selector, modifier, true), () => this.enqueueBatched(collection.name, 'updateOne', [selector, modifier]));
117
247
  },
118
248
  updateMany: async (selector, modifier) => {
119
- return this.enqueueBatched(collection.name, 'updateMany', [selector, modifier]);
249
+ return this.withPendingWrite(collection.name, this.resolveUpdate(collection.name, selector, modifier, false), () => this.enqueueBatched(collection.name, 'updateMany', [selector, modifier]));
120
250
  },
121
251
  replaceOne: async (selector, replacement) => {
122
- return this.enqueueBatched(collection.name, 'replaceOne', [selector, replacement]);
252
+ const [item] = this.matchObservableItems(collection.name, selector, true);
253
+ return this.withPendingWrite(collection.name, item == null
254
+ ? { upserts: [], deletes: [] }
255
+ : this.resolveReplacement(item, replacement), () => this.enqueueBatched(collection.name, 'replaceOne', [selector, replacement]));
123
256
  },
124
257
  removeOne: async (selector) => {
125
- return this.enqueueBatched(collection.name, 'removeOne', [selector]);
258
+ return this.withPendingWrite(collection.name, { upserts: [], deletes: this.resolveRemoval(collection.name, selector, true) }, () => this.enqueueBatched(collection.name, 'removeOne', [selector]));
126
259
  },
127
260
  removeMany: async (selector) => {
128
- return this.enqueueBatched(collection.name, 'removeMany', [selector]);
261
+ return this.withPendingWrite(collection.name, { upserts: [], deletes: this.resolveRemoval(collection.name, selector, false) }, () => this.enqueueBatched(collection.name, 'removeMany', [selector]));
129
262
  },
130
263
  // methods for registering and unregistering queries that will be called from the collection during find/findOne
131
264
  registerQuery: (selector, options) => {
@@ -176,7 +309,12 @@ class WorkerDataAdapter {
176
309
  },
177
310
  getQueryResult: (selector, options) => {
178
311
  const query = this.queries[collection.name]?.get((0, queryId_1.default)(selector, options));
179
- return query?.items || [];
312
+ if (!query)
313
+ return [];
314
+ const pending = this.pendingWrites.get(collection.name);
315
+ if (!pending || pending.size === 0)
316
+ return query.items;
317
+ return (0, applyQueryOptions_1.default)(this.mergePendingWrites(collection.name, query.items), selector, options);
180
318
  },
181
319
  onQueryStateChange: (selector, options, callback) => {
182
320
  this.updateQuery(collection.name, { selector, options }, {
@@ -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, TransformAll, SortSpecifier, FieldSpecifier, AnyFindOptions, AsyncFindOptions, FindOptions, SyncFindOptions, CollectionOptions, } from './Collection';
5
+ export type { BaseItem, ObserveCallbacks, CursorOptions, Transform, TransformAll, SortSpecifier, FieldSpecifier, AnyFindOptions, AsyncFindOptions, FindOptions, SyncFindOptions, CollectionOptions, QueryStateAccessor, } 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';
@@ -0,0 +1,15 @@
1
+ import type { QueryOptions } from '../DataAdapter';
2
+ import type Selector from '../types/Selector';
3
+ import type { BaseItem } from '../Collection/types';
4
+ /**
5
+ * Filters, sorts, paginates and projects a plain in-memory array the same way
6
+ * DefaultDataAdapter and WorkerDataAdapterHost apply a selector/QueryOptions
7
+ * pair to their stored items. Used to re-derive a query's result locally after
8
+ * a write, without asking the backing store again.
9
+ * @template T - The type of the items.
10
+ * @param items - The items to filter, sort, paginate and project.
11
+ * @param selector - The selector to match items against.
12
+ * @param options - Sort, skip, limit and field projection options.
13
+ * @returns The resulting items.
14
+ */
15
+ export default function applyQueryOptions<T extends BaseItem>(items: T[], selector: Selector<T>, options?: QueryOptions<T>): T[];
@@ -0,0 +1,36 @@
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.default = applyQueryOptions;
7
+ const match_1 = __importDefault(require("./match"));
8
+ const sortItems_1 = __importDefault(require("./sortItems"));
9
+ const project_1 = __importDefault(require("./project"));
10
+ /**
11
+ * Filters, sorts, paginates and projects a plain in-memory array the same way
12
+ * DefaultDataAdapter and WorkerDataAdapterHost apply a selector/QueryOptions
13
+ * pair to their stored items. Used to re-derive a query's result locally after
14
+ * a write, without asking the backing store again.
15
+ * @template T - The type of the items.
16
+ * @param items - The items to filter, sort, paginate and project.
17
+ * @param selector - The selector to match items against.
18
+ * @param options - Sort, skip, limit and field projection options.
19
+ * @returns The resulting items.
20
+ */
21
+ function applyQueryOptions(items, selector, options) {
22
+ const matched = selector == null ? [] : items.filter(item => (0, match_1.default)(item, selector));
23
+ const { sort, skip, limit, fields } = options || {};
24
+ const sorted = sort ? (0, sortItems_1.default)(matched, sort) : matched;
25
+ const skipped = skip ? sorted.slice(skip) : sorted;
26
+ const limited = limit ? skipped.slice(0, limit) : skipped;
27
+ const idExcluded = fields && fields.id === 0;
28
+ return limited.map((item) => {
29
+ if (!fields)
30
+ return item;
31
+ return {
32
+ ...idExcluded ? {} : { id: item.id },
33
+ ...(0, project_1.default)(item, fields),
34
+ };
35
+ });
36
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@signaldb/svelte",
3
3
  "type": "module",
4
- "version": "2.0.0-beta.11",
4
+ "version": "2.0.0-beta.13",
5
5
  "scripts": {
6
6
  "build": "tsc -d --noEmit false",
7
7
  "analyze-bundle": "bundle-analyzer ./dist --upload-token=$BUNDLE_ANALYZER_UPLOAD_TOKEN --bundle-name=@signaldb/svelte",