remote-state-sync 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,9 +48,10 @@ const userState = usersNs.sync<UserState>('data', {
48
48
  });
49
49
 
50
50
  // 1. snapshotGetter implementation via HTTP endpoint
51
- app.get('/snapshot/:namespace', (c) => {
51
+ app.get('/snapshot/:namespace/:key', (c) => {
52
52
  const ns = c.req.param('namespace');
53
- return c.json(provider.getStateSnapshot(ns));
53
+ const key = c.req.param('key');
54
+ return c.json(provider.getStateSnapshot(ns, key));
54
55
  });
55
56
 
56
57
  io.on('connection', (socket) => {
@@ -78,8 +79,8 @@ const socket = io('ws://localhost:3000');
78
79
 
79
80
  const receiver = new SyncReceiver({
80
81
  // 1. Fetch the initial snapshot over HTTP
81
- snapshotGetter: async (namespace) => {
82
- const res = await fetch(`http://localhost:3000/snapshot/${namespace}`);
82
+ snapshotGetter: async (namespace, key) => {
83
+ const res = await fetch(`http://localhost:3000/snapshot/${namespace}/${key}`);
83
84
  return res.json();
84
85
  },
85
86
  });
@@ -93,7 +94,7 @@ async function main() {
93
94
  const usersNs = await receiver.register('users_space');
94
95
 
95
96
  type UserState = { connected: number; history: string[] };
96
- const userState = usersNs.sync<UserState>('data');
97
+ const userState = await usersNs.sync<UserState>('data');
97
98
 
98
99
  // Output: { connected: 1, history: [] }
99
100
  console.log(userState.toValue());
@@ -129,8 +130,8 @@ const settings = appNs.sync<SettingsState>('settings', {
129
130
  });
130
131
 
131
132
  // 1. snapshotGetter via ipcMain.handle
132
- ipcMain.handle('get-sync-snapshot', (_, namespace) => {
133
- return provider.getStateSnapshot(namespace);
133
+ ipcMain.handle('get-sync-snapshot', (_, namespace, key) => {
134
+ return provider.getStateSnapshot(namespace, key);
134
135
  });
135
136
 
136
137
  // 2. Broadcast patches to all renderer windows
@@ -155,7 +156,7 @@ import { watch } from 'vue';
155
156
 
156
157
  const receiver = new SyncReceiver({
157
158
  // 1. Fetch the snapshot via ipcRenderer.invoke
158
- snapshotGetter: (namespace) => ipcRenderer.invoke('get-sync-snapshot', namespace),
159
+ snapshotGetter: (namespace, key) => ipcRenderer.invoke('get-sync-snapshot', namespace, key),
159
160
  });
160
161
 
161
162
  // 2. Listen for patches from Main process
@@ -165,7 +166,7 @@ ipcRenderer.on('sync-update', (_, namespace, patches) => {
165
166
 
166
167
  async function setup() {
167
168
  const appNs = await receiver.register('app_ns');
168
- const settings = appNs.sync<SettingsState>('settings');
169
+ const settings = await appNs.sync<SettingsState>('settings');
169
170
 
170
171
  // Vue Reactivity directly tied to the remote state!
171
172
  const settingsRef = settings.toRef();
package/README.zh-CN.md CHANGED
@@ -49,9 +49,10 @@ const userState = usersNs.sync<UserState>('data', {
49
49
 
50
50
  // 第一步:利用 Hono 提供 HTTP 接口
51
51
  // 向客户端暴露 Snapshot
52
- app.get('/snapshot/:namespace', (c) => {
52
+ app.get('/snapshot/:namespace/:key', (c) => {
53
53
  const ns = c.req.param('namespace');
54
- return c.json(provider.getStateSnapshot(ns));
54
+ const key = c.req.param('key');
55
+ return c.json(provider.getStateSnapshot(ns, key));
55
56
  });
56
57
 
57
58
  io.on('connection', (socket) => {
@@ -77,21 +78,24 @@ import { io } from 'socket.io-client';
77
78
 
78
79
  const socket = io('ws://localhost:3000');
79
80
 
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
- }));
81
+ const receiver = new SyncReceiver({
82
+ // 第一步:通过 HTTP 拉取远端快照,作为初始化状态
83
+ snapshotGetter: async (namespace, key) => {
84
+ const res = await fetch(`http://localhost:3000/snapshot/${namespace}/${key}`);
85
+ return res.json();
86
+ },
87
+ });
88
+
89
+ // 第二步:接管增量 Patches,增量更新本地状态树
90
+ socket.on('state-update', (namespace, patches) => {
91
+ receiver.applyPatches(namespace, patches);
92
+ });
89
93
 
90
94
  async function main() {
91
95
  const usersNs = await receiver.register('users_space');
92
96
 
93
97
  type UserState = { connected: number; history: string[] };
94
- const userState = usersNs.sync<UserState>('data');
98
+ const userState = await usersNs.sync<UserState>('data');
95
99
 
96
100
  // Output: { connected: 1, history: [] }
97
101
  console.log(userState.toValue());
@@ -126,8 +130,8 @@ const settings = appNs.sync<SettingsState>('settings', {
126
130
  });
127
131
 
128
132
  // 第一步:通过 ipcMain.handle 暴露 snapshot 获取能力
129
- ipcMain.handle('get-sync-snapshot', (_, namespace) => {
130
- return provider.getStateSnapshot(namespace);
133
+ ipcMain.handle('get-sync-snapshot', (_, namespace, key) => {
134
+ return provider.getStateSnapshot(namespace, key);
131
135
  });
132
136
 
133
137
  // 第二步:将状态补丁通过 ipcEvent 主动投递给所有渲染进程
@@ -152,7 +156,7 @@ import { watch } from 'vue';
152
156
 
153
157
  const receiver = new SyncReceiver({
154
158
  // 第一步:利用 ipcRenderer.invoke 异步请求 Snapshot
155
- snapshotGetter: (namespace) => ipcRenderer.invoke('get-sync-snapshot', namespace),
159
+ snapshotGetter: (namespace, key) => ipcRenderer.invoke('get-sync-snapshot', namespace, key),
156
160
  });
157
161
 
158
162
  // 第二步:监听来自主进程投递的 Patches
@@ -162,7 +166,7 @@ ipcRenderer.on('sync-update', (_, namespace, patches) => {
162
166
 
163
167
  async function setup() {
164
168
  const appNs = await receiver.register('app_ns');
165
- const settings = appNs.sync<SettingsState>('settings');
169
+ const settings = await appNs.sync<SettingsState>('settings');
166
170
 
167
171
  // 第三步:将远端状态一键接入 Vue 的响应式生态内!
168
172
  const settingsRef = settings.toRef(); // 或 toShallowRef()
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { SuperJSONResult } from 'superjson';
1
2
  import Nanobus from 'nanobus';
2
- import { Ref, ShallowRef } from '@vue/reactivity';
3
+ import { Ref, DeepReadonly, ShallowRef } from '@vue/reactivity';
3
4
 
4
5
  type PatchOperation = 'set' | 'delete' | 'clear' | 'add';
5
6
  interface Patch {
@@ -9,7 +10,7 @@ interface Patch {
9
10
  value?: unknown;
10
11
  }
11
12
  interface SyncOptions {
12
- snapshotGetter: (namespace: string) => Promise<Record<string, unknown>>;
13
+ snapshotGetter: (namespace: string, key: string) => Promise<SyncStateSnapshot>;
13
14
  }
14
15
  type SyncUpdater<T> = (state: T) => T | void;
15
16
  type SyncBusDefinition = {
@@ -19,13 +20,14 @@ type SyncBusDefinition = {
19
20
  type ReceiverItemBusDefinition<T> = {
20
21
  update: (newValue: T, oldValue: T, patches: Patch[]) => void;
21
22
  };
23
+ type SyncStateSnapshot = SuperJSONResult;
22
24
 
23
25
  declare class SyncProvider {
24
- private namespaces;
25
26
  readonly bus: Nanobus<SyncBusDefinition>;
27
+ private namespaces;
26
28
  constructor();
27
29
  register(namespace: string): SyncNamespaceProvider;
28
- getStateSnapshot(namespace: string): Promise<Record<string, unknown>>;
30
+ getStateSnapshot(namespace: string, key: string): SyncStateSnapshot;
29
31
  }
30
32
  declare class SyncNamespaceProvider {
31
33
  readonly namespace: string;
@@ -35,7 +37,7 @@ declare class SyncNamespaceProvider {
35
37
  private emitTimeout;
36
38
  constructor(namespace: string, bus: Nanobus<SyncBusDefinition>);
37
39
  sync<T>(key: string, initialValue?: T): SyncItemProvider<T>;
38
- getSnapshot(): Record<string, unknown>;
40
+ getSnapshot(key: string): SyncStateSnapshot;
39
41
  private queuePatch;
40
42
  private emitPatches;
41
43
  }
@@ -43,9 +45,11 @@ declare class SyncItemProvider<T> {
43
45
  readonly key: string;
44
46
  private onPatch;
45
47
  private value;
48
+ private rawValue;
46
49
  constructor(key: string, initialValue: T | undefined, onPatch: (patch: Patch) => void);
47
50
  set(valOrUpdater: T | SyncUpdater<T>): void;
48
- toValue(): T;
51
+ get raw(): Readonly<T>;
52
+ toStructure(): SyncStateSnapshot;
49
53
  private setValue;
50
54
  }
51
55
 
@@ -58,27 +62,24 @@ declare class SyncReceiver {
58
62
  }
59
63
  declare class SyncNamespaceReceiver {
60
64
  readonly namespace: string;
61
- private snapshot;
65
+ private snapshotGetter;
62
66
  private items;
63
- constructor(namespace: string, snapshot: Record<string, unknown>);
64
- sync<T>(key: string): SyncItemReceiver<T>;
67
+ constructor(namespace: string, snapshotGetter: (namespace: string, key: string) => Promise<SyncStateSnapshot>);
68
+ sync<T>(key: string): Promise<SyncItemReceiver<T>>;
65
69
  applyPatches(patches: Patch[]): void;
66
- private applyPatchToObject;
67
70
  }
68
71
  declare class SyncItemReceiver<T> {
69
72
  readonly key: string;
73
+ readonly bus: Nanobus<ReceiverItemBusDefinition<T>>;
70
74
  private value;
71
75
  private _ref;
72
76
  private _shallowRef;
73
- readonly bus: Nanobus<ReceiverItemBusDefinition<T>>;
74
77
  constructor(key: string, initialValue: T);
75
- on(event: 'update', cb: (newValue: T, oldValue: T, patches: Patch[]) => void): void;
76
- toValue(): T;
77
- toRef(): Ref<T>;
78
- toShallowRef(): ShallowRef<T>;
78
+ get raw(): Readonly<T>;
79
+ toRef(): Readonly<Ref<DeepReadonly<T>>>;
80
+ toShallowRef(): Readonly<ShallowRef<DeepReadonly<T>>>;
79
81
  applyPatch(patch: Patch): void;
80
82
  triggerReactivity(): void;
81
- dispose(): void;
82
83
  }
83
84
 
84
- export { type Patch, type PatchOperation, type ReceiverItemBusDefinition, type SyncBusDefinition, SyncItemProvider, SyncItemReceiver, SyncNamespaceProvider, SyncNamespaceReceiver, type SyncOptions, SyncProvider, SyncReceiver, type SyncUpdater };
85
+ export { type Patch, type PatchOperation, type ReceiverItemBusDefinition, type SyncBusDefinition, SyncItemProvider, SyncItemReceiver, SyncNamespaceProvider, SyncNamespaceReceiver, type SyncOptions, SyncProvider, SyncReceiver, type SyncStateSnapshot, type SyncUpdater };
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  // src/provider.ts
2
2
  import Nanobus from "nanobus";
3
+ import SuperJSON2 from "superjson";
3
4
 
4
5
  // src/utils.ts
6
+ import SuperJSON from "superjson";
5
7
  function isObject(val) {
6
8
  return val !== null && typeof val === "object";
7
9
  }
@@ -51,6 +53,9 @@ function clearValue(current) {
51
53
  current.clear();
52
54
  }
53
55
  }
56
+ function deepStructureClone(raw) {
57
+ return SuperJSON.deserialize(SuperJSON.serialize(raw));
58
+ }
54
59
 
55
60
  // src/proxy.ts
56
61
  function createDeepProxy(target, rootKey, path, onPatch) {
@@ -237,8 +242,8 @@ function createSetProxy(target, rootKey, path, onPatch) {
237
242
 
238
243
  // src/provider.ts
239
244
  var SyncProvider = class {
240
- namespaces = /* @__PURE__ */ new Map();
241
245
  bus = new Nanobus("SyncProvider");
246
+ namespaces = /* @__PURE__ */ new Map();
242
247
  constructor() {
243
248
  }
244
249
  register(namespace) {
@@ -250,12 +255,12 @@ var SyncProvider = class {
250
255
  this.bus.emit("register", namespace);
251
256
  return ns;
252
257
  }
253
- async getStateSnapshot(namespace) {
258
+ getStateSnapshot(namespace, key) {
254
259
  const ns = this.namespaces.get(namespace);
255
260
  if (!ns) {
256
261
  throw new Error(`Namespace ${namespace} not found`);
257
262
  }
258
- return ns.getSnapshot();
263
+ return ns.getSnapshot(key);
259
264
  }
260
265
  };
261
266
  var SyncNamespaceProvider = class {
@@ -276,12 +281,12 @@ var SyncNamespaceProvider = class {
276
281
  this.items.set(key, item);
277
282
  return item;
278
283
  }
279
- getSnapshot() {
280
- const snapshot = {};
281
- for (const [key, item] of this.items.entries()) {
282
- snapshot[key] = item.toValue();
284
+ getSnapshot(key) {
285
+ const item = this.items.get(key);
286
+ if (!item) {
287
+ throw new Error(`Item ${key} not found in namespace ${this.namespace}`);
283
288
  }
284
- return snapshot;
289
+ return item.toStructure();
285
290
  }
286
291
  queuePatch(patch) {
287
292
  this.queuedPatches.push(patch);
@@ -308,6 +313,7 @@ var SyncItemProvider = class {
308
313
  }
309
314
  }
310
315
  value;
316
+ rawValue;
311
317
  set(valOrUpdater) {
312
318
  if (typeof valOrUpdater === "function") {
313
319
  const updater = valOrUpdater;
@@ -319,10 +325,14 @@ var SyncItemProvider = class {
319
325
  this.setValue(valOrUpdater);
320
326
  }
321
327
  }
322
- toValue() {
323
- return this.value;
328
+ get raw() {
329
+ return this.rawValue;
330
+ }
331
+ toStructure() {
332
+ return SuperJSON2.serialize(this.rawValue);
324
333
  }
325
334
  setValue(newVal) {
335
+ this.rawValue = newVal;
326
336
  this.onPatch({
327
337
  op: "set",
328
338
  key: this.key,
@@ -335,7 +345,13 @@ var SyncItemProvider = class {
335
345
 
336
346
  // src/receiver.ts
337
347
  import Nanobus2 from "nanobus";
338
- import { shallowRef, ref, triggerRef } from "@vue/reactivity";
348
+ import SuperJSON3 from "superjson";
349
+ import {
350
+ shallowRef,
351
+ ref,
352
+ triggerRef,
353
+ readonly
354
+ } from "@vue/reactivity";
339
355
  var SyncReceiver = class {
340
356
  constructor(options) {
341
357
  this.options = options;
@@ -345,8 +361,7 @@ var SyncReceiver = class {
345
361
  if (this.namespaces.has(namespace)) {
346
362
  return this.namespaces.get(namespace);
347
363
  }
348
- const snapshot = await this.options.snapshotGetter(namespace);
349
- const ns = new SyncNamespaceReceiver(namespace, snapshot);
364
+ const ns = new SyncNamespaceReceiver(namespace, this.options.snapshotGetter);
350
365
  this.namespaces.set(namespace, ns);
351
366
  return ns;
352
367
  }
@@ -358,16 +373,17 @@ var SyncReceiver = class {
358
373
  }
359
374
  };
360
375
  var SyncNamespaceReceiver = class {
361
- constructor(namespace, snapshot) {
376
+ constructor(namespace, snapshotGetter) {
362
377
  this.namespace = namespace;
363
- this.snapshot = snapshot;
378
+ this.snapshotGetter = snapshotGetter;
364
379
  }
365
380
  items = /* @__PURE__ */ new Map();
366
- sync(key) {
381
+ async sync(key) {
367
382
  if (this.items.has(key)) {
368
383
  return this.items.get(key);
369
384
  }
370
- const val = this.snapshot[key];
385
+ const snapshot = await this.snapshotGetter(this.namespace, key);
386
+ const val = SuperJSON3.deserialize(snapshot);
371
387
  const item = new SyncItemReceiver(key, val);
372
388
  this.items.set(key, item);
373
389
  return item;
@@ -379,36 +395,15 @@ var SyncNamespaceReceiver = class {
379
395
  const item = this.items.get(key);
380
396
  if (item) {
381
397
  if (!affectedItems.has(item)) {
382
- affectedItems.set(item, { oldVal: item.toValue(), patches: [] });
398
+ affectedItems.set(item, { oldVal: item.raw, patches: [] });
383
399
  }
384
400
  item.applyPatch(patch);
385
401
  affectedItems.get(item).patches.push(patch);
386
- } else {
387
- this.applyPatchToObject(this.snapshot, patch);
388
402
  }
389
403
  }
390
404
  for (const [item, data] of affectedItems.entries()) {
391
405
  item.triggerReactivity();
392
- item.bus.emit("update", item.toValue(), data.oldVal, data.patches);
393
- }
394
- }
395
- applyPatchToObject(obj, patch) {
396
- const fullPath = [patch.key, ...patch.path];
397
- if (patch.op === "clear" || patch.op === "add") {
398
- const target = navigatePath(obj, fullPath, 0, fullPath.length);
399
- if (patch.op === "clear") {
400
- clearValue(target);
401
- } else {
402
- addValueToSet(target, patch.value);
403
- }
404
- return;
405
- }
406
- const current = navigatePath(obj, fullPath, 0, fullPath.length - 1);
407
- const lastKey = fullPath[fullPath.length - 1];
408
- if (patch.op === "set") {
409
- setValueAtPath(current, lastKey, patch.value);
410
- } else if (patch.op === "delete") {
411
- deleteValueAtPath(current, lastKey);
406
+ item.bus.emit("update", item.raw, data.oldVal, data.patches);
412
407
  }
413
408
  }
414
409
  };
@@ -417,27 +412,24 @@ var SyncItemReceiver = class {
417
412
  this.key = key;
418
413
  this.value = initialValue;
419
414
  }
415
+ bus = new Nanobus2("SyncItemReceiver");
420
416
  value;
421
417
  _ref = null;
422
418
  _shallowRef = null;
423
- bus = new Nanobus2("SyncItemReceiver");
424
- on(event, cb) {
425
- this.bus.on(event, cb);
426
- }
427
- toValue() {
419
+ get raw() {
428
420
  return this.value;
429
421
  }
430
422
  toRef() {
431
423
  if (!this._ref) {
432
- this._ref = ref(this.value);
424
+ this._ref = ref(deepStructureClone(this.value));
433
425
  }
434
- return this._ref;
426
+ return readonly(this._ref);
435
427
  }
436
428
  toShallowRef() {
437
429
  if (!this._shallowRef) {
438
430
  this._shallowRef = shallowRef(this.value);
439
431
  }
440
- return this._shallowRef;
432
+ return readonly(this._shallowRef);
441
433
  }
442
434
  applyPatch(patch) {
443
435
  if (patch.path.length === 0) {
@@ -496,10 +488,6 @@ var SyncItemReceiver = class {
496
488
  triggerRef(this._shallowRef);
497
489
  }
498
490
  }
499
- dispose() {
500
- this._ref = null;
501
- this._shallowRef = null;
502
- }
503
491
  };
504
492
  export {
505
493
  SyncItemProvider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-state-sync",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "A lightweight, fully type-safe unidirectional remote state synchronization library.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -26,9 +26,11 @@
26
26
  "types": "./dist/index.d.ts",
27
27
  "exports": {
28
28
  ".": {
29
+ "types": "./dist/index.d.ts",
29
30
  "import": "./dist/index.js",
30
- "types": "./dist/index.d.ts"
31
- }
31
+ "default": "./dist/index.js"
32
+ },
33
+ "./package.json": "./package.json"
32
34
  },
33
35
  "files": [
34
36
  "dist"
@@ -66,6 +68,7 @@
66
68
  },
67
69
  "dependencies": {
68
70
  "@vue/reactivity": "^3.5.28",
69
- "nanobus": "^4.5.0"
71
+ "nanobus": "^4.5.0",
72
+ "superjson": "^2.2.6"
70
73
  }
71
74
  }