tinbase 0.1.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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -0
  3. package/assets/logo.svg +18 -0
  4. package/dist/auth/handler.d.ts +43 -0
  5. package/dist/auth/handler.js +291 -0
  6. package/dist/auth/handler.js.map +1 -0
  7. package/dist/auth/password.d.ts +3 -0
  8. package/dist/auth/password.js +40 -0
  9. package/dist/auth/password.js.map +1 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.js +181 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/db/bootstrap.d.ts +6 -0
  14. package/dist/db/bootstrap.js +213 -0
  15. package/dist/db/bootstrap.js.map +1 -0
  16. package/dist/db/database.d.ts +83 -0
  17. package/dist/db/database.js +270 -0
  18. package/dist/db/database.js.map +1 -0
  19. package/dist/db/engine.d.ts +30 -0
  20. package/dist/db/engine.js +28 -0
  21. package/dist/db/engine.js.map +1 -0
  22. package/dist/db/pglite-engine.d.ts +3 -0
  23. package/dist/db/pglite-engine.js +39 -0
  24. package/dist/db/pglite-engine.js.map +1 -0
  25. package/dist/index.d.ts +23 -0
  26. package/dist/index.js +145 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/jwt.d.ts +20 -0
  29. package/dist/jwt.js +72 -0
  30. package/dist/jwt.js.map +1 -0
  31. package/dist/node/bun-server.d.ts +8 -0
  32. package/dist/node/bun-server.js +41 -0
  33. package/dist/node/bun-server.js.map +1 -0
  34. package/dist/node/fs-driver.d.ts +11 -0
  35. package/dist/node/fs-driver.js +39 -0
  36. package/dist/node/fs-driver.js.map +1 -0
  37. package/dist/node/index.d.ts +7 -0
  38. package/dist/node/index.js +8 -0
  39. package/dist/node/index.js.map +1 -0
  40. package/dist/node/native/engine.d.ts +13 -0
  41. package/dist/node/native/engine.js +186 -0
  42. package/dist/node/native/engine.js.map +1 -0
  43. package/dist/node/native/wire.d.ts +38 -0
  44. package/dist/node/native/wire.js +345 -0
  45. package/dist/node/native/wire.js.map +1 -0
  46. package/dist/node/project.d.ts +6 -0
  47. package/dist/node/project.js +29 -0
  48. package/dist/node/project.js.map +1 -0
  49. package/dist/node/server-shared.d.ts +6 -0
  50. package/dist/node/server-shared.js +2 -0
  51. package/dist/node/server-shared.js.map +1 -0
  52. package/dist/node/server.d.ts +14 -0
  53. package/dist/node/server.js +83 -0
  54. package/dist/node/server.js.map +1 -0
  55. package/dist/node/ws.d.ts +8 -0
  56. package/dist/node/ws.js +165 -0
  57. package/dist/node/ws.js.map +1 -0
  58. package/dist/realtime/engine.d.ts +53 -0
  59. package/dist/realtime/engine.js +437 -0
  60. package/dist/realtime/engine.js.map +1 -0
  61. package/dist/rest/build.d.ts +53 -0
  62. package/dist/rest/build.js +505 -0
  63. package/dist/rest/build.js.map +1 -0
  64. package/dist/rest/errors.d.ts +2 -0
  65. package/dist/rest/errors.js +48 -0
  66. package/dist/rest/errors.js.map +1 -0
  67. package/dist/rest/handler.d.ts +11 -0
  68. package/dist/rest/handler.js +332 -0
  69. package/dist/rest/handler.js.map +1 -0
  70. package/dist/rest/parse.d.ts +69 -0
  71. package/dist/rest/parse.js +261 -0
  72. package/dist/rest/parse.js.map +1 -0
  73. package/dist/storage/driver.d.ts +9 -0
  74. package/dist/storage/driver.js +18 -0
  75. package/dist/storage/driver.js.map +1 -0
  76. package/dist/storage/handler.d.ts +49 -0
  77. package/dist/storage/handler.js +534 -0
  78. package/dist/storage/handler.js.map +1 -0
  79. package/dist/types.d.ts +51 -0
  80. package/dist/types.js +11 -0
  81. package/dist/types.js.map +1 -0
  82. package/package.json +59 -0
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Minimal RFC 6455 WebSocket server on top of node:http upgrade events —
3
+ * enough for Phoenix-protocol JSON text frames, so no `ws` dependency.
4
+ */
5
+ import { createHash } from 'node:crypto';
6
+ const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
7
+ export function acceptWebSocket(req, socket, head) {
8
+ const key = req.headers['sec-websocket-key'];
9
+ if (!key || (req.headers.upgrade ?? '').toLowerCase() !== 'websocket') {
10
+ socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
11
+ socket.destroy();
12
+ return null;
13
+ }
14
+ const accept = createHash('sha1').update(key + WS_GUID).digest('base64');
15
+ socket.write('HTTP/1.1 101 Switching Protocols\r\n' +
16
+ 'Upgrade: websocket\r\n' +
17
+ 'Connection: Upgrade\r\n' +
18
+ `Sec-WebSocket-Accept: ${accept}\r\n` +
19
+ '\r\n');
20
+ let buffer = head.length > 0 ? Buffer.from(head) : Buffer.alloc(0);
21
+ let fragments = [];
22
+ let fragmentOpcode = 0;
23
+ let closed = false;
24
+ const conn = {
25
+ onMessage: null,
26
+ onClose: null,
27
+ send(data) {
28
+ if (closed)
29
+ return;
30
+ if (typeof data === 'string') {
31
+ socket.write(encodeFrame(0x1, Buffer.from(data, 'utf8')));
32
+ }
33
+ else {
34
+ socket.write(encodeFrame(0x2, Buffer.from(data)));
35
+ }
36
+ },
37
+ close(code = 1000, reason = '') {
38
+ if (closed)
39
+ return;
40
+ const payload = Buffer.alloc(2 + Buffer.byteLength(reason));
41
+ payload.writeUInt16BE(code, 0);
42
+ payload.write(reason, 2);
43
+ socket.write(encodeFrame(0x8, payload));
44
+ finish();
45
+ },
46
+ };
47
+ function finish() {
48
+ if (closed)
49
+ return;
50
+ closed = true;
51
+ socket.end();
52
+ conn.onClose?.();
53
+ }
54
+ function deliver(opcode, payload) {
55
+ if (opcode === 0x1)
56
+ conn.onMessage?.(payload.toString('utf8'));
57
+ else if (opcode === 0x2)
58
+ conn.onMessage?.(new Uint8Array(payload));
59
+ }
60
+ socket.on('data', (chunk) => {
61
+ buffer = Buffer.concat([buffer, chunk]);
62
+ processFrames();
63
+ });
64
+ socket.on('close', finish);
65
+ socket.on('error', finish);
66
+ function processFrames() {
67
+ while (true) {
68
+ const frame = decodeFrame(buffer);
69
+ if (!frame)
70
+ return;
71
+ buffer = buffer.subarray(frame.frameLength);
72
+ switch (frame.opcode) {
73
+ case 0x0: // continuation
74
+ fragments.push(frame.payload);
75
+ if (frame.fin) {
76
+ const full = Buffer.concat(fragments);
77
+ fragments = [];
78
+ deliver(fragmentOpcode, full);
79
+ }
80
+ break;
81
+ case 0x1: // text
82
+ case 0x2: // binary
83
+ if (frame.fin) {
84
+ deliver(frame.opcode, frame.payload);
85
+ }
86
+ else {
87
+ fragments = [frame.payload];
88
+ fragmentOpcode = frame.opcode;
89
+ }
90
+ break;
91
+ case 0x8: // close
92
+ if (!closed) {
93
+ socket.write(encodeFrame(0x8, frame.payload.subarray(0, 2)));
94
+ }
95
+ finish();
96
+ return;
97
+ case 0x9: // ping
98
+ socket.write(encodeFrame(0xa, frame.payload));
99
+ break;
100
+ case 0xa: // pong
101
+ break;
102
+ }
103
+ }
104
+ }
105
+ return conn;
106
+ }
107
+ function decodeFrame(buf) {
108
+ if (buf.length < 2)
109
+ return null;
110
+ const fin = (buf[0] & 0x80) !== 0;
111
+ const opcode = buf[0] & 0x0f;
112
+ const masked = (buf[1] & 0x80) !== 0;
113
+ let payloadLength = buf[1] & 0x7f;
114
+ let offset = 2;
115
+ if (payloadLength === 126) {
116
+ if (buf.length < offset + 2)
117
+ return null;
118
+ payloadLength = buf.readUInt16BE(offset);
119
+ offset += 2;
120
+ }
121
+ else if (payloadLength === 127) {
122
+ if (buf.length < offset + 8)
123
+ return null;
124
+ const big = buf.readBigUInt64BE(offset);
125
+ if (big > BigInt(64 * 1024 * 1024))
126
+ throw new Error('frame too large');
127
+ payloadLength = Number(big);
128
+ offset += 8;
129
+ }
130
+ let maskKey = null;
131
+ if (masked) {
132
+ if (buf.length < offset + 4)
133
+ return null;
134
+ maskKey = buf.subarray(offset, offset + 4);
135
+ offset += 4;
136
+ }
137
+ if (buf.length < offset + payloadLength)
138
+ return null;
139
+ let payload = Buffer.from(buf.subarray(offset, offset + payloadLength));
140
+ if (maskKey) {
141
+ for (let i = 0; i < payload.length; i++)
142
+ payload[i] ^= maskKey[i % 4];
143
+ }
144
+ return { fin, opcode, payload, frameLength: offset + payloadLength };
145
+ }
146
+ function encodeFrame(opcode, payload) {
147
+ let header;
148
+ if (payload.length < 126) {
149
+ header = Buffer.from([0x80 | opcode, payload.length]);
150
+ }
151
+ else if (payload.length < 65536) {
152
+ header = Buffer.alloc(4);
153
+ header[0] = 0x80 | opcode;
154
+ header[1] = 126;
155
+ header.writeUInt16BE(payload.length, 2);
156
+ }
157
+ else {
158
+ header = Buffer.alloc(10);
159
+ header[0] = 0x80 | opcode;
160
+ header[1] = 127;
161
+ header.writeBigUInt64BE(BigInt(payload.length), 2);
162
+ }
163
+ return Buffer.concat([header, payload]);
164
+ }
165
+ //# sourceMappingURL=ws.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws.js","sourceRoot":"","sources":["../../src/node/ws.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Realtime engine speaking the Phoenix-channel protocol that
3
+ * @supabase/realtime-js uses (vsn=1.0.0 JSON serialization): channel
4
+ * join/leave, heartbeat, broadcast, presence, and postgres_changes fed by
5
+ * the trigger-based CDC pipeline in the database layer.
6
+ *
7
+ * Transport-agnostic: sockets are anything with send()/close(), so the same
8
+ * engine works over Node WebSockets today and an in-process pair in the
9
+ * browser.
10
+ */
11
+ import type { Database } from '../db/database.js';
12
+ export interface RealtimeSocketLike {
13
+ send(data: string | Uint8Array): void;
14
+ close(code?: number, reason?: string): void;
15
+ }
16
+ export declare class RealtimeEngine {
17
+ private db;
18
+ private connections;
19
+ private bindingCounter;
20
+ private phxRefCounter;
21
+ /** topic → key → metas */
22
+ private presence;
23
+ private stopCdc;
24
+ constructor(db: Database);
25
+ start(): Promise<void>;
26
+ stop(): void;
27
+ /** Attach a socket. Returns callbacks the transport must wire up. */
28
+ connect(socket: RealtimeSocketLike, opts?: {
29
+ vsn?: string;
30
+ }): {
31
+ onMessage: (data: string | Uint8Array) => void;
32
+ onClose: () => void;
33
+ };
34
+ private send;
35
+ private reply;
36
+ private handleMessage;
37
+ private handleJoin;
38
+ private leaveChannel;
39
+ /**
40
+ * Phoenix binary serializer, kind 3 (userBroadcastPush):
41
+ * [3, joinRefLen, refLen, topicLen, eventLen, metaLen, encoding,
42
+ * joinRef, ref, topic, event, meta, payloadBytes]
43
+ * encoding: 0 = raw binary payload, 1 = JSON payload
44
+ */
45
+ private handleBinary;
46
+ private handleBroadcast;
47
+ private handlePresence;
48
+ private broadcastToTopic;
49
+ private dispatchCdc;
50
+ private bindingMatches;
51
+ }
52
+ /** Evaluate a realtime filter string ("col=eq.value") against a row. */
53
+ export declare function matchFilter(filter: string, row: Record<string, unknown>): boolean;
@@ -0,0 +1,437 @@
1
+ export class RealtimeEngine {
2
+ db;
3
+ connections = new Set();
4
+ bindingCounter = 1;
5
+ phxRefCounter = 1;
6
+ /** topic → key → metas */
7
+ presence = new Map();
8
+ stopCdc = null;
9
+ constructor(db) {
10
+ this.db = db;
11
+ }
12
+ async start() {
13
+ if (this.stopCdc)
14
+ return;
15
+ this.stopCdc = await this.db.onCdcEvent((e) => this.dispatchCdc(e));
16
+ }
17
+ stop() {
18
+ this.stopCdc?.();
19
+ this.stopCdc = null;
20
+ for (const conn of this.connections)
21
+ conn.socket.close(1001, 'server shutting down');
22
+ this.connections.clear();
23
+ }
24
+ /** Attach a socket. Returns callbacks the transport must wire up. */
25
+ connect(socket, opts = {}) {
26
+ const conn = { socket, channels: new Map(), vsn: opts.vsn ?? '1.0.0' };
27
+ this.connections.add(conn);
28
+ return {
29
+ onMessage: (data) => {
30
+ void this.handleMessage(conn, data);
31
+ },
32
+ onClose: () => {
33
+ for (const topic of conn.channels.keys())
34
+ this.leaveChannel(conn, topic);
35
+ this.connections.delete(conn);
36
+ },
37
+ };
38
+ }
39
+ send(conn, msg) {
40
+ try {
41
+ const encoded = conn.vsn === '2.0.0'
42
+ ? JSON.stringify([msg.join_ref ?? null, msg.ref ?? null, msg.topic, msg.event, msg.payload])
43
+ : JSON.stringify(msg);
44
+ conn.socket.send(encoded);
45
+ }
46
+ catch {
47
+ // transport already closed
48
+ }
49
+ }
50
+ reply(conn, orig, status, response) {
51
+ this.send(conn, {
52
+ topic: orig.topic,
53
+ event: 'phx_reply',
54
+ payload: { status, response },
55
+ ref: orig.ref,
56
+ join_ref: orig.join_ref ?? null,
57
+ });
58
+ }
59
+ async handleMessage(conn, data) {
60
+ if (typeof data !== 'string') {
61
+ this.handleBinary(conn, data);
62
+ return;
63
+ }
64
+ let msg;
65
+ try {
66
+ const parsed = JSON.parse(data);
67
+ if (Array.isArray(parsed)) {
68
+ // Phoenix v2 serializer: [join_ref, ref, topic, event, payload]
69
+ const [join_ref, ref, topic, event, payload] = parsed;
70
+ msg = { join_ref, ref, topic, event, payload };
71
+ }
72
+ else {
73
+ msg = parsed;
74
+ }
75
+ }
76
+ catch {
77
+ return;
78
+ }
79
+ if (msg.topic === 'phoenix' && msg.event === 'heartbeat') {
80
+ this.reply(conn, msg, 'ok', {});
81
+ return;
82
+ }
83
+ switch (msg.event) {
84
+ case 'phx_join':
85
+ await this.handleJoin(conn, msg);
86
+ break;
87
+ case 'phx_leave':
88
+ this.leaveChannel(conn, msg.topic);
89
+ this.reply(conn, msg, 'ok', {});
90
+ break;
91
+ case 'broadcast':
92
+ this.handleBroadcast(conn, msg);
93
+ break;
94
+ case 'presence':
95
+ this.handlePresence(conn, msg);
96
+ break;
97
+ case 'access_token':
98
+ // token refresh — nothing to re-validate in this single-tenant setup
99
+ break;
100
+ default:
101
+ break;
102
+ }
103
+ }
104
+ async handleJoin(conn, msg) {
105
+ const config = (msg.payload?.config ?? {});
106
+ const bindings = [];
107
+ for (const spec of config.postgres_changes ?? []) {
108
+ const binding = {
109
+ id: this.bindingCounter++,
110
+ event: (spec.event ?? '*').toUpperCase(),
111
+ schema: spec.schema ?? 'public',
112
+ table: spec.table ?? '*',
113
+ filter: spec.filter,
114
+ };
115
+ bindings.push(binding);
116
+ if (binding.table !== '*') {
117
+ try {
118
+ await this.db.ensureCdcTrigger(binding.schema, binding.table);
119
+ }
120
+ catch {
121
+ this.reply(conn, msg, 'error', {
122
+ reason: `unable to subscribe to changes on ${binding.schema}.${binding.table}`,
123
+ });
124
+ return;
125
+ }
126
+ }
127
+ else {
128
+ // wildcard table: attach triggers to every table currently in the schema
129
+ const info = await this.db.getSchemaInfo(binding.schema);
130
+ for (const table of info.tables.keys()) {
131
+ await this.db.ensureCdcTrigger(binding.schema, table).catch(() => { });
132
+ }
133
+ }
134
+ }
135
+ const channel = {
136
+ topic: msg.topic,
137
+ joinRef: msg.join_ref ?? msg.ref ?? null,
138
+ bindings,
139
+ broadcastSelf: config.broadcast?.self ?? false,
140
+ broadcastAck: config.broadcast?.ack ?? false,
141
+ presenceKey: config.presence?.key || crypto.randomUUID(),
142
+ presenceEnabled: config.presence?.enabled ?? true,
143
+ };
144
+ conn.channels.set(msg.topic, channel);
145
+ this.reply(conn, msg, 'ok', {
146
+ postgres_changes: bindings.map((b) => ({
147
+ id: b.id,
148
+ event: b.event,
149
+ schema: b.schema,
150
+ table: b.table,
151
+ ...(b.filter ? { filter: b.filter } : {}),
152
+ })),
153
+ });
154
+ // initial presence snapshot
155
+ const state = this.presence.get(msg.topic);
156
+ this.send(conn, {
157
+ topic: msg.topic,
158
+ event: 'presence_state',
159
+ payload: state ? Object.fromEntries(state) : {},
160
+ ref: null,
161
+ });
162
+ // postgres_changes readiness signal (realtime-js listens for this)
163
+ if (bindings.length > 0) {
164
+ this.send(conn, {
165
+ topic: msg.topic,
166
+ event: 'system',
167
+ payload: {
168
+ status: 'ok',
169
+ extension: 'postgres_changes',
170
+ message: 'Subscribed to PostgreSQL',
171
+ channel: msg.topic.replace(/^realtime:/, ''),
172
+ },
173
+ ref: null,
174
+ });
175
+ }
176
+ }
177
+ leaveChannel(conn, topic) {
178
+ const channel = conn.channels.get(topic);
179
+ if (!channel)
180
+ return;
181
+ conn.channels.delete(topic);
182
+ // presence leave
183
+ const state = this.presence.get(topic);
184
+ if (state?.has(channel.presenceKey)) {
185
+ const metas = state.get(channel.presenceKey);
186
+ state.delete(channel.presenceKey);
187
+ if (state.size === 0)
188
+ this.presence.delete(topic);
189
+ this.broadcastToTopic(topic, {
190
+ topic,
191
+ event: 'presence_diff',
192
+ payload: { joins: {}, leaves: { [channel.presenceKey]: metas } },
193
+ ref: null,
194
+ });
195
+ }
196
+ }
197
+ /**
198
+ * Phoenix binary serializer, kind 3 (userBroadcastPush):
199
+ * [3, joinRefLen, refLen, topicLen, eventLen, metaLen, encoding,
200
+ * joinRef, ref, topic, event, meta, payloadBytes]
201
+ * encoding: 0 = raw binary payload, 1 = JSON payload
202
+ */
203
+ handleBinary(conn, bytes) {
204
+ if (bytes.length < 7 || bytes[0] !== 3)
205
+ return;
206
+ const [, joinRefLen, refLen, topicLen, eventLen, metaLen, encoding] = bytes;
207
+ let offset = 7;
208
+ const decoder = new TextDecoder();
209
+ const read = (len) => {
210
+ const out = decoder.decode(bytes.subarray(offset, offset + len));
211
+ offset += len;
212
+ return out;
213
+ };
214
+ const joinRef = read(joinRefLen);
215
+ const ref = read(refLen);
216
+ const topic = read(topicLen);
217
+ const userEvent = read(eventLen);
218
+ read(metaLen); // metadata — not relayed
219
+ const payloadBytes = bytes.subarray(offset);
220
+ const payload = {
221
+ type: 'broadcast',
222
+ event: userEvent,
223
+ payload: encoding === 1 ? safeJsonParse(decoder.decode(payloadBytes)) : payloadBytes,
224
+ };
225
+ this.handleBroadcast(conn, {
226
+ topic,
227
+ event: 'broadcast',
228
+ payload,
229
+ ref: ref || null,
230
+ join_ref: joinRef || null,
231
+ });
232
+ }
233
+ handleBroadcast(conn, msg) {
234
+ const sender = conn.channels.get(msg.topic);
235
+ if (!sender)
236
+ return;
237
+ const userPayload = msg.payload.payload;
238
+ const binaryFrame = userPayload instanceof Uint8Array
239
+ ? encodeUserBroadcast(msg.topic, String(msg.payload.event ?? ''), userPayload)
240
+ : null;
241
+ for (const other of this.connections) {
242
+ const channel = other.channels.get(msg.topic);
243
+ if (!channel)
244
+ continue;
245
+ if (other === conn && !sender.broadcastSelf)
246
+ continue;
247
+ if (binaryFrame) {
248
+ try {
249
+ other.socket.send(binaryFrame);
250
+ }
251
+ catch {
252
+ // transport already closed
253
+ }
254
+ }
255
+ else {
256
+ this.send(other, { topic: msg.topic, event: 'broadcast', payload: msg.payload, ref: null });
257
+ }
258
+ }
259
+ if (sender.broadcastAck && msg.ref)
260
+ this.reply(conn, msg, 'ok', {});
261
+ }
262
+ handlePresence(conn, msg) {
263
+ const channel = conn.channels.get(msg.topic);
264
+ if (!channel)
265
+ return;
266
+ const event = String((msg.payload?.event ?? '')).toLowerCase();
267
+ if (event === 'track') {
268
+ const meta = {
269
+ ...(msg.payload?.payload ?? {}),
270
+ phx_ref: `F${this.phxRefCounter++}`,
271
+ };
272
+ let state = this.presence.get(msg.topic);
273
+ if (!state) {
274
+ state = new Map();
275
+ this.presence.set(msg.topic, state);
276
+ }
277
+ const previous = state.get(channel.presenceKey);
278
+ state.set(channel.presenceKey, { metas: [meta] });
279
+ this.broadcastToTopic(msg.topic, {
280
+ topic: msg.topic,
281
+ event: 'presence_diff',
282
+ payload: {
283
+ joins: { [channel.presenceKey]: { metas: [meta] } },
284
+ leaves: previous ? { [channel.presenceKey]: previous } : {},
285
+ },
286
+ ref: null,
287
+ });
288
+ if (msg.ref)
289
+ this.reply(conn, msg, 'ok', {});
290
+ return;
291
+ }
292
+ if (event === 'untrack') {
293
+ const state = this.presence.get(msg.topic);
294
+ const metas = state?.get(channel.presenceKey);
295
+ if (state && metas) {
296
+ state.delete(channel.presenceKey);
297
+ if (state.size === 0)
298
+ this.presence.delete(msg.topic);
299
+ this.broadcastToTopic(msg.topic, {
300
+ topic: msg.topic,
301
+ event: 'presence_diff',
302
+ payload: { joins: {}, leaves: { [channel.presenceKey]: metas } },
303
+ ref: null,
304
+ });
305
+ }
306
+ if (msg.ref)
307
+ this.reply(conn, msg, 'ok', {});
308
+ }
309
+ }
310
+ broadcastToTopic(topic, msg) {
311
+ for (const conn of this.connections) {
312
+ if (conn.channels.has(topic))
313
+ this.send(conn, msg);
314
+ }
315
+ }
316
+ // ── postgres_changes fan-out ──────────────────────────────────────────
317
+ async dispatchCdc(event) {
318
+ // column metadata lets realtime-js run its type conversion
319
+ let columns = [];
320
+ try {
321
+ const info = await this.db.getSchemaInfo(event.schema);
322
+ columns = (info.tables.get(event.table)?.columns ?? []).map((c) => ({
323
+ name: c.name,
324
+ type: c.udtName,
325
+ }));
326
+ }
327
+ catch {
328
+ // schema went away — still deliver the event without columns
329
+ }
330
+ for (const conn of this.connections) {
331
+ for (const channel of conn.channels.values()) {
332
+ const ids = channel.bindings
333
+ .filter((b) => this.bindingMatches(b, event))
334
+ .map((b) => b.id);
335
+ if (ids.length === 0)
336
+ continue;
337
+ this.send(conn, {
338
+ topic: channel.topic,
339
+ event: 'postgres_changes',
340
+ payload: {
341
+ ids,
342
+ data: {
343
+ schema: event.schema,
344
+ table: event.table,
345
+ commit_timestamp: event.commit_timestamp,
346
+ eventType: event.type,
347
+ type: event.type,
348
+ columns,
349
+ record: event.record ?? {},
350
+ old_record: event.old_record ?? {},
351
+ errors: event.errors ?? null,
352
+ },
353
+ },
354
+ ref: null,
355
+ });
356
+ }
357
+ }
358
+ }
359
+ bindingMatches(b, e) {
360
+ if (b.schema !== '*' && b.schema !== e.schema)
361
+ return false;
362
+ if (b.table !== '*' && b.table !== e.table)
363
+ return false;
364
+ if (b.event !== '*' && b.event !== e.type)
365
+ return false;
366
+ if (b.filter) {
367
+ const row = e.type === 'DELETE' ? e.old_record : e.record;
368
+ if (!row || !matchFilter(b.filter, row))
369
+ return false;
370
+ }
371
+ return true;
372
+ }
373
+ }
374
+ /** Evaluate a realtime filter string ("col=eq.value") against a row. */
375
+ export function matchFilter(filter, row) {
376
+ const m = filter.match(/^([^=]+)=(eq|neq|lt|lte|gt|gte|in)\.(.*)$/s);
377
+ if (!m)
378
+ return false;
379
+ const [, column, op, rawValue] = m;
380
+ const actual = row[column.trim()];
381
+ if (op === 'in') {
382
+ const list = rawValue.replace(/^\(/, '').replace(/\)$/, '').split(',').map((s) => s.trim());
383
+ return list.some((v) => looseEquals(actual, v));
384
+ }
385
+ if (op === 'eq')
386
+ return looseEquals(actual, rawValue);
387
+ if (op === 'neq')
388
+ return !looseEquals(actual, rawValue);
389
+ const a = Number(actual);
390
+ const b = Number(rawValue);
391
+ if (Number.isNaN(a) || Number.isNaN(b))
392
+ return false;
393
+ switch (op) {
394
+ case 'lt':
395
+ return a < b;
396
+ case 'lte':
397
+ return a <= b;
398
+ case 'gt':
399
+ return a > b;
400
+ case 'gte':
401
+ return a >= b;
402
+ }
403
+ return false;
404
+ }
405
+ function looseEquals(actual, expected) {
406
+ if (actual === null || actual === undefined)
407
+ return expected === 'null';
408
+ return String(actual) === expected;
409
+ }
410
+ function safeJsonParse(text) {
411
+ try {
412
+ return JSON.parse(text);
413
+ }
414
+ catch {
415
+ return null;
416
+ }
417
+ }
418
+ /**
419
+ * Phoenix binary serializer, kind 4 (userBroadcast):
420
+ * [4, topicLen, eventLen, metaLen, encoding, topic, event, meta, payloadBytes]
421
+ */
422
+ function encodeUserBroadcast(topic, event, payload) {
423
+ const enc = new TextEncoder();
424
+ const topicBytes = enc.encode(topic);
425
+ const eventBytes = enc.encode(event);
426
+ const out = new Uint8Array(5 + topicBytes.length + eventBytes.length + payload.length);
427
+ out[0] = 4;
428
+ out[1] = topicBytes.length;
429
+ out[2] = eventBytes.length;
430
+ out[3] = 0;
431
+ out[4] = 0;
432
+ out.set(topicBytes, 5);
433
+ out.set(eventBytes, 5 + topicBytes.length);
434
+ out.set(payload, 5 + topicBytes.length + eventBytes.length);
435
+ return out;
436
+ }
437
+ //# sourceMappingURL=engine.js.map