@janhendry/nanostore-ipc-bridge 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jan Hendry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # @janhendry/nanostore-ipc-bridge
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@janhendry/nanostore-ipc-bridge.svg)](https://www.npmjs.com/package/@janhendry/nanostore-ipc-bridge)
4
+ [![license](https://img.shields.io/npm/l/@janhendry/nanostore-ipc-bridge.svg)](https://github.com/janhendry/nanostore-ipc-bridge/blob/main/LICENSE)
5
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@janhendry/nanostore-ipc-bridge)](https://bundlephobia.com/package/@janhendry/nanostore-ipc-bridge)
6
+ [![changelog](https://img.shields.io/badge/changelog-📝-blue)](https://github.com/janhendry/nanostore-ipc-bridge/blob/main/packages/nanostore-ipc-bridge/CHANGELOG.md)
7
+
8
+ **Zero-config Electron IPC bridge for NanoStores** – Synchronize nanostores between main and renderer processes with full TypeScript support.
9
+
10
+ ## Features
11
+
12
+ ✅ **Zero-config** – Import once, works everywhere
13
+ ✅ **Type-safe** – Full TypeScript support with inference
14
+ ✅ **Multi-window sync** – All renderer windows stay in sync automatically
15
+ ✅ **Race-condition free** – Monotonic revision tracking prevents stale updates
16
+ ✅ **Services/RPC** – Define type-safe services with events
17
+ ✅ **Developer-friendly** – No boilerplate, no manual registration
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install @janhendry/nanostore-ipc-bridge nanostores
25
+ ```
26
+
27
+ 📦 **[View on npm](https://www.npmjs.com/package/@janhendry/nanostore-ipc-bridge)** | 💬 **[Report Issues](https://github.com/janhendry/nanostore-ipc-bridge/issues)** | 📖 **[Full Documentation](https://github.com/janhendry/nanostore-ipc-bridge)** | 📝 **[Changelog](https://github.com/janhendry/nanostore-ipc-bridge/blob/main/packages/nanostore-ipc-bridge/CHANGELOG.md)**
28
+
29
+ ## Quick Start
30
+
31
+ ### 1. Define a synced store (shared file)
32
+
33
+ ```typescript
34
+ // shared/stores.ts
35
+ import { syncedAtom } from "@janhendry/nanostore-ipc-bridge/universal";
36
+
37
+ export const $counter = syncedAtom("counter", 0);
38
+ export const $settings = syncedAtom("settings", { theme: "dark" });
39
+ ```
40
+
41
+ ### 2. Initialize in Main process
42
+
43
+ ```typescript
44
+ // electron/main.ts
45
+ import { initNanoStoreIPC } from "@janhendry/nanostore-ipc-bridge/main";
46
+ import "../shared/stores"; // Import stores to register them
47
+
48
+ const mainWindow = new BrowserWindow({
49
+ webPreferences: { preload: path.join(__dirname, "preload.js") },
50
+ });
51
+
52
+ initNanoStoreIPC({ autoRegisterWindows: true });
53
+ ```
54
+
55
+ ### 3. Expose API in Preload script
56
+
57
+ ```typescript
58
+ // electron/preload.ts
59
+ import { exposeNanoStoreIPC } from "@janhendry/nanostore-ipc-bridge/preload";
60
+
61
+ exposeNanoStoreIPC();
62
+ ```
63
+
64
+ ### 4. Use in Renderer
65
+
66
+ ```typescript
67
+ // renderer/App.tsx
68
+ import { useStore } from "@nanostores/react";
69
+ import { $counter, $settings } from "../shared/stores";
70
+
71
+ export function App() {
72
+ const counter = useStore($counter);
73
+ const settings = useStore($settings);
74
+
75
+ return (
76
+ <div>
77
+ <p>Counter: {counter}</p>
78
+ <button onClick={() => $counter.set(counter + 1)}>+1</button>
79
+
80
+ <p>Theme: {settings.theme}</p>
81
+ <button onClick={() => $settings.set({ ...settings, theme: "light" })}>
82
+ Toggle Theme
83
+ </button>
84
+ </div>
85
+ );
86
+ }
87
+ ```
88
+
89
+ **That's it!** All windows will stay in sync automatically.
90
+
91
+ ---
92
+
93
+ ## Services (RPC + Events)
94
+
95
+ Define type-safe services for complex operations:
96
+
97
+ ```typescript
98
+ // shared/todoService.ts
99
+ import { defineService } from "@janhendry/nanostore-ipc-bridge/services";
100
+
101
+ export const todoService = defineService("todos", {
102
+ addTodo: async (text: string) => {
103
+ const todo = { id: Date.now(), text, completed: false };
104
+ todos.push(todo);
105
+ todoService.broadcast("todoAdded", todo);
106
+ return todo;
107
+ },
108
+
109
+ deleteTodo: async (id: number) => {
110
+ todos = todos.filter((t) => t.id !== id);
111
+ todoService.broadcast("todoDeleted", id);
112
+ },
113
+ });
114
+ ```
115
+
116
+ Use in Renderer:
117
+
118
+ ```typescript
119
+ // Add todo via RPC
120
+ const todo = await todoService.addTodo("Buy milk");
121
+
122
+ // Listen to events
123
+ todoService.on("todoAdded", (todo) => {
124
+ console.log("New todo:", todo);
125
+ });
126
+ ```
127
+
128
+ ---
129
+
130
+ ## API Reference
131
+
132
+ ### `syncedAtom(id, initialValue)`
133
+
134
+ Creates a synchronized atom that works in both Main and Renderer.
135
+
136
+ - **Main process**: Real nanostore with automatic registration
137
+ - **Renderer process**: IPC-backed proxy that syncs with Main
138
+ - **Returns**: Standard nanostore atom with `.get()`, `.set()`, `.subscribe()`
139
+
140
+ ### `initNanoStoreIPC(options?)`
141
+
142
+ Initialize the IPC bridge in Main process.
143
+
144
+ **Options:**
145
+
146
+ - `channelPrefix?: string` – IPC channel prefix (default: `'ns'`)
147
+ - `autoRegisterWindows?: boolean` – Auto-register new windows (default: `true`)
148
+ - `allowRendererSet?: boolean` – Allow renderer to modify stores (default: `true`)
149
+
150
+ ### `exposeNanoStoreIPC(options?)`
151
+
152
+ Expose IPC API in Preload script.
153
+
154
+ **Options:**
155
+
156
+ - `channelPrefix?: string` – Must match Main process (default: `'ns'`)
157
+ - `globalName?: string` – Global variable name (default: `'nanostoreIPC'`)
158
+
159
+ ### `defineService(name, handlers)`
160
+
161
+ Define a type-safe service with RPC methods and events.
162
+
163
+ - **Main process**: Handlers execute locally
164
+ - **Renderer process**: Returns RPC proxy
165
+ - **Events**: Use `.broadcast(event, data)` in handlers, `.on(event, cb)` in renderer
166
+
167
+ ---
168
+
169
+ ## How it works
170
+
171
+ 1. **Main process** creates real nanostores and handles IPC requests
172
+ 2. **Renderer processes** get IPC-backed proxies that forward operations
173
+ 3. **Revision tracking** prevents race conditions (subscribe-before-get is safe)
174
+ 4. **Auto-sync** broadcasts changes to all connected windows
175
+
176
+ ---
177
+
178
+ ## Repository
179
+
180
+ Full source code, demo app, and detailed architecture documentation:
181
+
182
+ 👉 **[github.com/janhendry/nanostore-ipc-bridge](https://github.com/janhendry/nanostore-ipc-bridge)**
183
+
184
+ ---
185
+
186
+ ## License
187
+
188
+ MIT
@@ -0,0 +1,3 @@
1
+ export { DefineServiceOptions, SyncedAtomOptions, defineService, syncedAtom } from './universal/index.mjs';
2
+ import './types-CyJJt8gf.mjs';
3
+ import 'nanostores';
@@ -0,0 +1,3 @@
1
+ export { DefineServiceOptions, SyncedAtomOptions, defineService, syncedAtom } from './universal/index.js';
2
+ import './types-CyJJt8gf.js';
3
+ import 'nanostores';
package/dist/index.js ADDED
@@ -0,0 +1,232 @@
1
+ 'use strict';
2
+
3
+ var nanostores = require('nanostores');
4
+
5
+ // src/internal/symbols.ts
6
+ var WF_NS_QUEUE = /* @__PURE__ */ Symbol.for("wf.nanostore.ipc.queue");
7
+ var WF_NS_SERVICE_QUEUE = /* @__PURE__ */ Symbol.for("wf.nanostore.ipc.serviceQueue");
8
+ var WF_NS_MAIN_API = /* @__PURE__ */ Symbol.for("wf.nanostore.ipc.mainApi");
9
+
10
+ // src/universal/defineService.ts
11
+ function isElectronMain() {
12
+ return typeof process !== "undefined" && !!process.versions?.electron && globalThis.window === void 0;
13
+ }
14
+ function getMainApi() {
15
+ const globalWithSymbols = globalThis;
16
+ return globalWithSymbols[WF_NS_MAIN_API] ?? null;
17
+ }
18
+ function getRendererServiceIPC(globalName) {
19
+ if (globalThis.window === void 0) return null;
20
+ return globalThis[globalName];
21
+ }
22
+ function getServiceQueue() {
23
+ const globalWithSymbols = globalThis;
24
+ const q = globalWithSymbols[WF_NS_SERVICE_QUEUE] ?? /* @__PURE__ */ new Map();
25
+ globalWithSymbols[WF_NS_SERVICE_QUEUE] = q;
26
+ return q;
27
+ }
28
+ function defineService(options) {
29
+ const { id, handlers, hooks, globalName = "nanostoreIPC" } = options;
30
+ if (isElectronMain()) {
31
+ const api = getMainApi();
32
+ const definition = {
33
+ handlers,
34
+ beforeAll: hooks?.beforeAll,
35
+ afterAll: hooks?.afterAll
36
+ };
37
+ if (!api) {
38
+ const queue = getServiceQueue();
39
+ queue.set(id, { id, definition });
40
+ } else {
41
+ api.registerService(id, definition);
42
+ }
43
+ const proxy = {};
44
+ proxy.broadcast = (eventName, data) => {
45
+ const currentApi = getMainApi();
46
+ if (currentApi) {
47
+ currentApi.broadcast(id, eventName, data);
48
+ }
49
+ };
50
+ proxy.on = () => {
51
+ throw new Error(
52
+ `[defineService] Cannot use on() in Main process. Use broadcast() to send events to Renderer.`
53
+ );
54
+ };
55
+ for (const [methodName, handler] of Object.entries(handlers)) {
56
+ proxy[methodName] = handler;
57
+ }
58
+ return proxy;
59
+ } else {
60
+ const ipc = getRendererServiceIPC(globalName);
61
+ if (!ipc) {
62
+ throw new Error(
63
+ `[defineService] Renderer IPC not available. Ensure exposeNanoStoreIPC() is called in preload script with globalName="${globalName}".`
64
+ );
65
+ }
66
+ const proxy = {};
67
+ proxy.on = (eventName, callback) => {
68
+ return ipc.subscribeServiceEvent(id, eventName, callback);
69
+ };
70
+ proxy.broadcast = () => {
71
+ throw new Error(
72
+ `[defineService] Cannot use broadcast() in Renderer process. Use RPC methods to trigger events from Main.`
73
+ );
74
+ };
75
+ for (const methodName of Object.keys(handlers)) {
76
+ proxy[methodName] = (...args) => {
77
+ return ipc.callService(id, methodName, ...args);
78
+ };
79
+ }
80
+ return proxy;
81
+ }
82
+ }
83
+
84
+ // src/internal/types.ts
85
+ var NanoStoreIPCError = class extends Error {
86
+ constructor(message, code, storeId, originalError) {
87
+ super(message);
88
+ this.code = code;
89
+ this.storeId = storeId;
90
+ this.originalError = originalError;
91
+ this.name = "NanoStoreIPCError";
92
+ }
93
+ };
94
+
95
+ // src/universal/syncedAtom.ts
96
+ function isElectronMain2() {
97
+ return typeof process !== "undefined" && !!process.versions?.electron && globalThis.window === void 0;
98
+ }
99
+ function getRendererIPC(globalName) {
100
+ if (globalThis.window === void 0) return null;
101
+ return globalThis[globalName];
102
+ }
103
+ function getMainApi2() {
104
+ const globalWithSymbols = globalThis;
105
+ return globalWithSymbols[WF_NS_MAIN_API] ?? null;
106
+ }
107
+ function getQueue() {
108
+ const globalWithSymbols = globalThis;
109
+ const q = globalWithSymbols[WF_NS_QUEUE] ?? /* @__PURE__ */ new Map();
110
+ globalWithSymbols[WF_NS_QUEUE] = q;
111
+ return q;
112
+ }
113
+ function syncedAtom(id, initial, options = {}) {
114
+ const globalName = options.globalName ?? "nanostoreIPC";
115
+ if (isElectronMain2()) {
116
+ const $store = nanostores.atom(initial);
117
+ const api = getMainApi2();
118
+ if (api) {
119
+ api.registerStore(id, $store);
120
+ } else {
121
+ getQueue().set(id, $store);
122
+ }
123
+ return $store;
124
+ }
125
+ const ipc = getRendererIPC(globalName);
126
+ const $local = nanostores.atom(initial);
127
+ if (!ipc) {
128
+ if (options.warnIfNoIPC) {
129
+ console.warn(
130
+ `[syncedAtom] IPC not available for "${id}". Falling back to local atom().`
131
+ );
132
+ }
133
+ return $local;
134
+ }
135
+ let applyingRemote = false;
136
+ let lastRev = -1;
137
+ let readyForOutbound = false;
138
+ const rendererCanSet = options.rendererCanSet ?? true;
139
+ const unsubscribeRemote = ipc.subscribe(id, (snap) => {
140
+ if (snap.rev <= lastRev) return;
141
+ lastRev = snap.rev;
142
+ applyingRemote = true;
143
+ $local.set(snap.value);
144
+ applyingRemote = false;
145
+ readyForOutbound = true;
146
+ });
147
+ ipc.get(id).then((snap) => {
148
+ if (snap.rev <= lastRev) return;
149
+ lastRev = snap.rev;
150
+ applyingRemote = true;
151
+ $local.set(snap.value);
152
+ applyingRemote = false;
153
+ readyForOutbound = true;
154
+ }).catch((err) => {
155
+ const ipcError = err instanceof NanoStoreIPCError ? err : new NanoStoreIPCError(
156
+ `Failed to get initial value for store "${id}"`,
157
+ "IPC_FAILED",
158
+ id,
159
+ err
160
+ );
161
+ if (options.onError) {
162
+ options.onError(ipcError);
163
+ } else if (options.warnIfNoIPC) {
164
+ console.warn("[syncedAtom]", ipcError.message, ipcError);
165
+ }
166
+ readyForOutbound = true;
167
+ });
168
+ const unsubscribeLocal = $local.subscribe((value) => {
169
+ if (!rendererCanSet) return;
170
+ if (!readyForOutbound) return;
171
+ if (applyingRemote) return;
172
+ if (options.validateValue) {
173
+ const isValid = options.validateValue(value);
174
+ if (isValid === false) {
175
+ const err = new NanoStoreIPCError(
176
+ `Validation failed for store "${id}"`,
177
+ "SERIALIZATION_FAILED",
178
+ id
179
+ );
180
+ if (options.onError) options.onError(err);
181
+ return;
182
+ }
183
+ }
184
+ ipc.set(id, value).catch((err) => {
185
+ const ipcError = err instanceof NanoStoreIPCError ? err : new NanoStoreIPCError(
186
+ `Failed to set value for store "${id}"`,
187
+ "IPC_FAILED",
188
+ id,
189
+ err
190
+ );
191
+ if (options.onError) {
192
+ options.onError(ipcError);
193
+ }
194
+ });
195
+ });
196
+ $local.destroy = () => {
197
+ try {
198
+ unsubscribeRemote();
199
+ } catch (err) {
200
+ if (options.onError) {
201
+ options.onError(
202
+ new NanoStoreIPCError(
203
+ "Failed to unsubscribe from remote updates",
204
+ "IPC_FAILED",
205
+ id,
206
+ err
207
+ )
208
+ );
209
+ }
210
+ }
211
+ try {
212
+ unsubscribeLocal();
213
+ } catch (err) {
214
+ if (options.onError) {
215
+ options.onError(
216
+ new NanoStoreIPCError(
217
+ "Failed to unsubscribe from local updates",
218
+ "IPC_FAILED",
219
+ id,
220
+ err
221
+ )
222
+ );
223
+ }
224
+ }
225
+ };
226
+ return $local;
227
+ }
228
+
229
+ exports.defineService = defineService;
230
+ exports.syncedAtom = syncedAtom;
231
+ //# sourceMappingURL=index.js.map
232
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/internal/symbols.ts","../src/universal/defineService.ts","../src/internal/types.ts","../src/universal/syncedAtom.ts"],"names":["isElectronMain","getMainApi","atom"],"mappings":"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","file":"index.js","sourcesContent":["export const WF_NS_QUEUE = Symbol.for(\"wf.nanostore.ipc.queue\");\nexport const WF_NS_SERVICE_QUEUE = Symbol.for(\"wf.nanostore.ipc.serviceQueue\");\nexport const WF_NS_MAIN_API = Symbol.for(\"wf.nanostore.ipc.mainApi\");\n","import { WF_NS_MAIN_API, WF_NS_SERVICE_QUEUE } from \"../internal/symbols\";\nimport type {\n\tMainApi,\n\tServiceHandlers,\n\tServiceHooks,\n\tServiceQueueEntry,\n} from \"../internal/types\";\n\nexport interface DefineServiceOptions<THandlers extends ServiceHandlers> {\n\t/**\n\t * Service identifier - must be unique across your app\n\t */\n\tid: string;\n\t/**\n\t * Service method handlers\n\t * Main: executed locally\n\t * Renderer: creates RPC proxy\n\t */\n\thandlers: THandlers;\n\t/**\n\t * Optional middleware hooks for Main process only\n\t */\n\thooks?: ServiceHooks;\n\t/**\n\t * Window global name used by preload exposure.\n\t * Default: \"nanostoreIPC\"\n\t */\n\tglobalName?: string;\n}\n\ntype ServiceProxy<THandlers extends ServiceHandlers> = {\n\t[K in keyof THandlers]: THandlers[K] extends (\n\t\t...args: infer Args\n\t) => Promise<infer Return>\n\t\t? (...args: Args) => Promise<Return>\n\t\t: never;\n} & {\n\t/**\n\t * Subscribe to service events (Renderer only)\n\t */\n\ton: (eventName: string, callback: (data: unknown) => void) => () => void;\n\t/**\n\t * Broadcast an event to all renderer windows (Main only)\n\t */\n\tbroadcast: (eventName: string, data?: unknown) => void;\n};\n\nfunction isElectronMain(): boolean {\n\treturn (\n\t\ttypeof process !== \"undefined\" &&\n\t\t!!(process as { versions?: { electron?: string } }).versions?.electron &&\n\t\tglobalThis.window === undefined\n\t);\n}\n\nfunction getMainApi(): MainApi | null {\n\tconst globalWithSymbols = globalThis as Record<symbol, unknown>;\n\treturn (globalWithSymbols[WF_NS_MAIN_API] as MainApi | undefined) ?? null;\n}\n\ntype RendererServiceIPC = {\n\tcallService: <T = unknown>(\n\t\tserviceId: string,\n\t\tmethod: string,\n\t\t...args: unknown[]\n\t) => Promise<T>;\n\tsubscribeServiceEvent: (\n\t\tserviceId: string,\n\t\teventName: string,\n\t\tcb: (data: unknown) => void,\n\t) => () => void;\n};\n\nfunction getRendererServiceIPC(globalName: string): RendererServiceIPC | null {\n\tif (globalThis.window === undefined) return null;\n\treturn (globalThis as unknown as Record<string, unknown>)[\n\t\tglobalName\n\t] as RendererServiceIPC | null;\n}\n\nfunction getServiceQueue(): Map<string, ServiceQueueEntry> {\n\tconst globalWithSymbols = globalThis as Record<symbol, unknown>;\n\tconst q: Map<string, ServiceQueueEntry> =\n\t\t(globalWithSymbols[WF_NS_SERVICE_QUEUE] as\n\t\t\t| Map<string, ServiceQueueEntry>\n\t\t\t| undefined) ?? new Map();\n\tglobalWithSymbols[WF_NS_SERVICE_QUEUE] = q;\n\treturn q;\n}\n\n/**\n * Define a service that works in both Main and Renderer processes.\n *\n * **Main Process:**\n * - Registers handlers to be called via IPC\n * - Returns proxy with broadcast() method for sending events\n * - Handlers execute locally\n *\n * **Renderer Process:**\n * - Returns RPC proxy that calls Main process via IPC\n * - Returns proxy with on() method for listening to events\n * - All methods are async and execute remotely\n *\n * @example\n * ```ts\n * // shared/services/todoService.ts\n * export const todoService = defineService({\n * id: 'todos',\n * handlers: {\n * async addTodo(text: string) {\n * const todo = { id: Date.now(), text };\n * todos.push(todo);\n * todoService.broadcast('todoAdded', todo); // Main only\n * return todo;\n * },\n * async getTodos() {\n * return todos;\n * }\n * }\n * });\n *\n * // Main process (electron/main.ts)\n * import './shared/services/todoService'; // Auto-registers\n *\n * // Renderer (React component)\n * const todo = await todoService.addTodo('Buy milk'); // RPC call\n * todoService.on('todoAdded', (todo) => {\n * console.log('New todo:', todo);\n * });\n * ```\n */\nexport function defineService<THandlers extends ServiceHandlers>(\n\toptions: DefineServiceOptions<THandlers>,\n): ServiceProxy<THandlers> {\n\tconst { id, handlers, hooks, globalName = \"nanostoreIPC\" } = options;\n\n\tif (isElectronMain()) {\n\t\t// Main Process: Register service handlers or queue\n\t\tconst api = getMainApi();\n\t\tconst definition = {\n\t\t\thandlers,\n\t\t\tbeforeAll: hooks?.beforeAll,\n\t\t\tafterAll: hooks?.afterAll,\n\t\t};\n\n\t\tif (!api) {\n\t\t\t// Main API not ready yet - add to queue\n\t\t\tconst queue = getServiceQueue();\n\t\t\tqueue.set(id, { id, definition });\n\t\t} else {\n\t\t\t// Main API ready - register immediately\n\t\t\tapi.registerService(id, definition);\n\t\t}\n\n\t\t// Create proxy with broadcast() method\n\t\tconst proxy = {} as ServiceProxy<THandlers>;\n\n\t\t// Add broadcast method (works with or without API)\n\t\tproxy.broadcast = (eventName: string, data?: unknown) => {\n\t\t\tconst currentApi = getMainApi();\n\t\t\tif (currentApi) {\n\t\t\t\tcurrentApi.broadcast(id, eventName, data);\n\t\t\t}\n\t\t\t// If API not ready, broadcast is silently skipped (service not fully initialized yet)\n\t\t};\n\n\t\t// Add dummy on() that throws (Main shouldn't listen to events)\n\t\tproxy.on = () => {\n\t\t\tthrow new Error(\n\t\t\t\t`[defineService] Cannot use on() in Main process. Use broadcast() to send events to Renderer.`,\n\t\t\t);\n\t\t};\n\n\t\t// Bind handler methods directly (no RPC needed in Main)\n\t\tfor (const [methodName, handler] of Object.entries(handlers)) {\n\t\t\t(proxy as Record<string, unknown>)[methodName] = handler;\n\t\t}\n\n\t\treturn proxy;\n\t} else {\n\t\t// Renderer Process: Create RPC proxy\n\t\tconst ipc = getRendererServiceIPC(globalName);\n\t\tif (!ipc) {\n\t\t\tthrow new Error(\n\t\t\t\t`[defineService] Renderer IPC not available. Ensure exposeNanoStoreIPC() is called in preload script with globalName=\"${globalName}\".`,\n\t\t\t);\n\t\t}\n\n\t\tconst proxy = {} as ServiceProxy<THandlers>;\n\n\t\t// Add on() method for event subscription\n\t\tproxy.on = (eventName: string, callback: (data: unknown) => void) => {\n\t\t\treturn ipc.subscribeServiceEvent(id, eventName, callback);\n\t\t};\n\n\t\t// Add dummy broadcast() that throws (Renderer shouldn't broadcast)\n\t\tproxy.broadcast = () => {\n\t\t\tthrow new Error(\n\t\t\t\t`[defineService] Cannot use broadcast() in Renderer process. Use RPC methods to trigger events from Main.`,\n\t\t\t);\n\t\t};\n\n\t\t// Create RPC proxy for each handler\n\t\tfor (const methodName of Object.keys(handlers)) {\n\t\t\t(proxy as Record<string, unknown>)[methodName] = (...args: unknown[]) => {\n\t\t\t\treturn ipc.callService(id, methodName, ...args);\n\t\t\t};\n\t\t}\n\n\t\treturn proxy;\n\t}\n}\n","import type { Store } from \"nanostores\";\n\nexport type Snapshot<T> = { id: string; rev: number; value: T };\n\nexport type MainRegisterFn = <T = unknown>(id: string, store: Store<T>) => void;\n\nexport type MainApi = {\n\tregisterStore: MainRegisterFn;\n\tisInitialized: () => boolean;\n\tregisterService: ServiceRegisterFn;\n\tbroadcast: (serviceId: string, eventName: string, data?: unknown) => void;\n};\n\nexport type ErrorHandler = (error: NanoStoreIPCError) => void;\n\nexport class NanoStoreIPCError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic code:\n\t\t\t| \"STORE_NOT_FOUND\"\n\t\t\t| \"RENDERER_WRITE_DISABLED\"\n\t\t\t| \"SERIALIZATION_FAILED\"\n\t\t\t| \"IPC_FAILED\"\n\t\t\t| \"SERVICE_NOT_FOUND\"\n\t\t\t| \"SERVICE_METHOD_NOT_FOUND\"\n\t\t\t| \"ALREADY_INITIALIZED\",\n\t\tpublic storeId?: string,\n\t\tpublic originalError?: unknown,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"NanoStoreIPCError\";\n\t}\n}\n\n// Service types\nexport type ServiceHandler = (...args: unknown[]) => Promise<unknown>;\n\nexport type ServiceHandlers = Record<string, ServiceHandler>;\n\nexport type ServiceHooks = {\n\tbeforeAll?: (methodName: string, args: unknown[]) => void | Promise<void>;\n\tafterAll?: (\n\t\tmethodName: string,\n\t\tresult: unknown,\n\t\tduration: number,\n\t) => void | Promise<void>;\n};\n\nexport type ServiceDefinition<T extends ServiceHandlers = ServiceHandlers> = {\n\thandlers: T;\n\tbeforeAll?: (methodName: string, args: unknown[]) => void | Promise<void>;\n\tafterAll?: (\n\t\tmethodName: string,\n\t\tresult: unknown,\n\t\tduration: number,\n\t) => void | Promise<void>;\n};\n\nexport type ServiceQueueEntry<T extends ServiceHandlers = ServiceHandlers> = {\n\tid: string;\n\tdefinition: ServiceDefinition<T>;\n};\n\nexport type ServiceEventCallback = (data: unknown) => void;\n\nexport type ServiceBroadcastFn = (eventName: string, data: unknown) => void;\n\nexport type ServiceRegisterFn = <T extends ServiceHandlers>(\n\tid: string,\n\thandlers: ServiceDefinition<T>,\n) => void;\n\nexport type ServiceCallResult<T = unknown> = {\n\tsuccess: boolean;\n\tresult?: T;\n\terror?: {\n\t\tmessage: string;\n\t\tcode: string;\n\t\tstack?: string;\n\t};\n};\n","import type { Store, WritableAtom } from \"nanostores\";\nimport { atom } from \"nanostores\";\nimport { WF_NS_MAIN_API, WF_NS_QUEUE } from \"../internal/symbols\";\nimport type { ErrorHandler, MainApi, Snapshot } from \"../internal/types\";\nimport { NanoStoreIPCError } from \"../internal/types\";\n\nexport interface SyncedAtomOptions<T> {\n\t/**\n\t * If true, renderer writes are blocked even if main allows writes.\n\t * Useful to force \"actions only\" mutability later without breaking API.\n\t */\n\trendererCanSet?: boolean;\n\t/**\n\t * Optional: warn when IPC is not available (e.g. during SSR/tests).\n\t */\n\twarnIfNoIPC?: boolean;\n\t/**\n\t * Optional: channel prefix to match init/expose\n\t * If you set it here, you must also pass it to expose/init.\n\t * If omitted, uses unprefixed channels.\n\t */\n\tchannelPrefix?: string;\n\t/**\n\t * Window global name used by preload exposure.\n\t * Default: \"nanostoreIPC\"\n\t */\n\tglobalName?: string;\n\t/**\n\t * Error handler called when errors occur in IPC operations.\n\t */\n\tonError?: ErrorHandler;\n\t/**\n\t * Optional value validator. Return false to reject the value.\n\t * Can be used for runtime validation (e.g., with Zod).\n\t */\n\tvalidateValue?: (value: T) => boolean | Promise<boolean>;\n}\n\nfunction isElectronMain(): boolean {\n\t// In Electron main, `process.versions.electron` exists and there is no window/document.\n\treturn (\n\t\ttypeof process !== \"undefined\" &&\n\t\t!!(process as { versions?: { electron?: string } }).versions?.electron &&\n\t\tglobalThis.window === undefined\n\t);\n}\n\ntype RendererIPC = {\n\tget: <T = unknown>(id: string) => Promise<Snapshot<T>>;\n\tset: <T = unknown>(id: string, value: T) => Promise<void>;\n\tsubscribe: <T = unknown>(\n\t\tid: string,\n\t\tcb: (snap: Snapshot<T>) => void,\n\t) => () => void;\n\tsubscribeAll: (cb: (snap: Snapshot<unknown>) => void) => () => void;\n};\n\nfunction getRendererIPC(globalName: string): RendererIPC | null {\n\tif (globalThis.window === undefined) return null;\n\treturn (globalThis as unknown as Record<string, unknown>)[\n\t\tglobalName\n\t] as RendererIPC | null;\n}\n\nfunction getMainApi(): MainApi | null {\n\tconst globalWithSymbols = globalThis as Record<symbol, unknown>;\n\treturn (globalWithSymbols[WF_NS_MAIN_API] as MainApi | undefined) ?? null;\n}\n\nfunction getQueue(): Map<string, Store<unknown>> {\n\tconst globalWithSymbols = globalThis as Record<symbol, unknown>;\n\tconst q: Map<string, Store<unknown>> = (globalWithSymbols[WF_NS_QUEUE] as\n\t\t| Map<string, Store<unknown>>\n\t\t| undefined) ?? new Map();\n\tglobalWithSymbols[WF_NS_QUEUE] = q;\n\treturn q;\n}\n\n/**\n * syncedAtom(id, initial):\n * - In Electron Main: creates a real atom and registers it for IPC broadcast.\n * - In Electron Renderer: creates a proxy atom and syncs it via preload-exposed IPC API.\n * - Outside Electron: behaves as a normal atom(initial).\n *\n * No central \"bridge definition\" required; ID is the single piece of shared contract.\n */\nexport function syncedAtom<T>(\n\tid: string,\n\tinitial: T,\n\toptions: SyncedAtomOptions<T> = {},\n) {\n\tconst globalName = options.globalName ?? \"nanostoreIPC\";\n\n\t// MAIN: real store + registration\n\tif (isElectronMain()) {\n\t\tconst $store = atom<T>(initial);\n\n\t\tconst api = getMainApi();\n\t\tif (api) {\n\t\t\tapi.registerStore(id, $store);\n\t\t} else {\n\t\t\t// initNanoStoreIPC not called yet -> queue for later\n\t\t\tgetQueue().set(id, $store);\n\t\t}\n\n\t\treturn $store;\n\t}\n\n\t// RENDERER: IPC-backed proxy store (if IPC is available)\n\tconst ipc = getRendererIPC(globalName);\n\n\ttype AtomWithDestroy<T> = WritableAtom<T> & { destroy: () => void };\n\tconst $local = atom<T>(initial) as AtomWithDestroy<T>;\n\n\tif (!ipc) {\n\t\tif (options.warnIfNoIPC) {\n\t\t\t// eslint-disable-next-line no-console\n\t\t\tconsole.warn(\n\t\t\t\t`[syncedAtom] IPC not available for \"${id}\". Falling back to local atom().`,\n\t\t\t);\n\t\t}\n\t\treturn $local;\n\t}\n\n\tlet applyingRemote = false;\n\tlet lastRev = -1;\n\tlet readyForOutbound = false;\n\tconst rendererCanSet = options.rendererCanSet ?? true;\n\n\t// remote -> local (subscribe first)\n\tconst unsubscribeRemote = ipc.subscribe(id, (snap: Snapshot<T>) => {\n\t\tif (snap.rev <= lastRev) return;\n\t\tlastRev = snap.rev;\n\t\tapplyingRemote = true;\n\t\t$local.set(snap.value);\n\t\tapplyingRemote = false;\n\t\treadyForOutbound = true;\n\t});\n\n\t// then get snapshot (rev-gated)\n\tipc\n\t\t.get<T>(id)\n\t\t.then((snap: Snapshot<T>) => {\n\t\t\tif (snap.rev <= lastRev) return;\n\t\t\tlastRev = snap.rev;\n\n\t\t\tapplyingRemote = true;\n\t\t\t$local.set(snap.value);\n\t\t\tapplyingRemote = false;\n\t\t\treadyForOutbound = true;\n\t\t})\n\t\t.catch((err: unknown) => {\n\t\t\tconst ipcError =\n\t\t\t\terr instanceof NanoStoreIPCError\n\t\t\t\t\t? err\n\t\t\t\t\t: new NanoStoreIPCError(\n\t\t\t\t\t\t\t`Failed to get initial value for store \"${id}\"`,\n\t\t\t\t\t\t\t\"IPC_FAILED\",\n\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\terr,\n\t\t\t\t\t\t);\n\n\t\t\tif (options.onError) {\n\t\t\t\toptions.onError(ipcError);\n\t\t\t} else if (options.warnIfNoIPC) {\n\t\t\t\tconsole.warn(\"[syncedAtom]\", ipcError.message, ipcError);\n\t\t\t}\n\n\t\t\treadyForOutbound = true; // allow local usage even if remote missing\n\t\t});\n\n\t// local -> remote (after first remote snapshot or after get failed)\n\tconst unsubscribeLocal = $local.subscribe((value: T) => {\n\t\tif (!rendererCanSet) return;\n\t\tif (!readyForOutbound) return;\n\t\tif (applyingRemote) return;\n\n\t\t// Optional validation\n\t\tif (options.validateValue) {\n\t\t\tconst isValid = options.validateValue(value);\n\t\t\tif (isValid === false) {\n\t\t\t\tconst err = new NanoStoreIPCError(\n\t\t\t\t\t`Validation failed for store \"${id}\"`,\n\t\t\t\t\t\"SERIALIZATION_FAILED\",\n\t\t\t\t\tid,\n\t\t\t\t);\n\t\t\t\tif (options.onError) options.onError(err);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tipc.set(id, value).catch((err: unknown) => {\n\t\t\tconst ipcError =\n\t\t\t\terr instanceof NanoStoreIPCError\n\t\t\t\t\t? err\n\t\t\t\t\t: new NanoStoreIPCError(\n\t\t\t\t\t\t\t`Failed to set value for store \"${id}\"`,\n\t\t\t\t\t\t\t\"IPC_FAILED\",\n\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\terr,\n\t\t\t\t\t\t);\n\t\t\tif (options.onError) {\n\t\t\t\toptions.onError(ipcError);\n\t\t\t}\n\t\t});\n\t});\n\n\t// Optional cleanup hook\n\t$local.destroy = () => {\n\t\ttry {\n\t\t\tunsubscribeRemote();\n\t\t} catch (err) {\n\t\t\tif (options.onError) {\n\t\t\t\toptions.onError(\n\t\t\t\t\tnew NanoStoreIPCError(\n\t\t\t\t\t\t\"Failed to unsubscribe from remote updates\",\n\t\t\t\t\t\t\"IPC_FAILED\",\n\t\t\t\t\t\tid,\n\t\t\t\t\t\terr,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\tunsubscribeLocal();\n\t\t} catch (err) {\n\t\t\tif (options.onError) {\n\t\t\t\toptions.onError(\n\t\t\t\t\tnew NanoStoreIPCError(\n\t\t\t\t\t\t\"Failed to unsubscribe from local updates\",\n\t\t\t\t\t\t\"IPC_FAILED\",\n\t\t\t\t\t\tid,\n\t\t\t\t\t\terr,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t};\n\n\treturn $local;\n}\n"]}