http-air 1.3.2 → 1.3.4
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 +9 -33
- package/lib/client/client.d.ts +1 -2
- package/lib/client/client.js +0 -3
- package/lib/client/index.d.ts +1 -1
- package/lib/client/rpc.d.ts +0 -3
- package/lib/client/rpc.js +0 -21
- package/lib/server/events.d.ts +4 -1
- package/lib/server/events.js +15 -1
- package/lib/server/index.d.ts +1 -0
- package/lib/server/server.d.ts +2 -0
- package/lib/server/server.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,6 @@ HTTP library for batched RPC calls and real-time server-push over a single endpo
|
|
|
4
4
|
|
|
5
5
|
- **RPC** — calls made in the same tick are grouped into one HTTP request. The server executes them concurrently and streams each result back as it resolves, matched to the original call by index. The client resolves each promise individually as its result arrives.
|
|
6
6
|
- **Events** — server pushes named events to subscribed clients over a persistent HTTP connection, with heartbeat and auto-reconnect.
|
|
7
|
-
- **SWR cache** — RPC calls support stale-while-revalidate: return a cached value immediately and revalidate in the background, with a cancel handle.
|
|
8
7
|
|
|
9
8
|
## Why http-air
|
|
10
9
|
|
|
@@ -81,6 +80,12 @@ server.handleRpc(async ({ method, params }) => {
|
|
|
81
80
|
return myService[method](...params)
|
|
82
81
|
})
|
|
83
82
|
|
|
83
|
+
// validate session before accepting an events connection
|
|
84
|
+
server.handleEventsConnect(async (req, res) => {
|
|
85
|
+
const token = req.getHeader('authorization')
|
|
86
|
+
if (!isValid(token)) throw new Error('unauthorized')
|
|
87
|
+
})
|
|
88
|
+
|
|
84
89
|
// push to all subscribers whenever something happens
|
|
85
90
|
server.notifyEvent('price-update', { symbol: 'BTC', price: 70000 })
|
|
86
91
|
```
|
|
@@ -158,6 +163,9 @@ const router = new Router()
|
|
|
158
163
|
const rpc = new RpcServer(router)
|
|
159
164
|
rpc.handle(handler)
|
|
160
165
|
const events = new EventsServer(router)
|
|
166
|
+
events.handleEventsConnect(async (req, res) => {
|
|
167
|
+
if (!isValid(req.getHeader('authorization'))) throw new Error('unauthorized')
|
|
168
|
+
})
|
|
161
169
|
```
|
|
162
170
|
|
|
163
171
|
---
|
|
@@ -209,38 +217,6 @@ const [r1, r2, r3] = await Promise.all([
|
|
|
209
217
|
])
|
|
210
218
|
```
|
|
211
219
|
|
|
212
|
-
### SWR (stale-while-revalidate)
|
|
213
|
-
|
|
214
|
-
`callSwr` returns a tuple `[stale, fresh, cancel]` — immediately a cached (stale) value and a background revalidation promise. If no cache exists both point to the same request. If the cached value is within `ttl` (ms) no request is made.
|
|
215
|
-
|
|
216
|
-
```ts
|
|
217
|
-
const [stale, fresh, cancel] = client.callSwr('getUser', [42], 5000)
|
|
218
|
-
|
|
219
|
-
// render immediately with cached data
|
|
220
|
-
const user = await stale
|
|
221
|
-
|
|
222
|
-
// update once revalidated
|
|
223
|
-
const updated = await fresh
|
|
224
|
-
|
|
225
|
-
// cancel the background revalidation (e.g. component unmounted)
|
|
226
|
-
cancel()
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
Works well inside a React `useEffect`:
|
|
230
|
-
|
|
231
|
-
```ts
|
|
232
|
-
useEffect(() => {
|
|
233
|
-
const [stale, fresh, cancel] = rpcClient.callSwr('getUser', [id], 5_000)
|
|
234
|
-
stale.then(setUser)
|
|
235
|
-
fresh.then(setUser)
|
|
236
|
-
return cancel
|
|
237
|
-
}, [id])
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
`Client` exposes the same method as `callRpcSwr`.
|
|
241
|
-
|
|
242
|
-
---
|
|
243
|
-
|
|
244
220
|
### Events
|
|
245
221
|
|
|
246
222
|
Connects automatically on the first `subscribe()` call and disconnects when all subscriptions are removed. Multiple subscribe/unsubscribe calls in the same tick are batched. Auto-reconnect is always enabled.
|
package/lib/client/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RpcClientConfig
|
|
1
|
+
import { RpcClientConfig } from './rpc';
|
|
2
2
|
export interface ClientConfig {
|
|
3
3
|
/** Endpoint URL. Defaults to current page location */
|
|
4
4
|
url?: string;
|
|
@@ -19,7 +19,6 @@ export declare class Client {
|
|
|
19
19
|
private events;
|
|
20
20
|
constructor(config?: RpcClientConfig);
|
|
21
21
|
callRpc(method: string, params: any[]): Promise<any>;
|
|
22
|
-
callRpcSwr(method: string, params: any[], ttl: number): SwrResult;
|
|
23
22
|
subscribeEvent(eventName: string | string[], handler: (data: any) => void): () => void;
|
|
24
23
|
unsubscribeEvent(eventName: string | string[], handler: (data: any) => void): void;
|
|
25
24
|
disconnect(): void;
|
package/lib/client/client.js
CHANGED
|
@@ -28,9 +28,6 @@ class Client {
|
|
|
28
28
|
callRpc(method, params) {
|
|
29
29
|
return this.rpc.call(method, params);
|
|
30
30
|
}
|
|
31
|
-
callRpcSwr(method, params, ttl) {
|
|
32
|
-
return this.rpc.callSwr(method, params, ttl);
|
|
33
|
-
}
|
|
34
31
|
subscribeEvent(eventName, handler) {
|
|
35
32
|
return this.events.subscribe(eventName, handler);
|
|
36
33
|
}
|
package/lib/client/index.d.ts
CHANGED
package/lib/client/rpc.d.ts
CHANGED
|
@@ -8,7 +8,6 @@ export interface RpcClientConfig extends ClientConfig {
|
|
|
8
8
|
/** Called on each response message (result or error) */
|
|
9
9
|
onResponse?: (resp: ResponseMessage) => void;
|
|
10
10
|
}
|
|
11
|
-
export type SwrResult = [stale: Promise<any>, fresh: Promise<any>, cancel: () => void];
|
|
12
11
|
export declare class RpcClient {
|
|
13
12
|
private url;
|
|
14
13
|
private fetchFn;
|
|
@@ -16,9 +15,7 @@ export declare class RpcClient {
|
|
|
16
15
|
private deduplicate;
|
|
17
16
|
private batch;
|
|
18
17
|
private batcher;
|
|
19
|
-
private swrCache;
|
|
20
18
|
constructor(config?: RpcClientConfig);
|
|
21
19
|
call(method: string, params: any[]): Promise<any>;
|
|
22
|
-
callSwr(method: string, params: any[], ttl: number): SwrResult;
|
|
23
20
|
private send;
|
|
24
21
|
}
|
package/lib/client/rpc.js
CHANGED
|
@@ -17,7 +17,6 @@ class RpcClient {
|
|
|
17
17
|
deduplicate;
|
|
18
18
|
batch;
|
|
19
19
|
batcher;
|
|
20
|
-
swrCache = new Map();
|
|
21
20
|
constructor(config = {}) {
|
|
22
21
|
const { url, fetchFn } = (0, client_1.resolveConfig)(config);
|
|
23
22
|
this.url = url;
|
|
@@ -34,26 +33,6 @@ class RpcClient {
|
|
|
34
33
|
this.batcher.flush();
|
|
35
34
|
return dp.promise;
|
|
36
35
|
}
|
|
37
|
-
callSwr(method, params, ttl) {
|
|
38
|
-
const key = method + JSON.stringify(params);
|
|
39
|
-
const cached = this.swrCache.get(key);
|
|
40
|
-
let cancelled = false;
|
|
41
|
-
const cancel = () => { cancelled = true; };
|
|
42
|
-
const revalidate = () => this.call(method, params).then(value => {
|
|
43
|
-
if (!cancelled)
|
|
44
|
-
this.swrCache.set(key, { value, fetchedAt: Date.now() });
|
|
45
|
-
return value;
|
|
46
|
-
});
|
|
47
|
-
if (!cached) {
|
|
48
|
-
const fresh = revalidate();
|
|
49
|
-
return [fresh, fresh, cancel];
|
|
50
|
-
}
|
|
51
|
-
const stale = Promise.resolve(cached.value);
|
|
52
|
-
if (Date.now() - cached.fetchedAt < ttl) {
|
|
53
|
-
return [stale, stale, cancel];
|
|
54
|
-
}
|
|
55
|
-
return [stale, revalidate(), cancel];
|
|
56
|
-
}
|
|
57
36
|
send(items) {
|
|
58
37
|
const requests = [];
|
|
59
38
|
const deferredGroups = [];
|
package/lib/server/events.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { Router } from './router';
|
|
1
|
+
import { Router, ServerReq, ServerRes } from './router';
|
|
2
|
+
export type EventsConnectHandler = (req: ServerReq, res: ServerRes) => Promise<void> | void;
|
|
2
3
|
export declare class EventsServer {
|
|
3
4
|
readonly serverId: string;
|
|
5
|
+
private connectHandler;
|
|
4
6
|
private responsesMap;
|
|
5
7
|
private messageQueueMap;
|
|
6
8
|
private drainingSet;
|
|
7
9
|
private listenersMap;
|
|
8
10
|
private heartbeatInterval;
|
|
9
11
|
constructor(server: Router);
|
|
12
|
+
handleEventsConnect(handler: EventsConnectHandler): void;
|
|
10
13
|
notify(eventName: string, data: any): void;
|
|
11
14
|
private push;
|
|
12
15
|
private drain;
|
package/lib/server/events.js
CHANGED
|
@@ -6,6 +6,7 @@ const json_stream_1 = require("../shared/json-stream");
|
|
|
6
6
|
const stream_constants_1 = require("../shared/stream-constants");
|
|
7
7
|
class EventsServer {
|
|
8
8
|
serverId = (0, crypto_1.randomBytes)(16).toString('base64url');
|
|
9
|
+
connectHandler;
|
|
9
10
|
responsesMap = new Map();
|
|
10
11
|
messageQueueMap = new Map();
|
|
11
12
|
drainingSet = new Set();
|
|
@@ -16,6 +17,9 @@ class EventsServer {
|
|
|
16
17
|
server.setHandler('events-subscribe', this.handleSubscribe.bind(this));
|
|
17
18
|
server.setHandler('events-unsubscribe', this.handleUnsubscribe.bind(this));
|
|
18
19
|
}
|
|
20
|
+
handleEventsConnect(handler) {
|
|
21
|
+
this.connectHandler = handler;
|
|
22
|
+
}
|
|
19
23
|
notify(eventName, data) {
|
|
20
24
|
const sessions = this.listenersMap.get(eventName) ?? [];
|
|
21
25
|
sessions.forEach(sessionId => this.push(sessionId, { type: 'event', name: eventName, data }));
|
|
@@ -86,7 +90,17 @@ class EventsServer {
|
|
|
86
90
|
parser.close();
|
|
87
91
|
return names;
|
|
88
92
|
}
|
|
89
|
-
handleConnect(req, res) {
|
|
93
|
+
async handleConnect(req, res) {
|
|
94
|
+
if (this.connectHandler) {
|
|
95
|
+
try {
|
|
96
|
+
await this.connectHandler(req, res);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
res.writeHead(401, {});
|
|
100
|
+
res.end();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
90
104
|
let sessionId = req.getHeader(stream_constants_1.SESSION_ID_KEY).trim();
|
|
91
105
|
if (!sessionId.startsWith(stream_constants_1.SESSION_ID_PREFIX)) {
|
|
92
106
|
sessionId = stream_constants_1.SESSION_ID_PREFIX + (0, crypto_1.randomBytes)(60).toString('base64url');
|
package/lib/server/index.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ export { Server } from './server';
|
|
|
2
2
|
export { RpcServer } from './rpc';
|
|
3
3
|
export type { RpcHandler } from './rpc';
|
|
4
4
|
export { EventsServer } from './events';
|
|
5
|
+
export type { EventsConnectHandler } from './events';
|
|
5
6
|
export { Router } from './router';
|
|
6
7
|
export type { ServerReq, ServerRes, HttpHandler } from './router';
|
package/lib/server/server.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HttpHandler, Router, ServerReq, ServerRes } from './router';
|
|
2
2
|
import { RpcHandler } from './rpc';
|
|
3
|
+
import { EventsConnectHandler } from './events';
|
|
3
4
|
export { ServerReq, ServerRes, HttpHandler, Router };
|
|
4
5
|
export declare class Server implements HttpHandler {
|
|
5
6
|
private router;
|
|
@@ -7,6 +8,7 @@ export declare class Server implements HttpHandler {
|
|
|
7
8
|
private eventsServer;
|
|
8
9
|
constructor();
|
|
9
10
|
handleRpc(handler: RpcHandler): void;
|
|
11
|
+
handleEventsConnect(handler: EventsConnectHandler): void;
|
|
10
12
|
notifyEvent(eventName: string, data: any): void;
|
|
11
13
|
handleHttp(req: ServerReq, res: ServerRes): void;
|
|
12
14
|
}
|
package/lib/server/server.js
CHANGED
package/package.json
CHANGED