@remit/mailbox-service 0.0.111 → 0.0.113

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.
Files changed (298) hide show
  1. package/dist/account-credentials.d.ts +83 -0
  2. package/dist/account-credentials.js +104 -0
  3. package/dist/attribute-mapper.d.ts +36 -0
  4. package/dist/attribute-mapper.js +119 -0
  5. package/dist/body-parse.d.ts +28 -0
  6. package/{src/body-parse.ts → dist/body-parse.js} +18 -26
  7. package/dist/body-part-mapper.d.ts +92 -0
  8. package/dist/body-part-mapper.js +301 -0
  9. package/dist/body-sync-queue.d.ts +59 -0
  10. package/dist/body-sync-queue.js +56 -0
  11. package/dist/body-sync-terminal.d.ts +58 -0
  12. package/dist/body-sync-terminal.js +95 -0
  13. package/dist/body-sync.d.ts +351 -0
  14. package/dist/body-sync.js +1204 -0
  15. package/dist/calendar-parts.d.ts +23 -0
  16. package/dist/calendar-parts.js +18 -0
  17. package/dist/classification-backfill.d.ts +91 -0
  18. package/dist/classification-backfill.js +180 -0
  19. package/dist/classify-message.d.ts +31 -0
  20. package/dist/classify-message.js +89 -0
  21. package/dist/connection-factory.d.ts +54 -0
  22. package/dist/connection-factory.js +54 -0
  23. package/dist/connection-test.d.ts +41 -0
  24. package/dist/connection-test.js +152 -0
  25. package/dist/external-move.d.ts +94 -0
  26. package/dist/external-move.js +173 -0
  27. package/dist/filter-operations.d.ts +42 -0
  28. package/dist/filter-operations.js +211 -0
  29. package/dist/filters/filter-config.d.ts +14 -0
  30. package/dist/filters/filter-config.js +10 -0
  31. package/{src/filters/list-id.ts → dist/filters/list-id.d.ts} +2 -16
  32. package/dist/filters/list-id.js +26 -0
  33. package/dist/filters/match.d.ts +69 -0
  34. package/dist/filters/match.js +165 -0
  35. package/dist/filters/pipeline.d.ts +64 -0
  36. package/dist/filters/pipeline.js +100 -0
  37. package/{src/flag-push-terminal.ts → dist/flag-push-terminal.d.ts} +19 -77
  38. package/dist/flag-push-terminal.js +63 -0
  39. package/dist/flag-push.d.ts +69 -0
  40. package/dist/flag-push.js +111 -0
  41. package/dist/flag-queue.d.ts +175 -0
  42. package/dist/flag-queue.js +291 -0
  43. package/dist/folder-operations.d.ts +39 -0
  44. package/dist/folder-operations.js +112 -0
  45. package/dist/folder-placement.d.ts +13 -0
  46. package/dist/folder-placement.js +23 -0
  47. package/dist/heuristics/authenticationResults.d.ts +10 -0
  48. package/dist/heuristics/authenticationResults.js +187 -0
  49. package/dist/heuristics/classifyByHeaders.d.ts +82 -0
  50. package/dist/heuristics/classifyByHeaders.js +365 -0
  51. package/dist/heuristics/classifyPlacement.d.ts +67 -0
  52. package/dist/heuristics/classifyPlacement.js +153 -0
  53. package/dist/heuristics/machineSenders.d.ts +21 -0
  54. package/{src/heuristics/machineSenders.ts → dist/heuristics/machineSenders.js} +34 -40
  55. package/dist/heuristics/senderMismatch.d.ts +81 -0
  56. package/dist/heuristics/senderMismatch.js +192 -0
  57. package/dist/heuristics/socialDomains.d.ts +14 -0
  58. package/{src/heuristics/socialDomains.ts → dist/heuristics/socialDomains.js} +9 -9
  59. package/dist/heuristics/transactionalDomains.d.ts +18 -0
  60. package/{src/heuristics/transactionalDomains.ts → dist/heuristics/transactionalDomains.js} +8 -8
  61. package/dist/imapflow-connection.d.ts +377 -0
  62. package/dist/imapflow-connection.js +1147 -0
  63. package/dist/index.d.ts +63 -0
  64. package/dist/index.js +65 -0
  65. package/dist/list-id-backfill.d.ts +83 -0
  66. package/dist/list-id-backfill.js +132 -0
  67. package/dist/mailbox-cursor-rebuild.d.ts +74 -0
  68. package/dist/mailbox-cursor-rebuild.js +71 -0
  69. package/{src/mailbox-cursor.ts → dist/mailbox-cursor.d.ts} +20 -98
  70. package/dist/mailbox-cursor.js +102 -0
  71. package/dist/mailbox-intent.d.ts +41 -0
  72. package/{src/mailbox-intent.ts → dist/mailbox-intent.js} +39 -63
  73. package/dist/mailbox-management.d.ts +240 -0
  74. package/dist/mailbox-management.js +576 -0
  75. package/dist/mailbox-presence.d.ts +52 -0
  76. package/{src/mailbox-presence.ts → dist/mailbox-presence.js} +19 -30
  77. package/dist/mailbox-queue.d.ts +126 -0
  78. package/dist/mailbox-queue.js +214 -0
  79. package/dist/mailbox-sync.d.ts +93 -0
  80. package/dist/mailbox-sync.js +490 -0
  81. package/dist/mailbox-upstream.d.ts +38 -0
  82. package/{src/mailbox-upstream.ts → dist/mailbox-upstream.js} +37 -47
  83. package/dist/message-category.d.ts +51 -0
  84. package/{src/message-category.ts → dist/message-category.js} +28 -71
  85. package/dist/message-move.d.ts +285 -0
  86. package/dist/message-move.js +858 -0
  87. package/dist/message-parser.d.ts +14 -0
  88. package/dist/message-parser.js +17 -0
  89. package/{src/message-presence.ts → dist/message-presence.d.ts} +1 -13
  90. package/dist/message-presence.js +29 -0
  91. package/dist/message-read.d.ts +51 -0
  92. package/dist/message-read.js +31 -0
  93. package/dist/message-sync.d.ts +345 -0
  94. package/dist/message-sync.js +1358 -0
  95. package/dist/mime-walker.d.ts +102 -0
  96. package/dist/mime-walker.js +198 -0
  97. package/dist/organize-job.d.ts +20 -0
  98. package/dist/organize-job.js +37 -0
  99. package/dist/outbox-attachment-content.d.ts +37 -0
  100. package/dist/outbox-attachment-content.js +50 -0
  101. package/dist/outbox-attachment-filename.d.ts +19 -0
  102. package/dist/outbox-attachment-filename.js +64 -0
  103. package/dist/outbox-attachment.d.ts +148 -0
  104. package/dist/outbox-attachment.js +273 -0
  105. package/dist/outbox-queue.d.ts +73 -0
  106. package/dist/outbox-queue.js +284 -0
  107. package/{src/outbox-status.ts → dist/outbox-status.d.ts} +1 -6
  108. package/dist/outbox-status.js +18 -0
  109. package/dist/ownership.d.ts +24 -0
  110. package/dist/ownership.js +83 -0
  111. package/dist/placement-move-terminal.d.ts +67 -0
  112. package/dist/placement-move-terminal.js +88 -0
  113. package/dist/placement-move.d.ts +83 -0
  114. package/dist/placement-move.js +216 -0
  115. package/{src/placement-settled.ts → dist/placement-settled.d.ts} +19 -50
  116. package/dist/placement-settled.js +75 -0
  117. package/dist/quarantine.d.ts +95 -0
  118. package/dist/quarantine.js +131 -0
  119. package/dist/refusal.d.ts +31 -0
  120. package/dist/refusal.js +3 -0
  121. package/{src/restore-source-placement.ts → dist/restore-source-placement.d.ts} +29 -101
  122. package/dist/restore-source-placement.js +79 -0
  123. package/dist/search-text-backfill.d.ts +15 -0
  124. package/dist/search-text-backfill.js +33 -0
  125. package/dist/snippet.d.ts +61 -0
  126. package/dist/snippet.js +129 -0
  127. package/dist/spam-report.d.ts +86 -0
  128. package/dist/spam-report.js +241 -0
  129. package/dist/sync-warning.d.ts +21 -0
  130. package/dist/sync-warning.js +26 -0
  131. package/dist/sync-watermarks.d.ts +138 -0
  132. package/dist/sync-watermarks.js +205 -0
  133. package/dist/test-helpers/fake-imap.d.ts +151 -0
  134. package/dist/test-helpers/fake-imap.js +508 -0
  135. package/dist/test-helpers/folder-roles.d.ts +34 -0
  136. package/dist/test-helpers/folder-roles.js +47 -0
  137. package/dist/test-helpers/mime-tree-generator.d.ts +82 -0
  138. package/dist/test-helpers/mime-tree-generator.js +329 -0
  139. package/dist/test-helpers/test-store.d.ts +69 -0
  140. package/dist/test-helpers/test-store.js +193 -0
  141. package/dist/text/normalizer.d.ts +20 -0
  142. package/dist/text/normalizer.js +102 -0
  143. package/dist/thread-message-rows.d.ts +69 -0
  144. package/dist/thread-message-rows.js +40 -0
  145. package/dist/thread-root.d.ts +7 -0
  146. package/dist/thread-root.js +25 -0
  147. package/dist/types.d.ts +343 -0
  148. package/dist/types.js +19 -0
  149. package/package.json +86 -73
  150. package/src/account-credentials.ts +0 -210
  151. package/src/adversarial-mime.e2e.test.ts +0 -307
  152. package/src/attribute-mapper.test.ts +0 -104
  153. package/src/attribute-mapper.ts +0 -148
  154. package/src/body-parse.test.ts +0 -55
  155. package/src/body-part-mapper.property.test.ts +0 -354
  156. package/src/body-part-mapper.ts +0 -360
  157. package/src/body-sync-auto-read-writeonce.test.ts +0 -219
  158. package/src/body-sync-calendar-suggestion.test.ts +0 -433
  159. package/src/body-sync-category-denormalize.test.ts +0 -352
  160. package/src/body-sync-category-override.test.ts +0 -501
  161. package/src/body-sync-category-writeonce.test.ts +0 -234
  162. package/src/body-sync-filter.test.ts +0 -237
  163. package/src/body-sync-message-gone.test.ts +0 -139
  164. package/src/body-sync-placement-flags.test.ts +0 -347
  165. package/src/body-sync-placement-writeonce.test.ts +0 -337
  166. package/src/body-sync-quarantine.test.ts +0 -276
  167. package/src/body-sync-queue.test.ts +0 -124
  168. package/src/body-sync-queue.ts +0 -131
  169. package/src/body-sync-resync.test.ts +0 -605
  170. package/src/body-sync-terminal.test.ts +0 -310
  171. package/src/body-sync-terminal.ts +0 -176
  172. package/src/body-sync-unclassifiable-once.test.ts +0 -260
  173. package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
  174. package/src/body-sync.ts +0 -1970
  175. package/src/calendar-parts.ts +0 -41
  176. package/src/classification-backfill.test.ts +0 -615
  177. package/src/classification-backfill.ts +0 -363
  178. package/src/classify-message.test.ts +0 -205
  179. package/src/classify-message.ts +0 -116
  180. package/src/connection-factory.ts +0 -102
  181. package/src/connection-test.ts +0 -208
  182. package/src/credentials.test.ts +0 -181
  183. package/src/external-move.test.ts +0 -369
  184. package/src/external-move.ts +0 -272
  185. package/src/filters/anchor-drift.ts +0 -55
  186. package/src/filters/filter-config.test.ts +0 -229
  187. package/src/filters/filter-config.ts +0 -69
  188. package/src/filters/list-id.test.ts +0 -50
  189. package/src/filters/match.test.ts +0 -429
  190. package/src/filters/match.ts +0 -274
  191. package/src/filters/message-embedder.test.ts +0 -39
  192. package/src/filters/message-embedder.ts +0 -33
  193. package/src/filters/pipeline.test.ts +0 -330
  194. package/src/filters/pipeline.ts +0 -284
  195. package/src/flag-push-terminal.test.ts +0 -209
  196. package/src/flag-push.test.ts +0 -408
  197. package/src/flag-push.ts +0 -198
  198. package/src/flag-queue-stars.test.ts +0 -171
  199. package/src/flag-queue.ts +0 -509
  200. package/src/folder-placement.ts +0 -56
  201. package/src/heuristics/authenticationResults.test.ts +0 -189
  202. package/src/heuristics/authenticationResults.ts +0 -212
  203. package/src/heuristics/classifyByHeaders.test.ts +0 -889
  204. package/src/heuristics/classifyByHeaders.ts +0 -467
  205. package/src/heuristics/classifyPlacement.test.ts +0 -366
  206. package/src/heuristics/classifyPlacement.ts +0 -211
  207. package/src/heuristics/classifyRealMail.test.ts +0 -242
  208. package/src/heuristics/machineSenders.test.ts +0 -101
  209. package/src/heuristics/senderMismatch.test.ts +0 -346
  210. package/src/heuristics/senderMismatch.ts +0 -259
  211. package/src/imapflow-connect-retry.test.ts +0 -134
  212. package/src/imapflow-connection.e2e.test.ts +0 -335
  213. package/src/imapflow-connection.integ.test.ts +0 -316
  214. package/src/imapflow-connection.test.ts +0 -469
  215. package/src/imapflow-connection.ts +0 -1475
  216. package/src/index.ts +0 -350
  217. package/src/list-id-backfill.test.ts +0 -573
  218. package/src/list-id-backfill.ts +0 -274
  219. package/src/mailbox-cursor-rebuild.test.ts +0 -171
  220. package/src/mailbox-cursor-rebuild.ts +0 -146
  221. package/src/mailbox-cursor.test.ts +0 -249
  222. package/src/mailbox-delete-intent.test.ts +0 -246
  223. package/src/mailbox-management.test.ts +0 -356
  224. package/src/mailbox-management.ts +0 -839
  225. package/src/mailbox-presence.test.ts +0 -72
  226. package/src/mailbox-queue.ts +0 -415
  227. package/src/mailbox-rename-intent.test.ts +0 -389
  228. package/src/mailbox-rename-subtree.test.ts +0 -664
  229. package/src/mailbox-sync.test.ts +0 -848
  230. package/src/mailbox-sync.ts +0 -724
  231. package/src/message-delete-junk-reconcile.test.ts +0 -117
  232. package/src/message-delete-no-trash.test.ts +0 -245
  233. package/src/message-delete-unsettled-placement.test.ts +0 -396
  234. package/src/message-move-copy.test.ts +0 -562
  235. package/src/message-move-empty-trash.test.ts +0 -326
  236. package/src/message-move-same-mailbox.test.ts +0 -139
  237. package/src/message-move-unsettled-placement.test.ts +0 -378
  238. package/src/message-move.ts +0 -1348
  239. package/src/message-parser.ts +0 -26
  240. package/src/message-presence.test.ts +0 -50
  241. package/src/message-sync-address-spoof.test.ts +0 -293
  242. package/src/message-sync-category.test.ts +0 -142
  243. package/src/message-sync-changedsince.test.ts +0 -840
  244. package/src/message-sync-flags.test.ts +0 -158
  245. package/src/message-sync-junk-harvest.test.ts +0 -244
  246. package/src/message-sync-quarantine.test.ts +0 -416
  247. package/src/message-sync-repoint.test.ts +0 -419
  248. package/src/message-sync.ts +0 -1963
  249. package/src/mime-walker.test.ts +0 -311
  250. package/src/mime-walker.ts +0 -305
  251. package/src/outbox-attachment-content.test.ts +0 -153
  252. package/src/outbox-attachment-content.ts +0 -104
  253. package/src/outbox-attachment-filename.test.ts +0 -132
  254. package/src/outbox-attachment-filename.ts +0 -76
  255. package/src/outbox-attachment.test.ts +0 -738
  256. package/src/outbox-attachment.ts +0 -536
  257. package/src/outbox-failed-recovery.test.ts +0 -370
  258. package/src/outbox-queue-settled-row.test.ts +0 -140
  259. package/src/outbox-queue.test.ts +0 -291
  260. package/src/outbox-queue.ts +0 -508
  261. package/src/pass-through-unit-of-work.ts +0 -17
  262. package/src/placement-move-terminal.test.ts +0 -300
  263. package/src/placement-move-terminal.ts +0 -148
  264. package/src/placement-move-unsettled.test.ts +0 -275
  265. package/src/placement-move.ts +0 -336
  266. package/src/placement-settled.test.ts +0 -147
  267. package/src/placement-transition-race.test.ts +0 -482
  268. package/src/quarantine.test.ts +0 -191
  269. package/src/quarantine.ts +0 -228
  270. package/src/snippet.test.ts +0 -213
  271. package/src/snippet.ts +0 -152
  272. package/src/spam-report.test.ts +0 -801
  273. package/src/spam-report.ts +0 -357
  274. package/src/stale-message-reconcile.test.ts +0 -82
  275. package/src/stale-message-reconcile.ts +0 -55
  276. package/src/sync-watermarks.test.ts +0 -409
  277. package/src/sync-watermarks.ts +0 -319
  278. package/src/system-flag-wire-format.test.ts +0 -77
  279. package/src/test-helpers/folder-roles.ts +0 -66
  280. package/src/test-helpers/isolated-mailbox.ts +0 -44
  281. package/src/test-helpers/mailfuzz-connection.ts +0 -27
  282. package/src/test-helpers/mime-tree-generator.ts +0 -461
  283. package/src/text/normalizer.test.ts +0 -223
  284. package/src/text/normalizer.ts +0 -140
  285. package/src/thread-message-rows.ts +0 -77
  286. package/src/types/mailparser-augment.d.ts +0 -15
  287. package/src/types/natural-porter-stemmer.d.ts +0 -50
  288. package/src/types/stopword.d.ts +0 -69
  289. package/src/types.ts +0 -371
  290. package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +0 -21
  291. package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +0 -31
  292. package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +0 -33
  293. package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +0 -42
  294. package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +0 -25
  295. package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +0 -15
  296. package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +0 -17
  297. package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +0 -18
  298. package/tsconfig.json +0 -8
