@debros/orama 0.122.4-nightly

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +665 -0
  3. package/dist/index.d.ts +1334 -0
  4. package/dist/index.js +2553 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +82 -0
  7. package/src/auth/client.ts +276 -0
  8. package/src/auth/index.ts +3 -0
  9. package/src/auth/types.ts +62 -0
  10. package/src/cache/client.ts +203 -0
  11. package/src/cache/index.ts +14 -0
  12. package/src/core/http.ts +541 -0
  13. package/src/core/index.ts +10 -0
  14. package/src/core/interfaces/IAuthStrategy.ts +28 -0
  15. package/src/core/interfaces/IHttpTransport.ts +73 -0
  16. package/src/core/interfaces/IRetryPolicy.ts +20 -0
  17. package/src/core/interfaces/IWebSocketClient.ts +60 -0
  18. package/src/core/interfaces/index.ts +4 -0
  19. package/src/core/transport/AuthHeaderStrategy.ts +108 -0
  20. package/src/core/transport/RequestLogger.ts +116 -0
  21. package/src/core/transport/RequestRetryPolicy.ts +53 -0
  22. package/src/core/transport/TLSConfiguration.ts +53 -0
  23. package/src/core/transport/index.ts +4 -0
  24. package/src/core/ws.ts +246 -0
  25. package/src/db/client.ts +126 -0
  26. package/src/db/index.ts +13 -0
  27. package/src/db/qb.ts +111 -0
  28. package/src/db/repository.ts +128 -0
  29. package/src/db/types.ts +67 -0
  30. package/src/errors.ts +38 -0
  31. package/src/functions/client.ts +62 -0
  32. package/src/functions/index.ts +2 -0
  33. package/src/functions/types.ts +21 -0
  34. package/src/index.ts +201 -0
  35. package/src/network/client.ts +119 -0
  36. package/src/network/index.ts +7 -0
  37. package/src/pubsub/client.ts +361 -0
  38. package/src/pubsub/index.ts +12 -0
  39. package/src/pubsub/types.ts +46 -0
  40. package/src/storage/client.ts +272 -0
  41. package/src/storage/index.ts +7 -0
  42. package/src/utils/codec.ts +68 -0
  43. package/src/utils/index.ts +3 -0
  44. package/src/utils/platform.ts +44 -0
  45. package/src/utils/retry.ts +58 -0
  46. package/src/vault/auth.ts +98 -0
  47. package/src/vault/client.ts +197 -0
  48. package/src/vault/crypto/aes.ts +271 -0
  49. package/src/vault/crypto/hkdf.ts +42 -0
  50. package/src/vault/crypto/index.ts +27 -0
  51. package/src/vault/crypto/shamir.ts +173 -0
  52. package/src/vault/index.ts +65 -0
  53. package/src/vault/quorum.ts +16 -0
  54. package/src/vault/transport/fanout.ts +94 -0
  55. package/src/vault/transport/guardian.ts +285 -0
  56. package/src/vault/transport/index.ts +19 -0
  57. package/src/vault/transport/types.ts +101 -0
  58. package/src/vault/types.ts +62 -0
