@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.
- package/LICENSE +21 -0
- package/README.md +665 -0
- package/dist/index.d.ts +1334 -0
- package/dist/index.js +2553 -0
- package/dist/index.js.map +1 -0
- package/package.json +82 -0
- package/src/auth/client.ts +276 -0
- package/src/auth/index.ts +3 -0
- package/src/auth/types.ts +62 -0
- package/src/cache/client.ts +203 -0
- package/src/cache/index.ts +14 -0
- package/src/core/http.ts +541 -0
- package/src/core/index.ts +10 -0
- package/src/core/interfaces/IAuthStrategy.ts +28 -0
- package/src/core/interfaces/IHttpTransport.ts +73 -0
- package/src/core/interfaces/IRetryPolicy.ts +20 -0
- package/src/core/interfaces/IWebSocketClient.ts +60 -0
- package/src/core/interfaces/index.ts +4 -0
- package/src/core/transport/AuthHeaderStrategy.ts +108 -0
- package/src/core/transport/RequestLogger.ts +116 -0
- package/src/core/transport/RequestRetryPolicy.ts +53 -0
- package/src/core/transport/TLSConfiguration.ts +53 -0
- package/src/core/transport/index.ts +4 -0
- package/src/core/ws.ts +246 -0
- package/src/db/client.ts +126 -0
- package/src/db/index.ts +13 -0
- package/src/db/qb.ts +111 -0
- package/src/db/repository.ts +128 -0
- package/src/db/types.ts +67 -0
- package/src/errors.ts +38 -0
- package/src/functions/client.ts +62 -0
- package/src/functions/index.ts +2 -0
- package/src/functions/types.ts +21 -0
- package/src/index.ts +201 -0
- package/src/network/client.ts +119 -0
- package/src/network/index.ts +7 -0
- package/src/pubsub/client.ts +361 -0
- package/src/pubsub/index.ts +12 -0
- package/src/pubsub/types.ts +46 -0
- package/src/storage/client.ts +272 -0
- package/src/storage/index.ts +7 -0
- package/src/utils/codec.ts +68 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/platform.ts +44 -0
- package/src/utils/retry.ts +58 -0
- package/src/vault/auth.ts +98 -0
- package/src/vault/client.ts +197 -0
- package/src/vault/crypto/aes.ts +271 -0
- package/src/vault/crypto/hkdf.ts +42 -0
- package/src/vault/crypto/index.ts +27 -0
- package/src/vault/crypto/shamir.ts +173 -0
- package/src/vault/index.ts +65 -0
- package/src/vault/quorum.ts +16 -0
- package/src/vault/transport/fanout.ts +94 -0
- package/src/vault/transport/guardian.ts +285 -0
- package/src/vault/transport/index.ts +19 -0
- package/src/vault/transport/types.ts +101 -0
- 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,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
|
+
}
|