@remit/mailbox-service 2.0.3 → 2.1.1

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/index.d.ts CHANGED
@@ -41,7 +41,7 @@ export { denormalizeMessageCategory, type MessageCategoryDenormalizeDeps, type M
41
41
  export { type DeleteMessagesOutcome, type DeleteOptions, type MessageMoveConfig, type MessageMoveLogger, type MessageMoveQueueEvent, MessageMoveService, NoTrashMailboxError, requireTrashMailbox, StaleTrashAppointmentError, UnconfirmedTrashMailboxError, } from "./message-move.js";
42
42
  export { type ParsedMessageContent, parseMessageContent, } from "./message-parser.js";
43
43
  export { isMessageGoneFromOpenMailbox } from "./message-presence.js";
44
- export { type BodyPartMaterializer, type BodySyncCue, type MaterializeLogger, type MaterializeResult, materializeBodyParts, } from "./message-read.js";
44
+ export { type BodyPartMaterializer, type BodyReadMiss, type BodyReadMissDeps, type BodyReadMissInput, type BodySyncCue, type MaterializeLogger, type MaterializeResult, materializeBodyParts, resolveBodyReadMiss, } from "./message-read.js";
45
45
  export { type ImapConnectionFactory, MessageSyncService, type SyncedMessage, type SyncMessagesResult, } from "./message-sync.js";
46
46
  export { type OrganizeJobDeps, type OrganizeJobLogger, type OrganizeJobOwner, type OrganizeJobPredicate, type OrganizeJobRequest, queueOrganizeJob, } from "./organize-job.js";
47
47
  export { type CompleteOutboxAttachmentInput, type CompleteOutboxAttachmentOutcome, type MintOutboxAttachmentInput, type MintOutboxAttachmentOutcome, OUTBOX_ATTACHMENT_MAX_COUNT, OUTBOX_ATTACHMENT_MAX_TOTAL_BYTES, type OutboxAttachmentConfig, type OutboxAttachmentRejectionDetail, type OutboxAttachmentRejectionReasonValue, type OutboxAttachmentReservation, OutboxAttachmentService, } from "./outbox-attachment.js";
package/dist/index.js CHANGED
@@ -44,7 +44,7 @@ export { denormalizeMessageCategory, } from "./message-category.js";
44
44
  export { MessageMoveService, NoTrashMailboxError, requireTrashMailbox, StaleTrashAppointmentError, UnconfirmedTrashMailboxError, } from "./message-move.js";
45
45
  export { parseMessageContent, } from "./message-parser.js";
46
46
  export { isMessageGoneFromOpenMailbox } from "./message-presence.js";
47
- export { materializeBodyParts, } from "./message-read.js";
47
+ export { materializeBodyParts, resolveBodyReadMiss, } from "./message-read.js";
48
48
  export { MessageSyncService, } from "./message-sync.js";
49
49
  export { queueOrganizeJob, } from "./organize-job.js";
50
50
  export { OUTBOX_ATTACHMENT_MAX_COUNT, OUTBOX_ATTACHMENT_MAX_TOTAL_BYTES, OutboxAttachmentService, } from "./outbox-attachment.js";
@@ -1,3 +1,5 @@
1
+ import type { IEnvelopeRepository, IMailboxRepository, IMessageRepository } from "@remit/data-ports";
2
+ import { type StorageService } from "@remit/storage-service";
1
3
  /**
2
4
  * The slice of `BodySyncService` the read path needs to lazily materialize
3
5
  * deferred per-part objects.
@@ -49,3 +51,33 @@ export declare const materializeBodyParts: (materializer: BodyPartMaterializer,
49
51
  cue: BodySyncCue;
50
52
  logger: MaterializeLogger;
51
53
  }) => Promise<MaterializeResult>;
54
+ export type BodyReadMiss = "NotFound" | "Broken" | "Pending";
55
+ export interface BodyReadMissDeps {
56
+ message: Pick<IMessageRepository, "get">;
57
+ mailbox: Pick<IMailboxRepository, "get">;
58
+ envelope: Pick<IEnvelopeRepository, "listBodyParts">;
59
+ storage: Pick<StorageService, "bodyPartExists" | "deleteKeys">;
60
+ }
61
+ export interface BodyReadMissInput {
62
+ accountConfigId: string;
63
+ accountId: string;
64
+ messageId: string;
65
+ partPath: string;
66
+ }
67
+ /**
68
+ * What a content read that found no object at a part key should answer. It
69
+ * never enqueues: the describe read path (`materializeBodyParts`) re-arms the
70
+ * body sync, and the client re-describes on every 202.
71
+ *
72
+ * - `NotFound` — the message, its mailbox under `accountId`, or a leaf at
73
+ * `partPath` does not exist; no retry can produce the object.
74
+ * - `Broken` — the body sync exhausted its retries against a message the
75
+ * server still holds (`isMessageBodySyncBroken`).
76
+ * - `Pending` — anything else; a retry after the next describe succeeds.
77
+ *
78
+ * A missing leaf under a present `.materialized` sentinel means the sentinel
79
+ * no longer vouches for its parts, and `ensureBodyPartsStored` would skip the
80
+ * message forever. The sentinel is dropped so the next describe materializes
81
+ * the parts again; that delete is the one write here.
82
+ */
83
+ export declare const resolveBodyReadMiss: (deps: BodyReadMissDeps, { accountConfigId, accountId, messageId, partPath }: BodyReadMissInput) => Promise<BodyReadMiss>;
@@ -1,4 +1,6 @@
1
- import { isStorageNotFoundError } from "@remit/storage-service";
1
+ import { isEncapsulatedMessage } from "@remit/data-ports/message-parts";
2
+ import { buildBodyPartKey, isStorageNotFoundError, MATERIALIZED_SENTINEL_PART_PATH, MATERIALIZED_SENTINEL_V2_PART_PATH, } from "@remit/storage-service";
3
+ import { isMessageBodySyncBroken } from "./body-sync-terminal.js";
2
4
  /**
3
5
  * Materialize the deferred per-part storage objects for a message before any
4
6
  * part is read. Distinguishes the two failure modes the body contract cares
@@ -29,3 +31,45 @@ export const materializeBodyParts = async (materializer, args) => {
29
31
  return { ready: false };
30
32
  }
31
33
  };
34
+ /**
35
+ * What a content read that found no object at a part key should answer. It
36
+ * never enqueues: the describe read path (`materializeBodyParts`) re-arms the
37
+ * body sync, and the client re-describes on every 202.
38
+ *
39
+ * - `NotFound` — the message, its mailbox under `accountId`, or a leaf at
40
+ * `partPath` does not exist; no retry can produce the object.
41
+ * - `Broken` — the body sync exhausted its retries against a message the
42
+ * server still holds (`isMessageBodySyncBroken`).
43
+ * - `Pending` — anything else; a retry after the next describe succeeds.
44
+ *
45
+ * A missing leaf under a present `.materialized` sentinel means the sentinel
46
+ * no longer vouches for its parts, and `ensureBodyPartsStored` would skip the
47
+ * message forever. The sentinel is dropped so the next describe materializes
48
+ * the parts again; that delete is the one write here.
49
+ */
50
+ export const resolveBodyReadMiss = async (deps, { accountConfigId, accountId, messageId, partPath }) => {
51
+ const [message] = await deps.message.get([messageId]);
52
+ if (!message)
53
+ return "NotFound";
54
+ const [mailbox] = await deps.mailbox.get(accountId, [message.mailboxId]);
55
+ if (!mailbox)
56
+ return "NotFound";
57
+ if (await isMessageBodySyncBroken(deps.storage, accountConfigId, accountId, messageId)) {
58
+ return "Broken";
59
+ }
60
+ const bodyParts = await deps.envelope.listBodyParts(messageId);
61
+ const leaf = bodyParts.find((part) => part.partPath === partPath && !part.isMultipart);
62
+ if (!leaf)
63
+ return "NotFound";
64
+ if (!message.bodyStorageKey)
65
+ return "Pending";
66
+ const sentinel = bodyParts.some(isEncapsulatedMessage)
67
+ ? MATERIALIZED_SENTINEL_V2_PART_PATH
68
+ : MATERIALIZED_SENTINEL_PART_PATH;
69
+ if (await deps.storage.bodyPartExists(accountConfigId, accountId, messageId, sentinel)) {
70
+ await deps.storage.deleteKeys([
71
+ buildBodyPartKey(accountConfigId, accountId, messageId, sentinel),
72
+ ]);
73
+ }
74
+ return "Pending";
75
+ };
@@ -10,7 +10,8 @@ import type { OutboxMessageItem } from "@remit/data-ports";
10
10
  * queued the same envelope, and Edit took a 409 on the flush that precedes the
11
11
  * send (#933).
12
12
  *
13
- * Everything absent is in the worker's hands or settled: `queued` and `sending`
14
- * are mid-flight, `sent` and `unfiled` were delivered.
13
+ * `unfiled` may have been delivered, so it is open only to a deliberate edit
14
+ * and send; nothing re-sends it on its own. `queued` and `sending` are
15
+ * mid-flight and `sent` was delivered.
15
16
  */
16
17
  export declare const isOpenForWork: (status: OutboxMessageItem["status"]) => boolean;
@@ -10,9 +10,11 @@ import { OutboxMessageStatus } from "@remit/domain-enums";
10
10
  * queued the same envelope, and Edit took a 409 on the flush that precedes the
11
11
  * send (#933).
12
12
  *
13
- * Everything absent is in the worker's hands or settled: `queued` and `sending`
14
- * are mid-flight, `sent` and `unfiled` were delivered.
13
+ * `unfiled` may have been delivered, so it is open only to a deliberate edit
14
+ * and send; nothing re-sends it on its own. `queued` and `sending` are
15
+ * mid-flight and `sent` was delivered.
15
16
  */
16
17
  export const isOpenForWork = (status) => status === OutboxMessageStatus.draft ||
17
18
  status === OutboxMessageStatus.failed ||
18
- status === OutboxMessageStatus.blocked;
19
+ status === OutboxMessageStatus.blocked ||
20
+ status === OutboxMessageStatus.unfiled;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/mailbox-service",
3
- "version": "2.0.3",
3
+ "version": "2.1.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",