Skip to content

Commit f469d6f

Browse files
committed
add Free Proxy API
1 parent c466a8b commit f469d6f

File tree

10 files changed

+894
-10
lines changed

10 files changed

+894
-10
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,3 +660,9 @@ MIT license (see `LICENSE`).
660660

661661
Created by [Martin Kleppmann](https://martin.kleppmann.com/) and
662662
[many great contributors](https://github.com/automerge/automerge/graphs/contributors).
663+
664+
665+
# Proxy Free API
666+
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**.
667+
668+
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).

frontend/index.js

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const { OPTIONS, CACHE, STATE, OBJECT_ID, CONFLICTS, CHANGE, ELEM_IDS } = requir
22
const { isObject, copyObject } = require('../src/common')
33
const uuid = require('../src/uuid')
44
const { interpretPatch, cloneRootObject } = require('./apply_patch')
5-
const { rootObjectProxy } = require('./proxies')
5+
const { rootObjectProxy, setProxyFree } = require('./proxies')
66
const { Context } = require('./context')
77
const { Text } = require('./text')
88
const { Table } = require('./table')
@@ -160,6 +160,13 @@ function applyPatchToDoc(doc, patch, state, fromBackend) {
160160
return updateRootObject(doc, updated, state)
161161
}
162162

163+
/**
164+
* This function will set syntax defined by `ListProxyPolyfill`/`MapProxyPolyfill` as frontend interface
165+
*/
166+
function useProxyFreeAPI() {
167+
setProxyFree(true)
168+
}
169+
163170
/**
164171
* Creates an empty document object with no changes.
165172
*/
@@ -325,12 +332,21 @@ function applyPatch(doc, patch, backendState = undefined) {
325332
return updateRootObject(doc, {}, state)
326333
}
327334
}
335+
/**
336+
* Returns the Automerge value associated with `key` of the given object.
337+
*/
338+
function get(object, key) {
339+
if (typeof object.get === 'function') {
340+
return object.get(key)
341+
}
342+
return object[key]
343+
}
328344

329345
/**
330346
* Returns the Automerge object ID of the given object.
331347
*/
332348
function getObjectId(object) {
333-
return object[OBJECT_ID]
349+
return get(object, OBJECT_ID)
334350
}
335351

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

352368
/**
353369
* Returns the Automerge actor ID of the given document.
354370
*/
355371
function getActorId(doc) {
356-
return doc[STATE].actorId || doc[OPTIONS].actorId
372+
return get(doc, STATE).actorId || get(doc, OPTIONS).actorId
357373
}
358374

359375
/**
@@ -409,7 +425,7 @@ function getElementIds(list) {
409425
}
410426

411427
module.exports = {
412-
init, from, change, emptyChange, applyPatch,
428+
useProxyFreeAPI, init, from, change, emptyChange, applyPatch,
413429
getObjectId, getObjectById, getActorId, setActorId, getConflicts, getLastLocalChange,
414430
getBackendState, getElementIds,
415431
Text, Table, Counter, Observable, Float64, Int, Uint

frontend/proxies.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@ const { OBJECT_ID, CHANGE, STATE } = require('./constants')
22
const { createArrayOfNulls } = require('../src/common')
33
const { Text } = require('./text')
44
const { Table } = require('./table')
5+
const { ListProxyPolyfill, MapProxyPolyfill } = require('./proxy_polyfill')
6+
7+
/**
8+
* This variable express if interface will be defined by `ListProxyPolyfill`/`MapProxyPolyfill` (if `true`) or native `Proxy` (if `false`)
9+
*/
10+
let ProxyFree = false
11+
12+
/**
13+
* 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`)
14+
*/
15+
function setProxyFree(value) {
16+
ProxyFree = value
17+
}
518

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

