@protobase/server 0.58.7 → 0.59.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/dist/assistant/query-runner/query-runner.d.ts +7 -2
- package/dist/assistant/query-runner/query-runner.js +8 -4
- package/dist/assistant/query-runner/runner-plan.js +4 -4
- package/dist/better-auth/auth-settings.d.ts +1 -0
- package/dist/better-auth/auth-tables.d.ts +13 -6
- package/dist/better-auth/auth-tables.js +9 -6
- package/dist/better-auth/create-auth.d.ts +1 -0
- package/dist/better-auth/store/resources-outside.d.ts +4 -1
- package/dist/better-auth/store/resources-outside.js +8 -3
- package/dist/better-auth/user-table.d.ts +6 -0
- package/dist/better-auth/user-table.js +3 -2
- package/dist/channels/channel-bounds.js +17 -0
- package/dist/channels/channel-name.d.ts +1 -0
- package/dist/channels/channel-name.js +30 -0
- package/dist/channels/channel-protocol.js +33 -0
- package/dist/channels/channel-refusals.js +26 -0
- package/dist/channels/channel-routes.js +98 -0
- package/dist/channels/channel-sender.d.ts +20 -0
- package/dist/channels/channel-sender.js +39 -0
- package/dist/channels/channel-socket.js +253 -0
- package/dist/channels/presence-entries.js +24 -0
- package/dist/channels/record-readable.js +28 -0
- package/dist/channels/sent.js +8 -0
- package/dist/create-admin.js +22 -2
- package/dist/functions/define-function.d.ts +3 -0
- package/dist/functions/routes.js +3 -1
- package/dist/index.d.ts +2 -1
- package/dist/package.js +1 -1
- package/dist/realtime/broadcast-hub.js +1 -1
- package/dist/realtime/broadcast-request.js +1 -1
- package/dist/realtime/database-broadcasts.js +1 -1
- package/dist/realtime/realtime-app.js +2 -18
- package/dist/realtime/realtime-hubs.js +30 -0
- package/dist/realtime/realtime-socket.js +2 -2
- package/dist/realtime/relay/realtime-options.d.ts +1 -1
- package/dist/realtime/relay/realtime-relay.js +2 -2
- package/dist/realtime/relay/relay-frame-sql.js +7 -4
- package/dist/realtime/relay/relay-presence-sql.js +13 -13
- package/dist/realtime/relay/relay-schema-sql.js +11 -2
- package/dist/realtime/relay/relay-schema.d.ts +1 -1
- package/dist/realtime/relay/relay-schema.js +4 -2
- package/dist/realtime/relay/relayed-hubs.js +63 -40
- package/dist/webhooks/attempt.js +1 -0
- package/dist/webhooks/webhooks.js +2 -1
- package/package.json +4 -4
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { fitsFrame } from "../realtime/broadcast-frame.js";
|
|
2
|
+
import { realtimeLimits } from "../realtime/realtime-limits.js";
|
|
3
|
+
import { fixedWindow, fixedWindows } from "../realtime/fixed-window.js";
|
|
4
|
+
import { channelRefusals } from "./channel-refusals.js";
|
|
5
|
+
import { channelBounds } from "./channel-bounds.js";
|
|
6
|
+
import { decodeClientMessage } from "./channel-protocol.js";
|
|
7
|
+
import { entriesOf, entryDiff } from "./presence-entries.js";
|
|
8
|
+
import { senderOf } from "./sent.js";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { Buffer } from "node:buffer";
|
|
11
|
+
//#region src/channels/channel-socket.ts
|
|
12
|
+
const messageTooBig = 1009;
|
|
13
|
+
const policyViolation = 1008;
|
|
14
|
+
const frameBytes = (frame) => {
|
|
15
|
+
if (typeof frame === "string") return Buffer.byteLength(frame);
|
|
16
|
+
if (frame instanceof ArrayBuffer || ArrayBuffer.isView(frame)) return frame.byteLength;
|
|
17
|
+
if (frame instanceof Blob) return frame.size;
|
|
18
|
+
return NaN;
|
|
19
|
+
};
|
|
20
|
+
const userOf = (session) => session.anonymous ? null : session.user.id;
|
|
21
|
+
/**
|
|
22
|
+
* One WebSocket of `/api/channels`: channels named by records, which a caller joins only while it may read the record,
|
|
23
|
+
* with presence and broadcasts between the connections on each. Messages are handled one at a time, in order. Every
|
|
24
|
+
* presence and broadcast carries the session's user, never one the client names. The session and each channel's read
|
|
25
|
+
* access are checked again every `channelBounds.recheckMs`, and a send or track trusts the last check for
|
|
26
|
+
* `channelBounds.readCacheMs`: a channel whose record the caller may no longer read is closed, and a session that signs
|
|
27
|
+
* in nobody, or someone else, closes the socket. A socket silent for `channelBounds.idleMs` is closed, and a close
|
|
28
|
+
* leaves every channel, its presences with it. The limits are `realtimeLimits`, as on `/api/sb/realtime/v1`.
|
|
29
|
+
*/
|
|
30
|
+
const channelSocket = ({ send, buffered, close, session: first, reauthenticate, record, readable, broadcasts, presences, report, now = Date.now }) => {
|
|
31
|
+
const channels = /* @__PURE__ */ new Map();
|
|
32
|
+
let session = first;
|
|
33
|
+
let token;
|
|
34
|
+
let queue = Promise.resolve();
|
|
35
|
+
let queuedMessages = 0;
|
|
36
|
+
let queuedBytes = 0;
|
|
37
|
+
let idle;
|
|
38
|
+
let open = true;
|
|
39
|
+
const events = fixedWindow(realtimeLimits.eventsPerSecond, 1e3, now);
|
|
40
|
+
const joins = fixedWindow(realtimeLimits.joinsPerSecond, 1e3, now);
|
|
41
|
+
const presenceRate = fixedWindows(realtimeLimits.presencePerWindow, realtimeLimits.presenceWindowMs, now);
|
|
42
|
+
const leaveAll = () => {
|
|
43
|
+
open = false;
|
|
44
|
+
clearTimeout(idle);
|
|
45
|
+
clearInterval(recheck);
|
|
46
|
+
for (const name of [...channels.keys()]) leave(name);
|
|
47
|
+
};
|
|
48
|
+
const shut = (code) => {
|
|
49
|
+
if (!open) return;
|
|
50
|
+
open = false;
|
|
51
|
+
clearTimeout(idle);
|
|
52
|
+
clearInterval(recheck);
|
|
53
|
+
close(code);
|
|
54
|
+
Promise.resolve().then(leaveAll).catch(report);
|
|
55
|
+
};
|
|
56
|
+
const push = (message) => {
|
|
57
|
+
if (!open) return;
|
|
58
|
+
if (!(buffered() <= realtimeLimits.maxUnsentBytes)) return shut(policyViolation);
|
|
59
|
+
send(JSON.stringify(message));
|
|
60
|
+
};
|
|
61
|
+
const ok = (ref, channel) => {
|
|
62
|
+
if (ref === void 0) return;
|
|
63
|
+
push({
|
|
64
|
+
type: "reply",
|
|
65
|
+
ref,
|
|
66
|
+
ok: true,
|
|
67
|
+
...channel !== void 0 && { channel }
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
const refuse = (ref, error) => {
|
|
71
|
+
if (ref === void 0) return push({
|
|
72
|
+
type: "error",
|
|
73
|
+
error
|
|
74
|
+
});
|
|
75
|
+
push({
|
|
76
|
+
type: "reply",
|
|
77
|
+
ref,
|
|
78
|
+
ok: false,
|
|
79
|
+
error
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
const leave = (name) => {
|
|
83
|
+
channels.get(name)?.remove();
|
|
84
|
+
channels.delete(name);
|
|
85
|
+
};
|
|
86
|
+
const end = (name, error) => {
|
|
87
|
+
if (!channels.has(name)) return;
|
|
88
|
+
leave(name);
|
|
89
|
+
push({
|
|
90
|
+
type: "closed",
|
|
91
|
+
channel: name,
|
|
92
|
+
error
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
const join = async (message) => {
|
|
96
|
+
if (!joins()) return refuse(message.ref, channelRefusals.tooManyJoins);
|
|
97
|
+
const found = record(message.resource, message.key);
|
|
98
|
+
if (found === "too-long") return refuse(message.ref, channelRefusals.nameTooLong);
|
|
99
|
+
if (found && channels.has(found.name)) return refuse(message.ref, channelRefusals.alreadyJoined);
|
|
100
|
+
if (channels.size >= realtimeLimits.channelsPerSocket) return refuse(message.ref, channelRefusals.tooManyChannels);
|
|
101
|
+
if (!found || !await readable(session, found)) return refuse(message.ref, channelRefusals.notFound);
|
|
102
|
+
await presences.ready();
|
|
103
|
+
if (!open || channels.has(found.name)) return;
|
|
104
|
+
const { name } = found;
|
|
105
|
+
const member = {
|
|
106
|
+
self: false,
|
|
107
|
+
private: false,
|
|
108
|
+
receive: (event, payload) => hear(name, event, payload)
|
|
109
|
+
};
|
|
110
|
+
const seat = presences.join(name, {
|
|
111
|
+
key: randomUUID(),
|
|
112
|
+
private: false,
|
|
113
|
+
listens: true,
|
|
114
|
+
receive: (diff) => push({
|
|
115
|
+
type: "presence_diff",
|
|
116
|
+
channel: name,
|
|
117
|
+
...entryDiff(diff)
|
|
118
|
+
})
|
|
119
|
+
});
|
|
120
|
+
const offBroadcast = broadcasts.join(name, member);
|
|
121
|
+
channels.set(name, {
|
|
122
|
+
record: found,
|
|
123
|
+
member,
|
|
124
|
+
seat,
|
|
125
|
+
checkedAt: now(),
|
|
126
|
+
remove: () => (offBroadcast(), seat.leave())
|
|
127
|
+
});
|
|
128
|
+
ok(message.ref, name);
|
|
129
|
+
push({
|
|
130
|
+
type: "presence",
|
|
131
|
+
channel: name,
|
|
132
|
+
entries: entriesOf(seat.state())
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
const hear = (name, event, sent) => {
|
|
136
|
+
if (!channels.has(name)) return;
|
|
137
|
+
push({
|
|
138
|
+
type: "broadcast",
|
|
139
|
+
channel: name,
|
|
140
|
+
event,
|
|
141
|
+
payload: sent.payload,
|
|
142
|
+
...senderOf(sent)
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
const stillReadable = async (name, channel) => {
|
|
146
|
+
if (now() - channel.checkedAt < channelBounds.readCacheMs) return true;
|
|
147
|
+
const allowed = await readable(session, channel.record);
|
|
148
|
+
if (channels.get(name) !== channel) return false;
|
|
149
|
+
if (!allowed) {
|
|
150
|
+
end(name, channelRefusals.notFound);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
channel.checkedAt = now();
|
|
154
|
+
return true;
|
|
155
|
+
};
|
|
156
|
+
const sendBroadcast = async (message, channel) => {
|
|
157
|
+
if (!events()) return refuse(message.ref, channelRefusals.tooManyMessages);
|
|
158
|
+
if (!fitsFrame(message.event)) return refuse(message.ref, channelRefusals.eventTooLong);
|
|
159
|
+
if (!await stillReadable(message.channel, channel)) return refuse(message.ref, channelRefusals.notFound);
|
|
160
|
+
const sent = {
|
|
161
|
+
user: userOf(session),
|
|
162
|
+
payload: message.payload
|
|
163
|
+
};
|
|
164
|
+
if (!await broadcasts.send(message.channel, channel.member, message.event, sent)) return refuse(message.ref, channelRefusals.channelTooBusy);
|
|
165
|
+
if (message.self === true) hear(message.channel, message.event, sent);
|
|
166
|
+
if (message.ack === true) ok(message.ref);
|
|
167
|
+
};
|
|
168
|
+
const updatePresence = async (message, channel) => {
|
|
169
|
+
if (!events()) return refuse(message.ref, channelRefusals.tooManyMessages);
|
|
170
|
+
if (!presenceRate(message.channel)) return refuse(message.ref, channelRefusals.tooManyPresenceUpdates);
|
|
171
|
+
if (message.type === "untrack") {
|
|
172
|
+
channel.seat.untrack();
|
|
173
|
+
return ok(message.ref);
|
|
174
|
+
}
|
|
175
|
+
if (Buffer.byteLength(JSON.stringify(message.state) ?? "") > realtimeLimits.maxPresenceBytes) return refuse(message.ref, channelRefusals.presenceTooLarge);
|
|
176
|
+
if (!await stillReadable(message.channel, channel)) return refuse(message.ref, channelRefusals.notFound);
|
|
177
|
+
channel.seat.track({
|
|
178
|
+
user: userOf(session),
|
|
179
|
+
state: message.state
|
|
180
|
+
});
|
|
181
|
+
ok(message.ref);
|
|
182
|
+
};
|
|
183
|
+
const handle = async (message) => {
|
|
184
|
+
if (!open) return;
|
|
185
|
+
if (message.type === "ping") return push({ type: "pong" });
|
|
186
|
+
if (message.type === "token") {
|
|
187
|
+
token = message.token;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (message.type === "join") return join(message);
|
|
191
|
+
const channel = channels.get(message.channel);
|
|
192
|
+
if (!channel) return refuse(message.ref, channelRefusals.notJoined);
|
|
193
|
+
if (message.type === "send") return sendBroadcast(message, channel);
|
|
194
|
+
if (message.type === "track" || message.type === "untrack") return updatePresence(message, channel);
|
|
195
|
+
leave(message.channel);
|
|
196
|
+
ok(message.ref);
|
|
197
|
+
};
|
|
198
|
+
const checkAgain = async () => {
|
|
199
|
+
const again = await reauthenticate(token).then((next) => next, () => void 0);
|
|
200
|
+
if (!open) return;
|
|
201
|
+
if (!again || userOf(again) !== userOf(session)) return shut(policyViolation);
|
|
202
|
+
session = again;
|
|
203
|
+
for (const [name, channel] of [...channels]) {
|
|
204
|
+
const allowed = await readable(session, channel.record);
|
|
205
|
+
if (channels.get(name) !== channel) continue;
|
|
206
|
+
if (!allowed) end(name, channelRefusals.notFound);
|
|
207
|
+
else channel.checkedAt = now();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
let rechecking = false;
|
|
211
|
+
const recheck = setInterval(() => {
|
|
212
|
+
if (rechecking || !open) return;
|
|
213
|
+
rechecking = true;
|
|
214
|
+
checkAgain().catch(fail).finally(() => rechecking = false);
|
|
215
|
+
}, channelBounds.recheckMs);
|
|
216
|
+
const stayAwake = () => {
|
|
217
|
+
clearTimeout(idle);
|
|
218
|
+
idle = setTimeout(() => shut(), channelBounds.idleMs);
|
|
219
|
+
};
|
|
220
|
+
stayAwake();
|
|
221
|
+
const fail = (error) => {
|
|
222
|
+
report(error);
|
|
223
|
+
shut();
|
|
224
|
+
};
|
|
225
|
+
return {
|
|
226
|
+
/**
|
|
227
|
+
* Handles a text frame after those before it; a binary one, or one that is not a message, gets an error. A frame
|
|
228
|
+
* over the size cap, or of a kind whose size is unknown, closes the socket.
|
|
229
|
+
*/
|
|
230
|
+
message: (frame) => {
|
|
231
|
+
if (!open) return;
|
|
232
|
+
const size = frameBytes(frame);
|
|
233
|
+
if (!(size <= realtimeLimits.maxFrameBytes)) return shut(messageTooBig);
|
|
234
|
+
stayAwake();
|
|
235
|
+
const message = typeof frame === "string" ? decodeClientMessage(frame) : void 0;
|
|
236
|
+
if (!message) return push({
|
|
237
|
+
type: "error",
|
|
238
|
+
error: channelRefusals.invalidMessage
|
|
239
|
+
});
|
|
240
|
+
queuedMessages += 1;
|
|
241
|
+
queuedBytes += size;
|
|
242
|
+
if (queuedMessages > realtimeLimits.maxQueuedMessages || queuedBytes > realtimeLimits.maxQueuedBytes) return shut(policyViolation);
|
|
243
|
+
queue = queue.then(() => handle(message)).catch(fail).finally(() => {
|
|
244
|
+
queuedMessages -= 1;
|
|
245
|
+
queuedBytes -= size;
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
/** Ends every channel of a socket that closed. */
|
|
249
|
+
closed: leaveAll
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
//#endregion
|
|
253
|
+
export { channelSocket };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/channels/presence-entries.ts
|
|
2
|
+
/** The hub's presences as entries: each key is one connection on the channel, so it holds one presence at most. */
|
|
3
|
+
const entriesOf = (state) => Object.entries(state).flatMap(([id, { metas }]) => metas.map(({ user, state: tracked }) => ({
|
|
4
|
+
id,
|
|
5
|
+
user,
|
|
6
|
+
state: tracked
|
|
7
|
+
})));
|
|
8
|
+
/**
|
|
9
|
+
* A hub diff as joins, updates and leaves: a key that left and joined in one diff tracked again, an update. A key
|
|
10
|
+
* that joins without the leave of its old state, as from another process whose leave this one never saw, is a join.
|
|
11
|
+
*/
|
|
12
|
+
const entryDiff = (diff) => {
|
|
13
|
+
const joined = entriesOf(diff.joins);
|
|
14
|
+
const left = entriesOf(diff.leaves);
|
|
15
|
+
const joinedIds = new Set(joined.map((entry) => entry.id));
|
|
16
|
+
const leftIds = new Set(left.map((entry) => entry.id));
|
|
17
|
+
return {
|
|
18
|
+
joins: joined.filter((entry) => !leftIds.has(entry.id)),
|
|
19
|
+
updates: joined.filter((entry) => leftIds.has(entry.id)),
|
|
20
|
+
leaves: left.filter((entry) => !joinedIds.has(entry.id))
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { entriesOf, entryDiff };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { HttpProblem } from "../problem.js";
|
|
2
|
+
import { requestAccess } from "../request-access.js";
|
|
3
|
+
import { readTransaction } from "../transactions.js";
|
|
4
|
+
import { fetchReadable } from "../fetch-readable.js";
|
|
5
|
+
//#region src/channels/record-readable.ts
|
|
6
|
+
const refusedStatuses = /* @__PURE__ */ new Set([
|
|
7
|
+
401,
|
|
8
|
+
403,
|
|
9
|
+
404
|
|
10
|
+
]);
|
|
11
|
+
/**
|
|
12
|
+
* Whether the caller may read the record, as `GET /api/<resource>/<key>` decides it: the resource's read access, the
|
|
13
|
+
* tenant, the row filter, the parents read and a record-level read rule. A soft-deleted record is not readable.
|
|
14
|
+
*/
|
|
15
|
+
const recordReadable = async (deps, session, { entry, key }) => {
|
|
16
|
+
const read = async () => {
|
|
17
|
+
const access = await requestAccess(deps, entry, session);
|
|
18
|
+
if (!access.resolved.operations.read) return false;
|
|
19
|
+
const primary = access.full.fields[access.full.primaryKey[0]];
|
|
20
|
+
return await readTransaction(deps.db, deps.statementTimeoutMs, (trx) => fetchReadable(trx, access, key, "hide", { fields: [primary] })) !== void 0;
|
|
21
|
+
};
|
|
22
|
+
return read().catch((error) => {
|
|
23
|
+
if (error instanceof HttpProblem && refusedStatuses.has(error.status)) return false;
|
|
24
|
+
throw error;
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
//#endregion
|
|
28
|
+
export { recordReadable };
|
package/dist/create-admin.js
CHANGED
|
@@ -16,8 +16,11 @@ import { apiCors, readCorsOrigins, withMobileOrigins } from "./api-cors.js";
|
|
|
16
16
|
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
|
-
import { relayed } from "./realtime/relay/realtime-options.js";
|
|
20
19
|
import { realtimeApp } from "./realtime/realtime-app.js";
|
|
20
|
+
import { relayed } from "./realtime/relay/realtime-options.js";
|
|
21
|
+
import { realtimeHubs } from "./realtime/realtime-hubs.js";
|
|
22
|
+
import { channelSender } from "./channels/channel-sender.js";
|
|
23
|
+
import { channelRoutes } from "./channels/channel-routes.js";
|
|
21
24
|
import { startRealtimePrune } from "./realtime/realtime-prune.js";
|
|
22
25
|
import { restRoutes } from "./rest/rest-routes.js";
|
|
23
26
|
import { storageRoutes } from "./storage/storage-routes.js";
|
|
@@ -376,6 +379,8 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
376
379
|
}
|
|
377
380
|
});
|
|
378
381
|
const decryptField = fieldDecrypter(deps);
|
|
382
|
+
const hubs = realtimeHubs(deps, realtime, report);
|
|
383
|
+
const channels = channelSender(registry, hubs.records.broadcasts);
|
|
379
384
|
app.route("/", functionRoutes({
|
|
380
385
|
basePath: `${systemPath}/functions`,
|
|
381
386
|
functions: apiFunctions,
|
|
@@ -383,6 +388,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
383
388
|
access: deps.accessOptions,
|
|
384
389
|
db,
|
|
385
390
|
records: (session) => callerRecords(deps, session),
|
|
391
|
+
channels,
|
|
386
392
|
decryptField,
|
|
387
393
|
onError,
|
|
388
394
|
wake: () => wakeDispatch(deps),
|
|
@@ -391,6 +397,19 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
391
397
|
...calls && { calls },
|
|
392
398
|
report
|
|
393
399
|
}));
|
|
400
|
+
if (upgradeWebSocket) {
|
|
401
|
+
const visitors = (caller) => withAnonymous(caller, options.roles);
|
|
402
|
+
app.route(systemPath || "/", channelRoutes({
|
|
403
|
+
deps,
|
|
404
|
+
upgradeWebSocket,
|
|
405
|
+
authenticate: visitors(authenticate),
|
|
406
|
+
authenticateUpgrade: visitors(upgradeAuthenticate),
|
|
407
|
+
onError,
|
|
408
|
+
hubs: hubs.records,
|
|
409
|
+
follow: hubs.follow,
|
|
410
|
+
report
|
|
411
|
+
}));
|
|
412
|
+
}
|
|
394
413
|
const verifyGoTrue = auth ? jwtVerifier(goTrueTokens({ auth })) : () => Promise.reject(unauthorized("This app signs no user tokens"));
|
|
395
414
|
app.route("/", supabaseFunctionRoutes({
|
|
396
415
|
functions: apiFunctions,
|
|
@@ -435,7 +454,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
435
454
|
upgradeWebSocket,
|
|
436
455
|
caller,
|
|
437
456
|
report,
|
|
438
|
-
|
|
457
|
+
hubs
|
|
439
458
|
}));
|
|
440
459
|
if (files?.storage) {
|
|
441
460
|
const storage = {
|
|
@@ -462,6 +481,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
462
481
|
deps,
|
|
463
482
|
subscriptions,
|
|
464
483
|
functions: apiFunctions,
|
|
484
|
+
channels,
|
|
465
485
|
schema: changes,
|
|
466
486
|
log: (line) => console.log(line),
|
|
467
487
|
...table && { table }
|
|
@@ -3,6 +3,7 @@ import { Session } from "../types.js";
|
|
|
3
3
|
import { Caller } from "../api-keys/resolve-caller.js";
|
|
4
4
|
import { SocketEvents } from "./function-socket.js";
|
|
5
5
|
import { CallsAccess } from "../calls/calls-access.js";
|
|
6
|
+
import { ChannelSender } from "../channels/channel-sender.js";
|
|
6
7
|
import { FunctionRecords } from "./records.js";
|
|
7
8
|
import { Capability } from "@protobase/schema";
|
|
8
9
|
import { Db } from "@protobase/query";
|
|
@@ -13,6 +14,8 @@ export type PublicFunctionContext = {
|
|
|
13
14
|
name: string;
|
|
14
15
|
/** The database itself, with no access rules, row filters or tenant scope: privileged work only. */
|
|
15
16
|
db: Db;
|
|
17
|
+
/** Broadcasts to the channel of a record, as the server, to whoever is on it (see `/api/channels`). */
|
|
18
|
+
channels: ChannelSender;
|
|
16
19
|
/** The caller, through the app's own `authenticate`: rejects with a 401 problem when the request carries no valid token. */
|
|
17
20
|
caller: () => Promise<FunctionCaller>;
|
|
18
21
|
/**
|
package/dist/functions/routes.js
CHANGED
|
@@ -24,7 +24,7 @@ const isUpgrade = (request) => request.headers.get("upgrade")?.toLowerCase() ===
|
|
|
24
24
|
* A function may answer an upgrade with a socket, only once it returned. While it is open, its caller is checked again
|
|
25
25
|
* every minute: a protected function's always, a public one's once `caller()` resolved.
|
|
26
26
|
*/
|
|
27
|
-
const functionRoutes = ({ basePath, functions, authenticate, access, db, records, decryptField, onError, wake, upgradeWebSocket, upgradeAuthenticate = authenticate, calls: callsOptions, report }) => {
|
|
27
|
+
const functionRoutes = ({ basePath, functions, authenticate, access, db, records, channels, decryptField, onError, wake, upgradeWebSocket, upgradeAuthenticate = authenticate, calls: callsOptions, report }) => {
|
|
28
28
|
const calls = callsAccess(callsOptions);
|
|
29
29
|
const app = new Hono().basePath(basePath);
|
|
30
30
|
app.onError(onError);
|
|
@@ -68,6 +68,7 @@ const functionRoutes = ({ basePath, functions, authenticate, access, db, records
|
|
|
68
68
|
const context = {
|
|
69
69
|
name,
|
|
70
70
|
db,
|
|
71
|
+
channels,
|
|
71
72
|
caller: resolveCaller,
|
|
72
73
|
acceptWebSocket: acceptor.accept,
|
|
73
74
|
calls
|
|
@@ -95,6 +96,7 @@ const functionRoutes = ({ basePath, functions, authenticate, access, db, records
|
|
|
95
96
|
const context = {
|
|
96
97
|
name,
|
|
97
98
|
db,
|
|
99
|
+
channels,
|
|
98
100
|
...caller,
|
|
99
101
|
caller: async () => caller,
|
|
100
102
|
decryptField: decryptField(reader),
|
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { FunctionSocket, SocketEvents } from "./functions/function-socket.js";
|
|
|
32
32
|
import { CallMember } from "./calls/call-member.js";
|
|
33
33
|
import { CallsAccess } from "./calls/calls-access.js";
|
|
34
34
|
import { ResourceSource, configExports } from "./resource-source.js";
|
|
35
|
+
import { ChannelBroadcast, ChannelSender } from "./channels/channel-sender.js";
|
|
35
36
|
import { ErrorReportingOptions, ErrorReportingSettings, errorReportingSettings, errorReportingVariable } from "./error-reporting/error-reporting-settings.js";
|
|
36
37
|
import { ChatMessage, ChatTextPart, Completion, CompletionOptions, ModelError, ModelSettings, ReasoningDetail, ReasoningEffort, ToolCall, ToolSpec, streamChatCompletion } from "./assistant/chat-completions.js";
|
|
37
38
|
import { CleanupEntry, CleanupSchedule, ScheduledDelete, storageCleanupSchedule } from "./files/cleanup-schedule.js";
|
|
@@ -174,4 +175,4 @@ import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/ru
|
|
|
174
175
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
175
176
|
import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
|
|
176
177
|
import { AdminAuth, CreateAuthOptions, UserCreatedHook, normalizeRoles } from "./better-auth/create-auth.js";
|
|
177
|
-
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 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 };
|
|
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 };
|
package/dist/package.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { fixedWindows } from "./fixed-window.js";
|
|
2
1
|
import { realtimeLimits } from "./realtime-limits.js";
|
|
2
|
+
import { fixedWindows } from "./fixed-window.js";
|
|
3
3
|
//#region src/realtime/broadcast-hub.ts
|
|
4
4
|
/**
|
|
5
5
|
* The channels of a process by topic, which hear each other's broadcasts in-process; with several processes,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HttpProblem } from "../problem.js";
|
|
2
|
-
import { realtimeLimits } from "./realtime-limits.js";
|
|
3
2
|
import { fitsFrame } from "./broadcast-frame.js";
|
|
3
|
+
import { realtimeLimits } from "./realtime-limits.js";
|
|
4
4
|
//#region src/realtime/broadcast-request.ts
|
|
5
5
|
/** A refused broadcast request; `message` too, which realtime-js's `httpSend` reads as the error. */
|
|
6
6
|
const broadcastProblem = (status, slug, title, detail) => new HttpProblem(status, slug, title, detail, { message: detail });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { writeTransaction } from "../transactions.js";
|
|
2
|
-
import { realtimeLimits } from "./realtime-limits.js";
|
|
3
2
|
import { fitsFrame } from "./broadcast-frame.js";
|
|
3
|
+
import { realtimeLimits } from "./realtime-limits.js";
|
|
4
4
|
import { sql } from "kysely";
|
|
5
5
|
import { Buffer } from "node:buffer";
|
|
6
6
|
//#region src/realtime/database-broadcasts.ts
|
|
@@ -1,26 +1,10 @@
|
|
|
1
|
-
import { broadcastHub } from "./broadcast-hub.js";
|
|
2
1
|
import { changeChecker } from "./change-checker.js";
|
|
3
2
|
import { changeHub } from "./change-hub.js";
|
|
4
3
|
import { channelAccess } from "./channel-access.js";
|
|
5
4
|
import { filterCheck } from "./filter-check.js";
|
|
6
5
|
import { databaseBroadcasts } from "./database-broadcasts.js";
|
|
7
|
-
import { presenceHub } from "./presence-hub.js";
|
|
8
6
|
import { realtimeRoutes } from "./realtime-routes.js";
|
|
9
|
-
import { relayed } from "./relay/realtime-options.js";
|
|
10
|
-
import { relayedHubs } from "./relay/relayed-hubs.js";
|
|
11
7
|
//#region src/realtime/realtime-app.ts
|
|
12
|
-
const hubsOf = (deps, options, report) => {
|
|
13
|
-
if (relayed(options)) return relayedHubs({
|
|
14
|
-
db: deps.db,
|
|
15
|
-
statementTimeoutMs: deps.statementTimeoutMs,
|
|
16
|
-
report
|
|
17
|
-
});
|
|
18
|
-
return {
|
|
19
|
-
broadcasts: broadcastHub(),
|
|
20
|
-
presences: presenceHub(),
|
|
21
|
-
follow: () => () => {}
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
8
|
/**
|
|
25
9
|
* `/api/sb/realtime/v1`: the `postgres_changes` of the live resources, each checked under the subscriber's role, claims and
|
|
26
10
|
* the tables' RLS policies before it is sent, and broadcasts and presence between the channels on a topic, of this
|
|
@@ -28,7 +12,7 @@ const hubsOf = (deps, options, report) => {
|
|
|
28
12
|
* private channel as the policies on `realtime.messages` allow, and the broadcasts the database sends with
|
|
29
13
|
* `realtime.send()`.
|
|
30
14
|
*/
|
|
31
|
-
const realtimeApp = ({ deps, feed, upgradeWebSocket, caller, report,
|
|
15
|
+
const realtimeApp = ({ deps, feed, upgradeWebSocket, caller, report, hubs }) => {
|
|
32
16
|
const live = deps.registry.entries.filter((entry) => entry.model.live);
|
|
33
17
|
const check = changeChecker({
|
|
34
18
|
db: deps.db,
|
|
@@ -49,7 +33,7 @@ const realtimeApp = ({ deps, feed, upgradeWebSocket, caller, report, options })
|
|
|
49
33
|
db: deps.db,
|
|
50
34
|
statementTimeoutMs: deps.statementTimeoutMs
|
|
51
35
|
});
|
|
52
|
-
const { broadcasts, presences, follow } =
|
|
36
|
+
const { topics: { broadcasts, presences }, follow } = hubs;
|
|
53
37
|
const database = databaseBroadcasts({
|
|
54
38
|
db: deps.db,
|
|
55
39
|
statementTimeoutMs: deps.statementTimeoutMs,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { broadcastHub } from "./broadcast-hub.js";
|
|
2
|
+
import { presenceHub } from "./presence-hub.js";
|
|
3
|
+
import { relayed } from "./relay/realtime-options.js";
|
|
4
|
+
import { relayedHubs } from "./relay/relayed-hubs.js";
|
|
5
|
+
//#region src/realtime/realtime-hubs.ts
|
|
6
|
+
/**
|
|
7
|
+
* The broadcast and presence hubs of a process: those of the topic channels of `/api/sb/realtime/v1`, and apart from
|
|
8
|
+
* them those of the record channels of `/api/channels`, so neither hears the other. With `processes: 'many'` both go
|
|
9
|
+
* through the one relay, which `follow` reads while a socket of either is open.
|
|
10
|
+
*/
|
|
11
|
+
const realtimeHubs = (deps, options, report) => {
|
|
12
|
+
if (relayed(options)) return relayedHubs({
|
|
13
|
+
db: deps.db,
|
|
14
|
+
statementTimeoutMs: deps.statementTimeoutMs,
|
|
15
|
+
report
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
topics: {
|
|
19
|
+
broadcasts: broadcastHub(),
|
|
20
|
+
presences: presenceHub()
|
|
21
|
+
},
|
|
22
|
+
records: {
|
|
23
|
+
broadcasts: broadcastHub(),
|
|
24
|
+
presences: presenceHub()
|
|
25
|
+
},
|
|
26
|
+
follow: () => () => {}
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
export { realtimeHubs };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { HttpProblem } from "../problem.js";
|
|
2
|
-
import { fixedWindow, fixedWindows } from "./fixed-window.js";
|
|
3
|
-
import { realtimeLimits } from "./realtime-limits.js";
|
|
4
2
|
import { callerSettings } from "../rest/caller-settings.js";
|
|
5
3
|
import { parseBindings } from "./postgres-changes.js";
|
|
6
4
|
import "./filter-check.js";
|
|
7
5
|
import { decodeBroadcastPush, encodeBroadcast, fitsFrame } from "./broadcast-frame.js";
|
|
6
|
+
import { realtimeLimits } from "./realtime-limits.js";
|
|
8
7
|
import { decodeMessage, encodeMessage, reply } from "./phoenix-message.js";
|
|
9
8
|
import { privateGate, publicGate, readRefusalText } from "./channel-gate.js";
|
|
9
|
+
import { fixedWindow, fixedWindows } from "./fixed-window.js";
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
11
|
import { Buffer } from "node:buffer";
|
|
12
12
|
//#region src/realtime/realtime-socket.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/realtime/relay/realtime-options.d.ts
|
|
2
|
-
/** How `/api/sb/realtime/v1`
|
|
2
|
+
/** How `/api/channels` and `/api/sb/realtime/v1` are served (`realtime` in `protobase.config.ts`). */
|
|
3
3
|
export type RealtimeOptions = {
|
|
4
4
|
/**
|
|
5
5
|
* `'one'`, the default, when one process serves the app: broadcasts and presence go between its channels in memory.
|
|
@@ -95,9 +95,9 @@ const realtimeRelay = ({ db, statementTimeoutMs, heard, loaded, tracked, report,
|
|
|
95
95
|
/** Resolves once the other processes' presences are read, or their snapshot failed. */
|
|
96
96
|
ready: () => run?.ready ?? Promise.resolve(),
|
|
97
97
|
/** Counts and adds the broadcasts as frames, all of them or, over a topic's rate, none: `false` then. */
|
|
98
|
-
broadcast: (publications) => admitBroadcasts(db, statementTimeoutMs, self, publications),
|
|
98
|
+
broadcast: (space, publications) => admitBroadcasts(db, statementTimeoutMs, self, space, publications),
|
|
99
99
|
/** Writes a change of this process's presences, after those before it. */
|
|
100
|
-
changed: (change) => void inTurn(() => writePresence(db, self, change))
|
|
100
|
+
changed: (space, change) => void inTurn(() => writePresence(db, self, space, change))
|
|
101
101
|
};
|
|
102
102
|
};
|
|
103
103
|
//#endregion
|