@remit/imap-worker 0.0.90 → 0.0.91

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 (172) hide show
  1. package/dist/account-check.d.ts +27 -0
  2. package/dist/account-check.js +70 -0
  3. package/dist/body-sync-gate.d.ts +12 -0
  4. package/dist/body-sync-gate.js +53 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +193 -0
  7. package/dist/config-env.d.ts +5 -0
  8. package/dist/config-env.js +37 -0
  9. package/dist/connection-pool.d.ts +22 -0
  10. package/dist/connection-pool.js +97 -0
  11. package/dist/connection-scope.d.ts +8 -0
  12. package/dist/connection-scope.js +10 -0
  13. package/dist/context.d.ts +46 -0
  14. package/dist/context.js +15 -0
  15. package/dist/e2e-processor-shim.d.ts +2 -0
  16. package/dist/e2e-processor-shim.js +181 -0
  17. package/dist/emit.d.ts +15 -0
  18. package/dist/emit.js +76 -0
  19. package/dist/events.d.ts +203 -0
  20. package/dist/events.js +1 -0
  21. package/dist/failure-kind.d.ts +10 -0
  22. package/{src/failure-kind.ts → dist/failure-kind.js} +6 -6
  23. package/dist/handlers/append-sent-message.d.ts +3 -0
  24. package/dist/handlers/append-sent-message.js +152 -0
  25. package/dist/handlers/delete-account-objects.d.ts +3 -0
  26. package/dist/handlers/delete-account-objects.js +35 -0
  27. package/dist/handlers/empty-trash.d.ts +14 -0
  28. package/dist/handlers/empty-trash.js +217 -0
  29. package/dist/handlers/flag-push.d.ts +38 -0
  30. package/dist/handlers/flag-push.js +195 -0
  31. package/dist/handlers/imported-folder.d.ts +4 -0
  32. package/dist/handlers/imported-folder.js +23 -0
  33. package/dist/handlers/mailbox-management.d.ts +6 -0
  34. package/dist/handlers/mailbox-management.js +294 -0
  35. package/dist/handlers/mailbox-sync-order.d.ts +21 -0
  36. package/dist/handlers/mailbox-sync-order.js +50 -0
  37. package/dist/handlers/message-copy.d.ts +40 -0
  38. package/dist/handlers/message-copy.js +371 -0
  39. package/dist/handlers/message-delete-terminal.d.ts +60 -0
  40. package/dist/handlers/message-delete-terminal.js +77 -0
  41. package/dist/handlers/message-delete.d.ts +57 -0
  42. package/dist/handlers/message-delete.js +520 -0
  43. package/{src/handlers/message-move-terminal.ts → dist/handlers/message-move-terminal.d.ts} +18 -105
  44. package/dist/handlers/message-move-terminal.js +94 -0
  45. package/dist/handlers/message-move.d.ts +146 -0
  46. package/dist/handlers/message-move.js +490 -0
  47. package/dist/handlers/placement-move-push.d.ts +62 -0
  48. package/dist/handlers/placement-move-push.js +307 -0
  49. package/dist/handlers/probe-message-id.d.ts +2 -0
  50. package/dist/handlers/probe-message-id.js +6 -0
  51. package/dist/handlers/sync-mailboxes.d.ts +45 -0
  52. package/dist/handlers/sync-mailboxes.js +233 -0
  53. package/dist/handlers/sync-message-body.d.ts +23 -0
  54. package/dist/handlers/sync-message-body.js +182 -0
  55. package/dist/handlers/sync-messages.d.ts +29 -0
  56. package/dist/handlers/sync-messages.js +285 -0
  57. package/dist/index.d.ts +9 -0
  58. package/dist/index.js +67 -0
  59. package/dist/lambda-context.d.ts +8 -0
  60. package/dist/lambda-context.js +33 -0
  61. package/dist/mailbox-row.d.ts +9 -0
  62. package/dist/mailbox-row.js +6 -0
  63. package/dist/poller.d.ts +1 -0
  64. package/dist/poller.js +51 -0
  65. package/dist/process.d.ts +6 -0
  66. package/dist/process.js +3 -0
  67. package/dist/processor.d.ts +3 -0
  68. package/dist/processor.js +44 -0
  69. package/dist/queues.d.ts +10 -0
  70. package/dist/queues.js +1 -0
  71. package/dist/s3-account-objects.d.ts +3 -0
  72. package/dist/s3-account-objects.js +26 -0
  73. package/dist/scheduler/attachment-sweep.d.ts +11 -0
  74. package/dist/scheduler/attachment-sweep.js +23 -0
  75. package/dist/scheduler/calendar-subscriptions.d.ts +26 -0
  76. package/dist/scheduler/calendar-subscriptions.js +35 -0
  77. package/dist/scheduler/config.d.ts +27 -0
  78. package/{src/scheduler/config.ts → dist/scheduler/config.js} +7 -26
  79. package/{src/scheduler/decide-due.ts → dist/scheduler/decide-due.d.ts} +3 -12
  80. package/dist/scheduler/decide-due.js +17 -0
  81. package/dist/scheduler/handler.d.ts +21 -0
  82. package/{src/scheduler/handler.ts → dist/scheduler/handler.js} +25 -42
  83. package/{src/scheduler/loop.ts → dist/scheduler/loop.d.ts} +7 -23
  84. package/dist/scheduler/loop.js +8 -0
  85. package/dist/scheduler/run-tick.d.ts +62 -0
  86. package/dist/scheduler/run-tick.js +164 -0
  87. package/dist/scheduler/runner.d.ts +2 -0
  88. package/{src/scheduler/runner.ts → dist/scheduler/runner.js} +57 -78
  89. package/dist/scheduler/stranded-placement-redrive.d.ts +16 -0
  90. package/dist/scheduler/stranded-placement-redrive.js +172 -0
  91. package/dist/sqs-queues.d.ts +4 -0
  92. package/dist/sqs-queues.js +22 -0
  93. package/dist/with-oauth-lifecycle-deps.d.ts +5 -0
  94. package/dist/with-oauth-lifecycle-deps.js +17 -0
  95. package/dist/with-oauth-lifecycle.d.ts +56 -0
  96. package/dist/with-oauth-lifecycle.js +92 -0
  97. package/{src/worker-version.ts → dist/worker-version.d.ts} +1 -2
  98. package/dist/worker-version.js +12 -0
  99. package/package.json +75 -69
  100. package/src/account-check.test.ts +0 -149
  101. package/src/account-check.ts +0 -101
  102. package/src/body-sync-gate.test.ts +0 -185
  103. package/src/body-sync-gate.ts +0 -86
  104. package/src/cli.ts +0 -218
  105. package/src/connection-scope.test.ts +0 -266
  106. package/src/connection-scope.ts +0 -314
  107. package/src/credential-less-sync-fence.test.ts +0 -152
  108. package/src/e2e-processor-shim.ts +0 -232
  109. package/src/emit.test.ts +0 -145
  110. package/src/emit.ts +0 -115
  111. package/src/events.ts +0 -252
  112. package/src/failure-kind.test.ts +0 -42
  113. package/src/handlers/append-sent-message.sqlite.test.ts +0 -176
  114. package/src/handlers/append-sent-message.ts +0 -264
  115. package/src/handlers/delete-account-objects.test.ts +0 -126
  116. package/src/handlers/delete-account-objects.ts +0 -96
  117. package/src/handlers/empty-trash.sqlite.test.ts +0 -209
  118. package/src/handlers/empty-trash.ts +0 -352
  119. package/src/handlers/flag-push.ts +0 -285
  120. package/src/handlers/imported-folder.test.ts +0 -63
  121. package/src/handlers/imported-folder.ts +0 -38
  122. package/src/handlers/mailbox-management.sqlite.test.ts +0 -265
  123. package/src/handlers/mailbox-management.ts +0 -486
  124. package/src/handlers/mailbox-sync-order.test.ts +0 -93
  125. package/src/handlers/mailbox-sync-order.ts +0 -65
  126. package/src/handlers/message-copy.sqlite.test.ts +0 -298
  127. package/src/handlers/message-copy.ts +0 -569
  128. package/src/handlers/message-delete-terminal.ts +0 -147
  129. package/src/handlers/message-delete.sqlite.test.ts +0 -291
  130. package/src/handlers/message-delete.ts +0 -888
  131. package/src/handlers/message-move.sqlite.test.ts +0 -271
  132. package/src/handlers/message-move.test.ts +0 -216
  133. package/src/handlers/message-move.ts +0 -755
  134. package/src/handlers/placement-move-push.test.ts +0 -137
  135. package/src/handlers/placement-move-push.ts +0 -478
  136. package/src/handlers/placement-push.sqlite.test.ts +0 -299
  137. package/src/handlers/probe-message-id.sqlite.test.ts +0 -192
  138. package/src/handlers/probe-message-id.ts +0 -15
  139. package/src/handlers/sync-mailboxes.test.ts +0 -183
  140. package/src/handlers/sync-mailboxes.ts +0 -425
  141. package/src/handlers/sync-message-body.sqlite.test.ts +0 -314
  142. package/src/handlers/sync-message-body.test.ts +0 -150
  143. package/src/handlers/sync-message-body.ts +0 -366
  144. package/src/handlers/sync-messages.sqlite.test.ts +0 -195
  145. package/src/handlers/sync-messages.ts +0 -537
  146. package/src/index.test.ts +0 -22
  147. package/src/index.ts +0 -82
  148. package/src/mail-sync-selection.sqlite.test.ts +0 -227
  149. package/src/mailbox-row.ts +0 -20
  150. package/src/pending-sync.sqlite.test.ts +0 -165
  151. package/src/poller.ts +0 -55
  152. package/src/processor.ts +0 -62
  153. package/src/queues.ts +0 -40
  154. package/src/scheduler/attachment-sweep.sqlite.test.ts +0 -74
  155. package/src/scheduler/attachment-sweep.ts +0 -46
  156. package/src/scheduler/calendar-subscriptions.sqlite.test.ts +0 -147
  157. package/src/scheduler/calendar-subscriptions.ts +0 -103
  158. package/src/scheduler/config.test.ts +0 -40
  159. package/src/scheduler/decide-due.test.ts +0 -44
  160. package/src/scheduler/half-deleted-accounts.sqlite.test.ts +0 -196
  161. package/src/scheduler/loop.test.ts +0 -143
  162. package/src/scheduler/run-tick.sqlite.test.ts +0 -191
  163. package/src/scheduler/run-tick.ts +0 -281
  164. package/src/scheduler/stranded-placement-redrive.ts +0 -265
  165. package/src/stranded-placement.sqlite.test.ts +0 -799
  166. package/src/test-helpers/test-world.ts +0 -425
  167. package/src/test-helpers/worker-queues.ts +0 -11
  168. package/src/thread-join.sqlite.test.ts +0 -106
  169. package/src/with-oauth-lifecycle-deps.sqlite.test.ts +0 -269
  170. package/src/with-oauth-lifecycle-deps.ts +0 -71
  171. package/src/with-oauth-lifecycle.ts +0 -158
  172. package/tsconfig.json +0 -8
