@remit/data-ports 0.0.59 → 0.0.60

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/data-ports",
3
- "version": "0.0.59",
3
+ "version": "0.0.60",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -52,6 +52,10 @@
52
52
  "./update-manifest": {
53
53
  "types": "./src/update-manifest.ts",
54
54
  "default": "./src/update-manifest.ts"
55
+ },
56
+ "./message-settlement": {
57
+ "types": "./src/message-settlement.ts",
58
+ "default": "./src/message-settlement.ts"
55
59
  }
56
60
  },
57
61
  "scripts": {
package/src/index.ts CHANGED
@@ -51,6 +51,8 @@ export type {
51
51
  IUnitOfWork,
52
52
  UnitOfWorkRepositories,
53
53
  } from "./interfaces/unit-of-work.js";
54
+ export type { MessageSettlementFields } from "./message-settlement.js";
55
+ export { hasAbandonedDelete } from "./message-settlement.js";
54
56
  export {
55
57
  FILTER_NO_ACTION,
56
58
  isSenderMuted,
@@ -0,0 +1,72 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, test } from "node:test";
3
+ import { MessageStatus, MessageSyncStatus } from "@remit/domain-enums";
4
+ import { hasAbandonedDelete } from "./message-settlement.js";
5
+
6
+ describe("hasAbandonedDelete", () => {
7
+ test("the pair abandonDelete writes, its only writer", () => {
8
+ assert.equal(
9
+ hasAbandonedDelete({
10
+ status: MessageStatus.active,
11
+ syncStatus: MessageSyncStatus.failed,
12
+ }),
13
+ true,
14
+ );
15
+ });
16
+
17
+ test("a move mid-retry is not a give-up, whatever `failed` suggests", () => {
18
+ assert.equal(
19
+ hasAbandonedDelete({
20
+ status: MessageStatus.moving,
21
+ syncStatus: MessageSyncStatus.failed,
22
+ }),
23
+ false,
24
+ );
25
+ });
26
+
27
+ test("a delete mid-retry keeps `deleting`, so it is not a give-up", () => {
28
+ assert.equal(
29
+ hasAbandonedDelete({
30
+ status: MessageStatus.deleting,
31
+ syncStatus: MessageSyncStatus.failed,
32
+ }),
33
+ false,
34
+ );
35
+ });
36
+
37
+ test("a copy that gave up hides its row as `deleted` rather than claiming this", () => {
38
+ assert.equal(
39
+ hasAbandonedDelete({
40
+ status: MessageStatus.deleted,
41
+ syncStatus: MessageSyncStatus.failed,
42
+ }),
43
+ false,
44
+ );
45
+ });
46
+
47
+ /**
48
+ * Both a settled move (`updateUid`) and a delete that exhausted its retries
49
+ * (#1143 repairs the row to where the message actually is) land on this pair.
50
+ * The second is a give-up this predicate deliberately cannot see — pinned so
51
+ * the gap is a decision on record, not an oversight.
52
+ */
53
+ test("`active` + `synced` is never a give-up, however it was reached", () => {
54
+ assert.equal(
55
+ hasAbandonedDelete({
56
+ status: MessageStatus.active,
57
+ syncStatus: MessageSyncStatus.synced,
58
+ }),
59
+ false,
60
+ );
61
+ });
62
+
63
+ test("an ordinary inbound row, `pending` forever, says nothing", () => {
64
+ assert.equal(
65
+ hasAbandonedDelete({
66
+ status: MessageStatus.active,
67
+ syncStatus: MessageSyncStatus.pending,
68
+ }),
69
+ false,
70
+ );
71
+ });
72
+ });
@@ -0,0 +1,48 @@
1
+ import { MessageStatus, MessageSyncStatus } from "@remit/domain-enums";
2
+ import type { MessageItem } from "./types.js";
3
+
4
+ export type MessageSettlementFields = Pick<
5
+ MessageItem,
6
+ "status" | "syncStatus"
7
+ >;
8
+
9
+ /**
10
+ * A delete Remit refused to run, having already removed the message locally.
11
+ * `abandonDelete` reaches this from four checks, all of them made before any
12
+ * expunge: the Trash folder the event names is not on the server (TRYCREATE),
13
+ * the event carries no destination, it names an operation this build does not
14
+ * recognise, or it was minted under an unknown contract. The row is handed back
15
+ * to the folder the server still holds the message in.
16
+ *
17
+ * `status: active` alongside `syncStatus: failed` is the whole signal, and
18
+ * `abandonDelete` (`imap-worker/src/handlers/message-delete.ts`) is its ONLY
19
+ * writer. Every other writer of either value writes the other along with it:
20
+ *
21
+ * - `updateUid` (`drizzle-service/src/repos/message.ts`) settles a confirmed
22
+ * move by writing `active` and `synced` in the same statement, so a move that
23
+ * worked after a failed attempt cannot leave `failed` behind.
24
+ * - `empty-trash` and `message-copy`'s `settleCopied` both hand a row back as
25
+ * `active` + `synced`.
26
+ * - `upsertWithStatus` leaves an existing row alone, so no inbound sync writes
27
+ * either value onto a row that already has them.
28
+ *
29
+ * `syncStatus: failed` ON ITS OWN is NOT a give-up marker, whatever
30
+ * `placement-settled.ts`'s docstring says. `message-move.ts`, `message-delete.ts`
31
+ * and `message-copy.ts` each write it on an ORDINARY TRANSIENT attempt and then
32
+ * re-throw for queue redelivery — the row is mid-retry and about to succeed. In
33
+ * those handlers `status` stays at its in-flight value (`moving`, `deleting`),
34
+ * which is what separates them from the pair above.
35
+ *
36
+ * Three give-ups this cannot see, and must not pretend to:
37
+ *
38
+ * - A MOVE that gave up leaves `moving` + `failed`, the same pair a first
39
+ * dropped connection writes. Nothing persisted tells the two apart.
40
+ * - A DELETE that exhausted its retries settles `active` + `synced` (#1143's
41
+ * `resolveExhaustedMessageDeleteFailure` repairs the row to where the message
42
+ * actually is), so it reads as fully settled here.
43
+ * - `flag-push` and `placement-move-push` never write either field at all.
44
+ * Their give-up state lives on their own marker rows.
45
+ */
46
+ export const hasAbandonedDelete = (message: MessageSettlementFields): boolean =>
47
+ message.status === MessageStatus.active &&
48
+ message.syncStatus === MessageSyncStatus.failed;