@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.
@@ -1,17 +1,16 @@
1
1
  import { HttpClient } from "../core/http";
2
2
  import { WSClient, WSClientConfig } from "../core/ws";
3
-
4
- export interface Message {
5
- data: string;
6
- topic: string;
7
- timestamp: number;
8
- }
9
-
10
- export interface RawEnvelope {
11
- data: string; // base64-encoded
12
- timestamp: number;
13
- topic: string;
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
- * No connection pooling, no reference counting - keep it simple
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
- handlers: {
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 (handlers.onMessage) {
139
- subscription.onMessage(handlers.onMessage);
152
+ if (options.onMessage) {
153
+ subscription.onMessage(options.onMessage);
140
154
  }
141
- if (handlers.onError) {
142
- subscription.onError(handlers.onError);
155
+ if (options.onError) {
156
+ subscription.onError(options.onError);
143
157
  }
144
- if (handlers.onClose) {
145
- subscription.onClose(handlers.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(wsClient: WSClient, topic: string) {
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: 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
+