Skip to content

feat(actor): add actors namespace with subscribe/send#212

Draft
ImriKochWix wants to merge 28 commits into
mainfrom
feat/realtime-handler
Draft

feat(actor): add actors namespace with subscribe/send#212
ImriKochWix wants to merge 28 commits into
mainfrom
feat/realtime-handler

Conversation

@ImriKochWix

@ImriKochWix ImriKochWix commented Jun 30, 2026

Copy link
Copy Markdown

Summary

PR B of 3 for the Actor feature.

  • New src/modules/actors.ts: createActorsModule returns a Proxy so base44.actors.GameRoom works dynamically. subscribe(instanceId, cb) fetches a 30s token from the backend, opens a PartySocket to wss://dispatcher/parties/{handlerName}/{instanceId}?token=..., re-fetches token on reconnect. Returns { send, close }.
  • New src/modules/actors.types.ts: exported types ActorsModule, ActorClient, ActorSubscription
  • src/client.ts: wired actors module with getToken POST to /api/apps/{appId}/actor-token, derives dispatcherWsUrl from serverUrl (https://wss://)
  • package.json: added partysocket dependency

Depends on

PR A deployed to preview (needs the token endpoint + Dispatcher WS handling live).

🤖 Generated with Claude Code

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

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown

🚀 Package Preview Available!


Install this PR's preview build with npm:

npm i @base44-preview/sdk@0.8.37-pr.212.f0764ee

Prefer not to change any import paths? Install using npm alias so your code still imports @base44/sdk:

npm i "@base44/sdk@npm:@base44-preview/sdk@0.8.37-pr.212.f0764ee"

Or add it to your package.json dependencies:

{
  "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

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown

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

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown

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

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown

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

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown

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

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown

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

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown

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

claude Bot commented Jul 1, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@claude

claude Bot commented Jul 1, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@claude

claude Bot commented Jul 1, 2026

Copy link
Copy Markdown

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

claude Bot commented Jul 2, 2026

Copy link
Copy Markdown

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

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown

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>
@ImriKochWix ImriKochWix force-pushed the feat/realtime-handler branch from d5416b2 to d9732eb Compare July 5, 2026 08:26
@claude

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown

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

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown

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

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown

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

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown

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

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown

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

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown

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

claude Bot commented Jul 7, 2026

Copy link
Copy Markdown

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

claude Bot commented Jul 7, 2026

Copy link
Copy Markdown

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

claude Bot commented Jul 7, 2026

Copy link
Copy Markdown

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

claude Bot commented Jul 7, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

ImriKochWix and others added 2 commits July 7, 2026 15:53
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@claude

claude Bot commented Jul 9, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

ImriKochWix and others added 2 commits July 9, 2026 11:34
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>
@ImriKochWix ImriKochWix force-pushed the feat/realtime-handler branch from 7a7f481 to cdf4869 Compare July 9, 2026 08:38
@claude

claude Bot commented Jul 9, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

ImriKochWix and others added 2 commits July 9, 2026 16:22
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

claude Bot commented Jul 9, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@ImriKochWix ImriKochWix changed the title feat(realtime): add realtime namespace with subscribe/send feat(actor): add actors namespace with subscribe/send Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant