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 +21 -21
- package/README.md +182 -182
- package/README.zh-CN.md +179 -179
- package/dist/index.d.ts +4 -2
- package/dist/index.js +35 -26
- package/package.json +14 -3
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
|
-
|
|
328
|
+
key: this.key,
|
|
329
|
+
path: [],
|
|
320
330
|
value: newVal
|
|
321
331
|
});
|
|
322
|
-
this.value = createDeepProxy(newVal,
|
|
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
|
-
|
|
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
|
-
|
|
396
|
+
const fullPath = [patch.key, ...patch.path];
|
|
388
397
|
if (patch.op === "clear" || patch.op === "add") {
|
|
389
|
-
const target = navigatePath(obj,
|
|
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,
|
|
398
|
-
const lastKey =
|
|
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 ===
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|