Glancerexample-callout.ts1 match
7// Initialize Notion client
8const notion = new Client({
9auth: Deno.env.get("NOTION_API_KEY"),
10});
11
filterValsmain.tsx1 match
3export async function filterVals(id: string, filter: (value: any, index: number, array: any[]) => unknown) {
4const token = Deno.env.get("valtown");
5const res = await fetchPaginatedData(`https://api.val.town/v1/users/${id}/vals`, {
6headers: { Authorization: `Bearer ${token}` },
7});
filterValsmain.tsx1 match
3export async function filterVals(id: string, filter: (value: any, index: number, array: any[]) => unknown) {
4const token = Deno.env.get("valtown");
5const res = await fetchPaginatedData(`https://api.val.town/v1/users/${id}/vals`, {
6headers: { Authorization: `Bearer ${token}` },
7});
blob_adminmain.tsx6 matches
1516// Public route without authentication
17app.get("/api/public/:id", async (c) => {
18const key = `__public/${c.req.param("id")}`;
19const { blob } = await import("https://esm.town/v/std/blob");
133};
134135app.get("/api/blobs", checkAuth, async (c) => {
136const prefix = c.req.query("prefix") || "";
137const limit = parseInt(c.req.query("limit") || "20", 10);
142});
143144app.get("/api/blob", checkAuth, async (c) => {
145const key = c.req.query("key");
146if (!key) return c.text("Missing key parameter", 400);
150});
151152app.put("/api/blob", checkAuth, async (c) => {
153const key = c.req.query("key");
154if (!key) return c.text("Missing key parameter", 400);
159});
160161app.delete("/api/blob", checkAuth, async (c) => {
162const key = c.req.query("key");
163if (!key) return c.text("Missing key parameter", 400);
167});
168169app.post("/api/blob", checkAuth, async (c) => {
170const { file, key } = await c.req.parseBody();
171if (!file || !key) return c.text("Missing file or key", 400);
blob_adminapp.tsx19 matches
70const menuRef = useRef(null);
71const isPublic = blob.key.startsWith("__public/");
72const publicUrl = isPublic ? `${window.location.origin}/api/public/${encodeURIComponent(blob.key.slice(9))}` : null;
7374useEffect(() => {
234setLoading(true);
235try {
236const response = await fetch(`/api/blobs?prefix=${encodeKey(searchPrefix)}&limit=${limit}`);
237const data = await response.json();
238setBlobs(data);
261setBlobContentLoading(true);
262try {
263const response = await fetch(`/api/blob?key=${encodeKey(clickedBlob.key)}`);
264const content = await response.text();
265setSelectedBlob({ ...clickedBlob, key: decodeKey(clickedBlob.key) });
275const handleSave = async () => {
276try {
277await fetch(`/api/blob?key=${encodeKey(selectedBlob.key)}`, {
278method: "PUT",
279body: editContent,
287const handleDelete = async (key) => {
288try {
289await fetch(`/api/blob?key=${encodeKey(key)}`, { method: "DELETE" });
290setBlobs(blobs.filter(b => b.key !== key));
291if (selectedBlob && selectedBlob.key === key) {
304const key = `${searchPrefix}${file.name}`;
305formData.append("key", encodeKey(key));
306await fetch("/api/blob", { method: "POST", body: formData });
307const newBlob = { key, size: file.size, lastModified: new Date().toISOString() };
308setBlobs([newBlob, ...blobs]);
326try {
327const fullKey = `${searchPrefix}${key}`;
328await fetch(`/api/blob?key=${encodeKey(fullKey)}`, {
329method: "PUT",
330body: "",
341const handleDownload = async (key) => {
342try {
343const response = await fetch(`/api/blob?key=${encodeKey(key)}`);
344const blob = await response.blob();
345const url = window.URL.createObjectURL(blob);
360if (newKey && newKey !== oldKey) {
361try {
362const response = await fetch(`/api/blob?key=${encodeKey(oldKey)}`);
363const content = await response.blob();
364await fetch(`/api/blob?key=${encodeKey(newKey)}`, {
365method: "PUT",
366body: content,
367});
368await fetch(`/api/blob?key=${encodeKey(oldKey)}`, { method: "DELETE" });
369setBlobs(blobs.map(b => b.key === oldKey ? { ...b, key: newKey } : b));
370if (selectedBlob && selectedBlob.key === oldKey) {
380const newKey = `__public/${key}`;
381try {
382const response = await fetch(`/api/blob?key=${encodeKey(key)}`);
383const content = await response.blob();
384await fetch(`/api/blob?key=${encodeKey(newKey)}`, {
385method: "PUT",
386body: content,
387});
388await fetch(`/api/blob?key=${encodeKey(key)}`, { method: "DELETE" });
389setBlobs(blobs.map(b => b.key === key ? { ...b, key: newKey } : b));
390if (selectedBlob && selectedBlob.key === key) {
399const newKey = key.slice(9); // Remove "__public/" prefix
400try {
401const response = await fetch(`/api/blob?key=${encodeKey(key)}`);
402const content = await response.blob();
403await fetch(`/api/blob?key=${encodeKey(newKey)}`, {
404method: "PUT",
405body: content,
406});
407await fetch(`/api/blob?key=${encodeKey(key)}`, { method: "DELETE" });
408setBlobs(blobs.map(b => b.key === key ? { ...b, key: newKey } : b));
409if (selectedBlob && selectedBlob.key === key) {
554onClick={() =>
555copyToClipboard(
556`${window.location.origin}/api/public/${encodeURIComponent(selectedBlob.key.slice(9))}`,
557)}
558className="text-blue-400 hover:text-blue-300 text-sm"
577>
578<img
579src={`/api/blob?key=${encodeKey(selectedBlob.key)}`}
580alt="Blob content"
581className="max-w-full h-auto"
ValTownBlog2025-04-17-vt-cli.md8 matches
34`vt` is also fast! Operations all happen concurrently whenever possible. Watch 62 files get cloned in 0.17 seconds!
3536`vt` is still in beta, and we'd love your feedback! Join [the Val Town Discord](https://discord.gg/hd9U4GGB) to leave feedback. Vt is built entirely within Val Town "userspace" (with the public API), and the codebase can be found at <https://github.com/val-town/vt> .
3738## Try it out!
48
4950Respond yes to the prompt, and ensure you select to create an API key with user read & project read+write permissions.
5152Then try cloning one of your projects with `vt clone`!
72
7374One of the very first sight of Val Town "localdev" came with Pomdtr's [Val Town vscode extension](https://marketplace.visualstudio.com/items?itemName=pomdtr.valtown). The extension was pretty straightforward, it hooked into the Val Town API to let you edit your vals locally and then push them to Val Town. It was made pre-project era, so it only supports traditional Vals. VSCode runs on electron, and gives extension developers a lot of power; the extension also offered web previews, and the ability to use Val Town SQL and blobstore.
7576A bit later, I came along and implemented a fuse file system for Val Town vals, also pre-project era. Fuse is a Linux protocol that lets you implement arbitrary file systems in userspace. By implementing a fuse Val Town file system, all edits locally are instantly reflected on the Val Town website, and vice versa (if you update remotely, then if you try to save locally your editor will say something along the lines of "more recent edits found, are you sure you want to write"). Fuse is very powerful -- writes, reads, and all other file system syscalls can be handled with whatever response you want.
90We decided to rewrite it mostly for compatibility reasons. Linux offers native fuse support, but MacOS and Windows definitely do not. For Mac, there's a project called [MacFuse](https://macfuse.github.io/) that acts as a kernel extension to provide fuse support to Mac. However, it's not totally stable, and [Apple is deprecating kernel extensions](https://developer.apple.com/support/kernel-extensions/) and it may not be the best long term solution. There's a really cool project called [fuse-t](https://www.fuse-t.org/) that takes a different approach, where it implements the fuse protocol by forwarding fuse to NFS (network file system), a protocol that Macs do naively support.
9192One idea we had to avoid dealing with fuse was to build a generic FTP server, largely inspired by Pomdtr's [Webdav](https://www.val.town/x/pomdtr/webdav/code/webdav.ts) server. His server works totally in userspace and maps the Val Town API to Webdav really nicely. [Webdav](https://en.wikipedia.org/wiki/WebDAV) is a generic "file system" ish (it's quite limited) protocol over HTTP, and making a FTP server would just be implementing on a fancier file transfer protocol that's more capable. What's nice about using protocols like these is that you can then just use `rclone` for mounting or syncing.
9394In addition to compatibility issues, implementing a fuse backend is just plain complicated. The way `valfs` worked, on [every write syscall](https://github.com/404Wolf/valfs/blob/418eff73b080fe8cdb7fe7f7afd3fe30dbae2720/valfs/vals/valfile.go#L116C1-L153C2), `valfs` would have to parse the file, extract metadata, and do multiple API calls. Even though `vt` is a total rewrite, many design choices for `vt` came from `vtfs`, like considerations on how we handle metadata, the notion that "a val is a file," and more.
9596At its core, `vt` was built to be a decentralized synchronization tool. Unlike `vtfs`, where the sync is guaranteed and live, syncing happens asynchronously with `vt` (even when "live syncing" with `vt watch`, which is only "psudo-live" since you could create conflicts by updating state on the website while watching). We rebuilt `vt` in typescript with `Deno`, because, well, we (and our community) love typescript and Deno, there's an official [typescript sdk](https://docs.val.town/api/sdk/) for Val Town, and because we have hopes of turning `vt` into a esm library in the future (so you can use `vt` in your own, non-val-town workflows).
9798We originally wanted to host `vt` itself on Val Town, where we would begin development with git and github, and then eventually transition to using `vt` itself to continue development of `vt` (bootstrapping!).
141One of the most important use cases of `vt` is "watch" functionality. There's a lot of reasons for this. One of the big ones is that, as you (and maybe your favorite LLM) edit your code locally, it is really handy to remove the friction of syncing with Val Town.
142143From the beginning, my plan for this was to implement "git" behavior first (pushing and pulling), and then just doing file system watching to add live syncing using [Deno.watchFs](https://docs.deno.com/api/deno/~/Deno.watchFs) to handle file system events.
144145
222I spent a long time working on setting up a pipe where I would handle consecutive writes, writes that start at index i, end at index i+k, and then a new write that starts at index i+k+1, etc, as a special case. Eventually, I got something working!
223224For `vtfs` I was using [Openapi Generator](https://github.com/OpenAPITools/openapi-generator), and it turned out that Val Town's OpenAPI specification didn't accept file sizes on the order of my tests -- where the response would include the file size, it was an integer, not a `format: int64` (long).
225226
ValTownForNotion1webhookAPI0 matches
1import { Hono } from "npm:hono@3";
2// import { cors } from "npm:hono@3/cors";
34// Import route modules
5import auth from "./routes/auth.ts";
ValTownForNotion1root.ts1 match
910const content = {
11headline: "👋 Welcome to the root API endpoint of this Hono app, running on val.town",
12message: "Find more detail in the README.",
13url: notionURL,
ValTownForNotion1resets4 matches
4// Initialize Notion client
5const notion = new Client({
6auth: Deno.env.get("NOTION_API_KEY"),
7});
837// the block exists, so you can do the reset
3839// reset via the API
40// this way we keep the notion data and the blob data (our cache) in sync
41// the API resets the blocks on notion pages, and also knows what to do with the blob
42// (note the x-blob-key header)
43if (content != "default" && diffInMinutes > .5) {
48"x-asking-for": "default",
49"x-blob-key": item.key,
50"x-api-key": Deno.env.get("X_API_KEY"),
51},
52body: JSON.stringify({
ValTownForNotion1README.md1 match
12## Handling Notion Webhooks
1314This is done in the webhookAPI file.
1516## Resetting state