@foony/realtime 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/README.md +118 -0
- package/lib/channel.d.ts +64 -0
- package/lib/channel.d.ts.map +1 -0
- package/lib/channel.js +123 -0
- package/lib/channel.js.map +1 -0
- package/lib/connection.d.ts +139 -0
- package/lib/connection.d.ts.map +1 -0
- package/lib/connection.js +333 -0
- package/lib/connection.js.map +1 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +13 -0
- package/lib/index.js.map +1 -0
- package/lib/realtime.d.ts +35 -0
- package/lib/realtime.d.ts.map +1 -0
- package/lib/realtime.js +64 -0
- package/lib/realtime.js.map +1 -0
- package/lib/server.d.ts +45 -0
- package/lib/server.d.ts.map +1 -0
- package/lib/server.js +53 -0
- package/lib/server.js.map +1 -0
- package/lib/wire.d.ts +128 -0
- package/lib/wire.d.ts.map +1 -0
- package/lib/wire.js +27 -0
- package/lib/wire.js.map +1 -0
- package/package.json +57 -0
- package/src/channel.ts +138 -0
- package/src/connection.ts +450 -0
- package/src/index.ts +40 -0
- package/src/realtime.ts +80 -0
- package/src/server.ts +83 -0
- package/src/wire.ts +177 -0
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# @foony/realtime
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for the Foony Realtime service. A small client for the
|
|
4
|
+
wire protocol implemented by `services/realtime-saas` — connect, sub /
|
|
5
|
+
unsub, publish, and presence.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @foony/realtime
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The package ships compiled ESM output and TypeScript declarations.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
### Browser / Foony client
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { Realtime } from '@foony/realtime';
|
|
21
|
+
|
|
22
|
+
const realtime = new Realtime({
|
|
23
|
+
url: 'wss://realtime.foony.com',
|
|
24
|
+
authCallback: async () => {
|
|
25
|
+
const response = await fetch('/api/realtime/token');
|
|
26
|
+
return await response.text();
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const channel = realtime.channels.get('chat:room-1');
|
|
31
|
+
|
|
32
|
+
channel.subscribe((message) => {
|
|
33
|
+
console.log('chat message:', message.data);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await channel.publish('chat', { text: 'hello world' });
|
|
37
|
+
|
|
38
|
+
channel.presence.subscribe((event) => {
|
|
39
|
+
console.log(event.action, event.clientId, event.data);
|
|
40
|
+
});
|
|
41
|
+
await channel.presence.enter({ name: 'Alice' });
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Node / server (token minting)
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { mintRealtimeToken } from '@foony/realtime/server';
|
|
48
|
+
|
|
49
|
+
app.get('/api/realtime/token', (req, res) => {
|
|
50
|
+
const token = mintRealtimeToken({
|
|
51
|
+
signingKey: process.env.REALTIME_JWT_SIGNING_KEY!,
|
|
52
|
+
appId: 'foony',
|
|
53
|
+
clientId: req.user.id,
|
|
54
|
+
capability: '{"chat:*":["subscribe","publish","presence"]}',
|
|
55
|
+
ttlMs: 15 * 60 * 1000,
|
|
56
|
+
});
|
|
57
|
+
res.type('text/plain').send(token);
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The signing key must exactly match the `JWT_SIGNING_KEY` env var the
|
|
62
|
+
realtime edge binary boots with.
|
|
63
|
+
|
|
64
|
+
## Local development against the realtime backend
|
|
65
|
+
|
|
66
|
+
Start the backend following `services/realtime-saas/README.md`. Then
|
|
67
|
+
mint a dev token:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
cd services/realtime-saas
|
|
71
|
+
JWT_SIGNING_KEY=local-dev-key go run ./cmd/devtoken -app foony -client alice
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Use the printed token in the SDK:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const realtime = new Realtime({
|
|
78
|
+
url: 'ws://localhost:3000',
|
|
79
|
+
token: process.env.FOONY_REALTIME_DEV_TOKEN!,
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Channel names
|
|
84
|
+
|
|
85
|
+
Channel names must match `[A-Za-z0-9._-]{1,255}` and cannot start or
|
|
86
|
+
end with a `.`. Use dots to express hierarchy (`chat.rooms.42`). The
|
|
87
|
+
server rejects invalid names with error code `40001` (`BadFrame`).
|
|
88
|
+
|
|
89
|
+
## API surface
|
|
90
|
+
|
|
91
|
+
- `Realtime` — top-level client. Owns the WebSocket; channels attach lazily.
|
|
92
|
+
- `client.channels.get(name)` — returns a stable `Channel` for that name.
|
|
93
|
+
- `channel.subscribe(fn)` — message listener; returns an unsubscribe fn.
|
|
94
|
+
- `channel.publish(name, data)` — publish one message; resolves on ack.
|
|
95
|
+
- `channel.presence.subscribe(fn)` — presence listener.
|
|
96
|
+
- `channel.presence.enter|update|leave(data?)` — mutate this connection's membership.
|
|
97
|
+
- `client.onStateChange(fn)` — observe `connecting | connected | disconnected | closed | failed`.
|
|
98
|
+
|
|
99
|
+
## Reconnect
|
|
100
|
+
|
|
101
|
+
When the connection drops unexpectedly the client retries with
|
|
102
|
+
exponential backoff (1s, 2s, 4s, ..., capped at 30s). All
|
|
103
|
+
subscriptions that were established before the disconnect are
|
|
104
|
+
re-issued automatically; presence membership is NOT automatically
|
|
105
|
+
restored — call `enter()` again on the `disconnected -> connected`
|
|
106
|
+
transition if you need it.
|
|
107
|
+
|
|
108
|
+
Pass `autoReconnect: false` to disable retries entirely (useful in tests).
|
|
109
|
+
|
|
110
|
+
## Tests
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npm test
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Runs unit tests (wire + token mint) plus an in-process end-to-end test
|
|
117
|
+
that drives the SDK against a fake edge built on `ws`. No external
|
|
118
|
+
services required.
|
package/lib/channel.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel + Presence public API. Wraps the Connection layer with
|
|
3
|
+
* per-channel state.
|
|
4
|
+
*/
|
|
5
|
+
import type { Connection, MessageListener, PresenceEventListener } from './connection.js';
|
|
6
|
+
/** Listener handle returned by `subscribe` — call to remove the listener. */
|
|
7
|
+
export type UnsubscribeFn = () => void;
|
|
8
|
+
/**
|
|
9
|
+
* One subscription handle per (channel, listener) pair. Channels are
|
|
10
|
+
* value-equal by name on a given Realtime client — calling
|
|
11
|
+
* `client.channels.get('chat:1')` twice returns the same instance.
|
|
12
|
+
*/
|
|
13
|
+
export declare class Channel {
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly presence: Presence;
|
|
16
|
+
private readonly connection;
|
|
17
|
+
private attachPromise;
|
|
18
|
+
private attached;
|
|
19
|
+
constructor(connection: Connection, name: string);
|
|
20
|
+
/**
|
|
21
|
+
* Ensure the server is subscribed to this channel. Called implicitly
|
|
22
|
+
* by `subscribe()` and `presence.subscribe()`; expose it so callers
|
|
23
|
+
* can pre-attach if they want to surface attach errors before the
|
|
24
|
+
* first message arrives.
|
|
25
|
+
*/
|
|
26
|
+
attach(): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Detach from the server (stop receiving messages and presence
|
|
29
|
+
* events). Local listeners are preserved — call `unsubscribe()` to
|
|
30
|
+
* clear them.
|
|
31
|
+
*/
|
|
32
|
+
detach(): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Register a listener for message frames on this channel. Implicitly
|
|
35
|
+
* attaches if needed. Returns an unsubscribe function.
|
|
36
|
+
*/
|
|
37
|
+
subscribe(listener: MessageListener): UnsubscribeFn;
|
|
38
|
+
/** Publish one application-level message to the channel. */
|
|
39
|
+
publish(name: string, data: unknown): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Per-channel presence facade. Wraps the `pres` frame and `presEvt`
|
|
43
|
+
* listener dispatch.
|
|
44
|
+
*/
|
|
45
|
+
export declare class Presence {
|
|
46
|
+
private readonly connection;
|
|
47
|
+
private readonly channelName;
|
|
48
|
+
private readonly channel;
|
|
49
|
+
constructor(connection: Connection, channelName: string, channel: Channel);
|
|
50
|
+
/**
|
|
51
|
+
* Register a listener for presence events. Implicitly attaches the
|
|
52
|
+
* underlying channel — presence events arrive on the same WebSocket
|
|
53
|
+
* subscription as message frames.
|
|
54
|
+
*/
|
|
55
|
+
subscribe(listener: PresenceEventListener): UnsubscribeFn;
|
|
56
|
+
/** Announce this connection as present in the channel. */
|
|
57
|
+
enter(data?: unknown): Promise<void>;
|
|
58
|
+
/** Update the data attached to this connection's presence entry. */
|
|
59
|
+
update(data?: unknown): Promise<void>;
|
|
60
|
+
/** Remove this connection's presence entry. */
|
|
61
|
+
leave(): Promise<void>;
|
|
62
|
+
private send;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAG1F,6EAA6E;AAC7E,MAAM,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC;AAEvC;;;;GAIG;AACH,qBAAa,OAAO;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IACxC,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,QAAQ,CAAS;gBAEb,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM;IAMhD;;;;;OAKG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAe7B;;;;OAIG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAO7B;;;OAGG;IACH,SAAS,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa;IAWnD,4DAA4D;IACtD,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAI1D;AAED;;;GAGG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IACxC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;gBAEtB,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IAMzE;;;;OAIG;IACH,SAAS,CAAC,QAAQ,EAAE,qBAAqB,GAAG,aAAa;IASzD,0DAA0D;IACpD,KAAK,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1C,oEAAoE;IAC9D,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3C,+CAA+C;IACzC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAId,IAAI;CASnB"}
|
package/lib/channel.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel + Presence public API. Wraps the Connection layer with
|
|
3
|
+
* per-channel state.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* One subscription handle per (channel, listener) pair. Channels are
|
|
7
|
+
* value-equal by name on a given Realtime client — calling
|
|
8
|
+
* `client.channels.get('chat:1')` twice returns the same instance.
|
|
9
|
+
*/
|
|
10
|
+
export class Channel {
|
|
11
|
+
name;
|
|
12
|
+
presence;
|
|
13
|
+
connection;
|
|
14
|
+
attachPromise = null;
|
|
15
|
+
attached = false;
|
|
16
|
+
constructor(connection, name) {
|
|
17
|
+
this.connection = connection;
|
|
18
|
+
this.name = name;
|
|
19
|
+
this.presence = new Presence(connection, name, this);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Ensure the server is subscribed to this channel. Called implicitly
|
|
23
|
+
* by `subscribe()` and `presence.subscribe()`; expose it so callers
|
|
24
|
+
* can pre-attach if they want to surface attach errors before the
|
|
25
|
+
* first message arrives.
|
|
26
|
+
*/
|
|
27
|
+
async attach() {
|
|
28
|
+
if (this.attached)
|
|
29
|
+
return;
|
|
30
|
+
if (this.attachPromise)
|
|
31
|
+
return this.attachPromise;
|
|
32
|
+
this.attachPromise = this.connection
|
|
33
|
+
.request({ t: 'sub', channel: this.name })
|
|
34
|
+
.then(() => {
|
|
35
|
+
this.attached = true;
|
|
36
|
+
this.connection.rememberSubscription(this.name);
|
|
37
|
+
})
|
|
38
|
+
.finally(() => {
|
|
39
|
+
this.attachPromise = null;
|
|
40
|
+
});
|
|
41
|
+
return this.attachPromise;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Detach from the server (stop receiving messages and presence
|
|
45
|
+
* events). Local listeners are preserved — call `unsubscribe()` to
|
|
46
|
+
* clear them.
|
|
47
|
+
*/
|
|
48
|
+
async detach() {
|
|
49
|
+
if (!this.attached)
|
|
50
|
+
return;
|
|
51
|
+
await this.connection.request({ t: 'unsub', channel: this.name });
|
|
52
|
+
this.attached = false;
|
|
53
|
+
this.connection.forgetSubscription(this.name);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Register a listener for message frames on this channel. Implicitly
|
|
57
|
+
* attaches if needed. Returns an unsubscribe function.
|
|
58
|
+
*/
|
|
59
|
+
subscribe(listener) {
|
|
60
|
+
const listeners = this.connection.addChannelListeners(this.name);
|
|
61
|
+
listeners.messages.add(listener);
|
|
62
|
+
// Fire-and-forget attach; the listener stays registered even if
|
|
63
|
+
// attach fails so a retry-on-reconnect surfaces the right state.
|
|
64
|
+
this.attach().catch(() => { });
|
|
65
|
+
return () => {
|
|
66
|
+
listeners.messages.delete(listener);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Publish one application-level message to the channel. */
|
|
70
|
+
async publish(name, data) {
|
|
71
|
+
await this.attach();
|
|
72
|
+
await this.connection.request({ t: 'pub', channel: this.name, name, data });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Per-channel presence facade. Wraps the `pres` frame and `presEvt`
|
|
77
|
+
* listener dispatch.
|
|
78
|
+
*/
|
|
79
|
+
export class Presence {
|
|
80
|
+
connection;
|
|
81
|
+
channelName;
|
|
82
|
+
channel;
|
|
83
|
+
constructor(connection, channelName, channel) {
|
|
84
|
+
this.connection = connection;
|
|
85
|
+
this.channelName = channelName;
|
|
86
|
+
this.channel = channel;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Register a listener for presence events. Implicitly attaches the
|
|
90
|
+
* underlying channel — presence events arrive on the same WebSocket
|
|
91
|
+
* subscription as message frames.
|
|
92
|
+
*/
|
|
93
|
+
subscribe(listener) {
|
|
94
|
+
const listeners = this.connection.addChannelListeners(this.channelName);
|
|
95
|
+
listeners.presence.add(listener);
|
|
96
|
+
this.channel.attach().catch(() => { });
|
|
97
|
+
return () => {
|
|
98
|
+
listeners.presence.delete(listener);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Announce this connection as present in the channel. */
|
|
102
|
+
async enter(data) {
|
|
103
|
+
await this.send('enter', data);
|
|
104
|
+
}
|
|
105
|
+
/** Update the data attached to this connection's presence entry. */
|
|
106
|
+
async update(data) {
|
|
107
|
+
await this.send('update', data);
|
|
108
|
+
}
|
|
109
|
+
/** Remove this connection's presence entry. */
|
|
110
|
+
async leave() {
|
|
111
|
+
await this.send('leave', undefined);
|
|
112
|
+
}
|
|
113
|
+
async send(action, data) {
|
|
114
|
+
await this.channel.attach();
|
|
115
|
+
await this.connection.request({
|
|
116
|
+
t: 'pres',
|
|
117
|
+
channel: this.channelName,
|
|
118
|
+
action,
|
|
119
|
+
...(data === undefined ? {} : { data }),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=channel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.js","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level WebSocket connection manager. Handles framing, request /
|
|
3
|
+
* response correlation, and dispatch to per-channel listeners.
|
|
4
|
+
*
|
|
5
|
+
* The class is intentionally protocol-aware but channel-agnostic — the
|
|
6
|
+
* Channel and Realtime classes layer the public API on top.
|
|
7
|
+
*/
|
|
8
|
+
import type { AckFrame, ClientFrame, MessageFrame, PresenceEventFrame, PresenceFrame, PublishFrame, SubscribeFrame, UnsubscribeFrame } from './wire.js';
|
|
9
|
+
/**
|
|
10
|
+
* Frames the SDK can issue with `request()`. Each carries an `id` the
|
|
11
|
+
* server echoes on the matching ack/err frame; Connection assigns the
|
|
12
|
+
* id so callers can omit it.
|
|
13
|
+
*/
|
|
14
|
+
export type AckableFrame = Omit<SubscribeFrame, 'id'> | Omit<UnsubscribeFrame, 'id'> | Omit<PublishFrame, 'id'> | Omit<PresenceFrame, 'id'>;
|
|
15
|
+
/** Options that control how Connection reaches the edge. */
|
|
16
|
+
export type ConnectionOptions = {
|
|
17
|
+
/** ws:// or wss:// URL pointing at the realtime edge binary. */
|
|
18
|
+
readonly url: string;
|
|
19
|
+
/**
|
|
20
|
+
* A Realtime API key in `appSlug.publicKeyId:privateKey` form. Convenient for trusted
|
|
21
|
+
* quick starts and server-side scripts; browser apps should prefer JWTs
|
|
22
|
+
* returned from `authCallback`.
|
|
23
|
+
*/
|
|
24
|
+
readonly key?: string;
|
|
25
|
+
/** Optional client id to attach to a direct key-auth connection. */
|
|
26
|
+
readonly clientId?: string;
|
|
27
|
+
/**
|
|
28
|
+
* A static JWT to send in the auth handshake. Mutually exclusive with
|
|
29
|
+
* `authCallback`. Useful for local dev and short scripts.
|
|
30
|
+
*/
|
|
31
|
+
readonly token?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Async callback that returns a fresh JWT. Called once on connect and
|
|
34
|
+
* again on every reconnect. Use this when the token is short-lived
|
|
35
|
+
* (the production path).
|
|
36
|
+
*/
|
|
37
|
+
readonly authCallback?: () => Promise<string> | string;
|
|
38
|
+
/**
|
|
39
|
+
* Override the global WebSocket constructor. Mostly useful in tests;
|
|
40
|
+
* defaults to `globalThis.WebSocket` which is present in browsers and
|
|
41
|
+
* Node 22+.
|
|
42
|
+
*/
|
|
43
|
+
readonly webSocket?: typeof WebSocket;
|
|
44
|
+
/**
|
|
45
|
+
* If true, attempt to reconnect after unexpected disconnects with
|
|
46
|
+
* exponential backoff. Defaults to true.
|
|
47
|
+
*/
|
|
48
|
+
readonly autoReconnect?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Initial backoff for reconnects (default 1000ms). Doubles each
|
|
51
|
+
* attempt up to maxReconnectDelayMs.
|
|
52
|
+
*/
|
|
53
|
+
readonly initialReconnectDelayMs?: number;
|
|
54
|
+
/** Cap on the reconnect backoff (default 30000ms). */
|
|
55
|
+
readonly maxReconnectDelayMs?: number;
|
|
56
|
+
};
|
|
57
|
+
/** Connection lifecycle states. */
|
|
58
|
+
export type ConnectionState = 'initialized' | 'connecting' | 'connected' | 'disconnected' | 'closing' | 'closed' | 'failed';
|
|
59
|
+
/** Listener for state transitions. */
|
|
60
|
+
export type ConnectionStateListener = (state: ConnectionState, reason?: Error) => void;
|
|
61
|
+
/** Listener invoked for every message frame on a channel. */
|
|
62
|
+
export type MessageListener = (message: MessageFrame) => void;
|
|
63
|
+
/** Listener invoked for every presence event frame on a channel. */
|
|
64
|
+
export type PresenceEventListener = (event: PresenceEventFrame) => void;
|
|
65
|
+
/**
|
|
66
|
+
* Internal listener registry, keyed by channel name. Connection owns
|
|
67
|
+
* the maps so reconnect can transparently re-subscribe.
|
|
68
|
+
*/
|
|
69
|
+
type ChannelListeners = {
|
|
70
|
+
readonly messages: Set<MessageListener>;
|
|
71
|
+
readonly presence: Set<PresenceEventListener>;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Connection is the transport layer. One Realtime client owns one
|
|
75
|
+
* Connection; channels share it.
|
|
76
|
+
*/
|
|
77
|
+
export declare class Connection {
|
|
78
|
+
readonly options: ConnectionOptions;
|
|
79
|
+
private socket;
|
|
80
|
+
private state;
|
|
81
|
+
private connectionId;
|
|
82
|
+
private serverClientId;
|
|
83
|
+
private nextRequestId;
|
|
84
|
+
private readonly pending;
|
|
85
|
+
private readonly channelListeners;
|
|
86
|
+
private readonly stateListeners;
|
|
87
|
+
private connectPromise;
|
|
88
|
+
private reconnectTimer;
|
|
89
|
+
private reconnectAttempt;
|
|
90
|
+
/** Channels the SDK has asked to be subscribed to; re-sent on reconnect. */
|
|
91
|
+
private readonly desiredSubscriptions;
|
|
92
|
+
constructor(options: ConnectionOptions);
|
|
93
|
+
/** Current connection state. */
|
|
94
|
+
getState(): ConnectionState;
|
|
95
|
+
/** The server-issued connection id, populated after a successful auth handshake. */
|
|
96
|
+
getConnectionId(): string | null;
|
|
97
|
+
/** The client id encoded in the token, populated after auth. */
|
|
98
|
+
getClientId(): string | null;
|
|
99
|
+
/** Register a state-change listener. Returns an unsubscribe function. */
|
|
100
|
+
onStateChange(listener: ConnectionStateListener): () => void;
|
|
101
|
+
/**
|
|
102
|
+
* Open the WebSocket and complete the auth handshake. Idempotent —
|
|
103
|
+
* concurrent calls await the same in-flight connect.
|
|
104
|
+
*/
|
|
105
|
+
connect(): Promise<void>;
|
|
106
|
+
/** Close the WebSocket and release resources. */
|
|
107
|
+
close(): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Send a frame that expects an ack. Returns the matching AckFrame, or
|
|
110
|
+
* rejects with the server's ErrorFrame (wrapped in an Error).
|
|
111
|
+
*/
|
|
112
|
+
request(frame: AckableFrame): Promise<AckFrame>;
|
|
113
|
+
/** Send a fire-and-forget frame (no ack expected). */
|
|
114
|
+
send(frame: ClientFrame): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Register listeners for a channel. Connection remembers the
|
|
117
|
+
* registration so it can re-attach across reconnects, but actually
|
|
118
|
+
* issuing the `sub` frame is the caller's job (Channel does that).
|
|
119
|
+
*/
|
|
120
|
+
addChannelListeners(channel: string): ChannelListeners;
|
|
121
|
+
/** Forget all listeners for a channel. Called from Channel.detach. */
|
|
122
|
+
removeChannelListeners(channel: string): void;
|
|
123
|
+
/** Add `channel` to the set of subscriptions to restore on reconnect. */
|
|
124
|
+
rememberSubscription(channel: string): void;
|
|
125
|
+
/** Stop restoring this subscription on future reconnects. */
|
|
126
|
+
forgetSubscription(channel: string): void;
|
|
127
|
+
private doConnect;
|
|
128
|
+
private makeSocket;
|
|
129
|
+
private createAuthFrame;
|
|
130
|
+
/** Steady-state message handler; installed after a successful auth. */
|
|
131
|
+
private readonly handleMessage;
|
|
132
|
+
private handleClose;
|
|
133
|
+
private scheduleReconnect;
|
|
134
|
+
private restoreSubscriptionsOnReconnect;
|
|
135
|
+
private sendRaw;
|
|
136
|
+
private setState;
|
|
137
|
+
}
|
|
138
|
+
export {};
|
|
139
|
+
//# sourceMappingURL=connection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"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"}
|