@livequery/client 2.0.135 → 2.0.139

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/README.md CHANGED
@@ -24,6 +24,7 @@ bun add @livequery/client @livequery/react rxjs
24
24
  export * from "./LivequeryCollection"
25
25
  export * from "./LivequeryClient"
26
26
  export * from "./LivequeryMemoryStorage"
27
+ export * from "./LivequeryStorage"
27
28
  export * from "./LivequeryStorge"
28
29
  export * from "./LivequeryTransporter"
29
30
  export * from "./types"
@@ -31,7 +32,7 @@ export * from "./helpers/filterDocs"
31
32
  export * from "./LivequeryDocument"
32
33
  ```
33
34
 
34
- The public storage interface is intentionally named `LivequeryStorge`. The spelling is part of the current public API.
35
+ The public storage interface is `LivequeryStorage`. The previous misspelled name `LivequeryStorge` remains exported as a backward-compatible alias.
35
36
 
36
37
  ## Mental Model
37
38
 
@@ -42,13 +43,13 @@ LivequeryCollection / LivequeryDocument
42
43
  LivequeryClient
43
44
  / \
44
45
  v v
45
- LivequeryStorge LivequeryTransporter(s)
46
+ LivequeryStorage LivequeryTransporter(s)
46
47
  ```
47
48
 
48
49
  - `LivequeryClient` is the coordination core. It owns collection registrations, query orchestration, transporter fan-out, local storage writes, broadcast delivery, and optimistic mutation reconciliation.
49
50
  - `LivequeryCollection<T>` is the main consumer-facing list or document wrapper. It exposes reactive subjects such as `items`, `loading`, `filters`, `paging`, `summary`, and `error`.
50
51
  - `LivequeryDocument<T>` wraps one document in a `BehaviorSubject` and forwards `update`, `del`, `trigger`, and `select` calls to its collection.
51
- - `LivequeryStorge` is the local persistence contract used by the client.
52
+ - `LivequeryStorage` is the local persistence contract used by the client. `LivequeryStorge` is a backward-compatible alias.
52
53
  - `LivequeryMemoryStorage` is the in-memory reference storage adapter.
53
54
  - `LivequeryTransporter` is the remote sync/action contract.
54
55
 
