{"version":3,"file":"daf9207f743089cc34d1.chunk.js","mappings":"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","sources":["webpack://ch.enlightware.gamecreator/./src/apps/common/browser-app.ts","webpack://ch.enlightware.gamecreator/./src/network/api-connect.ts","webpack://ch.enlightware.gamecreator/./src/utils/async.ts","webpack://ch.enlightware.gamecreator/./src/utils/heartbeat.ts"],"sourcesContent":["/**\n * Utils for browser based apps.\n * @module utils\n * @preferred\n */\n/** comment to work-around limitation of typedoc module plugin */\n\n// Copyright 2018-2024 Enlightware GmbH, Switzerland\n\nimport { connect } from 'network/api-connect';\nimport { RustServerDb } from 'storage/rust-db';\nimport { setDevMode } from 'utils/assert';\nimport { deserialiseMap, serialiseMap } from 'utils/serialization';\nimport { showVersion } from './version';\nimport { Analytics } from './analytics';\nimport { Features, Parameters } from './parameters';\nimport { getWorkspaceId, getDeviceWorkspaceId, removeDeviceWorkspaceIdFromLocalStorage } from './workspace';\nimport { LoginManager } from './login-manager';\nimport { dynamicCast, nonnull } from 'utils/types';\nimport { getChildElementByClass } from 'utils/html';\nimport { qsT } from 'translator/translator';\nimport { UserIdLocalStorageKey } from './local-storage-keys';\nimport { RustDbFbsReadOnlyStorage, RustDbFbsStorage } from 'storage/rust-db-storage';\nimport { setStorage } from 'storage/storage';\nimport { LoggedInUserInfo } from 'network/api-types';\n\nsetDevMode(Features.devMode);\n\nfunction showUsernameInDrawer(displayName: string | null) {\n\t// drawer login information\n\tconst drawerLogin = dynamicCast(HTMLAnchorElement, document.getElementById('mainmenu-login'));\n\tif (drawerLogin === null) {\n\t\t// no drawer, return\n\t\treturn;\n\t}\n\tconst drawerName = getChildElementByClass(drawerLogin, 'mdc-list-item__text');\n\tif (displayName !== null) {\n\t\t// we are logged in\n\t\tdrawerName.textContent = displayName;\n\t\t// are we paid version?\n\t\tif (Features.paid) {\n\t\t\tnonnull(document.getElementById('mainmenu-login__badge')).style.display = '';\n\t\t}\n\t} else {\n\t\t// we are not logged in\n\t\tdrawerName.textContent = qsT('Login');\n\t}\n}\n\nconst UserIdToCurrentGameKey = 'gc-user-idToCurrentGame';\nlet userToCurrentGameMap = new Map();\nif (localStorage) {\n\tconst userToCurrentGameMapSerialized = localStorage.getItem(UserIdToCurrentGameKey);\n\tif (userToCurrentGameMapSerialized !== null) {\n\t\tuserToCurrentGameMap = deserialiseMap(userToCurrentGameMapSerialized);\n\t}\n}\n\nexport function getRequestedGameId() {\n\treturn Parameters.gameIdFromUrl?.substring(0, 32) || userToCurrentGameMap.get(getWorkspaceId()) || null;\n}\nexport function saveCurrentGame(gameKey: string | null) {\n\tconst ws = getWorkspaceId();\n\tif (gameKey === null) {\n\t\tuserToCurrentGameMap.delete(ws);\n\t} else {\n\t\tuserToCurrentGameMap.set(ws, gameKey);\n\t}\n\tif (localStorage) {\n\t\tlocalStorage.setItem(UserIdToCurrentGameKey, serialiseMap(userToCurrentGameMap));\n\t}\n}\n\nexport function displayUserName(name: string | null) {\n\treturn name ?? qsT('anonymous');\n}\n\nexport function displayName(userInfoAndWorkspace: LoggedInUserInfo | null) {\n\treturn userInfoAndWorkspace !== null ?\n\t\tdisplayUserName(userInfoAndWorkspace.userInfo.name ?? null) :\n\t\tnull\n\t;\n}\n\nasync function connectToServer(doLogin: boolean) {\n\tconst deviceWorkspace = doLogin ? getDeviceWorkspaceId() : null;\n\tconst loginManager = doLogin ? LoginManager.getInstance() : null;\n\tconst dissolveDws = deviceWorkspace !== null && loginManager !== null;\n\tconst connectResult = await connect(deviceWorkspace, loginManager, dissolveDws);\n\tconst serverRevision = connectResult.server.revision;\n\tconsole.info(`Connected to the server ${serverRevision}.`);\n\tshowVersion(`server ${serverRevision}`);\n\tAnalytics.serverVersion = serverRevision;\n\treturn connectResult;\n}\n\nfunction handleLoggedInUser(userInfoAndWorkspace: LoggedInUserInfo | null) {\n\tconst userId = userInfoAndWorkspace?.userInfo.uuid ?? 'null';\n\tlocalStorage.setItem(UserIdLocalStorageKey, userId);\n\tshowUsernameInDrawer(displayName(userInfoAndWorkspace));\n\tif (userInfoAndWorkspace) {\n\t\tconsole.info('Received user info with workspace:', userInfoAndWorkspace);\n\t\tif (userInfoAndWorkspace.dWsOwned) {\n\t\t\t// If the device's anonymous workspace (remembered in local storage) belongs already to some user we must forget it (and next time create a new one).\n\t\t\tconsole.warn(`Devices workspace ${getDeviceWorkspaceId()} is owned. 'Going to create a fresh one later.`)\n\t\t\tremoveDeviceWorkspaceIdFromLocalStorage();\n\t\t}\n\t} else {\n\t\tconsole.info('Received no user info. Assuming anonymous session.');\n\t}\n}\n\nexport async function connectAndCreateRustDbReadOnlyStorage(doLogin = false) {\n\tawait connectToServer(doLogin);\n\tconst db = new RustServerDb();\n\tconst storage = new RustDbFbsReadOnlyStorage(db);\n\tsetStorage(storage);\n\treturn storage;\n}\n\nexport async function connectAndCreateRustDbStorage() {\n\tconst connectResult = await connectToServer(true);\n\thandleLoggedInUser(connectResult.user);\n\tconst db = new RustServerDb();\n\tconst storage = new RustDbFbsStorage(db);\n\tsetStorage(storage);\n\treturn storage;\n}\n","/**\n * @module utils\n */\n/** comment to work-around limitation of typedoc module plugin */\n\n// Copyright 2018-2024 Enlightware GmbH, Switzerland\n\nimport { setLoggedInUserInfo } from 'apps/common/parameters';\nimport { getWorkspaceId, isFirstUse } from 'apps/common/workspace';\nimport { Login, LoginManager, rememberMe } from 'apps/common/login-manager';\nimport { Analytics, reportLog, reportScreenSize, reportWarning } from 'apps/common/analytics';\nimport { isBrowserOk } from 'apps/common/browser-check';\nimport { getLanguage } from 'translator/translator';\nimport { fetchJson, getApiBaseUrl, migrateAttributesToServer, ServerInfo } from './api';\nimport { LoggedInUserInfo } from './api-types';\nimport { OrganisationMembership } from './api-organisation';\nimport { reportError } from 'apps/common/analytics';\nimport { CodeVersion } from 'apps/common/version';\nimport { sleep } from 'utils/async';\nimport { reloadIfCurrentCodeVersionIsUnexpected, ReloadIfWrongCodeVersionResult, TargetCodeVersionForThisBranchKey } from 'utils/heartbeat';\nimport { isRunningInJest } from 'utils/types';\n\nexport interface ConnectResult {\n\tserver: ServerInfo;\n\tuser: LoggedInUserInfo | null;\n\tremoveLogin: boolean; // The secret was invalid / outdated / wrong user and should be removed.\n\tnewLogin: Login | null; // if the login needs upgrade the server sends the new value right away.\n\texpectedFrontendRevision: string | null;\n}\n\n// We store the connect parameters for a later reconnect.\nlet deviceWorkspaceUsedForConnect: string | null = null;\nlet loginManagerUsedForConnect: LoginManager | null = null;\nlet dissolveDeviceWorkspaceUsedForConnect = false;\n\n\nexport async function connect(deviceWorkspace: string | null = null, loginManager: LoginManager | null = null, dissolveDeviceWorkspace = false, currentCodeVersion = CodeVersion) {\n\tdeviceWorkspaceUsedForConnect = deviceWorkspace;\n\tloginManagerUsedForConnect = loginManager;\n\tdissolveDeviceWorkspaceUsedForConnect = dissolveDeviceWorkspace;\n\tconst login = loginManager?.getLogin() ?? null;\n\tconst json = await fetchJson(\n\t\tgetApiBaseUrl() + '/v0/connect',\n\t\t'POST',\n\t\t{\n\t\t\tdws: deviceWorkspace ? decodeURIComponent(deviceWorkspace) : null,\n\t\t\tdissolveDws: dissolveDeviceWorkspace,\n\t\t\tlogin,\n\t\t\trememberMe: rememberMe(),\n\t\t\tlang: getLanguage()\n\t\t}\n\t);\n\tconst connectResult = json as ConnectResult;\n\n\tif (connectResult.expectedFrontendRevision) {\n\t\twindow.localStorage.setItem(TargetCodeVersionForThisBranchKey, connectResult.expectedFrontendRevision);\n\t}\n\tif (reloadIfCurrentCodeVersionIsUnexpected(connectResult.expectedFrontendRevision, currentCodeVersion) === ReloadIfWrongCodeVersionResult.Initiated) {\n\t\tif (!isRunningInJest()) {\n\t\t\tawait sleep(60);\n\t\t}\n\t\tthrow Error('Attempted to reload because of frontend version mismatch but reload never happened!');\n\t}\n\tconsole.info(`Frontend version '${CodeVersion}' matches version expected by the server.`);\n\n\tif (connectResult.user) {\n\t\tconnectResult.user.attributes = new Map(connectResult.user.attributes);\n\t\tconst organisations = (connectResult.user.userInfo as any).organisations as readonly OrganisationMembership[];\n\t\tif (organisations.length > 1) {\n\t\t\treportError('unsupportedMultipleOrganisations', 'Multiple organisations not supported in the front-end.');\n\t\t}\n\t\tconnectResult.user.userInfo.organisationMembership = organisations[0] ?? null; // Over the wire an array of memberships is sent.\n\t\t// compute expiration dates\n\t\tfor (const product of connectResult.user.userInfo.products) {\n\t\t\tif (product.expiresInSecs !== null) {\n\t\t\t\tproduct.expirationDate = new Date(Date.now() + product.expiresInSecs * 1000);\n\t\t\t}\n\t\t}\n\t}\n\tif (loginManager) {\n\t\tif (connectResult.newLogin) {\n\t\t\tconsole.debug(`Server is providing a new login ${connectResult.newLogin.id}. Storing it.`);\n\t\t\tloginManager.setLogin(connectResult.newLogin);\n\t\t} else if (connectResult.removeLogin) {\n\t\t\tconsole.debug('Server is asking to remove the secret. Doing so.');\n\t\t\tloginManager.removeLogin();\n\t\t}\n\t\tsetLoggedInUserInfo(connectResult.user);\n\t\tawait migrateAttributesToServer();\n\t}\n\n\treportLog('connect', {\n\t\tres: connectResult,\n\t\tcws: getWorkspaceId(),\n\t\tbs: isBrowserOk(),\n\t\tapp: Analytics.appInfo,\n\t\turl: window.location.href,\n\t\tlang: getLanguage(),\n\t\tfirst: isFirstUse\n\t});\n\treportScreenSize();\n\treturn connectResult;\n}\n\nexport async function reconnect() {\n\treturn connect(deviceWorkspaceUsedForConnect, loginManagerUsedForConnect, dissolveDeviceWorkspaceUsedForConnect);\n}\n","/**\n * @module utils\n */\n/** comment to work-around limitation of typedoc module plugin */\n\n// Copyright 2018-2024 Enlightware GmbH, Switzerland\n\nexport async function sleep(durationSeconds: number) {\n\treturn new Promise((resolve) => { setTimeout(resolve, durationSeconds * 1000); });\n}\n\nexport function promiseAny(promises: PromiseLike[]): PromiseLike {\n\treturn Promise.all(\n\t\tpromises.map((promise) =>\n\t\t\tpromise.then((val) => {\n\t\t\t\t// eslint-disable-next-line @typescript-eslint/only-throw-error\n\t\t\t\tthrow val;\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-return\n\t\t\t}, (reason) => reason)\n\t\t)\n\t).then((reasons) => {\n\t\t// eslint-disable-next-line @typescript-eslint/only-throw-error\n\t\tthrow reasons;\n\t// eslint-disable-next-line @typescript-eslint/no-unsafe-return\n\t}, (firstResolved) => firstResolved);\n}\n\nexport async function asyncFilter(arr: T[], predicate: (_: T) => PromiseLike): Promise {\n\tconst results = await Promise.all(arr.map(predicate));\n\treturn arr.filter((_v, index) => results[index]);\n}\n\ntype CallbackObject = {\n\t[K in CN]: null | ((_: CA) => void);\n};\n\ninterface CancellableCallbackPromiseInterface extends PromiseLike {\n\treadonly identifier: string;\n\tcancelCallback(): void;\n}\n\n// Note: due to a bug in Typescript, if argument callbackHolder is before testFn, compilation fails.\n// Note: the executor is called twice, see\n// https://stackoverflow.com/questions/53146565/constructor-of-a-custom-promise-class-is-called-twice-extending-standard-promis\n// and https://v8.dev/blog/fast-async#await-under-the-hood\nexport class CancellableCallbackPromise\n\textends Promise\n\timplements CancellableCallbackPromiseInterface {\n\tconstructor(\n\t\tpublic identifier: string,\n\t\ttestFn: (arg: CA) => boolean,\n\t\tprivate callbackHolder: CallbackObject,\n\t\tprivate callbackName: CN\n\t) {\n\t\tsuper(\n\t\t\t(resolve, _) => {\n\t\t\t\tcallbackHolder[callbackName] = (arg: CA) => {\n\t\t\t\t\tif (testFn(arg)) {\n\t\t\t\t\t\tcallbackHolder[callbackName] = null;\n\t\t\t\t\t\tconsole.debug(`CancellableCallbackPromise: Resolving ${identifier}`);\n\t\t\t\t\t\tresolve(identifier);\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t}\n\t\t);\n\t\tconsole.debug(`CancellableCallbackPromise: Creating ${identifier}`);\n\t}\n\tcancelCallback() {\n\t\tthis.callbackHolder[this.callbackName] = null;\n\t\tconsole.debug(`CancellableCallbackPromise: Cancelling ${this.identifier}`);\n\t}\n}\n// HACK to work around the way await deals with promise creation (see above)\nCancellableCallbackPromise.prototype.constructor = Promise;\n\n// we have to do this because keyof GlobalEventHandlers is too hard to parse for Typescript :-(\ntype CallbackAbleEvents = 'onclick';\n\nexport class CancellableEventPromise\n\textends Promise\n\timplements CancellableCallbackPromiseInterface {\n\tconstructor(\n\t\tpublic identifier: string,\n\t\tprivate target: HTMLElement,\n\t\tprivate eventName: K\n\t) {\n\t\tsuper(\n\t\t\t(resolve, _) => {\n\t\t\t\ttarget[eventName] = () => {\n\t\t\t\t\ttarget[eventName] = null;\n\t\t\t\t\tconsole.debug(`CancellableEventPromise: Resolving ${identifier}`);\n\t\t\t\t\tresolve(identifier);\n\t\t\t\t};\n\t\t\t}\n\t\t);\n\t\tconsole.debug(`CancellableEventPromise: Creating ${identifier}`);\n\t}\n\tcancelCallback() {\n\t\tthis.target[this.eventName] = null;\n\t\tconsole.debug(`CancellableEventPromise: Cancelling ${this.identifier}`);\n\t}\n}\n// HACK to work around the way await deals with promise creation (see above)\nCancellableEventPromise.prototype.constructor = Promise;\n\nexport async function waitAny(promises: CancellableCallbackPromiseInterface[]) {\n\tconsole.debug(`CancellableCallbackPromise: Waiting on ${promises.map((c) => c.identifier).join(', ')}`);\n\tconst result = await promiseAny(promises);\n\tfor (const promise of promises) {\n\t\tif (promise.identifier !== result) {\n\t\t\tpromise.cancelCallback();\n\t\t}\n\t}\n\tconsole.debug(`CancellableCallbackPromise: Done waiting on ${promises.map((c) => c.identifier).join(', ')}`);\n\treturn result;\n}\n\nexport async function domEvent(event: K, element: HTMLElement) {\n\treturn new Promise(\n\t\t(resolve, _) => {\n\t\t\telement.addEventListener(event.substring(2), () => resolve());\n\t\t}\n\t);\n}\n\nexport function constantPromise(value: T): Promise {\n\t// eslint-disable-next-line @typescript-eslint/require-await\n\treturn (async () => value)();\n}\n\nexport function makeSync(f: () => Promise) {\n\treturn () => { void f() };\n}\n\nexport function awaitClick(element: HTMLElement, onclick = (_pos: [number, number]) => {}): Promise<[number, number]> {\n\treturn new Promise<[number, number]>((resolve, _) => {\n\t\telement.onclick = (e: MouseEvent) => {\n\t\t\telement.onclick = null;\n\t\t\tonclick([e.clientX, e.clientY]);\n\t\t\tresolve([e.clientX, e.clientY]);\n\t\t}\n\t});\n}\n\nexport function readFileAsync(file: File): Promise {\n\treturn new Promise((resolve, reject) => {\n\t\tconst reader = new FileReader();\n\n\t\treader.onload = () => {\n\t\t\tresolve(reader.result);\n\t\t};\n\n\t\treader.onerror = reject;\n\n\t\treader.readAsArrayBuffer(file);\n\t})\n}\n\nexport function readFileAsTextAsync(file: File): Promise {\n\treturn new Promise((resolve, reject) => {\n\t\tconst reader = new FileReader();\n\n\t\treader.onload = () => {\n\t\t\tresolve(reader.result);\n\t\t};\n\n\t\treader.onerror = reject;\n\n\t\treader.readAsText(file);\n\t})\n}\n\nexport function readFileAsDataUrlAsync(file: File): Promise {\n\treturn new Promise((resolve, reject) => {\n\t\tconst reader = new FileReader();\n\n\t\treader.onload = () => {\n\t\t\tresolve(reader.result);\n\t\t};\n\n\t\treader.onerror = reject;\n\n\t\treader.readAsDataURL(file);\n\t})\n}\n\nexport function loadImageAsync(src: string): Promise {\n\treturn new Promise((resolve, reject) => {\n\t\tconst image = new Image();\n\n\t\timage.onload = () => {\n\t\t\tresolve(image);\n\t\t};\n\n\t\timage.onerror = reject;\n\n\t\timage.src = src;\n\t})\n}","/**\n * @module utils\n */\n/** comment to work-around limitation of typedoc module plugin */\n\n// Copyright 2018-2024 Enlightware GmbH, Switzerland\n\nimport { disableErrorReporting, reportError, reportWarning } from 'apps/common/analytics';\nimport { Features, getLoggedInUserInfo, Parameters } from 'apps/common/parameters';\nimport { CodeVersion } from 'apps/common/version';\nimport { rememberMe } from 'apps/common/login-manager';\nimport { reportLog } from 'apps/common/analytics';\nimport { makeSync } from './async';\nimport * as Api from '../network/api';\nimport { reconnect } from '../network/api-connect';\nimport { UserIdLocalStorageKey } from 'apps/common/local-storage-keys';\nimport { isRunningInJest } from './types';\nimport { assert } from './assert';\n\nexport const TargetCodeVersionForThisBranchKey = 'gc-target-code-version' + (Parameters.devBranch === null ? '' : `-${Parameters.devBranch}`);\n\nconst KeepAliveInterval = 10 * 60 * 1000; // Ten minutes\ntype KeepAliveAction = 'reload' | 'reauth';\n\nlet reAuthLock = false;\nlet mustReload = false;\n\nexport function reload() {\n\tdisableErrorReporting();\n\twindow.location.reload();\n}\n\nexport function reloadIfNeeded() {\n\tif (mustReload) {\n\t\treportLog('forceReload');\n\t\treload();\n\t}\n}\n\nasync function sendKeepAliveAndTakeAction() {\n\tif (reAuthLock) {\n\t\treturn;\n\t}\n\treAuthLock = true;\n\ttry {\n\t\ttype Reply = { action?: KeepAliveAction; expectedFrontendRevision: string | null; };\n\n\t\tconst currentUserUuid = getLoggedInUserInfo()?.userInfo.uuid ?? null;\n\t\tconst { action, expectedFrontendRevision }: Reply = await Api.fetchJson(\n\t\t\tApi.getApiBaseUrl() + '/v0/keep_alive', 'POST', { codeVersion: CodeVersion, userUuid: currentUserUuid }\n\t\t);\n\t\tif (expectedFrontendRevision && expectedFrontendRevision !== CodeVersion) {\n\t\t\tif (!Features.localhost) {\n\t\t\t\t// TODO Inform the user about outdated code and suggest to reload.\n\t\t\t\tconsole.warn(`Code version mismatch detected: Server expects \"${expectedFrontendRevision}\" but current is \"${CodeVersion}\"!`);\n\t\t\t}\n\t\t}\n\t\tswitch (action) {\n\t\t\tcase 'reload': {\n\t\t\t\treportLog('scheduleReload');\n\t\t\t\tconsole.info('Backend was asking to reload. Scheduling to do so.');\n\t\t\t\tmustReload = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase 'reauth': {\n\t\t\t\tif (rememberMe()) {\n\t\t\t\t\tconsole.info('Server asked for a reauth and user has remember me on. Trying on-the-fly re-authentication.');\n\t\t\t\t\t// we attempt to re-auth\n\t\t\t\t\tconst result = await reconnect();\n\t\t\t\t\tif (result.user === null ||\n\t\t\t\t\t\t(currentUserUuid !== null && currentUserUuid !== result.user.userInfo.uuid)) {\n\t\t\t\t\t\treportLog('reauthWrongUserReload');\n\t\t\t\t\t\tconsole.warn('We have no user or a different user. Forcing a reload.');\n\t\t\t\t\t\treload();\n\t\t\t\t\t} else {\n\t\t\t\t\t\treportLog('reauthSuccess');\n\t\t\t\t\t\tconsole.info('Success! Re-authentication appears to have worked.');\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\treportLog('reauthMustReload');\n\t\t\t\t\tconsole.debug('We have no way to re-auth, reloading page.');\n\t\t\t\t\treload();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t} catch (e) {\n\t\tconsole.info('Keep-alive or reload/auth failed with exception:', e);\n\t} finally {\n\t\treAuthLock = false;\n\t}\n}\n\nasync function sendKeepAliveAndReloadIfNeeded() {\n\tawait sendKeepAliveAndTakeAction();\n\treloadIfNeeded();\n}\n\nexport function setupKeepAlive() {\n\tsetInterval(makeSync(sendKeepAliveAndTakeAction), KeepAliveInterval);\n\twindow.addEventListener('online', makeSync(sendKeepAliveAndReloadIfNeeded));\n}\n\nexport function setupReloadOnUserIdOrTargetVersionChange() {\n\twindow.addEventListener('storage', (event) => {\n\t\tif (event.key === UserIdLocalStorageKey && event.newValue !== event.oldValue && event.oldValue !== null) {\n\t\t\tconst params = {\n\t\t\t\t'old': event.oldValue,\n\t\t\t\t'new': event.newValue\n\t\t\t};\n\t\t\treportLog('reloadDueToUserIdChange', params);\n\t\t\treload();\n\t\t}\n\n\t\t// Reload if tab using the same branch finds a new target code version and it differs from the current one.\n\t\tif (event.key === TargetCodeVersionForThisBranchKey && event.newValue) {\n\t\t\tif (reloadIfCurrentCodeVersionIsUnexpected(event.newValue)) {\n\t\t\t\treportLog('reloadDueToTargetCodeVersionChange', {\n\t\t\t\t\t'old': event.oldValue,\n\t\t\t\t\t'new': event.newValue\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t});\n}\n\n// const LogStatsInterval = 60 * 1000; // 1 minute\n\n// interface MemoryInfo {\n// \tjsHeapSizeLimit: number,\n// \ttotalJSHeapSize: number,\n// \tusedJSHeapSize: number\n// }\n// function memoryInfoToJSON(info: MemoryInfo) {\n// \treturn {\n// \t\tlimit: info.jsHeapSizeLimit,\n// \t\ttotal: info.totalJSHeapSize,\n// \t\tused: info.usedJSHeapSize\n// \t}\n// }\n\n// function logStats() {\n// \tconst p = performance as any as { memory?: MemoryInfo };\n// \tconst memory = p?.memory ?? null;\n// \tif (memory !== null && document.visibilityState === 'visible') {\n// \t\tconst stats = { memory: memoryInfoToJSON(memory) };\n// \t\treportLog('stats', stats);\n// \t}\n// }\n\nexport function setupLogStats() {\n\t// At the moment, we do not log stats\n\t// logStats();\n\t// setInterval(logStats, LogStatsInterval);\n}\n\n\nexport const enum ReloadIfWrongCodeVersionResult {\n\tInitiated,\n\tNotNeeded,\n\tGivenUp,\n}\n\nconst PreviouslyMismatchingCodeVersion = 'gc-previously-mismatching-code-version';\nlet reloadInProgress = false;\n\nexport function resetReloadInProgress() {\n\tassert(isRunningInJest());\n\ttry {\n\t\treturn reloadInProgress;\n\t} finally {\n\t\treloadInProgress = false;\n\t}\n}\n\nexport function reloadIfCurrentCodeVersionIsUnexpected(expectedCodeVersion: string | null, currentCodeVersion = CodeVersion) {\n\tconsole.debug(`reloadIfCurrentCodeVersionIsUnexpected(expectedCodeVersion=${expectedCodeVersion}, current_code_version=${currentCodeVersion})'.`);\n\tif (!isRunningInJest()) {\n\t\tif (Features.localhost) {\n\t\t\tconsole.debug(`Not reloading despite unexpected code version ${currentCodeVersion} (expected: ${expectedCodeVersion}) because we are served from localhost and not in a Jest test.`);\n\t\t\treturn ReloadIfWrongCodeVersionResult.NotNeeded;\n\t\t}\n\t\tif (expectedCodeVersion === 'TEST_REV') {\n\t\t\tconsole.debug(`Not reloading despite unexpected code version ${currentCodeVersion} (expected: ${expectedCodeVersion}) because the test revision is expected and we are not in a Jest test.`);\n\t\t\treturn ReloadIfWrongCodeVersionResult.NotNeeded;\n\t\t}\n\t}\n\tif (reloadInProgress) { // Prevent double reload and giving up within one session because the code doesn't seem to change.\n\t\treturn ReloadIfWrongCodeVersionResult.Initiated;\n\t}\n\tif (expectedCodeVersion === null || expectedCodeVersion === currentCodeVersion) {\n\t\twindow.sessionStorage.removeItem(PreviouslyMismatchingCodeVersion);\n\t\treturn ReloadIfWrongCodeVersionResult.NotNeeded;\n\t} else {\n\t\tconst previouslyMismatchingCodeVersion = window.sessionStorage.getItem(PreviouslyMismatchingCodeVersion);\n\t\tconsole.debug(`Previously mismatching code version: '${previouslyMismatchingCodeVersion}'.`);\n\t\tif (previouslyMismatchingCodeVersion === CodeVersion) {\n\t\t\tconst message = `Frontend version mismatch (expected: '${expectedCodeVersion}', running: '${CodeVersion}') and previously mismatching version is the same as the current. Giving up to avoid reload loop.`;\n\t\t\treportError('unrecoverableFrontendVersionMismatch', message, new Error(message));\n\t\t\treturn ReloadIfWrongCodeVersionResult.GivenUp;\n\t\t} else {\n\t\t\twindow.sessionStorage.setItem(PreviouslyMismatchingCodeVersion, CodeVersion);\n\t\t\treportWarning('frontendVersionMismatch', `Frontend version mismatch (expected: '${expectedCodeVersion}', running: '${CodeVersion}')! Attempting reload.`);\n\t\t\treloadInProgress = true;\n\t\t\treload();\n\t\t\treturn ReloadIfWrongCodeVersionResult.Initiated;\n\t\t}\n\t}\n}\n"],"names":["devMode","UserIdToCurrentGameKey","userToCurrentGameMap","Map","localStorage","userToCurrentGameMapSerialized","getItem","getRequestedGameId","gameIdFromUrl","substring","get","async","connectToServer","doLogin","deviceWorkspace","loginManager","getInstance","dissolveDws","connectResult","serverRevision","server","revision","console","info","serverVersion","connectAndCreateRustDbReadOnlyStorage","db","storage","deviceWorkspaceUsedForConnect","loginManagerUsedForConnect","dissolveDeviceWorkspaceUsedForConnect","connect","dissolveDeviceWorkspace","currentCodeVersion","login","getLogin","dws","decodeURIComponent","rememberMe","lang","expectedFrontendRevision","window","setItem","Error","user","attributes","organisations","userInfo","length","organisationMembership","product","products","expiresInSecs","expirationDate","Date","now","newLogin","debug","id","setLogin","removeLogin","res","cws","bs","app","appInfo","url","location","href","first","sleep","durationSeconds","Promise","resolve","setTimeout","identifier","callbackHolder","callbackName","constructor","testFn","super","_","arg","cancelCallback","this","prototype","domEvent","event","element","addEventListener","target","eventName","TargetCodeVersionForThisBranchKey","devBranch","reload","PreviouslyMismatchingCodeVersion","reloadInProgress","reloadIfCurrentCodeVersionIsUnexpected","expectedCodeVersion","localhost","sessionStorage","removeItem","previouslyMismatchingCodeVersion","message"],"sourceRoot":""}