@livequery/rest 2.0.91 → 2.0.92

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.
Files changed (2) hide show
  1. package/README.md +405 -43
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,18 +1,40 @@
1
1
  # @livequery/rest
2
2
 
3
- REST transporter for [livequery](https://github.com/livequery) connects your livequery client to a REST API backend with optional real-time WebSocket support.
3
+ `@livequery/rest` is a REST + WebSocket transporter for `@livequery/core`.
4
+
5
+ It adapts the `LivequeryTransporter` interface to:
6
+
7
+ - HTTP requests for `query`, `add`, `update`, `delete`, and `trigger`
8
+ - optional WebSocket subscriptions for realtime collection updates
9
+ - request/response hooks for auth, caching, logging, or mocking
10
+
11
+ This package does not implement local cache or collection state management. That stays in `@livequery/core`. This package is only the transport layer between a livequery client and your backend.
4
12
 
5
13
  ## Installation
6
14
 
7
15
  ```bash
8
16
  npm install @livequery/rest
9
- # or
17
+ ```
18
+
19
+ ```bash
10
20
  bun add @livequery/rest
11
21
  ```
12
22
 
13
- ## Usage
23
+ ## What It Implements
24
+
25
+ `RestTransporter` implements the `LivequeryTransporter` contract from `@livequery/core`:
26
+
27
+ - `query()` returns an `Observable<Partial<LivequeryQueryResult>>`
28
+ - `add()` sends `POST`
29
+ - `update()` sends `PATCH`
30
+ - `delete()` sends `DELETE`
31
+ - `trigger()` sends `POST /<ref>/~<action>`
32
+
33
+ When a WebSocket endpoint is configured, `query()` can also attach a realtime subscription and merge server-pushed `DataChangeEvent`s into the observable stream.
34
+
35
+ ## Quick Start
14
36
 
15
- ### Basic REST
37
+ ### REST only
16
38
 
17
39
  ```ts
18
40
  import { RestTransporter } from '@livequery/rest'
@@ -22,88 +44,424 @@ const transporter = new RestTransporter({
22
44
  })
23
45
  ```
24
46
 
25
- ### With WebSocket (real-time updates)
47
+ ### REST + realtime
48
+
49
+ ```ts
50
+ import { RestTransporter } from '@livequery/rest'
51
+
52
+ const transporter = new RestTransporter({
53
+ api: 'https://api.example.com',
54
+ ws: 'wss://api.example.com/ws'
55
+ })
56
+ ```
57
+
58
+ ### With `@livequery/core`
26
59
 
27
60
  ```ts
61
+ import { LivequeryCore } from '@livequery/core'
62
+ import { RestTransporter } from '@livequery/rest'
63
+
28
64
  const transporter = new RestTransporter({
29
65
  api: 'https://api.example.com',
30
66
  ws: 'wss://api.example.com/ws'
31
67
  })
68
+
69
+ const core = new LivequeryCore({
70
+ storage,
71
+ transporters: {
72
+ rest: transporter
73
+ }
74
+ })
75
+ ```
76
+
77
+ ## Constructor
78
+
79
+ ```ts
80
+ type RestTransporterConfig = {
81
+ api: string
82
+ ws?: string
83
+ onRequest?: (
84
+ request: RestTransporterRequest & { ref: string }
85
+ ) =>
86
+ | void
87
+ | Partial<RestTransporterRequest & { response?: LivequeryResult<any> }>
88
+ | Promise<void | Partial<RestTransporterRequest & { response?: LivequeryResult<any> }>>
89
+ onResponse?: (
90
+ request: RestTransporterRequest & { ref: string },
91
+ response: LivequeryResult<any>
92
+ ) => void | Promise<void>
93
+ }
94
+ ```
95
+
96
+ ### Options
97
+
98
+ | Option | Type | Description |
99
+ | --- | --- | --- |
100
+ | `api` | `string` | Base HTTP URL used for all REST calls. |
101
+ | `ws` | `string` | Optional WebSocket endpoint for realtime sync. |
102
+ | `onRequest` | `function` | Optional interceptor before `fetch()`. Can override request fields or return a fake response. |
103
+ | `onResponse` | `function` | Optional hook called after the response is resolved. |
104
+
105
+ ## Request Model
106
+
107
+ Outgoing requests use this shape internally:
108
+
109
+ ```ts
110
+ type RestTransporterRequest = {
111
+ url: string
112
+ method: string
113
+ query?: Record<string, any>
114
+ body?: Record<string, any> | string
115
+ headers?: Record<string, string | undefined>
116
+ }
117
+ ```
118
+
119
+ The transporter builds URLs like this:
120
+
121
+ ```text
122
+ <api>/<ref>
123
+ <api>/<ref>?<query>
124
+ <api>/<ref>/~<action>
125
+ ```
126
+
127
+ Examples:
128
+
129
+ ```text
130
+ GET /users
131
+ GET /users/123
132
+ POST /users
133
+ PATCH /users/123
134
+ DELETE /users/123
135
+ POST /users/~ban
32
136
  ```
33
137
 
34
- ### Request / Response Hooks
138
+ ## Hooks
35
139
 
36
- Use `onRequest` to modify or intercept outgoing requests (e.g. inject auth headers), and `onResponse` to inspect responses.
140
+ ### `onRequest`
141
+
142
+ Use `onRequest` to:
143
+
144
+ - inject auth headers
145
+ - override request body or URL
146
+ - short-circuit requests from cache
147
+ - mock server responses in tests
37
148
 
38
149
  ```ts
39
150
  const transporter = new RestTransporter({
40
151
  api: 'https://api.example.com',
41
152
  ws: 'wss://api.example.com/ws',
42
-
43
- onRequest: async ({ url, method, headers, ref }) => {
153
+ onRequest: async ({ headers }) => {
44
154
  const token = await getAccessToken()
155
+
156
+ return {
157
+ headers: {
158
+ ...headers,
159
+ Authorization: `Bearer ${token}`
160
+ }
161
+ }
162
+ }
163
+ })
164
+ ```
165
+
166
+ To skip the network completely, return a `response`:
167
+
168
+ ```ts
169
+ const transporter = new RestTransporter({
170
+ api: 'https://api.example.com',
171
+ onRequest: ({ ref }) => {
172
+ const cached = cache.get(ref)
173
+ if (!cached) return
174
+
45
175
  return {
46
- headers: { Authorization: `Bearer ${token}` }
176
+ response: {
177
+ data: cached
178
+ }
47
179
  }
48
- },
180
+ }
181
+ })
182
+ ```
183
+
184
+ ### `onResponse`
185
+
186
+ Use `onResponse` for logging, metrics, or centralized error inspection:
49
187
 
188
+ ```ts
189
+ const transporter = new RestTransporter({
190
+ api: 'https://api.example.com',
50
191
  onResponse: async (request, response) => {
51
- if (response.error) console.error('API error', response.error)
192
+ if (response.error) {
193
+ console.error('Livequery REST error', {
194
+ url: request.url,
195
+ method: request.method,
196
+ error: response.error
197
+ })
198
+ }
52
199
  }
53
200
  })
54
201
  ```
55
202
 
56
- You can also short-circuit a request by returning a `response` from `onRequest` — useful for caching or mocking:
203
+ ## REST Response Contract
204
+
205
+ The transporter expects your backend to return a `LivequeryResult<T>` envelope:
57
206
 
58
207
  ```ts
59
- onRequest: ({ ref }) => {
60
- const cached = cache.get(ref)
61
- if (cached) return { response: { data: cached } }
208
+ type LivequeryResult<T> = {
209
+ data: T
210
+ error?: {
211
+ code: string
212
+ message: string
213
+ }
62
214
  }
63
215
  ```
64
216
 
65
- ## API
217
+ ### Collection query response
66
218
 
67
- ### `RestTransporter`
219
+ For collection reads, `data` should look like:
68
220
 
69
- #### Constructor options (`RestTransporterConfig`)
221
+ ```ts
222
+ type LivequeryCollectionResponse<T> = {
223
+ summary?: Record<string, any>
224
+ items: T[]
225
+ subscription_token?: string
226
+ count?: {
227
+ prev: number
228
+ next: number
229
+ total: number
230
+ current: number
231
+ }
232
+ has?: {
233
+ prev: boolean
234
+ next: boolean
235
+ }
236
+ cursor?: {
237
+ first: string
238
+ last: string
239
+ }
240
+ }
241
+ ```
70
242
 
71
- | Option | Type | Description |
72
- |---|---|---|
73
- | `api` | `string` | Base URL of your REST API |
74
- | `ws` | `string` (optional) | WebSocket endpoint for real-time updates |
75
- | `onRequest` | function (optional) | Interceptor called before each request. Return partial request overrides or a fake response. |
76
- | `onResponse` | function (optional) | Called after each response. |
243
+ Example:
244
+
245
+ ```json
246
+ {
247
+ "data": {
248
+ "items": [
249
+ { "id": "u1", "name": "Ada" },
250
+ { "id": "u2", "name": "Linus" }
251
+ ],
252
+ "summary": {
253
+ "active": 2
254
+ },
255
+ "count": {
256
+ "prev": 0,
257
+ "next": 20,
258
+ "current": 2,
259
+ "total": 22
260
+ },
261
+ "has": {
262
+ "prev": false,
263
+ "next": true
264
+ },
265
+ "cursor": {
266
+ "first": "cursor-1",
267
+ "last": "cursor-2"
268
+ },
269
+ "subscription_token": "rt_abc123"
270
+ }
271
+ }
272
+ ```
273
+
274
+ ### Single document response
77
275
 
78
- #### Methods
276
+ For document reads, `data` should contain `item`:
277
+
278
+ ```json
279
+ {
280
+ "data": {
281
+ "item": {
282
+ "id": "u1",
283
+ "name": "Ada"
284
+ }
285
+ }
286
+ }
287
+ ```
79
288
 
80
- These follow the `LivequeryTransporter` interface from `@livequery/core`:
289
+ ### Create response
81
290
 
82
- | Method | Description |
83
- |---|---|
84
- | `query(ref, filters)` | Query a collection or document. Returns an Observable. |
85
- | `add(ref, data)` | Create a new document (`POST`) |
86
- | `update(ref, id, data)` | Update a document (`PATCH`) |
87
- | `delete(ref, id)` | Delete a document (`DELETE`) |
88
- | `trigger({ ref, action, payload })` | Trigger a custom action (`POST /ref/~action`) |
291
+ `add()` accepts either of these backend shapes:
89
292
 
90
- ### `Socket`
293
+ ```json
294
+ {
295
+ "data": {
296
+ "item": {
297
+ "id": "u1",
298
+ "name": "Ada"
299
+ }
300
+ }
301
+ }
302
+ ```
303
+
304
+ ```json
305
+ {
306
+ "data": {
307
+ "id": "u1",
308
+ "name": "Ada"
309
+ }
310
+ }
311
+ ```
312
+
313
+ ## Query Output Shape
314
+
315
+ `query()` converts server data into the shape expected by `@livequery/core`:
316
+
317
+ ```ts
318
+ type LivequeryQueryResult = {
319
+ changes: DataChangeEvent[]
320
+ summary: Record<string, any>
321
+ paging: {
322
+ total: number
323
+ current: number
324
+ next?: { count: number; cursor: string }
325
+ prev?: { count: number; cursor: string }
326
+ }
327
+ source: 'query' | 'realtime' | 'action'
328
+ error: { code: string; message: string }
329
+ }
330
+ ```
331
+
332
+ Collection responses are converted to `added` events for each returned item. Document responses are converted to a single `added` event for the returned document.
333
+
334
+ ## Realtime
335
+
336
+ If `ws` is provided, the transporter creates a `Socket` instance and adds these headers to REST calls:
337
+
338
+ - `socket_id`
339
+ - `x-lcid`
340
+ - `x-lgid`
341
+
342
+ This lets your backend bind the HTTP query to the active realtime session.
343
+
344
+ ### Realtime flow
345
+
346
+ 1. A collection query returns `subscription_token`.
347
+ 2. The transporter forwards that token to the socket with a `subscribe` event.
348
+ 3. The socket listens for server `sync` messages.
349
+ 4. Incoming changes are emitted as `DataChangeEvent`s with `source: 'realtime'`.
350
+
351
+ Realtime listening is only attached for standard collection queries. It is skipped when:
352
+
353
+ - no `ws` endpoint is configured
354
+ - the request is a cursor query using `:after`
355
+ - the request is a cursor query using `:before`
356
+ - the request is an around query using `:around`
357
+
358
+ ### WebSocket protocol expected by `Socket`
359
+
360
+ When the socket opens, it sends:
361
+
362
+ ```json
363
+ { "event": "start", "data": { "id": "<client_id>" } }
364
+ ```
365
+
366
+ It also sends a heartbeat every 60 seconds:
367
+
368
+ ```json
369
+ { "event": "ping" }
370
+ ```
91
371
 
92
- Manages the WebSocket connection lifecycle automatically — reconnects on failure, sends heartbeat pings, and routes server-pushed `DataChangeEvent`s to the appropriate collection streams.
372
+ To subscribe a collection query, it sends:
373
+
374
+ ```json
375
+ { "event": "subscribe", "data": { "realtime_token": "<token>" } }
376
+ ```
377
+
378
+ The server should respond with a hello message containing the gateway id:
379
+
380
+ ```json
381
+ { "event": "hello", "gid": "gateway-1" }
382
+ ```
383
+
384
+ Realtime sync messages should look like:
385
+
386
+ ```json
387
+ {
388
+ "event": "sync",
389
+ "data": {
390
+ "changes": [
391
+ {
392
+ "ref": "users",
393
+ "id": "u1",
394
+ "type": "modified",
395
+ "data": { "name": "Ada Lovelace" }
396
+ }
397
+ ]
398
+ }
399
+ }
400
+ ```
401
+
402
+ Each sync change is routed to `listen(ref)` subscribers and normalized to:
403
+
404
+ ```ts
405
+ type DataChangeEvent = {
406
+ collection_ref: string
407
+ id: string
408
+ type: 'added' | 'removed' | 'modified'
409
+ data?: Record<string, any>
410
+ }
411
+ ```
412
+
413
+ ## Public API
414
+
415
+ ### `RestTransporter`
416
+
417
+ ```ts
418
+ import { RestTransporter } from '@livequery/rest'
419
+ ```
420
+
421
+ Methods:
422
+
423
+ - `query({ ref, filters })`
424
+ - `add(ref, data)`
425
+ - `update(collectionRef, id, data)`
426
+ - `delete(collectionRef, id)`
427
+ - `trigger({ ref, action, payload })`
428
+
429
+ ### `Socket`
93
430
 
94
431
  ```ts
95
432
  import { Socket } from '@livequery/rest'
433
+ ```
434
+
435
+ The socket class is exported for low-level integrations and debugging.
436
+
437
+ Example:
96
438
 
439
+ ```ts
97
440
  const socket = new Socket('wss://api.example.com/ws')
98
- socket.listen('users/123').subscribe(change => console.log(change))
99
- socket.stop() // close connection
441
+
442
+ socket.listen('users').subscribe(change => {
443
+ console.log(change)
444
+ })
445
+
446
+ socket.stop()
447
+ ```
448
+
449
+ ## Error Handling
450
+
451
+ If `fetch()` throws or the backend returns an error envelope, the transporter surfaces it as:
452
+
453
+ ```ts
454
+ {
455
+ error: {
456
+ code: string,
457
+ message: string
458
+ }
459
+ }
100
460
  ```
101
461
 
102
- ## Real-time Flow
462
+ For `query()`, errors are emitted as an observable result with `source: 'query'`.
103
463
 
104
- 1. On `query()`, a `subscription_token` returned by the server is forwarded to the socket.
105
- 2. The socket subscribes to the token and listens for `sync` events from the server.
106
- 3. Incoming changes are emitted as `DataChangeEvent`s with `source: "realtime"`.
464
+ For `add()`, `update()`, `delete()`, and `trigger()`, errors are thrown as rejected promises.
107
465
 
108
466
  ## Build
109
467
 
@@ -111,7 +469,11 @@ socket.stop() // close connection
111
469
  bun run build
112
470
  ```
113
471
 
114
- Outputs ESM + type declarations to `dist/`.
472
+ Build steps:
473
+
474
+ - clean `dist/`
475
+ - bundle ESM output with Bun
476
+ - generate `.d.ts` files with TypeScript
115
477
 
116
478
  ## License
117
479
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "repository": {
4
4
  "url": "https://github.com/livequery/rest"
5
5
  },
6
- "version": "2.0.91",
6
+ "version": "2.0.92",
7
7
  "type": "module",
8
8
  "description": "",
9
9
  "main": "build/index.js",
@@ -23,7 +23,7 @@
23
23
  "uuid": "^13.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@livequery/core": "2.0.74",
26
+ "@livequery/core": "2.0.92",
27
27
  "typescript": "5.6.3"
28
28
  },
29
29
  "peerDependencies": {},