package/src/flag-push.ts DELETED
@@ -1,198 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { SendMessageCommand, type SQSClient } from "@aws-sdk/client-sqs";
3
- import type { IMessageFlagPushRepository } from "@remit/data-ports";
4
- import { createQueueProducer } from "@remit/sqs-client/producer";
5
-
6
- /**
7
- * Event the reconciler (imap-worker `handleFlagPush`) drains. Carries ONLY
8
- * our own message id + the flag field — never a UID and never the desired
9
- * operation (that lives on the marker, resolved fresh at push time, epic
10
- * #1281 invariant 1). The event stays valid across any amount of queue delay
11
- * or a later local flip of the SAME field (which replaces the marker before
12
- * this event is ever processed).
13
- */
14
- export interface FlagPushEvent {
15
- type: "FLAG_PUSH";
16
- eventId: string;
17
- timestamp: number;
18
- accountId: string;
19
- accountConfigId: string;
20
- messageId: string;
21
- flagName: string;
22
- }
23
-
24
- export interface FlagPushLogger {
25
- info(obj: Record<string, unknown>, msg: string): void;
26
- error(obj: Record<string, unknown>, msg: string): void;
27
- }
28
-
29
- const noopLogger: FlagPushLogger = {
30
- info: () => {},
31
- error: () => {},
32
- };
33
-
34
- export interface FlagPushConfig {
35
- markerService: IMessageFlagPushRepository;
36
- sqsQueueUrl: string;
37
- sqsEndpoint?: string;
38
- logger?: FlagPushLogger;
39
- }
40
-
41
- export type FlagPushOperationValue = "add" | "remove";
42
-
43
- /**
44
- * Writes a pending flag-push marker and best-effort enqueues a wake-up hint
45
- * (issue #1273, epic #1281). Called AFTER the caller (`FlagQueueService`) has
46
- * already durably written the local `MessageFlag`/`ThreadMessage` state in
47
- * the SAME request — the user's intent is durable the moment `flip` writes
48
- * the marker, before the enqueue even runs.
49
- *
50
- * Mirrors `PlacementMoveService` (#1289)'s marker lifecycle, scoped per
51
- * (`messageId`, `flagName`) instead of per `messageId` — a message can carry
52
- * an independent pending read-state marker and pending star marker at once
53
- * (epic FAQ: "a flag flip replaces a pending flag flip", scoped per field).
54
- * `put` (not `create`) always resets the marker to `pending`, so a later flip
55
- * of the SAME field simply replaces the earlier marker — "later intent wins
56
- * locally".
57
- *
58
- * Unlike #1289's placement move (where an enqueue failure must propagate,
59
- * because nothing besides the marker+enqueue sequence has committed the
60
- * user's intent), the flag flip here is durable the instant the marker write
61
- * lands — the enqueue is purely a latency optimization so the push does not
62
- * have to wait for the next periodic sync tick. An enqueue failure is
63
- * therefore swallowed on purpose: it must never fail the caller's request.
64
- * It must also never be silent — logged with an `alert`-shaped entry so a
65
- * string of failures (SQS itself unhealthy) is operationally visible, same
66
- * convention as the enqueue this replaces (`flag-queue.ts`'s prior
67
- * `flag_sync_enqueue_failed`).
68
- */
69
- export class FlagPushService {
70
- private markerService: IMessageFlagPushRepository;
71
- private sqs: SQSClient;
72
- private queueUrl: string;
73
- private log: FlagPushLogger;
74
-
75
- constructor(config: FlagPushConfig) {
76
- this.markerService = config.markerService;
77
- this.queueUrl = config.sqsQueueUrl;
78
- this.log = config.logger ?? noopLogger;
79
- this.sqs = createQueueProducer({
80
- queueUrl: config.sqsQueueUrl,
81
- endpoint: config.sqsEndpoint,
82
- });
83
- }
84
-
85
- flip = async (params: {
86
- accountId: string;
87
- accountConfigId: string;
88
- messageId: string;
89
- mailboxId: string;
90
- flagName: string;
91
- operation: FlagPushOperationValue;
92
- }): Promise<void> => {
93
- const {
94
- accountId,
95
- accountConfigId,
96
- messageId,
97
- mailboxId,
98
- flagName,
99
- operation,
100
- } = params;
101
-
102
- await this.markerService.put({
103
- messageId,
104
- flagName,
105
- accountId,
106
- accountConfigId,
107
- mailboxId,
108
- operation,
109
- });
110
-
111
- await this.enqueueHint({ accountId, accountConfigId, messageId, flagName });
112
- };
113
-
114
- private enqueueHint = async (params: {
115
- accountId: string;
116
- accountConfigId: string;
117
- messageId: string;
118
- flagName: string;
119
- }): Promise<void> => {
120
- const event: FlagPushEvent = {
121
- type: "FLAG_PUSH",
122
- eventId: randomUUID(),
123
- timestamp: Date.now(),
124
- ...params,
125
- };
126
-
127
- const useFifo = this.queueUrl.endsWith(".fifo");
128
-
129
- // The marker persisted in `flip` is the durable record — the queue,
130
- // per the issue's own words. This send is only a wake-up hint: a
131
- // queue-down failure must NOT fail the caller's flip (intent is
132
- // already durable) and must NOT be silent either (standing rule: our
133
- // infra failing is never a quiet blip). The periodic per-mailbox
134
- // drain (issue #1273) still finds and pushes the marker regardless.
135
- await this.sqs
136
- .send(
137
- new SendMessageCommand({
138
- QueueUrl: this.queueUrl,
139
- MessageBody: JSON.stringify(event),
140
- ...(useFifo && {
141
- MessageGroupId: params.accountId,
142
- MessageDeduplicationId: event.eventId,
143
- }),
144
- }),
145
- )
146
- .then(async () => {
147
- // Only advance past `pending` once the hint is CONFIRMED sent — a
148
- // marker still `pending` is exactly the periodic drain's query
149
- // target (`sync-messages.ts`'s `drainPendingFlagPushes`), so a
150
- // failed send below must leave it there, never here.
151
- await this.markerService
152
- .updateState(params.messageId, params.flagName, "queued")
153
- .catch(async (error: unknown) => {
154
- // Fast-path race (review finding on #1292): the worker can
155
- // drain AND delete the marker before this transition runs —
156
- // SQS delivery + processing can outrun this .then(). The
157
- // enqueue itself already succeeded, so re-check whether the
158
- // marker still exists before treating this as an infra
159
- // failure: if it's already gone, the push already completed
160
- // and this is expected, routine — never the
161
- // flag_push_hint_enqueue_failed alarm shape. Any OTHER
162
- // failure (e.g. the re-check itself fails because the
163
- // backend is genuinely down) still propagates to the outer
164
- // catch below.
165
- const stillPending = await this.markerService.find(
166
- params.messageId,
167
- params.flagName,
168
- );
169
- if (!stillPending) {
170
- this.log.info(
171
- { eventId: event.eventId, ...params },
172
- "Marker already drained before the queued-state transition (fast-path race, harmless)",
173
- );
174
- return;
175
- }
176
- throw error;
177
- });
178
- this.log.info(
179
- { eventId: event.eventId, ...params },
180
- "Enqueued FLAG_PUSH wake-up hint",
181
- );
182
- })
183
- .catch((error: unknown) => {
184
- this.log.error(
185
- {
186
- alert: "flag_push_hint_enqueue_failed",
187
- eventId: event.eventId,
188
- ...params,
189
- errorName: (error as { name?: string })?.name,
190
- errorCode:
191
- (error as { Code?: string })?.Code ??
192
- (error as { code?: string })?.code,
193
- },
194
- "Failed to enqueue FLAG_PUSH wake-up hint (marker persisted; the periodic sync tick will still drain it)",
195
- );
196
- });
197
- };
198
- }
@@ -1,171 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, test } from "node:test";
3
- import type {
4
- IMessageFlagRepository,
5
- IMessageRepository,
6
- IThreadMessageRepository,
7
- MessageItem,
8
- ThreadMessageItem,
9
- UpdateThreadMessageInput,
10
- } from "@remit/data-ports";
11
- import { MessageSystemFlag } from "@remit/domain-enums";
12
- import type { FlagPushService } from "./flag-push.js";
13
- import { FlagQueueService } from "./flag-queue.js";
14
-
15
- // #44: `hasStars` is the boolean of record and `star` its presentation colour.
16
- // The UI's star toggle sends only `isStarred`, so a star that left `star` at
17
- // the `none` sentinel disagreed with `hasStars` and every read site that
18
- // consulted the colour rejected the row. The colour now follows the boolean.
19
-
20
- const ACCOUNT_CONFIG_ID = "cfg-1";
21
- const ACCOUNT_ID = "acct-1";
22
- const MESSAGE_ID = "msg-1";
23
- const MAILBOX_ID = "mbx-1";
24
- const THREAD_MESSAGE_ID = "tm-1";
25
-
26
- const threadMessage = {
27
- threadMessageId: THREAD_MESSAGE_ID,
28
- accountConfigId: ACCOUNT_CONFIG_ID,
29
- mailboxId: MAILBOX_ID,
30
- sentDate: 1,
31
- isRead: false,
32
- isDeleted: false,
33
- hasStars: false,
34
- hasAttachment: false,
35
- } as unknown as ThreadMessageItem;
36
-
37
- const buildService = ({
38
- alreadyFlagged = false,
39
- }: {
40
- alreadyFlagged?: boolean;
41
- } = {}): {
42
- service: FlagQueueService;
43
- updates: UpdateThreadMessageInput[];
44
- flips: Array<{ flagName: string; operation: string }>;
45
- } => {
46
- const updates: UpdateThreadMessageInput[] = [];
47
- const flips: Array<{ flagName: string; operation: string }> = [];
48
-
49
- const messageService = {
50
- get: async () => ({ mailboxId: MAILBOX_ID }) as unknown as MessageItem,
51
- } as unknown as IMessageRepository;
52
-
53
- const messageFlagService = {
54
- hasFlag: async (_messageId: string, flagName: string) =>
55
- flagName === MessageSystemFlag.Flagged ? alreadyFlagged : false,
56
- addFlag: async () => {},
57
- removeFlag: async () => {},
58
- } as unknown as IMessageFlagRepository;
59
-
60
- const threadMessageService = {
61
- findAllByMessageId: async () => [threadMessage],
62
- update: async (
63
- _accountConfigId: string,
64
- _threadMessageId: string,
65
- input: UpdateThreadMessageInput,
66
- ) => {
67
- updates.push(input);
68
- return threadMessage;
69
- },
70
- } as unknown as IThreadMessageRepository;
71
-
72
- const flagPushService = {
73
- flip: async (event: { flagName: string; operation: string }) => {
74
- flips.push({ flagName: event.flagName, operation: event.operation });
75
- },
76
- } as unknown as FlagPushService;
77
-
78
- const service = new FlagQueueService({
79
- messageFlagService,
80
- messageService,
81
- threadMessageService,
82
- flagPushService,
83
- });
84
-
85
- return { service, updates, flips };
86
- };
87
-
88
- describe("updateFlags keeps hasStars and the star colour in step", () => {
89
- test("starring without a colour sets both the boolean and a visible colour", async () => {
90
- const { service, updates } = buildService();
91
-
92
- await service.updateFlags(ACCOUNT_CONFIG_ID, MESSAGE_ID, ACCOUNT_ID, {
93
- isStarred: true,
94
- });
95
-
96
- assert.deepEqual(updates, [{ hasStars: true, star: "yellow" }]);
97
- });
98
-
99
- test("unstarring clears the colour back to the none sentinel", async () => {
100
- const { service, updates } = buildService({ alreadyFlagged: true });
101
-
102
- await service.updateFlags(ACCOUNT_CONFIG_ID, MESSAGE_ID, ACCOUNT_ID, {
103
- isStarred: false,
104
- });
105
-
106
- assert.deepEqual(updates, [{ hasStars: false, star: "none" }]);
107
- });
108
-
109
- test("an explicit colour wins over the default", async () => {
110
- const { service, updates } = buildService();
111
-
112
- await service.updateFlags(ACCOUNT_CONFIG_ID, MESSAGE_ID, ACCOUNT_ID, {
113
- isStarred: true,
114
- starColor: "red",
115
- });
116
-
117
- assert.deepEqual(updates, [{ hasStars: true, star: "red" }]);
118
- });
119
-
120
- // A colour-only request is legal on the wire. Before, it wrote the colour
121
- // and left `hasStars` alone, so a coloured-but-unstarred row rendered
122
- // unstarred, stayed out of Starred, and never pushed \Flagged.
123
- test("a colour alone stars the message and pushes the flag", async () => {
124
- const { service, updates, flips } = buildService();
125
-
126
- await service.updateFlags(ACCOUNT_CONFIG_ID, MESSAGE_ID, ACCOUNT_ID, {
127
- starColor: "blue",
128
- });
129
-
130
- assert.deepEqual(updates, [{ hasStars: true, star: "blue" }]);
131
- assert.deepEqual(flips, [
132
- { flagName: MessageSystemFlag.Flagged, operation: "add" },
133
- ]);
134
- });
135
-
136
- test("the none colour unstars the message", async () => {
137
- const { service, updates, flips } = buildService({ alreadyFlagged: true });
138
-
139
- await service.updateFlags(ACCOUNT_CONFIG_ID, MESSAGE_ID, ACCOUNT_ID, {
140
- starColor: "none",
141
- });
142
-
143
- assert.deepEqual(updates, [{ hasStars: false, star: "none" }]);
144
- assert.deepEqual(flips, [
145
- { flagName: MessageSystemFlag.Flagged, operation: "remove" },
146
- ]);
147
- });
148
-
149
- test("isStarred decides the boolean when both fields are sent", async () => {
150
- const { service, updates } = buildService();
151
-
152
- await service.updateFlags(ACCOUNT_CONFIG_ID, MESSAGE_ID, ACCOUNT_ID, {
153
- isStarred: true,
154
- starColor: "none",
155
- });
156
-
157
- assert.deepEqual(updates, [{ hasStars: true, star: "none" }]);
158
- });
159
-
160
- test("starring pushes the Flagged keyword", async () => {
161
- const { service, flips } = buildService();
162
-
163
- await service.updateFlags(ACCOUNT_CONFIG_ID, MESSAGE_ID, ACCOUNT_ID, {
164
- isStarred: true,
165
- });
166
-
167
- assert.deepEqual(flips, [
168
- { flagName: MessageSystemFlag.Flagged, operation: "add" },
169
- ]);
170
- });
171
- });