@serve.zone/dcrouter 16.5.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.
Files changed (153) hide show
  1. package/.smartconfig.json +5 -2
  2. package/deno.json +1 -1
  3. package/dist_serve/bundle.js +1316 -1151
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/classes.dcrouter.d.ts +6 -1
  6. package/dist_ts/classes.dcrouter.js +119 -41
  7. package/dist_ts/db/classes.cache.cleaner.d.ts +3 -0
  8. package/dist_ts/db/classes.cache.cleaner.js +7 -3
  9. package/dist_ts/db/classes.dcrouter-db.js +1 -2
  10. package/dist_ts/db/documents/classes.cached.email.d.ts +41 -4
  11. package/dist_ts/db/documents/classes.cached.email.js +36 -5
  12. package/dist_ts/db/documents/classes.dns-record.doc.js +3 -1
  13. package/dist_ts/db/documents/classes.smartmta-storage.doc.d.ts +8 -0
  14. package/dist_ts/db/documents/classes.smartmta-storage.doc.js +91 -0
  15. package/dist_ts/db/documents/index.d.ts +1 -0
  16. package/dist_ts/db/documents/index.js +2 -1
  17. package/dist_ts/dns/classes.dns-server-runtime.d.ts +0 -1
  18. package/dist_ts/dns/classes.dns-server-runtime.js +4 -36
  19. package/dist_ts/dns/index.d.ts +1 -0
  20. package/dist_ts/dns/index.js +2 -1
  21. package/dist_ts/dns/manager.dns.d.ts +1 -0
  22. package/dist_ts/dns/manager.dns.js +49 -14
  23. package/dist_ts/dns/providers/cloudflare.provider.js +19 -4
  24. package/dist_ts/dns/txt-record-presentation.d.ts +20 -0
  25. package/dist_ts/dns/txt-record-presentation.js +131 -0
  26. package/dist_ts/email/classes.accepted-email-spool.d.ts +32 -3
  27. package/dist_ts/email/classes.accepted-email-spool.js +174 -13
  28. package/dist_ts/email/classes.email-domain.manager.d.ts +14 -1
  29. package/dist_ts/email/classes.email-domain.manager.js +151 -8
  30. package/dist_ts/email/classes.email-settings.manager.js +2 -1
  31. package/dist_ts/email/classes.mail-dns-sync.d.ts +31 -5
  32. package/dist_ts/email/classes.mail-dns-sync.js +508 -117
  33. package/dist_ts/email/classes.smartmta-blob-storage-manager.d.ts +27 -0
  34. package/dist_ts/email/classes.smartmta-blob-storage-manager.js +110 -0
  35. package/dist_ts/email/classes.smartmta-storage-manager.d.ts +6 -8
  36. package/dist_ts/email/classes.smartmta-storage-manager.js +63 -86
  37. package/dist_ts/email/classes.workapp-mail-manager.d.ts +1 -0
  38. package/dist_ts/email/classes.workapp-mail-manager.js +31 -4
  39. package/dist_ts/email/index.d.ts +4 -0
  40. package/dist_ts/email/index.js +5 -1
  41. package/dist_ts/email/mail-dns-txt-policy.d.ts +18 -0
  42. package/dist_ts/email/mail-dns-txt-policy.js +466 -0
  43. package/dist_ts/email/reconcile-mail-dns-singleton.d.ts +47 -0
  44. package/dist_ts/email/reconcile-mail-dns-singleton.js +159 -0
  45. package/dist_ts/email/smartmta-storage-policy.d.ts +9 -0
  46. package/dist_ts/email/smartmta-storage-policy.js +58 -0
  47. package/dist_ts/opsserver/classes.opsserver.d.ts +8 -0
  48. package/dist_ts/opsserver/classes.opsserver.js +34 -1
  49. package/dist_ts/opsserver/handlers/certificate.handler.js +25 -4
  50. package/dist_ts/opsserver/handlers/config.handler.js +9 -5
  51. package/dist_ts/opsserver/handlers/dns-provider.handler.js +19 -2
  52. package/dist_ts/opsserver/handlers/dns-record.handler.js +25 -4
  53. package/dist_ts/opsserver/handlers/domain.handler.js +37 -3
  54. package/dist_ts/opsserver/handlers/email-domain.handler.js +5 -7
  55. package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +1 -0
  56. package/dist_ts/opsserver/handlers/email-ops.handler.js +26 -7
  57. package/dist_ts/opsserver/handlers/index.d.ts +1 -0
  58. package/dist_ts/opsserver/handlers/index.js +2 -1
  59. package/dist_ts/opsserver/handlers/logs.handler.d.ts +1 -1
  60. package/dist_ts/opsserver/handlers/logs.handler.js +3 -3
  61. package/dist_ts/opsserver/handlers/realtime.handler.d.ts +37 -0
  62. package/dist_ts/opsserver/handlers/realtime.handler.js +154 -0
  63. package/dist_ts/opsserver/handlers/remoteingress.handler.js +22 -1
  64. package/dist_ts/opsserver/handlers/route-management.handler.js +47 -5
  65. package/dist_ts/opsserver/handlers/stats.handler.js +3 -5
  66. package/dist_ts/paths.d.ts +0 -11
  67. package/dist_ts/paths.js +1 -22
  68. package/dist_ts/plugins.d.ts +2 -35
  69. package/dist_ts/plugins.js +3 -63
  70. package/dist_ts_interfaces/data/email-domain.d.ts +14 -0
  71. package/dist_ts_interfaces/requests/config.d.ts +1 -1
  72. package/dist_ts_interfaces/requests/email-ops.d.ts +34 -1
  73. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  74. package/dist_ts_interfaces/requests/index.js +2 -1
  75. package/dist_ts_interfaces/requests/realtime.d.ts +38 -0
  76. package/dist_ts_interfaces/requests/realtime.js +11 -0
  77. package/dist_ts_migrations/index.d.ts +8 -0
  78. package/dist_ts_migrations/index.js +489 -12
  79. package/dist_ts_web/00_commitinfo_data.js +1 -1
  80. package/dist_ts_web/appstate/certificates.js +9 -2
  81. package/dist_ts_web/appstate/domains.js +16 -5
  82. package/dist_ts_web/appstate/email-domains.d.ts +3 -0
  83. package/dist_ts_web/appstate/email-domains.js +47 -14
  84. package/dist_ts_web/appstate/email-ops.d.ts +1 -0
  85. package/dist_ts_web/appstate/email-ops.js +20 -2
  86. package/dist_ts_web/appstate/realtime.d.ts +27 -0
  87. package/dist_ts_web/appstate/realtime.js +71 -0
  88. package/dist_ts_web/appstate/remoteingress.js +9 -3
  89. package/dist_ts_web/appstate/routes.js +16 -5
  90. package/dist_ts_web/appstate/runtime.js +135 -7
  91. package/dist_ts_web/appstate.d.ts +1 -0
  92. package/dist_ts_web/appstate.js +2 -1
  93. package/dist_ts_web/elements/email/email-domain-creation-progress.d.ts +33 -0
  94. package/dist_ts_web/elements/email/email-domain-creation-progress.js +257 -0
  95. package/dist_ts_web/elements/email/ops-view-email-domains.js +233 -68
  96. package/dist_ts_web/elements/email/ops-view-emails.d.ts +4 -2
  97. package/dist_ts_web/elements/email/ops-view-emails.js +25 -13
  98. package/dist_ts_web/elements/overview/ops-view-config.js +5 -5
  99. package/package.json +23 -19
  100. package/ts/00_commitinfo_data.ts +1 -1
  101. package/ts/classes.dcrouter.ts +136 -44
  102. package/ts/db/classes.cache.cleaner.ts +18 -3
  103. package/ts/db/classes.dcrouter-db.ts +0 -2
  104. package/ts/db/documents/classes.cached.email.ts +78 -4
  105. package/ts/db/documents/classes.dns-record.doc.ts +1 -0
  106. package/ts/db/documents/classes.smartmta-storage.doc.ts +25 -0
  107. package/ts/db/documents/index.ts +1 -0
  108. package/ts/dns/classes.dns-server-runtime.ts +9 -45
  109. package/ts/dns/index.ts +1 -0
  110. package/ts/dns/manager.dns.ts +50 -16
  111. package/ts/dns/providers/cloudflare.provider.ts +21 -3
  112. package/ts/dns/txt-record-presentation.ts +134 -0
  113. package/ts/email/classes.accepted-email-spool.ts +220 -14
  114. package/ts/email/classes.email-domain.manager.ts +201 -7
  115. package/ts/email/classes.email-settings.manager.ts +1 -0
  116. package/ts/email/classes.mail-dns-sync.ts +588 -122
  117. package/ts/email/classes.smartmta-blob-storage-manager.ts +139 -0
  118. package/ts/email/classes.smartmta-storage-manager.ts +86 -83
  119. package/ts/email/classes.workapp-mail-manager.ts +47 -4
  120. package/ts/email/index.ts +4 -0
  121. package/ts/email/mail-dns-txt-policy.ts +505 -0
  122. package/ts/email/reconcile-mail-dns-singleton.ts +224 -0
  123. package/ts/email/smartmta-storage-policy.ts +73 -0
  124. package/ts/opsserver/classes.opsserver.ts +42 -0
  125. package/ts/opsserver/handlers/certificate.handler.ts +24 -3
  126. package/ts/opsserver/handlers/config.handler.ts +9 -5
  127. package/ts/opsserver/handlers/dns-provider.handler.ts +18 -1
  128. package/ts/opsserver/handlers/dns-record.handler.ts +24 -3
  129. package/ts/opsserver/handlers/domain.handler.ts +36 -2
  130. package/ts/opsserver/handlers/email-domain.handler.ts +4 -5
  131. package/ts/opsserver/handlers/email-ops.handler.ts +29 -6
  132. package/ts/opsserver/handlers/index.ts +1 -0
  133. package/ts/opsserver/handlers/logs.handler.ts +2 -2
  134. package/ts/opsserver/handlers/realtime.handler.ts +222 -0
  135. package/ts/opsserver/handlers/remoteingress.handler.ts +21 -0
  136. package/ts/opsserver/handlers/route-management.handler.ts +46 -4
  137. package/ts/opsserver/handlers/stats.handler.ts +2 -4
  138. package/ts/paths.ts +0 -25
  139. package/ts/plugins.ts +2 -69
  140. package/ts_web/00_commitinfo_data.ts +1 -1
  141. package/ts_web/appstate/certificates.ts +6 -1
  142. package/ts_web/appstate/domains.ts +11 -4
  143. package/ts_web/appstate/email-domains.ts +68 -14
  144. package/ts_web/appstate/email-ops.ts +20 -1
  145. package/ts_web/appstate/realtime.ts +109 -0
  146. package/ts_web/appstate/remoteingress.ts +6 -2
  147. package/ts_web/appstate/routes.ts +11 -4
  148. package/ts_web/appstate/runtime.ts +176 -7
  149. package/ts_web/appstate.ts +1 -0
  150. package/ts_web/elements/email/email-domain-creation-progress.ts +340 -0
  151. package/ts_web/elements/email/ops-view-email-domains.ts +273 -74
  152. package/ts_web/elements/email/ops-view-emails.ts +23 -16
  153. 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
  // Domains State (DNS providers + domains + records)
