@remit/backend 0.0.143 → 0.0.145
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/dist/dev-server/content-handler.d.ts +38 -0
- package/dist/dev-server/content-handler.js +46 -0
- package/dist/src/api.d.ts +5 -0
- package/dist/src/api.js +73 -0
- package/dist/src/auth.d.ts +24 -0
- package/dist/src/auth.js +81 -0
- package/dist/src/config/api-config.d.ts +20 -0
- package/dist/src/config/api-config.js +1 -0
- package/dist/src/config/from-env.d.ts +2 -0
- package/dist/src/config/from-env.js +65 -0
- package/dist/src/config/msoauth-secrets-manager.d.ts +2 -0
- package/dist/src/config/msoauth-secrets-manager.js +28 -0
- package/dist/src/config/msoauth.d.ts +19 -0
- package/dist/src/config/msoauth.js +6 -0
- package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
- package/dist/src/derive/autoMoved.js +49 -0
- package/dist/src/derive/contentSignature.d.ts +40 -0
- package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +23 -77
- package/dist/src/derive/contentUrl.d.ts +33 -0
- package/dist/src/derive/contentUrl.js +27 -0
- package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
- package/dist/src/derive/deriveMuted.js +7 -0
- package/dist/src/derive/enrichThreadRows.d.ts +118 -0
- package/dist/src/derive/enrichThreadRows.js +151 -0
- package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
- package/dist/src/derive/filterThreadCriteria.js +24 -0
- package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
- package/dist/src/derive/pendingMoveCounts.js +53 -0
- package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
- package/dist/src/derive/senderTrust.js +19 -0
- package/dist/src/error.d.ts +21 -0
- package/dist/src/error.js +85 -0
- package/dist/src/handlers/account-guards.d.ts +36 -0
- package/dist/src/handlers/account-guards.js +107 -0
- package/dist/src/handlers/account-oauth.d.ts +15 -0
- package/dist/src/handlers/account-oauth.js +334 -0
- package/dist/src/handlers/account-overrides.d.ts +53 -0
- package/dist/src/handlers/account-overrides.js +233 -0
- package/dist/src/handlers/account-signature.d.ts +28 -0
- package/dist/src/handlers/account-signature.js +85 -0
- package/dist/src/handlers/account.d.ts +7 -0
- package/dist/src/handlers/account.js +337 -0
- package/dist/src/handlers/address.d.ts +35 -0
- package/dist/src/handlers/address.js +133 -0
- package/dist/src/handlers/calendar-event.d.ts +70 -0
- package/dist/src/handlers/calendar-event.js +417 -0
- package/dist/src/handlers/calendar-feed.d.ts +47 -0
- package/dist/src/handlers/calendar-feed.js +184 -0
- package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
- package/dist/src/handlers/calendar-suggestion.js +204 -0
- package/dist/src/handlers/calendar.d.ts +116 -0
- package/dist/src/handlers/calendar.js +323 -0
- package/dist/src/handlers/config.d.ts +43 -0
- package/dist/src/handlers/config.js +164 -0
- package/dist/src/handlers/ensure-account-config.d.ts +9 -0
- package/dist/src/handlers/ensure-account-config.js +27 -0
- package/dist/src/handlers/filter.d.ts +3 -0
- package/dist/src/handlers/filter.js +88 -0
- package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
- package/dist/src/handlers/folder-role-appointments.js +168 -0
- package/dist/src/handlers/folder-role-labels.d.ts +26 -0
- package/dist/src/handlers/folder-role-labels.js +40 -0
- package/dist/src/handlers/folder-role.d.ts +9 -0
- package/dist/src/handlers/folder-role.js +37 -0
- package/dist/src/handlers/index.d.ts +88 -0
- package/dist/src/handlers/index.js +62 -0
- package/dist/src/handlers/label.d.ts +43 -0
- package/dist/src/handlers/label.js +107 -0
- package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
- package/dist/src/handlers/mailbox-bindings.js +61 -0
- package/dist/src/handlers/mailbox.d.ts +23 -0
- package/dist/src/handlers/mailbox.js +211 -0
- package/dist/src/handlers/me.d.ts +2 -0
- package/dist/src/handlers/me.js +117 -0
- package/dist/src/handlers/message.d.ts +82 -0
- package/dist/src/handlers/message.js +595 -0
- package/dist/src/handlers/organize.d.ts +10 -0
- package/dist/src/handlers/organize.js +103 -0
- package/dist/src/handlers/outbox-attachment.d.ts +25 -0
- package/dist/src/handlers/outbox-attachment.js +77 -0
- package/dist/src/handlers/outbox.d.ts +4 -0
- package/dist/src/handlers/outbox.js +136 -0
- package/dist/src/handlers/sync-progress.d.ts +46 -0
- package/dist/src/handlers/sync-progress.js +63 -0
- package/dist/src/handlers/sync.d.ts +28 -0
- package/dist/src/handlers/sync.js +73 -0
- package/dist/src/handlers/system-update.d.ts +2 -0
- package/dist/src/handlers/system-update.js +159 -0
- package/dist/src/handlers/thread.d.ts +148 -0
- package/dist/src/handlers/thread.js +228 -0
- package/dist/src/handlers/unified-threads.d.ts +247 -0
- package/dist/src/handlers/unified-threads.js +406 -0
- package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
- package/dist/src/handlers/vip-suggestions.js +16 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +53 -0
- package/{src/json.ts → dist/src/json.d.ts} +1 -4
- package/dist/src/json.js +10 -0
- package/dist/src/jwt-auth.d.ts +15 -0
- package/dist/src/jwt-auth.js +45 -0
- package/dist/src/lambda.d.ts +13 -0
- package/dist/src/lambda.js +33 -0
- package/dist/src/request.d.ts +9 -0
- package/dist/src/request.js +17 -0
- package/dist/src/response.d.ts +44 -0
- package/dist/src/response.js +80 -0
- package/dist/src/serve.d.ts +31 -0
- package/dist/src/serve.js +117 -0
- package/dist/src/service/account-deletion.d.ts +20 -0
- package/dist/src/service/account-deletion.js +33 -0
- package/dist/src/service/compose-sqlite.d.ts +3 -0
- package/dist/src/service/compose-sqlite.js +13 -0
- package/dist/src/service/create-remit-client.d.ts +109 -0
- package/dist/src/service/create-remit-client.js +188 -0
- package/dist/src/service/data-client.d.ts +12 -0
- package/dist/src/service/data-client.js +42 -0
- package/dist/src/service/export-identity.d.ts +4 -0
- package/dist/src/service/export-identity.js +33 -0
- package/dist/src/service/fire-and-forget.d.ts +54 -0
- package/dist/src/service/fire-and-forget.js +37 -0
- package/dist/src/service/organize.d.ts +181 -0
- package/dist/src/service/organize.js +367 -0
- package/dist/src/service/queues.d.ts +12 -0
- package/dist/src/service/queues.js +1 -0
- package/dist/src/service/refusal.d.ts +4 -0
- package/dist/src/service/refusal.js +20 -0
- package/dist/src/service/sender-category.d.ts +51 -0
- package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
- package/dist/src/service/shared-deps.d.ts +6 -0
- package/dist/src/service/shared-deps.js +24 -0
- package/dist/src/service/trigger-sync.d.ts +57 -0
- package/dist/src/service/trigger-sync.js +31 -0
- package/dist/src/types.d.ts +72 -0
- package/dist/src/types.js +1 -0
- package/package.json +125 -98
- package/.env.test +0 -4
- package/dev-server/content-auth.test.ts +0 -113
- package/dev-server/content-auth.ts +0 -54
- package/dev-server/content-handler.test.ts +0 -105
- package/dev-server/content-handler.ts +0 -79
- package/dev-server/content-path.test.ts +0 -37
- package/dev-server/content-path.ts +0 -27
- package/dev-server/cors.test.ts +0 -48
- package/dev-server/cors.ts +0 -25
- package/dev-server/keep-alive.test.ts +0 -24
- package/dev-server/lambda-helpers.ts +0 -69
- package/dev-server/log-output.test.ts +0 -173
- package/dev-server/oauth-callback-route.test.ts +0 -66
- package/dev-server/relational-health.test.ts +0 -83
- package/dev-server/relational-health.ts +0 -39
- package/dev-server/server.ts +0 -384
- package/dev-server/sync-age.test.ts +0 -91
- package/dev-server/sync-age.ts +0 -55
- package/dev-server/upload-handler.ts +0 -132
- package/dev-server/upload-route.test.ts +0 -196
- package/scripts/backfill-classification.ts +0 -91
- package/scripts/backfill-list-id.ts +0 -83
- package/scripts/config-save.ts +0 -128
- package/src/auth.test.ts +0 -12
- package/src/auth.ts +0 -92
- package/src/config/msoauth.test.ts +0 -48
- package/src/config/msoauth.ts +0 -48
- package/src/data-backend.test.ts +0 -23
- package/src/data-backend.ts +0 -14
- package/src/derive/autoMoved.test.ts +0 -104
- package/src/derive/contentSignature.test.ts +0 -161
- package/src/derive/contentUrl.test.ts +0 -172
- package/src/derive/contentUrl.ts +0 -86
- package/src/derive/deriveMuted.test.ts +0 -34
- package/src/derive/enrichThreadRows.test.ts +0 -360
- package/src/derive/enrichThreadRows.ts +0 -234
- package/src/derive/filterThreadCriteria.test.ts +0 -126
- package/src/derive/filterThreadCriteria.ts +0 -51
- package/src/derive/senderTrust.test.ts +0 -67
- package/src/error.test.ts +0 -201
- package/src/error.ts +0 -123
- package/src/handlers/account-guards.test.ts +0 -67
- package/src/handlers/account-guards.ts +0 -169
- package/src/handlers/account-oauth-callback.test.ts +0 -155
- package/src/handlers/account-oauth-consent.test.ts +0 -336
- package/src/handlers/account-oauth-start.test.ts +0 -191
- package/src/handlers/account-oauth.test.ts +0 -439
- package/src/handlers/account-oauth.ts +0 -635
- package/src/handlers/account-overrides.test.ts +0 -88
- package/src/handlers/account-overrides.ts +0 -323
- package/src/handlers/account-ownership.ts +0 -25
- package/src/handlers/account-signature.ts +0 -117
- package/src/handlers/account-synced-services.test.ts +0 -245
- package/src/handlers/account.ts +0 -552
- package/src/handlers/address.test.ts +0 -381
- package/src/handlers/address.ts +0 -200
- package/src/handlers/calendar-event.test.ts +0 -685
- package/src/handlers/calendar-event.ts +0 -738
- package/src/handlers/calendar-feed-validators.test.ts +0 -228
- package/src/handlers/calendar-feed.test.ts +0 -439
- package/src/handlers/calendar-feed.ts +0 -291
- package/src/handlers/calendar-sqlite-fixture.ts +0 -36
- package/src/handlers/calendar-suggestion.test.ts +0 -1269
- package/src/handlers/calendar-suggestion.ts +0 -425
- package/src/handlers/calendar.test.ts +0 -1080
- package/src/handlers/calendar.ts +0 -517
- package/src/handlers/config.test.ts +0 -245
- package/src/handlers/config.ts +0 -332
- package/src/handlers/ensure-account-config.ts +0 -30
- package/src/handlers/filter.test.ts +0 -644
- package/src/handlers/filter.ts +0 -518
- package/src/handlers/folder-role-appointments.test.ts +0 -438
- package/src/handlers/folder-role-appointments.ts +0 -249
- package/src/handlers/folder-role-labels.ts +0 -63
- package/src/handlers/folder-role.test.ts +0 -209
- package/src/handlers/folder-role.ts +0 -80
- package/src/handlers/index.ts +0 -83
- package/src/handlers/label.test.ts +0 -112
- package/src/handlers/label.ts +0 -210
- package/src/handlers/mailbox-bindings.ts +0 -111
- package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
- package/src/handlers/mailbox-settled-target.test.ts +0 -251
- package/src/handlers/mailbox.test.ts +0 -280
- package/src/handlers/mailbox.ts +0 -581
- package/src/handlers/me.test.ts +0 -81
- package/src/handlers/me.ts +0 -203
- package/src/handlers/message-read-backfill.test.ts +0 -103
- package/src/handlers/message.test.ts +0 -131
- package/src/handlers/message.ts +0 -1131
- package/src/handlers/organize.test.ts +0 -277
- package/src/handlers/organize.ts +0 -197
- package/src/handlers/outbox-attachment.test.ts +0 -765
- package/src/handlers/outbox-attachment.ts +0 -126
- package/src/handlers/outbox-list.test.ts +0 -179
- package/src/handlers/outbox.test.ts +0 -772
- package/src/handlers/outbox.ts +0 -258
- package/src/handlers/search.test.ts +0 -161
- package/src/handlers/search.ts +0 -105
- package/src/handlers/sync-progress.test.ts +0 -205
- package/src/handlers/sync-progress.ts +0 -99
- package/src/handlers/sync.test.ts +0 -168
- package/src/handlers/sync.ts +0 -118
- package/src/handlers/system-update.test.ts +0 -497
- package/src/handlers/system-update.ts +0 -230
- package/src/handlers/thread.test.ts +0 -396
- package/src/handlers/thread.ts +0 -427
- package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
- package/src/handlers/unified-threads.test.ts +0 -785
- package/src/handlers/unified-threads.ts +0 -669
- package/src/index.test.ts +0 -83
- package/src/index.ts +0 -228
- package/src/jwt-auth.test.ts +0 -152
- package/src/jwt-auth.ts +0 -132
- package/src/log-redaction.test.ts +0 -144
- package/src/openapi-spec.test.ts +0 -118
- package/src/request-context.test.ts +0 -61
- package/src/request-context.ts +0 -33
- package/src/request-pipeline.test.ts +0 -88
- package/src/request.ts +0 -10
- package/src/response.test.ts +0 -130
- package/src/response.ts +0 -144
- package/src/service/compose-sqlite.ts +0 -98
- package/src/service/config-import.ts +0 -39
- package/src/service/create-remit-client.ts +0 -522
- package/src/service/data-client.test.ts +0 -97
- package/src/service/data-client.ts +0 -58
- package/src/service/export-identity.ts +0 -39
- package/src/service/filter.ts +0 -40
- package/src/service/fire-and-forget.test.ts +0 -126
- package/src/service/fire-and-forget.ts +0 -79
- package/src/service/localhost-env-config.test.ts +0 -36
- package/src/service/organize-queue.ts +0 -98
- package/src/service/organize.test.ts +0 -1451
- package/src/service/organize.ts +0 -1025
- package/src/service/read-path-filter-config.test.ts +0 -243
- package/src/service/semantic-capability.test.ts +0 -78
- package/src/service/semantic-capability.ts +0 -95
- package/src/service/sender-category.test.ts +0 -235
- package/src/service/sqs.ts +0 -4
- package/src/service/trigger-sync.test.ts +0 -205
- package/src/service/trigger-sync.ts +0 -100
- package/src/stdout-capture-fixture.ts +0 -40
- package/src/types.ts +0 -271
- package/tsconfig.json +0 -7
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for sync-status aggregation math and phase derivation.
|
|
3
|
-
*
|
|
4
|
-
* Imports the pure helpers from sync-progress.ts directly so the tests pin
|
|
5
|
-
* the shipped implementation (no DynamoDB or AWS infrastructure required).
|
|
6
|
-
*/
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { describe, it } from "node:test";
|
|
9
|
-
import {
|
|
10
|
-
computeMessagesSynced,
|
|
11
|
-
deriveMailboxPhase,
|
|
12
|
-
type MailboxProgressFields,
|
|
13
|
-
type MailboxSyncFields,
|
|
14
|
-
toMailboxSyncProgress,
|
|
15
|
-
} from "./sync-progress.js";
|
|
16
|
-
|
|
17
|
-
// ─── deriveMailboxPhase tests ───────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
describe("deriveMailboxPhase", () => {
|
|
20
|
-
it("returns pending when lastMessageSyncAt is 0", () => {
|
|
21
|
-
const mailbox: MailboxSyncFields = {
|
|
22
|
-
lastMessageSyncAt: 0,
|
|
23
|
-
messageCount: 100,
|
|
24
|
-
lastSyncUid: 0,
|
|
25
|
-
highWaterMarkUid: 0,
|
|
26
|
-
};
|
|
27
|
-
assert.equal(deriveMailboxPhase(mailbox), "pending");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("returns pending even when watermarks are set but lastMessageSyncAt is 0", () => {
|
|
31
|
-
const mailbox: MailboxSyncFields = {
|
|
32
|
-
lastMessageSyncAt: 0,
|
|
33
|
-
messageCount: 50,
|
|
34
|
-
lastSyncUid: 50,
|
|
35
|
-
highWaterMarkUid: 100,
|
|
36
|
-
};
|
|
37
|
-
assert.equal(deriveMailboxPhase(mailbox), "pending");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("returns complete when the worker stamped initialSyncCompletedAt after the last batch", () => {
|
|
41
|
-
const now = Date.now();
|
|
42
|
-
const mailbox: MailboxSyncFields = {
|
|
43
|
-
lastMessageSyncAt: now - 1000,
|
|
44
|
-
initialSyncCompletedAt: now,
|
|
45
|
-
messageCount: 200,
|
|
46
|
-
// Backfill settles at the smallest real UID (sparse UIDs) — the
|
|
47
|
-
// marker, not the watermark, decides completion.
|
|
48
|
-
lastSyncUid: 42,
|
|
49
|
-
highWaterMarkUid: 200,
|
|
50
|
-
};
|
|
51
|
-
assert.equal(deriveMailboxPhase(mailbox), "complete");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("returns complete when marker equals lastMessageSyncAt (same-millisecond writes)", () => {
|
|
55
|
-
const now = Date.now();
|
|
56
|
-
const mailbox: MailboxSyncFields = {
|
|
57
|
-
lastMessageSyncAt: now,
|
|
58
|
-
initialSyncCompletedAt: now,
|
|
59
|
-
messageCount: 10,
|
|
60
|
-
lastSyncUid: 5,
|
|
61
|
-
highWaterMarkUid: 20,
|
|
62
|
-
};
|
|
63
|
-
assert.equal(deriveMailboxPhase(mailbox), "complete");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("returns complete for empty mailbox (messageCount 0) without a marker", () => {
|
|
67
|
-
const mailbox: MailboxSyncFields = {
|
|
68
|
-
lastMessageSyncAt: Date.now(),
|
|
69
|
-
messageCount: 0,
|
|
70
|
-
lastSyncUid: 0,
|
|
71
|
-
highWaterMarkUid: 0,
|
|
72
|
-
};
|
|
73
|
-
assert.equal(deriveMailboxPhase(mailbox), "complete");
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("returns syncing when a batch was written after the completion marker", () => {
|
|
77
|
-
const now = Date.now();
|
|
78
|
-
const mailbox: MailboxSyncFields = {
|
|
79
|
-
lastMessageSyncAt: now,
|
|
80
|
-
initialSyncCompletedAt: now - 60_000,
|
|
81
|
-
messageCount: 500,
|
|
82
|
-
lastSyncUid: 150,
|
|
83
|
-
highWaterMarkUid: 500,
|
|
84
|
-
};
|
|
85
|
-
assert.equal(deriveMailboxPhase(mailbox), "syncing");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("returns syncing when backfill is in progress and no marker is set", () => {
|
|
89
|
-
const mailbox: MailboxSyncFields = {
|
|
90
|
-
lastMessageSyncAt: Date.now(),
|
|
91
|
-
messageCount: 500,
|
|
92
|
-
lastSyncUid: 150,
|
|
93
|
-
highWaterMarkUid: 500,
|
|
94
|
-
};
|
|
95
|
-
assert.equal(deriveMailboxPhase(mailbox), "syncing");
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// ─── computeMessagesSynced tests ───────────────────────────────────────────
|
|
100
|
-
|
|
101
|
-
describe("computeMessagesSynced", () => {
|
|
102
|
-
it("returns 0 when no sync has started (highWaterMarkUid = 0)", () => {
|
|
103
|
-
const mailbox: MailboxSyncFields = {
|
|
104
|
-
lastMessageSyncAt: 0,
|
|
105
|
-
messageCount: 100,
|
|
106
|
-
lastSyncUid: 0,
|
|
107
|
-
highWaterMarkUid: 0,
|
|
108
|
-
};
|
|
109
|
-
assert.equal(computeMessagesSynced(mailbox), 0);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("computes UID range approximation", () => {
|
|
113
|
-
// highWaterMarkUid - lastSyncUid + 1 = 200 - 150 + 1 = 51
|
|
114
|
-
const mailbox: MailboxSyncFields = {
|
|
115
|
-
lastMessageSyncAt: Date.now(),
|
|
116
|
-
messageCount: 200,
|
|
117
|
-
lastSyncUid: 150,
|
|
118
|
-
highWaterMarkUid: 200,
|
|
119
|
-
};
|
|
120
|
-
assert.equal(computeMessagesSynced(mailbox), 51);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("clamps result to messagesTotal", () => {
|
|
124
|
-
// UID range would be 200 - 1 + 1 = 200; total is 180 (sparse UIDs)
|
|
125
|
-
const mailbox: MailboxSyncFields = {
|
|
126
|
-
lastMessageSyncAt: Date.now(),
|
|
127
|
-
messageCount: 180,
|
|
128
|
-
lastSyncUid: 1,
|
|
129
|
-
highWaterMarkUid: 200,
|
|
130
|
-
};
|
|
131
|
-
assert.equal(
|
|
132
|
-
computeMessagesSynced(mailbox),
|
|
133
|
-
180,
|
|
134
|
-
"should clamp to messagesTotal",
|
|
135
|
-
);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("never returns negative value", () => {
|
|
139
|
-
const mailbox: MailboxSyncFields = {
|
|
140
|
-
lastMessageSyncAt: Date.now(),
|
|
141
|
-
messageCount: 0,
|
|
142
|
-
lastSyncUid: 100,
|
|
143
|
-
highWaterMarkUid: 50,
|
|
144
|
-
};
|
|
145
|
-
assert.ok(computeMessagesSynced(mailbox) >= 0, "should never be negative");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("returns messagesTotal when fully synced (backfill reached the oldest UID)", () => {
|
|
149
|
-
// Fully synced: lastSyncUid settled at the smallest real UID (42).
|
|
150
|
-
// Range 100 - 42 + 1 = 59, clamped to the 50 actual messages.
|
|
151
|
-
const mailbox: MailboxSyncFields = {
|
|
152
|
-
lastMessageSyncAt: Date.now(),
|
|
153
|
-
initialSyncCompletedAt: Date.now(),
|
|
154
|
-
messageCount: 50,
|
|
155
|
-
lastSyncUid: 42,
|
|
156
|
-
highWaterMarkUid: 100,
|
|
157
|
-
};
|
|
158
|
-
assert.equal(computeMessagesSynced(mailbox), 50);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
// ─── toMailboxSyncProgress tests ────────────────────────────────────────────
|
|
163
|
-
|
|
164
|
-
const mailboxRow = (
|
|
165
|
-
overrides: Partial<MailboxProgressFields> = {},
|
|
166
|
-
): MailboxProgressFields => ({
|
|
167
|
-
mailboxId: "mb-1",
|
|
168
|
-
fullPath: "INBOX",
|
|
169
|
-
lastMessageSyncAt: 1_700_000_000_000,
|
|
170
|
-
initialSyncCompletedAt: 1_700_000_000_001,
|
|
171
|
-
messageCount: 10,
|
|
172
|
-
lastSyncUid: 1,
|
|
173
|
-
highWaterMarkUid: 100,
|
|
174
|
-
...overrides,
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
describe("toMailboxSyncProgress", () => {
|
|
178
|
-
// Issue #771: the client cannot tell an arrival from a departure by count —
|
|
179
|
-
// one message in and one out nets to zero. The highest UID only ever moves
|
|
180
|
-
// up, so the projection has to carry it or the arrival stays invisible.
|
|
181
|
-
it("carries the mailbox's highest UID", () => {
|
|
182
|
-
assert.equal(
|
|
183
|
-
toMailboxSyncProgress(mailboxRow({ highWaterMarkUid: 4321 }))
|
|
184
|
-
.highWaterMarkUid,
|
|
185
|
-
4321,
|
|
186
|
-
);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("reports the sentinel 0 for a mailbox that has never taken a message", () => {
|
|
190
|
-
assert.equal(
|
|
191
|
-
toMailboxSyncProgress(mailboxRow({ highWaterMarkUid: 0 }))
|
|
192
|
-
.highWaterMarkUid,
|
|
193
|
-
0,
|
|
194
|
-
);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("projects identity, phase and totals alongside it", () => {
|
|
198
|
-
const progress = toMailboxSyncProgress(mailboxRow());
|
|
199
|
-
assert.equal(progress.mailboxId, "mb-1");
|
|
200
|
-
assert.equal(progress.fullPath, "INBOX");
|
|
201
|
-
assert.equal(progress.phase, "complete");
|
|
202
|
-
assert.equal(progress.messagesTotal, 10);
|
|
203
|
-
assert.equal(progress.lastSyncedAt, 1_700_000_000_000);
|
|
204
|
-
});
|
|
205
|
-
});
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import type { MailboxItem } from "@remit/data-ports";
|
|
2
|
-
|
|
3
|
-
/** The subset of mailbox fields needed to derive sync progress. */
|
|
4
|
-
export type MailboxSyncFields = Pick<
|
|
5
|
-
MailboxItem,
|
|
6
|
-
| "lastMessageSyncAt"
|
|
7
|
-
| "initialSyncCompletedAt"
|
|
8
|
-
| "messageCount"
|
|
9
|
-
| "lastSyncUid"
|
|
10
|
-
| "highWaterMarkUid"
|
|
11
|
-
>;
|
|
12
|
-
|
|
13
|
-
/** A mailbox row, as far as the sync-status projection reads it. */
|
|
14
|
-
export type MailboxProgressFields = MailboxSyncFields &
|
|
15
|
-
Pick<MailboxItem, "mailboxId" | "fullPath">;
|
|
16
|
-
|
|
17
|
-
/** The per-mailbox entry `getSyncStatus` returns. */
|
|
18
|
-
export interface MailboxSyncProgress {
|
|
19
|
-
mailboxId: string;
|
|
20
|
-
fullPath: string;
|
|
21
|
-
phase: string;
|
|
22
|
-
messagesTotal: number;
|
|
23
|
-
messagesSynced: number;
|
|
24
|
-
highWaterMarkUid: number;
|
|
25
|
-
lastSyncedAt: number | undefined;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Derive a per-mailbox sync phase from worker-written sync state.
|
|
30
|
-
*
|
|
31
|
-
* - pending: never synced (lastMessageSyncAt is 0 / unset)
|
|
32
|
-
* - complete: the imap-worker drained the mailbox and stamped
|
|
33
|
-
* initialSyncCompletedAt after the last batch write
|
|
34
|
-
* (initialSyncCompletedAt >= lastMessageSyncAt), or the
|
|
35
|
-
* mailbox is empty
|
|
36
|
-
* - syncing: sync has started but is not yet complete
|
|
37
|
-
*
|
|
38
|
-
* Note: UID watermarks alone cannot express completion — backfill settles
|
|
39
|
-
* at the mailbox's smallest real UID (UIDs are sparse), so the completion
|
|
40
|
-
* marker written by the worker is the source of truth.
|
|
41
|
-
*/
|
|
42
|
-
export const deriveMailboxPhase = (mailbox: MailboxSyncFields): string => {
|
|
43
|
-
if (!mailbox.lastMessageSyncAt) return "pending";
|
|
44
|
-
const messageCount = mailbox.messageCount ?? 0;
|
|
45
|
-
const initialSyncCompletedAt = mailbox.initialSyncCompletedAt ?? 0;
|
|
46
|
-
|
|
47
|
-
// Completion marker is fresh: no batch was written after the worker
|
|
48
|
-
// recorded the mailbox as drained.
|
|
49
|
-
if (
|
|
50
|
-
initialSyncCompletedAt > 0 &&
|
|
51
|
-
initialSyncCompletedAt >= mailbox.lastMessageSyncAt
|
|
52
|
-
) {
|
|
53
|
-
return "complete";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Also complete if messageCount is 0 (empty mailbox)
|
|
57
|
-
if (messageCount === 0) return "complete";
|
|
58
|
-
|
|
59
|
-
return "syncing";
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Compute approximate messagesSynced from UID watermarks.
|
|
64
|
-
*
|
|
65
|
-
* UIDs are sparse (not necessarily contiguous), so this is an approximation.
|
|
66
|
-
* We clamp the result to [0, messagesTotal].
|
|
67
|
-
*/
|
|
68
|
-
export const computeMessagesSynced = (mailbox: MailboxSyncFields): number => {
|
|
69
|
-
const messagesTotal = mailbox.messageCount ?? 0;
|
|
70
|
-
const lastSyncUid = mailbox.lastSyncUid ?? 0;
|
|
71
|
-
const highWaterMarkUid = mailbox.highWaterMarkUid ?? 0;
|
|
72
|
-
|
|
73
|
-
if (highWaterMarkUid === 0) return 0;
|
|
74
|
-
|
|
75
|
-
// UID range approximation: number of UIDs in the synced range
|
|
76
|
-
const synced = highWaterMarkUid - lastSyncUid + 1;
|
|
77
|
-
|
|
78
|
-
return Math.min(Math.max(synced, 0), messagesTotal);
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Project a mailbox row into its sync-status entry.
|
|
83
|
-
*
|
|
84
|
-
* `highWaterMarkUid` is carried through because a total cannot tell an arrival
|
|
85
|
-
* from a departure: a message landing in the same sync round that another one
|
|
86
|
-
* leaves nets to zero, and the client's freshness check saw nothing (#771). The
|
|
87
|
-
* highest UID only ever moves up, so it names the arrival on its own.
|
|
88
|
-
*/
|
|
89
|
-
export const toMailboxSyncProgress = (
|
|
90
|
-
mailbox: MailboxProgressFields,
|
|
91
|
-
): MailboxSyncProgress => ({
|
|
92
|
-
mailboxId: mailbox.mailboxId,
|
|
93
|
-
fullPath: mailbox.fullPath,
|
|
94
|
-
phase: deriveMailboxPhase(mailbox),
|
|
95
|
-
messagesTotal: mailbox.messageCount ?? 0,
|
|
96
|
-
messagesSynced: computeMessagesSynced(mailbox),
|
|
97
|
-
highWaterMarkUid: mailbox.highWaterMarkUid ?? 0,
|
|
98
|
-
lastSyncedAt: mailbox.lastMessageSyncAt || undefined,
|
|
99
|
-
});
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import type { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
|
|
4
|
-
import { triggerSyncSafe } from "./sync.js";
|
|
5
|
-
|
|
6
|
-
const QUEUE_URL = "http://localhost:9324/000000000000/remit-dev-mailboxes";
|
|
7
|
-
|
|
8
|
-
interface LogCall {
|
|
9
|
-
fields: Record<string, unknown>;
|
|
10
|
-
message: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const createLoggerSpy = () => {
|
|
14
|
-
const info: LogCall[] = [];
|
|
15
|
-
const error: LogCall[] = [];
|
|
16
|
-
const logger = {
|
|
17
|
-
info: (fields: Record<string, unknown>, message: string): void => {
|
|
18
|
-
info.push({ fields, message });
|
|
19
|
-
},
|
|
20
|
-
error: (fields: Record<string, unknown>, message: string): void => {
|
|
21
|
-
error.push({ fields, message });
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
return { info, error, logger };
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const okSqsClient = (sent: SendMessageCommand[]): SQSClient =>
|
|
28
|
-
({
|
|
29
|
-
send: async (cmd: SendMessageCommand) => {
|
|
30
|
-
sent.push(cmd);
|
|
31
|
-
return {};
|
|
32
|
-
},
|
|
33
|
-
}) as unknown as SQSClient;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Reproduces the real smoke failure shape: the SQS send does NOT reject inline —
|
|
37
|
-
* it rejects on a later microtask/timer, the way a socket ECONNREFUSED settles
|
|
38
|
-
* after the AWS SDK's connect + retries. A try/catch that only guards the
|
|
39
|
-
* synchronous call would miss it; only awaiting the actual promise contains it.
|
|
40
|
-
*/
|
|
41
|
-
const deferredFailingSqsClient = (error: Error): SQSClient =>
|
|
42
|
-
({
|
|
43
|
-
send: () =>
|
|
44
|
-
new Promise((_resolve, reject) => {
|
|
45
|
-
setImmediate(() => reject(error));
|
|
46
|
-
}),
|
|
47
|
-
}) as unknown as SQSClient;
|
|
48
|
-
|
|
49
|
-
describe("triggerSyncSafe", () => {
|
|
50
|
-
it("enqueues a SYNC_MAILBOXES event and logs success", async () => {
|
|
51
|
-
const sent: SendMessageCommand[] = [];
|
|
52
|
-
const { logger, info, error } = createLoggerSpy();
|
|
53
|
-
|
|
54
|
-
await triggerSyncSafe("acc-a", "config-1", {
|
|
55
|
-
sqsClient: okSqsClient(sent),
|
|
56
|
-
queueUrl: QUEUE_URL,
|
|
57
|
-
logger,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
assert.equal(sent.length, 1);
|
|
61
|
-
assert.equal(info.length, 1);
|
|
62
|
-
assert.equal(error.length, 0);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// The guarantee behind issue #37: POST /sync asks for a sync of this
|
|
66
|
-
// account by name, so the event it enqueues has to carry the mark that
|
|
67
|
-
// makes the worker's fan-out sync every mailbox rather than skip the fresh
|
|
68
|
-
// ones. Its callers are the refresh control, pull-to-refresh and the
|
|
69
|
-
// client's automatic poll — the poll is kept from abusing this by the
|
|
70
|
-
// interval floor in useStaleAccountSync, not by the server.
|
|
71
|
-
it("marks the event as an explicit request", async () => {
|
|
72
|
-
const sent: SendMessageCommand[] = [];
|
|
73
|
-
const { logger } = createLoggerSpy();
|
|
74
|
-
|
|
75
|
-
await triggerSyncSafe("acc-a", "config-1", {
|
|
76
|
-
sqsClient: okSqsClient(sent),
|
|
77
|
-
queueUrl: QUEUE_URL,
|
|
78
|
-
logger,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const body = JSON.parse(sent[0]?.input.MessageBody ?? "{}") as {
|
|
82
|
-
explicitRequest?: boolean;
|
|
83
|
-
};
|
|
84
|
-
assert.equal(body.explicitRequest, true);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("does NOT reject when the SQS enqueue fails (POST /sync stays resilient)", async () => {
|
|
88
|
-
const econnrefused = Object.assign(new Error(""), {
|
|
89
|
-
name: "AggregateError",
|
|
90
|
-
code: "ECONNREFUSED",
|
|
91
|
-
});
|
|
92
|
-
const { logger } = createLoggerSpy();
|
|
93
|
-
|
|
94
|
-
await assert.doesNotReject(
|
|
95
|
-
triggerSyncSafe("acc-a", "config-1", {
|
|
96
|
-
sqsClient: deferredFailingSqsClient(econnrefused),
|
|
97
|
-
queueUrl: QUEUE_URL,
|
|
98
|
-
logger,
|
|
99
|
-
}),
|
|
100
|
-
);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("logs the failure loudly with the alertable structured fields", async () => {
|
|
104
|
-
const econnrefused = Object.assign(new Error(""), {
|
|
105
|
-
name: "AggregateError",
|
|
106
|
-
code: "ECONNREFUSED",
|
|
107
|
-
});
|
|
108
|
-
const { logger, error } = createLoggerSpy();
|
|
109
|
-
|
|
110
|
-
await triggerSyncSafe("acc-a", "config-1", {
|
|
111
|
-
sqsClient: deferredFailingSqsClient(econnrefused),
|
|
112
|
-
queueUrl: QUEUE_URL,
|
|
113
|
-
logger,
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
assert.equal(error.length, 1);
|
|
117
|
-
const call = error[0];
|
|
118
|
-
if (!call) throw new Error("expected an error log");
|
|
119
|
-
assert.equal(call.fields.alert, "sync_trigger_failed");
|
|
120
|
-
assert.equal(call.fields.source, "trigger_sync");
|
|
121
|
-
assert.equal(call.fields.accountId, "acc-a");
|
|
122
|
-
assert.equal(call.fields.accountConfigId, "config-1");
|
|
123
|
-
assert.equal(call.fields.errorCode, "ECONNREFUSED");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// The actual smoke-storm failure mode #746/#753 missed: POST /sync void-fires
|
|
127
|
-
// this trigger, the queue is unreachable, and a concurrent read is in flight on
|
|
128
|
-
// the same event loop. Before containment the escaped rejection landed on that
|
|
129
|
-
// read and 500'd it. The SQS send here rejects on a LATER tick (a real socket
|
|
130
|
-
// error never settles inline), which is exactly why a synchronous-only guard
|
|
131
|
-
// did not reproduce the failure. Assert nothing leaks onto the concurrent read.
|
|
132
|
-
it("void-fired with an unreachable queue leaks NO unhandled rejection onto a concurrent read", async () => {
|
|
133
|
-
const leaked: unknown[] = [];
|
|
134
|
-
const onUnhandled = (reason: unknown): void => {
|
|
135
|
-
leaked.push(reason);
|
|
136
|
-
};
|
|
137
|
-
process.on("unhandledRejection", onUnhandled);
|
|
138
|
-
|
|
139
|
-
const econnrefused = Object.assign(new Error(""), {
|
|
140
|
-
name: "AggregateError",
|
|
141
|
-
code: "ECONNREFUSED",
|
|
142
|
-
});
|
|
143
|
-
const { logger } = createLoggerSpy();
|
|
144
|
-
|
|
145
|
-
let readResolved = false;
|
|
146
|
-
try {
|
|
147
|
-
// Fire-and-forget exactly as SyncOperations_triggerSync does.
|
|
148
|
-
void triggerSyncSafe("acc-a", "config-1", {
|
|
149
|
-
sqsClient: deferredFailingSqsClient(econnrefused),
|
|
150
|
-
queueUrl: QUEUE_URL,
|
|
151
|
-
logger,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// A concurrent "read" sharing the event loop, like /mailboxes or /outbox.
|
|
155
|
-
await new Promise((resolve) => setImmediate(resolve)).then(() => {
|
|
156
|
-
readResolved = true;
|
|
157
|
-
});
|
|
158
|
-
// Drain remaining microtasks so any escaped rejection would surface.
|
|
159
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
160
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
161
|
-
} finally {
|
|
162
|
-
process.off("unhandledRejection", onUnhandled);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
assert.equal(readResolved, true);
|
|
166
|
-
assert.equal(leaked.length, 0);
|
|
167
|
-
});
|
|
168
|
-
});
|
package/src/handlers/sync.ts
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import type { SQSClient } from "@aws-sdk/client-sqs";
|
|
2
|
-
import { logger } from "@remit/logger-lambda";
|
|
3
|
-
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
4
|
-
import { env } from "expect-env";
|
|
5
|
-
import { getAccountConfigIdFromEvent } from "../auth.js";
|
|
6
|
-
import { getClient } from "../service/data-client.js";
|
|
7
|
-
import {
|
|
8
|
-
type FireAndForgetLogger,
|
|
9
|
-
fireAndForget,
|
|
10
|
-
} from "../service/fire-and-forget.js";
|
|
11
|
-
import { sqsClient } from "../service/sqs.js";
|
|
12
|
-
import { triggerAccountSync } from "../service/trigger-sync.js";
|
|
13
|
-
import type { OperationHandler, SyncOperationIds } from "../types.js";
|
|
14
|
-
import { assertAccountOwnership } from "./account-ownership.js";
|
|
15
|
-
import { toMailboxSyncProgress } from "./sync-progress.js";
|
|
16
|
-
|
|
17
|
-
interface SyncTriggerDeps {
|
|
18
|
-
sqsClient: SQSClient;
|
|
19
|
-
queueUrl: string;
|
|
20
|
-
logger: FireAndForgetLogger & {
|
|
21
|
-
info: (fields: Record<string, unknown>, message: string) => void;
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const defaultSyncTriggerDeps = (): SyncTriggerDeps => ({
|
|
26
|
-
sqsClient,
|
|
27
|
-
queueUrl: env.SQS_QUEUE_URL,
|
|
28
|
-
logger,
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Fire-and-forget enqueue for the POST /sync kick.
|
|
33
|
-
*
|
|
34
|
-
* POST /sync is a best-effort nudge: the web client polls it to wake a sync and
|
|
35
|
-
* acts on no enqueue result in the response. So the enqueue must never fail —
|
|
36
|
-
* nor leak onto — this request or any concurrent read when the queue is
|
|
37
|
-
* unreachable. Previously this handler `await`ed the SQS send directly; with the
|
|
38
|
-
* queue down (smoke/e2e, or an SQS outage in prod) the awaited rejection escaped
|
|
39
|
-
* the handler's promise and landed on whatever read was in flight on the shared
|
|
40
|
-
* event loop, 500-ing unrelated `/mailboxes`, `/threads` and `/outbox` reads.
|
|
41
|
-
*
|
|
42
|
-
* Routing it through fireAndForget catches the rejection at the source, logs it
|
|
43
|
-
* loudly with the alertable structured fields, and resolves to void. A genuine
|
|
44
|
-
* account-lookup / ownership failure still propagates and 500s as normal.
|
|
45
|
-
*/
|
|
46
|
-
export const triggerSyncSafe = async (
|
|
47
|
-
accountId: string,
|
|
48
|
-
accountConfigId: string,
|
|
49
|
-
deps: SyncTriggerDeps = defaultSyncTriggerDeps(),
|
|
50
|
-
): Promise<void> => {
|
|
51
|
-
await fireAndForget(
|
|
52
|
-
async () => {
|
|
53
|
-
const { eventId } = await triggerAccountSync({
|
|
54
|
-
sqsClient: deps.sqsClient,
|
|
55
|
-
queueUrl: deps.queueUrl,
|
|
56
|
-
accountId,
|
|
57
|
-
// POST /sync asks for a sync of this account by name — the refresh
|
|
58
|
-
// control, pull-to-refresh, and the client's automatic poll all land
|
|
59
|
-
// here — so it syncs every mailbox regardless of how recently one
|
|
60
|
-
// ran.
|
|
61
|
-
explicitRequest: true,
|
|
62
|
-
});
|
|
63
|
-
deps.logger.info(
|
|
64
|
-
{ accountId, eventId },
|
|
65
|
-
"Sync triggered - enqueued SYNC_MAILBOXES event",
|
|
66
|
-
);
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
source: "trigger_sync",
|
|
70
|
-
message: "Failed to enqueue SYNC_MAILBOXES on POST /sync (best-effort)",
|
|
71
|
-
ids: { accountId, accountConfigId },
|
|
72
|
-
logger: deps.logger,
|
|
73
|
-
},
|
|
74
|
-
);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export const SyncOperations: Record<
|
|
78
|
-
SyncOperationIds,
|
|
79
|
-
OperationHandler<SyncOperationIds>
|
|
80
|
-
> = {
|
|
81
|
-
SyncOperations_triggerSync: async (context, ...args: unknown[]) => {
|
|
82
|
-
const event = args[0] as APIGatewayProxyEvent;
|
|
83
|
-
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
84
|
-
const { accountId } = context.request.params as { accountId: string };
|
|
85
|
-
|
|
86
|
-
const account = await (await getClient()).account.get(accountId);
|
|
87
|
-
assertAccountOwnership(account, accountConfigId, "act");
|
|
88
|
-
|
|
89
|
-
void triggerSyncSafe(account.accountId, accountConfigId);
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
triggered: true,
|
|
93
|
-
message: `Sync triggered for account ${accountId}`,
|
|
94
|
-
};
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
SyncOperations_getSyncStatus: async (context, ...args: unknown[]) => {
|
|
98
|
-
const event = args[0] as APIGatewayProxyEvent;
|
|
99
|
-
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
100
|
-
const { accountId } = context.request.params as { accountId: string };
|
|
101
|
-
|
|
102
|
-
const client = await getClient();
|
|
103
|
-
const account = await client.account.get(accountId);
|
|
104
|
-
assertAccountOwnership(account, accountConfigId, "read");
|
|
105
|
-
|
|
106
|
-
// Collect all mailboxes (read-only — does not write or contend with sync lock)
|
|
107
|
-
const mailboxes = await client.mailbox.listAllByAccount(accountId);
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
accountId: account.accountId,
|
|
111
|
-
syncPhase: account.syncPhase,
|
|
112
|
-
mailboxCountTotal: account.mailboxCountTotal,
|
|
113
|
-
mailboxCountSynced: account.mailboxCountSynced,
|
|
114
|
-
lastSyncAt: account.lastSyncAt,
|
|
115
|
-
mailboxes: mailboxes.map(toMailboxSyncProgress),
|
|
116
|
-
};
|
|
117
|
-
},
|
|
118
|
-
};
|