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.
package/README.md ADDED
@@ -0,0 +1,458 @@
1
+ # electron-pinia-sync
2
+
3
+ > Synchronize Pinia stores between Electron Main and Renderer processes with persistence support
4
+
5
+ [![npm version](https://img.shields.io/npm/v/electron-pinia-sync.svg)](https://www.npmjs.com/package/electron-pinia-sync)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Features
9
+
10
+ - 🔄 **Bidirectional Sync**: Synchronize Pinia stores between Main and multiple Renderer processes
11
+ - 🎯 **Single Source of Truth**: Main process maintains the authoritative state
12
+ - 💾 **Persistent Storage**: Selective persistence to disk using `electron-store`
13
+ - 🔒 **Type-Safe**: Full TypeScript support with strict mode
14
+ - 🚀 **Zero Config**: Works out of the box with sensible defaults
15
+ - 🔁 **Echo Prevention**: Intelligent transaction tracking prevents infinite loops
16
+ - 📦 **ESM-Only**: Modern ES Modules build (~4 KB per module)
17
+ - ⚡ **Performance**: Efficient diffing with `microdiff` minimizes data transfer
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install electron-pinia-sync
23
+ # or
24
+ yarn add electron-pinia-sync
25
+ # or
26
+ pnpm add electron-pinia-sync
27
+ ```
28
+
29
+ ### Peer Dependencies
30
+
31
+ **Important**: This library does **not** bundle Electron, Pinia, or Vue. You must install them separately:
32
+
33
+ ```bash
34
+ npm install electron pinia vue
35
+ ```
36
+
37
+ **Required versions**:
38
+ - Electron >= 28
39
+ - Pinia >= 3.0
40
+ - Vue >= 3.5
41
+ - Node.js >= 20
42
+
43
+ **Why?** This keeps the bundle size small and prevents dependency conflicts. You use your own versions of Electron and Pinia.
44
+
45
+ ## Quick Start
46
+
47
+ ### 1. Preload Script
48
+
49
+ Set up the secure IPC bridge in your preload script:
50
+
51
+ ```typescript
52
+ // preload.ts
53
+ import 'electron-pinia-sync/preload';
54
+ ```
55
+
56
+ ### 2. Main Process
57
+
58
+ Initialize the sync manager in your main process:
59
+
60
+ ```typescript
61
+ // main.ts
62
+ import { app } from 'electron';
63
+ import { createMainSync } from 'electron-pinia-sync/main';
64
+ import { defineStore } from 'pinia';
65
+
66
+ const mainSync = createMainSync({
67
+ storeOptions: {
68
+ // Optional: electron-store configuration
69
+ name: 'my-app-store',
70
+ },
71
+ });
72
+
73
+ // Get the Pinia instance
74
+ const store = mainSync.getPinia();
75
+
76
+ // Define your store
77
+ const useCounterStore = defineStore('counter', {
78
+ state: () => ({
79
+ count: 0,
80
+ name: 'Counter',
81
+ }),
82
+ actions: {
83
+ increment() {
84
+ this.count++;
85
+ },
86
+ },
87
+ });
88
+
89
+ // Create and register the store
90
+ const counterStore = useCounterStore(store);
91
+ mainSync.registerStore('counter', counterStore, {
92
+ persist: true, // Enable persistence for this store
93
+ });
94
+
95
+ app.on('quit', () => {
96
+ mainSync.destroy();
97
+ });
98
+ ```
99
+
100
+ ### 3. Renderer Process
101
+
102
+ Set up the Pinia plugin in your renderer process:
103
+
104
+ ```typescript
105
+ // renderer.ts (or main.ts in your Vue app)
106
+ import { createApp } from 'vue';
107
+ import { createPinia, defineStore } from 'pinia';
108
+ import { createRendererSync } from 'electron-pinia-sync/renderer';
109
+ import App from './App.vue';
110
+
111
+ const pinia = createPinia();
112
+
113
+ // Add the sync plugin
114
+ pinia.use(createRendererSync());
115
+
116
+ const app = createApp(App);
117
+ app.use(pinia);
118
+ app.mount('#app');
119
+
120
+ // Define the same store (structure must match Main process)
121
+ export const useCounterStore = defineStore('counter', {
122
+ state: () => ({
123
+ count: 0,
124
+ name: 'Counter',
125
+ }),
126
+ actions: {
127
+ increment() {
128
+ this.count++;
129
+ },
130
+ },
131
+ });
132
+ ```
133
+
134
+ ### 4. Use in Vue Components
135
+
136
+ ```vue
137
+ <template>
138
+ <div>
139
+ <h1>{{ counter.name }}</h1>
140
+ <p>Count: {{ counter.count }}</p>
141
+ <button @click="counter.increment()">Increment</button>
142
+ </div>
143
+ </template>
144
+
145
+ <script setup lang="ts">
146
+ import { useCounterStore } from './stores/counter';
147
+
148
+ const counter = useCounterStore();
149
+ </script>
150
+ ```
151
+
152
+ ## API Reference
153
+
154
+ ### Main Process
155
+
156
+ #### `createMainSync(options?)`
157
+
158
+ Creates and initializes the Main process sync manager.
159
+
160
+ **Options:**
161
+
162
+ ```typescript
163
+ interface MainSyncOptions {
164
+ // Custom Pinia instance (optional, will create one if not provided)
165
+ pinia?: Pinia;
166
+
167
+ // electron-store configuration
168
+ storeOptions?: {
169
+ name?: string;
170
+ cwd?: string;
171
+ encryptionKey?: string;
172
+ // ... other electron-store options
173
+ };
174
+ }
175
+ ```
176
+
177
+ **Returns:** `MainSync` instance
178
+
179
+ #### `mainSync.registerStore(storeId, store, options?)`
180
+
181
+ Registers a Pinia store with the sync manager.
182
+
183
+ **Parameters:**
184
+
185
+ - `storeId` (string): Unique identifier for the store
186
+ - `store` (Store): Pinia store instance
187
+ - `options` (object, optional):
188
+ - `persist` (boolean | PersistOptions): Persistence configuration
189
+
190
+ **Persistence Options:**
191
+
192
+ ```typescript
193
+ // Simple boolean
194
+ { persist: true }
195
+
196
+ // Advanced configuration
197
+ {
198
+ persist: {
199
+ enabled: true,
200
+ key: 'custom-storage-key', // Optional custom key
201
+ }
202
+ }
203
+ ```
204
+
205
+ #### `mainSync.getPinia()`
206
+
207
+ Returns the managed Pinia instance.
208
+
209
+ #### `mainSync.destroy()`
210
+
211
+ Cleanup IPC handlers. Call this when your app is shutting down.
212
+
213
+ ### Renderer Process
214
+
215
+ #### `createRendererSync(options?)`
216
+
217
+ Creates the Pinia plugin for renderer process synchronization.
218
+
219
+ **Options:**
220
+
221
+ ```typescript
222
+ interface RendererSyncOptions {
223
+ // Custom logger (default: console)
224
+ logger?: {
225
+ warn: (message: string, ...args: any[]) => void;
226
+ error: (message: string, ...args: any[]) => void;
227
+ };
228
+ }
229
+ ```
230
+
231
+ **Returns:** Pinia plugin function
232
+
233
+ ## Advanced Usage
234
+
235
+ ### Multiple Windows
236
+
237
+ The library automatically synchronizes state across all renderer processes:
238
+
239
+ ```typescript
240
+ // main.ts
241
+ import { BrowserWindow } from 'electron';
242
+
243
+ const window1 = new BrowserWindow({
244
+ webPreferences: {
245
+ preload: path.join(__dirname, 'preload.js'),
246
+ },
247
+ });
248
+
249
+ const window2 = new BrowserWindow({
250
+ webPreferences: {
251
+ preload: path.join(__dirname, 'preload.js'),
252
+ },
253
+ });
254
+
255
+ // Both windows will stay in sync automatically
256
+ ```
257
+
258
+ ### Selective Persistence
259
+
260
+ Choose which stores to persist:
261
+
262
+ ```typescript
263
+ // Persist user settings
264
+ mainSync.registerStore('settings', settingsStore, { persist: true });
265
+
266
+ // Don't persist temporary UI state
267
+ mainSync.registerStore('ui', uiStore, { persist: false });
268
+ ```
269
+
270
+ ### Custom Storage Keys
271
+
272
+ Use custom keys for electron-store:
273
+
274
+ ```typescript
275
+ mainSync.registerStore('user', userStore, {
276
+ persist: {
277
+ enabled: true,
278
+ key: 'app-user-data', // Custom key
279
+ },
280
+ });
281
+ ```
282
+
283
+ ### Error Handling
284
+
285
+ Provide a custom logger to handle errors:
286
+
287
+ ```typescript
288
+ const pinia = createPinia();
289
+
290
+ pinia.use(createRendererSync({
291
+ logger: {
292
+ warn: (msg, ...args) => {
293
+ // Custom warning handler
294
+ console.warn('[MyApp]', msg, ...args);
295
+ },
296
+ error: (msg, ...args) => {
297
+ // Custom error handler
298
+ Sentry.captureException(new Error(msg));
299
+ },
300
+ },
301
+ }));
302
+ ```
303
+
304
+ ## How It Works
305
+
306
+ ### Synchronization Flow
307
+
308
+ 1. **Initialization**: When a renderer process starts, it pulls the current state from the Main process
309
+ 2. **Renderer → Main**: When state changes in a renderer, a patch is sent to the Main process
310
+ 3. **Main Processing**: Main process applies the patch and optionally persists to disk
311
+ 4. **Main → Renderers**: Main process broadcasts the updated state to all renderer processes
312
+ 5. **Echo Prevention**: Transaction IDs prevent the originating renderer from applying its own update
313
+
314
+ ### Architecture
315
+
316
+ ```
317
+ ┌─────────────────────────────────────────────────────────────┐
318
+ │ Main Process │
319
+ │ ┌────────────────────────────────────────────────────────┐ │
320
+ │ │ Pinia Store (Single Source of Truth) │ │
321
+ │ │ - Receives patches from renderers │ │
322
+ │ │ - Persists to electron-store │ │
323
+ │ │ - Broadcasts updates to all renderers │ │
324
+ │ └────────────────────────────────────────────────────────┘ │
325
+ └───────────────┬──────────────────────────┬──────────────────┘
326
+ │ │
327
+ IPC │ │ IPC
328
+ (patches) │ │ (updates)
329
+ │ │
330
+ ┌───────────▼──────────┐ ┌──────────▼───────────┐
331
+ │ Renderer 1 │ │ Renderer 2 │
332
+ │ ┌────────────────┐ │ │ ┌────────────────┐ │
333
+ │ │ Pinia Store │ │ │ │ Pinia Store │ │
334
+ │ │ (Local Copy) │ │ │ │ (Local Copy) │ │
335
+ │ └────────────────┘ │ │ └────────────────┘ │
336
+ └─────────────────────┘ └──────────────────────┘
337
+ ```
338
+
339
+ ## TypeScript Support
340
+
341
+ Full TypeScript support with type inference:
342
+
343
+ ```typescript
344
+ import { defineStore } from 'pinia';
345
+
346
+ interface CounterState {
347
+ count: number;
348
+ name: string;
349
+ }
350
+
351
+ export const useCounterStore = defineStore('counter', {
352
+ state: (): CounterState => ({
353
+ count: 0,
354
+ name: 'Counter',
355
+ }),
356
+ getters: {
357
+ doubleCount: (state) => state.count * 2,
358
+ },
359
+ actions: {
360
+ increment() {
361
+ this.count++;
362
+ },
363
+ },
364
+ });
365
+
366
+ // Full type inference in components
367
+ const counter = useCounterStore();
368
+ counter.count; // number
369
+ counter.name; // string
370
+ counter.doubleCount; // number
371
+ counter.increment(); // void
372
+ ```
373
+
374
+ ## Best Practices
375
+
376
+ 1. **Store Definition**: Define stores with the same structure in both Main and Renderer processes
377
+ 2. **Persistence**: Only persist stores that need to survive app restarts
378
+ 3. **State Size**: Keep state size reasonable for IPC transfer performance
379
+ 4. **Actions**: Actions can be defined only in Renderer (they're not synced, only state is)
380
+ 5. **Initialization**: Wait for store initialization before using in components
381
+
382
+ ## Debugging
383
+
384
+ Enable debug logging to see synchronization details:
385
+
386
+ **Main Process:**
387
+ ```typescript
388
+ const mainSync = createMainSync({
389
+ debug: true, // or 'verbose' for detailed logs
390
+ });
391
+ ```
392
+
393
+ **Renderer Process:**
394
+ ```typescript
395
+ pinia.use(createRendererSync({
396
+ debug: 'verbose', // Shows state diffs and patches
397
+ }));
398
+ ```
399
+
400
+ **Debug Levels:**
401
+ - `false` (default): Only errors/warnings
402
+ - `true`: Important operations
403
+ - `'verbose'`: Detailed logs with state diffs
404
+
405
+ For advanced debugging, see [DEBUG.md](./DEBUG.md).
406
+
407
+ ## Troubleshooting
408
+
409
+ ### Store not syncing
410
+
411
+ **Problem**: Changes in one process don't reflect in others
412
+
413
+ **Solution**:
414
+ - Ensure the preload script is loaded correctly
415
+ - Check that store IDs match between Main and Renderer
416
+ - Verify `registerStore` is called in Main process
417
+
418
+ ### State not persisting
419
+
420
+ **Problem**: State resets on app restart
421
+
422
+ **Solution**:
423
+ - Confirm `persist: true` is set when registering the store
424
+ - Check electron-store permissions and storage location
425
+ - Verify the Main process has write permissions
426
+
427
+ ### Type errors with window.piniaSync
428
+
429
+ **Problem**: TypeScript doesn't recognize `window.piniaSync`
430
+
431
+ **Solution**:
432
+ - Import types: `import 'electron-pinia-sync/preload'`
433
+ - The types are automatically augmented to the global `Window` interface
434
+
435
+ ## Examples
436
+
437
+ Check the `examples/` directory for complete working examples:
438
+
439
+ - **Basic Counter**: Simple counter app with persistence
440
+ - **Multi-Window**: Todo app synchronized across multiple windows
441
+ - **Complex State**: E-commerce app with nested state
442
+
443
+ ## Contributing
444
+
445
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup and guidelines.
446
+
447
+ ## License
448
+
449
+ MIT © simpli.fyi GbR
450
+
451
+ ## Credits
452
+
453
+ Built with:
454
+ - [Electron](https://www.electronjs.org/)
455
+ - [Vue 3](https://vuejs.org/)
456
+ - [Pinia](https://pinia.vuejs.org/)
457
+ - [electron-store](https://github.com/sindresorhus/electron-store)
458
+
@@ -0,0 +1,153 @@
1
+ import { StateTree, Pinia, Store as Store$1 } from 'pinia';
2
+ import Store from 'electron-store';
3
+
4
+ /**
5
+ * Common types shared across Main, Renderer, and Preload processes
6
+ */
7
+
8
+ /**
9
+ * Debug level configuration
10
+ */
11
+ type DebugLevel$1 = boolean | 'verbose' | 'minimal';
12
+ /**
13
+ * Message sent from Main to Renderer when state is updated
14
+ */
15
+ interface StateUpdateMessage {
16
+ /** Store ID */
17
+ storeId: string;
18
+ /** New state */
19
+ state: StateTree;
20
+ /** Transaction ID that caused this update (if any) */
21
+ transactionId?: string;
22
+ }
23
+ /**
24
+ * Options for configuring store persistence
25
+ */
26
+ interface PersistOptions {
27
+ /** Whether to persist this store to disk */
28
+ enabled: boolean;
29
+ /** Optional custom key for electron-store (defaults to storeId) */
30
+ key?: string;
31
+ }
32
+ /**
33
+ * API exposed to Renderer via contextBridge
34
+ */
35
+ interface PiniaSyncAPI {
36
+ /**
37
+ * Pull initial state from Main process
38
+ */
39
+ pullState: (storeId: string) => Promise<StateTree | null>;
40
+ /**
41
+ * Send state patch to Main process
42
+ */
43
+ patchState: (storeId: string, patch: Partial<StateTree>, transactionId: string) => Promise<void>;
44
+ /**
45
+ * Subscribe to state updates from Main process
46
+ */
47
+ onStateUpdate: (callback: (message: StateUpdateMessage) => void) => () => void;
48
+ }
49
+ declare global {
50
+ interface Window {
51
+ piniaSync?: PiniaSyncAPI;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Debug utilities for electron-pinia-sync
57
+ *
58
+ * Enable debug logging programmatically:
59
+ * - { debug: true } - enable debug logging
60
+ * - { debug: 'verbose' } - enable verbose logging with state diffs
61
+ * - { debug: 'minimal' } - only log errors and warnings
62
+ */
63
+ type DebugLevel = boolean | 'verbose' | 'minimal';
64
+ interface DebugLogger {
65
+ log: (message: string, ...args: unknown[]) => void;
66
+ warn: (message: string, ...args: unknown[]) => void;
67
+ error: (message: string, ...args: unknown[]) => void;
68
+ debug: (message: string, ...args: unknown[]) => void;
69
+ verbose: (message: string, ...args: unknown[]) => void;
70
+ }
71
+ /**
72
+ * Create a debug logger for a specific namespace
73
+ */
74
+ declare function createDebugLogger(namespace: string, debugLevel?: DebugLevel, customLogger?: Partial<DebugLogger>): DebugLogger;
75
+ /**
76
+ * Format state for debug output (truncate large objects)
77
+ */
78
+ declare function formatStateForDebug(state: unknown, maxLength?: number): string;
79
+ /**
80
+ * Format patch for debug output
81
+ */
82
+ declare function formatPatchForDebug(patch: unknown): string;
83
+
84
+ interface MainSyncOptions {
85
+ /**
86
+ * electron-store configuration options
87
+ */
88
+ storeOptions?: ConstructorParameters<typeof Store<Record<string, StateTree>>>[0];
89
+ /**
90
+ * Custom Pinia instance (optional, will create one if not provided)
91
+ */
92
+ pinia?: Pinia;
93
+ /**
94
+ * Enable debug logging
95
+ * - true: enable debug logging
96
+ * - 'verbose': enable verbose logging with state diffs
97
+ * - 'minimal': only log errors and warnings
98
+ * - false: disable debug logging (default)
99
+ */
100
+ debug?: DebugLevel$1;
101
+ /**
102
+ * Custom logger (for testing or custom logging)
103
+ */
104
+ logger?: Partial<DebugLogger>;
105
+ }
106
+
107
+ /**
108
+ * Main process synchronization manager
109
+ * Manages Pinia stores in the Main process and handles IPC communication
110
+ */
111
+
112
+ declare class MainSync {
113
+ private pinia;
114
+ private electronStore;
115
+ private storeMetadata;
116
+ private processingTransactions;
117
+ private readonly MAX_TRANSACTION_HISTORY;
118
+ private debug;
119
+ constructor(options?: MainSyncOptions);
120
+ /**
121
+ * Get the Pinia instance managed by this sync manager
122
+ */
123
+ getPinia(): Pinia;
124
+ /**
125
+ * Register a store with the sync manager
126
+ */
127
+ registerStore(storeId: string, store: Store$1, options?: {
128
+ persist?: boolean | PersistOptions;
129
+ }): void;
130
+ /**
131
+ * Normalize persist options to standard format
132
+ */
133
+ private normalizePersistOptions;
134
+ /**
135
+ * Setup IPC handlers for communication with renderers
136
+ */
137
+ private setupIpcHandlers;
138
+ private addTransaction;
139
+ /**
140
+ * Broadcast state update to all renderer processes
141
+ */
142
+ private broadcastStateUpdate;
143
+ /**
144
+ * Cleanup IPC handlers
145
+ */
146
+ destroy(): void;
147
+ }
148
+ /**
149
+ * Create and initialize the Main process sync manager
150
+ */
151
+ declare function createMainSync(options?: MainSyncOptions): MainSync;
152
+
153
+ export { type DebugLevel, type DebugLogger, MainSync, createDebugLogger, createMainSync, formatPatchForDebug, formatStateForDebug };