frontend/proxies.js (223 lines of code) (raw):

const { OBJECT_ID, CHANGE, STATE } = require('./constants') const { createArrayOfNulls } = require('../src/common') const { Text } = require('./text') const { Table } = require('./table') function parseListIndex(key) { if (typeof key === 'string' && /^[0-9]+$/.test(key)) key = parseInt(key, 10) if (typeof key !== 'number') { throw new TypeError('A list index must be a number, but you passed ' + JSON.stringify(key)) } if (key < 0 || isNaN(key) || key === Infinity || key === -Infinity) { throw new RangeError('A list index must be positive, but you passed ' + key) } return key } function listMethods(context, listId, path) { const methods = { deleteAt(index, numDelete) { context.splice(path, parseListIndex(index), numDelete || 1, []) return this }, fill(value, start, end) { let list = context.getObject(listId) for (let index = parseListIndex(start || 0); index < parseListIndex(end || list.length); index++) { context.setListIndex(path, index, value) } return this }, indexOf(o, start = 0) { const id = o[OBJECT_ID] if (id) { const list = context.getObject(listId) for (let index = start; index < list.length; index++) { if (list[index][OBJECT_ID] === id) { return index } } return -1 } else { return context.getObject(listId).indexOf(o, start) } }, insertAt(index, ...values) { context.splice(path, parseListIndex(index), 0, values) return this }, pop() { let list = context.getObject(listId) if (list.length == 0) return const last = context.getObjectField(path, listId, list.length - 1) context.splice(path, list.length - 1, 1, []) return last }, push(...values) { let list = context.getObject(listId) context.splice(path, list.length, 0, values) // need to getObject() again because the list object above may be immutable return context.getObject(listId).length }, shift() { let list = context.getObject(listId) if (list.length == 0) return const first = context.getObjectField(path, listId, 0) context.splice(path, 0, 1, []) return first }, splice(start, deleteCount, ...values) { let list = context.getObject(listId) start = parseListIndex(start) if (deleteCount === undefined || deleteCount > list.length - start) { deleteCount = list.length - start } const deleted = [] for (let n = 0; n < deleteCount; n++) { deleted.push(context.getObjectField(path, listId, start + n)) } context.splice(path, start, deleteCount, values) return deleted }, unshift(...values) { context.splice(path, 0, 0, values) return context.getObject(listId).length } } for (let iterator of ['entries', 'keys', 'values']) { let list = context.getObject(listId) methods[iterator] = () => list[iterator]() } // Read-only methods that can delegate to the JavaScript built-in implementations for (let method of ['concat', 'every', 'filter', 'find', 'findIndex', 'forEach', 'includes', 'join', 'lastIndexOf', 'map', 'reduce', 'reduceRight', 'slice', 'some', 'toLocaleString', 'toString']) { methods[method] = (...args) => { const list = context.getObject(listId) .map((item, index) => context.getObjectField(path, listId, index)) return list[method](...args) } } return methods } const MapHandler = { get (target, key) { const { context, objectId, path } = target if (key === OBJECT_ID) return objectId if (key === CHANGE) return context if (key === STATE) return {actorId: context.actorId} return context.getObjectField(path, objectId, key) }, set (target, key, value) { const { context, path, readonly } = target if (Array.isArray(readonly) && readonly.indexOf(key) >= 0) { throw new RangeError(`Object property "${key}" cannot be modified`) } context.setMapKey(path, key, value) return true }, deleteProperty (target, key) { const { context, path, readonly } = target if (Array.isArray(readonly) && readonly.indexOf(key) >= 0) { throw new RangeError(`Object property "${key}" cannot be modified`) } context.deleteMapKey(path, key) return true }, has (target, key) { const { context, objectId } = target return [OBJECT_ID, CHANGE].includes(key) || (key in context.getObject(objectId)) }, getOwnPropertyDescriptor (target, key) { const { context, objectId } = target const object = context.getObject(objectId) if (key in object) { return { configurable: true, enumerable: true, value: context.getObjectField(objectId, key) } } }, ownKeys (target) { const { context, objectId } = target return Object.keys(context.getObject(objectId)) } } const ListHandler = { get (target, key) { const [context, objectId, path] = target if (key === Symbol.iterator) return context.getObject(objectId)[Symbol.iterator] if (key === OBJECT_ID) return objectId if (key === CHANGE) return context if (key === 'length') return context.getObject(objectId).length if (typeof key === 'string' && /^[0-9]+$/.test(key)) { return context.getObjectField(path, objectId, parseListIndex(key)) } return listMethods(context, objectId, path)[key] }, set (target, key, value) { const [context, objectId, path] = target if (key === 'length') { if (typeof value !== 'number') { throw new RangeError("Invalid array length") } const length = context.getObject(objectId).length if (length > value) { context.splice(path, value, length - value, []) } else { context.splice(path, length, 0, createArrayOfNulls(value - length)) } } else { context.setListIndex(path, parseListIndex(key), value) } return true }, deleteProperty (target, key) { const [context, /* objectId */, path] = target context.splice(path, parseListIndex(key), 1, []) return true }, has (target, key) { const [context, objectId, /* path */] = target if (typeof key === 'string' && /^[0-9]+$/.test(key)) { return parseListIndex(key) < context.getObject(objectId).length } return ['length', OBJECT_ID, CHANGE].includes(key) }, getOwnPropertyDescriptor (target, key) { const [context, objectId, /* path */] = target const object = context.getObject(objectId) if (key === 'length') return {writable: true, value: object.length} if (key === OBJECT_ID) return {configurable: false, enumerable: false, value: objectId} if (typeof key === 'string' && /^[0-9]+$/.test(key)) { const index = parseListIndex(key) if (index < object.length) return { configurable: true, enumerable: true, value: context.getObjectField(objectId, index) } } }, ownKeys (target) { const [context, objectId, /* path */] = target const object = context.getObject(objectId) let keys = ['length'] for (let key of Object.keys(object)) keys.push(key) return keys } } function mapProxy(context, objectId, path, readonly) { return new Proxy({context, objectId, path, readonly}, MapHandler) } function listProxy(context, objectId, path) { return new Proxy([context, objectId, path], ListHandler) } /** * Instantiates a proxy object for the given `objectId`. * This function is added as a method to the context object by rootObjectProxy(). * When it is called, `this` is the context object. * `readonly` is a list of map property names that cannot be modified. */ function instantiateProxy(path, objectId, readonly) { const object = this.getObject(objectId) if (Array.isArray(object)) { return listProxy(this, objectId, path) } else if (object instanceof Text || object instanceof Table) { return object.getWriteable(this, path) } else { return mapProxy(this, objectId, path, readonly) } } function rootObjectProxy(context) { context.instantiateObject = instantiateProxy return mapProxy(context, '_root', []) } module.exports = { rootObjectProxy }