abxbus 2.4.1

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 (137) hide show
  1. package/README.md +647 -0
  2. package/dist/cjs/async_context.d.ts +12 -0
  3. package/dist/cjs/async_context.js +70 -0
  4. package/dist/cjs/async_context.js.map +7 -0
  5. package/dist/cjs/base_event.d.ts +207 -0
  6. package/dist/cjs/base_event.js +871 -0
  7. package/dist/cjs/base_event.js.map +7 -0
  8. package/dist/cjs/bridge_jsonl.d.ts +26 -0
  9. package/dist/cjs/bridge_jsonl.js +170 -0
  10. package/dist/cjs/bridge_jsonl.js.map +7 -0
  11. package/dist/cjs/bridge_nats.d.ts +20 -0
  12. package/dist/cjs/bridge_nats.js +108 -0
  13. package/dist/cjs/bridge_nats.js.map +7 -0
  14. package/dist/cjs/bridge_postgres.d.ts +31 -0
  15. package/dist/cjs/bridge_postgres.js +251 -0
  16. package/dist/cjs/bridge_postgres.js.map +7 -0
  17. package/dist/cjs/bridge_redis.d.ts +34 -0
  18. package/dist/cjs/bridge_redis.js +175 -0
  19. package/dist/cjs/bridge_redis.js.map +7 -0
  20. package/dist/cjs/bridge_sqlite.d.ts +30 -0
  21. package/dist/cjs/bridge_sqlite.js +255 -0
  22. package/dist/cjs/bridge_sqlite.js.map +7 -0
  23. package/dist/cjs/bridges.d.ts +49 -0
  24. package/dist/cjs/bridges.js +326 -0
  25. package/dist/cjs/bridges.js.map +7 -0
  26. package/dist/cjs/event_bus.d.ts +127 -0
  27. package/dist/cjs/event_bus.js +1058 -0
  28. package/dist/cjs/event_bus.js.map +7 -0
  29. package/dist/cjs/event_handler.d.ts +139 -0
  30. package/dist/cjs/event_handler.js +299 -0
  31. package/dist/cjs/event_handler.js.map +7 -0
  32. package/dist/cjs/event_history.d.ts +45 -0
  33. package/dist/cjs/event_history.js +192 -0
  34. package/dist/cjs/event_history.js.map +7 -0
  35. package/dist/cjs/event_result.d.ts +86 -0
  36. package/dist/cjs/event_result.js +446 -0
  37. package/dist/cjs/event_result.js.map +7 -0
  38. package/dist/cjs/events_suck.d.ts +40 -0
  39. package/dist/cjs/events_suck.js +59 -0
  40. package/dist/cjs/events_suck.js.map +7 -0
  41. package/dist/cjs/helpers.d.ts +1 -0
  42. package/dist/cjs/helpers.js +84 -0
  43. package/dist/cjs/helpers.js.map +7 -0
  44. package/dist/cjs/index.d.ts +17 -0
  45. package/dist/cjs/index.js +54 -0
  46. package/dist/cjs/index.js.map +7 -0
  47. package/dist/cjs/lock_manager.d.ts +70 -0
  48. package/dist/cjs/lock_manager.js +343 -0
  49. package/dist/cjs/lock_manager.js.map +7 -0
  50. package/dist/cjs/logging.d.ts +16 -0
  51. package/dist/cjs/logging.js +216 -0
  52. package/dist/cjs/logging.js.map +7 -0
  53. package/dist/cjs/middlewares.d.ts +13 -0
  54. package/dist/cjs/middlewares.js +17 -0
  55. package/dist/cjs/middlewares.js.map +7 -0
  56. package/dist/cjs/optional_deps.d.ts +3 -0
  57. package/dist/cjs/optional_deps.js +64 -0
  58. package/dist/cjs/optional_deps.js.map +7 -0
  59. package/dist/cjs/package.json +5 -0
  60. package/dist/cjs/retry.d.ts +52 -0
  61. package/dist/cjs/retry.js +257 -0
  62. package/dist/cjs/retry.js.map +7 -0
  63. package/dist/cjs/timing.d.ts +3 -0
  64. package/dist/cjs/timing.js +76 -0
  65. package/dist/cjs/timing.js.map +7 -0
  66. package/dist/cjs/type_inference.test.d.ts +1 -0
  67. package/dist/cjs/types.d.ts +36 -0
  68. package/dist/cjs/types.js +104 -0
  69. package/dist/cjs/types.js.map +7 -0
  70. package/dist/esm/async_context.js +50 -0
  71. package/dist/esm/async_context.js.map +7 -0
  72. package/dist/esm/base_event.js +857 -0
  73. package/dist/esm/base_event.js.map +7 -0
  74. package/dist/esm/bridge_jsonl.js +150 -0
  75. package/dist/esm/bridge_jsonl.js.map +7 -0
  76. package/dist/esm/bridge_nats.js +88 -0
  77. package/dist/esm/bridge_nats.js.map +7 -0
  78. package/dist/esm/bridge_postgres.js +231 -0
  79. package/dist/esm/bridge_postgres.js.map +7 -0
  80. package/dist/esm/bridge_redis.js +155 -0
  81. package/dist/esm/bridge_redis.js.map +7 -0
  82. package/dist/esm/bridge_sqlite.js +235 -0
  83. package/dist/esm/bridge_sqlite.js.map +7 -0
  84. package/dist/esm/bridges.js +306 -0
  85. package/dist/esm/bridges.js.map +7 -0
  86. package/dist/esm/event_bus.js +1046 -0
  87. package/dist/esm/event_bus.js.map +7 -0
  88. package/dist/esm/event_handler.js +279 -0
  89. package/dist/esm/event_handler.js.map +7 -0
  90. package/dist/esm/event_history.js +172 -0
  91. package/dist/esm/event_history.js.map +7 -0
  92. package/dist/esm/event_result.js +426 -0
  93. package/dist/esm/event_result.js.map +7 -0
  94. package/dist/esm/events_suck.js +39 -0
  95. package/dist/esm/events_suck.js.map +7 -0
  96. package/dist/esm/helpers.js +64 -0
  97. package/dist/esm/helpers.js.map +7 -0
  98. package/dist/esm/index.js +47 -0
  99. package/dist/esm/index.js.map +7 -0
  100. package/dist/esm/lock_manager.js +323 -0
  101. package/dist/esm/lock_manager.js.map +7 -0
  102. package/dist/esm/logging.js +196 -0
  103. package/dist/esm/logging.js.map +7 -0
  104. package/dist/esm/middlewares.js +1 -0
  105. package/dist/esm/middlewares.js.map +7 -0
  106. package/dist/esm/optional_deps.js +44 -0
  107. package/dist/esm/optional_deps.js.map +7 -0
  108. package/dist/esm/retry.js +237 -0
  109. package/dist/esm/retry.js.map +7 -0
  110. package/dist/esm/timing.js +56 -0
  111. package/dist/esm/timing.js.map +7 -0
  112. package/dist/esm/types.js +84 -0
  113. package/dist/esm/types.js.map +7 -0
  114. package/dist/types/async_context.d.ts +12 -0
  115. package/dist/types/base_event.d.ts +207 -0
  116. package/dist/types/bridge_jsonl.d.ts +26 -0
  117. package/dist/types/bridge_nats.d.ts +20 -0
  118. package/dist/types/bridge_postgres.d.ts +31 -0
  119. package/dist/types/bridge_redis.d.ts +34 -0
  120. package/dist/types/bridge_sqlite.d.ts +30 -0
  121. package/dist/types/bridges.d.ts +49 -0
  122. package/dist/types/event_bus.d.ts +127 -0
  123. package/dist/types/event_handler.d.ts +139 -0
  124. package/dist/types/event_history.d.ts +45 -0
  125. package/dist/types/event_result.d.ts +86 -0
  126. package/dist/types/events_suck.d.ts +40 -0
  127. package/dist/types/helpers.d.ts +1 -0
  128. package/dist/types/index.d.ts +17 -0
  129. package/dist/types/lock_manager.d.ts +70 -0
  130. package/dist/types/logging.d.ts +16 -0
  131. package/dist/types/middlewares.d.ts +13 -0
  132. package/dist/types/optional_deps.d.ts +3 -0
  133. package/dist/types/retry.d.ts +52 -0
  134. package/dist/types/timing.d.ts +3 -0
  135. package/dist/types/type_inference.test.d.ts +1 -0
  136. package/dist/types/types.d.ts +36 -0
  137. package/package.json +87 -0
