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 +80 -112
- package/dist/{chunk-42VOZR6E.js → chunk-4JDWAUYM.js} +216 -93
- package/dist/{chunk-BQCNBNBT.cjs → chunk-TGEXRVAL.cjs} +219 -92
- package/dist/index.cjs +41 -25
- package/dist/index.d.cts +110 -5
- package/dist/index.d.ts +110 -5
- package/dist/index.js +2 -2
- package/dist/{types-BtK4ixKz.d.cts → instance-5LIItazN.d.cts} +58 -80
- package/dist/{types-BtK4ixKz.d.ts → instance-5LIItazN.d.ts} +58 -80
- package/dist/react/index.cjs +82 -9
- package/dist/react/index.d.cts +47 -2
- package/dist/react/index.d.ts +47 -2
- package/dist/react/index.js +79 -9
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -17,25 +17,16 @@
|
|
|
17
17
|
[](https://www.npmjs.com/package/tab-bridge)
|
|
18
18
|
[](https://bundlephobia.com/package/tab-bridge)
|
|
19
19
|
[](https://www.typescriptlang.org)
|
|
20
|
-
[](./LICENSE)
|
|
21
|
+
[](https://github.com/serbi2012/tab-bridge)
|
|
22
22
|
|
|
23
23
|
<br />
|
|
24
24
|
|
|
25
|
-
|
|
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
|
|
62
|
+
Fully typed arguments, Promise-based calls with `callAll` broadcast support
|
|
72
63
|
|
|
73
|
-
####
|
|
74
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
|