UNPKG

@casual-simulation/aux-runtime

Version:
807 lines 29.2 kB
import { IS_PROXY_OBJECT, REGULAR_OBJECT, UNCOPIABLE, INTERPRETER_OBJECT, } from '@casual-simulation/js-interpreter/InterpreterUtils'; import { ADD_BOT_LISTENER_SYMBOL, applyTagEdit, GET_DYNAMIC_LISTENERS_SYMBOL, isTagEdit, mergeEdits, remoteEdit, REMOVE_BOT_LISTENER_SYMBOL, } from '@casual-simulation/aux-common/bots'; import { BOT_SPACE_TAG, getBotSpace, getTagMaskSpaces, hasValue, DEFAULT_TAG_MASK_SPACE, TAG_MASK_SPACE_PRIORITIES_REVERSE, TAG_MASK_SPACE_PRIORITIES, CLEAR_CHANGES_SYMBOL, SET_TAG_MASK_SYMBOL, CLEAR_TAG_MASKS_SYMBOL, EDIT_TAG_SYMBOL, EDIT_TAG_MASK_SYMBOL, getOriginalObject, GET_TAG_MASKS_SYMBOL, } from '@casual-simulation/aux-common/bots'; import { REPLACE_BOT_SYMBOL } from '@casual-simulation/aux-common/bots/Bot'; import { createBotLink, isBot, isBotLink, ORIGINAL_OBJECT, } from '@casual-simulation/aux-common/bots/BotCalculations'; import { INTERPRETABLE_FUNCTION } from './AuxCompiler'; const KNOWN_SYMBOLS = new Set([ REGULAR_OBJECT, INTERPRETER_OBJECT, INTERPRETABLE_FUNCTION, IS_PROXY_OBJECT, UNCOPIABLE, ]); /** * Adds any known symbols that the given target contains to the end of the given list of keys and returns a new list containing the combination of both. * @param target The target. * @param keys The keys that the symbols should be added to. */ export function addKnownSymbolsToList(target, keys) { let result = keys; for (let symbol of KNOWN_SYMBOLS) { if (symbol in target) { if (result === keys) { result = [...keys]; } result.push(symbol); } } return result; } /** * Flattens the given tag masks into a normal tags object. * Spaces are prioritized accoring to the TAG_MASK_SPACE_PRIORITIES_REVERSE list. * @param masks The masks to flatten. */ export function flattenTagMasks(masks) { let result = {}; if (masks) { for (let space of TAG_MASK_SPACE_PRIORITIES_REVERSE) { if (!!masks[space]) { Object.assign(result, masks[space]); } } } return result; } /** * Constructs a new script bot for the given bot. * Script bots provide special behaviors by implemlementing getters and setters for tag values as well * as handling extra compatibility concerns like serialization. * * @param bot The bot. * @param manager The service that is able to track updates on a bot. * @param context The global context. */ export function createRuntimeBot(bot, manager) { if (!bot) { return null; } let replacement = null; const constantTags = { id: bot.id, space: getBotSpace(bot), }; let changedRawTags = {}; let rawTags = { ...bot.tags, }; let rawMasks = flattenTagMasks(bot.masks || {}); let rawLinks = {}; let changedMasks = {}; const arrayModifyMethods = new Set([ 'push', 'shift', 'unshift', 'pop', 'splice', 'fill', 'sort', ]); // const arrayModifyProperties = new Set(['length']); const wrapValue = (tag, value) => { if (Array.isArray(value)) { const isTagValue = () => value === manager.getRawValue(bot, tag); const isMaskValue = () => value === manager.getTagMask(bot, tag); const proxy = new Proxy(value, { get(target, key, proxy) { if (arrayModifyMethods.has(key)) { const func = Reflect.get(target, key, proxy); return function () { // eslint-disable-next-line prefer-rest-params const ret = func.apply(this, arguments); if (isMaskValue()) { updateTagMask(tag, value); } if (isTagValue()) { updateTag(tag, value); } return ret; }; } return Reflect.get(target, key, proxy); }, set(target, key, proxy) { const ret = Reflect.set(target, key, proxy); // if (arrayModifyProperties.has(key)) { if (isMaskValue()) { updateTagMask(tag, value); } if (isTagValue()) { updateTag(tag, value); } // } return ret; }, }); Object.defineProperty(proxy, ORIGINAL_OBJECT, { configurable: true, enumerable: false, writable: false, value: value, }); return proxy; } return value; }; const tagsProxy = new Proxy(rawTags, { get(target, key, proxy) { if (replacement) { return Reflect.get(replacement.tags, key, replacement.tags); } if (typeof key === 'symbol') { return Reflect.get(target, key, proxy); } if (key === 'toJSON') { return Reflect.get(target, key, proxy); } else if (key in constantTags) { return constantTags[key]; } return wrapValue(key, manager.getValue(bot, key)); }, set(target, key, value, receiver) { if (replacement) { return Reflect.set(replacement.tags, key, value, replacement.tags); } if (typeof key === 'symbol' && KNOWN_SYMBOLS.has(key)) { return Reflect.set(target, key, value, receiver); } if (key in constantTags) { return true; } updateTag(key, getOriginalObject(value)); return true; }, deleteProperty(target, key) { if (replacement) { return Reflect.deleteProperty(replacement.tags, key); } if (typeof key === 'symbol' && KNOWN_SYMBOLS.has(key)) { return Reflect.deleteProperty(target, key); } if (key in constantTags) { return true; } const value = null; updateTag(key, value); return true; }, ownKeys(target) { if (replacement) { return Reflect.ownKeys(replacement.tags); } const keys = Object.keys(bot.values); return addKnownSymbolsToList(target, keys); }, getOwnPropertyDescriptor(target, property) { if (replacement) { return Reflect.getOwnPropertyDescriptor(replacement.tags, property); } if (typeof property === 'symbol') { return Reflect.getOwnPropertyDescriptor(target, property); } if (property === 'toJSON') { return Reflect.getOwnPropertyDescriptor(target, property); } return Reflect.getOwnPropertyDescriptor(bot.values, property); }, }); const rawProxy = new Proxy(rawTags, { get(target, key, proxy) { if (replacement) { return Reflect.get(replacement.raw, key, replacement.raw); } if (typeof key === 'symbol') { return Reflect.get(target, key, proxy); } if (key in constantTags) { return constantTags[key]; } return manager.getRawValue(bot, key); }, set(target, key, value, receiver) { if (replacement) { return Reflect.set(replacement.raw, key, value, replacement.raw); } if (typeof key === 'symbol' && KNOWN_SYMBOLS.has(key)) { return Reflect.set(target, key, value, receiver); } if (key in constantTags) { return true; } updateTag(key, getOriginalObject(value)); return true; }, deleteProperty(target, key) { if (replacement) { return Reflect.deleteProperty(replacement.raw, key); } if (typeof key === 'symbol' && KNOWN_SYMBOLS.has(key)) { return Reflect.deleteProperty(target, key); } if (key in constantTags) { return true; } const value = null; updateTag(key, value); return true; }, ownKeys(target) { if (replacement) { return Reflect.ownKeys(replacement.raw); } const keys = Object.keys(bot.tags); return addKnownSymbolsToList(target, keys); }, getOwnPropertyDescriptor(target, property) { if (replacement) { return Reflect.getOwnPropertyDescriptor(replacement.raw, property); } if (typeof property === 'symbol') { return Reflect.getOwnPropertyDescriptor(target, property); } if (property === 'toJSON') { return Reflect.getOwnPropertyDescriptor(target, property); } return Reflect.getOwnPropertyDescriptor(bot.tags, property); }, }); const listenersProxy = new Proxy(bot.listeners, { get(target, key, proxy) { if (replacement) { return Reflect.get(replacement.listeners, key, replacement.listeners); } if (typeof key === 'symbol') { return Reflect.get(target, key, proxy); } if (key in constantTags) { return null; } return manager.getListener(bot, key); }, set(target, key, value, proxy) { if (replacement) { return Reflect.set(replacement.listeners, key, value, replacement.listeners); } if (typeof key === 'symbol') { return Reflect.set(target, key, value, proxy); } if (key in constantTags) { return true; } if (typeof value !== 'function' && value !== null && value !== undefined) { return false; } manager.setListener(bot, key, value !== null && value !== void 0 ? value : null); // Keep the bot listener keys and the listener override keys in sync. if (key in bot.listenerOverrides && !(key in bot.listeners)) { bot.listeners[key] = undefined; } else if (!hasValue(bot.listeners[key])) { delete bot.listeners[key]; } return true; }, deleteProperty(target, key) { if (replacement) { return Reflect.deleteProperty(replacement.listeners, key); } if (typeof key === 'symbol') { return Reflect.deleteProperty(target, key); } if (key in constantTags) { return true; } manager.setListener(bot, key, null); // Keep the bot listener keys and the listener override keys in sync. if (key in bot.listenerOverrides && !(key in bot.listeners)) { bot.listeners[key] = undefined; } else if (!hasValue(bot.listeners[key])) { delete bot.listeners[key]; } return true; }, }); const signatures = bot.signatures || {}; const signaturesProxy = new Proxy(signatures, { get(target, key, proxy) { if (replacement) { return Reflect.get(replacement.signatures, key, replacement.signatures); } if (typeof key === 'symbol') { return Reflect.get(target, key, proxy); } if (key in constantTags) { return constantTags[key]; } return manager.getSignature(bot, key); }, set(target, key, proxy) { return true; }, deleteProperty(target, key) { return true; }, ownKeys(target) { if (replacement) { return Reflect.ownKeys(replacement.signatures); } return Reflect.ownKeys(target); }, getOwnPropertyDescriptor(target, property) { if (replacement) { return Reflect.getOwnPropertyDescriptor(replacement.signatures, property); } return Reflect.getOwnPropertyDescriptor(target, property); }, }); const maskProxy = new Proxy(rawMasks, { get(target, key, proxy) { if (replacement) { return Reflect.get(replacement.masks, key, replacement.masks); } if (typeof key === 'symbol') { return Reflect.get(target, key, proxy); } return wrapValue(key, manager.getTagMask(bot, key)); }, set(target, key, value, proxy) { if (replacement) { return Reflect.set(replacement.masks, key, value, replacement.masks); } if (typeof key === 'symbol' && KNOWN_SYMBOLS.has(key)) { return Reflect.set(target, key, value, proxy); } if (key in constantTags) { return true; } updateTagMask(key, getOriginalObject(value)); return true; }, deleteProperty(target, key) { if (replacement) { return Reflect.deleteProperty(replacement.masks, key); } if (typeof key === 'symbol' && KNOWN_SYMBOLS.has(key)) { return Reflect.deleteProperty(target, key); } if (key in constantTags) { return true; } const spaces = getTagMaskSpaces(bot, key); const config = manager.updateTagMask(bot, key, spaces, null); if (config.mode === RealtimeEditMode.Immediate) { delete rawMasks[key]; } changeTagMask(key, config.changedValue, spaces); return true; }, ownKeys(target) { if (replacement) { return Reflect.ownKeys(replacement.masks); } const keys = Object.keys(flattenTagMasks(bot.masks)); return addKnownSymbolsToList(target, keys); }, getOwnPropertyDescriptor(target, property) { if (replacement) { return Reflect.getOwnPropertyDescriptor(replacement.masks, property); } if (typeof property === 'symbol') { return Reflect.getOwnPropertyDescriptor(target, property); } if (property === 'toJSON') { return Reflect.getOwnPropertyDescriptor(target, property); } const flat = flattenTagMasks(bot.masks); return Reflect.getOwnPropertyDescriptor(flat, property); }, }); const linkProxy = new Proxy(rawLinks, { set(target, key, value, proxy) { if (replacement) { return Reflect.set(replacement.links, key, value, replacement.links); } if (typeof key === 'symbol' && KNOWN_SYMBOLS.has(key)) { return Reflect.set(target, key, value, proxy); } if (key in constantTags) { return true; } if (isBot(value)) { updateTag(key, createBotLink([value.id])); } else if (Array.isArray(value)) { const tagValue = value.map((v) => (isBot(v) ? v.id : null)); updateTag(key, createBotLink(tagValue)); } else if (isBotLink(value)) { updateTag(key, value); } else if (typeof value === 'string') { updateTag(key, createBotLink([value])); } else if (!hasValue(value) && isBotLink(manager.getValue(bot, key))) { updateTag(key, value); } return true; }, get(target, key, proxy) { if (replacement) { return Reflect.get(replacement.links, key, replacement.links); } if (typeof key === 'symbol') { return Reflect.get(target, key, proxy); } if (key === 'toJSON') { return Reflect.get(target, key, proxy); } else if (key in constantTags) { return undefined; } return manager.getTagLink(bot, key); }, ownKeys(target) { if (replacement) { return Reflect.ownKeys(replacement.links); } const keys = Object.keys(bot.values); return addKnownSymbolsToList(target, keys.filter((key) => { return isBotLink(manager.getValue(bot, key)); })); }, deleteProperty(target, key) { if (replacement) { return Reflect.deleteProperty(replacement.links, key); } if (typeof key === 'symbol' && KNOWN_SYMBOLS.has(key)) { return Reflect.deleteProperty(target, key); } if (key in constantTags) { return true; } if (isBotLink(manager.getValue(bot, key))) { const value = null; updateTag(key, value); } return true; }, getOwnPropertyDescriptor(target, property) { if (replacement) { return Reflect.getOwnPropertyDescriptor(replacement.links, property); } if (typeof property === 'symbol') { return Reflect.getOwnPropertyDescriptor(target, property); } if (property === 'toJSON') { return Reflect.getOwnPropertyDescriptor(target, property); } if (isBotLink(manager.getValue(bot, property))) { return Reflect.getOwnPropertyDescriptor(bot.values, property); } return undefined; }, }); // Define a toJSON() function but // make it not enumerable so it is not included // in Object.keys() and for..in expressions. Object.defineProperty(tagsProxy, 'toJSON', { value: () => bot.tags, writable: false, enumerable: false, // This is so the function can be wrapped with another proxy // if needed. (Like for VM2Sandbox) configurable: true, }); Object.defineProperty(linkProxy, 'toJSON', { value: () => { const linkKeys = Object.keys(linkProxy); let result = {}; for (let key of linkKeys) { result[key] = manager.getValue(bot, key); } return result; }, writable: false, enumerable: false, configurable: true, }); let script = { id: bot.id, link: createBotLink([bot.id]), tags: tagsProxy, raw: rawProxy, masks: maskProxy, links: linkProxy, vars: {}, changes: changedRawTags, maskChanges: changedMasks, listeners: listenersProxy, signatures: signaturesProxy, [CLEAR_CHANGES_SYMBOL]: null, [SET_TAG_MASK_SYMBOL]: null, [GET_TAG_MASKS_SYMBOL]: null, [CLEAR_TAG_MASKS_SYMBOL]: null, [EDIT_TAG_SYMBOL]: null, [EDIT_TAG_MASK_SYMBOL]: null, [REPLACE_BOT_SYMBOL]: null, [ADD_BOT_LISTENER_SYMBOL]: null, [REMOVE_BOT_LISTENER_SYMBOL]: null, [GET_DYNAMIC_LISTENERS_SYMBOL]: null, }; Object.defineProperty(script, CLEAR_CHANGES_SYMBOL, { value: () => { changedRawTags = {}; changedMasks = {}; script.changes = changedRawTags; script.maskChanges = changedMasks; }, configurable: true, enumerable: false, writable: false, }); Object.defineProperty(script, SET_TAG_MASK_SYMBOL, { value: (key, value, space) => { if (key in constantTags) { return true; } const spaces = !hasValue(space) ? hasValue(value) ? [DEFAULT_TAG_MASK_SPACE] : getTagMaskSpaces(bot, key) : [space]; const valueToSet = getOriginalObject(value); const config = manager.updateTagMask(bot, key, spaces, valueToSet); if (config.mode === RealtimeEditMode.Immediate) { rawMasks[key] = valueToSet; } changeTagMask(key, config.changedValue, spaces); return value; }, configurable: true, enumerable: false, writable: false, }); Object.defineProperty(script, GET_TAG_MASKS_SYMBOL, { value: () => { let masks = {}; if (bot.masks) { for (let space in bot.masks) { let spaceMasks = {}; let hasSpaceMasks = false; const botMasks = bot.masks[space]; for (let tag in botMasks) { const val = botMasks[tag]; if (hasValue(val)) { hasSpaceMasks = true; spaceMasks[tag] = val; } } if (hasSpaceMasks) { masks[space] = spaceMasks; } } } return masks; }, configurable: true, enumerable: false, writable: false, }); Object.defineProperty(script, CLEAR_TAG_MASKS_SYMBOL, { value: (space) => { if (bot.masks) { let spaces = hasValue(space) ? [space] : TAG_MASK_SPACE_PRIORITIES; for (let space of spaces) { const tags = bot.masks[space]; for (let tag in tags) { script[SET_TAG_MASK_SYMBOL](tag, null, space); } } } }, configurable: true, enumerable: false, writable: false, }); Object.defineProperty(script, EDIT_TAG_SYMBOL, { value: (tag, ops) => { if (tag in constantTags) { return; } const e = remoteEdit(manager.currentVersion.vector, ...ops); script.tags[tag] = e; }, configurable: true, enumerable: false, writable: false, }); Object.defineProperty(script, EDIT_TAG_MASK_SYMBOL, { value: (tag, ops, space) => { if (tag in constantTags) { return; } const e = remoteEdit(manager.currentVersion.vector, ...ops); if (!hasValue(space)) { const availableSpaces = getTagMaskSpaces(bot, tag); if (availableSpaces.length <= 0) { space = DEFAULT_TAG_MASK_SPACE; } else { for (let possibleSpace of TAG_MASK_SPACE_PRIORITIES) { if (availableSpaces.indexOf(possibleSpace) >= 0) { space = possibleSpace; break; } } } } script[SET_TAG_MASK_SYMBOL](tag, e, space); }, configurable: true, enumerable: false, writable: false, }); Object.defineProperty(script, REPLACE_BOT_SYMBOL, { value: (bot) => { if (bot === scriptProxy) { throw new Error('Cannot replace a bot with itself!'); } if (!replacement) { replacement = bot; bot.vars = script.vars; } else { replacement[REPLACE_BOT_SYMBOL](bot); } }, configurable: true, enumerable: false, writable: false, }); Object.defineProperty(script, ADD_BOT_LISTENER_SYMBOL, { value: (tag, listener) => { manager.addDynamicListener(bot, tag, listener); }, configurable: true, enumerable: false, writable: false, }); Object.defineProperty(script, REMOVE_BOT_LISTENER_SYMBOL, { value: (tag, listener) => { manager.removeDynamicListener(bot, tag, listener); }, configurable: true, enumerable: false, writable: false, }); Object.defineProperty(script, GET_DYNAMIC_LISTENERS_SYMBOL, { value: (tag) => { return manager.getDynamicListeners(bot, tag); }, configurable: true, enumerable: false, writable: false, }); Object.defineProperty(script, 'toJSON', { value: () => { if ('space' in bot) { return { id: bot.id, space: bot.space, tags: tagsProxy, }; } else { return { id: bot.id, tags: tagsProxy, }; } }, writable: false, enumerable: false, // This is so the function can be wrapped with another proxy // if needed. (Like for VM2Sandbox) configurable: true, }); if (BOT_SPACE_TAG in bot) { script.space = bot.space; } const scriptProxy = new Proxy(script, { get(target, prop, reciever) { if (replacement) { return Reflect.get(replacement, prop, replacement); } if (prop in target) { return Reflect.get(target, prop, reciever); } else if (typeof prop === 'string') { const listener = manager.getListener(bot, prop); if (listener) { return listener; } } return undefined; }, }); return scriptProxy; function updateTag(tag, value) { const { mode, changedValue } = manager.updateTag(bot, tag, value); if (mode === RealtimeEditMode.Immediate) { rawTags[tag] = value; changeTag(tag, changedValue); } else if (mode === RealtimeEditMode.Delayed) { changeTag(tag, changedValue); } } function updateTagMask(tag, value) { const spaces = hasValue(value) ? [DEFAULT_TAG_MASK_SPACE] : getTagMaskSpaces(bot, tag); const { mode, changedValue } = manager.updateTagMask(bot, tag, spaces, value); if (mode === RealtimeEditMode.Immediate) { rawMasks[tag] = value; } changeTagMask(tag, changedValue, spaces); } function changeTag(tag, value) { if (isTagEdit(value)) { const currentValue = changedRawTags[tag]; if (isTagEdit(currentValue)) { value = mergeEdits(currentValue, value); } else if (hasValue(currentValue)) { value = applyTagEdit(currentValue, value); } } changedRawTags[tag] = value; } function changeTagMask(tag, value, spaces) { for (let space of spaces) { if (!changedMasks[space]) { changedMasks[space] = {}; } if (isTagEdit(value)) { const currentValue = changedMasks[space][tag]; if (isTagEdit(currentValue)) { value = mergeEdits(currentValue, value); } else if (hasValue(currentValue)) { value = applyTagEdit(currentValue, value); } } changedMasks[space][tag] = value; } } } /** * The list of possible realtime edit modes. */ export var RealtimeEditMode; (function (RealtimeEditMode) { /** * Specifies that bots in this edit mode cannot be edited. */ RealtimeEditMode[RealtimeEditMode["None"] = 0] = "None"; /** * Specifies that all changes to the bot will be accepted. * This allows the changes to be immediately used. */ RealtimeEditMode[RealtimeEditMode["Immediate"] = 1] = "Immediate"; /** * Specifies that some changes to the bot may be rejected. * This requires that changes be delayed until the related * partition accepts/denies them. */ RealtimeEditMode[RealtimeEditMode["Delayed"] = 2] = "Delayed"; })(RealtimeEditMode || (RealtimeEditMode = {})); //# sourceMappingURL=RuntimeBot.js.map