mirror of
https://github.com/appy-one/acebase-core.git
synced 2026-07-11 22:01:16 -06:00
1219 lines
No EOL
58 KiB
JavaScript
1219 lines
No EOL
58 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.OrderedCollectionProxy = exports.proxyAccess = exports.LiveDataProxy = void 0;
|
|
const utils_1 = require("./utils");
|
|
const data_reference_1 = require("./data-reference");
|
|
const data_snapshot_1 = require("./data-snapshot");
|
|
const path_reference_1 = require("./path-reference");
|
|
const id_1 = require("./id");
|
|
const optional_observable_1 = require("./optional-observable");
|
|
const process_1 = require("./process");
|
|
const path_info_1 = require("./path-info");
|
|
const simple_event_emitter_1 = require("./simple-event-emitter");
|
|
class RelativeNodeTarget extends Array {
|
|
static areEqual(t1, t2) {
|
|
return t1.length === t2.length && t1.every((key, i) => t2[i] === key);
|
|
}
|
|
static isAncestor(ancestor, other) {
|
|
return ancestor.length < other.length && ancestor.every((key, i) => other[i] === key);
|
|
}
|
|
static isDescendant(descendant, other) {
|
|
return descendant.length > other.length && other.every((key, i) => descendant[i] === key);
|
|
}
|
|
}
|
|
const isProxy = Symbol('isProxy');
|
|
class LiveDataProxy {
|
|
/**
|
|
* Creates a live data proxy for the given reference. The data of the reference's path will be loaded, and kept in-sync
|
|
* with live data by listening for 'mutations' events. Any changes made to the value by the client will be synced back
|
|
* to the database.
|
|
* @param ref DataReference to create proxy for.
|
|
* @param options proxy initialization options
|
|
* be written to the database.
|
|
*/
|
|
static async create(ref, options) {
|
|
var _a;
|
|
ref = new data_reference_1.DataReference(ref.db, ref.path); // Use copy to prevent context pollution on original reference
|
|
let cache, loaded = false;
|
|
let latestCursor = options === null || options === void 0 ? void 0 : options.cursor;
|
|
let proxy;
|
|
const proxyId = id_1.ID.generate(); //ref.push().key;
|
|
// let onMutationCallback: ProxyObserveMutationsCallback;
|
|
// let onErrorCallback: ProxyObserveErrorCallback = err => {
|
|
// console.error(err.message, err.details);
|
|
// };
|
|
const clientSubscriptions = [];
|
|
const clientEventEmitter = new simple_event_emitter_1.SimpleEventEmitter();
|
|
clientEventEmitter.on('cursor', (cursor) => latestCursor = cursor);
|
|
clientEventEmitter.on('error', (err) => {
|
|
console.error(err.message, err.details);
|
|
});
|
|
const applyChange = (keys, newValue) => {
|
|
// Make changes to cache
|
|
if (keys.length === 0) {
|
|
cache = newValue;
|
|
return true;
|
|
}
|
|
const allowCreation = false; //cache === null; // If the proxy'd target did not exist upon load, we must allow it to be created now.
|
|
if (allowCreation) {
|
|
cache = typeof keys[0] === 'number' ? [] : {};
|
|
}
|
|
let target = cache;
|
|
const trailKeys = keys.slice();
|
|
while (trailKeys.length > 1) {
|
|
const key = trailKeys.shift();
|
|
if (!(key in target)) {
|
|
if (allowCreation) {
|
|
target[key] = typeof key === 'number' ? [] : {};
|
|
}
|
|
else {
|
|
// Have we missed an event, or are local pending mutations creating this conflict?
|
|
return false; // Do not proceed
|
|
}
|
|
}
|
|
target = target[key];
|
|
}
|
|
const prop = trailKeys.shift();
|
|
if (newValue === null) {
|
|
// Remove it
|
|
target instanceof Array ? target.splice(prop, 1) : delete target[prop];
|
|
}
|
|
else {
|
|
// Set or update it
|
|
target[prop] = newValue;
|
|
}
|
|
return true;
|
|
};
|
|
// Subscribe to mutations events on the target path
|
|
const syncFallback = async () => {
|
|
if (!loaded) {
|
|
return;
|
|
}
|
|
await reload();
|
|
};
|
|
const subscription = ref.on('mutations', { syncFallback }).subscribe(async (snap) => {
|
|
var _a;
|
|
if (!loaded) {
|
|
return;
|
|
}
|
|
const context = snap.context();
|
|
const isRemote = ((_a = context.acebase_proxy) === null || _a === void 0 ? void 0 : _a.id) !== proxyId;
|
|
if (!isRemote) {
|
|
return; // Update was done through this proxy, no need to update cache or trigger local value subscriptions
|
|
}
|
|
const mutations = snap.val(false);
|
|
const proceed = mutations.every(mutation => {
|
|
if (!applyChange(mutation.target, mutation.val)) {
|
|
return false;
|
|
}
|
|
// if (onMutationCallback) {
|
|
const changeRef = mutation.target.reduce((ref, key) => ref.child(key), ref);
|
|
const changeSnap = new data_snapshot_1.DataSnapshot(changeRef, mutation.val, false, mutation.prev, snap.context());
|
|
// onMutationCallback(changeSnap, isRemote); // onMutationCallback uses try/catch for client callback
|
|
clientEventEmitter.emit('mutation', { snapshot: changeSnap, isRemote });
|
|
// }
|
|
return true;
|
|
});
|
|
if (proceed) {
|
|
clientEventEmitter.emit('cursor', context.acebase_cursor); // // NOTE: cursor is only present in mutations done remotely. For our own updates, server cursors are returned by ref.set and ref.update
|
|
localMutationsEmitter.emit('mutations', { origin: 'remote', snap });
|
|
}
|
|
else {
|
|
console.warn(`Cached value of live data proxy on "${ref.path}" appears outdated, will be reloaded`);
|
|
await reload();
|
|
}
|
|
});
|
|
// Setup updating functionality: enqueue all updates, process them at next tick in the order they were issued
|
|
let processPromise = Promise.resolve();
|
|
const mutationQueue = [];
|
|
const transactions = [];
|
|
const pushLocalMutations = async () => {
|
|
// Sync all local mutations that are not in a transaction
|
|
const mutations = [];
|
|
for (let i = 0, m = mutationQueue[0]; i < mutationQueue.length; i++, m = mutationQueue[i]) {
|
|
if (!transactions.find(t => RelativeNodeTarget.areEqual(t.target, m.target) || RelativeNodeTarget.isAncestor(t.target, m.target))) {
|
|
mutationQueue.splice(i, 1);
|
|
i--;
|
|
mutations.push(m);
|
|
}
|
|
}
|
|
if (mutations.length === 0) {
|
|
return;
|
|
}
|
|
// Add current (new) values to mutations
|
|
mutations.forEach(mutation => {
|
|
mutation.value = (0, utils_1.cloneObject)(getTargetValue(cache, mutation.target));
|
|
});
|
|
// Run local onMutation & onChange callbacks in the next tick
|
|
process_1.default.nextTick(() => {
|
|
// Run onMutation callback for each changed node
|
|
const context = { acebase_proxy: { id: proxyId, source: 'update' } };
|
|
// if (onMutationCallback) {
|
|
mutations.forEach(mutation => {
|
|
const mutationRef = mutation.target.reduce((ref, key) => ref.child(key), ref);
|
|
const mutationSnap = new data_snapshot_1.DataSnapshot(mutationRef, mutation.value, false, mutation.previous, context);
|
|
// onMutationCallback(mutationSnap, false);
|
|
clientEventEmitter.emit('mutation', { snapshot: mutationSnap, isRemote: false });
|
|
});
|
|
// }
|
|
// Notify local subscribers
|
|
const snap = new data_snapshot_1.MutationsDataSnapshot(ref, mutations.map(m => ({ target: m.target, val: m.value, prev: m.previous })), context);
|
|
localMutationsEmitter.emit('mutations', { origin: 'local', snap });
|
|
});
|
|
// Update database async
|
|
// const batchId = ID.generate();
|
|
processPromise = mutations
|
|
.reduce((mutations, m, i, arr) => {
|
|
// Only keep top path mutations to prevent unneccessary child path updates
|
|
if (!arr.some(other => RelativeNodeTarget.isAncestor(other.target, m.target))) {
|
|
mutations.push(m);
|
|
}
|
|
return mutations;
|
|
}, [])
|
|
.reduce((updates, m) => {
|
|
// Prepare db updates
|
|
const target = m.target;
|
|
if (target.length === 0) {
|
|
// Overwrite this proxy's root value
|
|
updates.push({ ref, target, value: cache, type: 'set', previous: m.previous });
|
|
}
|
|
else {
|
|
const parentTarget = target.slice(0, -1);
|
|
const key = target.slice(-1)[0];
|
|
const parentRef = parentTarget.reduce((ref, key) => ref.child(key), ref);
|
|
const parentUpdate = updates.find(update => update.ref.path === parentRef.path);
|
|
const cacheValue = getTargetValue(cache, target); // m.value?
|
|
const prevValue = m.previous;
|
|
if (parentUpdate) {
|
|
parentUpdate.value[key] = cacheValue;
|
|
parentUpdate.previous[key] = prevValue;
|
|
}
|
|
else {
|
|
updates.push({ ref: parentRef, target: parentTarget, value: { [key]: cacheValue }, type: 'update', previous: { [key]: prevValue } });
|
|
}
|
|
}
|
|
return updates;
|
|
}, [])
|
|
.reduce(async (promise, update /*, i, updates */) => {
|
|
// Execute db update
|
|
// i === 0 && console.log(`Proxy: processing ${updates.length} db updates to paths:`, updates.map(update => update.ref.path));
|
|
const context = {
|
|
acebase_proxy: {
|
|
id: proxyId,
|
|
source: update.type,
|
|
// update_id: ID.generate(),
|
|
// batch_id: batchId,
|
|
// batch_updates: updates.length
|
|
},
|
|
};
|
|
await promise;
|
|
await update.ref
|
|
.context(context)[update.type](update.value) // .set or .update
|
|
.catch(err => {
|
|
clientEventEmitter.emit('error', { source: 'update', message: `Error processing update of "/${ref.path}"`, details: err });
|
|
// console.warn(`Proxy could not update DB, should rollback (${update.type}) the proxy value of "${update.ref.path}" to: `, update.previous);
|
|
const context = { acebase_proxy: { id: proxyId, source: 'update-rollback' } };
|
|
const mutations = [];
|
|
if (update.type === 'set') {
|
|
setTargetValue(cache, update.target, update.previous);
|
|
const mutationSnap = new data_snapshot_1.DataSnapshot(update.ref, update.previous, false, update.value, context);
|
|
clientEventEmitter.emit('mutation', { snapshot: mutationSnap, isRemote: false });
|
|
mutations.push({ target: update.target, val: update.previous, prev: update.value });
|
|
}
|
|
else {
|
|
// update
|
|
Object.keys(update.previous).forEach(key => {
|
|
setTargetValue(cache, update.target.concat(key), update.previous[key]);
|
|
const mutationSnap = new data_snapshot_1.DataSnapshot(update.ref.child(key), update.previous[key], false, update.value[key], context);
|
|
clientEventEmitter.emit('mutation', { snapshot: mutationSnap, isRemote: false });
|
|
mutations.push({ target: update.target.concat(key), val: update.previous[key], prev: update.value[key] });
|
|
});
|
|
}
|
|
// Run onMutation callback for each node being rolled back
|
|
mutations.forEach(m => {
|
|
const mutationRef = m.target.reduce((ref, key) => ref.child(key), ref);
|
|
const mutationSnap = new data_snapshot_1.DataSnapshot(mutationRef, m.val, false, m.prev, context);
|
|
clientEventEmitter.emit('mutation', { snapshot: mutationSnap, isRemote: false });
|
|
});
|
|
// Notify local subscribers:
|
|
const snap = new data_snapshot_1.MutationsDataSnapshot(update.ref, mutations, context);
|
|
localMutationsEmitter.emit('mutations', { origin: 'local', snap });
|
|
});
|
|
if (update.ref.cursor) {
|
|
// Should also be available in context.acebase_cursor now
|
|
clientEventEmitter.emit('cursor', update.ref.cursor);
|
|
}
|
|
}, processPromise);
|
|
await processPromise;
|
|
};
|
|
let syncInProgress = false;
|
|
const syncPromises = [];
|
|
const syncCompleted = () => {
|
|
let resolve;
|
|
const promise = new Promise(rs => resolve = rs);
|
|
syncPromises.push({ resolve });
|
|
return promise;
|
|
};
|
|
let processQueueTimeout = null;
|
|
const scheduleSync = () => {
|
|
if (!processQueueTimeout) {
|
|
processQueueTimeout = setTimeout(async () => {
|
|
syncInProgress = true;
|
|
processQueueTimeout = null;
|
|
await pushLocalMutations();
|
|
syncInProgress = false;
|
|
syncPromises.splice(0).forEach(p => p.resolve());
|
|
}, 0);
|
|
}
|
|
};
|
|
const flagOverwritten = (target) => {
|
|
if (!mutationQueue.find(m => RelativeNodeTarget.areEqual(m.target, target))) {
|
|
mutationQueue.push({ target, previous: (0, utils_1.cloneObject)(getTargetValue(cache, target)) });
|
|
}
|
|
// schedule database updates
|
|
scheduleSync();
|
|
};
|
|
const localMutationsEmitter = new simple_event_emitter_1.SimpleEventEmitter();
|
|
const addOnChangeHandler = (target, callback) => {
|
|
const isObject = (val) => val !== null && typeof val === 'object';
|
|
const mutationsHandler = async (details) => {
|
|
var _a;
|
|
const { snap, origin } = details;
|
|
const context = snap.context();
|
|
const causedByOurProxy = ((_a = context.acebase_proxy) === null || _a === void 0 ? void 0 : _a.id) === proxyId;
|
|
if (details.origin === 'remote' && causedByOurProxy) {
|
|
// Any local changes already triggered subscription callbacks
|
|
console.error('DEV ISSUE: mutationsHandler was called from remote event originating from our own proxy');
|
|
return;
|
|
}
|
|
const mutations = snap.val(false).filter(mutation => {
|
|
// Keep mutations impacting the subscribed target: mutations on target, or descendant or ancestor of target
|
|
return mutation.target.slice(0, target.length).every((key, i) => target[i] === key);
|
|
});
|
|
if (mutations.length === 0) {
|
|
return;
|
|
}
|
|
let newValue, previousValue;
|
|
// If there is a mutation on the target itself, or parent/ancestor path, there can only be one. We can take a shortcut
|
|
const singleMutation = mutations.find(m => m.target.length <= target.length);
|
|
if (singleMutation) {
|
|
const trailKeys = target.slice(singleMutation.target.length);
|
|
newValue = trailKeys.reduce((val, key) => !isObject(val) || !(key in val) ? null : val[key], singleMutation.val);
|
|
previousValue = trailKeys.reduce((val, key) => !isObject(val) || !(key in val) ? null : val[key], singleMutation.prev);
|
|
}
|
|
else {
|
|
// All mutations are on children/descendants of our target
|
|
// Construct new & previous values by combining cache and snapshot
|
|
const currentValue = getTargetValue(cache, target);
|
|
newValue = (0, utils_1.cloneObject)(currentValue);
|
|
previousValue = (0, utils_1.cloneObject)(newValue);
|
|
mutations.forEach(mutation => {
|
|
// mutation.target is relative to proxy root
|
|
const trailKeys = mutation.target.slice(target.length);
|
|
for (let i = 0, val = newValue, prev = previousValue; i < trailKeys.length; i++) { // arr = PathInfo.getPathKeys(mutationPath).slice(PathInfo.getPathKeys(targetRef.path).length)
|
|
const last = i + 1 === trailKeys.length, key = trailKeys[i];
|
|
if (last) {
|
|
val[key] = mutation.val;
|
|
if (val[key] === null) {
|
|
delete val[key];
|
|
}
|
|
prev[key] = mutation.prev;
|
|
if (prev[key] === null) {
|
|
delete prev[key];
|
|
}
|
|
}
|
|
else {
|
|
val = val[key] = key in val ? val[key] : {};
|
|
prev = prev[key] = key in prev ? prev[key] : {};
|
|
}
|
|
}
|
|
});
|
|
}
|
|
process_1.default.nextTick(() => {
|
|
// Run callback with read-only (frozen) values in next tick
|
|
let keepSubscription = true;
|
|
try {
|
|
keepSubscription = false !== callback(Object.freeze(newValue), Object.freeze(previousValue), !causedByOurProxy, context);
|
|
}
|
|
catch (err) {
|
|
clientEventEmitter.emit('error', { source: origin === 'remote' ? 'remote_update' : 'local_update', message: 'Error running subscription callback', details: err });
|
|
}
|
|
if (keepSubscription === false) {
|
|
stop();
|
|
}
|
|
});
|
|
};
|
|
localMutationsEmitter.on('mutations', mutationsHandler);
|
|
const stop = () => {
|
|
localMutationsEmitter.off('mutations').off('mutations', mutationsHandler);
|
|
clientSubscriptions.splice(clientSubscriptions.findIndex(cs => cs.stop === stop), 1);
|
|
};
|
|
clientSubscriptions.push({ target, stop });
|
|
return { stop };
|
|
};
|
|
const handleFlag = (flag, target, args) => {
|
|
if (flag === 'write') {
|
|
return flagOverwritten(target);
|
|
}
|
|
else if (flag === 'onChange') {
|
|
return addOnChangeHandler(target, args.callback);
|
|
}
|
|
else if (flag === 'subscribe' || flag === 'observe') {
|
|
const subscribe = (subscriber) => {
|
|
const currentValue = getTargetValue(cache, target);
|
|
subscriber.next(currentValue);
|
|
const subscription = addOnChangeHandler(target, (value /*, previous, isRemote, context */) => {
|
|
subscriber.next(value);
|
|
});
|
|
return function unsubscribe() {
|
|
subscription.stop();
|
|
};
|
|
};
|
|
if (flag === 'subscribe') {
|
|
return subscribe;
|
|
}
|
|
// Try to load Observable
|
|
const Observable = (0, optional_observable_1.getObservable)();
|
|
return new Observable(subscribe);
|
|
}
|
|
else if (flag === 'transaction') {
|
|
const hasConflictingTransaction = transactions.some(t => RelativeNodeTarget.areEqual(target, t.target) || RelativeNodeTarget.isAncestor(target, t.target) || RelativeNodeTarget.isDescendant(target, t.target));
|
|
if (hasConflictingTransaction) {
|
|
// TODO: Wait for this transaction to finish, then try again
|
|
return Promise.reject(new Error('Cannot start transaction because it conflicts with another transaction'));
|
|
}
|
|
return new Promise(async (resolve) => {
|
|
// If there are pending mutations on target (or deeper), wait until they have been synchronized
|
|
const hasPendingMutations = mutationQueue.some(m => RelativeNodeTarget.areEqual(target, m.target) || RelativeNodeTarget.isAncestor(target, m.target));
|
|
if (hasPendingMutations) {
|
|
if (!syncInProgress) {
|
|
scheduleSync();
|
|
}
|
|
await syncCompleted();
|
|
}
|
|
const tx = { target, status: 'started', transaction: null };
|
|
transactions.push(tx);
|
|
tx.transaction = {
|
|
get status() { return tx.status; },
|
|
get completed() { return tx.status !== 'started'; },
|
|
get mutations() {
|
|
return mutationQueue.filter(m => RelativeNodeTarget.areEqual(tx.target, m.target) || RelativeNodeTarget.isAncestor(tx.target, m.target));
|
|
},
|
|
get hasMutations() {
|
|
return this.mutations.length > 0;
|
|
},
|
|
async commit() {
|
|
if (this.completed) {
|
|
throw new Error(`Transaction has completed already (status '${tx.status}')`);
|
|
}
|
|
tx.status = 'finished';
|
|
transactions.splice(transactions.indexOf(tx), 1);
|
|
if (syncInProgress) {
|
|
// Currently syncing without our mutations
|
|
await syncCompleted();
|
|
}
|
|
scheduleSync();
|
|
await syncCompleted();
|
|
},
|
|
rollback() {
|
|
// Remove mutations from queue
|
|
if (this.completed) {
|
|
throw new Error(`Transaction has completed already (status '${tx.status}')`);
|
|
}
|
|
tx.status = 'canceled';
|
|
const mutations = [];
|
|
for (let i = 0; i < mutationQueue.length; i++) {
|
|
const m = mutationQueue[i];
|
|
if (RelativeNodeTarget.areEqual(tx.target, m.target) || RelativeNodeTarget.isAncestor(tx.target, m.target)) {
|
|
mutationQueue.splice(i, 1);
|
|
i--;
|
|
mutations.push(m);
|
|
}
|
|
}
|
|
// Replay mutations in reverse order
|
|
mutations.reverse()
|
|
.forEach(m => {
|
|
if (m.target.length === 0) {
|
|
cache = m.previous;
|
|
}
|
|
else {
|
|
setTargetValue(cache, m.target, m.previous);
|
|
}
|
|
});
|
|
// Remove transaction
|
|
transactions.splice(transactions.indexOf(tx), 1);
|
|
},
|
|
};
|
|
resolve(tx.transaction);
|
|
});
|
|
}
|
|
};
|
|
const snap = await ref.get({ cache_mode: 'allow', cache_cursor: options === null || options === void 0 ? void 0 : options.cursor });
|
|
// const gotOfflineStartValue = snap.context().acebase_origin === 'cache';
|
|
// if (gotOfflineStartValue) {
|
|
// console.warn(`Started data proxy with cached value of "${ref.path}", check if its value is reloaded on next connection!`);
|
|
// }
|
|
if (snap.context().acebase_origin !== 'cache') {
|
|
clientEventEmitter.emit('cursor', (_a = ref.cursor) !== null && _a !== void 0 ? _a : null); // latestCursor = snap.context().acebase_cursor ?? null;
|
|
}
|
|
loaded = true;
|
|
cache = snap.val();
|
|
if (cache === null && typeof (options === null || options === void 0 ? void 0 : options.defaultValue) !== 'undefined') {
|
|
cache = options.defaultValue;
|
|
const context = {
|
|
acebase_proxy: {
|
|
id: proxyId,
|
|
source: 'default',
|
|
// update_id: ID.generate()
|
|
},
|
|
};
|
|
await ref.context(context).set(cache);
|
|
}
|
|
proxy = createProxy({ root: { ref, get cache() { return cache; } }, target: [], id: proxyId, flag: handleFlag });
|
|
const assertProxyAvailable = () => {
|
|
if (proxy === null) {
|
|
throw new Error('Proxy was destroyed');
|
|
}
|
|
};
|
|
const reload = async () => {
|
|
// Manually reloads current value when cache is out of sync, which should only
|
|
// be able to happen if an AceBaseClient is used without cache database,
|
|
// and the connection to the server was lost for a while. In all other cases,
|
|
// there should be no need to call this method.
|
|
assertProxyAvailable();
|
|
mutationQueue.splice(0); // Remove pending mutations. Will be empty in production, but might not be while debugging, leading to weird behaviour.
|
|
const snap = await ref.get({ allow_cache: false });
|
|
const oldVal = cache, newVal = snap.val();
|
|
cache = newVal;
|
|
// Compare old and new values
|
|
const mutations = (0, utils_1.getMutations)(oldVal, newVal);
|
|
if (mutations.length === 0) {
|
|
return; // Nothing changed
|
|
}
|
|
// Run onMutation callback for each changed node
|
|
const context = snap.context(); // context might contain acebase_cursor if server support that
|
|
context.acebase_proxy = { id: proxyId, source: 'reload' };
|
|
// if (onMutationCallback) {
|
|
mutations.forEach(m => {
|
|
const targetRef = getTargetRef(ref, m.target);
|
|
const newSnap = new data_snapshot_1.DataSnapshot(targetRef, m.val, m.val === null, m.prev, context);
|
|
clientEventEmitter.emit('mutation', { snapshot: newSnap, isRemote: true });
|
|
});
|
|
// }
|
|
// Notify local subscribers
|
|
const mutationsSnap = new data_snapshot_1.MutationsDataSnapshot(ref, mutations, context);
|
|
localMutationsEmitter.emit('mutations', { origin: 'local', snap: mutationsSnap });
|
|
};
|
|
return {
|
|
async destroy() {
|
|
await processPromise;
|
|
const promises = [
|
|
subscription.stop(),
|
|
...clientSubscriptions.map(cs => cs.stop()),
|
|
];
|
|
await Promise.all(promises);
|
|
['cursor', 'mutation', 'error'].forEach(event => clientEventEmitter.off(event));
|
|
cache = null; // Remove cache
|
|
proxy = null;
|
|
},
|
|
stop() {
|
|
this.destroy();
|
|
},
|
|
get value() {
|
|
assertProxyAvailable();
|
|
return proxy;
|
|
},
|
|
get hasValue() {
|
|
assertProxyAvailable();
|
|
return cache !== null;
|
|
},
|
|
set value(val) {
|
|
// Overwrite the value of the proxied path itself!
|
|
assertProxyAvailable();
|
|
if (val !== null && typeof val === 'object' && val[isProxy]) {
|
|
// Assigning one proxied value to another
|
|
val = val.valueOf();
|
|
}
|
|
flagOverwritten([]);
|
|
cache = val;
|
|
},
|
|
get ref() {
|
|
return ref;
|
|
},
|
|
get cursor() {
|
|
return latestCursor;
|
|
},
|
|
reload,
|
|
onMutation(callback) {
|
|
// Fires callback each time anything changes
|
|
assertProxyAvailable();
|
|
clientEventEmitter.off('mutation'); // Mimic legacy behaviour that overwrites handler
|
|
clientEventEmitter.on('mutation', ({ snapshot, isRemote }) => {
|
|
try {
|
|
callback(snapshot, isRemote);
|
|
}
|
|
catch (err) {
|
|
clientEventEmitter.emit('error', { source: 'mutation_callback', message: 'Error in dataproxy onMutation callback', details: err });
|
|
}
|
|
});
|
|
},
|
|
onError(callback) {
|
|
// Fires callback each time anything goes wrong
|
|
assertProxyAvailable();
|
|
clientEventEmitter.off('error'); // Mimic legacy behaviour that overwrites handler
|
|
clientEventEmitter.on('error', (err) => {
|
|
try {
|
|
callback(err);
|
|
}
|
|
catch (err) {
|
|
console.error(`Error in dataproxy onError callback: ${err.message}`);
|
|
}
|
|
});
|
|
},
|
|
on(event, callback) {
|
|
clientEventEmitter.on(event, callback);
|
|
},
|
|
off(event, callback) {
|
|
clientEventEmitter.off(event, callback);
|
|
},
|
|
};
|
|
}
|
|
}
|
|
exports.LiveDataProxy = LiveDataProxy;
|
|
function getTargetValue(obj, target) {
|
|
let val = obj;
|
|
for (const key of target) {
|
|
val = typeof val === 'object' && val !== null && key in val ? val[key] : null;
|
|
}
|
|
return val;
|
|
}
|
|
function setTargetValue(obj, target, value) {
|
|
if (target.length === 0) {
|
|
throw new Error('Cannot update root target, caller must do that itself!');
|
|
}
|
|
const targetObject = target.slice(0, -1).reduce((obj, key) => obj[key], obj);
|
|
const prop = target.slice(-1)[0];
|
|
if (value === null || typeof value === 'undefined') {
|
|
// Remove it
|
|
targetObject instanceof Array ? targetObject.splice(prop, 1) : delete targetObject[prop];
|
|
}
|
|
else {
|
|
// Set or update it
|
|
targetObject[prop] = value;
|
|
}
|
|
}
|
|
function getTargetRef(ref, target) {
|
|
// Create new DataReference to prevent context reuse
|
|
const path = path_info_1.PathInfo.get(ref.path).childPath(target);
|
|
return new data_reference_1.DataReference(ref.db, path);
|
|
}
|
|
function createProxy(context) {
|
|
const targetRef = getTargetRef(context.root.ref, context.target);
|
|
const childProxies = [];
|
|
const handler = {
|
|
get(target, prop, receiver) {
|
|
target = getTargetValue(context.root.cache, context.target);
|
|
if (typeof prop === 'symbol') {
|
|
if (prop.toString() === Symbol.iterator.toString()) {
|
|
// Use .values for @@iterator symbol
|
|
prop = 'values';
|
|
}
|
|
else if (prop.toString() === isProxy.toString()) {
|
|
return true;
|
|
}
|
|
else {
|
|
return Reflect.get(target, prop, receiver);
|
|
}
|
|
}
|
|
if (prop === 'valueOf') {
|
|
return function valueOf() { return target; };
|
|
}
|
|
if (target === null || typeof target !== 'object') {
|
|
throw new Error(`Cannot read property "${prop}" of ${target}. Value of path "/${targetRef.path}" is not an object (anymore)`);
|
|
}
|
|
if (target instanceof Array && typeof prop === 'string' && /^[0-9]+$/.test(prop)) {
|
|
// Proxy type definitions say prop can be a number, but this is never the case.
|
|
prop = parseInt(prop);
|
|
}
|
|
const value = target[prop];
|
|
if (value === null) {
|
|
// Removed property. Should never happen, but if it does:
|
|
delete target[prop];
|
|
return; // undefined
|
|
}
|
|
// Check if we have a child proxy for this property already.
|
|
// If so, and the properties' typeof value did not change, return that
|
|
const childProxy = childProxies.find(proxy => proxy.prop === prop);
|
|
if (childProxy) {
|
|
if (childProxy.typeof === typeof value) {
|
|
return childProxy.value;
|
|
}
|
|
childProxies.splice(childProxies.indexOf(childProxy), 1);
|
|
}
|
|
const proxifyChildValue = (prop) => {
|
|
const value = target[prop]; //
|
|
const childProxy = childProxies.find(child => child.prop === prop);
|
|
if (childProxy) {
|
|
if (childProxy.typeof === typeof value) {
|
|
return childProxy.value;
|
|
}
|
|
childProxies.splice(childProxies.indexOf(childProxy), 1);
|
|
}
|
|
if (typeof value !== 'object') {
|
|
// Can't proxify non-object values
|
|
return value;
|
|
}
|
|
const newChildProxy = createProxy({ root: context.root, target: context.target.concat(prop), id: context.id, flag: context.flag });
|
|
childProxies.push({ typeof: typeof value, prop, value: newChildProxy });
|
|
return newChildProxy;
|
|
};
|
|
const unproxyValue = (value) => {
|
|
return value !== null && typeof value === 'object' && value[isProxy]
|
|
? value.getTarget()
|
|
: value;
|
|
};
|
|
// If the property contains a simple value, return it.
|
|
if (['string', 'number', 'boolean'].includes(typeof value)
|
|
|| value instanceof Date
|
|
|| value instanceof path_reference_1.PathReference
|
|
|| value instanceof ArrayBuffer
|
|
|| (typeof value === 'object' && 'buffer' in value) // Typed Arrays
|
|
) {
|
|
return value;
|
|
}
|
|
const isArray = target instanceof Array;
|
|
if (prop === 'toString') {
|
|
return function toString() {
|
|
return `[LiveDataProxy for "${targetRef.path}"]`;
|
|
};
|
|
}
|
|
if (typeof value === 'undefined') {
|
|
if (prop === 'push') {
|
|
// Push item to an object collection
|
|
return function push(item) {
|
|
const childRef = targetRef.push();
|
|
context.flag('write', context.target.concat(childRef.key)); //, { previous: null }
|
|
target[childRef.key] = item;
|
|
return childRef.key;
|
|
};
|
|
}
|
|
if (prop === 'getTarget') {
|
|
// Get unproxied readonly (but still live) version of data.
|
|
return function (warn = true) {
|
|
warn && console.warn('Use getTarget with caution - any changes will not be synchronized!');
|
|
return target;
|
|
};
|
|
}
|
|
if (prop === 'getRef') {
|
|
// Gets the DataReference to this data target
|
|
return function getRef() {
|
|
const ref = getTargetRef(context.root.ref, context.target);
|
|
return ref;
|
|
};
|
|
}
|
|
if (prop === 'forEach') {
|
|
return function forEach(callback) {
|
|
const keys = Object.keys(target);
|
|
// Fix: callback with unproxied value
|
|
let stop = false;
|
|
for (let i = 0; !stop && i < keys.length; i++) {
|
|
const key = keys[i];
|
|
const value = proxifyChildValue(key); //, target[key]
|
|
stop = callback(value, key, i) === false;
|
|
}
|
|
};
|
|
}
|
|
if (['values', 'entries', 'keys'].includes(prop)) {
|
|
return function* generator() {
|
|
const keys = Object.keys(target);
|
|
for (const key of keys) {
|
|
if (prop === 'keys') {
|
|
yield key;
|
|
}
|
|
else {
|
|
const value = proxifyChildValue(key); //, target[key]
|
|
if (prop === 'entries') {
|
|
yield [key, value];
|
|
}
|
|
else {
|
|
yield value;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
if (prop === 'toArray') {
|
|
return function toArray(sortFn) {
|
|
const arr = Object.keys(target).map(key => proxifyChildValue(key)); //, target[key]
|
|
if (sortFn) {
|
|
arr.sort(sortFn);
|
|
}
|
|
return arr;
|
|
};
|
|
}
|
|
if (prop === 'onChanged') {
|
|
// Starts monitoring the value
|
|
return function onChanged(callback) {
|
|
return context.flag('onChange', context.target, { callback });
|
|
};
|
|
}
|
|
if (prop === 'subscribe') {
|
|
// Gets subscriber function to use with Observables, or custom handling
|
|
return function subscribe() {
|
|
return context.flag('subscribe', context.target);
|
|
};
|
|
}
|
|
if (prop === 'getObservable') {
|
|
// Creates an observable for monitoring the value
|
|
return function getObservable() {
|
|
return context.flag('observe', context.target);
|
|
};
|
|
}
|
|
if (prop === 'getOrderedCollection') {
|
|
return function getOrderedCollection(orderProperty, orderIncrement) {
|
|
return new OrderedCollectionProxy(this, orderProperty, orderIncrement);
|
|
};
|
|
}
|
|
if (prop === 'startTransaction') {
|
|
return function startTransaction() {
|
|
return context.flag('transaction', context.target);
|
|
};
|
|
}
|
|
if (prop === 'remove' && !isArray) {
|
|
// Removes target from object collection
|
|
return function remove() {
|
|
if (context.target.length === 0) {
|
|
throw new Error('Can\'t remove proxy root value');
|
|
}
|
|
const parent = getTargetValue(context.root.cache, context.target.slice(0, -1));
|
|
const key = context.target.slice(-1)[0];
|
|
context.flag('write', context.target);
|
|
delete parent[key];
|
|
};
|
|
}
|
|
return; // undefined
|
|
}
|
|
else if (typeof value === 'function') {
|
|
if (isArray) {
|
|
// Handle array methods
|
|
const writeArray = (action) => {
|
|
context.flag('write', context.target);
|
|
return action();
|
|
};
|
|
const cleanArrayValues = (values) => values.map((value) => {
|
|
value = unproxyValue(value);
|
|
removeVoidProperties(value);
|
|
return value;
|
|
});
|
|
// Methods that directly change the array:
|
|
if (prop === 'push') {
|
|
return function push(...items) {
|
|
items = cleanArrayValues(items);
|
|
return writeArray(() => target.push(...items)); // push the items to the cache array
|
|
};
|
|
}
|
|
if (prop === 'pop') {
|
|
return function pop() {
|
|
return writeArray(() => target.pop());
|
|
};
|
|
}
|
|
if (prop === 'splice') {
|
|
return function splice(start, deleteCount, ...items) {
|
|
items = cleanArrayValues(items);
|
|
return writeArray(() => target.splice(start, deleteCount, ...items));
|
|
};
|
|
}
|
|
if (prop === 'shift') {
|
|
return function shift() {
|
|
return writeArray(() => target.shift());
|
|
};
|
|
}
|
|
if (prop === 'unshift') {
|
|
return function unshift(...items) {
|
|
items = cleanArrayValues(items);
|
|
return writeArray(() => target.unshift(...items));
|
|
};
|
|
}
|
|
if (prop === 'sort') {
|
|
return function sort(compareFn) {
|
|
return writeArray(() => target.sort(compareFn));
|
|
};
|
|
}
|
|
if (prop === 'reverse') {
|
|
return function reverse() {
|
|
return writeArray(() => target.reverse());
|
|
};
|
|
}
|
|
// Methods that do not change the array themselves, but
|
|
// have callbacks that might, or return child values:
|
|
if (['indexOf', 'lastIndexOf'].includes(prop)) {
|
|
return function indexOf(item, start) {
|
|
if (item !== null && typeof item === 'object' && item[isProxy]) {
|
|
// Use unproxied value, or array.indexOf will return -1 (fixes issue #1)
|
|
item = item.getTarget(false);
|
|
}
|
|
return target[prop](item, start);
|
|
};
|
|
}
|
|
if (['forEach', 'every', 'some', 'filter', 'map'].includes(prop)) {
|
|
return function iterate(callback) {
|
|
return target[prop]((value, i) => {
|
|
return callback(proxifyChildValue(i), i, proxy); //, value
|
|
});
|
|
};
|
|
}
|
|
if (['reduce', 'reduceRight'].includes(prop)) {
|
|
return function reduce(callback, initialValue) {
|
|
return target[prop]((prev, value, i) => {
|
|
return callback(prev, proxifyChildValue(i), i, proxy); //, value
|
|
}, initialValue);
|
|
};
|
|
}
|
|
if (['find', 'findIndex'].includes(prop)) {
|
|
return function find(callback) {
|
|
let value = target[prop]((value, i) => {
|
|
return callback(proxifyChildValue(i), i, proxy); // , value
|
|
});
|
|
if (prop === 'find' && value) {
|
|
const index = target.indexOf(value);
|
|
value = proxifyChildValue(index); //, value
|
|
}
|
|
return value;
|
|
};
|
|
}
|
|
if (['values', 'entries', 'keys'].includes(prop)) {
|
|
return function* generator() {
|
|
for (let i = 0; i < target.length; i++) {
|
|
if (prop === 'keys') {
|
|
yield i;
|
|
}
|
|
else {
|
|
const value = proxifyChildValue(i); //, target[i]
|
|
if (prop === 'entries') {
|
|
yield [i, value];
|
|
}
|
|
else {
|
|
yield value;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
// Other function (or not an array), should not alter its value
|
|
// return function fn(...args) {
|
|
// return target[prop](...args);
|
|
// }
|
|
return value;
|
|
}
|
|
// Proxify any other value
|
|
return proxifyChildValue(prop); //, value
|
|
},
|
|
set(target, prop, value, receiver) {
|
|
// Eg: chats.chat1.title = 'New chat title';
|
|
// target === chats.chat1, prop === 'title'
|
|
target = getTargetValue(context.root.cache, context.target);
|
|
if (typeof prop === 'symbol') {
|
|
return Reflect.set(target, prop, value, receiver);
|
|
}
|
|
if (target === null || typeof target !== 'object') {
|
|
throw new Error(`Cannot set property "${prop}" of ${target}. Value of path "/${targetRef.path}" is not an object`);
|
|
}
|
|
if (target instanceof Array && typeof prop === 'string') {
|
|
if (!/^[0-9]+$/.test(prop)) {
|
|
throw new Error(`Cannot set property "${prop}" on array value of path "/${targetRef.path}"`);
|
|
}
|
|
prop = parseInt(prop);
|
|
}
|
|
if (value !== null) {
|
|
if (typeof value === 'object') {
|
|
if (value[isProxy]) {
|
|
// Assigning one proxied value to another
|
|
value = value.valueOf();
|
|
}
|
|
// else if (Object.isFrozen(value)) {
|
|
// // Create a copy to unfreeze it
|
|
// value = cloneObject(value);
|
|
// }
|
|
value = (0, utils_1.cloneObject)(value); // Fix #10, always clone objects so changes made through the proxy won't change the original object (and vice versa)
|
|
}
|
|
if ((0, utils_1.valuesAreEqual)(value, target[prop])) { //if (compareValues(value, target[prop]) === 'identical') { // (typeof value !== 'object' && target[prop] === value) {
|
|
// not changing the actual value, ignore
|
|
return true;
|
|
}
|
|
}
|
|
if (context.target.some(key => typeof key === 'number')) {
|
|
// Updating an object property inside an array. Flag the first array in target to be written.
|
|
// Eg: when chat.members === [{ name: 'Ewout', id: 'someid' }]
|
|
// --> chat.members[0].name = 'Ewout' --> Rewrite members array instead of chat/members[0]/name
|
|
context.flag('write', context.target.slice(0, context.target.findIndex(key => typeof key === 'number')));
|
|
}
|
|
else if (target instanceof Array) {
|
|
// Flag the entire array to be overwritten
|
|
context.flag('write', context.target);
|
|
}
|
|
else {
|
|
// Flag child property
|
|
context.flag('write', context.target.concat(prop));
|
|
}
|
|
// Set cached value:
|
|
if (value === null) {
|
|
delete target[prop];
|
|
}
|
|
else {
|
|
removeVoidProperties(value);
|
|
target[prop] = value;
|
|
}
|
|
return true;
|
|
},
|
|
deleteProperty(target, prop) {
|
|
target = getTargetValue(context.root.cache, context.target);
|
|
if (target === null) {
|
|
throw new Error(`Cannot delete property ${prop.toString()} of null`);
|
|
}
|
|
if (typeof prop === 'symbol') {
|
|
return Reflect.deleteProperty(target, prop);
|
|
}
|
|
if (!(prop in target)) {
|
|
return true; // Nothing to delete
|
|
}
|
|
context.flag('write', context.target.concat(prop));
|
|
delete target[prop];
|
|
return true;
|
|
},
|
|
ownKeys(target) {
|
|
target = getTargetValue(context.root.cache, context.target);
|
|
return Reflect.ownKeys(target);
|
|
},
|
|
has(target, prop) {
|
|
target = getTargetValue(context.root.cache, context.target);
|
|
return Reflect.has(target, prop);
|
|
},
|
|
getOwnPropertyDescriptor(target, prop) {
|
|
target = getTargetValue(context.root.cache, context.target);
|
|
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
if (descriptor) {
|
|
descriptor.configurable = true; // prevent "TypeError: 'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property '...' which is either non-existant or configurable in the proxy target"
|
|
}
|
|
return descriptor;
|
|
},
|
|
getPrototypeOf(target) {
|
|
target = getTargetValue(context.root.cache, context.target);
|
|
return Reflect.getPrototypeOf(target);
|
|
},
|
|
};
|
|
const proxy = new Proxy({}, handler);
|
|
return proxy;
|
|
}
|
|
function removeVoidProperties(obj) {
|
|
if (typeof obj !== 'object') {
|
|
return;
|
|
}
|
|
Object.keys(obj).forEach(key => {
|
|
const val = obj[key];
|
|
if (val === null || typeof val === 'undefined') {
|
|
delete obj[key];
|
|
}
|
|
else if (typeof val === 'object') {
|
|
removeVoidProperties(val);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Convenience function to access ILiveDataProxyValue methods on a proxied value
|
|
* @param proxiedValue The proxied value to get access to
|
|
* @returns Returns the same object typecasted to an ILiveDataProxyValue
|
|
* @example
|
|
* // IChatMessages is an ObjectCollection<IChatMessage>
|
|
* let observable: Observable<IChatMessages>;
|
|
*
|
|
* // Allows you to do this:
|
|
* observable = proxyAccess<IChatMessages>(chat.messages).getObservable();
|
|
*
|
|
* // Instead of:
|
|
* observable = (chat.messages.msg1 as any as ILiveDataProxyValue<IChatMessages>).getObservable();
|
|
*
|
|
* // Both do the exact same, but the first is less obscure
|
|
*/
|
|
function proxyAccess(proxiedValue) {
|
|
if (typeof proxiedValue !== 'object' || !proxiedValue[isProxy]) {
|
|
throw new Error('Given value is not proxied. Make sure you are referencing the value through the live data proxy.');
|
|
}
|
|
return proxiedValue;
|
|
}
|
|
exports.proxyAccess = proxyAccess;
|
|
/**
|
|
* Provides functionality to work with ordered collections through a live data proxy. Eliminates
|
|
* the need for arrays to handle ordered data by adding a 'sort' properties to child objects in a
|
|
* collection, and provides functionality to sort and reorder items with a minimal amount of database
|
|
* updates.
|
|
*/
|
|
class OrderedCollectionProxy {
|
|
constructor(collection, orderProperty = 'order', orderIncrement = 10) {
|
|
this.collection = collection;
|
|
this.orderProperty = orderProperty;
|
|
this.orderIncrement = orderIncrement;
|
|
if (typeof collection !== 'object' || !collection[isProxy]) {
|
|
throw new Error('Collection is not proxied');
|
|
}
|
|
if (collection.valueOf() instanceof Array) {
|
|
throw new Error('Collection is an array, not an object collection');
|
|
}
|
|
if (!Object.keys(collection).every(key => typeof collection[key] === 'object')) {
|
|
throw new Error('Collection has non-object children');
|
|
}
|
|
// Check if the collection has order properties. If not, assign them now
|
|
const ok = Object.keys(collection).every(key => typeof collection[key][orderProperty] === 'number');
|
|
if (!ok) {
|
|
// Assign order properties now. Database will be updated automatically
|
|
const keys = Object.keys(collection);
|
|
for (let i = 0; i < keys.length; i++) {
|
|
const item = collection[keys[i]];
|
|
item[orderProperty] = i * orderIncrement; // 0, 10, 20, 30 etc
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Gets an observable for the target object collection. Same as calling `collection.getObservable()`
|
|
* @returns
|
|
*/
|
|
getObservable() {
|
|
return proxyAccess(this.collection).getObservable();
|
|
}
|
|
/**
|
|
* Gets an observable that emits a new ordered array representation of the object collection each time
|
|
* the unlaying data is changed. Same as calling `getArray()` in a `getObservable().subscribe` callback
|
|
* @returns
|
|
*/
|
|
getArrayObservable() {
|
|
const Observable = (0, optional_observable_1.getObservable)();
|
|
return new Observable((subscriber => {
|
|
const subscription = this.getObservable().subscribe(( /*value*/) => {
|
|
const newArray = this.getArray();
|
|
subscriber.next(newArray);
|
|
});
|
|
return function unsubscribe() {
|
|
subscription.unsubscribe();
|
|
};
|
|
}));
|
|
}
|
|
/**
|
|
* Gets an ordered array representation of the items in your object collection. The items in the array
|
|
* are proxied values, changes will be in sync with the database. Note that the array itself
|
|
* is not mutable: adding or removing items to it will NOT update the collection in the
|
|
* the database and vice versa. Use `add`, `delete`, `sort` and `move` methods to make changes
|
|
* that impact the collection's sorting order
|
|
* @returns order array
|
|
*/
|
|
getArray() {
|
|
const arr = proxyAccess(this.collection).toArray((a, b) => a[this.orderProperty] - b[this.orderProperty]);
|
|
// arr.push = (...items: T[]) => {
|
|
// items.forEach(item => this.add(item));
|
|
// return arr.length;
|
|
// };
|
|
return arr;
|
|
}
|
|
add(newItem, index, from) {
|
|
const item = newItem;
|
|
const arr = this.getArray();
|
|
let minOrder = Number.POSITIVE_INFINITY, maxOrder = Number.NEGATIVE_INFINITY;
|
|
for (let i = 0; i < arr.length; i++) {
|
|
const order = arr[i][this.orderProperty];
|
|
minOrder = Math.min(order, minOrder);
|
|
maxOrder = Math.max(order, maxOrder);
|
|
}
|
|
let fromKey;
|
|
if (typeof from === 'number') {
|
|
// Moving existing item
|
|
fromKey = Object.keys(this.collection).find(key => this.collection[key] === item);
|
|
if (!fromKey) {
|
|
throw new Error('item not found in collection');
|
|
}
|
|
if (from === index) {
|
|
return { key: fromKey, index };
|
|
}
|
|
if (Math.abs(from - index) === 1) {
|
|
// Position being swapped, swap their order property values
|
|
const otherItem = arr[index];
|
|
const otherOrder = otherItem[this.orderProperty];
|
|
otherItem[this.orderProperty] = item[this.orderProperty];
|
|
item[this.orderProperty] = otherOrder;
|
|
return { key: fromKey, index };
|
|
}
|
|
else {
|
|
// Remove from array, code below will add again
|
|
arr.splice(from, 1);
|
|
}
|
|
}
|
|
if (typeof index !== 'number' || index >= arr.length) {
|
|
// append at the end
|
|
index = arr.length;
|
|
item[this.orderProperty] = arr.length == 0 ? 0 : maxOrder + this.orderIncrement;
|
|
}
|
|
else if (index === 0) {
|
|
// insert before all others
|
|
item[this.orderProperty] = arr.length == 0 ? 0 : minOrder - this.orderIncrement;
|
|
}
|
|
else {
|
|
// insert between 2 others
|
|
const orders = arr.map(item => item[this.orderProperty]);
|
|
const gap = orders[index] - orders[index - 1];
|
|
if (gap > 1) {
|
|
item[this.orderProperty] = orders[index] - Math.floor(gap / 2);
|
|
}
|
|
else {
|
|
// TODO: Can this gap be enlarged by moving one of both orders?
|
|
// For now, change all other orders
|
|
arr.splice(index, 0, item);
|
|
for (let i = 0; i < arr.length; i++) {
|
|
arr[i][this.orderProperty] = i * this.orderIncrement;
|
|
}
|
|
}
|
|
}
|
|
const key = typeof fromKey === 'string'
|
|
? fromKey // Moved item, don't add it
|
|
: proxyAccess(this.collection).push(item);
|
|
return { key, index };
|
|
}
|
|
/**
|
|
* Deletes an item from the object collection using the their index in the sorted array representation
|
|
* @param index
|
|
* @returns the key of the collection's child that was deleted
|
|
*/
|
|
delete(index) {
|
|
const arr = this.getArray();
|
|
const item = arr[index];
|
|
if (!item) {
|
|
throw new Error(`Item at index ${index} not found`);
|
|
}
|
|
const key = Object.keys(this.collection).find(key => this.collection[key] === item);
|
|
if (!key) {
|
|
throw new Error('Cannot find target object to delete');
|
|
}
|
|
this.collection[key] = null; // Deletes it from db
|
|
return { key, index };
|
|
}
|
|
/**
|
|
* Moves an item in the object collection by reordering it
|
|
* @param fromIndex Current index in the array (the ordered representation of the object collection)
|
|
* @param toIndex Target index in the array
|
|
* @returns
|
|
*/
|
|
move(fromIndex, toIndex) {
|
|
const arr = this.getArray();
|
|
return this.add(arr[fromIndex], toIndex, fromIndex);
|
|
}
|
|
/**
|
|
* Reorders the object collection using given sort function. Allows quick reordering of the collection which is persisted in the database
|
|
* @param sortFn
|
|
*/
|
|
sort(sortFn) {
|
|
const arr = this.getArray();
|
|
arr.sort(sortFn);
|
|
for (let i = 0; i < arr.length; i++) {
|
|
arr[i][this.orderProperty] = i * this.orderIncrement;
|
|
}
|
|
}
|
|
}
|
|
exports.OrderedCollectionProxy = OrderedCollectionProxy;
|
|
//# sourceMappingURL=data-proxy.js.map
|