Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/skills/sdk-docs-writing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ Every public method needs: description, `@param` tags, `@returns`, and at least
- **Document side effects** explicitly (e.g., "automatically sets the token for subsequent requests").
- **Link method references.** When mentioning another SDK method or module by name in JSDoc prose, always use `{@link}` or `{@linkcode}` to create a cross-reference.
- **Never use a colon as a sentence break.** Write "Use this when you sync data from another system. Name the field..." not "...another system: name the field...". Reserve colons for `label: description` bullet-list items, not prose sentences.
- **Name the two SDKs.** Call `@base44/sdk` the "App SDK" and `@base44/platform` the "Platform SDK" wherever a page mentions both. Docs for only one package can say "the SDK".
- **Use a serial comma.** Put a comma before the final "and" or "or" in every series of three or more, as in "snapshots, events, and errors".
- **Never start a sentence with a lowercase inline-code term.** If the natural phrasing would open with one (e.g., "`week` is only supported on..."), restructure so a capitalized word leads instead (e.g., "Every date field supports `day`, `month` and `year`. The `created_date` and `updated_date` fields also support `week`.").
- **Name the place and the AI correctly.** Write "app editor" (lowercase) for the place where you build, and "the AI" or "the AI chat" for the assistant doing the work ("The AI inserts this ID", "ask the AI chat to fix it"). Never use "builder" for either. It stays only for the Builder plan, the Base44 Builder GitHub App, people who build apps ("builders", "app builders"), and exact UI labels or code names.

Expand Down
4 changes: 2 additions & 2 deletions packages/platform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ fix the cause and call `chat.connect()`.

`chat.subscribe(appId, options)` returns a handle with `appId`, `active` and
idempotent `unsubscribe()`. App IDs are 24 lowercase hexadecimal characters. One
subscription per app and at most eight subscriptions are allowed per chat session.
subscription per app and at most eight subscriptions are allowed per session.
Subscriptions can be created before connecting. `chat.close()` is terminal for that
session; call `client.chat.init(...)` again to start another.

Expand Down Expand Up @@ -230,7 +230,7 @@ exceptions, payloads and credentials are never attached.
| `delivery_overflow` | 1,000 deliveries queued for the app; subscribe again for a fresh snapshot. |
| `protocol_error` | Invalid routing or envelope. Stop and investigate. |
| `handler_failed` | An application callback failed; fix it and subscribe again. |
| `client_closed` | The chat session was explicitly closed; initialize another session. |
| `client_closed` | The session was explicitly closed; initialize another session. |

No SDK dependency, package version or lockfile changes are required. Socket.IO
remains the existing SDK dependency. Chat commands and HTTP APIs are outside this entry point.
Expand Down
22 changes: 18 additions & 4 deletions packages/platform/scripts/gen-events.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { compile } from "json-schema-to-typescript";

