@remit/imap-worker 0.0.90 → 0.0.92
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 +22 -0
- package/dist/scheduler/calendar-subscriptions.js +35 -0
- package/dist/scheduler/config.d.ts +29 -0
- package/{src/scheduler/config.ts → dist/scheduler/config.js} +9 -28
- 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/dist/scheduler/handler.js +50 -0
- 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 +66 -0
- package/dist/scheduler/run-tick.js +164 -0
- package/dist/scheduler/runner.d.ts +2 -0
- package/dist/scheduler/runner.js +102 -0
- package/dist/scheduler/stranded-placement-redrive.d.ts +16 -0
- package/dist/scheduler/stranded-placement-redrive.js +172 -0
- package/dist/scheduler/tick.d.ts +5 -0
- package/dist/scheduler/tick.js +5 -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 +79 -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 -218
- 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 -145
- package/src/emit.ts +0 -115
- 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 -126
- package/src/handlers/delete-account-objects.ts +0 -96
- 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 -569
- 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 -888
- 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 -755
- package/src/handlers/placement-move-push.test.ts +0 -137
- package/src/handlers/placement-move-push.ts +0 -478
- package/src/handlers/placement-push.sqlite.test.ts +0 -299
- 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 -425
- 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 -366
- package/src/handlers/sync-messages.sqlite.test.ts +0 -195
- package/src/handlers/sync-messages.ts +0 -537
- package/src/index.test.ts +0 -22
- package/src/index.ts +0 -82
- package/src/mail-sync-selection.sqlite.test.ts +0 -227
- package/src/mailbox-row.ts +0 -20
- package/src/pending-sync.sqlite.test.ts +0 -165
- package/src/poller.ts +0 -55
- package/src/processor.ts +0 -62
- package/src/queues.ts +0 -40
- package/src/scheduler/attachment-sweep.sqlite.test.ts +0 -74
- 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/handler.ts +0 -69
- 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/runner.ts +0 -125
- package/src/scheduler/stranded-placement-redrive.ts +0 -265
- package/src/stranded-placement.sqlite.test.ts +0 -799
- package/src/test-helpers/test-world.ts +0 -425
- package/src/test-helpers/worker-queues.ts +0 -11
- 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,266 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert";
|
|
2
|
-
import { afterEach, beforeEach, describe, test } from "node:test";
|
|
3
|
-
import type { IImapConnection } from "@remit/mailbox-service";
|
|
4
|
-
import {
|
|
5
|
-
__evictWarmConnectionsForTest,
|
|
6
|
-
__resetWarmPoolsForTest,
|
|
7
|
-
__warmPoolSizeForTest,
|
|
8
|
-
borrowWarmConnection,
|
|
9
|
-
type ConnectionScope,
|
|
10
|
-
} from "./connection-scope.js";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* A fake connection whose liveness can be toggled, recording connect/disconnect
|
|
14
|
-
* so tests can assert reuse (no reconnect) and leak-freedom (every connect is
|
|
15
|
-
* eventually disconnected or reused, never dangling).
|
|
16
|
-
*/
|
|
17
|
-
interface FakeScope extends ConnectionScope {
|
|
18
|
-
id: number;
|
|
19
|
-
connectCount: number;
|
|
20
|
-
disconnectCount: number;
|
|
21
|
-
kill: () => void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
let nextId = 0;
|
|
25
|
-
|
|
26
|
-
const makeFakeScope = (): FakeScope => {
|
|
27
|
-
const id = nextId++;
|
|
28
|
-
let alive = true;
|
|
29
|
-
let conn: IImapConnection | null = null;
|
|
30
|
-
|
|
31
|
-
const scope: FakeScope = {
|
|
32
|
-
id,
|
|
33
|
-
connectCount: 0,
|
|
34
|
-
disconnectCount: 0,
|
|
35
|
-
kill: () => {
|
|
36
|
-
alive = false;
|
|
37
|
-
},
|
|
38
|
-
getConnection: async () => {
|
|
39
|
-
if (!conn) {
|
|
40
|
-
scope.connectCount++;
|
|
41
|
-
conn = {
|
|
42
|
-
get isConnected() {
|
|
43
|
-
return alive;
|
|
44
|
-
},
|
|
45
|
-
} as IImapConnection;
|
|
46
|
-
}
|
|
47
|
-
return conn;
|
|
48
|
-
},
|
|
49
|
-
disconnect: async () => {
|
|
50
|
-
scope.disconnectCount++;
|
|
51
|
-
conn = null;
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
return scope;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
describe("borrowWarmConnection — cross-invocation warm reuse", () => {
|
|
58
|
-
const accountId = "acct-warm-1";
|
|
59
|
-
|
|
60
|
-
beforeEach(() => {
|
|
61
|
-
__resetWarmPoolsForTest();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
afterEach(async () => {
|
|
65
|
-
await __evictWarmConnectionsForTest(accountId);
|
|
66
|
-
__resetWarmPoolsForTest();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("warm invocation reuses the live connection without reconnecting", async () => {
|
|
70
|
-
const scopes: FakeScope[] = [];
|
|
71
|
-
const createScope = (): ConnectionScope => {
|
|
72
|
-
const s = makeFakeScope();
|
|
73
|
-
scopes.push(s);
|
|
74
|
-
return s;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const first = borrowWarmConnection(accountId, createScope);
|
|
78
|
-
const connA = await first.getConnection();
|
|
79
|
-
await first.release();
|
|
80
|
-
|
|
81
|
-
const second = borrowWarmConnection(accountId, createScope);
|
|
82
|
-
const connB = await second.getConnection();
|
|
83
|
-
await second.release();
|
|
84
|
-
|
|
85
|
-
assert.strictEqual(
|
|
86
|
-
connA,
|
|
87
|
-
connB,
|
|
88
|
-
"second invocation reuses same connection",
|
|
89
|
-
);
|
|
90
|
-
assert.strictEqual(scopes.length, 1, "no second scope created");
|
|
91
|
-
assert.strictEqual(scopes[0].connectCount, 1, "connected exactly once");
|
|
92
|
-
assert.strictEqual(scopes[0].disconnectCount, 0, "never disconnected");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("dead connection triggers reconnect and replaces the cache entry", async () => {
|
|
96
|
-
const scopes: FakeScope[] = [];
|
|
97
|
-
const createScope = (): ConnectionScope => {
|
|
98
|
-
const s = makeFakeScope();
|
|
99
|
-
scopes.push(s);
|
|
100
|
-
return s;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const first = borrowWarmConnection(accountId, createScope);
|
|
104
|
-
await first.getConnection();
|
|
105
|
-
await first.release();
|
|
106
|
-
|
|
107
|
-
// Provider drops the idle socket: imapflow flips isConnected to false.
|
|
108
|
-
scopes[0].kill();
|
|
109
|
-
|
|
110
|
-
const second = borrowWarmConnection(accountId, createScope);
|
|
111
|
-
const connB = await second.getConnection();
|
|
112
|
-
await second.release();
|
|
113
|
-
|
|
114
|
-
assert.strictEqual(scopes.length, 2, "a replacement scope was created");
|
|
115
|
-
assert.strictEqual(scopes[0].disconnectCount, 1, "dead conn cleaned up");
|
|
116
|
-
assert.strictEqual(scopes[1].connectCount, 1, "replacement connected once");
|
|
117
|
-
assert.ok(connB.isConnected, "replacement connection is live");
|
|
118
|
-
assert.strictEqual(
|
|
119
|
-
__warmPoolSizeForTest(accountId),
|
|
120
|
-
1,
|
|
121
|
-
"pool did not grow when replacing a dead entry",
|
|
122
|
-
);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("no connection leak: pool size stays bounded across many invocations", async () => {
|
|
126
|
-
const scopes: FakeScope[] = [];
|
|
127
|
-
const createScope = (): ConnectionScope => {
|
|
128
|
-
const s = makeFakeScope();
|
|
129
|
-
scopes.push(s);
|
|
130
|
-
return s;
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
for (let i = 0; i < 20; i++) {
|
|
134
|
-
const borrowed = borrowWarmConnection(accountId, createScope);
|
|
135
|
-
await borrowed.getConnection();
|
|
136
|
-
await borrowed.release();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
assert.strictEqual(scopes.length, 1, "serial reuse never re-dials");
|
|
140
|
-
assert.strictEqual(
|
|
141
|
-
__warmPoolSizeForTest(accountId),
|
|
142
|
-
1,
|
|
143
|
-
"pool stays at one for serial same-account work",
|
|
144
|
-
);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test("concurrent same-account borrows get distinct pooled connections", async () => {
|
|
148
|
-
const scopes: FakeScope[] = [];
|
|
149
|
-
const createScope = (): ConnectionScope => {
|
|
150
|
-
const s = makeFakeScope();
|
|
151
|
-
scopes.push(s);
|
|
152
|
-
return s;
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const a = borrowWarmConnection(accountId, createScope);
|
|
156
|
-
const b = borrowWarmConnection(accountId, createScope);
|
|
157
|
-
const connA = await a.getConnection();
|
|
158
|
-
const connB = await b.getConnection();
|
|
159
|
-
|
|
160
|
-
assert.notStrictEqual(
|
|
161
|
-
connA,
|
|
162
|
-
connB,
|
|
163
|
-
"concurrent borrows must not share one imapflow connection",
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
await a.release();
|
|
167
|
-
await b.release();
|
|
168
|
-
|
|
169
|
-
// default connectionsPerAccount is 2 — both stay pooled, none leaked.
|
|
170
|
-
assert.strictEqual(__warmPoolSizeForTest(accountId), 2);
|
|
171
|
-
assert.ok(scopes.every((s) => s.disconnectCount === 0));
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test("overflow connection (pool saturated) is disconnected on release", async () => {
|
|
175
|
-
const scopes: FakeScope[] = [];
|
|
176
|
-
const createScope = (): ConnectionScope => {
|
|
177
|
-
const s = makeFakeScope();
|
|
178
|
-
scopes.push(s);
|
|
179
|
-
return s;
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
// Saturate the pool (default size 2) with two held borrows.
|
|
183
|
-
const a = borrowWarmConnection(accountId, createScope);
|
|
184
|
-
const b = borrowWarmConnection(accountId, createScope);
|
|
185
|
-
await a.getConnection();
|
|
186
|
-
await b.getConnection();
|
|
187
|
-
|
|
188
|
-
// Third concurrent borrow overflows the pool.
|
|
189
|
-
const c = borrowWarmConnection(accountId, createScope);
|
|
190
|
-
await c.getConnection();
|
|
191
|
-
await c.release();
|
|
192
|
-
|
|
193
|
-
assert.strictEqual(scopes.length, 3, "overflow created a third scope");
|
|
194
|
-
assert.strictEqual(scopes[2].disconnectCount, 1, "overflow torn down");
|
|
195
|
-
assert.strictEqual(
|
|
196
|
-
__warmPoolSizeForTest(accountId),
|
|
197
|
-
2,
|
|
198
|
-
"overflow did not grow the pool",
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
await a.release();
|
|
202
|
-
await b.release();
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test("concurrent borrows racing on a dead pooled entry get distinct connections", async () => {
|
|
206
|
-
const scopes: FakeScope[] = [];
|
|
207
|
-
const createScope = (): ConnectionScope => {
|
|
208
|
-
const s = makeFakeScope();
|
|
209
|
-
scopes.push(s);
|
|
210
|
-
return s;
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
// Seed the pool with a single connection, then kill it so it is a dead
|
|
214
|
-
// FREE entry — the recycle path that recreates a scope across an awaited
|
|
215
|
-
// disconnect (the race window).
|
|
216
|
-
const seed = borrowWarmConnection(accountId, createScope);
|
|
217
|
-
await seed.getConnection();
|
|
218
|
-
await seed.release();
|
|
219
|
-
scopes[0].kill();
|
|
220
|
-
|
|
221
|
-
// Two borrows started before either resolves: both run their synchronous
|
|
222
|
-
// claim phase, then A awaits disconnectQuietly while B's claim runs. The
|
|
223
|
-
// dead entry must be claimed atomically so they never adopt the same slot.
|
|
224
|
-
const a = borrowWarmConnection(accountId, createScope);
|
|
225
|
-
const b = borrowWarmConnection(accountId, createScope);
|
|
226
|
-
const [connA, connB] = await Promise.all([
|
|
227
|
-
a.getConnection(),
|
|
228
|
-
b.getConnection(),
|
|
229
|
-
]);
|
|
230
|
-
|
|
231
|
-
assert.notStrictEqual(
|
|
232
|
-
connA,
|
|
233
|
-
connB,
|
|
234
|
-
"racing borrows must not share one imapflow connection",
|
|
235
|
-
);
|
|
236
|
-
assert.ok(connA.isConnected && connB.isConnected, "both are live");
|
|
237
|
-
|
|
238
|
-
await a.release();
|
|
239
|
-
await b.release();
|
|
240
|
-
|
|
241
|
-
// One slot recycled in place + one grown = pool of 2 (default cap), no leak.
|
|
242
|
-
assert.strictEqual(__warmPoolSizeForTest(accountId), 2);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
test("__evictWarmConnectionsForTest disconnects and clears the pool", async () => {
|
|
246
|
-
const scopes: FakeScope[] = [];
|
|
247
|
-
const createScope = (): ConnectionScope => {
|
|
248
|
-
const s = makeFakeScope();
|
|
249
|
-
scopes.push(s);
|
|
250
|
-
return s;
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
const borrowed = borrowWarmConnection(accountId, createScope);
|
|
254
|
-
await borrowed.getConnection();
|
|
255
|
-
await borrowed.release();
|
|
256
|
-
|
|
257
|
-
await __evictWarmConnectionsForTest(accountId);
|
|
258
|
-
|
|
259
|
-
assert.strictEqual(__warmPoolSizeForTest(accountId), 0);
|
|
260
|
-
assert.strictEqual(
|
|
261
|
-
scopes[0].disconnectCount,
|
|
262
|
-
1,
|
|
263
|
-
"evicted conn disconnected",
|
|
264
|
-
);
|
|
265
|
-
});
|
|
266
|
-
});
|
package/src/connection-scope.ts
DELETED
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Connection scope utility for managing IMAP connections across event processing.
|
|
3
|
-
*
|
|
4
|
-
* Creates a lazily-connected, cached connection that can be shared across
|
|
5
|
-
* multiple operations during a single event's lifetime.
|
|
6
|
-
*
|
|
7
|
-
* On top of the per-event scope, this module keeps a MODULE-scoped pool of warm
|
|
8
|
-
* connections keyed by accountId so a warm Lambda container reuses live IMAP
|
|
9
|
-
* connections across invocations instead of paying TCP+TLS+LOGIN+SELECT every
|
|
10
|
-
* time (see #605).
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
createConnection,
|
|
15
|
-
createConnectionWithCredentials,
|
|
16
|
-
type IImapConnection,
|
|
17
|
-
type ImapConnectionConfig,
|
|
18
|
-
type MailCredentials,
|
|
19
|
-
} from "@remit/mailbox-service";
|
|
20
|
-
|
|
21
|
-
export interface ConnectionScope {
|
|
22
|
-
/**
|
|
23
|
-
* Get the connection, connecting lazily if not already connected.
|
|
24
|
-
* Returns the same connection instance on subsequent calls.
|
|
25
|
-
*/
|
|
26
|
-
getConnection: () => Promise<IImapConnection>;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Disconnect the connection if it was ever connected.
|
|
30
|
-
* Safe to call multiple times.
|
|
31
|
-
*/
|
|
32
|
-
disconnect: () => Promise<void>;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Create a connection scope that manages a single IMAP connection's lifecycle.
|
|
37
|
-
*
|
|
38
|
-
* The connection is created lazily on first call to getConnection() and
|
|
39
|
-
* reused for all subsequent calls. Call disconnect() when done to clean up.
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* ```typescript
|
|
43
|
-
* const scope = createConnectionScope(config);
|
|
44
|
-
*
|
|
45
|
-
* await doWork(scope.getConnection)
|
|
46
|
-
* .finally(() => scope.disconnect());
|
|
47
|
-
* ```
|
|
48
|
-
*/
|
|
49
|
-
export const createConnectionScope = (
|
|
50
|
-
config: ImapConnectionConfig,
|
|
51
|
-
): ConnectionScope => {
|
|
52
|
-
let connection: IImapConnection | null = null;
|
|
53
|
-
let connectPromise: Promise<IImapConnection> | null = null;
|
|
54
|
-
|
|
55
|
-
const getConnection = async (): Promise<IImapConnection> => {
|
|
56
|
-
if (connectPromise) {
|
|
57
|
-
return connectPromise;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const conn = createConnection(config);
|
|
61
|
-
connection = conn;
|
|
62
|
-
connectPromise = conn.connect().then(() => conn);
|
|
63
|
-
|
|
64
|
-
return connectPromise;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const disconnect = async (): Promise<void> => {
|
|
68
|
-
if (connection) {
|
|
69
|
-
await connection.disconnect();
|
|
70
|
-
connection = null;
|
|
71
|
-
connectPromise = null;
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
return { getConnection, disconnect };
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Create a connection scope from account data and a MailCredentials union.
|
|
80
|
-
* Use this for all handlers that support both password and OAuth accounts.
|
|
81
|
-
*/
|
|
82
|
-
export const createConnectionScopeWithCredentials = (
|
|
83
|
-
account: {
|
|
84
|
-
username: string;
|
|
85
|
-
imapHost: string;
|
|
86
|
-
imapPort: number;
|
|
87
|
-
imapTls: boolean;
|
|
88
|
-
},
|
|
89
|
-
credentials: MailCredentials,
|
|
90
|
-
): ConnectionScope => {
|
|
91
|
-
let connection: IImapConnection | null = null;
|
|
92
|
-
let connectPromise: Promise<IImapConnection> | null = null;
|
|
93
|
-
|
|
94
|
-
const getConnection = async (): Promise<IImapConnection> => {
|
|
95
|
-
if (connectPromise) {
|
|
96
|
-
return connectPromise;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const conn = createConnectionWithCredentials(account, credentials);
|
|
100
|
-
connection = conn;
|
|
101
|
-
connectPromise = conn.connect().then(() => conn);
|
|
102
|
-
|
|
103
|
-
return connectPromise;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const disconnect = async (): Promise<void> => {
|
|
107
|
-
if (connection) {
|
|
108
|
-
await connection.disconnect();
|
|
109
|
-
connection = null;
|
|
110
|
-
connectPromise = null;
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
return { getConnection, disconnect };
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Number of warm connections to hold per account in a single container.
|
|
119
|
-
*
|
|
120
|
-
* Default 2 (Hostnet-conservative). imapflow runs one command at a time, so a
|
|
121
|
-
* single cached connection cannot be shared by concurrent invocations of the
|
|
122
|
-
* same account in one container (the worker runs handlers under p-map). The
|
|
123
|
-
* pool lets concurrent same-account invocations each borrow a live connection
|
|
124
|
-
* while still reusing them across invocations.
|
|
125
|
-
*
|
|
126
|
-
* CAVEAT: warm reuse only helps WITHIN one container. Lambda may run many
|
|
127
|
-
* containers (each with its own pool) and recycle them, so this cuts login
|
|
128
|
-
* churn but does NOT by itself bound the connections an account opens against
|
|
129
|
-
* the provider — the queue concurrency cap (#610) does that.
|
|
130
|
-
*/
|
|
131
|
-
const connectionsPerAccount = (() => {
|
|
132
|
-
const raw = Number(process.env.CONNECTIONS_PER_ACCOUNT);
|
|
133
|
-
return Number.isInteger(raw) && raw > 0 ? raw : 2;
|
|
134
|
-
})();
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* A pooled, lazily-connected connection plus its liveness/borrow bookkeeping.
|
|
138
|
-
*/
|
|
139
|
-
interface PooledConnection {
|
|
140
|
-
scope: ConnectionScope;
|
|
141
|
-
/** The established connection once getConnection() has resolved. */
|
|
142
|
-
connection: IImapConnection | null;
|
|
143
|
-
/** Borrowed by an in-flight invocation; must not be lent out concurrently. */
|
|
144
|
-
busy: boolean;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** accountId -> warm pool. Module scope so it survives across invocations. */
|
|
148
|
-
const warmPools = new Map<string, PooledConnection[]>();
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* A connection borrowed from the warm pool for the duration of one invocation.
|
|
152
|
-
*/
|
|
153
|
-
export interface BorrowedConnection {
|
|
154
|
-
/** Get the (possibly cached, liveness-checked) live connection. */
|
|
155
|
-
getConnection: () => Promise<IImapConnection>;
|
|
156
|
-
/**
|
|
157
|
-
* Return the connection to the pool. Pooled connections stay connected for
|
|
158
|
-
* reuse; overflow connections (created when the pool is saturated) are
|
|
159
|
-
* disconnected so the pool never grows past connectionsPerAccount.
|
|
160
|
-
*/
|
|
161
|
-
release: () => Promise<void>;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* A live connection passes liveness when the underlying socket is still
|
|
166
|
-
* authenticated. imapflow flips `isConnected` to false on its `close`/`error`
|
|
167
|
-
* events, which fire when the provider drops an idle socket — the dominant
|
|
168
|
-
* warm-container failure mode. A connection that fails this check is replaced.
|
|
169
|
-
*/
|
|
170
|
-
const isLive = (entry: PooledConnection): boolean =>
|
|
171
|
-
entry.connection?.isConnected ?? false;
|
|
172
|
-
|
|
173
|
-
const disconnectQuietly = async (entry: PooledConnection): Promise<void> => {
|
|
174
|
-
try {
|
|
175
|
-
await entry.scope.disconnect();
|
|
176
|
-
} catch {
|
|
177
|
-
// A dead connection may already be gone; reclaiming it must never throw.
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Borrow a warm connection for one invocation, keyed by accountId.
|
|
183
|
-
*
|
|
184
|
-
* Reuses a free, live pooled connection when one exists (no reconnect). A free
|
|
185
|
-
* connection that has gone dead is disconnected and replaced. When every pooled
|
|
186
|
-
* connection is busy and the pool is full, an overflow connection is created and
|
|
187
|
-
* torn down on release so the steady-state pool size stays at
|
|
188
|
-
* connectionsPerAccount.
|
|
189
|
-
*
|
|
190
|
-
* The caller MUST call release() (e.g. in a finally) so the connection returns
|
|
191
|
-
* to the pool; releasing does NOT disconnect a pooled connection.
|
|
192
|
-
*/
|
|
193
|
-
export const borrowWarmConnection = (
|
|
194
|
-
accountId: string,
|
|
195
|
-
createScope: () => ConnectionScope,
|
|
196
|
-
): BorrowedConnection => {
|
|
197
|
-
const pool = warmPools.get(accountId) ?? [];
|
|
198
|
-
if (!warmPools.has(accountId)) {
|
|
199
|
-
warmPools.set(accountId, pool);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
let entry: PooledConnection | undefined;
|
|
203
|
-
let isOverflow = false;
|
|
204
|
-
|
|
205
|
-
const claimFreeLiveEntry = (): PooledConnection | undefined => {
|
|
206
|
-
for (const candidate of pool) {
|
|
207
|
-
if (candidate.busy) {
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
if (isLive(candidate)) {
|
|
211
|
-
candidate.busy = true;
|
|
212
|
-
return candidate;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return undefined;
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
const claimDeadOrEmptyEntry = (): PooledConnection | undefined => {
|
|
219
|
-
for (const candidate of pool) {
|
|
220
|
-
if (!candidate.busy && !isLive(candidate)) {
|
|
221
|
-
// Claim synchronously BEFORE the awaited disconnect in the recycle
|
|
222
|
-
// branch, mirroring claimFreeLiveEntry. Without this, a concurrent
|
|
223
|
-
// same-account borrow could claim the same dead entry during that
|
|
224
|
-
// await and two invocations would share one imapflow connection.
|
|
225
|
-
candidate.busy = true;
|
|
226
|
-
return candidate;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return undefined;
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
const getConnection = async (): Promise<IImapConnection> => {
|
|
233
|
-
if (entry) {
|
|
234
|
-
const conn = await entry.scope.getConnection();
|
|
235
|
-
entry.connection = conn;
|
|
236
|
-
return conn;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const reused = claimFreeLiveEntry();
|
|
240
|
-
if (reused) {
|
|
241
|
-
entry = reused;
|
|
242
|
-
const conn = await reused.scope.getConnection();
|
|
243
|
-
reused.connection = conn;
|
|
244
|
-
return conn;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// No free live connection: reuse a dead/never-connected slot if one is
|
|
248
|
-
// free, otherwise grow the pool, otherwise go overflow.
|
|
249
|
-
// claimDeadOrEmptyEntry already set busy=true synchronously, so the
|
|
250
|
-
// recycle slot is fenced off across the awaited disconnect below.
|
|
251
|
-
const recyclable = claimDeadOrEmptyEntry();
|
|
252
|
-
if (recyclable) {
|
|
253
|
-
await disconnectQuietly(recyclable);
|
|
254
|
-
recyclable.scope = createScope();
|
|
255
|
-
recyclable.connection = null;
|
|
256
|
-
entry = recyclable;
|
|
257
|
-
} else if (pool.length < connectionsPerAccount) {
|
|
258
|
-
const fresh: PooledConnection = {
|
|
259
|
-
scope: createScope(),
|
|
260
|
-
connection: null,
|
|
261
|
-
busy: true,
|
|
262
|
-
};
|
|
263
|
-
pool.push(fresh);
|
|
264
|
-
entry = fresh;
|
|
265
|
-
} else {
|
|
266
|
-
isOverflow = true;
|
|
267
|
-
entry = { scope: createScope(), connection: null, busy: true };
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const conn = await entry.scope.getConnection();
|
|
271
|
-
entry.connection = conn;
|
|
272
|
-
return conn;
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
const release = async (): Promise<void> => {
|
|
276
|
-
if (!entry) {
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
if (isOverflow) {
|
|
280
|
-
await disconnectQuietly(entry);
|
|
281
|
-
entry = undefined;
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
entry.busy = false;
|
|
285
|
-
entry = undefined;
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
return { getConnection, release };
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Test-only: disconnect and drop an account's warm pool so a test never leaks a
|
|
293
|
-
* live connection across cases. Not part of the production lifecycle — in
|
|
294
|
-
* steady state dead entries are recycled lazily on the next borrow.
|
|
295
|
-
*/
|
|
296
|
-
export const __evictWarmConnectionsForTest = async (
|
|
297
|
-
accountId: string,
|
|
298
|
-
): Promise<void> => {
|
|
299
|
-
const pool = warmPools.get(accountId);
|
|
300
|
-
if (!pool) {
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
warmPools.delete(accountId);
|
|
304
|
-
await Promise.all(pool.map(disconnectQuietly));
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
/** Test-only: clear all warm pools without disconnecting (fixtures own teardown). */
|
|
308
|
-
export const __resetWarmPoolsForTest = (): void => {
|
|
309
|
-
warmPools.clear();
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
/** Test-only: inspect the live pool size for an account. */
|
|
313
|
-
export const __warmPoolSizeForTest = (accountId: string): number =>
|
|
314
|
-
warmPools.get(accountId)?.length ?? 0;
|