@debros/network-ts-sdk 0.3.2 → 0.4.2

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/src/core/ws.ts CHANGED
@@ -15,10 +15,11 @@ export type WSOpenHandler = () => void;
15
15
 
16
16
  /**
17
17
  * Simple WebSocket client with minimal abstractions
18
- * No complex reconnection, no heartbeats - keep it simple
18
+ * No complex reconnection, no failover - keep it simple
19
+ * Gateway failover is handled at the application layer
19
20
  */
20
21
  export class WSClient {
21
- private url: string;
22
+ private wsURL: string;
22
23
  private timeout: number;
23
24
  private authToken?: string;
24
25
  private WebSocketClass: typeof WebSocket;
@@ -31,12 +32,19 @@ export class WSClient {
31
32
  private isClosed = false;
32
33
 
33
34
  constructor(config: WSClientConfig) {
34
- this.url = config.wsURL;
35
+ this.wsURL = config.wsURL;
35
36
  this.timeout = config.timeout ?? 30000;
36
37
  this.authToken = config.authToken;
37
38
  this.WebSocketClass = config.WebSocket ?? WebSocket;
38
39
  }
39
40
 
41
+ /**
42
+ * Get the current WebSocket URL
43
+ */
44
+ get url(): string {
45
+ return this.wsURL;
46
+ }
47
+
40
48
  /**
41
49
  * Connect to WebSocket server
42
50
  */
@@ -56,7 +64,7 @@ export class WSClient {
56
64
 
57
65
  this.ws.addEventListener("open", () => {
58
66
  clearTimeout(timeout);
59
- console.log("[WSClient] Connected to", this.url);
67
+ console.log("[WSClient] Connected to", this.wsURL);
60
68
  this.openHandlers.forEach((handler) => handler());
61
69
  resolve();
62
70
  });
@@ -71,6 +79,7 @@ export class WSClient {
71
79
  clearTimeout(timeout);
72
80
  const error = new SDKError("WebSocket error", 500, "WS_ERROR", event);
73
81
  this.errorHandlers.forEach((handler) => handler(error));
82
+ reject(error);
74
83
  });
75
84
 
76
85
  this.ws.addEventListener("close", () => {
@@ -88,7 +97,7 @@ export class WSClient {
88
97
  * Build WebSocket URL with auth token
89
98
  */
90
99
  private buildWSUrl(): string {
91
- let url = this.url;
100
+ let url = this.wsURL;
92
101
 
93
102
  if (this.authToken) {
94
103
  const separator = url.includes("?") ? "&" : "?";
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Functions Client
3
+ * Client for calling serverless functions on the Orama Network
4
+ */
5
+
6
+ import { HttpClient } from "../core/http";
7
+ import { SDKError } from "../errors";
8
+
9
+ export interface FunctionsClientConfig {
10
+ /**
11
+ * Base URL for the functions gateway
12
+ * Defaults to using the same baseURL as the HTTP client
13
+ */
14
+ gatewayURL?: string;
15
+
16
+ /**
17
+ * Namespace for the functions
18
+ */
19
+ namespace: string;
20
+ }
21
+
22
+ export class FunctionsClient {
23
+ private httpClient: HttpClient;
24
+ private gatewayURL?: string;
25
+ private namespace: string;
26
+
27
+ constructor(httpClient: HttpClient, config?: FunctionsClientConfig) {
28
+ this.httpClient = httpClient;
29
+ this.gatewayURL = config?.gatewayURL;
30
+ this.namespace = config?.namespace ?? "default";
31
+ }
32
+
33
+ /**
34
+ * Invoke a serverless function by name
35
+ *
36
+ * @param functionName - Name of the function to invoke
37
+ * @param input - Input payload for the function
38
+ * @returns The function response
39
+ */
40
+ async invoke<TInput = any, TOutput = any>(
41
+ functionName: string,
42
+ input: TInput
43
+ ): Promise<TOutput> {
44
+ const url = this.gatewayURL
45
+ ? `${this.gatewayURL}/v1/invoke/${this.namespace}/${functionName}`
46
+ : `/v1/invoke/${this.namespace}/${functionName}`;
47
+
48
+ try {
49
+ const response = await this.httpClient.post<TOutput>(url, input);
50
+ return response;
51
+ } catch (error) {
52
+ if (error instanceof SDKError) {
53
+ throw error;
54
+ }
55
+ throw new SDKError(
56
+ `Function ${functionName} failed`,
57
+ 500,
58
+ error instanceof Error ? error.message : String(error)
59
+ );
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Serverless Functions Types
3
+ * Type definitions for calling serverless functions on the Orama Network
4
+ */
5
+
6
+ /**
7
+ * Generic response from a serverless function
8
+ */
9
+ export interface FunctionResponse<T = unknown> {
10
+ success: boolean;
11
+ error?: string;
12
+ data?: T;
13
+ }
14
+
15
+ /**
16
+ * Standard success/error response used by many functions
17
+ */
18
+ export interface SuccessResponse {
19
+ success: boolean;
20
+ error?: string;
21
+ }
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { PubSubClient } from "./pubsub/client";
5
5
  import { NetworkClient } from "./network/client";
6
6
  import { CacheClient } from "./cache/client";
7
7
  import { StorageClient } from "./storage/client";
8
+ import { FunctionsClient, FunctionsClientConfig } from "./functions/client";
8
9
  import { WSClientConfig } from "./core/ws";
9
10
  import {
10
11
  StorageAdapter,
@@ -16,7 +17,8 @@ export interface ClientConfig extends Omit<HttpClientConfig, "fetch"> {
16
17
  apiKey?: string;
17
18
  jwt?: string;
18
19
  storage?: StorageAdapter;
19
- wsConfig?: Partial<WSClientConfig>;
20
+ wsConfig?: Partial<Omit<WSClientConfig, "wsURL">>;
21
+ functionsConfig?: FunctionsClientConfig;
20
22
  fetch?: typeof fetch;
21
23
  }
22
24
 
@@ -27,6 +29,7 @@ export interface Client {
27
29
  network: NetworkClient;
28
30
  cache: CacheClient;
29
31
  storage: StorageClient;
32
+ functions: FunctionsClient;
30
33
  }
31
34
 
32
35
  export function createClient(config: ClientConfig): Client {
@@ -45,10 +48,8 @@ export function createClient(config: ClientConfig): Client {
45
48
  jwt: config.jwt,
46
49
  });
47
50
 
48
- // Derive WebSocket URL from baseURL if not explicitly provided
49
- const wsURL =
50
- config.wsConfig?.wsURL ??
51
- config.baseURL.replace(/^http/, "ws").replace(/\/$/, "");
51
+ // Derive WebSocket URL from baseURL
52
+ const wsURL = config.baseURL.replace(/^http/, "ws").replace(/\/$/, "");
52
53
 
53
54
  const db = new DBClient(httpClient);
54
55
  const pubsub = new PubSubClient(httpClient, {
@@ -58,6 +59,7 @@ export function createClient(config: ClientConfig): Client {
58
59
  const network = new NetworkClient(httpClient);
59
60
  const cache = new CacheClient(httpClient);
60
61
  const storage = new StorageClient(httpClient);
62
+ const functions = new FunctionsClient(httpClient, config.functionsConfig);
61
63
 
62
64
  return {
63
65
  auth,
@@ -66,6 +68,7 @@ export function createClient(config: ClientConfig): Client {
66
68
  network,
67
69
  cache,
68
70
  storage,
71
+ functions,
69
72
  };
70
73
  }
71
74
 
@@ -79,16 +82,21 @@ export { PubSubClient, Subscription } from "./pubsub/client";
79
82
  export { NetworkClient } from "./network/client";
80
83
  export { CacheClient } from "./cache/client";
81
84
  export { StorageClient } from "./storage/client";
85
+ export { FunctionsClient } from "./functions/client";
82
86
  export { SDKError } from "./errors";
83
87
  export { MemoryStorage, LocalStorageAdapter } from "./auth/types";
84
88
  export type { StorageAdapter, AuthConfig, WhoAmI } from "./auth/types";
85
89
  export type * from "./db/types";
86
90
  export type {
87
- Message,
88
91
  MessageHandler,
89
92
  ErrorHandler,
90
93
  CloseHandler,
91
- } from "./pubsub/client";
94
+ PresenceMember,
95
+ PresenceResponse,
96
+ PresenceOptions,
97
+ SubscribeOptions,
98
+ } from "./pubsub/types";
99
+ export { type PubSubMessage } from "./pubsub/types";
92
100
  export type {
93
101
  PeerInfo,
94
102
  NetworkStatus,
@@ -114,3 +122,5 @@ export type {
114
122
  StoragePinResponse,
115
123
  StorageStatus,
116
124
  } from "./storage/client";
125
+ export type { FunctionsClientConfig } from "./functions/client";
126
+ export type * from "./functions/types";
@@ -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
+