electron-pinia-sync 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,221 @@
1
+ import { ipcMain, BrowserWindow } from 'electron';
2
+ import { createPinia } from 'pinia';
3
+ import Store from 'electron-store';
4
+
5
+ // src/main/index.ts
6
+
7
+ // src/types.ts
8
+ var IPC_CHANNELS = {
9
+ /** Renderer requests initial state from Main */
10
+ STATE_PULL: "pinia-sync:state-pull",
11
+ /** Renderer sends patch to Main */
12
+ STATE_PATCH: "pinia-sync:state-patch",
13
+ /** Main broadcasts state update to all Renderers */
14
+ STATE_UPDATED: "pinia-sync:state-updated"};
15
+
16
+ // src/debug.ts
17
+ function createDebugLogger(namespace, debugLevel = false, customLogger) {
18
+ const isEnabled = debugLevel !== false;
19
+ const isVerbose = debugLevel === "verbose" || debugLevel === true;
20
+ const prefix = `[${namespace}]`;
21
+ const noop = () => {
22
+ };
23
+ const logger = customLogger || console;
24
+ return {
25
+ log: logger.log?.bind(logger, prefix) || console.log.bind(console, prefix),
26
+ warn: logger.warn?.bind(logger, prefix) || console.warn.bind(console, prefix),
27
+ error: logger.error?.bind(logger, prefix) || console.error.bind(console, prefix),
28
+ debug: isEnabled ? logger.log?.bind(logger, prefix) || console.log.bind(console, prefix) : noop,
29
+ verbose: isVerbose ? logger.log?.bind(logger, `${prefix}[VERBOSE]`) || console.log.bind(console, `${prefix}[VERBOSE]`) : noop
30
+ };
31
+ }
32
+ function formatStateForDebug(state, maxLength = 200) {
33
+ try {
34
+ const json = JSON.stringify(state);
35
+ if (json.length > maxLength) {
36
+ return json.substring(0, maxLength) + `... (${json.length} chars total)`;
37
+ }
38
+ return json;
39
+ } catch {
40
+ return "[Circular or non-serializable]";
41
+ }
42
+ }
43
+ function formatPatchForDebug(patch) {
44
+ try {
45
+ const keys = Object.keys(patch);
46
+ if (keys.length === 0) {
47
+ return "{}";
48
+ }
49
+ if (keys.length > 5) {
50
+ return `{ ${keys.slice(0, 5).join(", ")}, ... (${keys.length} keys) }`;
51
+ }
52
+ return JSON.stringify(patch, null, 2);
53
+ } catch {
54
+ return "[Invalid patch]";
55
+ }
56
+ }
57
+
58
+ // src/utils/toRawState.ts
59
+ function toRawState(state) {
60
+ return JSON.parse(JSON.stringify(state));
61
+ }
62
+
63
+ // src/main/index.ts
64
+ var MainSync = class {
65
+ pinia;
66
+ electronStore;
67
+ storeMetadata = /* @__PURE__ */ new Map();
68
+ processingTransactions = /* @__PURE__ */ new Set();
69
+ MAX_TRANSACTION_HISTORY = 100;
70
+ debug;
71
+ constructor(options = {}) {
72
+ this.debug = createDebugLogger("electron-pinia-sync:main", options.debug ?? false, options.logger);
73
+ this.debug.debug("Initializing MainSync");
74
+ this.pinia = options.pinia ?? createPinia();
75
+ this.electronStore = new Store({
76
+ name: "pinia-sync-store",
77
+ ...options.storeOptions
78
+ });
79
+ this.debug.verbose("electron-store initialized with options:", options.storeOptions);
80
+ this.setupIpcHandlers();
81
+ this.debug.debug("MainSync initialized successfully");
82
+ }
83
+ /**
84
+ * Get the Pinia instance managed by this sync manager
85
+ */
86
+ getPinia() {
87
+ return this.pinia;
88
+ }
89
+ /**
90
+ * Register a store with the sync manager
91
+ */
92
+ registerStore(storeId, store, options = {}) {
93
+ this.debug.debug(`Registering store: ${storeId}`);
94
+ const persistConfig = this.normalizePersistOptions(options.persist);
95
+ this.storeMetadata.set(storeId, {
96
+ persist: persistConfig
97
+ });
98
+ if (persistConfig) {
99
+ const key = persistConfig.key ?? storeId;
100
+ const persistedState = this.electronStore.get(key);
101
+ if (persistedState) {
102
+ this.debug.verbose(`Loading persisted state for ${storeId}:`, formatStateForDebug(persistedState));
103
+ store.$patch(persistedState);
104
+ } else {
105
+ this.debug.verbose(`No persisted state found for ${storeId}`);
106
+ }
107
+ }
108
+ store.$subscribe((_mutation, state) => {
109
+ this.debug.verbose(`Store ${storeId} changed:`, formatStateForDebug(state));
110
+ const serializedState = toRawState(state);
111
+ if (persistConfig) {
112
+ const key = persistConfig.key ?? storeId;
113
+ this.electronStore.set(key, serializedState);
114
+ this.debug.verbose(`Persisted state for ${storeId} to key: ${key}`);
115
+ }
116
+ this.broadcastStateUpdate(storeId, serializedState);
117
+ }, { detached: true });
118
+ this.debug.debug(`Store ${storeId} registered successfully (persist: ${!!persistConfig})`);
119
+ }
120
+ /**
121
+ * Normalize persist options to standard format
122
+ */
123
+ normalizePersistOptions(persist) {
124
+ if (persist === true) {
125
+ return { enabled: true };
126
+ } else if (persist === false || persist === void 0) {
127
+ return false;
128
+ } else {
129
+ return persist;
130
+ }
131
+ }
132
+ /**
133
+ * Setup IPC handlers for communication with renderers
134
+ */
135
+ setupIpcHandlers() {
136
+ this.debug.debug("Setting up IPC handlers");
137
+ ipcMain.handle(
138
+ IPC_CHANNELS.STATE_PULL,
139
+ async (_event, request) => {
140
+ this.debug.debug(`IPC handler called: STATE_PULL for store: ${request.storeId}`);
141
+ const store = this.pinia._s.get(request.storeId);
142
+ if (store) {
143
+ this.debug.verbose(`Sending state for ${request.storeId}:`, formatStateForDebug(store.$state));
144
+ } else {
145
+ this.debug.warn(`Store "${request.storeId}" not found in Main process`);
146
+ }
147
+ return {
148
+ storeId: request.storeId,
149
+ state: store ? toRawState(store.$state) : null
150
+ };
151
+ }
152
+ );
153
+ this.debug.debug(`IPC handler registered: ${IPC_CHANNELS.STATE_PULL}`);
154
+ ipcMain.handle(
155
+ IPC_CHANNELS.STATE_PATCH,
156
+ async (_event, message) => {
157
+ this.debug.debug(`IPC handler called: STATE_PATCH for store: ${message.storeId}, transaction: ${message.transactionId}`);
158
+ this.debug.verbose(`Patch data:`, formatPatchForDebug(message.patch));
159
+ const store = this.pinia._s.get(message.storeId);
160
+ if (!store) {
161
+ this.debug.warn(`Store "${message.storeId}" not found in Main process`);
162
+ return;
163
+ }
164
+ this.addTransaction(message.transactionId);
165
+ try {
166
+ store.$patch(message.patch);
167
+ this.debug.debug(`Successfully applied patch to store: ${message.storeId}`);
168
+ } catch (error) {
169
+ this.debug.error(`Failed to patch store "${message.storeId}":`, error);
170
+ }
171
+ }
172
+ );
173
+ this.debug.debug(`IPC handler registered: ${IPC_CHANNELS.STATE_PATCH}`);
174
+ this.debug.debug("IPC handlers setup complete");
175
+ }
176
+ addTransaction(id) {
177
+ this.processingTransactions.add(id);
178
+ if (this.processingTransactions.size > this.MAX_TRANSACTION_HISTORY) {
179
+ const first = this.processingTransactions.values().next().value;
180
+ if (first) this.processingTransactions.delete(first);
181
+ }
182
+ setTimeout(() => this.processingTransactions.delete(id), 5e3);
183
+ }
184
+ /**
185
+ * Broadcast state update to all renderer processes
186
+ */
187
+ broadcastStateUpdate(storeId, state, transactionId) {
188
+ this.debug.verbose(`Broadcasting state update for store: ${storeId}`, transactionId ? `(transaction: ${transactionId})` : "");
189
+ const message = {
190
+ storeId,
191
+ state,
192
+ transactionId
193
+ };
194
+ const windows = BrowserWindow.getAllWindows();
195
+ this.debug.verbose(`Broadcasting to ${windows.length} window(s)`);
196
+ windows.forEach((window, index) => {
197
+ if (!window.isDestroyed()) {
198
+ this.debug.verbose(`Sending ${IPC_CHANNELS.STATE_UPDATED} to window ${index + 1}`);
199
+ window.webContents.send(IPC_CHANNELS.STATE_UPDATED, message);
200
+ } else {
201
+ this.debug.verbose(`Skipping destroyed window ${index + 1}`);
202
+ }
203
+ });
204
+ this.debug.debug(`Broadcast complete for store: ${storeId}`);
205
+ }
206
+ /**
207
+ * Cleanup IPC handlers
208
+ */
209
+ destroy() {
210
+ this.debug.debug("Destroying MainSync, cleaning up IPC handlers");
211
+ ipcMain.removeHandler(IPC_CHANNELS.STATE_PULL);
212
+ ipcMain.removeHandler(IPC_CHANNELS.STATE_PATCH);
213
+ this.debug.debug("MainSync destroyed");
214
+ }
215
+ };
216
+ function createMainSync(options) {
217
+ return new MainSync(options);
218
+ }
219
+ console.log("main sync plugin loaded");
220
+
221
+ export { MainSync, createDebugLogger, createMainSync, formatPatchForDebug, formatStateForDebug };
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,60 @@
1
+ import { contextBridge, ipcRenderer } from 'electron';
2
+
3
+ // src/preload/index.ts
4
+
5
+ // src/types.ts
6
+ var IPC_CHANNELS = {
7
+ /** Renderer requests initial state from Main */
8
+ STATE_PULL: "pinia-sync:state-pull",
9
+ /** Renderer sends patch to Main */
10
+ STATE_PATCH: "pinia-sync:state-patch",
11
+ /** Main broadcasts state update to all Renderers */
12
+ STATE_UPDATED: "pinia-sync:state-updated"};
13
+
14
+ // src/preload/index.ts
15
+ var piniaSyncAPI = {
16
+ /**
17
+ * Pull initial state from Main process
18
+ */
19
+ async pullState(storeId) {
20
+ console.log(`[preload] IPC invoke: ${IPC_CHANNELS.STATE_PULL} for store "${storeId}"`);
21
+ const request = { storeId };
22
+ const response = await ipcRenderer.invoke(
23
+ IPC_CHANNELS.STATE_PULL,
24
+ request
25
+ );
26
+ console.log(`[preload] IPC response: STATE_PULL for "${storeId}" - state:`, response.state ? "received" : "null");
27
+ return response.state;
28
+ },
29
+ /**
30
+ * Send state patch to Main process
31
+ */
32
+ async patchState(storeId, patch, transactionId) {
33
+ console.log(`[preload] IPC invoke: ${IPC_CHANNELS.STATE_PATCH} for store "${storeId}", transaction: ${transactionId}`);
34
+ const message = {
35
+ storeId,
36
+ patch,
37
+ transactionId
38
+ };
39
+ await ipcRenderer.invoke(IPC_CHANNELS.STATE_PATCH, message);
40
+ console.log(`[preload] IPC response: STATE_PATCH for "${storeId}" completed`);
41
+ },
42
+ /**
43
+ * Subscribe to state updates from Main process
44
+ */
45
+ onStateUpdate(callback) {
46
+ console.log(`[preload] IPC listener registered: ${IPC_CHANNELS.STATE_UPDATED}`);
47
+ const listener = (_event, message) => {
48
+ console.log(`[preload] IPC event received: ${IPC_CHANNELS.STATE_UPDATED} for store "${message.storeId}", transaction: ${message.transactionId || "none"}`);
49
+ callback(message);
50
+ };
51
+ ipcRenderer.on(IPC_CHANNELS.STATE_UPDATED, listener);
52
+ return () => {
53
+ console.log(`[preload] IPC listener removed: ${IPC_CHANNELS.STATE_UPDATED}`);
54
+ ipcRenderer.removeListener(IPC_CHANNELS.STATE_UPDATED, listener);
55
+ };
56
+ }
57
+ };
58
+ contextBridge.exposeInMainWorld("piniaSync", piniaSyncAPI);
59
+ console.log("[preload] piniaSync API exposed to window");
60
+ console.log("[preload] electron-pinia-sync preload script initialized");
@@ -0,0 +1,123 @@
1
+ import { StateTree, PiniaPluginContext } from 'pinia';
2
+
3
+ /**
4
+ * Common types shared across Main, Renderer, and Preload processes
5
+ */
6
+
7
+ /**
8
+ * Debug level configuration
9
+ */
10
+ type DebugLevel$1 = boolean | 'verbose' | 'minimal';
11
+ /**
12
+ * Message sent from Main to Renderer when state is updated
13
+ */
14
+ interface StateUpdateMessage {
15
+ /** Store ID */
16
+ storeId: string;
17
+ /** New state */
18
+ state: StateTree;
19
+ /** Transaction ID that caused this update (if any) */
20
+ transactionId?: string;
21
+ }
22
+ /**
23
+ * Options for configuring store persistence
24
+ */
25
+ interface PersistOptions {
26
+ /** Whether to persist this store to disk */
27
+ enabled: boolean;
28
+ /** Optional custom key for electron-store (defaults to storeId) */
29
+ key?: string;
30
+ }
31
+ /**
32
+ * Extended Pinia store options with persistence support
33
+ */
34
+ interface SyncStoreOptions {
35
+ /** Persistence configuration */
36
+ persist?: boolean | PersistOptions;
37
+ }
38
+ /**
39
+ * API exposed to Renderer via contextBridge
40
+ */
41
+ interface PiniaSyncAPI {
42
+ /**
43
+ * Pull initial state from Main process
44
+ */
45
+ pullState: (storeId: string) => Promise<StateTree | null>;
46
+ /**
47
+ * Send state patch to Main process
48
+ */
49
+ patchState: (storeId: string, patch: Partial<StateTree>, transactionId: string) => Promise<void>;
50
+ /**
51
+ * Subscribe to state updates from Main process
52
+ */
53
+ onStateUpdate: (callback: (message: StateUpdateMessage) => void) => () => void;
54
+ }
55
+ declare global {
56
+ interface Window {
57
+ piniaSync?: PiniaSyncAPI;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Debug utilities for electron-pinia-sync
63
+ *
64
+ * Enable debug logging programmatically:
65
+ * - { debug: true } - enable debug logging
66
+ * - { debug: 'verbose' } - enable verbose logging with state diffs
67
+ * - { debug: 'minimal' } - only log errors and warnings
68
+ */
69
+ type DebugLevel = boolean | 'verbose' | 'minimal';
70
+ interface DebugLogger {
71
+ log: (message: string, ...args: unknown[]) => void;
72
+ warn: (message: string, ...args: unknown[]) => void;
73
+ error: (message: string, ...args: unknown[]) => void;
74
+ debug: (message: string, ...args: unknown[]) => void;
75
+ verbose: (message: string, ...args: unknown[]) => void;
76
+ }
77
+ /**
78
+ * Create a debug logger for a specific namespace
79
+ */
80
+ declare function createDebugLogger(namespace: string, debugLevel?: DebugLevel, customLogger?: Partial<DebugLogger>): DebugLogger;
81
+ /**
82
+ * Format state for debug output (truncate large objects)
83
+ */
84
+ declare function formatStateForDebug(state: unknown, maxLength?: number): string;
85
+ /**
86
+ * Format patch for debug output
87
+ */
88
+ declare function formatPatchForDebug(patch: unknown): string;
89
+
90
+ /**
91
+ * Options for the renderer sync plugin
92
+ */
93
+ interface RendererSyncOptions {
94
+ /**
95
+ * Enable debug logging
96
+ * - true: enable debug logging
97
+ * - 'verbose': enable verbose logging with state diffs
98
+ * - 'minimal': only log errors and warnings
99
+ * - false: disable debug logging (default)
100
+ */
101
+ debug?: DebugLevel$1;
102
+ /**
103
+ * Custom logger (default: console)
104
+ */
105
+ logger?: Partial<DebugLogger>;
106
+ /**
107
+ * Custom API implementation (for testing)
108
+ * @internal
109
+ */
110
+ customApi?: PiniaSyncAPI;
111
+ }
112
+
113
+ /**
114
+ * Renderer process Pinia plugin
115
+ * Synchronizes local store changes with Main process and receives updates
116
+ */
117
+
118
+ /**
119
+ * Create the Pinia plugin for renderer process synchronization
120
+ */
121
+ declare function createRendererSync(options?: RendererSyncOptions): (context: PiniaPluginContext) => void;
122
+
123
+ export { type DebugLevel, type DebugLogger, type SyncStoreOptions, createDebugLogger, createRendererSync, formatPatchForDebug, formatStateForDebug };
@@ -0,0 +1,199 @@
1
+ import diff from 'microdiff';
2
+
3
+ // src/renderer/index.ts
4
+
5
+ // src/debug.ts
6
+ function createDebugLogger(namespace, debugLevel = false, customLogger) {
7
+ const isEnabled = debugLevel !== false;
8
+ const isVerbose = debugLevel === "verbose" || debugLevel === true;
9
+ const prefix = `[${namespace}]`;
10
+ const noop = () => {
11
+ };
12
+ const logger = customLogger || console;
13
+ return {
14
+ log: logger.log?.bind(logger, prefix) || console.log.bind(console, prefix),
15
+ warn: logger.warn?.bind(logger, prefix) || console.warn.bind(console, prefix),
16
+ error: logger.error?.bind(logger, prefix) || console.error.bind(console, prefix),
17
+ debug: isEnabled ? logger.log?.bind(logger, prefix) || console.log.bind(console, prefix) : noop,
18
+ verbose: isVerbose ? logger.log?.bind(logger, `${prefix}[VERBOSE]`) || console.log.bind(console, `${prefix}[VERBOSE]`) : noop
19
+ };
20
+ }
21
+ function formatStateForDebug(state, maxLength = 200) {
22
+ try {
23
+ const json = JSON.stringify(state);
24
+ if (json.length > maxLength) {
25
+ return json.substring(0, maxLength) + `... (${json.length} chars total)`;
26
+ }
27
+ return json;
28
+ } catch {
29
+ return "[Circular or non-serializable]";
30
+ }
31
+ }
32
+ function formatPatchForDebug(patch) {
33
+ try {
34
+ const keys = Object.keys(patch);
35
+ if (keys.length === 0) {
36
+ return "{}";
37
+ }
38
+ if (keys.length > 5) {
39
+ return `{ ${keys.slice(0, 5).join(", ")}, ... (${keys.length} keys) }`;
40
+ }
41
+ return JSON.stringify(patch, null, 2);
42
+ } catch {
43
+ return "[Invalid patch]";
44
+ }
45
+ }
46
+
47
+ // src/utils/toRawState.ts
48
+ function toRawState(state) {
49
+ return JSON.parse(JSON.stringify(state));
50
+ }
51
+
52
+ // src/renderer/index.ts
53
+ function generateTransactionId() {
54
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
55
+ }
56
+ function calculatePatch(oldState, newState, mutation) {
57
+ const differences = diff(oldState, newState);
58
+ if (differences.length === 0) {
59
+ return extractPatchFromMutation(mutation, newState);
60
+ }
61
+ const patch = {};
62
+ for (const change of differences) {
63
+ if (change.path.length === 0) {
64
+ return newState;
65
+ }
66
+ const topLevelKey = change.path[0];
67
+ if (typeof topLevelKey === "string" || typeof topLevelKey === "number") {
68
+ patch[topLevelKey] = newState[topLevelKey];
69
+ }
70
+ }
71
+ return Object.keys(patch).length > 0 ? patch : extractPatchFromMutation(mutation, newState);
72
+ }
73
+ function extractPatchFromMutation(mutation, state) {
74
+ if (mutation.type === "patch object") {
75
+ return mutation.payload;
76
+ } else if (mutation.type === "patch function") {
77
+ return state;
78
+ } else if (mutation.type === "direct") {
79
+ const patch = {};
80
+ const events = mutation.events;
81
+ if (events && Array.isArray(events) && events.length > 0) {
82
+ const firstEvent = events[0];
83
+ const key = firstEvent?.key;
84
+ if (typeof key === "string" && key in state) {
85
+ patch[key] = state[key];
86
+ return patch;
87
+ }
88
+ }
89
+ return state;
90
+ } else {
91
+ return state;
92
+ }
93
+ }
94
+ function createRendererSync(options = {}) {
95
+ const debug = createDebugLogger("electron-pinia-sync:renderer", options.debug ?? false, options.logger);
96
+ debug.debug("Initializing RendererSync");
97
+ let api = options.customApi;
98
+ if (!api) {
99
+ if (typeof window !== "undefined" && window.piniaSync) {
100
+ api = window.piniaSync;
101
+ } else if (typeof globalThis !== "undefined" && globalThis.window?.piniaSync) {
102
+ api = globalThis.window.piniaSync;
103
+ }
104
+ }
105
+ if (!api) {
106
+ debug.error("window.piniaSync is not available. Make sure the preload script is loaded correctly.");
107
+ throw new Error("Pinia sync API not available");
108
+ }
109
+ debug.debug("Pinia sync API found");
110
+ const processingTransactions = /* @__PURE__ */ new Set();
111
+ return function rendererSyncPlugin(context) {
112
+ const { store } = context;
113
+ debug.debug(`Initializing sync for store: ${store.$id}`);
114
+ let isApplyingRemoteUpdate = false;
115
+ let previousState = toRawState(store.$state);
116
+ const initializeState = async () => {
117
+ debug.debug(`Pulling initial state for store: ${store.$id}`);
118
+ try {
119
+ const state = await api.pullState(store.$id);
120
+ if (state !== null) {
121
+ debug.verbose(`Received initial state for ${store.$id}:`, formatStateForDebug(state));
122
+ isApplyingRemoteUpdate = true;
123
+ store.$patch(state);
124
+ previousState = toRawState(store.$state);
125
+ isApplyingRemoteUpdate = false;
126
+ debug.debug(`Successfully initialized state for store: ${store.$id}`);
127
+ } else {
128
+ debug.debug(`No initial state available for store: ${store.$id}`);
129
+ }
130
+ } catch (error) {
131
+ debug.error(`Failed to pull initial state for store "${store.$id}":`, error);
132
+ }
133
+ };
134
+ const subscribeToLocalChanges = () => {
135
+ debug.debug(`Subscribing to local changes for store: ${store.$id}`);
136
+ store.$subscribe((mutation, state) => {
137
+ if (isApplyingRemoteUpdate) {
138
+ debug.verbose(`Skipping sync for ${store.$id} (applying remote update)`);
139
+ return;
140
+ }
141
+ debug.verbose(`Local state changed for ${store.$id}, mutation type: ${mutation.type}`);
142
+ const patch = calculatePatch(previousState, state, mutation);
143
+ previousState = toRawState(state);
144
+ if (Object.keys(patch).length === 0) {
145
+ debug.verbose(`No changes detected for ${store.$id}, skipping sync`);
146
+ return;
147
+ }
148
+ debug.verbose(`Calculated patch for ${store.$id}:`, formatPatchForDebug(patch));
149
+ const transactionId = generateTransactionId();
150
+ debug.debug(`Syncing patch to Main (transaction: ${transactionId})`);
151
+ processingTransactions.add(transactionId);
152
+ const rawPatch = toRawState(patch);
153
+ api.patchState(store.$id, rawPatch, transactionId).catch((error) => {
154
+ debug.error(`Failed to sync state for store "${store.$id}":`, error);
155
+ }).finally(() => {
156
+ setTimeout(() => {
157
+ processingTransactions.delete(transactionId);
158
+ }, 100);
159
+ });
160
+ }, { detached: true });
161
+ };
162
+ const subscribeToRemoteUpdates = () => {
163
+ debug.debug(`Subscribing to remote updates for store: ${store.$id}`);
164
+ const unsubscribe = api.onStateUpdate((message) => {
165
+ if (message.storeId !== store.$id) {
166
+ return;
167
+ }
168
+ debug.verbose(`Received remote update for ${store.$id}`, message.transactionId ? `(transaction: ${message.transactionId})` : "");
169
+ if (message.transactionId && processingTransactions.has(message.transactionId)) {
170
+ debug.verbose(`Skipping echo update for ${store.$id} (transaction: ${message.transactionId})`);
171
+ return;
172
+ }
173
+ debug.verbose(`Applying remote state to ${store.$id}:`, formatStateForDebug(message.state));
174
+ isApplyingRemoteUpdate = true;
175
+ store.$patch(message.state);
176
+ previousState = toRawState(store.$state);
177
+ isApplyingRemoteUpdate = false;
178
+ debug.debug(`Successfully applied remote update to store: ${store.$id}`);
179
+ });
180
+ store._piniaSync_unsubscribe = unsubscribe;
181
+ };
182
+ debug.debug(`Initializing store synchronization for: ${store.$id}`);
183
+ initializeState();
184
+ subscribeToLocalChanges();
185
+ subscribeToRemoteUpdates();
186
+ debug.debug(`Store ${store.$id} sync setup complete`);
187
+ const originalDispose = store.$dispose.bind(store);
188
+ store.$dispose = () => {
189
+ debug.debug(`Disposing store: ${store.$id}`);
190
+ const unsubscribe = store._piniaSync_unsubscribe;
191
+ if (unsubscribe) {
192
+ unsubscribe();
193
+ }
194
+ originalDispose();
195
+ };
196
+ };
197
+ }
198
+
199
+ export { createDebugLogger, createRendererSync, formatPatchForDebug, formatStateForDebug };
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "electron-pinia-sync",
3
+ "version": "1.0.0",
4
+ "description": "A comprehensive Pinia plugin for Electron that synchronizes store states across multiple windows via IPC and provides seamless persistence through electron-store.",
5
+ "license": "MIT",
6
+ "author": "simpli.fyi",
7
+ "website": "https://simpli.fyi",
8
+ "keywords": [
9
+ "electron",
10
+ "pinia",
11
+ "vue",
12
+ "state-management",
13
+ "synchronization",
14
+ "ipc",
15
+ "persistence"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/simpli-fyi/electron-pinia-sync.git"
20
+ },
21
+ "type": "module",
22
+ "packageManager": "npm@10.9.0",
23
+ "engines": {
24
+ "node": ">=20.0.0"
25
+ },
26
+ "exports": {
27
+ "./main": {
28
+ "types": "./dist/main/index.d.ts",
29
+ "import": "./dist/main/index.js"
30
+ },
31
+ "./preload": {
32
+ "types": "./dist/preload/index.d.ts",
33
+ "import": "./dist/preload/index.js"
34
+ },
35
+ "./renderer": {
36
+ "types": "./dist/renderer/index.d.ts",
37
+ "import": "./dist/renderer/index.js"
38
+ }
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "README.md",
43
+ "LICENSE",
44
+ "CONTRIBUTING.md"
45
+ ],
46
+ "scripts": {
47
+ "build": "npm run clean && npm run build:main && npm run build:renderer && npm run build:preload",
48
+ "build:main": "tsup src/main/index.ts --format esm --dts --outDir dist/main",
49
+ "build:preload": "tsup src/preload/index.ts --format esm --dts --outDir dist/preload",
50
+ "build:renderer": "tsup src/renderer/index.ts --format esm --dts --outDir dist/renderer",
51
+ "clean": "rm -rf dist",
52
+ "dev": "npm run build --watch",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest",
55
+ "test:coverage": "vitest run --coverage",
56
+ "test:e2e": "playwright test",
57
+ "lint": "eslint src",
58
+ "lint:fix": "eslint src --fix",
59
+ "typecheck": "tsc --noEmit",
60
+ "prepublishOnly": "npm run build && npm run test && npm run typecheck"
61
+ },
62
+ "peerDependencies": {
63
+ "electron": ">=40.0.0",
64
+ "pinia": ">=3.0.0",
65
+ "vue": ">=3.5.0"
66
+ },
67
+ "dependencies": {
68
+ "electron-store": "^11.0.0",
69
+ "microdiff": "^1.5.0"
70
+ },
71
+ "devDependencies": {
72
+ "@playwright/test": "^1.58.0",
73
+ "@types/node": "^25.1.0",
74
+ "@vitest/coverage-v8": "^4.0.0",
75
+ "electron": "^40.1.0",
76
+ "eslint": "^9.39.0",
77
+ "pinia": "^3.0.0",
78
+ "playwright": "^1.58.0",
79
+ "tsup": "^8.5.0",
80
+ "tsx": "^4.21.0",
81
+ "typescript": "^5.7.0",
82
+ "typescript-eslint": "^8.32.0",
83
+ "vitest": "^4.0.0",
84
+ "vue": "^3.5.0"
85
+ }
86
+ }