Permalink
Browse files

Dirty persistent containers of modified objects.

If an object is referred to by a named object, then when that object is
dirtied, the named object is dirtied as well. This ensures that changes
to the anonymous object are saved.
  • Loading branch information...
1 parent be2b818 commit 736fa55020bd092116e21b02f5ed0d2d9ed8f6ac @kpreid committed Jun 17, 2012
Showing with 75 additions and 10 deletions.
  1. +47 −10 storage.js
  2. +28 −0 test/test-storage.js
View
@@ -10,6 +10,8 @@
var DirtyQueue = cubes.util.DirtyQueue;
var Notifier = cubes.util.Notifier;
+ function noop() {}
+
var SERIAL_TYPE_NAME = "()";
function cyclicSerialize(root, typeNameFunc, getName) {
@@ -47,8 +49,9 @@
}
storage.cyclicSerialize = cyclicSerialize;
- function cyclicUnserialize(json, unserializers, lookupName) {
+ function cyclicUnserialize(json, unserializers, lookupName, fixupHook) {
if (!lookupName) lookupName = function () { throw new Error("got name w/ no lookup function"); };
+ if (!fixupHook) fixupHook = noop;
var seen = [];
function findConstructor(json) {
@@ -64,7 +67,9 @@
} else if (typeof json === "string") {
return lookupName(json);
} else if (typeof json === "object") {
- return seen[+(json["#"])] = findConstructor(json).unserialize(json, unserialize);
+ var object = seen[+(json["#"])] = findConstructor(json).unserialize(json, unserialize);
+ fixupHook(object);
+ return object;
} else {
throw new Error("Don't know how to unserialize from a " + typeof json);
}
@@ -254,13 +259,19 @@
return null;
} else {
console.log("Persister: retrieving", name);
+ var fixupList = [];
var object = cyclicUnserialize(JSON.parse(data), Persister.types, function (name) {
var obj = pool.get(name);
if (obj) {
return obj;
} else {
throw new Error("Serialized object contained reference to missing object: " + name);
}
+ }, fixupList.push.bind(fixupList));
+ fixupList.forEach(function (o) {
+ if (o.persistence && o !== object) {
+ o.persistence._container = object;
+ }
});
register(object, name);
return object;
@@ -342,11 +353,8 @@
var name = null;
var dirty = false;
- function getObjName(obj) {
- if (obj && obj !== object) {
- return pool.getObjectName(obj);
- }
- }
+ var contained = [];
+ this._container = null;
this._registerName = function (newPool, newName) { // TODO internal, should not be published
pool = newPool;
@@ -356,21 +364,50 @@
this._getName = function () { return name; };
this.dirty = function () {
if (name !== null && !dirty) {
- console.log("Persister: dirtied", name);
+ if (typeof console !== "undefined")
+ console.log("Persister: dirtied", name);
dirty = true;
pool._dirty(name);
+ } else if (this._container !== null) {
+ this._container.persistence.dirty();
+ if (typeof console !== "undefined")
+ console.log("Persister: (on behalf of ", object, ")");
}
};
this.commit = function () {
if (name === null) return;
if (!dirty) {
console.log("Persister: not writing clean", name);
return;
- } else {
+ } else (function () {
console.log("Persister: writing dirty", name);
+
+ var newContained = [];
+ function getObjName(obj) {
+ if (obj && obj !== object) {
+ if (obj.persistence) newContained.push(obj);
+ return pool.getObjectName(obj);
+ }
+ }
+
pool._write(name, JSON.stringify(cyclicSerialize(object, Persister.findType, getObjName)));
+ contained.forEach(function (c) {
+ if (c.persistence._container !== object && c.persistence._container !== null && typeof console !== "undefined") {
+ console.warn("Inconsistent persistence backreference!", c, "has", c.persistence._container, "but already found in", object);
+ }
+ c.persistence._container = null;
+ });
+ newContained.forEach(function (c) {
+ if (c.persistence._container !== null && c.persistence._container !== object) {
+ // TODO this should be a fatal error because it means an obj is contained by two persistent objects, but we need to enforce it at set-time rather than save-time to avoid unsaveable states
+ console.warn("Inconsistent persistence backreference!", c, "has", c.persistence._container, "but also found in", object);
+ }
+ c.persistence._container = object;
+ });
+ contained = newContained;
+
dirty = false;
- }
+ }());
};
}
Persister.types = {}; // TODO global mutable state
View
@@ -37,9 +37,11 @@ describe("Persister", function () {
var World = cubes.World;
// Class for testing persistence behavior
+ var nextSerial = 0;
function PersistenceTestObject(a, b) {
this.persistence = new Persister(this);
Object.defineProperties(this, {
+ serial: { enumerable: true, value: nextSerial++ },
a: {
enumerable: true,
get: function () { return a; },
@@ -52,6 +54,9 @@ describe("Persister", function () {
}
});
}
+ PersistenceTestObject.prototype.toString = function () {
+ return "[PersistenceTestObject " + this.serial + "]";
+ };
PersistenceTestObject.prototype.serialize = function (subSerialize) {
var json = {
// TODO make serialization handle non-object values
@@ -109,6 +114,29 @@ describe("Persister", function () {
expect(outer2.a).toBe(inner2);
});
+ it("should dirty persistent containers of modified objects", function () {
+ var inner = new PersistenceTestObject(null, null);
+ var outer = new PersistenceTestObject(inner, null);
+ pool.persist(outer, "outer");
+ pool.flushNow();
+
+ expect(pool.status.get()).toBe(0);
+ inner.a = new PersistenceTestObject(null, null);
+ expect(pool.status.get()).toBe(1); // got dirtied
+
+ pool.flushNow();
+
+ // Check if dirtying wrote the changes
+ var pool2 = createTestPool();
+ var outer2 = pool2.get("outer");
+ expect(outer2.a.a).not.toBeNull();
+
+ // Check if *unserialized* objects have correct containment relations
+ expect(pool2.status.get()).toBe(0);
+ outer2.a.a = null;
+ expect(pool2.status.get()).toBe(1);
+ });
+
it("handles renaming", function () {
var obj = new PersistenceTestObject(null, null);
pool.persist(obj, "foo");

0 comments on commit 736fa55

Please sign in to comment.