@rapidmx/web-client 0.36.0 → 0.37.1
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/apps/shared/calendar/calendarReminders.ts +35 -0
- package/apps/shared/calendar/sealedEvents.ts +42 -9
- package/apps/shared/calendar/useCalendarReminders.ts +28 -9
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -0
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +29 -1
- package/apps/shared/components/calendar/EventDetails.tsx +13 -2
- package/apps/shared/components/calendar/EventEditor.tsx +124 -17
- package/apps/shared/components/calendar/EventFormParts.tsx +1 -0
- package/apps/shared/components/calendar/EventModal.tsx +19 -2
- package/apps/shared/components/calendar/RequestChangeForm.tsx +4 -1
- package/apps/shared/components/calendar/eventForm.ts +2 -0
- package/apps/shared/components/contacts/ContactCard.tsx +13 -1
- package/apps/shared/components/contacts/ContactDetailPane.tsx +189 -9
- package/apps/shared/components/contacts/ContactForm.tsx +56 -7
- package/apps/shared/components/contacts/KeyChangeReview.tsx +5 -1
- package/apps/shared/components/contacts/contactCardData.ts +6 -3
- package/apps/shared/components/contacts/contactKeys.ts +3 -3
- package/apps/shared/components/layout/AppShell.tsx +34 -5
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +11 -4
- package/apps/shared/components/layout/SharedMailboxKeyManager.tsx +37 -6
- package/apps/shared/components/mail/InviteCard.tsx +11 -1
- package/apps/shared/components/mail/MessageDetailPane.tsx +25 -14
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +9 -2
- package/apps/shared/components/mail/compose/RecipientInput.tsx +2 -1
- package/apps/shared/components/mail/compose/protectedContactSuggestions.ts +121 -0
- package/apps/shared/components/mail/invite/inviteStore.ts +11 -1
- package/apps/shared/components/mail/pinnedSigners.ts +14 -0
- package/apps/shared/components/mail/reading/conversationPrint.ts +3 -3
- package/apps/shared/components/settings/KeyExportSetting.tsx +4 -1
- package/apps/shared/components/settings/OpenPgpKeySetting.tsx +240 -0
- package/apps/shared/components/settings/ZeroAccessSetting.tsx +13 -2
- package/apps/shared/components/settings/ZeroAccessTypes.tsx +175 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +9 -2
- package/apps/shared/mail/outbox/sendJob.ts +349 -50
- package/apps/shared/mail/outbox/sendOutcomes.ts +91 -20
- package/apps/shared/search/LocalIndexLifecycle.tsx +62 -33
- package/apps/shared/search/localIndexBuilder.ts +35 -24
- package/apps/shared/search/localIndexRpcClient.ts +91 -11
- package/apps/shared/search/localIndexSchema.ts +18 -2
- package/apps/shared/search/localIndexTransport.ts +6 -3
- package/apps/shared/search/localIndexWorker.ts +35 -18
- package/apps/shared/search/searchTier2.ts +5 -0
- package/apps/shared/search/zeroAccessIndexSync.ts +232 -40
- package/apps/shared/search/zeroAccessSyncState.ts +70 -0
- package/apps/shared/zeroAccess/ProtectedMark.tsx +19 -0
- package/apps/shared/zeroAccess/useKeysLocked.ts +25 -0
- package/apps/shared/zeroAccess/useOpenedRecord.ts +71 -0
- package/apps/www/_shell.tsx +2 -0
- package/apps/www/calendar/index.tsx +49 -5
- package/apps/www/contacts/index.tsx +67 -11
- package/apps/www/index.tsx +10 -1
- package/apps/www/settings/encryption/index.tsx +16 -1
- package/apps/www/settings/privacy/index.tsx +87 -38
- package/apps/www/tasks/index.tsx +24 -6
- package/dist/apps/shared/calendar/calendarReminders.d.ts +14 -0
- package/dist/apps/shared/calendar/calendarReminders.js +26 -0
- package/dist/apps/shared/calendar/sealedEvents.d.ts +3 -2
- package/dist/apps/shared/calendar/sealedEvents.js +40 -9
- package/dist/apps/shared/calendar/useCalendarReminders.d.ts +2 -1
- package/dist/apps/shared/calendar/useCalendarReminders.js +27 -9
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +3 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +9 -2
- package/dist/apps/shared/components/calendar/EventDetails.js +13 -2
- package/dist/apps/shared/components/calendar/EventEditor.js +115 -18
- package/dist/apps/shared/components/calendar/EventFormParts.js +1 -1
- package/dist/apps/shared/components/calendar/EventModal.d.ts +1 -1
- package/dist/apps/shared/components/calendar/EventModal.js +15 -2
- package/dist/apps/shared/components/calendar/RequestChangeForm.d.ts +3 -0
- package/dist/apps/shared/components/calendar/RequestChangeForm.js +1 -1
- package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -0
- package/dist/apps/shared/components/contacts/ContactCard.js +12 -1
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +2 -1
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +103 -9
- package/dist/apps/shared/components/contacts/ContactForm.d.ts +4 -1
- package/dist/apps/shared/components/contacts/ContactForm.js +37 -7
- package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +3 -1
- package/dist/apps/shared/components/contacts/KeyChangeReview.js +3 -2
- package/dist/apps/shared/components/contacts/contactCardData.js +6 -3
- package/dist/apps/shared/components/contacts/contactKeys.d.ts +1 -1
- package/dist/apps/shared/components/contacts/contactKeys.js +3 -3
- package/dist/apps/shared/components/layout/AppShell.d.ts +18 -1
- package/dist/apps/shared/components/layout/AppShell.js +17 -6
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -4
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.d.ts +3 -1
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.js +30 -4
- package/dist/apps/shared/components/mail/InviteCard.js +11 -1
- package/dist/apps/shared/components/mail/MessageDetailPane.js +15 -11
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +2 -2
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +3 -2
- package/dist/apps/shared/components/mail/compose/protectedContactSuggestions.d.ts +14 -0
- package/dist/apps/shared/components/mail/compose/protectedContactSuggestions.js +96 -0
- package/dist/apps/shared/components/mail/invite/inviteStore.js +11 -1
- package/dist/apps/shared/components/mail/pinnedSigners.d.ts +6 -0
- package/dist/apps/shared/components/mail/pinnedSigners.js +10 -1
- package/dist/apps/shared/components/mail/reading/conversationPrint.d.ts +2 -2
- package/dist/apps/shared/components/mail/reading/conversationPrint.js +3 -3
- package/dist/apps/shared/components/settings/KeyExportSetting.js +3 -1
- package/dist/apps/shared/components/settings/OpenPgpKeySetting.d.ts +13 -0
- package/dist/apps/shared/components/settings/OpenPgpKeySetting.js +143 -0
- package/dist/apps/shared/components/settings/ZeroAccessSetting.d.ts +2 -0
- package/dist/apps/shared/components/settings/ZeroAccessSetting.js +12 -3
- package/dist/apps/shared/components/settings/ZeroAccessTypes.d.ts +29 -0
- package/dist/apps/shared/components/settings/ZeroAccessTypes.js +107 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +3 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +3 -2
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +37 -4
- package/dist/apps/shared/mail/outbox/sendJob.js +287 -42
- package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +12 -0
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +76 -20
- package/dist/apps/shared/search/LocalIndexLifecycle.js +55 -33
- package/dist/apps/shared/search/localIndexBuilder.d.ts +4 -1
- package/dist/apps/shared/search/localIndexBuilder.js +31 -20
- package/dist/apps/shared/search/localIndexRpcClient.d.ts +5 -39
- package/dist/apps/shared/search/localIndexRpcClient.js +126 -10
- package/dist/apps/shared/search/localIndexSchema.d.ts +14 -2
- package/dist/apps/shared/search/localIndexSchema.js +16 -2
- package/dist/apps/shared/search/localIndexTransport.d.ts +6 -3
- package/dist/apps/shared/search/localIndexWorker.d.ts +9 -0
- package/dist/apps/shared/search/localIndexWorker.js +22 -15
- package/dist/apps/shared/search/searchTier2.js +5 -0
- package/dist/apps/shared/search/zeroAccessIndexSync.d.ts +37 -6
- package/dist/apps/shared/search/zeroAccessIndexSync.js +199 -37
- package/dist/apps/shared/search/zeroAccessSyncState.d.ts +20 -0
- package/dist/apps/shared/search/zeroAccessSyncState.js +65 -0
- package/dist/apps/shared/zeroAccess/ProtectedMark.d.ts +7 -0
- package/dist/apps/shared/zeroAccess/ProtectedMark.js +10 -0
- package/dist/apps/shared/zeroAccess/useKeysLocked.d.ts +6 -0
- package/dist/apps/shared/zeroAccess/useKeysLocked.js +20 -0
- package/dist/apps/shared/zeroAccess/useOpenedRecord.d.ts +18 -0
- package/dist/apps/shared/zeroAccess/useOpenedRecord.js +66 -0
- package/dist/apps/www/_shell.d.ts +1 -1
- package/dist/apps/www/_shell.js +2 -2
- package/dist/apps/www/calendar/index.js +47 -5
- package/dist/apps/www/contacts/index.js +64 -11
- package/dist/apps/www/index.js +10 -1
- package/dist/apps/www/settings/encryption/index.js +14 -4
- package/dist/apps/www/settings/privacy/index.js +46 -5
- package/dist/apps/www/tasks/index.js +21 -7
- package/dist/lib/admin/domainsApi.d.ts +1 -1
- package/dist/lib/calendar/calendarApi.d.ts +46 -6
- package/dist/lib/calendar/calendarApi.js +110 -17
- package/dist/lib/calendar/calendarMutations.d.ts +20 -7
- package/dist/lib/calendar/calendarMutations.js +73 -14
- package/dist/lib/calendar/inviteApi.d.ts +17 -3
- package/dist/lib/calendar/inviteApi.js +61 -4
- package/dist/lib/calendar/protectedEvents.d.ts +31 -0
- package/dist/lib/calendar/protectedEvents.js +113 -0
- package/dist/lib/calendar/recurrence.d.ts +2 -0
- package/dist/lib/calendar/recurrence.js +16 -0
- package/dist/lib/calendar/sealedInvites.d.ts +6 -8
- package/dist/lib/calendar/sealedInvites.js +43 -6
- package/dist/lib/contacts/contactsApi.d.ts +29 -3
- package/dist/lib/contacts/contactsApi.js +23 -7
- package/dist/lib/contacts/useContactPhotoSrc.d.ts +5 -2
- package/dist/lib/contacts/useContactPhotoSrc.js +91 -11
- package/dist/lib/crypto/composeSecurity.d.ts +11 -3
- package/dist/lib/crypto/composeSecurity.js +13 -5
- package/dist/lib/crypto/keyExport.js +2 -2
- package/dist/lib/crypto/keyPairMatch.d.ts +9 -0
- package/dist/lib/crypto/keyPairMatch.js +65 -0
- package/dist/lib/crypto/keySession.d.ts +19 -2
- package/dist/lib/crypto/keySession.js +89 -13
- package/dist/lib/crypto/keyvaultApi.d.ts +41 -5
- package/dist/lib/crypto/keyvaultApi.js +13 -3
- package/dist/lib/crypto/meetingKeys.d.ts +25 -0
- package/dist/lib/crypto/meetingKeys.js +87 -0
- package/dist/lib/crypto/messageSecurity.d.ts +26 -2
- package/dist/lib/crypto/messageSecurity.js +122 -10
- package/dist/lib/crypto/openPgpKey.d.ts +39 -0
- package/dist/lib/crypto/openPgpKey.js +89 -0
- package/dist/lib/crypto/openpgp.d.ts +47 -0
- package/dist/lib/crypto/openpgp.js +88 -0
- package/dist/lib/crypto/pgpMessage.d.ts +51 -0
- package/dist/lib/crypto/pgpMessage.js +271 -0
- package/dist/lib/crypto/sharedMailboxKeys.d.ts +15 -3
- package/dist/lib/crypto/sharedMailboxKeys.js +102 -29
- package/dist/lib/crypto/smimeMessage.d.ts +21 -0
- package/dist/lib/crypto/smimeMessage.js +11 -7
- package/dist/lib/crypto/zeroAccessKey.js +5 -0
- package/dist/lib/mail/mailApi.d.ts +33 -0
- package/dist/lib/mail/mailboxImportApi.d.ts +10 -0
- package/dist/lib/mail/mailboxImportApi.js +15 -0
- package/dist/lib/search/zeroAccessIndexApi.d.ts +4 -0
- package/dist/lib/tasks/tasksApi.d.ts +11 -1
- package/dist/lib/tasks/tasksApi.js +10 -4
- package/dist/lib/util/clientPlatform.d.ts +14 -0
- package/dist/lib/util/clientPlatform.js +26 -0
- package/dist/lib/videoconf/protectedMeetings.d.ts +74 -0
- package/dist/lib/videoconf/protectedMeetings.js +181 -0
- package/dist/lib/videoconf/videoMeetingsApi.d.ts +25 -4
- package/dist/lib/videoconf/videoMeetingsApi.js +1 -1
- package/dist/lib/zeroAccess/sealedMoves.d.ts +41 -0
- package/dist/lib/zeroAccess/sealedMoves.js +63 -0
- package/dist/lib/zeroAccess/sealedRecords.d.ts +29 -0
- package/dist/lib/zeroAccess/sealedRecords.js +136 -0
- package/lib/admin/domainsApi.ts +1 -1
- package/lib/calendar/calendarApi.ts +140 -17
- package/lib/calendar/calendarMutations.ts +79 -12
- package/lib/calendar/inviteApi.ts +75 -5
- package/lib/calendar/protectedEvents.ts +143 -0
- package/lib/calendar/recurrence.ts +17 -0
- package/lib/calendar/sealedInvites.ts +47 -7
- package/lib/contacts/contactsApi.ts +43 -17
- package/lib/contacts/useContactPhotoSrc.ts +103 -13
- package/lib/crypto/composeSecurity.ts +20 -5
- package/lib/crypto/keyExport.ts +2 -2
- package/lib/crypto/keyPairMatch.ts +69 -0
- package/lib/crypto/keySession.ts +102 -12
- package/lib/crypto/keyvaultApi.ts +49 -7
- package/lib/crypto/meetingKeys.ts +118 -0
- package/lib/crypto/messageSecurity.ts +174 -13
- package/lib/crypto/openPgpKey.ts +118 -0
- package/lib/crypto/openpgp.ts +115 -0
- package/lib/crypto/pgpMessage.ts +336 -0
- package/lib/crypto/sharedMailboxKeys.ts +106 -28
- package/lib/crypto/smimeMessage.ts +11 -7
- package/lib/crypto/zeroAccessKey.ts +5 -0
- package/lib/mail/mailApi.ts +27 -0
- package/lib/mail/mailboxImportApi.ts +24 -0
- package/lib/search/zeroAccessIndexApi.ts +4 -0
- package/lib/tasks/tasksApi.ts +19 -5
- package/lib/util/clientPlatform.ts +50 -0
- package/lib/videoconf/protectedMeetings.ts +221 -0
- package/lib/videoconf/videoMeetingsApi.ts +15 -4
- package/lib/zeroAccess/sealedMoves.ts +79 -0
- package/lib/zeroAccess/sealedRecords.ts +153 -0
- package/package.json +12 -3
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
* replacement. `searchTier2.ts` additionally races its own calls against a timeout, since a Worker that's
|
|
27
27
|
* merely stuck (not crashed) posts no `error` event at all.
|
|
28
28
|
*
|
|
29
|
+
* **A lock destroys the index**: `initLocalIndex()` subscribes once to the key session (`subscribeKeySession()`); when the keys of
|
|
30
|
+
* a mailbox whose index it opened are destroyed (an idle lock, a manual lock, a sign-out), that mailbox's build is cancelled and its
|
|
31
|
+
* index destroyed (spec section 11), whether or not a mail view (`LocalIndexLifecycle`) is still mounted to notice - after leaving Mail
|
|
32
|
+
* for Calendar, or in a host whose account switch unmounted the shell that opened it (`tauri-client`).
|
|
33
|
+
*
|
|
29
34
|
* **Swappable transport**: everything above is specific to this one implementation - the Worker/OPFS-backed
|
|
30
35
|
* default. Every function this module exports below (`initLocalIndex`, `searchLocal`, `destroyLocalIndex`, ...)
|
|
31
36
|
* is a thin delegator to whichever `LocalIndexTransport` (`localIndexTransport.ts`) is "current," which defaults
|
|
@@ -38,6 +43,7 @@
|
|
|
38
43
|
* plain exports, not part of the swappable interface - they name this one transport's own cross-tab/localStorage
|
|
39
44
|
* mechanics, which a different transport has no reason to share.
|
|
40
45
|
*/
|
|
46
|
+
import { subscribeKeySession } from "../../../lib/crypto/keySession.js";
|
|
41
47
|
import type { ParsedSearchQuery } from "../../../lib/search/queryGrammar.js";
|
|
42
48
|
import type {
|
|
43
49
|
Coverage,
|
|
@@ -48,6 +54,7 @@ import type {
|
|
|
48
54
|
IndexedVersionsParams,
|
|
49
55
|
InitParams,
|
|
50
56
|
LocalIndexRequest,
|
|
57
|
+
LocalIndexResetNotice,
|
|
51
58
|
LocalIndexResponse,
|
|
52
59
|
LocalSearchPage,
|
|
53
60
|
MoveEntityParams,
|
|
@@ -60,6 +67,7 @@ import type {
|
|
|
60
67
|
} from "./localIndexWorker.js";
|
|
61
68
|
import type { LocalIndexEntity } from "./localIndexSchema.js";
|
|
62
69
|
import { removeLocalIndexDirectories, removeLocalIndexDirectory } from "./localIndexStorage.js";
|
|
70
|
+
import { forgetAllZeroAccessSync, forgetZeroAccessSync, forgetZeroAccessSyncExcept } from "./zeroAccessSyncState.js";
|
|
63
71
|
import type { LocalIndexTransport, PruneLocalEntitiesOptions, SetLocalIndexBuildingCompletion } from "./localIndexTransport.js";
|
|
64
72
|
|
|
65
73
|
let worker: Worker | undefined;
|
|
@@ -94,7 +102,12 @@ function getWorker(): Worker {
|
|
|
94
102
|
// listener's own comment for why that guard matters.
|
|
95
103
|
const created = new Worker(new URL("./localIndexWorker.js", import.meta.url), { type: "module" });
|
|
96
104
|
worker = created;
|
|
97
|
-
created.addEventListener("message", (event: MessageEvent<LocalIndexResponse>) => {
|
|
105
|
+
created.addEventListener("message", (event: MessageEvent<LocalIndexResponse | LocalIndexResetNotice>) => {
|
|
106
|
+
if ("type" in event.data) {
|
|
107
|
+
// The Worker discarded a corrupted index: what sync passes remembered of it goes too, or a rebuilt one would skip it.
|
|
108
|
+
forgetZeroAccessSync(event.data.mailboxUid);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
98
111
|
const entry = pending.get(event.data.id);
|
|
99
112
|
if (!entry) {
|
|
100
113
|
return;
|
|
@@ -251,11 +264,17 @@ async function removeLocalEntityImpl(mailboxUid: string, entityUid: string): Pro
|
|
|
251
264
|
|
|
252
265
|
/** Re-points one indexed message at its new folder (archive, cancel-scheduled-send) so `folder:`-scoped
|
|
253
266
|
* local searches stay correct. Same no-op/never-rejects rules as `removeLocalEntityImpl()`. */
|
|
254
|
-
async function moveLocalEntityImpl(mailboxUid: string, entityUid: string, folderUid: string, entityVersion?: string): Promise<void> {
|
|
267
|
+
async function moveLocalEntityImpl(mailboxUid: string, entityUid: string, folderUid: string, entityVersion?: string, flags?: string): Promise<void> {
|
|
255
268
|
if (!initializedMailboxes.has(mailboxUid)) {
|
|
256
269
|
return;
|
|
257
270
|
}
|
|
258
|
-
await call("moveEntity", {
|
|
271
|
+
await call("moveEntity", {
|
|
272
|
+
mailboxUid,
|
|
273
|
+
entityUid,
|
|
274
|
+
folderUid,
|
|
275
|
+
...(entityVersion !== undefined ? { entityVersion } : {}),
|
|
276
|
+
...(flags !== undefined ? { flags } : {}),
|
|
277
|
+
} satisfies MoveEntityParams).catch(
|
|
259
278
|
() => undefined,
|
|
260
279
|
);
|
|
261
280
|
}
|
|
@@ -290,6 +309,9 @@ function setLocalIndexBuildingImpl(mailboxUid: string, building: boolean, comple
|
|
|
290
309
|
* when the index could not be removed, e.g. because another tab still has it open; the deletion is then
|
|
291
310
|
* retried on the next page load. */
|
|
292
311
|
async function destroyLocalIndexImpl(mailboxUid: string): Promise<boolean> {
|
|
312
|
+
// What zero-access sync passes remembered of it goes with it, here too, so a host wrapper destroying mailboxes one by one through this
|
|
313
|
+
// transport (`tauri-client`'s scoped sign-out) forgets theirs.
|
|
314
|
+
forgetZeroAccessSync(mailboxUid);
|
|
293
315
|
updatePendingDeletions((current) => current.add(mailboxUid));
|
|
294
316
|
try {
|
|
295
317
|
await call("destroy", { mailboxUid, generation: nextLocalIndexGenerationImpl() } satisfies DestroyParams);
|
|
@@ -317,6 +339,9 @@ export const DESTROY_ALL_TIMEOUT_MS = 3_000;
|
|
|
317
339
|
*/
|
|
318
340
|
async function destroyAllLocalIndexesImpl(timeoutMs: number): Promise<boolean> {
|
|
319
341
|
signedOut = true;
|
|
342
|
+
// Here, in the transport, as for prune: a host wrapper that destroys only some accounts' indexes (`tauri-client`, several accounts in
|
|
343
|
+
// one origin) keeps the other accounts' sync state too.
|
|
344
|
+
forgetAllZeroAccessSync();
|
|
320
345
|
writePendingDeletions([ALL_INDEXES]);
|
|
321
346
|
try {
|
|
322
347
|
getChannel()?.postMessage({ type: "sign-out" });
|
|
@@ -349,9 +374,13 @@ async function destroyAllLocalIndexesImpl(timeoutMs: number): Promise<boolean> {
|
|
|
349
374
|
}
|
|
350
375
|
|
|
351
376
|
/** Removes local indexes for mailboxes outside `accessibleMailboxUids` - e.g. left behind by a different
|
|
352
|
-
* user who signed in on this device without signing out
|
|
377
|
+
* user who signed in on this device without signing out - and what zero-access sync passes remembered of them, as on a destroy. Both
|
|
378
|
+
* here, in the transport, so a host's wrapper that keeps other indexes (`tauri-client`, several accounts in one origin) keeps their sync
|
|
379
|
+
* state too. Never rejects. */
|
|
353
380
|
async function pruneInaccessibleLocalIndexesImpl(accessibleMailboxUids: Iterable<string>): Promise<void> {
|
|
354
|
-
|
|
381
|
+
const accessible = new Set(accessibleMailboxUids);
|
|
382
|
+
forgetZeroAccessSyncExcept(accessible);
|
|
383
|
+
await removeLocalIndexDirectories(accessible).catch(() => undefined);
|
|
355
384
|
}
|
|
356
385
|
|
|
357
386
|
/**
|
|
@@ -396,8 +425,49 @@ export function nextLocalIndexGeneration(): number {
|
|
|
396
425
|
return currentTransport.nextGeneration();
|
|
397
426
|
}
|
|
398
427
|
|
|
399
|
-
|
|
400
|
-
|
|
428
|
+
/** Every mailbox whose index `initLocalIndex()` opened, through whichever transport, and that wasn't destroyed since. */
|
|
429
|
+
const openedMailboxes = new Set<string>();
|
|
430
|
+
/** How many times each mailbox's keys were destroyed this page load - an `initLocalIndex()` that a lock overtook destroys what it opened. */
|
|
431
|
+
const lockCounts = new Map<string, number>();
|
|
432
|
+
let watchingLocks = false;
|
|
433
|
+
|
|
434
|
+
/** Cancels `mailboxUid`'s build and destroys its index, as `LocalIndexLifecycle` does on a lock. Never rejects. */
|
|
435
|
+
function destroyOnLock(mailboxUid: string): void {
|
|
436
|
+
// Stop a build pass still running with the destroyed keys; the destroy also makes the Worker reject anything that pass still sends.
|
|
437
|
+
// Loaded lazily (it imports this module, and carries the message security code).
|
|
438
|
+
import("./localIndexBuilder.js")
|
|
439
|
+
.then(({ cancelLocalIndexBuild }) => cancelLocalIndexBuild(mailboxUid))
|
|
440
|
+
.catch(() => undefined);
|
|
441
|
+
void destroyLocalIndex(mailboxUid);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Subscribes, once, to key destruction: see this module's doc comment ("A lock destroys the index"). */
|
|
445
|
+
function watchKeyLocks(): void {
|
|
446
|
+
if (watchingLocks) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
watchingLocks = true;
|
|
450
|
+
subscribeKeySession(({ mailboxUid, state }) => {
|
|
451
|
+
if (state !== "locked") {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
lockCounts.set(mailboxUid, (lockCounts.get(mailboxUid) ?? 0) + 1);
|
|
455
|
+
if (openedMailboxes.has(mailboxUid)) {
|
|
456
|
+
destroyOnLock(mailboxUid);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export async function initLocalIndex(params: InitParams): Promise<void> {
|
|
462
|
+
watchKeyLocks();
|
|
463
|
+
const locks = lockCounts.get(params.mailboxUid) ?? 0;
|
|
464
|
+
await currentTransport.init(params);
|
|
465
|
+
if ((lockCounts.get(params.mailboxUid) ?? 0) !== locks) {
|
|
466
|
+
// Its keys were destroyed while it opened: it never counted as open, so the lock's own destroy didn't reach it.
|
|
467
|
+
destroyOnLock(params.mailboxUid);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
openedMailboxes.add(params.mailboxUid);
|
|
401
471
|
}
|
|
402
472
|
|
|
403
473
|
export function indexLocalEntities(mailboxUid: string, entities: LocalIndexEntity[], generation?: number): Promise<IndexEntitiesResult> {
|
|
@@ -411,8 +481,12 @@ export function removeLocalEntity(mailboxUid: string, entityUid: string): Promis
|
|
|
411
481
|
}
|
|
412
482
|
|
|
413
483
|
/** Re-points one indexed message at its new folder (archive, cancel-scheduled-send) so `folder:`-scoped
|
|
414
|
-
* local searches stay correct. Same no-op/never-rejects rules as `removeLocalEntity()`.
|
|
415
|
-
|
|
484
|
+
* local searches stay correct. Same no-op/never-rejects rules as `removeLocalEntity()`. `flags` (`,flag,flag,`) sets the row's flags
|
|
485
|
+
* too - a zero-access row's, which nothing else records. */
|
|
486
|
+
export function moveLocalEntity(mailboxUid: string, entityUid: string, folderUid: string, entityVersion?: string, flags?: string): Promise<void> {
|
|
487
|
+
if (flags !== undefined) {
|
|
488
|
+
return currentTransport.moveEntity(mailboxUid, entityUid, folderUid, entityVersion, flags);
|
|
489
|
+
}
|
|
416
490
|
return entityVersion !== undefined
|
|
417
491
|
? currentTransport.moveEntity(mailboxUid, entityUid, folderUid, entityVersion)
|
|
418
492
|
: currentTransport.moveEntity(mailboxUid, entityUid, folderUid);
|
|
@@ -452,19 +526,25 @@ export function setLocalIndexBuilding(mailboxUid: string, building: boolean, com
|
|
|
452
526
|
* resolves `false` when the index could not be removed - see the current transport's own doc comment for what
|
|
453
527
|
* that means for it (the Worker transport also logs why, and retries on the next page load). */
|
|
454
528
|
export function destroyLocalIndex(mailboxUid: string): Promise<boolean> {
|
|
529
|
+
openedMailboxes.delete(mailboxUid);
|
|
530
|
+
// What zero-access sync passes remembered importing into it goes with it, whatever the transport.
|
|
531
|
+
forgetZeroAccessSync(mailboxUid);
|
|
455
532
|
return currentTransport.destroy(mailboxUid);
|
|
456
533
|
}
|
|
457
534
|
|
|
458
535
|
/** Destroys **every** local index this transport knows of - not just the mailboxes this session opened - e.g.
|
|
459
536
|
* on sign-out. Never rejects. Resolves `true` only if everything was removed within `timeoutMs`. */
|
|
460
537
|
export function destroyAllLocalIndexes(timeoutMs = DESTROY_ALL_TIMEOUT_MS): Promise<boolean> {
|
|
538
|
+
// `openedMailboxes` is left as it is: a host transport may keep other accounts' indexes open (`tauri-client`), and a lock of one
|
|
539
|
+
// already destroyed finds nothing to close.
|
|
461
540
|
return currentTransport.destroyAll(timeoutMs);
|
|
462
541
|
}
|
|
463
542
|
|
|
464
543
|
/** Removes local indexes for mailboxes outside `accessibleMailboxUids` - e.g. left behind by a different
|
|
465
|
-
* user who signed in on this device without signing out.
|
|
544
|
+
* user who signed in on this device without signing out. The transport decides what goes, zero-access sync state included (the
|
|
545
|
+
* Worker transport forgets it with the index; a host wrapper that skips the prune keeps both). Never rejects. */
|
|
466
546
|
export function pruneInaccessibleLocalIndexes(accessibleMailboxUids: Iterable<string>): Promise<void> {
|
|
467
|
-
return currentTransport.pruneInaccessible(accessibleMailboxUids);
|
|
547
|
+
return currentTransport.pruneInaccessible(new Set(accessibleMailboxUids));
|
|
468
548
|
}
|
|
469
549
|
|
|
470
550
|
/** Retries every deletion an earlier session couldn't finish. Never rejects. */
|
|
@@ -114,9 +114,25 @@ export interface LocalIndexEntity {
|
|
|
114
114
|
entityVersion?: string;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/** The change marker a message's row stores (`LocalIndexEntity.entityVersion`): a new server `version` (flags, content) or a folder move
|
|
118
|
+
* both invalidate the indexed copy. */
|
|
119
|
+
export function messageRowVersion(message: { version: number; folderUid: string }): string {
|
|
120
|
+
return `${message.version}:${message.folderUid}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A message's set flags as a row stores them. Leading/trailing comma so `flags LIKE '%,x,%'` (`buildSearchPredicates()`) matches
|
|
124
|
+
* correctly even for the first/last flag in the list. */
|
|
125
|
+
export function messageRowFlags(message: { flags: object }): string {
|
|
126
|
+
const set = Object.entries(message.flags as Record<string, unknown>)
|
|
127
|
+
.filter(([, value]) => value)
|
|
128
|
+
.map(([key]) => key);
|
|
129
|
+
return `,${set.join(",")},`;
|
|
130
|
+
}
|
|
131
|
+
|
|
117
132
|
/** `entity_uid` upsert - `ON CONFLICT` (SQLite's UPSERT syntax) rather than a separate delete-then-insert,
|
|
118
133
|
* so re-indexing an already-present message (a flag changed, a folder move) updates it in place and the
|
|
119
|
-
* `entities_au` trigger keeps `entities_fts` in sync automatically.
|
|
134
|
+
* `entities_au` trigger keeps `entities_fts` in sync automatically. A row written without a folder, flags or version (a zero-access
|
|
135
|
+
* document, which carries none) keeps the ones recorded: re-importing it must not make it look unrecorded again. */
|
|
120
136
|
export const UPSERT_ENTITY_SQL = `
|
|
121
137
|
INSERT 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)
|
|
122
138
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -124,7 +140,7 @@ ON CONFLICT(entity_uid) DO UPDATE SET
|
|
|
124
140
|
folder_uid = COALESCE(excluded.folder_uid, entities.folder_uid), date_for_sort = excluded.date_for_sort, participants = excluded.participants,
|
|
125
141
|
flags = CASE WHEN excluded.flags = '' THEN entities.flags ELSE excluded.flags END, has_attachments = excluded.has_attachments, subject = excluded.subject,
|
|
126
142
|
body = excluded.body, attachment_text = excluded.attachment_text, byte_size = excluded.byte_size,
|
|
127
|
-
entity_version = excluded.entity_version
|
|
143
|
+
entity_version = COALESCE(excluded.entity_version, entities.entity_version)
|
|
128
144
|
`;
|
|
129
145
|
|
|
130
146
|
/** Bind values for `UPSERT_ENTITY_SQL`, in column order - kept alongside it so the two can never drift
|
|
@@ -59,8 +59,9 @@ export interface LocalIndexTransport {
|
|
|
59
59
|
/** Drops one indexed entity. */
|
|
60
60
|
removeEntity(mailboxUid: string, entityUid: string): Promise<void>;
|
|
61
61
|
/** Re-points one indexed entity at its new folder. */
|
|
62
|
-
/** `entityVersion`, when given, is recorded with the folder (a transport that can't record it leaves the row's version cleared)
|
|
63
|
-
|
|
62
|
+
/** `entityVersion`, when given, is recorded with the folder (a transport that can't record it leaves the row's version cleared), and
|
|
63
|
+
* `flags` replaces the row's flags (a transport that can't record them leaves them as they are). */
|
|
64
|
+
moveEntity(mailboxUid: string, entityUid: string, folderUid: string, entityVersion?: string, flags?: string): Promise<void>;
|
|
64
65
|
/** The indexed version marker (`Message.version` + folder) currently stored for each of the given entity uids. */
|
|
65
66
|
indexedVersions(mailboxUid: string, entityUids: string[]): Promise<Record<string, string>>;
|
|
66
67
|
/** Removes indexed rows outside `keepEntityUids` (and `since`/`options.folderUids`'s own scope). Returns how many rows were pruned. */
|
|
@@ -78,7 +79,9 @@ export interface LocalIndexTransport {
|
|
|
78
79
|
/** Destroys every index this transport knows of, e.g. on sign-out. Never rejects; resolves `true` only if
|
|
79
80
|
* everything was removed within `timeoutMs`. */
|
|
80
81
|
destroyAll(timeoutMs: number): Promise<boolean>;
|
|
81
|
-
/** Removes indexes for mailboxes outside `accessibleMailboxUids
|
|
82
|
+
/** Removes indexes for mailboxes outside `accessibleMailboxUids`, and what zero-access sync remembered of each one it removes
|
|
83
|
+
* (`forgetZeroAccessSyncExcept()`; `pruneInaccessibleLocalIndexes()` leaves that to the transport, so a host can keep both). Never
|
|
84
|
+
* rejects. */
|
|
82
85
|
pruneInaccessible(accessibleMailboxUids: Iterable<string>): Promise<void>;
|
|
83
86
|
/** Retries any deletion an earlier session couldn't finish. Never rejects. */
|
|
84
87
|
retryPendingDeletions(): Promise<void>;
|
|
@@ -72,6 +72,12 @@ export type LocalIndexResponse =
|
|
|
72
72
|
| { id: number; ok: true; result: unknown }
|
|
73
73
|
| { id: number; ok: false; error: string };
|
|
74
74
|
|
|
75
|
+
/** Posted unprompted when a corrupted index was discarded (`resetCorruptedConnection()`), so the main thread forgets what it remembered of it. */
|
|
76
|
+
export interface LocalIndexResetNotice {
|
|
77
|
+
type: "reset";
|
|
78
|
+
mailboxUid: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
75
81
|
/**
|
|
76
82
|
* Carried by every call a build pass makes (`init`, `setWindow`, `setBuilding`, `indexEntities`,
|
|
77
83
|
* `pruneEntities`). Generations come from one monotonic counter on the main thread
|
|
@@ -126,6 +132,8 @@ export interface MoveEntityParams {
|
|
|
126
132
|
folderUid: string;
|
|
127
133
|
/** Recorded as the row's `entity_version`; cleared (the row then counts as changed) when absent. */
|
|
128
134
|
entityVersion?: string;
|
|
135
|
+
/** The row's flags (`,flag,flag,`), set too when given - a zero-access row, whose imported document carries none. */
|
|
136
|
+
flags?: string;
|
|
129
137
|
}
|
|
130
138
|
|
|
131
139
|
export interface IndexedVersionsParams {
|
|
@@ -521,6 +529,7 @@ async function withCurrentConnection<T>(params: GenerationParams & { mailboxUid:
|
|
|
521
529
|
async function resetCorruptedConnection(mailboxUid: string, connection: OpenConnection): Promise<void> {
|
|
522
530
|
console.warn(`localIndexWorker: local search index for mailbox ${mailboxUid} is corrupted; discarding and rebuilding.`);
|
|
523
531
|
connections.delete(mailboxUid);
|
|
532
|
+
self.postMessage({ type: "reset", mailboxUid } satisfies LocalIndexResetNotice);
|
|
524
533
|
await closeRawConnection(connection).catch(() => undefined);
|
|
525
534
|
try {
|
|
526
535
|
await removeLocalIndexDirectory(mailboxUid);
|
|
@@ -637,31 +646,39 @@ async function removeEntity(connection: OpenConnection, entityUid: string): Prom
|
|
|
637
646
|
}
|
|
638
647
|
|
|
639
648
|
/** Re-points an indexed message at a new folder in place (archive, cancel-scheduled-send). Clears
|
|
640
|
-
* `entity_version` so the next build pass re-validates the row against the server. */
|
|
641
|
-
async function moveEntity(connection: OpenConnection, entityUid: string, folderUid: string, entityVersion?: string): Promise<void> {
|
|
642
|
-
|
|
643
|
-
|
|
649
|
+
* `entity_version` so the next build pass re-validates the row against the server. Sets its flags too when `flags` is given. */
|
|
650
|
+
async function moveEntity(connection: OpenConnection, entityUid: string, folderUid: string, entityVersion?: string, flags?: string): Promise<void> {
|
|
651
|
+
const [sql, values] =
|
|
652
|
+
flags === undefined
|
|
653
|
+
? ["UPDATE entities SET folder_uid = ?, entity_version = ? WHERE entity_uid = ?", [folderUid, entityVersion ?? null, entityUid]]
|
|
654
|
+
: ["UPDATE entities SET folder_uid = ?, entity_version = ?, flags = ? WHERE entity_uid = ?", [folderUid, entityVersion ?? null, flags, entityUid]];
|
|
655
|
+
for await (const stmt of connection.sqlite3.statements(connection.db, sql)) {
|
|
656
|
+
connection.sqlite3.bind_collection(stmt, values);
|
|
644
657
|
await connection.sqlite3.step(stmt);
|
|
645
658
|
}
|
|
646
659
|
}
|
|
647
660
|
|
|
661
|
+
/** How many uids one `indexedVersions()` statement binds. */
|
|
662
|
+
export const INDEXED_VERSIONS_BATCH = 500;
|
|
663
|
+
|
|
648
664
|
/** `entity_version` for whichever of `entityUids` are already indexed - one batched query per builder page,
|
|
649
665
|
* so a rebuild can skip re-fetching/decrypting unchanged messages. */
|
|
650
666
|
async function indexedVersions(connection: OpenConnection, entityUids: string[]): Promise<Record<string, string>> {
|
|
651
667
|
const versions: Record<string, string> = {};
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
668
|
+
// In statements of at most `INDEXED_VERSIONS_BATCH` uids: SQLite refuses one binding more than its variable limit (32766 here).
|
|
669
|
+
for (let i = 0; i < entityUids.length; i += INDEXED_VERSIONS_BATCH) {
|
|
670
|
+
const batch: string[] = entityUids.slice(i, i + INDEXED_VERSIONS_BATCH);
|
|
671
|
+
const placeholders = batch.map(() => "?").join(", ");
|
|
672
|
+
for await (const stmt of connection.sqlite3.statements(
|
|
673
|
+
connection.db,
|
|
674
|
+
`SELECT entity_uid, entity_version FROM entities WHERE entity_uid IN (${placeholders})`,
|
|
675
|
+
)) {
|
|
676
|
+
connection.sqlite3.bind_collection(stmt, batch);
|
|
677
|
+
while ((await connection.sqlite3.step(stmt)) === SQLite.SQLITE_ROW) {
|
|
678
|
+
const version = connection.sqlite3.column(stmt, 1) as string | null;
|
|
679
|
+
if (version !== null) {
|
|
680
|
+
versions[connection.sqlite3.column(stmt, 0) as string] = version;
|
|
681
|
+
}
|
|
665
682
|
}
|
|
666
683
|
}
|
|
667
684
|
}
|
|
@@ -873,7 +890,7 @@ export async function handleRequest(method: LocalIndexRequest["method"], params:
|
|
|
873
890
|
}
|
|
874
891
|
case "moveEntity": {
|
|
875
892
|
const p = params as MoveEntityParams;
|
|
876
|
-
return runExclusive(p.mailboxUid, () => withConnection(p.mailboxUid, (c) => moveEntity(c, p.entityUid, p.folderUid, p.entityVersion)));
|
|
893
|
+
return runExclusive(p.mailboxUid, () => withConnection(p.mailboxUid, (c) => moveEntity(c, p.entityUid, p.folderUid, p.entityVersion, p.flags)));
|
|
877
894
|
}
|
|
878
895
|
case "indexedVersions": {
|
|
879
896
|
const p = params as IndexedVersionsParams;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* search* fail just because this one, most novel piece had a bad moment.
|
|
19
19
|
*/
|
|
20
20
|
import type { UnlockedKeys } from "../../../lib/crypto/keySession.js";
|
|
21
|
+
import { KeysLockedError } from "../../../lib/crypto/masterKey.js";
|
|
21
22
|
import type { ParsedSearchQuery } from "../../../lib/search/queryGrammar.js";
|
|
22
23
|
import type { SearchResult } from "../../../lib/search/searchApi.js";
|
|
23
24
|
import { deriveLocalIndexKey } from "./localIndexKey.js";
|
|
@@ -41,6 +42,10 @@ export interface Tier2SearchOutcome {
|
|
|
41
42
|
* wins, and the other is a cheap no-op. */
|
|
42
43
|
async function ensureInitialized(mailboxUid: string, unlocked: UnlockedKeys): Promise<void> {
|
|
43
44
|
const indexKey = await deriveLocalIndexKey(unlocked.masterKey, mailboxUid);
|
|
45
|
+
// Locked during the derive, which still succeeds: open nothing with the destroyed keys (the lock's own destroy found nothing open).
|
|
46
|
+
if (unlocked.destroyed) {
|
|
47
|
+
throw new KeysLockedError();
|
|
48
|
+
}
|
|
44
49
|
await initLocalIndex({ mailboxUid, indexKey });
|
|
45
50
|
}
|
|
46
51
|
|