@yiin/reactive-proxy-state 1.0.22 → 1.0.23

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/dist/index.cjs CHANGED
@@ -34856,6 +34856,9 @@ __export(exports_src, {
34856
34856
  isComputed: () => isComputed,
34857
34857
  deepEqual: () => deepEqual,
34858
34858
  deepClone: () => deepClone,
34859
+ createRendererBridgeEmitter: () => createRendererBridgeEmitter,
34860
+ createMainBridgeEmitter: () => createMainBridgeEmitter,
34861
+ createBridgeEmitter: () => createBridgeEmitter,
34859
34862
  computed: () => computed,
34860
34863
  cleanupEffect: () => cleanupEffect,
34861
34864
  activeEffect: () => activeEffect
@@ -36487,3 +36490,94 @@ function trackVueReactiveEvents(vueState, emit, options = {}) {
36487
36490
  }
36488
36491
  return stop;
36489
36492
  }
36493
+ // src/integrations/electron-bridge.ts
36494
+ function makeTx() {
36495
+ try {
36496
+ const g = globalThis;
36497
+ if (g.crypto && typeof g.crypto.randomUUID === "function") {
36498
+ return g.crypto.randomUUID();
36499
+ }
36500
+ } catch {}
36501
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
36502
+ }
36503
+ function createBridgeEmitter(opts) {
36504
+ const { id, apply, send, onMessage, forward, seenLimit = 1000 } = opts;
36505
+ let muteCount = 0;
36506
+ const mute = (fn) => {
36507
+ muteCount++;
36508
+ try {
36509
+ return fn();
36510
+ } finally {
36511
+ muteCount--;
36512
+ }
36513
+ };
36514
+ const seen = new Set;
36515
+ const order = [];
36516
+ const markSeen = (tx) => {
36517
+ if (seen.has(tx))
36518
+ return;
36519
+ seen.add(tx);
36520
+ order.push(tx);
36521
+ if (order.length > seenLimit) {
36522
+ const oldest = order.shift();
36523
+ seen.delete(oldest);
36524
+ }
36525
+ };
36526
+ const emit = (event) => {
36527
+ if (muteCount > 0)
36528
+ return;
36529
+ const msg = { tx: makeTx(), origin: id, event };
36530
+ markSeen(msg.tx);
36531
+ send(msg);
36532
+ };
36533
+ const unsubscribe = onMessage((msg, ctx) => {
36534
+ if (!msg || !msg.tx)
36535
+ return;
36536
+ if (seen.has(msg.tx))
36537
+ return;
36538
+ markSeen(msg.tx);
36539
+ if (msg.origin === id)
36540
+ return;
36541
+ mute(() => apply(msg.event));
36542
+ if (forward)
36543
+ forward(msg, ctx);
36544
+ });
36545
+ const stop = () => unsubscribe();
36546
+ return { emit, stop, mute };
36547
+ }
36548
+ function createRendererBridgeEmitter(opts) {
36549
+ const { id, channel, ipcRenderer, apply, seenLimit } = opts;
36550
+ return createBridgeEmitter({
36551
+ id,
36552
+ apply,
36553
+ seenLimit,
36554
+ send: (msg) => ipcRenderer.send(channel, msg),
36555
+ onMessage: (cb) => {
36556
+ const handler = (_e, msg) => cb(msg);
36557
+ ipcRenderer.on(channel, handler);
36558
+ return () => ipcRenderer.off(channel, handler);
36559
+ }
36560
+ });
36561
+ }
36562
+ function createMainBridgeEmitter(opts) {
36563
+ const { id = "main", channel, ipcMain, windows, apply, seenLimit } = opts;
36564
+ const broadcast = (msg, exceptId) => {
36565
+ for (const w of windows()) {
36566
+ if (exceptId != null && w.webContents.id === exceptId)
36567
+ continue;
36568
+ w.webContents.send(channel, msg);
36569
+ }
36570
+ };
36571
+ return createBridgeEmitter({
36572
+ id,
36573
+ apply,
36574
+ seenLimit,
36575
+ send: (msg) => broadcast(msg),
36576
+ onMessage: (cb) => {
36577
+ const handler = (e, msg) => cb(msg, { senderId: e?.sender?.id });
36578
+ ipcMain.on(channel, handler);
36579
+ return () => ipcMain.off(channel, handler);
36580
+ },
36581
+ forward: (msg, ctx) => broadcast(msg, ctx?.senderId)
36582
+ });
36583
+ }
package/dist/index.d.ts CHANGED
@@ -8,3 +8,4 @@ export * from './ref';
8
8
  export * from './computed';
9
9
  export * from './mark-raw';
10
10
  export * from './integrations/vue3';
11
+ export * from './integrations/electron-bridge';
package/dist/index.js CHANGED
@@ -36431,6 +36431,97 @@ function trackVueReactiveEvents(vueState, emit, options = {}) {
36431
36431
  }
36432
36432
  return stop;
36433
36433
  }
