Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
cfc5dec
feat(realtime): add realtime namespace with subscribe/send
ImriKochWix Jun 30, 2026
30b4ed6
fix: add package-lock.json with partysocket, fix .npmrc syntax
ImriKochWix Jun 30, 2026
efdc8d9
feat(realtime): export RealtimeHandler type from SDK
ImriKochWix Jun 30, 2026
c84c44c
feat(realtime): typed subscribe/send with RealtimeHandlerRegistry
ImriKochWix Jun 30, 2026
c42d1e6
revert: restore .npmrc to pre-branch state
ImriKochWix Jun 30, 2026
b82ccf2
fix(realtime): preserve party+room in updateProperties to fix WebSock…
ImriKochWix Jun 30, 2026
308c687
fix(realtime): use startClosed + reconnect after token fetch
ImriKochWix Jun 30, 2026
ddb6f6d
feat(realtime): add storage getter and onStart hook to RealtimeHandler
ImriKochWix Jul 1, 2026
9260662
feat(realtime): add createServiceClient() + fix kebab party routing +…
ImriKochWix Jul 1, 2026
e1f770a
fix(realtime): revert kebab-case party conversion — dispatcher routes…
ImriKochWix Jul 1, 2026
f9a22d2
fix(realtime): add client heartbeat to detect half-open connections
ImriKochWix Jul 2, 2026
9be245c
fix(realtime): tighten heartbeat to 1s ping / 3s dead for realtime UX
ImriKochWix Jul 2, 2026
d9732eb
feat(realtime): expose connection id from subscribe
ImriKochWix Jul 5, 2026
57dc67b
feat(realtime): add managed-ticker API to RealtimeHandler type
ImriKochWix Jul 5, 2026
9f47622
fix(realtime): add conn.id to the Conn type stub
ImriKochWix Jul 5, 2026
f922705
feat(realtime): type handler on both message directions
ImriKochWix Jul 5, 2026
1939e67
feat(realtime): carry connection id inside the signed realtime token
ImriKochWix Jul 5, 2026
133f94c
feat(realtime)!: registry keys inbound/outbound -> toClient/toServer
ImriKochWix Jul 5, 2026
f4493de
feat(realtime): send Base44-Functions-Version on token requests
ImriKochWix Jul 7, 2026
95ea4dc
feat(realtime): deliver user session token in-band for createUserClient
ImriKochWix Jul 7, 2026
f6a9a48
feat(realtime): webSocketImpl option for runtimes without global WebS…
ImriKochWix Jul 7, 2026
93da902
feat(realtime): drop in-band __auth — handler acts as user by verifie…
ImriKochWix Jul 7, 2026
3e361c8
realtime: createUserClient returns anonymous client for anon connections
ImriKochWix Jul 7, 2026
32683e3
realtime: rename createUserClient -> createClientFromConnection
ImriKochWix Jul 7, 2026
241ee17
realtime: default WS to the app origin, not the API host
ImriKochWix Jul 9, 2026
cdf4869
ci: unblock preview publish + drop dead eslint directive
ImriKochWix Jul 9, 2026
9202263
refactor(sdk): rename RealtimeHandler -> Actor (realtime -> actors)
ImriKochWix Jul 9, 2026
932544b
Merge remote-tracking branch 'origin/main' into feat/realtime-handler
ImriKochWix Jul 9, 2026
43e4e53
feat(actor): handleStart wake hook replaces user onStart overrides
ImriKochWix Jul 12, 2026
227561d
feat(actor): Storage.deleteAll for match-end room cleanup
ImriKochWix Jul 19, 2026
76e440c
feat(actors): phase 1 — drop createServiceClient / createClientFromCo…
ImriKochWix Jul 22, 2026
860daa4
feat(actors): connect without a token mint — auth rides the WS query
ImriKochWix Jul 22, 2026
3c91616
chore: retrigger preview publish (dropped workflow event)
ImriKochWix Jul 22, 2026
e370e5f
Merge remote-tracking branch 'origin/main' into feat/realtime-handler
ImriKochWix Jul 22, 2026
b501f2e
feat(actors): room-handle API — actors.Name(id).connect() then subscr…
ImriKochWix Jul 22, 2026
8541b38
chore(actors): trim over-explanatory comments in the actors module
ImriKochWix Jul 23, 2026
c961d5f
refactor(actor): drop Node<22 support, rename option to actorsWsUrl, …
ImriKochWix Jul 28, 2026
461840e
ci: drop preview-publish.yml comment (unrelated to actors)
ImriKochWix Jul 28, 2026
06f9447
chore(actor): trim redundant inline comments in actors module
ImriKochWix Jul 28, 2026
a181d61
chore(actor): drop redundant getAuthToken call-site comment
ImriKochWix Jul 28, 2026
d06d15a
fix(actor): close actor rooms on client.cleanup()
ImriKochWix Jul 28, 2026
543d9e6
fix(actor): don't let the actors Proxy look thenable
ImriKochWix Jul 28, 2026
973772e
fix(actor): clear connId on close() so id is valid only while connected
ImriKochWix Jul 28, 2026
41f7567
refactor(actor): split closeAll off the actors Proxy
ImriKochWix Jul 28, 2026
c4c2094
test(actor): cover heartbeat, fv, and ws-url derivation; soften send doc
ImriKochWix Jul 28, 2026
e75ce02
refactor(actor): pass serverUrl as the socket host, drop URL-resoluti…
ImriKochWix Jul 29, 2026
9717c67
feat(actor): add type-only this.client getter on Actor base class
ImriKochWix Jul 29, 2026
dce51fa
fix(actor): resolve empty/relative serverUrl to the page origin for t…
ImriKochWix Jul 29, 2026
4cb5682
chore(actor): drop duplicate blank lines in actor.ts
ImriKochWix Jul 29, 2026
310a333
test(actor): assert connect() after close() opens a fresh socket
ImriKochWix Jul 29, 2026
2de08ae
docs(actor): note this.client always operates on production data
ImriKochWix Jul 29, 2026
db7cded
refactor(actor)!: connect() returns a Connection; drop the Room handle
ImriKochWix Jul 29, 2026
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
52 changes: 22 additions & 30 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
},
"dependencies": {
"axios": "^1.18.1",
"partysocket": "^0.0.23",
"socket.io-client": "^4.8.3",
"uuid": "^13.0.2"
},
Expand Down
113 changes: 113 additions & 0 deletions src/actor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Type-only base class for Actors.
*
* Import and extend this in your actor files:
* import { Actor } from "@base44/sdk";
* export class MyActor extends Actor { ... }
*
* At deploy time the bundler replaces this import with the compiled
* Cloudflare Durable Object implementation — this file provides types only.
*/

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

