return function connect(protocolInstance, protocolName, url, options, dbAliveID) { /// <param name="protocolInstance" type="ISyncProtocol"></param> var existingPeer = activePeers.filter(function (peer) { return peer.url === url; }); if (existingPeer.length > 0) { // Never create multiple syncNodes with same protocolName and url. Instead, let the next call to connect() return the same promise that // have already been started and eventually also resolved. If promise has already resolved (node connected), calling existing promise.then() will give a callback directly. return existingPeer[0].connectPromise; } // Use an object otherwise we wouldn't be able to get the reject promise from // connectProtocol var rejectConnectPromise = {p: null}; const connectProtocol = initConnectProtocol(db, protocolInstance, dbAliveID, options, rejectConnectPromise); const getOrCreateSyncNode = initGetOrCreateSyncNode(db, protocolName, url); var connectPromise = getOrCreateSyncNode(options).then(function (node) { return connectProtocol(node, activePeer); }); var disconnected = false; var activePeer = { url: url, status: Statuses.OFFLINE, connectPromise: connectPromise, on: Dexie.Events(null, "disconnect"), disconnect: function (newStatus, error) { var pos = activePeers.indexOf(activePeer); if (pos >= 0) activePeers.splice(pos, 1); if (error && rejectConnectPromise.p) rejectConnectPromise.p(error); if (!disconnected) { activePeer.on.disconnect.fire(newStatus, error); } disconnected = true; } }; activePeers.push(activePeer); return connectPromise; };
export default function Syncable (db) { /// <param name="db" type="Dexie"></param> var activePeers = []; const connectFn = initConnectFn(db, activePeers); const syncableConnect = initSyncableConnect(db, connectFn); db.on('message', function(msg) { // Message from other local node arrives... Dexie.vip(function() { if (msg.type === 'connect') { // We are master node and another non-master node wants us to do the connect. db.syncable.connect(msg.message.protocolName, msg.message.url, msg.message.options).then(msg.resolve, msg.reject); } else if (msg.type === 'disconnect') { db.syncable.disconnect(msg.message.url).then(msg.resolve, msg.reject); } else if (msg.type === 'syncStatusChanged') { // We are client and a master node informs us about syncStatus change. // Lookup the connectedProvider and call its event db.syncable.on.statusChanged.fire(msg.message.newStatus, msg.message.url); } }); }); db.on('cleanup', function(weBecameMaster) { // A cleanup (done in Dexie.Observable) may result in that a master node is removed and we become master. if (weBecameMaster) { // We took over the master role in Observable's cleanup method. // We should connect to remote servers now. // At this point, also reconnect servers with status ERROR_WILL_RETRY as well as plain ERROR. // Reason to reconnect to those with plain "ERROR" is that the ERROR state may occur when a database // connection has been closed. The new master would then be expected to reconnect. // Also, this is not an infinite poll(). This is rare event that a new browser tab takes over from // an old closed one. Dexie.ignoreTransaction(()=>Dexie.vip(()=>{ return db._syncNodes.where({type: 'remote'}) .filter(node => node.status !== Statuses.OFFLINE) .toArray(connectedRemoteNodes => Promise.all(connectedRemoteNodes.map(node => db.syncable.connect(node.syncProtocol, node.url, node.syncOptions).catch(e => { console.warn(`Dexie.Syncable: Could not connect to ${node.url}. ${e.stack || e}`); }) ))); })).catch('DatabaseClosedError', ()=>{}); } }); // "ready" subscriber for the master node that makes sure it will always connect to sync server // when the database opens. It will not wait for the connection to complete, just initiate the // connection so that it will continue syncing as long as the database is open. // Dexie.Observable's 'ready' subscriber will have been invoked prior to this, making sure // that db._localSyncNode exists and persisted before this subscriber kicks in. db.on('ready', function onReady() { // Again, in onReady: If we ARE master, make sure to connect to remote servers that is in a connected state. if (db._localSyncNode && db._localSyncNode.isMaster) { // Make sure to connect to remote servers that is in a connected state (NOT OFFLINE or ERROR!) // This "ready" subscriber will never be the one performing the initial sync request, because // even after calling db.syncable.connect(), there won't exist any "remote" sync node yet. // Instead, db.syncable.connect() will subscribe to "ready" also, and that subscriber will be // called after this one. There, in that subscriber, the initial sync request will take place // and the "remote" node will be created so that this "ready" subscriber can auto-connect the // next time this database is opened. // CONCLUSION: We can always assume that the local DB has been in sync with the server at least // once in the past for each "connectedRemoteNode" we find in query below. // Don't halt db.ready while connecting (i.e. we do not return a promise here!) db._syncNodes .where('type').equals('remote') .and(node => node.status !== Statuses.OFFLINE) .toArray(connectedRemoteNodes => { // There are connected remote nodes that we must manage (or take over to manage) connectedRemoteNodes.forEach( node => db.syncable.connect( node.syncProtocol, node.url, node.syncOptions) .catch (()=>{}) // A failure will be triggered in on('statusChanged'). We can ignore. ); }).catch('DatabaseClosedError', ()=>{}); } }, true); // True means the ready event will survive a db reopen - db.close()/db.open() db.syncable = {}; db.syncable.getStatus = function(url, cb) { if (db.isOpen()) { return Dexie.vip(function() { return db._syncNodes.where('url').equals(url).first(function(node) { return node ? node.status : Statuses.OFFLINE; }); }).then(cb); } else { return Promise.resolve(Syncable.Statuses.OFFLINE).then(cb); } }; db.syncable.getOptions = function(url, cb) { return db.transaction('r?', db._syncNodes, () => { return db._syncNodes.where('url').equals(url).first(function(node) { return node.syncOptions; }).then(cb); }); }; db.syncable.list = function() { return db.transaction('r?', db._syncNodes, ()=>{ return db._syncNodes.where('type').equals('remote').toArray(function(a) { return a.map(function(node) { return node.url; }); }); }); }; db.syncable.on = Dexie.Events(db, { statusChanged: "asap" }); db.syncable.disconnect = function(url) { return Dexie.ignoreTransaction(()=>{ return Promise.resolve().then(()=>{ if (db._localSyncNode && db._localSyncNode.isMaster) { return Promise.all(activePeers.filter(peer => peer.url === url).map(peer => { return peer.disconnect(Statuses.OFFLINE); })); } else { return db._syncNodes.where('isMaster').above(0).first(masterNode => { return db.observable.sendMessage('disconnect', { url: url }, masterNode.id, {wantReply: true}); }); } }).then(()=>{ return db._syncNodes.where("url").equals(url).modify(node => { node.status = Statuses.OFFLINE; }); }); }); }; db.syncable.connect = function(protocolName, url, options) { options = options || {}; // Make sure options is always an object because 1) Provider expects it to be. 2) We'll be persisting it and you cannot persist undefined. var protocolInstance = Syncable.registeredProtocols[protocolName]; if (protocolInstance) { return syncableConnect(protocolInstance, protocolName, url, options); } else { return Promise.reject( new Error("ISyncProtocol '" + protocolName + "' is not registered in Dexie.Syncable.registerSyncProtocol()") ); } }; db.syncable.delete = function(url) { return db.syncable.disconnect(url).then(()=>{ return db.transaction('rw!', db._syncNodes, db._changes, db._uncommittedChanges, ()=>{ // Find the node(s) // Several can be found, as detected by @martindiphoorn, // let's delete them and cleanup _uncommittedChanges and _changes // accordingly. let nodeIDsToDelete; return db._syncNodes .where("url").equals(url) .toArray(nodes => nodes.map(node => node.id)) .then(nodeIDs => { nodeIDsToDelete = nodeIDs; // Delete the syncNode that represents the remote endpoint. return db._syncNodes.where('id').anyOf(nodeIDs).delete() }) .then (() => // In case there were uncommittedChanges belonging to this, delete them as well db._uncommittedChanges.where('node').anyOf(nodeIDsToDelete).delete()); }).then(()=> { // Spawn background job to delete old changes, now that a node has been deleted, // there might be changes in _changes table that is not needed to keep anymore. // This is done in its own transaction, or possible several transaction to prohibit // starvation Observable.deleteOldChanges(db); }); }); }; db.syncable.unsyncedChanges = function(url) { return db._syncNodes.where("url").equals(url).first(function(node) { return db._changes.where('rev').above(node.myRevision).toArray(); }); }; db.close = override(db.close, function(origClose) { return function() { activePeers.forEach(function(peer) { peer.disconnect(); }); return origClose.apply(this, arguments); }; }); Object.defineProperty( db.observable.SyncNode.prototype, 'save', { enumerable: false, configurable: true, writable: true, value() { return db.transaction('rw?', db._syncNodes, () => { return db._syncNodes.put(this); }); } }); }
if (res && typeof res.then === 'function') { var thiz = this, args = arguments; return res.then(function() { return f2.apply(thiz, args); }); } return f2.apply(this, arguments); }; } // // Static properties and methods // Observable.latestRevision = {}; // Latest revision PER DATABASE. Example: Observable.latestRevision.FriendsDB = 37; Observable.on = Dexie.Events(null, "latestRevisionIncremented", "suicideNurseCall", "intercomm", "beforeunload"); // fire(dbname, value); Observable.createUUID = function() { // Decent solution from http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript var d = Date.now(); var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = (d + Math.random() * 16) % 16 | 0; d = Math.floor(d / 16); return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16); }); return uuid; }; Observable.deleteOldChanges = function(db) { db._syncNodes.orderBy("myRevision").first(function (oldestNode) { var timeout = Date.now() + 300, timedout = false;