@inworld/web-core
Version:
278 lines (277 loc) • 12.2 kB
JavaScript
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;
}
}