@vielzeug/pulse 2.0.1 → 2.1.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.
Files changed (46) hide show
  1. package/README.md +37 -42
  2. package/dist/_connection.cjs +2 -0
  3. package/dist/_connection.cjs.map +1 -0
  4. package/dist/_connection.d.ts +9 -0
  5. package/dist/_connection.d.ts.map +1 -0
  6. package/dist/_connection.js +107 -0
  7. package/dist/_connection.js.map +1 -0
  8. package/dist/channel.cjs +1 -1
  9. package/dist/channel.cjs.map +1 -1
  10. package/dist/channel.js +32 -35
  11. package/dist/channel.js.map +1 -1
  12. package/dist/heartbeat.cjs +1 -1
  13. package/dist/heartbeat.cjs.map +1 -1
  14. package/dist/heartbeat.d.ts.map +1 -1
  15. package/dist/heartbeat.js +1 -2
  16. package/dist/heartbeat.js.map +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/presence.cjs +1 -1
  20. package/dist/presence.cjs.map +1 -1
  21. package/dist/presence.d.ts.map +1 -1
  22. package/dist/presence.js +34 -35
  23. package/dist/presence.js.map +1 -1
  24. package/dist/pulse.cjs +1 -1
  25. package/dist/pulse.cjs.map +1 -1
  26. package/dist/pulse.d.ts +4 -13
  27. package/dist/pulse.d.ts.map +1 -1
  28. package/dist/pulse.iife.js +1 -1
  29. package/dist/pulse.iife.js.map +1 -1
  30. package/dist/pulse.js +1 -1
  31. package/dist/pulse.js.map +1 -1
  32. package/dist/types.d.ts +40 -69
  33. package/dist/types.d.ts.map +1 -1
  34. package/package.json +2 -2
  35. package/dist/_dev.cjs +0 -2
  36. package/dist/_dev.cjs.map +0 -1
  37. package/dist/_dev.d.ts +0 -2
  38. package/dist/_dev.d.ts.map +0 -1
  39. package/dist/_dev.js +0 -6
  40. package/dist/_dev.js.map +0 -1
  41. package/dist/reconnect.cjs +0 -2
  42. package/dist/reconnect.cjs.map +0 -1
  43. package/dist/reconnect.d.ts +0 -13
  44. package/dist/reconnect.d.ts.map +0 -1
  45. package/dist/reconnect.js +0 -32
  46. package/dist/reconnect.js.map +0 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @vielzeug/pulse
2
2
 
3
- Typed WebSocket client with channel multiplexing, room management, reactive presence, auto-reconnect, and heartbeat — built on `@vielzeug/ripple` signals.
3
+ Typed WebSocket sessions with explicit connection ownership, scoped channels, reactive presence, reconnect restoration, and heartbeat support.
4
4
 
5
5
  ## Install
6
6
 
@@ -12,58 +12,53 @@ pnpm add @vielzeug/pulse @vielzeug/ripple
12
12
 
13
13
  ```ts
14
14
  import { createPulse } from '@vielzeug/pulse';
15
- import { effect } from '@vielzeug/ripple';
16
15
 
17
- type ServerEvents = {
18
- 'chat:message': { user: string; text: string };
19
- 'user:joined': { userId: string };
16
+ type ServerEvents = { 'chat:message': { text: string } };
17
+ type ClientEvents = { 'chat:send': { text: string } };
18
+ type Channels = {
19
+ chat: {
20
+ client: { send: { text: string } };
21
+ server: { message: { text: string } };
22
+ };
20
23
  };
24
+ type Presence = { lobby: { name: string } };
21
25
 
22
- type ClientEvents = {
23
- 'chat:send': { text: string };
24
- };
25
-
26
- const pulse = createPulse<ServerEvents, ClientEvents>('wss://api.example.com/ws', {
27
- reconnect: { maxAttempts: 5 },
28
- heartbeat: true,
26
+ const pulse = createPulse<ServerEvents, ClientEvents, Channels, Presence>('wss://api.example.com/ws', {
27
+ reconnect: true,
28
+ onError: (error) => console.error(error),
29
29
  });
30
30
 
31
- // Reactive status via ripple signal
32
- effect(() => console.log('status:', pulse.status.value));
33
-
34
- // Typed server events
35
- pulse.on('chat:message', ({ user, text }) => console.log(`${user}: ${text}`));
31
+ const chat = pulse.channel('chat');
32
+ const lobby = pulse.presence('lobby');
36
33
 
37
- // Typed client messages
38
- pulse.send('chat:send', { text: 'Hello!' });
34
+ try {
35
+ await pulse.connect();
36
+ chat.send('send', { text: 'Hello!' });
37
+ lobby.update({ name: 'Ada' });
38
+ } catch (error) {
39
+ console.error('Pulse connection failed:', error);
40
+ }
39
41
 
40
- // Isolated channel namespace
41
- const notif = pulse.channel<{ alert: { level: string; msg: string } }>('notifications');
42
- notif.on('alert', ({ level, msg }) => console.log(`[${level}] ${msg}`));
42
+ pulse.dispose();
43
+ ```
43
44
 
44
- // Reactive presence tracking
45
- const lobby = pulse.presence<{ name: string; status: string }>('lobby');
46
- effect(() => console.log('online:', [...lobby.state.value.keys()]));
47
- lobby.update({ name: 'Alice', status: 'active' });
45
+ ## Key Behavior
48
46
 
