@voltro/runtime 0.53.0 → 0.55.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
@@ -94,6 +94,7 @@ import { SyncLogger } from '@voltro/logger';
94
94
  import { TableLike } from '@voltro/database';
95
95
  import { TableName } from '@voltro/protocol';
96
96
  import { TableValidationFailed } from '@voltro/protocol';
97
+ import { TargetRelation } from '@voltro/protocol';
97
98
  import { TenantRowNotFound } from '@voltro/protocol';
98
99
  import { TenantScopeViolation } from '@voltro/protocol';
99
100
  import { Tracer } from 'effect';
@@ -1124,7 +1125,14 @@ export declare const applyConnectionCredential: (clientId: number, headers: Reco
1124
1125
  * - a non-array value is a refusal naming the field — silently ignoring it
1125
1126
  * would read as "saved" while the links stayed stale;
1126
1127
  * - the row id is `output.id`, else `input.id`; with neither the declaration
1127
- * is unusable and says so.
1128
+ * is unusable and says so;
1129
+ * - the DECLARED junction columns must match the junction's real reference
1130
+ * pair. They are declaration data because the browser drives junction
1131
+ * auto-optimistic from the same block and cannot derive them; this is the
1132
+ * one place both readings meet, so a disagreement is refused here — before
1133
+ * any write — with the correct pair named. Letting it through would mean the
1134
+ * client patched one column while the server wrote another, and the UI would
1135
+ * look right until the delta contradicted it.
1128
1136
  */
1129
1137
  export declare const applyDeclaredRelations: (params: {
1130
1138
  readonly store: RelationLinksStore;
@@ -2640,6 +2648,8 @@ export declare const counter: (name: string, description?: string) => Metric.Met
2640
2648
  */
2641
2649
  export declare const countRunningWorkflows: (store: DataStore) => Promise<number>;
2642
2650
 
2651
+ export declare const crdtCompactMaxBytes: () => number;
2652
+
2643
2653
  /**
2644
2654
  * Every firing instant of `def` in `(from, to]`, oldest first, bounded by
2645
2655
  * `cap + 1` entries.
@@ -2917,6 +2927,8 @@ export declare const DEFAULT_CLAIM_BUCKET_MS = 60000;
2917
2927
 
2918
2928
  export declare const DEFAULT_COMPRESSION_MIN_BYTES = 1024;
2919
2929
 
2930
+ export declare const DEFAULT_CRDT_COMPACT_MAX_BYTES: number;
2931
+
2920
2932
  /**
2921
2933
  * How many subscribers one change event is delivered to CONCURRENTLY.
2922
2934
  *
@@ -3301,7 +3313,12 @@ export declare class Dispatcher {
3301
3313
  /** Re-run the query's `guards:` before every delivery — see
3302
3314
  * `ActiveSubscription.reauthorize`. Omitted for unguarded queries. */
3303
3315
  reauthorize?: () => Promise<unknown>,
3304
- /** Re-resolve row visibility per delivery — see `ActiveSubscription.refilter`. */
3316
+ /** Re-resolve row visibility per delivery — see `ActiveSubscription.refilter`.
3317
+ *
3318
+ * Optional ONLY in the sense that an app with no registered row filter has
3319
+ * nothing to resolve; it is not the caller's choice. Omitting it while a
3320
+ * filter IS registered throws below, because the read would then ignore the
3321
+ * filter entirely. Build it with `makeDefaultRefilter(subject)`. */
3305
3322
  refilter?: () => Promise<RowFilterScope>): Promise<() => void>;
3306
3323
  /**
3307
3324
  * Tear a subscription down because it can no longer be served CORRECTLY
@@ -5235,6 +5252,17 @@ export declare const ivmValue: (shape: AggregateShape, g: GroupState) => number
5235
5252
  * columns is rejected with a message naming what it found.
5236
5253
  */
5237
5254
  export declare interface JunctionLinks {
5255
+ /**
5256
+ * The junction's two reference columns as RESOLVED for this anchor — the
5257
+ * source of truth a declared `relations:` is verified against. It is exposed
5258
+ * because the pair also has to be stated in the descriptor (the browser
5259
+ * cannot derive it), and a stated fact that nothing compares against is a
5260
+ * divergence waiting to happen.
5261
+ */
5262
+ readonly columns: {
5263
+ readonly anchorColumn: string;
5264
+ readonly targetColumn: string;
5265
+ };
5238
5266
  /** The current target ids linked to the anchor. */
5239
5267
  list(): Promise<ReadonlyArray<string>>;
5240
5268
  /** Reconcile the links to EXACTLY `targetIds` — insert the missing, delete the
@@ -5552,6 +5580,25 @@ export declare const makeCoordinatedScheduler: (deps: CoordinatedScheduleDeps) =
5552
5580
  */
5553
5581
  export declare const makeDataLoader: (deps: LoaderDeps) => DataLoader;
5554
5582
 
5583
+ /**
5584
+ * The per-delivery refilter EVERY subscribing transport must hand the
5585
+ * dispatcher — the one that re-resolves this subject's visibility before each
5586
+ * delivery instead of freezing it at subscribe.
5587
+ *
5588
+ * It lives here, beside the registration it reads, because it was a private
5589
+ * helper of the WebSocket entrypoint and the transports that did not import it
5590
+ * did not filter at all. `dispatcher.subscribe` resolves visibility itself and
5591
+ * treats an absent refilter as "this app has no filter", so a transport that
5592
+ * simply omitted the argument read the UNFILTERED descriptor — on the initial
5593
+ * snapshot and on every delta. The SSE and gRPC projections omitted it.
5594
+ *
5595
+ * Returning `undefined` when no filter is registered is the fast path, not an
5596
+ * opt-out: it is what keeps a filterless app free of a per-delivery await, and
5597
+ * `dispatcher.subscribe` refuses the ambiguous case (nothing passed WHILE a
5598
+ * filter is registered) rather than reading it as this one.
5599
+ */
5600
+ export declare const makeDefaultRefilter: (subject: Subject, onError?: (error: unknown) => void) => (() => Promise<RowFilterScope>) | undefined;
5601
+
5555
5602
  /** `__voltro.connections.disconnect` — forget the caller's credential. */
5556
5603
  export declare const makeDisconnectConnectionExecutor: (deps: ConnectionBuiltinDeps) => (input: {
5557
5604
  readonly connectionId: string;
@@ -6059,10 +6106,10 @@ export declare interface MutationLike {
6059
6106
  readonly requiresApproval?: AnyApprovalPolicy | undefined;
6060
6107
  readonly target?: {
6061
6108
  readonly table: string;
6062
- readonly relations?: Readonly<Record<string, string>> | undefined;
6109
+ readonly relations?: Readonly<Record<string, TargetRelation>> | undefined;
6063
6110
  } | ReadonlyArray<{
6064
6111
  readonly table: string;
6065
- readonly relations?: Readonly<Record<string, string>> | undefined;
6112
+ readonly relations?: Readonly<Record<string, TargetRelation>> | undefined;
6066
6113
  }> | undefined;
6067
6114
  };
6068
6115
  executor(input: unknown, ctx: unknown): unknown;
@@ -6271,6 +6318,16 @@ declare const NoRowFound_base: Schema.TaggedErrorClass<NoRowFound, "NoRowFound",
6271
6318
  /** Note one read against the in-flight procedure. No scope → nothing happens. */
6272
6319
  export declare const noteRead: (descriptor: QueryDescriptor) => void;
6273
6320
 
6321
+ /**
6322
+ * Record the resume verdict for one subscription setup. `true` means a ring
6323
+ * was recorded; anything else names why it was not.
6324
+ *
6325
+ * Logs at debug ONCE per (label, verdict) — a hot query must not narrate every
6326
+ * subscribe, and the second identical verdict carries no information the first
6327
+ * did not.
6328
+ */
6329
+ export declare const noteResumeVerdict: (rawLabel: string, verdict: true | ResumeExclusion) => void;
6330
+
6274
6331
  export declare interface Notification {
6275
6332
  readonly subscriberId: string;
6276
6333
  readonly fingerprint: string;
@@ -6768,6 +6825,16 @@ export declare const publishEvent: <Name extends string, Key extends Schema.Sche
6768
6825
  readonly deferred: boolean;
6769
6826
  }, EventPublishError>;
6770
6827
 
6828
+ /**
6829
+ * A Layer that publishes whatever tracer is in scope when it is built.
6830
+ *
6831
+ * Provided immediately INSIDE the tracer layer at boot, so it captures the
6832
+ * configured tracer rather than the default one. With tracing off it captures
6833
+ * Effect's no-op tracer, which is the correct answer — detached work then
6834
+ * behaves exactly as in-request work does.
6835
+ */
6836
+ export declare const publishServerTracerLayer: Layer.Layer<never, never, never>;
6837
+
6771
6838
  /**
6772
6839
  * The two scopes, composed once, for every path that finalises a descriptor.
6773
6840
  *
@@ -6829,9 +6896,23 @@ export declare interface QueryProducerDeps<D> {
6829
6896
  }
6830
6897
 
6831
6898
  export declare interface QuerySubscriberDeps<D> extends QueryProducerDeps<D> {
6832
- /** Open a dispatcher subscription for a finalized descriptor. */
6833
- readonly subscribeDescriptor: (descriptor: D, emit: (event: unknown) => void, context: ServeRequestContext) => Promise<() => void>;
6834
- /** Open a dispatcher subscription for a COMPUTED query (re-runs on source change). */
6899
+ /**
6900
+ * Open a dispatcher subscription for a finalized descriptor.
6901
+ *
6902
+ * `reauthorize` and `refilter` are REQUIRED parameters of this callback, not
6903
+ * optional extras, and the requirement is the guard: they used to be
6904
+ * `dispatcher.subscribe`'s trailing optional arguments, this callback simply
6905
+ * did not pass them, and the dispatcher reads their absence as "this query is
6906
+ * unguarded and this app has no row filter". So the SSE and gRPC projections
6907
+ * re-ran no guard and read the UNFILTERED descriptor — initial snapshot and
6908
+ * every delta — while the WebSocket path (which goes through
6909
+ * `bindSubscription`) did both. Threading them through the signature makes the
6910
+ * next transport unable to repeat that by omission.
6911
+ */
6912
+ readonly subscribeDescriptor: (descriptor: D, emit: (event: unknown) => void, context: ServeRequestContext, reauthorize: (() => Promise<unknown>) | undefined, refilter: (() => Promise<RowFilterScope>) | undefined) => Promise<() => void>;
6913
+ /** Open a dispatcher subscription for a COMPUTED query (re-runs on source change).
6914
+ * A computed query re-runs its HANDLER per change, so guards and the row
6915
+ * filter are re-applied by that re-run — it needs no separate pair. */
6835
6916
  readonly subscribeComputed: (computed: ComputedQuery, emit: (event: unknown) => void, context: ServeRequestContext) => Promise<() => void>;
6836
6917
  }
6837
6918
 
@@ -7479,9 +7560,15 @@ export declare interface RegistryTableLike {
7479
7560
  * request was. */
7480
7561
  export declare const rehydrateGuards: (serialized: ReadonlyArray<SerializedGuard>) => ReadonlyArray<AnyGuardSpec>;
7481
7562
 
7482
- /** The store slice `applyDeclaredRelations` needs. */
7563
+ /** The store slice `applyDeclaredRelations` needs. `columns` is the junction's
7564
+ * DERIVED reference pair — the truth the descriptor's declaration is checked
7565
+ * against before anything is written. */
7483
7566
  declare interface RelationLinksStore {
7484
7567
  relationLinks(junctionTable: string, anchorTable: string, anchorId: string): {
7568
+ readonly columns: {
7569
+ readonly anchorColumn: string;
7570
+ readonly targetColumn: string;
7571
+ };
7485
7572
  set(targetIds: ReadonlyArray<string>): Promise<unknown>;
7486
7573
  };
7487
7574
  }
@@ -7680,6 +7767,9 @@ export declare const resetEventMetricTagCacheForTests: () => void;
7680
7767
  /** Drop everything recorded. For tests; not called by the runtime. */
7681
7768
  export declare const resetObservedGraph: () => void;
7682
7769
 
7770
+ /** Clear the census. For tests; a running app has no reason to forget. */
7771
+ export declare const resetResumeCensus: () => void;
7772
+
7683
7773
  /** Test seam. */
7684
7774
  export declare const resetResumeRingsForTest: () => void;
7685
7775
 
@@ -8019,6 +8109,31 @@ export declare type ResumeAnswer =
8019
8109
  readonly kind: 'snapshot';
8020
8110
  };
8021
8111
 
8112
+ /** The census as of now, one entry per label that has been subscribed. */
8113
+ export declare const resumeCensus: () => ReadonlyArray<ResumeVerdict>;
8114
+
8115
+ /**
8116
+ * Why a subscription carries no resume ring.
8117
+ *
8118
+ * - `computed` — the executor returned a value, not a descriptor. There is no
8119
+ * delta chain to replay: the handler re-runs opaquely and emits snapshots.
8120
+ * Declaring `tables:` on the row filter cannot change this; returning the
8121
+ * query builder instead of a mapped value can.
8122
+ * - `row-filter` — the registered filter may narrow this subscription's source
8123
+ * table, so its row set is re-resolved per delivery. Declaring `tables:`
8124
+ * fixes it for every source the filter does NOT touch; for a source it does
8125
+ * touch, the exclusion is the point.
8126
+ * - `eager-load` — the descriptor carries `.with(...)` while a filter is
8127
+ * registered, and the relation is resolved below the seam that would narrow
8128
+ * it. Reported only when the base table is NOT itself narrowed, so this
8129
+ * reason always means: drop the eager load and the subscription resumes.
8130
+ * - `uncanonical-input` — the input cannot be reduced to a stable ring key, so
8131
+ * two different inputs would collide under one label.
8132
+ * - `not-offered` — the binder was driven without resume input at all (the
8133
+ * lower-level entrypoint used by tests).
8134
+ */
8135
+ export declare type ResumeExclusion = 'computed' | 'row-filter' | 'eager-load' | 'uncanonical-input' | 'not-offered';
8136
+
8022
8137
  /**
8023
8138
  * The identity a ring is recorded under — and the identity a resume must
8024
8139
  * present to read it back. `subjectId`/`tenantId` are part of the KEY, so a
@@ -8047,6 +8162,14 @@ export declare interface ResumeTunables {
8047
8162
 
8048
8163
  export declare const resumeTunables: () => ResumeTunables;
8049
8164
 
8165
+ /** One label's tally. `resumable + Σ excluded` is the number of subscriptions
8166
+ * set up on this label since the process started. */
8167
+ export declare interface ResumeVerdict {
8168
+ readonly label: string;
8169
+ readonly resumable: number;
8170
+ readonly excluded: Readonly<Partial<Record<ResumeExclusion, number>>>;
8171
+ }
8172
+
8050
8173
  export { RetentionConflict }
8051
8174
 
8052
8175
  export { RetentionSource }
@@ -8219,7 +8342,85 @@ export declare interface RowFilter<Ctx = unknown> {
8219
8342
  * the handler still carries the check the filter was meant to replace.
8220
8343
  */
8221
8344
  readonly onLoadError?: 'fail' | 'deny';
8222
- }
8345
+ /**
8346
+ * The ONLY tables this filter may narrow. Optional; declaring it buys
8347
+ * delta-resume back for every subscription whose source is not in the set.
8348
+ *
8349
+ * WHY IT EXISTS. A subscription whose row set is re-resolved per delivery
8350
+ * must not replay deltas on reconnect — the answer can change while the
8351
+ * socket is down (a membership ends) and a wrong replay leaks rows. But the
8352
+ * question the framework could ask was only "is a filter registered at
8353
+ * all?", so ONE registration disabled delta-resume for the whole process,
8354
+ * including every subscription reading a table the predicate never returns
8355
+ * anything for.
8356
+ *
8357
+ * WHAT IT DOES NOT BUY, and this bounds the whole feature: a query whose
8358
+ * executor returns a VALUE rather than a descriptor — anything that maps the
8359
+ * rows or wraps them in a page envelope — has no delta chain to resume in
8360
+ * the first place. It re-runs an opaque handler and emits snapshots, filter
8361
+ * or no filter. So the number of subscriptions a declaration can give back
8362
+ * is the number that bind a descriptor, not the number of queries in the
8363
+ * app; declare it anyway (it costs nothing and applies the moment a query
8364
+ * returns a builder), but read `resumeCensus()` — or
8365
+ * `/_voltro/inspect/subscriptions` — before expecting a change.
8366
+ *
8367
+ * WHY A DECLARATION RATHER THAN A PROBE. Resolving the scope at subscribe
8368
+ * and treating `predicate(ctx, source) === undefined` as safe is cheaper and
8369
+ * unsound: the predicate is a function of freshly loaded context, so a table
8370
+ * it does not narrow now may be narrowed on the next delivery — which is the
8371
+ * entire reason the refilter is per-delivery. A static list is a promise
8372
+ * about every future resolution.
8373
+ *
8374
+ * IT IS VERIFIED, not trusted. Returning a predicate for a table outside
8375
+ * this set raises {@link RowFilterDeclarationViolated} at the read that did
8376
+ * it — the request fails and a subscription is revoked, rather than serving
8377
+ * rows under a resume grant the declaration no longer earns. An undeclared
8378
+ * filter (this field absent) keeps the conservative behaviour: no resume
8379
+ * anywhere, no verification.
8380
+ *
8381
+ * const OWNED = new Set(['documents', 'comments'])
8382
+ * setRowFilter({
8383
+ * load,
8384
+ * predicate: (ctx, table) => (OWNED.has(table) ? eq('ownerId', ctx.userId) : undefined),
8385
+ * // Derived from the same set the predicate reads. Two hand-kept
8386
+ * // lists is the shape in which a table lands in exactly one of
8387
+ * // them — and the penalty here is a failing read, not a warning.
8388
+ * tables: [...OWNED],
8389
+ * })
8390
+ */
8391
+ readonly tables?: ReadonlyArray<string>;
8392
+ }
8393
+
8394
+ /**
8395
+ * A row filter narrowed a table its `tables:` declaration does not list.
8396
+ *
8397
+ * Fail-closed by construction: the declaration is what the framework hands
8398
+ * delta-resume, so a filter that quietly narrows beyond it would have rings
8399
+ * recorded for a table whose visibility CAN change — the leak the exclusion
8400
+ * exists to prevent. Raised at the read that violated it, naming the table and
8401
+ * the fix, rather than degrading to an unfiltered or empty answer.
8402
+ */
8403
+ export declare class RowFilterDeclarationViolated extends RowFilterDeclarationViolated_base {
8404
+ }
8405
+
8406
+ declare const RowFilterDeclarationViolated_base: Schema.TaggedErrorClass<RowFilterDeclarationViolated, "RowFilterDeclarationViolated", {
8407
+ readonly _tag: Schema.tag<"RowFilterDeclarationViolated">;
8408
+ } & {
8409
+ /** The table the predicate narrowed. */
8410
+ table: typeof Schema.String;
8411
+ /** The declared set, for the message. */
8412
+ declared: Schema.Array$<typeof Schema.String>;
8413
+ }>;
8414
+
8415
+ /**
8416
+ * Could the registered filter narrow ANY of `tables`?
8417
+ *
8418
+ * The question delta-resume asks before recording a ring. Answers `true`
8419
+ * whenever it cannot prove otherwise — no declaration means the framework does
8420
+ * not know which tables the predicate may reach, and "unknown" must read as
8421
+ * "yes" or the exclusion stops protecting anything.
8422
+ */
8423
+ export declare const rowFilterMayNarrow: (tables: ReadonlyArray<string>) => boolean;
8223
8424
 
8224
8425
  /**
8225
8426
  * Is a row filter registered right now — asked by a path about to serve an
@@ -8342,9 +8543,16 @@ export declare interface RpcServerOptions<Rpcs extends Rpc.Any> {
8342
8543
  * http app, so it fires on EVERY request — inspect, webhooks, AND the
8343
8544
  * rpc websocket upgrade — before auth/routing. A non-null return
8344
8545
  * short-circuits with that response (e.g. a rate-limit 429 / geo-block
8345
- * 451). Absent → no wrapper. Fail-OPEN: a throwing interceptor is
8346
- * swallowed and the request continues, so a buggy shield can't take
8347
- * the whole listener down.
8546
+ * 451). Absent → no wrapper.
8547
+ *
8548
+ * FAIL-CLOSED: a throwing interceptor answers 500 and logs, it does NOT
8549
+ * let the request through. The composed chain is where security gates
8550
+ * live (an IP shield, a tenant fence — anything a plugin mounts
8551
+ * pre-auth), and a gate that crashes open has silently stopped
8552
+ * guarding while still reading as installed. An interceptor that must
8553
+ * not take the listener down with its own dependency owns that
8554
+ * decision itself — plugin-ratelimit's httpShield catches its store
8555
+ * failure and degrades to "unlimited, loudly" rather than throwing.
8348
8556
  */
8349
8557
  readonly pluginHttpInterceptor?: HttpRequestInterceptor;
8350
8558
  /**
@@ -9276,12 +9484,19 @@ export declare type ServeRpcInterceptor = (base: Effect.Effect<unknown, unknown,
9276
9484
  readonly target?: ReadonlyArray<string>;
9277
9485
  }) => Effect.Effect<unknown, unknown, never>;
9278
9486
 
9487
+ /** The published tracer, or `undefined` when tracing is off / no server booted. */
9488
+ export declare const serverTracer: () => Tracer.Tracer | undefined;
9489
+
9279
9490
  export declare const setBufferedBytes: (label: string, bytes: number) => void;
9280
9491
 
9281
9492
  /** Register the process-wide connection resolver (or clear with `undefined`).
9282
9493
  * Called by the CLI at boot; tests call it directly. */
9283
9494
  export declare const setConnectionResolver: (resolver: ConnectionResolver | undefined) => void;
9284
9495
 
9496
+ /** Boot/test override — the slot `wireCrdtTunables` writes. `null` restores
9497
+ * env/default resolution. */
9498
+ export declare const setCrdtCompactMaxBytes: (maxBytes: number | null) => void;
9499
+
9285
9500
  /** Register the field cipher (or clear with `undefined`). */
9286
9501
  export declare const setFieldCipher: (cipher: FieldCipher | undefined) => void;
9287
9502
 
@@ -9305,6 +9520,9 @@ export declare const setRowFilter: <Ctx>(filter: RowFilter<Ctx> | undefined) =>
9305
9520
  /** Install the process-wide secrets backend (called once at boot). */
9306
9521
  export declare const setSecretsBackend: (backend: SecretsBackend) => void;
9307
9522
 
9523
+ /** Publish the server's tracer. Called once, from inside the provided scope. */
9524
+ export declare const setServerTracer: (tracer: Tracer.Tracer | undefined) => void;
9525
+
9308
9526
  /** Install the finding sink. `undefined` turns recording back off. */
9309
9527
  export declare const setSourceGapSink: (fn: ((finding: string) => void) | undefined) => void;
9310
9528
 
@@ -10742,6 +10960,16 @@ export declare const withRowFilter: <R extends ServeRequestContext>(request: R)
10742
10960
  */
10743
10961
  export declare const withScopedRequest: <R extends ServeRequestContext, A>(requestContext: R, body: (scoped: R) => A | Effect.Effect<A, unknown, never>) => A | Effect.Effect<A, unknown, never>;
10744
10962
 
10963
+ /**
10964
+ * Run detached work under the server's tracer, so a span it opens is exported
10965
+ * rather than created and dropped.
10966
+ *
10967
+ * A no-op when nothing is published (a unit test, an embedder, a process with no
10968
+ * rpc server) — never a second tracer, never a throw. The alternative to this
10969
+ * function is not "a different tracer", it is silence.
10970
+ */
10971
+ export declare const withServerTracer: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
10972
+
10745
10973
  /**
10746
10974
  * What the gate decided. `passthrough` is the common case — no controls
10747
10975
  * declared — and is distinct from `start` so the facade can skip the commit