Skip to content

Commit ce2bb3f

Browse files
defer - expose internals and solidify subscriptions (#9760)
- export DeferredData class - add settledKey to DeferredData subscription callback - expose pendingKeys and deferredKeys as public API on DeferredData - allow multiple DeferredData subscriptions - allow unsubscribe from DeferredData - mark API unsafe Co-authored-by: Matt Brophy <[email protected]>
1 parent 78a72c3 commit ce2bb3f

File tree

6 files changed

+379
-65
lines changed

6 files changed

+379
-65
lines changed

.changeset/eight-tomatoes-breathe.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@remix-run/router": minor
3+
---
4+
5+
Expose deferred information from createStaticHandler

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107
},
108108
"filesize": {
109109
"packages/router/dist/router.umd.min.js": {
110-
"none": "38 kB"
110+
"none": "38.5 kB"
111111
},
112112
"packages/react-router/dist/react-router.production.min.js": {
113113
"none": "12.5 kB"

packages/router/__tests__/router-test.ts

+274-17
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
createRouter,
1717
createStaticHandler,
1818
defer,
19+
UNSAFE_DEFERRED_SYMBOL,
1920
ErrorResponse,
2021
IDLE_FETCHER,
2122
IDLE_NAVIGATION,
@@ -30,6 +31,7 @@ import type {
3031
AgnosticIndexRouteObject,
3132
AgnosticNonIndexRouteObject,
3233
AgnosticRouteObject,
34+
DeferredData,
3335
TrackedPromise,
3436
} from "../utils";
3537
import {
@@ -157,8 +159,13 @@ function isRedirect(result: any) {
157159
);
158160
}
159161

160-
interface CustomMatchers<R = unknown> {
162+
interface CustomMatchers<R = jest.Expect> {
161163
trackedPromise(data?: any, error?: any, aborted?: boolean): R;
164+
deferredData(
165+
done: boolean,
166+
status?: number,
167+
headers?: Record<string, string>
168+
): R;
162169
}
163170

164171
declare global {
@@ -169,12 +176,40 @@ declare global {
169176
}
170177
}
171178

172-
// Custom matcher for asserting deferred promise results inside of `toEqual()`
173-
// - expect.trackedPromise() => pending promise
174-
// - expect.trackedPromise(value) => promise resolved with `value`
175-
// - expect.trackedPromise(null, error) => promise rejected with `error`
176-
// - expect.trackedPromise(null, null, true) => promise aborted
177179
expect.extend({
180+
// Custom matcher for asserting deferred promise results for static handler
181+
// - expect(val).deferredData(false) => Unresolved promise
182+
// - expect(val).deferredData(false) => Resolved promise
183+
// - expect(val).deferredData(false, 201, { 'x-custom': 'yes' })
184+
// => Unresolved promise with status + headers
185+
// - expect(val).deferredData(true, 201, { 'x-custom': 'yes' })
186+
// => Resolved promise with status + headers
187+
deferredData(received, done, status = 200, headers = {}) {
188+
let deferredData = received as DeferredData;
189+
190+
return {
191+
message: () =>
192+
`expected done=${String(
193+
done
194+
)}/status=${status}/headers=${JSON.stringify(headers)}, ` +
195+
`instead got done=${String(deferredData.done)}/status=${
196+
deferredData.init!.status || 200
197+
}/headers=${JSON.stringify(
198+
Object.fromEntries(new Headers(deferredData.init!.headers).entries())
199+
)}`,
200+
pass:
201+
deferredData.done === done &&
202+
(deferredData.init!.status || 200) === status &&
203+
JSON.stringify(
204+
Object.fromEntries(new Headers(deferredData.init!.headers).entries())
205+
) === JSON.stringify(headers),
206+
};
207+
},
208+
// Custom matcher for asserting deferred promise results inside of `toEqual()`
209+
// - expect.trackedPromise() => pending promise
210+
// - expect.trackedPromise(value) => promise resolved with `value`
211+
// - expect.trackedPromise(null, error) => promise rejected with `error`
212+
// - expect.trackedPromise(null, null, true) => promise aborted
178213
trackedPromise(received, data, error, aborted = false) {
179214
let promise = received as TrackedPromise;
180215
let isTrackedPromise =
@@ -10948,14 +10983,32 @@ describe("a router", () => {
1094810983
{
1094910984
id: "deferred",
1095010985
path: "deferred",
10951-
loader: () =>
10952-
defer({
10986+
loader: ({ request }) => {
10987+
if (new URL(request.url).searchParams.has("reject")) {
10988+
return defer({
10989+
critical: "loader",
10990+
lazy: new Promise((_, r) =>
10991+
setTimeout(() => r(new Error("broken!")), 10)
10992+
),
10993+
});
10994+
}
10995+
if (new URL(request.url).searchParams.has("status")) {
10996+
return defer(
10997+
{
10998+
critical: "loader",
10999+
lazy: new Promise((r) => setTimeout(() => r("lazy"), 10)),
11000+
},
11001+
{ status: 201, headers: { "X-Custom": "yes" } }
11002+
);
11003+
}
11004+
return defer({
1095311005
critical: "loader",
1095411006
lazy: new Promise((r) => setTimeout(() => r("lazy"), 10)),
10955-
}),
11007+
});
11008+
},
1095611009
action: () =>
1095711010
defer({
10958-
critical: "action",
11011+
critical: "critical",
1095911012
lazy: new Promise((r) => setTimeout(() => r("lazy"), 10)),
1096011013
}),
1096111014
},
@@ -11112,8 +11165,7 @@ describe("a router", () => {
1111211165
});
1111311166
});
1111411167

11115-
// Note: this is only until we wire up the remix streaming
11116-
it("should abort deferred data on load navigations (for now)", async () => {
11168+
it("should support document load navigations returning deferred", async () => {
1111711169
let { query } = createStaticHandler(SSR_ROUTES);
1111811170
let context = await query(createRequest("/parent/deferred"));
1111911171
expect(context).toMatchObject({
@@ -11122,19 +11174,29 @@ describe("a router", () => {
1112211174
parent: "PARENT LOADER",
1112311175
deferred: {
1112411176
critical: "loader",
11125-
lazy: expect.trackedPromise(null, null, true),
11177+
lazy: expect.trackedPromise(),
1112611178
},
1112711179
},
11180+
activeDeferreds: {
11181+
deferred: expect.deferredData(false),
11182+
},
1112811183
errors: null,
1112911184
location: { pathname: "/parent/deferred" },
1113011185
matches: [{ route: { id: "parent" } }, { route: { id: "deferred" } }],
1113111186
});
1113211187

1113311188
await new Promise((r) => setTimeout(r, 10));
11134-
expect(
11135-
(context as StaticHandlerContext).loaderData.deferred.lazy instanceof
11136-
Promise
11137-
).toBe(true);
11189+
11190+
expect(context).toMatchObject({
11191+
loaderData: {
11192+
deferred: {
11193+
lazy: expect.trackedPromise("lazy"),
11194+
},
11195+
},
11196+
activeDeferreds: {
11197+
deferred: expect.deferredData(true),
11198+
},
11199+
});
1113811200
});
1113911201

1114011202
it("should support document submit navigations", async () => {
@@ -11685,6 +11747,127 @@ describe("a router", () => {
1168511747
expect(arg(childStub).context.sessionId).toBe("12345");
1168611748
});
1168711749

11750+
describe("deferred", () => {
11751+
let { query } = createStaticHandler(SSR_ROUTES);
11752+
11753+
it("should return DeferredData on symbol", async () => {
11754+
let context = (await query(
11755+
createRequest("/parent/deferred")
11756+
)) as StaticHandlerContext;
11757+
expect(context).toMatchObject({
11758+
loaderData: {
11759+
parent: "PARENT LOADER",
11760+
deferred: {
11761+
critical: "loader",
11762+
lazy: expect.trackedPromise(),
11763+
},
11764+
},
11765+
activeDeferreds: {
11766+
deferred: expect.deferredData(false),
11767+
},
11768+
});
11769+
await new Promise((r) => setTimeout(r, 10));
11770+
expect(context).toMatchObject({
11771+
loaderData: {
11772+
parent: "PARENT LOADER",
11773+
deferred: {
11774+
critical: "loader",
11775+
lazy: expect.trackedPromise("lazy"),
11776+
},
11777+
},
11778+
activeDeferreds: {
11779+
deferred: expect.deferredData(true),
11780+
},
11781+
});
11782+
});
11783+
11784+
it("should return rejected DeferredData on symbol", async () => {
11785+
let context = (await query(
11786+
createRequest("/parent/deferred?reject")
11787+
)) as StaticHandlerContext;
11788+
expect(context).toMatchObject({
11789+
loaderData: {
11790+
parent: "PARENT LOADER",
11791+
deferred: {
11792+
critical: "loader",
11793+
lazy: expect.trackedPromise(),
11794+
},
11795+
},
11796+
activeDeferreds: {
11797+
deferred: expect.deferredData(false),
11798+
},
11799+
});
11800+
await new Promise((r) => setTimeout(r, 10));
11801+
expect(context).toMatchObject({
11802+
loaderData: {
11803+
parent: "PARENT LOADER",
11804+
deferred: {
11805+
critical: "loader",
11806+
lazy: expect.trackedPromise(undefined, new Error("broken!")),
11807+
},
11808+
},
11809+
activeDeferreds: {
11810+
deferred: expect.deferredData(true),
11811+
},
11812+
});
11813+
});
11814+
11815+
it("should return DeferredData on symbol with status + headers", async () => {
11816+
let context = (await query(
11817+
createRequest("/parent/deferred?status")
11818+
)) as StaticHandlerContext;
11819+
expect(context).toMatchObject({
11820+
loaderData: {
11821+
parent: "PARENT LOADER",
11822+
deferred: {
11823+
critical: "loader",
11824+
lazy: expect.trackedPromise(),
11825+
},
11826+
},
11827+
activeDeferreds: {
11828+
deferred: expect.deferredData(false, 201, {
11829+
"x-custom": "yes",
11830+
}),
11831+
},
11832+
});
11833+
await new Promise((r) => setTimeout(r, 10));
11834+
expect(context).toMatchObject({
11835+
loaderData: {
11836+
parent: "PARENT LOADER",
11837+
deferred: {
11838+
critical: "loader",
11839+
lazy: expect.trackedPromise("lazy"),
11840+
},
11841+
},
11842+
activeDeferreds: {
11843+
deferred: expect.deferredData(true, 201, {
11844+
"x-custom": "yes",
11845+
}),
11846+
},
11847+
});
11848+
});
11849+
11850+
it("does not support deferred on submissions", async () => {
11851+
let context = (await query(
11852+
createSubmitRequest("/parent/deferred")
11853+
)) as StaticHandlerContext;
11854+
expect(context.actionData).toEqual(null);
11855+
expect(context.loaderData).toEqual({
11856+
parent: null,
11857+
deferred: null,
11858+
});
11859+
expect(context.activeDeferreds).toEqual(null);
11860+
expect(context.errors).toEqual({
11861+
parent: new ErrorResponse(
11862+
400,
11863+
"Bad Request",
11864+
new Error("defer() is not supported in actions"),
11865+
true
11866+
),
11867+
});
11868+
});
11869+
});
11870+
1168811871
describe("statusCode", () => {
1168911872
it("should expose a 200 status code by default", async () => {
1169011873
let { query } = createStaticHandler([
@@ -12661,6 +12844,80 @@ describe("a router", () => {
1266112844
expect(arg(actionStub).context.sessionId).toBe("12345");
1266212845
});
1266312846

12847+
describe("deferred", () => {
12848+
let { queryRoute } = createStaticHandler(SSR_ROUTES);
12849+
12850+
it("should return DeferredData on symbol", async () => {
12851+
let result = await queryRoute(createRequest("/parent/deferred"));
12852+
expect(result).toMatchObject({
12853+
critical: "loader",
12854+
lazy: expect.trackedPromise(),
12855+
});
12856+
expect(result[UNSAFE_DEFERRED_SYMBOL]).deferredData(false);
12857+
await new Promise((r) => setTimeout(r, 10));
12858+
expect(result).toMatchObject({
12859+
critical: "loader",
12860+
lazy: expect.trackedPromise("lazy"),
12861+
});
12862+
expect(result[UNSAFE_DEFERRED_SYMBOL]).deferredData(true);
12863+
});
12864+
12865+
it("should return rejected DeferredData on symbol", async () => {
12866+
let result = await queryRoute(
12867+
createRequest("/parent/deferred?reject")
12868+
);
12869+
expect(result).toMatchObject({
12870+
critical: "loader",
12871+
lazy: expect.trackedPromise(),
12872+
});
12873+
expect(result[UNSAFE_DEFERRED_SYMBOL]).deferredData(false);
12874+
await new Promise((r) => setTimeout(r, 10));
12875+
expect(result).toMatchObject({
12876+
critical: "loader",
12877+
lazy: expect.trackedPromise(null, new Error("broken!")),
12878+
});
12879+
expect(result[UNSAFE_DEFERRED_SYMBOL]).deferredData(true);
12880+
});
12881+
12882+
it("should return DeferredData on symbol with status + headers", async () => {
12883+
let result = await queryRoute(
12884+
createRequest("/parent/deferred?status")
12885+
);
12886+
expect(result).toMatchObject({
12887+
critical: "loader",
12888+
lazy: expect.trackedPromise(),
12889+
});
12890+
expect(result[UNSAFE_DEFERRED_SYMBOL]).deferredData(false, 201, {
12891+
"x-custom": "yes",
12892+
});
12893+
await new Promise((r) => setTimeout(r, 10));
12894+
expect(result).toMatchObject({
12895+
critical: "loader",
12896+
lazy: expect.trackedPromise("lazy"),
12897+
});
12898+
expect(result[UNSAFE_DEFERRED_SYMBOL]).deferredData(true, 201, {
12899+
"x-custom": "yes",
12900+
});
12901+
});
12902+
12903+
it("does not support deferred on submissions", async () => {
12904+
try {
12905+
await queryRoute(createSubmitRequest("/parent/deferred"));
12906+
expect(false).toBe(true);
12907+
} catch (e) {
12908+
// eslint-disable-next-line jest/no-conditional-expect
12909+
expect(e).toEqual(
12910+
new ErrorResponse(
12911+
400,
12912+
"Bad Request",
12913+
new Error("defer() is not supported in actions"),
12914+
true
12915+
)
12916+
);
12917+
}
12918+
});
12919+
});
12920+
1266412921
describe("Errors with Status Codes", () => {
1266512922
/* eslint-disable jest/no-conditional-expect */
1266612923
let { queryRoute } = createStaticHandler([

0 commit comments

Comments
 (0)