@tinycloud/sdk-services 3.2.0-beta.2 → 3.2.0-beta.5
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-DWbPbov7.d.cts → BaseService-BDQXjWMx.d.cts} +1 -1
- package/dist/{BaseService-B_UKhVht.d.ts → BaseService-Di_hVfUV.d.ts} +1 -1
- package/dist/encryption/index.cjs +7 -1
- 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 +7 -1
- package/dist/encryption/index.js.map +1 -1
- package/dist/index.cjs +605 -357
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -26
- package/dist/index.d.ts +37 -26
- package/dist/index.js +600 -357
- 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 +540 -338
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +18 -468
- package/dist/kv/index.d.ts +18 -468
- package/dist/kv/index.js +534 -337
- package/dist/kv/index.js.map +1 -1
- package/dist/kv/replication/index.cjs +1398 -0
- package/dist/kv/replication/index.cjs.map +1 -0
- package/dist/kv/replication/index.d.cts +10 -0
- package/dist/kv/replication/index.d.ts +10 -0
- package/dist/kv/replication/index.js +1365 -0
- package/dist/kv/replication/index.js.map +1 -0
- package/dist/kv/replication/prefix.cjs +43 -0
- package/dist/kv/replication/prefix.cjs.map +1 -0
- package/dist/kv/replication/prefix.d.cts +6 -0
- package/dist/kv/replication/prefix.d.ts +6 -0
- package/dist/kv/replication/prefix.js +17 -0
- package/dist/kv/replication/prefix.js.map +1 -0
- package/dist/scope-BKge7zDT.d.ts +957 -0
- package/dist/scope-C05VVAXH.d.cts +957 -0
- package/dist/sql/index.cjs +12 -6
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +2 -2
- package/dist/sql/index.d.ts +2 -2
- package/dist/sql/index.js +12 -6
- package/dist/sql/index.js.map +1 -1
- package/dist/{types-CVwwxqXF.d.cts → types-7A5rHWRY.d.cts} +10 -1
- package/dist/{types-CVwwxqXF.d.ts → types-7A5rHWRY.d.ts} +10 -1
- package/package.json +31 -1
package/dist/kv/index.js
CHANGED
|
@@ -60,7 +60,12 @@ var TelemetryEvents = {
|
|
|
60
60
|
SERVICE_ERROR: "service.error",
|
|
61
61
|
SERVICE_RETRY: "service.retry",
|
|
62
62
|
SESSION_CHANGED: "session.changed",
|
|
63
|
-
SESSION_EXPIRED: "session.expired"
|
|
63
|
+
SESSION_EXPIRED: "session.expired",
|
|
64
|
+
REPLICATION_READ: "replication.read",
|
|
65
|
+
REPLICATION_WRITE: "replication.write",
|
|
66
|
+
REPLICATION_SYNC: "replication.sync",
|
|
67
|
+
REPLICATION_STATE: "replication.state",
|
|
68
|
+
REPLICATION_DIVERGENCE: "replication.divergence"
|
|
64
69
|
};
|
|
65
70
|
function ok(data) {
|
|
66
71
|
return { ok: true, data };
|
|
@@ -588,6 +593,7 @@ var BaseService = class {
|
|
|
588
593
|
const parents = [
|
|
589
594
|
this.abortController.signal,
|
|
590
595
|
this.context?.abortSignal,
|
|
596
|
+
this.context?.operationAbortSignal,
|
|
591
597
|
signal
|
|
592
598
|
].filter((parent) => parent !== void 0);
|
|
593
599
|
for (const parent of parents) {
|
|
@@ -833,14 +839,28 @@ var KVService = class extends BaseService {
|
|
|
833
839
|
*/
|
|
834
840
|
constructor(config = {}) {
|
|
835
841
|
super();
|
|
842
|
+
this.readThrough = null;
|
|
843
|
+
this.readThroughReady = null;
|
|
836
844
|
this._config = config;
|
|
837
845
|
}
|
|
846
|
+
/** Delay KV routing until an asynchronously loaded read-through is attached. */
|
|
847
|
+
setReadThroughReady(ready) {
|
|
848
|
+
const settled = ready.then(() => void 0, () => void 0);
|
|
849
|
+
this.readThroughReady = settled;
|
|
850
|
+
void settled.then(() => {
|
|
851
|
+
if (this.readThroughReady === settled) this.readThroughReady = null;
|
|
852
|
+
});
|
|
853
|
+
}
|
|
838
854
|
/**
|
|
839
855
|
* Get the service configuration.
|
|
840
856
|
*/
|
|
841
857
|
get config() {
|
|
842
858
|
return this._config;
|
|
843
859
|
}
|
|
860
|
+
/** Install or remove the optional replication read-through policy. */
|
|
861
|
+
setReadThrough(readThrough) {
|
|
862
|
+
this.readThrough = readThrough;
|
|
863
|
+
}
|
|
844
864
|
handleQuotaErrorResponse(response, errorText, key) {
|
|
845
865
|
if (response.status !== 402 && response.status !== 413) {
|
|
846
866
|
return void 0;
|
|
@@ -1094,6 +1114,7 @@ var KVService = class extends BaseService {
|
|
|
1094
1114
|
return aligned;
|
|
1095
1115
|
}
|
|
1096
1116
|
async batchRead(keys, action, options) {
|
|
1117
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
1097
1118
|
if (!this.requireAuth()) return err(authRequiredError("kv"));
|
|
1098
1119
|
if (keys.length === 0) return ok({ results: [], count: 0 });
|
|
1099
1120
|
if (keys.length > MAX_KV_BATCH_READ_ITEMS) {
|
|
@@ -1344,6 +1365,7 @@ var KVService = class extends BaseService {
|
|
|
1344
1365
|
*/
|
|
1345
1366
|
async get(key, options) {
|
|
1346
1367
|
return this.withTelemetry("get", key, async () => {
|
|
1368
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
1347
1369
|
if (!this.requireAuth()) {
|
|
1348
1370
|
return err(authRequiredError("kv"));
|
|
1349
1371
|
}
|
|
@@ -1357,64 +1379,38 @@ var KVService = class extends BaseService {
|
|
|
1357
1379
|
const path = this.getFullPath(key, options?.prefix);
|
|
1358
1380
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1359
1381
|
try {
|
|
1360
|
-
const
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
response,
|
|
1383
|
-
errorText2,
|
|
1384
|
-
[path],
|
|
1385
|
-
KVAction.GET,
|
|
1386
|
-
structuredCapability ? { permissionHint, ...structuredCapability } : {}
|
|
1387
|
-
);
|
|
1388
|
-
}
|
|
1389
|
-
if (response.status === 404) {
|
|
1390
|
-
return await this.classifyNotFound(response, key);
|
|
1391
|
-
}
|
|
1392
|
-
const errorText = await response.text();
|
|
1393
|
-
if (response.status === 413) {
|
|
1394
|
-
return err(serviceError(
|
|
1395
|
-
ErrorCodes.KV_RESPONSE_TOO_LARGE,
|
|
1396
|
-
`KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
|
|
1397
|
-
"kv",
|
|
1398
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1399
|
-
));
|
|
1382
|
+
const network = () => this.getFromNetwork(key, path, options, request.signal);
|
|
1383
|
+
if (options?.source === "network") {
|
|
1384
|
+
const observer = this.readThrough;
|
|
1385
|
+
if (observer === null) return await network();
|
|
1386
|
+
let outcome = "error";
|
|
1387
|
+
const startedAt = Date.now();
|
|
1388
|
+
try {
|
|
1389
|
+
const result = await network();
|
|
1390
|
+
outcome = result.ok ? "found" : result.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error";
|
|
1391
|
+
return result;
|
|
1392
|
+
} finally {
|
|
1393
|
+
try {
|
|
1394
|
+
observer.observeNetworkRequested({
|
|
1395
|
+
op: "get",
|
|
1396
|
+
space: this.context.session.spaceId,
|
|
1397
|
+
path,
|
|
1398
|
+
reason: "NETWORK_REQUESTED",
|
|
1399
|
+
outcome,
|
|
1400
|
+
latencyMs: Date.now() - startedAt
|
|
1401
|
+
});
|
|
1402
|
+
} catch {
|
|
1403
|
+
}
|
|
1400
1404
|
}
|
|
1401
|
-
return err(
|
|
1402
|
-
serviceError(
|
|
1403
|
-
ErrorCodes.NETWORK_ERROR,
|
|
1404
|
-
`Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
1405
|
-
"kv",
|
|
1406
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1407
|
-
)
|
|
1408
|
-
);
|
|
1409
1405
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1406
|
+
if (this.readThrough === null) return await network();
|
|
1407
|
+
return await this.readThrough.get({
|
|
1408
|
+
space: this.context.session.spaceId,
|
|
1409
|
+
key,
|
|
1410
|
+
path,
|
|
1411
|
+
options,
|
|
1412
|
+
signal: request.signal,
|
|
1413
|
+
network
|
|
1418
1414
|
});
|
|
1419
1415
|
} catch (error) {
|
|
1420
1416
|
return err(wrapError("kv", error));
|
|
@@ -1423,6 +1419,56 @@ var KVService = class extends BaseService {
|
|
|
1423
1419
|
}
|
|
1424
1420
|
});
|
|
1425
1421
|
}
|
|
1422
|
+
async getFromNetwork(key, path, options, signal) {
|
|
1423
|
+
const response = await this.invokeOperation(
|
|
1424
|
+
path,
|
|
1425
|
+
KVAction.GET,
|
|
1426
|
+
void 0,
|
|
1427
|
+
signal,
|
|
1428
|
+
options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
|
|
1429
|
+
);
|
|
1430
|
+
if (!response.ok) {
|
|
1431
|
+
if (response.status === 401 || response.status === 403) {
|
|
1432
|
+
const errorText2 = await this.readAuthorizationText(response);
|
|
1433
|
+
const permissionHint = parsePermissionHintFromErrorText(errorText2);
|
|
1434
|
+
const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
|
|
1435
|
+
service: "kv",
|
|
1436
|
+
code: ErrorCodes.AUTH_UNAUTHORIZED,
|
|
1437
|
+
meta: {
|
|
1438
|
+
status: response.status,
|
|
1439
|
+
resource: `${this.context.session.spaceId}/kv/${path}`,
|
|
1440
|
+
requiredAction: KVAction.GET
|
|
1441
|
+
}
|
|
1442
|
+
}) : void 0;
|
|
1443
|
+
return this.authorizationFailure(
|
|
1444
|
+
`Failed to get key ${JSON.stringify(key)}`,
|
|
1445
|
+
response,
|
|
1446
|
+
errorText2,
|
|
1447
|
+
[path],
|
|
1448
|
+
KVAction.GET,
|
|
1449
|
+
structuredCapability ? { permissionHint, ...structuredCapability } : {}
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
if (response.status === 404) return this.classifyNotFound(response, key);
|
|
1453
|
+
const errorText = await response.text();
|
|
1454
|
+
if (response.status === 413) {
|
|
1455
|
+
return err(serviceError(
|
|
1456
|
+
ErrorCodes.KV_RESPONSE_TOO_LARGE,
|
|
1457
|
+
`KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
|
|
1458
|
+
"kv",
|
|
1459
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1460
|
+
));
|
|
1461
|
+
}
|
|
1462
|
+
return err(serviceError(
|
|
1463
|
+
ErrorCodes.NETWORK_ERROR,
|
|
1464
|
+
`Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
1465
|
+
"kv",
|
|
1466
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1467
|
+
));
|
|
1468
|
+
}
|
|
1469
|
+
const data = await this.parseResponse(response, options?.raw, options?.binary);
|
|
1470
|
+
return ok({ data, headers: this.createResponseHeaders(response.headers) });
|
|
1471
|
+
}
|
|
1426
1472
|
async batchGet(keys, options) {
|
|
1427
1473
|
return this.withTelemetry(
|
|
1428
1474
|
"batchGet",
|
|
@@ -1435,6 +1481,7 @@ var KVService = class extends BaseService {
|
|
|
1435
1481
|
*/
|
|
1436
1482
|
async put(key, value, options) {
|
|
1437
1483
|
return this.withTelemetry("put", key, async () => {
|
|
1484
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
1438
1485
|
if (!this.requireAuth()) {
|
|
1439
1486
|
return err(authRequiredError("kv"));
|
|
1440
1487
|
}
|
|
@@ -1449,63 +1496,15 @@ var KVService = class extends BaseService {
|
|
|
1449
1496
|
const body = this.serializePutValue(value, options?.contentType);
|
|
1450
1497
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1451
1498
|
try {
|
|
1452
|
-
const
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
{
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
);
|
|
1462
|
-
if (response.status === 401 || response.status === 403) {
|
|
1463
|
-
return this.authorizationFailure(
|
|
1464
|
-
`Failed to put key ${JSON.stringify(key)}`,
|
|
1465
|
-
response,
|
|
1466
|
-
await this.readAuthorizationText(response),
|
|
1467
|
-
[path],
|
|
1468
|
-
KVAction.PUT
|
|
1469
|
-
);
|
|
1470
|
-
}
|
|
1471
|
-
if (!response.ok) {
|
|
1472
|
-
const errorText = await response.text();
|
|
1473
|
-
if (response.status === 412) {
|
|
1474
|
-
return err(serviceError(
|
|
1475
|
-
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
1476
|
-
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
1477
|
-
"kv",
|
|
1478
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1479
|
-
));
|
|
1480
|
-
}
|
|
1481
|
-
if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
|
|
1482
|
-
return err(serviceError(
|
|
1483
|
-
ErrorCodes.KV_CONFLICT,
|
|
1484
|
-
`Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
|
|
1485
|
-
"kv",
|
|
1486
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1487
|
-
));
|
|
1488
|
-
}
|
|
1489
|
-
const quotaError = this.handleQuotaErrorResponse(
|
|
1490
|
-
response,
|
|
1491
|
-
errorText,
|
|
1492
|
-
key
|
|
1493
|
-
);
|
|
1494
|
-
if (quotaError) {
|
|
1495
|
-
return quotaError;
|
|
1496
|
-
}
|
|
1497
|
-
return err(
|
|
1498
|
-
serviceError(
|
|
1499
|
-
ErrorCodes.KV_WRITE_FAILED,
|
|
1500
|
-
`Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
1501
|
-
"kv",
|
|
1502
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1503
|
-
)
|
|
1504
|
-
);
|
|
1505
|
-
}
|
|
1506
|
-
return ok({
|
|
1507
|
-
data: void 0,
|
|
1508
|
-
headers: this.createResponseHeaders(response.headers)
|
|
1499
|
+
const network = () => this.putToNetwork(key, path, body, options, request.signal);
|
|
1500
|
+
if (this.readThrough === null) return await network();
|
|
1501
|
+
return await this.readThrough.write({
|
|
1502
|
+
op: "put",
|
|
1503
|
+
space: this.context.session.spaceId,
|
|
1504
|
+
entries: [{ path, body }],
|
|
1505
|
+
signal: request.signal,
|
|
1506
|
+
conditional: options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0,
|
|
1507
|
+
network
|
|
1509
1508
|
});
|
|
1510
1509
|
} catch (error) {
|
|
1511
1510
|
return err(wrapError("kv", error));
|
|
@@ -1514,11 +1513,64 @@ var KVService = class extends BaseService {
|
|
|
1514
1513
|
}
|
|
1515
1514
|
});
|
|
1516
1515
|
}
|
|
1516
|
+
async putToNetwork(key, path, body, options, signal) {
|
|
1517
|
+
const response = await this.invokeOperation(
|
|
1518
|
+
path,
|
|
1519
|
+
KVAction.PUT,
|
|
1520
|
+
body,
|
|
1521
|
+
signal,
|
|
1522
|
+
{
|
|
1523
|
+
...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
|
|
1524
|
+
...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
|
|
1525
|
+
}
|
|
1526
|
+
);
|
|
1527
|
+
if (response.status === 401 || response.status === 403) {
|
|
1528
|
+
return this.authorizationFailure(
|
|
1529
|
+
`Failed to put key ${JSON.stringify(key)}`,
|
|
1530
|
+
response,
|
|
1531
|
+
await this.readAuthorizationText(response),
|
|
1532
|
+
[path],
|
|
1533
|
+
KVAction.PUT
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
if (!response.ok) {
|
|
1537
|
+
const errorText = await response.text();
|
|
1538
|
+
if (response.status === 412) {
|
|
1539
|
+
return err(serviceError(
|
|
1540
|
+
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
1541
|
+
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
1542
|
+
"kv",
|
|
1543
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1544
|
+
));
|
|
1545
|
+
}
|
|
1546
|
+
if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
|
|
1547
|
+
return err(serviceError(
|
|
1548
|
+
ErrorCodes.KV_CONFLICT,
|
|
1549
|
+
`Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
|
|
1550
|
+
"kv",
|
|
1551
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1552
|
+
));
|
|
1553
|
+
}
|
|
1554
|
+
const quotaError = this.handleQuotaErrorResponse(response, errorText, key);
|
|
1555
|
+
if (quotaError) return quotaError;
|
|
1556
|
+
return err(serviceError(
|
|
1557
|
+
ErrorCodes.KV_WRITE_FAILED,
|
|
1558
|
+
`Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
1559
|
+
"kv",
|
|
1560
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1561
|
+
));
|
|
1562
|
+
}
|
|
1563
|
+
return ok({
|
|
1564
|
+
data: void 0,
|
|
1565
|
+
headers: this.createResponseHeaders(response.headers)
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1517
1568
|
/**
|
|
1518
1569
|
* Store multiple values in one TinyCloud KV invocation.
|
|
1519
1570
|
*/
|
|
1520
1571
|
async batchPut(items, options) {
|
|
1521
1572
|
return this.withTelemetry("batchPut", String(items.length), async () => {
|
|
1573
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
1522
1574
|
if (!this.requireAuth()) {
|
|
1523
1575
|
return err(authRequiredError("kv"));
|
|
1524
1576
|
}
|
|
@@ -1549,155 +1601,177 @@ var KVService = class extends BaseService {
|
|
|
1549
1601
|
}
|
|
1550
1602
|
seen.add(path);
|
|
1551
1603
|
}
|
|
1552
|
-
let requestMayHaveDispatched = false;
|
|
1553
1604
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1554
1605
|
try {
|
|
1555
|
-
const
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
session,
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1606
|
+
const network = () => this.batchPutToNetwork(items, paths, request.signal);
|
|
1607
|
+
if (this.readThrough === null) return await network();
|
|
1608
|
+
const entries = items.map((item, index) => ({
|
|
1609
|
+
path: paths[index],
|
|
1610
|
+
body: this.serializeBatchPutValue(item)
|
|
1611
|
+
}));
|
|
1612
|
+
return await this.readThrough.write({
|
|
1613
|
+
op: "batchPut",
|
|
1614
|
+
space: session.spaceId,
|
|
1615
|
+
entries,
|
|
1616
|
+
signal: request.signal,
|
|
1617
|
+
network: () => this.batchPutToNetwork(items, paths, request.signal, entries)
|
|
1618
|
+
});
|
|
1619
|
+
} catch (error) {
|
|
1620
|
+
const wrapped = wrapError("kv", error);
|
|
1621
|
+
return err({ ...wrapped, meta: { ...wrapped.meta, requestMayHaveDispatched: false } });
|
|
1622
|
+
} finally {
|
|
1623
|
+
request.dispose();
|
|
1624
|
+
}
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
async batchPutToNetwork(items, paths, signal, entries) {
|
|
1628
|
+
const session = this.context.session;
|
|
1629
|
+
let requestMayHaveDispatched = false;
|
|
1630
|
+
try {
|
|
1631
|
+
const body = new FormData();
|
|
1632
|
+
for (let index = 0; index < items.length; index++) {
|
|
1633
|
+
body.append(
|
|
1634
|
+
encodeKvBatchPartName(paths[index]),
|
|
1635
|
+
entries?.[index]?.body ?? this.serializeBatchPutValue(items[index])
|
|
1570
1636
|
);
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
return this.authorizationFailure(
|
|
1598
|
-
`Failed to batch put ${items.length} key(s)`,
|
|
1599
|
-
response,
|
|
1600
|
-
errorText,
|
|
1601
|
-
paths,
|
|
1602
|
-
KVAction.PUT
|
|
1603
|
-
);
|
|
1604
|
-
}
|
|
1605
|
-
const quotaError = this.handleQuotaErrorResponse(
|
|
1606
|
-
response,
|
|
1607
|
-
errorText,
|
|
1608
|
-
"batch"
|
|
1609
|
-
);
|
|
1610
|
-
if (quotaError) {
|
|
1611
|
-
return quotaError;
|
|
1612
|
-
}
|
|
1637
|
+
}
|
|
1638
|
+
const headers = this.context.invokeAny(
|
|
1639
|
+
session,
|
|
1640
|
+
paths.map((path) => ({
|
|
1641
|
+
spaceId: session.spaceId,
|
|
1642
|
+
service: "kv",
|
|
1643
|
+
path,
|
|
1644
|
+
action: KVAction.PUT
|
|
1645
|
+
}))
|
|
1646
|
+
);
|
|
1647
|
+
const url = `${this.host}/invoke`;
|
|
1648
|
+
const init = {
|
|
1649
|
+
method: "POST",
|
|
1650
|
+
headers,
|
|
1651
|
+
body,
|
|
1652
|
+
signal
|
|
1653
|
+
};
|
|
1654
|
+
const fetchFn = this.context.fetch;
|
|
1655
|
+
requestMayHaveDispatched = true;
|
|
1656
|
+
const response = await fetchFn(url, init);
|
|
1657
|
+
if (!response.ok) {
|
|
1658
|
+
let errorText;
|
|
1659
|
+
try {
|
|
1660
|
+
errorText = await response.text();
|
|
1661
|
+
} catch (textError) {
|
|
1662
|
+
const cause = textError instanceof Error ? textError : new Error(String(textError));
|
|
1613
1663
|
return err(
|
|
1614
1664
|
serviceError(
|
|
1615
1665
|
ErrorCodes.KV_WRITE_FAILED,
|
|
1616
|
-
`Failed to batch put ${items.length} key(s): ${response.status} - ${
|
|
1666
|
+
`Failed to batch put ${items.length} key(s): ${response.status} - <response body could not be read: ${cause.message}>`,
|
|
1617
1667
|
"kv",
|
|
1618
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1668
|
+
{ cause, meta: { status: response.status, statusText: response.statusText } }
|
|
1619
1669
|
)
|
|
1620
1670
|
);
|
|
1621
1671
|
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
serviceError(
|
|
1630
|
-
timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
|
|
1631
|
-
timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
|
|
1632
|
-
"kv",
|
|
1633
|
-
{
|
|
1634
|
-
cause,
|
|
1635
|
-
meta: {
|
|
1636
|
-
...timedOut ? { timeoutMs: cause.timeoutMs } : {},
|
|
1637
|
-
requestMayHaveDispatched: true,
|
|
1638
|
-
responseReceived: true,
|
|
1639
|
-
status: response.status,
|
|
1640
|
-
outcome: "batch-unconfirmed"
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
)
|
|
1672
|
+
if (response.status === 401 || response.status === 403) {
|
|
1673
|
+
return this.authorizationFailure(
|
|
1674
|
+
`Failed to batch put ${items.length} key(s)`,
|
|
1675
|
+
response,
|
|
1676
|
+
errorText,
|
|
1677
|
+
paths,
|
|
1678
|
+
KVAction.PUT
|
|
1644
1679
|
);
|
|
1645
1680
|
}
|
|
1646
|
-
const
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
{
|
|
1654
|
-
meta: {
|
|
1655
|
-
requestMayHaveDispatched: true,
|
|
1656
|
-
responseReceived: true,
|
|
1657
|
-
status: response.status,
|
|
1658
|
-
outcome: "batch-unconfirmed"
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
)
|
|
1662
|
-
);
|
|
1681
|
+
const quotaError = this.handleQuotaErrorResponse(
|
|
1682
|
+
response,
|
|
1683
|
+
errorText,
|
|
1684
|
+
"batch"
|
|
1685
|
+
);
|
|
1686
|
+
if (quotaError) {
|
|
1687
|
+
return quotaError;
|
|
1663
1688
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1689
|
+
return err(
|
|
1690
|
+
serviceError(
|
|
1691
|
+
ErrorCodes.KV_WRITE_FAILED,
|
|
1692
|
+
`Failed to batch put ${items.length} key(s): ${response.status} - ${errorText}`,
|
|
1693
|
+
"kv",
|
|
1694
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1695
|
+
)
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
let rawBody;
|
|
1699
|
+
try {
|
|
1700
|
+
rawBody = await response.json();
|
|
1701
|
+
} catch (jsonError) {
|
|
1702
|
+
const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
|
|
1703
|
+
const timedOut = isRequestTimeoutError(cause);
|
|
1704
|
+
return err(
|
|
1705
|
+
serviceError(
|
|
1706
|
+
timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
|
|
1707
|
+
timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
|
|
1708
|
+
"kv",
|
|
1709
|
+
{
|
|
1710
|
+
cause,
|
|
1711
|
+
meta: {
|
|
1712
|
+
...timedOut ? { timeoutMs: cause.timeoutMs } : {},
|
|
1713
|
+
requestMayHaveDispatched: true,
|
|
1714
|
+
responseReceived: true,
|
|
1715
|
+
status: response.status,
|
|
1716
|
+
outcome: "batch-unconfirmed"
|
|
1680
1717
|
}
|
|
1681
|
-
|
|
1682
|
-
)
|
|
1683
|
-
|
|
1684
|
-
return ok(batchResponse);
|
|
1685
|
-
} catch (error) {
|
|
1686
|
-
const wrapped = wrapError("kv", error);
|
|
1687
|
-
return err({
|
|
1688
|
-
...wrapped,
|
|
1689
|
-
meta: { ...wrapped.meta, requestMayHaveDispatched }
|
|
1690
|
-
});
|
|
1691
|
-
} finally {
|
|
1692
|
-
request.dispose();
|
|
1718
|
+
}
|
|
1719
|
+
)
|
|
1720
|
+
);
|
|
1693
1721
|
}
|
|
1694
|
-
|
|
1722
|
+
const batchResponse = this.normalizeBatchPutResponse(rawBody);
|
|
1723
|
+
if (!batchResponse) {
|
|
1724
|
+
return err(
|
|
1725
|
+
serviceError(
|
|
1726
|
+
ErrorCodes.NETWORK_ERROR,
|
|
1727
|
+
"KV batchPut response did not include matching written keys and count",
|
|
1728
|
+
"kv",
|
|
1729
|
+
{
|
|
1730
|
+
meta: {
|
|
1731
|
+
requestMayHaveDispatched: true,
|
|
1732
|
+
responseReceived: true,
|
|
1733
|
+
status: response.status,
|
|
1734
|
+
outcome: "batch-unconfirmed"
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
)
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
const requestedPaths = new Set(paths);
|
|
1741
|
+
const writtenPaths = new Set(batchResponse.written);
|
|
1742
|
+
const matchesRequest = batchResponse.count === paths.length && batchResponse.written.length === paths.length && writtenPaths.size === paths.length && batchResponse.written.every((key) => requestedPaths.has(key));
|
|
1743
|
+
if (!matchesRequest) {
|
|
1744
|
+
return err(
|
|
1745
|
+
serviceError(
|
|
1746
|
+
ErrorCodes.NETWORK_ERROR,
|
|
1747
|
+
`KV batchPut response did not confirm all ${paths.length} requested key(s) were written`,
|
|
1748
|
+
"kv",
|
|
1749
|
+
{
|
|
1750
|
+
meta: {
|
|
1751
|
+
requestMayHaveDispatched: true,
|
|
1752
|
+
responseReceived: true,
|
|
1753
|
+
status: response.status,
|
|
1754
|
+
outcome: "batch-unconfirmed"
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
)
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
return ok(batchResponse);
|
|
1761
|
+
} catch (error) {
|
|
1762
|
+
const wrapped = wrapError("kv", error);
|
|
1763
|
+
return err({
|
|
1764
|
+
...wrapped,
|
|
1765
|
+
meta: { ...wrapped.meta, requestMayHaveDispatched }
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1695
1768
|
}
|
|
1696
1769
|
/**
|
|
1697
1770
|
* List keys with optional prefix filtering.
|
|
1698
1771
|
*/
|
|
1699
1772
|
async list(options) {
|
|
1700
1773
|
return this.withTelemetry("list", options?.prefix, async () => {
|
|
1774
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
1701
1775
|
if (!this.requireAuth()) {
|
|
1702
1776
|
return err(authRequiredError("kv"));
|
|
1703
1777
|
}
|
|
@@ -1714,49 +1788,44 @@ var KVService = class extends BaseService {
|
|
|
1714
1788
|
}
|
|
1715
1789
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1716
1790
|
try {
|
|
1717
|
-
const
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1791
|
+
const network = () => this.listFromNetwork(listPath, options, request.signal);
|
|
1792
|
+
let page;
|
|
1793
|
+
if (options?.source === "network") {
|
|
1794
|
+
const observer = this.readThrough;
|
|
1795
|
+
if (observer === null) {
|
|
1796
|
+
page = await network();
|
|
1797
|
+
} else {
|
|
1798
|
+
let outcome = "error";
|
|
1799
|
+
const startedAt = Date.now();
|
|
1800
|
+
try {
|
|
1801
|
+
page = await network();
|
|
1802
|
+
outcome = page.ok ? "found" : "error";
|
|
1803
|
+
} finally {
|
|
1804
|
+
try {
|
|
1805
|
+
observer.observeNetworkRequested({
|
|
1806
|
+
op: "list",
|
|
1807
|
+
space: this.context.session.spaceId,
|
|
1808
|
+
path: listPath,
|
|
1809
|
+
reason: "NETWORK_REQUESTED",
|
|
1810
|
+
outcome,
|
|
1811
|
+
latencyMs: Date.now() - startedAt
|
|
1812
|
+
});
|
|
1813
|
+
} catch {
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1736
1816
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
let keys = await this.parseResponse(response, options?.raw);
|
|
1748
|
-
keys = keys ?? [];
|
|
1749
|
-
if (options?.removePrefix && listPath) {
|
|
1750
|
-
const prefixWithSlash = listPath.endsWith("/") ? listPath : `${listPath}/`;
|
|
1751
|
-
keys = keys.map(
|
|
1752
|
-
(key) => key.startsWith(prefixWithSlash) ? key.slice(prefixWithSlash.length) : key
|
|
1753
|
-
);
|
|
1817
|
+
} else if (this.readThrough === null) {
|
|
1818
|
+
page = await network();
|
|
1819
|
+
} else {
|
|
1820
|
+
page = await this.readThrough.list({
|
|
1821
|
+
space: this.context.session.spaceId,
|
|
1822
|
+
listPath,
|
|
1823
|
+
options,
|
|
1824
|
+
signal: request.signal,
|
|
1825
|
+
network
|
|
1826
|
+
});
|
|
1754
1827
|
}
|
|
1755
|
-
return ok(
|
|
1756
|
-
keys,
|
|
1757
|
-
...response.headers.get("x-tinycloud-truncated") === null ? {} : { truncated: response.headers.get("x-tinycloud-truncated") === "true" },
|
|
1758
|
-
...response.headers.get("x-tinycloud-next-cursor") === null ? {} : { nextCursor: response.headers.get("x-tinycloud-next-cursor") ?? void 0 }
|
|
1759
|
-
});
|
|
1828
|
+
return page.ok ? this.finishList(page.data, listPath, options?.removePrefix) : page;
|
|
1760
1829
|
} catch (error) {
|
|
1761
1830
|
return err(wrapError("kv", error));
|
|
1762
1831
|
} finally {
|
|
@@ -1764,6 +1833,56 @@ var KVService = class extends BaseService {
|
|
|
1764
1833
|
}
|
|
1765
1834
|
});
|
|
1766
1835
|
}
|
|
1836
|
+
async listFromNetwork(listPath, options, signal) {
|
|
1837
|
+
const response = await this.invokeOperation(
|
|
1838
|
+
listPath,
|
|
1839
|
+
KVAction.LIST,
|
|
1840
|
+
void 0,
|
|
1841
|
+
signal,
|
|
1842
|
+
options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
|
|
1843
|
+
...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
|
|
1844
|
+
...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
|
|
1845
|
+
}
|
|
1846
|
+
);
|
|
1847
|
+
if (!response.ok) {
|
|
1848
|
+
if (response.status === 401 || response.status === 403) {
|
|
1849
|
+
return this.authorizationFailure(
|
|
1850
|
+
"Failed to list keys",
|
|
1851
|
+
response,
|
|
1852
|
+
await this.readAuthorizationText(response),
|
|
1853
|
+
[listPath],
|
|
1854
|
+
KVAction.LIST
|
|
1855
|
+
);
|
|
1856
|
+
}
|
|
1857
|
+
const errorText = await response.text();
|
|
1858
|
+
return err(serviceError(
|
|
1859
|
+
ErrorCodes.NETWORK_ERROR,
|
|
1860
|
+
`Failed to list keys: ${response.status} - ${errorText}`,
|
|
1861
|
+
"kv",
|
|
1862
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1863
|
+
));
|
|
1864
|
+
}
|
|
1865
|
+
const keys = await this.parseResponse(response, options?.raw) ?? [];
|
|
1866
|
+
return ok({
|
|
1867
|
+
keys,
|
|
1868
|
+
...response.headers.get("x-tinycloud-truncated") === null ? {} : { truncated: response.headers.get("x-tinycloud-truncated") === "true" },
|
|
1869
|
+
...response.headers.get("x-tinycloud-next-cursor") === null ? {} : { nextCursor: response.headers.get("x-tinycloud-next-cursor") ?? void 0 }
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
finishList(page, listPath, removePrefix) {
|
|
1873
|
+
let keys = page.keys;
|
|
1874
|
+
if (removePrefix && listPath) {
|
|
1875
|
+
const prefixWithSlash = listPath.endsWith("/") ? listPath : `${listPath}/`;
|
|
1876
|
+
keys = keys.map(
|
|
1877
|
+
(key) => key.startsWith(prefixWithSlash) ? key.slice(prefixWithSlash.length) : key
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
return ok({
|
|
1881
|
+
keys,
|
|
1882
|
+
...page.truncated === void 0 ? {} : { truncated: page.truncated },
|
|
1883
|
+
...page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor }
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1767
1886
|
/**
|
|
1768
1887
|
* Read one page of the `tinycloud.kv/sync` change feed for a prefix.
|
|
1769
1888
|
*/
|
|
@@ -1786,6 +1905,7 @@ var KVService = class extends BaseService {
|
|
|
1786
1905
|
"kv"
|
|
1787
1906
|
));
|
|
1788
1907
|
}
|
|
1908
|
+
const requestPrefix = options.prefix === "/" ? "" : options.prefix;
|
|
1789
1909
|
const headers = {};
|
|
1790
1910
|
if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
|
|
1791
1911
|
if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
|
|
@@ -1795,7 +1915,7 @@ var KVService = class extends BaseService {
|
|
|
1795
1915
|
const request = this.createRequestSignal(options.signal, options.timeout);
|
|
1796
1916
|
try {
|
|
1797
1917
|
const response = await this.invokeOperation(
|
|
1798
|
-
|
|
1918
|
+
requestPrefix,
|
|
1799
1919
|
KVAction.SYNC,
|
|
1800
1920
|
void 0,
|
|
1801
1921
|
request.signal,
|
|
@@ -1811,7 +1931,7 @@ var KVService = class extends BaseService {
|
|
|
1811
1931
|
} catch {
|
|
1812
1932
|
body = void 0;
|
|
1813
1933
|
}
|
|
1814
|
-
const page = this.normalizeChangesResponse(body);
|
|
1934
|
+
const page = this.normalizeChangesResponse(body, options.prefix);
|
|
1815
1935
|
if (!page) {
|
|
1816
1936
|
return err(serviceError(
|
|
1817
1937
|
ErrorCodes.NETWORK_ERROR,
|
|
@@ -1891,7 +2011,7 @@ var KVService = class extends BaseService {
|
|
|
1891
2011
|
{ meta }
|
|
1892
2012
|
));
|
|
1893
2013
|
}
|
|
1894
|
-
normalizeChangesResponse(data) {
|
|
2014
|
+
normalizeChangesResponse(data, requestedPrefix) {
|
|
1895
2015
|
if (!isJsonObject(data)) return void 0;
|
|
1896
2016
|
const { changes, more, cursor, source, authority } = data;
|
|
1897
2017
|
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)) {
|
|
@@ -1923,7 +2043,11 @@ var KVService = class extends BaseService {
|
|
|
1923
2043
|
changes: parsed,
|
|
1924
2044
|
more,
|
|
1925
2045
|
cursor,
|
|
1926
|
-
source: {
|
|
2046
|
+
source: {
|
|
2047
|
+
nodeDid: source.nodeDid,
|
|
2048
|
+
space: source.space,
|
|
2049
|
+
prefix: requestedPrefix === "/" && source.prefix === "" ? "/" : source.prefix
|
|
2050
|
+
},
|
|
1927
2051
|
authority: { notBefore, expiresAt, retainUntil }
|
|
1928
2052
|
};
|
|
1929
2053
|
}
|
|
@@ -1932,61 +2056,22 @@ var KVService = class extends BaseService {
|
|
|
1932
2056
|
*/
|
|
1933
2057
|
async delete(key, options) {
|
|
1934
2058
|
return this.withTelemetry("delete", key, async () => {
|
|
2059
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
1935
2060
|
if (!this.requireAuth()) {
|
|
1936
2061
|
return err(authRequiredError("kv"));
|
|
1937
2062
|
}
|
|
1938
2063
|
const path = this.getFullPath(key, options?.prefix);
|
|
1939
2064
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
1940
2065
|
try {
|
|
1941
|
-
const
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
return this.authorizationFailure(
|
|
1951
|
-
`Failed to delete key ${JSON.stringify(key)}`,
|
|
1952
|
-
response,
|
|
1953
|
-
await this.readAuthorizationText(response),
|
|
1954
|
-
[path],
|
|
1955
|
-
KVAction.DELETE
|
|
1956
|
-
);
|
|
1957
|
-
}
|
|
1958
|
-
if (response.status === 404) {
|
|
1959
|
-
return await this.classifyNotFound(response, key);
|
|
1960
|
-
}
|
|
1961
|
-
const errorText = await response.text();
|
|
1962
|
-
if (response.status === 412) {
|
|
1963
|
-
return err(serviceError(
|
|
1964
|
-
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
1965
|
-
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
1966
|
-
"kv",
|
|
1967
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1968
|
-
));
|
|
1969
|
-
}
|
|
1970
|
-
if (response.status === 503 && options?.ifMatch !== void 0) {
|
|
1971
|
-
return err(serviceError(
|
|
1972
|
-
ErrorCodes.KV_CONFLICT,
|
|
1973
|
-
`Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
|
|
1974
|
-
"kv",
|
|
1975
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1976
|
-
));
|
|
1977
|
-
}
|
|
1978
|
-
return err(
|
|
1979
|
-
serviceError(
|
|
1980
|
-
ErrorCodes.NETWORK_ERROR,
|
|
1981
|
-
`Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
1982
|
-
"kv",
|
|
1983
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1984
|
-
)
|
|
1985
|
-
);
|
|
1986
|
-
}
|
|
1987
|
-
return ok({
|
|
1988
|
-
data: void 0,
|
|
1989
|
-
headers: this.createResponseHeaders(response.headers)
|
|
2066
|
+
const network = () => this.deleteFromNetwork(key, path, options, request.signal);
|
|
2067
|
+
if (this.readThrough === null) return await network();
|
|
2068
|
+
return await this.readThrough.write({
|
|
2069
|
+
op: "delete",
|
|
2070
|
+
space: this.context.session.spaceId,
|
|
2071
|
+
entries: [{ path }],
|
|
2072
|
+
signal: request.signal,
|
|
2073
|
+
conditional: options?.ifMatch !== void 0,
|
|
2074
|
+
network
|
|
1990
2075
|
});
|
|
1991
2076
|
} catch (error) {
|
|
1992
2077
|
return err(wrapError("kv", error));
|
|
@@ -1995,6 +2080,54 @@ var KVService = class extends BaseService {
|
|
|
1995
2080
|
}
|
|
1996
2081
|
});
|
|
1997
2082
|
}
|
|
2083
|
+
async deleteFromNetwork(key, path, options, signal) {
|
|
2084
|
+
const response = await this.invokeOperation(
|
|
2085
|
+
path,
|
|
2086
|
+
KVAction.DELETE,
|
|
2087
|
+
void 0,
|
|
2088
|
+
signal,
|
|
2089
|
+
options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
|
|
2090
|
+
);
|
|
2091
|
+
if (!response.ok) {
|
|
2092
|
+
if (response.status === 401 || response.status === 403) {
|
|
2093
|
+
return this.authorizationFailure(
|
|
2094
|
+
`Failed to delete key ${JSON.stringify(key)}`,
|
|
2095
|
+
response,
|
|
2096
|
+
await this.readAuthorizationText(response),
|
|
2097
|
+
[path],
|
|
2098
|
+
KVAction.DELETE
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
2101
|
+
if (response.status === 404) return this.classifyNotFound(response, key);
|
|
2102
|
+
const errorText = await response.text();
|
|
2103
|
+
if (response.status === 412) {
|
|
2104
|
+
return err(serviceError(
|
|
2105
|
+
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
2106
|
+
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
2107
|
+
"kv",
|
|
2108
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2109
|
+
));
|
|
2110
|
+
}
|
|
2111
|
+
if (response.status === 503 && options?.ifMatch !== void 0) {
|
|
2112
|
+
return err(serviceError(
|
|
2113
|
+
ErrorCodes.KV_CONFLICT,
|
|
2114
|
+
`Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
|
|
2115
|
+
"kv",
|
|
2116
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2117
|
+
));
|
|
2118
|
+
}
|
|
2119
|
+
return err(serviceError(
|
|
2120
|
+
ErrorCodes.NETWORK_ERROR,
|
|
2121
|
+
`Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
2122
|
+
"kv",
|
|
2123
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2124
|
+
));
|
|
2125
|
+
}
|
|
2126
|
+
return ok({
|
|
2127
|
+
data: void 0,
|
|
2128
|
+
headers: this.createResponseHeaders(response.headers)
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
1998
2131
|
/**
|
|
1999
2132
|
* Get metadata for a key without retrieving the value.
|
|
2000
2133
|
*/
|
|
@@ -2161,10 +2294,74 @@ var KVService = class extends BaseService {
|
|
|
2161
2294
|
* Service identifier for registration.
|
|
2162
2295
|
*/
|
|
2163
2296
|
KVService.serviceName = "kv";
|
|
2297
|
+
|
|
2298
|
+
// src/kv/replication/constants.ts
|
|
2299
|
+
var CLEAR_PENDING_WARNING = "Clearing pending writes surrenders read-your-writes protection for the cleared keys. If a write whose record was cleared had an unknown outcome or never finished, it may commit now or later; this device can serve an older value until the next sync after that commit, as it would for a write from another device.";
|
|
2300
|
+
|
|
2301
|
+
// src/kv/replication/identity.ts
|
|
2302
|
+
var SPACE_ID = /^(tinycloud:pkh:eip155:\d+:)([^:]+)(:.+)$/;
|
|
2303
|
+
var PRINCIPAL_DID = /^(did:pkh:eip155:\d+:)([^:]+)$/;
|
|
2304
|
+
function canonicalReplicationIdentity(raw) {
|
|
2305
|
+
if (!raw || typeof raw.host !== "string" || typeof raw.space !== "string" || typeof raw.principal !== "string") {
|
|
2306
|
+
throw new TypeError(
|
|
2307
|
+
"Replication identity host, space, and principal must be strings"
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
const authority = /^https?:\/\/([^/?#]*)/i.exec(raw.host)?.[1];
|
|
2311
|
+
let url;
|
|
2312
|
+
try {
|
|
2313
|
+
url = new URL(raw.host);
|
|
2314
|
+
} catch {
|
|
2315
|
+
throw new TypeError(
|
|
2316
|
+
"Replication identity host must be an absolute HTTP(S) URL"
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
if (!authority || authority.includes("@") || raw.host.includes("?") || raw.host.includes("#") || url.username || url.password) {
|
|
2320
|
+
throw new TypeError(
|
|
2321
|
+
"Replication identity host must be HTTP(S) without credentials, query, or fragment"
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
const space = SPACE_ID.exec(raw.space);
|
|
2325
|
+
const principal = PRINCIPAL_DID.exec(raw.principal);
|
|
2326
|
+
if (!space || !principal) {
|
|
2327
|
+
throw new TypeError(
|
|
2328
|
+
"Replication identity requires a full TinyCloud space id and a pkh principal DID"
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
return {
|
|
2332
|
+
host: `${url.origin}${url.pathname.replace(/\/+$/, "")}`,
|
|
2333
|
+
space: `${space[1]}${space[2].toLowerCase()}${space[3]}`,
|
|
2334
|
+
principal: `${principal[1]}${principal[2].toLowerCase()}`
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
function replicationIdentityKey(id) {
|
|
2338
|
+
const canonical = canonicalReplicationIdentity(id);
|
|
2339
|
+
return `tc-replication/v1
|
|
2340
|
+
${canonical.host}
|
|
2341
|
+
${canonical.space}
|
|
2342
|
+
${canonical.principal}`;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// src/kv/replication/prefix.ts
|
|
2346
|
+
function kvPrefixCovers(prefix, key) {
|
|
2347
|
+
return prefix === "/" || key.startsWith(prefix);
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
// src/kv/replication/scope.ts
|
|
2351
|
+
function requiresSecretsOptIn(space, prefix) {
|
|
2352
|
+
if (prefix === "/") return true;
|
|
2353
|
+
if (space === "secrets" || space.endsWith(":secrets")) return true;
|
|
2354
|
+
return prefix.split("/", 1)[0] === "vault";
|
|
2355
|
+
}
|
|
2164
2356
|
export {
|
|
2357
|
+
CLEAR_PENDING_WARNING,
|
|
2165
2358
|
DEFAULT_SIGNED_READ_URL_EXPIRY_MS,
|
|
2166
2359
|
KVAction,
|
|
2167
2360
|
KVService,
|
|
2168
|
-
PrefixedKVService
|
|
2361
|
+
PrefixedKVService,
|
|
2362
|
+
canonicalReplicationIdentity,
|
|
2363
|
+
kvPrefixCovers,
|
|
2364
|
+
replicationIdentityKey,
|
|
2365
|
+
requiresSecretsOptIn
|
|
2169
2366
|
};
|
|
2170
2367
|
//# sourceMappingURL=index.js.map
|