@@ -37,6 +38,7 @@ export const fetchDomainsAndProvidersAction = domainsStatePart.createAction(
37
38
  const context = getActionContext();
38
39
  const currentState = statePartArg.getState()!;
39
40
  if (!context.identity) return currentState;
41
+ const ticket = captureRealtimeTicket('dns');
40
42
 
41
43
  try {
42
44
  const providersRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -50,9 +52,10 @@ export const fetchDomainsAndProvidersAction = domainsStatePart.createAction(
50
52
  providersRequest.fire({ identity: context.identity }),
51
53
  domainsRequest.fire({ identity: context.identity }),
52
54
  ]);
55
+ if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
53
56
 
54
57
  return {
55
- ...currentState,
58
+ ...statePartArg.getState()!,
56
59
  providers: providersResponse.providers,
57
60
  domains: domainsResponse.domains,
58
61
  isLoading: false,
@@ -60,8 +63,9 @@ export const fetchDomainsAndProvidersAction = domainsStatePart.createAction(
60
63
  lastUpdated: Date.now(),
61
64
  };
62
65
  } catch (error: unknown) {
66
+ if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
63
67
  return {
64
- ...currentState,
68
+ ...statePartArg.getState()!,
65
69
  isLoading: false,
66
70
  error: error instanceof Error ? error.message : 'Failed to fetch domains/providers',
67
71
  };
@@ -74,6 +78,7 @@ export const fetchDnsRecordsForDomainAction = domainsStatePart.createAction<{ do
74
78
  const context = getActionContext();
75
79
  const currentState = statePartArg.getState()!;
76
80
  if (!context.identity) return currentState;
81
+ const ticket = captureRealtimeTicket('dns');
77
82
 
78
83
  try {
79
84
  const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -83,15 +88,17 @@ export const fetchDnsRecordsForDomainAction = domainsStatePart.createAction<{ do
83
88
  identity: context.identity,
84
89
  domainId: dataArg.domainId,
85
90
  });
91
+ if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
86
92
  return {
87
- ...currentState,
93
+ ...statePartArg.getState()!,
88
94
  records: response.records,
89
95
  selectedDomainId: dataArg.domainId,
90
96
  error: null,
91
97
  };
92
98
  } catch (error: unknown) {
99
+ if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
93
100
  return {
94
- ...currentState,
101
+ ...statePartArg.getState()!,
95
102
  error: error instanceof Error ? error.message : 'Failed to fetch DNS records',
96
103
  };
97
104
  }
@@ -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
- ...currentState,
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
- ...currentState,
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(refreshedState, operation, 'Email domain created');
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
- return applyOperationState(currentState, operation, 'Failed to create email domain');
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, actionContext) => {
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
- if (!response.success) {
277
- return applyOperationState(currentState, operation, 'Failed to validate email domain DNS');
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
- const refreshedState = await actionContext!.dispatch(fetchEmailDomainsAction, null);
280
- return applyOperationState(refreshedState, operation, 'Email domain DNS validated');
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(currentState, operation, 'Failed to validate email domain DNS');
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
- ...currentState,
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
- ...currentState,
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
- ...currentState,
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
- ...currentState,
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
- ...currentState,
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
- ...currentState,
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
- ...currentState,
100
+ ...statePartArg.getState()!,
94
101
  isLoading: false,
95
102
  error: error instanceof Error ? error.message : 'Failed to fetch HTTP redirects',
96
103
  };