@livequery/client 2.0.31 → 2.0.105
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 +477 -0
- package/dist/LivequeryClient.d.ts +67 -0
- package/dist/LivequeryClient.d.ts.map +1 -0
- package/dist/LivequeryClient.js +343 -0
- package/dist/LivequeryClient.js.map +1 -0
- package/dist/LivequeryCollection.d.ts +92 -0
- package/dist/LivequeryCollection.d.ts.map +1 -0
- package/dist/LivequeryCollection.js +231 -0
- package/dist/LivequeryCollection.js.map +1 -0
- package/dist/LivequeryDocument.d.ts +23 -0
- package/dist/LivequeryDocument.d.ts.map +1 -0
- package/dist/LivequeryDocument.js +22 -0
- package/dist/LivequeryDocument.js.map +1 -0
- package/dist/LivequeryMemoryStorage.d.ts +14 -0
- package/dist/LivequeryMemoryStorage.d.ts.map +1 -0
- package/dist/LivequeryMemoryStorage.js +89 -0
- package/dist/LivequeryMemoryStorage.js.map +1 -0
- package/dist/LivequeryStorge.d.ts +12 -0
- package/dist/LivequeryStorge.d.ts.map +1 -0
- package/dist/LivequeryStorge.js +2 -0
- package/dist/LivequeryStorge.js.map +1 -0
- package/dist/LivequeryTransporter.d.ts +22 -0
- package/dist/LivequeryTransporter.d.ts.map +1 -0
- package/dist/LivequeryTransporter.js +2 -0
- package/dist/LivequeryTransporter.js.map +1 -0
- package/dist/helpers/filterDocs.d.ts +5 -0
- package/dist/helpers/filterDocs.d.ts.map +1 -0
- package/dist/helpers/filterDocs.js +80 -0
- package/dist/helpers/filterDocs.js.map +1 -0
- package/dist/helpers/tryCatch.d.ts +2 -0
- package/dist/helpers/tryCatch.d.ts.map +1 -0
- package/dist/helpers/tryCatch.js +10 -0
- package/dist/helpers/tryCatch.js.map +1 -0
- package/dist/helpers/whenCompleted.d.ts +3 -0
- package/dist/helpers/whenCompleted.d.ts.map +1 -0
- package/dist/helpers/whenCompleted.js +5 -0
- package/dist/helpers/whenCompleted.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +95 -18
- package/build/Collection.d.ts +0 -52
- package/build/Collection.js +0 -366
- package/build/index.d.ts +0 -1
- package/build/index.js +0 -1
- package/build/tsconfig.tsbuildinfo +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# @livequery/client
|
|
2
|
+
|
|
3
|
+
Reactive local-first data primitives for browser clients.
|
|
4
|
+
|
|
5
|
+
This repository is the client library package, not an application. Changes here should preserve reusable public API behavior unless a task explicitly targets a breaking change.
|
|
6
|
+
|
|
7
|
+
This package provides the core building blocks behind Livequery collections: reactive document state, pluggable local storage, pluggable transporters, optimistic mutations, and typed inline filters.
|
|
8
|
+
|
|
9
|
+
## AI Agent Guidance
|
|
10
|
+
|
|
11
|
+
Repository-specific agent guidance lives in `AGENTS.md` and `copilot-instructions.md`.
|
|
12
|
+
|
|
13
|
+
- `AGENTS.md` is the implementation-focused guide for coding agents modifying this package.
|
|
14
|
+
- `copilot-instructions.md` provides repo-level instructions for Copilot when generating or reviewing code in this workspace.
|
|
15
|
+
- Both documents assume this repo is a library package, so agent changes should avoid app-specific scaffolding and should preserve public API compatibility by default.
|
|
16
|
+
- Agents generating consumer code should also follow the usage patterns documented below: create a shared `LivequeryClient`, initialize collections before querying, and subscribe to collection state instead of relying on one-time `.value` reads.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bun add @livequery/client rxjs
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
For React projects:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bun add @livequery/client @livequery/react rxjs
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The package is published as ESM and targets browser usage.
|
|
31
|
+
|
|
32
|
+
## Public Exports
|
|
33
|
+
|
|
34
|
+
The package re-exports:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
export * from "./LivequeryCollection"
|
|
38
|
+
export * from "./LivequeryClient"
|
|
39
|
+
export * from "./LivequeryMemoryStorage"
|
|
40
|
+
export * from "./LivequeryStorge"
|
|
41
|
+
export * from "./LivequeryTransporter"
|
|
42
|
+
export * from "./types"
|
|
43
|
+
export * from "./helpers/filterDocs"
|
|
44
|
+
export * from "./LivequeryDocument"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Core Types
|
|
48
|
+
|
|
49
|
+
### `Doc`
|
|
50
|
+
|
|
51
|
+
Every record must have an `id`.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
type Doc<T = {}> = T & {
|
|
55
|
+
id: string
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### `DocState`
|
|
60
|
+
|
|
61
|
+
Collections and documents expose `DocState<T>`, which adds optimistic mutation metadata.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
type DocState<T extends Doc> = T & {
|
|
65
|
+
_deleting?: boolean
|
|
66
|
+
_deleting_error?: { code: string; message: string; transporter_id: string }
|
|
67
|
+
_updating?: boolean
|
|
68
|
+
_updating_error?: { code: string; message: string; transporter_id: string }
|
|
69
|
+
_adding?: boolean
|
|
70
|
+
_adding_error?: { code: string; message: string; transporter_id: string }
|
|
71
|
+
_remotes?: Record<string, string | number>
|
|
72
|
+
_prev?: Partial<T>
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `DataChangeEvent`
|
|
77
|
+
|
|
78
|
+
Transporters stream incremental change events back into the core.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
type DataChangeEvent = {
|
|
82
|
+
collection_ref: string
|
|
83
|
+
id: string
|
|
84
|
+
type: "added" | "removed" | "modified"
|
|
85
|
+
data?: Record<string, any>
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Architecture
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
LivequeryCollection / LivequeryDocument
|
|
93
|
+
|
|
|
94
|
+
v
|
|
95
|
+
LivequeryClient
|
|
96
|
+
/ \
|
|
97
|
+
v v
|
|
98
|
+
LivequeryStorge LivequeryTransporter(s)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- `LivequeryCollection` owns the reactive state for one collection ref or one document ref.
|
|
102
|
+
- `LivequeryDocument` wraps an item as a `BehaviorSubject` with convenience mutation methods.
|
|
103
|
+
- `LivequeryClient` coordinates storage, transporters, optimistic writes, and fan-out to watchers.
|
|
104
|
+
- `LivequeryStorge` is the local persistence contract.
|
|
105
|
+
- `LivequeryTransporter` is the remote sync contract.
|
|
106
|
+
|
|
107
|
+
## Quick Start
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import {
|
|
111
|
+
LivequeryCollection,
|
|
112
|
+
LivequeryClient,
|
|
113
|
+
LivequeryMemoryStorage,
|
|
114
|
+
type Doc,
|
|
115
|
+
type LivequeryQueryResult,
|
|
116
|
+
type LivequeryTransporter,
|
|
117
|
+
} from "@livequery/client"
|
|
118
|
+
import { of } from "rxjs"
|
|
119
|
+
|
|
120
|
+
type Todo = Doc<{
|
|
121
|
+
title: string
|
|
122
|
+
done: boolean
|
|
123
|
+
createdAt: number
|
|
124
|
+
}>
|
|
125
|
+
|
|
126
|
+
const storage = new LivequeryMemoryStorage()
|
|
127
|
+
|
|
128
|
+
const transporter: LivequeryTransporter = {
|
|
129
|
+
query(_query) {
|
|
130
|
+
return of<Partial<LivequeryQueryResult>>({
|
|
131
|
+
changes: [],
|
|
132
|
+
summary: {},
|
|
133
|
+
paging: { total: 0, current: 0 },
|
|
134
|
+
metadata: {},
|
|
135
|
+
source: "query",
|
|
136
|
+
})
|
|
137
|
+
},
|
|
138
|
+
async add(_ref, doc) {
|
|
139
|
+
return { id: crypto.randomUUID(), ...doc } as Todo
|
|
140
|
+
},
|
|
141
|
+
async update(_ref, id, doc) {
|
|
142
|
+
return { id, ...doc } as Todo
|
|
143
|
+
},
|
|
144
|
+
async delete(_ref, id) {
|
|
145
|
+
return { id } as Todo
|
|
146
|
+
},
|
|
147
|
+
async trigger(_action) {
|
|
148
|
+
return { ok: true }
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const core = new LivequeryClient({
|
|
153
|
+
storage,
|
|
154
|
+
transporters: {
|
|
155
|
+
primary: transporter,
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const todos = new LivequeryCollection<Todo>(core, {
|
|
160
|
+
filters: { "createdAt:sort": "desc" },
|
|
161
|
+
mode: "server-first",
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
todos.initialize("todos")
|
|
165
|
+
|
|
166
|
+
todos.items.subscribe((items) => {
|
|
167
|
+
console.log(items.map((doc) => doc.value))
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
await todos.query({ ":limit": 20, "createdAt:sort": "desc" })
|
|
171
|
+
await todos.add({ title: "Buy milk", done: false, createdAt: Date.now() })
|
|
172
|
+
await todos.update("todo-1", { done: true })
|
|
173
|
+
await todos.delete("todo-1")
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## `LivequeryClient`
|
|
177
|
+
|
|
178
|
+
Create one core with a storage adapter and one or more transporters:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
const core = new LivequeryClient({
|
|
182
|
+
storage,
|
|
183
|
+
transporters: {
|
|
184
|
+
primary: transporter,
|
|
185
|
+
},
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Mutation flow
|
|
190
|
+
|
|
191
|
+
For `add`, `update`, and `delete`, the core:
|
|
192
|
+
|
|
193
|
+
1. writes to local storage first
|
|
194
|
+
2. broadcasts the optimistic change to active watchers
|
|
195
|
+
3. pushes the mutation to each transporter
|
|
196
|
+
4. clears optimistic flags or stores mutation errors after the remote call finishes
|
|
197
|
+
|
|
198
|
+
Documents created locally receive ids prefixed with `local:` until a transporter returns a persisted id.
|
|
199
|
+
|
|
200
|
+
### Query modes
|
|
201
|
+
|
|
202
|
+
Collections support three modes through `LivequeryCollectionOptions.mode`:
|
|
203
|
+
|
|
204
|
+
- `server-first`: queries are driven by transporters, and collection state is built from streamed change events.
|
|
205
|
+
- `cache-first`: first query can hydrate from local storage, then transporters refresh the result.
|
|
206
|
+
- `local-first`: queries resolve from local storage while remote sync runs in the background and rebroadcasts matching changes.
|
|
207
|
+
|
|
208
|
+
Implementation detail: in `local-first` mode, filters are applied by the storage adapter, while the remote query path is triggered with empty filters and matching is re-checked when added events are broadcast locally.
|
|
209
|
+
|
|
210
|
+
## `LivequeryCollection`
|
|
211
|
+
|
|
212
|
+
`LivequeryCollection<T>` manages one collection or one document ref.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
type LivequeryCollectionOptions<T extends Doc> = {
|
|
216
|
+
core: LivequeryClient
|
|
217
|
+
filters: Partial<LivequeryFilters<T>>
|
|
218
|
+
lazy: boolean
|
|
219
|
+
debounce: number
|
|
220
|
+
mode: "server-first" | "local-first" | "cache-first"
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Create and initialize a collection
|
|
225
|
+
|
|
226
|
+
The current constructor takes `core` as the first argument and options as the second argument.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const posts = new LivequeryCollection<Post>(core, {
|
|
230
|
+
filters: { "publishedAt:sort": "desc" },
|
|
231
|
+
lazy: false,
|
|
232
|
+
debounce: 250,
|
|
233
|
+
mode: "cache-first",
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
posts.initialize("posts")
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`initialize(ref)` subscribes the collection to `LivequeryClient.watch(ref, id, mode)`. In the current implementation, it is browser-only and returns early when `window` is unavailable.
|
|
240
|
+
|
|
241
|
+
### Collection refs and document refs
|
|
242
|
+
|
|
243
|
+
If a ref has an even number of path segments, the last segment is treated as a document id.
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
posts.initialize("posts")
|
|
247
|
+
singlePost.initialize("posts/post-1")
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
For collection mutations, `add`, `update`, and `delete` always target the collection portion of the ref.
|
|
251
|
+
|
|
252
|
+
### Reactive state
|
|
253
|
+
|
|
254
|
+
- `items`: `BehaviorSubject<LivequeryDocument<DocState<T>>[]>`
|
|
255
|
+
- `summary`: `BehaviorSubject<Record<string, any>>`
|
|
256
|
+
- `loading`: `BehaviorSubject<null | "all" | "next" | "prev">`
|
|
257
|
+
- `filters`: `BehaviorSubject<Partial<LivequeryFilters<T>>>`
|
|
258
|
+
- `paging`: `BehaviorSubject<LivequeryPaging>`
|
|
259
|
+
- `error`: `BehaviorSubject<{ code: string; message: string } | null>`
|
|
260
|
+
|
|
261
|
+
`items` is a `BehaviorSubject`, not a plain array. Reading `collection.items.value` gives the current snapshot only. If you need live updates, subscribe.
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
const subscription = posts.items.subscribe((items) => {
|
|
265
|
+
console.log("realtime items", items.map((doc) => doc.value))
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
subscription.unsubscribe()
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
In React, reading only `collection.items.value` during render will not trigger rerenders when new events arrive. Bridge the `BehaviorSubject` into component state.
|
|
272
|
+
|
|
273
|
+
```tsx
|
|
274
|
+
function TodoList({ collection }: { collection: LivequeryCollection<Todo> }) {
|
|
275
|
+
const [items, setItems] = useState(() => collection.items.value)
|
|
276
|
+
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
const subscription = collection.items.subscribe(setItems)
|
|
279
|
+
return () => subscription.unsubscribe()
|
|
280
|
+
}, [collection])
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<ul>
|
|
284
|
+
{items.map((item) => (
|
|
285
|
+
<li key={item.value.id}>{item.value.title}</li>
|
|
286
|
+
))}
|
|
287
|
+
</ul>
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Main methods
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
query(filters: Partial<LivequeryFilters<T>>): Promise<void>
|
|
296
|
+
debounceQuery(filters: Partial<LivequeryFilters<T>>): Promise<void>
|
|
297
|
+
loadMore(): Promise<void>
|
|
298
|
+
loadPrev(): Promise<void>
|
|
299
|
+
loadAround(cursor: string): Promise<void>
|
|
300
|
+
add(payload: Partial<T>): Promise<T>
|
|
301
|
+
update(id: string, payload: Partial<T>): Promise<T | undefined>
|
|
302
|
+
delete(id: string): Promise<void | T | undefined>
|
|
303
|
+
trigger<R>(action: string, payload?: Record<string, any>): Observable<{ data: R; error?: Error }>
|
|
304
|
+
resetError(): void
|
|
305
|
+
watch(check: (prev: T, next: T) => boolean): Observable<[DocState<T>, DocState<T>]>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Notes about current behavior:
|
|
309
|
+
|
|
310
|
+
- `query()` requires `initialize()` to have run first so the collection has a `ref` and watcher registration.
|
|
311
|
+
- `debounceQuery()` only emits through the debounced path when `options.debounce` is truthy.
|
|
312
|
+
- `loadMore()` uses `paging.next.cursor` as `:after`.
|
|
313
|
+
- `loadPrev()` uses `paging.prev.cursor` as `:before`.
|
|
314
|
+
- `loadAround()` currently sets both `:after` and `:before` to the provided cursor.
|
|
315
|
+
|
|
316
|
+
## `LivequeryDocument`
|
|
317
|
+
|
|
318
|
+
Each entry inside `collection.items` is a `LivequeryDocument`, which extends `BehaviorSubject<DocState<T>>`.
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
class LivequeryDocument<T extends Doc> extends BehaviorSubject<DocState<T>> {
|
|
322
|
+
update(data: Partial<T>): Promise<T | undefined>
|
|
323
|
+
del(): Promise<void | T | undefined>
|
|
324
|
+
trigger<R>(action: string, payload: Record<string, any>): Observable<{ data: R; error?: Error }>
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Example:
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
const first = todos.items.value[0]
|
|
332
|
+
|
|
333
|
+
first.subscribe((doc) => {
|
|
334
|
+
console.log(doc.title, doc._updating)
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
await first.update({ done: true })
|
|
338
|
+
await first.del()
|
|
339
|
+
first.trigger("archive", { reason: "completed" }).subscribe()
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## `LivequeryStorge`
|
|
343
|
+
|
|
344
|
+
Local persistence adapters must implement:
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
type LivequeryStorge = {
|
|
348
|
+
query<T extends Doc>(
|
|
349
|
+
collection: string,
|
|
350
|
+
filters?: Record<string, any>
|
|
351
|
+
): Promise<{
|
|
352
|
+
documents: T[]
|
|
353
|
+
paging: LivequeryPaging
|
|
354
|
+
}>
|
|
355
|
+
get<T extends Doc>(ref: string, id: string): Promise<T | null>
|
|
356
|
+
add<T extends Doc>(collection: string, document: T): Promise<T>
|
|
357
|
+
update<T extends Doc>(collection: string, id: string, document: Record<string, any>): Promise<T | null>
|
|
358
|
+
delete<T extends Doc>(collection: string, id: string): Promise<T | null>
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
The package ships with `LivequeryMemoryStorage`, an in-memory adapter useful for tests, demos, and ephemeral state.
|
|
363
|
+
|
|
364
|
+
### `LivequeryMemoryStorage`
|
|
365
|
+
|
|
366
|
+
The built-in adapter:
|
|
367
|
+
|
|
368
|
+
- stores documents in `Map<string, Map<string, Doc>>`
|
|
369
|
+
- generates a local id with `local:${crypto.randomUUID()}` when `id` is missing
|
|
370
|
+
- applies filters through the exported `filterDocs()` helper
|
|
371
|
+
- supports nested sort keys such as `profile.createdAt:sort`
|
|
372
|
+
|
|
373
|
+
## `LivequeryTransporter`
|
|
374
|
+
|
|
375
|
+
Remote adapters must implement:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
type LivequeryTransporter = {
|
|
379
|
+
query<T extends Doc>(query: LivequeryQueryParams<T>): Observable<Partial<LivequeryQueryResult>>
|
|
380
|
+
add<T extends Doc>(ref: string, doc: Omit<T, "id">): Promise<T>
|
|
381
|
+
update<T extends Doc>(ref: string, id: string, doc: Partial<T>): Promise<T>
|
|
382
|
+
delete<T extends Doc>(ref: string, id: string): Promise<T>
|
|
383
|
+
trigger<T>(action: LivequeryAction): Promise<T>
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Query result shape
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
type LivequeryQueryResult = {
|
|
391
|
+
error: { code: string; message: string }
|
|
392
|
+
changes: DataChangeEvent[]
|
|
393
|
+
summary: Record<string, any>
|
|
394
|
+
paging: LivequeryPaging
|
|
395
|
+
metadata: Record<string, any>
|
|
396
|
+
source: "query" | "action" | "realtime"
|
|
397
|
+
loading?: "all" | "next" | "prev" | null
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Transporters can emit partial results. In practice, the most useful fields are `changes`, `paging`, `summary`, `metadata`, and `error`.
|
|
402
|
+
|
|
403
|
+
## Query Filters
|
|
404
|
+
|
|
405
|
+
Filters are flat keys derived from the document type.
|
|
406
|
+
|
|
407
|
+
### Pagination keys
|
|
408
|
+
|
|
409
|
+
- `:limit`
|
|
410
|
+
- `:before`
|
|
411
|
+
- `:after`
|
|
412
|
+
- `:around`
|
|
413
|
+
- `:page`
|
|
414
|
+
|
|
415
|
+
### Supported operators
|
|
416
|
+
|
|
417
|
+
- `field:sort` with `"asc" | "desc"` for string and number fields
|
|
418
|
+
- `field:gt`, `field:gte`, `field:lt`, `field:lte` for numeric fields
|
|
419
|
+
- `field:eq-number` for numeric equality
|
|
420
|
+
- `field` for string equality
|
|
421
|
+
- `field:in`, `field:nin` for string or number membership
|
|
422
|
+
- `field:include` for array containment
|
|
423
|
+
- `field:boolean` with `"true" | "false" | "not-true" | "not-false"`
|
|
424
|
+
- `field:like` for case-insensitive substring matching on strings
|
|
425
|
+
- `field:null` with `"null-only" | "not-null"`
|
|
426
|
+
|
|
427
|
+
Nested field paths are supported, for example `"profile.createdAt:sort"`.
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
await todos.query({
|
|
431
|
+
":limit": 20,
|
|
432
|
+
"done:boolean": "false",
|
|
433
|
+
"title:like": "milk",
|
|
434
|
+
"createdAt:gte": 1714176000000,
|
|
435
|
+
"createdAt:sort": "desc",
|
|
436
|
+
})
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Helper Exports
|
|
440
|
+
|
|
441
|
+
### `filterDocs()`
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
import { filterDocs } from "@livequery/client"
|
|
445
|
+
|
|
446
|
+
const visible = filterDocs(docs, {
|
|
447
|
+
"done:boolean": "false",
|
|
448
|
+
"title:like": "milk",
|
|
449
|
+
})
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### `matchesAllFilters()`
|
|
453
|
+
|
|
454
|
+
The helper module also exports `matchesAllFilters(doc, filters)` for direct predicate checks.
|
|
455
|
+
|
|
456
|
+
## Caveats
|
|
457
|
+
|
|
458
|
+
- `initialize()` is browser-only because it exits early when `window` is unavailable.
|
|
459
|
+
- The public storage interface name is intentionally spelled `LivequeryStorge`, matching the source.
|
|
460
|
+
- Optimistic flags such as `_adding`, `_updating`, `_deleting`, and `_prev` are system-managed fields.
|
|
461
|
+
- Transporter query streams are expected to emit incremental `changes`, not full snapshots.
|
|
462
|
+
- `LivequeryCollection` declares a `metadata` subject but does not initialize it in the constructor, so transporter-emitted `metadata` is not safe to rely on yet.
|
|
463
|
+
- `trigger()` is typed at the collection and document layer as `Observable<{ data, error? }>` but currently forwards raw transporter results from `LivequeryCore.trigger()`.
|
|
464
|
+
|
|
465
|
+
## Development
|
|
466
|
+
|
|
467
|
+
```bash
|
|
468
|
+
bun run build
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Available scripts:
|
|
472
|
+
|
|
473
|
+
- `bun run clean`
|
|
474
|
+
- `bun run build:js`
|
|
475
|
+
- `bun run build:types`
|
|
476
|
+
- `bun run build`
|
|
477
|
+
- `bun run build:watch`
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Observable, Subject } from "rxjs";
|
|
2
|
+
import type { LivequeryStorge } from "./LivequeryStorge.js";
|
|
3
|
+
import type { LivequeryQueryResult, LivequeryTransporter } from "./LivequeryTransporter.js";
|
|
4
|
+
import type { DataChangeEvent, LivequeryAction, Doc, LivequeryQueryParams, LivequeryFilters, RealtimeChangeSource } from "./types.js";
|
|
5
|
+
export type LivequeryClientOptions = {
|
|
6
|
+
transporters: Record<string, LivequeryTransporter>;
|
|
7
|
+
storage: LivequeryStorge;
|
|
8
|
+
};
|
|
9
|
+
export type LivequeryLoadingState = null | 'next' | 'prev' | 'all';
|
|
10
|
+
export type SyncRequest = DataChangeEvent & {
|
|
11
|
+
ref: string;
|
|
12
|
+
collection_ref: string;
|
|
13
|
+
source: RealtimeChangeSource;
|
|
14
|
+
};
|
|
15
|
+
export type ConflictResolverFunction = <T extends Doc>(e: {
|
|
16
|
+
from: Record<string, string | number | boolean>;
|
|
17
|
+
old_document: T;
|
|
18
|
+
change: DataChangeEvent;
|
|
19
|
+
}) => {
|
|
20
|
+
approved: boolean;
|
|
21
|
+
document: T;
|
|
22
|
+
};
|
|
23
|
+
export type LivequeryClientConfig = {
|
|
24
|
+
storage: LivequeryStorge;
|
|
25
|
+
transporters: Record<string, LivequeryTransporter>;
|
|
26
|
+
};
|
|
27
|
+
export type CollectionMetadata = {
|
|
28
|
+
collection_id: string;
|
|
29
|
+
document_id?: string;
|
|
30
|
+
data$: Subject<Partial<LivequeryQueryResult> & {
|
|
31
|
+
from: RealtimeChangeSource;
|
|
32
|
+
}>;
|
|
33
|
+
collection_ref: string;
|
|
34
|
+
mode: 'server-first' | 'local-first' | 'cache-first';
|
|
35
|
+
filters: Partial<LivequeryFilters<any>>;
|
|
36
|
+
};
|
|
37
|
+
export declare class LivequeryClient {
|
|
38
|
+
#private;
|
|
39
|
+
private readonly config;
|
|
40
|
+
constructor(config: LivequeryClientConfig);
|
|
41
|
+
watch(ref: string, collection_id: string, mode: CollectionMetadata['mode']): Observable<Partial<LivequeryQueryResult> & {
|
|
42
|
+
from: RealtimeChangeSource;
|
|
43
|
+
}>;
|
|
44
|
+
query<T extends Doc>(req: LivequeryQueryParams<T> & {
|
|
45
|
+
collection_id: string;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
documents: T[];
|
|
48
|
+
paging: import("./types.js").LivequeryPaging;
|
|
49
|
+
} | {
|
|
50
|
+
documents: T[];
|
|
51
|
+
} | undefined>;
|
|
52
|
+
add<T extends Doc>(collection_ref: string, doc: Record<string, any>): Promise<{
|
|
53
|
+
[key: string]: T;
|
|
54
|
+
}>;
|
|
55
|
+
update<T extends Doc>(collection_ref: string, id: string, data: Record<string, any>): Promise<{
|
|
56
|
+
[key: string]: {
|
|
57
|
+
id: string;
|
|
58
|
+
} & Partial<T>;
|
|
59
|
+
} | undefined>;
|
|
60
|
+
delete<T extends Doc>(collection_ref: string, id: string): Promise<{
|
|
61
|
+
[key: string]: {
|
|
62
|
+
id: string;
|
|
63
|
+
};
|
|
64
|
+
} | undefined>;
|
|
65
|
+
trigger<Response>(action: LivequeryAction): Observable<Response>;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=LivequeryClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LivequeryClient.d.ts","sourceRoot":"","sources":["../src/LivequeryClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmH,UAAU,EAAiC,OAAO,EAAiD,MAAM,MAAM,CAAA;AACzO,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,KAAK,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAC3F,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,GAAG,EAAE,oBAAoB,EAAY,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAM/I,MAAM,MAAM,sBAAsB,GAAG;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAA;IAClD,OAAO,EAAE,eAAe,CAAA;CAC3B,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,eAAe,CAAA;IACxB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAA;CACrD,CAAA;AAED,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,CAAA;IACpD,OAAO,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAA;CAC1C,CAAA;AAMD,qBAAa,eAAe;;IAOZ,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,qBAAqB;IA+H1D,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,CAAC,MAAM,CAAC;cAjJhE,oBAAoB;;IA4KxB,KAAK,CAAC,CAAC,SAAS,GAAG,EAAE,GAAG,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAAG;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE;;;;;;IAoL7E,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;;;IAkBnE,MAAM,CAAC,CAAC,SAAS,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;;gBAyB5C,MAAM;;;IAG7C,MAAM,CAAC,CAAC,SAAS,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM;;gBAsBjB,MAAM;;;IAGnD,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,eAAe;CAK5C"}
|