packages/oo7/src/bondCache.js
// (C) Copyright 2016-2017 Parity Technologies (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// The parent-side cache-server to which child-side BondCaches can connect.
// Will send messages of the form { bondCacheUpdate: { uuid: '...', value: ... }}
// value, if provided is the actual Bond value, not a stringification of it.
// Will try to send these only for UUIDs that it knows the child is interested
// in - child can register interest with a message { useBond: uuid } and
// unregister interest with { dropBond: uuid }.
//
// If you construct BondCache passing a deferParentPrefix arg, then it's up to
// you to ensure that the parent actually has a BondCacheProxy constructed. If
// it doesn't, things will go screwy.
let consoleDebug = typeof window !== 'undefined' && window.debugging ? console.debug : () => {};
class BondCache {
constructor (backupStorage, deferParentPrefix, surrogateWindow = null) {
this.window = surrogateWindow || (typeof window === 'undefined' ? null : window);
if (this.window) {
this.window.addEventListener('storage', this.onStorageChanged.bind(this));
this.window.addEventListener('unload', this.onUnload.bind(this));
this.window.addEventListener('message', this.onMessage.bind(this));
}
this.deferParentPrefix = this.window && this.window.parent ? deferParentPrefix : null;
this.regs = {};
// TODO: would be nice if this were better.
this.sessionId = Math.floor((1 + Math.random()) * 0x100000000).toString(16).substr(1);
consoleDebug('BondCache: Constructing', this.sessionId);
try {
this.storage = this.window ? this.window.localStorage : backupStorage;
} catch (e) {
this.storage = backupStorage;
}
}
initialise (uuid, bond, stringify, parse) {
consoleDebug('BondCache.initialise', this.sessionId, uuid, bond, this.regs);
if (!this.regs[uuid]) {
consoleDebug('BondCache.initialise: creating...');
this.regs[uuid] = { owned: false, deferred: false, users: [bond], primary: null, stringify, parse };
let key = '$_Bonds.' + uuid;
if (this.storage[key] !== undefined) {
consoleDebug('BondCache.initialise: restoring from persistent cache');
bond.changed(parse(this.storage[key]));
}
this.ensureActive(uuid);
consoleDebug('BondCache.initialise: Created reg', this.regs);
} else if (this.regs[uuid].primary === bond) {
consoleDebug('BondCache.initialise: Reactivating an inactive primary.');
if (this.regs[uuid].owned) {
console.error('BondCache.initialise: initialise called on already-active Bond.');
}
this.regs[uuid].owned = true;
} else {
consoleDebug('BondCache.initialise: appending to pre-existing entry', JSON.parse(JSON.stringify(this.regs[uuid])));
if (!this.regs[uuid].primary && !this.regs[uuid].deferred) {
console.error('BondCache.initialise: Registered Bond that has neither primary nor deferred.');
}
this.regs[uuid].users.push(bond);
let equivBond = (this.regs[uuid].primary || this.regs[uuid].users[0]);
if (equivBond.isReady()) {
consoleDebug('BondCache.initialise: restoring from equivalent active');
bond.changed(equivBond._value);
}
}
if (typeof window !== 'undefined' && window.debugging) {
this.checkConsistency();
}
}
checkConsistency () {
Object.keys(this.regs).forEach(uuid => {
let item = this.regs[uuid];
if (
(item.primary === null &&
!item.deferred &&
item.users.length > 0 &&
(this.storage['$_Bonds^' + uuid] === this.sessionId ||
!this.storage['$_Bonds^' + uuid])
) || (item.primary === null && item.owned)
) {
console.error('BondCache consistency failed!', this.regs);
}
});
}
changed (uuid, value) {
consoleDebug('BondCache.changed', this.sessionId, uuid, value, this.regs);
let item = this.regs[uuid];
if (item && this.storage['$_Bonds^' + uuid] === this.sessionId) {
let key = '$_Bonds.' + uuid;
if (value === undefined) {
delete this.storage[key];
item.users.forEach(bond => bond.reset());
} else {
this.storage[key] = item.stringify(value);
item.users.forEach(bond => bond.changed(value));
}
}
consoleDebug('BondCache.changed: complete', this.regs[uuid]);
}
finalise (uuid, bond) {
consoleDebug('BondCache.finalise', uuid, bond, this.regs);
let item = this.regs[uuid];
if (typeof item === 'undefined') {
console.error(`BondCache.finalise: called for unregistered UUID ${uuid}`, bond);
return;
}
if (item.primary === bond) {
consoleDebug('BondCache.finalise: We own; finalising Bond');
// TODO: decide whether to delete directly, or keep around.
let keepAround = true;
if (keepAround) {
item.owned = false;
// TODO: record the current time as an LRU and place the bond in a map for eventual deletion.
} else {
item.primary.finalise();
item.primary = null;
if (item.users.length === 0) {
consoleDebug('BondCache.finalise: No users; deleting entry and unreging from storage.');
// no owner and no users. we shold be the owner in
// storage. if we are, remove our key to signify to other
// tabs we're no longer maintaining this.
let storageKey = '$_Bonds^' + uuid;
let owner = this.storage[storageKey];
if (owner === this.sessionId) {
delete this.storage[storageKey];
}
} else {
consoleDebug('BondCache.finalise: Still users; ensuring active.');
// we removed the owner and there are users, must ensure that
// the bond is maintained.
this.ensureActive(uuid);
}
}
} else {
consoleDebug('BondCache.finalise: Not owner. Removing self from users.');
// otherwise, just remove the exiting bond from the users.
item.users = item.users.filter(b => b !== bond);
// If we're the last user from a parent-deferred Bond, then notify
// parent we're no longer bothered about further updates.
if (item.users.length === 0 && this.regs[uuid].deferred) {
consoleDebug('BondCache.finalise: dropping deferral from parent frame', uuid);
this.window.parent.postMessage({ dropBond: uuid }, '*');
this.regs[uuid].deferred = false;
}
}
if (item.primary === null && !item.deferred && item.users.length === 0) {
delete this.regs[uuid];
}
if (typeof window !== 'undefined' && window.debugging) {
this.checkConsistency();
}
}
ensureActive (uuid, key = '$_Bonds^' + uuid) {
consoleDebug('BondCache.ensureActive', uuid);
let item = this.regs[uuid];
if (item && item.users.length > 0 && item.primary && !item.owned) {
// would-be owners (users). no need for the primary any more.
consoleDebug('BondCache.ensureActive: Cleaning up orphan primary.');
item.primary.finalise();
item.primary = null;
item.owned = false;
}
if (item && item.users.length > 0 && item.primary === null && !item.deferred) {
consoleDebug('BondCache.ensureActive: Activating...');
if (item.owned) {
console.error('BondCache.ensureActive: INCONSISTENT. Cannot have no primary but be owned.');
}
if (this.deferParentPrefix && uuid.startsWith(this.deferParentPrefix)) {
consoleDebug('BondCache.ensureActive: deferring to parent frame', uuid);
item.deferred = true;
this.window.parent.postMessage({ useBond: uuid }, '*');
// One that we use - adopt it if necessary.
} else {
consoleDebug('BondCache.ensureActive: One that we use - adopt it if necessary.', this.storage[key], this.sessionId);
if (!this.storage[key]) {
consoleDebug('BondCache.ensureActive: No registered owner yet. Adopting');
this.storage[key] = this.sessionId;
}
if (this.storage[key] === this.sessionId) {
consoleDebug('BondCache.ensureActive: We are responsible for this UUID - initialise');
item.primary = item.users.pop();
item.owned = true;
item.primary.initialise();
}
}
}
}
reconstruct (updateMessage, bond) {
if (updateMessage.valueString) {
return bond._parse(updateMessage.valueString);
}
return updateMessage.value;
}
onMessage (e) {
// console.log('Received message', e);
if (this.window && e.source === this.window.parent) {
// Comes from parent.
// console.log('Message is from parent');
if (typeof e.data === 'object' && e.data !== null) {
let up = e.data.bondCacheUpdate;
if (up && this.regs[up.uuid]) {
consoleDebug('BondCache.onMessage: Bond cache update that we care about:', up.uuid);
let item = this.regs[up.uuid];
if (item.users.length > 0) {
let value = this.reconstruct(up, item.users[0]);
if (typeof value !== 'undefined') {
consoleDebug('BondCache.onMessage: Updating bond:', up.uuid, value, item.users);
item.users.forEach(bond => bond.changed(value));
} else {
consoleDebug('BondCache.onMessage: Resetting bond:', up.uuid, item.users);
item.users.forEach(bond => bond.reset());
}
}
}
}
}
}
onStorageChanged (e) {
if (!e.key.startsWith('$_Bonds')) {
return;
}
let uuid = e.key.substr(8);
let item = this.regs[uuid];
consoleDebug('BondCache.onStorageChanged', uuid, item);
if (!item) {
return;
}
if (e.key[7] === '.') {
// Bond changed...
if (typeof (this.storage[e.key]) === 'undefined') {
item.users.forEach(bond => bond.reset());
} else {
let v = item.parse(this.storage[e.key]);
item.users.forEach(bond => bond.changed(v));
}
} else if (e.key[7] === '^') {
// Owner going offline...
this.ensureActive(uuid, e.key);
}
}
onUnload () {
consoleDebug('BondCache.onUnload');
// Like drop for all items, except that we don't care about usage; we
// drop anyway.
Object.keys(this.regs).forEach(uuid => {
if (this.regs[uuid].deferred) {
consoleDebug('BondCache.onUnload: dropping deferral from parent frame', uuid);
this.window.parent.postMessage({ dropBond: uuid }, '*');
} else {
consoleDebug('BondCache.onUnload: dropping ownership key from storage', uuid);
let storageKey = '$_Bonds^' + uuid;
let owner = this.storage[storageKey];
if (owner === this.sessionId) {
delete this.storage[storageKey];
}
}
});
this.regs = {};
}
}
module.exports = BondCache;