@voltro/runtime 0.54.0 → 0.56.0

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/index.d.ts CHANGED
@@ -78,6 +78,7 @@ import { Sampler } from '@opentelemetry/sdk-trace-base';
78
78
  import { Schedule } from 'effect';
79
79
  import { Schema } from 'effect';
80
80
  import { ScopeError } from '@voltro/protocol';
81
+ import { Server } from 'node:http';
81
82
  import { ServerOnlyColumnWrite } from '@voltro/protocol';
82
83
  import { SpanProcessor } from '@opentelemetry/sdk-trace-base';
83
84
  import { spawn } from 'node:child_process';
@@ -94,6 +95,7 @@ import { SyncLogger } from '@voltro/logger';
94
95
  import { TableLike } from '@voltro/database';
95
96
  import { TableName } from '@voltro/protocol';
96
97
  import { TableValidationFailed } from '@voltro/protocol';
98
+ import { TargetRelation } from '@voltro/protocol';
97
99
  import { TenantRowNotFound } from '@voltro/protocol';
98
100
  import { TenantScopeViolation } from '@voltro/protocol';
99
101
  import { Tracer } from 'effect';
@@ -1124,7 +1126,14 @@ export declare const applyConnectionCredential: (clientId: number, headers: Reco
1124
1126
  * - a non-array value is a refusal naming the field — silently ignoring it
1125
1127
  * would read as "saved" while the links stayed stale;
1126
1128
  * - the row id is `output.id`, else `input.id`; with neither the declaration
1127
- * is unusable and says so.
1129
+ * is unusable and says so;
1130
+ * - the DECLARED junction columns must match the junction's real reference
1131
+ * pair. They are declaration data because the browser drives junction
1132
+ * auto-optimistic from the same block and cannot derive them; this is the
1133
+ * one place both readings meet, so a disagreement is refused here — before
1134
+ * any write — with the correct pair named. Letting it through would mean the
1135
+ * client patched one column while the server wrote another, and the UI would
1136
+ * look right until the delta contradicted it.
1128
1137
  */
1129
1138
  export declare const applyDeclaredRelations: (params: {
1130
1139
  readonly store: RelationLinksStore;
@@ -1163,6 +1172,9 @@ export declare const applyInverse: (store: UndoApplyStore, op: InverseOp) => Pro
1163
1172
  * `synthesizeInverse`), as the caller's transaction wraps them. */
1164
1173
  export declare const applyInverses: (store: UndoApplyStore, ops: ReadonlyArray<InverseOp>) => Promise<void>;
1165
1174
 
1175
+ /** Apply the timeouts to a node HTTP server. */
1176
+ export declare const applyKeepAlive: (server: Server, t: KeepAliveTimeouts) => void;
1177
+
1166
1178
  /**
1167
1179
  * Apply every rule matching `change`.
1168
1180
  *
@@ -2939,6 +2951,18 @@ export declare const DEFAULT_DELIVERY_CONCURRENCY = 8;
2939
2951
  * framework default. */
2940
2952
  export declare const DEFAULT_HSTS = "max-age=15552000; includeSubDomains";
2941
2953
 
2954
+ /**
2955
+ * 72 seconds.
2956
+ *
2957
+ * The bound that matters is the PROXY's idle timeout, and the defaults cluster
2958
+ * at 60s (ALB, Envoy) and 75s (nginx). 72s clears the 60s group with margin and
2959
+ * sits just under nginx's — a deployment on nginx should raise this, and the
2960
+ * doc says so. Picking 76s instead would leave every ALB user holding a
2961
+ * connection the load balancer has already dropped, which is the same defect
2962
+ * mirrored; the 60s group is the larger one.
2963
+ */
2964
+ export declare const DEFAULT_KEEP_ALIVE_MS = 72000;
2965
+
2942
2966
  /** Ceiling the idle backoff climbs to. Deliberately short enough to be a
2943
2967
  * FLOOR under a missed wake rather than a substitute for one. */
2944
2968
  export declare const DEFAULT_MAX_IDLE_INTERVAL_MS = 30000;
@@ -2947,6 +2971,9 @@ export declare const DEFAULT_MAX_IDLE_INTERVAL_MS = 30000;
2947
2971
  * small enough to stop a pathological body being buffered into memory. */
2948
2972
  export declare const DEFAULT_MAX_RPC_BODY_BYTES: number;
2949
2973
 
2974
+ /** See {@link DrainDeps.claimLeaseMs}. */
2975
+ export declare const DEFAULT_OUTBOX_CLAIM_LEASE_MS: number;
2976
+
2950
2977
  /**
2951
2978
  * How many raw-SQL reads one request records for the undeclared-`dependsOn`
2952
2979
  * diagnostic. A bound rather than none: the recording lives for as long as the
@@ -2983,6 +3010,15 @@ export declare const DEFAULT_SOCKET_OVERRUN_AFTER_MS = 10000;
2983
3010
 
2984
3011
  export declare const DEFAULT_SOCKET_OVERSIZED_EVENT_BYTES = 262144;
2985
3012
 
3013
+ /**
3014
+ * How old a row may be before a reader should stop trusting it as current.
3015
+ *
3016
+ * Tunable rather than a constant because the write cadence is: a fleet that
3017
+ * writes every 30s and one that writes every 5m disagree about what "stale"
3018
+ * means, and a single number would be wrong for one of them.
3019
+ */
3020
+ export declare const DEFAULT_STALE_AFTER_MS = 120000;
3021
+
2986
3022
  /**
2987
3023
  * The message of an UNDECLARED throw, bounded, with nothing else attached.
2988
3024
  *
@@ -3428,13 +3464,39 @@ export declare interface DispatcherDependencies {
3428
3464
  export declare const DORMANCY_WAKEUP_TENANT = "default";
3429
3465
 
3430
3466
  export declare interface DrainDeps {
3431
- readonly store: Pick<DataStore, 'query' | 'update' | 'insert' | 'delete'>;
3467
+ readonly store: Pick<DataStore, 'query' | 'update' | 'updateMany' | 'insert' | 'delete'>;
3432
3468
  readonly handlers: ReadonlyMap<string, OutboxHandlerDefinition>;
3433
3469
  readonly now?: () => Date;
3434
3470
  /** Max rows per drain pass. */
3435
3471
  readonly batchSize?: number;
3436
3472
  /** Per-entry cap on retained attempt rows. See {@link OUTBOX_ATTEMPT_LOG_CAP}. */
3437
3473
  readonly attemptLogCap?: number;
3474
+ /**
3475
+ * Who this process is, stamped on every row it claims.
3476
+ *
3477
+ * REQUIRED, and deliberately so. The first version made it optional "so an
3478
+ * embedder driving a single process is not forced to invent an identity",
3479
+ * which meant a caller who omitted it silently got the old behaviour: every
3480
+ * replica delivers every row. An exactly-once gate that can be switched off by
3481
+ * leaving a field out is not a gate — it is a default, and the wrong one.
3482
+ *
3483
+ * The INSTANCE rather than the replica name: a pod that restarts under a
3484
+ * stable name must not look like it still holds the claim it died inside, or
3485
+ * the lease is reading its own ghost.
3486
+ */
3487
+ readonly claimedBy: string;
3488
+ /**
3489
+ * How long a claim stands before another replica may take the row.
3490
+ *
3491
+ * It is a LEASE, not a lock: the holder can die, and nothing it left behind
3492
+ * may block delivery forever. It must therefore exceed the slowest handler
3493
+ * you are willing to wait for — a lease shorter than a real delivery hands
3494
+ * the same row to a second replica while the first is still working, which is
3495
+ * the duplicate this whole mechanism removes, reintroduced by a number.
3496
+ *
3497
+ * Default 5 minutes.
3498
+ */
3499
+ readonly claimLeaseMs?: number;
3438
3500
  }
3439
3501
 
3440
3502
  /**
@@ -3466,14 +3528,6 @@ export declare const drainForShutdown: (options: {
3466
3528
  }) => void;
3467
3529
  }) => void;
3468
3530
 
3469
- /**
3470
- * One drain pass: claim due rows, run their handler, record the outcome.
3471
- *
3472
- * A row whose handler is unknown is left PENDING rather than dead-lettered —
3473
- * the usual cause is a deploy where the enqueuing code shipped before the
3474
- * handler, and discarding those would turn a rollout ordering detail into
3475
- * permanent data loss.
3476
- */
3477
3531
  export declare const drainOutbox: (deps: DrainDeps) => Promise<DrainResult>;
3478
3532
 
3479
3533
  export declare interface DrainResult {
@@ -4536,6 +4590,38 @@ export declare interface FinopsRunnerHandle {
4536
4590
  readonly runTick: () => void;
4537
4591
  }
4538
4592
 
4593
+ /** A member of the fleet, as membership sees it. The shape this module needs
4594
+ * from `instanceMembership`, named so the merge can be tested without one. */
4595
+ export declare interface FleetMember {
4596
+ readonly instanceId: string;
4597
+ readonly startedAt: number;
4598
+ }
4599
+
4600
+ export declare interface FleetMergeResult<T> {
4601
+ /** One entry per replica that has written, newest generation only. */
4602
+ readonly byReplica: ReadonlyArray<{
4603
+ readonly replicaId: string;
4604
+ readonly version: string | undefined;
4605
+ readonly ageMs: number;
4606
+ /** Where a peer can reach it, or `undefined`. */
4607
+ readonly reachableAt: string | undefined;
4608
+ readonly payload: T;
4609
+ }>;
4610
+ /** Replicas membership knows about that have written nothing readable. */
4611
+ readonly missing: ReadonlyArray<string>;
4612
+ readonly responded: number;
4613
+ readonly expected: number;
4614
+ /** Rows old enough that a reader should not treat them as current. */
4615
+ readonly stale: ReadonlyArray<{
4616
+ readonly replicaId: string;
4617
+ readonly ageMs: number;
4618
+ }>;
4619
+ /** Framework versions among the responders, counted. Present only when they
4620
+ * DIFFER — a single-version fleet needs no such note, and a note nobody
4621
+ * needs is one everybody learns to skip. */
4622
+ readonly versions: Readonly<Record<string, number>> | undefined;
4623
+ }
4624
+
4539
4625
  export declare interface FluentStore extends Omit<MutationStore, 'update' | 'delete' | 'query'> {
4540
4626
  /**
4541
4627
  * Diff-based many-to-many link writer for a junction table. `anchor` names the
@@ -4657,6 +4743,17 @@ export declare const forbiddenColumnsForSubject: (tableName: string, subject: Su
4657
4743
  /** Forget the calling subject's credential. Returns whether a row was removed. */
4658
4744
  export declare const forgetCredential: (store: VaultStore, connectionId: string, subjectId: string) => Promise<boolean>;
4659
4745
 
4746
+ /**
4747
+ * Drop rows for a replica GENERATION that is gone.
4748
+ *
4749
+ * Not a retention sweep — the table has no history to age out. This removes
4750
+ * the row of a process that restarted, because the replica's NEW row is
4751
+ * written under the same `(replicaId, kind)` key and the old generation is
4752
+ * simply overwritten. What this handles is a replica that went away for good
4753
+ * (a scale-down), whose row would otherwise be counted forever.
4754
+ */
4755
+ export declare const forgetReplicaObservations: (store: DataStore, replicaId: string) => Promise<void>;
4756
+
4660
4757
  /** A boot-ready message for a set of cache-scope leaks (empty → `undefined`). */
4661
4758
  export declare const formatCacheScopeLeaks: (leaks: ReadonlyArray<CacheScopeLeak>) => string | undefined;
4662
4759
 
@@ -4768,6 +4865,9 @@ export declare type HandlerBody = void | Promise<unknown> | Effect.Effect<unknow
4768
4865
  */
4769
4866
  export declare const hashUnitInterval: (s: string) => number;
4770
4867
 
4868
+ /** How much longer than `keepAliveTimeout` the headers deadline runs. */
4869
+ export declare const HEADERS_TIMEOUT_MARGIN_MS = 5000;
4870
+
4771
4871
  /** Define a histogram. `boundaries` default to the framework duration buckets. */
4772
4872
  export declare const histogramMetric: (name: string, boundaries?: MetricBoundaries.MetricBoundaries, description?: string) => Metric.Metric.Histogram<number>;
4773
4873
 
@@ -5244,6 +5344,17 @@ export declare const ivmValue: (shape: AggregateShape, g: GroupState) => number
5244
5344
  * columns is rejected with a message naming what it found.
5245
5345
  */
5246
5346
  export declare interface JunctionLinks {
5347
+ /**
5348
+ * The junction's two reference columns as RESOLVED for this anchor — the
5349
+ * source of truth a declared `relations:` is verified against. It is exposed
5350
+ * because the pair also has to be stated in the descriptor (the browser
5351
+ * cannot derive it), and a stated fact that nothing compares against is a
5352
+ * divergence waiting to happen.
5353
+ */
5354
+ readonly columns: {
5355
+ readonly anchorColumn: string;
5356
+ readonly targetColumn: string;
5357
+ };
5247
5358
  /** The current target ids linked to the anchor. */
5248
5359
  list(): Promise<ReadonlyArray<string>>;
5249
5360
  /** Reconcile the links to EXACTLY `targetIds` — insert the missing, delete the
@@ -5275,6 +5386,23 @@ export declare interface JunctionLinks {
5275
5386
  }>;
5276
5387
  }
5277
5388
 
5389
+ /** The `http:` block's keep-alive knobs. */
5390
+ export declare interface KeepAliveConfig {
5391
+ readonly http?: {
5392
+ /** Idle keep-alive lifetime, ms. Default 72000. Raise it above YOUR
5393
+ * proxy's idle timeout — see `DEFAULT_KEEP_ALIVE_MS` for why 72s. */
5394
+ readonly keepAliveTimeoutMs?: number;
5395
+ /** Header-completion deadline, ms. Must exceed `keepAliveTimeoutMs`.
5396
+ * Default `keepAliveTimeoutMs + 5000`. */
5397
+ readonly headersTimeoutMs?: number;
5398
+ };
5399
+ }
5400
+
5401
+ export declare interface KeepAliveTimeouts {
5402
+ readonly keepAliveTimeoutMs: number;
5403
+ readonly headersTimeoutMs: number;
5404
+ }
5405
+
5278
5406
  export declare interface KvFacade {
5279
5407
  readonly kv: AsyncKv;
5280
5408
  /** The resolved Effect-native `Kv` service instance. Its methods close over
@@ -6010,6 +6138,25 @@ export declare const memoryRywPositionStore: (options?: MemoryRywOptions) => Ryw
6010
6138
  */
6011
6139
  export declare const mergeCookieHeader: (existing: string | undefined, patch: Readonly<Record<string, string>>) => string;
6012
6140
 
6141
+ /**
6142
+ * Merge rows into a fleet view, and say what is missing from it.
6143
+ *
6144
+ * The three things that make this honest, each of which a naive `rows.map()`
6145
+ * gets wrong:
6146
+ *
6147
+ * 1. **A replica membership knows about but that has no row is MISSING**, not
6148
+ * absent. Dropping it silently is the unmarked sample this whole feature
6149
+ * exists to end — one level up, and more expensive, because the reader now
6150
+ * believes they asked everybody.
6151
+ * 2. **A stale row is reported WITH its age** rather than filtered out.
6152
+ * Removing it would make a partial answer look complete; keeping it
6153
+ * unmarked would make fiction look current. Neither is available.
6154
+ * 3. **Mixed versions are named.** During a rolling deploy the fleet genuinely
6155
+ * spans two shapes, and an aggregate that averages them silently is wrong
6156
+ * in a way nothing downstream can detect.
6157
+ */
6158
+ export declare const mergeFleetObservations: <T>(rows: ReadonlyArray<ReplicaObservationRow>, members: ReadonlyArray<FleetMember>, staleAfterMs?: number) => FleetMergeResult<T>;
6159
+
6013
6160
  export { MetricBoundaries }
6014
6161
 
6015
6162
  export declare interface MetricBucketPoint {
@@ -6087,10 +6234,10 @@ export declare interface MutationLike {
6087
6234
  readonly requiresApproval?: AnyApprovalPolicy | undefined;
6088
6235
  readonly target?: {
6089
6236
  readonly table: string;
6090
- readonly relations?: Readonly<Record<string, string>> | undefined;
6237
+ readonly relations?: Readonly<Record<string, TargetRelation>> | undefined;
6091
6238
  } | ReadonlyArray<{
6092
6239
  readonly table: string;
6093
- readonly relations?: Readonly<Record<string, string>> | undefined;
6240
+ readonly relations?: Readonly<Record<string, TargetRelation>> | undefined;
6094
6241
  }> | undefined;
6095
6242
  };
6096
6243
  executor(input: unknown, ctx: unknown): unknown;
@@ -6299,6 +6446,16 @@ declare const NoRowFound_base: Schema.TaggedErrorClass<NoRowFound, "NoRowFound",
6299
6446
  /** Note one read against the in-flight procedure. No scope → nothing happens. */
6300
6447
  export declare const noteRead: (descriptor: QueryDescriptor) => void;
6301
6448
 
6449
+ /**
6450
+ * Record the resume verdict for one subscription setup. `true` means a ring
6451
+ * was recorded; anything else names why it was not.
6452
+ *
6453
+ * Logs at debug ONCE per (label, verdict) — a hot query must not narrate every
6454
+ * subscribe, and the second identical verdict carries no information the first
6455
+ * did not.
6456
+ */
6457
+ export declare const noteResumeVerdict: (rawLabel: string, verdict: true | ResumeExclusion) => void;
6458
+
6302
6459
  export declare interface Notification {
6303
6460
  readonly subscriberId: string;
6304
6461
  readonly fingerprint: string;
@@ -7015,10 +7172,21 @@ export declare class ReactionRateLimiter {
7015
7172
  }
7016
7173
 
7017
7174
  export declare interface ReactionRunDeps {
7018
- /** Idempotency store — `has(key)` true ⇒ already acted. */
7175
+ /**
7176
+ * Atomically claim ONE logical change. `true` = ours, act. `false` = someone
7177
+ * already has it (or the claim could not be taken — fail-closed, like the
7178
+ * rate slot and the budget beside it).
7179
+ *
7180
+ * ONE operation, not `has` then `add`, and the difference is the whole
7181
+ * guarantee. A read-then-write cannot be won by exactly one caller: two
7182
+ * replicas both read "not seen", both act, and `dedupeKey` — the field
7183
+ * `defineReaction` REQUIRES precisely so an act cannot repeat — delivers N
7184
+ * acts on N replicas. It was worse than a missing gate, because it read as a
7185
+ * present one: the in-memory set behind it was per-process, so every test and
7186
+ * every single-instance run confirmed it worked.
7187
+ */
7019
7188
  readonly dedupe: {
7020
- readonly has: (key: string) => boolean | Promise<boolean>;
7021
- readonly add: (key: string) => void | Promise<void>;
7189
+ readonly claim: (key: string) => boolean | Promise<boolean>;
7022
7190
  };
7023
7191
  /** Resolve + run the reaction's act AS the agent actor. Injected by the serve
7024
7192
  * layer (it owns agent/workflow execution). */
@@ -7137,6 +7305,16 @@ export declare const readPollCeilingMs: (env?: {
7137
7305
  /** Is anything listening? Call sites skip their work when nothing is. */
7138
7306
  export declare const readRecordingActive: () => boolean;
7139
7307
 
7308
+ /**
7309
+ * Every replica's rows for one kind.
7310
+ *
7311
+ * `now` is injected so a caller can compute ages against one instant rather
7312
+ * than a moving clock — two rows compared against two `Date.now()` calls are
7313
+ * two measurements, and the difference between them is noise a reader would
7314
+ * read as signal.
7315
+ */
7316
+ export declare const readReplicaObservations: (store: DataStore, kind: ReplicaObservationKind, now?: number) => Promise<ReadonlyArray<ReplicaObservationRow>>;
7317
+
7140
7318
  /** `admin:full` (mirrors `@voltro/protocol`'s ADMIN_SCOPE) bypasses ReBAC. */
7141
7319
  export declare const REBAC_ADMIN_SCOPE = "admin:full";
7142
7320
 
@@ -7531,9 +7709,15 @@ export declare interface RegistryTableLike {
7531
7709
  * request was. */
7532
7710
  export declare const rehydrateGuards: (serialized: ReadonlyArray<SerializedGuard>) => ReadonlyArray<AnyGuardSpec>;
7533
7711
 
7534
- /** The store slice `applyDeclaredRelations` needs. */
7712
+ /** The store slice `applyDeclaredRelations` needs. `columns` is the junction's
7713
+ * DERIVED reference pair — the truth the descriptor's declaration is checked
7714
+ * against before anything is written. */
7535
7715
  declare interface RelationLinksStore {
7536
7716
  relationLinks(junctionTable: string, anchorTable: string, anchorId: string): {
7717
+ readonly columns: {
7718
+ readonly anchorColumn: string;
7719
+ readonly targetColumn: string;
7720
+ };
7537
7721
  set(targetIds: ReadonlyArray<string>): Promise<unknown>;
7538
7722
  };
7539
7723
  }
@@ -7556,6 +7740,8 @@ export declare interface RelationTuple {
7556
7740
  */
7557
7741
  export declare const releaseWakeup: (store: DataStore, ref: WakeupRef) => Promise<void>;
7558
7742
 
7743
+ export declare const REPLICA_OBSERVATIONS_TABLE = "_voltro_replica_observations";
7744
+
7559
7745
  export declare interface ReplicaHealth {
7560
7746
  /** Whether this replica is currently eligible for reads. */
7561
7747
  isHealthy(replica: DataStore): boolean;
@@ -7585,6 +7771,27 @@ export declare interface ReplicaHealthOptions {
7585
7771
  readonly now?: () => number;
7586
7772
  }
7587
7773
 
7774
+ /** Which observation a row carries. A closed set — a new kind is a deliberate
7775
+ * addition with a reader, not a free-form label. */
7776
+ export declare type ReplicaObservationKind = 'resume' | 'subscriptions';
7777
+
7778
+ /** One replica's row, as a reader gets it back. */
7779
+ export declare interface ReplicaObservationRow {
7780
+ readonly replicaId: string;
7781
+ readonly startedAt: number;
7782
+ readonly version: string | undefined;
7783
+ readonly kind: ReplicaObservationKind;
7784
+ readonly payload: unknown;
7785
+ /** Where a peer can reach it, or `undefined` — never a guess. */
7786
+ readonly reachableAt: string | undefined;
7787
+ readonly writtenAt: number;
7788
+ /** How old this row is, on the READER's clock. Approximate by construction —
7789
+ * it compares two machines' clocks, which is exactly what
7790
+ * `instanceMembership` refuses to do for liveness. It is good enough to
7791
+ * say "this is minutes old", and must never be used to order two rows. */
7792
+ readonly ageMs: number;
7793
+ }
7794
+
7588
7795
  /** Captured per-read for observability. Pushed into trace buffers
7589
7796
  * by the runtime so `voltro traces` shows the routing decision +
7590
7797
  * any RYW wait latency. */
@@ -7732,6 +7939,9 @@ export declare const resetEventMetricTagCacheForTests: () => void;
7732
7939
  /** Drop everything recorded. For tests; not called by the runtime. */
7733
7940
  export declare const resetObservedGraph: () => void;
7734
7941
 
7942
+ /** Clear the census. For tests; a running app has no reason to forget. */
7943
+ export declare const resetResumeCensus: () => void;
7944
+
7735
7945
  /** Test seam. */
7736
7946
  export declare const resetResumeRingsForTest: () => void;
7737
7947
 
@@ -7881,6 +8091,8 @@ export declare interface ResolvedWorkflowCallerContext extends WorkflowCallerCon
7881
8091
  readonly traceId: string;
7882
8092
  }
7883
8093
 
8094
+ export declare const resolveKeepAlive: (config: KeepAliveConfig | null | undefined) => KeepAliveTimeouts;
8095
+
7884
8096
  /**
7885
8097
  * Resolve the guard's configuration.
7886
8098
  *
@@ -8071,6 +8283,31 @@ export declare type ResumeAnswer =
8071
8283
  readonly kind: 'snapshot';
8072
8284
  };
8073
8285
 
8286
+ /** The census as of now, one entry per label that has been subscribed. */
8287
+ export declare const resumeCensus: () => ReadonlyArray<ResumeVerdict>;
8288
+
8289
+ /**
8290
+ * Why a subscription carries no resume ring.
8291
+ *
8292
+ * - `computed` — the executor returned a value, not a descriptor. There is no
8293
+ * delta chain to replay: the handler re-runs opaquely and emits snapshots.
8294
+ * Declaring `tables:` on the row filter cannot change this; returning the
8295
+ * query builder instead of a mapped value can.
8296
+ * - `row-filter` — the registered filter may narrow this subscription's source
8297
+ * table, so its row set is re-resolved per delivery. Declaring `tables:`
8298
+ * fixes it for every source the filter does NOT touch; for a source it does
8299
+ * touch, the exclusion is the point.
8300
+ * - `eager-load` — the descriptor carries `.with(...)` while a filter is
8301
+ * registered, and the relation is resolved below the seam that would narrow
8302
+ * it. Reported only when the base table is NOT itself narrowed, so this
8303
+ * reason always means: drop the eager load and the subscription resumes.
8304
+ * - `uncanonical-input` — the input cannot be reduced to a stable ring key, so
8305
+ * two different inputs would collide under one label.
8306
+ * - `not-offered` — the binder was driven without resume input at all (the
8307
+ * lower-level entrypoint used by tests).
8308
+ */
8309
+ export declare type ResumeExclusion = 'computed' | 'row-filter' | 'eager-load' | 'uncanonical-input' | 'not-offered';
8310
+
8074
8311
  /**
8075
8312
  * The identity a ring is recorded under — and the identity a resume must
8076
8313
  * present to read it back. `subjectId`/`tenantId` are part of the KEY, so a
@@ -8099,6 +8336,14 @@ export declare interface ResumeTunables {
8099
8336
 
8100
8337
  export declare const resumeTunables: () => ResumeTunables;
8101
8338
 
8339
+ /** One label's tally. `resumable + Σ excluded` is the number of subscriptions
8340
+ * set up on this label since the process started. */
8341
+ export declare interface ResumeVerdict {
8342
+ readonly label: string;
8343
+ readonly resumable: number;
8344
+ readonly excluded: Readonly<Partial<Record<ResumeExclusion, number>>>;
8345
+ }
8346
+
8102
8347
  export { RetentionConflict }
8103
8348
 
8104
8349
  export { RetentionSource }
@@ -8279,9 +8524,19 @@ export declare interface RowFilter<Ctx = unknown> {
8279
8524
  * must not replay deltas on reconnect — the answer can change while the
8280
8525
  * socket is down (a membership ends) and a wrong replay leaks rows. But the
8281
8526
  * question the framework could ask was only "is a filter registered at
8282
- * all?", so ONE registration disabled delta-resume for the whole process. A
8283
- * deployment measured a filter narrowing 4 tables costing the feature on all
8284
- * 173 of their query descriptors, 55 of whose source tables it never touches.
8527
+ * all?", so ONE registration disabled delta-resume for the whole process,
8528
+ * including every subscription reading a table the predicate never returns
8529
+ * anything for.
8530
+ *
8531
+ * WHAT IT DOES NOT BUY, and this bounds the whole feature: a query whose
8532
+ * executor returns a VALUE rather than a descriptor — anything that maps the
8533
+ * rows or wraps them in a page envelope — has no delta chain to resume in
8534
+ * the first place. It re-runs an opaque handler and emits snapshots, filter
8535
+ * or no filter. So the number of subscriptions a declaration can give back
8536
+ * is the number that bind a descriptor, not the number of queries in the
8537
+ * app; declare it anyway (it costs nothing and applies the moment a query
8538
+ * returns a builder), but read `resumeCensus()` — or
8539
+ * `/_voltro/inspect/subscriptions` — before expecting a change.
8285
8540
  *
8286
8541
  * WHY A DECLARATION RATHER THAN A PROBE. Resolving the scope at subscribe
8287
8542
  * and treating `predicate(ctx, source) === undefined` as safe is cheaper and
@@ -8297,9 +8552,14 @@ export declare interface RowFilter<Ctx = unknown> {
8297
8552
  * filter (this field absent) keeps the conservative behaviour: no resume
8298
8553
  * anywhere, no verification.
8299
8554
  *
8555
+ * const OWNED = new Set(['documents', 'comments'])
8300
8556
  * setRowFilter({
8301
- * load, predicate,
8302
- * tables: ['bookmarks', 'recentSearches', 'todoSchedules', 'todoTags'],
8557
+ * load,
8558
+ * predicate: (ctx, table) => (OWNED.has(table) ? eq('ownerId', ctx.userId) : undefined),
8559
+ * // Derived from the same set the predicate reads. Two hand-kept
8560
+ * // lists is the shape in which a table lands in exactly one of
8561
+ * // them — and the penalty here is a failing read, not a warning.
8562
+ * tables: [...OWNED],
8303
8563
  * })
8304
8564
  */
8305
8565
  readonly tables?: ReadonlyArray<string>;
@@ -9814,6 +10074,61 @@ export declare interface SubscriberDefinition {
9814
10074
  readonly table: string;
9815
10075
  /** Default `'any'`. */
9816
10076
  readonly on?: SubscribeOp | ReadonlyArray<Exclude<SubscribeOp, 'any'>>;
10077
+ /**
10078
+ * Run the handler ONCE PER CHANGE across the whole fleet, instead of once on
10079
+ * every replica that sees it.
10080
+ *
10081
+ * **Without it, the handler runs on EVERY api instance.** That is the correct
10082
+ * default and not a gap: a subscriber that refreshes a search index, drops a
10083
+ * process-local cache entry or updates in-memory state has to run everywhere,
10084
+ * and a fleet-wide gate would leave every other replica stale. The default
10085
+ * assumes the handler is IDEMPOTENT.
10086
+ *
10087
+ * It is the wrong default for an EFFECT — a notification, a mail, a webhook, a
10088
+ * payment — because there is nothing to make idempotent: the effect IS the
10089
+ * write, so each run produces another one. Two replicas send two mails.
10090
+ *
10091
+ * `once` returns a deterministic key for the change, and exactly one replica
10092
+ * wins the claim on it (`_voltro_change_claims`, INSERT-wins on a UNIQUE — the
10093
+ * same arbiter the cron scheduler uses for a firing). The key is namespaced by
10094
+ * this subscriber's file id, so two subscribers on one table never lock each
10095
+ * other out.
10096
+ *
10097
+ * ```ts
10098
+ * export default defineSubscriber({
10099
+ * table: 'absence_requests',
10100
+ * on: ['insert'],
10101
+ * once: true,
10102
+ * handler: notifyApprovers,
10103
+ * })
10104
+ * ```
10105
+ *
10106
+ * **`once: true` derives the key for you**, and is the right default choice.
10107
+ * It names the change by its CONTENT plus its position among content-identical
10108
+ * repeats (`changeDigest` + `OccurrenceCounter`, `@voltro/database`) — which is
10109
+ * the only thing two replicas provably agree on, since a fleet change carries
10110
+ * no LSN, no commit id and no `traceId`. `A→B`, then `B→A`, then `A→B` again is
10111
+ * three changes, not two.
10112
+ *
10113
+ * **A function is for when you want to be COARSER than one-per-change.** Two
10114
+ * updates that differ only in a field you do not care about are two changes to
10115
+ * `once: true` and can be one to a key function that ignores that field.
10116
+ *
10117
+ * **If you write one, it must tell two genuine changes apart.** A row id is
10118
+ * enough for `insert` and `delete`, where a row changes state once. It is NOT
10119
+ * enough for `update`: two edits to the same row produce the same id, and the
10120
+ * second would be dropped as a duplicate of the first. Put something that moves
10121
+ * in the key — `` `${row.id}:${row.updatedAt}` `` — or use `once: true`.
10122
+ *
10123
+ * **AT MOST once, not exactly once.** The claim is taken BEFORE the handler
10124
+ * runs, so a replica that wins and then dies takes the event with it, and a
10125
+ * claim that cannot be written (database unreachable) is not taken by anyone.
10126
+ * Both are loud in the log and neither is retried — a subscriber is
10127
+ * best-effort by construction. When the effect must not be lost, the change
10128
+ * stream is the wrong seam for it: run it in the mutation, or start a workflow
10129
+ * from a `*.reaction.tsx`, where the durability is the primitive's job.
10130
+ */
10131
+ readonly once?: true | ((event: SubscribeEvent) => string);
9817
10132
  readonly handler: SubscribeHandler;
9818
10133
  }
9819
10134
 
@@ -10650,6 +10965,8 @@ export declare const VOLTRO_TENANT_MIXIN_ID: "voltro/tenant";
10650
10965
  */
10651
10966
  export declare const _voltroApprovalsTable: TableLike;
10652
10967
 
10968
+ export declare const _voltroChangeClaimsTable: TableLike;
10969
+
10653
10970
  /**
10654
10971
  * `_voltro_connection_grants` — an IN-FLIGHT oauth2 handshake. Separate from
10655
10972
  * the credential table on purpose: a handshake is a short-lived, single-use
@@ -11403,4 +11720,13 @@ export declare const wrapStoreWithBootCodec: (underlying: DataStore, dialectId:
11403
11720
 
11404
11721
  export declare const wrapStoreWithMixinBehaviour: (rawUnderlying: DataStore, ctx: StoreMiddlewareContext) => FluentStore;
11405
11722
 
11723
+ /**
11724
+ * Write (upsert) this replica's counters for one kind.
11725
+ *
11726
+ * Best-effort throughout: a diagnostic that can fail a request, or a boot, is a
11727
+ * new way to be down. A failure logs at debug and leaves the previous row —
11728
+ * which then reads as stale, which is the truth.
11729
+ */
11730
+ export declare const writeReplicaObservation: (store: DataStore, kind: ReplicaObservationKind, payload: unknown) => Promise<void>;
11731
+
11406
11732
  export { }