@agentick/client-multiplexer 0.0.1
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/LICENSE +21 -0
- package/README.md +168 -0
- package/dist/broadcast-bridge.d.ts +115 -0
- package/dist/broadcast-bridge.d.ts.map +1 -0
- package/dist/broadcast-bridge.js +59 -0
- package/dist/broadcast-bridge.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/leader-elector.d.ts +19 -0
- package/dist/leader-elector.d.ts.map +1 -0
- package/dist/leader-elector.js +193 -0
- package/dist/leader-elector.js.map +1 -0
- package/dist/shared-transport.d.ts +84 -0
- package/dist/shared-transport.d.ts.map +1 -0
- package/dist/shared-transport.js +697 -0
- package/dist/shared-transport.js.map +1 -0
- package/package.json +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Agentick Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# @agentick/client-multiplexer
|
|
2
|
+
|
|
3
|
+
Multi-tab connection multiplexer for Agentick client. Reduces server connections by sharing a single SSE connection across all browser tabs.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @agentick/client-multiplexer
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @agentick/client-multiplexer
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createClient } from '@agentick/client';
|
|
17
|
+
import { createSharedTransport } from '@agentick/client-multiplexer';
|
|
18
|
+
|
|
19
|
+
// Create client with shared transport
|
|
20
|
+
const client = createClient({
|
|
21
|
+
baseUrl: '/api',
|
|
22
|
+
transport: createSharedTransport({ baseUrl: '/api', token: 'your-token' }),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Use exactly like a regular client
|
|
26
|
+
const session = client.session('main');
|
|
27
|
+
session.subscribe();
|
|
28
|
+
session.onEvent((event) => console.log(event));
|
|
29
|
+
|
|
30
|
+
const handle = session.send('Hello!');
|
|
31
|
+
await handle.result;
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## How It Works
|
|
35
|
+
|
|
36
|
+
The multiplexer uses browser tab leader election to ensure only one tab maintains the actual server connection:
|
|
37
|
+
|
|
38
|
+
1. **Leader Election**: Uses Web Locks API (instant, reliable) with BroadcastChannel fallback for older browsers
|
|
39
|
+
2. **Connection Sharing**: Only the leader tab opens the SSE connection to the server
|
|
40
|
+
3. **Message Forwarding**: Follower tabs send requests via BroadcastChannel to the leader
|
|
41
|
+
4. **Event Broadcasting**: Leader broadcasts server events to all tabs
|
|
42
|
+
5. **Automatic Failover**: When leader tab closes, a new leader is elected and re-establishes subscriptions
|
|
43
|
+
|
|
44
|
+
## Features
|
|
45
|
+
|
|
46
|
+
- **Resource Efficient**: Single server connection regardless of tab count
|
|
47
|
+
- **Transparent**: Works with existing Agentick client code
|
|
48
|
+
- **Automatic Failover**: Seamless recovery when leader tab closes
|
|
49
|
+
- **Subscription Aggregation**: Leader maintains union of all tabs' subscriptions
|
|
50
|
+
- **Per-Tab Filtering**: Each tab only receives events for its own sessions
|
|
51
|
+
|
|
52
|
+
## API
|
|
53
|
+
|
|
54
|
+
### createSharedTransport(config)
|
|
55
|
+
|
|
56
|
+
Creates a shared transport instance. Supports both SSE and WebSocket transports.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { createSharedTransport, type SharedTransportConfig } from '@agentick/client-multiplexer';
|
|
60
|
+
|
|
61
|
+
// SSE transport (default for http:// URLs)
|
|
62
|
+
const sseTransport = createSharedTransport({
|
|
63
|
+
baseUrl: 'https://api.example.com',
|
|
64
|
+
token: 'your-auth-token', // Optional
|
|
65
|
+
timeout: 30000, // Optional
|
|
66
|
+
withCredentials: true, // Optional
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// WebSocket transport (default for ws:// URLs)
|
|
70
|
+
const wsTransport = createSharedTransport({
|
|
71
|
+
baseUrl: 'wss://api.example.com',
|
|
72
|
+
token: 'your-auth-token',
|
|
73
|
+
clientId: 'my-client', // Optional
|
|
74
|
+
reconnect: { // Optional
|
|
75
|
+
enabled: true,
|
|
76
|
+
maxAttempts: 5,
|
|
77
|
+
delay: 1000,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Explicit transport selection
|
|
82
|
+
const explicitTransport = createSharedTransport({
|
|
83
|
+
baseUrl: 'https://api.example.com',
|
|
84
|
+
transport: 'websocket', // Force WebSocket even with http:// URL
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### SharedTransport
|
|
89
|
+
|
|
90
|
+
The transport implements `ClientTransport` from `@agentick/client` plus additional properties:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// Check leadership status
|
|
94
|
+
transport.isLeader; // boolean
|
|
95
|
+
|
|
96
|
+
// Get unique tab identifier
|
|
97
|
+
transport.tabId; // string
|
|
98
|
+
|
|
99
|
+
// Listen for leadership changes
|
|
100
|
+
transport.onLeadershipChange((isLeader) => {
|
|
101
|
+
console.log(isLeader ? 'This tab is now the leader' : 'Leadership transferred');
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Accessing Transport from Client
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { createClient, type ClientTransport } from '@agentick/client';
|
|
109
|
+
import { createSharedTransport, type SharedTransport } from '@agentick/client-multiplexer';
|
|
110
|
+
|
|
111
|
+
const client = createClient({
|
|
112
|
+
baseUrl: '/api',
|
|
113
|
+
transport: createSharedTransport({ baseUrl: '/api' }),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Access the transport for leadership info
|
|
117
|
+
const transport = client.getTransport() as SharedTransport | undefined;
|
|
118
|
+
console.log('Is leader:', transport?.isLeader);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Architecture
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
125
|
+
│ Browser Tabs │
|
|
126
|
+
├──────────────────┬──────────────────┬──────────────────────────────┤
|
|
127
|
+
│ Tab 1 │ Tab 2 │ Tab 3 │
|
|
128
|
+
│ (Leader) │ (Follower) │ (Follower) │
|
|
129
|
+
│ │ │ │
|
|
130
|
+
│ SharedTransport │ SharedTransport │ SharedTransport │
|
|
131
|
+
│ │ │ │ │ │ │
|
|
132
|
+
│ │ │ │ │ │ │
|
|
133
|
+
│ ┌───▼───┐ │ ┌───▼───┐ │ ┌───▼───┐ │
|
|
134
|
+
│ │ SSE │ │ │Bridge │ │ │Bridge │ │
|
|
135
|
+
│ │ Conn │ │ │ Only │ │ │ Only │ │
|
|
136
|
+
│ └───┬───┘ │ └───┬───┘ │ └───┬───┘ │
|
|
137
|
+
│ │ │ │ │ │ │
|
|
138
|
+
└──────┼───────────┴──────┼───────────┴──────┼───────────────────────┘
|
|
139
|
+
│ │ │
|
|
140
|
+
│ ◄─────────────┴──────────────────┘
|
|
141
|
+
│ BroadcastChannel
|
|
142
|
+
│
|
|
143
|
+
▼
|
|
144
|
+
┌───────┐
|
|
145
|
+
│Server │
|
|
146
|
+
└───────┘
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Failover
|
|
150
|
+
|
|
151
|
+
When the leader tab closes:
|
|
152
|
+
|
|
153
|
+
1. Other tabs detect leadership vacancy (via Web Locks or heartbeat timeout)
|
|
154
|
+
2. New leader is elected (fastest tab to acquire lock)
|
|
155
|
+
3. New leader broadcasts `leader:ready` message
|
|
156
|
+
4. Follower tabs respond with their current subscriptions
|
|
157
|
+
5. New leader aggregates subscriptions and re-subscribes on the server
|
|
158
|
+
6. Events flow again to all tabs
|
|
159
|
+
|
|
160
|
+
## Browser Support
|
|
161
|
+
|
|
162
|
+
- **Web Locks API**: Chrome 69+, Firefox 96+, Safari 15.4+, Edge 79+
|
|
163
|
+
- **BroadcastChannel**: All modern browsers
|
|
164
|
+
- **Fallback**: Heartbeat-based election for browsers without Web Locks
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
ISC
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broadcast Bridge
|
|
3
|
+
*
|
|
4
|
+
* Handles inter-tab communication via BroadcastChannel.
|
|
5
|
+
* Used for forwarding requests from followers to leader and events back.
|
|
6
|
+
*/
|
|
7
|
+
import type { TransportEventData } from "@agentick/client";
|
|
8
|
+
import type { SendInput, ChannelEvent, ToolConfirmationResponse } from "@agentick/client";
|
|
9
|
+
export type BridgeMessage = {
|
|
10
|
+
type: "leader:collecting_subscriptions";
|
|
11
|
+
tabId: string;
|
|
12
|
+
} | {
|
|
13
|
+
type: "leader:transport_ready";
|
|
14
|
+
tabId: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: "ping:leader";
|
|
17
|
+
tabId: string;
|
|
18
|
+
} | {
|
|
19
|
+
type: "pong:leader";
|
|
20
|
+
tabId: string;
|
|
21
|
+
} | {
|
|
22
|
+
type: "subscriptions:announce";
|
|
23
|
+
tabId: string;
|
|
24
|
+
sessions: string[];
|
|
25
|
+
channels: string[];
|
|
26
|
+
} | {
|
|
27
|
+
type: "request:send";
|
|
28
|
+
requestId: string;
|
|
29
|
+
tabId: string;
|
|
30
|
+
sessionId: string;
|
|
31
|
+
input: SendInput;
|
|
32
|
+
} | {
|
|
33
|
+
type: "request:subscribe";
|
|
34
|
+
requestId: string;
|
|
35
|
+
tabId: string;
|
|
36
|
+
sessionId: string;
|
|
37
|
+
} | {
|
|
38
|
+
type: "request:unsubscribe";
|
|
39
|
+
requestId: string;
|
|
40
|
+
tabId: string;
|
|
41
|
+
sessionId: string;
|
|
42
|
+
} | {
|
|
43
|
+
type: "request:abort";
|
|
44
|
+
requestId: string;
|
|
45
|
+
tabId: string;
|
|
46
|
+
sessionId: string;
|
|
47
|
+
reason?: string;
|
|
48
|
+
} | {
|
|
49
|
+
type: "request:close";
|
|
50
|
+
requestId: string;
|
|
51
|
+
tabId: string;
|
|
52
|
+
sessionId: string;
|
|
53
|
+
} | {
|
|
54
|
+
type: "request:toolResult";
|
|
55
|
+
requestId: string;
|
|
56
|
+
tabId: string;
|
|
57
|
+
sessionId: string;
|
|
58
|
+
toolUseId: string;
|
|
59
|
+
result: ToolConfirmationResponse;
|
|
60
|
+
} | {
|
|
61
|
+
type: "request:channelSubscribe";
|
|
62
|
+
requestId: string;
|
|
63
|
+
tabId: string;
|
|
64
|
+
sessionId: string;
|
|
65
|
+
channel: string;
|
|
66
|
+
} | {
|
|
67
|
+
type: "request:channelPublish";
|
|
68
|
+
requestId: string;
|
|
69
|
+
tabId: string;
|
|
70
|
+
sessionId: string;
|
|
71
|
+
channel: string;
|
|
72
|
+
event: ChannelEvent;
|
|
73
|
+
} | {
|
|
74
|
+
type: "response";
|
|
75
|
+
requestId: string;
|
|
76
|
+
ok: true;
|
|
77
|
+
result?: unknown;
|
|
78
|
+
} | {
|
|
79
|
+
type: "response";
|
|
80
|
+
requestId: string;
|
|
81
|
+
ok: false;
|
|
82
|
+
error: string;
|
|
83
|
+
} | {
|
|
84
|
+
type: "event";
|
|
85
|
+
event: TransportEventData;
|
|
86
|
+
} | {
|
|
87
|
+
type: "stream:event";
|
|
88
|
+
requestId: string;
|
|
89
|
+
event: TransportEventData;
|
|
90
|
+
} | {
|
|
91
|
+
type: "stream:end";
|
|
92
|
+
requestId: string;
|
|
93
|
+
} | {
|
|
94
|
+
type: "stream:error";
|
|
95
|
+
requestId: string;
|
|
96
|
+
error: string;
|
|
97
|
+
};
|
|
98
|
+
export type MessageHandler = (message: BridgeMessage) => void;
|
|
99
|
+
export interface BroadcastBridge {
|
|
100
|
+
readonly tabId: string;
|
|
101
|
+
/** Send a message to all tabs */
|
|
102
|
+
broadcast(message: BridgeMessage): void;
|
|
103
|
+
/** Register handler for incoming messages */
|
|
104
|
+
onMessage(handler: MessageHandler): () => void;
|
|
105
|
+
/**
|
|
106
|
+
* Collect responses from all tabs within a timeout.
|
|
107
|
+
* Useful for gathering subscription announcements during failover.
|
|
108
|
+
*/
|
|
109
|
+
collectResponses<T extends BridgeMessage>(messageType: T["type"], timeout: number): Promise<T[]>;
|
|
110
|
+
/** Close the bridge */
|
|
111
|
+
close(): void;
|
|
112
|
+
}
|
|
113
|
+
export declare function createBroadcastBridge(channelName: string, tabId: string): BroadcastBridge;
|
|
114
|
+
export declare function generateRequestId(tabId: string): string;
|
|
115
|
+
//# sourceMappingURL=broadcast-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"broadcast-bridge.d.ts","sourceRoot":"","sources":["../src/broadcast-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broadcast Bridge
|
|
3
|
+
*
|
|
4
|
+
* Handles inter-tab communication via BroadcastChannel.
|
|
5
|
+
* Used for forwarding requests from followers to leader and events back.
|
|
6
|
+
*/
|
|
7
|
+
export function createBroadcastBridge(channelName, tabId) {
|
|
8
|
+
const channel = new BroadcastChannel(`agentick:bridge:${channelName}`);
|
|
9
|
+
const handlers = new Set();
|
|
10
|
+
channel.onmessage = (event) => {
|
|
11
|
+
const message = event.data;
|
|
12
|
+
for (const handler of handlers) {
|
|
13
|
+
try {
|
|
14
|
+
handler(message);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
console.error("Error in bridge message handler:", e);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
get tabId() {
|
|
23
|
+
return tabId;
|
|
24
|
+
},
|
|
25
|
+
broadcast(message) {
|
|
26
|
+
channel.postMessage(message);
|
|
27
|
+
},
|
|
28
|
+
onMessage(handler) {
|
|
29
|
+
handlers.add(handler);
|
|
30
|
+
return () => handlers.delete(handler);
|
|
31
|
+
},
|
|
32
|
+
collectResponses(messageType, timeout) {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const responses = [];
|
|
35
|
+
const cleanup = this.onMessage((msg) => {
|
|
36
|
+
if (msg.type === messageType) {
|
|
37
|
+
responses.push(msg);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
cleanup();
|
|
42
|
+
resolve(responses);
|
|
43
|
+
}, timeout);
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
close() {
|
|
47
|
+
channel.close();
|
|
48
|
+
handlers.clear();
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Request Helper
|
|
54
|
+
// ============================================================================
|
|
55
|
+
let requestIdCounter = 0;
|
|
56
|
+
export function generateRequestId(tabId) {
|
|
57
|
+
return `${tabId}-${++requestIdCounter}`;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=broadcast-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"broadcast-bridge.js","sourceRoot":"","sources":["../src/broadcast-bridge.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoFH,MAAM,UAAU,qBAAqB,CAAC,WAAmB,EAAE,KAAa;IACtE,MAAM,OAAO,GAAG,IAAI,gBAAgB,CAAC,mBAAmB,WAAW,EAAE,CAAC,CAAC;IACvE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE3C,OAAO,CAAC,SAAS,GAAG,CAAC,KAAK,EAAE,EAAE;QAC5B,MAAM,OAAO,GAAG,KAAK,CAAC,IAAqB,CAAC;QAC5C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,IAAI,CAAC;gBACH,OAAO,CAAC,OAAO,CAAC,CAAC;YACnB,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,OAAO,CAAC,KAAK,CAAC,kCAAkC,EAAE,CAAC,CAAC,CAAC;YACvD,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEF,OAAO;QACL,IAAI,KAAK;YACP,OAAO,KAAK,CAAC;QACf,CAAC;QAED,SAAS,CAAC,OAAsB;YAC9B,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAC/B,CAAC;QAED,SAAS,CAAC,OAAuB;YAC/B,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACtB,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACxC,CAAC;QAED,gBAAgB,CACd,WAAsB,EACtB,OAAe;YAEf,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;gBAC7B,MAAM,SAAS,GAAQ,EAAE,CAAC;gBAE1B,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE;oBACrC,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;wBAC7B,SAAS,CAAC,IAAI,CAAC,GAAQ,CAAC,CAAC;oBAC3B,CAAC;gBACH,CAAC,CAAC,CAAC;gBAEH,UAAU,CAAC,GAAG,EAAE;oBACd,OAAO,EAAE,CAAC;oBACV,OAAO,CAAC,SAAS,CAAC,CAAC;gBACrB,CAAC,EAAE,OAAO,CAAC,CAAC;YACd,CAAC,CAAC,CAAC;QACL,CAAC;QAED,KAAK;YACH,OAAO,CAAC,KAAK,EAAE,CAAC;YAChB,QAAQ,CAAC,KAAK,EAAE,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,+EAA+E;AAC/E,iBAAiB;AACjB,+EAA+E;AAE/E,IAAI,gBAAgB,GAAG,CAAC,CAAC;AAEzB,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,OAAO,GAAG,KAAK,IAAI,EAAE,gBAAgB,EAAE,CAAC;AAC1C,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agentick/client-multiplexer
|
|
3
|
+
*
|
|
4
|
+
* Multi-tab connection multiplexer for Agentick client.
|
|
5
|
+
*
|
|
6
|
+
* Reduces server connections by electing a leader tab that maintains
|
|
7
|
+
* the SSE connection while other tabs communicate via BroadcastChannel.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Leader election using Web Locks API (instant, reliable)
|
|
11
|
+
* - BroadcastChannel fallback for older browsers
|
|
12
|
+
* - Automatic failover when leader tab closes
|
|
13
|
+
* - Per-tab subscription filtering (each tab only receives its events)
|
|
14
|
+
* - Subscription aggregation (leader subscribes to union of all tabs' sessions)
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* import { createClient } from '@agentick/client';
|
|
19
|
+
* import { createSharedTransport } from '@agentick/client-multiplexer';
|
|
20
|
+
*
|
|
21
|
+
* // Create client with shared transport
|
|
22
|
+
* const client = createClient({
|
|
23
|
+
* baseUrl: '/api',
|
|
24
|
+
* transport: createSharedTransport({ baseUrl: '/api', token: 'your-token' }),
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // Use exactly like a regular client
|
|
28
|
+
* const session = client.session('main');
|
|
29
|
+
* session.subscribe(); // Subscribe to events
|
|
30
|
+
* session.onEvent((event) => console.log(event));
|
|
31
|
+
*
|
|
32
|
+
* // Send a message
|
|
33
|
+
* const handle = session.send('Hello!');
|
|
34
|
+
* await handle.result;
|
|
35
|
+
*
|
|
36
|
+
* // Check leadership status (optional, for debugging/UI)
|
|
37
|
+
* const transport = client.getTransport() as SharedTransport | undefined;
|
|
38
|
+
* console.log('Is leader:', transport?.isLeader);
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export { SharedTransport, createSharedTransport, type SharedTransportConfig, } from "./shared-transport.js";
|
|
42
|
+
export { createLeaderElector, type LeaderElector } from "./leader-elector.js";
|
|
43
|
+
export { createBroadcastBridge, type BroadcastBridge, type BridgeMessage, } from "./broadcast-bridge.js";
|
|
44
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,KAAK,qBAAqB,GAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,mBAAmB,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAC9E,OAAO,EACL,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,aAAa,GACnB,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agentick/client-multiplexer
|
|
3
|
+
*
|
|
4
|
+
* Multi-tab connection multiplexer for Agentick client.
|
|
5
|
+
*
|
|
6
|
+
* Reduces server connections by electing a leader tab that maintains
|
|
7
|
+
* the SSE connection while other tabs communicate via BroadcastChannel.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Leader election using Web Locks API (instant, reliable)
|
|
11
|
+
* - BroadcastChannel fallback for older browsers
|
|
12
|
+
* - Automatic failover when leader tab closes
|
|
13
|
+
* - Per-tab subscription filtering (each tab only receives its events)
|
|
14
|
+
* - Subscription aggregation (leader subscribes to union of all tabs' sessions)
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* import { createClient } from '@agentick/client';
|
|
19
|
+
* import { createSharedTransport } from '@agentick/client-multiplexer';
|
|
20
|
+
*
|
|
21
|
+
* // Create client with shared transport
|
|
22
|
+
* const client = createClient({
|
|
23
|
+
* baseUrl: '/api',
|
|
24
|
+
* transport: createSharedTransport({ baseUrl: '/api', token: 'your-token' }),
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // Use exactly like a regular client
|
|
28
|
+
* const session = client.session('main');
|
|
29
|
+
* session.subscribe(); // Subscribe to events
|
|
30
|
+
* session.onEvent((event) => console.log(event));
|
|
31
|
+
*
|
|
32
|
+
* // Send a message
|
|
33
|
+
* const handle = session.send('Hello!');
|
|
34
|
+
* await handle.result;
|
|
35
|
+
*
|
|
36
|
+
* // Check leadership status (optional, for debugging/UI)
|
|
37
|
+
* const transport = client.getTransport() as SharedTransport | undefined;
|
|
38
|
+
* console.log('Is leader:', transport?.isLeader);
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export { SharedTransport, createSharedTransport, } from "./shared-transport.js";
|
|
42
|
+
export { createLeaderElector } from "./leader-elector.js";
|
|
43
|
+
export { createBroadcastBridge, } from "./broadcast-bridge.js";
|
|
44
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,EACL,eAAe,EACf,qBAAqB,GAEtB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,mBAAmB,EAAsB,MAAM,qBAAqB,CAAC;AAC9E,OAAO,EACL,qBAAqB,GAGtB,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leader Elector
|
|
3
|
+
*
|
|
4
|
+
* Uses Web Locks API for instant, reliable leader election across browser tabs.
|
|
5
|
+
* Falls back to BroadcastChannel-based election if Web Locks unavailable.
|
|
6
|
+
*/
|
|
7
|
+
export interface LeaderElector {
|
|
8
|
+
readonly isLeader: boolean;
|
|
9
|
+
readonly tabId: string;
|
|
10
|
+
awaitLeadership(): Promise<void>;
|
|
11
|
+
resign(): void;
|
|
12
|
+
onLeadershipChange(callback: (isLeader: boolean) => void): () => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Create a leader elector for the given channel name.
|
|
16
|
+
* Uses Web Locks API (instant) with BroadcastChannel fallback.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createLeaderElector(channelName: string): LeaderElector;
|
|
19
|
+
//# sourceMappingURL=leader-elector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leader-elector.d.ts","sourceRoot":"","sources":["../src/leader-elector.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,IAAI,IAAI,CAAC;IACf,kBAAkB,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACvE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,CAqNtE"}
|