@voltro/runtime 0.31.0 → 0.33.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 +368 -0
- package/THIRD-PARTY-NOTICES.md +1 -29
- package/dist/index.d.ts +199 -23
- package/dist/index.js +1551 -1487
- package/package.json +16 -10
package/dist/index.d.ts
CHANGED
|
@@ -1602,6 +1602,36 @@ declare const CircuitOpen_base: Schema.TaggedErrorClass<CircuitOpen, "CircuitOpe
|
|
|
1602
1602
|
|
|
1603
1603
|
export declare type CircuitState = 'closed' | 'open' | 'half-open';
|
|
1604
1604
|
|
|
1605
|
+
/** How many bucket widths of predecessors a won claim leaves behind. */
|
|
1606
|
+
export declare const CLAIM_RETENTION_BUCKETS = 64;
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* How long a claim row outlives its own bucket before the next winner deletes
|
|
1610
|
+
* it — and the reasoning is the whole safety argument, so read it before
|
|
1611
|
+
* shrinking the number.
|
|
1612
|
+
*
|
|
1613
|
+
* A claim answers ONE question ("has this bucket been taken"), and it is only
|
|
1614
|
+
* ever asked while some replica still has a timer pending for that bucket.
|
|
1615
|
+
* Delete it too early and a straggler re-claims a bucket that already fired,
|
|
1616
|
+
* which is a DOUBLE FIRE — the failure this table exists to prevent. So the
|
|
1617
|
+
* grace has to cover the longest a replica can plausibly be late.
|
|
1618
|
+
*
|
|
1619
|
+
* The two callers are late in different ways, which is why the width is a
|
|
1620
|
+
* parameter rather than a constant:
|
|
1621
|
+
*
|
|
1622
|
+
* - `scheduleCoordinated` computes its bucket from `Date.now()` AT TICK TIME.
|
|
1623
|
+
* A tick stalled by ten minutes therefore claims the CURRENT bucket, never
|
|
1624
|
+
* the one it was armed for — a stale bucket is unreachable by construction,
|
|
1625
|
+
* and 64 widths is generous past the point of paranoia.
|
|
1626
|
+
* - a CRON firing carries its own scheduled instant, so a stalled firing DOES
|
|
1627
|
+
* re-present an old bucket. It passes no width, so it gets 64 × 60 s ≈ 68
|
|
1628
|
+
* minutes of grace, and a cron fires at most once a minute, so that costs
|
|
1629
|
+
* ~68 rows.
|
|
1630
|
+
*
|
|
1631
|
+
* The floor keeps a sub-second task from computing a grace measured in seconds.
|
|
1632
|
+
*/
|
|
1633
|
+
export declare const claimGraceMs: (bucketWidthMs: number | undefined) => number;
|
|
1634
|
+
|
|
1605
1635
|
/**
|
|
1606
1636
|
* Decide whether a candidate matches a maintainable shape. Conservative by
|
|
1607
1637
|
* design — anything not provably maintainable is rejected (→ full recompute),
|
|
@@ -1852,6 +1882,8 @@ export declare interface ConnectionTokens {
|
|
|
1852
1882
|
readonly scopes: ReadonlyArray<string>;
|
|
1853
1883
|
}
|
|
1854
1884
|
|
|
1885
|
+
export declare type CoordinatedEffect = () => void | CoordinatedTickOutcome | Promise<void | CoordinatedTickOutcome>;
|
|
1886
|
+
|
|
1855
1887
|
export declare interface CoordinatedScheduleDeps {
|
|
1856
1888
|
/** The exactly-once gate. `singleCoordinator` for one-process
|
|
1857
1889
|
* deployments; `makeAdvisoryLockCoordinator(store, replicaId)` for
|
|
@@ -1861,6 +1893,31 @@ export declare interface CoordinatedScheduleDeps {
|
|
|
1861
1893
|
readonly log?: SchedulerLogger;
|
|
1862
1894
|
/** Stable id of this replica — recorded on the claim row it wins. */
|
|
1863
1895
|
readonly replicaId: string;
|
|
1896
|
+
/**
|
|
1897
|
+
* Ceiling for the idle backoff. Defaults to `VOLTRO_POLL_CEILING_MS` (see
|
|
1898
|
+
* {@link readPollCeilingMs}), else {@link DEFAULT_MAX_IDLE_INTERVAL_MS}.
|
|
1899
|
+
* Set it EQUAL to the base interval to
|
|
1900
|
+
* opt a task out of backing off entirely — which is the right call only for a
|
|
1901
|
+
* task whose work cannot announce itself.
|
|
1902
|
+
*/
|
|
1903
|
+
readonly maxIdleIntervalMs?: number;
|
|
1904
|
+
/**
|
|
1905
|
+
* **Stop ticking entirely** on an idle tick with no known deadline, and come
|
|
1906
|
+
* back only on `wake()`. Default `false`.
|
|
1907
|
+
*
|
|
1908
|
+
* This is the difference between "a poller that got cheaper" and "no poller".
|
|
1909
|
+
* A deployment that never uses the queue this task drains pays ONE tick at
|
|
1910
|
+
* boot — which is not optional, it is what finds work a previous process left
|
|
1911
|
+
* behind — and then nothing at all.
|
|
1912
|
+
*
|
|
1913
|
+
* **Only pass `true` when an arrival is GUARANTEED to produce a `wake()`.**
|
|
1914
|
+
* That is a claim about the deployment, not about the task: with Postgres
|
|
1915
|
+
* LISTEN/NOTIFY or a broadcast transport every replica sees every enqueue, so
|
|
1916
|
+
* it holds. Without either, a REMOTE replica's enqueue produces no local
|
|
1917
|
+
* event, and a disarmed task would sleep through it forever. The backoff
|
|
1918
|
+
* ceiling exists for exactly that case and is the correct choice there.
|
|
1919
|
+
*/
|
|
1920
|
+
readonly disarmWhenIdle?: boolean;
|
|
1864
1921
|
}
|
|
1865
1922
|
|
|
1866
1923
|
export declare interface CoordinatedScheduleHandle {
|
|
@@ -1868,6 +1925,52 @@ export declare interface CoordinatedScheduleHandle {
|
|
|
1868
1925
|
readonly stop: () => void;
|
|
1869
1926
|
/** The task name (for logging / dedup diagnostics). */
|
|
1870
1927
|
readonly name: string;
|
|
1928
|
+
/**
|
|
1929
|
+
* Run a tick now, because something arrived.
|
|
1930
|
+
*
|
|
1931
|
+
* Coalesced to at most one extra tick per BASE interval: a queue drain writes
|
|
1932
|
+
* to the very table whose change events trigger this, so an uncoalesced wake
|
|
1933
|
+
* is a loop that feeds itself. Safe to call from a change handler, from any
|
|
1934
|
+
* replica, at any rate.
|
|
1935
|
+
*/
|
|
1936
|
+
readonly wake: () => void;
|
|
1937
|
+
/** The delay the next tick is currently armed for. Exposed for tests and the
|
|
1938
|
+
* inspect surface — a task sitting at the idle ceiling and one hammering the
|
|
1939
|
+
* base interval look identical from outside otherwise. */
|
|
1940
|
+
readonly currentIntervalMs: () => number;
|
|
1941
|
+
/** `false` once the task has stopped ticking and is waiting for `wake()`
|
|
1942
|
+
* (see `disarmWhenIdle`). A disarmed task and a stopped one are the same
|
|
1943
|
+
* thing from outside otherwise, and only one of them comes back. */
|
|
1944
|
+
readonly isArmed: () => boolean;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
/** Per-task overrides a caller may pass alongside the effect. */
|
|
1948
|
+
export declare interface CoordinatedTaskOptions {
|
|
1949
|
+
/** See {@link CoordinatedScheduleDeps.disarmWhenIdle}. Per TASK rather than
|
|
1950
|
+
* per process, because whether an arrival wakes you is a property of the
|
|
1951
|
+
* queue you drain — one plugin may have a change channel on its table and
|
|
1952
|
+
* another none. */
|
|
1953
|
+
readonly disarmWhenIdle?: boolean;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
/** What a tick learned. Returning nothing means "assume there was work" —
|
|
1957
|
+
* the conservative reading, so a task that does not report cannot be backed
|
|
1958
|
+
* off into missing something. */
|
|
1959
|
+
export declare interface CoordinatedTickOutcome {
|
|
1960
|
+
/** `true` when the tick found nothing to do. Only an idle tick backs off. */
|
|
1961
|
+
readonly idle: boolean;
|
|
1962
|
+
/**
|
|
1963
|
+
* Milliseconds until the earliest thing this task already knows is coming —
|
|
1964
|
+
* a debounce window closing, a lease expiring. Caps the backoff, so a task
|
|
1965
|
+
* that is idle RIGHT NOW but has a deadline in 400 ms is armed for 400 ms
|
|
1966
|
+
* rather than for 30 s.
|
|
1967
|
+
*
|
|
1968
|
+
* This is the part a fixed interval cannot express and the part that makes
|
|
1969
|
+
* the backoff safe: without it, backing off is a bet that nothing time-based
|
|
1970
|
+
* is pending, and deferring controls are exactly the case where that bet is
|
|
1971
|
+
* wrong.
|
|
1972
|
+
*/
|
|
1973
|
+
readonly nextDueInMs?: number;
|
|
1871
1974
|
}
|
|
1872
1975
|
|
|
1873
1976
|
export declare type CoordinationOutcome = 'single' | 'wonLock' | 'lostLock' | 'external' | 'cluster';
|
|
@@ -1880,7 +1983,15 @@ export declare type CoordinationOutcome = 'single' | 'wonLock' | 'lostLock' | 'e
|
|
|
1880
1983
|
*/
|
|
1881
1984
|
export declare interface Coordinator {
|
|
1882
1985
|
readonly kind: 'single' | 'advisoryLock' | 'cluster';
|
|
1883
|
-
|
|
1986
|
+
/**
|
|
1987
|
+
* @param bucketWidthMs How far apart two consecutive buckets of THIS caller
|
|
1988
|
+
* are. Optional, and it is not used to decide the claim — it sizes how long
|
|
1989
|
+
* a won claim keeps its own predecessors around (see `claimGraceMs`). A cron
|
|
1990
|
+
* omits it and gets the conservative default; `scheduleCoordinated` passes
|
|
1991
|
+
* its interval, which is how a 250 ms task stops leaving a day of rows
|
|
1992
|
+
* behind.
|
|
1993
|
+
*/
|
|
1994
|
+
tryClaim(scheduleName: string, scheduledAt: Date, bucketWidthMs?: number): Promise<boolean>;
|
|
1884
1995
|
}
|
|
1885
1996
|
|
|
1886
1997
|
/**
|
|
@@ -2300,6 +2411,14 @@ export declare const dataStoreKvStore: (store: DataStore) => KvStoreShape;
|
|
|
2300
2411
|
* cipher is registered (a genuine ciphertext with a wrong key throws GCM). */
|
|
2301
2412
|
export declare const decryptField: (value: string) => string;
|
|
2302
2413
|
|
|
2414
|
+
/** Assumed distance between buckets when a caller passes none — the cron
|
|
2415
|
+
* engine's finest useful cadence. */
|
|
2416
|
+
export declare const DEFAULT_CLAIM_BUCKET_MS = 60000;
|
|
2417
|
+
|
|
2418
|
+
/** Ceiling the idle backoff climbs to. Deliberately short enough to be a
|
|
2419
|
+
* FLOOR under a missed wake rather than a substitute for one. */
|
|
2420
|
+
export declare const DEFAULT_MAX_IDLE_INTERVAL_MS = 30000;
|
|
2421
|
+
|
|
2303
2422
|
/** Default `POST /rpc` body cap: 8 MiB. Generous for any JSON rpc envelope,
|
|
2304
2423
|
* small enough to stop a pathological body being buffered into memory. */
|
|
2305
2424
|
export declare const DEFAULT_MAX_RPC_BODY_BYTES: number;
|
|
@@ -4516,9 +4635,21 @@ export declare const makeActionRunner: (deps: ActionRunnerDeps) => (action: Muta
|
|
|
4516
4635
|
* session-level `pg_advisory_lock` (which is tied to a connection
|
|
4517
4636
|
* that a pool may hand to another query before we unlock).
|
|
4518
4637
|
*
|
|
4519
|
-
* The
|
|
4520
|
-
* winner doesn't block the next firing (= next
|
|
4521
|
-
*
|
|
4638
|
+
* The bucket keying makes claims self-expiring as a DECISION: a crashed
|
|
4639
|
+
* winner doesn't block the next firing (= next bucket = new key). It did
|
|
4640
|
+
* not make them self-expiring as ROWS, and that distinction cost a consumer
|
|
4641
|
+
* their whole deployment — 86 214 rows / 33 MB over two days, read in full
|
|
4642
|
+
* on every claim check, ten of a fifteen-slot pooler pinned on the scan, an
|
|
4643
|
+
* SSR render behind them at 300 490 ms, and a `rollout restart` that could
|
|
4644
|
+
* not complete because the surge pod could not get a connection.
|
|
4645
|
+
*
|
|
4646
|
+
* **A won claim now deletes its own predecessors** (`claimGraceMs` above),
|
|
4647
|
+
* which is what bounds the table rather than merely slowing its growth. The
|
|
4648
|
+
* retention sweep both boot paths register (`wireRetentionSweep`,
|
|
4649
|
+
* `VOLTRO_SCHEDULE_CLAIMS_TTL_HOURS`) STAYS as the backstop, and it is not
|
|
4650
|
+
* redundant: this prune is per SCHEDULE NAME and only runs when that name wins
|
|
4651
|
+
* again, so the rows of a schedule that was renamed or deleted have nothing
|
|
4652
|
+
* left to clean them up.
|
|
4522
4653
|
*
|
|
4523
4654
|
* `scheduledAt` is the DETERMINISTIC cron instant (not `Date.now()`),
|
|
4524
4655
|
* so every replica computes the SAME bucket regardless of clock skew
|
|
@@ -4616,7 +4747,7 @@ export declare const makeConnectionsFacade: (deps: ConnectionsFacadeDeps) => Con
|
|
|
4616
4747
|
* handle is tracked by the caller (the CLI) so `onDeactivate` can stop
|
|
4617
4748
|
* every task a plugin armed.
|
|
4618
4749
|
*/
|
|
4619
|
-
export declare const makeCoordinatedScheduler: (deps: CoordinatedScheduleDeps) =>
|
|
4750
|
+
export declare const makeCoordinatedScheduler: (deps: CoordinatedScheduleDeps) => PluginScheduleCoordinated;
|
|
4620
4751
|
|
|
4621
4752
|
/**
|
|
4622
4753
|
* Build a request-scoped loader. One instance per AppContext — see the module
|
|
@@ -5121,6 +5252,18 @@ export declare interface MutationStore extends DataStore {
|
|
|
5121
5252
|
hardDelete(table: string, primaryKey: string): Promise<boolean>;
|
|
5122
5253
|
}
|
|
5123
5254
|
|
|
5255
|
+
/**
|
|
5256
|
+
* The backoff curve, extracted so it can be asserted directly — a schedule
|
|
5257
|
+
* that backs off wrongly is otherwise only visible as a latency an integration
|
|
5258
|
+
* test does not measure.
|
|
5259
|
+
*
|
|
5260
|
+
* Doubling rather than jumping to the ceiling: a queue that just went quiet is
|
|
5261
|
+
* the likeliest one to receive something next, and doubling keeps the first few
|
|
5262
|
+
* idle ticks cheap in latency while still reaching the ceiling in five steps
|
|
5263
|
+
* from 1 s.
|
|
5264
|
+
*/
|
|
5265
|
+
export declare const nextDelay: (outcome: void | CoordinatedTickOutcome, baseMs: number, currentMs: number, maxIdleMs: number, disarmWhenIdle?: boolean) => number | "disarm";
|
|
5266
|
+
|
|
5124
5267
|
/** Next firing strictly after `after` (default: now). */
|
|
5125
5268
|
export declare const nextFiring: (def: ScheduleDefinition, after?: Date) => Date;
|
|
5126
5269
|
|
|
@@ -5356,6 +5499,18 @@ export declare interface OrchestratorTickDeps {
|
|
|
5356
5499
|
readonly log?: WakeOrchestratorLogger;
|
|
5357
5500
|
}
|
|
5358
5501
|
|
|
5502
|
+
/**
|
|
5503
|
+
* Map an `import('@effect/opentelemetry')` rejection to what the reader needs.
|
|
5504
|
+
*
|
|
5505
|
+
* Exported so the branch is testable without uninstalling the package. Only a
|
|
5506
|
+
* module-NOT-FOUND becomes the install instruction: anything else is a real
|
|
5507
|
+
* load failure inside a package that IS present, and renaming that to "not
|
|
5508
|
+
* installed" sends the reader to reinstall something already there. A catch-all
|
|
5509
|
+
* that relabels every failure is how a diagnosis gets buried — the same reason
|
|
5510
|
+
* the serve bundle marks its deliberate refusals instead of swallowing throws.
|
|
5511
|
+
*/
|
|
5512
|
+
export declare const otelImportFailure: (cause: unknown) => Error;
|
|
5513
|
+
|
|
5359
5514
|
/**
|
|
5360
5515
|
* Retention, part 1 of 2 — the PER-ENTRY cap.
|
|
5361
5516
|
*
|
|
@@ -5544,8 +5699,8 @@ export declare interface PluginRefStore {
|
|
|
5544
5699
|
delete: (table: string, id: string) => Promise<unknown>;
|
|
5545
5700
|
}
|
|
5546
5701
|
|
|
5547
|
-
/** The
|
|
5548
|
-
export declare type PluginScheduleCoordinated = (name: string, intervalMs: number, effect:
|
|
5702
|
+
/** The signature a plugin sees on its bind-ctx. */
|
|
5703
|
+
export declare type PluginScheduleCoordinated = (name: string, intervalMs: number, effect: CoordinatedEffect, options?: CoordinatedTaskOptions) => CoordinatedScheduleHandle;
|
|
5549
5704
|
|
|
5550
5705
|
export declare const powerOfTwoSelector: (options?: P2COptions) => ReplicaSelector;
|
|
5551
5706
|
|
|
@@ -5879,6 +6034,29 @@ export declare type ReactiveReturn<D extends ExecutorDescriptor> = D extends {
|
|
|
5879
6034
|
readonly descriptor: QueryDescriptor;
|
|
5880
6035
|
} : never;
|
|
5881
6036
|
|
|
6037
|
+
/**
|
|
6038
|
+
* The ceiling, tunable per deployment via `VOLTRO_POLL_CEILING_MS`.
|
|
6039
|
+
*
|
|
6040
|
+
* This is the ONE number worth exposing, and the reason is what the ceiling
|
|
6041
|
+
* means: it is how long an arrival can wait when nothing woke the task. With
|
|
6042
|
+
* reactivity it is never reached. WITHOUT it — a dialect with no CDC and no
|
|
6043
|
+
* broadcast transport, where a remote replica's enqueue produces no local
|
|
6044
|
+
* event — it is the whole latency budget, and only the operator knows how much
|
|
6045
|
+
* of one they have.
|
|
6046
|
+
*
|
|
6047
|
+
* An env var rather than an `app.config.ts` field on purpose: it is an
|
|
6048
|
+
* operational number, it must be identical under `voltro dev` and `voltro
|
|
6049
|
+
* serve`, and a second source for one value is how the two boot paths come to
|
|
6050
|
+
* disagree. Read here, once, so neither path can supply its own.
|
|
6051
|
+
*
|
|
6052
|
+
* An unparseable or non-positive value is ignored rather than honoured — a
|
|
6053
|
+
* ceiling of 0 would turn every idle task into a spin, which is the exact
|
|
6054
|
+
* pathology the backoff exists to remove.
|
|
6055
|
+
*/
|
|
6056
|
+
export declare const readPollCeilingMs: (env?: {
|
|
6057
|
+
readonly VOLTRO_POLL_CEILING_MS?: string;
|
|
6058
|
+
}) => number;
|
|
6059
|
+
|
|
5882
6060
|
/** `admin:full` (mirrors `@voltro/protocol`'s ADMIN_SCOPE) bypasses ReBAC. */
|
|
5883
6061
|
export declare const REBAC_ADMIN_SCOPE = "admin:full";
|
|
5884
6062
|
|
|
@@ -7080,29 +7258,27 @@ export declare interface ScheduleContext {
|
|
|
7080
7258
|
}
|
|
7081
7259
|
|
|
7082
7260
|
/**
|
|
7083
|
-
* Run `effect`
|
|
7261
|
+
* Run `effect` on only ONE replica per tick, on a cadence that follows the
|
|
7262
|
+
* work rather than a fixed clock.
|
|
7084
7263
|
*
|
|
7085
|
-
*
|
|
7086
|
-
*
|
|
7087
|
-
*
|
|
7088
|
-
*
|
|
7089
|
-
*
|
|
7090
|
-
* the window and races on the identical claim key — the INSERT-wins
|
|
7091
|
-
* arbiter picks one.
|
|
7264
|
+
* Each tick floors the wall clock to an `intervalMs` bucket and asks the
|
|
7265
|
+
* coordinator to claim `(name, bucket)`. Only the replica that wins runs the
|
|
7266
|
+
* effect; the rest skip. Because the bucket comes from the shared wall clock
|
|
7267
|
+
* (not each replica's tick offset), every replica computes the same bucket
|
|
7268
|
+
* within the window and races on the identical claim key.
|
|
7092
7269
|
*
|
|
7093
|
-
* Non-dying: a throw inside `effect` is caught + logged
|
|
7094
|
-
* always re-armed. The timer is `unref`'d so it never keeps the process
|
|
7095
|
-
* alive on its own (mirrors the bare-`setInterval` behaviour it replaces).
|
|
7270
|
+
* Non-dying: a throw inside `effect` is caught + logged and the next tick is
|
|
7271
|
+
* always re-armed. The timer is `unref`'d so it never keeps the process alive.
|
|
7096
7272
|
*
|
|
7097
7273
|
* @param name Stable task name, namespaced by the caller (a plugin
|
|
7098
7274
|
* passes e.g. `presence.sweep`). Used as the claim key
|
|
7099
7275
|
* prefix + in logs.
|
|
7100
|
-
* @param intervalMs
|
|
7101
|
-
*
|
|
7102
|
-
*
|
|
7103
|
-
*
|
|
7276
|
+
* @param intervalMs The BASE period — the fastest this task ticks, the claim
|
|
7277
|
+
* bucket width, and the wake coalescing window.
|
|
7278
|
+
* @param effect The work. Return a {@link CoordinatedTickOutcome} to let
|
|
7279
|
+
* the runner back off when there is nothing to do.
|
|
7104
7280
|
*/
|
|
7105
|
-
export declare const scheduleCoordinated: (name: string, intervalMs: number, effect:
|
|
7281
|
+
export declare const scheduleCoordinated: (name: string, intervalMs: number, effect: CoordinatedEffect, deps: CoordinatedScheduleDeps) => CoordinatedScheduleHandle;
|
|
7106
7282
|
|
|
7107
7283
|
export declare interface ScheduleDefinition {
|
|
7108
7284
|
readonly name: string;
|