mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 16:14:07 +00:00
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.
89 lines
3.1 KiB
TypeScript
89 lines
3.1 KiB
TypeScript
/** Messages between the page and the store worker. */
|
|
|
|
import type { StoreEvent, StoreScope } from '../area';
|
|
import type { JmapResponse, MethodCall, StoreSession } from '../engine/transport';
|
|
|
|
export const RPC_CHANNEL = 'bulwark-store-rpc';
|
|
export const OWNER_LOCK = 'bulwark-store-owner';
|
|
/** The OPFS folder of the database pool. The page can delete it without loading wasm. */
|
|
export const POOL_DIRECTORY = '.bulwark-store';
|
|
|
|
export interface OpenAccountParams {
|
|
account: string;
|
|
/** `/a<hex>.db`, no account data: the name is plaintext in the OPFS slot. */
|
|
file: string;
|
|
/** 32 bytes in hex. Only ever sent to this tab's own worker, never over the channel. */
|
|
keyHex: string;
|
|
session: StoreSession | null;
|
|
scope: StoreScope;
|
|
/** Null when this tab's client hasn't probed it: the stored answer stays. */
|
|
invertedKeywordSort: boolean | null;
|
|
/** The page's current Authorization header. */
|
|
auth: string | null;
|
|
}
|
|
|
|
/** What other tabs can ask for, over the channel. */
|
|
export interface StoreRpc {
|
|
answer(params: { account: string; calls: MethodCall[] }): JmapResponse | null;
|
|
sync(params: { account: string }): void;
|
|
noteStateChange(params: { account: string; changed: Record<string, Record<string, string>> }): void;
|
|
setOnline(params: { online: boolean }): void;
|
|
status(params: { account: string }): StoreStatus | null;
|
|
/** A non-JMAP call into an area of the store. */
|
|
area(params: { account: string; name: string; params: unknown }): unknown;
|
|
/** Closes an account and deletes its file. The name holds no account data. */
|
|
removeAccount(params: { account: string; file: string }): boolean;
|
|
}
|
|
|
|
export interface StoreStatus {
|
|
account: string;
|
|
open: boolean;
|
|
emails: number;
|
|
bodies: number;
|
|
lastSyncedAt: number | null;
|
|
}
|
|
|
|
export type RpcRequest<K extends keyof StoreRpc = keyof StoreRpc> = {
|
|
kind: 'rpc';
|
|
id: string;
|
|
from: string;
|
|
method: K;
|
|
params: Parameters<StoreRpc[K]>[0];
|
|
};
|
|
|
|
export type RpcReply = {
|
|
kind: 'reply';
|
|
id: string;
|
|
to: string;
|
|
result?: unknown;
|
|
error?: string;
|
|
errorName?: string;
|
|
};
|
|
|
|
export type ChannelMessage =
|
|
| RpcRequest
|
|
| RpcReply
|
|
| { kind: 'event'; event: StoreEvent }
|
|
| { kind: 'owner-changed'; owner: string | null }
|
|
// A tab started after the owner announced itself asks.
|
|
| { kind: 'who-owns' }
|
|
// Everyone signed out: every tab stops its worker, so the pool can go.
|
|
| { kind: 'shutdown' };
|
|
|
|
/** From this tab's page to its own worker, never on the channel. */
|
|
export type PageToWorker =
|
|
| { kind: 'init'; clientId: string; moduleUrl: string }
|
|
| { kind: 'open'; id: string; params: OpenAccountParams }
|
|
| { kind: 'close-account'; id: string; account: string }
|
|
| { kind: 'set-auth'; account: string; auth: string | null }
|
|
| { kind: 'set-inverted'; account: string; inverted: boolean }
|
|
| { kind: 'set-session'; account: string; session: StoreSession }
|
|
| { kind: 'set-scope'; account: string; scope: StoreScope }
|
|
| { kind: 'release'; id: string };
|
|
|
|
export type WorkerToPage =
|
|
| { kind: 'owner' }
|
|
| { kind: 'result'; id: string; result?: unknown; error?: string; errorName?: string }
|
|
| { kind: 'need-auth'; account: string }
|
|
| { kind: 'event'; event: StoreEvent };
|