36434
+ // src/integrations/electron-bridge.ts
36435
+ function makeTx() {
36436
+ try {
36437
+ const g = globalThis;
36438
+ if (g.crypto && typeof g.crypto.randomUUID === "function") {
36439
+ return g.crypto.randomUUID();
36440
+ }
36441
+ } catch {}
36442
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
36443
+ }
36444
+ function createBridgeEmitter(opts) {
36445
+ const { id, apply, send, onMessage, forward, seenLimit = 1000 } = opts;
36446
+ let muteCount = 0;
36447
+ const mute = (fn) => {
36448
+ muteCount++;
36449
+ try {
36450
+ return fn();
36451
+ } finally {
36452
+ muteCount--;
36453
+ }
36454
+ };
36455
+ const seen = new Set;
36456
+ const order = [];
36457
+ const markSeen = (tx) => {
36458
+ if (seen.has(tx))
36459
+ return;
36460
+ seen.add(tx);
36461
+ order.push(tx);
36462
+ if (order.length > seenLimit) {
36463
+ const oldest = order.shift();
36464
+ seen.delete(oldest);
36465
+ }
36466
+ };
36467
+ const emit = (event) => {
36468
+ if (muteCount > 0)
36469
+ return;
36470
+ const msg = { tx: makeTx(), origin: id, event };
36471
+ markSeen(msg.tx);
36472
+ send(msg);
36473
+ };
36474
+ const unsubscribe = onMessage((msg, ctx) => {
36475
+ if (!msg || !msg.tx)
36476
+ return;
36477
+ if (seen.has(msg.tx))
36478
+ return;
36479
+ markSeen(msg.tx);
36480
+ if (msg.origin === id)
36481
+ return;
36482
+ mute(() => apply(msg.event));
36483
+ if (forward)
36484
+ forward(msg, ctx);
36485
+ });
36486
+ const stop = () => unsubscribe();
36487
+ return { emit, stop, mute };
36488
+ }
36489
+ function createRendererBridgeEmitter(opts) {
36490
+ const { id, channel, ipcRenderer, apply, seenLimit } = opts;
36491
+ return createBridgeEmitter({
36492
+ id,
36493
+ apply,
36494
+ seenLimit,
36495
+ send: (msg) => ipcRenderer.send(channel, msg),
36496
+ onMessage: (cb) => {
36497
+ const handler = (_e, msg) => cb(msg);
36498
+ ipcRenderer.on(channel, handler);
36499
+ return () => ipcRenderer.off(channel, handler);
36500
+ }
36501
+ });
36502
+ }
36503
+ function createMainBridgeEmitter(opts) {
36504
+ const { id = "main", channel, ipcMain, windows, apply, seenLimit } = opts;
36505
+ const broadcast = (msg, exceptId) => {
36506
+ for (const w of windows()) {
36507
+ if (exceptId != null && w.webContents.id === exceptId)
36508
+ continue;
36509
+ w.webContents.send(channel, msg);
36510
+ }
36511
+ };
36512
+ return createBridgeEmitter({
36513
+ id,
36514
+ apply,
36515
+ seenLimit,
36516
+ send: (msg) => broadcast(msg),
36517
+ onMessage: (cb) => {
36518
+ const handler = (e, msg) => cb(msg, { senderId: e?.sender?.id });
36519
+ ipcMain.on(channel, handler);
36520
+ return () => ipcMain.off(channel, handler);
36521
+ },
36522
+ forward: (msg, ctx) => broadcast(msg, ctx?.senderId)
36523
+ });
36524
+ }
36434
36525
  export {
36435
36526
  watchEffect,
36436
36527
  watch,
@@ -36454,6 +36545,9 @@ export {
36454
36545
  isComputed,
36455
36546
  deepEqual,
36456
36547
  deepClone,
36548
+ createRendererBridgeEmitter,
36549
+ createMainBridgeEmitter,
36550
+ createBridgeEmitter,
36457
36551
  computed,
36458
36552
  cleanupEffect,
36459
36553
  activeEffect
@@ -0,0 +1,71 @@
1
+ import type { StateEvent } from "../types";
2
+ export type BridgeMessage = {
3
+ tx: string;
4
+ origin: string;
5
+ event: StateEvent;
6
+ };
7
+ type OnMessage = (cb: (msg: BridgeMessage, ctx?: any) => void) => () => void;
8
+ type Send = (msg: BridgeMessage, ctx?: any) => void;
9
+ type Forward = (msg: BridgeMessage, ctx?: any) => void;
10
+ /**
11
+ * Create a loop-safe bridge emitter for bi-directional sync.
12
+ * - Tags every message with tx + origin
13
+ * - Mutes emit while applying remote updates
14
+ * - Dedupe by tx using a small LRU
15
+ */
16
+ export declare function createBridgeEmitter(opts: {
17
+ id: string;
18
+ apply: (event: StateEvent) => void;
19
+ send: Send;
20
+ onMessage: OnMessage;
21
+ forward?: Forward;
22
+ seenLimit?: number;
23
+ }): {
24
+ emit: (event: StateEvent) => void;
25
+ stop: () => void;
26
+ mute: <T>(fn: () => T) => T;
27
+ };
28
+ /**
29
+ * Renderer-side bridge bound to Electron's ipcRenderer.
30
+ * Keep this generic by accepting a minimal ipcRenderer-like object.
31
+ */
32
+ export declare function createRendererBridgeEmitter(opts: {
33
+ id: string;
34
+ channel: string;
35
+ ipcRenderer: {
36
+ send: (channel: string, msg: any) => void;
37
+ on: (channel: string, handler: (event: any, msg: any) => void) => void;
38
+ off: (channel: string, handler: (event: any, msg: any) => void) => void;
39
+ };
40
+ apply: (event: StateEvent) => void;
41
+ seenLimit?: number;
42
+ }): {
43
+ emit: (event: StateEvent) => void;
44
+ stop: () => void;
45
+ mute: <T>(fn: () => T) => T;
46
+ };
47
+ /**
48
+ * Main-process bridge bound to Electron's ipcMain and BrowserWindows.
49
+ * Accepts a provider for windows to avoid importing Electron types.
50
+ */
51
+ export declare function createMainBridgeEmitter(opts: {
52
+ id?: string;
53
+ channel: string;
54
+ ipcMain: {
55
+ on: (channel: string, handler: (event: any, msg: any) => void) => void;
56
+ off: (channel: string, handler: (event: any, msg: any) => void) => void;
57
+ };
58
+ windows: () => {
59
+ webContents: {
60
+ id: number;
61
+ send: (channel: string, msg: any) => void;
62
+ };
63
+ }[];
64
+ apply: (event: StateEvent) => void;
65
+ seenLimit?: number;
66
+ }): {
67
+ emit: (event: StateEvent) => void;
68
+ stop: () => void;
69
+ mute: <T>(fn: () => T) => T;
70
+ };
71
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yiin/reactive-proxy-state",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
4
4
  "author": "Yiin <stanislovas@yiin.lt>",
5
5
  "repository": {
6
6
  "type": "git",