tabus-js 0.1.3 → 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 +23 -0
- package/dist/index.d.mts +19 -2
- package/dist/index.d.ts +19 -2
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -259,6 +259,29 @@ Closes the channel, emits `tab:leave`, and removes all handlers. Safe to call mu
|
|
|
259
259
|
|
|
260
260
|
A unique UUID identifying this tab instance. Read-only.
|
|
261
261
|
|
|
262
|
+
## Throttle
|
|
263
|
+
|
|
264
|
+
Limit how often messages are sent to the channel. Useful for high-frequency
|
|
265
|
+
events like `mousemove`, `scroll`, or real-time input sync.
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
// Allow at most one message every 16ms (~60fps)
|
|
269
|
+
const bus = new Tabus('canvas', { throttle: 16 })
|
|
270
|
+
|
|
271
|
+
window.addEventListener('mousemove', (e) => {
|
|
272
|
+
bus.emit('cursor:moved', { x: e.clientX, y: e.clientY })
|
|
273
|
+
// Without throttle: 60 messages/sec per tab
|
|
274
|
+
// With throttle: 1 message every 16ms, rest discarded
|
|
275
|
+
})
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Without throttle (default), every `emit()` call sends immediately.
|
|
279
|
+
With throttle, calls that arrive faster than `throttleMs` are silently discarded.
|
|
280
|
+
|
|
281
|
+
| Option | Type | Default | Description |
|
|
282
|
+
|--------|------|---------|-------------|
|
|
283
|
+
| `throttle` | `number` | `0` | Minimum ms between emitted messages. `0` = no throttle. |
|
|
284
|
+
|
|
262
285
|
## Lifecycle events
|
|
263
286
|
|
|
264
287
|
```ts
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
type Handler<T = unknown> = (payload: T) => void;
|
|
2
|
+
interface TabusOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Minimum time in milliseconds between emitted messages.
|
|
5
|
+
* When set, emit() calls that arrive faster than this interval
|
|
6
|
+
* are discarded. Useful for high-frequency events like mousemove
|
|
7
|
+
* or scroll to prevent flooding the main thread.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // Allow at most one message every 16ms (~60fps)
|
|
11
|
+
* const bus = new Tabus('canvas', { throttle: 16 })
|
|
12
|
+
*/
|
|
13
|
+
throttle?: number;
|
|
14
|
+
}
|
|
2
15
|
type EventMap = Record<string, unknown>;
|
|
3
16
|
interface TabusMessage<T = unknown> {
|
|
4
17
|
tabId: string;
|
|
@@ -32,6 +45,8 @@ declare class Tabus<Events extends EventMap = EventMap> {
|
|
|
32
45
|
private readonly localHandlers;
|
|
33
46
|
private destroyed;
|
|
34
47
|
private joinTimer;
|
|
48
|
+
private readonly throttleMs;
|
|
49
|
+
private lastEmitAt;
|
|
35
50
|
/**
|
|
36
51
|
* Creates a new Tabus instance on the given channel.
|
|
37
52
|
*
|
|
@@ -43,6 +58,8 @@ declare class Tabus<Events extends EventMap = EventMap> {
|
|
|
43
58
|
*
|
|
44
59
|
* @param channelName - Shared channel name (default: `"tabus"`). All
|
|
45
60
|
* instances with the same channel name can communicate with each other.
|
|
61
|
+
* @param options - Configuration options for this instance.
|
|
62
|
+
* @param options.throttle - Minimum ms between emitted messages (default: 0, no throttle).
|
|
46
63
|
*
|
|
47
64
|
* @example
|
|
48
65
|
* ```ts
|
|
@@ -51,7 +68,7 @@ declare class Tabus<Events extends EventMap = EventMap> {
|
|
|
51
68
|
* bus.on("tab:join", ({ tabId }) => console.log("peer joined:", tabId));
|
|
52
69
|
* ```
|
|
53
70
|
*/
|
|
54
|
-
constructor(channelName?: string);
|
|
71
|
+
constructor(channelName?: string, options?: TabusOptions);
|
|
55
72
|
/**
|
|
56
73
|
* Subscribes to an event, including internal lifecycle events
|
|
57
74
|
* `"tab:join"` and `"tab:leave"`.
|
|
@@ -136,4 +153,4 @@ interface ITransport {
|
|
|
136
153
|
destroy(): void;
|
|
137
154
|
}
|
|
138
155
|
|
|
139
|
-
export { type EventMap, type Handler, type ITransport, type InternalEvents, Tabus, type TabusMessage };
|
|
156
|
+
export { type EventMap, type Handler, type ITransport, type InternalEvents, Tabus, type TabusMessage, type TabusOptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
type Handler<T = unknown> = (payload: T) => void;
|
|
2
|
+
interface TabusOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Minimum time in milliseconds between emitted messages.
|
|
5
|
+
* When set, emit() calls that arrive faster than this interval
|
|
6
|
+
* are discarded. Useful for high-frequency events like mousemove
|
|
7
|
+
* or scroll to prevent flooding the main thread.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // Allow at most one message every 16ms (~60fps)
|
|
11
|
+
* const bus = new Tabus('canvas', { throttle: 16 })
|
|
12
|
+
*/
|
|
13
|
+
throttle?: number;
|
|
14
|
+
}
|
|
2
15
|
type EventMap = Record<string, unknown>;
|
|
3
16
|
interface TabusMessage<T = unknown> {
|
|
4
17
|
tabId: string;
|
|
@@ -32,6 +45,8 @@ declare class Tabus<Events extends EventMap = EventMap> {
|
|
|
32
45
|
private readonly localHandlers;
|
|
33
46
|
private destroyed;
|
|
34
47
|
private joinTimer;
|
|
48
|
+
private readonly throttleMs;
|
|
49
|
+
private lastEmitAt;
|
|
35
50
|
/**
|
|
36
51
|
* Creates a new Tabus instance on the given channel.
|
|
37
52
|
*
|
|
@@ -43,6 +58,8 @@ declare class Tabus<Events extends EventMap = EventMap> {
|
|
|
43
58
|
*
|
|
44
59
|
* @param channelName - Shared channel name (default: `"tabus"`). All
|
|
45
60
|
* instances with the same channel name can communicate with each other.
|
|
61
|
+
* @param options - Configuration options for this instance.
|
|
62
|
+
* @param options.throttle - Minimum ms between emitted messages (default: 0, no throttle).
|
|
46
63
|
*
|
|
47
64
|
* @example
|
|
48
65
|
* ```ts
|
|
@@ -51,7 +68,7 @@ declare class Tabus<Events extends EventMap = EventMap> {
|
|
|
51
68
|
* bus.on("tab:join", ({ tabId }) => console.log("peer joined:", tabId));
|
|
52
69
|
* ```
|
|
53
70
|
*/
|
|
54
|
-
constructor(channelName?: string);
|
|
71
|
+
constructor(channelName?: string, options?: TabusOptions);
|
|
55
72
|
/**
|
|
56
73
|
* Subscribes to an event, including internal lifecycle events
|
|
57
74
|
* `"tab:join"` and `"tab:leave"`.
|
|
@@ -136,4 +153,4 @@ interface ITransport {
|
|
|
136
153
|
destroy(): void;
|
|
137
154
|
}
|
|
138
155
|
|
|
139
|
-
export { type EventMap, type Handler, type ITransport, type InternalEvents, Tabus, type TabusMessage };
|
|
156
|
+
export { type EventMap, type Handler, type ITransport, type InternalEvents, Tabus, type TabusMessage, type TabusOptions };
|
package/dist/index.js
CHANGED
|
@@ -102,6 +102,8 @@ var Tabus = class {
|
|
|
102
102
|
*
|
|
103
103
|
* @param channelName - Shared channel name (default: `"tabus"`). All
|
|
104
104
|
* instances with the same channel name can communicate with each other.
|
|
105
|
+
* @param options - Configuration options for this instance.
|
|
106
|
+
* @param options.throttle - Minimum ms between emitted messages (default: 0, no throttle).
|
|
105
107
|
*
|
|
106
108
|
* @example
|
|
107
109
|
* ```ts
|
|
@@ -110,11 +112,13 @@ var Tabus = class {
|
|
|
110
112
|
* bus.on("tab:join", ({ tabId }) => console.log("peer joined:", tabId));
|
|
111
113
|
* ```
|
|
112
114
|
*/
|
|
113
|
-
constructor(channelName) {
|
|
115
|
+
constructor(channelName, options) {
|
|
114
116
|
this.localHandlers = /* @__PURE__ */ new Map();
|
|
115
117
|
this.destroyed = false;
|
|
116
118
|
this.joinTimer = null;
|
|
119
|
+
this.lastEmitAt = 0;
|
|
117
120
|
this.tabId = generateId();
|
|
121
|
+
this.throttleMs = options?.throttle ?? 0;
|
|
118
122
|
const name = channelName ?? "tabus";
|
|
119
123
|
this.transport = typeof BroadcastChannel !== "undefined" ? new BroadcastTransport(name) : new MemoryTransport(name);
|
|
120
124
|
this.transport.onMessage((msg) => {
|
|
@@ -193,6 +197,11 @@ var Tabus = class {
|
|
|
193
197
|
*/
|
|
194
198
|
emit(event, payload) {
|
|
195
199
|
if (this.destroyed) return this;
|
|
200
|
+
if (this.throttleMs > 0) {
|
|
201
|
+
const now = Date.now();
|
|
202
|
+
if (now - this.lastEmitAt < this.throttleMs) return this;
|
|
203
|
+
this.lastEmitAt = now;
|
|
204
|
+
}
|
|
196
205
|
this.transport.send({ tabId: this.tabId, event: String(event), payload });
|
|
197
206
|
return this;
|
|
198
207
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/core/generate-id.ts","../src/transport/memory.transport.ts","../src/transport/broadcast.transport.ts","../src/core/tabus.ts"],"sourcesContent":["export { Tabus } from \"./core/tabus\";\r\nexport type { ITransport } from \"./transport/transport.interface\";\r\nexport type { EventMap, InternalEvents, Handler, TabusMessage } from \"./core/types\";\r\n","/**\r\n * Generates a RFC-4122 v4 UUID string.\r\n * Uses `crypto.randomUUID()` when available; falls back to a `Math.random`-based\r\n * implementation for environments that lack the Web Crypto API.\r\n */\r\nexport const generateId = (): string => {\r\n if (\r\n typeof crypto !== \"undefined\" &&\r\n typeof (crypto as Crypto).randomUUID === \"function\"\r\n ) {\r\n return (crypto as Crypto).randomUUID();\r\n }\r\n\r\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\r\n const r = (Math.random() * 16) | 0;\r\n return (c === \"x\" ? r : (r & 0x3) | 0x8).toString(16);\r\n });\r\n}\r\n","import type { ITransport } from \"./transport.interface\";\r\nimport type { TabusMessage } from \"../core/types\";\r\n\r\n// Module-level registries shared across all MemoryTransport instances.\r\nconst channels = new Map<string, Set<(msg: TabusMessage) => void>>();\r\n// Tracks which channels have already been warned, so the warning fires once per channel.\r\nconst warnedChannels = new Set<string>();\r\n\r\n/**\r\n * In-process transport used when `BroadcastChannel` is unavailable.\r\n * All instances sharing the same `channelName` communicate via a shared Set\r\n * of listeners. Unlike `BroadcastTransport`, the sender's own listener IS\r\n * called by `send()` — deduplication is the responsibility of the caller\r\n * (i.e. `Tabus`).\r\n */\r\nexport class MemoryTransport implements ITransport {\r\n private readonly channelName: string;\r\n private listener: ((msg: TabusMessage) => void) | null = null;\r\n\r\n constructor(channelName: string) {\r\n this.channelName = channelName;\r\n\r\n if (!channels.has(channelName)) {\r\n channels.set(channelName, new Set());\r\n }\r\n\r\n if (!warnedChannels.has(channelName)) {\r\n console.warn(\r\n `[tabus] BroadcastChannel is not available. Falling back to in-memory bus (channel: \"${channelName}\").`,\r\n );\r\n warnedChannels.add(channelName);\r\n }\r\n }\r\n\r\n send(msg: TabusMessage): void {\r\n channels.get(this.channelName)?.forEach((l) => l(msg));\r\n }\r\n\r\n onMessage(listener: (msg: TabusMessage) => void): void {\r\n // Remove any previously registered listener before swapping in the new one.\r\n if (this.listener) {\r\n channels.get(this.channelName)?.delete(this.listener);\r\n }\r\n\r\n this.listener = listener;\r\n channels.get(this.channelName)?.add(listener);\r\n }\r\n\r\n destroy(): void {\r\n if (this.listener) {\r\n channels.get(this.channelName)?.delete(this.listener);\r\n this.listener = null;\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Resets all module-level state.\r\n * @internal Only for use in unit tests.\r\n */\r\nexport function __resetMemoryBus(): void {\r\n channels.clear();\r\n warnedChannels.clear();\r\n}\r\n","import type { ITransport } from \"./transport.interface\";\r\nimport type { TabusMessage } from \"../core/types\";\r\n\r\n/** Transport backed by the browser / worker `BroadcastChannel` API. */\r\nexport class BroadcastTransport implements ITransport {\r\n private readonly bc: BroadcastChannel;\r\n\r\n constructor(channelName: string) {\r\n this.bc = new BroadcastChannel(channelName);\r\n }\r\n\r\n send(msg: TabusMessage): void {\r\n this.bc.postMessage(msg);\r\n }\r\n\r\n onMessage(listener: (msg: TabusMessage) => void): void {\r\n this.bc.addEventListener(\"message\", (ev: MessageEvent<TabusMessage>) =>\r\n listener(ev.data),\r\n );\r\n }\r\n\r\n destroy(): void {\r\n this.bc.close();\r\n }\r\n}\r\n","import { generateId } from \"./generate-id\";\r\nimport { MemoryTransport } from \"../transport/memory.transport\";\r\nimport { BroadcastTransport } from \"../transport/broadcast.transport\";\r\nimport type { ITransport } from \"../transport/transport.interface\";\r\nimport type { Handler, EventMap, TabusMessage, FullEventMap } from \"./types\";\r\n\r\nexport class Tabus<Events extends EventMap = EventMap> {\r\n /**\r\n * Unique identifier for this tab / instance.\r\n * Generated once per instance using `crypto.randomUUID()` or a manual fallback.\r\n *\r\n * @example\r\n * ```ts\r\n * const bus = new Tabus();\r\n * console.log(bus.tabId); // \"3f2a…\"\r\n * ```\r\n */\r\n readonly tabId: string;\r\n\r\n private readonly transport: ITransport;\r\n private readonly localHandlers = new Map<string, Set<Handler>>();\r\n private destroyed = false;\r\n private joinTimer: ReturnType<typeof setTimeout> | null = null;\r\n\r\n /**\r\n * Creates a new Tabus instance on the given channel.\r\n *\r\n * Uses `BroadcastChannel` when available (cross-tab communication); falls\r\n * back to an in-process in-memory bus otherwise.\r\n *\r\n * **`tab:join` is emitted asynchronously** (via `setTimeout 0`) so handlers\r\n * can be registered on the instance before the event is broadcast.\r\n *\r\n * @param channelName - Shared channel name (default: `\"tabus\"`). All\r\n * instances with the same channel name can communicate with each other.\r\n *\r\n * @example\r\n * ```ts\r\n * type MyEvents = { data: { value: number }; reset: void };\r\n * const bus = new Tabus<MyEvents>(\"my-app\");\r\n * bus.on(\"tab:join\", ({ tabId }) => console.log(\"peer joined:\", tabId));\r\n * ```\r\n */\r\n constructor(channelName?: string) {\r\n this.tabId = generateId();\r\n const name = channelName ?? \"tabus\";\r\n\r\n this.transport = typeof BroadcastChannel !== \"undefined\" ? new BroadcastTransport(name) : new MemoryTransport(name);\r\n\r\n // Deduplication: ignore messages that originated from this instance.\r\n this.transport.onMessage((msg: TabusMessage) => {\r\n if (msg.tabId === this.tabId) return;\r\n this.dispatch(msg.event, msg.payload);\r\n });\r\n\r\n // Defer so callers can register tab:join handlers synchronously after\r\n // the constructor returns before the broadcast goes out.\r\n this.joinTimer = setTimeout(() => {\r\n this.joinTimer = null;\r\n if (!this.destroyed) {\r\n this.transport.send({\r\n tabId : this.tabId,\r\n event : \"tab:join\",\r\n payload: { tabId: this.tabId },\r\n });\r\n }\r\n }, 0);\r\n }\r\n\r\n /**\r\n * Subscribes to an event, including internal lifecycle events\r\n * `\"tab:join\"` and `\"tab:leave\"`.\r\n *\r\n * @param event - The event name to listen to.\r\n * @param handler - Callback invoked with the event payload when the event fires.\r\n * @returns `this` for chaining.\r\n *\r\n * @example\r\n * ```ts\r\n * bus\r\n * .on(\"data\", ({ value }) => console.log(value))\r\n * .on(\"tab:join\", ({ tabId }) => console.log(\"new peer:\", tabId));\r\n * ```\r\n */\r\n on<K extends keyof FullEventMap<Events>>(\r\n event: K,\r\n handler: Handler<FullEventMap<Events>[K]>,\r\n ): this {\r\n if (this.destroyed) return this;\r\n\r\n const key = String(event);\r\n\r\n if (!this.localHandlers.has(key)) {\r\n this.localHandlers.set(key, new Set());\r\n }\r\n\r\n this.localHandlers.get(key)!.add(handler as Handler);\r\n return this;\r\n }\r\n\r\n /**\r\n * Unsubscribes a previously registered handler.\r\n *\r\n * @param event - The event name.\r\n * @param handler - The exact same handler reference passed to `on()`.\r\n * @returns `this` for chaining.\r\n *\r\n * @example\r\n * ```ts\r\n * const handler = (p: { value: number }) => console.log(p.value);\r\n * bus.on(\"data\", handler);\r\n * // later:\r\n * bus.off(\"data\", handler);\r\n * ```\r\n */\r\n off<K extends keyof FullEventMap<Events>>(\r\n event: K,\r\n handler: Handler<FullEventMap<Events>[K]>,\r\n ): this {\r\n this.localHandlers.get(String(event))?.delete(handler as Handler);\r\n return this;\r\n }\r\n\r\n /**\r\n * Broadcasts an event to **all other instances** on the same channel.\r\n *\r\n * **The emitting instance does NOT receive its own event.** If local handling\r\n * is needed, call the handler directly after emitting.\r\n *\r\n * @param event - A user-defined event name (internal events cannot be emitted).\r\n * @param payload - The value to broadcast.\r\n * @returns `this` for chaining.\r\n *\r\n * @example\r\n * ```ts\r\n * bus.emit(\"data\", { value: 42 });\r\n * // other tabs receive { value: 42 }; this tab does not.\r\n * ```\r\n */\r\n emit<K extends keyof Events>(event: K, payload: Events[K]): this {\r\n if (this.destroyed) return this;\r\n this.transport.send({ tabId: this.tabId, event: String(event), payload });\r\n return this;\r\n }\r\n\r\n /**\r\n * Broadcasts `\"tab:leave\"`, closes the transport, and removes all handlers.\r\n * Safe to call multiple times — subsequent calls are no-ops.\r\n *\r\n * If the deferred `\"tab:join\"` has not yet fired (i.e., the instance is\r\n * destroyed within the same tick it was created), neither `\"tab:join\"` nor\r\n * `\"tab:leave\"` are sent to peers.\r\n *\r\n * @example\r\n * ```ts\r\n * window.addEventListener(\"beforeunload\", () => bus.destroy());\r\n * ```\r\n */\r\n destroy(): void {\r\n if (this.destroyed) return;\r\n this.destroyed = true;\r\n\r\n if (this.joinTimer !== null) {\r\n // tab:join hasn't fired yet — cancel it; peers don't know we existed,\r\n // so there is nothing to announce on departure either.\r\n clearTimeout(this.joinTimer);\r\n this.joinTimer = null;\r\n } else {\r\n // tab:join already fired — announce departure to peers.\r\n this.transport.send({\r\n tabId : this.tabId,\r\n event : \"tab:leave\",\r\n payload: { tabId: this.tabId },\r\n });\r\n }\r\n\r\n this.transport.destroy();\r\n this.localHandlers.clear();\r\n }\r\n\r\n /** Invokes all registered handlers for the given event with the payload. */\r\n private dispatch(event: string, payload: unknown): void {\r\n this.localHandlers.get(event)?.forEach((h) => h(payload));\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/core/generate-id.ts","../src/transport/memory.transport.ts","../src/transport/broadcast.transport.ts","../src/core/tabus.ts"],"sourcesContent":["export { Tabus } from \"./core/tabus\";\r\nexport type { ITransport } from \"./transport/transport.interface\";\r\nexport type { EventMap, InternalEvents, Handler, TabusMessage, TabusOptions } from \"./core/types\";\r\n","/**\r\n * Generates a RFC-4122 v4 UUID string.\r\n * Uses `crypto.randomUUID()` when available; falls back to a `Math.random`-based\r\n * implementation for environments that lack the Web Crypto API.\r\n */\r\nexport const generateId = (): string => {\r\n if (\r\n typeof crypto !== \"undefined\" &&\r\n typeof (crypto as Crypto).randomUUID === \"function\"\r\n ) {\r\n return (crypto as Crypto).randomUUID();\r\n }\r\n\r\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\r\n const r = (Math.random() * 16) | 0;\r\n return (c === \"x\" ? r : (r & 0x3) | 0x8).toString(16);\r\n });\r\n}\r\n","import type { ITransport } from \"./transport.interface\";\r\nimport type { TabusMessage } from \"../core/types\";\r\n\r\n// Module-level registries shared across all MemoryTransport instances.\r\nconst channels = new Map<string, Set<(msg: TabusMessage) => void>>();\r\n// Tracks which channels have already been warned, so the warning fires once per channel.\r\nconst warnedChannels = new Set<string>();\r\n\r\n/**\r\n * In-process transport used when `BroadcastChannel` is unavailable.\r\n * All instances sharing the same `channelName` communicate via a shared Set\r\n * of listeners. Unlike `BroadcastTransport`, the sender's own listener IS\r\n * called by `send()` — deduplication is the responsibility of the caller\r\n * (i.e. `Tabus`).\r\n */\r\nexport class MemoryTransport implements ITransport {\r\n private readonly channelName: string;\r\n private listener: ((msg: TabusMessage) => void) | null = null;\r\n\r\n constructor(channelName: string) {\r\n this.channelName = channelName;\r\n\r\n if (!channels.has(channelName)) {\r\n channels.set(channelName, new Set());\r\n }\r\n\r\n if (!warnedChannels.has(channelName)) {\r\n console.warn(\r\n `[tabus] BroadcastChannel is not available. Falling back to in-memory bus (channel: \"${channelName}\").`,\r\n );\r\n warnedChannels.add(channelName);\r\n }\r\n }\r\n\r\n send(msg: TabusMessage): void {\r\n channels.get(this.channelName)?.forEach((l) => l(msg));\r\n }\r\n\r\n onMessage(listener: (msg: TabusMessage) => void): void {\r\n // Remove any previously registered listener before swapping in the new one.\r\n if (this.listener) {\r\n channels.get(this.channelName)?.delete(this.listener);\r\n }\r\n\r\n this.listener = listener;\r\n channels.get(this.channelName)?.add(listener);\r\n }\r\n\r\n destroy(): void {\r\n if (this.listener) {\r\n channels.get(this.channelName)?.delete(this.listener);\r\n this.listener = null;\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Resets all module-level state.\r\n * @internal Only for use in unit tests.\r\n */\r\nexport function __resetMemoryBus(): void {\r\n channels.clear();\r\n warnedChannels.clear();\r\n}\r\n","import type { ITransport } from \"./transport.interface\";\r\nimport type { TabusMessage } from \"../core/types\";\r\n\r\n/** Transport backed by the browser / worker `BroadcastChannel` API. */\r\nexport class BroadcastTransport implements ITransport {\r\n private readonly bc: BroadcastChannel;\r\n\r\n constructor(channelName: string) {\r\n this.bc = new BroadcastChannel(channelName);\r\n }\r\n\r\n send(msg: TabusMessage): void {\r\n this.bc.postMessage(msg);\r\n }\r\n\r\n onMessage(listener: (msg: TabusMessage) => void): void {\r\n this.bc.addEventListener(\"message\", (ev: MessageEvent<TabusMessage>) =>\r\n listener(ev.data),\r\n );\r\n }\r\n\r\n destroy(): void {\r\n this.bc.close();\r\n }\r\n}\r\n","import { generateId } from \"./generate-id\";\r\nimport { MemoryTransport } from \"../transport/memory.transport\";\r\nimport { BroadcastTransport } from \"../transport/broadcast.transport\";\r\nimport type { ITransport } from \"../transport/transport.interface\";\r\nimport type { Handler, EventMap, TabusMessage, FullEventMap, TabusOptions } from \"./types\";\r\n\r\nexport class Tabus<Events extends EventMap = EventMap> {\r\n /**\r\n * Unique identifier for this tab / instance.\r\n * Generated once per instance using `crypto.randomUUID()` or a manual fallback.\r\n *\r\n * @example\r\n * ```ts\r\n * const bus = new Tabus();\r\n * console.log(bus.tabId); // \"3f2a…\"\r\n * ```\r\n */\r\n readonly tabId: string;\r\n\r\n private readonly transport: ITransport;\r\n private readonly localHandlers = new Map<string, Set<Handler>>();\r\n private destroyed = false;\r\n private joinTimer: ReturnType<typeof setTimeout> | null = null;\r\n private readonly throttleMs: number;\r\n private lastEmitAt = 0;\r\n\r\n /**\r\n * Creates a new Tabus instance on the given channel.\r\n *\r\n * Uses `BroadcastChannel` when available (cross-tab communication); falls\r\n * back to an in-process in-memory bus otherwise.\r\n *\r\n * **`tab:join` is emitted asynchronously** (via `setTimeout 0`) so handlers\r\n * can be registered on the instance before the event is broadcast.\r\n *\r\n * @param channelName - Shared channel name (default: `\"tabus\"`). All\r\n * instances with the same channel name can communicate with each other.\r\n * @param options - Configuration options for this instance.\r\n * @param options.throttle - Minimum ms between emitted messages (default: 0, no throttle).\r\n *\r\n * @example\r\n * ```ts\r\n * type MyEvents = { data: { value: number }; reset: void };\r\n * const bus = new Tabus<MyEvents>(\"my-app\");\r\n * bus.on(\"tab:join\", ({ tabId }) => console.log(\"peer joined:\", tabId));\r\n * ```\r\n */\r\n constructor(channelName?: string, options?: TabusOptions) {\r\n this.tabId = generateId();\r\n this.throttleMs = options?.throttle ?? 0;\r\n const name = channelName ?? \"tabus\";\r\n\r\n this.transport = typeof BroadcastChannel !== \"undefined\" ? new BroadcastTransport(name) : new MemoryTransport(name);\r\n\r\n // Deduplication: ignore messages that originated from this instance.\r\n this.transport.onMessage((msg: TabusMessage) => {\r\n if (msg.tabId === this.tabId) return;\r\n this.dispatch(msg.event, msg.payload);\r\n });\r\n\r\n // Defer so callers can register tab:join handlers synchronously after\r\n // the constructor returns before the broadcast goes out.\r\n this.joinTimer = setTimeout(() => {\r\n this.joinTimer = null;\r\n if (!this.destroyed) {\r\n this.transport.send({\r\n tabId : this.tabId,\r\n event : \"tab:join\",\r\n payload: { tabId: this.tabId },\r\n });\r\n }\r\n }, 0);\r\n }\r\n\r\n /**\r\n * Subscribes to an event, including internal lifecycle events\r\n * `\"tab:join\"` and `\"tab:leave\"`.\r\n *\r\n * @param event - The event name to listen to.\r\n * @param handler - Callback invoked with the event payload when the event fires.\r\n * @returns `this` for chaining.\r\n *\r\n * @example\r\n * ```ts\r\n * bus\r\n * .on(\"data\", ({ value }) => console.log(value))\r\n * .on(\"tab:join\", ({ tabId }) => console.log(\"new peer:\", tabId));\r\n * ```\r\n */\r\n on<K extends keyof FullEventMap<Events>>(\r\n event: K,\r\n handler: Handler<FullEventMap<Events>[K]>,\r\n ): this {\r\n if (this.destroyed) return this;\r\n\r\n const key = String(event);\r\n\r\n if (!this.localHandlers.has(key)) {\r\n this.localHandlers.set(key, new Set());\r\n }\r\n\r\n this.localHandlers.get(key)!.add(handler as Handler);\r\n return this;\r\n }\r\n\r\n /**\r\n * Unsubscribes a previously registered handler.\r\n *\r\n * @param event - The event name.\r\n * @param handler - The exact same handler reference passed to `on()`.\r\n * @returns `this` for chaining.\r\n *\r\n * @example\r\n * ```ts\r\n * const handler = (p: { value: number }) => console.log(p.value);\r\n * bus.on(\"data\", handler);\r\n * // later:\r\n * bus.off(\"data\", handler);\r\n * ```\r\n */\r\n off<K extends keyof FullEventMap<Events>>(\r\n event: K,\r\n handler: Handler<FullEventMap<Events>[K]>,\r\n ): this {\r\n this.localHandlers.get(String(event))?.delete(handler as Handler);\r\n return this;\r\n }\r\n\r\n /**\r\n * Broadcasts an event to **all other instances** on the same channel.\r\n *\r\n * **The emitting instance does NOT receive its own event.** If local handling\r\n * is needed, call the handler directly after emitting.\r\n *\r\n * @param event - A user-defined event name (internal events cannot be emitted).\r\n * @param payload - The value to broadcast.\r\n * @returns `this` for chaining.\r\n *\r\n * @example\r\n * ```ts\r\n * bus.emit(\"data\", { value: 42 });\r\n * // other tabs receive { value: 42 }; this tab does not.\r\n * ```\r\n */\r\n emit<K extends keyof Events>(event: K, payload: Events[K]): this {\r\n if (this.destroyed) return this;\r\n\r\n if (this.throttleMs > 0) {\r\n const now = Date.now();\r\n if (now - this.lastEmitAt < this.throttleMs) return this;\r\n this.lastEmitAt = now;\r\n }\r\n\r\n this.transport.send({ tabId: this.tabId, event: String(event), payload });\r\n return this;\r\n }\r\n\r\n /**\r\n * Broadcasts `\"tab:leave\"`, closes the transport, and removes all handlers.\r\n * Safe to call multiple times — subsequent calls are no-ops.\r\n *\r\n * If the deferred `\"tab:join\"` has not yet fired (i.e., the instance is\r\n * destroyed within the same tick it was created), neither `\"tab:join\"` nor\r\n * `\"tab:leave\"` are sent to peers.\r\n *\r\n * @example\r\n * ```ts\r\n * window.addEventListener(\"beforeunload\", () => bus.destroy());\r\n * ```\r\n */\r\n destroy(): void {\r\n if (this.destroyed) return;\r\n this.destroyed = true;\r\n\r\n if (this.joinTimer !== null) {\r\n // tab:join hasn't fired yet — cancel it; peers don't know we existed,\r\n // so there is nothing to announce on departure either.\r\n clearTimeout(this.joinTimer);\r\n this.joinTimer = null;\r\n } else {\r\n // tab:join already fired — announce departure to peers.\r\n this.transport.send({\r\n tabId : this.tabId,\r\n event : \"tab:leave\",\r\n payload: { tabId: this.tabId },\r\n });\r\n }\r\n\r\n this.transport.destroy();\r\n this.localHandlers.clear();\r\n }\r\n\r\n /** Invokes all registered handlers for the given event with the payload. */\r\n private dispatch(event: string, payload: unknown): void {\r\n this.localHandlers.get(event)?.forEach((h) => h(payload));\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -76,6 +76,8 @@ var Tabus = class {
|
|
|
76
76
|
*
|
|
77
77
|
* @param channelName - Shared channel name (default: `"tabus"`). All
|
|
78
78
|
* instances with the same channel name can communicate with each other.
|
|
79
|
+
* @param options - Configuration options for this instance.
|
|
80
|
+
* @param options.throttle - Minimum ms between emitted messages (default: 0, no throttle).
|
|
79
81
|
*
|
|
80
82
|
* @example
|
|
81
83
|
* ```ts
|
|
@@ -84,11 +86,13 @@ var Tabus = class {
|
|
|
84
86
|
* bus.on("tab:join", ({ tabId }) => console.log("peer joined:", tabId));
|
|
85
87
|
* ```
|
|
86
88
|
*/
|
|
87
|
-
constructor(channelName) {
|
|
89
|
+
constructor(channelName, options) {
|
|
88
90
|
this.localHandlers = /* @__PURE__ */ new Map();
|
|
89
91
|
this.destroyed = false;
|
|
90
92
|
this.joinTimer = null;
|
|
93
|
+
this.lastEmitAt = 0;
|
|
91
94
|
this.tabId = generateId();
|
|
95
|
+
this.throttleMs = options?.throttle ?? 0;
|
|
92
96
|
const name = channelName ?? "tabus";
|
|
93
97
|
this.transport = typeof BroadcastChannel !== "undefined" ? new BroadcastTransport(name) : new MemoryTransport(name);
|
|
94
98
|
this.transport.onMessage((msg) => {
|
|
@@ -167,6 +171,11 @@ var Tabus = class {
|
|
|
167
171
|
*/
|
|
168
172
|
emit(event, payload) {
|
|
169
173
|
if (this.destroyed) return this;
|
|
174
|
+
if (this.throttleMs > 0) {
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
if (now - this.lastEmitAt < this.throttleMs) return this;
|
|
177
|
+
this.lastEmitAt = now;
|
|
178
|
+
}
|
|
170
179
|
this.transport.send({ tabId: this.tabId, event: String(event), payload });
|
|
171
180
|
return this;
|
|
172
181
|
}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/generate-id.ts","../src/transport/memory.transport.ts","../src/transport/broadcast.transport.ts","../src/core/tabus.ts"],"sourcesContent":["/**\r\n * Generates a RFC-4122 v4 UUID string.\r\n * Uses `crypto.randomUUID()` when available; falls back to a `Math.random`-based\r\n * implementation for environments that lack the Web Crypto API.\r\n */\r\nexport const generateId = (): string => {\r\n if (\r\n typeof crypto !== \"undefined\" &&\r\n typeof (crypto as Crypto).randomUUID === \"function\"\r\n ) {\r\n return (crypto as Crypto).randomUUID();\r\n }\r\n\r\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\r\n const r = (Math.random() * 16) | 0;\r\n return (c === \"x\" ? r : (r & 0x3) | 0x8).toString(16);\r\n });\r\n}\r\n","import type { ITransport } from \"./transport.interface\";\r\nimport type { TabusMessage } from \"../core/types\";\r\n\r\n// Module-level registries shared across all MemoryTransport instances.\r\nconst channels = new Map<string, Set<(msg: TabusMessage) => void>>();\r\n// Tracks which channels have already been warned, so the warning fires once per channel.\r\nconst warnedChannels = new Set<string>();\r\n\r\n/**\r\n * In-process transport used when `BroadcastChannel` is unavailable.\r\n * All instances sharing the same `channelName` communicate via a shared Set\r\n * of listeners. Unlike `BroadcastTransport`, the sender's own listener IS\r\n * called by `send()` — deduplication is the responsibility of the caller\r\n * (i.e. `Tabus`).\r\n */\r\nexport class MemoryTransport implements ITransport {\r\n private readonly channelName: string;\r\n private listener: ((msg: TabusMessage) => void) | null = null;\r\n\r\n constructor(channelName: string) {\r\n this.channelName = channelName;\r\n\r\n if (!channels.has(channelName)) {\r\n channels.set(channelName, new Set());\r\n }\r\n\r\n if (!warnedChannels.has(channelName)) {\r\n console.warn(\r\n `[tabus] BroadcastChannel is not available. Falling back to in-memory bus (channel: \"${channelName}\").`,\r\n );\r\n warnedChannels.add(channelName);\r\n }\r\n }\r\n\r\n send(msg: TabusMessage): void {\r\n channels.get(this.channelName)?.forEach((l) => l(msg));\r\n }\r\n\r\n onMessage(listener: (msg: TabusMessage) => void): void {\r\n // Remove any previously registered listener before swapping in the new one.\r\n if (this.listener) {\r\n channels.get(this.channelName)?.delete(this.listener);\r\n }\r\n\r\n this.listener = listener;\r\n channels.get(this.channelName)?.add(listener);\r\n }\r\n\r\n destroy(): void {\r\n if (this.listener) {\r\n channels.get(this.channelName)?.delete(this.listener);\r\n this.listener = null;\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Resets all module-level state.\r\n * @internal Only for use in unit tests.\r\n */\r\nexport function __resetMemoryBus(): void {\r\n channels.clear();\r\n warnedChannels.clear();\r\n}\r\n","import type { ITransport } from \"./transport.interface\";\r\nimport type { TabusMessage } from \"../core/types\";\r\n\r\n/** Transport backed by the browser / worker `BroadcastChannel` API. */\r\nexport class BroadcastTransport implements ITransport {\r\n private readonly bc: BroadcastChannel;\r\n\r\n constructor(channelName: string) {\r\n this.bc = new BroadcastChannel(channelName);\r\n }\r\n\r\n send(msg: TabusMessage): void {\r\n this.bc.postMessage(msg);\r\n }\r\n\r\n onMessage(listener: (msg: TabusMessage) => void): void {\r\n this.bc.addEventListener(\"message\", (ev: MessageEvent<TabusMessage>) =>\r\n listener(ev.data),\r\n );\r\n }\r\n\r\n destroy(): void {\r\n this.bc.close();\r\n }\r\n}\r\n","import { generateId } from \"./generate-id\";\r\nimport { MemoryTransport } from \"../transport/memory.transport\";\r\nimport { BroadcastTransport } from \"../transport/broadcast.transport\";\r\nimport type { ITransport } from \"../transport/transport.interface\";\r\nimport type { Handler, EventMap, TabusMessage, FullEventMap } from \"./types\";\r\n\r\nexport class Tabus<Events extends EventMap = EventMap> {\r\n /**\r\n * Unique identifier for this tab / instance.\r\n * Generated once per instance using `crypto.randomUUID()` or a manual fallback.\r\n *\r\n * @example\r\n * ```ts\r\n * const bus = new Tabus();\r\n * console.log(bus.tabId); // \"3f2a…\"\r\n * ```\r\n */\r\n readonly tabId: string;\r\n\r\n private readonly transport: ITransport;\r\n private readonly localHandlers = new Map<string, Set<Handler>>();\r\n private destroyed = false;\r\n private joinTimer: ReturnType<typeof setTimeout> | null = null;\r\n\r\n /**\r\n * Creates a new Tabus instance on the given channel.\r\n *\r\n * Uses `BroadcastChannel` when available (cross-tab communication); falls\r\n * back to an in-process in-memory bus otherwise.\r\n *\r\n * **`tab:join` is emitted asynchronously** (via `setTimeout 0`) so handlers\r\n * can be registered on the instance before the event is broadcast.\r\n *\r\n * @param channelName - Shared channel name (default: `\"tabus\"`). All\r\n * instances with the same channel name can communicate with each other.\r\n *\r\n * @example\r\n * ```ts\r\n * type MyEvents = { data: { value: number }; reset: void };\r\n * const bus = new Tabus<MyEvents>(\"my-app\");\r\n * bus.on(\"tab:join\", ({ tabId }) => console.log(\"peer joined:\", tabId));\r\n * ```\r\n */\r\n constructor(channelName?: string) {\r\n this.tabId = generateId();\r\n const name = channelName ?? \"tabus\";\r\n\r\n this.transport = typeof BroadcastChannel !== \"undefined\" ? new BroadcastTransport(name) : new MemoryTransport(name);\r\n\r\n // Deduplication: ignore messages that originated from this instance.\r\n this.transport.onMessage((msg: TabusMessage) => {\r\n if (msg.tabId === this.tabId) return;\r\n this.dispatch(msg.event, msg.payload);\r\n });\r\n\r\n // Defer so callers can register tab:join handlers synchronously after\r\n // the constructor returns before the broadcast goes out.\r\n this.joinTimer = setTimeout(() => {\r\n this.joinTimer = null;\r\n if (!this.destroyed) {\r\n this.transport.send({\r\n tabId : this.tabId,\r\n event : \"tab:join\",\r\n payload: { tabId: this.tabId },\r\n });\r\n }\r\n }, 0);\r\n }\r\n\r\n /**\r\n * Subscribes to an event, including internal lifecycle events\r\n * `\"tab:join\"` and `\"tab:leave\"`.\r\n *\r\n * @param event - The event name to listen to.\r\n * @param handler - Callback invoked with the event payload when the event fires.\r\n * @returns `this` for chaining.\r\n *\r\n * @example\r\n * ```ts\r\n * bus\r\n * .on(\"data\", ({ value }) => console.log(value))\r\n * .on(\"tab:join\", ({ tabId }) => console.log(\"new peer:\", tabId));\r\n * ```\r\n */\r\n on<K extends keyof FullEventMap<Events>>(\r\n event: K,\r\n handler: Handler<FullEventMap<Events>[K]>,\r\n ): this {\r\n if (this.destroyed) return this;\r\n\r\n const key = String(event);\r\n\r\n if (!this.localHandlers.has(key)) {\r\n this.localHandlers.set(key, new Set());\r\n }\r\n\r\n this.localHandlers.get(key)!.add(handler as Handler);\r\n return this;\r\n }\r\n\r\n /**\r\n * Unsubscribes a previously registered handler.\r\n *\r\n * @param event - The event name.\r\n * @param handler - The exact same handler reference passed to `on()`.\r\n * @returns `this` for chaining.\r\n *\r\n * @example\r\n * ```ts\r\n * const handler = (p: { value: number }) => console.log(p.value);\r\n * bus.on(\"data\", handler);\r\n * // later:\r\n * bus.off(\"data\", handler);\r\n * ```\r\n */\r\n off<K extends keyof FullEventMap<Events>>(\r\n event: K,\r\n handler: Handler<FullEventMap<Events>[K]>,\r\n ): this {\r\n this.localHandlers.get(String(event))?.delete(handler as Handler);\r\n return this;\r\n }\r\n\r\n /**\r\n * Broadcasts an event to **all other instances** on the same channel.\r\n *\r\n * **The emitting instance does NOT receive its own event.** If local handling\r\n * is needed, call the handler directly after emitting.\r\n *\r\n * @param event - A user-defined event name (internal events cannot be emitted).\r\n * @param payload - The value to broadcast.\r\n * @returns `this` for chaining.\r\n *\r\n * @example\r\n * ```ts\r\n * bus.emit(\"data\", { value: 42 });\r\n * // other tabs receive { value: 42 }; this tab does not.\r\n * ```\r\n */\r\n emit<K extends keyof Events>(event: K, payload: Events[K]): this {\r\n if (this.destroyed) return this;\r\n this.transport.send({ tabId: this.tabId, event: String(event), payload });\r\n return this;\r\n }\r\n\r\n /**\r\n * Broadcasts `\"tab:leave\"`, closes the transport, and removes all handlers.\r\n * Safe to call multiple times — subsequent calls are no-ops.\r\n *\r\n * If the deferred `\"tab:join\"` has not yet fired (i.e., the instance is\r\n * destroyed within the same tick it was created), neither `\"tab:join\"` nor\r\n * `\"tab:leave\"` are sent to peers.\r\n *\r\n * @example\r\n * ```ts\r\n * window.addEventListener(\"beforeunload\", () => bus.destroy());\r\n * ```\r\n */\r\n destroy(): void {\r\n if (this.destroyed) return;\r\n this.destroyed = true;\r\n\r\n if (this.joinTimer !== null) {\r\n // tab:join hasn't fired yet — cancel it; peers don't know we existed,\r\n // so there is nothing to announce on departure either.\r\n clearTimeout(this.joinTimer);\r\n this.joinTimer = null;\r\n } else {\r\n // tab:join already fired — announce departure to peers.\r\n this.transport.send({\r\n tabId : this.tabId,\r\n event : \"tab:leave\",\r\n payload: { tabId: this.tabId },\r\n });\r\n }\r\n\r\n this.transport.destroy();\r\n this.localHandlers.clear();\r\n }\r\n\r\n /** Invokes all registered handlers for the given event with the payload. */\r\n private dispatch(event: string, payload: unknown): void {\r\n this.localHandlers.get(event)?.forEach((h) => h(payload));\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/core/generate-id.ts","../src/transport/memory.transport.ts","../src/transport/broadcast.transport.ts","../src/core/tabus.ts"],"sourcesContent":["/**\r\n * Generates a RFC-4122 v4 UUID string.\r\n * Uses `crypto.randomUUID()` when available; falls back to a `Math.random`-based\r\n * implementation for environments that lack the Web Crypto API.\r\n */\r\nexport const generateId = (): string => {\r\n if (\r\n typeof crypto !== \"undefined\" &&\r\n typeof (crypto as Crypto).randomUUID === \"function\"\r\n ) {\r\n return (crypto as Crypto).randomUUID();\r\n }\r\n\r\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\r\n const r = (Math.random() * 16) | 0;\r\n return (c === \"x\" ? r : (r & 0x3) | 0x8).toString(16);\r\n });\r\n}\r\n","import type { ITransport } from \"./transport.interface\";\r\nimport type { TabusMessage } from \"../core/types\";\r\n\r\n// Module-level registries shared across all MemoryTransport instances.\r\nconst channels = new Map<string, Set<(msg: TabusMessage) => void>>();\r\n// Tracks which channels have already been warned, so the warning fires once per channel.\r\nconst warnedChannels = new Set<string>();\r\n\r\n/**\r\n * In-process transport used when `BroadcastChannel` is unavailable.\r\n * All instances sharing the same `channelName` communicate via a shared Set\r\n * of listeners. Unlike `BroadcastTransport`, the sender's own listener IS\r\n * called by `send()` — deduplication is the responsibility of the caller\r\n * (i.e. `Tabus`).\r\n */\r\nexport class MemoryTransport implements ITransport {\r\n private readonly channelName: string;\r\n private listener: ((msg: TabusMessage) => void) | null = null;\r\n\r\n constructor(channelName: string) {\r\n this.channelName = channelName;\r\n\r\n if (!channels.has(channelName)) {\r\n channels.set(channelName, new Set());\r\n }\r\n\r\n if (!warnedChannels.has(channelName)) {\r\n console.warn(\r\n `[tabus] BroadcastChannel is not available. Falling back to in-memory bus (channel: \"${channelName}\").`,\r\n );\r\n warnedChannels.add(channelName);\r\n }\r\n }\r\n\r\n send(msg: TabusMessage): void {\r\n channels.get(this.channelName)?.forEach((l) => l(msg));\r\n }\r\n\r\n onMessage(listener: (msg: TabusMessage) => void): void {\r\n // Remove any previously registered listener before swapping in the new one.\r\n if (this.listener) {\r\n channels.get(this.channelName)?.delete(this.listener);\r\n }\r\n\r\n this.listener = listener;\r\n channels.get(this.channelName)?.add(listener);\r\n }\r\n\r\n destroy(): void {\r\n if (this.listener) {\r\n channels.get(this.channelName)?.delete(this.listener);\r\n this.listener = null;\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Resets all module-level state.\r\n * @internal Only for use in unit tests.\r\n */\r\nexport function __resetMemoryBus(): void {\r\n channels.clear();\r\n warnedChannels.clear();\r\n}\r\n","import type { ITransport } from \"./transport.interface\";\r\nimport type { TabusMessage } from \"../core/types\";\r\n\r\n/** Transport backed by the browser / worker `BroadcastChannel` API. */\r\nexport class BroadcastTransport implements ITransport {\r\n private readonly bc: BroadcastChannel;\r\n\r\n constructor(channelName: string) {\r\n this.bc = new BroadcastChannel(channelName);\r\n }\r\n\r\n send(msg: TabusMessage): void {\r\n this.bc.postMessage(msg);\r\n }\r\n\r\n onMessage(listener: (msg: TabusMessage) => void): void {\r\n this.bc.addEventListener(\"message\", (ev: MessageEvent<TabusMessage>) =>\r\n listener(ev.data),\r\n );\r\n }\r\n\r\n destroy(): void {\r\n this.bc.close();\r\n }\r\n}\r\n","import { generateId } from \"./generate-id\";\r\nimport { MemoryTransport } from \"../transport/memory.transport\";\r\nimport { BroadcastTransport } from \"../transport/broadcast.transport\";\r\nimport type { ITransport } from \"../transport/transport.interface\";\r\nimport type { Handler, EventMap, TabusMessage, FullEventMap, TabusOptions } from \"./types\";\r\n\r\nexport class Tabus<Events extends EventMap = EventMap> {\r\n /**\r\n * Unique identifier for this tab / instance.\r\n * Generated once per instance using `crypto.randomUUID()` or a manual fallback.\r\n *\r\n * @example\r\n * ```ts\r\n * const bus = new Tabus();\r\n * console.log(bus.tabId); // \"3f2a…\"\r\n * ```\r\n */\r\n readonly tabId: string;\r\n\r\n private readonly transport: ITransport;\r\n private readonly localHandlers = new Map<string, Set<Handler>>();\r\n private destroyed = false;\r\n private joinTimer: ReturnType<typeof setTimeout> | null = null;\r\n private readonly throttleMs: number;\r\n private lastEmitAt = 0;\r\n\r\n /**\r\n * Creates a new Tabus instance on the given channel.\r\n *\r\n * Uses `BroadcastChannel` when available (cross-tab communication); falls\r\n * back to an in-process in-memory bus otherwise.\r\n *\r\n * **`tab:join` is emitted asynchronously** (via `setTimeout 0`) so handlers\r\n * can be registered on the instance before the event is broadcast.\r\n *\r\n * @param channelName - Shared channel name (default: `\"tabus\"`). All\r\n * instances with the same channel name can communicate with each other.\r\n * @param options - Configuration options for this instance.\r\n * @param options.throttle - Minimum ms between emitted messages (default: 0, no throttle).\r\n *\r\n * @example\r\n * ```ts\r\n * type MyEvents = { data: { value: number }; reset: void };\r\n * const bus = new Tabus<MyEvents>(\"my-app\");\r\n * bus.on(\"tab:join\", ({ tabId }) => console.log(\"peer joined:\", tabId));\r\n * ```\r\n */\r\n constructor(channelName?: string, options?: TabusOptions) {\r\n this.tabId = generateId();\r\n this.throttleMs = options?.throttle ?? 0;\r\n const name = channelName ?? \"tabus\";\r\n\r\n this.transport = typeof BroadcastChannel !== \"undefined\" ? new BroadcastTransport(name) : new MemoryTransport(name);\r\n\r\n // Deduplication: ignore messages that originated from this instance.\r\n this.transport.onMessage((msg: TabusMessage) => {\r\n if (msg.tabId === this.tabId) return;\r\n this.dispatch(msg.event, msg.payload);\r\n });\r\n\r\n // Defer so callers can register tab:join handlers synchronously after\r\n // the constructor returns before the broadcast goes out.\r\n this.joinTimer = setTimeout(() => {\r\n this.joinTimer = null;\r\n if (!this.destroyed) {\r\n this.transport.send({\r\n tabId : this.tabId,\r\n event : \"tab:join\",\r\n payload: { tabId: this.tabId },\r\n });\r\n }\r\n }, 0);\r\n }\r\n\r\n /**\r\n * Subscribes to an event, including internal lifecycle events\r\n * `\"tab:join\"` and `\"tab:leave\"`.\r\n *\r\n * @param event - The event name to listen to.\r\n * @param handler - Callback invoked with the event payload when the event fires.\r\n * @returns `this` for chaining.\r\n *\r\n * @example\r\n * ```ts\r\n * bus\r\n * .on(\"data\", ({ value }) => console.log(value))\r\n * .on(\"tab:join\", ({ tabId }) => console.log(\"new peer:\", tabId));\r\n * ```\r\n */\r\n on<K extends keyof FullEventMap<Events>>(\r\n event: K,\r\n handler: Handler<FullEventMap<Events>[K]>,\r\n ): this {\r\n if (this.destroyed) return this;\r\n\r\n const key = String(event);\r\n\r\n if (!this.localHandlers.has(key)) {\r\n this.localHandlers.set(key, new Set());\r\n }\r\n\r\n this.localHandlers.get(key)!.add(handler as Handler);\r\n return this;\r\n }\r\n\r\n /**\r\n * Unsubscribes a previously registered handler.\r\n *\r\n * @param event - The event name.\r\n * @param handler - The exact same handler reference passed to `on()`.\r\n * @returns `this` for chaining.\r\n *\r\n * @example\r\n * ```ts\r\n * const handler = (p: { value: number }) => console.log(p.value);\r\n * bus.on(\"data\", handler);\r\n * // later:\r\n * bus.off(\"data\", handler);\r\n * ```\r\n */\r\n off<K extends keyof FullEventMap<Events>>(\r\n event: K,\r\n handler: Handler<FullEventMap<Events>[K]>,\r\n ): this {\r\n this.localHandlers.get(String(event))?.delete(handler as Handler);\r\n return this;\r\n }\r\n\r\n /**\r\n * Broadcasts an event to **all other instances** on the same channel.\r\n *\r\n * **The emitting instance does NOT receive its own event.** If local handling\r\n * is needed, call the handler directly after emitting.\r\n *\r\n * @param event - A user-defined event name (internal events cannot be emitted).\r\n * @param payload - The value to broadcast.\r\n * @returns `this` for chaining.\r\n *\r\n * @example\r\n * ```ts\r\n * bus.emit(\"data\", { value: 42 });\r\n * // other tabs receive { value: 42 }; this tab does not.\r\n * ```\r\n */\r\n emit<K extends keyof Events>(event: K, payload: Events[K]): this {\r\n if (this.destroyed) return this;\r\n\r\n if (this.throttleMs > 0) {\r\n const now = Date.now();\r\n if (now - this.lastEmitAt < this.throttleMs) return this;\r\n this.lastEmitAt = now;\r\n }\r\n\r\n this.transport.send({ tabId: this.tabId, event: String(event), payload });\r\n return this;\r\n }\r\n\r\n /**\r\n * Broadcasts `\"tab:leave\"`, closes the transport, and removes all handlers.\r\n * Safe to call multiple times — subsequent calls are no-ops.\r\n *\r\n * If the deferred `\"tab:join\"` has not yet fired (i.e., the instance is\r\n * destroyed within the same tick it was created), neither `\"tab:join\"` nor\r\n * `\"tab:leave\"` are sent to peers.\r\n *\r\n * @example\r\n * ```ts\r\n * window.addEventListener(\"beforeunload\", () => bus.destroy());\r\n * ```\r\n */\r\n destroy(): void {\r\n if (this.destroyed) return;\r\n this.destroyed = true;\r\n\r\n if (this.joinTimer !== null) {\r\n // tab:join hasn't fired yet — cancel it; peers don't know we existed,\r\n // so there is nothing to announce on departure either.\r\n clearTimeout(this.joinTimer);\r\n this.joinTimer = null;\r\n } else {\r\n // tab:join already fired — announce departure to peers.\r\n this.transport.send({\r\n tabId : this.tabId,\r\n event : \"tab:leave\",\r\n payload: { tabId: this.tabId },\r\n });\r\n }\r\n\r\n this.transport.destroy();\r\n this.localHandlers.clear();\r\n }\r\n\r\n /** Invokes all registered handlers for the given event with the payload. */\r\n private dispatch(event: string, payload: unknown): void {\r\n this.localHandlers.get(event)?.forEach((h) => h(payload));\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED