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.
Files changed (70) hide show
  1. package/README.md +93 -0
  2. package/dist/adapters/cassandra.cjs +117 -0
  3. package/dist/adapters/cassandra.cjs.map +1 -0
  4. package/dist/adapters/cassandra.d.cts +37 -0
  5. package/dist/adapters/cassandra.d.ts +37 -0
  6. package/dist/adapters/cassandra.js +90 -0
  7. package/dist/adapters/cassandra.js.map +1 -0
  8. package/dist/adapters/dynamodb.cjs +180 -0
  9. package/dist/adapters/dynamodb.cjs.map +1 -0
  10. package/dist/adapters/dynamodb.d.cts +48 -0
  11. package/dist/adapters/dynamodb.d.ts +48 -0
  12. package/dist/adapters/dynamodb.js +153 -0
  13. package/dist/adapters/dynamodb.js.map +1 -0
  14. package/dist/adapters/elasticsearch.cjs +120 -0
  15. package/dist/adapters/elasticsearch.cjs.map +1 -0
  16. package/dist/adapters/elasticsearch.d.cts +43 -0
  17. package/dist/adapters/elasticsearch.d.ts +43 -0
  18. package/dist/adapters/elasticsearch.js +93 -0
  19. package/dist/adapters/elasticsearch.js.map +1 -0
  20. package/dist/adapters/mongodb.cjs +159 -0
  21. package/dist/adapters/mongodb.cjs.map +1 -0
  22. package/dist/adapters/mongodb.d.cts +54 -0
  23. package/dist/adapters/mongodb.d.ts +54 -0
  24. package/dist/adapters/mongodb.js +132 -0
  25. package/dist/adapters/mongodb.js.map +1 -0
  26. package/dist/adapters/mysql.cjs +159 -0
  27. package/dist/adapters/mysql.cjs.map +1 -0
  28. package/dist/adapters/mysql.d.cts +63 -0
  29. package/dist/adapters/mysql.d.ts +63 -0
  30. package/dist/adapters/mysql.js +132 -0
  31. package/dist/adapters/mysql.js.map +1 -0
  32. package/dist/adapters/opensearch.cjs +120 -0
  33. package/dist/adapters/opensearch.cjs.map +1 -0
  34. package/dist/adapters/opensearch.d.cts +2 -0
  35. package/dist/adapters/opensearch.d.ts +2 -0
  36. package/dist/adapters/opensearch.js +93 -0
  37. package/dist/adapters/opensearch.js.map +1 -0
  38. package/dist/adapters/postgres.cjs +271 -0
  39. package/dist/adapters/postgres.cjs.map +1 -0
  40. package/dist/adapters/postgres.d.cts +81 -0
  41. package/dist/adapters/postgres.d.ts +81 -0
  42. package/dist/adapters/postgres.js +244 -0
  43. package/dist/adapters/postgres.js.map +1 -0
  44. package/dist/adapters/redis.cjs +153 -0
  45. package/dist/adapters/redis.cjs.map +1 -0
  46. package/dist/adapters/redis.d.cts +40 -0
  47. package/dist/adapters/redis.d.ts +40 -0
  48. package/dist/adapters/redis.js +126 -0
  49. package/dist/adapters/redis.js.map +1 -0
  50. package/dist/adapters/snowflake.cjs +117 -0
  51. package/dist/adapters/snowflake.cjs.map +1 -0
  52. package/dist/adapters/snowflake.d.cts +37 -0
  53. package/dist/adapters/snowflake.d.ts +37 -0
  54. package/dist/adapters/snowflake.js +90 -0
  55. package/dist/adapters/snowflake.js.map +1 -0
  56. package/dist/client/index.cjs +484 -0
  57. package/dist/client/index.cjs.map +1 -0
  58. package/dist/client/index.d.cts +174 -0
  59. package/dist/client/index.d.ts +174 -0
  60. package/dist/client/index.js +455 -0
  61. package/dist/client/index.js.map +1 -0
  62. package/dist/index.cjs +935 -0
  63. package/dist/index.cjs.map +1 -0
  64. package/dist/index.d.cts +190 -0
  65. package/dist/index.d.ts +190 -0
  66. package/dist/index.js +890 -0
  67. package/dist/index.js.map +1 -0
  68. package/dist/types-tPDla8AE.d.cts +75 -0
  69. package/dist/types-tPDla8AE.d.ts +75 -0
  70. package/package.json +157 -0
