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

[RFC] Add alternative frontend API without proxy #438

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
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
29 changes: 23 additions & 6 deletions frontend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ 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, setFacebookSyntax } = require('./proxies')
const { Context } = require('./context')
const { Text } = require('./text')
const { Table } = require('./table')
const { Counter } = require('./counter')
const { Float64, Int, Uint } = require('./numbers')
const { Observable } = require('./observable')
const { ProxyPolyfill } = require('./proxy_polyfill')

/**
* Actor IDs must consist only of hexadecimal digits so that they can be encoded
Expand Down Expand Up @@ -160,6 +161,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 facebookSyntaxOn() {
setFacebookSyntax(true)
}

/**
* Creates an empty document object with no changes.
*/
Expand Down Expand Up @@ -325,12 +333,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 +360,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 +426,7 @@ function getElementIds(list) {
}

module.exports = {
init, from, change, emptyChange, applyPatch,
facebookSyntaxOn, 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`)
*/
var facebookSyntax = false

/**
* This function will set global varible `facebookSyntax` which will express if interface will be defined by `ListProxyPolyfill`/`MapProxyPolyfill` (if `true`) or native `Proxy` (if `false`)
*/
function setFacebookSyntax(value) {
facebookSyntax = 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 (facebookSyntax) {
return new MapProxyPolyfill({context, objectId, path, readonly}, MapHandler)
}
return new Proxy({context, objectId, path, readonly}, MapHandler)
}

function listProxy(context, objectId, path) {
if (facebookSyntax) {
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, setFacebookSyntax }
215 changes: 215 additions & 0 deletions frontend/proxy_polyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/**
* 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) {
this[item] = (...args) => {
return 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 = () => {
return 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.
let 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) {
this[item] = (...args) => {
return 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 = () => {
return 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
for (const methodName in listMethods(context, objectId, path)) {
this[methodName] = (...args) => {
const [context, objectId, path] = target
return listMethods(context, objectId, path)[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.
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 }
9 changes: 8 additions & 1 deletion src/automerge.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ const { encodeChange, decodeChange } = require('../backend/columnar')
const { isObject } = require('./common')
let backend = require('../backend') // mutable: can be overridden with setDefaultBackend()

/**
* This function will set syntax defined by `ListProxyPolyfill`/`MapProxyPolyfill` as frontend interface
*/
function facebookSyntaxOn() {
Frontend.facebookSyntaxOn()
}

/**
* Automerge.* API
* The functions in this file constitute the publicly facing Automerge API which combines
Expand Down Expand Up @@ -152,7 +159,7 @@ function setDefaultBackend(newBackend) {
}

module.exports = {
init, from, change, emptyChange, clone, free,
facebookSyntaxOn, init, from, change, emptyChange, clone, free,
load, save, merge, getChanges, getAllChanges, applyChanges,
encodeChange, decodeChange, equals, getHistory, uuid,
Frontend, setDefaultBackend, generateSyncMessage, receiveSyncMessage, initSyncState,
Expand Down
Loading