diff --git a/packages/client/components/state/stores/NotificationOptions.ts b/packages/client/components/state/stores/NotificationOptions.ts index 641c37ed1..e4112d8ac 100644 --- a/packages/client/components/state/stores/NotificationOptions.ts +++ b/packages/client/components/state/stores/NotificationOptions.ts @@ -37,6 +37,16 @@ export interface MuteState { until?: number; } +/** + * Which objects in the TypeNotificationOptions that are comparable. Shortcut for the deep equals function. + */ +const NotificationComparableObjects: (keyof TypeNotificationOptions)[] = [ + "channel", + "channel_mutes", + "server", + "server_mutes", +]; + export interface TypeNotificationOptions { /** * Per-server settings @@ -292,4 +302,32 @@ export class NotificationOptions extends AbstractStore< const value = this.get().channel_mutes[channel.id]; return !!value && (!value.until || value.until > this.#now()); } + + /** + * Check whether other is equal to stored state. + * @param other The other notification object to compare against + * @returns Whether other notification object is equal to the stored notification object + */ + equals(other: TypeNotificationOptions): boolean { + for (let compI = 0; compI < NotificationComparableObjects.length; compI++) { + const comparable = NotificationComparableObjects[compI]; + const localComparableEntries = Object.entries( + this.get()[comparable] || {}, + ); + const otherComparable = other[comparable] || {}; + if ( + localComparableEntries.length !== Object.keys(otherComparable).length + ) { + return false; + } + for (let i = 0; i < localComparableEntries.length; i++) { + const entry = localComparableEntries[i]; + if (entry[1] !== otherComparable[entry[0]]) { + return false; + } + } + } + + return true; + } } diff --git a/packages/client/components/state/stores/Ordering.ts b/packages/client/components/state/stores/Ordering.ts index 3e06f924f..be4913754 100644 --- a/packages/client/components/state/stores/Ordering.ts +++ b/packages/client/components/state/stores/Ordering.ts @@ -103,4 +103,22 @@ export class Ordering extends AbstractStore<"ordering", TypeOrdering> { .sort((a, b) => +b.updatedAt - +a.updatedAt) ?? [] ); } + + /** + * Check whether other is equal to stored state. + * @param other The other ordering object to compare against + * @returns Whether other ordering object is equal to stored ordering object + */ + equals(other: TypeOrdering): boolean { + const localServers = this.get().servers; + if (localServers.length !== other.servers.length) { + return false; + } + for (let i = 0; i < localServers.length; i++) { + if (localServers[i] !== other.servers[i]) { + return false; + } + } + return true; + } } diff --git a/packages/client/components/state/stores/Sync.ts b/packages/client/components/state/stores/Sync.ts index 26b367f18..eb7a988ea 100644 --- a/packages/client/components/state/stores/Sync.ts +++ b/packages/client/components/state/stores/Sync.ts @@ -6,6 +6,8 @@ import { Client } from "stoat.js"; import { State } from ".."; import { AbstractStore } from "."; +import { TypeNotificationOptions } from "./NotificationOptions"; +import { TypeOrdering } from "./Ordering"; type SynchronisedStores = "ordering" | "notifications"; @@ -163,15 +165,22 @@ export class Sync extends AbstractStore<"sync", TypeSynchronisation> { if (import.meta.env.DEV) console.info(`[sync] merge ${key} at ${ts} with`, data); + const parsed = this.state[key].clean(JSON.parse(data)); if (ts > this.ts(key)) { // if ts is newer, hydrate the store with it - const parsed = this.state[key].clean(JSON.parse(data)); this.set("revision", key, ts); this.#blockSync.add(key); this.state.set(key, parsed); } else if (ts !== this.ts(key)) { - // if ts is old, trigger write to synchronise to remote - this.touch(key); + // if ts is old, trigger write to synchronise to remote, but only if the data has been updated + if ( + !this.state[key].equals( + // We can guarantee that parsed matches the type of this.state[key] due to the clean function call above + parsed as TypeOrdering & TypeNotificationOptions, + ) + ) { + this.touch(key); + } } }