- Consolidated duplicate UndoManagers to single instance - Fixed connection promise to only resolve on 'connected' status - Fixed WebSocketProvider import (WebsocketProvider) - Added proper doc.destroy() cleanup - Renamed isPresenceInitialized property to avoid conflict Co-Authored-By: Paperclip <noreply@paperclip.ing>
701 lines
23 KiB
JavaScript
701 lines
23 KiB
JavaScript
import { __toESM, require_defineProperty, require_objectSpread2 } from "./objectSpread2-BvkFp-_Y.mjs";
|
|
import { TRPCClientError } from "./TRPCClientError-apv8gw59.mjs";
|
|
import { getTransformer } from "./unstable-internals-Bg7n9BBj.mjs";
|
|
import { behaviorSubject, observable } from "@trpc/server/observable";
|
|
import { run, sleep, transformResult } from "@trpc/server/unstable-core-do-not-import";
|
|
|
|
//#region src/links/wsLink/wsClient/encoder.ts
|
|
const jsonEncoder = {
|
|
encode: (data) => JSON.stringify(data),
|
|
decode: (data) => {
|
|
if (typeof data !== "string") throw new Error("jsonEncoder received binary data. JSON uses text frames. Use a binary encoder for binary data.");
|
|
return JSON.parse(data);
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/links/wsLink/wsClient/options.ts
|
|
const lazyDefaults = {
|
|
enabled: false,
|
|
closeMs: 0
|
|
};
|
|
const keepAliveDefaults = {
|
|
enabled: false,
|
|
pongTimeoutMs: 1e3,
|
|
intervalMs: 5e3
|
|
};
|
|
/**
|
|
* Calculates a delay for exponential backoff based on the retry attempt index.
|
|
* The delay starts at 0 for the first attempt and doubles for each subsequent attempt,
|
|
* capped at 30 seconds.
|
|
*/
|
|
const exponentialBackoff = (attemptIndex) => {
|
|
return attemptIndex === 0 ? 0 : Math.min(1e3 * 2 ** attemptIndex, 3e4);
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/links/internals/urlWithConnectionParams.ts
|
|
/**
|
|
* Get the result of a value or function that returns a value
|
|
* It also optionally accepts typesafe arguments for the function
|
|
*/
|
|
const resultOf = (value, ...args) => {
|
|
return typeof value === "function" ? value(...args) : value;
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/links/wsLink/wsClient/utils.ts
|
|
var import_defineProperty$3 = __toESM(require_defineProperty(), 1);
|
|
var TRPCWebSocketClosedError = class TRPCWebSocketClosedError extends Error {
|
|
constructor(opts) {
|
|
super(opts.message, { cause: opts.cause });
|
|
this.name = "TRPCWebSocketClosedError";
|
|
Object.setPrototypeOf(this, TRPCWebSocketClosedError.prototype);
|
|
}
|
|
};
|
|
/**
|
|
* Utility class for managing a timeout that can be started, stopped, and reset.
|
|
* Useful for scenarios where the timeout duration is reset dynamically based on events.
|
|
*/
|
|
var ResettableTimeout = class {
|
|
constructor(onTimeout, timeoutMs) {
|
|
this.onTimeout = onTimeout;
|
|
this.timeoutMs = timeoutMs;
|
|
(0, import_defineProperty$3.default)(this, "timeout", void 0);
|
|
}
|
|
/**
|
|
* Resets the current timeout, restarting it with the same duration.
|
|
* Does nothing if no timeout is active.
|
|
*/
|
|
reset() {
|
|
if (!this.timeout) return;
|
|
clearTimeout(this.timeout);
|
|
this.timeout = setTimeout(this.onTimeout, this.timeoutMs);
|
|
}
|
|
start() {
|
|
clearTimeout(this.timeout);
|
|
this.timeout = setTimeout(this.onTimeout, this.timeoutMs);
|
|
}
|
|
stop() {
|
|
clearTimeout(this.timeout);
|
|
this.timeout = void 0;
|
|
}
|
|
};
|
|
function withResolvers() {
|
|
let resolve;
|
|
let reject;
|
|
const promise = new Promise((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
return {
|
|
promise,
|
|
resolve,
|
|
reject
|
|
};
|
|
}
|
|
/**
|
|
* Resolves a WebSocket URL and optionally appends connection parameters.
|
|
*
|
|
* If connectionParams are provided, appends 'connectionParams=1' query parameter.
|
|
*/
|
|
async function prepareUrl(urlOptions) {
|
|
const url = await resultOf(urlOptions.url);
|
|
if (!urlOptions.connectionParams) return url;
|
|
const prefix = url.includes("?") ? "&" : "?";
|
|
const connectionParams = `${prefix}connectionParams=1`;
|
|
return url + connectionParams;
|
|
}
|
|
async function buildConnectionMessage(connectionParams, encoder) {
|
|
const message = {
|
|
method: "connectionParams",
|
|
data: await resultOf(connectionParams)
|
|
};
|
|
return encoder.encode(message);
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/links/wsLink/wsClient/requestManager.ts
|
|
var import_defineProperty$2 = __toESM(require_defineProperty(), 1);
|
|
/**
|
|
* Manages WebSocket requests, tracking their lifecycle and providing utility methods
|
|
* for handling outgoing and pending requests.
|
|
*
|
|
* - **Outgoing requests**: Requests that are queued and waiting to be sent.
|
|
* - **Pending requests**: Requests that have been sent and are in flight awaiting a response.
|
|
* For subscriptions, multiple responses may be received until the subscription is closed.
|
|
*/
|
|
var RequestManager = class {
|
|
constructor() {
|
|
(0, import_defineProperty$2.default)(this, "outgoingRequests", new Array());
|
|
(0, import_defineProperty$2.default)(this, "pendingRequests", {});
|
|
}
|
|
/**
|
|
* Registers a new request by adding it to the outgoing queue and setting up
|
|
* callbacks for lifecycle events such as completion or error.
|
|
*
|
|
* @param message - The outgoing message to be sent.
|
|
* @param callbacks - Callback functions to observe the request's state.
|
|
* @returns A cleanup function to manually remove the request.
|
|
*/
|
|
register(message, callbacks) {
|
|
const { promise: end, resolve } = withResolvers();
|
|
this.outgoingRequests.push({
|
|
id: String(message.id),
|
|
message,
|
|
end,
|
|
callbacks: {
|
|
next: callbacks.next,
|
|
complete: () => {
|
|
callbacks.complete();
|
|
resolve();
|
|
},
|
|
error: (e) => {
|
|
callbacks.error(e);
|
|
resolve();
|
|
}
|
|
}
|
|
});
|
|
return () => {
|
|
this.delete(message.id);
|
|
callbacks.complete();
|
|
resolve();
|
|
};
|
|
}
|
|
/**
|
|
* Deletes a request from both the outgoing and pending collections, if it exists.
|
|
*/
|
|
delete(messageId) {
|
|
if (messageId === null) return;
|
|
this.outgoingRequests = this.outgoingRequests.filter(({ id }) => id !== String(messageId));
|
|
delete this.pendingRequests[String(messageId)];
|
|
}
|
|
/**
|
|
* Moves all outgoing requests to the pending state and clears the outgoing queue.
|
|
*
|
|
* The caller is expected to handle the actual sending of the requests
|
|
* (e.g., sending them over the network) after this method is called.
|
|
*
|
|
* @returns The list of requests that were transitioned to the pending state.
|
|
*/
|
|
flush() {
|
|
const requests = this.outgoingRequests;
|
|
this.outgoingRequests = [];
|
|
for (const request of requests) this.pendingRequests[request.id] = request;
|
|
return requests;
|
|
}
|
|
/**
|
|
* Retrieves all currently pending requests, which are in flight awaiting responses
|
|
* or handling ongoing subscriptions.
|
|
*/
|
|
getPendingRequests() {
|
|
return Object.values(this.pendingRequests);
|
|
}
|
|
/**
|
|
* Retrieves a specific pending request by its message ID.
|
|
*/
|
|
getPendingRequest(messageId) {
|
|
if (messageId === null) return null;
|
|
return this.pendingRequests[String(messageId)];
|
|
}
|
|
/**
|
|
* Retrieves all outgoing requests, which are waiting to be sent.
|
|
*/
|
|
getOutgoingRequests() {
|
|
return this.outgoingRequests;
|
|
}
|
|
/**
|
|
* Retrieves all requests, both outgoing and pending, with their respective states.
|
|
*
|
|
* @returns An array of all requests with their state ("outgoing" or "pending").
|
|
*/
|
|
getRequests() {
|
|
return [...this.getOutgoingRequests().map((request) => ({
|
|
state: "outgoing",
|
|
message: request.message,
|
|
end: request.end,
|
|
callbacks: request.callbacks
|
|
})), ...this.getPendingRequests().map((request) => ({
|
|
state: "pending",
|
|
message: request.message,
|
|
end: request.end,
|
|
callbacks: request.callbacks
|
|
}))];
|
|
}
|
|
/**
|
|
* Checks if there are any pending requests, including ongoing subscriptions.
|
|
*/
|
|
hasPendingRequests() {
|
|
return this.getPendingRequests().length > 0;
|
|
}
|
|
/**
|
|
* Checks if there are any pending subscriptions
|
|
*/
|
|
hasPendingSubscriptions() {
|
|
return this.getPendingRequests().some((request) => request.message.method === "subscription");
|
|
}
|
|
/**
|
|
* Checks if there are any outgoing requests waiting to be sent.
|
|
*/
|
|
hasOutgoingRequests() {
|
|
return this.outgoingRequests.length > 0;
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/links/wsLink/wsClient/wsConnection.ts
|
|
var import_defineProperty$1 = __toESM(require_defineProperty(), 1);
|
|
/**
|
|
* Opens a WebSocket connection asynchronously and returns a promise
|
|
* that resolves when the connection is successfully established.
|
|
* The promise rejects if an error occurs during the connection attempt.
|
|
*/
|
|
function asyncWsOpen(ws) {
|
|
const { promise, resolve, reject } = withResolvers();
|
|
ws.addEventListener("open", () => {
|
|
ws.removeEventListener("error", reject);
|
|
resolve();
|
|
});
|
|
ws.addEventListener("error", reject);
|
|
return promise;
|
|
}
|
|
/**
|
|
* Sets up a periodic ping-pong mechanism to keep the WebSocket connection alive.
|
|
*
|
|
* - Sends "PING" messages at regular intervals defined by `intervalMs`.
|
|
* - If a "PONG" response is not received within the `pongTimeoutMs`, the WebSocket is closed.
|
|
* - The ping timer resets upon receiving any message to maintain activity.
|
|
* - Automatically starts the ping process when the WebSocket connection is opened.
|
|
* - Cleans up timers when the WebSocket is closed.
|
|
*
|
|
* @param ws - The WebSocket instance to manage.
|
|
* @param options - Configuration options for ping-pong intervals and timeouts.
|
|
*/
|
|
function setupPingInterval(ws, { intervalMs, pongTimeoutMs }) {
|
|
let pingTimeout;
|
|
let pongTimeout;
|
|
function start() {
|
|
pingTimeout = setTimeout(() => {
|
|
ws.send("PING");
|
|
pongTimeout = setTimeout(() => {
|
|
ws.close();
|
|
}, pongTimeoutMs);
|
|
}, intervalMs);
|
|
}
|
|
function reset() {
|
|
clearTimeout(pingTimeout);
|
|
start();
|
|
}
|
|
function pong() {
|
|
clearTimeout(pongTimeout);
|
|
reset();
|
|
}
|
|
ws.addEventListener("open", start);
|
|
ws.addEventListener("message", ({ data }) => {
|
|
clearTimeout(pingTimeout);
|
|
start();
|
|
if (data === "PONG") pong();
|
|
});
|
|
ws.addEventListener("close", () => {
|
|
clearTimeout(pingTimeout);
|
|
clearTimeout(pongTimeout);
|
|
});
|
|
}
|
|
/**
|
|
* Manages a WebSocket connection with support for reconnection, keep-alive mechanisms,
|
|
* and observable state tracking.
|
|
*/
|
|
var WsConnection = class WsConnection {
|
|
constructor(opts) {
|
|
var _opts$WebSocketPonyfi;
|
|
(0, import_defineProperty$1.default)(this, "id", ++WsConnection.connectCount);
|
|
(0, import_defineProperty$1.default)(this, "WebSocketPonyfill", void 0);
|
|
(0, import_defineProperty$1.default)(this, "urlOptions", void 0);
|
|
(0, import_defineProperty$1.default)(this, "keepAliveOpts", void 0);
|
|
(0, import_defineProperty$1.default)(this, "encoder", void 0);
|
|
(0, import_defineProperty$1.default)(this, "wsObservable", behaviorSubject(null));
|
|
(0, import_defineProperty$1.default)(this, "openPromise", null);
|
|
this.WebSocketPonyfill = (_opts$WebSocketPonyfi = opts.WebSocketPonyfill) !== null && _opts$WebSocketPonyfi !== void 0 ? _opts$WebSocketPonyfi : WebSocket;
|
|
if (!this.WebSocketPonyfill) throw new Error("No WebSocket implementation found - you probably don't want to use this on the server, but if you do you need to pass a `WebSocket`-ponyfill");
|
|
this.urlOptions = opts.urlOptions;
|
|
this.keepAliveOpts = opts.keepAlive;
|
|
this.encoder = opts.encoder;
|
|
}
|
|
get ws() {
|
|
return this.wsObservable.get();
|
|
}
|
|
set ws(ws) {
|
|
this.wsObservable.next(ws);
|
|
}
|
|
/**
|
|
* Checks if the WebSocket connection is open and ready to communicate.
|
|
*/
|
|
isOpen() {
|
|
return !!this.ws && this.ws.readyState === this.WebSocketPonyfill.OPEN && !this.openPromise;
|
|
}
|
|
/**
|
|
* Checks if the WebSocket connection is closed or in the process of closing.
|
|
*/
|
|
isClosed() {
|
|
return !!this.ws && (this.ws.readyState === this.WebSocketPonyfill.CLOSING || this.ws.readyState === this.WebSocketPonyfill.CLOSED);
|
|
}
|
|
async open() {
|
|
var _this = this;
|
|
if (_this.openPromise) return _this.openPromise;
|
|
_this.id = ++WsConnection.connectCount;
|
|
const wsPromise = prepareUrl(_this.urlOptions).then((url) => new _this.WebSocketPonyfill(url));
|
|
_this.openPromise = wsPromise.then(async (ws) => {
|
|
_this.ws = ws;
|
|
ws.binaryType = "arraybuffer";
|
|
ws.addEventListener("message", function({ data }) {
|
|
if (data === "PING") this.send("PONG");
|
|
});
|
|
if (_this.keepAliveOpts.enabled) setupPingInterval(ws, _this.keepAliveOpts);
|
|
ws.addEventListener("close", () => {
|
|
if (_this.ws === ws) _this.ws = null;
|
|
});
|
|
await asyncWsOpen(ws);
|
|
if (_this.urlOptions.connectionParams) ws.send(await buildConnectionMessage(_this.urlOptions.connectionParams, _this.encoder));
|
|
});
|
|
try {
|
|
await _this.openPromise;
|
|
} finally {
|
|
_this.openPromise = null;
|
|
}
|
|
}
|
|
/**
|
|
* Closes the WebSocket connection gracefully.
|
|
* Waits for any ongoing open operation to complete before closing.
|
|
*/
|
|
async close() {
|
|
var _this2 = this;
|
|
try {
|
|
await _this2.openPromise;
|
|
} finally {
|
|
var _this$ws;
|
|
(_this$ws = _this2.ws) === null || _this$ws === void 0 || _this$ws.close();
|
|
}
|
|
}
|
|
};
|
|
(0, import_defineProperty$1.default)(WsConnection, "connectCount", 0);
|
|
/**
|
|
* Provides a backward-compatible representation of the connection state.
|
|
*/
|
|
function backwardCompatibility(connection) {
|
|
if (connection.isOpen()) return {
|
|
id: connection.id,
|
|
state: "open",
|
|
ws: connection.ws
|
|
};
|
|
if (connection.isClosed()) return {
|
|
id: connection.id,
|
|
state: "closed",
|
|
ws: connection.ws
|
|
};
|
|
if (!connection.ws) return null;
|
|
return {
|
|
id: connection.id,
|
|
state: "connecting",
|
|
ws: connection.ws
|
|
};
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/links/wsLink/wsClient/wsClient.ts
|
|
var import_defineProperty = __toESM(require_defineProperty(), 1);
|
|
var import_objectSpread2 = __toESM(require_objectSpread2(), 1);
|
|
/**
|
|
* A WebSocket client for managing TRPC operations, supporting lazy initialization,
|
|
* reconnection, keep-alive, and request management.
|
|
*/
|
|
var WsClient = class {
|
|
constructor(opts) {
|
|
var _opts$experimental_en, _opts$retryDelayMs;
|
|
(0, import_defineProperty.default)(this, "connectionState", void 0);
|
|
(0, import_defineProperty.default)(this, "allowReconnect", false);
|
|
(0, import_defineProperty.default)(this, "requestManager", new RequestManager());
|
|
(0, import_defineProperty.default)(this, "activeConnection", void 0);
|
|
(0, import_defineProperty.default)(this, "reconnectRetryDelay", void 0);
|
|
(0, import_defineProperty.default)(this, "inactivityTimeout", void 0);
|
|
(0, import_defineProperty.default)(this, "callbacks", void 0);
|
|
(0, import_defineProperty.default)(this, "lazyMode", void 0);
|
|
(0, import_defineProperty.default)(this, "encoder", void 0);
|
|
(0, import_defineProperty.default)(this, "reconnecting", null);
|
|
this.encoder = (_opts$experimental_en = opts.experimental_encoder) !== null && _opts$experimental_en !== void 0 ? _opts$experimental_en : jsonEncoder;
|
|
this.callbacks = {
|
|
onOpen: opts.onOpen,
|
|
onClose: opts.onClose,
|
|
onError: opts.onError
|
|
};
|
|
const lazyOptions = (0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, lazyDefaults), opts.lazy);
|
|
this.inactivityTimeout = new ResettableTimeout(() => {
|
|
if (this.requestManager.hasOutgoingRequests() || this.requestManager.hasPendingRequests()) {
|
|
this.inactivityTimeout.reset();
|
|
return;
|
|
}
|
|
this.close().catch(() => null);
|
|
}, lazyOptions.closeMs);
|
|
this.activeConnection = new WsConnection({
|
|
WebSocketPonyfill: opts.WebSocket,
|
|
urlOptions: opts,
|
|
keepAlive: (0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, keepAliveDefaults), opts.keepAlive),
|
|
encoder: this.encoder
|
|
});
|
|
this.activeConnection.wsObservable.subscribe({ next: (ws) => {
|
|
if (!ws) return;
|
|
this.setupWebSocketListeners(ws);
|
|
} });
|
|
this.reconnectRetryDelay = (_opts$retryDelayMs = opts.retryDelayMs) !== null && _opts$retryDelayMs !== void 0 ? _opts$retryDelayMs : exponentialBackoff;
|
|
this.lazyMode = lazyOptions.enabled;
|
|
this.connectionState = behaviorSubject({
|
|
type: "state",
|
|
state: lazyOptions.enabled ? "idle" : "connecting",
|
|
error: null
|
|
});
|
|
if (!this.lazyMode) this.open().catch(() => null);
|
|
}
|
|
/**
|
|
* Opens the WebSocket connection. Handles reconnection attempts and updates
|
|
* the connection state accordingly.
|
|
*/
|
|
async open() {
|
|
var _this = this;
|
|
_this.allowReconnect = true;
|
|
if (_this.connectionState.get().state === "idle") _this.connectionState.next({
|
|
type: "state",
|
|
state: "connecting",
|
|
error: null
|
|
});
|
|
try {
|
|
await _this.activeConnection.open();
|
|
} catch (error) {
|
|
_this.reconnect(new TRPCWebSocketClosedError({
|
|
message: "Initialization error",
|
|
cause: error
|
|
}));
|
|
return _this.reconnecting;
|
|
}
|
|
}
|
|
/**
|
|
* Closes the WebSocket connection and stops managing requests.
|
|
* Ensures all outgoing and pending requests are properly finalized.
|
|
*/
|
|
async close() {
|
|
var _this2 = this;
|
|
_this2.allowReconnect = false;
|
|
_this2.inactivityTimeout.stop();
|
|
const requestsToAwait = [];
|
|
for (const request of _this2.requestManager.getRequests()) if (request.message.method === "subscription") request.callbacks.complete();
|
|
else if (request.state === "outgoing") request.callbacks.error(TRPCClientError.from(new TRPCWebSocketClosedError({ message: "Closed before connection was established" })));
|
|
else requestsToAwait.push(request.end);
|
|
await Promise.all(requestsToAwait).catch(() => null);
|
|
await _this2.activeConnection.close().catch(() => null);
|
|
_this2.connectionState.next({
|
|
type: "state",
|
|
state: "idle",
|
|
error: null
|
|
});
|
|
}
|
|
/**
|
|
* Method to request the server.
|
|
* Handles data transformation, batching of requests, and subscription lifecycle.
|
|
*
|
|
* @param op - The operation details including id, type, path, input and signal
|
|
* @param transformer - Data transformer for serializing requests and deserializing responses
|
|
* @param lastEventId - Optional ID of the last received event for subscriptions
|
|
*
|
|
* @returns An observable that emits operation results and handles cleanup
|
|
*/
|
|
request({ op: { id, type, path, input, signal }, transformer, lastEventId }) {
|
|
return observable((observer) => {
|
|
const abort = this.batchSend({
|
|
id,
|
|
method: type,
|
|
params: {
|
|
input: transformer.input.serialize(input),
|
|
path,
|
|
lastEventId
|
|
}
|
|
}, (0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, observer), {}, { next(event) {
|
|
const transformed = transformResult(event, transformer.output);
|
|
if (!transformed.ok) {
|
|
observer.error(TRPCClientError.from(transformed.error));
|
|
return;
|
|
}
|
|
observer.next({ result: transformed.result });
|
|
} }));
|
|
return () => {
|
|
abort();
|
|
if (type === "subscription" && this.activeConnection.isOpen()) this.send({
|
|
id,
|
|
method: "subscription.stop"
|
|
});
|
|
signal === null || signal === void 0 || signal.removeEventListener("abort", abort);
|
|
};
|
|
});
|
|
}
|
|
get connection() {
|
|
return backwardCompatibility(this.activeConnection);
|
|
}
|
|
reconnect(closedError) {
|
|
var _this3 = this;
|
|
this.connectionState.next({
|
|
type: "state",
|
|
state: "connecting",
|
|
error: TRPCClientError.from(closedError)
|
|
});
|
|
if (this.reconnecting) return;
|
|
const tryReconnect = async (attemptIndex) => {
|
|
try {
|
|
await sleep(_this3.reconnectRetryDelay(attemptIndex));
|
|
if (_this3.allowReconnect) {
|
|
await _this3.activeConnection.close();
|
|
await _this3.activeConnection.open();
|
|
if (_this3.requestManager.hasPendingRequests()) _this3.send(_this3.requestManager.getPendingRequests().map(({ message }) => message));
|
|
}
|
|
_this3.reconnecting = null;
|
|
} catch (_unused) {
|
|
await tryReconnect(attemptIndex + 1);
|
|
}
|
|
};
|
|
this.reconnecting = tryReconnect(0);
|
|
}
|
|
setupWebSocketListeners(ws) {
|
|
var _this4 = this;
|
|
const handleCloseOrError = (cause) => {
|
|
const reqs = this.requestManager.getPendingRequests();
|
|
for (const { message, callbacks } of reqs) {
|
|
if (message.method === "subscription") continue;
|
|
callbacks.error(TRPCClientError.from(cause !== null && cause !== void 0 ? cause : new TRPCWebSocketClosedError({
|
|
message: "WebSocket closed",
|
|
cause
|
|
})));
|
|
this.requestManager.delete(message.id);
|
|
}
|
|
};
|
|
ws.addEventListener("open", () => {
|
|
run(async () => {
|
|
var _this$callbacks$onOpe, _this$callbacks;
|
|
if (_this4.lazyMode) _this4.inactivityTimeout.start();
|
|
(_this$callbacks$onOpe = (_this$callbacks = _this4.callbacks).onOpen) === null || _this$callbacks$onOpe === void 0 || _this$callbacks$onOpe.call(_this$callbacks);
|
|
_this4.connectionState.next({
|
|
type: "state",
|
|
state: "pending",
|
|
error: null
|
|
});
|
|
}).catch((error) => {
|
|
ws.close(3e3);
|
|
handleCloseOrError(error);
|
|
});
|
|
});
|
|
ws.addEventListener("message", ({ data }) => {
|
|
this.inactivityTimeout.reset();
|
|
if (["PING", "PONG"].includes(data)) return;
|
|
const incomingMessage = this.encoder.decode(data);
|
|
if ("method" in incomingMessage) {
|
|
this.handleIncomingRequest(incomingMessage);
|
|
return;
|
|
}
|
|
this.handleResponseMessage(incomingMessage);
|
|
});
|
|
ws.addEventListener("close", (event) => {
|
|
var _this$callbacks$onClo, _this$callbacks2;
|
|
handleCloseOrError(event);
|
|
(_this$callbacks$onClo = (_this$callbacks2 = this.callbacks).onClose) === null || _this$callbacks$onClo === void 0 || _this$callbacks$onClo.call(_this$callbacks2, event);
|
|
if (!this.lazyMode || this.requestManager.hasPendingSubscriptions()) this.reconnect(new TRPCWebSocketClosedError({
|
|
message: "WebSocket closed",
|
|
cause: event
|
|
}));
|
|
});
|
|
ws.addEventListener("error", (event) => {
|
|
var _this$callbacks$onErr, _this$callbacks3;
|
|
handleCloseOrError(event);
|
|
(_this$callbacks$onErr = (_this$callbacks3 = this.callbacks).onError) === null || _this$callbacks$onErr === void 0 || _this$callbacks$onErr.call(_this$callbacks3, event);
|
|
this.reconnect(new TRPCWebSocketClosedError({
|
|
message: "WebSocket closed",
|
|
cause: event
|
|
}));
|
|
});
|
|
}
|
|
handleResponseMessage(message) {
|
|
const request = this.requestManager.getPendingRequest(message.id);
|
|
if (!request) return;
|
|
request.callbacks.next(message);
|
|
let completed = true;
|
|
if ("result" in message && request.message.method === "subscription") {
|
|
if (message.result.type === "data") request.message.params.lastEventId = message.result.id;
|
|
if (message.result.type !== "stopped") completed = false;
|
|
}
|
|
if (completed) {
|
|
request.callbacks.complete();
|
|
this.requestManager.delete(message.id);
|
|
}
|
|
}
|
|
handleIncomingRequest(message) {
|
|
if (message.method === "reconnect") this.reconnect(new TRPCWebSocketClosedError({ message: "Server requested reconnect" }));
|
|
}
|
|
/**
|
|
* Sends a message or batch of messages directly to the server.
|
|
*/
|
|
send(messageOrMessages) {
|
|
if (!this.activeConnection.isOpen()) throw new Error("Active connection is not open");
|
|
const messages = messageOrMessages instanceof Array ? messageOrMessages : [messageOrMessages];
|
|
this.activeConnection.ws.send(this.encoder.encode(messages.length === 1 ? messages[0] : messages));
|
|
}
|
|
/**
|
|
* Groups requests for batch sending.
|
|
*
|
|
* @returns A function to abort the batched request.
|
|
*/
|
|
batchSend(message, callbacks) {
|
|
var _this5 = this;
|
|
this.inactivityTimeout.reset();
|
|
run(async () => {
|
|
if (!_this5.activeConnection.isOpen()) await _this5.open();
|
|
await sleep(0);
|
|
if (!_this5.requestManager.hasOutgoingRequests()) return;
|
|
_this5.send(_this5.requestManager.flush().map(({ message: message$1 }) => message$1));
|
|
}).catch((err) => {
|
|
this.requestManager.delete(message.id);
|
|
callbacks.error(TRPCClientError.from(err));
|
|
});
|
|
return this.requestManager.register(message, callbacks);
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/links/wsLink/createWsClient.ts
|
|
function createWSClient(opts) {
|
|
return new WsClient(opts);
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/links/wsLink/wsLink.ts
|
|
function wsLink(opts) {
|
|
const { client } = opts;
|
|
const transformer = getTransformer(opts.transformer);
|
|
return () => {
|
|
return ({ op }) => {
|
|
return observable((observer) => {
|
|
const connStateSubscription = op.type === "subscription" ? client.connectionState.subscribe({ next(result) {
|
|
observer.next({
|
|
result,
|
|
context: op.context
|
|
});
|
|
} }) : null;
|
|
const requestSubscription = client.request({
|
|
op,
|
|
transformer
|
|
}).subscribe(observer);
|
|
return () => {
|
|
requestSubscription.unsubscribe();
|
|
connStateSubscription === null || connStateSubscription === void 0 || connStateSubscription.unsubscribe();
|
|
};
|
|
});
|
|
};
|
|
};
|
|
}
|
|
|
|
//#endregion
|
|
export { createWSClient, jsonEncoder, resultOf, wsLink };
|
|
//# sourceMappingURL=wsLink-DSf4KOdW.mjs.map
|