From 20e9fa13e7980012cd36d527508489cb6da2d9a7 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Sat, 27 Dec 2025 17:39:26 +0100 Subject: [PATCH 1/2] refactor: Remove cross-store dependencies using Zustand event store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace direct store imports and calls with a Zustand-based event system for decoupled cross-store communication. Changes: - Created storeEvents.ts using Zustand for event management - Removed direct usePatternStore import from useMachineStore - Removed dynamic imports for useMachineUploadStore and useMachineCacheStore - Added event subscriptions in usePatternStore, useMachineUploadStore, and useMachineCacheStore - useMachineStore now emits patternDeleted event instead of calling other stores directly Benefits: - Stores can be tested in isolation - No tight coupling between stores - Clear, explicit event-driven data flow - Uses Zustand's built-in subscription system - Easier to refactor stores independently Fixes #37 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/stores/storeEvents.ts | 44 +++++++++++++++++++++++++++++ src/stores/useMachineCacheStore.ts | 6 ++++ src/stores/useMachineStore.ts | 14 ++------- src/stores/useMachineUploadStore.ts | 6 ++++ src/stores/usePatternStore.ts | 6 ++++ 5 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 src/stores/storeEvents.ts diff --git a/src/stores/storeEvents.ts b/src/stores/storeEvents.ts new file mode 100644 index 0000000..f4d93a2 --- /dev/null +++ b/src/stores/storeEvents.ts @@ -0,0 +1,44 @@ +/** + * Store Events + * + * Zustand-based event store for cross-store communication without tight coupling. + * Uses Zustand's built-in subscription system to emit and react to events. + */ + +import { create } from "zustand"; + +interface EventState { + // Event counters - incrementing these triggers subscriptions + patternDeletedCount: number; + + // Actions to emit events + emitPatternDeleted: () => void; +} + +/** + * Event store using Zustand for cross-store communication. + * Stores can emit events by calling actions, and subscribe to events using Zustand's subscribe. + */ +export const useEventStore = create((set) => ({ + patternDeletedCount: 0, + + emitPatternDeleted: () => { + set((state) => ({ patternDeletedCount: state.patternDeletedCount + 1 })); + }, +})); + +/** + * Subscribe to pattern deleted event + * @param callback - Function to call when event is emitted + * @returns Unsubscribe function + */ +export const onPatternDeleted = (callback: () => void): (() => void) => { + let prevCount = useEventStore.getState().patternDeletedCount; + + return useEventStore.subscribe((state) => { + if (state.patternDeletedCount !== prevCount) { + prevCount = state.patternDeletedCount; + callback(); + } + }); +}; diff --git a/src/stores/useMachineCacheStore.ts b/src/stores/useMachineCacheStore.ts index f13899d..5023a2e 100644 --- a/src/stores/useMachineCacheStore.ts +++ b/src/stores/useMachineCacheStore.ts @@ -1,6 +1,7 @@ import { create } from "zustand"; import type { PesPatternData } from "../formats/import/pesImporter"; import { uuidToString } from "../services/PatternCacheService"; +import { onPatternDeleted } from "./storeEvents"; /** * Machine Cache Store @@ -192,3 +193,8 @@ export const useMachineCacheStore = create((set, get) => ({ }); }, })); + +// Subscribe to pattern deleted event +onPatternDeleted(() => { + useMachineCacheStore.getState().clearResumeState(); +}); diff --git a/src/stores/useMachineStore.ts b/src/stores/useMachineStore.ts index 7ee6c57..b0c35db 100644 --- a/src/stores/useMachineStore.ts +++ b/src/stores/useMachineStore.ts @@ -13,7 +13,7 @@ import { SewingMachineError } from "../utils/errorCodeHelpers"; import { uuidToString } from "../services/PatternCacheService"; import { createStorageService } from "../platform"; import type { IStorageService } from "../platform/interfaces/IStorageService"; -import { usePatternStore } from "./usePatternStore"; +import { useEventStore } from "./storeEvents"; interface MachineState { // Service instances @@ -291,16 +291,8 @@ export const useMachineStore = create((set, get) => ({ sewingProgress: null, }); - // Clear uploaded pattern data in pattern store - usePatternStore.getState().clearUploadedPattern(); - - // Clear upload state in upload store - const { useMachineUploadStore } = await import("./useMachineUploadStore"); - useMachineUploadStore.getState().reset(); - - // Clear resume state in cache store - const { useMachineCacheStore } = await import("./useMachineCacheStore"); - useMachineCacheStore.getState().clearResumeState(); + // Emit pattern deleted event for other stores to react + useEventStore.getState().emitPatternDeleted(); await refreshStatus(); } catch (err) { diff --git a/src/stores/useMachineUploadStore.ts b/src/stores/useMachineUploadStore.ts index ce4ac3a..621200c 100644 --- a/src/stores/useMachineUploadStore.ts +++ b/src/stores/useMachineUploadStore.ts @@ -1,6 +1,7 @@ import { create } from "zustand"; import type { PesPatternData } from "../formats/import/pesImporter"; import { uuidToString } from "../services/PatternCacheService"; +import { onPatternDeleted } from "./storeEvents"; /** * Machine Upload Store @@ -126,3 +127,8 @@ export const useMachineUploadStore = create((set) => ({ set({ uploadProgress: 0, isUploading: false }); }, })); + +// Subscribe to pattern deleted event +onPatternDeleted(() => { + useMachineUploadStore.getState().reset(); +}); diff --git a/src/stores/usePatternStore.ts b/src/stores/usePatternStore.ts index 94f568b..a05297b 100644 --- a/src/stores/usePatternStore.ts +++ b/src/stores/usePatternStore.ts @@ -1,5 +1,6 @@ import { create } from "zustand"; import type { PesPatternData } from "../formats/import/pesImporter"; +import { onPatternDeleted } from "./storeEvents"; interface PatternState { // Original pattern (pre-upload) @@ -121,3 +122,8 @@ export const useUploadedPatternOffset = () => usePatternStore((state) => state.uploadedPatternOffset); export const usePatternRotation = () => usePatternStore((state) => state.patternRotation); + +// Subscribe to pattern deleted event +onPatternDeleted(() => { + usePatternStore.getState().clearUploadedPattern(); +}); From 9299f5aed980676a7ac673e22dbcb0ba04d144dd Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Sat, 27 Dec 2025 17:45:22 +0100 Subject: [PATCH 2/2] fix: Address Copilot review feedback on event subscriptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add error handling and documentation to event subscriptions based on Copilot review feedback. Changes: - Added try-catch blocks to all event callbacks for graceful error handling - Added comments documenting that subscriptions persist for app lifetime - Improved JSDoc for onPatternDeleted function with lifecycle details - Added error logging to help debug potential issues Benefits: - Prevents silent failures in event callbacks - Clear documentation about subscription lifecycle - Better developer experience with error messages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/stores/storeEvents.ts | 14 +++++++++++--- src/stores/useMachineCacheStore.ts | 14 ++++++++++++-- src/stores/useMachineUploadStore.ts | 14 ++++++++++++-- src/stores/usePatternStore.ts | 14 ++++++++++++-- 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/stores/storeEvents.ts b/src/stores/storeEvents.ts index f4d93a2..862fb53 100644 --- a/src/stores/storeEvents.ts +++ b/src/stores/storeEvents.ts @@ -28,9 +28,17 @@ export const useEventStore = create((set) => ({ })); /** - * Subscribe to pattern deleted event - * @param callback - Function to call when event is emitted - * @returns Unsubscribe function + * Subscribe to the pattern deleted event. + * + * The subscription remains active until the returned unsubscribe function is called. + * If the unsubscribe function is not called, the listener will persist for the + * lifetime of the event store (typically the lifetime of the application). + * + * Call the returned unsubscribe function when the listener is no longer needed, + * especially for short-lived components or non-module-level subscriptions. + * + * @param callback - Function to call when the event is emitted. + * @returns Unsubscribe function that removes the listener when invoked. */ export const onPatternDeleted = (callback: () => void): (() => void) => { let prevCount = useEventStore.getState().patternDeletedCount; diff --git a/src/stores/useMachineCacheStore.ts b/src/stores/useMachineCacheStore.ts index 5023a2e..b18d3ed 100644 --- a/src/stores/useMachineCacheStore.ts +++ b/src/stores/useMachineCacheStore.ts @@ -194,7 +194,17 @@ export const useMachineCacheStore = create((set, get) => ({ }, })); -// Subscribe to pattern deleted event +// Subscribe to pattern deleted event. +// This subscription is intended to persist for the lifetime of the application, +// so the unsubscribe function returned by `onPatternDeleted` is intentionally +// not stored or called. onPatternDeleted(() => { - useMachineCacheStore.getState().clearResumeState(); + try { + useMachineCacheStore.getState().clearResumeState(); + } catch (error) { + console.error( + "[MachineCacheStore] Failed to clear resume state on pattern deleted event:", + error, + ); + } }); diff --git a/src/stores/useMachineUploadStore.ts b/src/stores/useMachineUploadStore.ts index 621200c..14c711f 100644 --- a/src/stores/useMachineUploadStore.ts +++ b/src/stores/useMachineUploadStore.ts @@ -128,7 +128,17 @@ export const useMachineUploadStore = create((set) => ({ }, })); -// Subscribe to pattern deleted event +// Subscribe to pattern deleted event. +// This subscription is intended to persist for the lifetime of the application, +// so the unsubscribe function returned by `onPatternDeleted` is intentionally +// not stored or called. onPatternDeleted(() => { - useMachineUploadStore.getState().reset(); + try { + useMachineUploadStore.getState().reset(); + } catch (error) { + console.error( + "[MachineUploadStore] Failed to reset on pattern deleted event:", + error, + ); + } }); diff --git a/src/stores/usePatternStore.ts b/src/stores/usePatternStore.ts index a05297b..610fe34 100644 --- a/src/stores/usePatternStore.ts +++ b/src/stores/usePatternStore.ts @@ -123,7 +123,17 @@ export const useUploadedPatternOffset = () => export const usePatternRotation = () => usePatternStore((state) => state.patternRotation); -// Subscribe to pattern deleted event +// Subscribe to pattern deleted event. +// This subscription is intended to persist for the lifetime of the application, +// so the unsubscribe function returned by `onPatternDeleted` is intentionally +// not stored or called. onPatternDeleted(() => { - usePatternStore.getState().clearUploadedPattern(); + try { + usePatternStore.getState().clearUploadedPattern(); + } catch (error) { + console.error( + "[PatternStore] Failed to clear uploaded pattern on pattern deleted event:", + error, + ); + } });