@voltro/runtime 0.25.0 → 0.27.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
@@ -43,6 +43,7 @@ import { Metric } from 'effect';
43
43
  import { MetricBoundaries } from 'effect';
44
44
  import { MetricReader } from '@opentelemetry/sdk-metrics';
45
45
  import { PluginHttpRoute } from '@voltro/protocol';
46
+ import { PluginRefOrphanPolicy } from '@voltro/database';
46
47
  import { Predicate } from '@voltro/database';
47
48
  import { Query } from '@voltro/database';
48
49
  import { QueryDescriptor } from '@voltro/database';
@@ -871,10 +872,23 @@ export declare interface AppContext {
871
872
  * `*.workflow.tsx` files. Starts return a run handle immediately;
872
873
  * `wait`/`poll`/`signal`/`cancel` are explicit operations. */
873
874
  readonly workflows?: WorkflowsAppContext;
874
- /** Domain event service — present when the app has workflow event
875
- * triggers. `emit(name, payload)` records the event and fans out to
876
- * matching workflow triggers. */
877
- readonly events?: EventsAppContext;
875
+ /**
876
+ * `ctx.events.publish(descriptor, key, payload)` present when the app
877
+ * declares `*.event.ts` files.
878
+ *
879
+ * This was typed as the OLD string-emitter facade (`emit(name, data)`) long
880
+ * after that facade was deleted, so the documented call was a `tsc` error
881
+ * while the runtime carried only `publish` — a consumer measured
882
+ * `eventKeys: ["publish"], emitType: undefined` with a cron probe because the
883
+ * type and the docs disagreed and they could not tell which was lying.
884
+ *
885
+ * What let it drift is worth naming: the builder installed the publisher with
886
+ * `as never`, so the compiler had the answer the whole time and was told not
887
+ * to give it. `appContext.ts` even carried a comment stating `emit` is gone —
888
+ * beside the cast that hid it. A context field is the one place this repo
889
+ * treats such a cast as a defect in its own right.
890
+ */
891
+ readonly events?: EventPublisher;
878
892
  /**
879
893
  * Transactional outbox (`ctx.outbox`). Absent when the app declares no
880
894
  * `*.outbox.ts` handler — an enqueue with nobody to deliver it would be a
@@ -936,6 +950,18 @@ export declare const applyInverse: (store: UndoApplyStore, op: InverseOp) => Pro
936
950
  * `synthesizeInverse`), as the caller's transaction wraps them. */
937
951
  export declare const applyInverses: (store: UndoApplyStore, ops: ReadonlyArray<InverseOp>) => Promise<void>;
938
952
 
953
+ /**
954
+ * Apply every rule matching `change`.
955
+ *
956
+ * Returns what it did, so a caller can log it. Never throws for a rule that
957
+ * finds nothing — an orphan rule firing on a row nobody references is the
958
+ * normal case, not an error.
959
+ */
960
+ export declare const applyPluginRefRules: (store: PluginRefStore, rules: ReadonlyArray<PluginRefRule>, change: PluginRefChange) => Promise<ReadonlyArray<{
961
+ readonly rule: PluginRefRule;
962
+ readonly affected: number;
963
+ }>>;
964
+
939
965
  /** AND-merge the row filter for `table` onto an existing predicate. */
940
966
  export declare const applyRowFilter: (scope: RowFilterScope, table: string, predicate: Predicate | undefined) => Predicate | undefined;
941
967
 
@@ -1125,15 +1151,6 @@ export declare interface BeginOAuthResult {
1125
1151
  readonly state: string;
1126
1152
  }
1127
1153
 
1128
- /**
1129
- * Override the subject for an active connection. Called from the
1130
- * `auth.signin` (or any "I just authenticated this caller") handler
1131
- * after the credential check passes. Future calls on the same
1132
- * connection see the new subject via the AuthMiddleware fast-path.
1133
- *
1134
- * Emits to all `onBind` listeners synchronously so subscription
1135
- * registries can re-scope active subscriptions.
1136
- */
1137
1154
  export declare const bindConnectionSubject: (clientId: number, subject: Subject) => void;
1138
1155
 
1139
1156
  /**
@@ -1178,7 +1195,25 @@ export declare const bindMutation: <Input, Output, E = never>(execute: (input: I
1178
1195
  * finalizers run → the provider request aborts), reusing the same
1179
1196
  * per-connection interrupt registry subscriptions use.
1180
1197
  */
1181
- export declare const bindStream: <Element, Err = unknown>(buildStream: (context: RuntimeContext) => Stream.Stream<Element, Err, never> | Effect.Effect<Stream.Stream<Element, Err, never>, unknown, never>, spanName?: string) => Stream.Stream<Element, Err, SubjectService | ConnectionInfo>;
1198
+ export declare const bindStream: <Element, Err = unknown>(buildStream: (context: RuntimeContext) => Stream.Stream<Element, Err, never> | Effect.Effect<Stream.Stream<Element, Err, never>, unknown, never>, spanName?: string,
1199
+ /**
1200
+ * The stream's declared `guards:`, when it has any.
1201
+ *
1202
+ * Checked once at subscribe AND before every element, exactly as a query's
1203
+ * are. A stream was the one realtime primitive that could not express
1204
+ * authorization at all, so whatever protection existed was hand-written in an
1205
+ * executor where nothing could verify it was there.
1206
+ *
1207
+ * A denial ENDS the stream rather than dropping the element: a skipped
1208
+ * element is indistinguishable from "nothing to send", and the client must
1209
+ * learn it lost access instead of inferring it from silence.
1210
+ */
1211
+ guards?: ReadonlyArray<unknown> | undefined,
1212
+ /** The call's decoded input — the guard INPUT, so a resource-scoped guard
1213
+ * (`{ scope: 'x:read', from: 'id' }`) can see which resource was asked for.
1214
+ * Passed explicitly rather than read off RuntimeContext, which does not
1215
+ * carry it. */
1216
+ guardInput?: unknown) => Stream.Stream<Element, Err, SubjectService | ConnectionInfo>;
1182
1217
 
1183
1218
  /**
1184
1219
  * Build a `Stream<SubscriptionEvent<T>>` that an @effect/rpc streaming
@@ -1473,6 +1508,34 @@ export { clearRetentions }
1473
1508
  /** Clear the process-wide handle (test teardown). */
1474
1509
  export declare const clearSystemStoreHandle: () => void;
1475
1510
 
1511
+ /**
1512
+ * Collect the declared rules from the discovered tables.
1513
+ *
1514
+ * `tables` is the boot's table set. Each column carrying a `pluginRef` spec
1515
+ * contributes one rule, with the target resolved through the plugin's exported
1516
+ * handle — so a rename carries the rule.
1517
+ *
1518
+ * Throws when a `pluginRef` names a table nothing registered: a declared rule
1519
+ * against an absent plugin would sit there looking enforced and never fire,
1520
+ * which is the exact failure this framework has shipped too often. The message
1521
+ * names both sides so the fix is obvious (install the plugin, or drop the
1522
+ * column).
1523
+ */
1524
+ export declare const collectPluginRefRules: (tables: ReadonlyArray<{
1525
+ readonly name: string;
1526
+ /** The DECLARED references, carried on the built table. Reading the column
1527
+ * bag instead does not work: `table()` materialises builders into plain
1528
+ * field descriptors, so the spec is gone by the time anyone can ask. That
1529
+ * is how this collector silently produced ZERO rules while its own wiring
1530
+ * test — which only checked that it was CALLED — stayed green. */
1531
+ readonly appliedPluginRefs?: ReadonlyArray<{
1532
+ readonly column: string;
1533
+ readonly target: () => unknown;
1534
+ readonly orphanPolicy: PluginRefOrphanPolicy;
1535
+ readonly onSoftDelete: boolean;
1536
+ }>;
1537
+ }>, registered: ReadonlySet<string>) => ReadonlyArray<PluginRefRule>;
1538
+
1476
1539
  /** Parse + return the compiled `Cron` for a definition. Throws the
1477
1540
  * same way `defineSchedule` validated — callers that already hold a
1478
1541
  * branded definition can trust this won't throw. */
@@ -2834,6 +2897,9 @@ export declare const formatServerOnlyLeaks: (leaks: ReadonlyArray<ServerOnlyLeak
2834
2897
  /** Serialise a span's context as a `traceparent` header value. */
2835
2898
  export declare const formatTraceparent: (ctx: TraceContext) => string;
2836
2899
 
2900
+ /** The boot warning for unresolved sources, or `undefined` when there are none. */
2901
+ export declare const formatUnresolvedSources: (found: ReadonlyArray<UnresolvedSource>) => string | undefined;
2902
+
2837
2903
  /** One captured row change within a mutation invocation. */
2838
2904
  export declare interface ForwardChange {
2839
2905
  readonly table: string;
@@ -2874,6 +2940,8 @@ export declare const getConnectionResolver: () => ConnectionResolver | undefined
2874
2940
  /** Look up the override for a connection. Returns `undefined` if no override is set. */
2875
2941
  export declare const getConnectionSubject: (clientId: number) => Subject | undefined;
2876
2942
 
2943
+ export declare const getCredentialExpiry: (clientId: number) => number | undefined;
2944
+
2877
2945
  /** The registered field cipher, if any. The store middleware reads this. */
2878
2946
  export declare const getFieldCipher: () => FieldCipher | undefined;
2879
2947
 
@@ -3267,6 +3335,20 @@ export declare const isSafeToApply: (current: Record<string, unknown> | null | u
3267
3335
 
3268
3336
  export declare const isScheduleDefinition: (v: unknown) => v is BrandedScheduleDefinition;
3269
3337
 
3338
+ /**
3339
+ * Did this change soft-delete the row?
3340
+ *
3341
+ * A soft delete is NOT a `delete` event — it is an `update` that sets
3342
+ * `deletedAt`. That distinction is why the first version of `onSoftDelete` was
3343
+ * inert in every case: the matcher only looked at `op === 'delete'`, which a
3344
+ * soft delete never is, so the option existed and could not fire. Exactly the
3345
+ * defect this whole round was about, one level down.
3346
+ *
3347
+ * The transition matters, not the value: a row that was already deleted and is
3348
+ * updated again must not re-fire the rule.
3349
+ */
3350
+ export declare const isSoftDelete: (change: PluginRefChange) => boolean;
3351
+
3270
3352
  /** A freshly issued key — `token` is shown ONCE and never stored in clear. */
3271
3353
  export declare interface IssuedApiKey {
3272
3354
  readonly id: string;
@@ -4474,6 +4556,34 @@ export declare const payloadPropertyNames: (schema: Schema.Schema.Any) => {
4474
4556
  readonly optional: ReadonlyArray<string>;
4475
4557
  };
4476
4558
 
4559
+ /** A change as the post-commit channel reports it. */
4560
+ export declare interface PluginRefChange {
4561
+ readonly table: string;
4562
+ readonly op: 'insert' | 'update' | 'delete';
4563
+ readonly rowId: string;
4564
+ readonly tenantId?: string | null;
4565
+ /** The row before, when there was one — used to spot a soft delete. */
4566
+ readonly old?: Record<string, unknown> | null;
4567
+ /** The row after, when there is one. */
4568
+ readonly new?: Record<string, unknown> | null;
4569
+ }
4570
+
4571
+ /** One resolved rule: "when <targetTable> loses a row, do <policy> to <table.column>". */
4572
+ export declare interface PluginRefRule {
4573
+ readonly table: string;
4574
+ readonly column: string;
4575
+ readonly targetTable: string;
4576
+ readonly policy: PluginRefOrphanPolicy;
4577
+ readonly onSoftDelete: boolean;
4578
+ }
4579
+
4580
+ /** The store surface the enforcement needs. */
4581
+ export declare interface PluginRefStore {
4582
+ query: (descriptor: unknown) => Promise<ReadonlyArray<Record<string, unknown>>>;
4583
+ update: (table: string, id: string, patch: Record<string, unknown>) => Promise<unknown>;
4584
+ delete: (table: string, id: string) => Promise<unknown>;
4585
+ }
4586
+
4477
4587
  /** The 3-arg signature a plugin sees on its bind-ctx. */
4478
4588
  export declare type PluginScheduleCoordinated = (name: string, intervalMs: number, effect: () => void | Promise<void>) => CoordinatedScheduleHandle;
4479
4589
 
@@ -4762,6 +4872,8 @@ export declare interface RebacSubject {
4762
4872
  readonly scopes?: ReadonlyArray<string>;
4763
4873
  }
4764
4874
 
4875
+ export declare const recordCredentialExpiry: (clientId: number, exp: number | undefined) => void;
4876
+
4765
4877
  /** What the framework emit-seams (servePipeline / rpcServer / plugin wrappers)
4766
4878
  * hand to the registry. A flat, transport-agnostic sample shape the
4767
4879
  * recording callbacks can build without touching Effect. */
@@ -5653,6 +5765,14 @@ export declare interface RpcServerOptions<Rpcs extends Rpc.Any> {
5653
5765
  readonly maxRpcBodyBytes?: number;
5654
5766
  }
5655
5767
 
5768
+ /**
5769
+ * Rules that apply to one change, or an empty list.
5770
+ *
5771
+ * Exported separately from the execution so the matching is testable without a
5772
+ * store: which rules fire is the part with the edge cases.
5773
+ */
5774
+ export declare const rulesFor: (rules: ReadonlyArray<PluginRefRule>, change: PluginRefChange) => ReadonlyArray<PluginRefRule>;
5775
+
5656
5776
  /**
5657
5777
  * Run `fn` with `SubjectService` bound to a `system` subject and a
5658
5778
  * system-scoped fluent `ctx.store`. Returns whatever `fn` returns.
@@ -7259,6 +7379,27 @@ export declare class UndoStack<E> {
7259
7379
  };
7260
7380
  }
7261
7381
 
7382
+ /** One `source:` entry that resolves to no declared table. */
7383
+ export declare interface UnresolvedSource {
7384
+ /** The procedure that declares it. */
7385
+ readonly procedure: string;
7386
+ /** The name as written. */
7387
+ readonly source: string;
7388
+ /** A declared table whose name is close — the rename case, usually. */
7389
+ readonly didYouMean: string | undefined;
7390
+ }
7391
+
7392
+ /**
7393
+ * Every `source:` across `procedures` that names no declared table.
7394
+ *
7395
+ * `declared` is the table-name set the boot already built. Procedures with no
7396
+ * `source` are skipped — reactivity is opt-in and its absence is not a defect.
7397
+ */
7398
+ export declare const unresolvedSources: (procedures: ReadonlyArray<{
7399
+ readonly name: string;
7400
+ readonly source: string | ReadonlyArray<string> | undefined;
7401
+ }>, declared: ReadonlySet<string>) => ReadonlyArray<UnresolvedSource>;
7402
+
7262
7403
  export declare class UpdateBuilder {
7263
7404
  private readonly backend;
7264
7405
  private readonly scope;
@@ -7609,6 +7750,17 @@ export declare interface WorkflowFacadeOptions {
7609
7750
  * Inspect HTTP + the `voltro workflows retry` CLI delegate to the same
7610
7751
  * implementation. */
7611
7752
  readonly retry?: (runId: string, options: WorkflowRetryOptions | undefined) => Promise<WorkflowRetryResult>;
7753
+ /** Backs {@link WorkflowsAppContext.redrive}. Supplied by the CLI's facade
7754
+ * builder, which owns the runs/steps store query + the cluster journal
7755
+ * adapter. Inspect HTTP + `voltro workflows redrive` delegate to the same
7756
+ * implementation. */
7757
+ readonly redrive?: (runId: string) => Promise<WorkflowRedriveResult>;
7758
+ /** Fired (fire-and-forget) right after a workflow is ENQUEUED — a start,
7759
+ * child, or run. The CLI wires this to a cross-replica "wake" broadcast so
7760
+ * the replica that OWNS the new run's shard polls immediately instead of
7761
+ * waiting for its next storage tick. No-op when unset (single replica / no
7762
+ * broker → the poll interval covers it). */
7763
+ readonly onEnqueue?: () => void;
7612
7764
  }
7613
7765
 
7614
7766
  export declare interface WorkflowLayerExecutionContext {
@@ -7649,6 +7801,16 @@ export declare class WorkflowPayloadError extends Error {
7649
7801
  missingFields: ReadonlyArray<string>, detail: string);
7650
7802
  }
7651
7803
 
7804
+ /** Result of {@link WorkflowsAppContext.redrive} — whether the failed run's
7805
+ * durable journal was re-driven, how many failed step attempts were reset so
7806
+ * they re-execute, and a `reason` when it declined (no journal / still
7807
+ * running / already succeeded). */
7808
+ export declare interface WorkflowRedriveResult {
7809
+ readonly redriven: boolean;
7810
+ readonly activitiesReset: number;
7811
+ readonly reason: string | null;
7812
+ }
7813
+
7652
7814
  /** Options for {@link WorkflowsAppContext.retry}. */
7653
7815
  export declare interface WorkflowRetryOptions {
7654
7816
  /** Re-run the workflow against this payload instead of the original
@@ -7670,6 +7832,11 @@ export declare interface WorkflowRunListFilter {
7670
7832
  readonly workflowName?: string;
7671
7833
  readonly tag?: string;
7672
7834
  readonly status?: WorkflowRunRecordStatus;
7835
+ /** The DEAD-LETTER view: failed runs an operator has NOT yet discarded
7836
+ * (`status = 'failed' AND discardedAt IS NULL`). Since the framework applies no
7837
+ * retry, a `failed` run is terminal — this is the queue of unhandled failures.
7838
+ * Combines with the other filters (e.g. by `workflowName`). */
7839
+ readonly deadLettered?: boolean;
7673
7840
  readonly limit?: number;
7674
7841
  readonly offset?: number;
7675
7842
  }
@@ -7727,6 +7894,9 @@ export declare interface WorkflowRunSummary<Payload = unknown, Output = unknown>
7727
7894
  readonly durationMs: number | null;
7728
7895
  readonly traceId: string | null;
7729
7896
  readonly parentExecutionId: string | null;
7897
+ /** When set, an operator has acknowledged this (failed) run — it is off the
7898
+ * dead-letter view. Null = unacknowledged. See `WorkflowRunListFilter.deadLettered`. */
7899
+ readonly discardedAt: Date | null;
7730
7900
  }
7731
7901
 
7732
7902
  export declare interface WorkflowsAppContext {
@@ -7752,6 +7922,14 @@ export declare interface WorkflowsAppContext {
7752
7922
  * workflow by tag and re-executes it; pass `payloadOverride` to replay
7753
7923
  * against a corrected input. */
7754
7924
  retry(runId: string, options?: WorkflowRetryOptions): Promise<WorkflowRetryResult>;
7925
+ /** Re-drive a terminally-`failed` run from the step it died on, reusing
7926
+ * its durable journal — the counterpart to {@link retry} (fresh execution,
7927
+ * empty journal) and to {@link resume} (which only re-drives a *suspended*
7928
+ * run). Addressed by run id (`wfrun_…`). Completed steps replay from the
7929
+ * journal; only the failed steps re-execute. Fix the downstream cause
7930
+ * first, then redrive. Refuses a run that is not a not-yet-discarded
7931
+ * failure. */
7932
+ redrive(runId: string): Promise<WorkflowRedriveResult>;
7755
7933
  }
7756
7934
 
7757
7935
  export declare interface WorkflowSignalTarget {