@uniflowed/router 0.0.0-alpha.34 → 0.0.0-alpha.35

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.
@@ -0,0 +1,155 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/router`, as a React Server Component imports it.
4
+ //
5
+ // The package root has two entries and an export condition picks between them:
6
+ // a module graph resolved under `react-server` — the one uf renders server
7
+ // components in (ubugeeei-prod/uf#519) — gets this file, and every other graph
8
+ // gets `./index.js`. Both export the same names, which is the point. A layout
9
+ // imports `Link` and `useRoute` from `@uniflowed/router` and means the same
10
+ // thing wherever it is rendered; what differs is what the names *are* here.
11
+ //
12
+ // * `Link`, `RouterProvider`, `RouteView` and `routerView` come from
13
+ // `./internal/runtime.js`, a `"use client"` module, so in this graph each one
14
+ // is a client reference: rendered as markup on the server and hydrated in
15
+ // the browser, with its code shipped for the browser's half only.
16
+ // * `useRoute`, `useLoaderData` and `useSeo` are server implementations. A
17
+ // server component has no context to read, so they read the route the
18
+ // Flight renderer is rendering — `./internal/server-route.js` — which is the
19
+ // same route the browser's hooks read back out of the payload. This
20
+ // repository's documentation site highlights the section a reader is in
21
+ // from a server layout, and would otherwise have needed a client component to
22
+ // find out where it was.
23
+ // * `useRouter` refuses, by name. Navigation happens in the browser, and a
24
+ // router a server component could hold would be methods that can never do
25
+ // anything.
26
+ // * `useIsServer` answers `true`, which is what it has always answered while a
27
+ // server renders.
28
+ //
29
+ // Everything else — matching, the router's control errors, resolution — is
30
+ // React-free and is the same code `./index.js` exports.
31
+
32
+ import * as React from "react";
33
+ import { use } from "react";
34
+
35
+ import { Head } from "./internal/head.js";
36
+ import type { Metadata } from "./internal/resolve.js";
37
+ import type { RouteInfo, Router } from "./internal/runtime.js";
38
+ import { serverRoute } from "./internal/server-route.js";
39
+
40
+ export type { ErrorProps, LayoutProps, PageProps } from "./index.js";
41
+
42
+ export type {
43
+ AppProps,
44
+ ErrorBoundary,
45
+ ErrorModule,
46
+ Interception,
47
+ JsonLd,
48
+ LayoutModule,
49
+ LinkPrefetch,
50
+ LoaderArgs,
51
+ Metadata,
52
+ MetadataArgs,
53
+ NavigateOptions,
54
+ NotFoundBoundary,
55
+ PageModule,
56
+ ResolvedRoute,
57
+ Robots,
58
+ RouteError,
59
+ RouteInfo,
60
+ RouteMatch,
61
+ RouteParamSpec,
62
+ RouteParams,
63
+ RouteRecord,
64
+ RouteTable,
65
+ Router,
66
+ ResolvedSlot,
67
+ SearchParams,
68
+ SlotRecord,
69
+ SlotRouteRecord,
70
+ TemplateModule,
71
+ TwitterCard,
72
+ } from "./internal/runtime.js";
73
+
74
+ export { Link, RouteView, RouterProvider, routerView } from "./internal/runtime.js";
75
+
76
+ export {
77
+ ForbiddenError,
78
+ NotFoundError,
79
+ RedirectError,
80
+ UnauthorizedError,
81
+ buildRoute,
82
+ forbidden,
83
+ hasClientPage,
84
+ matchRoute,
85
+ notFound,
86
+ parseSearch,
87
+ permanentRedirect,
88
+ redirect,
89
+ routeErrorStatus,
90
+ splitUrl,
91
+ unauthorized,
92
+ } from "./internal/routing.js";
93
+
94
+ export { resolveFailure, resolveMatch } from "./internal/resolve.js";
95
+
96
+ /**
97
+ * The route this server component is rendering in.
98
+ *
99
+ * `pending` is always `false`: a pending navigation is a fact about a browser
100
+ * that has asked for the next route and not received it, and a server renders
101
+ * the route it was asked for. `data` waits for a loader the router deferred,
102
+ * which is what the browser's `useRoute` does too, so a component that reads
103
+ * the data suspends at the same boundary on both sides.
104
+ */
105
+ export hook useRoute(): RouteInfo {
106
+ const route = serverRoute("useRoute");
107
+ return {
108
+ path: route.path,
109
+ pathname: route.pathname,
110
+ params: route.params,
111
+ searchParams: route.searchParams,
112
+ data: route.deferred == null ? route.data : use(route.deferred),
113
+ pending: false,
114
+ };
115
+ }
116
+
117
+ /** The current page's loader data; see `useRoute` for what a deferred loader does. */
118
+ export hook useLoaderData(): mixed {
119
+ const route = serverRoute("useLoaderData");
120
+ return route.deferred == null ? route.data : use(route.deferred);
121
+ }
122
+
123
+ /**
124
+ * A refusal: navigation is the browser's.
125
+ *
126
+ * Thrown rather than handed back as methods that do nothing, because a server
127
+ * component that called `router.push` in response to something would be
128
+ * waiting for a navigation that cannot happen, and nothing would say so.
129
+ */
130
+ export hook useRouter(): Router {
131
+ throw new Error(
132
+ "@uniflowed/router: useRouter() was called in a Server Component, and navigation happens " +
133
+ "in the browser. Call it from a client component — a module that opens with the use " +
134
+ "client directive — or render a <Link>, which is one, and which a Server Component can " +
135
+ "render.",
136
+ );
137
+ }
138
+
139
+ /**
140
+ * Head elements a server component contributes, with the route's
141
+ * `metadataBase` applied to the relative URLs it names.
142
+ *
143
+ * The same component the browser's `useSeo` renders, and the same base: the one
144
+ * the root layout declared, read from the route rather than from context.
145
+ */
146
+ export hook useSeo(seo: Metadata): React.Node {
147
+ const route = serverRoute("useSeo");
148
+ const base = seo.metadataBase ?? route.metadata.metadataBase;
149
+ return <Head metadata={base == null ? seo : { ...seo, metadataBase: base }} />;
150
+ }
151
+
152
+ /** Whether the app is being rendered on the server: here, always. */
153
+ export hook useIsServer(): boolean {
154
+ return true;
155
+ }
package/server.js CHANGED
@@ -46,7 +46,15 @@ import {
46
46
  resolveMatch,
47
47
  } from "./internal/runtime.js";
48
48
 
49
- import { type StreamDiagnostic, streamReporter } from "./internal/inspector.js";
49
+ import { type StreamDiagnostic, type StreamRecord, streamReporter } from "./internal/inspector.js";
50
+
51
+ import {
52
+ type ClientModuleLoader,
53
+ installServerModules,
54
+ readPayload,
55
+ } from "./internal/flight-ssr.js";
56
+ import { FLIGHT_CONTENT_TYPE, flightUrl } from "./internal/flight.js";
57
+ import type { FlightRenderer } from "./rsc.js";
50
58
 
51
59
  /** Asset URLs to reference from the document. */
52
60
  export type RenderAssets = {|
@@ -101,6 +109,31 @@ export type PrerenderResult = {|
101
109
  readonly headers?: { readonly [string]: string },
102
110
  /** The exception this render fell back to its error boundary for; see [`RenderResult`]. */
103
111
  readonly error?: mixed,
112
+ /**
113
+ * The Flight payload the document was rendered from, for a document React
114
+ * Server Components rendered.
115
+ *
116
+ * `uf build` writes it beside the document as the route's payload file, so a
117
+ * browser that navigates to a prerendered route fetches a file rather than
118
+ * asking a server — which is what makes a static host able to serve client
119
+ * navigation at all. Absent for a document rendered from its modules.
120
+ */
121
+ readonly payload?: Uint8Array,
122
+ |};
123
+
124
+ /**
125
+ * A route's payload, as a browser navigating to it is answered.
126
+ *
127
+ * `stream` is `null` for a redirect, whose `location` is already the target's
128
+ * payload URL when the target is on this origin: `fetch` follows it and lands
129
+ * on a payload.
130
+ */
131
+ export type FlightResponse = {|
132
+ readonly status: number,
133
+ readonly headers: { readonly [string]: string },
134
+ readonly stream: ReadableStream<Uint8Array> | null,
135
+ /** The exception the route resolved to its error boundary for; see [`RenderResult`]. */
136
+ readonly error?: mixed,
104
137
  |};
105
138
 
106
139
  /** What a host may tell the renderer about one request. */
@@ -258,6 +291,14 @@ export type Renderer = {|
258
291
  assets: RenderAssets,
259
292
  options?: RenderOptions,
260
293
  ) => Promise<PrerenderResult>,
294
+ /**
295
+ * A route's payload, for a browser that is navigating rather than loading a
296
+ * document. Only a renderer for React Server Components has one.
297
+ */
298
+ readonly flight?: (
299
+ url: string,
300
+ options?: {| readonly onError?: (error: mixed) => void |},
301
+ ) => Promise<FlightResponse>,
261
302
  |};
262
303
 
263
304
  export function createRenderer(options: {|
@@ -430,6 +471,392 @@ export function createRenderer(options: {|
430
471
  return { render, prerender };
431
472
  }
432
473
 
474
+ /** What `virtual:uf/server` hands the renderer for React Server Components. */
475
+ export type DocumentRendererOptions = {|
476
+ readonly App: React.ComponentType<AppProps>,
477
+ /** The Flight renderer, from the module graph resolved under `react-server`. */
478
+ readonly renderFlight: FlightRenderer,
479
+ /** The server copy of the client module at a browser chunk URL. */
480
+ readonly loadClientModule: ClientModuleLoader,
481
+ |};
482
+
483
+ /**
484
+ * The renderer for an application rendered by React Server Components.
485
+ *
486
+ * [`createRenderer`] renders a route's modules into HTML. This one renders no
487
+ * route module at all: the Flight renderer (`./rsc.js`), in the module graph
488
+ * resolved under `react-server`, renders the route into a payload, and this
489
+ * reads that payload with React's own Flight client and renders what comes
490
+ * out. The same bytes are written into the document as they arrive, so the
491
+ * browser hydrates the tree the server rendered rather than rendering the
492
+ * route's modules a second time — which is what keeps a Server Component's
493
+ * module out of the browser. See ubugeeei-prod/uf#519.
494
+ *
495
+ * The contract with a host is [`Renderer`]'s, unchanged: `render` resolves
496
+ * when the shell is ready with a status and a body, `prerender` resolves with a
497
+ * finished document, and `error` is the exception a route fell back to its
498
+ * error boundary for. It adds `flight`, which answers a browser that is
499
+ * navigating, and `prerender` adds the payload a static host serves for one.
500
+ *
501
+ * # When the shell throws
502
+ *
503
+ * The HTML renderer only ever sees React's serialisation of what a Server
504
+ * Component threw — in a build, a sentence and a digest — and resolving the
505
+ * error boundary needs the exception itself: `forbidden()` is a 403 and a
506
+ * `RedirectError` is a redirect. So the first exception the Flight renderer
507
+ * reports is kept, as thrown, and it is what the error route is resolved for.
508
+ * An exception only the HTML renderer saw — a client component that threw
509
+ * while it rendered on the server — is resolved for as it is.
510
+ */
511
+ export function createDocumentRenderer(options: DocumentRendererOptions): Renderer {
512
+ const { App, renderFlight } = options;
513
+ installServerModules(options.loadClientModule);
514
+
515
+ /**
516
+ * The document for one payload: React's Flight client reads one copy of the
517
+ * stream while the other copy is written into the document.
518
+ */
519
+ async function documentOf(
520
+ stream: ReadableStream<Uint8Array>,
521
+ url: string,
522
+ assets: RenderAssets,
523
+ settings: {|
524
+ readonly onError: (error: mixed) => void,
525
+ readonly transformHead?: (html: string) => Promise<string>,
526
+ readonly onStream?: (record: StreamRecord) => void,
527
+ |},
528
+ ): Promise<DocumentBody> {
529
+ const [forHtml, forBrowser] = stream.tee();
530
+ try {
531
+ return await renderDocument(<App url={url} flight={readPayload(forHtml)} />, {
532
+ shell: shellFor(assets),
533
+ onError: settings.onError,
534
+ transformHead: settings.transformHead,
535
+ onStream: settings.onStream,
536
+ payload: forBrowser,
537
+ });
538
+ } catch (error) {
539
+ void forBrowser.cancel();
540
+ throw error;
541
+ }
542
+ }
543
+
544
+ async function render(
545
+ url: string,
546
+ assets: RenderAssets,
547
+ settings?: RenderOptions,
548
+ ): Promise<RenderResult> {
549
+ const report = settings?.onError ?? (() => {});
550
+ const send = settings?.onStream;
551
+ const onStream = send == null ? undefined : streamReporter(url, send);
552
+ const transformHead = settings?.transformHead;
553
+ const ledger = createErrorLedger();
554
+
555
+ // Held until the shell is known to have survived, for the reason
556
+ // [`createRenderer`] holds them — and dropped rather than reported once the
557
+ // render they came from has been given up for the error boundary. That
558
+ // render goes on reporting while it stops, and nothing it says then is
559
+ // about the response.
560
+ let streaming = false;
561
+ let abandoned = false;
562
+ let held: Array<mixed> = [];
563
+ const onError = (error: mixed) => {
564
+ if (abandoned) {
565
+ return;
566
+ }
567
+ if (streaming) {
568
+ report(error);
569
+ return;
570
+ }
571
+ held.push(error);
572
+ };
573
+
574
+ const stop = new AbortController();
575
+ const rendered = await renderFlight(url, {
576
+ onError: (error: mixed) => {
577
+ onError(error);
578
+ return ledger.record(error);
579
+ },
580
+ signal: stop.signal,
581
+ });
582
+ if (rendered.kind === "redirect") {
583
+ return redirectDocument(new RedirectError(rendered.location, rendered.status === 308));
584
+ }
585
+ let status = rendered.status;
586
+ let failure = rendered.failure;
587
+ let body: DocumentBody;
588
+ try {
589
+ body = await documentOf(rendered.stream, url, assets, {
590
+ onError: (error: mixed) => {
591
+ if (!ledger.isCopy(error)) {
592
+ onError(error);
593
+ }
594
+ },
595
+ transformHead,
596
+ onStream,
597
+ });
598
+ streaming = true;
599
+ for (const error of held) {
600
+ report(error);
601
+ }
602
+ held = [];
603
+ } catch (error) {
604
+ abandoned = true;
605
+ held = [];
606
+ stop.abort();
607
+ const cause = ledger.originalOf(error);
608
+ if (cause instanceof RedirectError) {
609
+ return redirectDocument(cause);
610
+ }
611
+ // Deliberately not caught again: this render is the boundary's own, and a
612
+ // boundary that throws has nothing left to answer with. It reaches
613
+ // `uf dev`'s overlay and fails `uf build`'s route, which is where somebody
614
+ // can fix it.
615
+ const recovered = await renderFlight(url, {
616
+ failure: { error: cause },
617
+ onError: (late: mixed) => {
618
+ report(late);
619
+ return ledger.record(late);
620
+ },
621
+ });
622
+ if (recovered.kind === "redirect") {
623
+ return redirectDocument(new RedirectError(recovered.location, recovered.status === 308));
624
+ }
625
+ status = recovered.status;
626
+ failure = recovered.failure;
627
+ try {
628
+ body = await documentOf(recovered.stream, url, assets, {
629
+ onError: (late: mixed) => {
630
+ if (!ledger.isCopy(late)) {
631
+ report(late);
632
+ }
633
+ },
634
+ transformHead,
635
+ onStream,
636
+ });
637
+ } catch (thrown) {
638
+ // The boundary's own render threw; see `prerender` for why what leaves
639
+ // is the exception as thrown rather than React's copy of it.
640
+ throw ledger.originalOf(thrown);
641
+ }
642
+ }
643
+
644
+ return { status, pipe: body.pipe, stream: body.stream, text: body.text, error: failure };
645
+ }
646
+
647
+ async function prerender(
648
+ url: string,
649
+ assets: RenderAssets,
650
+ settings?: RenderOptions,
651
+ ): Promise<PrerenderResult> {
652
+ const report = settings?.onError ?? (() => {});
653
+ const ledger = createErrorLedger();
654
+ // The Flight renderer's copy of an exception is the one reported, because it
655
+ // is the exception as thrown; the HTML renderer's copy of the same one is
656
+ // recognised by its digest and dropped. See [`createErrorLedger`].
657
+ const onServerError = (error: mixed): string => {
658
+ report(error);
659
+ return ledger.record(error);
660
+ };
661
+ const onHtmlError = (error: mixed) => {
662
+ if (!ledger.isCopy(error)) {
663
+ report(error);
664
+ }
665
+ };
666
+
667
+ // `defer: false`, for the reason [`createRenderer`]'s prerender passes it:
668
+ // a file has no fallback to show first.
669
+ const rendered = await renderFlight(url, { defer: false, onError: onServerError });
670
+ if (rendered.kind === "redirect") {
671
+ return redirectResult(
672
+ redirectDocument(new RedirectError(rendered.location, rendered.status === 308)),
673
+ );
674
+ }
675
+ let status = rendered.status;
676
+ let failure = rendered.failure;
677
+ let payload: Uint8Array;
678
+ let html: string;
679
+ try {
680
+ // Inside the `try`, with the document: a payload the Flight renderer
681
+ // could not finish — a function handed to a client component, say —
682
+ // fails the stream itself, and that is this route's failure like any
683
+ // other.
684
+ payload = await bytesOf(rendered.stream);
685
+ html = await staticDocumentOf(payload, url, assets, onHtmlError);
686
+ } catch (error) {
687
+ const cause = ledger.originalOf(error);
688
+ if (cause instanceof RedirectError) {
689
+ return redirectResult(redirectDocument(cause));
690
+ }
691
+ const recovered = await renderFlight(url, {
692
+ defer: false,
693
+ failure: { error: cause },
694
+ onError: onServerError,
695
+ });
696
+ if (recovered.kind === "redirect") {
697
+ return redirectResult(
698
+ redirectDocument(new RedirectError(recovered.location, recovered.status === 308)),
699
+ );
700
+ }
701
+ status = recovered.status;
702
+ failure = recovered.failure;
703
+ try {
704
+ payload = await bytesOf(recovered.stream);
705
+ html = await staticDocumentOf(payload, url, assets, onHtmlError);
706
+ } catch (thrown) {
707
+ // The boundary's own render threw, and nothing is left to answer with.
708
+ // What leaves is the exception as thrown rather than the copy React's
709
+ // client rebuilt from its row, whose message in a build says only that
710
+ // the real one was omitted — which is all `uf build` would then print.
711
+ throw ledger.originalOf(thrown);
712
+ }
713
+ }
714
+ return { status, html, error: failure, payload };
715
+ }
716
+
717
+ /** A finished document for a finished payload, with the payload written into it. */
718
+ function staticDocumentOf(
719
+ payload: Uint8Array,
720
+ url: string,
721
+ assets: RenderAssets,
722
+ onError: (error: mixed) => void,
723
+ ): Promise<string> {
724
+ return prerenderDocument(<App url={url} flight={readPayload(streamOf(payload))} />, {
725
+ shell: shellFor(assets),
726
+ onError,
727
+ payload: streamOf(payload),
728
+ });
729
+ }
730
+
731
+ async function flight(
732
+ url: string,
733
+ settings?: {| readonly onError?: (error: mixed) => void |},
734
+ ): Promise<FlightResponse> {
735
+ const rendered = await renderFlight(url, { onError: settings?.onError });
736
+ if (rendered.kind === "redirect") {
737
+ const { location } = rendered;
738
+ // A redirect on this origin points at its target's payload, so the
739
+ // browser's `fetch` follows it and lands on one. A redirect elsewhere is
740
+ // left as written: what the browser finds there is not a payload, and it
741
+ // loads the URL as a document instead.
742
+ const onThisOrigin = location.startsWith("/") && !location.startsWith("//");
743
+ return {
744
+ status: rendered.status,
745
+ headers: { location: onThisOrigin ? flightUrl(location) : location },
746
+ stream: null,
747
+ };
748
+ }
749
+ return {
750
+ status: rendered.status,
751
+ headers: { "content-type": FLIGHT_CONTENT_TYPE },
752
+ stream: rendered.stream,
753
+ error: rendered.failure,
754
+ };
755
+ }
756
+
757
+ return { render, prerender, flight };
758
+ }
759
+
760
+ /**
761
+ * Pairs each exception the Flight renderer reports with the copy of it the HTML
762
+ * renderer sees.
763
+ *
764
+ * A Server Component that throws is seen twice. The Flight renderer catches the
765
+ * exception as it was thrown and writes an error row where that part of the
766
+ * tree would have been; React's Flight client reads the row and throws an error
767
+ * rebuilt from it, which is what the HTML renderer sees at the same spot — and
768
+ * in a build, that error's message is a sentence saying the real one was
769
+ * omitted. Reporting both tells a host about one failure twice, once uselessly.
770
+ * Resolving the error boundary for the rebuilt one is worse: `forbidden()`
771
+ * would become a 500 and `redirect()` an error page, because neither survives
772
+ * being rebuilt as a plain `Error`.
773
+ *
774
+ * The row carries a digest, and that is React's field for exactly this: what
775
+ * the Flight renderer's `onError` returns is written into the row and set on the
776
+ * rebuilt error as `digest`. So every exception is recorded here under a digest
777
+ * of its own, and an error the HTML renderer reports with a recorded digest is a
778
+ * copy — dropped from the report, and exchanged for the original when the
779
+ * boundary is resolved.
780
+ *
781
+ * The digest reaches the browser inside the payload, and says nothing: it counts
782
+ * the exceptions one render has recorded.
783
+ */
784
+ function createErrorLedger(): ErrorLedger {
785
+ const originals: Map<string, mixed> = new Map();
786
+ const recorded = (error: mixed): string | null => {
787
+ const digest = digestOf(error);
788
+ return digest != null && originals.has(digest) ? digest : null;
789
+ };
790
+ return {
791
+ record(error: mixed): string {
792
+ const digest = `uf:${originals.size + 1}`;
793
+ originals.set(digest, error);
794
+ return digest;
795
+ },
796
+ isCopy(error: mixed): boolean {
797
+ return recorded(error) != null;
798
+ },
799
+ originalOf(error: mixed): mixed {
800
+ const digest = recorded(error);
801
+ return digest == null ? error : originals.get(digest);
802
+ },
803
+ };
804
+ }
805
+
806
+ /** See [`createErrorLedger`]. */
807
+ type ErrorLedger = {|
808
+ /** Keep an exception the Flight renderer reported, and return its digest. */
809
+ readonly record: (error: mixed) => string,
810
+ /** Whether an error the HTML renderer reported is the copy of a kept one. */
811
+ readonly isCopy: (error: mixed) => boolean,
812
+ /** The exception as thrown, for a copy; any other error, as it is. */
813
+ readonly originalOf: (error: mixed) => mixed,
814
+ |};
815
+
816
+ /** The digest React set on an error, when it set one. */
817
+ function digestOf(error: mixed): string | null {
818
+ if (error == null || typeof error !== "object") {
819
+ return null;
820
+ }
821
+ const tagged: { +digest?: mixed, ... } = (error: $FlowFixMe);
822
+ return typeof tagged.digest === "string" ? tagged.digest : null;
823
+ }
824
+
825
+ /** Every byte of a stream, as one array. */
826
+ async function bytesOf(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
827
+ const reader = stream.getReader();
828
+ const chunks: Array<Uint8Array> = [];
829
+ let total = 0;
830
+ while (true) {
831
+ const step = await reader.read();
832
+ if (step.done === true) {
833
+ break;
834
+ }
835
+ const chunk = step.value;
836
+ if (chunk != null) {
837
+ chunks.push(chunk);
838
+ total += chunk.byteLength;
839
+ }
840
+ }
841
+ const bytes = new Uint8Array(total);
842
+ let at = 0;
843
+ for (const chunk of chunks) {
844
+ bytes.set(chunk, at);
845
+ at += chunk.byteLength;
846
+ }
847
+ return bytes;
848
+ }
849
+
850
+ /** A stream of one array, read as many times as a caller constructs one. */
851
+ function streamOf(bytes: Uint8Array): ReadableStream<Uint8Array> {
852
+ return new ReadableStream({
853
+ start(controller) {
854
+ controller.enqueue(bytes);
855
+ controller.close();
856
+ },
857
+ });
858
+ }
859
+
433
860
  /** The exception a resolved route fell back to its error boundary for. */
434
861
  function renderFailure(resolved: ResolvedRoute): mixed {
435
862
  if (resolved.error == null) {