Generated Bindings
Type-safe frontend bindings generated from your Rust code and Forge macros.
When to Run It
forge generate is a manual step — it is not part of cargo build and does not run automatically when you change handler signatures. Run it whenever you:
- Add a new
#[forge::query],#[forge::mutation], or other handler - Rename or remove a handler
- Change a handler's arguments or return type
- Add, rename, or remove a
#[forge::model]or#[forge::forge_enum]
forge generate
forge generate auto-detects the frontend target from frontend/ and can also be forced with --target sveltekit or --target dioxus.
If you forget to run it, forge check will catch the drift — it diffs the current bindings against freshly generated output and reports staleness.
Automating the step
To ensure bindings never drift, add forge generate -y to your workflow in one of these places:
Pre-commit hook (.git/hooks/pre-commit):
#!/bin/sh
forge generate -y
git add frontend/src/lib/forge # or frontend/src/forge for Dioxus
package.json script (SvelteKit projects):
{
"scripts": {
"generate": "forge generate -y",
"dev": "forge generate -y && vite dev"
}
}
Makefile:
generate:
forge generate -y
dev: generate
cargo run
In CI, run forge check rather than forge generate — it validates that checked-in bindings are already up to date without overwriting them.
SvelteKit Output
Your Rust models, input DTOs, and function signatures become TypeScript:
// types.ts - generated from #[forge::model] and input DTOs
export interface User {
id: string;
email: string;
created_at: string;
}
// api.ts - generated from #[forge::query] and #[forge::mutation]
export const getUser = (args: { id: string }): Promise<User> =>
getForgeClient().call("get_user", args);
export const createUser = (args: {
name: string;
email: string;
}): Promise<User> => getForgeClient().call("create_user", args);
// Reactive subscription (Svelte store)
export const getUserStore$ = (args: { id: string }) =>
createSubscriptionStore<{ id: string }, User>("get_user", args);
What Happens
The forge generate command:
- Scans your
src/directory for models, enums, and functions - Extracts type information from Rust macros
- Generates TypeScript interfaces with correct nullability
- Creates API bindings that call your functions with type safety
- Produces subscription factories for queries with real-time support
When a function takes a single argument whose type name ends in Args or Input and that type exists in the schema registry, the generator uses it directly as the params type instead of wrapping it in a generated struct. This means your custom input DTOs pass through as-is.
Output goes to frontend/src/lib/forge/ by default for SvelteKit projects.
Dioxus Output
For Dioxus projects, Forge generates Rust bindings under frontend/src/forge/:
// api.rs - generated from #[forge::query] and #[forge::mutation]
pub struct GetUserParams {
pub id: String,
}
impl GetUserParams {
pub fn new(id: impl Into<String>) -> Self {
Self { id: id.into() }
}
}
pub async fn get_user(client: &ForgeClient, args: GetUserParams) -> Result<User, ForgeClientError> {
client.call("get_user", args).await
}
// Default: one-shot query
pub fn use_get_user(args: GetUserParams) -> QueryState<User> {
use_forge_query("get_user", args)
}
// Opt-in: real-time subscription
pub fn use_get_user_live(args: GetUserParams) -> SubscriptionState<User> {
use_forge_subscription("get_user", args)
}
// Signal variant — returns Signal<QueryState<T>> for reactive child component passing
pub fn use_get_user_signal(args: GetUserParams) -> Signal<QueryState<User>> {
use_forge_query_signal("get_user", args)
}
// Mutations return a typed Mutation handle
pub fn use_create_user() -> Mutation<CreateUserParams, User> {
use_forge_mutation("create_user")
}
The generated mod.rs also re-exports ForgeProvider, ForgeClient, error types, and the Dioxus hooks from forge-dioxus.
use_<name>() is a one-shot query. Append _live for real-time subscriptions. Append _signal for Signal<QueryState<T>> variants useful when passing reactive query state to child components.
Mutations return a Mutation<Args, Result> with a .call(args) method.
Generated params and DTO structs include new(...) constructors plus builder methods for
optional fields, so common calls avoid verbose Rust struct literals.
Dioxus Auth
The generated mod.rs re-exports ForgeAuthProvider from forge-dioxus. Wrap your app in it to provide auth context:
use crate::forge::ForgeAuthProvider;
fn App() -> Element {
rsx! {
ForgeAuthProvider {
// your routes / app content
}
}
}
Inside the provider tree, use these hooks:
| Hook | Returns | Purpose |
|---|---|---|
use_forge_auth() | ForgeAuth | Auth handle with login(), logout(), update_tokens(), viewer::<T>() |
use_viewer::<T>() | Option<T> | Typed viewer access (shorthand for use_forge_auth().viewer::<T>()) |
use_auth_key() | usize | Counter that increments on identity changes, useful for router keying |
Type Mapping
Rust to TypeScript
| Rust Type | TypeScript Type |
|---|---|
String, &str | string |
Uuid | string |
i32, i64, f32, f64 | number |
bool | boolean |
Instant | string |
LocalDate, LocalTime | string |
DateTime<Utc> | string |
NaiveDateTime | string |
NaiveDate | string |
Timestamp | string |
Option<T> | T | null |
Vec<T> | T[] |
HashMap<K, V> | Record<string, V> |
Value (JSON) | unknown |
Page<T> | Page<T> |
Cursor | string |
Upload | File | Blob |
Bytes (return) | Blob |
Bytes (arg) | Uint8Array |
Rust to Dioxus
| Rust Type | Dioxus Type |
|---|---|
String, &str | String |
Uuid | String |
Instant | String (ISO 8601) |
DateTime<Utc> | String |
NaiveDateTime | String |
NaiveDate | String |
Timestamp | String |
i32 | i32 |
i64 | i64 |
f32 | f32 |
f64 | f64 |
u32, usize, isize | i64 |
u64 | i64 |
bool | bool |
Option<T> | Option<T> |
Vec<T> | Vec<T> |
Page<T> | forge_core::Page<T> |
Upload | ForgeUpload |
serde_json::Value | JsonValue |
Bytes | Vec<u8> |
Enums to Union Types
Rust enums become TypeScript union types:
#[forge::forge_enum]
pub enum TaskStatus {
Draft,
Active,
Completed,
}
export type TaskStatus = "draft" | "active" | "completed";
Query Bindings
One-Shot RPC
Call a query once and get the result:
import { getUser, listTodos } from "$lib/forge";
// With arguments
const user = await getUser({ id: userId });
// No arguments
const todos = await listTodos();
Returns a Promise<T> that resolves with the data or rejects with a ForgeClientError.
Reactive Subscriptions
Subscribe to query changes with automatic updates:
import { getUserStore$ } from "$lib/forge";
// Create a subscription store
const userStore = getUserStore$({ id: userId });
// Use in Svelte component
$: ({ loading, data, error, stale } = $userStore);
Svelte 5 Runes
For Svelte 5 runes-based reactivity:
import { getUser$ } from '$lib/forge';
// Returns a reactive object
const user = getUser$({ id: userId });
// Access properties directly
{#if user.loading}
Loading...
{:else if user.error}
Error: {user.error.message}
{:else}
Welcome, {user.data.name}
{/if}
The $ suffix functions use $state internally for fine-grained reactivity.
SvelteKit Lower-Level APIs
The generated stores are built on top of a few primitives you can use directly:
| Function | Purpose |
|---|---|
createQueryStore(fn, args) | Underlying factory used by generated store functions. Creates a subscription store for any function name and argument object. |
setForgeClient(client) | Manually inject a ForgeClient into Svelte context. Useful for testing or custom setups where you don't use the generated provider. |
setAuthState(state) | Manually inject auth state into Svelte context. Same use case as setForgeClient but for the auth layer. |
Subscription State
All subscription stores expose the same shape:
SvelteKit:
interface SubscriptionResult<T> {
loading: boolean; // true until first data arrives
data: T | null; // the query result
error: Error | null;
stale: boolean; // reserved for reconnect-aware UIs
}
| Field | Description |
|---|---|
loading | Initial fetch in progress |
stale | Currently reserved in the Svelte runtime; use createConnectionStore() for connection status |
error | Initial query or subscription registration failed |
The current Svelte runtime keeps stale at false and exposes connection state separately through createConnectionStore().
Dioxus: The SubscriptionState<T> type also includes a connection_state: ConnectionState field that tracks the underlying SSE connection (Disconnected, Connecting, Connected). This field is not present in the Svelte runtime.
In Dioxus generated types, WorkflowStepState.status is an untyped String rather than a union type. TypeScript targets get a proper string union, but the Dioxus codegen emits a plain String since Rust doesn't have an equivalent inline union.
Mutations
Mutations return Promises; subscriptions are not required for mutation calls:
import { createUser, updateTodo, deleteTodo } from "$lib/forge";
// Create
const user = await createUser({ name: "Alice", email: "alice@example.com" });
// Update
await updateTodo({ id: todoId, completed: true });
// Delete
await deleteTodo({ id: todoId });
On success, all active subscriptions that depend on affected tables re-evaluate automatically via server-side change detection.
File Uploads
The Upload type maps to File | Blob on the client:
#[forge::mutation]
pub async fn upload_avatar(
ctx: &MutationContext,
file: Upload,
user_id: Uuid,
) -> Result<String> {
let bytes = file.bytes();
let filename = file.name();
let content_type = file.content_type();
// Store file, return URL
}
import { uploadAvatar } from "$lib/forge";
// From file input
const url = await uploadAvatar({
file: inputElement.files[0],
userId: currentUserId,
});
// From Blob
const blob = new Blob([data], { type: "application/pdf" });
await uploadDocument({ file: blob, name: "report.pdf" });
When arguments contain File or Blob, the client automatically:
- Switches to
multipart/form-dataencoding - Encodes non-file arguments into the
_jsonmultipart field - Extracts filename and content type from the
Fileobject
Auto-Reconnect
The client handles connection drops automatically:
| Behavior | Value |
|---|---|
| Max attempts | 10 |
| Initial delay | 1 second |
| Backoff | Exponential with jitter |
| Max delay | 30s (SvelteKit), 16s (Dioxus) |
The Dioxus client caps at 16 seconds (1s * 2^min(attempts, 4)), while SvelteKit caps at 30 seconds.
During reconnection:
- The SSE transport reconnects with backoff
- Active query subscriptions re-register on reconnect
- One-shot RPC calls do not retry automatically
Connection Lifecycle
import { createConnectionStore } from "$lib/forge";
const connection = createConnectionStore();
$: state = $connection; // 'disconnected' | 'connecting' | 'connected'
Connection IDs prevent race conditions. If a reconnect starts while another is in progress, the stale attempt is cancelled.
Token Change Detection
When the auth token changes, the client detects it and reconnects:
// In auth.svelte.ts
auth.setAuth(newAccessToken, newRefreshToken, user); // Triggers reconnect
auth.clearAuth(); // Triggers reconnect
The client hashes the current token and compares it before each subscription registration. If the hash differs from the connected token, it triggers a full reconnect to re-authenticate subscriptions.
This handles:
- User login (token acquired)
- Token refresh (token replaced)
- User logout (token cleared)
Generated Files
SvelteKit files
After forge generate, a SvelteKit target gets:
| File | Contents |
|---|---|
types.ts | Interfaces from #[forge::model], input DTOs, and union types from #[forge::forge_enum] |
api.ts | Query, mutation, job, and workflow bindings |
stores.ts | Re-exports from @forge-rs/svelte |
runes.svelte.ts | Svelte 5 toReactive() and toReactiveMutation() helpers, plus ReactiveQuery and ReactiveMutation interfaces |
reactive.svelte.ts | Runes-based subscription functions (getUser$), generated when queries exist |
auth.svelte.ts | Auth store (when auth configured) |
index.ts | Barrel export |
Dioxus files
After forge generate --target dioxus, a Dioxus target gets:
| File | Contents |
|---|---|
types.rs | Rust structs for models and input DTOs, plus enums from #[forge::forge_enum] |
api.rs | Query hooks, _live subscription hooks, Mutation handles, job/workflow hooks |
mod.rs | Re-exports generated modules plus forge-dioxus runtime types |
CLI Options
forge generate [OPTIONS]
| Option | Description |
|---|---|
--force | Regenerate even if files exist |
--output, -o | Output directory (defaults to the target's standard binding path) |
--target | Frontend target (sveltekit or dioxus) |
--src, -s | Source directory to scan (default: src) |
-y, --yes | Auto-accept prompts (for CI) |
Client API Reference
The generated bindings are thin wrappers over the @forge-rs/svelte runtime. This section documents the runtime API directly so you know what you're working with.
ForgeProvider
ForgeProvider is a Svelte component that creates the ForgeClient, opens the SSE connection, and puts both into Svelte context. Every page that uses generated bindings must be inside it.
<!-- frontend/src/routes/+layout.svelte -->
<script lang="ts">
import { ForgeProvider } from '@forge-rs/svelte';
import { auth } from '$lib/forge/auth.svelte';
import { type Snippet } from 'svelte';
let { children }: { children: Snippet } = $props();
</script>
<ForgeProvider
url="http://localhost:3000"
getToken={() => auth.token}
onAuthError={(err) => auth.clearAuth()}
onMutationError={(err) => console.error('Mutation failed', err)}
onConnectionChange={(state) => console.log('SSE', state)}
>
{@render children()}
</ForgeProvider>
// frontend/src/routes/+layout.ts
export const ssr = false; // Required: EventSource and localStorage are browser-only
| Prop | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Base URL of your Forge backend (no trailing slash) |
getToken | () => string | null | Promise<string | null> | No | Returns the current access token. Called before every RPC and SSE connect. |
onAuthError | (err: ForgeError) => void | No | Fired on HTTP 401/403 from RPC or SSE. Use it to redirect to login or clear auth state. |
onMutationError | (err: ForgeClientError) => void | No | Global fallback for mutation errors not handled locally. Good for toast notifications. |
onConnectionChange | (state: ConnectionState) => void | No | Fired whenever the SSE state changes ('disconnected', 'connecting', 'connected'). |
signals | SignalsConfig | false | No | Analytics configuration. Pass false to disable entirely. See Signals below. |
ForgeClientConfig
If you need to create a client manually (e.g., for testing), use createForgeClient:
import { createForgeClient } from '@forge-rs/svelte';
const client = createForgeClient({
url: 'http://localhost:3000',
getToken: () => localStorage.getItem('token'),
refreshToken: async () => {
const res = await fetch('/auth/refresh', { method: 'POST', credentials: 'include' });
if (!res.ok) return null;
const { access_token } = await res.json();
return access_token;
},
onAuthError: (err) => router.goto('/login'),
onMutationError: (err) => showToast(err.message),
timeout: 30000,
});
| Option | Type | Default | Description |
|---|---|---|---|
url | string | — | Backend base URL. |
getToken | () => string | null | Promise<string | null> | undefined | Token provider. Called before every request. |
refreshToken | () => Promise<string | null> | undefined | Called automatically on 401. Should return a fresh access token or null. The failed call is retried once. Concurrent 401s coalesce on a single refresh attempt. |
onAuthError | (err: ForgeError) => void | undefined | Fired when auth fails and cannot be recovered via refreshToken. |
onMutationError | (err: ForgeClientError) => void | undefined | Global mutation error sink. |
timeout | number (ms) | 30000 | SSE connection timeout. |
ForgeClientError
All RPC and subscription failures throw ForgeClientError, which extends Error:
import { ForgeClientError } from '@forge-rs/svelte';
try {
await createUser({ email: 'alice@example.com' });
} catch (err) {
if (err instanceof ForgeClientError) {
if (err.isRateLimited()) {
console.warn(`Slow down, retry in ${err.retryAfterSecs}s`);
} else if (err.isValidation()) {
console.error('Bad input:', err.details);
} else if (err.isUnauthorized()) {
router.goto('/login');
} else {
console.error(`[${err.code}] ${err.message}`);
}
}
}
| Property | Type | Description |
|---|---|---|
code | string | Machine-readable error code (e.g., RATE_LIMITED, VALIDATION_ERROR, UNAUTHORIZED, NOT_FOUND, INTERNAL). |
message | string | Human-readable description. |
retryAfterSecs | number | undefined | Present on RATE_LIMITED errors. Seconds to wait before retrying. |
details | Record<string, unknown> | undefined | Extra context from the server (e.g., field-level validation errors). |
isRateLimited() | () => boolean | code === 'RATE_LIMITED' |
isUnauthorized() | () => boolean | code === 'UNAUTHORIZED' |
isValidation() | () => boolean | code === 'VALIDATION_ERROR' |
Store Primitives
The generated subscription functions are built from three primitives exported by @forge-rs/svelte. You can use them directly for advanced cases.
createQueryStore
One-shot fetch with no live updates. Fetches once on creation and exposes refetch() to re-run.
import { createQueryStore } from '@forge-rs/svelte';
const store = createQueryStore<{ id: string }, User>('get_user', { id: userId });
Returns a QueryStore<T> with:
| Member | Type | Description |
|---|---|---|
subscribe | (run: (v: QueryResult<T>) => void) => ()=> void | Svelte store protocol. |
refetch | () => Promise<void> | Re-runs the RPC call. |
reset | () => void | Resets to { loading: true, data: null, error: null }. |
createSubscriptionStore
Live subscription that re-runs whenever the server detects a relevant DB change:
import { createSubscriptionStore } from '@forge-rs/svelte';
const store = createSubscriptionStore<{ id: string }, User>('get_user', { id: userId });
Returns a SubscriptionStore<T> with the same members as QueryStore<T> plus:
| Member | Type | Description |
|---|---|---|
unsubscribe | () => void | Detaches from SSE and cleans up. Call when you manage lifecycle manually. |
The store auto-unsubscribes when its last Svelte subscriber leaves (last $-ref removed from the DOM). Manual cleanup is only needed when using the store outside a component.
createConnectionStore
Tracks the SSE connection state:
import { createConnectionStore } from '@forge-rs/svelte';
const conn = createConnectionStore();
// $conn === 'disconnected' | 'connecting' | 'connected'
createJobStore
Dispatches a job and streams its progress over SSE:
import { createJobStore } from '@forge-rs/svelte';
const job = createJobStore<ExportArgs, ExportResult>('export_users', { format: 'csv' });
The store value is JobState<TOutput> & { loading: boolean }:
| Field | Type | Description |
|---|---|---|
jobId | string | UUID assigned by the server. |
status | 'pending' | 'claimed' | 'running' | 'completed' | 'failed' | 'cancelled' | 'retry' | 'dead_letter' | 'cancel_requested' | Current job status. |
progress | number | null | 0–100 progress percentage, if reported. |
message | string | null | Latest progress message from the job. |
output | TOutput | null | Final output once status is completed. |
error | string | null | Error message when status is failed. |
loading | boolean | true until the job ID is received. |
createWorkflowStore
Starts a durable workflow and streams step-level state over SSE:
import { createWorkflowStore } from '@forge-rs/svelte';
const wf = createWorkflowStore<OnboardArgs, OnboardResult>('onboard_user', { userId });
The store value is WorkflowState<TOutput> & { loading: boolean }:
| Field | Type | Description |
|---|---|---|
workflowId | string | UUID assigned by the server. |
status | 'pending' | 'running' | 'sleeping' | 'waiting' | 'completed' | 'failed' | 'blocked_missing_version' | 'blocked_signature_mismatch' | 'blocked_missing_handler' | Overall workflow status. The three blocked_* variants indicate the workflow cannot resume until an operator intervenes. |
step | string | null | Name of the currently executing step. |
waitingFor | string | null | Name of the external event the workflow is waiting on. |
steps | WorkflowStepState[] | All step records with individual status and error. |
output | TOutput | null | Final output once status is completed. |
error | string | null | Error message when status is failed. |
fireMutation
Fire-and-forget wrapper that routes errors to the global onMutationError handler:
import { fireMutation } from '@forge-rs/svelte';
import { deleteTodo } from '$lib/forge';
// Errors go to onMutationError; no await needed
fireMutation(deleteTodo, { id: todoId });
// Per-call override
fireMutation(deleteTodo, { id: todoId }, (err) => showToast(err.message));
Use fireMutation for fire-and-forget interactions (delete, toggle, increment) where the UI doesn't need to track pending state. Use await mutationFn(args) or the runes $() form when you need local pending/error state.
createOptimisticMutation
Applies a local patch immediately and reverts automatically on error or after a TTL:
import { createOptimisticMutation, createSubscriptionStore } from '@forge-rs/svelte';
import { reorderTask } from '$lib/forge';
const todos = createSubscriptionStore('list_todos', null);
const reorder = createOptimisticMutation(
reorderTask,
todos,
(data, args) => data.map(t => t.id === args.id ? { ...t, position: args.position } : t),
{ ttlMs: 3000 }, // auto-revert after 3s if no SSE confirmation
);
// Read from reorder.data, not todos directly
reorder.fire({ id: todoId, position: 2 });
The data member is a Readable<TData | null> that layers the optimistic patch over the live subscription. Once the server confirms the change via SSE, the patch is dropped and the server data takes over.
Auth Token Management
The auth.svelte.ts file generated alongside the bindings exports a global auth object:
import { auth } from '$lib/forge/auth.svelte';
// After successful login
const result = await signIn({ email, password });
auth.setAuth(result.access_token, result.refresh_token, result.user);
// After logout
await signOut();
auth.clearAuth();
// Start background token rotation (call once, on mount)
auth.startRefreshLoop('http://localhost:3000/auth/refresh');
setAuth and clearAuth both update localStorage and trigger an SSE reconnect, so active subscriptions re-register under the new identity. Writing to localStorage directly bypasses the reconnect and leaves subscriptions authenticated as the previous user.
| Method | Description |
|---|---|
auth.setAuth(access, refresh, user) | Persists tokens, updates auth.token and auth.user, triggers reconnect. |
auth.clearAuth() | Clears stored tokens, resets state, triggers anonymous reconnect. |
auth.startRefreshLoop(url) | Polls the refresh endpoint before token expiry. Concurrent 401s coalesce. |
auth.token | Current access token, or null. |
auth.user | Typed viewer object (pass generic: auth.user as MyUser), or null. |
auth.loading | true until the first token resolution completes. |
Signals
ForgeSignals handles product analytics and diagnostics. It is created inside ForgeProvider and is mostly automatic.
<ForgeProvider
url="http://localhost:3000"
signals={{
enabled: true,
autoPageViews: true,
autoCaptureErrors: true,
autoWebVitals: true,
autoNetworkEvents: true,
flushInterval: 5000,
maxBatchSize: 20,
respectDnt: true,
persistQueue: true,
}}
>
To track custom events, get the signals instance from context:
import { getForgeSignals } from '@forge-rs/svelte';
const signals = getForgeSignals();
signals.track('plan_upgraded', { plan: 'pro', seats: 10 });
signals.identify(user.id, { email: user.email });
| Config Option | Default | Description |
|---|---|---|
enabled | true | Master switch. Set false or pass signals={false} to disable entirely. |
autoPageViews | true | Tracks navigation via history.pushState / popstate. |
autoCaptureErrors | true | Captures unhandled window.onerror and unhandledrejection events. |
autoWebVitals | true | Captures LCP, CLS, INP, FCP, TTFB, and navigation timing. |
autoNetworkEvents | true | Tracks online/offline transitions. |
flushInterval | 5000 | How often (ms) buffered events are flushed to the server. |
maxBatchSize | 20 | Maximum events per flush batch. |
respectDnt | true | Disables collection if the browser sends DNT: 1 or Sec-GPC: 1. |
persistQueue | true | Saves the outbound queue to localStorage so events survive page reloads. |
Correlation IDs are automatically injected into RPC request headers, linking frontend events to backend traces.
Usage in SvelteKit Components
<script lang="ts">
import { listTodos$, createTodo, toggleTodo } from '$lib/forge';
const todos = listTodos$();
async function addTodo(title: string) {
await createTodo({ title });
// Refetches are handled by the subscription updates
}
async function toggle(id: string, completed: boolean) {
await toggleTodo({ id, completed });
}
</script>
{#if todos.loading}
<p>Loading...</p>
{:else if todos.error}
<p>Error: {todos.error.message}</p>
{:else}
<ul>
{#each todos.data as todo}
<li>
<input
type="checkbox"
checked={todo.completed}
on:change={() => toggle(todo.id, !todo.completed)}
/>
{todo.title}
</li>
{/each}
</ul>
{/if}
For Dioxus, the same backend contract is consumed through generated Rust hooks:
use dioxus::prelude::*;
use crate::forge::{CreateTodoInput, use_create_todo, use_list_todos_live};
#[component]
fn TodoList() -> Element {
let create_todo = use_create_todo();
let todos = use_list_todos_live();
rsx! {
button {
onclick: move |_| {
let create_todo = create_todo.clone();
spawn(async move {
let _ = create_todo.call(CreateTodoInput::new("Ship it")).await;
});
},
"Add"
}
ul {
for todo in todos.data.clone().unwrap_or_default() {
li { "{todo.title}" }
}
}
}
}
Under the Hood
SSE Transport
Subscriptions use Server-Sent Events with a dedicated connection. The client:
- Opens an EventSource to
/_api/eventswith the auth token - Receives a session ID and session secret on connection
- Registers each subscription via POST to
/_api/subscribe - Receives updates via the SSE channel with
targetrouting
SSE was chosen over WebSocket for:
- HTTP/2 multiplexing support
- Compatible with standard proxy configuration
- Automatic reconnection built into EventSource
- Simpler firewall traversal
Subscription Re-registration
On reconnect, the client re-registers all active query subscriptions. Each subscription tracks:
- Function name and arguments
- Failed attempt count
- Callback reference
After 3 consecutive re-registration failures, the subscription emits a MAX_RETRIES_EXCEEDED error and stops retrying.
Connection ID Pattern
Each connect() call increments a connection ID. All async operations check this ID before modifying state:
const currentConnectionId = ++this.connectionId;
// ... async work ...
if (currentConnectionId !== this.connectionId) return; // Stale, abort
This prevents race conditions when rapid reconnects overlap.