You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
801 lines
37 KiB
801 lines
37 KiB
4 months ago
|
(function (global, factory) {
|
||
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
||
|
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
||
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.graphqlWs = {}));
|
||
|
})(this, (function (exports) { 'use strict';
|
||
|
|
||
|
/******************************************************************************
|
||
|
Copyright (c) Microsoft Corporation.
|
||
|
|
||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||
|
purpose with or without fee is hereby granted.
|
||
|
|
||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||
|
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||
|
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||
|
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||
|
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||
|
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||
|
PERFORMANCE OF THIS SOFTWARE.
|
||
|
***************************************************************************** */
|
||
|
/* global Reflect, Promise */
|
||
|
|
||
|
|
||
|
function __await(v) {
|
||
|
return this instanceof __await ? (this.v = v, this) : new __await(v);
|
||
|
}
|
||
|
|
||
|
function __asyncGenerator(thisArg, _arguments, generator) {
|
||
|
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
||
|
var g = generator.apply(thisArg, _arguments || []), i, q = [];
|
||
|
return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i;
|
||
|
function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }
|
||
|
function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
|
||
|
function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
|
||
|
function fulfill(value) { resume("next", value); }
|
||
|
function reject(value) { resume("throw", value); }
|
||
|
function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
|
||
|
}
|
||
|
|
||
|
/** @private */
|
||
|
function extendedTypeof(val) {
|
||
|
if (val === null) {
|
||
|
return 'null';
|
||
|
}
|
||
|
if (Array.isArray(val)) {
|
||
|
return 'array';
|
||
|
}
|
||
|
return typeof val;
|
||
|
}
|
||
|
/** @private */
|
||
|
function isObject(val) {
|
||
|
return extendedTypeof(val) === 'object';
|
||
|
}
|
||
|
/** @private */
|
||
|
function areGraphQLErrors(obj) {
|
||
|
return (Array.isArray(obj) &&
|
||
|
// must be at least one error
|
||
|
obj.length > 0 &&
|
||
|
// error has at least a message
|
||
|
obj.every((ob) => 'message' in ob));
|
||
|
}
|
||
|
/**
|
||
|
* Limits the WebSocket close event reason to not exceed a length of one frame.
|
||
|
* Reference: https://datatracker.ietf.org/doc/html/rfc6455#section-5.2.
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
function limitCloseReason(reason, whenTooLong) {
|
||
|
return reason.length < 124 ? reason : whenTooLong;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* common
|
||
|
*
|
||
|
*/
|
||
|
/**
|
||
|
* The WebSocket sub-protocol used for the [GraphQL over WebSocket Protocol](https://github.com/graphql/graphql-over-http/blob/main/rfcs/GraphQLOverWebSocket.md).
|
||
|
*
|
||
|
* @category Common
|
||
|
*/
|
||
|
const GRAPHQL_TRANSPORT_WS_PROTOCOL = 'graphql-transport-ws';
|
||
|
/**
|
||
|
* The deprecated subprotocol used by [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws).
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
const DEPRECATED_GRAPHQL_WS_PROTOCOL = 'graphql-ws';
|
||
|
/**
|
||
|
* `graphql-ws` expected and standard close codes of the [GraphQL over WebSocket Protocol](https://github.com/graphql/graphql-over-http/blob/main/rfcs/GraphQLOverWebSocket.md).
|
||
|
*
|
||
|
* @category Common
|
||
|
*/
|
||
|
exports.CloseCode = void 0;
|
||
|
(function (CloseCode) {
|
||
|
CloseCode[CloseCode["InternalServerError"] = 4500] = "InternalServerError";
|
||
|
CloseCode[CloseCode["InternalClientError"] = 4005] = "InternalClientError";
|
||
|
CloseCode[CloseCode["BadRequest"] = 4400] = "BadRequest";
|
||
|
CloseCode[CloseCode["BadResponse"] = 4004] = "BadResponse";
|
||
|
/** Tried subscribing before connect ack */
|
||
|
CloseCode[CloseCode["Unauthorized"] = 4401] = "Unauthorized";
|
||
|
CloseCode[CloseCode["Forbidden"] = 4403] = "Forbidden";
|
||
|
CloseCode[CloseCode["SubprotocolNotAcceptable"] = 4406] = "SubprotocolNotAcceptable";
|
||
|
CloseCode[CloseCode["ConnectionInitialisationTimeout"] = 4408] = "ConnectionInitialisationTimeout";
|
||
|
CloseCode[CloseCode["ConnectionAcknowledgementTimeout"] = 4504] = "ConnectionAcknowledgementTimeout";
|
||
|
/** Subscriber distinction is very important */
|
||
|
CloseCode[CloseCode["SubscriberAlreadyExists"] = 4409] = "SubscriberAlreadyExists";
|
||
|
CloseCode[CloseCode["TooManyInitialisationRequests"] = 4429] = "TooManyInitialisationRequests";
|
||
|
})(exports.CloseCode || (exports.CloseCode = {}));
|
||
|
/**
|
||
|
* Types of messages allowed to be sent by the client/server over the WS protocol.
|
||
|
*
|
||
|
* @category Common
|
||
|
*/
|
||
|
exports.MessageType = void 0;
|
||
|
(function (MessageType) {
|
||
|
MessageType["ConnectionInit"] = "connection_init";
|
||
|
MessageType["ConnectionAck"] = "connection_ack";
|
||
|
MessageType["Ping"] = "ping";
|
||
|
MessageType["Pong"] = "pong";
|
||
|
MessageType["Subscribe"] = "subscribe";
|
||
|
MessageType["Next"] = "next";
|
||
|
MessageType["Error"] = "error";
|
||
|
MessageType["Complete"] = "complete";
|
||
|
})(exports.MessageType || (exports.MessageType = {}));
|
||
|
/**
|
||
|
* Validates the message against the GraphQL over WebSocket Protocol.
|
||
|
*
|
||
|
* Invalid messages will throw descriptive errors.
|
||
|
*
|
||
|
* @category Common
|
||
|
*/
|
||
|
function validateMessage(val) {
|
||
|
if (!isObject(val)) {
|
||
|
throw new Error(`Message is expected to be an object, but got ${extendedTypeof(val)}`);
|
||
|
}
|
||
|
if (!val.type) {
|
||
|
throw new Error(`Message is missing the 'type' property`);
|
||
|
}
|
||
|
if (typeof val.type !== 'string') {
|
||
|
throw new Error(`Message is expects the 'type' property to be a string, but got ${extendedTypeof(val.type)}`);
|
||
|
}
|
||
|
switch (val.type) {
|
||
|
case exports.MessageType.ConnectionInit:
|
||
|
case exports.MessageType.ConnectionAck:
|
||
|
case exports.MessageType.Ping:
|
||
|
case exports.MessageType.Pong: {
|
||
|
if (val.payload != null && !isObject(val.payload)) {
|
||
|
throw new Error(`"${val.type}" message expects the 'payload' property to be an object or nullish or missing, but got "${val.payload}"`);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case exports.MessageType.Subscribe: {
|
||
|
if (typeof val.id !== 'string') {
|
||
|
throw new Error(`"${val.type}" message expects the 'id' property to be a string, but got ${extendedTypeof(val.id)}`);
|
||
|
}
|
||
|
if (!val.id) {
|
||
|
throw new Error(`"${val.type}" message requires a non-empty 'id' property`);
|
||
|
}
|
||
|
if (!isObject(val.payload)) {
|
||
|
throw new Error(`"${val.type}" message expects the 'payload' property to be an object, but got ${extendedTypeof(val.payload)}`);
|
||
|
}
|
||
|
if (typeof val.payload.query !== 'string') {
|
||
|
throw new Error(`"${val.type}" message payload expects the 'query' property to be a string, but got ${extendedTypeof(val.payload.query)}`);
|
||
|
}
|
||
|
if (val.payload.variables != null && !isObject(val.payload.variables)) {
|
||
|
throw new Error(`"${val.type}" message payload expects the 'variables' property to be a an object or nullish or missing, but got ${extendedTypeof(val.payload.variables)}`);
|
||
|
}
|
||
|
if (val.payload.operationName != null &&
|
||
|
extendedTypeof(val.payload.operationName) !== 'string') {
|
||
|
throw new Error(`"${val.type}" message payload expects the 'operationName' property to be a string or nullish or missing, but got ${extendedTypeof(val.payload.operationName)}`);
|
||
|
}
|
||
|
if (val.payload.extensions != null && !isObject(val.payload.extensions)) {
|
||
|
throw new Error(`"${val.type}" message payload expects the 'extensions' property to be a an object or nullish or missing, but got ${extendedTypeof(val.payload.extensions)}`);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case exports.MessageType.Next: {
|
||
|
if (typeof val.id !== 'string') {
|
||
|
throw new Error(`"${val.type}" message expects the 'id' property to be a string, but got ${extendedTypeof(val.id)}`);
|
||
|
}
|
||
|
if (!val.id) {
|
||
|
throw new Error(`"${val.type}" message requires a non-empty 'id' property`);
|
||
|
}
|
||
|
if (!isObject(val.payload)) {
|
||
|
throw new Error(`"${val.type}" message expects the 'payload' property to be an object, but got ${extendedTypeof(val.payload)}`);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case exports.MessageType.Error: {
|
||
|
if (typeof val.id !== 'string') {
|
||
|
throw new Error(`"${val.type}" message expects the 'id' property to be a string, but got ${extendedTypeof(val.id)}`);
|
||
|
}
|
||
|
if (!val.id) {
|
||
|
throw new Error(`"${val.type}" message requires a non-empty 'id' property`);
|
||
|
}
|
||
|
if (!areGraphQLErrors(val.payload)) {
|
||
|
throw new Error(`"${val.type}" message expects the 'payload' property to be an array of GraphQL errors, but got ${JSON.stringify(val.payload)}`);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case exports.MessageType.Complete: {
|
||
|
if (typeof val.id !== 'string') {
|
||
|
throw new Error(`"${val.type}" message expects the 'id' property to be a string, but got ${extendedTypeof(val.id)}`);
|
||
|
}
|
||
|
if (!val.id) {
|
||
|
throw new Error(`"${val.type}" message requires a non-empty 'id' property`);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
default:
|
||
|
throw new Error(`Invalid message 'type' property "${val.type}"`);
|
||
|
}
|
||
|
return val;
|
||
|
}
|
||
|
/**
|
||
|
* Checks if the provided value is a valid GraphQL over WebSocket message.
|
||
|
*
|
||
|
* @deprecated Use `validateMessage` instead.
|
||
|
*
|
||
|
* @category Common
|
||
|
*/
|
||
|
function isMessage(val) {
|
||
|
try {
|
||
|
validateMessage(val);
|
||
|
return true;
|
||
|
}
|
||
|
catch (_a) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Parses the raw websocket message data to a valid message.
|
||
|
*
|
||
|
* @category Common
|
||
|
*/
|
||
|
function parseMessage(data, reviver) {
|
||
|
return validateMessage(typeof data === 'string' ? JSON.parse(data, reviver) : data);
|
||
|
}
|
||
|
/**
|
||
|
* Stringifies a valid message ready to be sent through the socket.
|
||
|
*
|
||
|
* @category Common
|
||
|
*/
|
||
|
function stringifyMessage(msg, replacer) {
|
||
|
validateMessage(msg);
|
||
|
return JSON.stringify(msg, replacer);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* client
|
||
|
*
|
||
|
*/
|
||
|
/**
|
||
|
* Creates a disposable GraphQL over WebSocket client.
|
||
|
*
|
||
|
* @category Client
|
||
|
*/
|
||
|
function createClient(options) {
|
||
|
const { url, connectionParams, lazy = true, onNonLazyError = console.error, lazyCloseTimeout: lazyCloseTimeoutMs = 0, keepAlive = 0, disablePong, connectionAckWaitTimeout = 0, retryAttempts = 5, retryWait = async function randomisedExponentialBackoff(retries) {
|
||
|
let retryDelay = 1000; // start with 1s delay
|
||
|
for (let i = 0; i < retries; i++) {
|
||
|
retryDelay *= 2;
|
||
|
}
|
||
|
await new Promise((resolve) => setTimeout(resolve, retryDelay +
|
||
|
// add random timeout from 300ms to 3s
|
||
|
Math.floor(Math.random() * (3000 - 300) + 300)));
|
||
|
}, shouldRetry = isLikeCloseEvent, isFatalConnectionProblem, on, webSocketImpl,
|
||
|
/**
|
||
|
* Generates a v4 UUID to be used as the ID using `Math`
|
||
|
* as the random number generator. Supply your own generator
|
||
|
* in case you need more uniqueness.
|
||
|
*
|
||
|
* Reference: https://gist.github.com/jed/982883
|
||
|
*/
|
||
|
generateID = function generateUUID() {
|
||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||
|
const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
|
||
|
return v.toString(16);
|
||
|
});
|
||
|
}, jsonMessageReplacer: replacer, jsonMessageReviver: reviver, } = options;
|
||
|
let ws;
|
||
|
if (webSocketImpl) {
|
||
|
if (!isWebSocket(webSocketImpl)) {
|
||
|
throw new Error('Invalid WebSocket implementation provided');
|
||
|
}
|
||
|
ws = webSocketImpl;
|
||
|
}
|
||
|
else if (typeof WebSocket !== 'undefined') {
|
||
|
ws = WebSocket;
|
||
|
}
|
||
|
else if (typeof global !== 'undefined') {
|
||
|
ws =
|
||
|
global.WebSocket ||
|
||
|
// @ts-expect-error: Support more browsers
|
||
|
global.MozWebSocket;
|
||
|
}
|
||
|
else if (typeof window !== 'undefined') {
|
||
|
ws =
|
||
|
window.WebSocket ||
|
||
|
// @ts-expect-error: Support more browsers
|
||
|
window.MozWebSocket;
|
||
|
}
|
||
|
if (!ws)
|
||
|
throw new Error("WebSocket implementation missing; on Node you can `import WebSocket from 'ws';` and pass `webSocketImpl: WebSocket` to `createClient`");
|
||
|
const WebSocketImpl = ws;
|
||
|
// websocket status emitter, subscriptions are handled differently
|
||
|
const emitter = (() => {
|
||
|
const message = (() => {
|
||
|
const listeners = {};
|
||
|
return {
|
||
|
on(id, listener) {
|
||
|
listeners[id] = listener;
|
||
|
return () => {
|
||
|
delete listeners[id];
|
||
|
};
|
||
|
},
|
||
|
emit(message) {
|
||
|
var _a;
|
||
|
if ('id' in message)
|
||
|
(_a = listeners[message.id]) === null || _a === void 0 ? void 0 : _a.call(listeners, message);
|
||
|
},
|
||
|
};
|
||
|
})();
|
||
|
const listeners = {
|
||
|
connecting: (on === null || on === void 0 ? void 0 : on.connecting) ? [on.connecting] : [],
|
||
|
opened: (on === null || on === void 0 ? void 0 : on.opened) ? [on.opened] : [],
|
||
|
connected: (on === null || on === void 0 ? void 0 : on.connected) ? [on.connected] : [],
|
||
|
ping: (on === null || on === void 0 ? void 0 : on.ping) ? [on.ping] : [],
|
||
|
pong: (on === null || on === void 0 ? void 0 : on.pong) ? [on.pong] : [],
|
||
|
message: (on === null || on === void 0 ? void 0 : on.message) ? [message.emit, on.message] : [message.emit],
|
||
|
closed: (on === null || on === void 0 ? void 0 : on.closed) ? [on.closed] : [],
|
||
|
error: (on === null || on === void 0 ? void 0 : on.error) ? [on.error] : [],
|
||
|
};
|
||
|
return {
|
||
|
onMessage: message.on,
|
||
|
on(event, listener) {
|
||
|
const l = listeners[event];
|
||
|
l.push(listener);
|
||
|
return () => {
|
||
|
l.splice(l.indexOf(listener), 1);
|
||
|
};
|
||
|
},
|
||
|
emit(event, ...args) {
|
||
|
// we copy the listeners so that unlistens dont "pull the rug under our feet"
|
||
|
for (const listener of [...listeners[event]]) {
|
||
|
// @ts-expect-error: The args should fit
|
||
|
listener(...args);
|
||
|
}
|
||
|
},
|
||
|
};
|
||
|
})();
|
||
|
// invokes the callback either when an error or closed event is emitted,
|
||
|
// first one that gets called prevails, other emissions are ignored
|
||
|
function errorOrClosed(cb) {
|
||
|
const listening = [
|
||
|
// errors are fatal and more critical than close events, throw them first
|
||
|
emitter.on('error', (err) => {
|
||
|
listening.forEach((unlisten) => unlisten());
|
||
|
cb(err);
|
||
|
}),
|
||
|
// closes can be graceful and not fatal, throw them second (if error didnt throw)
|
||
|
emitter.on('closed', (event) => {
|
||
|
listening.forEach((unlisten) => unlisten());
|
||
|
cb(event);
|
||
|
}),
|
||
|
];
|
||
|
}
|
||
|
let connecting, locks = 0, lazyCloseTimeout, retrying = false, retries = 0, disposed = false;
|
||
|
async function connect() {
|
||
|
// clear the lazy close timeout immediatelly so that close gets debounced
|
||
|
// see: https://github.com/enisdenjo/graphql-ws/issues/388
|
||
|
clearTimeout(lazyCloseTimeout);
|
||
|
const [socket, throwOnClose] = await (connecting !== null && connecting !== void 0 ? connecting : (connecting = new Promise((connected, denied) => (async () => {
|
||
|
if (retrying) {
|
||
|
await retryWait(retries);
|
||
|
// subscriptions might complete while waiting for retry
|
||
|
if (!locks) {
|
||
|
connecting = undefined;
|
||
|
return denied({ code: 1000, reason: 'All Subscriptions Gone' });
|
||
|
}
|
||
|
retries++;
|
||
|
}
|
||
|
emitter.emit('connecting', retrying);
|
||
|
const socket = new WebSocketImpl(typeof url === 'function' ? await url() : url, GRAPHQL_TRANSPORT_WS_PROTOCOL);
|
||
|
let connectionAckTimeout, queuedPing;
|
||
|
function enqueuePing() {
|
||
|
if (isFinite(keepAlive) && keepAlive > 0) {
|
||
|
clearTimeout(queuedPing); // in case where a pong was received before a ping (this is valid behaviour)
|
||
|
queuedPing = setTimeout(() => {
|
||
|
if (socket.readyState === WebSocketImpl.OPEN) {
|
||
|
socket.send(stringifyMessage({ type: exports.MessageType.Ping }));
|
||
|
emitter.emit('ping', false, undefined);
|
||
|
}
|
||
|
}, keepAlive);
|
||
|
}
|
||
|
}
|
||
|
errorOrClosed((errOrEvent) => {
|
||
|
connecting = undefined;
|
||
|
clearTimeout(connectionAckTimeout);
|
||
|
clearTimeout(queuedPing);
|
||
|
denied(errOrEvent);
|
||
|
if (errOrEvent instanceof TerminatedCloseEvent) {
|
||
|
socket.close(4499, 'Terminated'); // close event is artificial and emitted manually, see `Client.terminate()` below
|
||
|
socket.onerror = null;
|
||
|
socket.onclose = null;
|
||
|
}
|
||
|
});
|
||
|
socket.onerror = (err) => emitter.emit('error', err);
|
||
|
socket.onclose = (event) => emitter.emit('closed', event);
|
||
|
socket.onopen = async () => {
|
||
|
try {
|
||
|
emitter.emit('opened', socket);
|
||
|
const payload = typeof connectionParams === 'function'
|
||
|
? await connectionParams()
|
||
|
: connectionParams;
|
||
|
// connectionParams might take too long causing the server to kick off the client
|
||
|
// the necessary error/close event is already reported - simply stop execution
|
||
|
if (socket.readyState !== WebSocketImpl.OPEN)
|
||
|
return;
|
||
|
socket.send(stringifyMessage(payload
|
||
|
? {
|
||
|
type: exports.MessageType.ConnectionInit,
|
||
|
payload,
|
||
|
}
|
||
|
: {
|
||
|
type: exports.MessageType.ConnectionInit,
|
||
|
// payload is completely absent if not provided
|
||
|
}, replacer));
|
||
|
if (isFinite(connectionAckWaitTimeout) &&
|
||
|
connectionAckWaitTimeout > 0) {
|
||
|
connectionAckTimeout = setTimeout(() => {
|
||
|
socket.close(exports.CloseCode.ConnectionAcknowledgementTimeout, 'Connection acknowledgement timeout');
|
||
|
}, connectionAckWaitTimeout);
|
||
|
}
|
||
|
enqueuePing(); // enqueue ping (noop if disabled)
|
||
|
}
|
||
|
catch (err) {
|
||
|
emitter.emit('error', err);
|
||
|
socket.close(exports.CloseCode.InternalClientError, limitCloseReason(err instanceof Error ? err.message : new Error(err).message, 'Internal client error'));
|
||
|
}
|
||
|
};
|
||
|
let acknowledged = false;
|
||
|
socket.onmessage = ({ data }) => {
|
||
|
try {
|
||
|
const message = parseMessage(data, reviver);
|
||
|
emitter.emit('message', message);
|
||
|
if (message.type === 'ping' || message.type === 'pong') {
|
||
|
emitter.emit(message.type, true, message.payload); // received
|
||
|
if (message.type === 'pong') {
|
||
|
enqueuePing(); // enqueue next ping (noop if disabled)
|
||
|
}
|
||
|
else if (!disablePong) {
|
||
|
// respond with pong on ping
|
||
|
socket.send(stringifyMessage(message.payload
|
||
|
? {
|
||
|
type: exports.MessageType.Pong,
|
||
|
payload: message.payload,
|
||
|
}
|
||
|
: {
|
||
|
type: exports.MessageType.Pong,
|
||
|
// payload is completely absent if not provided
|
||
|
}));
|
||
|
emitter.emit('pong', false, message.payload);
|
||
|
}
|
||
|
return; // ping and pongs can be received whenever
|
||
|
}
|
||
|
if (acknowledged)
|
||
|
return; // already connected and acknowledged
|
||
|
if (message.type !== exports.MessageType.ConnectionAck)
|
||
|
throw new Error(`First message cannot be of type ${message.type}`);
|
||
|
clearTimeout(connectionAckTimeout);
|
||
|
acknowledged = true;
|
||
|
emitter.emit('connected', socket, message.payload, retrying); // connected = socket opened + acknowledged
|
||
|
retrying = false; // future lazy connects are not retries
|
||
|
retries = 0; // reset the retries on connect
|
||
|
connected([
|
||
|
socket,
|
||
|
new Promise((_, reject) => errorOrClosed(reject)),
|
||
|
]);
|
||
|
}
|
||
|
catch (err) {
|
||
|
socket.onmessage = null; // stop reading messages as soon as reading breaks once
|
||
|
emitter.emit('error', err);
|
||
|
socket.close(exports.CloseCode.BadResponse, limitCloseReason(err instanceof Error ? err.message : new Error(err).message, 'Bad response'));
|
||
|
}
|
||
|
};
|
||
|
})())));
|
||
|
// if the provided socket is in a closing state, wait for the throw on close
|
||
|
if (socket.readyState === WebSocketImpl.CLOSING)
|
||
|
await throwOnClose;
|
||
|
let release = () => {
|
||
|
// releases this connection
|
||
|
};
|
||
|
const released = new Promise((resolve) => (release = resolve));
|
||
|
return [
|
||
|
socket,
|
||
|
release,
|
||
|
Promise.race([
|
||
|
// wait for
|
||
|
released.then(() => {
|
||
|
if (!locks) {
|
||
|
// and if no more locks are present, complete the connection
|
||
|
const complete = () => socket.close(1000, 'Normal Closure');
|
||
|
if (isFinite(lazyCloseTimeoutMs) && lazyCloseTimeoutMs > 0) {
|
||
|
// if the keepalive is set, allow for the specified calmdown time and
|
||
|
// then complete if the socket is still open.
|
||
|
lazyCloseTimeout = setTimeout(() => {
|
||
|
if (socket.readyState === WebSocketImpl.OPEN)
|
||
|
complete();
|
||
|
}, lazyCloseTimeoutMs);
|
||
|
}
|
||
|
else {
|
||
|
// otherwise complete immediately
|
||
|
complete();
|
||
|
}
|
||
|
}
|
||
|
}),
|
||
|
// or
|
||
|
throwOnClose,
|
||
|
]),
|
||
|
];
|
||
|
}
|
||
|
/**
|
||
|
* Checks the `connect` problem and evaluates if the client should retry.
|
||
|
*/
|
||
|
function shouldRetryConnectOrThrow(errOrCloseEvent) {
|
||
|
// some close codes are worth reporting immediately
|
||
|
if (isLikeCloseEvent(errOrCloseEvent) &&
|
||
|
(isFatalInternalCloseCode(errOrCloseEvent.code) ||
|
||
|
[
|
||
|
exports.CloseCode.InternalServerError,
|
||
|
exports.CloseCode.InternalClientError,
|
||
|
exports.CloseCode.BadRequest,
|
||
|
exports.CloseCode.BadResponse,
|
||
|
exports.CloseCode.Unauthorized,
|
||
|
// CloseCode.Forbidden, might grant access out after retry
|
||
|
exports.CloseCode.SubprotocolNotAcceptable,
|
||
|
// CloseCode.ConnectionInitialisationTimeout, might not time out after retry
|
||
|
// CloseCode.ConnectionAcknowledgementTimeout, might not time out after retry
|
||
|
exports.CloseCode.SubscriberAlreadyExists,
|
||
|
exports.CloseCode.TooManyInitialisationRequests,
|
||
|
// 4499, // Terminated, probably because the socket froze, we want to retry
|
||
|
].includes(errOrCloseEvent.code)))
|
||
|
throw errOrCloseEvent;
|
||
|
// client was disposed, no retries should proceed regardless
|
||
|
if (disposed)
|
||
|
return false;
|
||
|
// normal closure (possibly all subscriptions have completed)
|
||
|
// if no locks were acquired in the meantime, shouldnt try again
|
||
|
if (isLikeCloseEvent(errOrCloseEvent) && errOrCloseEvent.code === 1000)
|
||
|
return locks > 0;
|
||
|
// retries are not allowed or we tried to many times, report error
|
||
|
if (!retryAttempts || retries >= retryAttempts)
|
||
|
throw errOrCloseEvent;
|
||
|
// throw non-retryable connection problems
|
||
|
if (!shouldRetry(errOrCloseEvent))
|
||
|
throw errOrCloseEvent;
|
||
|
// @deprecated throw fatal connection problems immediately
|
||
|
if (isFatalConnectionProblem === null || isFatalConnectionProblem === void 0 ? void 0 : isFatalConnectionProblem(errOrCloseEvent))
|
||
|
throw errOrCloseEvent;
|
||
|
// looks good, start retrying
|
||
|
return (retrying = true);
|
||
|
}
|
||
|
// in non-lazy (hot?) mode always hold one connection lock to persist the socket
|
||
|
if (!lazy) {
|
||
|
(async () => {
|
||
|
locks++;
|
||
|
for (;;) {
|
||
|
try {
|
||
|
const [, , throwOnClose] = await connect();
|
||
|
await throwOnClose; // will always throw because releaser is not used
|
||
|
}
|
||
|
catch (errOrCloseEvent) {
|
||
|
try {
|
||
|
if (!shouldRetryConnectOrThrow(errOrCloseEvent))
|
||
|
return;
|
||
|
}
|
||
|
catch (errOrCloseEvent) {
|
||
|
// report thrown error, no further retries
|
||
|
return onNonLazyError === null || onNonLazyError === void 0 ? void 0 : onNonLazyError(errOrCloseEvent);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
})();
|
||
|
}
|
||
|
function subscribe(payload, sink) {
|
||
|
const id = generateID(payload);
|
||
|
let done = false, errored = false, releaser = () => {
|
||
|
// for handling completions before connect
|
||
|
locks--;
|
||
|
done = true;
|
||
|
};
|
||
|
(async () => {
|
||
|
locks++;
|
||
|
for (;;) {
|
||
|
try {
|
||
|
const [socket, release, waitForReleaseOrThrowOnClose] = await connect();
|
||
|
// if done while waiting for connect, release the connection lock right away
|
||
|
if (done)
|
||
|
return release();
|
||
|
const unlisten = emitter.onMessage(id, (message) => {
|
||
|
switch (message.type) {
|
||
|
case exports.MessageType.Next: {
|
||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- payload will fit type
|
||
|
sink.next(message.payload);
|
||
|
return;
|
||
|
}
|
||
|
case exports.MessageType.Error: {
|
||
|
(errored = true), (done = true);
|
||
|
sink.error(message.payload);
|
||
|
releaser();
|
||
|
return;
|
||
|
}
|
||
|
case exports.MessageType.Complete: {
|
||
|
done = true;
|
||
|
releaser(); // release completes the sink
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
socket.send(stringifyMessage({
|
||
|
id,
|
||
|
type: exports.MessageType.Subscribe,
|
||
|
payload,
|
||
|
}, replacer));
|
||
|
releaser = () => {
|
||
|
if (!done && socket.readyState === WebSocketImpl.OPEN)
|
||
|
// if not completed already and socket is open, send complete message to server on release
|
||
|
socket.send(stringifyMessage({
|
||
|
id,
|
||
|
type: exports.MessageType.Complete,
|
||
|
}, replacer));
|
||
|
locks--;
|
||
|
done = true;
|
||
|
release();
|
||
|
};
|
||
|
// either the releaser will be called, connection completed and
|
||
|
// the promise resolved or the socket closed and the promise rejected.
|
||
|
// whatever happens though, we want to stop listening for messages
|
||
|
await waitForReleaseOrThrowOnClose.finally(unlisten);
|
||
|
return; // completed, shouldnt try again
|
||
|
}
|
||
|
catch (errOrCloseEvent) {
|
||
|
if (!shouldRetryConnectOrThrow(errOrCloseEvent))
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
})()
|
||
|
.then(() => {
|
||
|
// delivering either an error or a complete terminates the sequence
|
||
|
if (!errored)
|
||
|
sink.complete();
|
||
|
}) // resolves on release or normal closure
|
||
|
.catch((err) => {
|
||
|
sink.error(err);
|
||
|
}); // rejects on close events and errors
|
||
|
return () => {
|
||
|
// dispose only of active subscriptions
|
||
|
if (!done)
|
||
|
releaser();
|
||
|
};
|
||
|
}
|
||
|
return {
|
||
|
on: emitter.on,
|
||
|
subscribe,
|
||
|
iterate(request) {
|
||
|
const pending = [];
|
||
|
const deferred = {
|
||
|
done: false,
|
||
|
error: null,
|
||
|
resolve: () => {
|
||
|
// noop
|
||
|
},
|
||
|
};
|
||
|
const dispose = subscribe(request, {
|
||
|
next(val) {
|
||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
pending.push(val);
|
||
|
deferred.resolve();
|
||
|
},
|
||
|
error(err) {
|
||
|
deferred.done = true;
|
||
|
deferred.error = err;
|
||
|
deferred.resolve();
|
||
|
},
|
||
|
complete() {
|
||
|
deferred.done = true;
|
||
|
deferred.resolve();
|
||
|
},
|
||
|
});
|
||
|
const iterator = (function iterator() {
|
||
|
return __asyncGenerator(this, arguments, function* iterator_1() {
|
||
|
for (;;) {
|
||
|
if (!pending.length) {
|
||
|
// only wait if there are no pending messages available
|
||
|
yield __await(new Promise((resolve) => (deferred.resolve = resolve)));
|
||
|
}
|
||
|
// first flush
|
||
|
while (pending.length) {
|
||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||
|
yield yield __await(pending.shift());
|
||
|
}
|
||
|
// then error
|
||
|
if (deferred.error) {
|
||
|
throw deferred.error;
|
||
|
}
|
||
|
// or complete
|
||
|
if (deferred.done) {
|
||
|
return yield __await(void 0);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
})();
|
||
|
iterator.throw = async (err) => {
|
||
|
if (!deferred.done) {
|
||
|
deferred.done = true;
|
||
|
deferred.error = err;
|
||
|
deferred.resolve();
|
||
|
}
|
||
|
return { done: true, value: undefined };
|
||
|
};
|
||
|
iterator.return = async () => {
|
||
|
dispose();
|
||
|
return { done: true, value: undefined };
|
||
|
};
|
||
|
return iterator;
|
||
|
},
|
||
|
async dispose() {
|
||
|
disposed = true;
|
||
|
if (connecting) {
|
||
|
// if there is a connection, close it
|
||
|
const [socket] = await connecting;
|
||
|
socket.close(1000, 'Normal Closure');
|
||
|
}
|
||
|
},
|
||
|
terminate() {
|
||
|
if (connecting) {
|
||
|
// only if there is a connection
|
||
|
emitter.emit('closed', new TerminatedCloseEvent());
|
||
|
}
|
||
|
},
|
||
|
};
|
||
|
}
|
||
|
/**
|
||
|
* A syntetic close event `4499: Terminated` is issued to the current to immediately
|
||
|
* close the connection without waiting for the one coming from `WebSocket.onclose`.
|
||
|
*
|
||
|
* Terminating is not considered fatal and a connection retry will occur as expected.
|
||
|
*
|
||
|
* Useful in cases where the WebSocket is stuck and not emitting any events;
|
||
|
* can happen on iOS Safari, see: https://github.com/enisdenjo/graphql-ws/discussions/290.
|
||
|
*/
|
||
|
class TerminatedCloseEvent extends Error {
|
||
|
constructor() {
|
||
|
super(...arguments);
|
||
|
this.name = 'TerminatedCloseEvent';
|
||
|
this.message = '4499: Terminated';
|
||
|
this.code = 4499;
|
||
|
this.reason = 'Terminated';
|
||
|
this.wasClean = false;
|
||
|
}
|
||
|
}
|
||
|
function isLikeCloseEvent(val) {
|
||
|
return isObject(val) && 'code' in val && 'reason' in val;
|
||
|
}
|
||
|
function isFatalInternalCloseCode(code) {
|
||
|
if ([
|
||
|
1000,
|
||
|
1001,
|
||
|
1006,
|
||
|
1005,
|
||
|
1012,
|
||
|
1013,
|
||
|
1014, // Bad Gateway
|
||
|
].includes(code))
|
||
|
return false;
|
||
|
// all other internal errors are fatal
|
||
|
return code >= 1000 && code <= 1999;
|
||
|
}
|
||
|
function isWebSocket(val) {
|
||
|
return (typeof val === 'function' &&
|
||
|
'constructor' in val &&
|
||
|
'CLOSED' in val &&
|
||
|
'CLOSING' in val &&
|
||
|
'CONNECTING' in val &&
|
||
|
'OPEN' in val);
|
||
|
}
|
||
|
|
||
|
exports.DEPRECATED_GRAPHQL_WS_PROTOCOL = DEPRECATED_GRAPHQL_WS_PROTOCOL;
|
||
|
exports.GRAPHQL_TRANSPORT_WS_PROTOCOL = GRAPHQL_TRANSPORT_WS_PROTOCOL;
|
||
|
exports.TerminatedCloseEvent = TerminatedCloseEvent;
|
||
|
exports.createClient = createClient;
|
||
|
exports.isMessage = isMessage;
|
||
|
exports.parseMessage = parseMessage;
|
||
|
exports.stringifyMessage = stringifyMessage;
|
||
|
exports.validateMessage = validateMessage;
|
||
|
|
||
|
}));
|