package/src/index.ts ADDED
@@ -0,0 +1,201 @@
1
+ import { HttpClient, HttpClientConfig, NetworkErrorCallback } from "./core/http";
2
+ import { AuthClient } from "./auth/client";
3
+ import { DBClient } from "./db/client";
4
+ import { PubSubClient } from "./pubsub/client";
5
+ import { NetworkClient } from "./network/client";
6
+ import { CacheClient } from "./cache/client";
7
+ import { StorageClient } from "./storage/client";
8
+ import { FunctionsClient, FunctionsClientConfig } from "./functions/client";
9
+ import { VaultClient } from "./vault/client";
10
+ import { WSClientConfig } from "./core/ws";
11
+ import {
12
+ StorageAdapter,
13
+ MemoryStorage,
14
+ LocalStorageAdapter,
15
+ } from "./auth/types";
16
+ import type { VaultConfig } from "./vault/types";
17
+
18
+ export interface ClientConfig extends Omit<HttpClientConfig, "fetch"> {
19
+ apiKey?: string;
20
+ jwt?: string;
21
+ storage?: StorageAdapter;
22
+ wsConfig?: Partial<Omit<WSClientConfig, "wsURL">>;
23
+ functionsConfig?: FunctionsClientConfig;
24
+ fetch?: typeof fetch;
25
+ /**
26
+ * Callback invoked on network errors (HTTP and WebSocket).
27
+ * Use this to trigger gateway failover at the application layer.
28
+ */
29
+ onNetworkError?: NetworkErrorCallback;
30
+ /** Configuration for the vault (distributed secrets store). */
31
+ vaultConfig?: VaultConfig;
32
+ }
33
+
34
+ export interface Client {
35
+ auth: AuthClient;
36
+ db: DBClient;
37
+ pubsub: PubSubClient;
38
+ network: NetworkClient;
39
+ cache: CacheClient;
40
+ storage: StorageClient;
41
+ functions: FunctionsClient;
42
+ vault: VaultClient | null;
43
+ }
44
+
45
+ export function createClient(config: ClientConfig): Client {
46
+ const httpClient = new HttpClient({
47
+ baseURL: config.baseURL,
48
+ timeout: config.timeout,
49
+ maxRetries: config.maxRetries,
50
+ retryDelayMs: config.retryDelayMs,
51
+ debug: config.debug,
52
+ fetch: config.fetch,
53
+ onNetworkError: config.onNetworkError,
54
+ });
55
+
56
+ const auth = new AuthClient({
57
+ httpClient,
58
+ storage: config.storage,
59
+ apiKey: config.apiKey,
60
+ jwt: config.jwt,
61
+ });
62
+
63
+ // Derive WebSocket URL from baseURL
64
+ const wsURL = config.baseURL.replace(/^http/, "ws").replace(/\/$/, "");
65
+
66
+ const db = new DBClient(httpClient);
67
+ const pubsub = new PubSubClient(httpClient, {
68
+ ...config.wsConfig,
69
+ wsURL,
70
+ onNetworkError: config.onNetworkError,
71
+ });
72
+ const network = new NetworkClient(httpClient);
73
+ const cache = new CacheClient(httpClient);
74
+ const storage = new StorageClient(httpClient);
75
+ const functions = new FunctionsClient(httpClient, config.functionsConfig);
76
+ const vault = config.vaultConfig
77
+ ? new VaultClient(config.vaultConfig)
78
+ : null;
79
+
80
+ return {
81
+ auth,
82
+ db,
83
+ pubsub,
84
+ network,
85
+ cache,
86
+ storage,
87
+ functions,
88
+ vault,
89
+ };
90
+ }
91
+
92
+ export { HttpClient } from "./core/http";
93
+ export type { NetworkErrorCallback, NetworkErrorContext } from "./core/http";
94
+ export { WSClient } from "./core/ws";
95
+ export { AuthClient } from "./auth/client";
96
+ export { DBClient } from "./db/client";
97
+ export { QueryBuilder } from "./db/qb";
98
+ export { Repository } from "./db/repository";
99
+ export { PubSubClient, Subscription } from "./pubsub/client";
100
+ export { NetworkClient } from "./network/client";
101
+ export { CacheClient } from "./cache/client";
102
+ export { StorageClient } from "./storage/client";
103
+ export { FunctionsClient } from "./functions/client";
104
+ export { SDKError } from "./errors";
105
+ export { MemoryStorage, LocalStorageAdapter } from "./auth/types";
106
+ export type { StorageAdapter, AuthConfig, WhoAmI } from "./auth/types";
107
+ export type * from "./db/types";
108
+ export type {
109
+ MessageHandler,
110
+ ErrorHandler,
111
+ CloseHandler,
112
+ PresenceMember,
113
+ PresenceResponse,
114
+ PresenceOptions,
115
+ SubscribeOptions,
116
+ } from "./pubsub/types";
117
+ export { type PubSubMessage } from "./pubsub/types";
118
+ export type {
119
+ PeerInfo,
120
+ NetworkStatus,
121
+ ProxyRequest,
122
+ ProxyResponse,
123
+ } from "./network/client";
124
+ export type {
125
+ CacheGetRequest,
126
+ CacheGetResponse,
127
+ CachePutRequest,
128
+ CachePutResponse,
129
+ CacheDeleteRequest,
130
+ CacheDeleteResponse,
131
+ CacheMultiGetRequest,
132
+ CacheMultiGetResponse,
133
+ CacheScanRequest,
134
+ CacheScanResponse,
135
+ CacheHealthResponse,
136
+ } from "./cache/client";
137
+ export type {
138
+ StorageUploadResponse,
139
+ StoragePinRequest,
140
+ StoragePinResponse,
141
+ StorageStatus,
142
+ } from "./storage/client";
143
+ export type { FunctionsClientConfig } from "./functions/client";
144
+ export type * from "./functions/types";
145
+ // Vault module
146
+ export { VaultClient } from "./vault/client";
147
+ export { AuthClient as VaultAuthClient } from "./vault/auth";
148
+ export { GuardianClient, GuardianError } from "./vault/transport";
149
+ export { fanOut, fanOutIndexed, withTimeout, withRetry } from "./vault/transport";
150
+ export { adaptiveThreshold, writeQuorum } from "./vault/quorum";
151
+ export {
152
+ encrypt,
153
+ decrypt,
154
+ encryptString,
155
+ decryptString,
156
+ serializeEncrypted,
157
+ deserializeEncrypted,
158
+ encryptAndSerialize,
159
+ deserializeAndDecrypt,
160
+ encryptedToHex,
161
+ encryptedFromHex,
162
+ encryptedToBase64,
163
+ encryptedFromBase64,
164
+ generateKey,
165
+ generateNonce,
166
+ clearKey,
167
+ isValidEncryptedData,
168
+ KEY_SIZE,
169
+ NONCE_SIZE,
170
+ TAG_SIZE,
171
+ deriveKeyHKDF,
172
+ shamirSplit,
173
+ shamirCombine,
174
+ } from "./vault";
175
+ export type {
176
+ VaultConfig,
177
+ SecretMeta,
178
+ StoreResult,
179
+ RetrieveResult,
180
+ ListResult,
181
+ DeleteResult,
182
+ GuardianResult as VaultGuardianResult,
183
+ EncryptedData,
184
+ SerializedEncryptedData,
185
+ ShamirShare,
186
+ GuardianEndpoint,
187
+ GuardianErrorCode,
188
+ GuardianInfo,
189
+ GuardianHealthResponse,
190
+ GuardianStatusResponse,
191
+ PushResponse,
192
+ PullResponse,
193
+ StoreSecretResponse,
194
+ GetSecretResponse,
195
+ DeleteSecretResponse,
196
+ ListSecretsResponse,
197
+ SecretEntry,
198
+ GuardianChallengeResponse,
199
+ GuardianSessionResponse,
200
+ FanOutResult,
201
+ } from "./vault";
@@ -0,0 +1,119 @@
1
+ import { HttpClient } from "../core/http";
2
+
3
+ export interface PeerInfo {
4
+ id: string;
5
+ addresses: string[];
6
+ lastSeen?: string;
7
+ }
8
+
9
+ export interface NetworkStatus {
10
+ node_id: string;
11
+ connected: boolean;
12
+ peer_count: number;
13
+ database_size: number;
14
+ uptime: number;
15
+ }
16
+
17
+ export interface ProxyRequest {
18
+ url: string;
19
+ method: string;
20
+ headers?: Record<string, string>;
21
+ body?: string;
22
+ }
23
+
24
+ export interface ProxyResponse {
25
+ status_code: number;
26
+ headers: Record<string, string>;
27
+ body: string;
28
+ error?: string;
29
+ }
30
+
31
+ export class NetworkClient {
32
+ private httpClient: HttpClient;
33
+
34
+ constructor(httpClient: HttpClient) {
35
+ this.httpClient = httpClient;
36
+ }
37
+
38
+ /**
39
+ * Check gateway health.
40
+ */
41
+ async health(): Promise<boolean> {
42
+ try {
43
+ await this.httpClient.get("/v1/health");
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Get network status.
52
+ */
53
+ async status(): Promise<NetworkStatus> {
54
+ const response = await this.httpClient.get<NetworkStatus>(
55
+ "/v1/network/status"
56
+ );
57
+ return response;
58
+ }
59
+
60
+ /**
61
+ * Get connected peers.
62
+ */
63
+ async peers(): Promise<PeerInfo[]> {
64
+ const response = await this.httpClient.get<{ peers: PeerInfo[] }>(
65
+ "/v1/network/peers"
66
+ );
67
+ return response.peers || [];
68
+ }
69
+
70
+ /**
71
+ * Connect to a peer.
72
+ */
73
+ async connect(peerAddr: string): Promise<void> {
74
+ await this.httpClient.post("/v1/network/connect", { peer_addr: peerAddr });
75
+ }
76
+
77
+ /**
78
+ * Disconnect from a peer.
79
+ */
80
+ async disconnect(peerId: string): Promise<void> {
81
+ await this.httpClient.post("/v1/network/disconnect", { peer_id: peerId });
82
+ }
83
+
84
+ /**
85
+ * Proxy an HTTP request through the Anyone network.
86
+ * Requires authentication (API key or JWT).
87
+ *
88
+ * @param request - The proxy request configuration
89
+ * @returns The proxied response
90
+ * @throws {SDKError} If the Anyone proxy is not available or the request fails
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * const response = await client.network.proxyAnon({
95
+ * url: 'https://api.example.com/data',
96
+ * method: 'GET',
97
+ * headers: {
98
+ * 'Accept': 'application/json'
99
+ * }
100
+ * });
101
+ *
102
+ * console.log(response.status_code); // 200
103
+ * console.log(response.body); // Response data
104
+ * ```
105
+ */
106
+ async proxyAnon(request: ProxyRequest): Promise<ProxyResponse> {
107
+ const response = await this.httpClient.post<ProxyResponse>(
108
+ "/v1/proxy/anon",
109
+ request
110
+ );
111
+
112
+ // Check if the response contains an error
113
+ if (response.error) {
114
+ throw new Error(`Proxy request failed: ${response.error}`);
115
+ }
116
+
117
+ return response;
118
+ }
119
+ }
@@ -0,0 +1,7 @@
1
+ export { NetworkClient } from "./client";
2
+ export type {
3
+ PeerInfo,
4
+ NetworkStatus,
5
+ ProxyRequest,
6
+ ProxyResponse,
7
+ } from "./client";
@@ -0,0 +1,361 @@
1
+ import { HttpClient } from "../core/http";
2
+ import { WSClient, WSClientConfig } from "../core/ws";
3
+ import {
4
+ PubSubMessage,
5
+ RawEnvelope,
6
+ MessageHandler,
7
+ ErrorHandler,
8
+ CloseHandler,
9
+ SubscribeOptions,
10
+ PresenceResponse,
11
+ PresenceMember,
12
+ PresenceOptions,
13
+ } from "./types";
14
+
15
+ // Cross-platform base64 encoding/decoding utilities
16
+ function base64Encode(str: string): string {
17
+ if (typeof Buffer !== "undefined") {
18
+ return Buffer.from(str).toString("base64");
19
+ } else if (typeof btoa !== "undefined") {
20
+ return btoa(
21
+ encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) =>
22
+ String.fromCharCode(parseInt(p1, 16))
23
+ )
24
+ );
25
+ }
26
+ throw new Error("No base64 encoding method available");
27
+ }
28
+
29
+ function base64EncodeBytes(bytes: Uint8Array): string {
30
+ if (typeof Buffer !== "undefined") {
31
+ return Buffer.from(bytes).toString("base64");
32
+ } else if (typeof btoa !== "undefined") {
33
+ let binary = "";
34
+ for (let i = 0; i < bytes.length; i++) {
35
+ binary += String.fromCharCode(bytes[i]);
36
+ }
37
+ return btoa(binary);
38
+ }
39
+ throw new Error("No base64 encoding method available");
40
+ }
41
+
42
+ function base64Decode(b64: string): string {
43
+ if (typeof Buffer !== "undefined") {
44
+ return Buffer.from(b64, "base64").toString("utf-8");
45
+ } else if (typeof atob !== "undefined") {
46
+ const binary = atob(b64);
47
+ const bytes = new Uint8Array(binary.length);
48
+ for (let i = 0; i < binary.length; i++) {
49
+ bytes[i] = binary.charCodeAt(i);
50
+ }
51
+ return new TextDecoder().decode(bytes);
52
+ }
53
+ throw new Error("No base64 decoding method available");
54
+ }
55
+
56
+ /**
57
+ * Simple PubSub client - one WebSocket connection per topic
58
+ * Gateway failover is handled at the application layer
59
+ */
60
+ export class PubSubClient {
61
+ private httpClient: HttpClient;
62
+ private wsConfig: Partial<WSClientConfig>;
63
+
64
+ constructor(httpClient: HttpClient, wsConfig: Partial<WSClientConfig> = {}) {
65
+ this.httpClient = httpClient;
66
+ this.wsConfig = wsConfig;
67
+ }
68
+
69
+ /**
70
+ * Publish a message to a topic via HTTP
71
+ */
72
+ async publish(topic: string, data: string | Uint8Array): Promise<void> {
73
+ let dataBase64: string;
74
+ if (typeof data === "string") {
75
+ dataBase64 = base64Encode(data);
76
+ } else {
77
+ dataBase64 = base64EncodeBytes(data);
78
+ }
79
+
80
+ await this.httpClient.post(
81
+ "/v1/pubsub/publish",
82
+ {
83
+ topic,
84
+ data_base64: dataBase64,
85
+ },
86
+ {
87
+ timeout: 30000,
88
+ }
89
+ );
90
+ }
91
+
92
+ /**
93
+ * List active topics in the current namespace
94
+ */
95
+ async topics(): Promise<string[]> {
96
+ const response = await this.httpClient.get<{ topics: string[] }>(
97
+ "/v1/pubsub/topics"
98
+ );
99
+ return response.topics || [];
100
+ }
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
+
112
+ /**
113
+ * Subscribe to a topic via WebSocket
114
+ * Creates one WebSocket connection per topic
115
+ */
116
+ async subscribe(
117
+ topic: string,
118
+ options: SubscribeOptions = {}
119
+ ): Promise<Subscription> {
120
+ // Build WebSocket URL for this topic
121
+ const wsUrl = new URL(this.wsConfig.wsURL || "ws://127.0.0.1:6001");
122
+ wsUrl.pathname = "/v1/pubsub/ws";
123
+ wsUrl.searchParams.set("topic", topic);
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
+
136
+ const authToken = this.httpClient.getApiKey() ?? this.httpClient.getToken();
137
+
138
+ // Create WebSocket client
139
+ const wsClient = new WSClient({
140
+ ...this.wsConfig,
141
+ wsURL: wsUrl.toString(),
142
+ authToken,
143
+ });
144
+
145
+ await wsClient.connect();
146
+
147
+ // Create subscription wrapper
148
+ const subscription = new Subscription(wsClient, topic, presence, () =>
149
+ this.getPresence(topic)
150
+ );
151
+
152
+ if (options.onMessage) {
153
+ subscription.onMessage(options.onMessage);
154
+ }
155
+ if (options.onError) {
156
+ subscription.onError(options.onError);
157
+ }
158
+ if (options.onClose) {
159
+ subscription.onClose(options.onClose);
160
+ }
161
+
162
+ return subscription;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Subscription represents an active WebSocket subscription to a topic
168
+ */
169
+ export class Subscription {
170
+ private wsClient: WSClient;
171
+ private topic: string;
172
+ private presenceOptions?: PresenceOptions;
173
+ private messageHandlers: Set<MessageHandler> = new Set();
174
+ private errorHandlers: Set<ErrorHandler> = new Set();
175
+ private closeHandlers: Set<CloseHandler> = new Set();
176
+ private isClosed = false;
177
+ private wsMessageHandler: ((data: string) => void) | null = null;
178
+ private wsErrorHandler: ((error: Error) => void) | null = null;
179
+ private wsCloseHandler: ((code: number, reason: string) => void) | null = null;
180
+ private getPresenceFn: () => Promise<PresenceResponse>;
181
+
182
+ constructor(
183
+ wsClient: WSClient,
184
+ topic: string,
185
+ presenceOptions: PresenceOptions | undefined,
186
+ getPresenceFn: () => Promise<PresenceResponse>
187
+ ) {
188
+ this.wsClient = wsClient;
189
+ this.topic = topic;
190
+ this.presenceOptions = presenceOptions;
191
+ this.getPresenceFn = getPresenceFn;
192
+
193
+ // Register message handler
194
+ this.wsMessageHandler = (data) => {
195
+ try {
196
+ // Parse gateway JSON envelope: {data: base64String, timestamp, topic}
197
+ const envelope: RawEnvelope = JSON.parse(data);
198
+
199
+ // Validate envelope structure
200
+ if (!envelope || typeof envelope !== "object") {
201
+ throw new Error("Invalid envelope: not an object");
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
+
234
+ if (!envelope.data || typeof envelope.data !== "string") {
235
+ throw new Error("Invalid envelope: missing or invalid data field");
236
+ }
237
+ if (!envelope.topic || typeof envelope.topic !== "string") {
238
+ throw new Error("Invalid envelope: missing or invalid topic field");
239
+ }
240
+ if (typeof envelope.timestamp !== "number") {
241
+ throw new Error(
242
+ "Invalid envelope: missing or invalid timestamp field"
243
+ );
244
+ }
245
+
246
+ // Decode base64 data
247
+ const messageData = base64Decode(envelope.data);
248
+
249
+ const message: PubSubMessage = {
250
+ topic: envelope.topic,
251
+ data: messageData,
252
+ timestamp: envelope.timestamp,
253
+ };
254
+
255
+ console.log("[Subscription] Received message on topic:", this.topic);
256
+ this.messageHandlers.forEach((handler) => handler(message));
257
+ } catch (error) {
258
+ console.error("[Subscription] Error processing message:", error);
259
+ this.errorHandlers.forEach((handler) =>
260
+ handler(error instanceof Error ? error : new Error(String(error)))
261
+ );
262
+ }
263
+ };
264
+
265
+ this.wsClient.onMessage(this.wsMessageHandler);
266
+
267
+ // Register error handler
268
+ this.wsErrorHandler = (error) => {
269
+ this.errorHandlers.forEach((handler) => handler(error));
270
+ };
271
+ this.wsClient.onError(this.wsErrorHandler);
272
+
273
+ // Register close handler
274
+ this.wsCloseHandler = (code: number, reason: string) => {
275
+ this.closeHandlers.forEach((handler) => handler(code, reason));
276
+ };
277
+ this.wsClient.onClose(this.wsCloseHandler);
278
+ }
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
+
299
+ /**
300
+ * Register message handler
301
+ */
302
+ onMessage(handler: MessageHandler): () => void {
303
+ this.messageHandlers.add(handler);
304
+ return () => this.messageHandlers.delete(handler);
305
+ }
306
+
307
+ /**
308
+ * Register error handler
309
+ */
310
+ onError(handler: ErrorHandler): () => void {
311
+ this.errorHandlers.add(handler);
312
+ return () => this.errorHandlers.delete(handler);
313
+ }
314
+
315
+ /**
316
+ * Register close handler
317
+ */
318
+ onClose(handler: CloseHandler): () => void {
319
+ this.closeHandlers.add(handler);
320
+ return () => this.closeHandlers.delete(handler);
321
+ }
322
+
323
+ /**
324
+ * Close subscription and underlying WebSocket
325
+ */
326
+ close(): void {
327
+ if (this.isClosed) {
328
+ return;
329
+ }
330
+ this.isClosed = true;
331
+
332
+ // Remove handlers from WSClient
333
+ if (this.wsMessageHandler) {
334
+ this.wsClient.offMessage(this.wsMessageHandler);
335
+ this.wsMessageHandler = null;
336
+ }
337
+ if (this.wsErrorHandler) {
338
+ this.wsClient.offError(this.wsErrorHandler);
339
+ this.wsErrorHandler = null;
340
+ }
341
+ if (this.wsCloseHandler) {
342
+ this.wsClient.offClose(this.wsCloseHandler);
343
+ this.wsCloseHandler = null;
344
+ }
345
+
346
+ // Clear all local handlers
347
+ this.messageHandlers.clear();
348
+ this.errorHandlers.clear();
349
+ this.closeHandlers.clear();
350
+
351
+ // Close WebSocket connection
352
+ this.wsClient.close();
353
+ }
354
+
355
+ /**
356
+ * Check if subscription is active
357
+ */
358
+ isConnected(): boolean {
359
+ return !this.isClosed && this.wsClient.isConnected();
360
+ }
361
+ }
@@ -0,0 +1,12 @@
1
+ export { PubSubClient, Subscription } from "./client";
2
+ export type {
3
+ PubSubMessage,
4
+ RawEnvelope,
5
+ MessageHandler,
6
+ ErrorHandler,
7
+ CloseHandler,
8
+ PresenceMember,
9
+ PresenceResponse,
10
+ PresenceOptions,
11
+ SubscribeOptions,
12
+ } from "./types";