@nifrajs/client 4.0.0 → 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/README.md +5 -2
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +141 -52
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/routes.d.ts +25 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js.map +1 -1
- package/dist/treaty.d.ts +23 -9
- package/dist/treaty.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +137 -48
- package/src/index.ts +3 -0
- package/src/routes.ts +49 -1
- package/src/treaty.ts +48 -14
package/dist/treaty.d.ts
CHANGED
|
@@ -20,16 +20,20 @@ type CallOptions<I extends RouteInfo> = {
|
|
|
20
20
|
type JsonifyResponses<T> = T extends object ? {
|
|
21
21
|
[K in keyof T]: Jsonify<T[K]>;
|
|
22
22
|
} : never;
|
|
23
|
-
|
|
23
|
+
/** Where a verb's body goes: slot 0 for POST/PUT/PATCH, `options.body` for DELETE, nowhere else. */
|
|
24
|
+
type BodySlot<M extends string> = M extends "POST" | "PUT" | "PATCH" ? "positional" : M extends "DELETE" ? "option" : "none";
|
|
24
25
|
/**
|
|
25
26
|
* A terminal method call. The shape is **verb-aware** so the runtime can read
|
|
26
27
|
* args unambiguously with no route info: body-verbs (POST/PUT/PATCH) always take
|
|
27
28
|
* the body positionally Eden-style (typed `undefined` when the route has no body
|
|
28
|
-
* schema, so it's still slot 0); GET/DELETE/HEAD take only options
|
|
29
|
+
* schema, so it's still slot 0); GET/DELETE/HEAD take only options, and a DELETE
|
|
30
|
+
* whose route declares a body schema requires it as `options.body`. Returns a
|
|
29
31
|
* `Result` whose data is the handler's output as it arrives over the wire
|
|
30
32
|
* (`Jsonify`).
|
|
31
33
|
*/
|
|
32
|
-
type MethodCall<I extends RouteInfo,
|
|
34
|
+
type MethodCall<I extends RouteInfo, Slot extends "positional" | "option" | "none"> = Slot extends "positional" ? [I["body"]] extends [never] ? (body?: undefined, options?: CallOptions<I>) => Promise<Result<Jsonify<I["output"]>, Jsonify<I["errors"]>, JsonifyResponses<I["responses"]>>> : (body: I["body"], options?: CallOptions<I>) => Promise<Result<Jsonify<I["output"]>, Jsonify<I["errors"]>, JsonifyResponses<I["responses"]>>> : Slot extends "option" ? [I["body"]] extends [never] ? (options?: CallOptions<I>) => Promise<Result<Jsonify<I["output"]>, Jsonify<I["errors"]>, JsonifyResponses<I["responses"]>>> : (options: CallOptions<I> & {
|
|
35
|
+
body: I["body"];
|
|
36
|
+
}) => Promise<Result<Jsonify<I["output"]>, Jsonify<I["errors"]>, JsonifyResponses<I["responses"]>>> : (options?: CallOptions<I>) => Promise<Result<Jsonify<I["output"]>, Jsonify<I["errors"]>, JsonifyResponses<I["responses"]>>>;
|
|
33
37
|
/** The SSE event payload of a route (from `app.sse()`'s `sse` schema); `never` for ordinary routes. */
|
|
34
38
|
type SseOf<I> = I extends {
|
|
35
39
|
sse: infer E;
|
|
@@ -40,8 +44,9 @@ export interface SubscribeOptions<I extends RouteInfo> {
|
|
|
40
44
|
/** Abort to close the subscription (same effect as calling `close()`). */
|
|
41
45
|
signal?: AbortSignal;
|
|
42
46
|
/**
|
|
43
|
-
* Reconnect after a dropped/errored stream
|
|
44
|
-
* closing an idle feed doesn't silently kill it)
|
|
47
|
+
* Reconnect after a dropped/errored stream. Default `true` for a GET stream (EventSource semantics,
|
|
48
|
+
* so a proxy closing an idle feed doesn't silently kill it) and `false` for one that posts a body,
|
|
49
|
+
* since a reconnect sends the body again. A FINITE stream should pass `false` so a clean
|
|
45
50
|
* server-side end completes the subscription instead of replaying it. Delays follow exponential
|
|
46
51
|
* backoff with jitter, honoring the server's `retry:` hint when sent.
|
|
47
52
|
*/
|
|
@@ -59,12 +64,21 @@ export interface Subscription {
|
|
|
59
64
|
close(): void;
|
|
60
65
|
}
|
|
61
66
|
type SubscribeCall<I extends RouteInfo> = (onEvent: (event: Jsonify<SseOf<I>>) => void, options?: SubscribeOptions<I>) => Subscription;
|
|
62
|
-
/**
|
|
63
|
-
type
|
|
67
|
+
/** A POST stream's `.subscribe()`: the route's body is required and is posted on every connect. */
|
|
68
|
+
type SubscribeBodyCall<I extends RouteInfo> = (onEvent: (event: Jsonify<SseOf<I>>) => void, options: SubscribeOptions<I> & {
|
|
69
|
+
body: I["body"];
|
|
70
|
+
}) => Subscription;
|
|
71
|
+
/** Routes declared via `app.sse()` grow a `.subscribe()` beside their verbs; one whose schema
|
|
72
|
+
* declares a body is a POST route, and its `.subscribe()` takes that body. */
|
|
73
|
+
type SseMethods<MethodMap> = (MethodMap extends {
|
|
64
74
|
GET: infer G;
|
|
65
75
|
} ? [SseOf<G>] extends [never] ? unknown : {
|
|
66
76
|
subscribe: SubscribeCall<G & RouteInfo>;
|
|
67
|
-
} : unknown
|
|
77
|
+
} : unknown) & (MethodMap extends {
|
|
78
|
+
POST: infer P;
|
|
79
|
+
} ? [SseOf<P>] extends [never] ? unknown : {
|
|
80
|
+
subscribe: SubscribeBodyCall<P & RouteInfo>;
|
|
81
|
+
} : unknown);
|
|
68
82
|
/** The WS frame contract of a route (from `app.ws()`'s schemas); `never` for ordinary routes. */
|
|
69
83
|
type WsOf<I> = I extends {
|
|
70
84
|
ws: infer W extends {
|
|
@@ -111,7 +125,7 @@ type WsMethods<MethodMap> = MethodMap extends {
|
|
|
111
125
|
ws: WsCall<G & RouteInfo>;
|
|
112
126
|
} : unknown;
|
|
113
127
|
type Methods<MethodMap> = {
|
|
114
|
-
[M in Exclude<keyof MethodMap, "WS"> as Lowercase<M & string>]: MethodCall<MethodMap[M] & RouteInfo,
|
|
128
|
+
[M in Exclude<keyof MethodMap, "WS"> as Lowercase<M & string>]: MethodCall<MethodMap[M] & RouteInfo, BodySlot<M & string>>;
|
|
115
129
|
} & SseMethods<MethodMap> & WsMethods<MethodMap>;
|
|
116
130
|
/**
|
|
117
131
|
* Property names the runtime proxy intercepts BEFORE path resolution (`resolveSegment` and the
|
package/dist/treaty.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"treaty.d.ts","sourceRoot":"","sources":["../src/treaty.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAC1E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,mHAAmH;AACnH,MAAM,MAAM,UAAU,CAAC,GAAG,IAAI,GAAG,SAAS,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;AAIjF;;;;;GAKG;AACH,KAAK,aAAa,GAChB,sHAAsH,CAAA;AAExH,KAAK,WAAW,CAAC,CAAC,SAAS,SAAS,IAAI;IACtC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,aAAa,GAAG,CAAC,CAAC,OAAO,CAAC,CAAA;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB,CAAA;AAED;qGACqG;AACrG,KAAK,gBAAgB,CAAC,CAAC,IAAI,CAAC,SAAS,MAAM,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,GAAG,KAAK,CAAA;AAEvF,KAAK,
|
|
1
|
+
{"version":3,"file":"treaty.d.ts","sourceRoot":"","sources":["../src/treaty.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/client",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "Browser-safe, end-to-end-typed client for @nifrajs/core servers (Eden-style proxy).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@nifrajs/core": "^
|
|
45
|
+
"@nifrajs/core": "^5.0.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@nifrajs/core": "workspace:*",
|
package/src/client.ts
CHANGED
|
@@ -78,7 +78,7 @@ export interface ClientRetryOptions {
|
|
|
78
78
|
|
|
79
79
|
export interface ClientOptions {
|
|
80
80
|
/**
|
|
81
|
-
* Headers sent on every HTTP request (a per-call `headers` option is merged on top). A WebSocket
|
|
81
|
+
* Headers sent on every HTTP request (a per-call `headers` option is merged on top, names matched in any letter case). A WebSocket
|
|
82
82
|
* handshake carries none of them, on any runtime: a browser cannot set them, and a socket that
|
|
83
83
|
* authenticated under Bun but not in the browser would be worse than one that never did.
|
|
84
84
|
*/
|
|
@@ -211,6 +211,8 @@ interface CallOptions {
|
|
|
211
211
|
readonly query?: Record<string, unknown>
|
|
212
212
|
readonly headers?: Record<string, string>
|
|
213
213
|
readonly signal?: AbortSignal
|
|
214
|
+
/** A DELETE's body; the body verbs take theirs positionally. */
|
|
215
|
+
readonly body?: unknown
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
/**
|
|
@@ -486,11 +488,24 @@ function createProxy(
|
|
|
486
488
|
? Object.values(first as Record<string, unknown>)[0]
|
|
487
489
|
: first
|
|
488
490
|
if (value === undefined || value === null) return createProxy(base, path, options, false)
|
|
489
|
-
return createProxy(base, `${path}/${
|
|
491
|
+
return createProxy(base, `${path}/${encodeSegment(String(value))}`, options, false)
|
|
490
492
|
},
|
|
491
493
|
})
|
|
492
494
|
}
|
|
493
495
|
|
|
496
|
+
/**
|
|
497
|
+
* A path segment, percent-encoded. A value with an unpaired surrogate has no UTF-8 spelling and
|
|
498
|
+
* `encodeURIComponent` throws on it; it becomes a `..` segment instead, which every call refuses as
|
|
499
|
+
* `invalid_path` before anything is sent, so the call still resolves to a Result.
|
|
500
|
+
*/
|
|
501
|
+
function encodeSegment(value: string): string {
|
|
502
|
+
try {
|
|
503
|
+
return encodeURIComponent(value)
|
|
504
|
+
} catch {
|
|
505
|
+
return ".."
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
494
509
|
/** Resolve one get-trap key on a proxy node - the (memoizable) body of the `get` trap above. */
|
|
495
510
|
function resolveSegment(
|
|
496
511
|
base: string,
|
|
@@ -567,6 +582,29 @@ function formBody(body: unknown): FormData | undefined {
|
|
|
567
582
|
return form
|
|
568
583
|
}
|
|
569
584
|
|
|
585
|
+
// Header names are case-insensitive: a plain object keyed `Authorization` and `authorization` would
|
|
586
|
+
// send both values joined into one header.
|
|
587
|
+
function assignHeaders(
|
|
588
|
+
target: Record<string, string>,
|
|
589
|
+
source: Record<string, string> | undefined,
|
|
590
|
+
): Record<string, string> {
|
|
591
|
+
if (source === undefined) return target
|
|
592
|
+
for (const [name, value] of Object.entries(source)) {
|
|
593
|
+
deleteHeader(target, name)
|
|
594
|
+
target[name] = value
|
|
595
|
+
}
|
|
596
|
+
return target
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function deleteHeader(headers: Record<string, string>, name: string): void {
|
|
600
|
+
const lower = name.toLowerCase()
|
|
601
|
+
for (const key of Object.keys(headers)) if (key.toLowerCase() === lower) delete headers[key]
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
605
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === name)
|
|
606
|
+
}
|
|
607
|
+
|
|
570
608
|
async function execute(
|
|
571
609
|
base: string,
|
|
572
610
|
path: string,
|
|
@@ -580,35 +618,38 @@ async function execute(
|
|
|
580
618
|
return { ok: false, status: 0, data: null, error: { error: "invalid_path" } }
|
|
581
619
|
}
|
|
582
620
|
const isBodyVerb = BODY_VERBS.has(verb)
|
|
583
|
-
const body = isBodyVerb ? args[0] : undefined
|
|
584
621
|
const callOptions = (isBodyVerb ? args[1] : args[0]) as CallOptions | undefined
|
|
622
|
+
const body = isBodyVerb ? args[0] : verb === "delete" ? callOptions?.body : undefined
|
|
585
623
|
|
|
586
624
|
let url = base + (path === "" ? "/" : path)
|
|
587
625
|
const query = callOptions?.query ? buildQuery(callOptions.query) : ""
|
|
588
626
|
if (query !== "") url += `?${query}`
|
|
589
627
|
|
|
590
628
|
const method = verb.toUpperCase()
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
629
|
+
const baseHeaders = assignHeaders(assignHeaders({}, options.headers), callOptions?.headers)
|
|
630
|
+
const init: RequestInit = { method }
|
|
631
|
+
const form = body === undefined ? undefined : formBody(body)
|
|
632
|
+
const codec = options.transport?.codec ?? plainJsonCodec
|
|
633
|
+
if (form !== undefined) init.body = form
|
|
634
|
+
else if (body !== undefined) init.body = codec.encode(body)
|
|
635
|
+
// Built for every attempt, so a retry carries what `onRequest` returns now (a refreshed token).
|
|
636
|
+
const headersFor = async (): Promise<Record<string, string>> => {
|
|
637
|
+
const headers = { ...baseHeaders }
|
|
638
|
+
if (options.onRequest !== undefined) {
|
|
639
|
+
const extra = await options.onRequest({ url, method, headers, body })
|
|
640
|
+
assignHeaders(headers, extra)
|
|
641
|
+
}
|
|
599
642
|
if (form !== undefined) {
|
|
600
|
-
init.body = form
|
|
601
643
|
// The platform writes this header itself, boundary included; one set by hand would name a
|
|
602
644
|
// boundary the body does not use and the server would refuse the request.
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
headers["content-type"] = codec.mediaType
|
|
610
|
-
if (options.transport !== undefined) headers.accept ??= codec.mediaType
|
|
645
|
+
deleteHeader(headers, "content-type")
|
|
646
|
+
} else if (body !== undefined) {
|
|
647
|
+
// The codec labels the bytes it wrote; a type set by hand would name a different encoding.
|
|
648
|
+
assignHeaders(headers, { "content-type": codec.mediaType })
|
|
649
|
+
if (options.transport !== undefined && !hasHeader(headers, "accept"))
|
|
650
|
+
headers.accept = codec.mediaType
|
|
611
651
|
}
|
|
652
|
+
return headers
|
|
612
653
|
}
|
|
613
654
|
const { signal, timeout } = buildSignal(callOptions?.signal, options.timeoutMs)
|
|
614
655
|
if (signal !== undefined) init.signal = signal
|
|
@@ -631,8 +672,9 @@ async function execute(
|
|
|
631
672
|
let response: Response
|
|
632
673
|
let attempt = 0
|
|
633
674
|
for (;;) {
|
|
675
|
+
const attemptInit = { ...init, headers: await headersFor() }
|
|
634
676
|
try {
|
|
635
|
-
response = await doFetch(url,
|
|
677
|
+
response = await doFetch(url, attemptInit)
|
|
636
678
|
} catch (error) {
|
|
637
679
|
// A contract violation is a test assertion (validateResponses), not a call outcome - let it
|
|
638
680
|
// fail the test instead of degrading into a `Result` the test would happily branch on.
|
|
@@ -675,10 +717,13 @@ async function execute(
|
|
|
675
717
|
if (isPayloadTooLarge(cause)) {
|
|
676
718
|
return { ok: false, status: 0, data: null, error: { error: "response_too_large" } }
|
|
677
719
|
}
|
|
678
|
-
// A
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
|
|
720
|
+
// A body its content type does not decode - a proxy's HTML error page labelled JSON, say - is
|
|
721
|
+
// no answer the caller can use either. Anything else is the read itself failing - the deadline
|
|
722
|
+
// or the caller's signal firing mid-body, or the connection dropping - which the fetch above
|
|
723
|
+
// already reports as a Result.
|
|
724
|
+
if (cause instanceof Error && cause.name === "TransportCodecError") {
|
|
725
|
+
return { ok: false, status: 0, data: null, error: { error: "malformed_response" } }
|
|
726
|
+
}
|
|
682
727
|
const code = timeout?.aborted === true ? "timeout" : "network_error"
|
|
683
728
|
return { ok: false, status: 0, data: null, error: { error: code } }
|
|
684
729
|
}
|
|
@@ -694,6 +739,8 @@ async function execute(
|
|
|
694
739
|
|
|
695
740
|
interface SubscribeCallOptions {
|
|
696
741
|
readonly query?: Record<string, unknown>
|
|
742
|
+
/** The body of a POST stream (`app.sse()` with a body schema). */
|
|
743
|
+
readonly body?: unknown
|
|
697
744
|
readonly headers?: Record<string, string>
|
|
698
745
|
readonly signal?: AbortSignal
|
|
699
746
|
readonly reconnect?: boolean | { baseDelayMs?: number; maxDelayMs?: number }
|
|
@@ -715,21 +762,23 @@ const MAX_RETRY_MS = 2_147_483_647
|
|
|
715
762
|
* Incrementally parse a `text/event-stream` body, invoking `onFrame` per dispatched event.
|
|
716
763
|
* Implements the SSE wire format: CRLF, CR, and LF all end a line, `data:` accumulates multi-line,
|
|
717
764
|
* `id:`/`retry:` update stream state, `:` lines are comments, a blank line dispatches. An event
|
|
718
|
-
* still being assembled may hold at most `
|
|
765
|
+
* still being assembled may hold at most `maxEventBytes` UTF-8 bytes (fields and normalized
|
|
766
|
+
* line separators included, the empty dispatch line excluded).
|
|
719
767
|
*/
|
|
720
768
|
async function readSseStream(
|
|
721
769
|
body: ReadableStream<Uint8Array>,
|
|
722
770
|
onFrame: (frame: SseFrame) => void,
|
|
723
|
-
|
|
771
|
+
maxEventBytes: number,
|
|
724
772
|
): Promise<void> {
|
|
725
773
|
const reader = body.getReader()
|
|
726
774
|
const decoder = new TextDecoder()
|
|
775
|
+
const encoder = new TextEncoder()
|
|
727
776
|
const lineEnd = /\r\n?|\n/g
|
|
728
777
|
// Only newly decoded text is scanned; the unfinished line is carried here, so a line that arrives
|
|
729
778
|
// over many small reads costs its length once rather than once per read.
|
|
730
|
-
let partial =
|
|
779
|
+
let partial: string[] = []
|
|
731
780
|
let afterCr = false
|
|
732
|
-
let
|
|
781
|
+
let eventBytes = 0
|
|
733
782
|
let dataLines: string[] = []
|
|
734
783
|
let frame: SseFrame = {}
|
|
735
784
|
|
|
@@ -739,7 +788,7 @@ async function readSseStream(
|
|
|
739
788
|
onFrame(frame)
|
|
740
789
|
}
|
|
741
790
|
dataLines = []
|
|
742
|
-
|
|
791
|
+
eventBytes = 0
|
|
743
792
|
frame = {}
|
|
744
793
|
}
|
|
745
794
|
|
|
@@ -755,7 +804,6 @@ async function readSseStream(
|
|
|
755
804
|
if (value.startsWith(" ")) value = value.slice(1)
|
|
756
805
|
if (field === "data") {
|
|
757
806
|
dataLines.push(value)
|
|
758
|
-
held += value.length + 1
|
|
759
807
|
} else if (field === "id") {
|
|
760
808
|
// The grammar ignores an id with a NUL; it could never be sent back as `Last-Event-ID`.
|
|
761
809
|
if (!value.includes("\0")) frame.id = value
|
|
@@ -771,13 +819,21 @@ async function readSseStream(
|
|
|
771
819
|
let start = afterCr && text.charCodeAt(0) === 10 ? 1 : 0
|
|
772
820
|
lineEnd.lastIndex = start
|
|
773
821
|
for (let end = lineEnd.exec(text); end !== null; end = lineEnd.exec(text)) {
|
|
774
|
-
|
|
775
|
-
|
|
822
|
+
const piece = text.slice(start, end.index)
|
|
823
|
+
eventBytes += encoder.encode(piece).byteLength
|
|
824
|
+
// Check before dispatch or JSON parsing, including frames completed in this read.
|
|
825
|
+
if (piece !== "" || partial.length > 0) eventBytes += 1
|
|
826
|
+
if (eventBytes > maxEventBytes) throw new Error("sse_event_too_large")
|
|
827
|
+
partial.push(piece)
|
|
828
|
+
handleLine(partial.join(""))
|
|
829
|
+
partial = []
|
|
776
830
|
start = lineEnd.lastIndex
|
|
777
831
|
}
|
|
778
832
|
afterCr = text.charCodeAt(text.length - 1) === 13
|
|
779
|
-
|
|
780
|
-
|
|
833
|
+
const tail = text.slice(start)
|
|
834
|
+
eventBytes += encoder.encode(tail).byteLength
|
|
835
|
+
if (eventBytes > maxEventBytes) throw new Error("sse_event_too_large")
|
|
836
|
+
if (tail !== "") partial.push(tail)
|
|
781
837
|
}
|
|
782
838
|
|
|
783
839
|
try {
|
|
@@ -787,7 +843,7 @@ async function readSseStream(
|
|
|
787
843
|
feed(decoder.decode(value, { stream: true }))
|
|
788
844
|
}
|
|
789
845
|
feed(decoder.decode())
|
|
790
|
-
if (partial
|
|
846
|
+
if (partial.length > 0) handleLine(partial.join(""))
|
|
791
847
|
dispatch() // an unterminated final frame still dispatches
|
|
792
848
|
} catch (error) {
|
|
793
849
|
// Stopping early must not leave the response open behind a released lock.
|
|
@@ -812,7 +868,9 @@ function lastEventIdHeader(id: string): string {
|
|
|
812
868
|
* streams over the configured fetcher - network, an in-process bridge, or a test mock - with
|
|
813
869
|
* EventSource semantics where they matter: auto-reconnect with backoff + jitter (honoring the
|
|
814
870
|
* server's `retry:` hint after a stream it served), `Last-Event-ID` resumption, JSON-parsed typed
|
|
815
|
-
* events.
|
|
871
|
+
* events. A `body` makes it a POST stream, encoded like a POST call's body; it is sent on every
|
|
872
|
+
* connect, so such a stream reconnects only when asked to. `onRequest` runs before each connect.
|
|
873
|
+
* Never throws: failures reach `onError`; a terminal end reaches `onClose`.
|
|
816
874
|
*/
|
|
817
875
|
function subscribeSse(
|
|
818
876
|
base: string,
|
|
@@ -826,7 +884,10 @@ function subscribeSse(
|
|
|
826
884
|
let lastEventId: string | undefined
|
|
827
885
|
let serverRetryMs: number | undefined
|
|
828
886
|
|
|
829
|
-
const
|
|
887
|
+
const body = callOptions?.body
|
|
888
|
+
const method = body === undefined ? "GET" : "POST"
|
|
889
|
+
const codec = options.transport?.codec ?? plainJsonCodec
|
|
890
|
+
const reconnectConfig = callOptions?.reconnect ?? body === undefined
|
|
830
891
|
const reconnectEnabled = reconnectConfig !== false
|
|
831
892
|
const baseDelayMs =
|
|
832
893
|
(typeof reconnectConfig === "object" ? reconnectConfig.baseDelayMs : undefined) ?? 1_000
|
|
@@ -862,16 +923,34 @@ function subscribeSse(
|
|
|
862
923
|
close()
|
|
863
924
|
return
|
|
864
925
|
}
|
|
926
|
+
let form: FormData | undefined
|
|
927
|
+
let encoded: FormData | string | undefined
|
|
928
|
+
try {
|
|
929
|
+
form = body === undefined ? undefined : formBody(body)
|
|
930
|
+
encoded = form ?? (body === undefined ? undefined : codec.encode(body))
|
|
931
|
+
} catch (error) {
|
|
932
|
+
// A body that cannot be encoded sends nothing; reported after `subscribe()` returns.
|
|
933
|
+
await Promise.resolve()
|
|
934
|
+
if (closed) return
|
|
935
|
+
callOptions?.onError?.(error)
|
|
936
|
+
close()
|
|
937
|
+
return
|
|
938
|
+
}
|
|
865
939
|
let attempt = 0
|
|
866
940
|
while (!closed) {
|
|
867
941
|
try {
|
|
868
|
-
const headers
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
accept: "text/event-stream",
|
|
872
|
-
...(lastEventId ? { "last-event-id": lastEventIdHeader(lastEventId) } : {}),
|
|
942
|
+
const headers = assignHeaders(assignHeaders({}, options.headers), callOptions?.headers)
|
|
943
|
+
if (options.onRequest !== undefined) {
|
|
944
|
+
assignHeaders(headers, await options.onRequest({ url, method, headers, body }))
|
|
873
945
|
}
|
|
874
|
-
|
|
946
|
+
assignHeaders(headers, { accept: "text/event-stream" })
|
|
947
|
+
// The platform writes a form's content type, boundary included; the codec labels its bytes.
|
|
948
|
+
if (form !== undefined) deleteHeader(headers, "content-type")
|
|
949
|
+
else if (body !== undefined) assignHeaders(headers, { "content-type": codec.mediaType })
|
|
950
|
+
if (lastEventId) assignHeaders(headers, { "last-event-id": lastEventIdHeader(lastEventId) })
|
|
951
|
+
const init: RequestInit = { method, headers, signal: controller.signal }
|
|
952
|
+
if (encoded !== undefined) init.body = encoded
|
|
953
|
+
const response = await doFetch(url, init)
|
|
875
954
|
if (!response.ok || response.body === null) {
|
|
876
955
|
void response.body?.cancel().catch(() => {})
|
|
877
956
|
throw new Error(`sse_http_${response.status}`)
|
|
@@ -968,8 +1047,21 @@ const TEXTUAL_TYPES = new Set([
|
|
|
968
1047
|
"application/ecmascript",
|
|
969
1048
|
"application/x-www-form-urlencoded",
|
|
970
1049
|
"application/x-ndjson",
|
|
1050
|
+
"application/jsonl",
|
|
971
1051
|
])
|
|
972
1052
|
|
|
1053
|
+
const JSON_MEDIA = "application/json"
|
|
1054
|
+
|
|
1055
|
+
/** A content type the transport codecs decode. `application/json-patch+json` and `application/jsonl`
|
|
1056
|
+
* share the JSON prefix but are other formats, read like any other textual body. */
|
|
1057
|
+
function isTransportMedia(contentType: string): boolean {
|
|
1058
|
+
if (contentType.startsWith(JSON_MEDIA)) {
|
|
1059
|
+
const next = contentType[JSON_MEDIA.length]
|
|
1060
|
+
return next === undefined || next === ";" || next === " " || next === "\t"
|
|
1061
|
+
}
|
|
1062
|
+
return contentType.startsWith("application/vnd.nifra.")
|
|
1063
|
+
}
|
|
1064
|
+
|
|
973
1065
|
/**
|
|
974
1066
|
* True when a body can survive being decoded as UTF-8 text.
|
|
975
1067
|
*
|
|
@@ -1023,10 +1115,7 @@ async function parseBody(response: Response, options: ClientOptions): Promise<un
|
|
|
1023
1115
|
// is ~23x cheaper and the SAME cap is enforced on the result (identical error). A network
|
|
1024
1116
|
// client is never marked and keeps the bounded-while-streaming read.
|
|
1025
1117
|
const local = (options as { readonly [LOCAL_FETCH]?: true })[LOCAL_FETCH] === true
|
|
1026
|
-
if (
|
|
1027
|
-
contentType.startsWith("application/json") ||
|
|
1028
|
-
contentType.startsWith("application/vnd.nifra.")
|
|
1029
|
-
) {
|
|
1118
|
+
if (isTransportMedia(contentType)) {
|
|
1030
1119
|
if (local) {
|
|
1031
1120
|
return decodeTransportText(
|
|
1032
1121
|
await response.text(),
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ export type {
|
|
|
27
27
|
ActionArgs,
|
|
28
28
|
ActionData,
|
|
29
29
|
ApiProxy,
|
|
30
|
+
InputOf,
|
|
30
31
|
LoaderArgs,
|
|
31
32
|
LoaderData,
|
|
32
33
|
LoaderResponseControls,
|
|
@@ -34,7 +35,9 @@ export type {
|
|
|
34
35
|
Register,
|
|
35
36
|
RegisteredBackend,
|
|
36
37
|
RegisteredEnv,
|
|
38
|
+
RouteActionReturn,
|
|
37
39
|
RouteLoaderArgs,
|
|
40
|
+
RouteLoaderReturn,
|
|
38
41
|
SearchSchemaOf,
|
|
39
42
|
} from "./routes.ts"
|
|
40
43
|
export type {
|
package/src/routes.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { ContractShape, RegistryFor } from "@nifrajs/core/contract"
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
CookieOptions,
|
|
4
|
+
InferInput,
|
|
5
|
+
InferOutput,
|
|
6
|
+
ResponseResult,
|
|
7
|
+
StandardSchemaV1,
|
|
8
|
+
} from "@nifrajs/core/server"
|
|
3
9
|
import type { Treaty, TreatyFromRegistry } from "./treaty.ts"
|
|
4
10
|
|
|
5
11
|
/**
|
|
@@ -102,6 +108,48 @@ export type OutputOf<Module, Name extends string> = Module extends { readonly [K
|
|
|
102
108
|
: never
|
|
103
109
|
: null
|
|
104
110
|
|
|
111
|
+
/**
|
|
112
|
+
* What a route module's output schema accepts from its loader or action: the input type of its `Name`
|
|
113
|
+
* export, or `null` when the module declares none. A `t.deferred(inner)` field accepts `defer(...)`.
|
|
114
|
+
*/
|
|
115
|
+
export type InputOf<Module, Name extends string> = Module extends { readonly [K in Name]: infer S }
|
|
116
|
+
? S extends StandardSchemaV1
|
|
117
|
+
? InferInput<S>
|
|
118
|
+
: never
|
|
119
|
+
: null
|
|
120
|
+
|
|
121
|
+
type RouteControl = Response | ResponseResult | null | undefined
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* What a route module's `loader` may return: data its `loaderOutput` accepts, a `Response` or a
|
|
125
|
+
* `ResponseResult` such as `redirect(...)`, or nothing. With no `loaderOutput`, returning data is a
|
|
126
|
+
* type error, as it is a refusal at runtime.
|
|
127
|
+
*/
|
|
128
|
+
export type RouteLoaderReturn<Module> =
|
|
129
|
+
| InputOf<Module, "loaderOutput">
|
|
130
|
+
| RouteControl
|
|
131
|
+
| Promise<InputOf<Module, "loaderOutput"> | RouteControl>
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* What a route module's `action` may return: as {@link RouteLoaderReturn} against `actionOutput`,
|
|
135
|
+
* plus that data wrapped in `revalidate(paths, data)`.
|
|
136
|
+
*/
|
|
137
|
+
export type RouteActionReturn<Module> =
|
|
138
|
+
| InputOf<Module, "actionOutput">
|
|
139
|
+
| RouteRevalidate<InputOf<Module, "actionOutput">>
|
|
140
|
+
| RouteControl
|
|
141
|
+
| Promise<
|
|
142
|
+
| InputOf<Module, "actionOutput">
|
|
143
|
+
| RouteRevalidate<InputOf<Module, "actionOutput">>
|
|
144
|
+
| RouteControl
|
|
145
|
+
>
|
|
146
|
+
|
|
147
|
+
// Structural, so this stays decoupled from `@nifrajs/web`'s `revalidate()`.
|
|
148
|
+
type RouteRevalidate<Data> = {
|
|
149
|
+
readonly __nifraRevalidate: readonly string[]
|
|
150
|
+
readonly data: Data
|
|
151
|
+
}
|
|
152
|
+
|
|
105
153
|
/** The `searchSchema` a route's frontend half declares, or `undefined`. */
|
|
106
154
|
export type SearchSchemaOf<Module> = Module extends { readonly searchSchema: infer S }
|
|
107
155
|
? S
|