diff --git a/.changeset/weft-server-rpc-stylesheets.md b/.changeset/weft-server-rpc-stylesheets.md
new file mode 100644
index 0000000..39ea687
--- /dev/null
+++ b/.changeset/weft-server-rpc-stylesheets.md
@@ -0,0 +1,9 @@
+---
+'@e-cline/weft_server': patch
+---
+
+RPC answers bring their stylesheets: `RpcServer.respond` takes `~assets` (`virtual:weft/assets`, as
+`Page.respond` does), and an answer with elements in it sends the sheets of their classes, as a
+page's payload does. The client's call resolves once they've loaded, so what it renders never shows
+without its CSS. Without `~assets`, nothing changes. `Page.stylesOf` gives the stylesheets in an
+app's assets, as `Render` takes them.
diff --git a/docs/weft_css_plan.md b/docs/weft_css_plan.md
index 837f549..bb7bf02 100644
--- a/docs/weft_css_plan.md
+++ b/docs/weft_css_plan.md
@@ -59,7 +59,8 @@ for all of them). The decisions were taken as recommended (Decisions). The desig
first written, each followed by "As built" notes where the implementation went its own way or
settled what the design left open: phase 0's spikes under "Phase 0: what the spikes found" (after
Phases), then phase 1's after §2, phase 2's after §3, phases 3 and 4's after §4, phase 5's after §6,
-phase 6's after §7, and phase 7's after §8. What's left to do is under Follow-ups, at the end.
+phase 6's after §7, and phase 7's after §8. What's left to do is under Follow-ups, at the end;
+follow-up 1 (RPC responses' sheets) is done since, with its own "As built" note there.
The things most worth knowing, for whoever works on this next:
@@ -468,8 +469,8 @@ other way (a `Weft.Lazy` around an `import()` in a client-only app), and develop
stylesheet links for the `C` rows written so far, the entrypoints' `modulepreload`s (which used to
be at the start of the body), and `
`. Payload responses (navigation) get `C` rows
too. A page without HTML (`html=false`) closes its head at once, as before: its sheets are the
- client's to load. RPC responses (`RpcServer`) don't send sheets yet: an element an RPC returns is
- styled only by what's already loaded (phase 4).
+ client's to load. RPC responses (`RpcServer`) didn't send sheets yet: an element an RPC returned
+ was styled only by what had loaded already (follow-up 1 added them).
- **The layer order is in every sheet**, `@layer weft.base, weft.components;` first
(`Extract.layerOrder`), not in the page's head: the first stylesheet to name a layer fixes its
place, whichever that is, and `weftCss()` apps, with no page of Weft's, need it as much.
@@ -895,12 +896,26 @@ Settled in review: each recommendation below was taken.
## Follow-ups
-What's left, most useful first. The first two are the gaps in what's sent:
+What's left, most useful first. The first two are the gaps in what's sent (the first is done):
+
+1. **Sheets for RPC responses** (done). `RpcServer` encoded without `~styles`, so an element an RPC
+ returned (`weft_shop`'s "Load more" cards) was styled only by sheets already loaded.
+
+ **As built.** `RpcServer.respond` takes `~assets` (an app's route passes `virtual:weft/assets`,
+ as it does to `Page.respond`), and encodes the answer with their styles (`Page.stylesOf`, now
+ public): the `C` rows of what the answer renders come before the rows needing them, as in a
+ page's payload. The client needed nothing: `Rpc.call` decodes with `Decoder.valueFromStream`,
+ which already holds the root back until its sheets load, so the call resolves only then, and what
+ the caller renders with it is styled; a later row (a card's stock) waits as any other does. On
+ the router rather than `respond` would have meant giving the assets to the module that lists the
+ handlers, which has no reason to import them. `weft_shop`'s cards gained a sale badge, in a file
+ of its own (`SaleBadge`), which is what the test needs: sorted dearest first, the shoes' first
+ page has nothing on sale and "Load more" brings two. Tests: `weft_server`'s `Sheets_test` ("RPCs,
+ with sheets": the answer held back until its sheet loads; a boundary in it keeping its fallback,
+ after the answer, until its later row's sheet loads; without assets, no sheets), and
+ `weft_shop`'s `Styles_test` (the grid keeps its old cards, "Loading…", until the badge's sheet
+ loads, and nothing is ever unstyled). Both fail with the styles left out.
-1. **Sheets for RPC responses**: `RpcServer` encodes without `~styles`, so an element an RPC returns
- (`weft_shop`'s "Load more" cards) is styled only by sheets already loaded. They are there in
- `weft_shop` (the grid's first cards brought them), but needn't be. Small: `RpcServer` given the
- assets, as `Page` is.
2. **Style modules loaded some other way** (a `Weft.Lazy` around an `import()`, in a `weft()` app)
bring no sheets: entrypoints' come with the rows using them, but nothing registers a module's own
when the client runs it (§4's "client code's own sheets"). Small.
diff --git a/examples/weft_notes/src/server/Server.res b/examples/weft_notes/src/server/Server.res
index 54c4393..99c20ee 100644
--- a/examples/weft_notes/src/server/Server.res
+++ b/examples/weft_notes/src/server/Server.res
@@ -50,7 +50,7 @@ let noteRoutes = [
),
// The RPCs (`Api`'s, answered by `ApiServer`'s handlers).
Route.post(list{"_weft", "rpc", ":id"}, async ctx =>
- await ApiServer.router->WeftServer.RpcServer.respond(ctx.request, ~id=id(ctx))
+ await ApiServer.router->WeftServer.RpcServer.respond(ctx.request, ~id=id(ctx), ~assets)
),
]
diff --git a/examples/weft_notes/src/server/Server.res.js b/examples/weft_notes/src/server/Server.res.js
index 1241d43..951a4fd 100644
--- a/examples/weft_notes/src/server/Server.res.js
+++ b/examples/weft_notes/src/server/Server.res.js
@@ -76,7 +76,7 @@ let noteRoutes = [
tl: /* [] */0
}
}
- }, async ctx => await RpcServer$WeftServer.respond(ApiServer$WeftNotes.router, ctx.request, RequestContext$Servy.paramOrThrow(ctx, "id")))
+ }, async ctx => await RpcServer$WeftServer.respond(ApiServer$WeftNotes.router, ctx.request, RequestContext$Servy.paramOrThrow(ctx, "id"), Assets))
];
let testRoutes = [
diff --git a/examples/weft_shop/src/server/SaleBadge.res b/examples/weft_shop/src/server/SaleBadge.res
new file mode 100644
index 0000000..2e0e8e6
--- /dev/null
+++ b/examples/weft_shop/src/server/SaleBadge.res
@@ -0,0 +1,33 @@
+@@weft.server
+
+/***
+A card's sale badge: how much a product on sale is off. Its styles are its own file's (a sheet of
+their own), so a page sends them only if it shows something on sale.
+*/
+
+open Types
+open WeftCss
+
+module Styles = {
+ let badge = Css.class({
+ display: InlineBlock,
+ padding: Pair(Px(2), Px(8)),
+ borderRadius: Px(999),
+ backgroundColor: Var(Theme.sale),
+ color: Named("white"),
+ fontSize: Px(12),
+ fontWeight: 600,
+ })
+}
+
+/** The badge for `summary`, if it's on sale. */
+@weft.component({kind: Server})
+let make = (~summary: summary) =>
+ switch summary.compareAt {
+ | Some(was) if was > summary.price =>
+ let off = Math.round(Int.toFloat(was - summary.price) /. Int.toFloat(was) *. 100.0)
+
diff --git a/examples/weft_shop/src/server/Ui.res.js b/examples/weft_shop/src/server/Ui.res.js
index c095b06..cfec528 100644
--- a/examples/weft_shop/src/server/Ui.res.js
+++ b/examples/weft_shop/src/server/Ui.res.js
@@ -14,6 +14,7 @@ import * as Theme$WeftShop from "../shared/Theme.res.js";
import * as Latency$WeftShop from "./Latency.res.js";
import * as Session$WeftShop from "./Session.res.js";
import * as CartBadge$WeftShop from "../client/CartBadge.res.js";
+import * as SaleBadge$WeftShop from "./SaleBadge.res.js";
import * as SearchBox$WeftShop from "../client/SearchBox.res.js";
import * as ShopShell$WeftShop from "../client/ShopShell.res.js";
import * as AccountMenu$WeftShop from "../client/AccountMenu.res.js";
@@ -279,6 +280,9 @@ function Ui$ProductCard(props) {
})
]
}),
+ Jsx$WeftDom.jsx(SaleBadge$WeftShop.make, {
+ summary: summary
+ }),
Jsx$WeftDom.Elements.jsxs("div", {
className: cardMeta,
children: [
diff --git a/examples/weft_shop/tests/Styles_test.res b/examples/weft_shop/tests/Styles_test.res
index 0fe5189..2b5fc9a 100644
--- a/examples/weft_shop/tests/Styles_test.res
+++ b/examples/weft_shop/tests/Styles_test.res
@@ -81,4 +81,36 @@ describe("the store's styles", () => {
await page->WeftTesting.waitFor(() => has(page, "[data-id=cart-title]"))
t->expect(page->WeftTesting.unstyled)->Expect.toEqual([])
})
+
+ testAsync(
+ "bring the sheets of what an RPC renders: \"Load more\" shows its cards once their sheets load",
+ async t => {
+ await app()->releaseAll
+ // Dearest first: the first page has nothing on sale, the second has two.
+ let page = app()->open_("/c/shoes?sort=price-desc")
+ await page->WeftTesting.waitFor(() => has(page, "[data-id=load-more]"))
+ await hydratedAsServed(page)
+ t->expect(has(page, "[data-id^=sale-]"))->Expect.toBe(false)
+ let head = headSheets(await page->WeftTesting.shell)
+ t->expect(head->hasSheet("server/SaleBadge"))->Expect.toBe(false)
+ t->expect(page->WeftTesting.stylesheets->hasSheet("server/SaleBadge"))->Expect.toBe(false)
+ page->WeftTesting.holdStylesheets(~matching=sheetOf("server/SaleBadge"))
+ page->WeftTesting.click("[data-id=load-more]")
+ await page->WeftTesting.waitFor(
+ () =>
+ page->WeftTesting.stylesheets->hasSheet("server/SaleBadge") ||
+ has(page, "[data-id=card-p6]"),
+ )
+ await page->WeftTesting.settle
+ // The answer is in, but for its badges' sheet: the grid stays as it was.
+ t->expect(text(page, "[data-id=load-more]"))->Expect.toBe("Loading…")
+ t->expect(has(page, "[data-id=card-p6]"))->Expect.toBe(false)
+ t->expect(page->WeftTesting.unstyled)->Expect.toEqual([])
+ page->WeftTesting.releaseStylesheets
+ await page->WeftTesting.waitFor(() => has(page, "[data-id=sale-p6]"))
+ t->expect(text(page, "[data-id=sale-p6]"))->Expect.toBe("20% off")
+ t->expect(has(page, "[data-id=sale-p3]"))->Expect.toBe(true)
+ t->expect(page->WeftTesting.unstyled)->Expect.toEqual([])
+ },
+ )
})
diff --git a/examples/weft_shop/tests/Styles_test.res.js b/examples/weft_shop/tests/Styles_test.res.js
index f768507..bec60ca 100644
--- a/examples/weft_shop/tests/Styles_test.res.js
+++ b/examples/weft_shop/tests/Styles_test.res.js
@@ -74,6 +74,34 @@ Vitest.describe("the store's styles", undefined, undefined, undefined, undefined
await WeftTesting.waitFor(page, undefined, () => ShopSupport$WeftShop.has(page, "[data-id=cart-title]"));
return t.expect(WeftTesting.unstyled(page)).toEqual([]);
});
+ Vitest.testAsync("bring the sheets of what an RPC renders: \"Load more\" shows its cards once their sheets load", undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, async t => {
+ await ShopSupport$WeftShop.releaseAll(app());
+ let page = ShopSupport$WeftShop.open_(app(), undefined, undefined, "/c/shoes?sort=price-desc");
+ await WeftTesting.waitFor(page, undefined, () => ShopSupport$WeftShop.has(page, "[data-id=load-more]"));
+ await ShopSupport$WeftShop.hydratedAsServed(page, undefined);
+ t.expect(ShopSupport$WeftShop.has(page, "[data-id^=sale-]")).toBe(false);
+ let head = headSheets(await WeftTesting.shell(page));
+ t.expect(hasSheet(head, "server/SaleBadge")).toBe(false);
+ t.expect(hasSheet(WeftTesting.stylesheets(page), "server/SaleBadge")).toBe(false);
+ WeftTesting.holdStylesheets(page, sheetOf("server/SaleBadge"));
+ WeftTesting.click(page, "[data-id=load-more]");
+ await WeftTesting.waitFor(page, undefined, () => {
+ if (hasSheet(WeftTesting.stylesheets(page), "server/SaleBadge")) {
+ return true;
+ } else {
+ return ShopSupport$WeftShop.has(page, "[data-id=card-p6]");
+ }
+ });
+ await WeftTesting.settle(page);
+ t.expect(ShopSupport$WeftShop.text(page, "[data-id=load-more]")).toBe("Loading…");
+ t.expect(ShopSupport$WeftShop.has(page, "[data-id=card-p6]")).toBe(false);
+ t.expect(WeftTesting.unstyled(page)).toEqual([]);
+ WeftTesting.releaseStylesheets(page);
+ await WeftTesting.waitFor(page, undefined, () => ShopSupport$WeftShop.has(page, "[data-id=sale-p6]"));
+ t.expect(ShopSupport$WeftShop.text(page, "[data-id=sale-p6]")).toBe("20% off");
+ t.expect(ShopSupport$WeftShop.has(page, "[data-id=sale-p3]")).toBe(true);
+ return t.expect(WeftTesting.unstyled(page)).toEqual([]);
+ });
});
export {
diff --git a/packages/weft_server/src/server/Page.res.js b/packages/weft_server/src/server/Page.res.js
index bb9d0c9..90e0231 100644
--- a/packages/weft_server/src/server/Page.res.js
+++ b/packages/weft_server/src/server/Page.res.js
@@ -368,6 +368,7 @@ function respond(request, element, assets, html, title, head, onError) {
}
export {
+ stylesOf,
toStream,
rowScript,
wantsPayload,
diff --git a/packages/weft_server/src/server/Page.resi b/packages/weft_server/src/server/Page.resi
index 2f7d577..2fca7e8 100644
--- a/packages/weft_server/src/server/Page.resi
+++ b/packages/weft_server/src/server/Page.resi
@@ -58,6 +58,12 @@ type assets = {
entrypointSheets?: dict
>,
}
+/**
+The app's stylesheets in `assets`, as `Render` takes them (its `styles`): `None` if the app has
+none.
+*/
+let stylesOf: assets => option
+
/**
The HTML page for `element`, streamed as it renders. `url` (the request's path and query, `"/"` by
default) is what the server-side page says it is; `html=false` leaves the HTML out. `title` and
diff --git a/packages/weft_server/src/server/RpcServer.res b/packages/weft_server/src/server/RpcServer.res
index 93ff63b..7d80e38 100644
--- a/packages/weft_server/src/server/RpcServer.res
+++ b/packages/weft_server/src/server/RpcServer.res
@@ -199,7 +199,7 @@ let readBody = async (request, ~limit) =>
}
}
-let respond = async (router, request: Request.t, ~id) =>
+let respond = async (router, request: Request.t, ~id, ~assets=?) =>
switch router.handlers->Map.get(id) {
| None => refuse(404, `There's no RPC ${id}.`)
| Some(handler) =>
@@ -232,6 +232,7 @@ let respond = async (router, request: Request.t, ~id) =>
toUnknown(root),
~onError=router.onError,
~signal=toSignal(request.signal),
+ ~styles=?assets->Option.flatMap(Page.stylesOf),
),
)
| exception error => payloadResponse(500, failure(router, error))
diff --git a/packages/weft_server/src/server/RpcServer.res.js b/packages/weft_server/src/server/RpcServer.res.js
index 4e95de1..7459d8f 100644
--- a/packages/weft_server/src/server/RpcServer.res.js
+++ b/packages/weft_server/src/server/RpcServer.res.js
@@ -5,6 +5,7 @@ import * as Env$Common from "@e-cline/common/src/Env.res.js";
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
+import * as Page$WeftServer from "./Page.res.js";
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
@@ -224,7 +225,7 @@ async function readBody(request, limit) {
}
}
-async function respond(router, request, id) {
+async function respond(router, request, id, assets) {
let handler = router.handlers.get(id);
if (handler === undefined) {
return refuse(404, `There's no RPC ` + id + `.`);
@@ -277,7 +278,7 @@ async function respond(router, request, id) {
return payloadResponse(500, failure(router, error));
}
if (exit$2 === 3) {
- return payloadResponse(val[0], Render$WeftServer.valueToStream(val[1], router.onError, Primitive_option.some(request.signal), undefined));
+ return payloadResponse(val[0], Render$WeftServer.valueToStream(val[1], router.onError, Primitive_option.some(request.signal), Stdlib_Option.flatMap(assets, Page$WeftServer.stylesOf)));
}
}
}
diff --git a/packages/weft_server/src/server/RpcServer.resi b/packages/weft_server/src/server/RpcServer.resi
index 79255df..2f21cd9 100644
--- a/packages/weft_server/src/server/RpcServer.resi
+++ b/packages/weft_server/src/server/RpcServer.resi
@@ -12,10 +12,13 @@ let addComment = WeftServer.RpcServer.handler(Api.addComment, async (request, ~h
let router = WeftServer.RpcServer.router([addComment])
// src/Server.res
+@module("virtual:weft/assets") external assets: WeftServer.Page.assets = "default"
+
Route.post(list{"_weft", "rpc", ":id"}, async ctx =>
await ApiServer.router->WeftServer.RpcServer.respond(
ctx.request,
~id=ctx->RequestContext.paramOrThrow("id"),
+ ~assets,
)
)
```
@@ -68,8 +71,13 @@ The response to `request`, a call of the RPC `id`. In order:
6. otherwise the handler's result is the answer, as a payload, with `Payload.cacheControl`. A
handler that throws is a 500 (`Failed`), with the digest `onError` gave it and, in development
only, its message.
+
+With `assets` (`virtual:weft/assets`, as `Page.respond` is given), the answer brings the
+stylesheets of the elements in it, as a page's payload does (see `Render`'s `styles`), and the
+client's call resolves once they've loaded, so what it renders never shows without its CSS. Without
+them, an element in the answer is styled only by what the page has loaded already.
*/
-let respond: (router, Request.t, ~id: string) => promise
+let respond: (router, Request.t, ~id: string, ~assets: Page.assets=?) => promise
/** The IDs the router answers, sorted. */
let ids: router => array
diff --git a/packages/weft_server/tests/Rpc_test.res.js b/packages/weft_server/tests/Rpc_test.res.js
index 32a7959..a5e9de9 100644
--- a/packages/weft_server/tests/Rpc_test.res.js
+++ b/packages/weft_server/tests/Rpc_test.res.js
@@ -155,7 +155,7 @@ function post(idOpt, headersOpt, methodOpt, body) {
method: method,
headers: headers,
body: body
- })), id);
+ })), id, undefined);
}
function contentType(response) {
@@ -271,7 +271,7 @@ Vitest.describe("the router", undefined, undefined, undefined, undefined, undefi
method: "POST",
headers: headers,
body: `"a"`
- }), "notes.small");
+ }), "notes.small", undefined);
};
t.expect((await call("http://app.test")).status).toBe(200);
return t.expect((await call(origin)).status).toBe(403);
@@ -301,7 +301,7 @@ Vitest.describe("the router", undefined, undefined, undefined, undefined, undefi
let response = await RpcServer$WeftServer.respond(router, request(origin, {
"content-type": "application/json",
"x-weft-rpc": "1"
- }, bytes), "notes.small");
+ }, bytes), "notes.small", undefined);
return t.expect(response.status).toBe(400);
});
Vitest.testAsync("answers a handler that throws with a 500 and an error row, through onError", undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, async t => {
@@ -500,7 +500,7 @@ function fakeFetch(url, init) {
headers: init.headers,
body: init.body,
signal: init.signal
- }), id);
+ }), id, undefined);
let signal = init.signal;
if (signal === undefined) {
return response;
diff --git a/packages/weft_server/tests/Sheets_test.res b/packages/weft_server/tests/Sheets_test.res
index b838ba7..410d82a 100644
--- a/packages/weft_server/tests/Sheets_test.res
+++ b/packages/weft_server/tests/Sheets_test.res
@@ -333,3 +333,105 @@ describe("Navigation, with sheets", () => {
t->expect(TestRenderer.findById(renderer, "styled")->Option.isSome)->Expect.toBe(true)
})
})
+
+// An RPC's answer, through the router in process: its elements bring their sheets, as a page's do.
+
+let cards: Rpc.t = Rpc.make(
+ ~id="shop.cards",
+ ~request=Rpc.Codec.string,
+)
+/** An answer whose stock comes later, in a row of its own: once a test resolves `stockReady`. */
+let stocked: Rpc.t = Rpc.make(
+ ~id="shop.stocked",
+ ~request=Rpc.Codec.string,
+)
+let stockReady = ref(Promise.withResolvers())
+
+let cardsRouter = RpcServer.router([
+ RpcServer.handler(cards, async (label, ~http as _) => Ok(
+ {text(label)}
,
+ )),
+ RpcServer.handler(stocked, async (label, ~http as _) => Ok(
+
+ {text(label)}
+
+
+
+
+
+
,
+ )),
+])
+
+type requestInit = {method: string, headers: dict, body: string}
+@new external makeRequest: (string, requestInit) => Request.t = "Request"
+
+let globalFetch: unknown = %raw(`globalThis.fetch`)
+let setFetch: unknown => unit = %raw(`fetch => { globalThis.fetch = fetch }`)
+external toUnknown: 'a => unknown = "%identity"
+
+/** A `fetch` answering RPCs with `cardsRouter`, given `assets` (or none). */
+let fetchWith = (~assets=?) =>
+ (url: string, init: requestInit) =>
+ cardsRouter->RpcServer.respond(
+ makeRequest(`http://shop.test${url}`, init),
+ ~id=url->String.split("/")->Array.at(-1)->Option.getOr(""),
+ ~assets?,
+ )
+
+describe("RPCs, with sheets", () => {
+ afterEach(() => setFetch(globalFetch))
+
+ testAsync("hold the answer back until its elements' sheets have loaded", async t => {
+ setFetch(toUnknown(fetchWith(~assets=pageAssets())))
+ let sheets = FakeSheets.make()
+ let answer = Rpc.call(cards, ~client=clientWith(sheets), "more")
+ t->expect(await isSettled(answer))->Expect.toBe(false)
+ t->expect(sheets.asked)->Expect.toEqual(["/d.css"])
+ sheets->FakeSheets.release(["/d.css"])
+ switch await answer {
+ | Ok(element) =>
+ let renderer = TestRenderer.create()
+ TestRenderer.render(renderer, element)
+ t->expect(TestRenderer.findById(renderer, "card")->Option.isSome)->Expect.toBe(true)
+ | Error(_) => JsError.throwWithMessage("the RPC failed")
+ }
+ })
+
+ testAsync(
+ "keep a boundary's fallback until a later row's sheets have loaded, after the answer",
+ async t => {
+ setFetch(toUnknown(fetchWith(~assets=pageAssets())))
+ stockReady := Promise.withResolvers()
+ let sheets = FakeSheets.make()
+ sheets->FakeSheets.release(["/cycle.css", "/base.css", "/a.css"])
+ // The answer's own sheets have loaded: the call resolves at once, its stock still to come.
+ let answer = Rpc.call(stocked, ~client=clientWith(sheets), "more")
+ t->expect(await isSettled(answer))->Expect.toBe(true)
+ let renderer = TestRenderer.create()
+ switch await answer {
+ | Ok(element) => TestRenderer.render(renderer, element)
+ | Error(_) => JsError.throwWithMessage("the RPC failed")
+ }
+ await settle()
+ t->expect(TestRenderer.toString(renderer))->Expect.String.toContain("checking")
+ stockReady.contents.resolve()
+ await settle()
+ // The stock's row is in, but its sheet isn't: the fallback stays.
+ t->expect(sheets.asked)->Expect.toEqual(["/d.css"])
+ t->expect(TestRenderer.toString(renderer))->Expect.String.toContain("checking")
+ t->expect(TestRenderer.findById(renderer, "stock")->Option.isSome)->Expect.toBe(false)
+ sheets->FakeSheets.release(["/d.css"])
+ await settle()
+ t->expect(TestRenderer.findById(renderer, "stock")->Option.isSome)->Expect.toBe(true)
+ },
+ )
+
+ testAsync("without the assets, send no sheets, and wait for none", async t => {
+ setFetch(toUnknown(fetchWith()))
+ let sheets = FakeSheets.make()
+ let answer = Rpc.call(cards, ~client=clientWith(sheets), "more")
+ t->expect(await isSettled(answer))->Expect.toBe(true)
+ t->expect(sheets.asked)->Expect.toEqual([])
+ })
+})
diff --git a/packages/weft_server/tests/Sheets_test.res.js b/packages/weft_server/tests/Sheets_test.res.js
index c39eb18..ba2e731 100644
--- a/packages/weft_server/tests/Sheets_test.res.js
+++ b/packages/weft_server/tests/Sheets_test.res.js
@@ -1,10 +1,13 @@
// Generated by ReScript, PLEASE EDIT WITH CARE
import * as Vitest from "rescript-vitest/src/Vitest.res.js";
+import * as Vitest$1 from "vitest";
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
import * as TestJsx$Weft from "@e-cline/weft/src/testing/TestJsx.res.js";
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
import * as Suspense$Weft from "@e-cline/weft/src/shared/Suspense.res.js";
+import * as Rpc$WeftServer from "../src/shared/Rpc.res.js";
+import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
import * as Page$WeftServer from "../src/server/Page.res.js";
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
import * as Render$WeftServer from "../src/server/Render.res.js";
@@ -13,6 +16,8 @@ import * as Decoder$WeftServer from "../src/shared/Decoder.res.js";
import * as Payload$WeftServer from "../src/shared/Payload.res.js";
import * as Support$WeftServer from "./Support.res.js";
import * as Testing$WeftServer from "../src/testing/Testing.res.js";
+import * as RpcCodec$WeftServer from "../src/shared/RpcCodec.res.js";
+import * as RpcServer$WeftServer from "../src/server/RpcServer.res.js";
import * as Navigation$WeftServer from "../src/shared/Navigation.res.js";
import * as PageSupport$WeftServer from "./PageSupport.res.js";
@@ -402,6 +407,108 @@ Vitest.describe("Navigation, with sheets", undefined, undefined, undefined, unde
return t.expect(Stdlib_Option.isSome(TestRenderer$Weft.findById(renderer, "styled"))).toBe(true);
}));
+let cards = Rpc$WeftServer.make("shop.cards", RpcCodec$WeftServer.string, undefined, undefined);
+
+let stocked = Rpc$WeftServer.make("shop.stocked", RpcCodec$WeftServer.string, undefined, undefined);
+
+let stockReady = {
+ contents: Promise.withResolvers()
+};
+
+let cardsRouter = RpcServer$WeftServer.router([
+ RpcServer$WeftServer.handler(cards, async (label, param) => ({
+ TAG: "Ok",
+ _0: TestJsx$Weft.Elements.jsx("p", {
+ id: "card",
+ className: "D-z_wdddddd",
+ children: Support$WeftServer.text(label)
+ })
+ })),
+ RpcServer$WeftServer.handler(stocked, async (label, param) => ({
+ TAG: "Ok",
+ _0: TestJsx$Weft.Elements.jsxs("div", {
+ id: "card",
+ className: "A-x_waaaaaa",
+ children: [
+ Support$WeftServer.text(label),
+ TestJsx$Weft.jsx(Suspense$Weft.make, {
+ fallback: Support$WeftServer.text("checking"),
+ children: TestJsx$Weft.jsx(PageSupport$WeftServer.Later.make, {
+ ready: stockReady.contents.promise,
+ children: TestJsx$Weft.Elements.jsx("i", {
+ id: "stock",
+ className: "D-z_wdddddd"
+ })
+ })
+ })
+ ]
+ })
+ }))
+], undefined, undefined, undefined);
+
+let globalFetch = globalThis.fetch;
+
+let setFetch = (fetch => { globalThis.fetch = fetch });
+
+function fetchWith(assets) {
+ return (url, init) => RpcServer$WeftServer.respond(cardsRouter, new Request(`http://shop.test` + url, init), Stdlib_Option.getOr(url.split("/").at(-1), ""), assets);
+}
+
+Vitest.describe("RPCs, with sheets", undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, () => {
+ Vitest$1.afterEach(() => setFetch(globalFetch));
+ Vitest.testAsync("hold the answer back until its elements' sheets have loaded", undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, async t => {
+ setFetch(fetchWith(pageAssets()));
+ let sheets = make();
+ let answer = Rpc$WeftServer.call(cards, clientWith(sheets), "more", undefined, undefined);
+ t.expect(await isSettled(answer)).toBe(false);
+ t.expect(sheets.asked).toEqual(["/d.css"]);
+ release(sheets, ["/d.css"]);
+ let element = await answer;
+ if (element.TAG !== "Ok") {
+ return Stdlib_JsError.throwWithMessage("the RPC failed");
+ }
+ let renderer = TestRenderer$Weft.create();
+ TestRenderer$Weft.render(renderer, element._0);
+ return t.expect(Stdlib_Option.isSome(TestRenderer$Weft.findById(renderer, "card"))).toBe(true);
+ });
+ Vitest.testAsync("keep a boundary's fallback until a later row's sheets have loaded, after the answer", undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, async t => {
+ setFetch(fetchWith(pageAssets()));
+ stockReady.contents = Promise.withResolvers();
+ let sheets = make();
+ release(sheets, [
+ "/cycle.css",
+ "/base.css",
+ "/a.css"
+ ]);
+ let answer = Rpc$WeftServer.call(stocked, clientWith(sheets), "more", undefined, undefined);
+ t.expect(await isSettled(answer)).toBe(true);
+ let renderer = TestRenderer$Weft.create();
+ let element = await answer;
+ if (element.TAG === "Ok") {
+ TestRenderer$Weft.render(renderer, element._0);
+ } else {
+ Stdlib_JsError.throwWithMessage("the RPC failed");
+ }
+ await Support$WeftServer.settle();
+ t.expect(TestRenderer$Weft.toString(renderer)).toContain("checking");
+ stockReady.contents.resolve();
+ await Support$WeftServer.settle();
+ t.expect(sheets.asked).toEqual(["/d.css"]);
+ t.expect(TestRenderer$Weft.toString(renderer)).toContain("checking");
+ t.expect(Stdlib_Option.isSome(TestRenderer$Weft.findById(renderer, "stock"))).toBe(false);
+ release(sheets, ["/d.css"]);
+ await Support$WeftServer.settle();
+ return t.expect(Stdlib_Option.isSome(TestRenderer$Weft.findById(renderer, "stock"))).toBe(true);
+ });
+ Vitest.testAsync("without the assets, send no sheets, and wait for none", undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, async t => {
+ setFetch(fetchWith(undefined));
+ let sheets = make();
+ let answer = Rpc$WeftServer.call(cards, clientWith(sheets), "more", undefined, undefined);
+ t.expect(await isSettled(answer)).toBe(true);
+ return t.expect(sheets.asked).toEqual([]);
+ });
+});
+
export {
styles,
payload,
@@ -411,5 +518,12 @@ export {
clientWith,
decoded,
isSettled,
+ cards,
+ stocked,
+ stockReady,
+ cardsRouter,
+ globalFetch,
+ setFetch,
+ fetchWith,
}
/* Not a pure module */