The IndexedDB cache, its crypto, the responder and the page-side sync
go. lib/offline keeps its structure, policy, wipes, sign-out gates and
notices; answers, the outbox and sync now live in lib/store, in a worker
behind lib/store/client/api.ts.
What users get besides that:
- every folder is kept, not just the Inbox and opened ones
- delete for good and empty trash work offline
- a change the server refuses, or that meets a message changed on
another device, is shown instead of dropped
- a send whose response got lost is looked up by a Message-ID fixed when
it was queued, then finished, submitted or sent again
- with the store on, a start paints from the local copy, then connects
An account the app drops by itself no longer wipes its data: only a
sign-out or an explicit removal does. The outbox of the old database
moves over once, in lib/offline/migrate-legacy.ts, and the old database
goes only after the store committed. `isNetworkFailure` now counts
timeouts and proxy 5xx. Sends and outbox requests are never answered
from the device.
Tests moved with the code: cache.test.ts and responder.test.ts are
replaced by lib/store/__tests__/, and review-fixes.test.ts, outbox.test.ts
and client-fallback.test.ts now run the real engine on node:sqlite.
Changed assertions: an uncertain send is looked up rather than left alone
forever, destroy and empty trash are queued rather than refused, and #9
(opened folders) became "a folder the server deleted goes", since all
folders are kept.
An existing assertion changes: a token endpoint that can't be reached now answers 503, not 500, because the client counts repeated 500s as permanent and signs the user out.
Fixes from the review of api.files:
- `files.attach` to a new draft runs `onBeforeAttachmentUpload` for each
file and the server's total size limit before `compose.open`, which
runs neither; refused files come back in `refused`
- a write is refused when a folder above the target is missing from the
listing, instead of counting it as not shared
- `onFileMove` reports the item as it was, from where it was, like the
Files app's own move
- writes are counted against the 20-a-minute limit before the drive is
listed, so refused writes count and cost no listing
- reads stop the download past 20 MB (`fetchBlobArrayBuffer` with a
limit) and answer `too_big` when the listed size was wrong
- when the server refuses the new content of a replaced file, the old
version's copy is taken out of the trash again
The two getText assertions on `fetchBlobArrayBuffer` now expect the
account (undefined for the own drive) and the byte limit, since the
call passes both.
Both make the server fetch the sender's domain. The test that every
default value is allowed now also names senderFavicons (on by default,
which a plugin may no longer set); the rest of it is as it was.
exif.test.ts: the case "falls back to the file date" now expects no
date, since IFD0's DateTime is the file's change date, not the date
the photo was taken.
The host reads the size a PNG, GIF, JPEG, WebP or BMP declares in its
header first: over 50 megapixels it answers too_big, trackers and tiny
images are never decoded, a format whose header it can't read is
unsupported, and large images are decoded at the target width. A browser
that throws a TypeError on createImageBitmap's options decodes without
them.
The tests' stand-in pictures are now PNG headers instead of JSON (the
code reads the header), so image-shrink.test.ts and
plugin-host-attachment-image.test.ts build them that way, and the first
shrink test expects the resize options on createImageBitmap.
api.email.getThread fetches only the newest 50 messages of a thread
(Thread/get, then Email/get for those ids) instead of all of them with
their bodies. api.email.getThreadCount answers the number of messages
from Thread/get, with its own rate limit.
plugin-host-thread.test.ts now expects getThreadEmails to be called with
{ newest: 50 }, the new option that limits what is fetched.
vacation.get now also says whether the out-of-office forwards mail
(forwardActive, from the area c host APIs), so the test that pins the
whole answer names it too. The other fields are as they were.
- The button and the panel of Calendar, Contacts, Files and Settings
show only when the plugin's shouldShow says yes, so a plugin switched
off for these views (admin, user or no service) leaves no button that
opens an empty panel, and no extra rows in Contacts and Settings.
- Files: while the details are open the panel steps aside there only;
the other views keep it as the user left it. With both stored open the
details fold when the page loads.
- Resizing stops where the view keeps its 480 px, so a drag or the arrow
keys no longer fold the panel away (and the handle keeps the focus).
- Settings and Contacts render the same element with or without the
panel, so a form keeps what was typed when the panel starts docking.
- Contacts on a phone: the panel button sits over an open contact too.
- The panel buttons of the reader and the views have one name, "Plugin
panel", with aria-pressed, and name the plugin's panel shortcut in
their title; the open panels are named landmarks.
The reader tests look the button up by its new name and pressed state
(viewer-toolbar-over-side-panel, reader-plugin-surfaces,
viewer-plugin-panel-width), since "Show/Hide plugin panel" is gone.
api.host.features.sidePanel now says whether this plugin may offer the
app-side-panel (ui:side-panel granted), not only that the webmail has
it. A plugin enabled before an update added the permission can then say
so instead of offering a switch that does nothing. The host computes the
assistant's flags per plugin for a frame's init and for each push.
plugin-host-features.test.ts: the host bridge assertions take the
assistant's flags as the test plugin sees them (it lacks ui:side-panel,
so sidePanel is false there).