@ak--47/dungeon-master 1.4.4 → 1.4.5
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/CHANGELOG.md +8 -0
- package/index.js +29 -6
- package/lib/core/context.js +58 -0
- package/lib/orchestrators/mixpanel-sender.js +27 -1
- package/lib/orchestrators/user-loop.js +10 -0
- package/package.json +1 -1
- package/types.d.ts +77 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@ak--47/dungeon-master`.
|
|
4
4
|
|
|
5
|
+
## 1.4.5 — 2026-05-06
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Progress callback.** Callers can pass `onProgress: (update) => void` on the dungeon config to receive throttled updates during generation, import, and pipeline step transitions. Update frequency is configurable via `progressInterval` (default 500ms). The callback is fault-tolerant — bad functions are caught and disabled after 3 failures, never breaking the job. Return value includes a `progress` summary with update count, error count, and disabled flag.
|
|
10
|
+
- **Mixpanel import progress.** When `onProgress` is set and a Mixpanel token is provided, import progress from `mixpanel-import`'s `progressCallback` is surfaced through the same `onProgress` interface as `{ phase: "import" }` updates.
|
|
11
|
+
- **Full TypeScript typings** for `ProgressUpdate` (discriminated union), `ProgressSummary`, `ProgressGeneration`, `ProgressImport`, and `ProgressStep`.
|
|
12
|
+
|
|
5
13
|
## 1.4.4 — 2026-05-06
|
|
6
14
|
|
|
7
15
|
The "GCS imports actually work now" release.
|
package/index.js
CHANGED
|
@@ -157,31 +157,49 @@ async function runDungeon(config) {
|
|
|
157
157
|
|
|
158
158
|
// Step 4: Generate ad spend data (if enabled)
|
|
159
159
|
if (validatedConfig.hasAdSpend) {
|
|
160
|
+
context.reportProgress({ phase: "step", step: "adspend", status: "start" });
|
|
161
|
+
const _t4 = Date.now();
|
|
160
162
|
await generateAdSpendData(context);
|
|
163
|
+
context.reportProgress({ phase: "step", step: "adspend", status: "complete", duration: Date.now() - _t4 });
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
if (context.config.verbose) logger.info('Starting user and event generation...');
|
|
164
167
|
// Step 5: Main user and event generation
|
|
168
|
+
context.reportProgress({ phase: "step", step: "users", status: "start" });
|
|
169
|
+
const _t5 = Date.now();
|
|
165
170
|
await userLoop(context);
|
|
171
|
+
context.reportProgress({ phase: "step", step: "users", status: "complete", duration: Date.now() - _t5 });
|
|
166
172
|
|
|
167
173
|
// Step 6: Generate group profiles (if configured)
|
|
168
174
|
if (validatedConfig.groupKeys && validatedConfig.groupKeys.length > 0) {
|
|
175
|
+
context.reportProgress({ phase: "step", step: "group-profiles", status: "start" });
|
|
176
|
+
const _t6 = Date.now();
|
|
169
177
|
await generateGroupProfiles(context);
|
|
178
|
+
context.reportProgress({ phase: "step", step: "group-profiles", status: "complete", duration: Date.now() - _t6 });
|
|
170
179
|
}
|
|
171
180
|
|
|
172
181
|
// Step 7: Generate group SCDs (if configured)
|
|
173
182
|
if (validatedConfig.scdProps && validatedConfig.groupKeys && validatedConfig.groupKeys.length > 0) {
|
|
183
|
+
context.reportProgress({ phase: "step", step: "group-scds", status: "start" });
|
|
184
|
+
const _t7 = Date.now();
|
|
174
185
|
await generateGroupSCDs(context);
|
|
186
|
+
context.reportProgress({ phase: "step", step: "group-scds", status: "complete", duration: Date.now() - _t7 });
|
|
175
187
|
}
|
|
176
188
|
|
|
177
189
|
// Step 8: Generate lookup tables (if configured)
|
|
178
190
|
if (validatedConfig.lookupTables && validatedConfig.lookupTables.length > 0) {
|
|
191
|
+
context.reportProgress({ phase: "step", step: "lookups", status: "start" });
|
|
192
|
+
const _t8 = Date.now();
|
|
179
193
|
await generateLookupTables(context);
|
|
194
|
+
context.reportProgress({ phase: "step", step: "lookups", status: "complete", duration: Date.now() - _t8 });
|
|
180
195
|
}
|
|
181
196
|
|
|
182
197
|
// Step 9: Generate mirror datasets (if configured)
|
|
183
198
|
if (validatedConfig.mirrorProps && Object.keys(validatedConfig.mirrorProps).length > 0) {
|
|
199
|
+
context.reportProgress({ phase: "step", step: "mirrors", status: "start" });
|
|
200
|
+
const _t9 = Date.now();
|
|
184
201
|
await makeMirror(context);
|
|
202
|
+
context.reportProgress({ phase: "step", step: "mirrors", status: "complete", duration: Date.now() - _t9 });
|
|
185
203
|
}
|
|
186
204
|
|
|
187
205
|
if (context.config.verbose) logger.info('Data generation completed successfully');
|
|
@@ -191,21 +209,23 @@ async function runDungeon(config) {
|
|
|
191
209
|
// Flush when writeToDisk is enabled OR batch mode activated (to capture tail data)
|
|
192
210
|
const shouldFlush = validatedConfig.writeToDisk || context.isBatchMode();
|
|
193
211
|
|
|
194
|
-
// Step 10: Flush
|
|
212
|
+
// Step 10-11: Flush to disk
|
|
195
213
|
if (shouldFlush) {
|
|
214
|
+
context.reportProgress({ phase: "step", step: "flush", status: "start" });
|
|
215
|
+
const _tFlush = Date.now();
|
|
196
216
|
await flushLookupTablesToDisk(storage, validatedConfig);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Step 11: Flush other storage containers to disk
|
|
200
|
-
if (shouldFlush) {
|
|
201
217
|
await flushStorageToDisk(storage, validatedConfig);
|
|
218
|
+
context.reportProgress({ phase: "step", step: "flush", status: "complete", duration: Date.now() - _tFlush });
|
|
202
219
|
}
|
|
203
220
|
|
|
204
221
|
// Step 12: Send to Mixpanel (if token provided)
|
|
205
222
|
// Now happens AFTER disk flush so batch files are available for import
|
|
206
223
|
let importResults;
|
|
207
224
|
if (validatedConfig.token) {
|
|
225
|
+
context.reportProgress({ phase: "step", step: "import", status: "start" });
|
|
226
|
+
const _t12 = Date.now();
|
|
208
227
|
importResults = await sendToMixpanel(context);
|
|
228
|
+
context.reportProgress({ phase: "step", step: "import", status: "complete", duration: Date.now() - _t12 });
|
|
209
229
|
}
|
|
210
230
|
|
|
211
231
|
// Step 13: Compile results
|
|
@@ -214,6 +234,8 @@ async function runDungeon(config) {
|
|
|
214
234
|
|
|
215
235
|
const extractedData = extractStorageData(storage);
|
|
216
236
|
|
|
237
|
+
const progressSummary = context.getProgressSummary();
|
|
238
|
+
|
|
217
239
|
return {
|
|
218
240
|
...extractedData,
|
|
219
241
|
importResults,
|
|
@@ -221,7 +243,8 @@ async function runDungeon(config) {
|
|
|
221
243
|
time: { start, end, delta, human },
|
|
222
244
|
operations: context.getOperations(),
|
|
223
245
|
eventCount: context.getStoredEventCount(),
|
|
224
|
-
userCount: context.getUserCount()
|
|
246
|
+
userCount: context.getUserCount(),
|
|
247
|
+
...(progressSummary.updates > 0 || progressSummary.errors > 0 ? { progress: progressSummary } : {})
|
|
225
248
|
};
|
|
226
249
|
|
|
227
250
|
} catch (error) {
|
package/lib/core/context.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
/** @typedef {import('../../types.js').Context} Context */
|
|
9
9
|
/** @typedef {import('../../types.js').RuntimeState} RuntimeState */
|
|
10
10
|
/** @typedef {import('../../types.js').Defaults} Defaults */
|
|
11
|
+
/** @typedef {import('../../types.js').ProgressUpdate} ProgressUpdate */
|
|
12
|
+
/** @typedef {import('../../types.js').ProgressSummary} ProgressSummary */
|
|
11
13
|
|
|
12
14
|
import dayjs from "dayjs";
|
|
13
15
|
import { campaigns, devices, locations } from '../templates/defaults.js';
|
|
@@ -78,6 +80,58 @@ function createRuntimeState() {
|
|
|
78
80
|
};
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
/**
|
|
84
|
+
* @param {Dungeon} config
|
|
85
|
+
* @returns {{ reportProgress: (update: ProgressUpdate) => void, getProgressSummary: () => ProgressSummary }}
|
|
86
|
+
*/
|
|
87
|
+
function createProgressReporter(config) {
|
|
88
|
+
const interval = config.progressInterval ?? 500;
|
|
89
|
+
const verbose = config.verbose || false;
|
|
90
|
+
let callback = config.onProgress ?? null;
|
|
91
|
+
let lastFireTime = 0;
|
|
92
|
+
let errorCount = 0;
|
|
93
|
+
let totalUpdates = 0;
|
|
94
|
+
let disabled = false;
|
|
95
|
+
|
|
96
|
+
if (callback !== null && typeof callback !== 'function') {
|
|
97
|
+
if (verbose) console.warn(`[dungeon-master] onProgress is not a function (got ${typeof callback}), ignoring`);
|
|
98
|
+
callback = null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function reportProgress(/** @type {ProgressUpdate} */ update) {
|
|
102
|
+
if (!callback || disabled) return;
|
|
103
|
+
|
|
104
|
+
const isThrottled = update.phase === 'generation' || update.phase === 'import';
|
|
105
|
+
if (isThrottled) {
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
if (now - lastFireTime < interval) return;
|
|
108
|
+
lastFireTime = now;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const result = /** @type {any} */ (callback(update));
|
|
113
|
+
totalUpdates++;
|
|
114
|
+
if (result && typeof result.then === 'function') {
|
|
115
|
+
result.then(undefined, (/** @type {any} */ err) => {
|
|
116
|
+
errorCount++;
|
|
117
|
+
if (verbose) console.warn(`[dungeon-master] onProgress async error (${errorCount}/3): ${err?.message || err}`);
|
|
118
|
+
if (errorCount >= 3) disabled = true;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
errorCount++;
|
|
123
|
+
if (verbose) console.warn(`[dungeon-master] onProgress error (${errorCount}/3): ${err?.message || err}`);
|
|
124
|
+
if (errorCount >= 3) disabled = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getProgressSummary() {
|
|
129
|
+
return { updates: totalUpdates, errors: errorCount, disabled };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { reportProgress, getProgressSummary };
|
|
133
|
+
}
|
|
134
|
+
|
|
81
135
|
/**
|
|
82
136
|
* Context factory that creates a complete context object for data generation
|
|
83
137
|
* @param {Dungeon} config - Validated configuration object
|
|
@@ -99,12 +153,16 @@ export function createContext(config, storage = null, timeConstants = {}) {
|
|
|
99
153
|
runtime.verbose = config.verbose || false;
|
|
100
154
|
runtime.isBatchMode = config.batchSize && config.batchSize < config.numEvents;
|
|
101
155
|
|
|
156
|
+
const { reportProgress, getProgressSummary } = createProgressReporter(config);
|
|
157
|
+
|
|
102
158
|
const context = {
|
|
103
159
|
config,
|
|
104
160
|
storage,
|
|
105
161
|
defaults,
|
|
106
162
|
campaigns: campaignData,
|
|
107
163
|
runtime,
|
|
164
|
+
reportProgress,
|
|
165
|
+
getProgressSummary,
|
|
108
166
|
|
|
109
167
|
// Helper methods for updating state
|
|
110
168
|
incrementOperations() {
|
|
@@ -63,6 +63,21 @@ export async function sendToMixpanel(context) {
|
|
|
63
63
|
workers: 35
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
const hasProgressCb = typeof config.onProgress === 'function';
|
|
67
|
+
function makeProgressCallback(total) {
|
|
68
|
+
if (!hasProgressCb) return undefined;
|
|
69
|
+
return (recordType, processed, requests, eps, bytesProcessed) => {
|
|
70
|
+
context.reportProgress({
|
|
71
|
+
phase: "import",
|
|
72
|
+
recordType,
|
|
73
|
+
processed,
|
|
74
|
+
total,
|
|
75
|
+
eps,
|
|
76
|
+
bytesProcessed
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
66
81
|
log(`\n${'─'.repeat(50)}`);
|
|
67
82
|
log(` Importing data to Mixpanel (${region})`);
|
|
68
83
|
log(`${'─'.repeat(50)}\n`);
|
|
@@ -76,9 +91,11 @@ export async function sendToMixpanel(context) {
|
|
|
76
91
|
const files = eventData.getWrittenFiles();
|
|
77
92
|
if (files.length > 0) eventDataToImport = files;
|
|
78
93
|
}
|
|
94
|
+
const eventTotal = Array.isArray(eventDataToImport) ? eventDataToImport.length : 0;
|
|
79
95
|
const imported = await mp(creds, eventDataToImport, {
|
|
80
96
|
recordType: "event",
|
|
81
97
|
...commonOpts,
|
|
98
|
+
progressCallback: makeProgressCallback(eventTotal),
|
|
82
99
|
});
|
|
83
100
|
log(` -> ${comma(imported.success)} events sent\n`);
|
|
84
101
|
importResults.events = imported;
|
|
@@ -93,9 +110,11 @@ export async function sendToMixpanel(context) {
|
|
|
93
110
|
const files = userProfilesData.getWrittenFiles();
|
|
94
111
|
if (files.length > 0) userProfilesToImport = files;
|
|
95
112
|
}
|
|
113
|
+
const userTotal = Array.isArray(userProfilesToImport) ? userProfilesToImport.length : 0;
|
|
96
114
|
const imported = await mp(creds, userProfilesToImport, {
|
|
97
115
|
recordType: "user",
|
|
98
116
|
...commonOpts,
|
|
117
|
+
progressCallback: makeProgressCallback(userTotal),
|
|
99
118
|
});
|
|
100
119
|
log(` -> ${comma(imported.success)} user profiles sent\n`);
|
|
101
120
|
importResults.users = imported;
|
|
@@ -110,9 +129,11 @@ export async function sendToMixpanel(context) {
|
|
|
110
129
|
const files = adSpendData.getWrittenFiles();
|
|
111
130
|
if (files.length > 0) adSpendDataToImport = files;
|
|
112
131
|
}
|
|
132
|
+
const adTotal = Array.isArray(adSpendDataToImport) ? adSpendDataToImport.length : 0;
|
|
113
133
|
const imported = await mp(creds, adSpendDataToImport, {
|
|
114
134
|
recordType: "event",
|
|
115
135
|
...commonOpts,
|
|
136
|
+
progressCallback: makeProgressCallback(adTotal),
|
|
116
137
|
});
|
|
117
138
|
log(` -> ${comma(imported.success)} ad spend events sent\n`);
|
|
118
139
|
importResults.adSpend = imported;
|
|
@@ -131,10 +152,12 @@ export async function sendToMixpanel(context) {
|
|
|
131
152
|
const files = groupEntity.getWrittenFiles();
|
|
132
153
|
if (files.length > 0) groupProfilesToImport = files;
|
|
133
154
|
}
|
|
155
|
+
const groupTotal = Array.isArray(groupProfilesToImport) ? groupProfilesToImport.length : 0;
|
|
134
156
|
const imported = await mp({ token, groupKey }, groupProfilesToImport, {
|
|
135
157
|
recordType: "group",
|
|
136
158
|
...commonOpts,
|
|
137
159
|
groupKey,
|
|
160
|
+
progressCallback: makeProgressCallback(groupTotal),
|
|
138
161
|
});
|
|
139
162
|
log(` -> ${comma(imported.success)} ${groupKey} profiles sent\n`);
|
|
140
163
|
importResults.groups.push(imported);
|
|
@@ -150,9 +173,11 @@ export async function sendToMixpanel(context) {
|
|
|
150
173
|
const files = groupEventData.getWrittenFiles();
|
|
151
174
|
if (files.length > 0) groupEventDataToImport = files;
|
|
152
175
|
}
|
|
176
|
+
const groupEvTotal = Array.isArray(groupEventDataToImport) ? groupEventDataToImport.length : 0;
|
|
153
177
|
const imported = await mp(creds, groupEventDataToImport, {
|
|
154
178
|
recordType: "event",
|
|
155
179
|
...commonOpts,
|
|
180
|
+
progressCallback: makeProgressCallback(groupEvTotal),
|
|
156
181
|
});
|
|
157
182
|
log(` -> ${comma(imported.success)} group events sent\n`);
|
|
158
183
|
importResults.groupEvents = imported;
|
|
@@ -197,6 +222,7 @@ export async function sendToMixpanel(context) {
|
|
|
197
222
|
}
|
|
198
223
|
|
|
199
224
|
try {
|
|
225
|
+
const scdTotal = Array.isArray(scdDataToImport) ? scdDataToImport.length : 0;
|
|
200
226
|
const imported = await mp(
|
|
201
227
|
{
|
|
202
228
|
token,
|
|
@@ -205,7 +231,7 @@ export async function sendToMixpanel(context) {
|
|
|
205
231
|
project: projectId
|
|
206
232
|
},
|
|
207
233
|
scdDataToImport,
|
|
208
|
-
options
|
|
234
|
+
{ ...options, progressCallback: makeProgressCallback(scdTotal) }
|
|
209
235
|
);
|
|
210
236
|
log(` -> ${comma(imported.success)} ${scdKey} SCD entries sent\n`);
|
|
211
237
|
importResults[`${scdKey}_scd`] = imported;
|
|
@@ -104,6 +104,16 @@ export async function userLoop(context) {
|
|
|
104
104
|
]);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
context.reportProgress({
|
|
108
|
+
phase: "generation",
|
|
109
|
+
users: context.getUserCount(),
|
|
110
|
+
events: context.getEventCount(),
|
|
111
|
+
eps,
|
|
112
|
+
memory: memUsed,
|
|
113
|
+
elapsed: duration,
|
|
114
|
+
percentComplete: Math.min(100, Math.round((context.getUserCount() / numUsers) * 100))
|
|
115
|
+
});
|
|
116
|
+
|
|
107
117
|
const userId = chance.guid();
|
|
108
118
|
const user = u.generateUser(userId, { numDays, isAnonymous, hasAvatar, hasAnonIds, hasSessionIds, datasetEndUnix: context.FIXED_NOW, avgDevicePerUser });
|
|
109
119
|
const { distinct_id, created } = user;
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -119,6 +119,24 @@ export interface Dungeon {
|
|
|
119
119
|
gzip?: boolean;
|
|
120
120
|
/** If true, prints progress to stdout during generation. */
|
|
121
121
|
verbose?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Optional callback that receives periodic progress updates during generation,
|
|
124
|
+
* import, and pipeline step transitions. Fire-and-forget: the callback is never
|
|
125
|
+
* awaited. If it throws 3 times, it is silently disabled for the rest of the job.
|
|
126
|
+
*
|
|
127
|
+
* The `update` argument is a discriminated union on `phase`:
|
|
128
|
+
* - `"generation"` — user/event counts, EPS, memory, percent complete
|
|
129
|
+
* - `"import"` — record type, processed/total counts, EPS, bytes
|
|
130
|
+
* - `"step"` — pipeline step name with start/complete status and duration
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* onProgress: (update) => {
|
|
134
|
+
* if (update.phase === 'generation') ws.send(JSON.stringify(update));
|
|
135
|
+
* }
|
|
136
|
+
*/
|
|
137
|
+
onProgress?: (update: ProgressUpdate) => void;
|
|
138
|
+
/** Minimum interval (ms) between progress callback invocations. Default: 500. Only throttles `generation` and `import` phases; `step` updates always fire immediately. */
|
|
139
|
+
progressInterval?: number;
|
|
122
140
|
/**
|
|
123
141
|
* @deprecated Prefer `avgDevicePerUser`. `true` is now an alias for `avgDevicePerUser: 1`
|
|
124
142
|
* (single sticky device per user, every event stamped with that `device_id`). `false`
|
|
@@ -657,6 +675,12 @@ export interface Context {
|
|
|
657
675
|
incrementUserCount(): void;
|
|
658
676
|
incrementEventCount(): void;
|
|
659
677
|
isBatchMode(): boolean;
|
|
678
|
+
|
|
679
|
+
// Progress callback
|
|
680
|
+
/** Fire a progress update to the caller's `onProgress` callback (throttled, fault-tolerant). */
|
|
681
|
+
reportProgress(update: ProgressUpdate): void;
|
|
682
|
+
/** Return the progress callback summary (updates delivered, errors, disabled flag). */
|
|
683
|
+
getProgressSummary(): ProgressSummary;
|
|
660
684
|
}
|
|
661
685
|
|
|
662
686
|
/**
|
|
@@ -1053,8 +1077,61 @@ export type Result = {
|
|
|
1053
1077
|
userCount?: number;
|
|
1054
1078
|
groupCount?: number;
|
|
1055
1079
|
avgEPS?: number;
|
|
1080
|
+
/** Progress callback summary. Only present when `onProgress` was provided. */
|
|
1081
|
+
progress?: ProgressSummary;
|
|
1056
1082
|
};
|
|
1057
1083
|
|
|
1084
|
+
// ============= Progress Callback Types =============
|
|
1085
|
+
|
|
1086
|
+
/** Discriminator for progress update types. */
|
|
1087
|
+
export type ProgressPhase = "generation" | "import" | "step";
|
|
1088
|
+
|
|
1089
|
+
/** Progress update emitted during user/event generation (throttled to `progressInterval`). */
|
|
1090
|
+
export interface ProgressGeneration {
|
|
1091
|
+
phase: "generation";
|
|
1092
|
+
users: number;
|
|
1093
|
+
events: number;
|
|
1094
|
+
eps: number;
|
|
1095
|
+
memory: string;
|
|
1096
|
+
elapsed: string;
|
|
1097
|
+
percentComplete: number;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/** Progress update emitted during Mixpanel import (throttled to `progressInterval`). */
|
|
1101
|
+
export interface ProgressImport {
|
|
1102
|
+
phase: "import";
|
|
1103
|
+
recordType: string;
|
|
1104
|
+
processed: number;
|
|
1105
|
+
total: number;
|
|
1106
|
+
eps: string;
|
|
1107
|
+
bytesProcessed: number;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/** Progress update emitted at pipeline step boundaries (not throttled). */
|
|
1111
|
+
export interface ProgressStep {
|
|
1112
|
+
phase: "step";
|
|
1113
|
+
step: string;
|
|
1114
|
+
status: "start" | "complete";
|
|
1115
|
+
/** Milliseconds elapsed (only present on `status: "complete"`). */
|
|
1116
|
+
duration?: number;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/** Discriminated union of all progress update types. Discriminate on the `phase` field. */
|
|
1120
|
+
export type ProgressUpdate = ProgressGeneration | ProgressImport | ProgressStep;
|
|
1121
|
+
|
|
1122
|
+
/** Convenience type for the `onProgress` callback signature. */
|
|
1123
|
+
export type ProgressCallback = (update: ProgressUpdate) => void;
|
|
1124
|
+
|
|
1125
|
+
/** Summary of progress callback activity, included in the job result. */
|
|
1126
|
+
export interface ProgressSummary {
|
|
1127
|
+
/** Total number of updates successfully delivered to the callback. */
|
|
1128
|
+
updates: number;
|
|
1129
|
+
/** Number of times the callback threw (0-3; disabled after 3). */
|
|
1130
|
+
errors: number;
|
|
1131
|
+
/** True if the callback was disabled due to repeated failures. */
|
|
1132
|
+
disabled: boolean;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1058
1135
|
// ============= Advanced Feature Types =============
|
|
1059
1136
|
|
|
1060
1137
|
/**
|