mirror of
https://github.com/outline/outline.git
synced 2026-08-03 13:27:25 +03:00
refactor: Simplify store persistence enablement and clearing
- Move the persistable guard into Store.enablePersistence so RootStore can call it unconditionally on every store - StorePersistence derives its own database name from the store and teamId rather than receiving a prebuilt name - StorePersistence.clear returns a promise, making the test deterministic instead of sleeping Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XisV6sRdexfnPSiehurKoV
This commit is contained in:
@@ -131,7 +131,7 @@ export default class RootStore {
|
||||
}
|
||||
|
||||
Object.values(this).forEach((store) => {
|
||||
if (store instanceof Store && store.persistable) {
|
||||
if (store instanceof Store) {
|
||||
void store.enablePersistence(teamId);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -107,26 +107,28 @@ export default abstract class Store<T extends Model> {
|
||||
@action
|
||||
clear() {
|
||||
this.data.clear();
|
||||
this.persistence?.clear();
|
||||
void this.persistence?.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables persistence of this store's data to IndexedDB, scoped to the
|
||||
* given team, and hydrates any previously persisted records into the store.
|
||||
* Safe to call multiple times, subsequent calls are a no-op.
|
||||
* A no-op if the store is not persistable, persistence is unsupported, or it
|
||||
* is already enabled, so it is safe to call on every store and more than once.
|
||||
*
|
||||
* @param teamId the ID of the team the persisted data belongs to.
|
||||
* @returns a promise that resolves when hydration is complete.
|
||||
*/
|
||||
async enablePersistence(teamId: string): Promise<void> {
|
||||
if (this.persistence || !StorePersistence.isSupported) {
|
||||
if (
|
||||
!this.persistable ||
|
||||
this.persistence ||
|
||||
!StorePersistence.isSupported
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.persistence = new StorePersistence<T>(
|
||||
this,
|
||||
StorePersistence.databaseName(this.apiEndpoint, teamId)
|
||||
);
|
||||
this.persistence = new StorePersistence<T>(this, teamId);
|
||||
await this.persistence.hydrate();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ import StorePersistence from "./StorePersistence";
|
||||
|
||||
describe("StorePersistence", () => {
|
||||
test("round-trips models through IndexedDB into a fresh store", async () => {
|
||||
const name = StorePersistence.databaseName("policies", "team-1");
|
||||
const teamId = "team-1";
|
||||
const source = new RootStore();
|
||||
const persistence = new StorePersistence(source.policies, name);
|
||||
const persistence = new StorePersistence(source.policies, teamId);
|
||||
|
||||
source.policies.add({
|
||||
id: "doc-1",
|
||||
@@ -22,7 +22,7 @@ describe("StorePersistence", () => {
|
||||
await persistence.flush();
|
||||
|
||||
const target = new RootStore();
|
||||
const targetPersistence = new StorePersistence(target.policies, name);
|
||||
const targetPersistence = new StorePersistence(target.policies, teamId);
|
||||
await targetPersistence.hydrate();
|
||||
|
||||
expect(toJS(target.policies.get("doc-1")?.abilities)).toEqual({
|
||||
@@ -34,9 +34,9 @@ describe("StorePersistence", () => {
|
||||
});
|
||||
|
||||
test("does not overwrite models already in the store when hydrating", async () => {
|
||||
const name = StorePersistence.databaseName("policies", "team-2");
|
||||
const teamId = "team-2";
|
||||
const source = new RootStore();
|
||||
const persistence = new StorePersistence(source.policies, name);
|
||||
const persistence = new StorePersistence(source.policies, teamId);
|
||||
|
||||
source.policies.add({ id: "doc-1", abilities: { read: false } });
|
||||
persistence.persist("doc-1");
|
||||
@@ -44,16 +44,16 @@ describe("StorePersistence", () => {
|
||||
|
||||
const target = new RootStore();
|
||||
target.policies.add({ id: "doc-1", abilities: { read: true } });
|
||||
const targetPersistence = new StorePersistence(target.policies, name);
|
||||
const targetPersistence = new StorePersistence(target.policies, teamId);
|
||||
await targetPersistence.hydrate();
|
||||
|
||||
expect(target.policies.get("doc-1")?.abilities).toEqual({ read: true });
|
||||
});
|
||||
|
||||
test("deletes the persisted record when the model has been removed", async () => {
|
||||
const name = StorePersistence.databaseName("policies", "team-3");
|
||||
const teamId = "team-3";
|
||||
const source = new RootStore();
|
||||
const persistence = new StorePersistence(source.policies, name);
|
||||
const persistence = new StorePersistence(source.policies, teamId);
|
||||
|
||||
source.policies.add({ id: "doc-1", abilities: { read: true } });
|
||||
persistence.persist("doc-1");
|
||||
@@ -64,16 +64,16 @@ describe("StorePersistence", () => {
|
||||
await persistence.flush();
|
||||
|
||||
const target = new RootStore();
|
||||
const targetPersistence = new StorePersistence(target.policies, name);
|
||||
const targetPersistence = new StorePersistence(target.policies, teamId);
|
||||
await targetPersistence.hydrate();
|
||||
|
||||
expect(target.policies.get("doc-1")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("clear removes all persisted records", async () => {
|
||||
const name = StorePersistence.databaseName("policies", "team-4");
|
||||
const teamId = "team-4";
|
||||
const source = new RootStore();
|
||||
const persistence = new StorePersistence(source.policies, name);
|
||||
const persistence = new StorePersistence(source.policies, teamId);
|
||||
|
||||
source.policies.add({ id: "doc-1", abilities: { read: true } });
|
||||
source.policies.add({ id: "doc-2", abilities: { read: true } });
|
||||
@@ -81,13 +81,10 @@ describe("StorePersistence", () => {
|
||||
persistence.persist("doc-2");
|
||||
await persistence.flush();
|
||||
|
||||
persistence.clear();
|
||||
|
||||
// clear() runs asynchronously in the background, wait for it to finish.
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
await persistence.clear();
|
||||
|
||||
const target = new RootStore();
|
||||
const targetPersistence = new StorePersistence(target.policies, name);
|
||||
const targetPersistence = new StorePersistence(target.policies, teamId);
|
||||
await targetPersistence.hydrate();
|
||||
|
||||
expect(target.policies.orderedData).toHaveLength(0);
|
||||
|
||||
@@ -42,9 +42,9 @@ export default class StorePersistence<T extends Model> {
|
||||
return `outline.${storeName}.${teamId}`;
|
||||
}
|
||||
|
||||
constructor(store: Store<T>, databaseName: string) {
|
||||
constructor(store: Store<T>, teamId: string) {
|
||||
this.store = store;
|
||||
this.name = databaseName;
|
||||
this.name = StorePersistence.databaseName(store.apiEndpoint, teamId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,29 +130,30 @@ export default class StorePersistence<T extends Model> {
|
||||
|
||||
/**
|
||||
* Removes all persisted records, used when the store is cleared on logout.
|
||||
*
|
||||
* @returns a promise that resolves when the records have been removed.
|
||||
*/
|
||||
public clear = () => {
|
||||
public clear = async (): Promise<void> => {
|
||||
this.dirty.clear();
|
||||
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
void this.open()
|
||||
.then((database) =>
|
||||
promisifyRequest(
|
||||
database
|
||||
.transaction(OBJECT_STORE_NAME, "readwrite")
|
||||
.objectStore(OBJECT_STORE_NAME)
|
||||
.clear()
|
||||
)
|
||||
)
|
||||
.catch((err) => {
|
||||
Logger.warn("Failed to clear persisted store in IndexedDB", {
|
||||
database: this.name,
|
||||
error: err,
|
||||
});
|
||||
try {
|
||||
const database = await this.open();
|
||||
await promisifyRequest(
|
||||
database
|
||||
.transaction(OBJECT_STORE_NAME, "readwrite")
|
||||
.objectStore(OBJECT_STORE_NAME)
|
||||
.clear()
|
||||
);
|
||||
} catch (err) {
|
||||
Logger.warn("Failed to clear persisted store in IndexedDB", {
|
||||
database: this.name,
|
||||
error: err,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private open = (): Promise<IDBDatabase> => {
|
||||
|
||||
Reference in New Issue
Block a user