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 f0d1f16
Show file tree
Hide file tree
Showing 11 changed files with 1,016 additions and 19 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).
46 changes: 37 additions & 9 deletions backend/encoding.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,47 @@

/**
* UTF-8 decoding and encoding using API that is supported in Node >= 12 and modern browsers:
* https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encode
* https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/decode
* If you're running in an environment where it's not available, please use a polyfill, such as:
* https://github.com/anonyco/FastestSmallestTextEncoderDecoder
* UTF-8 decoding and encoding
*/
const utf8encoder = new TextEncoder()
const utf8decoder = new TextDecoder('utf-8')

function stringToUtf8(string) {
return utf8encoder.encode(string)
if (typeof TextEncoder === 'function' && typeof TextDecoder === 'function') {
// Modern web browsers:
// https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encode
const utf8encoder = new TextEncoder()
return utf8encoder.encode(string)

} else if (typeof Buffer === 'function') {
// Node.js:
// https://nodejs.org/api/buffer.html
return Buffer.from(string, 'utf8')

} else {
// Could use a polyfill? e.g. https://github.com/anonyco/FastestSmallestTextEncoderDecoder
throw new Error('Platform does not provide UTF-8 encoding/decoding feature')
}
}

function utf8ToString(buffer) {
return utf8decoder.decode(buffer)
if (typeof TextEncoder === 'function' && typeof TextDecoder === 'function') {
// Modern web browsers:
// https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/decode
const utf8decoder = new TextDecoder('utf-8')
return utf8decoder.decode(buffer)

} else if (typeof Buffer === 'function') {
// Node.js:
// https://nodejs.org/api/string_decoder.html
const { StringDecoder } = require('string_decoder')
const utf8decoder = new StringDecoder('utf8')
// In Node >= 10 we can simply do "utf8decoder.end(buffer)". However, in Node 8 there
// is a bug that causes an Uint8Array to be incorrectly decoded when passed directly to
// StringDecoder.end(). Wrapping in an additional "Buffer.from()" works around this bug.
return utf8decoder.end(Buffer.from(buffer))

} else {
// Could use a polyfill? e.g. https://github.com/anonyco/FastestSmallestTextEncoderDecoder
throw new Error('Platform does not provide UTF-8 encoding/decoding feature')
}
}

/**
Expand Down
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 }
Loading

0 comments on commit f0d1f16

Please sign in to comment.