UNPKG

@inworld/web-core

Version:
278 lines (277 loc) 12.2 kB
import { v4 } from 'uuid'; import { version } from '../../package.js'; import { ContinuationContinuationType, ControlEventAction, } from '../../proto/ai/inworld/packets/packets.pb.js'; import { CLIENT_ID } from '../common/constants.js'; import { InworldError } from '../entities/error.entity.js'; const INWORLD_USER_ID = 'inworldUserId'; const SESSION_PATH = '/v1/session/open'; const NORMAL_CLOSURE_CODE = 1000; export class WebSocketConnection { constructor(props) { this.connectionProps = props; this.onMessage = (event) => { const [err, packet] = this.parseEvent(event); if (err) { this.onError(err); } else if (packet) { this.connectionProps.onMessage(packet); } }; this.onError = (err) => { if (err instanceof InworldError) { this.connectionProps.onError(err); } else { this.connectionProps.onError(new InworldError(err.toString())); } }; this.extension = props.extension; } isActive() { var _a; return ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN; } async openSession(props) { var _a; const ws = await this.combineWebSocket(props.session); const finalPackets = this.getPackets({ capabilities: this.connectionProps.config.capabilities, client: props.client, gameSessionId: this.connectionProps.config.gameSessionId, name: props.name, sessionContinuation: props.sessionContinuation, user: props.user, useDefaultClient: !props.client, }); const needHistory = ((_a = this.connectionProps.config.history) === null || _a === void 0 ? void 0 : _a.previousState) && !!finalPackets.find((p) => { var _a, _b; return (_b = (_a = p.control) === null || _a === void 0 ? void 0 : _a.sessionConfiguration) === null || _b === void 0 ? void 0 : _b.continuation; }); const write = this.write.bind({ ws, extension: this.extension, }); ws.addEventListener('open', () => { for (const packet of finalPackets) { write({ getPacket: () => packet }); } this.connectionProps.onReady(); }); this.ws = ws; return new Promise((resolve, reject) => ws.addEventListener('message', this.onLoad({ needHistory, ws, write, resolve, reject, }))); } async reopenSession(session) { const ws = await this.combineWebSocket(session); ws.addEventListener('message', this.onMessage); return new Promise((resolve) => { ws.addEventListener('open', () => { this.ws = ws; this.connectionProps.onReady(); resolve(); }); }); } async updateSession(props) { var _a; const finalPackets = this.getPackets({ capabilities: props.capabilities, gameSessionId: props.gameSessionId, name: props.name, sessionContinuation: props.sessionContinuation, }); const write = this.write.bind({ ws: this.ws, extension: this.extension, }); const needHistory = ((_a = this.connectionProps.config.history) === null || _a === void 0 ? void 0 : _a.previousState) && !!finalPackets.find((p) => { var _a, _b; return (_b = (_a = p.control) === null || _a === void 0 ? void 0 : _a.sessionConfiguration) === null || _b === void 0 ? void 0 : _b.continuation; }); for (const packet of finalPackets) { write({ getPacket: () => packet, afterWriting: () => this.connectionProps.onMessage(packet), }); } if (!props.name) return; this.ws.removeEventListener('message', this.onMessage); return new Promise((resolve, reject) => this.ws.addEventListener('message', this.onLoad({ firstLoad: false, needHistory, ws: this.ws, write, resolve, reject, }))); } async close() { var _a, _b, _c; if (this.isActive()) { this.ws.close(NORMAL_CLOSURE_CODE, 'Client closed the connection'); this.connectionProps.onDisconnect(); } (_a = this.ws) === null || _a === void 0 ? void 0 : _a.removeEventListener('error', this.onError); (_b = this.ws) === null || _b === void 0 ? void 0 : _b.removeEventListener('close', this.connectionProps.onDisconnect); (_c = this.ws) === null || _c === void 0 ? void 0 : _c.removeEventListener('message', this.onMessage); await new Promise((resolve) => { var _a; if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) !== WebSocket.CLOSING) { return resolve(); } const interval = setInterval(() => { if (this.ws) { if (this.ws.readyState === WebSocket.CLOSED) { clearInterval(interval); resolve(); } } else { clearInterval(interval); resolve(); } }); }); this.ws = undefined; } async write(item) { var _a, _b, _c, _d; const originalPacket = item.getPacket(); const packet = (_b = (_a = item.convertPacket) === null || _a === void 0 ? void 0 : _a.call(item, originalPacket)) !== null && _b !== void 0 ? _b : originalPacket; const inworldPacket = this.extension.convertPacketFromProto(packet); await ((_c = item.beforeWriting) === null || _c === void 0 ? void 0 : _c.call(item, inworldPacket)); this.ws.send(JSON.stringify(packet)); (_d = item.afterWriting) === null || _d === void 0 ? void 0 : _d.call(item, inworldPacket); } async combineWebSocket(session) { const { onDisconnect } = this.connectionProps; const { hostname, ssl } = this.connectionProps.config.connection.gateway; const url = `${ssl ? 'wss' : 'ws'}://${hostname}${SESSION_PATH}?session_id=${session.sessionId}`; const ws = new WebSocket(url, [session.type, session.token]); if (onDisconnect) { ws.addEventListener('close', onDisconnect); } ws.addEventListener('error', this.onError); return ws; } parseEvent(event) { let payload; try { payload = JSON.parse(event.data); } catch (_a) { } if (!payload) { return [new InworldError('Invalid JSON received as WS event data')]; } else if (payload.error) { return [InworldError.fromProto(payload.error)]; } if (payload.result) { return [undefined, payload.result]; } } onLoad({ firstLoad = true, needHistory, write, ws, resolve, reject, }) { const { parseEvent, onMessage, connectionProps: { eventFactory }, } = this; let historyLoaded = true; let sceneStatus; return function (event) { var _a, _b, _c; const [err, packet] = parseEvent(event); if (err) { reject(err); } else if ((!sceneStatus && ((_a = packet === null || packet === void 0 ? void 0 : packet.control) === null || _a === void 0 ? void 0 : _a.action) === ControlEventAction.CURRENT_SCENE_STATUS) || (!historyLoaded && ((_b = packet === null || packet === void 0 ? void 0 : packet.sessionControlResponse) === null || _b === void 0 ? void 0 : _b.sessionHistory))) { if (!firstLoad && !sceneStatus && (packet === null || packet === void 0 ? void 0 : packet.control.currentSceneStatus)) { onMessage(event); } const sessionHistory = (_c = packet === null || packet === void 0 ? void 0 : packet.sessionControlResponse) === null || _c === void 0 ? void 0 : _c.sessionHistory; sceneStatus = sceneStatus !== null && sceneStatus !== void 0 ? sceneStatus : packet === null || packet === void 0 ? void 0 : packet.control.currentSceneStatus; historyLoaded = !!sessionHistory || !needHistory; if (!!sceneStatus && !historyLoaded && needHistory) { write({ getPacket: () => eventFactory.sessionControl({ sessionHistory: {} }), }); } else { ws.removeEventListener('message', this); ws.addEventListener('message', onMessage); resolve({ sceneStatus, sessionHistory, }); } } }; } getPackets(props) { var _a, _b; const { eventFactory } = this.connectionProps; const continuation = this.getContinuation({ sessionContinuation: props.sessionContinuation, }); const packets = [ eventFactory.sessionControl(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (props.capabilities && { capabilities: props.capabilities, })), (props.gameSessionId && { sessionConfiguration: { gameSessionId: props.gameSessionId }, })), ((props.client || props.useDefaultClient) && { clientConfiguration: this.getClient({ client: props.client, }), })), (props.user && { userConfiguration: this.getUserConfiguration(props), })), (continuation && { continuation }))), ]; if (props.name) { packets.push(eventFactory.loadScene(props.name)); } return ((_b = (_a = this.extension).beforeLoadScene) === null || _b === void 0 ? void 0 : _b.call(_a, packets)) || packets; } getClient(props) { var _a; const description = [CLIENT_ID, version, navigator.userAgent]; if ((_a = props.client) === null || _a === void 0 ? void 0 : _a.id) { description.push(props.client.id); } return { id: CLIENT_ID, version, description: description.join('; '), }; } getUserConfiguration(props) { const { id, fullName, profile } = props.user || {}; return Object.assign(Object.assign({ id: id ? id : this.getUserId() }, (fullName && { name: fullName })), ((profile === null || profile === void 0 ? void 0 : profile.fields.length) && { userSettings: { playerProfile: { fields: profile.fields.map(({ id: fieldId, value: fieldValue }) => ({ fieldId, fieldValue })), }, }, })); } getUserId() { let id = localStorage.getItem(INWORLD_USER_ID); if (!id) { id = v4(); localStorage.setItem(INWORLD_USER_ID, id); } return id; } getContinuation(props) { const { sessionContinuation } = props; const continuation = Object.assign(Object.assign({}, ((sessionContinuation === null || sessionContinuation === void 0 ? void 0 : sessionContinuation.previousState) && { continuationType: ContinuationContinuationType.CONTINUATION_TYPE_EXTERNALLY_SAVED_STATE, externallySavedState: sessionContinuation.previousState, })), ((sessionContinuation === null || sessionContinuation === void 0 ? void 0 : sessionContinuation.previousDialog) && { continuationType: ContinuationContinuationType.CONTINUATION_TYPE_DIALOG_HISTORY, dialogHistory: sessionContinuation.previousDialog.toProto(), })); return continuation.continuationType ? continuation : undefined; } }