@debros/network-ts-sdk 0.2.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/dist/index.js CHANGED
@@ -34,14 +34,6 @@ var HttpClient = class {
34
34
  }
35
35
  setApiKey(apiKey) {
36
36
  this.apiKey = apiKey;
37
- if (typeof console !== "undefined") {
38
- console.log(
39
- "[HttpClient] API key set:",
40
- !!apiKey,
41
- "JWT still present:",
42
- !!this.jwt
43
- );
44
- }
45
37
  }
46
38
  setJwt(jwt) {
47
39
  this.jwt = jwt;
@@ -59,12 +51,21 @@ var HttpClient = class {
59
51
  const isDbOperation = path.includes("/v1/rqlite/");
60
52
  const isPubSubOperation = path.includes("/v1/pubsub/");
61
53
  const isProxyOperation = path.includes("/v1/proxy/");
62
- if (isDbOperation || isPubSubOperation || isProxyOperation) {
54
+ const isCacheOperation = path.includes("/v1/cache/");
55
+ const isAuthOperation = path.includes("/v1/auth/");
56
+ if (isDbOperation || isPubSubOperation || isProxyOperation || isCacheOperation) {
63
57
  if (this.apiKey) {
64
58
  headers["X-API-Key"] = this.apiKey;
65
59
  } else if (this.jwt) {
66
60
  headers["Authorization"] = `Bearer ${this.jwt}`;
67
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
+ }
68
69
  } else {
69
70
  if (this.jwt) {
70
71
  headers["Authorization"] = `Bearer ${this.jwt}`;
@@ -82,6 +83,7 @@ var HttpClient = class {
82
83
  return this.apiKey;
83
84
  }
84
85
  async request(method, path, options = {}) {
86
+ const startTime = performance.now();
85
87
  const url = new URL(this.baseURL + path);
86
88
  if (options.query) {
87
89
  Object.entries(options.query).forEach(([key, value]) => {
@@ -93,14 +95,6 @@ var HttpClient = class {
93
95
  ...this.getAuthHeaders(path),
94
96
  ...options.headers
95
97
  };
96
- if (typeof console !== "undefined" && (path.includes("/db/") || path.includes("/query") || path.includes("/auth/") || path.includes("/pubsub/") || path.includes("/proxy/"))) {
97
- console.log("[HttpClient] Request headers for", path, {
98
- hasAuth: !!headers["Authorization"],
99
- hasApiKey: !!headers["X-API-Key"],
100
- authPrefix: headers["Authorization"] ? headers["Authorization"].substring(0, 20) : "none",
101
- apiKeyPrefix: headers["X-API-Key"] ? headers["X-API-Key"].substring(0, 20) : "none"
102
- });
103
- }
104
98
  const controller = new AbortController();
105
99
  const requestTimeout = options.timeout ?? this.timeout;
106
100
  const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
@@ -113,12 +107,55 @@ var HttpClient = class {
113
107
  fetchOptions.body = JSON.stringify(options.body);
114
108
  }
115
109
  try {
116
- return await this.requestWithRetry(url.toString(), fetchOptions);
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;
117
154
  } finally {
118
155
  clearTimeout(timeoutId);
119
156
  }
120
157
  }
121
- async requestWithRetry(url, options, attempt = 0) {
158
+ async requestWithRetry(url, options, attempt = 0, startTime) {
122
159
  try {
123
160
  const response = await this.fetch(url, options);
124
161
  if (!response.ok) {
@@ -140,7 +177,7 @@ var HttpClient = class {
140
177
  await new Promise(
141
178
  (resolve) => setTimeout(resolve, this.retryDelayMs * (attempt + 1))
142
179
  );
143
- return this.requestWithRetry(url, options, attempt + 1);
180
+ return this.requestWithRetry(url, options, attempt + 1, startTime);
144
181
  }
145
182
  throw error;
146
183
  }
@@ -157,6 +194,90 @@ var HttpClient = class {
157
194
  async delete(path, options) {
158
195
  return this.request("DELETE", path, options);
159
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
+ }
160
281
  getToken() {
161
282
  return this.getAuthToken();
162
283
  }
@@ -522,7 +643,9 @@ var Repository = class {
522
643
  buildInsertSql(entity) {
523
644
  const columns = Object.keys(entity).filter((k) => entity[k] !== void 0);
524
645
  const placeholders = columns.map(() => "?").join(", ");
525
- return `INSERT INTO ${this.tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
646
+ return `INSERT INTO ${this.tableName} (${columns.join(
647
+ ", "
648
+ )}) VALUES (${placeholders})`;
526
649
  }
527
650
  buildInsertArgs(entity) {
528
651
  return Object.entries(entity).filter(([, v]) => v !== void 0).map(([, v]) => v);
@@ -1062,6 +1185,293 @@ var NetworkClient = class {
1062
1185
  }
1063
1186
  };
1064
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
+ }
1473
+ };
1474
+
1065
1475
  // src/index.ts
1066
1476
  function createClient(config) {
1067
1477
  const httpClient = new HttpClient({
@@ -1084,15 +1494,20 @@ function createClient(config) {
1084
1494
  wsURL
1085
1495
  });
1086
1496
  const network = new NetworkClient(httpClient);
1497
+ const cache = new CacheClient(httpClient);
1498
+ const storage = new StorageClient(httpClient);
1087
1499
  return {
1088
1500
  auth,
1089
1501
  db,
1090
1502
  pubsub,
1091
- network
1503
+ network,
1504
+ cache,
1505
+ storage
1092
1506
  };
1093
1507
  }
1094
1508
  export {
1095
1509
  AuthClient,
1510
+ CacheClient,
1096
1511
  DBClient,
1097
1512
  HttpClient,
1098
1513
  LocalStorageAdapter,
@@ -1102,6 +1517,7 @@ export {
1102
1517
  QueryBuilder,
1103
1518
  Repository,
1104
1519
  SDKError,
1520
+ StorageClient,
1105
1521
  Subscription,
1106
1522
  WSClient,
1107
1523
  createClient