@livequery/core 2.0.104 → 2.0.106
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/LIVEQUERY_SPEC.md +451 -0
- package/README.md +571 -314
- package/build/src/ApiGatewayHandler.d.ts +56 -0
- package/build/src/ApiGatewayHandler.js +210 -0
- package/build/src/ApiGatewayHandler.js.map +1 -0
- package/build/src/ApiGatewayLinker.d.ts +61 -0
- package/build/src/ApiGatewayLinker.js +229 -0
- package/build/src/ApiGatewayLinker.js.map +1 -0
- package/build/src/ApiServiceLinker.d.ts +18 -0
- package/build/src/ApiServiceLinker.js +61 -0
- package/build/src/ApiServiceLinker.js.map +1 -0
- package/build/src/LivequeryContext.d.ts +43 -0
- package/build/src/LivequeryContext.js +2 -0
- package/build/src/LivequeryContext.js.map +1 -0
- package/build/src/LivequeryDatasource.d.ts +8 -0
- package/build/src/LivequeryDatasource.js +2 -0
- package/build/src/LivequeryDatasource.js.map +1 -0
- package/build/src/LivequeryRequestParser.d.ts +13 -0
- package/build/src/LivequeryRequestParser.js +34 -0
- package/build/src/LivequeryRequestParser.js.map +1 -0
- package/build/src/UdpDiscovery.d.ts +26 -0
- package/build/src/UdpDiscovery.js +238 -0
- package/build/src/UdpDiscovery.js.map +1 -0
- package/build/src/WebsocketGateway.d.ts +36 -0
- package/build/src/WebsocketGateway.js +353 -0
- package/build/src/WebsocketGateway.js.map +1 -0
- package/build/src/const.d.ts +8 -0
- package/build/src/const.js +10 -0
- package/build/src/const.js.map +1 -0
- package/build/src/headers.d.ts +9 -0
- package/build/src/headers.js +32 -0
- package/build/src/headers.js.map +1 -0
- package/build/src/helpers/PathHelper.d.ts +11 -0
- package/build/src/helpers/PathHelper.js +41 -0
- package/build/src/helpers/PathHelper.js.map +1 -0
- package/build/src/helpers/hidePrivateFields.d.ts +3 -0
- package/build/src/helpers/hidePrivateFields.js +29 -0
- package/build/src/helpers/hidePrivateFields.js.map +1 -0
- package/build/src/helpers/nodeRequestToWebRequest.d.ts +7 -0
- package/build/src/helpers/nodeRequestToWebRequest.js +17 -0
- package/build/src/helpers/nodeRequestToWebRequest.js.map +1 -0
- package/build/src/helpers/writeWebResponse.d.ts +2 -0
- package/build/src/helpers/writeWebResponse.js +9 -0
- package/build/src/helpers/writeWebResponse.js.map +1 -0
- package/build/src/index.d.ts +9 -0
- package/build/src/index.js +10 -0
- package/build/src/index.js.map +1 -0
- package/build/src/parseLivequeryRequest.d.ts +10 -0
- package/build/src/parseLivequeryRequest.js +17 -0
- package/build/src/parseLivequeryRequest.js.map +1 -0
- package/build/tsconfig.tsbuildinfo +1 -0
- package/package.json +40 -103
- package/dist/LivequeryCollection.d.ts +0 -92
- package/dist/LivequeryCollection.d.ts.map +0 -1
- package/dist/LivequeryCollection.js +0 -231
- package/dist/LivequeryCollection.js.map +0 -1
- package/dist/LivequeryCore.d.ts +0 -67
- package/dist/LivequeryCore.d.ts.map +0 -1
- package/dist/LivequeryCore.js +0 -343
- package/dist/LivequeryCore.js.map +0 -1
- package/dist/LivequeryDocument.d.ts +0 -23
- package/dist/LivequeryDocument.d.ts.map +0 -1
- package/dist/LivequeryDocument.js +0 -22
- package/dist/LivequeryDocument.js.map +0 -1
- package/dist/LivequeryMemoryStorage.d.ts +0 -14
- package/dist/LivequeryMemoryStorage.d.ts.map +0 -1
- package/dist/LivequeryMemoryStorage.js +0 -89
- package/dist/LivequeryMemoryStorage.js.map +0 -1
- package/dist/LivequeryStorge.d.ts +0 -12
- package/dist/LivequeryStorge.d.ts.map +0 -1
- package/dist/LivequeryStorge.js +0 -2
- package/dist/LivequeryStorge.js.map +0 -1
- package/dist/LivequeryTransporter.d.ts +0 -22
- package/dist/LivequeryTransporter.d.ts.map +0 -1
- package/dist/LivequeryTransporter.js +0 -2
- package/dist/LivequeryTransporter.js.map +0 -1
- package/dist/helpers/filterDocs.d.ts +0 -5
- package/dist/helpers/filterDocs.d.ts.map +0 -1
- package/dist/helpers/filterDocs.js +0 -80
- package/dist/helpers/filterDocs.js.map +0 -1
- package/dist/helpers/tryCatch.d.ts +0 -2
- package/dist/helpers/tryCatch.d.ts.map +0 -1
- package/dist/helpers/tryCatch.js +0 -10
- package/dist/helpers/tryCatch.js.map +0 -1
- package/dist/helpers/whenCompleted.d.ts +0 -3
- package/dist/helpers/whenCompleted.d.ts.map +0 -1
- package/dist/helpers/whenCompleted.js +0 -5
- package/dist/helpers/whenCompleted.js.map +0 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -9
- package/dist/index.js.map +0 -1
- package/dist/types.d.ts +0 -70
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,477 +1,734 @@
|
|
|
1
1
|
# @livequery/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@livequery/core` is the framework-agnostic runtime layer for Livequery.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It provides the shared primitives used by HTTP adapters, data-source adapters, API gateway processes, service processes, and realtime synchronization layers. The package does not run database queries by itself and does not require a specific HTTP framework.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The framework-independent Livequery protocol is defined in [`LIVEQUERY_SPEC.md`](./LIVEQUERY_SPEC.md). Read that file for the canonical definitions of refs, collection/document paths, actions, custom actions, response envelopes, fake/non-database handlers, and realtime update emission.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## What This Project Does
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Livequery treats an HTTP request path as a structured data reference.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
13
|
+
Examples:
|
|
14
|
+
|
|
15
|
+
- `/livequery/posts` points to the `posts` collection.
|
|
16
|
+
- `/livequery/posts/p1` points to the `posts/p1` document.
|
|
17
|
+
- `/livequery/users/u1/posts` points to the nested `users/u1/posts` collection.
|
|
18
|
+
- `/livequery/users/u1/posts/p1` points to the nested `users/u1/posts/p1` document.
|
|
19
|
+
|
|
20
|
+
This package handles the core infrastructure around that model:
|
|
21
|
+
|
|
22
|
+
- Parse raw framework requests into normalized `LivequeryRequest` objects.
|
|
23
|
+
- Pass request state through a shared `LivequeryContext`.
|
|
24
|
+
- Define a handler interface for parser, middleware, datasource, and realtime handlers.
|
|
25
|
+
- Discover gateway and service nodes over UDP.
|
|
26
|
+
- Route HTTP requests through an API gateway to online service nodes.
|
|
27
|
+
- Publish service metadata from service nodes.
|
|
28
|
+
- Manage realtime WebSocket subscriptions and update forwarding.
|
|
29
|
+
- Sanitize response objects by hiding private fields.
|
|
17
30
|
|
|
18
31
|
## Installation
|
|
19
32
|
|
|
20
|
-
```
|
|
21
|
-
bun add @livequery/core
|
|
33
|
+
```sh
|
|
34
|
+
bun add @livequery/core
|
|
22
35
|
```
|
|
23
36
|
|
|
24
|
-
For
|
|
37
|
+
For local development in this repository:
|
|
25
38
|
|
|
26
|
-
```
|
|
27
|
-
bun
|
|
39
|
+
```sh
|
|
40
|
+
bun install
|
|
41
|
+
bun run build
|
|
42
|
+
bun test tests/
|
|
28
43
|
```
|
|
29
44
|
|
|
30
|
-
|
|
45
|
+
Type-check tests:
|
|
31
46
|
|
|
32
|
-
|
|
47
|
+
```sh
|
|
48
|
+
bunx tsc -p tests/tsconfig.json --noEmit
|
|
49
|
+
```
|
|
33
50
|
|
|
34
|
-
|
|
51
|
+
## Public Entry Point
|
|
35
52
|
|
|
36
53
|
```ts
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
import {
|
|
55
|
+
ApiGatewayHandler,
|
|
56
|
+
ApiServiceLinker,
|
|
57
|
+
LivequeryRequestParser,
|
|
58
|
+
UdpDiscovery,
|
|
59
|
+
WebsocketGateway,
|
|
60
|
+
hidePrivateFields,
|
|
61
|
+
} from '@livequery/core'
|
|
45
62
|
```
|
|
46
63
|
|
|
47
64
|
## Core Types
|
|
48
65
|
|
|
49
|
-
### `
|
|
66
|
+
### `CollectionResponse<T>`
|
|
67
|
+
|
|
68
|
+
Response shape for collection queries.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
type CollectionResponse<T> = {
|
|
72
|
+
items: T[]
|
|
73
|
+
paging: {
|
|
74
|
+
current: number
|
|
75
|
+
total: number
|
|
76
|
+
}
|
|
77
|
+
cursor: {
|
|
78
|
+
current: string
|
|
79
|
+
next: string
|
|
80
|
+
prev: string
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Use this when a handler returns a list of items with paging and cursor metadata.
|
|
86
|
+
|
|
87
|
+
### `DocumentResponse<T>`
|
|
50
88
|
|
|
51
|
-
|
|
89
|
+
Response shape for document queries.
|
|
52
90
|
|
|
53
91
|
```ts
|
|
54
|
-
type
|
|
55
|
-
|
|
92
|
+
type DocumentResponse<T> = {
|
|
93
|
+
item: T
|
|
56
94
|
}
|
|
57
95
|
```
|
|
58
96
|
|
|
59
|
-
|
|
97
|
+
Use this when a handler returns one document.
|
|
60
98
|
|
|
61
|
-
|
|
99
|
+
### `RawRequest`
|
|
100
|
+
|
|
101
|
+
The request shape expected from a framework adapter before Livequery parsing.
|
|
62
102
|
|
|
63
103
|
```ts
|
|
64
|
-
type
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
_prev?: Partial<T>
|
|
104
|
+
type RawRequest = {
|
|
105
|
+
path: string
|
|
106
|
+
ref: string
|
|
107
|
+
method: string
|
|
108
|
+
body?: any
|
|
109
|
+
params: Record<string, any>
|
|
110
|
+
query: Record<string, any>
|
|
111
|
+
headers: Map<string, string>
|
|
73
112
|
}
|
|
74
113
|
```
|
|
75
114
|
|
|
76
|
-
|
|
115
|
+
- `path` is the actual request path, for example `/livequery/posts/p1`.
|
|
116
|
+
- `ref` is the route pattern, for example `/livequery/posts/:id`.
|
|
117
|
+
- `params` contains framework route params.
|
|
118
|
+
- `query` contains parsed query params.
|
|
119
|
+
- `headers` is a string map used by handlers such as `WebsocketGateway`.
|
|
120
|
+
|
|
121
|
+
### `LivequeryRequest<I>`
|
|
77
122
|
|
|
78
|
-
|
|
123
|
+
The normalized request shape created by `LivequeryRequestParser`.
|
|
79
124
|
|
|
80
125
|
```ts
|
|
81
|
-
type
|
|
126
|
+
type LivequeryRequest<I> = {
|
|
127
|
+
keys: Record<string, any>
|
|
128
|
+
path: string
|
|
129
|
+
document_id?: string
|
|
82
130
|
collection_ref: string
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
131
|
+
schema_collection_ref: string
|
|
132
|
+
ref: string
|
|
133
|
+
method: string
|
|
134
|
+
body: I
|
|
135
|
+
query: Record<string, any>
|
|
86
136
|
}
|
|
87
137
|
```
|
|
88
138
|
|
|
89
|
-
|
|
139
|
+
### `LivequeryContext<T>`
|
|
90
140
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
141
|
+
The shared context passed through Livequery handlers.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
type LivequeryContext<T = {}> = {
|
|
145
|
+
request: RawRequest
|
|
146
|
+
livequery?: LivequeryRequest<any>
|
|
147
|
+
response?: T
|
|
148
|
+
}
|
|
99
149
|
```
|
|
100
150
|
|
|
101
|
-
|
|
102
|
-
- `LivequeryDocument` wraps an item as a `BehaviorSubject` with convenience mutation methods.
|
|
103
|
-
- `LivequeryCore` coordinates storage, transporters, optimistic writes, and fan-out to watchers.
|
|
104
|
-
- `LivequeryStorge` is the local persistence contract.
|
|
105
|
-
- `LivequeryTransporter` is the remote sync contract.
|
|
151
|
+
### `LivequeryHandler<O>`
|
|
106
152
|
|
|
107
|
-
|
|
153
|
+
The common handler contract.
|
|
108
154
|
|
|
109
155
|
```ts
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
LivequeryCore,
|
|
113
|
-
LivequeryMemoryStorage,
|
|
114
|
-
type Doc,
|
|
115
|
-
type LivequeryQueryResult,
|
|
116
|
-
type LivequeryTransporter,
|
|
117
|
-
} from "@livequery/core"
|
|
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
|
-
},
|
|
156
|
+
type LivequeryHandler<O = {}> = {
|
|
157
|
+
handle(ctx: LivequeryContext<O>): any
|
|
150
158
|
}
|
|
159
|
+
```
|
|
151
160
|
|
|
152
|
-
|
|
153
|
-
storage,
|
|
154
|
-
transporters: {
|
|
155
|
-
primary: transporter,
|
|
156
|
-
},
|
|
157
|
-
})
|
|
161
|
+
Use this interface for parsers, middleware, datasource adapters, auth handlers, and realtime handlers.
|
|
158
162
|
|
|
159
|
-
|
|
160
|
-
filters: { "createdAt:sort": "desc" },
|
|
161
|
-
mode: "server-first",
|
|
162
|
-
})
|
|
163
|
+
## `LivequeryRequestParser`
|
|
163
164
|
|
|
164
|
-
|
|
165
|
+
`LivequeryRequestParser` is the first handler in a typical request pipeline. It reads `ctx.request` and writes `ctx.livequery`.
|
|
165
166
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
### When To Use It
|
|
168
|
+
|
|
169
|
+
Use it inside HTTP framework adapters before invoking datasource or business logic handlers. Downstream handlers should rely on `ctx.livequery` instead of reparsing paths.
|
|
169
170
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
### Constructor
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
new LivequeryRequestParser()
|
|
174
175
|
```
|
|
175
176
|
|
|
176
|
-
|
|
177
|
+
### `handle(ctx)`
|
|
178
|
+
|
|
179
|
+
Parses a raw request into:
|
|
180
|
+
|
|
181
|
+
- `ref`: actual data reference, for example `posts/p1`.
|
|
182
|
+
- `collection_ref`: collection reference, for example `posts`.
|
|
183
|
+
- `schema_collection_ref`: route-pattern-based collection reference, for example `users/uid/posts`.
|
|
184
|
+
- `document_id`: document id when the request targets a document.
|
|
185
|
+
- `method`: uppercased request method.
|
|
186
|
+
- `keys`, `body`, `query`, and original `path`.
|
|
187
|
+
|
|
188
|
+
It also removes:
|
|
177
189
|
|
|
178
|
-
|
|
190
|
+
- The route prefix before the data ref, such as `livequery`.
|
|
191
|
+
- Query strings before parsing path segments.
|
|
192
|
+
- Realtime suffixes after `~` in the pathname.
|
|
193
|
+
|
|
194
|
+
Query values are preserved in `query`. A `~` inside the query string is not treated as a realtime suffix.
|
|
195
|
+
|
|
196
|
+
### Example
|
|
179
197
|
|
|
180
198
|
```ts
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
199
|
+
import { LivequeryRequestParser, type LivequeryContext } from '@livequery/core'
|
|
200
|
+
|
|
201
|
+
const ctx: LivequeryContext = {
|
|
202
|
+
request: {
|
|
203
|
+
path: '/livequery/posts/p1',
|
|
204
|
+
ref: '/livequery/posts/:id',
|
|
205
|
+
method: 'get',
|
|
206
|
+
params: { id: 'p1' },
|
|
207
|
+
query: {},
|
|
208
|
+
headers: new Map(),
|
|
185
209
|
},
|
|
186
|
-
}
|
|
187
|
-
```
|
|
210
|
+
}
|
|
188
211
|
|
|
189
|
-
|
|
212
|
+
new LivequeryRequestParser().handle(ctx)
|
|
213
|
+
|
|
214
|
+
console.log(ctx.livequery)
|
|
215
|
+
// {
|
|
216
|
+
// ref: 'posts/p1',
|
|
217
|
+
// collection_ref: 'posts',
|
|
218
|
+
// schema_collection_ref: 'posts',
|
|
219
|
+
// document_id: 'p1',
|
|
220
|
+
// keys: { id: 'p1' },
|
|
221
|
+
// method: 'GET',
|
|
222
|
+
// ...
|
|
223
|
+
// }
|
|
224
|
+
```
|
|
190
225
|
|
|
191
|
-
|
|
226
|
+
## `LivequeryDatasource`
|
|
192
227
|
|
|
193
|
-
|
|
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
|
|
228
|
+
`LivequeryDatasource<RouteConfig>` is a type for datasource adapters.
|
|
197
229
|
|
|
198
|
-
|
|
230
|
+
```ts
|
|
231
|
+
type LivequeryDatasourceInitConfig<Config> = Config & {
|
|
232
|
+
method: string
|
|
233
|
+
path: string
|
|
234
|
+
}
|
|
199
235
|
|
|
200
|
-
|
|
236
|
+
type LivequeryDatasource<RouteConfig> = LivequeryHandler & {
|
|
237
|
+
init(routes: Array<LivequeryDatasourceInitConfig<RouteConfig>>): Promise<void> | void
|
|
238
|
+
}
|
|
239
|
+
```
|
|
201
240
|
|
|
202
|
-
|
|
241
|
+
### When To Use It
|
|
203
242
|
|
|
204
|
-
|
|
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.
|
|
243
|
+
Use this type when implementing an adapter that connects Livequery requests to a database, external API, or framework-specific route system.
|
|
207
244
|
|
|
208
|
-
|
|
245
|
+
A datasource should:
|
|
209
246
|
|
|
210
|
-
|
|
247
|
+
- Implement `handle(ctx)` to process a request.
|
|
248
|
+
- Implement `init(routes)` to register route configuration.
|
|
211
249
|
|
|
212
|
-
|
|
250
|
+
### Example
|
|
213
251
|
|
|
214
252
|
```ts
|
|
215
|
-
type
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
253
|
+
import type { LivequeryContext, LivequeryDatasource } from '@livequery/core'
|
|
254
|
+
|
|
255
|
+
type RouteConfig = { table: string }
|
|
256
|
+
|
|
257
|
+
class MemoryDatasource implements LivequeryDatasource<RouteConfig> {
|
|
258
|
+
#routes = new Map<string, RouteConfig>()
|
|
259
|
+
|
|
260
|
+
init(routes: Array<RouteConfig & { method: string; path: string }>) {
|
|
261
|
+
for (const route of routes) {
|
|
262
|
+
this.#routes.set(`${route.method.toUpperCase()} ${route.path}`, route)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
handle(ctx: LivequeryContext) {
|
|
267
|
+
if (!ctx.livequery) return
|
|
268
|
+
|
|
269
|
+
ctx.response = {
|
|
270
|
+
item: {
|
|
271
|
+
id: ctx.livequery.document_id,
|
|
272
|
+
ref: ctx.livequery.ref,
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
}
|
|
221
276
|
}
|
|
222
277
|
```
|
|
223
278
|
|
|
224
|
-
|
|
279
|
+
## `ApiGatewayHandler`
|
|
280
|
+
|
|
281
|
+
`ApiGatewayHandler` is an HTTP reverse proxy and route registry for Livequery service nodes.
|
|
282
|
+
|
|
283
|
+
It can:
|
|
284
|
+
|
|
285
|
+
- Receive service metadata from `UdpDiscovery`.
|
|
286
|
+
- Register routes by method and path.
|
|
287
|
+
- Forward HTTP requests to online service nodes.
|
|
288
|
+
- Round-robin between multiple hosts for the same route.
|
|
289
|
+
- Connect to service WebSocket gateways when service metadata includes `ws`.
|
|
290
|
+
|
|
291
|
+
### When To Use It
|
|
225
292
|
|
|
226
|
-
|
|
293
|
+
Use this in a gateway process. Public HTTP requests enter the gateway and are forwarded to service nodes discovered at runtime.
|
|
294
|
+
|
|
295
|
+
### Constructor
|
|
227
296
|
|
|
228
297
|
```ts
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
mode: "cache-first",
|
|
298
|
+
new ApiGatewayHandler({
|
|
299
|
+
node_id?: string
|
|
300
|
+
discovery?: UdpDiscovery<ServiceApiMetadata>
|
|
301
|
+
ws?: WebsocketGateway
|
|
234
302
|
})
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
- `node_id`: stable id for this gateway. A random id is used when omitted.
|
|
306
|
+
- `discovery`: custom discovery instance, useful in tests or custom network setups.
|
|
307
|
+
- `ws`: realtime gateway used for cross-gateway WebSocket forwarding.
|
|
308
|
+
|
|
309
|
+
### `register(options)`
|
|
235
310
|
|
|
236
|
-
|
|
311
|
+
Registers service routes manually.
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
gateway.register({
|
|
315
|
+
node_id: 'service-1',
|
|
316
|
+
hostname: '127.0.0.1',
|
|
317
|
+
port: 3001,
|
|
318
|
+
paths: [{ method: 'GET', path: 'livequery/posts' }],
|
|
319
|
+
})
|
|
237
320
|
```
|
|
238
321
|
|
|
239
|
-
|
|
322
|
+
### `deregister(node_id)`
|
|
323
|
+
|
|
324
|
+
Removes all route hosts for a service node.
|
|
325
|
+
|
|
326
|
+
Use this when a service goes offline or when a forwarded request fails.
|
|
240
327
|
|
|
241
|
-
###
|
|
328
|
+
### `fetch(request)`
|
|
242
329
|
|
|
243
|
-
|
|
330
|
+
Accepts a Web `Request`, forwards it to the selected service, and returns a Web `Response`.
|
|
244
331
|
|
|
245
332
|
```ts
|
|
246
|
-
|
|
247
|
-
|
|
333
|
+
const response = await gateway.fetch(
|
|
334
|
+
new Request('http://gateway/livequery/posts')
|
|
335
|
+
)
|
|
248
336
|
```
|
|
249
337
|
|
|
250
|
-
|
|
338
|
+
### `fetch(req, res, extraHeaders?)`
|
|
339
|
+
|
|
340
|
+
Accepts Node.js `IncomingMessage` and `ServerResponse`.
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
import * as http from 'http'
|
|
344
|
+
import { ApiGatewayHandler } from '@livequery/core'
|
|
345
|
+
|
|
346
|
+
const gateway = new ApiGatewayHandler({})
|
|
347
|
+
|
|
348
|
+
http.createServer((req, res) => {
|
|
349
|
+
gateway.fetch(req as any, res)
|
|
350
|
+
}).listen(3000)
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### `fetchRequest(request)`
|
|
354
|
+
|
|
355
|
+
Alias for `fetch(request)`.
|
|
356
|
+
|
|
357
|
+
### `close()`
|
|
358
|
+
|
|
359
|
+
Unsubscribes from discovery, closes discovery sockets, disconnects service subscriptions, and clears service state.
|
|
360
|
+
|
|
361
|
+
### Error Responses
|
|
362
|
+
|
|
363
|
+
- Missing route: `404 { error: { status: 404, code: 'API_NOT_FOUND' } }`
|
|
364
|
+
- Route exists but no host is online: `503 { error: { status: 503, code: 'API_OFFLINE' } }`
|
|
365
|
+
- Forwarded service request fails: `502 { error: { status: 502, code: 'SERVICE_API_OFFLINE' } }`
|
|
366
|
+
|
|
367
|
+
## `ApiServiceLinker`
|
|
368
|
+
|
|
369
|
+
`ApiServiceLinker` publishes service metadata so gateways can discover and route to a service node.
|
|
251
370
|
|
|
252
|
-
###
|
|
371
|
+
### When To Use It
|
|
253
372
|
|
|
254
|
-
|
|
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>`
|
|
373
|
+
Use this inside each service process that should be discoverable by an `ApiGatewayHandler`.
|
|
260
374
|
|
|
261
|
-
|
|
375
|
+
### Constructor
|
|
262
376
|
|
|
263
377
|
```ts
|
|
264
|
-
|
|
265
|
-
|
|
378
|
+
new ApiServiceLinker({
|
|
379
|
+
paths: [{ method: 'GET', path: 'livequery/posts' }],
|
|
380
|
+
node_id?: 'service-1',
|
|
381
|
+
discovery?: customDiscovery,
|
|
382
|
+
ws?: websocketGateway,
|
|
266
383
|
})
|
|
267
|
-
|
|
268
|
-
subscription.unsubscribe()
|
|
269
384
|
```
|
|
270
385
|
|
|
271
|
-
|
|
386
|
+
### `start(name, port)`
|
|
272
387
|
|
|
273
|
-
|
|
274
|
-
function TodoList({ collection }: { collection: LivequeryCollection<Todo> }) {
|
|
275
|
-
const [items, setItems] = useState(() => collection.items.value)
|
|
388
|
+
Broadcasts service metadata through UDP discovery.
|
|
276
389
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
390
|
+
```ts
|
|
391
|
+
const linker = new ApiServiceLinker({
|
|
392
|
+
paths: [{ method: 'GET', path: 'livequery/posts' }],
|
|
393
|
+
})
|
|
281
394
|
|
|
282
|
-
|
|
283
|
-
<ul>
|
|
284
|
-
{items.map((item) => (
|
|
285
|
-
<li key={item.value.id}>{item.value.title}</li>
|
|
286
|
-
))}
|
|
287
|
-
</ul>
|
|
288
|
-
)
|
|
289
|
-
}
|
|
395
|
+
linker.start('posts-service', 3001)
|
|
290
396
|
```
|
|
291
397
|
|
|
292
|
-
|
|
398
|
+
When the service sees a gateway in the same namespace, it refreshes its metadata version and broadcasts again.
|
|
399
|
+
|
|
400
|
+
### `close()`
|
|
401
|
+
|
|
402
|
+
Unsubscribes from discovery and closes the discovery instance.
|
|
403
|
+
|
|
404
|
+
## `UdpDiscovery`
|
|
405
|
+
|
|
406
|
+
`UdpDiscovery<T>` is an observable UDP discovery layer. It sends and receives msgpack packets signed with HMAC SHA-256.
|
|
407
|
+
|
|
408
|
+
### When To Use It
|
|
409
|
+
|
|
410
|
+
Use it when gateway and service nodes need to discover each other without a central registry. `ApiGatewayHandler` and `ApiServiceLinker` create a default instance when no custom discovery is provided.
|
|
411
|
+
|
|
412
|
+
### Constructor
|
|
293
413
|
|
|
294
414
|
```ts
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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>]>
|
|
415
|
+
const discovery = new UdpDiscovery<MyNode>({
|
|
416
|
+
key: 'shared-secret',
|
|
417
|
+
port: 11001,
|
|
418
|
+
})
|
|
306
419
|
```
|
|
307
420
|
|
|
308
|
-
|
|
421
|
+
All trusted nodes must use the same key.
|
|
422
|
+
|
|
423
|
+
### `status$`
|
|
424
|
+
|
|
425
|
+
Observable lifecycle status:
|
|
426
|
+
|
|
427
|
+
- `not_ready`
|
|
428
|
+
- `ready`
|
|
429
|
+
- `closed`
|
|
309
430
|
|
|
310
|
-
|
|
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.
|
|
431
|
+
### `broadcast(node, targetIp?)`
|
|
315
432
|
|
|
316
|
-
|
|
433
|
+
Broadcasts a node metadata packet.
|
|
317
434
|
|
|
318
|
-
|
|
435
|
+
If `targetIp` is omitted, the packet is sent to configured multicast peers and local multicast. If `targetIp` is provided, the packet is sent only to that address or list of addresses.
|
|
319
436
|
|
|
320
437
|
```ts
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
438
|
+
await discovery.broadcast({
|
|
439
|
+
node_id: 'service-1',
|
|
440
|
+
namespace: 'default',
|
|
441
|
+
version: Date.now(),
|
|
442
|
+
role: 'service',
|
|
443
|
+
})
|
|
326
444
|
```
|
|
327
445
|
|
|
328
|
-
|
|
446
|
+
### `close()`
|
|
447
|
+
|
|
448
|
+
Closes sockets and completes the observable streams.
|
|
449
|
+
|
|
450
|
+
### Example
|
|
329
451
|
|
|
330
452
|
```ts
|
|
331
|
-
|
|
453
|
+
import { UdpDiscovery, type UdpDiscoveryNode } from '@livequery/core'
|
|
332
454
|
|
|
333
|
-
|
|
334
|
-
|
|
455
|
+
type Node = UdpDiscoveryNode & { role: 'service' | 'gateway' }
|
|
456
|
+
|
|
457
|
+
const discovery = new UdpDiscovery<Node>({ key: 'livequery/' })
|
|
458
|
+
|
|
459
|
+
discovery.subscribe(node => {
|
|
460
|
+
console.log('node online', node)
|
|
335
461
|
})
|
|
336
462
|
|
|
337
|
-
await
|
|
338
|
-
|
|
339
|
-
|
|
463
|
+
await discovery.broadcast({
|
|
464
|
+
node_id: 'service-1',
|
|
465
|
+
namespace: 'default',
|
|
466
|
+
version: Date.now(),
|
|
467
|
+
role: 'service',
|
|
468
|
+
})
|
|
340
469
|
```
|
|
341
470
|
|
|
342
|
-
## `
|
|
471
|
+
## `WebsocketGateway`
|
|
472
|
+
|
|
473
|
+
`WebsocketGateway` manages realtime subscriptions and forwards update events. It extends `Subject<UpdatedData>`, so callers can publish updates with `next(update)`.
|
|
474
|
+
|
|
475
|
+
### When To Use It
|
|
343
476
|
|
|
344
|
-
|
|
477
|
+
Use it when clients need realtime updates for Livequery refs.
|
|
478
|
+
|
|
479
|
+
Typical flow:
|
|
480
|
+
|
|
481
|
+
1. A client connects to the WebSocket endpoint.
|
|
482
|
+
2. The client starts a socket session.
|
|
483
|
+
3. HTTP requests register subscriptions by passing client/gateway headers.
|
|
484
|
+
4. Services publish `UpdatedData`.
|
|
485
|
+
5. The gateway sends `sync` events to subscribed clients.
|
|
486
|
+
|
|
487
|
+
### Constructor
|
|
345
488
|
|
|
346
489
|
```ts
|
|
347
|
-
|
|
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
|
-
}
|
|
490
|
+
new WebsocketGateway(serverOrPort)
|
|
360
491
|
```
|
|
361
492
|
|
|
362
|
-
|
|
493
|
+
- Pass an `http.Server` in Node.js.
|
|
494
|
+
- Pass a port number in Bun runtime.
|
|
495
|
+
|
|
496
|
+
### Properties
|
|
363
497
|
|
|
364
|
-
|
|
498
|
+
- `id`: unique gateway id.
|
|
499
|
+
- `auth`: token used by trusted gateway-to-gateway connections.
|
|
365
500
|
|
|
366
|
-
|
|
501
|
+
### `handle(ctx)`
|
|
367
502
|
|
|
368
|
-
|
|
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`
|
|
503
|
+
Reads:
|
|
372
504
|
|
|
373
|
-
|
|
505
|
+
- `ctx.livequery.ref`
|
|
506
|
+
- `x-lcid` or `socket_id`
|
|
507
|
+
- `x-lgid`
|
|
374
508
|
|
|
375
|
-
|
|
509
|
+
Then registers a realtime subscription for the current request ref.
|
|
510
|
+
|
|
511
|
+
### `listen(events)`
|
|
512
|
+
|
|
513
|
+
Registers one or more realtime subscriptions.
|
|
376
514
|
|
|
377
515
|
```ts
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
516
|
+
wsGateway.listen([{
|
|
517
|
+
ref: 'posts',
|
|
518
|
+
client_id: 'client-1',
|
|
519
|
+
gateway_id: wsGateway.id,
|
|
520
|
+
listener_node_id: wsGateway.id,
|
|
521
|
+
}])
|
|
385
522
|
```
|
|
386
523
|
|
|
387
|
-
###
|
|
524
|
+
### `unsubscribe_client(socket, body)`
|
|
525
|
+
|
|
526
|
+
Removes subscriptions for a client by `ref` or `refs`.
|
|
527
|
+
|
|
528
|
+
### `link(ref, handler)`
|
|
529
|
+
|
|
530
|
+
Attaches an observable update stream for a ref that already has subscribers.
|
|
388
531
|
|
|
389
532
|
```ts
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
533
|
+
import { Subject } from 'rxjs'
|
|
534
|
+
|
|
535
|
+
const updates$ = new Subject<any>()
|
|
536
|
+
|
|
537
|
+
await wsGateway.link('posts', () => updates$)
|
|
538
|
+
|
|
539
|
+
updates$.next({
|
|
540
|
+
ref: 'posts',
|
|
541
|
+
type: 'modified',
|
|
542
|
+
data: { id: 'p1', title: 'New title' },
|
|
543
|
+
})
|
|
399
544
|
```
|
|
400
545
|
|
|
401
|
-
|
|
546
|
+
### `connect(url, auth, ondisconnect?)`
|
|
547
|
+
|
|
548
|
+
Creates an outbound connection to another WebSocket gateway and forwards subscription/sync events.
|
|
549
|
+
|
|
550
|
+
### `close()`
|
|
551
|
+
|
|
552
|
+
Closes the WebSocket server, active sockets, subscriptions, update streams, and completes the subject.
|
|
402
553
|
|
|
403
|
-
|
|
554
|
+
### Client Protocol
|
|
404
555
|
|
|
405
|
-
|
|
556
|
+
Client connects to `WEBSOCKET_PATH`, then sends:
|
|
406
557
|
|
|
407
|
-
|
|
558
|
+
```json
|
|
559
|
+
{ "event": "start", "data": { "id": "client-1", "auth": "" } }
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
Gateway responds:
|
|
408
563
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
- `:around`
|
|
413
|
-
- `:page`
|
|
564
|
+
```json
|
|
565
|
+
{ "event": "hello", "gid": "...", "binary": true }
|
|
566
|
+
```
|
|
414
567
|
|
|
415
|
-
|
|
568
|
+
Server update sent to client:
|
|
416
569
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
570
|
+
```json
|
|
571
|
+
{
|
|
572
|
+
"event": "sync",
|
|
573
|
+
"data": {
|
|
574
|
+
"changes": [
|
|
575
|
+
{ "ref": "posts", "type": "modified", "data": { "id": "p1" } }
|
|
576
|
+
]
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
```
|
|
426
580
|
|
|
427
|
-
|
|
581
|
+
## Helpers
|
|
582
|
+
|
|
583
|
+
### `hidePrivateFieldsInItem(item)`
|
|
584
|
+
|
|
585
|
+
Returns a new object with private fields removed. Fields beginning with `_` are removed, except `_id`, which is mapped to `id` when `id` is missing.
|
|
428
586
|
|
|
429
587
|
```ts
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
"done:boolean": "false",
|
|
433
|
-
"title:like": "milk",
|
|
434
|
-
"createdAt:gte": 1714176000000,
|
|
435
|
-
"createdAt:sort": "desc",
|
|
436
|
-
})
|
|
588
|
+
hidePrivateFieldsInItem({ _id: '1', name: 'Alice', _secret: true })
|
|
589
|
+
// { id: '1', name: 'Alice' }
|
|
437
590
|
```
|
|
438
591
|
|
|
439
|
-
|
|
592
|
+
### `hidePrivateFields(data)`
|
|
440
593
|
|
|
441
|
-
|
|
594
|
+
Sanitizes a plain item, a `DocumentResponse`, or a `CollectionResponse`.
|
|
442
595
|
|
|
443
596
|
```ts
|
|
444
|
-
|
|
597
|
+
hidePrivateFields({
|
|
598
|
+
item: { _id: 'p1', title: 'Hello', _internal: true },
|
|
599
|
+
})
|
|
600
|
+
// { item: { id: 'p1', title: 'Hello' } }
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### `nodeRequestToWebRequest(req, extraHeaders?)`
|
|
604
|
+
|
|
605
|
+
Converts a Node.js request with optional `rawBody` into a Web `Request`.
|
|
606
|
+
|
|
607
|
+
- Uses the `host` header, or `127.0.0.1` as fallback.
|
|
608
|
+
- Merges `extraHeaders`.
|
|
609
|
+
- Omits body for `GET` and `HEAD`.
|
|
610
|
+
- Uses `req.rawBody` for methods that support a body.
|
|
611
|
+
|
|
612
|
+
### `writeWebResponse(res, response)`
|
|
445
613
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
614
|
+
Copies a Web `Response` into a Node.js `ServerResponse`.
|
|
615
|
+
|
|
616
|
+
- Copies status.
|
|
617
|
+
- Copies headers.
|
|
618
|
+
- Writes the response body.
|
|
619
|
+
|
|
620
|
+
## Constants
|
|
621
|
+
|
|
622
|
+
| Constant | Meaning | Default |
|
|
623
|
+
| --- | --- | --- |
|
|
624
|
+
| `API_GATEWAY_NAMESPACE` | Namespace used by gateway and service metadata filtering | `default` |
|
|
625
|
+
| `LIVEQUERY_MAGIC_KEY` | Livequery path prefix and default discovery key suffix | `livequery/` |
|
|
626
|
+
| `API_GATEWAY_MULTICAST_PORT` | UDP discovery port | `11001` |
|
|
627
|
+
| `API_GATEWAY_MULTICAST_ADDRESS` | UDP multicast address | `239.0.1.1` |
|
|
628
|
+
| `API_GATEWAY_WHITELIST_ADDRESS` | Additional peer IPs or prefixes | empty |
|
|
629
|
+
| `NODE_ID` | Runtime node id | random UUID |
|
|
630
|
+
| `LIVEQUERY_API_GATEWAY_DEBUG` | Enables gateway logs | false |
|
|
631
|
+
| `WEBSOCKET_PATH` | Realtime WebSocket path | `/livequery/realtime-updates` |
|
|
632
|
+
|
|
633
|
+
## Example: Service Process
|
|
634
|
+
|
|
635
|
+
```ts
|
|
636
|
+
import * as http from 'http'
|
|
637
|
+
import {
|
|
638
|
+
ApiServiceLinker,
|
|
639
|
+
LivequeryRequestParser,
|
|
640
|
+
hidePrivateFields,
|
|
641
|
+
type LivequeryContext,
|
|
642
|
+
} from '@livequery/core'
|
|
643
|
+
|
|
644
|
+
const parser = new LivequeryRequestParser()
|
|
645
|
+
|
|
646
|
+
const server = http.createServer(async (req, res) => {
|
|
647
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host}`)
|
|
648
|
+
const id = url.pathname.split('/').at(-1)
|
|
649
|
+
|
|
650
|
+
const ctx: LivequeryContext = {
|
|
651
|
+
request: {
|
|
652
|
+
path: url.pathname,
|
|
653
|
+
ref: '/livequery/posts/:id',
|
|
654
|
+
method: req.method ?? 'GET',
|
|
655
|
+
params: { id },
|
|
656
|
+
query: Object.fromEntries(url.searchParams),
|
|
657
|
+
headers: new Map(Object.entries(req.headers).map(([k, v]) => [k, String(v)])),
|
|
658
|
+
},
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
parser.handle(ctx)
|
|
662
|
+
|
|
663
|
+
ctx.response = hidePrivateFields({
|
|
664
|
+
item: { _id: ctx.livequery?.document_id, title: 'Hello', _internal: true },
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
res.setHeader('content-type', 'application/json')
|
|
668
|
+
res.end(JSON.stringify(ctx.response))
|
|
449
669
|
})
|
|
670
|
+
|
|
671
|
+
server.listen(3001)
|
|
672
|
+
|
|
673
|
+
new ApiServiceLinker({
|
|
674
|
+
paths: [{ method: 'GET', path: 'livequery/posts/:id' }],
|
|
675
|
+
}).start('posts-service', 3001)
|
|
450
676
|
```
|
|
451
677
|
|
|
452
|
-
|
|
678
|
+
## Example: Gateway Process
|
|
453
679
|
|
|
454
|
-
|
|
680
|
+
```ts
|
|
681
|
+
import * as http from 'http'
|
|
682
|
+
import {
|
|
683
|
+
ApiGatewayHandler,
|
|
684
|
+
WebsocketGateway,
|
|
685
|
+
} from '@livequery/core'
|
|
455
686
|
|
|
456
|
-
|
|
687
|
+
const server = http.createServer()
|
|
688
|
+
const ws = new WebsocketGateway(server)
|
|
689
|
+
const gateway = new ApiGatewayHandler({ ws })
|
|
457
690
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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()`.
|
|
691
|
+
server.on('request', (req, res) => {
|
|
692
|
+
gateway.fetch(req as any, res)
|
|
693
|
+
})
|
|
464
694
|
|
|
465
|
-
|
|
695
|
+
server.listen(3000)
|
|
696
|
+
```
|
|
466
697
|
|
|
467
|
-
|
|
468
|
-
|
|
698
|
+
## Environment Variables
|
|
699
|
+
|
|
700
|
+
```sh
|
|
701
|
+
API_GATEWAY_NAMESPACE=default
|
|
702
|
+
LIVEQUERY_MAGIC_KEY=livequery
|
|
703
|
+
UDP_PUBLIC_PORT=11001
|
|
704
|
+
UDP_MULTICAST_ADDRESS=239.0.1.1
|
|
705
|
+
UDP_WHITELIST_ADDRESS=192.168.1
|
|
706
|
+
REALTIME_UPDATE_SOCKET_PATH=/livequery/realtime-updates
|
|
707
|
+
LIVEQUERY_API_GATEWAY_DEBUG=1
|
|
708
|
+
LIVEQUERY_UDP_DEBUG=1
|
|
469
709
|
```
|
|
470
710
|
|
|
471
|
-
|
|
711
|
+
`UDP_WHITELIST_ADDRESS` accepts:
|
|
712
|
+
|
|
713
|
+
- A full IP address, for example `192.168.1.10`.
|
|
714
|
+
- A three-part prefix, for example `192.168.1`, expanded to `192.168.1.0` through `192.168.1.255`.
|
|
715
|
+
|
|
716
|
+
## Tests
|
|
717
|
+
|
|
718
|
+
```sh
|
|
719
|
+
bun run build
|
|
720
|
+
bun test tests/
|
|
721
|
+
bunx tsc -p tests/tsconfig.json --noEmit
|
|
722
|
+
```
|
|
472
723
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
-
|
|
476
|
-
-
|
|
477
|
-
-
|
|
724
|
+
The test suite covers:
|
|
725
|
+
|
|
726
|
+
- Public entrypoint exports.
|
|
727
|
+
- Request parsing, including nested params, realtime suffixes, and query strings containing `~`.
|
|
728
|
+
- API gateway routing, metadata updates, header/body forwarding, error responses, and round-robin.
|
|
729
|
+
- Service metadata publishing with `ApiServiceLinker`.
|
|
730
|
+
- UDP discovery signatures, TTL, status, and close behavior.
|
|
731
|
+
- WebSocket gateway lifecycle, subscriptions, observable links, and gateway-to-gateway forwarding.
|
|
732
|
+
- Hono integration and multi-process gateway/service discovery flows.
|
|
733
|
+
- Response field sanitization.
|
|
734
|
+
- Node/Web HTTP helper conversion.
|