- Add debounced state updates for title and content (500ms delay) - Immediate UI updates with delayed history saving - Prevent one-letter-per-undo issue - Add cleanup for debounce timers on unmount
412 lines
10 KiB
JavaScript
412 lines
10 KiB
JavaScript
// src/context.ts
|
|
import { HonoRequest } from "./request.js";
|
|
import { HtmlEscapedCallbackPhase, resolveCallback } from "./utils/html.js";
|
|
var TEXT_PLAIN = "text/plain; charset=UTF-8";
|
|
var setDefaultContentType = (contentType, headers) => {
|
|
return {
|
|
"Content-Type": contentType,
|
|
...headers
|
|
};
|
|
};
|
|
var Context = class {
|
|
#rawRequest;
|
|
#req;
|
|
/**
|
|
* `.env` can get bindings (environment variables, secrets, KV namespaces, D1 database, R2 bucket etc.) in Cloudflare Workers.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#env}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* // Environment object for Cloudflare Workers
|
|
* app.get('*', async c => {
|
|
* const counter = c.env.COUNTER
|
|
* })
|
|
* ```
|
|
*/
|
|
env = {};
|
|
#var;
|
|
finalized = false;
|
|
/**
|
|
* `.error` can get the error object from the middleware if the Handler throws an error.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#error}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.use('*', async (c, next) => {
|
|
* await next()
|
|
* if (c.error) {
|
|
* // do something...
|
|
* }
|
|
* })
|
|
* ```
|
|
*/
|
|
error;
|
|
#status;
|
|
#executionCtx;
|
|
#res;
|
|
#layout;
|
|
#renderer;
|
|
#notFoundHandler;
|
|
#preparedHeaders;
|
|
#matchResult;
|
|
#path;
|
|
/**
|
|
* Creates an instance of the Context class.
|
|
*
|
|
* @param req - The Request object.
|
|
* @param options - Optional configuration options for the context.
|
|
*/
|
|
constructor(req, options) {
|
|
this.#rawRequest = req;
|
|
if (options) {
|
|
this.#executionCtx = options.executionCtx;
|
|
this.env = options.env;
|
|
this.#notFoundHandler = options.notFoundHandler;
|
|
this.#path = options.path;
|
|
this.#matchResult = options.matchResult;
|
|
}
|
|
}
|
|
/**
|
|
* `.req` is the instance of {@link HonoRequest}.
|
|
*/
|
|
get req() {
|
|
this.#req ??= new HonoRequest(this.#rawRequest, this.#path, this.#matchResult);
|
|
return this.#req;
|
|
}
|
|
/**
|
|
* @see {@link https://hono.dev/docs/api/context#event}
|
|
* The FetchEvent associated with the current request.
|
|
*
|
|
* @throws Will throw an error if the context does not have a FetchEvent.
|
|
*/
|
|
get event() {
|
|
if (this.#executionCtx && "respondWith" in this.#executionCtx) {
|
|
return this.#executionCtx;
|
|
} else {
|
|
throw Error("This context has no FetchEvent");
|
|
}
|
|
}
|
|
/**
|
|
* @see {@link https://hono.dev/docs/api/context#executionctx}
|
|
* The ExecutionContext associated with the current request.
|
|
*
|
|
* @throws Will throw an error if the context does not have an ExecutionContext.
|
|
*/
|
|
get executionCtx() {
|
|
if (this.#executionCtx) {
|
|
return this.#executionCtx;
|
|
} else {
|
|
throw Error("This context has no ExecutionContext");
|
|
}
|
|
}
|
|
/**
|
|
* @see {@link https://hono.dev/docs/api/context#res}
|
|
* The Response object for the current request.
|
|
*/
|
|
get res() {
|
|
return this.#res ||= new Response(null, {
|
|
headers: this.#preparedHeaders ??= new Headers()
|
|
});
|
|
}
|
|
/**
|
|
* Sets the Response object for the current request.
|
|
*
|
|
* @param _res - The Response object to set.
|
|
*/
|
|
set res(_res) {
|
|
if (this.#res && _res) {
|
|
_res = new Response(_res.body, _res);
|
|
for (const [k, v] of this.#res.headers.entries()) {
|
|
if (k === "content-type") {
|
|
continue;
|
|
}
|
|
if (k === "set-cookie") {
|
|
const cookies = this.#res.headers.getSetCookie();
|
|
_res.headers.delete("set-cookie");
|
|
for (const cookie of cookies) {
|
|
_res.headers.append("set-cookie", cookie);
|
|
}
|
|
} else {
|
|
_res.headers.set(k, v);
|
|
}
|
|
}
|
|
}
|
|
this.#res = _res;
|
|
this.finalized = true;
|
|
}
|
|
/**
|
|
* `.render()` can create a response within a layout.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#render-setrenderer}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.get('/', (c) => {
|
|
* return c.render('Hello!')
|
|
* })
|
|
* ```
|
|
*/
|
|
render = (...args) => {
|
|
this.#renderer ??= (content) => this.html(content);
|
|
return this.#renderer(...args);
|
|
};
|
|
/**
|
|
* Sets the layout for the response.
|
|
*
|
|
* @param layout - The layout to set.
|
|
* @returns The layout function.
|
|
*/
|
|
setLayout = (layout) => this.#layout = layout;
|
|
/**
|
|
* Gets the current layout for the response.
|
|
*
|
|
* @returns The current layout function.
|
|
*/
|
|
getLayout = () => this.#layout;
|
|
/**
|
|
* `.setRenderer()` can set the layout in the custom middleware.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#render-setrenderer}
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* app.use('*', async (c, next) => {
|
|
* c.setRenderer((content) => {
|
|
* return c.html(
|
|
* <html>
|
|
* <body>
|
|
* <p>{content}</p>
|
|
* </body>
|
|
* </html>
|
|
* )
|
|
* })
|
|
* await next()
|
|
* })
|
|
* ```
|
|
*/
|
|
setRenderer = (renderer) => {
|
|
this.#renderer = renderer;
|
|
};
|
|
/**
|
|
* `.header()` can set headers.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#header}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.get('/welcome', (c) => {
|
|
* // Set headers
|
|
* c.header('X-Message', 'Hello!')
|
|
* c.header('Content-Type', 'text/plain')
|
|
*
|
|
* return c.body('Thank you for coming')
|
|
* })
|
|
* ```
|
|
*/
|
|
header = (name, value, options) => {
|
|
if (this.finalized) {
|
|
this.#res = new Response(this.#res.body, this.#res);
|
|
}
|
|
const headers = this.#res ? this.#res.headers : this.#preparedHeaders ??= new Headers();
|
|
if (value === void 0) {
|
|
headers.delete(name);
|
|
} else if (options?.append) {
|
|
headers.append(name, value);
|
|
} else {
|
|
headers.set(name, value);
|
|
}
|
|
};
|
|
status = (status) => {
|
|
this.#status = status;
|
|
};
|
|
/**
|
|
* `.set()` can set the value specified by the key.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#set-get}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.use('*', async (c, next) => {
|
|
* c.set('message', 'Hono is hot!!')
|
|
* await next()
|
|
* })
|
|
* ```
|
|
*/
|
|
set = (key, value) => {
|
|
this.#var ??= /* @__PURE__ */ new Map();
|
|
this.#var.set(key, value);
|
|
};
|
|
/**
|
|
* `.get()` can use the value specified by the key.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#set-get}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.get('/', (c) => {
|
|
* const message = c.get('message')
|
|
* return c.text(`The message is "${message}"`)
|
|
* })
|
|
* ```
|
|
*/
|
|
get = (key) => {
|
|
return this.#var ? this.#var.get(key) : void 0;
|
|
};
|
|
/**
|
|
* `.var` can access the value of a variable.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#var}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const result = c.var.client.oneMethod()
|
|
* ```
|
|
*/
|
|
// c.var.propName is a read-only
|
|
get var() {
|
|
if (!this.#var) {
|
|
return {};
|
|
}
|
|
return Object.fromEntries(this.#var);
|
|
}
|
|
#newResponse(data, arg, headers) {
|
|
const responseHeaders = this.#res ? new Headers(this.#res.headers) : this.#preparedHeaders ?? new Headers();
|
|
if (typeof arg === "object" && "headers" in arg) {
|
|
const argHeaders = arg.headers instanceof Headers ? arg.headers : new Headers(arg.headers);
|
|
for (const [key, value] of argHeaders) {
|
|
if (key.toLowerCase() === "set-cookie") {
|
|
responseHeaders.append(key, value);
|
|
} else {
|
|
responseHeaders.set(key, value);
|
|
}
|
|
}
|
|
}
|
|
if (headers) {
|
|
for (const [k, v] of Object.entries(headers)) {
|
|
if (typeof v === "string") {
|
|
responseHeaders.set(k, v);
|
|
} else {
|
|
responseHeaders.delete(k);
|
|
for (const v2 of v) {
|
|
responseHeaders.append(k, v2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const status = typeof arg === "number" ? arg : arg?.status ?? this.#status;
|
|
return new Response(data, { status, headers: responseHeaders });
|
|
}
|
|
newResponse = (...args) => this.#newResponse(...args);
|
|
/**
|
|
* `.body()` can return the HTTP response.
|
|
* You can set headers with `.header()` and set HTTP status code with `.status`.
|
|
* This can also be set in `.text()`, `.json()` and so on.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#body}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.get('/welcome', (c) => {
|
|
* // Set headers
|
|
* c.header('X-Message', 'Hello!')
|
|
* c.header('Content-Type', 'text/plain')
|
|
* // Set HTTP status code
|
|
* c.status(201)
|
|
*
|
|
* // Return the response body
|
|
* return c.body('Thank you for coming')
|
|
* })
|
|
* ```
|
|
*/
|
|
body = (data, arg, headers) => this.#newResponse(data, arg, headers);
|
|
/**
|
|
* `.text()` can render text as `Content-Type:text/plain`.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#text}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.get('/say', (c) => {
|
|
* return c.text('Hello!')
|
|
* })
|
|
* ```
|
|
*/
|
|
text = (text, arg, headers) => {
|
|
return !this.#preparedHeaders && !this.#status && !arg && !headers && !this.finalized ? new Response(text) : this.#newResponse(
|
|
text,
|
|
arg,
|
|
setDefaultContentType(TEXT_PLAIN, headers)
|
|
);
|
|
};
|
|
/**
|
|
* `.json()` can render JSON as `Content-Type:application/json`.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#json}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.get('/api', (c) => {
|
|
* return c.json({ message: 'Hello!' })
|
|
* })
|
|
* ```
|
|
*/
|
|
json = (object, arg, headers) => {
|
|
return this.#newResponse(
|
|
JSON.stringify(object),
|
|
arg,
|
|
setDefaultContentType("application/json", headers)
|
|
);
|
|
};
|
|
html = (html, arg, headers) => {
|
|
const res = (html2) => this.#newResponse(html2, arg, setDefaultContentType("text/html; charset=UTF-8", headers));
|
|
return typeof html === "object" ? resolveCallback(html, HtmlEscapedCallbackPhase.Stringify, false, {}).then(res) : res(html);
|
|
};
|
|
/**
|
|
* `.redirect()` can Redirect, default status code is 302.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#redirect}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.get('/redirect', (c) => {
|
|
* return c.redirect('/')
|
|
* })
|
|
* app.get('/redirect-permanently', (c) => {
|
|
* return c.redirect('/', 301)
|
|
* })
|
|
* ```
|
|
*/
|
|
redirect = (location, status) => {
|
|
const locationString = String(location);
|
|
this.header(
|
|
"Location",
|
|
// Multibyes should be encoded
|
|
// eslint-disable-next-line no-control-regex
|
|
!/[^\x00-\xFF]/.test(locationString) ? locationString : encodeURI(locationString)
|
|
);
|
|
return this.newResponse(null, status ?? 302);
|
|
};
|
|
/**
|
|
* `.notFound()` can return the Not Found Response.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#notfound}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.get('/notfound', (c) => {
|
|
* return c.notFound()
|
|
* })
|
|
* ```
|
|
*/
|
|
notFound = () => {
|
|
this.#notFoundHandler ??= () => new Response();
|
|
return this.#notFoundHandler(this);
|
|
};
|
|
};
|
|
export {
|
|
Context,
|
|
TEXT_PLAIN
|
|
};
|