@@ -0,0 +1,231 @@
1
+ import { BaseEvent } from "./base_event.js";
2
+ import { EventBus } from "./event_bus.js";
3
+ import { assertOptionalDependencyAvailable, importOptionalDependency, isNodeRuntime } from "./optional_deps.js";
4
+ const randomSuffix = () => Math.random().toString(36).slice(2, 10);
5
+ const IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
6
+ const DEFAULT_POSTGRES_TABLE = "abxbus_events";
7
+ const DEFAULT_POSTGRES_CHANNEL = "abxbus_events";
8
+ const EVENT_PAYLOAD_COLUMN = "event_payload";
9
+ const validateIdentifier = (value, label) => {
10
+ if (!IDENTIFIER_RE.test(value)) {
11
+ throw new Error(`Invalid ${label}: ${JSON.stringify(value)}. Use only [A-Za-z0-9_] and start with a letter/_`);
12
+ }
13
+ return value;
14
+ };
15
+ const indexName = (table, suffix) => validateIdentifier(`${table}_${suffix}`.slice(0, 63), "index name");
16
+ const parseTableUrl = (table_url) => {
17
+ let parsed;
18
+ try {
19
+ parsed = new URL(table_url);
20
+ } catch {
21
+ throw new Error(
22
+ "PostgresEventBridge URL must include at least database in path, e.g. postgresql://user:pass@host:5432/dbname[/tablename]"
23
+ );
24
+ }
25
+ const segments = parsed.pathname.split("/").filter(Boolean);
26
+ if (segments.length < 1) {
27
+ throw new Error(
28
+ "PostgresEventBridge URL must include at least database in path, e.g. postgresql://user:pass@host:5432/dbname[/tablename]"
29
+ );
30
+ }
31
+ const db_name = segments[0];
32
+ const table = segments.length >= 2 ? validateIdentifier(segments[1], "table name") : DEFAULT_POSTGRES_TABLE;
33
+ const dsn_url = new URL(parsed.toString());
34
+ dsn_url.pathname = `/${db_name}`;
35
+ return { dsn: dsn_url.toString(), table };
36
+ };
37
+ const splitBridgePayload = (payload) => {
38
+ const event_fields = {};
39
+ const event_payload = { ...payload };
40
+ for (const [key, value] of Object.entries(payload)) {
41
+ if (key.startsWith("event_")) {
42
+ event_fields[key] = value;
43
+ }
44
+ }
45
+ return { event_fields, event_payload };
46
+ };
47
+ class PostgresEventBridge {
48
+ table_url;
49
+ dsn;
50
+ table;
51
+ channel;
52
+ name;
53
+ inbound_bus;
54
+ running;
55
+ client;
56
+ table_columns;
57
+ notification_handler;
58
+ constructor(table_url, channel, name) {
59
+ assertOptionalDependencyAvailable("PostgresEventBridge", "pg");
60
+ const parsed = parseTableUrl(table_url);
61
+ this.table_url = table_url;
62
+ this.dsn = parsed.dsn;
63
+ this.table = parsed.table;
64
+ const derived_channel = channel ?? DEFAULT_POSTGRES_CHANNEL;
65
+ this.channel = validateIdentifier(derived_channel.slice(0, 63), "channel name");
66
+ this.name = name ?? `PostgresEventBridge_${randomSuffix()}`;
67
+ this.inbound_bus = new EventBus(this.name, { max_history_size: 0 });
68
+ this.running = false;
69
+ this.client = null;
70
+ this.table_columns = /* @__PURE__ */ new Set(["event_id", "event_created_at", "event_type", EVENT_PAYLOAD_COLUMN]);
71
+ this.notification_handler = null;
72
+ this.dispatch = this.dispatch.bind(this);
73
+ this.emit = this.emit.bind(this);
74
+ this.on = this.on.bind(this);
75
+ }
76
+ on(event_pattern, handler) {
77
+ this.ensureStarted();
78
+ if (typeof event_pattern === "string") {
79
+ this.inbound_bus.on(event_pattern, handler);
80
+ return;
81
+ }
82
+ this.inbound_bus.on(event_pattern, handler);
83
+ }
84
+ async emit(event) {
85
+ this.ensureStarted();
86
+ if (!this.client) await this.start();
87
+ const payload = event.toJSON();
88
+ const { event_fields, event_payload } = splitBridgePayload(payload);
89
+ const write_payload = { ...event_fields, [EVENT_PAYLOAD_COLUMN]: event_payload };
90
+ const keys = Object.keys(write_payload).sort();
91
+ await this.ensureColumns(keys);
92
+ const columns_sql = keys.map((key) => `"${key}"`).join(", ");
93
+ const placeholders_sql = keys.map((_, index) => `$${index + 1}`).join(", ");
94
+ const values = keys.map(
95
+ (key) => write_payload[key] === null || write_payload[key] === void 0 ? null : JSON.stringify(write_payload[key])
96
+ );
97
+ const update_fields = keys.filter((key) => key !== "event_id");
98
+ let upsert_sql = `INSERT INTO "${this.table}" (${columns_sql}) VALUES (${placeholders_sql})`;
99
+ if (update_fields.length > 0) {
100
+ const updates_sql = update_fields.map((key) => `"${key}" = EXCLUDED."${key}"`).join(", ");
101
+ upsert_sql += ` ON CONFLICT ("event_id") DO UPDATE SET ${updates_sql}`;
102
+ } else {
103
+ upsert_sql += ' ON CONFLICT ("event_id") DO NOTHING';
104
+ }
105
+ await this.client.query(upsert_sql, values);
106
+ await this.client.query("SELECT pg_notify($1, $2)", [this.channel, JSON.stringify(String(event.event_id))]);
107
+ }
108
+ async dispatch(event) {
109
+ return this.emit(event);
110
+ }
111
+ async start() {
112
+ if (this.running) return;
113
+ if (!isNodeRuntime()) {
114
+ throw new Error("PostgresEventBridge is only supported in Node.js runtimes");
115
+ }
116
+ const mod = await importOptionalDependency("PostgresEventBridge", "pg");
117
+ const Client = mod.Client ?? mod.default?.Client;
118
+ this.client = new Client({ connectionString: this.dsn });
119
+ this.client.on("error", () => {
120
+ });
121
+ await this.client.connect();
122
+ await this.ensureTableExists();
123
+ await this.refreshColumnCache();
124
+ await this.ensureColumns(["event_id", "event_created_at", "event_type", EVENT_PAYLOAD_COLUMN]);
125
+ await this.ensureBaseIndexes();
126
+ this.notification_handler = (msg) => {
127
+ if (msg.channel !== this.channel || !msg.payload) return;
128
+ void this.dispatchByEventId(msg.payload).catch(() => {
129
+ });
130
+ };
131
+ this.client.on("notification", this.notification_handler);
132
+ await this.client.query(`LISTEN ${this.channel}`);
133
+ this.running = true;
134
+ }
135
+ async close() {
136
+ this.running = false;
137
+ if (this.client) {
138
+ try {
139
+ await this.client.query(`UNLISTEN ${this.channel}`);
140
+ } catch {
141
+ }
142
+ if (this.notification_handler) {
143
+ this.client.off("notification", this.notification_handler);
144
+ this.notification_handler = null;
145
+ }
146
+ await this.client.end();
147
+ this.client = null;
148
+ }
149
+ this.inbound_bus.destroy();
150
+ }
151
+ ensureStarted() {
152
+ if (this.running) return;
153
+ void this.start().catch((error) => {
154
+ console.error("[abxbus] PostgresEventBridge failed to start", error);
155
+ });
156
+ }
157
+ async dispatchByEventId(event_id) {
158
+ if (!this.running || !this.client) return;
159
+ const result = await this.client.query(`SELECT * FROM "${this.table}" WHERE "event_id" = $1`, [event_id]);
160
+ const row = result.rows?.[0];
161
+ if (!row) return;
162
+ const payload = {};
163
+ const raw_event_payload = row[EVENT_PAYLOAD_COLUMN];
164
+ if (typeof raw_event_payload === "string") {
165
+ try {
166
+ const decoded_event_payload = JSON.parse(raw_event_payload);
167
+ if (decoded_event_payload && typeof decoded_event_payload === "object" && !Array.isArray(decoded_event_payload)) {
168
+ Object.assign(payload, decoded_event_payload);
169
+ }
170
+ } catch {
171
+ }
172
+ }
173
+ for (const [key, raw_value] of Object.entries(row)) {
174
+ if (key === EVENT_PAYLOAD_COLUMN || !key.startsWith("event_")) continue;
175
+ if (raw_value === null || raw_value === void 0) continue;
176
+ if (typeof raw_value !== "string") {
177
+ payload[key] = raw_value;
178
+ continue;
179
+ }
180
+ try {
181
+ payload[key] = JSON.parse(raw_value);
182
+ } catch {
183
+ payload[key] = raw_value;
184
+ }
185
+ }
186
+ await this.dispatchInboundPayload(payload);
187
+ }
188
+ async dispatchInboundPayload(payload) {
189
+ const event = BaseEvent.fromJSON(payload).eventReset();
190
+ this.inbound_bus.emit(event);
191
+ }
192
+ async ensureTableExists() {
193
+ if (!this.client) return;
194
+ await this.client.query(
195
+ `CREATE TABLE IF NOT EXISTS "${this.table}" ("event_id" TEXT PRIMARY KEY, "event_created_at" TEXT, "event_type" TEXT, "event_payload" TEXT)`
196
+ );
197
+ }
198
+ async ensureBaseIndexes() {
199
+ if (!this.client) return;
200
+ const event_created_at_idx = indexName(this.table, "event_created_at_idx");
201
+ const event_type_idx = indexName(this.table, "event_type_idx");
202
+ await this.client.query(`CREATE INDEX IF NOT EXISTS "${event_created_at_idx}" ON "${this.table}" ("event_created_at")`);
203
+ await this.client.query(`CREATE INDEX IF NOT EXISTS "${event_type_idx}" ON "${this.table}" ("event_type")`);
204
+ }
205
+ async refreshColumnCache() {
206
+ if (!this.client) return;
207
+ const result = await this.client.query(
208
+ `SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1`,
209
+ [this.table]
210
+ );
211
+ this.table_columns = new Set(result.rows.map((row) => row.column_name));
212
+ }
213
+ async ensureColumns(keys) {
214
+ if (!this.client) return;
215
+ for (const key of keys) {
216
+ validateIdentifier(key, "event field name");
217
+ if (key !== EVENT_PAYLOAD_COLUMN && !key.startsWith("event_")) {
218
+ throw new Error(`Invalid event field name for bridge column: ${JSON.stringify(key)}. Only event_* fields become columns`);
219
+ }
220
+ }
221
+ const missing = keys.filter((key) => !this.table_columns.has(key));
222
+ for (const key of missing) {
223
+ await this.client.query(`ALTER TABLE "${this.table}" ADD COLUMN IF NOT EXISTS "${key}" TEXT`);
224
+ this.table_columns.add(key);
225
+ }
226
+ }
227
+ }
228
+ export {
229
+ PostgresEventBridge
230
+ };
231
+ //# sourceMappingURL=bridge_postgres.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/bridge_postgres.ts"],
4
+ "sourcesContent": ["/**\n * PostgreSQL LISTEN/NOTIFY + flat-table bridge for forwarding events.\n */\nimport { BaseEvent } from './base_event.js'\nimport { EventBus } from './event_bus.js'\nimport { assertOptionalDependencyAvailable, importOptionalDependency, isNodeRuntime } from './optional_deps.js'\nimport type { EventClass, EventHandlerCallable, EventPattern, UntypedEventHandlerFunction } from './types.js'\n\nconst randomSuffix = (): string => Math.random().toString(36).slice(2, 10)\nconst IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/\nconst DEFAULT_POSTGRES_TABLE = 'abxbus_events'\nconst DEFAULT_POSTGRES_CHANNEL = 'abxbus_events'\nconst EVENT_PAYLOAD_COLUMN = 'event_payload'\n\nconst validateIdentifier = (value: string, label: string): string => {\n if (!IDENTIFIER_RE.test(value)) {\n throw new Error(`Invalid ${label}: ${JSON.stringify(value)}. Use only [A-Za-z0-9_] and start with a letter/_`)\n }\n return value\n}\n\nconst indexName = (table: string, suffix: string): string => validateIdentifier(`${table}_${suffix}`.slice(0, 63), 'index name')\n\nconst parseTableUrl = (table_url: string): { dsn: string; table: string } => {\n let parsed: URL\n try {\n parsed = new URL(table_url)\n } catch {\n throw new Error(\n 'PostgresEventBridge URL must include at least database in path, e.g. postgresql://user:pass@host:5432/dbname[/tablename]'\n )\n }\n\n const segments = parsed.pathname.split('/').filter(Boolean)\n if (segments.length < 1) {\n throw new Error(\n 'PostgresEventBridge URL must include at least database in path, e.g. postgresql://user:pass@host:5432/dbname[/tablename]'\n )\n }\n\n const db_name = segments[0]\n const table = segments.length >= 2 ? validateIdentifier(segments[1], 'table name') : DEFAULT_POSTGRES_TABLE\n const dsn_url = new URL(parsed.toString())\n dsn_url.pathname = `/${db_name}`\n return { dsn: dsn_url.toString(), table }\n}\n\nconst splitBridgePayload = (\n payload: Record<string, unknown>\n): { event_fields: Record<string, unknown>; event_payload: Record<string, unknown> } => {\n const event_fields: Record<string, unknown> = {}\n const event_payload: Record<string, unknown> = { ...payload }\n for (const [key, value] of Object.entries(payload)) {\n if (key.startsWith('event_')) {\n event_fields[key] = value\n }\n }\n return { event_fields, event_payload }\n}\n\nexport class PostgresEventBridge {\n readonly table_url: string\n readonly dsn: string\n readonly table: string\n readonly channel: string\n readonly name: string\n\n private readonly inbound_bus: EventBus\n private running: boolean\n private client: any | null\n private table_columns: Set<string>\n private notification_handler: ((msg: { channel: string; payload?: string }) => void) | null\n\n constructor(table_url: string, channel?: string, name?: string) {\n assertOptionalDependencyAvailable('PostgresEventBridge', 'pg')\n\n const parsed = parseTableUrl(table_url)\n this.table_url = table_url\n this.dsn = parsed.dsn\n this.table = parsed.table\n\n const derived_channel = channel ?? DEFAULT_POSTGRES_CHANNEL\n this.channel = validateIdentifier(derived_channel.slice(0, 63), 'channel name')\n this.name = name ?? `PostgresEventBridge_${randomSuffix()}`\n\n this.inbound_bus = new EventBus(this.name, { max_history_size: 0 })\n this.running = false\n this.client = null\n this.table_columns = new Set(['event_id', 'event_created_at', 'event_type', EVENT_PAYLOAD_COLUMN])\n this.notification_handler = null\n\n this.dispatch = this.dispatch.bind(this)\n this.emit = this.emit.bind(this)\n this.on = this.on.bind(this)\n }\n\n on<T extends BaseEvent>(event_pattern: EventClass<T>, handler: EventHandlerCallable<T>): void\n on<T extends BaseEvent>(event_pattern: string | '*', handler: UntypedEventHandlerFunction<T>): void\n on(event_pattern: EventPattern | '*', handler: EventHandlerCallable | UntypedEventHandlerFunction): void {\n this.ensureStarted()\n if (typeof event_pattern === 'string') {\n this.inbound_bus.on(event_pattern, handler as UntypedEventHandlerFunction<BaseEvent>)\n return\n }\n this.inbound_bus.on(event_pattern as EventClass<BaseEvent>, handler as EventHandlerCallable<BaseEvent>)\n }\n\n async emit<T extends BaseEvent>(event: T): Promise<void> {\n this.ensureStarted()\n if (!this.client) await this.start()\n\n const payload = event.toJSON() as Record<string, unknown>\n const { event_fields, event_payload } = splitBridgePayload(payload)\n const write_payload: Record<string, unknown> = { ...event_fields, [EVENT_PAYLOAD_COLUMN]: event_payload }\n const keys = Object.keys(write_payload).sort()\n await this.ensureColumns(keys)\n\n const columns_sql = keys.map((key) => `\"${key}\"`).join(', ')\n const placeholders_sql = keys.map((_, index) => `$${index + 1}`).join(', ')\n const values = keys.map((key) =>\n write_payload[key] === null || write_payload[key] === undefined ? null : JSON.stringify(write_payload[key])\n )\n\n const update_fields = keys.filter((key) => key !== 'event_id')\n let upsert_sql = `INSERT INTO \"${this.table}\" (${columns_sql}) VALUES (${placeholders_sql})`\n if (update_fields.length > 0) {\n const updates_sql = update_fields.map((key) => `\"${key}\" = EXCLUDED.\"${key}\"`).join(', ')\n upsert_sql += ` ON CONFLICT (\"event_id\") DO UPDATE SET ${updates_sql}`\n } else {\n upsert_sql += ' ON CONFLICT (\"event_id\") DO NOTHING'\n }\n\n await this.client.query(upsert_sql, values)\n await this.client.query('SELECT pg_notify($1, $2)', [this.channel, JSON.stringify(String(event.event_id))])\n }\n\n async dispatch<T extends BaseEvent>(event: T): Promise<void> {\n return this.emit(event)\n }\n\n async start(): Promise<void> {\n if (this.running) return\n if (!isNodeRuntime()) {\n throw new Error('PostgresEventBridge is only supported in Node.js runtimes')\n }\n\n const mod = await importOptionalDependency('PostgresEventBridge', 'pg')\n const Client = mod.Client ?? mod.default?.Client\n this.client = new Client({ connectionString: this.dsn })\n this.client.on('error', () => {})\n await this.client.connect()\n\n await this.ensureTableExists()\n await this.refreshColumnCache()\n await this.ensureColumns(['event_id', 'event_created_at', 'event_type', EVENT_PAYLOAD_COLUMN])\n await this.ensureBaseIndexes()\n\n this.notification_handler = (msg: { channel: string; payload?: string }) => {\n if (msg.channel !== this.channel || !msg.payload) return\n void this.dispatchByEventId(msg.payload).catch(() => {\n // Ignore transient shutdown races while closing connections.\n })\n }\n\n this.client.on('notification', this.notification_handler)\n await this.client.query(`LISTEN ${this.channel}`)\n this.running = true\n }\n\n async close(): Promise<void> {\n this.running = false\n if (this.client) {\n try {\n await this.client.query(`UNLISTEN ${this.channel}`)\n } catch {\n // ignore\n }\n if (this.notification_handler) {\n this.client.off('notification', this.notification_handler)\n this.notification_handler = null\n }\n await this.client.end()\n this.client = null\n }\n this.inbound_bus.destroy()\n }\n\n private ensureStarted(): void {\n if (this.running) return\n void this.start().catch((error: unknown) => {\n console.error('[abxbus] PostgresEventBridge failed to start', error)\n })\n }\n\n private async dispatchByEventId(event_id: string): Promise<void> {\n if (!this.running || !this.client) return\n const result = await this.client.query(`SELECT * FROM \"${this.table}\" WHERE \"event_id\" = $1`, [event_id])\n const row = result.rows?.[0] as Record<string, unknown> | undefined\n if (!row) return\n\n const payload: Record<string, unknown> = {}\n const raw_event_payload = row[EVENT_PAYLOAD_COLUMN]\n if (typeof raw_event_payload === 'string') {\n try {\n const decoded_event_payload = JSON.parse(raw_event_payload)\n if (decoded_event_payload && typeof decoded_event_payload === 'object' && !Array.isArray(decoded_event_payload)) {\n Object.assign(payload, decoded_event_payload as Record<string, unknown>)\n }\n } catch {\n // ignore malformed payload column\n }\n }\n\n for (const [key, raw_value] of Object.entries(row)) {\n if (key === EVENT_PAYLOAD_COLUMN || !key.startsWith('event_')) continue\n if (raw_value === null || raw_value === undefined) continue\n if (typeof raw_value !== 'string') {\n payload[key] = raw_value\n continue\n }\n try {\n payload[key] = JSON.parse(raw_value)\n } catch {\n payload[key] = raw_value\n }\n }\n\n await this.dispatchInboundPayload(payload)\n }\n\n private async dispatchInboundPayload(payload: unknown): Promise<void> {\n const event = BaseEvent.fromJSON(payload).eventReset()\n this.inbound_bus.emit(event)\n }\n\n private async ensureTableExists(): Promise<void> {\n if (!this.client) return\n await this.client.query(\n `CREATE TABLE IF NOT EXISTS \"${this.table}\" (\"event_id\" TEXT PRIMARY KEY, \"event_created_at\" TEXT, \"event_type\" TEXT, \"event_payload\" TEXT)`\n )\n }\n\n private async ensureBaseIndexes(): Promise<void> {\n if (!this.client) return\n\n const event_created_at_idx = indexName(this.table, 'event_created_at_idx')\n const event_type_idx = indexName(this.table, 'event_type_idx')\n\n await this.client.query(`CREATE INDEX IF NOT EXISTS \"${event_created_at_idx}\" ON \"${this.table}\" (\"event_created_at\")`)\n await this.client.query(`CREATE INDEX IF NOT EXISTS \"${event_type_idx}\" ON \"${this.table}\" (\"event_type\")`)\n }\n\n private async refreshColumnCache(): Promise<void> {\n if (!this.client) return\n const result = await this.client.query(\n `SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1`,\n [this.table]\n )\n this.table_columns = new Set((result.rows as Array<{ column_name: string }>).map((row) => row.column_name))\n }\n\n private async ensureColumns(keys: string[]): Promise<void> {\n if (!this.client) return\n for (const key of keys) {\n validateIdentifier(key, 'event field name')\n if (key !== EVENT_PAYLOAD_COLUMN && !key.startsWith('event_')) {\n throw new Error(`Invalid event field name for bridge column: ${JSON.stringify(key)}. Only event_* fields become columns`)\n }\n }\n\n const missing = keys.filter((key) => !this.table_columns.has(key))\n for (const key of missing) {\n await this.client.query(`ALTER TABLE \"${this.table}\" ADD COLUMN IF NOT EXISTS \"${key}\" TEXT`)\n this.table_columns.add(key)\n }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,155 @@
1
+ import { BaseEvent } from "./base_event.js";
2
+ import { EventBus } from "./event_bus.js";
3
+ import { assertOptionalDependencyAvailable, importOptionalDependency, isNodeRuntime } from "./optional_deps.js";
4
+ const randomSuffix = () => Math.random().toString(36).slice(2, 10);
5
+ const DEFAULT_REDIS_CHANNEL = "abxbus_events";
6
+ const DB_INIT_KEY = "__abxbus:bridge_init__";
7
+ const parseRedisUrl = (redis_url, channel) => {
8
+ let parsed;
9
+ try {
10
+ parsed = new URL(redis_url);
11
+ } catch {
12
+ throw new Error(`RedisEventBridge URL must be a valid redis:// or rediss:// URL, got: ${redis_url}`);
13
+ }
14
+ const protocol = parsed.protocol.replace(/:$/, "").toLowerCase();
15
+ if (protocol !== "redis" && protocol !== "rediss") {
16
+ throw new Error(`RedisEventBridge URL must use redis:// or rediss://, got: ${redis_url}`);
17
+ }
18
+ const segments = parsed.pathname.split("/").filter(Boolean);
19
+ if (segments.length > 2) {
20
+ throw new Error(`RedisEventBridge URL path must be /<db> or /<db>/<channel>, got: ${parsed.pathname || "/"}`);
21
+ }
22
+ let db_index = "0";
23
+ let channel_from_url;
24
+ if (segments.length > 0) {
25
+ db_index = segments[0];
26
+ if (!/^\d+$/.test(db_index)) {
27
+ throw new Error(`RedisEventBridge URL db path segment must be numeric, got: ${JSON.stringify(db_index)} in ${redis_url}`);
28
+ }
29
+ if (segments.length === 2) {
30
+ channel_from_url = segments[1];
31
+ }
32
+ }
33
+ const resolved_channel = channel ?? channel_from_url ?? DEFAULT_REDIS_CHANNEL;
34
+ if (!resolved_channel) {
35
+ throw new Error("RedisEventBridge channel must not be empty");
36
+ }
37
+ const normalized = new URL(parsed.toString());
38
+ normalized.pathname = `/${db_index}`;
39
+ return { url: normalized.toString(), channel: resolved_channel };
40
+ };
41
+ class RedisEventBridge {
42
+ url;
43
+ channel;
44
+ name;
45
+ inbound_bus;
46
+ running;
47
+ start_promise;
48
+ redis_pub;
49
+ redis_sub;
50
+ constructor(redis_url, channel, name) {
51
+ assertOptionalDependencyAvailable("RedisEventBridge", "ioredis");
52
+ const parsed = parseRedisUrl(redis_url, channel);
53
+ this.url = parsed.url;
54
+ this.channel = parsed.channel;
55
+ this.name = name ?? `RedisEventBridge_${randomSuffix()}`;
56
+ this.inbound_bus = new EventBus(this.name, { max_history_size: 0 });
57
+ this.running = false;
58
+ this.start_promise = null;
59
+ this.redis_pub = null;
60
+ this.redis_sub = null;
61
+ this.dispatch = this.dispatch.bind(this);
62
+ this.emit = this.emit.bind(this);
63
+ this.on = this.on.bind(this);
64
+ }
65
+ on(event_pattern, handler) {
66
+ this.ensureStarted();
67
+ if (typeof event_pattern === "string") {
68
+ this.inbound_bus.on(event_pattern, handler);
69
+ return;
70
+ }
71
+ this.inbound_bus.on(event_pattern, handler);
72
+ }
73
+ async emit(event) {
74
+ this.ensureStarted();
75
+ if (!this.redis_pub) await this.start();
76
+ const payload = JSON.stringify(event.toJSON());
77
+ await this.redis_pub.publish(this.channel, payload);
78
+ }
79
+ async dispatch(event) {
80
+ return this.emit(event);
81
+ }
82
+ async start() {
83
+ if (this.running) return;
84
+ if (this.start_promise) {
85
+ await this.start_promise;
86
+ return;
87
+ }
88
+ this.start_promise = (async () => {
89
+ if (!isNodeRuntime()) {
90
+ throw new Error("RedisEventBridge is only supported in Node.js runtimes");
91
+ }
92
+ const mod = await importOptionalDependency("RedisEventBridge", "ioredis");
93
+ const Redis = mod.default ?? mod.Redis ?? mod;
94
+ const redis_pub = new Redis(this.url);
95
+ const redis_sub = new Redis(this.url);
96
+ redis_pub.on("error", () => {
97
+ });
98
+ redis_sub.on("error", () => {
99
+ });
100
+ await redis_pub.set(DB_INIT_KEY, "1", "EX", 60, "NX");
101
+ redis_sub.on("message", (channel_name, message) => {
102
+ if (channel_name !== this.channel) return;
103
+ try {
104
+ const payload = JSON.parse(message);
105
+ void this.dispatchInboundPayload(payload);
106
+ } catch {
107
+ }
108
+ });
109
+ await redis_sub.subscribe(this.channel);
110
+ this.redis_pub = redis_pub;
111
+ this.redis_sub = redis_sub;
112
+ this.running = true;
113
+ })();
114
+ try {
115
+ await this.start_promise;
116
+ } finally {
117
+ this.start_promise = null;
118
+ }
119
+ }
120
+ async close() {
121
+ if (this.start_promise) {
122
+ await this.start_promise.catch(() => {
123
+ });
124
+ }
125
+ this.running = false;
126
+ if (this.redis_sub) {
127
+ try {
128
+ await this.redis_sub.unsubscribe(this.channel);
129
+ } catch {
130
+ }
131
+ await this.redis_sub.quit();
132
+ this.redis_sub = null;
133
+ }
134
+ if (this.redis_pub) {
135
+ await this.redis_pub.quit();
136
+ this.redis_pub = null;
137
+ }
138
+ this.inbound_bus.destroy();
139
+ }
140
+ ensureStarted() {
141
+ if (this.running) return;
142
+ if (this.start_promise) return;
143
+ void this.start().catch((error) => {
144
+ console.error("[abxbus] RedisEventBridge failed to start", error);
145
+ });
146
+ }
147
+ async dispatchInboundPayload(payload) {
148
+ const event = BaseEvent.fromJSON(payload).eventReset();
149
+ this.inbound_bus.emit(event);
150
+ }
151
+ }
152
+ export {
153
+ RedisEventBridge
154
+ };
155
+ //# sourceMappingURL=bridge_redis.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/bridge_redis.ts"],
4
+ "sourcesContent": ["/**\n * Redis pub/sub bridge for forwarding events between runtimes.\n *\n * Usage:\n * // channel from URL path\n * const bridge = new RedisEventBridge('redis://user:pass@localhost:6379/1/my_channel')\n *\n * // explicit channel override\n * const bridge2 = new RedisEventBridge('redis://user:pass@localhost:6379/1', 'my_channel')\n *\n * URL format:\n * redis://user:pass@host:6379/<db>/<optional_channel>\n */\nimport { BaseEvent } from './base_event.js'\nimport { EventBus } from './event_bus.js'\nimport { assertOptionalDependencyAvailable, importOptionalDependency, isNodeRuntime } from './optional_deps.js'\nimport type { EventClass, EventHandlerCallable, EventPattern, UntypedEventHandlerFunction } from './types.js'\n\nconst randomSuffix = (): string => Math.random().toString(36).slice(2, 10)\nconst DEFAULT_REDIS_CHANNEL = 'abxbus_events'\nconst DB_INIT_KEY = '__abxbus:bridge_init__'\n\nconst parseRedisUrl = (redis_url: string, channel?: string): { url: string; channel: string } => {\n let parsed: URL\n try {\n parsed = new URL(redis_url)\n } catch {\n throw new Error(`RedisEventBridge URL must be a valid redis:// or rediss:// URL, got: ${redis_url}`)\n }\n\n const protocol = parsed.protocol.replace(/:$/, '').toLowerCase()\n if (protocol !== 'redis' && protocol !== 'rediss') {\n throw new Error(`RedisEventBridge URL must use redis:// or rediss://, got: ${redis_url}`)\n }\n\n const segments = parsed.pathname.split('/').filter(Boolean)\n if (segments.length > 2) {\n throw new Error(`RedisEventBridge URL path must be /<db> or /<db>/<channel>, got: ${parsed.pathname || '/'}`)\n }\n\n let db_index = '0'\n let channel_from_url: string | undefined\n\n if (segments.length > 0) {\n db_index = segments[0]\n if (!/^\\d+$/.test(db_index)) {\n throw new Error(`RedisEventBridge URL db path segment must be numeric, got: ${JSON.stringify(db_index)} in ${redis_url}`)\n }\n if (segments.length === 2) {\n channel_from_url = segments[1]\n }\n }\n\n const resolved_channel = channel ?? channel_from_url ?? DEFAULT_REDIS_CHANNEL\n if (!resolved_channel) {\n throw new Error('RedisEventBridge channel must not be empty')\n }\n\n const normalized = new URL(parsed.toString())\n normalized.pathname = `/${db_index}`\n return { url: normalized.toString(), channel: resolved_channel }\n}\n\nexport class RedisEventBridge {\n readonly url: string\n readonly channel: string\n readonly name: string\n\n private readonly inbound_bus: EventBus\n private running: boolean\n private start_promise: Promise<void> | null\n private redis_pub: any | null\n private redis_sub: any | null\n\n constructor(redis_url: string, channel?: string, name?: string) {\n assertOptionalDependencyAvailable('RedisEventBridge', 'ioredis')\n\n const parsed = parseRedisUrl(redis_url, channel)\n this.url = parsed.url\n this.channel = parsed.channel\n this.name = name ?? `RedisEventBridge_${randomSuffix()}`\n this.inbound_bus = new EventBus(this.name, { max_history_size: 0 })\n this.running = false\n this.start_promise = null\n this.redis_pub = null\n this.redis_sub = null\n\n this.dispatch = this.dispatch.bind(this)\n this.emit = this.emit.bind(this)\n this.on = this.on.bind(this)\n }\n\n on<T extends BaseEvent>(event_pattern: EventClass<T>, handler: EventHandlerCallable<T>): void\n on<T extends BaseEvent>(event_pattern: string | '*', handler: UntypedEventHandlerFunction<T>): void\n on(event_pattern: EventPattern | '*', handler: EventHandlerCallable | UntypedEventHandlerFunction): void {\n this.ensureStarted()\n if (typeof event_pattern === 'string') {\n this.inbound_bus.on(event_pattern, handler as UntypedEventHandlerFunction<BaseEvent>)\n return\n }\n this.inbound_bus.on(event_pattern as EventClass<BaseEvent>, handler as EventHandlerCallable<BaseEvent>)\n }\n\n async emit<T extends BaseEvent>(event: T): Promise<void> {\n this.ensureStarted()\n if (!this.redis_pub) await this.start()\n const payload = JSON.stringify(event.toJSON())\n await this.redis_pub.publish(this.channel, payload)\n }\n\n async dispatch<T extends BaseEvent>(event: T): Promise<void> {\n return this.emit(event)\n }\n\n async start(): Promise<void> {\n if (this.running) return\n if (this.start_promise) {\n await this.start_promise\n return\n }\n\n // `on(...)` auto-start and explicit `await start()` can happen back-to-back; use one in-flight\n // startup promise so we do not leak extra Redis clients.\n this.start_promise = (async () => {\n if (!isNodeRuntime()) {\n throw new Error('RedisEventBridge is only supported in Node.js runtimes')\n }\n\n const mod = await importOptionalDependency('RedisEventBridge', 'ioredis')\n const Redis = mod.default ?? mod.Redis ?? mod\n const redis_pub = new Redis(this.url)\n const redis_sub = new Redis(this.url)\n\n redis_pub.on('error', () => {})\n redis_sub.on('error', () => {})\n\n // Redis logical DBs are created lazily; writing a short-lived key initializes/validates the selected DB.\n await redis_pub.set(DB_INIT_KEY, '1', 'EX', 60, 'NX')\n redis_sub.on('message', (channel_name: string, message: string) => {\n if (channel_name !== this.channel) return\n try {\n const payload = JSON.parse(message)\n void this.dispatchInboundPayload(payload)\n } catch {\n // Ignore malformed payloads.\n }\n })\n await redis_sub.subscribe(this.channel)\n this.redis_pub = redis_pub\n this.redis_sub = redis_sub\n this.running = true\n })()\n\n try {\n await this.start_promise\n } finally {\n this.start_promise = null\n }\n }\n\n async close(): Promise<void> {\n if (this.start_promise) {\n await this.start_promise.catch(() => {})\n }\n this.running = false\n if (this.redis_sub) {\n try {\n await this.redis_sub.unsubscribe(this.channel)\n } catch {\n // ignore\n }\n await this.redis_sub.quit()\n this.redis_sub = null\n }\n if (this.redis_pub) {\n await this.redis_pub.quit()\n this.redis_pub = null\n }\n this.inbound_bus.destroy()\n }\n\n private ensureStarted(): void {\n if (this.running) return\n if (this.start_promise) return\n void this.start().catch((error: unknown) => {\n console.error('[abxbus] RedisEventBridge failed to start', error)\n })\n }\n\n private async dispatchInboundPayload(payload: unknown): Promise<void> {\n const event = BaseEvent.fromJSON(payload).eventReset()\n this.inbound_bus.emit(event)\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }