electron-pinia-sync 1.0.0 → 1.2.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/CONTRIBUTING.md +214 -33
- package/README.md +3 -5
- package/dist/main/index.cjs +266 -0
- package/dist/main/index.d.cts +153 -0
- package/dist/main/index.js +14 -8
- package/dist/preload/index.cjs +63 -0
- package/dist/preload/index.d.cts +2 -0
- package/dist/preload/index.js +5 -4
- package/dist/renderer/index.cjs +268 -0
- package/dist/renderer/index.d.cts +123 -0
- package/dist/renderer/index.js +51 -5
- package/package.json +12 -8
|
@@ -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 };
|
package/dist/main/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { ipcMain, BrowserWindow } from 'electron';
|
|
2
|
-
import { createPinia } from 'pinia';
|
|
3
|
-
import Store from 'electron-store';
|
|
4
|
-
|
|
5
1
|
// src/main/index.ts
|
|
2
|
+
import { BrowserWindow, ipcMain } from "electron";
|
|
3
|
+
import { createPinia } from "pinia";
|
|
4
|
+
import Store from "electron-store";
|
|
6
5
|
|
|
7
6
|
// src/types.ts
|
|
8
7
|
var IPC_CHANNELS = {
|
|
@@ -11,7 +10,10 @@ var IPC_CHANNELS = {
|
|
|
11
10
|
/** Renderer sends patch to Main */
|
|
12
11
|
STATE_PATCH: "pinia-sync:state-patch",
|
|
13
12
|
/** Main broadcasts state update to all Renderers */
|
|
14
|
-
STATE_UPDATED: "pinia-sync:state-updated"
|
|
13
|
+
STATE_UPDATED: "pinia-sync:state-updated",
|
|
14
|
+
/** Renderer requests full state sync */
|
|
15
|
+
STATE_SYNC: "pinia-sync:state-sync"
|
|
16
|
+
};
|
|
15
17
|
|
|
16
18
|
// src/debug.ts
|
|
17
19
|
function createDebugLogger(namespace, debugLevel = false, customLogger) {
|
|
@@ -216,6 +218,10 @@ var MainSync = class {
|
|
|
216
218
|
function createMainSync(options) {
|
|
217
219
|
return new MainSync(options);
|
|
218
220
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
221
|
+
export {
|
|
222
|
+
MainSync,
|
|
223
|
+
createDebugLogger,
|
|
224
|
+
createMainSync,
|
|
225
|
+
formatPatchForDebug,
|
|
226
|
+
formatStateForDebug
|
|
227
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// src/preload/index.ts
|
|
4
|
+
var import_electron = require("electron");
|
|
5
|
+
|
|
6
|
+
// src/types.ts
|
|
7
|
+
var IPC_CHANNELS = {
|
|
8
|
+
/** Renderer requests initial state from Main */
|
|
9
|
+
STATE_PULL: "pinia-sync:state-pull",
|
|
10
|
+
/** Renderer sends patch to Main */
|
|
11
|
+
STATE_PATCH: "pinia-sync:state-patch",
|
|
12
|
+
/** Main broadcasts state update to all Renderers */
|
|
13
|
+
STATE_UPDATED: "pinia-sync:state-updated",
|
|
14
|
+
/** Renderer requests full state sync */
|
|
15
|
+
STATE_SYNC: "pinia-sync:state-sync"
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/preload/index.ts
|
|
19
|
+
var piniaSyncAPI = {
|
|
20
|
+
/**
|
|
21
|
+
* Pull initial state from Main process
|
|
22
|
+
*/
|
|
23
|
+
async pullState(storeId) {
|
|
24
|
+
console.log(`[preload] IPC invoke: ${IPC_CHANNELS.STATE_PULL} for store "${storeId}"`);
|
|
25
|
+
const request = { storeId };
|
|
26
|
+
const response = await import_electron.ipcRenderer.invoke(
|
|
27
|
+
IPC_CHANNELS.STATE_PULL,
|
|
28
|
+
request
|
|
29
|
+
);
|
|
30
|
+
console.log(`[preload] IPC response: STATE_PULL for "${storeId}" - state:`, response.state ? "received" : "null");
|
|
31
|
+
return response.state;
|
|
32
|
+
},
|
|
33
|
+
/**
|
|
34
|
+
* Send state patch to Main process
|
|
35
|
+
*/
|
|
36
|
+
async patchState(storeId, patch, transactionId) {
|
|
37
|
+
console.log(`[preload] IPC invoke: ${IPC_CHANNELS.STATE_PATCH} for store "${storeId}", transaction: ${transactionId}`);
|
|
38
|
+
const message = {
|
|
39
|
+
storeId,
|
|
40
|
+
patch,
|
|
41
|
+
transactionId
|
|
42
|
+
};
|
|
43
|
+
await import_electron.ipcRenderer.invoke(IPC_CHANNELS.STATE_PATCH, message);
|
|
44
|
+
console.log(`[preload] IPC response: STATE_PATCH for "${storeId}" completed`);
|
|
45
|
+
},
|
|
46
|
+
/**
|
|
47
|
+
* Subscribe to state updates from Main process
|
|
48
|
+
*/
|
|
49
|
+
onStateUpdate(callback) {
|
|
50
|
+
console.log(`[preload] IPC listener registered: ${IPC_CHANNELS.STATE_UPDATED}`);
|
|
51
|
+
const listener = (_event, message) => {
|
|
52
|
+
console.log(`[preload] IPC event received: ${IPC_CHANNELS.STATE_UPDATED} for store "${message.storeId}", transaction: ${message.transactionId || "none"}`);
|
|
53
|
+
callback(message);
|
|
54
|
+
};
|
|
55
|
+
import_electron.ipcRenderer.on(IPC_CHANNELS.STATE_UPDATED, listener);
|
|
56
|
+
return () => {
|
|
57
|
+
console.log(`[preload] IPC listener removed: ${IPC_CHANNELS.STATE_UPDATED}`);
|
|
58
|
+
import_electron.ipcRenderer.removeListener(IPC_CHANNELS.STATE_UPDATED, listener);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
import_electron.contextBridge.exposeInMainWorld("piniaSync", piniaSyncAPI);
|
|
63
|
+
console.log("[preload] piniaSync API exposed to window");
|
package/dist/preload/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { contextBridge, ipcRenderer } from 'electron';
|
|
2
|
-
|
|
3
1
|
// src/preload/index.ts
|
|
2
|
+
import { contextBridge, ipcRenderer } from "electron";
|
|
4
3
|
|
|
5
4
|
// src/types.ts
|
|
6
5
|
var IPC_CHANNELS = {
|
|
@@ -9,7 +8,10 @@ var IPC_CHANNELS = {
|
|
|
9
8
|
/** Renderer sends patch to Main */
|
|
10
9
|
STATE_PATCH: "pinia-sync:state-patch",
|
|
11
10
|
/** Main broadcasts state update to all Renderers */
|
|
12
|
-
STATE_UPDATED: "pinia-sync:state-updated"
|
|
11
|
+
STATE_UPDATED: "pinia-sync:state-updated",
|
|
12
|
+
/** Renderer requests full state sync */
|
|
13
|
+
STATE_SYNC: "pinia-sync:state-sync"
|
|
14
|
+
};
|
|
13
15
|
|
|
14
16
|
// src/preload/index.ts
|
|
15
17
|
var piniaSyncAPI = {
|
|
@@ -57,4 +59,3 @@ var piniaSyncAPI = {
|
|
|
57
59
|
};
|
|
58
60
|
contextBridge.exposeInMainWorld("piniaSync", piniaSyncAPI);
|
|
59
61
|
console.log("[preload] piniaSync API exposed to window");
|
|
60
|
-
console.log("[preload] electron-pinia-sync preload script initialized");
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/renderer/index.ts
|
|
21
|
+
var renderer_exports = {};
|
|
22
|
+
__export(renderer_exports, {
|
|
23
|
+
createDebugLogger: () => createDebugLogger,
|
|
24
|
+
createRendererSync: () => createRendererSync,
|
|
25
|
+
formatPatchForDebug: () => formatPatchForDebug,
|
|
26
|
+
formatStateForDebug: () => formatStateForDebug
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(renderer_exports);
|
|
29
|
+
|
|
30
|
+
// node_modules/microdiff/dist/index.js
|
|
31
|
+
var richTypes = { Date: true, RegExp: true, String: true, Number: true };
|
|
32
|
+
function diff(obj, newObj, options = { cyclesFix: true }, _stack = []) {
|
|
33
|
+
let diffs = [];
|
|
34
|
+
const isObjArray = Array.isArray(obj);
|
|
35
|
+
for (const key in obj) {
|
|
36
|
+
const objKey = obj[key];
|
|
37
|
+
const path = isObjArray ? +key : key;
|
|
38
|
+
if (!(key in newObj)) {
|
|
39
|
+
diffs.push({
|
|
40
|
+
type: "REMOVE",
|
|
41
|
+
path: [path],
|
|
42
|
+
oldValue: obj[key]
|
|
43
|
+
});
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const newObjKey = newObj[key];
|
|
47
|
+
const areCompatibleObjects = typeof objKey === "object" && typeof newObjKey === "object" && Array.isArray(objKey) === Array.isArray(newObjKey);
|
|
48
|
+
if (objKey && newObjKey && areCompatibleObjects && !richTypes[Object.getPrototypeOf(objKey)?.constructor?.name] && (!options.cyclesFix || !_stack.includes(objKey))) {
|
|
49
|
+
diffs.push.apply(diffs, diff(objKey, newObjKey, options, options.cyclesFix ? _stack.concat([objKey]) : []).map((difference) => {
|
|
50
|
+
difference.path.unshift(path);
|
|
51
|
+
return difference;
|
|
52
|
+
}));
|
|
53
|
+
} else if (objKey !== newObjKey && // treat NaN values as equivalent
|
|
54
|
+
!(Number.isNaN(objKey) && Number.isNaN(newObjKey)) && !(areCompatibleObjects && (isNaN(objKey) ? objKey + "" === newObjKey + "" : +objKey === +newObjKey))) {
|
|
55
|
+
diffs.push({
|
|
56
|
+
path: [path],
|
|
57
|
+
type: "CHANGE",
|
|
58
|
+
value: newObjKey,
|
|
59
|
+
oldValue: objKey
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const isNewObjArray = Array.isArray(newObj);
|
|
64
|
+
for (const key in newObj) {
|
|
65
|
+
if (!(key in obj)) {
|
|
66
|
+
diffs.push({
|
|
67
|
+
type: "CREATE",
|
|
68
|
+
path: [isNewObjArray ? +key : key],
|
|
69
|
+
value: newObj[key]
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return diffs;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/debug.ts
|
|
77
|
+
function createDebugLogger(namespace, debugLevel = false, customLogger) {
|
|
78
|
+
const isEnabled = debugLevel !== false;
|
|
79
|
+
const isVerbose = debugLevel === "verbose" || debugLevel === true;
|
|
80
|
+
const prefix = `[${namespace}]`;
|
|
81
|
+
const noop = () => {
|
|
82
|
+
};
|
|
83
|
+
const logger = customLogger || console;
|
|
84
|
+
return {
|
|
85
|
+
log: logger.log?.bind(logger, prefix) || console.log.bind(console, prefix),
|
|
86
|
+
warn: logger.warn?.bind(logger, prefix) || console.warn.bind(console, prefix),
|
|
87
|
+
error: logger.error?.bind(logger, prefix) || console.error.bind(console, prefix),
|
|
88
|
+
debug: isEnabled ? logger.log?.bind(logger, prefix) || console.log.bind(console, prefix) : noop,
|
|
89
|
+
verbose: isVerbose ? logger.log?.bind(logger, `${prefix}[VERBOSE]`) || console.log.bind(console, `${prefix}[VERBOSE]`) : noop
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function formatStateForDebug(state, maxLength = 200) {
|
|
93
|
+
try {
|
|
94
|
+
const json = JSON.stringify(state);
|
|
95
|
+
if (json.length > maxLength) {
|
|
96
|
+
return json.substring(0, maxLength) + `... (${json.length} chars total)`;
|
|
97
|
+
}
|
|
98
|
+
return json;
|
|
99
|
+
} catch {
|
|
100
|
+
return "[Circular or non-serializable]";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function formatPatchForDebug(patch) {
|
|
104
|
+
try {
|
|
105
|
+
const keys = Object.keys(patch);
|
|
106
|
+
if (keys.length === 0) {
|
|
107
|
+
return "{}";
|
|
108
|
+
}
|
|
109
|
+
if (keys.length > 5) {
|
|
110
|
+
return `{ ${keys.slice(0, 5).join(", ")}, ... (${keys.length} keys) }`;
|
|
111
|
+
}
|
|
112
|
+
return JSON.stringify(patch, null, 2);
|
|
113
|
+
} catch {
|
|
114
|
+
return "[Invalid patch]";
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/utils/toRawState.ts
|
|
119
|
+
function toRawState(state) {
|
|
120
|
+
return JSON.parse(JSON.stringify(state));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/renderer/index.ts
|
|
124
|
+
function generateTransactionId() {
|
|
125
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
126
|
+
}
|
|
127
|
+
function calculatePatch(oldState, newState, mutation) {
|
|
128
|
+
const differences = diff(oldState, newState);
|
|
129
|
+
if (differences.length === 0) {
|
|
130
|
+
return extractPatchFromMutation(mutation, newState);
|
|
131
|
+
}
|
|
132
|
+
const patch = {};
|
|
133
|
+
for (const change of differences) {
|
|
134
|
+
if (change.path.length === 0) {
|
|
135
|
+
return newState;
|
|
136
|
+
}
|
|
137
|
+
const topLevelKey = change.path[0];
|
|
138
|
+
if (typeof topLevelKey === "string" || typeof topLevelKey === "number") {
|
|
139
|
+
patch[topLevelKey] = newState[topLevelKey];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return Object.keys(patch).length > 0 ? patch : extractPatchFromMutation(mutation, newState);
|
|
143
|
+
}
|
|
144
|
+
function extractPatchFromMutation(mutation, state) {
|
|
145
|
+
if (mutation.type === "patch object") {
|
|
146
|
+
return mutation.payload;
|
|
147
|
+
} else if (mutation.type === "patch function") {
|
|
148
|
+
return state;
|
|
149
|
+
} else if (mutation.type === "direct") {
|
|
150
|
+
const patch = {};
|
|
151
|
+
const events = mutation.events;
|
|
152
|
+
if (events && Array.isArray(events) && events.length > 0) {
|
|
153
|
+
const firstEvent = events[0];
|
|
154
|
+
const key = firstEvent?.key;
|
|
155
|
+
if (typeof key === "string" && key in state) {
|
|
156
|
+
patch[key] = state[key];
|
|
157
|
+
return patch;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return state;
|
|
161
|
+
} else {
|
|
162
|
+
return state;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function createRendererSync(options = {}) {
|
|
166
|
+
const debug = createDebugLogger("electron-pinia-sync:renderer", options.debug ?? false, options.logger);
|
|
167
|
+
debug.debug("Initializing RendererSync");
|
|
168
|
+
let api = options.customApi;
|
|
169
|
+
if (!api) {
|
|
170
|
+
if (typeof window !== "undefined" && window.piniaSync) {
|
|
171
|
+
api = window.piniaSync;
|
|
172
|
+
} else if (typeof globalThis !== "undefined" && globalThis.window?.piniaSync) {
|
|
173
|
+
api = globalThis.window.piniaSync;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!api) {
|
|
177
|
+
debug.error("window.piniaSync is not available. Make sure the preload script is loaded correctly.");
|
|
178
|
+
throw new Error("Pinia sync API not available");
|
|
179
|
+
}
|
|
180
|
+
debug.debug("Pinia sync API found");
|
|
181
|
+
const processingTransactions = /* @__PURE__ */ new Set();
|
|
182
|
+
return function rendererSyncPlugin(context) {
|
|
183
|
+
const { store } = context;
|
|
184
|
+
debug.debug(`Initializing sync for store: ${store.$id}`);
|
|
185
|
+
let isApplyingRemoteUpdate = false;
|
|
186
|
+
let previousState = toRawState(store.$state);
|
|
187
|
+
const initializeState = async () => {
|
|
188
|
+
debug.debug(`Pulling initial state for store: ${store.$id}`);
|
|
189
|
+
try {
|
|
190
|
+
const state = await api.pullState(store.$id);
|
|
191
|
+
if (state !== null) {
|
|
192
|
+
debug.verbose(`Received initial state for ${store.$id}:`, formatStateForDebug(state));
|
|
193
|
+
isApplyingRemoteUpdate = true;
|
|
194
|
+
store.$patch(state);
|
|
195
|
+
previousState = toRawState(store.$state);
|
|
196
|
+
isApplyingRemoteUpdate = false;
|
|
197
|
+
debug.debug(`Successfully initialized state for store: ${store.$id}`);
|
|
198
|
+
} else {
|
|
199
|
+
debug.debug(`No initial state available for store: ${store.$id}`);
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
debug.error(`Failed to pull initial state for store "${store.$id}":`, error);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const subscribeToLocalChanges = () => {
|
|
206
|
+
debug.debug(`Subscribing to local changes for store: ${store.$id}`);
|
|
207
|
+
store.$subscribe((mutation, state) => {
|
|
208
|
+
if (isApplyingRemoteUpdate) {
|
|
209
|
+
debug.verbose(`Skipping sync for ${store.$id} (applying remote update)`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
debug.verbose(`Local state changed for ${store.$id}, mutation type: ${mutation.type}`);
|
|
213
|
+
const patch = calculatePatch(previousState, state, mutation);
|
|
214
|
+
previousState = toRawState(state);
|
|
215
|
+
if (Object.keys(patch).length === 0) {
|
|
216
|
+
debug.verbose(`No changes detected for ${store.$id}, skipping sync`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
debug.verbose(`Calculated patch for ${store.$id}:`, formatPatchForDebug(patch));
|
|
220
|
+
const transactionId = generateTransactionId();
|
|
221
|
+
debug.debug(`Syncing patch to Main (transaction: ${transactionId})`);
|
|
222
|
+
processingTransactions.add(transactionId);
|
|
223
|
+
const rawPatch = toRawState(patch);
|
|
224
|
+
api.patchState(store.$id, rawPatch, transactionId).catch((error) => {
|
|
225
|
+
debug.error(`Failed to sync state for store "${store.$id}":`, error);
|
|
226
|
+
}).finally(() => {
|
|
227
|
+
setTimeout(() => {
|
|
228
|
+
processingTransactions.delete(transactionId);
|
|
229
|
+
}, 100);
|
|
230
|
+
});
|
|
231
|
+
}, { detached: true });
|
|
232
|
+
};
|
|
233
|
+
const subscribeToRemoteUpdates = () => {
|
|
234
|
+
debug.debug(`Subscribing to remote updates for store: ${store.$id}`);
|
|
235
|
+
const unsubscribe = api.onStateUpdate((message) => {
|
|
236
|
+
if (message.storeId !== store.$id) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
debug.verbose(`Received remote update for ${store.$id}`, message.transactionId ? `(transaction: ${message.transactionId})` : "");
|
|
240
|
+
if (message.transactionId && processingTransactions.has(message.transactionId)) {
|
|
241
|
+
debug.verbose(`Skipping echo update for ${store.$id} (transaction: ${message.transactionId})`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
debug.verbose(`Applying remote state to ${store.$id}:`, formatStateForDebug(message.state));
|
|
245
|
+
isApplyingRemoteUpdate = true;
|
|
246
|
+
store.$patch(message.state);
|
|
247
|
+
previousState = toRawState(store.$state);
|
|
248
|
+
isApplyingRemoteUpdate = false;
|
|
249
|
+
debug.debug(`Successfully applied remote update to store: ${store.$id}`);
|
|
250
|
+
});
|
|
251
|
+
store._piniaSync_unsubscribe = unsubscribe;
|
|
252
|
+
};
|
|
253
|
+
debug.debug(`Initializing store synchronization for: ${store.$id}`);
|
|
254
|
+
initializeState();
|
|
255
|
+
subscribeToLocalChanges();
|
|
256
|
+
subscribeToRemoteUpdates();
|
|
257
|
+
debug.debug(`Store ${store.$id} sync setup complete`);
|
|
258
|
+
const originalDispose = store.$dispose.bind(store);
|
|
259
|
+
store.$dispose = () => {
|
|
260
|
+
debug.debug(`Disposing store: ${store.$id}`);
|
|
261
|
+
const unsubscribe = store._piniaSync_unsubscribe;
|
|
262
|
+
if (unsubscribe) {
|
|
263
|
+
unsubscribe();
|
|
264
|
+
}
|
|
265
|
+
originalDispose();
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
}
|
|
@@ -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 };
|