partyserver
Version:
Build real-time applications powered by [Durable Objects](https://developers.cloudflare.com/durable-objects/), inspired by [PartyKit](https://www.partykit.io/).
392 lines (390 loc) • 14.2 kB
TypeScript
import { DurableObject } from "cloudflare:workers";
//#region src/types.d.ts
type ImmutablePrimitive = undefined | null | boolean | string | number;
type Immutable<T> = T extends ImmutablePrimitive
? T
: T extends Array<infer U>
? ImmutableArray<U>
: T extends Map<infer K, infer V>
? ImmutableMap<K, V>
: T extends Set<infer M>
? ImmutableSet<M>
: ImmutableObject<T>;
type ImmutableArray<T> = ReadonlyArray<Immutable<T>>;
type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> };
type ConnectionState<T> = ImmutableObject<T> | null;
type ConnectionSetStateFn<T> = (prevState: ConnectionState<T>) => T;
type ConnectionContext = {
request: Request;
};
/** A WebSocket connected to the Server */
type Connection<TState = unknown> = WebSocket & {
/** Connection identifier */ id: string;
/**
* The URL of the original WebSocket upgrade request.
* Persisted in the WebSocket attachment so it survives hibernation.
*/
uri: string | null;
/**
* Arbitrary state associated with this connection.
* Read-only — use {@link Connection.setState} to update.
*
* This property is configurable, meaning it can be redefined via
* `Object.defineProperty` by downstream consumers (e.g. the Cloudflare
* Agents SDK) to namespace or wrap internal state storage.
*/
state: ConnectionState<TState>;
/**
* Update the state associated with this connection.
*
* Accepts either a new state value or an updater function that receives
* the previous state and returns the next state.
*
* This property is configurable, meaning it can be redefined via
* `Object.defineProperty` by downstream consumers. If you redefine
* `state` and `setState`, you are responsible for calling
* `serializeAttachment` / `deserializeAttachment` yourself if you need
* the state to survive hibernation.
*/
setState(
state: TState | ConnectionSetStateFn<TState> | null
): ConnectionState<TState>;
/**
* @deprecated use {@link Connection.setState} instead.
*
* Low-level method to persist data in the connection's attachment storage.
* This property is configurable and can be redefined by downstream
* consumers that need to wrap or namespace the underlying storage.
*/
serializeAttachment<T = unknown>(attachment: T): void;
/**
* @deprecated use {@link Connection.state} instead.
*
* Low-level method to read data from the connection's attachment storage.
* This property is configurable and can be redefined by downstream
* consumers that need to wrap or namespace the underlying storage.
*/
deserializeAttachment<T = unknown>(): T | null;
/**
* Tags assigned to this connection via {@link Server.getConnectionTags}.
* Always includes the connection id as the first tag.
*/
tags: readonly string[];
/**
* @deprecated Use `this.name` on the Server instead.
* The server name. Populated from `Server.name` after initialization.
*/
server: string;
};
//#endregion
//#region src/index.d.ts
type WSMessage = ArrayBuffer | ArrayBufferView | string;
interface RoutingRetryEvent {
error: unknown;
attempt: number;
maxAttempts: number;
delayMs: number;
name: string;
className?: string;
}
interface RoutingRetryOptions {
/** Max number of attempts, including the first. Default: 3 */
maxAttempts?: number;
/** Base delay in ms for exponential backoff. Default: 100 */
baseDelayMs?: number;
/** Max delay cap in ms. Default: 800 */
maxDelayMs?: number;
/** Optional callback invoked before each retry delay. */
onRetry?: (event: RoutingRetryEvent) => void | Promise<void>;
}
/**
* For a given server namespace, create a server with a name.
*
* Makes an RPC that awaits the DO's `onStart()` before returning, so callers
* can invoke user-defined RPC methods on the returned stub and trust that
* `onStart()` has completed. (User-defined RPC methods don't
* otherwise pass through `Server.fetch()`, which is where initialization
* would normally be triggered.)
*
* `this.name` inside the DO is always populated from `ctx.id.name`, so
* the RPC no longer needs to carry the name for bookkeeping; it exists
* purely to synchronize `onStart()` and to deliver `props`.
*/
declare function getServerByName<
Env extends Cloudflare.Env = Cloudflare.Env,
T extends Server<Env> = Server<Env>,
Props extends Record<string, unknown> = Record<string, unknown>
>(
serverNamespace: DurableObjectNamespace<T>,
name: string,
options?: {
jurisdiction?: DurableObjectJurisdiction;
locationHint?: DurableObjectLocationHint;
props?: Props;
routingRetry?: false | RoutingRetryOptions;
}
): Promise<DurableObjectStub<T>>;
interface Lobby<Env = Cloudflare.Env> {
/**
* The kebab-case namespace from the URL path (e.g. `"my-agent"`).
* @deprecated Use `className` instead, which returns the Durable Object class name.
* In the next major version, `party` will return the class name instead of the kebab-case namespace.
*/
party: string;
/** The Durable Object class name / env binding name (e.g. `"MyAgent"`). */
className: Extract<keyof Env, string>;
/** The room / instance name extracted from the URL. */
name: string;
}
interface PartyServerOptions<
Env = Cloudflare.Env,
Props = Record<string, unknown>
> {
prefix?: string;
jurisdiction?: DurableObjectJurisdiction;
locationHint?: DurableObjectLocationHint;
props?: Props;
/**
* Whether to enable CORS for matched routes.
*
* When `true`, uses default permissive CORS headers:
* - Access-Control-Allow-Origin: *
* - Access-Control-Allow-Methods: GET, POST, HEAD, OPTIONS
* - Access-Control-Allow-Headers: *
* - Access-Control-Max-Age: 86400
*
* For credentialed requests, pass explicit headers with a specific origin:
* ```ts
* cors: {
* "Access-Control-Allow-Origin": "https://myapp.com",
* "Access-Control-Allow-Credentials": "true",
* "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
* "Access-Control-Allow-Headers": "Content-Type, Authorization"
* }
* ```
*
* When set to a `HeadersInit` value, uses those as the CORS headers instead.
* CORS preflight (OPTIONS) requests are handled automatically for matched routes.
* Non-WebSocket responses on matched routes will also have the CORS headers appended.
*/
cors?: boolean | HeadersInit;
/**
* Retry transient Durable Object infrastructure errors thrown while routing
* to the target DO. Enabled by default; pass `false` to disable.
*
* Only errors marked `retryable === true` are retried, and overloaded
* errors (`overloaded === true`) are never retried.
*/
routingRetry?: false | RoutingRetryOptions;
onBeforeConnect?: (
req: Request,
lobby: Lobby<Env>
) => Response | Request | void | Promise<Response | Request | void>;
onBeforeRequest?: (
req: Request,
lobby: Lobby<Env>
) =>
| Response
| Request
| void
| Promise<Response | Request | undefined | void>;
}
declare function routePartykitRequest<
Env extends Cloudflare.Env = Cloudflare.Env,
T extends Server<Env> = Server<Env>,
Props extends Record<string, unknown> = Record<string, unknown>
>(
req: Request,
env?: Env,
options?: PartyServerOptions<Env, Props>
): Promise<Response | null>;
declare class Server<
Env extends Cloudflare.Env = Cloudflare.Env,
Props extends Record<string, unknown> = Record<string, unknown>
> extends DurableObject<Env> {
#private;
static options: {
hibernate?: boolean;
};
/**
* Execute SQL queries against the Server's database
* @template T Type of the returned rows
* @param strings SQL query template strings
* @param values Values to be inserted into the query
* @returns Array of query results
*/
sql<T = Record<string, string | number | boolean | null>>(
strings: TemplateStringsArray,
...values: (string | number | boolean | null)[]
): T[];
constructor(ctx: DurableObjectState, env: Env);
/**
* Handle incoming requests to the server.
*/
fetch(request: Request): Promise<Response>;
webSocketMessage(ws: WebSocket, message: WSMessage): Promise<void>;
webSocketClose(
ws: WebSocket,
code: number,
reason: string,
wasClean: boolean
): Promise<void>;
webSocketError(ws: WebSocket, error: unknown): Promise<void>;
/**
* @internal — Do not use directly. This is an escape hatch for frameworks
* (like Agents) that receive calls via native DO RPC, bypassing the
* standard fetch/alarm/webSocket entry points where initialization
* normally happens. Calling this from application code is unsupported
* and may break without notice.
*/
__unsafe_ensureInitialized(): Promise<void>;
/**
* The name for this server.
*
* Resolves from `this.ctx.id.name` — the native DO id name, populated
* whenever the stub was created via `idFromName()` or `getByName()`.
* This is available inside every entry point (including the constructor,
* alarms, and hibernating websocket handlers).
*
* For alarm handlers firing on stale on-disk alarm records from
* older workerd versions that didn't persist `name` into the alarm
* record, the name is recovered from a storage fallback record.
*
* Throws if neither source is available — typically this means the DO
* was addressed via `idFromString()` or `newUniqueId()`, which is not
* supported by PartyServer.
*/
get name(): string;
/**
* Establish this server's name and trigger `onStart()`.
*
* Use cases:
*
* 1. **Framework-level bootstrap of DOs where `ctx.id.name` is
* undefined** — e.g. DOs addressed via `idFromString()` /
* `newUniqueId()`. `setName()` stashes the name in memory and
* persists it under `__ps_name` so cold-wake invocations
* recover it via `#ensureInitialized()`'s legacy fallback.
* 2. **Delivering initial `props` to `onStart()`** via the
* optional second argument.
*
* For DOs addressed via `idFromName()` / `getByName()`, calling
* `setName()` is redundant — `this.name` is available automatically
* from `ctx.id.name`. The normal initialization path also persists
* a fallback record so old-compat alarm handlers can recover the name.
* Throws if `name` does not match `ctx.id.name`.
*
* **Not appropriate for facets.** Cloudflare Agents and any other
* framework using `ctx.facets.get(...)` should pass an explicit
* `id` in `FacetStartupOptions` so the facet has its own
* `ctx.id.name`:
*
* ```ts
* const stub = ctx.facets.get(facetKey, () => ({
* class: ChildClass,
* id: ctx.exports.SomeBoundDOClass.idFromName(facetName),
* }));
* ```
*
* Without an explicit `id`, the facet inherits the parent DO's
* `ctx.id` (including `ctx.id.name`), and `setName()` will throw
* the ctx.id.name-mismatch error because the facet's intended
* name differs from the parent's. See
* https://developers.cloudflare.com/dynamic-workers/usage/durable-object-facets/
* for the `FacetStartupOptions.id` semantics.
*
* @deprecated for callers that address DOs via `idFromName()` /
* `getByName()`. Still the supported API for framework-level
* bootstrap of header/`newUniqueId`-addressed DOs and for
* delivering initial `props` to `onStart()`.
*/
setName(name: string, props?: Props): Promise<void>;
/**
* @internal
* @deprecated Retained for backward compatibility with older callers.
* `routePartykitRequest` no longer uses this method; it sends props via
* the `x-partykit-props` header on the underlying `fetch()` request.
*/
_initAndFetch(
name: string,
props: Props | undefined,
request: Request
): Promise<Response>;
/** Send a message to all connected clients, except connection ids listed in `without` */
broadcast(
msg: string | ArrayBuffer | ArrayBufferView,
without?: string[] | undefined
): void;
/** Get a connection by connection id */
getConnection<TState = unknown>(id: string): Connection<TState> | undefined;
/**
* Get all connections. Optionally, you can provide a tag to filter returned connections.
* Use `Server#getConnectionTags` to tag the connection on connect.
*/
getConnections<TState = unknown>(tag?: string): Iterable<Connection<TState>>;
/**
* You can tag a connection to filter them in Server#getConnections.
* Each connection supports up to 9 tags, each tag max length is 256 characters.
*/
getConnectionTags(
connection: Connection,
context: ConnectionContext
): string[] | Promise<string[]>;
/**
* Called when the server is started for the first time.
*/
onStart(props?: Props): void | Promise<void>;
/**
* Called when a new connection is made to the server.
*/
onConnect(
connection: Connection,
ctx: ConnectionContext
): void | Promise<void>;
/**
* Called when a message is received from a connection.
*/
onMessage(connection: Connection, message: WSMessage): void | Promise<void>;
/**
* Called when a connection is closed.
*/
onClose(
connection: Connection,
code: number,
reason: string,
wasClean: boolean
): void | Promise<void>;
/**
* Called when an error occurs on a connection.
*/
onError(connection: Connection, error: unknown): void | Promise<void>;
/**
* Called when a request is made to the server.
*/
onRequest(request: Request): Response | Promise<Response>;
/**
* Called when an exception occurs.
* @param error - The error that occurred.
*/
onException(error: unknown): void | Promise<void>;
onAlarm(): void | Promise<void>;
alarm(): Promise<void>;
}
//#endregion
export {
Connection,
ConnectionContext,
ConnectionSetStateFn,
ConnectionState,
Lobby,
PartyServerOptions,
RoutingRetryEvent,
RoutingRetryOptions,
Server,
WSMessage,
getServerByName,
routePartykitRequest
};
//# sourceMappingURL=index.d.ts.map