diff --git a/src/dashboard/example/stores/commentary.ts b/src/dashboard/example/stores/commentary.ts index 9ae5b4d..0db68b0 100644 --- a/src/dashboard/example/stores/commentary.ts +++ b/src/dashboard/example/stores/commentary.ts @@ -1,7 +1,8 @@ import { defineStore } from 'pinia'; -import { computed, ref, watch } from 'vue'; +import { computed, ref } from 'vue'; import { commentaryReplicant } from '../../../browser_shared/replicants'; import type { Schemas } from '../../../types'; +import { syncStateWithReplicant } from './store-sync'; type Commentary = Schemas.Commentary; @@ -21,32 +22,7 @@ const normalizeCommentary = (input: unknown): Commentary => { export const useCommentaryStore = defineStore('commentary', () => { const commentary = ref({ ...defaultCommentary }); const replicant = commentaryReplicant; - const isApplyingReplicant = ref(false); - - watch( - () => replicant?.data, - (value) => { - if (!value) { - return; - } - isApplyingReplicant.value = true; - commentary.value = normalizeCommentary(value); - isApplyingReplicant.value = false; - }, - { deep: true, immediate: true }, - ); - - watch( - commentary, - (value) => { - if (isApplyingReplicant.value || !replicant) { - return; - } - replicant.data = normalizeCommentary(value); - replicant.save(); - }, - { deep: true, flush: 'sync' }, - ); + syncStateWithReplicant(commentary, replicant, normalizeCommentary); const leftCommentator = computed({ get: () => commentary.value.leftCommentator, diff --git a/src/dashboard/example/stores/players.ts b/src/dashboard/example/stores/players.ts index d079b22..a600bdc 100644 --- a/src/dashboard/example/stores/players.ts +++ b/src/dashboard/example/stores/players.ts @@ -1,7 +1,8 @@ import { defineStore } from 'pinia'; -import { computed, ref, watch } from 'vue'; +import { computed, ref } from 'vue'; import { playersReplicant } from '../../../browser_shared/replicants'; import type { Schemas } from '../../../types'; +import { readStorageSnapshot, syncStateWithReplicant } from './store-sync'; type PlayersMap = Schemas.Players; type Player = PlayersMap[string]; @@ -33,69 +34,15 @@ const normalizePlayers = (input: unknown): PlayersMap => { return result; }; -const readStorage = (): PlayersMap | null => { - if (typeof window === 'undefined') { - return null; - } - try { - const raw = window.localStorage.getItem(STORAGE_KEY); - if (!raw) { - return null; - } - const parsed = JSON.parse(raw) as unknown; - return normalizePlayers(parsed); - } catch { - return null; - } -}; - -const writeStorage = (value: PlayersMap) => { - if (typeof window === 'undefined') { - return; - } - try { - window.localStorage.setItem(STORAGE_KEY, JSON.stringify(value)); - } catch { - // Ignore storage errors (quota, disabled, etc.) - } -}; - export const usePlayersStore = defineStore('players', () => { const players = ref({}); const replicant = playersReplicant; - const storageSnapshot = readStorage(); + const storageSnapshot = readStorageSnapshot(STORAGE_KEY, normalizePlayers); if (storageSnapshot) { players.value = storageSnapshot; } - const isApplyingReplicant = ref(false); - - watch( - () => replicant?.data, - (value) => { - if (!value) { - return; - } - isApplyingReplicant.value = true; - players.value = normalizePlayers(value); - isApplyingReplicant.value = false; - writeStorage(players.value); - }, - { deep: true, immediate: true } - ); - - watch( - players, - (value) => { - writeStorage(value); - if (isApplyingReplicant.value || !replicant) { - return; - } - replicant.data = normalizePlayers(value); - replicant.save(); - }, - { deep: true, flush: 'sync' } - ); + syncStateWithReplicant(players, replicant, normalizePlayers, STORAGE_KEY); const setPlayers = (value: PlayersMap) => { players.value = normalizePlayers(value); diff --git a/src/dashboard/example/stores/scoreboard.ts b/src/dashboard/example/stores/scoreboard.ts index 031f5fd..a3e3ee0 100644 --- a/src/dashboard/example/stores/scoreboard.ts +++ b/src/dashboard/example/stores/scoreboard.ts @@ -1,7 +1,8 @@ import { defineStore } from 'pinia'; -import { computed, ref, watch } from 'vue'; +import { computed, ref } from 'vue'; import { scoreboardReplicant } from '../../../browser_shared/replicants'; import type { Schemas } from '../../../types'; +import { readStorageSnapshot, syncStateWithReplicant } from './store-sync'; type Scoreboard = Schemas.Scoreboard; @@ -44,69 +45,15 @@ const normalizeScoreboard = (input: unknown): Scoreboard => { }; }; -const readStorage = (): Scoreboard | null => { - if (typeof window === 'undefined') { - return null; - } - try { - const raw = window.localStorage.getItem(STORAGE_KEY); - if (!raw) { - return null; - } - const parsed = JSON.parse(raw) as unknown; - return normalizeScoreboard(parsed); - } catch { - return null; - } -}; - -const writeStorage = (value: Scoreboard) => { - if (typeof window === 'undefined') { - return; - } - try { - window.localStorage.setItem(STORAGE_KEY, JSON.stringify(value)); - } catch { - // Ignore storage errors (quota, disabled, etc.) - } -}; - export const useScoreboardStore = defineStore('scoreboard', () => { const scoreboard = ref({ ...defaultScoreboard }); const replicant = scoreboardReplicant; - const storageSnapshot = readStorage(); + const storageSnapshot = readStorageSnapshot(STORAGE_KEY, normalizeScoreboard); if (storageSnapshot) { scoreboard.value = storageSnapshot; } - const isApplyingReplicant = ref(false); - - watch( - () => replicant?.data, - (value) => { - if (!value) { - return; - } - isApplyingReplicant.value = true; - scoreboard.value = normalizeScoreboard(value); - writeStorage(scoreboard.value); - isApplyingReplicant.value = false; - }, - { deep: true, immediate: true } - ); - - watch( - scoreboard, - (value) => { - writeStorage(value); - if (isApplyingReplicant.value || !replicant) { - return; - } - replicant.data = normalizeScoreboard(value); - replicant.save(); - }, - { deep: true, flush: 'sync' } - ); + syncStateWithReplicant(scoreboard, replicant, normalizeScoreboard, STORAGE_KEY); const setScoreboard = (value: Scoreboard) => { scoreboard.value = normalizeScoreboard(value); diff --git a/src/dashboard/example/stores/store-sync.ts b/src/dashboard/example/stores/store-sync.ts new file mode 100644 index 0000000..a5842f0 --- /dev/null +++ b/src/dashboard/example/stores/store-sync.ts @@ -0,0 +1,81 @@ +import { ref, watch, type Ref } from 'vue'; + +interface ReplicantLike { + data: T | undefined; + save: () => void; +} + +export const readStorageSnapshot = ( + storageKey: string, + normalize: (input: unknown) => T, +): T | null => { + if (typeof window === 'undefined') { + return null; + } + + try { + const raw = window.localStorage.getItem(storageKey); + if (!raw) { + return null; + } + return normalize(JSON.parse(raw) as unknown); + } catch { + return null; + } +}; + +export const writeStorageSnapshot = (storageKey: string, value: T): void => { + if (typeof window === 'undefined') { + return; + } + + try { + window.localStorage.setItem(storageKey, JSON.stringify(value)); + } catch { + // Ignore storage errors (quota, disabled, etc.) + } +}; + +export const syncStateWithReplicant = ( + state: Ref, + replicant: ReplicantLike | undefined, + normalize: (input: unknown) => T, + storageKey?: string, +): void => { + const isApplyingReplicant = ref(false); + + watch( + () => replicant?.data, + (value) => { + if (!value) { + return; + } + + isApplyingReplicant.value = true; + state.value = normalize(value); + isApplyingReplicant.value = false; + + if (storageKey) { + writeStorageSnapshot(storageKey, state.value); + } + }, + { deep: true, immediate: true }, + ); + + watch( + state, + (value) => { + if (storageKey) { + writeStorageSnapshot(storageKey, value); + } + + if (isApplyingReplicant.value || !replicant) { + return; + } + + replicant.data = normalize(value); + replicant.save(); + }, + { deep: true, flush: 'sync' }, + ); +};