2026-09-13 14:14:38 -04:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
|
|
|
value: true
|
|
|
|
|
});
|
|
|
|
|
exports.IndexedDBStore = void 0;
|
|
|
|
|
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
|
|
|
var _memory = require("./memory");
|
|
|
|
|
var _indexeddbLocalBackend = require("./indexeddb-local-backend");
|
|
|
|
|
var _indexeddbRemoteBackend = require("./indexeddb-remote-backend");
|
|
|
|
|
var _user = require("../models/user");
|
|
|
|
|
var _event = require("../models/event");
|
|
|
|
|
var _logger = require("../logger");
|
|
|
|
|
var _typedEventEmitter = require("../models/typed-event-emitter");
|
2026-09-12 23:57:45 -04:00
|
|
|
/*
|
|
|
|
|
Copyright 2017 - 2021 Vector Creations 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.
|
|
|
|
|
*/
|
|
|
|
|
|
2026-09-13 14:14:38 -04:00
|
|
|
/* eslint-disable @babel/no-invalid-this */
|
|
|
|
|
|
2026-09-12 23:57:45 -04:00
|
|
|
/**
|
|
|
|
|
* This is an internal module. See {@link IndexedDBStore} for the public class.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// If this value is too small we'll be writing very often which will cause
|
|
|
|
|
// noticeable stop-the-world pauses. If this value is too big we'll be writing
|
|
|
|
|
// so infrequently that the /sync size gets bigger on reload. Writing more
|
|
|
|
|
// often does not affect the length of the pause since the entire /sync
|
|
|
|
|
// response is persisted each time.
|
|
|
|
|
const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
|
|
|
|
|
|
2026-09-13 14:14:38 -04:00
|
|
|
class IndexedDBStore extends _memory.MemoryStore {
|
2026-09-12 23:57:45 -04:00
|
|
|
static exists(indexedDB, dbName) {
|
2026-09-13 14:14:38 -04:00
|
|
|
return _indexeddbLocalBackend.LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
|
2026-09-12 23:57:45 -04:00
|
|
|
}
|
2026-09-13 14:14:38 -04:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The backend instance.
|
|
|
|
|
* Call through to this API if you need to perform specific indexeddb actions like deleting the database.
|
|
|
|
|
*/
|
|
|
|
|
|
2026-09-12 23:57:45 -04:00
|
|
|
/**
|
|
|
|
|
* Construct a new Indexed Database store, which extends MemoryStore.
|
|
|
|
|
*
|
|
|
|
|
* This store functions like a MemoryStore except it periodically persists
|
|
|
|
|
* the contents of the store to an IndexedDB backend.
|
|
|
|
|
*
|
|
|
|
|
* All data is still kept in-memory but can be loaded from disk by calling
|
|
|
|
|
* `startup()`. This can make startup times quicker as a complete
|
|
|
|
|
* sync from the server is not required. This does not reduce memory usage as all
|
|
|
|
|
* the data is eagerly fetched when `startup()` is called.
|
|
|
|
|
* ```
|
|
|
|
|
* let opts = { indexedDB: window.indexedDB, localStorage: window.localStorage };
|
|
|
|
|
* let store = new IndexedDBStore(opts);
|
2026-09-13 14:14:38 -04:00
|
|
|
* await store.startup(); // load from indexed db
|
2026-09-12 23:57:45 -04:00
|
|
|
* let client = sdk.createClient({
|
|
|
|
|
* store: store,
|
|
|
|
|
* });
|
|
|
|
|
* client.startClient();
|
|
|
|
|
* client.on("sync", function(state, prevState, data) {
|
|
|
|
|
* if (state === "PREPARED") {
|
|
|
|
|
* console.log("Started up, now with go faster stripes!");
|
|
|
|
|
* }
|
|
|
|
|
* });
|
|
|
|
|
* ```
|
|
|
|
|
*
|
|
|
|
|
* @param opts - Options object.
|
|
|
|
|
*/
|
|
|
|
|
constructor(opts) {
|
|
|
|
|
super(opts);
|
2026-09-13 14:14:38 -04:00
|
|
|
(0, _defineProperty2.default)(this, "backend", void 0);
|
|
|
|
|
(0, _defineProperty2.default)(this, "startedUp", false);
|
|
|
|
|
(0, _defineProperty2.default)(this, "syncTs", 0);
|
2026-09-12 23:57:45 -04:00
|
|
|
// Records the last-modified-time of each user at the last point we saved
|
|
|
|
|
// the database, such that we can derive the set if users that have been
|
|
|
|
|
// modified since we last saved.
|
2026-09-13 14:14:38 -04:00
|
|
|
(0, _defineProperty2.default)(this, "userModifiedMap", {});
|
2026-09-12 23:57:45 -04:00
|
|
|
// user_id : timestamp
|
2026-09-13 14:14:38 -04:00
|
|
|
(0, _defineProperty2.default)(this, "emitter", new _typedEventEmitter.TypedEventEmitter());
|
|
|
|
|
(0, _defineProperty2.default)(this, "on", this.emitter.on.bind(this.emitter));
|
|
|
|
|
(0, _defineProperty2.default)(this, "onClose", () => {
|
2026-09-12 23:57:45 -04:00
|
|
|
this.emitter.emit("closed");
|
|
|
|
|
});
|
|
|
|
|
/**
|
|
|
|
|
* @returns Promise which resolves with a sync response to restore the
|
|
|
|
|
* client state to where it was at the last save, or null if there
|
|
|
|
|
* is no saved sync data.
|
|
|
|
|
*/
|
2026-09-13 14:14:38 -04:00
|
|
|
(0, _defineProperty2.default)(this, "getSavedSync", this.degradable(() => {
|
2026-09-12 23:57:45 -04:00
|
|
|
return this.backend.getSavedSync();
|
|
|
|
|
}, "getSavedSync"));
|
|
|
|
|
/** @returns whether or not the database was newly created in this session. */
|
2026-09-13 14:14:38 -04:00
|
|
|
(0, _defineProperty2.default)(this, "isNewlyCreated", this.degradable(() => {
|
2026-09-12 23:57:45 -04:00
|
|
|
return this.backend.isNewlyCreated();
|
|
|
|
|
}, "isNewlyCreated"));
|
|
|
|
|
/**
|
|
|
|
|
* @returns If there is a saved sync, the nextBatch token
|
|
|
|
|
* for this sync, otherwise null.
|
|
|
|
|
*/
|
2026-09-13 14:14:38 -04:00
|
|
|
(0, _defineProperty2.default)(this, "getSavedSyncToken", this.degradable(() => {
|
2026-09-12 23:57:45 -04:00
|
|
|
return this.backend.getNextBatchToken();
|
|
|
|
|
}, "getSavedSyncToken"));
|
|
|
|
|
/**
|
|
|
|
|
* Delete all data from this store.
|
|
|
|
|
* @returns Promise which resolves if the data was deleted from the database.
|
|
|
|
|
*/
|
2026-09-13 14:14:38 -04:00
|
|
|
(0, _defineProperty2.default)(this, "deleteAllData", this.degradable(() => {
|
2026-09-12 23:57:45 -04:00
|
|
|
super.deleteAllData();
|
|
|
|
|
return this.backend.clearDatabase().then(() => {
|
2026-09-13 14:14:38 -04:00
|
|
|
_logger.logger.log("Deleted indexeddb data.");
|
2026-09-12 23:57:45 -04:00
|
|
|
}, err => {
|
2026-09-13 14:14:38 -04:00
|
|
|
_logger.logger.error(`Failed to delete indexeddb data: ${err}`);
|
2026-09-12 23:57:45 -04:00
|
|
|
throw err;
|
|
|
|
|
});
|
2026-09-13 14:14:38 -04:00
|
|
|
}));
|
|
|
|
|
(0, _defineProperty2.default)(this, "reallySave", this.degradable(() => {
|
2026-09-12 23:57:45 -04:00
|
|
|
this.syncTs = Date.now(); // set now to guard against multi-writes
|
|
|
|
|
|
|
|
|
|
// work out changed users (this doesn't handle deletions but you
|
|
|
|
|
// can't 'delete' users as they are just presence events).
|
|
|
|
|
const userTuples = [];
|
|
|
|
|
for (const u of this.getUsers()) {
|
|
|
|
|
if (this.userModifiedMap[u.userId] === u.getLastModifiedTime()) continue;
|
|
|
|
|
if (!u.events.presence) continue;
|
|
|
|
|
userTuples.push([u.userId, u.events.presence.event]);
|
|
|
|
|
|
|
|
|
|
// note that we've saved this version of the user
|
|
|
|
|
this.userModifiedMap[u.userId] = u.getLastModifiedTime();
|
|
|
|
|
}
|
|
|
|
|
return this.backend.syncToDatabase(userTuples);
|
2026-09-13 14:14:38 -04:00
|
|
|
}));
|
|
|
|
|
(0, _defineProperty2.default)(this, "setSyncData", this.degradable(syncData => {
|
2026-09-12 23:57:45 -04:00
|
|
|
return this.backend.setSyncData(syncData);
|
|
|
|
|
}, "setSyncData"));
|
|
|
|
|
/**
|
|
|
|
|
* Returns the out-of-band membership events for this room that
|
|
|
|
|
* were previously loaded.
|
|
|
|
|
* @returns the events, potentially an empty array if OOB loading didn't yield any new members
|
|
|
|
|
* @returns in case the members for this room haven't been stored yet
|
|
|
|
|
*/
|
2026-09-13 14:14:38 -04:00
|
|
|
(0, _defineProperty2.default)(this, "getOutOfBandMembers", this.degradable(roomId => {
|
2026-09-12 23:57:45 -04:00
|
|
|
return this.backend.getOutOfBandMembers(roomId);
|
|
|
|
|
}, "getOutOfBandMembers"));
|
|
|
|
|
/**
|
|
|
|
|
* Stores the out-of-band membership events for this room. Note that
|
|
|
|
|
* it still makes sense to store an empty array as the OOB status for the room is
|
|
|
|
|
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
|
|
|
|
|
* @param membershipEvents - the membership events to store
|
|
|
|
|
* @returns when all members have been stored
|
|
|
|
|
*/
|
2026-09-13 14:14:38 -04:00
|
|
|
(0, _defineProperty2.default)(this, "setOutOfBandMembers", this.degradable((roomId, membershipEvents) => {
|
2026-09-12 23:57:45 -04:00
|
|
|
super.setOutOfBandMembers(roomId, membershipEvents);
|
|
|
|
|
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
|
|
|
|
|
}, "setOutOfBandMembers"));
|
2026-09-13 14:14:38 -04:00
|
|
|
(0, _defineProperty2.default)(this, "clearOutOfBandMembers", this.degradable(roomId => {
|
2026-09-12 23:57:45 -04:00
|
|
|
super.clearOutOfBandMembers(roomId);
|
|
|
|
|
return this.backend.clearOutOfBandMembers(roomId);
|
|
|
|
|
}, "clearOutOfBandMembers"));
|
2026-09-13 14:14:38 -04:00
|
|
|
(0, _defineProperty2.default)(this, "getClientOptions", this.degradable(() => {
|
2026-09-12 23:57:45 -04:00
|
|
|
return this.backend.getClientOptions();
|
|
|
|
|
}, "getClientOptions"));
|
2026-09-13 14:14:38 -04:00
|
|
|
(0, _defineProperty2.default)(this, "storeClientOptions", this.degradable(options => {
|
2026-09-12 23:57:45 -04:00
|
|
|
super.storeClientOptions(options);
|
|
|
|
|
return this.backend.storeClientOptions(options);
|
|
|
|
|
}, "storeClientOptions"));
|
|
|
|
|
if (!opts.indexedDB) {
|
|
|
|
|
throw new Error("Missing required option: indexedDB");
|
|
|
|
|
}
|
|
|
|
|
if (opts.workerFactory) {
|
2026-09-13 14:14:38 -04:00
|
|
|
this.backend = new _indexeddbRemoteBackend.RemoteIndexedDBStoreBackend(opts.workerFactory, opts.dbName);
|
2026-09-12 23:57:45 -04:00
|
|
|
} else {
|
2026-09-13 14:14:38 -04:00
|
|
|
this.backend = new _indexeddbLocalBackend.LocalIndexedDBStoreBackend(opts.indexedDB, opts.dbName);
|
2026-09-12 23:57:45 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* @returns Resolved when loaded from indexed db.
|
|
|
|
|
*/
|
|
|
|
|
startup() {
|
|
|
|
|
if (this.startedUp) {
|
2026-09-13 14:14:38 -04:00
|
|
|
_logger.logger.log(`IndexedDBStore.startup: already started`);
|
2026-09-12 23:57:45 -04:00
|
|
|
return Promise.resolve();
|
|
|
|
|
}
|
2026-09-13 14:14:38 -04:00
|
|
|
_logger.logger.log(`IndexedDBStore.startup: connecting to backend`);
|
|
|
|
|
return this.backend.connect(this.onClose).then(() => {
|
|
|
|
|
_logger.logger.log(`IndexedDBStore.startup: loading presence events`);
|
2026-09-12 23:57:45 -04:00
|
|
|
return this.backend.getUserPresenceEvents();
|
|
|
|
|
}).then(userPresenceEvents => {
|
2026-09-13 14:14:38 -04:00
|
|
|
_logger.logger.log(`IndexedDBStore.startup: processing presence events`);
|
2026-09-12 23:57:45 -04:00
|
|
|
userPresenceEvents.forEach(([userId, rawEvent]) => {
|
2026-09-13 14:14:38 -04:00
|
|
|
const u = new _user.User(userId);
|
2026-09-12 23:57:45 -04:00
|
|
|
if (rawEvent) {
|
2026-09-13 14:14:38 -04:00
|
|
|
u.setPresenceEvent(new _event.MatrixEvent(rawEvent));
|
2026-09-12 23:57:45 -04:00
|
|
|
}
|
|
|
|
|
this.userModifiedMap[u.userId] = u.getLastModifiedTime();
|
|
|
|
|
this.storeUser(u);
|
|
|
|
|
});
|
|
|
|
|
this.startedUp = true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Close the database and destroy any associated workers
|
|
|
|
|
*/
|
|
|
|
|
destroy() {
|
|
|
|
|
return this.backend.destroy();
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Whether this store would like to save its data
|
|
|
|
|
* Note that obviously whether the store wants to save or
|
|
|
|
|
* not could change between calling this function and calling
|
|
|
|
|
* save().
|
|
|
|
|
*
|
|
|
|
|
* @returns True if calling save() will actually save
|
|
|
|
|
* (at the time this function is called).
|
|
|
|
|
*/
|
|
|
|
|
wantsSave() {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
return now - this.syncTs > WRITE_DELAY_MS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Possibly write data to the database.
|
|
|
|
|
*
|
|
|
|
|
* @param force - True to force a save to happen
|
|
|
|
|
* @returns Promise resolves after the write completes
|
|
|
|
|
* (or immediately if no write is performed)
|
|
|
|
|
*/
|
|
|
|
|
save(force = false) {
|
|
|
|
|
if (force || this.wantsSave()) {
|
|
|
|
|
return this.reallySave();
|
|
|
|
|
}
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* All member functions of `IndexedDBStore` that access the backend use this wrapper to
|
|
|
|
|
* watch for failures after initial store startup, including `QuotaExceededError` as
|
|
|
|
|
* free disk space changes, etc.
|
|
|
|
|
*
|
|
|
|
|
* When IndexedDB fails via any of these paths, we degrade this back to a `MemoryStore`
|
|
|
|
|
* in place so that the current operation and all future ones are in-memory only.
|
|
|
|
|
*
|
|
|
|
|
* @param func - The degradable work to do.
|
|
|
|
|
* @param fallback - The method name for fallback.
|
|
|
|
|
* @returns A wrapped member function.
|
|
|
|
|
*/
|
|
|
|
|
degradable(func, fallback) {
|
|
|
|
|
const fallbackFn = fallback ? super[fallback] : null;
|
|
|
|
|
return async (...args) => {
|
|
|
|
|
try {
|
|
|
|
|
return await func.call(this, ...args);
|
|
|
|
|
} catch (e) {
|
2026-09-13 14:14:38 -04:00
|
|
|
_logger.logger.error("IndexedDBStore failure, degrading to MemoryStore", e);
|
2026-09-12 23:57:45 -04:00
|
|
|
this.emitter.emit("degraded", e);
|
|
|
|
|
try {
|
|
|
|
|
// We try to delete IndexedDB after degrading since this store is only a
|
|
|
|
|
// cache (the app will still function correctly without the data).
|
|
|
|
|
// It's possible that deleting repair IndexedDB for the next app load,
|
|
|
|
|
// potentially by making a little more space available.
|
2026-09-13 14:14:38 -04:00
|
|
|
_logger.logger.log("IndexedDBStore trying to delete degraded data");
|
2026-09-12 23:57:45 -04:00
|
|
|
await this.backend.clearDatabase();
|
2026-09-13 14:14:38 -04:00
|
|
|
_logger.logger.log("IndexedDBStore delete after degrading succeeded");
|
2026-09-12 23:57:45 -04:00
|
|
|
} catch (e) {
|
2026-09-13 14:14:38 -04:00
|
|
|
_logger.logger.warn("IndexedDBStore delete after degrading failed", e);
|
2026-09-12 23:57:45 -04:00
|
|
|
}
|
|
|
|
|
// Degrade the store from being an instance of `IndexedDBStore` to instead be
|
|
|
|
|
// an instance of `MemoryStore` so that future API calls use the memory path
|
|
|
|
|
// directly and skip IndexedDB entirely. This should be safe as
|
|
|
|
|
// `IndexedDBStore` already extends from `MemoryStore`, so we are making the
|
|
|
|
|
// store become its parent type in a way. The mutator methods of
|
|
|
|
|
// `IndexedDBStore` also maintain the state that `MemoryStore` uses (many are
|
|
|
|
|
// not overridden at all).
|
|
|
|
|
if (fallbackFn) {
|
|
|
|
|
return fallbackFn.call(this, ...args);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// XXX: ideally these would be stored in indexeddb as part of the room but,
|
|
|
|
|
// we don't store rooms as such and instead accumulate entire sync responses atm.
|
|
|
|
|
async getPendingEvents(roomId) {
|
|
|
|
|
if (!this.localStorage) return super.getPendingEvents(roomId);
|
|
|
|
|
const serialized = this.localStorage.getItem(pendingEventsKey(roomId));
|
|
|
|
|
if (serialized) {
|
|
|
|
|
try {
|
|
|
|
|
return JSON.parse(serialized);
|
|
|
|
|
} catch (e) {
|
2026-09-13 14:14:38 -04:00
|
|
|
_logger.logger.error("Could not parse persisted pending events", e);
|
2026-09-12 23:57:45 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
async setPendingEvents(roomId, events) {
|
|
|
|
|
if (!this.localStorage) return super.setPendingEvents(roomId, events);
|
|
|
|
|
if (events.length > 0) {
|
|
|
|
|
this.localStorage.setItem(pendingEventsKey(roomId), JSON.stringify(events));
|
|
|
|
|
} else {
|
|
|
|
|
this.localStorage.removeItem(pendingEventsKey(roomId));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
saveToDeviceBatches(batches) {
|
|
|
|
|
return this.backend.saveToDeviceBatches(batches);
|
|
|
|
|
}
|
|
|
|
|
getOldestToDeviceBatch() {
|
|
|
|
|
return this.backend.getOldestToDeviceBatch();
|
|
|
|
|
}
|
|
|
|
|
removeToDeviceBatch(id) {
|
|
|
|
|
return this.backend.removeToDeviceBatch(id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param roomId - ID of the current room
|
|
|
|
|
* @returns Storage key to retrieve pending events
|
|
|
|
|
*/
|
2026-09-13 14:14:38 -04:00
|
|
|
exports.IndexedDBStore = IndexedDBStore;
|
2026-09-12 23:57:45 -04:00
|
|
|
function pendingEventsKey(roomId) {
|
|
|
|
|
return `mx_pending_events_${roomId}`;
|
|
|
|
|
}
|
|
|
|
|
//# sourceMappingURL=indexeddb.js.map
|