Skip to content

Commit c84c44c

Browse files
ImriKochWixclaude
andcommitted
feat(realtime): typed subscribe/send with RealtimeHandlerRegistry
- subscribe() now returns a sync unsubscribe function instead of Promise<RealtimeSubscription> - send() is typed via RealtimeHandlerRegistry (user-declared message types) - Add RealtimeHandlerNameRegistry for CLI codegen (no conflict with user augmentation) - Drop RealtimeSubscription in favor of the simpler sync cleanup pattern Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent efdc8d9 commit c84c44c

3 files changed

Lines changed: 63 additions & 43 deletions

File tree

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ export type { AppLogsModule } from "./modules/app-logs.types.js";
105105
export type {
106106
RealtimeModule,
107107
RealtimeHandlerClient,
108-
RealtimeSubscription,
108+
RealtimeHandlerNameRegistry,
109+
RealtimeHandlerRegistry,
109110
} from "./modules/realtime.types.js";
110111

111112
export type { SsoModule, SsoAccessTokenResponse } from "./modules/sso.types.js";

src/modules/realtime.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,24 @@ export function createRealtimeModule(config: {
1515
return new Proxy({} as Record<string, RealtimeHandler>, {
1616
get(_, handlerName: string) {
1717
return {
18-
async subscribe(
19-
instanceId: string,
20-
callback: (data: unknown) => void,
21-
): Promise<{ send(data: unknown): void; close(): void }> {
18+
subscribe(instanceId: string, callback: (data: unknown) => void): () => void {
2219
const key = socketKey(handlerName, instanceId);
2320
// close existing if any
2421
activeSockets.get(key)?.close();
2522

26-
const token = await config.getToken(handlerName, instanceId);
2723
const ws = new PartySocket({
2824
host: config.dispatcherWsUrl,
2925
party: handlerName,
3026
room: instanceId,
31-
query: { token },
3227
});
3328

3429
activeSockets.set(key, ws);
3530

31+
// Fetch token and attach on connect
32+
config.getToken(handlerName, instanceId).then((token) => {
33+
ws.updateProperties({ query: { token } });
34+
});
35+
3636
ws.addEventListener("message", (ev) => {
3737
try {
3838
callback(JSON.parse(ev.data));
@@ -52,14 +52,9 @@ export function createRealtimeModule(config: {
5252
}
5353
});
5454

55-
return {
56-
send(data: unknown) {
57-
ws.send(JSON.stringify(data));
58-
},
59-
close() {
60-
activeSockets.delete(key);
61-
ws.close();
62-
},
55+
return () => {
56+
activeSockets.delete(key);
57+
ws.close();
6358
};
6459
},
6560
send(instanceId: string, data: unknown) {
@@ -74,6 +69,6 @@ export function createRealtimeModule(config: {
7469
}
7570

7671
interface RealtimeHandler {
77-
subscribe(instanceId: string, callback: (data: unknown) => void): Promise<{ send(data: unknown): void; close(): void }>;
72+
subscribe(instanceId: string, callback: (data: unknown) => void): () => void;
7873
send(instanceId: string, data: unknown): void;
7974
}

src/modules/realtime.types.ts

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,57 @@
11
/**
2-
* A subscription handle returned by {@link RealtimeHandlerClient.subscribe}.
2+
* Extend this interface to add typed `subscribe` callbacks and `send` payloads
3+
* for your deployed RealtimeHandlers.
4+
*
5+
* This is separate from {@link RealtimeHandlerNameRegistry} (which is auto-generated
6+
* by `base44 types generate`), so there are no conflicts.
7+
*
8+
* @example
9+
* ```typescript
10+
* declare module "@base44/sdk" {
11+
* interface RealtimeHandlerRegistry {
12+
* ChatRoom: {
13+
* inbound: { type: "joined" | "left" | "message"; userId?: string; from?: string; text?: string };
14+
* outbound: { text: string };
15+
* };
16+
* }
17+
* }
18+
* ```
319
*/
4-
export interface RealtimeSubscription {
5-
/** Send a message to all subscribers of this instance. */
6-
send(data: unknown): void;
7-
/** Close the WebSocket connection and remove the subscription. */
8-
close(): void;
9-
}
20+
export interface RealtimeHandlerRegistry {}
21+
22+
/**
23+
* Auto-populated by `base44 types generate` with the names of your deployed handlers.
24+
* Do not edit this interface manually — use {@link RealtimeHandlerRegistry} for message types.
25+
*/
26+
export interface RealtimeHandlerNameRegistry {}
27+
28+
type AllHandlerNames = keyof RealtimeHandlerRegistry | keyof RealtimeHandlerNameRegistry;
29+
30+
type InboundFor<N extends string> = N extends keyof RealtimeHandlerRegistry
31+
? RealtimeHandlerRegistry[N] extends { inbound: infer I }
32+
? I
33+
: unknown
34+
: unknown;
35+
36+
type OutboundFor<N extends string> = N extends keyof RealtimeHandlerRegistry
37+
? RealtimeHandlerRegistry[N] extends { outbound: infer O }
38+
? O
39+
: unknown
40+
: unknown;
1041

1142
/**
1243
* Client for a single named RealtimeHandler.
44+
* Typed automatically when the handler is registered in {@link RealtimeHandlerRegistry}.
1345
*/
14-
export interface RealtimeHandlerClient {
15-
/**
16-
* Subscribe to messages from a specific RealtimeHandler instance.
17-
*
18-
* @param instanceId - The instance ID of the Durable Object.
19-
* @param callback - Called with each parsed message payload.
20-
* @returns A subscription handle with `send` and `close` methods.
21-
*/
46+
export interface RealtimeHandlerClient<N extends string = string> {
47+
/** Open a WebSocket subscription. Returns a synchronous unsubscribe function. */
2248
subscribe(
2349
instanceId: string,
24-
callback: (data: unknown) => void,
25-
): Promise<RealtimeSubscription>;
50+
callback: (data: InboundFor<N>) => void,
51+
): () => void;
2652

27-
/**
28-
* Send a message to an existing active subscription.
29-
*
30-
* @param instanceId - The instance ID of the Durable Object.
31-
* @param data - The data to send (will be JSON-serialized).
32-
* @throws {Error} When no active subscription exists for this handler/instance pair.
33-
*/
34-
send(instanceId: string, data: unknown): void;
53+
/** Send a message over the open socket. Throws if not subscribed. */
54+
send(instanceId: string, data: OutboundFor<N>): void;
3555
}
3656

3757
/**
@@ -41,10 +61,14 @@ export interface RealtimeHandlerClient {
4161
* Handler names are accessed as dynamic properties on this module:
4262
* ```typescript
4363
* const sub = await base44.realtime.MyHandler.subscribe("room-1", (msg) => {
44-
* console.log(msg);
64+
* console.log(msg); // typed if MyHandler is in RealtimeHandlerRegistry
4565
* });
4666
* sub.send({ text: "hello" });
4767
* sub.close();
4868
* ```
4969
*/
50-
export type RealtimeModule = Record<string, RealtimeHandlerClient>;
70+
export type RealtimeModule = {
71+
[K in AllHandlerNames]: K extends keyof RealtimeHandlerRegistry
72+
? RealtimeHandlerClient<string & K>
73+
: RealtimeHandlerClient;
74+
} & Record<string, RealtimeHandlerClient>;

0 commit comments

Comments
 (0)