@serve.zone/coremail 1.0.0
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/.smartconfig.json +50 -0
- package/changelog.md +10 -0
- package/cli.js +4 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.auth.d.ts +45 -0
- package/dist_ts/classes.auth.js +204 -0
- package/dist_ts/classes.config.d.ts +2 -0
- package/dist_ts/classes.config.js +72 -0
- package/dist_ts/classes.coremail.d.ts +41 -0
- package/dist_ts/classes.coremail.js +155 -0
- package/dist_ts/classes.desiredstate.d.ts +40 -0
- package/dist_ts/classes.desiredstate.js +382 -0
- package/dist_ts/classes.gateway.d.ts +62 -0
- package/dist_ts/classes.gateway.js +669 -0
- package/dist_ts/classes.inbound.d.ts +49 -0
- package/dist_ts/classes.inbound.js +702 -0
- package/dist_ts/classes.maintenance.d.ts +16 -0
- package/dist_ts/classes.maintenance.js +339 -0
- package/dist_ts/classes.models.d.ts +161 -0
- package/dist_ts/classes.models.js +595 -0
- package/dist_ts/classes.server.d.ts +40 -0
- package/dist_ts/classes.server.js +309 -0
- package/dist_ts/classes.storage.d.ts +32 -0
- package/dist_ts/classes.storage.js +317 -0
- package/dist_ts/classes.submissions.d.ts +33 -0
- package/dist_ts/classes.submissions.js +385 -0
- package/dist_ts/classes.transfer.d.ts +41 -0
- package/dist_ts/classes.transfer.js +289 -0
- package/dist_ts/coremail.cursor.d.ts +20 -0
- package/dist_ts/coremail.cursor.js +191 -0
- package/dist_ts/coremail.log.d.ts +3 -0
- package/dist_ts/coremail.log.js +11 -0
- package/dist_ts/coremail.mime.d.ts +8 -0
- package/dist_ts/coremail.mime.js +63 -0
- package/dist_ts/coremail.persistence.d.ts +148 -0
- package/dist_ts/coremail.persistence.js +695 -0
- package/dist_ts/coremail.quota.d.ts +29 -0
- package/dist_ts/coremail.quota.js +149 -0
- package/dist_ts/coremail.selectors.d.ts +3 -0
- package/dist_ts/coremail.selectors.js +21 -0
- package/dist_ts/coremail.validation.d.ts +9 -0
- package/dist_ts/coremail.validation.js +325 -0
- package/dist_ts/index.d.ts +4 -0
- package/dist_ts/index.js +39 -0
- package/dist_ts/interfaces.d.ts +92 -0
- package/dist_ts/interfaces.js +2 -0
- package/dist_ts/plugins.d.ts +11 -0
- package/dist_ts/plugins.js +12 -0
- package/license.md +19 -0
- package/package.json +48 -0
- package/readme.md +199 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.auth.ts +321 -0
- package/ts/classes.config.ts +104 -0
- package/ts/classes.coremail.ts +249 -0
- package/ts/classes.desiredstate.ts +508 -0
- package/ts/classes.gateway.ts +811 -0
- package/ts/classes.inbound.ts +898 -0
- package/ts/classes.maintenance.ts +335 -0
- package/ts/classes.models.ts +530 -0
- package/ts/classes.server.ts +444 -0
- package/ts/classes.storage.ts +448 -0
- package/ts/classes.submissions.ts +554 -0
- package/ts/classes.transfer.ts +379 -0
- package/ts/coremail.cursor.ts +328 -0
- package/ts/coremail.log.ts +21 -0
- package/ts/coremail.mime.ts +94 -0
- package/ts/coremail.persistence.ts +1156 -0
- package/ts/coremail.quota.ts +214 -0
- package/ts/coremail.selectors.ts +35 -0
- package/ts/coremail.validation.ts +446 -0
- package/ts/index.ts +38 -0
- package/ts/interfaces.ts +109 -0
- package/ts/plugins.ts +11 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { ICoreMailModels } from './classes.models.js';
|
|
3
|
+
import type { CoreMailStorage } from './classes.storage.js';
|
|
4
|
+
import { logCoreMailFailure } from './coremail.log.js';
|
|
5
|
+
|
|
6
|
+
const sweepIntervalMs = 60_000;
|
|
7
|
+
const batchSize = 100;
|
|
8
|
+
const transferRetirementGraceMs =
|
|
9
|
+
plugins.serveZoneInterfaces.data.coreMailLimits.transferGrantTtlMs
|
|
10
|
+
+ plugins.serveZoneInterfaces.data.coreMailLimits.transferOverallTimeoutMs
|
|
11
|
+
+ 2 * 60_000;
|
|
12
|
+
|
|
13
|
+
export class CoreMailMaintenanceService {
|
|
14
|
+
private sweepTimer: ReturnType<typeof setInterval> | null = null;
|
|
15
|
+
private sweepTask: Promise<void> | null = null;
|
|
16
|
+
private sweepController: AbortController | null = null;
|
|
17
|
+
|
|
18
|
+
public constructor(
|
|
19
|
+
private readonly models: ICoreMailModels,
|
|
20
|
+
private readonly storage: CoreMailStorage,
|
|
21
|
+
private readonly now: () => number = Date.now,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
public start(): void {
|
|
25
|
+
if (this.sweepTimer) return;
|
|
26
|
+
this.sweepTimer = setInterval(() => {
|
|
27
|
+
void this.runSweep();
|
|
28
|
+
}, sweepIntervalMs);
|
|
29
|
+
this.sweepTimer.unref?.();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public async stop(): Promise<void> {
|
|
33
|
+
if (this.sweepTimer) {
|
|
34
|
+
clearInterval(this.sweepTimer);
|
|
35
|
+
this.sweepTimer = null;
|
|
36
|
+
}
|
|
37
|
+
this.sweepController?.abort();
|
|
38
|
+
await this.sweepTask?.catch(() => undefined);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public async sweep(signalArg?: AbortSignal): Promise<void> {
|
|
42
|
+
const now = this.now();
|
|
43
|
+
const terminalOutboundCutoff = now
|
|
44
|
+
- plugins.serveZoneInterfaces.data.coreMailRetentionPolicy.terminalOutboundMs;
|
|
45
|
+
const acknowledgedInboundCutoff = now
|
|
46
|
+
- plugins.serveZoneInterfaces.data.coreMailRetentionPolicy.acknowledgedInboundMs;
|
|
47
|
+
const idempotencyReceiptCutoff = now
|
|
48
|
+
- plugins.serveZoneInterfaces.data.coreMailRetentionPolicy.idempotencyReceiptMs;
|
|
49
|
+
const expiredCapabilityCutoff = now
|
|
50
|
+
- plugins.serveZoneInterfaces.data.coreMailRetentionPolicy.expiredCapabilityMs;
|
|
51
|
+
const transferRetirementCutoff = now - transferRetirementGraceMs;
|
|
52
|
+
while (!signalArg?.aborted) {
|
|
53
|
+
const terminalSubmissions = await this.models.Submission.exact.findStored({
|
|
54
|
+
filter: {
|
|
55
|
+
state: { $in: ['delivered', 'failed', 'deadLettered'] },
|
|
56
|
+
terminalAt: { $lte: terminalOutboundCutoff },
|
|
57
|
+
$or: [
|
|
58
|
+
{ retiringAt: { $exists: false } },
|
|
59
|
+
{ retiringAt: { $lte: transferRetirementCutoff } },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
sort: { terminalAt: 1, submissionId: 1 },
|
|
63
|
+
limit: batchSize,
|
|
64
|
+
});
|
|
65
|
+
for (const submission of terminalSubmissions) {
|
|
66
|
+
signalArg?.throwIfAborted();
|
|
67
|
+
await this.purgeSubmission(submission, signalArg);
|
|
68
|
+
}
|
|
69
|
+
if (terminalSubmissions.length < batchSize) break;
|
|
70
|
+
}
|
|
71
|
+
while (!signalArg?.aborted) {
|
|
72
|
+
const abandonedSubmissions = await this.models.Submission.exact.findStored({
|
|
73
|
+
filter: {
|
|
74
|
+
state: { $in: ['preparing', 'uploading', 'ready'] },
|
|
75
|
+
updatedAt: { $lte: idempotencyReceiptCutoff },
|
|
76
|
+
$or: [
|
|
77
|
+
{ leaseExpiresAt: { $exists: false } },
|
|
78
|
+
{ leaseExpiresAt: { $lte: now } },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
sort: { createdAt: 1, submissionId: 1 },
|
|
82
|
+
limit: batchSize,
|
|
83
|
+
});
|
|
84
|
+
for (const submission of abandonedSubmissions) {
|
|
85
|
+
signalArg?.throwIfAborted();
|
|
86
|
+
const retirement = await this.models.Submission.exact.transition({
|
|
87
|
+
current: submission,
|
|
88
|
+
change: (modelArg) => {
|
|
89
|
+
modelArg.state = 'failed';
|
|
90
|
+
modelArg.error = { code: 'STATE_CONFLICT', retryable: false };
|
|
91
|
+
modelArg.terminalAt = submission.updatedAt;
|
|
92
|
+
modelArg.retiringAt = now;
|
|
93
|
+
modelArg.updatedAt = now;
|
|
94
|
+
modelArg.revision += 1;
|
|
95
|
+
delete modelArg.nextAttemptAt;
|
|
96
|
+
delete modelArg.workerRetryAt;
|
|
97
|
+
delete modelArg.deliveredAt;
|
|
98
|
+
delete modelArg.leaseOwnerTaskId;
|
|
99
|
+
delete modelArg.leaseToken;
|
|
100
|
+
delete modelArg.leaseExpiresAt;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
if (retirement.status !== 'transitioned') continue;
|
|
104
|
+
}
|
|
105
|
+
if (abandonedSubmissions.length < batchSize) break;
|
|
106
|
+
}
|
|
107
|
+
while (!signalArg?.aborted) {
|
|
108
|
+
const deliveries = await this.models.InboundDelivery.exact.findStored({
|
|
109
|
+
filter: {
|
|
110
|
+
state: 'acknowledged',
|
|
111
|
+
updatedAt: { $lte: acknowledgedInboundCutoff },
|
|
112
|
+
},
|
|
113
|
+
sort: { updatedAt: 1, deliveryId: 1 },
|
|
114
|
+
limit: batchSize,
|
|
115
|
+
});
|
|
116
|
+
for (const delivery of deliveries) {
|
|
117
|
+
signalArg?.throwIfAborted();
|
|
118
|
+
const retainedReferences = await this.models.InboundDelivery.exact.count({
|
|
119
|
+
rawMimeObjectKey: delivery.rawMimeObjectKey,
|
|
120
|
+
deliveryId: { $ne: delivery.deliveryId },
|
|
121
|
+
$or: [
|
|
122
|
+
{ state: { $ne: 'acknowledged' } },
|
|
123
|
+
{ updatedAt: { $gt: acknowledgedInboundCutoff } },
|
|
124
|
+
],
|
|
125
|
+
});
|
|
126
|
+
if (retainedReferences === 0) {
|
|
127
|
+
await this.storage.purgeExact(delivery.rawMimeObjectKey, signalArg);
|
|
128
|
+
}
|
|
129
|
+
await this.models.InboundDelivery.exact.delete({ current: delivery });
|
|
130
|
+
}
|
|
131
|
+
if (deliveries.length < batchSize) break;
|
|
132
|
+
}
|
|
133
|
+
let handoffCursor: { createdAt: number; handoffId: string } | undefined;
|
|
134
|
+
while (!signalArg?.aborted) {
|
|
135
|
+
const incompleteHandoffs = await this.models.InboundHandoff.exact.findStored({
|
|
136
|
+
filter: {
|
|
137
|
+
$and: [
|
|
138
|
+
{
|
|
139
|
+
$or: [
|
|
140
|
+
{
|
|
141
|
+
state: { $in: ['prepared', 'uploaded', 'failed'] },
|
|
142
|
+
updatedAt: { $lte: idempotencyReceiptCutoff },
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
state: 'retiring',
|
|
146
|
+
retiringAt: { $lte: transferRetirementCutoff },
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
...(handoffCursor
|
|
151
|
+
? [{
|
|
152
|
+
$or: [
|
|
153
|
+
{ createdAt: { $gt: handoffCursor.createdAt } },
|
|
154
|
+
{
|
|
155
|
+
createdAt: handoffCursor.createdAt,
|
|
156
|
+
handoffId: { $gt: handoffCursor.handoffId },
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
}]
|
|
160
|
+
: []),
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
sort: { createdAt: 1, handoffId: 1 },
|
|
164
|
+
limit: batchSize,
|
|
165
|
+
});
|
|
166
|
+
for (const handoff of incompleteHandoffs) {
|
|
167
|
+
signalArg?.throwIfAborted();
|
|
168
|
+
let retiringHandoff = handoff;
|
|
169
|
+
if (handoff.state !== 'retiring') {
|
|
170
|
+
const retirement = await this.models.InboundHandoff.exact.transition({
|
|
171
|
+
current: handoff,
|
|
172
|
+
change: (modelArg) => {
|
|
173
|
+
modelArg.state = 'retiring';
|
|
174
|
+
modelArg.retiringAt = now;
|
|
175
|
+
modelArg.updatedAt = now;
|
|
176
|
+
modelArg.revision += 1;
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
if (retirement.status !== 'transitioned') continue;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const deliveryReferences = await this.models.InboundDelivery.exact.count({
|
|
183
|
+
rawMimeObjectKey: retiringHandoff.objectKey,
|
|
184
|
+
});
|
|
185
|
+
if (deliveryReferences === 0) {
|
|
186
|
+
await this.storage.purgeExact(retiringHandoff.objectKey, signalArg);
|
|
187
|
+
await this.models.InboundHandoff.exact.delete({ current: retiringHandoff });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const lastHandoff = incompleteHandoffs.at(-1);
|
|
191
|
+
if (!lastHandoff || incompleteHandoffs.length < batchSize) break;
|
|
192
|
+
handoffCursor = {
|
|
193
|
+
createdAt: lastHandoff.createdAt,
|
|
194
|
+
handoffId: lastHandoff.handoffId,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
while (!signalArg?.aborted) {
|
|
198
|
+
const completedHandoffs = await this.models.InboundHandoff.exact.findStored({
|
|
199
|
+
filter: {
|
|
200
|
+
state: 'completed',
|
|
201
|
+
completedAt: { $lte: idempotencyReceiptCutoff },
|
|
202
|
+
},
|
|
203
|
+
sort: { completedAt: 1, handoffId: 1 },
|
|
204
|
+
limit: batchSize,
|
|
205
|
+
});
|
|
206
|
+
for (const handoff of completedHandoffs) {
|
|
207
|
+
signalArg?.throwIfAborted();
|
|
208
|
+
await this.models.InboundHandoff.exact.delete({ current: handoff });
|
|
209
|
+
}
|
|
210
|
+
if (completedHandoffs.length < batchSize) break;
|
|
211
|
+
}
|
|
212
|
+
while (!signalArg?.aborted) {
|
|
213
|
+
const activeState = await this.models.State.exact.findStoredOne({
|
|
214
|
+
stateId: 'active',
|
|
215
|
+
});
|
|
216
|
+
const snapshots = await this.models.DesiredSnapshot.exact.findStored({
|
|
217
|
+
filter: {
|
|
218
|
+
$or: [
|
|
219
|
+
{
|
|
220
|
+
state: { $in: ['staged', 'superseded', 'failed'] },
|
|
221
|
+
updatedAt: { $lte: idempotencyReceiptCutoff },
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
state: 'retiring',
|
|
225
|
+
retiringAt: { $lte: transferRetirementCutoff },
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
...(activeState ? { snapshotId: { $ne: activeState.snapshotId } } : {}),
|
|
229
|
+
},
|
|
230
|
+
sort: { createdAt: 1, snapshotId: 1 },
|
|
231
|
+
limit: batchSize,
|
|
232
|
+
});
|
|
233
|
+
for (const snapshot of snapshots) {
|
|
234
|
+
signalArg?.throwIfAborted();
|
|
235
|
+
let retiringSnapshot = snapshot;
|
|
236
|
+
if (snapshot.state !== 'retiring') {
|
|
237
|
+
const retirement = await this.models.DesiredSnapshot.exact.transition({
|
|
238
|
+
current: snapshot,
|
|
239
|
+
change: (modelArg) => {
|
|
240
|
+
modelArg.state = 'retiring';
|
|
241
|
+
modelArg.retiringAt = now;
|
|
242
|
+
modelArg.updatedAt = now;
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
if (retirement.status !== 'transitioned') continue;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
await this.storage.purgeExact(retiringSnapshot.objectKey, signalArg);
|
|
249
|
+
await this.models.DesiredSnapshot.exact.delete({ current: retiringSnapshot });
|
|
250
|
+
}
|
|
251
|
+
if (snapshots.length < batchSize) break;
|
|
252
|
+
}
|
|
253
|
+
while (!signalArg?.aborted) {
|
|
254
|
+
const grants = await this.models.TransferGrant.exact.findStored({
|
|
255
|
+
filter: { expiresAt: { $lte: expiredCapabilityCutoff } },
|
|
256
|
+
sort: { expiresAt: 1, grantId: 1 },
|
|
257
|
+
limit: batchSize,
|
|
258
|
+
});
|
|
259
|
+
for (const grant of grants) {
|
|
260
|
+
signalArg?.throwIfAborted();
|
|
261
|
+
await this.models.TransferGrant.exact.delete({ current: grant });
|
|
262
|
+
}
|
|
263
|
+
if (grants.length < batchSize) break;
|
|
264
|
+
}
|
|
265
|
+
while (!signalArg?.aborted) {
|
|
266
|
+
const handles = await this.models.RecipientHandle.exact.findStored({
|
|
267
|
+
filter: { expiresAt: { $lte: expiredCapabilityCutoff } },
|
|
268
|
+
sort: { expiresAt: 1, routingHandle: 1 },
|
|
269
|
+
limit: batchSize,
|
|
270
|
+
});
|
|
271
|
+
for (const handle of handles) {
|
|
272
|
+
signalArg?.throwIfAborted();
|
|
273
|
+
await this.models.RecipientHandle.exact.delete({ current: handle });
|
|
274
|
+
}
|
|
275
|
+
if (handles.length < batchSize) break;
|
|
276
|
+
}
|
|
277
|
+
while (!signalArg?.aborted) {
|
|
278
|
+
const counters = await this.models.QuotaCounter.exact.findStored({
|
|
279
|
+
filter: {
|
|
280
|
+
updatedAt: { $lte: idempotencyReceiptCutoff },
|
|
281
|
+
$or: [{
|
|
282
|
+
kind: { $in: ['outbound-minute', 'outbound-day'] },
|
|
283
|
+
}, {
|
|
284
|
+
kind: 'pending-inbound',
|
|
285
|
+
count: 0,
|
|
286
|
+
}],
|
|
287
|
+
},
|
|
288
|
+
sort: { updatedAt: 1, counterId: 1 },
|
|
289
|
+
limit: batchSize,
|
|
290
|
+
});
|
|
291
|
+
for (const counter of counters) {
|
|
292
|
+
signalArg?.throwIfAborted();
|
|
293
|
+
await this.models.QuotaCounter.exact.delete({ current: counter });
|
|
294
|
+
}
|
|
295
|
+
if (counters.length < batchSize) break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private async purgeSubmission(
|
|
300
|
+
submissionArg: Awaited<ReturnType<
|
|
301
|
+
typeof this.models.Submission.exact.findStored
|
|
302
|
+
>>[number],
|
|
303
|
+
signalArg?: AbortSignal,
|
|
304
|
+
): Promise<void> {
|
|
305
|
+
const objectKeys = new Set(submissionArg.parts.map((partArg) => partArg.objectKey));
|
|
306
|
+
if (submissionArg.mimeObjectKey) objectKeys.add(submissionArg.mimeObjectKey);
|
|
307
|
+
for (const objectKey of objectKeys) {
|
|
308
|
+
signalArg?.throwIfAborted();
|
|
309
|
+
await this.storage.purgeExact(objectKey, signalArg);
|
|
310
|
+
}
|
|
311
|
+
await this.models.Submission.exact.delete({ current: submissionArg });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private async runSweep(): Promise<void> {
|
|
315
|
+
if (this.sweepTask) return await this.sweepTask;
|
|
316
|
+
const controller = new AbortController();
|
|
317
|
+
this.sweepController = controller;
|
|
318
|
+
const task = this.sweep(controller.signal).catch(async () => {
|
|
319
|
+
if (!controller.signal.aborted) {
|
|
320
|
+
await logCoreMailFailure('retention', 'SWEEP_FAILED').catch(() => undefined);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
this.sweepTask = task;
|
|
324
|
+
try {
|
|
325
|
+
await task;
|
|
326
|
+
} finally {
|
|
327
|
+
if (this.sweepTask === task) {
|
|
328
|
+
this.sweepTask = null;
|
|
329
|
+
}
|
|
330
|
+
if (this.sweepController === controller) {
|
|
331
|
+
this.sweepController = null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|