@remit/data-ports 0.0.9 → 0.0.11

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.9",
3
+ "version": "0.0.11",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
package/src/id.test.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { describe, it } from "node:test";
3
- import { deriveQuarantineId, quarantineMessageIdHash } from "./id.js";
3
+ import {
4
+ deriveCopyMessageId,
5
+ deriveMessageId,
6
+ deriveQuarantineId,
7
+ quarantineMessageIdHash,
8
+ } from "./id.js";
4
9
 
5
10
  const ACCOUNT = "acct-1";
6
11
  const MAILBOX = "mbx-1";
@@ -30,6 +35,38 @@ describe("deriveQuarantineId", () => {
30
35
  });
31
36
  });
32
37
 
38
+ describe("deriveCopyMessageId", () => {
39
+ const SOURCE = deriveMessageId(ACCOUNT, "<abc@example.com>");
40
+ const DEST = "mbx-archive";
41
+
42
+ it("is stable for the same source and destination (idempotent copy)", () => {
43
+ assert.equal(
44
+ deriveCopyMessageId(SOURCE, DEST),
45
+ deriveCopyMessageId(SOURCE, DEST),
46
+ );
47
+ });
48
+
49
+ it("separates a copy of one mail into two folders", () => {
50
+ assert.notEqual(
51
+ deriveCopyMessageId(SOURCE, DEST),
52
+ deriveCopyMessageId(SOURCE, "mbx-other"),
53
+ );
54
+ });
55
+
56
+ it("separates copies of two different mails into one folder", () => {
57
+ assert.notEqual(
58
+ deriveCopyMessageId(SOURCE, DEST),
59
+ deriveCopyMessageId(deriveMessageId(ACCOUNT, "<xyz@example.com>"), DEST),
60
+ );
61
+ });
62
+
63
+ it("never equals the folder-independent id sync derives", () => {
64
+ // Sync keys a message on deriveMessageId; the copy id folds in the
65
+ // destination mailbox, so the two can never collide.
66
+ assert.notEqual(deriveCopyMessageId(SOURCE, DEST), SOURCE);
67
+ });
68
+ });
69
+
33
70
  describe("quarantineMessageIdHash", () => {
34
71
  it("hashes a real Message-ID to a pinned sha256 value", () => {
35
72
  const hash = quarantineMessageIdHash("<abc@example.com>");
package/src/id.ts CHANGED
@@ -73,6 +73,29 @@ export const deriveMessageIdFromSource = (
73
73
  source: MessageIdSource,
74
74
  ): string => deriveMessageId(accountId, normalizeMessageIdHeader(source));
75
75
 
76
+ /**
77
+ * Identity of a message copied into another folder (issue #75). Derived from
78
+ * the source message and the destination mailbox so a copy is deterministic:
79
+ * copying the same mail into the same folder twice — a replayed COPY event or a
80
+ * repeated user copy — resolves to one row instead of a fresh unreachable
81
+ * duplicate each time. A random id, which is what the copy path used before,
82
+ * gave every attempt a new identity that no later sync or delete could reach.
83
+ *
84
+ * The destination mailbox is part of the name so a copy of one mail into two
85
+ * folders is two rows, one per folder. It also keeps this id out of the space
86
+ * sync assigns: sync keys a message on `deriveMessageId(accountId,
87
+ * messageIdHeader)`, which is folder-independent, so the copy row can never
88
+ * collide with, or be overwritten by, the canonical synced row.
89
+ */
90
+ export const deriveCopyMessageId = (
91
+ sourceMessageId: string,
92
+ destinationMailboxId: string,
93
+ ): string =>
94
+ base36uuidv5(
95
+ `messagecopy:${sourceMessageId}:${destinationMailboxId}`,
96
+ REMIT_NAMESPACE,
97
+ );
98
+
76
99
  export const deriveThreadId = (
77
100
  accountId: string,
78
101
  rootMessageIdHeader: string,
package/src/index.ts CHANGED
@@ -83,6 +83,7 @@ export type {
83
83
  PutMessagePlacementMoveInput,
84
84
  QuarantineItem,
85
85
  QuarantineMimeNodeItem,
86
+ QuarantineUpsertInput,
86
87
  RawMessageStorageItem,
87
88
  ResultList,
88
89
  SearchOptions,
@@ -1,4 +1,4 @@
1
- import type { QuarantineItem } from "../types.js";
1
+ import type { QuarantineItem, QuarantineUpsertInput } from "../types.js";
2
2
 
3
3
  export interface IQuarantineRepository {
4
4
  /**
@@ -7,4 +7,16 @@ export interface IQuarantineRepository {
7
7
  * surface and by a sync round that keeps it in memory.
8
8
  */
9
9
  listByAccountConfigId(accountConfigId: string): Promise<QuarantineItem[]>;
10
+
11
+ /**
12
+ * Record a message the sync path could not apply.
13
+ *
14
+ * Idempotent on the message's identity: the row is keyed by an id derived
15
+ * from (accountId, mailboxId, uidValidity, uid), so quarantining the same
16
+ * message twice rewrites one row instead of accumulating. The derivation
17
+ * happens here rather than at the call site — see `QuarantineUpsertInput`.
18
+ *
19
+ * A cursor may only move past a message once this has resolved.
20
+ */
21
+ upsert(input: QuarantineUpsertInput): Promise<void>;
10
22
  }
package/src/types.ts CHANGED
@@ -89,6 +89,21 @@ export type MessageFlagPushItem = z.infer<typeof MessageFlagPushSchema>;
89
89
  export type QuarantineItem = z.infer<typeof QuarantineSchema>;
90
90
  export type QuarantineMimeNodeItem = QuarantineItem["structure"][number];
91
91
 
92
+ /**
93
+ * What a caller supplies to quarantine a message.
94
+ *
95
+ * `quarantineId` is absent on purpose. The column carries a random-id default
96
+ * (the house pattern for every entity here), so an input that merely allowed
97
+ * the id would let a writer omit it, take a fresh random one on every retry,
98
+ * and turn the idempotent write this record depends on into an accumulating
99
+ * pile of rows that the derived-id lookup never finds. The repository derives
100
+ * it from the four fields that name the message, which are all required here.
101
+ */
102
+ export type QuarantineUpsertInput = Omit<
103
+ QuarantineItem,
104
+ "quarantineId" | "createdAt" | "updatedAt" | "structure"
105
+ > & { structure?: QuarantineMimeNodeItem[] };
106
+
92
107
  export type MailboxSpecialUseValue = z.infer<typeof MailboxSpecialUseSchema>;
93
108
 
94
109
  export type AccountSettingValue =