@serve.zone/dcrouter 17.0.0 → 17.1.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 +5 -2
- package/deno.json +1 -1
- package/dist_serve/bundle.js +1316 -1151
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +3 -1
- package/dist_ts/classes.dcrouter.js +110 -49
- package/dist_ts/db/classes.cache.cleaner.d.ts +3 -0
- package/dist_ts/db/classes.cache.cleaner.js +7 -3
- package/dist_ts/db/classes.dcrouter-db.js +1 -2
- package/dist_ts/db/documents/classes.cached.email.d.ts +41 -4
- package/dist_ts/db/documents/classes.cached.email.js +36 -5
- package/dist_ts/db/documents/classes.dns-record.doc.js +3 -1
- package/dist_ts/dns/classes.dns-server-runtime.d.ts +0 -1
- package/dist_ts/dns/classes.dns-server-runtime.js +4 -36
- package/dist_ts/dns/index.d.ts +1 -0
- package/dist_ts/dns/index.js +2 -1
- package/dist_ts/dns/manager.dns.d.ts +1 -0
- package/dist_ts/dns/manager.dns.js +49 -14
- package/dist_ts/dns/providers/cloudflare.provider.js +19 -4
- package/dist_ts/dns/txt-record-presentation.d.ts +20 -0
- package/dist_ts/dns/txt-record-presentation.js +131 -0
- package/dist_ts/email/classes.accepted-email-spool.d.ts +32 -3
- package/dist_ts/email/classes.accepted-email-spool.js +174 -13
- package/dist_ts/email/classes.email-domain.manager.d.ts +14 -1
- package/dist_ts/email/classes.email-domain.manager.js +151 -8
- package/dist_ts/email/classes.email-settings.manager.js +2 -2
- package/dist_ts/email/classes.mail-dns-sync.d.ts +31 -5
- package/dist_ts/email/classes.mail-dns-sync.js +508 -117
- package/dist_ts/email/classes.smartmta-blob-storage-manager.d.ts +2 -2
- package/dist_ts/email/classes.smartmta-blob-storage-manager.js +4 -3
- package/dist_ts/email/classes.workapp-mail-manager.d.ts +1 -0
- package/dist_ts/email/classes.workapp-mail-manager.js +26 -2
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/email/mail-dns-txt-policy.d.ts +18 -0
- package/dist_ts/email/mail-dns-txt-policy.js +466 -0
- package/dist_ts/email/reconcile-mail-dns-singleton.d.ts +47 -0
- package/dist_ts/email/reconcile-mail-dns-singleton.js +159 -0
- package/dist_ts/opsserver/classes.opsserver.d.ts +8 -0
- package/dist_ts/opsserver/classes.opsserver.js +34 -1
- package/dist_ts/opsserver/handlers/certificate.handler.js +25 -4
- package/dist_ts/opsserver/handlers/config.handler.js +9 -5
- package/dist_ts/opsserver/handlers/dns-provider.handler.js +19 -2
- package/dist_ts/opsserver/handlers/dns-record.handler.js +25 -4
- package/dist_ts/opsserver/handlers/domain.handler.js +37 -3
- package/dist_ts/opsserver/handlers/email-domain.handler.js +5 -7
- package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +1 -0
- package/dist_ts/opsserver/handlers/email-ops.handler.js +26 -7
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts/opsserver/handlers/logs.handler.d.ts +1 -1
- package/dist_ts/opsserver/handlers/logs.handler.js +3 -3
- package/dist_ts/opsserver/handlers/realtime.handler.d.ts +37 -0
- package/dist_ts/opsserver/handlers/realtime.handler.js +154 -0
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +22 -1
- package/dist_ts/opsserver/handlers/route-management.handler.js +47 -5
- package/dist_ts/paths.d.ts +0 -11
- package/dist_ts/paths.js +1 -22
- package/dist_ts/plugins.d.ts +0 -34
- package/dist_ts/plugins.js +1 -62
- package/dist_ts_interfaces/data/email-domain.d.ts +14 -0
- package/dist_ts_interfaces/requests/config.d.ts +1 -1
- package/dist_ts_interfaces/requests/email-ops.d.ts +34 -1
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_interfaces/requests/realtime.d.ts +38 -0
- package/dist_ts_interfaces/requests/realtime.js +11 -0
- package/dist_ts_migrations/index.d.ts +6 -0
- package/dist_ts_migrations/index.js +204 -9
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate/certificates.js +9 -2
- package/dist_ts_web/appstate/domains.js +16 -5
- package/dist_ts_web/appstate/email-domains.d.ts +3 -0
- package/dist_ts_web/appstate/email-domains.js +47 -14
- package/dist_ts_web/appstate/email-ops.d.ts +1 -0
- package/dist_ts_web/appstate/email-ops.js +20 -2
- package/dist_ts_web/appstate/realtime.d.ts +27 -0
- package/dist_ts_web/appstate/realtime.js +71 -0
- package/dist_ts_web/appstate/remoteingress.js +9 -3
- package/dist_ts_web/appstate/routes.js +16 -5
- package/dist_ts_web/appstate/runtime.js +135 -7
- package/dist_ts_web/appstate.d.ts +1 -0
- package/dist_ts_web/appstate.js +2 -1
- package/dist_ts_web/elements/email/email-domain-creation-progress.d.ts +33 -0
- package/dist_ts_web/elements/email/email-domain-creation-progress.js +257 -0
- package/dist_ts_web/elements/email/ops-view-email-domains.js +233 -68
- package/dist_ts_web/elements/email/ops-view-emails.d.ts +4 -2
- package/dist_ts_web/elements/email/ops-view-emails.js +25 -13
- package/dist_ts_web/elements/overview/ops-view-config.js +5 -5
- package/package.json +22 -19
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +121 -54
- package/ts/db/classes.cache.cleaner.ts +18 -3
- package/ts/db/classes.dcrouter-db.ts +0 -2
- package/ts/db/documents/classes.cached.email.ts +78 -4
- package/ts/db/documents/classes.dns-record.doc.ts +1 -0
- package/ts/dns/classes.dns-server-runtime.ts +9 -45
- package/ts/dns/index.ts +1 -0
- package/ts/dns/manager.dns.ts +50 -16
- package/ts/dns/providers/cloudflare.provider.ts +21 -3
- package/ts/dns/txt-record-presentation.ts +134 -0
- package/ts/email/classes.accepted-email-spool.ts +220 -14
- package/ts/email/classes.email-domain.manager.ts +201 -7
- package/ts/email/classes.email-settings.manager.ts +1 -1
- package/ts/email/classes.mail-dns-sync.ts +588 -122
- package/ts/email/classes.smartmta-blob-storage-manager.ts +3 -2
- package/ts/email/classes.workapp-mail-manager.ts +34 -1
- package/ts/email/index.ts +2 -0
- package/ts/email/mail-dns-txt-policy.ts +505 -0
- package/ts/email/reconcile-mail-dns-singleton.ts +224 -0
- package/ts/opsserver/classes.opsserver.ts +42 -0
- package/ts/opsserver/handlers/certificate.handler.ts +24 -3
- package/ts/opsserver/handlers/config.handler.ts +9 -5
- package/ts/opsserver/handlers/dns-provider.handler.ts +18 -1
- package/ts/opsserver/handlers/dns-record.handler.ts +24 -3
- package/ts/opsserver/handlers/domain.handler.ts +36 -2
- package/ts/opsserver/handlers/email-domain.handler.ts +4 -5
- package/ts/opsserver/handlers/email-ops.handler.ts +29 -6
- package/ts/opsserver/handlers/index.ts +1 -0
- package/ts/opsserver/handlers/logs.handler.ts +2 -2
- package/ts/opsserver/handlers/realtime.handler.ts +222 -0
- package/ts/opsserver/handlers/remoteingress.handler.ts +21 -0
- package/ts/opsserver/handlers/route-management.handler.ts +46 -4
- package/ts/paths.ts +0 -25
- package/ts/plugins.ts +0 -68
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate/certificates.ts +6 -1
- package/ts_web/appstate/domains.ts +11 -4
- package/ts_web/appstate/email-domains.ts +68 -14
- package/ts_web/appstate/email-ops.ts +20 -1
- package/ts_web/appstate/realtime.ts +109 -0
- package/ts_web/appstate/remoteingress.ts +6 -2
- package/ts_web/appstate/routes.ts +11 -4
- package/ts_web/appstate/runtime.ts +176 -7
- package/ts_web/appstate.ts +1 -0
- package/ts_web/elements/email/email-domain-creation-progress.ts +340 -0
- package/ts_web/elements/email/ops-view-email-domains.ts +273 -74
- package/ts_web/elements/email/ops-view-emails.ts +23 -16
- package/ts_web/elements/overview/ops-view-config.ts +4 -4
|
@@ -2,6 +2,7 @@ import * as plugins from '../plugins.js';
|
|
|
2
2
|
import * as interfaces from '../../ts_interfaces/index.js';
|
|
3
3
|
import { appState } from './shared.js';
|
|
4
4
|
import { getActionContext } from './login.js';
|
|
5
|
+
import { captureRealtimeTicket, isRealtimeTicketCurrent } from './realtime.js';
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
// ============================================================================
|
|
@@ -20,6 +21,7 @@ export type TEmailDomainOperationName =
|
|
|
20
21
|
export interface IEmailDomainOperationState extends interfaces.requests.IEmailDomainActionResult {
|
|
21
22
|
operation: TEmailDomainOperationName;
|
|
22
23
|
completedAt: number;
|
|
24
|
+
domain?: interfaces.data.IEmailDomain;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export interface IEmailDomainsState {
|
|
@@ -46,7 +48,9 @@ export const emailDomainsStatePart = await appState.getStatePart<IEmailDomainsSt
|
|
|
46
48
|
|
|
47
49
|
const createOperationState = (
|
|
48
50
|
operationArg: TEmailDomainOperationName,
|
|
49
|
-
responseArg: interfaces.requests.IEmailDomainActionResult
|
|
51
|
+
responseArg: interfaces.requests.IEmailDomainActionResult & {
|
|
52
|
+
domain?: interfaces.data.IEmailDomain;
|
|
53
|
+
},
|
|
50
54
|
): IEmailDomainOperationState => ({
|
|
51
55
|
operation: operationArg,
|
|
52
56
|
completedAt: Date.now(),
|
|
@@ -120,6 +124,7 @@ export const fetchEmailDomainsAction = emailDomainsStatePart.createAction(
|
|
|
120
124
|
const context = getActionContext();
|
|
121
125
|
const currentState = statePartArg.getState()!;
|
|
122
126
|
if (!context.identity) return currentState;
|
|
127
|
+
const ticket = captureRealtimeTicket('emailDomains');
|
|
123
128
|
|
|
124
129
|
try {
|
|
125
130
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
@@ -130,8 +135,9 @@ export const fetchEmailDomainsAction = emailDomainsStatePart.createAction(
|
|
|
130
135
|
>('/typedrequest', 'getEmailServerSettings');
|
|
131
136
|
const response = await request.fire({ identity: context.identity });
|
|
132
137
|
const settingsResponse = await settingsRequest.fire({ identity: context.identity });
|
|
138
|
+
if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
|
|
133
139
|
return {
|
|
134
|
-
...
|
|
140
|
+
...statePartArg.getState()!,
|
|
135
141
|
domains: response.domains,
|
|
136
142
|
settings: settingsResponse.settings,
|
|
137
143
|
isLoading: false,
|
|
@@ -139,8 +145,9 @@ export const fetchEmailDomainsAction = emailDomainsStatePart.createAction(
|
|
|
139
145
|
lastUpdated: Date.now(),
|
|
140
146
|
};
|
|
141
147
|
} catch (error: unknown) {
|
|
148
|
+
if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
|
|
142
149
|
return {
|
|
143
|
-
...
|
|
150
|
+
...statePartArg.getState()!,
|
|
144
151
|
isLoading: false,
|
|
145
152
|
error: error instanceof Error ? error.message : 'Failed to fetch email domains',
|
|
146
153
|
};
|
|
@@ -169,14 +176,16 @@ export const createEmailDomainAction = emailDomainsStatePart.createAction<{
|
|
|
169
176
|
>('/typedrequest', 'createEmailDomain');
|
|
170
177
|
const response = await request.fire({ identity: context.identity, ...args });
|
|
171
178
|
const operation = createOperationState('create', response);
|
|
172
|
-
if (!response.success) {
|
|
173
|
-
return applyOperationState(currentState, operation, 'Failed to create email domain');
|
|
174
|
-
}
|
|
175
179
|
const refreshedState = await actionContext!.dispatch(fetchEmailDomainsAction, null);
|
|
176
|
-
return applyOperationState(
|
|
180
|
+
return applyOperationState(
|
|
181
|
+
refreshedState,
|
|
182
|
+
operation,
|
|
183
|
+
response.success ? 'Email domain created' : 'Failed to create email domain',
|
|
184
|
+
);
|
|
177
185
|
} catch (error: unknown) {
|
|
178
186
|
const operation = createFailedOperationState('create', error, 'Failed to create email domain');
|
|
179
|
-
|
|
187
|
+
const refreshedState = await actionContext!.dispatch(fetchEmailDomainsAction, null);
|
|
188
|
+
return applyOperationState(refreshedState, operation, 'Failed to create email domain');
|
|
180
189
|
}
|
|
181
190
|
});
|
|
182
191
|
|
|
@@ -260,27 +269,50 @@ export const deleteEmailDomainAction = emailDomainsStatePart.createAction<string
|
|
|
260
269
|
);
|
|
261
270
|
|
|
262
271
|
export const validateEmailDomainAction = emailDomainsStatePart.createAction<string>(
|
|
263
|
-
async (statePartArg, id
|
|
272
|
+
async (statePartArg, id) => {
|
|
264
273
|
const context = getActionContext();
|
|
265
274
|
const currentState = statePartArg.getState()!;
|
|
266
275
|
if (!context.identity) {
|
|
267
276
|
const operation = createFailedOperationState('validate', new Error('Not authenticated'), 'Not authenticated');
|
|
268
277
|
return applyOperationState(currentState, operation, 'Failed to validate email domain DNS');
|
|
269
278
|
}
|
|
279
|
+
|
|
280
|
+
const ticket = captureRealtimeTicket('emailDomains');
|
|
270
281
|
try {
|
|
271
282
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
272
283
|
interfaces.requests.IReq_ValidateEmailDomain
|
|
273
284
|
>('/typedrequest', 'validateEmailDomain');
|
|
274
285
|
const response = await request.fire({ identity: context.identity, id });
|
|
275
286
|
const operation = createOperationState('validate', response);
|
|
276
|
-
|
|
277
|
-
|
|
287
|
+
let latestState = statePartArg.getState()!;
|
|
288
|
+
|
|
289
|
+
if (response.domain && isRealtimeTicketCurrent(ticket)) {
|
|
290
|
+
const existingIndex = latestState.domains.findIndex((domain) => domain.id === response.domain!.id);
|
|
291
|
+
const domains = [...latestState.domains];
|
|
292
|
+
if (existingIndex >= 0) {
|
|
293
|
+
domains[existingIndex] = response.domain;
|
|
294
|
+
} else {
|
|
295
|
+
domains.push(response.domain);
|
|
296
|
+
}
|
|
297
|
+
latestState = {
|
|
298
|
+
...latestState,
|
|
299
|
+
domains,
|
|
300
|
+
lastUpdated: Date.now(),
|
|
301
|
+
};
|
|
278
302
|
}
|
|
279
|
-
|
|
280
|
-
return applyOperationState(
|
|
303
|
+
|
|
304
|
+
return applyOperationState(
|
|
305
|
+
latestState,
|
|
306
|
+
operation,
|
|
307
|
+
response.success ? 'Email domain DNS validated' : 'Failed to validate email domain DNS',
|
|
308
|
+
);
|
|
281
309
|
} catch (error: unknown) {
|
|
282
310
|
const operation = createFailedOperationState('validate', error, 'Failed to validate email domain DNS');
|
|
283
|
-
return applyOperationState(
|
|
311
|
+
return applyOperationState(
|
|
312
|
+
statePartArg.getState()!,
|
|
313
|
+
operation,
|
|
314
|
+
'Failed to validate email domain DNS',
|
|
315
|
+
);
|
|
284
316
|
}
|
|
285
317
|
},
|
|
286
318
|
);
|
|
@@ -315,6 +347,28 @@ export const provisionEmailDomainDnsAction = emailDomainsStatePart.createAction<
|
|
|
315
347
|
// Email Domain Standalone Functions
|
|
316
348
|
// ============================================================================
|
|
317
349
|
|
|
350
|
+
export async function fetchEmailDomainsSnapshot(): Promise<interfaces.data.IEmailDomain[]> {
|
|
351
|
+
const context = getActionContext();
|
|
352
|
+
if (!context.identity) throw new Error('Not authenticated');
|
|
353
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
354
|
+
interfaces.requests.IReq_GetEmailDomains
|
|
355
|
+
>('/typedrequest', 'getEmailDomains');
|
|
356
|
+
const response = await request.fire({ identity: context.identity });
|
|
357
|
+
return response.domains;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export async function fetchEmailDomainSnapshot(
|
|
361
|
+
idArg: string,
|
|
362
|
+
): Promise<interfaces.data.IEmailDomain | null> {
|
|
363
|
+
const context = getActionContext();
|
|
364
|
+
if (!context.identity) throw new Error('Not authenticated');
|
|
365
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
366
|
+
interfaces.requests.IReq_GetEmailDomain
|
|
367
|
+
>('/typedrequest', 'getEmailDomain');
|
|
368
|
+
const response = await request.fire({ identity: context.identity, id: idArg });
|
|
369
|
+
return response.domain;
|
|
370
|
+
}
|
|
371
|
+
|
|
318
372
|
export async function fetchEmailDomainDnsRecords(id: string) {
|
|
319
373
|
const context = getActionContext();
|
|
320
374
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
@@ -2,6 +2,7 @@ import * as plugins from '../plugins.js';
|
|
|
2
2
|
import * as interfaces from '../../ts_interfaces/index.js';
|
|
3
3
|
import { appState } from './shared.js';
|
|
4
4
|
import { getActionContext } from './login.js';
|
|
5
|
+
import { captureRealtimeTicket, isRealtimeTicketCurrent } from './realtime.js';
|
|
5
6
|
|
|
6
7
|
export interface IEmailOpsState {
|
|
7
8
|
emails: interfaces.requests.IEmail[];
|
|
@@ -29,6 +30,7 @@ export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (stateP
|
|
|
29
30
|
const context = getActionContext();
|
|
30
31
|
const currentState = statePartArg.getState()!;
|
|
31
32
|
if (!context.identity) return currentState;
|
|
33
|
+
const ticket = captureRealtimeTicket(['emailQueue', 'smtpAttempts']);
|
|
32
34
|
|
|
33
35
|
try {
|
|
34
36
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
@@ -38,16 +40,19 @@ export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (stateP
|
|
|
38
40
|
const response = await request.fire({
|
|
39
41
|
identity: context.identity,
|
|
40
42
|
});
|
|
43
|
+
if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
|
|
41
44
|
|
|
42
45
|
return {
|
|
46
|
+
...statePartArg.getState()!,
|
|
43
47
|
emails: response.emails,
|
|
44
48
|
isLoading: false,
|
|
45
49
|
error: null,
|
|
46
50
|
lastUpdated: Date.now(),
|
|
47
51
|
};
|
|
48
52
|
} catch (error) {
|
|
53
|
+
if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
|
|
49
54
|
return {
|
|
50
|
-
...
|
|
55
|
+
...statePartArg.getState()!,
|
|
51
56
|
isLoading: false,
|
|
52
57
|
error: error instanceof Error ? error.message : 'Failed to fetch emails',
|
|
53
58
|
};
|
|
@@ -55,3 +60,17 @@ export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (stateP
|
|
|
55
60
|
});
|
|
56
61
|
|
|
57
62
|
// ============================================================================
|
|
63
|
+
export async function fetchEmailDetailSnapshot(
|
|
64
|
+
emailIdArg: string,
|
|
65
|
+
): Promise<interfaces.requests.IEmailDetail | null> {
|
|
66
|
+
const context = getActionContext();
|
|
67
|
+
if (!context.identity) return null;
|
|
68
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
69
|
+
interfaces.requests.IReq_GetEmailDetail
|
|
70
|
+
>('/typedrequest', 'getEmailDetail');
|
|
71
|
+
const response = await request.fire({
|
|
72
|
+
identity: context.identity,
|
|
73
|
+
emailId: emailIdArg,
|
|
74
|
+
});
|
|
75
|
+
return response.email;
|
|
76
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as interfaces from '../../ts_interfaces/index.js';
|
|
2
|
+
|
|
3
|
+
export interface IOpsRealtimeTicket {
|
|
4
|
+
serverEpoch: string | null;
|
|
5
|
+
revisions: Partial<interfaces.requests.IOpsRealtimeRevisionMap>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type TOpsRealtimeListener = (
|
|
9
|
+
invalidationsArg: interfaces.requests.IOpsRealtimeInvalidation[],
|
|
10
|
+
) => void;
|
|
11
|
+
|
|
12
|
+
const emptyRevisions = (): interfaces.requests.IOpsRealtimeRevisionMap => {
|
|
13
|
+
return Object.fromEntries(
|
|
14
|
+
interfaces.requests.opsRealtimeResources.map((resource) => [resource, 0]),
|
|
15
|
+
) as unknown as interfaces.requests.IOpsRealtimeRevisionMap;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Orders pushes and HTTP reads. A read may apply only while every captured
|
|
20
|
+
* resource revision is still current.
|
|
21
|
+
*/
|
|
22
|
+
export class OpsRealtimeFreshness {
|
|
23
|
+
private serverEpoch: string | null = null;
|
|
24
|
+
private revisions = emptyRevisions();
|
|
25
|
+
private listeners = new Set<TOpsRealtimeListener>();
|
|
26
|
+
|
|
27
|
+
public getKnownState(): {
|
|
28
|
+
knownServerEpoch?: string;
|
|
29
|
+
knownRevisions: interfaces.requests.IOpsRealtimeRevisionMap;
|
|
30
|
+
} {
|
|
31
|
+
return {
|
|
32
|
+
knownServerEpoch: this.serverEpoch || undefined,
|
|
33
|
+
knownRevisions: { ...this.revisions },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public capture(
|
|
38
|
+
resourcesArg:
|
|
39
|
+
| interfaces.requests.TOpsRealtimeResource
|
|
40
|
+
| interfaces.requests.TOpsRealtimeResource[],
|
|
41
|
+
): IOpsRealtimeTicket {
|
|
42
|
+
const resources = Array.isArray(resourcesArg) ? resourcesArg : [resourcesArg];
|
|
43
|
+
return {
|
|
44
|
+
serverEpoch: this.serverEpoch,
|
|
45
|
+
revisions: Object.fromEntries(
|
|
46
|
+
resources.map((resource) => [resource, this.revisions[resource]]),
|
|
47
|
+
),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public isCurrent(ticketArg: IOpsRealtimeTicket): boolean {
|
|
52
|
+
if (ticketArg.serverEpoch !== this.serverEpoch) return false;
|
|
53
|
+
return Object.entries(ticketArg.revisions).every(([resource, revision]) => {
|
|
54
|
+
return this.revisions[resource as interfaces.requests.TOpsRealtimeResource] === revision;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public accept(
|
|
59
|
+
serverEpochArg: string,
|
|
60
|
+
invalidationsArg: interfaces.requests.IOpsRealtimeInvalidation[],
|
|
61
|
+
): interfaces.requests.IOpsRealtimeInvalidation[] {
|
|
62
|
+
const epochChanged = this.serverEpoch !== serverEpochArg;
|
|
63
|
+
if (epochChanged) {
|
|
64
|
+
this.serverEpoch = serverEpochArg;
|
|
65
|
+
this.revisions = emptyRevisions();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const accepted: interfaces.requests.IOpsRealtimeInvalidation[] = [];
|
|
69
|
+
for (const invalidation of invalidationsArg) {
|
|
70
|
+
if (!interfaces.requests.opsRealtimeResources.includes(invalidation.resource)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (invalidation.revision <= this.revisions[invalidation.resource]) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
this.revisions[invalidation.resource] = invalidation.revision;
|
|
77
|
+
accepted.push(invalidation);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (accepted.length > 0) {
|
|
81
|
+
for (const listener of Array.from(this.listeners)) {
|
|
82
|
+
listener(accepted);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return accepted;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public subscribe(listenerArg: TOpsRealtimeListener): () => void {
|
|
89
|
+
this.listeners.add(listenerArg);
|
|
90
|
+
return () => this.listeners.delete(listenerArg);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public reset(): void {
|
|
94
|
+
this.serverEpoch = null;
|
|
95
|
+
this.revisions = emptyRevisions();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const opsRealtimeFreshness = new OpsRealtimeFreshness();
|
|
100
|
+
|
|
101
|
+
export const captureRealtimeTicket = (
|
|
102
|
+
resourcesArg:
|
|
103
|
+
| interfaces.requests.TOpsRealtimeResource
|
|
104
|
+
| interfaces.requests.TOpsRealtimeResource[],
|
|
105
|
+
): IOpsRealtimeTicket => opsRealtimeFreshness.capture(resourcesArg);
|
|
106
|
+
|
|
107
|
+
export const isRealtimeTicketCurrent = (ticketArg: IOpsRealtimeTicket): boolean => {
|
|
108
|
+
return opsRealtimeFreshness.isCurrent(ticketArg);
|
|
109
|
+
};
|
|
@@ -2,6 +2,7 @@ import * as plugins from '../plugins.js';
|
|
|
2
2
|
import * as interfaces from '../../ts_interfaces/index.js';
|
|
3
3
|
import { appState } from './shared.js';
|
|
4
4
|
import { getActionContext } from './login.js';
|
|
5
|
+
import { captureRealtimeTicket, isRealtimeTicketCurrent } from './realtime.js';
|
|
5
6
|
|
|
6
7
|
// ============================================================================
|
|
7
8
|
// Remote Ingress State
|
|
@@ -52,6 +53,7 @@ export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(asyn
|
|
|
52
53
|
const context = getActionContext();
|
|
53
54
|
const currentState = statePartArg.getState()!;
|
|
54
55
|
if (!context.identity) return currentState;
|
|
56
|
+
const ticket = captureRealtimeTicket('edges');
|
|
55
57
|
|
|
56
58
|
try {
|
|
57
59
|
const edgesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
@@ -71,9 +73,10 @@ export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(asyn
|
|
|
71
73
|
statusRequest.fire({ identity: context.identity }),
|
|
72
74
|
hubSettingsRequest.fire({ identity: context.identity }),
|
|
73
75
|
]);
|
|
76
|
+
if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
|
|
74
77
|
|
|
75
78
|
return {
|
|
76
|
-
...
|
|
79
|
+
...statePartArg.getState()!,
|
|
77
80
|
edges: edgesResponse.edges,
|
|
78
81
|
statuses: statusResponse.statuses,
|
|
79
82
|
hubSettings: hubSettingsResponse.settings,
|
|
@@ -82,8 +85,9 @@ export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(asyn
|
|
|
82
85
|
lastUpdated: Date.now(),
|
|
83
86
|
};
|
|
84
87
|
} catch (error) {
|
|
88
|
+
if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
|
|
85
89
|
return {
|
|
86
|
-
...
|
|
90
|
+
...statePartArg.getState()!,
|
|
87
91
|
isLoading: false,
|
|
88
92
|
error: error instanceof Error ? error.message : 'Failed to fetch remote ingress data',
|
|
89
93
|
};
|
|
@@ -2,6 +2,7 @@ import * as plugins from '../plugins.js';
|
|
|
2
2
|
import * as interfaces from '../../ts_interfaces/index.js';
|
|
3
3
|
import { appState } from './shared.js';
|
|
4
4
|
import { getActionContext } from './login.js';
|
|
5
|
+
import { captureRealtimeTicket, isRealtimeTicketCurrent } from './realtime.js';
|
|
5
6
|
|
|
6
7
|
// ============================================================================
|
|
7
8
|
// Route Management State
|
|
@@ -40,6 +41,7 @@ export const fetchMergedRoutesAction = routeManagementStatePart.createAction(asy
|
|
|
40
41
|
const context = getActionContext();
|
|
41
42
|
const currentState = statePartArg.getState()!;
|
|
42
43
|
if (!context.identity) return currentState;
|
|
44
|
+
const ticket = captureRealtimeTicket('routes');
|
|
43
45
|
|
|
44
46
|
try {
|
|
45
47
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
@@ -49,9 +51,10 @@ export const fetchMergedRoutesAction = routeManagementStatePart.createAction(asy
|
|
|
49
51
|
const response = await request.fire({
|
|
50
52
|
identity: context.identity,
|
|
51
53
|
});
|
|
54
|
+
if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
|
|
52
55
|
|
|
53
56
|
return {
|
|
54
|
-
...
|
|
57
|
+
...statePartArg.getState()!,
|
|
55
58
|
mergedRoutes: response.routes,
|
|
56
59
|
warnings: response.warnings,
|
|
57
60
|
isLoading: false,
|
|
@@ -59,8 +62,9 @@ export const fetchMergedRoutesAction = routeManagementStatePart.createAction(asy
|
|
|
59
62
|
lastUpdated: Date.now(),
|
|
60
63
|
};
|
|
61
64
|
} catch (error) {
|
|
65
|
+
if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
|
|
62
66
|
return {
|
|
63
|
-
...
|
|
67
|
+
...statePartArg.getState()!,
|
|
64
68
|
isLoading: false,
|
|
65
69
|
error: error instanceof Error ? error.message : 'Failed to fetch routes',
|
|
66
70
|
};
|
|
@@ -71,6 +75,7 @@ export const fetchHttpRedirectsAction = routeManagementStatePart.createAction(as
|
|
|
71
75
|
const context = getActionContext();
|
|
72
76
|
const currentState = statePartArg.getState()!;
|
|
73
77
|
if (!context.identity) return currentState;
|
|
78
|
+
const ticket = captureRealtimeTicket('routes');
|
|
74
79
|
|
|
75
80
|
try {
|
|
76
81
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
@@ -80,17 +85,19 @@ export const fetchHttpRedirectsAction = routeManagementStatePart.createAction(as
|
|
|
80
85
|
const response = await request.fire({
|
|
81
86
|
identity: context.identity,
|
|
82
87
|
});
|
|
88
|
+
if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
|
|
83
89
|
|
|
84
90
|
return {
|
|
85
|
-
...
|
|
91
|
+
...statePartArg.getState()!,
|
|
86
92
|
httpRedirects: response.redirects,
|
|
87
93
|
isLoading: false,
|
|
88
94
|
error: null,
|
|
89
95
|
lastUpdated: Date.now(),
|
|
90
96
|
};
|
|
91
97
|
} catch (error) {
|
|
98
|
+
if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
|
|
92
99
|
return {
|
|
93
|
-
...
|
|
100
|
+
...statePartArg.getState()!,
|
|
94
101
|
isLoading: false,
|
|
95
102
|
error: error instanceof Error ? error.message : 'Failed to fetch HTTP redirects',
|
|
96
103
|
};
|
|
@@ -10,14 +10,124 @@ import { securityPolicyStatePart, fetchSecurityPolicyAction } from './security.j
|
|
|
10
10
|
import { certificateStatePart, fetchCertificateOverviewAction } from './certificates.js';
|
|
11
11
|
import { remoteIngressStatePart, fetchRemoteIngressAction } from './remoteingress.js';
|
|
12
12
|
import { vpnStatePart, fetchVpnAction } from './vpn.js';
|
|
13
|
-
|
|
13
|
+
import { emailDomainsStatePart, fetchEmailDomainsAction } from './email-domains.js';
|
|
14
|
+
import { emailOpsStatePart, fetchAllEmailsAction } from './email-ops.js';
|
|
15
|
+
import {
|
|
16
|
+
domainsStatePart,
|
|
17
|
+
fetchDnsRecordsForDomainAction,
|
|
18
|
+
fetchDomainsAndProvidersAction,
|
|
19
|
+
} from './domains.js';
|
|
20
|
+
import {
|
|
21
|
+
fetchHttpRedirectsAction,
|
|
22
|
+
fetchMergedRoutesAction,
|
|
23
|
+
routeManagementStatePart,
|
|
24
|
+
} from './routes.js';
|
|
25
|
+
import { opsRealtimeFreshness } from './realtime.js';
|
|
14
26
|
|
|
15
27
|
// ============================================================================
|
|
16
28
|
// TypedSocket Client for Real-time Log Streaming
|
|
17
29
|
// ============================================================================
|
|
18
30
|
|
|
19
31
|
let socketClient: plugins.typedsocket.TypedSocket | null = null;
|
|
32
|
+
let socketStatusSubscription: { unsubscribe: () => void } | null = null;
|
|
33
|
+
let socketConnectInFlight = false;
|
|
34
|
+
let socketGeneration = 0;
|
|
20
35
|
const socketRouter = new plugins.domtools.plugins.typedrequest.TypedRouter();
|
|
36
|
+
type TRealtimeRefreshGroup =
|
|
37
|
+
| 'emailDomains'
|
|
38
|
+
| 'emailOps'
|
|
39
|
+
| 'dns'
|
|
40
|
+
| 'routes'
|
|
41
|
+
| 'edges'
|
|
42
|
+
| 'certificates';
|
|
43
|
+
|
|
44
|
+
const pendingRealtimeRefreshes = new Set<TRealtimeRefreshGroup>();
|
|
45
|
+
let realtimeRefreshLoopRunning = false;
|
|
46
|
+
|
|
47
|
+
const resourceRefreshGroups: Record<
|
|
48
|
+
interfaces.requests.TOpsRealtimeResource,
|
|
49
|
+
TRealtimeRefreshGroup
|
|
50
|
+
> = {
|
|
51
|
+
emailDomains: 'emailDomains',
|
|
52
|
+
emailQueue: 'emailOps',
|
|
53
|
+
smtpAttempts: 'emailOps',
|
|
54
|
+
dns: 'dns',
|
|
55
|
+
routes: 'routes',
|
|
56
|
+
edges: 'edges',
|
|
57
|
+
certificates: 'certificates',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const refreshRealtimeGroup = async (groupArg: TRealtimeRefreshGroup): Promise<void> => {
|
|
61
|
+
switch (groupArg) {
|
|
62
|
+
case 'emailDomains':
|
|
63
|
+
await emailDomainsStatePart.dispatchAction(fetchEmailDomainsAction, null);
|
|
64
|
+
break;
|
|
65
|
+
case 'emailOps':
|
|
66
|
+
await emailOpsStatePart.dispatchAction(fetchAllEmailsAction, null);
|
|
67
|
+
break;
|
|
68
|
+
case 'dns': {
|
|
69
|
+
const selectedDomainId = domainsStatePart.getState()!.selectedDomainId;
|
|
70
|
+
await domainsStatePart.dispatchAction(fetchDomainsAndProvidersAction, null);
|
|
71
|
+
if (selectedDomainId) {
|
|
72
|
+
await domainsStatePart.dispatchAction(
|
|
73
|
+
fetchDnsRecordsForDomainAction,
|
|
74
|
+
{ domainId: selectedDomainId },
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case 'routes':
|
|
80
|
+
await Promise.all([
|
|
81
|
+
routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null),
|
|
82
|
+
routeManagementStatePart.dispatchAction(fetchHttpRedirectsAction, null),
|
|
83
|
+
]);
|
|
84
|
+
break;
|
|
85
|
+
case 'edges':
|
|
86
|
+
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
|
87
|
+
break;
|
|
88
|
+
case 'certificates':
|
|
89
|
+
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const runRealtimeRefreshLoop = async (): Promise<void> => {
|
|
95
|
+
if (realtimeRefreshLoopRunning) return;
|
|
96
|
+
realtimeRefreshLoopRunning = true;
|
|
97
|
+
try {
|
|
98
|
+
while (pendingRealtimeRefreshes.size > 0) {
|
|
99
|
+
const groups = Array.from(pendingRealtimeRefreshes);
|
|
100
|
+
pendingRealtimeRefreshes.clear();
|
|
101
|
+
await Promise.all(
|
|
102
|
+
groups.map(async (group) => {
|
|
103
|
+
try {
|
|
104
|
+
await refreshRealtimeGroup(group);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Realtime resource refresh failed:', group, error);
|
|
107
|
+
}
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
} finally {
|
|
112
|
+
realtimeRefreshLoopRunning = false;
|
|
113
|
+
if (pendingRealtimeRefreshes.size > 0) {
|
|
114
|
+
queueMicrotask(() => void runRealtimeRefreshLoop());
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const acceptRealtimeInvalidations = (
|
|
120
|
+
serverEpochArg: string,
|
|
121
|
+
invalidationsArg: interfaces.requests.IOpsRealtimeInvalidation[],
|
|
122
|
+
): void => {
|
|
123
|
+
const accepted = opsRealtimeFreshness.accept(serverEpochArg, invalidationsArg);
|
|
124
|
+
for (const invalidation of accepted) {
|
|
125
|
+
pendingRealtimeRefreshes.add(resourceRefreshGroups[invalidation.resource]);
|
|
126
|
+
}
|
|
127
|
+
if (accepted.length > 0) {
|
|
128
|
+
queueMicrotask(() => void runRealtimeRefreshLoop());
|
|
129
|
+
}
|
|
130
|
+
};
|
|
21
131
|
|
|
22
132
|
// Batched log entry handler — buffers incoming entries and flushes once per animation frame
|
|
23
133
|
let logEntryBuffer: interfaces.data.ILogEntry[] = [];
|
|
@@ -51,28 +161,85 @@ socketRouter.addTypedHandler(
|
|
|
51
161
|
)
|
|
52
162
|
);
|
|
53
163
|
|
|
164
|
+
socketRouter.addTypedHandler(
|
|
165
|
+
new plugins.domtools.plugins.typedrequest.TypedHandler<
|
|
166
|
+
interfaces.requests.IReq_PushOpsRealtimeInvalidation
|
|
167
|
+
>(
|
|
168
|
+
'pushOpsRealtimeInvalidation',
|
|
169
|
+
async (dataArg) => {
|
|
170
|
+
acceptRealtimeInvalidations(dataArg.serverEpoch, dataArg.invalidations);
|
|
171
|
+
return {};
|
|
172
|
+
},
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const subscribeSocket = async (
|
|
177
|
+
clientArg: plugins.typedsocket.TypedSocket,
|
|
178
|
+
generationArg: number,
|
|
179
|
+
): Promise<void> => {
|
|
180
|
+
const identity = getActionContext().identity;
|
|
181
|
+
if (!identity || generationArg !== socketGeneration || clientArg !== socketClient) return;
|
|
182
|
+
|
|
183
|
+
const request = clientArg.createTypedRequest<interfaces.requests.IReq_SubscribeOpsRealtime>(
|
|
184
|
+
'subscribeOpsRealtime',
|
|
185
|
+
);
|
|
186
|
+
const response = await request.fire({
|
|
187
|
+
identity,
|
|
188
|
+
...opsRealtimeFreshness.getKnownState(),
|
|
189
|
+
});
|
|
190
|
+
if (generationArg !== socketGeneration || clientArg !== socketClient) return;
|
|
191
|
+
acceptRealtimeInvalidations(response.serverEpoch, response.invalidations);
|
|
192
|
+
};
|
|
193
|
+
|
|
54
194
|
async function connectSocket() {
|
|
55
|
-
if (socketClient) return;
|
|
195
|
+
if (socketClient || socketConnectInFlight || document.hidden) return;
|
|
196
|
+
socketConnectInFlight = true;
|
|
197
|
+
const generation = ++socketGeneration;
|
|
56
198
|
try {
|
|
57
|
-
|
|
199
|
+
const client = await plugins.typedsocket.TypedSocket.createClient(
|
|
58
200
|
socketRouter,
|
|
59
201
|
plugins.typedsocket.TypedSocket.useWindowLocationOriginUrl(),
|
|
202
|
+
{ autoReconnect: true },
|
|
60
203
|
);
|
|
61
|
-
|
|
204
|
+
if (generation !== socketGeneration || !loginStatePart.getState()!.isLoggedIn) {
|
|
205
|
+
await client.stop();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
socketClient = client;
|
|
210
|
+
socketStatusSubscription = client.statusSubject.subscribe((status) => {
|
|
211
|
+
if (status === 'connected') {
|
|
212
|
+
void subscribeSocket(client, generation).catch((error) => {
|
|
213
|
+
console.error('Realtime subscription failed after reconnect:', error);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
await subscribeSocket(client, generation);
|
|
62
218
|
} catch (err) {
|
|
63
219
|
console.error('TypedSocket connection failed:', err);
|
|
220
|
+
socketStatusSubscription?.unsubscribe();
|
|
221
|
+
socketStatusSubscription = null;
|
|
222
|
+
if (socketClient) {
|
|
223
|
+
await socketClient.stop().catch(() => undefined);
|
|
224
|
+
}
|
|
64
225
|
socketClient = null;
|
|
226
|
+
} finally {
|
|
227
|
+
socketConnectInFlight = false;
|
|
65
228
|
}
|
|
66
229
|
}
|
|
67
230
|
|
|
68
231
|
async function disconnectSocket() {
|
|
69
|
-
|
|
232
|
+
socketGeneration++;
|
|
233
|
+
socketStatusSubscription?.unsubscribe();
|
|
234
|
+
socketStatusSubscription = null;
|
|
235
|
+
const client = socketClient;
|
|
236
|
+
socketClient = null;
|
|
237
|
+
if (client) {
|
|
70
238
|
try {
|
|
71
|
-
await
|
|
239
|
+
await client.stop();
|
|
72
240
|
} catch {
|
|
73
241
|
// ignore disconnect errors
|
|
74
242
|
}
|
|
75
|
-
socketClient = null;
|
|
76
243
|
}
|
|
77
244
|
}
|
|
78
245
|
|
|
@@ -285,6 +452,8 @@ loginStatePart.select((s) => s.isLoggedIn).subscribe((isLoggedIn) => {
|
|
|
285
452
|
if (isLoggedIn) {
|
|
286
453
|
connectSocket();
|
|
287
454
|
} else {
|
|
455
|
+
pendingRealtimeRefreshes.clear();
|
|
456
|
+
opsRealtimeFreshness.reset();
|
|
288
457
|
disconnectSocket();
|
|
289
458
|
}
|
|
290
459
|
}
|
package/ts_web/appstate.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * from './appstate/domains.js';
|
|
|
19
19
|
export * from './appstate/routes.js';
|
|
20
20
|
export * from './appstate/users.js';
|
|
21
21
|
export * from './appstate/email-domains.js';
|
|
22
|
+
export * from './appstate/realtime.js';
|
|
22
23
|
|
|
23
24
|
// Side-effect module: auto-refresh scheduling and TypedSocket log streaming.
|
|
24
25
|
import './appstate/runtime.js';
|