@tinycloud/sdk-services 3.1.0-beta.6 → 3.1.0-beta.9
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/{BaseService-BhpVpA9b.d.cts → BaseService-B_UKhVht.d.ts} +74 -2
- package/dist/{BaseService-CuV-ZVPs.d.ts → BaseService-DWbPbov7.d.cts} +74 -2
- package/dist/encryption/index.cjs +93 -3
- package/dist/encryption/index.cjs.map +1 -1
- package/dist/encryption/index.d.cts +2 -2
- package/dist/encryption/index.d.ts +2 -2
- package/dist/encryption/index.js +93 -3
- package/dist/encryption/index.js.map +1 -1
- package/dist/index.cjs +389 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -42
- package/dist/index.d.ts +14 -42
- package/dist/index.js +389 -31
- package/dist/index.js.map +1 -1
- package/dist/internal/decrypt-transport-response-error.d.cts +1 -1
- package/dist/internal/decrypt-transport-response-error.d.ts +1 -1
- package/dist/kv/index.cjs +326 -16
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +192 -13
- package/dist/kv/index.d.ts +192 -13
- package/dist/kv/index.js +326 -16
- package/dist/kv/index.js.map +1 -1
- package/dist/sql/index.cjs +114 -9
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +6 -3
- package/dist/sql/index.d.ts +6 -3
- package/dist/sql/index.js +114 -9
- package/dist/sql/index.js.map +1 -1
- package/dist/{types-qwMmB7qB.d.cts → types-CVwwxqXF.d.cts} +8 -0
- package/dist/{types-qwMmB7qB.d.ts → types-CVwwxqXF.d.ts} +8 -0
- package/package.json +3 -3
package/dist/kv/index.js
CHANGED
|
@@ -5,6 +5,10 @@ var ErrorCodes = {
|
|
|
5
5
|
AUTH_EXPIRED: "AUTH_EXPIRED",
|
|
6
6
|
AUTH_REQUIRED: "AUTH_REQUIRED",
|
|
7
7
|
AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
|
|
8
|
+
/** HTTP 401: the delegation the invocation cites was revoked. */
|
|
9
|
+
AUTH_DELEGATION_REVOKED: "AUTH_DELEGATION_REVOKED",
|
|
10
|
+
/** HTTP 401: a delegation the cited one rests on was revoked. */
|
|
11
|
+
AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
|
|
8
12
|
NETWORK_ERROR: "NETWORK_ERROR",
|
|
9
13
|
TIMEOUT: "TIMEOUT",
|
|
10
14
|
ABORTED: "ABORTED",
|
|
@@ -16,6 +20,10 @@ var ErrorCodes = {
|
|
|
16
20
|
KV_PRECONDITION_FAILED: "KV_PRECONDITION_FAILED",
|
|
17
21
|
KV_CONFLICT: "KV_CONFLICT",
|
|
18
22
|
KV_RESPONSE_TOO_LARGE: "KV_RESPONSE_TOO_LARGE",
|
|
23
|
+
/** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */
|
|
24
|
+
KV_SYNC_RESET_REQUIRED: "KV_SYNC_RESET_REQUIRED",
|
|
25
|
+
/** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */
|
|
26
|
+
KV_RETENTION_GRANT_REFUSED: "KV_RETENTION_GRANT_REFUSED",
|
|
19
27
|
// SQL-specific errors
|
|
20
28
|
SQL_ERROR: "SQL_ERROR",
|
|
21
29
|
SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
|
|
@@ -163,11 +171,31 @@ function authRequiredError(service) {
|
|
|
163
171
|
service
|
|
164
172
|
};
|
|
165
173
|
}
|
|
166
|
-
|
|
174
|
+
var RequestTimeoutError = class extends Error {
|
|
175
|
+
constructor(timeoutMs) {
|
|
176
|
+
super(`Request timed out after ${timeoutMs}ms.`);
|
|
177
|
+
this.name = "TimeoutError";
|
|
178
|
+
this.timeoutMs = timeoutMs;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
function isRequestTimeoutError(error) {
|
|
182
|
+
return error instanceof RequestTimeoutError || error instanceof Error && error.name === "TimeoutError" && typeof error.timeoutMs === "number";
|
|
183
|
+
}
|
|
184
|
+
function timeoutError(service, timeoutMs, cause) {
|
|
185
|
+
if (timeoutMs === void 0) {
|
|
186
|
+
return {
|
|
187
|
+
code: ErrorCodes.TIMEOUT,
|
|
188
|
+
message: "Request timed out.",
|
|
189
|
+
service,
|
|
190
|
+
...cause === void 0 ? {} : { cause }
|
|
191
|
+
};
|
|
192
|
+
}
|
|
167
193
|
return {
|
|
168
194
|
code: ErrorCodes.TIMEOUT,
|
|
169
|
-
message:
|
|
170
|
-
service
|
|
195
|
+
message: `Request timed out after ${timeoutMs}ms.`,
|
|
196
|
+
service,
|
|
197
|
+
...cause === void 0 ? {} : { cause },
|
|
198
|
+
meta: { timeoutMs }
|
|
171
199
|
};
|
|
172
200
|
}
|
|
173
201
|
function abortedError(service) {
|
|
@@ -314,6 +342,9 @@ function storageRejectionError(service, status, meta, responseText) {
|
|
|
314
342
|
}
|
|
315
343
|
function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
|
|
316
344
|
if (error instanceof Error) {
|
|
345
|
+
if (isRequestTimeoutError(error)) {
|
|
346
|
+
return timeoutError(service, error.timeoutMs, error);
|
|
347
|
+
}
|
|
317
348
|
if (error.name === "AbortError") {
|
|
318
349
|
return abortedError(service);
|
|
319
350
|
}
|
|
@@ -335,6 +366,7 @@ function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
|
|
|
335
366
|
}
|
|
336
367
|
|
|
337
368
|
// src/base/BaseService.ts
|
|
369
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
338
370
|
var BaseService = class {
|
|
339
371
|
constructor() {
|
|
340
372
|
/**
|
|
@@ -515,6 +547,64 @@ var BaseService = class {
|
|
|
515
547
|
}
|
|
516
548
|
return controller.signal;
|
|
517
549
|
}
|
|
550
|
+
/**
|
|
551
|
+
* Create the abort signal for one request, bounded by the request timeout.
|
|
552
|
+
*
|
|
553
|
+
* The signal aborts when the service signs out, the context aborts, the
|
|
554
|
+
* caller's `signal` aborts, or the timeout elapses, whichever comes first.
|
|
555
|
+
* A timeout aborts with a {@link RequestTimeoutError} (`TimeoutError`), which
|
|
556
|
+
* `wrapError` maps to `ErrorCodes.TIMEOUT`; the other sources keep their own
|
|
557
|
+
* reason (normally `AbortError`, mapped to `ErrorCodes.ABORTED`).
|
|
558
|
+
*
|
|
559
|
+
* The timeout is `timeoutMs` when given, otherwise `config.timeout`. Only a
|
|
560
|
+
* positive, finite value (at most 2^31-1 ms) applies; `undefined`, `0`, and
|
|
561
|
+
* `Infinity` mean no timeout, so a per-call `0` opts out of a configured one.
|
|
562
|
+
*
|
|
563
|
+
* Callers must call `dispose()` once the response body has been consumed,
|
|
564
|
+
* which clears the timer and removes the listeners this adds to the
|
|
565
|
+
* long-lived service and context signals.
|
|
566
|
+
*
|
|
567
|
+
* @param signal - The caller's abort signal for this request
|
|
568
|
+
* @param timeoutMs - Per-request timeout override in milliseconds
|
|
569
|
+
*/
|
|
570
|
+
createRequestSignal(signal, timeoutMs) {
|
|
571
|
+
const controller = new AbortController();
|
|
572
|
+
const detachers = [];
|
|
573
|
+
let timer;
|
|
574
|
+
const dispose = () => {
|
|
575
|
+
if (timer !== void 0) {
|
|
576
|
+
clearTimeout(timer);
|
|
577
|
+
timer = void 0;
|
|
578
|
+
}
|
|
579
|
+
for (const detach of detachers.splice(0)) {
|
|
580
|
+
detach();
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
const abort = (reason) => {
|
|
584
|
+
dispose();
|
|
585
|
+
controller.abort(reason);
|
|
586
|
+
};
|
|
587
|
+
const requestSignal = { signal: controller.signal, dispose };
|
|
588
|
+
const parents = [
|
|
589
|
+
this.abortController.signal,
|
|
590
|
+
this.context?.abortSignal,
|
|
591
|
+
signal
|
|
592
|
+
].filter((parent) => parent !== void 0);
|
|
593
|
+
for (const parent of parents) {
|
|
594
|
+
if (parent.aborted) {
|
|
595
|
+
abort(parent.reason);
|
|
596
|
+
return requestSignal;
|
|
597
|
+
}
|
|
598
|
+
const onAbort = () => abort(parent.reason);
|
|
599
|
+
parent.addEventListener("abort", onAbort, { once: true });
|
|
600
|
+
detachers.push(() => parent.removeEventListener("abort", onAbort));
|
|
601
|
+
}
|
|
602
|
+
const timeout = timeoutMs ?? this._config.timeout;
|
|
603
|
+
if (typeof timeout === "number" && timeout > 0 && timeout <= MAX_TIMER_DELAY_MS) {
|
|
604
|
+
timer = setTimeout(() => abort(new RequestTimeoutError(timeout)), timeout);
|
|
605
|
+
}
|
|
606
|
+
return requestSignal;
|
|
607
|
+
}
|
|
518
608
|
/**
|
|
519
609
|
* Wrap an operation with error handling and telemetry.
|
|
520
610
|
*
|
|
@@ -623,6 +713,30 @@ var PrefixedKVService = class _PrefixedKVService {
|
|
|
623
713
|
removePrefix
|
|
624
714
|
});
|
|
625
715
|
}
|
|
716
|
+
/**
|
|
717
|
+
* Read the change feed for everything under this prefix. A view with an
|
|
718
|
+
* empty prefix (`""` or `"/"`) is refused: the node does not serve a
|
|
719
|
+
* whole-space feed.
|
|
720
|
+
*/
|
|
721
|
+
async changes(options) {
|
|
722
|
+
if (this._prefix.replace(/\/+$/, "") === "") {
|
|
723
|
+
return err(serviceError(
|
|
724
|
+
ErrorCodes.INVALID_INPUT,
|
|
725
|
+
"KV changes needs a non-empty prefix; this view has none",
|
|
726
|
+
"kv"
|
|
727
|
+
));
|
|
728
|
+
}
|
|
729
|
+
const syncPrefix = `${this._prefix}/`;
|
|
730
|
+
const response = await this._kv.changes({ ...options, prefix: syncPrefix });
|
|
731
|
+
if (!response.ok) return response;
|
|
732
|
+
return ok({
|
|
733
|
+
...response.data,
|
|
734
|
+
changes: response.data.changes.map((change) => ({
|
|
735
|
+
...change,
|
|
736
|
+
key: change.key.startsWith(syncPrefix) ? change.key.slice(syncPrefix.length) : change.key
|
|
737
|
+
}))
|
|
738
|
+
});
|
|
739
|
+
}
|
|
626
740
|
/**
|
|
627
741
|
* Delete a key.
|
|
628
742
|
*/
|
|
@@ -675,11 +789,18 @@ var KVAction = {
|
|
|
675
789
|
PUT: "tinycloud.kv/put",
|
|
676
790
|
LIST: "tinycloud.kv/list",
|
|
677
791
|
DELETE: "tinycloud.kv/del",
|
|
678
|
-
HEAD: "tinycloud.kv/metadata"
|
|
792
|
+
HEAD: "tinycloud.kv/metadata",
|
|
793
|
+
/** The change feed; never implied by `*` or `tinycloud.kv/*`. */
|
|
794
|
+
SYNC: "tinycloud.kv/sync",
|
|
795
|
+
/** Retention attestation; presented through `retentionGrant`, never invoked or implied. */
|
|
796
|
+
RETAIN: "tinycloud.kv/retain"
|
|
679
797
|
};
|
|
680
798
|
|
|
681
799
|
// src/kv/KVService.ts
|
|
682
800
|
var MAX_KV_BATCH_READ_ITEMS = 100;
|
|
801
|
+
function isJsonObject(value) {
|
|
802
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
803
|
+
}
|
|
683
804
|
function encodeKvBatchPartName(path) {
|
|
684
805
|
return encodeURIComponent(path).replace(
|
|
685
806
|
/[!'()*]/g,
|
|
@@ -775,7 +896,7 @@ var KVService = class extends BaseService {
|
|
|
775
896
|
* @param path - Resource path
|
|
776
897
|
* @param action - KV action
|
|
777
898
|
* @param body - Optional request body
|
|
778
|
-
* @param signal -
|
|
899
|
+
* @param signal - Request signal from createRequestSignal()
|
|
779
900
|
* @returns Fetch response
|
|
780
901
|
*/
|
|
781
902
|
async invokeOperation(path, action, body, signal, extraHeaders) {
|
|
@@ -791,7 +912,7 @@ var KVService = class extends BaseService {
|
|
|
791
912
|
method: "POST",
|
|
792
913
|
headers: requestHeaders,
|
|
793
914
|
body,
|
|
794
|
-
signal
|
|
915
|
+
signal
|
|
795
916
|
});
|
|
796
917
|
}
|
|
797
918
|
/**
|
|
@@ -968,6 +1089,7 @@ var KVService = class extends BaseService {
|
|
|
968
1089
|
"kv"
|
|
969
1090
|
));
|
|
970
1091
|
}
|
|
1092
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
971
1093
|
try {
|
|
972
1094
|
const session = this.context.session;
|
|
973
1095
|
const invocationHeaders = this.context.invokeAny(
|
|
@@ -989,7 +1111,7 @@ var KVService = class extends BaseService {
|
|
|
989
1111
|
const response = await this.context.fetch(`${this.host}/invoke`, {
|
|
990
1112
|
method: "POST",
|
|
991
1113
|
headers,
|
|
992
|
-
signal:
|
|
1114
|
+
signal: request.signal
|
|
993
1115
|
});
|
|
994
1116
|
if (!response.ok) {
|
|
995
1117
|
const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
|
|
@@ -1054,6 +1176,8 @@ var KVService = class extends BaseService {
|
|
|
1054
1176
|
return ok({ results, count: results.length });
|
|
1055
1177
|
} catch (error) {
|
|
1056
1178
|
return err(wrapError("kv", error));
|
|
1179
|
+
} finally {
|
|
1180
|
+
request.dispose();
|
|
1057
1181
|
}
|
|
1058
1182
|
}
|
|
1059
1183
|
/**
|
|
@@ -1186,12 +1310,13 @@ var KVService = class extends BaseService {
|
|
|
1186
1310
|
));
|
|
1187
1311
|
}
|
|
1188
1312
|
const path = this.getFullPath(key, options?.prefix);
|
|
1313
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1189
1314
|
try {
|
|
1190
1315
|
const response = await this.invokeOperation(
|
|
1191
1316
|
path,
|
|
1192
1317
|
KVAction.GET,
|
|
1193
1318
|
void 0,
|
|
1194
|
-
|
|
1319
|
+
request.signal,
|
|
1195
1320
|
options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
|
|
1196
1321
|
);
|
|
1197
1322
|
if (!response.ok) {
|
|
@@ -1248,6 +1373,8 @@ var KVService = class extends BaseService {
|
|
|
1248
1373
|
});
|
|
1249
1374
|
} catch (error) {
|
|
1250
1375
|
return err(wrapError("kv", error));
|
|
1376
|
+
} finally {
|
|
1377
|
+
request.dispose();
|
|
1251
1378
|
}
|
|
1252
1379
|
});
|
|
1253
1380
|
}
|
|
@@ -1275,12 +1402,13 @@ var KVService = class extends BaseService {
|
|
|
1275
1402
|
}
|
|
1276
1403
|
const path = this.getFullPath(key, options?.prefix);
|
|
1277
1404
|
const body = this.serializePutValue(value, options?.contentType);
|
|
1405
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1278
1406
|
try {
|
|
1279
1407
|
const response = await this.invokeOperation(
|
|
1280
1408
|
path,
|
|
1281
1409
|
KVAction.PUT,
|
|
1282
1410
|
body,
|
|
1283
|
-
|
|
1411
|
+
request.signal,
|
|
1284
1412
|
{
|
|
1285
1413
|
...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
|
|
1286
1414
|
...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
|
|
@@ -1336,6 +1464,8 @@ var KVService = class extends BaseService {
|
|
|
1336
1464
|
});
|
|
1337
1465
|
} catch (error) {
|
|
1338
1466
|
return err(wrapError("kv", error));
|
|
1467
|
+
} finally {
|
|
1468
|
+
request.dispose();
|
|
1339
1469
|
}
|
|
1340
1470
|
});
|
|
1341
1471
|
}
|
|
@@ -1375,6 +1505,7 @@ var KVService = class extends BaseService {
|
|
|
1375
1505
|
seen.add(path);
|
|
1376
1506
|
}
|
|
1377
1507
|
let requestMayHaveDispatched = false;
|
|
1508
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1378
1509
|
try {
|
|
1379
1510
|
const body = new FormData();
|
|
1380
1511
|
for (let index = 0; index < items.length; index++) {
|
|
@@ -1397,7 +1528,7 @@ var KVService = class extends BaseService {
|
|
|
1397
1528
|
method: "POST",
|
|
1398
1529
|
headers,
|
|
1399
1530
|
body,
|
|
1400
|
-
signal:
|
|
1531
|
+
signal: request.signal
|
|
1401
1532
|
};
|
|
1402
1533
|
const fetchFn = this.context.fetch;
|
|
1403
1534
|
requestMayHaveDispatched = true;
|
|
@@ -1448,14 +1579,16 @@ var KVService = class extends BaseService {
|
|
|
1448
1579
|
rawBody = await response.json();
|
|
1449
1580
|
} catch (jsonError) {
|
|
1450
1581
|
const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
|
|
1582
|
+
const timedOut = isRequestTimeoutError(cause);
|
|
1451
1583
|
return err(
|
|
1452
1584
|
serviceError(
|
|
1453
|
-
ErrorCodes.NETWORK_ERROR,
|
|
1454
|
-
`KV batchPut response was not valid JSON: ${cause.message}`,
|
|
1585
|
+
timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
|
|
1586
|
+
timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
|
|
1455
1587
|
"kv",
|
|
1456
1588
|
{
|
|
1457
1589
|
cause,
|
|
1458
1590
|
meta: {
|
|
1591
|
+
...timedOut ? { timeoutMs: cause.timeoutMs } : {},
|
|
1459
1592
|
requestMayHaveDispatched: true,
|
|
1460
1593
|
responseReceived: true,
|
|
1461
1594
|
status: response.status,
|
|
@@ -1510,6 +1643,8 @@ var KVService = class extends BaseService {
|
|
|
1510
1643
|
...wrapped,
|
|
1511
1644
|
meta: { ...wrapped.meta, requestMayHaveDispatched }
|
|
1512
1645
|
});
|
|
1646
|
+
} finally {
|
|
1647
|
+
request.dispose();
|
|
1513
1648
|
}
|
|
1514
1649
|
});
|
|
1515
1650
|
}
|
|
@@ -1532,12 +1667,13 @@ var KVService = class extends BaseService {
|
|
|
1532
1667
|
if (options?.path) {
|
|
1533
1668
|
listPath = listPath ? `${listPath}/${options.path}` : options.path;
|
|
1534
1669
|
}
|
|
1670
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1535
1671
|
try {
|
|
1536
1672
|
const response = await this.invokeOperation(
|
|
1537
1673
|
listPath,
|
|
1538
1674
|
KVAction.LIST,
|
|
1539
1675
|
void 0,
|
|
1540
|
-
|
|
1676
|
+
request.signal,
|
|
1541
1677
|
options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
|
|
1542
1678
|
...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
|
|
1543
1679
|
...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
|
|
@@ -1578,9 +1714,174 @@ var KVService = class extends BaseService {
|
|
|
1578
1714
|
});
|
|
1579
1715
|
} catch (error) {
|
|
1580
1716
|
return err(wrapError("kv", error));
|
|
1717
|
+
} finally {
|
|
1718
|
+
request.dispose();
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Read one page of the `tinycloud.kv/sync` change feed for a prefix.
|
|
1724
|
+
*/
|
|
1725
|
+
async changes(options) {
|
|
1726
|
+
return this.withTelemetry("changes", options?.prefix, async () => {
|
|
1727
|
+
if (!this.requireAuth()) {
|
|
1728
|
+
return err(authRequiredError("kv"));
|
|
1729
|
+
}
|
|
1730
|
+
if (typeof options?.prefix !== "string" || options.prefix.length === 0) {
|
|
1731
|
+
return err(serviceError(
|
|
1732
|
+
ErrorCodes.INVALID_INPUT,
|
|
1733
|
+
"KV changes prefix must be a non-empty string",
|
|
1734
|
+
"kv"
|
|
1735
|
+
));
|
|
1736
|
+
}
|
|
1737
|
+
if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
|
|
1738
|
+
return err(serviceError(
|
|
1739
|
+
ErrorCodes.INVALID_INPUT,
|
|
1740
|
+
"KV changes limit must be an integer from 1 through 1000",
|
|
1741
|
+
"kv"
|
|
1742
|
+
));
|
|
1743
|
+
}
|
|
1744
|
+
const headers = {};
|
|
1745
|
+
if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
|
|
1746
|
+
if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
|
|
1747
|
+
if (options.retentionGrant !== void 0) {
|
|
1748
|
+
headers["x-tinycloud-retention-grant"] = options.retentionGrant;
|
|
1749
|
+
}
|
|
1750
|
+
const request = this.createRequestSignal(options.signal, options.timeout);
|
|
1751
|
+
try {
|
|
1752
|
+
const response = await this.invokeOperation(
|
|
1753
|
+
options.prefix,
|
|
1754
|
+
KVAction.SYNC,
|
|
1755
|
+
void 0,
|
|
1756
|
+
request.signal,
|
|
1757
|
+
headers
|
|
1758
|
+
);
|
|
1759
|
+
if (!response.ok) {
|
|
1760
|
+
return await this.changesFailure(response, options.prefix, request.signal);
|
|
1761
|
+
}
|
|
1762
|
+
const text = await response.text();
|
|
1763
|
+
let body;
|
|
1764
|
+
try {
|
|
1765
|
+
body = JSON.parse(text);
|
|
1766
|
+
} catch {
|
|
1767
|
+
body = void 0;
|
|
1768
|
+
}
|
|
1769
|
+
const page = this.normalizeChangesResponse(body);
|
|
1770
|
+
if (!page) {
|
|
1771
|
+
return err(serviceError(
|
|
1772
|
+
ErrorCodes.NETWORK_ERROR,
|
|
1773
|
+
"KV changes response was not a tinycloud.kv/sync page",
|
|
1774
|
+
"kv",
|
|
1775
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1776
|
+
));
|
|
1777
|
+
}
|
|
1778
|
+
return ok(page);
|
|
1779
|
+
} catch (error) {
|
|
1780
|
+
return err(wrapError("kv", error));
|
|
1781
|
+
} finally {
|
|
1782
|
+
request.dispose();
|
|
1581
1783
|
}
|
|
1582
1784
|
});
|
|
1583
1785
|
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Map a failed `kv/sync` response. 410 and the retention 403 carry
|
|
1788
|
+
* `{"error":{"code","reason"}}`; a revoked grant is a 401 whose text names
|
|
1789
|
+
* `delegation-revoked` or `delegation-ancestor-revoked`. A body read that
|
|
1790
|
+
* fails because the request was cancelled or timed out rethrows, so the
|
|
1791
|
+
* caller sees `ABORTED`/`TIMEOUT` rather than a mapped status.
|
|
1792
|
+
*/
|
|
1793
|
+
async changesFailure(response, prefix, signal) {
|
|
1794
|
+
if (response.status === 404) {
|
|
1795
|
+
return this.classifyNotFound(response, prefix);
|
|
1796
|
+
}
|
|
1797
|
+
let errorText;
|
|
1798
|
+
try {
|
|
1799
|
+
errorText = await response.text();
|
|
1800
|
+
} catch (error) {
|
|
1801
|
+
if (signal.aborted) throw error;
|
|
1802
|
+
errorText = "";
|
|
1803
|
+
}
|
|
1804
|
+
const meta = { status: response.status, statusText: response.statusText };
|
|
1805
|
+
let reason;
|
|
1806
|
+
let bodyCode;
|
|
1807
|
+
try {
|
|
1808
|
+
const body = JSON.parse(errorText);
|
|
1809
|
+
const error = isJsonObject(body) ? body.error : void 0;
|
|
1810
|
+
if (isJsonObject(error)) {
|
|
1811
|
+
if (typeof error.code === "string") bodyCode = error.code;
|
|
1812
|
+
if (typeof error.reason === "string") reason = error.reason;
|
|
1813
|
+
}
|
|
1814
|
+
} catch {
|
|
1815
|
+
}
|
|
1816
|
+
const context = `Failed to read KV changes for ${JSON.stringify(prefix)}`;
|
|
1817
|
+
if (response.status === 410) {
|
|
1818
|
+
return err(serviceError(
|
|
1819
|
+
ErrorCodes.KV_SYNC_RESET_REQUIRED,
|
|
1820
|
+
`${context}: the node requires a reset (${reason ?? "reason not given"}); discard sync state and restart without a cursor`,
|
|
1821
|
+
"kv",
|
|
1822
|
+
{ meta: { ...meta, reason } }
|
|
1823
|
+
));
|
|
1824
|
+
}
|
|
1825
|
+
if (response.status === 403 && bodyCode === "RETENTION_GRANT_REFUSED") {
|
|
1826
|
+
return err(serviceError(
|
|
1827
|
+
ErrorCodes.KV_RETENTION_GRANT_REFUSED,
|
|
1828
|
+
`${context}: the retention grant was refused (${reason ?? "reason not given"})`,
|
|
1829
|
+
"kv",
|
|
1830
|
+
{ meta: { ...meta, reason } }
|
|
1831
|
+
));
|
|
1832
|
+
}
|
|
1833
|
+
if (response.status === 401) {
|
|
1834
|
+
const revoked = /\bdelegation-ancestor-revoked\b/.test(errorText) ? ErrorCodes.AUTH_DELEGATION_ANCESTOR_REVOKED : /\bdelegation-revoked\b/.test(errorText) ? ErrorCodes.AUTH_DELEGATION_REVOKED : void 0;
|
|
1835
|
+
if (revoked !== void 0) {
|
|
1836
|
+
return err(serviceError(revoked, `${context}: 401 - ${errorText}`, "kv", { meta }));
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
if (response.status === 401 || response.status === 403) {
|
|
1840
|
+
return this.authorizationFailure(context, response, errorText, [prefix], KVAction.SYNC);
|
|
1841
|
+
}
|
|
1842
|
+
return err(serviceError(
|
|
1843
|
+
response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR,
|
|
1844
|
+
`${context}: ${response.status} - ${errorText}`,
|
|
1845
|
+
"kv",
|
|
1846
|
+
{ meta }
|
|
1847
|
+
));
|
|
1848
|
+
}
|
|
1849
|
+
normalizeChangesResponse(data) {
|
|
1850
|
+
if (!isJsonObject(data)) return void 0;
|
|
1851
|
+
const { changes, more, cursor, source, authority } = data;
|
|
1852
|
+
if (!Array.isArray(changes) || typeof more !== "boolean" || typeof cursor !== "string" || !isJsonObject(source) || typeof source.nodeDid !== "string" || typeof source.space !== "string" || typeof source.prefix !== "string" || !isJsonObject(authority)) {
|
|
1853
|
+
return void 0;
|
|
1854
|
+
}
|
|
1855
|
+
const { notBefore, expiresAt, retainUntil } = authority;
|
|
1856
|
+
if (notBefore !== null && typeof notBefore !== "string" || expiresAt !== null && typeof expiresAt !== "string" || retainUntil !== null && typeof retainUntil !== "string") {
|
|
1857
|
+
return void 0;
|
|
1858
|
+
}
|
|
1859
|
+
const parsed = [];
|
|
1860
|
+
for (const change of changes) {
|
|
1861
|
+
if (!isJsonObject(change) || typeof change.key !== "string") return void 0;
|
|
1862
|
+
if (change.deleted === true) {
|
|
1863
|
+
parsed.push({ key: change.key, deleted: true });
|
|
1864
|
+
continue;
|
|
1865
|
+
}
|
|
1866
|
+
const { metadata } = change;
|
|
1867
|
+
if (change.deleted !== false || typeof change.etag !== "string" || !isJsonObject(metadata) || !Object.values(metadata).every((value) => typeof value === "string")) {
|
|
1868
|
+
return void 0;
|
|
1869
|
+
}
|
|
1870
|
+
parsed.push({
|
|
1871
|
+
key: change.key,
|
|
1872
|
+
deleted: false,
|
|
1873
|
+
etag: change.etag,
|
|
1874
|
+
metadata
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
return {
|
|
1878
|
+
changes: parsed,
|
|
1879
|
+
more,
|
|
1880
|
+
cursor,
|
|
1881
|
+
source: { nodeDid: source.nodeDid, space: source.space, prefix: source.prefix },
|
|
1882
|
+
authority: { notBefore, expiresAt, retainUntil }
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1584
1885
|
/**
|
|
1585
1886
|
* Delete a key.
|
|
1586
1887
|
*/
|
|
@@ -1590,12 +1891,13 @@ var KVService = class extends BaseService {
|
|
|
1590
1891
|
return err(authRequiredError("kv"));
|
|
1591
1892
|
}
|
|
1592
1893
|
const path = this.getFullPath(key, options?.prefix);
|
|
1894
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1593
1895
|
try {
|
|
1594
1896
|
const response = await this.invokeOperation(
|
|
1595
1897
|
path,
|
|
1596
1898
|
KVAction.DELETE,
|
|
1597
1899
|
void 0,
|
|
1598
|
-
|
|
1900
|
+
request.signal,
|
|
1599
1901
|
options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
|
|
1600
1902
|
);
|
|
1601
1903
|
if (!response.ok) {
|
|
@@ -1643,6 +1945,8 @@ var KVService = class extends BaseService {
|
|
|
1643
1945
|
});
|
|
1644
1946
|
} catch (error) {
|
|
1645
1947
|
return err(wrapError("kv", error));
|
|
1948
|
+
} finally {
|
|
1949
|
+
request.dispose();
|
|
1646
1950
|
}
|
|
1647
1951
|
});
|
|
1648
1952
|
}
|
|
@@ -1655,12 +1959,13 @@ var KVService = class extends BaseService {
|
|
|
1655
1959
|
return err(authRequiredError("kv"));
|
|
1656
1960
|
}
|
|
1657
1961
|
const path = this.getFullPath(key, options?.prefix);
|
|
1962
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1658
1963
|
try {
|
|
1659
1964
|
const response = await this.invokeOperation(
|
|
1660
1965
|
path,
|
|
1661
1966
|
KVAction.HEAD,
|
|
1662
1967
|
void 0,
|
|
1663
|
-
|
|
1968
|
+
request.signal
|
|
1664
1969
|
);
|
|
1665
1970
|
if (!response.ok) {
|
|
1666
1971
|
if (response.status === 401 || response.status === 403) {
|
|
@@ -1691,6 +1996,8 @@ var KVService = class extends BaseService {
|
|
|
1691
1996
|
});
|
|
1692
1997
|
} catch (error) {
|
|
1693
1998
|
return err(wrapError("kv", error));
|
|
1999
|
+
} finally {
|
|
2000
|
+
request.dispose();
|
|
1694
2001
|
}
|
|
1695
2002
|
});
|
|
1696
2003
|
}
|
|
@@ -1728,12 +2035,13 @@ var KVService = class extends BaseService {
|
|
|
1728
2035
|
if (options?.etag !== void 0) {
|
|
1729
2036
|
body.etag = options.etag;
|
|
1730
2037
|
}
|
|
2038
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1731
2039
|
try {
|
|
1732
2040
|
const response = await this.context.fetch(`${this.host}/signed/kv`, {
|
|
1733
2041
|
method: "POST",
|
|
1734
2042
|
headers: this.withJsonContentType(headers),
|
|
1735
2043
|
body: JSON.stringify(body),
|
|
1736
|
-
signal:
|
|
2044
|
+
signal: request.signal
|
|
1737
2045
|
});
|
|
1738
2046
|
if (!response.ok) {
|
|
1739
2047
|
return this.createSignedReadUrlError(response, key, path);
|
|
@@ -1753,6 +2061,8 @@ var KVService = class extends BaseService {
|
|
|
1753
2061
|
return ok(signedUrl);
|
|
1754
2062
|
} catch (error) {
|
|
1755
2063
|
return err(wrapError("kv", error));
|
|
2064
|
+
} finally {
|
|
2065
|
+
request.dispose();
|
|
1756
2066
|
}
|
|
1757
2067
|
});
|
|
1758
2068
|
}
|