@gwakko/shared-websocket 0.3.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +722 -0
- package/dist/SharedSocket.d.ts +2 -0
- package/dist/SharedWebSocket.d.ts +71 -13
- package/dist/adapters/react.d.ts +29 -2
- package/dist/adapters/vue.d.ts +23 -1
- package/dist/{chunk-JJTAPRPG.js → chunk-4D2ZDCA6.js} +196 -18
- package/dist/chunk-4D2ZDCA6.js.map +1 -0
- package/dist/{chunk-Q4OKSJX7.cjs → chunk-UEOFAFLV.cjs} +197 -19
- package/dist/chunk-UEOFAFLV.cjs.map +1 -0
- package/dist/index.cjs +3 -3
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/react.cjs +33 -3
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +31 -1
- package/dist/react.js.map +1 -1
- package/dist/types.d.ts +73 -2
- package/dist/vue.cjs +33 -11
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +31 -9
- package/dist/vue.js.map +1 -1
- package/package.json +1 -1
- package/src/SharedSocket.ts +6 -2
- package/src/SharedWebSocket.ts +237 -25
- package/src/adapters/react.ts +63 -4
- package/src/adapters/vue.ts +57 -11
- package/src/index.ts +21 -2
- package/src/types.ts +79 -2
- package/dist/chunk-JJTAPRPG.js.map +0 -1
- package/dist/chunk-Q4OKSJX7.cjs.map +0 -1
package/dist/SharedSocket.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ interface SharedSocketOptions {
|
|
|
9
9
|
auth?: () => string | Promise<string>;
|
|
10
10
|
authToken?: string;
|
|
11
11
|
authParam?: string;
|
|
12
|
+
/** Heartbeat payload (default: { type: "ping" }). */
|
|
13
|
+
pingPayload?: unknown;
|
|
12
14
|
}
|
|
13
15
|
export declare class SharedSocket implements Disposable {
|
|
14
16
|
private url;
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import './utils/disposable';
|
|
2
|
-
import type { SharedWebSocketOptions, TabRole, Unsubscribe, EventHandler } from './types';
|
|
2
|
+
import type { SharedWebSocketOptions, TabRole, Unsubscribe, EventHandler, Channel, EventMap, Middleware } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* SharedWebSocket — shares ONE WebSocket connection across browser tabs.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* @typeParam TEvents - Event map for type-safe subscriptions.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // Typed events
|
|
10
|
+
* type Events = {
|
|
11
|
+
* 'chat.message': { text: string; userId: string };
|
|
12
|
+
* 'order.created': { id: string; total: number };
|
|
13
|
+
* };
|
|
14
|
+
* const ws = new SharedWebSocket<Events>(url);
|
|
15
|
+
* ws.on('chat.message', (msg) => msg.text); // ← msg: { text, userId }
|
|
9
16
|
*/
|
|
10
|
-
export declare class SharedWebSocket implements Disposable {
|
|
17
|
+
export declare class SharedWebSocket<TEvents extends EventMap = EventMap> implements Disposable {
|
|
11
18
|
private readonly url;
|
|
12
19
|
private readonly options;
|
|
13
20
|
private bus;
|
|
@@ -18,18 +25,53 @@ export declare class SharedWebSocket implements Disposable {
|
|
|
18
25
|
private tabId;
|
|
19
26
|
private cleanups;
|
|
20
27
|
private disposed;
|
|
21
|
-
|
|
28
|
+
private readonly proto;
|
|
29
|
+
private readonly log;
|
|
30
|
+
private outgoingMiddleware;
|
|
31
|
+
private incomingMiddleware;
|
|
32
|
+
constructor(url: string, options?: SharedWebSocketOptions<TEvents>);
|
|
22
33
|
get connected(): boolean;
|
|
23
34
|
get tabRole(): TabRole;
|
|
24
35
|
/** Start leader election and connect. */
|
|
25
36
|
connect(): Promise<void>;
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
/** Called when WebSocket connection opens (broadcast to all tabs). */
|
|
38
|
+
onConnect(fn: () => void): Unsubscribe;
|
|
39
|
+
/** Called when WebSocket connection closes (broadcast to all tabs). */
|
|
40
|
+
onDisconnect(fn: () => void): Unsubscribe;
|
|
41
|
+
/** Called when WebSocket starts reconnecting (broadcast to all tabs). */
|
|
42
|
+
onReconnecting(fn: () => void): Unsubscribe;
|
|
43
|
+
/** Called when this tab becomes leader or loses leadership. */
|
|
44
|
+
onLeaderChange(fn: (isLeader: boolean) => void): Unsubscribe;
|
|
45
|
+
/** Called on WebSocket or network error (broadcast to all tabs). */
|
|
46
|
+
onError(fn: (error: unknown) => void): Unsubscribe;
|
|
47
|
+
/**
|
|
48
|
+
* Add middleware to transform messages before send or after receive.
|
|
49
|
+
* Return null from middleware to drop the message.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // Add timestamp to every outgoing message
|
|
53
|
+
* ws.use('outgoing', (msg) => ({ ...msg, timestamp: Date.now() }));
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* // Decrypt incoming messages
|
|
57
|
+
* ws.use('incoming', (msg) => ({ ...msg, data: decrypt(msg.data) }));
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* // Drop messages from blocked users
|
|
61
|
+
* ws.use('incoming', (msg) => blockedUsers.has(msg.userId) ? null : msg);
|
|
62
|
+
*/
|
|
63
|
+
use(direction: 'outgoing' | 'incoming', fn: Middleware): this;
|
|
64
|
+
/** Subscribe to server events (works in ALL tabs). Type-safe with EventMap. */
|
|
65
|
+
on<K extends string & keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>): Unsubscribe;
|
|
66
|
+
on(event: string, handler: EventHandler<unknown>): Unsubscribe;
|
|
67
|
+
once<K extends string & keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>): Unsubscribe;
|
|
68
|
+
once(event: string, handler: EventHandler<unknown>): Unsubscribe;
|
|
29
69
|
off(event: string, handler?: EventHandler): void;
|
|
30
|
-
/** Async generator for consuming events. */
|
|
70
|
+
/** Async generator for consuming events. Type-safe with EventMap. */
|
|
71
|
+
stream<K extends string & keyof TEvents>(event: K, signal?: AbortSignal): AsyncGenerator<TEvents[K]>;
|
|
31
72
|
stream(event: string, signal?: AbortSignal): AsyncGenerator<unknown>;
|
|
32
|
-
/** Send message to server (auto-routed through leader). */
|
|
73
|
+
/** Send message to server (auto-routed through leader). Type-safe with EventMap. */
|
|
74
|
+
send<K extends string & keyof TEvents>(event: K, data: TEvents[K]): void;
|
|
33
75
|
send(event: string, data: unknown): void;
|
|
34
76
|
/** Request/response through server via leader. */
|
|
35
77
|
request<T>(event: string, data: unknown, timeout?: number): Promise<T>;
|
|
@@ -37,9 +79,25 @@ export declare class SharedWebSocket implements Disposable {
|
|
|
37
79
|
sync<T>(key: string, value: T): void;
|
|
38
80
|
getSync<T>(key: string): T | undefined;
|
|
39
81
|
onSync<T>(key: string, fn: (value: T) => void): Unsubscribe;
|
|
82
|
+
/**
|
|
83
|
+
* Subscribe to a private/scoped channel. Returns a channel handle with
|
|
84
|
+
* scoped on/send/stream methods. Sends join on subscribe, leave on unsubscribe.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* const chat = ws.channel('chat:room_123');
|
|
88
|
+
* chat.on('message', (msg) => render(msg));
|
|
89
|
+
* chat.send('message', { text: 'Hello' });
|
|
90
|
+
* chat.leave(); // sends leave + unsubscribes
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* // Private notifications for tenant
|
|
94
|
+
* const notifications = ws.channel(`tenant:${tenantId}:notifications`);
|
|
95
|
+
* notifications.on('alert', (alert) => showToast(alert));
|
|
96
|
+
*/
|
|
97
|
+
channel(name: string): Channel;
|
|
40
98
|
disconnect(): void;
|
|
41
99
|
private createSocket;
|
|
42
|
-
private
|
|
43
|
-
private
|
|
100
|
+
private handleBecomeLeader;
|
|
101
|
+
private handleLoseLeadership;
|
|
44
102
|
[Symbol.dispose](): void;
|
|
45
103
|
}
|
package/dist/adapters/react.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import { SharedWebSocket } from '../SharedWebSocket';
|
|
3
|
-
import type { SharedWebSocketOptions, TabRole } from '../types';
|
|
3
|
+
import type { SharedWebSocketOptions, TabRole, SocketLifecycleHandlers } from '../types';
|
|
4
4
|
/**
|
|
5
5
|
* Provider props — pass URL and options as props for flexibility.
|
|
6
6
|
*
|
|
@@ -32,7 +32,7 @@ export interface SharedWebSocketProviderProps {
|
|
|
32
32
|
* );
|
|
33
33
|
* }
|
|
34
34
|
*/
|
|
35
|
-
export declare function SharedWebSocketProvider({ url, options, children }: SharedWebSocketProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<SharedWebSocket | null>>;
|
|
35
|
+
export declare function SharedWebSocketProvider({ url, options, children }: SharedWebSocketProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<SharedWebSocket<import("..").EventMap> | null>>;
|
|
36
36
|
/**
|
|
37
37
|
* Access the SharedWebSocket instance from context.
|
|
38
38
|
*
|
|
@@ -137,3 +137,30 @@ export declare function useSocketStatus(): {
|
|
|
137
137
|
connected: boolean;
|
|
138
138
|
tabRole: TabRole;
|
|
139
139
|
};
|
|
140
|
+
/**
|
|
141
|
+
* Lifecycle hooks — react to connection state changes.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* useSocketLifecycle({
|
|
145
|
+
* onConnect: () => console.log('Connected!'),
|
|
146
|
+
* onDisconnect: () => console.log('Disconnected'),
|
|
147
|
+
* onReconnecting: () => showSpinner(),
|
|
148
|
+
* onLeaderChange: (isLeader) => console.log('Leader:', isLeader),
|
|
149
|
+
* onError: (err) => reportError(err),
|
|
150
|
+
* });
|
|
151
|
+
*/
|
|
152
|
+
export declare function useSocketLifecycle(handlers: SocketLifecycleHandlers): void;
|
|
153
|
+
/**
|
|
154
|
+
* Subscribe to a private channel. Auto-joins on mount, leaves on unmount.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* const chat = useChannel('chat:room_123');
|
|
158
|
+
* const message = useSocketEvent('chat:room_123:message');
|
|
159
|
+
* chat.send('message', { text: 'Hello' });
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* // Tenant notifications
|
|
163
|
+
* const notifications = useChannel(`tenant:${tenantId}:notifications`);
|
|
164
|
+
* useSocketCallback(`tenant:${tenantId}:notifications:alert`, showToast);
|
|
165
|
+
*/
|
|
166
|
+
export declare function useChannel(name: string): import("..").Channel;
|
package/dist/adapters/vue.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Ref, type InjectionKey, type App } from 'vue';
|
|
2
2
|
import { SharedWebSocket } from '../SharedWebSocket';
|
|
3
|
-
import type { SharedWebSocketOptions, TabRole } from '../types';
|
|
3
|
+
import type { SharedWebSocketOptions, TabRole, SocketLifecycleHandlers } from '../types';
|
|
4
4
|
export declare const SharedWebSocketKey: InjectionKey<SharedWebSocket>;
|
|
5
5
|
/**
|
|
6
6
|
* Vue 3 plugin for SharedWebSocket.
|
|
@@ -101,3 +101,25 @@ export declare function useSocketStatus(): {
|
|
|
101
101
|
connected: Ref<boolean>;
|
|
102
102
|
tabRole: Ref<TabRole>;
|
|
103
103
|
};
|
|
104
|
+
/**
|
|
105
|
+
* Lifecycle hooks — react to connection state changes.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* useSocketLifecycle({
|
|
109
|
+
* onConnect: () => console.log('Connected!'),
|
|
110
|
+
* onDisconnect: () => showOfflineBanner(),
|
|
111
|
+
* onReconnecting: () => showSpinner(),
|
|
112
|
+
* onLeaderChange: (isLeader) => console.log('Leader:', isLeader),
|
|
113
|
+
* onError: (err) => reportError(err),
|
|
114
|
+
* });
|
|
115
|
+
*/
|
|
116
|
+
export declare function useSocketLifecycle(handlers: SocketLifecycleHandlers): void;
|
|
117
|
+
/**
|
|
118
|
+
* Subscribe to a private channel. Auto-joins on mount, leaves on unmount.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* const chat = useChannel('chat:room_123');
|
|
122
|
+
* // Listen via useSocketEvent('chat:room_123:message')
|
|
123
|
+
* // Send via chat.send('message', { text: 'Hello' })
|
|
124
|
+
*/
|
|
125
|
+
export declare function useChannel(name: string): import("..").Channel;
|
|
@@ -253,7 +253,8 @@ var SharedSocket = class {
|
|
|
253
253
|
sendBuffer: options.sendBuffer ?? 100,
|
|
254
254
|
auth: options.auth,
|
|
255
255
|
authToken: options.authToken,
|
|
256
|
-
authParam: options.authParam ?? "token"
|
|
256
|
+
authParam: options.authParam ?? "token",
|
|
257
|
+
pingPayload: options.pingPayload ?? { type: "ping" }
|
|
257
258
|
};
|
|
258
259
|
}
|
|
259
260
|
url;
|
|
@@ -353,7 +354,7 @@ var SharedSocket = class {
|
|
|
353
354
|
this.stopHeartbeat();
|
|
354
355
|
this.heartbeatTimer = setInterval(() => {
|
|
355
356
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
356
|
-
this.ws.send(JSON.stringify(
|
|
357
|
+
this.ws.send(JSON.stringify(this.opts.pingPayload));
|
|
357
358
|
}
|
|
358
359
|
}, this.opts.heartbeatInterval);
|
|
359
360
|
}
|
|
@@ -576,11 +577,32 @@ var SubscriptionManager = class {
|
|
|
576
577
|
};
|
|
577
578
|
|
|
578
579
|
// src/SharedWebSocket.ts
|
|
580
|
+
var DEFAULT_PROTOCOL = {
|
|
581
|
+
eventField: "event",
|
|
582
|
+
dataField: "data",
|
|
583
|
+
channelJoin: "$channel:join",
|
|
584
|
+
channelLeave: "$channel:leave",
|
|
585
|
+
ping: { type: "ping" },
|
|
586
|
+
defaultEvent: "message"
|
|
587
|
+
};
|
|
588
|
+
var NOOP_LOGGER = {
|
|
589
|
+
debug() {
|
|
590
|
+
},
|
|
591
|
+
info() {
|
|
592
|
+
},
|
|
593
|
+
warn() {
|
|
594
|
+
},
|
|
595
|
+
error() {
|
|
596
|
+
}
|
|
597
|
+
};
|
|
579
598
|
var SharedWebSocket = class {
|
|
580
599
|
constructor(url, options = {}) {
|
|
581
600
|
this.url = url;
|
|
582
601
|
this.options = options;
|
|
602
|
+
this.proto = { ...DEFAULT_PROTOCOL, ...options.events };
|
|
603
|
+
this.log = options.debug ? options.logger ?? console : NOOP_LOGGER;
|
|
583
604
|
this.tabId = generateId();
|
|
605
|
+
this.log.debug("[SharedWS] init", { tabId: this.tabId, url });
|
|
584
606
|
this.bus = new MessageBus("shared-ws", this.tabId);
|
|
585
607
|
this.coordinator = new TabCoordinator(this.bus, this.tabId, {
|
|
586
608
|
electionTimeout: options.electionTimeout,
|
|
@@ -595,7 +617,7 @@ var SharedWebSocket = class {
|
|
|
595
617
|
this.cleanups.push(
|
|
596
618
|
this.bus.subscribe("ws:send", (msg) => {
|
|
597
619
|
if (this.coordinator.isLeader && this.socket) {
|
|
598
|
-
this.socket.send({
|
|
620
|
+
this.socket.send({ [this.proto.eventField]: msg.event, [this.proto.dataField]: msg.data });
|
|
599
621
|
}
|
|
600
622
|
})
|
|
601
623
|
);
|
|
@@ -605,8 +627,35 @@ var SharedWebSocket = class {
|
|
|
605
627
|
this.subs.emit(`sync:${msg.key}`, msg.value);
|
|
606
628
|
})
|
|
607
629
|
);
|
|
608
|
-
this.coordinator.onBecomeLeader(() =>
|
|
609
|
-
|
|
630
|
+
this.coordinator.onBecomeLeader(() => {
|
|
631
|
+
this.handleBecomeLeader();
|
|
632
|
+
this.bus.broadcast("ws:lifecycle", { type: "leader", isLeader: true });
|
|
633
|
+
});
|
|
634
|
+
this.coordinator.onLoseLeadership(() => {
|
|
635
|
+
this.handleLoseLeadership();
|
|
636
|
+
this.bus.broadcast("ws:lifecycle", { type: "leader", isLeader: false });
|
|
637
|
+
});
|
|
638
|
+
this.cleanups.push(
|
|
639
|
+
this.bus.subscribe("ws:lifecycle", (msg) => {
|
|
640
|
+
switch (msg.type) {
|
|
641
|
+
case "connect":
|
|
642
|
+
this.subs.emit("$lifecycle:connect", void 0);
|
|
643
|
+
break;
|
|
644
|
+
case "disconnect":
|
|
645
|
+
this.subs.emit("$lifecycle:disconnect", void 0);
|
|
646
|
+
break;
|
|
647
|
+
case "reconnecting":
|
|
648
|
+
this.subs.emit("$lifecycle:reconnecting", void 0);
|
|
649
|
+
break;
|
|
650
|
+
case "leader":
|
|
651
|
+
this.subs.emit("$lifecycle:leader", msg.isLeader);
|
|
652
|
+
break;
|
|
653
|
+
case "error":
|
|
654
|
+
this.subs.emit("$lifecycle:error", msg.error);
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
})
|
|
658
|
+
);
|
|
610
659
|
if (typeof window !== "undefined") {
|
|
611
660
|
const onBeforeUnload = () => this[Symbol.dispose]();
|
|
612
661
|
window.addEventListener("beforeunload", onBeforeUnload);
|
|
@@ -623,6 +672,10 @@ var SharedWebSocket = class {
|
|
|
623
672
|
tabId;
|
|
624
673
|
cleanups = [];
|
|
625
674
|
disposed = false;
|
|
675
|
+
proto;
|
|
676
|
+
log;
|
|
677
|
+
outgoingMiddleware = [];
|
|
678
|
+
incomingMiddleware = [];
|
|
626
679
|
get connected() {
|
|
627
680
|
return this.socket?.state === "connected" || !this.coordinator.isLeader;
|
|
628
681
|
}
|
|
@@ -633,24 +686,78 @@ var SharedWebSocket = class {
|
|
|
633
686
|
async connect() {
|
|
634
687
|
await this.coordinator.elect();
|
|
635
688
|
}
|
|
636
|
-
|
|
689
|
+
// ─── Lifecycle Hooks ─────────────────────────────────
|
|
690
|
+
/** Called when WebSocket connection opens (broadcast to all tabs). */
|
|
691
|
+
onConnect(fn) {
|
|
692
|
+
return this.subs.on("$lifecycle:connect", fn);
|
|
693
|
+
}
|
|
694
|
+
/** Called when WebSocket connection closes (broadcast to all tabs). */
|
|
695
|
+
onDisconnect(fn) {
|
|
696
|
+
return this.subs.on("$lifecycle:disconnect", fn);
|
|
697
|
+
}
|
|
698
|
+
/** Called when WebSocket starts reconnecting (broadcast to all tabs). */
|
|
699
|
+
onReconnecting(fn) {
|
|
700
|
+
return this.subs.on("$lifecycle:reconnecting", fn);
|
|
701
|
+
}
|
|
702
|
+
/** Called when this tab becomes leader or loses leadership. */
|
|
703
|
+
onLeaderChange(fn) {
|
|
704
|
+
return this.subs.on("$lifecycle:leader", fn);
|
|
705
|
+
}
|
|
706
|
+
/** Called on WebSocket or network error (broadcast to all tabs). */
|
|
707
|
+
onError(fn) {
|
|
708
|
+
return this.subs.on("$lifecycle:error", fn);
|
|
709
|
+
}
|
|
710
|
+
// ─── Middleware ───────────────────────────────────────
|
|
711
|
+
/**
|
|
712
|
+
* Add middleware to transform messages before send or after receive.
|
|
713
|
+
* Return null from middleware to drop the message.
|
|
714
|
+
*
|
|
715
|
+
* @example
|
|
716
|
+
* // Add timestamp to every outgoing message
|
|
717
|
+
* ws.use('outgoing', (msg) => ({ ...msg, timestamp: Date.now() }));
|
|
718
|
+
*
|
|
719
|
+
* @example
|
|
720
|
+
* // Decrypt incoming messages
|
|
721
|
+
* ws.use('incoming', (msg) => ({ ...msg, data: decrypt(msg.data) }));
|
|
722
|
+
*
|
|
723
|
+
* @example
|
|
724
|
+
* // Drop messages from blocked users
|
|
725
|
+
* ws.use('incoming', (msg) => blockedUsers.has(msg.userId) ? null : msg);
|
|
726
|
+
*/
|
|
727
|
+
use(direction, fn) {
|
|
728
|
+
if (direction === "outgoing") {
|
|
729
|
+
this.outgoingMiddleware.push(fn);
|
|
730
|
+
} else {
|
|
731
|
+
this.incomingMiddleware.push(fn);
|
|
732
|
+
}
|
|
733
|
+
return this;
|
|
734
|
+
}
|
|
735
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
637
736
|
on(event, handler) {
|
|
638
737
|
return this.subs.on(event, handler);
|
|
639
738
|
}
|
|
739
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
640
740
|
once(event, handler) {
|
|
641
741
|
return this.subs.once(event, handler);
|
|
642
742
|
}
|
|
643
743
|
off(event, handler) {
|
|
644
744
|
this.subs.off(event, handler);
|
|
645
745
|
}
|
|
646
|
-
/** Async generator for consuming events. */
|
|
647
746
|
stream(event, signal) {
|
|
648
747
|
return this.subs.stream(event, signal);
|
|
649
748
|
}
|
|
650
|
-
/** Send message to server (auto-routed through leader). */
|
|
651
749
|
send(event, data) {
|
|
750
|
+
let payload = { [this.proto.eventField]: event, [this.proto.dataField]: data };
|
|
751
|
+
for (const mw of this.outgoingMiddleware) {
|
|
752
|
+
payload = mw(payload);
|
|
753
|
+
if (payload === null) {
|
|
754
|
+
this.log.debug("[SharedWS] \u2717 outgoing dropped by middleware", event);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
this.log.debug("[SharedWS] \u2192 send", event, data);
|
|
652
759
|
if (this.coordinator.isLeader && this.socket) {
|
|
653
|
-
this.socket.send(
|
|
760
|
+
this.socket.send(payload);
|
|
654
761
|
} else {
|
|
655
762
|
this.bus.publish("ws:send", { event, data });
|
|
656
763
|
}
|
|
@@ -670,6 +777,50 @@ var SharedWebSocket = class {
|
|
|
670
777
|
onSync(key, fn) {
|
|
671
778
|
return this.subs.on(`sync:${key}`, fn);
|
|
672
779
|
}
|
|
780
|
+
/**
|
|
781
|
+
* Subscribe to a private/scoped channel. Returns a channel handle with
|
|
782
|
+
* scoped on/send/stream methods. Sends join on subscribe, leave on unsubscribe.
|
|
783
|
+
*
|
|
784
|
+
* @example
|
|
785
|
+
* const chat = ws.channel('chat:room_123');
|
|
786
|
+
* chat.on('message', (msg) => render(msg));
|
|
787
|
+
* chat.send('message', { text: 'Hello' });
|
|
788
|
+
* chat.leave(); // sends leave + unsubscribes
|
|
789
|
+
*
|
|
790
|
+
* @example
|
|
791
|
+
* // Private notifications for tenant
|
|
792
|
+
* const notifications = ws.channel(`tenant:${tenantId}:notifications`);
|
|
793
|
+
* notifications.on('alert', (alert) => showToast(alert));
|
|
794
|
+
*/
|
|
795
|
+
channel(name) {
|
|
796
|
+
this.send(this.proto.channelJoin, { channel: name });
|
|
797
|
+
const self = this;
|
|
798
|
+
const unsubs = [];
|
|
799
|
+
return {
|
|
800
|
+
name,
|
|
801
|
+
on(event, handler) {
|
|
802
|
+
const unsub = self.subs.on(`${name}:${event}`, handler);
|
|
803
|
+
unsubs.push(unsub);
|
|
804
|
+
return unsub;
|
|
805
|
+
},
|
|
806
|
+
once(event, handler) {
|
|
807
|
+
const unsub = self.subs.once(`${name}:${event}`, handler);
|
|
808
|
+
unsubs.push(unsub);
|
|
809
|
+
return unsub;
|
|
810
|
+
},
|
|
811
|
+
send(event, data) {
|
|
812
|
+
self.send(`${name}:${event}`, data);
|
|
813
|
+
},
|
|
814
|
+
stream(event, signal) {
|
|
815
|
+
return self.subs.stream(`${name}:${event}`, signal);
|
|
816
|
+
},
|
|
817
|
+
leave() {
|
|
818
|
+
self.send(self.proto.channelLeave, { channel: name });
|
|
819
|
+
for (const unsub of unsubs) unsub();
|
|
820
|
+
unsubs.length = 0;
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
}
|
|
673
824
|
disconnect() {
|
|
674
825
|
this[Symbol.dispose]();
|
|
675
826
|
}
|
|
@@ -679,7 +830,8 @@ var SharedWebSocket = class {
|
|
|
679
830
|
reconnect: this.options.reconnect,
|
|
680
831
|
reconnectMaxDelay: this.options.reconnectMaxDelay,
|
|
681
832
|
heartbeatInterval: this.options.heartbeatInterval,
|
|
682
|
-
sendBuffer: this.options.sendBuffer
|
|
833
|
+
sendBuffer: this.options.sendBuffer,
|
|
834
|
+
pingPayload: this.proto.ping
|
|
683
835
|
};
|
|
684
836
|
if (this.options.useWorker) {
|
|
685
837
|
return new WorkerSocket(this.url, {
|
|
@@ -694,20 +846,46 @@ var SharedWebSocket = class {
|
|
|
694
846
|
authParam: this.options.authParam
|
|
695
847
|
});
|
|
696
848
|
}
|
|
697
|
-
|
|
849
|
+
handleBecomeLeader() {
|
|
850
|
+
this.log.info("[SharedWS] \u{1F451} became leader");
|
|
698
851
|
this.socket = this.createSocket();
|
|
699
|
-
this.socket.onMessage((
|
|
700
|
-
|
|
701
|
-
const
|
|
852
|
+
this.socket.onMessage((raw) => {
|
|
853
|
+
let data = raw;
|
|
854
|
+
for (const mw of this.incomingMiddleware) {
|
|
855
|
+
data = mw(data);
|
|
856
|
+
if (data === null) {
|
|
857
|
+
this.log.debug("[SharedWS] \u2717 incoming dropped by middleware");
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const msg = data;
|
|
862
|
+
const event = msg?.[this.proto.eventField] ?? this.proto.defaultEvent;
|
|
863
|
+
const payload = msg?.[this.proto.dataField] ?? data;
|
|
864
|
+
this.log.debug("[SharedWS] \u2190 recv", event, payload);
|
|
702
865
|
this.bus.broadcast("ws:message", { event, data: payload });
|
|
703
866
|
});
|
|
867
|
+
this.socket.onStateChange((state) => {
|
|
868
|
+
this.log.info("[SharedWS]", state === "connected" ? "\u2713 connected" : state === "reconnecting" ? "\u{1F504} reconnecting" : `state: ${state}`);
|
|
869
|
+
switch (state) {
|
|
870
|
+
case "connected":
|
|
871
|
+
this.bus.broadcast("ws:lifecycle", { type: "connect" });
|
|
872
|
+
break;
|
|
873
|
+
case "closed":
|
|
874
|
+
this.bus.broadcast("ws:lifecycle", { type: "disconnect" });
|
|
875
|
+
break;
|
|
876
|
+
case "reconnecting":
|
|
877
|
+
this.bus.broadcast("ws:lifecycle", { type: "reconnecting" });
|
|
878
|
+
break;
|
|
879
|
+
}
|
|
880
|
+
});
|
|
704
881
|
this.cleanups.push(
|
|
705
882
|
this.bus.respond("ws:request", async (req) => {
|
|
706
883
|
return new Promise((resolve) => {
|
|
707
884
|
const unsub = this.socket.onMessage((response) => {
|
|
708
|
-
|
|
885
|
+
const res = response;
|
|
886
|
+
if (res?.[this.proto.eventField] === req.event || res?.requestId) {
|
|
709
887
|
unsub();
|
|
710
|
-
resolve(
|
|
888
|
+
resolve(res?.[this.proto.dataField] ?? response);
|
|
711
889
|
}
|
|
712
890
|
});
|
|
713
891
|
this.socket.send({ event: req.event, data: req.data });
|
|
@@ -716,7 +894,7 @@ var SharedWebSocket = class {
|
|
|
716
894
|
);
|
|
717
895
|
this.socket.connect();
|
|
718
896
|
}
|
|
719
|
-
|
|
897
|
+
handleLoseLeadership() {
|
|
720
898
|
if (this.socket) {
|
|
721
899
|
this.socket[Symbol.dispose]();
|
|
722
900
|
this.socket = null;
|
|
@@ -746,4 +924,4 @@ export {
|
|
|
746
924
|
SubscriptionManager,
|
|
747
925
|
SharedWebSocket
|
|
748
926
|
};
|
|
749
|
-
//# sourceMappingURL=chunk-
|
|
927
|
+
//# sourceMappingURL=chunk-4D2ZDCA6.js.map
|