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