stitchkit 0.44.1 → 0.45.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.
@@ -21,6 +21,8 @@
21
21
  * unsubscribe), so it plugs straight into `createCacheBridge`.
22
22
  */
23
23
  import type { EventsMap } from '@socket.io/component-emitter';
24
+ import type { RealtimeContract, RealtimeEventRegistry, RealtimeRejectedEventHook } from '../realtime/contract';
25
+ import { type ValidatedRealtimeSocket } from '../realtime/socket';
24
26
  /**
25
27
  * Socket.IO event map — `{ event: (...args) => void }`. Aliases Socket.IO's
26
28
  * own `EventsMap` constraint, which intentionally maps to `any`: that is what
@@ -113,5 +115,17 @@ export interface SocketIOClient<TServerEvents extends SocketEventMap, TClientEve
113
115
  */
114
116
  onConnectionChange(listener: (connected: boolean, reason?: string) => void): () => void;
115
117
  }
118
+ export interface RealtimeClient<TServerToClient extends RealtimeEventRegistry, TClientToServer extends RealtimeEventRegistry> extends ValidatedRealtimeSocket<TServerToClient, TClientToServer> {
119
+ connect(): void;
120
+ disconnect(): void;
121
+ readonly connected: boolean;
122
+ onConnectionChange(listener: (connected: boolean, reason?: string) => void): () => void;
123
+ }
124
+ export interface RealtimeClientOptions<TServerToClient extends RealtimeEventRegistry> extends SocketIOClientConfig<{
125
+ [TEvent in keyof TServerToClient]: (...args: unknown[]) => void;
126
+ }> {
127
+ onRejected?: RealtimeRejectedEventHook;
128
+ }
129
+ export declare function createRealtimeClient<const TServerToClient extends RealtimeEventRegistry, const TClientToServer extends RealtimeEventRegistry>(contract: RealtimeContract<TServerToClient, TClientToServer>, { onRejected, ...config }: RealtimeClientOptions<TServerToClient>): RealtimeClient<TServerToClient, TClientToServer>;
116
130
  export declare function createSocketIOClient<TServerEvents extends SocketEventMap, TClientEvents extends SocketEventMap>(config: SocketIOClientConfig<TServerEvents>): SocketIOClient<TServerEvents, TClientEvents>;
117
131
  //# sourceMappingURL=socket-io.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"socket-io.d.ts","sourceRoot":"","sources":["../../src/browser/socket-io.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AA0D9D;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,SAAS,CAAC;AAEvC,MAAM,WAAW,oBAAoB,CAAC,aAAa,SAAS,cAAc,GAAG,cAAc;IACzF,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAC;IACZ,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;OAMG;IACH,IAAI,CAAC,EACD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACvB,CAAC,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACvE,kFAAkF;IAClF,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IAClD;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,2EAA2E;IAC3E,UAAU,CAAC,EAAE,KAAK,CAAC,WAAW,GAAG,SAAS,CAAC,CAAC;IAC5C,iDAAiD;IACjD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,uDAAuD;IACvD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mDAAmD;IACnD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;;;;;OAcG;IACH,2BAA2B,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC7C;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,KAAK,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,CAAC;CAC9C;AAED,MAAM,WAAW,cAAc,CAC7B,aAAa,SAAS,cAAc,EACpC,aAAa,SAAS,cAAc;IAEpC,6DAA6D;IAC7D,OAAO,IAAI,IAAI,CAAC;IAChB,+CAA+C;IAC/C,UAAU,IAAI,IAAI,CAAC;IACnB,qDAAqD;IACrD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;OAIG;IACH,EAAE,CAAC,CAAC,SAAS,MAAM,aAAa,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,CAAC;IAC5F,8DAA8D;IAC9D,IAAI,CAAC,CAAC,SAAS,MAAM,aAAa,GAAG,MAAM,EACzC,KAAK,EAAE,CAAC,EACR,GAAG,IAAI,EAAE,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,GACpC,IAAI,CAAC;IACR;;;;;;OAMG;IACH,kBAAkB,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACzF;AAED,wBAAgB,oBAAoB,CAClC,aAAa,SAAS,cAAc,EACpC,aAAa,SAAS,cAAc,EACpC,MAAM,EAAE,oBAAoB,CAAC,aAAa,CAAC,GAAG,cAAc,CAAC,aAAa,EAAE,aAAa,CAAC,CA8J3F"}
1
+ {"version":3,"file":"socket-io.d.ts","sourceRoot":"","sources":["../../src/browser/socket-io.ts"],"names":[],"mappings":"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"}
@@ -1056,6 +1056,176 @@ function createImplement() {
1056
1056
  return (contract, handlers) => implement(contract, handlers);
1057
1057
  }
1058
1058
 
