@tinycloud/sdk-services 3.2.6-beta.4 → 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-1Ug6y7DC.d.cts → BaseService-Cc6Ueicm.d.cts} +1 -1
- package/dist/{BaseService-Bn07Vudy.d.ts → BaseService-DOdXd7sr.d.ts} +1 -1
- package/dist/encryption/index.cjs +8 -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 +8 -0
- package/dist/encryption/index.js.map +1 -1
- package/dist/index.cjs +345 -203
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +107 -7
- package/dist/index.d.ts +107 -7
- package/dist/index.js +344 -203
- 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 +66 -21
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +4 -4
- package/dist/kv/index.d.ts +4 -4
- package/dist/kv/index.js +66 -21
- package/dist/kv/index.js.map +1 -1
- 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/index.cjs +571 -128
- package/dist/kv/replication/index.cjs.map +1 -1
- package/dist/kv/replication/index.d.cts +3 -3
- package/dist/kv/replication/index.d.ts +3 -3
- package/dist/kv/replication/index.js +571 -128
- package/dist/kv/replication/index.js.map +1 -1
- package/dist/{scope-BnyVPZ5E.d.ts → scope-B-m6cKH-.d.ts} +310 -7
- package/dist/{scope-CQ5kmHlt.d.cts → scope-B7oeKdLM.d.cts} +310 -7
- package/dist/sql/index.cjs +70 -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 +70 -36
- package/dist/sql/index.js.map +1 -1
- package/dist/{types-gmdUiorz.d.cts → types-BjH0ZtGy.d.cts} +6 -0
- package/dist/{types-gmdUiorz.d.ts → types-BjH0ZtGy.d.ts} +6 -0
- package/package.json +16 -2
|
@@ -111,6 +111,14 @@ var ErrorCodes = {
|
|
|
111
111
|
ABORTED: "ABORTED",
|
|
112
112
|
INVALID_INPUT: "INVALID_INPUT",
|
|
113
113
|
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
114
|
+
// Node retry signals (tinycloud-node #347). `meta.retryable` says whether a
|
|
115
|
+
// retry is safe; `meta.retryAfterMs` carries the node's Retry-After.
|
|
116
|
+
/** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`: nothing was applied; retry after `meta.retryAfterMs`. */
|
|
117
|
+
NODE_BUSY: "NODE_BUSY",
|
|
118
|
+
/** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */
|
|
119
|
+
REQUEST_TIMEOUT: "REQUEST_TIMEOUT",
|
|
120
|
+
/** HTTP 500 `outcome_unknown`: a write may have been applied; read back before retrying a non-idempotent operation. */
|
|
121
|
+
OUTCOME_UNKNOWN: "OUTCOME_UNKNOWN",
|
|
114
122
|
// KV-specific errors
|
|
115
123
|
KV_NOT_FOUND: "KV_NOT_FOUND",
|
|
116
124
|
KV_WRITE_FAILED: "KV_WRITE_FAILED",
|
|
@@ -404,6 +412,35 @@ function emitEvent(emit, scheduler, event) {
|
|
|
404
412
|
}
|
|
405
413
|
}
|
|
406
414
|
|
|
415
|
+
// src/kv/replication/grantPolicyConstants.ts
|
|
416
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
417
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
418
|
+
var DEFAULT_GRANT_TTL_MS = DAY_MS;
|
|
419
|
+
var MAX_GRANT_TTL_MS = 7 * DAY_MS;
|
|
420
|
+
var CONSENT_WINDOW_MS = 7 * DAY_MS;
|
|
421
|
+
var RENEW_BELOW_FRACTION = 0.25;
|
|
422
|
+
|
|
423
|
+
// src/kv/replication/grantPolicy.ts
|
|
424
|
+
var EXPIRED = Number.NEGATIVE_INFINITY;
|
|
425
|
+
var consentDeadlineMs = (anchor) => Number.isFinite(anchor.at) ? anchor.at + CONSENT_WINDOW_MS : EXPIRED;
|
|
426
|
+
var clampConsentAnchor = (anchor, now) => {
|
|
427
|
+
if (anchor === null || !Number.isFinite(anchor.at) || !Number.isFinite(now)) return null;
|
|
428
|
+
return anchor.at > now ? { at: now, source: anchor.source } : anchor;
|
|
429
|
+
};
|
|
430
|
+
var mintRemainingMs = ({ now, grantTtlMs, consentDeadline }) => {
|
|
431
|
+
if (!Number.isFinite(now) || !Number.isFinite(grantTtlMs) || !Number.isFinite(consentDeadline)) return 0;
|
|
432
|
+
return Math.min(Math.min(grantTtlMs, MAX_GRANT_TTL_MS), consentDeadline - now);
|
|
433
|
+
};
|
|
434
|
+
var needsSilentRenewal = ({ now, installedAt, effectiveExpiresAt, consentDeadline }) => {
|
|
435
|
+
if (!Number.isFinite(now) || !Number.isFinite(installedAt) || !Number.isFinite(effectiveExpiresAt) || !Number.isFinite(consentDeadline)) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
if (!(now < consentDeadline)) return false;
|
|
439
|
+
const remaining = effectiveExpiresAt - now;
|
|
440
|
+
const lifetime = effectiveExpiresAt - installedAt;
|
|
441
|
+
return remaining < lifetime * RENEW_BELOW_FRACTION;
|
|
442
|
+
};
|
|
443
|
+
|
|
407
444
|
// src/kv/replication/controller.ts
|
|
408
445
|
var CLOSE_TIMEOUT_MS = 3e3;
|
|
409
446
|
var STATUS_REFRESH_TIMEOUT_MS = CLOSE_TIMEOUT_MS;
|
|
@@ -431,6 +468,128 @@ function createKVReplication(deps) {
|
|
|
431
468
|
let previous = deps.previous;
|
|
432
469
|
let pinnedStateReported = false;
|
|
433
470
|
const inProcessProof = /* @__PURE__ */ new Set();
|
|
471
|
+
let anchorUnavailable = false;
|
|
472
|
+
let consentAnchor = null;
|
|
473
|
+
try {
|
|
474
|
+
consentAnchor = clampConsentAnchor(authority.consentAnchor?.() ?? null, scheduler.now());
|
|
475
|
+
if (consentAnchor?.source === "first_install") consentAnchor = null;
|
|
476
|
+
} catch {
|
|
477
|
+
anchorUnavailable = true;
|
|
478
|
+
}
|
|
479
|
+
const grantPolicy = { grantTtlMs: options.grantTtlMs ?? DEFAULT_GRANT_TTL_MS, consentAnchor };
|
|
480
|
+
const quota = options.maxBytes === void 0 && options.minFreeBytes === void 0 ? void 0 : { maxBytes: options.maxBytes, minFreeBytes: options.minFreeBytes };
|
|
481
|
+
const policyDeadline = consentAnchor === null ? null : consentDeadlineMs(consentAnchor);
|
|
482
|
+
let scheduledConsentDeadline = null;
|
|
483
|
+
let cancelConsentTimer;
|
|
484
|
+
function durableDeadline() {
|
|
485
|
+
if (storage.kind !== "indexeddb" || !deps.stamp) return null;
|
|
486
|
+
try {
|
|
487
|
+
const deadline = storage.lifecycle?.consentDeadline(deps.stamp);
|
|
488
|
+
return typeof deadline === "number" && Number.isFinite(deadline) ? deadline : null;
|
|
489
|
+
} catch {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function mintDeadline() {
|
|
494
|
+
if (policyDeadline === null) return null;
|
|
495
|
+
if (storage.kind !== "indexeddb") return policyDeadline;
|
|
496
|
+
const durable = durableDeadline();
|
|
497
|
+
return durable === null ? null : Math.min(policyDeadline, durable);
|
|
498
|
+
}
|
|
499
|
+
function scheduleConsentDeadline() {
|
|
500
|
+
const deadline = durableDeadline();
|
|
501
|
+
if (deadline === null || consentDenyStarted || isClosed) return;
|
|
502
|
+
const now = scheduler.now();
|
|
503
|
+
if (!Number.isFinite(now)) return;
|
|
504
|
+
if (scheduledConsentDeadline === deadline && cancelConsentTimer) return;
|
|
505
|
+
scheduledConsentDeadline = deadline;
|
|
506
|
+
cancelConsentTimer?.();
|
|
507
|
+
cancelConsentTimer = scheduler.setTimeout(() => {
|
|
508
|
+
cancelConsentTimer = void 0;
|
|
509
|
+
checkDelivery();
|
|
510
|
+
}, Math.max(0, deadline - now));
|
|
511
|
+
}
|
|
512
|
+
const reportedDeadline = /* @__PURE__ */ new Set();
|
|
513
|
+
let consentDenyStarted = false;
|
|
514
|
+
function checkDelivery() {
|
|
515
|
+
const gate = storage.lifecycle;
|
|
516
|
+
if (!gate) return storage.kind === "indexeddb" || anchorUnavailable || deps.stamp !== void 0 ? { status: "gated", gate: "gate_unavailable" } : { status: "allow" };
|
|
517
|
+
if (!deps.stamp) return { status: "gated", gate: "not_established" };
|
|
518
|
+
if (consentDenyStarted) return { status: "gated", gate: "stamp_stale" };
|
|
519
|
+
try {
|
|
520
|
+
const decision = gate.checkDelivery(deps.stamp);
|
|
521
|
+
if (decision.status === "consent_expired") startConsentDenial();
|
|
522
|
+
if (decision.status !== "allow") return decision;
|
|
523
|
+
if (storage.kind === "indexeddb") {
|
|
524
|
+
const deadline = durableDeadline();
|
|
525
|
+
if (deadline === null) return { status: "gated", gate: "gate_unavailable" };
|
|
526
|
+
const now = scheduler.now();
|
|
527
|
+
if (!Number.isFinite(now)) return { status: "gated", gate: "gate_unavailable" };
|
|
528
|
+
if (now >= deadline) {
|
|
529
|
+
startConsentDenial();
|
|
530
|
+
return { status: "consent_expired", code: "CONSENT_EXPIRED" };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return anchorUnavailable ? { status: "gated", gate: "gate_unavailable" } : decision;
|
|
534
|
+
} catch {
|
|
535
|
+
return { status: "gated", gate: "gate_unavailable" };
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function markConsentExpired() {
|
|
539
|
+
if (consentDenyStarted || isClosed || purgeStarted) return false;
|
|
540
|
+
cancelConsentTimer?.();
|
|
541
|
+
cancelConsentTimer = void 0;
|
|
542
|
+
consentDenyStarted = true;
|
|
543
|
+
for (const state of replicas.values()) {
|
|
544
|
+
state.renewalTimer?.();
|
|
545
|
+
state.renewalTimer = void 0;
|
|
546
|
+
state.mintAbort?.abort();
|
|
547
|
+
const id = state.handle?.replicaId;
|
|
548
|
+
if (id && !reportedDeadline.has(id)) {
|
|
549
|
+
reportedDeadline.add(id);
|
|
550
|
+
emit({ type: "replication.grant", replicaId: id, action: "consent_deadline_reached", code: "CONSENT_EXPIRED" });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
function observeConsentExpired(stamp) {
|
|
556
|
+
if (!deps.stamp || stamp.incarnation !== deps.stamp.incarnation || stamp.lifecycleEpoch !== deps.stamp.lifecycleEpoch) return;
|
|
557
|
+
if (markConsentExpired()) emit({ type: "replication.lifecycle", action: "denied", reason: "consent_expired" });
|
|
558
|
+
}
|
|
559
|
+
function startConsentDenial() {
|
|
560
|
+
const gate = storage.lifecycle;
|
|
561
|
+
if (!gate || !markConsentExpired()) return;
|
|
562
|
+
void gate.deny({ reason: "consent_expired", principal: identity.principal }).then(
|
|
563
|
+
(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" }),
|
|
564
|
+
() => emit({ type: "replication.lifecycle", action: "deny_failed", reason: "consent_expired" })
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
function deliveryError(decision) {
|
|
568
|
+
return Object.assign(new Error(decision.status === "consent_expired" ? "Replica consent expired" : "Replica session is gated"), {
|
|
569
|
+
code: decision.status === "consent_expired" ? decision.code : "REPLICA_GATED",
|
|
570
|
+
...decision.status === "gated" ? { gate: decision.gate } : {}
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
function deliveryReason(code) {
|
|
574
|
+
return code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? "grant_expired" : code === "REPLICA_GATED" ? "replica_gated" : "replica_error";
|
|
575
|
+
}
|
|
576
|
+
function reportGrantExpiry(prefix) {
|
|
577
|
+
const state = replicas.get(prefix);
|
|
578
|
+
if (!state) return;
|
|
579
|
+
state.reason = "grant_expired";
|
|
580
|
+
state.lastError = "GRANT_EXPIRED";
|
|
581
|
+
const replicaId = state.handle?.replicaId;
|
|
582
|
+
if (!replicaId || state.reportedExpired) return;
|
|
583
|
+
state.reportedExpired = true;
|
|
584
|
+
emit({ type: "replication.grant", replicaId, action: "expired", code: "GRANT_EXPIRED" });
|
|
585
|
+
}
|
|
586
|
+
function reportGrantCap(state, handle, grant) {
|
|
587
|
+
if (!grant?.unconstrained || !handle.replicaId || typeof grant.effectiveExpiresAt !== "number" || !Number.isFinite(grant.effectiveExpiresAt) || !(grant.expiresAt === null || grant.effectiveExpiresAt < grant.expiresAt)) return;
|
|
588
|
+
const capKey = `${grant.cid}:${grant.effectiveExpiresAt}`;
|
|
589
|
+
if (state.reportedCappedCid === capKey) return;
|
|
590
|
+
state.reportedCappedCid = capKey;
|
|
591
|
+
emit({ type: "replication.grant", replicaId: handle.replicaId, action: "capped", effectiveExpiresAt: nowDate(grant.effectiveExpiresAt) });
|
|
592
|
+
}
|
|
434
593
|
const configuredPrefix = (path) => options.prefixes.find((prefix) => kvPrefixCovers(prefix, path));
|
|
435
594
|
function recordRevocation(prefix, error) {
|
|
436
595
|
if (errorCode(error) !== "GRANT_REVOKED") return;
|
|
@@ -454,7 +613,26 @@ function createKVReplication(deps) {
|
|
|
454
613
|
markRootUnsupported(prefix);
|
|
455
614
|
return Object.assign(new Error("This node doesn't support whole-space sync yet."), { code: "REPLICATION_ROOT_UNSUPPORTED" });
|
|
456
615
|
}
|
|
616
|
+
function canReuseInstalled(grant, sessionOnly) {
|
|
617
|
+
if (!grant?.unconstrained) return false;
|
|
618
|
+
if (!deps.stamp) return true;
|
|
619
|
+
if (!sessionOnly && consentAnchor === null) return false;
|
|
620
|
+
const now = scheduler.now();
|
|
621
|
+
const deadline = storage.kind === "indexeddb" ? durableDeadline() : policyDeadline ?? grant.consentDeadline;
|
|
622
|
+
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);
|
|
623
|
+
}
|
|
624
|
+
function sessionRefusal(prefix) {
|
|
625
|
+
try {
|
|
626
|
+
if (authority.sessionOnly !== true) return void 0;
|
|
627
|
+
const grant = authority.sessionGrant(prefix);
|
|
628
|
+
return "refused" in grant ? grant.refused : void 0;
|
|
629
|
+
} catch {
|
|
630
|
+
return "NOT_COVERED";
|
|
631
|
+
}
|
|
632
|
+
}
|
|
457
633
|
async function open(prefix) {
|
|
634
|
+
const delivery = checkDelivery();
|
|
635
|
+
if (delivery.status !== "allow") throw deliveryError(delivery);
|
|
458
636
|
if (isClosed || purgeStarted) throw Object.assign(new Error("Replication controller is closed"), { code: "REPLICA_CLOSED" });
|
|
459
637
|
let state = replicas.get(prefix);
|
|
460
638
|
if (state?.handle) return state.handle;
|
|
@@ -475,46 +653,99 @@ function createKVReplication(deps) {
|
|
|
475
653
|
await previous;
|
|
476
654
|
previous = void 0;
|
|
477
655
|
}
|
|
656
|
+
const delivery2 = checkDelivery();
|
|
657
|
+
if (delivery2.status !== "allow") throw deliveryError(delivery2);
|
|
658
|
+
const sessionOnly = authority.sessionOnly === true;
|
|
478
659
|
const sessionGrant = authority.sessionGrant(prefix);
|
|
479
660
|
const delegated = "refused" in sessionGrant ? void 0 : sessionGrant;
|
|
480
|
-
const plan =
|
|
661
|
+
const plan = sessionOnly ? void 0 : authority.plan(prefix);
|
|
481
662
|
let refusalCode;
|
|
482
663
|
if ("refused" in sessionGrant) refusalCode = sessionGrant.refused;
|
|
483
664
|
if (plan && "refused" in plan) refusalCode ?? (refusalCode = plan.refused);
|
|
665
|
+
if (deps.stamp && consentAnchor === null && !sessionOnly) refusalCode = "REPLICA_GATED";
|
|
484
666
|
let device;
|
|
485
667
|
if (delegated) {
|
|
486
668
|
device = delegated.device;
|
|
487
669
|
state.strategy = "session";
|
|
488
670
|
}
|
|
489
|
-
const spec = {
|
|
671
|
+
const spec = {
|
|
672
|
+
identity,
|
|
673
|
+
space: session.space,
|
|
674
|
+
prefix,
|
|
675
|
+
allowSecrets: options.allowSecrets,
|
|
676
|
+
grantPolicy,
|
|
677
|
+
...quota ? { quota } : {},
|
|
678
|
+
...deps.stamp ? { stamp: deps.stamp } : {},
|
|
679
|
+
...device ? { device } : {},
|
|
680
|
+
...deps.fetch ? { fetch: deps.fetch } : {},
|
|
681
|
+
onPeerCommit: (info) => {
|
|
682
|
+
if (isClosed || purgeStarted || state.handle === void 0) return;
|
|
683
|
+
emit({
|
|
684
|
+
type: "replication.sync",
|
|
685
|
+
space: session.space,
|
|
686
|
+
replica: prefix,
|
|
687
|
+
trigger: "peer",
|
|
688
|
+
origin: "peer",
|
|
689
|
+
outcome: "ok",
|
|
690
|
+
durationMs: 0,
|
|
691
|
+
lagMs: null,
|
|
692
|
+
...info.changes === void 0 ? {} : { changes: info.changes },
|
|
693
|
+
...info.coverage === void 0 ? {} : { coverage: info.coverage }
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
};
|
|
490
697
|
if (prefix === "/" && deps.nodeFeatures !== void 0 && !deps.nodeFeatures.includes("kv-sync-root")) {
|
|
491
698
|
throw unsupportedRoot(prefix);
|
|
492
699
|
}
|
|
493
700
|
const handle = await storage.open(spec);
|
|
494
701
|
state.handle = handle;
|
|
702
|
+
const afterOpen = checkDelivery();
|
|
703
|
+
if (afterOpen.status !== "allow") throw deliveryError(afterOpen);
|
|
495
704
|
emit({ type: "replication.state", space: session.space, replica: prefix, state: "opened" });
|
|
496
|
-
const runtimePlan = !
|
|
497
|
-
let installed = runtimePlan ? await handle.grant() : void 0;
|
|
498
|
-
if (
|
|
499
|
-
|
|
705
|
+
const runtimePlan = !sessionOnly && plan && !("refused" in plan) ? plan : void 0;
|
|
706
|
+
let installed = runtimePlan || delegated ? await handle.grant() : void 0;
|
|
707
|
+
if (installed && !canReuseInstalled(installed, sessionOnly)) {
|
|
708
|
+
if (installed.unconstrained) refusalCode = consentAnchor === null && !sessionOnly ? "REPLICA_GATED" : "GRANT_EXPIRED";
|
|
709
|
+
installed = void 0;
|
|
710
|
+
}
|
|
711
|
+
let mintedExpiryMs;
|
|
712
|
+
if (delegated && consentAnchor !== null) {
|
|
713
|
+
const beforeInstall = checkDelivery();
|
|
714
|
+
if (beforeInstall.status !== "allow") throw deliveryError(beforeInstall);
|
|
715
|
+
installed = await handle.installGrant(delegated.ucan);
|
|
716
|
+
} else if (runtimePlan) {
|
|
500
717
|
if (!installed || !installed.unconstrained || installed.parentCid !== runtimePlan.parentCid || installed.expiresAt !== null && runtimePlan.expiresAt > installed.expiresAt + 3e5) {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
718
|
+
const deadline = mintDeadline();
|
|
719
|
+
const expiryMs = mintRemainingMs({ now: scheduler.now(), grantTtlMs: grantPolicy.grantTtlMs, consentDeadline: deadline ?? Number.NaN });
|
|
720
|
+
if (expiryMs > 0) {
|
|
721
|
+
try {
|
|
722
|
+
const mintAbort = new AbortController();
|
|
723
|
+
state.mintAbort = mintAbort;
|
|
724
|
+
const minted = await authority.mint(handle.deviceDid, prefix, mintAbort.signal, { expiryMs });
|
|
725
|
+
const beforeInstall = checkDelivery();
|
|
726
|
+
if (beforeInstall.status !== "allow") throw deliveryError(beforeInstall);
|
|
727
|
+
installed = await handle.installGrant(minted.ucan);
|
|
728
|
+
state.strategy = "minted";
|
|
729
|
+
mintedExpiryMs = expiryMs;
|
|
730
|
+
} catch (error) {
|
|
731
|
+
if (errorCode(error) === "CONSENT_EXPIRED" || errorCode(error) === "REPLICA_GATED" || !installed?.unconstrained) throw error;
|
|
732
|
+
} finally {
|
|
733
|
+
state.mintAbort = void 0;
|
|
734
|
+
}
|
|
511
735
|
}
|
|
512
736
|
}
|
|
513
737
|
if (installed?.unconstrained) {
|
|
514
738
|
state.strategy ?? (state.strategy = "installed");
|
|
515
739
|
state.reason = void 0;
|
|
516
740
|
}
|
|
741
|
+
} else if ((delegated || sessionOnly) && installed?.unconstrained) {
|
|
742
|
+
state.strategy = "installed";
|
|
517
743
|
}
|
|
744
|
+
const beforeGrantEvent = checkDelivery();
|
|
745
|
+
if (beforeGrantEvent.status !== "allow") throw deliveryError(beforeGrantEvent);
|
|
746
|
+
reportGrantCap(state, handle, installed);
|
|
747
|
+
if (mintedExpiryMs !== void 0) state.reportedExpired = false;
|
|
748
|
+
if (mintedExpiryMs !== void 0 && handle.replicaId) emit({ type: "replication.grant", replicaId: handle.replicaId, action: "minted", expiryMs: mintedExpiryMs });
|
|
518
749
|
if (!installed?.unconstrained) {
|
|
519
750
|
state.reason = "grant_missing";
|
|
520
751
|
state.lastError = refusalCode ?? "SESSION_LACKS_SYNC";
|
|
@@ -524,6 +755,7 @@ function createKVReplication(deps) {
|
|
|
524
755
|
await handle.close();
|
|
525
756
|
throw Object.assign(new Error("Replication controller is closed"), { code: "REPLICA_CLOSED" });
|
|
526
757
|
}
|
|
758
|
+
if (installed?.unconstrained) await maybeRenew(prefix, handle, installed, runtimePlan);
|
|
527
759
|
if (mode === "background" && state.reason !== "grant_missing") {
|
|
528
760
|
void syncPrefix(prefix, "start").catch(() => void 0);
|
|
529
761
|
schedule(prefix);
|
|
@@ -534,12 +766,14 @@ function createKVReplication(deps) {
|
|
|
534
766
|
return await state.opening;
|
|
535
767
|
} catch (error) {
|
|
536
768
|
const code = errorCode(error);
|
|
769
|
+
if (code === "CONSENT_EXPIRED") startConsentDenial();
|
|
537
770
|
recordRevocation(prefix, error);
|
|
538
|
-
if (code
|
|
771
|
+
if (code === "REPLICA_GATED" || code === "CONSENT_EXPIRED") state.reason = deliveryReason(code);
|
|
772
|
+
else if (code !== "GRANT_REVOKED") state.reason = "replica_unavailable";
|
|
539
773
|
state.lastError = code;
|
|
540
774
|
state.failures = (state.failures ?? 0) + 1;
|
|
541
775
|
if (code === "RUNTIME_UNSUPPORTED") state.unsupported = true;
|
|
542
|
-
else if (code !== "REPLICA_CLOSED" && code !== "REPLICATION_ROOT_UNSUPPORTED") state.retryAt = scheduler.now() + Math.min(3e5, 5e3 * 2 ** Math.min(state.failures - 1, 6));
|
|
776
|
+
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));
|
|
543
777
|
state.handle?.close().catch(() => void 0);
|
|
544
778
|
state.handle = void 0;
|
|
545
779
|
throw error;
|
|
@@ -547,6 +781,96 @@ function createKVReplication(deps) {
|
|
|
547
781
|
state.opening = void 0;
|
|
548
782
|
}
|
|
549
783
|
}
|
|
784
|
+
function scheduleRenewal(prefix, handle, grant, plan) {
|
|
785
|
+
const state = replicas.get(prefix);
|
|
786
|
+
state?.renewalTimer?.();
|
|
787
|
+
if (state) state.renewalTimer = void 0;
|
|
788
|
+
if (!state || isClosed || purgeStarted || plan.path !== "session" || !authority.canRenew) return;
|
|
789
|
+
const { installedAt, effectiveExpiresAt } = grant;
|
|
790
|
+
const deadline = mintDeadline();
|
|
791
|
+
if (typeof installedAt !== "number" || !Number.isFinite(installedAt) || typeof effectiveExpiresAt !== "number" || !Number.isFinite(effectiveExpiresAt) || !(effectiveExpiresAt > installedAt) || deadline === null) return;
|
|
792
|
+
const threshold = installedAt + (effectiveExpiresAt - installedAt) * (1 - RENEW_BELOW_FRACTION);
|
|
793
|
+
const now = scheduler.now();
|
|
794
|
+
if (!Number.isFinite(now) || !(threshold < deadline) || now > threshold) return;
|
|
795
|
+
state.renewalTimer = scheduler.setTimeout(() => {
|
|
796
|
+
state.renewalTimer = void 0;
|
|
797
|
+
void maybeRenew(prefix, handle).catch(() => void 0);
|
|
798
|
+
}, Math.max(0, Math.floor(threshold) + 1 - now));
|
|
799
|
+
}
|
|
800
|
+
async function maybeRenew(prefix, handle, knownGrant, knownPlan) {
|
|
801
|
+
const state = replicas.get(prefix);
|
|
802
|
+
if (!state || state.handle !== handle || isClosed || purgeStarted || !authority.canRenew) return;
|
|
803
|
+
if (state.renewing) return state.renewing;
|
|
804
|
+
const decision = checkDelivery();
|
|
805
|
+
if (decision.status !== "allow") return;
|
|
806
|
+
const grant = knownGrant ?? await handle.grant();
|
|
807
|
+
if (!grant?.unconstrained) return;
|
|
808
|
+
const { installedAt, effectiveExpiresAt } = grant;
|
|
809
|
+
const deadline = mintDeadline();
|
|
810
|
+
if (typeof installedAt !== "number" || !Number.isFinite(installedAt) || typeof effectiveExpiresAt !== "number" || !Number.isFinite(effectiveExpiresAt) || deadline === null) return;
|
|
811
|
+
const now = scheduler.now();
|
|
812
|
+
if (!needsSilentRenewal({ now, installedAt, effectiveExpiresAt, consentDeadline: deadline })) {
|
|
813
|
+
if (knownPlan) scheduleRenewal(prefix, handle, grant, knownPlan);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
let plan = knownPlan;
|
|
817
|
+
try {
|
|
818
|
+
if (!plan) {
|
|
819
|
+
const selected = authority.plan(prefix);
|
|
820
|
+
if ("refused" in selected) {
|
|
821
|
+
if (handle.replicaId) emit({ type: "replication.grant", replicaId: handle.replicaId, action: "renewal_refused", code: selected.refused });
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
plan = selected;
|
|
825
|
+
}
|
|
826
|
+
} catch {
|
|
827
|
+
if (handle.replicaId) emit({ type: "replication.grant", replicaId: handle.replicaId, action: "renewal_deferred" });
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
if (plan?.path !== "session") {
|
|
831
|
+
if (handle.replicaId && state.reportedRenewalUnqualifiedCid !== grant.cid) {
|
|
832
|
+
state.reportedRenewalUnqualifiedCid = grant.cid;
|
|
833
|
+
emit({ type: "replication.grant", replicaId: handle.replicaId, action: "renewal_deferred" });
|
|
834
|
+
}
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const expiryMs = mintRemainingMs({ now, grantTtlMs: grantPolicy.grantTtlMs, consentDeadline: deadline });
|
|
838
|
+
if (!(expiryMs > 0)) return;
|
|
839
|
+
state.renewalTimer?.();
|
|
840
|
+
state.renewalTimer = void 0;
|
|
841
|
+
const mintAbort = new AbortController();
|
|
842
|
+
state.mintAbort = mintAbort;
|
|
843
|
+
const job = Promise.resolve().then(async () => {
|
|
844
|
+
const minted = await authority.mint(handle.deviceDid, prefix, mintAbort.signal, { expiryMs, renewalOnly: true });
|
|
845
|
+
const beforeInstall = checkDelivery();
|
|
846
|
+
if (beforeInstall.status !== "allow") throw deliveryError(beforeInstall);
|
|
847
|
+
const installed = await handle.installGrant(minted.ucan);
|
|
848
|
+
const beforeEvent = checkDelivery();
|
|
849
|
+
if (beforeEvent.status !== "allow") throw deliveryError(beforeEvent);
|
|
850
|
+
reportGrantCap(state, handle, installed);
|
|
851
|
+
state.reportedExpired = false;
|
|
852
|
+
if (state.reason === "grant_expired") state.reason = void 0;
|
|
853
|
+
if (state.lastError === "GRANT_EXPIRED") state.lastError = void 0;
|
|
854
|
+
if (handle.replicaId) emit({ type: "replication.grant", replicaId: handle.replicaId, action: "renewed", expiryMs });
|
|
855
|
+
if (typeof installed.effectiveExpiresAt === "number" && installed.effectiveExpiresAt > effectiveExpiresAt) {
|
|
856
|
+
scheduleRenewal(prefix, handle, installed, plan);
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
state.renewing = job;
|
|
860
|
+
try {
|
|
861
|
+
await job;
|
|
862
|
+
} catch (error) {
|
|
863
|
+
const code = errorCode(error);
|
|
864
|
+
if (code === "CONSENT_EXPIRED" || code === "REPLICA_GATED") throw error;
|
|
865
|
+
if (!isClosed && handle.replicaId) {
|
|
866
|
+
const refused = typeof error === "object" && error !== null && "refused" in error && typeof error.refused === "string" ? error.refused : null;
|
|
867
|
+
emit({ type: "replication.grant", replicaId: handle.replicaId, action: refused ? "renewal_refused" : "renewal_deferred", code: refused ?? code });
|
|
868
|
+
}
|
|
869
|
+
} finally {
|
|
870
|
+
state.renewing = void 0;
|
|
871
|
+
if (state.mintAbort === mintAbort) state.mintAbort = void 0;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
550
874
|
function schedule(prefix) {
|
|
551
875
|
const state = replicas.get(prefix);
|
|
552
876
|
if (!state || mode !== "background" || isClosed || purgeStarted) return;
|
|
@@ -559,6 +883,8 @@ function createKVReplication(deps) {
|
|
|
559
883
|
if (prefix === "/" && deps.nodeFeatures !== void 0 && !deps.nodeFeatures.includes("kv-sync-root")) {
|
|
560
884
|
throw unsupportedRoot(prefix);
|
|
561
885
|
}
|
|
886
|
+
const initialRefusal = sessionRefusal(prefix);
|
|
887
|
+
if (initialRefusal) throw Object.assign(new Error("Current session does not cover replica sync"), { code: initialRefusal });
|
|
562
888
|
const handle = await open(prefix);
|
|
563
889
|
const state = replicas.get(prefix);
|
|
564
890
|
if (state.sync) return state.sync;
|
|
@@ -576,22 +902,36 @@ function createKVReplication(deps) {
|
|
|
576
902
|
job = (async () => {
|
|
577
903
|
try {
|
|
578
904
|
const syncStartEpoch = (await pending.read()).committedEpoch;
|
|
905
|
+
const beforeSync = checkDelivery();
|
|
906
|
+
if (beforeSync.status !== "allow") throw deliveryError(beforeSync);
|
|
907
|
+
await maybeRenew(prefix, handle);
|
|
908
|
+
const afterRenewal = checkDelivery();
|
|
909
|
+
if (afterRenewal.status !== "allow") throw deliveryError(afterRenewal);
|
|
910
|
+
const refusal = sessionRefusal(prefix);
|
|
911
|
+
if (refusal) throw Object.assign(new Error("Current session does not cover replica sync"), { code: refusal });
|
|
579
912
|
const result = await handle.sync({ signal: abort.signal, syncStartEpoch });
|
|
913
|
+
const afterSyncDelivery = checkDelivery();
|
|
914
|
+
if (afterSyncDelivery.status !== "allow") throw deliveryError(afterSyncDelivery);
|
|
580
915
|
if (result.status === "busy") {
|
|
581
916
|
emit({ type: "replication.sync", space: session.space, replica: prefix, trigger, outcome: "busy", durationMs: scheduler.now() - started, lagMs: null });
|
|
582
917
|
return result;
|
|
583
918
|
}
|
|
584
919
|
inProcessProof.add(prefix);
|
|
585
920
|
const snapshot = await pending.read();
|
|
921
|
+
const beforePending = checkDelivery();
|
|
922
|
+
if (beforePending.status !== "allow") throw deliveryError(beforePending);
|
|
586
923
|
const clearedCandidates = snapshot.records.some((r) => r.state === "committed" && r.epoch !== null && r.epoch <= syncStartEpoch && kvPrefixCovers(prefix, r.key));
|
|
587
924
|
const cleared = clearedCandidates ? await pending.update((s) => afterSync(s, prefix, syncStartEpoch)) : 0;
|
|
925
|
+
const beforeResult = checkDelivery();
|
|
926
|
+
if (beforeResult.status !== "allow") throw deliveryError(beforeResult);
|
|
588
927
|
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 });
|
|
589
928
|
return result;
|
|
590
929
|
} catch (error) {
|
|
591
930
|
const code = errorCode(error);
|
|
592
931
|
state.lastError = code;
|
|
593
932
|
recordRevocation(prefix, error);
|
|
594
|
-
|
|
933
|
+
if (code === "CONSENT_EXPIRED") startConsentDenial();
|
|
934
|
+
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 });
|
|
595
935
|
throw error;
|
|
596
936
|
} finally {
|
|
597
937
|
signal?.removeEventListener("abort", abortFromCaller);
|
|
@@ -747,8 +1087,19 @@ function createKVReplication(deps) {
|
|
|
747
1087
|
return { status: currentStatus, syncedBeforeRead: false };
|
|
748
1088
|
}
|
|
749
1089
|
function readEvent(op, path, prefix, source, reason, outcome, started, extra = {}) {
|
|
1090
|
+
if (prefix !== null && reason === "grant_expired" && extra.code !== "CONSENT_EXPIRED") reportGrantExpiry(prefix);
|
|
750
1091
|
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 });
|
|
751
1092
|
}
|
|
1093
|
+
async function networkRead(network, op, path, prefix, reason, started, extra = {}) {
|
|
1094
|
+
let outcome = "error";
|
|
1095
|
+
try {
|
|
1096
|
+
const result = await network();
|
|
1097
|
+
outcome = result.ok ? "found" : op === "get" && (reason === "pending_write" || reason === "not_covered") && result.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error";
|
|
1098
|
+
return result;
|
|
1099
|
+
} finally {
|
|
1100
|
+
readEvent(op, path, prefix, "network", reason, outcome, started, extra);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
752
1103
|
async function verifyGet(r, local, reason, started, extra) {
|
|
753
1104
|
const prefix = configuredPrefix(r.path) ?? null;
|
|
754
1105
|
const localFound = local.ok;
|
|
@@ -760,6 +1111,16 @@ function createKVReplication(deps) {
|
|
|
760
1111
|
readEvent("get", r.path, prefix, "none", "aborted", "error", started, { ...extra, verify: "aborted" });
|
|
761
1112
|
return abortedResult(r.signal);
|
|
762
1113
|
}
|
|
1114
|
+
const delivery = checkDelivery();
|
|
1115
|
+
if (delivery.status !== "allow") {
|
|
1116
|
+
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" });
|
|
1117
|
+
return network;
|
|
1118
|
+
}
|
|
1119
|
+
const refusal = prefix === null ? void 0 : sessionRefusal(prefix);
|
|
1120
|
+
if (refusal) {
|
|
1121
|
+
readEvent("get", r.path, prefix, "network", "grant_missing", network.ok ? "found" : "error", started, { code: refusal });
|
|
1122
|
+
return network;
|
|
1123
|
+
}
|
|
763
1124
|
if (!network.ok && network.error.code !== ErrorCodes.KV_NOT_FOUND) {
|
|
764
1125
|
readEvent("get", r.path, prefix, "replica", reason, outcome, started, { ...extra, code: network.error.code, verify: "error" });
|
|
765
1126
|
return local;
|
|
@@ -790,6 +1151,16 @@ function createKVReplication(deps) {
|
|
|
790
1151
|
readEvent("get", r.path, prefix, "none", "aborted", "error", started, { ...extra, verify: "aborted" });
|
|
791
1152
|
return abortedResult(r.signal);
|
|
792
1153
|
}
|
|
1154
|
+
const delivery = checkDelivery();
|
|
1155
|
+
if (delivery.status !== "allow") {
|
|
1156
|
+
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" });
|
|
1157
|
+
return err(wrapError("kv", error));
|
|
1158
|
+
}
|
|
1159
|
+
const refusal = prefix === null ? void 0 : sessionRefusal(prefix);
|
|
1160
|
+
if (refusal) {
|
|
1161
|
+
readEvent("get", r.path, prefix, "network", "grant_missing", "error", started, { code: refusal });
|
|
1162
|
+
return err(wrapError("kv", error));
|
|
1163
|
+
}
|
|
793
1164
|
readEvent("get", r.path, prefix, "replica", reason, outcome, started, { ...extra, code: errorCode(error), verify: "error" });
|
|
794
1165
|
return local;
|
|
795
1166
|
}
|
|
@@ -802,6 +1173,16 @@ function createKVReplication(deps) {
|
|
|
802
1173
|
readEvent("list", r.listPath, prefix, "none", "aborted", "error", started, { ...extra, verify: "aborted" });
|
|
803
1174
|
return abortedResult(r.signal);
|
|
804
1175
|
}
|
|
1176
|
+
const delivery = checkDelivery();
|
|
1177
|
+
if (delivery.status !== "allow") {
|
|
1178
|
+
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" });
|
|
1179
|
+
return network;
|
|
1180
|
+
}
|
|
1181
|
+
const refusal = prefix === null ? void 0 : sessionRefusal(prefix);
|
|
1182
|
+
if (refusal) {
|
|
1183
|
+
readEvent("list", r.listPath, prefix, "network", "grant_missing", network.ok ? "found" : "error", started, { code: refusal });
|
|
1184
|
+
return network;
|
|
1185
|
+
}
|
|
805
1186
|
if (!network.ok) {
|
|
806
1187
|
readEvent("list", r.listPath, prefix, "replica", "hit", "found", started, { ...extra, code: network.error.code, verify: "error" });
|
|
807
1188
|
return ok(local);
|
|
@@ -832,6 +1213,16 @@ function createKVReplication(deps) {
|
|
|
832
1213
|
readEvent("list", r.listPath, prefix, "none", "aborted", "error", started, { ...extra, verify: "aborted" });
|
|
833
1214
|
return abortedResult(r.signal);
|
|
834
1215
|
}
|
|
1216
|
+
const delivery = checkDelivery();
|
|
1217
|
+
if (delivery.status !== "allow") {
|
|
1218
|
+
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" });
|
|
1219
|
+
return err(wrapError("kv", error));
|
|
1220
|
+
}
|
|
1221
|
+
const refusal = prefix === null ? void 0 : sessionRefusal(prefix);
|
|
1222
|
+
if (refusal) {
|
|
1223
|
+
readEvent("list", r.listPath, prefix, "network", "grant_missing", "error", started, { code: refusal });
|
|
1224
|
+
return err(wrapError("kv", error));
|
|
1225
|
+
}
|
|
835
1226
|
readEvent("list", r.listPath, prefix, "replica", "hit", "found", started, { ...extra, code: errorCode(error), verify: "error" });
|
|
836
1227
|
return ok(local);
|
|
837
1228
|
}
|
|
@@ -860,11 +1251,26 @@ function createKVReplication(deps) {
|
|
|
860
1251
|
observeNetworkRequested({ op: "get", space: r.space, path: r.path, reason: "NETWORK_REQUESTED", outcome, latencyMs: scheduler.now() - started });
|
|
861
1252
|
}
|
|
862
1253
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1254
|
+
let networkRejected = false;
|
|
1255
|
+
const fallback = async (replica, reason, extra = {}) => {
|
|
1256
|
+
try {
|
|
1257
|
+
return await networkRead(r.network, "get", r.path, replica, reason, started, extra);
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
networkRejected = true;
|
|
1260
|
+
throw error;
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
if (prefix === void 0) return networkRead(r.network, "get", r.path, null, "not_covered", started);
|
|
1264
|
+
const delivery = checkDelivery();
|
|
1265
|
+
if (delivery.status !== "allow") {
|
|
1266
|
+
return fallback(
|
|
1267
|
+
prefix,
|
|
1268
|
+
delivery.status === "consent_expired" ? "grant_expired" : "replica_gated",
|
|
1269
|
+
{ code: delivery.status === "consent_expired" ? delivery.code : "REPLICA_GATED" }
|
|
1270
|
+
);
|
|
867
1271
|
}
|
|
1272
|
+
const refusal = sessionRefusal(prefix);
|
|
1273
|
+
if (refusal) return fallback(prefix, "grant_missing", { code: refusal });
|
|
868
1274
|
try {
|
|
869
1275
|
await raceSignal(reportExistingPins(), r.signal);
|
|
870
1276
|
} catch {
|
|
@@ -875,86 +1281,63 @@ function createKVReplication(deps) {
|
|
|
875
1281
|
handle = await raceSignal(open(prefix), r.signal);
|
|
876
1282
|
} catch (error) {
|
|
877
1283
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
return
|
|
1284
|
+
if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
|
|
1285
|
+
const code = errorCode(error);
|
|
1286
|
+
return fallback(prefix, code === "RUNTIME_UNSUPPORTED" ? "runtime_unsupported" : code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_unavailable", { code });
|
|
881
1287
|
}
|
|
882
1288
|
if (replicas.get(prefix)?.reason === "grant_missing") {
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
return value;
|
|
1289
|
+
const code = replicas.get(prefix)?.lastError;
|
|
1290
|
+
return fallback(prefix, code === "GRANT_EXPIRED" ? "grant_expired" : code === "REPLICA_GATED" ? "replica_gated" : "grant_missing", { code });
|
|
886
1291
|
}
|
|
887
1292
|
let fresh;
|
|
888
1293
|
try {
|
|
889
1294
|
fresh = await freshness(prefix, handle, r.signal);
|
|
890
1295
|
} catch (error) {
|
|
891
1296
|
recordRevocation(prefix, error);
|
|
1297
|
+
if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
|
|
892
1298
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
return value;
|
|
1299
|
+
const code = errorCode(error);
|
|
1300
|
+
return fallback(prefix, code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_error", { code });
|
|
896
1301
|
}
|
|
897
1302
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
898
1303
|
if (fresh.failure) {
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
1304
|
+
const code = fresh.failure === "busy" ? "REPLICA_BUSY" : fresh.syncError;
|
|
1305
|
+
return fallback(
|
|
1306
|
+
prefix,
|
|
1307
|
+
code === "GRANT_REVOKED" ? "grant_revoked" : code === "REPLICA_GATED" || code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "stale",
|
|
1308
|
+
{ code, syncedBeforeRead: fresh.syncedBeforeRead }
|
|
1309
|
+
);
|
|
902
1310
|
}
|
|
903
1311
|
let pendingState;
|
|
904
1312
|
try {
|
|
905
1313
|
pendingState = await raceSignal(pending.read(), r.signal);
|
|
906
1314
|
} catch (error) {
|
|
907
1315
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
return value;
|
|
1316
|
+
const code = errorCode(error);
|
|
1317
|
+
return fallback(prefix, code === "REPLICA_GATED" || code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_unavailable", { code });
|
|
911
1318
|
}
|
|
912
1319
|
const record = pendingState.records.find((x) => x.key === r.path);
|
|
913
|
-
if (record) {
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
if (!pending.durable && !inProcessProof.has(prefix)) {
|
|
919
|
-
const value = await r.network();
|
|
920
|
-
readEvent("get", r.path, prefix, "network", "REPLICA_UNPROVEN_SINCE_START", value.ok ? "found" : "error", started);
|
|
921
|
-
return value;
|
|
922
|
-
}
|
|
923
|
-
if (fresh.status.syncedThroughEpoch < pendingState.committedEpoch) {
|
|
924
|
-
const value = await r.network();
|
|
925
|
-
readEvent("get", r.path, prefix, "network", "REPLICA_BEHIND_OWN_WRITES", value.ok ? "found" : "error", started);
|
|
926
|
-
return value;
|
|
927
|
-
}
|
|
928
|
-
if (fresh.status.authority.state !== "valid") {
|
|
929
|
-
const value = await r.network();
|
|
930
|
-
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);
|
|
931
|
-
return value;
|
|
932
|
-
}
|
|
933
|
-
if (fresh.status.coverage !== "complete") {
|
|
934
|
-
const value = await r.network();
|
|
935
|
-
readEvent("get", r.path, prefix, "network", "coverage_incomplete", value.ok ? "found" : "error", started);
|
|
936
|
-
return value;
|
|
937
|
-
}
|
|
1320
|
+
if (record) return fallback(prefix, "pending_write", { pendingState: record.state, syncedBeforeRead: fresh.syncedBeforeRead });
|
|
1321
|
+
if (!pending.durable && !inProcessProof.has(prefix)) return fallback(prefix, "REPLICA_UNPROVEN_SINCE_START");
|
|
1322
|
+
if (fresh.status.syncedThroughEpoch < pendingState.committedEpoch) return fallback(prefix, "REPLICA_BEHIND_OWN_WRITES");
|
|
1323
|
+
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");
|
|
1324
|
+
if (fresh.status.coverage !== "complete") return fallback(prefix, "coverage_incomplete");
|
|
938
1325
|
try {
|
|
939
1326
|
const local = await raceSignal(handle.get(r.path), r.signal);
|
|
1327
|
+
const delivery2 = checkDelivery();
|
|
1328
|
+
if (delivery2.status !== "allow") throw deliveryError(delivery2);
|
|
1329
|
+
const refusal2 = sessionRefusal(prefix);
|
|
1330
|
+
if (refusal2) return await fallback(prefix, "grant_missing", { code: refusal2 });
|
|
940
1331
|
try {
|
|
941
1332
|
validateReadMeta(local.meta);
|
|
942
1333
|
} catch (error) {
|
|
943
1334
|
const code = errorCode(error);
|
|
944
1335
|
recordRevocation(prefix, error);
|
|
945
|
-
|
|
946
|
-
readEvent("get", r.path, prefix, "network", reasonForReadMetaCode(code), value.ok ? "found" : "error", started, { code });
|
|
947
|
-
return value;
|
|
948
|
-
}
|
|
949
|
-
if (local.status === "coverage_incomplete" || local.status === "not_covered" || local.status === "content_missing") {
|
|
950
|
-
const value = await r.network();
|
|
951
|
-
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);
|
|
952
|
-
return value;
|
|
1336
|
+
return await fallback(prefix, reasonForReadMetaCode(code), { code });
|
|
953
1337
|
}
|
|
1338
|
+
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");
|
|
954
1339
|
if (local.status === "present" && r.options?.maxResponseBytes !== void 0 && local.value.byteLength > r.options.maxResponseBytes) {
|
|
955
|
-
|
|
956
|
-
readEvent("get", r.path, prefix, "network", "unsupported_option", value.ok ? "found" : "error", started);
|
|
957
|
-
return value;
|
|
1340
|
+
return await fallback(prefix, "unsupported_option");
|
|
958
1341
|
}
|
|
959
1342
|
const result = localGet(local, r.path, r.options ?? {});
|
|
960
1343
|
const reason = local.status === "present" ? "hit" : local.status;
|
|
@@ -970,11 +1353,11 @@ function createKVReplication(deps) {
|
|
|
970
1353
|
readEvent("get", r.path, prefix, "replica", reason, result.ok ? "found" : result.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error", started, event);
|
|
971
1354
|
return result;
|
|
972
1355
|
} catch (error) {
|
|
1356
|
+
if (networkRejected) throw error;
|
|
973
1357
|
recordRevocation(prefix, error);
|
|
1358
|
+
if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
|
|
974
1359
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
975
|
-
|
|
976
|
-
readEvent("get", r.path, prefix, "network", "replica_error", value.ok ? "found" : "error", started, { code: errorCode(error) });
|
|
977
|
-
return value;
|
|
1360
|
+
return fallback(prefix, deliveryReason(errorCode(error)), { code: errorCode(error) });
|
|
978
1361
|
}
|
|
979
1362
|
}
|
|
980
1363
|
async function list(r) {
|
|
@@ -995,17 +1378,46 @@ function createKVReplication(deps) {
|
|
|
995
1378
|
readEvent("list", r.listPath, null, "none", "cursor_restart", "error", started);
|
|
996
1379
|
return cursorRestart();
|
|
997
1380
|
}
|
|
998
|
-
if (cursor && !cursor.startsWith("tcr1."))
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1381
|
+
if (cursor && !cursor.startsWith("tcr1.")) return networkRead(r.network, "list", r.listPath, null, "network_cursor", started);
|
|
1382
|
+
const prefix = listPathCovered(options.prefixes, r.listPath);
|
|
1383
|
+
let networkRejected = false;
|
|
1384
|
+
const fallback = async (replica, reason, extra = {}) => {
|
|
1385
|
+
try {
|
|
1386
|
+
return await networkRead(r.network, "list", r.listPath, replica, reason, started, extra);
|
|
1387
|
+
} catch (error) {
|
|
1388
|
+
networkRejected = true;
|
|
1389
|
+
throw error;
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
if (prefix !== void 0 && !r.options?.raw) {
|
|
1393
|
+
const delivery = checkDelivery();
|
|
1394
|
+
if (delivery.status !== "allow") {
|
|
1395
|
+
if (decoded) {
|
|
1396
|
+
readEvent("list", r.listPath, prefix, "none", "cursor_restart", "error", started);
|
|
1397
|
+
return cursorRestart();
|
|
1398
|
+
}
|
|
1399
|
+
return fallback(
|
|
1400
|
+
prefix,
|
|
1401
|
+
delivery.status === "consent_expired" ? "grant_expired" : "replica_gated",
|
|
1402
|
+
{ code: delivery.status === "consent_expired" ? delivery.code : "REPLICA_GATED" }
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
if (prefix !== void 0 && !r.options?.raw) {
|
|
1407
|
+
const refusal = sessionRefusal(prefix);
|
|
1408
|
+
if (refusal) {
|
|
1409
|
+
if (decoded) {
|
|
1410
|
+
readEvent("list", r.listPath, prefix, "none", "cursor_restart", "error", started);
|
|
1411
|
+
return cursorRestart();
|
|
1412
|
+
}
|
|
1413
|
+
return fallback(prefix, "grant_missing", { code: refusal });
|
|
1414
|
+
}
|
|
1002
1415
|
}
|
|
1003
1416
|
try {
|
|
1004
1417
|
await raceSignal(reportExistingPins(), r.signal);
|
|
1005
1418
|
} catch {
|
|
1006
1419
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
1007
1420
|
}
|
|
1008
|
-
const prefix = listPathCovered(options.prefixes, r.listPath);
|
|
1009
1421
|
const restart = () => {
|
|
1010
1422
|
readEvent("list", r.listPath, prefix ?? null, "none", "cursor_restart", "error", started);
|
|
1011
1423
|
return cursorRestart();
|
|
@@ -1013,36 +1425,33 @@ function createKVReplication(deps) {
|
|
|
1013
1425
|
if (cursor && decoded && (decoded.space !== r.space || decoded.path !== r.listPath)) return restart();
|
|
1014
1426
|
if (prefix === void 0 || r.options?.raw) {
|
|
1015
1427
|
if (cursor && decoded) return restart();
|
|
1016
|
-
|
|
1017
|
-
readEvent("list", r.listPath, prefix ?? null, "network", prefix !== void 0 ? "unsupported_option" : "not_covered", result.ok ? "found" : "error", started);
|
|
1018
|
-
return result;
|
|
1428
|
+
return networkRead(r.network, "list", r.listPath, prefix ?? null, prefix !== void 0 ? "unsupported_option" : "not_covered", started);
|
|
1019
1429
|
}
|
|
1020
1430
|
let handle;
|
|
1021
1431
|
try {
|
|
1022
1432
|
handle = await raceSignal(open(prefix), r.signal);
|
|
1023
1433
|
} catch (error) {
|
|
1024
1434
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
1435
|
+
if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
|
|
1025
1436
|
if (cursor && decoded) return restart();
|
|
1026
|
-
const
|
|
1027
|
-
|
|
1028
|
-
return result;
|
|
1437
|
+
const code = errorCode(error);
|
|
1438
|
+
return fallback(prefix, code === "RUNTIME_UNSUPPORTED" ? "runtime_unsupported" : code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_unavailable", { code });
|
|
1029
1439
|
}
|
|
1030
1440
|
if (replicas.get(prefix)?.reason === "grant_missing") {
|
|
1031
1441
|
if (cursor && decoded) return restart();
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
return result;
|
|
1442
|
+
const code = replicas.get(prefix)?.lastError;
|
|
1443
|
+
return fallback(prefix, code === "GRANT_EXPIRED" ? "grant_expired" : code === "REPLICA_GATED" ? "replica_gated" : "grant_missing", { code });
|
|
1035
1444
|
}
|
|
1036
1445
|
let fresh;
|
|
1037
1446
|
try {
|
|
1038
1447
|
fresh = await freshness(prefix, handle, r.signal);
|
|
1039
1448
|
} catch (error) {
|
|
1040
1449
|
recordRevocation(prefix, error);
|
|
1450
|
+
if (errorCode(error) === "CONSENT_EXPIRED") startConsentDenial();
|
|
1041
1451
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
1042
1452
|
if (cursor && decoded) return restart();
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
return result;
|
|
1453
|
+
const code = errorCode(error);
|
|
1454
|
+
return fallback(prefix, code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_error", { code });
|
|
1046
1455
|
}
|
|
1047
1456
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
1048
1457
|
let state;
|
|
@@ -1051,41 +1460,35 @@ function createKVReplication(deps) {
|
|
|
1051
1460
|
} catch (error) {
|
|
1052
1461
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
1053
1462
|
if (cursor && decoded) return restart();
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
return result;
|
|
1463
|
+
const code = errorCode(error);
|
|
1464
|
+
return fallback(prefix, code === "REPLICA_GATED" || code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "replica_unavailable", { code });
|
|
1057
1465
|
}
|
|
1058
|
-
const pendingRange = state.records.find((record) => listRangeCovers(r.listPath, record.key)
|
|
1466
|
+
const pendingRange = state.records.find((record) => listRangeCovers(r.listPath, record.key));
|
|
1059
1467
|
const behind = fresh.status.syncedThroughEpoch < state.committedEpoch;
|
|
1060
1468
|
if (cursor && decoded && (fresh.failure || pendingRange || behind || fresh.status.coverage !== "complete" || fresh.status.authority.state !== "valid" || !pending.durable && !inProcessProof.has(prefix))) return restart();
|
|
1061
|
-
if (behind)
|
|
1062
|
-
const result = await r.network();
|
|
1063
|
-
readEvent("list", r.listPath, prefix, "network", "REPLICA_BEHIND_OWN_WRITES", result.ok ? "found" : "error", started);
|
|
1064
|
-
return result;
|
|
1065
|
-
}
|
|
1469
|
+
if (behind) return fallback(prefix, "REPLICA_BEHIND_OWN_WRITES");
|
|
1066
1470
|
if (fresh.failure) {
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
return result;
|
|
1070
|
-
}
|
|
1071
|
-
if (pendingRange) {
|
|
1072
|
-
const result = await r.network();
|
|
1073
|
-
readEvent("list", r.listPath, prefix, "network", "pending_write", result.ok ? "found" : "error", started, { pendingState: pendingRange.state });
|
|
1074
|
-
return result;
|
|
1075
|
-
}
|
|
1076
|
-
if (!pending.durable && !inProcessProof.has(prefix)) {
|
|
1077
|
-
const result = await r.network();
|
|
1078
|
-
readEvent("list", r.listPath, prefix, "network", "REPLICA_UNPROVEN_SINCE_START", result.ok ? "found" : "error", started);
|
|
1079
|
-
return result;
|
|
1080
|
-
}
|
|
1081
|
-
if (fresh.status.coverage !== "complete" || fresh.status.authority.state !== "valid") {
|
|
1082
|
-
const result = await r.network();
|
|
1083
|
-
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);
|
|
1084
|
-
return result;
|
|
1471
|
+
const code = fresh.failure === "busy" ? "REPLICA_BUSY" : fresh.syncError;
|
|
1472
|
+
return fallback(prefix, code === "GRANT_REVOKED" ? "grant_revoked" : code === "REPLICA_GATED" || code === "CONSENT_EXPIRED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : "stale", { code });
|
|
1085
1473
|
}
|
|
1474
|
+
if (pendingRange) return fallback(prefix, "pending_write", { pendingState: pendingRange.state });
|
|
1475
|
+
if (!pending.durable && !inProcessProof.has(prefix)) return fallback(prefix, "REPLICA_UNPROVEN_SINCE_START");
|
|
1476
|
+
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");
|
|
1086
1477
|
try {
|
|
1087
1478
|
const signalHandle = { list: (o) => raceSignal(handle.list(o), r.signal) };
|
|
1088
1479
|
const page = await raceSignal(localList(signalHandle, r.space, r.listPath, r.options?.limit, cursor, validateReadMeta), r.signal);
|
|
1480
|
+
const latest = await raceSignal(pending.read(), r.signal);
|
|
1481
|
+
const laterPending = latest.records.find((record) => listRangeCovers(r.listPath, record.key));
|
|
1482
|
+
if (cursor && decoded && (laterPending || fresh.status.syncedThroughEpoch < latest.committedEpoch)) return restart();
|
|
1483
|
+
if (fresh.status.syncedThroughEpoch < latest.committedEpoch) return await fallback(prefix, "REPLICA_BEHIND_OWN_WRITES");
|
|
1484
|
+
if (laterPending) return await fallback(prefix, "pending_write", { pendingState: laterPending.state });
|
|
1485
|
+
const delivery = checkDelivery();
|
|
1486
|
+
if (delivery.status !== "allow") throw deliveryError(delivery);
|
|
1487
|
+
const refusal = sessionRefusal(prefix);
|
|
1488
|
+
if (refusal) {
|
|
1489
|
+
if (decoded) return restart();
|
|
1490
|
+
return await fallback(prefix, "grant_missing", { code: refusal });
|
|
1491
|
+
}
|
|
1089
1492
|
const event = {
|
|
1090
1493
|
count: page.keys.length,
|
|
1091
1494
|
syncedBeforeRead: fresh.syncedBeforeRead,
|
|
@@ -1096,14 +1499,14 @@ function createKVReplication(deps) {
|
|
|
1096
1499
|
readEvent("list", r.listPath, prefix, "replica", "hit", "found", started, event);
|
|
1097
1500
|
return ok(page);
|
|
1098
1501
|
} catch (error) {
|
|
1502
|
+
if (networkRejected) throw error;
|
|
1099
1503
|
if (r.signal.aborted) return abortedResult(r.signal);
|
|
1100
1504
|
const code = errorCode(error);
|
|
1505
|
+
if (code === "CONSENT_EXPIRED") startConsentDenial();
|
|
1101
1506
|
recordRevocation(prefix, error);
|
|
1102
1507
|
if (cursor && decoded) return restart();
|
|
1103
|
-
const reason = reasonForReadMetaCode(code);
|
|
1104
|
-
|
|
1105
|
-
readEvent("list", r.listPath, prefix, "network", reason, result.ok ? "found" : "error", started, { code });
|
|
1106
|
-
return result;
|
|
1508
|
+
const reason = code === "CONSENT_EXPIRED" || code === "REPLICA_GATED" || code === "GRANT_EXPIRED" ? deliveryReason(code) : reasonForReadMetaCode(code);
|
|
1509
|
+
return fallback(prefix, reason, { code });
|
|
1107
1510
|
}
|
|
1108
1511
|
}
|
|
1109
1512
|
async function write(r) {
|
|
@@ -1112,11 +1515,14 @@ function createKVReplication(deps) {
|
|
|
1112
1515
|
return prefix !== void 0 ? [{ key: entry.path, op: r.op === "delete" ? "delete" : "put" }] : [];
|
|
1113
1516
|
});
|
|
1114
1517
|
if (covered.length === 0) return r.network();
|
|
1518
|
+
if (checkDelivery().status !== "allow") return r.network();
|
|
1115
1519
|
await reportExistingPins();
|
|
1520
|
+
if (checkDelivery().status !== "allow") return r.network();
|
|
1116
1521
|
const opId = globalThis.crypto?.randomUUID?.() ?? `${scheduler.now()}-${Math.random()}`;
|
|
1117
1522
|
try {
|
|
1118
1523
|
await pending.update((s) => begin(s, opId, covered, nowDate(scheduler.now())));
|
|
1119
1524
|
} catch (error) {
|
|
1525
|
+
if (errorCode(error) === "REPLICA_GATED" || errorCode(error) === "CONSENT_EXPIRED" || checkDelivery().status !== "allow") return r.network();
|
|
1120
1526
|
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 } }));
|
|
1121
1527
|
}
|
|
1122
1528
|
const started = scheduler.now();
|
|
@@ -1181,8 +1587,32 @@ function createKVReplication(deps) {
|
|
|
1181
1587
|
for (const inspection of active) inspection.abort.abort(Object.assign(new Error("Replica status inspection cancelled"), { code: "ABORTED" }));
|
|
1182
1588
|
return Promise.all(active.map((inspection) => inspection.settled)).then(() => void 0);
|
|
1183
1589
|
}
|
|
1590
|
+
function gatedStatus(decision) {
|
|
1591
|
+
return options.prefixes.map((prefix) => ({
|
|
1592
|
+
prefix,
|
|
1593
|
+
state: "gated",
|
|
1594
|
+
reason: "replica_gated",
|
|
1595
|
+
gate: decision.status === "gated" ? decision.gate : "stamp_stale",
|
|
1596
|
+
errorCode: decision.status === "consent_expired" ? decision.code : "REPLICA_GATED",
|
|
1597
|
+
pending: { inFlight: 0, committed: 0, ambiguous: 0 },
|
|
1598
|
+
pinned: [],
|
|
1599
|
+
lagMs: null,
|
|
1600
|
+
redacted: true
|
|
1601
|
+
}));
|
|
1602
|
+
}
|
|
1184
1603
|
async function status() {
|
|
1185
|
-
const
|
|
1604
|
+
const beforeStatus = checkDelivery();
|
|
1605
|
+
if (beforeStatus.status !== "allow") return gatedStatus(beforeStatus);
|
|
1606
|
+
let persistedSnapshot;
|
|
1607
|
+
try {
|
|
1608
|
+
persistedSnapshot = pending.readExisting ? await pending.readExisting() : await pending.read();
|
|
1609
|
+
} catch (error) {
|
|
1610
|
+
const delivery = checkDelivery();
|
|
1611
|
+
if (delivery.status !== "allow") return gatedStatus(delivery);
|
|
1612
|
+
throw error;
|
|
1613
|
+
}
|
|
1614
|
+
const afterPending = checkDelivery();
|
|
1615
|
+
if (afterPending.status !== "allow") return gatedStatus(afterPending);
|
|
1186
1616
|
const snapshot = persistedSnapshot ?? emptyPendingState(identity);
|
|
1187
1617
|
const entries = await Promise.all(options.prefixes.map(async (prefix) => {
|
|
1188
1618
|
if (prefix === "/" && deps.nodeFeatures !== void 0 && !deps.nodeFeatures.includes("kv-sync-root")) {
|
|
@@ -1204,7 +1634,7 @@ function createKVReplication(deps) {
|
|
|
1204
1634
|
const full = local && !("redacted" in local) ? local : void 0;
|
|
1205
1635
|
const revoked = opened?.authorityRevoked === true || full?.authority.state === "revoked";
|
|
1206
1636
|
const rootUnsupported = opened?.lastError === "REPLICATION_ROOT_UNSUPPORTED";
|
|
1207
|
-
const state = isClosed ? "closed" : revoked ? "revoked" : rootUnsupported ? "unavailable" : opened?.reason === "grant_missing" ? "grant_missing" : full?.coverage === "complete" && full.authority.state === "valid" ? "ready" : opened?.reason || inspected.errorCode ? "unavailable" : "idle";
|
|
1637
|
+
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";
|
|
1208
1638
|
return {
|
|
1209
1639
|
prefix,
|
|
1210
1640
|
state,
|
|
@@ -1221,6 +1651,8 @@ function createKVReplication(deps) {
|
|
|
1221
1651
|
lagMs: full?.lastSyncAt ? scheduler.now() - Date.parse(full.lastSyncAt) : null
|
|
1222
1652
|
};
|
|
1223
1653
|
}));
|
|
1654
|
+
const beforeReturn = checkDelivery();
|
|
1655
|
+
if (beforeReturn.status !== "allow") return gatedStatus(beforeReturn);
|
|
1224
1656
|
return entries.filter((entry) => entry !== void 0);
|
|
1225
1657
|
}
|
|
1226
1658
|
async function sync(o) {
|
|
@@ -1236,11 +1668,15 @@ function createKVReplication(deps) {
|
|
|
1236
1668
|
var _a;
|
|
1237
1669
|
purgeStarted = true;
|
|
1238
1670
|
purgeInProgress = true;
|
|
1671
|
+
cancelConsentTimer?.();
|
|
1672
|
+
cancelConsentTimer = void 0;
|
|
1239
1673
|
const hasStatusInspections = statusInspections.size > 0;
|
|
1240
1674
|
const statusDrain = cancelStatusInspections();
|
|
1241
1675
|
for (const state of replicas.values()) {
|
|
1242
1676
|
state.timer?.();
|
|
1243
1677
|
state.timer = void 0;
|
|
1678
|
+
state.renewalTimer?.();
|
|
1679
|
+
state.renewalTimer = void 0;
|
|
1244
1680
|
state.syncContext && ((_a = state.syncContext).abortCause ?? (_a.abortCause = "purge"));
|
|
1245
1681
|
state.abort?.abort();
|
|
1246
1682
|
state.mintAbort?.abort();
|
|
@@ -1264,6 +1700,7 @@ function createKVReplication(deps) {
|
|
|
1264
1700
|
...[...replicas.values()].flatMap((state) => [
|
|
1265
1701
|
state.opening?.catch(() => void 0),
|
|
1266
1702
|
state.sync?.catch(() => void 0),
|
|
1703
|
+
state.renewing?.catch(() => void 0),
|
|
1267
1704
|
state.handle?.close().catch(() => void 0)
|
|
1268
1705
|
]),
|
|
1269
1706
|
...hasStatusInspections ? [statusDrain] : []
|
|
@@ -1301,11 +1738,15 @@ function createKVReplication(deps) {
|
|
|
1301
1738
|
var _a;
|
|
1302
1739
|
if (isClosed) return;
|
|
1303
1740
|
isClosed = true;
|
|
1741
|
+
cancelConsentTimer?.();
|
|
1742
|
+
cancelConsentTimer = void 0;
|
|
1304
1743
|
const hasStatusInspections = statusInspections.size > 0;
|
|
1305
1744
|
const statusDrain = cancelStatusInspections();
|
|
1306
1745
|
for (const [prefix, state] of replicas) {
|
|
1307
1746
|
state.timer?.();
|
|
1308
1747
|
state.timer = void 0;
|
|
1748
|
+
state.renewalTimer?.();
|
|
1749
|
+
state.renewalTimer = void 0;
|
|
1309
1750
|
state.mintAbort?.abort();
|
|
1310
1751
|
state.syncContext && ((_a = state.syncContext).abortCause ?? (_a.abortCause = "close"));
|
|
1311
1752
|
state.abort?.abort();
|
|
@@ -1314,6 +1755,7 @@ function createKVReplication(deps) {
|
|
|
1314
1755
|
const drain = Promise.all([
|
|
1315
1756
|
...[...replicas.values()].map(async (state) => {
|
|
1316
1757
|
await state.opening?.catch(() => void 0);
|
|
1758
|
+
await state.renewing?.catch(() => void 0);
|
|
1317
1759
|
await state.sync?.catch(() => void 0);
|
|
1318
1760
|
await state.handle?.close().catch(() => void 0);
|
|
1319
1761
|
}),
|
|
@@ -1343,7 +1785,8 @@ function createKVReplication(deps) {
|
|
|
1343
1785
|
cancel();
|
|
1344
1786
|
}
|
|
1345
1787
|
}
|
|
1346
|
-
|
|
1788
|
+
scheduleConsentDeadline();
|
|
1789
|
+
return { get, list, write, observeNetworkRequested, observeConsentExpired, status, sync, purge, clearPending: clearPending2, close };
|
|
1347
1790
|
}
|
|
1348
1791
|
function cancellationCode(signal) {
|
|
1349
1792
|
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;
|