49
- // Clean disposal
50
- using _ = pulse;
51
- ```
47
+ - Call `connect()` before sending, joining rooms, or publishing presence.
48
+ - Define root events, channel events, and presence state at `createPulse()` so named scopes are type-safe.
49
+ - Each `channel()` and `presence()` call returns an independent disposable scope. Server subscriptions and rooms use reference counting.
50
+ - Reconnect restores channels, desired rooms, and the last successfully published local presence state.
51
+ - `send()` throws `PulseConnectionError` while disconnected; Pulse never silently drops or buffers application messages.
52
+ - `onError` receives typed transport and protocol errors.
52
53
 
53
- ## Features
54
+ ## Migration
54
55
 
55
- - **Typed event maps** — `TServer` and `TClient` generics enforce payload types at compile time
56
- - **`on()` / `once()` / `wait()`** — persistent, one-shot, and async-await subscriptions
57
- - **`channel()`** — isolated namespaces multiplexed over the shared connection
58
- - **`presence()`** — room-scoped presence channels are memoized by room name
59
- - **`join()` / `leave()`** — room membership with server-confirmation promises
60
- - **`presence()`** — reactive `Signal<Map<memberId, T>>` with `onJoin`/`onLeave` callbacks
61
- - **Middleware** — intercept outgoing `send()` calls; omit `next()` to suppress
62
- - **Auto-reconnect** — exponential backoff, configurable `maxAttempts` and `delay`
63
- - **Heartbeat** — ping/pong keep-alive with dead-connection detection
64
- - **Reactive signals** — `pulse.status` and `pulse.rooms` are ripple `Reactive`s
65
- - **`dispose()` + `[Symbol.dispose]`** — deterministic teardown via `using` declarations
56
+ See the [Pulse migration guide](https://vielzeug.dev/pulse/migration).
66
57
 
67
58
  ## Documentation
68
59
 
69
- Full docs at [vielzeug.dev/pulse](https://vielzeug.dev/pulse/).
60
+ - [Overview](https://vielzeug.dev/pulse/)
61
+ - [Usage](https://vielzeug.dev/pulse/usage)
62
+ - [API](https://vielzeug.dev/pulse/api)
63
+ - [Examples](https://vielzeug.dev/pulse/examples)
64
+ - [Pulse 3.0 Migration](https://vielzeug.dev/pulse/migration)
@@ -0,0 +1,2 @@
1
+ const e=require("./errors.cjs"),t=require("./_utils.cjs");var n=5;function r(r,i,a,o){let s=a===!0?{}:a||void 0,c=s?s.maxAttempts??n:0,l=s?.delay,u=typeof l==`function`?l:typeof l==`number`?()=>l:t.defaultReconnectDelay,d=!1,f=!1,p=!1,m,h,g,_=new Set;function v(e){o.onStatus(e)}function y(e){for(let t of _)t.reject(e);_.clear()}function b(){for(let e of _)e.resolve();_.clear()}function x(e,t){g?.socket===e&&(g.reject(t),g=void 0)}function S(){if(h?.readyState===WebSocket.OPEN)return Promise.resolve();if(g)return g.promise;let t=new WebSocket(r,i);h=t;let n,a,s=new Promise((e,t)=>{n=e,a=t});return g={promise:s,reject:a,resolve:n,socket:t},t.onopen=()=>{if(h!==t||!f){t.close(1e3,`stale connection`);return}g?.socket===t&&(g.resolve(),g=void 0),v(`open`),o.onOpen(),b()},t.onmessage=e=>{h===t&&o.onMessage(e)},t.onerror=()=>{if(h!==t)return;let n=new e.PulseConnectionError(`WebSocket error`,r);o.onError(n),x(t,n),y(n)},t.onclose=()=>{if(h!==t)return;h=void 0,o.onClose();let n=new e.PulseConnectionError(`Connection closed`,r);if(x(t,n),!f||d){v(`closed`);return}p||C()},s}async function C(){if(p||d||!f)return;p=!0;let n=new AbortController;m=n,v(`reconnecting`);let i;for(let a=0;a<c&&f&&!d&&(await t.sleep(u(a),n.signal),!(n.signal.aborted||!f||d));a++)try{await S(),p=!1;return}catch(t){i=t instanceof e.PulseConnectionError?t:new e.PulseConnectionError(`Reconnect failed`,r)}!n.signal.aborted&&f&&!d&&(f=!1,v(`closed`),o.onError(i??new e.PulseConnectionError(`Reconnect budget exhausted`,r)),y(i??new e.PulseConnectionError(`Reconnect budget exhausted`,r))),m===n&&(m=void 0),p=!1}function w(){m?.abort(),m=void 0,p=!1}return{connect(){if(d)return Promise.reject(new e.PulseDisposedError);if(f=!0,h?.readyState===WebSocket.OPEN)return Promise.resolve();let t=new Promise((e,t)=>{_.add({reject:t,resolve:e})});return!g&&!p&&(v(`connecting`),S().catch(()=>{})),t},disconnect(t=1e3,n=``){f=!1,w(),y(new e.PulseConnectionError(`Connection closed`,r)),v(`closed`),h?.close(t,n)},dispose(){d||(d=!0,f=!1,w(),y(new e.PulseDisposedError),h?.close(1e3,`disposed`),h=void 0,g=void 0,v(`closed`))},forceReconnect(e,t){f&&h?.readyState===WebSocket.OPEN&&h.close(e,t)},get open(){return h?.readyState===WebSocket.OPEN},send(t){if(h?.readyState!==WebSocket.OPEN)throw new e.PulseConnectionError(`Connection is not open`,r);h.send(t)}}}exports.createConnection=r;
2
+ //# sourceMappingURL=_connection.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_connection.cjs","names":[],"sources":["../src/_connection.ts"],"sourcesContent":["import type { PulseStatus, ReconnectOptions } from './types';\n\nimport { defaultReconnectDelay, sleep } from './_utils';\nimport { PulseConnectionError, PulseDisposedError } from './errors';\n\ntype ConnectionCallbacks = {\n onClose(): void;\n onError(error: PulseConnectionError): void;\n onMessage(event: MessageEvent): void;\n onOpen(): void;\n onStatus(status: PulseStatus): void;\n};\n\nexport type ConnectionController = {\n connect(): Promise<void>;\n disconnect(code?: number, reason?: string): void;\n dispose(): void;\n forceReconnect(code: number, reason: string): void;\n readonly open: boolean;\n send(frame: string): void;\n};\n\ntype Opening = {\n promise: Promise<void>;\n reject(error: PulseConnectionError): void;\n resolve(): void;\n socket: WebSocket;\n};\n\nconst DEFAULT_MAX_ATTEMPTS = 5;\n\n/** Owns the one active socket and its retry loop. @internal */\nexport function createConnection(\n url: string,\n protocols: string | string[] | undefined,\n reconnect: boolean | ReconnectOptions | undefined,\n callbacks: ConnectionCallbacks,\n): ConnectionController {\n const options: ReconnectOptions | undefined = reconnect === true ? {} : reconnect || undefined;\n const maxAttempts = options ? (options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS) : 0;\n const configuredDelay = options?.delay;\n const delay: (attempt: number) => number =\n typeof configuredDelay === 'function'\n ? configuredDelay\n : typeof configuredDelay === 'number'\n ? () => configuredDelay\n : defaultReconnectDelay;\n\n let disposed = false;\n let desired = false;\n let retrying = false;\n let retryCtrl: AbortController | undefined;\n let socket: WebSocket | undefined;\n let opening: Opening | undefined;\n const waiters = new Set<{ reject(error: Error): void; resolve(): void }>();\n\n function setStatus(status: PulseStatus): void {\n callbacks.onStatus(status);\n }\n\n function rejectWaiters(error: Error): void {\n for (const waiter of waiters) waiter.reject(error);\n waiters.clear();\n }\n\n function resolveWaiters(): void {\n for (const waiter of waiters) waiter.resolve();\n waiters.clear();\n }\n\n function rejectOpening(socketToReject: WebSocket, error: PulseConnectionError): void {\n if (opening?.socket !== socketToReject) return;\n\n opening.reject(error);\n opening = undefined;\n }\n\n function openSocket(): Promise<void> {\n if (socket?.readyState === WebSocket.OPEN) return Promise.resolve();\n\n if (opening) return opening.promise;\n\n const nextSocket = new WebSocket(url, protocols);\n\n socket = nextSocket;\n\n let resolveOpening: () => void;\n let rejectOpeningPromise: (error: PulseConnectionError) => void;\n const promise = new Promise<void>((resolve, reject) => {\n resolveOpening = resolve;\n rejectOpeningPromise = reject;\n });\n\n opening = { promise, reject: rejectOpeningPromise!, resolve: resolveOpening!, socket: nextSocket };\n\n nextSocket.onopen = (): void => {\n if (socket !== nextSocket || !desired) {\n nextSocket.close(1000, 'stale connection');\n\n return;\n }\n\n if (opening?.socket === nextSocket) {\n opening.resolve();\n opening = undefined;\n }\n\n setStatus('open');\n callbacks.onOpen();\n resolveWaiters();\n };\n\n nextSocket.onmessage = (event: MessageEvent): void => {\n if (socket === nextSocket) callbacks.onMessage(event);\n };\n\n nextSocket.onerror = (): void => {\n if (socket !== nextSocket) return;\n\n const error = new PulseConnectionError('WebSocket error', url);\n\n callbacks.onError(error);\n rejectOpening(nextSocket, error);\n rejectWaiters(error);\n };\n\n nextSocket.onclose = (): void => {\n if (socket !== nextSocket) return;\n\n socket = undefined;\n callbacks.onClose();\n\n const error = new PulseConnectionError('Connection closed', url);\n\n rejectOpening(nextSocket, error);\n\n if (!desired || disposed) {\n setStatus('closed');\n\n return;\n }\n\n if (!retrying) void reconnectLoop();\n };\n\n return promise;\n }\n\n async function reconnectLoop(): Promise<void> {\n if (retrying || disposed || !desired) return;\n\n retrying = true;\n\n const controller = new AbortController();\n\n retryCtrl = controller;\n setStatus('reconnecting');\n\n let lastError: PulseConnectionError | undefined;\n\n for (let attempt = 0; attempt < maxAttempts && desired && !disposed; attempt++) {\n await sleep(delay(attempt), controller.signal);\n\n if (controller.signal.aborted || !desired || disposed) break;\n\n try {\n await openSocket();\n retrying = false;\n\n return;\n } catch (error) {\n lastError = error instanceof PulseConnectionError ? error : new PulseConnectionError('Reconnect failed', url);\n }\n }\n\n if (!controller.signal.aborted && desired && !disposed) {\n desired = false;\n setStatus('closed');\n callbacks.onError(lastError ?? new PulseConnectionError('Reconnect budget exhausted', url));\n rejectWaiters(lastError ?? new PulseConnectionError('Reconnect budget exhausted', url));\n }\n\n if (retryCtrl === controller) retryCtrl = undefined;\n\n retrying = false;\n }\n\n function cancelRetry(): void {\n retryCtrl?.abort();\n retryCtrl = undefined;\n retrying = false;\n }\n\n return {\n connect(): Promise<void> {\n if (disposed) return Promise.reject(new PulseDisposedError());\n\n desired = true;\n\n if (socket?.readyState === WebSocket.OPEN) return Promise.resolve();\n\n const connected = new Promise<void>((resolve, reject) => {\n waiters.add({ reject, resolve });\n });\n\n if (!opening && !retrying) {\n setStatus('connecting');\n void openSocket().catch(() => {});\n }\n\n return connected;\n },\n\n disconnect(code = 1000, reason = ''): void {\n desired = false;\n cancelRetry();\n rejectWaiters(new PulseConnectionError('Connection closed', url));\n setStatus('closed');\n socket?.close(code, reason);\n },\n\n dispose(): void {\n if (disposed) return;\n\n disposed = true;\n desired = false;\n cancelRetry();\n rejectWaiters(new PulseDisposedError());\n socket?.close(1000, 'disposed');\n socket = undefined;\n opening = undefined;\n setStatus('closed');\n },\n\n forceReconnect(code: number, reason: string): void {\n if (desired && socket?.readyState === WebSocket.OPEN) socket.close(code, reason);\n },\n\n get open() {\n return socket?.readyState === WebSocket.OPEN;\n },\n\n send(frame: string): void {\n if (socket?.readyState !== WebSocket.OPEN) {\n throw new PulseConnectionError('Connection is not open', url);\n }\n\n socket.send(frame);\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export type ConnectionController = {
2
+ connect(): Promise<void>;
3
+ disconnect(code?: number, reason?: string): void;
4
+ dispose(): void;
5
+ forceReconnect(code: number, reason: string): void;
6
+ readonly open: boolean;
7
+ send(frame: string): void;
8
+ };
9
+ //# sourceMappingURL=_connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_connection.d.ts","sourceRoot":"","sources":["../src/_connection.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,UAAU,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjD,OAAO,IAAI,IAAI,CAAC;IAChB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC"}
@@ -0,0 +1,107 @@
1
+ import { PulseConnectionError as e, PulseDisposedError as t } from "./errors.js";
2
+ import { defaultReconnectDelay as n, sleep as r } from "./_utils.js";
3
+ //#region src/_connection.ts
4
+ var i = 5;
5
+ function a(a, o, s, c) {
6
+ let l = s === !0 ? {} : s || void 0, u = l ? l.maxAttempts ?? i : 0, d = l?.delay, f = typeof d == "function" ? d : typeof d == "number" ? () => d : n, p = !1, m = !1, h = !1, g, _, v, y = /* @__PURE__ */ new Set();
7
+ function b(e) {
8
+ c.onStatus(e);
9
+ }
10
+ function x(e) {
11
+ for (let t of y) t.reject(e);
12
+ y.clear();
13
+ }
14
+ function S() {
15
+ for (let e of y) e.resolve();
16
+ y.clear();
17
+ }
18
+ function C(e, t) {
19
+ v?.socket === e && (v.reject(t), v = void 0);
20
+ }
21
+ function w() {
22
+ if (_?.readyState === WebSocket.OPEN) return Promise.resolve();
23
+ if (v) return v.promise;
24
+ let t = new WebSocket(a, o);
25
+ _ = t;
26
+ let n, r, i = new Promise((e, t) => {
27
+ n = e, r = t;
28
+ });
29
+ return v = {
30
+ promise: i,
31
+ reject: r,
32
+ resolve: n,
33
+ socket: t
34
+ }, t.onopen = () => {
35
+ if (_ !== t || !m) {
36
+ t.close(1e3, "stale connection");
37
+ return;
38
+ }
39
+ v?.socket === t && (v.resolve(), v = void 0), b("open"), c.onOpen(), S();
40
+ }, t.onmessage = (e) => {
41
+ _ === t && c.onMessage(e);
42
+ }, t.onerror = () => {
43
+ if (_ !== t) return;
44
+ let n = new e("WebSocket error", a);
45
+ c.onError(n), C(t, n), x(n);
46
+ }, t.onclose = () => {
47
+ if (_ !== t) return;
48
+ _ = void 0, c.onClose();
49
+ let n = new e("Connection closed", a);
50
+ if (C(t, n), !m || p) {
51
+ b("closed");
52
+ return;
53
+ }
54
+ h || T();
55
+ }, i;
56
+ }
57
+ async function T() {
58
+ if (h || p || !m) return;
59
+ h = !0;
60
+ let t = new AbortController();
61
+ g = t, b("reconnecting");
62
+ let n;
63
+ for (let i = 0; i < u && m && !p && (await r(f(i), t.signal), !(t.signal.aborted || !m || p)); i++) try {
64
+ await w(), h = !1;
65
+ return;
66
+ } catch (t) {
67
+ n = t instanceof e ? t : new e("Reconnect failed", a);
68
+ }
69
+ !t.signal.aborted && m && !p && (m = !1, b("closed"), c.onError(n ?? new e("Reconnect budget exhausted", a)), x(n ?? new e("Reconnect budget exhausted", a))), g === t && (g = void 0), h = !1;
70
+ }
71
+ function E() {
72
+ g?.abort(), g = void 0, h = !1;
73
+ }
74
+ return {
75
+ connect() {
76
+ if (p) return Promise.reject(new t());
77
+ if (m = !0, _?.readyState === WebSocket.OPEN) return Promise.resolve();
78
+ let e = new Promise((e, t) => {
79
+ y.add({
80
+ reject: t,
81
+ resolve: e
82
+ });
83
+ });
84
+ return !v && !h && (b("connecting"), w().catch(() => {})), e;
85
+ },
86
+ disconnect(t = 1e3, n = "") {
87
+ m = !1, E(), x(new e("Connection closed", a)), b("closed"), _?.close(t, n);
88
+ },
89
+ dispose() {
90
+ p || (p = !0, m = !1, E(), x(new t()), _?.close(1e3, "disposed"), _ = void 0, v = void 0, b("closed"));
91
+ },
92
+ forceReconnect(e, t) {
93
+ m && _?.readyState === WebSocket.OPEN && _.close(e, t);
94
+ },
95
+ get open() {
96
+ return _?.readyState === WebSocket.OPEN;
97
+ },
98
+ send(t) {
99
+ if (_?.readyState !== WebSocket.OPEN) throw new e("Connection is not open", a);
100
+ _.send(t);
101
+ }
102
+ };
103
+ }
104
+ //#endregion
105
+ export { a as createConnection };
106
+
107
+ //# sourceMappingURL=_connection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_connection.js","names":[],"sources":["../src/_connection.ts"],"sourcesContent":["import type { PulseStatus, ReconnectOptions } from './types';\n\nimport { defaultReconnectDelay, sleep } from './_utils';\nimport { PulseConnectionError, PulseDisposedError } from './errors';\n\ntype ConnectionCallbacks = {\n onClose(): void;\n onError(error: PulseConnectionError): void;\n onMessage(event: MessageEvent): void;\n onOpen(): void;\n onStatus(status: PulseStatus): void;\n};\n\nexport type ConnectionController = {\n connect(): Promise<void>;\n disconnect(code?: number, reason?: string): void;\n dispose(): void;\n forceReconnect(code: number, reason: string): void;\n readonly open: boolean;\n send(frame: string): void;\n};\n\ntype Opening = {\n promise: Promise<void>;\n reject(error: PulseConnectionError): void;\n resolve(): void;\n socket: WebSocket;\n};\n\nconst DEFAULT_MAX_ATTEMPTS = 5;\n\n/** Owns the one active socket and its retry loop. @internal */\nexport function createConnection(\n url: string,\n protocols: string | string[] | undefined,\n reconnect: boolean | ReconnectOptions | undefined,\n callbacks: ConnectionCallbacks,\n): ConnectionController {\n const options: ReconnectOptions | undefined = reconnect === true ? {} : reconnect || undefined;\n const maxAttempts = options ? (options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS) : 0;\n const configuredDelay = options?.delay;\n const delay: (attempt: number) => number =\n typeof configuredDelay === 'function'\n ? configuredDelay\n : typeof configuredDelay === 'number'\n ? () => configuredDelay\n : defaultReconnectDelay;\n\n let disposed = false;\n let desired = false;\n let retrying = false;\n let retryCtrl: AbortController | undefined;\n let socket: WebSocket | undefined;\n let opening: Opening | undefined;\n const waiters = new Set<{ reject(error: Error): void; resolve(): void }>();\n\n function setStatus(status: PulseStatus): void {\n callbacks.onStatus(status);\n }\n\n function rejectWaiters(error: Error): void {\n for (const waiter of waiters) waiter.reject(error);\n waiters.clear();\n }\n\n function resolveWaiters(): void {\n for (const waiter of waiters) waiter.resolve();\n waiters.clear();\n }\n\n function rejectOpening(socketToReject: WebSocket, error: PulseConnectionError): void {\n if (opening?.socket !== socketToReject) return;\n\n opening.reject(error);\n opening = undefined;\n }\n\n function openSocket(): Promise<void> {\n if (socket?.readyState === WebSocket.OPEN) return Promise.resolve();\n\n if (opening) return opening.promise;\n\n const nextSocket = new WebSocket(url, protocols);\n\n socket = nextSocket;\n\n let resolveOpening: () => void;\n let rejectOpeningPromise: (error: PulseConnectionError) => void;\n const promise = new Promise<void>((resolve, reject) => {\n resolveOpening = resolve;\n rejectOpeningPromise = reject;\n });\n\n opening = { promise, reject: rejectOpeningPromise!, resolve: resolveOpening!, socket: nextSocket };\n\n nextSocket.onopen = (): void => {\n if (socket !== nextSocket || !desired) {\n nextSocket.close(1000, 'stale connection');\n\n return;\n }\n\n if (opening?.socket === nextSocket) {\n opening.resolve();\n opening = undefined;\n }\n\n setStatus('open');\n callbacks.onOpen();\n resolveWaiters();\n };\n\n nextSocket.onmessage = (event: MessageEvent): void => {\n if (socket === nextSocket) callbacks.onMessage(event);\n };\n\n nextSocket.onerror = (): void => {\n if (socket !== nextSocket) return;\n\n const error = new PulseConnectionError('WebSocket error', url);\n\n callbacks.onError(error);\n rejectOpening(nextSocket, error);\n rejectWaiters(error);\n };\n\n nextSocket.onclose = (): void => {\n if (socket !== nextSocket) return;\n\n socket = undefined;\n callbacks.onClose();\n\n const error = new PulseConnectionError('Connection closed', url);\n\n rejectOpening(nextSocket, error);\n\n if (!desired || disposed) {\n setStatus('closed');\n\n return;\n }\n\n if (!retrying) void reconnectLoop();\n };\n\n return promise;\n }\n\n async function reconnectLoop(): Promise<void> {\n if (retrying || disposed || !desired) return;\n\n retrying = true;\n\n const controller = new AbortController();\n\n retryCtrl = controller;\n setStatus('reconnecting');\n\n let lastError: PulseConnectionError | undefined;\n\n for (let attempt = 0; attempt < maxAttempts && desired && !disposed; attempt++) {\n await sleep(delay(attempt), controller.signal);\n\n if (controller.signal.aborted || !desired || disposed) break;\n\n try {\n await openSocket();\n retrying = false;\n\n return;\n } catch (error) {\n lastError = error instanceof PulseConnectionError ? error : new PulseConnectionError('Reconnect failed', url);\n }\n }\n\n if (!controller.signal.aborted && desired && !disposed) {\n desired = false;\n setStatus('closed');\n callbacks.onError(lastError ?? new PulseConnectionError('Reconnect budget exhausted', url));\n rejectWaiters(lastError ?? new PulseConnectionError('Reconnect budget exhausted', url));\n }\n\n if (retryCtrl === controller) retryCtrl = undefined;\n\n retrying = false;\n }\n\n function cancelRetry(): void {\n retryCtrl?.abort();\n retryCtrl = undefined;\n retrying = false;\n }\n\n return {\n connect(): Promise<void> {\n if (disposed) return Promise.reject(new PulseDisposedError());\n\n desired = true;\n\n if (socket?.readyState === WebSocket.OPEN) return Promise.resolve();\n\n const connected = new Promise<void>((resolve, reject) => {\n waiters.add({ reject, resolve });\n });\n\n if (!opening && !retrying) {\n setStatus('connecting');\n void openSocket().catch(() => {});\n }\n\n return connected;\n },\n\n disconnect(code = 1000, reason = ''): void {\n desired = false;\n cancelRetry();\n rejectWaiters(new PulseConnectionError('Connection closed', url));\n setStatus('closed');\n socket?.close(code, reason);\n },\n\n dispose(): void {\n if (disposed) return;\n\n disposed = true;\n desired = false;\n cancelRetry();\n rejectWaiters(new PulseDisposedError());\n socket?.close(1000, 'disposed');\n socket = undefined;\n opening = undefined;\n setStatus('closed');\n },\n\n forceReconnect(code: number, reason: string): void {\n if (desired && socket?.readyState === WebSocket.OPEN) socket.close(code, reason);\n },\n\n get open() {\n return socket?.readyState === WebSocket.OPEN;\n },\n\n send(frame: string): void {\n if (socket?.readyState !== WebSocket.OPEN) {\n throw new PulseConnectionError('Connection is not open', url);\n }\n\n socket.send(frame);\n },\n };\n}\n"],"mappings":"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"}
package/dist/channel.cjs CHANGED
@@ -1,2 +1,2 @@
1
- require("./_dev.cjs");const e=require("./_utils.cjs"),t=require("./_wait.cjs");function n(n,r,i,a,o){let s=e.deriveAbortController(a),c=new Set,l=s.signal.aborted;return s.signal.addEventListener(`abort`,()=>{l=!0},{once:!0}),{get disposalSignal(){return s.signal},dispose(){if(!l){l=!0,s.abort();for(let e of c)e();c.clear(),o?.()}},get disposed(){return l},get name(){return n},on(e,t){if(l)return`${String(e)}${n}`,()=>{};let r=i(n,e,t);return c.add(r),()=>{r(),c.delete(r)}},once(e,t){if(l)return`${String(e)}${n}`,()=>{};let r=()=>{};return r=i(n,e,e=>{c.delete(r),r(),t(e)}),c.add(r),()=>{c.delete(r),r()}},send(e,t){if(l){`${String(e)}${n}`;return}r(n,e,t)},[Symbol.dispose](){this.dispose()},wait(e,r){return t.createWaitPromise(e,s.signal,r,(e,t)=>i(n,e,t))}}}exports.createChannel=n;
1
+ const e=require("./errors.cjs"),t=require("./_utils.cjs"),n=require("./_wait.cjs");function r(r,i,a,o,s){let c=t.deriveAbortController(o),l=new Set,u=c.signal.aborted;return c.signal.addEventListener(`abort`,()=>{u=!0},{once:!0}),{get disposalSignal(){return c.signal},dispose(){if(!u){u=!0,c.abort();for(let e of l)e();l.clear(),s?.()}},get disposed(){return u},get name(){return r},on(t,n){if(u)throw new e.PulseDisposedError(`Channel "${r}"`);let i=a(r,t,n);return l.add(i),()=>{i(),l.delete(i)}},once(t,n){if(u)throw new e.PulseDisposedError(`Channel "${r}"`);let i=()=>{};return i=a(r,t,e=>{l.delete(i),i(),n(e)}),l.add(i),()=>{l.delete(i),i()}},send(t,n){if(u)throw new e.PulseDisposedError(`Channel "${r}"`);i(r,t,n)},[Symbol.dispose](){this.dispose()},wait(e,t){return n.createWaitPromise(e,c.signal,t,(e,t)=>a(r,e,t))}}}exports.createChannel=r;
2
2
  //# sourceMappingURL=channel.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"channel.cjs","names":[],"sources":["../src/channel.ts"],"sourcesContent":["import type { EventKey, MessageMap, PulseChannel, Unsubscribe } from './types';\n\nimport { warn } from './_dev';\nimport { deriveAbortController } from './_utils';\nimport { createWaitPromise } from './_wait';\n\ntype SendFn = (channel: string, event: string, payload: unknown) => void;\ntype SubscribeFn = (channel: string, event: string, handler: (payload: unknown) => void) => () => void;\n\n/**\n * Create a typed channel object that scopes messaging to a named namespace.\n * @internal\n */\nexport function createChannel<TServer extends MessageMap, TClient extends MessageMap>(\n name: string,\n send: SendFn,\n subscribe: SubscribeFn,\n disposalSignal: AbortSignal,\n onDispose?: () => void,\n): PulseChannel<TServer, TClient> {\n const ctrl = deriveAbortController(disposalSignal);\n const tracked = new Set<() => void>();\n\n let disposed = ctrl.signal.aborted;\n\n ctrl.signal.addEventListener(\n 'abort',\n () => {\n disposed = true;\n },\n { once: true },\n );\n\n const channel: PulseChannel<TServer, TClient> = {\n get disposalSignal() {\n return ctrl.signal;\n },\n\n dispose() {\n if (disposed) return;\n\n disposed = true;\n ctrl.abort();\n\n for (const unsub of tracked) unsub();\n\n tracked.clear();\n onDispose?.();\n },\n\n get disposed() {\n return disposed;\n },\n\n get name() {\n return name;\n },\n\n on<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe {\n if (disposed) {\n warn(`on('${String(event)}') called on a disposed channel '${name}' — listener ignored`);\n\n return () => {};\n }\n\n const unsub = subscribe(name, event, handler as (payload: unknown) => void);\n\n tracked.add(unsub);\n\n return () => {\n unsub();\n tracked.delete(unsub);\n };\n },\n\n once<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe {\n if (disposed) {\n warn(`once('${String(event)}') called on a disposed channel '${name}' — listener ignored`);\n\n return () => {};\n }\n\n let unsub: Unsubscribe = () => {};\n\n const wrapped = (payload: unknown): void => {\n tracked.delete(unsub);\n unsub();\n (handler as (payload: unknown) => void)(payload);\n };\n\n unsub = subscribe(name, event, wrapped);\n tracked.add(unsub);\n\n return () => {\n tracked.delete(unsub);\n unsub();\n };\n },\n\n send<K extends EventKey<TClient>>(event: K, payload: TClient[K]): void {\n if (disposed) {\n warn(`send('${String(event)}') called on a disposed channel '${name}' — message dropped`);\n\n return;\n }\n\n send(name, event, payload);\n },\n\n [Symbol.dispose]() {\n this.dispose();\n },\n\n wait<K extends EventKey<TServer>>(\n event: K,\n opts?: { signal?: AbortSignal; timeout?: number },\n ): Promise<TServer[K]> {\n return createWaitPromise<TServer[K]>(event as string, ctrl.signal, opts, (ev, handler) =>\n subscribe(name, ev, handler),\n );\n },\n };\n\n return channel;\n}\n"],"mappings":"+EAaA,SAAgB,EACd,EACA,EACA,EACA,EACA,EACgC,CAChC,IAAM,EAAO,EAAA,sBAAsB,CAAc,EAC3C,EAAU,IAAI,IAEhB,EAAW,EAAK,OAAO,QAoG3B,OAlGA,EAAK,OAAO,iBACV,YACM,CACJ,EAAW,EACb,EACA,CAAE,KAAM,EAAK,CACf,EA4FO,CAzFL,IAAI,gBAAiB,CACnB,OAAO,EAAK,MACd,EAEA,SAAU,CACJ,MAGJ,CADA,EAAW,GACX,EAAK,MAAM,EAEX,IAAK,IAAM,KAAS,EAAS,EAAM,EAEnC,EAAQ,MAAM,EACd,IAAY,CALD,CAMb,EAEA,IAAI,UAAW,CACb,OAAO,CACT,EAEA,IAAI,MAAO,CACT,OAAO,CACT,EAEA,GAAgC,EAAU,EAAqD,CAC7F,GAAI,EAGF,MAFK,GAAO,OAAO,CAAK,EAAnB,EAAwD,EAAxD,MAEQ,CAAC,EAGhB,IAAM,EAAQ,EAAU,EAAM,EAAO,CAAqC,EAI1E,OAFA,EAAQ,IAAI,CAAK,MAEJ,CACX,EAAM,EACN,EAAQ,OAAO,CAAK,CACtB,CACF,EAEA,KAAkC,EAAU,EAAqD,CAC/F,GAAI,EAGF,MAFK,GAAS,OAAO,CAAK,EAArB,EAA0D,EAA1D,MAEQ,CAAC,EAGhB,IAAI,MAA2B,CAAC,EAWhC,MAHA,GAAQ,EAAU,EAAM,EANP,GAA2B,CAC1C,EAAQ,OAAO,CAAK,EACpB,EAAM,EACN,EAAwC,CAAO,CACjD,CAEsC,EACtC,EAAQ,IAAI,CAAK,MAEJ,CACX,EAAQ,OAAO,CAAK,EACpB,EAAM,CACR,CACF,EAEA,KAAkC,EAAU,EAA2B,CACrE,GAAI,EAAU,CACP,GAAS,OAAO,CAAK,EAArB,EAA0D,EAA1D,EAEL,MACF,CAEA,EAAK,EAAM,EAAO,CAAO,CAC3B,EAEA,CAAC,OAAO,UAAW,CACjB,KAAK,QAAQ,CACf,EAEA,KACE,EACA,EACqB,CACrB,OAAO,EAAA,kBAA8B,EAAiB,EAAK,OAAQ,GAAO,EAAI,IAC5E,EAAU,EAAM,EAAI,CAAO,CAC7B,CACF,CAGK,CACT"}
1
+ {"version":3,"file":"channel.cjs","names":[],"sources":["../src/channel.ts"],"sourcesContent":["import type { EventKey, MessageMap, PulseChannel, Unsubscribe } from './types';\n\nimport { deriveAbortController } from './_utils';\nimport { createWaitPromise } from './_wait';\nimport { PulseDisposedError } from './errors';\n\ntype SendFn = (channel: string, event: string, payload: unknown) => void;\ntype SubscribeFn = (channel: string, event: string, handler: (payload: unknown) => void) => () => void;\n\n/**\n * Create a typed channel object that scopes messaging to a named namespace.\n * @internal\n */\nexport function createChannel<TServer extends MessageMap, TClient extends MessageMap>(\n name: string,\n send: SendFn,\n subscribe: SubscribeFn,\n disposalSignal: AbortSignal,\n onDispose?: () => void,\n): PulseChannel<TServer, TClient> {\n const ctrl = deriveAbortController(disposalSignal);\n const tracked = new Set<() => void>();\n\n let disposed = ctrl.signal.aborted;\n\n ctrl.signal.addEventListener(\n 'abort',\n () => {\n disposed = true;\n },\n { once: true },\n );\n\n const channel: PulseChannel<TServer, TClient> = {\n get disposalSignal() {\n return ctrl.signal;\n },\n\n dispose() {\n if (disposed) return;\n\n disposed = true;\n ctrl.abort();\n\n for (const unsub of tracked) unsub();\n\n tracked.clear();\n onDispose?.();\n },\n\n get disposed() {\n return disposed;\n },\n\n get name() {\n return name;\n },\n\n on<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe {\n if (disposed) {\n throw new PulseDisposedError(`Channel \"${name}\"`);\n }\n\n const unsub = subscribe(name, event, handler as (payload: unknown) => void);\n\n tracked.add(unsub);\n\n return () => {\n unsub();\n tracked.delete(unsub);\n };\n },\n\n once<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe {\n if (disposed) {\n throw new PulseDisposedError(`Channel \"${name}\"`);\n }\n\n let unsub: Unsubscribe = () => {};\n\n const wrapped = (payload: unknown): void => {\n tracked.delete(unsub);\n unsub();\n (handler as (payload: unknown) => void)(payload);\n };\n\n unsub = subscribe(name, event, wrapped);\n tracked.add(unsub);\n\n return () => {\n tracked.delete(unsub);\n unsub();\n };\n },\n\n send<K extends EventKey<TClient>>(event: K, payload: TClient[K]): void {\n if (disposed) {\n throw new PulseDisposedError(`Channel \"${name}\"`);\n }\n\n send(name, event, payload);\n },\n\n [Symbol.dispose]() {\n this.dispose();\n },\n\n wait<K extends EventKey<TServer>>(\n event: K,\n opts?: { signal?: AbortSignal; timeout?: number },\n ): Promise<TServer[K]> {\n return createWaitPromise<TServer[K]>(event as string, ctrl.signal, opts, (ev, handler) =>\n subscribe(name, ev, handler),\n );\n },\n };\n\n return channel;\n}\n"],"mappings":"mFAaA,SAAgB,EACd,EACA,EACA,EACA,EACA,EACgC,CAChC,IAAM,EAAO,EAAA,sBAAsB,CAAc,EAC3C,EAAU,IAAI,IAEhB,EAAW,EAAK,OAAO,QA8F3B,OA5FA,EAAK,OAAO,iBACV,YACM,CACJ,EAAW,EACb,EACA,CAAE,KAAM,EAAK,CACf,EAsFO,CAnFL,IAAI,gBAAiB,CACnB,OAAO,EAAK,MACd,EAEA,SAAU,CACJ,MAGJ,CADA,EAAW,GACX,EAAK,MAAM,EAEX,IAAK,IAAM,KAAS,EAAS,EAAM,EAEnC,EAAQ,MAAM,EACd,IAAY,CALD,CAMb,EAEA,IAAI,UAAW,CACb,OAAO,CACT,EAEA,IAAI,MAAO,CACT,OAAO,CACT,EAEA,GAAgC,EAAU,EAAqD,CAC7F,GAAI,EACF,MAAM,IAAI,EAAA,mBAAmB,YAAY,EAAK,EAAE,EAGlD,IAAM,EAAQ,EAAU,EAAM,EAAO,CAAqC,EAI1E,OAFA,EAAQ,IAAI,CAAK,MAEJ,CACX,EAAM,EACN,EAAQ,OAAO,CAAK,CACtB,CACF,EAEA,KAAkC,EAAU,EAAqD,CAC/F,GAAI,EACF,MAAM,IAAI,EAAA,mBAAmB,YAAY,EAAK,EAAE,EAGlD,IAAI,MAA2B,CAAC,EAWhC,MAHA,GAAQ,EAAU,EAAM,EANP,GAA2B,CAC1C,EAAQ,OAAO,CAAK,EACpB,EAAM,EACN,EAAwC,CAAO,CACjD,CAEsC,EACtC,EAAQ,IAAI,CAAK,MAEJ,CACX,EAAQ,OAAO,CAAK,EACpB,EAAM,CACR,CACF,EAEA,KAAkC,EAAU,EAA2B,CACrE,GAAI,EACF,MAAM,IAAI,EAAA,mBAAmB,YAAY,EAAK,EAAE,EAGlD,EAAK,EAAM,EAAO,CAAO,CAC3B,EAEA,CAAC,OAAO,UAAW,CACjB,KAAK,QAAQ,CACf,EAEA,KACE,EACA,EACqB,CACrB,OAAO,EAAA,kBAA8B,EAAiB,EAAK,OAAQ,GAAO,EAAI,IAC5E,EAAU,EAAM,EAAI,CAAO,CAC7B,CACF,CAGK,CACT"}
package/dist/channel.js CHANGED
@@ -1,60 +1,57 @@
1
- import "./_dev.js";
2
- import { deriveAbortController as e } from "./_utils.js";
3
- import { createWaitPromise as t } from "./_wait.js";
1
+ import { PulseDisposedError as e } from "./errors.js";
2
+ import { deriveAbortController as t } from "./_utils.js";
3
+ import { createWaitPromise as n } from "./_wait.js";
4
4
  //#region src/channel.ts