1059
+ // src/realtime/socket.ts
1060
+ import { z } from "zod";
1061
+ var AcknowledgementCallbackSchema = z.function();
1062
+ function method(target, name) {
1063
+ const value = Reflect.get(target, name);
1064
+ if (typeof value !== "function") {
1065
+ throw new Error(`Realtime target does not implement ${name}()`);
1066
+ }
1067
+ return (...args) => Reflect.apply(value, target, args);
1068
+ }
1069
+ function splitWireArguments(definition, args) {
1070
+ if (!definition.ack)
1071
+ return { values: args };
1072
+ const acknowledgement = args.at(-1);
1073
+ if (typeof acknowledgement !== "function")
1074
+ return { values: args };
1075
+ return {
1076
+ values: args.slice(0, -1),
1077
+ acknowledgement: (...callbackArgs) => Reflect.apply(acknowledgement, undefined, callbackArgs)
1078
+ };
1079
+ }
1080
+ function eventDefinition(registry, event) {
1081
+ const definition = registry[event];
1082
+ if (!definition)
1083
+ throw new Error(`Unknown realtime event "${event}"`);
1084
+ return definition;
1085
+ }
1086
+ function reportRejected(rejected, hook) {
1087
+ if (!hook) {
1088
+ console.error(`[stitchkit] rejected realtime ${rejected.direction} event "${rejected.event}" (${rejected.phase})`, rejected.error);
1089
+ return;
1090
+ }
1091
+ Promise.resolve(hook(rejected)).catch((error) => {
1092
+ console.error("[stitchkit] realtime rejection hook failed", error);
1093
+ });
1094
+ }
1095
+ function createValidatedRealtimeSocket({
1096
+ target,
1097
+ inbound,
1098
+ outbound,
1099
+ inboundDirection,
1100
+ outboundDirection,
1101
+ onRejected,
1102
+ subscribe
1103
+ }) {
1104
+ const onTarget = method(target, "on");
1105
+ const offTarget = subscribe ? undefined : method(target, "off");
1106
+ const emitTarget = method(target, "emit");
1107
+ return {
1108
+ on: (event, handler) => {
1109
+ const definition = eventDefinition(inbound, event);
1110
+ const wrapped = (...wireArgs) => {
1111
+ const { values, acknowledgement } = splitWireArguments(definition, wireArgs);
1112
+ const parsed = definition.args.safeParse(values);
1113
+ if (!parsed.success) {
1114
+ reportRejected({
1115
+ event,
1116
+ direction: inboundDirection,
1117
+ phase: "arguments",
1118
+ error: parsed.error
1119
+ }, onRejected);
1120
+ return;
1121
+ }
1122
+ if (definition.ack && !acknowledgement) {
1123
+ const callback = AcknowledgementCallbackSchema.safeParse(undefined);
1124
+ if (!callback.success) {
1125
+ reportRejected({
1126
+ event,
1127
+ direction: inboundDirection,
1128
+ phase: "acknowledgement",
1129
+ error: callback.error
1130
+ }, onRejected);
1131
+ }
1132
+ return;
1133
+ }
1134
+ const applicationArgs = [...parsed.data];
1135
+ if (definition.ack) {
1136
+ applicationArgs.push((value) => {
1137
+ const ack = definition.ack?.safeParse(value);
1138
+ if (!ack?.success) {
1139
+ if (ack) {
1140
+ reportRejected({
1141
+ event,
1142
+ direction: outboundDirection,
1143
+ phase: "acknowledgement",
1144
+ error: ack.error
1145
+ }, onRejected);
1146
+ }
1147
+ return;
1148
+ }
1149
+ acknowledgement?.(ack.data);
1150
+ });
1151
+ }
1152
+ Reflect.apply(handler, undefined, applicationArgs);
1153
+ };
1154
+ if (subscribe)
1155
+ return subscribe(event, wrapped);
1156
+ onTarget(event, wrapped);
1157
+ return () => {
1158
+ offTarget?.(event, wrapped);
1159
+ };
1160
+ },
1161
+ emit: (event, ...args) => {
1162
+ const definition = eventDefinition(outbound, event);
1163
+ const { values, acknowledgement } = splitWireArguments(definition, args);
1164
+ const parsed = definition.args.parse(values);
1165
+ if (definition.ack && !acknowledgement) {
1166
+ AcknowledgementCallbackSchema.parse(undefined);
1167
+ }
1168
+ const wireArgs = [...parsed];
1169
+ if (definition.ack && acknowledgement) {
1170
+ wireArgs.push((value) => {
1171
+ const ack = definition.ack?.safeParse(value);
1172
+ if (!ack?.success) {
1173
+ if (ack) {
1174
+ reportRejected({
1175
+ event,
1176
+ direction: inboundDirection,
1177
+ phase: "acknowledgement",
1178
+ error: ack.error
1179
+ }, onRejected);
1180
+ }
1181
+ return;
1182
+ }
1183
+ acknowledgement(ack.data);
1184
+ });
1185
+ }
1186
+ emitTarget(event, ...wireArgs);
1187
+ }
1188
+ };
1189
+ }
1190
+
1191
+ // src/server/realtime.ts
1192
+ function bindRealtimeServer(contract, handle, options = {}) {
1193
+ const outbound = (target) => createValidatedRealtimeSocket({
1194
+ target,
1195
+ inbound: {},
1196
+ outbound: contract.serverToClient,
1197
+ inboundDirection: "server-inbound",
1198
+ outboundDirection: "server-outbound",
1199
+ onRejected: options.onRejected
1200
+ });
1201
+ const broadcast = outbound(handle.io);
1202
+ return {
1203
+ onConnection: (handler) => {
1204
+ const listener = (raw) => {
1205
+ const events = createValidatedRealtimeSocket({
1206
+ target: raw,
1207
+ inbound: contract.clientToServer,
1208
+ outbound: contract.serverToClient,
1209
+ inboundDirection: "server-inbound",
1210
+ outboundDirection: "server-outbound",
1211
+ onRejected: options.onRejected
1212
+ });
1213
+ handler({
1214
+ raw,
1215
+ events,
1216
+ to: (room) => outbound(raw.to(room))
1217
+ });
1218
+ };
1219
+ handle.io.on("connection", listener);
1220
+ return () => {
1221
+ handle.io.off("connection", listener);
1222
+ };
1223
+ },
1224
+ emit: broadcast.emit,
1225
+ to: (room) => outbound(handle.io.to(room))
1226
+ };
1227
+ }
1228
+
1059
1229
  // src/server/websocket.ts
1060
1230
  function webSocketLane(lane) {
1061
1231
  const { match, handlers } = lane;
@@ -1202,4 +1372,4 @@ function socketIoLane(websocket) {
1202
1372
  });
1203
1373
  }
1204
1374
 
1205
- export { parseMultipart, mimeForPath, staticRoute, createHandler, implement, createImplement, webSocketLane, composeWebSocketHandlers, createSocketIOServer, socketIoLane };
1375
+ export { parseMultipart, mimeForPath, staticRoute, createHandler, implement, createImplement, bindRealtimeServer, webSocketLane, composeWebSocketHandlers, createSocketIOServer, socketIoLane };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  export { type ClientConfig, type ClientContract, type ClientRegistryValue, type ContractClientConfig, contractEndpointMatchers, createClient, createClients, createScopedClients, createScopedUrlBuilders, createUrlBuilder, createUrlBuilders, type PathPrefixArgs, type RegistryScope, type ScopeClientConfigs, type ScopedClientRegistry, type ScopedUrlBuilderRegistry, type UrlBuilderConfig, } from './browser/client';
2
2
  export { ApiError, type ApiEvent, type ApiEventListener, type ConfiguredHttpClient, createHttpClient, type HeaderProvider, type HttpClient, type HttpClientConfig, type RequestOptions, type UnauthorizedMatcher, } from './browser/http';