const source = new URL("../asyncapi.json", import.meta.url);
const target = new URL("../src/client/modules/chat.events.generated.ts", import.meta.url);
const typesTarget = new URL("../src/client/modules/chat.events.types.ts", import.meta.url);
const UNION_BEGIN = " // Generated by scripts/gen-events.mjs, begin\n";
const UNION_END = " // Generated by scripts/gen-events.mjs, end\n";
// The client handles these itself, so they are not `PlatformEvent` members. Keep in step with `PlatformEventMap`.
const clientHandled = (name) => name === "app.snapshot" || name === "session.ended" || name.startsWith("room.");
const doc = JSON.parse(await readFile(source, "utf8"));
const schemas = doc.components.schemas;
const refName = (ref) => ref.split("/").pop();
Expand Down Expand Up @@ -82,14 +87,23 @@ ${mapEntries(clientMessages)}
export const serverEventNames = ${JSON.stringify(serverEvents.map((event) => event.name))} as const;
`;

const appEvents = serverEvents.filter((event) => !clientHandled(event.name));
const unionMembers = appEvents.map((event) => ` | AppEvent<${JSON.stringify(event.name)}>\n`).join("");
const typesSource = await readFile(typesTarget, "utf8");
const begin = typesSource.indexOf(UNION_BEGIN);
const end = typesSource.indexOf(UNION_END);
if (begin === -1 || end === -1 || end < begin) throw new Error("Could not find the generated union markers in chat.events.types.ts");
const typesOutput = typesSource.slice(0, begin + UNION_BEGIN.length) + unionMembers + typesSource.slice(end);

if (process.argv.includes("--check")) {
const committed = await readFile(target, "utf8").catch(() => "");
if (committed !== output) {
console.error("chat.events.generated.ts is out of date with asyncapi.json. Run `npm run gen:events -w @base44/platform` and commit the result.");
if (committed !== output || typesSource !== typesOutput) {
console.error("chat.events.generated.ts or the PlatformEvent union is out of date with asyncapi.json. Run `npm run gen:events -w @base44/platform` and commit the result.");
process.exit(1);
}
console.log("chat.events.generated.ts is up to date.");
console.log("chat.events.generated.ts and the PlatformEvent union are up to date.");
} else {
await writeFile(target, output);
console.log(`Wrote ${Object.keys(schemas).length} schemas and ${serverEvents.length} server events.`);
await writeFile(typesTarget, typesOutput);
console.log(`Wrote ${Object.keys(schemas).length} schemas and ${serverEvents.length} server events, ${unionMembers.split("\n").length - 1} in the PlatformEvent union.`);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,17 @@
"interfaces/ChatSession",
"interfaces/SocketSession",
"type-aliases/PlatformEvent",
"interfaces/PlatformSnapshot"
],
"classes/PlatformSocketError": [
"interfaces/PlatformSnapshot",
"classes/PlatformSocketError",
"type-aliases/SessionErrorCode",
"type-aliases/AppErrorCode"
],
"interfaces/webhooks": [
"type-aliases/WebhookEvent",
"interfaces/UnknownWebhookEvent"
],
"classes/WebhookVerificationError": [
"type-aliases/WebhookVerificationErrorCode"
],
"classes/WebhookConfigurationError": [
"interfaces/UnknownWebhookEvent",
"classes/WebhookVerificationError",
"type-aliases/WebhookVerificationErrorCode",
"classes/WebhookConfigurationError",
"type-aliases/WebhookConfigurationErrorCode"
],
"functions/useBase44Chat": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"functions": "Clients",
"interfaces": "Modules",
"type-aliases": "Modules",
"classes": "Errors"
"type-aliases": "Modules"
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"path": "developers/references/platform-sdk/docs",
"dropdown": "Platform SDK",
"dropdown": "SDKs",
"group": "Platform SDK Reference",
"icon": "brackets-curly"
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@
},
"typeDefinitionGroups": [
["SocketSession", "PlatformEvent", "PlatformSnapshot"],
{ "title": "Error codes", "types": ["SessionErrorCode", "AppErrorCode"] }
{
"title": "Errors",
"types": ["PlatformSocketError", "SessionErrorCode", "AppErrorCode"],
"nested": { "SessionErrorCode": "Session error codes", "AppErrorCode": "App error codes" }
},
["WebhookEvent", "UnknownWebhookEvent"],
{
"title": "Errors",
"types": ["WebhookVerificationError", "WebhookVerificationErrorCode", "WebhookConfigurationError", "WebhookConfigurationErrorCode"],
"nested": {
"WebhookVerificationErrorCode": "Verification error codes",
"WebhookConfigurationErrorCode": "Configuration error codes"
}
}
]
}
16 changes: 10 additions & 6 deletions packages/platform/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import { createChat } from "./modules/chat.js";
/**
* Creates a platform client.
*
* This is the entry point of the Platform SDK. The client gives your white-label app editor
* access to the platform's modules, such as [`chat`](/developers/references/platform-sdk/docs/interfaces/chat).
* It takes no options and opens nothing: each chat session gets its socket session from the
* `getSession` you pass to `init()`.
* This is the entry point of the Platform SDK in the browser. Import it from `@base44/platform`.
* The client gives your white-label app editor access to the platform's modules, such as
* [`chat`](/developers/references/platform-sdk/docs/interfaces/chat). It takes no options and
* doesn't connect to anything. Each call to
* [`client.chat.init()`](/developers/references/platform-sdk/docs/interfaces/chat#init) gets a
* socket session from the `getSession()` function you pass in. On your backend, use
* [`createPlatformServer()`](/developers/references/platform-sdk/docs/functions/createPlatformServer)
* from `@base44/platform/server`.
*
* @returns A platform client with access to the platform's modules.
* @returns A new platform client.
*
* @example
* ```typescript
Expand All @@ -24,7 +28,7 @@ import { createChat } from "./modules/chat.js";
* const response = await fetch('/api/chat-socket-session', { method: 'POST' });
* return response.json();
* },
* onError: (error) => console.error(error.code),
* onError: (error) => console.error('Live updates error', error.code),
* });
* await chat.connect();
* ```
Expand Down
6 changes: 2 additions & 4 deletions packages/platform/src/client/client.types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import type { ChatModule } from "./modules/chat.types.js";

/**
* The platform client.
*
* Provides access to the platform's modules.
* The platform client, with one property for each module.
*/
export interface PlatformClient {
/** [Chat module](/developers/references/platform-sdk/docs/interfaces/chat) for following the AI chat in your users' apps live. */
/** [Chat module](/developers/references/platform-sdk/docs/interfaces/chat) for following the AI chat of your users' apps. */
readonly chat: ChatModule;
}
22 changes: 14 additions & 8 deletions packages/platform/src/client/errors.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import type { PlatformSocketErrorCode } from "./errors.types.js";

/**
* The error the SDK reports when something goes wrong in a chat session.
* The error the SDK reports when something goes wrong with live updates.
*
* You don't create it. The SDK passes it to your `onError` callbacks, `connect()` rejects with it,
* and `subscribe()` throws it. Check `code` to see what happened, then look the code up below:
* - [Session error codes](#sessionerrorcode) go to the `onError` you pass to `init()`. The whole
* You don't create these errors. The SDK passes them to your `onError()` callbacks, rejects
* `connect()` with them, and throws them from `subscribe()`. Check `code` to see what happened,
* then look the code up below:
* - Session error codes go to the `onError()` you pass to `init()`. The whole
* session is affected, and `appId` isn't set.
* - [App error codes](#apperrorcode) go to the subscription's `onError`. Only that app is affected,
* and `appId` says which one.
* - App error codes go to the subscription's `onError()`. Only that app is
* affected, and `appId` says which one.
*
* The error never carries the session token, a callback's exception, or the server's exception text.
* These errors never carry the session token, a callback's exception, or the server's exception
* text.
*
* @example
* ```typescript
* // Handle session and app errors
* const chat = client.chat.init({
* async getSession() {
* const response = await fetch('/api/chat-socket-session', { method: 'POST' });
* return response.json();
* },
* // Session error codes: the whole session is affected
* onError(error) {
* if (error.code === 'session_revoked') showSignedOut();
Expand All @@ -33,7 +39,7 @@ import type { PlatformSocketErrorCode } from "./errors.types.js";
* ```
*/
export class PlatformSocketError extends Error {
/** What happened. One of the [session error codes](#sessionerrorcode) or [app error codes](#apperrorcode). */
/** What happened. One of the session error codes or app error codes. */
readonly code: PlatformSocketErrorCode;
/** ID of the app the error is about. Set for app error codes, and not set for session error codes. */
readonly appId?: string;
Expand Down
44 changes: 25 additions & 19 deletions packages/platform/src/client/errors.types.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,47 @@
/**
* Codes of errors that affect the whole session. They go to the `onError` you pass to `init()`, and
* `connect()` rejects with them. Retryable means the same call can succeed again without changing anything.
* Codes of errors that affect the whole session. They go to the `onError()` you pass to `init()`,
* and `connect()` rejects with them. A retryable error is one where the same call can succeed again
* without changing anything.
*/
export type SessionErrorCode =
/**
* `getSession` threw, took longer than 20 seconds, or returned a session without a valid
* `session_token`, `socket_url` (an HTTP or HTTPS origin) or `socket_path` (an absolute path).
* The `getSession()` callback threw, took longer than 20 seconds, or returned a session without a
* valid `session_token`, a `socket_url` that is an HTTP or HTTPS origin, or a `socket_path` that is
* an absolute path.
*
* Check the endpoint that opens sessions, then call `connect()` again. Retryable, except that an
* invalid field needs a fix on your server first.
* Check the endpoint that opens sessions, then call `connect()` again. Retryable. If a field is
* invalid, fix it on your server first.
*/
| "session_unavailable"
/**
* Base44 refused the session even with a fresh token: the token is invalid, or live updates aren't enabled for the workspace.
* Base44 refused the session even with a fresh token. Either the token is invalid, or live updates
* aren't turned on for the workspace.
*
* Check that your server opens a new session on every call. Not retryable.
*/
| "connection_denied"
/**
* The socket couldn't connect after five retries, or the server closed it.
* The socket couldn't connect after five retries, or the server closed the connection.
*
* Call `connect()` again, for example when the tab next gets focus. Retryable.
*/
| "connection_failed"
/**
* Your server or an admin ended the session, or its key lost access.
* Your server or an admin ended the session, or the workspace API key that opened the session lost
* access.
*
* Open a new session from your server if the page should still follow apps. Not retryable.
*/
| "session_revoked"
/**
* Another socket connected with the same session token.
*
* Use one session per tab. Don't reconnect automatically; call `connect()` only to take the session back on purpose. Not retryable.
* Use one session per tab. Don't reconnect automatically. Call `connect()` only to take the
* session back on purpose. Not retryable.
*/
| "session_replaced"
/**
* A session frame didn't match the protocol.
* A message about the whole session didn't match the protocol.
*
* Update the SDK, then call `connect()` again. Not retryable.
*/
Expand All @@ -49,26 +54,27 @@ export type SessionErrorCode =
| "client_closed";

/**
* Codes of errors for one app. They go to the subscription's `onError`, and `subscribe()` throws
* `subscription_limit`. Every one ends the subscription except `snapshot_unavailable`.
* Codes of errors for a single app. They go to the subscription's `onError()`, and `subscribe()`
* throws `subscription_limit`. Every one ends the subscription except `snapshot_unavailable`.
*/
export type AppErrorCode =
/**
* The app isn't on the session's allowlist, or the socket joins too often.
* The session doesn't have access to the app, or the connection subscribed to apps too often.
*
* Add the app to the session from your server, then subscribe again. Not retryable.
*/
| "access_denied"
/**
* The app left the session's allowlist, or its workspace.
* The session lost access to the app. Your server removed the app from the session, or the app
* left the workspace.
*
* Stop showing the app's live updates. Not retryable.
*/
| "access_revoked"
/**
* The subscription is active but its snapshot failed. Events keep arriving.
* The subscription is active, but its snapshot failed. Events keep arriving.
*
* Load the app's messages over the Apps API instead.
* Load the app's messages from the Apps API instead.
*/
| "snapshot_unavailable"
/**
Expand All @@ -78,13 +84,13 @@ export type AppErrorCode =
*/
| "subscription_limit"
/**
* More than 1,000 deliveries waited for your callbacks.
* More than 1,000 snapshots and events were waiting for your callbacks.
*
* Make the callbacks faster, then subscribe again for a fresh snapshot. Retryable.
*/
| "delivery_overflow"
/**
* Your `onSnapshot` or `onEvent` callback threw or rejected.
* Your `onSnapshot()` or `onEvent()` callback threw or rejected.
*
* Fix the callback, then subscribe again. Not retryable.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/platform/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ export { PlatformSocketError } from "./errors.js";
export type { AppErrorCode, PlatformSocketErrorCode, SessionErrorCode } from "./errors.types.js";
export type { PlatformClient } from "./client.types.js";
export type { ChatModule, ChatInitOptions, ChatSession, PlatformSubscription, SocketSession, SubscriptionOptions } from "./modules/chat.types.js";
export type { GuardApproval, PlatformEvent, PlatformEventMap, PlatformSnapshot, ToolCall } from "./modules/chat.events.types.js";
export type { AppEvent, GuardApproval, PlatformEvent, PlatformEventMap, PlatformSnapshot, ToolCall, UnknownEvent } from "./modules/chat.events.types.js";
export type * from "./modules/chat.events.generated.js";
Loading
Loading