@@ -143,7 +144,7 @@ const subscription = todos.items.subscribe((items) => {
143
144
 
144
145
  await todos.query({
145
146
  ":limit": 20,
146
- "done:boolean": "false",
147
+ "done:eq-boolean": "false",
147
148
  "createdAt:sort": "desc",
148
149
  })
149
150
 
@@ -210,6 +211,22 @@ type DocState<T extends Doc> = T & {
210
211
 
211
212
  Do not strip `_adding`, `_updating`, `_deleting`, or error fields if your UI needs to show mutation progress or failure state.
212
213
 
214
+ Field reference:
215
+
216
+ | Field | Set when | Meaning |
217
+ |---|---|---|
218
+ | `_adding` | `local-first` / `local-only` add in progress | Document is being created on the server |
219
+ | `_adding_error` | Server add rejected | Error from the failed transporter call |
220
+ | `_updating` | `local-first` update in progress | Document is being synced to the server |
221
+ | `_updating_error` | Server update rejected | Error from the failed transporter call |
222
+ | `_deleting` | `local-first` delete in progress | Document is pending deletion on the server |
223
+ | `_deleting_error` | Server delete rejected | Error from the failed transporter call |
224
+ | `_local_only` | `local-only` add | Document was created locally and never sent to the server |
225
+ | `_prev` | `local-first` update pending | Fields that existed before the local update — used to push only changed fields to the server |
226
+ | `_selected` | `select()` called | Whether the document is currently selected |
227
+ | `_index` | Assigned on insert | Stable insertion order used for sort reset |
228
+ | `_remotes` | Transporter-specific | Optional metadata from transporters; not used by the client core |
229
+
213
230
  ### `DataChangeEvent`
214
231
 
215
232
  Transporter query streams and internal broadcasts use incremental change events:
@@ -247,7 +264,7 @@ new LivequeryClient({
247
264
  })
248
265
  ```
249
266
 
250
- - `storage`: a `LivequeryStorge` adapter.
267
+ - `storage`: a `LivequeryStorage` adapter.
251
268
  - `transporters`: a map of transporter id to `LivequeryTransporter`. Use one transporter for a simple app. Use multiple transporters when the same client should fan out to more than one backend.
252
269
 
253
270
  ### `watch(ref, collection_id, mode)`
@@ -264,7 +281,7 @@ Lower-level query entry point used by `LivequeryCollection.query()`.
264
281
  await client.query<Todo>({
265
282
  ref: "todos",
266
283
  collection_id: todos.id,
267
- filters: { "done:boolean": "false" },
284
+ filters: { "done:eq-boolean": "false" },
268
285
  })
269
286
  ```
270
287
 
@@ -331,7 +348,7 @@ const todos = new LivequeryCollection<Todo>(client, {
331
348
  lazy: false,
332
349
  debounce: 250,
333
350
  filters: {
334
- "done:boolean": "false",
351
+ "done:eq-boolean": "false",
335
352
  "createdAt:sort": "desc",
336
353
  },
337
354
  })
@@ -345,6 +362,10 @@ type LivequeryCollectionOptions<T extends Doc> = {
345
362
  lazy: boolean
346
363
  debounce: number
347
364
  mode: "server-first" | "cache-first" | "local-first" | "local-only"
365
+ seed: {
366
+ data: T[]
367
+ persist: boolean
368
+ }
348
369
  }
349
370
  ```
350
371
 
@@ -352,6 +373,37 @@ type LivequeryCollectionOptions<T extends Doc> = {
352
373
  - `lazy`: when not `true`, `initialize()` schedules an automatic query with current filters.
353
374
  - `debounce`: enables `debounceQuery()`.
354
375
  - `mode`: controls query behavior. Mutation methods still default to `server-first` unless you pass a mode override.
376
+ - `seed`: optional initial data. `seed.data` is an array of documents loaded into `items` before any query runs. `seed.persist: false` populates items in memory only — storage is not written. `seed.persist: true` writes the seed to storage before the first query, so a `cache-first` or `local-first` query can read from it immediately.
377
+
378
+ ### `seed`
379
+
380
+ Pre-populate a collection with data before any query runs. Useful for hardcoded defaults, SSR-hydrated data, or offline stubs.
381
+
382
+ ```ts
383
+ const todos = new LivequeryCollection<Todo>(client, {
384
+ mode: "cache-first",
385
+ seed: {
386
+ data: [
387
+ { id: "1", title: "Buy milk", done: false, createdAt: Date.now() },
388
+ ],
389
+ persist: true,
390
+ },
391
+ })
392
+
393
+ todos.initialize("todos")
394
+ // items.value already has the seeded document before the first query
395
+ ```
396
+
397
+ `persist: false` — items are loaded into `items` immediately in the constructor. Storage is never written. The seed disappears after a query replaces items.
398
+
399
+ `persist: true` — seed data is written to storage before the first auto-query runs. This lets a `cache-first` query read the seed from storage on the first render.
400
+
401
+ Rules:
402
+
403
+ - Seed documents must include `id`.
404
+ - When `persist: true` and `lazy: false`, the client calls `seedToStorage()` then starts the auto-query. The auto-query may overwrite seed items when the transporter responds.
405
+ - When `persist: false`, seed items are available immediately in `items.value` from the constructor but are replaced on the first `query()` call.
406
+ - `seed` has no effect on the mode behavior. The collection still uses the configured mode for queries.
355
407
 
356
408
  ### Reactive Properties
357
409
 
@@ -397,7 +449,7 @@ Runs a query and replaces current `items` when cached/local documents are return
397
449
  ```ts
398
450
  await todos.query({
399
451
  ":limit": 20,
400
- "done:boolean": "false",
452
+ "done:eq-boolean": "false",
401
453
  "createdAt:sort": "desc",
402
454
  })
403
455
  ```
@@ -446,7 +498,7 @@ await todos.loadAround("cursor-123")
446
498
 
447
499
  - `loadMore()` adds `:after`.
448
500
  - `loadPrev()` adds `:before`.
449
- - `loadAround(cursor)` currently sets both `:after` and `:before` to the same cursor.
501
+ - `loadAround(cursor)` loads a page centered on the given cursor. It sets both `:after` and `:before` to the same cursor value — the backend decides what "around a cursor" means for that collection.
450
502
 
451
503
  ### `add(payload, mode?)`
452
504
 
@@ -472,7 +524,42 @@ const many = await todos.add([
472
524
 
473
525
  The return shape follows the input shape: one payload returns one document; an array returns an array.
474
526
 
475
- Important: the current method signature defaults `mode` to `"server-first"`. A collection configured with `mode: "local-only"` still needs `add(payload, "local-only")` if you want the mutation to stay local.
527
+ The default mutation mode follows `#defaultMode()`:
528
+
529
+ | Collection `mode` | Mutation default |
530
+ |---|---|
531
+ | `server-first` | `server-first` |
532
+ | `cache-first` | `server-first` |
533
+ | `local-first` | `local-first` |
534
+ | `local-only` | `local-only` |
535
+ | not set | `server-first` |
536
+
537
+ ```ts
538
+ // local-only collection — mutations also default to local-only
539
+ const drafts = new LivequeryCollection<Todo>(client, { mode: "local-only" })
540
+ drafts.initialize("drafts")
541
+ await drafts.add({ title: "Draft", done: false, createdAt: Date.now() })
542
+ // ✓ stored locally, no server call
543
+
544
+ // local-first collection — optimistic local write + background server sync
545
+ const notes = new LivequeryCollection<Todo>(client, { mode: "local-first" })
546
+ notes.initialize("notes")
547
+ await notes.add({ title: "Note", done: false, createdAt: Date.now() })
548
+ // ✓ appears immediately, syncs to server in background
549
+
550
+ // cache-first collection — mutations go server-first by default
551
+ const posts = new LivequeryCollection<Post>(client, { mode: "cache-first" })
552
+ posts.initialize("posts")
553
+ await posts.add({ title: "Post", done: false, createdAt: Date.now() })
554
+ // ✓ blocks until server responds (server-first default)
555
+ ```
556
+
557
+ Pass an explicit mode to override the default on a per-call basis:
558
+
559
+ ```ts
560
+ await drafts.add({ title: "Force server", done: false, createdAt: Date.now() }, "server-first")
561
+ await posts.add({ title: "Local draft", done: false, createdAt: Date.now() }, "local-only")
562
+ ```
476
563
 
477
564
  ### `update(payload, mode?)`
478
565
 
@@ -546,14 +633,32 @@ todos.resetError()
546
633
 
547
634
  Watches pairwise document changes and emits when `check(prev, next)` returns `true`.
548
635
 
636
+ Returns `Observable<[DocState<T>, DocState<T>]>` — each emission is a `[previous, current]` pair for the document that changed.
637
+
549
638
  ```ts
550
- const doneSub = todos.watch((prev, next) => prev.done !== next.done).subscribe(([prev, next]) => {
551
- console.log(prev.id, "done changed", prev.done, "=>", next.done)
552
- })
639
+ // Watch when the `done` field changes on any todo
640
+ const doneSub = todos.watch((prev, next) => prev.done !== next.done)
641
+ .subscribe(([prev, next]) => {
642
+ console.log(next.id, "done changed:", prev.done, "→", next.done)
643
+ })
553
644
 
554
645
  doneSub.unsubscribe()
646
+
647
+ // Watch when any field changes
648
+ const anySub = todos.watch((prev, next) => prev !== next).subscribe(([prev, next]) => {
649
+ console.log("document changed:", next.id)
650
+ })
651
+
652
+ // Watch for optimistic mutation completion
653
+ const saveSub = todos.watch(
654
+ (prev, next) => prev._updating === true && next._updating == null
655
+ ).subscribe(([, next]) => {
656
+ console.log("save confirmed for:", next.id)
657
+ })
555
658
  ```
556
659
 
660
+ `check` is called for every field emission of every document in `items`. Keep it fast. Avoid closures that capture large objects.
661
+
557
662
  ### `flush()`
558
663
 
559
664
  Flushes storage through the client for this collection's `collection_ref`.
@@ -610,12 +715,12 @@ first.select(true)
610
715
  first.select(false)
611
716
  ```
612
717
 
613
- ## `LivequeryStorge`
718
+ ## `LivequeryStorage`
614
719
 
615
720
  Storage adapters provide local persistence and local filtering.
616
721
 
617
722
  ```ts
618
- type LivequeryStorge = {
723
+ type LivequeryStorage = {
619
724
  query<T extends Doc>(
620
725
  collection: string,
621
726
  filters?: Record<string, any>
@@ -631,6 +736,8 @@ type LivequeryStorge = {
631
736
  }
632
737
  ```
633
738
 
739
+ `LivequeryStorge` is still exported as an alias for existing consumers.
740
+
634
741
  Adapter guidance:
635
742
 
636
743
  - `query()` should apply the same filter semantics as `filterDocs()` when possible.
@@ -654,7 +761,7 @@ await storage.add<Todo>("todos", {
654
761
  })
655
762
 
656
763
  const page = await storage.query<Todo>("todos", {
657
- "done:boolean": "false",
764
+ "done:eq-boolean": "false",
658
765
  "createdAt:sort": "desc",
659
766
  })
660
767
  ```
@@ -753,27 +860,38 @@ const apiTransporter: LivequeryTransporter = {
753
860
 
754
861
  ## Query Modes
755
862
 
863
+ > For detailed data flow diagrams, mutation behavior per mode, and common mistakes, see [docs/modes.md](./docs/modes.md).
864
+
865
+ | Mode | Query reads from | Transporter called? | Mutation default |
866
+ |------|------|------|------|
867
+ | `server-first` | Transporter | Yes, always | `server-first` |
868
+ | `cache-first` | Storage first, then transporter | Yes, in background | `server-first` |
869
+ | `local-first` | Storage immediately | Yes, full sync in background | `local-first` |
870
+ | `local-only` | Storage only | No | `local-only` |
871
+
756
872
  ### `server-first`
757
873
 
758
- Transporters drive the query result. Collection state is built from streamed change events.
874
+ Transporters drive the query result. Collection state is built from streamed change events. Items are delivered asynchronously through the watch stream, not from the `query()` return value.
759
875
 
760
876
  Use it when remote data is the source of truth and local cache is secondary.
761
877
 
762
878
  ### `cache-first`
763
879
 
764
- The first query can hydrate from storage, then transporters refresh in the background.
880
+ The first query hydrates from local storage instantly, then transporters refresh in the background. For pagination queries (`:before` / `:after`), cache is skipped and the transporter is called directly. Mutations default to `server-first`.
765
881
 
766
882
  Use it when fast initial UI matters but remote sync should still run.
767
883
 
768
884
  ### `local-first`
769
885
 
770
- Storage serves the query immediately. Transporters refresh in the background. Remote query changes are written into storage and then broadcast back into matching local collections.
886
+ Storage serves the query immediately. The transporter syncs the full collection in the background by paginating all pages and writing results to storage. Remote changes are rebroadcast to local collections filtered by their current filters.
771
887
 
772
- For `local-first`, the remote query path receives empty filters. Local filtering is enforced by storage query results and by broadcast filtering.
888
+ The server receives **empty filters** local filtering happens during broadcast, not on the server.
889
+
890
+ Avoid this mode for large unbounded datasets; it attempts to sync every document locally.
773
891
 
774
892
  ### `local-only`
775
893
 
776
- Queries resolve only from storage and skip transporters. Mutations stay local only when you explicitly call them with `mode: "local-only"`.
894
+ Queries resolve only from storage. Transporters are never called. No loading state is emitted. Mutations stay local when explicitly called with `mode: "local-only"`.
777
895
 
778
896
  Use it for drafts, temporary UI state, offline-only collections, or local workspaces.
779
897
 
@@ -821,11 +939,13 @@ Filters are flat object keys derived from document fields.
821
939
  - `field:sort`: `"asc" | "desc"`
822
940
  - `field:gt`, `field:gte`, `field:lt`, `field:lte`: numeric comparisons
823
941
  - `field:eq-number`: numeric equality after `Number(value)`
942
+ - `field:neq-number`: numeric inequality after `Number(value)`
824
943
  - `field:in`, `field:nin`: membership for string or number values
825
- - `field:include`: array contains value
826
- - `field:boolean`: `"true" | "false" | "not-true" | "not-false"`
827
- - `field:like`: case-insensitive substring check
828
- - `field:null`: `"null-only" | "not-null"`
944
+ - `field:ne`: inequality
945
+ - `field:eq-boolean`, `field:neq-boolean`: boolean equality or inequality
946
+ - `field:eq-null`, `field:neq-null`: null equality or inequality
947
+ - `field:eq-oid`, `field:neq-oid`: ObjectId string equality or inequality for MongoDB-backed datasources
948
+ - `field:like`: regular expression match
829
949
 
830
950
  Nested field paths are supported:
831
951
 
@@ -833,7 +953,7 @@ Nested field paths are supported:
833
953
  await posts.query({
834
954
  "author.id": "user-1",
835
955
  "stats.views:gte": 100,
836
- "published:boolean": "true",
956
+ "published:eq-boolean": "true",
837
957
  "title:like": "livequery",
838
958
  "createdAt:sort": "desc",
839
959
  })
@@ -849,7 +969,7 @@ Filters an array with the same runtime semantics used by `LivequeryMemoryStorage
849
969
  import { filterDocs } from "@livequery/client"
850
970
 
851
971
  const openTodos = filterDocs(todos, {
852
- "done:boolean": "false",
972
+ "done:eq-boolean": "false",
853
973
  })
854
974
  ```
855
975
 
@@ -860,11 +980,57 @@ Predicate helper for checking one document.
860
980
  ```ts
861
981
  import { matchesAllFilters } from "@livequery/client"
862
982
 
863
- if (matchesAllFilters(todo, { "done:boolean": "false" })) {
983
+ if (matchesAllFilters(todo, { "done:eq-boolean": "false" })) {
864
984
  console.log("todo is open")
865
985
  }
866
986
  ```
867
987
 
988
+ ### `parseFilters(filters)`
989
+
990
+ Pre-parses a filter object into a `ParsedFilter[]` array. Call this once per query rather than calling `matchesAllFilters` in a tight loop.
991
+
992
+ ```ts
993
+ import { parseFilters, matchesParsedFilters } from "@livequery/client"
994
+
995
+ const filters = { "done:eq-boolean": "false", "createdAt:sort": "desc" }
996
+ const parsed = parseFilters(filters)
997
+
998
+ // Efficient: parse once, match many
999
+ const openTodos = todos.filter(doc => matchesParsedFilters(doc, parsed))
1000
+ ```
1001
+
1002
+ Pagination keys (`:limit`, `:before`, `:after`, `:around`, `:page`) and sort keys (`:sort` suffix) are excluded from the returned array.
1003
+
1004
+ ### `matchesParsedFilters(doc, parsedFilters)`
1005
+
1006
+ Matches one document against a pre-parsed `ParsedFilter[]`. Use together with `parseFilters()` when checking many documents against the same filters.
1007
+
1008
+ ```ts
1009
+ const parsed = parseFilters({ "status": "active", "score:gte": 10 })
1010
+
1011
+ for (const doc of largeList) {
1012
+ if (matchesParsedFilters(doc, parsed)) {
1013
+ // ...
1014
+ }
1015
+ }
1016
+ ```
1017
+
1018
+ ### `getByPath(obj, path)`
1019
+
1020
+ Reads a value from a nested object using dot-notation path. Returns `undefined` when any segment is missing.
1021
+
1022
+ ```ts
1023
+ import { getByPath } from "@livequery/client"
1024
+
1025
+ const doc = { author: { profile: { name: "Ada" } } }
1026
+
1027
+ getByPath(doc, "author.profile.name") // "Ada"
1028
+ getByPath(doc, "author.missing.field") // undefined
1029
+ getByPath(doc, "title") // undefined (not present)
1030
+ ```
1031
+
1032
+ Used internally by filter evaluation and storage sorting. Available as a public export for custom storage adapters.
1033
+
868
1034
  ## React Usage
869
1035
 
870
1036
  Bridge `BehaviorSubject` values into React state.
@@ -955,10 +1121,11 @@ A document ref still exposes `items`; the matching document is represented as a
955
1121
 
956
1122
  - `LivequeryCollection.initialize()` is browser-only in the current implementation.
957
1123
  - Mutations default to `server-first` because the method parameter has that default. Pass `"local-first"` or `"local-only"` explicitly when needed.
1124
+ - `ConflictResolverFunction` is exported as a TypeScript type but is not wired into `LivequeryClient`. It documents the intended shape for future conflict resolution support. Do not pass it to the client — there is currently no parameter that accepts it.
958
1125
  - `LivequeryCollection` has no initialized `metadata` subject in the current constructor, so transporter `metadata` should not be considered reliable consumer state yet.
959
1126
  - `trigger()` returns an observable with a Promise-like `then()` method.
960
1127
  - Transporter streams should emit incremental changes. Do not send full snapshots as repeated `added` events unless the client can safely deduplicate by id.
961
- - There is no dedicated test suite in this package yet. Use `bun run build` for the current validation baseline.
1128
+ - Run `bun test` to execute the test suite. It covers collection behavior, seed loading, mutation mode defaults, query error propagation, filter parsing, and sort stability.
962
1129
 
963
1130
  ## Development
964
1131
 
@@ -1,10 +1,11 @@
1
1
  import { Observable, Subject } from "rxjs";
2
- import type { LivequeryStorge } from "./LivequeryStorge.js";
2
+ import type { LivequeryStorage } from "./LivequeryStorage.js";
3
3
  import type { LivequeryQueryResult, LivequeryTransporter } from "./LivequeryTransporter.js";
4
4
  import type { DataChangeEvent, LivequeryAction, Doc, LivequeryQueryParams, DocState, LivequeryFilters, RealtimeChangeSource, ParitalDocState } from "./types.js";
5
+ import { type ParsedFilter } from "./helpers/filterDocs.js";
5
6
  export type LivequeryClientOptions = {
6
7
  transporters: Record<string, LivequeryTransporter>;
7
- storage: LivequeryStorge;
8
+ storage: LivequeryStorage;
8
9
  };
9
10
  export type LivequeryLoadingState = null | 'next' | 'prev' | 'all';
10
11
  export type SyncRequest = DataChangeEvent & {
@@ -21,7 +22,7 @@ export type ConflictResolverFunction = <T extends Doc>(e: {
21
22
  document: T;
22
23
  };
23
24
  export type LivequeryClientConfig = {
24
- storage: LivequeryStorge;
25
+ storage: LivequeryStorage;
25
26
  transporters: Record<string, LivequeryTransporter>;
26
27
  };
27
28
  export type ActionMode = 'server-first' | 'local-first' | 'local-only';
@@ -34,6 +35,7 @@ export type CollectionMetadata = {
34
35
  collection_ref: string;
35
36
  mode: 'server-first' | 'local-first' | 'cache-first' | 'local-only';
36
37
  filters: Partial<LivequeryFilters<any>>;
38
+ parsedFilters: ParsedFilter[];
37
39
  };
38
40
  export declare class LivequeryClient {
39
41
  #private;
@@ -52,8 +54,9 @@ export declare class LivequeryClient {
52
54
  } | undefined>;
53
55
  add<T extends Doc>(collection_ref: string, documents: Partial<DocState<T>>[], mode: ActionMode): Promise<DocState<T>[]>;
54
56
  update<T extends Doc>(collection_ref: string, documents: ParitalDocState<T>[], mode: ActionMode): Promise<DocState<T>[]>;
55
- delete<T extends Doc>(collection_ref: string, ids: string[], mode: ActionMode): Promise<DocState<T>[]>;
57
+ delete<T extends Doc>(collection_ref: string, ids: string[], mode: ActionMode): Promise<T[]>;
56
58
  trigger<Response>(action: LivequeryAction): Observable<Response>;
59
+ seedToStorage<T extends Doc>(collection_ref: string, docs: T[]): Promise<void>;
57
60
  flush(collection_ref: string): Promise<void>;
58
61
  destroy(): void;
59
62
  }
@@ -1 +1 @@
1
- {"version":3,"file":"LivequeryClient.d.ts","sourceRoot":"","sources":["../src/LivequeryClient.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"LivequeryClient.d.ts","sourceRoot":"","sources":["../src/LivequeryClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyF,UAAU,EAAyB,OAAO,EAAsD,MAAM,MAAM,CAAA;AAC5M,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,KAAK,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAC3F,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,GAAG,EAAE,oBAAoB,EAAE,QAAQ,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAGhK,OAAO,EAAsC,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAI/F,MAAM,MAAM,sBAAsB,GAAG;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAA;IAClD,OAAO,EAAE,gBAAgB,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAA;AAKlE,MAAM,MAAM,WAAW,GAAG,eAAe,GAAG;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,CAAA;IACtB,MAAM,EAAE,oBAAoB,CAAA;CAC/B,CAAA;AAID,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,CAAC,EAAE;IACtD,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAA;IAC/C,YAAY,EAAE,CAAC,CAAA;IACf,MAAM,EAAE,eAAe,CAAA;CAC1B,KAAK;IACF,QAAQ,EAAE,OAAO,CAAA;IACjB,QAAQ,EAAE,CAAC,CAAA;CACd,CAAA;AAGD,MAAM,MAAM,qBAAqB,GAAG;IAChC,OAAO,EAAE,gBAAgB,CAAA;IACzB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAA;CACrD,CAAA;AAED,MAAM,MAAM,UAAU,GAAG,cAAc,GAAG,aAAa,GAAG,YAAY,CAAA;AAEtE,MAAM,MAAM,kBAAkB,GAAG;IAC7B,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,oBAAoB,CAAC,GAAG;QAC3C,IAAI,EAAE,oBAAoB,CAAA;KAC7B,CAAC,CAAA;IACF,cAAc,EAAE,MAAM,CAAA;IACtB,IAAI,EAAE,cAAc,GAAG,aAAa,GAAG,aAAa,GAAG,YAAY,CAAA;IACnE,OAAO,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAA;IACvC,aAAa,EAAE,YAAY,EAAE,CAAA;CAChC,CAAA;AAMD,qBAAa,eAAe;;IAQZ,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,qBAAqB;IAmI1D,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,CAAC,MAAM,CAAC;cAvJhE,oBAAoB;;IAmLxB,KAAK,CAAC,CAAC,SAAS,GAAG,EAAE,GAAG,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAAG;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE;;;;;;IAoN7E,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,UAAU;IA0B9F,MAAM,CAAC,CAAC,SAAS,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,eAAe,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,UAAU;IA4B/F,MAAM,CAAC,CAAC,SAAS,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,UAAU;IA4CnF,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,eAAe;IAOnC,aAAa,CAAC,CAAC,SAAS,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE;IAI9D,KAAK,CAAC,cAAc,EAAE,MAAM;IAKlC,OAAO;CAGV"}
@@ -50,10 +50,10 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
50
50
  var e = new Error(message);
51
51
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
52
  });
53
- import { defer, EMPTY, expand, filter, finalize, forkJoin, from, groupBy, lastValueFrom, map, merge, mergeMap, Observable, of, scan, shareReplay, Subject, Subscription, switchMap, takeUntil, takeWhile, tap, toArray } from "rxjs";
53
+ import { defer, EMPTY, expand, filter, finalize, forkJoin, from, groupBy, map, merge, mergeMap, Observable, of, scan, shareReplay, Subject, Subscription, switchMap, takeUntil, takeWhile, tap } from "rxjs";
54
54
  import { tryCatch } from "./helpers/tryCatch.js";
55
55
  import { whenCompleted } from "./helpers/whenCompleted.js";
56
- import { matchesAllFilters } from "./helpers/filterDocs.js";
56
+ import { matchesParsedFilters, parseFilters } from "./helpers/filterDocs.js";
57
57
  import { useDispose } from "./helpers/useDispose.js";
58
58
  import { uuidv7 } from 'uuidv7';
59
59
  export class LivequeryClient {
@@ -104,7 +104,7 @@ export class LivequeryClient {
104
104
  this.#running = merge(
105
105
  // Server queries
106
106
  this.#queries$.pipe(filter(req => req.collection.mode == 'server-first' || req.collection.mode == 'cache-first'), mergeMap(e => {
107
- const deduplicate_key = `${e.collection.collection_id}:${JSON.stringify(e.filters)}`;
107
+ const deduplicate_key = `${e.collection.collection_id}:${JSON.stringify(e.filters, Object.keys(e.filters || {}).sort())}`;
108
108
  const before = e.filters?.[':before'];
109
109
  const after = e.filters?.[':after'];
110
110
  const around = e.filters?.[':around'];
@@ -156,7 +156,8 @@ export class LivequeryClient {
156
156
  collection_id,
157
157
  collection_ref,
158
158
  mode,
159
- filters: {}
159
+ filters: {},
160
+ parsedFilters: []
160
161
  });
161
162
  return data$.pipe(finalize(() => {
162
163
  this.#collections.delete(collection_id);
@@ -185,8 +186,9 @@ export class LivequeryClient {
185
186
  filters: collection.mode == 'local-first' ? {} : req.filters,
186
187
  collection
187
188
  }));
188
- // If collection
189
+ // If collection
189
190
  collection.filters = req.filters || {};
191
+ collection.parsedFilters = parseFilters(collection.filters);
190
192
  if (collection.mode == 'local-first') {
191
193
  return await this.config.storage.query(req.ref, req.filters);
192
194
  }
@@ -218,14 +220,14 @@ export class LivequeryClient {
218
220
  continue;
219
221
  }
220
222
  if (event.type == 'added') {
221
- event.data && matchesAllFilters(event.data, collection.filters) && changes.push(event);
223
+ event.data && matchesParsedFilters(event.data, collection.parsedFilters) && changes.push(event);
222
224
  continue;
223
225
  }
224
226
  const cache_key = `${event.collection_ref}/${event.id}`;
225
227
  const cached = docs.get(cache_key) || this.config.storage.get(collection.collection_ref, event.id);
226
228
  docs.set(cache_key, cached);
227
229
  const doc = await cached;
228
- if (doc && matchesAllFilters(doc, collection.filters)) {
230
+ if (doc && matchesParsedFilters(doc, collection.parsedFilters)) {
229
231
  changes.push(event);
230
232
  continue;
231
233
  }
@@ -238,6 +240,8 @@ export class LivequeryClient {
238
240
  return changes;
239
241
  }
240
242
  async #broadcast(collection_ref, from, events) {
243
+ if (events.length === 0)
244
+ return;
241
245
  const collections = this.#refs.get(collection_ref) || new Set();
242
246
  const docs = new Map();
243
247
  for (const collection_id of collections) {
@@ -272,13 +276,13 @@ export class LivequeryClient {
272
276
  });
273
277
  }
274
278
  }
275
- #push(collection_ref, docs, server_first) {
276
- return lastValueFrom(from(docs).pipe(mergeMap(doc => from(Object.entries(this.config.transporters)).pipe(mergeMap(async ([tid, transporter]) => {
279
+ async #push(collection_ref, docs, server_first) {
280
+ const results = await Promise.all(docs.flatMap(doc => Object.entries(this.config.transporters).map(async ([tid, transporter]) => {
277
281
  const id = doc.id;
278
282
  if (String(id).startsWith('local:')) {
279
283
  const env_1 = { stack: [], error: void 0, hasError: false };
280
284
  try {
281
- // lock by collection_ref
285
+ // lock by collection_ref
282
286
  const o = new Subject();
283
287
  this.#adding.set(collection_ref, o);
284
288
  const $ = __addDisposableResource(env_1, useDispose(() => {
@@ -289,7 +293,7 @@ export class LivequeryClient {
289
293
  const [e, data] = await tryCatch(() => transporter.add(collection_ref, doc), tid);
290
294
  if (e && server_first)
291
295
  throw e;
292
- // unlock
296
+ // unlock
293
297
  const fnd = {
294
298
  ...data,
295
299
  _adding: undefined,
@@ -312,7 +316,7 @@ export class LivequeryClient {
312
316
  __disposeResources(env_1);
313
317
  }
314
318
  }
315
- // _deleting flag → soft-delete on remote then hard-delete locally
319
+ // _deleting flag → soft-delete on remote then hard-delete locally
316
320
  if (doc._deleting) {
317
321
  const [e, data] = await tryCatch(() => transporter.delete(collection_ref, id), tid);
318
322
  if (e && server_first)
@@ -363,20 +367,19 @@ export class LivequeryClient {
363
367
  }]);
364
368
  return data;
365
369
  }
366
- }))), filter(Boolean), toArray()), { defaultValue: [] });
370
+ })));
371
+ return results.filter(Boolean);
367
372
  }
368
373
  async add(collection_ref, documents, mode) {
369
374
  if (mode == 'server-first') {
370
375
  const list = documents.map(doc => ({ ...doc, id: `local:${uuidv7()}` }));
371
376
  return await this.#push(collection_ref, list, true);
372
377
  }
373
- const docs = await lastValueFrom(from(documents).pipe(mergeMap(doc => {
374
- return this.config.storage.add(collection_ref, {
375
- ...doc,
376
- _adding: true,
377
- ...mode === 'local-only' ? { _local_only: true } : {}
378
- });
379
- }), toArray()));
378
+ const docs = await Promise.all(documents.map(doc => this.config.storage.add(collection_ref, {
379
+ ...doc,
380
+ _adding: true,
381
+ ...mode === 'local-only' ? { _local_only: true } : {}
382
+ })));
380
383
  await this.#broadcast(collection_ref, 'action', docs.map(data => ({
381
384
  collection_ref,
382
385
  id: data.id,
@@ -392,21 +395,17 @@ export class LivequeryClient {
392
395
  const list = documents.map(doc => ({ ...doc, _prev: doc }));
393
396
  return await this.#push(collection_ref, list, true);
394
397
  }
395
- const merged = await lastValueFrom(from(documents).pipe(mergeMap(async (doc) => {
398
+ const merged = (await Promise.all(documents.map(async (doc) => {
396
399
  const old = await this.config.storage.get(collection_ref, doc.id);
397
400
  if (!old)
398
401
  return;
399
402
  const _prev = Object.keys(doc).reduce((acc, key) => {
400
403
  if (key in (old._prev || {}))
401
404
  return acc;
402
- return {
403
- ...acc,
404
- [key]: old[key]
405
- };
405
+ return { ...acc, [key]: old[key] };
406
406
  }, old._prev || {});
407
- const data = await this.config.storage.update(collection_ref, doc.id, { _prev, _updating: true, ...doc, });
408
- return data;
409
- }), filter(Boolean), toArray()));
407
+ return await this.config.storage.update(collection_ref, doc.id, { _prev, _updating: true, ...doc });
408
+ }))).filter(Boolean);
410
409
  await this.#broadcast(collection_ref, 'action', merged.map(data => ({
411
410
  collection_ref,
412
411
  id: data.id,
@@ -423,14 +422,13 @@ export class LivequeryClient {
423
422
  return await this.#push(collection_ref, list, true);
424
423
  }
425
424
  const soft = Object.keys(this.config.transporters).length > 0;
426
- const merged = await lastValueFrom(from(ids).pipe(mergeMap(async (id) => {
425
+ const merged = (await Promise.all(ids.map(async (id) => {
427
426
  const is_local_doc = id.startsWith('local:');
428
427
  if (!soft || is_local_doc || mode == 'local-only') {
429
- const doc = await this.config.storage.delete(collection_ref, id);
430
- return doc;
428
+ return await this.config.storage.delete(collection_ref, id);
431
429
  }
432
430
  return await this.config.storage.update(collection_ref, id, { _deleting: true });
433
- }), filter(Boolean), toArray()));
431
+ }))).filter(Boolean);
434
432
  const deleting_list = merged.filter(doc => doc._deleting);
435
433
  const deleted_list = merged.filter(doc => !doc._deleting);
436
434
  await this.#broadcast(collection_ref, 'action', deleting_list.map(({ id }) => ({
@@ -453,6 +451,9 @@ export class LivequeryClient {
453
451
  trigger(action) {
454
452
  return from(Object.entries(this.config.transporters)).pipe(filter(([id]) => action.transporter_id ? id === action.transporter_id : true), mergeMap(([id, transporter]) => transporter.trigger(action)));
455
453
  }
454
+ async seedToStorage(collection_ref, docs) {
455
+ await Promise.all(docs.map(doc => this.config.storage.add(collection_ref, doc)));
456
+ }
456
457
  async flush(collection_ref) {
457
458
  await this.#broadcast(collection_ref, 'realtime', [{ collection_ref, id: '*', type: 'removed' }]);
458
459
  return this.config.storage.flush();