querysub 0.589.0 → 0.590.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/package.json +2 -2
- package/src/-a-archives/archiveCache.ts +9 -7
- package/src/-a-archives/archiveCache2.ts +153 -0
- package/src/-a-archives/archives.ts +4 -2
- package/src/-a-archives/archives2.ts +25 -9
- package/src/-a-archives/archivesJSONT.ts +20 -5
- package/src/-g-core-values/NodeCapabilities.ts +13 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +57 -19
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +17 -13
- package/src/0-path-value-core/pathValueArchives.ts +9 -9
- package/src/2-proxy/PathValueProxyWatcher.ts +6 -6
- package/src/4-dom/qreact.tsx +4 -4
- package/src/4-querysub/Querysub.ts +8 -1
- package/src/deployManager/components/RouteConfigView.tsx +10 -1
- package/src/deployManager/components/ServiceMeasureBars.tsx +169 -0
- package/src/deployManager/components/ServicesListPage.tsx +52 -2
- package/src/deployManager/components/StoragePage.tsx +43 -22
- package/src/diagnostics/logs/errorNotifications2/errorNotifications.ts +16 -47
- package/src/diagnostics/managementPages.tsx +3 -0
- package/src/diagnostics/misc-pages/SnapshotViewer.tsx +32 -9
- package/src/diagnostics/watchdog.ts +72 -1
- package/src/library-components/ATag.tsx +1 -0
- package/src/library-components/SyncedController.ts +8 -0
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { cacheLimited, lazy } from "socket-function/src/caching";
|
|
2
|
-
import { nestArchives } from "../../../-a-archives/archives";
|
|
3
|
-
import { getArchivesBackblaze } from "../../../-a-archives/archivesBackBlaze";
|
|
4
2
|
import { archiveJSONT } from "../../../-a-archives/archivesJSONT";
|
|
5
3
|
import { Querysub } from "../../../4-querysub/Querysub";
|
|
6
4
|
import { getDomain, isPublic } from "../../../config";
|
|
@@ -23,6 +21,7 @@ import { showingManagementURL, managementPageURL } from "../../managementPages";
|
|
|
23
21
|
import { atomic } from "../../../2-proxy/PathValueProxyWatcher";
|
|
24
22
|
import { magenta } from "socket-function/src/formatting/logColors";
|
|
25
23
|
import { TicketServiceBase } from "../errorTickets/tickets";
|
|
24
|
+
import { getArchives2 } from "../../../-a-archives/archives2";
|
|
26
25
|
|
|
27
26
|
|
|
28
27
|
export type SuppressionEntry = {
|
|
@@ -36,8 +35,7 @@ export type SuppressionEntry = {
|
|
|
36
35
|
createdTime: number;
|
|
37
36
|
lastUpdatedTime: number;
|
|
38
37
|
};
|
|
39
|
-
|
|
40
|
-
let suppressionCache: SuppressionEntry[] = [];
|
|
38
|
+
const suppression = archiveJSONT<SuppressionEntry>(() => getArchives2("logs/error-suppression/"));
|
|
41
39
|
|
|
42
40
|
// In-memory Discord notification throttling
|
|
43
41
|
const timeInHour = 60 * 60 * 1000;
|
|
@@ -50,17 +48,8 @@ let pendingDiscordNotifications = new Map<string, {
|
|
|
50
48
|
errorCount: number;
|
|
51
49
|
isNew: boolean;
|
|
52
50
|
}>();
|
|
53
|
-
let ensureWatching = lazy(async () => {
|
|
54
|
-
await runInfinitePollCallAtStart(timeInMinute * 5, updateNow);
|
|
55
|
-
});
|
|
56
|
-
async function updateNow() {
|
|
57
|
-
suppressionCache = await suppression.values();
|
|
58
|
-
// Can't wait, because that will cause a cyclic loop
|
|
59
|
-
void reapplySuppressions();
|
|
60
|
-
}
|
|
61
51
|
async function getSuppressionEntries(): Promise<SuppressionEntry[]> {
|
|
62
|
-
await
|
|
63
|
-
return suppressionCache;
|
|
52
|
+
return await suppression.values();
|
|
64
53
|
}
|
|
65
54
|
|
|
66
55
|
let getMatcher = cacheLimited(10000, (text: string) => createMatchesPattern(Buffer.from(text), false));
|
|
@@ -157,12 +146,6 @@ async function reapplySuppressions() {
|
|
|
157
146
|
}
|
|
158
147
|
}
|
|
159
148
|
|
|
160
|
-
async function onSuppressionsChanged() {
|
|
161
|
-
// NOTE: Update Now also reapplies suppression internally. after it finishes the update.
|
|
162
|
-
void updateNow();
|
|
163
|
-
await reapplySuppressions();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
149
|
type PatternResponse = {
|
|
167
150
|
pattern: string;
|
|
168
151
|
};
|
|
@@ -246,12 +229,13 @@ const trySendDiscordNotificationsBase = batchFunction({ delay: 1000 * 3 }, async
|
|
|
246
229
|
return;
|
|
247
230
|
}
|
|
248
231
|
|
|
232
|
+
const suppressionEntries = await getSuppressionEntries();
|
|
249
233
|
|
|
250
234
|
const newSuppressions: Array<{ id: string; count: number; pattern: string }> = [];
|
|
251
235
|
const existingSuppressions: Array<{ id: string; count: number; pattern: string }> = [];
|
|
252
236
|
|
|
253
237
|
for (const [suppressionId, data] of pendingDiscordNotifications.entries()) {
|
|
254
|
-
const suppressionEntry =
|
|
238
|
+
const suppressionEntry = suppressionEntries.find(e => e.id === suppressionId);
|
|
255
239
|
if (!suppressionEntry) continue;
|
|
256
240
|
|
|
257
241
|
if (data.isNew) {
|
|
@@ -309,6 +293,9 @@ export async function exposeErrorWatchService() {
|
|
|
309
293
|
SocketFunction.expose(ErrorNotificationServiceBase);
|
|
310
294
|
SocketFunction.expose(TicketServiceBase);
|
|
311
295
|
|
|
296
|
+
// Other nodes edit suppressions too, so periodically re-check the unmatched errors against the current entries.
|
|
297
|
+
void runInfinitePollCallAtStart(timeInMinute * 5, reapplySuppressions);
|
|
298
|
+
|
|
312
299
|
let errorLogs = await getErrorLogs();
|
|
313
300
|
for await (let error of watchAllValues(errorLogs)) {
|
|
314
301
|
try {
|
|
@@ -348,8 +335,7 @@ export async function exposeErrorWatchService() {
|
|
|
348
335
|
lastUpdatedTime: Date.now(),
|
|
349
336
|
};
|
|
350
337
|
|
|
351
|
-
|
|
352
|
-
void suppression.set(newEntry.id, newEntry);
|
|
338
|
+
await suppression.set(newEntry.id, newEntry);
|
|
353
339
|
applySuppression(error, newEntry);
|
|
354
340
|
queueDiscordNotification(newEntry.id, 1, true);
|
|
355
341
|
|
|
@@ -426,39 +412,22 @@ class ErrorNotificationService {
|
|
|
426
412
|
}
|
|
427
413
|
|
|
428
414
|
public async setSuppressionEntry(entry: SuppressionEntry) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (prevEntry !== -1) {
|
|
432
|
-
suppressionCache[prevEntry] = entry;
|
|
433
|
-
} else {
|
|
434
|
-
suppressionCache.push(entry);
|
|
435
|
-
}
|
|
436
|
-
await onSuppressionsChanged();
|
|
437
|
-
void suppressionPromise.finally(() => {
|
|
438
|
-
void onSuppressionsChanged();
|
|
439
|
-
});
|
|
415
|
+
await suppression.set(entry.id, entry);
|
|
416
|
+
await reapplySuppressions();
|
|
440
417
|
}
|
|
441
418
|
|
|
442
419
|
public async deleteSuppressionEntry(id: string) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
if (prevEntry !== -1) {
|
|
446
|
-
suppressionCache.splice(prevEntry, 1);
|
|
447
|
-
}
|
|
448
|
-
await onSuppressionsChanged();
|
|
449
|
-
void suppressionPromise.finally(() => {
|
|
450
|
-
void onSuppressionsChanged();
|
|
451
|
-
});
|
|
420
|
+
await suppression.delete(id);
|
|
421
|
+
await reapplySuppressions();
|
|
452
422
|
}
|
|
453
423
|
|
|
454
424
|
public async updateSuppressionNotes(id: string, notes: string | undefined) {
|
|
455
|
-
let
|
|
425
|
+
let entries = await getSuppressionEntries();
|
|
426
|
+
let entry = entries.find(e => e.id === id);
|
|
456
427
|
if (!entry) {
|
|
457
428
|
throw new Error(`Suppression entry ${id} not found`);
|
|
458
429
|
}
|
|
459
|
-
|
|
460
|
-
suppressionCache[suppressionCache.findIndex(e => e.id === id)] = updatedEntry;
|
|
461
|
-
void suppression.set(id, updatedEntry);
|
|
430
|
+
await suppression.set(id, { ...entry, notes, lastUpdatedTime: Date.now() });
|
|
462
431
|
}
|
|
463
432
|
}
|
|
464
433
|
|
|
@@ -384,6 +384,9 @@ class ManagementRoot extends qreact.Component {
|
|
|
384
384
|
.ManagementRoot-page a {
|
|
385
385
|
color: hsl(210, 75%, 50%);
|
|
386
386
|
}
|
|
387
|
+
.ATag-current {
|
|
388
|
+
color: hsl(270, 75%, 50%)!important;
|
|
389
|
+
}
|
|
387
390
|
`}
|
|
388
391
|
</style>
|
|
389
392
|
<div class={css.fillWidth.hbox(20, 4).wrap.hsl(245, 25, 80).pad2(20, 4).pointerEvents("all")}>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module.allowclient = true;
|
|
2
2
|
|
|
3
|
-
import { ArchiveSnapshotOverview, ArchiveSnapshotRead, downloadSnapshot,
|
|
3
|
+
import { ArchiveSnapshotList, ArchiveSnapshotOverview, ArchiveSnapshotRead, downloadSnapshot, getLiveSnapshotList, getSavedSnapshotList, getSnapshot, loadSnapshot, saveSnapshot, uploadSnapshot } from "../../0-path-value-core/archiveLocks/archiveSnapshots";
|
|
4
4
|
import { qreact } from "../../4-dom/qreact";
|
|
5
5
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
6
6
|
import { getBrowserUrlNode } from "../../-f-node-discovery/NodeDiscovery";
|
|
@@ -9,7 +9,8 @@ import { css } from "typesafecss";
|
|
|
9
9
|
import { Querysub, t } from "../../4-querysub/Querysub";
|
|
10
10
|
import { ATag, Anchor } from "../../library-components/ATag";
|
|
11
11
|
import { URLParam } from "../../library-components/URLParam";
|
|
12
|
-
import { formatDateTime, formatNumber } from "socket-function/src/formatting/format";
|
|
12
|
+
import { formatDateTime, formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
13
|
+
import { sort } from "socket-function/src/misc";
|
|
13
14
|
import { Button } from "../../library-components/Button";
|
|
14
15
|
import { showModal } from "../../5-diagnostics/Modal";
|
|
15
16
|
import { FullscreenModal } from "../../5-diagnostics/FullscreenModal";
|
|
@@ -26,6 +27,7 @@ import { getSyncedController } from "../../library-components/SyncedController";
|
|
|
26
27
|
|
|
27
28
|
const selectedSnapshot = new URLParam("selectedSnapshot", "");
|
|
28
29
|
export class SnapshotViewer extends qreact.Component {
|
|
30
|
+
static renderInProgress = true;
|
|
29
31
|
state = t.state({
|
|
30
32
|
// file => true
|
|
31
33
|
expanded: t.lookup(t.boolean),
|
|
@@ -35,12 +37,23 @@ export class SnapshotViewer extends qreact.Component {
|
|
|
35
37
|
});
|
|
36
38
|
render() {
|
|
37
39
|
let controller = SnapshotViewerSynced(getBrowserUrlNode());
|
|
38
|
-
let
|
|
39
|
-
|
|
40
|
+
let savedList = controller.getSavedSnapshotList();
|
|
41
|
+
let liveList = controller.getLiveSnapshotList();
|
|
42
|
+
let snapshotList: ArchiveSnapshotOverview[] = [];
|
|
43
|
+
if (liveList) snapshotList.push(...liveList.snapshots);
|
|
44
|
+
if (savedList) snapshotList.push(...savedList.snapshots);
|
|
45
|
+
sort(snapshotList, x => -x.time);
|
|
46
|
+
let loadStatus = (
|
|
47
|
+
<div class={css.vbox(2)}>
|
|
48
|
+
<div>{liveList ? `Live snapshot list loaded in ${formatTime(liveList.loadTime)}` : "Loading live snapshot list..."}</div>
|
|
49
|
+
<div>{savedList ? `Snapshot list loaded in ${formatTime(savedList.loadTime)}` : "Loading snapshot list..."}</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
40
52
|
let file = selectedSnapshot.value;
|
|
41
53
|
if (!file || typeof file !== "string") {
|
|
42
54
|
return (
|
|
43
55
|
<div class={css.pad2(10).vbox(10)}>
|
|
56
|
+
{loadStatus}
|
|
44
57
|
<h1>Snapshots are taken before every change. "zombie" means the file exists, but it has no confirm (so it will be ignored).</h1>
|
|
45
58
|
<div class={css.hbox(10)}>
|
|
46
59
|
<Button onClick={() => triggerSnapshotUpload(this)}>
|
|
@@ -90,7 +103,11 @@ export class SnapshotViewer extends qreact.Component {
|
|
|
90
103
|
|
|
91
104
|
const entry = snapshotList.find(x => x.file === file);
|
|
92
105
|
let snapshotInfo = controller.getSnapshot(file);
|
|
93
|
-
if (!entry)
|
|
106
|
+
if (!entry) {
|
|
107
|
+
let stillLoading = file === "live" ? !liveList : !savedList;
|
|
108
|
+
if (stillLoading) return <div>Loading...</div>;
|
|
109
|
+
return <div>Snapshot not found, {file}</div>;
|
|
110
|
+
}
|
|
94
111
|
return (
|
|
95
112
|
<div class={css.pad2(10).vbox(10)}>
|
|
96
113
|
<div class={css.hbox(10)}>
|
|
@@ -237,8 +254,12 @@ async function seriousConfirm(config: {
|
|
|
237
254
|
}
|
|
238
255
|
|
|
239
256
|
class SnapshotViewerControllerBase {
|
|
240
|
-
public async
|
|
241
|
-
return await
|
|
257
|
+
public async getSavedSnapshotList(): Promise<ArchiveSnapshotList> {
|
|
258
|
+
return await getSavedSnapshotList();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
public async getLiveSnapshotList(): Promise<ArchiveSnapshotList> {
|
|
262
|
+
return await getLiveSnapshotList();
|
|
242
263
|
}
|
|
243
264
|
|
|
244
265
|
public async loadSnapshot(config: {
|
|
@@ -271,7 +292,8 @@ export const SnapshotViewerController = SocketFunction.register(
|
|
|
271
292
|
"SnapshotViewerController-a78ae3cf-5024-49da-b71a-f064a5d4b5f4",
|
|
272
293
|
new SnapshotViewerControllerBase(),
|
|
273
294
|
() => ({
|
|
274
|
-
|
|
295
|
+
getSavedSnapshotList: {},
|
|
296
|
+
getLiveSnapshotList: {},
|
|
275
297
|
loadSnapshot: {},
|
|
276
298
|
getSnapshot: {},
|
|
277
299
|
saveLiveSnapshot: {},
|
|
@@ -288,7 +310,8 @@ export const SnapshotViewerController = SocketFunction.register(
|
|
|
288
310
|
);
|
|
289
311
|
const SnapshotViewerSynced = getSyncedController(SnapshotViewerController, {
|
|
290
312
|
reads: {
|
|
291
|
-
|
|
313
|
+
getSavedSnapshotList: ["snapshots"],
|
|
314
|
+
getLiveSnapshotList: ["snapshots"],
|
|
292
315
|
getSnapshot: ["snapshots"],
|
|
293
316
|
},
|
|
294
317
|
writes: {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
2
2
|
import { FormattedMeasureTable, MeasureProfile, logMeasureTable, startMeasure } from "socket-function/src/profiling/measure";
|
|
3
|
+
import { StatsValue, addToStats, createStatsValue } from "socket-function/src/profiling/stats";
|
|
3
4
|
import { logErrors } from "../errors";
|
|
4
|
-
import { isNode, sort } from "socket-function/src/misc";
|
|
5
|
+
import { isNode, sort, timeInHour } from "socket-function/src/misc";
|
|
5
6
|
import debugbreak from "debugbreak";
|
|
6
7
|
import { registerPeriodic } from "./periodic";
|
|
7
8
|
import { getOwnMachineId } from "sliftutils/misc/https/certs";
|
|
@@ -15,6 +16,75 @@ import { isPublic } from "../config";
|
|
|
15
16
|
|
|
16
17
|
let lastProfile: MeasureProfile | undefined;
|
|
17
18
|
|
|
19
|
+
const MEASURE_CHUNK_MAX_ENTRIES = 100;
|
|
20
|
+
// Matches the deeper of the two watchdog console tables, so the stored breakdown groups the same way the logs do.
|
|
21
|
+
const MEASURE_CHUNK_MERGE_DEPTH = 2;
|
|
22
|
+
const MEASURE_CHUNK_RETENTION = timeInHour;
|
|
23
|
+
|
|
24
|
+
export type MeasureChunkEntry = {
|
|
25
|
+
name: string;
|
|
26
|
+
ownTime: StatsValue;
|
|
27
|
+
totalTime: StatsValue;
|
|
28
|
+
stillOpenCount: number;
|
|
29
|
+
};
|
|
30
|
+
export type MeasureChunk = {
|
|
31
|
+
startTime: number;
|
|
32
|
+
endTime: number;
|
|
33
|
+
/** Sum over ALL groupings' own time, including groupings dropped past MEASURE_CHUNK_MAX_ENTRIES. */
|
|
34
|
+
profiledTime: number;
|
|
35
|
+
entries: MeasureChunkEntry[];
|
|
36
|
+
};
|
|
37
|
+
export type MeasureSummary = {
|
|
38
|
+
startTime: number;
|
|
39
|
+
endTime: number;
|
|
40
|
+
profiledTime: number;
|
|
41
|
+
entries: {
|
|
42
|
+
name: string;
|
|
43
|
+
ownTime: number;
|
|
44
|
+
}[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
let measureChunks: MeasureChunk[] = [];
|
|
48
|
+
|
|
49
|
+
function storeMeasureChunk(profile: MeasureProfile) {
|
|
50
|
+
let merged = new Map<string, MeasureChunkEntry>();
|
|
51
|
+
for (let entry of Object.values(profile.entries)) {
|
|
52
|
+
let key = entry.name.split("|").slice(0, MEASURE_CHUNK_MERGE_DEPTH).join("|");
|
|
53
|
+
let existing = merged.get(key);
|
|
54
|
+
if (!existing) {
|
|
55
|
+
existing = { name: key, ownTime: createStatsValue(), totalTime: createStatsValue(), stillOpenCount: 0 };
|
|
56
|
+
merged.set(key, existing);
|
|
57
|
+
}
|
|
58
|
+
addToStats(existing.ownTime, entry.ownTime);
|
|
59
|
+
addToStats(existing.totalTime, entry.totalTime);
|
|
60
|
+
existing.stillOpenCount += entry.stillOpenCount;
|
|
61
|
+
}
|
|
62
|
+
let entries = sort(Array.from(merged.values()), x => -x.ownTime.sum);
|
|
63
|
+
let profiledTime = entries.reduce((total, entry) => total + entry.ownTime.sum, 0);
|
|
64
|
+
measureChunks.push({
|
|
65
|
+
startTime: profile.startTime,
|
|
66
|
+
endTime: profile.endTime,
|
|
67
|
+
profiledTime,
|
|
68
|
+
entries: entries.slice(0, MEASURE_CHUNK_MAX_ENTRIES),
|
|
69
|
+
});
|
|
70
|
+
let cutoff = Date.now() - MEASURE_CHUNK_RETENTION;
|
|
71
|
+
measureChunks = measureChunks.filter(x => x.endTime >= cutoff);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getLastMeasureSummary(): MeasureSummary | undefined {
|
|
75
|
+
let chunk = measureChunks[measureChunks.length - 1];
|
|
76
|
+
if (!chunk) return undefined;
|
|
77
|
+
return {
|
|
78
|
+
startTime: chunk.startTime,
|
|
79
|
+
endTime: chunk.endTime,
|
|
80
|
+
profiledTime: chunk.profiledTime,
|
|
81
|
+
entries: chunk.entries.map(x => ({ name: x.name, ownTime: x.ownTime.sum })),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function getMeasureBreakdown(range: { startTime: number; endTime: number }): MeasureChunk | undefined {
|
|
85
|
+
return measureChunks.find(x => x.startTime === range.startTime && x.endTime === range.endTime);
|
|
86
|
+
}
|
|
87
|
+
|
|
18
88
|
addStatPeriodic({
|
|
19
89
|
title: "Performance|% Profiled",
|
|
20
90
|
getValue: () => {
|
|
@@ -94,6 +164,7 @@ function logProfileMeasuresTimingsNow() {
|
|
|
94
164
|
process.stdout.write(`\n${debuggerValue}\n`);
|
|
95
165
|
}
|
|
96
166
|
|
|
167
|
+
storeMeasureChunk(profile);
|
|
97
168
|
lastProfile = profile;
|
|
98
169
|
// if (isNode()) {
|
|
99
170
|
// if (SocketFunction.mountedNodeId) {
|
|
@@ -275,6 +275,7 @@ export function getSyncedController<T extends {
|
|
|
275
275
|
let promise = atomic(obj.promise);
|
|
276
276
|
|
|
277
277
|
// NOTE: If we are invalidated when the promise is running, nothing happens (as we don't watch invalidated if we are running with a promise). BUT, if the promise isn't running, we will run again, and start running again. In this way we don't queue up a lot if we invalidate a lot, but we do always run again after the invalidation to get the latest result!
|
|
278
|
+
let currentPromise: Promise<unknown> | undefined;
|
|
278
279
|
if (!promise && (!result || atomic(obj.invalidated))) {
|
|
279
280
|
obj.invalidated = false;
|
|
280
281
|
let timeStart = Date.now();
|
|
@@ -285,6 +286,7 @@ export function getSyncedController<T extends {
|
|
|
285
286
|
}
|
|
286
287
|
}
|
|
287
288
|
let promiseObjBase = new PromiseObj<unknown>();
|
|
289
|
+
currentPromise = promiseObjBase.promise;
|
|
288
290
|
let promiseObj: { promise: Promise<unknown> } = {
|
|
289
291
|
promise: promiseObjBase.promise,
|
|
290
292
|
};
|
|
@@ -348,6 +350,12 @@ export function getSyncedController<T extends {
|
|
|
348
350
|
return result.result;
|
|
349
351
|
}
|
|
350
352
|
}
|
|
353
|
+
currentPromise = currentPromise || promise?.promise;
|
|
354
|
+
if (currentPromise) {
|
|
355
|
+
Querysub.triggerOnPromiseFinish(currentPromise, {
|
|
356
|
+
waitReason: `Waiting for ${fncName} to finish`,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
351
359
|
return undefined;
|
|
352
360
|
}
|
|
353
361
|
call.promise = (...args: any[]) => {
|