UNPKG

@casual-simulation/aux-runtime

Version:
1,209 lines (1,208 loc) 125 kB
/* CasualOS is a set of web-based tools designed to facilitate the creation of real-time, multi-user, context-aware interactive experiences. * * Copyright (c) 2019-2025 Casual Simulation, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ import { hasValue, tagsOnBot, isFormula, isScript, isNumber, BOT_SPACE_TAG, botUpdated, isBot, ORIGINAL_OBJECT, DEFAULT_ENERGY, getBotSpace, ON_ACTION_ACTION_NAME, breakIntoIndividualEvents, ON_BOT_ADDED_ACTION_NAME, ON_ANY_BOTS_ADDED_ACTION_NAME, ON_ANY_BOTS_REMOVED_ACTION_NAME, ON_BOT_CHANGED_ACTION_NAME, ON_ANY_BOTS_CHANGED_ACTION_NAME, updatedBot, TAG_MASK_SPACE_PRIORITIES, CLEAR_CHANGES_SYMBOL, DNA_TAG_PREFIX, isRuntimeBot, createBot, ON_ERROR, action, isBotInDimension, asyncResult, registerBuiltinPortal, defineGlobalBot, isBotLink, parseBotLink, isBotDate, parseBotDate, formatBotDate, isTaggedString, parseTaggedString, parseNumber, isTaggedNumber, isBotVector, parseBotVector, formatBotVector, isBotRotation, parseBotRotation, formatBotRotation, parseTaggedNumber, REPLACE_BOT_SYMBOL, isModule, calculateStringTagValue, ON_RESOLVE_MODULE, } from '@casual-simulation/aux-common/bots'; import { Subject, Subscription } from 'rxjs'; import { AuxCompiler, getInterpretableFunction, isInterpretableFunction, IMPORT_META_FACTORY, IMPORT_FACTORY, EXPORT_FACTORY, } from './AuxCompiler'; import { addToContext, MemoryGlobalContext, removeFromContext, isInContext, } from './AuxGlobalContext'; import { createDefaultLibrary, GET_RUNTIME } from './AuxLibrary'; import { createRuntimeBot, RealtimeEditMode } from './RuntimeBot'; import { RanOutOfEnergyError } from './AuxResults'; import { isPromise, isRuntimePromise, isUrl, markAsRuntimePromise, } from './Utils'; import { DefaultRealtimeEditModeProvider } from './AuxRealtimeEditModeProvider'; import { sortBy, forOwn, merge, union } from 'es-toolkit/compat'; import { applyTagEdit, isTagEdit } from '@casual-simulation/aux-common/bots'; import { updateRuntimeVersion } from './RuntimeStateVersion'; import { replaceMacros } from './Transpiler'; import { DateTime } from 'luxon'; import { Rotation, Vector2, Vector3 } from '@casual-simulation/aux-common/math'; import { isGenerator, markAsUncopiableObject, UNCOPIABLE, } from '@casual-simulation/js-interpreter/InterpreterUtils'; import { v4 as uuid } from 'uuid'; import { importInterpreter as _dynamicImportInterpreter } from './AuxRuntimeDynamicImports'; import { UNMAPPABLE } from '@casual-simulation/aux-common/bots/BotEvents'; import { DeepObjectError, convertToCopiableValue, } from '@casual-simulation/aux-common/partitions/PartitionUtils'; let Interpreter; let DeclarativeEnvironmentRecord; let DefinePropertyOrThrow; let Descriptor; let Value; let hasModule = false; let interpreterImportPromise; export function registerInterpreterModule(module) { hasModule = true; Interpreter = module.Interpreter; DeclarativeEnvironmentRecord = module.DeclarativeEnvironmentRecord; DefinePropertyOrThrow = module.DefinePropertyOrThrow; Descriptor = module.Descriptor; Value = module.Value; } function importInterpreter() { if (hasModule) { return Promise.resolve(); } if (interpreterImportPromise) { return interpreterImportPromise; } else { return (interpreterImportPromise = _importInterpreterCore()); } } async function _importInterpreterCore() { const module = await _dynamicImportInterpreter(); registerInterpreterModule(module); } /** * Defines an class that is able to manage the runtime state of an AUX. * * Being a runtime means providing and managing the execution state that an AUX is in. * This means taking state updates events, shouts and whispers, and emitting additional events to affect the future state. */ export class AuxRuntime { get library() { return this._library; } get context() { return this._globalContext; } get currentVersion() { return this._currentVersion; } get globalObject() { return this._globalObject; } get canTriggerBreakpoint() { return !!this._interpreter && this._interpreter.debugging; } get systemMap() { return this._systemMap; } /** * Creates a new AuxRuntime using the given library factory. * @param libraryFactory * @param forceSignedScripts Whether to force the runtime to only allow scripts that are signed. * @param exemptSpaces The spaces that are exempt from requiring signed scripts. * @param interpreter The interpreter that should be used for the runtime. */ constructor(version, device, libraryFactory = createDefaultLibrary, editModeProvider = new DefaultRealtimeEditModeProvider(), exemptSpaces = ['local', 'tempLocal'], forceSyncScripts = false, interpreter = null) { this._compiledState = {}; this._existingMasks = {}; this._compiler = new AuxCompiler(); this._stopState = null; this._breakpoints = new Map(); this._currentStopCount = 0; this._currentPromise = null; this._actionBatch = []; this._errorBatch = []; this._currentVersion = { localSites: {}, vector: {}, }; this._updatedBots = new Map(); this._newBots = new Map(); this._batchPending = false; this._jobQueueCheckPending = false; this._jobQueueCheckCount = 0; this._processingErrors = false; this._portalBots = new Map(); this._builtinPortalBots = []; this._globalChanges = {}; this._beforeActionListeners = []; this._scriptActionEnqueuedListeners = []; this._scriptUpdatedTagListeners = []; this._scriptUpdatedTagMaskListeners = []; // private _beforeScriptEnterListeners: (( // trace: DebuggerScriptEnterTrace // ) => void)[] = []; // private _afterScriptExitListeners: (( // trace: DebuggerScriptExitTrace // ) => void)[] = []; /** * The counter that is used to generate function names. */ this._functionNameCounter = 0; /** * A map of function names to their respective functions. */ this._functionMap = new Map(); /** * A map of bot IDs to a list of function names. */ this._botFunctionMap = new Map(); /** * Whether changes should be automatically batched. */ this._autoBatch = true; this._forceSyncScripts = false; this._currentDebugger = null; /** * The map of module IDs to their exports. * Only used for global modules, which are modules that are not attached to a bot (e.g. source modules). */ this._cachedGlobalModules = new Map(); /** * The map of system IDs to their respective bot IDs. */ this._systemMap = new Map(); /** * The number of times that the runtime can call onError for an error from the same script. */ this.repeatedErrorLimit = 1000; this._libraryFactory = libraryFactory; this._interpreter = interpreter; this._globalContext = new MemoryGlobalContext(version, device, this, this, this); this._forceSyncScripts = forceSyncScripts; this._globalContext.mockAsyncActions = forceSyncScripts; this._library = merge(libraryFactory(this._globalContext), { api: { os: { createDebugger: this._createDebugger.bind(this), getExecutingDebugger: this._getExecutingDebugger.bind(this), }, }, }); this._editModeProvider = editModeProvider; this._exemptSpaces = exemptSpaces; this._onActions = new Subject(); this._onErrors = new Subject(); this._onRuntimeStop = new Subject(); let sub = (this._sub = new Subscription(() => { this._globalContext.cancelAllBotTimers(); })); sub.add(this._globalContext.startAnimationLoop()); if (globalThis.addEventListener) { const unhandledRejectionListener = (event) => { const data = this._handleError(event.reason, null, null); this._globalContext.enqueueError(data); event.preventDefault(); }; globalThis.addEventListener('unhandledrejection', unhandledRejectionListener); sub.add(() => { globalThis.removeEventListener('unhandledrejection', unhandledRejectionListener); }); } this._globalObject = new Proxy(globalThis, { get: (target, key, receiver) => { if (key in this._globalChanges) { return Reflect.get(this._globalChanges, key); } else if (key in target) { return Reflect.get(target, key); } return Reflect.get(target, key, receiver); }, set: (target, key, value, receiver) => { return Reflect.set(this._globalChanges, key, value); }, deleteProperty: (target, key) => { return Reflect.deleteProperty(this._globalChanges, key); }, defineProperty: (target, key, options) => { return Reflect.defineProperty(this._globalChanges, key, options); }, ownKeys: (target) => { const addedKeys = Reflect.ownKeys(this._globalChanges); const otherKeys = Reflect.ownKeys(target); return union(addedKeys, otherKeys); }, has: (target, key) => { return (Reflect.has(this._globalChanges, key) || Reflect.has(target, key)); }, getOwnPropertyDescriptor: (target, key) => { if (key in this._globalChanges) { return Reflect.getOwnPropertyDescriptor(this._globalChanges, key); } return Reflect.getOwnPropertyDescriptor(target, key); }, }); this._globalContext.global = this._globalObject; if (this._interpreter) { // Use the interpreted versions of APIs this._interpretedApi = { ...this._library.api }; this._interpretedTagSpecificApi = { ...this._library.tagSpecificApi, }; for (let key in this._interpretedApi) { const val = this._interpretedApi[key]; if (isInterpretableFunction(val)) { this._interpretedApi[key] = getInterpretableFunction(val); } } for (let key in this._interpretedTagSpecificApi) { const val = this._interpretedTagSpecificApi[key]; if (isInterpretableFunction(val)) { this._interpretedTagSpecificApi[key] = getInterpretableFunction(val); } } } } async _importModule(module, meta, dependencyChain = [], allowCustomResolution = true) { try { let m; let bot; const allowResolution = meta.tag !== ON_RESOLVE_MODULE && allowCustomResolution; if (typeof module !== 'string') { m = module; if (!m) { throw new Error('Module not found: ' + module); } } else { const globalModule = this._cachedGlobalModules.get(module); if (globalModule) { return await globalModule; } m = await this.resolveModule(module, meta, allowResolution); if (!m) { throw new Error('Module not found: ' + module); } if (dependencyChain.length > 1) { const index = dependencyChain.indexOf(m.id); if (index >= 0) { throw new Error(`Circular dependency detected: ${dependencyChain .slice(index) .join(' -> ')} -> ${m.id}`); } } if ('botId' in m) { bot = this._compiledState[m.botId]; if (bot) { const exports = bot.exports[m.tag]; if (exports) { return await exports; } } } } const promise = this._importModuleCore(m, [...dependencyChain, m.id], allowResolution); if (bot) { bot.exports[m.tag] = promise; } else { this._cachedGlobalModules.set(m.id, promise); } return await promise; } finally { this._scheduleJobQueueCheck(); } } async _importModuleCore(m, dependencyChain, allowCustomResolution) { try { const exports = {}; const importFunc = (id, meta) => this._importModule(id, meta, dependencyChain, allowCustomResolution); const exportFunc = async (valueOrSource, e, meta) => { const result = await this._resolveExports(valueOrSource, e, meta, dependencyChain, allowCustomResolution); this._scheduleJobQueueCheck(); Object.assign(exports, result); }; if ('botId' in m) { const bot = this._compiledState[m.botId]; const module = bot === null || bot === void 0 ? void 0 : bot.modules[m.tag]; if (module) { await module.moduleFunc(importFunc, exportFunc); } } else if ('source' in m) { const source = m.source; const mod = this._compile(null, null, source, {}); if (mod.moduleFunc) { await mod.moduleFunc(importFunc, exportFunc); } } else if ('exports' in m) { Object.assign(exports, m.exports); } else if ('url' in m) { return await this.dynamicImport(m.url); } return exports; } finally { this._scheduleJobQueueCheck(); } } async _resolveExports(valueOrSource, exports, meta, dependencyChain, allowCustomResolution) { if (typeof valueOrSource === 'string') { const sourceModule = await this._importModule(valueOrSource, meta, dependencyChain, allowCustomResolution); if (exports) { const result = {}; for (let val of exports) { if (typeof val === 'string') { result[val] = sourceModule[val]; } else { const [source, target] = val; const key = target !== null && target !== void 0 ? target : source; result[key] = sourceModule[source]; } } return result; } else { return sourceModule; } } else { return valueOrSource; } } /** * Performs a dynamic import() of the given module. * Uses the JS Engine's native import() functionality. * @param module The module that should be imported. * @returns Returns a promise that resolves with the module's exports. */ async dynamicImport(module) { return await import(/* @vite-ignore */ module); } /** * Attempts to resolve the module with the given name. * @param moduleName The name of the module to resolve. * @param meta The metadata that should be used to resolve the module. */ async resolveModule(moduleName, meta, allowCustomResolution = true) { if ((meta === null || meta === void 0 ? void 0 : meta.tag) === ON_RESOLVE_MODULE) { allowCustomResolution = false; } if (moduleName === 'casualos') { let exports = { ...this._library.api, }; const bot = (meta === null || meta === void 0 ? void 0 : meta.botId) ? this._compiledState[meta.botId] : null; const ctx = { bot, tag: meta === null || meta === void 0 ? void 0 : meta.tag, creator: bot ? this._getRuntimeBot(bot.script.tags.creator) : null, config: null, }; for (let key in this._library.tagSpecificApi) { if (!Object.prototype.hasOwnProperty.call(this._library.tagSpecificApi, key)) { continue; } const result = this._library.tagSpecificApi[key](ctx); exports[key] = result; } return { id: 'casualos', exports, }; } if (allowCustomResolution) { const shoutResult = this.shout(ON_RESOLVE_MODULE, undefined, { module: moduleName, meta, }); const actionResult = isRuntimePromise(shoutResult) ? await shoutResult : shoutResult; for (let scriptResult of actionResult.results) { const result = await scriptResult; if (result) { if (typeof result === 'object') { if (typeof result.botId === 'string' && typeof result.tag === 'string') { const bot = this._compiledState[result.botId]; const mod = bot === null || bot === void 0 ? void 0 : bot.modules[result.tag]; if (mod) { return { botId: result.botId, id: moduleName, tag: result.tag, }; } } else if (typeof result.exports === 'object' && result.exports) { return { id: moduleName, exports: result.exports, }; } } else if (typeof result === 'string') { if (isUrl(result)) { return { id: moduleName, url: result, }; } else { return { id: moduleName, source: result, }; } } } } } const isRelativeImport = moduleName.startsWith('.') || moduleName.startsWith(':'); if (isRelativeImport) { if (!meta) { throw new Error('Cannot resolve relative import without metadata'); } const bot = this._compiledState[meta.botId]; if (!bot) { throw new Error('Cannot resolve relative import without bot'); } const system = calculateStringTagValue(null, bot, 'system', `🔗${bot.id}`); const split = system.split('.'); for (let i = 0; i < moduleName.length; i++) { if (moduleName[i] === ':') { split.pop(); } else if (moduleName[i] === '.') { /* empty */ } else { moduleName = split.join('.') + '.' + moduleName.substring(i); break; } } } if (moduleName.startsWith('🔗')) { const [id, tag] = moduleName.substring('🔗'.length).split('.'); const bot = this._compiledState[id]; if (bot && tag) { return { id: moduleName, botId: bot.id, tag: tag, }; } } const lastIndex = moduleName.lastIndexOf('.'); if (lastIndex >= 0) { const system = moduleName.substring(0, lastIndex); const tag = moduleName.substring(lastIndex + 1); const botIds = this._systemMap.get(system); if (botIds) { for (let id of botIds) { const bot = this._compiledState[id]; if (bot && bot.modules[tag]) { return { botId: id, id: moduleName, tag: tag, }; } } } } return { id: moduleName, url: moduleName, }; } getShoutTimers() { return {}; } get closed() { return this._sub.closed; } unsubscribe() { return this._sub.unsubscribe(); } /** * Gets the current state that the runtime is operating on. */ get currentState() { return this._compiledState; } get userId() { return this._userId; } set userId(id) { this._userId = id; this._globalContext.playerBot = this.userBot; } get userBot() { if (!this._userId) { return; } const bot = this._compiledState[this._userId]; if (bot) { return bot.script; } else { return null; } } /** * An observable that resolves whenever the runtime issues an action. */ get onActions() { return this._onActions; } /** * An observable that resolves whenever the runtime issues an error. */ get onErrors() { return this._onErrors; } /** * An observable that resolves whenever the runtime pauses in a script. */ get onRuntimeStop() { return this._onRuntimeStop; } /** * Processes the given bot actions and dispatches the resulting actions in the future. * @param actions The actions to process. */ process(actions) { if (this._beforeActionListeners.length > 0) { for (let func of this._beforeActionListeners) { for (let action of actions) { try { func(action); } catch (err) { console.error(err); } } } } this._processBatch(); const result = this._processCore(actions); this._processBatch(); return result; } _getExecutingDebugger() { return this._currentDebugger; } async _createDebugger(options) { const forceSyncScripts = typeof (options === null || options === void 0 ? void 0 : options.allowAsynchronousScripts) === 'boolean' ? !options.allowAsynchronousScripts : false; await importInterpreter(); const interpreter = (options === null || options === void 0 ? void 0 : options.pausable) ? new Interpreter() : null; const runtime = new AuxRuntime(this._globalContext.version, this._globalContext.device, this._libraryFactory, this._editModeProvider, this._exemptSpaces, forceSyncScripts, interpreter); runtime._autoBatch = true; let idCount = 0; if (!(options === null || options === void 0 ? void 0 : options.useRealUUIDs)) { runtime._globalContext.uuid = () => { idCount += 1; return `uuid-${idCount}`; }; } let allActions = []; let allErrors = []; let create; if (interpreter && isInterpretableFunction(runtime._library.tagSpecificApi.create)) { const func = getInterpretableFunction(runtime._library.tagSpecificApi.create)({ bot: null, config: null, creator: null, tag: null, }); create = (...args) => { const result = func(...args); if (isGenerator(result)) { return runtime._processGenerator(result); } return result; }; } else { create = runtime._library.tagSpecificApi.create({ bot: null, config: null, creator: null, tag: null, }); } const isCommonAction = (action) => { return !(action.type === 'add_bot' || action.type === 'remove_bot' || action.type === 'update_bot' || action.type === 'apply_state'); }; const getAllActions = () => { const actions = runtime._processUnbatchedActions(); allActions.push(...actions); return allActions; }; // The config bot is always ID 0 in debuggers const configBotId = (options === null || options === void 0 ? void 0 : options.useRealUUIDs) ? runtime.context.uuid() : 'uuid-0'; const configBotTags = (options === null || options === void 0 ? void 0 : options.configBot) ? isBot(options === null || options === void 0 ? void 0 : options.configBot) ? options.configBot.tags : options.configBot : {}; runtime.context.createBot(createBot(configBotId, configBotTags, 'tempLocal')); runtime.process(this._builtinPortalBots.map((b) => registerBuiltinPortal(b))); runtime.userId = configBotId; const api = { ...runtime._library.api, }; if (interpreter) { for (let key in runtime._library.api) { const val = runtime._library.api[key]; if (isInterpretableFunction(val)) { const func = getInterpretableFunction(val); api[key] = (...args) => { const result = func(...args); if (isGenerator(result)) { return runtime._processGenerator(result); } return result; }; } } } if (interpreter && (options === null || options === void 0 ? void 0 : options.pausable)) { interpreter.debugging = true; } const debug = { [UNCOPIABLE]: true, ...api, getAllActions, getCommonActions: () => { return getAllActions().filter(isCommonAction); }, getBotActions: () => { return getAllActions().filter((a) => !isCommonAction(a)); }, getErrors: () => { const errors = runtime._processUnbatchedErrors(); allErrors.push(...errors); return allErrors; }, onBeforeUserAction: (listener) => { runtime._beforeActionListeners.push(listener); }, onScriptActionEnqueued: (listener) => { runtime._scriptActionEnqueuedListeners.push(listener); }, onAfterScriptUpdatedTag: (listener) => { runtime._scriptUpdatedTagListeners.push(listener); }, onAfterScriptUpdatedTagMask: (listener) => { runtime._scriptUpdatedTagMaskListeners.push(listener); }, getCallStack() { if (!interpreter) { throw new Error('getCallStack() is only supported on pausable debuggers.'); } return runtime._mapCallStack(interpreter.agent.executionContextStack); }, async performUserAction(...actions) { const result = await runtime.process(actions); return result.map((r) => (r ? r.results : null)); }, // TODO: Determine whether to support this // onBeforeScriptEnter: ( // listener: (trace: DebuggerScriptEnterTrace) => void // ) => { // runtime._beforeScriptEnterListeners.push(listener); // }, // onAfterScriptExit: ( // listener: (trace: DebuggerScriptExitTrace) => void // ) => { // runtime._afterScriptExitListeners.push(listener); // }, setPauseTrigger(b, tag, options) { var _a, _b, _c; if (typeof b === 'object' && 'triggerId' in b) { runtime.setBreakpoint({ id: b.triggerId, botId: b.botId, tag: b.tag, lineNumber: b.lineNumber, columnNumber: b.columnNumber, states: (_a = b.states) !== null && _a !== void 0 ? _a : ['before'], disabled: !((_b = b.enabled) !== null && _b !== void 0 ? _b : true), }); return b; } else { const id = isBot(b) ? b.id : b; const trigger = { triggerId: uuid(), botId: id, tag: tag, ...options, }; runtime.setBreakpoint({ id: trigger.triggerId, botId: id, tag: tag, lineNumber: trigger.lineNumber, columnNumber: trigger.columnNumber, states: (_c = trigger.states) !== null && _c !== void 0 ? _c : ['before'], disabled: false, }); return trigger; } }, removePauseTrigger(triggerOrId) { let id = typeof triggerOrId === 'string' ? triggerOrId : triggerOrId.triggerId; runtime.removeBreakpoint(id); }, disablePauseTrigger(triggerOrId) { var _a; if (typeof triggerOrId === 'string') { let trigger = runtime._breakpoints.get(triggerOrId); if (trigger) { trigger.disabled = true; } } else { runtime.setBreakpoint({ id: triggerOrId.triggerId, botId: triggerOrId.botId, tag: triggerOrId.tag, lineNumber: triggerOrId.lineNumber, columnNumber: triggerOrId.columnNumber, states: (_a = triggerOrId.states) !== null && _a !== void 0 ? _a : ['before'], disabled: true, }); } }, enablePauseTrigger(triggerOrId) { var _a; if (typeof triggerOrId === 'string') { let trigger = runtime._breakpoints.get(triggerOrId); if (trigger) { trigger.disabled = false; } } else { runtime.setBreakpoint({ id: triggerOrId.triggerId, botId: triggerOrId.botId, tag: triggerOrId.tag, lineNumber: triggerOrId.lineNumber, columnNumber: triggerOrId.columnNumber, states: (_a = triggerOrId.states) !== null && _a !== void 0 ? _a : ['before'], disabled: false, }); } }, listPauseTriggers() { let triggers = []; for (let breakpoint of runtime._breakpoints.values()) { triggers.push({ triggerId: breakpoint.id, botId: breakpoint.botId, tag: breakpoint.tag, columnNumber: breakpoint.columnNumber, lineNumber: breakpoint.lineNumber, states: breakpoint.states.slice(), enabled: !breakpoint.disabled, }); } return triggers; }, listCommonPauseTriggers(botOrId, tag) { const id = typeof botOrId === 'string' ? botOrId : botOrId.id; const bot = runtime.currentState[id]; const func = bot.listeners[tag]; if (!func) { return []; } return runtime._compiler.listPossibleBreakpoints(func, runtime._interpreter); }, onPause(callback) { runtime.onRuntimeStop.subscribe((stop) => { const pause = { pauseId: stop.stopId, state: stop.state, callStack: runtime._mapCallStack(stop.stack), trigger: { triggerId: stop.breakpoint.id, botId: stop.breakpoint.botId, tag: stop.breakpoint.tag, lineNumber: stop.breakpoint.lineNumber, columnNumber: stop.breakpoint.columnNumber, states: stop.breakpoint.states, }, }; callback(pause); }); }, resume(pause) { runtime.continueAfterStop(pause.pauseId); }, [GET_RUNTIME]() { return runtime; }, get configBot() { return runtime.userBot; }, getPortalBots() { var _a; let portalBots = new Map(); for (let [portal, id] of runtime._portalBots) { portalBots.set(portal, (_a = runtime.currentState[id]) === null || _a === void 0 ? void 0 : _a.script); } return portalBots; }, create, }; runtime._currentDebugger = debug; this._scheduleJobQueueCheck(); return debug; } _mapCallStack(stack) { const interpreter = this._interpreter; return stack.map((s) => { const callSite = s.callSite; const funcName = callSite.getFunctionName(); let funcLocation = {}; if (funcName) { const f = this._functionMap.get(funcName); if (f) { funcLocation.name = f.metadata.diagnosticFunctionName; const location = this._compiler.calculateOriginalLineLocation(f, { lineNumber: callSite.lineNumber, column: callSite.columnNumber, }); funcLocation.lineNumber = location.lineNumber + 1; funcLocation.columnNumber = location.column + 1; const tagName = f.metadata.context.tag; const bot = f.metadata.context.bot; if (bot) { funcLocation.botId = bot.id; } if (tagName) { funcLocation.tag = tagName; } } else { funcLocation.name = funcName; } } if (!hasValue(funcLocation.lineNumber) && !hasValue(funcLocation.columnNumber) && hasValue(callSite.lineNumber) && hasValue(callSite.columnNumber)) { funcLocation.lineNumber = callSite.lineNumber; funcLocation.columnNumber = callSite.columnNumber; } if (!hasValue(funcLocation.lineNumber) && !hasValue(funcLocation.columnNumber) && !hasValue(funcLocation.name)) { funcLocation = null; } const ret = { location: funcLocation, listVariables() { let variables = []; if (s.LexicalEnvironment instanceof DeclarativeEnvironmentRecord) { addBindingsFromEnvironment(s.LexicalEnvironment, 'block'); } if (s.VariableEnvironment instanceof DeclarativeEnvironmentRecord) { addBindingsFromEnvironment(s.VariableEnvironment, 'frame'); let parent = s.VariableEnvironment.OuterEnv; while (parent) { if (parent instanceof DeclarativeEnvironmentRecord) { addBindingsFromEnvironment(parent, 'closure'); } parent = parent.OuterEnv; } } return variables; function addBindingsFromEnvironment(env, scope) { for (let [nameValue, binding,] of env.bindings.entries()) { const name = interpreter.copyFromValue(nameValue); const initialized = !!binding.initialized; const mutable = !!binding.mutable; const value = initialized ? interpreter.reverseProxyObject(binding.value, false) : undefined; const variable = { name, value, writable: mutable, scope, }; if (!initialized) { variable.initialized = false; } variables.push(variable); } } }, setVariableValue(name, value) { if (s.LexicalEnvironment instanceof DeclarativeEnvironmentRecord) { const nameValue = interpreter.copyToValue(name); const proxiedValue = interpreter.proxyObject(value); if (nameValue.Type !== 'normal') { throw interpreter.copyFromValue(nameValue.Value); } if (proxiedValue.Type !== 'normal') { throw interpreter.copyFromValue(proxiedValue.Value); } const result = s.LexicalEnvironment.SetMutableBinding(nameValue.Value, proxiedValue.Value, Value.true); if (result.Type !== 'normal') { throw interpreter.copyFromValue(result.Value); } return interpreter.copyFromValue(result.Value); } }, }; return ret; }); } _processCore(actions) { const _this = this; const results = []; function processAction(action, addToResults) { let promise = _this._processAction(action); if (addToResults) { if (isRuntimePromise(promise)) { return markAsRuntimePromise(promise.then((result) => { results.push(result); })); } else { results.push(promise); } return; } return promise; } function handleRejection(action, rejection) { let promise = processListOfMaybePromises(null, rejection.newActions, (action) => { return processAction(action, false); }); if (rejection.rejected) { return; } if (promise) { return markAsRuntimePromise(promise.then((p) => processAction(action, true))); } else { return processAction(action, true); } } let promise = processListOfMaybePromises(null, actions, (action) => { let rejection = this._rejectAction(action); let result; if (isRuntimePromise(rejection)) { result = markAsRuntimePromise(rejection.then((result) => handleRejection(action, result))); } else { result = handleRejection(action, rejection); } return result; }); if (isRuntimePromise(promise)) { return markAsRuntimePromise(promise.then(() => results)); } else { return results; } } _processAction(action) { if (action.type === 'action') { const result = this._shout(action.eventName, action.botIds, action.argument, false); if (isRuntimePromise(result)) { return markAsRuntimePromise(result .then((result) => this._processCore(result.actions)) .then(() => result)); } else { let promise = this._processCore(result.actions); if (isRuntimePromise(promise)) { return markAsRuntimePromise(promise.then(() => result)); } else { return result; } } } else if (action.type === 'run_script') { const result = this._execute(action.script, false, false); if (isRuntimePromise(result)) { return markAsRuntimePromise(result.then((result) => { const p = this._processCore(result.actions); if (isPromise(p)) { return p.then(() => { if (hasValue(action.taskId)) { this._globalContext.resolveTask(action.taskId, result.result, false); } return null; }); } else { if (hasValue(action.taskId)) { if (this._globalContext.resolveTask(action.taskId, result.result, false)) { this._scheduleJobQueueCheck(); } } } return null; })); } else { const p = this._processCore(result.actions); if (isRuntimePromise(p)) { return markAsRuntimePromise(p.then(() => { if (hasValue(action.taskId)) { if (this._globalContext.resolveTask(action.taskId, result.result, false)) { this._scheduleJobQueueCheck(); } } return null; })); } if (hasValue(action.taskId)) { if (this._globalContext.resolveTask(action.taskId, result.result, false)) { this._scheduleJobQueueCheck(); } } } } else if (action.type === 'apply_state') { const events = breakIntoIndividualEvents(this.currentState, action); const promise = this._processCore(events); if (isRuntimePromise(promise)) { return markAsRuntimePromise(promise.then(() => null)); } else { return null; } } else if (action.type === 'async_result') { const value = action.mapBotsInResult === true ? this._mapBotsToRuntimeBots(action.result) : action.result; if (!this._globalContext.resolveTask(action.taskId, value, false)) { this._actionBatch.push(action); } else { this._scheduleJobQueueCheck(); } } else if (action.type === 'async_error') { if (!this._globalContext.rejectTask(action.taskId, action.error, false)) { this._actionBatch.push(action); } else { this._scheduleJobQueueCheck(); } } else if (action.type === 'device_result') { if (!this._globalContext.resolveTask(action.taskId, action.result, true)) { this._actionBatch.push(action); } else { this._scheduleJobQueueCheck(); } } else if (action.type === 'device_error') { if (!this._globalContext.rejectTask(action.taskId, action.error, true)) { this._actionBatch.push(action); } else { this._scheduleJobQueueCheck(); } } else if (action.type === 'iterable_next') { if (!this._globalContext.iterableNext(action.taskId, action.value, false)) { this._actionBatch.push(action); } else { this._scheduleJobQueueCheck(); } } else if (action.type === 'iterable_complete') { if (!this._globalContext.iterableComplete(action.taskId, false)) { this._actionBatch.push(action); } else { this._scheduleJobQueueCheck(); } } else if (action.type === 'iterable_throw') { if (!this._globalContext.iterableThrow(action.taskId, action.error, false)) { this._actionBatch.push(action); } else { this._scheduleJobQueueCheck(); } } else if (action.type === 'register_custom_app') { this._registerPortalBot(action.appId, action.botId); this._actionBatch.push(action); } else if (action.type === 'register_builtin_portal') { if (!this._portalBots.has(action.portalId)) { const newBot = this.context.createBot(createBot(this.context.uuid(), undefined, 'tempLocal')); this._builtinPortalBots.push(action.portalId); this._registerPortalBot(action.portalId, newBot.id); this._actionBatch.push(defineGlobalBot(action.portalId, newBot.id)); } else { const botId = this._portalBots.get(action.portalId); this._actionBatch.push(defineGlobalBot(action.portalId, botId)); } } else if (action.type === 'define_global_bot') { if (this._portalBots.get(action.name) !== action.botId) { this._registerPortalBot(action.name, action.botId); this._actionBatch.push(action); } if (hasValue(action.taskId)) { const promise = this._processCore([ asyncResult(action.taskId, null), ]); if (isRuntimePromise(promise)) { return markAsRuntimePromise(promise.then(() => null)); } else { return null; } } } else { this._actionBatch.push(action); } return null; } _registerPortalBot(portalId, botId) { const hadPortalBot = this._portalBots.has(portalId); this._portalBots.set(portalId, botId); if (!hadPortalBot) { const variableName = `${portalId}Bot`; const getValue = () => { const botId = this._portalBots.get(portalId); if (hasValue(botId)) { return this.context.state[botId]; } else { return undefined; } }; Object.defineProperty(this._globalObject, variableName, { get: getValue,