Skip to content
Open
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
13 changes: 10 additions & 3 deletions src/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@

import type { Base44Client } from "./client";

export type ActorConnectionIdentity =
| Readonly<{ type: "authenticated"; userId: string }>
| Readonly<{ type: "anonymous"; anonymousId: string }>;

/**
* A single client connection. `Send` is the message type this connection accepts
* via {@link send} — the actor's *outgoing* (server→client) messages.
Expand All @@ -20,6 +24,8 @@ export interface Conn<Send = unknown> {
* receives from `subscribe()`. Identifies a distinct client, so multiple
* tabs are separate connections. */
id: string;
/** Identity verified by the Actor Worker. Legacy Actors may not provide it. */
identity?: ActorConnectionIdentity;
send(data: Send): void;
reject(code: number, reason: string): void;
}
Expand Down Expand Up @@ -112,9 +118,10 @@ export abstract class Actor<Incoming = unknown, Outgoing = unknown> {

/**
* Anonymous Base44 client scoped to this actor instance — no user or service
* auth, so entity access is RLS-gated (same as a logged-out visitor). Always
* operates on production data: an actor runs server-side with no per-connection
* identity, so a Test DB preview selected in the editor does not apply here.
* auth, so entity access is RLS-gated (same as a logged-out visitor). A
* connection's verified `identity` is context only and is never applied to this
* client. Always operates on production data, so a Test DB preview selected in
* the editor does not apply here.
* Example: `const rows = await this.client.entities.Score.list();`
*/
protected get client(): Base44Client {
Expand Down
16 changes: 13 additions & 3 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ export function createClient(config: CreateClientConfig): Base44Client {
onError: options?.onError,
});

// Actors keeps raw responses for 409 routing and owns error reporting so
// response-validation failures use the same onError path as request failures.
const actorConnectionClient = createAxiosClient({
baseURL: `${serverUrl}/api`,
headers,
interceptResponses: false,
});

const serviceRoleHeaders = {
...headers,
...(token ? { "on-behalf-of": `Bearer ${token}` } : {}),
Expand Down Expand Up @@ -167,14 +175,16 @@ export function createClient(config: CreateClientConfig): Base44Client {

const actorsModule = createActorsModule({
appId,
// serverUrl is often relative/empty (same-origin app); PartySocket needs an
// absolute host, so fall back to the page origin.
connectionClient: actorConnectionClient,
onError: options?.onError,
// serverUrl is often relative/empty in same-origin apps, while the legacy
// WebSocket fallback needs an absolute host.
host: resolveActorsHost(
serverUrl,
typeof window !== "undefined" ? window.location?.origin : undefined,
),
functionsVersion,
getAuthToken: () => token || getAccessToken(),
getAuthToken: () => userAuthModule.getToken(),
});

const userModules = {
Expand Down
2 changes: 2 additions & 0 deletions src/client.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import type { ActorsModule } from "./modules/actors.types.js";
export interface CreateClientOptions {
/**
* Optional error handler that will be called whenever an API error occurs.
* Actor connection failures are contextual `ActorConnectionError` instances.
* Retryable Actor failures may be reported more than once.
*/
onError?: (error: Error) => void;
}
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,11 @@ export type {
ActorNameRegistry,
ActorRegistry,
} from "./modules/actors.types.js";
export { ActorConnectionError } from "./modules/actors.error.js";

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

export { Actor, type Conn } from "./actor.js";
export { Actor, type ActorConnectionIdentity, type Conn } from "./actor.js";

export type {
ConnectorsModule,
Expand Down
28 changes: 28 additions & 0 deletions src/modules/actors.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/** Contextual error for an Actor bootstrap, WebSocket, or closed-connection failure. */
export class ActorConnectionError extends Error {
/** HTTP response status, when the failure came from the bootstrap request. */
readonly status?: number;
/** WebSocket close code, when the far end closed the connection. */
readonly closeCode?: number;
/** WebSocket close reason, when the far end supplied one. */
readonly closeReason?: string;

constructor(
readonly actorName: string,
readonly instanceId: string,
readonly connectionId: string,
readonly cause: unknown,
status?: number,
closeCode?: number,
closeReason?: string,
) {
const causeMessage = cause instanceof Error ? cause.message : String(cause);
super(
`Actor "${actorName}" instance "${instanceId}" connection "${connectionId}": ${causeMessage}`,
);
this.name = "ActorConnectionError";
this.status = status;
this.closeCode = closeCode;
this.closeReason = closeReason;
}
}
Loading
Loading