Skip to content

Commit 6a67ac0

Browse files
committed
Minify dist file
1 parent a674d60 commit 6a67ac0

File tree

4 files changed

+411
-494
lines changed

4 files changed

+411
-494
lines changed

dist/jsonpatcherproxy.js

Lines changed: 391 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,391 @@
1+
'use strict';
2+
/*!
3+
* https://github.com/Palindrom/JSONPatcherProxy
4+
* JSONPatcherProxy version: 0.0.8
5+
* (c) 2017 Starcounter
6+
* MIT license
7+
*/
8+
9+
/** Class representing a JS Object observer */
10+
const JSONPatcherProxy = (function() {
11+
/**
12+
* Deep clones your object and returns a new object.
13+
*/
14+
function deepClone(obj) {
15+
switch (typeof obj) {
16+
case 'object':
17+
return JSON.parse(JSON.stringify(obj)); //Faster than ES5 clone - http://jsperf.com/deep-cloning-of-objects/5
18+
case 'undefined':
19+
return null; //this is how JSON.stringify behaves for array items
20+
default:
21+
return obj; //no need to clone primitives
22+
}
23+
}
24+
JSONPatcherProxy.deepClone = deepClone;
25+
26+
function escapePathComponent(str) {
27+
if (str.indexOf('/') == -1 && str.indexOf('~') == -1) return str;
28+
return str.replace(/~/g, '~0').replace(/\//g, '~1');
29+
}
30+
JSONPatcherProxy.escapePathComponent = escapePathComponent;
31+
32+
/**
33+
* Walk up the parenthood tree to get the path
34+
* @param {JSONPatcherProxy} instance
35+
* @param {Object} obj the object you need to find its path
36+
*/
37+
function findObjectPath(instance, obj) {
38+
const pathComponents = [];
39+
let parentAndPath = instance.parenthoodMap.get(obj);
40+
while (parentAndPath && parentAndPath.path) {
41+
// because we're walking up-tree, we need to use the array as a stack
42+
pathComponents.unshift(parentAndPath.path);
43+
parentAndPath = instance.parenthoodMap.get(parentAndPath.parent);
44+
}
45+
if (pathComponents.length) {
46+
const path = pathComponents.join('/');
47+
return '/' + path;
48+
}
49+
return '';
50+
}
51+
/**
52+
* A callback to be used as th proxy set trap callback.
53+
* It updates parenthood map if needed, proxifies nested newly-added objects, calls default callbacks with the changes occurred.
54+
* @param {JSONPatcherProxy} instance JSONPatcherProxy instance
55+
* @param {Object} target the affected object
56+
* @param {String} key the effect property's name
57+
* @param {Any} newValue the value being set
58+
*/
59+
function setTrap(instance, target, key, newValue) {
60+
const parentPath = findObjectPath(instance, target);
61+
62+
const destinationPropKey = parentPath + '/' + escapePathComponent(key);
63+
64+
if (instance.proxifiedObjectsMap.has(newValue)) {
65+
const newValueOriginalObject = instance.proxifiedObjectsMap.get(newValue);
66+
67+
instance.parenthoodMap.set(newValueOriginalObject.originalObject, {
68+
parent: target,
69+
path: key
70+
});
71+
}
72+
/*
73+
mark already proxified values as inherited.
74+
rationale: proxy.arr.shift()
75+
will emit
76+
{op: replace, path: '/arr/1', value: arr_2}
77+
{op: remove, path: '/arr/2'}
78+
79+
by default, the second operation would revoke the proxy, and this renders arr revoked.
80+
That's why we need to remember the proxies that are inherited.
81+
*/
82+
const revokableInstance = instance.proxifiedObjectsMap.get(newValue);
83+
/*
84+
Why do we need to check instance.isProxifyingTreeNow?
85+
86+
We need to make sure we mark revokables as inherited ONLY when we're observing,
87+
because throughout the first proxification, a sub-object is proxified and then assigned to
88+
its parent object. This assignment of a pre-proxified object can fool us into thinking
89+
that it's a proxified object moved around, while in fact it's the first assignment ever.
90+
91+
Checking isProxifyingTreeNow ensures this is not happening in the first proxification,
92+
but in fact is is a proxified object moved around the tree
93+
*/
94+
if (revokableInstance && !instance.isProxifyingTreeNow) {
95+
revokableInstance.inherited = true;
96+
}
97+
98+
// if the new value is an object, make sure to watch it
99+
if (
100+
newValue &&
101+
typeof newValue == 'object' &&
102+
!instance.proxifiedObjectsMap.has(newValue)
103+
) {
104+
instance.parenthoodMap.set(newValue, {
105+
parent: target,
106+
path: key
107+
});
108+
newValue = instance._proxifyObjectTreeRecursively(target, newValue, key);
109+
}
110+
// let's start with this operation, and may or may not update it later
111+
const operation = {
112+
op: 'remove',
113+
path: destinationPropKey
114+
};
115+
if (typeof newValue == 'undefined') {
116+
// applying De Morgan's laws would be a tad faster, but less readable
117+
if (!Array.isArray(target) && !target.hasOwnProperty(key)) {
118+
// `undefined` is being set to an already undefined value, keep silent
119+
return Reflect.set(target, key, newValue);
120+
} else {
121+
// when array element is set to `undefined`, should generate replace to `null`
122+
if (Array.isArray(target)) {
123+
// undefined array elements are JSON.stringified to `null`
124+
(operation.op = 'replace'), (operation.value = null);
125+
}
126+
const oldValue = instance.proxifiedObjectsMap.get(target[key]);
127+
// was the deleted a proxified object?
128+
if (oldValue) {
129+
instance.parenthoodMap.delete(target[key]);
130+
instance.disableTrapsForProxy(oldValue);
131+
instance.proxifiedObjectsMap.delete(oldValue);
132+
}
133+
}
134+
} else {
135+
if (Array.isArray(target) && !Number.isInteger(+key.toString())) {
136+
/* array props (as opposed to indices) don't emit any patches, to avoid needless `length` patches */
137+
return Reflect.set(target, key, newValue);
138+
}
139+
operation.op = 'add';
140+
if (target.hasOwnProperty(key)) {
141+
if (typeof target[key] !== 'undefined' || Array.isArray(target)) {
142+
operation.op = 'replace'; // setting `undefined` array elements is a `replace` op
143+
}
144+
}
145+
operation.value = newValue;
146+
}
147+
const reflectionResult = Reflect.set(target, key, newValue);
148+
instance.defaultCallback(operation);
149+
return reflectionResult;
150+
}
151+
/**
152+
* A callback to be used as th proxy delete trap callback.
153+
* It updates parenthood map if needed, calls default callbacks with the changes occurred.
154+
* @param {JSONPatcherProxy} instance JSONPatcherProxy instance
155+
* @param {Object} target the effected object
156+
* @param {String} key the effected property's name
157+
*/
158+
function deleteTrap(instance, target, key) {
159+
if (typeof target[key] !== 'undefined') {
160+
const parentPath = findObjectPath(instance, target);
161+
const destinationPropKey = parentPath + '/' + escapePathComponent(key);
162+
163+
const revokableProxyInstance = instance.proxifiedObjectsMap.get(
164+
target[key]
165+
);
166+
167+
if (revokableProxyInstance) {
168+
if (revokableProxyInstance.inherited) {
169+
/*
170+
this is an inherited proxy (an already proxified object that was moved around),
171+
we shouldn't revoke it, because even though it was removed from path1, it is still used in path2.
172+
And we know that because we mark moved proxies with `inherited` flag when we move them
173+
174+
it is a good idea to remove this flag if we come across it here, in deleteProperty trap.
175+
We DO want to revoke the proxy if it was removed again.
176+
*/
177+
revokableProxyInstance.inherited = false;
178+
} else {
179+
instance.parenthoodMap.delete(revokableProxyInstance.originalObject);
180+
instance.disableTrapsForProxy(revokableProxyInstance);
181+
instance.proxifiedObjectsMap.delete(target[key]);
182+
}
183+
}
184+
const reflectionResult = Reflect.deleteProperty(target, key);
185+
186+
instance.defaultCallback({
187+
op: 'remove',
188+
path: destinationPropKey
189+
});
190+
191+
return reflectionResult;
192+
}
193+
}
194+
/* pre-define resume and pause functions to enhance constructors performance */
195+
function resume() {
196+
this.defaultCallback = operation => {
197+
this.isRecording && this.patches.push(operation);
198+
this.userCallback && this.userCallback(operation);
199+
};
200+
this.isObserving = true;
201+
}
202+
function pause() {
203+
this.defaultCallback = () => {};
204+
this.isObserving = false;
205+
}
206+
/**
207+
* Creates an instance of JSONPatcherProxy around your object of interest `root`.
208+
* @param {Object|Array} root - the object you want to wrap
209+
* @param {Boolean} [showDetachedWarning = true] - whether to log a warning when a detached sub-object is modified @see {@link https://github.com/Palindrom/JSONPatcherProxy#detached-objects}
210+
* @returns {JSONPatcherProxy}
211+
* @constructor
212+
*/
213+
function JSONPatcherProxy(root, showDetachedWarning) {
214+
this.isProxifyingTreeNow = false;
215+
this.isObserving = false;
216+
this.proxifiedObjectsMap = new Map();
217+
this.parenthoodMap = new Map();
218+
// default to true
219+
if (typeof showDetachedWarning !== 'boolean') {
220+
showDetachedWarning = true;
221+
}
222+
223+
this.showDetachedWarning = showDetachedWarning;
224+
this.originalObject = root;
225+
this.cachedProxy = null;
226+
this.isRecording = false;
227+
this.userCallback;
228+
/**
229+
* @memberof JSONPatcherProxy
230+
* Restores callback back to the original one provided to `observe`.
231+
*/
232+
this.resume = resume.bind(this);
233+
/**
234+
* @memberof JSONPatcherProxy
235+
* Replaces your callback with a noop function.
236+
*/
237+
this.pause = pause.bind(this);
238+
}
239+
240+
JSONPatcherProxy.prototype.generateProxyAtPath = function(parent, obj, path) {
241+
if (!obj) {
242+
return obj;
243+
}
244+
const traps = {
245+
set: (target, key, value, receiver) =>
246+
setTrap(this, target, key, value, receiver),
247+
deleteProperty: (target, key) => deleteTrap(this, target, key)
248+
};
249+
const revocableInstance = Proxy.revocable(obj, traps);
250+
// cache traps object to disable them later.
251+
revocableInstance.trapsInstance = traps;
252+
revocableInstance.originalObject = obj;
253+
254+
/* keeping track of object's parent and path */
255+
256+
this.parenthoodMap.set(obj, { parent, path });
257+
258+
/* keeping track of all the proxies to be able to revoke them later */
259+
this.proxifiedObjectsMap.set(revocableInstance.proxy, revocableInstance);
260+
return revocableInstance.proxy;
261+
};
262+
// grab tree's leaves one by one, encapsulate them into a proxy and return
263+
JSONPatcherProxy.prototype._proxifyObjectTreeRecursively = function(
264+
parent,
265+
root,
266+
path
267+
) {
268+
for (let key in root) {
269+
if (root.hasOwnProperty(key)) {
270+
if (root[key] instanceof Object) {
271+
root[key] = this._proxifyObjectTreeRecursively(
272+
root,
273+
root[key],
274+
escapePathComponent(key)
275+
);
276+
}
277+
}
278+
}
279+
return this.generateProxyAtPath(parent, root, path);
280+
};
281+
// this function is for aesthetic purposes
282+
JSONPatcherProxy.prototype.proxifyObjectTree = function(root) {
283+
/*
284+
while proxyifying object tree,
285+
the proxyifying operation itself is being
286+
recorded, which in an unwanted behavior,
287+
that's why we disable recording through this
288+
initial process;
289+
*/
290+
this.pause();
291+
this.isProxifyingTreeNow = true;
292+
const proxifiedObject = this._proxifyObjectTreeRecursively(
293+
undefined,
294+
root,
295+
''
296+
);
297+
/* OK you can record now */
298+
this.isProxifyingTreeNow = false;
299+
this.resume();
300+
return proxifiedObject;
301+
};
302+
/**
303+
* Turns a proxified object into a forward-proxy object; doesn't emit any patches anymore, like a normal object
304+
* @param {Proxy} proxy - The target proxy object
305+
*/
306+
JSONPatcherProxy.prototype.disableTrapsForProxy = function(
307+
revokableProxyInstance
308+
) {
309+
if (this.showDetachedWarning) {
310+
const message =
311+
"You're accessing an object that is detached from the observedObject tree, see https://github.com/Palindrom/JSONPatcherProxy#detached-objects";
312+
313+
revokableProxyInstance.trapsInstance.set = (
314+
targetObject,
315+
propKey,
316+
newValue
317+
) => {
318+
console.warn(message);
319+
return Reflect.set(targetObject, propKey, newValue);
320+
};
321+
revokableProxyInstance.trapsInstance.set = (
322+
targetObject,
323+
propKey,
324+
newValue
325+
) => {
326+
console.warn(message);
327+
return Reflect.set(targetObject, propKey, newValue);
328+
};
329+
revokableProxyInstance.trapsInstance.deleteProperty = (
330+
targetObject,
331+
propKey
332+
) => {
333+
return Reflect.deleteProperty(targetObject, propKey);
334+
};
335+
} else {
336+
delete revokableProxyInstance.trapsInstance.set;
337+
delete revokableProxyInstance.trapsInstance.get;
338+
delete revokableProxyInstance.trapsInstance.deleteProperty;
339+
}
340+
};
341+
/**
342+
* Proxifies the object that was passed in the constructor and returns a proxified mirror of it. Even though both parameters are options. You need to pass at least one of them.
343+
* @param {Boolean} [record] - whether to record object changes to a later-retrievable patches array.
344+
* @param {Function} [callback] - this will be synchronously called with every object change with a single `patch` as the only parameter.
345+
*/
346+
JSONPatcherProxy.prototype.observe = function(record, callback) {
347+
if (!record && !callback) {
348+
throw new Error('You need to either record changes or pass a callback');
349+
}
350+
this.isRecording = record;
351+
this.userCallback = callback;
352+
/*
353+
I moved it here to remove it from `unobserve`,
354+
this will also make the constructor faster, why initiate
355+
the array before they decide to actually observe with recording?
356+
They might need to use only a callback.
357+
*/
358+
if (record) this.patches = [];
359+
this.cachedProxy = this.proxifyObjectTree(this.originalObject);
360+
return this.cachedProxy;
361+
};
362+
/**
363+
* If the observed is set to record, it will synchronously return all the patches and empties patches array.
364+
*/
365+
JSONPatcherProxy.prototype.generate = function() {
366+
if (!this.isRecording) {
367+
throw new Error('You should set record to true to get patches later');
368+
}
369+
return this.patches.splice(0, this.patches.length);
370+
};
371+
/**
372+
* Revokes all proxies rendering the observed object useless and good for garbage collection @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/revocable}
373+
*/
374+
JSONPatcherProxy.prototype.revoke = function() {
375+
this.proxifiedObjectsMap.forEach(el => {
376+
el.revoke();
377+
});
378+
};
379+
/**
380+
* Disables all proxies' traps, turning the observed object into a forward-proxy object, like a normal object that you can modify silently.
381+
*/
382+
JSONPatcherProxy.prototype.disableTraps = function() {
383+
this.proxifiedObjectsMap.forEach(this.disableTrapsForProxy, this);
384+
};
385+
return JSONPatcherProxy;
386+
})();
387+
388+
if (typeof module !== 'undefined') {
389+
module.exports = JSONPatcherProxy;
390+
module.exports.default = JSONPatcherProxy;
391+
}

0 commit comments

Comments
 (0)