purrtabby 0.1.2 → 0.1.3
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 +24 -18
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.global.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +21 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<br>
|
|
6
6
|
<a href="https://www.npmjs.org/package/purrtabby"><img src="https://img.shields.io/npm/v/purrtabby.svg" alt="npm"></a>
|
|
7
7
|
<img src="https://github.com/let-sunny/purrtabby/workflows/CI/badge.svg" alt="build status">
|
|
8
|
-
<a href="https://
|
|
8
|
+
<a href="https://bundlephobia.com/package/purrtabby"><img src="https://img.shields.io/badge/gzip-2.1%20KB-blue?style=flat" alt="bundle size (gzipped)"></a>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
> Lightweight browser tab communication and leader election library using BroadcastChannel and localStorage
|
|
@@ -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
|
|
17
|
+
**Microscopic**: weighs 6.3KB minified (2.1KB gzipped)
|
|
18
18
|
|
|
19
19
|
**Reliable**: leader election with lease-based heartbeat mechanism
|
|
20
20
|
|
|
@@ -34,7 +34,7 @@ Try purrtabby in your browser with our interactive demo. Test cross-tab communic
|
|
|
34
34
|
|
|
35
35
|
## 📚 Documentation
|
|
36
36
|
|
|
37
|
-
**[Architecture Documentation →](./docs/ARCHITECTURE.md)**
|
|
37
|
+
**[Architecture Documentation →](./docs/ARCHITECTURE.md)**
|
|
38
38
|
|
|
39
39
|
Learn about purrtabby's internal architecture and design decisions.
|
|
40
40
|
|
|
@@ -67,11 +67,13 @@ Learn about purrtabby's internal architecture and design decisions.
|
|
|
67
67
|
## Installation
|
|
68
68
|
|
|
69
69
|
**npm:**
|
|
70
|
+
|
|
70
71
|
```bash
|
|
71
72
|
npm install purrtabby
|
|
72
73
|
```
|
|
73
74
|
|
|
74
75
|
**UMD build (via unpkg):**
|
|
76
|
+
|
|
75
77
|
```html
|
|
76
78
|
<script src="https://unpkg.com/purrtabby/dist/index.global.js"></script>
|
|
77
79
|
```
|
|
@@ -79,10 +81,12 @@ npm install purrtabby
|
|
|
79
81
|
### Requirements
|
|
80
82
|
|
|
81
83
|
**Runtime:**
|
|
84
|
+
|
|
82
85
|
- **Browser**: Modern browsers with BroadcastChannel and localStorage support
|
|
83
86
|
- **Node.js**: Not supported (requires browser APIs)
|
|
84
87
|
|
|
85
88
|
**Development:**
|
|
89
|
+
|
|
86
90
|
- **Node.js**: 24.13.0 (tested with this version)
|
|
87
91
|
|
|
88
92
|
## Usage
|
|
@@ -123,9 +127,9 @@ import { createLeaderElector } from 'purrtabby';
|
|
|
123
127
|
const leader = createLeaderElector({
|
|
124
128
|
key: 'my-app-leader',
|
|
125
129
|
tabId: 'tab-1', // Optional: auto-generated if not provided
|
|
126
|
-
leaseMs: 5000,
|
|
130
|
+
leaseMs: 5000, // Lease duration in milliseconds
|
|
127
131
|
heartbeatMs: 2000, // Heartbeat interval
|
|
128
|
-
jitterMs: 500,
|
|
132
|
+
jitterMs: 500, // Jitter to avoid thundering herd
|
|
129
133
|
});
|
|
130
134
|
|
|
131
135
|
// Start leader election
|
|
@@ -256,10 +260,10 @@ Creates a new TabBus instance for cross-tab communication.
|
|
|
256
260
|
|
|
257
261
|
#### Options
|
|
258
262
|
|
|
259
|
-
| Option
|
|
260
|
-
|
|
261
|
-
| `channel` | `string` | **required**
|
|
262
|
-
| `tabId`
|
|
263
|
+
| Option | Type | Default | Description |
|
|
264
|
+
| --------- | -------- | -------------- | --------------------- |
|
|
265
|
+
| `channel` | `string` | **required** | BroadcastChannel name |
|
|
266
|
+
| `tabId` | `string` | auto-generated | Unique tab identifier |
|
|
263
267
|
|
|
264
268
|
#### TabBus Methods
|
|
265
269
|
|
|
@@ -315,16 +319,17 @@ Creates a new LeaderElector instance for leader election.
|
|
|
315
319
|
|
|
316
320
|
#### Options
|
|
317
321
|
|
|
318
|
-
| Option
|
|
319
|
-
|
|
320
|
-
| `key`
|
|
321
|
-
| `tabId`
|
|
322
|
-
| `leaseMs`
|
|
323
|
-
| `heartbeatMs` | `number`
|
|
324
|
-
| `jitterMs`
|
|
325
|
-
| `buffer`
|
|
322
|
+
| Option | Type | Default | Description |
|
|
323
|
+
| ------------- | -------------- | ----------------------------------- | ------------------------------------------ |
|
|
324
|
+
| `key` | `string` | **required** | localStorage key for leader lease |
|
|
325
|
+
| `tabId` | `string` | **required** | Unique tab identifier |
|
|
326
|
+
| `leaseMs` | `number` | `5000` | Lease duration in milliseconds |
|
|
327
|
+
| `heartbeatMs` | `number` | `2000` | Heartbeat interval in milliseconds |
|
|
328
|
+
| `jitterMs` | `number` | `500` | Jitter range to avoid synchronization |
|
|
329
|
+
| `buffer` | `BufferConfig` | `{ size: 100, overflow: 'oldest' }` | Buffer configuration for stream generators |
|
|
326
330
|
|
|
327
331
|
**BufferConfig:**
|
|
332
|
+
|
|
328
333
|
- `size`: Maximum queue size (default: 100)
|
|
329
334
|
- `overflow`: Overflow policy - `'oldest'` (drop oldest), `'newest'` (drop newest), or `'error'` (throw error)
|
|
330
335
|
|
|
@@ -347,6 +352,7 @@ Returns `true` if this tab is currently the leader.
|
|
|
347
352
|
Subscribes to a specific leader event. Returns an unsubscribe function.
|
|
348
353
|
|
|
349
354
|
Events:
|
|
355
|
+
|
|
350
356
|
- `'acquire'` - This tab became the leader
|
|
351
357
|
- `'lose'` - This tab lost leadership
|
|
352
358
|
- `'change'` - Leadership changed to another tab
|
|
@@ -398,7 +404,7 @@ leader.on('lose', () => {
|
|
|
398
404
|
// Listen for messages from other tabs
|
|
399
405
|
bus.subscribe('user-action', (message) => {
|
|
400
406
|
console.log('User action from tab:', message.tabId, message.payload);
|
|
401
|
-
|
|
407
|
+
|
|
402
408
|
// If we're the leader, process the action
|
|
403
409
|
if (leader.isLeader()) {
|
|
404
410
|
console.log('Processing action as leader');
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var y=Object.defineProperty;var z=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var G=Object.prototype.hasOwnProperty;var P=(e,r)=>{for(var a in r)y(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&&y(e,s,{get:()=>r[s],enumerable:!(n=z(r,s))||n.enumerable});return e};var F=e=>J(y({},"__esModule",{value:!0}),e);var H={};P(H,{createBus:()=>E,createLeaderElector:()=>h,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 R(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*O(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=R("err",{error:"Failed to receive message"})},{publish(o,f){let L={type:o,payload:f,tabId:s,ts:Date.now()};c.postMessage(L),queueMicrotask(()=>{j(L,s,i,t)})},subscribe(o,f){return i.messageCallbacks.has(o)||i.messageCallbacks.set(o,new Set),i.messageCallbacks.get(o).add(f),()=>{let L=i.messageCallbacks.get(o);L&&(L.delete(f),L.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 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 _(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 h(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},p=[];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||_(t,p)}function f(){t.isLeader&&g(t.key)?.tabId===t.tabId&&C(t.key)}return{start(){if(t.stopped&&(t.stopped=!1),V(t,p),t.isLeader){let I=B(t.heartbeatMs,t.jitterMs);t.heartbeatTimer=setInterval(()=>{$(t,p)},I)}let d=B(t.heartbeatMs/2,t.jitterMs);t.checkTimer=setInterval(()=>{_(t,p)},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,p)),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 k=t.eventCallbacks.get(d);k&&(k.delete(I),k.size===0&&t.eventCallbacks.delete(d))}},onAll(d){return t.allCallbacks.add(d),()=>{t.allCallbacks.delete(d)}},stream(d){return A(t,{events:p},d?.signal)},getTabId(){return t.tabId}}}var q={createBus:E,createLeaderElector:h};0&&(module.exports={createBus,createLeaderElector});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** TabBus message structure */
|
|
2
|
-
interface TabBusMessage<T =
|
|
2
|
+
interface TabBusMessage<T = unknown> {
|
|
3
3
|
type: string;
|
|
4
4
|
payload?: T;
|
|
5
5
|
tabId: string;
|
|
@@ -11,7 +11,7 @@ type TabBusEventType = 'msg' | 'err';
|
|
|
11
11
|
interface TabBusEvent {
|
|
12
12
|
type: TabBusEventType;
|
|
13
13
|
ts: number;
|
|
14
|
-
meta?: Record<string,
|
|
14
|
+
meta?: Record<string, unknown>;
|
|
15
15
|
}
|
|
16
16
|
/** Buffer overflow policy */
|
|
17
17
|
type BufferOverflowPolicy = 'oldest' | 'newest' | 'error';
|
|
@@ -27,7 +27,7 @@ interface BusOptions {
|
|
|
27
27
|
buffer?: BufferConfig;
|
|
28
28
|
}
|
|
29
29
|
/** Return type of createBus(), provides async iterables and callbacks */
|
|
30
|
-
interface TabBus<T =
|
|
30
|
+
interface TabBus<T = unknown> {
|
|
31
31
|
/** Publish a message to all tabs */
|
|
32
32
|
publish(type: string, payload?: T): void;
|
|
33
33
|
/** Subscribe to messages of a specific type */
|
|
@@ -49,7 +49,7 @@ type LeaderEventType = 'acquire' | 'lose' | 'change';
|
|
|
49
49
|
interface LeaderEvent {
|
|
50
50
|
type: LeaderEventType;
|
|
51
51
|
ts: number;
|
|
52
|
-
meta?: Record<string,
|
|
52
|
+
meta?: Record<string, unknown>;
|
|
53
53
|
}
|
|
54
54
|
/** Options for createLeaderElector() */
|
|
55
55
|
interface LeaderElectorOptions {
|
|
@@ -85,7 +85,7 @@ interface LeaderElector {
|
|
|
85
85
|
* @param options - Bus configuration options
|
|
86
86
|
* @returns TabBus instance
|
|
87
87
|
*/
|
|
88
|
-
declare function createBus<T =
|
|
88
|
+
declare function createBus<T = unknown>(options: BusOptions): TabBus<T>;
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
91
|
* Create a LeaderElector instance for leader election
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** TabBus message structure */
|
|
2
|
-
interface TabBusMessage<T =
|
|
2
|
+
interface TabBusMessage<T = unknown> {
|
|
3
3
|
type: string;
|
|
4
4
|
payload?: T;
|
|
5
5
|
tabId: string;
|
|
@@ -11,7 +11,7 @@ type TabBusEventType = 'msg' | 'err';
|
|
|
11
11
|
interface TabBusEvent {
|
|
12
12
|
type: TabBusEventType;
|
|
13
13
|
ts: number;
|
|
14
|
-
meta?: Record<string,
|
|
14
|
+
meta?: Record<string, unknown>;
|
|
15
15
|
}
|
|
16
16
|
/** Buffer overflow policy */
|
|
17
17
|
type BufferOverflowPolicy = 'oldest' | 'newest' | 'error';
|
|
@@ -27,7 +27,7 @@ interface BusOptions {
|
|
|
27
27
|
buffer?: BufferConfig;
|
|
28
28
|
}
|
|
29
29
|
/** Return type of createBus(), provides async iterables and callbacks */
|
|
30
|
-
interface TabBus<T =
|
|
30
|
+
interface TabBus<T = unknown> {
|
|
31
31
|
/** Publish a message to all tabs */
|
|
32
32
|
publish(type: string, payload?: T): void;
|
|
33
33
|
/** Subscribe to messages of a specific type */
|
|
@@ -49,7 +49,7 @@ type LeaderEventType = 'acquire' | 'lose' | 'change';
|
|
|
49
49
|
interface LeaderEvent {
|
|
50
50
|
type: LeaderEventType;
|
|
51
51
|
ts: number;
|
|
52
|
-
meta?: Record<string,
|
|
52
|
+
meta?: Record<string, unknown>;
|
|
53
53
|
}
|
|
54
54
|
/** Options for createLeaderElector() */
|
|
55
55
|
interface LeaderElectorOptions {
|
|
@@ -85,7 +85,7 @@ interface LeaderElector {
|
|
|
85
85
|
* @param options - Bus configuration options
|
|
86
86
|
* @returns TabBus instance
|
|
87
87
|
*/
|
|
88
|
-
declare function createBus<T =
|
|
88
|
+
declare function createBus<T = unknown>(options: BusOptions): TabBus<T>;
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
91
|
* Create a LeaderElector instance for leader election
|
package/dist/index.global.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var purrtabby=(()=>{var
|
|
1
|
+
"use strict";var purrtabby=(()=>{var y=Object.defineProperty;var z=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var G=Object.prototype.hasOwnProperty;var P=(e,r)=>{for(var a in r)y(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&&y(e,s,{get:()=>r[s],enumerable:!(n=z(r,s))||n.enumerable});return e};var F=e=>J(y({},"__esModule",{value:!0}),e);var H={};P(H,{createBus:()=>E,createLeaderElector:()=>h,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 R(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*O(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=R("err",{error:"Failed to receive message"})},{publish(o,f){let L={type:o,payload:f,tabId:s,ts:Date.now()};c.postMessage(L),queueMicrotask(()=>{j(L,s,i,t)})},subscribe(o,f){return i.messageCallbacks.has(o)||i.messageCallbacks.set(o,new Set),i.messageCallbacks.get(o).add(f),()=>{let L=i.messageCallbacks.get(o);L&&(L.delete(f),L.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 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 _(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 h(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},p=[];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||_(t,p)}function f(){t.isLeader&&g(t.key)?.tabId===t.tabId&&C(t.key)}return{start(){if(t.stopped&&(t.stopped=!1),V(t,p),t.isLeader){let I=B(t.heartbeatMs,t.jitterMs);t.heartbeatTimer=setInterval(()=>{$(t,p)},I)}let d=B(t.heartbeatMs/2,t.jitterMs);t.checkTimer=setInterval(()=>{_(t,p)},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,p)),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 k=t.eventCallbacks.get(d);k&&(k.delete(I),k.size===0&&t.eventCallbacks.delete(d))}},onAll(d){return t.allCallbacks.add(d),()=>{t.allCallbacks.delete(d)}},stream(d){return A(t,{events:p},d?.signal)},getTabId(){return t.tabId}}}var q={createBus:E,createLeaderElector:h};return F(H);})();
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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
|
|
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 h(e,r){let a=(Math.random()*2-1)*r;return Math.max(0,e+a)}function k(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 y(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*R(e,r,a){e.activeIterators++;try{for(;!a?.aborted;){for(;r.messages.length>0&&!a?.aborted;)yield r.messages.shift();await k(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*O(e,r,a){e.activeIterators++;try{for(;!a?.aborted;){for(;r.events.length>0&&!a?.aborted;)yield r.events.shift();await k(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 _(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=>{_(o,s,i,t)},c.onmessageerror=()=>{let o=x("err",{error:"Failed to receive message"})},{publish(o,f){let L={type:o,payload:f,tabId:s,ts:Date.now()};c.postMessage(L),queueMicrotask(()=>{A(L,s,i,t)})},subscribe(o,f){return i.messageCallbacks.has(o)||i.messageCallbacks.set(o,new Set),i.messageCallbacks.get(o).add(f),()=>{let L=i.messageCallbacks.get(o);L&&(L.delete(f),L.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 z(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(y(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()};y(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},p=[];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,p)}function f(){t.isLeader&&g(t.key)?.tabId===t.tabId&&B(t.key)}return{start(){if(t.stopped&&(t.stopped=!1),z(t,p),t.isLeader){let I=h(t.heartbeatMs,t.jitterMs);t.heartbeatTimer=setInterval(()=>{D(t,p)},I)}let d=h(t.heartbeatMs/2,t.jitterMs);t.checkTimer=setInterval(()=>{j(t,p)},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,p)),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 O(t,{events:p},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,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "purrtabby",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"unpkg": "dist/index.global.js",
|
|
9
9
|
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
10
18
|
"files": [
|
|
11
19
|
"dist",
|
|
12
20
|
"README.md",
|
|
@@ -18,6 +26,10 @@
|
|
|
18
26
|
"test": "vitest run",
|
|
19
27
|
"test:watch": "vitest",
|
|
20
28
|
"test:coverage": "vitest run --coverage",
|
|
29
|
+
"lint": "eslint .",
|
|
30
|
+
"lint:fix": "eslint . --fix",
|
|
31
|
+
"format": "prettier --write \"**/*.{ts,js,json,md}\"",
|
|
32
|
+
"format:check": "prettier --check \"**/*.{ts,js,json,md}\"",
|
|
21
33
|
"prepublishOnly": "npm run build && npm test",
|
|
22
34
|
"version": "npm run build"
|
|
23
35
|
},
|
|
@@ -42,7 +54,14 @@
|
|
|
42
54
|
"vitest": "^2.0.0",
|
|
43
55
|
"@vitest/coverage-v8": "^2.0.0",
|
|
44
56
|
"@types/node": "^20.0.0",
|
|
45
|
-
"jsdom": "^24.0.0"
|
|
57
|
+
"jsdom": "^24.0.0",
|
|
58
|
+
"prettier": "^3.8.0",
|
|
59
|
+
"eslint": "^9.0.0",
|
|
60
|
+
"@eslint/js": "^9.0.0",
|
|
61
|
+
"typescript-eslint": "^8.0.0",
|
|
62
|
+
"eslint-plugin-prettier": "^5.0.0",
|
|
63
|
+
"eslint-config-prettier": "^10.0.0",
|
|
64
|
+
"globals": "^15.0.0"
|
|
46
65
|
},
|
|
47
66
|
"engines": {
|
|
48
67
|
"node": ">=18.0.0"
|