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.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/assets/logo.svg +18 -0
- package/dist/auth/handler.d.ts +43 -0
- package/dist/auth/handler.js +291 -0
- package/dist/auth/handler.js.map +1 -0
- package/dist/auth/password.d.ts +3 -0
- package/dist/auth/password.js +40 -0
- package/dist/auth/password.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +181 -0
- package/dist/cli.js.map +1 -0
- package/dist/db/bootstrap.d.ts +6 -0
- package/dist/db/bootstrap.js +213 -0
- package/dist/db/bootstrap.js.map +1 -0
- package/dist/db/database.d.ts +83 -0
- package/dist/db/database.js +270 -0
- package/dist/db/database.js.map +1 -0
- package/dist/db/engine.d.ts +30 -0
- package/dist/db/engine.js +28 -0
- package/dist/db/engine.js.map +1 -0
- package/dist/db/pglite-engine.d.ts +3 -0
- package/dist/db/pglite-engine.js +39 -0
- package/dist/db/pglite-engine.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +145 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt.d.ts +20 -0
- package/dist/jwt.js +72 -0
- package/dist/jwt.js.map +1 -0
- package/dist/node/bun-server.d.ts +8 -0
- package/dist/node/bun-server.js +41 -0
- package/dist/node/bun-server.js.map +1 -0
- package/dist/node/fs-driver.d.ts +11 -0
- package/dist/node/fs-driver.js +39 -0
- package/dist/node/fs-driver.js.map +1 -0
- package/dist/node/index.d.ts +7 -0
- package/dist/node/index.js +8 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/native/engine.d.ts +13 -0
- package/dist/node/native/engine.js +186 -0
- package/dist/node/native/engine.js.map +1 -0
- package/dist/node/native/wire.d.ts +38 -0
- package/dist/node/native/wire.js +345 -0
- package/dist/node/native/wire.js.map +1 -0
- package/dist/node/project.d.ts +6 -0
- package/dist/node/project.js +29 -0
- package/dist/node/project.js.map +1 -0
- package/dist/node/server-shared.d.ts +6 -0
- package/dist/node/server-shared.js +2 -0
- package/dist/node/server-shared.js.map +1 -0
- package/dist/node/server.d.ts +14 -0
- package/dist/node/server.js +83 -0
- package/dist/node/server.js.map +1 -0
- package/dist/node/ws.d.ts +8 -0
- package/dist/node/ws.js +165 -0
- package/dist/node/ws.js.map +1 -0
- package/dist/realtime/engine.d.ts +53 -0
- package/dist/realtime/engine.js +437 -0
- package/dist/realtime/engine.js.map +1 -0
- package/dist/rest/build.d.ts +53 -0
- package/dist/rest/build.js +505 -0
- package/dist/rest/build.js.map +1 -0
- package/dist/rest/errors.d.ts +2 -0
- package/dist/rest/errors.js +48 -0
- package/dist/rest/errors.js.map +1 -0
- package/dist/rest/handler.d.ts +11 -0
- package/dist/rest/handler.js +332 -0
- package/dist/rest/handler.js.map +1 -0
- package/dist/rest/parse.d.ts +69 -0
- package/dist/rest/parse.js +261 -0
- package/dist/rest/parse.js.map +1 -0
- package/dist/storage/driver.d.ts +9 -0
- package/dist/storage/driver.js +18 -0
- package/dist/storage/driver.js.map +1 -0
- package/dist/storage/handler.d.ts +49 -0
- package/dist/storage/handler.js +534 -0
- package/dist/storage/handler.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/package.json +59 -0
package/dist/node/ws.js
ADDED
|
@@ -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
|