derby
Version:
MVC framework making it easy to write realtime, collaborative applications that run in both Node.js and browsers.
538 lines (537 loc) • 20.9 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.EventModel = void 0;
var templates_1 = require("./templates");
var util_1 = require("./templates/util");
// The many trees of bindings:
//
// - Model tree, containing your actual data. Eg:
// {users:{fred:{age:40}, wilma:{age:37}}}
//
// - Event model tree, whose structure mirrors the model tree. The event model
// tree lets us annotate the model tree with listeners which fire when events
// change. I think there are three types of listeners:
//
// 1. Reference binding binds to whatever is referred to by the path. Eg,
// {{each items as item}} binds item by reference as it goes through the
// list.
// 2. Fixed path bindings explicitly bind to whatever is at that path
// regardless of how the model changes underneath the event model
// 3. Listen on a subtree and fire when anything in the subtree changes. This
// is used for custom functions.
//
// {{foo.id}} would listen on the fixed path ['foo', 'id'].
//
//
// - Context tree represents the changing (embedded) contexts of the templating
// engine. This maps to the tree of templates and allows templates to reference
// anything in any of their enclosing template scopes.
//
// module.exports = EventModel;
// The code here uses object-based set pattern where objects are keyed using
// sequentially generated IDs.
var nextId = 1;
// A binding object is something with update(), insert()/move()/remove() defined.
// Given x[y] with model.get(y) == 5:
// item = 5
// segments = ['y']
// outside = the EventModel for x.
//
// Note that item could be a Context or another ModelRef - eg:
//
// {{ each foo as bar }} ... {{ x[bar] }} -or- {{ x[y[z]] }}
var ModelRef = /** @class */ (function () {
function ModelRef(model, item, segments, outside) {
this.id = nextId++;
// We need a reference to the model & our segment list so we can update our
// value.
this.model = model;
this.segments = segments;
// Our current value.
this.item = item;
// outside is a reference to the EventModel of the thing on the lhs of the
// brackets. For example, in x[y].z, outside is the EventModel of x.
this.outside = outside;
// result is the EventModel of the evaluated version of the brackets. In
// x[y].z, its the EventModel of x[y].
this.result = outside.child(item).refChild(this);
}
ModelRef.prototype.update = function () {
var segments = templates_1.expressions.pathSegments(this.segments);
var newItem = templates_1.expressions.lookup(segments, this.model.data);
if (this.item === newItem)
return;
// First remove myself.
delete this.outside.child(this.item).refChildren[this.id];
this.item = newItem;
var container = this.outside.child(this.item);
// I want to just call refChild but that would create a new EM. Instead I
// want to just implant my current EM there.
if (!container.refChildren)
container.refChildren = new RefChildrenMap();
container.refChildren[this.id] = this.result;
// Finally, update all the bindings in the tree.
this.result.update();
};
return ModelRef;
}());
var RefOutMap = /** @class */ (function () {
function RefOutMap() {
}
return RefOutMap;
}());
var RefChildrenMap = /** @class */ (function () {
function RefChildrenMap() {
}
return RefChildrenMap;
}());
var BindingsMap = /** @class */ (function () {
function BindingsMap() {
}
return BindingsMap;
}());
var ItemContextsMap = /** @class */ (function () {
function ItemContextsMap() {
}
return ItemContextsMap;
}());
var EventModelsMap = /** @class */ (function () {
function EventModelsMap() {
}
return EventModelsMap;
}());
function hasKeys(object) {
for (var key in object) {
return true;
}
return false;
}
function childSetWildcard(child) {
child._set();
}
var EventModel = /** @class */ (function () {
function EventModel() {
this.id = nextId++;
// Most of these won't ever be filled in, so I'm just leaving them null.
//
// These contain our EventModel children.
this.object = null;
this.array = null;
// This contains any EventModel children which have floating references.
this.arrayByReference = null;
// If the data stored here is ever used to lookup other values, this is an
// object mapping remote child ID -> ref.
//
// Eg given x[y], y.refOut[x.id] = <Binding>
this.refOut = null;
// This is a map from ref id -> event model for events bound to this
// EventModel but via a ref. We could just merge them into the main tree, but
// this way they're easy to move.
//
// Eg, given x[y] (y=1), x.1.refChildren[ref id] is an EventModel.
this.refChildren = null;
this.bindings = null;
// Item contexts are contexts which need their item number changed as this
// EventModel object moves around its surrounding list.
this.itemContexts = null;
}
EventModel.prototype.refChild = function (ref) {
if (!this.refChildren)
this.refChildren = new RefChildrenMap();
var id = ref.id;
if (!this.refChildren[id]) {
this.refChildren[id] = new EventModel();
}
return this.refChildren[id];
};
EventModel.prototype.arrayLookup = function (model, segmentsBefore, segmentsInside) {
var segments = templates_1.expressions.pathSegments(segmentsInside);
var item = templates_1.expressions.lookup(segments, model.data);
var source = this.at(segmentsInside);
// What the array currently resolves to. Given x[y] with y=1, container is
// the EM for x
var container = this.at(segmentsBefore);
if (!source.refOut)
source.refOut = new RefOutMap();
var ref = source.refOut[container.id];
if (ref == null) {
ref = new ModelRef(model, item, segmentsInside, container);
source.refOut[container.id] = ref;
}
return ref;
};
// Returns the EventModel node of the named child.
EventModel.prototype.child = function (segment) {
var container;
if (typeof segment === 'string') {
// Object
if (!this.object)
this.object = {};
container = this.object;
}
else if (typeof segment === 'number') {
// Array by value
if (!this.array)
this.array = [];
container = this.array;
}
else if (segment instanceof ModelRef) {
// Array reference. We'll need to lookup the child with the right
// value, then look inside its ref children for the right EventModel
// (so we can update it later). This is pretty janky, but should be
// *correct* even in the face of recursive array accessors.
//
// This will calculate it based on the current segment values, but refs
// cache the EM anyway.
//return this.child(segment.item).refChild(segment);
return segment.result;
}
else {
// Array by reference
if (!this.arrayByReference)
this.arrayByReference = [];
container = this.arrayByReference;
segment = segment.item;
}
(0, util_1.checkKeyIsSafe)(segment);
return container[segment] || (container[segment] = new EventModel());
};
// Returns the EventModel node at the given segments list. Note that although
// EventModel nodes are unique, its possible for multiple EventModel nodes to
// refer to the same section of the model because of references.
//
// If you want to update the bindings that refer to a specific path, use
// each().
//
// EventModel objects are created as needed.
EventModel.prototype.at = function (segments) {
// For unbound dependancies.
if (segments == null)
return this;
// eslint-disable-next-line @typescript-eslint/no-this-alias
var eventModel = this;
for (var i = 0; i < segments.length; i++) {
eventModel = eventModel.child(segments[i]);
}
return eventModel;
};
EventModel.prototype.isEmpty = function () {
if (hasKeys(this.dependancies))
return false;
if (hasKeys(this.itemContexts))
return false;
if (this.object) {
if (hasKeys(this.object))
return false;
this.object = null;
}
if (this.arrayByReference) {
for (var i = 0; i < this.arrayByReference.length; i++) {
if (this.arrayByReference[i] != null)
return false;
}
this.arrayByReference = null;
}
if (this.array) {
for (var i = 0; i < this.array.length; i++) {
if (this.array[i] != null)
return false;
}
this.array = null;
}
return true;
};
// **** Updating the EventModel
EventModel.prototype._addItemContext = function (context) {
if (!context._id)
context._id = nextId++;
if (!this.itemContexts)
this.itemContexts = new ItemContextsMap();
this.itemContexts[context._id] = context;
};
EventModel.prototype._removeItemContext = function (context) {
if (this.itemContexts) {
delete this.itemContexts[context._id];
}
};
EventModel.prototype._addBinding = function (binding) {
if (this.bindings == null) {
this.bindings = new BindingsMap();
}
var bindings = this.bindings;
if (binding.eventModels == null) {
binding.eventModels = new EventModelsMap();
}
bindings[binding.id] = binding;
binding.eventModels[this.id] = this;
};
// This is the main hook to add bindings to the event model tree. It should
// only be called on the root EventModel object.
EventModel.prototype.addBinding = function (segments, binding) {
this.at(segments)._addBinding(binding);
};
// This is used for objects (contexts in derby's case) that have a .item
// property which refers to an array index.
EventModel.prototype.addItemContext = function (segments, context) {
this.at(segments)._addItemContext(context);
};
EventModel.prototype.removeBinding = function (binding) {
if (!binding.eventModels)
return;
for (var id in binding.eventModels) {
var eventModel = binding.eventModels[id];
if (eventModel.bindings)
delete eventModel.bindings[binding.id];
}
binding.eventModels = null;
};
EventModel.prototype._each = function (segments, pos, fn) {
// Our refChildren are effectively merged into this object.
if (this.refChildren) {
for (var id in this.refChildren) {
var refChild = this.refChildren[id];
if (refChild)
refChild._each(segments, pos, fn);
}
}
if (segments.length === pos) {
fn(this);
return;
}
var segment = segments[pos];
var child;
if (typeof segment === 'string') {
// Object. Just recurse into our objects set. Its possible to rewrite this
// function to simply loop in the case of object lookups, but I don't think
// it'll buy us much.
child = this.object && this.object[segment];
if (child)
child._each(segments, pos + 1, fn);
}
else {
// Number. Recurse both into the fixed list and the reference list.
child = this.array && this.array[segment];
if (child)
child._each(segments, pos + 1, fn);
child = this.arrayByReference && this.arrayByReference[segment];
if (child)
child._each(segments, pos + 1, fn);
}
};
// Called when the scalar value at the path changes. This only calls update()
// on this node. See update() below if you want to update entire
// subtrees.
EventModel.prototype.localUpdate = function (previous, pass) {
if (this.bindings) {
for (var id in this.bindings) {
var binding = this.bindings[id];
if (binding)
binding.update(previous, pass);
}
}
// If our value changed, we also need to update anything that depends on it
// via refOut.
if (this.refOut) {
for (var id in this.refOut) {
var ref = this.refOut[id];
if (ref)
ref.update();
}
}
};
// This is used when an object subtree is replaced / removed.
EventModel.prototype.update = function (previous, pass) {
this.localUpdate(previous, pass);
if (this.object) {
for (var key in this.object) {
var binding = this.object[key];
if (binding)
binding.update();
}
}
if (this.array) {
for (var i = 0; i < this.array.length; i++) {
var binding = this.array[i];
if (binding)
binding.update();
}
}
if (this.arrayByReference) {
for (var i = 0; i < this.arrayByReference.length; i++) {
var binding = this.arrayByReference[i];
if (binding)
binding.update();
}
}
};
// Updates the indexes in itemContexts of our children in the range of
// [from, to). from and to both optional.
EventModel.prototype._updateChildItemContexts = function (from, to) {
if (!this.arrayByReference)
return;
if (from == null)
from = 0;
if (to == null)
to = this.arrayByReference.length;
for (var i = from; i < to; i++) {
var contexts = this.arrayByReference[i] &&
this.arrayByReference[i].itemContexts;
if (contexts) {
for (var key in contexts) {
contexts[key].item = i;
}
}
}
};
// Updates our array-by-value values. They have to recursively update every
// binding in their children. Sad.
EventModel.prototype._updateArray = function (from, to) {
if (!this.array)
return;
if (from == null)
from = 0;
if (to == null)
to = this.array.length;
for (var i = from; i < to; i++) {
var binding = this.array[i];
if (binding)
binding.update();
}
};
EventModel.prototype._updateObject = function () {
if (this.object) {
for (var key in this.object) {
var binding = this.object[key];
if (binding)
binding.update();
}
}
};
EventModel.prototype._set = function (previous, pass) {
// This just updates anything thats bound to the whole subtree. An alternate
// implementation could be passed in the new value at this node (which we
// cache), then compare with the old version and only update parts of the
// subtree which are relevant. I don't know if thats an important
// optimization - it really depends on your use case.
this.update(previous, pass);
};
// Insert into this EventModel node.
EventModel.prototype._insert = function (index, howMany) {
// Update fixed paths
this._updateArray(index);
// Update relative paths
if (this.arrayByReference && this.arrayByReference.length > index) {
// Shift the actual items in the array references array.
// This probably isn't the best way to implement insert. Other options are
// using concat() on slices or though constructing a temporary array and
// using splice.call. Hopefully if this method is slow it'll come up during
// profiling.
for (var i = 0; i < howMany; i++) {
this.arrayByReference.splice(index, 0, null);
}
// Update the path in the contexts
this._updateChildItemContexts(index + howMany);
}
// Finally call our bindings.
if (this.bindings) {
for (var id in this.bindings) {
var binding = this.bindings[id];
if (binding)
binding.insert(index, howMany);
}
}
this._updateObject();
};
// Remove howMany child elements from this EventModel at index.
EventModel.prototype._remove = function (index, howMany) {
// Update fixed paths. Both the removed items and items after it may have changed.
this._updateArray(index);
if (this.arrayByReference) {
// Update relative paths. First throw away all the children which have been removed.
this.arrayByReference.splice(index, howMany);
this._updateChildItemContexts(index);
}
// Call bindings.
if (this.bindings) {
for (var id in this.bindings) {
var binding = this.bindings[id];
if (binding)
binding.remove(index, howMany);
}
}
this._updateObject();
};
// Move howMany items from `from` to `to`.
EventModel.prototype._move = function (from, to, howMany) {
// first points to the first element that was moved. end points to the list
// element past the end of the changed region.
var first, end;
if (from < to) {
first = from;
end = to + howMany;
}
else {
first = to;
end = from + howMany;
}
// Update fixed paths.
this._updateArray(first, end);
// Update relative paths
var arr = this.arrayByReference;
if (arr && arr.length > first) {
// Remove from the old location
var values = arr.splice(from, howMany);
// Insert at the new location
// eslint-disable-next-line prefer-spread
arr.splice.apply(arr, [to, 0].concat(values));
// Update the path in the contexts
this._updateChildItemContexts(first, end);
}
// Finally call our bindings.
if (this.bindings) {
for (var id in this.bindings) {
var binding = this.bindings[id];
if (binding)
binding.move(from, to, howMany);
}
}
this._updateObject();
};
// Helpers.
EventModel.prototype.mutate = function (segments, fn) {
// This finds & returns a list of all event models which exist and could match
// the specified path. The path cannot contain contexts like derby expression
// segment lists (just because I don't think thats a useful feature and its not
// implemented)
this._each(segments, 0, fn);
// Also emit all mutations as sets on star paths, which are how dependencies
// for view helper functions are represented. They should react to a path
// or any child path being modified
for (var i = 0, len = segments.length; i++ < len;) {
var wildcardSegments = segments.slice(0, i);
wildcardSegments.push('*');
this._each(wildcardSegments, 0, childSetWildcard);
}
};
EventModel.prototype.set = function (segments, previous, pass) {
this.mutate(segments, function childSet(child) {
child._set(previous, pass);
});
};
EventModel.prototype.insert = function (segments, index, howMany) {
this.mutate(segments, function childInsert(child) {
child._insert(index, howMany);
});
};
EventModel.prototype.remove = function (segments, index, howMany) {
this.mutate(segments, function childRemove(child) {
child._remove(index, howMany);
});
};
EventModel.prototype.move = function (segments, from, to, howMany) {
this.mutate(segments, function childMove(child) {
child._move(from, to, howMany);
});
};
return EventModel;
}());
exports.EventModel = EventModel;