@@ -0,0 +1,244 @@
1
+ // src/adapters/postgres/postgres-adapter.ts
2
+ import { Client, DatabaseError } from "pg";
3
+
4
+ // src/core/errors.ts
5
+ var ReactiveApiError = class extends Error {
6
+ /** Machine-readable error code (e.g. 'ADAPTER_NOT_CONNECTED', 'INVALID_ROUTE') */
7
+ code;
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.name = "ReactiveApiError";
11
+ this.code = code;
12
+ Object.setPrototypeOf(this, new.target.prototype);
13
+ }
14
+ };
15
+
16
+ // src/adapters/postgres/trigger-sql.ts
17
+ function notifyChannel(prefix) {
18
+ return `${prefix}_changes`;
19
+ }
20
+ function createTriggerFunctionSQL(schema, prefix) {
21
+ const fnName = `${schema}.${prefix}_notify_changes`;
22
+ const channel = notifyChannel(prefix);
23
+ return `
24
+ CREATE OR REPLACE FUNCTION ${fnName}()
25
+ RETURNS trigger
26
+ LANGUAGE plpgsql
27
+ AS $$
28
+ DECLARE
29
+ payload TEXT;
30
+ new_row JSON;
31
+ old_row JSON;
32
+ BEGIN
33
+ IF (TG_OP = 'DELETE') THEN
34
+ new_row := NULL;
35
+ old_row := row_to_json(OLD);
36
+ ELSIF (TG_OP = 'INSERT') THEN
37
+ new_row := row_to_json(NEW);
38
+ old_row := NULL;
39
+ ELSE
40
+ new_row := row_to_json(NEW);
41
+ old_row := row_to_json(OLD);
42
+ END IF;
43
+
44
+ payload := json_build_object(
45
+ 'table', TG_TABLE_NAME,
46
+ 'operation', TG_OP,
47
+ 'new_row', new_row,
48
+ 'old_row', old_row
49
+ )::text;
50
+
51
+ -- Truncate gracefully if payload exceeds pg's 8000-byte NOTIFY limit
52
+ IF octet_length(payload) > 7900 THEN
53
+ payload := json_build_object(
54
+ 'table', TG_TABLE_NAME,
55
+ 'operation', TG_OP,
56
+ 'new_row', NULL,
57
+ 'old_row', NULL,
58
+ '_truncated', true
59
+ )::text;
60
+ END IF;
61
+
62
+ PERFORM pg_notify('${channel}', payload);
63
+ RETURN COALESCE(NEW, OLD);
64
+ END;
65
+ $$;
66
+ `.trim();
67
+ }
68
+ function createTableTriggerSQL(schema, prefix, table) {
69
+ const triggerName = `${prefix}_notify_${table}`;
70
+ const fnName = `${schema}.${prefix}_notify_changes`;
71
+ return `
72
+ DO $$
73
+ BEGIN
74
+ IF NOT EXISTS (
75
+ SELECT 1 FROM pg_trigger
76
+ WHERE tgname = '${triggerName}'
77
+ AND tgrelid = '${schema}.${table}'::regclass
78
+ ) THEN
79
+ CREATE TRIGGER ${triggerName}
80
+ AFTER INSERT OR UPDATE OR DELETE ON ${schema}.${table}
81
+ FOR EACH ROW EXECUTE FUNCTION ${fnName}();
82
+ END IF;
83
+ END;
84
+ $$;
85
+ `.trim();
86
+ }
87
+ function dropTableTriggerSQL(schema, prefix, table) {
88
+ const triggerName = `${prefix}_notify_${table}`;
89
+ return `DROP TRIGGER IF EXISTS ${triggerName} ON ${schema}.${table};`;
90
+ }
91
+ function dropTriggerFunctionSQL(schema, prefix) {
92
+ return `DROP FUNCTION IF EXISTS ${schema}.${prefix}_notify_changes();`;
93
+ }
94
+
95
+ // src/adapters/postgres/postgres-adapter.ts
96
+ var PostgresAdapter = class {
97
+ listenClient = null;
98
+ schema;
99
+ prefix;
100
+ connectionString;
101
+ /** table → Set<callback> */
102
+ listeners = /* @__PURE__ */ new Map();
103
+ /** Tables for which a trigger has already been installed */
104
+ installedTriggers = /* @__PURE__ */ new Set();
105
+ constructor(options) {
106
+ this.connectionString = options.connectionString;
107
+ this.schema = options.schema ?? "public";
108
+ this.prefix = options.triggerPrefix ?? "reactive_api";
109
+ }
110
+ // ---------------------------------------------------------------------------
111
+ // DatabaseAdapter interface
112
+ // ---------------------------------------------------------------------------
113
+ /** Connect the LISTEN client and install the shared trigger function. */
114
+ async connect() {
115
+ if (this.listenClient) return;
116
+ const client = new Client({ connectionString: this.connectionString });
117
+ try {
118
+ await client.connect();
119
+ } catch (err) {
120
+ throw new ReactiveApiError(
121
+ "POSTGRES_CONNECT_FAILED",
122
+ `Failed to connect to PostgreSQL: ${errorMessage(err)}`
123
+ );
124
+ }
125
+ try {
126
+ await client.query(createTriggerFunctionSQL(this.schema, this.prefix));
127
+ } catch (err) {
128
+ await client.end().catch(() => void 0);
129
+ throw new ReactiveApiError(
130
+ "POSTGRES_TRIGGER_FUNCTION_FAILED",
131
+ `Failed to install trigger function: ${errorMessage(err)}`
132
+ );
133
+ }
134
+ const channel = notifyChannel(this.prefix);
135
+ await client.query(`LISTEN "${channel}"`);
136
+ client.on("notification", (msg) => {
137
+ if (msg.channel !== channel || !msg.payload) return;
138
+ this.handleNotification(msg.payload);
139
+ });
140
+ client.on("error", (err) => {
141
+ console.error("[RouteFlow/postgres] LISTEN client error:", err.message);
142
+ });
143
+ this.listenClient = client;
144
+ }
145
+ /** Disconnect the LISTEN client and optionally clean up triggers. */
146
+ async disconnect() {
147
+ if (!this.listenClient) return;
148
+ const client = this.listenClient;
149
+ this.listenClient = null;
150
+ for (const table of this.installedTriggers) {
151
+ await client.query(dropTableTriggerSQL(this.schema, this.prefix, table)).catch(() => void 0);
152
+ }
153
+ this.installedTriggers.clear();
154
+ await client.query(dropTriggerFunctionSQL(this.schema, this.prefix)).catch(() => void 0);
155
+ await client.end().catch(() => void 0);
156
+ this.listeners.clear();
157
+ }
158
+ /**
159
+ * Register a callback for changes on `table`.
160
+ * Installs a PostgreSQL trigger on first registration for this table.
161
+ *
162
+ * @returns An unsubscribe function.
163
+ */
164
+ onChange(table, callback) {
165
+ if (!this.listeners.has(table)) {
166
+ this.listeners.set(table, /* @__PURE__ */ new Set());
167
+ }
168
+ this.listeners.get(table).add(callback);
169
+ this.ensureTriggerInstalled(table).catch((err) => {
170
+ console.error(
171
+ `[RouteFlow/postgres] Failed to install trigger for table "${table}":`,
172
+ errorMessage(err)
173
+ );
174
+ });
175
+ return () => {
176
+ this.listeners.get(table)?.delete(callback);
177
+ };
178
+ }
179
+ // ---------------------------------------------------------------------------
180
+ // Private helpers
181
+ // ---------------------------------------------------------------------------
182
+ async ensureTriggerInstalled(table) {
183
+ if (this.installedTriggers.has(table)) return;
184
+ if (!this.listenClient) return;
185
+ try {
186
+ await this.listenClient.query(
187
+ createTableTriggerSQL(this.schema, this.prefix, table)
188
+ );
189
+ this.installedTriggers.add(table);
190
+ } catch (err) {
191
+ if (err instanceof DatabaseError) {
192
+ throw new ReactiveApiError(
193
+ "POSTGRES_TRIGGER_INSTALL_FAILED",
194
+ `Failed to install trigger on "${table}": ${err.message}`
195
+ );
196
+ }
197
+ throw err;
198
+ }
199
+ }
200
+ handleNotification(rawPayload) {
201
+ let payload;
202
+ try {
203
+ payload = JSON.parse(rawPayload);
204
+ } catch {
205
+ console.error("[RouteFlow/postgres] Malformed NOTIFY payload:", rawPayload);
206
+ return;
207
+ }
208
+ if (!isNotifyPayload(payload)) {
209
+ console.error("[RouteFlow/postgres] Unexpected payload shape:", payload);
210
+ return;
211
+ }
212
+ const event = {
213
+ table: payload.table,
214
+ operation: payload.operation,
215
+ newRow: payload.new_row,
216
+ oldRow: payload.old_row,
217
+ timestamp: Date.now()
218
+ };
219
+ const callbacks = this.listeners.get(payload.table);
220
+ if (!callbacks) return;
221
+ for (const cb of callbacks) {
222
+ try {
223
+ cb(event);
224
+ } catch (err) {
225
+ console.error(
226
+ `[RouteFlow/postgres] Listener error on table "${payload.table}":`,
227
+ errorMessage(err)
228
+ );
229
+ }
230
+ }
231
+ }
232
+ };
233
+ function isNotifyPayload(value) {
234
+ if (typeof value !== "object" || value === null) return false;
235
+ const v = value;
236
+ return typeof v["table"] === "string" && (v["operation"] === "INSERT" || v["operation"] === "UPDATE" || v["operation"] === "DELETE");
237
+ }
238
+ function errorMessage(err) {
239
+ return err instanceof Error ? err.message : String(err);
240
+ }
241
+ export {
242
+ PostgresAdapter
243
+ };
244
+ //# sourceMappingURL=postgres.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/postgres/postgres-adapter.ts","../../src/core/errors.ts","../../src/adapters/postgres/trigger-sql.ts"],"sourcesContent":["import { Client, DatabaseError } from 'pg'\nimport type { ChangeEvent, DatabaseAdapter } from '../../core/types.js'\nimport { ReactiveApiError } from '../../core/errors.js'\nimport type { NotifyPayload, PostgresAdapterOptions } from './types.js'\nimport {\n notifyChannel,\n createTriggerFunctionSQL,\n createTableTriggerSQL,\n dropTableTriggerSQL,\n dropTriggerFunctionSQL,\n} from './trigger-sql.js'\n\n/**\n * PostgreSQL database adapter for RouteFlow.\n *\n * Uses a dedicated `pg.Client` (not a pool) for LISTEN so the connection\n * stays open and notifications are never missed. A separate pool/client\n * can still be used by the application for regular queries.\n *\n * Change detection mechanism:\n * - On `onChange(table)`: installs a per-table AFTER INSERT/UPDATE/DELETE trigger\n * that calls `pg_notify` with a JSON payload.\n * - A single LISTEN connection subscribes to one shared channel for all tables.\n * - Payloads > 7900 bytes are sent without row data (`_truncated: true`);\n * subscribers receive a ChangeEvent with `newRow: null` / `oldRow: null`.\n *\n * @example\n * ```ts\n * const adapter = new PostgresAdapter({\n * connectionString: process.env.DATABASE_URL,\n * })\n * await adapter.connect()\n * ```\n */\nexport class PostgresAdapter implements DatabaseAdapter {\n private listenClient: Client | null = null\n private readonly schema: string\n private readonly prefix: string\n private readonly connectionString: string\n\n /** table → Set<callback> */\n private readonly listeners: Map<string, Set<(event: ChangeEvent) => void>> = new Map()\n /** Tables for which a trigger has already been installed */\n private readonly installedTriggers: Set<string> = new Set()\n\n constructor(options: PostgresAdapterOptions) {\n this.connectionString = options.connectionString\n this.schema = options.schema ?? 'public'\n this.prefix = options.triggerPrefix ?? 'reactive_api'\n }\n\n // ---------------------------------------------------------------------------\n // DatabaseAdapter interface\n // ---------------------------------------------------------------------------\n\n /** Connect the LISTEN client and install the shared trigger function. */\n async connect(): Promise<void> {\n if (this.listenClient) return\n\n const client = new Client({ connectionString: this.connectionString })\n try {\n await client.connect()\n } catch (err) {\n throw new ReactiveApiError(\n 'POSTGRES_CONNECT_FAILED',\n `Failed to connect to PostgreSQL: ${errorMessage(err)}`,\n )\n }\n\n // Install (or replace) the shared trigger function once per connection\n try {\n await client.query(createTriggerFunctionSQL(this.schema, this.prefix))\n } catch (err) {\n await client.end().catch(() => undefined)\n throw new ReactiveApiError(\n 'POSTGRES_TRIGGER_FUNCTION_FAILED',\n `Failed to install trigger function: ${errorMessage(err)}`,\n )\n }\n\n // Start listening on the shared channel\n const channel = notifyChannel(this.prefix)\n await client.query(`LISTEN \"${channel}\"`)\n\n client.on('notification', (msg) => {\n if (msg.channel !== channel || !msg.payload) return\n this.handleNotification(msg.payload)\n })\n\n client.on('error', (err) => {\n // Log but don't crash — the adapter becomes unavailable; the app can\n // call disconnect() + connect() to recover.\n console.error('[RouteFlow/postgres] LISTEN client error:', err.message)\n })\n\n this.listenClient = client\n }\n\n /** Disconnect the LISTEN client and optionally clean up triggers. */\n async disconnect(): Promise<void> {\n if (!this.listenClient) return\n\n const client = this.listenClient\n this.listenClient = null\n\n // Remove all installed table triggers and the shared function\n for (const table of this.installedTriggers) {\n await client\n .query(dropTableTriggerSQL(this.schema, this.prefix, table))\n .catch(() => undefined)\n }\n this.installedTriggers.clear()\n\n await client\n .query(dropTriggerFunctionSQL(this.schema, this.prefix))\n .catch(() => undefined)\n\n await client.end().catch(() => undefined)\n this.listeners.clear()\n }\n\n /**\n * Register a callback for changes on `table`.\n * Installs a PostgreSQL trigger on first registration for this table.\n *\n * @returns An unsubscribe function.\n */\n onChange(table: string, callback: (event: ChangeEvent) => void): () => void {\n if (!this.listeners.has(table)) {\n this.listeners.set(table, new Set())\n }\n this.listeners.get(table)!.add(callback)\n\n // Install trigger asynchronously; errors are surfaced to the console\n // rather than throwing so registration can happen before connect().\n this.ensureTriggerInstalled(table).catch((err) => {\n console.error(\n `[RouteFlow/postgres] Failed to install trigger for table \"${table}\":`,\n errorMessage(err),\n )\n })\n\n return () => {\n this.listeners.get(table)?.delete(callback)\n }\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private async ensureTriggerInstalled(table: string): Promise<void> {\n if (this.installedTriggers.has(table)) return\n if (!this.listenClient) return // will be retried via connect() flow\n\n try {\n await this.listenClient.query(\n createTableTriggerSQL(this.schema, this.prefix, table),\n )\n this.installedTriggers.add(table)\n } catch (err) {\n if (err instanceof DatabaseError) {\n throw new ReactiveApiError(\n 'POSTGRES_TRIGGER_INSTALL_FAILED',\n `Failed to install trigger on \"${table}\": ${err.message}`,\n )\n }\n throw err\n }\n }\n\n private handleNotification(rawPayload: string): void {\n let payload: unknown\n try {\n payload = JSON.parse(rawPayload)\n } catch {\n console.error('[RouteFlow/postgres] Malformed NOTIFY payload:', rawPayload)\n return\n }\n\n if (!isNotifyPayload(payload)) {\n console.error('[RouteFlow/postgres] Unexpected payload shape:', payload)\n return\n }\n\n const event: ChangeEvent = {\n table: payload.table,\n operation: payload.operation,\n newRow: payload.new_row,\n oldRow: payload.old_row,\n timestamp: Date.now(),\n }\n\n const callbacks = this.listeners.get(payload.table)\n if (!callbacks) return\n\n for (const cb of callbacks) {\n try {\n cb(event)\n } catch (err) {\n console.error(\n `[RouteFlow/postgres] Listener error on table \"${payload.table}\":`,\n errorMessage(err),\n )\n }\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Type guards / utilities\n// ---------------------------------------------------------------------------\n\nfunction isNotifyPayload(value: unknown): value is NotifyPayload {\n if (typeof value !== 'object' || value === null) return false\n const v = value as Record<string, unknown>\n return (\n typeof v['table'] === 'string' &&\n (v['operation'] === 'INSERT' || v['operation'] === 'UPDATE' || v['operation'] === 'DELETE')\n )\n}\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n","/**\n * Base error class for all RouteFlow errors.\n * Always use this instead of plain `Error` throughout the framework.\n */\nexport class ReactiveApiError extends Error {\n /** Machine-readable error code (e.g. 'ADAPTER_NOT_CONNECTED', 'INVALID_ROUTE') */\n readonly code: string\n\n constructor(code: string, message: string) {\n super(message)\n this.name = 'ReactiveApiError'\n this.code = code\n // Restore prototype chain (required when extending built-ins in TS)\n Object.setPrototypeOf(this, new.target.prototype)\n }\n}\n","/**\n * Generates the SQL that installs a per-table NOTIFY trigger.\n *\n * Strategy:\n * - One shared trigger function per (schema, prefix) pair — handles all tables.\n * - One trigger per table that calls that function.\n *\n * Payload sent via NOTIFY (channel = triggerPrefix):\n * ```json\n * { \"table\": \"orders\", \"operation\": \"INSERT\", \"new_row\": {...}, \"old_row\": null }\n * ```\n *\n * IMPORTANT: PostgreSQL NOTIFY payloads are limited to 8000 bytes.\n * For larger rows the payload is truncated and only the primary-key columns\n * are guaranteed to be present. Callers that need the full row should\n * re-query after receiving the event.\n */\n\n/** Channel name used for LISTEN/NOTIFY — one shared channel for all tables. */\nexport function notifyChannel(prefix: string): string {\n return `${prefix}_changes`\n}\n\n/**\n * SQL to create (or replace) the shared trigger function.\n * The function encodes NEW/OLD as JSON and notifies the channel.\n */\nexport function createTriggerFunctionSQL(schema: string, prefix: string): string {\n const fnName = `${schema}.${prefix}_notify_changes`\n const channel = notifyChannel(prefix)\n\n return `\nCREATE OR REPLACE FUNCTION ${fnName}()\nRETURNS trigger\nLANGUAGE plpgsql\nAS $$\nDECLARE\n payload TEXT;\n new_row JSON;\n old_row JSON;\nBEGIN\n IF (TG_OP = 'DELETE') THEN\n new_row := NULL;\n old_row := row_to_json(OLD);\n ELSIF (TG_OP = 'INSERT') THEN\n new_row := row_to_json(NEW);\n old_row := NULL;\n ELSE\n new_row := row_to_json(NEW);\n old_row := row_to_json(OLD);\n END IF;\n\n payload := json_build_object(\n 'table', TG_TABLE_NAME,\n 'operation', TG_OP,\n 'new_row', new_row,\n 'old_row', old_row\n )::text;\n\n -- Truncate gracefully if payload exceeds pg's 8000-byte NOTIFY limit\n IF octet_length(payload) > 7900 THEN\n payload := json_build_object(\n 'table', TG_TABLE_NAME,\n 'operation', TG_OP,\n 'new_row', NULL,\n 'old_row', NULL,\n '_truncated', true\n )::text;\n END IF;\n\n PERFORM pg_notify('${channel}', payload);\n RETURN COALESCE(NEW, OLD);\nEND;\n$$;\n`.trim()\n}\n\n/**\n * SQL to attach the trigger to a specific table.\n * Idempotent — uses CREATE OR REPLACE (Postgres 14+).\n * Falls back to DROP + CREATE for older versions.\n */\nexport function createTableTriggerSQL(\n schema: string,\n prefix: string,\n table: string,\n): string {\n const triggerName = `${prefix}_notify_${table}`\n const fnName = `${schema}.${prefix}_notify_changes`\n\n return `\nDO $$\nBEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_trigger\n WHERE tgname = '${triggerName}'\n AND tgrelid = '${schema}.${table}'::regclass\n ) THEN\n CREATE TRIGGER ${triggerName}\n AFTER INSERT OR UPDATE OR DELETE ON ${schema}.${table}\n FOR EACH ROW EXECUTE FUNCTION ${fnName}();\n END IF;\nEND;\n$$;\n`.trim()\n}\n\n/**\n * SQL to drop the trigger from a table (used on disconnect/cleanup).\n */\nexport function dropTableTriggerSQL(\n schema: string,\n prefix: string,\n table: string,\n): string {\n const triggerName = `${prefix}_notify_${table}`\n return `DROP TRIGGER IF EXISTS ${triggerName} ON ${schema}.${table};`\n}\n\n/**\n * SQL to drop the shared trigger function (used on full adapter teardown).\n */\nexport function dropTriggerFunctionSQL(schema: string, prefix: string): string {\n return `DROP FUNCTION IF EXISTS ${schema}.${prefix}_notify_changes();`\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/adapters/redis.ts
21
+ var redis_exports = {};
22
+ __export(redis_exports, {
23
+ RedisAdapter: () => RedisAdapter
24
+ });
25
+ module.exports = __toCommonJS(redis_exports);
26
+
27
+ // src/core/errors.ts
28
+ var ReactiveApiError = class extends Error {
29
+ /** Machine-readable error code (e.g. 'ADAPTER_NOT_CONNECTED', 'INVALID_ROUTE') */
30
+ code;
31
+ constructor(code, message) {
32
+ super(message);
33
+ this.name = "ReactiveApiError";
34
+ this.code = code;
35
+ Object.setPrototypeOf(this, new.target.prototype);
36
+ }
37
+ };
38
+
39
+ // src/adapters/redis/redis-adapter.ts
40
+ var RedisAdapter = class {
41
+ subscriber;
42
+ channelPrefix;
43
+ onError;
44
+ listeners = /* @__PURE__ */ new Map();
45
+ handleMessageBound = (channel, payload) => {
46
+ this.handleMessage(channel, payload);
47
+ };
48
+ handleErrorBound = (error) => {
49
+ this.onError?.(error);
50
+ };
51
+ connected = false;
52
+ constructor(options) {
53
+ this.subscriber = options.subscriber;
54
+ this.channelPrefix = options.channelPrefix ?? "flux";
55
+ this.onError = options.onError;
56
+ }
57
+ async connect() {
58
+ if (this.connected) return;
59
+ this.subscriber.on("message", this.handleMessageBound);
60
+ this.subscriber.on("error", this.handleErrorBound);
61
+ for (const table of this.listeners.keys()) {
62
+ await this.subscriber.subscribe(channelName(this.channelPrefix, table));
63
+ }
64
+ this.connected = true;
65
+ }
66
+ async disconnect() {
67
+ if (!this.connected) return;
68
+ removeListener(this.subscriber, "message", this.handleMessageBound);
69
+ removeListener(this.subscriber, "error", this.handleErrorBound);
70
+ for (const table of this.listeners.keys()) {
71
+ await this.subscriber.unsubscribe(channelName(this.channelPrefix, table));
72
+ }
73
+ await this.subscriber.quit?.();
74
+ await this.subscriber.disconnect?.();
75
+ this.listeners.clear();
76
+ this.connected = false;
77
+ }
78
+ onChange(table, callback) {
79
+ const hadTable = this.listeners.has(table);
80
+ if (!hadTable) {
81
+ this.listeners.set(table, /* @__PURE__ */ new Set());
82
+ }
83
+ this.listeners.get(table).add(callback);
84
+ if (!hadTable && this.connected) {
85
+ void this.subscriber.subscribe(channelName(this.channelPrefix, table));
86
+ }
87
+ return () => {
88
+ const callbacks = this.listeners.get(table);
89
+ if (!callbacks) return;
90
+ callbacks.delete(callback);
91
+ if (callbacks.size === 0) {
92
+ this.listeners.delete(table);
93
+ if (this.connected) {
94
+ void this.subscriber.unsubscribe(channelName(this.channelPrefix, table));
95
+ }
96
+ }
97
+ };
98
+ }
99
+ handleMessage(channel, payload) {
100
+ const table = parseChannelName(this.channelPrefix, channel);
101
+ if (!table) return;
102
+ let data;
103
+ try {
104
+ data = JSON.parse(payload);
105
+ } catch (error) {
106
+ throw new ReactiveApiError(
107
+ "REDIS_PAYLOAD_INVALID",
108
+ `Failed to parse Redis payload for "${channel}": ${errorMessage(error)}`
109
+ );
110
+ }
111
+ if (!isRedisChangePayload(data)) return;
112
+ const callbacks = this.listeners.get(data.table);
113
+ if (!callbacks) return;
114
+ const event = {
115
+ table: data.table,
116
+ operation: data.operation,
117
+ newRow: data.newRow,
118
+ oldRow: data.oldRow,
119
+ timestamp: data.timestamp ?? Date.now()
120
+ };
121
+ for (const callback of callbacks) {
122
+ callback(event);
123
+ }
124
+ }
125
+ };
126
+ function channelName(prefix, table) {
127
+ return `${prefix}:${table}`;
128
+ }
129
+ function parseChannelName(prefix, channel) {
130
+ const expectedPrefix = `${prefix}:`;
131
+ if (!channel.startsWith(expectedPrefix)) return null;
132
+ return channel.slice(expectedPrefix.length);
133
+ }
134
+ function removeListener(subscriber, event, listener) {
135
+ if (subscriber.off) {
136
+ subscriber.off(event, listener);
137
+ return;
138
+ }
139
+ subscriber.removeListener?.(event, listener);
140
+ }
141
+ function isRedisChangePayload(value) {
142
+ if (typeof value !== "object" || value === null) return false;
143
+ const record = value;
144
+ return typeof record["table"] === "string" && (record["operation"] === "INSERT" || record["operation"] === "UPDATE" || record["operation"] === "DELETE");
145
+ }
146
+ function errorMessage(error) {
147
+ return error instanceof Error ? error.message : String(error);
148
+ }
149
+ // Annotate the CommonJS export names for ESM import in node:
150
+ 0 && (module.exports = {
151
+ RedisAdapter
152
+ });
153
+ //# sourceMappingURL=redis.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/redis.ts","../../src/core/errors.ts","../../src/adapters/redis/redis-adapter.ts"],"sourcesContent":["export { RedisAdapter } from './redis/redis-adapter.js'\nexport type { RedisAdapterOptions } from './redis/types.js'\n","/**\n * Base error class for all RouteFlow errors.\n * Always use this instead of plain `Error` throughout the framework.\n */\nexport class ReactiveApiError extends Error {\n /** Machine-readable error code (e.g. 'ADAPTER_NOT_CONNECTED', 'INVALID_ROUTE') */\n readonly code: string\n\n constructor(code: string, message: string) {\n super(message)\n this.name = 'ReactiveApiError'\n this.code = code\n // Restore prototype chain (required when extending built-ins in TS)\n Object.setPrototypeOf(this, new.target.prototype)\n }\n}\n","import type { ChangeEvent, DatabaseAdapter } from '../../core/types.js'\nimport { ReactiveApiError } from '../../core/errors.js'\nimport type { RedisAdapterOptions, RedisChangePayload, RedisSubscriber } from './types.js'\n\n/**\n * Redis adapter for RouteFlow.\n *\n * Each watched table maps to one Redis pub/sub channel. Publishers are expected\n * to publish JSON payloads in the RouteFlow change-event shape.\n */\nexport class RedisAdapter implements DatabaseAdapter {\n private readonly subscriber: RedisSubscriber\n private readonly channelPrefix: string\n private readonly onError?: RedisAdapterOptions['onError']\n private readonly listeners: Map<string, Set<(event: ChangeEvent) => void>> = new Map()\n private readonly handleMessageBound = (channel: string, payload: string) => {\n this.handleMessage(channel, payload)\n }\n private readonly handleErrorBound = (error: Error) => {\n this.onError?.(error)\n }\n private connected = false\n\n constructor(options: RedisAdapterOptions) {\n this.subscriber = options.subscriber\n this.channelPrefix = options.channelPrefix ?? 'flux'\n this.onError = options.onError\n }\n\n async connect(): Promise<void> {\n if (this.connected) return\n\n this.subscriber.on('message', this.handleMessageBound)\n this.subscriber.on('error', this.handleErrorBound)\n\n for (const table of this.listeners.keys()) {\n await this.subscriber.subscribe(channelName(this.channelPrefix, table))\n }\n\n this.connected = true\n }\n\n async disconnect(): Promise<void> {\n if (!this.connected) return\n\n removeListener(this.subscriber, 'message', this.handleMessageBound)\n removeListener(this.subscriber, 'error', this.handleErrorBound)\n\n for (const table of this.listeners.keys()) {\n await this.subscriber.unsubscribe(channelName(this.channelPrefix, table))\n }\n\n await this.subscriber.quit?.()\n await this.subscriber.disconnect?.()\n this.listeners.clear()\n this.connected = false\n }\n\n onChange(table: string, callback: (event: ChangeEvent) => void): () => void {\n const hadTable = this.listeners.has(table)\n if (!hadTable) {\n this.listeners.set(table, new Set())\n }\n\n this.listeners.get(table)!.add(callback)\n\n if (!hadTable && this.connected) {\n void this.subscriber.subscribe(channelName(this.channelPrefix, table))\n }\n\n return () => {\n const callbacks = this.listeners.get(table)\n if (!callbacks) return\n\n callbacks.delete(callback)\n if (callbacks.size === 0) {\n this.listeners.delete(table)\n if (this.connected) {\n void this.subscriber.unsubscribe(channelName(this.channelPrefix, table))\n }\n }\n }\n }\n\n private handleMessage(channel: string, payload: string): void {\n const table = parseChannelName(this.channelPrefix, channel)\n if (!table) return\n\n let data: unknown\n try {\n data = JSON.parse(payload)\n } catch (error) {\n throw new ReactiveApiError(\n 'REDIS_PAYLOAD_INVALID',\n `Failed to parse Redis payload for \"${channel}\": ${errorMessage(error)}`,\n )\n }\n\n if (!isRedisChangePayload(data)) return\n\n const callbacks = this.listeners.get(data.table)\n if (!callbacks) return\n\n const event: ChangeEvent = {\n table: data.table,\n operation: data.operation,\n newRow: data.newRow,\n oldRow: data.oldRow,\n timestamp: data.timestamp ?? Date.now(),\n }\n\n for (const callback of callbacks) {\n callback(event)\n }\n }\n}\n\nfunction channelName(prefix: string, table: string): string {\n return `${prefix}:${table}`\n}\n\nfunction parseChannelName(prefix: string, channel: string): string | null {\n const expectedPrefix = `${prefix}:`\n if (!channel.startsWith(expectedPrefix)) return null\n return channel.slice(expectedPrefix.length)\n}\n\nfunction removeListener(\n subscriber: RedisSubscriber,\n event: 'message' | 'error',\n listener: (...args: any[]) => void,\n): void {\n if (subscriber.off) {\n subscriber.off(event, listener)\n return\n }\n\n subscriber.removeListener?.(event, listener)\n}\n\nfunction isRedisChangePayload(value: unknown): value is RedisChangePayload {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Record<string, unknown>\n return (\n typeof record['table'] === 'string' &&\n (record['operation'] === 'INSERT' ||\n record['operation'] === 'UPDATE' ||\n record['operation'] === 'DELETE')\n )\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,40 @@
1
+ import { D as DatabaseAdapter, C as ChangeEvent } from '../types-tPDla8AE.cjs';
2
+
3
+ interface RedisSubscriber {
4
+ subscribe(channel: string): Promise<void> | void;
5
+ unsubscribe(channel: string): Promise<void> | void;
6
+ on(event: 'message', listener: (channel: string, payload: string) => void): void;
7
+ on(event: 'error', listener: (error: Error) => void): void;
8
+ off?(event: 'message' | 'error', listener: (...args: any[]) => void): void;
9
+ removeListener?(event: 'message' | 'error', listener: (...args: any[]) => void): void;
10
+ quit?(): Promise<void> | void;
11
+ disconnect?(): Promise<void> | void;
12
+ }
13
+ interface RedisAdapterOptions {
14
+ subscriber: RedisSubscriber;
15
+ channelPrefix?: string;
16
+ onError?: (error: unknown) => void;
17
+ }
18
+
19
+ /**
20
+ * Redis adapter for RouteFlow.
21
+ *
22
+ * Each watched table maps to one Redis pub/sub channel. Publishers are expected
23
+ * to publish JSON payloads in the RouteFlow change-event shape.
24
+ */
25
+ declare class RedisAdapter implements DatabaseAdapter {
26
+ private readonly subscriber;
27
+ private readonly channelPrefix;
28
+ private readonly onError?;
29
+ private readonly listeners;
30
+ private readonly handleMessageBound;
31
+ private readonly handleErrorBound;
32
+ private connected;
33
+ constructor(options: RedisAdapterOptions);
34
+ connect(): Promise<void>;
35
+ disconnect(): Promise<void>;
36
+ onChange(table: string, callback: (event: ChangeEvent) => void): () => void;
37
+ private handleMessage;
38
+ }
39
+
40
+ export { RedisAdapter, type RedisAdapterOptions };
@@ -0,0 +1,40 @@
1
+ import { D as DatabaseAdapter, C as ChangeEvent } from '../types-tPDla8AE.js';
2
+
3
+ interface RedisSubscriber {
4
+ subscribe(channel: string): Promise<void> | void;
5
+ unsubscribe(channel: string): Promise<void> | void;
6
+ on(event: 'message', listener: (channel: string, payload: string) => void): void;
7
+ on(event: 'error', listener: (error: Error) => void): void;
8
+ off?(event: 'message' | 'error', listener: (...args: any[]) => void): void;
9
+ removeListener?(event: 'message' | 'error', listener: (...args: any[]) => void): void;
10
+ quit?(): Promise<void> | void;
11
+ disconnect?(): Promise<void> | void;
12
+ }
13
+ interface RedisAdapterOptions {
14
+ subscriber: RedisSubscriber;
15
+ channelPrefix?: string;
16
+ onError?: (error: unknown) => void;
17
+ }
18
+
19
+ /**
20
+ * Redis adapter for RouteFlow.
21
+ *
22
+ * Each watched table maps to one Redis pub/sub channel. Publishers are expected
23
+ * to publish JSON payloads in the RouteFlow change-event shape.
24
+ */
25
+ declare class RedisAdapter implements DatabaseAdapter {
26
+ private readonly subscriber;
27
+ private readonly channelPrefix;
28
+ private readonly onError?;
29
+ private readonly listeners;
30
+ private readonly handleMessageBound;
31
+ private readonly handleErrorBound;
32
+ private connected;
33
+ constructor(options: RedisAdapterOptions);
34
+ connect(): Promise<void>;
35
+ disconnect(): Promise<void>;
36
+ onChange(table: string, callback: (event: ChangeEvent) => void): () => void;
37
+ private handleMessage;
38
+ }
39
+
40
+ export { RedisAdapter, type RedisAdapterOptions };
@@ -0,0 +1,126 @@
1
+ // src/core/errors.ts
2
+ var ReactiveApiError = class extends Error {
3
+ /** Machine-readable error code (e.g. 'ADAPTER_NOT_CONNECTED', 'INVALID_ROUTE') */
4
+ code;
5
+ constructor(code, message) {
6
+ super(message);
7
+ this.name = "ReactiveApiError";
8
+ this.code = code;
9
+ Object.setPrototypeOf(this, new.target.prototype);
10
+ }
11
+ };
12
+
13
+ // src/adapters/redis/redis-adapter.ts
14
+ var RedisAdapter = class {
15
+ subscriber;
16
+ channelPrefix;
17
+ onError;
18
+ listeners = /* @__PURE__ */ new Map();
19
+ handleMessageBound = (channel, payload) => {
20
+ this.handleMessage(channel, payload);
21
+ };
22
+ handleErrorBound = (error) => {
23
+ this.onError?.(error);
24
+ };
25
+ connected = false;
26
+ constructor(options) {
27
+ this.subscriber = options.subscriber;
28
+ this.channelPrefix = options.channelPrefix ?? "flux";
29
+ this.onError = options.onError;
30
+ }
31
+ async connect() {
32
+ if (this.connected) return;
33
+ this.subscriber.on("message", this.handleMessageBound);
34
+ this.subscriber.on("error", this.handleErrorBound);
35
+ for (const table of this.listeners.keys()) {
36
+ await this.subscriber.subscribe(channelName(this.channelPrefix, table));
37
+ }
38
+ this.connected = true;
39
+ }
40
+ async disconnect() {
41
+ if (!this.connected) return;
42
+ removeListener(this.subscriber, "message", this.handleMessageBound);
43
+ removeListener(this.subscriber, "error", this.handleErrorBound);
44
+ for (const table of this.listeners.keys()) {
45
+ await this.subscriber.unsubscribe(channelName(this.channelPrefix, table));
46
+ }
47
+ await this.subscriber.quit?.();
48
+ await this.subscriber.disconnect?.();
49
+ this.listeners.clear();
50
+ this.connected = false;
51
+ }
52
+ onChange(table, callback) {
53
+ const hadTable = this.listeners.has(table);
54
+ if (!hadTable) {
55
+ this.listeners.set(table, /* @__PURE__ */ new Set());
56
+ }
57
+ this.listeners.get(table).add(callback);
58
+ if (!hadTable && this.connected) {
59
+ void this.subscriber.subscribe(channelName(this.channelPrefix, table));
60
+ }
61
+ return () => {
62
+ const callbacks = this.listeners.get(table);
63
+ if (!callbacks) return;
64
+ callbacks.delete(callback);
65
+ if (callbacks.size === 0) {
66
+ this.listeners.delete(table);
67
+ if (this.connected) {
68
+ void this.subscriber.unsubscribe(channelName(this.channelPrefix, table));
69
+ }
70
+ }
71
+ };
72
+ }
73
+ handleMessage(channel, payload) {
74
+ const table = parseChannelName(this.channelPrefix, channel);
75
+ if (!table) return;
76
+ let data;
77
+ try {
78
+ data = JSON.parse(payload);
79
+ } catch (error) {
80
+ throw new ReactiveApiError(
81
+ "REDIS_PAYLOAD_INVALID",
82
+ `Failed to parse Redis payload for "${channel}": ${errorMessage(error)}`
83
+ );
84
+ }
85
+ if (!isRedisChangePayload(data)) return;
86
+ const callbacks = this.listeners.get(data.table);
87
+ if (!callbacks) return;
88
+ const event = {
89
+ table: data.table,
90
+ operation: data.operation,
91
+ newRow: data.newRow,
92
+ oldRow: data.oldRow,
93
+ timestamp: data.timestamp ?? Date.now()
94
+ };
95
+ for (const callback of callbacks) {
96
+ callback(event);
97
+ }
98
+ }
99
+ };
100
+ function channelName(prefix, table) {
101
+ return `${prefix}:${table}`;
102
+ }
103
+ function parseChannelName(prefix, channel) {
104
+ const expectedPrefix = `${prefix}:`;
105
+ if (!channel.startsWith(expectedPrefix)) return null;
106
+ return channel.slice(expectedPrefix.length);
107
+ }
108
+ function removeListener(subscriber, event, listener) {
109
+ if (subscriber.off) {
110
+ subscriber.off(event, listener);
111
+ return;
112
+ }
113
+ subscriber.removeListener?.(event, listener);
114
+ }
115
+ function isRedisChangePayload(value) {
116
+ if (typeof value !== "object" || value === null) return false;
117
+ const record = value;
118
+ return typeof record["table"] === "string" && (record["operation"] === "INSERT" || record["operation"] === "UPDATE" || record["operation"] === "DELETE");
119
+ }
120
+ function errorMessage(error) {
121
+ return error instanceof Error ? error.message : String(error);
122
+ }
123
+ export {
124
+ RedisAdapter
125
+ };
126
+ //# sourceMappingURL=redis.js.map