@voltro/runtime 0.55.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/CHANGELOG.md +497 -0
- package/dist/index.d.ts +252 -12
- package/dist/index.js +2081 -1908
- package/package.json +7 -7
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';
|
|
@@ -1171,6 +1172,9 @@ export declare const applyInverse: (store: UndoApplyStore, op: InverseOp) => Pro
|
|
|
1171
1172
|
* `synthesizeInverse`), as the caller's transaction wraps them. */
|
|
1172
1173
|
export declare const applyInverses: (store: UndoApplyStore, ops: ReadonlyArray<InverseOp>) => Promise<void>;
|
|
1173
1174
|
|
|
1175
|
+
/** Apply the timeouts to a node HTTP server. */
|
|
1176
|
+
export declare const applyKeepAlive: (server: Server, t: KeepAliveTimeouts) => void;
|
|
1177
|
+
|
|
1174
1178
|
/**
|
|
1175
1179
|
* Apply every rule matching `change`.
|
|
1176
1180
|
*
|
|
@@ -2947,6 +2951,18 @@ export declare const DEFAULT_DELIVERY_CONCURRENCY = 8;
|
|
|
2947
2951
|
* framework default. */
|
|
2948
2952
|
export declare const DEFAULT_HSTS = "max-age=15552000; includeSubDomains";
|
|
2949
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
|
+
|
|
2950
2966
|
/** Ceiling the idle backoff climbs to. Deliberately short enough to be a
|
|
2951
2967
|
* FLOOR under a missed wake rather than a substitute for one. */
|
|
2952
2968
|
export declare const DEFAULT_MAX_IDLE_INTERVAL_MS = 30000;
|
|
@@ -2955,6 +2971,9 @@ export declare const DEFAULT_MAX_IDLE_INTERVAL_MS = 30000;
|
|
|
2955
2971
|
* small enough to stop a pathological body being buffered into memory. */
|
|
2956
2972
|
export declare const DEFAULT_MAX_RPC_BODY_BYTES: number;
|
|
2957
2973
|
|
|
2974
|
+
/** See {@link DrainDeps.claimLeaseMs}. */
|
|
2975
|
+
export declare const DEFAULT_OUTBOX_CLAIM_LEASE_MS: number;
|
|
2976
|
+
|
|
2958
2977
|
/**
|
|
2959
2978
|
* How many raw-SQL reads one request records for the undeclared-`dependsOn`
|
|
2960
2979
|
* diagnostic. A bound rather than none: the recording lives for as long as the
|
|
@@ -2991,6 +3010,15 @@ export declare const DEFAULT_SOCKET_OVERRUN_AFTER_MS = 10000;
|
|
|
2991
3010
|
|
|
2992
3011
|
export declare const DEFAULT_SOCKET_OVERSIZED_EVENT_BYTES = 262144;
|
|
2993
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
|
+
|
|
2994
3022
|
/**
|
|
2995
3023
|
* The message of an UNDECLARED throw, bounded, with nothing else attached.
|
|
2996
3024
|
*
|
|
@@ -3436,13 +3464,39 @@ export declare interface DispatcherDependencies {
|
|
|
3436
3464
|
export declare const DORMANCY_WAKEUP_TENANT = "default";
|
|
3437
3465
|
|
|
3438
3466
|
export declare interface DrainDeps {
|
|
3439
|
-
readonly store: Pick<DataStore, 'query' | 'update' | 'insert' | 'delete'>;
|
|
3467
|
+
readonly store: Pick<DataStore, 'query' | 'update' | 'updateMany' | 'insert' | 'delete'>;
|
|
3440
3468
|
readonly handlers: ReadonlyMap<string, OutboxHandlerDefinition>;
|
|
3441
3469
|
readonly now?: () => Date;
|
|
3442
3470
|
/** Max rows per drain pass. */
|
|
3443
3471
|
readonly batchSize?: number;
|
|
3444
3472
|
/** Per-entry cap on retained attempt rows. See {@link OUTBOX_ATTEMPT_LOG_CAP}. */
|
|
3445
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;
|
|
3446
3500
|
}
|
|
3447
3501
|
|
|
3448
3502
|
/**
|
|
@@ -3474,14 +3528,6 @@ export declare const drainForShutdown: (options: {
|
|
|
3474
3528
|
}) => void;
|
|
3475
3529
|
}) => void;
|
|
3476
3530
|
|
|
3477
|
-
/**
|
|
3478
|
-
* One drain pass: claim due rows, run their handler, record the outcome.
|
|
3479
|
-
*
|
|
3480
|
-
* A row whose handler is unknown is left PENDING rather than dead-lettered —
|
|
3481
|
-
* the usual cause is a deploy where the enqueuing code shipped before the
|
|
3482
|
-
* handler, and discarding those would turn a rollout ordering detail into
|
|
3483
|
-
* permanent data loss.
|
|
3484
|
-
*/
|
|
3485
3531
|
export declare const drainOutbox: (deps: DrainDeps) => Promise<DrainResult>;
|
|
3486
3532
|
|
|
3487
3533
|
export declare interface DrainResult {
|
|
@@ -4544,6 +4590,38 @@ export declare interface FinopsRunnerHandle {
|
|
|
4544
4590
|
readonly runTick: () => void;
|
|
4545
4591
|
}
|
|
4546
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
|
+
|
|
4547
4625
|
export declare interface FluentStore extends Omit<MutationStore, 'update' | 'delete' | 'query'> {
|
|
4548
4626
|
/**
|
|
4549
4627
|
* Diff-based many-to-many link writer for a junction table. `anchor` names the
|
|
@@ -4665,6 +4743,17 @@ export declare const forbiddenColumnsForSubject: (tableName: string, subject: Su
|
|
|
4665
4743
|
/** Forget the calling subject's credential. Returns whether a row was removed. */
|
|
4666
4744
|
export declare const forgetCredential: (store: VaultStore, connectionId: string, subjectId: string) => Promise<boolean>;
|
|
4667
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
|
+
|
|
4668
4757
|
/** A boot-ready message for a set of cache-scope leaks (empty → `undefined`). */
|
|
4669
4758
|
export declare const formatCacheScopeLeaks: (leaks: ReadonlyArray<CacheScopeLeak>) => string | undefined;
|
|
4670
4759
|
|
|
@@ -4776,6 +4865,9 @@ export declare type HandlerBody = void | Promise<unknown> | Effect.Effect<unknow
|
|
|
4776
4865
|
*/
|
|
4777
4866
|
export declare const hashUnitInterval: (s: string) => number;
|
|
4778
4867
|
|
|
4868
|
+
/** How much longer than `keepAliveTimeout` the headers deadline runs. */
|
|
4869
|
+
export declare const HEADERS_TIMEOUT_MARGIN_MS = 5000;
|
|
4870
|
+
|
|
4779
4871
|
/** Define a histogram. `boundaries` default to the framework duration buckets. */
|
|
4780
4872
|
export declare const histogramMetric: (name: string, boundaries?: MetricBoundaries.MetricBoundaries, description?: string) => Metric.Metric.Histogram<number>;
|
|
4781
4873
|
|
|
@@ -5294,6 +5386,23 @@ export declare interface JunctionLinks {
|
|
|
5294
5386
|
}>;
|
|
5295
5387
|
}
|
|
5296
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
|
+
|
|
5297
5406
|
export declare interface KvFacade {
|
|
5298
5407
|
readonly kv: AsyncKv;
|
|
5299
5408
|
/** The resolved Effect-native `Kv` service instance. Its methods close over
|
|
@@ -6029,6 +6138,25 @@ export declare const memoryRywPositionStore: (options?: MemoryRywOptions) => Ryw
|
|
|
6029
6138
|
*/
|
|
6030
6139
|
export declare const mergeCookieHeader: (existing: string | undefined, patch: Readonly<Record<string, string>>) => string;
|
|
6031
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
|
+
|
|
6032
6160
|
export { MetricBoundaries }
|
|
6033
6161
|
|
|
6034
6162
|
export declare interface MetricBucketPoint {
|
|
@@ -7044,10 +7172,21 @@ export declare class ReactionRateLimiter {
|
|
|
7044
7172
|
}
|
|
7045
7173
|
|
|
7046
7174
|
export declare interface ReactionRunDeps {
|
|
7047
|
-
/**
|
|
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
|
+
*/
|
|
7048
7188
|
readonly dedupe: {
|
|
7049
|
-
readonly
|
|
7050
|
-
readonly add: (key: string) => void | Promise<void>;
|
|
7189
|
+
readonly claim: (key: string) => boolean | Promise<boolean>;
|
|
7051
7190
|
};
|
|
7052
7191
|
/** Resolve + run the reaction's act AS the agent actor. Injected by the serve
|
|
7053
7192
|
* layer (it owns agent/workflow execution). */
|
|
@@ -7166,6 +7305,16 @@ export declare const readPollCeilingMs: (env?: {
|
|
|
7166
7305
|
/** Is anything listening? Call sites skip their work when nothing is. */
|
|
7167
7306
|
export declare const readRecordingActive: () => boolean;
|
|
7168
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
|
+
|
|
7169
7318
|
/** `admin:full` (mirrors `@voltro/protocol`'s ADMIN_SCOPE) bypasses ReBAC. */
|
|
7170
7319
|
export declare const REBAC_ADMIN_SCOPE = "admin:full";
|
|
7171
7320
|
|
|
@@ -7591,6 +7740,8 @@ export declare interface RelationTuple {
|
|
|
7591
7740
|
*/
|
|
7592
7741
|
export declare const releaseWakeup: (store: DataStore, ref: WakeupRef) => Promise<void>;
|
|
7593
7742
|
|
|
7743
|
+
export declare const REPLICA_OBSERVATIONS_TABLE = "_voltro_replica_observations";
|
|
7744
|
+
|
|
7594
7745
|
export declare interface ReplicaHealth {
|
|
7595
7746
|
/** Whether this replica is currently eligible for reads. */
|
|
7596
7747
|
isHealthy(replica: DataStore): boolean;
|
|
@@ -7620,6 +7771,27 @@ export declare interface ReplicaHealthOptions {
|
|
|
7620
7771
|
readonly now?: () => number;
|
|
7621
7772
|
}
|
|
7622
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
|
+
|
|
7623
7795
|
/** Captured per-read for observability. Pushed into trace buffers
|
|
7624
7796
|
* by the runtime so `voltro traces` shows the routing decision +
|
|
7625
7797
|
* any RYW wait latency. */
|
|
@@ -7919,6 +8091,8 @@ export declare interface ResolvedWorkflowCallerContext extends WorkflowCallerCon
|
|
|
7919
8091
|
readonly traceId: string;
|
|
7920
8092
|
}
|
|
7921
8093
|
|
|
8094
|
+
export declare const resolveKeepAlive: (config: KeepAliveConfig | null | undefined) => KeepAliveTimeouts;
|
|
8095
|
+
|
|
7922
8096
|
/**
|
|
7923
8097
|
* Resolve the guard's configuration.
|
|
7924
8098
|
*
|
|
@@ -9900,6 +10074,61 @@ export declare interface SubscriberDefinition {
|
|
|
9900
10074
|
readonly table: string;
|
|
9901
10075
|
/** Default `'any'`. */
|
|
9902
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);
|
|
9903
10132
|
readonly handler: SubscribeHandler;
|
|
9904
10133
|
}
|
|
9905
10134
|
|
|
@@ -10736,6 +10965,8 @@ export declare const VOLTRO_TENANT_MIXIN_ID: "voltro/tenant";
|
|
|
10736
10965
|
*/
|
|
10737
10966
|
export declare const _voltroApprovalsTable: TableLike;
|
|
10738
10967
|
|
|
10968
|
+
export declare const _voltroChangeClaimsTable: TableLike;
|
|
10969
|
+
|
|
10739
10970
|
/**
|
|
10740
10971
|
* `_voltro_connection_grants` — an IN-FLIGHT oauth2 handshake. Separate from
|
|
10741
10972
|
* the credential table on purpose: a handshake is a short-lived, single-use
|
|
@@ -11489,4 +11720,13 @@ export declare const wrapStoreWithBootCodec: (underlying: DataStore, dialectId:
|
|
|
11489
11720
|
|
|
11490
11721
|
export declare const wrapStoreWithMixinBehaviour: (rawUnderlying: DataStore, ctx: StoreMiddlewareContext) => FluentStore;
|
|
11491
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
|
+
|
|
11492
11732
|
export { }
|