@livequery/rpc 2.0.67

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 ADDED
@@ -0,0 +1,669 @@
1
+ # @livequery/new
2
+
3
+ A local-first reactive data library for browser clients. Type-safe, RxJS-based collection system with pluggable storage and transporter adapters, optimistic local mutations, and real-time synchronisation support.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Architecture](#architecture)
8
+ - [Installation](#installation)
9
+ - [Quick Start](#quick-start)
10
+ - [Core Concepts](#core-concepts)
11
+ - [Doc](#doc)
12
+ - [LivequeryStorge](#livequerystorge)
13
+ - [LivequeryTransporter](#livequerytransporter)
14
+ - [LivequeryCore](#livequerycore)
15
+ - [LivequeryCollection](#livequerycollection)
16
+ - [LivequeryDocument](#livequerydocument)
17
+ - [Query Filters](#query-filters)
18
+ - [API Reference](#api-reference)
19
+ - [LivequeryMemoryStorage](#livequerymemorystorage)
20
+ - [LivequeryCollection methods](#livequerycollection-methods)
21
+ - [WorkerRpc](#workerrpc)
22
+ - [Writing a Custom Transporter](#writing-a-custom-transporter)
23
+ - [Writing a Custom Storage Adapter](#writing-a-custom-storage-adapter)
24
+ - [Types Reference](#types-reference)
25
+
26
+ ---
27
+
28
+ ## Architecture
29
+
30
+ ```
31
+ ┌─────────────────────────────────────┐
32
+ │ Your Application │
33
+ │ LivequeryCollection │
34
+ │ .items / .loading / .paging │
35
+ │ .query() / .add() / .update() / .delete()
36
+ └──────────────┬──────────────────────┘
37
+
38
+ ┌──────────────▼──────────────────────┐
39
+ │ LivequeryCore │
40
+ │ - coordinates storage & transport │
41
+ │ - optimistic local mutations │
42
+ │ - broadcasts changes to collections│
43
+ └───────┬──────────────┬──────────────┘
44
+ │ │
45
+ ┌───────▼──────┐ ┌─────▼──────────────┐
46
+ │ LivequeryStorge│ │ LivequeryTransporter│
47
+ │ (local) │ │ (remote backend) │
48
+ │ │ │ (can be many) │
49
+ └──────────────┘ └────────────────────┘
50
+ ```
51
+
52
+ **Data flow for a mutation (add / update / delete):**
53
+ 1. `LivequeryCollection.add/update/delete` calls `LivequeryCore.trigger`.
54
+ 2. The core applies the change to storage immediately (optimistic update).
55
+ 3. The change is broadcast to all live collections watching the same `ref`.
56
+ 4. The core then calls every configured transporter to sync the change remotely.
57
+
58
+ **Data flow for a query:**
59
+ 1. `LivequeryCollection.query(filters)` calls `LivequeryCore.query`.
60
+ 2. The core returns locally-stored documents instantly from storage.
61
+ 3. In parallel, it fires the query against every transporter.
62
+ 4. Each transporter streams `DataChangeEvent[]` back into the collection, which merges them reactively.
63
+
64
+ ---
65
+
66
+ ## Installation
67
+
68
+ ```bash
69
+ npm install @livequery/new rxjs
70
+ # or
71
+ bun add @livequery/new rxjs
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Quick Start
77
+
78
+ ```ts
79
+ import {
80
+ LivequeryCollection,
81
+ LivequeryCore,
82
+ LivequeryMemoryStorage,
83
+ type Doc,
84
+ type LivequeryTransporter,
85
+ } from "@livequery/new"
86
+ import { of } from "rxjs"
87
+
88
+ // 1. Define your document shape
89
+ type Todo = Doc & {
90
+ title: string
91
+ done: boolean
92
+ createdAt: number
93
+ }
94
+
95
+ // 2. Create a storage (in-memory for this example)
96
+ const storage = new LivequeryMemoryStorage()
97
+
98
+ // 3. Create a transporter (no-op; replace with your real backend)
99
+ const transporter: LivequeryTransporter = {
100
+ query(query) {
101
+ return of({ query_id: query.query_id, changes: [], summary: {}, paging: { total: 0, current: 0 }, metadata: {}, source: "query" })
102
+ },
103
+ trigger(_action) {
104
+ return of({ data: {} as any })
105
+ },
106
+ }
107
+
108
+ // 4. Create the core
109
+ const core = new LivequeryCore({
110
+ storage,
111
+ transporters: { primary: transporter },
112
+ resolver: ({ change, old_document }) => ({
113
+ approved: true,
114
+ document: { ...old_document, ...change.data } as Todo,
115
+ }),
116
+ })
117
+
118
+ // 5. Create a reactive collection
119
+ const todos = new LivequeryCollection<Todo>({
120
+ core,
121
+ ref: "todos",
122
+ filters: { "createdAt:sort": "desc", ":limit": 20, ":page": 1, ":before": "", ":after": "" },
123
+ lazy: true,
124
+ })
125
+
126
+ // 6. Call initialize() to start watching (required)
127
+ todos.initialize()
128
+
129
+ // 7. Subscribe to reactive state
130
+ todos.items.subscribe((docs) => {
131
+ console.log("items:", docs.map((doc$) => doc$.value))
132
+ })
133
+ todos.loading.subscribe((state) => console.log("loading:", state))
134
+ todos.paging.subscribe((p) => console.log("paging:", p))
135
+
136
+ // 8. Trigger a query
137
+ await todos.query({ "createdAt:sort": "desc", ":limit": 20, ":page": 1, ":before": "", ":after": "" })
138
+
139
+ // 9. Mutate data
140
+ await todos.add({ title: "Buy milk", done: false, createdAt: Date.now() })
141
+ await todos.update("some-id", { done: true })
142
+ await todos.delete("some-id")
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Core Concepts
148
+
149
+ ### Doc
150
+
151
+ Every document stored in livequery must extend `Doc<T>`:
152
+
153
+ ```ts
154
+ type Doc<T = {}> = T & {
155
+ id: string
156
+ }
157
+ ```
158
+
159
+ Your types extend this base:
160
+
161
+ ```ts
162
+ type Post = Doc & {
163
+ title: string
164
+ body: string
165
+ publishedAt: number
166
+ }
167
+ ```
168
+
169
+ When a document is held inside a `LivequeryCollection`, it is wrapped in `DocState<T>` which adds optimistic-update tracking fields:
170
+
171
+ ```ts
172
+ type DocState<T extends Doc> = T & {
173
+ _deleting?: boolean // pending deletion
174
+ _updating?: boolean // pending update
175
+ _adding?: boolean // pending add
176
+ _remotes?: Record<string, string | number> // per-transporter version cursors
177
+ _prev?: Partial<T> // previous values before last local mutation
178
+ }
179
+ ```
180
+
181
+ ---
182
+
183
+ ### LivequeryStorge
184
+
185
+ `LivequeryStorge` is the interface for local persistence. The library ships with `LivequeryMemoryStorage`. You can create adapters for `localStorage`, `IndexedDB`, SQLite, etc.
186
+
187
+ ```ts
188
+ type LivequeryStorge = {
189
+ query<T extends Doc>(
190
+ collection: string,
191
+ filters?: Record<string, any>
192
+ ): Promise<{ documents: T[]; paging: LivequeryPaging }>
193
+
194
+ get<T extends Doc>(ref: string, id: string): Promise<T | null>
195
+ add<T extends Doc>(collection: string, document: T): Promise<T>
196
+ update<T extends Doc>(collection: string, id: string, document: Record<string, any>): Promise<T | null>
197
+ delete<T extends Doc>(collection: string, id: string): Promise<T | null>
198
+ }
199
+ ```
200
+
201
+ ---
202
+
203
+ ### LivequeryTransporter
204
+
205
+ A transporter connects the core to a remote backend (REST API, WebSocket, Firebase, etc.). You can provide **multiple** transporters; the core fans out queries and mutations to all of them.
206
+
207
+ ```ts
208
+ type LivequeryTransporter = {
209
+ // Called for every query. Returns an Observable so the remote can stream results.
210
+ query<T extends Doc>(
211
+ query: LivequeryQueryParams<T>
212
+ ): Observable<Partial<LivequeryQueryResult<T>>>
213
+
214
+ // Called for add / update / delete / custom actions.
215
+ trigger<T>(action: LivequeryAction): Observable<{ data: T; error?: Error }>
216
+ }
217
+ ```
218
+
219
+ ---
220
+
221
+ ### LivequeryCore
222
+
223
+ The central coordinator. Instantiate once and share across your app.
224
+
225
+ ```ts
226
+ const core = new LivequeryCore({
227
+ storage, // LivequeryStorge implementation
228
+ transporters: { // one or more named transporters
229
+ primary: myTransporter,
230
+ },
231
+ resolver, // ConflictResolverFunction
232
+ })
233
+ ```
234
+
235
+ #### Conflict resolver
236
+
237
+ Called for every local mutation. Decides whether to approve the change and what the final merged document should be.
238
+
239
+ ```ts
240
+ type ConflictResolverFunction = <T extends Doc>(e: {
241
+ from: Record<string, string | number | boolean> // remote version cursors (_remotes)
242
+ old_document: T // current local document
243
+ change: DataChangeEvent<T> // incoming change
244
+ }) => {
245
+ approved: boolean // false → discard the change
246
+ document: T // resolved document to persist
247
+ }
248
+ ```
249
+
250
+ Simple last-write-wins example:
251
+
252
+ ```ts
253
+ const resolver: ConflictResolverFunction = ({ change, old_document }) => ({
254
+ approved: true,
255
+ document: { ...old_document, ...change.data } as typeof old_document,
256
+ })
257
+ ```
258
+
259
+ ---
260
+
261
+ ### LivequeryCollection
262
+
263
+ `LivequeryCollection<T>` holds reactive state for one collection path (`ref`). Its state is exposed as a set of `BehaviorSubject` properties.
264
+
265
+ ```ts
266
+ const posts = new LivequeryCollection<Post>({
267
+ core,
268
+ ref: "posts",
269
+ filters: { "publishedAt:sort": "desc", ":limit": 10, ":page": 1, ":before": "", ":after": "" },
270
+ lazy: true, // true = don't auto-load on initialize(); false = load immediately
271
+ })
272
+
273
+ // Must call initialize() to wire up the core watcher
274
+ posts.initialize()
275
+ ```
276
+
277
+ #### Reactive state properties
278
+
279
+ | Property | Type | Description |
280
+ |----------|------|-------------|
281
+ | `items` | `BehaviorSubject<LivequeryDocument<DocState<T>>[]>` | Current list of documents |
282
+ | `loading` | `BehaviorSubject<LivequeryLoadingState>` | Loading flags |
283
+ | `filters` | `BehaviorSubject<Partial<LivequeryFilters<T>>>` | Active filters |
284
+ | `paging` | `BehaviorSubject<LivequeryPaging>` | Pagination info |
285
+ | `summary` | `BehaviorSubject<Record<string, any>>` | Aggregation data from transporter |
286
+ | `metadata` | `BehaviorSubject<Record<string, any>>` | Arbitrary metadata from transporter |
287
+
288
+ ```ts
289
+ posts.items.subscribe((docs) => console.log(docs.map(d => d.value)))
290
+ posts.loading.subscribe(({ all, next, prev }) => console.log({ all, next, prev }))
291
+ ```
292
+
293
+ `LivequeryLoadingState`:
294
+
295
+ ```ts
296
+ type LivequeryLoadingState = {
297
+ all: boolean // initial query in progress
298
+ next: boolean // loading next page
299
+ prev: boolean // loading previous page
300
+ }
301
+ ```
302
+
303
+ ---
304
+
305
+ ### LivequeryDocument
306
+
307
+ Each element of `collection.items.value` is a `LivequeryDocument<DocState<T>>`, which extends `BehaviorSubject<T>`. It provides convenient mutation helpers scoped to that document.
308
+
309
+ ```ts
310
+ class LivequeryDocument<T extends Doc> extends BehaviorSubject<T> {
311
+ update(data: Partial<T>): Promise<void>
312
+ del(): Promise<void>
313
+ trigger<R>(action: LivequeryActionType, payload: Record<string, any>): Observable<{ data: R; error?: Error }>
314
+ }
315
+ ```
316
+
317
+ ```ts
318
+ const doc = posts.items.value[0]
319
+
320
+ // Subscribe to individual document changes
321
+ doc.subscribe((post) => console.log("post changed:", post))
322
+
323
+ // Mutate directly on the document
324
+ await doc.update({ title: "Updated title" })
325
+ await doc.del()
326
+
327
+ // Fire a custom action
328
+ doc.trigger("~publish", { scheduledAt: Date.now() }).subscribe()
329
+ ```
330
+
331
+ ---
332
+
333
+ ## Query Filters
334
+
335
+ Filters are fully type-safe. The TypeScript compiler will only allow valid field paths and operators for your document type.
336
+
337
+ ### Pagination / sorting
338
+
339
+ | Key | Type | Description |
340
+ |-----|------|-------------|
341
+ | `"<field>:sort"` | `"asc" \| "desc"` | Sort by a string field |
342
+ | `":limit"` | `number` | Max items per page |
343
+ | `":page"` | `number` | Page number (1-based) |
344
+ | `":before"` | `string` | Cursor for previous-page fetch |
345
+ | `":after"` | `string` | Cursor for next-page fetch |
346
+
347
+ ### Field operators
348
+
349
+ | Operator | Applies to | Description |
350
+ |----------|-----------|-------------|
351
+ | `gt` | `number` | Greater than |
352
+ | `gte` | `number` | Greater than or equal |
353
+ | `lt` | `number` | Less than |
354
+ | `lte` | `number` | Less than or equal |
355
+ | `eq-number` | `number` | Strict numeric equality |
356
+ | `in` | `number \| string` | Value is in array |
357
+ | `nin` | `number \| string` | Value is NOT in array |
358
+ | `include` | `number[] \| string[]` | Array field includes value |
359
+ | `boolean` | `boolean` | `"true"`, `"false"`, `"not-true"`, `"not-false"` |
360
+ | `like` | `string` | Case-insensitive substring match |
361
+ | `null` | any | `"null-only"` or `"not-null"` |
362
+
363
+ ```ts
364
+ type Article = Doc & {
365
+ score: number
366
+ tags: string[]
367
+ title: string
368
+ archived: boolean
369
+ deletedAt: number | null
370
+ }
371
+
372
+ const filters: LivequeryFilters<Article> = {
373
+ "score:gte": 5,
374
+ "tags:include": "typescript",
375
+ "title:like": "livequery",
376
+ "archived:boolean": "false",
377
+ "deletedAt:null": "null-only",
378
+ "score:sort": "desc",
379
+ ":limit": 20,
380
+ ":page": 1,
381
+ ":before": "",
382
+ ":after": "",
383
+ }
384
+ ```
385
+
386
+ ---
387
+
388
+ ## API Reference
389
+
390
+ ### LivequeryMemoryStorage
391
+
392
+ An in-memory `LivequeryStorge` implementation backed by a `Map`. Data is lost on page reload. Useful for testing and offline-first prototypes.
393
+
394
+ ```ts
395
+ const storage = new LivequeryMemoryStorage()
396
+ ```
397
+
398
+ | Method | Signature | Description |
399
+ |--------|-----------|-------------|
400
+ | `query` | `(collection, filters?) → Promise<{documents, paging}>` | Filter, sort and paginate documents |
401
+ | `get` | `(collection, id) → Promise<T \| null>` | Fetch a single document by id |
402
+ | `add` | `(collection, document) → Promise<T>` | Upsert a document (insert or replace by id) |
403
+ | `update` | `(collection, id, partial) → Promise<T \| null>` | Merge partial fields into existing document |
404
+ | `delete` | `(collection, id) → Promise<T \| null>` | Remove and return a document |
405
+ | `seed` | `(collection, docs[]) → void` | Bulk-load documents (replaces existing) |
406
+ | `clear` | `(collection?) → void` | Clear one collection or all collections |
407
+
408
+ ```ts
409
+ storage.seed<Todo>("todos", [
410
+ { id: "1", title: "Write docs", done: false, createdAt: Date.now() }
411
+ ])
412
+
413
+ storage.clear("todos") // clear one collection
414
+ storage.clear() // clear everything
415
+ ```
416
+
417
+ ---
418
+
419
+ ### LivequeryCollection methods
420
+
421
+ | Method | Description |
422
+ |--------|-------------|
423
+ | `initialize()` | Wire up the core watcher and optionally auto-load (required before use) |
424
+ | `query(filters)` | Execute a fresh query replacing current items |
425
+ | `loadMore()` | Append next page using `paging.next.cursor` |
426
+ | `loadPrev()` | Prepend previous page using `paging.prev.cursor` |
427
+ | `loadAround(cursor)` | Load items around a specific cursor (both directions) |
428
+ | `add(payload)` | Optimistically add a new document |
429
+ | `update(id, payload)` | Optimistically update a document |
430
+ | `delete(id)` | Optimistically delete a document |
431
+ | `trigger(action, payload?)` | Fire a custom action (e.g. `"~publish"`) |
432
+
433
+ ```ts
434
+ // Paginate
435
+ await collection.loadMore()
436
+ await collection.loadPrev()
437
+ await collection.loadAround("cursor-abc")
438
+
439
+ // Mutate
440
+ await collection.add({ title: "New item", done: false, createdAt: Date.now() })
441
+ await collection.update("doc-id", { done: true })
442
+ await collection.delete("doc-id")
443
+
444
+ // Custom action handled by your transporter
445
+ collection.trigger("~sendEmail", { to: "user@example.com" }).subscribe()
446
+ ```
447
+
448
+ ---
449
+
450
+ ### WorkerRpc
451
+
452
+ `WorkerRpc` is a utility for calling services across a `SharedWorker` boundary using an Observable / Promise-compatible API.
453
+
454
+ #### Expose a service inside a SharedWorker
455
+
456
+ ```ts
457
+ // worker.ts
458
+ import { WorkerRpc } from "@livequery/new"
459
+
460
+ class DataService {
461
+ async getUser(id: string) {
462
+ return { id, name: "Alice" }
463
+ }
464
+ }
465
+
466
+ const rpc = new WorkerRpc()
467
+ rpc.exposeWorkerService(new DataService())
468
+ ```
469
+
470
+ #### Consume the service in the main thread
471
+
472
+ ```ts
473
+ // main.ts
474
+ import { WorkerRpc } from "@livequery/new"
475
+
476
+ const worker = new SharedWorker(new URL("./worker.ts", import.meta.url), { type: "module" })
477
+
478
+ // Returns a typed proxy
479
+ const service = WorkerRpc.linkWorkerService<DataService>("DataService", worker)
480
+
481
+ // Call as a Promise
482
+ const user = await service.getUser("123")
483
+
484
+ // Or subscribe as an Observable (for streaming methods)
485
+ service.getUser("123").subscribe((user) => console.log(user))
486
+ ```
487
+
488
+ The proxy is transparent — if the underlying method returns an `Observable`, the proxy streams values back; if it returns a `Promise` or plain value, it resolves once.
489
+
490
+ ---
491
+
492
+ ## Writing a Custom Transporter
493
+
494
+ Implement `LivequeryTransporter` to connect to any backend:
495
+
496
+ ```ts
497
+ import { Observable } from "rxjs"
498
+ import type {
499
+ LivequeryTransporter, Doc,
500
+ LivequeryQueryParams, LivequeryAction
501
+ } from "@livequery/new"
502
+
503
+ const httpTransporter: LivequeryTransporter = {
504
+ query<T extends Doc>(params: LivequeryQueryParams<T>) {
505
+ return new Observable(subscriber => {
506
+ fetch(`/api/${params.ref}?${new URLSearchParams(params.filters as any)}`)
507
+ .then(r => r.json())
508
+ .then(data => {
509
+ subscriber.next({
510
+ query_id: params.query_id,
511
+ changes: data.items.map((item: T) => ({ id: item.id, type: "added", data: item })),
512
+ paging: data.paging,
513
+ summary: data.summary ?? {},
514
+ metadata: {},
515
+ source: "query",
516
+ })
517
+ subscriber.complete()
518
+ })
519
+ .catch(err => subscriber.error(err))
520
+ })
521
+ },
522
+
523
+ trigger<T>(action: LivequeryAction) {
524
+ return new Observable<{ data: T }>(subscriber => {
525
+ const method = action.action === "delete" ? "DELETE"
526
+ : action.action === "add" ? "POST" : "PATCH"
527
+ fetch(`/api/${action.ref}`, {
528
+ method,
529
+ headers: { "Content-Type": "application/json" },
530
+ body: JSON.stringify(action.payload),
531
+ })
532
+ .then(r => r.json())
533
+ .then(data => { subscriber.next({ data }); subscriber.complete() })
534
+ .catch(err => subscriber.error(err))
535
+ })
536
+ },
537
+ }
538
+ ```
539
+
540
+ ---
541
+
542
+ ## Writing a Custom Storage Adapter
543
+
544
+ Implement `LivequeryStorge` to persist data in `localStorage`, `IndexedDB`, SQLite, etc.:
545
+
546
+ ```ts
547
+ import type { LivequeryStorge, Doc, LivequeryPaging } from "@livequery/new"
548
+
549
+ class LocalStorageAdapter implements LivequeryStorge {
550
+ private read<T>(collection: string): T[] {
551
+ return JSON.parse(localStorage.getItem(collection) ?? "[]")
552
+ }
553
+ private write<T>(collection: string, docs: T[]) {
554
+ localStorage.setItem(collection, JSON.stringify(docs))
555
+ }
556
+
557
+ async query<T extends Doc>(collection: string, filters?: Record<string, any>) {
558
+ const docs = this.read<T>(collection)
559
+ // apply filters, sort, paginate …
560
+ return { documents: docs, paging: { total: docs.length, current: docs.length } }
561
+ }
562
+
563
+ async get<T extends Doc>(collection: string, id: string) {
564
+ return this.read<T>(collection).find(d => d.id === id) ?? null
565
+ }
566
+
567
+ async add<T extends Doc>(collection: string, document: T) {
568
+ const docs = this.read<T>(collection)
569
+ const i = docs.findIndex(d => d.id === document.id)
570
+ if (i >= 0) docs[i] = document; else docs.push(document)
571
+ this.write(collection, docs)
572
+ return document
573
+ }
574
+
575
+ async update<T extends Doc>(collection: string, id: string, patch: Record<string, any>) {
576
+ const docs = this.read<T>(collection)
577
+ const i = docs.findIndex(d => d.id === id)
578
+ if (i < 0) return null
579
+ docs[i] = { ...docs[i], ...patch }
580
+ this.write(collection, docs)
581
+ return docs[i]
582
+ }
583
+
584
+ async delete<T extends Doc>(collection: string, id: string) {
585
+ const docs = this.read<T>(collection)
586
+ const i = docs.findIndex(d => d.id === id)
587
+ if (i < 0) return null
588
+ const [removed] = docs.splice(i, 1)
589
+ this.write(collection, docs)
590
+ return removed ?? null
591
+ }
592
+ }
593
+ ```
594
+
595
+ ---
596
+
597
+ ## Types Reference
598
+
599
+ ```ts
600
+ // Base document type — all documents must have an `id`
601
+ type Doc<T = {}> = T & { id: string }
602
+
603
+ // Document state inside a collection (tracks optimistic-update flags)
604
+ type DocState<T extends Doc> = T & {
605
+ _deleting?: boolean
606
+ _updating?: boolean
607
+ _adding?: boolean
608
+ _remotes?: Record<string, string | number>
609
+ _prev?: Partial<T>
610
+ }
611
+
612
+ // Change event emitted by transporters and the core
613
+ type DataChangeEvent<T extends Doc> = {
614
+ id: string
615
+ type: "added" | "updated" | "removed"
616
+ data?: Partial<Omit<T, "id">> | null
617
+ }
618
+
619
+ // Query parameters forwarded to every transporter
620
+ type LivequeryQueryParams<T extends Doc> = {
621
+ ref: string
622
+ query_id: string
623
+ collection_id: string
624
+ filters?: Partial<LivequeryFilters<T>>
625
+ headers?: Record<string, string>
626
+ }
627
+
628
+ // Result streamed back from a transporter query
629
+ type LivequeryQueryResult<T extends Doc> = {
630
+ query_id: string
631
+ changes: DataChangeEvent<T>[]
632
+ summary: Record<string, any>
633
+ paging: LivequeryPaging
634
+ metadata: Record<string, any>
635
+ source: "query" | "action" | "realtime"
636
+ }
637
+
638
+ // Action sent to every transporter for mutations
639
+ type LivequeryAction = {
640
+ ref: string
641
+ collection_id: string
642
+ action: "add" | "update" | "delete" | `~${string}`
643
+ payload?: Record<string, any>
644
+ headers?: Record<string, string>
645
+ }
646
+
647
+ // Pagination info
648
+ type LivequeryPaging = {
649
+ total: number
650
+ current: number
651
+ next?: { count: number; cursor: string }
652
+ prev?: { count: number; cursor: string }
653
+ }
654
+ ```
655
+
656
+ ---
657
+
658
+ ## Build
659
+
660
+ ```bash
661
+ bun run build
662
+ ```
663
+
664
+ Output is placed in `dist/` as ESM with TypeScript declarations (browser target).
665
+
666
+ ```bash
667
+ bun run build:watch # watch mode (JS only, no type declarations)
668
+ bun run clean # remove dist/
669
+ ```
@@ -0,0 +1,21 @@
1
+ import { Subject } from "rxjs";
2
+ export type RpcMessage = {
3
+ id: number;
4
+ request?: {
5
+ service: string;
6
+ method: string[];
7
+ args: any[];
8
+ };
9
+ cancel?: boolean;
10
+ response?: Partial<{
11
+ data: any;
12
+ error: string;
13
+ completed: boolean;
14
+ }>;
15
+ };
16
+ export declare abstract class RpcChannel extends Subject<RpcMessage & {
17
+ respond: (msg: RpcMessage) => void;
18
+ }> {
19
+ abstract send(message: RpcMessage): void;
20
+ }
21
+ //# sourceMappingURL=RpcChannel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RpcChannel.d.ts","sourceRoot":"","sources":["../src/RpcChannel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAG9B,MAAM,MAAM,UAAU,GAAG;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,CAAC,EAAE;QACN,OAAO,EAAE,MAAM,CAAA;QACf,MAAM,EAAE,MAAM,EAAE,CAAA;QAChB,IAAI,EAAE,GAAG,EAAE,CAAA;KACd,CAAA;IACD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;QACf,IAAI,EAAE,GAAG,CAAA;QACT,KAAK,EAAE,MAAM,CAAA;QACb,SAAS,EAAE,OAAO,CAAA;KACrB,CAAC,CAAA;CACL,CAAA;AAGD,8BAAsB,UAAW,SAAQ,OAAO,CAAC,UAAU,GAAG;IAAE,OAAO,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,IAAI,CAAA;CAAE,CAAC;IACjG,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;CAC3C"}
@@ -0,0 +1,9 @@
1
+ import type { RpcChannel } from "./RpcChannel";
2
+ import type { WorkerService } from "./WorkerService";
3
+ export declare class ServiceLinker {
4
+ #private;
5
+ private channel;
6
+ constructor(channel: RpcChannel);
7
+ linkService<T>(name: string): WorkerService<T>;
8
+ }
9
+ //# sourceMappingURL=ServiceLinker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ServiceLinker.d.ts","sourceRoot":"","sources":["../src/ServiceLinker.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAIrD,qBAAa,aAAa;;IAQV,OAAO,CAAC,OAAO;gBAAP,OAAO,EAAE,UAAU;IAevC,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAAC,CAAC,CAAC;CAsFjD"}