http-air 1.0.0

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 (41) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/README.md +313 -0
  3. package/index.d.ts +0 -0
  4. package/index.js +1 -0
  5. package/lib/client/batch.d.ts +15 -0
  6. package/lib/client/batch.js +42 -0
  7. package/lib/client/client.d.ts +28 -0
  8. package/lib/client/client.js +61 -0
  9. package/lib/client/events.d.ts +21 -0
  10. package/lib/client/events.js +217 -0
  11. package/lib/client/index.d.ts +5 -0
  12. package/lib/client/index.js +9 -0
  13. package/lib/client/rpc.d.ts +24 -0
  14. package/lib/client/rpc.js +118 -0
  15. package/lib/server/adapters/express.d.ts +2 -0
  16. package/lib/server/adapters/express.js +27 -0
  17. package/lib/server/adapters/hono.d.ts +3 -0
  18. package/lib/server/adapters/hono.js +34 -0
  19. package/lib/server/adapters/micro.d.ts +3 -0
  20. package/lib/server/adapters/micro.js +24 -0
  21. package/lib/server/adapters/next.d.ts +3 -0
  22. package/lib/server/adapters/next.js +22 -0
  23. package/lib/server/events.d.ts +20 -0
  24. package/lib/server/events.js +141 -0
  25. package/lib/server/index.d.ts +6 -0
  26. package/lib/server/index.js +11 -0
  27. package/lib/server/router.d.ts +23 -0
  28. package/lib/server/router.js +20 -0
  29. package/lib/server/rpc.d.ts +9 -0
  30. package/lib/server/rpc.js +55 -0
  31. package/lib/server/server.d.ts +12 -0
  32. package/lib/server/server.js +27 -0
  33. package/lib/shared/json-stream.d.ts +26 -0
  34. package/lib/shared/json-stream.js +57 -0
  35. package/lib/shared/message.d.ts +13 -0
  36. package/lib/shared/message.js +2 -0
  37. package/lib/shared/stream-constants.d.ts +6 -0
  38. package/lib/shared/stream-constants.js +9 -0
  39. package/package.json +38 -0
  40. package/tsconfig.build.json +5 -0
  41. package/tsconfig.json +110 -0
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.EventsClient = void 0;
37
+ const p_retry_1 = __importStar(require("p-retry"));
38
+ const json_stream_1 = require("../shared/json-stream");
39
+ const stream_constants_1 = require("../shared/stream-constants");
40
+ const batch_1 = require("./batch");
41
+ const client_1 = require("./client");
42
+ const supportsSessionStorage = typeof sessionStorage !== 'undefined';
43
+ const connectRetries = 10;
44
+ var Status;
45
+ (function (Status) {
46
+ Status[Status["Connecting"] = 0] = "Connecting";
47
+ Status[Status["Connected"] = 1] = "Connected";
48
+ Status[Status["Disconnected"] = 2] = "Disconnected";
49
+ })(Status || (Status = {}));
50
+ class EventsClient {
51
+ fetcher;
52
+ sessionId;
53
+ serverId;
54
+ response;
55
+ abortController;
56
+ status = Status.Disconnected;
57
+ listenersMap = new Map();
58
+ subscribedSet = new Set();
59
+ subscribeBatch;
60
+ unsubscribeBatch;
61
+ constructor(config = {}) {
62
+ this.sessionId = supportsSessionStorage ? sessionStorage.getItem(stream_constants_1.SESSION_ID_KEY) ?? '' : '';
63
+ this.serverId = supportsSessionStorage ? sessionStorage.getItem(stream_constants_1.SERVER_ID_KEY) ?? '' : '';
64
+ this.fetcher = this.buildFetcher(config);
65
+ this.subscribeBatch = new batch_1.Batch(names => {
66
+ const toAdd = names.filter(n => !this.subscribedSet.has(n));
67
+ if (!toAdd.length)
68
+ return;
69
+ const [promise] = this.fetcher({ action: 'events-subscribe' }, (0, client_1.toNdjson)(toAdd.map(name => ({ name }))));
70
+ promise.then(() => toAdd.forEach(n => this.subscribedSet.add(n)));
71
+ }, { key: name => name });
72
+ this.unsubscribeBatch = new batch_1.Batch(names => {
73
+ if (!names.length)
74
+ return;
75
+ const [promise] = this.fetcher({ action: 'events-unsubscribe' }, (0, client_1.toNdjson)(names.map(name => ({ name }))));
76
+ promise.then(() => names.forEach(n => this.subscribedSet.delete(n)));
77
+ }, { key: name => name });
78
+ }
79
+ buildFetcher(config) {
80
+ const { url, fetchFn } = (0, client_1.resolveConfig)(config);
81
+ const init = config.init ?? {};
82
+ return (params = {}, body) => {
83
+ const p = { ...params, _dc: String(Math.random()) };
84
+ const search = Object.keys(p)
85
+ .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(p[k])}`)
86
+ .join('&');
87
+ const ac = new AbortController();
88
+ const promise = fetchFn(url + '?' + search, {
89
+ method: 'POST',
90
+ ...init,
91
+ headers: {
92
+ ...init.headers,
93
+ [stream_constants_1.SESSION_ID_KEY]: this.sessionId,
94
+ [stream_constants_1.SERVER_ID_KEY]: this.serverId,
95
+ },
96
+ signal: ac.signal,
97
+ body,
98
+ });
99
+ return [promise, ac];
100
+ };
101
+ }
102
+ async connect() {
103
+ if (this.status !== Status.Disconnected)
104
+ return;
105
+ this.status = Status.Connecting;
106
+ return (0, p_retry_1.default)(() => this.performConnect(), {
107
+ retries: connectRetries,
108
+ onFailedAttempt: () => { this.status = Status.Disconnected; },
109
+ });
110
+ }
111
+ async performConnect() {
112
+ if (this.status === Status.Connected)
113
+ return;
114
+ const [pResponse, abortController] = this.fetcher({ action: 'events-connect' });
115
+ this.abortController = abortController;
116
+ const response = await pResponse;
117
+ if (response.status !== 200)
118
+ throw new p_retry_1.AbortError(response.statusText);
119
+ this.response = response;
120
+ this.sessionId = response.headers.get(stream_constants_1.SESSION_ID_KEY) ?? '';
121
+ this.serverId = response.headers.get(stream_constants_1.SERVER_ID_KEY) ?? '';
122
+ if (supportsSessionStorage) {
123
+ sessionStorage.setItem(stream_constants_1.SESSION_ID_KEY, this.sessionId);
124
+ sessionStorage.setItem(stream_constants_1.SERVER_ID_KEY, this.serverId);
125
+ }
126
+ this.status = Status.Connected;
127
+ this.subscribedSet.clear();
128
+ this.listenersMap.forEach((_, name) => this.subscribeBatch.add(name));
129
+ await this.read();
130
+ }
131
+ async read() {
132
+ const onDone = () => {
133
+ this.status = Status.Disconnected;
134
+ this.connect().catch(() => { });
135
+ };
136
+ let heartbeatTimer = null;
137
+ const resetHeartbeatTimer = () => {
138
+ if (heartbeatTimer !== null)
139
+ clearTimeout(heartbeatTimer);
140
+ heartbeatTimer = setTimeout(() => {
141
+ if (this.status === Status.Connected) {
142
+ this.abortController.abort();
143
+ onDone();
144
+ }
145
+ }, stream_constants_1.HEARTBEAT_INTERVAL_MS * 2);
146
+ };
147
+ const parser = (0, json_stream_1.createParser)({
148
+ delimiter: stream_constants_1.DELIMITER,
149
+ onData: (data) => {
150
+ resetHeartbeatTimer();
151
+ if (data?.type === stream_constants_1.HEARTBEAT_TYPE)
152
+ return;
153
+ if (data?.type === 'event' && data.name !== void 0 && this.subscribedSet.has(data.name)) {
154
+ this.listenersMap.get(data.name)?.forEach(handler => handler(data));
155
+ }
156
+ },
157
+ });
158
+ resetHeartbeatTimer();
159
+ try {
160
+ await (0, client_1.readResponseBody)(this.response, chunk => {
161
+ if (this.abortController.signal.aborted || this.status !== Status.Connected)
162
+ return;
163
+ parser.write(chunk);
164
+ }, () => {
165
+ if (heartbeatTimer !== null)
166
+ clearTimeout(heartbeatTimer);
167
+ onDone();
168
+ });
169
+ }
170
+ catch (e) {
171
+ if (heartbeatTimer !== null)
172
+ clearTimeout(heartbeatTimer);
173
+ if (e instanceof TypeError) {
174
+ onDone();
175
+ return;
176
+ }
177
+ throw e;
178
+ }
179
+ }
180
+ disconnect() {
181
+ this.status = Status.Disconnected;
182
+ this.abortController?.abort();
183
+ this.listenersMap.forEach((_, name) => this.unsubscribeBatch.add(name));
184
+ this.listenersMap.clear();
185
+ }
186
+ subscribe(eventName, handler) {
187
+ const eventNames = Array.isArray(eventName) ? eventName : [eventName];
188
+ eventNames.forEach(name => {
189
+ if (!this.listenersMap.has(name))
190
+ this.listenersMap.set(name, []);
191
+ this.listenersMap.get(name).push(handler);
192
+ if (this.status === Status.Connected)
193
+ this.subscribeBatch.add(name);
194
+ });
195
+ this.connect().catch(() => { });
196
+ return () => eventNames.forEach(name => this.unsubscribe(name, handler));
197
+ }
198
+ unsubscribe(eventName, handler) {
199
+ const eventNames = Array.isArray(eventName) ? eventName : [eventName];
200
+ eventNames.forEach(name => {
201
+ const listeners = this.listenersMap.get(name);
202
+ if (!listeners)
203
+ return;
204
+ const idx = listeners.findIndex(h => h === handler);
205
+ if (idx !== -1)
206
+ listeners.splice(idx, 1);
207
+ if (listeners.length === 0) {
208
+ this.listenersMap.delete(name);
209
+ if (this.status === Status.Connected)
210
+ this.unsubscribeBatch.add(name);
211
+ }
212
+ });
213
+ if (this.listenersMap.size === 0)
214
+ this.disconnect();
215
+ }
216
+ }
217
+ exports.EventsClient = EventsClient;
@@ -0,0 +1,5 @@
1
+ export { Client } from './client';
2
+ export { RpcClient } from './rpc';
3
+ export type { RpcClientConfig, SwrResult } from './rpc';
4
+ export { EventsClient } from './events';
5
+ export type { ClientConfig } from './client';
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EventsClient = exports.RpcClient = exports.Client = void 0;
4
+ var client_1 = require("./client");
5
+ Object.defineProperty(exports, "Client", { enumerable: true, get: function () { return client_1.Client; } });
6
+ var rpc_1 = require("./rpc");
7
+ Object.defineProperty(exports, "RpcClient", { enumerable: true, get: function () { return rpc_1.RpcClient; } });
8
+ var events_1 = require("./events");
9
+ Object.defineProperty(exports, "EventsClient", { enumerable: true, get: function () { return events_1.EventsClient; } });
@@ -0,0 +1,24 @@
1
+ import { ResponseMessage } from '../shared/message';
2
+ import { ClientConfig } from './client';
3
+ export interface RpcClientConfig extends ClientConfig {
4
+ /** Group calls made in the same tick into one HTTP request. Default: true */
5
+ batch?: boolean;
6
+ /** Remove duplicated requests within a batch. Default: true */
7
+ deduplicate?: boolean;
8
+ /** Called on each response message (result or error) */
9
+ onResponse?: (resp: ResponseMessage) => void;
10
+ }
11
+ export type SwrResult = [stale: Promise<any>, fresh: Promise<any>, cancel: () => void];
12
+ export declare class RpcClient {
13
+ private url;
14
+ private fetchFn;
15
+ private onResponse;
16
+ private deduplicate;
17
+ private batch;
18
+ private batcher;
19
+ private swrCache;
20
+ constructor(config?: RpcClientConfig);
21
+ call(method: string, params: any[]): Promise<any>;
22
+ callSwr(method: string, params: any[], ttl: number): SwrResult;
23
+ private send;
24
+ }
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RpcClient = void 0;
7
+ const p_defer_1 = __importDefault(require("p-defer"));
8
+ const json_stream_1 = require("../shared/json-stream");
9
+ const stream_constants_1 = require("../shared/stream-constants");
10
+ const batch_1 = require("./batch");
11
+ const client_1 = require("./client");
12
+ const noop = () => { };
13
+ class RpcClient {
14
+ url;
15
+ fetchFn;
16
+ onResponse;
17
+ deduplicate;
18
+ batch;
19
+ batcher;
20
+ swrCache = new Map();
21
+ constructor(config = {}) {
22
+ const { url, fetchFn } = (0, client_1.resolveConfig)(config);
23
+ this.url = url;
24
+ this.fetchFn = fetchFn;
25
+ this.onResponse = config.onResponse ?? noop;
26
+ this.deduplicate = config.deduplicate ?? true;
27
+ this.batch = config.batch ?? true;
28
+ this.batcher = new batch_1.Batch(items => this.send(items));
29
+ }
30
+ call(method, params) {
31
+ const dp = (0, p_defer_1.default)();
32
+ this.batcher.add({ method, params, deferred: dp });
33
+ if (!this.batch)
34
+ this.batcher.flush();
35
+ return dp.promise;
36
+ }
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
+ send(items) {
58
+ const requests = [];
59
+ const deferredGroups = [];
60
+ if (this.deduplicate) {
61
+ const requestIndexMap = new Map();
62
+ items.forEach(({ method, params, deferred }) => {
63
+ const key = method + JSON.stringify(params);
64
+ const requestIndex = requestIndexMap.get(key);
65
+ if (requestIndex !== void 0) {
66
+ deferredGroups[requestIndex].push(deferred);
67
+ return;
68
+ }
69
+ const index = requests.length;
70
+ requestIndexMap.set(key, index);
71
+ requests.push({ index, method, params });
72
+ deferredGroups.push([deferred]);
73
+ });
74
+ }
75
+ else {
76
+ items.forEach(({ method, params, deferred }, index) => {
77
+ requests.push({ index, method, params });
78
+ deferredGroups.push([deferred]);
79
+ });
80
+ }
81
+ const payload = (0, client_1.toNdjson)(requests);
82
+ const parser = (0, json_stream_1.createParser)({
83
+ delimiter: stream_constants_1.DELIMITER,
84
+ onData: resp => {
85
+ const deferreds = deferredGroups[resp.index];
86
+ if (!deferreds)
87
+ return;
88
+ this.onResponse(resp);
89
+ if (resp.error === void 0) {
90
+ deferreds.forEach(dp => dp.resolve(resp.result));
91
+ }
92
+ else {
93
+ const err = new Error(resp.error.message);
94
+ err.name = resp.error.name;
95
+ deferreds.forEach(dp => dp.reject(err));
96
+ }
97
+ },
98
+ onError: (err, str) => console.error(`parse error: ${str}`, err),
99
+ });
100
+ this.fetchFn(this.url + '?action=rpc', {
101
+ method: 'POST',
102
+ headers: {
103
+ 'Content-Type': 'text/plain;charset=utf-8',
104
+ },
105
+ body: payload,
106
+ }).then(response => {
107
+ if (response.status === 207) {
108
+ (0, client_1.readResponseBody)(response, parser.write, parser.close);
109
+ }
110
+ else {
111
+ deferredGroups.flat().forEach(d => d.reject(new Error(`rpc: unexpected status ${response.status}`)));
112
+ }
113
+ }).catch(err => {
114
+ deferredGroups.flat().forEach(d => d.reject(err));
115
+ });
116
+ }
117
+ }
118
+ exports.RpcClient = RpcClient;
@@ -0,0 +1,2 @@
1
+ import { HttpHandler } from '../server';
2
+ export declare const expressHandler: (server: HttpHandler) => (req: any, res: any, next: Function) => void;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.expressHandler = void 0;
4
+ const expressHandler = (server) => {
5
+ const express = require('express');
6
+ const text = express.text({ type: '*/*' });
7
+ return (req, res, next) => {
8
+ text(req, res, () => {
9
+ server.handleHttp({
10
+ getHeader: (key) => req.get(key) ?? '',
11
+ getUrl: () => req.url,
12
+ getMethod: () => req.method,
13
+ getBody: () => req.body,
14
+ }, {
15
+ write: (content) => res.write(content),
16
+ isClosed: () => res.writableEnded,
17
+ writeHead: (status, headers) => res.writeHead(status, headers),
18
+ flushHeaders: () => res.flushHeaders(),
19
+ onClose: (cb) => { res.setMaxListeners(0); res.on('close', cb); },
20
+ end: () => res.end(),
21
+ destroy: () => res.destroy(),
22
+ });
23
+ next();
24
+ });
25
+ };
26
+ };
27
+ exports.expressHandler = expressHandler;
@@ -0,0 +1,3 @@
1
+ import { Context } from 'hono';
2
+ import { HttpHandler } from '../server';
3
+ export declare const honoHandler: (server: HttpHandler) => (c: Context) => Promise<import("undici-types").Response>;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.honoHandler = void 0;
4
+ const honoHandler = (server) => {
5
+ return async (c) => {
6
+ const body = await c.req.text();
7
+ const { readable, writable } = new TransformStream();
8
+ const writer = writable.getWriter();
9
+ const encoder = new TextEncoder();
10
+ const headers = new Headers();
11
+ let statusCode = 200;
12
+ // Do not await — server sets status/headers synchronously before its first
13
+ // internal await, so they are ready when we return the Response below
14
+ server.handleHttp({
15
+ getHeader: (key) => c.req.header(key) ?? '',
16
+ getUrl: () => c.req.url,
17
+ getMethod: () => c.req.method,
18
+ getBody: () => body,
19
+ }, {
20
+ write: (content) => { writer.write(encoder.encode(content)); return true; },
21
+ isClosed: () => false,
22
+ writeHead: (status, hdrs) => {
23
+ statusCode = status;
24
+ Object.entries(hdrs).forEach(([k, v]) => headers.set(k, v));
25
+ },
26
+ flushHeaders: () => { },
27
+ onClose: () => { },
28
+ end: () => writer.close(),
29
+ destroy: () => writer.abort(),
30
+ });
31
+ return new Response(readable, { status: statusCode, headers });
32
+ };
33
+ };
34
+ exports.honoHandler = honoHandler;
@@ -0,0 +1,3 @@
1
+ import { IncomingMessage, ServerResponse } from 'http';
2
+ import { HttpHandler } from '../server';
3
+ export declare const microHandler: (server: HttpHandler) => (req: IncomingMessage, res: ServerResponse) => Promise<void>;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.microHandler = void 0;
4
+ const microHandler = (server) => {
5
+ const { text } = require('micro');
6
+ return async (req, res) => {
7
+ const body = await text(req);
8
+ server.handleHttp({
9
+ getHeader: (key) => String(req.headers[key] ?? ''),
10
+ getUrl: () => req.url ?? '',
11
+ getMethod: () => req.method ?? '',
12
+ getBody: () => body,
13
+ }, {
14
+ write: (content) => res.write(content),
15
+ isClosed: () => res.writableEnded,
16
+ writeHead: (status, headers) => res.writeHead(status, headers),
17
+ flushHeaders: () => res.flushHeaders(),
18
+ onClose: (cb) => { res.setMaxListeners(0); res.on('close', cb); },
19
+ end: () => res.end(),
20
+ destroy: () => res.destroy(),
21
+ });
22
+ };
23
+ };
24
+ exports.microHandler = microHandler;
@@ -0,0 +1,3 @@
1
+ import { IncomingMessage, ServerResponse } from 'http';
2
+ import { HttpHandler } from '../server';
3
+ export declare const nextHandler: (server: HttpHandler) => (req: IncomingMessage, res: ServerResponse) => void;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nextHandler = void 0;
4
+ const nextHandler = (server) => {
5
+ return (req, res) => {
6
+ server.handleHttp({
7
+ getHeader: (key) => String(req.headers[key] ?? ''),
8
+ getUrl: () => req.url ?? '',
9
+ getMethod: () => req.method ?? '',
10
+ getBody: () => req.body ?? '',
11
+ }, {
12
+ write: (content) => res.write(content),
13
+ isClosed: () => res.writableEnded,
14
+ writeHead: (status, headers) => res.writeHead(status, headers),
15
+ flushHeaders: () => res.flushHeaders(),
16
+ onClose: (cb) => { res.setMaxListeners(0); res.on('close', cb); },
17
+ end: () => res.end(),
18
+ destroy: () => res.destroy(),
19
+ });
20
+ };
21
+ };
22
+ exports.nextHandler = nextHandler;
@@ -0,0 +1,20 @@
1
+ import { Router } from './router';
2
+ export declare class EventsServer {
3
+ readonly serverId: string;
4
+ private responsesMap;
5
+ private messageQueueMap;
6
+ private drainingSet;
7
+ private listenersMap;
8
+ private heartbeatInterval;
9
+ constructor(server: Router);
10
+ notify(eventName: string, data: any): void;
11
+ private push;
12
+ private drain;
13
+ private subscribe;
14
+ private unsubscribe;
15
+ private unsubscribeAll;
16
+ private parseEventNames;
17
+ private handleConnect;
18
+ private handleSubscribe;
19
+ private handleUnsubscribe;
20
+ }
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EventsServer = void 0;
4
+ const nanoid_1 = require("nanoid");
5
+ const json_stream_1 = require("../shared/json-stream");
6
+ const stream_constants_1 = require("../shared/stream-constants");
7
+ class EventsServer {
8
+ serverId = (0, nanoid_1.nanoid)();
9
+ responsesMap = new Map();
10
+ messageQueueMap = new Map();
11
+ drainingSet = new Set();
12
+ listenersMap = new Map();
13
+ heartbeatInterval;
14
+ constructor(server) {
15
+ server.setHandler('events-connect', this.handleConnect.bind(this));
16
+ server.setHandler('events-subscribe', this.handleSubscribe.bind(this));
17
+ server.setHandler('events-unsubscribe', this.handleUnsubscribe.bind(this));
18
+ }
19
+ notify(eventName, data) {
20
+ const sessions = this.listenersMap.get(eventName) ?? [];
21
+ sessions.forEach(sessionId => this.push(sessionId, { type: 'event', name: eventName, data }));
22
+ }
23
+ push(sessionId, data) {
24
+ if (!this.messageQueueMap.has(sessionId)) {
25
+ this.messageQueueMap.set(sessionId, []);
26
+ }
27
+ this.messageQueueMap.get(sessionId).push(data);
28
+ this.drain(sessionId);
29
+ }
30
+ async drain(sessionId) {
31
+ if (this.drainingSet.has(sessionId))
32
+ return;
33
+ this.drainingSet.add(sessionId);
34
+ try {
35
+ const res = this.responsesMap.get(sessionId);
36
+ const queue = this.messageQueueMap.get(sessionId);
37
+ if (res && queue) {
38
+ while (queue.length) {
39
+ const data = queue[0];
40
+ if (!res.isClosed() && res.write(JSON.stringify(data) + stream_constants_1.DELIMITER)) {
41
+ queue.shift();
42
+ await Promise.resolve(void 0);
43
+ }
44
+ else {
45
+ break;
46
+ }
47
+ }
48
+ }
49
+ }
50
+ finally {
51
+ this.drainingSet.delete(sessionId);
52
+ }
53
+ // re-drain if items were pushed while we were draining
54
+ if (this.messageQueueMap.get(sessionId)?.length)
55
+ this.drain(sessionId);
56
+ }
57
+ subscribe(eventName, sessionId) {
58
+ if (!this.listenersMap.has(eventName)) {
59
+ this.listenersMap.set(eventName, new Set());
60
+ }
61
+ this.listenersMap.get(eventName).add(sessionId);
62
+ }
63
+ unsubscribe(eventName, sessionId) {
64
+ const set = this.listenersMap.get(eventName);
65
+ if (set) {
66
+ set.delete(sessionId);
67
+ if (set.size === 0)
68
+ this.listenersMap.delete(eventName);
69
+ }
70
+ }
71
+ unsubscribeAll(sessionId) {
72
+ this.listenersMap.forEach((sessions, eventName) => {
73
+ sessions.delete(sessionId);
74
+ if (sessions.size === 0)
75
+ this.listenersMap.delete(eventName);
76
+ });
77
+ }
78
+ parseEventNames(body) {
79
+ const names = [];
80
+ const parser = (0, json_stream_1.createParser)({
81
+ delimiter: stream_constants_1.DELIMITER,
82
+ onData: ({ name }) => { if (name)
83
+ names.push(name); },
84
+ });
85
+ parser.write(body);
86
+ parser.close();
87
+ return names;
88
+ }
89
+ handleConnect(req, res) {
90
+ let sessionId = req.getHeader(stream_constants_1.SESSION_ID_KEY).trim();
91
+ if (!sessionId.startsWith(stream_constants_1.SESSION_ID_PREFIX)) {
92
+ sessionId = stream_constants_1.SESSION_ID_PREFIX + (0, nanoid_1.nanoid)(80);
93
+ }
94
+ const oldRes = this.responsesMap.get(sessionId);
95
+ if (oldRes) {
96
+ oldRes.end();
97
+ oldRes.destroy();
98
+ }
99
+ this.responsesMap.set(sessionId, res);
100
+ if (!this.heartbeatInterval) {
101
+ this.heartbeatInterval = setInterval(() => {
102
+ this.responsesMap.forEach((_, sid) => this.push(sid, { type: stream_constants_1.HEARTBEAT_TYPE, ts: Date.now() }));
103
+ }, stream_constants_1.HEARTBEAT_INTERVAL_MS);
104
+ }
105
+ res.onClose(() => {
106
+ this.responsesMap.delete(sessionId);
107
+ this.messageQueueMap.delete(sessionId);
108
+ this.unsubscribeAll(sessionId);
109
+ if (this.responsesMap.size === 0) {
110
+ clearInterval(this.heartbeatInterval);
111
+ this.heartbeatInterval = undefined;
112
+ }
113
+ });
114
+ res.writeHead(200, {
115
+ [stream_constants_1.SESSION_ID_KEY]: sessionId,
116
+ [stream_constants_1.SERVER_ID_KEY]: this.serverId,
117
+ 'Cache-Control': 'no-cache, no-transform',
118
+ 'Content-Type': 'text/plain; charset=utf-8',
119
+ 'Content-Encoding': 'none',
120
+ 'Transfer-Encoding': 'chunked',
121
+ 'Access-Control-Allow-Origin': '*',
122
+ 'Connection': 'keep-alive',
123
+ 'X-Accel-Buffering': 'no',
124
+ });
125
+ res.flushHeaders();
126
+ this.drain(sessionId);
127
+ }
128
+ handleSubscribe(req, res) {
129
+ const sessionId = req.getHeader(stream_constants_1.SESSION_ID_KEY).trim();
130
+ const eventNames = this.parseEventNames(req.getBody());
131
+ eventNames.forEach(eventName => this.subscribe(eventName, sessionId));
132
+ res.end();
133
+ }
134
+ handleUnsubscribe(req, res) {
135
+ const sessionId = req.getHeader(stream_constants_1.SESSION_ID_KEY).trim();
136
+ const eventNames = this.parseEventNames(req.getBody());
137
+ eventNames.forEach(eventName => this.unsubscribe(eventName, sessionId));
138
+ res.end();
139
+ }
140
+ }
141
+ exports.EventsServer = EventsServer;
@@ -0,0 +1,6 @@
1
+ export { Server } from './server';
2
+ export { RpcServer } from './rpc';
3
+ export type { RpcHandler } from './rpc';
4
+ export { EventsServer } from './events';
5
+ export { Router } from './router';
6
+ export type { ServerReq, ServerRes, HttpHandler } from './router';