/**
* A single client connection. `Send` is the message type this connection accepts
* via {@link send} — the actor's *outgoing* (server→client) messages.
*/
export interface Conn<Send = unknown> {
/** Unique per-connection id (one per socket/tab), the same value the client
* receives from `subscribe()`. Use this — not userId — to identify a distinct
* client, so multiple tabs of the same user are separate connections. */
id: string;
userId: string;
appId: string;
instanceId: string;
send(data: Send): void;
reject(code: number, reason: string): void;
}

export interface Storage {
get<T>(key: string): Promise<T | undefined>;
put(key: string, value: unknown): Promise<void>;
delete(key: string): Promise<boolean>;
/** Wipe the room's entire persisted storage (match-end cleanup). Safe: a
* later rejoin re-bootstraps exactly like a brand-new room. */
deleteAll(): Promise<void>;
}

/**
* Base class for an Actor.
*
* @typeParam Incoming - messages this actor *receives* from clients
* (`handleMessage`'s `msg`) — the schema's `toServer` section.
* @typeParam Outgoing - messages this actor *sends* to clients
* (`conn.send`/`broadcast`) — the schema's `toClient` section.
*
* With a generated `schema.jsonc`, wire both from the registry so they can't drift
* from the client's types:
* ```ts
* type Reg = ActorRegistry["MyActor"];
* class MyActor extends Actor<Reg["toServer"], Reg["toClient"]> { ... }
* ```
*/
export abstract class Actor<Incoming = unknown, Outgoing = unknown> {
abstract handleConnect(conn: Conn<Outgoing>): void | Promise<void>;
abstract handleMessage(conn: Conn<Outgoing>, msg: Incoming): void | Promise<void>;
abstract handleClose(conn: Conn<Outgoing>): void | Promise<void>;
abstract handleTick(): void | Promise<void>;

/**
* Optional wake hook: runs once when the instance starts, before any
* connection is handled — safe to load persisted state here.
*/
handleStart(): void | Promise<void> {}

/**
* Managed ticker (opt-in). Override {@link shouldTick} and the platform runs
* {@link handleTick} on a timer of {@link tickIntervalMs} while it returns true,
* and stops (letting the Durable Object hibernate — no compute cost) when it
* returns false. The platform owns scheduling, rescheduling, self-heal, and
* error-safety — you don't call {@link startLoop}/{@link stopLoop}.
*
* Re-evaluated after every connect/message/close and on every tick, so keep it
* cheap and pure (no async, no side effects). Example: `return this.players >= 2`.
*/
protected tickIntervalMs = 100;
protected shouldTick?(): boolean;

protected broadcast(_data: Outgoing): void {
throw new Error("Actor.broadcast() is only available inside a deployed actor");
}

protected getConnections(): Conn<Outgoing>[] {
throw new Error("Actor.getConnections() is only available inside a deployed actor");
}

protected startLoop(_ms: number): Promise<void> {
throw new Error("Actor.startLoop() is only available inside a deployed actor");
}

protected stopLoop(): Promise<void> {
throw new Error("Actor.stopLoop() is only available inside a deployed actor");
}

protected get instanceId(): string {
throw new Error("Actor.instanceId is only available inside a deployed actor");
}

protected get storage(): Storage {
throw new Error("Actor.storage is only available inside a deployed actor");
}

/**
* 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.
* Example: `const rows = await this.client.entities.Score.list();`
*/
protected get client(): Base44Client {
throw new Error("Actor.client is only available inside a deployed actor");
}
}
15 changes: 15 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
CreateClientOptions,
} from "./client.types.js";
import { createAnalyticsModule } from "./modules/analytics.js";
import { createActorsModule, resolveActorsHost } from "./modules/actors.js";

