@nifrajs/client 4.0.2 → 5.0.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/src/treaty.ts CHANGED
@@ -26,17 +26,26 @@ type CallOptions<I extends RouteInfo> = {
26
26
  * value instead of being treated like an omitted object property by the general `Jsonify` helper. */
27
27
  type JsonifyResponses<T> = T extends object ? { [K in keyof T]: Jsonify<T[K]> } : never
28
28
 
29
- type IsBodyVerb<M extends string> = M extends "POST" | "PUT" | "PATCH" ? true : false
29
+ /** Where a verb's body goes: slot 0 for POST/PUT/PATCH, `options.body` for DELETE, nowhere else. */
30
+ type BodySlot<M extends string> = M extends "POST" | "PUT" | "PATCH"
31
+ ? "positional"
32
+ : M extends "DELETE"
33
+ ? "option"
34
+ : "none"
30
35
 
31
36
  /**
32
37
  * A terminal method call. The shape is **verb-aware** so the runtime can read
33
38
  * args unambiguously with no route info: body-verbs (POST/PUT/PATCH) always take
34
39
  * the body positionally Eden-style (typed `undefined` when the route has no body
35
- * schema, so it's still slot 0); GET/DELETE/HEAD take only options. Returns a
40
+ * schema, so it's still slot 0); GET/DELETE/HEAD take only options, and a DELETE
41
+ * whose route declares a body schema requires it as `options.body`. Returns a
36
42
  * `Result` whose data is the handler's output as it arrives over the wire
37
43
  * (`Jsonify`).
38
44
  */
39
- type MethodCall<I extends RouteInfo, BodyVerb extends boolean> = BodyVerb extends true
45
+ type MethodCall<
46
+ I extends RouteInfo,
47
+ Slot extends "positional" | "option" | "none",
48
+ > = Slot extends "positional"
40
49
  ? [I["body"]] extends [never]
41
50
  ? (
42
51
  body?: undefined,
@@ -50,11 +59,23 @@ type MethodCall<I extends RouteInfo, BodyVerb extends boolean> = BodyVerb extend
50
59
  ) => Promise<
51
60
  Result<Jsonify<I["output"]>, Jsonify<I["errors"]>, JsonifyResponses<I["responses"]>>
52
61
  >
53
- : (
54
- options?: CallOptions<I>,
55
- ) => Promise<
56
- Result<Jsonify<I["output"]>, Jsonify<I["errors"]>, JsonifyResponses<I["responses"]>>
57
- >
62
+ : Slot extends "option"
63
+ ? [I["body"]] extends [never]
64
+ ? (
65
+ options?: CallOptions<I>,
66
+ ) => Promise<
67
+ Result<Jsonify<I["output"]>, Jsonify<I["errors"]>, JsonifyResponses<I["responses"]>>
68
+ >
69
+ : (
70
+ options: CallOptions<I> & { body: I["body"] },
71
+ ) => Promise<
72
+ Result<Jsonify<I["output"]>, Jsonify<I["errors"]>, JsonifyResponses<I["responses"]>>
73
+ >
74
+ : (
75
+ options?: CallOptions<I>,
76
+ ) => Promise<
77
+ Result<Jsonify<I["output"]>, Jsonify<I["errors"]>, JsonifyResponses<I["responses"]>>
78
+ >
58
79
 
59
80
  // --- typed SSE subscriptions ---
60
81
 
@@ -67,8 +88,9 @@ export interface SubscribeOptions<I extends RouteInfo> {
67
88
  /** Abort to close the subscription (same effect as calling `close()`). */
68
89
  signal?: AbortSignal
69
90
  /**
70
- * Reconnect after a dropped/errored stream (default true - EventSource semantics, so a proxy
71
- * closing an idle feed doesn't silently kill it). A FINITE stream should pass `false` so a clean
91
+ * Reconnect after a dropped/errored stream. Default `true` for a GET stream (EventSource semantics,
92
+ * so a proxy closing an idle feed doesn't silently kill it) and `false` for one that posts a body,
93
+ * since a reconnect sends the body again. A FINITE stream should pass `false` so a clean
72
94
  * server-side end completes the subscription instead of replaying it. Delays follow exponential
73
95
  * backoff with jitter, honoring the server's `retry:` hint when sent.
74
96
  */
@@ -89,12 +111,24 @@ type SubscribeCall<I extends RouteInfo> = (
89
111
  options?: SubscribeOptions<I>,
90
112
  ) => Subscription
91
113
 
92
- /** Routes declared via `app.sse()` grow a `.subscribe()` beside their verbs. */
93
- type SseMethods<MethodMap> = MethodMap extends { GET: infer G }
114
+ /** A POST stream's `.subscribe()`: the route's body is required and is posted on every connect. */
115
+ type SubscribeBodyCall<I extends RouteInfo> = (
116
+ onEvent: (event: Jsonify<SseOf<I>>) => void,
117
+ options: SubscribeOptions<I> & { body: I["body"] },
118
+ ) => Subscription
119
+
120
+ /** Routes declared via `app.sse()` grow a `.subscribe()` beside their verbs; one whose schema
121
+ * declares a body is a POST route, and its `.subscribe()` takes that body. */
122
+ type SseMethods<MethodMap> = (MethodMap extends { GET: infer G }
94
123
  ? [SseOf<G>] extends [never]
95
124
  ? unknown
96
125
  : { subscribe: SubscribeCall<G & RouteInfo> }
97
- : unknown
126
+ : unknown) &
127
+ (MethodMap extends { POST: infer P }
128
+ ? [SseOf<P>] extends [never]
129
+ ? unknown
130
+ : { subscribe: SubscribeBodyCall<P & RouteInfo> }
131
+ : unknown)
98
132
 
99
133
  // --- typed WebSocket handles ---
100
134
 
@@ -146,7 +180,7 @@ type WsMethods<MethodMap> = MethodMap extends { WS: infer G }
146
180
  type Methods<MethodMap> = {
147
181
  [M in Exclude<keyof MethodMap, "WS"> as Lowercase<M & string>]: MethodCall<
148
182
  MethodMap[M] & RouteInfo,
149
- IsBodyVerb<M & string>
183
+ BodySlot<M & string>
150
184
  >
151
185
  } & SseMethods<MethodMap> &
152
186
  WsMethods<MethodMap>