@protobase/server 0.59.1 → 0.59.2
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/dist/channels/binary-frame.js +44 -0
- package/dist/channels/channel-protocol.js +16 -3
- package/dist/channels/channel-refusals.js +1 -1
- package/dist/channels/channel-routes.js +22 -9
- package/dist/channels/channel-sender.d.ts +1 -1
- package/dist/channels/channel-sender.js +24 -12
- package/dist/channels/channel-socket.js +12 -7
- package/dist/channels/database-sends.js +57 -0
- package/dist/channels/sends-prune.js +15 -0
- package/dist/channels/sends-schema-sql.js +30 -0
- package/dist/channels/sends-schema.d.ts +7 -0
- package/dist/channels/sends-schema.js +18 -0
- package/dist/channels/sent.js +17 -1
- package/dist/create-admin.d.ts +1 -1
- package/dist/create-admin.js +16 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/package.js +1 -1
- package/dist/realtime/database-broadcasts.js +2 -25
- package/dist/realtime/followed-poll.js +34 -0
- package/dist/realtime/realtime-prune.d.ts +5 -3
- package/dist/realtime/realtime-prune.js +8 -3
- package/dist/realtime/relay/realtime-options.d.ts +7 -0
- package/dist/realtime/relay/realtime-options.js +7 -1
- package/package.json +4 -4
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//#region src/channels/binary-frame.ts
|
|
2
|
+
const encoder = new TextEncoder();
|
|
3
|
+
const decoder = new TextDecoder();
|
|
4
|
+
const lengthBytes = 4;
|
|
5
|
+
/**
|
|
6
|
+
* A binary frame of `/api/channels`: the byte length of a JSON header, the header, then the bytes it carries. A binary
|
|
7
|
+
* broadcast travels as one, its header the message without its payload.
|
|
8
|
+
*/
|
|
9
|
+
const encodeBinaryFrame = (header, bytes) => {
|
|
10
|
+
const head = encoder.encode(JSON.stringify(header));
|
|
11
|
+
const frame = new Uint8Array(lengthBytes + head.length + bytes.byteLength);
|
|
12
|
+
new DataView(frame.buffer).setUint32(0, head.length);
|
|
13
|
+
frame.set(head, lengthBytes);
|
|
14
|
+
frame.set(new Uint8Array(bytes), lengthBytes + head.length);
|
|
15
|
+
return frame.buffer;
|
|
16
|
+
};
|
|
17
|
+
/** The header and bytes of a binary frame, or `undefined` for one cut short or whose header is not a JSON object. */
|
|
18
|
+
const decodeBinaryFrame = (frame) => {
|
|
19
|
+
if (frame.byteLength < lengthBytes) return;
|
|
20
|
+
const end = lengthBytes + new DataView(frame).getUint32(0);
|
|
21
|
+
if (end > frame.byteLength) return;
|
|
22
|
+
const header = parsed(decoder.decode(new Uint8Array(frame, lengthBytes, end - lengthBytes)));
|
|
23
|
+
if (typeof header !== "object" || header === null || Array.isArray(header)) return;
|
|
24
|
+
return {
|
|
25
|
+
header,
|
|
26
|
+
bytes: frame.slice(end)
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const parsed = (text) => {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(text);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error instanceof SyntaxError) return;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
/** The bytes of an `ArrayBuffer` or a typed array, as an `ArrayBuffer` of their own; `undefined` for anything else. */
|
|
38
|
+
const bytesOf = (value) => {
|
|
39
|
+
if (value instanceof ArrayBuffer) return value;
|
|
40
|
+
if (!ArrayBuffer.isView(value)) return;
|
|
41
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength).slice().buffer;
|
|
42
|
+
};
|
|
43
|
+
//#endregion
|
|
44
|
+
export { bytesOf, decodeBinaryFrame, encodeBinaryFrame };
|
|
@@ -1,9 +1,22 @@
|
|
|
1
|
+
import { decodeBinaryFrame } from "./binary-frame.js";
|
|
1
2
|
//#region src/channels/channel-protocol.ts
|
|
2
3
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
4
|
const text = (value) => typeof value === "string";
|
|
4
5
|
/** The message a text frame carries, or `undefined` for one that is not JSON or not a message. */
|
|
5
|
-
const decodeClientMessage = (frame) =>
|
|
6
|
-
|
|
6
|
+
const decodeClientMessage = (frame) => messageOf(parsed(frame));
|
|
7
|
+
/**
|
|
8
|
+
* The send of bytes a binary frame carries (`decodeBinaryFrame`): its header a `send` without a payload, its bytes the
|
|
9
|
+
* payload. `undefined` for any other frame.
|
|
10
|
+
*/
|
|
11
|
+
const decodeBinaryMessage = (frame) => {
|
|
12
|
+
const decoded = decodeBinaryFrame(frame);
|
|
13
|
+
if (!decoded || decoded.header.type !== "send" || "payload" in decoded.header) return;
|
|
14
|
+
return messageOf({
|
|
15
|
+
...decoded.header,
|
|
16
|
+
payload: decoded.bytes
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
const messageOf = (message) => {
|
|
7
20
|
if (!isRecord(message)) return;
|
|
8
21
|
if (message.ref !== void 0 && !text(message.ref)) return;
|
|
9
22
|
const { type } = message;
|
|
@@ -30,4 +43,4 @@ const parsed = (frame) => {
|
|
|
30
43
|
}
|
|
31
44
|
};
|
|
32
45
|
//#endregion
|
|
33
|
-
export { decodeClientMessage };
|
|
46
|
+
export { decodeBinaryMessage, decodeClientMessage };
|
|
@@ -10,7 +10,7 @@ const refusal = (code, message) => ({
|
|
|
10
10
|
*/
|
|
11
11
|
const channelRefusals = {
|
|
12
12
|
notFound: refusal("not-found", "There is no such record"),
|
|
13
|
-
invalidMessage: refusal("invalid-message", "Send a JSON text frame of a ping, token, join, leave, track, untrack or send"),
|
|
13
|
+
invalidMessage: refusal("invalid-message", "Send a JSON text frame of a ping, token, join, leave, track, untrack or send, or a binary frame of a send"),
|
|
14
14
|
notJoined: refusal("not-joined", "Join the channel first"),
|
|
15
15
|
alreadyJoined: refusal("already-joined", "This connection has joined the channel already"),
|
|
16
16
|
nameTooLong: refusal("name-too-long", "A channel name, its resource, a slash and its key, is at most 255 bytes"),
|
|
@@ -16,11 +16,30 @@ const withBearer = (request, token) => {
|
|
|
16
16
|
headers.set("authorization", `Bearer ${token}`);
|
|
17
17
|
return new Request(request, { headers });
|
|
18
18
|
};
|
|
19
|
-
const invalidBody = () => new HttpProblem(422, "invalid-broadcast", "Unprocessable Content", "Send { \"event\": \"…\", \"payload\": … } as application/json");
|
|
19
|
+
const invalidBody = () => new HttpProblem(422, "invalid-broadcast", "Unprocessable Content", "Send { \"event\": \"…\", \"payload\": … } as application/json, or bytes as application/octet-stream with ?event=…");
|
|
20
|
+
const broadcastOf = async (request) => {
|
|
21
|
+
if (request.header("content-type")?.split(";")[0].trim().toLowerCase() === "application/octet-stream") {
|
|
22
|
+
const event = request.query("event");
|
|
23
|
+
if (event === void 0) throw invalidBody();
|
|
24
|
+
return {
|
|
25
|
+
event,
|
|
26
|
+
payload: await request.arrayBuffer()
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const body = await request.json().catch((error) => {
|
|
30
|
+
if (error instanceof SyntaxError) throw invalidBody();
|
|
31
|
+
throw error;
|
|
32
|
+
});
|
|
33
|
+
if (!isRecord(body) || typeof body.event !== "string" || !("payload" in body)) throw invalidBody();
|
|
34
|
+
return {
|
|
35
|
+
event: body.event,
|
|
36
|
+
payload: body.payload
|
|
37
|
+
};
|
|
38
|
+
};
|
|
20
39
|
/**
|
|
21
40
|
* `GET /channels`, a WebSocket of channels named by records (see `channelSocket`), refused before the upgrade like
|
|
22
41
|
* any request its caller cannot make. `POST /channels/:resource/:key/broadcast` sends one broadcast without a socket,
|
|
23
|
-
* as the caller, when they may read the record.
|
|
42
|
+
* as the caller, when they may read the record: JSON, or bytes (`broadcastOf`).
|
|
24
43
|
*/
|
|
25
44
|
const channelRoutes = ({ deps, upgradeWebSocket, authenticate, authenticateUpgrade, onError, hubs, follow, report }) => {
|
|
26
45
|
const app = new Hono();
|
|
@@ -72,16 +91,10 @@ const channelRoutes = ({ deps, upgradeWebSocket, authenticate, authenticateUpgra
|
|
|
72
91
|
throw new HttpProblem(413, "too-large", "Payload Too Large", `A broadcast request is at most ${realtimeLimits.maxFrameBytes} bytes`);
|
|
73
92
|
}
|
|
74
93
|
}), async (c) => {
|
|
75
|
-
const body = await c.req.json().catch((error) => {
|
|
76
|
-
if (error instanceof SyntaxError) throw invalidBody();
|
|
77
|
-
throw error;
|
|
78
|
-
});
|
|
79
|
-
if (!isRecord(body) || typeof body.event !== "string" || !("payload" in body)) throw invalidBody();
|
|
80
94
|
const message = {
|
|
81
95
|
resource: c.req.param("resource"),
|
|
82
96
|
key: c.req.param("key"),
|
|
83
|
-
|
|
84
|
-
payload: body.payload
|
|
97
|
+
...await broadcastOf(c.req)
|
|
85
98
|
};
|
|
86
99
|
const record = broadcastRecord(deps.registry, message);
|
|
87
100
|
const session = c.get("session");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "../registry.js";
|
|
2
2
|
import "./channel-name.js";
|
|
3
3
|
//#region src/channels/channel-sender.d.ts
|
|
4
|
-
/** A broadcast to the channel of a record, from outside a connection. */
|
|
4
|
+
/** A broadcast to the channel of a record, from outside a connection: a JSON payload, or bytes in an `ArrayBuffer` or typed array. */
|
|
5
5
|
export type ChannelBroadcast = {
|
|
6
6
|
resource: string;
|
|
7
7
|
key: string;
|
|
@@ -2,31 +2,43 @@ import { HttpProblem, notFound } from "../problem.js";
|
|
|
2
2
|
import { fitsFrame } from "../realtime/broadcast-frame.js";
|
|
3
3
|
import { realtimeLimits } from "../realtime/realtime-limits.js";
|
|
4
4
|
import { channelRecord } from "./channel-name.js";
|
|
5
|
+
import { bytesOf } from "./binary-frame.js";
|
|
5
6
|
import { channelRefusals } from "./channel-refusals.js";
|
|
7
|
+
import { packSent } from "./sent.js";
|
|
6
8
|
import { Buffer } from "node:buffer";
|
|
7
9
|
//#region src/channels/channel-sender.ts
|
|
8
10
|
const invalid = (refusal) => new HttpProblem(422, refusal.code, "Unprocessable Content", refusal.message);
|
|
11
|
+
const invalidPayload = {
|
|
12
|
+
code: "invalid-payload",
|
|
13
|
+
message: `A payload is JSON or bytes of at most ${realtimeLimits.maxFrameBytes} bytes`
|
|
14
|
+
};
|
|
15
|
+
const payloadBytes = (payload) => {
|
|
16
|
+
if (payload instanceof ArrayBuffer || ArrayBuffer.isView(payload)) return payload.byteLength;
|
|
17
|
+
const json = JSON.stringify(payload);
|
|
18
|
+
return json === void 0 ? NaN : Buffer.byteLength(json);
|
|
19
|
+
};
|
|
9
20
|
/** The record a broadcast names, its event and payload checked, or a problem. */
|
|
10
21
|
const broadcastRecord = (registry, { resource, key, event, payload }) => {
|
|
11
22
|
const found = channelRecord(registry, resource, key);
|
|
12
23
|
if (found === "too-long") throw invalid(channelRefusals.nameTooLong);
|
|
13
24
|
if (!found) throw notFound(channelRefusals.notFound.message);
|
|
14
25
|
if (!fitsFrame(event)) throw invalid(channelRefusals.eventTooLong);
|
|
15
|
-
|
|
16
|
-
if (json === void 0 || Buffer.byteLength(json) > realtimeLimits.maxFrameBytes) throw invalid({
|
|
17
|
-
code: "invalid-payload",
|
|
18
|
-
message: `A payload is JSON of at most ${realtimeLimits.maxFrameBytes} bytes`
|
|
19
|
-
});
|
|
26
|
+
if (!(payloadBytes(payload) <= realtimeLimits.maxFrameBytes)) throw invalid(invalidPayload);
|
|
20
27
|
return found;
|
|
21
28
|
};
|
|
29
|
+
/** A checked broadcast as the hub publishes it; a typed array's bytes go as an `ArrayBuffer` of their own. */
|
|
30
|
+
const publicationTo = (record, event, sent) => ({
|
|
31
|
+
topic: record.name,
|
|
32
|
+
event,
|
|
33
|
+
payload: packSent({
|
|
34
|
+
...sent,
|
|
35
|
+
payload: bytesOf(sent.payload) ?? sent.payload
|
|
36
|
+
}),
|
|
37
|
+
private: false
|
|
38
|
+
});
|
|
22
39
|
/** Publishes a checked broadcast, refused with 429 when the channel took its broadcasts for this second. */
|
|
23
40
|
const publishTo = async (broadcasts, record, event, sent) => {
|
|
24
|
-
if (await broadcasts.publish([
|
|
25
|
-
topic: record.name,
|
|
26
|
-
event,
|
|
27
|
-
payload: sent,
|
|
28
|
-
private: false
|
|
29
|
-
}])) return;
|
|
41
|
+
if (await broadcasts.publish([publicationTo(record, event, sent)])) return;
|
|
30
42
|
throw new HttpProblem(429, channelRefusals.channelTooBusy.code, "Too Many Requests", channelRefusals.channelTooBusy.message);
|
|
31
43
|
};
|
|
32
44
|
/** The server's own sender over the record channels' hub. */
|
|
@@ -36,4 +48,4 @@ const channelSender = (registry, broadcasts) => ({ broadcast: async (message) =>
|
|
|
36
48
|
payload: message.payload
|
|
37
49
|
}) });
|
|
38
50
|
//#endregion
|
|
39
|
-
export { broadcastRecord, channelSender, publishTo };
|
|
51
|
+
export { broadcastRecord, channelSender, publicationTo, publishTo };
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { fitsFrame } from "../realtime/broadcast-frame.js";
|
|
2
2
|
import { realtimeLimits } from "../realtime/realtime-limits.js";
|
|
3
3
|
import { fixedWindow, fixedWindows } from "../realtime/fixed-window.js";
|
|
4
|
+
import { encodeBinaryFrame } from "./binary-frame.js";
|
|
4
5
|
import { channelRefusals } from "./channel-refusals.js";
|
|
6
|
+
import { packSent, senderOf, unpackSent } from "./sent.js";
|
|
5
7
|
import { channelBounds } from "./channel-bounds.js";
|
|
6
|
-
import { decodeClientMessage } from "./channel-protocol.js";
|
|
8
|
+
import { decodeBinaryMessage, decodeClientMessage } from "./channel-protocol.js";
|
|
7
9
|
import { entriesOf, entryDiff } from "./presence-entries.js";
|
|
8
|
-
import { senderOf } from "./sent.js";
|
|
9
10
|
import { randomUUID } from "node:crypto";
|
|
10
11
|
import { Buffer } from "node:buffer";
|
|
11
12
|
//#region src/channels/channel-socket.ts
|
|
@@ -56,6 +57,10 @@ const channelSocket = ({ send, buffered, close, session: first, reauthenticate,
|
|
|
56
57
|
const push = (message) => {
|
|
57
58
|
if (!open) return;
|
|
58
59
|
if (!(buffered() <= realtimeLimits.maxUnsentBytes)) return shut(policyViolation);
|
|
60
|
+
if (message.type === "broadcast" && message.payload instanceof ArrayBuffer) {
|
|
61
|
+
const { payload, ...header } = message;
|
|
62
|
+
return send(encodeBinaryFrame(header, payload));
|
|
63
|
+
}
|
|
59
64
|
send(JSON.stringify(message));
|
|
60
65
|
};
|
|
61
66
|
const ok = (ref, channel) => {
|
|
@@ -105,7 +110,7 @@ const channelSocket = ({ send, buffered, close, session: first, reauthenticate,
|
|
|
105
110
|
const member = {
|
|
106
111
|
self: false,
|
|
107
112
|
private: false,
|
|
108
|
-
receive: (event, payload) => hear(name, event, payload)
|
|
113
|
+
receive: (event, payload) => hear(name, event, unpackSent(payload))
|
|
109
114
|
};
|
|
110
115
|
const seat = presences.join(name, {
|
|
111
116
|
key: randomUUID(),
|
|
@@ -161,7 +166,7 @@ const channelSocket = ({ send, buffered, close, session: first, reauthenticate,
|
|
|
161
166
|
user: userOf(session),
|
|
162
167
|
payload: message.payload
|
|
163
168
|
};
|
|
164
|
-
if (!await broadcasts.send(message.channel, channel.member, message.event, sent)) return refuse(message.ref, channelRefusals.channelTooBusy);
|
|
169
|
+
if (!await broadcasts.send(message.channel, channel.member, message.event, packSent(sent))) return refuse(message.ref, channelRefusals.channelTooBusy);
|
|
165
170
|
if (message.self === true) hear(message.channel, message.event, sent);
|
|
166
171
|
if (message.ack === true) ok(message.ref);
|
|
167
172
|
};
|
|
@@ -224,15 +229,15 @@ const channelSocket = ({ send, buffered, close, session: first, reauthenticate,
|
|
|
224
229
|
};
|
|
225
230
|
return {
|
|
226
231
|
/**
|
|
227
|
-
* Handles a
|
|
228
|
-
* over the size cap, or of a kind whose size is unknown, closes the socket.
|
|
232
|
+
* Handles a frame after those before it: a text one, or a binary `ArrayBuffer` of a send of bytes; one that is not
|
|
233
|
+
* a message gets an error. A frame over the size cap, or of a kind whose size is unknown, closes the socket.
|
|
229
234
|
*/
|
|
230
235
|
message: (frame) => {
|
|
231
236
|
if (!open) return;
|
|
232
237
|
const size = frameBytes(frame);
|
|
233
238
|
if (!(size <= realtimeLimits.maxFrameBytes)) return shut(messageTooBig);
|
|
234
239
|
stayAwake();
|
|
235
|
-
const message = typeof frame === "string" ? decodeClientMessage(frame) : void 0;
|
|
240
|
+
const message = typeof frame === "string" ? decodeClientMessage(frame) : frame instanceof ArrayBuffer ? decodeBinaryMessage(frame) : void 0;
|
|
236
241
|
if (!message) return push({
|
|
237
242
|
type: "error",
|
|
238
243
|
error: channelRefusals.invalidMessage
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { HttpProblem } from "../problem.js";
|
|
2
|
+
import { writeTransaction } from "../transactions.js";
|
|
3
|
+
import { followedPoll } from "../realtime/followed-poll.js";
|
|
4
|
+
import { broadcastRecord, publicationTo } from "./channel-sender.js";
|
|
5
|
+
import { sendsSchema } from "./sends-schema-sql.js";
|
|
6
|
+
import { sql } from "kysely";
|
|
7
|
+
//#region src/channels/database-sends.ts
|
|
8
|
+
/** Sends taken in one transaction at most. */
|
|
9
|
+
const batchSize = 1e3;
|
|
10
|
+
/**
|
|
11
|
+
* The broadcast a row sends, as the server, under the checks of `channels.broadcast` in API functions; `undefined` for
|
|
12
|
+
* one they refuse, such as for a record the app does not have or a payload over a frame, which is dropped.
|
|
13
|
+
*/
|
|
14
|
+
const publicationOfSend = (registry, row) => {
|
|
15
|
+
try {
|
|
16
|
+
const record = broadcastRecord(registry, row);
|
|
17
|
+
return publicationTo(record, row.event, {
|
|
18
|
+
user: null,
|
|
19
|
+
server: true,
|
|
20
|
+
payload: row.payload
|
|
21
|
+
});
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error instanceof HttpProblem) return;
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const takeSends = (db, statementTimeoutMs) => writeTransaction(db, statementTimeoutMs, async (trx) => {
|
|
28
|
+
const { rows } = await sql`with taken as (
|
|
29
|
+
delete from ${sql.id(sendsSchema, "sends")} where id in (
|
|
30
|
+
select id from ${sql.id(sendsSchema, "sends")} order by id limit ${batchSize} for update skip locked
|
|
31
|
+
) returning id, resource, key, event, payload
|
|
32
|
+
)
|
|
33
|
+
select id::text as id, resource, key, event, payload from taken order by id`.execute(trx);
|
|
34
|
+
return rows;
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* SQL's broadcasts to the record channels: `channels.send()` inserts a row in the caller's transaction, so only a
|
|
38
|
+
* committed one is seen, and while a record channel's socket of this process is open the table is read every
|
|
39
|
+
* `intervalMs`, its rows taken out of it and sent to the channels of their records, as the server, under the channel's
|
|
40
|
+
* broadcast rate. A send nobody hears is dropped, as is one the checks refuse. With several processes, one takes a row
|
|
41
|
+
* and the relay carries it to the others.
|
|
42
|
+
*/
|
|
43
|
+
const databaseSends = ({ db, statementTimeoutMs, registry, broadcasts, intervalMs = 1e3, report }) => {
|
|
44
|
+
const read = async () => {
|
|
45
|
+
for (;;) {
|
|
46
|
+
const rows = await takeSends(db, statementTimeoutMs);
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
const publication = publicationOfSend(registry, row);
|
|
49
|
+
if (publication) await broadcasts.publish([publication]);
|
|
50
|
+
}
|
|
51
|
+
if (rows.length < batchSize) return;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
return followedPoll(read, intervalMs, report);
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
export { databaseSends, publicationOfSend };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { writeTransaction } from "../transactions.js";
|
|
2
|
+
import { sendsSchema } from "./sends-schema-sql.js";
|
|
3
|
+
import { inBatches, pruneBatchSize } from "../realtime/broadcast-prune.js";
|
|
4
|
+
import { sql } from "kysely";
|
|
5
|
+
//#region src/channels/sends-prune.ts
|
|
6
|
+
const deleteOldSends = (db, statementTimeoutMs, limit) => writeTransaction(db, statementTimeoutMs, async (trx) => {
|
|
7
|
+
const { numAffectedRows } = await sql`delete from ${sql.id(sendsSchema, "sends")} where id in (
|
|
8
|
+
select id from ${sql.id(sendsSchema, "sends")} where sent_at < now() - interval '48 hours' limit ${limit} for update skip locked
|
|
9
|
+
)`.execute(trx);
|
|
10
|
+
return Number(numAffectedRows ?? 0n);
|
|
11
|
+
});
|
|
12
|
+
/** Deletes the sends of `channels.sends` more than 48 hours old, which no reader took because no socket was open, in batches. */
|
|
13
|
+
const pruneSends = (db, statementTimeoutMs) => inBatches(pruneBatchSize, (limit) => deleteOldSends(db, statementTimeoutMs, limit));
|
|
14
|
+
//#endregion
|
|
15
|
+
export { pruneSends };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/channels/sends-schema-sql.ts
|
|
2
|
+
/** The schema of SQL's broadcasts to the record channels. */
|
|
3
|
+
const sendsSchema = "channels";
|
|
4
|
+
/**
|
|
5
|
+
* - `sends`: each broadcast SQL sent, inserted in the sender's transaction, taken out by a serving process once
|
|
6
|
+
* committed, or pruned after 48 hours when none read it.
|
|
7
|
+
* - `send(resource, key, event, payload)`: inserts one. It runs as its owner, the app's role, which alone may call it,
|
|
8
|
+
* so that no other role, such as one of `/api/sb/rest/v1`'s, sends as the server; a trigger function that sends for
|
|
9
|
+
* them is `security definer`.
|
|
10
|
+
*/
|
|
11
|
+
const sendsSchemaSql = `create schema if not exists ${sendsSchema};
|
|
12
|
+
|
|
13
|
+
create table if not exists ${sendsSchema}.sends (
|
|
14
|
+
id bigint generated always as identity primary key,
|
|
15
|
+
resource text not null,
|
|
16
|
+
key text not null,
|
|
17
|
+
event text not null,
|
|
18
|
+
payload jsonb not null,
|
|
19
|
+
sent_at timestamptz not null default now()
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
create index if not exists sends_sent_at on ${sendsSchema}.sends (sent_at);
|
|
23
|
+
|
|
24
|
+
create or replace function ${sendsSchema}.send(resource text, key text, event text, payload jsonb) returns void
|
|
25
|
+
language sql security definer set search_path = ''
|
|
26
|
+
as $$ insert into ${sendsSchema}.sends (resource, key, event, payload) values (send.resource, send.key, send.event, send.payload) $$;
|
|
27
|
+
|
|
28
|
+
revoke execute on function ${sendsSchema}.send(text, text, text, jsonb) from public;`;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { sendsSchema, sendsSchemaSql };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Db } from "@protobase/query";
|
|
2
|
+
//#region src/channels/sends-schema.d.ts
|
|
3
|
+
/** The SQL that adds the `channels` schema `realtime: { sqlSends: true }` needs; `undefined` when it is there. */
|
|
4
|
+
export declare const channelSendsMigration: (db: Db) => Promise<"create schema if not exists channels;\n\ncreate table if not exists channels.sends (\n id bigint generated always as identity primary key,\n resource text not null,\n key text not null,\n event text not null,\n payload jsonb not null,\n sent_at timestamptz not null default now()\n);\n\ncreate index if not exists sends_sent_at on channels.sends (sent_at);\n\ncreate or replace function channels.send(resource text, key text, event text, payload jsonb) returns void\n language sql security definer set search_path = ''\n as $$ insert into channels.sends (resource, key, event, payload) values (send.resource, send.key, send.event, send.payload) $$;\n\nrevoke execute on function channels.send(text, text, text, jsonb) from public;" | undefined>;
|
|
5
|
+
/** Refuses to go on while the `channels` schema is missing; the project's migrations add it. */
|
|
6
|
+
export declare const checkChannelSends: (db: Db) => Promise<void>;
|
|
7
|
+
//#endregion
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { sendsSchema, sendsSchemaSql } from "./sends-schema-sql.js";
|
|
2
|
+
import { migrationCommand } from "../changes/capture-schema.js";
|
|
3
|
+
import { sql } from "kysely";
|
|
4
|
+
//#region src/channels/sends-schema.ts
|
|
5
|
+
const sendsCurrent = async (db) => {
|
|
6
|
+
const { rows } = await sql`select to_regclass(${`${sendsSchema}.sends`}) is not null
|
|
7
|
+
and to_regprocedure(${`${sendsSchema}.send(text, text, text, jsonb)`}) is not null as found`.execute(db);
|
|
8
|
+
return rows[0].found;
|
|
9
|
+
};
|
|
10
|
+
/** The SQL that adds the `channels` schema `realtime: { sqlSends: true }` needs; `undefined` when it is there. */
|
|
11
|
+
const channelSendsMigration = async (db) => await sendsCurrent(db) ? void 0 : sendsSchemaSql;
|
|
12
|
+
/** Refuses to go on while the `channels` schema is missing; the project's migrations add it. */
|
|
13
|
+
const checkChannelSends = async (db) => {
|
|
14
|
+
if (await sendsCurrent(db)) return;
|
|
15
|
+
throw new Error(`The ${sendsSchema} schema that realtime.sqlSends needs is missing. Write it as a migration with \`${migrationCommand}\`, apply it with the project's migrations, then start again.`);
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { channelSendsMigration, checkChannelSends };
|
package/dist/channels/sent.js
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
+
import { decodeBinaryFrame, encodeBinaryFrame } from "./binary-frame.js";
|
|
1
2
|
//#region src/channels/sent.ts
|
|
2
3
|
/** The sender as a broadcast message names it. */
|
|
3
4
|
const senderOf = (sent) => ({
|
|
4
5
|
user: sent.user,
|
|
5
6
|
...sent.server && { server: true }
|
|
6
7
|
});
|
|
8
|
+
/**
|
|
9
|
+
* A broadcast as the hub's payload. One of bytes goes as one binary frame, its sender the header, since the relay
|
|
10
|
+
* carries a payload of bytes as they are and any other as JSON, which has no bytes.
|
|
11
|
+
*/
|
|
12
|
+
const packSent = (sent) => sent.payload instanceof ArrayBuffer ? encodeBinaryFrame(senderOf(sent), sent.payload) : sent;
|
|
13
|
+
/** The broadcast a hub's payload carries (`packSent`). */
|
|
14
|
+
const unpackSent = (packed) => {
|
|
15
|
+
if (!(packed instanceof ArrayBuffer)) return packed;
|
|
16
|
+
const { header, bytes } = decodeBinaryFrame(packed);
|
|
17
|
+
return {
|
|
18
|
+
user: header.user,
|
|
19
|
+
...header.server === true && { server: true },
|
|
20
|
+
payload: bytes
|
|
21
|
+
};
|
|
22
|
+
};
|
|
7
23
|
//#endregion
|
|
8
|
-
export { senderOf };
|
|
24
|
+
export { packSent, senderOf, unpackSent };
|
package/dist/create-admin.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export type CreateAdminInput = {
|
|
|
51
51
|
subscriptions?: SubscriptionDefinition[];
|
|
52
52
|
/** Webhook subscriptions made in the admin app, from `webhooks: {}` (see `WebhooksOptions`). */
|
|
53
53
|
webhooks?: WebhooksOptions;
|
|
54
|
-
/** How `/api/sb/realtime/v1`
|
|
54
|
+
/** How `/api/sb/realtime/v1` and `/api/channels` are served: by one process or several, and whether SQL sends to channels (see `RealtimeOptions`). */
|
|
55
55
|
realtime?: RealtimeOptions;
|
|
56
56
|
/** Each user's devices and the endpoints that register them, from `notifications: {}` (see `NotificationsOptions`). */
|
|
57
57
|
notifications?: NotificationsOptions;
|
package/dist/create-admin.js
CHANGED
|
@@ -17,10 +17,11 @@ import { apiKeySettings } from "./api-keys/api-key-settings.js";
|
|
|
17
17
|
import { apiKeyCaller } from "./api-keys/resolve-caller.js";
|
|
18
18
|
import { jwtVerifier } from "./auth/jwt.js";
|
|
19
19
|
import { realtimeApp } from "./realtime/realtime-app.js";
|
|
20
|
-
import { relayed } from "./realtime/relay/realtime-options.js";
|
|
20
|
+
import { relayed, sqlSends } from "./realtime/relay/realtime-options.js";
|
|
21
21
|
import { realtimeHubs } from "./realtime/realtime-hubs.js";
|
|
22
22
|
import { channelSender } from "./channels/channel-sender.js";
|
|
23
23
|
import { channelRoutes } from "./channels/channel-routes.js";
|
|
24
|
+
import { databaseSends } from "./channels/database-sends.js";
|
|
24
25
|
import { startRealtimePrune } from "./realtime/realtime-prune.js";
|
|
25
26
|
import { restRoutes } from "./rest/rest-routes.js";
|
|
26
27
|
import { storageRoutes } from "./storage/storage-routes.js";
|
|
@@ -399,6 +400,18 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
399
400
|
}));
|
|
400
401
|
if (upgradeWebSocket) {
|
|
401
402
|
const visitors = (caller) => withAnonymous(caller, options.roles);
|
|
403
|
+
const sends = sqlSends(realtime) ? databaseSends({
|
|
404
|
+
db,
|
|
405
|
+
statementTimeoutMs: deps.statementTimeoutMs,
|
|
406
|
+
registry,
|
|
407
|
+
broadcasts: hubs.records.broadcasts,
|
|
408
|
+
report
|
|
409
|
+
}) : void 0;
|
|
410
|
+
const follow = () => {
|
|
411
|
+
const unrelay = hubs.follow();
|
|
412
|
+
const unsend = sends?.follow();
|
|
413
|
+
return () => (unsend?.(), unrelay());
|
|
414
|
+
};
|
|
402
415
|
app.route(systemPath || "/", channelRoutes({
|
|
403
416
|
deps,
|
|
404
417
|
upgradeWebSocket,
|
|
@@ -406,7 +419,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
406
419
|
authenticateUpgrade: visitors(upgradeAuthenticate),
|
|
407
420
|
onError,
|
|
408
421
|
hubs: hubs.records,
|
|
409
|
-
follow
|
|
422
|
+
follow,
|
|
410
423
|
report
|
|
411
424
|
}));
|
|
412
425
|
}
|
|
@@ -490,6 +503,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
490
503
|
db,
|
|
491
504
|
statementTimeoutMs: deps.statementTimeoutMs,
|
|
492
505
|
relayed: relayed(realtime),
|
|
506
|
+
sqlSends: sqlSends(realtime),
|
|
493
507
|
report
|
|
494
508
|
});
|
|
495
509
|
return Object.assign(app, {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MobileConfig } from "./mobile-config.js";
|
|
2
2
|
import { AppIdentity } from "./app-identity.js";
|
|
3
3
|
import { AppIcons, PngIcon, pngIcons } from "./web-app/app-icons.js";
|
|
4
|
-
import { RealtimeOptions, relayed } from "./realtime/relay/realtime-options.js";
|
|
4
|
+
import { RealtimeOptions, relayed, sqlSends } from "./realtime/relay/realtime-options.js";
|
|
5
5
|
import { EventsOptions } from "./changes/events-options.js";
|
|
6
6
|
import { DecryptField, FieldDecrypter, FieldReader, RecordKey } from "./encryption/decrypt-field-types.js";
|
|
7
7
|
import { StandardSchema, readJson } from "./functions/read-json.js";
|
|
@@ -101,6 +101,7 @@ import { parseColumns, readOutbox } from "./changes/outbox.js";
|
|
|
101
101
|
import { newWebhookSecret } from "./webhooks/standard-webhooks.js";
|
|
102
102
|
import { checkWebhooksSchema, webhooksMigration } from "./webhooks/table/schema.js";
|
|
103
103
|
import { checkRealtimeRelay, realtimeRelayMigration } from "./realtime/relay/relay-schema.js";
|
|
104
|
+
import { channelSendsMigration, checkChannelSends } from "./channels/sends-schema.js";
|
|
104
105
|
import { checkComments, commentsMigration } from "./comments/comments-sql.js";
|
|
105
106
|
import { CheckAiInput, checkAi } from "./ai/check-ai.js";
|
|
106
107
|
import { CommentItem } from "./comments/comment-item.js";
|
|
@@ -175,4 +176,4 @@ import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/ru
|
|
|
175
176
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
176
177
|
import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
|
|
177
178
|
import { AdminAuth, CreateAuthOptions, UserCreatedHook, normalizeRoles } from "./better-auth/create-auth.js";
|
|
178
|
-
export { type Admin, type AdminAuth, type AdminOptions, type AggregateParams, type ApiFunction, type ApiKeys, type ApiKeysOptions, type ApnsEnvironment, type AppIcons, type AppIdentity, type AppPoolConfig, type ApplyOptions, type ApplyResult, type ApprovalAnswer, type ApprovalRequest, type ArgumentsPayload, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type AuthHooks, type AuthSettings, type AuthenticateSettings, type Authenticator, type BaseToolContext, type BatchOperation, type BatchResult, type BetterAuthAuthenticator, type BetterAuthAuthenticatorOptions, type CallBackend, type CallMember, type Caller, type CallerOptions, type CallsAccess, type CallsOptions, type CapturedRow, type CascadingKey, type ChannelBroadcast, type ChannelSender, type ChatMessage, type ChatTextPart, type CheckAiInput, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type CommentItem, type CommentToolsInput, type CommentUsers, type Completion, type CompletionOptions, ConnectionsBusy, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DecryptField, type Device, type DeviceColumn, type DeviceKind, type DeviceStore, DeviceStoreError, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EncryptionStatus, type EncryptionTools, type EncryptionToolsInput, type EnqueueJob, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FieldApplied, type FieldDecrypter, type FieldReader, type FieldStatus, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileProviderSettings, type FileRef, type FileStore, type FileVariantObject, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSocket, type FunctionSource, type HistoryColumn, type HistoryOptions, type HistoryPosition, type HistoryStore, HistoryStoreError, type HookContext, HttpProblem, type IssueTokenOptions, type JobContext, type JobDeclaration, type JobDefinition, type JobName, type JobOptions, type JobPayload, type JobRun, type JobsRuntime, type JsonObject, type JsonValue, type JwtOptions, type JwtVerifyOptions, type KeyFunctionContext, type LinkedAccount, type ListParams, type LiveKitBackend, type LocalFilesOptions, type MailMessage, type Mailer, type MapLayer, type MapOptions, type MapSettings, type McpApprovals, type McpGateInput, type McpHandlerInput, type McpRoutesInput, type McpServerInput, type McpToolsInput, type McpWrites, type MemberInput, type Membership, MissingCredentials, type MobileConfig, ModelError, type ModelSettings, type NewDevice, type NewNotification, type NewOrganization, type NewUser, type NotificationRecipients, NotificationSendError, type NotificationsOptions, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PgModule, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type PngIcon, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type PushAnswer, type PushMessage, PushMessageError, type PushSender, type QueryOptions, type QueryResult, type ReadableResources, type RealtimeOptions, type RealtimePrune, type ReasoningDetail, type ReasoningEffort, type RebindOption, type RecordGroups, type RecordKey, type RecordKeyInput, type RecordPage, type RenderedEmail, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type S3Bucket, type S3FilesOptions, type S3ListPage, type S3Object, type ScanGuardMode, type ScheduledDelete, type SendNotification, type Session, type SessionOrganization, type Sharp, type SignInPolicy, type SignInRule, type SignatureOptions, type SocketEvents, type SourceOption, type SourceRef, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoreFileInput, type StoredMember, type StoredNotification, type StoredObject, type StoredOrganization, type StoredUser, type StrayValues, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolAnnotations, type ToolCall, type ToolComment, type ToolCommentPage, type ToolComments, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type UnreadableEntry, type Urgency, type User, type UserCreatedHook, type VapidSettings, type ViewSource, type WebPushFields, type WebPushOptions, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, type WriteRecordToolsInput, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, capSocketMessages, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, describeResourceTool, deviceIdParam, deviceStore, devicesPath, enqueueJob, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, generateVapidKeys, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, historyIdParam, historyIdPattern, historyLimits, historyPath, historyStore, honorSocketMessageCaps, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listCommentsTool, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpIcons, mcpInstructions, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, refusalOf, refused, relayed, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
|
179
|
+
export { type Admin, type AdminAuth, type AdminOptions, type AggregateParams, type ApiFunction, type ApiKeys, type ApiKeysOptions, type ApnsEnvironment, type AppIcons, type AppIdentity, type AppPoolConfig, type ApplyOptions, type ApplyResult, type ApprovalAnswer, type ApprovalRequest, type ArgumentsPayload, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type AuthHooks, type AuthSettings, type AuthenticateSettings, type Authenticator, type BaseToolContext, type BatchOperation, type BatchResult, type BetterAuthAuthenticator, type BetterAuthAuthenticatorOptions, type CallBackend, type CallMember, type Caller, type CallerOptions, type CallsAccess, type CallsOptions, type CapturedRow, type CascadingKey, type ChannelBroadcast, type ChannelSender, type ChatMessage, type ChatTextPart, type CheckAiInput, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type CommentItem, type CommentToolsInput, type CommentUsers, type Completion, type CompletionOptions, ConnectionsBusy, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DecryptField, type Device, type DeviceColumn, type DeviceKind, type DeviceStore, DeviceStoreError, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EncryptionStatus, type EncryptionTools, type EncryptionToolsInput, type EnqueueJob, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FieldApplied, type FieldDecrypter, type FieldReader, type FieldStatus, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileProviderSettings, type FileRef, type FileStore, type FileVariantObject, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSocket, type FunctionSource, type HistoryColumn, type HistoryOptions, type HistoryPosition, type HistoryStore, HistoryStoreError, type HookContext, HttpProblem, type IssueTokenOptions, type JobContext, type JobDeclaration, type JobDefinition, type JobName, type JobOptions, type JobPayload, type JobRun, type JobsRuntime, type JsonObject, type JsonValue, type JwtOptions, type JwtVerifyOptions, type KeyFunctionContext, type LinkedAccount, type ListParams, type LiveKitBackend, type LocalFilesOptions, type MailMessage, type Mailer, type MapLayer, type MapOptions, type MapSettings, type McpApprovals, type McpGateInput, type McpHandlerInput, type McpRoutesInput, type McpServerInput, type McpToolsInput, type McpWrites, type MemberInput, type Membership, MissingCredentials, type MobileConfig, ModelError, type ModelSettings, type NewDevice, type NewNotification, type NewOrganization, type NewUser, type NotificationRecipients, NotificationSendError, type NotificationsOptions, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PgModule, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type PngIcon, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type PushAnswer, type PushMessage, PushMessageError, type PushSender, type QueryOptions, type QueryResult, type ReadableResources, type RealtimeOptions, type RealtimePrune, type ReasoningDetail, type ReasoningEffort, type RebindOption, type RecordGroups, type RecordKey, type RecordKeyInput, type RecordPage, type RenderedEmail, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type S3Bucket, type S3FilesOptions, type S3ListPage, type S3Object, type ScanGuardMode, type ScheduledDelete, type SendNotification, type Session, type SessionOrganization, type Sharp, type SignInPolicy, type SignInRule, type SignatureOptions, type SocketEvents, type SourceOption, type SourceRef, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoreFileInput, type StoredMember, type StoredNotification, type StoredObject, type StoredOrganization, type StoredUser, type StrayValues, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolAnnotations, type ToolCall, type ToolComment, type ToolCommentPage, type ToolComments, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type UnreadableEntry, type Urgency, type User, type UserCreatedHook, type VapidSettings, type ViewSource, type WebPushFields, type WebPushOptions, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, type WriteRecordToolsInput, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, capSocketMessages, changesMigration, channelSendsMigration, checkAi, checkAuthSchema, checkChanges, checkChannelSends, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, describeResourceTool, deviceIdParam, deviceStore, devicesPath, enqueueJob, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, generateVapidKeys, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, historyIdParam, historyIdPattern, historyLimits, historyPath, historyStore, honorSocketMessageCaps, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listCommentsTool, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpIcons, mcpInstructions, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, refusalOf, refused, relayed, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, sqlSends, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@ import { apiKeySettings, apiKeyVariables } from "./api-keys/api-key-settings.js"
|
|
|
25
25
|
import { apiKeyCaller } from "./api-keys/resolve-caller.js";
|
|
26
26
|
import { jwtAuthenticator, jwtVerifier } from "./auth/jwt.js";
|
|
27
27
|
import { realtimeLimits } from "./realtime/realtime-limits.js";
|
|
28
|
-
import { relayed } from "./realtime/relay/realtime-options.js";
|
|
28
|
+
import { relayed, sqlSends } from "./realtime/relay/realtime-options.js";
|
|
29
29
|
import { backgroundSettled, waitUntil } from "./functions/wait-until.js";
|
|
30
30
|
import { objectKey } from "./storage/object-key.js";
|
|
31
31
|
import { appNameOver, defaultAppName, shownAppName } from "./app-name.js";
|
|
@@ -109,6 +109,7 @@ import { checkResourcesOutsideAuthStore } from "./better-auth/store/resources-ou
|
|
|
109
109
|
import { checkComputed, computedMigration } from "./computed/computed-schema.js";
|
|
110
110
|
import { checkWebhooksSchema, webhooksMigration } from "./webhooks/table/schema.js";
|
|
111
111
|
import { checkRealtimeRelay, realtimeRelayMigration } from "./realtime/relay/relay-schema.js";
|
|
112
|
+
import { channelSendsMigration, checkChannelSends } from "./channels/sends-schema.js";
|
|
112
113
|
import { checkAi } from "./ai/check-ai.js";
|
|
113
114
|
import { checkNotificationsSchema, notificationsMigration } from "./notifications/schema.js";
|
|
114
115
|
import { sendNotification } from "./notifications/send-notification.js";
|
|
@@ -127,4 +128,4 @@ import { enqueueJob } from "./jobs/declared-job.js";
|
|
|
127
128
|
import { defineEmail } from "./mail/define-email.js";
|
|
128
129
|
import { startJobs } from "./jobs/runtime.js";
|
|
129
130
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
130
|
-
export { ConnectionsBusy, DeviceStoreError, FatalJobError, HistoryStoreError, HttpProblem, MissingCredentials, ModelError, NotificationSendError, PushMessageError, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, capSocketMessages, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, describeResourceTool, deviceIdParam, deviceStore, devicesPath, enqueueJob, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, generateVapidKeys, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, historyIdParam, historyIdPattern, historyLimits, historyPath, historyStore, honorSocketMessageCaps, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listCommentsTool, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpIcons, mcpInstructions, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, refusalOf, refused, relayed, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
|
131
|
+
export { ConnectionsBusy, DeviceStoreError, FatalJobError, HistoryStoreError, HttpProblem, MissingCredentials, ModelError, NotificationSendError, PushMessageError, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, capSocketMessages, changesMigration, channelSendsMigration, checkAi, checkAuthSchema, checkChanges, checkChannelSends, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, describeResourceTool, deviceIdParam, deviceStore, devicesPath, enqueueJob, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, generateVapidKeys, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, historyIdParam, historyIdPattern, historyLimits, historyPath, historyStore, honorSocketMessageCaps, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listCommentsTool, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpIcons, mcpInstructions, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, refusalOf, refused, relayed, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, sqlSends, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
package/dist/package.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { writeTransaction } from "../transactions.js";
|
|
2
2
|
import { fitsFrame } from "./broadcast-frame.js";
|
|
3
|
+
import { followedPoll } from "./followed-poll.js";
|
|
3
4
|
import { realtimeLimits } from "./realtime-limits.js";
|
|
4
5
|
import { sql } from "kysely";
|
|
5
6
|
import { Buffer } from "node:buffer";
|
|
@@ -43,8 +44,6 @@ const messagesTableExists = async (db) => (await sql`select to_regclass('realtim
|
|
|
43
44
|
* `realtime.messages` sends nothing, and is checked again at each read.
|
|
44
45
|
*/
|
|
45
46
|
const databaseBroadcasts = ({ db, statementTimeoutMs, broadcasts, intervalMs = 1e3, report }) => {
|
|
46
|
-
let followers = 0;
|
|
47
|
-
let running = false;
|
|
48
47
|
const read = async () => {
|
|
49
48
|
if (!await messagesTableExists(db)) return;
|
|
50
49
|
for (;;) {
|
|
@@ -56,29 +55,7 @@ const databaseBroadcasts = ({ db, statementTimeoutMs, broadcasts, intervalMs = 1
|
|
|
56
55
|
if (rows.length < batchSize) return;
|
|
57
56
|
}
|
|
58
57
|
};
|
|
59
|
-
|
|
60
|
-
if (followers === 0) {
|
|
61
|
-
running = false;
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
await read().catch(report);
|
|
65
|
-
setTimeout(() => void tick(), intervalMs);
|
|
66
|
-
};
|
|
67
|
-
return {
|
|
68
|
-
/** Reads the table from now on, until the returned function is called; once for each call, however often. */
|
|
69
|
-
follow: () => {
|
|
70
|
-
followers += 1;
|
|
71
|
-
if (!running) {
|
|
72
|
-
running = true;
|
|
73
|
-
tick();
|
|
74
|
-
}
|
|
75
|
-
let followed = true;
|
|
76
|
-
return () => {
|
|
77
|
-
if (!followed) return;
|
|
78
|
-
followed = false;
|
|
79
|
-
followers -= 1;
|
|
80
|
-
};
|
|
81
|
-
} };
|
|
58
|
+
return followedPoll(read, intervalMs, report);
|
|
82
59
|
};
|
|
83
60
|
//#endregion
|
|
84
61
|
export { databaseBroadcasts, messagesTableExists, publicationOfRow };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/realtime/followed-poll.ts
|
|
2
|
+
/**
|
|
3
|
+
* Runs `read` every `intervalMs` while anyone follows, starting at once with the first follower; a failed read goes to
|
|
4
|
+
* `report`, and the next one tries again.
|
|
5
|
+
*/
|
|
6
|
+
const followedPoll = (read, intervalMs, report) => {
|
|
7
|
+
let followers = 0;
|
|
8
|
+
let running = false;
|
|
9
|
+
const tick = async () => {
|
|
10
|
+
if (followers === 0) {
|
|
11
|
+
running = false;
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
await read().catch(report);
|
|
15
|
+
setTimeout(() => void tick(), intervalMs);
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
/** Reads from now on, until the returned function is called; once for each call, however often. */
|
|
19
|
+
follow: () => {
|
|
20
|
+
followers += 1;
|
|
21
|
+
if (!running) {
|
|
22
|
+
running = true;
|
|
23
|
+
tick();
|
|
24
|
+
}
|
|
25
|
+
let followed = true;
|
|
26
|
+
return () => {
|
|
27
|
+
if (!followed) return;
|
|
28
|
+
followed = false;
|
|
29
|
+
followers -= 1;
|
|
30
|
+
};
|
|
31
|
+
} };
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { followedPoll };
|
|
@@ -5,15 +5,17 @@ export type RealtimePruneOptions = {
|
|
|
5
5
|
statementTimeoutMs: number;
|
|
6
6
|
/** Whether the app's processes relay through `realtime_relay`, whose frames and counts are pruned too. */
|
|
7
7
|
relayed: boolean;
|
|
8
|
+
/** Whether SQL sends to the record channels through `channels.sends`, whose old rows are pruned too. */
|
|
9
|
+
sqlSends: boolean;
|
|
8
10
|
/** Where a failed prune goes; the next one tries again. */
|
|
9
11
|
report: (error: unknown) => void;
|
|
10
12
|
};
|
|
11
13
|
/**
|
|
12
14
|
* Prunes what the realtime tables keep whether or not a socket is open, as on an app that never opens one: the old
|
|
13
|
-
* broadcast rows of `realtime.messages
|
|
14
|
-
* only while the process does, so it never wakes a sleeping app.
|
|
15
|
+
* broadcast rows of `realtime.messages`, relayed, the relay's old frames and counts and, with `sqlSends`, the old
|
|
16
|
+
* rows of `channels.sends`, on `pruneSchedule`. It runs only while the process does, so it never wakes a sleeping app.
|
|
15
17
|
*/
|
|
16
|
-
export declare const startRealtimePrune: ({ db, statementTimeoutMs, relayed, report }: RealtimePruneOptions) => {
|
|
18
|
+
export declare const startRealtimePrune: ({ db, statementTimeoutMs, relayed, sqlSends, report }: RealtimePruneOptions) => {
|
|
17
19
|
stop: () => Promise<void>;
|
|
18
20
|
};
|
|
19
21
|
export type RealtimePrune = ReturnType<typeof startRealtimePrune>;
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { pruneFrames } from "./relay/relay-frame-sql.js";
|
|
2
2
|
import { pruneBroadcasts } from "./broadcast-prune.js";
|
|
3
|
+
import { pruneSends } from "../channels/sends-prune.js";
|
|
3
4
|
import { pruneSchedule } from "./prune-schedule.js";
|
|
4
5
|
//#region src/realtime/realtime-prune.ts
|
|
5
6
|
/**
|
|
6
7
|
* Prunes what the realtime tables keep whether or not a socket is open, as on an app that never opens one: the old
|
|
7
|
-
* broadcast rows of `realtime.messages
|
|
8
|
-
* only while the process does, so it never wakes a sleeping app.
|
|
8
|
+
* broadcast rows of `realtime.messages`, relayed, the relay's old frames and counts and, with `sqlSends`, the old
|
|
9
|
+
* rows of `channels.sends`, on `pruneSchedule`. It runs only while the process does, so it never wakes a sleeping app.
|
|
9
10
|
*/
|
|
10
|
-
const startRealtimePrune = ({ db, statementTimeoutMs, relayed, report }) => pruneSchedule([
|
|
11
|
+
const startRealtimePrune = ({ db, statementTimeoutMs, relayed, sqlSends, report }) => pruneSchedule([
|
|
12
|
+
() => pruneBroadcasts(db, statementTimeoutMs),
|
|
13
|
+
...relayed ? [() => pruneFrames(db)] : [],
|
|
14
|
+
...sqlSends ? [() => pruneSends(db, statementTimeoutMs)] : []
|
|
15
|
+
], report);
|
|
11
16
|
//#endregion
|
|
12
17
|
export { startRealtimePrune };
|
|
@@ -7,7 +7,14 @@ export type RealtimeOptions = {
|
|
|
7
7
|
* `realtime_relay` schema of the app's database, which `protobase migration` writes.
|
|
8
8
|
*/
|
|
9
9
|
processes?: 'one' | 'many';
|
|
10
|
+
/**
|
|
11
|
+
* `true` lets SQL in the app's database broadcast to the record channels with `channels.send()`, from the `channels`
|
|
12
|
+
* schema `protobase migration` writes.
|
|
13
|
+
*/
|
|
14
|
+
sqlSends?: boolean;
|
|
10
15
|
};
|
|
11
16
|
/** Whether the options share broadcasts and presence between processes. */
|
|
12
17
|
export declare const relayed: (options?: RealtimeOptions) => boolean;
|
|
18
|
+
/** Whether the options let SQL broadcast to the record channels. */
|
|
19
|
+
export declare const sqlSends: (options?: RealtimeOptions) => boolean;
|
|
13
20
|
//#endregion
|
|
@@ -5,5 +5,11 @@ const relayed = (options = {}) => {
|
|
|
5
5
|
if (options.processes === "many") return true;
|
|
6
6
|
throw new Error(`Invalid realtime.processes "${String(options.processes)}": use "one" or "many"`);
|
|
7
7
|
};
|
|
8
|
+
/** Whether the options let SQL broadcast to the record channels. */
|
|
9
|
+
const sqlSends = (options = {}) => {
|
|
10
|
+
if (options.sqlSends === void 0) return false;
|
|
11
|
+
if (typeof options.sqlSends === "boolean") return options.sqlSends;
|
|
12
|
+
throw new Error(`Invalid realtime.sqlSends "${String(options.sqlSends)}": use true or false`);
|
|
13
|
+
};
|
|
8
14
|
//#endregion
|
|
9
|
-
export { relayed };
|
|
15
|
+
export { relayed, sqlSends };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.59.
|
|
3
|
+
"version": "0.59.2",
|
|
4
4
|
"description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"@better-auth/sso": "^1.7.12",
|
|
43
43
|
"@livekit/protocol": "1.53.1",
|
|
44
44
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
45
|
-
"@protobase/layout": "0.59.
|
|
46
|
-
"@protobase/query": "0.59.
|
|
47
|
-
"@protobase/schema": "0.59.
|
|
45
|
+
"@protobase/layout": "0.59.2",
|
|
46
|
+
"@protobase/query": "0.59.2",
|
|
47
|
+
"@protobase/schema": "0.59.2",
|
|
48
48
|
"@react-email/render": "^2.1.0",
|
|
49
49
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
50
50
|
"@sentry/core": "^11.7.0",
|