@debros/network-ts-sdk 0.3.4 → 0.4.3
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 +49 -0
- package/dist/index.d.ts +161 -26
- package/dist/index.js +205 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/http.ts +114 -9
- package/src/core/ws.ts +53 -8
- package/src/functions/client.ts +62 -0
- package/src/functions/types.ts +21 -0
- package/src/index.ts +26 -8
- package/src/pubsub/client.ts +104 -31
- package/src/pubsub/types.ts +46 -0
package/src/pubsub/client.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { HttpClient } from "../core/http";
|
|
2
2
|
import { WSClient, WSClientConfig } from "../core/ws";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
3
|
+
import {
|
|
4
|
+
PubSubMessage,
|
|
5
|
+
RawEnvelope,
|
|
6
|
+
MessageHandler,
|
|
7
|
+
ErrorHandler,
|
|
8
|
+
CloseHandler,
|
|
9
|
+
SubscribeOptions,
|
|
10
|
+
PresenceResponse,
|
|
11
|
+
PresenceMember,
|
|
12
|
+
PresenceOptions,
|
|
13
|
+
} from "./types";
|
|
15
14
|
|
|
16
15
|
// Cross-platform base64 encoding/decoding utilities
|
|
17
16
|
function base64Encode(str: string): string {
|
|
@@ -54,13 +53,9 @@ function base64Decode(b64: string): string {
|
|
|
54
53
|
throw new Error("No base64 decoding method available");
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
export type MessageHandler = (message: Message) => void;
|
|
58
|
-
export type ErrorHandler = (error: Error) => void;
|
|
59
|
-
export type CloseHandler = () => void;
|
|
60
|
-
|
|
61
56
|
/**
|
|
62
57
|
* Simple PubSub client - one WebSocket connection per topic
|
|
63
|
-
*
|
|
58
|
+
* Gateway failover is handled at the application layer
|
|
64
59
|
*/
|
|
65
60
|
export class PubSubClient {
|
|
66
61
|
private httpClient: HttpClient;
|
|
@@ -104,23 +99,40 @@ export class PubSubClient {
|
|
|
104
99
|
return response.topics || [];
|
|
105
100
|
}
|
|
106
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Get current presence for a topic without subscribing
|
|
104
|
+
*/
|
|
105
|
+
async getPresence(topic: string): Promise<PresenceResponse> {
|
|
106
|
+
const response = await this.httpClient.get<PresenceResponse>(
|
|
107
|
+
`/v1/pubsub/presence?topic=${encodeURIComponent(topic)}`
|
|
108
|
+
);
|
|
109
|
+
return response;
|
|
110
|
+
}
|
|
111
|
+
|
|
107
112
|
/**
|
|
108
113
|
* Subscribe to a topic via WebSocket
|
|
109
114
|
* Creates one WebSocket connection per topic
|
|
110
115
|
*/
|
|
111
116
|
async subscribe(
|
|
112
117
|
topic: string,
|
|
113
|
-
|
|
114
|
-
onMessage?: MessageHandler;
|
|
115
|
-
onError?: ErrorHandler;
|
|
116
|
-
onClose?: CloseHandler;
|
|
117
|
-
} = {}
|
|
118
|
+
options: SubscribeOptions = {}
|
|
118
119
|
): Promise<Subscription> {
|
|
119
120
|
// Build WebSocket URL for this topic
|
|
120
121
|
const wsUrl = new URL(this.wsConfig.wsURL || "ws://127.0.0.1:6001");
|
|
121
122
|
wsUrl.pathname = "/v1/pubsub/ws";
|
|
122
123
|
wsUrl.searchParams.set("topic", topic);
|
|
123
124
|
|
|
125
|
+
// Handle presence options
|
|
126
|
+
let presence: PresenceOptions | undefined;
|
|
127
|
+
if (options.presence?.enabled) {
|
|
128
|
+
presence = options.presence;
|
|
129
|
+
wsUrl.searchParams.set("presence", "true");
|
|
130
|
+
wsUrl.searchParams.set("member_id", presence.memberId);
|
|
131
|
+
if (presence.meta) {
|
|
132
|
+
wsUrl.searchParams.set("member_meta", JSON.stringify(presence.meta));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
124
136
|
const authToken = this.httpClient.getApiKey() ?? this.httpClient.getToken();
|
|
125
137
|
|
|
126
138
|
// Create WebSocket client
|
|
@@ -133,16 +145,18 @@ export class PubSubClient {
|
|
|
133
145
|
await wsClient.connect();
|
|
134
146
|
|
|
135
147
|
// Create subscription wrapper
|
|
136
|
-
const subscription = new Subscription(wsClient, topic)
|
|
148
|
+
const subscription = new Subscription(wsClient, topic, presence, () =>
|
|
149
|
+
this.getPresence(topic)
|
|
150
|
+
);
|
|
137
151
|
|
|
138
|
-
if (
|
|
139
|
-
subscription.onMessage(
|
|
152
|
+
if (options.onMessage) {
|
|
153
|
+
subscription.onMessage(options.onMessage);
|
|
140
154
|
}
|
|
141
|
-
if (
|
|
142
|
-
subscription.onError(
|
|
155
|
+
if (options.onError) {
|
|
156
|
+
subscription.onError(options.onError);
|
|
143
157
|
}
|
|
144
|
-
if (
|
|
145
|
-
subscription.onClose(
|
|
158
|
+
if (options.onClose) {
|
|
159
|
+
subscription.onClose(options.onClose);
|
|
146
160
|
}
|
|
147
161
|
|
|
148
162
|
return subscription;
|
|
@@ -155,6 +169,7 @@ export class PubSubClient {
|
|
|
155
169
|
export class Subscription {
|
|
156
170
|
private wsClient: WSClient;
|
|
157
171
|
private topic: string;
|
|
172
|
+
private presenceOptions?: PresenceOptions;
|
|
158
173
|
private messageHandlers: Set<MessageHandler> = new Set();
|
|
159
174
|
private errorHandlers: Set<ErrorHandler> = new Set();
|
|
160
175
|
private closeHandlers: Set<CloseHandler> = new Set();
|
|
@@ -162,10 +177,18 @@ export class Subscription {
|
|
|
162
177
|
private wsMessageHandler: ((data: string) => void) | null = null;
|
|
163
178
|
private wsErrorHandler: ((error: Error) => void) | null = null;
|
|
164
179
|
private wsCloseHandler: (() => void) | null = null;
|
|
180
|
+
private getPresenceFn: () => Promise<PresenceResponse>;
|
|
165
181
|
|
|
166
|
-
constructor(
|
|
182
|
+
constructor(
|
|
183
|
+
wsClient: WSClient,
|
|
184
|
+
topic: string,
|
|
185
|
+
presenceOptions: PresenceOptions | undefined,
|
|
186
|
+
getPresenceFn: () => Promise<PresenceResponse>
|
|
187
|
+
) {
|
|
167
188
|
this.wsClient = wsClient;
|
|
168
189
|
this.topic = topic;
|
|
190
|
+
this.presenceOptions = presenceOptions;
|
|
191
|
+
this.getPresenceFn = getPresenceFn;
|
|
169
192
|
|
|
170
193
|
// Register message handler
|
|
171
194
|
this.wsMessageHandler = (data) => {
|
|
@@ -177,6 +200,37 @@ export class Subscription {
|
|
|
177
200
|
if (!envelope || typeof envelope !== "object") {
|
|
178
201
|
throw new Error("Invalid envelope: not an object");
|
|
179
202
|
}
|
|
203
|
+
|
|
204
|
+
// Handle presence events
|
|
205
|
+
if (
|
|
206
|
+
envelope.type === "presence.join" ||
|
|
207
|
+
envelope.type === "presence.leave"
|
|
208
|
+
) {
|
|
209
|
+
if (!envelope.member_id) {
|
|
210
|
+
console.warn("[Subscription] Presence event missing member_id");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const presenceMember: PresenceMember = {
|
|
215
|
+
memberId: envelope.member_id,
|
|
216
|
+
joinedAt: envelope.timestamp,
|
|
217
|
+
meta: envelope.meta,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
if (
|
|
221
|
+
envelope.type === "presence.join" &&
|
|
222
|
+
this.presenceOptions?.onJoin
|
|
223
|
+
) {
|
|
224
|
+
this.presenceOptions.onJoin(presenceMember);
|
|
225
|
+
} else if (
|
|
226
|
+
envelope.type === "presence.leave" &&
|
|
227
|
+
this.presenceOptions?.onLeave
|
|
228
|
+
) {
|
|
229
|
+
this.presenceOptions.onLeave(presenceMember);
|
|
230
|
+
}
|
|
231
|
+
return; // Don't call regular onMessage for presence events
|
|
232
|
+
}
|
|
233
|
+
|
|
180
234
|
if (!envelope.data || typeof envelope.data !== "string") {
|
|
181
235
|
throw new Error("Invalid envelope: missing or invalid data field");
|
|
182
236
|
}
|
|
@@ -192,7 +246,7 @@ export class Subscription {
|
|
|
192
246
|
// Decode base64 data
|
|
193
247
|
const messageData = base64Decode(envelope.data);
|
|
194
248
|
|
|
195
|
-
const message:
|
|
249
|
+
const message: PubSubMessage = {
|
|
196
250
|
topic: envelope.topic,
|
|
197
251
|
data: messageData,
|
|
198
252
|
timestamp: envelope.timestamp,
|
|
@@ -223,6 +277,25 @@ export class Subscription {
|
|
|
223
277
|
this.wsClient.onClose(this.wsCloseHandler);
|
|
224
278
|
}
|
|
225
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Get current presence (requires presence.enabled on subscribe)
|
|
282
|
+
*/
|
|
283
|
+
async getPresence(): Promise<PresenceMember[]> {
|
|
284
|
+
if (!this.presenceOptions?.enabled) {
|
|
285
|
+
throw new Error("Presence is not enabled for this subscription");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const response = await this.getPresenceFn();
|
|
289
|
+
return response.members;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Check if presence is enabled for this subscription
|
|
294
|
+
*/
|
|
295
|
+
hasPresence(): boolean {
|
|
296
|
+
return !!this.presenceOptions?.enabled;
|
|
297
|
+
}
|
|
298
|
+
|
|
226
299
|
/**
|
|
227
300
|
* Register message handler
|
|
228
301
|
*/
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface PubSubMessage {
|
|
2
|
+
data: string;
|
|
3
|
+
topic: string;
|
|
4
|
+
timestamp: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface RawEnvelope {
|
|
8
|
+
type?: string;
|
|
9
|
+
data: string; // base64-encoded
|
|
10
|
+
timestamp: number;
|
|
11
|
+
topic: string;
|
|
12
|
+
member_id?: string;
|
|
13
|
+
meta?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PresenceMember {
|
|
17
|
+
memberId: string;
|
|
18
|
+
joinedAt: number;
|
|
19
|
+
meta?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PresenceResponse {
|
|
23
|
+
topic: string;
|
|
24
|
+
members: PresenceMember[];
|
|
25
|
+
count: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PresenceOptions {
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
memberId: string;
|
|
31
|
+
meta?: Record<string, unknown>;
|
|
32
|
+
onJoin?: (member: PresenceMember) => void;
|
|
33
|
+
onLeave?: (member: PresenceMember) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SubscribeOptions {
|
|
37
|
+
onMessage?: MessageHandler;
|
|
38
|
+
onError?: ErrorHandler;
|
|
39
|
+
onClose?: CloseHandler;
|
|
40
|
+
presence?: PresenceOptions;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type MessageHandler = (message: PubSubMessage) => void;
|
|
44
|
+
export type ErrorHandler = (error: Error) => void;
|
|
45
|
+
export type CloseHandler = () => void;
|
|
46
|
+
|