@voltro/protocol 0.24.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/CHANGELOG.md +426 -0
- package/dist/index.d.ts +450 -0
- package/dist/index.js +342 -275
- package/package.json +2 -2
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
|
|
|
@@ -693,6 +696,94 @@ export declare const defineAction: <const Name extends string, Input extends Sch
|
|
|
693
696
|
readonly internal?: boolean;
|
|
694
697
|
}) => ActionProcedureDescriptor<Name, Input, Output, Error>;
|
|
695
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
|
+
|
|
696
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: {
|
|
697
788
|
readonly name: Name;
|
|
698
789
|
readonly input: Input;
|
|
@@ -878,6 +969,17 @@ export declare const diffRows: (prev: ReadonlyArray<PatchRow>, next: ReadonlyArr
|
|
|
878
969
|
*/
|
|
879
970
|
export declare const effectiveScopes: (subject: Subject) => ReadonlyArray<string>;
|
|
880
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
|
+
|
|
881
983
|
/**
|
|
882
984
|
* Read the `_tag` off a thrown error value, or `undefined` when it has none.
|
|
883
985
|
*
|
|
@@ -895,6 +997,290 @@ export declare const effectiveScopes: (subject: Subject) => ReadonlyArray<string
|
|
|
895
997
|
*/
|
|
896
998
|
export declare const errorTag: (err: unknown) => string | undefined;
|
|
897
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
|
+
|
|
898
1284
|
/** Normalize the `exposeAsTool` shorthand. `true` is only valid when the
|
|
899
1285
|
* descriptor carries a top-level `description`; callers pass that in. */
|
|
900
1286
|
export declare type ExposeAsTool = boolean | ExposeAsToolSpec;
|
|
@@ -952,6 +1338,9 @@ export declare const findAdvisoryResourceGuards: (procedures: ReadonlyArray<{
|
|
|
952
1338
|
/** Record a completed response for replay. */
|
|
953
1339
|
export declare const finishIdempotent: (store: IdempotencyStore, scope: string, key: string, response: IdempotencyResponse, now: number) => Promise<void>;
|
|
954
1340
|
|
|
1341
|
+
/** A route rendered for humans — logs, the inspect surface, the dashboard. */
|
|
1342
|
+
export declare const formatEventRoute: (route: string) => string;
|
|
1343
|
+
|
|
955
1344
|
/** The currently-registered policy-guard resolver, or `undefined`. */
|
|
956
1345
|
export declare const getPolicyGuardResolver: () => PolicyGuardResolver | undefined;
|
|
957
1346
|
|
|
@@ -1133,6 +1522,8 @@ export declare interface InsertTarget<Input = unknown, Row = unknown, Item = Rec
|
|
|
1133
1522
|
readonly shapeItem?: ((input: Input, optimisticId: string) => Item) | undefined;
|
|
1134
1523
|
}
|
|
1135
1524
|
|
|
1525
|
+
export declare const isEventDescriptor: (value: unknown) => value is AnyEventDescriptor;
|
|
1526
|
+
|
|
1136
1527
|
/** True when every row in the set carries an `id`. The dispatcher uses
|
|
1137
1528
|
* this to decide patch-vs-full-snapshot: a result whose rows aren't
|
|
1138
1529
|
* id-keyed (rare — a custom projection that drops the id) can't be
|
|
@@ -1165,6 +1556,23 @@ export declare const isWireReachable: (descriptor: {
|
|
|
1165
1556
|
readonly internal?: boolean | undefined;
|
|
1166
1557
|
}) => boolean;
|
|
1167
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
|
+
|
|
1168
1576
|
/** In-memory store — the default for single-process dev + the test double. */
|
|
1169
1577
|
export declare const memoryIdempotencyStore: () => IdempotencyStore;
|
|
1170
1578
|
|
|
@@ -1235,6 +1643,13 @@ export declare interface ObservabilityContribution {
|
|
|
1235
1643
|
readonly sampler?: unknown;
|
|
1236
1644
|
}
|
|
1237
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
|
+
|
|
1238
1653
|
/** A single row in a subscription result. Must carry an `id`; everything
|
|
1239
1654
|
* else is opaque to the patch layer. */
|
|
1240
1655
|
export declare type PatchRow = Readonly<Record<string, unknown>> & {
|
|
@@ -1270,6 +1685,41 @@ export declare type PluginActivateHook = (ctx: PluginLifecycleContext) => Effect
|
|
|
1270
1685
|
* touches them.
|
|
1271
1686
|
*/
|
|
1272
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
|
+
};
|
|
1273
1723
|
/**
|
|
1274
1724
|
* The framework's already-open `SqlClient` (from `@effect/sql`) — the
|
|
1275
1725
|
* SAME pool the app's store uses. A plugin runs raw SQL through this
|