@rapidmx/web-client 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +25 -8
- package/apps/shared/components/admin/settings/BrandingForm.tsx +398 -385
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -112
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +159 -152
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1288 -1235
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +9 -1
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -290
- package/apps/shared/components/admin/setup/SetupWizard.tsx +443 -416
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +194 -192
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +199 -197
- package/apps/shared/components/layout/AppShell.tsx +288 -270
- package/apps/shared/components/mail/ConversationList.tsx +225 -33
- package/apps/shared/components/mail/ConversationThreadPane.tsx +434 -216
- package/apps/shared/components/mail/MailListToolbar.tsx +265 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +222 -0
- package/apps/shared/components/mail/MenuButton.tsx +384 -0
- package/apps/shared/components/mail/MessageDetailPane.tsx +280 -215
- package/apps/shared/components/mail/MoveToFolderDialog.tsx +263 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +29 -2
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1700 -1652
- package/apps/shared/components/mail/compose/RecipientInput.tsx +356 -0
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +38 -3
- package/apps/shared/components/mail/compose/quotedBody.ts +100 -0
- package/apps/shared/components/mail/compose/recipients.ts +104 -0
- package/apps/shared/components/mail/labelMenu.tsx +278 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +31 -4
- package/apps/shared/components/mail/listPreferences.ts +228 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +23 -10
- package/apps/shared/components/tasks/layout/TasksShell.tsx +201 -199
- package/apps/shared/plugins/pluginNav.ts +73 -0
- package/apps/shared/search/localIndexRpcClient.ts +3 -1
- package/apps/shared/styles/app.css +28 -5
- package/apps/www/index.tsx +1854 -1314
- package/apps/www/messages/[uid].tsx +5 -4
- package/dist/apps/admin/_404.d.ts +2 -0
- package/dist/apps/admin/_500.d.ts +2 -0
- package/dist/apps/admin/_layout.d.ts +8 -0
- package/dist/apps/admin/audit-log/index.d.ts +11 -0
- package/dist/apps/admin/branding/index.d.ts +3 -0
- package/dist/apps/admin/data-requests/index.d.ts +3 -0
- package/dist/apps/admin/distribution-lists/[uid].d.ts +7 -0
- package/dist/apps/admin/distribution-lists/index.d.ts +3 -0
- package/dist/apps/admin/distribution-lists/new/index.d.ts +3 -0
- package/dist/apps/admin/domains/[uid].d.ts +7 -0
- package/dist/apps/admin/domains/index.d.ts +3 -0
- package/dist/apps/admin/domains/new/index.d.ts +3 -0
- package/dist/apps/admin/encryption-policy/index.d.ts +3 -0
- package/dist/apps/admin/escrow-scopes/[uid].d.ts +21 -0
- package/dist/apps/admin/escrow-scopes/index.d.ts +3 -0
- package/dist/apps/admin/escrow-scopes/new/index.d.ts +3 -0
- package/dist/apps/admin/index.d.ts +3 -0
- package/dist/apps/admin/ingest-queue/index.d.ts +4 -0
- package/dist/apps/admin/mailbox-policy/index.d.ts +3 -0
- package/dist/apps/admin/mailboxes/[uid].d.ts +7 -0
- package/dist/apps/admin/mailboxes/new/index.d.ts +3 -0
- package/dist/apps/admin/plugins/index.d.ts +3 -0
- package/dist/apps/admin/quarantine/index.d.ts +6 -0
- package/dist/apps/admin/retention-policy/index.d.ts +3 -0
- package/dist/apps/admin/setup/index.d.ts +3 -0
- package/dist/apps/admin/transport-rules/[uid].d.ts +7 -0
- package/dist/apps/admin/transport-rules/_transportRuleConfig.d.ts +21 -0
- package/dist/apps/admin/transport-rules/index.d.ts +3 -0
- package/dist/apps/admin/transport-rules/new/index.d.ts +3 -0
- package/dist/apps/escrow/_layout.d.ts +10 -0
- package/dist/apps/escrow/audit-log/index.d.ts +9 -0
- package/dist/apps/escrow/index.d.ts +3 -0
- package/dist/apps/escrow/matters/[uid].d.ts +7 -0
- package/dist/apps/escrow/matters/new/index.d.ts +3 -0
- package/dist/apps/shared/components/admin/distributionLists/MemberListCard.d.ts +17 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +32 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +32 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.js +12 -5
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.d.ts +12 -0
- package/dist/apps/shared/components/admin/mailboxes/MailboxTable.d.ts +6 -0
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.d.ts +19 -0
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +12 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +13 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +6 -5
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +12 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +15 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +2 -2
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.d.ts +9 -0
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.d.ts +19 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +13 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +2 -2
- package/dist/apps/shared/components/admin/settings/PluginsManager.d.ts +11 -0
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +33 -14
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +12 -0
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +2 -2
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.d.ts +14 -0
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +1 -1
- package/dist/apps/shared/components/admin/setup/SetupWizard.d.ts +36 -0
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +14 -10
- package/dist/apps/shared/components/admin/signOut.d.ts +23 -0
- package/dist/apps/shared/components/admin/usePagedList.d.ts +39 -0
- package/dist/apps/shared/components/calendar/CalendarListSidebar.d.ts +26 -0
- package/dist/apps/shared/components/calendar/EventModal.d.ts +46 -0
- package/dist/apps/shared/components/calendar/MonthView.d.ts +15 -0
- package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +20 -0
- package/dist/apps/shared/components/calendar/ResourcePicker.d.ts +19 -0
- package/dist/apps/shared/components/calendar/SplitDayView.d.ts +28 -0
- package/dist/apps/shared/components/calendar/TimeGridView.d.ts +17 -0
- package/dist/apps/shared/components/calendar/allDay.d.ts +51 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +40 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +2 -2
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +23 -0
- package/dist/apps/shared/components/contacts/ContactForm.d.ts +20 -0
- package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +40 -0
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +25 -0
- package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +40 -0
- package/dist/apps/shared/components/contacts/contactKeys.d.ts +29 -0
- package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +21 -0
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +2 -2
- package/dist/apps/shared/components/escrow/layout/EscrowShell.d.ts +25 -0
- package/dist/apps/shared/components/forms/StringListField.d.ts +22 -0
- package/dist/apps/shared/components/layout/AppShell.d.ts +49 -0
- package/dist/apps/shared/components/layout/AppShell.js +15 -13
- package/dist/apps/shared/components/layout/BrandingChrome.d.ts +18 -0
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.d.ts +51 -0
- package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +13 -0
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +66 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +45 -0
- package/dist/apps/shared/components/layout/UserMenu.d.ts +23 -0
- package/dist/apps/shared/components/mail/ConversationList.d.ts +47 -0
- package/dist/apps/shared/components/mail/ConversationList.js +84 -10
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +62 -0
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +217 -60
- package/dist/apps/shared/components/mail/MailListToolbar.d.ts +55 -0
- package/dist/apps/shared/components/mail/MailListToolbar.js +149 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +59 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.js +48 -0
- package/dist/apps/shared/components/mail/MenuButton.d.ts +71 -0
- package/dist/apps/shared/components/mail/MenuButton.js +211 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +101 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +126 -129
- package/dist/apps/shared/components/mail/MoveToFolderDialog.d.ts +57 -0
- package/dist/apps/shared/components/mail/MoveToFolderDialog.js +138 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +73 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +3 -1
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +15 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +37 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +52 -20
- package/dist/apps/shared/components/mail/compose/EmojiPicker.d.ts +14 -0
- package/dist/apps/shared/components/mail/compose/GifPicker.d.ts +13 -0
- package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +36 -0
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +203 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +43 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.js +21 -3
- package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.d.ts +16 -0
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +27 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +37 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +85 -0
- package/dist/apps/shared/components/mail/compose/recipients.d.ts +24 -0
- package/dist/apps/shared/components/mail/compose/recipients.js +87 -0
- package/dist/apps/shared/components/mail/labelMenu.d.ts +97 -0
- package/dist/apps/shared/components/mail/labelMenu.js +134 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +65 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +17 -5
- package/dist/apps/shared/components/mail/listPreferences.d.ts +101 -0
- package/dist/apps/shared/components/mail/listPreferences.js +186 -0
- package/dist/apps/shared/components/mail/pinnedSigners.d.ts +33 -0
- package/dist/apps/shared/components/mail/verificationSeals.d.ts +22 -0
- package/dist/apps/shared/components/mail/writableMailboxes.d.ts +15 -0
- package/dist/apps/shared/components/rules/RuleBuilder.d.ts +70 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +41 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +19 -10
- package/dist/apps/shared/components/tasks/TasksSidebar.d.ts +41 -0
- package/dist/apps/shared/components/tasks/TasksToolbar.d.ts +14 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +24 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +2 -2
- package/dist/apps/shared/mail/findWellKnownFolderUid.d.ts +5 -0
- package/dist/apps/shared/mail/listAllPages.d.ts +17 -0
- package/dist/apps/shared/plugins/pluginNav.d.ts +39 -0
- package/dist/apps/shared/plugins/pluginNav.js +33 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +13 -0
- package/dist/apps/shared/search/localIndexBlockCipher.d.ts +36 -0
- package/dist/apps/shared/search/localIndexBuilder.d.ts +85 -0
- package/dist/apps/shared/search/localIndexKey.d.ts +4 -0
- package/dist/apps/shared/search/localIndexRpcClient.d.ts +71 -0
- package/dist/apps/shared/search/localIndexRpcClient.js +3 -1
- package/dist/apps/shared/search/localIndexSchema.d.ts +111 -0
- package/dist/apps/shared/search/localIndexSizePreference.d.ts +27 -0
- package/dist/apps/shared/search/localIndexStorage.d.ts +28 -0
- package/dist/apps/shared/search/localIndexVFS.d.ts +101 -0
- package/dist/apps/shared/search/localIndexWorker.d.ts +145 -0
- package/dist/apps/shared/search/searchTier2.d.ts +30 -0
- package/dist/apps/shared/styles/app.css +28 -5
- package/dist/apps/www/_404.d.ts +2 -0
- package/dist/apps/www/_500.d.ts +2 -0
- package/dist/apps/www/_layout.d.ts +14 -0
- package/dist/apps/www/calendar/index.d.ts +3 -0
- package/dist/apps/www/contacts/[uid].d.ts +13 -0
- package/dist/apps/www/contacts/index.d.ts +3 -0
- package/dist/apps/www/index.d.ts +3 -0
- package/dist/apps/www/index.js +506 -112
- package/dist/apps/www/messages/[uid].d.ts +11 -0
- package/dist/apps/www/messages/[uid].js +2 -3
- package/dist/apps/www/settings/auto-reply/index.d.ts +4 -0
- package/dist/apps/www/settings/encryption/index.d.ts +7 -0
- package/dist/apps/www/settings/filters/[uid].d.ts +8 -0
- package/dist/apps/www/settings/filters/_mailFilterRuleConfig.d.ts +11 -0
- package/dist/apps/www/settings/filters/index.d.ts +4 -0
- package/dist/apps/www/settings/filters/new/index.d.ts +4 -0
- package/dist/apps/www/settings/labels/index.d.ts +4 -0
- package/dist/apps/www/settings/privacy/index.d.ts +4 -0
- package/dist/apps/www/settings/read-receipts/index.d.ts +4 -0
- package/dist/apps/www/settings/sharing/index.d.ts +4 -0
- package/dist/apps/www/settings/signatures/[uid].d.ts +8 -0
- package/dist/apps/www/settings/signatures/index.d.ts +4 -0
- package/dist/apps/www/settings/signatures/new/index.d.ts +4 -0
- package/dist/apps/www/settings/signatures/signatureDefaults.d.ts +10 -0
- package/dist/apps/www/tasks/index.d.ts +3 -0
- package/package.json +3 -3
- package/apps/shared/components/booking/AvailabilityEditor.tsx +0 -118
- package/apps/www/settings/booking-types/[uid].tsx +0 -306
- package/apps/www/settings/booking-types/index.tsx +0 -98
- package/apps/www/settings/booking-types/new/index.tsx +0 -212
- package/apps/www/settings/focused-inbox/index.tsx +0 -150
- package/dist/apps/shared/components/booking/AvailabilityEditor.js +0 -45
- package/dist/apps/www/settings/booking-types/[uid].js +0 -134
- package/dist/apps/www/settings/booking-types/index.js +0 -29
- package/dist/apps/www/settings/booking-types/new/index.js +0 -86
- package/dist/apps/www/settings/focused-inbox/index.js +0 -71
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main-thread Promise-based RPC wrapper around `localIndexWorker.ts`. One Worker per tab (spawned
|
|
3
|
+
* lazily, on first use, not at module load - a page that never touches search shouldn't pay for booting
|
|
4
|
+
* the WASM module at all), shared across every mailbox `init()`-ed this session - the Worker itself keeps
|
|
5
|
+
* a per-mailbox connection map (see that file's own doc comment).
|
|
6
|
+
*
|
|
7
|
+
* `postMessage` has no native request/response pairing, so every call generates its own numeric `id` and
|
|
8
|
+
* this module correlates the eventual matching response via a pending-request map.
|
|
9
|
+
*
|
|
10
|
+
* **Generations**: `nextLocalIndexGeneration()` hands out one monotonic counter shared by build passes and
|
|
11
|
+
* destroys, so the Worker can reject a build call issued before the index was destroyed (see
|
|
12
|
+
* `GenerationParams`).
|
|
13
|
+
*
|
|
14
|
+
* **Sign-out reaches every tab**: `destroyAllLocalIndexes()` broadcasts on a `BroadcastChannel`; any other
|
|
15
|
+
* tab running a Worker closes its connections and refuses further `init`s.
|
|
16
|
+
*
|
|
17
|
+
* **Failed deletions are retried**: a destroy that couldn't remove a directory (another tab held it, the page
|
|
18
|
+
* navigated away first) is recorded in `localStorage` and retried before this tab's first `init`.
|
|
19
|
+
*/
|
|
20
|
+
import type { ParsedSearchQuery } from "@rapidmx/react-shared/search/queryGrammar.js";
|
|
21
|
+
import type { Coverage, IndexEntitiesResult, InitParams, LocalSearchPage, WindowState } from "./localIndexWorker.js";
|
|
22
|
+
import type { LocalIndexEntity } from "./localIndexSchema.js";
|
|
23
|
+
/** A fresh generation - see this module's doc comment. */
|
|
24
|
+
export declare function nextLocalIndexGeneration(): number;
|
|
25
|
+
export declare const SIGN_OUT_CHANNEL = "rapidmx-localsearch";
|
|
26
|
+
export declare const PENDING_DELETIONS_KEY = "rapidmx-localsearch-pending-deletions";
|
|
27
|
+
/** Retries every deletion an earlier page load couldn't finish. Runs once per page load (memoized), before
|
|
28
|
+
* this tab's first `init` - so it never races an index this tab itself opens. Never rejects. */
|
|
29
|
+
export declare function retryPendingLocalIndexDeletions(): Promise<void>;
|
|
30
|
+
export declare function initLocalIndex(params: InitParams): Promise<void>;
|
|
31
|
+
export declare function indexLocalEntities(mailboxUid: string, entities: LocalIndexEntity[], generation?: number): Promise<IndexEntitiesResult>;
|
|
32
|
+
/** Drops one message from this session's local index (a delete). A no-op for a mailbox not indexed in
|
|
33
|
+
* this tab - a later build pass prunes it instead. Never rejects: best-effort housekeeping. */
|
|
34
|
+
export declare function removeLocalEntity(mailboxUid: string, entityUid: string): Promise<void>;
|
|
35
|
+
/** Re-points one indexed message at its new folder (archive, cancel-scheduled-send) so `folder:`-scoped
|
|
36
|
+
* local searches stay correct. Same no-op/never-rejects rules as `removeLocalEntity()`. */
|
|
37
|
+
export declare function moveLocalEntity(mailboxUid: string, entityUid: string, folderUid: string): Promise<void>;
|
|
38
|
+
export declare function getIndexedVersions(mailboxUid: string, entityUids: string[]): Promise<Record<string, string>>;
|
|
39
|
+
export declare function pruneLocalEntities(mailboxUid: string, keepEntityUids: string[], since: string | undefined, options?: {
|
|
40
|
+
folderUids?: string[];
|
|
41
|
+
generation?: number;
|
|
42
|
+
}): Promise<number>;
|
|
43
|
+
export declare function searchLocal(mailboxUid: string, parsed: ParsedSearchQuery, limit: number, offset?: number): Promise<LocalSearchPage>;
|
|
44
|
+
export declare function getLocalCoverage(mailboxUid: string): Promise<Coverage>;
|
|
45
|
+
export declare function setLocalIndexWindow(mailboxUid: string, timeFloorMonths: number, byteBudgetBytes: number, generation?: number): Promise<WindowState>;
|
|
46
|
+
export declare function setLocalIndexBuilding(mailboxUid: string, building: boolean, completion?: {
|
|
47
|
+
complete: boolean;
|
|
48
|
+
coveredFrom?: string;
|
|
49
|
+
coveredUntil?: string;
|
|
50
|
+
generation?: number;
|
|
51
|
+
}): Promise<void>;
|
|
52
|
+
/** Destroys one mailbox's local index (both the SQLite connection and its on-disk OPFS storage) - spec
|
|
53
|
+
* §11 "MUST be destroyed on the same events that destroy private keys." Never rejects (it's called from
|
|
54
|
+
* lifecycle hooks where a failure mustn't block key destruction) but resolves `false` - and logs why -
|
|
55
|
+
* when the index could not be removed, e.g. because another tab still has it open; the deletion is then
|
|
56
|
+
* retried on the next page load. */
|
|
57
|
+
export declare function destroyLocalIndex(mailboxUid: string): Promise<boolean>;
|
|
58
|
+
/** How long sign-out waits for local index destruction before navigating anyway. */
|
|
59
|
+
export declare const DESTROY_ALL_TIMEOUT_MS = 3000;
|
|
60
|
+
/**
|
|
61
|
+
* Destroys **every** local index on this device - not just the mailboxes this page load opened (sign-out
|
|
62
|
+
* from Calendar, say, never opened any), by enumerating OPFS for the index directory prefix - and tells every
|
|
63
|
+
* other tab to close its own connections. Closes this tab's own open connections through the Worker first
|
|
64
|
+
* (only if one was ever spawned - there's nothing to close otherwise, and booting one just to delete files
|
|
65
|
+
* would be waste), then removes the directories. Anything left behind is retried on the next load. Resolves
|
|
66
|
+
* `true` when everything was removed within `timeoutMs`; never rejects.
|
|
67
|
+
*/
|
|
68
|
+
export declare function destroyAllLocalIndexes(timeoutMs?: number): Promise<boolean>;
|
|
69
|
+
/** Removes local indexes for mailboxes outside `accessibleMailboxUids` - e.g. left behind by a different
|
|
70
|
+
* user who signed in on this device without signing out. Never rejects. */
|
|
71
|
+
export declare function pruneInaccessibleLocalIndexes(accessibleMailboxUids: Iterable<string>): Promise<void>;
|
|
@@ -18,7 +18,9 @@ export const SIGN_OUT_CHANNEL = "rapidmx-localsearch";
|
|
|
18
18
|
let channel;
|
|
19
19
|
function getWorker() {
|
|
20
20
|
if (!worker) {
|
|
21
|
-
|
|
21
|
+
// Named by its compiled `.js` file, like every other relative import: tsc copies the literal into
|
|
22
|
+
// `dist` unchanged, where only the `.js` file exists, and Vite maps it back to the `.ts` source.
|
|
23
|
+
worker = new Worker(new URL("./localIndexWorker.js", import.meta.url), { type: "module" });
|
|
22
24
|
worker.addEventListener("message", (event) => {
|
|
23
25
|
const entry = pending.get(event.data.id);
|
|
24
26
|
if (!entry) {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tier 2 local index's SQLite schema (`specs/search.md` §13). One database per mailbox (see
|
|
3
|
+
* `localIndexWorker.ts`), holding both the FTS5 index and the metadata cache in the same store - the
|
|
4
|
+
* spec's own rationale for choosing SQLite over an in-memory JS index in the first place ("the index
|
|
5
|
+
* database also holds the decrypted metadata cache... so there is one local store rather than two").
|
|
6
|
+
*
|
|
7
|
+
* Bumping `SCHEMA_VERSION` is how a schema change invalidates every existing local index (§11
|
|
8
|
+
* "Invalidation... on schema version change") - `localIndexWorker.ts` compares it against `meta`'s
|
|
9
|
+
* stored value on open and discards+rebuilds on a mismatch, the same path a corruption/GCM-auth-failure
|
|
10
|
+
* takes.
|
|
11
|
+
*/
|
|
12
|
+
/** Bump whenever `CREATE_SCHEMA_SQL` changes in a way existing on-disk databases can't be reconciled
|
|
13
|
+
* with in place. A mismatch deletes and recreates the whole database file (not just its rows), so
|
|
14
|
+
* creation-time-only settings like `auto_vacuum` also apply to upgraded indexes.
|
|
15
|
+
*
|
|
16
|
+
* 2 - added `entities.entity_version` (incremental rebuild skip) and `auto_vacuum=INCREMENTAL`. */
|
|
17
|
+
export declare const SCHEMA_VERSION = 2;
|
|
18
|
+
/**
|
|
19
|
+
* `entities` is the real row store (metadata + the plaintext content fields), `entities_fts` is an FTS5
|
|
20
|
+
* *external content* table over it (`content='entities'`) - the standard SQLite pattern for keeping one
|
|
21
|
+
* copy of the text instead of duplicating it into the FTS5 shadow tables, kept in sync via the three
|
|
22
|
+
* triggers below (SQLite's own documented pattern for external-content FTS5 tables; there is no
|
|
23
|
+
* "ON CONFLICT UPDATE re-index" primitive, so update is modeled as delete-then-reinsert into the FTS
|
|
24
|
+
* index specifically, not into `entities` itself).
|
|
25
|
+
*
|
|
26
|
+
* Column order in `entities_fts` (`subject, participants, body, attachment_text`) is load-bearing: every
|
|
27
|
+
* `bm25(entities_fts, 3.0, 2.0, 1.0, 1.0)` call elsewhere in this module family assumes that exact
|
|
28
|
+
* positional order, matching `searchScoring.ts`'s `SEARCH_FIELD_WEIGHTS` (`subject: 3, participants: 2,
|
|
29
|
+
* body: 1, attachmentText: 1`) so Tier 2's local ranking agrees with the Tier 1/Tier 3 re-scoring the
|
|
30
|
+
* spec requires for one consistent ordering across tiers (§7).
|
|
31
|
+
*/
|
|
32
|
+
export declare const CREATE_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS meta (\n key TEXT PRIMARY KEY,\n value TEXT\n);\n\nCREATE TABLE IF NOT EXISTS entities (\n rowid INTEGER PRIMARY KEY,\n entity_type TEXT NOT NULL,\n entity_uid TEXT NOT NULL UNIQUE,\n mailbox_uid TEXT NOT NULL,\n folder_uid TEXT,\n date_for_sort TEXT NOT NULL,\n participants TEXT,\n flags TEXT,\n has_attachments INTEGER NOT NULL DEFAULT 0,\n subject TEXT,\n body TEXT,\n attachment_text TEXT,\n byte_size INTEGER NOT NULL DEFAULT 0,\n entity_version TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_entities_date ON entities(date_for_sort);\nCREATE INDEX IF NOT EXISTS idx_entities_mailbox ON entities(mailbox_uid);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(\n subject, participants, body, attachment_text,\n content='entities', content_rowid='rowid', tokenize='unicode61'\n);\n\nCREATE TRIGGER IF NOT EXISTS entities_ai AFTER INSERT ON entities BEGIN\n INSERT INTO entities_fts(rowid, subject, participants, body, attachment_text)\n VALUES (new.rowid, new.subject, new.participants, new.body, new.attachment_text);\nEND;\n\nCREATE TRIGGER IF NOT EXISTS entities_ad AFTER DELETE ON entities BEGIN\n INSERT INTO entities_fts(entities_fts, rowid, subject, participants, body, attachment_text)\n VALUES ('delete', old.rowid, old.subject, old.participants, old.body, old.attachment_text);\nEND;\n\nCREATE TRIGGER IF NOT EXISTS entities_au AFTER UPDATE ON entities BEGIN\n INSERT INTO entities_fts(entities_fts, rowid, subject, participants, body, attachment_text)\n VALUES ('delete', old.rowid, old.subject, old.participants, old.body, old.attachment_text);\n INSERT INTO entities_fts(rowid, subject, participants, body, attachment_text)\n VALUES (new.rowid, new.subject, new.participants, new.body, new.attachment_text);\nEND;\n";
|
|
33
|
+
/** The exact `bm25()` weight arguments every ranked query against `entities_fts` MUST pass, in column
|
|
34
|
+
* order - see this module's own doc comment on why the order is load-bearing. Centralized here so a
|
|
35
|
+
* future column reorder can't silently desync a query building its own literal weight list. */
|
|
36
|
+
export declare const BM25_WEIGHTS_SQL = "3.0, 2.0, 1.0, 1.0";
|
|
37
|
+
/** One message's decrypted content, ready to index - `localIndexBuilder.ts`'s own output shape, built
|
|
38
|
+
* from `Message` + the recovered `MessageSecurityResult` fields the same way `searchTier3.ts` already
|
|
39
|
+
* derives them for its own per-candidate matching. */
|
|
40
|
+
export interface LocalIndexEntity {
|
|
41
|
+
entityType: "message";
|
|
42
|
+
entityUid: string;
|
|
43
|
+
mailboxUid: string;
|
|
44
|
+
folderUid?: string;
|
|
45
|
+
/** ISO 8601 - compares correctly as plain text since every value here is UTC. */
|
|
46
|
+
dateForSort: string;
|
|
47
|
+
participants: string;
|
|
48
|
+
/** Comma-delimited, leading/trailing commas included (`,read,flagged,`) - simplest possible substring
|
|
49
|
+
* match (`flags LIKE '%,read,%'`) without needing SQLite's JSON1 extension compiled in. */
|
|
50
|
+
flags: string;
|
|
51
|
+
hasAttachments: boolean;
|
|
52
|
+
subject?: string;
|
|
53
|
+
body?: string;
|
|
54
|
+
attachmentText?: string;
|
|
55
|
+
/** Rough on-disk cost of this entity's own content, in bytes - what `localIndexBuilder.ts`'s
|
|
56
|
+
* byte-budget accounting (spec §11) sums against the configured budget. */
|
|
57
|
+
byteSize: number;
|
|
58
|
+
/** Opaque change marker for the source entity (the builder uses the message's `version` plus its
|
|
59
|
+
* folder) - lets a rebuild skip re-fetching/decrypting anything already indexed unchanged. */
|
|
60
|
+
entityVersion?: string;
|
|
61
|
+
}
|
|
62
|
+
/** `entity_uid` upsert - `ON CONFLICT` (SQLite's UPSERT syntax) rather than a separate delete-then-insert,
|
|
63
|
+
* so re-indexing an already-present message (a flag changed, a folder move) updates it in place and the
|
|
64
|
+
* `entities_au` trigger keeps `entities_fts` in sync automatically. */
|
|
65
|
+
export declare const UPSERT_ENTITY_SQL = "\nINSERT INTO entities (entity_type, entity_uid, mailbox_uid, folder_uid, date_for_sort, participants, flags, has_attachments, subject, body, attachment_text, byte_size, entity_version)\nVALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\nON CONFLICT(entity_uid) DO UPDATE SET\n folder_uid = excluded.folder_uid, date_for_sort = excluded.date_for_sort, participants = excluded.participants,\n flags = excluded.flags, has_attachments = excluded.has_attachments, subject = excluded.subject,\n body = excluded.body, attachment_text = excluded.attachment_text, byte_size = excluded.byte_size,\n entity_version = excluded.entity_version\n";
|
|
66
|
+
/** Bind values for `UPSERT_ENTITY_SQL`, in column order - kept alongside it so the two can never drift
|
|
67
|
+
* out of sync with each other. */
|
|
68
|
+
export declare function entityBindValues(entity: LocalIndexEntity): (string | number)[];
|
|
69
|
+
/** A parsed query's structured (non-free-text) fields - the subset of `ParsedSearchQuery`
|
|
70
|
+
* (`react-shared`'s `queryGrammar.ts`) this module's predicate builder reads. Typed locally rather than
|
|
71
|
+
* importing `ParsedSearchQuery` itself so this Worker-bundled module has no dependency on `@rapidmx/
|
|
72
|
+
* react-shared` beyond what it actually uses - `localIndexBuilder.ts`/`searchTier2.ts` (main-thread side)
|
|
73
|
+
* pass the real `ParsedSearchQuery` in, which structurally satisfies this. */
|
|
74
|
+
export interface LocalSearchPredicateFields {
|
|
75
|
+
from?: string;
|
|
76
|
+
to?: string;
|
|
77
|
+
cc?: string;
|
|
78
|
+
subject?: string;
|
|
79
|
+
hasAttachment?: boolean;
|
|
80
|
+
before?: Date;
|
|
81
|
+
after?: Date;
|
|
82
|
+
folderUid?: string;
|
|
83
|
+
flags?: string[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Builds the SQL `WHERE` predicate (and its bind params) for every *structured* operator this local
|
|
87
|
+
* index can actually evaluate. **Known simplification**: unlike Tier 1's server-side `SearchDocument`
|
|
88
|
+
* (which splits `from`/`to`/`cc` into distinct fields - confirmed already implemented server-side), this
|
|
89
|
+
* local schema keeps only the combined `participants` field (see `CREATE_SCHEMA_SQL`'s own doc comment) -
|
|
90
|
+
* `from:`/`to:`/`cc:` are therefore evaluated here as a substring match against that combined field
|
|
91
|
+
* rather than a precise per-role match. Reasonable for a bounded, best-effort recent-window cache
|
|
92
|
+
* (Tier 1 already serves the precise version for anything it indexes), but a real gap if Tier 2 is later
|
|
93
|
+
* extended to distinguish them - flagged here rather than left silently approximate.
|
|
94
|
+
*/
|
|
95
|
+
export declare function buildSearchPredicates(parsed: LocalSearchPredicateFields, mailboxUid: string): {
|
|
96
|
+
where: string;
|
|
97
|
+
params: (string | number)[];
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Builds the FTS5 `MATCH` expression for a parsed query's free-text and `subject:` portions, or
|
|
101
|
+
* `undefined` when there's nothing to match on text at all (a pure operator/structured-filter query -
|
|
102
|
+
* `buildSearchPredicates()`'s `WHERE` clause alone already narrows that case correctly, no `MATCH`
|
|
103
|
+
* needed). `parsed.text` is passed through close to verbatim (quoted phrases, `OR`, `-` negation - FTS5's
|
|
104
|
+
* own query syntax supports the same shape `queryGrammar.ts`'s own doc comment says every provider's
|
|
105
|
+
* free-text engine is expected to), wrapped only enough to combine it with a `subject:`-scoped clause
|
|
106
|
+
* when both are present.
|
|
107
|
+
*/
|
|
108
|
+
export declare function buildMatchExpression(parsed: {
|
|
109
|
+
text: string;
|
|
110
|
+
subject?: string;
|
|
111
|
+
}): string | undefined;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** §11's Window Sizing table, Web row. */
|
|
2
|
+
export declare const WEB_DEFAULT_BYTE_BUDGET_BYTES: number;
|
|
3
|
+
/** §11's Window Sizing table, Native desktop row - still a real, user-adjustable ceiling (not
|
|
4
|
+
* `UNBOUNDED`), just a roomier default given Electron's storage is disk-limited rather than
|
|
5
|
+
* browser-quota-limited. */
|
|
6
|
+
export declare const ELECTRON_DEFAULT_BYTE_BUDGET_BYTES: number;
|
|
7
|
+
export interface LocalIndexSizeOption {
|
|
8
|
+
bytes: number;
|
|
9
|
+
label: string;
|
|
10
|
+
}
|
|
11
|
+
/** Selectable presets for the Settings UI. `0` means "unlimited" - `applyEviction()`
|
|
12
|
+
* (`localIndexWorker.ts`) already treats a falsy byte budget as unconfigured/unenforced. */
|
|
13
|
+
export declare const LOCAL_INDEX_SIZE_OPTIONS: LocalIndexSizeOption[];
|
|
14
|
+
/** This device's default byte budget before any explicit preference is saved - `500 MB` in a browser
|
|
15
|
+
* tab, `1 GB` in the Electron shell. */
|
|
16
|
+
export declare function getDefaultLocalIndexByteBudget(): number;
|
|
17
|
+
/**
|
|
18
|
+
* Reads this device's configured local-index byte budget. Falls back to
|
|
19
|
+
* `getDefaultLocalIndexByteBudget()` for a never-configured device, a corrupted/non-numeric stored
|
|
20
|
+
* value, or a `localStorage` access that throws (private-browsing/storage-blocked contexts) - never
|
|
21
|
+
* throws itself, matching `getIdleTimeoutMinutes()`'s identical fallback posture.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getLocalIndexByteBudget(): number;
|
|
24
|
+
/** Persists this device's local-index byte-budget preference. A `localStorage` write failure is
|
|
25
|
+
* swallowed, not thrown - the setting just doesn't survive a reload in that case, same fallback-to-default
|
|
26
|
+
* behavior `getLocalIndexByteBudget()` already has for a storage-blocked context. */
|
|
27
|
+
export declare function setLocalIndexByteBudget(bytes: number): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tier 2 local index's on-disk naming and bulk-removal helpers, shared by the Worker
|
|
3
|
+
* (`localIndexWorker.ts`) and the main thread (`localIndexRpcClient.ts`). Deliberately free of any
|
|
4
|
+
* wa-sqlite import so the main thread can use it without pulling the WASM build into its own bundle.
|
|
5
|
+
*
|
|
6
|
+
* Every mailbox's index lives in its own top-level OPFS directory named `rapidmx-localsearch-<mailboxUid>`
|
|
7
|
+
* (`AccessHandlePoolVFS` creates it from the VFS name). Enumerating that prefix is how sign-out destroys
|
|
8
|
+
* *every* index on this device - including ones for mailboxes this page load never opened, which the
|
|
9
|
+
* RPC client's own session-scoped bookkeeping can't know about - and how a sign-in prunes indexes left
|
|
10
|
+
* behind for mailboxes the current user can no longer access.
|
|
11
|
+
*/
|
|
12
|
+
export declare const LOCAL_INDEX_POOL_PREFIX = "rapidmx-localsearch-";
|
|
13
|
+
/** The OPFS directory name (and `EncryptingVFS` name) a mailbox's index lives under. */
|
|
14
|
+
export declare function poolNameFor(mailboxUid: string): string;
|
|
15
|
+
/** Inverse of `poolNameFor()`; `undefined` for any OPFS entry that isn't a local index directory. */
|
|
16
|
+
export declare function mailboxUidFromPoolName(name: string): string | undefined;
|
|
17
|
+
/** Removes one mailbox's index directory. Resolves quietly when it doesn't exist; rejects for any other
|
|
18
|
+
* failure (most commonly `NoModificationAllowedError` - another tab still holds its access handles). */
|
|
19
|
+
export declare function removeLocalIndexDirectory(mailboxUid: string): Promise<void>;
|
|
20
|
+
export interface LocalIndexRemovalResult {
|
|
21
|
+
removed: string[];
|
|
22
|
+
/** Mailbox uids whose directory could not be removed (e.g. still open in another tab). */
|
|
23
|
+
failed: string[];
|
|
24
|
+
}
|
|
25
|
+
/** Every mailbox uid that has a local index directory on this origin. */
|
|
26
|
+
export declare function listLocalIndexMailboxUids(): Promise<string[]>;
|
|
27
|
+
/** Removes every local index directory on this origin except those whose mailbox uid is in `keep`. */
|
|
28
|
+
export declare function removeLocalIndexDirectories(keep?: ReadonlySet<string>): Promise<LocalIndexRemovalResult>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EncryptingVFS` - the Tier 2 local index's at-rest encryption layer (`specs/search.md` §11
|
|
3
|
+
* "Persistence and protection", §13 "Encryption at Rest": "no official SQLCipher WASM build exists...
|
|
4
|
+
* encryption at rest requires a custom VFS that encrypts pages before they reach OPFS").
|
|
5
|
+
*
|
|
6
|
+
* Wraps (composes, does not subclass - `AccessHandlePoolVFS`'s own file-table state is private `#`
|
|
7
|
+
* fields) an `AccessHandlePoolVFS` instance and delegates every `FacadeVFS` method straight through
|
|
8
|
+
* except `jRead`/`jWrite`, which it intercepts to decrypt/encrypt pages.
|
|
9
|
+
*
|
|
10
|
+
* ## Page layout
|
|
11
|
+
*
|
|
12
|
+
* SQLite is opened with a fixed 4096-byte page size (`PRAGMA page_size=4096` - set by
|
|
13
|
+
* `localIndexWorker.ts` before the first write, since SQLite fixes a database's page size on creation).
|
|
14
|
+
* Each *logical* 4096-byte page is stored *physically* as `nonce(12) || AES-256-GCM(ciphertext(4096) ||
|
|
15
|
+
* tag(16))` = 4124 bytes, at a remapped offset (`physicalOffset(page) = page * 4124`). This block size
|
|
16
|
+
* is this class's own fixed constant, independent of whatever byte range a given `jRead`/`jWrite` call
|
|
17
|
+
* actually requests - reads/writes are handled generically over whichever physical blocks the requested
|
|
18
|
+
* logical range overlaps (including a read-modify-write for a sub-block write), not by assuming SQLite
|
|
19
|
+
* only ever issues page-aligned, page-sized I/O. That assumption holds for the *main database file* once
|
|
20
|
+
* its page size is fixed, but handling the general case costs little and removes the need to rely on it.
|
|
21
|
+
*
|
|
22
|
+
* **A fresh random nonce is generated on every write, never reused or derived from a counter** - the
|
|
23
|
+
* simplest way to make an AES-GCM (key, nonce) pair never repeat across different plaintexts, which is
|
|
24
|
+
* the one hard requirement GCM has. The nonce travels with its block, so decryption never needs any
|
|
25
|
+
* external state (a lost/corrupted counter, a persisted salt) to reconstruct it.
|
|
26
|
+
*
|
|
27
|
+
* **AAD binds each block to its logical filename and page index** (not just its own ciphertext) - GCM
|
|
28
|
+
* authenticates a block's own content but not its *position*; without this, an attacker able to write
|
|
29
|
+
* directly into this origin's OPFS storage (outside this codebase's own threat model per spec §4, which
|
|
30
|
+
* excludes a compromised client, but cheap to close off anyway) could silently swap two blocks and each
|
|
31
|
+
* would still decrypt "successfully" on its own, corrupting data instead of failing loudly. Binding to
|
|
32
|
+
* position turns a swap into a caught decryption failure - see `PageCorruptedError` below - the same
|
|
33
|
+
* "invalidate and rebuild" path a schema-version mismatch already takes (spec §11 "Invalidation").
|
|
34
|
+
*
|
|
35
|
+
* **No WAL, no rollback journal.** `localIndexWorker.ts` opens the database with `journal_mode=OFF`. A
|
|
36
|
+
* page cipher for the main database file is one encryption surface; WAL frames (their own header +
|
|
37
|
+
* checksum format) and rollback-journal records (their own, different record format) would each be a
|
|
38
|
+
* *second* one. This index has no durability requirement to justify that cost - spec §11 already
|
|
39
|
+
* requires discarding and rebuilding it on corruption, schema change, key rotation, or platform storage
|
|
40
|
+
* eviction, and eviction "MUST NOT block search" - so an interrupted write in the worst case is just
|
|
41
|
+
* caught by the same GCM-auth-failure -> rebuild path as any other corruption, never partial/torn state
|
|
42
|
+
* silently trusted.
|
|
43
|
+
*
|
|
44
|
+
* Every `jRead`/`jWrite` here is `async` (declared `async` specifically so `FacadeVFS.hasAsyncMethod()`
|
|
45
|
+
* detects it via `instanceof AsyncFunction` and awaits it) because `crypto.subtle.encrypt`/`decrypt` has
|
|
46
|
+
* no synchronous form in a browser - this is *why* `localIndexWorker.ts` boots the Asyncify SQLite build
|
|
47
|
+
* (`dist/wa-sqlite-async.mjs`), not the plain synchronous one `AccessHandlePoolVFS`'s own doc comment
|
|
48
|
+
* says it's designed for: that claim is about `AccessHandlePoolVFS`'s *own* methods (real synchronous
|
|
49
|
+
* OPFS access-handle calls), which stay synchronous and work fine wrapped underneath an async outer VFS
|
|
50
|
+
* on an Asyncify build - the build choice is driven by this class's needs, not the inner VFS's.
|
|
51
|
+
*/
|
|
52
|
+
import { FacadeVFS } from "@journeyapps/wa-sqlite/src/FacadeVFS.js";
|
|
53
|
+
export { PageCorruptedError } from "./localIndexBlockCipher.js";
|
|
54
|
+
export declare class EncryptingVFS extends FacadeVFS {
|
|
55
|
+
#private;
|
|
56
|
+
private constructor();
|
|
57
|
+
/** `rawKey` MUST be exactly 32 bytes (AES-256) - see `localIndexKey.ts`'s `deriveLocalIndexKey()`,
|
|
58
|
+
* the only intended source of this value. */
|
|
59
|
+
static create(name: string, module: unknown, rawKey: Uint8Array): Promise<EncryptingVFS>;
|
|
60
|
+
/**
|
|
61
|
+
* Releases every pooled OPFS sync access handle `AccessHandlePoolVFS` opened and holds open for its
|
|
62
|
+
* entire lifetime (not per SQLite-file-open/close - `jOpen`/`jClose` above only associate/disassociate
|
|
63
|
+
* a SQLite fileId with an already-open handle, they never open or close the handles themselves). MUST
|
|
64
|
+
* be called before creating another VFS instance against the same OPFS pool `name` - confirmed by
|
|
65
|
+
* direct reproduction: skipping this and calling `create()` again with the same `name` throws
|
|
66
|
+
* "Access Handles cannot be created if there is another open Access Handle," since the previous
|
|
67
|
+
* instance's handles are still live. `localIndexWorker.ts` calls this alongside `sqlite3.close(db)`
|
|
68
|
+
* (which closes the SQLite *connection*, a separate, shorter-lived thing from the VFS itself) whenever
|
|
69
|
+
* it tears down a mailbox's connection - on `destroy()` and in `selfTest()`'s own close/reopen check.
|
|
70
|
+
*/
|
|
71
|
+
close(): void | Promise<void>;
|
|
72
|
+
jOpen(filename: string | null, pFile: number, flags: number, pOutFlags: DataView): number | Promise<number>;
|
|
73
|
+
jClose(pFile: number): number | Promise<number>;
|
|
74
|
+
jDelete(filename: string, syncDir: number): number | Promise<number>;
|
|
75
|
+
jAccess(filename: string, flags: number, pResOut: DataView): number | Promise<number>;
|
|
76
|
+
jFullPathname(filename: string, zOut: Uint8Array): number | Promise<number>;
|
|
77
|
+
jSync(pFile: number, flags: number): number | Promise<number>;
|
|
78
|
+
jSectorSize(pFile: number): number;
|
|
79
|
+
jDeviceCharacteristics(pFile: number): number;
|
|
80
|
+
/** Logical file size = physical size scaled back down to the logical block size - the inner VFS's
|
|
81
|
+
* own `jFileSize` reports the *physical* (post-remap) byte count, which is always an exact multiple
|
|
82
|
+
* of `PHYSICAL_BLOCK_SIZE` since every write here always fills whole physical blocks. */
|
|
83
|
+
jFileSize(pFile: number, pSize64: DataView): Promise<number>;
|
|
84
|
+
/** `iSize` is a logical byte size - truncates to the smallest whole number of *physical* blocks that
|
|
85
|
+
* still covers it, so a partially-truncated trailing block is never left half-written. */
|
|
86
|
+
jTruncate(pFile: number, iSize: number): Promise<number>;
|
|
87
|
+
/** Set once any block has failed to decrypt/authenticate (or the inner VFS failed a read/write outright)
|
|
88
|
+
* - `localIndexWorker.ts` checks this after a failed SQLite call to tell real corruption (discard and
|
|
89
|
+
* rebuild, spec §11 "Invalidation") apart from an ordinary error such as a malformed FTS5 query. */
|
|
90
|
+
corruptionDetected: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* `jRead`/`jWrite` MUST NOT throw: with the Asyncify build, an exception escaping an async VFS method
|
|
93
|
+
* never reaches the awaiting `sqlite3.*` call - it surfaces only as an unhandled rejection and the
|
|
94
|
+
* SQLite call hangs forever (confirmed by direct reproduction in node against the real wa-sqlite build).
|
|
95
|
+
* A thrown `PageCorruptedError` therefore used to wedge the whole connection instead of triggering a
|
|
96
|
+
* rebuild. Errors are converted to an I/O error return code here, which SQLite does propagate as a
|
|
97
|
+
* normal `SQLiteError`, and recorded on `corruptionDetected`.
|
|
98
|
+
*/
|
|
99
|
+
jRead(pFile: number, pData: Uint8Array, iOffset: number): Promise<number>;
|
|
100
|
+
jWrite(pFile: number, pData: Uint8Array, iOffset: number): Promise<number>;
|
|
101
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { ParsedSearchQuery } from "@rapidmx/react-shared/search/queryGrammar.js";
|
|
2
|
+
import { LocalIndexEntity } from "./localIndexSchema.js";
|
|
3
|
+
/** One request sent from the main thread. `id` correlates the eventual response - `postMessage` has no
|
|
4
|
+
* native request/response pairing, so every RPC call carries its own id (see `localIndexRpcClient.ts`,
|
|
5
|
+
* the main-thread counterpart that generates/awaits these). */
|
|
6
|
+
export interface LocalIndexRequest {
|
|
7
|
+
id: number;
|
|
8
|
+
method: "init" | "indexEntities" | "removeEntity" | "moveEntity" | "indexedVersions" | "pruneEntities" | "search" | "coverage" | "setWindow" | "setBuilding" | "destroy" | "destroyAll" | "selfTest" | "ping";
|
|
9
|
+
params?: unknown;
|
|
10
|
+
}
|
|
11
|
+
export type LocalIndexResponse = {
|
|
12
|
+
id: number;
|
|
13
|
+
ok: true;
|
|
14
|
+
result: unknown;
|
|
15
|
+
} | {
|
|
16
|
+
id: number;
|
|
17
|
+
ok: false;
|
|
18
|
+
error: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Carried by every call a build pass makes (`init`, `setWindow`, `setBuilding`, `indexEntities`,
|
|
22
|
+
* `pruneEntities`). Generations come from one monotonic counter on the main thread
|
|
23
|
+
* (`localIndexRpcClient.ts`), shared with `destroy`/`destroyAll`: a call whose generation is older than the
|
|
24
|
+
* last destroy of its mailbox is rejected (`StaleGenerationError`), so a build started before a lock/sign-out
|
|
25
|
+
* can't recreate the index with its captured keys or write into a rebuild that started after it. Calls
|
|
26
|
+
* without a generation (search, coverage, in-session moves) are never rejected.
|
|
27
|
+
*/
|
|
28
|
+
export interface GenerationParams {
|
|
29
|
+
generation?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface InitParams extends GenerationParams {
|
|
32
|
+
mailboxUid: string;
|
|
33
|
+
/** The mailbox's already-derived local-index key (`localIndexKey.ts`'s `deriveLocalIndexKey()`) -
|
|
34
|
+
* this Worker never touches the master key itself, only this one purpose-derived value, matching
|
|
35
|
+
* every other MK-derived-key boundary already established elsewhere in this codebase. */
|
|
36
|
+
indexKey: Uint8Array;
|
|
37
|
+
}
|
|
38
|
+
export interface IndexEntitiesParams extends GenerationParams {
|
|
39
|
+
mailboxUid: string;
|
|
40
|
+
entities: LocalIndexEntity[];
|
|
41
|
+
}
|
|
42
|
+
export interface IndexEntitiesResult {
|
|
43
|
+
/** `true` when this call's eviction pass had to delete anything to get back under the byte budget. */
|
|
44
|
+
budgetReached: boolean;
|
|
45
|
+
/** The eviction watermark after this call - see `WindowState.evictedBefore`. */
|
|
46
|
+
evictedBefore?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface WindowState {
|
|
49
|
+
/** Set once the byte budget has forced an eviction: messages dated strictly before this were evicted (or
|
|
50
|
+
* would be, on insert), so a build pass neither re-fetches them nor walks past them. Cleared by
|
|
51
|
+
* `setWindow` once the index has comfortably shrunk back under budget (or the budget was raised). */
|
|
52
|
+
evictedBefore?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface DestroyParams extends GenerationParams {
|
|
55
|
+
mailboxUid: string;
|
|
56
|
+
}
|
|
57
|
+
export interface RemoveEntityParams {
|
|
58
|
+
mailboxUid: string;
|
|
59
|
+
entityUid: string;
|
|
60
|
+
}
|
|
61
|
+
export interface MoveEntityParams {
|
|
62
|
+
mailboxUid: string;
|
|
63
|
+
entityUid: string;
|
|
64
|
+
folderUid: string;
|
|
65
|
+
}
|
|
66
|
+
export interface IndexedVersionsParams {
|
|
67
|
+
mailboxUid: string;
|
|
68
|
+
entityUids: string[];
|
|
69
|
+
}
|
|
70
|
+
export interface PruneEntitiesParams extends GenerationParams {
|
|
71
|
+
mailboxUid: string;
|
|
72
|
+
/** Every entity uid a build pass saw on the server. */
|
|
73
|
+
keepEntityUids: string[];
|
|
74
|
+
/** Only rows at or after this `date_for_sort` are candidates (the walk's own time floor - anything
|
|
75
|
+
* older was never re-listed, so its absence proves nothing). `undefined` means no floor. */
|
|
76
|
+
since?: string;
|
|
77
|
+
/** Only rows currently filed in one of these folders are candidates - folders whose listing was reliable
|
|
78
|
+
* for the whole walk. `undefined` means every folder. */
|
|
79
|
+
folderUids?: string[];
|
|
80
|
+
}
|
|
81
|
+
export interface SearchParams {
|
|
82
|
+
mailboxUid: string;
|
|
83
|
+
parsed: ParsedSearchQuery;
|
|
84
|
+
limit: number;
|
|
85
|
+
/** How many matches to skip before returning `limit` more - §8's composite pagination cursor's own
|
|
86
|
+
* "the local index position consumed so far" component, threaded straight through to SQL `OFFSET`.
|
|
87
|
+
* Defaults to `0` for a first page. */
|
|
88
|
+
offset?: number;
|
|
89
|
+
}
|
|
90
|
+
export interface LocalSearchHit {
|
|
91
|
+
entityUid: string;
|
|
92
|
+
/** Raw `bm25()` score - more negative is a better match, per SQLite's own convention. Normalized by
|
|
93
|
+
* `searchTier2.ts` (main thread) before merging (spec §7). */
|
|
94
|
+
score: number;
|
|
95
|
+
snippet?: string;
|
|
96
|
+
}
|
|
97
|
+
export interface LocalSearchPage {
|
|
98
|
+
hits: LocalSearchHit[];
|
|
99
|
+
/** `true` when at least one more match exists beyond this page - detected by fetching `limit + 1`
|
|
100
|
+
* rows and trimming the extra one, rather than a separate `COUNT(*)` query. */
|
|
101
|
+
hasMore: boolean;
|
|
102
|
+
}
|
|
103
|
+
export interface Coverage {
|
|
104
|
+
/** The oldest date the index is known to cover. While `complete`, every indexable message at least
|
|
105
|
+
* this recent is present; otherwise it is only the oldest row that happens to be present. `undefined`
|
|
106
|
+
* for an empty index. */
|
|
107
|
+
indexedFrom?: string;
|
|
108
|
+
indexedCount: number;
|
|
109
|
+
building: boolean;
|
|
110
|
+
/** `true` only once a build pass *in this connection's lifetime* has walked every mail folder all the
|
|
111
|
+
* way back to its time floor with no errors and no budget cut-off - reset whenever the index is opened,
|
|
112
|
+
* so a completion persisted by an earlier session never counts. Callers MUST NOT treat `indexedFrom` as
|
|
113
|
+
* a coverage guarantee (e.g. to narrow Tier 3) unless this is `true` and `building` is `false`. */
|
|
114
|
+
complete: boolean;
|
|
115
|
+
/** Only set while `complete`: the guarantee ends here. Nothing indexes mail that arrives after a pass
|
|
116
|
+
* started, so everything dated after this (ISO) is NOT covered and must still be searched elsewhere. */
|
|
117
|
+
indexedUntil?: string;
|
|
118
|
+
}
|
|
119
|
+
export interface SetWindowParams extends GenerationParams {
|
|
120
|
+
mailboxUid: string;
|
|
121
|
+
timeFloorMonths: number;
|
|
122
|
+
byteBudgetBytes: number;
|
|
123
|
+
}
|
|
124
|
+
export interface SetBuildingParams extends GenerationParams {
|
|
125
|
+
mailboxUid: string;
|
|
126
|
+
building: boolean;
|
|
127
|
+
/** Only meaningful with `building: false` - whether the pass that just ended walked everything. */
|
|
128
|
+
complete?: boolean;
|
|
129
|
+
/** Only meaningful with `complete: true` - the pass's time floor (ISO), or `undefined` for none. */
|
|
130
|
+
coveredFrom?: string;
|
|
131
|
+
/** Only meaningful with `complete: true` - when the pass started (ISO, minus a clock-skew margin). */
|
|
132
|
+
coveredUntil?: string;
|
|
133
|
+
}
|
|
134
|
+
/** Runs `task` after every previously queued task for `mailboxUid` has settled. Exported for tests. */
|
|
135
|
+
export declare function runExclusive<T>(mailboxUid: string, task: () => Promise<T>): Promise<T>;
|
|
136
|
+
/** Rejects a build-pass call issued before its mailbox's index was last destroyed. */
|
|
137
|
+
export declare class StaleGenerationError extends Error {
|
|
138
|
+
constructor(mailboxUid: string);
|
|
139
|
+
}
|
|
140
|
+
/** Thrown by `init` when another tab already holds this mailbox's index open. */
|
|
141
|
+
export declare class LocalIndexBusyError extends Error {
|
|
142
|
+
constructor(mailboxUid: string);
|
|
143
|
+
}
|
|
144
|
+
/** Routes one request to its handler, inside the owning mailbox's serial queue. Exported for tests. */
|
|
145
|
+
export declare function handleRequest(method: LocalIndexRequest["method"], params: unknown): Promise<unknown>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 2 (`specs/search.md` §6) - the local encrypted index's public entry point, mirroring
|
|
3
|
+
* `@rapidmx/react-shared`'s `searchTier3.ts#searchEncryptedCandidates()` in shape: given a parsed query
|
|
4
|
+
* and this session's unlocked keys, return already-scored, already-sourced results plus how much of the
|
|
5
|
+
* mailbox the local index actually covers right now.
|
|
6
|
+
*
|
|
7
|
+
* Returns `{ results: [], coverage: undefined }` - never throws, never rejects - both when `unlocked` is
|
|
8
|
+
* absent (nothing is indexed without a master key to derive the index key from, the same "silently
|
|
9
|
+
* contributes nothing" degradation `searchTier3.ts`'s own doc comment describes for its identical case)
|
|
10
|
+
* and when anything about the local index itself fails (Worker/WASM/OPFS unsupported or unavailable in
|
|
11
|
+
* this browser, a corrupted index mid-rebuild, a query timeout). This tier's own storage engine is
|
|
12
|
+
* strictly best-effort infrastructure Tier 1 doesn't depend on - a caller `Promise.all()`-ing this
|
|
13
|
+
* alongside Tier 1/Tier 3 (`apps/www/index.tsx`'s own search orchestration) must never see the *whole
|
|
14
|
+
* search* fail just because this one, most novel piece had a bad moment.
|
|
15
|
+
*/
|
|
16
|
+
import type { UnlockedKeys } from "@rapidmx/react-shared/crypto/keySession.js";
|
|
17
|
+
import type { ParsedSearchQuery } from "@rapidmx/react-shared/search/queryGrammar.js";
|
|
18
|
+
import type { SearchResult } from "@rapidmx/react-shared/search/searchApi.js";
|
|
19
|
+
import type { Coverage } from "./localIndexWorker.js";
|
|
20
|
+
export interface Tier2SearchOutcome {
|
|
21
|
+
results: SearchResult[];
|
|
22
|
+
/** `undefined` when `unlocked` was absent - there's no local index to report coverage for at all in
|
|
23
|
+
* that case, distinct from a real, empty (just-built) index. */
|
|
24
|
+
coverage?: Coverage;
|
|
25
|
+
/** `true` when at least one more match exists beyond `results` - §8's composite pagination cursor's
|
|
26
|
+
* own per-tier continuation signal. Always `false` alongside the `unlocked`-absent/error
|
|
27
|
+
* degradations below - there is nothing more to page through either way. */
|
|
28
|
+
hasMore: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function searchLocalIndex(mailboxUid: string, parsed: ParsedSearchQuery, unlocked: UnlockedKeys | undefined, limit?: number, offset?: number): Promise<Tier2SearchOutcome>;
|
|
@@ -12,11 +12,24 @@
|
|
|
12
12
|
|
|
13
13
|
/*
|
|
14
14
|
* Tailwind v4's automatic content scanning excludes `node_modules` by default, which is exactly where
|
|
15
|
-
* `@rapidmx/react-shared`'s components live
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* `@rapidmx/react-shared`'s components live - without these, none of its Tailwind classnames (Modal/
|
|
16
|
+
* Drawer/Button/Alert/Skeleton/FormField/PopoverPortal/ContactAvatar/MiniDatePicker/BottomTabBar) are
|
|
17
|
+
* generated, and e.g. a Modal renders with no overlay, position or width. The published package ships
|
|
18
|
+
* only its compiled `dist` (no `src`), and `@source` paths resolve from this file's directory
|
|
19
|
+
* (`apps/shared/styles/`), so both install layouts are listed; a path that doesn't exist is skipped:
|
|
20
|
+
* - `../../../node_modules/...`: react-shared under this package's own `node_modules` - this repo's
|
|
21
|
+
* checkout, or a consumer install that nests it under web-client.
|
|
22
|
+
* - `../../../../react-shared/dist`: react-shared hoisted beside this package, i.e. this file at
|
|
23
|
+
* `<consumer>/node_modules/@rapidmx/web-client/apps/shared/styles/` and react-shared at
|
|
24
|
+
* `<consumer>/node_modules/@rapidmx/react-shared` - how `server` installs both, for its pages and
|
|
25
|
+
* for the plugin pages' `plugin-ui.css`, which imports this file from there.
|
|
26
|
+
* `electron-client` links `node_modules/@rapidmx/web-client` to a checkout of this repo, so one of the
|
|
27
|
+
* two matches whether this file is reached through the link or its real path. From a checkout, the
|
|
28
|
+
* second path can also find a sibling `react-shared` checkout's build, which only adds classes
|
|
29
|
+
* react-shared itself uses.
|
|
18
30
|
*/
|
|
19
|
-
@source "
|
|
31
|
+
@source "../../../node_modules/@rapidmx/react-shared/dist";
|
|
32
|
+
@source "../../../../react-shared/dist";
|
|
20
33
|
|
|
21
34
|
/*
|
|
22
35
|
* `server`'s own build (unlike `electron-client`, which already explicitly sourced this same tree in
|
|
@@ -30,7 +43,7 @@
|
|
|
30
43
|
* silently absent from the built stylesheet, even though `AdminShell.tsx` itself was very much in the
|
|
31
44
|
* bundle - verified directly by diffing every classname `AdminShell.tsx` uses against the actual built
|
|
32
45
|
* CSS. An explicit `@source` here makes the scan independent of Vite's per-entry chunking, the same
|
|
33
|
-
* fix this file
|
|
46
|
+
* fix this file applies to `react-shared` above, just for this package's own source too.
|
|
34
47
|
*/
|
|
35
48
|
@source "../../";
|
|
36
49
|
|
|
@@ -263,3 +276,13 @@ h5,
|
|
|
263
276
|
h6 {
|
|
264
277
|
font-family: var(--rr-font-display);
|
|
265
278
|
}
|
|
279
|
+
|
|
280
|
+
/* The compose editor's quoted original (`composeQuoting.ts`'s blockquote). TipTap keeps the element but not the
|
|
281
|
+
* inline style it was seeded with, and Tailwind's preflight resets the browser's own blockquote indent - without
|
|
282
|
+
* this the quote under a reply reads as if it were part of the new message. */
|
|
283
|
+
.tiptap blockquote {
|
|
284
|
+
margin: 0;
|
|
285
|
+
padding-left: 1em;
|
|
286
|
+
border-left: 2px solid var(--rr-color-border);
|
|
287
|
+
color: var(--rr-color-text-muted);
|
|
288
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React, { PropsWithChildren } from "react";
|
|
2
|
+
import { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
|
|
3
|
+
export interface LayoutProps {
|
|
4
|
+
/**
|
|
5
|
+
* Supplied by `WwwRoute`'s `fetchProps()` override (`src/mongo/routes/wwwRoute.ts`/
|
|
6
|
+
* `src/sql/routes/wwwRoute.ts`) - `@rapidrest/react` spreads the same merged page props onto this
|
|
7
|
+
* layout as onto the page it wraps, so it renders correctly server-side on the very first byte of the
|
|
8
|
+
* response: no client-side flash from the stock defaults below, and a crawler reading the raw HTML
|
|
9
|
+
* sees the real branding too. `undefined` only on the framework's unwrapped `_500` fallback path,
|
|
10
|
+
* which doesn't receive props at all - falling back to the stock defaults there is acceptable.
|
|
11
|
+
*/
|
|
12
|
+
branding?: Branding;
|
|
13
|
+
}
|
|
14
|
+
export default function Layout({ children, branding }: PropsWithChildren<LayoutProps>): React.JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ContactsShellProps } from "../../shared/components/contacts/layout/ContactsShell.js";
|
|
3
|
+
/**
|
|
4
|
+
* Only reached on mobile (below the `md` breakpoint) — desktop's `apps/www/contacts/index.tsx` keeps its
|
|
5
|
+
* existing inline detail pane and never navigates here; see that file's `handleSelectRow`. There is no
|
|
6
|
+
* equivalent "new contact" route — an unsaved contact has no uid to route on, so creation stays an
|
|
7
|
+
* in-place mode-switch on every device (see `apps/www/contacts/index.tsx`'s `handleNew`).
|
|
8
|
+
*/
|
|
9
|
+
export default function ContactDetailPage(props: ContactsShellProps & {
|
|
10
|
+
params: {
|
|
11
|
+
uid: string;
|
|
12
|
+
};
|
|
13
|
+
}): React.JSX.Element;
|