hotpipe 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +390 -0
- package/dist/client/connection.d.ts +39 -0
- package/dist/client/connection.d.ts.map +1 -0
- package/dist/client/connection.js +184 -0
- package/dist/client/connection.js.map +1 -0
- package/dist/client/index.d.ts +49 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +91 -0
- package/dist/client/index.js.map +1 -0
- package/dist/server/index.d.ts +81 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +144 -0
- package/dist/server/index.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/package.json +20 -10
- package/src/client/connection.ts +0 -204
- package/src/client/index.tsx +0 -215
- package/src/server/index.ts +0 -145
package/README.md
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
# hotpipe
|
|
2
|
+
|
|
3
|
+
Type-safe real-time events for React and Next.js. Define your events with Zod, subscribe with React hooks, publish from client or server. Clients connect directly to the hotpipe API over WebSocket — your Next.js server is never in the hot path.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install hotpipe
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer dependencies: `react` (>=18), `zod` (>=3).
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
### 1. Create the route handler
|
|
16
|
+
|
|
17
|
+
Hotpipe uses your existing auth — check the session however you normally do. The `authorize` function runs on every connection attempt. Return `null` to deny access, or return the user's ID and pipe permissions to grant it. We'll cover pipe permissions in detail after the usage examples.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// app/api/realtime/[...all]/route.ts
|
|
21
|
+
import { createPipeHandler } from 'hotpipe/server';
|
|
22
|
+
|
|
23
|
+
export const { POST } = createPipeHandler({
|
|
24
|
+
secret: process.env.HOTPIPE_SECRET!,
|
|
25
|
+
authorize: async (req) => {
|
|
26
|
+
const { session } = await getAuth(req); // your auth
|
|
27
|
+
if (!session) return null;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
userId: session.userId,
|
|
31
|
+
pipes: {}, // we'll configure these in the permissions section
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Define your events
|
|
38
|
+
|
|
39
|
+
This is your single source of truth for event types across client and server.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// lib/realtime-events.ts
|
|
43
|
+
import { z } from 'zod';
|
|
44
|
+
|
|
45
|
+
export const realtimeEvents = {
|
|
46
|
+
'message.created': z.object({
|
|
47
|
+
id: z.string(),
|
|
48
|
+
text: z.string(),
|
|
49
|
+
userId: z.string(),
|
|
50
|
+
createdAt: z.number(),
|
|
51
|
+
}),
|
|
52
|
+
'message.deleted': z.object({
|
|
53
|
+
id: z.string(),
|
|
54
|
+
}),
|
|
55
|
+
'typing.start': z.object({
|
|
56
|
+
userId: z.string(),
|
|
57
|
+
}),
|
|
58
|
+
} as const;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 3. Create the client
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// lib/realtime-client.ts
|
|
65
|
+
import { createPipeClient } from 'hotpipe/client';
|
|
66
|
+
|
|
67
|
+
import { realtimeEvents } from './realtime-events';
|
|
68
|
+
|
|
69
|
+
export const { PipeProvider, usePipe } = createPipeClient({
|
|
70
|
+
events: realtimeEvents,
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This assumes your auth uses cookies (the default for most frameworks like Better Auth, NextAuth, and Clerk). If you're using bearer tokens instead, pass an `authHeaders` function:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
export const { PipeProvider, usePipe } = createPipeClient({
|
|
78
|
+
events: realtimeEvents,
|
|
79
|
+
authHeaders: async () => ({
|
|
80
|
+
Authorization: `Bearer ${getAccessToken()}`,
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 4. Add the provider
|
|
86
|
+
|
|
87
|
+
Wrap your app with `PipeProvider`. This connects everything — your frontend, your backend, and the hotpipe API.
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
// app/layout.tsx
|
|
91
|
+
import { PipeProvider } from '@/lib/realtime-client';
|
|
92
|
+
|
|
93
|
+
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
94
|
+
return <PipeProvider>{children}</PipeProvider>;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 5. Create the server publisher
|
|
99
|
+
|
|
100
|
+
You can also publish events from server actions, route handlers, and even background jobs. No limits.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
// lib/realtime-server.ts
|
|
104
|
+
import { createPipePublisher } from 'hotpipe/server';
|
|
105
|
+
|
|
106
|
+
import { realtimeEvents } from './realtime-events';
|
|
107
|
+
|
|
108
|
+
export const realtime = createPipePublisher({
|
|
109
|
+
secret: process.env.HOTPIPE_SECRET!,
|
|
110
|
+
events: realtimeEvents,
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Usage
|
|
115
|
+
|
|
116
|
+
Let's build a team chat. Users type messages, and everyone on the team sees them instantly.
|
|
117
|
+
|
|
118
|
+
### Listening for new messages
|
|
119
|
+
|
|
120
|
+
When someone sends a message, every connected user should see it appear. We exported a `usePipe` hook in step 3. This is where you'll use it.
|
|
121
|
+
|
|
122
|
+
The first argument to `usePipe` is the pipe name — it's just a string, and you can name it anything you want. We're calling this one `"team-chat"` because it's a shared space where everyone on the team sees every message. The second argument is an object of event handlers. When a `message.created` event comes in, our handler runs. The data is fully typed based on the schema you defined in step 2.
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import { usePipe } from '@/lib/realtime-client';
|
|
126
|
+
|
|
127
|
+
function Messages() {
|
|
128
|
+
const [messages, setMessages] = useState([]);
|
|
129
|
+
|
|
130
|
+
usePipe('team-chat', {
|
|
131
|
+
'message.created': (data) => {
|
|
132
|
+
setMessages((prev) => [...prev, data]);
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<ul>
|
|
138
|
+
{messages.map((msg) => (
|
|
139
|
+
<li key={msg.id}>{msg.text}</li>
|
|
140
|
+
))}
|
|
141
|
+
</ul>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
A few other pipe names to get your wheels turning:
|
|
147
|
+
|
|
148
|
+
- `"game-lobby-abc"` — real-time multiplayer game state
|
|
149
|
+
- `"player-xyz"` — events targeting a specific player
|
|
150
|
+
- `"org-acme"` — updates scoped to a whole organization
|
|
151
|
+
- `"doc-789"` — collaborative editing on a shared document
|
|
152
|
+
- `"auction-42"` — live bid updates on a specific item
|
|
153
|
+
|
|
154
|
+
The name is up to you. Whatever makes sense for your app.
|
|
155
|
+
|
|
156
|
+
### Sending a message from the browser
|
|
157
|
+
|
|
158
|
+
The same `usePipe` hook also gives you a `publish` function. When a user hits send, the event goes out to everyone listening on that pipe.
|
|
159
|
+
|
|
160
|
+
Hotpipe doesn't store anything — it's purely real-time delivery. You save data however you normally do (server actions, API calls, etc.) and publish the event alongside it.
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
function MessageInput() {
|
|
164
|
+
const [text, setText] = useState('');
|
|
165
|
+
const { publish } = usePipe('team-chat');
|
|
166
|
+
|
|
167
|
+
async function send() {
|
|
168
|
+
// save to your database first
|
|
169
|
+
const message = await createMessage({ text, userId: currentUser.id });
|
|
170
|
+
|
|
171
|
+
// then broadcast it to everyone in real time
|
|
172
|
+
publish('message.created', {
|
|
173
|
+
id: message.id,
|
|
174
|
+
text: message.text,
|
|
175
|
+
userId: message.userId,
|
|
176
|
+
createdAt: message.createdAt,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
setText('');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<form
|
|
184
|
+
onSubmit={(e) => {
|
|
185
|
+
e.preventDefault();
|
|
186
|
+
send();
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
<input value={text} onChange={(e) => setText(e.target.value)} />
|
|
190
|
+
<button type="submit">Send</button>
|
|
191
|
+
</form>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Sending events from the server
|
|
197
|
+
|
|
198
|
+
Same idea on the server. Maybe you're already saving data in a server action or handling a webhook — just publish the event after. Everyone listening on that pipe gets it instantly.
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { realtime } from '@/lib/realtime-server';
|
|
202
|
+
|
|
203
|
+
// save to your database
|
|
204
|
+
const message = await db.messages.create({ text, userId });
|
|
205
|
+
|
|
206
|
+
// broadcast to connected users
|
|
207
|
+
const teamChat = realtime.pipe('team-chat');
|
|
208
|
+
|
|
209
|
+
await teamChat.publish('message.created', {
|
|
210
|
+
id: message.id,
|
|
211
|
+
text: message.text,
|
|
212
|
+
userId: message.userId,
|
|
213
|
+
createdAt: message.createdAt,
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Connection status
|
|
218
|
+
|
|
219
|
+
You can check whether the user is connected to the hotpipe API at any time.
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
const { status } = usePipe('team-chat');
|
|
223
|
+
// 'connecting' | 'connected' | 'disconnected' | 'reconnecting'
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Hotpipe automatically reconnects if the connection drops.
|
|
227
|
+
|
|
228
|
+
## Pipes and permissions
|
|
229
|
+
|
|
230
|
+
Now that you've seen how pipes work in practice, let's go back to the `authorize` function from step 1 and talk about access control.
|
|
231
|
+
|
|
232
|
+
Every user who connects gets a set of pipe permissions. You define these in the object returned by `authorize`. Each pipe can grant two abilities: `subscribe` (receive events) and `publish` (send events).
|
|
233
|
+
|
|
234
|
+
### Team chat — everyone can read and write
|
|
235
|
+
|
|
236
|
+
In the chat example above, every team member should be able to send and receive messages. Give everyone both permissions on the `team-chat` pipe:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
authorize: async (req) => {
|
|
240
|
+
const { session } = await getAuth(req);
|
|
241
|
+
if (!session) return null;
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
userId: session.userId,
|
|
245
|
+
pipes: {
|
|
246
|
+
'team-chat': { subscribe: true, publish: true },
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### User-specific pipes — scoped to a single user
|
|
253
|
+
|
|
254
|
+
Sometimes you need to send events to a specific user — a notification, a status update, something only they should see. Use a dynamic pipe name with their user ID:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
return {
|
|
258
|
+
userId: session.userId,
|
|
259
|
+
pipes: {
|
|
260
|
+
[`user-${session.userId}`]: { subscribe: true },
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Now your server can publish to `user-abc123` and only that user receives it. Other users can't subscribe to someone else's pipe because it's not in their permissions.
|
|
266
|
+
|
|
267
|
+
### Read-only pipes — broadcast without client publish
|
|
268
|
+
|
|
269
|
+
For things like live dashboards, announcements, or system alerts, you might want users to receive events but not send them. Only grant `subscribe`:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
return {
|
|
273
|
+
userId: session.userId,
|
|
274
|
+
pipes: {
|
|
275
|
+
announcements: { subscribe: true },
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The server can publish to `announcements` anytime. Clients can listen but can't push events to this pipe.
|
|
281
|
+
|
|
282
|
+
### Combining pipes — a real-world example
|
|
283
|
+
|
|
284
|
+
Most apps need a mix. Here's what a team collaboration app might look like:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
authorize: async (req) => {
|
|
288
|
+
const { session } = await getAuth(req);
|
|
289
|
+
if (!session) return null;
|
|
290
|
+
|
|
291
|
+
const teamId = await getTeamId(session.userId);
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
userId: session.userId,
|
|
295
|
+
pipes: {
|
|
296
|
+
[`team-${teamId}`]: { subscribe: true, publish: true },
|
|
297
|
+
[`user-${session.userId}`]: { subscribe: true },
|
|
298
|
+
'announcements': { subscribe: true },
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
},
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Three pipes, three different access patterns:
|
|
305
|
+
|
|
306
|
+
- **`team-abc`** — everyone on the team can send and receive (chat, typing indicators, live cursors)
|
|
307
|
+
- **`user-xyz`** — only this user receives (notifications, direct messages from the server)
|
|
308
|
+
- **`announcements`** — all users receive, only the server publishes (maintenance alerts, feature launches)
|
|
309
|
+
|
|
310
|
+
If a user tries to subscribe or publish to a pipe that isn't in their permissions, the API ignores the request.
|
|
311
|
+
|
|
312
|
+
## Environment variables
|
|
313
|
+
|
|
314
|
+
| Variable | Where | Description |
|
|
315
|
+
| ---------------- | ------ | ----------------------------------------------------- |
|
|
316
|
+
| `HOTPIPE_SECRET` | Server | Shared secret for auth tokens and server-side publish |
|
|
317
|
+
|
|
318
|
+
## How it works
|
|
319
|
+
|
|
320
|
+
1. User loads your app
|
|
321
|
+
2. `PipeProvider` calls your handler to get an auth token
|
|
322
|
+
3. Your handler checks the session and signs a JWT with pipe permissions
|
|
323
|
+
4. The client opens a WebSocket directly to the hotpipe API
|
|
324
|
+
5. Events flow between clients and server through the hotpipe API
|
|
325
|
+
6. Your Next.js server is out of the loop — no long-lived connections, no Vercel cost impact
|
|
326
|
+
|
|
327
|
+
The hotpipe API is stateless. It handles connection management and event fan-out. Your database is the source of truth — the API doesn't persist anything.
|
|
328
|
+
|
|
329
|
+
## API reference
|
|
330
|
+
|
|
331
|
+
### `hotpipe/client`
|
|
332
|
+
|
|
333
|
+
#### `createPipeClient(config)`
|
|
334
|
+
|
|
335
|
+
Returns `{ PipeProvider, usePipe }`.
|
|
336
|
+
|
|
337
|
+
| Option | Type | Required | Description |
|
|
338
|
+
| ------------- | --------------------------------------- | -------- | ----------------------------------------------------------- |
|
|
339
|
+
| `events` | `Record<string, ZodType>` | Yes | Zod schemas for your events |
|
|
340
|
+
| `basePath` | `string` | No | Where your handler is mounted (defaults to `/api/realtime`) |
|
|
341
|
+
| `authHeaders` | `() => Promise<Record<string, string>>` | No | Custom headers for the auth request (e.g., bearer tokens) |
|
|
342
|
+
|
|
343
|
+
#### `PipeProvider`
|
|
344
|
+
|
|
345
|
+
Wrap your app (or a subtree) to establish the WebSocket connection to the hotpipe API. Connects on mount, disconnects on unmount.
|
|
346
|
+
|
|
347
|
+
#### `usePipe(pipe, handlers?)`
|
|
348
|
+
|
|
349
|
+
Subscribe to events on a pipe.
|
|
350
|
+
|
|
351
|
+
| Argument | Type | Description |
|
|
352
|
+
| ---------- | ------------------------------------------ | ---------------------------------------------------- |
|
|
353
|
+
| `pipe` | `string` | The pipe name to subscribe to |
|
|
354
|
+
| `handlers` | `{ [event]: (data) => void }` _(optional)_ | Event handlers, keyed by event name from your schema |
|
|
355
|
+
|
|
356
|
+
Returns `{ status, publish }`:
|
|
357
|
+
|
|
358
|
+
- **`status`** — Reactive connection state: `'connecting'`, `'connected'`, `'disconnected'`, or `'reconnecting'`. Updates automatically and triggers a re-render on change.
|
|
359
|
+
- **`publish(event, data)`** — Send a typed event to everyone on this pipe. Data is validated against your Zod schema before sending. Returns `true` if the event was sent or queued for delivery, `false` if the queue is full. Throws if the data fails validation. If the connection is temporarily down, events are buffered and flushed automatically when the connection comes back (up to 100 events, within 30 seconds).
|
|
360
|
+
|
|
361
|
+
The hook automatically subscribes when the component mounts and unsubscribes when it unmounts. If multiple components subscribe to the same pipe, hotpipe ref-counts them — the subscription stays active until the last component unmounts.
|
|
362
|
+
|
|
363
|
+
### `hotpipe/server`
|
|
364
|
+
|
|
365
|
+
#### `createPipeHandler(config)`
|
|
366
|
+
|
|
367
|
+
Returns `{ POST }` for your catch-all route handler.
|
|
368
|
+
|
|
369
|
+
| Option | Type | Required | Description |
|
|
370
|
+
| ------------- | ----------------------------------------------- | -------- | ----------------------------------------- |
|
|
371
|
+
| `secret` | `string` | Yes | Shared signing secret |
|
|
372
|
+
| `authorize` | `(req: Request) => Promise<AuthResult \| null>` | Yes | Your auth logic |
|
|
373
|
+
| `tokenExpiry` | `number` | No | Token lifetime in seconds (default: 3600) |
|
|
374
|
+
|
|
375
|
+
#### `createPipePublisher(config)`
|
|
376
|
+
|
|
377
|
+
Returns `{ pipe }`.
|
|
378
|
+
|
|
379
|
+
| Option | Type | Required | Description |
|
|
380
|
+
| -------- | ------------------------- | -------- | --------------------------- |
|
|
381
|
+
| `secret` | `string` | Yes | Your hotpipe secret |
|
|
382
|
+
| `events` | `Record<string, ZodType>` | Yes | Zod schemas for your events |
|
|
383
|
+
|
|
384
|
+
#### `publisher.pipe(name)`
|
|
385
|
+
|
|
386
|
+
Returns `{ publish }` for server-side event publishing. Unlike the client-side `publish`, this is async — it makes an HTTP request to the hotpipe API and will throw if the request fails.
|
|
387
|
+
|
|
388
|
+
## License
|
|
389
|
+
|
|
390
|
+
MIT
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
|
|
2
|
+
export type PipeListener = (event: string, data: unknown) => void;
|
|
3
|
+
interface ConnectionConfig {
|
|
4
|
+
brokerUrl: string;
|
|
5
|
+
authEndpoint: string;
|
|
6
|
+
authHeaders?: () => Promise<Record<string, string>>;
|
|
7
|
+
}
|
|
8
|
+
export declare class ConnectionManager {
|
|
9
|
+
private config;
|
|
10
|
+
private ws;
|
|
11
|
+
private status;
|
|
12
|
+
private statusListeners;
|
|
13
|
+
private pipeListeners;
|
|
14
|
+
private pipeRefs;
|
|
15
|
+
private reconnectAttempts;
|
|
16
|
+
private maxReconnectDelay;
|
|
17
|
+
private pingInterval;
|
|
18
|
+
private reconnectTimer;
|
|
19
|
+
private intentionalClose;
|
|
20
|
+
private eventQueue;
|
|
21
|
+
constructor(config: ConnectionConfig);
|
|
22
|
+
connect(): Promise<void>;
|
|
23
|
+
disconnect(): void;
|
|
24
|
+
subscribe(pipe: string): void;
|
|
25
|
+
unsubscribe(pipe: string): void;
|
|
26
|
+
publish(pipe: string, event: string, data: unknown): boolean;
|
|
27
|
+
addPipeListener(pipe: string, listener: PipeListener): void;
|
|
28
|
+
removePipeListener(pipe: string, listener: PipeListener): void;
|
|
29
|
+
getStatus(): ConnectionStatus;
|
|
30
|
+
onStatusChange(listener: (status: ConnectionStatus) => void): () => void;
|
|
31
|
+
private flushQueue;
|
|
32
|
+
private dispatch;
|
|
33
|
+
private setStatus;
|
|
34
|
+
private wsSend;
|
|
35
|
+
private cleanup;
|
|
36
|
+
private scheduleReconnect;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=connection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/client/connection.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,gBAAgB,GAAG,cAAc,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,CAAC;AAE5F,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;AAElE,UAAU,gBAAgB;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACrD;AAYD,qBAAa,iBAAiB;IAahB,OAAO,CAAC,MAAM;IAZ1B,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,MAAM,CAAoC;IAClD,OAAO,CAAC,eAAe,CAAiD;IACxE,OAAO,CAAC,aAAa,CAAwC;IAC7D,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,iBAAiB,CAAU;IACnC,OAAO,CAAC,YAAY,CAA+C;IACnE,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,UAAU,CAAqB;gBAEnB,MAAM,EAAE,gBAAgB;IAEtC,OAAO;IA2Eb,UAAU;IAeV,SAAS,CAAC,IAAI,EAAE,MAAM;IAStB,WAAW,CAAC,IAAI,EAAE,MAAM;IAcxB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO;IAkB5D,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY;IAQpD,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY;IAQvD,SAAS,IAAI,gBAAgB;IAI7B,cAAc,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,GAAG,MAAM,IAAI;IAKxE,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,OAAO;IAOf,OAAO,CAAC,iBAAiB;CAW1B"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { SDK_VERSION } from '../version';
|
|
2
|
+
const MAX_QUEUE_SIZE = 100;
|
|
3
|
+
const QUEUE_TTL = 30_000;
|
|
4
|
+
export class ConnectionManager {
|
|
5
|
+
config;
|
|
6
|
+
ws = null;
|
|
7
|
+
status = 'disconnected';
|
|
8
|
+
statusListeners = new Set();
|
|
9
|
+
pipeListeners = new Map();
|
|
10
|
+
pipeRefs = new Map();
|
|
11
|
+
reconnectAttempts = 0;
|
|
12
|
+
maxReconnectDelay = 30_000;
|
|
13
|
+
pingInterval = null;
|
|
14
|
+
reconnectTimer = null;
|
|
15
|
+
intentionalClose = false;
|
|
16
|
+
eventQueue = [];
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
}
|
|
20
|
+
async connect() {
|
|
21
|
+
if (this.status === 'connecting' || this.status === 'connected')
|
|
22
|
+
return;
|
|
23
|
+
this.intentionalClose = false;
|
|
24
|
+
this.setStatus(this.reconnectAttempts > 0 ? 'reconnecting' : 'connecting');
|
|
25
|
+
try {
|
|
26
|
+
const headers = this.config.authHeaders ? await this.config.authHeaders() : undefined;
|
|
27
|
+
const res = await fetch(this.config.authEndpoint, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
credentials: 'include',
|
|
30
|
+
headers,
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
throw new Error(`Auth failed: ${res.status}`);
|
|
34
|
+
}
|
|
35
|
+
const { token } = await res.json();
|
|
36
|
+
const wsUrl = `${this.config.brokerUrl}/ws?token=${encodeURIComponent(token)}&v=${SDK_VERSION}`;
|
|
37
|
+
this.ws = new WebSocket(wsUrl);
|
|
38
|
+
this.ws.onopen = () => {
|
|
39
|
+
this.setStatus('connected');
|
|
40
|
+
this.reconnectAttempts = 0;
|
|
41
|
+
// Resubscribe to all active pipes
|
|
42
|
+
for (const pipe of this.pipeRefs.keys()) {
|
|
43
|
+
this.wsSend({ type: 'subscribe', pipe });
|
|
44
|
+
}
|
|
45
|
+
// Flush queued events
|
|
46
|
+
this.flushQueue();
|
|
47
|
+
// Keepalive ping every 30s
|
|
48
|
+
this.pingInterval = setInterval(() => {
|
|
49
|
+
this.wsSend({ type: 'ping' });
|
|
50
|
+
}, 30_000);
|
|
51
|
+
};
|
|
52
|
+
this.ws.onmessage = (event) => {
|
|
53
|
+
try {
|
|
54
|
+
const msg = JSON.parse(event.data);
|
|
55
|
+
if (msg.type === 'event') {
|
|
56
|
+
this.dispatch(msg.pipe, msg.event, msg.data);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Ignore malformed messages
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
this.ws.onclose = () => {
|
|
64
|
+
this.cleanup();
|
|
65
|
+
this.setStatus('disconnected');
|
|
66
|
+
if (!this.intentionalClose) {
|
|
67
|
+
this.scheduleReconnect();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
this.ws.onerror = () => {
|
|
71
|
+
// onclose fires after onerror — reconnect handled there
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
this.setStatus('disconnected');
|
|
76
|
+
if (!this.intentionalClose) {
|
|
77
|
+
this.scheduleReconnect();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
disconnect() {
|
|
82
|
+
this.intentionalClose = true;
|
|
83
|
+
this.cleanup();
|
|
84
|
+
if (this.reconnectTimer) {
|
|
85
|
+
clearTimeout(this.reconnectTimer);
|
|
86
|
+
this.reconnectTimer = null;
|
|
87
|
+
}
|
|
88
|
+
this.ws?.close();
|
|
89
|
+
this.ws = null;
|
|
90
|
+
this.eventQueue = [];
|
|
91
|
+
this.setStatus('disconnected');
|
|
92
|
+
}
|
|
93
|
+
subscribe(pipe) {
|
|
94
|
+
const count = this.pipeRefs.get(pipe) || 0;
|
|
95
|
+
this.pipeRefs.set(pipe, count + 1);
|
|
96
|
+
if (count === 0 && this.ws?.readyState === WebSocket.OPEN) {
|
|
97
|
+
this.wsSend({ type: 'subscribe', pipe });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
unsubscribe(pipe) {
|
|
101
|
+
const count = this.pipeRefs.get(pipe) || 0;
|
|
102
|
+
if (count <= 1) {
|
|
103
|
+
this.pipeRefs.delete(pipe);
|
|
104
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
105
|
+
this.wsSend({ type: 'unsubscribe', pipe });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
this.pipeRefs.set(pipe, count - 1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
publish(pipe, event, data) {
|
|
113
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
114
|
+
this.wsSend({ type: 'publish', pipe, event, data });
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
if (this.intentionalClose) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (this.eventQueue.length >= MAX_QUEUE_SIZE) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
this.eventQueue.push({ pipe, event, data, timestamp: Date.now() });
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
addPipeListener(pipe, listener) {
|
|
127
|
+
if (!this.pipeListeners.has(pipe)) {
|
|
128
|
+
this.pipeListeners.set(pipe, new Set());
|
|
129
|
+
}
|
|
130
|
+
this.pipeListeners.get(pipe).add(listener);
|
|
131
|
+
}
|
|
132
|
+
removePipeListener(pipe, listener) {
|
|
133
|
+
this.pipeListeners.get(pipe)?.delete(listener);
|
|
134
|
+
if (this.pipeListeners.get(pipe)?.size === 0) {
|
|
135
|
+
this.pipeListeners.delete(pipe);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
getStatus() {
|
|
139
|
+
return this.status;
|
|
140
|
+
}
|
|
141
|
+
onStatusChange(listener) {
|
|
142
|
+
this.statusListeners.add(listener);
|
|
143
|
+
return () => this.statusListeners.delete(listener);
|
|
144
|
+
}
|
|
145
|
+
flushQueue() {
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
const pending = this.eventQueue.filter((e) => now - e.timestamp < QUEUE_TTL);
|
|
148
|
+
this.eventQueue = [];
|
|
149
|
+
for (const { pipe, event, data } of pending) {
|
|
150
|
+
this.wsSend({ type: 'publish', pipe, event, data });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
dispatch(pipe, event, data) {
|
|
154
|
+
const listeners = this.pipeListeners.get(pipe);
|
|
155
|
+
if (!listeners)
|
|
156
|
+
return;
|
|
157
|
+
for (const listener of listeners) {
|
|
158
|
+
listener(event, data);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
setStatus(status) {
|
|
162
|
+
this.status = status;
|
|
163
|
+
for (const listener of this.statusListeners) {
|
|
164
|
+
listener(status);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
wsSend(msg) {
|
|
168
|
+
this.ws?.send(JSON.stringify(msg));
|
|
169
|
+
}
|
|
170
|
+
cleanup() {
|
|
171
|
+
if (this.pingInterval) {
|
|
172
|
+
clearInterval(this.pingInterval);
|
|
173
|
+
this.pingInterval = null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
scheduleReconnect() {
|
|
177
|
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts) + Math.random() * 1000, this.maxReconnectDelay);
|
|
178
|
+
this.reconnectTimer = setTimeout(() => {
|
|
179
|
+
this.reconnectAttempts++;
|
|
180
|
+
this.connect();
|
|
181
|
+
}, delay);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=connection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../../src/client/connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAmBzC,MAAM,cAAc,GAAG,GAAG,CAAC;AAC3B,MAAM,SAAS,GAAG,MAAM,CAAC;AAEzB,MAAM,OAAO,iBAAiB;IAaR;IAZZ,EAAE,GAAqB,IAAI,CAAC;IAC5B,MAAM,GAAqB,cAAc,CAAC;IAC1C,eAAe,GAAG,IAAI,GAAG,EAAsC,CAAC;IAChE,aAAa,GAAG,IAAI,GAAG,EAA6B,CAAC;IACrD,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IACrC,iBAAiB,GAAG,CAAC,CAAC;IACtB,iBAAiB,GAAG,MAAM,CAAC;IAC3B,YAAY,GAA0C,IAAI,CAAC;IAC3D,cAAc,GAAyC,IAAI,CAAC;IAC5D,gBAAgB,GAAG,KAAK,CAAC;IACzB,UAAU,GAAkB,EAAE,CAAC;IAEvC,YAAoB,MAAwB;QAAxB,WAAM,GAAN,MAAM,CAAkB;IAAG,CAAC;IAEhD,KAAK,CAAC,OAAO;QACX,IAAI,IAAI,CAAC,MAAM,KAAK,YAAY,IAAI,IAAI,CAAC,MAAM,KAAK,WAAW;YAAE,OAAO;QAExE,IAAI,CAAC,gBAAgB,GAAG,KAAK,CAAC;QAC9B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,iBAAiB,GAAG,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;QAE3E,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;YAEtF,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE;gBAChD,MAAM,EAAE,MAAM;gBACd,WAAW,EAAE,SAAS;gBACtB,OAAO;aACR,CAAC,CAAC;YAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,gBAAgB,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;YAChD,CAAC;YAED,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;YACnC,MAAM,KAAK,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,aAAa,kBAAkB,CAAC,KAAK,CAAC,MAAM,WAAW,EAAE,CAAC;YAEhG,IAAI,CAAC,EAAE,GAAG,IAAI,SAAS,CAAC,KAAK,CAAC,CAAC;YAE/B,IAAI,CAAC,EAAE,CAAC,MAAM,GAAG,GAAG,EAAE;gBACpB,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;gBAC5B,IAAI,CAAC,iBAAiB,GAAG,CAAC,CAAC;gBAE3B,kCAAkC;gBAClC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;oBACxC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;gBAC3C,CAAC;gBAED,sBAAsB;gBACtB,IAAI,CAAC,UAAU,EAAE,CAAC;gBAElB,2BAA2B;gBAC3B,IAAI,CAAC,YAAY,GAAG,WAAW,CAAC,GAAG,EAAE;oBACnC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;gBAChC,CAAC,EAAE,MAAM,CAAC,CAAC;YACb,CAAC,CAAC;YAEF,IAAI,CAAC,EAAE,CAAC,SAAS,GAAG,CAAC,KAAK,EAAE,EAAE;gBAC5B,IAAI,CAAC;oBACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;oBAEnC,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;wBACzB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;oBAC/C,CAAC;gBACH,CAAC;gBAAC,MAAM,CAAC;oBACP,4BAA4B;gBAC9B,CAAC;YACH,CAAC,CAAC;YAEF,IAAI,CAAC,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE;gBACrB,IAAI,CAAC,OAAO,EAAE,CAAC;gBACf,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;gBAE/B,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;oBAC3B,IAAI,CAAC,iBAAiB,EAAE,CAAC;gBAC3B,CAAC;YACH,CAAC,CAAC;YAEF,IAAI,CAAC,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE;gBACrB,wDAAwD;YAC1D,CAAC,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;YAE/B,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;gBAC3B,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAC3B,CAAC;QACH,CAAC;IACH,CAAC;IAED,UAAU;QACR,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC7B,IAAI,CAAC,OAAO,EAAE,CAAC;QAEf,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YAClC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC7B,CAAC;QAED,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC;QACjB,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC;QACf,IAAI,CAAC,UAAU,GAAG,EAAE,CAAC;QACrB,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;IACjC,CAAC;IAED,SAAS,CAAC,IAAY;QACpB,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;QAEnC,IAAI,KAAK,KAAK,CAAC,IAAI,IAAI,CAAC,EAAE,EAAE,UAAU,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC;YAC1D,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC;IAED,WAAW,CAAC,IAAY;QACtB,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAE3C,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;YACf,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAE3B,IAAI,IAAI,CAAC,EAAE,EAAE,UAAU,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC;gBAC3C,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;YAC7C,CAAC;QACH,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IAED,OAAO,CAAC,IAAY,EAAE,KAAa,EAAE,IAAa;QAChD,IAAI,IAAI,CAAC,EAAE,EAAE,UAAU,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC;YAC3C,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACpD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC1B,OAAO,KAAK,CAAC;QACf,CAAC;QAED,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,IAAI,cAAc,EAAE,CAAC;YAC7C,OAAO,KAAK,CAAC;QACf,CAAC;QAED,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACnE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,eAAe,CAAC,IAAY,EAAE,QAAsB;QAClD,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAClC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;QAC1C,CAAC;QAED,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC9C,CAAC;IAED,kBAAkB,CAAC,IAAY,EAAE,QAAsB;QACrD,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;QAE/C,IAAI,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,KAAK,CAAC,EAAE,CAAC;YAC7C,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,cAAc,CAAC,QAA4C;QACzD,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACnC,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACrD,CAAC;IAEO,UAAU;QAChB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;QAC7E,IAAI,CAAC,UAAU,GAAG,EAAE,CAAC;QAErB,KAAK,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,OAAO,EAAE,CAAC;YAC5C,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;IAEO,QAAQ,CAAC,IAAY,EAAE,KAAa,EAAE,IAAa;QACzD,MAAM,SAAS,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC/C,IAAI,CAAC,SAAS;YAAE,OAAO;QAEvB,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;YACjC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;IAEO,SAAS,CAAC,MAAwB;QACxC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QAErB,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YAC5C,QAAQ,CAAC,MAAM,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;IAEO,MAAM,CAAC,GAA4B;QACzC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;IACrC,CAAC;IAEO,OAAO;QACb,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,aAAa,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YACjC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QAC3B,CAAC;IACH,CAAC;IAEO,iBAAiB;QACvB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CACpB,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,EACjE,IAAI,CAAC,iBAAiB,CACvB,CAAC;QAEF,IAAI,CAAC,cAAc,GAAG,UAAU,CAAC,GAAG,EAAE;YACpC,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACzB,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,CAAC,EAAE,KAAK,CAAC,CAAC;IACZ,CAAC;CACF"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import { type ConnectionStatus } from './connection';
|
|
3
|
+
/**
|
|
4
|
+
* A record mapping event names to Zod schemas.
|
|
5
|
+
*/
|
|
6
|
+
type EventMap = Record<string, z.ZodType>;
|
|
7
|
+
/**
|
|
8
|
+
* Extract event names from an EventMap.
|
|
9
|
+
*/
|
|
10
|
+
type EventName<T extends EventMap> = keyof T & string;
|
|
11
|
+
/**
|
|
12
|
+
* Infer the data type for a given event.
|
|
13
|
+
*/
|
|
14
|
+
type EventData<T extends EventMap, E extends EventName<T>> = z.infer<T[E]>;
|
|
15
|
+
/**
|
|
16
|
+
* Handler map for usePipe — a partial record of event handlers.
|
|
17
|
+
*/
|
|
18
|
+
type PipeHandlers<T extends EventMap> = {
|
|
19
|
+
[E in EventName<T>]?: (data: EventData<T, E>) => void;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Creates a typed real-time client bound to your event schemas.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* ```ts
|
|
26
|
+
* const { PipeProvider, usePipe } = createPipeClient({
|
|
27
|
+
* events: realtimeEvents,
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function createPipeClient<T extends EventMap>(config: {
|
|
32
|
+
events: T;
|
|
33
|
+
/** Override the base path where your handler is mounted. Defaults to `/api/realtime`. */
|
|
34
|
+
basePath?: string;
|
|
35
|
+
/** @internal Override the broker URL for local development. */
|
|
36
|
+
brokerUrl?: string;
|
|
37
|
+
/** Provide custom headers for the auth request (e.g., a Bearer token). */
|
|
38
|
+
authHeaders?: () => Promise<Record<string, string>>;
|
|
39
|
+
}): {
|
|
40
|
+
PipeProvider: ({ children }: {
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
43
|
+
usePipe: (pipe: string, handlers?: Partial<PipeHandlers<T>>) => {
|
|
44
|
+
status: ConnectionStatus;
|
|
45
|
+
publish: <E extends EventName<T>>(event: E, data: EventData<T, E>) => boolean;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
export type { ConnectionStatus, EventMap };
|
|
49
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAExE;;GAEG;AACH,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;AAE1C;;GAEG;AACH,KAAK,SAAS,CAAC,CAAC,SAAS,QAAQ,IAAI,MAAM,CAAC,GAAG,MAAM,CAAC;AAEtD;;GAEG;AACH,KAAK,SAAS,CAAC,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE3E;;GAEG;AACH,KAAK,YAAY,CAAC,CAAC,SAAS,QAAQ,IAAI;KACrC,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI;CACtD,CAAC;AAKF;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,QAAQ,EAAE,MAAM,EAAE;IAC3D,MAAM,EAAE,CAAC,CAAC;IACV,yFAAyF;IACzF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACrD;iCAiBqC;QAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;KAAE;oBAgCzD,MAAM,aACD,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,KAClC;QACD,MAAM,EAAE,gBAAgB,CAAC;QACzB,OAAO,EAAE,CAAC,CAAC,SAAS,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,OAAO,CAAC;KAC/E;EAoDF;AAED,YAAY,EAAE,gBAAgB,EAAE,QAAQ,EAAE,CAAC"}
|