Merge pull request #373 from vector-im/bwindels/fix-send-sync-race
Fix race between /send and /sync
This commit is contained in:
commit
aa2e1aad19
27 changed files with 372 additions and 55 deletions
|
@ -36,6 +36,7 @@
|
||||||
"commander": "^6.0.0",
|
"commander": "^6.0.0",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"eslint": "^7.25.0",
|
"eslint": "^7.25.0",
|
||||||
|
"fake-indexeddb": "^3.1.2",
|
||||||
"finalhandler": "^1.1.1",
|
"finalhandler": "^1.1.1",
|
||||||
"impunity": "^1.0.0",
|
"impunity": "^1.0.0",
|
||||||
"mdn-polyfills": "^5.20.0",
|
"mdn-polyfills": "^5.20.0",
|
||||||
|
|
|
@ -191,6 +191,8 @@ async function buildJs(mainFile, extraFiles, importOverrides) {
|
||||||
plugins.push(overridesAsRollupPlugin(importOverrides));
|
plugins.push(overridesAsRollupPlugin(importOverrides));
|
||||||
}
|
}
|
||||||
const bundle = await rollup({
|
const bundle = await rollup({
|
||||||
|
// for fake-indexeddb, so usage for tests only doesn't put it in bundle
|
||||||
|
treeshake: {moduleSideEffects: false},
|
||||||
input: extraFiles.concat(mainFile),
|
input: extraFiles.concat(mainFile),
|
||||||
plugins
|
plugins
|
||||||
});
|
});
|
||||||
|
@ -230,6 +232,8 @@ async function buildJsLegacy(mainFile, extraFiles, importOverrides) {
|
||||||
plugins.push(nodeResolve(), babelPlugin);
|
plugins.push(nodeResolve(), babelPlugin);
|
||||||
// create js bundle
|
// create js bundle
|
||||||
const rollupConfig = {
|
const rollupConfig = {
|
||||||
|
// for fake-indexeddb, so usage for tests only doesn't put it in bundle
|
||||||
|
treeshake: {moduleSideEffects: false},
|
||||||
// important the extraFiles come first,
|
// important the extraFiles come first,
|
||||||
// so polyfills are available in the global scope
|
// so polyfills are available in the global scope
|
||||||
// if needed for the mainfile
|
// if needed for the mainfile
|
||||||
|
|
4
scripts/package-overrides/fake-indexeddb.js
Normal file
4
scripts/package-overrides/fake-indexeddb.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
// we have our own main file for this module as we need both these symbols to
|
||||||
|
// be exported, and we also don't want to auto behaviour that modifies global vars
|
||||||
|
exports.FDBFactory = require("fake-indexeddb/lib/FDBFactory.js");
|
||||||
|
exports.FDBKeyRange = require("fake-indexeddb/lib/FDBKeyRange.js");
|
|
@ -51,6 +51,7 @@ function packageIterator(request, start, defaultIterator) {
|
||||||
async function commonjsToESM(src, dst) {
|
async function commonjsToESM(src, dst) {
|
||||||
// create js bundle
|
// create js bundle
|
||||||
const bundle = await rollup({
|
const bundle = await rollup({
|
||||||
|
treeshake: {moduleSideEffects: false},
|
||||||
input: src,
|
input: src,
|
||||||
plugins: [commonjs(), nodeResolve({
|
plugins: [commonjs(), nodeResolve({
|
||||||
browser: true,
|
browser: true,
|
||||||
|
@ -111,6 +112,14 @@ async function populateLib() {
|
||||||
require.resolve('es6-promise/lib/es6-promise/promise.js'),
|
require.resolve('es6-promise/lib/es6-promise/promise.js'),
|
||||||
path.join(libDir, "es6-promise/index.js")
|
path.join(libDir, "es6-promise/index.js")
|
||||||
);
|
);
|
||||||
|
// fake-indexeddb, used for tests (but unresolvable bare imports also makes the build complain)
|
||||||
|
// and might want to use it for in-memory storage too, although we probably do ts->es6 with esm
|
||||||
|
// directly rather than ts->es5->es6 as we do now. The bundle is 240K currently.
|
||||||
|
await fs.mkdir(path.join(libDir, "fake-indexeddb/"));
|
||||||
|
await commonjsToESM(
|
||||||
|
path.join(projectDir, "/scripts/package-overrides/fake-indexeddb.js"),
|
||||||
|
path.join(libDir, "fake-indexeddb/index.js")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
populateLib();
|
populateLib();
|
||||||
|
|
|
@ -31,6 +31,7 @@ export class SendQueue {
|
||||||
this._isSending = false;
|
this._isSending = false;
|
||||||
this._offline = false;
|
this._offline = false;
|
||||||
this._roomEncryption = null;
|
this._roomEncryption = null;
|
||||||
|
this._currentQueueIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
_createPendingEvent(data, attachments = null) {
|
_createPendingEvent(data, attachments = null) {
|
||||||
|
@ -55,6 +56,7 @@ export class SendQueue {
|
||||||
await log.wrap("send event", async log => {
|
await log.wrap("send event", async log => {
|
||||||
log.set("queueIndex", pendingEvent.queueIndex);
|
log.set("queueIndex", pendingEvent.queueIndex);
|
||||||
try {
|
try {
|
||||||
|
this._currentQueueIndex = pendingEvent.queueIndex;
|
||||||
await this._sendEvent(pendingEvent, log);
|
await this._sendEvent(pendingEvent, log);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
if (err instanceof ConnectionError) {
|
if (err instanceof ConnectionError) {
|
||||||
|
@ -75,6 +77,8 @@ export class SendQueue {
|
||||||
pendingEvent.setError(err);
|
pendingEvent.setError(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
this._currentQueueIndex = 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -241,7 +245,7 @@ export class SendQueue {
|
||||||
relatedTxnId = pe.txnId;
|
relatedTxnId = pe.txnId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.set("relatedTxnId", eventIdOrTxnId);
|
log.set("relatedTxnId", relatedTxnId);
|
||||||
log.set("relatedEventId", relatedEventId);
|
log.set("relatedEventId", relatedEventId);
|
||||||
await this._enqueueEvent(REDACTION_TYPE, {reason}, null, relatedTxnId, relatedEventId, log);
|
await this._enqueueEvent(REDACTION_TYPE, {reason}, null, relatedTxnId, relatedEventId, log);
|
||||||
}
|
}
|
||||||
|
@ -274,7 +278,11 @@ export class SendQueue {
|
||||||
let pendingEvent;
|
let pendingEvent;
|
||||||
try {
|
try {
|
||||||
const pendingEventsStore = txn.pendingEvents;
|
const pendingEventsStore = txn.pendingEvents;
|
||||||
const maxQueueIndex = await pendingEventsStore.getMaxQueueIndex(this._roomId) || 0;
|
const maxStorageQueueIndex = await pendingEventsStore.getMaxQueueIndex(this._roomId) || 0;
|
||||||
|
// don't use the queueIndex of the pendingEvent currently waiting for /send to return
|
||||||
|
// if the remote echo already removed the pendingEvent in storage, as the send loop
|
||||||
|
// wouldn't be able to detect the remote echo already arrived and end up overwriting the new event
|
||||||
|
const maxQueueIndex = Math.max(maxStorageQueueIndex, this._currentQueueIndex);
|
||||||
const queueIndex = maxQueueIndex + 1;
|
const queueIndex = maxQueueIndex + 1;
|
||||||
const needsEncryption = eventType !== REDACTION_TYPE && !!this._roomEncryption;
|
const needsEncryption = eventType !== REDACTION_TYPE && !!this._roomEncryption;
|
||||||
pendingEvent = this._createPendingEvent({
|
pendingEvent = this._createPendingEvent({
|
||||||
|
@ -303,3 +311,45 @@ export class SendQueue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import {HomeServer as MockHomeServer} from "../../../mocks/HomeServer.js";
|
||||||
|
import {createMockStorage} from "../../../mocks/Storage.js";
|
||||||
|
import {NullLogger} from "../../../logging/NullLogger.js";
|
||||||
|
import {event, withTextBody, withTxnId} from "../../../mocks/event.js";
|
||||||
|
import {poll} from "../../../mocks/poll.js";
|
||||||
|
|
||||||
|
export function tests() {
|
||||||
|
const logger = new NullLogger();
|
||||||
|
return {
|
||||||
|
"enqueue second message when remote echo of first arrives before /send returns": async assert => {
|
||||||
|
const storage = await createMockStorage();
|
||||||
|
const hs = new MockHomeServer();
|
||||||
|
// 1. enqueue and start send event 1
|
||||||
|
const queue = new SendQueue({roomId: "!abc", storage, hsApi: hs.api});
|
||||||
|
const event1 = withTextBody(event("m.room.message", "$123"), "message 1");
|
||||||
|
await logger.run("event1", log => queue.enqueueEvent(event1.type, event1.content, null, log));
|
||||||
|
assert.equal(queue.pendingEvents.length, 1);
|
||||||
|
const sendRequest1 = hs.requests.send[0];
|
||||||
|
// 2. receive remote echo, before /send has returned
|
||||||
|
const remoteEcho = withTxnId(event1, sendRequest1.arguments[2]);
|
||||||
|
const txn = await storage.readWriteTxn([storage.storeNames.pendingEvents]);
|
||||||
|
const removal = await logger.run("remote echo", log => queue.removeRemoteEchos([remoteEcho], txn, log));
|
||||||
|
await txn.complete();
|
||||||
|
assert.equal(removal.length, 1);
|
||||||
|
queue.emitRemovals(removal);
|
||||||
|
assert.equal(queue.pendingEvents.length, 0);
|
||||||
|
// 3. now enqueue event 2
|
||||||
|
const event2 = withTextBody(event("m.room.message", "$456"), "message 2");
|
||||||
|
await logger.run("event2", log => queue.enqueueEvent(event2.type, event2.content, null, log));
|
||||||
|
// even though the first pending event has been removed by the remote echo,
|
||||||
|
// the second should get the next index, as the send loop is still blocking on the first one
|
||||||
|
assert.equal(Array.from(queue.pendingEvents)[0].queueIndex, 2);
|
||||||
|
// 4. send for event 1 comes back
|
||||||
|
sendRequest1.respond({event_id: event1.event_id});
|
||||||
|
// 5. now expect second send request for event 2
|
||||||
|
const sendRequest2 = await poll(() => hs.requests.send[1]);
|
||||||
|
sendRequest2.respond({event_id: event2.event_id});
|
||||||
|
await poll(() => !queue._isSending);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,8 +21,9 @@ import { reqAsPromise } from "./utils.js";
|
||||||
const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey";
|
const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey";
|
||||||
|
|
||||||
export class Storage {
|
export class Storage {
|
||||||
constructor(idbDatabase, hasWebkitEarlyCloseTxnBug) {
|
constructor(idbDatabase, IDBKeyRange, hasWebkitEarlyCloseTxnBug) {
|
||||||
this._db = idbDatabase;
|
this._db = idbDatabase;
|
||||||
|
this._IDBKeyRange = IDBKeyRange;
|
||||||
this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug;
|
this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug;
|
||||||
const nameMap = STORE_NAMES.reduce((nameMap, name) => {
|
const nameMap = STORE_NAMES.reduce((nameMap, name) => {
|
||||||
nameMap[name] = name;
|
nameMap[name] = name;
|
||||||
|
@ -47,7 +48,7 @@ export class Storage {
|
||||||
if (this._hasWebkitEarlyCloseTxnBug) {
|
if (this._hasWebkitEarlyCloseTxnBug) {
|
||||||
await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY));
|
await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY));
|
||||||
}
|
}
|
||||||
return new Transaction(txn, storeNames);
|
return new Transaction(txn, storeNames, this._IDBKeyRange);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
throw new StorageError("readTxn failed", err);
|
throw new StorageError("readTxn failed", err);
|
||||||
}
|
}
|
||||||
|
@ -62,7 +63,7 @@ export class Storage {
|
||||||
if (this._hasWebkitEarlyCloseTxnBug) {
|
if (this._hasWebkitEarlyCloseTxnBug) {
|
||||||
await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY));
|
await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY));
|
||||||
}
|
}
|
||||||
return new Transaction(txn, storeNames);
|
return new Transaction(txn, storeNames, this._IDBKeyRange);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
throw new StorageError("readWriteTxn failed", err);
|
throw new StorageError("readWriteTxn failed", err);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,14 +21,18 @@ import { schema } from "./schema.js";
|
||||||
import { detectWebkitEarlyCloseTxnBug } from "./quirks.js";
|
import { detectWebkitEarlyCloseTxnBug } from "./quirks.js";
|
||||||
|
|
||||||
const sessionName = sessionId => `hydrogen_session_${sessionId}`;
|
const sessionName = sessionId => `hydrogen_session_${sessionId}`;
|
||||||
const openDatabaseWithSessionId = sessionId => openDatabase(sessionName(sessionId), createStores, schema.length);
|
const openDatabaseWithSessionId = function(sessionId, idbFactory) {
|
||||||
|
return openDatabase(sessionName(sessionId), createStores, schema.length, idbFactory);
|
||||||
|
}
|
||||||
|
|
||||||
async function requestPersistedStorage() {
|
async function requestPersistedStorage() {
|
||||||
if (navigator?.storage?.persist) {
|
// don't assume browser so we can run in node with fake-idb
|
||||||
return await navigator.storage.persist();
|
const glob = this;
|
||||||
} else if (document.requestStorageAccess) {
|
if (glob?.navigator?.storage?.persist) {
|
||||||
|
return await glob.navigator.storage.persist();
|
||||||
|
} else if (glob?.document.requestStorageAccess) {
|
||||||
try {
|
try {
|
||||||
await document.requestStorageAccess();
|
await glob.document.requestStorageAccess();
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -39,8 +43,10 @@ async function requestPersistedStorage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StorageFactory {
|
export class StorageFactory {
|
||||||
constructor(serviceWorkerHandler) {
|
constructor(serviceWorkerHandler, idbFactory = window.indexedDB, IDBKeyRange = window.IDBKeyRange) {
|
||||||
this._serviceWorkerHandler = serviceWorkerHandler;
|
this._serviceWorkerHandler = serviceWorkerHandler;
|
||||||
|
this._idbFactory = idbFactory;
|
||||||
|
this._IDBKeyRange = IDBKeyRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(sessionId) {
|
async create(sessionId) {
|
||||||
|
@ -52,24 +58,24 @@ export class StorageFactory {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasWebkitEarlyCloseTxnBug = await detectWebkitEarlyCloseTxnBug();
|
const hasWebkitEarlyCloseTxnBug = await detectWebkitEarlyCloseTxnBug(this._idbFactory);
|
||||||
const db = await openDatabaseWithSessionId(sessionId);
|
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory);
|
||||||
return new Storage(db, hasWebkitEarlyCloseTxnBug);
|
return new Storage(db, this._IDBKeyRange, hasWebkitEarlyCloseTxnBug);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(sessionId) {
|
delete(sessionId) {
|
||||||
const databaseName = sessionName(sessionId);
|
const databaseName = sessionName(sessionId);
|
||||||
const req = indexedDB.deleteDatabase(databaseName);
|
const req = this._idbFactory.deleteDatabase(databaseName);
|
||||||
return reqAsPromise(req);
|
return reqAsPromise(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
async export(sessionId) {
|
async export(sessionId) {
|
||||||
const db = await openDatabaseWithSessionId(sessionId);
|
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory);
|
||||||
return await exportSession(db);
|
return await exportSession(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
async import(sessionId, data) {
|
async import(sessionId, data) {
|
||||||
const db = await openDatabaseWithSessionId(sessionId);
|
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory);
|
||||||
return await importSession(db, data);
|
return await importSession(db, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,6 +126,10 @@ export class Store extends QueryTarget {
|
||||||
this._transaction = transaction;
|
this._transaction = transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get IDBKeyRange() {
|
||||||
|
return this._transaction.IDBKeyRange;
|
||||||
|
}
|
||||||
|
|
||||||
get _idbStore() {
|
get _idbStore() {
|
||||||
return this._target;
|
return this._target;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,10 +35,11 @@ import {OperationStore} from "./stores/OperationStore.js";
|
||||||
import {AccountDataStore} from "./stores/AccountDataStore.js";
|
import {AccountDataStore} from "./stores/AccountDataStore.js";
|
||||||
|
|
||||||
export class Transaction {
|
export class Transaction {
|
||||||
constructor(txn, allowedStoreNames) {
|
constructor(txn, allowedStoreNames, IDBKeyRange) {
|
||||||
this._txn = txn;
|
this._txn = txn;
|
||||||
this._allowedStoreNames = allowedStoreNames;
|
this._allowedStoreNames = allowedStoreNames;
|
||||||
this._stores = {};
|
this._stores = {};
|
||||||
|
this.IDBKeyRange = IDBKeyRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
_idbStore(name) {
|
_idbStore(name) {
|
||||||
|
@ -46,7 +47,7 @@ export class Transaction {
|
||||||
// more specific error? this is a bug, so maybe not ...
|
// more specific error? this is a bug, so maybe not ...
|
||||||
throw new StorageError(`Invalid store for transaction: ${name}, only ${this._allowedStoreNames.join(", ")} are allowed.`);
|
throw new StorageError(`Invalid store for transaction: ${name}, only ${this._allowedStoreNames.join(", ")} are allowed.`);
|
||||||
}
|
}
|
||||||
return new Store(this._txn.objectStore(name));
|
return new Store(this._txn.objectStore(name), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
_store(name, mapStore) {
|
_store(name, mapStore) {
|
||||||
|
|
|
@ -18,12 +18,12 @@ limitations under the License.
|
||||||
import {openDatabase, txnAsPromise, reqAsPromise} from "./utils.js";
|
import {openDatabase, txnAsPromise, reqAsPromise} from "./utils.js";
|
||||||
|
|
||||||
// filed as https://bugs.webkit.org/show_bug.cgi?id=222746
|
// filed as https://bugs.webkit.org/show_bug.cgi?id=222746
|
||||||
export async function detectWebkitEarlyCloseTxnBug() {
|
export async function detectWebkitEarlyCloseTxnBug(idbFactory) {
|
||||||
const dbName = "hydrogen_webkit_test_inactive_txn_bug";
|
const dbName = "hydrogen_webkit_test_inactive_txn_bug";
|
||||||
try {
|
try {
|
||||||
const db = await openDatabase(dbName, db => {
|
const db = await openDatabase(dbName, db => {
|
||||||
db.createObjectStore("test", {keyPath: "key"});
|
db.createObjectStore("test", {keyPath: "key"});
|
||||||
}, 1);
|
}, 1, idbFactory);
|
||||||
const readTxn = db.transaction(["test"], "readonly");
|
const readTxn = db.transaction(["test"], "readonly");
|
||||||
await reqAsPromise(readTxn.objectStore("test").get("somekey"));
|
await reqAsPromise(readTxn.objectStore("test").get("somekey"));
|
||||||
// schedule a macro task in between the two txns
|
// schedule a macro task in between the two txns
|
||||||
|
|
|
@ -31,7 +31,7 @@ export class DeviceIdentityStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllForUserId(userId) {
|
getAllForUserId(userId) {
|
||||||
const range = IDBKeyRange.lowerBound(encodeKey(userId, ""));
|
const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, ""));
|
||||||
return this._store.selectWhile(range, device => {
|
return this._store.selectWhile(range, device => {
|
||||||
return device.userId === userId;
|
return device.userId === userId;
|
||||||
});
|
});
|
||||||
|
@ -39,7 +39,7 @@ export class DeviceIdentityStore {
|
||||||
|
|
||||||
async getAllDeviceIds(userId) {
|
async getAllDeviceIds(userId) {
|
||||||
const deviceIds = [];
|
const deviceIds = [];
|
||||||
const range = IDBKeyRange.lowerBound(encodeKey(userId, ""));
|
const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, ""));
|
||||||
await this._store.iterateKeys(range, key => {
|
await this._store.iterateKeys(range, key => {
|
||||||
const decodedKey = decodeKey(key);
|
const decodedKey = decodeKey(key);
|
||||||
// prevent running into the next room
|
// prevent running into the next room
|
||||||
|
@ -72,7 +72,7 @@ export class DeviceIdentityStore {
|
||||||
removeAllForUser(userId) {
|
removeAllForUser(userId) {
|
||||||
// exclude both keys as they are theoretical min and max,
|
// exclude both keys as they are theoretical min and max,
|
||||||
// but we should't have a match for just the room id, or room id with max
|
// but we should't have a match for just the room id, or room id with max
|
||||||
const range = IDBKeyRange.bound(encodeKey(userId, MIN_UNICODE), encodeKey(userId, MAX_UNICODE), true, true);
|
const range = this._store.IDBKeyRange.bound(encodeKey(userId, MIN_UNICODE), encodeKey(userId, MAX_UNICODE), true, true);
|
||||||
this._store.delete(range);
|
this._store.delete(range);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ export class GroupSessionDecryptionStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAllForRoom(roomId) {
|
removeAllForRoom(roomId) {
|
||||||
const range = IDBKeyRange.bound(
|
const range = this._store.IDBKeyRange.bound(
|
||||||
encodeKey(roomId, MIN_UNICODE, MIN_UNICODE),
|
encodeKey(roomId, MIN_UNICODE, MIN_UNICODE),
|
||||||
encodeKey(roomId, MAX_UNICODE, MAX_UNICODE)
|
encodeKey(roomId, MAX_UNICODE, MAX_UNICODE)
|
||||||
);
|
);
|
||||||
|
|
|
@ -41,7 +41,7 @@ export class InboundGroupSessionStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAllForRoom(roomId) {
|
removeAllForRoom(roomId) {
|
||||||
const range = IDBKeyRange.bound(
|
const range = this._store.IDBKeyRange.bound(
|
||||||
encodeKey(roomId, MIN_UNICODE, MIN_UNICODE),
|
encodeKey(roomId, MIN_UNICODE, MIN_UNICODE),
|
||||||
encodeKey(roomId, MAX_UNICODE, MAX_UNICODE)
|
encodeKey(roomId, MAX_UNICODE, MAX_UNICODE)
|
||||||
);
|
);
|
||||||
|
|
|
@ -30,7 +30,7 @@ export class OlmSessionStore {
|
||||||
|
|
||||||
async getSessionIds(senderKey) {
|
async getSessionIds(senderKey) {
|
||||||
const sessionIds = [];
|
const sessionIds = [];
|
||||||
const range = IDBKeyRange.lowerBound(encodeKey(senderKey, ""));
|
const range = this._store.IDBKeyRange.lowerBound(encodeKey(senderKey, ""));
|
||||||
await this._store.iterateKeys(range, key => {
|
await this._store.iterateKeys(range, key => {
|
||||||
const decodedKey = decodeKey(key);
|
const decodedKey = decodeKey(key);
|
||||||
// prevent running into the next room
|
// prevent running into the next room
|
||||||
|
@ -44,7 +44,7 @@ export class OlmSessionStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
getAll(senderKey) {
|
getAll(senderKey) {
|
||||||
const range = IDBKeyRange.lowerBound(encodeKey(senderKey, ""));
|
const range = this._store.IDBKeyRange.lowerBound(encodeKey(senderKey, ""));
|
||||||
return this._store.selectWhile(range, session => {
|
return this._store.selectWhile(range, session => {
|
||||||
return session.senderKey === senderKey;
|
return session.senderKey === senderKey;
|
||||||
});
|
});
|
||||||
|
|
|
@ -55,7 +55,7 @@ export class OperationStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeAllForScope(scope) {
|
async removeAllForScope(scope) {
|
||||||
const range = IDBKeyRange.bound(
|
const range = this._store.IDBKeyRange.bound(
|
||||||
encodeScopeTypeKey(scope, MIN_UNICODE),
|
encodeScopeTypeKey(scope, MIN_UNICODE),
|
||||||
encodeScopeTypeKey(scope, MAX_UNICODE)
|
encodeScopeTypeKey(scope, MAX_UNICODE)
|
||||||
);
|
);
|
||||||
|
|
|
@ -33,7 +33,7 @@ export class PendingEventStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMaxQueueIndex(roomId) {
|
async getMaxQueueIndex(roomId) {
|
||||||
const range = IDBKeyRange.bound(
|
const range = this._eventStore.IDBKeyRange.bound(
|
||||||
encodeKey(roomId, KeyLimits.minStorageKey),
|
encodeKey(roomId, KeyLimits.minStorageKey),
|
||||||
encodeKey(roomId, KeyLimits.maxStorageKey),
|
encodeKey(roomId, KeyLimits.maxStorageKey),
|
||||||
false,
|
false,
|
||||||
|
@ -46,12 +46,12 @@ export class PendingEventStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(roomId, queueIndex) {
|
remove(roomId, queueIndex) {
|
||||||
const keyRange = IDBKeyRange.only(encodeKey(roomId, queueIndex));
|
const keyRange = this._eventStore.IDBKeyRange.only(encodeKey(roomId, queueIndex));
|
||||||
this._eventStore.delete(keyRange);
|
this._eventStore.delete(keyRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
async exists(roomId, queueIndex) {
|
async exists(roomId, queueIndex) {
|
||||||
const keyRange = IDBKeyRange.only(encodeKey(roomId, queueIndex));
|
const keyRange = this._eventStore.IDBKeyRange.only(encodeKey(roomId, queueIndex));
|
||||||
const key = await this._eventStore.getKey(keyRange);
|
const key = await this._eventStore.getKey(keyRange);
|
||||||
return !!key;
|
return !!key;
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ export class PendingEventStore {
|
||||||
removeAllForRoom(roomId) {
|
removeAllForRoom(roomId) {
|
||||||
const minKey = encodeKey(roomId, KeyLimits.minStorageKey);
|
const minKey = encodeKey(roomId, KeyLimits.minStorageKey);
|
||||||
const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey);
|
const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey);
|
||||||
const range = IDBKeyRange.bound(minKey, maxKey);
|
const range = this._eventStore.IDBKeyRange.bound(minKey, maxKey);
|
||||||
this._eventStore.delete(range);
|
this._eventStore.delete(range);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ export class RoomMemberStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
getAll(roomId) {
|
getAll(roomId) {
|
||||||
const range = IDBKeyRange.lowerBound(encodeKey(roomId, ""));
|
const range = this._roomMembersStore.IDBKeyRange.lowerBound(encodeKey(roomId, ""));
|
||||||
return this._roomMembersStore.selectWhile(range, member => {
|
return this._roomMembersStore.selectWhile(range, member => {
|
||||||
return member.roomId === roomId;
|
return member.roomId === roomId;
|
||||||
});
|
});
|
||||||
|
@ -50,7 +50,7 @@ export class RoomMemberStore {
|
||||||
|
|
||||||
async getAllUserIds(roomId) {
|
async getAllUserIds(roomId) {
|
||||||
const userIds = [];
|
const userIds = [];
|
||||||
const range = IDBKeyRange.lowerBound(encodeKey(roomId, ""));
|
const range = this._roomMembersStore.IDBKeyRange.lowerBound(encodeKey(roomId, ""));
|
||||||
await this._roomMembersStore.iterateKeys(range, key => {
|
await this._roomMembersStore.iterateKeys(range, key => {
|
||||||
const decodedKey = decodeKey(key);
|
const decodedKey = decodeKey(key);
|
||||||
// prevent running into the next room
|
// prevent running into the next room
|
||||||
|
@ -66,7 +66,7 @@ export class RoomMemberStore {
|
||||||
removeAllForRoom(roomId) {
|
removeAllForRoom(roomId) {
|
||||||
// exclude both keys as they are theoretical min and max,
|
// exclude both keys as they are theoretical min and max,
|
||||||
// but we should't have a match for just the room id, or room id with max
|
// but we should't have a match for just the room id, or room id with max
|
||||||
const range = IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true);
|
const range = this._roomMembersStore.IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true);
|
||||||
this._roomMembersStore.delete(range);
|
this._roomMembersStore.delete(range);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ export class RoomStateStore {
|
||||||
removeAllForRoom(roomId) {
|
removeAllForRoom(roomId) {
|
||||||
// exclude both keys as they are theoretical min and max,
|
// exclude both keys as they are theoretical min and max,
|
||||||
// but we should't have a match for just the room id, or room id with max
|
// but we should't have a match for just the room id, or room id with max
|
||||||
const range = IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true);
|
const range = this._roomStateStore.IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true);
|
||||||
this._roomStateStore.delete(range);
|
this._roomStateStore.delete(range);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,8 @@ function decodeEventIdKey(eventIdKey) {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Range {
|
class Range {
|
||||||
constructor(only, lower, upper, lowerOpen, upperOpen) {
|
constructor(IDBKeyRange, only, lower, upper, lowerOpen, upperOpen) {
|
||||||
|
this._IDBKeyRange = IDBKeyRange;
|
||||||
this._only = only;
|
this._only = only;
|
||||||
this._lower = lower;
|
this._lower = lower;
|
||||||
this._upper = upper;
|
this._upper = upper;
|
||||||
|
@ -45,12 +46,12 @@ class Range {
|
||||||
try {
|
try {
|
||||||
// only
|
// only
|
||||||
if (this._only) {
|
if (this._only) {
|
||||||
return IDBKeyRange.only(encodeKey(roomId, this._only.fragmentId, this._only.eventIndex));
|
return this._IDBKeyRange.only(encodeKey(roomId, this._only.fragmentId, this._only.eventIndex));
|
||||||
}
|
}
|
||||||
// lowerBound
|
// lowerBound
|
||||||
// also bound as we don't want to move into another roomId
|
// also bound as we don't want to move into another roomId
|
||||||
if (this._lower && !this._upper) {
|
if (this._lower && !this._upper) {
|
||||||
return IDBKeyRange.bound(
|
return this._IDBKeyRange.bound(
|
||||||
encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
|
encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
|
||||||
encodeKey(roomId, this._lower.fragmentId, KeyLimits.maxStorageKey),
|
encodeKey(roomId, this._lower.fragmentId, KeyLimits.maxStorageKey),
|
||||||
this._lowerOpen,
|
this._lowerOpen,
|
||||||
|
@ -60,7 +61,7 @@ class Range {
|
||||||
// upperBound
|
// upperBound
|
||||||
// also bound as we don't want to move into another roomId
|
// also bound as we don't want to move into another roomId
|
||||||
if (!this._lower && this._upper) {
|
if (!this._lower && this._upper) {
|
||||||
return IDBKeyRange.bound(
|
return this._IDBKeyRange.bound(
|
||||||
encodeKey(roomId, this._upper.fragmentId, KeyLimits.minStorageKey),
|
encodeKey(roomId, this._upper.fragmentId, KeyLimits.minStorageKey),
|
||||||
encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
|
encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
|
||||||
false,
|
false,
|
||||||
|
@ -69,7 +70,7 @@ class Range {
|
||||||
}
|
}
|
||||||
// bound
|
// bound
|
||||||
if (this._lower && this._upper) {
|
if (this._lower && this._upper) {
|
||||||
return IDBKeyRange.bound(
|
return this._IDBKeyRange.bound(
|
||||||
encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
|
encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
|
||||||
encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
|
encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
|
||||||
this._lowerOpen,
|
this._lowerOpen,
|
||||||
|
@ -107,7 +108,7 @@ export class TimelineEventStore {
|
||||||
* @return {Range} the created range
|
* @return {Range} the created range
|
||||||
*/
|
*/
|
||||||
onlyRange(eventKey) {
|
onlyRange(eventKey) {
|
||||||
return new Range(eventKey);
|
return new Range(this._timelineStore.IDBKeyRange, eventKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Creates a range that includes all keys before eventKey, and optionally also the key itself.
|
/** Creates a range that includes all keys before eventKey, and optionally also the key itself.
|
||||||
|
@ -116,7 +117,7 @@ export class TimelineEventStore {
|
||||||
* @return {Range} the created range
|
* @return {Range} the created range
|
||||||
*/
|
*/
|
||||||
upperBoundRange(eventKey, open=false) {
|
upperBoundRange(eventKey, open=false) {
|
||||||
return new Range(undefined, undefined, eventKey, undefined, open);
|
return new Range(this._timelineStore.IDBKeyRange, undefined, undefined, eventKey, undefined, open);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Creates a range that includes all keys after eventKey, and optionally also the key itself.
|
/** Creates a range that includes all keys after eventKey, and optionally also the key itself.
|
||||||
|
@ -125,7 +126,7 @@ export class TimelineEventStore {
|
||||||
* @return {Range} the created range
|
* @return {Range} the created range
|
||||||
*/
|
*/
|
||||||
lowerBoundRange(eventKey, open=false) {
|
lowerBoundRange(eventKey, open=false) {
|
||||||
return new Range(undefined, eventKey, undefined, open);
|
return new Range(this._timelineStore.IDBKeyRange, undefined, eventKey, undefined, open);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Creates a range that includes all keys between `lower` and `upper`, and optionally the given keys as well.
|
/** Creates a range that includes all keys between `lower` and `upper`, and optionally the given keys as well.
|
||||||
|
@ -136,7 +137,7 @@ export class TimelineEventStore {
|
||||||
* @return {Range} the created range
|
* @return {Range} the created range
|
||||||
*/
|
*/
|
||||||
boundRange(lower, upper, lowerOpen=false, upperOpen=false) {
|
boundRange(lower, upper, lowerOpen=false, upperOpen=false) {
|
||||||
return new Range(undefined, lower, upper, lowerOpen, upperOpen);
|
return new Range(this._timelineStore.IDBKeyRange, undefined, lower, upper, lowerOpen, upperOpen);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Looks up the last `amount` entries in the timeline for `roomId`.
|
/** Looks up the last `amount` entries in the timeline for `roomId`.
|
||||||
|
@ -261,7 +262,7 @@ export class TimelineEventStore {
|
||||||
removeAllForRoom(roomId) {
|
removeAllForRoom(roomId) {
|
||||||
const minKey = encodeKey(roomId, KeyLimits.minStorageKey, KeyLimits.minStorageKey);
|
const minKey = encodeKey(roomId, KeyLimits.minStorageKey, KeyLimits.minStorageKey);
|
||||||
const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey, KeyLimits.maxStorageKey);
|
const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey, KeyLimits.maxStorageKey);
|
||||||
const range = IDBKeyRange.bound(minKey, maxKey);
|
const range = this._timelineStore.IDBKeyRange.bound(minKey, maxKey);
|
||||||
this._timelineStore.delete(range);
|
this._timelineStore.delete(range);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ export class TimelineFragmentStore {
|
||||||
|
|
||||||
_allRange(roomId) {
|
_allRange(roomId) {
|
||||||
try {
|
try {
|
||||||
return IDBKeyRange.bound(
|
return this._store.IDBKeyRange.bound(
|
||||||
encodeKey(roomId, KeyLimits.minStorageKey),
|
encodeKey(roomId, KeyLimits.minStorageKey),
|
||||||
encodeKey(roomId, KeyLimits.maxStorageKey)
|
encodeKey(roomId, KeyLimits.maxStorageKey)
|
||||||
);
|
);
|
||||||
|
|
|
@ -64,8 +64,8 @@ export function decodeUint32(str) {
|
||||||
return parseInt(str, 16);
|
return parseInt(str, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openDatabase(name, createObjectStore, version) {
|
export function openDatabase(name, createObjectStore, version, idbFactory = window.indexedDB) {
|
||||||
const req = indexedDB.open(name, version);
|
const req = idbFactory.open(name, version);
|
||||||
req.onupgradeneeded = (ev) => {
|
req.onupgradeneeded = (ev) => {
|
||||||
const db = ev.target.result;
|
const db = ev.target.result;
|
||||||
const txn = ev.target.transaction;
|
const txn = ev.target.transaction;
|
||||||
|
|
62
src/mocks/HomeServer.js
Normal file
62
src/mocks/HomeServer.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {BaseRequest} from "./Request.js";
|
||||||
|
|
||||||
|
// a request as returned by the HomeServerApi
|
||||||
|
class HomeServerRequest extends BaseRequest {
|
||||||
|
constructor(args) {
|
||||||
|
super();
|
||||||
|
this.arguments = args;
|
||||||
|
}
|
||||||
|
|
||||||
|
respond(body) {
|
||||||
|
return this._respond(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Target {
|
||||||
|
constructor() {
|
||||||
|
this.requests = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMethod(target, name, ...args) {
|
||||||
|
let requests = target.requests[name]
|
||||||
|
if (!requests) {
|
||||||
|
target.requests[name] = requests = [];
|
||||||
|
}
|
||||||
|
const request = new HomeServerRequest(args);
|
||||||
|
requests.push(request);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Handler {
|
||||||
|
get(target, prop) {
|
||||||
|
return handleMethod.bind(null, target, prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HomeServer {
|
||||||
|
constructor() {
|
||||||
|
this._target = new Target();
|
||||||
|
this.api = new Proxy(this._target, new Handler());
|
||||||
|
}
|
||||||
|
|
||||||
|
get requests() {
|
||||||
|
return this._target.requests;
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,17 +16,19 @@ limitations under the License.
|
||||||
|
|
||||||
import {AbortError} from "../utils/error.js";
|
import {AbortError} from "../utils/error.js";
|
||||||
|
|
||||||
export class Request {
|
export class BaseRequest {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._responsePromise = new Promise((resolve, reject) => {
|
this._responsePromise = new Promise((resolve, reject) => {
|
||||||
this.resolve = resolve;
|
this.resolve = resolve;
|
||||||
this.reject = reject;
|
this.reject = reject;
|
||||||
});
|
});
|
||||||
|
this.responded = false;
|
||||||
this.aborted = false;
|
this.aborted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
respond(status, body) {
|
_respond(value) {
|
||||||
this.resolve({status, body});
|
this.responded = true;
|
||||||
|
this.resolve(value);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,3 +41,10 @@ export class Request {
|
||||||
return this._responsePromise;
|
return this._responsePromise;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this is a NetworkRequest as used by HomeServerApi
|
||||||
|
export class Request extends BaseRequest {
|
||||||
|
respond(status, body) {
|
||||||
|
return this._respond({status, body});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
22
src/mocks/Storage.js
Normal file
22
src/mocks/Storage.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {FDBFactory, FDBKeyRange} from "../../lib/fake-indexeddb/index.js";
|
||||||
|
import {StorageFactory} from "../matrix/storage/idb/StorageFactory.js";
|
||||||
|
|
||||||
|
export function createMockStorage() {
|
||||||
|
return new StorageFactory(null, new FDBFactory(), FDBKeyRange).create(1);
|
||||||
|
}
|
31
src/mocks/event.js
Normal file
31
src/mocks/event.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function event(type, id = null) {
|
||||||
|
return {type, event_id: id};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withContent(event, content) {
|
||||||
|
return Object.assign({}, event, {content});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withTextBody(event, body) {
|
||||||
|
return withContent(event, {body, msgtype: "m.text"});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withTxnId(event, txnId) {
|
||||||
|
return Object.assign({}, event, {unsigned: {transaction_id: txnId}});
|
||||||
|
}
|
27
src/mocks/poll.js
Normal file
27
src/mocks/poll.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function poll(fn) {
|
||||||
|
let result;
|
||||||
|
do {
|
||||||
|
const result = fn();
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
await new Promise(setImmediate); //eslint-disable-line no-undef
|
||||||
|
}
|
||||||
|
} while (1); //eslint-disable-line no-constant-condition
|
||||||
|
}
|
89
yarn.lock
89
yarn.lock
|
@ -1045,6 +1045,11 @@ base-x@^3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
|
base64-arraybuffer-es6@^0.7.0:
|
||||||
|
version "0.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.7.0.tgz#dbe1e6c87b1bf1ca2875904461a7de40f21abc86"
|
||||||
|
integrity sha512-ESyU/U1CFZDJUdr+neHRhNozeCv72Y7Vm0m1DCbjX3KBjT6eYocvAJlSk6+8+HkVwXlT1FNxhGW6q3UKAlCvvw==
|
||||||
|
|
||||||
base64-arraybuffer@^0.2.0:
|
base64-arraybuffer@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45"
|
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45"
|
||||||
|
@ -1209,6 +1214,11 @@ core-js-compat@^3.6.2:
|
||||||
browserslist "^4.8.5"
|
browserslist "^4.8.5"
|
||||||
semver "7.0.0"
|
semver "7.0.0"
|
||||||
|
|
||||||
|
core-js@^2.5.3:
|
||||||
|
version "2.6.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
|
||||||
|
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
|
||||||
|
|
||||||
core-js@^3.6.5:
|
core-js@^3.6.5:
|
||||||
version "3.6.5"
|
version "3.6.5"
|
||||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
|
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
|
||||||
|
@ -1324,6 +1334,13 @@ domelementtype@^2.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d"
|
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d"
|
||||||
integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==
|
integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==
|
||||||
|
|
||||||
|
domexception@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
|
||||||
|
integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==
|
||||||
|
dependencies:
|
||||||
|
webidl-conversions "^4.0.2"
|
||||||
|
|
||||||
domhandler@^2.3.0:
|
domhandler@^2.3.0:
|
||||||
version "2.4.2"
|
version "2.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
|
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
|
||||||
|
@ -1549,6 +1566,14 @@ extend@^3.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||||
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
||||||
|
|
||||||
|
fake-indexeddb@^3.1.2:
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-3.1.2.tgz#8073a12ed3b254f7afc064f3cc2629f0110a5303"
|
||||||
|
integrity sha512-W60eRBrE8r9o/EePyyUc63sr2I/MI9p3zVwLlC1WI1xdmQVqqM6+wec9KDWDz2EZyvJKhrDvy3cGC6hK8L1pfg==
|
||||||
|
dependencies:
|
||||||
|
realistic-structured-clone "^2.0.1"
|
||||||
|
setimmediate "^1.0.5"
|
||||||
|
|
||||||
fast-deep-equal@^3.1.1:
|
fast-deep-equal@^3.1.1:
|
||||||
version "3.1.3"
|
version "3.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||||
|
@ -1914,7 +1939,7 @@ lodash@^4.17.19:
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||||
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
||||||
|
|
||||||
lodash@^4.17.21:
|
lodash@^4.17.21, lodash@^4.7.0:
|
||||||
version "4.17.21"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
@ -2218,7 +2243,7 @@ progress@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
||||||
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
|
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
|
||||||
|
|
||||||
punycode@^2.1.0:
|
punycode@^2.1.0, punycode@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||||
|
@ -2244,6 +2269,16 @@ readable-stream@^3.1.1:
|
||||||
string_decoder "^1.1.1"
|
string_decoder "^1.1.1"
|
||||||
util-deprecate "^1.0.1"
|
util-deprecate "^1.0.1"
|
||||||
|
|
||||||
|
realistic-structured-clone@^2.0.1:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/realistic-structured-clone/-/realistic-structured-clone-2.0.2.tgz#2f8ec225b1f9af20efc79ac96a09043704414959"
|
||||||
|
integrity sha512-5IEvyfuMJ4tjQOuKKTFNvd+H9GSbE87IcendSBannE28PTrbolgaVg5DdEApRKhtze794iXqVUFKV60GLCNKEg==
|
||||||
|
dependencies:
|
||||||
|
core-js "^2.5.3"
|
||||||
|
domexception "^1.0.1"
|
||||||
|
typeson "^5.8.2"
|
||||||
|
typeson-registry "^1.0.0-alpha.20"
|
||||||
|
|
||||||
regenerate-unicode-properties@^8.2.0:
|
regenerate-unicode-properties@^8.2.0:
|
||||||
version "8.2.0"
|
version "8.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
|
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
|
||||||
|
@ -2411,6 +2446,11 @@ serve-static@^1.13.2:
|
||||||
parseurl "~1.3.3"
|
parseurl "~1.3.3"
|
||||||
send "0.17.1"
|
send "0.17.1"
|
||||||
|
|
||||||
|
setimmediate@^1.0.5:
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
|
||||||
|
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
|
||||||
|
|
||||||
setprototypeof@1.1.1:
|
setprototypeof@1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
|
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
|
||||||
|
@ -2548,6 +2588,13 @@ toidentifier@1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
||||||
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
||||||
|
|
||||||
|
tr46@^2.0.2:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240"
|
||||||
|
integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==
|
||||||
|
dependencies:
|
||||||
|
punycode "^2.1.1"
|
||||||
|
|
||||||
type-check@^0.4.0, type-check@~0.4.0:
|
type-check@^0.4.0, type-check@~0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
||||||
|
@ -2565,6 +2612,25 @@ type-fest@^0.8.1:
|
||||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
|
||||||
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
|
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
|
||||||
|
|
||||||
|
typeson-registry@^1.0.0-alpha.20:
|
||||||
|
version "1.0.0-alpha.39"
|
||||||
|
resolved "https://registry.yarnpkg.com/typeson-registry/-/typeson-registry-1.0.0-alpha.39.tgz#9e0f5aabd5eebfcffd65a796487541196f4b1211"
|
||||||
|
integrity sha512-NeGDEquhw+yfwNhguLPcZ9Oj0fzbADiX4R0WxvoY8nGhy98IbzQy1sezjoEFWOywOboj/DWehI+/aUlRVrJnnw==
|
||||||
|
dependencies:
|
||||||
|
base64-arraybuffer-es6 "^0.7.0"
|
||||||
|
typeson "^6.0.0"
|
||||||
|
whatwg-url "^8.4.0"
|
||||||
|
|
||||||
|
typeson@^5.8.2:
|
||||||
|
version "5.18.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/typeson/-/typeson-5.18.2.tgz#0d217fc0e11184a66aa7ca0076d9aa7707eb7bc2"
|
||||||
|
integrity sha512-Vetd+OGX05P4qHyHiSLdHZ5Z5GuQDrHHwSdjkqho9NSCYVSLSfRMjklD/unpHH8tXBR9Z/R05rwJSuMpMFrdsw==
|
||||||
|
|
||||||
|
typeson@^6.0.0:
|
||||||
|
version "6.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/typeson/-/typeson-6.1.0.tgz#5b2a53705a5f58ff4d6f82f965917cabd0d7448b"
|
||||||
|
integrity sha512-6FTtyGr8ldU0pfbvW/eOZrEtEkczHRUtduBnA90Jh9kMPCiFNnXIon3vF41N0S4tV1HHQt4Hk1j4srpESziCaA==
|
||||||
|
|
||||||
unicode-canonical-property-names-ecmascript@^1.0.4:
|
unicode-canonical-property-names-ecmascript@^1.0.4:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
|
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
|
||||||
|
@ -2610,6 +2676,25 @@ v8-compile-cache@^2.0.3:
|
||||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
||||||
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
|
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
|
||||||
|
|
||||||
|
webidl-conversions@^4.0.2:
|
||||||
|
version "4.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
|
||||||
|
integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
|
||||||
|
|
||||||
|
webidl-conversions@^6.1.0:
|
||||||
|
version "6.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
|
||||||
|
integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
|
||||||
|
|
||||||
|
whatwg-url@^8.4.0:
|
||||||
|
version "8.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.5.0.tgz#7752b8464fc0903fec89aa9846fc9efe07351fd3"
|
||||||
|
integrity sha512-fy+R77xWv0AiqfLl4nuGUlQ3/6b5uNfQ4WAbGQVMYshCTCCPK9psC1nWh3XHuxGVCtlcDDQPQW1csmmIQo+fwg==
|
||||||
|
dependencies:
|
||||||
|
lodash "^4.7.0"
|
||||||
|
tr46 "^2.0.2"
|
||||||
|
webidl-conversions "^6.1.0"
|
||||||
|
|
||||||
which@^2.0.1:
|
which@^2.0.1:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
||||||
|
|
Reference in a new issue