1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-06 12:02:40 +03:00
Files
matrix-js-sdk/spec/unit/stores/indexeddb.spec.ts
Michael Telatynski 1fcc375dd5 Deprecate utils function defer in favour of Promise.withResolvers (#4829)
* Switch from defer to Promise.withResolvers

As supported by the outgoing LTS version (v22) which has 99% support of ES2024

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Deprecate defer instead of killing it

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Knip

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate based on review

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate based on review

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate based on review

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-05-09 10:16:35 +00:00

341 lines
12 KiB
TypeScript

/*
Copyright 2022 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 "fake-indexeddb/auto";
import "jest-localstorage-mock";
import { IDBFactory } from "fake-indexeddb";
import { IndexedDBStore, type IStateEventWithRoomId, MemoryStore, User, UserEvent } from "../../../src";
import { emitPromise } from "../../test-utils/test-utils";
import { type LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend";
describe("IndexedDBStore", () => {
afterEach(() => {
jest.clearAllMocks();
});
const roomId = "!room:id";
it("should degrade to MemoryStore on IDB errors", async () => {
const store = new IndexedDBStore({
indexedDB: indexedDB,
dbName: "database",
localStorage,
});
await store.startup();
const member1: IStateEventWithRoomId = {
room_id: roomId,
event_id: "!ev1:id",
sender: "@user1:id",
state_key: "@user1:id",
type: "m.room.member",
origin_server_ts: 123,
content: {},
};
const member2: IStateEventWithRoomId = {
room_id: roomId,
event_id: "!ev2:id",
sender: "@user2:id",
state_key: "@user2:id",
type: "m.room.member",
origin_server_ts: 123,
content: {},
};
expect(await store.getOutOfBandMembers(roomId)).toBe(null);
await store.setOutOfBandMembers(roomId, [member1]);
expect(await store.getOutOfBandMembers(roomId)).toHaveLength(1);
// Simulate a broken IDB
(store.backend as LocalIndexedDBStoreBackend)["db"]!.transaction = (): IDBTransaction => {
const err = new Error(
"Failed to execute 'transaction' on 'IDBDatabase': " + "The database connection is closing.",
);
err.name = "InvalidStateError";
throw err;
};
expect(await store.getOutOfBandMembers(roomId)).toHaveLength(1);
await Promise.all([
emitPromise(store["emitter"], "degraded"),
store.setOutOfBandMembers(roomId, [member1, member2]),
]);
expect(await store.getOutOfBandMembers(roomId)).toHaveLength(2);
});
it("Should load presence events on startup", async () => {
// 1. Create idb database
const indexedDB = new IDBFactory();
const setupResolvers = Promise.withResolvers<Event>();
const req = indexedDB.open("matrix-js-sdk:db3", 1);
let db: IDBDatabase;
req.onupgradeneeded = () => {
db = req.result;
db.createObjectStore("users", { keyPath: ["userId"] });
db.createObjectStore("accountData", { keyPath: ["type"] });
db.createObjectStore("sync", { keyPath: ["clobber"] });
};
req.onsuccess = setupResolvers.resolve;
await setupResolvers.promise;
// 2. Fill in user presence data
const writeResolvers = Promise.withResolvers<Event>();
const transaction = db!.transaction(["users"], "readwrite");
const objectStore = transaction.objectStore("users");
const request = objectStore.put({
userId: "@alice:matrix.org",
event: {
content: {
presence: "online",
},
sender: "@alice:matrix.org",
type: "m.presence",
},
});
request.onsuccess = writeResolvers.resolve;
await writeResolvers.promise;
// 3. Close database
req.result.close();
// 2. Check if the code loads presence events
const store = new IndexedDBStore({
indexedDB: indexedDB,
dbName: "db3",
localStorage,
});
let userCreated = false;
let presenceEventEmitted = false;
store.setUserCreator((id: string) => {
userCreated = true;
const user = new User(id);
user.on(UserEvent.Presence, () => {
presenceEventEmitted = true;
});
return user;
});
await store.startup();
expect(userCreated).toBe(true);
expect(presenceEventEmitted).toBe(true);
});
it("should use MemoryStore methods for pending events if no localStorage", async () => {
jest.spyOn(MemoryStore.prototype, "setPendingEvents");
jest.spyOn(MemoryStore.prototype, "getPendingEvents");
const store = new IndexedDBStore({
indexedDB: indexedDB,
dbName: "database",
localStorage: undefined,
});
const events = [{ type: "test" }];
await store.setPendingEvents(roomId, events);
expect(MemoryStore.prototype.setPendingEvents).toHaveBeenCalledWith(roomId, events);
await expect(store.getPendingEvents(roomId)).resolves.toEqual(events);
expect(MemoryStore.prototype.getPendingEvents).toHaveBeenCalledWith(roomId);
});
it("should persist pending events to localStorage if available", async () => {
jest.spyOn(MemoryStore.prototype, "setPendingEvents");
jest.spyOn(MemoryStore.prototype, "getPendingEvents");
const store = new IndexedDBStore({
indexedDB: indexedDB,
dbName: "database",
localStorage,
});
await expect(store.getPendingEvents(roomId)).resolves.toEqual([]);
const events = [{ type: "test" }];
await store.setPendingEvents(roomId, events);
expect(MemoryStore.prototype.setPendingEvents).not.toHaveBeenCalled();
await expect(store.getPendingEvents(roomId)).resolves.toEqual(events);
expect(MemoryStore.prototype.getPendingEvents).not.toHaveBeenCalled();
expect(localStorage.getItem("mx_pending_events_" + roomId)).toBe(JSON.stringify(events));
await store.setPendingEvents(roomId, []);
expect(localStorage.getItem("mx_pending_events_" + roomId)).toBeNull();
});
it("should resolve isNewlyCreated to true if no database existed initially", async () => {
const store = new IndexedDBStore({
indexedDB,
dbName: "db1",
localStorage,
});
await store.startup();
await expect(store.isNewlyCreated()).resolves.toBeTruthy();
});
it("should resolve isNewlyCreated to false if database existed already", async () => {
let store = new IndexedDBStore({
indexedDB,
dbName: "db2",
localStorage,
});
await store.startup();
store = new IndexedDBStore({
indexedDB,
dbName: "db2",
localStorage,
});
await store.startup();
await expect(store.isNewlyCreated()).resolves.toBeFalsy();
});
it("should resolve isNewlyCreated to false if database existed already but needs upgrade", async () => {
const requestSuccessResolvers = Promise.withResolvers<Event>();
// seed db3 to Version 1 so it forces a migration
const req = indexedDB.open("matrix-js-sdk:db3", 1);
req.onupgradeneeded = () => {
const db = req.result;
db.createObjectStore("users", { keyPath: ["userId"] });
db.createObjectStore("accountData", { keyPath: ["type"] });
db.createObjectStore("sync", { keyPath: ["clobber"] });
};
req.onsuccess = requestSuccessResolvers.resolve;
await requestSuccessResolvers.promise;
req.result.close();
const store = new IndexedDBStore({
indexedDB,
dbName: "db3",
localStorage,
});
await store.startup();
await expect(store.isNewlyCreated()).resolves.toBeFalsy();
});
it("should emit 'closed' if database is unexpectedly closed", async () => {
const store = new IndexedDBStore({
indexedDB: indexedDB,
dbName: "database",
localStorage,
});
await store.startup();
const storeClosedResolvers = Promise.withResolvers<void>();
store.on("closed", storeClosedResolvers.resolve);
// @ts-ignore - private field access
(store.backend as LocalIndexedDBStoreBackend).db!.onclose!({} as Event);
await storeClosedResolvers.promise;
});
it("should use remote backend if workerFactory passed", async () => {
const workerPostMessageResolvers = Promise.withResolvers<void>();
class MockWorker {
postMessage(data: any) {
if (data.command === "setupWorker") {
workerPostMessageResolvers.resolve();
}
}
}
const store = new IndexedDBStore({
indexedDB: indexedDB,
dbName: "database",
localStorage,
workerFactory: () => new MockWorker() as Worker,
});
store.startup();
await workerPostMessageResolvers.promise;
});
it("remote worker should pass closed event", async () => {
const worker = new (class MockWorker {
postMessage(data: any) {}
})() as Worker;
const store = new IndexedDBStore({
indexedDB: indexedDB,
dbName: "database",
localStorage,
workerFactory: () => worker,
});
store.startup();
const storeClosedResolvers = Promise.withResolvers<void>();
store.on("closed", storeClosedResolvers.resolve);
(worker as any).onmessage({ data: { command: "closed" } });
await storeClosedResolvers.promise;
});
it("remote worker should pass command failures", async () => {
const worker = new (class MockWorker {
private onmessage!: (data: any) => void;
postMessage(data: any) {
if (data.command === "setupWorker" || data.command === "connect") {
this.onmessage({
data: {
command: "cmd_success",
seq: data.seq,
},
});
return;
}
this.onmessage({
data: {
command: "cmd_fail",
seq: data.seq,
error: new Error("Test"),
},
});
}
})() as unknown as Worker;
const store = new IndexedDBStore({
indexedDB: indexedDB,
dbName: "database",
localStorage,
workerFactory: () => worker,
});
await expect(store.startup()).rejects.toThrow("Test");
});
it("remote worker should terminate upon destroy call", async () => {
const terminate = jest.fn();
const worker = new (class MockWorker {
private onmessage!: (data: any) => void;
postMessage(data: any) {
this.onmessage({
data: {
command: "cmd_success",
seq: data.seq,
result: [],
},
});
}
public terminate = terminate;
})() as unknown as Worker;
const store = new IndexedDBStore({
indexedDB: indexedDB,
dbName: "database",
localStorage,
workerFactory: () => worker,
});
await store.startup();
await store.destroy();
expect(terminate).toHaveBeenCalled();
});
});