remote-state-sync 1.0.0 → 1.0.2

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 CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 ButterCookies
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.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ButterCookies
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 CHANGED
@@ -1,182 +1,182 @@
1
- # Remote State Sync
2
-
3
- A lightweight, fully type-safe unidirectional remote state synchronization library. Effortlessly sync complex states with fine-grained precision from a Provider (e.g., Server or Electron Main) to a Receiver (e.g., Web Client or Electron Renderer).
4
-
5
- English | [中文](./README.zh-CN.md)
6
-
7
- ## Features
8
-
9
- - **Unidirectional Sync**: Safely sync state from A to B with an efficient patch-based diffing system.
10
- - **Deep Reactivity & Proxies**: Built-in, automatic support for intercepting deep changes across `Object`, `Map`, and `Set` structures.
11
- - **Batch Patching**: Automatically queues and batches rapid state mutations into singular, optimized events for high performance.
12
- - **Framework Agnostic Transport**: Bring your own transport layer (WebSocket, Socket.io, Electron IPC, HTTP, etc).
13
- - **Vue Integration**: Seamlessly connects to Vue's Reactivity system out of the box (`toValue()`, `toRef()`, `toShallowRef()`).
14
-
15
- ## Installation
16
-
17
- ```bash
18
- npm install remote-state-sync
19
- ```
20
-
21
- ## Examples
22
-
23
- ### 1. Hono + Socket.IO (Server & Client Web)
24
-
25
- **Server (Provider)**
26
-
27
- ```typescript
28
- import { SyncProvider } from 'remote-state-sync';
29
- import { Hono } from 'hono';
30
- import { Server } from 'socket.io';
31
-
32
- const app = new Hono();
33
- const io = new Server(3000);
34
-
35
- const provider = new SyncProvider();
36
- const usersNs = provider.register('users_space');
37
-
38
- // Define a state
39
- type UserState = {
40
- connected: number;
41
- history: string[];
42
- };
43
-
44
- // Initialize the state
45
- const userState = usersNs.sync<UserState>('data', {
46
- connected: 0,
47
- history: [],
48
- });
49
-
50
- // 1. snapshotGetter implementation via HTTP endpoint
51
- app.get('/snapshot/:namespace', (c) => {
52
- const ns = c.req.param('namespace');
53
- return c.json(provider.getStateSnapshot(ns));
54
- });
55
-
56
- io.on('connection', (socket) => {
57
- // Mutating deeply tracks changes and queues patches automatically
58
- userState.set((state) => void state.connected++);
59
-
60
- socket.on('disconnect', () => {
61
- userState.set((state) => void state.connected--);
62
- });
63
- });
64
-
65
- // 2. Broadcast patches triggered by deeper modifications over WebSocket
66
- provider.bus.on('update', (namespace, patches) => {
67
- io.emit('state-update', namespace, patches);
68
- });
69
- ```
70
-
71
- **Client (Receiver)**
72
-
73
- ```typescript
74
- import { SyncReceiver } from 'remote-state-sync';
75
- import { io } from 'socket.io-client';
76
-
77
- const socket = io('ws://localhost:3000');
78
-
79
- const receiver = new SyncReceiver({
80
- // 1. Fetch the initial snapshot over HTTP
81
- snapshotGetter: async (namespace) => {
82
- const res = await fetch(`http://localhost:3000/snapshot/${namespace}`);
83
- return res.json();
84
- },
85
- });
86
-
87
- // 2. Receive and apply patches incrementally
88
- socket.on('state-update', (namespace, patches) => {
89
- receiver.applyPatches(namespace, patches);
90
- });
91
-
92
- async function main() {
93
- const usersNs = await receiver.register('users_space');
94
-
95
- type UserState = { connected: number; history: string[] };
96
- const userState = usersNs.sync<UserState>('data');
97
-
98
- // Output: { connected: 1, history: [] }
99
- console.log(userState.toValue());
100
-
101
- // Listen to specific item changes!
102
- userState.on('update', (newVal, oldVal, patches) => {
103
- console.log('State updated!', newVal.connected);
104
- });
105
- }
106
- main();
107
- ```
108
-
109
- ### 2. Electron (`ipcMain` + `ipcRenderer`)
110
-
111
- **Main Process (Provider)**
112
-
113
- ```typescript
114
- import { SyncProvider } from 'remote-state-sync';
115
- import { ipcMain, BrowserWindow } from 'electron';
116
-
117
- const provider = new SyncProvider();
118
- const appNs = provider.register('app_ns');
119
-
120
- type SettingsState = {
121
- theme: 'dark' | 'light';
122
- version: string;
123
- };
124
-
125
- // Initialize the state
126
- const settings = appNs.sync<SettingsState>('settings', {
127
- theme: 'dark',
128
- version: '1.0.0',
129
- });
130
-
131
- // 1. snapshotGetter via ipcMain.handle
132
- ipcMain.handle('get-sync-snapshot', (_, namespace) => {
133
- return provider.getStateSnapshot(namespace);
134
- });
135
-
136
- // 2. Broadcast patches to all renderer windows
137
- provider.bus.on('update', (namespace, patches) => {
138
- BrowserWindow.getAllWindows().forEach((win) => {
139
- win.webContents.send('sync-update', namespace, patches);
140
- });
141
- });
142
-
143
- // Example modification later down the line
144
- setTimeout(() => {
145
- settings.set((state) => void (state.theme = 'light'));
146
- }, 5000);
147
- ```
148
-
149
- **Renderer Process (Receiver with Vue)**
150
-
151
- ```typescript
152
- import { SyncReceiver } from 'remote-state-sync';
153
- import { ipcRenderer } from 'electron';
154
- import { watch } from 'vue';
155
-
156
- const receiver = new SyncReceiver({
157
- // 1. Fetch the snapshot via ipcRenderer.invoke
158
- snapshotGetter: (namespace) => ipcRenderer.invoke('get-sync-snapshot', namespace),
159
- });
160
-
161
- // 2. Listen for patches from Main process
162
- ipcRenderer.on('sync-update', (_, namespace, patches) => {
163
- receiver.applyPatches(namespace, patches);
164
- });
165
-
166
- async function setup() {
167
- const appNs = await receiver.register('app_ns');
168
- const settings = appNs.sync<SettingsState>('settings');
169
-
170
- // Vue Reactivity directly tied to the remote state!
171
- const settingsRef = settings.toRef();
172
-
173
- watch(
174
- settingsRef,
175
- (newSettings) => {
176
- console.log('Renderer theme changed to:', newSettings.theme);
177
- },
178
- { deep: true },
179
- );
180
- }
181
- setup();
182
- ```
1
+ # Remote State Sync
2
+
3
+ A lightweight, fully type-safe unidirectional remote state synchronization library. Effortlessly sync complex states with fine-grained precision from a Provider (e.g., Server or Electron Main) to a Receiver (e.g., Web Client or Electron Renderer).
4
+
5
+ English | [中文](./README.zh-CN.md)
6
+
7
+ ## Features
8
+
9
+ - **Unidirectional Sync**: Safely sync state from A to B with an efficient patch-based diffing system.
10
+ - **Deep Reactivity & Proxies**: Built-in, automatic support for intercepting deep changes across `Object`, `Map`, and `Set` structures.
11
+ - **Batch Patching**: Automatically queues and batches rapid state mutations into singular, optimized events for high performance.
12
+ - **Framework Agnostic Transport**: Bring your own transport layer (WebSocket, Socket.io, Electron IPC, HTTP, etc).
13
+ - **Vue Integration**: Seamlessly connects to Vue's Reactivity system out of the box (`toValue()`, `toRef()`, `toShallowRef()`).
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install remote-state-sync
19
+ ```
20
+
21
+ ## Examples
22
+
23
+ ### 1. Hono + Socket.IO (Server & Client Web)
24
+
25
+ **Server (Provider)**
26
+
27
+ ```typescript
28
+ import { SyncProvider } from 'remote-state-sync';
29
+ import { Hono } from 'hono';
30
+ import { Server } from 'socket.io';
31
+
32
+ const app = new Hono();
33
+ const io = new Server(3000);
34
+
35
+ const provider = new SyncProvider();
36
+ const usersNs = provider.register('users_space');
37
+
38
+ // Define a state
39
+ type UserState = {
40
+ connected: number;
41
+ history: string[];
42
+ };
43
+
44
+ // Initialize the state
45
+ const userState = usersNs.sync<UserState>('data', {
46
+ connected: 0,
47
+ history: [],
48
+ });
49
+
50
+ // 1. snapshotGetter implementation via HTTP endpoint
51
+ app.get('/snapshot/:namespace', (c) => {
52
+ const ns = c.req.param('namespace');
53
+ return c.json(provider.getStateSnapshot(ns));
54
+ });
55
+
56
+ io.on('connection', (socket) => {
57
+ // Mutating deeply tracks changes and queues patches automatically
58
+ userState.set((state) => void state.connected++);
59
+
60
+ socket.on('disconnect', () => {
61
+ userState.set((state) => void state.connected--);
62
+ });
63
+ });
64
+
65
+ // 2. Broadcast patches triggered by deeper modifications over WebSocket
66
+ provider.bus.on('update', (namespace, patches) => {
67
+ io.emit('state-update', namespace, patches);
68
+ });
69
+ ```
70
+
71
+ **Client (Receiver)**
72
+
73
+ ```typescript
74
+ import { SyncReceiver } from 'remote-state-sync';
75
+ import { io } from 'socket.io-client';
76
+
77
+ const socket = io('ws://localhost:3000');
78
+
79
+ const receiver = new SyncReceiver({
80
+ // 1. Fetch the initial snapshot over HTTP
81
+ snapshotGetter: async (namespace) => {
82
+ const res = await fetch(`http://localhost:3000/snapshot/${namespace}`);
83
+ return res.json();
84
+ },
85
+ });
86
+
87
+ // 2. Receive and apply patches incrementally
88
+ socket.on('state-update', (namespace, patches) => {
89
+ receiver.applyPatches(namespace, patches);
90
+ });
91
+
92
+ async function main() {
93
+ const usersNs = await receiver.register('users_space');
94
+
95
+ type UserState = { connected: number; history: string[] };
96
+ const userState = usersNs.sync<UserState>('data');
97
+
98
+ // Output: { connected: 1, history: [] }
99
+ console.log(userState.toValue());
100
+
101
+ // Listen to specific item changes!
102
+ userState.on('update', (newVal, oldVal, patches) => {
103
+ console.log('State updated!', newVal.connected);
104
+ });
105
+ }
106
+ main();
107
+ ```
108
+
109
+ ### 2. Electron (`ipcMain` + `ipcRenderer`)
110
+
111
+ **Main Process (Provider)**
112
+
113
+ ```typescript
114
+ import { SyncProvider } from 'remote-state-sync';
115
+ import { ipcMain, BrowserWindow } from 'electron';
116
+
117
+ const provider = new SyncProvider();
118
+ const appNs = provider.register('app_ns');
119
+
120
+ type SettingsState = {
121
+ theme: 'dark' | 'light';
122
+ version: string;
123
+ };
124
+
125
+ // Initialize the state
126
+ const settings = appNs.sync<SettingsState>('settings', {
127
+ theme: 'dark',
128
+ version: '1.0.0',
129
+ });
130
+
131
+ // 1. snapshotGetter via ipcMain.handle
132
+ ipcMain.handle('get-sync-snapshot', (_, namespace) => {
133
+ return provider.getStateSnapshot(namespace);
134
+ });
135
+
136
+ // 2. Broadcast patches to all renderer windows
137
+ provider.bus.on('update', (namespace, patches) => {
138
+ BrowserWindow.getAllWindows().forEach((win) => {
139
+ win.webContents.send('sync-update', namespace, patches);
140
+ });
141
+ });
142
+
143
+ // Example modification later down the line
144
+ setTimeout(() => {
145
+ settings.set((state) => void (state.theme = 'light'));
146
+ }, 5000);
147
+ ```
148
+
149
+ **Renderer Process (Receiver with Vue)**
150
+
151
+ ```typescript
152
+ import { SyncReceiver } from 'remote-state-sync';
153
+ import { ipcRenderer } from 'electron';
154
+ import { watch } from 'vue';
155
+
156
+ const receiver = new SyncReceiver({
157
+ // 1. Fetch the snapshot via ipcRenderer.invoke
158
+ snapshotGetter: (namespace) => ipcRenderer.invoke('get-sync-snapshot', namespace),
159
+ });
160
+
161
+ // 2. Listen for patches from Main process
162
+ ipcRenderer.on('sync-update', (_, namespace, patches) => {
163
+ receiver.applyPatches(namespace, patches);
164
+ });
165
+
166
+ async function setup() {
167
+ const appNs = await receiver.register('app_ns');
168
+ const settings = appNs.sync<SettingsState>('settings');
169
+
170
+ // Vue Reactivity directly tied to the remote state!
171
+ const settingsRef = settings.toRef();
172
+
173
+ watch(
174
+ settingsRef,
175
+ (newSettings) => {
176
+ console.log('Renderer theme changed to:', newSettings.theme);
177
+ },
178
+ { deep: true },
179
+ );
180
+ }
181
+ setup();
182
+ ```
package/README.zh-CN.md CHANGED
@@ -1,179 +1,179 @@
1
- # Remote State Sync
2
-
3
- 轻量级、完全类型安全的单向远程状态同步库。轻松将复杂状态以细小粒度从 Provider(如:服务端 Server 或 Electron 主进程)同步到 Receiver(如:Web 客户端 Client 或 Electron 渲染进程)。
4
-
5
- [English](./README.md) | 中文
6
-
7
- ## 特性
8
-
9
- - **单向同步**:通过高效的增量 Patch (补丁) 算法,将状态从 A 端安全地同步到 B 端。
10
- - **深度响应与代理拦截**:内建支持对 `Object`、`Map` 和 `Set` 数据类型结构进行深层侦听、拦截与同步同步。
11
- - **补丁按需批处理**:高频次、连续的状态变更会在底层自动批量打包合并为单次事件分发,最大化性能。
12
- - **多端传输解耦**:不强绑定任何传输协议。可以灵活适配 WebSocket、Socket.io、Electron IPC、HTTP 等任意传输层网络!
13
- - **完美兼容 Vue**:提供无缝的 `@vue/reactivity` 支持能力,开箱即用(支持 `.toValue()`、`.toRef()` 和 `.toShallowRef()`)。
14
-
15
- ## 安装
16
-
17
- ```bash
18
- npm install remote-state-sync
19
- ```
20
-
21
- ## 场景示例
22
-
23
- ### 1. 组合:Hono + Socket.IO (Server端与Web端通信)
24
-
25
- **服务端 Server (提供方 Provider)**
26
-
27
- ```typescript
28
- import { SyncProvider } from 'remote-state-sync';
29
- import { Hono } from 'hono';
30
- import { Server } from 'socket.io';
31
-
32
- const app = new Hono();
33
- const io = new Server(3000);
34
-
35
- const provider = new SyncProvider();
36
- const usersNs = provider.register('users_space');
37
-
38
- // 定义一个状态
39
- type UserState = {
40
- connected: number;
41
- history: string[];
42
- };
43
-
44
- // 初始化这个状态
45
- const userState = usersNs.sync<UserState>('data', {
46
- connected: 0,
47
- history: [],
48
- });
49
-
50
- // 第一步:利用 Hono 提供 HTTP 接口
51
- // 向客户端暴露 Snapshot
52
- app.get('/snapshot/:namespace', (c) => {
53
- const ns = c.req.param('namespace');
54
- return c.json(provider.getStateSnapshot(ns));
55
- });
56
-
57
- io.on('connection', (socket) => {
58
- // 修改复杂对象会自动生成补丁(得益于内部 Proxy)
59
- userState.set((state) => void state.connected++);
60
-
61
- socket.on('disconnect', () => {
62
- userState.set((state) => void state.connected--);
63
- });
64
- });
65
-
66
- // 第二步:通过 WebSocket 向外部广播由于变更生成的批量 Patches
67
- provider.bus.on('update', (namespace, patches) => {
68
- io.emit('state-update', namespace, patches);
69
- });
70
- ```
71
-
72
- **Web客户端 Client (接收方 Receiver)**
73
-
74
- ```typescript
75
- import { SyncReceiver } from 'remote-state-sync';
76
- import { io } from 'socket.io-client';
77
-
78
- const socket = io('ws://localhost:3000');
79
-
80
- // 第一步:通过 HTTP 拉取远端快照,作为初始化状态
81
- snapshotGetter: (async (namespace) => {
82
- const res = await fetch(`http://localhost:3000/snapshot/${namespace}`);
83
- return res.json();
84
- },
85
- // 第二步:接管增量 Patches,增量更新本地状态树
86
- socket.on('state-update', (namespace, patches) => {
87
- receiver.applyPatches(namespace, patches);
88
- }));
89
-
90
- async function main() {
91
- const usersNs = await receiver.register('users_space');
92
-
93
- type UserState = { connected: number; history: string[] };
94
- const userState = usersNs.sync<UserState>('data');
95
-
96
- // Output: { connected: 1, history: [] }
97
- console.log(userState.toValue());
98
-
99
- userState.on('update', (newVal, oldVal, patches) => {
100
- console.log('状态更新', newVal.connected);
101
- });
102
- }
103
- main();
104
- ```
105
-
106
- ### 2.组合:Electron (`ipcMain` + `ipcRenderer`)
107
-
108
- **主进程 Main Process (提供方 Provider)**
109
-
110
- ```typescript
111
- import { SyncProvider } from 'remote-state-sync';
112
- import { ipcMain, BrowserWindow } from 'electron';
113
-
114
- const provider = new SyncProvider();
115
- const appNs = provider.register('app_ns');
116
-
117
- type SettingsState = {
118
- theme: 'dark' | 'light';
119
- version: string;
120
- };
121
-
122
- // 初始化状态
123
- const settings = appNs.sync<SettingsState>('settings', {
124
- theme: 'dark',
125
- version: '1.0.0',
126
- });
127
-
128
- // 第一步:通过 ipcMain.handle 暴露 snapshot 获取能力
129
- ipcMain.handle('get-sync-snapshot', (_, namespace) => {
130
- return provider.getStateSnapshot(namespace);
131
- });
132
-
133
- // 第二步:将状态补丁通过 ipcEvent 主动投递给所有渲染进程
134
- provider.bus.on('update', (namespace, patches) => {
135
- BrowserWindow.getAllWindows().forEach((win) => {
136
- win.webContents.send('sync-update', namespace, patches);
137
- });
138
- });
139
-
140
- // 修改示例
141
- setTimeout(() => {
142
- settings.set((state) => void (state.theme = 'light'));
143
- }, 5000);
144
- ```
145
-
146
- **渲染进程 Renderer Process (结合Vue使用的 Receiver)**
147
-
148
- ```typescript
149
- import { SyncReceiver } from 'remote-state-sync';
150
- import { ipcRenderer } from 'electron';
151
- import { watch } from 'vue';
152
-
153
- const receiver = new SyncReceiver({
154
- // 第一步:利用 ipcRenderer.invoke 异步请求 Snapshot
155
- snapshotGetter: (namespace) => ipcRenderer.invoke('get-sync-snapshot', namespace),
156
- });
157
-
158
- // 第二步:监听来自主进程投递的 Patches
159
- ipcRenderer.on('sync-update', (_, namespace, patches) => {
160
- receiver.applyPatches(namespace, patches);
161
- });
162
-
163
- async function setup() {
164
- const appNs = await receiver.register('app_ns');
165
- const settings = appNs.sync<SettingsState>('settings');
166
-
167
- // 第三步:将远端状态一键接入 Vue 的响应式生态内!
168
- const settingsRef = settings.toRef(); // 或 toShallowRef()
169
-
170
- watch(
171
- settingsRef,
172
- (newSettings) => {
173
- console.log('Renderer theme changed to:', newSettings.theme);
174
- },
175
- { deep: true },
176
- );
177
- }
178
- setup();
179
- ```
1
+ # Remote State Sync
2
+
3
+ 轻量级、完全类型安全的单向远程状态同步库。轻松将复杂状态以细小粒度从 Provider(如:服务端 Server 或 Electron 主进程)同步到 Receiver(如:Web 客户端 Client 或 Electron 渲染进程)。
4
+
5
+ [English](./README.md) | 中文
6
+
7
+ ## 特性
8
+
9
+ - **单向同步**:通过高效的增量 Patch (补丁) 算法,将状态从 A 端安全地同步到 B 端。
10
+ - **深度响应与代理拦截**:内建支持对 `Object`、`Map` 和 `Set` 数据类型结构进行深层侦听、拦截与同步同步。
11
+ - **补丁按需批处理**:高频次、连续的状态变更会在底层自动批量打包合并为单次事件分发,最大化性能。
12
+ - **多端传输解耦**:不强绑定任何传输协议。可以灵活适配 WebSocket、Socket.io、Electron IPC、HTTP 等任意传输层网络!
13
+ - **完美兼容 Vue**:提供无缝的 `@vue/reactivity` 支持能力,开箱即用(支持 `.toValue()`、`.toRef()` 和 `.toShallowRef()`)。
14
+
15
+ ## 安装
16
+
17
+ ```bash
18
+ npm install remote-state-sync
19
+ ```
20
+
21
+ ## 场景示例
22
+
23
+ ### 1. 组合:Hono + Socket.IO (Server端与Web端通信)
24
+
25
+ **服务端 Server (提供方 Provider)**
26
+
27
+ ```typescript
28
+ import { SyncProvider } from 'remote-state-sync';
29
+ import { Hono } from 'hono';
30
+ import { Server } from 'socket.io';
31
+
32
+ const app = new Hono();
33
+ const io = new Server(3000);
34
+
35
+ const provider = new SyncProvider();
36
+ const usersNs = provider.register('users_space');
37
+
38
+ // 定义一个状态
39
+ type UserState = {
40
+ connected: number;
41
+ history: string[];
42
+ };
43
+
44
+ // 初始化这个状态
45
+ const userState = usersNs.sync<UserState>('data', {
46
+ connected: 0,
47
+ history: [],
48
+ });
49
+
50
+ // 第一步:利用 Hono 提供 HTTP 接口
51
+ // 向客户端暴露 Snapshot
52
+ app.get('/snapshot/:namespace', (c) => {
53
+ const ns = c.req.param('namespace');
54
+ return c.json(provider.getStateSnapshot(ns));
55
+ });
56
+
57
+ io.on('connection', (socket) => {
58
+ // 修改复杂对象会自动生成补丁(得益于内部 Proxy)
59
+ userState.set((state) => void state.connected++);
60
+
61
+ socket.on('disconnect', () => {
62
+ userState.set((state) => void state.connected--);
63
+ });
64
+ });
65
+
66
+ // 第二步:通过 WebSocket 向外部广播由于变更生成的批量 Patches
67
+ provider.bus.on('update', (namespace, patches) => {
68
+ io.emit('state-update', namespace, patches);
69
+ });
70
+ ```
71
+
72
+ **Web客户端 Client (接收方 Receiver)**
73
+
74
+ ```typescript
75
+ import { SyncReceiver } from 'remote-state-sync';
76
+ import { io } from 'socket.io-client';
77
+
78
+ const socket = io('ws://localhost:3000');
79
+
80
+ // 第一步:通过 HTTP 拉取远端快照,作为初始化状态
81
+ snapshotGetter: (async (namespace) => {
82
+ const res = await fetch(`http://localhost:3000/snapshot/${namespace}`);
83
+ return res.json();
84
+ },
85
+ // 第二步:接管增量 Patches,增量更新本地状态树
86
+ socket.on('state-update', (namespace, patches) => {
87
+ receiver.applyPatches(namespace, patches);
88
+ }));
89
+
90
+ async function main() {
91
+ const usersNs = await receiver.register('users_space');
92
+
93
+ type UserState = { connected: number; history: string[] };
94
+ const userState = usersNs.sync<UserState>('data');
95
+
96
+ // Output: { connected: 1, history: [] }
97
+ console.log(userState.toValue());
98
+
99
+ userState.on('update', (newVal, oldVal, patches) => {
100
+ console.log('状态更新', newVal.connected);
101
+ });
102
+ }
103
+ main();
104
+ ```
105
+
106
+ ### 2.组合:Electron (`ipcMain` + `ipcRenderer`)
107
+
108
+ **主进程 Main Process (提供方 Provider)**
109
+
110
+ ```typescript
111
+ import { SyncProvider } from 'remote-state-sync';
112
+ import { ipcMain, BrowserWindow } from 'electron';
113
+
114
+ const provider = new SyncProvider();
115
+ const appNs = provider.register('app_ns');
116
+
117
+ type SettingsState = {
118
+ theme: 'dark' | 'light';
119
+ version: string;
120
+ };
121
+
122
+ // 初始化状态
123
+ const settings = appNs.sync<SettingsState>('settings', {
124
+ theme: 'dark',
125
+ version: '1.0.0',
126
+ });
127
+
128
+ // 第一步:通过 ipcMain.handle 暴露 snapshot 获取能力
129
+ ipcMain.handle('get-sync-snapshot', (_, namespace) => {
130
+ return provider.getStateSnapshot(namespace);
131
+ });
132
+
133
+ // 第二步:将状态补丁通过 ipcEvent 主动投递给所有渲染进程
134
+ provider.bus.on('update', (namespace, patches) => {
135
+ BrowserWindow.getAllWindows().forEach((win) => {
136
+ win.webContents.send('sync-update', namespace, patches);
137
+ });
138
+ });
139
+
140
+ // 修改示例
141
+ setTimeout(() => {
142
+ settings.set((state) => void (state.theme = 'light'));
143
+ }, 5000);
144
+ ```
145
+
146
+ **渲染进程 Renderer Process (结合Vue使用的 Receiver)**
147
+
148
+ ```typescript
149
+ import { SyncReceiver } from 'remote-state-sync';
150
+ import { ipcRenderer } from 'electron';
151
+ import { watch } from 'vue';
152
+
153
+ const receiver = new SyncReceiver({
154
+ // 第一步:利用 ipcRenderer.invoke 异步请求 Snapshot
155
+ snapshotGetter: (namespace) => ipcRenderer.invoke('get-sync-snapshot', namespace),
156
+ });
157
+
158
+ // 第二步:监听来自主进程投递的 Patches
159
+ ipcRenderer.on('sync-update', (_, namespace, patches) => {
160
+ receiver.applyPatches(namespace, patches);
161
+ });
162
+
163
+ async function setup() {
164
+ const appNs = await receiver.register('app_ns');
165
+ const settings = appNs.sync<SettingsState>('settings');
166
+
167
+ // 第三步:将远端状态一键接入 Vue 的响应式生态内!
168
+ const settingsRef = settings.toRef(); // 或 toShallowRef()
169
+
170
+ watch(
171
+ settingsRef,
172
+ (newSettings) => {
173
+ console.log('Renderer theme changed to:', newSettings.theme);
174
+ },
175
+ { deep: true },
176
+ );
177
+ }
178
+ setup();
179
+ ```
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ import { Ref, ShallowRef } from '@vue/reactivity';
4
4
  type PatchOperation = 'set' | 'delete' | 'clear' | 'add';
5
5
  interface Patch {
6
6
  op: PatchOperation;
7
+ key: string;
7
8
  path: (string | number)[];
8
9
  value?: unknown;
9
10
  }
@@ -13,6 +14,7 @@ interface SyncOptions {
13
14
  type SyncUpdater<T> = (state: T) => T | void;
14
15
  type SyncBusDefinition = {
15
16
  update: (namespace: string, patches: Patch[]) => void;
17
+ register: (namespace: string) => void;
16
18
  };
17
19
  type ReceiverItemBusDefinition<T> = {
18
20
  update: (newValue: T, oldValue: T, patches: Patch[]) => void;
@@ -20,7 +22,7 @@ type ReceiverItemBusDefinition<T> = {
20
22
 
21
23
  declare class SyncProvider {
22
24
  private namespaces;
23
- bus: Nanobus<SyncBusDefinition>;
25
+ readonly bus: Nanobus<SyncBusDefinition>;
24
26
  constructor();
25
27
  register(namespace: string): SyncNamespaceProvider;
26
28
  getStateSnapshot(namespace: string): Promise<Record<string, unknown>>;
@@ -68,7 +70,7 @@ declare class SyncItemReceiver<T> {
68
70
  private value;
69
71
  private _ref;
70
72
  private _shallowRef;
71
- bus: Nanobus<ReceiverItemBusDefinition<T>>;
73
+ readonly bus: Nanobus<ReceiverItemBusDefinition<T>>;
72
74
  constructor(key: string, initialValue: T);
73
75
  on(event: 'update', cb: (newValue: T, oldValue: T, patches: Patch[]) => void): void;
74
76
  toValue(): T;
package/dist/index.js CHANGED
@@ -53,19 +53,19 @@ function clearValue(current) {
53
53
  }
54
54
 
55
55
  // src/proxy.ts
56
- function createDeepProxy(target, path, onPatch) {
56
+ function createDeepProxy(target, rootKey, path, onPatch) {
57
57
  if (!shouldProxy(target)) {
58
58
  return target;
59
59
  }
60
60
  if (target instanceof Map) {
61
- return createMapProxy(target, path, onPatch);
61
+ return createMapProxy(target, rootKey, path, onPatch);
62
62
  }
63
63
  if (target instanceof Set) {
64
- return createSetProxy(target, path, onPatch);
64
+ return createSetProxy(target, rootKey, path, onPatch);
65
65
  }
66
- return createObjectProxy(target, path, onPatch);
66
+ return createObjectProxy(target, rootKey, path, onPatch);
67
67
  }
68
- function createObjectProxy(target, path, onPatch) {
68
+ function createObjectProxy(target, rootKey, path, onPatch) {
69
69
  const handler = {
70
70
  get(obj, prop, receiver) {
71
71
  const value = Reflect.get(obj, prop, receiver);
@@ -73,7 +73,7 @@ function createObjectProxy(target, path, onPatch) {
73
73
  return value;
74
74
  }
75
75
  if (shouldProxy(value)) {
76
- return createDeepProxy(value, [...path, prop], onPatch);
76
+ return createDeepProxy(value, rootKey, [...path, prop], onPatch);
77
77
  }
78
78
  return value;
79
79
  },
@@ -85,6 +85,7 @@ function createObjectProxy(target, path, onPatch) {
85
85
  if (success) {
86
86
  onPatch({
87
87
  op: "set",
88
+ key: rootKey,
88
89
  path: [...path, prop],
89
90
  value
90
91
  });
@@ -99,6 +100,7 @@ function createObjectProxy(target, path, onPatch) {
99
100
  if (success) {
100
101
  onPatch({
101
102
  op: "delete",
103
+ key: rootKey,
102
104
  path: [...path, prop]
103
105
  });
104
106
  }
@@ -107,7 +109,7 @@ function createObjectProxy(target, path, onPatch) {
107
109
  };
108
110
  return new Proxy(target, handler);
109
111
  }
110
- function createMapProxy(target, path, onPatch) {
112
+ function createMapProxy(target, rootKey, path, onPatch) {
111
113
  const handler = {
112
114
  get(obj, prop) {
113
115
  const value = Reflect.get(obj, prop);
@@ -120,6 +122,7 @@ function createMapProxy(target, path, onPatch) {
120
122
  const result = obj.set(key, val);
121
123
  onPatch({
122
124
  op: "set",
125
+ key: rootKey,
123
126
  path: [...path, key],
124
127
  value: val
125
128
  });
@@ -133,6 +136,7 @@ function createMapProxy(target, path, onPatch) {
133
136
  if (hasKey) {
134
137
  onPatch({
135
138
  op: "delete",
139
+ key: rootKey,
136
140
  path: [...path, key]
137
141
  });
138
142
  }
@@ -145,6 +149,7 @@ function createMapProxy(target, path, onPatch) {
145
149
  const result = obj.clear();
146
150
  onPatch({
147
151
  op: "clear",
152
+ key: rootKey,
148
153
  path
149
154
  });
150
155
  return result;
@@ -155,7 +160,7 @@ function createMapProxy(target, path, onPatch) {
155
160
  return function(key) {
156
161
  const getVal = obj.get(key);
157
162
  if (shouldProxy(getVal)) {
158
- return createDeepProxy(getVal, [...path, key], onPatch);
163
+ return createDeepProxy(getVal, rootKey, [...path, key], onPatch);
159
164
  }
160
165
  return getVal;
161
166
  };
@@ -167,7 +172,7 @@ function createMapProxy(target, path, onPatch) {
167
172
  };
168
173
  return new Proxy(target, handler);
169
174
  }
170
- function createSetProxy(target, path, onPatch) {
175
+ function createSetProxy(target, rootKey, path, onPatch) {
171
176
  const handler = {
172
177
  get(obj, prop) {
173
178
  const value = Reflect.get(obj, prop);
@@ -182,6 +187,7 @@ function createSetProxy(target, path, onPatch) {
182
187
  if (!hasVal) {
183
188
  onPatch({
184
189
  op: "add",
190
+ key: rootKey,
185
191
  // For set, we don't have a key to navigate, but we can pass the value
186
192
  // In a true Sync scenario, Set diffing is tricky. Path is just the Set itself.
187
193
  path,
@@ -198,6 +204,7 @@ function createSetProxy(target, path, onPatch) {
198
204
  if (hasVal) {
199
205
  onPatch({
200
206
  op: "delete",
207
+ key: rootKey,
201
208
  path,
202
209
  // Similarly, deleting from set happens at Set boundary
203
210
  value: val
@@ -213,6 +220,7 @@ function createSetProxy(target, path, onPatch) {
213
220
  const result = obj.clear();
214
221
  onPatch({
215
222
  op: "clear",
223
+ key: rootKey,
216
224
  path
217
225
  });
218
226
  return result;
@@ -239,6 +247,7 @@ var SyncProvider = class {
239
247
  }
240
248
  const ns = new SyncNamespaceProvider(namespace, this.bus);
241
249
  this.namespaces.set(namespace, ns);
250
+ this.bus.emit("register", namespace);
242
251
  return ns;
243
252
  }
244
253
  async getStateSnapshot(namespace) {
@@ -316,10 +325,11 @@ var SyncItemProvider = class {
316
325
  setValue(newVal) {
317
326
  this.onPatch({
318
327
  op: "set",
319
- path: [this.key],
328
+ key: this.key,
329
+ path: [],
320
330
  value: newVal
321
331
  });
322
- this.value = createDeepProxy(newVal, [this.key], this.onPatch);
332
+ this.value = createDeepProxy(newVal, this.key, [], this.onPatch);
323
333
  }
324
334
  };
325
335
 
@@ -365,8 +375,7 @@ var SyncNamespaceReceiver = class {
365
375
  applyPatches(patches) {
366
376
  const affectedItems = /* @__PURE__ */ new Map();
367
377
  for (const patch of patches) {
368
- if (patch.path.length === 0) continue;
369
- const key = patch.path[0];
378
+ const key = patch.key;
370
379
  const item = this.items.get(key);
371
380
  if (item) {
372
381
  if (!affectedItems.has(item)) {
@@ -384,9 +393,9 @@ var SyncNamespaceReceiver = class {
384
393
  }
385
394
  }
386
395
  applyPatchToObject(obj, patch) {
387
- if (patch.path.length === 0) return;
396
+ const fullPath = [patch.key, ...patch.path];
388
397
  if (patch.op === "clear" || patch.op === "add") {
389
- const target = navigatePath(obj, patch.path, 0, patch.path.length);
398
+ const target = navigatePath(obj, fullPath, 0, fullPath.length);
390
399
  if (patch.op === "clear") {
391
400
  clearValue(target);
392
401
  } else {
@@ -394,8 +403,8 @@ var SyncNamespaceReceiver = class {
394
403
  }
395
404
  return;
396
405
  }
397
- const current = navigatePath(obj, patch.path, 0, patch.path.length - 1);
398
- const lastKey = patch.path[patch.path.length - 1];
406
+ const current = navigatePath(obj, fullPath, 0, fullPath.length - 1);
407
+ const lastKey = fullPath[fullPath.length - 1];
399
408
  if (patch.op === "set") {
400
409
  setValueAtPath(current, lastKey, patch.value);
401
410
  } else if (patch.op === "delete") {
@@ -431,7 +440,7 @@ var SyncItemReceiver = class {
431
440
  return this._shallowRef;
432
441
  }
433
442
  applyPatch(patch) {
434
- if (patch.path.length === 1) {
443
+ if (patch.path.length === 0) {
435
444
  if (patch.op === "set") {
436
445
  this.value = patch.value;
437
446
  if (this._ref) this._ref.value = patch.value;
@@ -441,41 +450,41 @@ var SyncItemReceiver = class {
441
450
  }
442
451
  const lastKey = patch.path[patch.path.length - 1];
443
452
  if (patch.op === "set") {
444
- const current = navigatePath(this.value, patch.path, 1, patch.path.length - 1);
453
+ const current = navigatePath(this.value, patch.path, 0, patch.path.length - 1);
445
454
  const refCurrent = navigatePath(
446
455
  this._ref ? this._ref.value : null,
447
456
  patch.path,
448
- 1,
457
+ 0,
449
458
  patch.path.length - 1
450
459
  );
451
460
  setValueAtPath(current, lastKey, patch.value);
452
461
  setValueAtPath(refCurrent, lastKey, patch.value);
453
462
  } else if (patch.op === "delete") {
454
- const current = navigatePath(this.value, patch.path, 1, patch.path.length - 1);
463
+ const current = navigatePath(this.value, patch.path, 0, patch.path.length - 1);
455
464
  const refCurrent = navigatePath(
456
465
  this._ref ? this._ref.value : null,
457
466
  patch.path,
458
- 1,
467
+ 0,
459
468
  patch.path.length - 1
460
469
  );
461
470
  deleteValueAtPath(current, lastKey);
462
471
  deleteValueAtPath(refCurrent, lastKey);
463
472
  } else if (patch.op === "add") {
464
- const target = navigatePath(this.value, patch.path, 1, patch.path.length);
473
+ const target = navigatePath(this.value, patch.path, 0, patch.path.length);
465
474
  const refTarget = navigatePath(
466
475
  this._ref ? this._ref.value : null,
467
476
  patch.path,
468
- 1,
477
+ 0,
469
478
  patch.path.length
470
479
  );
471
480
  addValueToSet(target, patch.value);
472
481
  addValueToSet(refTarget, patch.value);
473
482
  } else if (patch.op === "clear") {
474
- const target = navigatePath(this.value, patch.path, 1, patch.path.length);
483
+ const target = navigatePath(this.value, patch.path, 0, patch.path.length);
475
484
  const refTarget = navigatePath(
476
485
  this._ref ? this._ref.value : null,
477
486
  patch.path,
478
- 1,
487
+ 0,
479
488
  patch.path.length
480
489
  );
481
490
  clearValue(target);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-state-sync",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "A lightweight, fully type-safe unidirectional remote state synchronization library.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -12,6 +12,15 @@
12
12
  "name": "Coooookies",
13
13
  "email": "admin@mitay.net"
14
14
  },
15
+ "keywords": [
16
+ "remote",
17
+ "state",
18
+ "sync",
19
+ "typescript",
20
+ "electron",
21
+ "state-management",
22
+ "ipc"
23
+ ],
15
24
  "main": "./dist/index.js",
16
25
  "module": "./dist/index.js",
17
26
  "types": "./dist/index.d.ts",
@@ -27,9 +36,11 @@
27
36
  "engines": {
28
37
  "node": ">=18.0.0"
29
38
  },
39
+ "provenance": true,
30
40
  "publishConfig": {
31
41
  "registry": "https://registry.npmjs.org/",
32
- "access": "public"
42
+ "access": "public",
43
+ "provenance": true
33
44
  },
34
45
  "scripts": {
35
46
  "dev": "tsup src/index.ts --watch",
@@ -57,4 +68,4 @@
57
68
  "@vue/reactivity": "^3.5.28",
58
69
  "nanobus": "^4.5.0"
59
70
  }
60
- }
71
+ }