···5959 return concat(chunks);
6060};
61616262-// streamToBuffer identifier name already taken by @atproto/common-web
6362export const streamToBuffer = async (
6463 stream:
6564 | Iterable<Uint8Array>
-57
xrpc-server/errors.ts
···8888 get statusCode(): number {
8989 const { type } = this;
90909191- // Fool-proofing. `new XRPCError(123.5 as number, '')` does not generate a TypeScript error.
9292- // Because of this, we can end-up with any numeric value instead of an actual `ResponseType`.
9393- // For legacy reasons, the `type` argument is not checked in the constructor, so we check it here.
9491 if (type < 400 || type >= 600 || !Number.isFinite(type)) {
9592 return 500;
9693 }
···162159 * Used when the client request is malformed or invalid.
163160 */
164161export class InvalidRequestError extends XRPCError {
165165- /**
166166- * Creates a new InvalidRequestError.
167167- * @param errorMessage - Optional error message
168168- * @param customErrorName - Optional custom error name
169169- * @param options - Optional error options
170170- */
171162 constructor(
172163 errorMessage?: string,
173164 customErrorName?: string,
···189180 * Used when the request requires authentication but none was provided or it was invalid.
190181 */
191182export class AuthRequiredError extends XRPCError {
192192- /**
193193- * Creates a new AuthRequiredError.
194194- * @param errorMessage - Optional error message
195195- * @param customErrorName - Optional custom error name
196196- * @param options - Optional error options
197197- */
198183 constructor(
199184 errorMessage?: string,
200185 customErrorName?: string,
···221206 * Used when the client is authenticated but doesn't have permission to access the resource.
222207 */
223208export class ForbiddenError extends XRPCError {
224224- /**
225225- * Creates a new ForbiddenError.
226226- * @param errorMessage - Optional error message
227227- * @param customErrorName - Optional custom error name
228228- * @param options - Optional error options
229229- */
230209 constructor(
231210 errorMessage?: string,
232211 customErrorName?: string,
···247226 * Used when an unexpected error occurs on the server side.
248227 */
249228export class InternalServerError extends XRPCError {
250250- /**
251251- * Creates a new InternalServerError.
252252- * @param errorMessage - Optional error message
253253- * @param customErrorName - Optional custom error name
254254- * @param options - Optional error options
255255- */
256229 constructor(
257230 errorMessage?: string,
258231 customErrorName?: string,
···279252 * Used when a dependent service fails or returns an invalid response.
280253 */
281254export class UpstreamFailureError extends XRPCError {
282282- /**
283283- * Creates a new UpstreamFailureError.
284284- * @param errorMessage - Optional error message
285285- * @param customErrorName - Optional custom error name
286286- * @param options - Optional error options
287287- */
288255 constructor(
289256 errorMessage?: string,
290257 customErrorName?: string,
···306273 * Used when the server temporarily cannot handle the request due to resource constraints.
307274 */
308275export class NotEnoughResourcesError extends XRPCError {
309309- /**
310310- * Creates a new NotEnoughResourcesError.
311311- * @param errorMessage - Optional error message
312312- * @param customErrorName - Optional custom error name
313313- * @param options - Optional error options
314314- */
315276 constructor(
316277 errorMessage?: string,
317278 customErrorName?: string,
···338299 * Used when a dependent service times out or takes too long to respond.
339300 */
340301export class UpstreamTimeoutError extends XRPCError {
341341- /**
342342- * Creates a new UpstreamTimeoutError.
343343- * @param errorMessage - Optional error message
344344- * @param customErrorName - Optional custom error name
345345- * @param options - Optional error options
346346- */
347302 constructor(
348303 errorMessage?: string,
349304 customErrorName?: string,
···365320 * Used when the requested XRPC method is not implemented by the server.
366321 */
367322export class MethodNotImplementedError extends XRPCError {
368368- /**
369369- * Creates a new MethodNotImplementedError.
370370- * @param errorMessage - Optional error message
371371- * @param customErrorName - Optional custom error name
372372- * @param options - Optional error options
373373- */
374323 constructor(
375324 errorMessage?: string,
376325 customErrorName?: string,
···392341 }
393342}
394343395395-/**
396396- * Converts an upstream XRPC client error into a downstream ResponseType.
397397- * Maps client error status codes to appropriate server response types.
398398- * @param error The upstream XRPC client error
399399- * @returns Object containing error details and mapped response type
400400- */
401344function mapFromClientError(error: XRPCClientError): {
402345 error: string;
403346 message: string;
-3
xrpc-server/rate-limiter.ts
···77import { ResponseType, XRPCError } from "./errors.ts";
88import { logger } from "./logger.ts";
991010-// @NOTE Do not depend (directly or indirectly) on "./types" here, as it would
1111-// create a circular dependency.
1212-1310export interface RateLimiterContext {
1411 req: Request;
1512 res?: Response;
+1-42
xrpc-server/server.ts
···5858import type { CatchallHandler, RouteOptions } from "./types.ts";
59596060/**
6161- * Creates a new XRPC server instance.
6161+ * Creates a new XRPC server instance
6262 * @param lexicons - Optional array of lexicon documents to initialize the server with
6363 * @param options - Optional server configuration options
6464- * @returns A new Server instance
6564 */
6665export function createServer(
6766 lexicons?: LexiconDoc[],
···104103 this.addLexicons(lexicons);
105104 }
106105107107- // Add global middleware
108106 this.app.use("*", this.catchall);
109107 this.app.onError(createErrorHandler(opts));
110108111111- // Add 404 handler to catch unmatched XRPC routes
112109 this.app.notFound((c) => {
113110 const nsid = parseUrlNsid(c.req.url);
114111 if (nsid) {
···130127 throw error;
131128 }
132129 }
133133- // For non-XRPC routes, return standard 404
134130 return c.text("Not Found", 404);
135131 });
136132···156152 }
157153158154 // handlers
159159- // =
160155161156 /**
162157 * Registers a method handler for the specified NSID.
···223218 }
224219225220 // lexicon
226226- // =
227221228222 /**
229223 * Adds a lexicon document to the server's schema registry.
···244238 }
245239246240 // routes
247247- // =
248241249242 /**
250243 * Adds an HTTP route for the specified method.
···270263271264 /**
272265 * Catchall handler that processes all XRPC routes and applies global rate limiting.
273273- * Only applies to routes starting with "/xrpc/".
274266 */
275267 catchall: CatchallHandler = async (c, next) => {
276268 if (!c.req.url.includes("/xrpc/")) {
···510502 });
511503 }
512504513513- /**
514514- * Creates a route-specific rate limiter based on the method configuration.
515515- * @template A - The authentication type
516516- * @template C - The handler context type
517517- * @param nsid - The namespace identifier for the method
518518- * @param config - The method configuration containing rate limit options
519519- * @returns A route rate limiter or undefined if no rate limiting is configured
520520- * @private
521521- */
522505 private createRouteRateLimiter<A extends Auth, C extends HandlerContext>(
523506 nsid: string,
524507 config: MethodConfig<A>,
···590573 }
591574}
592575593593-/**
594594- * Creates an error handler function for the Hono application.
595595- * @param opts - Server options containing optional error parser
596596- * @returns An error handler function that converts errors to XRPC error responses
597597- */
598576function createErrorHandler(
599577 opts: Options,
600578): (err: Error, c: Context) => Response {
···612590 };
613591}
614592615615-/**
616616- * Builds rate limiter options from a server rate limit description.
617617- * @template C - The handler context type
618618- * @param options - The server rate limit description
619619- * @returns Rate limiter options with defaults applied
620620- */
621593function buildRateLimiterOptions<C extends HandlerContext = HandlerContext>({
622594 name,
623595 calcKey = defaultKey,
···627599 return { ...desc, calcKey, calcPoints, keyPrefix: `rl-${name}` };
628600}
629601630630-/**
631631- * Default function for calculating rate limit points consumed per request.
632632- * Always returns 1 point per request.
633633- */
634602const defaultPoints: CalcPointsFn = (): number => 1;
635603636636-/**
637637- * Default function for calculating rate limit keys based on client IP address.
638638- * Extracts IP from X-Forwarded-For, X-Real-IP headers, or falls back to "unknown".
639639- *
640640- * @note When using a proxy, ensure headers are getting forwarded correctly:
641641- * `app.set('trust proxy', true)`
642642- *
643643- * @see {@link https://expressjs.com/en/guide/behind-proxies.html}
644644- */
645604const defaultKey: CalcKeyFn<HandlerContext> = ({ req }) => {
646605 const forwarded = req.headers.get("x-forwarded-for");
647606 const ip = forwarded
+3
xrpc-server/stream/connection.ts
···121121122122 /**
123123 * Executes the streaming handler and processes yielded data.
124124+ * @param params - The request's parameters.
125125+ * @param auth - The request's authentication state.
126126+ * @param req - The raw request object.
124127 */
125128 private async executeHandler(
126129 params: Record<string, string>,
+8-23
xrpc-server/stream/frames.ts
···44444545 /**
4646 * Type guard to check if this frame is a MessageFrame.
4747- * @returns {boolean} True if this is a MessageFrame
4847 */
4948 isMessage(): this is MessageFrame<unknown> {
5049 return this.op === FrameType.Message;
···52515352 /**
5453 * Type guard to check if this frame is an ErrorFrame.
5555- * @returns {boolean} True if this is an ErrorFrame
5654 */
5755 isError(): this is ErrorFrame {
5856 return this.op === FrameType.Error;
···6260 * Deserializes a frame from its binary representation.
6361 * Validates the frame structure and creates the appropriate frame type.
6462 *
6565- * @param {Uint8Array} bytes - The serialized frame bytes
6666- * @returns {Frame} The deserialized frame (either MessageFrame or ErrorFrame)
6767- * @throws {Error} If the frame format is invalid or unknown
6363+ * @param bytes - The serialized frame bytes
6464+ * @returns The deserialized frame (either MessageFrame or ErrorFrame)
6565+ * @throws If the frame format is invalid or unknown
6866 */
6967 static fromBytes(bytes: Uint8Array): Frame {
7068 let decoded: unknown[];
7169 try {
7270 decoded = cborDecodeMulti(bytes);
7371 } catch {
7474- // Re-throw CBOR decode errors with a more generic message to match test expectations
7572 throw new Error("Unexpected end of CBOR data");
7673 }
77747878- // Check for empty or invalid decode results
7975 if (decoded.length === 0 || decoded[0] === undefined) {
8076 throw new Error("Unexpected end of CBOR data");
8177 }
···119115 * Frame type for sending messages/data over an XRPC stream.
120116 * Can contain any type of payload data and an optional message type identifier.
121117 *
122122- * @template T - The type of the message body, defaults to Record<string, unknown>
123123- * @extends {Frame}
124124- * @property {MessageFrameHeader} header - Message frame header
125125- * @property {T} body - Message payload data
118118+ * @template T - The type of the message body
119119+ * @property header - Message frame header
120120+ * @property body - Message payload data
126121 */
127122export class MessageFrame<T = Record<string, unknown>> extends Frame {
128123 header: MessageFrameHeader;
···156151 * Contains an error code and optional error message.
157152 *
158153 * @template T - The type of error code string
159159- * @extends {Frame}
160160- * @property {ErrorFrameHeader} header - Error frame header
161161- * @property {ErrorFrameBody<T>} body - Error details including code and message
154154+ * @property header - Error frame header
155155+ * @property body - Error details including code and message
162156 */
163157export class ErrorFrame<T extends string = string> extends Frame {
164158 header: ErrorFrameHeader;
165159 override body: ErrorFrameBody<T>;
166160167167- /**
168168- * Creates a new ErrorFrame.
169169- * @param {ErrorFrameBody<T>} body - The error details
170170- */
171161 constructor(body: ErrorFrameBody<T>) {
172162 super();
173163 this.header = { op: FrameType.Error };
···176166177167 /**
178168 * Gets the error code.
179179- * @returns {string} The error code
180169 */
181170 get code(): string {
182171 return this.body.error;
···191180 }
192181}
193182194194-/**
195195- * Symbol used internally to detect unset frame body.
196196- * @private
197197- */
198183const kUnset = Symbol("unset");
-17
xrpc-server/stream/logger.ts
···11import { subsystemLogger } from "@atp/common";
2233-/**
44- * Logger instance for XRPC streaming operations.
55- * This is a subsystem logger specifically configured for logging events
66- * related to WebSocket streaming, connection management, and stream processing.
77- *
88- * @example
99- * ```typescript
1010- * import { logger } from './logger';
1111- *
1212- * logger.info('WebSocket connection established');
1313- * logger.error(error, 'Stream processing failed');
1414- * ```
1515- */
163export const logger: ReturnType<typeof subsystemLogger> = subsystemLogger(
174 "xrpc-stream",
185);
1962020-/**
2121- * Default export of the XRPC stream logger.
2222- * Same as the named export, provided for convenience.
2323- */
247export default logger;
+5-24
xrpc-server/stream/server.ts
···1111export class XrpcStreamServer {
1212 wss: WebSocketServer;
13131414- /**
1515- * Creates a new XRPC streaming server instance.
1616- * @constructor
1717- * @param {Object} opts - Server configuration options
1818- * @param {Handler} opts.handler - Function to handle incoming WebSocket connections
1919- * @param {ServerOptions} opts - Additional WebSocket server options
2020- */
2114 constructor(opts: ServerOptions & { handler: Handler }) {
2215 const { handler, ...serverOpts } = opts;
2316 this.wss = new WebSocketServer(serverOpts);
···8174/**
8275 * Handler function type for WebSocket connections.
8376 * @callback Handler
8484- * @param {Request} req - The incoming WebSocket request
8585- * @param {AbortSignal} signal - Signal for detecting connection abort
8686- * @param {WebSocket} socket - The WebSocket connection
8787- * @param {XrpcStreamServer} server - The server instance
8888- * @returns {AsyncIterable<Frame>} An async iterable of frames to send
7777+ * @param req - The incoming WebSocket request
7878+ * @param signal - Signal for detecting connection abort
7979+ * @param socket - The WebSocket connection
8080+ * @param server - The server instance
8181+ * @returns An async iterable of frames to send
8982 */
9083export type Handler = (
9184 req: Request,
···9487 server: XrpcStreamServer,
9588) => AsyncIterable<Frame>;
96899797-/**
9898- * Unwraps an AsyncIterable into its AsyncIterator.
9999- * @template T - The type of values being iterated
100100- * @param {AsyncIterable<T>} iterable - The iterable to unwrap
101101- * @returns {AsyncIterator<T>} The unwrapped iterator
102102- */
10390function unwrapIterator<T>(iterable: AsyncIterable<T>): AsyncIterator<T> {
10491 return iterable[Symbol.asyncIterator]();
10592}
10693107107-/**
108108- * Wraps an AsyncIterator back into an AsyncIterable.
109109- * @template T - The type of values being iterated
110110- * @param {AsyncIterator<T>} iterator - The iterator to wrap
111111- * @returns {AsyncIterable<T>} The wrapped iterable
112112- */
11394function wrapIterator<T>(iterator: AsyncIterator<T>): AsyncIterable<T> {
11495 return {
11596 [Symbol.asyncIterator]() {
+8-12
xrpc-server/stream/stream.ts
···66 * Converts a WebSocket connection into an async generator of Frame objects.
77 * Handles both message and error frames, with proper error propagation.
88 *
99- * @param {WebSocket} ws - The WebSocket connection to read from
99+ * @param ws - The WebSocket connection to read from
1010 * @yields {Frame} Each frame received from the WebSocket
1111- * @throws {Error} Any WebSocket error that occurs during communication
1111+ * @throws Any WebSocket error that occurs during communication
1212 *
1313 * @example
1414 * ```typescript
···6767 }
6868}
69697070-/**
7171- * Waits for the next frame from a WebSocket connection.
7272- * Returns null if the connection closes normally.
7373- */
7470function waitForNextFrame(ws: WebSocket): Promise<Frame | null> {
7571 return new Promise<Frame | null>((resolve, reject) => {
7672 const cleanup = () => {
···135131 * Automatically filters and validates frames to ensure they are valid messages.
136132 * Error frames are converted to exceptions.
137133 *
138138- * @param {WebSocket} ws - The WebSocket connection to read from
139139- * @yields {MessageFrame<unknown>} Each message frame received from the WebSocket
140140- * @throws {XRPCError} If an error frame is received or an invalid frame type is encountered
134134+ * @param ws - The WebSocket connection to read from
135135+ * @yields Each message frame received from the WebSocket
136136+ * @throws If an error frame is received or an invalid frame type is encountered
141137 *
142138 * @example
143139 * ```typescript
···160156 * Validates that a frame is a MessageFrame and converts it to the appropriate type.
161157 * If the frame is an error frame, throws an XRPCError with the error details.
162158 *
163163- * @param {Frame} frame - The frame to validate
164164- * @returns {MessageFrame<unknown>} The frame as a MessageFrame if valid
165165- * @throws {XRPCError} If the frame is an error frame or an invalid type
159159+ * @param frame - The frame to validate
160160+ * @returns The frame as a MessageFrame if valid
161161+ * @throws If the frame is an error frame or an invalid type
166162 * @internal
167163 */
168164export function ensureChunkIsMessage(frame: Frame): MessageFrame<unknown> {
+2-32
xrpc-server/stream/subscription.ts
···66/**
77 * Represents a message body in a subscription stream.
88 * @interface
99- * @property {string} [$type] - Optional type identifier for the message
1010- * @property {unknown} [key: string] - Additional message properties
99+ * @property $type - Optional type identifier for the message
1010+ * @property [key] - Additional message properties
1111 */
1212interface MessageBody {
1313 $type?: string;
···2121 * @template T - The type of messages yielded by the subscription
2222 */
2323export class Subscription<T = unknown> {
2424- /**
2525- * Creates a new subscription instance.
2626- * @constructor
2727- * @param {Object} opts - Subscription configuration options
2828- * @param {string} opts.service - The base URL of the XRPC service
2929- * @param {string} opts.method - The XRPC method to subscribe to
3030- * @param {number} [opts.maxReconnectSeconds] - Maximum time in seconds between reconnection attempts
3131- * @param {number} [opts.heartbeatIntervalMs] - Interval in milliseconds for sending heartbeat messages
3232- * @param {AbortSignal} [opts.signal] - Signal for aborting the subscription
3333- * @param {Function} opts.validate - Function to validate and transform incoming messages
3434- * @param {Function} [opts.onReconnectError] - Callback for handling reconnection errors
3535- * @param {Function} [opts.getParams] - Function to get query parameters for the subscription URL
3636- */
3724 constructor(
3825 public opts: WebSocketOptions & {
3926 service: string;
···5441 },
5542 ) {}
56435757- /**
5858- * Implements the AsyncIterator protocol for the subscription.
5959- * Allows using the subscription in a for-await-of loop.
6060- * @returns {AsyncGenerator<T>} An async generator that yields validated messages
6161- */
6244 async *[Symbol.asyncIterator](): AsyncGenerator<T> {
6345 const ws = new WebSocketKeepAlive({
6446 ...this.opts,
···88708971export default Subscription;
90729191-/**
9292- * Encodes an object of parameters into a URL query string.
9393- * @param {Record<string, unknown>} obj - The parameters to encode
9494- * @returns {string} The encoded query string
9595- */
9673function encodeQueryParams(obj: Record<string, unknown>): string {
9774 const params = new URLSearchParams();
9875 Object.entries(obj).forEach(([key, value]) => {
···10683 return params.toString();
10784}
10885109109-/**
110110- * Encodes a single query parameter value into a string or array of strings.
111111- * Handles various types including strings, numbers, booleans, dates, and arrays.
112112- * @param {unknown} value - The value to encode
113113- * @returns {string | string[]} The encoded parameter value(s)
114114- * @throws {Error} If the value cannot be encoded as a query parameter
115115- */
11686function encodeQueryParam(value: unknown): string | string[] {
11787 if (typeof value === "string") {
11888 return value;
+11-16
xrpc-server/stream/types.ts
···1414/**
1515 * WebSocket connection options.
1616 * @interface
1717- * @property {Record<string, string>} [headers] - Additional headers for the WebSocket connection
1818- * @property {string[]} [protocols] - WebSocket subprotocols to use
1717+ * @property [headers] - Additional headers for the WebSocket connection
1818+ * @property [protocols] - WebSocket subprotocols to use
1919 */
2020export interface WebSocketOptions {
2121 headers?: Record<string, string>;
···2525/**
2626 * Header for message frames.
2727 * @interface
2828- * @property {FrameType.Message} op - Operation type, always Message
2929- * @property {string} [t] - Optional message type discriminator
2828+ * @property op - Operation type, always Message
2929+ * @property [t] - Optional message type discriminator
3030 */
3131export type MessageFrameHeader = {
3232 op: FrameType.Message;
···4141/**
4242 * Header for error frames.
4343 * @interface
4444- * @property {FrameType.Error} op - Operation type, always Error
4444+ * @property op - Operation type, always Error
4545 */
4646export type ErrorFrameHeader = {
4747 op: FrameType.Error;
···5454/**
5555 * Base type for error frame bodies.
5656 * @interface
5757- * @property {string} error - Error code or identifier
5858- * @property {string} [message] - Optional error message
5757+ * @property error - Error code or identifier
5858+ * @property [message] - Optional error message
5959 */
6060export type ErrorFrameBodyBase = {
6161 error: string;
···6666 * Generic error frame body with typed error codes.
6767 * @template T - The type of error codes allowed
6868 * @interface
6969- * @property {T} error - Typed error code
7070- * @property {string} [message] - Optional error message
6969+ * @property error - Typed error code
7070+ * @property [message] - Optional error message
7171 */
7272export type ErrorFrameBody<T extends string = string> = {
7373 error: T;
···9494 * Error class for handling WebSocket disconnections.
9595 * @class
9696 * @extends Error
9797- * @property {CloseCode} wsCode - WebSocket close code
9898- * @property {string} [xrpcCode] - XRPC-specific error code
9797+ * @property wsCode - WebSocket close code
9898+ * @property [xrpcCode] - XRPC-specific error code
9999 */
100100export class DisconnectError extends Error {
101101 constructor(
···106106 }
107107}
108108109109-/**
110110- * WebSocket close codes as defined in RFC 6455.
111111- * @see https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1
112112- * @enum {number}
113113- */
114109export enum CloseCode {
115110 /** Normal closure, meaning the purpose for which the connection was established has been fulfilled */
116111 Normal = 1000,
-50
xrpc-server/stream/websocket-keepalive.ts
···77 * @class
88 */
99export class WebSocketKeepAlive {
1010- /** Current WebSocket connection instance */
1110 public ws: WebSocket | null = null;
1212- /** Whether this is the first connection attempt */
1311 public initialSetup = true;
1414- /** Number of reconnection attempts made, or null if not reconnecting */
1512 public reconnects: number | null = null;
16131717- /**
1818- * Creates a new WebSocket client with keep-alive functionality.
1919- * @constructor
2020- * @param {Object} opts - Client configuration options
2121- * @param {Function} opts.getUrl - Function to get the WebSocket URL
2222- * @param {number} [opts.maxReconnectSeconds] - Maximum backoff time between reconnection attempts
2323- * @param {AbortSignal} [opts.signal] - Signal for aborting the connection
2424- * @param {number} [opts.heartbeatIntervalMs] - Interval between heartbeat messages
2525- * @param {Function} [opts.onReconnectError] - Callback for handling reconnection errors
2626- */
2714 constructor(
2815 public opts: WebSocketOptions & {
2916 getUrl: () => Promise<string>;
···3825 },
3926 ) {}
40274141- /**
4242- * Implements the AsyncIterator protocol for receiving WebSocket messages.
4343- * Handles automatic reconnection and message buffering.
4444- * @returns {AsyncGenerator<Uint8Array>} An async generator that yields received messages
4545- */
4628 async *[Symbol.asyncIterator](): AsyncGenerator<Uint8Array> {
4729 const maxReconnectMs = 1000 * (this.opts.maxReconnectSeconds ?? 64);
4830 while (true) {
···194176 }
195177 }
196178197197- /**
198198- * Starts the heartbeat mechanism for a WebSocket connection.
199199- * Sends periodic ping messages and monitors for pong responses.
200200- * @param {WebSocket} ws - The WebSocket connection to monitor
201201- */
202179 startHeartbeat(ws: WebSocket) {
203180 let isAlive = true;
204181 let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
···247224248225export default WebSocketKeepAlive;
249226250250-/**
251251- * Error class for abnormal WebSocket closures.
252252- * @class
253253- * @extends Error
254254- */
255227class AbnormalCloseError extends Error {
256228 code = "EWSABNORMALCLOSE";
257229}
···276248 return err !== null && typeof err === "object" && "code" in err;
277249}
278250279279-/**
280280- * Checks if an error should trigger a reconnection attempt.
281281- * Network-related errors are typically reconnectable.
282282- * @param {unknown} err - The error to check
283283- * @returns {boolean} True if the error should trigger a reconnection
284284- */
285251function isReconnectable(err: unknown): boolean {
286286- // Network errors are reconnectable.
287287- // AuthenticationRequired and InvalidRequest XRPCErrors are not reconnectable.
288288- // @TODO method-specific XRPCErrors may be reconnectable, need to consider. Receiving
289289- // an invalid message is not current reconnectable, but the user can decide to skip them.
290252 if (!isErrorWithCode(err)) return false;
291253 return typeof err.code === "string" && networkErrorCodes.includes(err.code);
292254}
···305267 "ECANCELED",
306268];
307269308308-/**
309309- * Calculates the backoff duration for reconnection attempts.
310310- * Uses exponential backoff with random jitter.
311311- * @param {number} n - The number of reconnection attempts so far
312312- * @param {number} maxMs - Maximum backoff duration in milliseconds
313313- * @returns {number} The backoff duration in milliseconds
314314- */
315270function backoffMs(n: number, maxMs: number) {
316271 const baseSec = Math.pow(2, n); // 1, 2, 4, ...
317272 const randSec = Math.random() - 0.5; // Random jitter between -.5 and .5 seconds
···319274 return Math.min(ms, maxMs);
320275}
321276322322-/**
323323- * Forwards abort signals from one AbortController to another.
324324- * @param {AbortSignal} signal - The source abort signal
325325- * @param {AbortController} ac - The target abort controller
326326- */
327277function forwardSignal(signal: AbortSignal, ac: AbortController) {
328278 if (signal.aborted) {
329279 return ac.abort(signal.reason);
-14
xrpc-server/util.ts
···209209 return base.trim().toLowerCase();
210210}
211211212212-/**
213213- * Checks if an actual encoding matches the expected encoding.
214214- * Supports wildcard matching and JSON aliases.
215215- * @param expected - The expected encoding from the lexicon
216216- * @param actual - The actual encoding from the request
217217- * @returns True if the encodings are compatible
218218- */
219212function isValidEncoding(output: LexXrpcBody, encoding: string) {
220213 const normalized = normalizeMime(encoding);
221214 if (!normalized) return false;
···226219227220type BodyPresence = "missing" | "empty" | "present";
228221229229-/**
230230- * Determines if a request body is present or missing.
231231- * Considers empty strings and empty arrays as missing when no content type is provided.
232232- * @param body - The request body
233233- * @param contentType - The Content-Type header value
234234- * @returns "present" if body exists, "missing" otherwise
235235- */
236222function getBodyPresence(req: Request): BodyPresence {
237223 if (req.headers.get("transfer-encoding") != null) return "present";
238224 if (req.headers.get("content-length") === "0") return "empty";