@voltro/protocol 0.23.0 → 0.25.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
@@ -54,6 +54,9 @@ export declare const anonymousSubject: (tenantId: string | null) => Subject;
54
54
  /** A guard entry is either a scope check or a relationship check. */
55
55
  export declare type AnyCheckSpec = GuardCheckSpec | PolicyCheckSpec;
56
56
 
57
+ /** Any declared event, with the generics erased — for registries and audits. */
58
+ export declare type AnyEventDescriptor = EventDescriptor<string, Schema.Schema.Any, Schema.Schema.Any>;
59
+
57
60
  /** A guard is either a scope check or a relationship check. */
58
61
  export declare type AnyGuardSpec<Input = unknown> = GuardSpec<Input> | PolicyGuardSpec<Input>;
59
62
 
@@ -481,6 +484,10 @@ export declare interface ConnectionInfoValue {
481
484
  * lifetime of the underlying WebSocket connection; reused as the key for
482
485
  * the per-connection subject override map. */
483
486
  readonly clientId: number;
487
+ /** The per-CALL `idempotency-key` request header, when the client attached one
488
+ * (every `useMutation` call does). Read by `bindMutation` to dedupe a retried
489
+ * mutation. Absent for callers that don't send it. */
490
+ readonly idempotencyKey?: string;
484
491
  }
485
492
 
