@base44-preview/sdk 0.8.35-pr.212.1d74ede → 0.8.35-pr.212.23f3cb2
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 +39 -4
- package/dist/client.types.d.ts +5 -0
- package/dist/index.d.ts +1 -0
- package/dist/modules/ai-gateway.d.ts +2 -0
- package/dist/modules/ai-gateway.js +14 -0
- package/dist/modules/ai-gateway.types.d.ts +103 -0
- package/dist/modules/ai-gateway.types.js +1 -0
- package/dist/modules/realtime.d.ts +18 -2
- package/dist/modules/realtime.js +80 -19
- package/dist/modules/realtime.types.d.ts +27 -11
- package/dist/modules/types.d.ts +1 -0
- package/dist/modules/types.js +1 -0
- package/dist/realtime-handler.d.ts +67 -8
- package/dist/realtime-handler.js +55 -0
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -7,11 +7,12 @@ import { createConnectorsModule, createUserConnectorsModule, } from "./modules/c
|
|
|
7
7
|
import { getAccessToken } from "./utils/auth-utils.js";
|
|
8
8
|
import { createFunctionsModule } from "./modules/functions.js";
|
|
9
9
|
import { createAgentsModule } from "./modules/agents.js";
|
|
10
|
+
import { createAiGatewayModule } from "./modules/ai-gateway.js";
|
|
10
11
|
import { createAppLogsModule } from "./modules/app-logs.js";
|
|
11
12
|
import { createUsersModule } from "./modules/users.js";
|
|
12
13
|
import { RoomsSocket } from "./utils/socket-utils.js";
|
|
13
14
|
import { createAnalyticsModule } from "./modules/analytics.js";
|
|
14
|
-
import { createRealtimeModule } from "./modules/realtime.js";
|
|
15
|
+
import { createRealtimeModule, pushUserTokenToActiveSockets } from "./modules/realtime.js";
|
|
15
16
|
/**
|
|
16
17
|
* Creates a Base44 client.
|
|
17
18
|
*
|
|
@@ -123,6 +124,21 @@ export function createClient(config) {
|
|
|
123
124
|
appBaseUrl: normalizedAppBaseUrl,
|
|
124
125
|
serverUrl,
|
|
125
126
|
});
|
|
127
|
+
// Current user session token (axios defaults are the single source of truth —
|
|
128
|
+
// createClient({token}) and every setToken() land there). Used for in-band
|
|
129
|
+
// realtime auth; read lazily so refreshes are always picked up.
|
|
130
|
+
const getUserToken = () => {
|
|
131
|
+
var _a;
|
|
132
|
+
const h = (_a = axiosClient.defaults.headers.common) === null || _a === void 0 ? void 0 : _a["Authorization"];
|
|
133
|
+
return typeof h === "string" && h.startsWith("Bearer ") ? h.slice(7) : null;
|
|
134
|
+
};
|
|
135
|
+
// Login / token refresh must reach long-lived realtime sockets too, so the
|
|
136
|
+
// handler-side credential never goes stale mid-connection.
|
|
137
|
+
const originalSetToken = userAuthModule.setToken.bind(userAuthModule);
|
|
138
|
+
userAuthModule.setToken = (newToken, saveToStorage) => {
|
|
139
|
+
originalSetToken(newToken, saveToStorage);
|
|
140
|
+
pushUserTokenToActiveSockets(newToken);
|
|
141
|
+
};
|
|
126
142
|
// Apply the access token before any module that may issue authenticated
|
|
127
143
|
// requests during construction (notably analytics, which fires an init
|
|
128
144
|
// event whose flush calls auth.me()). Without this, the first User/me
|
|
@@ -161,6 +177,7 @@ export function createClient(config) {
|
|
|
161
177
|
serverUrl,
|
|
162
178
|
token,
|
|
163
179
|
}),
|
|
180
|
+
aiGateway: createAiGatewayModule({ serverUrl, appBaseUrl: normalizedAppBaseUrl, token }),
|
|
164
181
|
appLogs: createAppLogsModule(axiosClient, appId),
|
|
165
182
|
users: createUsersModule(axiosClient, appId),
|
|
166
183
|
analytics: createAnalyticsModule({
|
|
@@ -172,9 +189,24 @@ export function createClient(config) {
|
|
|
172
189
|
realtime: createRealtimeModule({
|
|
173
190
|
appId,
|
|
174
191
|
dispatcherWsUrl: resolvedDispatcherWsUrl,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
192
|
+
getUserToken,
|
|
193
|
+
getToken: async (handlerName, instanceId, connId) => {
|
|
194
|
+
// axiosClient interceptors unwrap response.data, so the result is the body directly.
|
|
195
|
+
// conn_id rides inside the signed token (not a WS query param) so it survives
|
|
196
|
+
// proxies that strip params; the dispatcher forwards it as the handler's conn.id.
|
|
197
|
+
// Base44-Functions-Version rides along (like function calls) so live apps get
|
|
198
|
+
// tokens for the *published* realtime script and previews get the draft.
|
|
199
|
+
const data = await axiosClient.post(`/apps/${appId}/realtime-token`, {
|
|
200
|
+
handler_name: handlerName,
|
|
201
|
+
instance_id: instanceId,
|
|
202
|
+
conn_id: connId,
|
|
203
|
+
// Declares "an __auth message follows right after connect" — the
|
|
204
|
+
// handler delays handleConnect until it arrives (signed into the
|
|
205
|
+
// token so old SDKs, which never send __auth, are never waited on).
|
|
206
|
+
supports_inband_auth: getUserToken() != null,
|
|
207
|
+
}, functionsVersion
|
|
208
|
+
? { headers: { "Base44-Functions-Version": functionsVersion } }
|
|
209
|
+
: undefined);
|
|
178
210
|
return data.token;
|
|
179
211
|
},
|
|
180
212
|
}),
|
|
@@ -212,6 +244,7 @@ export function createClient(config) {
|
|
|
212
244
|
serverUrl,
|
|
213
245
|
token,
|
|
214
246
|
}),
|
|
247
|
+
aiGateway: createAiGatewayModule({ serverUrl, appBaseUrl: normalizedAppBaseUrl, token: serviceToken }),
|
|
215
248
|
appLogs: createAppLogsModule(serviceRoleAxiosClient, appId),
|
|
216
249
|
cleanup: () => {
|
|
217
250
|
if (socket) {
|
|
@@ -361,6 +394,7 @@ export function createClientFromRequest(request) {
|
|
|
361
394
|
const serviceRoleAuthHeader = request.headers.get("Base44-Service-Authorization");
|
|
362
395
|
const appId = request.headers.get("Base44-App-Id");
|
|
363
396
|
const serverUrlHeader = request.headers.get("Base44-Api-Url");
|
|
397
|
+
const appBaseUrlHeader = request.headers.get("Base44-App-Base-Url");
|
|
364
398
|
const functionsVersion = request.headers.get("Base44-Functions-Version");
|
|
365
399
|
const stateHeader = request.headers.get("Base44-State");
|
|
366
400
|
if (!appId) {
|
|
@@ -392,6 +426,7 @@ export function createClientFromRequest(request) {
|
|
|
392
426
|
}
|
|
393
427
|
return createClient({
|
|
394
428
|
serverUrl: serverUrlHeader || "https://base44.app",
|
|
429
|
+
appBaseUrl: appBaseUrlHeader !== null && appBaseUrlHeader !== void 0 ? appBaseUrlHeader : undefined,
|
|
395
430
|
appId,
|
|
396
431
|
token: userToken,
|
|
397
432
|
serviceToken: serviceRoleToken,
|
package/dist/client.types.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { SsoModule } from "./modules/sso.types.js";
|
|
|
5
5
|
import type { ConnectorsModule, UserConnectorsModule } from "./modules/connectors.types.js";
|
|
6
6
|
import type { FunctionsModule } from "./modules/functions.types.js";
|
|
7
7
|
import type { AgentsModule } from "./modules/agents.types.js";
|
|
8
|
+
import type { AiGatewayModule } from "./modules/ai-gateway.types.js";
|
|
8
9
|
import type { AppLogsModule } from "./modules/app-logs.types.js";
|
|
9
10
|
import type { AnalyticsModule } from "./modules/analytics.types.js";
|
|
10
11
|
import type { RealtimeModule } from "./modules/realtime.types.js";
|
|
@@ -89,6 +90,8 @@ export interface CreateClientConfig {
|
|
|
89
90
|
export interface Base44Client {
|
|
90
91
|
/** {@link AgentsModule | Agents module} for managing AI agent conversations. */
|
|
91
92
|
agents: AgentsModule;
|
|
93
|
+
/** {@link AiGatewayModule | AI Gateway module} for connecting to the Base44 AI Gateway with your own SDK. */
|
|
94
|
+
aiGateway: AiGatewayModule;
|
|
92
95
|
/** {@link AnalyticsModule | Analytics module} for tracking custom events in your app. */
|
|
93
96
|
analytics: AnalyticsModule;
|
|
94
97
|
/** {@link AppLogsModule | App logs module} for tracking app usage. */
|
|
@@ -134,6 +137,8 @@ export interface Base44Client {
|
|
|
134
137
|
readonly asServiceRole: {
|
|
135
138
|
/** {@link AgentsModule | Agents module} with elevated permissions. */
|
|
136
139
|
agents: AgentsModule;
|
|
140
|
+
/** {@link AiGatewayModule | AI Gateway module} with the service-role token. */
|
|
141
|
+
aiGateway: AiGatewayModule;
|
|
137
142
|
/** {@link AppLogsModule | App logs module} with elevated permissions. */
|
|
138
143
|
appLogs: AppLogsModule;
|
|
139
144
|
/** {@link ConnectorsModule | Connectors module} for OAuth token retrieval. */
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type { AuthModule, LoginResponse, RegisterParams, VerifyOtpParams, Change
|
|
|
9
9
|
export type { IntegrationsModule, IntegrationEndpointFunction, CoreIntegrations, InvokeLLMParams, GenerateImageParams, GenerateImageResult, UploadFileParams, UploadFileResult, SendEmailParams, SendEmailResult, ExtractDataFromUploadedFileParams, ExtractDataFromUploadedFileResult, UploadPrivateFileParams, UploadPrivateFileResult, CreateFileSignedUrlParams, CreateFileSignedUrlResult, } from "./modules/integrations.types.js";
|
|
10
10
|
export type { FunctionsModule, FunctionName, FunctionNameRegistry, } from "./modules/functions.types.js";
|
|
11
11
|
export type { AgentsModule, AgentName, AgentNameRegistry, AgentConversation, AgentMessage, AgentMessageReasoning, AgentMessageToolCall, AgentMessageUsage, AgentMessageCustomContext, AgentMessageMetadata, CreateConversationParams, } from "./modules/agents.types.js";
|
|
12
|
+
export type { AiGatewayModule, AiGatewayConnection, } from "./modules/ai-gateway.types.js";
|
|
12
13
|
export type { AppLogsModule } from "./modules/app-logs.types.js";
|
|
13
14
|
export type { RealtimeModule, RealtimeHandlerClient, RealtimeHandlerNameRegistry, RealtimeHandlerRegistry, } from "./modules/realtime.types.js";
|
|
14
15
|
export type { SsoModule, SsoAccessTokenResponse } from "./modules/sso.types.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getAccessToken } from "../utils/auth-utils.js";
|
|
2
|
+
export function createAiGatewayModule({ serverUrl, appBaseUrl, token, }) {
|
|
3
|
+
const gatewayOrigin = appBaseUrl || serverUrl;
|
|
4
|
+
const connection = () => {
|
|
5
|
+
var _a;
|
|
6
|
+
return ({
|
|
7
|
+
baseURL: `${gatewayOrigin}/api/ai/openai/v1`,
|
|
8
|
+
token: (_a = token !== null && token !== void 0 ? token : getAccessToken()) !== null && _a !== void 0 ? _a : "",
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
connection,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A connection to the Base44 AI Gateway.
|
|
3
|
+
*
|
|
4
|
+
* Contains the base URL and bearer token to use with any OpenAI-compatible client
|
|
5
|
+
* (OpenAI SDK, Mastra, Vercel AI SDK, and others) pointed at the Base44 AI
|
|
6
|
+
* Gateway.
|
|
7
|
+
*/
|
|
8
|
+
export interface AiGatewayConnection {
|
|
9
|
+
/** Base URL of the gateway's OpenAI-compatible endpoint. */
|
|
10
|
+
baseURL: string;
|
|
11
|
+
/** Bearer token used to authenticate requests to the gateway. */
|
|
12
|
+
token: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Configuration for the AI Gateway module.
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export interface AiGatewayModuleConfig {
|
|
19
|
+
/** Server URL */
|
|
20
|
+
serverUrl?: string;
|
|
21
|
+
/** The app's own public base URL (e.g. https://my-app.base44.app). */
|
|
22
|
+
appBaseUrl?: string;
|
|
23
|
+
/** Authentication token */
|
|
24
|
+
token?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* AI Gateway module for calling Base44's managed AI models from your own code.
|
|
28
|
+
*
|
|
29
|
+
* The gateway exposes an OpenAI-compatible Chat Completions endpoint, so any
|
|
30
|
+
* OpenAI-compatible SDK works against it:
|
|
31
|
+
* - Build custom AI agents with agent SDKs such as Mastra or the Vercel AI SDK
|
|
32
|
+
* - Uses your app's models, billing, and credit quota, no API key to manage
|
|
33
|
+
*
|
|
34
|
+
* Available in user authentication mode (`base44.aiGateway`) and with the
|
|
35
|
+
* service-role token via `base44.asServiceRole.aiGateway`.
|
|
36
|
+
*/
|
|
37
|
+
export interface AiGatewayModule {
|
|
38
|
+
/**
|
|
39
|
+
* Gets the connection details for the Base44 AI Gateway.
|
|
40
|
+
*
|
|
41
|
+
* Returns the `baseURL` and `token` to pass to any OpenAI-compatible client.
|
|
42
|
+
*
|
|
43
|
+
* The `token` is the current caller's bearer token: the app user's token for
|
|
44
|
+
* `base44.aiGateway`, or the service-role token for `base44.asServiceRole.aiGateway`.
|
|
45
|
+
* When the caller is unauthenticated, `token` is an empty string.
|
|
46
|
+
*
|
|
47
|
+
* @returns The gateway {@linkcode AiGatewayConnection | connection} (`baseURL` and `token`).
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* // Build an AI agent with Mastra on top of the gateway, inside a backend function
|
|
52
|
+
* import { createClientFromRequest } from 'npm:@base44/sdk';
|
|
53
|
+
* import { Agent } from 'npm:@mastra/core/agent';
|
|
54
|
+
* import { createTool } from 'npm:@mastra/core/tools';
|
|
55
|
+
* import { createOpenAICompatible } from 'npm:@ai-sdk/openai-compatible';
|
|
56
|
+
* import { z } from 'npm:zod';
|
|
57
|
+
*
|
|
58
|
+
* Deno.serve(async (req) => {
|
|
59
|
+
* const base44 = createClientFromRequest(req);
|
|
60
|
+
* const { baseURL, token } = base44.aiGateway.connection();
|
|
61
|
+
* const models = createOpenAICompatible({ name: 'base44', baseURL, apiKey: token });
|
|
62
|
+
*
|
|
63
|
+
* const agent = new Agent({
|
|
64
|
+
* id: 'order-helper',
|
|
65
|
+
* name: 'order-helper',
|
|
66
|
+
* instructions: 'Help the user with their orders.',
|
|
67
|
+
* model: models('claude_sonnet_4_6'),
|
|
68
|
+
* tools: {
|
|
69
|
+
* lookupOrder: createTool({
|
|
70
|
+
* id: 'lookup-order',
|
|
71
|
+
* description: 'Fetch an order by id',
|
|
72
|
+
* inputSchema: z.object({ orderId: z.string() }),
|
|
73
|
+
* execute: async ({ orderId }) => base44.entities.Order.get(orderId),
|
|
74
|
+
* }),
|
|
75
|
+
* },
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* const { text } = await agent.generate('Where is order 123?');
|
|
79
|
+
* return Response.json({ text });
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* // Call a model directly with the OpenAI SDK
|
|
86
|
+
* import { createClientFromRequest } from 'npm:@base44/sdk';
|
|
87
|
+
* import OpenAI from 'npm:openai';
|
|
88
|
+
*
|
|
89
|
+
* Deno.serve(async (req) => {
|
|
90
|
+
* const base44 = createClientFromRequest(req);
|
|
91
|
+
* const { baseURL, token } = base44.aiGateway.connection();
|
|
92
|
+
*
|
|
93
|
+
* const openai = new OpenAI({ baseURL, apiKey: token });
|
|
94
|
+
* const res = await openai.chat.completions.create({
|
|
95
|
+
* model: 'claude_sonnet_4_6',
|
|
96
|
+
* messages: [{ role: 'user', content: 'Hello!' }],
|
|
97
|
+
* });
|
|
98
|
+
* return Response.json({ text: res.choices[0].message.content });
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
connection(): AiGatewayConnection;
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,10 +1,26 @@
|
|
|
1
|
+
/** Push a (new) user session token to every open realtime socket — called on
|
|
2
|
+
* login/refresh so long-lived connections keep a valid credential server-side. */
|
|
3
|
+
export declare function pushUserTokenToActiveSockets(token: string): void;
|
|
1
4
|
export declare function createRealtimeModule(config: {
|
|
2
5
|
appId: string;
|
|
3
|
-
getToken(handlerName: string, instanceId: string): Promise<string>;
|
|
6
|
+
getToken(handlerName: string, instanceId: string, connId: string): Promise<string>;
|
|
7
|
+
/** Current user session token, if signed in. Sent in-band ({type:"__auth"})
|
|
8
|
+
* right after every socket open — never in the URL — so the handler can act
|
|
9
|
+
* as this user (createUserClient / RLS). */
|
|
10
|
+
getUserToken?: () => string | null;
|
|
4
11
|
dispatcherWsUrl: string;
|
|
5
12
|
}): Record<string, RealtimeHandler>;
|
|
13
|
+
/** Handle for an active realtime subscription. */
|
|
14
|
+
interface RealtimeSubscription {
|
|
15
|
+
/** This connection's id — the same value the handler receives as `conn.id`. */
|
|
16
|
+
id: string;
|
|
17
|
+
/** Close the subscription and its underlying socket. */
|
|
18
|
+
unsubscribe(): void;
|
|
19
|
+
}
|
|
6
20
|
interface RealtimeHandler {
|
|
7
|
-
subscribe(instanceId: string, callback: (data: unknown) => void
|
|
21
|
+
subscribe(instanceId: string, callback: (data: unknown) => void, options?: {
|
|
22
|
+
id?: string;
|
|
23
|
+
}): RealtimeSubscription;
|
|
8
24
|
send(instanceId: string, data: unknown): void;
|
|
9
25
|
}
|
|
10
26
|
export {};
|
package/dist/modules/realtime.js
CHANGED
|
@@ -4,48 +4,109 @@ const activeSockets = new Map();
|
|
|
4
4
|
function socketKey(handlerName, instanceId) {
|
|
5
5
|
return `${handlerName}:${instanceId}`;
|
|
6
6
|
}
|
|
7
|
+
/** Push a (new) user session token to every open realtime socket — called on
|
|
8
|
+
* login/refresh so long-lived connections keep a valid credential server-side. */
|
|
9
|
+
export function pushUserTokenToActiveSockets(token) {
|
|
10
|
+
if (!token)
|
|
11
|
+
return;
|
|
12
|
+
const payload = JSON.stringify({ type: "__auth", token });
|
|
13
|
+
for (const ws of activeSockets.values()) {
|
|
14
|
+
try {
|
|
15
|
+
ws.send(payload);
|
|
16
|
+
}
|
|
17
|
+
catch ( /* not open — the open handler will send */_a) { /* not open — the open handler will send */ }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
7
20
|
export function createRealtimeModule(config) {
|
|
8
21
|
return new Proxy({}, {
|
|
9
22
|
get(_, handlerName) {
|
|
10
23
|
return {
|
|
11
|
-
subscribe(instanceId, callback) {
|
|
12
|
-
var _a;
|
|
24
|
+
subscribe(instanceId, callback, options) {
|
|
25
|
+
var _a, _b;
|
|
13
26
|
const key = socketKey(handlerName, instanceId);
|
|
14
27
|
// close existing if any
|
|
15
28
|
(_a = activeSockets.get(key)) === null || _a === void 0 ? void 0 : _a.close();
|
|
29
|
+
// Connection id: caller-supplied (stable — reuse across reconnects/tabs as
|
|
30
|
+
// you see fit) or auto-generated per subscription. It travels INSIDE the
|
|
31
|
+
// signed realtime token (never as a WS query param, which proxies strip);
|
|
32
|
+
// the dispatcher forwards the verified claim as partyserver's _pk, so the
|
|
33
|
+
// handler sees this exact value as conn.id. Reconnects re-mint the token
|
|
34
|
+
// with the same id, so conn.id is stable across reconnects.
|
|
35
|
+
const connId = (_b = options === null || options === void 0 ? void 0 : options.id) !== null && _b !== void 0 ? _b : crypto.randomUUID();
|
|
36
|
+
// query as async fn: called on every (re)connect, fetches a fresh token each time
|
|
16
37
|
const ws = new PartySocket({
|
|
17
38
|
host: config.dispatcherWsUrl,
|
|
18
39
|
party: handlerName,
|
|
19
40
|
room: instanceId,
|
|
41
|
+
query: () => config.getToken(handlerName, instanceId, connId).then((token) => ({ token })),
|
|
20
42
|
});
|
|
21
43
|
activeSockets.set(key, ws);
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
44
|
+
// In-band credential delivery (Supabase-style): the user token rides the
|
|
45
|
+
// open socket, never the URL. Sent on every open (incl. reconnects); the
|
|
46
|
+
// server may also nudge with {type:"__auth_required"} (e.g. just before
|
|
47
|
+
// the held token expires) and we answer with the current one.
|
|
48
|
+
const sendAuth = () => {
|
|
49
|
+
var _a;
|
|
50
|
+
const t = (_a = config.getUserToken) === null || _a === void 0 ? void 0 : _a.call(config);
|
|
51
|
+
if (t) {
|
|
52
|
+
try {
|
|
53
|
+
ws.send(JSON.stringify({ type: "__auth", token: t }));
|
|
54
|
+
}
|
|
55
|
+
catch ( /* not open */_b) { /* not open */ }
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
ws.addEventListener("open", sendAuth);
|
|
59
|
+
// Heartbeat / half-open detection. PartySocket only reconnects on a
|
|
60
|
+
// browser close/error event, so a silently-dead connection (TCP alive,
|
|
61
|
+
// no data — common behind proxies/LBs) hangs until the OS idle timeout
|
|
62
|
+
// (~60s). We ping periodically and force a reconnect if nothing comes
|
|
63
|
+
// back within DEAD_MS, cutting detection from ~60s to a few seconds.
|
|
64
|
+
// Pairs with the handler's setWebSocketAutoResponse("__ping"→"__pong"),
|
|
65
|
+
// so idle handlers (no app broadcasts) still keep the connection proven.
|
|
66
|
+
const PING_MS = 1000;
|
|
67
|
+
const DEAD_MS = 3000;
|
|
68
|
+
let lastMsg = Date.now();
|
|
69
|
+
const bumpAlive = () => { lastMsg = Date.now(); };
|
|
70
|
+
ws.addEventListener("open", bumpAlive);
|
|
26
71
|
ws.addEventListener("message", (ev) => {
|
|
72
|
+
bumpAlive();
|
|
73
|
+
let data;
|
|
27
74
|
try {
|
|
28
|
-
|
|
75
|
+
data = JSON.parse(ev.data);
|
|
29
76
|
}
|
|
30
77
|
catch (_a) {
|
|
31
|
-
// ignore malformed
|
|
78
|
+
return; // ignore malformed
|
|
32
79
|
}
|
|
80
|
+
// Swallow platform messages — never surface them to the app.
|
|
81
|
+
const msgType = data && typeof data === "object" ? data.type : undefined;
|
|
82
|
+
if (msgType === "__pong")
|
|
83
|
+
return;
|
|
84
|
+
if (msgType === "__auth_required") {
|
|
85
|
+
sendAuth();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
callback(data);
|
|
33
89
|
});
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
90
|
+
const heartbeat = setInterval(() => {
|
|
91
|
+
if (Date.now() - lastMsg > DEAD_MS) {
|
|
92
|
+
bumpAlive(); // avoid a reconnect storm while the new socket comes up
|
|
93
|
+
ws.reconnect();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
38
96
|
try {
|
|
39
|
-
|
|
40
|
-
ws.updateProperties({ party: handlerName, room: instanceId, query: { token: newToken } });
|
|
97
|
+
ws.send(JSON.stringify({ type: "__ping" }));
|
|
41
98
|
}
|
|
42
99
|
catch (_a) {
|
|
43
|
-
//
|
|
100
|
+
// socket not open; the watchdog above will force a reconnect
|
|
44
101
|
}
|
|
45
|
-
});
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
|
|
102
|
+
}, PING_MS);
|
|
103
|
+
return {
|
|
104
|
+
id: connId, // the connection id (same value the handler sees as conn.id)
|
|
105
|
+
unsubscribe() {
|
|
106
|
+
clearInterval(heartbeat);
|
|
107
|
+
activeSockets.delete(key);
|
|
108
|
+
ws.close();
|
|
109
|
+
},
|
|
49
110
|
};
|
|
50
111
|
},
|
|
51
112
|
send(instanceId, data) {
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* declare module "@base44/sdk" {
|
|
11
11
|
* interface RealtimeHandlerRegistry {
|
|
12
12
|
* ChatRoom: {
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* toClient: { type: "joined" | "left" | "message"; userId?: string; from?: string; text?: string };
|
|
14
|
+
* toServer: { type: "message"; text: string };
|
|
15
15
|
* };
|
|
16
16
|
* }
|
|
17
17
|
* }
|
|
@@ -26,21 +26,37 @@ export interface RealtimeHandlerRegistry {
|
|
|
26
26
|
export interface RealtimeHandlerNameRegistry {
|
|
27
27
|
}
|
|
28
28
|
type AllHandlerNames = keyof RealtimeHandlerRegistry | keyof RealtimeHandlerNameRegistry;
|
|
29
|
-
type
|
|
30
|
-
|
|
29
|
+
type ToClientFor<N extends string> = N extends keyof RealtimeHandlerRegistry ? RealtimeHandlerRegistry[N] extends {
|
|
30
|
+
toClient: infer I;
|
|
31
31
|
} ? I : unknown : unknown;
|
|
32
|
-
type
|
|
33
|
-
|
|
32
|
+
type ToServerFor<N extends string> = N extends keyof RealtimeHandlerRegistry ? RealtimeHandlerRegistry[N] extends {
|
|
33
|
+
toServer: infer O;
|
|
34
34
|
} ? O : unknown : unknown;
|
|
35
35
|
/**
|
|
36
36
|
* Client for a single named RealtimeHandler.
|
|
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: ToClientFor<N>) => void, options?: {
|
|
49
|
+
id?: string;
|
|
50
|
+
}): RealtimeSubscription;
|
|
42
51
|
/** Send a message over the open socket. Throws if not subscribed. */
|
|
43
|
-
send(instanceId: string, data:
|
|
52
|
+
send(instanceId: string, data: ToServerFor<N>): void;
|
|
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;
|
|
44
60
|
}
|
|
45
61
|
/**
|
|
46
62
|
* The realtime module provides access to Cloudflare Durable Object-backed
|
|
@@ -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 = {
|
package/dist/modules/types.d.ts
CHANGED
package/dist/modules/types.js
CHANGED
|
@@ -8,20 +8,79 @@
|
|
|
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 schema's `toServer` section.
|
|
37
|
+
* @typeParam Outgoing - messages this handler *sends* to clients
|
|
38
|
+
* (`conn.send`/`broadcast`) — the schema's `toClient` section.
|
|
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["toServer"], Reg["toClient"]> { ... }
|
|
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
|
+
/**
|
|
72
|
+
* SDK client acting **as the connected user** — every entity call respects
|
|
73
|
+
* the app's row-level security, evaluated as that user at call time. The
|
|
74
|
+
* default wherever a `conn` is in scope (connect/message/close).
|
|
75
|
+
*
|
|
76
|
+
* Throws if the connection carries no user credential (anonymous visitor,
|
|
77
|
+
* signed-out session, or an app SDK that predates in-band auth).
|
|
78
|
+
*/
|
|
79
|
+
protected createUserClient(conn: Conn): Base44Client;
|
|
80
|
+
/**
|
|
81
|
+
* Service-role SDK client — bypasses RLS. For work with **no user in scope**
|
|
82
|
+
* (tick, alarm, onStart). Inside handleMessage/handleConnect prefer
|
|
83
|
+
* `createUserClient(conn)`.
|
|
84
|
+
*/
|
|
85
|
+
protected createServiceClient(): Base44Client;
|
|
27
86
|
}
|
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 schema's `toServer` section.
|
|
16
|
+
* @typeParam Outgoing - messages this handler *sends* to clients
|
|
17
|
+
* (`conn.send`/`broadcast`) — the schema's `toClient` section.
|
|
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["toServer"], Reg["toClient"]> { ... }
|
|
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,30 @@ 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
|
+
/**
|
|
60
|
+
* SDK client acting **as the connected user** — every entity call respects
|
|
61
|
+
* the app's row-level security, evaluated as that user at call time. The
|
|
62
|
+
* default wherever a `conn` is in scope (connect/message/close).
|
|
63
|
+
*
|
|
64
|
+
* Throws if the connection carries no user credential (anonymous visitor,
|
|
65
|
+
* signed-out session, or an app SDK that predates in-band auth).
|
|
66
|
+
*/
|
|
67
|
+
createUserClient(conn) {
|
|
68
|
+
void conn;
|
|
69
|
+
throw new Error("RealtimeHandler.createUserClient() is only available inside a deployed handler");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Service-role SDK client — bypasses RLS. For work with **no user in scope**
|
|
73
|
+
* (tick, alarm, onStart). Inside handleMessage/handleConnect prefer
|
|
74
|
+
* `createUserClient(conn)`.
|
|
75
|
+
*/
|
|
76
|
+
createServiceClient() {
|
|
77
|
+
throw new Error("RealtimeHandler.createServiceClient() is only available inside a deployed handler");
|
|
78
|
+
}
|
|
24
79
|
}
|