@debros/network-ts-sdk 0.1.5 → 0.3.1
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 +96 -1
- package/dist/index.d.ts +392 -29
- package/dist/index.js +795 -96
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/src/auth/client.ts +144 -10
- package/src/cache/client.ts +203 -0
- package/src/core/http.ts +248 -13
- package/src/core/ws.ts +87 -80
- package/src/db/repository.ts +6 -2
- package/src/index.ts +44 -10
- package/src/network/client.ts +58 -4
- package/src/pubsub/client.ts +174 -28
- package/src/storage/client.ts +270 -0
package/dist/index.js
CHANGED
|
@@ -27,32 +27,63 @@ var SDKError = class _SDKError extends Error {
|
|
|
27
27
|
var HttpClient = class {
|
|
28
28
|
constructor(config) {
|
|
29
29
|
this.baseURL = config.baseURL.replace(/\/$/, "");
|
|
30
|
-
this.timeout = config.timeout ??
|
|
30
|
+
this.timeout = config.timeout ?? 6e4;
|
|
31
31
|
this.maxRetries = config.maxRetries ?? 3;
|
|
32
32
|
this.retryDelayMs = config.retryDelayMs ?? 1e3;
|
|
33
33
|
this.fetch = config.fetch ?? globalThis.fetch;
|
|
34
34
|
}
|
|
35
35
|
setApiKey(apiKey) {
|
|
36
36
|
this.apiKey = apiKey;
|
|
37
|
-
this.jwt = void 0;
|
|
38
37
|
}
|
|
39
38
|
setJwt(jwt) {
|
|
40
39
|
this.jwt = jwt;
|
|
41
|
-
|
|
40
|
+
if (typeof console !== "undefined") {
|
|
41
|
+
console.log(
|
|
42
|
+
"[HttpClient] JWT set:",
|
|
43
|
+
!!jwt,
|
|
44
|
+
"API key still present:",
|
|
45
|
+
!!this.apiKey
|
|
46
|
+
);
|
|
47
|
+
}
|
|
42
48
|
}
|
|
43
|
-
getAuthHeaders() {
|
|
49
|
+
getAuthHeaders(path) {
|
|
44
50
|
const headers = {};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
const isDbOperation = path.includes("/v1/rqlite/");
|
|
52
|
+
const isPubSubOperation = path.includes("/v1/pubsub/");
|
|
53
|
+
const isProxyOperation = path.includes("/v1/proxy/");
|
|
54
|
+
const isCacheOperation = path.includes("/v1/cache/");
|
|
55
|
+
const isAuthOperation = path.includes("/v1/auth/");
|
|
56
|
+
if (isDbOperation || isPubSubOperation || isProxyOperation || isCacheOperation) {
|
|
57
|
+
if (this.apiKey) {
|
|
58
|
+
headers["X-API-Key"] = this.apiKey;
|
|
59
|
+
} else if (this.jwt) {
|
|
60
|
+
headers["Authorization"] = `Bearer ${this.jwt}`;
|
|
61
|
+
}
|
|
62
|
+
} else if (isAuthOperation) {
|
|
63
|
+
if (this.apiKey) {
|
|
64
|
+
headers["X-API-Key"] = this.apiKey;
|
|
65
|
+
}
|
|
66
|
+
if (this.jwt) {
|
|
67
|
+
headers["Authorization"] = `Bearer ${this.jwt}`;
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
if (this.jwt) {
|
|
71
|
+
headers["Authorization"] = `Bearer ${this.jwt}`;
|
|
72
|
+
}
|
|
73
|
+
if (this.apiKey) {
|
|
74
|
+
headers["X-API-Key"] = this.apiKey;
|
|
75
|
+
}
|
|
49
76
|
}
|
|
50
77
|
return headers;
|
|
51
78
|
}
|
|
52
79
|
getAuthToken() {
|
|
53
80
|
return this.jwt || this.apiKey;
|
|
54
81
|
}
|
|
82
|
+
getApiKey() {
|
|
83
|
+
return this.apiKey;
|
|
84
|
+
}
|
|
55
85
|
async request(method, path, options = {}) {
|
|
86
|
+
const startTime = performance.now();
|
|
56
87
|
const url = new URL(this.baseURL + path);
|
|
57
88
|
if (options.query) {
|
|
58
89
|
Object.entries(options.query).forEach(([key, value]) => {
|
|
@@ -61,20 +92,70 @@ var HttpClient = class {
|
|
|
61
92
|
}
|
|
62
93
|
const headers = {
|
|
63
94
|
"Content-Type": "application/json",
|
|
64
|
-
...this.getAuthHeaders(),
|
|
95
|
+
...this.getAuthHeaders(path),
|
|
65
96
|
...options.headers
|
|
66
97
|
};
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
const requestTimeout = options.timeout ?? this.timeout;
|
|
100
|
+
const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
|
|
67
101
|
const fetchOptions = {
|
|
68
102
|
method,
|
|
69
103
|
headers,
|
|
70
|
-
signal:
|
|
104
|
+
signal: controller.signal
|
|
71
105
|
};
|
|
72
106
|
if (options.body !== void 0) {
|
|
73
107
|
fetchOptions.body = JSON.stringify(options.body);
|
|
74
108
|
}
|
|
75
|
-
|
|
109
|
+
try {
|
|
110
|
+
const result = await this.requestWithRetry(
|
|
111
|
+
url.toString(),
|
|
112
|
+
fetchOptions,
|
|
113
|
+
0,
|
|
114
|
+
startTime
|
|
115
|
+
);
|
|
116
|
+
const duration = performance.now() - startTime;
|
|
117
|
+
if (typeof console !== "undefined") {
|
|
118
|
+
console.log(
|
|
119
|
+
`[HttpClient] ${method} ${path} completed in ${duration.toFixed(2)}ms`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
const duration = performance.now() - startTime;
|
|
125
|
+
if (typeof console !== "undefined") {
|
|
126
|
+
const isCacheGetNotFound = path === "/v1/cache/get" && error instanceof SDKError && (error.httpStatus === 404 || error.httpStatus === 500 && error.message?.toLowerCase().includes("key not found"));
|
|
127
|
+
const isBlockedUsersNotFound = path === "/v1/rqlite/find-one" && error instanceof SDKError && error.httpStatus === 404 && options.body && (() => {
|
|
128
|
+
try {
|
|
129
|
+
const body = typeof options.body === "string" ? JSON.parse(options.body) : options.body;
|
|
130
|
+
return body.table === "blocked_users";
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
})();
|
|
135
|
+
const isConversationParticipantNotFound = path === "/v1/rqlite/find-one" && error instanceof SDKError && error.httpStatus === 404 && options.body && (() => {
|
|
136
|
+
try {
|
|
137
|
+
const body = typeof options.body === "string" ? JSON.parse(options.body) : options.body;
|
|
138
|
+
return body.table === "conversation_participants";
|
|
139
|
+
} catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
})();
|
|
143
|
+
if (isCacheGetNotFound || isBlockedUsersNotFound || isConversationParticipantNotFound) {
|
|
144
|
+
} else {
|
|
145
|
+
console.error(
|
|
146
|
+
`[HttpClient] ${method} ${path} failed after ${duration.toFixed(
|
|
147
|
+
2
|
|
148
|
+
)}ms:`,
|
|
149
|
+
error
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
} finally {
|
|
155
|
+
clearTimeout(timeoutId);
|
|
156
|
+
}
|
|
76
157
|
}
|
|
77
|
-
async requestWithRetry(url, options, attempt = 0) {
|
|
158
|
+
async requestWithRetry(url, options, attempt = 0, startTime) {
|
|
78
159
|
try {
|
|
79
160
|
const response = await this.fetch(url, options);
|
|
80
161
|
if (!response.ok) {
|
|
@@ -96,7 +177,7 @@ var HttpClient = class {
|
|
|
96
177
|
await new Promise(
|
|
97
178
|
(resolve) => setTimeout(resolve, this.retryDelayMs * (attempt + 1))
|
|
98
179
|
);
|
|
99
|
-
return this.requestWithRetry(url, options, attempt + 1);
|
|
180
|
+
return this.requestWithRetry(url, options, attempt + 1, startTime);
|
|
100
181
|
}
|
|
101
182
|
throw error;
|
|
102
183
|
}
|
|
@@ -113,6 +194,90 @@ var HttpClient = class {
|
|
|
113
194
|
async delete(path, options) {
|
|
114
195
|
return this.request("DELETE", path, options);
|
|
115
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Upload a file using multipart/form-data
|
|
199
|
+
* This is a special method for file uploads that bypasses JSON serialization
|
|
200
|
+
*/
|
|
201
|
+
async uploadFile(path, formData, options) {
|
|
202
|
+
const startTime = performance.now();
|
|
203
|
+
const url = new URL(this.baseURL + path);
|
|
204
|
+
const headers = {
|
|
205
|
+
...this.getAuthHeaders(path)
|
|
206
|
+
// Don't set Content-Type - browser will set it with boundary
|
|
207
|
+
};
|
|
208
|
+
const controller = new AbortController();
|
|
209
|
+
const requestTimeout = options?.timeout ?? this.timeout * 5;
|
|
210
|
+
const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
|
|
211
|
+
const fetchOptions = {
|
|
212
|
+
method: "POST",
|
|
213
|
+
headers,
|
|
214
|
+
body: formData,
|
|
215
|
+
signal: controller.signal
|
|
216
|
+
};
|
|
217
|
+
try {
|
|
218
|
+
const result = await this.requestWithRetry(
|
|
219
|
+
url.toString(),
|
|
220
|
+
fetchOptions,
|
|
221
|
+
0,
|
|
222
|
+
startTime
|
|
223
|
+
);
|
|
224
|
+
const duration = performance.now() - startTime;
|
|
225
|
+
if (typeof console !== "undefined") {
|
|
226
|
+
console.log(
|
|
227
|
+
`[HttpClient] POST ${path} (upload) completed in ${duration.toFixed(
|
|
228
|
+
2
|
|
229
|
+
)}ms`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
const duration = performance.now() - startTime;
|
|
235
|
+
if (typeof console !== "undefined") {
|
|
236
|
+
console.error(
|
|
237
|
+
`[HttpClient] POST ${path} (upload) failed after ${duration.toFixed(
|
|
238
|
+
2
|
|
239
|
+
)}ms:`,
|
|
240
|
+
error
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
throw error;
|
|
244
|
+
} finally {
|
|
245
|
+
clearTimeout(timeoutId);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Get a binary response (returns Response object for streaming)
|
|
250
|
+
*/
|
|
251
|
+
async getBinary(path) {
|
|
252
|
+
const url = new URL(this.baseURL + path);
|
|
253
|
+
const headers = {
|
|
254
|
+
...this.getAuthHeaders(path)
|
|
255
|
+
};
|
|
256
|
+
const controller = new AbortController();
|
|
257
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout * 5);
|
|
258
|
+
const fetchOptions = {
|
|
259
|
+
method: "GET",
|
|
260
|
+
headers,
|
|
261
|
+
signal: controller.signal
|
|
262
|
+
};
|
|
263
|
+
try {
|
|
264
|
+
const response = await this.fetch(url.toString(), fetchOptions);
|
|
265
|
+
if (!response.ok) {
|
|
266
|
+
clearTimeout(timeoutId);
|
|
267
|
+
const error = await response.json().catch(() => ({
|
|
268
|
+
error: response.statusText
|
|
269
|
+
}));
|
|
270
|
+
throw SDKError.fromResponse(response.status, error);
|
|
271
|
+
}
|
|
272
|
+
return response;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
clearTimeout(timeoutId);
|
|
275
|
+
if (error instanceof SDKError) {
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
116
281
|
getToken() {
|
|
117
282
|
return this.getAuthToken();
|
|
118
283
|
}
|
|
@@ -178,13 +343,11 @@ var AuthClient = class {
|
|
|
178
343
|
}
|
|
179
344
|
setApiKey(apiKey) {
|
|
180
345
|
this.currentApiKey = apiKey;
|
|
181
|
-
this.currentJwt = void 0;
|
|
182
346
|
this.httpClient.setApiKey(apiKey);
|
|
183
347
|
this.storage.set("apiKey", apiKey);
|
|
184
348
|
}
|
|
185
349
|
setJwt(jwt) {
|
|
186
350
|
this.currentJwt = jwt;
|
|
187
|
-
this.currentApiKey = void 0;
|
|
188
351
|
this.httpClient.setJwt(jwt);
|
|
189
352
|
this.storage.set("jwt", jwt);
|
|
190
353
|
}
|
|
@@ -207,12 +370,50 @@ var AuthClient = class {
|
|
|
207
370
|
this.setJwt(token);
|
|
208
371
|
return token;
|
|
209
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* Logout user and clear JWT, but preserve API key
|
|
375
|
+
* Use this for user logout in apps where API key is app-level credential
|
|
376
|
+
*/
|
|
377
|
+
async logoutUser() {
|
|
378
|
+
if (this.currentJwt) {
|
|
379
|
+
try {
|
|
380
|
+
await this.httpClient.post("/v1/auth/logout", { all: true });
|
|
381
|
+
} catch (error) {
|
|
382
|
+
console.warn(
|
|
383
|
+
"Server-side logout failed, continuing with local cleanup:",
|
|
384
|
+
error
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
this.currentJwt = void 0;
|
|
389
|
+
this.httpClient.setJwt(void 0);
|
|
390
|
+
await this.storage.set("jwt", "");
|
|
391
|
+
if (!this.currentApiKey) {
|
|
392
|
+
const storedApiKey = await this.storage.get("apiKey");
|
|
393
|
+
if (storedApiKey) {
|
|
394
|
+
this.currentApiKey = storedApiKey;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (this.currentApiKey) {
|
|
398
|
+
this.httpClient.setApiKey(this.currentApiKey);
|
|
399
|
+
console.log("[Auth] API key restored after user logout");
|
|
400
|
+
} else {
|
|
401
|
+
console.warn("[Auth] No API key available after logout");
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Full logout - clears both JWT and API key
|
|
406
|
+
* Use this to completely reset authentication state
|
|
407
|
+
*/
|
|
210
408
|
async logout() {
|
|
211
409
|
if (this.currentJwt) {
|
|
212
410
|
try {
|
|
213
411
|
await this.httpClient.post("/v1/auth/logout", { all: true });
|
|
214
412
|
} catch (error) {
|
|
215
|
-
console.warn(
|
|
413
|
+
console.warn(
|
|
414
|
+
"Server-side logout failed, continuing with local cleanup:",
|
|
415
|
+
error
|
|
416
|
+
);
|
|
216
417
|
}
|
|
217
418
|
}
|
|
218
419
|
this.currentApiKey = void 0;
|
|
@@ -228,6 +429,51 @@ var AuthClient = class {
|
|
|
228
429
|
this.httpClient.setJwt(void 0);
|
|
229
430
|
await this.storage.clear();
|
|
230
431
|
}
|
|
432
|
+
/**
|
|
433
|
+
* Request a challenge nonce for wallet authentication
|
|
434
|
+
*/
|
|
435
|
+
async challenge(params) {
|
|
436
|
+
const response = await this.httpClient.post("/v1/auth/challenge", {
|
|
437
|
+
wallet: params.wallet,
|
|
438
|
+
purpose: params.purpose || "authentication",
|
|
439
|
+
namespace: params.namespace || "default"
|
|
440
|
+
});
|
|
441
|
+
return response;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Verify wallet signature and get JWT token
|
|
445
|
+
*/
|
|
446
|
+
async verify(params) {
|
|
447
|
+
const response = await this.httpClient.post("/v1/auth/verify", {
|
|
448
|
+
wallet: params.wallet,
|
|
449
|
+
nonce: params.nonce,
|
|
450
|
+
signature: params.signature,
|
|
451
|
+
namespace: params.namespace || "default",
|
|
452
|
+
chain_type: params.chain_type || "ETH"
|
|
453
|
+
});
|
|
454
|
+
this.setJwt(response.access_token);
|
|
455
|
+
if (response.api_key) {
|
|
456
|
+
this.setApiKey(response.api_key);
|
|
457
|
+
}
|
|
458
|
+
if (response.refresh_token) {
|
|
459
|
+
await this.storage.set("refreshToken", response.refresh_token);
|
|
460
|
+
}
|
|
461
|
+
return response;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Get API key for wallet (creates namespace ownership)
|
|
465
|
+
*/
|
|
466
|
+
async getApiKey(params) {
|
|
467
|
+
const response = await this.httpClient.post("/v1/auth/api-key", {
|
|
468
|
+
wallet: params.wallet,
|
|
469
|
+
nonce: params.nonce,
|
|
470
|
+
signature: params.signature,
|
|
471
|
+
namespace: params.namespace || "default",
|
|
472
|
+
chain_type: params.chain_type || "ETH"
|
|
473
|
+
});
|
|
474
|
+
this.setApiKey(response.api_key);
|
|
475
|
+
return response;
|
|
476
|
+
}
|
|
231
477
|
};
|
|
232
478
|
|
|
233
479
|
// src/db/qb.ts
|
|
@@ -397,7 +643,9 @@ var Repository = class {
|
|
|
397
643
|
buildInsertSql(entity) {
|
|
398
644
|
const columns = Object.keys(entity).filter((k) => entity[k] !== void 0);
|
|
399
645
|
const placeholders = columns.map(() => "?").join(", ");
|
|
400
|
-
return `INSERT INTO ${this.tableName} (${columns.join(
|
|
646
|
+
return `INSERT INTO ${this.tableName} (${columns.join(
|
|
647
|
+
", "
|
|
648
|
+
)}) VALUES (${placeholders})`;
|
|
401
649
|
}
|
|
402
650
|
buildInsertArgs(entity) {
|
|
403
651
|
return Object.entries(entity).filter(([, v]) => v !== void 0).map(([, v]) => v);
|
|
@@ -506,33 +754,35 @@ var DBClient = class {
|
|
|
506
754
|
import WebSocket from "isomorphic-ws";
|
|
507
755
|
var WSClient = class {
|
|
508
756
|
constructor(config) {
|
|
509
|
-
this.reconnectAttempts = 0;
|
|
510
757
|
this.messageHandlers = /* @__PURE__ */ new Set();
|
|
511
758
|
this.errorHandlers = /* @__PURE__ */ new Set();
|
|
512
759
|
this.closeHandlers = /* @__PURE__ */ new Set();
|
|
513
|
-
this.
|
|
760
|
+
this.openHandlers = /* @__PURE__ */ new Set();
|
|
761
|
+
this.isClosed = false;
|
|
514
762
|
this.url = config.wsURL;
|
|
515
763
|
this.timeout = config.timeout ?? 3e4;
|
|
516
|
-
this.maxReconnectAttempts = config.maxReconnectAttempts ?? 5;
|
|
517
|
-
this.reconnectDelayMs = config.reconnectDelayMs ?? 1e3;
|
|
518
|
-
this.heartbeatIntervalMs = config.heartbeatIntervalMs ?? 3e4;
|
|
519
|
-
this.authMode = config.authMode ?? "header";
|
|
520
764
|
this.authToken = config.authToken;
|
|
521
765
|
this.WebSocketClass = config.WebSocket ?? WebSocket;
|
|
522
766
|
}
|
|
767
|
+
/**
|
|
768
|
+
* Connect to WebSocket server
|
|
769
|
+
*/
|
|
523
770
|
connect() {
|
|
524
771
|
return new Promise((resolve, reject) => {
|
|
525
772
|
try {
|
|
526
773
|
const wsUrl = this.buildWSUrl();
|
|
527
774
|
this.ws = new this.WebSocketClass(wsUrl);
|
|
775
|
+
this.isClosed = false;
|
|
528
776
|
const timeout = setTimeout(() => {
|
|
529
777
|
this.ws?.close();
|
|
530
|
-
reject(
|
|
778
|
+
reject(
|
|
779
|
+
new SDKError("WebSocket connection timeout", 408, "WS_TIMEOUT")
|
|
780
|
+
);
|
|
531
781
|
}, this.timeout);
|
|
532
782
|
this.ws.addEventListener("open", () => {
|
|
533
783
|
clearTimeout(timeout);
|
|
534
|
-
|
|
535
|
-
this.
|
|
784
|
+
console.log("[WSClient] Connected to", this.url);
|
|
785
|
+
this.openHandlers.forEach((handler) => handler());
|
|
536
786
|
resolve();
|
|
537
787
|
});
|
|
538
788
|
this.ws.addEventListener("message", (event) => {
|
|
@@ -540,29 +790,24 @@ var WSClient = class {
|
|
|
540
790
|
this.messageHandlers.forEach((handler) => handler(msgEvent.data));
|
|
541
791
|
});
|
|
542
792
|
this.ws.addEventListener("error", (event) => {
|
|
793
|
+
console.error("[WSClient] WebSocket error:", event);
|
|
543
794
|
clearTimeout(timeout);
|
|
544
|
-
const error = new SDKError(
|
|
545
|
-
"WebSocket error",
|
|
546
|
-
500,
|
|
547
|
-
"WS_ERROR",
|
|
548
|
-
event
|
|
549
|
-
);
|
|
795
|
+
const error = new SDKError("WebSocket error", 500, "WS_ERROR", event);
|
|
550
796
|
this.errorHandlers.forEach((handler) => handler(error));
|
|
551
797
|
});
|
|
552
798
|
this.ws.addEventListener("close", () => {
|
|
553
799
|
clearTimeout(timeout);
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
this.attemptReconnect();
|
|
557
|
-
} else {
|
|
558
|
-
this.closeHandlers.forEach((handler) => handler());
|
|
559
|
-
}
|
|
800
|
+
console.log("[WSClient] Connection closed");
|
|
801
|
+
this.closeHandlers.forEach((handler) => handler());
|
|
560
802
|
});
|
|
561
803
|
} catch (error) {
|
|
562
804
|
reject(error);
|
|
563
805
|
}
|
|
564
806
|
});
|
|
565
807
|
}
|
|
808
|
+
/**
|
|
809
|
+
* Build WebSocket URL with auth token
|
|
810
|
+
*/
|
|
566
811
|
buildWSUrl() {
|
|
567
812
|
let url = this.url;
|
|
568
813
|
if (this.authToken) {
|
|
@@ -572,85 +817,152 @@ var WSClient = class {
|
|
|
572
817
|
}
|
|
573
818
|
return url;
|
|
574
819
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
this.ws.send(JSON.stringify({ type: "ping" }));
|
|
579
|
-
}
|
|
580
|
-
}, this.heartbeatIntervalMs);
|
|
581
|
-
}
|
|
582
|
-
stopHeartbeat() {
|
|
583
|
-
if (this.heartbeatInterval) {
|
|
584
|
-
clearInterval(this.heartbeatInterval);
|
|
585
|
-
this.heartbeatInterval = void 0;
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
attemptReconnect() {
|
|
589
|
-
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
590
|
-
this.reconnectAttempts++;
|
|
591
|
-
const delayMs = this.reconnectDelayMs * this.reconnectAttempts;
|
|
592
|
-
setTimeout(() => {
|
|
593
|
-
this.connect().catch((error) => {
|
|
594
|
-
this.errorHandlers.forEach((handler) => handler(error));
|
|
595
|
-
});
|
|
596
|
-
}, delayMs);
|
|
597
|
-
} else {
|
|
598
|
-
this.closeHandlers.forEach((handler) => handler());
|
|
599
|
-
}
|
|
600
|
-
}
|
|
820
|
+
/**
|
|
821
|
+
* Register message handler
|
|
822
|
+
*/
|
|
601
823
|
onMessage(handler) {
|
|
602
824
|
this.messageHandlers.add(handler);
|
|
603
825
|
return () => this.messageHandlers.delete(handler);
|
|
604
826
|
}
|
|
827
|
+
/**
|
|
828
|
+
* Unregister message handler
|
|
829
|
+
*/
|
|
830
|
+
offMessage(handler) {
|
|
831
|
+
this.messageHandlers.delete(handler);
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Register error handler
|
|
835
|
+
*/
|
|
605
836
|
onError(handler) {
|
|
606
837
|
this.errorHandlers.add(handler);
|
|
607
838
|
return () => this.errorHandlers.delete(handler);
|
|
608
839
|
}
|
|
840
|
+
/**
|
|
841
|
+
* Unregister error handler
|
|
842
|
+
*/
|
|
843
|
+
offError(handler) {
|
|
844
|
+
this.errorHandlers.delete(handler);
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Register close handler
|
|
848
|
+
*/
|
|
609
849
|
onClose(handler) {
|
|
610
850
|
this.closeHandlers.add(handler);
|
|
611
851
|
return () => this.closeHandlers.delete(handler);
|
|
612
852
|
}
|
|
853
|
+
/**
|
|
854
|
+
* Unregister close handler
|
|
855
|
+
*/
|
|
856
|
+
offClose(handler) {
|
|
857
|
+
this.closeHandlers.delete(handler);
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Register open handler
|
|
861
|
+
*/
|
|
862
|
+
onOpen(handler) {
|
|
863
|
+
this.openHandlers.add(handler);
|
|
864
|
+
return () => this.openHandlers.delete(handler);
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Send data through WebSocket
|
|
868
|
+
*/
|
|
613
869
|
send(data) {
|
|
614
870
|
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
615
|
-
throw new SDKError(
|
|
616
|
-
"WebSocket is not connected",
|
|
617
|
-
500,
|
|
618
|
-
"WS_NOT_CONNECTED"
|
|
619
|
-
);
|
|
871
|
+
throw new SDKError("WebSocket is not connected", 500, "WS_NOT_CONNECTED");
|
|
620
872
|
}
|
|
621
873
|
this.ws.send(data);
|
|
622
874
|
}
|
|
875
|
+
/**
|
|
876
|
+
* Close WebSocket connection
|
|
877
|
+
*/
|
|
623
878
|
close() {
|
|
624
|
-
this.
|
|
625
|
-
|
|
879
|
+
if (this.isClosed) {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
this.isClosed = true;
|
|
626
883
|
this.ws?.close();
|
|
627
884
|
}
|
|
885
|
+
/**
|
|
886
|
+
* Check if WebSocket is connected
|
|
887
|
+
*/
|
|
628
888
|
isConnected() {
|
|
629
|
-
return this.ws?.readyState === WebSocket.OPEN;
|
|
889
|
+
return !this.isClosed && this.ws?.readyState === WebSocket.OPEN;
|
|
630
890
|
}
|
|
891
|
+
/**
|
|
892
|
+
* Update auth token
|
|
893
|
+
*/
|
|
631
894
|
setAuthToken(token) {
|
|
632
895
|
this.authToken = token;
|
|
633
896
|
}
|
|
634
897
|
};
|
|
635
898
|
|
|
636
899
|
// src/pubsub/client.ts
|
|
900
|
+
function base64Encode(str) {
|
|
901
|
+
if (typeof Buffer !== "undefined") {
|
|
902
|
+
return Buffer.from(str).toString("base64");
|
|
903
|
+
} else if (typeof btoa !== "undefined") {
|
|
904
|
+
return btoa(
|
|
905
|
+
encodeURIComponent(str).replace(
|
|
906
|
+
/%([0-9A-F]{2})/g,
|
|
907
|
+
(match, p1) => String.fromCharCode(parseInt(p1, 16))
|
|
908
|
+
)
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
throw new Error("No base64 encoding method available");
|
|
912
|
+
}
|
|
913
|
+
function base64EncodeBytes(bytes) {
|
|
914
|
+
if (typeof Buffer !== "undefined") {
|
|
915
|
+
return Buffer.from(bytes).toString("base64");
|
|
916
|
+
} else if (typeof btoa !== "undefined") {
|
|
917
|
+
let binary = "";
|
|
918
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
919
|
+
binary += String.fromCharCode(bytes[i]);
|
|
920
|
+
}
|
|
921
|
+
return btoa(binary);
|
|
922
|
+
}
|
|
923
|
+
throw new Error("No base64 encoding method available");
|
|
924
|
+
}
|
|
925
|
+
function base64Decode(b64) {
|
|
926
|
+
if (typeof Buffer !== "undefined") {
|
|
927
|
+
return Buffer.from(b64, "base64").toString("utf-8");
|
|
928
|
+
} else if (typeof atob !== "undefined") {
|
|
929
|
+
const binary = atob(b64);
|
|
930
|
+
const bytes = new Uint8Array(binary.length);
|
|
931
|
+
for (let i = 0; i < binary.length; i++) {
|
|
932
|
+
bytes[i] = binary.charCodeAt(i);
|
|
933
|
+
}
|
|
934
|
+
return new TextDecoder().decode(bytes);
|
|
935
|
+
}
|
|
936
|
+
throw new Error("No base64 decoding method available");
|
|
937
|
+
}
|
|
637
938
|
var PubSubClient = class {
|
|
638
939
|
constructor(httpClient, wsConfig = {}) {
|
|
639
940
|
this.httpClient = httpClient;
|
|
640
941
|
this.wsConfig = wsConfig;
|
|
641
942
|
}
|
|
642
943
|
/**
|
|
643
|
-
* Publish a message to a topic
|
|
944
|
+
* Publish a message to a topic via HTTP
|
|
644
945
|
*/
|
|
645
946
|
async publish(topic, data) {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
947
|
+
let dataBase64;
|
|
948
|
+
if (typeof data === "string") {
|
|
949
|
+
dataBase64 = base64Encode(data);
|
|
950
|
+
} else {
|
|
951
|
+
dataBase64 = base64EncodeBytes(data);
|
|
952
|
+
}
|
|
953
|
+
await this.httpClient.post(
|
|
954
|
+
"/v1/pubsub/publish",
|
|
955
|
+
{
|
|
956
|
+
topic,
|
|
957
|
+
data_base64: dataBase64
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
timeout: 3e4
|
|
961
|
+
}
|
|
962
|
+
);
|
|
651
963
|
}
|
|
652
964
|
/**
|
|
653
|
-
* List active topics in the current namespace
|
|
965
|
+
* List active topics in the current namespace
|
|
654
966
|
*/
|
|
655
967
|
async topics() {
|
|
656
968
|
const response = await this.httpClient.get(
|
|
@@ -659,18 +971,20 @@ var PubSubClient = class {
|
|
|
659
971
|
return response.topics || [];
|
|
660
972
|
}
|
|
661
973
|
/**
|
|
662
|
-
* Subscribe to a topic via WebSocket
|
|
663
|
-
*
|
|
974
|
+
* Subscribe to a topic via WebSocket
|
|
975
|
+
* Creates one WebSocket connection per topic
|
|
664
976
|
*/
|
|
665
977
|
async subscribe(topic, handlers = {}) {
|
|
666
|
-
const wsUrl = new URL(this.wsConfig.wsURL || "ws://
|
|
978
|
+
const wsUrl = new URL(this.wsConfig.wsURL || "ws://127.0.0.1:6001");
|
|
667
979
|
wsUrl.pathname = "/v1/pubsub/ws";
|
|
668
980
|
wsUrl.searchParams.set("topic", topic);
|
|
981
|
+
const authToken = this.httpClient.getApiKey() ?? this.httpClient.getToken();
|
|
669
982
|
const wsClient = new WSClient({
|
|
670
983
|
...this.wsConfig,
|
|
671
984
|
wsURL: wsUrl.toString(),
|
|
672
|
-
authToken
|
|
985
|
+
authToken
|
|
673
986
|
});
|
|
987
|
+
await wsClient.connect();
|
|
674
988
|
const subscription = new Subscription(wsClient, topic);
|
|
675
989
|
if (handlers.onMessage) {
|
|
676
990
|
subscription.onMessage(handlers.onMessage);
|
|
@@ -681,7 +995,6 @@ var PubSubClient = class {
|
|
|
681
995
|
if (handlers.onClose) {
|
|
682
996
|
subscription.onClose(handlers.onClose);
|
|
683
997
|
}
|
|
684
|
-
await wsClient.connect();
|
|
685
998
|
return subscription;
|
|
686
999
|
}
|
|
687
1000
|
};
|
|
@@ -690,46 +1003,105 @@ var Subscription = class {
|
|
|
690
1003
|
this.messageHandlers = /* @__PURE__ */ new Set();
|
|
691
1004
|
this.errorHandlers = /* @__PURE__ */ new Set();
|
|
692
1005
|
this.closeHandlers = /* @__PURE__ */ new Set();
|
|
1006
|
+
this.isClosed = false;
|
|
1007
|
+
this.wsMessageHandler = null;
|
|
1008
|
+
this.wsErrorHandler = null;
|
|
1009
|
+
this.wsCloseHandler = null;
|
|
693
1010
|
this.wsClient = wsClient;
|
|
694
1011
|
this.topic = topic;
|
|
695
|
-
this.
|
|
1012
|
+
this.wsMessageHandler = (data) => {
|
|
696
1013
|
try {
|
|
1014
|
+
const envelope = JSON.parse(data);
|
|
1015
|
+
if (!envelope || typeof envelope !== "object") {
|
|
1016
|
+
throw new Error("Invalid envelope: not an object");
|
|
1017
|
+
}
|
|
1018
|
+
if (!envelope.data || typeof envelope.data !== "string") {
|
|
1019
|
+
throw new Error("Invalid envelope: missing or invalid data field");
|
|
1020
|
+
}
|
|
1021
|
+
if (!envelope.topic || typeof envelope.topic !== "string") {
|
|
1022
|
+
throw new Error("Invalid envelope: missing or invalid topic field");
|
|
1023
|
+
}
|
|
1024
|
+
if (typeof envelope.timestamp !== "number") {
|
|
1025
|
+
throw new Error(
|
|
1026
|
+
"Invalid envelope: missing or invalid timestamp field"
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
const messageData = base64Decode(envelope.data);
|
|
697
1030
|
const message = {
|
|
698
|
-
topic:
|
|
699
|
-
data,
|
|
700
|
-
timestamp:
|
|
1031
|
+
topic: envelope.topic,
|
|
1032
|
+
data: messageData,
|
|
1033
|
+
timestamp: envelope.timestamp
|
|
701
1034
|
};
|
|
1035
|
+
console.log("[Subscription] Received message on topic:", this.topic);
|
|
702
1036
|
this.messageHandlers.forEach((handler) => handler(message));
|
|
703
1037
|
} catch (error) {
|
|
1038
|
+
console.error("[Subscription] Error processing message:", error);
|
|
704
1039
|
this.errorHandlers.forEach(
|
|
705
1040
|
(handler) => handler(error instanceof Error ? error : new Error(String(error)))
|
|
706
1041
|
);
|
|
707
1042
|
}
|
|
708
|
-
}
|
|
709
|
-
this.wsClient.
|
|
1043
|
+
};
|
|
1044
|
+
this.wsClient.onMessage(this.wsMessageHandler);
|
|
1045
|
+
this.wsErrorHandler = (error) => {
|
|
710
1046
|
this.errorHandlers.forEach((handler) => handler(error));
|
|
711
|
-
}
|
|
712
|
-
this.wsClient.
|
|
1047
|
+
};
|
|
1048
|
+
this.wsClient.onError(this.wsErrorHandler);
|
|
1049
|
+
this.wsCloseHandler = () => {
|
|
713
1050
|
this.closeHandlers.forEach((handler) => handler());
|
|
714
|
-
}
|
|
1051
|
+
};
|
|
1052
|
+
this.wsClient.onClose(this.wsCloseHandler);
|
|
715
1053
|
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Register message handler
|
|
1056
|
+
*/
|
|
716
1057
|
onMessage(handler) {
|
|
717
1058
|
this.messageHandlers.add(handler);
|
|
718
1059
|
return () => this.messageHandlers.delete(handler);
|
|
719
1060
|
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Register error handler
|
|
1063
|
+
*/
|
|
720
1064
|
onError(handler) {
|
|
721
1065
|
this.errorHandlers.add(handler);
|
|
722
1066
|
return () => this.errorHandlers.delete(handler);
|
|
723
1067
|
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Register close handler
|
|
1070
|
+
*/
|
|
724
1071
|
onClose(handler) {
|
|
725
1072
|
this.closeHandlers.add(handler);
|
|
726
1073
|
return () => this.closeHandlers.delete(handler);
|
|
727
1074
|
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Close subscription and underlying WebSocket
|
|
1077
|
+
*/
|
|
728
1078
|
close() {
|
|
1079
|
+
if (this.isClosed) {
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
this.isClosed = true;
|
|
1083
|
+
if (this.wsMessageHandler) {
|
|
1084
|
+
this.wsClient.offMessage(this.wsMessageHandler);
|
|
1085
|
+
this.wsMessageHandler = null;
|
|
1086
|
+
}
|
|
1087
|
+
if (this.wsErrorHandler) {
|
|
1088
|
+
this.wsClient.offError(this.wsErrorHandler);
|
|
1089
|
+
this.wsErrorHandler = null;
|
|
1090
|
+
}
|
|
1091
|
+
if (this.wsCloseHandler) {
|
|
1092
|
+
this.wsClient.offClose(this.wsCloseHandler);
|
|
1093
|
+
this.wsCloseHandler = null;
|
|
1094
|
+
}
|
|
1095
|
+
this.messageHandlers.clear();
|
|
1096
|
+
this.errorHandlers.clear();
|
|
1097
|
+
this.closeHandlers.clear();
|
|
729
1098
|
this.wsClient.close();
|
|
730
1099
|
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Check if subscription is active
|
|
1102
|
+
*/
|
|
731
1103
|
isConnected() {
|
|
732
|
-
return this.wsClient.isConnected();
|
|
1104
|
+
return !this.isClosed && this.wsClient.isConnected();
|
|
733
1105
|
}
|
|
734
1106
|
};
|
|
735
1107
|
|
|
@@ -753,7 +1125,9 @@ var NetworkClient = class {
|
|
|
753
1125
|
* Get network status.
|
|
754
1126
|
*/
|
|
755
1127
|
async status() {
|
|
756
|
-
const response = await this.httpClient.get(
|
|
1128
|
+
const response = await this.httpClient.get(
|
|
1129
|
+
"/v1/network/status"
|
|
1130
|
+
);
|
|
757
1131
|
return response;
|
|
758
1132
|
}
|
|
759
1133
|
/**
|
|
@@ -777,6 +1151,325 @@ var NetworkClient = class {
|
|
|
777
1151
|
async disconnect(peerId) {
|
|
778
1152
|
await this.httpClient.post("/v1/network/disconnect", { peer_id: peerId });
|
|
779
1153
|
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Proxy an HTTP request through the Anyone network.
|
|
1156
|
+
* Requires authentication (API key or JWT).
|
|
1157
|
+
*
|
|
1158
|
+
* @param request - The proxy request configuration
|
|
1159
|
+
* @returns The proxied response
|
|
1160
|
+
* @throws {SDKError} If the Anyone proxy is not available or the request fails
|
|
1161
|
+
*
|
|
1162
|
+
* @example
|
|
1163
|
+
* ```ts
|
|
1164
|
+
* const response = await client.network.proxyAnon({
|
|
1165
|
+
* url: 'https://api.example.com/data',
|
|
1166
|
+
* method: 'GET',
|
|
1167
|
+
* headers: {
|
|
1168
|
+
* 'Accept': 'application/json'
|
|
1169
|
+
* }
|
|
1170
|
+
* });
|
|
1171
|
+
*
|
|
1172
|
+
* console.log(response.status_code); // 200
|
|
1173
|
+
* console.log(response.body); // Response data
|
|
1174
|
+
* ```
|
|
1175
|
+
*/
|
|
1176
|
+
async proxyAnon(request) {
|
|
1177
|
+
const response = await this.httpClient.post(
|
|
1178
|
+
"/v1/proxy/anon",
|
|
1179
|
+
request
|
|
1180
|
+
);
|
|
1181
|
+
if (response.error) {
|
|
1182
|
+
throw new Error(`Proxy request failed: ${response.error}`);
|
|
1183
|
+
}
|
|
1184
|
+
return response;
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
|
|
1188
|
+
// src/cache/client.ts
|
|
1189
|
+
var CacheClient = class {
|
|
1190
|
+
constructor(httpClient) {
|
|
1191
|
+
this.httpClient = httpClient;
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Check cache service health
|
|
1195
|
+
*/
|
|
1196
|
+
async health() {
|
|
1197
|
+
return this.httpClient.get("/v1/cache/health");
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Get a value from cache
|
|
1201
|
+
* Returns null if the key is not found (cache miss/expired), which is normal behavior
|
|
1202
|
+
*/
|
|
1203
|
+
async get(dmap, key) {
|
|
1204
|
+
try {
|
|
1205
|
+
return await this.httpClient.post("/v1/cache/get", {
|
|
1206
|
+
dmap,
|
|
1207
|
+
key
|
|
1208
|
+
});
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
if (error instanceof SDKError && (error.httpStatus === 404 || error.httpStatus === 500 && error.message?.toLowerCase().includes("key not found"))) {
|
|
1211
|
+
return null;
|
|
1212
|
+
}
|
|
1213
|
+
throw error;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Put a value into cache
|
|
1218
|
+
*/
|
|
1219
|
+
async put(dmap, key, value, ttl) {
|
|
1220
|
+
return this.httpClient.post("/v1/cache/put", {
|
|
1221
|
+
dmap,
|
|
1222
|
+
key,
|
|
1223
|
+
value,
|
|
1224
|
+
ttl
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Delete a value from cache
|
|
1229
|
+
*/
|
|
1230
|
+
async delete(dmap, key) {
|
|
1231
|
+
return this.httpClient.post("/v1/cache/delete", {
|
|
1232
|
+
dmap,
|
|
1233
|
+
key
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Get multiple values from cache in a single request
|
|
1238
|
+
* Returns a map of key -> value (or null if not found)
|
|
1239
|
+
* Gracefully handles 404 errors (endpoint not implemented) by returning empty results
|
|
1240
|
+
*/
|
|
1241
|
+
async multiGet(dmap, keys) {
|
|
1242
|
+
try {
|
|
1243
|
+
if (keys.length === 0) {
|
|
1244
|
+
return /* @__PURE__ */ new Map();
|
|
1245
|
+
}
|
|
1246
|
+
const response = await this.httpClient.post(
|
|
1247
|
+
"/v1/cache/mget",
|
|
1248
|
+
{
|
|
1249
|
+
dmap,
|
|
1250
|
+
keys
|
|
1251
|
+
}
|
|
1252
|
+
);
|
|
1253
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
1254
|
+
keys.forEach((key) => {
|
|
1255
|
+
resultMap.set(key, null);
|
|
1256
|
+
});
|
|
1257
|
+
if (response.results) {
|
|
1258
|
+
response.results.forEach(({ key, value }) => {
|
|
1259
|
+
resultMap.set(key, value);
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
return resultMap;
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
if (error instanceof SDKError && error.httpStatus === 404) {
|
|
1265
|
+
const resultMap2 = /* @__PURE__ */ new Map();
|
|
1266
|
+
keys.forEach((key) => {
|
|
1267
|
+
resultMap2.set(key, null);
|
|
1268
|
+
});
|
|
1269
|
+
return resultMap2;
|
|
1270
|
+
}
|
|
1271
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
1272
|
+
keys.forEach((key) => {
|
|
1273
|
+
resultMap.set(key, null);
|
|
1274
|
+
});
|
|
1275
|
+
console.error(`[CacheClient] Error in multiGet for ${dmap}:`, error);
|
|
1276
|
+
return resultMap;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Scan keys in a distributed map, optionally matching a regex pattern
|
|
1281
|
+
*/
|
|
1282
|
+
async scan(dmap, match) {
|
|
1283
|
+
return this.httpClient.post("/v1/cache/scan", {
|
|
1284
|
+
dmap,
|
|
1285
|
+
match
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
// src/storage/client.ts
|
|
1291
|
+
var StorageClient = class {
|
|
1292
|
+
constructor(httpClient) {
|
|
1293
|
+
this.httpClient = httpClient;
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Upload content to IPFS and optionally pin it.
|
|
1297
|
+
* Supports both File objects (browser) and Buffer/ReadableStream (Node.js).
|
|
1298
|
+
*
|
|
1299
|
+
* @param file - File to upload (File, Blob, or Buffer)
|
|
1300
|
+
* @param name - Optional filename
|
|
1301
|
+
* @param options - Optional upload options
|
|
1302
|
+
* @param options.pin - Whether to pin the content (default: true). Pinning happens asynchronously on the backend.
|
|
1303
|
+
* @returns Upload result with CID
|
|
1304
|
+
*
|
|
1305
|
+
* @example
|
|
1306
|
+
* ```ts
|
|
1307
|
+
* // Browser
|
|
1308
|
+
* const fileInput = document.querySelector('input[type="file"]');
|
|
1309
|
+
* const file = fileInput.files[0];
|
|
1310
|
+
* const result = await client.storage.upload(file, file.name);
|
|
1311
|
+
* console.log(result.cid);
|
|
1312
|
+
*
|
|
1313
|
+
* // Node.js
|
|
1314
|
+
* const fs = require('fs');
|
|
1315
|
+
* const fileBuffer = fs.readFileSync('image.jpg');
|
|
1316
|
+
* const result = await client.storage.upload(fileBuffer, 'image.jpg', { pin: true });
|
|
1317
|
+
* ```
|
|
1318
|
+
*/
|
|
1319
|
+
async upload(file, name, options) {
|
|
1320
|
+
const formData = new FormData();
|
|
1321
|
+
if (file instanceof File) {
|
|
1322
|
+
formData.append("file", file);
|
|
1323
|
+
} else if (file instanceof Blob) {
|
|
1324
|
+
formData.append("file", file, name);
|
|
1325
|
+
} else if (file instanceof ArrayBuffer) {
|
|
1326
|
+
const blob = new Blob([file]);
|
|
1327
|
+
formData.append("file", blob, name);
|
|
1328
|
+
} else if (file instanceof Uint8Array) {
|
|
1329
|
+
const buffer = file.buffer.slice(
|
|
1330
|
+
file.byteOffset,
|
|
1331
|
+
file.byteOffset + file.byteLength
|
|
1332
|
+
);
|
|
1333
|
+
const blob = new Blob([buffer], { type: "application/octet-stream" });
|
|
1334
|
+
formData.append("file", blob, name);
|
|
1335
|
+
} else if (file instanceof ReadableStream) {
|
|
1336
|
+
const chunks = [];
|
|
1337
|
+
const reader = file.getReader();
|
|
1338
|
+
while (true) {
|
|
1339
|
+
const { done, value } = await reader.read();
|
|
1340
|
+
if (done) break;
|
|
1341
|
+
const buffer = value.buffer.slice(
|
|
1342
|
+
value.byteOffset,
|
|
1343
|
+
value.byteOffset + value.byteLength
|
|
1344
|
+
);
|
|
1345
|
+
chunks.push(buffer);
|
|
1346
|
+
}
|
|
1347
|
+
const blob = new Blob(chunks);
|
|
1348
|
+
formData.append("file", blob, name);
|
|
1349
|
+
} else {
|
|
1350
|
+
throw new Error(
|
|
1351
|
+
"Unsupported file type. Use File, Blob, ArrayBuffer, Uint8Array, or ReadableStream."
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
const shouldPin = options?.pin !== false;
|
|
1355
|
+
formData.append("pin", shouldPin ? "true" : "false");
|
|
1356
|
+
return this.httpClient.uploadFile(
|
|
1357
|
+
"/v1/storage/upload",
|
|
1358
|
+
formData,
|
|
1359
|
+
{ timeout: 3e5 }
|
|
1360
|
+
// 5 minute timeout for large files
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Pin an existing CID
|
|
1365
|
+
*
|
|
1366
|
+
* @param cid - Content ID to pin
|
|
1367
|
+
* @param name - Optional name for the pin
|
|
1368
|
+
* @returns Pin result
|
|
1369
|
+
*/
|
|
1370
|
+
async pin(cid, name) {
|
|
1371
|
+
return this.httpClient.post("/v1/storage/pin", {
|
|
1372
|
+
cid,
|
|
1373
|
+
name
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Get the pin status for a CID
|
|
1378
|
+
*
|
|
1379
|
+
* @param cid - Content ID to check
|
|
1380
|
+
* @returns Pin status information
|
|
1381
|
+
*/
|
|
1382
|
+
async status(cid) {
|
|
1383
|
+
return this.httpClient.get(`/v1/storage/status/${cid}`);
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Retrieve content from IPFS by CID
|
|
1387
|
+
*
|
|
1388
|
+
* @param cid - Content ID to retrieve
|
|
1389
|
+
* @returns ReadableStream of the content
|
|
1390
|
+
*
|
|
1391
|
+
* @example
|
|
1392
|
+
* ```ts
|
|
1393
|
+
* const stream = await client.storage.get(cid);
|
|
1394
|
+
* const reader = stream.getReader();
|
|
1395
|
+
* while (true) {
|
|
1396
|
+
* const { done, value } = await reader.read();
|
|
1397
|
+
* if (done) break;
|
|
1398
|
+
* // Process chunk
|
|
1399
|
+
* }
|
|
1400
|
+
* ```
|
|
1401
|
+
*/
|
|
1402
|
+
async get(cid) {
|
|
1403
|
+
const maxAttempts = 8;
|
|
1404
|
+
let lastError = null;
|
|
1405
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1406
|
+
try {
|
|
1407
|
+
const response = await this.httpClient.getBinary(
|
|
1408
|
+
`/v1/storage/get/${cid}`
|
|
1409
|
+
);
|
|
1410
|
+
if (!response.body) {
|
|
1411
|
+
throw new Error("Response body is null");
|
|
1412
|
+
}
|
|
1413
|
+
return response.body;
|
|
1414
|
+
} catch (error) {
|
|
1415
|
+
lastError = error;
|
|
1416
|
+
const isNotFound = error?.httpStatus === 404 || error?.message?.includes("not found") || error?.message?.includes("404");
|
|
1417
|
+
if (!isNotFound || attempt === maxAttempts) {
|
|
1418
|
+
throw error;
|
|
1419
|
+
}
|
|
1420
|
+
const backoffMs = attempt * 2500;
|
|
1421
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
throw lastError || new Error("Failed to retrieve content");
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Retrieve content from IPFS by CID and return the full Response object
|
|
1428
|
+
* Useful when you need access to response headers (e.g., content-length)
|
|
1429
|
+
*
|
|
1430
|
+
* @param cid - Content ID to retrieve
|
|
1431
|
+
* @returns Response object with body stream and headers
|
|
1432
|
+
*
|
|
1433
|
+
* @example
|
|
1434
|
+
* ```ts
|
|
1435
|
+
* const response = await client.storage.getBinary(cid);
|
|
1436
|
+
* const contentLength = response.headers.get('content-length');
|
|
1437
|
+
* const reader = response.body.getReader();
|
|
1438
|
+
* // ... read stream
|
|
1439
|
+
* ```
|
|
1440
|
+
*/
|
|
1441
|
+
async getBinary(cid) {
|
|
1442
|
+
const maxAttempts = 8;
|
|
1443
|
+
let lastError = null;
|
|
1444
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1445
|
+
try {
|
|
1446
|
+
const response = await this.httpClient.getBinary(
|
|
1447
|
+
`/v1/storage/get/${cid}`
|
|
1448
|
+
);
|
|
1449
|
+
if (!response) {
|
|
1450
|
+
throw new Error("Response is null");
|
|
1451
|
+
}
|
|
1452
|
+
return response;
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
lastError = error;
|
|
1455
|
+
const isNotFound = error?.httpStatus === 404 || error?.message?.includes("not found") || error?.message?.includes("404");
|
|
1456
|
+
if (!isNotFound || attempt === maxAttempts) {
|
|
1457
|
+
throw error;
|
|
1458
|
+
}
|
|
1459
|
+
const backoffMs = attempt * 2500;
|
|
1460
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
throw lastError || new Error("Failed to retrieve content");
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Unpin a CID
|
|
1467
|
+
*
|
|
1468
|
+
* @param cid - Content ID to unpin
|
|
1469
|
+
*/
|
|
1470
|
+
async unpin(cid) {
|
|
1471
|
+
await this.httpClient.delete(`/v1/storage/unpin/${cid}`);
|
|
1472
|
+
}
|
|
780
1473
|
};
|
|
781
1474
|
|
|
782
1475
|
// src/index.ts
|
|
@@ -801,15 +1494,20 @@ function createClient(config) {
|
|
|
801
1494
|
wsURL
|
|
802
1495
|
});
|
|
803
1496
|
const network = new NetworkClient(httpClient);
|
|
1497
|
+
const cache = new CacheClient(httpClient);
|
|
1498
|
+
const storage = new StorageClient(httpClient);
|
|
804
1499
|
return {
|
|
805
1500
|
auth,
|
|
806
1501
|
db,
|
|
807
1502
|
pubsub,
|
|
808
|
-
network
|
|
1503
|
+
network,
|
|
1504
|
+
cache,
|
|
1505
|
+
storage
|
|
809
1506
|
};
|
|
810
1507
|
}
|
|
811
1508
|
export {
|
|
812
1509
|
AuthClient,
|
|
1510
|
+
CacheClient,
|
|
813
1511
|
DBClient,
|
|
814
1512
|
HttpClient,
|
|
815
1513
|
LocalStorageAdapter,
|
|
@@ -819,6 +1517,7 @@ export {
|
|
|
819
1517
|
QueryBuilder,
|
|
820
1518
|
Repository,
|
|
821
1519
|
SDKError,
|
|
1520
|
+
StorageClient,
|
|
822
1521
|
Subscription,
|
|
823
1522
|
WSClient,
|
|
824
1523
|
createClient
|