@casual-simulation/aux-runtime
Version:
Runtime for AUX projects
807 lines • 29.2 kB
JavaScript
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