@base44-preview/sdk 0.8.35-pr.212.e4f0f07 → 0.8.35-pr.212.e57a1a3
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/client.js +5 -3
- package/dist/modules/realtime.d.ts +11 -2
- package/dist/modules/realtime.js +46 -19
- package/dist/modules/realtime.types.d.ts +20 -4
- package/dist/realtime-handler.d.ts +53 -8
- package/dist/realtime-handler.js +38 -0
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -172,9 +172,11 @@ export function createClient(config) {
|
|
|
172
172
|
realtime: createRealtimeModule({
|
|
173
173
|
appId,
|
|
174
174
|
dispatcherWsUrl: resolvedDispatcherWsUrl,
|
|
175
|
-
getToken: async (handlerName, instanceId) => {
|
|
176
|
-
// axiosClient interceptors unwrap response.data, so the result is the body directly
|
|
177
|
-
|
|
175
|
+
getToken: async (handlerName, instanceId, connId) => {
|
|
176
|
+
// axiosClient interceptors unwrap response.data, so the result is the body directly.
|
|
177
|
+
// conn_id rides inside the signed token (not a WS query param) so it survives
|
|
178
|
+
// proxies that strip params; the dispatcher forwards it as the handler's conn.id.
|
|
179
|
+
const data = await axiosClient.post(`/apps/${appId}/realtime-token`, { handler_name: handlerName, instance_id: instanceId, conn_id: connId });
|
|
178
180
|
return data.token;
|
|
179
181
|
},
|
|
180
182
|
}),
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
export declare function createRealtimeModule(config: {
|
|
2
2
|
appId: string;
|
|
3
|
-
getToken(handlerName: string, instanceId: string): Promise<string>;
|
|
3
|
+
getToken(handlerName: string, instanceId: string, connId: string): Promise<string>;
|
|
4
4
|
dispatcherWsUrl: string;
|
|
5
5
|
}): Record<string, RealtimeHandler>;
|
|
6
|
+
/** Handle for an active realtime subscription. */
|
|
7
|
+
interface RealtimeSubscription {
|
|
8
|
+
/** This connection's id — the same value the handler receives as `conn.id`. */
|
|
9
|
+
id: string;
|
|
10
|
+
/** Close the subscription and its underlying socket. */
|
|
11
|
+
unsubscribe(): void;
|
|
12
|
+
}
|
|
6
13
|
interface RealtimeHandler {
|
|
7
|
-
subscribe(instanceId: string, callback: (data: unknown) => void
|
|
14
|
+
subscribe(instanceId: string, callback: (data: unknown) => void, options?: {
|
|
15
|
+
id?: string;
|
|
16
|
+
}): RealtimeSubscription;
|
|
8
17
|
send(instanceId: string, data: unknown): void;
|
|
9
18
|
}
|
|
10
19
|
export {};
|
package/dist/modules/realtime.js
CHANGED
|
@@ -8,45 +8,72 @@ export function createRealtimeModule(config) {
|
|
|
8
8
|
return new Proxy({}, {
|
|
9
9
|
get(_, handlerName) {
|
|
10
10
|
return {
|
|
11
|
-
subscribe(instanceId, callback) {
|
|
12
|
-
var _a;
|
|
11
|
+
subscribe(instanceId, callback, options) {
|
|
12
|
+
var _a, _b;
|
|
13
13
|
const key = socketKey(handlerName, instanceId);
|
|
14
14
|
// close existing if any
|
|
15
15
|
(_a = activeSockets.get(key)) === null || _a === void 0 ? void 0 : _a.close();
|
|
16
|
-
//
|
|
16
|
+
// Connection id: caller-supplied (stable — reuse across reconnects/tabs as
|
|
17
|
+
// you see fit) or auto-generated per subscription. It travels INSIDE the
|
|
18
|
+
// signed realtime token (never as a WS query param, which proxies strip);
|
|
19
|
+
// the dispatcher forwards the verified claim as partyserver's _pk, so the
|
|
20
|
+
// handler sees this exact value as conn.id. Reconnects re-mint the token
|
|
21
|
+
// with the same id, so conn.id is stable across reconnects.
|
|
22
|
+
const connId = (_b = options === null || options === void 0 ? void 0 : options.id) !== null && _b !== void 0 ? _b : crypto.randomUUID();
|
|
23
|
+
// query as async fn: called on every (re)connect, fetches a fresh token each time
|
|
17
24
|
const ws = new PartySocket({
|
|
18
25
|
host: config.dispatcherWsUrl,
|
|
19
26
|
party: handlerName,
|
|
20
27
|
room: instanceId,
|
|
21
|
-
|
|
28
|
+
query: () => config.getToken(handlerName, instanceId, connId).then((token) => ({ token })),
|
|
22
29
|
});
|
|
23
30
|
activeSockets.set(key, ws);
|
|
31
|
+
// Heartbeat / half-open detection. PartySocket only reconnects on a
|
|
32
|
+
// browser close/error event, so a silently-dead connection (TCP alive,
|
|
33
|
+
// no data — common behind proxies/LBs) hangs until the OS idle timeout
|
|
34
|
+
// (~60s). We ping periodically and force a reconnect if nothing comes
|
|
35
|
+
// back within DEAD_MS, cutting detection from ~60s to a few seconds.
|
|
36
|
+
// Pairs with the handler's setWebSocketAutoResponse("__ping"→"__pong"),
|
|
37
|
+
// so idle handlers (no app broadcasts) still keep the connection proven.
|
|
38
|
+
const PING_MS = 1000;
|
|
39
|
+
const DEAD_MS = 3000;
|
|
40
|
+
let lastMsg = Date.now();
|
|
41
|
+
const bumpAlive = () => { lastMsg = Date.now(); };
|
|
42
|
+
ws.addEventListener("open", bumpAlive);
|
|
24
43
|
ws.addEventListener("message", (ev) => {
|
|
44
|
+
bumpAlive();
|
|
45
|
+
let data;
|
|
25
46
|
try {
|
|
26
|
-
|
|
47
|
+
data = JSON.parse(ev.data);
|
|
27
48
|
}
|
|
28
49
|
catch (_a) {
|
|
29
|
-
// ignore malformed
|
|
50
|
+
return; // ignore malformed
|
|
30
51
|
}
|
|
52
|
+
// Swallow heartbeat acks — never surface them to the app.
|
|
53
|
+
if (data && typeof data === "object" && data.type === "__pong")
|
|
54
|
+
return;
|
|
55
|
+
callback(data);
|
|
31
56
|
});
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
57
|
+
const heartbeat = setInterval(() => {
|
|
58
|
+
if (Date.now() - lastMsg > DEAD_MS) {
|
|
59
|
+
bumpAlive(); // avoid a reconnect storm while the new socket comes up
|
|
60
|
+
ws.reconnect();
|
|
35
61
|
return;
|
|
62
|
+
}
|
|
36
63
|
try {
|
|
37
|
-
|
|
38
|
-
ws.updateProperties({ party: handlerName, room: instanceId, query: { token } });
|
|
39
|
-
ws.reconnect();
|
|
64
|
+
ws.send(JSON.stringify({ type: "__ping" }));
|
|
40
65
|
}
|
|
41
66
|
catch (_a) {
|
|
42
|
-
//
|
|
67
|
+
// socket not open; the watchdog above will force a reconnect
|
|
43
68
|
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
69
|
+
}, PING_MS);
|
|
70
|
+
return {
|
|
71
|
+
id: connId, // the connection id (same value the handler sees as conn.id)
|
|
72
|
+
unsubscribe() {
|
|
73
|
+
clearInterval(heartbeat);
|
|
74
|
+
activeSockets.delete(key);
|
|
75
|
+
ws.close();
|
|
76
|
+
},
|
|
50
77
|
};
|
|
51
78
|
},
|
|
52
79
|
send(instanceId, data) {
|
|
@@ -37,11 +37,27 @@ type OutboundFor<N extends string> = N extends keyof RealtimeHandlerRegistry ? R
|
|
|
37
37
|
* Typed automatically when the handler is registered in {@link RealtimeHandlerRegistry}.
|
|
38
38
|
*/
|
|
39
39
|
export interface RealtimeHandlerClient<N extends string = string> {
|
|
40
|
-
/**
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Open a WebSocket subscription. Returns a {@link RealtimeSubscription} with the
|
|
42
|
+
* connection `id` (same value the handler sees as `conn.id`) and an `unsubscribe()` method.
|
|
43
|
+
*
|
|
44
|
+
* Pass `options.id` to control the connection id (e.g. a stable per-tab id so a
|
|
45
|
+
* reconnect reuses the same server-side connection); omit it for an auto-generated
|
|
46
|
+
* per-connection id.
|
|
47
|
+
*/
|
|
48
|
+
subscribe(instanceId: string, callback: (data: InboundFor<N>) => void, options?: {
|
|
49
|
+
id?: string;
|
|
50
|
+
}): RealtimeSubscription;
|
|
42
51
|
/** Send a message over the open socket. Throws if not subscribed. */
|
|
43
52
|
send(instanceId: string, data: OutboundFor<N>): void;
|
|
44
53
|
}
|
|
54
|
+
/** Handle for an active realtime subscription. */
|
|
55
|
+
export interface RealtimeSubscription {
|
|
56
|
+
/** This connection's id — the same value the handler receives as `conn.id`. */
|
|
57
|
+
id: string;
|
|
58
|
+
/** Close the subscription and its underlying socket. */
|
|
59
|
+
unsubscribe(): void;
|
|
60
|
+
}
|
|
45
61
|
/**
|
|
46
62
|
* The realtime module provides access to Cloudflare Durable Object-backed
|
|
47
63
|
* RealtimeHandlers deployed by the Base44 platform.
|
|
@@ -51,8 +67,8 @@ export interface RealtimeHandlerClient<N extends string = string> {
|
|
|
51
67
|
* const sub = await base44.realtime.MyHandler.subscribe("room-1", (msg) => {
|
|
52
68
|
* console.log(msg); // typed if MyHandler is in RealtimeHandlerRegistry
|
|
53
69
|
* });
|
|
54
|
-
*
|
|
55
|
-
*
|
|
70
|
+
* const { id, unsubscribe } = sub;
|
|
71
|
+
* unsubscribe();
|
|
56
72
|
* ```
|
|
57
73
|
*/
|
|
58
74
|
export type RealtimeModule = {
|
|
@@ -8,20 +8,65 @@
|
|
|
8
8
|
* At deploy time the bundler replaces this import with the compiled
|
|
9
9
|
* Cloudflare Durable Object implementation — this file provides types only.
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
import type { Base44Client } from "./client.types.js";
|
|
12
|
+
/**
|
|
13
|
+
* A single client connection. `Send` is the message type this connection accepts
|
|
14
|
+
* via {@link send} — the handler's *outgoing* (server→client) messages.
|
|
15
|
+
*/
|
|
16
|
+
export interface Conn<Send = unknown> {
|
|
17
|
+
/** Unique per-connection id (one per socket/tab), the same value the client
|
|
18
|
+
* receives from `subscribe()`. Use this — not userId — to identify a distinct
|
|
19
|
+
* client, so multiple tabs of the same user are separate connections. */
|
|
20
|
+
id: string;
|
|
12
21
|
userId: string;
|
|
13
22
|
appId: string;
|
|
14
23
|
instanceId: string;
|
|
15
|
-
send(data:
|
|
24
|
+
send(data: Send): void;
|
|
16
25
|
reject(code: number, reason: string): void;
|
|
17
26
|
}
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
export interface Storage {
|
|
28
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
29
|
+
put(key: string, value: unknown): Promise<void>;
|
|
30
|
+
delete(key: string): Promise<boolean>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Base class for a Realtime Handler.
|
|
34
|
+
*
|
|
35
|
+
* @typeParam Incoming - messages this handler *receives* from clients
|
|
36
|
+
* (`handleMessage`'s `msg`) — the client's outbound direction.
|
|
37
|
+
* @typeParam Outgoing - messages this handler *sends* to clients
|
|
38
|
+
* (`conn.send`/`broadcast`) — the client's inbound direction.
|
|
39
|
+
*
|
|
40
|
+
* With a generated `schema.jsonc`, wire both from the registry so they can't drift
|
|
41
|
+
* from the client's types:
|
|
42
|
+
* ```ts
|
|
43
|
+
* type Reg = RealtimeHandlerRegistry["MyHandler"];
|
|
44
|
+
* class MyHandler extends RealtimeHandler<Reg["outbound"], Reg["inbound"]> { ... }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare abstract class RealtimeHandler<Incoming = unknown, Outgoing = unknown> {
|
|
48
|
+
abstract handleConnect(conn: Conn<Outgoing>): void | Promise<void>;
|
|
49
|
+
abstract handleMessage(conn: Conn<Outgoing>, msg: Incoming): void | Promise<void>;
|
|
50
|
+
abstract handleClose(conn: Conn<Outgoing>): void | Promise<void>;
|
|
22
51
|
abstract handleTick(): void | Promise<void>;
|
|
23
|
-
|
|
24
|
-
|
|
52
|
+
onStart(): void | Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Managed ticker (opt-in). Override {@link shouldTick} and the platform runs
|
|
55
|
+
* {@link handleTick} on a timer of {@link tickIntervalMs} while it returns true,
|
|
56
|
+
* and stops (letting the Durable Object hibernate — no compute cost) when it
|
|
57
|
+
* returns false. The platform owns scheduling, rescheduling, self-heal, and
|
|
58
|
+
* error-safety — you don't call {@link startLoop}/{@link stopLoop}.
|
|
59
|
+
*
|
|
60
|
+
* Re-evaluated after every connect/message/close and on every tick, so keep it
|
|
61
|
+
* cheap and pure (no async, no side effects). Example: `return this.players >= 2`.
|
|
62
|
+
*/
|
|
63
|
+
protected tickIntervalMs: number;
|
|
64
|
+
protected shouldTick?(): boolean;
|
|
65
|
+
protected broadcast(_data: Outgoing): void;
|
|
66
|
+
protected getConnections(): Conn<Outgoing>[];
|
|
25
67
|
protected startLoop(_ms: number): Promise<void>;
|
|
26
68
|
protected stopLoop(): Promise<void>;
|
|
69
|
+
protected get instanceId(): string;
|
|
70
|
+
protected get storage(): Storage;
|
|
71
|
+
protected createServiceClient(): Base44Client;
|
|
27
72
|
}
|
package/dist/realtime-handler.js
CHANGED
|
@@ -8,7 +8,36 @@
|
|
|
8
8
|
* At deploy time the bundler replaces this import with the compiled
|
|
9
9
|
* Cloudflare Durable Object implementation — this file provides types only.
|
|
10
10
|
*/
|
|
11
|
+
/**
|
|
12
|
+
* Base class for a Realtime Handler.
|
|
13
|
+
*
|
|
14
|
+
* @typeParam Incoming - messages this handler *receives* from clients
|
|
15
|
+
* (`handleMessage`'s `msg`) — the client's outbound direction.
|
|
16
|
+
* @typeParam Outgoing - messages this handler *sends* to clients
|
|
17
|
+
* (`conn.send`/`broadcast`) — the client's inbound direction.
|
|
18
|
+
*
|
|
19
|
+
* With a generated `schema.jsonc`, wire both from the registry so they can't drift
|
|
20
|
+
* from the client's types:
|
|
21
|
+
* ```ts
|
|
22
|
+
* type Reg = RealtimeHandlerRegistry["MyHandler"];
|
|
23
|
+
* class MyHandler extends RealtimeHandler<Reg["outbound"], Reg["inbound"]> { ... }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
11
26
|
export class RealtimeHandler {
|
|
27
|
+
constructor() {
|
|
28
|
+
/**
|
|
29
|
+
* Managed ticker (opt-in). Override {@link shouldTick} and the platform runs
|
|
30
|
+
* {@link handleTick} on a timer of {@link tickIntervalMs} while it returns true,
|
|
31
|
+
* and stops (letting the Durable Object hibernate — no compute cost) when it
|
|
32
|
+
* returns false. The platform owns scheduling, rescheduling, self-heal, and
|
|
33
|
+
* error-safety — you don't call {@link startLoop}/{@link stopLoop}.
|
|
34
|
+
*
|
|
35
|
+
* Re-evaluated after every connect/message/close and on every tick, so keep it
|
|
36
|
+
* cheap and pure (no async, no side effects). Example: `return this.players >= 2`.
|
|
37
|
+
*/
|
|
38
|
+
this.tickIntervalMs = 100;
|
|
39
|
+
}
|
|
40
|
+
onStart() { }
|
|
12
41
|
broadcast(_data) {
|
|
13
42
|
throw new Error("RealtimeHandler.broadcast() is only available inside a deployed handler");
|
|
14
43
|
}
|
|
@@ -21,4 +50,13 @@ export class RealtimeHandler {
|
|
|
21
50
|
stopLoop() {
|
|
22
51
|
throw new Error("RealtimeHandler.stopLoop() is only available inside a deployed handler");
|
|
23
52
|
}
|
|
53
|
+
get instanceId() {
|
|
54
|
+
throw new Error("RealtimeHandler.instanceId is only available inside a deployed handler");
|
|
55
|
+
}
|
|
56
|
+
get storage() {
|
|
57
|
+
throw new Error("RealtimeHandler.storage is only available inside a deployed handler");
|
|
58
|
+
}
|
|
59
|
+
createServiceClient() {
|
|
60
|
+
throw new Error("RealtimeHandler.createServiceClient() is only available inside a deployed handler");
|
|
61
|
+
}
|
|
24
62
|
}
|