- 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
618 lines
18 KiB
JavaScript
618 lines
18 KiB
JavaScript
"use strict";
|
|
var __create = Object.create;
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
// file that has been converted to a CommonJS file using a Babel-
|
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
mod
|
|
));
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
|
|
// src/server.ts
|
|
var server_exports = {};
|
|
__export(server_exports, {
|
|
createAdaptorServer: () => createAdaptorServer,
|
|
serve: () => serve
|
|
});
|
|
module.exports = __toCommonJS(server_exports);
|
|
var import_node_http = require("http");
|
|
|
|
// src/listener.ts
|
|
var import_node_http22 = require("http2");
|
|
|
|
// src/request.ts
|
|
var import_node_http2 = require("http2");
|
|
var import_node_stream = require("stream");
|
|
var RequestError = class extends Error {
|
|
constructor(message, options) {
|
|
super(message, options);
|
|
this.name = "RequestError";
|
|
}
|
|
};
|
|
var toRequestError = (e) => {
|
|
if (e instanceof RequestError) {
|
|
return e;
|
|
}
|
|
return new RequestError(e.message, { cause: e });
|
|
};
|
|
var GlobalRequest = global.Request;
|
|
var Request = class extends GlobalRequest {
|
|
constructor(input, options) {
|
|
if (typeof input === "object" && getRequestCache in input) {
|
|
input = input[getRequestCache]();
|
|
}
|
|
if (typeof options?.body?.getReader !== "undefined") {
|
|
;
|
|
options.duplex ??= "half";
|
|
}
|
|
super(input, options);
|
|
}
|
|
};
|
|
var newHeadersFromIncoming = (incoming) => {
|
|
const headerRecord = [];
|
|
const rawHeaders = incoming.rawHeaders;
|
|
for (let i = 0; i < rawHeaders.length; i += 2) {
|
|
const { [i]: key, [i + 1]: value } = rawHeaders;
|
|
if (key.charCodeAt(0) !== /*:*/
|
|
58) {
|
|
headerRecord.push([key, value]);
|
|
}
|
|
}
|
|
return new Headers(headerRecord);
|
|
};
|
|
var wrapBodyStream = Symbol("wrapBodyStream");
|
|
var newRequestFromIncoming = (method, url, headers, incoming, abortController) => {
|
|
const init = {
|
|
method,
|
|
headers,
|
|
signal: abortController.signal
|
|
};
|
|
if (method === "TRACE") {
|
|
init.method = "GET";
|
|
const req = new Request(url, init);
|
|
Object.defineProperty(req, "method", {
|
|
get() {
|
|
return "TRACE";
|
|
}
|
|
});
|
|
return req;
|
|
}
|
|
if (!(method === "GET" || method === "HEAD")) {
|
|
if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) {
|
|
init.body = new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(incoming.rawBody);
|
|
controller.close();
|
|
}
|
|
});
|
|
} else if (incoming[wrapBodyStream]) {
|
|
let reader;
|
|
init.body = new ReadableStream({
|
|
async pull(controller) {
|
|
try {
|
|
reader ||= import_node_stream.Readable.toWeb(incoming).getReader();
|
|
const { done, value } = await reader.read();
|
|
if (done) {
|
|
controller.close();
|
|
} else {
|
|
controller.enqueue(value);
|
|
}
|
|
} catch (error) {
|
|
controller.error(error);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
init.body = import_node_stream.Readable.toWeb(incoming);
|
|
}
|
|
}
|
|
return new Request(url, init);
|
|
};
|
|
var getRequestCache = Symbol("getRequestCache");
|
|
var requestCache = Symbol("requestCache");
|
|
var incomingKey = Symbol("incomingKey");
|
|
var urlKey = Symbol("urlKey");
|
|
var headersKey = Symbol("headersKey");
|
|
var abortControllerKey = Symbol("abortControllerKey");
|
|
var getAbortController = Symbol("getAbortController");
|
|
var requestPrototype = {
|
|
get method() {
|
|
return this[incomingKey].method || "GET";
|
|
},
|
|
get url() {
|
|
return this[urlKey];
|
|
},
|
|
get headers() {
|
|
return this[headersKey] ||= newHeadersFromIncoming(this[incomingKey]);
|
|
},
|
|
[getAbortController]() {
|
|
this[getRequestCache]();
|
|
return this[abortControllerKey];
|
|
},
|
|
[getRequestCache]() {
|
|
this[abortControllerKey] ||= new AbortController();
|
|
return this[requestCache] ||= newRequestFromIncoming(
|
|
this.method,
|
|
this[urlKey],
|
|
this.headers,
|
|
this[incomingKey],
|
|
this[abortControllerKey]
|
|
);
|
|
}
|
|
};
|
|
[
|
|
"body",
|
|
"bodyUsed",
|
|
"cache",
|
|
"credentials",
|
|
"destination",
|
|
"integrity",
|
|
"mode",
|
|
"redirect",
|
|
"referrer",
|
|
"referrerPolicy",
|
|
"signal",
|
|
"keepalive"
|
|
].forEach((k) => {
|
|
Object.defineProperty(requestPrototype, k, {
|
|
get() {
|
|
return this[getRequestCache]()[k];
|
|
}
|
|
});
|
|
});
|
|
["arrayBuffer", "blob", "clone", "formData", "json", "text"].forEach((k) => {
|
|
Object.defineProperty(requestPrototype, k, {
|
|
value: function() {
|
|
return this[getRequestCache]()[k]();
|
|
}
|
|
});
|
|
});
|
|
Object.setPrototypeOf(requestPrototype, Request.prototype);
|
|
var newRequest = (incoming, defaultHostname) => {
|
|
const req = Object.create(requestPrototype);
|
|
req[incomingKey] = incoming;
|
|
const incomingUrl = incoming.url || "";
|
|
if (incomingUrl[0] !== "/" && // short-circuit for performance. most requests are relative URL.
|
|
(incomingUrl.startsWith("http://") || incomingUrl.startsWith("https://"))) {
|
|
if (incoming instanceof import_node_http2.Http2ServerRequest) {
|
|
throw new RequestError("Absolute URL for :path is not allowed in HTTP/2");
|
|
}
|
|
try {
|
|
const url2 = new URL(incomingUrl);
|
|
req[urlKey] = url2.href;
|
|
} catch (e) {
|
|
throw new RequestError("Invalid absolute URL", { cause: e });
|
|
}
|
|
return req;
|
|
}
|
|
const host = (incoming instanceof import_node_http2.Http2ServerRequest ? incoming.authority : incoming.headers.host) || defaultHostname;
|
|
if (!host) {
|
|
throw new RequestError("Missing host header");
|
|
}
|
|
let scheme;
|
|
if (incoming instanceof import_node_http2.Http2ServerRequest) {
|
|
scheme = incoming.scheme;
|
|
if (!(scheme === "http" || scheme === "https")) {
|
|
throw new RequestError("Unsupported scheme");
|
|
}
|
|
} else {
|
|
scheme = incoming.socket && incoming.socket.encrypted ? "https" : "http";
|
|
}
|
|
const url = new URL(`${scheme}://${host}${incomingUrl}`);
|
|
if (url.hostname.length !== host.length && url.hostname !== host.replace(/:\d+$/, "")) {
|
|
throw new RequestError("Invalid host header");
|
|
}
|
|
req[urlKey] = url.href;
|
|
return req;
|
|
};
|
|
|
|
// src/response.ts
|
|
var responseCache = Symbol("responseCache");
|
|
var getResponseCache = Symbol("getResponseCache");
|
|
var cacheKey = Symbol("cache");
|
|
var GlobalResponse = global.Response;
|
|
var Response2 = class _Response {
|
|
#body;
|
|
#init;
|
|
[getResponseCache]() {
|
|
delete this[cacheKey];
|
|
return this[responseCache] ||= new GlobalResponse(this.#body, this.#init);
|
|
}
|
|
constructor(body, init) {
|
|
let headers;
|
|
this.#body = body;
|
|
if (init instanceof _Response) {
|
|
const cachedGlobalResponse = init[responseCache];
|
|
if (cachedGlobalResponse) {
|
|
this.#init = cachedGlobalResponse;
|
|
this[getResponseCache]();
|
|
return;
|
|
} else {
|
|
this.#init = init.#init;
|
|
headers = new Headers(init.#init.headers);
|
|
}
|
|
} else {
|
|
this.#init = init;
|
|
}
|
|
if (typeof body === "string" || typeof body?.getReader !== "undefined" || body instanceof Blob || body instanceof Uint8Array) {
|
|
headers ||= init?.headers || { "content-type": "text/plain; charset=UTF-8" };
|
|
this[cacheKey] = [init?.status || 200, body, headers];
|
|
}
|
|
}
|
|
get headers() {
|
|
const cache = this[cacheKey];
|
|
if (cache) {
|
|
if (!(cache[2] instanceof Headers)) {
|
|
cache[2] = new Headers(cache[2]);
|
|
}
|
|
return cache[2];
|
|
}
|
|
return this[getResponseCache]().headers;
|
|
}
|
|
get status() {
|
|
return this[cacheKey]?.[0] ?? this[getResponseCache]().status;
|
|
}
|
|
get ok() {
|
|
const status = this.status;
|
|
return status >= 200 && status < 300;
|
|
}
|
|
};
|
|
["body", "bodyUsed", "redirected", "statusText", "trailers", "type", "url"].forEach((k) => {
|
|
Object.defineProperty(Response2.prototype, k, {
|
|
get() {
|
|
return this[getResponseCache]()[k];
|
|
}
|
|
});
|
|
});
|
|
["arrayBuffer", "blob", "clone", "formData", "json", "text"].forEach((k) => {
|
|
Object.defineProperty(Response2.prototype, k, {
|
|
value: function() {
|
|
return this[getResponseCache]()[k]();
|
|
}
|
|
});
|
|
});
|
|
Object.setPrototypeOf(Response2, GlobalResponse);
|
|
Object.setPrototypeOf(Response2.prototype, GlobalResponse.prototype);
|
|
|
|
// src/utils.ts
|
|
async function readWithoutBlocking(readPromise) {
|
|
return Promise.race([readPromise, Promise.resolve().then(() => Promise.resolve(void 0))]);
|
|
}
|
|
function writeFromReadableStreamDefaultReader(reader, writable, currentReadPromise) {
|
|
const cancel = (error) => {
|
|
reader.cancel(error).catch(() => {
|
|
});
|
|
};
|
|
writable.on("close", cancel);
|
|
writable.on("error", cancel);
|
|
(currentReadPromise ?? reader.read()).then(flow, handleStreamError);
|
|
return reader.closed.finally(() => {
|
|
writable.off("close", cancel);
|
|
writable.off("error", cancel);
|
|
});
|
|
function handleStreamError(error) {
|
|
if (error) {
|
|
writable.destroy(error);
|
|
}
|
|
}
|
|
function onDrain() {
|
|
reader.read().then(flow, handleStreamError);
|
|
}
|
|
function flow({ done, value }) {
|
|
try {
|
|
if (done) {
|
|
writable.end();
|
|
} else if (!writable.write(value)) {
|
|
writable.once("drain", onDrain);
|
|
} else {
|
|
return reader.read().then(flow, handleStreamError);
|
|
}
|
|
} catch (e) {
|
|
handleStreamError(e);
|
|
}
|
|
}
|
|
}
|
|
function writeFromReadableStream(stream, writable) {
|
|
if (stream.locked) {
|
|
throw new TypeError("ReadableStream is locked.");
|
|
} else if (writable.destroyed) {
|
|
return;
|
|
}
|
|
return writeFromReadableStreamDefaultReader(stream.getReader(), writable);
|
|
}
|
|
var buildOutgoingHttpHeaders = (headers) => {
|
|
const res = {};
|
|
if (!(headers instanceof Headers)) {
|
|
headers = new Headers(headers ?? void 0);
|
|
}
|
|
const cookies = [];
|
|
for (const [k, v] of headers) {
|
|
if (k === "set-cookie") {
|
|
cookies.push(v);
|
|
} else {
|
|
res[k] = v;
|
|
}
|
|
}
|
|
if (cookies.length > 0) {
|
|
res["set-cookie"] = cookies;
|
|
}
|
|
res["content-type"] ??= "text/plain; charset=UTF-8";
|
|
return res;
|
|
};
|
|
|
|
// src/utils/response/constants.ts
|
|
var X_ALREADY_SENT = "x-hono-already-sent";
|
|
|
|
// src/globals.ts
|
|
var import_node_crypto = __toESM(require("crypto"));
|
|
var webFetch = global.fetch;
|
|
if (typeof global.crypto === "undefined") {
|
|
global.crypto = import_node_crypto.default;
|
|
}
|
|
global.fetch = (info, init) => {
|
|
init = {
|
|
// Disable compression handling so people can return the result of a fetch
|
|
// directly in the loader without messing with the Content-Encoding header.
|
|
compress: false,
|
|
...init
|
|
};
|
|
return webFetch(info, init);
|
|
};
|
|
|
|
// src/listener.ts
|
|
var outgoingEnded = Symbol("outgoingEnded");
|
|
var handleRequestError = () => new Response(null, {
|
|
status: 400
|
|
});
|
|
var handleFetchError = (e) => new Response(null, {
|
|
status: e instanceof Error && (e.name === "TimeoutError" || e.constructor.name === "TimeoutError") ? 504 : 500
|
|
});
|
|
var handleResponseError = (e, outgoing) => {
|
|
const err = e instanceof Error ? e : new Error("unknown error", { cause: e });
|
|
if (err.code === "ERR_STREAM_PREMATURE_CLOSE") {
|
|
console.info("The user aborted a request.");
|
|
} else {
|
|
console.error(e);
|
|
if (!outgoing.headersSent) {
|
|
outgoing.writeHead(500, { "Content-Type": "text/plain" });
|
|
}
|
|
outgoing.end(`Error: ${err.message}`);
|
|
outgoing.destroy(err);
|
|
}
|
|
};
|
|
var flushHeaders = (outgoing) => {
|
|
if ("flushHeaders" in outgoing && outgoing.writable) {
|
|
outgoing.flushHeaders();
|
|
}
|
|
};
|
|
var responseViaCache = async (res, outgoing) => {
|
|
let [status, body, header] = res[cacheKey];
|
|
if (header instanceof Headers) {
|
|
header = buildOutgoingHttpHeaders(header);
|
|
}
|
|
if (typeof body === "string") {
|
|
header["Content-Length"] = Buffer.byteLength(body);
|
|
} else if (body instanceof Uint8Array) {
|
|
header["Content-Length"] = body.byteLength;
|
|
} else if (body instanceof Blob) {
|
|
header["Content-Length"] = body.size;
|
|
}
|
|
outgoing.writeHead(status, header);
|
|
if (typeof body === "string" || body instanceof Uint8Array) {
|
|
outgoing.end(body);
|
|
} else if (body instanceof Blob) {
|
|
outgoing.end(new Uint8Array(await body.arrayBuffer()));
|
|
} else {
|
|
flushHeaders(outgoing);
|
|
await writeFromReadableStream(body, outgoing)?.catch(
|
|
(e) => handleResponseError(e, outgoing)
|
|
);
|
|
}
|
|
;
|
|
outgoing[outgoingEnded]?.();
|
|
};
|
|
var isPromise = (res) => typeof res.then === "function";
|
|
var responseViaResponseObject = async (res, outgoing, options = {}) => {
|
|
if (isPromise(res)) {
|
|
if (options.errorHandler) {
|
|
try {
|
|
res = await res;
|
|
} catch (err) {
|
|
const errRes = await options.errorHandler(err);
|
|
if (!errRes) {
|
|
return;
|
|
}
|
|
res = errRes;
|
|
}
|
|
} else {
|
|
res = await res.catch(handleFetchError);
|
|
}
|
|
}
|
|
if (cacheKey in res) {
|
|
return responseViaCache(res, outgoing);
|
|
}
|
|
const resHeaderRecord = buildOutgoingHttpHeaders(res.headers);
|
|
if (res.body) {
|
|
const reader = res.body.getReader();
|
|
const values = [];
|
|
let done = false;
|
|
let currentReadPromise = void 0;
|
|
if (resHeaderRecord["transfer-encoding"] !== "chunked") {
|
|
let maxReadCount = 2;
|
|
for (let i = 0; i < maxReadCount; i++) {
|
|
currentReadPromise ||= reader.read();
|
|
const chunk = await readWithoutBlocking(currentReadPromise).catch((e) => {
|
|
console.error(e);
|
|
done = true;
|
|
});
|
|
if (!chunk) {
|
|
if (i === 1) {
|
|
await new Promise((resolve) => setTimeout(resolve));
|
|
maxReadCount = 3;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
currentReadPromise = void 0;
|
|
if (chunk.value) {
|
|
values.push(chunk.value);
|
|
}
|
|
if (chunk.done) {
|
|
done = true;
|
|
break;
|
|
}
|
|
}
|
|
if (done && !("content-length" in resHeaderRecord)) {
|
|
resHeaderRecord["content-length"] = values.reduce((acc, value) => acc + value.length, 0);
|
|
}
|
|
}
|
|
outgoing.writeHead(res.status, resHeaderRecord);
|
|
values.forEach((value) => {
|
|
;
|
|
outgoing.write(value);
|
|
});
|
|
if (done) {
|
|
outgoing.end();
|
|
} else {
|
|
if (values.length === 0) {
|
|
flushHeaders(outgoing);
|
|
}
|
|
await writeFromReadableStreamDefaultReader(reader, outgoing, currentReadPromise);
|
|
}
|
|
} else if (resHeaderRecord[X_ALREADY_SENT]) {
|
|
} else {
|
|
outgoing.writeHead(res.status, resHeaderRecord);
|
|
outgoing.end();
|
|
}
|
|
;
|
|
outgoing[outgoingEnded]?.();
|
|
};
|
|
var getRequestListener = (fetchCallback, options = {}) => {
|
|
const autoCleanupIncoming = options.autoCleanupIncoming ?? true;
|
|
if (options.overrideGlobalObjects !== false && global.Request !== Request) {
|
|
Object.defineProperty(global, "Request", {
|
|
value: Request
|
|
});
|
|
Object.defineProperty(global, "Response", {
|
|
value: Response2
|
|
});
|
|
}
|
|
return async (incoming, outgoing) => {
|
|
let res, req;
|
|
try {
|
|
req = newRequest(incoming, options.hostname);
|
|
let incomingEnded = !autoCleanupIncoming || incoming.method === "GET" || incoming.method === "HEAD";
|
|
if (!incomingEnded) {
|
|
;
|
|
incoming[wrapBodyStream] = true;
|
|
incoming.on("end", () => {
|
|
incomingEnded = true;
|
|
});
|
|
if (incoming instanceof import_node_http22.Http2ServerRequest) {
|
|
;
|
|
outgoing[outgoingEnded] = () => {
|
|
if (!incomingEnded) {
|
|
setTimeout(() => {
|
|
if (!incomingEnded) {
|
|
setTimeout(() => {
|
|
incoming.destroy();
|
|
outgoing.destroy();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
};
|
|
}
|
|
}
|
|
outgoing.on("close", () => {
|
|
const abortController = req[abortControllerKey];
|
|
if (abortController) {
|
|
if (incoming.errored) {
|
|
req[abortControllerKey].abort(incoming.errored.toString());
|
|
} else if (!outgoing.writableFinished) {
|
|
req[abortControllerKey].abort("Client connection prematurely closed.");
|
|
}
|
|
}
|
|
if (!incomingEnded) {
|
|
setTimeout(() => {
|
|
if (!incomingEnded) {
|
|
setTimeout(() => {
|
|
incoming.destroy();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
res = fetchCallback(req, { incoming, outgoing });
|
|
if (cacheKey in res) {
|
|
return responseViaCache(res, outgoing);
|
|
}
|
|
} catch (e) {
|
|
if (!res) {
|
|
if (options.errorHandler) {
|
|
res = await options.errorHandler(req ? e : toRequestError(e));
|
|
if (!res) {
|
|
return;
|
|
}
|
|
} else if (!req) {
|
|
res = handleRequestError();
|
|
} else {
|
|
res = handleFetchError(e);
|
|
}
|
|
} else {
|
|
return handleResponseError(e, outgoing);
|
|
}
|
|
}
|
|
try {
|
|
return await responseViaResponseObject(res, outgoing, options);
|
|
} catch (e) {
|
|
return handleResponseError(e, outgoing);
|
|
}
|
|
};
|
|
};
|
|
|
|
// src/server.ts
|
|
var createAdaptorServer = (options) => {
|
|
const fetchCallback = options.fetch;
|
|
const requestListener = getRequestListener(fetchCallback, {
|
|
hostname: options.hostname,
|
|
overrideGlobalObjects: options.overrideGlobalObjects,
|
|
autoCleanupIncoming: options.autoCleanupIncoming
|
|
});
|
|
const createServer = options.createServer || import_node_http.createServer;
|
|
const server = createServer(options.serverOptions || {}, requestListener);
|
|
return server;
|
|
};
|
|
var serve = (options, listeningListener) => {
|
|
const server = createAdaptorServer(options);
|
|
server.listen(options?.port ?? 3e3, options.hostname, () => {
|
|
const serverInfo = server.address();
|
|
listeningListener && listeningListener(serverInfo);
|
|
});
|
|
return server;
|
|
};
|
|
// Annotate the CommonJS export names for ESM import in node:
|
|
0 && (module.exports = {
|
|
createAdaptorServer,
|
|
serve
|
|
});
|