You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-29 16:43:09 +03:00
Start first incremental sync request early (#629)
* Start first incremental sync request early So it can run while we process our sync data.
This commit is contained in:
committed by
Luke Barnard
parent
349297e495
commit
16c062c069
@@ -137,6 +137,7 @@ describe("MatrixClient", function() {
|
|||||||
"getSyncAccumulator", "startup", "deleteAllData",
|
"getSyncAccumulator", "startup", "deleteAllData",
|
||||||
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
|
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
|
||||||
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
|
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
|
||||||
|
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
|
||||||
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
|
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
|
||||||
client = new MatrixClient({
|
client = new MatrixClient({
|
||||||
baseUrl: "https://my.home.server",
|
baseUrl: "https://my.home.server",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -243,6 +244,10 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getNextBatchToken: function() {
|
||||||
|
return Promise.resolve(this._syncAccumulator.getNextBatchToken());
|
||||||
|
},
|
||||||
|
|
||||||
setSyncData: function(syncData) {
|
setSyncData: function(syncData) {
|
||||||
return Promise.resolve().then(() => {
|
return Promise.resolve().then(() => {
|
||||||
this._syncAccumulator.accumulate(syncData);
|
this._syncAccumulator.accumulate(syncData);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -74,6 +75,10 @@ RemoteIndexedDBStoreBackend.prototype = {
|
|||||||
return this._doCmd('getSavedSync');
|
return this._doCmd('getSavedSync');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getNextBatchToken: function() {
|
||||||
|
return this._doCmd('getNextBatchToken');
|
||||||
|
},
|
||||||
|
|
||||||
setSyncData: function(syncData) {
|
setSyncData: function(syncData) {
|
||||||
return this._doCmd('setSyncData', [syncData]);
|
return this._doCmd('setSyncData', [syncData]);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -88,6 +89,9 @@ class IndexedDBStoreWorker {
|
|||||||
case 'getUserPresenceEvents':
|
case 'getUserPresenceEvents':
|
||||||
prom = this.backend.getUserPresenceEvents();
|
prom = this.backend.getUserPresenceEvents();
|
||||||
break;
|
break;
|
||||||
|
case 'getNextBatchToken':
|
||||||
|
prom = this.backend.getNextBatchToken();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prom === undefined) {
|
if (prom === undefined) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -145,6 +146,14 @@ IndexedDBStore.prototype.getSavedSync = function() {
|
|||||||
return this.backend.getSavedSync();
|
return this.backend.getSavedSync();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise} If there is a saved sync, the nextBatch token
|
||||||
|
* for this sync, otherwise null.
|
||||||
|
*/
|
||||||
|
IndexedDBStore.prototype.getSavedSyncToken = function() {
|
||||||
|
return this.backend.getNextBatchToken();
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all data from this store.
|
* Delete all data from this store.
|
||||||
* @return {Promise} Resolves if the data was deleted from the database.
|
* @return {Promise} Resolves if the data was deleted from the database.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -346,6 +347,14 @@ module.exports.MatrixInMemoryStore.prototype = {
|
|||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise} If there is a saved sync, the nextBatch token
|
||||||
|
* for this sync, otherwise null.
|
||||||
|
*/
|
||||||
|
getSavedSyncToken: function() {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all data from this store.
|
* Delete all data from this store.
|
||||||
* @return {Promise} An immediately resolved promise.
|
* @return {Promise} An immediately resolved promise.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -247,6 +248,14 @@ StubStore.prototype = {
|
|||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise} If there is a saved sync, the nextBatch token
|
||||||
|
* for this sync, otherwise null.
|
||||||
|
*/
|
||||||
|
getSavedSyncToken: function() {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all data from this store. Does nothing since this store
|
* Delete all data from this store. Does nothing since this store
|
||||||
* doesn't store anything.
|
* doesn't store anything.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -520,6 +521,10 @@ class SyncAccumulator {
|
|||||||
accountData: accData,
|
accountData: accData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNextBatchToken() {
|
||||||
|
return this.nextBatch;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setState(eventMap, event) {
|
function setState(eventMap, event) {
|
||||||
|
|||||||
203
src/sync.js
203
src/sync.js
@@ -396,6 +396,17 @@ SyncApi.prototype.getSyncState = function() {
|
|||||||
return this._syncState;
|
return this._syncState;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
SyncApi.prototype.recoverFromSyncStartupError = async function(savedSyncPromise, err) {
|
||||||
|
// Wait for the saved sync to complete - we send the pushrules and filter requests
|
||||||
|
// before the saved sync has finished so they can run in parallel, but only process
|
||||||
|
// the results after the saved sync is done. Equivalently, we wait for it to finish
|
||||||
|
// before reporting failures from these functions.
|
||||||
|
await savedSyncPromise;
|
||||||
|
const keepaliveProm = this._startKeepAlives();
|
||||||
|
this._updateSyncState("ERROR", { error: err });
|
||||||
|
await keepaliveProm;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main entry point
|
* Main entry point
|
||||||
*/
|
*/
|
||||||
@@ -410,28 +421,32 @@ SyncApi.prototype.sync = function() {
|
|||||||
global.document.addEventListener("online", this._onOnlineBound, false);
|
global.document.addEventListener("online", this._onOnlineBound, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let savedSyncPromise = Promise.resolve();
|
||||||
|
let savedSyncToken = null;
|
||||||
|
|
||||||
// We need to do one-off checks before we can begin the /sync loop.
|
// We need to do one-off checks before we can begin the /sync loop.
|
||||||
// These are:
|
// These are:
|
||||||
// 1) We need to get push rules so we can check if events should bing as we get
|
// 1) We need to get push rules so we can check if events should bing as we get
|
||||||
// them from /sync.
|
// them from /sync.
|
||||||
// 2) We need to get/create a filter which we can use for /sync.
|
// 2) We need to get/create a filter which we can use for /sync.
|
||||||
|
|
||||||
function getPushRules() {
|
async function getPushRules() {
|
||||||
client.getPushRules().done((result) => {
|
try {
|
||||||
|
const result = await client.getPushRules();
|
||||||
debuglog("Got push rules");
|
debuglog("Got push rules");
|
||||||
|
|
||||||
client.pushRules = result;
|
client.pushRules = result;
|
||||||
|
} catch (err) {
|
||||||
getFilter(); // Now get the filter and start syncing
|
// wait for saved sync to complete before doing anything else,
|
||||||
}, (err) => {
|
// otherwise the sync state will end up being incorrect
|
||||||
self._startKeepAlives().done(() => {
|
await self.recoverFromSyncStartupError(savedSyncPromise, err);
|
||||||
getPushRules();
|
getPushRules();
|
||||||
});
|
return;
|
||||||
self._updateSyncState("ERROR", { error: err });
|
}
|
||||||
});
|
getFilter(); // Now get the filter and start syncing
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilter() {
|
async function getFilter() {
|
||||||
let filter;
|
let filter;
|
||||||
if (self.opts.filter) {
|
if (self.opts.filter) {
|
||||||
filter = self.opts.filter;
|
filter = self.opts.filter;
|
||||||
@@ -440,40 +455,55 @@ SyncApi.prototype.sync = function() {
|
|||||||
filter.setTimelineLimit(self.opts.initialSyncLimit);
|
filter.setTimelineLimit(self.opts.initialSyncLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
client.getOrCreateFilter(
|
let filterId;
|
||||||
|
try {
|
||||||
|
filterId = await client.getOrCreateFilter(
|
||||||
getFilterName(client.credentials.userId), filter,
|
getFilterName(client.credentials.userId), filter,
|
||||||
).done((filterId) => {
|
);
|
||||||
|
} catch (err) {
|
||||||
|
// wait for saved sync to complete before doing anything else,
|
||||||
|
// otherwise the sync state will end up being incorrect
|
||||||
|
await self.recoverFromSyncStartupError(savedSyncPromise, err);
|
||||||
|
getFilter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// reset the notifications timeline to prepare it to paginate from
|
// reset the notifications timeline to prepare it to paginate from
|
||||||
// the current point in time.
|
// the current point in time.
|
||||||
// The right solution would be to tie /sync pagination tokens into
|
// The right solution would be to tie /sync pagination tokens into
|
||||||
// /notifications API somehow.
|
// /notifications API somehow.
|
||||||
client.resetNotifTimelineSet();
|
client.resetNotifTimelineSet();
|
||||||
|
|
||||||
|
if (self._currentSyncRequest === null) {
|
||||||
|
// Send this first sync request here so we can then wait for the saved
|
||||||
|
// sync data to finish processing before we process the results of this one.
|
||||||
|
console.log("Sending first sync request...");
|
||||||
|
self._currentSyncRequest = self._doSyncRequest({ filterId }, savedSyncToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now wait for the saved sync to finish...
|
||||||
|
await savedSyncPromise;
|
||||||
self._sync({ filterId });
|
self._sync({ filterId });
|
||||||
}, (err) => {
|
|
||||||
self._startKeepAlives().done(function() {
|
|
||||||
getFilter();
|
|
||||||
});
|
|
||||||
self._updateSyncState("ERROR", { error: err });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client.isGuest()) {
|
if (client.isGuest()) {
|
||||||
// no push rules for guests, no access to POST filter for guests.
|
// no push rules for guests, no access to POST filter for guests.
|
||||||
self._sync({});
|
self._sync({});
|
||||||
} else {
|
} else {
|
||||||
// Before fetching push rules, fetching the filter and syncing, check
|
// Pull the saved sync token out first, before the worker starts sending
|
||||||
// for persisted /sync data and use that if present.
|
// all the sync data which could take a while. This will let us send our
|
||||||
client.store.getSavedSync().then((savedSync) => {
|
// first incremental sync request before we've processed our saved data.
|
||||||
|
savedSyncPromise = client.store.getSavedSyncToken().then((tok) => {
|
||||||
|
savedSyncToken = tok;
|
||||||
|
return client.store.getSavedSync();
|
||||||
|
}).then((savedSync) => {
|
||||||
if (savedSync) {
|
if (savedSync) {
|
||||||
return self._syncFromCache(savedSync);
|
return self._syncFromCache(savedSync);
|
||||||
}
|
}
|
||||||
}).then(() => {
|
|
||||||
// Get push rules and start syncing after getting the saved sync
|
|
||||||
// to handle the case where we needed the `nextBatch` token to
|
|
||||||
// start syncing from.
|
|
||||||
getPushRules();
|
|
||||||
});
|
});
|
||||||
|
// Now start the first incremental sync request: this can also
|
||||||
|
// take a while so if we set it going now, we can wait for it
|
||||||
|
// to finish while we process our saved sync data.
|
||||||
|
getPushRules();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -565,70 +595,20 @@ SyncApi.prototype._sync = async function(syncOptions) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let filterId = syncOptions.filterId;
|
|
||||||
if (client.isGuest() && !filterId) {
|
|
||||||
filterId = this._getGuestFilter();
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncToken = client.store.getSyncToken();
|
const syncToken = client.store.getSyncToken();
|
||||||
|
|
||||||
let pollTimeout = this.opts.pollTimeout;
|
|
||||||
|
|
||||||
if (this.getSyncState() !== 'SYNCING' || this._catchingUp) {
|
|
||||||
// unless we are happily syncing already, we want the server to return
|
|
||||||
// as quickly as possible, even if there are no events queued. This
|
|
||||||
// serves two purposes:
|
|
||||||
//
|
|
||||||
// * When the connection dies, we want to know asap when it comes back,
|
|
||||||
// so that we can hide the error from the user. (We don't want to
|
|
||||||
// have to wait for an event or a timeout).
|
|
||||||
//
|
|
||||||
// * We want to know if the server has any to_device messages queued up
|
|
||||||
// for us. We do that by calling it with a zero timeout until it
|
|
||||||
// doesn't give us any more to_device messages.
|
|
||||||
this._catchingUp = true;
|
|
||||||
pollTimeout = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// normal timeout= plus buffer time
|
|
||||||
const clientSideTimeoutMs = pollTimeout + BUFFER_PERIOD_MS;
|
|
||||||
|
|
||||||
const qps = {
|
|
||||||
filter: filterId,
|
|
||||||
timeout: pollTimeout,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.opts.disablePresence) {
|
|
||||||
qps.set_presence = "offline";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (syncToken) {
|
|
||||||
qps.since = syncToken;
|
|
||||||
} else {
|
|
||||||
// use a cachebuster for initialsyncs, to make sure that
|
|
||||||
// we don't get a stale sync
|
|
||||||
// (https://github.com/vector-im/vector-web/issues/1354)
|
|
||||||
qps._cacheBuster = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.getSyncState() == 'ERROR' || this.getSyncState() == 'RECONNECTING') {
|
|
||||||
// we think the connection is dead. If it comes back up, we won't know
|
|
||||||
// about it till /sync returns. If the timeout= is high, this could
|
|
||||||
// be a long time. Set it to 0 when doing retries so we don't have to wait
|
|
||||||
// for an event or a timeout before emiting the SYNCING event.
|
|
||||||
qps.timeout = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
//debuglog('Starting sync since=' + syncToken);
|
//debuglog('Starting sync since=' + syncToken);
|
||||||
this._currentSyncRequest = client._http.authedRequest(
|
if (this._currentSyncRequest === null) {
|
||||||
undefined, "GET", "/sync", qps, undefined, clientSideTimeoutMs,
|
this._currentSyncRequest = this._doSyncRequest(syncOptions, syncToken);
|
||||||
);
|
}
|
||||||
data = await this._currentSyncRequest;
|
data = await this._currentSyncRequest;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._onSyncError(e, syncOptions);
|
this._onSyncError(e, syncOptions);
|
||||||
return;
|
return;
|
||||||
|
} finally {
|
||||||
|
this._currentSyncRequest = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
//debuglog('Completed sync, next_batch=' + data.next_batch);
|
//debuglog('Completed sync, next_batch=' + data.next_batch);
|
||||||
@@ -699,6 +679,67 @@ SyncApi.prototype._sync = async function(syncOptions) {
|
|||||||
this._sync(syncOptions);
|
this._sync(syncOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
SyncApi.prototype._doSyncRequest = function(syncOptions, syncToken) {
|
||||||
|
const qps = this._getSyncParams(syncOptions, syncToken);
|
||||||
|
return this.client._http.authedRequest(
|
||||||
|
undefined, "GET", "/sync", qps, undefined,
|
||||||
|
qps.timeout + BUFFER_PERIOD_MS,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SyncApi.prototype._getSyncParams = function(syncOptions, syncToken) {
|
||||||
|
let pollTimeout = this.opts.pollTimeout;
|
||||||
|
|
||||||
|
if (this.getSyncState() !== 'SYNCING' || this._catchingUp) {
|
||||||
|
// unless we are happily syncing already, we want the server to return
|
||||||
|
// as quickly as possible, even if there are no events queued. This
|
||||||
|
// serves two purposes:
|
||||||
|
//
|
||||||
|
// * When the connection dies, we want to know asap when it comes back,
|
||||||
|
// so that we can hide the error from the user. (We don't want to
|
||||||
|
// have to wait for an event or a timeout).
|
||||||
|
//
|
||||||
|
// * We want to know if the server has any to_device messages queued up
|
||||||
|
// for us. We do that by calling it with a zero timeout until it
|
||||||
|
// doesn't give us any more to_device messages.
|
||||||
|
this._catchingUp = true;
|
||||||
|
pollTimeout = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let filterId = syncOptions.filterId;
|
||||||
|
if (this.client.isGuest() && !filterId) {
|
||||||
|
filterId = this._getGuestFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
const qps = {
|
||||||
|
filter: filterId,
|
||||||
|
timeout: pollTimeout,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.opts.disablePresence) {
|
||||||
|
qps.set_presence = "offline";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncToken) {
|
||||||
|
qps.since = syncToken;
|
||||||
|
} else {
|
||||||
|
// use a cachebuster for initialsyncs, to make sure that
|
||||||
|
// we don't get a stale sync
|
||||||
|
// (https://github.com/vector-im/vector-web/issues/1354)
|
||||||
|
qps._cacheBuster = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.getSyncState() == 'ERROR' || this.getSyncState() == 'RECONNECTING') {
|
||||||
|
// we think the connection is dead. If it comes back up, we won't know
|
||||||
|
// about it till /sync returns. If the timeout= is high, this could
|
||||||
|
// be a long time. Set it to 0 when doing retries so we don't have to wait
|
||||||
|
// for an event or a timeout before emiting the SYNCING event.
|
||||||
|
qps.timeout = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return qps;
|
||||||
|
};
|
||||||
|
|
||||||
SyncApi.prototype._onSyncError = function(err, syncOptions) {
|
SyncApi.prototype._onSyncError = function(err, syncOptions) {
|
||||||
if (!this._running) {
|
if (!this._running) {
|
||||||
debuglog("Sync no longer running: exiting");
|
debuglog("Sync no longer running: exiting");
|
||||||
|
|||||||
Reference in New Issue
Block a user