@rapidmx/web-client 0.4.0 → 0.6.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/admin/audit-log/index.tsx +35 -8
- package/apps/admin/data-requests/index.tsx +480 -485
- package/apps/admin/distribution-lists/[uid].tsx +19 -1
- package/apps/admin/escrow-scopes/[uid].tsx +348 -237
- package/apps/admin/escrow-scopes/new/index.tsx +8 -2
- package/apps/admin/index.tsx +19 -1
- package/apps/admin/ingest-queue/index.tsx +44 -5
- package/apps/admin/mailboxes/[uid].tsx +211 -184
- package/apps/admin/quarantine/index.tsx +46 -14
- package/apps/admin/transport-rules/[uid].tsx +30 -1
- package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
- package/apps/admin/transport-rules/index.tsx +24 -6
- package/apps/admin/transport-rules/new/index.tsx +30 -1
- package/apps/escrow/_layout.tsx +2 -3
- package/apps/escrow/audit-log/index.tsx +196 -168
- package/apps/escrow/matters/[uid].tsx +621 -521
- package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
- package/apps/shared/components/admin/layout/AdminShell.tsx +28 -9
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
- package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
- package/apps/shared/components/admin/settings/BrandingForm.tsx +15 -4
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +18 -7
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +10 -2
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +6 -0
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +45 -12
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -595
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -98
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +49 -13
- package/apps/shared/components/admin/setup/SetupWizard.tsx +150 -34
- package/apps/shared/components/admin/signOut.ts +79 -0
- package/apps/shared/components/admin/usePagedList.tsx +129 -0
- package/apps/shared/components/calendar/EventModal.tsx +717 -549
- package/apps/shared/components/calendar/MonthView.tsx +173 -139
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
- package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
- package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
- package/apps/shared/components/calendar/allDay.ts +124 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +194 -192
- package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
- package/apps/shared/components/contacts/ContactForm.tsx +377 -348
- package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
- package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
- package/apps/shared/components/contacts/contactKeys.ts +78 -0
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +199 -197
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
- package/apps/shared/components/layout/AppShell.tsx +123 -33
- package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +389 -298
- package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
- package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
- package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -128
- package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
- package/apps/shared/components/mail/MessageDetailPane.tsx +1318 -630
- package/apps/shared/components/mail/compose/ComposeContext.tsx +20 -15
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +854 -132
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +420 -408
- package/apps/shared/components/mail/pinnedSigners.ts +124 -0
- package/apps/shared/components/mail/verificationSeals.ts +125 -0
- package/apps/shared/components/mail/writableMailboxes.ts +101 -0
- package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
- package/apps/shared/components/settings/layout/SettingsShell.tsx +23 -9
- package/apps/shared/components/tasks/layout/TasksShell.tsx +201 -199
- package/apps/shared/mail/listAllPages.ts +39 -0
- package/apps/shared/plugins/pluginNav.ts +73 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +64 -55
- package/apps/shared/search/localIndexBuilder.ts +481 -179
- package/apps/shared/search/localIndexRpcClient.ts +231 -29
- package/apps/shared/search/localIndexSchema.ts +15 -6
- package/apps/shared/search/localIndexStorage.ts +98 -0
- package/apps/shared/search/localIndexVFS.ts +32 -1
- package/apps/shared/search/localIndexWorker.ts +648 -186
- package/apps/www/calendar/index.tsx +438 -417
- package/apps/www/contacts/[uid].tsx +21 -2
- package/apps/www/contacts/index.tsx +116 -12
- package/apps/www/index.tsx +1314 -1041
- package/apps/www/messages/[uid].tsx +18 -6
- package/apps/www/settings/auto-reply/index.tsx +134 -129
- package/apps/www/settings/encryption/index.tsx +739 -216
- package/apps/www/settings/filters/[uid].tsx +171 -155
- package/apps/www/settings/filters/new/index.tsx +8 -1
- package/apps/www/settings/focused-inbox/index.tsx +150 -148
- package/apps/www/settings/privacy/index.tsx +494 -436
- package/apps/www/settings/read-receipts/index.tsx +148 -125
- package/apps/www/settings/sharing/index.tsx +17 -14
- package/apps/www/tasks/index.tsx +638 -579
- 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/audit-log/index.js +38 -8
- 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/data-requests/index.js +52 -72
- package/dist/apps/admin/distribution-lists/[uid].d.ts +7 -0
- package/dist/apps/admin/distribution-lists/[uid].js +1 -1
- 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/[uid].js +91 -12
- 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/escrow-scopes/new/index.js +8 -3
- package/dist/apps/admin/index.d.ts +3 -0
- package/dist/apps/admin/index.js +4 -1
- package/dist/apps/admin/ingest-queue/index.d.ts +4 -0
- package/dist/apps/admin/ingest-queue/index.js +24 -6
- 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/[uid].js +20 -4
- 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/quarantine/index.js +20 -11
- 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/[uid].js +18 -2
- package/dist/apps/admin/transport-rules/_transportRuleConfig.d.ts +21 -0
- package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
- package/dist/apps/admin/transport-rules/index.d.ts +3 -0
- package/dist/apps/admin/transport-rules/index.js +22 -6
- package/dist/apps/admin/transport-rules/new/index.d.ts +3 -0
- package/dist/apps/admin/transport-rules/new/index.js +18 -2
- package/dist/apps/escrow/_layout.d.ts +10 -0
- package/dist/apps/escrow/_layout.js +3 -3
- package/dist/apps/escrow/audit-log/index.d.ts +9 -0
- package/dist/apps/escrow/audit-log/index.js +26 -1
- package/dist/apps/escrow/index.d.ts +3 -0
- package/dist/apps/escrow/matters/[uid].d.ts +7 -0
- package/dist/apps/escrow/matters/[uid].js +77 -42
- 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/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +32 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.js +15 -6
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.d.ts +12 -0
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -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/ResourceSettingsCard.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +12 -0
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
- package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +9 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +15 -4
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +12 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +13 -7
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +12 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +7 -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/MailboxCreateForm.js +6 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +10 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +34 -12
- package/dist/apps/shared/components/admin/settings/PluginsManager.d.ts +4 -0
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +390 -78
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +9 -0
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +40 -11
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.d.ts +14 -0
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +29 -13
- package/dist/apps/shared/components/admin/setup/SetupWizard.d.ts +36 -0
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +87 -21
- package/dist/apps/shared/components/admin/signOut.d.ts +23 -0
- package/dist/apps/shared/components/admin/signOut.js +75 -0
- package/dist/apps/shared/components/admin/usePagedList.d.ts +39 -0
- package/dist/apps/shared/components/admin/usePagedList.js +99 -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/EventModal.js +142 -21
- package/dist/apps/shared/components/calendar/MonthView.d.ts +15 -0
- package/dist/apps/shared/components/calendar/MonthView.js +12 -5
- package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +20 -0
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
- 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/SplitDayView.js +10 -4
- package/dist/apps/shared/components/calendar/TimeGridView.d.ts +17 -0
- package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
- package/dist/apps/shared/components/calendar/allDay.d.ts +51 -0
- package/dist/apps/shared/components/calendar/allDay.js +111 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +40 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +3 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +23 -0
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
- package/dist/apps/shared/components/contacts/ContactForm.d.ts +20 -0
- package/dist/apps/shared/components/contacts/ContactForm.js +32 -10
- package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +40 -0
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
- 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/KeyChangeReview.js +63 -0
- package/dist/apps/shared/components/contacts/contactKeys.d.ts +29 -0
- package/dist/apps/shared/components/contacts/contactKeys.js +64 -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/escrow/layout/EscrowShell.js +24 -5
- 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 +97 -26
- package/dist/apps/shared/components/layout/BrandingChrome.d.ts +18 -0
- package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.d.ts +51 -0
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +49 -12
- package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +13 -0
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +66 -0
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +45 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.js +73 -14
- package/dist/apps/shared/components/layout/UserMenu.d.ts +23 -0
- package/dist/apps/shared/components/mail/ConversationList.d.ts +11 -0
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +23 -0
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +93 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +441 -36
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +58 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
- 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 +699 -128
- 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/RichTextEditor.d.ts +34 -0
- 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/composeFlushRegistry.js +50 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +57 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +13 -4
- package/dist/apps/shared/components/mail/pinnedSigners.d.ts +33 -0
- package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
- package/dist/apps/shared/components/mail/verificationSeals.d.ts +22 -0
- package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
- package/dist/apps/shared/components/mail/writableMailboxes.d.ts +15 -0
- package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
- package/dist/apps/shared/components/rules/RuleBuilder.d.ts +70 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +41 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +19 -9
- 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/mail/listAllPages.js +25 -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/LocalIndexLifecycle.js +58 -54
- 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/localIndexBuilder.js +292 -37
- 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 +191 -26
- package/dist/apps/shared/search/localIndexSchema.d.ts +111 -0
- package/dist/apps/shared/search/localIndexSchema.js +12 -6
- 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/localIndexStorage.js +86 -0
- package/dist/apps/shared/search/localIndexVFS.d.ts +101 -0
- package/dist/apps/shared/search/localIndexVFS.js +68 -40
- package/dist/apps/shared/search/localIndexWorker.d.ts +145 -0
- package/dist/apps/shared/search/localIndexWorker.js +519 -173
- package/dist/apps/shared/search/searchTier2.d.ts +30 -0
- 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/calendar/index.js +28 -10
- package/dist/apps/www/contacts/[uid].d.ts +13 -0
- package/dist/apps/www/contacts/[uid].js +13 -2
- package/dist/apps/www/contacts/index.d.ts +3 -0
- package/dist/apps/www/contacts/index.js +80 -10
- package/dist/apps/www/index.d.ts +3 -0
- package/dist/apps/www/index.js +339 -108
- package/dist/apps/www/messages/[uid].d.ts +11 -0
- package/dist/apps/www/messages/[uid].js +18 -6
- package/dist/apps/www/settings/auto-reply/index.d.ts +4 -0
- package/dist/apps/www/settings/auto-reply/index.js +9 -4
- package/dist/apps/www/settings/encryption/index.d.ts +7 -0
- package/dist/apps/www/settings/encryption/index.js +514 -97
- package/dist/apps/www/settings/filters/[uid].d.ts +8 -0
- package/dist/apps/www/settings/filters/[uid].js +22 -7
- 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/filters/new/index.js +8 -2
- package/dist/apps/www/settings/focused-inbox/index.d.ts +4 -0
- package/dist/apps/www/settings/focused-inbox/index.js +3 -1
- 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/privacy/index.js +57 -22
- package/dist/apps/www/settings/read-receipts/index.d.ts +4 -0
- package/dist/apps/www/settings/read-receipts/index.js +10 -3
- package/dist/apps/www/settings/sharing/index.d.ts +4 -0
- package/dist/apps/www/settings/sharing/index.js +7 -11
- 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/dist/apps/www/tasks/index.js +50 -10
- package/package.json +3 -3
- package/apps/shared/components/booking/AvailabilityEditor.tsx +0 -118
- package/apps/www/settings/booking-types/[uid].tsx +0 -300
- package/apps/www/settings/booking-types/index.tsx +0 -98
- package/apps/www/settings/booking-types/new/index.tsx +0 -212
- package/dist/apps/shared/components/booking/AvailabilityEditor.js +0 -45
- package/dist/apps/www/settings/booking-types/[uid].js +0 -129
- package/dist/apps/www/settings/booking-types/index.js +0 -29
- package/dist/apps/www/settings/booking-types/new/index.js +0 -86
|
@@ -14,58 +14,234 @@
|
|
|
14
14
|
* see that file's own doc comment for why this doesn't conflict with `AccessHandlePoolVFS` itself being
|
|
15
15
|
* synchronous underneath it.
|
|
16
16
|
*
|
|
17
|
+
* **Every operation on a mailbox's connection runs through that mailbox's own serial queue**
|
|
18
|
+
* (`runExclusive()`). An Asyncify module is not re-entrant: starting a second `sqlite3.*` call while
|
|
19
|
+
* another is suspended inside an async VFS method corrupts the unwinding state. `postMessage` requests
|
|
20
|
+
* arrive concurrently (e.g. `searchTier2.ts` issues `search` and `coverage` together, while a build pass
|
|
21
|
+
* is mid-`indexEntities`), so without the queue they interleaved inside the same module. It also closes
|
|
22
|
+
* an `init` race where two concurrent `init`s both saw no connection and created two VFS instances over
|
|
23
|
+
* one OPFS pool. Each mailbox gets its own module instance, so separate mailboxes don't need to share
|
|
24
|
+
* one queue.
|
|
25
|
+
*
|
|
17
26
|
* One SQLite connection per mailbox, opened on `init` and kept for the Worker's lifetime (or until
|
|
18
|
-
* `destroy`).
|
|
19
|
-
*
|
|
27
|
+
* `destroy`). A Web Lock per mailbox (`navigator.locks`) ensures only one tab holds a given index open;
|
|
28
|
+
* a second tab's `init` fails with a logged reason and Tier 2 simply contributes nothing there.
|
|
20
29
|
*
|
|
21
|
-
*
|
|
22
|
-
* an internal diagnostic (proves the encrypted round trip end to end: write through `EncryptingVFS`,
|
|
23
|
-
* close the connection, reopen, read back) rather than being removed once the real surface existed.
|
|
30
|
+
* `entity_uid` is this module's identifier for a message (the only entity type Tier 2 covers today).
|
|
24
31
|
*/
|
|
25
32
|
import SQLiteESMFactory from "@journeyapps/wa-sqlite/dist/wa-sqlite-async.mjs";
|
|
26
33
|
import * as SQLite from "@journeyapps/wa-sqlite";
|
|
27
34
|
import { EncryptingVFS } from "./localIndexVFS.js";
|
|
35
|
+
import { listLocalIndexMailboxUids, poolNameFor, removeLocalIndexDirectory } from "./localIndexStorage.js";
|
|
28
36
|
import { BM25_WEIGHTS_SQL, CREATE_SCHEMA_SQL, SCHEMA_VERSION, UPSERT_ENTITY_SQL, buildMatchExpression, buildSearchPredicates, entityBindValues, } from "./localIndexSchema.js";
|
|
29
|
-
/** Keyed by `mailboxUid` - a Worker instance is per-tab, not per-mailbox
|
|
30
|
-
* though only one mailbox is ever unlocked in this app's UI at a time today. */
|
|
37
|
+
/** Keyed by `mailboxUid` - a Worker instance is per-tab, not per-mailbox. */
|
|
31
38
|
const connections = new Map();
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
function
|
|
36
|
-
|
|
39
|
+
/** Per-mailbox promise chains - see this module's doc comment. Each value never rejects. */
|
|
40
|
+
const queues = new Map();
|
|
41
|
+
/** Runs `task` after every previously queued task for `mailboxUid` has settled. Exported for tests. */
|
|
42
|
+
export function runExclusive(mailboxUid, task) {
|
|
43
|
+
const previous = queues.get(mailboxUid) ?? Promise.resolve();
|
|
44
|
+
const result = previous.then(task);
|
|
45
|
+
const tail = result.then(() => undefined, () => undefined);
|
|
46
|
+
queues.set(mailboxUid, tail);
|
|
47
|
+
void tail.then(() => {
|
|
48
|
+
if (queues.get(mailboxUid) === tail) {
|
|
49
|
+
queues.delete(mailboxUid);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
/** Generation of the most recent `destroy` per mailbox, and of the most recent `destroyAll`. */
|
|
55
|
+
const destroyedGenerations = new Map();
|
|
56
|
+
let destroyedAllGeneration = 0;
|
|
57
|
+
/** Rejects a build-pass call issued before its mailbox's index was last destroyed. */
|
|
58
|
+
export class StaleGenerationError extends Error {
|
|
59
|
+
constructor(mailboxUid) {
|
|
60
|
+
super(`Local search index for mailbox ${mailboxUid} was destroyed after this build started.`);
|
|
61
|
+
this.name = "StaleGenerationError";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function assertCurrentGeneration(mailboxUid, generation) {
|
|
65
|
+
if (generation === undefined) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (generation < Math.max(destroyedGenerations.get(mailboxUid) ?? 0, destroyedAllGeneration)) {
|
|
69
|
+
throw new StaleGenerationError(mailboxUid);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function recordDestroyGeneration(mailboxUid, generation) {
|
|
73
|
+
if (generation === undefined) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (mailboxUid === undefined) {
|
|
77
|
+
destroyedAllGeneration = Math.max(destroyedAllGeneration, generation);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
destroyedGenerations.set(mailboxUid, Math.max(destroyedGenerations.get(mailboxUid) ?? 0, generation));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Thrown by `init` when another tab already holds this mailbox's index open. */
|
|
84
|
+
export class LocalIndexBusyError extends Error {
|
|
85
|
+
constructor(mailboxUid) {
|
|
86
|
+
super(`Local search index for mailbox ${mailboxUid} is already open in another tab.`);
|
|
87
|
+
this.name = "LocalIndexBusyError";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Takes an exclusive, non-waiting Web Lock for one mailbox's index and holds it until the returned release
|
|
92
|
+
* function is called. `undefined` when the Web Locks API isn't available - `AccessHandlePoolVFS`'s own
|
|
93
|
+
* exclusive sync access handles still stop a second tab from opening the pool, just with a less specific
|
|
94
|
+
* error.
|
|
95
|
+
*/
|
|
96
|
+
async function acquireIndexLock(mailboxUid) {
|
|
97
|
+
const locks = typeof navigator === "undefined" ? undefined : navigator.locks;
|
|
98
|
+
if (!locks) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
locks
|
|
103
|
+
.request(`${poolNameFor(mailboxUid)}:lock`, { ifAvailable: true }, (lock) => {
|
|
104
|
+
if (!lock) {
|
|
105
|
+
reject(new LocalIndexBusyError(mailboxUid));
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
return new Promise((release) => resolve(release));
|
|
109
|
+
})
|
|
110
|
+
.catch(reject);
|
|
111
|
+
});
|
|
37
112
|
}
|
|
38
|
-
|
|
113
|
+
/** Wraps a failure to even open the database because its pages don't decrypt/authenticate. */
|
|
114
|
+
class LocalIndexCorruptedError extends Error {
|
|
115
|
+
constructor(cause) {
|
|
116
|
+
super(`Local search index is corrupted: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
117
|
+
this.name = "LocalIndexCorruptedError";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** SQLite's own exact messages for `SQLITE_CORRUPT`/`SQLITE_NOTADB`, for an error that lost its code. Anchored
|
|
121
|
+
* on both ends: an FTS5 error echoes the user's query text back (e.g. `no such column: malformed`), and a
|
|
122
|
+
* loose substring match on that used to wipe the whole index. */
|
|
123
|
+
const CORRUPTION_MESSAGE = /^(database disk image is malformed|file is not a database)$/i;
|
|
124
|
+
/** A real corruption signal (discard and rebuild) versus an ordinary error (e.g. a malformed MATCH). */
|
|
125
|
+
function isCorruptionError(err, connection) {
|
|
126
|
+
if (err instanceof LocalIndexCorruptedError || connection?.vfs.corruptionDetected) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
const code = err?.code;
|
|
130
|
+
if (code === SQLite.SQLITE_CORRUPT || code === SQLite.SQLITE_NOTADB) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
return err instanceof Error && CORRUPTION_MESSAGE.test(err.message);
|
|
134
|
+
}
|
|
135
|
+
async function openRawConnection(params) {
|
|
136
|
+
const { mailboxUid, indexKey } = params;
|
|
39
137
|
const module = await SQLiteESMFactory();
|
|
40
138
|
const sqlite3 = SQLite.Factory(module);
|
|
41
139
|
const vfs = await EncryptingVFS.create(poolNameFor(mailboxUid), module, indexKey);
|
|
42
140
|
sqlite3.vfs_register(vfs, true);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const storedVersion = await readMeta(connection, "schema_version");
|
|
50
|
-
if (storedVersion !== String(SCHEMA_VERSION)) {
|
|
51
|
-
// §11 "Invalidation... discarded and rebuilt... on schema version change" - drop every table's
|
|
52
|
-
// rows (the DDL itself is `CREATE ... IF NOT EXISTS`, already current) and start fresh, rather
|
|
53
|
-
// than attempting to migrate content built under an incompatible schema.
|
|
54
|
-
await sqlite3.exec(connection.db, "DELETE FROM entities; DELETE FROM entities_fts;");
|
|
55
|
-
await writeMeta(connection, "schema_version", String(SCHEMA_VERSION));
|
|
141
|
+
let db;
|
|
142
|
+
try {
|
|
143
|
+
db = await sqlite3.open_v2("index.db");
|
|
144
|
+
// No WAL, no rollback journal - see localIndexVFS.ts's own doc comment on why this index's lack of
|
|
145
|
+
// a durability requirement makes that an acceptable, deliberate simplification here.
|
|
146
|
+
await sqlite3.exec(db, "PRAGMA journal_mode=OFF;");
|
|
56
147
|
}
|
|
57
|
-
|
|
148
|
+
catch (err) {
|
|
149
|
+
// Opening reads the header page, so a wrong key or a corrupted first block fails right here.
|
|
150
|
+
if (db !== undefined) {
|
|
151
|
+
await sqlite3.close(db).catch(() => undefined);
|
|
152
|
+
}
|
|
153
|
+
await vfs.close();
|
|
154
|
+
throw vfs.corruptionDetected ? new LocalIndexCorruptedError(err) : err;
|
|
155
|
+
}
|
|
156
|
+
return { sqlite3, db, vfs, params };
|
|
58
157
|
}
|
|
59
|
-
async function
|
|
158
|
+
async function closeRawConnection(connection) {
|
|
159
|
+
try {
|
|
160
|
+
await connection.sqlite3.close(connection.db);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
await connection.vfs.close();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function queryValue(connection, sql, bindings = []) {
|
|
60
167
|
let value;
|
|
61
|
-
for await (const stmt of connection.sqlite3.statements(connection.db,
|
|
62
|
-
|
|
168
|
+
for await (const stmt of connection.sqlite3.statements(connection.db, sql)) {
|
|
169
|
+
if (bindings.length > 0) {
|
|
170
|
+
connection.sqlite3.bind_collection(stmt, bindings);
|
|
171
|
+
}
|
|
63
172
|
if ((await connection.sqlite3.step(stmt)) === SQLite.SQLITE_ROW) {
|
|
64
|
-
value = connection.sqlite3.column(stmt, 0);
|
|
173
|
+
value = connection.sqlite3.column(stmt, 0) ?? undefined;
|
|
65
174
|
}
|
|
66
175
|
}
|
|
67
176
|
return value;
|
|
68
177
|
}
|
|
178
|
+
/** Creates the schema on a brand-new database, or validates an existing one. Resolves `false` when an
|
|
179
|
+
* existing database was built under a different `SCHEMA_VERSION` and must be discarded (§11
|
|
180
|
+
* "Invalidation... on schema version change"). */
|
|
181
|
+
async function initializeSchema(connection) {
|
|
182
|
+
const hasMeta = await queryValue(connection, "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'meta'");
|
|
183
|
+
if (hasMeta) {
|
|
184
|
+
return (await readMeta(connection, "schema_version")) === String(SCHEMA_VERSION);
|
|
185
|
+
}
|
|
186
|
+
// Both pragmas only take effect before the first table exists, which is why a schema-version bump
|
|
187
|
+
// recreates the whole file rather than just clearing rows.
|
|
188
|
+
await connection.sqlite3.exec(connection.db, "PRAGMA page_size=4096; PRAGMA auto_vacuum=INCREMENTAL;");
|
|
189
|
+
await connection.sqlite3.exec(connection.db, CREATE_SCHEMA_SQL);
|
|
190
|
+
await writeMeta(connection, "schema_version", String(SCHEMA_VERSION));
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
/** Opens a just-discarded (empty) index and creates its schema - closing the connection again if the schema
|
|
194
|
+
* step fails, so its OPFS access handles aren't leaked for the rest of the Worker's life. */
|
|
195
|
+
async function openFreshConnection(params) {
|
|
196
|
+
const connection = await openRawConnection(params);
|
|
197
|
+
try {
|
|
198
|
+
await initializeSchema(connection);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
await closeRawConnection(connection).catch(() => undefined);
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
return connection;
|
|
205
|
+
}
|
|
206
|
+
async function openConnection(params) {
|
|
207
|
+
const releaseLock = await acquireIndexLock(params.mailboxUid);
|
|
208
|
+
try {
|
|
209
|
+
let connection;
|
|
210
|
+
let usable;
|
|
211
|
+
try {
|
|
212
|
+
connection = await openRawConnection(params);
|
|
213
|
+
usable = await initializeSchema(connection);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
if (connection) {
|
|
217
|
+
await closeRawConnection(connection).catch(() => undefined);
|
|
218
|
+
}
|
|
219
|
+
if (!isCorruptionError(err, connection)) {
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
connection = undefined;
|
|
223
|
+
usable = false;
|
|
224
|
+
}
|
|
225
|
+
if (!usable || !connection) {
|
|
226
|
+
// Corrupted (GCM auth failure, "malformed"/"not a database") or an old schema version: discard
|
|
227
|
+
// the whole database and start empty - the builder repopulates it (§11 "discarded and rebuilt").
|
|
228
|
+
if (connection) {
|
|
229
|
+
await closeRawConnection(connection).catch(() => undefined);
|
|
230
|
+
}
|
|
231
|
+
await removeLocalIndexDirectory(params.mailboxUid);
|
|
232
|
+
connection = await openFreshConnection(params);
|
|
233
|
+
}
|
|
234
|
+
connection.releaseLock = releaseLock;
|
|
235
|
+
return connection;
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
releaseLock?.();
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async function readMeta(connection, key) {
|
|
243
|
+
return queryValue(connection, "SELECT value FROM meta WHERE key = ?", [key]);
|
|
244
|
+
}
|
|
69
245
|
async function writeMeta(connection, key, value) {
|
|
70
246
|
for await (const stmt of connection.sqlite3.statements(connection.db, "INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)")) {
|
|
71
247
|
connection.sqlite3.bind_collection(stmt, [key, value]);
|
|
@@ -73,10 +249,30 @@ async function writeMeta(connection, key, value) {
|
|
|
73
249
|
}
|
|
74
250
|
}
|
|
75
251
|
async function init(params) {
|
|
252
|
+
assertCurrentGeneration(params.mailboxUid, params.generation);
|
|
76
253
|
if (connections.has(params.mailboxUid)) {
|
|
77
254
|
return;
|
|
78
255
|
}
|
|
79
|
-
|
|
256
|
+
try {
|
|
257
|
+
const connection = await openConnection(params);
|
|
258
|
+
try {
|
|
259
|
+
// A completion (or an in-progress flag) persisted by an earlier session proves nothing about mail
|
|
260
|
+
// that arrived since - only a pass that finishes while this connection is open may narrow Tier 3.
|
|
261
|
+
await clearBuildState(connection);
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
await closeRawConnection(connection).catch(() => undefined);
|
|
265
|
+
connection.releaseLock?.();
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
connections.set(params.mailboxUid, connection);
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
// Tier 2 degrades to "contributes nothing" in this tab - say why, once per attempt, rather than
|
|
272
|
+
// leaving a silently empty local tier.
|
|
273
|
+
console.warn(`localIndexWorker: local search unavailable for mailbox ${params.mailboxUid}: ${err instanceof Error ? err.message : String(err)}`);
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
80
276
|
}
|
|
81
277
|
function requireConnection(mailboxUid) {
|
|
82
278
|
const connection = connections.get(mailboxUid);
|
|
@@ -85,118 +281,225 @@ function requireConnection(mailboxUid) {
|
|
|
85
281
|
}
|
|
86
282
|
return connection;
|
|
87
283
|
}
|
|
88
|
-
/** Tears down a mailbox's connection completely: the SQLite connection
|
|
89
|
-
*
|
|
90
|
-
* (see `EncryptingVFS.close()`'s own doc comment on why skipping the second one breaks re-`init()`ing the
|
|
91
|
-
* same mailbox). Removes the entry from `connections` either way. */
|
|
284
|
+
/** Tears down a mailbox's connection completely: the SQLite connection, the underlying VFS's pooled
|
|
285
|
+
* access handles (see `EncryptingVFS.close()`), and the cross-tab Web Lock. */
|
|
92
286
|
async function closeConnection(mailboxUid) {
|
|
93
287
|
const connection = connections.get(mailboxUid);
|
|
94
288
|
if (!connection) {
|
|
95
289
|
return;
|
|
96
290
|
}
|
|
97
291
|
connections.delete(mailboxUid);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
async function sumBytes(connection) {
|
|
102
|
-
let total = 0;
|
|
103
|
-
for await (const stmt of connection.sqlite3.statements(connection.db, "SELECT COALESCE(SUM(byte_size), 0) FROM entities")) {
|
|
104
|
-
if ((await connection.sqlite3.step(stmt)) === SQLite.SQLITE_ROW) {
|
|
105
|
-
total = connection.sqlite3.column(stmt, 0);
|
|
106
|
-
}
|
|
292
|
+
try {
|
|
293
|
+
await closeRawConnection(connection);
|
|
107
294
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
async function oldestDateForSort(connection) {
|
|
111
|
-
let oldest;
|
|
112
|
-
for await (const stmt of connection.sqlite3.statements(connection.db, "SELECT MIN(date_for_sort) FROM entities")) {
|
|
113
|
-
if ((await connection.sqlite3.step(stmt)) === SQLite.SQLITE_ROW) {
|
|
114
|
-
oldest = connection.sqlite3.column(stmt, 0) ?? undefined;
|
|
115
|
-
}
|
|
295
|
+
finally {
|
|
296
|
+
connection.releaseLock?.();
|
|
116
297
|
}
|
|
117
|
-
return oldest;
|
|
118
298
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Runs `operation` against a mailbox's open connection. If it fails because the index is corrupted, the
|
|
301
|
+
* index is destroyed and reopened empty (same key, same lock) so the next build pass repopulates it, and
|
|
302
|
+
* the original error is rethrown - the caller's own degradation (e.g. `search()` returning no hits) still
|
|
303
|
+
* applies to this one call.
|
|
304
|
+
*/
|
|
305
|
+
async function withConnection(mailboxUid, operation) {
|
|
306
|
+
const connection = requireConnection(mailboxUid);
|
|
307
|
+
try {
|
|
308
|
+
return await operation(connection);
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
if (isCorruptionError(err, connection) && connections.get(mailboxUid) === connection) {
|
|
312
|
+
await resetCorruptedConnection(mailboxUid, connection);
|
|
124
313
|
}
|
|
314
|
+
throw err;
|
|
125
315
|
}
|
|
126
|
-
return count;
|
|
127
316
|
}
|
|
128
|
-
/**
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
317
|
+
/** `withConnection()` for a build-pass call - rejected first if the call's generation is stale. */
|
|
318
|
+
async function withCurrentConnection(params, operation) {
|
|
319
|
+
assertCurrentGeneration(params.mailboxUid, params.generation);
|
|
320
|
+
return withConnection(params.mailboxUid, operation);
|
|
321
|
+
}
|
|
322
|
+
/** Discards a corrupted index and reopens it empty under the same key and Web Lock. If reopening fails,
|
|
323
|
+
* the mailbox is left uninitialized (lock released) - the next `init()` tries again from scratch. */
|
|
324
|
+
async function resetCorruptedConnection(mailboxUid, connection) {
|
|
325
|
+
console.warn(`localIndexWorker: local search index for mailbox ${mailboxUid} is corrupted; discarding and rebuilding.`);
|
|
326
|
+
connections.delete(mailboxUid);
|
|
327
|
+
await closeRawConnection(connection).catch(() => undefined);
|
|
328
|
+
try {
|
|
329
|
+
await removeLocalIndexDirectory(mailboxUid);
|
|
330
|
+
const fresh = await openFreshConnection(connection.params);
|
|
331
|
+
fresh.releaseLock = connection.releaseLock;
|
|
332
|
+
connections.set(mailboxUid, fresh);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
connection.releaseLock?.();
|
|
138
336
|
}
|
|
139
|
-
return deleted;
|
|
140
337
|
}
|
|
141
|
-
/**
|
|
142
|
-
*
|
|
143
|
-
|
|
144
|
-
|
|
338
|
+
/** Bytes actually used by live pages - `page_count` minus free pages, times `page_size`. Counts the FTS5
|
|
339
|
+
* shadow tables and indexes too, unlike summing a per-row estimate. */
|
|
340
|
+
async function usedBytes(connection) {
|
|
341
|
+
const pageCount = (await queryValue(connection, "PRAGMA page_count")) ?? 0;
|
|
342
|
+
const freelist = (await queryValue(connection, "PRAGMA freelist_count")) ?? 0;
|
|
343
|
+
const pageSize = (await queryValue(connection, "PRAGMA page_size")) ?? 4096;
|
|
344
|
+
return (pageCount - freelist) * pageSize;
|
|
345
|
+
}
|
|
346
|
+
/** Physical database size (what OPFS actually stores) - exported via `coverage()` for diagnostics/tests. */
|
|
347
|
+
async function fileBytes(connection) {
|
|
348
|
+
const pageCount = (await queryValue(connection, "PRAGMA page_count")) ?? 0;
|
|
349
|
+
const pageSize = (await queryValue(connection, "PRAGMA page_size")) ?? 4096;
|
|
350
|
+
return pageCount * pageSize;
|
|
351
|
+
}
|
|
352
|
+
/** Maximum bulk-eviction rounds per call - each round deletes a whole date range sized from the measured
|
|
353
|
+
* overshoot, so more than a couple only happens when the per-row weights badly underestimate real size. */
|
|
354
|
+
const MAX_EVICTION_ROUNDS = 8;
|
|
355
|
+
/**
|
|
356
|
+
* Oldest-first eviction against the configured byte budget (spec §11). Measures the real database size,
|
|
357
|
+
* then deletes a contiguous oldest date range in one statement, sized by walking rows oldest-first with a
|
|
358
|
+
* running total of their `byte_size` weights scaled to the measured size - rather than the previous
|
|
359
|
+
* delete-one-row-then-re-SUM loop, which was O(n²). `incremental_vacuum` then returns freed pages to the
|
|
360
|
+
* filesystem. A no-op when no budget has been configured yet. Resolves whether anything was evicted.
|
|
361
|
+
*/
|
|
145
362
|
async function applyEviction(connection) {
|
|
146
363
|
const byteBudgetRaw = await readMeta(connection, "byte_budget");
|
|
147
364
|
const byteBudget = byteBudgetRaw ? Number(byteBudgetRaw) : undefined;
|
|
148
365
|
if (!byteBudget) {
|
|
149
|
-
return;
|
|
366
|
+
return false;
|
|
150
367
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
368
|
+
// The newest cutoff actually deleted up to; `undefined` while nothing has been evicted.
|
|
369
|
+
let watermark;
|
|
370
|
+
for (let round = 0; round < MAX_EVICTION_ROUNDS; round++) {
|
|
371
|
+
const used = await usedBytes(connection);
|
|
372
|
+
if (used <= byteBudget) {
|
|
373
|
+
break;
|
|
155
374
|
}
|
|
156
|
-
const
|
|
157
|
-
if (
|
|
158
|
-
|
|
375
|
+
const totalWeight = (await queryValue(connection, "SELECT COALESCE(SUM(byte_size), 0) FROM entities")) ?? 0;
|
|
376
|
+
if (totalWeight <= 0) {
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
const overshootWeight = ((used - byteBudget) / used) * totalWeight;
|
|
380
|
+
let running = 0;
|
|
381
|
+
let cutoffRowid;
|
|
382
|
+
let cutoffDate;
|
|
383
|
+
for await (const stmt of connection.sqlite3.statements(connection.db, "SELECT date_for_sort, rowid, byte_size FROM entities ORDER BY date_for_sort ASC, rowid ASC")) {
|
|
384
|
+
while ((await connection.sqlite3.step(stmt)) === SQLite.SQLITE_ROW) {
|
|
385
|
+
cutoffDate = connection.sqlite3.column(stmt, 0);
|
|
386
|
+
cutoffRowid = connection.sqlite3.column(stmt, 1);
|
|
387
|
+
running += connection.sqlite3.column(stmt, 2);
|
|
388
|
+
if (running >= overshootWeight) {
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (cutoffDate === undefined || cutoffRowid === undefined) {
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
for await (const stmt of connection.sqlite3.statements(connection.db, "DELETE FROM entities WHERE date_for_sort < ? OR (date_for_sort = ? AND rowid <= ?)")) {
|
|
397
|
+
connection.sqlite3.bind_collection(stmt, [cutoffDate, cutoffDate, cutoffRowid]);
|
|
398
|
+
await connection.sqlite3.step(stmt);
|
|
399
|
+
}
|
|
400
|
+
// The cutoff row was just read, so this always deleted at least that row.
|
|
401
|
+
watermark = cutoffDate;
|
|
402
|
+
}
|
|
403
|
+
if (watermark !== undefined) {
|
|
404
|
+
// Rows at exactly `watermark` may be partly evicted (the rowid tiebreaker), so only strictly older
|
|
405
|
+
// messages are treated as out of the window. Never moves backwards while at budget.
|
|
406
|
+
const previous = await readMeta(connection, "evicted_before");
|
|
407
|
+
if (!previous || watermark > previous) {
|
|
408
|
+
await writeMeta(connection, "evicted_before", watermark);
|
|
159
409
|
}
|
|
410
|
+
await connection.sqlite3.exec(connection.db, "PRAGMA incremental_vacuum;");
|
|
160
411
|
}
|
|
412
|
+
return watermark !== undefined;
|
|
161
413
|
}
|
|
162
|
-
async function indexEntities(
|
|
163
|
-
const connection = requireConnection(mailboxUid);
|
|
414
|
+
async function indexEntities(connection, entities) {
|
|
164
415
|
for (const entity of entities) {
|
|
165
416
|
for await (const stmt of connection.sqlite3.statements(connection.db, UPSERT_ENTITY_SQL)) {
|
|
166
417
|
connection.sqlite3.bind_collection(stmt, entityBindValues(entity));
|
|
167
418
|
await connection.sqlite3.step(stmt);
|
|
168
419
|
}
|
|
169
420
|
}
|
|
170
|
-
await applyEviction(connection);
|
|
421
|
+
const budgetReached = await applyEviction(connection);
|
|
422
|
+
return { budgetReached, ...(await readWindowState(connection)) };
|
|
171
423
|
}
|
|
172
|
-
async function removeEntity(
|
|
173
|
-
const connection = requireConnection(mailboxUid);
|
|
424
|
+
async function removeEntity(connection, entityUid) {
|
|
174
425
|
for await (const stmt of connection.sqlite3.statements(connection.db, "DELETE FROM entities WHERE entity_uid = ?")) {
|
|
175
426
|
connection.sqlite3.bind_collection(stmt, [entityUid]);
|
|
176
427
|
await connection.sqlite3.step(stmt);
|
|
177
428
|
}
|
|
178
429
|
}
|
|
179
|
-
|
|
180
|
-
|
|
430
|
+
/** Re-points an indexed message at a new folder in place (archive, cancel-scheduled-send). Clears
|
|
431
|
+
* `entity_version` so the next build pass re-validates the row against the server. */
|
|
432
|
+
async function moveEntity(connection, entityUid, folderUid) {
|
|
433
|
+
for await (const stmt of connection.sqlite3.statements(connection.db, "UPDATE entities SET folder_uid = ?, entity_version = NULL WHERE entity_uid = ?")) {
|
|
434
|
+
connection.sqlite3.bind_collection(stmt, [folderUid, entityUid]);
|
|
435
|
+
await connection.sqlite3.step(stmt);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/** `entity_version` for whichever of `entityUids` are already indexed - one batched query per builder page,
|
|
439
|
+
* so a rebuild can skip re-fetching/decrypting unchanged messages. */
|
|
440
|
+
async function indexedVersions(connection, entityUids) {
|
|
441
|
+
const versions = {};
|
|
442
|
+
if (entityUids.length === 0) {
|
|
443
|
+
return versions;
|
|
444
|
+
}
|
|
445
|
+
const placeholders = entityUids.map(() => "?").join(", ");
|
|
446
|
+
for await (const stmt of connection.sqlite3.statements(connection.db, `SELECT entity_uid, entity_version FROM entities WHERE entity_uid IN (${placeholders})`)) {
|
|
447
|
+
connection.sqlite3.bind_collection(stmt, entityUids);
|
|
448
|
+
while ((await connection.sqlite3.step(stmt)) === SQLite.SQLITE_ROW) {
|
|
449
|
+
const version = connection.sqlite3.column(stmt, 1);
|
|
450
|
+
if (version !== null) {
|
|
451
|
+
versions[connection.sqlite3.column(stmt, 0)] = version;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return versions;
|
|
456
|
+
}
|
|
457
|
+
/** Deletes rows a complete build pass didn't see (deleted, or moved out of every mail folder, on any
|
|
458
|
+
* client) within the pass's own time floor. Resolves how many rows were removed. */
|
|
459
|
+
async function pruneEntities(connection, { keepEntityUids, since, folderUids }) {
|
|
460
|
+
const keep = new Set(keepEntityUids);
|
|
461
|
+
const folders = folderUids ? new Set(folderUids) : undefined;
|
|
462
|
+
const stale = [];
|
|
463
|
+
for await (const stmt of connection.sqlite3.statements(connection.db, since ? "SELECT entity_uid, folder_uid FROM entities WHERE date_for_sort >= ?" : "SELECT entity_uid, folder_uid FROM entities")) {
|
|
464
|
+
if (since) {
|
|
465
|
+
connection.sqlite3.bind_collection(stmt, [since]);
|
|
466
|
+
}
|
|
467
|
+
while ((await connection.sqlite3.step(stmt)) === SQLite.SQLITE_ROW) {
|
|
468
|
+
const uid = connection.sqlite3.column(stmt, 0);
|
|
469
|
+
const folderUid = connection.sqlite3.column(stmt, 1);
|
|
470
|
+
if (!keep.has(uid) && (!folders || folders.has(folderUid))) {
|
|
471
|
+
stale.push(uid);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
for (const uid of stale) {
|
|
476
|
+
await removeEntity(connection, uid);
|
|
477
|
+
}
|
|
478
|
+
if (stale.length > 0) {
|
|
479
|
+
await connection.sqlite3.exec(connection.db, "PRAGMA incremental_vacuum;");
|
|
480
|
+
}
|
|
481
|
+
return stale.length;
|
|
482
|
+
}
|
|
483
|
+
async function search(connection, { mailboxUid, parsed, limit, offset = 0 }) {
|
|
181
484
|
const { where, params } = buildSearchPredicates(parsed, mailboxUid);
|
|
182
485
|
const matchExpr = buildMatchExpression(parsed);
|
|
183
486
|
const hits = [];
|
|
184
|
-
// A malformed MATCH string is a real, reachable case (FTS5's query syntax rejects some inputs
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
// degrades on its own per-candidate/per-provider failures, rather than breaking the whole search.
|
|
487
|
+
// A malformed MATCH string is a real, reachable case (FTS5's query syntax rejects some inputs the
|
|
488
|
+
// server's more lenient parser accepts) - fails soft to "this tier found nothing". Corruption is the
|
|
489
|
+
// one failure rethrown, so `withConnection()` can discard and rebuild the index.
|
|
188
490
|
try {
|
|
189
|
-
// Fetches one extra row beyond `limit` so `hasMore`
|
|
190
|
-
//
|
|
491
|
+
// Fetches one extra row beyond `limit` so `hasMore` can be determined without a COUNT(*). The
|
|
492
|
+
// `entity_uid` tiebreaker makes the ordering total, so OFFSET pages never overlap or skip rows
|
|
493
|
+
// that share a rank/date.
|
|
191
494
|
const fetchLimit = limit + 1;
|
|
192
495
|
const sql = matchExpr
|
|
193
496
|
? `SELECT e.entity_uid, bm25(entities_fts, ${BM25_WEIGHTS_SQL}) AS rank,
|
|
194
497
|
snippet(entities_fts, 2, '', '', '…', 24) AS snip
|
|
195
498
|
FROM entities_fts f JOIN entities e ON e.rowid = f.rowid
|
|
196
499
|
WHERE entities_fts MATCH ? AND ${where}
|
|
197
|
-
ORDER BY rank LIMIT ? OFFSET ?`
|
|
500
|
+
ORDER BY rank, e.entity_uid LIMIT ? OFFSET ?`
|
|
198
501
|
: `SELECT e.entity_uid, 0 AS rank, NULL AS snip FROM entities e WHERE ${where}
|
|
199
|
-
ORDER BY e.date_for_sort DESC LIMIT ? OFFSET ?`;
|
|
502
|
+
ORDER BY e.date_for_sort DESC, e.entity_uid LIMIT ? OFFSET ?`;
|
|
200
503
|
const bindings = matchExpr ? [matchExpr, ...params, fetchLimit, offset] : [...params, fetchLimit, offset];
|
|
201
504
|
for await (const stmt of connection.sqlite3.statements(connection.db, sql)) {
|
|
202
505
|
connection.sqlite3.bind_collection(stmt, bindings);
|
|
@@ -209,7 +512,10 @@ async function search({ mailboxUid, parsed, limit, offset = 0 }) {
|
|
|
209
512
|
}
|
|
210
513
|
}
|
|
211
514
|
}
|
|
212
|
-
catch {
|
|
515
|
+
catch (err) {
|
|
516
|
+
if (isCorruptionError(err, connection)) {
|
|
517
|
+
throw err;
|
|
518
|
+
}
|
|
213
519
|
return { hits: [], hasMore: false };
|
|
214
520
|
}
|
|
215
521
|
const hasMore = hits.length > limit;
|
|
@@ -218,43 +524,71 @@ async function search({ mailboxUid, parsed, limit, offset = 0 }) {
|
|
|
218
524
|
}
|
|
219
525
|
return { hits, hasMore };
|
|
220
526
|
}
|
|
221
|
-
async function coverage(
|
|
222
|
-
const
|
|
527
|
+
async function coverage(connection) {
|
|
528
|
+
const oldest = await queryValue(connection, "SELECT MIN(date_for_sort) FROM entities");
|
|
529
|
+
const complete = (await readMeta(connection, "build_complete")) === "1";
|
|
530
|
+
const coveredFrom = await readMeta(connection, "covered_from");
|
|
531
|
+
// A complete pass walked every folder back to its time floor, but a folder's last page can reach
|
|
532
|
+
// further back than another folder's did - so the guaranteed frontier is the later of the two.
|
|
533
|
+
const indexedFrom = complete && oldest && coveredFrom && coveredFrom > oldest ? coveredFrom : oldest;
|
|
534
|
+
const coveredUntil = await readMeta(connection, "covered_until");
|
|
223
535
|
return {
|
|
224
|
-
indexedFrom
|
|
225
|
-
indexedCount: await
|
|
536
|
+
indexedFrom,
|
|
537
|
+
indexedCount: (await queryValue(connection, "SELECT COUNT(*) FROM entities")) ?? 0,
|
|
226
538
|
building: (await readMeta(connection, "building")) === "1",
|
|
539
|
+
complete,
|
|
540
|
+
indexedUntil: complete && coveredUntil ? coveredUntil : undefined,
|
|
541
|
+
fileBytes: await fileBytes(connection),
|
|
227
542
|
};
|
|
228
543
|
}
|
|
229
|
-
|
|
230
|
-
|
|
544
|
+
/** Below this fraction of the budget, the eviction watermark is dropped so older mail can come back in -
|
|
545
|
+
* the gap between it and 1.0 keeps a pass from evicting and re-fetching the same boundary every time. */
|
|
546
|
+
const WATERMARK_RESET_FRACTION = 0.9;
|
|
547
|
+
async function readWindowState(connection) {
|
|
548
|
+
return { evictedBefore: (await readMeta(connection, "evicted_before")) || undefined };
|
|
549
|
+
}
|
|
550
|
+
async function setWindow(connection, { timeFloorMonths, byteBudgetBytes }) {
|
|
231
551
|
await writeMeta(connection, "time_floor_months", String(timeFloorMonths));
|
|
232
552
|
await writeMeta(connection, "byte_budget", String(byteBudgetBytes));
|
|
233
553
|
// A lowered budget must shrink the window immediately, not just gate future inserts (spec §11 "the
|
|
234
554
|
// client MUST reduce the window rather than fail writes when the budget is reached").
|
|
235
|
-
await applyEviction(connection);
|
|
555
|
+
const evicted = await applyEviction(connection);
|
|
556
|
+
if (!evicted && (!byteBudgetBytes || (await usedBytes(connection)) <= byteBudgetBytes * WATERMARK_RESET_FRACTION)) {
|
|
557
|
+
await writeMeta(connection, "evicted_before", "");
|
|
558
|
+
}
|
|
559
|
+
return readWindowState(connection);
|
|
236
560
|
}
|
|
237
|
-
async function setBuilding(
|
|
238
|
-
const
|
|
561
|
+
async function setBuilding(connection, { building, complete, coveredFrom, coveredUntil }) {
|
|
562
|
+
const done = !building && !!complete;
|
|
239
563
|
await writeMeta(connection, "building", building ? "1" : "0");
|
|
564
|
+
// Starting a pass invalidates the previous pass's completeness until this one finishes.
|
|
565
|
+
await writeMeta(connection, "build_complete", done ? "1" : "0");
|
|
566
|
+
await writeMeta(connection, "covered_from", done && coveredFrom ? coveredFrom : "");
|
|
567
|
+
await writeMeta(connection, "covered_until", done && coveredUntil ? coveredUntil : "");
|
|
568
|
+
}
|
|
569
|
+
async function clearBuildState(connection) {
|
|
570
|
+
await setBuilding(connection, { mailboxUid: connection.params.mailboxUid, building: false, complete: false });
|
|
240
571
|
}
|
|
241
572
|
/** Closes the connection (if open) and deletes the mailbox's entire OPFS directory - the spec's "MUST be
|
|
242
|
-
* destroyed on the same events that destroy private keys" (§11)
|
|
243
|
-
*
|
|
244
|
-
* for the deletion itself (there's no VFS-level "delete everything" primitive) - safe only because the
|
|
245
|
-
* connection is already closed at this point, so nothing else holds these files open. */
|
|
573
|
+
* destroyed on the same events that destroy private keys" (§11). Rejects if the directory couldn't be
|
|
574
|
+
* removed (e.g. another tab still has it open), so the caller can report it. */
|
|
246
575
|
async function destroy(mailboxUid) {
|
|
247
576
|
await closeConnection(mailboxUid);
|
|
248
|
-
|
|
249
|
-
|
|
577
|
+
await removeLocalIndexDirectory(mailboxUid);
|
|
578
|
+
}
|
|
579
|
+
/** Destroys every local index on this origin, including ones this Worker never opened. Each mailbox's close
|
|
580
|
+
* and directory removal runs inside that mailbox's own queue, so an `init` already queued can't recreate a
|
|
581
|
+
* directory after it was removed (a stale build's queued `init` is rejected by its generation instead). */
|
|
582
|
+
async function destroyAll() {
|
|
583
|
+
const mailboxUids = new Set([...connections.keys(), ...(await listLocalIndexMailboxUids())]);
|
|
584
|
+
const failed = [];
|
|
585
|
+
await Promise.all([...mailboxUids].map((mailboxUid) => runExclusive(mailboxUid, () => destroy(mailboxUid)).catch(() => failed.push(mailboxUid))));
|
|
586
|
+
return { failed: failed.sort() };
|
|
250
587
|
}
|
|
251
588
|
/**
|
|
252
|
-
* Proves the full encrypted round trip, not just a single write-then-read within one open connection
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* a real close/reopen through `EncryptingVFS`, `AccessHandlePoolVFS`, and OPFS, not merely reading back
|
|
256
|
-
* from an in-memory cache - and confirms the row (and a `bm25()`-ranked `MATCH` query against it) both
|
|
257
|
-
* still work after that reopen.
|
|
589
|
+
* Proves the full encrypted round trip, not just a single write-then-read within one open connection:
|
|
590
|
+
* writes a row, closes the connection, re-`init()`s the same mailbox from scratch, and confirms the row
|
|
591
|
+
* (and a `bm25()`-ranked `MATCH` query against it) both still work after that reopen.
|
|
258
592
|
*/
|
|
259
593
|
async function selfTest(params) {
|
|
260
594
|
await init(params);
|
|
@@ -283,59 +617,71 @@ async function selfTest(params) {
|
|
|
283
617
|
}
|
|
284
618
|
return { matchedAfterReopen };
|
|
285
619
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
break;
|
|
295
|
-
case "init":
|
|
296
|
-
await init(params);
|
|
297
|
-
result = undefined;
|
|
298
|
-
break;
|
|
299
|
-
case "indexEntities":
|
|
300
|
-
await indexEntities(params);
|
|
301
|
-
result = undefined;
|
|
302
|
-
break;
|
|
303
|
-
case "removeEntity":
|
|
304
|
-
await removeEntity(params);
|
|
305
|
-
result = undefined;
|
|
306
|
-
break;
|
|
307
|
-
case "search":
|
|
308
|
-
result = await search(params);
|
|
309
|
-
break;
|
|
310
|
-
case "coverage":
|
|
311
|
-
result = await coverage(params);
|
|
312
|
-
break;
|
|
313
|
-
case "setWindow":
|
|
314
|
-
await setWindow(params);
|
|
315
|
-
result = undefined;
|
|
316
|
-
break;
|
|
317
|
-
case "setBuilding": {
|
|
318
|
-
const { mailboxUid, building } = params;
|
|
319
|
-
await setBuilding(mailboxUid, building);
|
|
320
|
-
result = undefined;
|
|
321
|
-
break;
|
|
322
|
-
}
|
|
323
|
-
case "destroy":
|
|
324
|
-
await destroy(params);
|
|
325
|
-
result = undefined;
|
|
326
|
-
break;
|
|
327
|
-
case "selfTest":
|
|
328
|
-
result = await selfTest(params);
|
|
329
|
-
break;
|
|
330
|
-
default:
|
|
331
|
-
throw new Error(`Unknown localIndexWorker method: ${method}`);
|
|
332
|
-
}
|
|
333
|
-
const response = { id, ok: true, result };
|
|
334
|
-
self.postMessage(response);
|
|
620
|
+
/** Routes one request to its handler, inside the owning mailbox's serial queue. Exported for tests. */
|
|
621
|
+
export async function handleRequest(method, params) {
|
|
622
|
+
switch (method) {
|
|
623
|
+
case "ping":
|
|
624
|
+
return "pong";
|
|
625
|
+
case "init": {
|
|
626
|
+
const p = params;
|
|
627
|
+
return runExclusive(p.mailboxUid, () => init(p));
|
|
335
628
|
}
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
629
|
+
case "indexEntities": {
|
|
630
|
+
const p = params;
|
|
631
|
+
return runExclusive(p.mailboxUid, () => withCurrentConnection(p, (c) => indexEntities(c, p.entities)));
|
|
632
|
+
}
|
|
633
|
+
case "removeEntity": {
|
|
634
|
+
const p = params;
|
|
635
|
+
return runExclusive(p.mailboxUid, () => withConnection(p.mailboxUid, (c) => removeEntity(c, p.entityUid)));
|
|
636
|
+
}
|
|
637
|
+
case "moveEntity": {
|
|
638
|
+
const p = params;
|
|
639
|
+
return runExclusive(p.mailboxUid, () => withConnection(p.mailboxUid, (c) => moveEntity(c, p.entityUid, p.folderUid)));
|
|
640
|
+
}
|
|
641
|
+
case "indexedVersions": {
|
|
642
|
+
const p = params;
|
|
643
|
+
return runExclusive(p.mailboxUid, () => withConnection(p.mailboxUid, (c) => indexedVersions(c, p.entityUids)));
|
|
644
|
+
}
|
|
645
|
+
case "pruneEntities": {
|
|
646
|
+
const p = params;
|
|
647
|
+
return runExclusive(p.mailboxUid, () => withCurrentConnection(p, (c) => pruneEntities(c, p)));
|
|
648
|
+
}
|
|
649
|
+
case "search": {
|
|
650
|
+
const p = params;
|
|
651
|
+
return runExclusive(p.mailboxUid, () => withConnection(p.mailboxUid, (c) => search(c, p)));
|
|
652
|
+
}
|
|
653
|
+
case "coverage": {
|
|
654
|
+
const mailboxUid = params;
|
|
655
|
+
return runExclusive(mailboxUid, () => withConnection(mailboxUid, (c) => coverage(c)));
|
|
339
656
|
}
|
|
340
|
-
|
|
657
|
+
case "setWindow": {
|
|
658
|
+
const p = params;
|
|
659
|
+
return runExclusive(p.mailboxUid, () => withCurrentConnection(p, (c) => setWindow(c, p)));
|
|
660
|
+
}
|
|
661
|
+
case "setBuilding": {
|
|
662
|
+
const p = params;
|
|
663
|
+
return runExclusive(p.mailboxUid, () => withCurrentConnection(p, (c) => setBuilding(c, p)));
|
|
664
|
+
}
|
|
665
|
+
case "destroy": {
|
|
666
|
+
const p = params;
|
|
667
|
+
// Recorded synchronously, before queueing - any build call issued earlier but still queued behind
|
|
668
|
+
// this one is rejected once it runs.
|
|
669
|
+
recordDestroyGeneration(p.mailboxUid, p.generation);
|
|
670
|
+
return runExclusive(p.mailboxUid, () => destroy(p.mailboxUid));
|
|
671
|
+
}
|
|
672
|
+
case "destroyAll": {
|
|
673
|
+
recordDestroyGeneration(undefined, params?.generation);
|
|
674
|
+
return destroyAll();
|
|
675
|
+
}
|
|
676
|
+
case "selfTest": {
|
|
677
|
+
const p = params;
|
|
678
|
+
return runExclusive(p.mailboxUid, () => selfTest(p));
|
|
679
|
+
}
|
|
680
|
+
default:
|
|
681
|
+
throw new Error(`Unknown localIndexWorker method: ${method}`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
self.addEventListener("message", (event) => {
|
|
685
|
+
const { id, method, params } = event.data;
|
|
686
|
+
handleRequest(method, params).then((result) => self.postMessage({ id, ok: true, result }), (err) => self.postMessage({ id, ok: false, error: err instanceof Error ? err.message : String(err) }));
|
|
341
687
|
});
|