Skip to content

Commit a8abe99

Browse files
committed
feat(loki): add optional flag for Changes API to limit update operation output to modified properties only (#29)
Also includes properties flagged as unique, since they would be needed to identify the item that is updated. (from techfort/LokiJS#578)
1 parent 2ab7a1e commit a8abe99

File tree

4 files changed

+138
-41
lines changed

4 files changed

+138
-41
lines changed

packages/loki/spec/generic/changesApi.spec.js

+38
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,42 @@ describe("changesApi", () => {
5555

5656
expect(users.getChanges().length).toEqual(0);
5757
});
58+
59+
it("works with delta mode", function () {
60+
const db = new loki(),
61+
options = {
62+
asyncListeners: false,
63+
disableChangesApi: false,
64+
disableDeltaChangesApi: false
65+
},
66+
items = db.addCollection("items", options);
67+
68+
// Add some documents to the collection
69+
items.insert({name: "mjolnir", owner: "thor", maker: {name: "dwarves", count: 1}});
70+
items.insert({name: "gungnir", owner: "odin", maker: {name: "elves", count: 1}});
71+
items.insert({name: "tyrfing", owner: "Svafrlami", maker: {name: "dwarves", count: 1}});
72+
items.insert({name: "draupnir", owner: "odin", maker: {name: "elves", count: 1}});
73+
74+
// Find and update an existing document
75+
const tyrfing = items.findOne({"name": "tyrfing"});
76+
tyrfing.owner = "arngrim";
77+
items.update(tyrfing);
78+
tyrfing.maker.count = 4;
79+
items.update(tyrfing);
80+
81+
let changes = db.serializeChanges(["items"]);
82+
changes = JSON.parse(changes);
83+
84+
expect(changes.length).toEqual(6);
85+
86+
const firstUpdate = changes[4];
87+
expect(firstUpdate.operation).toEqual("U");
88+
expect(firstUpdate.obj.owner).toEqual("arngrim");
89+
expect(firstUpdate.obj.name).toBeUndefined();
90+
91+
const secondUpdate = changes[5];
92+
expect(secondUpdate.operation).toEqual("U");
93+
expect(secondUpdate.obj.owner).toBeUndefined();
94+
expect(secondUpdate.obj.maker).toEqual({count: 4});
95+
});
5896
});

packages/loki/src/collection.js

