Borrow more complete todo app from upstream TanStack/db as a more attractive example.

This commit is contained in:
Sebastian Jeltsch
2025-07-20 11:42:11 +02:00
parent 47c22255c6
commit 84e0e408ca
19 changed files with 343 additions and 1596 deletions
+11 -12
View File
@@ -1,16 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="data:image/png;base64,iVBORw0KGgo=" />
<link href="/src/index.css" rel="stylesheet" />
<title>Local-First Todo Example</title>
</head>
<head>
<meta charset="UTF-8" />
<link rel="icon" href="data:image/png;base64,iVBORw0KGgo=">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Local-First Example</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
+2
View File
@@ -19,10 +19,12 @@
"@tanstack/trailbase-db-collection": "^0.0.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwindcss": "^4.1.11",
"trailbase": "workspace:*"
},
"devDependencies": {
"@eslint/js": "^9.31.0",
"@tailwindcss/vite": "^4.1.11",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.7.0",
-42
View File
@@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
+225 -73
View File
@@ -1,40 +1,39 @@
import { useState, type FormEvent } from "react";
import { QueryClient } from "@tanstack/query-core";
import { useLiveQuery, createCollection } from "@tanstack/react-db";
import { queryCollectionOptions } from "@tanstack/query-db-collection";
import { trailBaseCollectionOptions } from "@tanstack/trailbase-db-collection";
import { initClient, type Client } from "trailbase";
import { useState } from "react";
import type { FormEvent } from "react";
import "./App.css";
import { getComplementaryColor } from "./lib/color";
const client: Client = initClient("http://localhost:4000");
type Data = {
id: number | null;
updated: number | null;
data: string;
};
const queryClient = new QueryClient();
const useTrailBase = true;
const dataCollection = useTrailBase
type Config = {
id: number;
key: string;
value: string;
created_at: number;
updated_at: number;
};
const configCollection = useTrailBase
? createCollection(
trailBaseCollectionOptions<Data>({
recordApi: client.records<Data>("data"),
trailBaseCollectionOptions<Config>({
recordApi: client.records<Config>("config"),
getKey: (item) => item.id ?? -1,
parse: {},
serialize: {},
}),
)
: createCollection(
queryCollectionOptions<Data>({
id: "data",
queryKey: ["data"],
queryCollectionOptions<Config>({
id: "config",
queryKey: ["config"],
queryFn: async () => {
const data = client.records<Data>("data");
const data = client.records<Config>("config");
return (await data.list()).records;
},
getKey: (item) => item.id ?? -1,
@@ -42,73 +41,226 @@ const dataCollection = useTrailBase
}),
);
function App() {
const [input, setInput] = useState("");
type Todo = {
id: number;
text: string;
completed: boolean;
created_at: number;
updated_at: number;
};
const { data } = useLiveQuery((q) =>
q.from({ record: dataCollection }).orderBy(({ record }) => record.updated),
const todoCollection = useTrailBase
? createCollection(
trailBaseCollectionOptions<Todo>({
recordApi: client.records<Todo>("todos"),
getKey: (item) => item.id ?? -1,
parse: {},
serialize: {},
}),
)
: createCollection(
queryCollectionOptions<Todo>({
id: "todos",
queryKey: ["todos"],
queryFn: async () => {
const data = client.records<Todo>("todos");
return (await data.list()).records;
},
getKey: (item) => item.id ?? -1,
queryClient: queryClient,
}),
);
function now(): number {
return Math.floor(Date.now() / 1000);
}
function App() {
// Get data using live queries with TrailBase collections
const { data: todos } = useLiveQuery((q) =>
q
.from({ todo: todoCollection })
.orderBy(({ todo }) => todo.created_at, `asc`),
);
function handleSubmit(e: FormEvent) {
e.preventDefault(); // Don't reload the page.
const { data: configData } = useLiveQuery((q) =>
q.from({ config: configCollection }),
);
const form = e.target;
const formData = new FormData(form as HTMLFormElement);
const [newTodo, setNewTodo] = useState(``);
const formJson = Object.fromEntries(formData.entries());
const text = formJson.text as string;
if (text) {
dataCollection.insert({
id: null,
updated: null,
data: formJson.text as string,
});
setInput("");
// Define a type-safe helper function to get config values
const getConfigValue = (key: string): string | undefined => {
for (const config of configData) {
if (config.key === key) {
return config.value;
}
}
}
return undefined;
};
// Define a helper function to update config values
const setConfigValue = (key: string, value: string): void => {
for (const config of configData) {
if (config.key === key) {
configCollection.update(config.id, (draft) => {
draft.value = value;
});
return;
}
}
// If the config doesn't exist yet, create it
configCollection.insert({
id: Math.round(Math.random() * 1000000),
key,
value,
created_at: now(),
updated_at: now(),
});
};
const backgroundColor = getConfigValue(`backgroundColor`);
const titleColor = getComplementaryColor(backgroundColor);
const handleColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newColor = e.target.value;
setConfigValue(`backgroundColor`, newColor);
};
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
const todo = newTodo.trim();
setNewTodo(``);
if (todo) {
todoCollection.insert({
text: todo,
completed: false,
id: Math.round(Math.random() * 1000000),
created_at: now(),
updated_at: now(),
});
}
};
const activeTodos = todos.filter((todo) => !todo.completed);
const completedTodos = todos.filter((todo) => todo.completed);
return (
<>
<h1>Local First</h1>
<main
className="flex h-dvh justify-center overflow-auto py-8"
style={{ backgroundColor }}
>
<div className="w-[550px]">
<h1
className="mb-4 text-center text-[70px] font-bold"
style={{ color: titleColor }}
>
TrailBase Todo
</h1>
<div className="card">
<table>
<thead>
<tr>
<th>id</th>
<th>updated</th>
<th>data</th>
</tr>
</thead>
<div className="flex justify-end py-4">
<div className="flex items-center">
<label
htmlFor="colorPicker"
className="mr-2 text-sm font-medium text-gray-700"
style={{ color: titleColor }}
>
Background Color:
</label>
<input
type="color"
id="colorPicker"
value={backgroundColor}
onChange={handleColorChange}
className="cursor-pointer rounded border border-gray-300"
/>
</div>
</div>
<tbody>
{data.map((d, idx) => (
<tr key={`row-${idx}`}>
<td>{d.id}</td>
<td>{d.updated}</td>
<td>{d.data}</td>
</tr>
<div className="relative bg-white shadow-[0_2px_4px_0_rgba(0,0,0,0.2),0_25px_50px_0_rgba(0,0,0,0.1)]">
<form onSubmit={handleSubmit} className="relative">
<button
type="button"
className="absolute h-full w-12 text-[30px] text-[#e6e6e6] hover:text-[#4d4d4d]"
disabled={todos.length === 0}
onClick={() => {
const todosToToggle =
activeTodos.length > 0 ? activeTodos : completedTodos;
todoCollection.update(
todosToToggle.map((todo) => todo.id),
(drafts) =>
drafts.forEach(
(draft) => (draft.completed = !draft.completed),
),
);
}}
>
</button>
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="What needs to be done?"
className="box-border h-[64px] w-full border-none pr-4 pl-[60px] text-2xl font-light shadow-[inset_0_-2px_1px_rgba(0,0,0,0.03)]"
/>
</form>
<ul className="list-none">
{todos.map((todo) => (
<li
key={`todo-${todo.id}`}
className="group relative border-b border-[#ededed] last:border-none"
>
<div className="gap-1.2 flex h-[58px] items-center pl-[60px]">
<input
type="checkbox"
checked={todo.completed}
onChange={() =>
todoCollection.update(todo.id, (draft) => {
draft.completed = !draft.completed;
})
}
className="absolute left-[12px] size-[40px] cursor-pointer"
/>
<label
className={`block p-[15px] text-2xl transition-colors ${todo.completed ? `text-[#d9d9d9] line-through` : ``}`}
>
{todo.text}
</label>
<button
onClick={() => todoCollection.delete(todo.id)}
className="absolute right-[20px] hidden text-[30px] text-[#cc9a9a] transition-colors group-hover:block hover:text-[#af5b5e]"
>
×
</button>
</div>
</li>
))}
</tbody>
</table>
</ul>
<footer className="flex h-[40px] items-center justify-between border-t border-[#e6e6e6] px-[15px] text-[14px] text-[#777]">
<span>
{`${activeTodos.length} ${activeTodos.length === 1 ? `item` : `items`} left`}
</span>
{completedTodos.length > 0 && (
<button
onClick={() =>
todoCollection.delete(completedTodos.map((todo) => todo.id))
}
className="hover:underline"
>
Clear completed
</button>
)}
</footer>
</div>
</div>
<form method="post" onSubmit={handleSubmit}>
<p className="read-the-docs">
<input
name="text"
type="text"
value={input}
onInput={(e) => setInput(e.currentTarget.value)}
/>
<button type="submit" disabled={input === ""}>
submit
</button>
</p>
</form>
</>
</main>
);
}
+1 -68
View File
@@ -1,68 +1 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
@import "tailwindcss";
+39
View File
@@ -0,0 +1,39 @@
// Function to generate a complementary color
export function getComplementaryColor(hexColor: string | undefined): string {
// Default to a nice blue if no color is provided
if (!hexColor) return `#3498db`;
// Remove the hash if it exists
const color = hexColor.replace(/^#/, ``);
// Convert hex to RGB
const r = parseInt(color.substring(0, 2), 16);
const g = parseInt(color.substring(2, 2), 16);
const b = parseInt(color.substring(4, 2), 16);
// Calculate complementary color (inverting the RGB values)
const compR = 255 - r;
const compG = 255 - g;
const compB = 255 - b;
// Calculate brightness of the background
const brightness = r * 0.299 + g * 0.587 + b * 0.114;
// If the complementary color doesn't have enough contrast, adjust it
const compBrightness = compR * 0.299 + compG * 0.587 + compB * 0.114;
const brightnessDiff = Math.abs(brightness - compBrightness);
if (brightnessDiff < 128) {
// Not enough contrast, use a more vibrant alternative
if (brightness > 128) {
// Dark color for light background
return `#8e44ad`; // Purple
} else {
// Light color for dark background
return `#f1c40f`; // Yellow
}
}
// Convert back to hex
return `#${((1 << 24) + (compR << 16) + (compG << 8) + compB).toString(16).slice(1)}`;
}
@@ -3,3 +3,6 @@ backups/
data/
secrets/
uploads/
trailbase.js
trailbase.d.ts
@@ -1,33 +1,24 @@
# Auto-generated config.Config textproto
email {
user_verification_template {
subject: "Verify your Email Address for {{ APP_NAME }}"
body: "<html>\n <body>\n <h1>Welcome {{ EMAIL }}</h1>\n\n <p>\n Thanks for joining {{ APP_NAME }}.\n </p>\n\n <p>\n To be able to log in, first validate your email by clicking the link below.\n </p>\n\n <a class=\"btn\" href=\"{{ VERIFICATION_URL }}\">\n {{ VERIFICATION_URL }}\n </a>\n </body>\n</html>"
}
password_reset_template {
subject: "Reset your Password for {{ APP_NAME }}"
body: "<html>\n <body>\n <h1>Password Reset</h1>\n\n <p>\n Click the link below to reset your password.\n </p>\n\n <a class=\"btn\" href=\"{{ VERIFICATION_URL }}\">\n {{ VERIFICATION_URL }}\n </a>\n </body>\n</html>"
}
change_email_template {
subject: "Change your Email Address for {{ APP_NAME }}"
body: "<html>\n <body>\n <h1>Change E-Mail Address</h1>\n\n <p>\n Click the link below to verify your new E-mail address:\n </p>\n\n <a class=\"btn\" href=\"{{ VERIFICATION_URL }}\">\n {{ VERIFICATION_URL }}\n </a>\n </body>\n</html>"
}
}
email {}
server {
application_name: "TrailBase"
application_name: "TanStack-DB TrailBase Example"
logs_retention_sec: 604800
}
auth {
auth_token_ttl_sec: 120
auth_token_ttl_sec: 3600
refresh_token_ttl_sec: 2592000
}
jobs {}
record_apis: [
{
name: "data"
table_name: "data"
conflict_resolution: REPLACE
enable_subscriptions: true
name: "todos"
table_name: "todos"
acl_world: [CREATE, READ, UPDATE, DELETE]
enable_subscriptions: true
},
{
name: "config"
table_name: "config"
acl_world: [CREATE, READ, UPDATE, DELETE]
enable_subscriptions: true
}
]
@@ -1,4 +0,0 @@
INSERT INTO _user
(id, email, password_hash, verified, admin)
VALUES
(uuid_v7(), 'admin@localhost', (hash_password('secret')), TRUE, TRUE);
@@ -1,12 +0,0 @@
CREATE TABLE IF NOT EXISTS data (
id INTEGER PRIMARY KEY,
updated INTEGER DEFAULT (UNIXEPOCH()) NOT NULL,
data TEXT NOT NULL
) STRICT;
CREATE TRIGGER __data__updated_trigger AFTER UPDATE ON data FOR EACH ROW
BEGIN
UPDATE data SET updated = UNIXEPOCH() WHERE id = OLD.id;
END;
INSERT INTO data (data) VALUES ('0'), ('1');
@@ -0,0 +1,5 @@
-- Create default admin user with top "secret" password.
INSERT INTO _user
(email, password_hash, verified, admin)
VALUES
('admin@localhost', (hash_password('secret')), TRUE, TRUE);
@@ -0,0 +1,12 @@
CREATE TABLE todos (
"id" INTEGER PRIMARY KEY NOT NULL,
"text" TEXT NOT NULL,
"completed" INTEGER NOT NULL DEFAULT 0,
"created_at" INTEGER NOT NULL DEFAULT(UNIXEPOCH()),
"updated_at" INTEGER NOT NULL DEFAULT(UNIXEPOCH())
) STRICT;
CREATE TRIGGER _todos__update_trigger AFTER UPDATE ON todos FOR EACH ROW
BEGIN
UPDATE todos SET updated_at = UNIXEPOCH() WHERE id = OLD.id;
END;
@@ -0,0 +1,17 @@
CREATE TABLE config (
"id" INTEGER PRIMARY KEY NOT NULL,
"key" TEXT NOT NULL,
"value" TEXT NOT NULL,
"created_at" INTEGER NOT NULL DEFAULT(UNIXEPOCH()),
"updated_at" INTEGER NOT NULL DEFAULT(UNIXEPOCH())
) STRICT;
CREATE UNIQUE INDEX _config_key_index ON config ("key");
CREATE TRIGGER _config__update_trigger AFTER UPDATE ON config FOR EACH ROW
BEGIN
UPDATE config SET updated_at = UNIXEPOCH() WHERE id = OLD.id;
END;
-- Insert default config for background color
INSERT INTO config ("key", "value") VALUES ('backgroundColor', '#f5f5f5');
-899
View File
@@ -1,899 +0,0 @@
export declare function addCronCallback(name: string, schedule: string, cb: () => void | Promise<void>): void;
export declare function addPeriodicCallback(milliseconds: number, cb: (cancel: () => void) => void): () => void;
export declare function addRoute(method: Method, route: string, callback: CallbackType): void;
export declare type CallbackType = (req: RequestType) => MaybeResponse<ResponseType_2>;
declare namespace Deno_2 {
interface ReadFileOptions {
/**
* An abort signal to allow cancellation of the file read operation.
* If the signal becomes aborted the readFile operation will be stopped
* and the promise returned will be rejected with an AbortError.
*/
signal?: AbortSignal;
}
interface WriteFileOptions {
/** If set to `true`, will append to a file instead of overwriting previous
* contents.
*
* @default {false} */
append?: boolean;
/** Sets the option to allow creating a new file, if one doesn't already
* exist at the specified path.
*
* @default {true} */
create?: boolean;
/** If set to `true`, no file, directory, or symlink is allowed to exist at
* the target location. When createNew is set to `true`, `create` is ignored.
*
* @default {false} */
createNew?: boolean;
/** Permissions always applied to file. */
mode?: number;
/** An abort signal to allow cancellation of the file write operation.
*
* If the signal becomes aborted the write file operation will be stopped
* and the promise returned will be rejected with an {@linkcode AbortError}.
*/
signal?: AbortSignal;
}
/**
* Options which can be set when using {@linkcode Deno.makeTempDir},
* {@linkcode Deno.makeTempDirSync}, {@linkcode Deno.makeTempFile}, and
* {@linkcode Deno.makeTempFileSync}.
*
* @category File System */
interface MakeTempOptions {
/** Directory where the temporary directory should be created (defaults to
* the env variable `TMPDIR`, or the system's default, usually `/tmp`).
*
* Note that if the passed `dir` is relative, the path returned by
* `makeTempFile()` and `makeTempDir()` will also be relative. Be mindful of
* this when changing working directory. */
dir?: string;
/** String that should precede the random portion of the temporary
* directory's name. */
prefix?: string;
/** String that should follow the random portion of the temporary
* directory's name. */
suffix?: string;
}
/**
* Options which can be set when using {@linkcode Deno.mkdir} and
* {@linkcode Deno.mkdirSync}.
*
* @category File System */
interface MkdirOptions {
/** If set to `true`, means that any intermediate directories will also be
* created (as with the shell command `mkdir -p`).
*
* Intermediate directories are created with the same permissions.
*
* When recursive is set to `true`, succeeds silently (without changing any
* permissions) if a directory already exists at the path, or if the path
* is a symlink to an existing directory.
*
* @default {false} */
recursive?: boolean;
/** Permissions to use when creating the directory (defaults to `0o777`,
* before the process's umask).
*
* Ignored on Windows. */
mode?: number;
}
/**
* Information about a directory entry returned from {@linkcode Deno.readDir}
* and {@linkcode Deno.readDirSync}.
*
* @category File System */
interface DirEntry {
/** The file name of the entry. It is just the entity name and does not
* include the full path. */
name: string;
/** True if this is info for a regular file. Mutually exclusive to
* `DirEntry.isDirectory` and `DirEntry.isSymlink`. */
isFile: boolean;
/** True if this is info for a regular directory. Mutually exclusive to
* `DirEntry.isFile` and `DirEntry.isSymlink`. */
isDirectory: boolean;
/** True if this is info for a symlink. Mutually exclusive to
* `DirEntry.isFile` and `DirEntry.isDirectory`. */
isSymlink: boolean;
}
/**
* Options which can be set when doing {@linkcode Deno.open} and
* {@linkcode Deno.openSync}.
*
* @category File System */
interface OpenOptions {
/** Sets the option for read access. This option, when `true`, means that
* the file should be read-able if opened.
*
* @default {true} */
read?: boolean;
/** Sets the option for write access. This option, when `true`, means that
* the file should be write-able if opened. If the file already exists,
* any write calls on it will overwrite its contents, by default without
* truncating it.
*
* @default {false} */
write?: boolean;
/** Sets the option for the append mode. This option, when `true`, means
* that writes will append to a file instead of overwriting previous
* contents.
*
* Note that setting `{ write: true, append: true }` has the same effect as
* setting only `{ append: true }`.
*
* @default {false} */
append?: boolean;
/** Sets the option for truncating a previous file. If a file is
* successfully opened with this option set it will truncate the file to `0`
* size if it already exists. The file must be opened with write access
* for truncate to work.
*
* @default {false} */
truncate?: boolean;
/** Sets the option to allow creating a new file, if one doesn't already
* exist at the specified path. Requires write or append access to be
* used.
*
* @default {false} */
create?: boolean;
/** If set to `true`, no file, directory, or symlink is allowed to exist at
* the target location. Requires write or append access to be used. When
* createNew is set to `true`, create and truncate are ignored.
*
* @default {false} */
createNew?: boolean;
/** Permissions to use if creating the file (defaults to `0o666`, before
* the process's umask).
*
* Ignored on Windows. */
mode?: number;
}
/**
* Options which can be set when using {@linkcode Deno.remove} and
* {@linkcode Deno.removeSync}.
*
* @category File System */
interface RemoveOptions {
/** If set to `true`, path will be removed even if it's a non-empty directory.
*
* @default {false} */
recursive?: boolean;
}
/** Options that can be used with {@linkcode symlink} and
* {@linkcode symlinkSync}.
*
* @category File System */
interface SymlinkOptions {
/** Specify the symbolic link type as file, directory or NTFS junction. This
* option only applies to Windows and is ignored on other operating systems. */
type: "file" | "dir" | "junction";
}
function writeFile(path: string | URL, data: Uint8Array | ReadableStream<Uint8Array>, options?: WriteFileOptions): Promise<void>;
function writeTextFile(path: string | URL, data: string | ReadableStream<string>, options?: WriteFileOptions): Promise<void>;
function readTextFile(path: string | URL, options?: ReadFileOptions): Promise<string>;
function readFile(path: string | URL, options?: ReadFileOptions): Promise<Uint8Array>;
function chmod(path: string | URL, mode: number): Promise<void>;
function chown(path: string | URL, uid: number | null, gid: number | null): Promise<void>;
function cwd(): string;
function makeTempDir(options?: MakeTempOptions): Promise<string>;
function makeTempFile(options?: MakeTempOptions): Promise<string>;
function mkdir(path: string | URL, options?: MkdirOptions): Promise<void>;
function chdir(directory: string | URL): void;
function copyFile(fromPath: string | URL, toPath: string | URL): Promise<void>;
function readDir(path: string | URL): AsyncIterable<DirEntry>;
function readLink(path: string | URL): Promise<string>;
function realPath(path: string | URL): Promise<string>;
function remove(path: string | URL, options?: RemoveOptions): Promise<void>;
function rename(oldpath: string | URL, newpath: string | URL): Promise<void>;
function stat(path: string | URL): Promise<FileInfo>;
function lstat(path: string | URL): Promise<FileInfo>;
function truncate(name: string, len?: number): Promise<void>;
function open(path: string | URL, options?: OpenOptions): Promise<FsFile>;
function create(path: string | URL): Promise<FsFile>;
function symlink(oldpath: string | URL, newpath: string | URL, options?: SymlinkOptions): Promise<void>;
function link(oldpath: string, newpath: string): Promise<void>;
function utime(path: string | URL, atime: number | Date, mtime: number | Date): Promise<void>;
function umask(mask?: number): number;
/** Provides information about a file and is returned by
* {@linkcode Deno.stat}, {@linkcode Deno.lstat}, {@linkcode Deno.statSync},
* and {@linkcode Deno.lstatSync} or from calling `stat()` and `statSync()`
* on an {@linkcode Deno.FsFile} instance.
*
* @category File System
*/
interface FileInfo {
/** True if this is info for a regular file. Mutually exclusive to
* `FileInfo.isDirectory` and `FileInfo.isSymlink`. */
isFile: boolean;
/** True if this is info for a regular directory. Mutually exclusive to
* `FileInfo.isFile` and `FileInfo.isSymlink`. */
isDirectory: boolean;
/** True if this is info for a symlink. Mutually exclusive to
* `FileInfo.isFile` and `FileInfo.isDirectory`. */
isSymlink: boolean;
/** The size of the file, in bytes. */
size: number;
/** The last modification time of the file. This corresponds to the `mtime`
* field from `stat` on Linux/Mac OS and `ftLastWriteTime` on Windows. This
* may not be available on all platforms. */
mtime: Date | null;
/** The last access time of the file. This corresponds to the `atime`
* field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not
* be available on all platforms. */
atime: Date | null;
/** The creation time of the file. This corresponds to the `birthtime`
* field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may
* not be available on all platforms. */
birthtime: Date | null;
/** The last change time of the file. This corresponds to the `ctime`
* field from `stat` on Mac/BSD and `ChangeTime` on Windows. This may
* not be available on all platforms. */
ctime: Date | null;
/** ID of the device containing the file. */
dev: number;
/** Inode number.
*
* _Linux/Mac OS only._ */
ino: number | null;
/** The underlying raw `st_mode` bits that contain the standard Unix
* permissions for this file/directory.
*/
mode: number | null;
/** Number of hard links pointing to this file.
*
* _Linux/Mac OS only._ */
nlink: number | null;
/** User ID of the owner of this file.
*
* _Linux/Mac OS only._ */
uid: number | null;
/** Group ID of the owner of this file.
*
* _Linux/Mac OS only._ */
gid: number | null;
/** Device ID of this file.
*
* _Linux/Mac OS only._ */
rdev: number | null;
/** Blocksize for filesystem I/O.
*
* _Linux/Mac OS only._ */
blksize: number | null;
/** Number of blocks allocated to the file, in 512-byte units.
*
* _Linux/Mac OS only._ */
blocks: number | null;
/** True if this is info for a block device.
*
* _Linux/Mac OS only._ */
isBlockDevice: boolean | null;
/** True if this is info for a char device.
*
* _Linux/Mac OS only._ */
isCharDevice: boolean | null;
/** True if this is info for a fifo.
*
* _Linux/Mac OS only._ */
isFifo: boolean | null;
/** True if this is info for a socket.
*
* _Linux/Mac OS only._ */
isSocket: boolean | null;
}
/**
* A enum which defines the seek mode for IO related APIs that support
* seeking.
*
* @category I/O */
enum SeekMode {
Start = 0,
Current = 1,
End = 2
}
/** @category I/O */
interface SetRawOptions {
/**
* The `cbreak` option can be used to indicate that characters that
* correspond to a signal should still be generated. When disabling raw
* mode, this option is ignored. This functionality currently only works on
* Linux and Mac OS.
*/
cbreak: boolean;
}
class FsFile implements Disposable {
/** A {@linkcode ReadableStream} instance representing to the byte contents
* of the file. This makes it easy to interoperate with other web streams
* based APIs.
*
* ```ts
* using file = await Deno.open("my_file.txt", { read: true });
* const decoder = new TextDecoder();
* for await (const chunk of file.readable) {
* console.log(decoder.decode(chunk));
* }
* ```
*/
readonly readable: ReadableStream<Uint8Array>;
/** A {@linkcode WritableStream} instance to write the contents of the
* file. This makes it easy to interoperate with other web streams based
* APIs.
*
* ```ts
* const items = ["hello", "world"];
* using file = await Deno.open("my_file.txt", { write: true });
* const encoder = new TextEncoder();
* const writer = file.writable.getWriter();
* for (const item of items) {
* await writer.write(encoder.encode(item));
* }
* ```
*/
readonly writable: WritableStream<Uint8Array>;
/** Write the contents of the array buffer (`p`) to the file.
*
* Resolves to the number of bytes written.
*
* **It is not guaranteed that the full buffer will be written in a single
* call.**
*
* ```ts
* const encoder = new TextEncoder();
* const data = encoder.encode("Hello world");
* using file = await Deno.open("/foo/bar.txt", { write: true });
* const bytesWritten = await file.write(data); // 11
* ```
*
* @category I/O
*/
write(p: Uint8Array): Promise<number>;
/** Synchronously write the contents of the array buffer (`p`) to the file.
*
* Returns the number of bytes written.
*
* **It is not guaranteed that the full buffer will be written in a single
* call.**
*
* ```ts
* const encoder = new TextEncoder();
* const data = encoder.encode("Hello world");
* using file = Deno.openSync("/foo/bar.txt", { write: true });
* const bytesWritten = file.writeSync(data); // 11
* ```
*/
writeSync(p: Uint8Array): number;
/** Truncates (or extends) the file to reach the specified `len`. If `len`
* is not specified, then the entire file contents are truncated.
*
* ### Truncate the entire file
*
* ```ts
* using file = await Deno.open("my_file.txt", { write: true });
* await file.truncate();
* ```
*
* ### Truncate part of the file
*
* ```ts
* // if "my_file.txt" contains the text "hello world":
* using file = await Deno.open("my_file.txt", { write: true });
* await file.truncate(7);
* const buf = new Uint8Array(100);
* await file.read(buf);
* const text = new TextDecoder().decode(buf); // "hello w"
* ```
*/
truncate(len?: number): Promise<void>;
/** Synchronously truncates (or extends) the file to reach the specified
* `len`. If `len` is not specified, then the entire file contents are
* truncated.
*
* ### Truncate the entire file
*
* ```ts
* using file = Deno.openSync("my_file.txt", { write: true });
* file.truncateSync();
* ```
*
* ### Truncate part of the file
*
* ```ts
* // if "my_file.txt" contains the text "hello world":
* using file = Deno.openSync("my_file.txt", { write: true });
* file.truncateSync(7);
* const buf = new Uint8Array(100);
* file.readSync(buf);
* const text = new TextDecoder().decode(buf); // "hello w"
* ```
*/
truncateSync(len?: number): void;
/** Read the file into an array buffer (`p`).
*
* Resolves to either the number of bytes read during the operation or EOF
* (`null`) if there was nothing more to read.
*
* It is possible for a read to successfully return with `0` bytes. This
* does not indicate EOF.
*
* **It is not guaranteed that the full buffer will be read in a single
* call.**
*
* ```ts
* // if "/foo/bar.txt" contains the text "hello world":
* using file = await Deno.open("/foo/bar.txt");
* const buf = new Uint8Array(100);
* const numberOfBytesRead = await file.read(buf); // 11 bytes
* const text = new TextDecoder().decode(buf); // "hello world"
* ```
*/
read(p: Uint8Array): Promise<number | null>;
/** Synchronously read from the file into an array buffer (`p`).
*
* Returns either the number of bytes read during the operation or EOF
* (`null`) if there was nothing more to read.
*
* It is possible for a read to successfully return with `0` bytes. This
* does not indicate EOF.
*
* **It is not guaranteed that the full buffer will be read in a single
* call.**
*
* ```ts
* // if "/foo/bar.txt" contains the text "hello world":
* using file = Deno.openSync("/foo/bar.txt");
* const buf = new Uint8Array(100);
* const numberOfBytesRead = file.readSync(buf); // 11 bytes
* const text = new TextDecoder().decode(buf); // "hello world"
* ```
*/
readSync(p: Uint8Array): number | null;
/** Seek to the given `offset` under mode given by `whence`. The call
* resolves to the new position within the resource (bytes from the start).
*
* ```ts
* // Given the file contains "Hello world" text, which is 11 bytes long:
* using file = await Deno.open(
* "hello.txt",
* { read: true, write: true, truncate: true, create: true },
* );
* await file.write(new TextEncoder().encode("Hello world"));
*
* // advance cursor 6 bytes
* const cursorPosition = await file.seek(6, Deno.SeekMode.Start);
* console.log(cursorPosition); // 6
* const buf = new Uint8Array(100);
* await file.read(buf);
* console.log(new TextDecoder().decode(buf)); // "world"
* ```
*
* The seek modes work as follows:
*
* ```ts
* // Given the file contains "Hello world" text, which is 11 bytes long:
* const file = await Deno.open(
* "hello.txt",
* { read: true, write: true, truncate: true, create: true },
* );
* await file.write(new TextEncoder().encode("Hello world"));
*
* // Seek 6 bytes from the start of the file
* console.log(await file.seek(6, Deno.SeekMode.Start)); // "6"
* // Seek 2 more bytes from the current position
* console.log(await file.seek(2, Deno.SeekMode.Current)); // "8"
* // Seek backwards 2 bytes from the end of the file
* console.log(await file.seek(-2, Deno.SeekMode.End)); // "9" (i.e. 11-2)
* ```
*/
seek(offset: number | bigint, whence: SeekMode): Promise<number>;
/** Synchronously seek to the given `offset` under mode given by `whence`.
* The new position within the resource (bytes from the start) is returned.
*
* ```ts
* using file = Deno.openSync(
* "hello.txt",
* { read: true, write: true, truncate: true, create: true },
* );
* file.writeSync(new TextEncoder().encode("Hello world"));
*
* // advance cursor 6 bytes
* const cursorPosition = file.seekSync(6, Deno.SeekMode.Start);
* console.log(cursorPosition); // 6
* const buf = new Uint8Array(100);
* file.readSync(buf);
* console.log(new TextDecoder().decode(buf)); // "world"
* ```
*
* The seek modes work as follows:
*
* ```ts
* // Given the file contains "Hello world" text, which is 11 bytes long:
* using file = Deno.openSync(
* "hello.txt",
* { read: true, write: true, truncate: true, create: true },
* );
* file.writeSync(new TextEncoder().encode("Hello world"));
*
* // Seek 6 bytes from the start of the file
* console.log(file.seekSync(6, Deno.SeekMode.Start)); // "6"
* // Seek 2 more bytes from the current position
* console.log(file.seekSync(2, Deno.SeekMode.Current)); // "8"
* // Seek backwards 2 bytes from the end of the file
* console.log(file.seekSync(-2, Deno.SeekMode.End)); // "9" (i.e. 11-2)
* ```
*/
seekSync(offset: number | bigint, whence: SeekMode): number;
/** Resolves to a {@linkcode Deno.FileInfo} for the file.
*
* ```ts
* import { assert } from "jsr:@std/assert";
*
* using file = await Deno.open("hello.txt");
* const fileInfo = await file.stat();
* assert(fileInfo.isFile);
* ```
*/
stat(): Promise<FileInfo>;
/** Synchronously returns a {@linkcode Deno.FileInfo} for the file.
*
* ```ts
* import { assert } from "jsr:@std/assert";
*
* using file = Deno.openSync("hello.txt")
* const fileInfo = file.statSync();
* assert(fileInfo.isFile);
* ```
*/
statSync(): FileInfo;
/**
* Flushes any pending data and metadata operations of the given file
* stream to disk.
*
* ```ts
* const file = await Deno.open(
* "my_file.txt",
* { read: true, write: true, create: true },
* );
* await file.write(new TextEncoder().encode("Hello World"));
* await file.truncate(1);
* await file.sync();
* console.log(await Deno.readTextFile("my_file.txt")); // H
* ```
*
* @category I/O
*/
sync(): Promise<void>;
/**
* Synchronously flushes any pending data and metadata operations of the given
* file stream to disk.
*
* ```ts
* const file = Deno.openSync(
* "my_file.txt",
* { read: true, write: true, create: true },
* );
* file.writeSync(new TextEncoder().encode("Hello World"));
* file.truncateSync(1);
* file.syncSync();
* console.log(Deno.readTextFileSync("my_file.txt")); // H
* ```
*
* @category I/O
*/
syncSync(): void;
/**
* Flushes any pending data operations of the given file stream to disk.
* ```ts
* using file = await Deno.open(
* "my_file.txt",
* { read: true, write: true, create: true },
* );
* await file.write(new TextEncoder().encode("Hello World"));
* await file.syncData();
* console.log(await Deno.readTextFile("my_file.txt")); // Hello World
* ```
*
* @category I/O
*/
syncData(): Promise<void>;
/**
* Synchronously flushes any pending data operations of the given file stream
* to disk.
*
* ```ts
* using file = Deno.openSync(
* "my_file.txt",
* { read: true, write: true, create: true },
* );
* file.writeSync(new TextEncoder().encode("Hello World"));
* file.syncDataSync();
* console.log(Deno.readTextFileSync("my_file.txt")); // Hello World
* ```
*
* @category I/O
*/
syncDataSync(): void;
/**
* Changes the access (`atime`) and modification (`mtime`) times of the
* file stream resource. Given times are either in seconds (UNIX epoch
* time) or as `Date` objects.
*
* ```ts
* using file = await Deno.open("file.txt", { create: true, write: true });
* await file.utime(1556495550, new Date());
* ```
*
* @category File System
*/
utime(atime: number | Date, mtime: number | Date): Promise<void>;
/**
* Synchronously changes the access (`atime`) and modification (`mtime`)
* times of the file stream resource. Given times are either in seconds
* (UNIX epoch time) or as `Date` objects.
*
* ```ts
* using file = Deno.openSync("file.txt", { create: true, write: true });
* file.utime(1556495550, new Date());
* ```
*
* @category File System
*/
utimeSync(atime: number | Date, mtime: number | Date): void;
/** **UNSTABLE**: New API, yet to be vetted.
*
* Checks if the file resource is a TTY (terminal).
*
* ```ts
* // This example is system and context specific
* using file = await Deno.open("/dev/tty6");
* file.isTerminal(); // true
* ```
*/
isTerminal(): boolean;
/** **UNSTABLE**: New API, yet to be vetted.
*
* Set TTY to be under raw mode or not. In raw mode, characters are read and
* returned as is, without being processed. All special processing of
* characters by the terminal is disabled, including echoing input
* characters. Reading from a TTY device in raw mode is faster than reading
* from a TTY device in canonical mode.
*
* ```ts
* using file = await Deno.open("/dev/tty6");
* file.setRaw(true, { cbreak: true });
* ```
*/
setRaw(mode: boolean, options?: SetRawOptions): void;
/**
* Acquire an advisory file-system lock for the file.
*
* @param [exclusive=false]
*/
lock(exclusive?: boolean): Promise<void>;
/**
* Synchronously acquire an advisory file-system lock synchronously for the file.
*
* @param [exclusive=false]
*/
lockSync(exclusive?: boolean): void;
/**
* Release an advisory file-system lock for the file.
*/
unlock(): Promise<void>;
/**
* Synchronously release an advisory file-system lock for the file.
*/
unlockSync(): void;
/** Close the file. Closing a file when you are finished with it is
* important to avoid leaking resources.
*
* ```ts
* using file = await Deno.open("my_file.txt");
* // do work with "file" object
* ```
*/
close(): void;
[Symbol.dispose](): void;
}
}
export declare function execute(sql: string, params: unknown[]): Promise<number>;
export declare namespace fs {
const writeFile: typeof Deno_2.writeFile;
const writeTextFile: typeof Deno_2.writeTextFile;
const readTextFile: typeof Deno_2.readTextFile;
const readFile: typeof Deno_2.readFile;
const chmod: typeof Deno_2.chmod;
const chown: typeof Deno_2.chown;
const cwd: typeof Deno_2.cwd;
const makeTempDir: typeof Deno_2.makeTempDir;
const makeTempFile: typeof Deno_2.makeTempFile;
const mkdir: typeof Deno_2.mkdir;
const chdir: typeof Deno_2.chdir;
const copyFile: typeof Deno_2.copyFile;
const readDir: typeof Deno_2.readDir;
const readLink: typeof Deno_2.readLink;
const realPath: typeof Deno_2.realPath;
const remove: typeof Deno_2.remove;
const rename: typeof Deno_2.rename;
const stat: typeof Deno_2.stat;
const lstat: typeof Deno_2.lstat;
const truncate: typeof Deno_2.truncate;
const FsFile: typeof Deno_2.FsFile;
const open: typeof Deno_2.open;
const create: typeof Deno_2.create;
const symlink: typeof Deno_2.symlink;
const link: typeof Deno_2.link;
const utime: typeof Deno_2.utime;
const umask: typeof Deno_2.umask;
}
export declare type HeaderMapType = {
[key: string]: string;
};
export declare function htmlHandler(f: (req: StringRequestType) => MaybeResponse<HtmlResponseType | string>): CallbackType;
export declare type HtmlResponseType = {
headers?: [string, string][];
status?: number;
body: string;
};
export declare class HttpError extends Error {
readonly statusCode: number;
readonly headers: [string, string][] | undefined;
constructor(statusCode: number, message?: string, headers?: [string, string][]);
toString(): string;
toResponse(): ResponseType_2;
}
export declare function jsonHandler(f: (req: JsonRequestType) => MaybeResponse<JsonRequestType | object>): CallbackType;
export declare type JsonRequestType = {
uri: string;
params: PathParamsType;
headers: HeaderMapType;
user?: UserType;
body?: object | string;
};
export declare interface JsonResponseType {
headers?: [string, string][];
status?: number;
body: object;
}
export declare type MaybeResponse<T> = Promise<T | undefined> | T | undefined;
export declare type Method = "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT" | "TRACE";
export declare type ParsedPath = {
path: string;
query: URLSearchParams;
};
export declare function parsePath(path: string): ParsedPath;
export declare type PathParamsType = {
[key: string]: string;
};
export declare function query(sql: string, params: unknown[]): Promise<unknown[][]>;
export declare type RequestType = {
uri: string;
params: PathParamsType;
headers: HeaderMapType;
user?: UserType;
body?: Uint8Array;
};
declare type ResponseType_2 = {
headers?: [string, string][];
status?: number;
body?: Uint8Array;
};
export { ResponseType_2 as ResponseType }
export declare enum StatusCodes {
CONTINUE = 100,
SWITCHING_PROTOCOLS = 101,
PROCESSING = 102,
EARLY_HINTS = 103,
OK = 200,
CREATED = 201,
ACCEPTED = 202,
NON_AUTHORITATIVE_INFORMATION = 203,
NO_CONTENT = 204,
RESET_CONTENT = 205,
PARTIAL_CONTENT = 206,
MULTI_STATUS = 207,
MULTIPLE_CHOICES = 300,
MOVED_PERMANENTLY = 301,
MOVED_TEMPORARILY = 302,
SEE_OTHER = 303,
NOT_MODIFIED = 304,
USE_PROXY = 305,
TEMPORARY_REDIRECT = 307,
PERMANENT_REDIRECT = 308,
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
PAYMENT_REQUIRED = 402,
FORBIDDEN = 403,
NOT_FOUND = 404,
METHOD_NOT_ALLOWED = 405,
NOT_ACCEPTABLE = 406,
PROXY_AUTHENTICATION_REQUIRED = 407,
REQUEST_TIMEOUT = 408,
CONFLICT = 409,
GONE = 410,
LENGTH_REQUIRED = 411,
PRECONDITION_FAILED = 412,
REQUEST_TOO_LONG = 413,
REQUEST_URI_TOO_LONG = 414,
UNSUPPORTED_MEDIA_TYPE = 415,
REQUESTED_RANGE_NOT_SATISFIABLE = 416,
EXPECTATION_FAILED = 417,
IM_A_TEAPOT = 418,
INSUFFICIENT_SPACE_ON_RESOURCE = 419,
METHOD_FAILURE = 420,
MISDIRECTED_REQUEST = 421,
UNPROCESSABLE_ENTITY = 422,
LOCKED = 423,
FAILED_DEPENDENCY = 424,
UPGRADE_REQUIRED = 426,
PRECONDITION_REQUIRED = 428,
TOO_MANY_REQUESTS = 429,
REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
UNAVAILABLE_FOR_LEGAL_REASONS = 451,
INTERNAL_SERVER_ERROR = 500,
NOT_IMPLEMENTED = 501,
BAD_GATEWAY = 502,
SERVICE_UNAVAILABLE = 503,
GATEWAY_TIMEOUT = 504,
HTTP_VERSION_NOT_SUPPORTED = 505,
INSUFFICIENT_STORAGE = 507,
NETWORK_AUTHENTICATION_REQUIRED = 511
}
export declare function stringHandler(f: (req: StringRequestType) => MaybeResponse<StringResponseType | string>): CallbackType;
export declare type StringRequestType = {
uri: string;
params: PathParamsType;
headers: HeaderMapType;
user?: UserType;
body?: string;
};
export declare type StringResponseType = {
headers?: [string, string][];
status?: number;
body: string;
};
export declare class Transaction {
finalized: boolean;
constructor();
query(queryStr: string, params: unknown[]): unknown[][];
execute(queryStr: string, params: unknown[]): number;
commit(): void;
rollback(): void;
}
export declare function transaction<T>(f: (tx: Transaction) => T): Promise<T>;
export declare type UserType = {
id: string;
email: string;
csrf: string;
};
export { }
@@ -1,460 +0,0 @@
var fs;
((fs2) => {
fs2.writeFile = Deno.writeFile;
fs2.writeTextFile = Deno.writeTextFile;
fs2.readTextFile = Deno.readTextFile;
fs2.readFile = Deno.readFile;
fs2.chmod = Deno.chmod;
fs2.chown = Deno.chown;
fs2.cwd = Deno.cwd;
fs2.makeTempDir = Deno.makeTempDir;
fs2.makeTempFile = Deno.makeTempFile;
fs2.mkdir = Deno.mkdir;
fs2.chdir = Deno.chdir;
fs2.copyFile = Deno.copyFile;
fs2.readDir = Deno.readDir;
fs2.readLink = Deno.readLink;
fs2.realPath = Deno.realPath;
fs2.remove = Deno.remove;
fs2.rename = Deno.rename;
fs2.stat = Deno.stat;
fs2.lstat = Deno.lstat;
fs2.truncate = Deno.truncate;
fs2.FsFile = Deno.FsFile;
fs2.open = Deno.open;
fs2.create = Deno.create;
fs2.symlink = Deno.symlink;
fs2.link = Deno.link;
fs2.utime = Deno.utime;
fs2.umask = Deno.umask;
})(fs || (fs = {}));
function decodeFallback(bytes) {
var inputIndex = 0;
var pendingSize = Math.min(256 * 256, bytes.length + 1);
var pending = new Uint16Array(pendingSize);
var chunks = [];
var pendingIndex = 0;
for (; ; ) {
var more = inputIndex < bytes.length;
if (!more || pendingIndex >= pendingSize - 1) {
var subarray = pending.subarray(0, pendingIndex);
var arraylike = subarray;
chunks.push(String.fromCharCode.apply(null, arraylike));
if (!more) {
return chunks.join("");
}
bytes = bytes.subarray(inputIndex);
inputIndex = 0;
pendingIndex = 0;
}
var byte1 = bytes[inputIndex++];
if ((byte1 & 128) === 0) {
pending[pendingIndex++] = byte1;
} else if ((byte1 & 224) === 192) {
var byte2 = bytes[inputIndex++] & 63;
pending[pendingIndex++] = (byte1 & 31) << 6 | byte2;
} else if ((byte1 & 240) === 224) {
var byte2 = bytes[inputIndex++] & 63;
var byte3 = bytes[inputIndex++] & 63;
pending[pendingIndex++] = (byte1 & 31) << 12 | byte2 << 6 | byte3;
} else if ((byte1 & 248) === 240) {
var byte2 = bytes[inputIndex++] & 63;
var byte3 = bytes[inputIndex++] & 63;
var byte4 = bytes[inputIndex++] & 63;
var codepoint = (byte1 & 7) << 18 | byte2 << 12 | byte3 << 6 | byte4;
if (codepoint > 65535) {
codepoint -= 65536;
pending[pendingIndex++] = codepoint >>> 10 & 1023 | 55296;
codepoint = 56320 | codepoint & 1023;
}
pending[pendingIndex++] = codepoint;
} else ;
}
}
function encodeFallback(string) {
var pos = 0;
var len = string.length;
var at = 0;
var tlen = Math.max(32, len + (len >>> 1) + 7);
var target = new Uint8Array(tlen >>> 3 << 3);
while (pos < len) {
var value = string.charCodeAt(pos++);
if (value >= 55296 && value <= 56319) {
if (pos < len) {
var extra = string.charCodeAt(pos);
if ((extra & 64512) === 56320) {
++pos;
value = ((value & 1023) << 10) + (extra & 1023) + 65536;
}
}
if (value >= 55296 && value <= 56319) {
continue;
}
}
if (at + 4 > target.length) {
tlen += 8;
tlen *= 1 + pos / string.length * 2;
tlen = tlen >>> 3 << 3;
var update = new Uint8Array(tlen);
update.set(target);
target = update;
}
if ((value & 4294967168) === 0) {
target[at++] = value;
continue;
} else if ((value & 4294965248) === 0) {
target[at++] = value >>> 6 & 31 | 192;
} else if ((value & 4294901760) === 0) {
target[at++] = value >>> 12 & 15 | 224;
target[at++] = value >>> 6 & 63 | 128;
} else if ((value & 4292870144) === 0) {
target[at++] = value >>> 18 & 7 | 240;
target[at++] = value >>> 12 & 63 | 128;
target[at++] = value >>> 6 & 63 | 128;
} else {
continue;
}
target[at++] = value & 63 | 128;
}
return target.slice ? target.slice(0, at) : target.subarray(0, at);
}
var StatusCodes = /* @__PURE__ */ ((StatusCodes2) => {
StatusCodes2[StatusCodes2["CONTINUE"] = 100] = "CONTINUE";
StatusCodes2[StatusCodes2["SWITCHING_PROTOCOLS"] = 101] = "SWITCHING_PROTOCOLS";
StatusCodes2[StatusCodes2["PROCESSING"] = 102] = "PROCESSING";
StatusCodes2[StatusCodes2["EARLY_HINTS"] = 103] = "EARLY_HINTS";
StatusCodes2[StatusCodes2["OK"] = 200] = "OK";
StatusCodes2[StatusCodes2["CREATED"] = 201] = "CREATED";
StatusCodes2[StatusCodes2["ACCEPTED"] = 202] = "ACCEPTED";
StatusCodes2[StatusCodes2["NON_AUTHORITATIVE_INFORMATION"] = 203] = "NON_AUTHORITATIVE_INFORMATION";
StatusCodes2[StatusCodes2["NO_CONTENT"] = 204] = "NO_CONTENT";
StatusCodes2[StatusCodes2["RESET_CONTENT"] = 205] = "RESET_CONTENT";
StatusCodes2[StatusCodes2["PARTIAL_CONTENT"] = 206] = "PARTIAL_CONTENT";
StatusCodes2[StatusCodes2["MULTI_STATUS"] = 207] = "MULTI_STATUS";
StatusCodes2[StatusCodes2["MULTIPLE_CHOICES"] = 300] = "MULTIPLE_CHOICES";
StatusCodes2[StatusCodes2["MOVED_PERMANENTLY"] = 301] = "MOVED_PERMANENTLY";
StatusCodes2[StatusCodes2["MOVED_TEMPORARILY"] = 302] = "MOVED_TEMPORARILY";
StatusCodes2[StatusCodes2["SEE_OTHER"] = 303] = "SEE_OTHER";
StatusCodes2[StatusCodes2["NOT_MODIFIED"] = 304] = "NOT_MODIFIED";
StatusCodes2[StatusCodes2["USE_PROXY"] = 305] = "USE_PROXY";
StatusCodes2[StatusCodes2["TEMPORARY_REDIRECT"] = 307] = "TEMPORARY_REDIRECT";
StatusCodes2[StatusCodes2["PERMANENT_REDIRECT"] = 308] = "PERMANENT_REDIRECT";
StatusCodes2[StatusCodes2["BAD_REQUEST"] = 400] = "BAD_REQUEST";
StatusCodes2[StatusCodes2["UNAUTHORIZED"] = 401] = "UNAUTHORIZED";
StatusCodes2[StatusCodes2["PAYMENT_REQUIRED"] = 402] = "PAYMENT_REQUIRED";
StatusCodes2[StatusCodes2["FORBIDDEN"] = 403] = "FORBIDDEN";
StatusCodes2[StatusCodes2["NOT_FOUND"] = 404] = "NOT_FOUND";
StatusCodes2[StatusCodes2["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
StatusCodes2[StatusCodes2["NOT_ACCEPTABLE"] = 406] = "NOT_ACCEPTABLE";
StatusCodes2[StatusCodes2["PROXY_AUTHENTICATION_REQUIRED"] = 407] = "PROXY_AUTHENTICATION_REQUIRED";
StatusCodes2[StatusCodes2["REQUEST_TIMEOUT"] = 408] = "REQUEST_TIMEOUT";
StatusCodes2[StatusCodes2["CONFLICT"] = 409] = "CONFLICT";
StatusCodes2[StatusCodes2["GONE"] = 410] = "GONE";
StatusCodes2[StatusCodes2["LENGTH_REQUIRED"] = 411] = "LENGTH_REQUIRED";
StatusCodes2[StatusCodes2["PRECONDITION_FAILED"] = 412] = "PRECONDITION_FAILED";
StatusCodes2[StatusCodes2["REQUEST_TOO_LONG"] = 413] = "REQUEST_TOO_LONG";
StatusCodes2[StatusCodes2["REQUEST_URI_TOO_LONG"] = 414] = "REQUEST_URI_TOO_LONG";
StatusCodes2[StatusCodes2["UNSUPPORTED_MEDIA_TYPE"] = 415] = "UNSUPPORTED_MEDIA_TYPE";
StatusCodes2[StatusCodes2["REQUESTED_RANGE_NOT_SATISFIABLE"] = 416] = "REQUESTED_RANGE_NOT_SATISFIABLE";
StatusCodes2[StatusCodes2["EXPECTATION_FAILED"] = 417] = "EXPECTATION_FAILED";
StatusCodes2[StatusCodes2["IM_A_TEAPOT"] = 418] = "IM_A_TEAPOT";
StatusCodes2[StatusCodes2["INSUFFICIENT_SPACE_ON_RESOURCE"] = 419] = "INSUFFICIENT_SPACE_ON_RESOURCE";
StatusCodes2[StatusCodes2["METHOD_FAILURE"] = 420] = "METHOD_FAILURE";
StatusCodes2[StatusCodes2["MISDIRECTED_REQUEST"] = 421] = "MISDIRECTED_REQUEST";
StatusCodes2[StatusCodes2["UNPROCESSABLE_ENTITY"] = 422] = "UNPROCESSABLE_ENTITY";
StatusCodes2[StatusCodes2["LOCKED"] = 423] = "LOCKED";
StatusCodes2[StatusCodes2["FAILED_DEPENDENCY"] = 424] = "FAILED_DEPENDENCY";
StatusCodes2[StatusCodes2["UPGRADE_REQUIRED"] = 426] = "UPGRADE_REQUIRED";
StatusCodes2[StatusCodes2["PRECONDITION_REQUIRED"] = 428] = "PRECONDITION_REQUIRED";
StatusCodes2[StatusCodes2["TOO_MANY_REQUESTS"] = 429] = "TOO_MANY_REQUESTS";
StatusCodes2[StatusCodes2["REQUEST_HEADER_FIELDS_TOO_LARGE"] = 431] = "REQUEST_HEADER_FIELDS_TOO_LARGE";
StatusCodes2[StatusCodes2["UNAVAILABLE_FOR_LEGAL_REASONS"] = 451] = "UNAVAILABLE_FOR_LEGAL_REASONS";
StatusCodes2[StatusCodes2["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
StatusCodes2[StatusCodes2["NOT_IMPLEMENTED"] = 501] = "NOT_IMPLEMENTED";
StatusCodes2[StatusCodes2["BAD_GATEWAY"] = 502] = "BAD_GATEWAY";
StatusCodes2[StatusCodes2["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE";
StatusCodes2[StatusCodes2["GATEWAY_TIMEOUT"] = 504] = "GATEWAY_TIMEOUT";
StatusCodes2[StatusCodes2["HTTP_VERSION_NOT_SUPPORTED"] = 505] = "HTTP_VERSION_NOT_SUPPORTED";
StatusCodes2[StatusCodes2["INSUFFICIENT_STORAGE"] = 507] = "INSUFFICIENT_STORAGE";
StatusCodes2[StatusCodes2["NETWORK_AUTHENTICATION_REQUIRED"] = 511] = "NETWORK_AUTHENTICATION_REQUIRED";
return StatusCodes2;
})(StatusCodes || {});
class HttpError extends Error {
statusCode;
headers;
constructor(statusCode, message, headers) {
super(message);
this.statusCode = statusCode;
this.headers = headers;
}
toString() {
return `HttpError(${this.statusCode}, ${this.message})`;
}
toResponse() {
const m = this.message;
return {
headers: this.headers,
status: this.statusCode,
body: m !== "" ? encodeFallback(m) : void 0
};
}
}
function stringHandler(f) {
return async (req) => {
try {
const body = req.body;
const resp = await f({
uri: req.uri,
params: req.params,
headers: req.headers,
user: req.user,
body: body && decodeFallback(body)
});
if (resp === void 0) {
return void 0;
}
if (typeof resp === "string") {
return {
status: 200,
body: encodeFallback(resp)
};
}
const respBody = resp.body;
return {
headers: resp.headers,
status: resp.status,
body: respBody ? encodeFallback(respBody) : void 0
};
} catch (err) {
if (err instanceof HttpError) {
return err.toResponse();
}
return {
status: 500,
body: encodeFallback(`Uncaught error: ${err}`)
};
}
};
}
function htmlHandler(f) {
return async (req) => {
try {
const body = req.body;
const resp = await f({
uri: req.uri,
params: req.params,
headers: req.headers,
user: req.user,
body: body && decodeFallback(body)
});
if (resp === void 0) {
return void 0;
}
if (typeof resp === "string") {
return {
headers: [["content-type", "text/html"]],
status: 200,
body: encodeFallback(resp)
};
}
const respBody = resp.body;
return {
headers: [["content-type", "text/html"], ...resp.headers ?? []],
status: resp.status,
body: respBody ? encodeFallback(respBody) : void 0
};
} catch (err) {
if (err instanceof HttpError) {
return err.toResponse();
}
return {
status: 500,
body: encodeFallback(`Uncaught error: ${err}`)
};
}
};
}
function jsonHandler(f) {
return async (req) => {
try {
const body = req.body;
const resp = await f({
uri: req.uri,
params: req.params,
headers: req.headers,
user: req.user,
body: body && decodeFallback(body)
});
if (resp === void 0) {
return void 0;
}
if ("body" in resp) {
const r = resp;
const rBody = r.body;
return {
headers: [["content-type", "application/json"], ...r.headers ?? []],
status: r.status,
body: rBody ? encodeFallback(JSON.stringify(rBody)) : void 0
};
}
return {
headers: [["content-type", "application/json"]],
status: 200,
body: encodeFallback(JSON.stringify(resp))
};
} catch (err) {
if (err instanceof HttpError) {
return err.toResponse();
}
return {
headers: [["content-type", "application/json"]],
status: 500,
body: encodeFallback(`Uncaught error: ${err}`)
};
}
};
}
const routerCallbacks = /* @__PURE__ */ new Map();
function isolateId() {
return rustyscript.functions.isolate_id();
}
function addRoute(method, route, callback) {
if (isolateId() === 0) {
rustyscript.functions.install_route(method, route);
console.debug("JS: Added route:", method, route);
}
routerCallbacks.set(`${method}:${route}`, callback);
}
async function dispatch(method, route, uri, pathParams, headers, user, body) {
const key = `${method}:${route}`;
const cb = routerCallbacks.get(key);
if (!cb) {
throw Error(`Missing callback: ${key}`);
}
return await cb({
uri,
params: Object.fromEntries(pathParams),
headers: Object.fromEntries(headers),
user,
body
}) ?? {
status: 200
/* OK */
};
}
globalThis.__dispatch = dispatch;
const cronCallbacks = /* @__PURE__ */ new Map();
function addCronCallback(name, schedule, cb) {
const cronRegex = /^(@(yearly|monthly|weekly|daily|hourly|))|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*)\s*){6,7})$/;
const matches = cronRegex.test(schedule);
if (!matches) {
throw Error(`Not a valid 6/7-component cron schedule: ${schedule}`);
}
if (isolateId() === 0) {
const id = rustyscript.functions.install_job(name, schedule);
console.debug(`JS: Added cron job (id=${id}): "${name}"`);
cronCallbacks.set(id, cb);
}
}
async function dispatchCron(id) {
const cb = cronCallbacks.get(id);
if (!cb) {
throw Error(`Missing cron callback: ${id}`);
}
try {
await cb();
} catch (err) {
return `${err}`;
}
}
globalThis.__dispatchCron = dispatchCron;
function addPeriodicCallback(milliseconds, cb) {
if (isolateId() !== 0) {
return () => {
};
}
const handle = setInterval(() => {
cb(() => clearInterval(handle));
}, milliseconds);
return () => clearInterval(handle);
}
async function query(sql, params) {
return await rustyscript.async_functions.query(sql, params);
}
async function execute(sql, params) {
return await rustyscript.async_functions.execute(sql, params);
}
class Transaction {
finalized;
constructor() {
this.finalized = false;
}
query(queryStr, params) {
return rustyscript.functions.transaction_query(queryStr, params);
}
execute(queryStr, params) {
return rustyscript.functions.transaction_execute(queryStr, params);
}
commit() {
this.finalized = true;
rustyscript.functions.transaction_commit();
}
rollback() {
this.finalized = true;
rustyscript.functions.transaction_rollback();
}
}
async function transaction(f) {
await rustyscript.async_functions.transaction_begin();
const tx = new Transaction();
try {
const r = f(tx);
if (!tx.finalized) {
rustyscript.functions.transaction_rollback();
}
return r;
} catch (e) {
rustyscript.functions.transaction_rollback();
throw e;
}
}
function parsePath(path) {
const queryIndex = path.indexOf("?");
if (queryIndex >= 0) {
return {
path: path.slice(0, queryIndex),
query: new URLSearchParams(path.slice(queryIndex + 1))
};
}
return {
path,
query: new URLSearchParams()
};
}
const _logStderr = function(...args) {
globalThis.Deno.core.print(
`${args.join(" ")}
`,
/* to stderr = */
true
);
};
globalThis.console.log = _logStderr;
globalThis.console.info = _logStderr;
globalThis.console.debug = _logStderr;
export {
HttpError,
StatusCodes,
Transaction,
addCronCallback,
addPeriodicCallback,
addRoute,
execute,
fs,
htmlHandler,
jsonHandler,
parsePath,
query,
stringHandler,
transaction
};
+1 -4
View File
@@ -28,8 +28,5 @@
}
},
"include": ["src"],
"exclude": [
"dist",
"node_modules",
],
"exclude": ["dist", "node_modules"]
}
+2 -1
View File
@@ -1,7 +1,8 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
plugins: [react(), tailwindcss()],
});
+13
View File
@@ -368,6 +368,9 @@ importers:
react-dom:
specifier: ^19.1.0
version: 19.1.0(react@19.1.0)
tailwindcss:
specifier: ^4.1.11
version: 4.1.11
trailbase:
specifier: workspace:*
version: link:../../trailbase-assets/js/client
@@ -375,6 +378,9 @@ importers:
'@eslint/js':
specifier: ^9.31.0
version: 9.31.0
'@tailwindcss/vite':
specifier: ^4.1.11
version: 4.1.11(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.0)(yaml@2.8.0))
'@types/react':
specifier: ^19.1.8
version: 19.1.8
@@ -7945,6 +7951,13 @@ snapshots:
tailwindcss: 4.1.11
vite: 6.3.5(@types/node@24.0.15)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.0)(yaml@2.8.0)
'@tailwindcss/vite@4.1.11(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.0)(yaml@2.8.0))':
dependencies:
'@tailwindcss/node': 4.1.11
'@tailwindcss/oxide': 4.1.11
tailwindcss: 4.1.11
vite: 7.0.5(@types/node@24.0.15)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.0)(yaml@2.8.0)
'@tanstack/db@0.0.27(typescript@5.8.3)':
dependencies:
'@electric-sql/d2mini': 0.1.7