@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,508 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { type ICoreMailModels } from './classes.models.js';
|
|
3
|
+
import { CoreMailStorage, createSha256 } from './classes.storage.js';
|
|
4
|
+
import { CoreMailTransferService } from './classes.transfer.js';
|
|
5
|
+
import type { ICoreMailReplicaRuntime, ITransferAuthority } from './interfaces.js';
|
|
6
|
+
import { logCoreMailFailure } from './coremail.log.js';
|
|
7
|
+
import { withCoreMailTransaction } from './coremail.quota.js';
|
|
8
|
+
import type {
|
|
9
|
+
ICoreMailDesiredSnapshotRecord,
|
|
10
|
+
ICoreMailStateRecord,
|
|
11
|
+
} from './coremail.persistence.js';
|
|
12
|
+
import {
|
|
13
|
+
requireCoreMailSafeInteger,
|
|
14
|
+
requireCoreMailUuid,
|
|
15
|
+
} from './coremail.selectors.js';
|
|
16
|
+
|
|
17
|
+
type TCoreMailSha256 = plugins.serveZoneInterfaces.data.TCoreMailSha256;
|
|
18
|
+
type TCoreMailDesiredState = plugins.serveZoneInterfaces.data.ICoreMailDesiredState;
|
|
19
|
+
type TCoreMailStatus = plugins.serveZoneInterfaces.data.ICoreMailReconciliationStatus;
|
|
20
|
+
type TStoredState = plugins.smartdata.TStoredDocument<ICoreMailStateRecord>;
|
|
21
|
+
type TStoredDesiredSnapshot =
|
|
22
|
+
plugins.smartdata.TStoredDocument<ICoreMailDesiredSnapshotRecord>;
|
|
23
|
+
|
|
24
|
+
const emptyStateDigest = createSha256('');
|
|
25
|
+
|
|
26
|
+
export class CoreMailDesiredStateService {
|
|
27
|
+
private activeDesiredState: TCoreMailDesiredState | null = null;
|
|
28
|
+
private activeStatePointer!: TStoredState;
|
|
29
|
+
private refreshTask: Promise<void> | null = null;
|
|
30
|
+
|
|
31
|
+
public constructor(
|
|
32
|
+
private readonly database: plugins.smartdata.SmartdataDb,
|
|
33
|
+
private readonly models: ICoreMailModels,
|
|
34
|
+
private readonly storage: CoreMailStorage,
|
|
35
|
+
private readonly transfers: CoreMailTransferService,
|
|
36
|
+
private readonly replica: ICoreMailReplicaRuntime,
|
|
37
|
+
private readonly activeSessionCounts: (
|
|
38
|
+
bindingArg: plugins.serveZoneInterfaces.data.ICoreMailBindingDesiredState,
|
|
39
|
+
configEpochArg: number,
|
|
40
|
+
) => Array<{ credentialId: string; version: number; count: number }>,
|
|
41
|
+
private readonly now: () => number = Date.now,
|
|
42
|
+
) {}
|
|
43
|
+
|
|
44
|
+
private controlAuthority(): ITransferAuthority {
|
|
45
|
+
return {
|
|
46
|
+
kind: 'control',
|
|
47
|
+
tenantId: '-',
|
|
48
|
+
serviceId: this.replica.serviceId,
|
|
49
|
+
bindingId: '-',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public async start(): Promise<void> {
|
|
54
|
+
const initialized = await this.models.State.exact.insert({
|
|
55
|
+
stateId: 'active',
|
|
56
|
+
appliedConfigEpoch: 0,
|
|
57
|
+
appliedDesiredStateDigest: emptyStateDigest,
|
|
58
|
+
snapshotId: '-',
|
|
59
|
+
updatedAt: this.now(),
|
|
60
|
+
});
|
|
61
|
+
const state = initialized.document;
|
|
62
|
+
if (!state) {
|
|
63
|
+
throw new Error('CoreMail could not initialize its desired-state pointer.');
|
|
64
|
+
}
|
|
65
|
+
this.activeStatePointer = state;
|
|
66
|
+
await this.refreshFromPointer(state);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public getActiveDesiredState = async (): Promise<TCoreMailDesiredState | null> => {
|
|
70
|
+
await this.refresh();
|
|
71
|
+
return this.activeDesiredState;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
private async refresh(): Promise<void> {
|
|
75
|
+
if (this.refreshTask) {
|
|
76
|
+
await this.refreshTask;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const task = (async () => {
|
|
80
|
+
const pointer = await this.models.State.exact.findStoredOne({
|
|
81
|
+
stateId: 'active',
|
|
82
|
+
});
|
|
83
|
+
if (!pointer) {
|
|
84
|
+
throw new Error('CoreMail desired-state pointer is unavailable.');
|
|
85
|
+
}
|
|
86
|
+
await this.refreshFromPointer(pointer);
|
|
87
|
+
})();
|
|
88
|
+
this.refreshTask = task;
|
|
89
|
+
try {
|
|
90
|
+
await task;
|
|
91
|
+
} finally {
|
|
92
|
+
if (this.refreshTask === task) {
|
|
93
|
+
this.refreshTask = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private async refreshFromPointer(pointerArg: TStoredState): Promise<void> {
|
|
99
|
+
if (pointerArg.appliedConfigEpoch === 0) {
|
|
100
|
+
this.activeStatePointer = pointerArg;
|
|
101
|
+
this.activeDesiredState = null;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (
|
|
105
|
+
this.activeDesiredState?.configEpoch === pointerArg.appliedConfigEpoch
|
|
106
|
+
&& this.activeStatePointer?.snapshotId === pointerArg.snapshotId
|
|
107
|
+
&& this.activeStatePointer.appliedDesiredStateDigest
|
|
108
|
+
=== pointerArg.appliedDesiredStateDigest
|
|
109
|
+
) {
|
|
110
|
+
this.activeStatePointer = pointerArg;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const desiredState = await this.loadDesiredState(pointerArg);
|
|
114
|
+
this.activeStatePointer = pointerArg;
|
|
115
|
+
this.activeDesiredState = desiredState;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private async loadDesiredState(stateArg: TStoredState): Promise<TCoreMailDesiredState> {
|
|
119
|
+
const snapshot = await this.models.DesiredSnapshot.exact.findStoredOne({
|
|
120
|
+
snapshotId: stateArg.snapshotId,
|
|
121
|
+
configEpoch: stateArg.appliedConfigEpoch,
|
|
122
|
+
desiredStateDigest: stateArg.appliedDesiredStateDigest,
|
|
123
|
+
});
|
|
124
|
+
if (
|
|
125
|
+
!snapshot
|
|
126
|
+
|| (snapshot.state !== 'staged' && snapshot.state !== 'active')
|
|
127
|
+
) {
|
|
128
|
+
throw new Error('CoreMail active desired-state snapshot is unavailable.');
|
|
129
|
+
}
|
|
130
|
+
const bytes = await this.storage.getVerifiedBytes(
|
|
131
|
+
snapshot.objectKey,
|
|
132
|
+
snapshot.lengthBytes,
|
|
133
|
+
snapshot.desiredStateDigest,
|
|
134
|
+
plugins.serveZoneInterfaces.data.coreMailLimits.desiredStateBytes,
|
|
135
|
+
);
|
|
136
|
+
const desiredState = await this.parseDesiredState(
|
|
137
|
+
bytes,
|
|
138
|
+
snapshot.configEpoch,
|
|
139
|
+
snapshot.desiredStateDigest,
|
|
140
|
+
);
|
|
141
|
+
if (snapshot.state === 'staged') {
|
|
142
|
+
await this.models.DesiredSnapshot.exact.transition({
|
|
143
|
+
current: snapshot,
|
|
144
|
+
change: (modelArg) => {
|
|
145
|
+
modelArg.state = 'active';
|
|
146
|
+
modelArg.activatedAt = stateArg.updatedAt;
|
|
147
|
+
},
|
|
148
|
+
}).catch(async () => {
|
|
149
|
+
await logCoreMailFailure('desired-state', 'SNAPSHOT_REPAIR_FAILED')
|
|
150
|
+
.catch(() => undefined);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return desiredState;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async parseDesiredState(
|
|
157
|
+
bytesArg: Uint8Array,
|
|
158
|
+
configEpochArg: number,
|
|
159
|
+
digestArg: TCoreMailSha256,
|
|
160
|
+
): Promise<TCoreMailDesiredState> {
|
|
161
|
+
let parsed: unknown;
|
|
162
|
+
try {
|
|
163
|
+
parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytesArg));
|
|
164
|
+
} catch {
|
|
165
|
+
throw new Error('CoreMail desired state is not canonical UTF-8 JSON.');
|
|
166
|
+
}
|
|
167
|
+
const desiredState =
|
|
168
|
+
plugins.serveZoneInterfaces.data.normalizeCoreMailDesiredState(parsed);
|
|
169
|
+
if (
|
|
170
|
+
desiredState.configEpoch !== configEpochArg
|
|
171
|
+
|| !await plugins.serveZoneInterfaces.data.verifyCoreMailDesiredStateDigest(
|
|
172
|
+
desiredState,
|
|
173
|
+
digestArg,
|
|
174
|
+
)
|
|
175
|
+
) {
|
|
176
|
+
throw new Error('CoreMail desired-state digest or epoch does not match.');
|
|
177
|
+
}
|
|
178
|
+
const canonical = plugins.serveZoneInterfaces.data.canonicalizeCoreMailDesiredState(
|
|
179
|
+
desiredState,
|
|
180
|
+
);
|
|
181
|
+
if (!Buffer.from(bytesArg).equals(Buffer.from(canonical, 'utf8'))) {
|
|
182
|
+
throw new Error('CoreMail desired-state bytes are not canonical.');
|
|
183
|
+
}
|
|
184
|
+
return desiredState;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public async prepare(
|
|
188
|
+
requestArg:
|
|
189
|
+
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailPrepareDesiredStateUpload['request'],
|
|
190
|
+
): Promise<{
|
|
191
|
+
reconciliationId: string;
|
|
192
|
+
grant: plugins.serveZoneInterfaces.data.ICoreMailUploadGrant;
|
|
193
|
+
replayed: boolean;
|
|
194
|
+
}> {
|
|
195
|
+
await this.refresh();
|
|
196
|
+
const desiredStateDigest =
|
|
197
|
+
plugins.serveZoneInterfaces.data.normalizeCoreMailSha256(
|
|
198
|
+
requestArg.desiredStateDigest,
|
|
199
|
+
);
|
|
200
|
+
const expectedAppliedConfigEpoch = requireCoreMailSafeInteger(
|
|
201
|
+
requestArg.expectedAppliedConfigEpoch,
|
|
202
|
+
'expectedAppliedConfigEpoch',
|
|
203
|
+
);
|
|
204
|
+
const configEpoch = requireCoreMailSafeInteger(
|
|
205
|
+
requestArg.configEpoch,
|
|
206
|
+
'configEpoch',
|
|
207
|
+
1,
|
|
208
|
+
);
|
|
209
|
+
if (
|
|
210
|
+
configEpoch <= expectedAppliedConfigEpoch
|
|
211
|
+
|| !Number.isSafeInteger(requestArg.lengthBytes)
|
|
212
|
+
|| requestArg.lengthBytes < 2
|
|
213
|
+
|| requestArg.lengthBytes >
|
|
214
|
+
plugins.serveZoneInterfaces.data.coreMailLimits.desiredStateBytes
|
|
215
|
+
) {
|
|
216
|
+
throw new Error('CoreMail desired-state preparation is invalid.');
|
|
217
|
+
}
|
|
218
|
+
const current = await this.models.State.exact.findStoredOne({
|
|
219
|
+
stateId: 'active',
|
|
220
|
+
appliedConfigEpoch: expectedAppliedConfigEpoch,
|
|
221
|
+
});
|
|
222
|
+
if (!current || current.appliedConfigEpoch !== this.activeStatePointer.appliedConfigEpoch) {
|
|
223
|
+
throw new Error('CoreMail desired-state compare-and-set fence changed.');
|
|
224
|
+
}
|
|
225
|
+
let snapshot = await this.models.DesiredSnapshot.exact.findStoredOne({
|
|
226
|
+
configEpoch,
|
|
227
|
+
desiredStateDigest,
|
|
228
|
+
});
|
|
229
|
+
let replayed = Boolean(snapshot);
|
|
230
|
+
if (snapshot) {
|
|
231
|
+
if (
|
|
232
|
+
snapshot.expectedAppliedConfigEpoch !== expectedAppliedConfigEpoch
|
|
233
|
+
|| snapshot.lengthBytes !== requestArg.lengthBytes
|
|
234
|
+
|| (snapshot.state !== 'staged' && snapshot.state !== 'active')
|
|
235
|
+
) {
|
|
236
|
+
throw new Error('CoreMail desired-state replay conflicts with persisted intent.');
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
const snapshotId = plugins.nodeCrypto.randomUUID();
|
|
240
|
+
const createdAt = this.now();
|
|
241
|
+
const candidate: ICoreMailDesiredSnapshotRecord = {
|
|
242
|
+
snapshotId,
|
|
243
|
+
reconciliationId: plugins.nodeCrypto.randomUUID(),
|
|
244
|
+
expectedAppliedConfigEpoch,
|
|
245
|
+
configEpoch,
|
|
246
|
+
desiredStateDigest,
|
|
247
|
+
lengthBytes: requestArg.lengthBytes,
|
|
248
|
+
objectKey: `desired-state/${snapshotId}.json`,
|
|
249
|
+
state: 'staged',
|
|
250
|
+
createdAt,
|
|
251
|
+
updatedAt: createdAt,
|
|
252
|
+
};
|
|
253
|
+
try {
|
|
254
|
+
const inserted = await this.models.DesiredSnapshot.exact.insert(candidate);
|
|
255
|
+
if (inserted.status === 'conflict') {
|
|
256
|
+
throw new Error(
|
|
257
|
+
'CoreMail desired-state preparation conflicts with persisted intent.',
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
snapshot = inserted.document;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (
|
|
263
|
+
!(error instanceof plugins.smartdata.SmartdataExactPersistenceError)
|
|
264
|
+
|| error.code !== 'unique_conflict'
|
|
265
|
+
) {
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
const concurrent =
|
|
269
|
+
await this.models.DesiredSnapshot.exact.findStoredOne({
|
|
270
|
+
configEpoch,
|
|
271
|
+
desiredStateDigest,
|
|
272
|
+
});
|
|
273
|
+
if (
|
|
274
|
+
!concurrent
|
|
275
|
+
|| concurrent.expectedAppliedConfigEpoch
|
|
276
|
+
!== expectedAppliedConfigEpoch
|
|
277
|
+
|| concurrent.lengthBytes !== requestArg.lengthBytes
|
|
278
|
+
|| (concurrent.state !== 'staged' && concurrent.state !== 'active')
|
|
279
|
+
) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
'CoreMail desired-state preparation conflicts with persisted intent.',
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
snapshot = concurrent;
|
|
285
|
+
replayed = true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (snapshot.state === 'staged') {
|
|
289
|
+
const touchedAt = this.now();
|
|
290
|
+
const touch = await this.models.DesiredSnapshot.exact.transition({
|
|
291
|
+
current: snapshot,
|
|
292
|
+
change: (modelArg) => {
|
|
293
|
+
modelArg.updatedAt = touchedAt;
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
if (touch.status !== 'transitioned') {
|
|
297
|
+
throw new Error('CoreMail desired-state preparation fence changed.');
|
|
298
|
+
}
|
|
299
|
+
snapshot = touch.document;
|
|
300
|
+
}
|
|
301
|
+
const grant = await this.transfers.issueGrant({
|
|
302
|
+
authority: this.controlAuthority(),
|
|
303
|
+
purpose: 'desired-state-upload',
|
|
304
|
+
method: 'PUT',
|
|
305
|
+
expectedSha256: snapshot.desiredStateDigest,
|
|
306
|
+
lengthBytes: snapshot.lengthBytes,
|
|
307
|
+
contentType: 'application/json',
|
|
308
|
+
objectKey: snapshot.objectKey,
|
|
309
|
+
});
|
|
310
|
+
return {
|
|
311
|
+
reconciliationId: snapshot.reconciliationId,
|
|
312
|
+
grant: grant as plugins.serveZoneInterfaces.data.ICoreMailUploadGrant,
|
|
313
|
+
replayed,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
public async apply(
|
|
318
|
+
requestArg:
|
|
319
|
+
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailApplyDesiredState['request'],
|
|
320
|
+
): Promise<TCoreMailStatus> {
|
|
321
|
+
await this.refresh();
|
|
322
|
+
const desiredStateDigest =
|
|
323
|
+
plugins.serveZoneInterfaces.data.normalizeCoreMailSha256(
|
|
324
|
+
requestArg.desiredStateDigest,
|
|
325
|
+
);
|
|
326
|
+
const reconciliationId = requireCoreMailUuid(
|
|
327
|
+
requestArg.reconciliationId,
|
|
328
|
+
'reconciliationId',
|
|
329
|
+
);
|
|
330
|
+
const grantId = requireCoreMailUuid(requestArg.grantId, 'grantId');
|
|
331
|
+
const lengthBytes = requireCoreMailSafeInteger(
|
|
332
|
+
requestArg.lengthBytes,
|
|
333
|
+
'lengthBytes',
|
|
334
|
+
2,
|
|
335
|
+
);
|
|
336
|
+
const snapshot = await this.models.DesiredSnapshot.exact.findStoredOne({
|
|
337
|
+
reconciliationId,
|
|
338
|
+
desiredStateDigest,
|
|
339
|
+
lengthBytes,
|
|
340
|
+
});
|
|
341
|
+
if (!snapshot || (snapshot.state !== 'staged' && snapshot.state !== 'active')) {
|
|
342
|
+
throw new Error('CoreMail desired-state reconciliation is unavailable.');
|
|
343
|
+
}
|
|
344
|
+
await this.transfers.assertConsumed({
|
|
345
|
+
grantId,
|
|
346
|
+
authority: this.controlAuthority(),
|
|
347
|
+
purpose: 'desired-state-upload',
|
|
348
|
+
expectedSha256: desiredStateDigest,
|
|
349
|
+
lengthBytes,
|
|
350
|
+
objectKey: snapshot.objectKey,
|
|
351
|
+
});
|
|
352
|
+
const bytes = await this.storage.getVerifiedBytes(
|
|
353
|
+
snapshot.objectKey,
|
|
354
|
+
snapshot.lengthBytes,
|
|
355
|
+
snapshot.desiredStateDigest,
|
|
356
|
+
plugins.serveZoneInterfaces.data.coreMailLimits.desiredStateBytes,
|
|
357
|
+
);
|
|
358
|
+
const desiredState = await this.parseDesiredState(
|
|
359
|
+
bytes,
|
|
360
|
+
snapshot.configEpoch,
|
|
361
|
+
snapshot.desiredStateDigest,
|
|
362
|
+
);
|
|
363
|
+
if (
|
|
364
|
+
snapshot.state === 'active'
|
|
365
|
+
&& this.activeStatePointer.snapshotId === snapshot.snapshotId
|
|
366
|
+
) {
|
|
367
|
+
return await this.createStatus('ready', desiredState);
|
|
368
|
+
}
|
|
369
|
+
const activatedAt = this.now();
|
|
370
|
+
const activation = await withCoreMailTransaction(
|
|
371
|
+
this.database,
|
|
372
|
+
async (transactionArg) => {
|
|
373
|
+
const transactionSnapshot =
|
|
374
|
+
await this.models.DesiredSnapshot.exact.findStoredOne({
|
|
375
|
+
snapshotId: snapshot.snapshotId,
|
|
376
|
+
reconciliationId: snapshot.reconciliationId,
|
|
377
|
+
state: 'staged',
|
|
378
|
+
}, { session: transactionArg });
|
|
379
|
+
const current = await this.models.State.exact.findStoredOne({
|
|
380
|
+
stateId: 'active',
|
|
381
|
+
appliedConfigEpoch: snapshot.expectedAppliedConfigEpoch,
|
|
382
|
+
}, { session: transactionArg });
|
|
383
|
+
if (
|
|
384
|
+
!transactionSnapshot
|
|
385
|
+
|| !current
|
|
386
|
+
|| snapshot.configEpoch <= current.appliedConfigEpoch
|
|
387
|
+
|| current.snapshotId !== this.activeStatePointer.snapshotId
|
|
388
|
+
|| current.appliedDesiredStateDigest
|
|
389
|
+
!== this.activeStatePointer.appliedDesiredStateDigest
|
|
390
|
+
) {
|
|
391
|
+
throw new Error('CoreMail desired-state anti-rollback fence changed.');
|
|
392
|
+
}
|
|
393
|
+
const snapshotActivation =
|
|
394
|
+
await this.models.DesiredSnapshot.exact.transition({
|
|
395
|
+
current: transactionSnapshot,
|
|
396
|
+
change: (modelArg) => {
|
|
397
|
+
modelArg.state = 'active';
|
|
398
|
+
modelArg.activatedAt = activatedAt;
|
|
399
|
+
modelArg.updatedAt = activatedAt;
|
|
400
|
+
},
|
|
401
|
+
}, { session: transactionArg });
|
|
402
|
+
if (snapshotActivation.status !== 'transitioned') {
|
|
403
|
+
throw new Error('CoreMail desired-state snapshot activation was contended.');
|
|
404
|
+
}
|
|
405
|
+
if (current.snapshotId !== '-') {
|
|
406
|
+
const prior = await this.models.DesiredSnapshot.exact.findStoredOne({
|
|
407
|
+
snapshotId: current.snapshotId,
|
|
408
|
+
}, { session: transactionArg });
|
|
409
|
+
if (!prior) {
|
|
410
|
+
throw new Error('CoreMail prior desired-state snapshot is unavailable.');
|
|
411
|
+
}
|
|
412
|
+
const superseded = await this.models.DesiredSnapshot.exact.transition({
|
|
413
|
+
current: prior,
|
|
414
|
+
change: (modelArg) => {
|
|
415
|
+
modelArg.state = 'superseded';
|
|
416
|
+
modelArg.activatedAt ??= current.updatedAt;
|
|
417
|
+
modelArg.updatedAt = activatedAt;
|
|
418
|
+
},
|
|
419
|
+
}, { session: transactionArg });
|
|
420
|
+
if (superseded.status !== 'transitioned') {
|
|
421
|
+
throw new Error('CoreMail prior desired-state snapshot was contended.');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const pointerActivation = await this.models.State.exact.transition({
|
|
425
|
+
current,
|
|
426
|
+
change: (modelArg) => {
|
|
427
|
+
modelArg.appliedConfigEpoch = snapshot.configEpoch;
|
|
428
|
+
modelArg.appliedDesiredStateDigest = snapshot.desiredStateDigest;
|
|
429
|
+
modelArg.snapshotId = snapshot.snapshotId;
|
|
430
|
+
modelArg.updatedAt = activatedAt;
|
|
431
|
+
},
|
|
432
|
+
}, { session: transactionArg });
|
|
433
|
+
if (pointerActivation.status !== 'transitioned') {
|
|
434
|
+
throw new Error('CoreMail desired-state activation was contended.');
|
|
435
|
+
}
|
|
436
|
+
return pointerActivation.document;
|
|
437
|
+
},
|
|
438
|
+
);
|
|
439
|
+
this.activeStatePointer = activation;
|
|
440
|
+
this.activeDesiredState = desiredState;
|
|
441
|
+
return await this.createStatus('ready', desiredState);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
public async getStatus(
|
|
445
|
+
requestArg:
|
|
446
|
+
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailGetReconciliationStatus['request'],
|
|
447
|
+
): Promise<TCoreMailStatus> {
|
|
448
|
+
await this.refresh();
|
|
449
|
+
if (this.activeStatePointer.appliedConfigEpoch === 0) {
|
|
450
|
+
throw new Error('CoreMail has no applied desired state.');
|
|
451
|
+
}
|
|
452
|
+
const digest = plugins.serveZoneInterfaces.data.normalizeCoreMailSha256(
|
|
453
|
+
requestArg.desiredStateDigest,
|
|
454
|
+
);
|
|
455
|
+
if (
|
|
456
|
+
this.activeDesiredState?.configEpoch === requestArg.configEpoch
|
|
457
|
+
&& this.activeStatePointer.appliedDesiredStateDigest === digest
|
|
458
|
+
) {
|
|
459
|
+
return await this.createStatus('ready', this.activeDesiredState);
|
|
460
|
+
}
|
|
461
|
+
const snapshot = await this.models.DesiredSnapshot.exact.findStoredOne({
|
|
462
|
+
configEpoch: requestArg.configEpoch,
|
|
463
|
+
desiredStateDigest: digest,
|
|
464
|
+
});
|
|
465
|
+
return await this.createStatus(
|
|
466
|
+
snapshot?.state === 'staged' ? 'applying' : 'failed',
|
|
467
|
+
this.activeDesiredState,
|
|
468
|
+
snapshot?.errorCode ?? 'RECONCILIATION_FENCE_MISMATCH',
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private async createStatus(
|
|
473
|
+
stateArg: plugins.serveZoneInterfaces.data.TCoreMailReconciliationState,
|
|
474
|
+
desiredStateArg: TCoreMailDesiredState | null,
|
|
475
|
+
errorCodeArg?: plugins.serveZoneInterfaces.data.TCoreMailErrorCode,
|
|
476
|
+
): Promise<TCoreMailStatus> {
|
|
477
|
+
const bindings = await Promise.all(
|
|
478
|
+
(desiredStateArg?.bindings ?? []).map(async (bindingArg) => ({
|
|
479
|
+
bindingId: bindingArg.bindingId,
|
|
480
|
+
revision: bindingArg.revision,
|
|
481
|
+
state: bindingArg.state,
|
|
482
|
+
pendingInboundCount: await this.models.InboundDelivery.exact.count({
|
|
483
|
+
tenantId: bindingArg.tenantId,
|
|
484
|
+
serviceId: bindingArg.serviceId,
|
|
485
|
+
bindingId: bindingArg.bindingId,
|
|
486
|
+
bindingRevision: bindingArg.revision,
|
|
487
|
+
state: {
|
|
488
|
+
$in: [...plugins.serveZoneInterfaces.data.coreMailQuotaPolicy
|
|
489
|
+
.pendingInboundStates],
|
|
490
|
+
},
|
|
491
|
+
}),
|
|
492
|
+
activeSessionsByCredential: this.activeSessionCounts(
|
|
493
|
+
bindingArg,
|
|
494
|
+
desiredStateArg!.configEpoch,
|
|
495
|
+
),
|
|
496
|
+
})),
|
|
497
|
+
);
|
|
498
|
+
return {
|
|
499
|
+
replica: { ...this.replica },
|
|
500
|
+
state: stateArg,
|
|
501
|
+
appliedConfigEpoch: this.activeStatePointer.appliedConfigEpoch,
|
|
502
|
+
appliedDesiredStateDigest: this.activeStatePointer.appliedDesiredStateDigest,
|
|
503
|
+
bindings,
|
|
504
|
+
updatedAt: this.now(),
|
|
505
|
+
...(errorCodeArg ? { errorCode: errorCodeArg } : {}),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
}
|