@@ -1,147 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import type { CalendarFeedFetcher } from "@remit/calendar-service";
4
- import type { CalendarCollectionItem } from "@remit/data-ports";
5
- import { CalendarSource } from "@remit/domain-enums";
6
- import type { Logger } from "@remit/logger";
7
- import { openStore } from "../test-helpers/test-world.js";
8
- import { refreshDueCalendarSubscriptions } from "./calendar-subscriptions.js";
9
-
10
- const INTERVAL_MS = 15 * 60 * 1000;
11
- const NOW = 1_800_000_000_000;
12
-
13
- const FEED = `${[
14
- "BEGIN:VCALENDAR",
15
- "VERSION:2.0",
16
- "PRODID:-//Example//Published Calendar//EN",
17
- "BEGIN:VEVENT",
18
- "UID:ferry@calendar.example",
19
- "DTSTAMP:20260901T000000Z",
20
- "DTSTART:20260908T070000Z",
21
- "DTEND:20260908T080000Z",
22
- "SUMMARY:Ferry crossing",
23
- "END:VEVENT",
24
- "END:VCALENDAR",
25
- ].join("\r\n")}\r\n`;
26
-
27
- const logged = (): { log: Logger; lines: string[] } => {
28
- const lines: string[] = [];
29
- const capture = (...args: unknown[]) => {
30
- lines.push(JSON.stringify(args));
31
- };
32
- const log = {
33
- info: capture,
34
- warn: capture,
35
- error: capture,
36
- debug: capture,
37
- fatal: capture,
38
- trace: capture,
39
- child: () => log,
40
- } as unknown as Logger;
41
- return { log, lines };
42
- };
43
-
44
- type Client = Awaited<ReturnType<typeof openStore>>["client"];
45
-
46
- const subscription = async (
47
- client: Client,
48
- urlSegment: string,
49
- checkedAt: number,
50
- ): Promise<CalendarCollectionItem> => {
51
- const created = await client.calendarCollection.create({
52
- accountConfigId: "account-config-1",
53
- urlSegment,
54
- displayName: urlSegment,
55
- source: CalendarSource.Subscribed,
56
- subscriptionUrl: `https://calendar.example/${urlSegment}/private-feedfacecafe.ics`,
57
- subscriptionEnabled: true,
58
- });
59
- return client.calendarCollection.update(
60
- created.accountConfigId,
61
- created.calendarId,
62
- { subscriptionCheckedAt: checkedAt },
63
- );
64
- };
65
-
66
- const recording =
67
- (asked: string[], answer: (url: string) => Response): CalendarFeedFetcher =>
68
- async (url) => {
69
- asked.push(url);
70
- return answer(url);
71
- };
72
-
73
- describe("refreshDueCalendarSubscriptions", () => {
74
- it("fetches the subscriptions that are due and skips the rest", async (t) => {
75
- const { client, close } = await openStore();
76
- t.after(close);
77
- const due = await subscription(client, "due", NOW - INTERVAL_MS);
78
- await subscription(client, "fresh", NOW - 60_000);
79
- const paused = await subscription(client, "paused", 0);
80
- await client.calendarCollection.update(
81
- paused.accountConfigId,
82
- paused.calendarId,
83
- { subscriptionEnabled: false },
84
- );
85
- const asked: string[] = [];
86
-
87
- const tally = await refreshDueCalendarSubscriptions(
88
- {
89
- calendarCollection: client.calendarCollection,
90
- calendarStore: client.calendarStore,
91
- log: logged().log,
92
- fetcher: recording(asked, () => new Response(FEED, { status: 200 })),
93
- },
94
- NOW,
95
- INTERVAL_MS,
96
- );
97
-
98
- assert.deepEqual(asked, [due.subscriptionUrl]);
99
- assert.deepEqual(tally, { refreshed: 1, failed: 0, notDue: 1 });
100
- const objects = await client.calendarObject.listByCalendar(due.calendarId);
101
- assert.deepEqual(
102
- objects.map((object) => object.summary),
103
- ["Ferry crossing"],
104
- );
105
- });
106
-
107
- it("counts a feed that fails, keeps refreshing the others, and never logs an address", async (t) => {
108
- const { client, close } = await openStore();
109
- t.after(close);
110
- const broken = await subscription(client, "broken", 0);
111
- const working = await subscription(client, "working", 0);
112
- const { log, lines } = logged();
113
-
114
- const tally = await refreshDueCalendarSubscriptions(
115
- {
116
- calendarCollection: client.calendarCollection,
117
- calendarStore: client.calendarStore,
118
- log,
119
- fetcher: recording([], (url) =>
120
- url === broken.subscriptionUrl
121
- ? new Response("gone", { status: 410 })
122
- : new Response(FEED, { status: 200 }),
123
- ),
124
- },
125
- NOW,
126
- INTERVAL_MS,
127
- );
128
-
129
- assert.deepEqual(tally, { refreshed: 1, failed: 1, notDue: 0 });
130
- const stored = await client.calendarCollection.get(
131
- broken.accountConfigId,
132
- broken.calendarId,
133
- );
134
- assert.equal(stored.subscriptionError, "the feed answered HTTP 410");
135
- assert.equal(stored.subscriptionCheckedAt, NOW);
136
- const refreshed = await client.calendarCollection.get(
137
- working.accountConfigId,
138
- working.calendarId,
139
- );
140
- assert.equal(refreshed.subscriptionFetchedAt, NOW);
141
- assert.ok(lines.some((line) => line.includes(broken.calendarId)));
142
- assert.equal(
143
- lines.some((line) => line.includes("private-feedfacecafe")),
144
- false,
145
- );
146
- });
147
- });
@@ -1,103 +0,0 @@
1
- import type { RemitClient } from "@remit/backend/client";
2
- import {
3
- type CalendarFeedFetcher,
4
- isSubscriptionDue,
5
- refreshCalendarSubscription,
6
- } from "@remit/calendar-service";
7
- import type {
8
- ICalendarCollectionRepository,
9
- ICalendarStore,
10
- } from "@remit/data-ports";
11
- import type { Logger } from "@remit/logger";
12
- import pMap from "p-map";
13
-
14
- const REFRESH_CONCURRENCY = 4;
15
-
16
- export interface CalendarSubscriptionTally {
17
- refreshed: number;
18
- failed: number;
19
- notDue: number;
20
- }
21
-
22
- export interface CalendarSubscriptionRefreshDeps {
23
- calendarCollection: Pick<
24
- ICalendarCollectionRepository,
25
- "listEnabledSubscriptions"
26
- >;
27
- calendarStore: ICalendarStore;
28
- log: Logger;
29
- fetcher?: CalendarFeedFetcher;
30
- }
31
-
32
- /**
33
- * Re-reads every enabled subscription whose last fetch is at least
34
- * `intervalMs` old. A disabled subscription is never listed, which is what
35
- * stops its refresh while leaving its events in place.
36
- *
37
- * One feed's failure is recorded on its own collection and counted here; it
38
- * never stops the others. The log names the calendar and the reason, never the
39
- * URL, which for a Google secret address is the whole credential.
40
- */
41
- export const refreshDueCalendarSubscriptions = async (
42
- deps: CalendarSubscriptionRefreshDeps,
43
- now: number,
44
- intervalMs: number,
45
- ): Promise<CalendarSubscriptionTally> => {
46
- const enabled = await deps.calendarCollection.listEnabledSubscriptions();
47
- const due = enabled.filter((collection) =>
48
- isSubscriptionDue(collection, now, intervalMs),
49
- );
50
-
51
- const outcomes = await pMap(
52
- due,
53
- (collection) =>
54
- refreshCalendarSubscription(
55
- deps.calendarStore,
56
- collection,
57
- now,
58
- deps.fetcher,
59
- ).then(
60
- (refresh) => {
61
- if (refresh.ok) return true;
62
- deps.log.warn(
63
- { calendarId: collection.calendarId, reason: refresh.reason },
64
- "Calendar subscription could not be refreshed",
65
- );
66
- return false;
67
- },
68
- (error: unknown) => {
69
- deps.log.error(
70
- { calendarId: collection.calendarId, error },
71
- "Calendar subscription refresh failed",
72
- );
73
- return false;
74
- },
75
- ),
76
- { concurrency: REFRESH_CONCURRENCY },
77
- );
78
-
79
- return {
80
- refreshed: outcomes.filter(Boolean).length,
81
- failed: outcomes.filter((ok) => !ok).length,
82
- notDue: enabled.length - due.length,
83
- };
84
- };
85
-
86
- export const buildCalendarSubscriptionRefresh =
87
- (
88
- client: Pick<RemitClient, "calendarCollection" | "calendarStore">,
89
- log: Logger,
90
- ): ((
91
- now: number,
92
- intervalMs: number,
93
- ) => Promise<CalendarSubscriptionTally>) =>
94
- (now, intervalMs) =>
95
- refreshDueCalendarSubscriptions(
96
- {
97
- calendarCollection: client.calendarCollection,
98
- calendarStore: client.calendarStore,
99
- log,
100
- },
101
- now,
102
- intervalMs,
103
- );
@@ -1,40 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { getOfflineIntervalMs, getTickIntervalMs } from "./config.js";
4
-
5
- describe("getTickIntervalMs", () => {
6
- it("defaults to 1 hour when unset", () => {
7
- assert.equal(getTickIntervalMs({}), 60 * 60 * 1000);
8
- });
9
-
10
- it("reads MAILBOX_SYNC_TICK_INTERVAL_SECONDS", () => {
11
- assert.equal(
12
- getTickIntervalMs({ MAILBOX_SYNC_TICK_INTERVAL_SECONDS: "60" }),
13
- 60_000,
14
- );
15
- });
16
-
17
- it("falls back to the default on a non-positive or non-numeric value", () => {
18
- assert.equal(
19
- getTickIntervalMs({ MAILBOX_SYNC_TICK_INTERVAL_SECONDS: "0" }),
20
- 60 * 60 * 1000,
21
- );
22
- assert.equal(
23
- getTickIntervalMs({ MAILBOX_SYNC_TICK_INTERVAL_SECONDS: "nope" }),
24
- 60 * 60 * 1000,
25
- );
26
- });
27
- });
28
-
29
- describe("getOfflineIntervalMs", () => {
30
- it("defaults to 12 hours when unset", () => {
31
- assert.equal(getOfflineIntervalMs({}), 12 * 60 * 60 * 1000);
32
- });
33
-
34
- it("reads MAILBOX_SYNC_OFFLINE_INTERVAL_SECONDS", () => {
35
- assert.equal(
36
- getOfflineIntervalMs({ MAILBOX_SYNC_OFFLINE_INTERVAL_SECONDS: "3600" }),
37
- 3_600_000,
38
- );
39
- });
40
- });
@@ -1,44 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { isSyncDue } from "./decide-due.js";
4
-
5
- const OFFLINE_INTERVAL_MS = 12 * 60 * 60 * 1000;
6
- const NOW = 1_700_000_000_000;
7
-
8
- describe("isSyncDue", () => {
9
- it("is always due when the account has never synced", () => {
10
- assert.equal(isSyncDue({ accountId: "a" }, NOW, OFFLINE_INTERVAL_MS), true);
11
- });
12
-
13
- it("is due once the sync is older than the offline interval", () => {
14
- const account = {
15
- accountId: "a",
16
- lastSyncAt: NOW - OFFLINE_INTERVAL_MS - 1,
17
- };
18
- assert.equal(isSyncDue(account, NOW, OFFLINE_INTERVAL_MS), true);
19
- });
20
-
21
- it("is not due within the offline interval", () => {
22
- const account = {
23
- accountId: "a",
24
- lastSyncAt: NOW - 60_000,
25
- };
26
- assert.equal(isSyncDue(account, NOW, OFFLINE_INTERVAL_MS), false);
27
- });
28
-
29
- it("is due exactly at the threshold", () => {
30
- const account = {
31
- accountId: "a",
32
- lastSyncAt: NOW - OFFLINE_INTERVAL_MS,
33
- };
34
- assert.equal(isSyncDue(account, NOW, OFFLINE_INTERVAL_MS), true);
35
- });
36
-
37
- it("is not due one millisecond before the threshold", () => {
38
- const account = {
39
- accountId: "a",
40
- lastSyncAt: NOW - OFFLINE_INTERVAL_MS + 1,
41
- };
42
- assert.equal(isSyncDue(account, NOW, OFFLINE_INTERVAL_MS), false);
43
- });
44
- });
@@ -1,196 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { randomBytes } from "node:crypto";
3
- import { after, before, beforeEach, describe, it } from "node:test";
4
- import { finishHalfDeletedAccounts } from "@remit/backend/account-deletion";
5
- import type { RemitClient } from "@remit/backend/client";
6
- import { importConfig } from "@remit/config-transfer";
7
- import type { AccountItem } from "@remit/data-ports";
8
- import {
9
- failingQueue,
10
- type RecordingQueue,
11
- recordingQueue,
12
- } from "@remit/data-ports/recording-queue";
13
- import { AccountAuthType, ConnectionState } from "@remit/domain-enums";
14
- import { noopLogger } from "@remit/logger";
15
- import { openStore } from "../test-helpers/test-world.js";
16
-
17
- let store: Awaited<ReturnType<typeof openStore>>;
18
- let client: RemitClient;
19
-
20
- before(async () => {
21
- store = await openStore();
22
- client = store.client;
23
- });
24
-
25
- after(() => store.close());
26
-
27
- const storedId = (): string => randomBytes(13).toString("hex").slice(0, 25);
28
-
29
- const configDocument = (accountId: string, email: string) => ({
30
- kind: "reader.config",
31
- schemaVersion: 1,
32
- generator: {
33
- app: "reader",
34
- version: "v0.1.0",
35
- exportedAt: "2026-08-27T09:15:00+02:00",
36
- },
37
- provenance: { accountConfigId: storedId(), instance: "reader.example.com" },
38
- accountConfig: { name: "Imported" },
39
- accounts: [
40
- {
41
- accountId,
42
- email,
43
- username: email,
44
- authType: "password",
45
- credentials: { required: "password" },
46
- isActive: true,
47
- imap: { host: "imap.example.com", port: 993, tls: true, startTls: false },
48
- smtp: {
49
- enabled: false,
50
- host: "",
51
- port: 587,
52
- tls: false,
53
- startTls: false,
54
- username: "",
55
- },
56
- displayName: "",
57
- muted: null,
58
- composeLanguages: [],
59
- signature: { plainText: "", html: "" },
60
- folderRoles: [],
61
- folderOverrides: [],
62
- pinnedFolders: [],
63
- },
64
- ],
65
- labels: [],
66
- filters: [],
67
- addressFlags: [],
68
- });
69
-
70
- const seedAccount = (
71
- accountConfigId: string,
72
- name: string,
73
- extra: { deletedAt?: number } = {},
74
- ): Promise<AccountItem> =>
75
- client.account.create({
76
- accountConfigId,
77
- username: `${name}@example.com`,
78
- email: `${name}@example.com`,
79
- authType: AccountAuthType.Password,
80
- passwordHash: '{"ciphertext":"x"}',
81
- imapHost: "imap.example.com",
82
- imapPort: 993,
83
- imapTls: true,
84
- imapStartTls: false,
85
- isActive: true,
86
- connectionState: ConnectionState.Authenticated,
87
- ...extra,
88
- });
89
-
90
- const importAccount = async (email: string): Promise<AccountItem> => {
91
- const accountConfigId = storedId();
92
- const accountId = storedId();
93
- const outcome = await importConfig(
94
- {
95
- repositories: client,
96
- appointFolderRole: async () => {},
97
- },
98
- {
99
- accountConfigId,
100
- userId: accountConfigId,
101
- document: configDocument(accountId, email),
102
- mode: "apply",
103
- onExisting: "abort",
104
- },
105
- );
106
- assert.equal(outcome.outcome, "report");
107
- return client.account.get(accountId);
108
- };
109
-
110
- const heal = (queue: RecordingQueue) =>
111
- finishHalfDeletedAccounts({
112
- account: client.account,
113
- configImport: client.configImport,
114
- queue,
115
- log: noopLogger,
116
- });
117
-
118
- describe("finishing an account deleted before #1384 on scheduler start", () => {
119
- let halfDeleted: AccountItem;
120
- let live: AccountItem;
121
- let deleted: AccountItem;
122
- let imported: AccountItem;
123
-
124
- beforeEach(async () => {
125
- const accountConfigId = storedId();
126
- live = await seedAccount(accountConfigId, `live-${storedId()}`);
127
- halfDeleted = await seedAccount(accountConfigId, `gone-${storedId()}`);
128
- await client.account.update(halfDeleted.accountId, { isActive: false });
129
- deleted = await seedAccount(accountConfigId, `deleted-${storedId()}`, {
130
- deletedAt: 1_700_000_000_000,
131
- });
132
- await client.account.update(deleted.accountId, { isActive: false });
133
- imported = await importAccount(`imported-${storedId()}@example.com`);
134
- });
135
-
136
- const allAccounts = () =>
137
- client.account.get([
138
- halfDeleted.accountId,
139
- live.accountId,
140
- deleted.accountId,
141
- imported.accountId,
142
- ]);
143
-
144
- it("tombstones it and asks for the same purge a delete asks for", async () => {
145
- const queue = recordingQueue();
146
-
147
- await heal(queue);
148
-
149
- const purges = queue.sent.map((message) => message.body);
150
- assert.ok(
151
- purges.some(
152
- (body) =>
153
- body.type === "AccountDataPurge" &&
154
- body.accountId === halfDeleted.accountId &&
155
- body.accountConfigId === halfDeleted.accountConfigId,
156
- ),
157
- );
158
- const byId = new Map(
159
- (await allAccounts()).map((account) => [account.accountId, account]),
160
- );
161
- assert.ok(byId.get(halfDeleted.accountId)?.deletedAt);
162
- assert.equal(byId.get(live.accountId)?.deletedAt, undefined);
163
- assert.equal(byId.get(imported.accountId)?.deletedAt, undefined);
164
- assert.equal(byId.get(deleted.accountId)?.deletedAt, 1_700_000_000_000);
165
- assert.ok(
166
- !purges.some((body) =>
167
- [live.accountId, imported.accountId, deleted.accountId].includes(
168
- String(body.accountId),
169
- ),
170
- ),
171
- );
172
- });
173
-
174
- it("does nothing for it on the next start", async () => {
175
- await heal(recordingQueue());
176
- const queue = recordingQueue();
177
-
178
- await heal(queue);
179
-
180
- assert.ok(
181
- !queue.sent.some(
182
- (message) => message.body.accountId === halfDeleted.accountId,
183
- ),
184
- );
185
- });
186
-
187
- it("fails the start and leaves it to be found again when the purge cannot be asked for", async () => {
188
- await assert.rejects(
189
- heal(failingQueue(new Error("queue unreachable"))),
190
- /queue unreachable/,
191
- );
192
-
193
- const after = await client.account.get(halfDeleted.accountId);
194
- assert.equal(after.deletedAt, undefined);
195
- });
196
- });
@@ -1,143 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { runSchedulerLoop, type SchedulerLoopOptions } from "./loop.js";
4
- import type { RunSchedulerTickDeps, SchedulerTickResult } from "./run-tick.js";
5
-
6
- const RESULT: SchedulerTickResult = {
7
- scanned: 0,
8
- enqueued: 0,
9
- skipped: 0,
10
- swept: 0,
11
- sweepFailed: 0,
12
- subscriptionsRefreshed: 0,
13
- subscriptionsFailed: 0,
14
- redriven: 0,
15
- redriveFailed: 0,
16
- };
17
-
18
- // Only the loop's own two calls matter here; the tick is a stub, so what it is
19
- // handed is never read.
20
- const TICK_DEPS = {} as RunSchedulerTickDeps;
21
-
22
- const buildLoop = (
23
- overrides: Partial<SchedulerLoopOptions>,
24
- ): SchedulerLoopOptions => ({
25
- tick: () => Promise.resolve(RESULT),
26
- tickDeps: TICK_DEPS,
27
- heartbeat: () => Promise.resolve(),
28
- onHeartbeatError: () => {},
29
- tickIntervalMs: 1000,
30
- wait: () => Promise.resolve(),
31
- ...overrides,
32
- });
33
-
34
- describe("runSchedulerLoop", () => {
35
- it("writes no heartbeat for a tick that throws", async () => {
36
- let beats = 0;
37
- await assert.rejects(
38
- runSchedulerLoop(
39
- buildLoop({
40
- tick: () => Promise.reject(new Error("tick failed")),
41
- heartbeat: () => {
42
- beats += 1;
43
- return Promise.resolve();
44
- },
45
- }),
46
- ),
47
- /tick failed/,
48
- );
49
- assert.equal(beats, 0);
50
- });
51
-
52
- it("beats once per completed round, and stops beating when a round stops completing", async () => {
53
- let rounds = 0;
54
- let beats = 0;
55
- await assert.rejects(
56
- runSchedulerLoop(
57
- buildLoop({
58
- tick: () => {
59
- rounds += 1;
60
- return rounds > 2
61
- ? Promise.reject(new Error("tick failed"))
62
- : Promise.resolve(RESULT);
63
- },
64
- heartbeat: () => {
65
- beats += 1;
66
- return Promise.resolve();
67
- },
68
- }),
69
- ),
70
- /tick failed/,
71
- );
72
- assert.equal(beats, 2);
73
- });
74
-
75
- it("keeps ticking when the heartbeat write fails", async () => {
76
- const errors: unknown[] = [];
77
- let rounds = 0;
78
- await assert.rejects(
79
- runSchedulerLoop(
80
- buildLoop({
81
- tick: () => {
82
- rounds += 1;
83
- return rounds > 3
84
- ? Promise.reject(new Error("done"))
85
- : Promise.resolve(RESULT);
86
- },
87
- heartbeat: () => Promise.reject(new Error("ENOSPC")),
88
- onHeartbeatError: (error) => errors.push(error),
89
- }),
90
- ),
91
- /done/,
92
- );
93
- assert.equal(rounds, 4);
94
- assert.equal(errors.length, 3);
95
- });
96
-
97
- it("waits a tick interval between rounds", async () => {
98
- const waited: number[] = [];
99
- let rounds = 0;
100
- await assert.rejects(
101
- runSchedulerLoop(
102
- buildLoop({
103
- tickIntervalMs: 300_000,
104
- tick: () => {
105
- rounds += 1;
106
- return rounds > 2
107
- ? Promise.reject(new Error("done"))
108
- : Promise.resolve(RESULT);
109
- },
110
- wait: (ms) => {
111
- waited.push(ms);
112
- return Promise.resolve();
113
- },
114
- }),
115
- ),
116
- /done/,
117
- );
118
- assert.deepEqual(waited, [300_000, 300_000]);
119
- });
120
-
121
- // The default is the only one production uses, so it is the one a wrong unit
122
- // or a dropped argument would ship in.
123
- it("sleeps the real timer when no wait is injected", async () => {
124
- let rounds = 0;
125
- const started = Date.now();
126
- await assert.rejects(
127
- runSchedulerLoop({
128
- ...buildLoop({
129
- tickIntervalMs: 25,
130
- tick: () => {
131
- rounds += 1;
132
- return rounds > 2
133
- ? Promise.reject(new Error("done"))
134
- : Promise.resolve(RESULT);
135
- },
136
- }),
137
- wait: undefined,
138
- }),
139
- /done/,
140
- );
141
- assert.ok(Date.now() - started >= 50);
142
- });
143
- });