486
493
  /** The two credential shapes a connection can hold. `oauth2` = an
@@ -689,6 +696,94 @@ export declare const defineAction: <const Name extends string, Input extends Sch
689
696
  readonly internal?: boolean;
690
697
  }) => ActionProcedureDescriptor<Name, Input, Output, Error>;
691
698
 
699
+ /**
700
+ * Declare an event.
701
+ *
702
+ * ```ts
703
+ * export const gameStarted = defineEvent({
704
+ * name: 'games.started',
705
+ * key: Schema.Struct({ arenaId: Schema.String }),
706
+ * payload: Schema.Struct({ gameId: Schema.String, startedAt: Schema.Number }),
707
+ * guards: [{ scope: 'display:read' }],
708
+ * })
709
+ * ```
710
+ */
711
+ export declare const defineEvent: <const Name extends string, Key extends Schema.Schema.Any, Payload extends Schema.Schema.Any>(options: {
712
+ /**
713
+ * Wire identifier, `camelCase.dotted` like an rpc tag — and it shares the rpc
714
+ * tag COLLISION SPACE, so a duplicate aborts boot rather than resolving itself
715
+ * at the first delivery. Two declarations answering to one name is precisely
716
+ * the failure mode a string channel has; a name that can collide silently
717
+ * would reintroduce it one level up.
718
+ */
719
+ readonly name: Name;
720
+ /**
721
+ * WHERE it goes. Part of the contract, not a filter the client applies:
722
+ * a subscriber receives only events published under a key it asked for, so
723
+ * the server never sends the others at all.
724
+ *
725
+ * ONLY ROUTING FIELDS BELONG HERE. Every field fragments the subscriber set
726
+ * and costs fan-out dedup — a discriminator the handler reads (`gameType`) is
727
+ * payload, an address the delivery is decided by (`arenaId`) is key.
728
+ *
729
+ * The tenant is NOT part of it and must never be added: it is derived from the
730
+ * subject on both sides, so a cross-tenant delivery is impossible by
731
+ * construction rather than by remembering to filter.
732
+ */
733
+ readonly key: Key;
734
+ /** WHAT happened. Decoded when publishing, so a mismatch is a typed error at
735
+ * the PRODUCER instead of a broken handler at every consumer. */
736
+ readonly payload: Payload;
737
+ /**
738
+ * WHO MAY LISTEN — the same vocabulary as a query's guards, so a
739
+ * resource-scoped rule ("may this terminal watch this arena") stays
740
+ * declarative. `ScopeError` is merged into the wire error union automatically.
741
+ *
742
+ * Re-checked when the SUBJECT changes (a revoked role ends the stream), not on
743
+ * every delivery. Per-delivery authorization is the design a comparable
744
+ * product measured into a scaling wall: one check per subscriber per message
745
+ * makes throughput scale with the audience instead of the publish rate.
746
+ */
747
+ readonly guards?: Guards<Schema.Schema.Type<Key>>;
748
+ /**
749
+ * Deliver recently-buffered events on a FIRST attach. Default `false`, and the
750
+ * default is the interesting half.
751
+ *
752
+ * A fresh subscriber wanting only what happens from now on, and a RECONNECTING
753
+ * subscriber wanting the messages it missed, are different requests that read
754
+ * as one contradiction ("never replay history" vs "never lose a message"). They
755
+ * are separated here: a first attach starts empty unless it opts in, while a
756
+ * re-attach always resumes from the last serial the client saw. Mounting a
757
+ * component is not the same event as a WebSocket dropping, and the framework
758
+ * knows which one it is.
759
+ */
760
+ readonly rewind?: boolean;
761
+ /**
762
+ * Deliver this event to subscribed HTTP targets as well.
763
+ *
764
+ * The unification the whole design is for: ONE declaration, and the audiences
765
+ * are consumers of it. Without this an app that both fans an event out to its
766
+ * screens and posts it to a partner declares the thing twice, in two shapes,
767
+ * and the two drift — which is the defect a declaration exists to remove, one
768
+ * level up from the string channel it already removed.
769
+ *
770
+ * Requires `@voltro/plugin-webhooks`. Absent ⇒ no outbound delivery, and no
771
+ * cost.
772
+ */
773
+ readonly webhook?: EventWebhookSpec;
774
+ /**
775
+ * `'each'` (default) — every delivery matters; a slow subscriber loses the
776
+ * oldest and is told how many.
777
+ *
778
+ * `'latest'` — a newer delivery supersedes a pending one; a slow subscriber
779
+ * gets the current value and is told nothing, because nothing was lost.
780
+ *
781
+ * See {@link EventDeliverySemantics}. The test is "would a consumer be wrong
782
+ * to miss one?" — not "is this event frequent?".
783
+ */
784
+ readonly delivery?: EventDeliverySemantics;
785
+ }) => EventDescriptor<Name, Key, Payload>;
786
+
692
787
  export declare const defineMutation: <const Name extends string, Input extends Schema.Schema.Any, Output extends Schema.Schema.Any, Error extends Schema.Schema.All = typeof Schema.Never>(options: {
693
788
  readonly name: Name;
694
789
  readonly input: Input;
@@ -874,6 +969,17 @@ export declare const diffRows: (prev: ReadonlyArray<PatchRow>, next: ReadonlyArr
874
969
  */
875
970
  export declare const effectiveScopes: (subject: Subject) => ReadonlyArray<string>;
876
971
 
972
+ /**
973
+ * Canonical string for a routing key.
974
+ *
975
+ * Object key ORDER must not change the routing: `{a,b}` and `{b,a}` are the same
976
+ * address, and a subscriber that spelled its literal in the other order is the
977
+ * kind of never-matches bug this whole design exists to make impossible. Sorting
978
+ * the entries is what makes the two sides agree without either knowing about the
979
+ * other.
980
+ */
981
+ export declare const encodeEventKey: (key: unknown) => string;
982
+
877
983
  /**
878
984
  * Read the `_tag` off a thrown error value, or `undefined` when it has none.
879
985
  *
@@ -891,6 +997,290 @@ export declare const effectiveScopes: (subject: Subject) => ReadonlyArray<string
891
997
  */
892
998
  export declare const errorTag: (err: unknown) => string | undefined;
893
999
 
1000
+ /**
1001
+ * The full routing address: tenant, event, key.
1002
+ *
1003
+ * The tenant is FIRST and is supplied by the caller of this function from the
1004
+ * SUBJECT — never from anything a client sent. Two apps in two tenants
1005
+ * publishing `games.started` for `arena-1` are addressing different channels,
1006
+ * and no amount of key collision can make them the same one.
1007
+ *
1008
+ * Separated by `EVENT_ROUTE_SEP` — NUL, and NOBODY WRITES IT INLINE. The character is
1009
+ * the right separator (it cannot occur in a tenant id or an event name, so no
1010
+ * pair of parts can spell another pair's address), but a source file containing
1011
+ * a literal one is BINARY to every text tool: `grep` skips it and prints
1012
+ * nothing, which reads exactly like a clean file. This repo has already lost an
1013
+ * audit that way.
1014
+ *
1015
+ * The constant exists because escaping it correctly at each site does NOT work
1016
+ * in practice: writing this module produced three literals in three different
1017
+ * files — including inside the comment warning against them — before the
1018
+ * separator was hoisted. So there is exactly one place the character appears,
1019
+ * and `parseEventRoute` / `formatEventRoute` mean no consumer needs to name it
1020
+ * at all. `eventRouteHygiene` in the test file scans the whole package.
1021
+ */
1022
+ export declare const EVENT_ROUTE_SEP = "\0";
1023
+
1024
+ /** First element of every subscription: the stream is live from here on. */
1025
+ export declare const eventAttached: Schema.Struct<{
1026
+ _tag: Schema.Literal<["attached"]>;
1027
+ }>;
1028
+
1029
+ /**
1030
+ * What it means for a subscriber to fall behind.
1031
+ *
1032
+ * `'each'` — every delivery matters. A subscriber that cannot keep up loses the
1033
+ * OLDEST and is told exactly how many. This is the default because it is the
1034
+ * safe reading: an arena that misses a game-start signal must find out.
1035
+ *
1036
+ * `'latest'` — only the current value matters, and a newer delivery SUPERSEDES a
1037
+ * pending one. A subscriber that falls behind receives the current state on its
1038
+ * next read and is told nothing, because nothing was lost: for a 60Hz stream of
1039
+ * positions, frame 1 stopped being interesting the moment frame 2 existed.
1040
+ *
1041
+ * The distinction is semantic, not a performance knob. Choosing `'latest'` for a
1042
+ * stream where each delivery matters silently drops the ones in between; choosing
1043
+ * `'each'` for a per-frame stream makes a slow client work through a backlog to
1044
+ * reach a state it could have had immediately, and report a "loss" that was
1045
+ * never a loss.
1046
+ *
1047
+ * The honest test: **would a consumer be wrong to miss one?** If the next value
1048
+ * supersedes it, that is `'latest'` — and it is probably state rather than an
1049
+ * event at all.
1050
+ */
1051
+ export declare type EventDeliverySemantics = 'each' | 'latest';
1052
+
1053
+ /**
1054
+ * A declared event: a name, a routing key, a payload, and who may listen.
1055
+ *
1056
+ * Type parameters are inferred from the Schemas, and every call site — `publish`,
1057
+ * `useEvent`, a workflow trigger — derives its types from this one value.
1058
+ */
1059
+ export declare interface EventDescriptor<Name extends string, Key extends Schema.Schema.Any, Payload extends Schema.Schema.Any> {
1060
+ readonly kind: 'event';
1061
+ readonly name: Name;
1062
+ readonly key: Key;
1063
+ readonly payload: Payload;
1064
+ /** Who may LISTEN. Re-checked when the subject changes, not per delivery —
1065
+ * see `EventDefinition.guards` for why that distinction is deliberate. */
1066
+ readonly guards: Guards<Schema.Schema.Type<Key>> | undefined;
1067
+ /** Deliver buffered events on a FIRST attach. Default false. */
1068
+ readonly rewind: boolean | undefined;
1069
+ /** `'each'` (default) or `'latest'` — see the definer. */
1070
+ readonly delivery: EventDeliverySemantics | undefined;
1071
+ /** Opt this event into outbound HTTP delivery. See `EventWebhookSpec`. */
1072
+ readonly webhook: EventWebhookSpec | undefined;
1073
+ }
1074
+
1075
+ /**
1076
+ * One delivery.
1077
+ *
1078
+ * `origin` + `n` are what make a lost message COUNTABLE. `n` is monotonic per
1079
+ * (origin, event, tenant, key), so a subscriber that sees 7 then 9 knows exactly
1080
+ * one delivery is gone — where a dropping buffer, which is what every
1081
+ * `onSlowConsumer: 'drop-oldest'` option in this space actually is, discards in
1082
+ * silence by definition. Silence is the one outcome you cannot build on: an
1083
+ * arena cannot tell "no game started" from "the start signal was dropped".
1084
+ *
1085
+ * It is per ORIGIN rather than global because two instances publishing the same
1086
+ * key have no shared clock and no shared counter. A global sequence would need
1087
+ * one owner per key — an extra hop on every publish and a failover story for
1088
+ * every key — which is a real architecture (a single-threaded actor per room)
1089
+ * and not the one this framework has.
1090
+ */
1091
+ export declare const eventEnvelope: <P extends Schema.Schema.Any>(payload: P) => Schema.Struct<{
1092
+ _tag: Schema.Literal<["event"]>;
1093
+ /** Publishing instance id — serials are only comparable within one. */
1094
+ origin: typeof Schema.String;
1095
+ /** Monotonic per (origin, event, tenant, key). */
1096
+ n: typeof Schema.Number;
1097
+ emittedAt: typeof Schema.Number;
1098
+ payload: P;
1099
+ }>;
1100
+
1101
+ /** Told to the subscriber when the SERVER knows it cannot fill a gap. */
1102
+ export declare const eventGap: Schema.Struct<{
1103
+ _tag: Schema.Literal<["gap"]>;
1104
+ /** How many deliveries are known lost. Never a guess: a shortfall is computed
1105
+ * from the requested serial against what the ring still holds. */
1106
+ missed: typeof Schema.Number;
1107
+ /** `buffer` — evicted before this subscriber could be served.
1108
+ * `resume` — the re-attach asked for a serial older than the ring. */
1109
+ reason: Schema.Literal<["buffer", "resume"]>;
1110
+ }>;
1111
+
1112
+ /** The routing key did not decode against the descriptor's schema. */
1113
+ export declare class EventKeyInvalid extends EventKeyInvalid_base {
1114
+ }
1115
+
1116
+ declare const EventKeyInvalid_base: Schema.TaggedErrorClass<EventKeyInvalid, "EventKeyInvalid", {
1117
+ readonly _tag: Schema.tag<"EventKeyInvalid">;
1118
+ } & {
1119
+ event: typeof Schema.String;
1120
+ message: typeof Schema.String;
1121
+ }>;
1122
+
1123
+ /** The payload did not decode against the descriptor's schema. */
1124
+ export declare class EventPayloadInvalid extends EventPayloadInvalid_base {
1125
+ }
1126
+
1127
+ declare const EventPayloadInvalid_base: Schema.TaggedErrorClass<EventPayloadInvalid, "EventPayloadInvalid", {
1128
+ readonly _tag: Schema.tag<"EventPayloadInvalid">;
1129
+ } & {
1130
+ event: typeof Schema.String;
1131
+ message: typeof Schema.String;
1132
+ }>;
1133
+
1134
+ /** The encoded envelope exceeds `MAX_EVENT_ENVELOPE_BYTES`. */
1135
+ export declare class EventPayloadTooLarge extends EventPayloadTooLarge_base {
1136
+ }
1137
+
1138
+ declare const EventPayloadTooLarge_base: Schema.TaggedErrorClass<EventPayloadTooLarge, "EventPayloadTooLarge", {
1139
+ readonly _tag: Schema.tag<"EventPayloadTooLarge">;
1140
+ } & {
1141
+ event: typeof Schema.String;
1142
+ bytes: typeof Schema.Number;
1143
+ limit: typeof Schema.Number;
1144
+ }>;
1145
+
1146
+ export declare type EventResumePoint = {
1147
+ readonly origin: string;
1148
+ readonly n: number;
1149
+ };
1150
+
1151
+ /** Where a re-attaching subscriber left off, per origin. */
1152
+ export declare const eventResumePoint: Schema.Struct<{
1153
+ origin: typeof Schema.String;
1154
+ n: typeof Schema.Number;
1155
+ }>;
1156
+
1157
+ export declare const eventRoute: (tenantId: string | null, event: string, key: unknown) => string;
1158
+
1159
+ export declare type EventStreamEvent<P> = {
1160
+ readonly _tag: 'attached';
1161
+ } | {
1162
+ readonly _tag: 'event';
1163
+ readonly origin: string;
1164
+ readonly n: number;
1165
+ readonly emittedAt: number;
1166
+ readonly payload: P;
1167
+ } | {
1168
+ readonly _tag: 'gap';
1169
+ readonly missed: number;
1170
+ readonly reason: 'buffer' | 'resume';
1171
+ };
1172
+
1173
+ /**
1174
+ * What a subscription emits. `attached` first, then `event`s, with `gap`
1175
+ * interleaved whenever the server can prove a loss.
1176
+ */
1177
+ export declare const eventStreamEvent: <P extends Schema.Schema.Any>(payload: P) => Schema.Union<[Schema.Struct<{
1178
+ _tag: Schema.Literal<["attached"]>;
1179
+ }>, Schema.Struct<{
1180
+ _tag: Schema.Literal<["event"]>;
1181
+ /** Publishing instance id — serials are only comparable within one. */
1182
+ origin: typeof Schema.String;
1183
+ /** Monotonic per (origin, event, tenant, key). */
1184
+ n: typeof Schema.Number;
1185
+ emittedAt: typeof Schema.Number;
1186
+ payload: P;
1187
+ }>, Schema.Struct<{
1188
+ _tag: Schema.Literal<["gap"]>;
1189
+ /** How many deliveries are known lost. Never a guess: a shortfall is computed
1190
+ * from the requested serial against what the ring still holds. */
1191
+ missed: typeof Schema.Number;
1192
+ /** `buffer` — evicted before this subscriber could be served.
1193
+ * `resume` — the re-attach asked for a serial older than the ring. */
1194
+ reason: Schema.Literal<["buffer", "resume"]>;
1195
+ }>]>;
1196
+
1197
+ /**
1198
+ * The subscription payload: the key, plus where to resume.
1199
+ *
1200
+ * `resume` present ⇒ this is a RE-attach and the client is owed continuity;
1201
+ * absent ⇒ a first attach, which gets nothing older than itself unless the
1202
+ * descriptor opted into `rewind`. The distinction lives on the wire because only
1203
+ * the client knows whether it has seen this stream before — the server cannot
1204
+ * tell a reconnect from a fresh mount.
1205
+ */
1206
+ export declare const eventSubscribeInput: <K extends Schema.Schema.Any>(key: K) => Schema.Struct<{
1207
+ key: K;
1208
+ resume: Schema.optional<Schema.Array$<Schema.Struct<{
1209
+ origin: typeof Schema.String;
1210
+ n: typeof Schema.Number;
1211
+ }>>>;
1212
+ }>;
1213
+
1214
+ /**
1215
+ * Lift an event descriptor into its rpc.
1216
+ *
1217
+ * A `stream: true` rpc, like a query — the framework's push transport is the
1218
+ * WebSocket a subscription already holds open, and reusing it is why `useEvent`
1219
+ * shares one connection lifecycle, one reconnect policy and one devtools view
1220
+ * with `useSubscription` instead of being a second realtime concept in the same
1221
+ * app.
1222
+ */
1223
+ export declare const eventToRpc: <Name extends string, Key extends Schema.Schema.Any, Payload extends Schema.Schema.Any>(descriptor: EventDescriptor<Name, Key, Payload>, extraErrors?: ReadonlyArray<Schema.Schema.All>) => Rpc.Rpc<Name, Schema.Struct<{
1224
+ key: Key;
1225
+ resume: Schema.optional<Schema.Array$<Schema.Struct<{
1226
+ origin: typeof Schema.String;
1227
+ n: typeof Schema.Number;
1228
+ }>>>;
1229
+ }>, Stream<Schema.Union<[Schema.Struct<{
1230
+ _tag: Schema.Literal<["attached"]>;
1231
+ }>, Schema.Struct<{
1232
+ _tag: Schema.Literal<["event"]>;
1233
+ /** Publishing instance id — serials are only comparable within one. */
1234
+ origin: typeof Schema.String;
1235
+ /** Monotonic per (origin, event, tenant, key). */
1236
+ n: typeof Schema.Number;
1237
+ emittedAt: typeof Schema.Number;
1238
+ payload: Payload;
1239
+ }>, Schema.Struct<{
1240
+ _tag: Schema.Literal<["gap"]>;
1241
+ /** How many deliveries are known lost. Never a guess: a shortfall is computed
1242
+ * from the requested serial against what the ring still holds. */
1243
+ missed: typeof Schema.Number;
1244
+ /** `buffer` — evicted before this subscriber could be served.
1245
+ * `resume` — the re-attach asked for a serial older than the ring. */
1246
+ reason: Schema.Literal<["buffer", "resume"]>;
1247
+ }>]>, Schema.Schema.Any | Schema.Schema<never, never, unknown>>, typeof Schema.Never, never>;
1248
+
1249
+ /**
1250
+ * The WEBHOOK audience of a declared event.
1251
+ *
1252
+ * Present ⇒ `@voltro/plugin-webhooks` treats this event as an outbound one:
1253
+ * subscribers can register HTTP targets for it and every publish is delivered to
1254
+ * them, signed and retried, from the SAME call that fans it out to clients.
1255
+ *
1256
+ * Structurally typed rather than importing the plugin's own types, and that is a
1257
+ * dependency direction rather than a preference: `@voltro/protocol` is
1258
+ * browser-safe and must not reach a plugin. The plugin reads this block and maps
1259
+ * it onto its own knobs — the shapes are the plugin's to define, so a value here
1260
+ * is a plain number or string, never one of its enums.
1261
+ *
1262
+ * Namespaced under `webhook:` rather than spread across the descriptor because
1263
+ * these settings are meaningless to the other three audiences. A `retry` at the
1264
+ * top level would read as if it applied to client delivery, which is
1265
+ * at-most-once by design and has no retry at all.
1266
+ */
1267
+ export declare interface EventWebhookSpec {
1268
+ /** Human-readable summary for the dashboard's event list. */
1269
+ readonly description?: string;
1270
+ /** Payload schema version. Bump when subscribers must adapt. Default 1. */
1271
+ readonly version?: number;
1272
+ /** Default retry policy for new subscriptions (`{ attempts, backoffMs }`). */
1273
+ readonly retry?: {
1274
+ readonly attempts?: number;
1275
+ readonly backoffMs?: number;
1276
+ };
1277
+ /** Shared ceiling across ALL deliveries of this event — the runaway-emit
1278
+ * guard. Over-limit deliveries are deferred, never dropped. */
1279
+ readonly rateLimit?: {
1280
+ readonly perMinute: number;
1281
+ };
1282
+ }
1283
+
894
1284
  /** Normalize the `exposeAsTool` shorthand. `true` is only valid when the
895
1285
  * descriptor carries a top-level `description`; callers pass that in. */
896
1286
  export declare type ExposeAsTool = boolean | ExposeAsToolSpec;
@@ -948,6 +1338,9 @@ export declare const findAdvisoryResourceGuards: (procedures: ReadonlyArray<{
948
1338
  /** Record a completed response for replay. */
949
1339
  export declare const finishIdempotent: (store: IdempotencyStore, scope: string, key: string, response: IdempotencyResponse, now: number) => Promise<void>;
950
1340
 
1341
+ /** A route rendered for humans — logs, the inspect surface, the dashboard. */
1342
+ export declare const formatEventRoute: (route: string) => string;
1343
+
951
1344
  /** The currently-registered policy-guard resolver, or `undefined`. */
952
1345
  export declare const getPolicyGuardResolver: () => PolicyGuardResolver | undefined;
953
1346
 
@@ -1129,6 +1522,8 @@ export declare interface InsertTarget<Input = unknown, Row = unknown, Item = Rec
1129
1522
  readonly shapeItem?: ((input: Input, optimisticId: string) => Item) | undefined;
1130
1523
  }
1131
1524
 
1525
+ export declare const isEventDescriptor: (value: unknown) => value is AnyEventDescriptor;
1526
+
1132
1527
  /** True when every row in the set carries an `id`. The dispatcher uses
1133
1528
  * this to decide patch-vs-full-snapshot: a result whose rows aren't
1134
1529
  * id-keyed (rare — a custom projection that drops the id) can't be
@@ -1161,6 +1556,23 @@ export declare const isWireReachable: (descriptor: {
1161
1556
  readonly internal?: boolean | undefined;
1162
1557
  }) => boolean;
1163
1558
 
1559
+ /**
1560
+ * How large one encoded envelope may be, on EVERY dialect.
1561
+ *
1562
+ * Postgres `NOTIFY` dies past 8000 bytes and the other transports have no such
1563
+ * bound. Enforcing the limit only where the wire imposes it would make "switch
1564
+ * the broadcast provider" a silent behaviour change — an app developed against
1565
+ * Redis would start failing when it moved to the pg-native path, at the worst
1566
+ * possible moment. So the smallest transport's ceiling is the framework's
1567
+ * ceiling, minus room for the envelope around the payload.
1568
+ *
1569
+ * An oversized payload is also a design smell in its own right: an event says
1570
+ * that something happened, so `photo.added` carries a photo REFERENCE, and the
1571
+ * consumer fetches the photo through a route that can stream, cache and
1572
+ * authorize it.
1573
+ */
1574
+ export declare const MAX_EVENT_ENVELOPE_BYTES = 7500;
1575
+
1164
1576
  /** In-memory store — the default for single-process dev + the test double. */
1165
1577
  export declare const memoryIdempotencyStore: () => IdempotencyStore;
1166
1578
 
@@ -1231,6 +1643,13 @@ export declare interface ObservabilityContribution {
1231
1643
  readonly sampler?: unknown;
1232
1644
  }
1233
1645
 
1646
+ /** Split a route back into its three parts. */
1647
+ export declare const parseEventRoute: (route: string) => {
1648
+ readonly tenantId: string | null;
1649
+ readonly event: string;
1650
+ readonly key: string;
1651
+ };
1652
+
1234
1653
  /** A single row in a subscription result. Must carry an `id`; everything
1235
1654
  * else is opaque to the patch layer. */
1236
1655
  export declare type PatchRow = Readonly<Record<string, unknown>> & {
@@ -1266,6 +1685,41 @@ export declare type PluginActivateHook = (ctx: PluginLifecycleContext) => Effect
1266
1685
  * touches them.
1267
1686
  */
1268
1687
  export declare interface PluginBindContext {
1688
+ /**
1689
+ * This process's replica identity — the same value the event bus stamps as
1690
+ * its publish `origin` and the membership registry announces under.
1691
+ *
1692
+ * ONE id across all three on purpose: a plugin holding state per replica
1693
+ * (presence is the case) must be able to correlate "who owns this" with "is
1694
+ * that one still alive", and three identities for one process would make the
1695
+ * correlation quietly wrong rather than obviously broken.
1696
+ */
1697
+ readonly instanceId?: string;
1698
+ /**
1699
+ * Live instance membership — subscribe to learn when a replica joins, leaves
1700
+ * or restarts.
1701
+ *
1702
+ * Present only when the host provides one. A plugin holding per-replica state
1703
+ * needs it and cannot derive it: pub/sub delivers messages, it does not report
1704
+ * who is on the channel, and an instance that dies simply goes quiet.
1705
+ */
1706
+ readonly membership?: {
1707
+ readonly onChange: (listener: (event: {
1708
+ readonly kind: 'joined' | 'left' | 'restarted';
1709
+ readonly instanceId: string;
1710
+ }) => void) => () => void;
1711
+ };
1712
+ /**
1713
+ * The app's cross-replica broadcast transport, when one is configured.
1714
+ *
1715
+ * Absent ⇒ single instance, which is a complete answer rather than a
1716
+ * degraded one. A plugin must not branch on "do we have a cluster" — that
1717
+ * branch is how a feature comes to work in dev and differ in production.
1718
+ */
1719
+ readonly broadcast?: {
1720
+ readonly publish: (channel: string, payload: string) => unknown;
1721
+ readonly subscribe: (channel: string, handler: (payload: string) => void) => unknown;
1722
+ };
1269
1723
  /**
1270
1724
  * The framework's already-open `SqlClient` (from `@effect/sql`) — the
1271
1725
  * SAME pool the app's store uses. A plugin runs raw SQL through this
@@ -3434,4 +3888,13 @@ export declare const WorkflowUpdateResultSchema: Schema.Struct<{
3434
3888
  result: typeof Schema.Unknown;
3435
3889
  }>;
3436
3890
 
3891
+ /**
3892
+ * Scope for a WS-rpc MUTATION. Unlike the REST scope (tenant + method + path),
3893
+ * this includes the acting SUBJECT id: a WS app is often single-tenant with many
3894
+ * users, and a key from subject A must never replay for subject B. The tag is the
3895
+ * effective rpc tag (e.g. `notes.create`), so the same key on two different
3896
+ * mutations stays independent.
3897
+ */
3898
+ export declare const wsMutationIdempotencyScope: (tenantId: string | null | undefined, subjectId: string | null | undefined, mutationTag: string) => string;
3899
+
3437
3900
  export { }