feat(actor): add actors namespace with subscribe/send#212
Conversation
Add a `realtime` module to the Base44 JS SDK that lets users subscribe to and send messages to Cloudflare Durable Object-backed RealtimeHandlers deployed by the Base44 platform. Uses PartySocket for WebSocket transport with automatic token refresh on reconnect. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
🚀 Package Preview Available!Install this PR's preview build with npm: npm i @base44-preview/sdk@0.8.37-pr.212.f0764eePrefer not to change any import paths? Install using npm alias so your code still imports npm i "@base44/sdk@npm:@base44-preview/sdk@0.8.37-pr.212.f0764ee"Or add it to your {
"dependencies": {
"@base44/sdk": "npm:@base44-preview/sdk@0.8.37-pr.212.f0764ee"
}
}
Preview published to npm registry — try new features instantly! |
partysocket was added to package.json but lock file was never generated. Also fixes .npmrc: was using env-var syntax (npm_config_registry=...) instead of npmrc syntax (registry=...). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
- subscribe() now returns a sync unsubscribe function instead of Promise<RealtimeSubscription> - send() is typed via RealtimeHandlerRegistry (user-declared message types) - Add RealtimeHandlerNameRegistry for CLI codegen (no conflict with user augmentation) - Drop RealtimeSubscription in favor of the simpler sync cleanup pattern Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
…et URL
partysocket@0.0.23's updateProperties only falls back for host/room/path,
not party. Passing {query:{token}} dropped party, changing the URL from
/parties/ChatRoom/room to /party/room.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Don't connect until we have a token — avoids initial tokenless connection being rejected and the updateProperties/reconnect timing race. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Expose this.storage (DO KV) and onStart() lifecycle hook so handlers can persist and load state. Both are backed by the compiled shim at runtime; the stub implementations throw to surface misuse in local dev/test outside a deployed context. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
… async token refresh
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
… by JWT script_name, not URL party name
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
partysocket only reconnects on a browser close/error event, so a silently
dead connection (TCP alive, no data — common behind proxies/LBs) hung until
the OS idle timeout (~60s), freezing the client. Add a ping/watchdog:
send {"type":"__ping"} every 5s and force ws.reconnect() if nothing arrives
for 12s, cutting detection from ~60s to seconds. __pong acks are swallowed.
Pairs with the handler shim answering __ping so idle handlers stay proven.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
12s was too long for interactive apps (a frozen game). Realtime handlers push frequently, so ~2-3s of total silence reliably means a dead socket. Recover in ≤3s instead of ≤12s. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
subscribe() now returns { id, unsubscribe } instead of a bare unsubscribe fn,
and accepts options.id to control the connection id (stable across
reconnects/tabs if supplied, else auto-generated per connection). id matches
the handler's conn.id, so clients can identify themselves without a server
your_id message.
BREAKING: subscribe() now returns RealtimeSubscription ({ id, unsubscribe() })
instead of a bare () => void; call sub.unsubscribe() instead of the old sub().
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
d5416b2 to
d9732eb
Compare
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Add protected tickIntervalMs + optional shouldTick() so handlers get types for the platform-managed tick loop (implemented in the deployed shim). Opting in means no more startLoop/stopLoop bookkeeping in the handler. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
PR212 shipped the runtime conn.id (populated in the compiled shim's buildConn) but never added it to the type-only Conn interface, so handlers using conn.id failed to typecheck against the published SDK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
RealtimeHandler<Incoming, Outgoing> and Conn<Send> replace the unused
_State slot, so conn.send/broadcast/getConnections are typed with the
handler outgoing (server->client) messages and handleMessage with the
incoming (client->server) ones. Pair with the generated RealtimeHandlerRegistry:
type Reg = RealtimeHandlerRegistry["MyHandler"];
class MyHandler extends RealtimeHandler<Reg["outbound"], Reg["inbound"]> {}
so client and handler are typed from one schema and cannot drift.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
subscribe() resolves connId (options.id or a random UUID) and sends it in the /realtime-token body instead of relying on a WS query param — proxies between the client and the dispatcher may strip every param except ?token (velino does). The backend signs it into the token claims; the dispatcher forwards the verified claim as partyserver's _pk, so the handler's conn.id equals the value subscribe() returns, synchronously and across reconnects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Matches the renamed schema.jsonc sections (see cli). Reg["toServer"] = what the handler receives; Reg["toClient"] = what it sends — no direction flip to remember on either side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Live apps (functionsVersion: "prod") get tokens for the published realtime script; previews get the draft. Same traffic signal function calls use. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
- __auth sent on every socket open and pushed to active sockets on setToken (login/refresh); __auth_required nudges answered with the current token; platform messages never surface to the app callback - token request declares supports_inband_auth so the handler knows to wait one round-trip for the credential - RealtimeHandler type mirror gains createUserClient(conn) with docs steering RLS-respecting calls wherever a conn is in scope Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
…ocket
Node < 22 has no global WebSocket, so PartySocket construction failed for
any server-side realtime use. createClient({ webSocketImpl: WS }) passes the
implementation through. Verified live on Node 20 with the ws package.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
…d id The handler now acts as the connected user via the service token + on-behalf-of the dispatcher-verified id, so the SDK no longer needs to deliver the user's session token over the socket. Removed pushUserTokenToActiveSockets, the __auth send + open listener, __auth_required handling, getUserToken, the setToken override, and the supports_inband_auth flag on the token request. Net removal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
createClient now derives the realtime dispatcher WS URL from the app's own origin (appBaseUrl → window.location.origin → serverUrl) instead of always serverUrl, so the socket is same-origin with the running app (which proxies /parties to the backend). Explicit dispatcherWsUrl still overrides. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- preview-publish: pin npm@11 (npm@latest is now 12.x, which needs node >=22 and fails EBADENGINE on the node-20 runner) — repo-wide breakage. - realtime.ts: remove eslint-disable for @typescript-eslint/no-explicit-any; that rule isn't registered in eslint.config.js, so the directive itself errored the Lint job. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7a7f481 to
cdf4869
Compare
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Rename the Durable-Object abstraction to the actor model across the SDK: - base class RealtimeHandler -> Actor (src/actor.ts) - client.realtime namespace -> client.actors; RealtimeModule -> ActorsModule; createRealtimeModule -> createActorsModule (src/modules/actors.ts) - registries/types: RealtimeHandlerClient -> ActorClient, *Registry, *Subscription - token endpoint POST /realtime-token -> /actor-token The entity-change RealtimeEvent feature and partyserver /parties/party/room terms are intentionally left unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # .github/workflows/preview-publish.yml
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Summary
PR B of 3 for the Actor feature.
src/modules/actors.ts:createActorsModulereturns a Proxy sobase44.actors.GameRoomworks dynamically.subscribe(instanceId, cb)fetches a 30s token from the backend, opens aPartySockettowss://dispatcher/parties/{handlerName}/{instanceId}?token=..., re-fetches token on reconnect. Returns{ send, close }.src/modules/actors.types.ts: exported typesActorsModule,ActorClient,ActorSubscriptionsrc/client.ts: wiredactorsmodule withgetTokenPOST to/api/apps/{appId}/actor-token, derivesdispatcherWsUrlfromserverUrl(https://→wss://)package.json: addedpartysocketdependencyDepends on
PR A deployed to preview (needs the token endpoint + Dispatcher WS handling live).
🤖 Generated with Claude Code