@remit/imap-worker 0.0.89 → 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.
- package/dist/account-check.d.ts +27 -0
- package/dist/account-check.js +70 -0
- package/dist/body-sync-gate.d.ts +12 -0
- package/dist/body-sync-gate.js +53 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +193 -0
- package/dist/config-env.d.ts +5 -0
- package/dist/config-env.js +37 -0
- package/dist/connection-pool.d.ts +22 -0
- package/dist/connection-pool.js +97 -0
- package/dist/connection-scope.d.ts +8 -0
- package/dist/connection-scope.js +10 -0
- package/dist/context.d.ts +46 -0
- package/dist/context.js +15 -0
- package/dist/e2e-processor-shim.d.ts +2 -0
- package/dist/e2e-processor-shim.js +181 -0
- package/dist/emit.d.ts +15 -0
- package/dist/emit.js +76 -0
- package/dist/events.d.ts +203 -0
- package/dist/events.js +1 -0
- package/dist/failure-kind.d.ts +10 -0
- package/{src/failure-kind.ts → dist/failure-kind.js} +6 -6
- package/dist/handlers/append-sent-message.d.ts +3 -0
- package/dist/handlers/append-sent-message.js +152 -0
- package/dist/handlers/delete-account-objects.d.ts +3 -0
- package/dist/handlers/delete-account-objects.js +35 -0
- package/dist/handlers/empty-trash.d.ts +14 -0
- package/dist/handlers/empty-trash.js +217 -0
- package/dist/handlers/flag-push.d.ts +38 -0
- package/dist/handlers/flag-push.js +195 -0
- package/dist/handlers/imported-folder.d.ts +4 -0
- package/dist/handlers/imported-folder.js +23 -0
- package/dist/handlers/mailbox-management.d.ts +6 -0
- package/dist/handlers/mailbox-management.js +294 -0
- package/dist/handlers/mailbox-sync-order.d.ts +21 -0
- package/dist/handlers/mailbox-sync-order.js +50 -0
- package/dist/handlers/message-copy.d.ts +40 -0
- package/dist/handlers/message-copy.js +371 -0
- package/dist/handlers/message-delete-terminal.d.ts +60 -0
- package/dist/handlers/message-delete-terminal.js +77 -0
- package/dist/handlers/message-delete.d.ts +57 -0
- package/dist/handlers/message-delete.js +520 -0
- package/{src/handlers/message-move-terminal.ts → dist/handlers/message-move-terminal.d.ts} +18 -105
- package/dist/handlers/message-move-terminal.js +94 -0
- package/dist/handlers/message-move.d.ts +146 -0
- package/dist/handlers/message-move.js +490 -0
- package/dist/handlers/placement-move-push.d.ts +62 -0
- package/dist/handlers/placement-move-push.js +307 -0
- package/dist/handlers/probe-message-id.d.ts +2 -0
- package/dist/handlers/probe-message-id.js +6 -0
- package/dist/handlers/sync-mailboxes.d.ts +45 -0
- package/dist/handlers/sync-mailboxes.js +233 -0
- package/dist/handlers/sync-message-body.d.ts +23 -0
- package/dist/handlers/sync-message-body.js +182 -0
- package/dist/handlers/sync-messages.d.ts +29 -0
- package/dist/handlers/sync-messages.js +285 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +67 -0
- package/dist/lambda-context.d.ts +8 -0
- package/dist/lambda-context.js +33 -0
- package/dist/mailbox-row.d.ts +9 -0
- package/dist/mailbox-row.js +6 -0
- package/dist/poller.d.ts +1 -0
- package/dist/poller.js +51 -0
- package/dist/process.d.ts +6 -0
- package/dist/process.js +3 -0
- package/dist/processor.d.ts +3 -0
- package/dist/processor.js +44 -0
- package/dist/queues.d.ts +10 -0
- package/dist/queues.js +1 -0
- package/dist/s3-account-objects.d.ts +3 -0
- package/dist/s3-account-objects.js +26 -0
- package/dist/scheduler/attachment-sweep.d.ts +11 -0
- package/dist/scheduler/attachment-sweep.js +23 -0
- package/dist/scheduler/calendar-subscriptions.d.ts +26 -0
- package/dist/scheduler/calendar-subscriptions.js +35 -0
- package/dist/scheduler/config.d.ts +27 -0
- package/{src/scheduler/config.ts → dist/scheduler/config.js} +7 -26
- package/{src/scheduler/decide-due.ts → dist/scheduler/decide-due.d.ts} +3 -12
- package/dist/scheduler/decide-due.js +17 -0
- package/dist/scheduler/handler.d.ts +21 -0
- package/{src/scheduler/handler.ts → dist/scheduler/handler.js} +24 -36
- package/{src/scheduler/loop.ts → dist/scheduler/loop.d.ts} +7 -23
- package/dist/scheduler/loop.js +8 -0
- package/dist/scheduler/run-tick.d.ts +62 -0
- package/dist/scheduler/run-tick.js +164 -0
- package/dist/scheduler/runner.d.ts +2 -0
- package/{src/scheduler/runner.ts → dist/scheduler/runner.js} +58 -84
- package/dist/scheduler/stranded-placement-redrive.d.ts +16 -0
- package/dist/scheduler/stranded-placement-redrive.js +172 -0
- package/dist/sqs-queues.d.ts +4 -0
- package/dist/sqs-queues.js +22 -0
- package/dist/with-oauth-lifecycle-deps.d.ts +5 -0
- package/dist/with-oauth-lifecycle-deps.js +17 -0
- package/dist/with-oauth-lifecycle.d.ts +56 -0
- package/dist/with-oauth-lifecycle.js +92 -0
- package/{src/worker-version.ts → dist/worker-version.d.ts} +1 -2
- package/dist/worker-version.js +12 -0
- package/package.json +75 -69
- package/src/account-check.test.ts +0 -149
- package/src/account-check.ts +0 -101
- package/src/body-sync-gate.test.ts +0 -185
- package/src/body-sync-gate.ts +0 -86
- package/src/cli.ts +0 -224
- package/src/connection-scope.test.ts +0 -266
- package/src/connection-scope.ts +0 -314
- package/src/credential-less-sync-fence.test.ts +0 -152
- package/src/e2e-processor-shim.ts +0 -232
- package/src/emit.test.ts +0 -127
- package/src/emit.ts +0 -117
- package/src/events.ts +0 -252
- package/src/failure-kind.test.ts +0 -42
- package/src/handlers/append-sent-message.sqlite.test.ts +0 -176
- package/src/handlers/append-sent-message.ts +0 -264
- package/src/handlers/delete-account-objects.test.ts +0 -128
- package/src/handlers/delete-account-objects.ts +0 -94
- package/src/handlers/empty-trash.sqlite.test.ts +0 -209
- package/src/handlers/empty-trash.ts +0 -352
- package/src/handlers/flag-push.ts +0 -285
- package/src/handlers/imported-folder.test.ts +0 -63
- package/src/handlers/imported-folder.ts +0 -38
- package/src/handlers/mailbox-management.sqlite.test.ts +0 -265
- package/src/handlers/mailbox-management.ts +0 -486
- package/src/handlers/mailbox-sync-order.test.ts +0 -93
- package/src/handlers/mailbox-sync-order.ts +0 -65
- package/src/handlers/message-copy.sqlite.test.ts +0 -298
- package/src/handlers/message-copy.ts +0 -566
- package/src/handlers/message-delete-terminal.ts +0 -147
- package/src/handlers/message-delete.sqlite.test.ts +0 -291
- package/src/handlers/message-delete.ts +0 -885
- package/src/handlers/message-move.sqlite.test.ts +0 -271
- package/src/handlers/message-move.test.ts +0 -216
- package/src/handlers/message-move.ts +0 -752
- package/src/handlers/placement-move-push.test.ts +0 -137
- package/src/handlers/placement-move-push.ts +0 -475
- package/src/handlers/placement-push.sqlite.test.ts +0 -298
- package/src/handlers/probe-message-id.sqlite.test.ts +0 -192
- package/src/handlers/probe-message-id.ts +0 -15
- package/src/handlers/sync-mailboxes.test.ts +0 -183
- package/src/handlers/sync-mailboxes.ts +0 -421
- package/src/handlers/sync-message-body.sqlite.test.ts +0 -314
- package/src/handlers/sync-message-body.test.ts +0 -150
- package/src/handlers/sync-message-body.ts +0 -365
- package/src/handlers/sync-messages.sqlite.test.ts +0 -183
- package/src/handlers/sync-messages.ts +0 -525
- package/src/index.test.ts +0 -22
- package/src/index.ts +0 -86
- package/src/mail-sync-selection.sqlite.test.ts +0 -234
- package/src/mailbox-row.ts +0 -20
- package/src/pending-sync.sqlite.test.ts +0 -166
- package/src/poller.ts +0 -55
- package/src/processor.ts +0 -60
- package/src/queues.ts +0 -49
- package/src/scheduler/attachment-sweep.sqlite.test.ts +0 -75
- package/src/scheduler/attachment-sweep.ts +0 -46
- package/src/scheduler/calendar-subscriptions.sqlite.test.ts +0 -147
- package/src/scheduler/calendar-subscriptions.ts +0 -103
- package/src/scheduler/config.test.ts +0 -40
- package/src/scheduler/decide-due.test.ts +0 -44
- package/src/scheduler/half-deleted-accounts.sqlite.test.ts +0 -196
- package/src/scheduler/loop.test.ts +0 -143
- package/src/scheduler/run-tick.sqlite.test.ts +0 -191
- package/src/scheduler/run-tick.ts +0 -281
- package/src/scheduler/stranded-placement-redrive.ts +0 -265
- package/src/stranded-placement.sqlite.test.ts +0 -794
- package/src/test-helpers/test-world.ts +0 -427
- package/src/thread-join.sqlite.test.ts +0 -106
- package/src/with-oauth-lifecycle-deps.sqlite.test.ts +0 -269
- package/src/with-oauth-lifecycle-deps.ts +0 -71
- package/src/with-oauth-lifecycle.ts +0 -158
- package/tsconfig.json +0 -8
|
@@ -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
|
-
});
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
-
import type { AccountItem } from "@remit/data-ports";
|
|
4
|
-
import { recordingQueue } from "@remit/data-ports/recording-queue";
|
|
5
|
-
import {
|
|
6
|
-
AccountAuthType,
|
|
7
|
-
AccountService,
|
|
8
|
-
ConnectionState,
|
|
9
|
-
} from "@remit/domain-enums";
|
|
10
|
-
import {
|
|
11
|
-
type AccountRepo,
|
|
12
|
-
createSqliteRepositories,
|
|
13
|
-
} from "@remit/drizzle-service";
|
|
14
|
-
import { createShippedSqliteDb } from "@remit/drizzle-service/test-sqlite";
|
|
15
|
-
import { capturingLogger, type LogLine } from "../test-helpers/test-world.js";
|
|
16
|
-
import { runSchedulerTick } from "./run-tick.js";
|
|
17
|
-
|
|
18
|
-
const TICK_INTERVAL_MS = 60 * 60 * 1000;
|
|
19
|
-
const OFFLINE_INTERVAL_MS = 12 * 60 * 60 * 1000;
|
|
20
|
-
const NOW = 1_700_000_000_000;
|
|
21
|
-
|
|
22
|
-
let accounts: AccountRepo;
|
|
23
|
-
let close: () => void;
|
|
24
|
-
|
|
25
|
-
beforeEach(() => {
|
|
26
|
-
const store = createShippedSqliteDb();
|
|
27
|
-
close = store.close;
|
|
28
|
-
accounts = createSqliteRepositories({
|
|
29
|
-
app: store.db,
|
|
30
|
-
mail: store.db,
|
|
31
|
-
search: store.db,
|
|
32
|
-
}).account;
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
afterEach(() => close());
|
|
36
|
-
|
|
37
|
-
const account = async (
|
|
38
|
-
name: string,
|
|
39
|
-
overrides: Partial<AccountItem> = {},
|
|
40
|
-
stored: { credential: boolean } = { credential: true },
|
|
41
|
-
): Promise<AccountItem> => {
|
|
42
|
-
const created = await accounts.create({
|
|
43
|
-
accountConfigId: `cfg-${name}`,
|
|
44
|
-
username: `${name}@example.com`,
|
|
45
|
-
email: `${name}@example.com`,
|
|
46
|
-
authType: AccountAuthType.Password,
|
|
47
|
-
passwordHash: stored.credential ? '{"ciphertext":"x"}' : undefined,
|
|
48
|
-
imapHost: "imap.example.com",
|
|
49
|
-
imapPort: 993,
|
|
50
|
-
imapTls: true,
|
|
51
|
-
imapStartTls: false,
|
|
52
|
-
isActive: true,
|
|
53
|
-
connectionState: ConnectionState.Authenticated,
|
|
54
|
-
} as Parameters<AccountRepo["create"]>[0]);
|
|
55
|
-
if (Object.keys(overrides).length === 0) return created;
|
|
56
|
-
await accounts.update(created.accountId, overrides);
|
|
57
|
-
return accounts.get(created.accountId);
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const tick = async (
|
|
61
|
-
options: Partial<Parameters<typeof runSchedulerTick>[0]> = {},
|
|
62
|
-
) => {
|
|
63
|
-
const queue = recordingQueue();
|
|
64
|
-
const { sent } = queue;
|
|
65
|
-
const logs: LogLine[] = [];
|
|
66
|
-
const result = await runSchedulerTick({
|
|
67
|
-
accountService: accounts,
|
|
68
|
-
queue,
|
|
69
|
-
log: capturingLogger(logs),
|
|
70
|
-
tickIntervalMs: TICK_INTERVAL_MS,
|
|
71
|
-
offlineIntervalMs: OFFLINE_INTERVAL_MS,
|
|
72
|
-
now: NOW,
|
|
73
|
-
redriveStrandedPlacements: async () => 0,
|
|
74
|
-
...options,
|
|
75
|
-
});
|
|
76
|
-
const enqueued = sent.map((message) => String(message.body.accountId));
|
|
77
|
-
return { result, sent, enqueued, logs };
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
describe("a scheduler tick", () => {
|
|
81
|
-
it("enqueues the accounts that are due and only those", async () => {
|
|
82
|
-
const due = await account("due", {
|
|
83
|
-
lastSyncAt: NOW - OFFLINE_INTERVAL_MS - 1,
|
|
84
|
-
});
|
|
85
|
-
const never = await account("never");
|
|
86
|
-
await account("fresh", { lastSyncAt: NOW - 60_000 });
|
|
87
|
-
|
|
88
|
-
const { enqueued } = await tick();
|
|
89
|
-
|
|
90
|
-
assert.deepEqual(enqueued.sort(), [due.accountId, never.accountId].sort());
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("skips an account it cannot or should not sync, and says so once in aggregate", async () => {
|
|
94
|
-
const live = await account("live");
|
|
95
|
-
await account("unsyncable", { imapHost: "mail.invalid" });
|
|
96
|
-
await account("reauth", {
|
|
97
|
-
connectionState: ConnectionState.ReauthRequired,
|
|
98
|
-
});
|
|
99
|
-
await account("calendar", { syncedServices: [AccountService.Calendar] });
|
|
100
|
-
|
|
101
|
-
const { enqueued, result, logs } = await tick();
|
|
102
|
-
|
|
103
|
-
assert.deepEqual(enqueued, [live.accountId]);
|
|
104
|
-
assert.equal(result.skipped, 3);
|
|
105
|
-
assert.equal(logs.length, 1, "one aggregate line, no per-account noise");
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Issue #1120: a sync for an account with no stored credential fenced the
|
|
109
|
-
// account's FIFO group for its whole visibility timeout.
|
|
110
|
-
it("skips an account that stores no credential", async () => {
|
|
111
|
-
const live = await account("live");
|
|
112
|
-
await account("missing", {}, { credential: false });
|
|
113
|
-
|
|
114
|
-
const { enqueued } = await tick();
|
|
115
|
-
|
|
116
|
-
assert.deepEqual(enqueued, [live.accountId]);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("gives each enqueued account a scheduler-namespaced, time-bucketed dedup id", async () => {
|
|
120
|
-
const live = await account("live");
|
|
121
|
-
|
|
122
|
-
const { sent } = await tick();
|
|
123
|
-
|
|
124
|
-
const dedupId = sent[0]?.deduplicationId;
|
|
125
|
-
assert.ok(
|
|
126
|
-
dedupId?.startsWith(`SYNC_MAILBOXES:scheduled:${live.accountId}:`),
|
|
127
|
-
);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("sweeps every account's attachments, due a sync or not", async () => {
|
|
131
|
-
const due = await account("due");
|
|
132
|
-
const fresh = await account("fresh", { lastSyncAt: NOW - 60_000 });
|
|
133
|
-
const swept: string[] = [];
|
|
134
|
-
|
|
135
|
-
const { result } = await tick({
|
|
136
|
-
sweepAttachments: async (item) => {
|
|
137
|
-
swept.push(item.accountId);
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
assert.deepEqual(swept.sort(), [due.accountId, fresh.accountId].sort());
|
|
142
|
-
assert.equal(result.swept, 2);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// Uncontained, one storage error reaches the loop, which exits, and the
|
|
146
|
-
// container restarts without ever enqueuing an account again.
|
|
147
|
-
it("keeps enqueuing mail when the attachment sweep throws", async () => {
|
|
148
|
-
const live = await account("live");
|
|
149
|
-
|
|
150
|
-
const { enqueued, result } = await tick({
|
|
151
|
-
sweepAttachments: async () => {
|
|
152
|
-
throw new Error("AccessDenied on ListObjectsV2");
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
assert.deepEqual(enqueued, [live.accountId]);
|
|
157
|
-
assert.equal(result.sweepFailed, 1);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("keeps the tick alive when the calendar subscription refresh throws", async () => {
|
|
161
|
-
const live = await account("live");
|
|
162
|
-
|
|
163
|
-
const { enqueued, logs } = await tick({
|
|
164
|
-
refreshCalendarSubscriptions: async () => {
|
|
165
|
-
throw new Error("database is locked");
|
|
166
|
-
},
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
assert.deepEqual(enqueued, [live.accountId]);
|
|
170
|
-
assert.ok(logs.some((line) => line.level === "error"));
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("counts what each account re-drove and keeps enqueuing past one that fails", async () => {
|
|
174
|
-
const live = await account("live");
|
|
175
|
-
const busy = await account("busy");
|
|
176
|
-
|
|
177
|
-
const { enqueued, result, logs } = await tick({
|
|
178
|
-
redriveStrandedPlacements: async (redriven) => {
|
|
179
|
-
if (redriven.accountId === busy.accountId) {
|
|
180
|
-
throw new Error("SQLITE_BUSY");
|
|
181
|
-
}
|
|
182
|
-
return 2;
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
assert.deepEqual(enqueued.sort(), [live.accountId, busy.accountId].sort());
|
|
187
|
-
assert.equal(result.redriven, 2);
|
|
188
|
-
assert.equal(result.redriveFailed, 1);
|
|
189
|
-
assert.ok(logs.some((line) => line.level === "error"));
|
|
190
|
-
});
|
|
191
|
-
});
|
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildScheduledSyncDedupId,
|
|
3
|
-
type SyncMailboxesEvent,
|
|
4
|
-
triggerAccountSync,
|
|
5
|
-
} from "@remit/backend/trigger-sync";
|
|
6
|
-
import type {
|
|
7
|
-
AccountItem,
|
|
8
|
-
FifoQueueProducer,
|
|
9
|
-
IAccountRepository,
|
|
10
|
-
} from "@remit/data-ports";
|
|
11
|
-
import type { Logger } from "@remit/logger";
|
|
12
|
-
import { hasStoredCredential } from "@remit/mailbox-service/account-credentials";
|
|
13
|
-
import pMap from "p-map";
|
|
14
|
-
import {
|
|
15
|
-
isAccountDeleted,
|
|
16
|
-
isAccountReauthRequired,
|
|
17
|
-
isMailSyncDisabled,
|
|
18
|
-
isUnsyncableHost,
|
|
19
|
-
} from "../account-check.js";
|
|
20
|
-
import type { CalendarSubscriptionTally } from "./calendar-subscriptions.js";
|
|
21
|
-
import {
|
|
22
|
-
SCHEDULER_ENQUEUE_CONCURRENCY,
|
|
23
|
-
SCHEDULER_PAGE_SIZE,
|
|
24
|
-
} from "./config.js";
|
|
25
|
-
import { isSyncDue } from "./decide-due.js";
|
|
26
|
-
|
|
27
|
-
export interface RunSchedulerTickDeps {
|
|
28
|
-
accountService: Pick<IAccountRepository, "listAllAccountsPage">;
|
|
29
|
-
queue: FifoQueueProducer<SyncMailboxesEvent>;
|
|
30
|
-
log: Logger;
|
|
31
|
-
offlineIntervalMs: number;
|
|
32
|
-
/**
|
|
33
|
-
* How often this tick itself runs — the dedup-id bucket width for
|
|
34
|
-
* `buildScheduledSyncDedupId`, so consecutive ticks each get a fresh id.
|
|
35
|
-
*/
|
|
36
|
-
tickIntervalMs: number;
|
|
37
|
-
/** Injectable for tests; defaults to `Date.now()`. */
|
|
38
|
-
now?: number;
|
|
39
|
-
/**
|
|
40
|
-
* Collect attachment bytes the database does not know about, for one account.
|
|
41
|
-
*
|
|
42
|
-
* The tick already walks every account on a cadence, which is the only thing
|
|
43
|
-
* this needs. It exists because an upload URL minted for a draft stays valid
|
|
44
|
-
* after that draft is discarded, and where the browser writes straight to
|
|
45
|
-
* block storage nothing is in the path to refuse it — see
|
|
46
|
-
* `sweepAbandonedOutboxAttachments`. Optional only so a test can leave it out;
|
|
47
|
-
* both real entry points pass it.
|
|
48
|
-
*/
|
|
49
|
-
sweepAttachments?: (account: AccountItem) => Promise<void>;
|
|
50
|
-
/**
|
|
51
|
-
* Re-read the calendar subscriptions whose last fetch is older than
|
|
52
|
-
* `offlineIntervalMs` — the same knob that says how stale mail may get.
|
|
53
|
-
* Optional only so a test can leave it out; both real entry points pass it.
|
|
54
|
-
*/
|
|
55
|
-
refreshCalendarSubscriptions?: (
|
|
56
|
-
now: number,
|
|
57
|
-
intervalMs: number,
|
|
58
|
-
) => Promise<CalendarSubscriptionTally>;
|
|
59
|
-
redriveStrandedPlacements: (
|
|
60
|
-
account: AccountItem,
|
|
61
|
-
now: number,
|
|
62
|
-
) => Promise<number>;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface SchedulerTickResult {
|
|
66
|
-
scanned: number;
|
|
67
|
-
enqueued: number;
|
|
68
|
-
skipped: number;
|
|
69
|
-
swept: number;
|
|
70
|
-
sweepFailed: number;
|
|
71
|
-
subscriptionsRefreshed: number;
|
|
72
|
-
subscriptionsFailed: number;
|
|
73
|
-
redriven: number;
|
|
74
|
-
redriveFailed: number;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* `isAccountDeleted` / `isUnsyncableHost` / `isAccountReauthRequired` each log
|
|
79
|
-
* one line per ineligible account — the right volume for their real call site
|
|
80
|
-
* (once per SYNC_MAILBOXES event). Run across the whole account base every
|
|
81
|
-
* tick, that becomes one log line per deleted/reauth/placeholder account
|
|
82
|
-
* every 5 minutes, forever (review #1250). The tick already reports the
|
|
83
|
-
* aggregate `skipped` count, so eligibility checks here go through a silent
|
|
84
|
-
* logger — the per-event path (sync-mailboxes.ts) is untouched and keeps
|
|
85
|
-
* logging normally.
|
|
86
|
-
*/
|
|
87
|
-
const silentLogger: Logger = (() => {
|
|
88
|
-
const noop = () => {};
|
|
89
|
-
const stub = {
|
|
90
|
-
info: noop,
|
|
91
|
-
warn: noop,
|
|
92
|
-
error: noop,
|
|
93
|
-
debug: noop,
|
|
94
|
-
fatal: noop,
|
|
95
|
-
trace: noop,
|
|
96
|
-
child: () => stub,
|
|
97
|
-
};
|
|
98
|
-
return stub as unknown as Logger;
|
|
99
|
-
})();
|
|
100
|
-
|
|
101
|
-
const isEligible = (account: AccountItem): boolean => {
|
|
102
|
-
if (isAccountDeleted(account, silentLogger)) return false;
|
|
103
|
-
if (isMailSyncDisabled(account, silentLogger)) return false;
|
|
104
|
-
if (isUnsyncableHost(account, silentLogger)) return false;
|
|
105
|
-
if (isAccountReauthRequired(account, silentLogger)) return false;
|
|
106
|
-
// An account storing no credential can only produce a sync that fails at
|
|
107
|
-
// credential resolution (issue #1120); the tick would enqueue one every
|
|
108
|
-
// tick, forever, for an account the user never finished setting up.
|
|
109
|
-
if (!hasStoredCredential(account)) return false;
|
|
110
|
-
return true;
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* One tick of the periodic mailbox-sync scheduler (#1247, restructured
|
|
115
|
-
* #1251). Pages through every account (never loading the whole account base
|
|
116
|
-
* into memory), decides per account via `isSyncDue` against the single
|
|
117
|
-
* offline threshold, and enqueues SYNC_MAILBOXES for the accounts that are
|
|
118
|
-
* due — in bounded-concurrency batches, never an unbounded `Promise.all`.
|
|
119
|
-
*
|
|
120
|
-
* Every enqueue goes through the same `triggerAccountSync` the manual
|
|
121
|
-
* POST /sync path uses, with a scheduler-specific, time-bucketed dedup id
|
|
122
|
-
* (`buildScheduledSyncDedupId`, bucketed by `tickIntervalMs`) so this tick
|
|
123
|
-
* can never collide with its own previous tick or with a concurrent manual
|
|
124
|
-
* trigger (see trigger-sync.ts). Concurrent-sync safety for the mailbox
|
|
125
|
-
* itself is MailboxLockService's job, inside the worker handler — this tick
|
|
126
|
-
* only decides "is a sync due" and enqueues; it never talks to IMAP.
|
|
127
|
-
*/
|
|
128
|
-
export const runSchedulerTick = async (
|
|
129
|
-
deps: RunSchedulerTickDeps,
|
|
130
|
-
): Promise<SchedulerTickResult> => {
|
|
131
|
-
const { accountService, queue, log, offlineIntervalMs, tickIntervalMs } =
|
|
132
|
-
deps;
|
|
133
|
-
const now = deps.now ?? Date.now();
|
|
134
|
-
|
|
135
|
-
let cursor: string | undefined;
|
|
136
|
-
let scanned = 0;
|
|
137
|
-
let enqueued = 0;
|
|
138
|
-
let skipped = 0;
|
|
139
|
-
let swept = 0;
|
|
140
|
-
let sweepFailed = 0;
|
|
141
|
-
let redriven = 0;
|
|
142
|
-
let redriveFailed = 0;
|
|
143
|
-
|
|
144
|
-
do {
|
|
145
|
-
const page = await accountService.listAllAccountsPage({
|
|
146
|
-
limit: SCHEDULER_PAGE_SIZE,
|
|
147
|
-
cursor,
|
|
148
|
-
});
|
|
149
|
-
scanned += page.items.length;
|
|
150
|
-
|
|
151
|
-
const due = page.items.filter(
|
|
152
|
-
(account) =>
|
|
153
|
-
isEligible(account) && isSyncDue(account, now, offlineIntervalMs),
|
|
154
|
-
);
|
|
155
|
-
skipped += page.items.length - due.length;
|
|
156
|
-
|
|
157
|
-
await pMap(
|
|
158
|
-
due,
|
|
159
|
-
(account) =>
|
|
160
|
-
triggerAccountSync({
|
|
161
|
-
queue,
|
|
162
|
-
accountId: account.accountId,
|
|
163
|
-
dedupId: buildScheduledSyncDedupId(
|
|
164
|
-
account.accountId,
|
|
165
|
-
now,
|
|
166
|
-
tickIntervalMs,
|
|
167
|
-
),
|
|
168
|
-
}),
|
|
169
|
-
{ concurrency: SCHEDULER_ENQUEUE_CONCURRENCY },
|
|
170
|
-
);
|
|
171
|
-
enqueued += due.length;
|
|
172
|
-
|
|
173
|
-
if (deps.sweepAttachments) {
|
|
174
|
-
// After the enqueue, never before it. Enqueuing mail is the tick's job
|
|
175
|
-
// and collecting bytes is housekeeping; on a hosted Lambda a slow sweep
|
|
176
|
-
// ahead of the enqueue eats the invocation budget, and a timeout is not
|
|
177
|
-
// catchable, so the remaining pages would never be enqueued at all.
|
|
178
|
-
//
|
|
179
|
-
// Every account, not only the ones due a sync: abandoned bytes have
|
|
180
|
-
// nothing to do with how recently mail was fetched. Bounded to the same
|
|
181
|
-
// concurrency as the enqueues.
|
|
182
|
-
//
|
|
183
|
-
// Contained, because this is housekeeping and enqueuing mail is not. An
|
|
184
|
-
// unreadable input or a storage permission error thrown from here would
|
|
185
|
-
// otherwise reach the loop, which logs and exits, and compose would
|
|
186
|
-
// restart the process every five seconds — no account would ever be
|
|
187
|
-
// enqueued again. A failed sweep costs a tick's collection and nothing
|
|
188
|
-
// else; the count is what makes that visible.
|
|
189
|
-
const sweep = deps.sweepAttachments;
|
|
190
|
-
const outcomes = await pMap(
|
|
191
|
-
page.items,
|
|
192
|
-
(account) =>
|
|
193
|
-
sweep(account).then(
|
|
194
|
-
() => true,
|
|
195
|
-
(error: unknown) => {
|
|
196
|
-
log.error(
|
|
197
|
-
{ accountId: account.accountId, error },
|
|
198
|
-
"Outbox attachment sweep failed for account",
|
|
199
|
-
);
|
|
200
|
-
return false;
|
|
201
|
-
},
|
|
202
|
-
),
|
|
203
|
-
{ concurrency: SCHEDULER_ENQUEUE_CONCURRENCY },
|
|
204
|
-
);
|
|
205
|
-
swept += outcomes.filter(Boolean).length;
|
|
206
|
-
sweepFailed += outcomes.filter((ok) => !ok).length;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Contained like the sweep, and for the same reason: a failed re-drive
|
|
210
|
-
// costs one account this tick, never the scheduler. Eligible accounts
|
|
211
|
-
// only, since a re-driven event needs a connection to settle anything.
|
|
212
|
-
const redrives = await pMap(
|
|
213
|
-
page.items.filter(isEligible),
|
|
214
|
-
(account) =>
|
|
215
|
-
deps.redriveStrandedPlacements(account, now).then(
|
|
216
|
-
(count) => count,
|
|
217
|
-
(error: unknown) => {
|
|
218
|
-
log.error(
|
|
219
|
-
{ accountId: account.accountId, error },
|
|
220
|
-
"Stranded placement re-drive failed for account",
|
|
221
|
-
);
|
|
222
|
-
return null;
|
|
223
|
-
},
|
|
224
|
-
),
|
|
225
|
-
{ concurrency: SCHEDULER_ENQUEUE_CONCURRENCY },
|
|
226
|
-
);
|
|
227
|
-
for (const count of redrives) {
|
|
228
|
-
if (count === null) {
|
|
229
|
-
redriveFailed += 1;
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
redriven += count;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
cursor = page.cursor ?? undefined;
|
|
236
|
-
} while (cursor);
|
|
237
|
-
|
|
238
|
-
let subscriptionsRefreshed = 0;
|
|
239
|
-
let subscriptionsFailed = 0;
|
|
240
|
-
if (deps.refreshCalendarSubscriptions) {
|
|
241
|
-
// After every page is enqueued, for the same reason the sweep runs after
|
|
242
|
-
// the enqueue: a slow feed must never cost an account its mail. Contained
|
|
243
|
-
// like the sweep, so an unreadable store costs a tick's refresh and not
|
|
244
|
-
// the scheduler.
|
|
245
|
-
const tally = await deps
|
|
246
|
-
.refreshCalendarSubscriptions(now, offlineIntervalMs)
|
|
247
|
-
.catch((error: unknown) => {
|
|
248
|
-
log.error({ error }, "Calendar subscription refresh failed");
|
|
249
|
-
return { refreshed: 0, failed: 0, notDue: 0 };
|
|
250
|
-
});
|
|
251
|
-
subscriptionsRefreshed = tally.refreshed;
|
|
252
|
-
subscriptionsFailed = tally.failed;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
log.info(
|
|
256
|
-
{
|
|
257
|
-
scanned,
|
|
258
|
-
enqueued,
|
|
259
|
-
skipped,
|
|
260
|
-
swept,
|
|
261
|
-
sweepFailed,
|
|
262
|
-
subscriptionsRefreshed,
|
|
263
|
-
subscriptionsFailed,
|
|
264
|
-
redriven,
|
|
265
|
-
redriveFailed,
|
|
266
|
-
},
|
|
267
|
-
"Scheduled-sync tick complete",
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
return {
|
|
271
|
-
scanned,
|
|
272
|
-
enqueued,
|
|
273
|
-
skipped,
|
|
274
|
-
swept,
|
|
275
|
-
sweepFailed,
|
|
276
|
-
subscriptionsRefreshed,
|
|
277
|
-
subscriptionsFailed,
|
|
278
|
-
redriven,
|
|
279
|
-
redriveFailed,
|
|
280
|
-
};
|
|
281
|
-
};
|