Skip to content
This repository was archived by the owner on Jul 6, 2025. It is now read-only.

Commit 6e681a1

Browse files
committed
Better data typings
1 parent 4696a81 commit 6e681a1

File tree

5 files changed

+98
-116
lines changed

5 files changed

+98
-116
lines changed

examples/feature-apps/suspense-ssr/routes/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ import Comments from "../components/Comments.tsx";
1010
const delay = 3000;
1111

1212
export const data: Data = {
13-
get: async (_req, ctx) => {
13+
get: async () => {
1414
await new Promise((resolve) => setTimeout(resolve, delay));
15-
return ctx.json({
15+
return {
1616
comments: [
1717
"Wait, it doesn't wait for React to load?",
1818
"How does this even work?",
1919
"I like marshmallows",
2020
],
21-
});
21+
};
2222
},
2323
};
2424

examples/react-app/routes/todos.tsx

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,53 +6,53 @@ type TodoItem = {
66
completed: boolean;
77
};
88

9-
type DataProps = {
9+
type Store = {
1010
todos: TodoItem[];
1111
};
1212

13-
const storage: DataProps = {
13+
const store: Store = {
1414
todos: JSON.parse(window.localStorage?.getItem("todos") || "[]"),
1515
};
1616

17-
export const data: Data<DataProps> = {
18-
cacheTtl: 0,
19-
get: (_req, ctx) => {
20-
return ctx.json(storage);
17+
export const data: Data<Store, Store> = {
18+
cacheTtl: 0, // no cache
19+
get: () => {
20+
return store;
2121
},
22-
put: async (req, ctx) => {
22+
put: async (req) => {
2323
const { message } = await req.json();
2424
if (typeof message === "string") {
25-
storage.todos.push({ id: Date.now(), message, completed: false });
26-
window.localStorage?.setItem("todos", JSON.stringify(storage.todos));
25+
store.todos.push({ id: Date.now(), message, completed: false });
26+
window.localStorage?.setItem("todos", JSON.stringify(store.todos));
2727
}
28-
return ctx.json(storage);
28+
return store;
2929
},
30-
patch: async (req, ctx) => {
30+
patch: async (req) => {
3131
const { id, message, completed } = await req.json();
32-
const todo = storage.todos.find((todo) => todo.id === id);
32+
const todo = store.todos.find((todo) => todo.id === id);
3333
if (todo) {
3434
if (typeof message === "string") {
3535
todo.message = message;
3636
}
3737
if (typeof completed === "boolean") {
3838
todo.completed = completed;
3939
}
40-
window.localStorage?.setItem("todos", JSON.stringify(storage.todos));
40+
window.localStorage?.setItem("todos", JSON.stringify(store.todos));
4141
}
42-
return ctx.json(storage);
42+
return store;
4343
},
44-
delete: async (req, ctx) => {
44+
delete: async (req) => {
4545
const { id } = await req.json();
4646
if (id) {
47-
storage.todos = storage.todos.filter((todo) => todo.id !== id);
48-
window.localStorage?.setItem("todos", JSON.stringify(storage.todos));
47+
store.todos = store.todos.filter((todo) => todo.id !== id);
48+
window.localStorage?.setItem("todos", JSON.stringify(store.todos));
4949
}
50-
return ctx.json(storage);
50+
return store;
5151
},
5252
};
5353

5454
export default function Todos() {
55-
const { data: { todos }, isMutating, mutation } = useData<DataProps>();
55+
const { data: { todos }, isMutating, mutation } = useData<Store>();
5656

5757
return (
5858
<div className="todos-app">
@@ -85,13 +85,13 @@ export default function Todos() {
8585
const message = fd.get("message")?.toString().trim();
8686
if (message) {
8787
await mutation.put({ message }, {
88-
// optimistic update without waiting for the server response
88+
// optimistic update data without waiting for the server response
8989
optimisticUpdate: (data) => {
9090
return {
9191
todos: [...data.todos, { id: 0, message, completed: false }],
9292
};
9393
},
94-
// replace the data with the new data from the server
94+
// replace the data from the server response
9595
replace: true,
9696
});
9797
form.reset();

server/mod.ts

Lines changed: 29 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -197,37 +197,6 @@ export const serve = (options: ServerOptions = {}) => {
197197
customHTMLRewriter.set(selector, handlers);
198198
},
199199
},
200-
redirect(url: string | URL, code?: number) {
201-
const headers = new Headers(ctx.headers);
202-
headers.set("Location", url.toString());
203-
return new Response(null, { status: code || 302, headers });
204-
},
205-
json: (data: unknown, init?: ResponseInit): Response => {
206-
let headers: Headers | null = null;
207-
ctx.headers.forEach((value, name) => {
208-
if (!headers) {
209-
headers = new Headers(init?.headers);
210-
}
211-
headers.set(name, value);
212-
});
213-
if (!headers) {
214-
return json(data, init);
215-
}
216-
return json(data, { ...init, headers });
217-
},
218-
content: (body: BodyInit, init?: ResponseInit): Response => {
219-
let headers: Headers | null = null;
220-
ctx.headers.forEach((value, name) => {
221-
if (!headers) {
222-
headers = new Headers(init?.headers);
223-
}
224-
headers.set(name, value);
225-
});
226-
if (!headers) {
227-
return content(body, init);
228-
}
229-
return content(body, { ...init, headers });
230-
},
231200
};
232201

233202
// use middlewares
@@ -248,6 +217,7 @@ export const serve = (options: ServerOptions = {}) => {
248217
}
249218
}
250219
} catch (err) {
220+
log.error(`Middleare${mw.name ? `(${mw.name}${mw.version ? " " + mw.version : ""})` : ""}:`, err);
251221
return onError?.(err, { by: "middleware", url: req.url, context: ctx }) ??
252222
new Response(generateErrorHtml(err.stack ?? err.message), {
253223
status: 500,
@@ -284,24 +254,42 @@ export const serve = (options: ServerOptions = {}) => {
284254
if (res instanceof Response) {
285255
if (res.status >= 300 && fromFetchApi) {
286256
const err = await FetchError.fromResponse(res);
287-
return ctx.json({ ...err }, { status: err.status >= 400 ? err.status : 501 });
257+
return json({ ...err }, { status: err.status >= 400 ? err.status : 501, headers: ctx.headers });
258+
}
259+
let headers: Headers | null = null;
260+
ctx.headers.forEach((value, name) => {
261+
if (!headers) {
262+
headers = new Headers(res.headers);
263+
}
264+
headers.set(name, value);
265+
});
266+
if (headers) {
267+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
288268
}
289269
return res;
290270
}
291271
if (
292-
typeof res === "string" || res instanceof ArrayBuffer || res instanceof ReadableStream
272+
typeof res === "string" ||
273+
res instanceof ArrayBuffer ||
274+
res instanceof Uint8Array ||
275+
res instanceof ReadableStream
293276
) {
294-
return ctx.content(res);
277+
return new Response(res, { headers: ctx.headers });
295278
}
296279
if (res instanceof Blob || res instanceof File) {
297-
return ctx.content(res, { headers: { "Content-Type": res.type } });
280+
ctx.headers.set("Content-Type", res.type);
281+
ctx.headers.set("Content-Length", res.size.toString());
282+
return new Response(res, { headers: ctx.headers });
283+
}
284+
if (util.isPlainObject(res) || Array.isArray(res)) {
285+
return json(res, { headers: ctx.headers });
298286
}
299-
if (util.isPlainObject(res) || Array.isArray(res) || res === null) {
300-
return ctx.json(res);
287+
if (res === null) {
288+
return new Response(null, { headers: ctx.headers });
301289
}
302-
return new Response(null, { headers: ctx.headers });
290+
return new Response("Invalid Reponse Type", { status: 500 });
303291
}
304-
return new Response("Method not allowed", { status: 405 });
292+
return new Response("Method Not Allowed", { status: 405 });
305293
}
306294
} catch (err) {
307295
const res = onError?.(err, { by: "route-api", url: req.url, context: ctx });
@@ -315,12 +303,9 @@ export const serve = (options: ServerOptions = {}) => {
315303
log.error(err);
316304
}
317305
const status: number = util.isUint(err.status || err.code) ? err.status || err.code : 500;
318-
return ctx.json({
319-
...err,
320-
message: err.message || String(err),
321-
status,
322-
}, {
306+
return json({ ...err, message: err.message || String(err), status }, {
323307
status: status >= 400 ? status : 501,
308+
headers: ctx.headers,
324309
});
325310
}
326311
}

server/renderer.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export type SSRContext = {
2222
};
2323

2424
export type SSRFn = {
25-
(ssr: SSRContext): Promise<ReadableStream> | ReadableStream;
25+
(ssr: SSRContext): Promise<ReadableStream | string> | ReadableStream | string;
2626
};
2727

2828
export type SSR = {
@@ -406,9 +406,12 @@ async function initSSR(
406406
throw new FetchError(500, {}, "Data must be valid JSON");
407407
}
408408
} else if (res === null || util.isPlainObject(res) || Array.isArray(res)) {
409+
if (suspense) {
410+
suspenseData[rmod.url.pathname + rmod.url.search] = res;
411+
}
409412
return res;
410413
} else {
411-
throw new FetchError(500, {}, "No response from data fetcher");
414+
throw new FetchError(500, {}, "Data must be valid JSON");
412415
}
413416
};
414417
rmod.withData = true;

types.d.ts

Lines changed: 40 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,73 @@
1-
type HTMLRewriterHandlers = {
2-
element?: (element: import("https://deno.land/x/[email protected]/types.d.ts").Element) => void;
3-
comments?: (comment: import("https://deno.land/x/[email protected]/types.d.ts").Comment) => void;
4-
text?: (text: import("https://deno.land/x/[email protected]/types.d.ts").TextChunk) => void;
5-
};
6-
7-
type HTMLRewriter = {
8-
on: (selector: string, handlers: HTMLRewriterHandlers) => void;
9-
};
1+
/** Information about the connection a request arrived on. */
2+
interface ConnInfo {
3+
/** The local address of the connection. */
4+
readonly localAddr: Deno.Addr;
5+
/** The remote address of the connection. */
6+
readonly remoteAddr: Deno.Addr;
7+
}
108

11-
declare type CookieOptions = {
9+
interface CookieOptions {
1210
expires?: number | Date;
1311
maxAge?: number;
1412
domain?: string;
1513
path?: string;
1614
httpOnly?: boolean;
1715
secure?: boolean;
1816
sameSite?: "lax" | "strict" | "none";
19-
};
17+
}
2018

21-
declare interface Cookies {
19+
interface Cookies {
2220
get(key: string): string | undefined;
2321
set(key: string, value: string, options?: CookieOptions): void;
2422
delete(key: string, options?: CookieOptions): void;
2523
}
2624

27-
declare type CacheControlOptions = {
28-
maxAge?: number;
29-
sMaxAge?: number;
30-
public?: boolean;
31-
private?: boolean;
32-
immutable?: boolean;
33-
mustRevalidate?: boolean;
34-
};
25+
interface HTMLRewriterHandlers {
26+
element?: (element: import("https://deno.land/x/[email protected]/types.d.ts").Element) => void;
27+
comments?: (comment: import("https://deno.land/x/[email protected]/types.d.ts").Comment) => void;
28+
text?: (text: import("https://deno.land/x/[email protected]/types.d.ts").TextChunk) => void;
29+
}
3530

36-
/** Information about the connection a request arrived on. */
37-
declare interface ConnInfo {
38-
/** The local address of the connection. */
39-
readonly localAddr: Deno.Addr;
40-
/** The remote address of the connection. */
41-
readonly remoteAddr: Deno.Addr;
31+
interface HTMLRewriter {
32+
on: (selector: string, handlers: HTMLRewriterHandlers) => void;
4233
}
4334

44-
declare interface Context<DataType = unknown> extends Record<string, unknown> {
35+
declare interface Context extends Record<string, unknown> {
4536
readonly connInfo: ConnInfo;
4637
readonly params: Record<string, string>;
4738
readonly headers: Headers;
4839
readonly cookies: Cookies;
4940
readonly htmlRewriter: HTMLRewriter;
50-
redirect(url: string | URL, code?: number): Response;
51-
json(data: DataType, init?: ResponseInit): Response;
52-
content(
53-
content: BodyInit,
54-
init?: ResponseInit & {
55-
contentType?: string;
56-
cacheControl?: "no-cache" | "immutable" | CacheControlOptions;
57-
},
58-
): Response;
5941
}
6042

61-
declare interface Data<DataType = unknown, ContextExtension = Record<never, never>> {
43+
declare type ResponseLike =
44+
| Response
45+
| ReadableStream
46+
| ArrayBuffer
47+
| Uint8Array
48+
| string
49+
| Blob
50+
| File
51+
| Record<string, unknown>
52+
| Array<unknown>
53+
| null;
54+
55+
declare interface Data<GetDataType = ResponseLike, ActionDataType = ResponseLike> {
6256
cacheTtl?: number;
63-
any?(request: Request, context: Context & ContextExtension): Promise<Response | void> | Response | void;
57+
any?(request: Request, context: Context): Promise<Response | void> | Response | void;
6458
get?(
6559
request: Request,
66-
context: Context<DataType> & ContextExtension,
67-
): Promise<Response | DataType> | Response | DataType;
68-
post?(request: Request, context: Context & ContextExtension): Promise<Response> | Response;
69-
put?(request: Request, context: Context & ContextExtension): Promise<Response> | Response;
70-
patch?(request: Request, context: Context & ContextExtension): Promise<Response> | Response;
71-
delete?(request: Request, context: Context & ContextExtension): Promise<Response> | Response;
60+
context: Context,
61+
): Promise<GetDataType> | GetDataType;
62+
post?(request: Request, context: Context): Promise<ActionDataType> | ActionDataType;
63+
put?(request: Request, context: Context): Promise<ActionDataType> | ActionDataType;
64+
patch?(request: Request, context: Context): Promise<ActionDataType> | ActionDataType;
65+
delete?(request: Request, context: Context): Promise<ActionDataType> | ActionDataType;
7266
}
7367

7468
declare interface Middleware {
75-
name?: string;
76-
version?: string;
69+
readonly name?: string;
70+
readonly version?: string;
7771
fetch(
7872
request: Request,
7973
context: Context,

0 commit comments

Comments
 (0)