+64-11
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export class Collection extends LokiEventEmitter {
8585
* @param {boolean} [options.adaptiveBinaryIndices=true] - collection indices will be actively rebuilt rather than lazily
8686
* @param {boolean} [options.asyncListeners=false] - whether listeners are invoked asynchronously
8787
* @param {boolean} [options.disableChangesApi=true] - set to false to enable Changes API
88+
* @param {boolean} [options.disableDeltaChangesApi=true] - set to false to enable Delta Changes API (requires Changes API, forces cloning)
8889
* @param {boolean} [options.autoupdate=false] - use Object.observe to update objects automatically
8990
* @param {boolean} [options.clone=false] - specify whether inserts and queries clone to/from user
9091
* @param {boolean} [options.serializableIndices =true[]] - converts date values on binary indexed property values are serializable
@@ -171,6 +172,12 @@ export class Collection extends LokiEventEmitter {
171172
// disable track changes
172173
this.disableChangesApi = options.disableChangesApi !== undefined ? options.disableChangesApi : true;
173174

175+
// disable delta update object style on changes
176+
this.disableDeltaChangesApi = options.disableDeltaChangesApi !== undefined ? options.disableDeltaChangesApi : true;
177+
if (this.disableChangesApi) {
178+
this.disableDeltaChangesApi = true;
179+
}
180+
174181
// option to observe objects and update them automatically, ignored if Object.observe is not supported
175182
this.autoupdate = options.autoupdate !== undefined ? options.autoupdate : false;
176183

@@ -258,14 +265,54 @@ export class Collection extends LokiEventEmitter {
258265
* This method creates a clone of the current status of an object and associates operation and collection name,
259266
* so the parent db can aggregate and generate a changes object for the entire db
260267
*/
261-
function createChange(name, op, obj) {
268+
function createChange(name, op, obj, old) {
262269
self.changes.push({
263270
name,
264271
operation: op,
265-
obj: JSON.parse(JSON.stringify(obj))
272+
obj: op === "U" && !self.disableDeltaChangesApi ? getChangeDelta(obj, old) : JSON.parse(JSON.stringify(obj))
266273
});
267274
}
268275

276+
//Compare changed object (which is a forced clone) with existing object and return the delta
277+
function getChangeDelta(obj, old) {
278+
if (old) {
279+
return getObjectDelta(old, obj);
280+
}
281+
else {
282+
return JSON.parse(JSON.stringify(obj));
283+
}
284+
}
285+
286+
this.getChangeDelta = getChangeDelta;
287+
288+
function getObjectDelta(oldObject, newObject) {
289+
const propertyNames = newObject !== null && typeof newObject === "object" ? Object.keys(newObject) : null;
290+
if (propertyNames && propertyNames.length && ["string", "boolean", "number"].indexOf(typeof(newObject)) < 0) {
291+
const delta = {};
292+
for (let i = 0; i < propertyNames.length; i++) {
293+
const propertyName = propertyNames[i];
294+
if (newObject.hasOwnProperty(propertyName)) {
295+
if (!oldObject.hasOwnProperty(propertyName) || self.uniqueNames.indexOf(propertyName) >= 0 || propertyName === "$loki" || propertyName === "meta") {
296+
delta[propertyName] = newObject[propertyName];
297+
}
298+
else {
299+
const propertyDelta = getObjectDelta(oldObject[propertyName], newObject[propertyName]);
300+
if (typeof propertyDelta !== "undefined" && propertyDelta !== {}) {
301+
delta[propertyName] = propertyDelta;
302+
}
303+
}
304+
}
305+
}
306+
return Object.keys(delta).length === 0 ? undefined : delta;
307+
}
308+
else {
309+
return oldObject === newObject ? undefined : newObject;
310+
}
311+
}
312+
313+
this.getObjectDelta = getObjectDelta;
314+
315+
269316
// clear all the changes
270317
function flushChanges() {
271318
self.changes = [];
@@ -323,18 +370,18 @@ export class Collection extends LokiEventEmitter {
323370
createChange(self.name, "I", obj);
324371
}
325372

326-
function createUpdateChange(obj) {
327-
createChange(self.name, "U", obj);
373+
function createUpdateChange(obj, old) {
374+
createChange(self.name, "U", obj, old);
328375
}
329376

330377
function insertMetaWithChange(obj) {
331378
insertMeta(obj);
332379
createInsertChange(obj);
333380
}
334381

335-
function updateMetaWithChange(obj) {
382+
function updateMetaWithChange(obj, old) {
336383
updateMeta(obj);
337-
createUpdateChange(obj);
384+
createUpdateChange(obj, old);
338385
}
339386

340387

@@ -352,6 +399,9 @@ export class Collection extends LokiEventEmitter {
352399

353400
this.setChangesApi = (enabled) => {
354401
this.disableChangesApi = !enabled;
402+
if (!enabled) {
403+
self.disableDeltaChangesApi = false;
404+
}
355405
setHandlers();
356406
};
357407
/**
@@ -361,8 +411,8 @@ export class Collection extends LokiEventEmitter {
361411
insertHandler(obj);
362412
});
363413

364-
this.on("update", (obj) => {
365-
updateHandler(obj);
414+
this.on("update", (obj, old) => {
415+
updateHandler(obj, old);
366416
});
367417

368418
this.on("delete", (obj) => {
@@ -418,7 +468,10 @@ export class Collection extends LokiEventEmitter {
418468
}
419469

420470
static fromJSONObject(obj, options, forceRebuild) {
421-
let coll = new Collection(obj.name, {disableChangesApi: obj.disableChangesApi});
471+
let coll = new Collection(obj.name, {
472+
disableChangesApi: obj.disableChangesApi,
473+
disableDeltaChangesApi: obj.disableDeltaChangesApi
474+
});
422475

423476
coll.adaptiveBinaryIndices = obj.adaptiveBinaryIndices !== undefined ? (obj.adaptiveBinaryIndices === true) : false;
424477
coll.transactional = obj.transactional;
@@ -1016,7 +1069,7 @@ export class Collection extends LokiEventEmitter {
10161069
position = arr[1]; // position in data array
10171070

10181071
// if configured to clone, do so now... otherwise just use same obj reference
1019-
newInternal = this.cloneObjects ? clone(doc, this.cloneMethod) : doc;
1072+
newInternal = this.cloneObjects || !this.disableDeltaChangesApi ? clone(doc, this.cloneMethod) : doc;
10201073

10211074
this.emit("pre-update", doc);
10221075

@@ -1059,7 +1112,7 @@ export class Collection extends LokiEventEmitter {
10591112
this.commit();
10601113
this.dirty = true; // for autosave scenarios
10611114

1062-
this.emit("update", doc, this.cloneObjects ? clone(oldInternal, this.cloneMethod) : null);
1115+
this.emit("update", doc, this.cloneObjects || !this.disableDeltaChangesApi ? clone(oldInternal, this.cloneMethod) : null);
10631116
return doc;
10641117
} catch (err) {
10651118
this.rollback();

packages/loki/src/event_emitter.js

+30-30
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,24 @@ export class LokiEventEmitter {
1313

1414
constructor() {
1515
/**
16-
* @prop {hashmap} events - a hashmap, with each property being an array of callbacks
17-
*/
16+
* @prop {hashmap} events - a hashmap, with each property being an array of callbacks
17+
*/
1818
this.events = {};
1919

2020
/**
21-
* @prop {boolean} asyncListeners - boolean determines whether or not the callbacks associated with each event
22-
* should happen in an async fashion or not
23-
* Default is false, which means events are synchronous
24-
*/
21+
* @prop {boolean} asyncListeners - boolean determines whether or not the callbacks associated with each event
22+
* should happen in an async fashion or not
23+
* Default is false, which means events are synchronous
24+
*/
2525
this.asyncListeners = false;
2626
}
2727

2828
/**
29-
* on(eventName, listener) - adds a listener to the queue of callbacks associated to an event
30-
* @param {string|string[]} eventName - the name(s) of the event(s) to listen to
31-
* @param {function} listener - callback function of listener to attach
32-
* @returns {int} the index of the callback in the array of listeners for a particular event
33-
*/
29+
* on(eventName, listener) - adds a listener to the queue of callbacks associated to an event
30+
* @param {string|string[]} eventName - the name(s) of the event(s) to listen to
31+
* @param {function} listener - callback function of listener to attach
32+
* @returns {int} the index of the callback in the array of listeners for a particular event
33+
*/
3434
on(eventName, listener) {
3535
let event;
3636

@@ -50,43 +50,43 @@ export class LokiEventEmitter {
5050
}
5151

5252
/**
53-
* emit(eventName, data) - emits a particular event
54-
* with the option of passing optional parameters which are going to be processed by the callback
55-
* provided signatures match (i.e. if passing emit(event, arg0, arg1) the listener should take two parameters)
56-
* @param {string} eventName - the name of the event
57-
* @param {object} data - optional object passed with the event
58-
*/
59-
emit(eventName, data) {
53+
* emit(eventName, data) - emits a particular event
54+
* with the option of passing optional parameters which are going to be processed by the callback
55+
* provided signatures match (i.e. if passing emit(event, arg0, arg1) the listener should take two parameters)
56+
* @param {string} eventName - the name of the event
57+
* @param {object} data - optional object passed with the event
58+
*/
59+
emit(eventName, ...data) {
6060
if (eventName && this.events[eventName]) {
6161
this.events[eventName].forEach((listener) => {
6262
if (this.asyncListeners) {
6363
setTimeout(() => {
64-
listener(data);
64+
listener(...data);
6565
}, 1);
6666
} else {
67-
listener(data);
67+
listener(...data);
6868
}
6969

7070
});
7171
}
7272
}
7373

7474
/**
75-
* Alias of LokiEventEmitter.prototype.on
76-
* addListener(eventName, listener) - adds a listener to the queue of callbacks associated to an event
77-
* @param {string|string[]} eventName - the name(s) of the event(s) to listen to
78-
* @param {function} listener - callback function of listener to attach
79-
* @returns {int} the index of the callback in the array of listeners for a particular event
80-
*/
75+
* Alias of LokiEventEmitter.prototype.on
76+
* addListener(eventName, listener) - adds a listener to the queue of callbacks associated to an event
77+
* @param {string|string[]} eventName - the name(s) of the event(s) to listen to
78+
* @param {function} listener - callback function of listener to attach
79+
* @returns {int} the index of the callback in the array of listeners for a particular event
80+
*/
8181
addListener(eventName, listener) {
8282
return this.on(eventName, listener);
8383
}
8484

8585
/**
86-
* removeListener() - removes the listener at position 'index' from the event 'eventName'
87-
* @param {string|string[]} eventName - the name(s) of the event(s) which the listener is attached to
88-
* @param {function} listener - the listener callback function to remove from emitter
89-
*/
86+
* removeListener() - removes the listener at position 'index' from the event 'eventName'
87+
* @param {string|string[]} eventName - the name(s) of the event(s) which the listener is attached to
88+
* @param {function} listener - the listener callback function to remove from emitter
89+
*/
9090
removeListener(eventName, listener) {
9191
if (Array.isArray(eventName)) {
9292
eventName.forEach((currentEventName) => {

packages/loki/src/resultset.js

+6
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,12 @@ export class Resultset {
976976
forceCloneMethod = "shallow";
977977
}
978978

979+
// if collection has delta changes active, then force clones and use 'parse-stringify' for effective change tracking of nested objects
980+
if (!this.collection.disableDeltaChangesApi) {
981+
forceClones = true;
982+
forceCloneMethod = "parse-stringify";
983+
}
984+
979985
// if this has no filters applied, just return collection.data
980986
if (!this.filterInitialized) {
981987
if (this.filteredrows.length === 0) {

0 commit comments

Comments
 (0)