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