Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#578: Add optional flag for Changes API to limit update operation out… #29

Merged
merged 1 commit into from
Sep 27, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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