tab-bridge 0.1.0 → 0.2.0

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
@@ -17,25 +17,16 @@
17
17
  [![npm version](https://img.shields.io/npm/v/tab-bridge?style=for-the-badge&color=cb3837&label=npm&logo=npm&logoColor=white)](https://www.npmjs.com/package/tab-bridge)
18
18
  [![bundle size](https://img.shields.io/bundlephobia/minzip/tab-bridge?style=for-the-badge&color=6ead0a&label=size&logo=webpack&logoColor=white)](https://bundlephobia.com/package/tab-bridge)
19
19
  [![TypeScript](https://img.shields.io/badge/TypeScript-first-3178c6?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org)
20
- [![license](https://img.shields.io/github/license/serbi2012/tab-sync?style=for-the-badge&color=blue&logo=open-source-initiative&logoColor=white)](./LICENSE)
21
- [![GitHub stars](https://img.shields.io/github/stars/serbi2012/tab-sync?style=for-the-badge&color=yellow&logo=github&logoColor=white)](https://github.com/serbi2012/tab-sync)
20
+ [![license](https://img.shields.io/github/license/serbi2012/tab-bridge?style=for-the-badge&color=blue&logo=open-source-initiative&logoColor=white)](./LICENSE)
21
+ [![GitHub stars](https://img.shields.io/github/stars/serbi2012/tab-bridge?style=for-the-badge&color=yellow&logo=github&logoColor=white)](https://github.com/serbi2012/tab-bridge)
22
22
 
23
23
  <br />
24
24
 
25
- ```mermaid
26
- graph LR
27
- A["<b>Tab A</b><br/>👑 Leader"] <-->|"realtime sync"| B["<b>Tab B</b><br/>Follower"]
28
- B <-->|"realtime sync"| C["<b>Tab C</b><br/>Follower"]
29
- A <-->|"realtime sync"| C
30
-
31
- style A fill:#4f46e5,stroke:#4338ca,color:#fff,stroke-width:2px
32
- style B fill:#6366f1,stroke:#4f46e5,color:#fff,stroke-width:2px
33
- style C fill:#6366f1,stroke:#4f46e5,color:#fff,stroke-width:2px
34
- ```
25
+ <img src="https://mermaid.ink/img/Z3JhcGggTFIKICAgIEFbIlRhYiBBXG7wn5GRIExlYWRlciJdIDwtLT58InJlYWx0aW1lIHN5bmMifCBCWyJUYWIgQlxuRm9sbG93ZXIiXQogICAgQiA8LS0-fCJyZWFsdGltZSBzeW5jInwgQ1siVGFiIENcbkZvbGxvd2VyIl0KICAgIEEgPC0tPnwicmVhbHRpbWUgc3luYyJ8IEMKICAgIHN0eWxlIEEgZmlsbDojNGY0NmU1LHN0cm9rZTojNDMzOGNhLGNvbG9yOiNmZmYsc3Ryb2tlLXdpZHRoOjJweAogICAgc3R5bGUgQiBmaWxsOiM2MzY2ZjEsc3Ryb2tlOiM0ZjQ2ZTUsY29sb3I6I2ZmZixzdHJva2Utd2lkdGg6MnB4CiAgICBzdHlsZSBDIGZpbGw6IzYzNjZmMSxzdHJva2U6IzRmNDZlNSxjb2xvcjojZmZmLHN0cm9rZS13aWR0aDoycHg?theme=dark&bgColor=0d1117" alt="tab-bridge sync diagram" />
35
26
 
36
27
  <br />
37
28
 
38
- [**Getting Started**](#-getting-started) · [**API**](#-api-reference) · [**React**](#%EF%B8%8F-react) · [**Architecture**](#-architecture) · [**Examples**](#-examples)
29
+ [**Getting Started**](#-getting-started) · [**API**](#-api-reference) · [**React**](#%EF%B8%8F-react) · [**Architecture**](#-architecture) · [**Examples**](#-examples) · [**Live Demo**](https://serbi2012.github.io/tab-bridge/)
39
30
 
40
31
  </div>
41
32
 
@@ -68,23 +59,23 @@ LWW conflict resolution with batched broadcasts and custom merge strategies
68
59
  Bully algorithm with heartbeat monitoring and automatic failover
69
60
 
70
61
  #### 📡 Cross-Tab RPC
71
- Fully typed arguments, Promise-based calls with timeout handling
62
+ Fully typed arguments, Promise-based calls with `callAll` broadcast support
72
63
 
73
- #### ⚛️ React Hooks
74
- Built on `useSyncExternalStore` for zero-tear concurrent rendering
64
+ #### 🔄 Atomic Transactions
65
+ `transaction()` for safe multi-key updates with abort support
75
66
 
76
67
  </td>
77
68
  <td width="50%" valign="top">
78
69
 
70
+ #### ⚛️ React Hooks
71
+ 7 hooks built on `useSyncExternalStore` — zero-tear concurrent rendering
72
+
79
73
  #### 🛡️ Middleware Pipeline
80
74
  Intercept, validate, and transform state changes before they're applied
81
75
 
82
76
  #### 💾 State Persistence
83
77
  Survive page reloads with key whitelisting and custom storage backends
84
78
 
85
- #### 🔒 End-to-End Type Safety
86
- Discriminated unions, full type inference, and generic constraints
87
-
88
79
  #### 📦 Zero Dependencies
89
80
  Native browser APIs only, ~4KB gzipped, fully tree-shakable
90
81
 
@@ -184,6 +175,12 @@ sync.get('theme') // Read single key
184
175
  sync.getAll() // Read full state (stable reference)
185
176
  sync.set('theme', 'dark') // Write single key → broadcasts to all tabs
186
177
  sync.patch({ theme: 'dark', count: 5 }) // Write multiple keys in one broadcast
178
+
179
+ // Atomic multi-key update — return null to abort
180
+ sync.transaction((state) => {
181
+ if (state.count >= 100) return null; // abort
182
+ return { count: state.count + 1, lastUpdated: Date.now() };
183
+ });
187
184
  ```
188
185
 
189
186
  </details>
@@ -205,6 +202,13 @@ sync.select(
205
202
  (state) => state.items.filter(i => i.done).length,
206
203
  (doneCount) => updateBadge(doneCount),
207
204
  );
205
+
206
+ // Debounced derived state — callback fires at most once per 200ms
207
+ sync.select(
208
+ (state) => state.items.length,
209
+ (count) => analytics.track('item_count', count),
210
+ { debounce: 200 },
211
+ );
208
212
  ```
209
213
 
210
214
  </details>
@@ -257,6 +261,10 @@ sync.handle('getServerTime', () => ({
257
261
 
258
262
  const { iso } = await sync.call('leader', 'getServerTime');
259
263
  const result = await sync.call(tabId, 'compute', payload, 10_000);
264
+
265
+ // Broadcast RPC to ALL other tabs and collect responses
266
+ const results = await sync.callAll('getStatus');
267
+ // results: Array<{ tabId: string; result?: T; error?: string }>
260
268
  ```
261
269
 
262
270
  </details>
@@ -328,19 +336,7 @@ const sync = createTabSync({
328
336
  });
329
337
  ```
330
338
 
331
- ```mermaid
332
- graph LR
333
- A["set('age', -5)"] --> B{Middleware<br/>Pipeline}
334
- B -->|"age < 0 → reject"| C["❌ Blocked"]
335
- D["set('name', ' Alice ')"] --> B
336
- B -->|"trim()"| E["✅ 'Alice'"]
337
-
338
- style A fill:#f59e0b,stroke:#d97706,color:#fff
339
- style D fill:#f59e0b,stroke:#d97706,color:#fff
340
- style B fill:#6366f1,stroke:#4f46e5,color:#fff
341
- style C fill:#ef4444,stroke:#dc2626,color:#fff
342
- style E fill:#22c55e,stroke:#16a34a,color:#fff
343
- ```
339
+ <img src="https://mermaid.ink/img/Z3JhcGggTFIKICAgIEFbInNldChhZ2UsIC01KSJdIC0tPiBCe01pZGRsZXdhcmUgUGlwZWxpbmV9CiAgICBCIC0tPnwiYWdlIDwgMCByZWplY3QifCBDWyJCbG9ja2VkIl0KICAgIERbInNldChuYW1lLCBBbGljZSkiXSAtLT4gQgogICAgQiAtLT58InRyaW0ifCBFWyJBbGljZSJdCiAgICBzdHlsZSBBIGZpbGw6I2Y1OWUwYixzdHJva2U6I2Q5NzcwNixjb2xvcjojZmZmCiAgICBzdHlsZSBEIGZpbGw6I2Y1OWUwYixzdHJva2U6I2Q5NzcwNixjb2xvcjojZmZmCiAgICBzdHlsZSBCIGZpbGw6IzYzNjZmMSxzdHJva2U6IzRmNDZlNSxjb2xvcjojZmZmCiAgICBzdHlsZSBDIGZpbGw6I2VmNDQ0NCxzdHJva2U6I2RjMjYyNixjb2xvcjojZmZmCiAgICBzdHlsZSBFIGZpbGw6IzIyYzU1ZSxzdHJva2U6IzE2YTM0YSxjb2xvcjojZmZm?theme=dark&bgColor=0d1117" alt="middleware pipeline diagram" />
344
340
 
345
341
  <br />
346
342
 
@@ -380,7 +376,8 @@ First-class React integration built on `useSyncExternalStore` for **zero-tear co
380
376
 
381
377
  ```tsx
382
378
  import {
383
- TabSyncProvider, useTabSync, useTabSyncValue, useTabSyncSelector, useIsLeader,
379
+ TabSyncProvider, useTabSync, useTabSyncValue, useTabSyncSelector,
380
+ useIsLeader, useTabs, useLeaderInfo, useTabSyncActions,
384
381
  } from 'tab-bridge/react';
385
382
  ```
386
383
 
@@ -465,6 +462,51 @@ function LeaderIndicator() {
465
462
 
466
463
  </details>
467
464
 
465
+ <details open>
466
+ <summary><b><code>useTabs()</code> — Active tab list</b></summary>
467
+
468
+ <br />
469
+
470
+ ```tsx
471
+ function TabList() {
472
+ const tabs = useTabs();
473
+ return <p>{tabs.length} tab(s) open</p>;
474
+ }
475
+ ```
476
+
477
+ </details>
478
+
479
+ <details open>
480
+ <summary><b><code>useLeaderInfo()</code> — Leader tab info</b></summary>
481
+
482
+ <br />
483
+
484
+ ```tsx
485
+ function LeaderDisplay() {
486
+ const leader = useLeaderInfo();
487
+ if (!leader) return <p>No leader yet</p>;
488
+ return <p>Leader: {leader.id}</p>;
489
+ }
490
+ ```
491
+
492
+ </details>
493
+
494
+ <details open>
495
+ <summary><b><code>useTabSyncActions()</code> — Write-only (no re-renders)</b></summary>
496
+
497
+ <br />
498
+
499
+ ```tsx
500
+ function IncrementButton() {
501
+ const { set, patch, transaction } = useTabSyncActions<MyState>();
502
+ return <button onClick={() => set('count', prev => prev + 1)}>+1</button>;
503
+ }
504
+ ```
505
+
506
+ Components using only `useTabSyncActions` **never re-render** due to state changes — perfect for write-only controls.
507
+
508
+ </details>
509
+
468
510
  <br />
469
511
 
470
512
  ---
@@ -505,42 +547,7 @@ createTabSync({ onError: (err) => Sentry.captureException(err) });
505
547
 
506
548
  <div align="center">
507
549
 
508
- ```mermaid
509
- graph TB
510
- subgraph API["🔌 Public API — createTabSync()"]
511
- SM["📊 State Manager<br/><i>get / set / patch / subscribe</i>"]
512
- LE["👑 Leader Election<br/><i>heartbeat / failover / resign</i>"]
513
- RPC["📡 RPC Handler<br/><i>call / handle / timeout</i>"]
514
- end
515
-
516
- subgraph CORE["⚙️ Core Layer"]
517
- TR["📋 Tab Registry<br/><i>tracks all active tabs</i>"]
518
- MW["🛡️ Middleware Pipeline<br/><i>intercept → validate → transform → apply</i>"]
519
- end
520
-
521
- subgraph TRANSPORT["📡 Transport Layer — auto-detect"]
522
- BC["⚡ BroadcastChannel<br/><i>primary, fast</i>"]
523
- LS["💾 localStorage<br/><i>fallback</i>"]
524
- end
525
-
526
- SM --> TR
527
- LE --> TR
528
- RPC --> TR
529
- TR --> MW
530
- MW --> BC
531
- MW --> LS
532
-
533
- style API fill:#4f46e5,stroke:#4338ca,color:#fff,stroke-width:2px
534
- style CORE fill:#7c3aed,stroke:#6d28d9,color:#fff,stroke-width:2px
535
- style TRANSPORT fill:#2563eb,stroke:#1d4ed8,color:#fff,stroke-width:2px
536
- style SM fill:#6366f1,stroke:#4f46e5,color:#fff
537
- style LE fill:#6366f1,stroke:#4f46e5,color:#fff
538
- style RPC fill:#6366f1,stroke:#4f46e5,color:#fff
539
- style TR fill:#8b5cf6,stroke:#7c3aed,color:#fff
540
- style MW fill:#8b5cf6,stroke:#7c3aed,color:#fff
541
- style BC fill:#3b82f6,stroke:#2563eb,color:#fff
542
- style LS fill:#3b82f6,stroke:#2563eb,color:#fff
543
- ```
550
+ <img src="https://mermaid.ink/img/Z3JhcGggVEIKICAgIHN1YmdyYXBoIEFQSVsiUHVibGljIEFQSSAtIGNyZWF0ZVRhYlN5bmMiXQogICAgICAgIFNNWyJTdGF0ZSBNYW5hZ2VyIl0KICAgICAgICBMRVsiTGVhZGVyIEVsZWN0aW9uIl0KICAgICAgICBSUENbIlJQQyBIYW5kbGVyIl0KICAgIGVuZAogICAgc3ViZ3JhcGggQ09SRVsiQ29yZSBMYXllciJdCiAgICAgICAgVFJbIlRhYiBSZWdpc3RyeSJdCiAgICAgICAgTVdbIk1pZGRsZXdhcmUgUGlwZWxpbmUiXQogICAgZW5kCiAgICBzdWJncmFwaCBUUkFOU1BPUlRbIlRyYW5zcG9ydCBMYXllciJdCiAgICAgICAgQkNbIkJyb2FkY2FzdENoYW5uZWwiXQogICAgICAgIExTWyJsb2NhbFN0b3JhZ2UiXQogICAgZW5kCiAgICBTTSAtLT4gVFIKICAgIExFIC0tPiBUUgogICAgUlBDIC0tPiBUUgogICAgVFIgLS0-IE1XCiAgICBNVyAtLT4gQkMKICAgIE1XIC0tPiBMUwogICAgc3R5bGUgQVBJIGZpbGw6IzRmNDZlNSxzdHJva2U6IzQzMzhjYSxjb2xvcjojZmZmLHN0cm9rZS13aWR0aDoycHgKICAgIHN0eWxlIENPUkUgZmlsbDojN2MzYWVkLHN0cm9rZTojNmQyOGQ5LGNvbG9yOiNmZmYsc3Ryb2tlLXdpZHRoOjJweAogICAgc3R5bGUgVFJBTlNQT1JUIGZpbGw6IzI1NjNlYixzdHJva2U6IzFkNGVkOCxjb2xvcjojZmZmLHN0cm9rZS13aWR0aDoycHg?theme=dark&bgColor=0d1117" alt="architecture diagram" />
544
551
 
545
552
  </div>
546
553
 
@@ -548,40 +555,11 @@ graph TB
548
555
 
549
556
  ### How State Sync Works
550
557
 
551
- ```mermaid
552
- sequenceDiagram
553
- participant A as Tab A (Leader 👑)
554
- participant BC as BroadcastChannel
555
- participant B as Tab B
556
- participant C as Tab C
557
-
558
- A->>A: set('theme', 'dark')
559
- Note over A: Local state updated instantly
560
- A->>BC: STATE_UPDATE { theme: 'dark' }
561
- BC-->>B:
562
- BC-->>C:
563
- B->>B: Apply state + notify subscribers
564
- C->>C: Apply state + notify subscribers
565
- ```
558
+ <img src="https://mermaid.ink/img/c2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBBIGFzIFRhYiBBIExlYWRlcgogICAgcGFydGljaXBhbnQgQkMgYXMgQnJvYWRjYXN0Q2hhbm5lbAogICAgcGFydGljaXBhbnQgQiBhcyBUYWIgQgogICAgcGFydGljaXBhbnQgQyBhcyBUYWIgQwogICAgQS0-PkE6IHNldCB0aGVtZSBkYXJrCiAgICBOb3RlIG92ZXIgQTogTG9jYWwgc3RhdGUgdXBkYXRlZAogICAgQS0-PkJDOiBTVEFURV9VUERBVEUKICAgIEJDLS0-PkI6IG1lc3NhZ2UKICAgIEJDLS0-PkM6IG1lc3NhZ2UKICAgIEItPj5COiBBcHBseSArIG5vdGlmeQogICAgQy0-PkM6IEFwcGx5ICsgbm90aWZ5?theme=dark&bgColor=0d1117" alt="state sync sequence diagram" />
566
559
 
567
560
  ### How Leader Election Works
568
561
 
569
- ```mermaid
570
- sequenceDiagram
571
- participant A as Tab A (oldest)
572
- participant B as Tab B
573
- participant C as Tab C (newest)
574
-
575
- Note over A,C: Leader (Tab A) closes...
576
- A--xB: ❌ Heartbeat stops
577
- B->>B: 3 missed heartbeats → leader dead
578
- B->>C: LEADER_CLAIM
579
- C->>C: Tab B is older → yield
580
- Note over B: Waits 300ms for higher-priority claims
581
- B->>C: LEADER_ACK
582
- Note over B: 👑 Tab B is now leader
583
- B->>C: LEADER_HEARTBEAT (every 2s)
584
- ```
562
+ <img src="https://mermaid.ink/img/c2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBBIGFzIFRhYiBBIG9sZGVzdAogICAgcGFydGljaXBhbnQgQiBhcyBUYWIgQgogICAgcGFydGljaXBhbnQgQyBhcyBUYWIgQyBuZXdlc3QKICAgIE5vdGUgb3ZlciBBLEM6IExlYWRlciBUYWIgQSBjbG9zZXMKICAgIEItPj5COiAzIG1pc3NlZCBoZWFydGJlYXRzCiAgICBCLT4-QzogTEVBREVSX0NMQUlNCiAgICBDLT4-QzogVGFiIEIgaXMgb2xkZXIgeWllbGQKICAgIE5vdGUgb3ZlciBCOiBXYWl0IDMwMG1zCiAgICBCLT4-QzogTEVBREVSX0FDSwogICAgTm90ZSBvdmVyIEI6IFRhYiBCIGlzIG5vdyBsZWFkZXIKICAgIEItPj5DOiBMRUFERVJfSEVBUlRCRUFU?theme=dark&bgColor=0d1117" alt="leader election sequence diagram" />
585
563
 
586
564
  <br />
587
565
 
@@ -692,17 +670,7 @@ function logout() {
692
670
 
693
671
  <br />
694
672
 
695
- ```mermaid
696
- graph LR
697
- Server["🖥️ Server"] <-->|WebSocket| A["Tab A<br/>👑 Leader"]
698
- A -->|"state sync"| B["Tab B"]
699
- A -->|"state sync"| C["Tab C"]
700
-
701
- style Server fill:#059669,stroke:#047857,color:#fff
702
- style A fill:#4f46e5,stroke:#4338ca,color:#fff
703
- style B fill:#6366f1,stroke:#4f46e5,color:#fff
704
- style C fill:#6366f1,stroke:#4f46e5,color:#fff
705
- ```
673
+ <img src="https://mermaid.ink/img/Z3JhcGggTFIKICAgIFNlcnZlclsiU2VydmVyIl0gPC0tPnxXZWJTb2NrZXR8IEFbIlRhYiBBIExlYWRlciJdCiAgICBBIC0tPnxzdGF0ZSBzeW5jfCBCWyJUYWIgQiJdCiAgICBBIC0tPnxzdGF0ZSBzeW5jfCBDWyJUYWIgQyJdCiAgICBzdHlsZSBTZXJ2ZXIgZmlsbDojMDU5NjY5LHN0cm9rZTojMDQ3ODU3LGNvbG9yOiNmZmYKICAgIHN0eWxlIEEgZmlsbDojNGY0NmU1LHN0cm9rZTojNDMzOGNhLGNvbG9yOiNmZmYKICAgIHN0eWxlIEIgZmlsbDojNjM2NmYxLHN0cm9rZTojNGY0NmU1LGNvbG9yOiNmZmYKICAgIHN0eWxlIEMgZmlsbDojNjM2NmYxLHN0cm9rZTojNGY0NmU1LGNvbG9yOiNmZmY?theme=dark&bgColor=0d1117" alt="websocket leader pattern diagram" />
706
674
 
707
675
  ```ts
708
676
  const sync = createTabSync({
@@ -820,7 +788,7 @@ MIT © [serbi2012](https://github.com/serbi2012)
820
788
 
821
789
  <br />
822
790
 
823
- <a href="https://github.com/serbi2012/tab-sync">
791
+ <a href="https://github.com/serbi2012/tab-bridge">
824
792
  <img src="https://img.shields.io/badge/GitHub-tab--bridge-4f46e5?style=for-the-badge&logo=github&logoColor=white" alt="GitHub" />
825
793
  </a>
826
794
  &nbsp;