@tinycloud/sdk-services 3.2.6-beta.2 → 3.3.0-beta.11
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-4wwJq1Gn.d.cts → BaseService-Cc6Ueicm.d.cts} +1 -1
- package/dist/{BaseService-BGr2slUz.d.ts → BaseService-DOdXd7sr.d.ts} +1 -1
- package/dist/encryption/index.cjs +10 -0
- 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 +10 -0
- package/dist/encryption/index.js.map +1 -1
- package/dist/index.cjs +360 -210
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +116 -13
- package/dist/index.d.ts +116 -13
- package/dist/index.js +359 -210
- 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 +68 -21
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +7 -4
- package/dist/kv/index.d.ts +7 -4
- package/dist/kv/index.js +68 -21
- package/dist/kv/index.js.map +1 -1
- package/dist/kv/replication/contractConstants.cjs +115 -0
- package/dist/kv/replication/contractConstants.cjs.map +1 -0
- package/dist/kv/replication/contractConstants.d.cts +77 -0
- package/dist/kv/replication/contractConstants.d.ts +77 -0
- package/dist/kv/replication/contractConstants.js +75 -0
- package/dist/kv/replication/contractConstants.js.map +1 -0
- package/dist/kv/replication/grantPolicy.cjs +82 -0
- package/dist/kv/replication/grantPolicy.cjs.map +1 -0
- package/dist/kv/replication/grantPolicy.d.cts +19 -0
- package/dist/kv/replication/grantPolicy.d.ts +19 -0
- package/dist/kv/replication/grantPolicy.js +50 -0
- package/dist/kv/replication/grantPolicy.js.map +1 -0
- package/dist/kv/replication/grantPolicyConstants.cjs +42 -0
- package/dist/kv/replication/grantPolicyConstants.cjs.map +1 -0
- package/dist/kv/replication/grantPolicyConstants.d.cts +10 -0
- package/dist/kv/replication/grantPolicyConstants.d.ts +10 -0
- package/dist/kv/replication/grantPolicyConstants.js +14 -0
- package/dist/kv/replication/grantPolicyConstants.js.map +1 -0
- package/dist/kv/replication/grantPolicyTypes.cjs +19 -0
- package/dist/kv/replication/grantPolicyTypes.cjs.map +1 -0
- package/dist/kv/replication/grantPolicyTypes.d.cts +87 -0
- package/dist/kv/replication/grantPolicyTypes.d.ts +87 -0
- package/dist/kv/replication/grantPolicyTypes.js +1 -0
- package/dist/kv/replication/grantPolicyTypes.js.map +1 -0
- package/dist/kv/replication/index.cjs +575 -131
- package/dist/kv/replication/index.cjs.map +1 -1
- package/dist/kv/replication/index.d.cts +6 -3
- package/dist/kv/replication/index.d.ts +6 -3
- package/dist/kv/replication/index.js +575 -131
- package/dist/kv/replication/index.js.map +1 -1
- package/dist/scope-B-m6cKH-.d.ts +1962 -0
- package/dist/scope-B7oeKdLM.d.cts +1962 -0
- package/dist/sql/index.cjs +72 -36
- 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 +72 -36
- package/dist/sql/index.js.map +1 -1
- package/dist/{types-D-9A3Sut.d.cts → types-BjH0ZtGy.d.cts} +8 -0
- package/dist/{types-D-9A3Sut.d.ts → types-BjH0ZtGy.d.ts} +8 -0
- package/package.json +55 -2
- package/dist/scope-CoKKmMiI.d.ts +0 -957
- package/dist/scope-D7wmjbMH.d.cts +0 -957
|
@@ -79,6 +79,14 @@ var ErrorCodes = {
|
|
|
79
79
|
ABORTED: "ABORTED",
|
|
80
80
|
INVALID_INPUT: "INVALID_INPUT",
|
|
81
81
|
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
82
|
+
// Node retry signals (tinycloud-node #347). `meta.retryable` says whether a
|
|
83
|
+
// retry is safe; `meta.retryAfterMs` carries the node's Retry-After.
|
|
84
|
+
/** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`: nothing was applied; retry after `meta.retryAfterMs`. */
|
|
85
|
+
NODE_BUSY: "NODE_BUSY",
|
|
86
|
+
/** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */
|
|
87
|
+
REQUEST_TIMEOUT: "REQUEST_TIMEOUT",
|
|
88
|
+
/** HTTP 500 `outcome_unknown`: a write may have been applied; read back before retrying a non-idempotent operation. */
|
|
89
|
+
OUTCOME_UNKNOWN: "OUTCOME_UNKNOWN",
|
|
82
90
|
// KV-specific errors
|
|
83
91
|
KV_NOT_FOUND: "KV_NOT_FOUND",
|
|
84
92
|
KV_WRITE_FAILED: "KV_WRITE_FAILED",
|
|
@@ -372,6 +380,35 @@ function emitEvent(emit, scheduler, event) {
|
|
|
372
380
|
}
|
|
373
381
|
}
|
|
374
382
|
|
|
383
|
+
// src/kv/replication/grantPolicyConstants.ts
|
|
384
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
385
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
386
|
+
var DEFAULT_GRANT_TTL_MS = DAY_MS;
|
|
387
|
+
var MAX_GRANT_TTL_MS = 7 * DAY_MS;
|
|
388
|
+
var CONSENT_WINDOW_MS = 7 * DAY_MS;
|
|
389
|
+
var RENEW_BELOW_FRACTION = 0.25;
|
|
390
|
+
|
|
391
|
+
// src/kv/replication/grantPolicy.ts
|
|
392
|
+
var EXPIRED = Number.NEGATIVE_INFINITY;
|
|
393
|
+
var consentDeadlineMs = (anchor) => Number.isFinite(anchor.at) ? anchor.at + CONSENT_WINDOW_MS : EXPIRED;
|
|
394
|
+
var clampConsentAnchor = (anchor, now) => {
|
|
395
|
+
if (anchor === null || !Number.isFinite(anchor.at) || !Number.isFinite(now)) return null;
|
|
396
|
+
return anchor.at > now ? { at: now, source: anchor.source } : anchor;
|
|
397
|
+
};
|
|
398
|
+
var mintRemainingMs = ({ now, grantTtlMs, consentDeadline }) => {
|
|
399
|
+
if (!Number.isFinite(now) || !Number.isFinite(grantTtlMs) || !Number.isFinite(consentDeadline)) return 0;
|
|
400
|
+
return Math.min(Math.min(grantTtlMs, MAX_GRANT_TTL_MS), consentDeadline - now);
|
|
401
|
+
};
|
|
402
|
+
var needsSilentRenewal = ({ now, installedAt, effectiveExpiresAt, consentDeadline }) => {
|
|
403
|
+
if (!Number.isFinite(now) || !Number.isFinite(installedAt) || !Number.isFinite(effectiveExpiresAt) || !Number.isFinite(consentDeadline)) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
if (!(now < consentDeadline)) return false;
|
|
407
|
+
const remaining = effectiveExpiresAt - now;
|
|
408
|
+
const lifetime = effectiveExpiresAt - installedAt;
|
|
409
|
+
return remaining < lifetime * RENEW_BELOW_FRACTION;
|
|
410
|
+
};
|
|
411
|
+
|
|
375
412
|
// src/kv/replication/controller.ts
|
|
376
413
|
var CLOSE_TIMEOUT_MS = 3e3;
|
|
377
414
|
var STATUS_REFRESH_TIMEOUT_MS = CLOSE_TIMEOUT_MS;
|
|
@@ -399,6 +436,128 @@ function createKVReplication(deps) {
|
|
|
399
436
|
let previous = deps.previous;
|
|
400
437
|
let pinnedStateReported = false;
|
|
401
438
|
const inProcessProof = /* @__PURE__ */ new Set();
|
|
439
|
+
let anchorUnavailable = false;
|
|
440
|
+
let consentAnchor = null;
|
|
441
|
+
try {
|
|
442
|
+
consentAnchor = clampConsentAnchor(authority.consentAnchor?.() ?? null, scheduler.now());
|
|
443
|
+
if (consentAnchor?.source === "first_install") consentAnchor = null;
|
|
444
|
+
} catch {
|
|
445
|
+
anchorUnavailable = true;
|
|
446
|
+
}
|
|
447
|
+
const grantPolicy = { grantTtlMs: options.grantTtlMs ?? DEFAULT_GRANT_TTL_MS, consentAnchor };
|
|
448
|
+
const quota = options.maxBytes === void 0 && options.minFreeBytes === void 0 ? void 0 : { maxBytes: options.maxBytes, minFreeBytes: options.minFreeBytes };
|
|
449
|
+
const policyDeadline = consentAnchor === null ? null : consentDeadlineMs(consentAnchor);
|
|
450
|
+
let scheduledConsentDeadline = null;
|
|
451
|
+
let cancelConsentTimer;
|
|
452
|
+
function durableDeadline() {
|
|
453
|
+
if (storage.kind !== "indexeddb" || !deps.stamp) return null;
|
|
454
|
+
try {
|
|
455
|
+
const deadline = storage.lifecycle?.consentDeadline(deps.stamp);
|
|
456
|
+
return typeof deadline === "number" && Number.isFinite(deadline) ? deadline : null;
|
|
457
|
+
} catch {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function mintDeadline() {
|
|
462
|
+
if (policyDeadline === null) return null;
|
|
463
|
+
if (storage.kind !== "indexeddb") return policyDeadline;
|
|
464
|
+
const durable = durableDeadline();
|
|
465
|
+
return durable === null ? null : Math.min(policyDeadline, durable);
|
|
466
|
+
}
|
|
467
|
+
function scheduleConsentDeadline() {
|
|
468
|
+
const deadline = durableDeadline();
|
|
469
|
+
if (deadline === null || consentDenyStarted || isClosed) return;
|
|
470
|
+
const now = scheduler.now();
|
|
471
|
+
if (!Number.isFinite(now)) return;
|
|
472
|
+
if (scheduledConsentDeadline === deadline && cancelConsentTimer) return;
|
|
473
|
+
scheduledConsentDeadline = deadline;
|
|
474
|
+
cancelConsentTimer?.();
|
|
475
|
+
cancelConsentTimer = scheduler.setTimeout(() => {
|
|
476
|
+
cancelConsentTimer = void 0;
|
|
477
|
+
checkDelivery();
|
|
478
|
+
}, Math.max(0, deadline - now));
|
|
479
|
+
}
|
|
480
|
+
const reportedDeadline = /* @__PURE__ */ new Set();
|
|
481
|
+
let consentDenyStarted = false;
|
|
482
|
+
function checkDelivery() {
|
|
483
|
+
const gate = storage.lifecycle;
|
|
484
|
+
if (!gate) return storage.kind === "indexeddb" || anchorUnavailable || deps.stamp !== void 0 ? { status: "gated", gate: "gate_unavailable" } : { status: "allow" };
|
|
485
|
+
if (!deps.stamp) return { status: "gated", gate: "not_established" };
|
|
486
|
+
if (consentDenyStarted) return { status: "gated", gate: "stamp_stale" };
|
|
487
|
+
try {
|
|
488
|
+
const decision = gate.checkDelivery(deps.stamp);
|
|
489
|
+
if (decision.status === "consent_expired") startConsentDenial();
|
|
490
|
+
if (decision.status !== "allow") return decision;
|
|
491
|
+
if (storage.kind === "indexeddb") {
|
|
492
|
+
const deadline = durableDeadline();
|
|
493
|
+
if (deadline === null) return { status: "gated", gate: "gate_unavailable" };
|
|
494
|
+
const now = scheduler.now();
|
|
495
|
+
if (!Number.isFinite(now)) return { status: "gated", gate: "gate_unavailable" };
|
|
496
|
+
if (now >= deadline) {
|
|
497
|
+
startConsentDenial();
|
|
498
|
+
return { status: "consent_expired", code: "CONSENT_EXPIRED" };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return anchorUnavailable ? { status: "gated", gate: "gate_unavailable" } : decision;
|
|
502
|
+
} catch {
|
|
503
|
+
return { status: "gated", gate: "gate_unavailable" };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function markConsentExpired() {
|
|
507
|
+
if (consentDenyStarted || isClosed || purgeStarted) return false;
|
|
508
|
+
cancelConsentTimer?.();
|
|
509
|
+
cancelConsentTimer = void 0;
|
|
510
|
+
consentDenyStarted = true;
|
|
511
|
+
for (const state of replicas.values()) {
|
|
512
|
+
state.renewalTimer?.();
|
|
513
|
+
state.renewalTimer = void 0;
|
|
514
|
+
state.mintAbort?.abort();
|
|
515
|
+
const id = state.handle?.replicaId;
|
|
516
|
+
if (id && !reportedDeadline.has(id)) {
|
|
517
|
+
reportedDeadline.add(id);
|
|
518
|
+
emit({ type: "replication.grant", replicaId: id, action: "consent_deadline_reached", code: "CONSENT_EXPIRED" });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
function observeConsentExpired(stamp) {
|
|
524
|
+
if (!deps.stamp || stamp.incarnation !== deps.stamp.incarnation || stamp.lifecycleEpoch !== deps.stamp.lifecycleEpoch) return;
|
|
525
|
+
if (markConsentExpired()) emit({ type: "replication.lifecycle", action: "denied", reason: "consent_expired" });
|
|
526
|
+
}
|
|
527
|
+
function startConsentDenial() {
|
|
528
|
+
const gate = storage.lifecycle;
|
|
529
|
+
if (!gate || !markConsentExpired()) return;
|
|
530
|
+
void gate.deny({ reason: "consent_expired", principal: identity.principal }).then(
|
|
531
|
+
(outcome) => emit({ type: "replication.lifecycle", action: outcome.status === "denied" ? "denied" : outcome.status === "stale" ? "deny_stale" : outcome.status === "deleted" ? "database_deleted" : outcome.status === "marker" ? "marker_set" : "unguarded", reason: "consent_expired" }),
|
|
532
|
+
() => emit({ type: "replication.lifecycle", action: "deny_failed", reason: "consent_expired" })
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
function deliveryError(decision) {
|
|
536
|
+
return Object.assign(new Error(decision.status === "consent_expired" ? "Replica consent expired" : "Replica session is gated"), {
|
|
537
|
+
code: decision.status === "consent_expired" ? decision.code : "REPLICA_GATED",
|
|
538
|
+
...decision.status === "gated" ? { gate: decision.gate } : {}
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
function deliveryReason(code) {
|
|
542
|
+
return code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? "grant_expired" : code === "REPLICA_GATED" ? "replica_gated" : "replica_error";
|
|
543
|
+
}
|
|
544
|
+
function reportGrantExpiry(prefix) {
|
|
545
|
+
const state = replicas.get(prefix);
|
|
546
|
+
if (!state) return;
|
|
547
|
+
state.reason = "grant_expired";
|
|
548
|
+
state.lastError = "GRANT_EXPIRED";
|
|
549
|
+
const replicaId = state.handle?.replicaId;
|
|
550
|
+
if (!replicaId || state.reportedExpired) return;
|
|
551
|
+
state.reportedExpired = true;
|
|
552
|
+
emit({ type: "replication.grant", replicaId, action: "expired", code: "GRANT_EXPIRED" });
|
|
553
|
+
}
|
|
554
|
+
function reportGrantCap(state, handle, grant) {
|
|
555
|
+
if (!grant?.unconstrained || !handle.replicaId || typeof grant.effectiveExpiresAt !== "number" || !Number.isFinite(grant.effectiveExpiresAt) || !(grant.expiresAt === null || grant.effectiveExpiresAt < grant.expiresAt)) return;
|
|
556
|
+
const capKey = `${grant.cid}:${grant.effectiveExpiresAt}`;
|
|
557
|
+
if (state.reportedCappedCid === capKey) return;
|
|
558
|
+
state.reportedCappedCid = capKey;
|
|
559
|
+
emit({ type: "replication.grant", replicaId: handle.replicaId, action: "capped", effectiveExpiresAt: nowDate(grant.effectiveExpiresAt) });
|
|
560
|
+
}
|
|
402
561
|
const configuredPrefix = (path) => options.prefixes.find((prefix) => kvPrefixCovers(prefix, path));
|
|
403
562
|
function recordRevocation(prefix, error) {
|
|
404
563
|
if (errorCode(error) !== "GRANT_REVOKED") return;
|
|
@@ -422,7 +581,26 @@ function createKVReplication(deps) {
|
|
|
422
581
|
markRootUnsupported(prefix);
|
|
423
582
|
return Object.assign(new Error("This node doesn't support whole-space sync yet."), { code: "REPLICATION_ROOT_UNSUPPORTED" });
|
|
424
583
|
}
|
|
584
|
+
function canReuseInstalled(grant, sessionOnly) {
|
|
585
|
+
if (!grant?.unconstrained) return false;
|
|
586
|
+
if (!deps.stamp) return true;
|
|
587
|
+
if (!sessionOnly && consentAnchor === null) return false;
|
|
588
|
+
const now = scheduler.now();
|
|
589
|
+
const deadline = storage.kind === "indexeddb" ? durableDeadline() : policyDeadline ?? grant.consentDeadline;
|
|
590
|
+
return grant.state === "active" && typeof grant.installedAt === "number" && Number.isFinite(grant.installedAt) && typeof grant.effectiveExpiresAt === "number" && Number.isFinite(grant.effectiveExpiresAt) && typeof grant.consentDeadline === "number" && Number.isFinite(grant.consentDeadline) && typeof deadline === "number" && Number.isFinite(deadline) && Number.isFinite(now) && now >= grant.installedAt && (grant.expiresAt === null || typeof grant.expiresAt === "number" && Number.isFinite(grant.expiresAt) && now < grant.expiresAt) && now < Math.min(grant.effectiveExpiresAt, grant.consentDeadline, deadline);
|
|
591
|
+
}
|
|
592
|
+
function sessionRefusal(prefix) {
|
|
593
|
+
try {
|
|
594
|
+
if (authority.sessionOnly !== true) return void 0;
|
|
595
|
+
const grant = authority.sessionGrant(prefix);
|
|
596
|
+
return "refused" in grant ? grant.refused : void 0;
|
|
597
|
+
} catch {
|
|
598
|
+
return "NOT_COVERED";
|
|
599
|
+
}
|
|
600
|
+
}
|
|
425
601
|
async function open(prefix) {
|
|
602
|
+
const delivery = checkDelivery();
|
|
603
|
+
if (delivery.status !== "allow") throw deliveryError(delivery);
|
|
426
604
|
if (isClosed || purgeStarted) throw Object.assign(new Error("Replication controller is closed"), { code: "REPLICA_CLOSED" });
|
|
427
605
|
let state = replicas.get(prefix);
|
|
428
606
|
if (state?.handle) return state.handle;
|
|
@@ -443,46 +621,99 @@ function createKVReplication(deps) {
|
|
|
443
621
|
await previous;
|
|
444
622
|
previous = void 0;
|
|
445
623
|
}
|
|
624
|
+
const delivery2 = checkDelivery();
|
|
625
|
+
if (delivery2.status !== "allow") throw deliveryError(delivery2);
|
|
626
|
+
const sessionOnly = authority.sessionOnly === true;
|
|
446
627
|
const sessionGrant = authority.sessionGrant(prefix);
|
|
447
628
|
const delegated = "refused" in sessionGrant ? void 0 : sessionGrant;
|
|
448
|
-
const plan =
|
|
629
|
+
const plan = sessionOnly ? void 0 : authority.plan(prefix);
|
|
449
630
|
let refusalCode;
|
|
450
631
|
if ("refused" in sessionGrant) refusalCode = sessionGrant.refused;
|
|
451
632
|
if (plan && "refused" in plan) refusalCode ?? (refusalCode = plan.refused);
|
|
633
|
+
if (deps.stamp && consentAnchor === null && !sessionOnly) refusalCode = "REPLICA_GATED";
|
|
452
634
|
let device;
|
|
453
635
|
if (delegated) {
|
|
454
636
|
device = delegated.device;
|
|
455
637
|
state.strategy = "session";
|
|
456
638
|
}
|
|
457
|
-
const spec = {
|
|
639
|
+
const spec = {
|
|
640
|
+
identity,
|
|
641
|
+
space: session.space,
|
|
642
|
+
prefix,
|
|
643
|
+
allowSecrets: options.allowSecrets,
|
|
644
|
+
grantPolicy,
|
|
645
|
+
...quota ? { quota } : {},
|
|
646
|
+
...deps.stamp ? { stamp: deps.stamp } : {},
|
|
647
|
+
...device ? { device } : {},
|
|
648
|
+
...deps.fetch ? { fetch: deps.fetch } : {},
|
|
649
|
+
onPeerCommit: (info) => {
|
|
650
|
+
if (isClosed || purgeStarted || state.handle === void 0) return;
|
|
651
|
+
emit({
|
|
652
|
+
type: "replication.sync",
|
|
653
|
+
space: session.space,
|
|
654
|
+
replica: prefix,
|
|
655
|
+
trigger: "peer",
|
|
656
|
+
origin: "peer",
|
|
657
|
+
outcome: "ok",
|
|
658
|
+
durationMs: 0,
|
|
659
|
+
lagMs: null,
|
|
660
|
+
...info.changes === void 0 ? {} : { changes: info.changes },
|
|
661
|
+
...info.coverage === void 0 ? {} : { coverage: info.coverage }
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
};
|
|
458
665
|
if (prefix === "/" && deps.nodeFeatures !== void 0 && !deps.nodeFeatures.includes("kv-sync-root")) {
|
|
459
666
|
throw unsupportedRoot(prefix);
|
|
460
667
|
}
|
|
461
668
|
const handle = await storage.open(spec);
|
|
462
669
|
state.handle = handle;
|
|
670
|
+
const afterOpen = checkDelivery();
|
|
671
|
+
if (afterOpen.status !== "allow") throw deliveryError(afterOpen);
|
|
463
672
|
emit({ type: "replication.state", space: session.space, replica: prefix, state: "opened" });
|
|
464
|
-
const runtimePlan = !
|
|
465
|
-
let installed = runtimePlan ? await handle.grant() : void 0;
|
|
466
|
-
if (
|
|
467
|
-
|
|
673
|
+
const runtimePlan = !sessionOnly && plan && !("refused" in plan) ? plan : void 0;
|
|
674
|
+
let installed = runtimePlan || delegated ? await handle.grant() : void 0;
|
|
675
|
+
if (installed && !canReuseInstalled(installed, sessionOnly)) {
|
|
676
|
+
if (installed.unconstrained) refusalCode = consentAnchor === null && !sessionOnly ? "REPLICA_GATED" : "GRANT_EXPIRED";
|
|
677
|
+
installed = void 0;
|
|
678
|
+
}
|
|
679
|
+
let mintedExpiryMs;
|
|
680
|
+
if (delegated && consentAnchor !== null) {
|
|
681
|
+
const beforeInstall = checkDelivery();
|
|
682
|
+
if (beforeInstall.status !== "allow") throw deliveryError(beforeInstall);
|
|
683
|
+
installed = await handle.installGrant(delegated.ucan);
|
|
684
|
+
} else if (runtimePlan) {
|
|
468
685
|
if (!installed || !installed.unconstrained || installed.parentCid !== runtimePlan.parentCid || installed.expiresAt !== null && runtimePlan.expiresAt > installed.expiresAt + 3e5) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
686
|
+
const deadline = mintDeadline();
|
|
687
|
+
const expiryMs = mintRemainingMs({ now: scheduler.now(), grantTtlMs: grantPolicy.grantTtlMs, consentDeadline: deadline ?? Number.NaN });
|
|
688
|
+
if (expiryMs > 0) {
|
|
689
|
+
try {
|
|
690
|
+
const mintAbort = new AbortController();
|
|
691
|
+
state.mintAbort = mintAbort;
|
|
692
|
+
const minted = await authority.mint(handle.deviceDid, prefix, mintAbort.signal, { expiryMs });
|
|
693
|
+
const beforeInstall = checkDelivery();
|
|
694
|
+
if (beforeInstall.status !== "allow") throw deliveryError(beforeInstall);
|
|
695
|
+
installed = await handle.installGrant(minted.ucan);
|
|
696
|
+
state.strategy = "minted";
|
|
697
|
+
mintedExpiryMs = expiryMs;
|
|
698
|
+
} catch (error) {
|
|
699
|
+
if (errorCode(error) === "CONSENT_EXPIRED" || errorCode(error) === "REPLICA_GATED" || !installed?.unconstrained) throw error;
|
|
700
|
+
} finally {
|
|
701
|
+
state.mintAbort = void 0;
|
|
702
|
+
}
|
|
479
703
|
}
|
|
480
704
|
}
|
|
481
705
|
if (installed?.unconstrained) {
|
|
482
706
|
state.strategy ?? (state.strategy = "installed");
|
|
483
707
|
state.reason = void 0;
|
|
484
708
|
}
|
|
709
|
+
} else if ((delegated || sessionOnly) && installed?.unconstrained) {
|
|
710
|
+
state.strategy = "installed";
|
|
485
711
|
}
|
|
712
|
+
const beforeGrantEvent = checkDelivery();
|
|
713
|
+
if (beforeGrantEvent.status !== "allow") throw deliveryError(beforeGrantEvent);
|
|
714
|
+
reportGrantCap(state, handle, installed);
|
|
715
|
+
if (mintedExpiryMs !== void 0) state.reportedExpired = false;
|
|
716
|
+
if (mintedExpiryMs !== void 0 && handle.replicaId) emit({ type: "replication.grant", replicaId: handle.replicaId, action: "minted", expiryMs: mintedExpiryMs });
|
|
486
717
|
if (!installed?.unconstrained) {
|
|
487
718
|
state.reason = "grant_missing";
|
|
488
719
|
state.lastError = refusalCode ?? "SESSION_LACKS_SYNC";
|
|
@@ -492,6 +723,7 @@ function createKVReplication(deps) {
|
|
|
492
723
|
await handle.close();
|
|
493
724
|
throw Object.assign(new Error("Replication controller is closed"), { code: "REPLICA_CLOSED" });
|
|
494
725
|
}
|
|
726
|
+
if (installed?.unconstrained) await maybeRenew(prefix, handle, installed, runtimePlan);
|
|
495
727
|
if (mode === "background" && state.reason !== "grant_missing") {
|
|
496
728
|
void syncPrefix(prefix, "start").catch(() => void 0);
|
|
497
729
|
schedule(prefix);
|
|
@@ -502,12 +734,14 @@ function createKVReplication(deps) {
|
|
|
502
734
|
return await state.opening;
|
|
503
735
|
} catch (error) {
|
|
504
736
|
const code = errorCode(error);
|
|
737
|
+
if (code === "CONSENT_EXPIRED") startConsentDenial();
|
|
505
738
|
recordRevocation(prefix, error);
|
|
506
|
-
if (code
|
|
739
|
+
if (code === "REPLICA_GATED" || code === "CONSENT_EXPIRED") state.reason = deliveryReason(code);
|
|
740
|
+
else if (code !== "GRANT_REVOKED") state.reason = "replica_unavailable";
|
|
507
741
|
state.lastError = code;
|
|
508
742
|
state.failures = (state.failures ?? 0) + 1;
|
|
509
743
|
if (code === "RUNTIME_UNSUPPORTED") state.unsupported = true;
|
|
510
|
-
else if (code !== "REPLICA_CLOSED" && code !== "REPLICATION_ROOT_UNSUPPORTED") state.retryAt = scheduler.now() + Math.min(3e5, 5e3 * 2 ** Math.min(state.failures - 1, 6));
|
|
744
|
+
else if (code !== "REPLICA_CLOSED" && code !== "REPLICATION_ROOT_UNSUPPORTED" && code !== "REPLICA_GATED" && code !== "CONSENT_EXPIRED") state.retryAt = scheduler.now() + Math.min(3e5, 5e3 * 2 ** Math.min(state.failures - 1, 6));
|
|
511
745
|
state.handle?.close().catch(() => void 0);
|
|
512
746
|
state.handle = void 0;
|
|
513
747
|
throw error;
|
|
@@ -515,6 +749,96 @@ function createKVReplication(deps) {
|
|
|
515
749
|
state.opening = void 0;
|
|
516
750
|
}
|
|
517
751
|
}
|
|
752
|
+
function scheduleRenewal(prefix, handle, grant, plan) {
|
|
753
|
+
const state = replicas.get(prefix);
|
|
754
|
+
state?.renewalTimer?.();
|
|
755
|
+
if (state) state.renewalTimer = void 0;
|
|
756
|
+
if (!state || isClosed || purgeStarted || plan.path !== "session" || !authority.canRenew) return;
|
|
757
|
+
const { installedAt, effectiveExpiresAt } = grant;
|
|
758
|
+
const deadline = mintDeadline();
|
|
759
|
+
if (typeof installedAt !== "number" || !Number.isFinite(installedAt) || typeof effectiveExpiresAt !== "number" || !Number.isFinite(effectiveExpiresAt) || !(effectiveExpiresAt > installedAt) || deadline === null) return;
|
|
760
|
+
const threshold = installedAt + (effectiveExpiresAt - installedAt) * (1 - RENEW_BELOW_FRACTION);
|
|
761
|
+
const now = scheduler.now();
|
|
762
|
+
if (!Number.isFinite(now) || !(threshold < deadline) || now > threshold) return;
|
|
763
|
+
state.renewalTimer = scheduler.setTimeout(() => {
|
|
764
|
+
state.renewalTimer = void 0;
|
|
765
|
+
void maybeRenew(prefix, handle).catch(() => void 0);
|
|
766
|
+
}, Math.max(0, Math.floor(threshold) + 1 - now));
|
|
767
|
+
}
|
|
768
|
+
async function maybeRenew(prefix, handle, knownGrant, knownPlan) {
|
|
769
|
+
const state = replicas.get(prefix);
|
|
770
|
+
if (!state || state.handle !== handle || isClosed || purgeStarted || !authority.canRenew) return;
|
|
771
|
+
if (state.renewing) return state.renewing;
|
|
772
|
+
const decision = checkDelivery();
|
|
773
|
+
if (decision.status !== "allow") return;
|
|
774
|
+
const grant = knownGrant ?? await handle.grant();
|
|
775
|
+
if (!grant?.unconstrained) return;
|
|
776
|
+
const { installedAt, effectiveExpiresAt } = grant;
|
|
777
|
+
const deadline = mintDeadline();
|
|
778
|
+
if (typeof installedAt !== "number" || !Number.isFinite(installedAt) || typeof effectiveExpiresAt !== "number" || !Number.isFinite(effectiveExpiresAt) || deadline === null) return;
|
|
779
|
+
const now = scheduler.now();
|
|
780
|
+
if (!needsSilentRenewal({ now, installedAt, effectiveExpiresAt, consentDeadline: deadline })) {
|
|
781
|
+
if (knownPlan) scheduleRenewal(prefix, handle, grant, knownPlan);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
let plan = knownPlan;
|
|
785
|
+
try {
|
|
786
|
+
if (!plan) {
|
|
787
|
+
const selected = authority.plan(prefix);
|
|
788
|
+
if ("refused" in selected) {
|
|
789
|
+
if (handle.replicaId) emit({ type: "replication.grant", replicaId: handle.replicaId, action: "renewal_refused", code: selected.refused });
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
plan = selected;
|
|
793
|
+
}
|
|
794
|
+
} catch {
|
|
795
|
+
if (handle.replicaId) emit({ type: "replication.grant", replicaId: handle.replicaId, action: "renewal_deferred" });
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (plan?.path !== "session") {
|
|
799
|
+
if (handle.replicaId && state.reportedRenewalUnqualifiedCid !== grant.cid) {
|
|
800
|
+
state.reportedRenewalUnqualifiedCid = grant.cid;
|
|
801
|
+
emit({ type: "replication.grant", replicaId: handle.replicaId, action: "renewal_deferred" });
|
|
802
|
+
}
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const expiryMs = mintRemainingMs({ now, grantTtlMs: grantPolicy.grantTtlMs, consentDeadline: deadline });
|
|
806
|
+
if (!(expiryMs > 0)) return;
|
|
807
|
+
state.renewalTimer?.();
|
|
808
|
+
state.renewalTimer = void 0;
|
|
809
|
+
const mintAbort = new AbortController();
|
|
810
|
+
state.mintAbort = mintAbort;
|
|
811
|
+
const job = Promise.resolve().then(async () => {
|
|
812
|
+
const minted = await authority.mint(handle.deviceDid, prefix, mintAbort.signal, { expiryMs, renewalOnly: true });
|
|
813
|
+
const beforeInstall = checkDelivery();
|
|
814
|
+
if (beforeInstall.status !== "allow") throw deliveryError(beforeInstall);
|
|
815
|
+
const installed = await handle.installGrant(minted.ucan);
|
|
816
|
+
const beforeEvent = checkDelivery();
|
|
817
|
+
if (beforeEvent.status !== "allow") throw deliveryError(beforeEvent);
|
|
818
|
+
reportGrantCap(state, handle, installed);
|
|
819
|
+
state.reportedExpired = false;
|
|
820
|
+
if (state.reason === "grant_expired") state.reason = void 0;
|
|
821
|
+
if (state.lastError === "GRANT_EXPIRED") state.lastError = void 0;
|
|
822
|
+
if (handle.replicaId) emit({ type: "replication.grant", replicaId: handle.replicaId, action: "renewed", expiryMs });
|
|
823
|
+
if (typeof installed.effectiveExpiresAt === "number" && installed.effectiveExpiresAt > effectiveExpiresAt) {
|
|
824
|
+
scheduleRenewal(prefix, handle, installed, plan);
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
state.renewing = job;
|
|
828
|
+
try {
|
|
829
|
+
await job;
|
|
830
|
+
} catch (error) {
|
|
831
|
+
const code = errorCode(error);
|
|
832
|
+
if (code === "CONSENT_EXPIRED" || code === "REPLICA_GATED") throw error;
|
|
833
|
+
if (!isClosed && handle.replicaId) {
|
|
834
|
+
const refused = typeof error === "object" && error !== null && "refused" in error && typeof error.refused === "string" ? error.refused : null;
|
|
835
|
+
emit({ type: "replication.grant", replicaId: handle.replicaId, action: refused ? "renewal_refused" : "renewal_deferred", code: refused ?? code });
|
|
836
|
+
}
|
|
837
|
+
} finally {
|
|
838
|
+
state.renewing = void 0;
|
|
839
|
+
if (state.mintAbort === mintAbort) state.mintAbort = void 0;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
518
842
|
function schedule(prefix) {
|
|
519
843
|
const state = replicas.get(prefix);
|
|
520
844
|
if (!state || mode !== "background" || isClosed || purgeStarted) return;
|
|
@@ -527,6 +851,8 @@ function createKVReplication(deps) {
|
|
|
527
851
|
if (prefix === "/" && deps.nodeFeatures !== void 0 && !deps.nodeFeatures.includes("kv-sync-root")) {
|
|
528
852
|
throw unsupportedRoot(prefix);
|
|
529
853
|
}
|
|
854
|
+
const initialRefusal = sessionRefusal(prefix);
|
|
855
|
+
if (initialRefusal) throw Object.assign(new Error("Current session does not cover replica sync"), { code: initialRefusal });
|
|
530
856
|
const handle = await open(prefix);
|
|
531
857
|
const state = replicas.get(prefix);
|
|
532
858
|
if (state.sync) return state.sync;
|
|
@@ -544,22 +870,36 @@ function createKVReplication(deps) {
|
|
|
544
870
|
job = (async () => {
|
|
545
871
|
try {
|
|
546
872
|
const syncStartEpoch = (await pending.read()).committedEpoch;
|
|
873
|
+
const beforeSync = checkDelivery();
|
|
874
|
+
if (beforeSync.status !== "allow") throw deliveryError(beforeSync);
|
|
875
|
+
await maybeRenew(prefix, handle);
|
|
876
|
+
const afterRenewal = checkDelivery();
|
|
877
|
+
if (afterRenewal.status !== "allow") throw deliveryError(afterRenewal);
|
|
878
|
+
const refusal = sessionRefusal(prefix);
|
|
879
|
+
if (refusal) throw Object.assign(new Error("Current session does not cover replica sync"), { code: refusal });
|
|
547
880
|
const result = await handle.sync({ signal: abort.signal, syncStartEpoch });
|
|
881
|
+
const afterSyncDelivery = checkDelivery();
|
|
882
|
+
if (afterSyncDelivery.status !== "allow") throw deliveryError(afterSyncDelivery);
|
|
548
883
|
if (result.status === "busy") {
|
|
549
884
|
emit({ type: "replication.sync", space: session.space, replica: prefix, trigger, outcome: "busy", durationMs: scheduler.now() - started, lagMs: null });
|
|
550
885
|
return result;
|
|
551
886
|
}
|
|
552
887
|
inProcessProof.add(prefix);
|
|
553
888
|
const snapshot = await pending.read();
|
|
889
|
+
const beforePending = checkDelivery();
|
|
890
|
+
if (beforePending.status !== "allow") throw deliveryError(beforePending);
|
|
554
891
|
const clearedCandidates = snapshot.records.some((r) => r.state === "committed" && r.epoch !== null && r.epoch <= syncStartEpoch && kvPrefixCovers(prefix, r.key));
|
|
555
892
|
const cleared = clearedCandidates ? await pending.update((s) => afterSync(s, prefix, syncStartEpoch)) : 0;
|
|
893
|
+
const beforeResult = checkDelivery();
|
|
894
|
+
if (beforeResult.status !== "allow") throw deliveryError(beforeResult);
|
|
556
895
|
emit({ type: "replication.sync", space: session.space, replica: prefix, trigger, outcome: "ok", durationMs: scheduler.now() - started, lagMs: null, pendingCleared: cleared, pages: result.pages, changes: result.changes, deleted: result.deleted, fetched: result.fetched, contentMissing: result.contentMissing, coverage: result.coverage });
|
|
557
896
|
return result;
|
|
558
897
|
} catch (error) {
|
|
559
898
|
const code = errorCode(error);
|
|
560
899
|
state.lastError = code;
|
|
561
900
|
recordRevocation(prefix, error);
|
|
562
|
-
|
|
901
|
+
if (code === "CONSENT_EXPIRED") startConsentDenial();
|
|
902
|
+
emit({ type: "replication.sync", space: session.space, replica: prefix, trigger, outcome: abort.signal.aborted ? "aborted" : "error", class: code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" ? "lifecycle" : code === "NETWORK_ERROR" || code === "TIMEOUT" ? "offline" : "node", code, durationMs: scheduler.now() - started, lagMs: null });
|
|
563
903
|
throw error;
|
|
564
904
|
} finally {
|
|
565
905
|
signal?.removeEventListener("abort", abortFromCaller);
|
|
@@ -715,8 +1055,19 @@ function createKVReplication(deps) {
|
|
|
715
1055
|
return { status: currentStatus, syncedBeforeRead: false };
|
|
716
1056
|
}
|
|
717
1057
|
function readEvent(op, path, prefix, source, reason, outcome, started, extra = {}) {
|
|
1058
|
+
if (prefix !== null && reason === "grant_expired" && extra.code !== "CONSENT_EXPIRED") reportGrantExpiry(prefix);
|
|
718
1059
|
emit({ type: "replication.read", op, space: session.space, key: path, replica: prefix, source, reason, outcome, latencyMs: scheduler.now() - started, stalenessMs: null, coverage: null, authority: null, ...extra });
|
|
719
1060
|
}
|
|
1061
|
+
async function networkRead(network, op, path, prefix, reason, started, extra = {}) {
|
|
1062
|
+
let outcome = "error";
|
|
1063
|
+
try {
|
|
1064
|
+
const result = await network();
|
|
1065
|
+
outcome = result.ok ? "found" : op === "get" && (reason === "pending_write" || reason === "not_covered") && result.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error";
|
|
1066
|
+
return result;
|
|
1067
|
+
} finally {
|
|
1068
|
+
readEvent(op, path, prefix, "network", reason, outcome, started, extra);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
720
1071
|
async function verifyGet(r, local, reason, started, extra) {
|
|
721
1072
|
const prefix = configuredPrefix(r.path) ?? null;
|
|
722
1073
|
const localFound = local.ok;
|
|
@@ -728,6 +1079,16 @@ function createKVReplication(deps) {
|
|
|
728
1079
|
readEvent("get", r.path, prefix, "none", "aborted", "error", started, { ...extra, verify: "aborted" });
|
|
729
1080
|
return abortedResult(r.signal);
|
|
730
1081
|
}
|
|
1082
|
+
const delivery = checkDelivery();
|
|
1083
|
+
if (delivery.status !== "allow") {
|
|
1084
|
+
readEvent("get", r.path, prefix, "network", delivery.status === "consent_expired" ? "grant_expired" : "replica_gated", network.ok ? "found" : "error", started, { code: delivery.status === "consent_expired" ? delivery.code : "REPLICA_GATED" });
|
|
1085
|
+
return network;
|
|
1086
|
+
}
|
|
1087
|
+
const refusal = prefix === null ? void 0 : sessionRefusal(prefix);
|
|
1088
|
+
if (refusal) {
|
|
1089
|
+
readEvent("get", r.path, prefix, "network", "grant_missing", network.ok ? "found" : "error", started, { code: refusal });
|
|
1090
|
+
return network;
|
|
1091
|
+
}
|
|
731
1092
|
if (!network.ok && network.error.code !== ErrorCodes.KV_NOT_FOUND) {
|
|
732
1093
|
readEvent("get", r.path, prefix, "replica", reason, outcome, started, { ...extra, code: network.error.code, verify: "error" });
|
|
733
1094
|
return local;
|
|
@@ -758,6 +1119,16 @@ function createKVReplication(deps) {
|
|
|
758
1119
|
readEvent("get", r.path, prefix, "none", "aborted", "error", started, { ...extra, verify: "aborted" });
|
|
759
1120
|
return abortedResult(r.signal);
|
|
760
1121
|
}
|
|
1122
|
+
const delivery = checkDelivery();
|
|
1123
|
+
if (delivery.status !== "allow") {
|
|
1124
|
+
readEvent("get", r.path, prefix, "network", delivery.status === "consent_expired" ? "grant_expired" : "replica_gated", "error", started, { code: delivery.status === "consent_expired" ? delivery.code : "REPLICA_GATED" });
|
|
1125
|
+
return err(wrapError("kv", error));
|
|
1126
|
+
}
|
|
1127
|
+
const refusal = prefix === null ? void 0 : sessionRefusal(prefix);
|
|
1128
|
+
if (refusal) {
|
|
1129
|
+
readEvent("get", r.path, prefix, "network", "grant_missing", "error", started, { code: refusal });
|
|
1130
|
+
return err(wrapError("kv", error));
|
|
1131
|
+
}
|
|
761
1132
|
readEvent("get", r.path, prefix, "replica", reason, outcome, started, { ...extra, code: errorCode(error), verify: "error" });
|
|
762
1133
|
return local;
|
|
763
1134
|
}
|
|
@@ -770,6 +1141,16 @@ function createKVReplication(deps) {
|
|
|
770
1141
|
readEvent("list", r.listPath, prefix, "none", "aborted", "error", started, { ...extra, verify: "aborted" });
|
|
771
1142
|
return abortedResult(r.signal);
|
|
772
1143
|
}
|
|
1144
|
+
const delivery = checkDelivery();
|
|
1145
|
+
if (delivery.status !== "allow") {
|
|
1146
|
+
readEvent("list", r.listPath, prefix, "network", delivery.status === "consent_expired" ? "grant_expired" : "replica_gated", network.ok ? "found" : "error", started, { code: delivery.status === "consent_expired" ? delivery.code : "REPLICA_GATED" });
|
|
1147
|
+
return network;
|
|
1148
|
+
}
|
|
1149
|
+
const refusal = prefix === null ? void 0 : sessionRefusal(prefix);
|
|
1150
|
+
if (refusal) {
|
|
1151
|
+
readEvent("list", r.listPath, prefix, "network", "grant_missing", network.ok ? "found" : "error", started, { code: refusal });
|
|
1152
|
+
return network;
|
|
1153
|
+
}
|
|
773
1154
|
if (!network.ok) {
|
|
774
1155
|
readEvent("list", r.listPath, prefix, "replica", "hit", "found", started, { ...extra, code: network.error.code, verify: "error" });
|
|
775
1156
|
return ok(local);
|
|
@@ -800,6 +1181,16 @@ function createKVReplication(deps) {
|
|
|
800
1181
|
readEvent("list", r.listPath, prefix, "none", "aborted", "error", started, { ...extra, verify: "aborted" });
|
|
801
1182
|
return abortedResult(r.signal);
|
|
802
1183
|
}
|
|
1184
|
+
const delivery = checkDelivery();
|
|
1185
|
+
if (delivery.status !== "allow") {
|
|
1186
|
+
readEvent("list", r.listPath, prefix, "network", delivery.status === "consent_expired" ? "grant_expired" : "replica_gated", "error", started, { code: delivery.status === "consent_expired" ? delivery.code : "REPLICA_GATED" });
|
|
1187
|
+
return err(wrapError("kv", error));
|
|
1188
|
+
}
|
|
1189
|
+
const refusal = prefix === null ? void 0 : sessionRefusal(prefix);
|
|
1190
|
+
if (refusal) {
|
|
1191
|
+
readEvent("list", r.listPath, prefix, "network", "grant_missing", "error", started, { code: refusal });
|
|
1192
|
+
return err(wrapError("kv", error));
|
|
1193
|
+
}
|
|
803
1194
|
readEvent("list", r.listPath, prefix, "replica", "hit", "found", started, { ...extra, code: errorCode(error), verify: "error" });
|
|
804
1195
|
return ok(local);
|
|
805
1196
|
}
|
|
@@ -828,11 +1219,26 @@ function createKVReplication(deps) {
|
|
|
828
1219
|
observeNetworkRequested({ op: "get", space: r.space, path: r.path, reason: "NETWORK_REQUESTED", outcome, latencyMs: scheduler.now() - started });
|
|
829
1220
|
}
|
|
830
1221
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1222
|
+
let networkRejected = false;
|
|
1223
|
+
const fallback = async (replica, reason, extra = {}) => {
|
|
1224
|
+
try {
|
|
1225
|
+
return await networkRead(r.network, "get", r.path, replica, reason, started, extra);
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
networkRejected = true;
|
|
1228
|
+
throw error;
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
if (prefix === void 0) return networkRead(r.network, "get", r.path, null, "not_covered", started);
|
|
1232
|
+
const delivery = checkDelivery();
|
|
1233
|
+
if (delivery.status !== "allow") {
|
|
1234
|
+
return fallback(
|
|
1235
|
+
prefix,
|
|
1236
|
+
delivery.status === "consent_expired" ? "grant_expired" : "replica_gated",
|
|
1237
|
+
{ code: delivery.status === "consent_expired" ? delivery.code : "REPLICA_GATED" }
|
|
1238
|
+
);
|
|
835
1239
|
}
|
|
1240
|
+
const refusal = sessionRefusal(prefix);
|
|
1241
|
+
if (refusal) return fallback(prefix, "grant_missing", { code: refusal });
|
|
836
1242
|
try {
|
|
837
1243
|
await raceSignal(reportExistingPins(), r.signal);
|
|
838
1244
|
} catch {
|
|
@@ -843,86 +1249,63 @@ function createKVReplication(deps) {
|
|
|
843
1249
|
handle = await raceSignal(open(prefix), r.signal);
|
|
844
1250
|
} catch (error) {
|
|
845
1251
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
return
|
|
1252
|
+
if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
|
|
1253
|
+
const code = errorCode(error);
|
|
1254
|
+
return fallback(prefix, code === "RUNTIME_UNSUPPORTED" ? "runtime_unsupported" : code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_unavailable", { code });
|
|
849
1255
|
}
|
|
850
1256
|
if (replicas.get(prefix)?.reason === "grant_missing") {
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
return value;
|
|
1257
|
+
const code = replicas.get(prefix)?.lastError;
|
|
1258
|
+
return fallback(prefix, code === "GRANT_EXPIRED" ? "grant_expired" : code === "REPLICA_GATED" ? "replica_gated" : "grant_missing", { code });
|
|
854
1259
|
}
|
|
855
1260
|
let fresh;
|
|
856
1261
|
try {
|
|
857
1262
|
fresh = await freshness(prefix, handle, r.signal);
|
|
858
1263
|
} catch (error) {
|
|
859
1264
|
recordRevocation(prefix, error);
|
|
1265
|
+
if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
|
|
860
1266
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
return value;
|
|
1267
|
+
const code = errorCode(error);
|
|
1268
|
+
return fallback(prefix, code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_error", { code });
|
|
864
1269
|
}
|
|
865
1270
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
866
1271
|
if (fresh.failure) {
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
|
|
1272
|
+
const code = fresh.failure === "busy" ? "REPLICA_BUSY" : fresh.syncError;
|
|
1273
|
+
return fallback(
|
|
1274
|
+
prefix,
|
|
1275
|
+
code === "GRANT_REVOKED" ? "grant_revoked" : code === "REPLICA_GATED" || code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "stale",
|
|
1276
|
+
{ code, syncedBeforeRead: fresh.syncedBeforeRead }
|
|
1277
|
+
);
|
|
870
1278
|
}
|
|
871
1279
|
let pendingState;
|
|
872
1280
|
try {
|
|
873
1281
|
pendingState = await raceSignal(pending.read(), r.signal);
|
|
874
1282
|
} catch (error) {
|
|
875
1283
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
return value;
|
|
1284
|
+
const code = errorCode(error);
|
|
1285
|
+
return fallback(prefix, code === "REPLICA_GATED" || code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_unavailable", { code });
|
|
879
1286
|
}
|
|
880
1287
|
const record = pendingState.records.find((x) => x.key === r.path);
|
|
881
|
-
if (record) {
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
if (!pending.durable && !inProcessProof.has(prefix)) {
|
|
887
|
-
const value = await r.network();
|
|
888
|
-
readEvent("get", r.path, prefix, "network", "REPLICA_UNPROVEN_SINCE_START", value.ok ? "found" : "error", started);
|
|
889
|
-
return value;
|
|
890
|
-
}
|
|
891
|
-
if (fresh.status.syncedThroughEpoch < pendingState.committedEpoch) {
|
|
892
|
-
const value = await r.network();
|
|
893
|
-
readEvent("get", r.path, prefix, "network", "REPLICA_BEHIND_OWN_WRITES", value.ok ? "found" : "error", started);
|
|
894
|
-
return value;
|
|
895
|
-
}
|
|
896
|
-
if (fresh.status.authority.state !== "valid") {
|
|
897
|
-
const value = await r.network();
|
|
898
|
-
readEvent("get", r.path, prefix, "network", fresh.status.authority.state === "expired" ? "grant_expired" : fresh.status.authority.state === "revoked" ? "grant_revoked" : "grant_not_yet_valid", value.ok ? "found" : "error", started);
|
|
899
|
-
return value;
|
|
900
|
-
}
|
|
901
|
-
if (fresh.status.coverage !== "complete") {
|
|
902
|
-
const value = await r.network();
|
|
903
|
-
readEvent("get", r.path, prefix, "network", "coverage_incomplete", value.ok ? "found" : "error", started);
|
|
904
|
-
return value;
|
|
905
|
-
}
|
|
1288
|
+
if (record) return fallback(prefix, "pending_write", { pendingState: record.state, syncedBeforeRead: fresh.syncedBeforeRead });
|
|
1289
|
+
if (!pending.durable && !inProcessProof.has(prefix)) return fallback(prefix, "REPLICA_UNPROVEN_SINCE_START");
|
|
1290
|
+
if (fresh.status.syncedThroughEpoch < pendingState.committedEpoch) return fallback(prefix, "REPLICA_BEHIND_OWN_WRITES");
|
|
1291
|
+
if (fresh.status.authority.state !== "valid") return fallback(prefix, fresh.status.authority.state === "expired" ? "grant_expired" : fresh.status.authority.state === "revoked" ? "grant_revoked" : "grant_not_yet_valid");
|
|
1292
|
+
if (fresh.status.coverage !== "complete") return fallback(prefix, "coverage_incomplete");
|
|
906
1293
|
try {
|
|
907
1294
|
const local = await raceSignal(handle.get(r.path), r.signal);
|
|
1295
|
+
const delivery2 = checkDelivery();
|
|
1296
|
+
if (delivery2.status !== "allow") throw deliveryError(delivery2);
|
|
1297
|
+
const refusal2 = sessionRefusal(prefix);
|
|
1298
|
+
if (refusal2) return await fallback(prefix, "grant_missing", { code: refusal2 });
|
|
908
1299
|
try {
|
|
909
1300
|
validateReadMeta(local.meta);
|
|
910
1301
|
} catch (error) {
|
|
911
1302
|
const code = errorCode(error);
|
|
912
1303
|
recordRevocation(prefix, error);
|
|
913
|
-
|
|
914
|
-
readEvent("get", r.path, prefix, "network", reasonForReadMetaCode(code), value.ok ? "found" : "error", started, { code });
|
|
915
|
-
return value;
|
|
916
|
-
}
|
|
917
|
-
if (local.status === "coverage_incomplete" || local.status === "not_covered" || local.status === "content_missing") {
|
|
918
|
-
const value = await r.network();
|
|
919
|
-
readEvent("get", r.path, prefix, "network", local.status === "content_missing" ? "content_missing" : local.status === "coverage_incomplete" ? "coverage_incomplete" : "not_covered", value.ok ? "found" : "error", started);
|
|
920
|
-
return value;
|
|
1304
|
+
return await fallback(prefix, reasonForReadMetaCode(code), { code });
|
|
921
1305
|
}
|
|
1306
|
+
if (local.status === "coverage_incomplete" || local.status === "not_covered" || local.status === "content_missing") return await fallback(prefix, local.status === "content_missing" ? "content_missing" : local.status === "coverage_incomplete" ? "coverage_incomplete" : "not_covered");
|
|
922
1307
|
if (local.status === "present" && r.options?.maxResponseBytes !== void 0 && local.value.byteLength > r.options.maxResponseBytes) {
|
|
923
|
-
|
|
924
|
-
readEvent("get", r.path, prefix, "network", "unsupported_option", value.ok ? "found" : "error", started);
|
|
925
|
-
return value;
|
|
1308
|
+
return await fallback(prefix, "unsupported_option");
|
|
926
1309
|
}
|
|
927
1310
|
const result = localGet(local, r.path, r.options ?? {});
|
|
928
1311
|
const reason = local.status === "present" ? "hit" : local.status;
|
|
@@ -938,11 +1321,11 @@ function createKVReplication(deps) {
|
|
|
938
1321
|
readEvent("get", r.path, prefix, "replica", reason, result.ok ? "found" : result.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error", started, event);
|
|
939
1322
|
return result;
|
|
940
1323
|
} catch (error) {
|
|
1324
|
+
if (networkRejected) throw error;
|
|
941
1325
|
recordRevocation(prefix, error);
|
|
1326
|
+
if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
|
|
942
1327
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
943
|
-
|
|
944
|
-
readEvent("get", r.path, prefix, "network", "replica_error", value.ok ? "found" : "error", started, { code: errorCode(error) });
|
|
945
|
-
return value;
|
|
1328
|
+
return fallback(prefix, deliveryReason(errorCode(error)), { code: errorCode(error) });
|
|
946
1329
|
}
|
|
947
1330
|
}
|
|
948
1331
|
async function list(r) {
|
|
@@ -963,17 +1346,46 @@ function createKVReplication(deps) {
|
|
|
963
1346
|
readEvent("list", r.listPath, null, "none", "cursor_restart", "error", started);
|
|
964
1347
|
return cursorRestart();
|
|
965
1348
|
}
|
|
966
|
-
if (cursor && !cursor.startsWith("tcr1."))
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1349
|
+
if (cursor && !cursor.startsWith("tcr1.")) return networkRead(r.network, "list", r.listPath, null, "network_cursor", started);
|
|
1350
|
+
const prefix = listPathCovered(options.prefixes, r.listPath);
|
|
1351
|
+
let networkRejected = false;
|
|
1352
|
+
const fallback = async (replica, reason, extra = {}) => {
|
|
1353
|
+
try {
|
|
1354
|
+
return await networkRead(r.network, "list", r.listPath, replica, reason, started, extra);
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
networkRejected = true;
|
|
1357
|
+
throw error;
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
if (prefix !== void 0 && !r.options?.raw) {
|
|
1361
|
+
const delivery = checkDelivery();
|
|
1362
|
+
if (delivery.status !== "allow") {
|
|
1363
|
+
if (decoded) {
|
|
1364
|
+
readEvent("list", r.listPath, prefix, "none", "cursor_restart", "error", started);
|
|
1365
|
+
return cursorRestart();
|
|
1366
|
+
}
|
|
1367
|
+
return fallback(
|
|
1368
|
+
prefix,
|
|
1369
|
+
delivery.status === "consent_expired" ? "grant_expired" : "replica_gated",
|
|
1370
|
+
{ code: delivery.status === "consent_expired" ? delivery.code : "REPLICA_GATED" }
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (prefix !== void 0 && !r.options?.raw) {
|
|
1375
|
+
const refusal = sessionRefusal(prefix);
|
|
1376
|
+
if (refusal) {
|
|
1377
|
+
if (decoded) {
|
|
1378
|
+
readEvent("list", r.listPath, prefix, "none", "cursor_restart", "error", started);
|
|
1379
|
+
return cursorRestart();
|
|
1380
|
+
}
|
|
1381
|
+
return fallback(prefix, "grant_missing", { code: refusal });
|
|
1382
|
+
}
|
|
970
1383
|
}
|
|
971
1384
|
try {
|
|
972
1385
|
await raceSignal(reportExistingPins(), r.signal);
|
|
973
1386
|
} catch {
|
|
974
1387
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
975
1388
|
}
|
|
976
|
-
const prefix = listPathCovered(options.prefixes, r.listPath);
|
|
977
1389
|
const restart = () => {
|
|
978
1390
|
readEvent("list", r.listPath, prefix ?? null, "none", "cursor_restart", "error", started);
|
|
979
1391
|
return cursorRestart();
|
|
@@ -981,36 +1393,33 @@ function createKVReplication(deps) {
|
|
|
981
1393
|
if (cursor && decoded && (decoded.space !== r.space || decoded.path !== r.listPath)) return restart();
|
|
982
1394
|
if (prefix === void 0 || r.options?.raw) {
|
|
983
1395
|
if (cursor && decoded) return restart();
|
|
984
|
-
|
|
985
|
-
readEvent("list", r.listPath, prefix ?? null, "network", prefix !== void 0 ? "unsupported_option" : "not_covered", result.ok ? "found" : "error", started);
|
|
986
|
-
return result;
|
|
1396
|
+
return networkRead(r.network, "list", r.listPath, prefix ?? null, prefix !== void 0 ? "unsupported_option" : "not_covered", started);
|
|
987
1397
|
}
|
|
988
1398
|
let handle;
|
|
989
1399
|
try {
|
|
990
1400
|
handle = await raceSignal(open(prefix), r.signal);
|
|
991
1401
|
} catch (error) {
|
|
992
1402
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
1403
|
+
if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
|
|
993
1404
|
if (cursor && decoded) return restart();
|
|
994
|
-
const
|
|
995
|
-
|
|
996
|
-
return result;
|
|
1405
|
+
const code = errorCode(error);
|
|
1406
|
+
return fallback(prefix, code === "RUNTIME_UNSUPPORTED" ? "runtime_unsupported" : code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_unavailable", { code });
|
|
997
1407
|
}
|
|
998
1408
|
if (replicas.get(prefix)?.reason === "grant_missing") {
|
|
999
1409
|
if (cursor && decoded) return restart();
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1002
|
-
return result;
|
|
1410
|
+
const code = replicas.get(prefix)?.lastError;
|
|
1411
|
+
return fallback(prefix, code === "GRANT_EXPIRED" ? "grant_expired" : code === "REPLICA_GATED" ? "replica_gated" : "grant_missing", { code });
|
|
1003
1412
|
}
|
|
1004
1413
|
let fresh;
|
|
1005
1414
|
try {
|
|
1006
1415
|
fresh = await freshness(prefix, handle, r.signal);
|
|
1007
1416
|
} catch (error) {
|
|
1008
1417
|
recordRevocation(prefix, error);
|
|
1418
|
+
if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
|
|
1009
1419
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
1010
1420
|
if (cursor && decoded) return restart();
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
return result;
|
|
1421
|
+
const code = errorCode(error);
|
|
1422
|
+
return fallback(prefix, code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_error", { code });
|
|
1014
1423
|
}
|
|
1015
1424
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
1016
1425
|
let state;
|
|
@@ -1019,41 +1428,35 @@ function createKVReplication(deps) {
|
|
|
1019
1428
|
} catch (error) {
|
|
1020
1429
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
1021
1430
|
if (cursor && decoded) return restart();
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
return result;
|
|
1431
|
+
const code = errorCode(error);
|
|
1432
|
+
return fallback(prefix, code === "REPLICA_GATED" || code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_unavailable", { code });
|
|
1025
1433
|
}
|
|
1026
|
-
const pendingRange = state.records.find((record) => listRangeCovers(r.listPath, record.key)
|
|
1434
|
+
const pendingRange = state.records.find((record) => listRangeCovers(r.listPath, record.key));
|
|
1027
1435
|
const behind = fresh.status.syncedThroughEpoch < state.committedEpoch;
|
|
1028
1436
|
if (cursor && decoded && (fresh.failure || pendingRange || behind || fresh.status.coverage !== "complete" || fresh.status.authority.state !== "valid" || !pending.durable && !inProcessProof.has(prefix))) return restart();
|
|
1029
|
-
if (behind)
|
|
1030
|
-
const result = await r.network();
|
|
1031
|
-
readEvent("list", r.listPath, prefix, "network", "REPLICA_BEHIND_OWN_WRITES", result.ok ? "found" : "error", started);
|
|
1032
|
-
return result;
|
|
1033
|
-
}
|
|
1437
|
+
if (behind) return fallback(prefix, "REPLICA_BEHIND_OWN_WRITES");
|
|
1034
1438
|
if (fresh.failure) {
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
return result;
|
|
1038
|
-
}
|
|
1039
|
-
if (pendingRange) {
|
|
1040
|
-
const result = await r.network();
|
|
1041
|
-
readEvent("list", r.listPath, prefix, "network", "pending_write", result.ok ? "found" : "error", started, { pendingState: pendingRange.state });
|
|
1042
|
-
return result;
|
|
1043
|
-
}
|
|
1044
|
-
if (!pending.durable && !inProcessProof.has(prefix)) {
|
|
1045
|
-
const result = await r.network();
|
|
1046
|
-
readEvent("list", r.listPath, prefix, "network", "REPLICA_UNPROVEN_SINCE_START", result.ok ? "found" : "error", started);
|
|
1047
|
-
return result;
|
|
1048
|
-
}
|
|
1049
|
-
if (fresh.status.coverage !== "complete" || fresh.status.authority.state !== "valid") {
|
|
1050
|
-
const result = await r.network();
|
|
1051
|
-
readEvent("list", r.listPath, prefix, "network", fresh.status.coverage !== "complete" ? "coverage_incomplete" : fresh.status.authority.state === "expired" ? "grant_expired" : fresh.status.authority.state === "revoked" ? "grant_revoked" : "grant_not_yet_valid", result.ok ? "found" : "error", started);
|
|
1052
|
-
return result;
|
|
1439
|
+
const code = fresh.failure === "busy" ? "REPLICA_BUSY" : fresh.syncError;
|
|
1440
|
+
return fallback(prefix, code === "GRANT_REVOKED" ? "grant_revoked" : code === "REPLICA_GATED" || code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "stale", { code });
|
|
1053
1441
|
}
|
|
1442
|
+
if (pendingRange) return fallback(prefix, "pending_write", { pendingState: pendingRange.state });
|
|
1443
|
+
if (!pending.durable && !inProcessProof.has(prefix)) return fallback(prefix, "REPLICA_UNPROVEN_SINCE_START");
|
|
1444
|
+
if (fresh.status.coverage !== "complete" || fresh.status.authority.state !== "valid") return fallback(prefix, fresh.status.coverage !== "complete" ? "coverage_incomplete" : fresh.status.authority.state === "expired" ? "grant_expired" : fresh.status.authority.state === "revoked" ? "grant_revoked" : "grant_not_yet_valid");
|
|
1054
1445
|
try {
|
|
1055
1446
|
const signalHandle = { list: (o) => raceSignal(handle.list(o), r.signal) };
|
|
1056
1447
|
const page = await raceSignal(localList(signalHandle, r.space, r.listPath, r.options?.limit, cursor, validateReadMeta), r.signal);
|
|
1448
|
+
const latest = await raceSignal(pending.read(), r.signal);
|
|
1449
|
+
const laterPending = latest.records.find((record) => listRangeCovers(r.listPath, record.key));
|
|
1450
|
+
if (cursor && decoded && (laterPending || fresh.status.syncedThroughEpoch < latest.committedEpoch)) return restart();
|
|
1451
|
+
if (fresh.status.syncedThroughEpoch < latest.committedEpoch) return await fallback(prefix, "REPLICA_BEHIND_OWN_WRITES");
|
|
1452
|
+
if (laterPending) return await fallback(prefix, "pending_write", { pendingState: laterPending.state });
|
|
1453
|
+
const delivery = checkDelivery();
|
|
1454
|
+
if (delivery.status !== "allow") throw deliveryError(delivery);
|
|
1455
|
+
const refusal = sessionRefusal(prefix);
|
|
1456
|
+
if (refusal) {
|
|
1457
|
+
if (decoded) return restart();
|
|
1458
|
+
return await fallback(prefix, "grant_missing", { code: refusal });
|
|
1459
|
+
}
|
|
1057
1460
|
const event = {
|
|
1058
1461
|
count: page.keys.length,
|
|
1059
1462
|
syncedBeforeRead: fresh.syncedBeforeRead,
|
|
@@ -1064,14 +1467,14 @@ function createKVReplication(deps) {
|
|
|
1064
1467
|
readEvent("list", r.listPath, prefix, "replica", "hit", "found", started, event);
|
|
1065
1468
|
return ok(page);
|
|
1066
1469
|
} catch (error) {
|
|
1470
|
+
if (networkRejected) throw error;
|
|
1067
1471
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
1068
1472
|
const code = errorCode(error);
|
|
1473
|
+
if (code === "CONSENT_EXPIRED") startConsentDenial();
|
|
1069
1474
|
recordRevocation(prefix, error);
|
|
1070
1475
|
if (cursor && decoded) return restart();
|
|
1071
|
-
const reason = reasonForReadMetaCode(code);
|
|
1072
|
-
|
|
1073
|
-
readEvent("list", r.listPath, prefix, "network", reason, result.ok ? "found" : "error", started, { code });
|
|
1074
|
-
return result;
|
|
1476
|
+
const reason = code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : reasonForReadMetaCode(code);
|
|
1477
|
+
return fallback(prefix, reason, { code });
|
|
1075
1478
|
}
|
|
1076
1479
|
}
|
|
1077
1480
|
async function write(r) {
|
|
@@ -1080,11 +1483,14 @@ function createKVReplication(deps) {
|
|
|
1080
1483
|
return prefix !== void 0 ? [{ key: entry.path, op: r.op === "delete" ? "delete" : "put" }] : [];
|
|
1081
1484
|
});
|
|
1082
1485
|
if (covered.length === 0) return r.network();
|
|
1486
|
+
if (checkDelivery().status !== "allow") return r.network();
|
|
1083
1487
|
await reportExistingPins();
|
|
1488
|
+
if (checkDelivery().status !== "allow") return r.network();
|
|
1084
1489
|
const opId = globalThis.crypto?.randomUUID?.() ?? `${scheduler.now()}-${Math.random()}`;
|
|
1085
1490
|
try {
|
|
1086
1491
|
await pending.update((s) => begin(s, opId, covered, nowDate(scheduler.now())));
|
|
1087
1492
|
} catch (error) {
|
|
1493
|
+
if (errorCode(error) === "REPLICA_GATED" || errorCode(error) === "CONSENT_EXPIRED" || checkDelivery().status !== "allow") return r.network();
|
|
1088
1494
|
return err(serviceError(ErrorCodes.KV_WRITE_FAILED, "Unable to persist pending KV write; request was not dispatched", "kv", { cause: error instanceof Error ? error : void 0, meta: { replication: "pending_store_unavailable", requestDispatched: false } }));
|
|
1089
1495
|
}
|
|
1090
1496
|
const started = scheduler.now();
|
|
@@ -1149,8 +1555,32 @@ function createKVReplication(deps) {
|
|
|
1149
1555
|
for (const inspection of active) inspection.abort.abort(Object.assign(new Error("Replica status inspection cancelled"), { code: "ABORTED" }));
|
|
1150
1556
|
return Promise.all(active.map((inspection) => inspection.settled)).then(() => void 0);
|
|
1151
1557
|
}
|
|
1558
|
+
function gatedStatus(decision) {
|
|
1559
|
+
return options.prefixes.map((prefix) => ({
|
|
1560
|
+
prefix,
|
|
1561
|
+
state: "gated",
|
|
1562
|
+
reason: "replica_gated",
|
|
1563
|
+
gate: decision.status === "gated" ? decision.gate : "stamp_stale",
|
|
1564
|
+
errorCode: decision.status === "consent_expired" ? decision.code : "REPLICA_GATED",
|
|
1565
|
+
pending: { inFlight: 0, committed: 0, ambiguous: 0 },
|
|
1566
|
+
pinned: [],
|
|
1567
|
+
lagMs: null,
|
|
1568
|
+
redacted: true
|
|
1569
|
+
}));
|
|
1570
|
+
}
|
|
1152
1571
|
async function status() {
|
|
1153
|
-
const
|
|
1572
|
+
const beforeStatus = checkDelivery();
|
|
1573
|
+
if (beforeStatus.status !== "allow") return gatedStatus(beforeStatus);
|
|
1574
|
+
let persistedSnapshot;
|
|
1575
|
+
try {
|
|
1576
|
+
persistedSnapshot = pending.readExisting ? await pending.readExisting() : await pending.read();
|
|
1577
|
+
} catch (error) {
|
|
1578
|
+
const delivery = checkDelivery();
|
|
1579
|
+
if (delivery.status !== "allow") return gatedStatus(delivery);
|
|
1580
|
+
throw error;
|
|
1581
|
+
}
|
|
1582
|
+
const afterPending = checkDelivery();
|
|
1583
|
+
if (afterPending.status !== "allow") return gatedStatus(afterPending);
|
|
1154
1584
|
const snapshot = persistedSnapshot ?? emptyPendingState(identity);
|
|
1155
1585
|
const entries = await Promise.all(options.prefixes.map(async (prefix) => {
|
|
1156
1586
|
if (prefix === "/" && deps.nodeFeatures !== void 0 && !deps.nodeFeatures.includes("kv-sync-root")) {
|
|
@@ -1169,25 +1599,28 @@ function createKVReplication(deps) {
|
|
|
1169
1599
|
if (!local && !opened?.opening && !opened?.authorityRevoked) inspected = await inspectPersistedStatus(prefix);
|
|
1170
1600
|
local ?? (local = inspected.local);
|
|
1171
1601
|
if (inspected.missing && !opened?.reason && !opened?.authorityRevoked && !opened?.opening && !isClosed) return void 0;
|
|
1172
|
-
const
|
|
1602
|
+
const full = local && !("redacted" in local) ? local : void 0;
|
|
1603
|
+
const revoked = opened?.authorityRevoked === true || full?.authority.state === "revoked";
|
|
1173
1604
|
const rootUnsupported = opened?.lastError === "REPLICATION_ROOT_UNSUPPORTED";
|
|
1174
|
-
const state = isClosed ? "closed" : revoked ? "revoked" : rootUnsupported ? "unavailable" : opened?.reason === "grant_missing" ? "grant_missing" :
|
|
1605
|
+
const state = isClosed ? "closed" : revoked ? "revoked" : rootUnsupported || opened?.reason === "grant_expired" ? "unavailable" : opened?.reason === "grant_missing" ? "grant_missing" : full?.coverage === "complete" && full.authority.state === "valid" ? "ready" : opened?.reason || inspected.errorCode ? "unavailable" : "idle";
|
|
1175
1606
|
return {
|
|
1176
1607
|
prefix,
|
|
1177
1608
|
state,
|
|
1178
1609
|
...opened?.reason ? { reason: opened.reason } : inspected.errorCode ? { reason: "replica_unavailable" } : {},
|
|
1179
1610
|
...inspected.errorCode ?? opened?.lastError ? { errorCode: inspected.errorCode ?? opened?.lastError } : {},
|
|
1180
1611
|
...local ?? {},
|
|
1181
|
-
...revoked ? { authority: { state: "revoked", expiresAt:
|
|
1612
|
+
...revoked ? { authority: { state: "revoked", expiresAt: full?.authority.expiresAt ?? null } } : {},
|
|
1182
1613
|
pending: {
|
|
1183
1614
|
inFlight: records.filter((r) => r.state === "in_flight").length,
|
|
1184
1615
|
committed: records.filter((r) => r.state === "committed").length,
|
|
1185
1616
|
ambiguous: records.filter((r) => r.state === "ambiguous").length
|
|
1186
1617
|
},
|
|
1187
1618
|
pinned: pinnedKeys(snapshot, prefix, scheduler.now()),
|
|
1188
|
-
lagMs:
|
|
1619
|
+
lagMs: full?.lastSyncAt ? scheduler.now() - Date.parse(full.lastSyncAt) : null
|
|
1189
1620
|
};
|
|
1190
1621
|
}));
|
|
1622
|
+
const beforeReturn = checkDelivery();
|
|
1623
|
+
if (beforeReturn.status !== "allow") return gatedStatus(beforeReturn);
|
|
1191
1624
|
return entries.filter((entry) => entry !== void 0);
|
|
1192
1625
|
}
|
|
1193
1626
|
async function sync(o) {
|
|
@@ -1203,11 +1636,15 @@ function createKVReplication(deps) {
|
|
|
1203
1636
|
var _a;
|
|
1204
1637
|
purgeStarted = true;
|
|
1205
1638
|
purgeInProgress = true;
|
|
1639
|
+
cancelConsentTimer?.();
|
|
1640
|
+
cancelConsentTimer = void 0;
|
|
1206
1641
|
const hasStatusInspections = statusInspections.size > 0;
|
|
1207
1642
|
const statusDrain = cancelStatusInspections();
|
|
1208
1643
|
for (const state of replicas.values()) {
|
|
1209
1644
|
state.timer?.();
|
|
1210
1645
|
state.timer = void 0;
|
|
1646
|
+
state.renewalTimer?.();
|
|
1647
|
+
state.renewalTimer = void 0;
|
|
1211
1648
|
state.syncContext && ((_a = state.syncContext).abortCause ?? (_a.abortCause = "purge"));
|
|
1212
1649
|
state.abort?.abort();
|
|
1213
1650
|
state.mintAbort?.abort();
|
|
@@ -1231,6 +1668,7 @@ function createKVReplication(deps) {
|
|
|
1231
1668
|
...[...replicas.values()].flatMap((state) => [
|
|
1232
1669
|
state.opening?.catch(() => void 0),
|
|
1233
1670
|
state.sync?.catch(() => void 0),
|
|
1671
|
+
state.renewing?.catch(() => void 0),
|
|
1234
1672
|
state.handle?.close().catch(() => void 0)
|
|
1235
1673
|
]),
|
|
1236
1674
|
...hasStatusInspections ? [statusDrain] : []
|
|
@@ -1268,11 +1706,15 @@ function createKVReplication(deps) {
|
|
|
1268
1706
|
var _a;
|
|
1269
1707
|
if (isClosed) return;
|
|
1270
1708
|
isClosed = true;
|
|
1709
|
+
cancelConsentTimer?.();
|
|
1710
|
+
cancelConsentTimer = void 0;
|
|
1271
1711
|
const hasStatusInspections = statusInspections.size > 0;
|
|
1272
1712
|
const statusDrain = cancelStatusInspections();
|
|
1273
1713
|
for (const [prefix, state] of replicas) {
|
|
1274
1714
|
state.timer?.();
|
|
1275
1715
|
state.timer = void 0;
|
|
1716
|
+
state.renewalTimer?.();
|
|
1717
|
+
state.renewalTimer = void 0;
|
|
1276
1718
|
state.mintAbort?.abort();
|
|
1277
1719
|
state.syncContext && ((_a = state.syncContext).abortCause ?? (_a.abortCause = "close"));
|
|
1278
1720
|
state.abort?.abort();
|
|
@@ -1281,6 +1723,7 @@ function createKVReplication(deps) {
|
|
|
1281
1723
|
const drain = Promise.all([
|
|
1282
1724
|
...[...replicas.values()].map(async (state) => {
|
|
1283
1725
|
await state.opening?.catch(() => void 0);
|
|
1726
|
+
await state.renewing?.catch(() => void 0);
|
|
1284
1727
|
await state.sync?.catch(() => void 0);
|
|
1285
1728
|
await state.handle?.close().catch(() => void 0);
|
|
1286
1729
|
}),
|
|
@@ -1310,7 +1753,8 @@ function createKVReplication(deps) {
|
|
|
1310
1753
|
cancel();
|
|
1311
1754
|
}
|
|
1312
1755
|
}
|
|
1313
|
-
|
|
1756
|
+
scheduleConsentDeadline();
|
|
1757
|
+
return { get, list, write, observeNetworkRequested, observeConsentExpired, status, sync, purge, clearPending: clearPending2, close };
|
|
1314
1758
|
}
|
|
1315
1759
|
function cancellationCode(signal) {
|
|
1316
1760
|
return wrapError("kv", signal.reason).code === ErrorCodes.TIMEOUT || typeof signal.reason === "object" && signal.reason !== null && "code" in signal.reason && signal.reason.code === ErrorCodes.TIMEOUT ? ErrorCodes.TIMEOUT : ErrorCodes.ABORTED;
|