Skip to content

Commit

Permalink
feat(loki): add optional flag for Changes API to limit update operati…
Browse files Browse the repository at this point in the history
…on 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)
  • Loading branch information
Viatorus committed Oct 5, 2017
1 parent 2ab7a1e commit a8abe99
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 41 deletions.
38 changes: 38 additions & 0 deletions packages/loki/spec/generic/changesApi.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,42 @@ describe("changesApi", () => {

expect(users.getChanges().length).toEqual(0);
});

it("works with delta mode", function () {
const db = new loki(),
options = {
asyncListeners: false,
disableChangesApi: false,
disableDeltaChangesApi: false
},
items = db.addCollection("items", options);

// Add some documents to the collection
items.insert({name: "mjolnir", owner: "thor", maker: {name: "dwarves", count: 1}});
items.insert({name: "gungnir", owner: "odin", maker: {name: "elves", count: 1}});
items.insert({name: "tyrfing", owner: "Svafrlami", maker: {name: "dwarves", count: 1}});
items.insert({name: "draupnir", owner: "odin", maker: {name: "elves", count: 1}});

// Find and update an existing document
const tyrfing = items.findOne({"name": "tyrfing"});
tyrfing.owner = "arngrim";
items.update(tyrfing);
tyrfing.maker.count = 4;
items.update(tyrfing);

let changes = db.serializeChanges(["items"]);
changes = JSON.parse(changes);

expect(changes.length).toEqual(6);

const firstUpdate = changes[4];
expect(firstUpdate.operation).toEqual("U");
expect(firstUpdate.obj.owner).toEqual("arngrim");
expect(firstUpdate.obj.name).toBeUndefined();

const secondUpdate = changes[5];
expect(secondUpdate.operation).toEqual("U");
expect(secondUpdate.obj.owner).toBeUndefined();
expect(secondUpdate.obj.maker).toEqual({count: 4});
});
});
75 changes: 64 additions & 11 deletions packages/loki/src/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export class Collection extends LokiEventEmitter {
* @param {boolean} [options.adaptiveBinaryIndices=true] - collection indices will be actively rebuilt rather than lazily
* @param {boolean} [options.asyncListeners=false] - whether listeners are invoked asynchronously
* @param {boolean} [options.disableChangesApi=true] - set to false to enable Changes API
* @param {boolean} [options.disableDeltaChangesApi=true] - set to false to enable Delta Changes API (requires Changes API, forces cloning)
* @param {boolean} [options.autoupdate=false] - use Object.observe to update objects automatically
* @param {boolean} [options.clone=false] - specify whether inserts and queries clone to/from user
* @param {boolean} [options.serializableIndices =true[]] - converts date values on binary indexed property values are serializable
Expand Down Expand Up @@ -171,6 +172,12 @@ export class Collection extends LokiEventEmitter {
// disable track changes
this.disableChangesApi = options.disableChangesApi !== undefined ? options.disableChangesApi : true;

// disable delta update object style on changes
this.disableDeltaChangesApi = options.disableDeltaChangesApi !== undefined ? options.disableDeltaChangesApi : true;
if (this.disableChangesApi) {
this.disableDeltaChangesApi = true;
}

// option to observe objects and update them automatically, ignored if Object.observe is not supported
this.autoupdate = options.autoupdate !== undefined ? options.autoupdate : false;

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

//Compare changed object (which is a forced clone) with existing object and return the delta
function getChangeDelta(obj, old) {
if (old) {
return getObjectDelta(old, obj);
}
else {
return JSON.parse(JSON.stringify(obj));
}
}

this.getChangeDelta = getChangeDelta;

function getObjectDelta(oldObject, newObject) {
const propertyNames = newObject !== null && typeof newObject === "object" ? Object.keys(newObject) : null;
if (propertyNames && propertyNames.length && ["string", "boolean", "number"].indexOf(typeof(newObject)) < 0) {
const delta = {};
for (let i = 0; i < propertyNames.length; i++) {
const propertyName = propertyNames[i];
if (newObject.hasOwnProperty(propertyName)) {
if (!oldObject.hasOwnProperty(propertyName) || self.uniqueNames.indexOf(propertyName) >= 0 || propertyName === "$loki" || propertyName === "meta") {
delta[propertyName] = newObject[propertyName];
}
else {
const propertyDelta = getObjectDelta(oldObject[propertyName], newObject[propertyName]);
if (typeof propertyDelta !== "undefined" && propertyDelta !== {}) {
delta[propertyName] = propertyDelta;
}
}
}
}
return Object.keys(delta).length === 0 ? undefined : delta;
}
else {
return oldObject === newObject ? undefined : newObject;
}
}

this.getObjectDelta = getObjectDelta;


// clear all the changes
function flushChanges() {
self.changes = [];
Expand Down Expand Up @@ -323,18 +370,18 @@ export class Collection extends LokiEventEmitter {
createChange(self.name, "I", obj);
}

function createUpdateChange(obj) {
createChange(self.name, "U", obj);
function createUpdateChange(obj, old) {
createChange(self.name, "U", obj, old);
}

function insertMetaWithChange(obj) {
insertMeta(obj);
createInsertChange(obj);
}

function updateMetaWithChange(obj) {
function updateMetaWithChange(obj, old) {
updateMeta(obj);
createUpdateChange(obj);
createUpdateChange(obj, old);
}


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

this.setChangesApi = (enabled) => {
this.disableChangesApi = !enabled;
if (!enabled) {
self.disableDeltaChangesApi = false;
}
setHandlers();
};
/**
Expand All @@ -361,8 +411,8 @@ export class Collection extends LokiEventEmitter {
insertHandler(obj);
});

this.on("update", (obj) => {
updateHandler(obj);
this.on("update", (obj, old) => {
updateHandler(obj, old);
});

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

static fromJSONObject(obj, options, forceRebuild) {
let coll = new Collection(obj.name, {disableChangesApi: obj.disableChangesApi});
let coll = new Collection(obj.name, {
disableChangesApi: obj.disableChangesApi,
disableDeltaChangesApi: obj.disableDeltaChangesApi
});

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

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

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

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

this.emit("update", doc, this.cloneObjects ? clone(oldInternal, this.cloneMethod) : null);
this.emit("update", doc, this.cloneObjects || !this.disableDeltaChangesApi ? clone(oldInternal, this.cloneMethod) : null);
return doc;
} catch (err) {
this.rollback();
Expand Down
60 changes: 30 additions & 30 deletions packages/loki/src/event_emitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,24 @@ export class LokiEventEmitter {

constructor() {
/**
* @prop {hashmap} events - a hashmap, with each property being an array of callbacks
*/
* @prop {hashmap} events - a hashmap, with each property being an array of callbacks
*/
this.events = {};

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

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

Expand All @@ -50,43 +50,43 @@ export class LokiEventEmitter {
}

/**
* emit(eventName, data) - emits a particular event
* with the option of passing optional parameters which are going to be processed by the callback
* provided signatures match (i.e. if passing emit(event, arg0, arg1) the listener should take two parameters)
* @param {string} eventName - the name of the event
* @param {object} data - optional object passed with the event
*/
emit(eventName, data) {
* emit(eventName, data) - emits a particular event
* with the option of passing optional parameters which are going to be processed by the callback
* provided signatures match (i.e. if passing emit(event, arg0, arg1) the listener should take two parameters)
* @param {string} eventName - the name of the event
* @param {object} data - optional object passed with the event
*/
emit(eventName, ...data) {
if (eventName && this.events[eventName]) {
this.events[eventName].forEach((listener) => {
if (this.asyncListeners) {
setTimeout(() => {
listener(data);
listener(...data);
}, 1);
} else {
listener(data);
listener(...data);
}

});
}
}

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

/**
* removeListener() - removes the listener at position 'index' from the event 'eventName'
* @param {string|string[]} eventName - the name(s) of the event(s) which the listener is attached to
* @param {function} listener - the listener callback function to remove from emitter
*/
* removeListener() - removes the listener at position 'index' from the event 'eventName'
* @param {string|string[]} eventName - the name(s) of the event(s) which the listener is attached to
* @param {function} listener - the listener callback function to remove from emitter
*/
removeListener(eventName, listener) {
if (Array.isArray(eventName)) {
eventName.forEach((currentEventName) => {
Expand Down
6 changes: 6 additions & 0 deletions packages/loki/src/resultset.js
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,12 @@ export class Resultset {
forceCloneMethod = "shallow";
}

// if collection has delta changes active, then force clones and use 'parse-stringify' for effective change tracking of nested objects
if (!this.collection.disableDeltaChangesApi) {
forceClones = true;
forceCloneMethod = "parse-stringify";
}

// if this has no filters applied, just return collection.data
if (!this.filterInitialized) {
if (this.filteredrows.length === 0) {
Expand Down

0 comments on commit a8abe99

Please sign in to comment.