6989a98d75
- Arkitektur: docs/auth/passwordless-architecture.md - Backend: iom/quixzoom-auth-service/ (FastAPI + Redis) - Webb: quixzoom-market-pages/se/login/ (QR-kod + polling) - App: iom/quixzoom-app/src/features/auth/ (push + deep links) Flöde: QR-kod → app-godkännande → webb-inloggad
476 lines
19 KiB
JavaScript
476 lines
19 KiB
JavaScript
"use strict";
|
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.ResponsesWSBase = void 0;
|
|
const internal_base_1 = require("./internal-base.js");
|
|
const EventEmitter_1 = require("../../core/EventEmitter.js");
|
|
const sleep_1 = require("../../internal/utils/sleep.js");
|
|
const ws_adapter_1 = require("../../internal/ws-adapter.js");
|
|
const ws_1 = require("../../internal/ws.js");
|
|
const error_1 = require("../../core/error.js");
|
|
class ResponsesWSBase extends internal_base_1.ResponsesEmitter {
|
|
constructor(client, options) {
|
|
super();
|
|
this._isReconnecting = false;
|
|
this._intentionallyClosed = false;
|
|
this._closeCode = 1000;
|
|
this._closeReason = 'OK';
|
|
this._lastCloseCode = 1006;
|
|
this._lastCloseReason = '';
|
|
// Necessary to keep the public event interface clean while we manage reconnecting
|
|
this._internalEvents = new EventEmitter_1.InternalEventEmitter();
|
|
this._client = client;
|
|
this._parameters = undefined;
|
|
this._reconnectOptions = options?.reconnect ?? null;
|
|
this._sendQueue = new ws_1.SendQueue(options?.maxQueueSize);
|
|
}
|
|
/** Establishes the initial WebSocket connection. */
|
|
_connectInitial() {
|
|
this.url = (0, internal_base_1.buildURL)(this._client, {});
|
|
this.socket = this._connect();
|
|
}
|
|
send(event) {
|
|
if (!this.socket) {
|
|
throw new error_1.OpenAIError('Internal error: failed to initialize socket. Please report this issue.');
|
|
}
|
|
if (this._isReconnecting || this.socket.readyState === ws_adapter_1.ReadyState.CONNECTING) {
|
|
if (!this._sendQueue.enqueue(event)) {
|
|
this._onError(null, 'send queue is full, message discarded', undefined);
|
|
}
|
|
return;
|
|
}
|
|
if (this.socket.readyState !== ws_adapter_1.ReadyState.OPEN) {
|
|
this._onError(null, 'cannot send on a closed WebSocket', undefined);
|
|
return;
|
|
}
|
|
try {
|
|
this.socket.send(JSON.stringify(event));
|
|
}
|
|
catch (err) {
|
|
this._onError(null, 'could not send data', err);
|
|
}
|
|
}
|
|
sendRaw(data) {
|
|
if (!this.socket) {
|
|
throw new error_1.OpenAIError('Internal error: failed to initialize socket. Please report this issue.');
|
|
}
|
|
if (this._isReconnecting || this.socket.readyState === ws_adapter_1.ReadyState.CONNECTING) {
|
|
if (!this._sendQueue.enqueueRaw(data)) {
|
|
this._onError(null, 'send queue is full, message discarded', undefined);
|
|
}
|
|
return;
|
|
}
|
|
if (this.socket.readyState !== ws_adapter_1.ReadyState.OPEN) {
|
|
this._onError(null, 'cannot send on a closed WebSocket', undefined);
|
|
return;
|
|
}
|
|
try {
|
|
this.socket.send((0, ws_1.flattenRawData)(data));
|
|
}
|
|
catch (err) {
|
|
this._onError(null, 'could not send data', err);
|
|
}
|
|
}
|
|
close(props) {
|
|
if (!this.socket) {
|
|
throw new error_1.OpenAIError('Internal error: failed to initialize socket. Please report this issue.');
|
|
}
|
|
this._intentionallyClosed = true;
|
|
this._closeCode = props?.code ?? 1000;
|
|
this._closeReason = props?.reason ?? 'OK';
|
|
try {
|
|
this.socket.close(this._closeCode, this._closeReason);
|
|
}
|
|
catch (err) {
|
|
this._onError(null, 'could not close the connection', err);
|
|
}
|
|
}
|
|
/**
|
|
* Returns an async iterator over WebSocket lifecycle and message events,
|
|
* providing an alternative to the event-based `.on()` API.
|
|
* The iterator will exit if the socket closes but exiting the iterator
|
|
* does not close the socket.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* for await (const event of client.stream()) {
|
|
* switch (event.type) {
|
|
* case 'message':
|
|
* console.log('received:', event.message);
|
|
* break;
|
|
* case 'error':
|
|
* console.error(event.error);
|
|
* break;
|
|
* case 'close':
|
|
* console.log('connection closed');
|
|
* break;
|
|
* }
|
|
* }
|
|
* ```
|
|
*/
|
|
stream() {
|
|
return this[Symbol.asyncIterator]();
|
|
}
|
|
[Symbol.asyncIterator]() {
|
|
if (!this.socket) {
|
|
throw new error_1.OpenAIError('Internal error: failed to initialize socket. Please report this issue.');
|
|
}
|
|
// Two-queue async iterator: `queue` buffers incoming messages,
|
|
// `resolvers` buffers waiting next() calls. A push wakes the
|
|
// oldest next(); a next() drains the oldest message.
|
|
const queue = [];
|
|
const resolvers = [];
|
|
let done = false;
|
|
let currentSocket = this.socket;
|
|
const push = (msg) => {
|
|
queue.push(msg);
|
|
resolvers.shift()?.();
|
|
};
|
|
const onEvent = (event) => {
|
|
if (event.type === 'error')
|
|
return; // handled by onEmitterError
|
|
push({ type: 'message', message: event });
|
|
};
|
|
const onRaw = (data) => {
|
|
push({ type: 'raw', data });
|
|
};
|
|
// All errors (API + socket) funnel through _onError → 'error' event
|
|
const onEmitterError = (err) => {
|
|
push({ type: 'error', error: err });
|
|
};
|
|
const onOpen = () => {
|
|
push({ type: 'open' });
|
|
};
|
|
const onReconnecting = (evt) => {
|
|
push({ type: 'reconnecting', reconnect: evt });
|
|
};
|
|
const onReconnected = () => {
|
|
push({ type: 'reconnected' });
|
|
};
|
|
const flushResolvers = () => {
|
|
for (let resolver = resolvers.shift(); resolver; resolver = resolvers.shift()) {
|
|
resolver();
|
|
}
|
|
};
|
|
const onClose = (code, reason, unsent) => {
|
|
push({ type: 'close', code, reason, unsent });
|
|
done = true;
|
|
flushResolvers();
|
|
cleanup();
|
|
};
|
|
const onSocketSwap = (oldSocket, newSocket) => {
|
|
oldSocket.off('open', onOpen);
|
|
newSocket.on('open', onOpen);
|
|
currentSocket = newSocket;
|
|
};
|
|
const cleanup = () => {
|
|
this.off('event', onEvent);
|
|
this.off('raw', onRaw);
|
|
this.off('error', onEmitterError);
|
|
currentSocket.off('open', onOpen);
|
|
this._internalEvents.off('close', onClose);
|
|
this._internalEvents.off('socketSwap', onSocketSwap);
|
|
this._internalEvents.off('reconnecting', onReconnecting);
|
|
this._internalEvents.off('reconnected', onReconnected);
|
|
};
|
|
this.on('event', onEvent);
|
|
this.on('raw', onRaw);
|
|
this.on('error', onEmitterError);
|
|
this.socket.on('open', onOpen);
|
|
this._internalEvents.on('close', onClose);
|
|
this._internalEvents.on('socketSwap', onSocketSwap);
|
|
this._internalEvents.on('reconnecting', onReconnecting);
|
|
this._internalEvents.on('reconnected', onReconnected);
|
|
if (this._isReconnecting) {
|
|
// A reconnect is already in flight. The socket may be CLOSED but the
|
|
// instance is still alive. Emit 'reconnecting' so the iterator stays
|
|
// open and receives the upcoming reconnected/message events.
|
|
push({
|
|
type: 'reconnecting',
|
|
reconnect: { attempt: 0, maxAttempts: 0, delay: 0, closeCode: 0, parameters: undefined },
|
|
});
|
|
}
|
|
else {
|
|
switch (this.socket.readyState) {
|
|
case ws_adapter_1.ReadyState.CONNECTING:
|
|
push({ type: 'connecting' });
|
|
break;
|
|
case ws_adapter_1.ReadyState.OPEN:
|
|
push({ type: 'open' });
|
|
break;
|
|
case ws_adapter_1.ReadyState.CLOSING:
|
|
push({ type: 'closing' });
|
|
break;
|
|
case ws_adapter_1.ReadyState.CLOSED:
|
|
push({
|
|
type: 'close',
|
|
code: this._lastCloseCode,
|
|
reason: this._lastCloseReason,
|
|
unsent: this._sendQueue.drain(),
|
|
});
|
|
done = true;
|
|
cleanup();
|
|
break;
|
|
}
|
|
}
|
|
const resolve = (res) => {
|
|
if (queue.length > 0) {
|
|
res({ value: queue.shift(), done: false });
|
|
}
|
|
else if (done) {
|
|
res({ value: undefined, done: true });
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
const next = () => new Promise((res) => {
|
|
if (resolve(res))
|
|
return;
|
|
resolvers.push(() => {
|
|
resolve(res);
|
|
});
|
|
});
|
|
return {
|
|
next,
|
|
return: () => {
|
|
done = true;
|
|
cleanup();
|
|
flushResolvers();
|
|
return Promise.resolve({ value: undefined, done: true });
|
|
},
|
|
[Symbol.asyncIterator]() {
|
|
return this;
|
|
},
|
|
};
|
|
}
|
|
_connect() {
|
|
this.url = (0, internal_base_1.buildURL)(this._client, this._parameters ?? {});
|
|
const socket = this._createSocket(this.url, this._authHeaders());
|
|
socket.on('message', (data, isBinary) => {
|
|
if (isBinary) {
|
|
this._emit('raw', data);
|
|
return;
|
|
}
|
|
// Coerce to string in case the adapter delivers a typed-array for text frames.
|
|
const text = typeof data === 'string' ? data : String(data);
|
|
let event;
|
|
try {
|
|
event = JSON.parse(text);
|
|
}
|
|
catch {
|
|
this._emit('raw', data);
|
|
return;
|
|
}
|
|
this._emit('event', event);
|
|
if (event.type === 'error') {
|
|
this._onError(event);
|
|
}
|
|
else {
|
|
// @ts-ignore TS isn't smart enough to get the relationship right here
|
|
this._emit(event.type, event);
|
|
}
|
|
});
|
|
socket.on('error', (err) => {
|
|
// Suppress transient errors during reconnection — the retry loop
|
|
// already handles them and will surface a close if retries exhaust.
|
|
if (this._isReconnecting)
|
|
return;
|
|
this._onError(null, err.message, err);
|
|
});
|
|
socket.on('open', () => {
|
|
this._flushSendQueue();
|
|
});
|
|
socket.on('close', (code, reason) => {
|
|
// Ignore close events from superseded sockets — a stale socket's
|
|
// late close must not kick off a second reconnect loop.
|
|
if (socket !== this.socket)
|
|
return;
|
|
if (!this._intentionallyClosed && this._canReconnect(code)) {
|
|
this._reconnect(code);
|
|
}
|
|
else if (!this._isReconnecting) {
|
|
this._emitPermanentClose(code, reason);
|
|
}
|
|
});
|
|
return socket;
|
|
}
|
|
// Reconnect is opt-in via onReconnecting so callers can pass
|
|
// state (e.g. session IDs) into the new connection.
|
|
_canReconnect(code) {
|
|
if (this._intentionallyClosed)
|
|
return false;
|
|
if (!this._reconnectOptions)
|
|
return false;
|
|
if (this._reconnectOptions.maxRetries === 0)
|
|
return false;
|
|
if (!this._reconnectOptions.onReconnecting)
|
|
return false;
|
|
return (0, ws_1.isRecoverableClose)(code);
|
|
}
|
|
async _reconnect(closeCode) {
|
|
if (!this.socket) {
|
|
throw new error_1.OpenAIError('Internal error: failed to initialize socket. Please report this issue.');
|
|
}
|
|
if (this._isReconnecting || !this._reconnectOptions)
|
|
return;
|
|
this._isReconnecting = true;
|
|
const maxRetries = this._reconnectOptions.maxRetries ?? 5;
|
|
const initialDelay = this._reconnectOptions.initialDelay ?? 500;
|
|
const maxDelay = this._reconnectOptions.maxDelay ?? 8000;
|
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
if (!this._canReconnect(closeCode)) {
|
|
this._isReconnecting = false;
|
|
if (!this._intentionallyClosed) {
|
|
this._onError(null, `WebSocket reconnect aborted: non-recoverable close code ${closeCode}`, undefined);
|
|
}
|
|
this._emitPermanentClose(this._intentionallyClosed ? this._closeCode : closeCode, this._intentionallyClosed ? this._closeReason : 'reconnect aborted');
|
|
return;
|
|
}
|
|
const baseDelay = Math.min(initialDelay * Math.pow(2, attempt - 1), maxDelay);
|
|
// Jitter: rand [0.75, 1.0] to spread out connection attempts without over-delaying
|
|
const jitter = 0.75 + Math.random() * 0.25;
|
|
const actualDelay = Math.round(baseDelay * jitter);
|
|
let reconnectingEvent = {
|
|
attempt,
|
|
maxAttempts: maxRetries,
|
|
delay: actualDelay,
|
|
closeCode,
|
|
parameters: this._parameters ? { ...this._parameters } : undefined,
|
|
};
|
|
let overrides;
|
|
try {
|
|
overrides = this._reconnectOptions.onReconnecting(reconnectingEvent);
|
|
}
|
|
catch (err) {
|
|
this._isReconnecting = false;
|
|
this._onError(null, 'onReconnecting callback threw', err);
|
|
this._emitPermanentClose(closeCode, 'onReconnecting callback threw');
|
|
return;
|
|
}
|
|
if (overrides && 'abort' in overrides && overrides.abort) {
|
|
this._isReconnecting = false;
|
|
this._emitPermanentClose(closeCode, 'reconnect aborted by handler');
|
|
return;
|
|
}
|
|
if (overrides && 'parameters' in overrides) {
|
|
this._parameters = overrides.parameters;
|
|
reconnectingEvent = { ...reconnectingEvent, parameters: this._parameters };
|
|
}
|
|
try {
|
|
this._emit('reconnecting', reconnectingEvent);
|
|
}
|
|
catch (err) {
|
|
this._onError(null, 'onReconnecting callback threw', err);
|
|
}
|
|
this._internalEvents._emit('reconnecting', reconnectingEvent);
|
|
if (!this._canReconnect(closeCode)) {
|
|
this._isReconnecting = false;
|
|
if (!this._intentionallyClosed) {
|
|
this._onError(null, `WebSocket reconnect aborted: non-recoverable close code ${closeCode}`, undefined);
|
|
}
|
|
this._emitPermanentClose(this._intentionallyClosed ? this._closeCode : closeCode, this._intentionallyClosed ? this._closeReason : 'reconnect aborted');
|
|
return;
|
|
}
|
|
await (0, sleep_1.sleep)(actualDelay);
|
|
if (!this._canReconnect(closeCode)) {
|
|
this._isReconnecting = false;
|
|
if (!this._intentionallyClosed) {
|
|
this._onError(null, `WebSocket reconnect aborted: non-recoverable close code ${closeCode}`, undefined);
|
|
}
|
|
this._emitPermanentClose(this._intentionallyClosed ? this._closeCode : closeCode, this._intentionallyClosed ? this._closeReason : 'reconnect aborted');
|
|
return;
|
|
}
|
|
let closeCodePromise;
|
|
try {
|
|
const oldSocket = this.socket;
|
|
this.socket = this._connect();
|
|
// Registered synchronously after _connect() and before any
|
|
// await so the code is captured even when ws emits 'close'
|
|
// in the same tick as 'error' (e.g. abortHandshake).
|
|
closeCodePromise = new Promise((resolve) => {
|
|
this.socket.once('close', resolve);
|
|
});
|
|
await this._awaitOpen(this.socket);
|
|
this._internalEvents._emit('socketSwap', oldSocket, this.socket);
|
|
this._isReconnecting = false;
|
|
this._flushSendQueue();
|
|
this._emit('reconnected');
|
|
this._internalEvents._emit('reconnected');
|
|
return;
|
|
}
|
|
catch {
|
|
if (closeCodePromise) {
|
|
// ws may emit 'error' before 'close', so await the code
|
|
// rather than reading it synchronously.
|
|
closeCode = await closeCodePromise;
|
|
}
|
|
}
|
|
}
|
|
// All retries exhausted — surface an error so consumers can
|
|
// distinguish retry failure from a clean close.
|
|
this._isReconnecting = false;
|
|
this._onError(null, `WebSocket reconnect failed after ${maxRetries} attempts (close code: ${closeCode})`, undefined);
|
|
this._emitPermanentClose(closeCode, `reconnect failed after ${maxRetries} attempts`);
|
|
}
|
|
/**
|
|
* Resolves once the socket is open, rejects if it errors or closes first
|
|
*/
|
|
_awaitOpen(socket) {
|
|
return new Promise((resolve, reject) => {
|
|
const cleanup = () => {
|
|
socket.off('open', onOpen);
|
|
socket.off('error', onError);
|
|
socket.off('close', onFail);
|
|
};
|
|
const onOpen = () => {
|
|
cleanup();
|
|
resolve();
|
|
};
|
|
const onError = (err) => {
|
|
cleanup();
|
|
reject(err);
|
|
};
|
|
const onFail = () => {
|
|
cleanup();
|
|
reject(new Error('socket closed before open'));
|
|
};
|
|
socket.once('open', onOpen);
|
|
socket.once('error', onError);
|
|
socket.once('close', onFail);
|
|
});
|
|
}
|
|
_flushSendQueue() {
|
|
if (!this.socket) {
|
|
throw new error_1.OpenAIError('Internal error: failed to initialize socket. Please report this issue.');
|
|
}
|
|
try {
|
|
this._sendQueue.flush((data) => this.socket.send((0, ws_1.flattenRawData)(data)));
|
|
}
|
|
catch (err) {
|
|
this._onError(null, 'could not send queued data', err);
|
|
}
|
|
}
|
|
/**
|
|
* Emits the public `close` event with unsent messages and the internal
|
|
* `close` event used by the async iterator.
|
|
*/
|
|
_emitPermanentClose(code, reason) {
|
|
this._lastCloseCode = code;
|
|
this._lastCloseReason = reason;
|
|
const unsent = this._sendQueue.drain();
|
|
// Internal close fires first so the async iterator is guaranteed to
|
|
// terminate even if a public 'close' listener throws.
|
|
this._internalEvents._emit('close', code, reason, unsent);
|
|
this._emit('close', code, reason, unsent);
|
|
}
|
|
_authHeaders() {
|
|
if (this._client.apiKey) {
|
|
return { Authorization: `Bearer ${this._client.apiKey}` };
|
|
}
|
|
return {};
|
|
}
|
|
}
|
|
exports.ResponsesWSBase = ResponsesWSBase;
|
|
//# sourceMappingURL=ws-base.js.map
|