@voltro/protocol 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
@@ -696,6 +696,10 @@ export declare interface ClientTarget {
696
696
  * `path` is set. 2nd arg is the optimistic id (insert) or the current item
697
697
  * (update) — `unknown` here since the erased view spans both; normalize casts. */
698
698
  readonly shapeItem?: ((input: Record<string, unknown>, currentOrOptimisticId: unknown) => Record<string, unknown>) | undefined;
699
+ /** Declared junction relations (see {@link TargetRelations}). Pure DATA —
700
+ * three strings per relation — so it crosses to the browser unchanged and
701
+ * drives junction auto-optimistic there. */
702
+ readonly relations?: TargetRelations | undefined;
699
703
  }
700
704
 
701
705
  export declare const composeAuthStrategies: (strategies: ReadonlyArray<AuthStrategy>, options?: {
@@ -2655,6 +2659,18 @@ export declare type PendingApproval = Schema.Schema.Type<typeof PendingApproval>
2655
2659
  */
2656
2660
  export declare type PluginActivateHook = (ctx: PluginLifecycleContext) => Effect.Effect<void, unknown> | Promise<void> | void;
2657
2661
 
2662
+ /**
2663
+ * Derive the short alias used as the rpc-tag prefix for a plugin's routes.
2664
+ * Pure — depends only on the plugin name.
2665
+ *
2666
+ * Examples:
2667
+ * '@voltro/plugin-audit' → 'audit'
2668
+ * '@scope/plugin-rateLimit' → 'rateLimit'
2669
+ * 'plain-name' → 'plainName'
2670
+ * '@voltro/audit' → 'audit' (no plugin- prefix)
2671
+ */
2672
+ export declare const pluginAlias: (pluginName: string) => string;
2673
+
2658
2674
  /**
2659
2675
  * Server-only context delivered to `bindDataStore`, ALONGSIDE the store,
2660
2676
  * AFTER the framework has opened its store + pool. Gives a plugin the
@@ -3257,11 +3273,18 @@ export declare type PluginInstallHook = (ctx: PluginLifecycleContext) => Effect.
3257
3273
  *
3258
3274
  * **What an alias costs, stated because it is not obvious and nothing else
3259
3275
  * says it:** the local and cloud dashboards fetch a plugin's inspect panel at
3260
- * `/_voltro/inspect/plugins/<slug>/…` with the DEFAULT slug compiled in. Alias
3261
- * a plugin that ships `inspectEndpoints` and the endpoints keep working, the
3262
- * rpc tags move as intended, and the dashboard panel 404s because the panel
3263
- * is in a different repository and cannot follow. Alias to dodge a tag
3264
- * collision; do not alias a plugin whose dashboard panel you use.
3276
+ * `/_voltro/inspect/plugins/<slug>/…` with the DEFAULT slug compiled in, and
3277
+ * they are in different repositories, so they cannot follow an alias. That
3278
+ * used to mean an aliased plugin kept working while its panel 404'd. It no
3279
+ * longer does: `makePluginInspectRegistry` also mounts each plugin's inspect
3280
+ * endpoints under its CANONICAL slug, so the compiled-in path stays correct
3281
+ * however the app renames the plugin.
3282
+ *
3283
+ * The residue is `instance`, not `alias`. Two installs are two panels with one
3284
+ * canonical name, so they get NO shared mount — showing either under the
3285
+ * canonical path would hand a dashboard the other install's rows under a name
3286
+ * that looks right. Each is reachable at its own slug, which
3287
+ * `/_voltro/inspect/plugins` reports as `inspectSlug`.
3265
3288
  *
3266
3289
  * @param base the plugin's canonical package name, e.g. `'@voltro/plugin-cdc-out'`
3267
3290
  * @param alias replaces the whole namespace — an app-chosen name
@@ -3508,6 +3531,55 @@ export declare interface PluginSchemaContribution {
3508
3531
  readonly migrations?: ReadonlyArray<PluginMigration>;
3509
3532
  }
3510
3533
 
3534
+ /**
3535
+ * Derive the URL slug for a plugin's HTTP-facing surfaces (the inspect mount
3536
+ * `/_voltro/inspect/plugins/<slug>/…`). Unlike `pluginAlias` (an identifier
3537
+ * for rpc tags / codegen, camelCase), the slug stays KEBAB-CASE — the shape
3538
+ * every dashboard fetches (`plugins/cdc-out/sinks`, never `plugins/cdcOut/…`)
3539
+ * and the shape the package name already carries. Instance-suffixed names
3540
+ * (`@voltro/plugin-cdc-out#analytics`) map `#` → `--` so the slug stays a
3541
+ * fetchable URL path segment.
3542
+ *
3543
+ * Examples:
3544
+ * '@voltro/plugin-audit' → 'audit'
3545
+ * '@voltro/plugin-cdc-out' → 'cdc-out'
3546
+ * '@scope/plugin-rateLimit' → 'rate-limit'
3547
+ * '@voltro/plugin-cdc-out#analytics'→ 'cdc-out--analytics'
3548
+ */
3549
+ export declare const pluginSlug: (pluginName: string) => string;
3550
+
3551
+ /**
3552
+ * The wire tag a plugin's own client code must call for one of its routes.
3553
+ *
3554
+ * `pluginTag('@voltro/plugin-notifications', 'inbox')` is `'notifications.inbox'`
3555
+ * in a default app and `'inbox.inbox'` in an app that installed the plugin as
3556
+ * `notificationsPlugin({ alias: 'inbox' })`. Every hook a plugin ships goes
3557
+ * through this instead of spelling its own namespace, because the namespace is
3558
+ * the app's to choose and the hook is the half of the move that a literal
3559
+ * string cannot follow.
3560
+ *
3561
+ * **Resolution, and the one case that fails closed.** A base may have several
3562
+ * registered aliases when an app installs the same plugin twice
3563
+ * (`instance: 'analytics'` → `notifications#analytics`):
3564
+ *
3565
+ * - exactly one registered alias → that one;
3566
+ * - several, exactly one of which carries no `#` → the un-suffixed install.
3567
+ * Two installs of a plugin have a primary and a secondary, and a hook with
3568
+ * no way to name an install means the primary;
3569
+ * - several, none or more than one un-suffixed → **throws**. The hook is being
3570
+ * asked which of two servers it addresses and has no basis to pick; a guess
3571
+ * here writes to the wrong install's tables, which is worse than a stack
3572
+ * trace naming both candidates. Call the tag explicitly (`useSubscription(api,
3573
+ * 'notifications#analytics.inbox')`) to say which you mean.
3574
+ * - none registered → the canonical `pluginAlias(base)`. This is the path a
3575
+ * unit test rendering a hook in isolation takes, and the path an app takes
3576
+ * before its generated rpcGroup has loaded. It is NOT a fail-open guess: an
3577
+ * app that aliased and somehow lost its registration calls a tag the client
3578
+ * group does not contain, and the transport refuses it by name rather than
3579
+ * routing it somewhere plausible.
3580
+ */
3581
+ export declare const pluginTag: (baseName: string, route: string) => string;
3582
+
3511
3583
  /**
3512
3584
  * A scaffold template a plugin contributes to `voltro init` /
3513
3585
  * `voltro add-app`. Templates are listed under the plugin's name in
@@ -3887,10 +3959,29 @@ export declare type ReactivitySource = TableName | ReactivityChannel;
3887
3959
  */
3888
3960
  export declare type ReactivitySourceValue = string | ReactivityChannel;
3889
3961
 
3962
+ /**
3963
+ * Declare, for this realm, what namespace each installed plugin actually
3964
+ * answers to. Emitted into `rpcGroup.generated.ts` by the codegen — one call
3965
+ * per api, mapping each plugin's CANONICAL base name to its EFFECTIVE alias.
3966
+ *
3967
+ * Emitted unconditionally whenever an app installs a plugin that contributes
3968
+ * client routes, not only when something was aliased. A registration that only
3969
+ * appeared in the aliased case would be a second code path exercised by nobody,
3970
+ * and the default case is where a regression would hide.
3971
+ *
3972
+ * Accumulates rather than overwrites: two apis in one browser bundle each
3973
+ * register their own map, and an app may install one plugin twice (`instance`).
3974
+ * The disambiguation happens at READ time — see `pluginTag`.
3975
+ */
3976
+ export declare const registerPluginAliases: (aliases: Readonly<Record<string, string>>) => void;
3977
+
3890
3978
  /** Gate a handler on a scope; fails with a typed `ScopeError` if missing.
3891
3979
  * Checks the EFFECTIVE set so role-derived scopes count. */
3892
3980
  export declare const requireScope: (subject: Subject, scope: string) => Effect.Effect<void, ScopeError>;
3893
3981
 
3982
+ /** Drop every registration. Tests only — production registers once at module load. */
3983
+ export declare const resetPluginAliases: () => void;
3984
+
3894
3985
  /** The question the runtime asks a registered resource-scope resolver. */
3895
3986
  export declare interface ResourceScopeRequest {
3896
3987
  /** The authenticated caller. */
@@ -4704,21 +4795,52 @@ declare const TableValidationFailed_base: Schema.TaggedErrorClass<TableValidatio
4704
4795
 
4705
4796
  export declare type Target<Input = unknown, Row = unknown> = TargetSpec<Input, Row> | ReadonlyArray<TargetSpec<Input, Row>>;
4706
4797
 
4798
+ /**
4799
+ * ONE declared many-to-many relation of a write target: the junction table
4800
+ * plus its two reference columns.
4801
+ *
4802
+ * **Why the columns are declared and not derived.** The server can derive
4803
+ * them (`store.relationLinks` reads the junction's `reference()` targets),
4804
+ * and it still does — but the same declaration drives JUNCTION AUTO-OPTIMISTIC
4805
+ * in the BROWSER, and the browser has no table registry to derive from
4806
+ * (`@voltro/database` is server-only by construction). Guessing a column name
4807
+ * from the table name is the failure mode this codebase refuses everywhere
4808
+ * else, so the pair is stated. It is not taken on trust: before it writes, the
4809
+ * server compares the declaration against the junction's real reference
4810
+ * columns and refuses — naming the right pair — if they disagree. A wrong
4811
+ * declaration is a boot-loud error, never a silent client/server divergence.
4812
+ */
4813
+ export declare interface TargetRelation {
4814
+ /** The junction (link) table. */
4815
+ readonly junction: string;
4816
+ /** The junction's reference column pointing at the TARGET's own table. */
4817
+ readonly anchorColumn: string;
4818
+ /** The junction's other reference column — the linked row's id. */
4819
+ readonly targetColumn: string;
4820
+ }
4821
+
4707
4822
  /**
4708
4823
  * Declared many-to-many RELATIONS of a write target: `{ inputField:
4709
- * junctionTable }`. After the executor succeeds — inside the SAME
4824
+ * TargetRelation }`. After the executor succeeds — inside the SAME
4710
4825
  * transaction — the framework reconciles the junction's links for the
4711
4826
  * written row against `input[inputField]` (an array of target ids) via the
4712
4827
  * diff-based `store.relationLinks`, so a form's multi-reference field saves
4713
- * in one mutation with no hand-written junction code. The anchor column is
4714
- * derived from the junction's `reference()` targets (a self-junction is
4715
- * refused, never guessed).
4828
+ * in one mutation with no hand-written junction code.
4829
+ *
4830
+ * The SAME declaration drives the client's junction auto-optimistic: the
4831
+ * cache reconciles the junction rows of every subscription sourced on
4832
+ * `junction` the moment the mutation is sent, so a multi-reference field
4833
+ * flips as instantly as a scalar one instead of waiting for the server delta.
4834
+ * Those patches ride the ordinary optimistic lane (staged under the mutation
4835
+ * id): reverted on failure, kept on success until the base actually moves.
4716
4836
  *
4717
4837
  * An ABSENT input field leaves the links untouched (absent ≠ empty — an
4718
4838
  * empty array is the explicit "clear them all"). The row id is the
4719
- * executor's `output.id`, falling back to `input.id`.
4839
+ * executor's `output.id`, falling back to `input.id`; the client, which
4840
+ * cannot see `output` yet, uses `input.id` (and, for an insert, the same
4841
+ * optimistic id it stamped on the new row).
4720
4842
  */
4721
- export declare type TargetRelations = Readonly<Record<string, string>>;
4843
+ export declare type TargetRelations = Readonly<Record<string, TargetRelation>>;
4722
4844
 
4723
4845
  export declare type TargetSpec<Input = unknown, Row = unknown> = InsertTarget<Input, Row> | UpdateTarget<Input, Row> | DeleteTarget<Input>;
4724
4846
 
@@ -5068,8 +5190,10 @@ export declare interface VoltroPlugin {
5068
5190
  * declarations across seven plugins), so the escape hatch fires on all of
5069
5191
  * them and the alias moves NOTHING on the rpc surface. A user aliasing
5070
5192
  * `notifications` to escape a collision with their own `notifications.*`
5071
- * routes would still collide — and would additionally lose the dashboard
5072
- * panel, which fetches the default slug. Worse than not shipping the field.
5193
+ * routes would still collide — and, at that time, would additionally have
5194
+ * lost the dashboard panel, which fetches the default slug. Worse than not
5195
+ * shipping the field. (The panel half is fixed since: see the canonical
5196
+ * mount in `makePluginInspectRegistry`.)
5073
5197
  *
5074
5198
  * With `baseName` set, both tag derivations strip a leading
5075
5199
  * `<default-alias>.` before applying the EFFECTIVE alias, so
@@ -5497,6 +5621,34 @@ export declare const wireErrorUnion: (descriptor: {
5497
5621
  readonly requiresApproval?: AnyApprovalPolicy | undefined;
5498
5622
  }, kind: "query" | "mutation" | "action" | "stream") => Schema.Schema.All;
5499
5623
 
5624
+ /**
5625
+ * Re-tag a procedure descriptor for the wire.
5626
+ *
5627
+ * Every lifter below is `Rpc.make(descriptor.name, …)`, so the tag a procedure
5628
+ * answers to on the wire is the one baked into the DESCRIPTOR — not the one the
5629
+ * caller thinks it is emitting. That is fine for app-authored procedures, whose
5630
+ * descriptor and tag have one source. It was wrong for PLUGIN routes: an app
5631
+ * that installs a plugin under an `alias` moves the namespace, the server
5632
+ * registers the moved tag, and the generated client lifted the plugin's
5633
+ * descriptor unchanged — so the browser sent the tag the plugin AUTHORED to a
5634
+ * server that had stopped serving it. The identifier moved, the descriptor map
5635
+ * key moved, the type key moved; the only string the transport reads did not.
5636
+ *
5637
+ * The codegen wraps every plugin-contributed descriptor with this, always —
5638
+ * including the un-aliased case, where it is an identity in effect. A wrap that
5639
+ * only appeared under an alias would be a branch nothing exercises, and the
5640
+ * un-aliased case is the one a regression would hide in.
5641
+ *
5642
+ * Returns a COPY. The descriptor is imported by reference from the plugin's
5643
+ * browser-safe module and may be lifted by several apps in one process; mutating
5644
+ * `name` in place would re-tag it for all of them.
5645
+ */
5646
+ export declare const withRpcTag: <D extends {
5647
+ readonly name: string;
5648
+ }, const T extends string>(descriptor: D, tag: T) => Omit<D, "name"> & {
5649
+ readonly name: T;
5650
+ };
5651
+
5500
5652
  export declare const workflowCancelDescriptor: ActionProcedureDescriptor<"__voltro.workflow.cancel", Schema.Struct<{
5501
5653
  workflowName: typeof Schema.String;
5502
5654
  executionId: typeof Schema.String;