3245
indexOf(o, start = 0) {
33-
const id = o[OBJECT_ID]
46+
let id = o[OBJECT_ID]
47+
if (typeof o.get === 'function') {
48+
id = o.get(OBJECT_ID)
49+
}
3450
if (id) {
3551
const list = context.getObject(listId)
3652
for (let index = start; index < list.length; index++) {
@@ -231,10 +247,16 @@ const ListHandler = {
231247
}
232248

233249
function mapProxy(context, objectId, path, readonly) {
250+
if (ProxyFree) {
251+
return new MapProxyPolyfill({context, objectId, path, readonly}, MapHandler)
252+
}
234253
return new Proxy({context, objectId, path, readonly}, MapHandler)
235254
}
236255

237256
function listProxy(context, objectId, path) {
257+
if (ProxyFree) {
258+
return new ListProxyPolyfill([context, objectId, path], ListHandler, listMethods)
259+
}
238260
return new Proxy([context, objectId, path], ListHandler)
239261
}
240262

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

263-
module.exports = { rootObjectProxy }
285+
module.exports = { rootObjectProxy, setProxyFree }

frontend/proxy_polyfill.js

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/**
2+
* ProxyPolyfill is a dump wrapper for `handler`
3+
* where `target` is a map and is always passed as parameter.
4+
*/
5+
class MapProxyPolyfill {
6+
/**
7+
* Creates ProxyPolyfill and defines methos dynamically.
8+
* All methods are a dump wrapper to `handler` methods with `target` as first parameter.
9+
*/
10+
constructor(target, handler) {
11+
this.target = target
12+
for (const item in handler) {
13+
if (Object.prototype.hasOwnProperty.call(handler, item)) {
14+
this[item] = (...args) => handler[item](this.target, ...args)
15+
}
16+
}
17+
18+
19+
// Implements `getOwnPropertyNames` method for wrapped class.
20+
// This is needed because it is not possible to override `Object.getOwnPropertyNames()` without a `Proxy`.
21+
//
22+
// This method is a dump wrapper of `ownKey()` so it must be created only if the handle has `ownKey()` method.
23+
// (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/ownKeys for more info)
24+
if (typeof handler.ownKeys === 'function') {
25+
this.getOwnPropertyNames = () => handler.ownKeys(this.target)
26+
}
27+
28+
// Implements `assign` method for wrapped class.
29+
// This is needed because it is not possible to override `Object.assign()` without a `Proxy`.
30+
if (typeof handler.set === 'function') {
31+
this.assign = (object) => {
32+
Object.keys(object).forEach(function(key) {
33+
handler.set(target, key, object[key])
34+
})
35+
}
36+
}
37+
}
38+
39+
iterator () {
40+
// NOTE: this method used to be a generator; it has been converted to a regular
41+
// method (that mimics the interface of a generator) to avoid having to include
42+
// generator polyfills in the distribution build.
43+
// eslint-disable-next-line consistent-this
44+
const doc = this
45+
let keys = doc.ownKeys()
46+
let index = 0
47+
return {
48+
next () {
49+
let key = keys[index]
50+
if (!key) return { value: undefined, done: true }
51+
index = index + 1
52+
return {value: [key, doc.get(key)], done: false}
53+
},
54+
[Symbol.iterator]: () => this.iterator(),
55+
}
56+
}
57+
58+
/**
59+
* Defines iterator. Iterates the map's key and values
60+
*/
61+
[Symbol.iterator] () {
62+
return this.iterator()
63+
}
64+
65+
/**
66+
* To be used by JSON.stringify() function.
67+
* It returns the wrapped instance.
68+
* (more info https://javascript.info/json#custom-tojson)
69+
*/
70+
toJSON () {
71+
const { context, objectId } = this.target
72+
let object = context.getObject(objectId)
73+
return object
74+
}
75+
76+
/**
77+
* Implements isArray method for wrapped class.
78+
* This is needed because it is not possible to override Array.isArray() without a Proxy.
79+
*/
80+
isArray () {
81+
return false
82+
}
83+
}
84+
85+
/**
86+
* ListProxyPolyfill is a dump wrapper for `handler`
87+
* where `target` is an array and is always passed as parameter.
88+
*/
89+
class ListProxyPolyfill {
90+
/**
91+
* Creates ListProxyPolyfill and defines methos dynamically.
92+
* All methods are a dump wrapper to `handler` methods with `target` as first parameter.
93+
*/
94+
constructor(target, handler, listMethods) {
95+
this.target = target
96+
for (const item in handler) {
97+
if (Object.prototype.hasOwnProperty.call(handler, item)) {
98+
this[item] = (...args) => handler[item](this.target, ...args)
99+
}
100+
}
101+
102+
// Casts `key` to string before calling `handler`s `get` method.
103+
// This is needed because Proxy does so and the handler is prepared for that.
104+
this.get = (key) => {
105+
if (typeof key == 'number') {
106+
key = key.toString()
107+
}
108+
return handler.get(this.target, key)
109+
}
110+
111+
// Casts `key` to string before calling `handler`s `get` method.
112+
// This is needed because Proxy does so and the handler is prepared for that.
113+
this.has = (key) => {
114+
if (typeof key == 'number') {
115+
key = key.toString()
116+
}
117+
return handler.has(this.target, key)
118+
}
119+
120+
121+
// Implements `objectKeys` method for wrapped class.
122+
// This is needed because it is not possible to override `Object.keys()` without a `Proxy`.
123+
//
124+
// This method returns only enumerable property names.
125+
// (more info https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys)
126+
if (typeof handler.ownKeys === 'function' && typeof handler.getOwnPropertyDescriptor === 'function') {
127+
this.objectKeys = () => {
128+
let keys = []
129+
for (let key of handler.ownKeys(this.target)) {
130+
let description = handler.getOwnPropertyDescriptor(this.target, key)
131+
if (description.enumerable) {
132+
keys.push(key)
133+
}
134+
}
135+
return keys
136+
}
137+
}
138+
139+
// Implements `getOwnPropertyNames` method for wrapped class.
140+
// This is needed because it is not possible to override `Object.getOwnPropertyNames()` without a `Proxy`.
141+
//
142+
// This method is a dump wrapper of `ownKey()` so it must be created only if the handle has `ownKey()` method.
143+
// (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/ownKeys for more info)
144+
if (typeof handler.ownKeys === 'function') {
145+
this.getOwnPropertyNames = () => handler.ownKeys(this.target)
146+
}
147+
148+
// Defines same methods as listMethods
149+
// All methods are a dump wrapper to the ones defined on listMethods.
150+
const [context, objectId, path] = target
151+
const _listMethods = listMethods(context, objectId, path)
152+
for (const methodName in _listMethods) {
153+
if (Object.prototype.hasOwnProperty.call(_listMethods, methodName)) {
154+
this[methodName] = (...args) => _listMethods[methodName](...args)
155+
}
156+
}
157+
}
158+
159+
iterator () {
160+
// NOTE: this method used to be a generator; it has been converted to a regular
161+
// method (that mimics the interface of a generator) to avoid having to include
162+
// generator polyfills in the distribution build.
163+
// eslint-disable-next-line consistent-this
164+
let doc = this
165+
let keysIterator = doc.keys()
166+
return {
167+
next () {
168+
let nextKey = keysIterator.next()
169+
if (nextKey.done) return nextKey
170+
return {value: doc.get(nextKey.value), done: false}
171+
},
172+
[Symbol.iterator]: () => this.iterator(),
173+
}
174+
}
175+
176+
/**
177+
* Defines iterator. Iterates the array's values
178+
*/
179+
[Symbol.iterator] () {
180+
return this.iterator()
181+
}
182+
183+
/**
184+
* Implements isArray method for wrapped class.
185+
* This is needed because it is not possible to override Array.isArray() without a Proxy.
186+
*/
187+
isArray () {
188+
return true
189+
}
190+
191+
/**
192+
* Implements length method for wrapped class.
193+
* This is needed because it is not possible to override .length without a Proxy.
194+
*/
195+
length () {
196+
const [context, objectId, /* path */] = this.target
197+
const object = context.getObject(objectId)
198+
return object.length
199+
}
200+
201+
/**
202+
* To be used by JSON.stringify() function.
203+
* It returns the wrapped instance.
204+
* (more info https://javascript.info/json#custom-tojson)
205+
*/
206+
toJSON () {
207+
const [ context, objectId ] = this.target
208+
let object = context.getObject(objectId)
209+
return object
210+
}
211+
}
212+
213+
214+
module.exports = { ListProxyPolyfill, MapProxyPolyfill }

0 commit comments

Comments
 (0)