Skip to content

Commit

Permalink
add Free Proxy API
Browse files Browse the repository at this point in the history
  • Loading branch information
camiboj committed Oct 8, 2021
1 parent c466a8b commit f469d6f
Show file tree
Hide file tree
Showing 10 changed files with 894 additions and 10 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -660,3 +660,9 @@ MIT license (see `LICENSE`).

Created by [Martin Kleppmann](https://martin.kleppmann.com/) and
[many great contributors](https://github.com/automerge/automerge/graphs/contributors).


# Proxy Free API
Automerge uses JS Proxy extensively for its front-end API. However, to be able to support multiple JS runtime which does not support `Proxy` you can use the **Proxy Free API**.

To use the Proxy Free API, you will only need to change a flag by calling `Automerge.useProxyFreeAPI()`. Read more documentation on this API on [`proxy_free.md`]https://github.com/automerge/automerge/blob/main/proxy_free.md).
28 changes: 22 additions & 6 deletions frontend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const { OPTIONS, CACHE, STATE, OBJECT_ID, CONFLICTS, CHANGE, ELEM_IDS } = requir
const { isObject, copyObject } = require('../src/common')
const uuid = require('../src/uuid')
const { interpretPatch, cloneRootObject } = require('./apply_patch')
const { rootObjectProxy } = require('./proxies')
const { rootObjectProxy, setProxyFree } = require('./proxies')
const { Context } = require('./context')
const { Text } = require('./text')
const { Table } = require('./table')
Expand Down Expand Up @@ -160,6 +160,13 @@ function applyPatchToDoc(doc, patch, state, fromBackend) {
return updateRootObject(doc, updated, state)
}

/**
* This function will set syntax defined by `ListProxyPolyfill`/`MapProxyPolyfill` as frontend interface
*/
function useProxyFreeAPI() {
setProxyFree(true)
}

/**
* Creates an empty document object with no changes.
*/
Expand Down Expand Up @@ -325,12 +332,21 @@ function applyPatch(doc, patch, backendState = undefined) {
return updateRootObject(doc, {}, state)
}
}
/**
* Returns the Automerge value associated with `key` of the given object.
*/
function get(object, key) {
if (typeof object.get === 'function') {
return object.get(key)
}
return object[key]
}

/**
* Returns the Automerge object ID of the given object.
*/
function getObjectId(object) {
return object[OBJECT_ID]
return get(object, OBJECT_ID)
}

/**
Expand All @@ -343,17 +359,17 @@ function getObjectById(doc, objectId) {
// However, that requires knowing the path from the root to the current
// object, which we don't have if we jumped straight to the object by its ID.
// If we maintained an index from object ID to parent ID we could work out the path.
if (doc[CHANGE]) {
if (get(doc, CHANGE)) {
throw new TypeError('Cannot use getObjectById in a change callback')
}
return doc[CACHE][objectId]
return get(get(doc, CACHE), objectId)
}

/**
* Returns the Automerge actor ID of the given document.
*/
function getActorId(doc) {
return doc[STATE].actorId || doc[OPTIONS].actorId
return get(doc, STATE).actorId || get(doc, OPTIONS).actorId
}

/**
Expand Down Expand Up @@ -409,7 +425,7 @@ function getElementIds(list) {
}

module.exports = {
init, from, change, emptyChange, applyPatch,
useProxyFreeAPI, init, from, change, emptyChange, applyPatch,
getObjectId, getObjectById, getActorId, setActorId, getConflicts, getLastLocalChange,
getBackendState, getElementIds,
Text, Table, Counter, Observable, Float64, Int, Uint
Expand Down
26 changes: 24 additions & 2 deletions frontend/proxies.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@ const { OBJECT_ID, CHANGE, STATE } = require('./constants')
const { createArrayOfNulls } = require('../src/common')
const { Text } = require('./text')
const { Table } = require('./table')
const { ListProxyPolyfill, MapProxyPolyfill } = require('./proxy_polyfill')

/**
* This variable express if interface will be defined by `ListProxyPolyfill`/`MapProxyPolyfill` (if `true`) or native `Proxy` (if `false`)
*/
let ProxyFree = false

/**
* This function will set global varible `ProxyFree` which will express if interface will be defined by `ListProxyPolyfill`/`MapProxyPolyfill` (if `true`) or native `Proxy` (if `false`)
*/
function setProxyFree(value) {
ProxyFree = value
}

function parseListIndex(key) {
if (typeof key === 'string' && /^[0-9]+$/.test(key)) key = parseInt(key, 10)
Expand Down Expand Up @@ -30,7 +43,10 @@ function listMethods(context, listId, path) {
},

indexOf(o, start = 0) {
const id = o[OBJECT_ID]
let id = o[OBJECT_ID]
if (typeof o.get === 'function') {
id = o.get(OBJECT_ID)
}
if (id) {
const list = context.getObject(listId)
for (let index = start; index < list.length; index++) {
Expand Down Expand Up @@ -231,10 +247,16 @@ const ListHandler = {
}

function mapProxy(context, objectId, path, readonly) {
if (ProxyFree) {
return new MapProxyPolyfill({context, objectId, path, readonly}, MapHandler)
}
return new Proxy({context, objectId, path, readonly}, MapHandler)
}

function listProxy(context, objectId, path) {
if (ProxyFree) {
return new ListProxyPolyfill([context, objectId, path], ListHandler, listMethods)
}
return new Proxy([context, objectId, path], ListHandler)
}

Expand All @@ -260,4 +282,4 @@ function rootObjectProxy(context) {
return mapProxy(context, '_root', [])
}

module.exports = { rootObjectProxy }
module.exports = { rootObjectProxy, setProxyFree }
214 changes: 214 additions & 0 deletions frontend/proxy_polyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/**
* ProxyPolyfill is a dump wrapper for `handler`
* where `target` is a map and is always passed as parameter.
*/
class MapProxyPolyfill {
/**
* Creates ProxyPolyfill and defines methos dynamically.
* All methods are a dump wrapper to `handler` methods with `target` as first parameter.
*/
constructor(target, handler) {
this.target = target
for (const item in handler) {
if (Object.prototype.hasOwnProperty.call(handler, item)) {
this[item] = (...args) => handler[item](this.target, ...args)
}
}


// Implements `getOwnPropertyNames` method for wrapped class.
// This is needed because it is not possible to override `Object.getOwnPropertyNames()` without a `Proxy`.
//
// This method is a dump wrapper of `ownKey()` so it must be created only if the handle has `ownKey()` method.
// (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/ownKeys for more info)
if (typeof handler.ownKeys === 'function') {
this.getOwnPropertyNames = () => handler.ownKeys(this.target)
}

// Implements `assign` method for wrapped class.
// This is needed because it is not possible to override `Object.assign()` without a `Proxy`.
if (typeof handler.set === 'function') {
this.assign = (object) => {
Object.keys(object).forEach(function(key) {
handler.set(target, key, object[key])
})
}
}
}

iterator () {
// NOTE: this method used to be a generator; it has been converted to a regular
// method (that mimics the interface of a generator) to avoid having to include
// generator polyfills in the distribution build.
// eslint-disable-next-line consistent-this
const doc = this
let keys = doc.ownKeys()
let index = 0
return {
next () {
let key = keys[index]
if (!key) return { value: undefined, done: true }
index = index + 1
return {value: [key, doc.get(key)], done: false}
},
[Symbol.iterator]: () => this.iterator(),
}
}

/**
* Defines iterator. Iterates the map's key and values
*/
[Symbol.iterator] () {
return this.iterator()
}

/**
* To be used by JSON.stringify() function.
* It returns the wrapped instance.
* (more info https://javascript.info/json#custom-tojson)
*/
toJSON () {
const { context, objectId } = this.target
let object = context.getObject(objectId)
return object
}

/**
* Implements isArray method for wrapped class.
* This is needed because it is not possible to override Array.isArray() without a Proxy.
*/
isArray () {
return false
}
}

/**
* ListProxyPolyfill is a dump wrapper for `handler`
* where `target` is an array and is always passed as parameter.
*/
class ListProxyPolyfill {
/**
* Creates ListProxyPolyfill and defines methos dynamically.
* All methods are a dump wrapper to `handler` methods with `target` as first parameter.
*/
constructor(target, handler, listMethods) {
this.target = target
for (const item in handler) {
if (Object.prototype.hasOwnProperty.call(handler, item)) {
this[item] = (...args) => handler[item](this.target, ...args)
}
}

// Casts `key` to string before calling `handler`s `get` method.
// This is needed because Proxy does so and the handler is prepared for that.
this.get = (key) => {
if (typeof key == 'number') {
key = key.toString()
}
return handler.get(this.target, key)
}

// Casts `key` to string before calling `handler`s `get` method.
// This is needed because Proxy does so and the handler is prepared for that.
this.has = (key) => {
if (typeof key == 'number') {
key = key.toString()
}
return handler.has(this.target, key)
}


// Implements `objectKeys` method for wrapped class.
// This is needed because it is not possible to override `Object.keys()` without a `Proxy`.
//
// This method returns only enumerable property names.
// (more info https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys)
if (typeof handler.ownKeys === 'function' && typeof handler.getOwnPropertyDescriptor === 'function') {
this.objectKeys = () => {
let keys = []
for (let key of handler.ownKeys(this.target)) {
let description = handler.getOwnPropertyDescriptor(this.target, key)
if (description.enumerable) {
keys.push(key)
}
}
return keys
}
}

// Implements `getOwnPropertyNames` method for wrapped class.
// This is needed because it is not possible to override `Object.getOwnPropertyNames()` without a `Proxy`.
//
// This method is a dump wrapper of `ownKey()` so it must be created only if the handle has `ownKey()` method.
// (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/ownKeys for more info)
if (typeof handler.ownKeys === 'function') {
this.getOwnPropertyNames = () => handler.ownKeys(this.target)
}

// Defines same methods as listMethods
// All methods are a dump wrapper to the ones defined on listMethods.
const [context, objectId, path] = target
const _listMethods = listMethods(context, objectId, path)
for (const methodName in _listMethods) {
if (Object.prototype.hasOwnProperty.call(_listMethods, methodName)) {
this[methodName] = (...args) => _listMethods[methodName](...args)
}
}
}

iterator () {
// NOTE: this method used to be a generator; it has been converted to a regular
// method (that mimics the interface of a generator) to avoid having to include
// generator polyfills in the distribution build.
// eslint-disable-next-line consistent-this
let doc = this
let keysIterator = doc.keys()
return {
next () {
let nextKey = keysIterator.next()
if (nextKey.done) return nextKey
return {value: doc.get(nextKey.value), done: false}
},
[Symbol.iterator]: () => this.iterator(),
}
}

/**
* Defines iterator. Iterates the array's values
*/
[Symbol.iterator] () {
return this.iterator()
}

/**
* Implements isArray method for wrapped class.
* This is needed because it is not possible to override Array.isArray() without a Proxy.
*/
isArray () {
return true
}

/**
* Implements length method for wrapped class.
* This is needed because it is not possible to override .length without a Proxy.
*/
length () {
const [context, objectId, /* path */] = this.target
const object = context.getObject(objectId)
return object.length
}

/**
* To be used by JSON.stringify() function.
* It returns the wrapped instance.
* (more info https://javascript.info/json#custom-tojson)
*/
toJSON () {
const [ context, objectId ] = this.target
let object = context.getObject(objectId)
return object
}
}


module.exports = { ListProxyPolyfill, MapProxyPolyfill }
Loading

0 comments on commit f469d6f

Please sign in to comment.