remote-state-sync 1.0.3 → 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,6 +1,6 @@
1
1
  import { SuperJSONResult } from 'superjson';
2
2
  import Nanobus from 'nanobus';
3
- import { Ref, ShallowRef } from '@vue/reactivity';
3
+ import { Ref, DeepReadonly, ShallowRef } from '@vue/reactivity';
4
4
 
5
5
  type PatchOperation = 'set' | 'delete' | 'clear' | 'add';
6
6
  interface Patch {
@@ -10,7 +10,7 @@ interface Patch {
10
10
  value?: unknown;
11
11
  }
12
12
  interface SyncOptions {
13
- snapshotGetter: (namespace: string) => Promise<SyncSnapshot>;
13
+ snapshotGetter: (namespace: string, key: string) => Promise<SyncStateSnapshot>;
14
14
  }
15
15
  type SyncUpdater<T> = (state: T) => T | void;
16
16
  type SyncBusDefinition = {
@@ -20,14 +20,14 @@ type SyncBusDefinition = {
20
20
  type ReceiverItemBusDefinition<T> = {
21
21
  update: (newValue: T, oldValue: T, patches: Patch[]) => void;
22
22
  };
23
- type SyncSnapshot = SuperJSONResult;
23
+ type SyncStateSnapshot = SuperJSONResult;
24
24
 
25
25
  declare class SyncProvider {
26
- private namespaces;
27
26
  readonly bus: Nanobus<SyncBusDefinition>;
27
+ private namespaces;
28
28
  constructor();
29
29
  register(namespace: string): SyncNamespaceProvider;
30
- getStateSnapshot(namespace: string): SyncSnapshot;
30
+ getStateSnapshot(namespace: string, key: string): SyncStateSnapshot;
31
31
  }
32
32
  declare class SyncNamespaceProvider {
33
33
  readonly namespace: string;
@@ -37,7 +37,7 @@ declare class SyncNamespaceProvider {
37
37
  private emitTimeout;
38
38
  constructor(namespace: string, bus: Nanobus<SyncBusDefinition>);
39
39
  sync<T>(key: string, initialValue?: T): SyncItemProvider<T>;
40
- getSnapshot(): SyncSnapshot;
40
+ getSnapshot(key: string): SyncStateSnapshot;
41
41
  private queuePatch;
42
42
  private emitPatches;
43
43
  }
@@ -45,9 +45,11 @@ declare class SyncItemProvider<T> {
45
45
  readonly key: string;
46
46
  private onPatch;
47
47
  private value;
48
+ private rawValue;
48
49
  constructor(key: string, initialValue: T | undefined, onPatch: (patch: Patch) => void);
49
50
  set(valOrUpdater: T | SyncUpdater<T>): void;
50
- toValue(): T;
51
+ get raw(): Readonly<T>;
52
+ toStructure(): SyncStateSnapshot;
51
53
  private setValue;
52
54
  }
53
55
 
@@ -60,27 +62,24 @@ declare class SyncReceiver {
60
62
  }
61
63
  declare class SyncNamespaceReceiver {
62
64
  readonly namespace: string;
63
- private snapshot;
65
+ private snapshotGetter;
64
66
  private items;
65
- constructor(namespace: string, snapshot: SyncSnapshot);
66
- 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>>;
67
69
  applyPatches(patches: Patch[]): void;
68
- private applyPatchToObject;
69
70
  }
70
71
  declare class SyncItemReceiver<T> {
71
72
  readonly key: string;
73
+ readonly bus: Nanobus<ReceiverItemBusDefinition<T>>;
72
74
  private value;
73
75
  private _ref;
74
76
  private _shallowRef;
75
- readonly bus: Nanobus<ReceiverItemBusDefinition<T>>;
76
77
  constructor(key: string, initialValue: T);
77
- on(event: 'update', cb: (newValue: T, oldValue: T, patches: Patch[]) => void): void;
78
- toValue(): T;
79
- toRef(): Ref<T>;
80
- toShallowRef(): ShallowRef<T>;
78
+ get raw(): Readonly<T>;
79
+ toRef(): Readonly<Ref<DeepReadonly<T>>>;
80
+ toShallowRef(): Readonly<ShallowRef<DeepReadonly<T>>>;
81
81
  applyPatch(patch: Patch): void;
82
82
  triggerReactivity(): void;
83
- dispose(): void;
84
83
  }
85
84
 
86
- export { type Patch, type PatchOperation, type ReceiverItemBusDefinition, type SyncBusDefinition, SyncItemProvider, SyncItemReceiver, SyncNamespaceProvider, SyncNamespaceReceiver, type SyncOptions, SyncProvider, SyncReceiver, type SyncSnapshot, 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,8 +1,9 @@
1
1
  // src/provider.ts
2
2
  import Nanobus from "nanobus";
3
- import SuperJSON from "superjson";
3
+ import SuperJSON2 from "superjson";
4
4
 
5
5
  // src/utils.ts
6
+ import SuperJSON from "superjson";
6
7
  function isObject(val) {
7
8
  return val !== null && typeof val === "object";
8
9
  }
@@ -52,6 +53,9 @@ function clearValue(current) {
52
53
  current.clear();
53
54
  }
54
55
  }
56
+ function deepStructureClone(raw) {
57
+ return SuperJSON.deserialize(SuperJSON.serialize(raw));
58
+ }
55
59
 
56
60
  // src/proxy.ts
57
61
  function createDeepProxy(target, rootKey, path, onPatch) {
@@ -238,8 +242,8 @@ function createSetProxy(target, rootKey, path, onPatch) {
238
242
 
239
243
  // src/provider.ts
240
244
  var SyncProvider = class {
241
- namespaces = /* @__PURE__ */ new Map();
242
245
  bus = new Nanobus("SyncProvider");
246
+ namespaces = /* @__PURE__ */ new Map();
243
247
  constructor() {
244
248
  }
245
249
  register(namespace) {
@@ -251,12 +255,12 @@ var SyncProvider = class {
251
255
  this.bus.emit("register", namespace);
252
256
  return ns;
253
257
  }
254
- getStateSnapshot(namespace) {
258
+ getStateSnapshot(namespace, key) {
255
259
  const ns = this.namespaces.get(namespace);
256
260
  if (!ns) {
257
261
  throw new Error(`Namespace ${namespace} not found`);
258
262
  }
259
- return ns.getSnapshot();
263
+ return ns.getSnapshot(key);
260
264
  }
261
265
  };
262
266
  var SyncNamespaceProvider = class {
@@ -277,12 +281,12 @@ var SyncNamespaceProvider = class {
277
281
  this.items.set(key, item);
278
282
  return item;
279
283
  }
280
- getSnapshot() {
281
- const snapshot = {};
282
- for (const [key, item] of this.items.entries()) {
283
- 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}`);
284
288
  }
285
- return SuperJSON.serialize(snapshot);
289
+ return item.toStructure();
286
290
  }
287
291
  queuePatch(patch) {
288
292
  this.queuedPatches.push(patch);
@@ -309,6 +313,7 @@ var SyncItemProvider = class {
309
313
  }
310
314
  }
311
315
  value;
316
+ rawValue;
312
317
  set(valOrUpdater) {
313
318
  if (typeof valOrUpdater === "function") {
314
319
  const updater = valOrUpdater;
@@ -320,10 +325,14 @@ var SyncItemProvider = class {
320
325
  this.setValue(valOrUpdater);
321
326
  }
322
327
  }
323
- toValue() {
324
- return this.value;
328
+ get raw() {
329
+ return this.rawValue;
330
+ }
331
+ toStructure() {
332
+ return SuperJSON2.serialize(this.rawValue);
325
333
  }
326
334
  setValue(newVal) {
335
+ this.rawValue = newVal;
327
336
  this.onPatch({
328
337
  op: "set",
329
338
  key: this.key,
@@ -336,8 +345,13 @@ var SyncItemProvider = class {
336
345
 
337
346
  // src/receiver.ts
338
347
  import Nanobus2 from "nanobus";
339
- import { shallowRef, ref, triggerRef } from "@vue/reactivity";
340
- import SuperJSON2 from "superjson";
348
+ import SuperJSON3 from "superjson";
349
+ import {
350
+ shallowRef,
351
+ ref,
352
+ triggerRef,
353
+ readonly
354
+ } from "@vue/reactivity";
341
355
  var SyncReceiver = class {
342
356
  constructor(options) {
343
357
  this.options = options;
@@ -347,8 +361,7 @@ var SyncReceiver = class {
347
361
  if (this.namespaces.has(namespace)) {
348
362
  return this.namespaces.get(namespace);
349
363
  }
350
- const snapshot = await this.options.snapshotGetter(namespace);
351
- const ns = new SyncNamespaceReceiver(namespace, snapshot);
364
+ const ns = new SyncNamespaceReceiver(namespace, this.options.snapshotGetter);
352
365
  this.namespaces.set(namespace, ns);
353
366
  return ns;
354
367
  }
@@ -360,17 +373,17 @@ var SyncReceiver = class {
360
373
  }
361
374
  };
362
375
  var SyncNamespaceReceiver = class {
363
- constructor(namespace, snapshot) {
376
+ constructor(namespace, snapshotGetter) {
364
377
  this.namespace = namespace;
365
- this.snapshot = snapshot;
378
+ this.snapshotGetter = snapshotGetter;
366
379
  }
367
380
  items = /* @__PURE__ */ new Map();
368
- sync(key) {
381
+ async sync(key) {
369
382
  if (this.items.has(key)) {
370
383
  return this.items.get(key);
371
384
  }
372
- const snapshot = SuperJSON2.deserialize(this.snapshot);
373
- const val = snapshot[key];
385
+ const snapshot = await this.snapshotGetter(this.namespace, key);
386
+ const val = SuperJSON3.deserialize(snapshot);
374
387
  const item = new SyncItemReceiver(key, val);
375
388
  this.items.set(key, item);
376
389
  return item;
@@ -382,36 +395,15 @@ var SyncNamespaceReceiver = class {
382
395
  const item = this.items.get(key);
383
396
  if (item) {
384
397
  if (!affectedItems.has(item)) {
385
- affectedItems.set(item, { oldVal: item.toValue(), patches: [] });
398
+ affectedItems.set(item, { oldVal: item.raw, patches: [] });
386
399
  }
387
400
  item.applyPatch(patch);
388
401
  affectedItems.get(item).patches.push(patch);
389
- } else {
390
- this.applyPatchToObject(this.snapshot, patch);
391
402
  }
392
403
  }
393
404
  for (const [item, data] of affectedItems.entries()) {
394
405
  item.triggerReactivity();
395
- item.bus.emit("update", item.toValue(), data.oldVal, data.patches);
396
- }
397
- }
398
- applyPatchToObject(obj, patch) {
399
- const fullPath = [patch.key, ...patch.path];
400
- if (patch.op === "clear" || patch.op === "add") {
401
- const target = navigatePath(obj, fullPath, 0, fullPath.length);
402
- if (patch.op === "clear") {
403
- clearValue(target);
404
- } else {
405
- addValueToSet(target, patch.value);
406
- }
407
- return;
408
- }
409
- const current = navigatePath(obj, fullPath, 0, fullPath.length - 1);
410
- const lastKey = fullPath[fullPath.length - 1];
411
- if (patch.op === "set") {
412
- setValueAtPath(current, lastKey, patch.value);
413
- } else if (patch.op === "delete") {
414
- deleteValueAtPath(current, lastKey);
406
+ item.bus.emit("update", item.raw, data.oldVal, data.patches);
415
407
  }
416
408
  }
417
409
  };
@@ -420,27 +412,24 @@ var SyncItemReceiver = class {
420
412
  this.key = key;
421
413
  this.value = initialValue;
422
414
  }
415
+ bus = new Nanobus2("SyncItemReceiver");
423
416
  value;
424
417
  _ref = null;
425
418
  _shallowRef = null;
426
- bus = new Nanobus2("SyncItemReceiver");
427
- on(event, cb) {
428
- this.bus.on(event, cb);
429
- }
430
- toValue() {
419
+ get raw() {
431
420
  return this.value;
432
421
  }
433
422
  toRef() {
434
423
  if (!this._ref) {
435
- this._ref = ref(this.value);
424
+ this._ref = ref(deepStructureClone(this.value));
436
425
  }
437
- return this._ref;
426
+ return readonly(this._ref);
438
427
  }
439
428
  toShallowRef() {
440
429
  if (!this._shallowRef) {
441
430
  this._shallowRef = shallowRef(this.value);
442
431
  }
443
- return this._shallowRef;
432
+ return readonly(this._shallowRef);
444
433
  }
445
434
  applyPatch(patch) {
446
435
  if (patch.path.length === 0) {
@@ -499,10 +488,6 @@ var SyncItemReceiver = class {
499
488
  triggerRef(this._shallowRef);
500
489
  }
501
490
  }
502
- dispose() {
503
- this._ref = null;
504
- this._shallowRef = null;
505
- }
506
491
  };
507
492
  export {
508
493
  SyncItemProvider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-state-sync",
3
- "version": "1.0.3",
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": {