routeflow-api 0.2.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.
- package/README.md +93 -0
- package/dist/adapters/cassandra.cjs +117 -0
- package/dist/adapters/cassandra.cjs.map +1 -0
- package/dist/adapters/cassandra.d.cts +37 -0
- package/dist/adapters/cassandra.d.ts +37 -0
- package/dist/adapters/cassandra.js +90 -0
- package/dist/adapters/cassandra.js.map +1 -0
- package/dist/adapters/dynamodb.cjs +180 -0
- package/dist/adapters/dynamodb.cjs.map +1 -0
- package/dist/adapters/dynamodb.d.cts +48 -0
- package/dist/adapters/dynamodb.d.ts +48 -0
- package/dist/adapters/dynamodb.js +153 -0
- package/dist/adapters/dynamodb.js.map +1 -0
- package/dist/adapters/elasticsearch.cjs +120 -0
- package/dist/adapters/elasticsearch.cjs.map +1 -0
- package/dist/adapters/elasticsearch.d.cts +43 -0
- package/dist/adapters/elasticsearch.d.ts +43 -0
- package/dist/adapters/elasticsearch.js +93 -0
- package/dist/adapters/elasticsearch.js.map +1 -0
- package/dist/adapters/mongodb.cjs +159 -0
- package/dist/adapters/mongodb.cjs.map +1 -0
- package/dist/adapters/mongodb.d.cts +54 -0
- package/dist/adapters/mongodb.d.ts +54 -0
- package/dist/adapters/mongodb.js +132 -0
- package/dist/adapters/mongodb.js.map +1 -0
- package/dist/adapters/mysql.cjs +159 -0
- package/dist/adapters/mysql.cjs.map +1 -0
- package/dist/adapters/mysql.d.cts +63 -0
- package/dist/adapters/mysql.d.ts +63 -0
- package/dist/adapters/mysql.js +132 -0
- package/dist/adapters/mysql.js.map +1 -0
- package/dist/adapters/opensearch.cjs +120 -0
- package/dist/adapters/opensearch.cjs.map +1 -0
- package/dist/adapters/opensearch.d.cts +2 -0
- package/dist/adapters/opensearch.d.ts +2 -0
- package/dist/adapters/opensearch.js +93 -0
- package/dist/adapters/opensearch.js.map +1 -0
- package/dist/adapters/postgres.cjs +271 -0
- package/dist/adapters/postgres.cjs.map +1 -0
- package/dist/adapters/postgres.d.cts +81 -0
- package/dist/adapters/postgres.d.ts +81 -0
- package/dist/adapters/postgres.js +244 -0
- package/dist/adapters/postgres.js.map +1 -0
- package/dist/adapters/redis.cjs +153 -0
- package/dist/adapters/redis.cjs.map +1 -0
- package/dist/adapters/redis.d.cts +40 -0
- package/dist/adapters/redis.d.ts +40 -0
- package/dist/adapters/redis.js +126 -0
- package/dist/adapters/redis.js.map +1 -0
- package/dist/adapters/snowflake.cjs +117 -0
- package/dist/adapters/snowflake.cjs.map +1 -0
- package/dist/adapters/snowflake.d.cts +37 -0
- package/dist/adapters/snowflake.d.ts +37 -0
- package/dist/adapters/snowflake.js +90 -0
- package/dist/adapters/snowflake.js.map +1 -0
- package/dist/client/index.cjs +484 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +174 -0
- package/dist/client/index.d.ts +174 -0
- package/dist/client/index.js +455 -0
- package/dist/client/index.js.map +1 -0
- package/dist/index.cjs +935 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +190 -0
- package/dist/index.d.ts +190 -0
- package/dist/index.js +890 -0
- package/dist/index.js.map +1 -0
- package/dist/types-tPDla8AE.d.cts +75 -0
- package/dist/types-tPDla8AE.d.ts +75 -0
- package/package.json +157 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
MemoryAdapter: () => MemoryAdapter,
|
|
34
|
+
PollingAdapter: () => PollingAdapter,
|
|
35
|
+
Reactive: () => Reactive,
|
|
36
|
+
ReactiveApiError: () => ReactiveApiError,
|
|
37
|
+
ReactiveApp: () => ReactiveApp,
|
|
38
|
+
Route: () => Route,
|
|
39
|
+
SUPPORTED_DATABASES: () => SUPPORTED_DATABASES,
|
|
40
|
+
createApp: () => createApp,
|
|
41
|
+
getDatabaseSupport: () => getDatabaseSupport,
|
|
42
|
+
listOfficialDatabases: () => listOfficialDatabases,
|
|
43
|
+
listSupportedDatabases: () => listSupportedDatabases
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(src_exports);
|
|
46
|
+
var import_reflect_metadata = require("reflect-metadata");
|
|
47
|
+
var import_fastify = __toESM(require("fastify"), 1);
|
|
48
|
+
|
|
49
|
+
// src/core/decorator/route.ts
|
|
50
|
+
var ROUTE_METADATA = /* @__PURE__ */ Symbol("reactive-api:route");
|
|
51
|
+
function Route(method, path) {
|
|
52
|
+
return function(target, propertyKey) {
|
|
53
|
+
const metadata = { method, path };
|
|
54
|
+
Reflect.defineMetadata(ROUTE_METADATA, metadata, target, propertyKey);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/core/decorator/reactive.ts
|
|
59
|
+
var REACTIVE_METADATA = /* @__PURE__ */ Symbol("reactive-api:reactive");
|
|
60
|
+
function Reactive(options) {
|
|
61
|
+
return function(target, propertyKey) {
|
|
62
|
+
Reflect.defineMetadata(REACTIVE_METADATA, options, target, propertyKey);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/core/errors.ts
|
|
67
|
+
var ReactiveApiError = class extends Error {
|
|
68
|
+
/** Machine-readable error code (e.g. 'ADAPTER_NOT_CONNECTED', 'INVALID_ROUTE') */
|
|
69
|
+
code;
|
|
70
|
+
constructor(code, message) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.name = "ReactiveApiError";
|
|
73
|
+
this.code = code;
|
|
74
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/core/reactive/engine.ts
|
|
79
|
+
var ReactiveEngine = class {
|
|
80
|
+
constructor(adapter) {
|
|
81
|
+
this.adapter = adapter;
|
|
82
|
+
}
|
|
83
|
+
adapter;
|
|
84
|
+
endpoints = [];
|
|
85
|
+
/** clientId → Subscription */
|
|
86
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
87
|
+
/** table → adapter unsubscribe fn */
|
|
88
|
+
tableWatchers = /* @__PURE__ */ new Map();
|
|
89
|
+
/** "clientId:path" → debounce timer id */
|
|
90
|
+
debounceTimers = /* @__PURE__ */ new Map();
|
|
91
|
+
/**
|
|
92
|
+
* Register a reactive endpoint so the engine can fan-out pushes to subscribers.
|
|
93
|
+
*/
|
|
94
|
+
registerEndpoint(endpoint) {
|
|
95
|
+
this.endpoints.push(endpoint);
|
|
96
|
+
const tables = Array.isArray(endpoint.options.watch) ? endpoint.options.watch : [endpoint.options.watch];
|
|
97
|
+
for (const table of tables) {
|
|
98
|
+
this.setupTableWatcher(table);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Subscribe a WebSocket client to a path.
|
|
103
|
+
* When the watched table(s) change and the filter passes, pushFn is called.
|
|
104
|
+
*
|
|
105
|
+
* @param clientId - Unique identifier for the client connection
|
|
106
|
+
* @param path - The concrete path the client subscribed to
|
|
107
|
+
* @param ctx - Context built from the subscribed path
|
|
108
|
+
* @param pushFn - Callback to deliver data to the client
|
|
109
|
+
*/
|
|
110
|
+
subscribe(clientId, path, ctx, pushFn) {
|
|
111
|
+
this.subscriptions.set(clientId, { path, ctx, pushFn });
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Remove a client's subscription and clean up any pending debounce timers.
|
|
115
|
+
*/
|
|
116
|
+
unsubscribe(clientId) {
|
|
117
|
+
this.subscriptions.delete(clientId);
|
|
118
|
+
for (const key of this.debounceTimers.keys()) {
|
|
119
|
+
if (key.startsWith(`${clientId}:`)) {
|
|
120
|
+
clearTimeout(this.debounceTimers.get(key));
|
|
121
|
+
this.debounceTimers.delete(key);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Tear down all table watchers. Call this when the app shuts down.
|
|
127
|
+
*/
|
|
128
|
+
destroy() {
|
|
129
|
+
for (const unsubscribe of this.tableWatchers.values()) {
|
|
130
|
+
unsubscribe();
|
|
131
|
+
}
|
|
132
|
+
this.tableWatchers.clear();
|
|
133
|
+
for (const timer of this.debounceTimers.values()) {
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
}
|
|
136
|
+
this.debounceTimers.clear();
|
|
137
|
+
}
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Private helpers
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
setupTableWatcher(table) {
|
|
142
|
+
if (this.tableWatchers.has(table)) return;
|
|
143
|
+
const unsubscribe = this.adapter.onChange(table, (event) => {
|
|
144
|
+
this.onChangeEvent(event);
|
|
145
|
+
});
|
|
146
|
+
this.tableWatchers.set(table, unsubscribe);
|
|
147
|
+
}
|
|
148
|
+
onChangeEvent(event) {
|
|
149
|
+
const matchingEndpoints = this.endpoints.filter((ep) => {
|
|
150
|
+
const tables = Array.isArray(ep.options.watch) ? ep.options.watch : [ep.options.watch];
|
|
151
|
+
return tables.includes(event.table);
|
|
152
|
+
});
|
|
153
|
+
for (const endpoint of matchingEndpoints) {
|
|
154
|
+
for (const [clientId, sub] of this.subscriptions) {
|
|
155
|
+
if (!pathMatchesPattern(sub.path, endpoint.routePath)) continue;
|
|
156
|
+
if (endpoint.options.filter) {
|
|
157
|
+
try {
|
|
158
|
+
if (!endpoint.options.filter(event, sub.ctx)) continue;
|
|
159
|
+
} catch (err) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
this.schedulePush(clientId, endpoint, sub, event);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
schedulePush(clientId, endpoint, sub, _event) {
|
|
168
|
+
const debounceMs = endpoint.options.debounce;
|
|
169
|
+
if (debounceMs !== void 0 && debounceMs > 0) {
|
|
170
|
+
const timerKey = `${clientId}:${sub.path}`;
|
|
171
|
+
const existing = this.debounceTimers.get(timerKey);
|
|
172
|
+
if (existing !== void 0) clearTimeout(existing);
|
|
173
|
+
const timer = setTimeout(() => {
|
|
174
|
+
this.debounceTimers.delete(timerKey);
|
|
175
|
+
this.executePush(endpoint, sub);
|
|
176
|
+
}, debounceMs);
|
|
177
|
+
this.debounceTimers.set(timerKey, timer);
|
|
178
|
+
} else {
|
|
179
|
+
this.executePush(endpoint, sub);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
executePush(endpoint, sub) {
|
|
183
|
+
endpoint.handler(sub.ctx).then(
|
|
184
|
+
(data) => sub.pushFn(sub.path, data),
|
|
185
|
+
(err) => {
|
|
186
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
187
|
+
throw new ReactiveApiError("HANDLER_ERROR", `Reactive handler failed: ${message}`);
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
function pathMatchesPattern(concretePath, pattern) {
|
|
193
|
+
const regexStr = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, "([^/]+)");
|
|
194
|
+
const regex = new RegExp(`^${regexStr}$`);
|
|
195
|
+
return regex.test(concretePath);
|
|
196
|
+
}
|
|
197
|
+
function extractParams(concretePath, pattern) {
|
|
198
|
+
const paramNames = [];
|
|
199
|
+
const regexStr = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
|
|
200
|
+
paramNames.push(name);
|
|
201
|
+
return "([^/]+)";
|
|
202
|
+
});
|
|
203
|
+
const regex = new RegExp(`^${regexStr}$`);
|
|
204
|
+
const match = concretePath.match(regex);
|
|
205
|
+
if (!match) return {};
|
|
206
|
+
return Object.fromEntries(paramNames.map((name, i) => [name, match[i + 1]]));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/core/transport/websocket-transport.ts
|
|
210
|
+
var import_node_crypto = require("crypto");
|
|
211
|
+
var import_ws = require("ws");
|
|
212
|
+
function isSubscribeMessage(value) {
|
|
213
|
+
return typeof value === "object" && value !== null && value["type"] === "subscribe" && typeof value["path"] === "string";
|
|
214
|
+
}
|
|
215
|
+
var WebSocketTransport = class {
|
|
216
|
+
constructor(engine, routePatterns) {
|
|
217
|
+
this.engine = engine;
|
|
218
|
+
this.routePatterns = routePatterns;
|
|
219
|
+
this.wss = new import_ws.WebSocketServer({ noServer: true });
|
|
220
|
+
this.wss.on("connection", (ws, req) => this.handleConnection(ws, req));
|
|
221
|
+
}
|
|
222
|
+
engine;
|
|
223
|
+
routePatterns;
|
|
224
|
+
wss;
|
|
225
|
+
/** Maps clientId → registered route pattern, for param extraction */
|
|
226
|
+
clientPatterns = /* @__PURE__ */ new Map();
|
|
227
|
+
/**
|
|
228
|
+
* Attach to the raw Node.js HTTP server so WebSocket upgrade requests are
|
|
229
|
+
* handled alongside Fastify routes.
|
|
230
|
+
*/
|
|
231
|
+
attach(httpServer) {
|
|
232
|
+
httpServer.on("upgrade", (req, socket, head) => {
|
|
233
|
+
this.wss.handleUpgrade(req, socket, head, (ws) => {
|
|
234
|
+
this.wss.emit("connection", ws, req);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/** Gracefully close all connections. */
|
|
239
|
+
async close() {
|
|
240
|
+
return new Promise((resolve, reject) => {
|
|
241
|
+
this.wss.close((err) => err ? reject(err) : resolve());
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// Connection handling
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
handleConnection(ws, _req) {
|
|
248
|
+
const clientId = (0, import_node_crypto.randomUUID)();
|
|
249
|
+
ws.on("message", (raw) => {
|
|
250
|
+
let parsed;
|
|
251
|
+
try {
|
|
252
|
+
parsed = JSON.parse(raw.toString());
|
|
253
|
+
} catch {
|
|
254
|
+
this.sendError(ws, "INVALID_JSON", "Message must be valid JSON");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (!isSubscribeMessage(parsed)) {
|
|
258
|
+
this.sendError(ws, "INVALID_MESSAGE", 'Expected { type: "subscribe", path: string }');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.handleSubscribe(ws, clientId, parsed);
|
|
262
|
+
});
|
|
263
|
+
ws.on("close", () => {
|
|
264
|
+
this.engine.unsubscribe(clientId);
|
|
265
|
+
this.clientPatterns.delete(clientId);
|
|
266
|
+
});
|
|
267
|
+
ws.on("error", (err) => {
|
|
268
|
+
this.engine.unsubscribe(clientId);
|
|
269
|
+
this.clientPatterns.delete(clientId);
|
|
270
|
+
if (process.env["NODE_ENV"] !== "production") {
|
|
271
|
+
console.error("[RouteFlow] WebSocket error:", err.message);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
handleSubscribe(ws, clientId, msg) {
|
|
276
|
+
const { path, query = {} } = msg;
|
|
277
|
+
const pattern = this.routePatterns.find((p) => this.matchesPattern(path, p));
|
|
278
|
+
if (!pattern) {
|
|
279
|
+
this.sendError(ws, "NO_REACTIVE_ENDPOINT", `No reactive endpoint found for path: ${path}`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const params = extractParams(path, pattern);
|
|
283
|
+
const ctx = {
|
|
284
|
+
params,
|
|
285
|
+
query,
|
|
286
|
+
body: void 0,
|
|
287
|
+
headers: {}
|
|
288
|
+
};
|
|
289
|
+
const pushFn = (subscribedPath, data) => {
|
|
290
|
+
if (ws.readyState !== import_ws.WebSocket.OPEN) return;
|
|
291
|
+
const msg2 = { type: "update", path: subscribedPath, data };
|
|
292
|
+
ws.send(JSON.stringify(msg2));
|
|
293
|
+
};
|
|
294
|
+
this.engine.unsubscribe(clientId);
|
|
295
|
+
this.clientPatterns.set(clientId, pattern);
|
|
296
|
+
this.engine.subscribe(clientId, path, ctx, pushFn);
|
|
297
|
+
}
|
|
298
|
+
matchesPattern(concretePath, pattern) {
|
|
299
|
+
const regexStr = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, "([^/]+)");
|
|
300
|
+
return new RegExp(`^${regexStr}$`).test(concretePath);
|
|
301
|
+
}
|
|
302
|
+
sendError(ws, code, message) {
|
|
303
|
+
if (ws.readyState !== import_ws.WebSocket.OPEN) return;
|
|
304
|
+
const msg = { type: "error", code, message };
|
|
305
|
+
ws.send(JSON.stringify(msg));
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// src/core/transport/sse-transport.ts
|
|
310
|
+
var import_node_crypto2 = require("crypto");
|
|
311
|
+
var SseTransport = class {
|
|
312
|
+
constructor(engine, routePatterns) {
|
|
313
|
+
this.engine = engine;
|
|
314
|
+
this.routePatterns = routePatterns;
|
|
315
|
+
}
|
|
316
|
+
engine;
|
|
317
|
+
routePatterns;
|
|
318
|
+
/** clientId → reply (kept open) */
|
|
319
|
+
connections = /* @__PURE__ */ new Map();
|
|
320
|
+
/**
|
|
321
|
+
* Register the SSE subscription endpoint on the Fastify instance.
|
|
322
|
+
* Must be called before `fastify.listen()`.
|
|
323
|
+
*/
|
|
324
|
+
register(fastify) {
|
|
325
|
+
fastify.get("/_sse/subscribe", async (req, reply) => {
|
|
326
|
+
const query = req.query;
|
|
327
|
+
const path = query["path"];
|
|
328
|
+
if (!path) {
|
|
329
|
+
throw new ReactiveApiError("SSE_MISSING_PATH", 'Query param "path" is required');
|
|
330
|
+
}
|
|
331
|
+
const decodedPath = decodeURIComponent(path);
|
|
332
|
+
const pattern = this.routePatterns.find((p) => this.matchesPattern(decodedPath, p));
|
|
333
|
+
if (!pattern) {
|
|
334
|
+
reply.code(404);
|
|
335
|
+
throw new ReactiveApiError(
|
|
336
|
+
"SSE_NO_REACTIVE_ENDPOINT",
|
|
337
|
+
`No reactive endpoint found for path: ${decodedPath}`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
const params = extractParams(decodedPath, pattern);
|
|
341
|
+
const clientQuery = { ...query };
|
|
342
|
+
delete clientQuery["path"];
|
|
343
|
+
const ctx = {
|
|
344
|
+
params,
|
|
345
|
+
query: clientQuery,
|
|
346
|
+
body: void 0,
|
|
347
|
+
headers: req.headers
|
|
348
|
+
};
|
|
349
|
+
const clientId = (0, import_node_crypto2.randomUUID)();
|
|
350
|
+
reply.raw.writeHead(200, {
|
|
351
|
+
"Content-Type": "text/event-stream",
|
|
352
|
+
"Cache-Control": "no-cache",
|
|
353
|
+
Connection: "keep-alive",
|
|
354
|
+
"X-Accel-Buffering": "no"
|
|
355
|
+
// disable nginx buffering
|
|
356
|
+
});
|
|
357
|
+
reply.raw.write(": connected\n\n");
|
|
358
|
+
this.connections.set(clientId, reply);
|
|
359
|
+
const pushFn = (subscribedPath, data) => {
|
|
360
|
+
if (reply.raw.destroyed) return;
|
|
361
|
+
const payload = JSON.stringify({ type: "update", path: subscribedPath, data });
|
|
362
|
+
reply.raw.write(`data: ${payload}
|
|
363
|
+
|
|
364
|
+
`);
|
|
365
|
+
};
|
|
366
|
+
this.engine.subscribe(clientId, decodedPath, ctx, pushFn);
|
|
367
|
+
req.raw.on("close", () => {
|
|
368
|
+
this.engine.unsubscribe(clientId);
|
|
369
|
+
this.connections.delete(clientId);
|
|
370
|
+
if (!reply.raw.destroyed) reply.raw.end();
|
|
371
|
+
});
|
|
372
|
+
await new Promise((resolve) => {
|
|
373
|
+
req.raw.on("close", resolve);
|
|
374
|
+
req.raw.on("error", resolve);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
/** Close all open SSE connections. */
|
|
379
|
+
async close() {
|
|
380
|
+
for (const reply of this.connections.values()) {
|
|
381
|
+
if (!reply.raw.destroyed) reply.raw.end();
|
|
382
|
+
}
|
|
383
|
+
this.connections.clear();
|
|
384
|
+
}
|
|
385
|
+
matchesPattern(concretePath, pattern) {
|
|
386
|
+
const regexStr = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, "([^/]+)");
|
|
387
|
+
return new RegExp(`^${regexStr}$`).test(concretePath);
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// src/core/database-support.ts
|
|
392
|
+
var SUPPORTED_DATABASES = [
|
|
393
|
+
{
|
|
394
|
+
key: "postgresql",
|
|
395
|
+
name: "PostgreSQL",
|
|
396
|
+
aliases: ["postgres", "psql"],
|
|
397
|
+
categories: ["rdbms"],
|
|
398
|
+
supportedModes: ["native-adapter", "polling-adapter", "external-cdc-bridge"],
|
|
399
|
+
tier: "official"
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
key: "mysql",
|
|
403
|
+
name: "MySQL",
|
|
404
|
+
aliases: [],
|
|
405
|
+
categories: ["rdbms"],
|
|
406
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
407
|
+
tier: "official"
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
key: "mariadb",
|
|
411
|
+
name: "MariaDB",
|
|
412
|
+
aliases: [],
|
|
413
|
+
categories: ["rdbms"],
|
|
414
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
415
|
+
tier: "experimental"
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
key: "oracle-db",
|
|
419
|
+
name: "Oracle DB",
|
|
420
|
+
aliases: ["oracle", "oracle database"],
|
|
421
|
+
categories: ["rdbms"],
|
|
422
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
423
|
+
tier: "experimental"
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
key: "ms-sql-server",
|
|
427
|
+
name: "MS SQL Server",
|
|
428
|
+
aliases: ["sql server", "mssql", "ms sql"],
|
|
429
|
+
categories: ["rdbms"],
|
|
430
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
431
|
+
tier: "experimental"
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
key: "sqlite",
|
|
435
|
+
name: "SQLite",
|
|
436
|
+
aliases: [],
|
|
437
|
+
categories: ["rdbms"],
|
|
438
|
+
supportedModes: ["polling-adapter"],
|
|
439
|
+
tier: "experimental"
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
key: "mongodb",
|
|
443
|
+
name: "MongoDB",
|
|
444
|
+
aliases: ["mongo"],
|
|
445
|
+
categories: ["nosql"],
|
|
446
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
447
|
+
tier: "official"
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
key: "redis",
|
|
451
|
+
name: "Redis",
|
|
452
|
+
aliases: [],
|
|
453
|
+
categories: ["nosql", "in-memory"],
|
|
454
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
455
|
+
tier: "official"
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
key: "cassandra",
|
|
459
|
+
name: "Cassandra",
|
|
460
|
+
aliases: ["apache cassandra"],
|
|
461
|
+
categories: ["nosql"],
|
|
462
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
463
|
+
tier: "experimental"
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
key: "dynamodb",
|
|
467
|
+
name: "DynamoDB",
|
|
468
|
+
aliases: ["dynamo"],
|
|
469
|
+
categories: ["nosql"],
|
|
470
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
471
|
+
tier: "official"
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
key: "neo4j",
|
|
475
|
+
name: "Neo4j",
|
|
476
|
+
aliases: [],
|
|
477
|
+
categories: ["nosql"],
|
|
478
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
479
|
+
tier: "experimental"
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
key: "elasticsearch",
|
|
483
|
+
name: "Elasticsearch",
|
|
484
|
+
aliases: ["elastic"],
|
|
485
|
+
categories: ["nosql", "search-engine"],
|
|
486
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
487
|
+
tier: "official"
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
key: "hbase",
|
|
491
|
+
name: "HBase",
|
|
492
|
+
aliases: ["apache hbase"],
|
|
493
|
+
categories: ["nosql"],
|
|
494
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
495
|
+
tier: "experimental"
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
key: "couchdb",
|
|
499
|
+
name: "CouchDB",
|
|
500
|
+
aliases: ["apache couchdb"],
|
|
501
|
+
categories: ["nosql"],
|
|
502
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
503
|
+
tier: "experimental"
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
key: "influxdb",
|
|
507
|
+
name: "InfluxDB",
|
|
508
|
+
aliases: ["influx"],
|
|
509
|
+
categories: ["time-series"],
|
|
510
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
511
|
+
tier: "experimental"
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
key: "timescaledb",
|
|
515
|
+
name: "TimescaleDB",
|
|
516
|
+
aliases: ["timescale"],
|
|
517
|
+
categories: ["time-series", "rdbms"],
|
|
518
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
519
|
+
tier: "experimental"
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
key: "prometheus",
|
|
523
|
+
name: "Prometheus",
|
|
524
|
+
aliases: ["prom"],
|
|
525
|
+
categories: ["time-series"],
|
|
526
|
+
supportedModes: ["polling-adapter"],
|
|
527
|
+
tier: "experimental"
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
key: "opensearch",
|
|
531
|
+
name: "OpenSearch",
|
|
532
|
+
aliases: [],
|
|
533
|
+
categories: ["search-engine"],
|
|
534
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
535
|
+
tier: "official"
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
key: "solr",
|
|
539
|
+
name: "Solr",
|
|
540
|
+
aliases: ["apache solr"],
|
|
541
|
+
categories: ["search-engine"],
|
|
542
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
543
|
+
tier: "experimental"
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
key: "snowflake",
|
|
547
|
+
name: "Snowflake",
|
|
548
|
+
aliases: [],
|
|
549
|
+
categories: ["cloud-data-warehouse"],
|
|
550
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
551
|
+
tier: "official"
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
key: "bigquery",
|
|
555
|
+
name: "BigQuery",
|
|
556
|
+
aliases: ["google bigquery"],
|
|
557
|
+
categories: ["cloud-data-warehouse"],
|
|
558
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
559
|
+
tier: "experimental"
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
key: "redshift",
|
|
563
|
+
name: "Redshift",
|
|
564
|
+
aliases: ["amazon redshift"],
|
|
565
|
+
categories: ["cloud-data-warehouse"],
|
|
566
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
567
|
+
tier: "experimental"
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
key: "azure-synapse",
|
|
571
|
+
name: "Azure Synapse",
|
|
572
|
+
aliases: ["synapse", "azure synapse analytics"],
|
|
573
|
+
categories: ["cloud-data-warehouse"],
|
|
574
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
575
|
+
tier: "experimental"
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
key: "memcached",
|
|
579
|
+
name: "Memcached",
|
|
580
|
+
aliases: [],
|
|
581
|
+
categories: ["in-memory"],
|
|
582
|
+
supportedModes: ["polling-adapter"],
|
|
583
|
+
tier: "experimental"
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
key: "voltdb",
|
|
587
|
+
name: "VoltDB",
|
|
588
|
+
aliases: [],
|
|
589
|
+
categories: ["in-memory"],
|
|
590
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
591
|
+
tier: "experimental"
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
key: "cockroachdb",
|
|
595
|
+
name: "CockroachDB",
|
|
596
|
+
aliases: ["cockroach"],
|
|
597
|
+
categories: ["newsql", "rdbms"],
|
|
598
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
599
|
+
tier: "experimental"
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
key: "tidb",
|
|
603
|
+
name: "TiDB",
|
|
604
|
+
aliases: [],
|
|
605
|
+
categories: ["newsql", "rdbms"],
|
|
606
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
607
|
+
tier: "experimental"
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
key: "spanner",
|
|
611
|
+
name: "Spanner",
|
|
612
|
+
aliases: ["google spanner", "cloud spanner"],
|
|
613
|
+
categories: ["newsql"],
|
|
614
|
+
supportedModes: ["polling-adapter", "external-cdc-bridge"],
|
|
615
|
+
tier: "experimental"
|
|
616
|
+
}
|
|
617
|
+
];
|
|
618
|
+
function normaliseDatabaseName(value) {
|
|
619
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
620
|
+
}
|
|
621
|
+
function getDatabaseSupport(name) {
|
|
622
|
+
const normalised = normaliseDatabaseName(name);
|
|
623
|
+
return SUPPORTED_DATABASES.find((database) => {
|
|
624
|
+
if (normaliseDatabaseName(database.name) === normalised) return true;
|
|
625
|
+
if (normaliseDatabaseName(database.key) === normalised) return true;
|
|
626
|
+
return database.aliases.some((alias) => normaliseDatabaseName(alias) === normalised);
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
function listSupportedDatabases(category) {
|
|
630
|
+
if (!category) return SUPPORTED_DATABASES;
|
|
631
|
+
return SUPPORTED_DATABASES.filter((database) => database.categories.includes(category));
|
|
632
|
+
}
|
|
633
|
+
function listOfficialDatabases(category) {
|
|
634
|
+
const databases = listSupportedDatabases(category);
|
|
635
|
+
return databases.filter((database) => database.tier === "official");
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/core/adapter/memory-adapter.ts
|
|
639
|
+
var MemoryAdapter = class {
|
|
640
|
+
listeners = /* @__PURE__ */ new Map();
|
|
641
|
+
connected = false;
|
|
642
|
+
/** No-op — MemoryAdapter requires no real connection. */
|
|
643
|
+
async connect() {
|
|
644
|
+
this.connected = true;
|
|
645
|
+
}
|
|
646
|
+
/** No-op — clears all listeners on disconnect. */
|
|
647
|
+
async disconnect() {
|
|
648
|
+
this.listeners.clear();
|
|
649
|
+
this.connected = false;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Register a listener for changes on a specific table.
|
|
653
|
+
* @returns An unsubscribe function.
|
|
654
|
+
*/
|
|
655
|
+
onChange(table, callback) {
|
|
656
|
+
if (!this.listeners.has(table)) {
|
|
657
|
+
this.listeners.set(table, /* @__PURE__ */ new Set());
|
|
658
|
+
}
|
|
659
|
+
this.listeners.get(table).add(callback);
|
|
660
|
+
return () => {
|
|
661
|
+
this.listeners.get(table)?.delete(callback);
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Manually emit a change event on a table.
|
|
666
|
+
* Useful in tests and examples to simulate DB changes without a real database.
|
|
667
|
+
*
|
|
668
|
+
* @param table - Table name to emit the event on
|
|
669
|
+
* @param event - Change event data (table and timestamp are filled in automatically)
|
|
670
|
+
*/
|
|
671
|
+
emit(table, event) {
|
|
672
|
+
const fullEvent = {
|
|
673
|
+
...event,
|
|
674
|
+
table,
|
|
675
|
+
timestamp: Date.now()
|
|
676
|
+
};
|
|
677
|
+
const callbacks = this.listeners.get(table);
|
|
678
|
+
if (!callbacks) return;
|
|
679
|
+
for (const cb of callbacks) {
|
|
680
|
+
cb(fullEvent);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/** Returns true if connect() has been called and disconnect() has not. */
|
|
684
|
+
get isConnected() {
|
|
685
|
+
return this.connected;
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
// src/core/adapter/polling-adapter.ts
|
|
690
|
+
var PollingAdapter = class {
|
|
691
|
+
listeners = /* @__PURE__ */ new Map();
|
|
692
|
+
cursors = /* @__PURE__ */ new Map();
|
|
693
|
+
timers = /* @__PURE__ */ new Map();
|
|
694
|
+
activeTables = /* @__PURE__ */ new Set();
|
|
695
|
+
intervalMs;
|
|
696
|
+
now;
|
|
697
|
+
readChanges;
|
|
698
|
+
onError;
|
|
699
|
+
connected = false;
|
|
700
|
+
constructor(options) {
|
|
701
|
+
this.intervalMs = options.intervalMs ?? 1e3;
|
|
702
|
+
this.now = options.now ?? (() => Date.now());
|
|
703
|
+
this.readChanges = options.readChanges;
|
|
704
|
+
this.onError = options.onError;
|
|
705
|
+
}
|
|
706
|
+
async connect() {
|
|
707
|
+
this.connected = true;
|
|
708
|
+
for (const table of this.listeners.keys()) {
|
|
709
|
+
this.ensurePolling(table);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async disconnect() {
|
|
713
|
+
this.connected = false;
|
|
714
|
+
for (const timer of this.timers.values()) {
|
|
715
|
+
clearTimeout(timer);
|
|
716
|
+
}
|
|
717
|
+
this.timers.clear();
|
|
718
|
+
this.activeTables.clear();
|
|
719
|
+
}
|
|
720
|
+
onChange(table, callback) {
|
|
721
|
+
if (!this.listeners.has(table)) {
|
|
722
|
+
this.listeners.set(table, /* @__PURE__ */ new Set());
|
|
723
|
+
}
|
|
724
|
+
this.listeners.get(table).add(callback);
|
|
725
|
+
this.ensurePolling(table);
|
|
726
|
+
return () => {
|
|
727
|
+
const callbacks = this.listeners.get(table);
|
|
728
|
+
if (!callbacks) return;
|
|
729
|
+
callbacks.delete(callback);
|
|
730
|
+
if (callbacks.size === 0) {
|
|
731
|
+
this.listeners.delete(table);
|
|
732
|
+
this.stopPolling(table);
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
ensurePolling(table) {
|
|
737
|
+
if (!this.connected || this.activeTables.has(table)) return;
|
|
738
|
+
this.activeTables.add(table);
|
|
739
|
+
void this.poll(table);
|
|
740
|
+
}
|
|
741
|
+
stopPolling(table) {
|
|
742
|
+
const timer = this.timers.get(table);
|
|
743
|
+
if (timer) clearTimeout(timer);
|
|
744
|
+
this.timers.delete(table);
|
|
745
|
+
this.activeTables.delete(table);
|
|
746
|
+
this.cursors.delete(table);
|
|
747
|
+
}
|
|
748
|
+
scheduleNext(table) {
|
|
749
|
+
if (!this.connected || !this.listeners.has(table)) {
|
|
750
|
+
this.stopPolling(table);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const timer = setTimeout(() => {
|
|
754
|
+
void this.poll(table);
|
|
755
|
+
}, this.intervalMs);
|
|
756
|
+
this.timers.set(table, timer);
|
|
757
|
+
}
|
|
758
|
+
async poll(table) {
|
|
759
|
+
if (!this.connected || !this.listeners.has(table)) {
|
|
760
|
+
this.stopPolling(table);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
try {
|
|
764
|
+
const result = await this.readChanges({
|
|
765
|
+
table,
|
|
766
|
+
cursor: this.cursors.get(table)
|
|
767
|
+
});
|
|
768
|
+
this.cursors.set(table, result.cursor);
|
|
769
|
+
for (const event of result.events) {
|
|
770
|
+
this.emit(table, event);
|
|
771
|
+
}
|
|
772
|
+
} catch (error) {
|
|
773
|
+
this.onError?.(error, { table });
|
|
774
|
+
} finally {
|
|
775
|
+
this.activeTables.delete(table);
|
|
776
|
+
this.scheduleNext(table);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
emit(defaultTable, event) {
|
|
780
|
+
const fullEvent = {
|
|
781
|
+
...event,
|
|
782
|
+
table: event.table ?? defaultTable,
|
|
783
|
+
timestamp: event.timestamp ?? this.now()
|
|
784
|
+
};
|
|
785
|
+
const callbacks = this.listeners.get(fullEvent.table);
|
|
786
|
+
if (!callbacks) return;
|
|
787
|
+
for (const callback of callbacks) {
|
|
788
|
+
callback(fullEvent);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// src/index.ts
|
|
794
|
+
var ReactiveApp = class {
|
|
795
|
+
fastify;
|
|
796
|
+
engine;
|
|
797
|
+
transport = null;
|
|
798
|
+
options;
|
|
799
|
+
/** Collected route patterns for reactive endpoints */
|
|
800
|
+
reactivePatterns = [];
|
|
801
|
+
constructor(options) {
|
|
802
|
+
this.options = {
|
|
803
|
+
transport: "websocket",
|
|
804
|
+
port: 3e3,
|
|
805
|
+
...options
|
|
806
|
+
};
|
|
807
|
+
this.fastify = (0, import_fastify.default)({ logger: false });
|
|
808
|
+
this.engine = new ReactiveEngine(this.options.adapter);
|
|
809
|
+
this.fastify.setErrorHandler((error, _req, reply) => {
|
|
810
|
+
if (error instanceof ReactiveApiError) {
|
|
811
|
+
const status = error.statusCode ?? 500;
|
|
812
|
+
reply.status(status).send({ error: error.code, message: error.message });
|
|
813
|
+
} else {
|
|
814
|
+
reply.status(500).send({ error: "INTERNAL_ERROR", message: error.message });
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Register a controller class. Scans its methods for @Route and @Reactive
|
|
820
|
+
* decorators and registers HTTP routes and reactive endpoints accordingly.
|
|
821
|
+
*
|
|
822
|
+
* @param ControllerClass - A class constructor whose methods may be decorated
|
|
823
|
+
* with @Route and/or @Reactive.
|
|
824
|
+
*/
|
|
825
|
+
register(ControllerClass) {
|
|
826
|
+
const instance = new ControllerClass();
|
|
827
|
+
const proto = Object.getPrototypeOf(instance);
|
|
828
|
+
const methodNames = Object.getOwnPropertyNames(proto).filter(
|
|
829
|
+
(name) => name !== "constructor" && typeof proto[name] === "function"
|
|
830
|
+
);
|
|
831
|
+
for (const methodName of methodNames) {
|
|
832
|
+
const routeMeta = Reflect.getMetadata(
|
|
833
|
+
ROUTE_METADATA,
|
|
834
|
+
proto,
|
|
835
|
+
methodName
|
|
836
|
+
);
|
|
837
|
+
if (!routeMeta) continue;
|
|
838
|
+
const reactiveMeta = Reflect.getMetadata(
|
|
839
|
+
REACTIVE_METADATA,
|
|
840
|
+
proto,
|
|
841
|
+
methodName
|
|
842
|
+
);
|
|
843
|
+
const handler = instance[methodName];
|
|
844
|
+
this.fastify.route({
|
|
845
|
+
method: routeMeta.method,
|
|
846
|
+
url: routeMeta.path,
|
|
847
|
+
handler: async (req, reply) => {
|
|
848
|
+
const ctx = {
|
|
849
|
+
params: req.params,
|
|
850
|
+
query: req.query,
|
|
851
|
+
body: req.body,
|
|
852
|
+
headers: req.headers
|
|
853
|
+
};
|
|
854
|
+
try {
|
|
855
|
+
const result = await handler.call(instance, ctx);
|
|
856
|
+
return reply.send(result);
|
|
857
|
+
} catch (err) {
|
|
858
|
+
if (err instanceof ReactiveApiError) throw err;
|
|
859
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
860
|
+
throw new ReactiveApiError("HANDLER_ERROR", msg);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
if (reactiveMeta) {
|
|
865
|
+
const endpoint = {
|
|
866
|
+
routePath: routeMeta.path,
|
|
867
|
+
options: reactiveMeta,
|
|
868
|
+
handler: (ctx) => handler.call(instance, ctx)
|
|
869
|
+
};
|
|
870
|
+
this.engine.registerEndpoint(endpoint);
|
|
871
|
+
this.reactivePatterns.push(routeMeta.path);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return this;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Access the underlying Fastify instance for supplemental routes such as
|
|
878
|
+
* health checks, static assets, or demo pages.
|
|
879
|
+
*/
|
|
880
|
+
getFastify() {
|
|
881
|
+
return this.fastify;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Start the HTTP server.
|
|
885
|
+
* Connects the database adapter before accepting connections.
|
|
886
|
+
*
|
|
887
|
+
* @param port - Override the port set in AppOptions
|
|
888
|
+
*/
|
|
889
|
+
async listen(port) {
|
|
890
|
+
const listenPort = port ?? this.options.port;
|
|
891
|
+
await this.options.adapter.connect();
|
|
892
|
+
if (this.options.transport === "websocket") {
|
|
893
|
+
this.transport = new WebSocketTransport(this.engine, this.reactivePatterns);
|
|
894
|
+
} else if (this.options.transport === "sse") {
|
|
895
|
+
const sseTransport = new SseTransport(this.engine, this.reactivePatterns);
|
|
896
|
+
sseTransport.register(this.fastify);
|
|
897
|
+
this.transport = sseTransport;
|
|
898
|
+
}
|
|
899
|
+
await this.fastify.ready();
|
|
900
|
+
if (this.transport instanceof WebSocketTransport) {
|
|
901
|
+
this.transport.attach(this.fastify.server);
|
|
902
|
+
}
|
|
903
|
+
await this.fastify.listen({ port: listenPort, host: "0.0.0.0" });
|
|
904
|
+
console.log(
|
|
905
|
+
`[RouteFlow] Listening on port ${listenPort} (transport: ${this.options.transport})`
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Gracefully shut down the server and disconnect from the database.
|
|
910
|
+
*/
|
|
911
|
+
async close() {
|
|
912
|
+
this.engine.destroy();
|
|
913
|
+
if (this.transport) await this.transport.close();
|
|
914
|
+
await this.fastify.close();
|
|
915
|
+
await this.options.adapter.disconnect();
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
function createApp(options) {
|
|
919
|
+
return new ReactiveApp(options);
|
|
920
|
+
}
|
|
921
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
922
|
+
0 && (module.exports = {
|
|
923
|
+
MemoryAdapter,
|
|
924
|
+
PollingAdapter,
|
|
925
|
+
Reactive,
|
|
926
|
+
ReactiveApiError,
|
|
927
|
+
ReactiveApp,
|
|
928
|
+
Route,
|
|
929
|
+
SUPPORTED_DATABASES,
|
|
930
|
+
createApp,
|
|
931
|
+
getDatabaseSupport,
|
|
932
|
+
listOfficialDatabases,
|
|
933
|
+
listSupportedDatabases
|
|
934
|
+
});
|
|
935
|
+
//# sourceMappingURL=index.cjs.map
|