@remit/imap-worker 0.0.87 → 0.0.89
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 +6 -6
- package/src/cli.ts +7 -0
- package/src/credential-less-sync-fence.test.ts +5 -9
- package/src/e2e-processor-shim.ts +2 -2
- package/src/emit.test.ts +24 -59
- package/src/emit.ts +50 -73
- package/src/events.ts +1 -19
- package/src/handlers/delete-account-objects.test.ts +25 -14
- package/src/handlers/delete-account-objects.ts +2 -11
- package/src/handlers/placement-push.sqlite.test.ts +1 -1
- package/src/handlers/probe-message-id.sqlite.test.ts +23 -28
- package/src/handlers/sync-mailboxes.ts +2 -2
- package/src/handlers/sync-message-body.ts +14 -18
- package/src/index.ts +6 -0
- package/src/mail-sync-selection.sqlite.test.ts +26 -23
- package/src/pending-sync.sqlite.test.ts +27 -28
- package/src/processor.ts +0 -8
- package/src/queues.ts +49 -0
- package/src/scheduler/attachment-sweep.sqlite.test.ts +1 -0
- package/src/scheduler/half-deleted-accounts.sqlite.test.ts +20 -27
- package/src/scheduler/handler.ts +8 -6
- package/src/scheduler/run-tick.sqlite.test.ts +15 -16
- package/src/scheduler/run-tick.ts +10 -14
- package/src/scheduler/runner.ts +18 -10
- package/src/stranded-placement.sqlite.test.ts +4 -5
- package/src/test-helpers/test-world.ts +40 -11
- package/src/thread-join.sqlite.test.ts +7 -16
- package/src/with-oauth-lifecycle-deps.sqlite.test.ts +9 -2
- package/src/processor.test.ts +0 -58
package/src/scheduler/runner.ts
CHANGED
|
@@ -4,11 +4,16 @@ import { finishHalfDeletedAccounts } from "@remit/backend/account-deletion";
|
|
|
4
4
|
import { getClient } from "@remit/backend/client";
|
|
5
5
|
import { createLogger } from "@remit/logger";
|
|
6
6
|
import { installNodeLogger } from "@remit/logger-lambda/node";
|
|
7
|
+
import { backfillSearchText } from "@remit/mailbox-service";
|
|
7
8
|
import { clearHeartbeats, createHeartbeat } from "@remit/sqs-client/heartbeat";
|
|
8
|
-
import { createQueueProducer } from "@remit/sqs-client/producer";
|
|
9
9
|
|
|
10
|
-
import { env } from "expect-env";
|
|
11
10
|
import { emitEvent } from "../emit.js";
|
|
11
|
+
import {
|
|
12
|
+
setWorkerQueues,
|
|
13
|
+
sqsWorkerQueues,
|
|
14
|
+
workerQueues,
|
|
15
|
+
workerQueueUrlsFromEnv,
|
|
16
|
+
} from "../queues.js";
|
|
12
17
|
import { buildAttachmentSweep } from "./attachment-sweep.js";
|
|
13
18
|
import { buildCalendarSubscriptionRefresh } from "./calendar-subscriptions.js";
|
|
14
19
|
import { getOfflineIntervalMs, getTickIntervalMs } from "./config.js";
|
|
@@ -40,9 +45,7 @@ installNodeLogger();
|
|
|
40
45
|
|
|
41
46
|
const log = createLogger();
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
const sqsClient = createQueueProducer({ queueUrl: mailboxesQueueUrl });
|
|
45
|
-
const accountFanoutQueueUrl = env.SQS_QUEUE_URL_ACCOUNT_FANOUT;
|
|
48
|
+
setWorkerQueues(sqsWorkerQueues(workerQueueUrlsFromEnv()));
|
|
46
49
|
|
|
47
50
|
const tickIntervalMs = getTickIntervalMs();
|
|
48
51
|
const offlineIntervalMs = getOfflineIntervalMs();
|
|
@@ -81,16 +84,20 @@ const runLoop = async (): Promise<void> => {
|
|
|
81
84
|
await finishHalfDeletedAccounts({
|
|
82
85
|
account,
|
|
83
86
|
configImport: client.configImport,
|
|
84
|
-
|
|
85
|
-
accountFanoutQueueUrl,
|
|
87
|
+
queue: client.queues.accountFanout,
|
|
86
88
|
log,
|
|
87
89
|
});
|
|
88
|
-
|
|
90
|
+
const searchTextBackfill = backfillSearchText(
|
|
91
|
+
{ messageService: client.message, storageService: client.storage },
|
|
92
|
+
log,
|
|
93
|
+
).then((result) => {
|
|
94
|
+
log.debug({ ...result }, "Search text backfill finished");
|
|
95
|
+
});
|
|
96
|
+
const schedulerLoop = runSchedulerLoop({
|
|
89
97
|
tick: runSchedulerTick,
|
|
90
98
|
tickDeps: {
|
|
91
99
|
accountService: account,
|
|
92
|
-
|
|
93
|
-
queueUrl: mailboxesQueueUrl,
|
|
100
|
+
queue: workerQueues().mailboxes,
|
|
94
101
|
log,
|
|
95
102
|
tickIntervalMs,
|
|
96
103
|
offlineIntervalMs,
|
|
@@ -110,6 +117,7 @@ const runLoop = async (): Promise<void> => {
|
|
|
110
117
|
onHeartbeatError: onBeatError,
|
|
111
118
|
tickIntervalMs,
|
|
112
119
|
});
|
|
120
|
+
await Promise.all([searchTextBackfill, schedulerLoop]);
|
|
113
121
|
};
|
|
114
122
|
|
|
115
123
|
runLoop()
|
|
@@ -84,7 +84,7 @@ const placementMover = (w: World): PlacementMoveService =>
|
|
|
84
84
|
addressService: w.client.address,
|
|
85
85
|
mailboxSpecialUseService: w.client.mailboxSpecialUse,
|
|
86
86
|
mailboxService: w.client.mailbox,
|
|
87
|
-
|
|
87
|
+
queue: w.client.queues.messageManagement,
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
const placementOf = async (a: Arranged) => {
|
|
@@ -131,7 +131,8 @@ const queueRefuses = (w: World): void => {
|
|
|
131
131
|
};
|
|
132
132
|
|
|
133
133
|
const queueTakes = (w: World): void => {
|
|
134
|
-
w.queue.answer(async () => ({
|
|
134
|
+
w.queue.answer(async () => ({}));
|
|
135
|
+
w.queue.refuse([]);
|
|
135
136
|
};
|
|
136
137
|
|
|
137
138
|
const redriveLater = async (
|
|
@@ -255,9 +256,7 @@ describe("a move whose event dies leaves no row moving", () => {
|
|
|
255
256
|
|
|
256
257
|
it("hands back a move to Trash whose batch entry the queue refused", async () => {
|
|
257
258
|
const a = await arrange();
|
|
258
|
-
a.w.queue.
|
|
259
|
-
Failed: [{ Id: "0", Code: "InvalidParameterValue", SenderFault: true }],
|
|
260
|
-
}));
|
|
259
|
+
a.w.queue.refuse([0]);
|
|
261
260
|
|
|
262
261
|
await assert.rejects(
|
|
263
262
|
a.w.client.messageMove.deleteMessages(
|
|
@@ -5,17 +5,23 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import {
|
|
7
7
|
_resetForTest,
|
|
8
|
-
getClient,
|
|
9
8
|
type RemitClient,
|
|
10
9
|
setClient,
|
|
11
10
|
} from "@remit/backend/client";
|
|
11
|
+
import { composeSqliteClient } from "@remit/backend/compose-sqlite";
|
|
12
|
+
import { buildSharedDeps } from "@remit/backend/shared-deps";
|
|
12
13
|
import type {
|
|
13
14
|
AccountItem,
|
|
14
15
|
MailboxItem,
|
|
15
16
|
MailboxSpecialUseValue,
|
|
16
17
|
MessageItem,
|
|
17
18
|
OutboxMessageItem,
|
|
19
|
+
QueueProducer,
|
|
18
20
|
} from "@remit/data-ports";
|
|
21
|
+
import {
|
|
22
|
+
type RecordingQueue,
|
|
23
|
+
recordingQueue,
|
|
24
|
+
} from "@remit/data-ports/recording-queue";
|
|
19
25
|
import {
|
|
20
26
|
AccountAuthType,
|
|
21
27
|
ConnectionState,
|
|
@@ -28,13 +34,10 @@ import {
|
|
|
28
34
|
eml,
|
|
29
35
|
FakeImapServer,
|
|
30
36
|
} from "@remit/mailbox-service/test-helpers/fake-imap";
|
|
31
|
-
import {
|
|
32
|
-
captureQueue,
|
|
33
|
-
type QueueCapture,
|
|
34
|
-
} from "@remit/mailbox-service/test-helpers/test-queue";
|
|
35
37
|
import { serializeEncryptedPayload } from "@remit/secrets-service";
|
|
36
38
|
import type { createConnectionScopeWithCredentials } from "../connection-scope.js";
|
|
37
39
|
import { syncMessages } from "../handlers/sync-messages.js";
|
|
40
|
+
import { setWorkerQueues } from "../queues.js";
|
|
38
41
|
|
|
39
42
|
export interface LogLine {
|
|
40
43
|
level: string;
|
|
@@ -93,6 +96,7 @@ export interface SeededMessage {
|
|
|
93
96
|
*/
|
|
94
97
|
export const openStore = async (): Promise<{
|
|
95
98
|
client: RemitClient;
|
|
99
|
+
bindWorkerQueues: () => void;
|
|
96
100
|
world: (accountConfigId?: string) => Promise<World>;
|
|
97
101
|
close: () => void;
|
|
98
102
|
}> => {
|
|
@@ -103,12 +107,38 @@ export const openStore = async (): Promise<{
|
|
|
103
107
|
process.env.BETTER_AUTH_URL = "https://mail.example.com";
|
|
104
108
|
process.env.BETTER_AUTH_SECRET = "a-signing-secret-of-at-least-32-characters";
|
|
105
109
|
_resetForTest();
|
|
106
|
-
|
|
110
|
+
let current = recordingQueue();
|
|
111
|
+
const queue: QueueProducer<unknown> = {
|
|
112
|
+
send: (message) => current.send(message),
|
|
113
|
+
sendBatch: (messages) => current.sendBatch(messages),
|
|
114
|
+
};
|
|
115
|
+
const client = await composeSqliteClient(
|
|
116
|
+
buildSharedDeps({
|
|
117
|
+
sync: queue,
|
|
118
|
+
accountFanout: queue,
|
|
119
|
+
smtp: queue,
|
|
120
|
+
messageManagement: queue,
|
|
121
|
+
body: queue,
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
107
124
|
setClient(client);
|
|
125
|
+
const bindWorkerQueues = (): void =>
|
|
126
|
+
setWorkerQueues({
|
|
127
|
+
mailboxes: queue,
|
|
128
|
+
messages: queue,
|
|
129
|
+
flags: queue,
|
|
130
|
+
body: queue,
|
|
131
|
+
mailboxManagement: queue,
|
|
132
|
+
messageManagement: queue,
|
|
133
|
+
});
|
|
134
|
+
bindWorkerQueues();
|
|
108
135
|
return {
|
|
109
136
|
client,
|
|
110
|
-
|
|
111
|
-
|
|
137
|
+
bindWorkerQueues,
|
|
138
|
+
world: (accountConfigId = `cfg-${randomUUID()}`) => {
|
|
139
|
+
current = recordingQueue();
|
|
140
|
+
return createWorld(client, current, accountConfigId);
|
|
141
|
+
},
|
|
112
142
|
close: () => {
|
|
113
143
|
_resetForTest();
|
|
114
144
|
rmSync(directory, { recursive: true, force: true });
|
|
@@ -135,7 +165,7 @@ export interface World {
|
|
|
135
165
|
) => Promise<Array<{ mailboxId: string; uid?: number; isDeleted: boolean }>>;
|
|
136
166
|
queued: () => Array<Record<string, unknown>>;
|
|
137
167
|
refuseSends: (error: Error | undefined) => void;
|
|
138
|
-
queue:
|
|
168
|
+
queue: RecordingQueue;
|
|
139
169
|
draft: (
|
|
140
170
|
attachment?: Buffer,
|
|
141
171
|
) => Promise<{ outbox: OutboxMessageItem; attachmentId?: string }>;
|
|
@@ -145,10 +175,9 @@ export interface World {
|
|
|
145
175
|
|
|
146
176
|
const createWorld = async (
|
|
147
177
|
client: RemitClient,
|
|
178
|
+
queue: RecordingQueue,
|
|
148
179
|
accountConfigId: string,
|
|
149
180
|
): Promise<World> => {
|
|
150
|
-
const queue = captureQueue();
|
|
151
|
-
|
|
152
181
|
const imap = new FakeImapServer();
|
|
153
182
|
const logs: LogLine[] = [];
|
|
154
183
|
const password = await client.secrets.encrypt("app-password");
|
|
@@ -6,14 +6,7 @@ import type {
|
|
|
6
6
|
MailboxItem,
|
|
7
7
|
} from "@remit/data-ports";
|
|
8
8
|
import { deriveThreadId } from "@remit/data-ports/id";
|
|
9
|
-
import {
|
|
10
|
-
AddressRepo,
|
|
11
|
-
DrizzleEnvelopeRepository,
|
|
12
|
-
DrizzleMessageRepository,
|
|
13
|
-
DrizzleThreadMessageRepository,
|
|
14
|
-
SearchIndex,
|
|
15
|
-
SyncWarningRepo,
|
|
16
|
-
} from "@remit/drizzle-service";
|
|
9
|
+
import { createSqliteRepositories } from "@remit/drizzle-service";
|
|
17
10
|
import { createShippedSqliteDb } from "@remit/drizzle-service/test-sqlite";
|
|
18
11
|
import { MessageSyncService, SyncWarningService } from "@remit/mailbox-service";
|
|
19
12
|
import type { ImapMessage } from "@remit/mailbox-service/types";
|
|
@@ -63,18 +56,16 @@ const syncInOrder = async (
|
|
|
63
56
|
messages: ImapMessage[],
|
|
64
57
|
): Promise<{ threadIds: (messageId: string) => string[] }> => {
|
|
65
58
|
const { db, sqlite } = createShippedSqliteDb();
|
|
66
|
-
const
|
|
67
|
-
typeof DrizzleMessageRepository
|
|
68
|
-
>[0];
|
|
59
|
+
const repos = createSqliteRepositories({ app: db, mail: db, search: db });
|
|
69
60
|
const service = new MessageSyncService(
|
|
70
61
|
{} as never,
|
|
71
62
|
{} as IMailboxRepository,
|
|
72
63
|
{} as IMailboxSpecialUseRepository,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
new SyncWarningService(
|
|
64
|
+
repos.message,
|
|
65
|
+
repos.envelope,
|
|
66
|
+
repos.address,
|
|
67
|
+
repos.threadMessage,
|
|
68
|
+
new SyncWarningService(repos.syncWarning),
|
|
78
69
|
);
|
|
79
70
|
const save = (
|
|
80
71
|
service as unknown as {
|
|
@@ -12,7 +12,10 @@ import assert from "node:assert/strict";
|
|
|
12
12
|
import { after, before, describe, it } from "node:test";
|
|
13
13
|
import type { AccountItem } from "@remit/data-ports";
|
|
14
14
|
import { AccountAuthType, ConnectionState } from "@remit/domain-enums";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
type AccountRepo,
|
|
17
|
+
createSqliteRepositories,
|
|
18
|
+
} from "@remit/drizzle-service";
|
|
16
19
|
import { createShippedSqliteDb } from "@remit/drizzle-service/test-sqlite";
|
|
17
20
|
import { noopLogger } from "@remit/logger";
|
|
18
21
|
import { RefreshTokenError } from "@remit/mail-oauth-service";
|
|
@@ -67,7 +70,11 @@ const account = async (): Promise<AccountItem> =>
|
|
|
67
70
|
before(() => {
|
|
68
71
|
const db = createShippedSqliteDb();
|
|
69
72
|
close = db.close;
|
|
70
|
-
repo =
|
|
73
|
+
repo = createSqliteRepositories({
|
|
74
|
+
app: db.db,
|
|
75
|
+
mail: db.db,
|
|
76
|
+
search: db.db,
|
|
77
|
+
}).account;
|
|
71
78
|
});
|
|
72
79
|
|
|
73
80
|
after(() => {
|
package/src/processor.test.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import type { Logger } from "@remit/logger";
|
|
4
|
-
import type { ImapWorkerStopEvent } from "./events.js";
|
|
5
|
-
import { processEvent } from "./processor.js";
|
|
6
|
-
|
|
7
|
-
interface CapturedLogEntry {
|
|
8
|
-
args: unknown[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const createCapturingLogger = (): {
|
|
12
|
-
log: Logger;
|
|
13
|
-
infoCalls: CapturedLogEntry[];
|
|
14
|
-
} => {
|
|
15
|
-
const infoCalls: CapturedLogEntry[] = [];
|
|
16
|
-
const noop = () => {};
|
|
17
|
-
const log = {
|
|
18
|
-
info: (...args: unknown[]) => infoCalls.push({ args }),
|
|
19
|
-
warn: noop,
|
|
20
|
-
error: noop,
|
|
21
|
-
debug: noop,
|
|
22
|
-
fatal: noop,
|
|
23
|
-
trace: noop,
|
|
24
|
-
child: () => log,
|
|
25
|
-
} as unknown as Logger;
|
|
26
|
-
return { log, infoCalls };
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
describe("processEvent — IMAP_WORKER_STOP", () => {
|
|
30
|
-
it("returns undefined and logs the stop signal — tombstone fence on the account row already halts work", async () => {
|
|
31
|
-
const event: ImapWorkerStopEvent = {
|
|
32
|
-
type: "IMAP_WORKER_STOP",
|
|
33
|
-
accountConfigId: "acfg_alice_replay_safe_test_id",
|
|
34
|
-
accountId: "acct_alice_replay_safe_test_id",
|
|
35
|
-
};
|
|
36
|
-
const { log, infoCalls } = createCapturingLogger();
|
|
37
|
-
|
|
38
|
-
const result = await processEvent(event, log);
|
|
39
|
-
|
|
40
|
-
assert.equal(result, undefined);
|
|
41
|
-
const matched = infoCalls.find((c) => {
|
|
42
|
-
const [meta, msg] = c.args;
|
|
43
|
-
return (
|
|
44
|
-
typeof msg === "string" &&
|
|
45
|
-
msg === "Imap worker stop signal received" &&
|
|
46
|
-
typeof meta === "object" &&
|
|
47
|
-
meta !== null &&
|
|
48
|
-
(meta as { accountConfigId?: string }).accountConfigId ===
|
|
49
|
-
event.accountConfigId &&
|
|
50
|
-
(meta as { accountId?: string }).accountId === event.accountId
|
|
51
|
-
);
|
|
52
|
-
});
|
|
53
|
-
assert.ok(
|
|
54
|
-
matched,
|
|
55
|
-
"processor must log the cascade-contract stop signal with both ids",
|
|
56
|
-
);
|
|
57
|
-
});
|
|
58
|
-
});
|