purrtabby 0.1.0 → 0.1.2
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 +83 -59
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +10 -43
- package/dist/index.d.ts +10 -43
- package/dist/index.global.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ A lightweight library for cross-tab communication and leader election in browser
|
|
|
14
14
|
|
|
15
15
|
## Highlights
|
|
16
16
|
|
|
17
|
-
**Microscopic**: weighs less than
|
|
17
|
+
**Microscopic**: weighs less than 7KB minified (~3KB gzipped)
|
|
18
18
|
|
|
19
19
|
**Reliable**: leader election with lease-based heartbeat mechanism
|
|
20
20
|
|
|
@@ -26,6 +26,18 @@ A lightweight library for cross-tab communication and leader election in browser
|
|
|
26
26
|
|
|
27
27
|
**Flexible**: callback-based or generator-based APIs, your choice
|
|
28
28
|
|
|
29
|
+
## 🎮 Try It Live
|
|
30
|
+
|
|
31
|
+
**[Interactive Demo →](https://let-sunny.github.io/purrtabby/)**
|
|
32
|
+
|
|
33
|
+
Try purrtabby in your browser with our interactive demo. Test cross-tab communication and leader election between multiple tabs.
|
|
34
|
+
|
|
35
|
+
## 📚 Documentation
|
|
36
|
+
|
|
37
|
+
**[Architecture Documentation →](./docs/ARCHITECTURE.md)**
|
|
38
|
+
|
|
39
|
+
Learn about purrtabby's internal architecture and design decisions.
|
|
40
|
+
|
|
29
41
|
## Features
|
|
30
42
|
|
|
31
43
|
- **Cross-tab communication** using BroadcastChannel
|
|
@@ -125,15 +137,15 @@ if (leader.isLeader()) {
|
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
// Listen for leader events
|
|
128
|
-
leader.on('
|
|
140
|
+
leader.on('acquire', () => {
|
|
129
141
|
console.log('Became the leader!');
|
|
130
142
|
});
|
|
131
143
|
|
|
132
|
-
leader.on('
|
|
144
|
+
leader.on('lose', (event) => {
|
|
133
145
|
console.log('Lost leadership:', event.meta?.newLeader);
|
|
134
146
|
});
|
|
135
147
|
|
|
136
|
-
leader.on('
|
|
148
|
+
leader.on('change', (event) => {
|
|
137
149
|
console.log('Leadership changed to:', event.meta?.newLeader);
|
|
138
150
|
});
|
|
139
151
|
|
|
@@ -141,11 +153,62 @@ leader.on('changed', (event) => {
|
|
|
141
153
|
leader.stop();
|
|
142
154
|
```
|
|
143
155
|
|
|
144
|
-
### Generator
|
|
156
|
+
### Choosing Between Callback and Generator APIs
|
|
157
|
+
|
|
158
|
+
purrtabby provides two ways to consume messages and events: **callbacks** and **generators**. Choose based on your needs:
|
|
159
|
+
|
|
160
|
+
- **Callbacks**: Simple, event-driven, good for one-off handlers
|
|
161
|
+
- **Generators**: Modern async/await patterns, better for complex flows, supports AbortSignal
|
|
162
|
+
|
|
163
|
+
### Callback-based API
|
|
145
164
|
|
|
146
165
|
```typescript
|
|
147
|
-
|
|
166
|
+
const bus = createBus({ channel: 'my-channel' });
|
|
167
|
+
|
|
168
|
+
// Subscribe to specific message types
|
|
169
|
+
const unsubscribeMessage = bus.subscribe('user-action', (message) => {
|
|
170
|
+
console.log('Message:', message.payload);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Subscribe to all messages
|
|
174
|
+
const unsubscribeAll = bus.subscribeAll((message) => {
|
|
175
|
+
console.log('Any message:', message.type, message.payload);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Unsubscribe when done
|
|
179
|
+
unsubscribeMessage();
|
|
180
|
+
unsubscribeAll();
|
|
181
|
+
|
|
182
|
+
const leader = createLeaderElector({
|
|
183
|
+
key: 'my-leader',
|
|
184
|
+
tabId: 'tab-1',
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
leader.start();
|
|
188
|
+
|
|
189
|
+
// Subscribe to specific leader events
|
|
190
|
+
const unsubscribeAcquired = leader.on('acquire', (event) => {
|
|
191
|
+
console.log('Became leader');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const unsubscribeLost = leader.on('lose', (event) => {
|
|
195
|
+
console.log('Lost leadership');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Subscribe to all leader events
|
|
199
|
+
const unsubscribeAllEvents = leader.onAll((event) => {
|
|
200
|
+
console.log('Leader event:', event.type);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Unsubscribe when done
|
|
204
|
+
unsubscribeAcquired();
|
|
205
|
+
unsubscribeLost();
|
|
206
|
+
unsubscribeAllEvents();
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Generator-based Streams
|
|
148
210
|
|
|
211
|
+
```typescript
|
|
149
212
|
const bus = createBus({ channel: 'my-channel' });
|
|
150
213
|
|
|
151
214
|
// Consume messages as async iterable
|
|
@@ -185,52 +248,6 @@ const controller = new AbortController();
|
|
|
185
248
|
})();
|
|
186
249
|
```
|
|
187
250
|
|
|
188
|
-
### Callback-based API
|
|
189
|
-
|
|
190
|
-
```typescript
|
|
191
|
-
const bus = createBus({ channel: 'my-channel' });
|
|
192
|
-
|
|
193
|
-
// Subscribe to messages
|
|
194
|
-
const unsubscribeMessage = bus.subscribe('type', (message) => {
|
|
195
|
-
console.log('Message:', message);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// Subscribe to all messages
|
|
199
|
-
const unsubscribeAll = bus.subscribeAll((message) => {
|
|
200
|
-
console.log('Any message:', message);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// Unsubscribe when done
|
|
204
|
-
unsubscribeMessage();
|
|
205
|
-
unsubscribeAll();
|
|
206
|
-
|
|
207
|
-
const leader = createLeaderElector({
|
|
208
|
-
key: 'my-leader',
|
|
209
|
-
tabId: 'tab-1',
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
leader.start();
|
|
213
|
-
|
|
214
|
-
// Subscribe to leader events
|
|
215
|
-
const unsubscribeAcquired = leader.on('acquired', (event) => {
|
|
216
|
-
console.log('Became leader');
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
const unsubscribeLost = leader.on('lost', (event) => {
|
|
220
|
-
console.log('Lost leadership');
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
// Subscribe to all leader events
|
|
224
|
-
const unsubscribeAll = leader.onAll((event) => {
|
|
225
|
-
console.log('Leader event:', event.type);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
// Unsubscribe when done
|
|
229
|
-
unsubscribeAcquired();
|
|
230
|
-
unsubscribeLost();
|
|
231
|
-
unsubscribeAll();
|
|
232
|
-
```
|
|
233
|
-
|
|
234
251
|
## API
|
|
235
252
|
|
|
236
253
|
### `createBus(options)`
|
|
@@ -305,6 +322,11 @@ Creates a new LeaderElector instance for leader election.
|
|
|
305
322
|
| `leaseMs` | `number` | `5000` | Lease duration in milliseconds |
|
|
306
323
|
| `heartbeatMs` | `number` | `2000` | Heartbeat interval in milliseconds |
|
|
307
324
|
| `jitterMs` | `number` | `500` | Jitter range to avoid synchronization |
|
|
325
|
+
| `buffer` | `BufferConfig` | `{ size: 100, overflow: 'oldest' }` | Buffer configuration for stream generators |
|
|
326
|
+
|
|
327
|
+
**BufferConfig:**
|
|
328
|
+
- `size`: Maximum queue size (default: 100)
|
|
329
|
+
- `overflow`: Overflow policy - `'oldest'` (drop oldest), `'newest'` (drop newest), or `'error'` (throw error)
|
|
308
330
|
|
|
309
331
|
#### LeaderElector Methods
|
|
310
332
|
|
|
@@ -325,9 +347,9 @@ Returns `true` if this tab is currently the leader.
|
|
|
325
347
|
Subscribes to a specific leader event. Returns an unsubscribe function.
|
|
326
348
|
|
|
327
349
|
Events:
|
|
328
|
-
- `'
|
|
329
|
-
- `'
|
|
330
|
-
- `'
|
|
350
|
+
- `'acquire'` - This tab became the leader
|
|
351
|
+
- `'lose'` - This tab lost leadership
|
|
352
|
+
- `'change'` - Leadership changed to another tab
|
|
331
353
|
|
|
332
354
|
##### `onAll(handler)`
|
|
333
355
|
|
|
@@ -363,13 +385,13 @@ const leader = createLeaderElector({
|
|
|
363
385
|
leader.start();
|
|
364
386
|
|
|
365
387
|
// Handle leader events
|
|
366
|
-
leader.on('
|
|
388
|
+
leader.on('acquire', () => {
|
|
367
389
|
console.log('This tab is now the leader');
|
|
368
390
|
// Only the leader performs certain tasks
|
|
369
391
|
bus.publish('leader-announcement', { tabId: leader.getTabId() });
|
|
370
392
|
});
|
|
371
393
|
|
|
372
|
-
leader.on('
|
|
394
|
+
leader.on('lose', () => {
|
|
373
395
|
console.log('This tab is no longer the leader');
|
|
374
396
|
});
|
|
375
397
|
|
|
@@ -391,7 +413,9 @@ leader.stop();
|
|
|
391
413
|
bus.close();
|
|
392
414
|
```
|
|
393
415
|
|
|
394
|
-
### Using with purrcat (WebSocket)
|
|
416
|
+
### Using with [purrcat](https://www.npmjs.com/package/purrcat) (WebSocket)
|
|
417
|
+
|
|
418
|
+
> **[📖 Full WebSocket Usage Guide →](./docs/WEBSOCKET-USAGE.md)** - Learn how to share WebSocket connections across tabs, implement request-response patterns, and handle edge cases.
|
|
395
419
|
|
|
396
420
|
```typescript
|
|
397
421
|
import createSocket from 'purrcat';
|
|
@@ -406,7 +430,7 @@ const leader = createLeaderElector({
|
|
|
406
430
|
leader.start();
|
|
407
431
|
|
|
408
432
|
// Only the leader maintains WebSocket connection
|
|
409
|
-
leader.on('
|
|
433
|
+
leader.on('acquire', () => {
|
|
410
434
|
const socket = createSocket({
|
|
411
435
|
url: 'wss://api.example.com/ws',
|
|
412
436
|
});
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var k=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var G=Object.prototype.hasOwnProperty;var P=(e,r)=>{for(var a in r)k(e,a,{get:r[a],enumerable:!0})},J=(e,r,a,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let s of D(r))!G.call(e,s)&&s!==a&&k(e,s,{get:()=>r[s],enumerable:!(n=_(r,s))||n.enumerable});return e};var F=e=>J(k({},"__esModule",{value:!0}),e);var H={};P(H,{createBus:()=>E,createLeaderElector:()=>y,default:()=>q});module.exports=F(H);function m(e,r,a){e&&e.forEach(n=>{try{n(r)}catch(s){console.error(a,s)}})}function x(){return`${Date.now()}-${Math.random().toString(36).substring(2,11)}`}function O(e,r){return{type:e,ts:Date.now(),meta:r}}function b(e,r){return{type:e,ts:Date.now(),meta:r}}function B(e,r){let a=(Math.random()*2-1)*r;return Math.max(0,e+a)}function S(e,r,a,n,s){return new Promise(l=>{if(e?.aborted){l();return}if(r()){l();return}let u=()=>{s(l),e&&e.removeEventListener("abort",c)},c=()=>{u(),l()};e&&e.addEventListener("abort",c),n(()=>{u(),l()})})}function g(e){try{let r=localStorage.getItem(e);return r?JSON.parse(r):null}catch{return null}}function M(e,r){try{localStorage.setItem(e,JSON.stringify(r))}catch(a){console.error("Error writing leader lease:",a)}}function C(e){try{localStorage.removeItem(e)}catch(r){console.error("Error removing leader lease:",r)}}function T(e){return e?Date.now()-e.timestamp<e.leaseMs:!1}function w(e,r,a,n){return r.length<n?{action:"add"}:e==="oldest"?{action:"drop_oldest",dropped:r.shift()}:e==="newest"?{action:"drop_newest",dropped:a}:{action:"error"}}async function*R(e,r,a){e.activeIterators++;try{for(;!a?.aborted;){for(;r.messages.length>0&&!a?.aborted;)yield r.messages.shift();await S(a,()=>r.messages.length>0,e.messageResolvers,n=>e.messageResolvers.add(n),n=>e.messageResolvers.delete(n))}}finally{e.activeIterators--,e.activeIterators===0&&(r.messages=[])}}async function*A(e,r,a){e.activeIterators++;try{for(;!a?.aborted;){for(;r.events.length>0&&!a?.aborted;)yield r.events.shift();await S(a,()=>r.events.length>0,e.eventResolvers,n=>e.eventResolvers.add(n),n=>e.eventResolvers.delete(n))}}finally{e.activeIterators--,e.activeIterators===0&&(r.events=[])}}function j(e,r,a,n){let s=a.messageCallbacks.get(e.type);if(m(s,e,"Error in TabBus callback:"),m(a.allCallbacks,e,"Error in TabBus all callback:"),a.activeIterators===0)return;let l=w(a.bufferOverflow,n,e,a.bufferSize);if(l.action==="error"){console.error("Message buffer overflow");return}l.action!=="drop_newest"&&(l.action,n.push(e),a.messageResolvers.forEach(u=>u()),a.messageResolvers.clear())}function N(e,r,a,n){try{let s=e.data;j(s,r,a,n)}catch(s){console.error("Error handling TabBus message:",s)}}function E(e){let{channel:r,tabId:a,buffer:n}=e,s=a||x(),l=n?.size??100,u=n?.overflow??"oldest";if(typeof BroadcastChannel>"u")throw new Error("BroadcastChannel is not supported in this environment");let c=new BroadcastChannel(r),i={channel:c,tabId:s,messageCallbacks:new Map,allCallbacks:new Set,messageResolvers:new Set,activeIterators:0,bufferSize:l,bufferOverflow:u},t=[];return c.onmessage=o=>{N(o,s,i,t)},c.onmessageerror=()=>{let o=O("err",{error:"Failed to receive message"})},{publish(o,f){let p={type:o,payload:f,tabId:s,ts:Date.now()};c.postMessage(p),queueMicrotask(()=>{j(p,s,i,t)})},subscribe(o,f){return i.messageCallbacks.has(o)||i.messageCallbacks.set(o,new Set),i.messageCallbacks.get(o).add(f),()=>{let p=i.messageCallbacks.get(o);p&&(p.delete(f),p.size===0&&i.messageCallbacks.delete(o))}},subscribeAll(o){return i.allCallbacks.add(o),()=>{i.allCallbacks.delete(o)}},stream(o){return R(i,{messages:t},o?.signal)},getTabId(){return s},close(){c.close(),i.channel=null,i.messageCallbacks.clear(),i.allCallbacks.clear(),i.messageResolvers.clear()}}}function v(e,r,a){let n=r.eventCallbacks.get(e.type);if(m(n,e,"Error in LeaderElector callback:"),m(r.allCallbacks,e,"Error in LeaderElector all callback:"),r.activeIterators===0)return;let s=w(r.bufferOverflow,a,e,r.bufferSize);if(s.action==="error"){console.error("Event buffer overflow");return}s.action!=="drop_newest"&&(s.action,a.push(e),r.eventResolvers.forEach(l=>l()),r.eventResolvers.clear())}function V(e,r){if(e.stopped)return!1;let a=g(e.key);if(!T(a)){let n={tabId:e.tabId,timestamp:Date.now(),leaseMs:e.leaseMs};if(M(e.key,n),g(e.key)?.tabId===e.tabId)return e.isLeader||(e.isLeader=!0,v(b("acquire",{tabId:e.tabId}),e,r)),!0}return a?.tabId===e.tabId&&T(a)?(e.isLeader||(e.isLeader=!0,v(b("acquire",{tabId:e.tabId}),e,r)),!0):(e.isLeader&&(e.isLeader=!1,v(b("lose",{tabId:e.tabId,newLeader:a?.tabId}),e,r)),!1)}function $(e,r){if(e.stopped||!e.isLeader)return;let a=g(e.key);if(a?.tabId===e.tabId){let n={...a,timestamp:Date.now()};M(e.key,n)}else e.isLeader&&(e.isLeader=!1,v(b("lose",{tabId:e.tabId}),e,r))}function z(e,r){if(e.stopped)return;let a=g(e.key),n=e.isLeader,s=a?.tabId===e.tabId&&T(a);!n&&s?(e.isLeader=!0,v(b("acquire",{tabId:e.tabId}),e,r)):n&&!s?(e.isLeader=!1,v(b("lose",{tabId:e.tabId,newLeader:a?.tabId}),e,r)):n&&s&&a?.tabId!==e.tabId&&v(b("change",{tabId:e.tabId,newLeader:a.tabId}),e,r)}function y(e){let{key:r,tabId:a,leaseMs:n=5e3,heartbeatMs:s=2e3,jitterMs:l=500,buffer:u}=e,c=u?.size??100,i=u?.overflow??"oldest";if(typeof localStorage>"u")throw new Error("localStorage is not supported in this environment");let t={key:r,tabId:a,leaseMs:n,heartbeatMs:s,jitterMs:l,isLeader:!1,heartbeatTimer:null,checkTimer:null,eventCallbacks:new Map,allCallbacks:new Set,eventResolvers:new Set,activeIterators:0,stopped:!1,bufferSize:c,bufferOverflow:i},L=[];typeof window<"u"&&(window.addEventListener("storage",o),window.addEventListener("pagehide",f),window.addEventListener("beforeunload",f));function o(d){d.key!==t.key||d.storageArea!==localStorage||z(t,L)}function f(){t.isLeader&&g(t.key)?.tabId===t.tabId&&C(t.key)}return{start(){if(t.stopped&&(t.stopped=!1),V(t,L),t.isLeader){let I=B(t.heartbeatMs,t.jitterMs);t.heartbeatTimer=setInterval(()=>{$(t,L)},I)}let d=B(t.heartbeatMs/2,t.jitterMs);t.checkTimer=setInterval(()=>{z(t,L)},d)},stop(){t.stopped=!0,t.heartbeatTimer&&(clearInterval(t.heartbeatTimer),t.heartbeatTimer=null),t.checkTimer&&(clearInterval(t.checkTimer),t.checkTimer=null),t.isLeader&&(g(t.key)?.tabId===t.tabId&&C(t.key),t.isLeader=!1,v(b("lose",{tabId:t.tabId,reason:"stopped"}),t,L)),typeof window<"u"&&(window.removeEventListener("storage",o),window.removeEventListener("pagehide",f),window.removeEventListener("beforeunload",f))},isLeader(){return t.isLeader},on(d,I){return t.eventCallbacks.has(d)||t.eventCallbacks.set(d,new Set),t.eventCallbacks.get(d).add(I),()=>{let h=t.eventCallbacks.get(d);h&&(h.delete(I),h.size===0&&t.eventCallbacks.delete(d))}},onAll(d){return t.allCallbacks.add(d),()=>{t.allCallbacks.delete(d)}},stream(d){return A(t,{events:L},d?.signal)},getTabId(){return t.tabId}}}var q={createBus:E,createLeaderElector:y};0&&(module.exports={createBus,createLeaderElector});
|
package/dist/index.d.cts
CHANGED
|
@@ -13,10 +13,18 @@ interface TabBusEvent {
|
|
|
13
13
|
ts: number;
|
|
14
14
|
meta?: Record<string, any>;
|
|
15
15
|
}
|
|
16
|
+
/** Buffer overflow policy */
|
|
17
|
+
type BufferOverflowPolicy = 'oldest' | 'newest' | 'error';
|
|
18
|
+
/** Buffer configuration */
|
|
19
|
+
interface BufferConfig {
|
|
20
|
+
size?: number;
|
|
21
|
+
overflow?: BufferOverflowPolicy;
|
|
22
|
+
}
|
|
16
23
|
/** Options for createBus() */
|
|
17
24
|
interface BusOptions {
|
|
18
25
|
channel: string;
|
|
19
26
|
tabId?: string;
|
|
27
|
+
buffer?: BufferConfig;
|
|
20
28
|
}
|
|
21
29
|
/** Return type of createBus(), provides async iterables and callbacks */
|
|
22
30
|
interface TabBus<T = any> {
|
|
@@ -50,6 +58,7 @@ interface LeaderElectorOptions {
|
|
|
50
58
|
leaseMs?: number;
|
|
51
59
|
heartbeatMs?: number;
|
|
52
60
|
jitterMs?: number;
|
|
61
|
+
buffer?: BufferConfig;
|
|
53
62
|
}
|
|
54
63
|
/** Return type of createLeaderElector() */
|
|
55
64
|
interface LeaderElector {
|
|
@@ -70,40 +79,6 @@ interface LeaderElector {
|
|
|
70
79
|
/** Get the current tab ID */
|
|
71
80
|
getTabId(): string;
|
|
72
81
|
}
|
|
73
|
-
/** RPC request message */
|
|
74
|
-
interface RPCRequest<T = any> {
|
|
75
|
-
type: 'rpc-request';
|
|
76
|
-
method: string;
|
|
77
|
-
params?: T;
|
|
78
|
-
requestId: string;
|
|
79
|
-
tabId: string;
|
|
80
|
-
ts: number;
|
|
81
|
-
}
|
|
82
|
-
/** RPC response message */
|
|
83
|
-
interface RPCResponse<T = any> {
|
|
84
|
-
type: 'rpc-response';
|
|
85
|
-
requestId: string;
|
|
86
|
-
result?: T;
|
|
87
|
-
error?: string;
|
|
88
|
-
tabId: string;
|
|
89
|
-
ts: number;
|
|
90
|
-
}
|
|
91
|
-
/** Options for createRPC() */
|
|
92
|
-
interface RPCOptions {
|
|
93
|
-
bus: TabBus;
|
|
94
|
-
timeout?: number;
|
|
95
|
-
}
|
|
96
|
-
/** Return type of createRPC() */
|
|
97
|
-
interface RPC {
|
|
98
|
-
/** Call a method on the leader (or any tab) */
|
|
99
|
-
call<TParams = any, TResult = any>(method: string, params?: TParams, options?: {
|
|
100
|
-
timeout?: number;
|
|
101
|
-
}): Promise<TResult>;
|
|
102
|
-
/** Handle incoming RPC requests */
|
|
103
|
-
handle<TParams = any, TResult = any>(method: string, handler: (params?: TParams) => Promise<TResult> | TResult): () => void;
|
|
104
|
-
/** Close the RPC instance and cleanup */
|
|
105
|
-
close(): void;
|
|
106
|
-
}
|
|
107
82
|
|
|
108
83
|
/**
|
|
109
84
|
* Create a TabBus instance for cross-tab communication
|
|
@@ -119,17 +94,9 @@ declare function createBus<T = any>(options: BusOptions): TabBus<T>;
|
|
|
119
94
|
*/
|
|
120
95
|
declare function createLeaderElector(options: LeaderElectorOptions): LeaderElector;
|
|
121
96
|
|
|
122
|
-
/**
|
|
123
|
-
* Create an RPC instance for request-response communication
|
|
124
|
-
* @param options - RPC configuration options
|
|
125
|
-
* @returns RPC instance
|
|
126
|
-
*/
|
|
127
|
-
declare function createRPC(options: RPCOptions): RPC;
|
|
128
|
-
|
|
129
97
|
declare const _default: {
|
|
130
98
|
createBus: typeof createBus;
|
|
131
99
|
createLeaderElector: typeof createLeaderElector;
|
|
132
|
-
createRPC: typeof createRPC;
|
|
133
100
|
};
|
|
134
101
|
|
|
135
|
-
export { type BusOptions, createBus as CreateBus, createLeaderElector as CreateLeaderElector,
|
|
102
|
+
export { type BusOptions, createBus as CreateBus, createLeaderElector as CreateLeaderElector, type LeaderElector, type LeaderElectorOptions, type LeaderEvent, type LeaderEventType, type TabBus, type TabBusEvent, type TabBusEventType, type TabBusMessage, createBus, createLeaderElector, _default as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -13,10 +13,18 @@ interface TabBusEvent {
|
|
|
13
13
|
ts: number;
|
|
14
14
|
meta?: Record<string, any>;
|
|
15
15
|
}
|
|
16
|
+
/** Buffer overflow policy */
|
|
17
|
+
type BufferOverflowPolicy = 'oldest' | 'newest' | 'error';
|
|
18
|
+
/** Buffer configuration */
|
|
19
|
+
interface BufferConfig {
|
|
20
|
+
size?: number;
|
|
21
|
+
overflow?: BufferOverflowPolicy;
|
|
22
|
+
}
|
|
16
23
|
/** Options for createBus() */
|
|
17
24
|
interface BusOptions {
|
|
18
25
|
channel: string;
|
|
19
26
|
tabId?: string;
|
|
27
|
+
buffer?: BufferConfig;
|
|
20
28
|
}
|
|
21
29
|
/** Return type of createBus(), provides async iterables and callbacks */
|
|
22
30
|
interface TabBus<T = any> {
|
|
@@ -50,6 +58,7 @@ interface LeaderElectorOptions {
|
|
|
50
58
|
leaseMs?: number;
|
|
51
59
|
heartbeatMs?: number;
|
|
52
60
|
jitterMs?: number;
|
|
61
|
+
buffer?: BufferConfig;
|
|
53
62
|
}
|
|
54
63
|
/** Return type of createLeaderElector() */
|
|
55
64
|
interface LeaderElector {
|
|
@@ -70,40 +79,6 @@ interface LeaderElector {
|
|
|
70
79
|
/** Get the current tab ID */
|
|
71
80
|
getTabId(): string;
|
|
72
81
|
}
|
|
73
|
-
/** RPC request message */
|
|
74
|
-
interface RPCRequest<T = any> {
|
|
75
|
-
type: 'rpc-request';
|
|
76
|
-
method: string;
|
|
77
|
-
params?: T;
|
|
78
|
-
requestId: string;
|
|
79
|
-
tabId: string;
|
|
80
|
-
ts: number;
|
|
81
|
-
}
|
|
82
|
-
/** RPC response message */
|
|
83
|
-
interface RPCResponse<T = any> {
|
|
84
|
-
type: 'rpc-response';
|
|
85
|
-
requestId: string;
|
|
86
|
-
result?: T;
|
|
87
|
-
error?: string;
|
|
88
|
-
tabId: string;
|
|
89
|
-
ts: number;
|
|
90
|
-
}
|
|
91
|
-
/** Options for createRPC() */
|
|
92
|
-
interface RPCOptions {
|
|
93
|
-
bus: TabBus;
|
|
94
|
-
timeout?: number;
|
|
95
|
-
}
|
|
96
|
-
/** Return type of createRPC() */
|
|
97
|
-
interface RPC {
|
|
98
|
-
/** Call a method on the leader (or any tab) */
|
|
99
|
-
call<TParams = any, TResult = any>(method: string, params?: TParams, options?: {
|
|
100
|
-
timeout?: number;
|
|
101
|
-
}): Promise<TResult>;
|
|
102
|
-
/** Handle incoming RPC requests */
|
|
103
|
-
handle<TParams = any, TResult = any>(method: string, handler: (params?: TParams) => Promise<TResult> | TResult): () => void;
|
|
104
|
-
/** Close the RPC instance and cleanup */
|
|
105
|
-
close(): void;
|
|
106
|
-
}
|
|
107
82
|
|
|
108
83
|
/**
|
|
109
84
|
* Create a TabBus instance for cross-tab communication
|
|
@@ -119,17 +94,9 @@ declare function createBus<T = any>(options: BusOptions): TabBus<T>;
|
|
|
119
94
|
*/
|
|
120
95
|
declare function createLeaderElector(options: LeaderElectorOptions): LeaderElector;
|
|
121
96
|
|
|
122
|
-
/**
|
|
123
|
-
* Create an RPC instance for request-response communication
|
|
124
|
-
* @param options - RPC configuration options
|
|
125
|
-
* @returns RPC instance
|
|
126
|
-
*/
|
|
127
|
-
declare function createRPC(options: RPCOptions): RPC;
|
|
128
|
-
|
|
129
97
|
declare const _default: {
|
|
130
98
|
createBus: typeof createBus;
|
|
131
99
|
createLeaderElector: typeof createLeaderElector;
|
|
132
|
-
createRPC: typeof createRPC;
|
|
133
100
|
};
|
|
134
101
|
|
|
135
|
-
export { type BusOptions, createBus as CreateBus, createLeaderElector as CreateLeaderElector,
|
|
102
|
+
export { type BusOptions, createBus as CreateBus, createLeaderElector as CreateLeaderElector, type LeaderElector, type LeaderElectorOptions, type LeaderEvent, type LeaderEventType, type TabBus, type TabBusEvent, type TabBusEventType, type TabBusMessage, createBus, createLeaderElector, _default as default };
|
package/dist/index.global.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var purrtabby=(()=>{var
|
|
1
|
+
"use strict";var purrtabby=(()=>{var k=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var G=Object.prototype.hasOwnProperty;var P=(e,r)=>{for(var a in r)k(e,a,{get:r[a],enumerable:!0})},J=(e,r,a,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let s of D(r))!G.call(e,s)&&s!==a&&k(e,s,{get:()=>r[s],enumerable:!(n=_(r,s))||n.enumerable});return e};var F=e=>J(k({},"__esModule",{value:!0}),e);var H={};P(H,{createBus:()=>E,createLeaderElector:()=>y,default:()=>q});function m(e,r,a){e&&e.forEach(n=>{try{n(r)}catch(s){console.error(a,s)}})}function x(){return`${Date.now()}-${Math.random().toString(36).substring(2,11)}`}function O(e,r){return{type:e,ts:Date.now(),meta:r}}function b(e,r){return{type:e,ts:Date.now(),meta:r}}function B(e,r){let a=(Math.random()*2-1)*r;return Math.max(0,e+a)}function S(e,r,a,n,s){return new Promise(l=>{if(e?.aborted){l();return}if(r()){l();return}let u=()=>{s(l),e&&e.removeEventListener("abort",c)},c=()=>{u(),l()};e&&e.addEventListener("abort",c),n(()=>{u(),l()})})}function g(e){try{let r=localStorage.getItem(e);return r?JSON.parse(r):null}catch{return null}}function M(e,r){try{localStorage.setItem(e,JSON.stringify(r))}catch(a){console.error("Error writing leader lease:",a)}}function C(e){try{localStorage.removeItem(e)}catch(r){console.error("Error removing leader lease:",r)}}function T(e){return e?Date.now()-e.timestamp<e.leaseMs:!1}function w(e,r,a,n){return r.length<n?{action:"add"}:e==="oldest"?{action:"drop_oldest",dropped:r.shift()}:e==="newest"?{action:"drop_newest",dropped:a}:{action:"error"}}async function*R(e,r,a){e.activeIterators++;try{for(;!a?.aborted;){for(;r.messages.length>0&&!a?.aborted;)yield r.messages.shift();await S(a,()=>r.messages.length>0,e.messageResolvers,n=>e.messageResolvers.add(n),n=>e.messageResolvers.delete(n))}}finally{e.activeIterators--,e.activeIterators===0&&(r.messages=[])}}async function*A(e,r,a){e.activeIterators++;try{for(;!a?.aborted;){for(;r.events.length>0&&!a?.aborted;)yield r.events.shift();await S(a,()=>r.events.length>0,e.eventResolvers,n=>e.eventResolvers.add(n),n=>e.eventResolvers.delete(n))}}finally{e.activeIterators--,e.activeIterators===0&&(r.events=[])}}function j(e,r,a,n){let s=a.messageCallbacks.get(e.type);if(m(s,e,"Error in TabBus callback:"),m(a.allCallbacks,e,"Error in TabBus all callback:"),a.activeIterators===0)return;let l=w(a.bufferOverflow,n,e,a.bufferSize);if(l.action==="error"){console.error("Message buffer overflow");return}l.action!=="drop_newest"&&(l.action,n.push(e),a.messageResolvers.forEach(u=>u()),a.messageResolvers.clear())}function N(e,r,a,n){try{let s=e.data;j(s,r,a,n)}catch(s){console.error("Error handling TabBus message:",s)}}function E(e){let{channel:r,tabId:a,buffer:n}=e,s=a||x(),l=n?.size??100,u=n?.overflow??"oldest";if(typeof BroadcastChannel>"u")throw new Error("BroadcastChannel is not supported in this environment");let c=new BroadcastChannel(r),i={channel:c,tabId:s,messageCallbacks:new Map,allCallbacks:new Set,messageResolvers:new Set,activeIterators:0,bufferSize:l,bufferOverflow:u},t=[];return c.onmessage=o=>{N(o,s,i,t)},c.onmessageerror=()=>{let o=O("err",{error:"Failed to receive message"})},{publish(o,f){let p={type:o,payload:f,tabId:s,ts:Date.now()};c.postMessage(p),queueMicrotask(()=>{j(p,s,i,t)})},subscribe(o,f){return i.messageCallbacks.has(o)||i.messageCallbacks.set(o,new Set),i.messageCallbacks.get(o).add(f),()=>{let p=i.messageCallbacks.get(o);p&&(p.delete(f),p.size===0&&i.messageCallbacks.delete(o))}},subscribeAll(o){return i.allCallbacks.add(o),()=>{i.allCallbacks.delete(o)}},stream(o){return R(i,{messages:t},o?.signal)},getTabId(){return s},close(){c.close(),i.channel=null,i.messageCallbacks.clear(),i.allCallbacks.clear(),i.messageResolvers.clear()}}}function v(e,r,a){let n=r.eventCallbacks.get(e.type);if(m(n,e,"Error in LeaderElector callback:"),m(r.allCallbacks,e,"Error in LeaderElector all callback:"),r.activeIterators===0)return;let s=w(r.bufferOverflow,a,e,r.bufferSize);if(s.action==="error"){console.error("Event buffer overflow");return}s.action!=="drop_newest"&&(s.action,a.push(e),r.eventResolvers.forEach(l=>l()),r.eventResolvers.clear())}function V(e,r){if(e.stopped)return!1;let a=g(e.key);if(!T(a)){let n={tabId:e.tabId,timestamp:Date.now(),leaseMs:e.leaseMs};if(M(e.key,n),g(e.key)?.tabId===e.tabId)return e.isLeader||(e.isLeader=!0,v(b("acquire",{tabId:e.tabId}),e,r)),!0}return a?.tabId===e.tabId&&T(a)?(e.isLeader||(e.isLeader=!0,v(b("acquire",{tabId:e.tabId}),e,r)),!0):(e.isLeader&&(e.isLeader=!1,v(b("lose",{tabId:e.tabId,newLeader:a?.tabId}),e,r)),!1)}function $(e,r){if(e.stopped||!e.isLeader)return;let a=g(e.key);if(a?.tabId===e.tabId){let n={...a,timestamp:Date.now()};M(e.key,n)}else e.isLeader&&(e.isLeader=!1,v(b("lose",{tabId:e.tabId}),e,r))}function z(e,r){if(e.stopped)return;let a=g(e.key),n=e.isLeader,s=a?.tabId===e.tabId&&T(a);!n&&s?(e.isLeader=!0,v(b("acquire",{tabId:e.tabId}),e,r)):n&&!s?(e.isLeader=!1,v(b("lose",{tabId:e.tabId,newLeader:a?.tabId}),e,r)):n&&s&&a?.tabId!==e.tabId&&v(b("change",{tabId:e.tabId,newLeader:a.tabId}),e,r)}function y(e){let{key:r,tabId:a,leaseMs:n=5e3,heartbeatMs:s=2e3,jitterMs:l=500,buffer:u}=e,c=u?.size??100,i=u?.overflow??"oldest";if(typeof localStorage>"u")throw new Error("localStorage is not supported in this environment");let t={key:r,tabId:a,leaseMs:n,heartbeatMs:s,jitterMs:l,isLeader:!1,heartbeatTimer:null,checkTimer:null,eventCallbacks:new Map,allCallbacks:new Set,eventResolvers:new Set,activeIterators:0,stopped:!1,bufferSize:c,bufferOverflow:i},L=[];typeof window<"u"&&(window.addEventListener("storage",o),window.addEventListener("pagehide",f),window.addEventListener("beforeunload",f));function o(d){d.key!==t.key||d.storageArea!==localStorage||z(t,L)}function f(){t.isLeader&&g(t.key)?.tabId===t.tabId&&C(t.key)}return{start(){if(t.stopped&&(t.stopped=!1),V(t,L),t.isLeader){let I=B(t.heartbeatMs,t.jitterMs);t.heartbeatTimer=setInterval(()=>{$(t,L)},I)}let d=B(t.heartbeatMs/2,t.jitterMs);t.checkTimer=setInterval(()=>{z(t,L)},d)},stop(){t.stopped=!0,t.heartbeatTimer&&(clearInterval(t.heartbeatTimer),t.heartbeatTimer=null),t.checkTimer&&(clearInterval(t.checkTimer),t.checkTimer=null),t.isLeader&&(g(t.key)?.tabId===t.tabId&&C(t.key),t.isLeader=!1,v(b("lose",{tabId:t.tabId,reason:"stopped"}),t,L)),typeof window<"u"&&(window.removeEventListener("storage",o),window.removeEventListener("pagehide",f),window.removeEventListener("beforeunload",f))},isLeader(){return t.isLeader},on(d,I){return t.eventCallbacks.has(d)||t.eventCallbacks.set(d,new Set),t.eventCallbacks.get(d).add(I),()=>{let h=t.eventCallbacks.get(d);h&&(h.delete(I),h.size===0&&t.eventCallbacks.delete(d))}},onAll(d){return t.allCallbacks.add(d),()=>{t.allCallbacks.delete(d)}},stream(d){return A(t,{events:L},d?.signal)},getTabId(){return t.tabId}}}var q={createBus:E,createLeaderElector:y};return F(H);})();
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function
|
|
1
|
+
function m(e,r,a){e&&e.forEach(n=>{try{n(r)}catch(s){console.error(a,s)}})}function C(){return`${Date.now()}-${Math.random().toString(36).substring(2,11)}`}function x(e,r){return{type:e,ts:Date.now(),meta:r}}function b(e,r){return{type:e,ts:Date.now(),meta:r}}function y(e,r){let a=(Math.random()*2-1)*r;return Math.max(0,e+a)}function h(e,r,a,n,s){return new Promise(l=>{if(e?.aborted){l();return}if(r()){l();return}let u=()=>{s(l),e&&e.removeEventListener("abort",c)},c=()=>{u(),l()};e&&e.addEventListener("abort",c),n(()=>{u(),l()})})}function g(e){try{let r=localStorage.getItem(e);return r?JSON.parse(r):null}catch{return null}}function k(e,r){try{localStorage.setItem(e,JSON.stringify(r))}catch(a){console.error("Error writing leader lease:",a)}}function B(e){try{localStorage.removeItem(e)}catch(r){console.error("Error removing leader lease:",r)}}function T(e){return e?Date.now()-e.timestamp<e.leaseMs:!1}function w(e,r,a,n){return r.length<n?{action:"add"}:e==="oldest"?{action:"drop_oldest",dropped:r.shift()}:e==="newest"?{action:"drop_newest",dropped:a}:{action:"error"}}async function*O(e,r,a){e.activeIterators++;try{for(;!a?.aborted;){for(;r.messages.length>0&&!a?.aborted;)yield r.messages.shift();await h(a,()=>r.messages.length>0,e.messageResolvers,n=>e.messageResolvers.add(n),n=>e.messageResolvers.delete(n))}}finally{e.activeIterators--,e.activeIterators===0&&(r.messages=[])}}async function*R(e,r,a){e.activeIterators++;try{for(;!a?.aborted;){for(;r.events.length>0&&!a?.aborted;)yield r.events.shift();await h(a,()=>r.events.length>0,e.eventResolvers,n=>e.eventResolvers.add(n),n=>e.eventResolvers.delete(n))}}finally{e.activeIterators--,e.activeIterators===0&&(r.events=[])}}function A(e,r,a,n){let s=a.messageCallbacks.get(e.type);if(m(s,e,"Error in TabBus callback:"),m(a.allCallbacks,e,"Error in TabBus all callback:"),a.activeIterators===0)return;let l=w(a.bufferOverflow,n,e,a.bufferSize);if(l.action==="error"){console.error("Message buffer overflow");return}l.action!=="drop_newest"&&(l.action,n.push(e),a.messageResolvers.forEach(u=>u()),a.messageResolvers.clear())}function z(e,r,a,n){try{let s=e.data;A(s,r,a,n)}catch(s){console.error("Error handling TabBus message:",s)}}function S(e){let{channel:r,tabId:a,buffer:n}=e,s=a||C(),l=n?.size??100,u=n?.overflow??"oldest";if(typeof BroadcastChannel>"u")throw new Error("BroadcastChannel is not supported in this environment");let c=new BroadcastChannel(r),i={channel:c,tabId:s,messageCallbacks:new Map,allCallbacks:new Set,messageResolvers:new Set,activeIterators:0,bufferSize:l,bufferOverflow:u},t=[];return c.onmessage=o=>{z(o,s,i,t)},c.onmessageerror=()=>{let o=x("err",{error:"Failed to receive message"})},{publish(o,f){let p={type:o,payload:f,tabId:s,ts:Date.now()};c.postMessage(p),queueMicrotask(()=>{A(p,s,i,t)})},subscribe(o,f){return i.messageCallbacks.has(o)||i.messageCallbacks.set(o,new Set),i.messageCallbacks.get(o).add(f),()=>{let p=i.messageCallbacks.get(o);p&&(p.delete(f),p.size===0&&i.messageCallbacks.delete(o))}},subscribeAll(o){return i.allCallbacks.add(o),()=>{i.allCallbacks.delete(o)}},stream(o){return O(i,{messages:t},o?.signal)},getTabId(){return s},close(){c.close(),i.channel=null,i.messageCallbacks.clear(),i.allCallbacks.clear(),i.messageResolvers.clear()}}}function v(e,r,a){let n=r.eventCallbacks.get(e.type);if(m(n,e,"Error in LeaderElector callback:"),m(r.allCallbacks,e,"Error in LeaderElector all callback:"),r.activeIterators===0)return;let s=w(r.bufferOverflow,a,e,r.bufferSize);if(s.action==="error"){console.error("Event buffer overflow");return}s.action!=="drop_newest"&&(s.action,a.push(e),r.eventResolvers.forEach(l=>l()),r.eventResolvers.clear())}function _(e,r){if(e.stopped)return!1;let a=g(e.key);if(!T(a)){let n={tabId:e.tabId,timestamp:Date.now(),leaseMs:e.leaseMs};if(k(e.key,n),g(e.key)?.tabId===e.tabId)return e.isLeader||(e.isLeader=!0,v(b("acquire",{tabId:e.tabId}),e,r)),!0}return a?.tabId===e.tabId&&T(a)?(e.isLeader||(e.isLeader=!0,v(b("acquire",{tabId:e.tabId}),e,r)),!0):(e.isLeader&&(e.isLeader=!1,v(b("lose",{tabId:e.tabId,newLeader:a?.tabId}),e,r)),!1)}function D(e,r){if(e.stopped||!e.isLeader)return;let a=g(e.key);if(a?.tabId===e.tabId){let n={...a,timestamp:Date.now()};k(e.key,n)}else e.isLeader&&(e.isLeader=!1,v(b("lose",{tabId:e.tabId}),e,r))}function j(e,r){if(e.stopped)return;let a=g(e.key),n=e.isLeader,s=a?.tabId===e.tabId&&T(a);!n&&s?(e.isLeader=!0,v(b("acquire",{tabId:e.tabId}),e,r)):n&&!s?(e.isLeader=!1,v(b("lose",{tabId:e.tabId,newLeader:a?.tabId}),e,r)):n&&s&&a?.tabId!==e.tabId&&v(b("change",{tabId:e.tabId,newLeader:a.tabId}),e,r)}function M(e){let{key:r,tabId:a,leaseMs:n=5e3,heartbeatMs:s=2e3,jitterMs:l=500,buffer:u}=e,c=u?.size??100,i=u?.overflow??"oldest";if(typeof localStorage>"u")throw new Error("localStorage is not supported in this environment");let t={key:r,tabId:a,leaseMs:n,heartbeatMs:s,jitterMs:l,isLeader:!1,heartbeatTimer:null,checkTimer:null,eventCallbacks:new Map,allCallbacks:new Set,eventResolvers:new Set,activeIterators:0,stopped:!1,bufferSize:c,bufferOverflow:i},L=[];typeof window<"u"&&(window.addEventListener("storage",o),window.addEventListener("pagehide",f),window.addEventListener("beforeunload",f));function o(d){d.key!==t.key||d.storageArea!==localStorage||j(t,L)}function f(){t.isLeader&&g(t.key)?.tabId===t.tabId&&B(t.key)}return{start(){if(t.stopped&&(t.stopped=!1),_(t,L),t.isLeader){let I=y(t.heartbeatMs,t.jitterMs);t.heartbeatTimer=setInterval(()=>{D(t,L)},I)}let d=y(t.heartbeatMs/2,t.jitterMs);t.checkTimer=setInterval(()=>{j(t,L)},d)},stop(){t.stopped=!0,t.heartbeatTimer&&(clearInterval(t.heartbeatTimer),t.heartbeatTimer=null),t.checkTimer&&(clearInterval(t.checkTimer),t.checkTimer=null),t.isLeader&&(g(t.key)?.tabId===t.tabId&&B(t.key),t.isLeader=!1,v(b("lose",{tabId:t.tabId,reason:"stopped"}),t,L)),typeof window<"u"&&(window.removeEventListener("storage",o),window.removeEventListener("pagehide",f),window.removeEventListener("beforeunload",f))},isLeader(){return t.isLeader},on(d,I){return t.eventCallbacks.has(d)||t.eventCallbacks.set(d,new Set),t.eventCallbacks.get(d).add(I),()=>{let E=t.eventCallbacks.get(d);E&&(E.delete(I),E.size===0&&t.eventCallbacks.delete(d))}},onAll(d){return t.allCallbacks.add(d),()=>{t.allCallbacks.delete(d)}},stream(d){return R(t,{events:L},d?.signal)},getTabId(){return t.tabId}}}var Q={createBus:S,createLeaderElector:M};export{S as createBus,M as createLeaderElector,Q as default};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "purrtabby",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Lightweight browser tab communication and leader election library using BroadcastChannel and localStorage",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
"inter-tab",
|
|
31
31
|
"cross-tab",
|
|
32
32
|
"pub-sub",
|
|
33
|
-
"rpc",
|
|
34
33
|
"typescript",
|
|
35
34
|
"async-iterable",
|
|
36
35
|
"generator"
|