Skip to content

Commit 9748035

Browse files
committed
support direct actors
1 parent 8c2ea8e commit 9748035

15 files changed

Lines changed: 1509 additions & 102 deletions

src/actor.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

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

14+
export type ActorConnectionIdentity =
15+
| Readonly<{ type: "authenticated"; userId: string }>
16+
| Readonly<{ type: "anonymous"; anonymousId: string }>;
17+
1418
/**
1519
* A single client connection. `Send` is the message type this connection accepts
1620
* via {@link send} — the actor's *outgoing* (server→client) messages.
@@ -20,6 +24,8 @@ export interface Conn<Send = unknown> {
2024
* receives from `subscribe()`. Identifies a distinct client, so multiple
2125
* tabs are separate connections. */
2226
id: string;
27+
/** Identity verified by the Actor Worker. Legacy Actors may not provide it. */
28+
identity?: ActorConnectionIdentity;
2329
send(data: Send): void;
2430
reject(code: number, reason: string): void;
2531
}
@@ -112,9 +118,10 @@ export abstract class Actor<Incoming = unknown, Outgoing = unknown> {
112118

113119
/**
114120
* Anonymous Base44 client scoped to this actor instance — no user or service
115-
* auth, so entity access is RLS-gated (same as a logged-out visitor). Always
116-
* operates on production data: an actor runs server-side with no per-connection
117-
* identity, so a Test DB preview selected in the editor does not apply here.
121+
* auth, so entity access is RLS-gated (same as a logged-out visitor). A
122+
* connection's verified `identity` is context only and is never applied to this
123+
* client. Always operates on production data, so a Test DB preview selected in
124+
* the editor does not apply here.
118125
* Example: `const rows = await this.client.entities.Score.list();`
119126
*/
120127
protected get client(): Base44Client {

src/client.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ export function createClient(config: CreateClientConfig): Base44Client {
124124
onError: options?.onError,
125125
});
126126

127+
// Actors keeps raw responses for 409 routing and owns error reporting so
128+
// response-validation failures use the same onError path as request failures.
129+
const actorConnectionClient = createAxiosClient({
130+
baseURL: `${serverUrl}/api`,
131+
headers,
132+
interceptResponses: false,
133+
});
134+
127135
const serviceRoleHeaders = {
128136
...headers,
129137
...(token ? { "on-behalf-of": `Bearer ${token}` } : {}),
@@ -167,14 +175,16 @@ export function createClient(config: CreateClientConfig): Base44Client {
167175

168176
const actorsModule = createActorsModule({
169177
appId,
170-
// serverUrl is often relative/empty (same-origin app); PartySocket needs an
171-
// absolute host, so fall back to the page origin.
178+
connectionClient: actorConnectionClient,
179+
onError: options?.onError,
180+
// serverUrl is often relative/empty in same-origin apps, while the legacy
181+
// WebSocket fallback needs an absolute host.
172182
host: resolveActorsHost(
173183
serverUrl,
174184
typeof window !== "undefined" ? window.location?.origin : undefined,
175185
),
176186
functionsVersion,
177-
getAuthToken: () => token || getAccessToken(),
187+
getAuthToken: () => userAuthModule.getToken(),
178188
});
179189

180190
const userModules = {

src/client.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import type { ActorsModule } from "./modules/actors.types.js";
1919
export interface CreateClientOptions {
2020
/**
2121
* Optional error handler that will be called whenever an API error occurs.
22+
* Actor connection failures are contextual `ActorConnectionError` instances.
23+
* Retryable Actor failures may be reported more than once.
2224
*/
2325
onError?: (error: Error) => void;
2426
}

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,11 @@ export type {
117117
ActorNameRegistry,
118118
ActorRegistry,
119119
} from "./modules/actors.types.js";
120+
export { ActorConnectionError } from "./modules/actors.error.js";
120121

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

123-
export { Actor, type Conn } from "./actor.js";
124+
export { Actor, type ActorConnectionIdentity, type Conn } from "./actor.js";
124125

125126
export type {
126127
ConnectorsModule,

src/modules/actors.error.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/** Contextual error for an Actor bootstrap, WebSocket, or closed-connection failure. */
2+
export class ActorConnectionError extends Error {
3+
/** HTTP response status, when the failure came from the bootstrap request. */
4+
readonly status?: number;
5+
/** WebSocket close code, when the far end closed the connection. */
6+
readonly closeCode?: number;
7+
/** WebSocket close reason, when the far end supplied one. */
8+
readonly closeReason?: string;
9+
10+
constructor(
11+
readonly actorName: string,
12+
readonly instanceId: string,
13+
readonly connectionId: string,
14+
readonly cause: unknown,
15+
status?: number,
16+
closeCode?: number,
17+
closeReason?: string,
18+
) {
19+
const causeMessage = cause instanceof Error ? cause.message : String(cause);
20+
super(
21+
`Actor "${actorName}" instance "${instanceId}" connection "${connectionId}": ${causeMessage}`,
22+
);
23+
this.name = "ActorConnectionError";
24+
this.status = status;
25+
this.closeCode = closeCode;
26+
this.closeReason = closeReason;
27+
}
28+
}

0 commit comments

Comments
 (0)