@asmartbear/augmented
TypeScript icon, indicating that this package has built-in type declarations

1.1.14 • Public • Published

Augmented Objects

Objects that look and act exactly like native JavaScript types ("POJO"), but augmented with validation of type and value, callbacks before and after changes, and synchronization with other replicas (peer-to-peer or hub-and-poke to a server).

Usage

Install through npm, or get the source from Github.

Creating objects and using objects normally

createManager() and so on....

Use createObject() to create augmented objects. They behave just like regular JavaScript objects, both with scalar types (null, boolean, number, string) and nested objects and arrays, including reflection and common algorithms like conversion to JSON:

import * as A from '@asmartbear/augmented'

const mgr = A.createManager();
const x = mgr.createObject();
x.a = 1;             // supports all scalar types
x.b = { c:2, d:4 };  // supports nested objects and arrays
x.b.d = "hi";        // change data types
console.log(x);
/// {a:1,b:{c:2,d:"hi"}}
console.log(Object.keys(x));
/// a,b
console.log(JSON.stringify(x));
/// {"a":1,"b":{"c":2,"d":"hi"}}

Serialization & Cloning

You can serialize objects into a compressed binary form suitable for storage or communication. The state includes not only the content of the object, but synchronization state as well.

Cloning is like serializing and unserializing in succession, except faster (because we skip data compression).

You can serialize and clone native objects and augmented objects with the same functions.

import * as A from '@asmartbear/augmented'

const mgr = A.createManager();
const data = {a:1,b:{c:2,d:"hi"}};
const x = Object.assign(mgr.createObject(), data);
console.log(x);
/// {a:1,b:{c:2,d:"hi"}}
const ser = mgr.serialize(x);       // convert to compressed byte array
console.log(ser.byteLength < 150);  // even with sync data, it's small!
/// true
const y = mgr.unserialize(ser);     // recreate the object
console.log(y);
/// {a:1,b:{c:2,d:"hi"}}
}   // omit

Synchronization

The algorithm of synchronization is described further below. Augmented objects can merge their state with any number of replicas, no matter how out-of-date, without tombstones that grow without bound (like classic CRDTs), and without a central server or ever-growing log (like operation/OTs). Merge conflicts resolve automatically. Synchronization state is included in serialize()/unserialize(), so you can move objects between processes and synchronize them with a single line of code.

import * as A from '@asmartbear/augmented'

// Create an object on Replica 1
const replica1 = A.createManager("R1");
const x = replica1.createObject();
Object.assign(x, {a:1,b:2,c:3});
console.log(x);
/// {a:1,b:2,c:3}

// Create an object on Replica 2, and "synchronize" it.
// In this (working!) sample code, it's another replica in the same
// process space, but in practice you would presumably `serialize()`
// the object, send to Replica 2, then `unserialize()` there.
const replica2 = A.createManager("R2");
const y = replica2.createObject();
A.synchronize(y, x);    // synchronizes y <-- x
console.log(y);
/// {a:1,b:2,c:3}
console.log(x === y);   // they are distinct objects!
/// false

// Modify the first object `x`.  If you attempt to synchronize `y`
// back to `x`, nothing happens, because x's changes are newer, and
// synchronization is idempotent.
delete x.a;
x.b = 222;
x.d = 4;
console.log(x);
/// {b:222,c:3,d:4}
A.synchronize(x, y);
console.log(x);         // no change!
/// {b:222,c:3,d:4}

// Modify `y` also, then synchronize `x`.  If one changes a key that
// the other deletes, the result is a deletion. But if two changes
// conflict, it's last-write-wins.
y.a = 111;          // the delete from `x` will win
y.b = "twotwotwo";  // the change in `y` will win, because it's later
y.d = 4;            // both made the same change here; no problem
y.e = 5;            // a new key
console.log(y);
/// {a:111,b:"twotwotwo",c:3,d:4,e:5}
A.synchronize(y, x);        // y <-- x
console.log(y);
/// {b:"twotwotwo",c:3,d:4,e:5}
A.synchronize(x, y);        // x <-- y
console.log(x);             // now both are the same
/// {b:"twotwotwo",c:3,d:4,e:5}

Observed-Remove semantics

Yadda yadda!

import * as A from '@asmartbear/augmented'

// Initial setup, three replicas, with no synchronization yet.
//
// Notice `x` and `y` both create field `a` with different values.  If we
// synchronize now, they'll all converge to `{a:111,b:2,c:3,d:4}`, merging
// the fields, taking the latest copy of `a`.
const x = Object.assign(A.createManager("X").createObject(), {a:1,b:2});
const y = Object.assign(A.createManager("Y").createObject(), {a:111,c:3});
const z = Object.assign(A.createManager("Z").createObject(), {d:4});

// Synchronize `z` <-- `y`, then `z` deletes field `a`.
A.synchronize(z,y);
console.log(z);
/// {a:111,c:3,d:4}
delete z.a;
console.log(z);
/// {c:3,d:4}

// If we synchronize this back to `y`, the deletion is synchronized, and
// both objects are identical, as you would always expect with eventual
// consistency.
A.synchronize(y,z);
console.log(y);
/// {c:3,d:4}

// However, the `a` field in `x` is a different field!  It's the same
// name by coincidence, but when `z` deleted `y.a`, it didn't know about
// `x.a` yet; we say `z` didn't "observe" `x.a`.  Therefore, synchronizing
// `x` <-- `z` will _not_ result in a deletion!
A.synchronize(x,z);
console.log(x);
/// {a:1,b:2,c:3,d:4}

// Even synchronizing `x` <-- `y` will not result in a deletion, because
// `y` also has not "observed" `x.a` yet.
A.synchronize(x,y);
console.log(x);
/// {a:1,b:2,c:3,d:4}

// However, synchronizing the other direction will succeed; both `y` and
// `z` will take this previously-unobserved field `a`.  Thus, eventual
// consistency has been achieved, even with "observation" semantics.
A.synchronize(y,x);
A.synchronize(z,x);
console.log(y);
/// {a:1,b:2,c:3,d:4}
console.log(z);
/// {a:1,b:2,c:3,d:4}

// On the other hand, if we create `x.n` and `y.n`, and first synchronize
// both to `z`, and _then_ delete `z.n`, then the deletion will synchronize
// back to both, because both were "observed" by `z` this time.
x.n = 123;
y.n = 321;
A.synchronize(z,x);
A.synchronize(z,y);
console.log(z);             // the later (321) value will win
/// {a:1,b:2,c:3,d:4,n:321}
delete z.n;
console.log(z);
/// {a:1,b:2,c:3,d:4}
A.synchronize(x,z);
console.log(x);
/// {a:1,b:2,c:3,d:4}
A.synchronize(y,z);
console.log(y);
/// {a:1,b:2,c:3,d:4}

Synchronization Algorithm

Tacos are great.

Package Sidebar

Install

npm i @asmartbear/augmented

Weekly Downloads

20

Version

1.1.14

License

MIT

Unpacked Size

290 kB

Total Files

91

Last publish

Collaborators

  • asmartbear