@protobase/server 0.26.1 → 0.26.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/create-admin.js +18 -20
- package/dist/realtime/broadcast-frame.js +71 -0
- package/dist/realtime/broadcast-hub.js +25 -0
- package/dist/realtime/presence-hub.js +59 -0
- package/dist/realtime/realtime-app.js +1 -1
- package/dist/realtime/realtime-routes.js +9 -2
- package/dist/realtime/realtime-socket.js +67 -17
- package/package.json +4 -4
package/dist/create-admin.js
CHANGED
|
@@ -270,27 +270,25 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
270
270
|
app.route("/", batchRoute(deps, authenticate, meta));
|
|
271
271
|
app.route("/", fileRoutes(deps, authenticate));
|
|
272
272
|
if (forms) app.route(`${systemPath}/forms`, formRoutes(deps, forms));
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
273
|
+
const report = options.onUnhandledError ?? ((error) => console.error(error));
|
|
274
|
+
const feed = tableFeed({
|
|
275
|
+
db,
|
|
276
|
+
schema: changes,
|
|
277
|
+
report
|
|
278
|
+
});
|
|
279
|
+
if (appModels.some((model) => model.live)) app.route(systemPath || "/", guarded((live) => void live.route("/", liveRoutes(feed, (session) => visibleNotices(deps, session))), withAnonymous(authenticate, options.roles)));
|
|
280
|
+
if (upgradeWebSocket && (apiKeys.publishable || apiKeys.secret)) {
|
|
281
|
+
const verify = verifyToken ?? (auth ? jwtVerifier(betterAuthTokens({ auth })) : () => Promise.reject(unauthorized("This app signs no user tokens")));
|
|
282
|
+
app.route("/realtime/v1", realtimeApp({
|
|
283
|
+
deps,
|
|
284
|
+
feed,
|
|
285
|
+
upgradeWebSocket,
|
|
286
|
+
caller: apiKeyCaller({
|
|
287
|
+
keys: apiKeys,
|
|
288
|
+
verify
|
|
289
|
+
}),
|
|
278
290
|
report
|
|
279
|
-
});
|
|
280
|
-
app.route(systemPath || "/", guarded((live) => void live.route("/", liveRoutes(feed, (session) => visibleNotices(deps, session))), withAnonymous(authenticate, options.roles)));
|
|
281
|
-
if (upgradeWebSocket && (apiKeys.publishable || apiKeys.secret)) {
|
|
282
|
-
const verify = verifyToken ?? (auth ? jwtVerifier(betterAuthTokens({ auth })) : () => Promise.reject(unauthorized("This app signs no user tokens")));
|
|
283
|
-
app.route("/realtime/v1", realtimeApp({
|
|
284
|
-
deps,
|
|
285
|
-
feed,
|
|
286
|
-
upgradeWebSocket,
|
|
287
|
-
caller: apiKeyCaller({
|
|
288
|
-
keys: apiKeys,
|
|
289
|
-
verify
|
|
290
|
-
}),
|
|
291
|
-
report
|
|
292
|
-
}));
|
|
293
|
-
}
|
|
291
|
+
}));
|
|
294
292
|
}
|
|
295
293
|
app.route(basePath, api);
|
|
296
294
|
app.route(`${deps.systemPath}/meta`, metaApp);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//#region src/realtime/broadcast-frame.ts
|
|
2
|
+
const userBroadcastPush = 3;
|
|
3
|
+
const userBroadcast = 4;
|
|
4
|
+
const binaryEncoding = 0;
|
|
5
|
+
const jsonEncoding = 1;
|
|
6
|
+
const decoder = new TextDecoder();
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
/**
|
|
9
|
+
* A binary frame as the `broadcast` push it carries: a header of kind, the byte lengths of join ref, ref, topic, user
|
|
10
|
+
* event and metadata, and the payload's encoding, then those fields and the payload. The metadata is dropped, as only
|
|
11
|
+
* keys the server knows go in it. `undefined` for a frame of another kind or one cut short.
|
|
12
|
+
*/
|
|
13
|
+
const decodeBroadcastPush = (frame) => {
|
|
14
|
+
const bytes = new Uint8Array(frame);
|
|
15
|
+
if (bytes.length < 7 || bytes[0] !== userBroadcastPush) return void 0;
|
|
16
|
+
const [joinRefSize, refSize, topicSize, eventSize, metadataSize, encoding] = bytes.subarray(1, 7);
|
|
17
|
+
let offset = 7;
|
|
18
|
+
const field = (size) => decoder.decode(bytes.subarray(offset, offset += size));
|
|
19
|
+
const joinRef = field(joinRefSize);
|
|
20
|
+
const ref = field(refSize);
|
|
21
|
+
const topic = field(topicSize);
|
|
22
|
+
const event = field(eventSize);
|
|
23
|
+
offset += metadataSize;
|
|
24
|
+
if (offset > bytes.length) return void 0;
|
|
25
|
+
const body = frame.slice(offset);
|
|
26
|
+
let payload = body;
|
|
27
|
+
if (encoding === jsonEncoding) try {
|
|
28
|
+
payload = JSON.parse(decoder.decode(body));
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error instanceof SyntaxError) return void 0;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
joinRef: joinRef || null,
|
|
35
|
+
ref: ref || null,
|
|
36
|
+
topic,
|
|
37
|
+
event: "broadcast",
|
|
38
|
+
payload: {
|
|
39
|
+
type: "broadcast",
|
|
40
|
+
event,
|
|
41
|
+
payload
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
/** Whether a topic or event fits a binary frame's one-byte length. */
|
|
46
|
+
const fitsFrame = (text) => encoder.encode(text).length <= 255;
|
|
47
|
+
/**
|
|
48
|
+
* A broadcast as the binary frame realtime-js reads: kind, topic, user event and metadata lengths, encoding, fields,
|
|
49
|
+
* payload. A payload is JSON, or the bytes of a binary broadcast.
|
|
50
|
+
*/
|
|
51
|
+
const encodeBroadcast = (topic, event, payload) => {
|
|
52
|
+
const topicBytes = encoder.encode(topic);
|
|
53
|
+
const eventBytes = encoder.encode(event);
|
|
54
|
+
const binary = payload instanceof ArrayBuffer;
|
|
55
|
+
const body = binary ? new Uint8Array(payload) : encoder.encode(JSON.stringify(payload ?? {}));
|
|
56
|
+
if (topicBytes.length > 255 || eventBytes.length > 255) throw new Error("A broadcast topic and event are at most 255 bytes each");
|
|
57
|
+
const frame = new Uint8Array(5 + topicBytes.length + eventBytes.length + body.length);
|
|
58
|
+
frame.set([
|
|
59
|
+
userBroadcast,
|
|
60
|
+
topicBytes.length,
|
|
61
|
+
eventBytes.length,
|
|
62
|
+
0,
|
|
63
|
+
binary ? binaryEncoding : jsonEncoding
|
|
64
|
+
]);
|
|
65
|
+
frame.set(topicBytes, 5);
|
|
66
|
+
frame.set(eventBytes, 5 + topicBytes.length);
|
|
67
|
+
frame.set(body, 5 + topicBytes.length + eventBytes.length);
|
|
68
|
+
return frame.buffer;
|
|
69
|
+
};
|
|
70
|
+
//#endregion
|
|
71
|
+
export { decodeBroadcastPush, encodeBroadcast, fitsFrame };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/realtime/broadcast-hub.ts
|
|
2
|
+
/**
|
|
3
|
+
* The channels of a process by topic, which hear each other's broadcasts in-process: a cloud app runs as one process,
|
|
4
|
+
* so channels of other processes are not reached.
|
|
5
|
+
*/
|
|
6
|
+
const broadcastHub = () => {
|
|
7
|
+
const topics = /* @__PURE__ */ new Map();
|
|
8
|
+
return {
|
|
9
|
+
/** Puts the member on the topic until the returned function takes it off. */
|
|
10
|
+
join: (topic, member) => {
|
|
11
|
+
const members = topics.get(topic) ?? /* @__PURE__ */ new Set();
|
|
12
|
+
topics.set(topic, members.add(member));
|
|
13
|
+
return () => {
|
|
14
|
+
members.delete(member);
|
|
15
|
+
if (members.size === 0 && topics.get(topic) === members) topics.delete(topic);
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
/** Sends a broadcast to the topic's members, the sender only when it hears its own. */
|
|
19
|
+
send: (topic, from, event, payload) => {
|
|
20
|
+
for (const member of topics.get(topic) ?? []) if (member !== from || member.self) member.receive(event, payload);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { broadcastHub };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//#region src/realtime/presence-hub.ts
|
|
2
|
+
const stateOf = (tracked) => {
|
|
3
|
+
const state = Object.create(null);
|
|
4
|
+
for (const [key, meta] of tracked) (state[key] ??= { metas: [] }).metas.push(meta);
|
|
5
|
+
return state;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* The presences of a process by topic, in-process like broadcasts: each joined channel tracks at most one, under its
|
|
9
|
+
* key, and channels that listen hear every join and leave on their topic, their own included, as Phoenix Presence
|
|
10
|
+
* sends them. Tracking again replaces the presence: a leave of the old one and a join of the new. Refs count up, unique
|
|
11
|
+
* in the process.
|
|
12
|
+
*/
|
|
13
|
+
const presenceHub = () => {
|
|
14
|
+
const topics = /* @__PURE__ */ new Map();
|
|
15
|
+
let lastRef = 0;
|
|
16
|
+
const tell = (members, diff) => {
|
|
17
|
+
for (const member of members.keys()) if (member.listens) member.receive(diff);
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
/** Puts the member on the topic, with what it may do there until it leaves. */
|
|
21
|
+
join: (topic, member) => {
|
|
22
|
+
const members = topics.get(topic) ?? /* @__PURE__ */ new Map();
|
|
23
|
+
topics.set(topic, members.set(member, void 0));
|
|
24
|
+
const untrack = () => {
|
|
25
|
+
const old = members.get(member);
|
|
26
|
+
if (!old) return;
|
|
27
|
+
members.set(member, void 0);
|
|
28
|
+
tell(members, {
|
|
29
|
+
joins: {},
|
|
30
|
+
leaves: stateOf([[member.key, old]])
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
/** The topic's presences now, for `presence_state`. */
|
|
35
|
+
state: () => stateOf([...members].flatMap(([{ key }, meta]) => meta ? [[key, meta]] : [])),
|
|
36
|
+
track: (payload) => {
|
|
37
|
+
const old = members.get(member);
|
|
38
|
+
const meta = {
|
|
39
|
+
...payload,
|
|
40
|
+
phx_ref: String(++lastRef),
|
|
41
|
+
...old && { phx_ref_prev: old.phx_ref }
|
|
42
|
+
};
|
|
43
|
+
members.set(member, meta);
|
|
44
|
+
tell(members, {
|
|
45
|
+
joins: stateOf([[member.key, meta]]),
|
|
46
|
+
leaves: old ? stateOf([[member.key, old]]) : {}
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
untrack,
|
|
50
|
+
leave: () => {
|
|
51
|
+
untrack();
|
|
52
|
+
members.delete(member);
|
|
53
|
+
if (members.size === 0 && topics.get(topic) === members) topics.delete(topic);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
} };
|
|
57
|
+
};
|
|
58
|
+
//#endregion
|
|
59
|
+
export { presenceHub };
|
|
@@ -4,7 +4,7 @@ import { realtimeRoutes } from "./realtime-routes.js";
|
|
|
4
4
|
//#region src/realtime/realtime-app.ts
|
|
5
5
|
/**
|
|
6
6
|
* `/realtime/v1`: the `postgres_changes` of the live resources, each checked under the subscriber's role, claims and
|
|
7
|
-
* the tables' RLS policies before it is sent.
|
|
7
|
+
* the tables' RLS policies before it is sent, and broadcasts and presence between the channels of the process.
|
|
8
8
|
*/
|
|
9
9
|
const realtimeApp = ({ deps, feed, upgradeWebSocket, caller, report }) => {
|
|
10
10
|
const live = deps.registry.entries.filter((entry) => entry.model.live);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { HttpProblem } from "../problem.js";
|
|
2
|
+
import { broadcastHub } from "./broadcast-hub.js";
|
|
2
3
|
import { vsnOf } from "./phoenix-message.js";
|
|
4
|
+
import { presenceHub } from "./presence-hub.js";
|
|
3
5
|
import { realtimeSocket } from "./realtime-socket.js";
|
|
4
6
|
import { Hono } from "hono";
|
|
5
7
|
//#region src/realtime/realtime-routes.ts
|
|
@@ -9,10 +11,13 @@ const keyRequest = (apikey, token) => new Request("http://realtime.invalid/", {
|
|
|
9
11
|
} });
|
|
10
12
|
/**
|
|
11
13
|
* `GET /realtime/v1/websocket?apikey=…&vsn=…`: Supabase Realtime's socket, for `postgres_changes` of the live
|
|
12
|
-
* resources (see `realtimeSocket`). A missing or wrong
|
|
14
|
+
* resources, and broadcasts and presence between the sockets of this process (see `realtimeSocket`). A missing or wrong
|
|
15
|
+
* key is refused before the upgrade, with 401.
|
|
13
16
|
*/
|
|
14
17
|
const realtimeRoutes = ({ upgradeWebSocket, caller, live, hub, report }) => {
|
|
15
18
|
const app = new Hono();
|
|
19
|
+
const broadcasts = broadcastHub();
|
|
20
|
+
const presences = presenceHub();
|
|
16
21
|
let lastId = 0;
|
|
17
22
|
const nextId = () => ++lastId;
|
|
18
23
|
app.get("/websocket", async (c, next) => {
|
|
@@ -31,12 +36,14 @@ const realtimeRoutes = ({ upgradeWebSocket, caller, live, hub, report }) => {
|
|
|
31
36
|
resolve: (token) => caller(keyRequest(apikey, token)),
|
|
32
37
|
live,
|
|
33
38
|
hub,
|
|
39
|
+
broadcasts,
|
|
40
|
+
presences,
|
|
34
41
|
nextId,
|
|
35
42
|
report
|
|
36
43
|
});
|
|
37
44
|
},
|
|
38
45
|
onMessage: (event) => {
|
|
39
|
-
if (typeof event.data === "string") socket?.message(event.data);
|
|
46
|
+
if (typeof event.data === "string" || event.data instanceof ArrayBuffer) socket?.message(event.data);
|
|
40
47
|
},
|
|
41
48
|
onClose: () => socket?.closed()
|
|
42
49
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { HttpProblem } from "../problem.js";
|
|
2
2
|
import { parseBindings } from "./postgres-changes.js";
|
|
3
3
|
import { decodeMessage, encodeMessage, reply } from "./phoenix-message.js";
|
|
4
|
+
import { decodeBroadcastPush, encodeBroadcast, fitsFrame } from "./broadcast-frame.js";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
4
6
|
//#region src/realtime/realtime-socket.ts
|
|
5
7
|
const tokenOf = (payload) => typeof payload.access_token === "string" && payload.access_token !== "" ? payload.access_token : void 0;
|
|
6
8
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -10,11 +12,11 @@ const refusal = (error) => {
|
|
|
10
12
|
};
|
|
11
13
|
/**
|
|
12
14
|
* One WebSocket of `/realtime/v1`, speaking Phoenix channels as realtime-js expects: heartbeats, channel joins with
|
|
13
|
-
* `postgres_changes` bindings and an access token, token refreshes on an open channel,
|
|
14
|
-
*
|
|
15
|
-
*
|
|
15
|
+
* `postgres_changes` bindings and an access token, token refreshes on an open channel, broadcasts with their `self`
|
|
16
|
+
* and `ack` options, presence tracking, and leaves. A channel's token ends it when it expires, unless a newer one came
|
|
17
|
+
* first. Messages are handled one at a time, in order. Private channels are refused for now.
|
|
16
18
|
*/
|
|
17
|
-
const realtimeSocket = ({ vsn, send, close, resolve, live, hub, nextId, report, idleMs = 6e4, now = Date.now }) => {
|
|
19
|
+
const realtimeSocket = ({ vsn, send, close, resolve, live, hub, broadcasts, presences, nextId, report, idleMs = 6e4, now = Date.now }) => {
|
|
18
20
|
const channels = /* @__PURE__ */ new Map();
|
|
19
21
|
let queue = Promise.resolve();
|
|
20
22
|
let idle;
|
|
@@ -26,6 +28,15 @@ const realtimeSocket = ({ vsn, send, close, resolve, live, hub, nextId, report,
|
|
|
26
28
|
event,
|
|
27
29
|
payload
|
|
28
30
|
});
|
|
31
|
+
const hear = (topic, channel, event, payload) => {
|
|
32
|
+
if (vsn === "2.0.0") return send(encodeBroadcast(topic, event, payload));
|
|
33
|
+
if (payload instanceof ArrayBuffer) return;
|
|
34
|
+
toChannel(topic, channel, "broadcast", {
|
|
35
|
+
type: "broadcast",
|
|
36
|
+
event,
|
|
37
|
+
payload
|
|
38
|
+
});
|
|
39
|
+
};
|
|
29
40
|
const leave = (topic) => {
|
|
30
41
|
const channel = channels.get(topic);
|
|
31
42
|
if (!channel) return;
|
|
@@ -54,24 +65,46 @@ const realtimeSocket = ({ vsn, send, close, resolve, live, hub, nextId, report,
|
|
|
54
65
|
leave(message.topic);
|
|
55
66
|
const config = isRecord(message.payload.config) ? message.payload.config : {};
|
|
56
67
|
if (config.private === true) return push(reply(message, "error", { reason: "Not implemented: private channels" }));
|
|
57
|
-
|
|
68
|
+
const broadcast = isRecord(config.broadcast) ? config.broadcast : {};
|
|
69
|
+
const presence = isRecord(config.presence) ? config.presence : {};
|
|
58
70
|
const caller = await resolve(tokenOf(message.payload)).catch(refusal);
|
|
59
71
|
if (typeof caller === "string") return push(reply(message, "error", { reason: caller }));
|
|
60
72
|
const parsed = parseBindings(config.postgres_changes, live, nextId);
|
|
61
73
|
if (typeof parsed === "string") return push(reply(message, "error", { reason: parsed }));
|
|
74
|
+
const { topic } = message;
|
|
75
|
+
const member = {
|
|
76
|
+
self: broadcast.self === true,
|
|
77
|
+
receive: (event, payload) => hear(topic, channel, event, payload)
|
|
78
|
+
};
|
|
79
|
+
const key = typeof presence.key === "string" && presence.key !== "" ? presence.key : randomUUID();
|
|
80
|
+
const listens = presence.enabled === true;
|
|
81
|
+
const seat = presences.join(topic, {
|
|
82
|
+
key,
|
|
83
|
+
listens,
|
|
84
|
+
receive: (diff) => toChannel(topic, channel, "presence_diff", diff)
|
|
85
|
+
});
|
|
86
|
+
const offBroadcast = broadcasts.join(topic, member);
|
|
87
|
+
const offChanges = parsed.bindings.length > 0 ? hub.add({
|
|
88
|
+
caller: () => channel.caller,
|
|
89
|
+
bindings: parsed.bindings,
|
|
90
|
+
send: (payload) => toChannel(topic, channel, "postgres_changes", payload)
|
|
91
|
+
}) : () => {};
|
|
62
92
|
const channel = {
|
|
63
93
|
joinRef: message.joinRef,
|
|
64
94
|
caller,
|
|
65
|
-
|
|
95
|
+
ack: broadcast.ack === true,
|
|
96
|
+
member,
|
|
97
|
+
presence: seat,
|
|
98
|
+
remove: () => {
|
|
99
|
+
offChanges();
|
|
100
|
+
offBroadcast();
|
|
101
|
+
seat.leave();
|
|
102
|
+
}
|
|
66
103
|
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
bindings: parsed.bindings,
|
|
70
|
-
send: (payload) => toChannel(message.topic, channel, "postgres_changes", payload)
|
|
71
|
-
});
|
|
72
|
-
channels.set(message.topic, channel);
|
|
73
|
-
expireAt(message.topic, channel);
|
|
104
|
+
channels.set(topic, channel);
|
|
105
|
+
expireAt(topic, channel);
|
|
74
106
|
push(reply(message, "ok", { postgres_changes: parsed.replies }));
|
|
107
|
+
if (listens) toChannel(topic, channel, "presence_state", seat.state());
|
|
75
108
|
if (parsed.bindings.length > 0) toChannel(message.topic, channel, "system", {
|
|
76
109
|
extension: "postgres_changes",
|
|
77
110
|
status: "ok",
|
|
@@ -87,14 +120,31 @@ const realtimeSocket = ({ vsn, send, close, resolve, live, hub, nextId, report,
|
|
|
87
120
|
channel.caller = caller;
|
|
88
121
|
expireAt(message.topic, channel);
|
|
89
122
|
};
|
|
123
|
+
const sendBroadcast = (message, channel) => {
|
|
124
|
+
const { event, payload } = message.payload;
|
|
125
|
+
if (typeof event !== "string") return push(reply(message, "error", { reason: "A broadcast needs an event" }));
|
|
126
|
+
if (!fitsFrame(message.topic) || !fitsFrame(event)) return push(reply(message, "error", { reason: "A broadcast topic and event are at most 255 bytes each" }));
|
|
127
|
+
broadcasts.send(message.topic, channel.member, event, payload);
|
|
128
|
+
if (channel.ack) push(reply(message, "ok", {}));
|
|
129
|
+
};
|
|
130
|
+
const updatePresence = (message, channel) => {
|
|
131
|
+
const { event, payload } = message.payload;
|
|
132
|
+
if (event === "track" && isRecord(payload)) channel.presence.track(payload);
|
|
133
|
+
else if (event === "untrack") channel.presence.untrack();
|
|
134
|
+
else return push(reply(message, "error", { reason: "A presence message tracks an object or untracks" }));
|
|
135
|
+
push(reply(message, "ok", {}));
|
|
136
|
+
};
|
|
90
137
|
const handle = async (message) => {
|
|
91
138
|
if (message.topic === "phoenix") {
|
|
92
139
|
if (message.event === "heartbeat") push(reply(message, "ok", {}));
|
|
93
140
|
return;
|
|
94
141
|
}
|
|
95
142
|
if (message.event === "phx_join") return join(message);
|
|
96
|
-
|
|
143
|
+
const channel = channels.get(message.topic);
|
|
144
|
+
if (!channel) return push(reply(message, "error", { reason: "unmatched topic" }));
|
|
97
145
|
if (message.event === "access_token") return refresh(message);
|
|
146
|
+
if (message.event === "broadcast") return sendBroadcast(message, channel);
|
|
147
|
+
if (message.event === "presence") return updatePresence(message, channel);
|
|
98
148
|
if (message.event === "phx_leave") {
|
|
99
149
|
leave(message.topic);
|
|
100
150
|
push(reply(message, "ok", {}));
|
|
@@ -106,7 +156,7 @@ const realtimeSocket = ({ vsn, send, close, resolve, live, hub, nextId, report,
|
|
|
106
156
|
payload: {}
|
|
107
157
|
});
|
|
108
158
|
}
|
|
109
|
-
push(reply(message, "error", { reason: `Not implemented: ${message.event}
|
|
159
|
+
push(reply(message, "error", { reason: `Not implemented: ${message.event}` }));
|
|
110
160
|
};
|
|
111
161
|
const stayAwake = () => {
|
|
112
162
|
clearTimeout(idle);
|
|
@@ -114,10 +164,10 @@ const realtimeSocket = ({ vsn, send, close, resolve, live, hub, nextId, report,
|
|
|
114
164
|
};
|
|
115
165
|
stayAwake();
|
|
116
166
|
return {
|
|
117
|
-
/** Handles a
|
|
167
|
+
/** Handles a frame after those before it: text, or binary for a broadcast. */
|
|
118
168
|
message: (frame) => {
|
|
119
169
|
stayAwake();
|
|
120
|
-
const message = decodeMessage(vsn, frame);
|
|
170
|
+
const message = typeof frame === "string" ? decodeMessage(vsn, frame) : decodeBroadcastPush(frame);
|
|
121
171
|
if (!message) return;
|
|
122
172
|
queue = queue.then(() => handle(message)).catch((error) => {
|
|
123
173
|
report(error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.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": {
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@better-auth/passkey": "^1.7.7",
|
|
34
34
|
"@better-auth/sso": "^1.7.7",
|
|
35
|
-
"@protobase/layout": "0.26.
|
|
36
|
-
"@protobase/query": "0.26.
|
|
37
|
-
"@protobase/schema": "0.26.
|
|
35
|
+
"@protobase/layout": "0.26.2",
|
|
36
|
+
"@protobase/query": "0.26.2",
|
|
37
|
+
"@protobase/schema": "0.26.2",
|
|
38
38
|
"@react-email/render": "^2.1.0",
|
|
39
39
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
40
40
|
"@sentry/core": "^11.7.0",
|