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