Changelog

All versions and downloads Jump to Added | Changed | Fixed | Security

Unreleased [Not released yet]

Part 5 of 11. Previous part

Fixed (464)

  • Vercel: the build converts a weekday 7 to 0. It stops with an error for a schedule that restricts both the day of the month and the day of the week, which Vercel does not take. Before, the schedule went into config.json as written.
  • The build stops with "wisp::cron takes no L, W or #" for a schedule with L, W or # in the day of the month or the weekday (5L, 5#2, 5W). This holds on the Cloudflare, Pages, Vercel, Deno and Lambda (EventBridge) targets. wisp::cron does not take them. The W of the day name WED is no error. Before, the weekday case gave another text, and a weekday 5W gave a text about the weekday field.
  • The check for wisp::channel skips char literals ('"'), raw strings (r"C:\", r#"..."#) and lifetimes. Before, a '"' in a file hid the call, and the Cloudflare build wrote no WispRoom.
  • wisp build --target <host> writes the Open Graph cards of wisp::og(.., "auto") and the google fonts, as a binary build does. Before, the cards and the fonts were 404 on a host build.
  • wisp deploy init fly|render|railway|cloud-run builds the CSS, images, fonts and NPM modules, and changes .gitignore to keep .wisp/app.css, .wisp/npm, .wisp/img and .wisp/fonts in git. Before, an image that a host built from git had no built CSS or NPM modules.
  • The Cloudflare config (wrangler.json) of an app with wisp::channel adds the WispRoom binding and migration to the durable_objects and migrations of the user. Before, the file had each key twice, and a JSON reader kept one.
  • The Cloudflare config (wrangler.toml) reads a cron trigger written as a TOML literal string ('0 0 * * *'). Before, the build deleted the trigger with no message. A # comment in the same array no longer starts a string.
  • Vercel and Netlify with Edge routes: a Node route that wins over an edge pattern (item/new beside the edge route item/[id]) runs on the Node function. This holds for three more cases. One is a Node route with a matcher. One is a Node route beside an edge route with a matcher (item/[slug] beside item/[id=int]). One is a Node route beside an edge static route and an edge rest route (a/[x] beside a/b and a/[...r]). Vercel gets the rules of both kinds in the order of the route priority. Before, the edge pattern caught the Node route.
  • The WispRoom Durable Object deletes the event stream of a client that does not read it. It does so at the first send that finds more than 256 sends waiting for the stream. It also deletes the sends that wait, and the client can connect again. The count is sends, not bytes. Before, the queue of that stream grew without a limit.
  • The host build keeps a static file after a rename that changes only its case (Logo.png to logo.png) on a file system that ignores case. Before, the sweep deleted it.
  • WISP_HANDLER_TIMEOUT no longer closes a WebSocket. Before, the handler of each WebSocket ended at the timeout (30 seconds in the docs example), even while messages flowed, and the client got close code 1011. Now WISP_WS_IDLE closes a quiet client while the handler waits in recv. A handler that does not read must stop on Gone. A panic in the handler still closes it with 1011.
  • A WebSocket on a TCP socket breaks when a send or receive is dropped in the middle of a frame write, so no later frame follows the torn frame. Before, the next frame started after the cut and the client read garbage.
  • A WebSocket on a TCP socket now ends its side after the close frame and reads what the client still sends, for 2 seconds or 1 MiB at most. Before, the socket closed with unread bytes, and the reset could replace the close code (1009, 1008) with 1006 on the client.
  • wisp::sign_out_everywhere reads the store before it writes the count, and the new count is above the stored one. Before, an instance with an old count could write a lower count over the row of another instance, so sessions that a sign-out ended were valid again.
  • Kv::get_json and Sql::all and Sql::first read a stored Password as Hashed, as a saved table does. Before, it read back as Plain, and the next put_json hashed the hash, so the user could not sign in.
  • OAuth: the default callback address is ORIGIN + WISP_BASE + /auth/{name}/callback. Before, it had no WISP_BASE, so a sign-in under a base path failed. Requests to a provider carry user-agent: wisp on every target, as GitHub's API needs one and the edge fetch adds none.
  • Cx::signed_in gives a 503 while the server cannot read the table of sign-outs (wisp_sign_outs), and the server tries to read it again. Before, the server accepted every session, so a session that sign_out_everywhere ended worked again.
  • A job reads its Password back as Hashed, as a saved table does. Before, it read back as Plain.
  • REST PATCH hashes a password typed in a list or in a nested struct, and for a row that had none (Option<Password> that was None). A PUT or PATCH cannot set a #[json(skip)] field of a nested struct or a list item. The row keeps its value. A PATCH that cannot read the row back is a 500. A Password in a tuple struct is sealed like one in a named field. A deleted sign-out log ends the 503 of cx.signed_in(). Before, a PATCH stored the text as if it were a hash. A body that repeats a name hashes one password. Before, it hashed one for each repeat. A before_update hook that sets a Password typed gets it hashed outside the table lock. ?fields= lists each field once.
  • With public_env = true, a page that the build bakes (a page with no Rust) carries the PUBLIC_* tag. Before, import { env } from 'wisp' returned {} on such a page.
  • wisp lsp keeps the attribute name when it renames a shorthand. The shorthand <a ${href}> means href=${href}, so a rename of href to url now writes <a href=${url}>. Before, it wrote <a ${url}>, which sets the attribute url. The same holds for a prop in the file of its component and for a prop on a component tag (<Card ${title} />). A hole after an equal sign (href = ${url}) is a value, not a shorthand. A rename edits only its name.
  • wisp lsp finds the prop shorthand ${prop} on a component tag when it renames a prop. Before, it looked for {prop}, which the build rejects, and it missed ${prop}.
  • wisp lsp starts the scope of a name that a block head declares after the head. The expression of the head is outside the scope: in {#if let Some(user) = user}, a rename of the inner user no longer changes the right side.
  • wisp lsp stops a rename of a form action when a form posts to it with a full path in the same route (action="/todos?/save"). Before, the rename changed only the fn.
  • wisp lsp reads back at most 256 characters from a word to find out if it is a shorthand field. Before, a hole with very many words took quadratic time for highlights and references. Past the limit, the word counts as a possible field. Highlights and references leave it out, and a rename stops.
  • wisp lsp stops a rename of a component when its name is also in other text. That text can be a string, the --- block, a script, a style or an attribute value (of="Card"). In a .rs file, a name in a string stops it too. Before, the rename left that text with the old name.
  • wisp lsp reports an incomplete file list when a folder or an entry under src fails to read, except a folder that is gone. A rename does not use a partial list.
  • wisp lsp splits the parameters of a {#snippet} at the commas outside brackets. Before, p: (u32, u32) declared the name u32. Signature help no longer counts the > of -> as a close.
  • wisp lsp opens the file that serves a route for go to definition. That is the Markdown file of a Markdown route and the +page@.wisp file of a reset page. A parameter with the matcher int fits only digits that fit a u64. Before, it fit any segment.
  • wisp lsp keeps at most 64 closure parameter names per hole for semantic tokens. Before, a hole with very many parameters took quadratic time.
  • Kept pages (CACHE, PRERENDER) with {#await}: a component with --var props (<Shell --accent="red">) no longer breaks the kept shell. Before, css_vars added a style attribute or a <wisp-css> wrapper after the await cuts had their byte offsets. The answers then landed at wrong places. The cuts now move with the added bytes. A hole of an i18n kept page can use t("key"). A baked page now has the literal --x props of a component, as the run-time render writes them. A page with a --x prop that is not a literal renders at run time. Each await cut now has its own number, as a defense (the build already rejects an {#await} inside another).
  • wisp dev: when the post of the generation to the app fails, Wisp tries it up to three times. It stops after a try that gets no answer for more than one second, because the app may be hung. If no try works, Wisp does not count that update. Before, one failed post left the app with the old count. The stream told the new count. So every open tab reloaded without end. The dev client also reloads once for each count now.
  • wisp dev on Windows: wisp dev now finds the exit line of a companion when the last output of the command has no line end. Before, that output stuck to the exit line, so wisp dev did not see that the command had ended. It then ran taskkill on a shell pid that another program could own. Now it waits up to two seconds for the exit line. If the line does not come, it kills the shell only when that process still has the supervisor as its parent. A later line of output that looks like the pid line no longer changes the pid to kill.
  • Browser code: five fixes. A markup read selected === row.id after a read that changed (row.done ? row.note : '') no longer loses its update. A component in an {#await} answer inside an island loads late, as its island does. A <wisp:element this={:tag}> that is a whole block body, or its first or last node, now leaves with its block and no longer removes the DOM of other items. A transition whose easing the browser refuses, or whose options or function throw, now ends and reports the error. Before, an outro stayed on screen. A duration or delay is now 0 when it is no number or under 0, and at most 10 s. Before, duration: 20000 played 20 s. A plural t() works for a locale like pt_BR.
  • A release build with the build cache (the default) now also passes the rustc flags that remap the workspace folder to a relative path, as a release build without the cache does. Before, the cached build passed only the flag for the cache folder.
  • The offline worker names its caches with the WISP_BASE, and deletes only its own. Before, two apps on one origin (/a and /b) deleted the caches of each other. An app with a base path leaves the old caches wisp-<version> and wisp-pages in the browser, and the worker never serves them. The worker deletes its caches itself only when /service-worker.js answers 404, 410, or 200 with a reply that is not JavaScript. A redirect never deletes them. The 5 MB limit of "offline": "all" counts the pages already kept. A src/manifest.json that the build cannot read (for example UTF-16) is an error now, not "no manifest".
  • "offline": "all" keeps a page under its TRAILING_SLASH address (/about/ for Always), so the worker fetches the page ahead. With public_env = true, the worker fetches prerendered and baked pages again after each show, as the server changes their env tag at start.
  • An error in src/service-worker.js or .ts no longer panics the build when a multi-byte character comes before the error. Before, the line number came from the wrong text.
  • The script = "ts" check treats a { that starts no block, tag or ${ as text. Before, a lone { hid every later <script> in the file.
  • A [...rest] param with segments after it ends before them. Before, in repo/[...file]/edit, /repo/a/b/edit gave file = a/b/edit, and the URL of the typed routes:: builder gave a different value. Now file = a/b. In [...p]/[[x]], x used to be always empty. Now x gets the last segment. wisp route add accepts segments after a rest.
  • A route [...path]/edit now goes before [...path]. Before, the route with the bare rest took /foo/edit and the edit route never served.
  • Tables: a batch create that a table whose store could not be read cannot keep deletes its rows from memory again. A load of the store replaces the rows in memory. Before, such rows showed in lists and held their #[unique] keys until a restart.
  • Tables: deleting the highest row keeps the last id given, also when an add that failed took an id after that row. Before, a restart could give the id of a deleted row to a new row. This happened after a batch create that failed on a #[unique] field, then DELETE of the highest row.
  • Tables: a table whose store could not be read tries again after one second on every native host. Before, a host that does not start the clock thread (tower, Lambda) never tried again.
  • Tables: a poll that finds the table read again, or failed, applies none of the rows it read before.
  • WISP_DEV in .env.local or another mode file no longer splits the mode. Before, the files that Wisp loaded followed .env and the process, but dev read the mode files as well. Now both follow .env and the process only.
  • wisp::env("WISP_DEV") reads the value that chose the mode. Before, a WISP_DEV line in a mode file such as .env.local changed what it read, but not the mode. So an app that shows its own debug output when wisp::env("WISP_DEV") is on can act on the wrong value. Now Wisp ignores that line and logs it. A bad WISP_DEV value in a mode file no longer stops the server.
  • wisp::test: the client puts its cookies in the cookie header that the request already has. Before, the request had two cookie headers. A cookie expires date now counts its time of day.
  • fetch: a reply with more than 64 headers now fails with "the reply has too many headers". Before, the error said "the reply is not HTTP".
  • Content collections: a date must be a day that exists (2026-02-31 fails the build). A time after it must be HH:MM, with optional seconds, a fraction and a Z or +02:00 zone. A folder counts as an entry against the limit of 10000, so empty folders cannot make the walk unbounded.
  • wisp lsp skips rustfmt for a document whose last format ran over the 30 second limit, until the text of the document changes. Before, each format request waited 30 seconds again.
  • swr: interval counts from the last answer. A fetch that a focus, a network return or refresh() starts moves the next tick. Before, the timer counted from the wake and fetched again soon after such a fetch.
  • swr: a key with a cycle gives a TypeError ("the key has a cycle") at once. Before, it ran into a stack overflow (RangeError) on each read. A Set and a Map in a key compare by their entries. Before, all of them had the same id.
  • wisp test --coverage reads a Cargo config folder once when it is both CARGO_HOME and above the app. wisp test --e2e creates its data folder with a unique name that only the user can open on Unix.
  • A build plugin that times out no longer waits without a limit for taskkill on Windows. The build stops that helper after 5 seconds and kills the plugin itself.
  • A build plugin stop on Unix cannot hit a new process group. Before, a stop after the command ended used a group number that the system could give to another process. Now a helper process holds the number until the run ends.
  • A build plugin output file replaces its old file on Windows also when another program holds the old file open for a short time. The build tries the rename again 10 times, 50 ms apart. Before, it failed at once.
  • On Linux, a worker gives its listener to its late io_uring ring only when the listener is blocking again. Before, a listener that stayed O_NONBLOCK went to the ring, and each accept with no connection waiting failed with EAGAIN. Now the worker stays on epoll, and the first such worker writes wisp: io: epoll (the listener stays O_NONBLOCK: <cause>).
  • The server reads .env once at start. Before, it read the file twice: once for WISP_DEV and once for its variables.
  • The OpenAPI document finds a cx.bearer() or cx.user() call that a line break splits, as rustfmt writes a long chain. Before, such a handler had no security, and the document had no securitySchemes.
  • wisp dev swaps the literals of the top-level statements of a .wisp --- block (let n = 5;) with no compile, as it swaps the literals of the block's functions. Before, an edit of such a literal compiled and restarted the app.
  • wisp dev shows a literal of a --- block that a save sets back to its compiled value. Before, the app kept the last swapped value until the next compile.
  • Linux: a client that connected in the first milliseconds of a start no longer gets a reset. The check that no other server holds the port binds a socket that does not listen now. Before, it listened, and its close reset the connections in its queue (7 of 150 starts with clients from the start, 0 of 150 after).
  • Linux: with WISP_IO not set, the server answers on epoll at once and the io_uring proof runs on a thread of its own. When it passes, each worker gives its listener to its ring without closing it, so no waiting connection is lost. A worker whose ring then cannot start keeps its listener on epoll, and the server writes wisp: io: epoll (io_uring stopped working: ...) once. The proof no longer delays the first response (exec to first 200, median of 20: 3.66 ms before, 2.29 to 2.53 ms after, on WSL 6.8). With WISP_IO=uring the start waits for the proof as before.
  • wisp fmt stops a rustfmt that runs longer than 30 seconds. It kills rustfmt and, on Windows, the processes it started. It prints one warning, keeps the --- blocks as written and does not start rustfmt again in that run. wisp lsp skips rustfmt for that document until its text changes. Before, a hung rustfmt blocked wisp fmt forever.
  • wisp fmt starts the lines of a <script> or <style> body at 32 levels at most. A deep tag no longer costs time in proportion to its depth for each line.
  • wisp fmt checks a <style> body with one scan of its tokens, not two.
  • wisp fmt no longer reads past the end of a gap between two script tokens when the gap ends in /.
  • wisp lsp finds the column of a position on a very long line after one pass over the line, in any order of positions. Before, a descending order counted the whole line each time.
  • wisp lsp reads at most the size cap from a file of the app in all its reads, also when the file is a symlink to a large file.
  • wisp lsp stops the outline and fold scan at the first {# hole that no } closes. Before, a file of many such holes took quadratic time.
  • wisp lsp renames ?/name in a form action only when the value starts with ?/. It no longer renames action="/other?/name".
  • wisp lsp finds the inner declarations of a name that hide a rename with a sorted list of ranges, not a pass over all declarations for each word.
  • wisp lsp reads a .wisp file that starts with a byte order mark: references, rename and highlights find the --- block, as the build does.
  • The server no longer falls to epoll or hangs on Linux 6.18 and 7.0 where the eventfd of a DEFER_TASKRUN ring stops waking for new connections. On 6.18.54.1-microsoft-standard-WSL2 it woke the worker only for the first new connection. The start proof now serves 3 test requests over TCP and needs an eventfd wake for each step. A request has 500 ms to connect and 500 ms to answer, and one request can try again. A kernel that fails it gets a buffer ring without DEFER_TASKRUN, then PROVIDE_BUFFERS with DEFER_TASKRUN, then PROVIDE_BUFFERS without it, then epoll. The stderr line then says wisp: io: io_uring, no DEFER_TASKRUN (...). Known issue: v1.0.0-pre.3 can hang on this kernel. Set WISP_IO=epoll.
  • io_uring: the start proof also serves one request of 160 KiB and its echo, then closes the connection through the ring. So it checks the steps that small requests do not use: a receive that pauses on a full inbox and starts again, a reply in more than one send, and OP_CLOSE. A kernel that fails a step gets the next way, as above. The large request has 1 s. No new way starts its proof after 2 s, so the server starts with epoll after 10.5 s at most (before, 4 ways could wait 34 s).

Next part (6 of 11)

Source: CHANGELOG.md