5
- function n(n, r, i, a, o) {
6
- let s = e(a), c = /* @__PURE__ */ new Set(), l = s.signal.aborted;
7
- return s.signal.addEventListener("abort", () => {
8
- l = !0;
5
+ function r(r, i, a, o, s) {
6
+ let c = t(o), l = /* @__PURE__ */ new Set(), u = c.signal.aborted;
7
+ return c.signal.addEventListener("abort", () => {
8
+ u = !0;
9
9
  }, { once: !0 }), {
10
10
  get disposalSignal() {
11
- return s.signal;
11
+ return c.signal;
12
12
  },
13
13
  dispose() {
14
- if (!l) {
15
- l = !0, s.abort();
16
- for (let e of c) e();
17
- c.clear(), o?.();
14
+ if (!u) {
15
+ u = !0, c.abort();
16
+ for (let e of l) e();
17
+ l.clear(), s?.();
18
18
  }
19
19
  },
20
20
  get disposed() {
21
- return l;
21
+ return u;
22
22
  },
23
23
  get name() {
24
- return n;
24
+ return r;
25
25
  },
26
- on(e, t) {
27
- if (l) return `${String(e)}${n}`, () => {};
28
- let r = i(n, e, t);
29
- return c.add(r), () => {
30
- r(), c.delete(r);
26
+ on(t, n) {
27
+ if (u) throw new e(`Channel "${r}"`);
28
+ let i = a(r, t, n);
29
+ return l.add(i), () => {
30
+ i(), l.delete(i);
31
31
  };
32
32
  },
33
- once(e, t) {
34
- if (l) return `${String(e)}${n}`, () => {};
35
- let r = () => {};
36
- return r = i(n, e, (e) => {
37
- c.delete(r), r(), t(e);
38
- }), c.add(r), () => {
39
- c.delete(r), r();
33
+ once(t, n) {
34
+ if (u) throw new e(`Channel "${r}"`);
35
+ let i = () => {};
36
+ return i = a(r, t, (e) => {
37
+ l.delete(i), i(), n(e);
38
+ }), l.add(i), () => {
39
+ l.delete(i), i();
40
40
  };
41
41
  },
42
- send(e, t) {
43
- if (l) {
44
- `${String(e)}${n}`;
45
- return;
46
- }
47
- r(n, e, t);
42
+ send(t, n) {
43
+ if (u) throw new e(`Channel "${r}"`);
44
+ i(r, t, n);
48
45
  },
49
46
  [Symbol.dispose]() {
50
47
  this.dispose();
51
48
  },
52
- wait(e, r) {
53
- return t(e, s.signal, r, (e, t) => i(n, e, t));
49
+ wait(e, t) {
50
+ return n(e, c.signal, t, (e, t) => a(r, e, t));
54
51
  }
55
52
  };
56
53
  }
57
54
  //#endregion
58
- export { n as createChannel };
55
+ export { r as createChannel };
59
56
 
60
57
  //# sourceMappingURL=channel.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"channel.js","names":[],"sources":["../src/channel.ts"],"sourcesContent":["import type { EventKey, MessageMap, PulseChannel, Unsubscribe } from './types';\n\nimport { warn } from './_dev';\nimport { deriveAbortController } from './_utils';\nimport { createWaitPromise } from './_wait';\n\ntype SendFn = (channel: string, event: string, payload: unknown) => void;\ntype SubscribeFn = (channel: string, event: string, handler: (payload: unknown) => void) => () => void;\n\n/**\n * Create a typed channel object that scopes messaging to a named namespace.\n * @internal\n */\nexport function createChannel<TServer extends MessageMap, TClient extends MessageMap>(\n name: string,\n send: SendFn,\n subscribe: SubscribeFn,\n disposalSignal: AbortSignal,\n onDispose?: () => void,\n): PulseChannel<TServer, TClient> {\n const ctrl = deriveAbortController(disposalSignal);\n const tracked = new Set<() => void>();\n\n let disposed = ctrl.signal.aborted;\n\n ctrl.signal.addEventListener(\n 'abort',\n () => {\n disposed = true;\n },\n { once: true },\n );\n\n const channel: PulseChannel<TServer, TClient> = {\n get disposalSignal() {\n return ctrl.signal;\n },\n\n dispose() {\n if (disposed) return;\n\n disposed = true;\n ctrl.abort();\n\n for (const unsub of tracked) unsub();\n\n tracked.clear();\n onDispose?.();\n },\n\n get disposed() {\n return disposed;\n },\n\n get name() {\n return name;\n },\n\n on<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe {\n if (disposed) {\n warn(`on('${String(event)}') called on a disposed channel '${name}' — listener ignored`);\n\n return () => {};\n }\n\n const unsub = subscribe(name, event, handler as (payload: unknown) => void);\n\n tracked.add(unsub);\n\n return () => {\n unsub();\n tracked.delete(unsub);\n };\n },\n\n once<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe {\n if (disposed) {\n warn(`once('${String(event)}') called on a disposed channel '${name}' — listener ignored`);\n\n return () => {};\n }\n\n let unsub: Unsubscribe = () => {};\n\n const wrapped = (payload: unknown): void => {\n tracked.delete(unsub);\n unsub();\n (handler as (payload: unknown) => void)(payload);\n };\n\n unsub = subscribe(name, event, wrapped);\n tracked.add(unsub);\n\n return () => {\n tracked.delete(unsub);\n unsub();\n };\n },\n\n send<K extends EventKey<TClient>>(event: K, payload: TClient[K]): void {\n if (disposed) {\n warn(`send('${String(event)}') called on a disposed channel '${name}' — message dropped`);\n\n return;\n }\n\n send(name, event, payload);\n },\n\n [Symbol.dispose]() {\n this.dispose();\n },\n\n wait<K extends EventKey<TServer>>(\n event: K,\n opts?: { signal?: AbortSignal; timeout?: number },\n ): Promise<TServer[K]> {\n return createWaitPromise<TServer[K]>(event as string, ctrl.signal, opts, (ev, handler) =>\n subscribe(name, ev, handler),\n );\n },\n };\n\n return channel;\n}\n"],"mappings":";;;;AAaA,SAAgB,EACd,GACA,GACA,GACA,GACA,GACgC;CAChC,IAAM,IAAO,EAAsB,CAAc,GAC3C,oBAAU,IAAI,IAAgB,GAEhC,IAAW,EAAK,OAAO;CAoG3B,OAlGA,EAAK,OAAO,iBACV,eACM;EACJ,IAAW;CACb,GACA,EAAE,MAAM,GAAK,CACf,GA4FO;EAzFL,IAAI,iBAAiB;GACnB,OAAO,EAAK;EACd;EAEA,UAAU;GACJ,QAGJ;IADA,IAAW,IACX,EAAK,MAAM;IAEX,KAAK,IAAM,KAAS,GAAS,EAAM;IAGnC,AADA,EAAQ,MAAM,GACd,IAAY;GALD;EAMb;EAEA,IAAI,WAAW;GACb,OAAO;EACT;EAEA,IAAI,OAAO;GACT,OAAO;EACT;EAEA,GAAgC,GAAU,GAAqD;GAC7F,IAAI,GAGF,OAFK,GAAO,OAAO,CAAK,EAAnB,EAAwD,EAAxD,SAEQ,CAAC;GAGhB,IAAM,IAAQ,EAAU,GAAM,GAAO,CAAqC;GAI1E,OAFA,EAAQ,IAAI,CAAK,SAEJ;IAEX,AADA,EAAM,GACN,EAAQ,OAAO,CAAK;GACtB;EACF;EAEA,KAAkC,GAAU,GAAqD;GAC/F,IAAI,GAGF,OAFK,GAAS,OAAO,CAAK,EAArB,EAA0D,EAA1D,SAEQ,CAAC;GAGhB,IAAI,UAA2B,CAAC;GAWhC,OAHA,IAAQ,EAAU,GAAM,IANP,MAA2B;IAG1C,AAFA,EAAQ,OAAO,CAAK,GACpB,EAAM,GACN,EAAwC,CAAO;GACjD,CAEsC,GACtC,EAAQ,IAAI,CAAK,SAEJ;IAEX,AADA,EAAQ,OAAO,CAAK,GACpB,EAAM;GACR;EACF;EAEA,KAAkC,GAAU,GAA2B;GACrE,IAAI,GAAU;IACZ,AAAK,GAAS,OAAO,CAAK,EAArB,EAA0D,EAA1D;IAEL;GACF;GAEA,EAAK,GAAM,GAAO,CAAO;EAC3B;EAEA,CAAC,OAAO,WAAW;GACjB,KAAK,QAAQ;EACf;EAEA,KACE,GACA,GACqB;GACrB,OAAO,EAA8B,GAAiB,EAAK,QAAQ,IAAO,GAAI,MAC5E,EAAU,GAAM,GAAI,CAAO,CAC7B;EACF;CAGK;AACT"}
1
+ {"version":3,"file":"channel.js","names":[],"sources":["../src/channel.ts"],"sourcesContent":["import type { EventKey, MessageMap, PulseChannel, Unsubscribe } from './types';\n\nimport { deriveAbortController } from './_utils';\nimport { createWaitPromise } from './_wait';\nimport { PulseDisposedError } from './errors';\n\ntype SendFn = (channel: string, event: string, payload: unknown) => void;\ntype SubscribeFn = (channel: string, event: string, handler: (payload: unknown) => void) => () => void;\n\n/**\n * Create a typed channel object that scopes messaging to a named namespace.\n * @internal\n */\nexport function createChannel<TServer extends MessageMap, TClient extends MessageMap>(\n name: string,\n send: SendFn,\n subscribe: SubscribeFn,\n disposalSignal: AbortSignal,\n onDispose?: () => void,\n): PulseChannel<TServer, TClient> {\n const ctrl = deriveAbortController(disposalSignal);\n const tracked = new Set<() => void>();\n\n let disposed = ctrl.signal.aborted;\n\n ctrl.signal.addEventListener(\n 'abort',\n () => {\n disposed = true;\n },\n { once: true },\n );\n\n const channel: PulseChannel<TServer, TClient> = {\n get disposalSignal() {\n return ctrl.signal;\n },\n\n dispose() {\n if (disposed) return;\n\n disposed = true;\n ctrl.abort();\n\n for (const unsub of tracked) unsub();\n\n tracked.clear();\n onDispose?.();\n },\n\n get disposed() {\n return disposed;\n },\n\n get name() {\n return name;\n },\n\n on<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe {\n if (disposed) {\n throw new PulseDisposedError(`Channel \"${name}\"`);\n }\n\n const unsub = subscribe(name, event, handler as (payload: unknown) => void);\n\n tracked.add(unsub);\n\n return () => {\n unsub();\n tracked.delete(unsub);\n };\n },\n\n once<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe {\n if (disposed) {\n throw new PulseDisposedError(`Channel \"${name}\"`);\n }\n\n let unsub: Unsubscribe = () => {};\n\n const wrapped = (payload: unknown): void => {\n tracked.delete(unsub);\n unsub();\n (handler as (payload: unknown) => void)(payload);\n };\n\n unsub = subscribe(name, event, wrapped);\n tracked.add(unsub);\n\n return () => {\n tracked.delete(unsub);\n unsub();\n };\n },\n\n send<K extends EventKey<TClient>>(event: K, payload: TClient[K]): void {\n if (disposed) {\n throw new PulseDisposedError(`Channel \"${name}\"`);\n }\n\n send(name, event, payload);\n },\n\n [Symbol.dispose]() {\n this.dispose();\n },\n\n wait<K extends EventKey<TServer>>(\n event: K,\n opts?: { signal?: AbortSignal; timeout?: number },\n ): Promise<TServer[K]> {\n return createWaitPromise<TServer[K]>(event as string, ctrl.signal, opts, (ev, handler) =>\n subscribe(name, ev, handler),\n );\n },\n };\n\n return channel;\n}\n"],"mappings":";;;;AAaA,SAAgB,EACd,GACA,GACA,GACA,GACA,GACgC;CAChC,IAAM,IAAO,EAAsB,CAAc,GAC3C,oBAAU,IAAI,IAAgB,GAEhC,IAAW,EAAK,OAAO;CA8F3B,OA5FA,EAAK,OAAO,iBACV,eACM;EACJ,IAAW;CACb,GACA,EAAE,MAAM,GAAK,CACf,GAsFO;EAnFL,IAAI,iBAAiB;GACnB,OAAO,EAAK;EACd;EAEA,UAAU;GACJ,QAGJ;IADA,IAAW,IACX,EAAK,MAAM;IAEX,KAAK,IAAM,KAAS,GAAS,EAAM;IAGnC,AADA,EAAQ,MAAM,GACd,IAAY;GALD;EAMb;EAEA,IAAI,WAAW;GACb,OAAO;EACT;EAEA,IAAI,OAAO;GACT,OAAO;EACT;EAEA,GAAgC,GAAU,GAAqD;GAC7F,IAAI,GACF,MAAM,IAAI,EAAmB,YAAY,EAAK,EAAE;GAGlD,IAAM,IAAQ,EAAU,GAAM,GAAO,CAAqC;GAI1E,OAFA,EAAQ,IAAI,CAAK,SAEJ;IAEX,AADA,EAAM,GACN,EAAQ,OAAO,CAAK;GACtB;EACF;EAEA,KAAkC,GAAU,GAAqD;GAC/F,IAAI,GACF,MAAM,IAAI,EAAmB,YAAY,EAAK,EAAE;GAGlD,IAAI,UAA2B,CAAC;GAWhC,OAHA,IAAQ,EAAU,GAAM,IANP,MAA2B;IAG1C,AAFA,EAAQ,OAAO,CAAK,GACpB,EAAM,GACN,EAAwC,CAAO;GACjD,CAEsC,GACtC,EAAQ,IAAI,CAAK,SAEJ;IAEX,AADA,EAAQ,OAAO,CAAK,GACpB,EAAM;GACR;EACF;EAEA,KAAkC,GAAU,GAA2B;GACrE,IAAI,GACF,MAAM,IAAI,EAAmB,YAAY,EAAK,EAAE;GAGlD,EAAK,GAAM,GAAO,CAAO;EAC3B;EAEA,CAAC,OAAO,WAAW;GACjB,KAAK,QAAQ;EACf;EAEA,KACE,GACA,GACqB;GACrB,OAAO,EAA8B,GAAiB,EAAK,QAAQ,IAAO,GAAI,MAC5E,EAAU,GAAM,GAAI,CAAO,CAC7B;EACF;CAGK;AACT"}
@@ -1,2 +1,2 @@
1
- require("./_dev.cjs");const e=require("./protocol.cjs");var t=3e4,n=5e3;function r(r,i,a){if(!r)return{onPong(){},start(){},stop(){}};let o=r===!0?{}:r,s=o.interval??t,c=o.timeout??n,l,u,d=!1;function f(){l=setTimeout(()=>{i(e.encode({ts:Date.now(),type:`ping`})),u=setTimeout(()=>{`${c}`,a()},c)},s)}return{onPong(){clearTimeout(u),u=void 0,d&&f()},start(){d=!0,clearTimeout(l),clearTimeout(u),u=void 0,f()},stop(){d=!1,clearTimeout(l),clearTimeout(u),l=void 0,u=void 0}}}exports.createHeartbeat=r;
1
+ const e=require("./protocol.cjs");var t=3e4,n=5e3;function r(r,i,a){if(!r)return{onPong(){},start(){},stop(){}};let o=r===!0?{}:r,s=o.interval??t,c=o.timeout??n,l,u,d=!1;function f(){l=setTimeout(()=>{i(e.encode({ts:Date.now(),type:`ping`})),u=setTimeout(()=>{a()},c)},s)}return{onPong(){clearTimeout(u),u=void 0,d&&f()},start(){d=!0,clearTimeout(l),clearTimeout(u),u=void 0,f()},stop(){d=!1,clearTimeout(l),clearTimeout(u),l=void 0,u=void 0}}}exports.createHeartbeat=r;
2
2
  //# sourceMappingURL=heartbeat.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"heartbeat.cjs","names":[],"sources":["../src/heartbeat.ts"],"sourcesContent":["import type { HeartbeatOptions } from './types';\n\nimport { warn } from './_dev';\nimport { encode } from './protocol';\n\nconst DEFAULT_INTERVAL = 30_000;\nconst DEFAULT_TIMEOUT = 5_000;\n\nexport type HeartbeatHandle = {\n /** Called when a `pong` frame is received. Resets the timeout timer. */\n onPong(): void;\n /** Start the heartbeat loop. */\n start(): void;\n /** Stop the heartbeat loop and cancel any pending timers. */\n stop(): void;\n};\n\n/**\n * Create a heartbeat manager that sends periodic pings and expects pong replies.\n * Calls `onDead` when a pong is not received within the timeout window.\n * @internal\n */\nexport function createHeartbeat(\n opts: boolean | HeartbeatOptions | undefined,\n send: (frame: string) => void,\n onDead: () => void,\n): HeartbeatHandle {\n if (!opts) {\n return { onPong() {}, start() {}, stop() {} };\n }\n\n const options: HeartbeatOptions = opts === true ? {} : opts;\n const interval = options.interval ?? DEFAULT_INTERVAL;\n const timeout = options.timeout ?? DEFAULT_TIMEOUT;\n\n let pingTimer: ReturnType<typeof setTimeout> | undefined;\n let pongTimer: ReturnType<typeof setTimeout> | undefined;\n let running = false;\n\n function schedulePing(): void {\n pingTimer = setTimeout(() => {\n const ts = Date.now();\n\n send(encode({ ts, type: 'ping' }));\n\n pongTimer = setTimeout(() => {\n warn(`Heartbeat timed out after ${timeout}ms — treating connection as dead`);\n onDead();\n }, timeout);\n }, interval);\n }\n\n return {\n onPong() {\n clearTimeout(pongTimer);\n pongTimer = undefined;\n\n if (running) schedulePing();\n },\n\n start() {\n running = true;\n clearTimeout(pingTimer);\n clearTimeout(pongTimer);\n pongTimer = undefined;\n schedulePing();\n },\n\n stop() {\n running = false;\n clearTimeout(pingTimer);\n clearTimeout(pongTimer);\n pingTimer = undefined;\n pongTimer = undefined;\n },\n };\n}\n"],"mappings":"wDAKA,IAAM,EAAmB,IACnB,EAAkB,IAgBxB,SAAgB,EACd,EACA,EACA,EACiB,CACjB,GAAI,CAAC,EACH,MAAO,CAAE,QAAS,CAAC,EAAG,OAAQ,CAAC,EAAG,MAAO,CAAC,CAAE,EAG9C,IAAM,EAA4B,IAAS,GAAO,CAAC,EAAI,EACjD,EAAW,EAAQ,UAAY,EAC/B,EAAU,EAAQ,SAAW,EAE/B,EACA,EACA,EAAU,GAEd,SAAS,GAAqB,CAC5B,EAAY,eAAiB,CAG3B,EAAK,EAAA,OAAO,CAAE,GAFH,KAAK,IAEF,EAAI,KAAM,MAAO,CAAC,CAAC,EAEjC,EAAY,eAAiB,CACtB,GAA6B,EAA7B,EACL,EAAO,CACT,EAAG,CAAO,CACZ,EAAG,CAAQ,CACb,CAEA,MAAO,CACL,QAAS,CACP,aAAa,CAAS,EACtB,EAAY,IAAA,GAER,GAAS,EAAa,CAC5B,EAEA,OAAQ,CACN,EAAU,GACV,aAAa,CAAS,EACtB,aAAa,CAAS,EACtB,EAAY,IAAA,GACZ,EAAa,CACf,EAEA,MAAO,CACL,EAAU,GACV,aAAa,CAAS,EACtB,aAAa,CAAS,EACtB,EAAY,IAAA,GACZ,EAAY,IAAA,EACd,CACF,CACF"}
1
+ {"version":3,"file":"heartbeat.cjs","names":[],"sources":["../src/heartbeat.ts"],"sourcesContent":["import type { HeartbeatOptions } from './types';\n\nimport { encode } from './protocol';\n\nconst DEFAULT_INTERVAL = 30_000;\nconst DEFAULT_TIMEOUT = 5_000;\n\nexport type HeartbeatHandle = {\n /** Called when a `pong` frame is received. Resets the timeout timer. */\n onPong(): void;\n /** Start the heartbeat loop. */\n start(): void;\n /** Stop the heartbeat loop and cancel any pending timers. */\n stop(): void;\n};\n\n/**\n * Create a heartbeat manager that sends periodic pings and expects pong replies.\n * Calls `onDead` when a pong is not received within the timeout window.\n * @internal\n */\nexport function createHeartbeat(\n opts: boolean | HeartbeatOptions | undefined,\n send: (frame: string) => void,\n onDead: () => void,\n): HeartbeatHandle {\n if (!opts) {\n return { onPong() {}, start() {}, stop() {} };\n }\n\n const options: HeartbeatOptions = opts === true ? {} : opts;\n const interval = options.interval ?? DEFAULT_INTERVAL;\n const timeout = options.timeout ?? DEFAULT_TIMEOUT;\n\n let pingTimer: ReturnType<typeof setTimeout> | undefined;\n let pongTimer: ReturnType<typeof setTimeout> | undefined;\n let running = false;\n\n function schedulePing(): void {\n pingTimer = setTimeout(() => {\n const ts = Date.now();\n\n send(encode({ ts, type: 'ping' }));\n\n pongTimer = setTimeout(() => {\n onDead();\n }, timeout);\n }, interval);\n }\n\n return {\n onPong() {\n clearTimeout(pongTimer);\n pongTimer = undefined;\n\n if (running) schedulePing();\n },\n\n start() {\n running = true;\n clearTimeout(pingTimer);\n clearTimeout(pongTimer);\n pongTimer = undefined;\n schedulePing();\n },\n\n stop() {\n running = false;\n clearTimeout(pingTimer);\n clearTimeout(pongTimer);\n pingTimer = undefined;\n pongTimer = undefined;\n },\n };\n}\n"],"mappings":"kCAIA,IAAM,EAAmB,IACnB,EAAkB,IAgBxB,SAAgB,EACd,EACA,EACA,EACiB,CACjB,GAAI,CAAC,EACH,MAAO,CAAE,QAAS,CAAC,EAAG,OAAQ,CAAC,EAAG,MAAO,CAAC,CAAE,EAG9C,IAAM,EAA4B,IAAS,GAAO,CAAC,EAAI,EACjD,EAAW,EAAQ,UAAY,EAC/B,EAAU,EAAQ,SAAW,EAE/B,EACA,EACA,EAAU,GAEd,SAAS,GAAqB,CAC5B,EAAY,eAAiB,CAG3B,EAAK,EAAA,OAAO,CAAE,GAFH,KAAK,IAEF,EAAI,KAAM,MAAO,CAAC,CAAC,EAEjC,EAAY,eAAiB,CAC3B,EAAO,CACT,EAAG,CAAO,CACZ,EAAG,CAAQ,CACb,CAEA,MAAO,CACL,QAAS,CACP,aAAa,CAAS,EACtB,EAAY,IAAA,GAER,GAAS,EAAa,CAC5B,EAEA,OAAQ,CACN,EAAU,GACV,aAAa,CAAS,EACtB,aAAa,CAAS,EACtB,EAAY,IAAA,GACZ,EAAa,CACf,EAEA,MAAO,CACL,EAAU,GACV,aAAa,CAAS,EACtB,aAAa,CAAS,EACtB,EAAY,IAAA,GACZ,EAAY,IAAA,EACd,CACF,CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"heartbeat.d.ts","sourceRoot":"","sources":["../src/heartbeat.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,eAAe,GAAG;IAC5B,wEAAwE;IACxE,MAAM,IAAI,IAAI,CAAC;IACf,gCAAgC;IAChC,KAAK,IAAI,IAAI,CAAC;IACd,6DAA6D;IAC7D,IAAI,IAAI,IAAI,CAAC;CACd,CAAC"}
1
+ {"version":3,"file":"heartbeat.d.ts","sourceRoot":"","sources":["../src/heartbeat.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,eAAe,GAAG;IAC5B,wEAAwE;IACxE,MAAM,IAAI,IAAI,CAAC;IACf,gCAAgC;IAChC,KAAK,IAAI,IAAI,CAAC;IACd,6DAA6D;IAC7D,IAAI,IAAI,IAAI,CAAC;CACd,CAAC"}
package/dist/heartbeat.js CHANGED
@@ -1,4 +1,3 @@
1
- import "./_dev.js";
2
1
  import { encode as e } from "./protocol.js";
3
2
  //#region src/heartbeat.ts
4
3
  var t = 3e4, n = 5e3;
@@ -15,7 +14,7 @@ function r(r, i, a) {
15
14
  ts: Date.now(),
16
15
  type: "ping"
17
16
  })), u = setTimeout(() => {
18
- `${c}`, a();
17
+ a();
19
18
  }, c);
20
19
  }, s);
21
20
  }
@@ -1 +1 @@
1
- {"version":3,"file":"heartbeat.js","names":[],"sources":["../src/heartbeat.ts"],"sourcesContent":["import type { HeartbeatOptions } from './types';\n\nimport { warn } from './_dev';\nimport { encode } from './protocol';\n\nconst DEFAULT_INTERVAL = 30_000;\nconst DEFAULT_TIMEOUT = 5_000;\n\nexport type HeartbeatHandle = {\n /** Called when a `pong` frame is received. Resets the timeout timer. */\n onPong(): void;\n /** Start the heartbeat loop. */\n start(): void;\n /** Stop the heartbeat loop and cancel any pending timers. */\n stop(): void;\n};\n\n/**\n * Create a heartbeat manager that sends periodic pings and expects pong replies.\n * Calls `onDead` when a pong is not received within the timeout window.\n * @internal\n */\nexport function createHeartbeat(\n opts: boolean | HeartbeatOptions | undefined,\n send: (frame: string) => void,\n onDead: () => void,\n): HeartbeatHandle {\n if (!opts) {\n return { onPong() {}, start() {}, stop() {} };\n }\n\n const options: HeartbeatOptions = opts === true ? {} : opts;\n const interval = options.interval ?? DEFAULT_INTERVAL;\n const timeout = options.timeout ?? DEFAULT_TIMEOUT;\n\n let pingTimer: ReturnType<typeof setTimeout> | undefined;\n let pongTimer: ReturnType<typeof setTimeout> | undefined;\n let running = false;\n\n function schedulePing(): void {\n pingTimer = setTimeout(() => {\n const ts = Date.now();\n\n send(encode({ ts, type: 'ping' }));\n\n pongTimer = setTimeout(() => {\n warn(`Heartbeat timed out after ${timeout}ms — treating connection as dead`);\n onDead();\n }, timeout);\n }, interval);\n }\n\n return {\n onPong() {\n clearTimeout(pongTimer);\n pongTimer = undefined;\n\n if (running) schedulePing();\n },\n\n start() {\n running = true;\n clearTimeout(pingTimer);\n clearTimeout(pongTimer);\n pongTimer = undefined;\n schedulePing();\n },\n\n stop() {\n running = false;\n clearTimeout(pingTimer);\n clearTimeout(pongTimer);\n pingTimer = undefined;\n pongTimer = undefined;\n },\n };\n}\n"],"mappings":";;;AAKA,IAAM,IAAmB,KACnB,IAAkB;AAgBxB,SAAgB,EACd,GACA,GACA,GACiB;CACjB,IAAI,CAAC,GACH,OAAO;EAAE,SAAS,CAAC;EAAG,QAAQ,CAAC;EAAG,OAAO,CAAC;CAAE;CAG9C,IAAM,IAA4B,MAAS,KAAO,CAAC,IAAI,GACjD,IAAW,EAAQ,YAAY,GAC/B,IAAU,EAAQ,WAAW,GAE/B,GACA,GACA,IAAU;CAEd,SAAS,IAAqB;EAC5B,IAAY,iBAAiB;GAK3B,AAFA,EAAK,EAAO;IAAE,IAFH,KAAK,IAEF;IAAI,MAAM;GAAO,CAAC,CAAC,GAEjC,IAAY,iBAAiB;IAE3B,AADK,GAA6B,EAA7B,GACL,EAAO;GACT,GAAG,CAAO;EACZ,GAAG,CAAQ;CACb;CAEA,OAAO;EACL,SAAS;GAIP,AAHA,aAAa,CAAS,GACtB,IAAY,KAAA,GAER,KAAS,EAAa;EAC5B;EAEA,QAAQ;GAKN,AAJA,IAAU,IACV,aAAa,CAAS,GACtB,aAAa,CAAS,GACtB,IAAY,KAAA,GACZ,EAAa;EACf;EAEA,OAAO;GAKL,AAJA,IAAU,IACV,aAAa,CAAS,GACtB,aAAa,CAAS,GACtB,IAAY,KAAA,GACZ,IAAY,KAAA;EACd;CACF;AACF"}
1
+ {"version":3,"file":"heartbeat.js","names":[],"sources":["../src/heartbeat.ts"],"sourcesContent":["import type { HeartbeatOptions } from './types';\n\nimport { encode } from './protocol';\n\nconst DEFAULT_INTERVAL = 30_000;\nconst DEFAULT_TIMEOUT = 5_000;\n\nexport type HeartbeatHandle = {\n /** Called when a `pong` frame is received. Resets the timeout timer. */\n onPong(): void;\n /** Start the heartbeat loop. */\n start(): void;\n /** Stop the heartbeat loop and cancel any pending timers. */\n stop(): void;\n};\n\n/**\n * Create a heartbeat manager that sends periodic pings and expects pong replies.\n * Calls `onDead` when a pong is not received within the timeout window.\n * @internal\n */\nexport function createHeartbeat(\n opts: boolean | HeartbeatOptions | undefined,\n send: (frame: string) => void,\n onDead: () => void,\n): HeartbeatHandle {\n if (!opts) {\n return { onPong() {}, start() {}, stop() {} };\n }\n\n const options: HeartbeatOptions = opts === true ? {} : opts;\n const interval = options.interval ?? DEFAULT_INTERVAL;\n const timeout = options.timeout ?? DEFAULT_TIMEOUT;\n\n let pingTimer: ReturnType<typeof setTimeout> | undefined;\n let pongTimer: ReturnType<typeof setTimeout> | undefined;\n let running = false;\n\n function schedulePing(): void {\n pingTimer = setTimeout(() => {\n const ts = Date.now();\n\n send(encode({ ts, type: 'ping' }));\n\n pongTimer = setTimeout(() => {\n onDead();\n }, timeout);\n }, interval);\n }\n\n return {\n onPong() {\n clearTimeout(pongTimer);\n pongTimer = undefined;\n\n if (running) schedulePing();\n },\n\n start() {\n running = true;\n clearTimeout(pingTimer);\n clearTimeout(pongTimer);\n pongTimer = undefined;\n schedulePing();\n },\n\n stop() {\n running = false;\n clearTimeout(pingTimer);\n clearTimeout(pongTimer);\n pingTimer = undefined;\n pongTimer = undefined;\n },\n };\n}\n"],"mappings":";;AAIA,IAAM,IAAmB,KACnB,IAAkB;AAgBxB,SAAgB,EACd,GACA,GACA,GACiB;CACjB,IAAI,CAAC,GACH,OAAO;EAAE,SAAS,CAAC;EAAG,QAAQ,CAAC;EAAG,OAAO,CAAC;CAAE;CAG9C,IAAM,IAA4B,MAAS,KAAO,CAAC,IAAI,GACjD,IAAW,EAAQ,YAAY,GAC/B,IAAU,EAAQ,WAAW,GAE/B,GACA,GACA,IAAU;CAEd,SAAS,IAAqB;EAC5B,IAAY,iBAAiB;GAK3B,AAFA,EAAK,EAAO;IAAE,IAFH,KAAK,IAEF;IAAI,MAAM;GAAO,CAAC,CAAC,GAEjC,IAAY,iBAAiB;IAC3B,EAAO;GACT,GAAG,CAAO;EACZ,GAAG,CAAQ;CACb;CAEA,OAAO;EACL,SAAS;GAIP,AAHA,aAAa,CAAS,GACtB,IAAY,KAAA,GAER,KAAS,EAAa;EAC5B;EAEA,QAAQ;GAKN,AAJA,IAAU,IACV,aAAa,CAAS,GACtB,aAAa,CAAS,GACtB,IAAY,KAAA,GACZ,EAAa;EACf;EAEA,OAAO;GAKL,AAJA,IAAU,IACV,aAAa,CAAS,GACtB,aAAa,CAAS,GACtB,IAAY,KAAA,GACZ,IAAY,KAAA;EACd;CACF;AACF"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { BufferOptions, EventKey, HeartbeatOptions, MessageMap, Middleware, PresenceChannel, Pulse, PulseChannel, PulseOptions, PulseStatus, ReadonlyMap, ReconnectOptions, Unsubscribe, } from './types';
1
+ export type { ChannelDefinition, ChannelDefinitions, EventKey, HeartbeatOptions, MessageMap, OutgoingMessage, OutgoingTransform, PresenceChannel, Pulse, PulseChannel, PulseOptions, PulseStatus, PresenceDefinitions, ReconnectOptions, Unsubscribe, } from './types';
2
2
  export { PulseAbortError, PulseConnectionError, PulseDisposedError, PulseError, PulseProtocolError, PulseTimeoutError, } from './errors';
3
3
  export { createPulse } from './pulse';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,aAAa,EACb,QAAQ,EACR,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,eAAe,EACf,KAAK,EACL,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,WAAW,GACZ,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,UAAU,EACV,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,QAAQ,EACR,gBAAgB,EAChB,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,KAAK,EACL,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,WAAW,GACZ,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,UAAU,EACV,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"}
package/dist/presence.cjs CHANGED
@@ -1,2 +1,2 @@
1
- require("./_dev.cjs");const e=require("./_utils.cjs"),t=require("./protocol.cjs");let n=require("@vielzeug/ripple");function r(r,i,a,o,s){let c=e.deriveAbortController(o),l=c.signal.aborted;c.signal.addEventListener(`abort`,()=>{l=!0},{once:!0});let u=(0,n.signal)(new Map),d=new Set,f=new Set,p=[a.onState(e=>{if(e.room!==r)return;let t=new Map;for(let[n,r]of Object.entries(e.members))t.set(n,r);u.value=t}),a.onJoin(e=>{if(e.room!==r)return;let t=new Map(u.value);t.set(e.id,e.state),u.value=t;for(let t of d)t(e.id,e.state)}),a.onLeave(e=>{if(e.room!==r)return;let t=new Map(u.value);t.delete(e.id),u.value=t;for(let t of f)t(e.id)})];return{get disposalSignal(){return c.signal},dispose(){if(!l){l=!0,i(t.encode({room:r,type:`leave`})),c.abort();for(let e of p)e();p.length=0,d.clear(),f.clear(),s?.()}},get disposed(){return l},onJoin(e){return l?(`${r}`,()=>{}):(d.add(e),()=>d.delete(e))},onLeave(e){return l?(`${r}`,()=>{}):(f.add(e),()=>f.delete(e))},get room(){return r},get state(){return u},[Symbol.dispose](){this.dispose()},update(e){l||i(t.encode({room:r,state:e,type:`presence`}))}}}exports.createPresence=r;
1
+ const e=require("./errors.cjs"),t=require("./_utils.cjs");let n=require("@vielzeug/ripple");function r(r,i,a,o,s,c){let l=t.deriveAbortController(o),u=l.signal.aborted;l.signal.addEventListener(`abort`,()=>{u=!0},{once:!0});let d=(0,n.signal)(new Map);s?.(()=>{d.value=new Map});let f=new Set,p=new Set,m=[a.onState(e=>{if(e.room!==r)return;let t=new Map;for(let[n,r]of Object.entries(e.members))t.set(n,r);d.value=t}),a.onJoin(e=>{if(e.room!==r)return;let t=new Map(d.value);t.set(e.id,e.state),d.value=t;for(let t of f)t(e.id,e.state)}),a.onLeave(e=>{if(e.room!==r)return;let t=new Map(d.value);t.delete(e.id),d.value=t;for(let t of p)t(e.id)})];return{get disposalSignal(){return l.signal},dispose(){if(!u){u=!0,l.abort();for(let e of m)e();m.length=0,f.clear(),p.clear(),c?.()}},get disposed(){return u},onJoin(t){if(u)throw new e.PulseDisposedError(`Presence "${r}"`);return f.add(t),()=>f.delete(t)},onLeave(t){if(u)throw new e.PulseDisposedError(`Presence "${r}"`);return p.add(t),()=>p.delete(t)},get room(){return r},get state(){return d},[Symbol.dispose](){this.dispose()},update(t){if(u)throw new e.PulseDisposedError(`Presence "${r}"`);i(r,t)}}}exports.createPresence=r;
2
2
  //# sourceMappingURL=presence.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"presence.cjs","names":[],"sources":["../src/presence.ts"],"sourcesContent":["import { type Signal, signal } from '@vielzeug/ripple';\n\nimport type { PresenceChannel, Unsubscribe } from './types';\n\nimport { warn } from './_dev';\nimport { deriveAbortController } from './_utils';\nimport { type InPresenceJoinFrame, type InPresenceLeaveFrame, type InPresenceStateFrame, encode } from './protocol';\n\ntype RawSendFn = (frame: string) => void;\n\nexport type PresenceEvents = {\n onJoin(handler: (frame: InPresenceJoinFrame) => void): Unsubscribe;\n onLeave(handler: (frame: InPresenceLeaveFrame) => void): Unsubscribe;\n onState(handler: (frame: InPresenceStateFrame) => void): Unsubscribe;\n};\n\n/**\n * Create a presence channel that reactively tracks room member states.\n * @internal\n */\nexport function createPresence<T>(\n room: string,\n rawSend: RawSendFn,\n events: PresenceEvents,\n disposalSignal: AbortSignal,\n onDispose?: () => void,\n): PresenceChannel<T> {\n const ctrl = deriveAbortController(disposalSignal);\n\n let disposed = ctrl.signal.aborted;\n\n ctrl.signal.addEventListener(\n 'abort',\n () => {\n disposed = true;\n },\n { once: true },\n );\n\n const members: Signal<Map<string, T>> = signal(new Map<string, T>());\n const joinHandlers = new Set<(memberId: string, state: T) => void>();\n const leaveHandlers = new Set<(memberId: string) => void>();\n\n const unsubs: Array<() => void> = [\n events.onState((frame) => {\n if (frame.room !== room) return;\n\n const next = new Map<string, T>();\n\n for (const [id, state] of Object.entries(frame.members)) {\n next.set(id, state as T);\n }\n\n members.value = next;\n }),\n events.onJoin((frame) => {\n if (frame.room !== room) return;\n\n const next = new Map(members.value);\n\n next.set(frame.id, frame.state as T);\n members.value = next;\n\n for (const handler of joinHandlers) {\n handler(frame.id, frame.state as T);\n }\n }),\n events.onLeave((frame) => {\n if (frame.room !== room) return;\n\n const next = new Map(members.value);\n\n next.delete(frame.id);\n members.value = next;\n\n for (const handler of leaveHandlers) {\n handler(frame.id);\n }\n }),\n ];\n\n const presence: PresenceChannel<T> = {\n get disposalSignal() {\n return ctrl.signal;\n },\n\n dispose() {\n if (disposed) return;\n\n disposed = true;\n rawSend(encode({ room, type: 'leave' }));\n ctrl.abort();\n\n for (const unsub of unsubs) unsub();\n\n unsubs.length = 0;\n joinHandlers.clear();\n leaveHandlers.clear();\n onDispose?.();\n },\n\n get disposed() {\n return disposed;\n },\n\n onJoin(handler: (memberId: string, state: T) => void): Unsubscribe {\n if (disposed) {\n warn(`onJoin() called on a disposed presence channel '${room}' — listener ignored`);\n\n return () => {};\n }\n\n joinHandlers.add(handler);\n\n return () => joinHandlers.delete(handler);\n },\n\n onLeave(handler: (memberId: string) => void): Unsubscribe {\n if (disposed) {\n warn(`onLeave() called on a disposed presence channel '${room}' — listener ignored`);\n\n return () => {};\n }\n\n leaveHandlers.add(handler);\n\n return () => leaveHandlers.delete(handler);\n },\n\n get room() {\n return room;\n },\n\n get state() {\n return members;\n },\n\n [Symbol.dispose]() {\n this.dispose();\n },\n\n update(state: T): void {\n if (disposed) return;\n\n rawSend(encode({ room, state, type: 'presence' }));\n },\n };\n\n return presence;\n}\n"],"mappings":"oHAoBA,SAAgB,EACd,EACA,EACA,EACA,EACA,EACoB,CACpB,IAAM,EAAO,EAAA,sBAAsB,CAAc,EAE7C,EAAW,EAAK,OAAO,QAE3B,EAAK,OAAO,iBACV,YACM,CACJ,EAAW,EACb,EACA,CAAE,KAAM,EAAK,CACf,EAEA,IAAM,GAAA,EAAkC,EAAA,OAAA,CAAO,IAAI,GAAgB,EAC7D,EAAe,IAAI,IACnB,EAAgB,IAAI,IAEpB,EAA4B,CAChC,EAAO,QAAS,GAAU,CACxB,GAAI,EAAM,OAAS,EAAM,OAEzB,IAAM,EAAO,IAAI,IAEjB,IAAK,GAAM,CAAC,EAAI,KAAU,OAAO,QAAQ,EAAM,OAAO,EACpD,EAAK,IAAI,EAAI,CAAU,EAGzB,EAAQ,MAAQ,CAClB,CAAC,EACD,EAAO,OAAQ,GAAU,CACvB,GAAI,EAAM,OAAS,EAAM,OAEzB,IAAM,EAAO,IAAI,IAAI,EAAQ,KAAK,EAElC,EAAK,IAAI,EAAM,GAAI,EAAM,KAAU,EACnC,EAAQ,MAAQ,EAEhB,IAAK,IAAM,KAAW,EACpB,EAAQ,EAAM,GAAI,EAAM,KAAU,CAEtC,CAAC,EACD,EAAO,QAAS,GAAU,CACxB,GAAI,EAAM,OAAS,EAAM,OAEzB,IAAM,EAAO,IAAI,IAAI,EAAQ,KAAK,EAElC,EAAK,OAAO,EAAM,EAAE,EACpB,EAAQ,MAAQ,EAEhB,IAAK,IAAM,KAAW,EACpB,EAAQ,EAAM,EAAE,CAEpB,CAAC,CACH,EAqEA,MAAO,CAlEL,IAAI,gBAAiB,CACnB,OAAO,EAAK,MACd,EAEA,SAAU,CACJ,MAIJ,CAFA,EAAW,GACX,EAAQ,EAAA,OAAO,CAAE,OAAM,KAAM,OAAQ,CAAC,CAAC,EACvC,EAAK,MAAM,EAEX,IAAK,IAAM,KAAS,EAAQ,EAAM,EAElC,EAAO,OAAS,EAChB,EAAa,MAAM,EACnB,EAAc,MAAM,EACpB,IAAY,CAPD,CAQb,EAEA,IAAI,UAAW,CACb,OAAO,CACT,EAEA,OAAO,EAA4D,CASjE,OARI,GACG,GAAmD,EAAnD,MAEQ,CAAC,IAGhB,EAAa,IAAI,CAAO,MAEX,EAAa,OAAO,CAAO,EAC1C,EAEA,QAAQ,EAAkD,CASxD,OARI,GACG,GAAoD,EAApD,MAEQ,CAAC,IAGhB,EAAc,IAAI,CAAO,MAEZ,EAAc,OAAO,CAAO,EAC3C,EAEA,IAAI,MAAO,CACT,OAAO,CACT,EAEA,IAAI,OAAQ,CACV,OAAO,CACT,EAEA,CAAC,OAAO,UAAW,CACjB,KAAK,QAAQ,CACf,EAEA,OAAO,EAAgB,CACjB,GAEJ,EAAQ,EAAA,OAAO,CAAE,OAAM,QAAO,KAAM,UAAW,CAAC,CAAC,CACnD,CAGK,CACT"}
1
+ {"version":3,"file":"presence.cjs","names":[],"sources":["../src/presence.ts"],"sourcesContent":["import { type Signal, signal } from '@vielzeug/ripple';\n\nimport type { PresenceChannel, Unsubscribe } from './types';\n\nimport { deriveAbortController } from './_utils';\nimport { PulseDisposedError } from './errors';\nimport { type InPresenceJoinFrame, type InPresenceLeaveFrame, type InPresenceStateFrame } from './protocol';\n\ntype UpdateFn<T> = (room: string, state: T) => void;\n\nexport type PresenceEvents = {\n onJoin(handler: (frame: InPresenceJoinFrame) => void): Unsubscribe;\n onLeave(handler: (frame: InPresenceLeaveFrame) => void): Unsubscribe;\n onState(handler: (frame: InPresenceStateFrame) => void): Unsubscribe;\n};\n\n/**\n * Create a presence channel that reactively tracks room member states.\n * @internal\n */\nexport function createPresence<T>(\n room: string,\n update: UpdateFn<T>,\n events: PresenceEvents,\n disposalSignal: AbortSignal,\n registerReset?: (reset: () => void) => void,\n onDispose?: () => void,\n): PresenceChannel<T> {\n const ctrl = deriveAbortController(disposalSignal);\n\n let disposed = ctrl.signal.aborted;\n\n ctrl.signal.addEventListener(\n 'abort',\n () => {\n disposed = true;\n },\n { once: true },\n );\n\n const members: Signal<Map<string, T>> = signal(new Map<string, T>());\n\n registerReset?.(() => {\n members.value = new Map<string, T>();\n });\n\n const joinHandlers = new Set<(memberId: string, state: T) => void>();\n const leaveHandlers = new Set<(memberId: string) => void>();\n\n const unsubs: Array<() => void> = [\n events.onState((frame) => {\n if (frame.room !== room) return;\n\n const next = new Map<string, T>();\n\n for (const [id, state] of Object.entries(frame.members)) {\n next.set(id, state as T);\n }\n\n members.value = next;\n }),\n events.onJoin((frame) => {\n if (frame.room !== room) return;\n\n const next = new Map(members.value);\n\n next.set(frame.id, frame.state as T);\n members.value = next;\n\n for (const handler of joinHandlers) {\n handler(frame.id, frame.state as T);\n }\n }),\n events.onLeave((frame) => {\n if (frame.room !== room) return;\n\n const next = new Map(members.value);\n\n next.delete(frame.id);\n members.value = next;\n\n for (const handler of leaveHandlers) {\n handler(frame.id);\n }\n }),\n ];\n\n const presence: PresenceChannel<T> = {\n get disposalSignal() {\n return ctrl.signal;\n },\n\n dispose() {\n if (disposed) return;\n\n disposed = true;\n ctrl.abort();\n\n for (const unsub of unsubs) unsub();\n\n unsubs.length = 0;\n joinHandlers.clear();\n leaveHandlers.clear();\n onDispose?.();\n },\n\n get disposed() {\n return disposed;\n },\n\n onJoin(handler: (memberId: string, state: T) => void): Unsubscribe {\n if (disposed) {\n throw new PulseDisposedError(`Presence \"${room}\"`);\n }\n\n joinHandlers.add(handler);\n\n return () => joinHandlers.delete(handler);\n },\n\n onLeave(handler: (memberId: string) => void): Unsubscribe {\n if (disposed) {\n throw new PulseDisposedError(`Presence \"${room}\"`);\n }\n\n leaveHandlers.add(handler);\n\n return () => leaveHandlers.delete(handler);\n },\n\n get room() {\n return room;\n },\n\n get state() {\n return members;\n },\n\n [Symbol.dispose]() {\n this.dispose();\n },\n\n update(state: T): void {\n if (disposed) throw new PulseDisposedError(`Presence \"${room}\"`);\n\n update(room, state);\n },\n };\n\n return presence;\n}\n"],"mappings":"4FAoBA,SAAgB,EACd,EACA,EACA,EACA,EACA,EACA,EACoB,CACpB,IAAM,EAAO,EAAA,sBAAsB,CAAc,EAE7C,EAAW,EAAK,OAAO,QAE3B,EAAK,OAAO,iBACV,YACM,CACJ,EAAW,EACb,EACA,CAAE,KAAM,EAAK,CACf,EAEA,IAAM,GAAA,EAAkC,EAAA,OAAA,CAAO,IAAI,GAAgB,EAEnE,QAAsB,CACpB,EAAQ,MAAQ,IAAI,GACtB,CAAC,EAED,IAAM,EAAe,IAAI,IACnB,EAAgB,IAAI,IAEpB,EAA4B,CAChC,EAAO,QAAS,GAAU,CACxB,GAAI,EAAM,OAAS,EAAM,OAEzB,IAAM,EAAO,IAAI,IAEjB,IAAK,GAAM,CAAC,EAAI,KAAU,OAAO,QAAQ,EAAM,OAAO,EACpD,EAAK,IAAI,EAAI,CAAU,EAGzB,EAAQ,MAAQ,CAClB,CAAC,EACD,EAAO,OAAQ,GAAU,CACvB,GAAI,EAAM,OAAS,EAAM,OAEzB,IAAM,EAAO,IAAI,IAAI,EAAQ,KAAK,EAElC,EAAK,IAAI,EAAM,GAAI,EAAM,KAAU,EACnC,EAAQ,MAAQ,EAEhB,IAAK,IAAM,KAAW,EACpB,EAAQ,EAAM,GAAI,EAAM,KAAU,CAEtC,CAAC,EACD,EAAO,QAAS,GAAU,CACxB,GAAI,EAAM,OAAS,EAAM,OAEzB,IAAM,EAAO,IAAI,IAAI,EAAQ,KAAK,EAElC,EAAK,OAAO,EAAM,EAAE,EACpB,EAAQ,MAAQ,EAEhB,IAAK,IAAM,KAAW,EACpB,EAAQ,EAAM,EAAE,CAEpB,CAAC,CACH,EAgEA,MAAO,CA7DL,IAAI,gBAAiB,CACnB,OAAO,EAAK,MACd,EAEA,SAAU,CACJ,MAGJ,CADA,EAAW,GACX,EAAK,MAAM,EAEX,IAAK,IAAM,KAAS,EAAQ,EAAM,EAElC,EAAO,OAAS,EAChB,EAAa,MAAM,EACnB,EAAc,MAAM,EACpB,IAAY,CAPD,CAQb,EAEA,IAAI,UAAW,CACb,OAAO,CACT,EAEA,OAAO,EAA4D,CACjE,GAAI,EACF,MAAM,IAAI,EAAA,mBAAmB,aAAa,EAAK,EAAE,EAKnD,OAFA,EAAa,IAAI,CAAO,MAEX,EAAa,OAAO,CAAO,CAC1C,EAEA,QAAQ,EAAkD,CACxD,GAAI,EACF,MAAM,IAAI,EAAA,mBAAmB,aAAa,EAAK,EAAE,EAKnD,OAFA,EAAc,IAAI,CAAO,MAEZ,EAAc,OAAO,CAAO,CAC3C,EAEA,IAAI,MAAO,CACT,OAAO,CACT,EAEA,IAAI,OAAQ,CACV,OAAO,CACT,EAEA,CAAC,OAAO,UAAW,CACjB,KAAK,QAAQ,CACf,EAEA,OAAO,EAAgB,CACrB,GAAI,EAAU,MAAM,IAAI,EAAA,mBAAmB,aAAa,EAAK,EAAE,EAE/D,EAAO,EAAM,CAAK,CACpB,CAGK,CACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"presence.d.ts","sourceRoot":"","sources":["../src/presence.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAmB,WAAW,EAAE,MAAM,SAAS,CAAC;AAI5D,OAAO,EAAE,KAAK,mBAAmB,EAAE,KAAK,oBAAoB,EAAE,KAAK,oBAAoB,EAAU,MAAM,YAAY,CAAC;AAIpH,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,GAAG,WAAW,CAAC;IACnE,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,WAAW,CAAC;IACrE,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,WAAW,CAAC;CACtE,CAAC"}
1
+ {"version":3,"file":"presence.d.ts","sourceRoot":"","sources":["../src/presence.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAmB,WAAW,EAAE,MAAM,SAAS,CAAC;AAI5D,OAAO,EAAE,KAAK,mBAAmB,EAAE,KAAK,oBAAoB,EAAE,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAI5G,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,GAAG,WAAW,CAAC;IACnE,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,WAAW,CAAC;IACrE,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,WAAW,CAAC;CACtE,CAAC"}