@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/dist/index.js
CHANGED
|
@@ -40,6 +40,13 @@ var HttpClient = class {
|
|
|
40
40
|
this.maxRetries = config.maxRetries ?? 3;
|
|
41
41
|
this.retryDelayMs = config.retryDelayMs ?? 1e3;
|
|
42
42
|
this.fetch = config.fetch ?? createFetchWithTLSConfig();
|
|
43
|
+
this.onNetworkError = config.onNetworkError;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Set the network error callback
|
|
47
|
+
*/
|
|
48
|
+
setOnNetworkError(callback) {
|
|
49
|
+
this.onNetworkError = callback;
|
|
43
50
|
}
|
|
44
51
|
setApiKey(apiKey) {
|
|
45
52
|
this.apiKey = apiKey;
|
|
@@ -91,6 +98,12 @@ var HttpClient = class {
|
|
|
91
98
|
getApiKey() {
|
|
92
99
|
return this.apiKey;
|
|
93
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Get the base URL
|
|
103
|
+
*/
|
|
104
|
+
getBaseURL() {
|
|
105
|
+
return this.baseURL;
|
|
106
|
+
}
|
|
94
107
|
async request(method, path, options = {}) {
|
|
95
108
|
const startTime = performance.now();
|
|
96
109
|
const url = new URL(this.baseURL + path);
|
|
@@ -189,6 +202,21 @@ var HttpClient = class {
|
|
|
189
202
|
}
|
|
190
203
|
}
|
|
191
204
|
}
|
|
205
|
+
if (this.onNetworkError) {
|
|
206
|
+
const sdkError = error instanceof SDKError ? error : new SDKError(
|
|
207
|
+
error instanceof Error ? error.message : String(error),
|
|
208
|
+
0,
|
|
209
|
+
// httpStatus 0 indicates network-level failure
|
|
210
|
+
"NETWORK_ERROR"
|
|
211
|
+
);
|
|
212
|
+
this.onNetworkError(sdkError, {
|
|
213
|
+
method,
|
|
214
|
+
path,
|
|
215
|
+
isRetry: false,
|
|
216
|
+
attempt: this.maxRetries
|
|
217
|
+
// All retries exhausted
|
|
218
|
+
});
|
|
219
|
+
}
|
|
192
220
|
throw error;
|
|
193
221
|
} finally {
|
|
194
222
|
clearTimeout(timeoutId);
|
|
@@ -212,7 +240,13 @@ var HttpClient = class {
|
|
|
212
240
|
}
|
|
213
241
|
return response.text();
|
|
214
242
|
} catch (error) {
|
|
215
|
-
|
|
243
|
+
const isRetryableError = error instanceof SDKError && [408, 429, 500, 502, 503, 504].includes(error.httpStatus);
|
|
244
|
+
if (isRetryableError && attempt < this.maxRetries) {
|
|
245
|
+
if (typeof console !== "undefined") {
|
|
246
|
+
console.warn(
|
|
247
|
+
`[HttpClient] Retrying request (attempt ${attempt + 1}/${this.maxRetries})`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
216
250
|
await new Promise(
|
|
217
251
|
(resolve) => setTimeout(resolve, this.retryDelayMs * (attempt + 1))
|
|
218
252
|
);
|
|
@@ -279,6 +313,19 @@ var HttpClient = class {
|
|
|
279
313
|
error
|
|
280
314
|
);
|
|
281
315
|
}
|
|
316
|
+
if (this.onNetworkError) {
|
|
317
|
+
const sdkError = error instanceof SDKError ? error : new SDKError(
|
|
318
|
+
error instanceof Error ? error.message : String(error),
|
|
319
|
+
0,
|
|
320
|
+
"NETWORK_ERROR"
|
|
321
|
+
);
|
|
322
|
+
this.onNetworkError(sdkError, {
|
|
323
|
+
method: "POST",
|
|
324
|
+
path,
|
|
325
|
+
isRetry: false,
|
|
326
|
+
attempt: this.maxRetries
|
|
327
|
+
});
|
|
328
|
+
}
|
|
282
329
|
throw error;
|
|
283
330
|
} finally {
|
|
284
331
|
clearTimeout(timeoutId);
|
|
@@ -303,16 +350,26 @@ var HttpClient = class {
|
|
|
303
350
|
const response = await this.fetch(url.toString(), fetchOptions);
|
|
304
351
|
if (!response.ok) {
|
|
305
352
|
clearTimeout(timeoutId);
|
|
306
|
-
const
|
|
353
|
+
const errorBody = await response.json().catch(() => ({
|
|
307
354
|
error: response.statusText
|
|
308
355
|
}));
|
|
309
|
-
throw SDKError.fromResponse(response.status,
|
|
356
|
+
throw SDKError.fromResponse(response.status, errorBody);
|
|
310
357
|
}
|
|
311
358
|
return response;
|
|
312
359
|
} catch (error) {
|
|
313
360
|
clearTimeout(timeoutId);
|
|
314
|
-
if (
|
|
315
|
-
|
|
361
|
+
if (this.onNetworkError) {
|
|
362
|
+
const sdkError = error instanceof SDKError ? error : new SDKError(
|
|
363
|
+
error instanceof Error ? error.message : String(error),
|
|
364
|
+
0,
|
|
365
|
+
"NETWORK_ERROR"
|
|
366
|
+
);
|
|
367
|
+
this.onNetworkError(sdkError, {
|
|
368
|
+
method: "GET",
|
|
369
|
+
path,
|
|
370
|
+
isRetry: false,
|
|
371
|
+
attempt: 0
|
|
372
|
+
});
|
|
316
373
|
}
|
|
317
374
|
throw error;
|
|
318
375
|
}
|
|
@@ -798,10 +855,23 @@ var WSClient = class {
|
|
|
798
855
|
this.closeHandlers = /* @__PURE__ */ new Set();
|
|
799
856
|
this.openHandlers = /* @__PURE__ */ new Set();
|
|
800
857
|
this.isClosed = false;
|
|
801
|
-
this.
|
|
858
|
+
this.wsURL = config.wsURL;
|
|
802
859
|
this.timeout = config.timeout ?? 3e4;
|
|
803
860
|
this.authToken = config.authToken;
|
|
804
861
|
this.WebSocketClass = config.WebSocket ?? WebSocket;
|
|
862
|
+
this.onNetworkError = config.onNetworkError;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Set the network error callback
|
|
866
|
+
*/
|
|
867
|
+
setOnNetworkError(callback) {
|
|
868
|
+
this.onNetworkError = callback;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Get the current WebSocket URL
|
|
872
|
+
*/
|
|
873
|
+
get url() {
|
|
874
|
+
return this.wsURL;
|
|
805
875
|
}
|
|
806
876
|
/**
|
|
807
877
|
* Connect to WebSocket server
|
|
@@ -814,13 +884,20 @@ var WSClient = class {
|
|
|
814
884
|
this.isClosed = false;
|
|
815
885
|
const timeout = setTimeout(() => {
|
|
816
886
|
this.ws?.close();
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
887
|
+
const error = new SDKError("WebSocket connection timeout", 408, "WS_TIMEOUT");
|
|
888
|
+
if (this.onNetworkError) {
|
|
889
|
+
this.onNetworkError(error, {
|
|
890
|
+
method: "WS",
|
|
891
|
+
path: this.wsURL,
|
|
892
|
+
isRetry: false,
|
|
893
|
+
attempt: 0
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
reject(error);
|
|
820
897
|
}, this.timeout);
|
|
821
898
|
this.ws.addEventListener("open", () => {
|
|
822
899
|
clearTimeout(timeout);
|
|
823
|
-
console.log("[WSClient] Connected to", this.
|
|
900
|
+
console.log("[WSClient] Connected to", this.wsURL);
|
|
824
901
|
this.openHandlers.forEach((handler) => handler());
|
|
825
902
|
resolve();
|
|
826
903
|
});
|
|
@@ -832,7 +909,16 @@ var WSClient = class {
|
|
|
832
909
|
console.error("[WSClient] WebSocket error:", event);
|
|
833
910
|
clearTimeout(timeout);
|
|
834
911
|
const error = new SDKError("WebSocket error", 500, "WS_ERROR", event);
|
|
912
|
+
if (this.onNetworkError) {
|
|
913
|
+
this.onNetworkError(error, {
|
|
914
|
+
method: "WS",
|
|
915
|
+
path: this.wsURL,
|
|
916
|
+
isRetry: false,
|
|
917
|
+
attempt: 0
|
|
918
|
+
});
|
|
919
|
+
}
|
|
835
920
|
this.errorHandlers.forEach((handler) => handler(error));
|
|
921
|
+
reject(error);
|
|
836
922
|
});
|
|
837
923
|
this.ws.addEventListener("close", () => {
|
|
838
924
|
clearTimeout(timeout);
|
|
@@ -848,7 +934,7 @@ var WSClient = class {
|
|
|
848
934
|
* Build WebSocket URL with auth token
|
|
849
935
|
*/
|
|
850
936
|
buildWSUrl() {
|
|
851
|
-
let url = this.
|
|
937
|
+
let url = this.wsURL;
|
|
852
938
|
if (this.authToken) {
|
|
853
939
|
const separator = url.includes("?") ? "&" : "?";
|
|
854
940
|
const paramName = this.authToken.startsWith("ak_") ? "api_key" : "token";
|
|
@@ -1009,14 +1095,32 @@ var PubSubClient = class {
|
|
|
1009
1095
|
);
|
|
1010
1096
|
return response.topics || [];
|
|
1011
1097
|
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Get current presence for a topic without subscribing
|
|
1100
|
+
*/
|
|
1101
|
+
async getPresence(topic) {
|
|
1102
|
+
const response = await this.httpClient.get(
|
|
1103
|
+
`/v1/pubsub/presence?topic=${encodeURIComponent(topic)}`
|
|
1104
|
+
);
|
|
1105
|
+
return response;
|
|
1106
|
+
}
|
|
1012
1107
|
/**
|
|
1013
1108
|
* Subscribe to a topic via WebSocket
|
|
1014
1109
|
* Creates one WebSocket connection per topic
|
|
1015
1110
|
*/
|
|
1016
|
-
async subscribe(topic,
|
|
1111
|
+
async subscribe(topic, options = {}) {
|
|
1017
1112
|
const wsUrl = new URL(this.wsConfig.wsURL || "ws://127.0.0.1:6001");
|
|
1018
1113
|
wsUrl.pathname = "/v1/pubsub/ws";
|
|
1019
1114
|
wsUrl.searchParams.set("topic", topic);
|
|
1115
|
+
let presence;
|
|
1116
|
+
if (options.presence?.enabled) {
|
|
1117
|
+
presence = options.presence;
|
|
1118
|
+
wsUrl.searchParams.set("presence", "true");
|
|
1119
|
+
wsUrl.searchParams.set("member_id", presence.memberId);
|
|
1120
|
+
if (presence.meta) {
|
|
1121
|
+
wsUrl.searchParams.set("member_meta", JSON.stringify(presence.meta));
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1020
1124
|
const authToken = this.httpClient.getApiKey() ?? this.httpClient.getToken();
|
|
1021
1125
|
const wsClient = new WSClient({
|
|
1022
1126
|
...this.wsConfig,
|
|
@@ -1024,21 +1128,26 @@ var PubSubClient = class {
|
|
|
1024
1128
|
authToken
|
|
1025
1129
|
});
|
|
1026
1130
|
await wsClient.connect();
|
|
1027
|
-
const subscription = new Subscription(
|
|
1028
|
-
|
|
1029
|
-
|
|
1131
|
+
const subscription = new Subscription(
|
|
1132
|
+
wsClient,
|
|
1133
|
+
topic,
|
|
1134
|
+
presence,
|
|
1135
|
+
() => this.getPresence(topic)
|
|
1136
|
+
);
|
|
1137
|
+
if (options.onMessage) {
|
|
1138
|
+
subscription.onMessage(options.onMessage);
|
|
1030
1139
|
}
|
|
1031
|
-
if (
|
|
1032
|
-
subscription.onError(
|
|
1140
|
+
if (options.onError) {
|
|
1141
|
+
subscription.onError(options.onError);
|
|
1033
1142
|
}
|
|
1034
|
-
if (
|
|
1035
|
-
subscription.onClose(
|
|
1143
|
+
if (options.onClose) {
|
|
1144
|
+
subscription.onClose(options.onClose);
|
|
1036
1145
|
}
|
|
1037
1146
|
return subscription;
|
|
1038
1147
|
}
|
|
1039
1148
|
};
|
|
1040
1149
|
var Subscription = class {
|
|
1041
|
-
constructor(wsClient, topic) {
|
|
1150
|
+
constructor(wsClient, topic, presenceOptions, getPresenceFn) {
|
|
1042
1151
|
this.messageHandlers = /* @__PURE__ */ new Set();
|
|
1043
1152
|
this.errorHandlers = /* @__PURE__ */ new Set();
|
|
1044
1153
|
this.closeHandlers = /* @__PURE__ */ new Set();
|
|
@@ -1048,12 +1157,31 @@ var Subscription = class {
|
|
|
1048
1157
|
this.wsCloseHandler = null;
|
|
1049
1158
|
this.wsClient = wsClient;
|
|
1050
1159
|
this.topic = topic;
|
|
1160
|
+
this.presenceOptions = presenceOptions;
|
|
1161
|
+
this.getPresenceFn = getPresenceFn;
|
|
1051
1162
|
this.wsMessageHandler = (data) => {
|
|
1052
1163
|
try {
|
|
1053
1164
|
const envelope = JSON.parse(data);
|
|
1054
1165
|
if (!envelope || typeof envelope !== "object") {
|
|
1055
1166
|
throw new Error("Invalid envelope: not an object");
|
|
1056
1167
|
}
|
|
1168
|
+
if (envelope.type === "presence.join" || envelope.type === "presence.leave") {
|
|
1169
|
+
if (!envelope.member_id) {
|
|
1170
|
+
console.warn("[Subscription] Presence event missing member_id");
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
const presenceMember = {
|
|
1174
|
+
memberId: envelope.member_id,
|
|
1175
|
+
joinedAt: envelope.timestamp,
|
|
1176
|
+
meta: envelope.meta
|
|
1177
|
+
};
|
|
1178
|
+
if (envelope.type === "presence.join" && this.presenceOptions?.onJoin) {
|
|
1179
|
+
this.presenceOptions.onJoin(presenceMember);
|
|
1180
|
+
} else if (envelope.type === "presence.leave" && this.presenceOptions?.onLeave) {
|
|
1181
|
+
this.presenceOptions.onLeave(presenceMember);
|
|
1182
|
+
}
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1057
1185
|
if (!envelope.data || typeof envelope.data !== "string") {
|
|
1058
1186
|
throw new Error("Invalid envelope: missing or invalid data field");
|
|
1059
1187
|
}
|
|
@@ -1090,6 +1218,22 @@ var Subscription = class {
|
|
|
1090
1218
|
};
|
|
1091
1219
|
this.wsClient.onClose(this.wsCloseHandler);
|
|
1092
1220
|
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Get current presence (requires presence.enabled on subscribe)
|
|
1223
|
+
*/
|
|
1224
|
+
async getPresence() {
|
|
1225
|
+
if (!this.presenceOptions?.enabled) {
|
|
1226
|
+
throw new Error("Presence is not enabled for this subscription");
|
|
1227
|
+
}
|
|
1228
|
+
const response = await this.getPresenceFn();
|
|
1229
|
+
return response.members;
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Check if presence is enabled for this subscription
|
|
1233
|
+
*/
|
|
1234
|
+
hasPresence() {
|
|
1235
|
+
return !!this.presenceOptions?.enabled;
|
|
1236
|
+
}
|
|
1093
1237
|
/**
|
|
1094
1238
|
* Register message handler
|
|
1095
1239
|
*/
|
|
@@ -1511,6 +1655,38 @@ var StorageClient = class {
|
|
|
1511
1655
|
}
|
|
1512
1656
|
};
|
|
1513
1657
|
|
|
1658
|
+
// src/functions/client.ts
|
|
1659
|
+
var FunctionsClient = class {
|
|
1660
|
+
constructor(httpClient, config) {
|
|
1661
|
+
this.httpClient = httpClient;
|
|
1662
|
+
this.gatewayURL = config?.gatewayURL;
|
|
1663
|
+
this.namespace = config?.namespace ?? "default";
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Invoke a serverless function by name
|
|
1667
|
+
*
|
|
1668
|
+
* @param functionName - Name of the function to invoke
|
|
1669
|
+
* @param input - Input payload for the function
|
|
1670
|
+
* @returns The function response
|
|
1671
|
+
*/
|
|
1672
|
+
async invoke(functionName, input) {
|
|
1673
|
+
const url = this.gatewayURL ? `${this.gatewayURL}/v1/invoke/${this.namespace}/${functionName}` : `/v1/invoke/${this.namespace}/${functionName}`;
|
|
1674
|
+
try {
|
|
1675
|
+
const response = await this.httpClient.post(url, input);
|
|
1676
|
+
return response;
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
if (error instanceof SDKError) {
|
|
1679
|
+
throw error;
|
|
1680
|
+
}
|
|
1681
|
+
throw new SDKError(
|
|
1682
|
+
`Function ${functionName} failed`,
|
|
1683
|
+
500,
|
|
1684
|
+
error instanceof Error ? error.message : String(error)
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
|
|
1514
1690
|
// src/index.ts
|
|
1515
1691
|
function createClient(config) {
|
|
1516
1692
|
const httpClient = new HttpClient({
|
|
@@ -1518,7 +1694,8 @@ function createClient(config) {
|
|
|
1518
1694
|
timeout: config.timeout,
|
|
1519
1695
|
maxRetries: config.maxRetries,
|
|
1520
1696
|
retryDelayMs: config.retryDelayMs,
|
|
1521
|
-
fetch: config.fetch
|
|
1697
|
+
fetch: config.fetch,
|
|
1698
|
+
onNetworkError: config.onNetworkError
|
|
1522
1699
|
});
|
|
1523
1700
|
const auth = new AuthClient({
|
|
1524
1701
|
httpClient,
|
|
@@ -1526,28 +1703,32 @@ function createClient(config) {
|
|
|
1526
1703
|
apiKey: config.apiKey,
|
|
1527
1704
|
jwt: config.jwt
|
|
1528
1705
|
});
|
|
1529
|
-
const wsURL = config.
|
|
1706
|
+
const wsURL = config.baseURL.replace(/^http/, "ws").replace(/\/$/, "");
|
|
1530
1707
|
const db = new DBClient(httpClient);
|
|
1531
1708
|
const pubsub = new PubSubClient(httpClient, {
|
|
1532
1709
|
...config.wsConfig,
|
|
1533
|
-
wsURL
|
|
1710
|
+
wsURL,
|
|
1711
|
+
onNetworkError: config.onNetworkError
|
|
1534
1712
|
});
|
|
1535
1713
|
const network = new NetworkClient(httpClient);
|
|
1536
1714
|
const cache = new CacheClient(httpClient);
|
|
1537
1715
|
const storage = new StorageClient(httpClient);
|
|
1716
|
+
const functions = new FunctionsClient(httpClient, config.functionsConfig);
|
|
1538
1717
|
return {
|
|
1539
1718
|
auth,
|
|
1540
1719
|
db,
|
|
1541
1720
|
pubsub,
|
|
1542
1721
|
network,
|
|
1543
1722
|
cache,
|
|
1544
|
-
storage
|
|
1723
|
+
storage,
|
|
1724
|
+
functions
|
|
1545
1725
|
};
|
|
1546
1726
|
}
|
|
1547
1727
|
export {
|
|
1548
1728
|
AuthClient,
|
|
1549
1729
|
CacheClient,
|
|
1550
1730
|
DBClient,
|
|
1731
|
+
FunctionsClient,
|
|
1551
1732
|
HttpClient,
|
|
1552
1733
|
LocalStorageAdapter,
|
|
1553
1734
|
MemoryStorage,
|