diff --git a/packages/sdk/scripts/mintlify-post-processing/appended-articles.json b/packages/sdk/scripts/mintlify-post-processing/appended-articles.json index 2804d07..3002ae7 100644 --- a/packages/sdk/scripts/mintlify-post-processing/appended-articles.json +++ b/packages/sdk/scripts/mintlify-post-processing/appended-articles.json @@ -22,5 +22,13 @@ "type-aliases/integrations": [ "interfaces/CoreIntegrations", "interfaces/CustomIntegrationsModule" + ], + "type-aliases/ActorsModule": [ + "interfaces/ActorRef", + "interfaces/Connection", + "interfaces/ActorSubscription", + "interfaces/ActorClient", + "interfaces/ActorRegistry", + "interfaces/ActorNameRegistry" ] } diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index e3104b8..43a7dcb 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -145,8 +145,9 @@ function processLinksInFile(filePath) { // Remove undesirable type-alias definition lines like: // > **IntegrationsModule** = `object` & `object` // > **EntitiesModule** = `TypedEntitiesModule` & `DynamicEntitiesModule` + // > **ActorsModule** = `{ [K in AllActorNames]: ... }` & `Record`\<`string`, `ActorClient`\> // These appear in type alias files using intersection types and are not useful in docs. - const typeDefinitionRegex = /^> \*\*\w+\*\* = `\w+` & `\w+`\s*$/m; + const typeDefinitionRegex = /^> \*\*\w+Module\*\* = .+\s*$/m; if (typeDefinitionRegex.test(content)) { content = content.replace(typeDefinitionRegex, ""); modified = true; @@ -1429,6 +1430,20 @@ function applyTypeDeclarationLinking(dir) { } } +/** + * Interfaces that are appended to a module page and have methods of their own. + * Each keeps its own `## ` heading with its methods directly underneath, so the + * generic `## Methods` heading that follows it is dropped, along with any + * `## Properties` section. The value renames the heading, or is null to keep the + * interface name. + */ +const TYPES_WITH_OWN_METHODS = { + EntityHandler: "Entity Handler Methods", + ActorRef: "Actor methods", + Connection: "Connection methods", + ActorSubscription: "Subscription methods", +}; + /** * Group intro sections (like "Built-in User Entity", "Generated Types") under an "Overview" heading * The Overview heading goes at the top of the page content, and intro paragraph becomes part of Overview @@ -1468,7 +1483,7 @@ function groupIntroSections(content) { // Find the first main section heading (Methods, EntityHandler, Properties, etc.) // These mark the end of intro/overview sections - const mainSectionNames = ["Methods", "EntityHandler", "Properties", "Type Definitions"]; + const mainSectionNames = ["Methods", "Properties", "Type Definitions", ...Object.keys(TYPES_WITH_OWN_METHODS)]; let mainSectionIndex = -1; for (let i = insertIndex; i < lines.length; i++) { @@ -1576,6 +1591,11 @@ function groupTypeDefinitions(content) { { types: ["AgentName", "AgentNameRegistry"], indicator: "AgentName" + }, + // Actors module + { + types: ["ActorClient", "ActorRegistry", "ActorNameRegistry"], + indicator: "ActorClient" } ]; @@ -1850,23 +1870,31 @@ function mergeSectionWithMethods(content, filePath) { for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); - // Handle EntityHandler + Methods → Entity Handler Methods - if (line === "## EntityHandler") { - // Look ahead to find the next ## Methods heading + // Handle interfaces with their own methods (see TYPES_WITH_OWN_METHODS): drop the + // generic ## Methods heading so the methods sit directly under the interface. + const typeName = line.startsWith("## ") ? line.slice(3) : ""; + if (Object.hasOwn(TYPES_WITH_OWN_METHODS, typeName)) { + // Look ahead to the next ## Methods heading, noting a ## Properties heading on the way let methodsIndex = -1; + let propertiesIndex = -1; for (let j = i + 1; j < lines.length; j++) { const nextLine = lines[j].trim(); - if (nextLine.startsWith("## ")) { + if (nextLine === "## Properties") { + propertiesIndex = j; + } else if (nextLine.startsWith("## ")) { if (nextLine === "## Methods") { methodsIndex = j; } break; } } - + if (methodsIndex !== -1) { - lines[i] = "## Entity Handler Methods"; - lines.splice(methodsIndex, 1); + lines[i] = `## ${TYPES_WITH_OWN_METHODS[typeName] ?? typeName}`; + // Drop the ## Properties section too: the heading and its content sit + // between the type heading and ## Methods. + const removeFrom = propertiesIndex !== -1 ? propertiesIndex : methodsIndex; + lines.splice(removeFrom, methodsIndex - removeFrom + 1); modified = true; } } @@ -2513,6 +2541,29 @@ function applyOverloadPresentation(dir) { } } +/** + * Tidy the generated actors page. TypeDoc inlines the ActorSubscription return + * type under `subscribe()`, which adds a duplicate `unsubscribe()` block. This + * drops the duplicate and points links to `ActorRef` at its renamed heading, Actor methods. A page + * without the expected shape is left unchanged. + */ +function restructureActorsPage() { + const file = path.join(DOCS_DIR, "content", "type-aliases", "actors.mdx"); + if (!fs.existsSync(file)) return; + + const duplicateUnsubscribe = /\n## Methods\n\n### unsubscribe\(\)[\s\S]*?<\/CodeGroup>\n/; + const content = fs.readFileSync(file, "utf-8"); + if (!duplicateUnsubscribe.test(content)) { + console.warn("Warning: actors page has an unexpected structure and was left unchanged"); + return; + } + + const tidied = content + .replace(duplicateUnsubscribe, "\n") + .replace("](#actorref)", "](#actor-methods)"); + fs.writeFileSync(file, tidied, "utf-8"); +} + function main() { console.log("Processing TypeDoc MDX files for Mintlify...\n"); @@ -2569,6 +2620,9 @@ function main() { // listed in overload-presentation.json applyOverloadPresentation(DOCS_DIR); + // Restructure the actors page so its table of contents matches other module pages + restructureActorsPage(); + // Link type names in Type Declarations sections to their corresponding headings applyTypeDeclarationLinking(DOCS_DIR); diff --git a/packages/sdk/scripts/mintlify-post-processing/types-to-delete-after-processing.json b/packages/sdk/scripts/mintlify-post-processing/types-to-delete-after-processing.json index 894f127..92d90b4 100644 --- a/packages/sdk/scripts/mintlify-post-processing/types-to-delete-after-processing.json +++ b/packages/sdk/scripts/mintlify-post-processing/types-to-delete-after-processing.json @@ -1,5 +1,6 @@ [ "AiGatewayConnection", + "ActorConnectOptions", "DeleteManyResult", "DeleteResult", "EntityAggregateResult", diff --git a/packages/sdk/scripts/mintlify-post-processing/types-to-expose.json b/packages/sdk/scripts/mintlify-post-processing/types-to-expose.json index 052a8e4..5ec2810 100644 --- a/packages/sdk/scripts/mintlify-post-processing/types-to-expose.json +++ b/packages/sdk/scripts/mintlify-post-processing/types-to-expose.json @@ -1,4 +1,12 @@ [ + "ActorClient", + "ActorConnectOptions", + "ActorNameRegistry", + "ActorRef", + "ActorRegistry", + "ActorSubscription", + "ActorsModule", + "Connection", "AgentName", "AgentNameRegistry", "AgentsModule", diff --git a/packages/sdk/src/actor.ts b/packages/sdk/src/actor.ts index 153224c..4854eea 100644 --- a/packages/sdk/src/actor.ts +++ b/packages/sdk/src/actor.ts @@ -28,8 +28,8 @@ export interface Storage { get(key: string): Promise; put(key: string, value: unknown): Promise; delete(key: string): Promise; - /** Wipe the room's entire persisted storage (match-end cleanup). Safe: a - * later rejoin re-bootstraps exactly like a brand-new room. */ + /** Deletes all persisted storage for the session. A later connection starts + * with empty storage, the same as a new session. */ deleteAll(): Promise; } diff --git a/packages/sdk/src/modules/actors.types.ts b/packages/sdk/src/modules/actors.types.ts index bb55bbe..72e5f37 100644 --- a/packages/sdk/src/modules/actors.types.ts +++ b/packages/sdk/src/modules/actors.types.ts @@ -1,15 +1,23 @@ /** - * Extend this interface to add typed `subscribe` callbacks and `send` payloads - * for your deployed Actors. + * Maps actor names to their incoming and outgoing message types. * - * This is separate from {@link ActorNameRegistry} (which is auto-generated - * by `base44 types generate`), so there are no conflicts. + * Extend this interface when you want typed actor + * messages without generating types with the CLI: + * - `toServer`: Defines incoming messages that a client sends to the actor. + * - `toClient`: Defines outgoing messages that the actor sends to connected clients. + * + * To generate types from deployed actors, use the + * [`types generate`](/developers/references/cli/commands/types-generate) CLI command. + * + * To learn how incoming and outgoing messages work, see + * [message types](/developers/backend/resources/actors/overview#message-types). * * @example * ```typescript + * // Type messages for an actor * declare module "@base44/sdk" { * interface ActorRegistry { - * ChatRoom: { + * chatRoom: { * toClient: { type: "joined" | "left" | "message"; userId?: string; from?: string; text?: string }; * toServer: { type: "message"; text: string }; * }; @@ -20,8 +28,11 @@ export interface ActorRegistry {} /** - * Auto-populated by `base44 types generate` with the names of your deployed actors. - * Do not edit this interface manually — use {@link ActorRegistry} for message types. + * Lists actor names when your project includes types generated by the CLI + * with [`types generate`](/developers/references/cli/commands/types-generate). + * + * The generated names provide autocomplete for deployed actors. To define + * incoming and outgoing message types manually, augment {@linkcode ActorRegistry}. */ export interface ActorNameRegistry {} @@ -39,83 +50,177 @@ type ToServerFor = N extends keyof ActorRegistry : unknown : unknown; -/** Options for {@link ActorRef.connect}. */ +/** + * Configures the connection that {@linkcode ActorRef.connect | connect()} opens. + */ export interface ActorConnectOptions { /** - * The connection id — becomes the actor's `conn.id`. Supply a stable value - * (e.g. persisted per tab) so a reconnect reuses the same server-side - * identity; omit for an auto-generated per-connection id. + * Connection ID that the actor receives as `conn.id`. + * + * To let the actor recognize a client when it reconnects, use a stable + * value, such as an ID stored per browser tab. If you omit this property, the + * SDK generates a new connection ID. */ id?: string; } -/** Handle for one listener registered via {@link Connection.subscribe}. */ +/** + * Represents a listener for messages from the actor, registered with + * {@linkcode Connection.subscribe | subscribe()}. + */ export interface ActorSubscription { - /** Remove this listener; other listeners and the socket stay live. */ + /** + * Removes this listener. Other listeners and the socket stay open. + * + * @example + * ```typescript + * // Remove a listener + * sub.unsubscribe(); + * ``` + */ unsubscribe(): void; } /** - * A live connection to an actor instance, returned by {@link ActorRef.connect}. - * `subscribe`/`send` are always valid — you only get a `Connection` once the - * socket has been opened, so there's no pre-connect state to guard against. + * Represents a client's [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) connection to an actor session, returned after calling + * [`connect()`](#connect). The WebSocket queues messages you send before it opens. + * + * Learn more about + * [connections](/developers/backend/resources/actors/reference#connections). */ export interface Connection { - /** The connection id (the value the actor sees as `conn.id`). */ + /** Connection ID that the actor receives as `conn.id`. */ readonly id: string; - /** Register a message listener. Multiple are allowed; returns a per-listener unsubscribe. */ + /** + * Registers a listener for messages from the actor. + * + * You can register multiple listeners on the same connection. + * + * @param callback - Callback that runs for each message the actor sends to this connection. + * @returns A subscription handle. Call `unsubscribe()` on it to remove this listener without closing the socket. + * + * @example + * ```typescript + * // Listen for messages from the actor + * const sub = conn.subscribe((msg) => { + * console.log(msg); + * }); + * + * // Stop listening without closing the socket. + * sub.unsubscribe(); + * ``` + */ subscribe(callback: (data: ToClientFor) => void): ActorSubscription; - /** Send a message. Buffered by the socket until it's open; dropped after - * {@link close}. */ + /** + * Sends a message to the actor. + * + * The WebSocket queues messages until it opens. When you call {@linkcode Connection.close | close()}, + * the WebSocket drops any messages you send afterward. + * + * @param data - Message to send to the actor. The type comes from {@linkcode ActorRegistry} when you register the actor there. + * + * @example + * ```typescript + * // Send a message to the actor + * conn.send({ type: "message", text: "Hello" }); + * ``` + */ send(data: ToServerFor): void; /** - * Tear down the socket, heartbeat, and all listeners. Safe to call more - * than once. A connection also closes itself when it fails permanently — - * see {@link ActorRef.connect}. + * Closes the connection and removes all listeners. + * + * You can call this method more than once. A connection also closes itself + * when it fails permanently. + * + * @example + * ```typescript + * // Close the connection + * conn.close(); + * ``` */ close(): void; } /** - * A handle to one actor instance — `base44.actors.MyActor(id)`. Call - * {@link connect} to open the socket and get a {@link Connection}. + * Represents a reference to an actor session, identified by actor name and session ID. + * + * Call {@linkcode ActorRef.connect | connect()} to open the WebSocket and get a [connection](/developers/backend/resources/actors/overview#connections). */ export interface ActorRef { /** - * Open the WebSocket and return the {@link Connection}. Idempotent while the - * connection is open. + * Creates or returns the connection for this session. + * + * Calling `connect()` again on the same session reference returns the same + * connection until it closes. + * + * For a sample flow, see + * [connect a client to a session](/developers/backend/resources/actors/sample-flows#connect-a-client-to-a-session). * - * A connection that fails permanently (for example, the actor doesn't exist - * or the caller isn't allowed to connect) closes itself and reports the - * error to the client's `onError` handler. Call `connect()` again after - * fixing the cause to get a fresh {@link Connection}, and re-subscribe. + * @param options - Optional connection settings, such as a stable connection ID. + * @returns The connection for this actor session. + * + * @example + * ```typescript + * // Connect to a session + * const conn = base44.actors.chatRoom("session-1").connect({ id: "tab-1" }); + * ``` */ connect(options?: ActorConnectOptions): Connection; } /** - * Client for a single named Actor — call it with an instance id to get an - * {@link ActorRef}. Typed automatically when the actor is registered in - * {@link ActorRegistry}. + * Selects a session for a named actor. + * + * TypeScript infers message types when you register the actor in + * {@linkcode ActorRegistry}. {@linkcode ActorNameRegistry} + * provides autocomplete for actor names only. */ export interface ActorClient { + /** + * Gets a reference to an actor session. + * + * Clients that specify the same actor name and session ID join the same session. + * + * @param instanceId - Session ID that identifies which session to connect to. + * @returns A reference to the actor session. + * + * @example + * ```typescript + * // Select a session + * const session = base44.actors.chatRoom("session-1"); + * ``` + */ (instanceId: string): ActorRef; } /** - * The actors module provides access to Cloudflare Durable Object-backed - * Actors deployed by the Base44 platform. + * Actors module for interacting with [actors](/developers/backend/resources/actors/overview) from your app. * - * ```typescript - * const conn = base44.actors.MyActor("room-1").connect(); - * const sub = conn.subscribe((msg) => console.log(msg)); // typed via ActorRegistry - * conn.send({ type: "message", text: "hi" }); - * sub.unsubscribe(); - * conn.close(); - * ``` + * An actor is a long-running backend process that multiple clients connect to simultaneously. A session + * is a running instance of an actor, identified by the actor name and a session ID. Each + * session manages its own state, storage, and client connections independently. + * + * The actors module provides the functionality to: + * + * - Manage connections: {@link ActorRef.connect | Open} and {@link Connection.close | close} a WebSocket connection to a session. + * - Exchange messages: {@link Connection.send | Send} and {@link Connection.subscribe | listen} for messages to and from an actor. + * - Manage subscriptions: {@link Connection.subscribe | Subscribe} and {@link ActorSubscription.unsubscribe | unsubscribe} from actor messages without closing the connection. + * + * For a sample flow, see + * [connect a client to a session](/developers/backend/resources/actors/sample-flows#connect-a-client-to-a-session). + * + * ## Authentication modes + * + * This module is available to use with a client in anonymous or user authentication mode. Access it + * through `base44.actors`. It isn't available in + * [service role authentication mode](/developers/references/sdk/getting-started/client#service-role). + * + * The actor receives each client's [identity](/developers/backend/resources/actors/reference#connections) + * when the client connects. A client that hasn't logged in connects as anonymous, and a client that + * has [logged in](/developers/references/sdk/docs/interfaces/auth) connects as authenticated. */ export type ActorsModule = { [K in AllActorNames]: K extends keyof ActorRegistry