3
- export type { SocketEventMap, SocketIOClient, SocketIOClientConfig, } from './browser/socket-io';
4
- export { createSocketIOClient } from './browser/socket-io';
3
+ export type { RealtimeClient, RealtimeClientOptions, SocketEventMap, SocketIOClient, SocketIOClientConfig, } from './browser/socket-io';
4
+ export { createRealtimeClient, createSocketIOClient } from './browser/socket-io';
5
5
  export { type ParseSSEOptions, parseSSE } from './browser/stream';
6
6
  export * from './contract';
7
7
  export { childSpan, createTraceContext, formatTraceparent, parseTraceparent, type TraceContext, } from './observability/trace';
8
+ export * from './realtime';
8
9
  export { createRetainedTopics, type RetainedTopics } from './retained';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,wBAAwB,EACxB,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,cAAc,EACd,cAAc,EACd,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAClE,cAAc,YAAY,CAAC;AAG3B,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,YAAY,GAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,wBAAwB,EACxB,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAClE,cAAc,YAAY,CAAC;AAG3B,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,YAAY,GAClB,MAAM,uBAAuB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -646,6 +646,138 @@ function createUrlBuilders(contracts, source, contractConfig) {
646
646
  function createScopedUrlBuilders(contracts, source, scopeConfigs) {
647
647
  return buildScopedRegistry(contracts, scopeConfigs, "URL builder", (contract, config) => createUrlBuilder(contract, source, config));
648
648
  }
649
+ // src/realtime/socket.ts
650
+ import { z } from "zod";
651
+ var AcknowledgementCallbackSchema = z.function();
652
+ function method(target, name) {
653
+ const value = Reflect.get(target, name);
654
+ if (typeof value !== "function") {
655
+ throw new Error(`Realtime target does not implement ${name}()`);
656
+ }
657
+ return (...args) => Reflect.apply(value, target, args);
658
+ }
659
+ function splitWireArguments(definition, args) {
660
+ if (!definition.ack)
661
+ return { values: args };
662
+ const acknowledgement = args.at(-1);
663
+ if (typeof acknowledgement !== "function")
664
+ return { values: args };
665
+ return {
666
+ values: args.slice(0, -1),
667
+ acknowledgement: (...callbackArgs) => Reflect.apply(acknowledgement, undefined, callbackArgs)
668
+ };
669
+ }
670
+ function eventDefinition(registry, event) {
671
+ const definition = registry[event];
672
+ if (!definition)
673
+ throw new Error(`Unknown realtime event "${event}"`);
674
+ return definition;
675
+ }
676
+ function reportRejected(rejected, hook) {
677
+ if (!hook) {
678
+ console.error(`[stitchkit] rejected realtime ${rejected.direction} event "${rejected.event}" (${rejected.phase})`, rejected.error);
679
+ return;
680
+ }
681
+ Promise.resolve(hook(rejected)).catch((error) => {
682
+ console.error("[stitchkit] realtime rejection hook failed", error);
683
+ });
684
+ }
685
+ function createValidatedRealtimeSocket({
686
+ target,
687
+ inbound,
688
+ outbound,
689
+ inboundDirection,
690
+ outboundDirection,
691
+ onRejected,
692
+ subscribe
693
+ }) {
694
+ const onTarget = method(target, "on");
695
+ const offTarget = subscribe ? undefined : method(target, "off");
696
+ const emitTarget = method(target, "emit");
697
+ return {
698
+ on: (event, handler) => {
699
+ const definition = eventDefinition(inbound, event);
700
+ const wrapped = (...wireArgs) => {
701
+ const { values, acknowledgement } = splitWireArguments(definition, wireArgs);
702
+ const parsed = definition.args.safeParse(values);
703
+ if (!parsed.success) {
704
+ reportRejected({
705
+ event,
706
+ direction: inboundDirection,
707
+ phase: "arguments",
708
+ error: parsed.error
709
+ }, onRejected);
710
+ return;
711
+ }
712
+ if (definition.ack && !acknowledgement) {
713
+ const callback = AcknowledgementCallbackSchema.safeParse(undefined);
714
+ if (!callback.success) {
715
+ reportRejected({
716
+ event,
717
+ direction: inboundDirection,
718
+ phase: "acknowledgement",
719
+ error: callback.error
720
+ }, onRejected);
721
+ }
722
+ return;
723
+ }
724
+ const applicationArgs = [...parsed.data];
725
+ if (definition.ack) {
726
+ applicationArgs.push((value) => {
727
+ const ack = definition.ack?.safeParse(value);
728
+ if (!ack?.success) {
729
+ if (ack) {
730
+ reportRejected({
731
+ event,
732
+ direction: outboundDirection,
733
+ phase: "acknowledgement",
734
+ error: ack.error
735
+ }, onRejected);
736
+ }
737
+ return;
738
+ }
739
+ acknowledgement?.(ack.data);
740
+ });
741
+ }
742
+ Reflect.apply(handler, undefined, applicationArgs);
743
+ };
744
+ if (subscribe)
745
+ return subscribe(event, wrapped);
746
+ onTarget(event, wrapped);
747
+ return () => {
748
+ offTarget?.(event, wrapped);
749
+ };
750
+ },
751
+ emit: (event, ...args) => {
752
+ const definition = eventDefinition(outbound, event);
753
+ const { values, acknowledgement } = splitWireArguments(definition, args);
754
+ const parsed = definition.args.parse(values);
755
+ if (definition.ack && !acknowledgement) {
756
+ AcknowledgementCallbackSchema.parse(undefined);
757
+ }
758
+ const wireArgs = [...parsed];
759
+ if (definition.ack && acknowledgement) {
760
+ wireArgs.push((value) => {
761
+ const ack = definition.ack?.safeParse(value);
762
+ if (!ack?.success) {
763
+ if (ack) {
764
+ reportRejected({
765
+ event,
766
+ direction: inboundDirection,
767
+ phase: "acknowledgement",
768
+ error: ack.error
769
+ }, onRejected);
770
+ }
771
+ return;
772
+ }
773
+ acknowledgement(ack.data);
774
+ });
775
+ }
776
+ emitTarget(event, ...wireArgs);
777
+ }
778
+ };
779
+ }
780
+
649
781
  // src/retained.ts
650
782
  function createRetainedTopics() {
651
783
  const last = {};
@@ -691,6 +823,39 @@ function toIoAuth(auth) {
691
823
  Promise.resolve().then(auth).then(cb, () => cb({}));
692
824
  };
693
825
  }
826
+ function createRealtimeClient(contract, { onRejected, ...config }) {
827
+ const transport = createSocketIOClient(config);
828
+ const events = createValidatedRealtimeSocket({
829
+ target: transport,
830
+ inbound: contract.serverToClient,
831
+ outbound: contract.clientToServer,
832
+ inboundDirection: "client-inbound",
833
+ outboundDirection: "client-outbound",
834
+ onRejected,
835
+ subscribe: (event, handler) => {
836
+ const subscribe = Reflect.get(transport, "on");
837
+ if (typeof subscribe !== "function") {
838
+ throw new Error("Socket.IO client does not implement on()");
839
+ }
840
+ const unsubscribe = Reflect.apply(subscribe, transport, [event, handler]);
841
+ if (typeof unsubscribe !== "function") {
842
+ throw new Error("Socket.IO client on() did not return an unsubscribe function");
843
+ }
844
+ return () => {
845
+ Reflect.apply(unsubscribe, undefined, []);
846
+ };
847
+ }
848
+ });
849
+ return {
850
+ ...events,
851
+ connect: transport.connect,
852
+ disconnect: transport.disconnect,
853
+ get connected() {
854
+ return transport.connected;
855
+ },
856
+ onConnectionChange: transport.onConnectionChange
857
+ };
858
+ }
694
859
  function createSocketIOClient(config) {
695
860
  let socket = null;
696
861
  let desiredConnected = false;
@@ -802,7 +967,7 @@ function createSocketIOClient(config) {
802
967
  };
803
968
  }
804
969
  // src/internal/errors.ts
805
- import { z } from "zod";
970
+ import { z as z2 } from "zod";
806
971
 
807
972
  // src/server/stream.ts
808
973
  async function* parseSSE(response, options) {
@@ -838,6 +1003,10 @@ async function* parseSSE(response, options) {
838
1003
  reader.releaseLock();
839
1004
  }
840
1005
  }
1006
+ // src/realtime/contract.ts
1007
+ function defineRealtimeContract(contract) {
1008
+ return contract;
1009
+ }
841
1010
  export {
842
1011
  unauthorized,
843
1012
  rateLimited,
@@ -849,6 +1018,7 @@ export {
849
1018
  formatTraceparent,
850
1019
  forbidden,
851
1020
  encodeCursor,
1021
+ defineRealtimeContract,
852
1022
  defineErrors,
853
1023
  defineContract,
854
1024
  decodeCursor,
@@ -859,6 +1029,7 @@ export {
859
1029
  createScopedUrlBuilders,
860
1030
  createScopedClients,
861
1031
  createRetainedTopics,
1032
+ createRealtimeClient,
862
1033
  createHttpClient,
863
1034
  createContractFactory,
864
1035
  createClients,
package/dist/node.d.ts CHANGED
@@ -9,6 +9,7 @@ export { createHandler } from './server/create';
9
9
  export { createImplement, implement } from './server/implement';
10
10
  export type { LogFormat } from './server/logger';
11
11
  export { type NodeServerConfig, type NodeServerHandle, serveNode } from './server/node';
12
+ export { bindRealtimeServer, type RealtimeServer, type RealtimeServerConnection, type RealtimeServerHandle, } from './server/realtime';
12
13
  export { createNodeSocketIOServer as createSocketIOServer, type NodeSocketIOServerHandle as SocketIOServerHandle, type SocketIOServerConfig, } from './server/socket-io-node';
13
14
  export type { FetchComposition, FetchHandler, HandlerConfig, LoggingConfig, LogOutcome, RawRoute, RawRouteContext, ServiceDef, } from './server/types';
14
15
  //# sourceMappingURL=node.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,YAAY,GACb,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAChE,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACxF,OAAO,EACL,wBAAwB,IAAI,oBAAoB,EAChD,KAAK,wBAAwB,IAAI,oBAAoB,EACrD,KAAK,oBAAoB,GAC1B,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,aAAa,EACb,UAAU,EACV,QAAQ,EACR,eAAe,EACf,UAAU,GACX,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,YAAY,GACb,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAChE,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACxF,OAAO,EACL,kBAAkB,EAClB,KAAK,cAAc,EACnB,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,GAC1B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,wBAAwB,IAAI,oBAAoB,EAChD,KAAK,wBAAwB,IAAI,oBAAoB,EACrD,KAAK,oBAAoB,GAC1B,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,aAAa,EACb,UAAU,EACV,QAAQ,EACR,eAAe,EACf,UAAU,GACX,MAAM,gBAAgB,CAAC"}
package/dist/node.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import {
2
+ bindRealtimeServer,
2
3
  createHandler,
3
4
  createImplement,
4
5
  createSocketIOServer,
5
6
  implement
6
- } from "./index-4fyn9f09.js";
7
+ } from "./index-w0mec79h.js";
7
8
  import"./index-6jypn22c.js";
8
9
  import"./index-x3fcszf8.js";
9
10
  import {
@@ -55,6 +56,7 @@ export {
55
56
  createImplement,
56
57
  createHandler,
57
58
  conflict,
59
+ bindRealtimeServer,
58
60
  badRequest,
59
61
  appError,
60
62
  AppError
@@ -0,0 +1,36 @@
1
+ import type { z } from 'zod';
2
+ export interface RealtimeEventDefinition<TArgs extends z.ZodType<unknown[]> = z.ZodType<unknown[]>, TAck extends z.ZodType | undefined = z.ZodType | undefined> {
3
+ args: TArgs;
4
+ ack?: TAck;
5
+ }
6
+ export type RealtimeEventRegistry = Record<string, RealtimeEventDefinition>;
7
+ export interface RealtimeContract<TServerToClient extends RealtimeEventRegistry, TClientToServer extends RealtimeEventRegistry> {
8
+ serverToClient: TServerToClient;
9
+ clientToServer: TClientToServer;
10
+ }
11
+ export declare function defineRealtimeContract<const TServerToClient extends RealtimeEventRegistry, const TClientToServer extends RealtimeEventRegistry>(contract: RealtimeContract<TServerToClient, TClientToServer>): RealtimeContract<TServerToClient, TClientToServer>;
12
+ export type RealtimeEventArguments<TDefinition extends RealtimeEventDefinition> = [
13
+ ...z.output<TDefinition['args']>,
14
+ ...(TDefinition extends {
15
+ ack: infer TAck extends z.ZodType;
16
+ } ? [(value: z.input<TAck>) => void] : [])
17
+ ];
18
+ export type RealtimeEmitArguments<TDefinition extends RealtimeEventDefinition> = [
19
+ ...(z.input<TDefinition['args']> extends unknown[] ? z.input<TDefinition['args']> : never),
20
+ ...(TDefinition extends {
21
+ ack: infer TAck extends z.ZodType;
22
+ } ? [(value: z.output<TAck>) => void] : [])
23
+ ];
24
+ export type RealtimeEventHandler<TDefinition extends RealtimeEventDefinition> = (...args: RealtimeEventArguments<TDefinition>) => void;
25
+ export type InferRealtimeEventMap<TRegistry extends RealtimeEventRegistry> = {
26
+ [TEvent in keyof TRegistry]: RealtimeEventHandler<TRegistry[TEvent]>;
27
+ };
28
+ export type RealtimeRejectDirection = 'client-inbound' | 'client-outbound' | 'server-inbound' | 'server-outbound';
29
+ export interface RealtimeRejectedEvent {
30
+ event: string;
31
+ direction: RealtimeRejectDirection;
32
+ phase: 'arguments' | 'acknowledgement';
33
+ error: z.ZodError;
34
+ }
35
+ export type RealtimeRejectedEventHook = (rejected: RealtimeRejectedEvent) => void | Promise<void>;
36
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../src/realtime/contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,MAAM,WAAW,uBAAuB,CACtC,KAAK,SAAS,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,EACzD,IAAI,SAAS,CAAC,CAAC,OAAO,GAAG,SAAS,GAAG,CAAC,CAAC,OAAO,GAAG,SAAS;IAE1D,IAAI,EAAE,KAAK,CAAC;IACZ,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAED,MAAM,MAAM,qBAAqB,GAAG,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC;AAE5E,MAAM,WAAW,gBAAgB,CAC/B,eAAe,SAAS,qBAAqB,EAC7C,eAAe,SAAS,qBAAqB;IAE7C,cAAc,EAAE,eAAe,CAAC;IAChC,cAAc,EAAE,eAAe,CAAC;CACjC;AAED,wBAAgB,sBAAsB,CACpC,KAAK,CAAC,eAAe,SAAS,qBAAqB,EACnD,KAAK,CAAC,eAAe,SAAS,qBAAqB,EACnD,QAAQ,EAAE,gBAAgB,CAAC,eAAe,EAAE,eAAe,CAAC,sDAE7D;AAED,MAAM,MAAM,sBAAsB,CAAC,WAAW,SAAS,uBAAuB,IAAI;IAChF,GAAG,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAChC,GAAG,CAAC,WAAW,SAAS;QAAE,GAAG,EAAE,MAAM,IAAI,SAAS,CAAC,CAAC,OAAO,CAAA;KAAE,GACzD,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,GAChC,EAAE,CAAC;CACR,CAAC;AAEF,MAAM,MAAM,qBAAqB,CAAC,WAAW,SAAS,uBAAuB,IAAI;IAC/E,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,SAAS,OAAO,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC;IAC1F,GAAG,CAAC,WAAW,SAAS;QAAE,GAAG,EAAE,MAAM,IAAI,SAAS,CAAC,CAAC,OAAO,CAAA;KAAE,GACzD,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,GACjC,EAAE,CAAC;CACR,CAAC;AAEF,MAAM,MAAM,oBAAoB,CAAC,WAAW,SAAS,uBAAuB,IAAI,CAC9E,GAAG,IAAI,EAAE,sBAAsB,CAAC,WAAW,CAAC,KACzC,IAAI,CAAC;AAEV,MAAM,MAAM,qBAAqB,CAAC,SAAS,SAAS,qBAAqB,IAAI;KAC1E,MAAM,IAAI,MAAM,SAAS,GAAG,oBAAoB,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;CACrE,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAC/B,gBAAgB,GAChB,iBAAiB,GACjB,gBAAgB,GAChB,iBAAiB,CAAC;AAEtB,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,uBAAuB,CAAC;IACnC,KAAK,EAAE,WAAW,GAAG,iBAAiB,CAAC;IACvC,KAAK,EAAE,CAAC,CAAC,QAAQ,CAAC;CACnB;AAED,MAAM,MAAM,yBAAyB,GAAG,CACtC,QAAQ,EAAE,qBAAqB,KAC5B,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { defineRealtimeContract, type InferRealtimeEventMap, type RealtimeContract, type RealtimeEmitArguments, type RealtimeEventArguments, type RealtimeEventDefinition, type RealtimeEventHandler, type RealtimeEventRegistry, type RealtimeRejectDirection, type RealtimeRejectedEvent, type RealtimeRejectedEventHook, } from './contract';
2
+ export type { ValidatedRealtimeSocket } from './socket';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/realtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,KAAK,yBAAyB,GAC/B,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { RealtimeEmitArguments, RealtimeEventHandler, RealtimeEventRegistry, RealtimeRejectDirection, RealtimeRejectedEventHook } from './contract';
2
+ interface ValidatedRealtimeSocketOptions<TInbound extends RealtimeEventRegistry, TOutbound extends RealtimeEventRegistry> {
3
+ target: object;
4
+ inbound: TInbound;
5
+ outbound: TOutbound;
6
+ inboundDirection: RealtimeRejectDirection;
7
+ outboundDirection: RealtimeRejectDirection;
8
+ onRejected?: RealtimeRejectedEventHook;
9
+ subscribe?: (event: string, handler: (...args: unknown[]) => void) => () => void;
10
+ }
11
+ export interface ValidatedRealtimeSocket<TInbound extends RealtimeEventRegistry, TOutbound extends RealtimeEventRegistry> {
12
+ on<TEvent extends keyof TInbound & string>(event: TEvent, handler: RealtimeEventHandler<TInbound[TEvent]>): () => void;
13
+ emit<TEvent extends keyof TOutbound & string>(event: TEvent, ...args: RealtimeEmitArguments<TOutbound[TEvent]>): void;
14
+ }
15
+ export declare function createValidatedRealtimeSocket<const TInbound extends RealtimeEventRegistry, const TOutbound extends RealtimeEventRegistry>({ target, inbound, outbound, inboundDirection, outboundDirection, onRejected, subscribe, }: ValidatedRealtimeSocketOptions<TInbound, TOutbound>): ValidatedRealtimeSocket<TInbound, TOutbound>;
16
+ export {};
17
+ //# sourceMappingURL=socket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../../src/realtime/socket.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,qBAAqB,EAErB,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EAEvB,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAEpB,UAAU,8BAA8B,CACtC,QAAQ,SAAS,qBAAqB,EACtC,SAAS,SAAS,qBAAqB;IAEvC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,QAAQ,CAAC;IAClB,QAAQ,EAAE,SAAS,CAAC;IACpB,gBAAgB,EAAE,uBAAuB,CAAC;IAC1C,iBAAiB,EAAE,uBAAuB,CAAC;IAC3C,UAAU,CAAC,EAAE,yBAAyB,CAAC;IACvC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,uBAAuB,CACtC,QAAQ,SAAS,qBAAqB,EACtC,SAAS,SAAS,qBAAqB;IAEvC,EAAE,CAAC,MAAM,SAAS,MAAM,QAAQ,GAAG,MAAM,EACvC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,oBAAoB,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,GAC9C,MAAM,IAAI,CAAC;IACd,IAAI,CAAC,MAAM,SAAS,MAAM,SAAS,GAAG,MAAM,EAC1C,KAAK,EAAE,MAAM,EACb,GAAG,IAAI,EAAE,qBAAqB,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,GAChD,IAAI,CAAC;CACT;AAmDD,wBAAgB,6BAA6B,CAC3C,KAAK,CAAC,QAAQ,SAAS,qBAAqB,EAC5C,KAAK,CAAC,SAAS,SAAS,qBAAqB,EAC7C,EACA,MAAM,EACN,OAAO,EACP,QAAQ,EACR,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,EACV,SAAS,GACV,EAAE,8BAA8B,CAAC,QAAQ,EAAE,SAAS,CAAC,GAAG,uBAAuB,CAC9E,QAAQ,EACR,SAAS,CACV,CAkGA"}
@@ -17,6 +17,7 @@ export { type MultipartResult, parseMultipart } from './multipart';
17
17
  export { generateOpenApiDocument, type OpenApiConfig, type OpenApiDocument, type OpenApiInfo, type OpenApiServer, openApiRoute, } from './openapi';
18
18
  export { createRateLimiter, type RateLimitConfig } from './rate-limit';
19
19
  export { errorResponse, parseBody, respondJson } from './raw';
20
+ export { bindRealtimeServer, type RealtimeServer, type RealtimeServerConnection, type RealtimeServerHandle, } from './realtime';
20
21
  export { type ClientIpOptions, extractIp, generateTraceId, getClientInfo, resolveSocketIp, resolveTraceId, } from './request';
21
22
  export { staticRoute } from './router';
22
23
  export type { SocketIOServerConfig, SocketIOServerHandle } from './socket-io';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import {
2
+ bindRealtimeServer,
2
3
  composeWebSocketHandlers,
3
4
  createHandler,
4
5
  createImplement,
@@ -9,7 +10,7 @@ import {
9
10
  socketIoLane,
10
11
  staticRoute,
11
12
  webSocketLane
12
- } from "../index-4fyn9f09.js";
13
+ } from "../index-w0mec79h.js";
13
14
  import {
14
15
  createAuthHook,
15
16
  createBearerResolver,
@@ -718,6 +719,7 @@ export {
718
719
  conflict,
719
720
  composeWebSocketHandlers,
720
721
  cacheHeaders,
722
+ bindRealtimeServer,
721
723
  badRequest,
722
724
  appError,
723
725
  STITCH_ERROR_STATUS,
@@ -0,0 +1,22 @@
1
+ import type { Socket, Server as SocketIOServer } from 'socket.io';
2
+ import type { SocketEventMap } from '../browser/socket-io';
3
+ import type { RealtimeContract, RealtimeEventRegistry, RealtimeRejectedEventHook } from '../realtime/contract';
4
+ import { type ValidatedRealtimeSocket } from '../realtime/socket';
5
+ export interface RealtimeServerConnection<TServerToClient extends RealtimeEventRegistry, TClientToServer extends RealtimeEventRegistry> {
6
+ /** Raw Socket.IO socket for handshake auth, rooms and application-owned delivery policy. */
7
+ raw: Socket;
8
+ events: ValidatedRealtimeSocket<TClientToServer, TServerToClient>;
9
+ to(room: string): Pick<ValidatedRealtimeSocket<RealtimeEventRegistry, TServerToClient>, 'emit'>;
10
+ }
11
+ export interface RealtimeServer<TServerToClient extends RealtimeEventRegistry, TClientToServer extends RealtimeEventRegistry> {
12
+ onConnection(handler: (connection: RealtimeServerConnection<TServerToClient, TClientToServer>) => void | Promise<void>): () => void;
13
+ emit: ValidatedRealtimeSocket<RealtimeEventRegistry, TServerToClient>['emit'];
14
+ to(room: string): Pick<ValidatedRealtimeSocket<RealtimeEventRegistry, TServerToClient>, 'emit'>;
15
+ }
16
+ export interface RealtimeServerHandle {
17
+ io: SocketIOServer<SocketEventMap, SocketEventMap>;
18
+ }
19
+ export declare function bindRealtimeServer<const TServerToClient extends RealtimeEventRegistry, const TClientToServer extends RealtimeEventRegistry>(contract: RealtimeContract<TServerToClient, TClientToServer>, handle: RealtimeServerHandle, options?: {
20
+ onRejected?: RealtimeRejectedEventHook;
21
+ }): RealtimeServer<TServerToClient, TClientToServer>;
22
+ //# sourceMappingURL=realtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime.d.ts","sourceRoot":"","sources":["../../src/server/realtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,IAAI,cAAc,EAAE,MAAM,WAAW,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EACV,gBAAgB,EAChB,qBAAqB,EACrB,yBAAyB,EAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,wBAAwB,CACvC,eAAe,SAAS,qBAAqB,EAC7C,eAAe,SAAS,qBAAqB;IAE7C,4FAA4F;IAC5F,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,uBAAuB,CAAC,eAAe,EAAE,eAAe,CAAC,CAAC;IAClE,EAAE,CACA,IAAI,EAAE,MAAM,GACX,IAAI,CAAC,uBAAuB,CAAC,qBAAqB,EAAE,eAAe,CAAC,EAAE,MAAM,CAAC,CAAC;CAClF;AAED,MAAM,WAAW,cAAc,CAC7B,eAAe,SAAS,qBAAqB,EAC7C,eAAe,SAAS,qBAAqB;IAE7C,YAAY,CACV,OAAO,EAAE,CACP,UAAU,EAAE,wBAAwB,CAAC,eAAe,EAAE,eAAe,CAAC,KACnE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GACxB,MAAM,IAAI,CAAC;IACd,IAAI,EAAE,uBAAuB,CAAC,qBAAqB,EAAE,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;IAC9E,EAAE,CACA,IAAI,EAAE,MAAM,GACX,IAAI,CAAC,uBAAuB,CAAC,qBAAqB,EAAE,eAAe,CAAC,EAAE,MAAM,CAAC,CAAC;CAClF;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,cAAc,CAAC,cAAc,EAAE,cAAc,CAAC,CAAC;CACpD;AAED,wBAAgB,kBAAkB,CAChC,KAAK,CAAC,eAAe,SAAS,qBAAqB,EACnD,KAAK,CAAC,eAAe,SAAS,qBAAqB,EAEnD,QAAQ,EAAE,gBAAgB,CAAC,eAAe,EAAE,eAAe,CAAC,EAC5D,MAAM,EAAE,oBAAoB,EAC5B,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,yBAAyB,CAAA;CAAO,GACvD,cAAc,CAAC,eAAe,EAAE,eAAe,CAAC,CAoClD"}
package/llms-full.txt CHANGED
@@ -2799,35 +2799,61 @@ WebSocket engine; it ships thin, typed wrappers over Socket.IO and a bridge that
2799
2799
  syncs socket events into the TanStack Query cache. See
2800
2800
  [ADR 0008](../decisions/0008-thin-wrappers.md).
2801
2801
 
2802
- ## Typed events
2802
+ ## Zod-first event contract
2803
2803
 
2804
- Declare the event maps once, in the shared module both sides import them:
2804
+ Declare wire events once in the shared module. Each `args` schema is a tuple, so
2805
+ Socket.IO's no-payload and variadic forms stay intact; `ack` describes the value
2806
+ returned through an acknowledgement callback.
2805
2807
 
2806
2808
  ```ts
2807
2809
  // shared/contracts.ts
2808
- export interface ServerToClientEvents {
2809
- 'note:created': (note: Note) => void
2810
- 'note:deleted': (id: string) => void
2811
- }
2812
- export interface ClientToServerEvents {
2813
- 'room:join': (room: string) => void
2814
- }
2810
+ import { defineRealtimeContract } from 'stitchkit'
2811
+ import { z } from 'zod'
2812
+
2813
+ export const realtimeContract = defineRealtimeContract({
2814
+ serverToClient: {
2815
+ 'note:created': { args: z.tuple([NoteSchema]) },
2816
+ 'note:deleted': { args: z.tuple([z.string()]) },
2817
+ },
2818
+ clientToServer: {
2819
+ 'room:join': {
2820
+ args: z.tuple([z.string()]),
2821
+ ack: z.object({ joined: z.boolean() }),
2822
+ },
2823
+ ready: { args: z.tuple([]) },
2824
+ },
2825
+ })
2815
2826
  ```
2816
2827
 
2817
- Every `emit` and `on` on both the server and client wrapper is typed against
2818
- these maps.
2828
+ The contract derives every event handler type and validates both directions at
2829
+ runtime. A malformed inbound tuple never reaches the application handler;
2830
+ invalid outbound data throws before Socket.IO publishes it. Rejections call the
2831
+ optional `onRejected` hook with event, direction, phase and the Zod error.
2819
2832
 
2820
2833
  ## Server — `createSocketIOServer`
2821
2834
 
2822
2835
  ```ts
2823
- import { createServer, createSocketIOServer } from 'stitchkit/server'
2836
+ import {
2837
+ bindRealtimeServer,
2838
+ createServer,
2839
+ createSocketIOServer,
2840
+ } from 'stitchkit/server'
2841
+ import { realtimeContract } from '@app/shared'
2824
2842
 
2825
- const socket = await createSocketIOServer<ServerToClientEvents, ClientToServerEvents>({
2843
+ const socket = await createSocketIOServer({
2826
2844
  cors: { origin: 'https://app.example.com' },
2827
2845
  })
2828
2846
 
2829
- socket.io.on('connection', (s) => {
2830
- s.on('room:join', (room) => s.join(room)) // rooms, handshake auth — your logic
2847
+ const realtime = bindRealtimeServer(realtimeContract, socket, {
2848
+ onRejected: (event) => audit.realtimeRejected(event),
2849
+ })
2850
+
2851
+ realtime.onConnection(({ raw, events, to }) => {
2852
+ events.on('room:join', (room, acknowledge) => {
2853
+ raw.join(room) // authorization and room membership remain application policy
2854
+ acknowledge({ joined: true })
2855
+ to(room).emit('note:created', note)
2856
+ })
2831
2857
  })
2832
2858
  ```
2833
2859
 
@@ -2840,13 +2866,13 @@ createServer({
2840
2866
  rawRoutes: [socket.route], // ready-made /socket.io/*socketPath route
2841
2867
  })
2842
2868
 
2843
- // elsewhere — broadcast:
2844
- socket.io.emit('note:created', note)
2869
+ // elsewhere — validated broadcast:
2870
+ realtime.emit('note:created', note)
2845
2871
  ```
2846
2872
 
2847
2873
  | Handle field | Purpose |
2848
2874
  |--------------|---------|
2849
- | `io` | the typed Socket.IO server attach `connection` handlers, broadcast |
2875
+ | `io` | raw Socket.IO server for middleware, handshake auth and transport ownership |
2850
2876
  | `websocket` | Bun WebSocket handlers — pass to `createServer({ websocket })` |
2851
2877
  | `route` | the `/socket.io/*socketPath` raw route — pass to `createServer({ rawRoutes })` |
2852
2878
 
@@ -2868,20 +2894,29 @@ precedence over the same keys in `serverOptions`. On Bun the engine-level option
2868
2894
  `@socket.io/bun-engine` too — so a configured `maxHttpBufferSize` actually applies
2869
2895
  instead of silently truncating at 1 MB.
2870
2896
 
2871
- ## Client — `createSocketIOClient`
2897
+ ## Client — `createRealtimeClient`
2872
2898
 
2873
2899
  ```ts
2874
- import { createSocketIOClient } from 'stitchkit'
2900
+ import { createRealtimeClient } from 'stitchkit'
2901
+ import { realtimeContract } from '@app/shared'
2875
2902
 
2876
- const socket = createSocketIOClient<ServerToClientEvents, ClientToServerEvents>({
2903
+ const socket = createRealtimeClient(realtimeContract, {
2877
2904
  url: 'https://api.example.com',
2905
+ retain: ['note:created'],
2906
+ onRejected: (event) => reportClientError(event),
2878
2907
  })
2879
2908
 
2880
2909
  socket.connect()
2881
2910
  socket.on('note:created', (note) => { /* typed note */ })
2882
- socket.emit('room:join', 'general') // typed
2911
+ socket.emit('room:join', 'general', ({ joined }) => { /* typed + validated */ })
2883
2912
  ```
2884
2913
 
2914
+ ## Low-level transport
2915
+
2916
+ `createSocketIOClient` remains the low-level Socket.IO transport wrapper for
2917
+ schema-agnostic infrastructure. Application wire events should use
2918
+ `createRealtimeClient`; it adds the shared contract without replacing Socket.IO.
2919
+
2885
2920
  ### Durable subscriptions
2886
2921
 
2887
2922
  `socket.on(...)` returns an unsubscribe and is **durable** — the handler is
@@ -2901,7 +2936,7 @@ subscribes (and on the next subscribe after a re-render). It is the pub/sub
2901
2936
  analogue of an MQTT *retained* message or an RxJS `BehaviorSubject`.
2902
2937
 
2903
2938
  ```ts
2904
- const client = createSocketIOClient<ServerEvents, ClientEvents>({
2939
+ const client = createRealtimeClient(realtimeContract, {
2905
2940
  url,
2906
2941
  retain: ['presence:changed', 'job:state'], // events to keep the last value of
2907
2942
  })
@@ -2938,7 +2973,7 @@ handshake with **`auth`** instead — the token reaches the server as
2938
2973
  // client — a function is re-read on every (re)connect, so a rotated token is
2939
2974
  // picked up automatically; no need to recreate the client (or lose durable
2940
2975
  // subscriptions). It may be async.
2941
- const socket = createSocketIOClient<ServerToClientEvents, ClientToServerEvents>({
2976
+ const socket = createRealtimeClient(realtimeContract, {
2942
2977
  url: 'https://api.example.com',
2943
2978
  auth: () => ({ token: getAccessToken() }),
2944
2979
  })
@@ -5207,12 +5242,16 @@ The browser-and-server entrypoint. Re-exports everything from
5207
5242
 
5208
5243
  | Export | Kind | Summary |
5209
5244
  |--------|------|---------|
5210
- | `createSocketIOClient` | function | the typed Socket.IO client — [guide](../guide/realtime.md#client--createsocketioclient) |
5245
+ | `createSocketIOClient` | function | low-level typed Socket.IO transport primitive — [guide](../guide/realtime.md#low-level-transport) |
5246
+ | `defineRealtimeContract` | function | Zod-first shared Socket.IO event contract — [guide](../guide/realtime.md#zod-first-event-contract) |
5247
+ | `createRealtimeClient` | function | inferred, runtime-validated Socket.IO client — [guide](../guide/realtime.md#client--createrealtimeclient) |
5211
5248
  | `createRetainedTopics` | function | retained last-value store for sticky events — [guide](../guide/realtime.md#sticky-events) |
5212
5249
  | `parseSSE` | function | parse an SSE `Response` into an async generator — [guide](../guide/client.md#sse) |
5213
5250
  | `SocketIOClient` | _type_ | the client handle |
5214
5251
  | `SocketIOClientConfig` | _type_ | config for `createSocketIOClient` (incl. `retain`) |
5215
5252
  | `SocketEventMap` | _type_ | the shape of an event map |
5253
+ | `RealtimeClient` | _type_ | validated client inferred from a realtime contract |
5254
+ | `RealtimeClientOptions` | _type_ | transport options and the rejected-event hook for `createRealtimeClient` |
5216
5255
  | `RetainedTopics` | _type_ | the `createRetainedTopics` handle |
5217
5256
  | `ParseSSEOptions` | _type_ | options for `parseSSE` |
5218
5257
 
@@ -5407,6 +5446,10 @@ Also re-exports the error helpers from `stitchkit/contract`.
5407
5446
  | Export | Kind | Summary |
5408
5447
  |--------|------|---------|
5409
5448
  | `createSocketIOServer` | function | the typed Socket.IO server — [guide](../guide/realtime.md#server--createsocketioserver) |
5449
+ | `bindRealtimeServer` | function | inferred, runtime-validated connection and broadcast boundary |
5450
+ | `RealtimeServer` | _type_ | validated broadcast and connection API inferred from a realtime contract |
5451
+ | `RealtimeServerConnection` | _type_ | one validated connection with raw socket access for auth and rooms |
5452
+ | `RealtimeServerHandle` | _type_ | minimal Socket.IO server handle accepted by `bindRealtimeServer` |
5410
5453
  | `SocketIOServerConfig` | _type_ | config for `createSocketIOServer` |
5411
5454
  | `SocketIOServerHandle` | _type_ | the `{ io, websocket, route }` handle |
5412
5455
  | `composeWebSocketHandlers` | function | compose one Bun `websocket` from N lanes — a raw binary lane beside Socket.IO ([guide](../guide/realtime.md#raw-binary-lane-bun)) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.44.1",
3
+ "version": "0.45.0",
4
4
  "description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
5
5
  "keywords": [
6
6
  "bun",