// Re-export client types
export type { Base44Client, CreateClientConfig, CreateClientOptions };
Expand Down Expand Up @@ -163,6 +164,18 @@ 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.
host: resolveActorsHost(
serverUrl,
typeof window !== "undefined" ? window.location?.origin : undefined,
),
functionsVersion,
getAuthToken: () => token || getAccessToken(),
});

const userModules = {
entities: createEntitiesModule({
axios: axiosClient,
Expand Down Expand Up @@ -200,8 +213,10 @@ export function createClient(config: CreateClientConfig): Base44Client {
appId,
userAuthModule,
}),
actors: actorsModule.module,
cleanup: () => {
userModules.analytics.cleanup();
actorsModule.closeAll();
if (socket) {
socket.disconnect();
}
Expand Down
3 changes: 3 additions & 0 deletions src/client.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { AgentsModule } from "./modules/agents.types.js";
import type { AiGatewayModule } from "./modules/ai-gateway.types.js";
import type { AppLogsModule } from "./modules/app-logs.types.js";
import type { AnalyticsModule } from "./modules/analytics.types.js";
import type { ActorsModule } from "./modules/actors.types.js";

/**
* Options for creating a Base44 client.
Expand Down Expand Up @@ -94,6 +95,8 @@ export interface Base44Client {
analytics: AnalyticsModule;
/** {@link AppLogsModule | App logs module} for tracking app usage. */
appLogs: AppLogsModule;
/** {@link ActorsModule | Actors module} for subscribing to and sending messages via Cloudflare Durable Object-backed Actors. */
actors: ActorsModule;
/** {@link AuthModule | Auth module} for user authentication and management. */
auth: AuthModule;
/** {@link UserConnectorsModule | Connectors module} for app-user OAuth flows. */
Expand Down
13 changes: 13 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,21 @@ export type {

export type { AppLogsModule } from "./modules/app-logs.types.js";

export type {
ActorsModule,
ActorClient,
ActorRef,
Connection,
ActorSubscription,
ActorConnectOptions,
ActorNameRegistry,
ActorRegistry,
} from "./modules/actors.types.js";

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

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

export type {
ConnectorsModule,
UserConnectorsModule,
Expand Down
Loading
Loading