Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proxy Free API #439

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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