@remit/mailbox-service 0.0.89 → 0.0.90
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/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/message-move-empty-trash.test.ts +42 -7
- package/src/message-move.ts +14 -1
- package/src/placement-settled.test.ts +46 -1
- package/src/placement-settled.ts +17 -3
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -46,8 +46,27 @@ const proposedDeletedFolder: RoleResolution<TrashMailbox> = {
|
|
|
46
46
|
interface TrashMessage {
|
|
47
47
|
messageId: string;
|
|
48
48
|
syncStatus: string;
|
|
49
|
+
status?: string;
|
|
50
|
+
mailboxId?: string;
|
|
51
|
+
uid?: number;
|
|
52
|
+
originalMailboxId?: string;
|
|
53
|
+
originalUid?: number;
|
|
49
54
|
}
|
|
50
55
|
|
|
56
|
+
/**
|
|
57
|
+
* A row mid-move into Trash: the folder is already written, the uid is still
|
|
58
|
+
* the inbox's, and only `status: moving` says so.
|
|
59
|
+
*/
|
|
60
|
+
const movingIntoTrash = (messageId: string, uid: number): TrashMessage => ({
|
|
61
|
+
messageId,
|
|
62
|
+
syncStatus: "pending",
|
|
63
|
+
status: "moving",
|
|
64
|
+
mailboxId: REAL_TRASH,
|
|
65
|
+
uid,
|
|
66
|
+
originalMailboxId: "mbx-inbox",
|
|
67
|
+
originalUid: uid,
|
|
68
|
+
});
|
|
69
|
+
|
|
51
70
|
interface EnqueuedEvent {
|
|
52
71
|
type: string;
|
|
53
72
|
schemaVersion?: number;
|
|
@@ -194,22 +213,38 @@ describe("MessageMoveService.emptyTrash", () => {
|
|
|
194
213
|
assert.equal(deletedCount, 3);
|
|
195
214
|
});
|
|
196
215
|
|
|
197
|
-
it("marks and counts a message whose
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
216
|
+
it("marks and counts a message whose sync is merely pending", async () => {
|
|
217
|
+
// `syncStatus: pending` is where every freshly synced inbound row sits
|
|
218
|
+
// forever; it says nothing about where the message is. The user saw it in
|
|
219
|
+
// Trash and asked for the folder to be emptied, and skipping it would
|
|
220
|
+
// report a number the folder contradicts.
|
|
202
221
|
const { service, markedDeleting } = buildWorld(appointedTrash, [
|
|
203
222
|
{ messageId: "settled-1", syncStatus: "synced" },
|
|
204
|
-
{ messageId: "
|
|
223
|
+
{ messageId: "pending-1", syncStatus: "pending" },
|
|
205
224
|
]);
|
|
206
225
|
|
|
207
226
|
const { deletedCount } = await service.emptyTrash(ACCOUNT_CONFIG, ACCOUNT);
|
|
208
227
|
|
|
209
|
-
assert.deepEqual(markedDeleting, ["settled-1", "
|
|
228
|
+
assert.deepEqual(markedDeleting, ["settled-1", "pending-1"]);
|
|
210
229
|
assert.equal(deletedCount, 2);
|
|
211
230
|
});
|
|
212
231
|
|
|
232
|
+
it("leaves a row whose move into Trash has not settled unmarked and uncounted", async () => {
|
|
233
|
+
// Issue #1217. Marking it `deleting` overwrites the `moving` its own
|
|
234
|
+
// MESSAGE_MOVE reads to decide there is still work to do, so that move
|
|
235
|
+
// returns without touching IMAP and the worker's expunge then binds the
|
|
236
|
+
// inbox's uid against whatever Trash really holds at it.
|
|
237
|
+
const { service, markedDeleting } = buildWorld(appointedTrash, [
|
|
238
|
+
{ messageId: "settled-1", syncStatus: "synced" },
|
|
239
|
+
movingIntoTrash("moving-1", 10),
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
const { deletedCount } = await service.emptyTrash(ACCOUNT_CONFIG, ACCOUNT);
|
|
243
|
+
|
|
244
|
+
assert.deepEqual(markedDeleting, ["settled-1"]);
|
|
245
|
+
assert.equal(deletedCount, 1);
|
|
246
|
+
});
|
|
247
|
+
|
|
213
248
|
it("reports the same count when pressed twice before the worker runs", async () => {
|
|
214
249
|
// The rows it marked are still in that folder, so N stays true and the
|
|
215
250
|
// re-mark is idempotent. Reporting 0 the second time while still
|
package/src/message-move.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from "@remit/domain-enums";
|
|
35
35
|
import { createQueueProducer } from "@remit/sqs-client/producer";
|
|
36
36
|
import {
|
|
37
|
+
bindsForeignUid,
|
|
37
38
|
type PlacementBinding,
|
|
38
39
|
placementBindingOf,
|
|
39
40
|
waitForPlacementToSettle,
|
|
@@ -917,10 +918,22 @@ export class MessageMoveService {
|
|
|
917
918
|
trashMailbox.mailboxId,
|
|
918
919
|
);
|
|
919
920
|
|
|
920
|
-
const
|
|
921
|
+
const rows = await this.messageService.listAllByMailbox(
|
|
921
922
|
trashMailbox.mailboxId,
|
|
922
923
|
);
|
|
923
924
|
|
|
925
|
+
// Reconciles, never waits (imap-mutations R2). A row whose move into
|
|
926
|
+
// Trash has not settled names this folder while still carrying the SOURCE
|
|
927
|
+
// folder's uid, and marking it `deleting` overwrites the only marker that
|
|
928
|
+
// says so: its own MESSAGE_MOVE then reads the row as settled and returns
|
|
929
|
+
// without moving anything, and the worker's expunge binds that borrowed
|
|
930
|
+
// uid against whatever Trash really holds at it. Left as it stands, that
|
|
931
|
+
// move runs ahead of this expunge in the account's FIFO group and settles
|
|
932
|
+
// the row, so the message is in Trash by the time the sweep reaches it and
|
|
933
|
+
// goes with the rest of this same press — only the count below is one
|
|
934
|
+
// short of what the sweep ends up removing.
|
|
935
|
+
const messages = rows.filter((message) => !bindsForeignUid(message));
|
|
936
|
+
|
|
924
937
|
// Mark all as deleting locally
|
|
925
938
|
for (const message of messages) {
|
|
926
939
|
await this.messageService.update(message.messageId, {
|
|
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
3
|
import type { MessageItem } from "@remit/data-ports";
|
|
4
4
|
import { MessageStatus, MessageSyncStatus } from "@remit/domain-enums";
|
|
5
|
-
import { placementBindingOf } from "./placement-settled.js";
|
|
5
|
+
import { carriesForeignUid, placementBindingOf } from "./placement-settled.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Pins `placementBindingOf` directly. It is the guard #1098/#1143 depend on and
|
|
@@ -70,3 +70,48 @@ describe("placementBindingOf", () => {
|
|
|
70
70
|
);
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The same question with the `status` gate off, for callers that resolve a uid
|
|
76
|
+
* off a row some other operation may have re-marked on its way past (#1217).
|
|
77
|
+
*/
|
|
78
|
+
describe("carriesForeignUid", () => {
|
|
79
|
+
test("holds whatever an operation has since written to `status`", () => {
|
|
80
|
+
for (const status of [
|
|
81
|
+
MessageStatus.moving,
|
|
82
|
+
MessageStatus.deleting,
|
|
83
|
+
MessageStatus.active,
|
|
84
|
+
]) {
|
|
85
|
+
assert.equal(carriesForeignUid(row({ status })), true, status);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("is false once `updateUid` settles the move", () => {
|
|
90
|
+
// The settle writes the destination's own uid and drops `originalUid` in
|
|
91
|
+
// the same statement, so a destination counter that hands back the
|
|
92
|
+
// source's number cannot leave the row reading foreign forever.
|
|
93
|
+
assert.equal(
|
|
94
|
+
carriesForeignUid(
|
|
95
|
+
row({
|
|
96
|
+
status: MessageStatus.active,
|
|
97
|
+
syncStatus: MessageSyncStatus.synced,
|
|
98
|
+
originalUid: undefined,
|
|
99
|
+
}),
|
|
100
|
+
),
|
|
101
|
+
false,
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("is false for a row that was never moved", () => {
|
|
106
|
+
assert.equal(
|
|
107
|
+
carriesForeignUid(
|
|
108
|
+
row({ originalUid: undefined, originalMailboxId: undefined }),
|
|
109
|
+
),
|
|
110
|
+
false,
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("is false once a restore points the row back at that folder", () => {
|
|
115
|
+
assert.equal(carriesForeignUid(row({ mailboxId: "mbx-src" })), false);
|
|
116
|
+
});
|
|
117
|
+
});
|
package/src/placement-settled.ts
CHANGED
|
@@ -29,19 +29,33 @@ export const isPlacementUnsettled = (
|
|
|
29
29
|
* never moved, so its uid names nothing but itself — a freshly copied row is
|
|
30
30
|
* `moving` with `uid: 0` until COPYUID lands, which is a uid that is not ready,
|
|
31
31
|
* not a uid that names somebody else's message.
|
|
32
|
+
*
|
|
33
|
+
* Answered without reference to `status`, because `status` is not what clears
|
|
34
|
+
* it: `updateUid` does, dropping `originalUid` in the same statement that
|
|
35
|
+
* writes the destination's own, and a restore does by pointing `mailboxId`
|
|
36
|
+
* back at `originalMailboxId`. An operation that rewrites `status` on its way
|
|
37
|
+
* past — Empty Trash marking a whole folder `deleting` — leaves the pair
|
|
38
|
+
* untouched, so anything that resolves a uid off a row it did not settle
|
|
39
|
+
* itself asks this rather than the binding below (issue #1217).
|
|
32
40
|
*/
|
|
33
|
-
export const
|
|
41
|
+
export const carriesForeignUid = (
|
|
34
42
|
message: Pick<
|
|
35
43
|
MessageItem,
|
|
36
|
-
"
|
|
44
|
+
"mailboxId" | "uid" | "originalMailboxId" | "originalUid"
|
|
37
45
|
>,
|
|
38
46
|
): boolean =>
|
|
39
|
-
isPlacementUnsettled(message) &&
|
|
40
47
|
message.originalUid !== undefined &&
|
|
41
48
|
message.originalUid === message.uid &&
|
|
42
49
|
message.originalMailboxId !== undefined &&
|
|
43
50
|
message.originalMailboxId !== message.mailboxId;
|
|
44
51
|
|
|
52
|
+
export const bindsForeignUid = (
|
|
53
|
+
message: Pick<
|
|
54
|
+
MessageItem,
|
|
55
|
+
"status" | "mailboxId" | "uid" | "originalMailboxId" | "originalUid"
|
|
56
|
+
>,
|
|
57
|
+
): boolean => isPlacementUnsettled(message) && carriesForeignUid(message);
|
|
58
|
+
|
|
45
59
|
/**
|
|
46
60
|
* `in_flight` — a mover is still working on the row, so waiting resolves it.
|
|
47
61
|
* `abandoned` — the last attempt failed (`syncStatus: failed`) and the row is
|