@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.cjs
CHANGED
|
@@ -34,6 +34,10 @@ var ErrorCodes = {
|
|
|
34
34
|
AUTH_EXPIRED: "AUTH_EXPIRED",
|
|
35
35
|
AUTH_REQUIRED: "AUTH_REQUIRED",
|
|
36
36
|
AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
|
|
37
|
+
/** HTTP 401: the delegation the invocation cites was revoked. */
|
|
38
|
+
AUTH_DELEGATION_REVOKED: "AUTH_DELEGATION_REVOKED",
|
|
39
|
+
/** HTTP 401: a delegation the cited one rests on was revoked. */
|
|
40
|
+
AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
|
|
37
41
|
NETWORK_ERROR: "NETWORK_ERROR",
|
|
38
42
|
TIMEOUT: "TIMEOUT",
|
|
39
43
|
ABORTED: "ABORTED",
|
|
@@ -45,6 +49,10 @@ var ErrorCodes = {
|
|
|
45
49
|
KV_PRECONDITION_FAILED: "KV_PRECONDITION_FAILED",
|
|
46
50
|
KV_CONFLICT: "KV_CONFLICT",
|
|
47
51
|
KV_RESPONSE_TOO_LARGE: "KV_RESPONSE_TOO_LARGE",
|
|
52
|
+
/** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */
|
|
53
|
+
KV_SYNC_RESET_REQUIRED: "KV_SYNC_RESET_REQUIRED",
|
|
54
|
+
/** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */
|
|
55
|
+
KV_RETENTION_GRANT_REFUSED: "KV_RETENTION_GRANT_REFUSED",
|
|
48
56
|
// SQL-specific errors
|
|
49
57
|
SQL_ERROR: "SQL_ERROR",
|
|
50
58
|
SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
|
|
@@ -192,11 +200,31 @@ function authRequiredError(service) {
|
|
|
192
200
|
service
|
|
193
201
|
};
|
|
194
202
|
}
|
|
195
|
-
|
|
203
|
+
var RequestTimeoutError = class extends Error {
|
|
204
|
+
constructor(timeoutMs) {
|
|
205
|
+
super(`Request timed out after ${timeoutMs}ms.`);
|
|
206
|
+
this.name = "TimeoutError";
|
|
207
|
+
this.timeoutMs = timeoutMs;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
function isRequestTimeoutError(error) {
|
|
211
|
+
return error instanceof RequestTimeoutError || error instanceof Error && error.name === "TimeoutError" && typeof error.timeoutMs === "number";
|
|
212
|
+
}
|
|
213
|
+
function timeoutError(service, timeoutMs, cause) {
|
|
214
|
+
if (timeoutMs === void 0) {
|
|
215
|
+
return {
|
|
216
|
+
code: ErrorCodes.TIMEOUT,
|
|
217
|
+
message: "Request timed out.",
|
|
218
|
+
service,
|
|
219
|
+
...cause === void 0 ? {} : { cause }
|
|
220
|
+
};
|
|
221
|
+
}
|
|
196
222
|
return {
|
|
197
223
|
code: ErrorCodes.TIMEOUT,
|
|
198
|
-
message:
|
|
199
|
-
service
|
|
224
|
+
message: `Request timed out after ${timeoutMs}ms.`,
|
|
225
|
+
service,
|
|
226
|
+
...cause === void 0 ? {} : { cause },
|
|
227
|
+
meta: { timeoutMs }
|
|
200
228
|
};
|
|
201
229
|
}
|
|
202
230
|
function abortedError(service) {
|
|
@@ -343,6 +371,9 @@ function storageRejectionError(service, status, meta, responseText) {
|
|
|
343
371
|
}
|
|
344
372
|
function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
|
|
345
373
|
if (error instanceof Error) {
|
|
374
|
+
if (isRequestTimeoutError(error)) {
|
|
375
|
+
return timeoutError(service, error.timeoutMs, error);
|
|
376
|
+
}
|
|
346
377
|
if (error.name === "AbortError") {
|
|
347
378
|
return abortedError(service);
|
|
348
379
|
}
|
|
@@ -364,6 +395,7 @@ function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
|
|
|
364
395
|
}
|
|
365
396
|
|
|
366
397
|
// src/base/BaseService.ts
|
|
398
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
367
399
|
var BaseService = class {
|
|
368
400
|
constructor() {
|
|
369
401
|
/**
|
|
@@ -544,6 +576,64 @@ var BaseService = class {
|
|
|
544
576
|
}
|
|
545
577
|
return controller.signal;
|
|
546
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* Create the abort signal for one request, bounded by the request timeout.
|
|
581
|
+
*
|
|
582
|
+
* The signal aborts when the service signs out, the context aborts, the
|
|
583
|
+
* caller's `signal` aborts, or the timeout elapses, whichever comes first.
|
|
584
|
+
* A timeout aborts with a {@link RequestTimeoutError} (`TimeoutError`), which
|
|
585
|
+
* `wrapError` maps to `ErrorCodes.TIMEOUT`; the other sources keep their own
|
|
586
|
+
* reason (normally `AbortError`, mapped to `ErrorCodes.ABORTED`).
|
|
587
|
+
*
|
|
588
|
+
* The timeout is `timeoutMs` when given, otherwise `config.timeout`. Only a
|
|
589
|
+
* positive, finite value (at most 2^31-1 ms) applies; `undefined`, `0`, and
|
|
590
|
+
* `Infinity` mean no timeout, so a per-call `0` opts out of a configured one.
|
|
591
|
+
*
|
|
592
|
+
* Callers must call `dispose()` once the response body has been consumed,
|
|
593
|
+
* which clears the timer and removes the listeners this adds to the
|
|
594
|
+
* long-lived service and context signals.
|
|
595
|
+
*
|
|
596
|
+
* @param signal - The caller's abort signal for this request
|
|
597
|
+
* @param timeoutMs - Per-request timeout override in milliseconds
|
|
598
|
+
*/
|
|
599
|
+
createRequestSignal(signal, timeoutMs) {
|
|
600
|
+
const controller = new AbortController();
|
|
601
|
+
const detachers = [];
|
|
602
|
+
let timer;
|
|
603
|
+
const dispose = () => {
|
|
604
|
+
if (timer !== void 0) {
|
|
605
|
+
clearTimeout(timer);
|
|
606
|
+
timer = void 0;
|
|
607
|
+
}
|
|
608
|
+
for (const detach of detachers.splice(0)) {
|
|
609
|
+
detach();
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
const abort = (reason) => {
|
|
613
|
+
dispose();
|
|
614
|
+
controller.abort(reason);
|
|
615
|
+
};
|
|
616
|
+
const requestSignal = { signal: controller.signal, dispose };
|
|
617
|
+
const parents = [
|
|
618
|
+
this.abortController.signal,
|
|
619
|
+
this.context?.abortSignal,
|
|
620
|
+
signal
|
|
621
|
+
].filter((parent) => parent !== void 0);
|
|
622
|
+
for (const parent of parents) {
|
|
623
|
+
if (parent.aborted) {
|
|
624
|
+
abort(parent.reason);
|
|
625
|
+
return requestSignal;
|
|
626
|
+
}
|
|
627
|
+
const onAbort = () => abort(parent.reason);
|
|
628
|
+
parent.addEventListener("abort", onAbort, { once: true });
|
|
629
|
+
detachers.push(() => parent.removeEventListener("abort", onAbort));
|
|
630
|
+
}
|
|
631
|
+
const timeout = timeoutMs ?? this._config.timeout;
|
|
632
|
+
if (typeof timeout === "number" && timeout > 0 && timeout <= MAX_TIMER_DELAY_MS) {
|
|
633
|
+
timer = setTimeout(() => abort(new RequestTimeoutError(timeout)), timeout);
|
|
634
|
+
}
|
|
635
|
+
return requestSignal;
|
|
636
|
+
}
|
|
547
637
|
/**
|
|
548
638
|
* Wrap an operation with error handling and telemetry.
|
|
549
639
|
*
|
|
@@ -652,6 +742,30 @@ var PrefixedKVService = class _PrefixedKVService {
|
|
|
652
742
|
removePrefix
|
|
653
743
|
});
|
|
654
744
|
}
|
|
745
|
+
/**
|
|
746
|
+
* Read the change feed for everything under this prefix. A view with an
|
|
747
|
+
* empty prefix (`""` or `"/"`) is refused: the node does not serve a
|
|
748
|
+
* whole-space feed.
|
|
749
|
+
*/
|
|
750
|
+
async changes(options) {
|
|
751
|
+
if (this._prefix.replace(/\/+$/, "") === "") {
|
|
752
|
+
return err(serviceError(
|
|
753
|
+
ErrorCodes.INVALID_INPUT,
|
|
754
|
+
"KV changes needs a non-empty prefix; this view has none",
|
|
755
|
+
"kv"
|
|
756
|
+
));
|
|
757
|
+
}
|
|
758
|
+
const syncPrefix = `${this._prefix}/`;
|
|
759
|
+
const response = await this._kv.changes({ ...options, prefix: syncPrefix });
|
|
760
|
+
if (!response.ok) return response;
|
|
761
|
+
return ok({
|
|
762
|
+
...response.data,
|
|
763
|
+
changes: response.data.changes.map((change) => ({
|
|
764
|
+
...change,
|
|
765
|
+
key: change.key.startsWith(syncPrefix) ? change.key.slice(syncPrefix.length) : change.key
|
|
766
|
+
}))
|
|
767
|
+
});
|
|
768
|
+
}
|
|
655
769
|
/**
|
|
656
770
|
* Delete a key.
|
|
657
771
|
*/
|
|
@@ -704,11 +818,18 @@ var KVAction = {
|
|
|
704
818
|
PUT: "tinycloud.kv/put",
|
|
705
819
|
LIST: "tinycloud.kv/list",
|
|
706
820
|
DELETE: "tinycloud.kv/del",
|
|
707
|
-
HEAD: "tinycloud.kv/metadata"
|
|
821
|
+
HEAD: "tinycloud.kv/metadata",
|
|
822
|
+
/** The change feed; never implied by `*` or `tinycloud.kv/*`. */
|
|
823
|
+
SYNC: "tinycloud.kv/sync",
|
|
824
|
+
/** Retention attestation; presented through `retentionGrant`, never invoked or implied. */
|
|
825
|
+
RETAIN: "tinycloud.kv/retain"
|
|
708
826
|
};
|
|
709
827
|
|
|
710
828
|
// src/kv/KVService.ts
|
|
711
829
|
var MAX_KV_BATCH_READ_ITEMS = 100;
|
|
830
|
+
function isJsonObject(value) {
|
|
831
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
832
|
+
}
|
|
712
833
|
function encodeKvBatchPartName(path) {
|
|
713
834
|
return encodeURIComponent(path).replace(
|
|
714
835
|
/[!'()*]/g,
|
|
@@ -804,7 +925,7 @@ var KVService = class extends BaseService {
|
|
|
804
925
|
* @param path - Resource path
|
|
805
926
|
* @param action - KV action
|
|
806
927
|
* @param body - Optional request body
|
|
807
|
-
* @param signal -
|
|
928
|
+
* @param signal - Request signal from createRequestSignal()
|
|
808
929
|
* @returns Fetch response
|
|
809
930
|
*/
|
|
810
931
|
async invokeOperation(path, action, body, signal, extraHeaders) {
|
|
@@ -820,7 +941,7 @@ var KVService = class extends BaseService {
|
|
|
820
941
|
method: "POST",
|
|
821
942
|
headers: requestHeaders,
|
|
822
943
|
body,
|
|
823
|
-
signal
|
|
944
|
+
signal
|
|
824
945
|
});
|
|
825
946
|
}
|
|
826
947
|
/**
|
|
@@ -997,6 +1118,7 @@ var KVService = class extends BaseService {
|
|
|
997
1118
|
"kv"
|
|
998
1119
|
));
|
|
999
1120
|
}
|
|
1121
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1000
1122
|
try {
|
|
1001
1123
|
const session = this.context.session;
|
|
1002
1124
|
const invocationHeaders = this.context.invokeAny(
|
|
@@ -1018,7 +1140,7 @@ var KVService = class extends BaseService {
|
|
|
1018
1140
|
const response = await this.context.fetch(`${this.host}/invoke`, {
|
|
1019
1141
|
method: "POST",
|
|
1020
1142
|
headers,
|
|
1021
|
-
signal:
|
|
1143
|
+
signal: request.signal
|
|
1022
1144
|
});
|
|
1023
1145
|
if (!response.ok) {
|
|
1024
1146
|
const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
|
|
@@ -1083,6 +1205,8 @@ var KVService = class extends BaseService {
|
|
|
1083
1205
|
return ok({ results, count: results.length });
|
|
1084
1206
|
} catch (error) {
|
|
1085
1207
|
return err(wrapError("kv", error));
|
|
1208
|
+
} finally {
|
|
1209
|
+
request.dispose();
|
|
1086
1210
|
}
|
|
1087
1211
|
}
|
|
1088
1212
|
/**
|
|
@@ -1215,12 +1339,13 @@ var KVService = class extends BaseService {
|
|
|
1215
1339
|
));
|
|
1216
1340
|
}
|
|
1217
1341
|
const path = this.getFullPath(key, options?.prefix);
|
|
1342
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1218
1343
|
try {
|
|
1219
1344
|
const response = await this.invokeOperation(
|
|
1220
1345
|
path,
|
|
1221
1346
|
KVAction.GET,
|
|
1222
1347
|
void 0,
|
|
1223
|
-
|
|
1348
|
+
request.signal,
|
|
1224
1349
|
options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
|
|
1225
1350
|
);
|
|
1226
1351
|
if (!response.ok) {
|
|
@@ -1277,6 +1402,8 @@ var KVService = class extends BaseService {
|
|
|
1277
1402
|
});
|
|
1278
1403
|
} catch (error) {
|
|
1279
1404
|
return err(wrapError("kv", error));
|
|
1405
|
+
} finally {
|
|
1406
|
+
request.dispose();
|
|
1280
1407
|
}
|
|
1281
1408
|
});
|
|
1282
1409
|
}
|
|
@@ -1304,12 +1431,13 @@ var KVService = class extends BaseService {
|
|
|
1304
1431
|
}
|
|
1305
1432
|
const path = this.getFullPath(key, options?.prefix);
|
|
1306
1433
|
const body = this.serializePutValue(value, options?.contentType);
|
|
1434
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1307
1435
|
try {
|
|
1308
1436
|
const response = await this.invokeOperation(
|
|
1309
1437
|
path,
|
|
1310
1438
|
KVAction.PUT,
|
|
1311
1439
|
body,
|
|
1312
|
-
|
|
1440
|
+
request.signal,
|
|
1313
1441
|
{
|
|
1314
1442
|
...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
|
|
1315
1443
|
...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
|
|
@@ -1365,6 +1493,8 @@ var KVService = class extends BaseService {
|
|
|
1365
1493
|
});
|
|
1366
1494
|
} catch (error) {
|
|
1367
1495
|
return err(wrapError("kv", error));
|
|
1496
|
+
} finally {
|
|
1497
|
+
request.dispose();
|
|
1368
1498
|
}
|
|
1369
1499
|
});
|
|
1370
1500
|
}
|
|
@@ -1404,6 +1534,7 @@ var KVService = class extends BaseService {
|
|
|
1404
1534
|
seen.add(path);
|
|
1405
1535
|
}
|
|
1406
1536
|
let requestMayHaveDispatched = false;
|
|
1537
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1407
1538
|
try {
|
|
1408
1539
|
const body = new FormData();
|
|
1409
1540
|
for (let index = 0; index < items.length; index++) {
|
|
@@ -1426,7 +1557,7 @@ var KVService = class extends BaseService {
|
|
|
1426
1557
|
method: "POST",
|
|
1427
1558
|
headers,
|
|
1428
1559
|
body,
|
|
1429
|
-
signal:
|
|
1560
|
+
signal: request.signal
|
|
1430
1561
|
};
|
|
1431
1562
|
const fetchFn = this.context.fetch;
|
|
1432
1563
|
requestMayHaveDispatched = true;
|
|
@@ -1477,14 +1608,16 @@ var KVService = class extends BaseService {
|
|
|
1477
1608
|
rawBody = await response.json();
|
|
1478
1609
|
} catch (jsonError) {
|
|
1479
1610
|
const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
|
|
1611
|
+
const timedOut = isRequestTimeoutError(cause);
|
|
1480
1612
|
return err(
|
|
1481
1613
|
serviceError(
|
|
1482
|
-
ErrorCodes.NETWORK_ERROR,
|
|
1483
|
-
`KV batchPut response was not valid JSON: ${cause.message}`,
|
|
1614
|
+
timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
|
|
1615
|
+
timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
|
|
1484
1616
|
"kv",
|
|
1485
1617
|
{
|
|
1486
1618
|
cause,
|
|
1487
1619
|
meta: {
|
|
1620
|
+
...timedOut ? { timeoutMs: cause.timeoutMs } : {},
|
|
1488
1621
|
requestMayHaveDispatched: true,
|
|
1489
1622
|
responseReceived: true,
|
|
1490
1623
|
status: response.status,
|
|
@@ -1539,6 +1672,8 @@ var KVService = class extends BaseService {
|
|
|
1539
1672
|
...wrapped,
|
|
1540
1673
|
meta: { ...wrapped.meta, requestMayHaveDispatched }
|
|
1541
1674
|
});
|
|
1675
|
+
} finally {
|
|
1676
|
+
request.dispose();
|
|
1542
1677
|
}
|
|
1543
1678
|
});
|
|
1544
1679
|
}
|
|
@@ -1561,12 +1696,13 @@ var KVService = class extends BaseService {
|
|
|
1561
1696
|
if (options?.path) {
|
|
1562
1697
|
listPath = listPath ? `${listPath}/${options.path}` : options.path;
|
|
1563
1698
|
}
|
|
1699
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1564
1700
|
try {
|
|
1565
1701
|
const response = await this.invokeOperation(
|
|
1566
1702
|
listPath,
|
|
1567
1703
|
KVAction.LIST,
|
|
1568
1704
|
void 0,
|
|
1569
|
-
|
|
1705
|
+
request.signal,
|
|
1570
1706
|
options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
|
|
1571
1707
|
...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
|
|
1572
1708
|
...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
|
|
@@ -1607,9 +1743,174 @@ var KVService = class extends BaseService {
|
|
|
1607
1743
|
});
|
|
1608
1744
|
} catch (error) {
|
|
1609
1745
|
return err(wrapError("kv", error));
|
|
1746
|
+
} finally {
|
|
1747
|
+
request.dispose();
|
|
1748
|
+
}
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Read one page of the `tinycloud.kv/sync` change feed for a prefix.
|
|
1753
|
+
*/
|
|
1754
|
+
async changes(options) {
|
|
1755
|
+
return this.withTelemetry("changes", options?.prefix, async () => {
|
|
1756
|
+
if (!this.requireAuth()) {
|
|
1757
|
+
return err(authRequiredError("kv"));
|
|
1758
|
+
}
|
|
1759
|
+
if (typeof options?.prefix !== "string" || options.prefix.length === 0) {
|
|
1760
|
+
return err(serviceError(
|
|
1761
|
+
ErrorCodes.INVALID_INPUT,
|
|
1762
|
+
"KV changes prefix must be a non-empty string",
|
|
1763
|
+
"kv"
|
|
1764
|
+
));
|
|
1765
|
+
}
|
|
1766
|
+
if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
|
|
1767
|
+
return err(serviceError(
|
|
1768
|
+
ErrorCodes.INVALID_INPUT,
|
|
1769
|
+
"KV changes limit must be an integer from 1 through 1000",
|
|
1770
|
+
"kv"
|
|
1771
|
+
));
|
|
1772
|
+
}
|
|
1773
|
+
const headers = {};
|
|
1774
|
+
if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
|
|
1775
|
+
if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
|
|
1776
|
+
if (options.retentionGrant !== void 0) {
|
|
1777
|
+
headers["x-tinycloud-retention-grant"] = options.retentionGrant;
|
|
1778
|
+
}
|
|
1779
|
+
const request = this.createRequestSignal(options.signal, options.timeout);
|
|
1780
|
+
try {
|
|
1781
|
+
const response = await this.invokeOperation(
|
|
1782
|
+
options.prefix,
|
|
1783
|
+
KVAction.SYNC,
|
|
1784
|
+
void 0,
|
|
1785
|
+
request.signal,
|
|
1786
|
+
headers
|
|
1787
|
+
);
|
|
1788
|
+
if (!response.ok) {
|
|
1789
|
+
return await this.changesFailure(response, options.prefix, request.signal);
|
|
1790
|
+
}
|
|
1791
|
+
const text = await response.text();
|
|
1792
|
+
let body;
|
|
1793
|
+
try {
|
|
1794
|
+
body = JSON.parse(text);
|
|
1795
|
+
} catch {
|
|
1796
|
+
body = void 0;
|
|
1797
|
+
}
|
|
1798
|
+
const page = this.normalizeChangesResponse(body);
|
|
1799
|
+
if (!page) {
|
|
1800
|
+
return err(serviceError(
|
|
1801
|
+
ErrorCodes.NETWORK_ERROR,
|
|
1802
|
+
"KV changes response was not a tinycloud.kv/sync page",
|
|
1803
|
+
"kv",
|
|
1804
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1805
|
+
));
|
|
1806
|
+
}
|
|
1807
|
+
return ok(page);
|
|
1808
|
+
} catch (error) {
|
|
1809
|
+
return err(wrapError("kv", error));
|
|
1810
|
+
} finally {
|
|
1811
|
+
request.dispose();
|
|
1610
1812
|
}
|
|
1611
1813
|
});
|
|
1612
1814
|
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Map a failed `kv/sync` response. 410 and the retention 403 carry
|
|
1817
|
+
* `{"error":{"code","reason"}}`; a revoked grant is a 401 whose text names
|
|
1818
|
+
* `delegation-revoked` or `delegation-ancestor-revoked`. A body read that
|
|
1819
|
+
* fails because the request was cancelled or timed out rethrows, so the
|
|
1820
|
+
* caller sees `ABORTED`/`TIMEOUT` rather than a mapped status.
|
|
1821
|
+
*/
|
|
1822
|
+
async changesFailure(response, prefix, signal) {
|
|
1823
|
+
if (response.status === 404) {
|
|
1824
|
+
return this.classifyNotFound(response, prefix);
|
|
1825
|
+
}
|
|
1826
|
+
let errorText;
|
|
1827
|
+
try {
|
|
1828
|
+
errorText = await response.text();
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
if (signal.aborted) throw error;
|
|
1831
|
+
errorText = "";
|
|
1832
|
+
}
|
|
1833
|
+
const meta = { status: response.status, statusText: response.statusText };
|
|
1834
|
+
let reason;
|
|
1835
|
+
let bodyCode;
|
|
1836
|
+
try {
|
|
1837
|
+
const body = JSON.parse(errorText);
|
|
1838
|
+
const error = isJsonObject(body) ? body.error : void 0;
|
|
1839
|
+
if (isJsonObject(error)) {
|
|
1840
|
+
if (typeof error.code === "string") bodyCode = error.code;
|
|
1841
|
+
if (typeof error.reason === "string") reason = error.reason;
|
|
1842
|
+
}
|
|
1843
|
+
} catch {
|
|
1844
|
+
}
|
|
1845
|
+
const context = `Failed to read KV changes for ${JSON.stringify(prefix)}`;
|
|
1846
|
+
if (response.status === 410) {
|
|
1847
|
+
return err(serviceError(
|
|
1848
|
+
ErrorCodes.KV_SYNC_RESET_REQUIRED,
|
|
1849
|
+
`${context}: the node requires a reset (${reason ?? "reason not given"}); discard sync state and restart without a cursor`,
|
|
1850
|
+
"kv",
|
|
1851
|
+
{ meta: { ...meta, reason } }
|
|
1852
|
+
));
|
|
1853
|
+
}
|
|
1854
|
+
if (response.status === 403 && bodyCode === "RETENTION_GRANT_REFUSED") {
|
|
1855
|
+
return err(serviceError(
|
|
1856
|
+
ErrorCodes.KV_RETENTION_GRANT_REFUSED,
|
|
1857
|
+
`${context}: the retention grant was refused (${reason ?? "reason not given"})`,
|
|
1858
|
+
"kv",
|
|
1859
|
+
{ meta: { ...meta, reason } }
|
|
1860
|
+
));
|
|
1861
|
+
}
|
|
1862
|
+
if (response.status === 401) {
|
|
1863
|
+
const revoked = /\bdelegation-ancestor-revoked\b/.test(errorText) ? ErrorCodes.AUTH_DELEGATION_ANCESTOR_REVOKED : /\bdelegation-revoked\b/.test(errorText) ? ErrorCodes.AUTH_DELEGATION_REVOKED : void 0;
|
|
1864
|
+
if (revoked !== void 0) {
|
|
1865
|
+
return err(serviceError(revoked, `${context}: 401 - ${errorText}`, "kv", { meta }));
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
if (response.status === 401 || response.status === 403) {
|
|
1869
|
+
return this.authorizationFailure(context, response, errorText, [prefix], KVAction.SYNC);
|
|
1870
|
+
}
|
|
1871
|
+
return err(serviceError(
|
|
1872
|
+
response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR,
|
|
1873
|
+
`${context}: ${response.status} - ${errorText}`,
|
|
1874
|
+
"kv",
|
|
1875
|
+
{ meta }
|
|
1876
|
+
));
|
|
1877
|
+
}
|
|
1878
|
+
normalizeChangesResponse(data) {
|
|
1879
|
+
if (!isJsonObject(data)) return void 0;
|
|
1880
|
+
const { changes, more, cursor, source, authority } = data;
|
|
1881
|
+
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)) {
|
|
1882
|
+
return void 0;
|
|
1883
|
+
}
|
|
1884
|
+
const { notBefore, expiresAt, retainUntil } = authority;
|
|
1885
|
+
if (notBefore !== null && typeof notBefore !== "string" || expiresAt !== null && typeof expiresAt !== "string" || retainUntil !== null && typeof retainUntil !== "string") {
|
|
1886
|
+
return void 0;
|
|
1887
|
+
}
|
|
1888
|
+
const parsed = [];
|
|
1889
|
+
for (const change of changes) {
|
|
1890
|
+
if (!isJsonObject(change) || typeof change.key !== "string") return void 0;
|
|
1891
|
+
if (change.deleted === true) {
|
|
1892
|
+
parsed.push({ key: change.key, deleted: true });
|
|
1893
|
+
continue;
|
|
1894
|
+
}
|
|
1895
|
+
const { metadata } = change;
|
|
1896
|
+
if (change.deleted !== false || typeof change.etag !== "string" || !isJsonObject(metadata) || !Object.values(metadata).every((value) => typeof value === "string")) {
|
|
1897
|
+
return void 0;
|
|
1898
|
+
}
|
|
1899
|
+
parsed.push({
|
|
1900
|
+
key: change.key,
|
|
1901
|
+
deleted: false,
|
|
1902
|
+
etag: change.etag,
|
|
1903
|
+
metadata
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
return {
|
|
1907
|
+
changes: parsed,
|
|
1908
|
+
more,
|
|
1909
|
+
cursor,
|
|
1910
|
+
source: { nodeDid: source.nodeDid, space: source.space, prefix: source.prefix },
|
|
1911
|
+
authority: { notBefore, expiresAt, retainUntil }
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1613
1914
|
/**
|
|
1614
1915
|
* Delete a key.
|
|
1615
1916
|
*/
|
|
@@ -1619,12 +1920,13 @@ var KVService = class extends BaseService {
|
|
|
1619
1920
|
return err(authRequiredError("kv"));
|
|
1620
1921
|
}
|
|
1621
1922
|
const path = this.getFullPath(key, options?.prefix);
|
|
1923
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1622
1924
|
try {
|
|
1623
1925
|
const response = await this.invokeOperation(
|
|
1624
1926
|
path,
|
|
1625
1927
|
KVAction.DELETE,
|
|
1626
1928
|
void 0,
|
|
1627
|
-
|
|
1929
|
+
request.signal,
|
|
1628
1930
|
options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
|
|
1629
1931
|
);
|
|
1630
1932
|
if (!response.ok) {
|
|
@@ -1672,6 +1974,8 @@ var KVService = class extends BaseService {
|
|
|
1672
1974
|
});
|
|
1673
1975
|
} catch (error) {
|
|
1674
1976
|
return err(wrapError("kv", error));
|
|
1977
|
+
} finally {
|
|
1978
|
+
request.dispose();
|
|
1675
1979
|
}
|
|
1676
1980
|
});
|
|
1677
1981
|
}
|
|
@@ -1684,12 +1988,13 @@ var KVService = class extends BaseService {
|
|
|
1684
1988
|
return err(authRequiredError("kv"));
|
|
1685
1989
|
}
|
|
1686
1990
|
const path = this.getFullPath(key, options?.prefix);
|
|
1991
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1687
1992
|
try {
|
|
1688
1993
|
const response = await this.invokeOperation(
|
|
1689
1994
|
path,
|
|
1690
1995
|
KVAction.HEAD,
|
|
1691
1996
|
void 0,
|
|
1692
|
-
|
|
1997
|
+
request.signal
|
|
1693
1998
|
);
|
|
1694
1999
|
if (!response.ok) {
|
|
1695
2000
|
if (response.status === 401 || response.status === 403) {
|
|
@@ -1720,6 +2025,8 @@ var KVService = class extends BaseService {
|
|
|
1720
2025
|
});
|
|
1721
2026
|
} catch (error) {
|
|
1722
2027
|
return err(wrapError("kv", error));
|
|
2028
|
+
} finally {
|
|
2029
|
+
request.dispose();
|
|
1723
2030
|
}
|
|
1724
2031
|
});
|
|
1725
2032
|
}
|
|
@@ -1757,12 +2064,13 @@ var KVService = class extends BaseService {
|
|
|
1757
2064
|
if (options?.etag !== void 0) {
|
|
1758
2065
|
body.etag = options.etag;
|
|
1759
2066
|
}
|
|
2067
|
+
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1760
2068
|
try {
|
|
1761
2069
|
const response = await this.context.fetch(`${this.host}/signed/kv`, {
|
|
1762
2070
|
method: "POST",
|
|
1763
2071
|
headers: this.withJsonContentType(headers),
|
|
1764
2072
|
body: JSON.stringify(body),
|
|
1765
|
-
signal:
|
|
2073
|
+
signal: request.signal
|
|
1766
2074
|
});
|
|
1767
2075
|
if (!response.ok) {
|
|
1768
2076
|
return this.createSignedReadUrlError(response, key, path);
|
|
@@ -1782,6 +2090,8 @@ var KVService = class extends BaseService {
|
|
|
1782
2090
|
return ok(signedUrl);
|
|
1783
2091
|
} catch (error) {
|
|
1784
2092
|
return err(wrapError("kv", error));
|
|
2093
|
+
} finally {
|
|
2094
|
+
request.dispose();
|
|
1785
2095
|
}
|
|
1786
2096
|
});
|
|
1787
2097
|
}
|