@ak--47/dungeon-master 1.4.0 → 1.4.2
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/index.js +17 -71
- package/lib/core/config-validator.js +9 -9
- package/lib/core/storage.js +5 -1
- package/lib/orchestrators/mixpanel-sender.js +46 -51
- package/lib/templates/macro-presets.js +5 -5
- package/lib/utils/utils.js +10 -0
- package/package.json +1 -1
- package/types.d.ts +8 -2
package/index.js
CHANGED
|
@@ -18,14 +18,14 @@ import { detectInputType, loadFromFile, loadFromText, parseJSONDungeon, validate
|
|
|
18
18
|
|
|
19
19
|
// Orchestrators
|
|
20
20
|
import { userLoop } from './lib/orchestrators/user-loop.js';
|
|
21
|
-
import { sendToMixpanel } from './lib/orchestrators/mixpanel-sender.js';
|
|
21
|
+
import { sendToMixpanel, collectWrittenFiles } from './lib/orchestrators/mixpanel-sender.js';
|
|
22
22
|
// Generators
|
|
23
23
|
import { makeAdSpend } from './lib/generators/adspend.js';
|
|
24
24
|
import { makeMirror } from './lib/generators/mirror.js';
|
|
25
25
|
import { makeGroupProfile, makeProfile } from './lib/generators/profiles.js';
|
|
26
26
|
|
|
27
27
|
// Utilities
|
|
28
|
-
import { initChance, setDatasetNow } from './lib/utils/utils.js';
|
|
28
|
+
import { initChance, setDatasetNow, deleteFile } from './lib/utils/utils.js';
|
|
29
29
|
|
|
30
30
|
// External dependencies
|
|
31
31
|
import dayjs from "dayjs";
|
|
@@ -122,6 +122,7 @@ async function runDungeon(config) {
|
|
|
122
122
|
|
|
123
123
|
if (config.verbose) logger.info({ seed: config.seed }, 'Configuring dungeon');
|
|
124
124
|
let validatedConfig;
|
|
125
|
+
let storage;
|
|
125
126
|
try {
|
|
126
127
|
// Initialize seeded RNG BEFORE validation — config-validator captures a
|
|
127
128
|
// chance reference for default userProps (spiritAnimal). If we init after,
|
|
@@ -148,7 +149,7 @@ async function runDungeon(config) {
|
|
|
148
149
|
|
|
149
150
|
// Step 3: Initialize storage containers
|
|
150
151
|
const storageManager = new StorageManager(context);
|
|
151
|
-
|
|
152
|
+
storage = await storageManager.initializeContainers();
|
|
152
153
|
updateContextWithStorage(context, storage);
|
|
153
154
|
|
|
154
155
|
// ! DATA GENERATION STARTS HERE
|
|
@@ -215,7 +216,7 @@ async function runDungeon(config) {
|
|
|
215
216
|
return {
|
|
216
217
|
...extractedData,
|
|
217
218
|
importResults,
|
|
218
|
-
files:
|
|
219
|
+
files: extractFileInfo(storage),
|
|
219
220
|
time: { start, end, delta, human },
|
|
220
221
|
operations: context.getOperations(),
|
|
221
222
|
eventCount: context.getStoredEventCount(),
|
|
@@ -225,6 +226,15 @@ async function runDungeon(config) {
|
|
|
225
226
|
} catch (error) {
|
|
226
227
|
logger.error({ err: error }, `Error: ${error.message}`);
|
|
227
228
|
throw error;
|
|
229
|
+
} finally {
|
|
230
|
+
if (validatedConfig?.cleanup && storage) {
|
|
231
|
+
const allFiles = collectWrittenFiles(storage);
|
|
232
|
+
if (allFiles.length > 0) {
|
|
233
|
+
if (validatedConfig.verbose) console.log(`\nCleaning up ${allFiles.length} written files...`);
|
|
234
|
+
await Promise.allSettled(allFiles.map(f => deleteFile(f)));
|
|
235
|
+
if (validatedConfig.verbose) console.log(`Cleanup complete`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
228
238
|
}
|
|
229
239
|
}
|
|
230
240
|
|
|
@@ -480,74 +490,10 @@ async function flushStorageToDisk(storage, config) {
|
|
|
480
490
|
/**
|
|
481
491
|
* Extract file information from storage containers
|
|
482
492
|
* @param {import('./types').Storage} storage - Storage object
|
|
483
|
-
* @
|
|
484
|
-
* @returns {Promise<string[]>} Array of file paths
|
|
493
|
+
* @returns {string[]} Array of file paths
|
|
485
494
|
*/
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
// Try to get paths from containers first
|
|
490
|
-
Object.values(storage).forEach(container => {
|
|
491
|
-
if (Array.isArray(container)) {
|
|
492
|
-
container.forEach(subContainer => {
|
|
493
|
-
if (subContainer?.getWritePath) {
|
|
494
|
-
files.push(subContainer.getWritePath());
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
} else if (container?.getWritePath) {
|
|
498
|
-
files.push(container.getWritePath());
|
|
499
|
-
}
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
// If no files found from containers and writeToDisk is enabled, scan the data directory
|
|
503
|
-
if (files.length === 0 && config.writeToDisk) {
|
|
504
|
-
try {
|
|
505
|
-
const fs = await import('fs');
|
|
506
|
-
const path = await import('path');
|
|
507
|
-
|
|
508
|
-
let dataDir = path.resolve("./data");
|
|
509
|
-
if (!fs.existsSync(dataDir)) {
|
|
510
|
-
dataDir = path.resolve("./");
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
if (fs.existsSync(dataDir)) {
|
|
514
|
-
const allFiles = fs.readdirSync(dataDir);
|
|
515
|
-
const simulationName = config.name;
|
|
516
|
-
|
|
517
|
-
// Filter files that match our patterns and were likely created by this run
|
|
518
|
-
const relevantFiles = allFiles.filter(file => {
|
|
519
|
-
// Skip system files
|
|
520
|
-
if (file.startsWith('.')) return false;
|
|
521
|
-
|
|
522
|
-
// If we have a simulation name, only include files with that prefix
|
|
523
|
-
if (simulationName && !file.startsWith(simulationName)) {
|
|
524
|
-
return false;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// Check for common patterns
|
|
528
|
-
const hasEventPattern = file.includes('-EVENTS.');
|
|
529
|
-
const hasUserPattern = file.includes('-USERS.');
|
|
530
|
-
const hasScdPattern = file.includes('-SCD.');
|
|
531
|
-
const hasGroupPattern = file.includes('-GROUPS.');
|
|
532
|
-
const hasLookupPattern = file.includes('-LOOKUP.');
|
|
533
|
-
const hasAdspendPattern = file.includes('-ADSPEND.');
|
|
534
|
-
const hasMirrorPattern = file.includes('-MIRROR.');
|
|
535
|
-
|
|
536
|
-
return hasEventPattern || hasUserPattern || hasScdPattern ||
|
|
537
|
-
hasGroupPattern || hasLookupPattern || hasAdspendPattern || hasMirrorPattern;
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
// Convert to full paths
|
|
541
|
-
relevantFiles.forEach(file => {
|
|
542
|
-
files.push(path.join(dataDir, file));
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
} catch (error) {
|
|
546
|
-
// If scanning fails, just return empty array
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
return files;
|
|
495
|
+
function extractFileInfo(storage) {
|
|
496
|
+
return collectWrittenFiles(storage);
|
|
551
497
|
}
|
|
552
498
|
|
|
553
499
|
/**
|
|
@@ -30,7 +30,7 @@ import { resolveMacro } from "../templates/macro-presets.js";
|
|
|
30
30
|
* @param {number} [userNumDays]
|
|
31
31
|
* @returns {{ datasetStartUnix: number, datasetEndUnix: number, numDays: number }}
|
|
32
32
|
*/
|
|
33
|
-
function resolveDatasetWindow(datasetStart, datasetEnd, userNumDays) {
|
|
33
|
+
function resolveDatasetWindow(datasetStart, datasetEnd, userNumDays, verbose = false) {
|
|
34
34
|
const hasStart = datasetStart !== undefined && datasetStart !== null;
|
|
35
35
|
const hasEnd = datasetEnd !== undefined && datasetEnd !== null;
|
|
36
36
|
|
|
@@ -48,7 +48,7 @@ function resolveDatasetWindow(datasetStart, datasetEnd, userNumDays) {
|
|
|
48
48
|
throw new Error(`datasetEnd (${datasetEnd}) must be after datasetStart (${datasetStart}).`);
|
|
49
49
|
}
|
|
50
50
|
const derivedNumDays = Math.max(1, Math.round((endUnix - startUnix) / 86400));
|
|
51
|
-
if (userNumDays !== undefined && userNumDays !== null && userNumDays !== derivedNumDays) {
|
|
51
|
+
if (verbose && userNumDays !== undefined && userNumDays !== null && userNumDays !== derivedNumDays) {
|
|
52
52
|
console.warn(
|
|
53
53
|
`⚠️ datasetStart/datasetEnd take precedence; user-supplied numDays=${userNumDays} ignored, derived numDays=${derivedNumDays}.`
|
|
54
54
|
);
|
|
@@ -60,7 +60,7 @@ function resolveDatasetWindow(datasetStart, datasetEnd, userNumDays) {
|
|
|
60
60
|
const fallbackNumDays = (typeof userNumDays === 'number' && userNumDays > 0) ? userNumDays : 30;
|
|
61
61
|
const todayStart = dayjs().startOf('day').unix();
|
|
62
62
|
const fallbackStart = todayStart - fallbackNumDays * 86400;
|
|
63
|
-
console.warn(
|
|
63
|
+
if (verbose) console.warn(
|
|
64
64
|
`⚠️ No 'datasetStart'/'datasetEnd' set — dataset window anchored to today's date and will shift across runs. Pin both for full determinism.`
|
|
65
65
|
);
|
|
66
66
|
return { datasetStartUnix: fallbackStart, datasetEndUnix: todayStart, numDays: fallbackNumDays };
|
|
@@ -160,7 +160,7 @@ function stripKilledConfigKeys(config) {
|
|
|
160
160
|
const found = KILLED_CONFIG_KEYS.filter(k => config[k] !== undefined && config[k] !== null);
|
|
161
161
|
if (!found.length) return;
|
|
162
162
|
for (const k of found) delete config[k];
|
|
163
|
-
if (config.verbose
|
|
163
|
+
if (config.verbose) {
|
|
164
164
|
console.warn(
|
|
165
165
|
`⚠️ dungeon-master 1.4 removed engine support for: ${found.join(', ')}. ` +
|
|
166
166
|
`These config keys are silently ignored. Recreate via hooks (see lib/hook-patterns/* once Phase 4 lands).`
|
|
@@ -346,7 +346,7 @@ export function validateDungeonConfig(config) {
|
|
|
346
346
|
// ── Resolve dataset window ──
|
|
347
347
|
// Preferred path: explicit datasetStart + datasetEnd → pinned, deterministic window.
|
|
348
348
|
// Fallback: numDays only → today_start - numDays back (sliding, warn-emitted).
|
|
349
|
-
const windowResolution = resolveDatasetWindow(config.datasetStart, config.datasetEnd, config.numDays);
|
|
349
|
+
const windowResolution = resolveDatasetWindow(config.datasetStart, config.datasetEnd, config.numDays, verbose);
|
|
350
350
|
const datasetStartUnix = windowResolution.datasetStartUnix;
|
|
351
351
|
const datasetEndUnix = windowResolution.datasetEndUnix;
|
|
352
352
|
numDays = windowResolution.numDays;
|
|
@@ -378,7 +378,7 @@ export function validateDungeonConfig(config) {
|
|
|
378
378
|
// only avgEventsPerUserPerDay would never trigger auto-batch.
|
|
379
379
|
if (numEvents >= 2_000_000 && config.batchSize === undefined) {
|
|
380
380
|
batchSize = 1_000_000;
|
|
381
|
-
console.warn(`⚠️ Auto-enabling batch mode: numEvents (${numEvents.toLocaleString()}) >= 2M. Using batchSize of ${batchSize.toLocaleString()}.`);
|
|
381
|
+
if (verbose) console.warn(`⚠️ Auto-enabling batch mode: numEvents (${numEvents.toLocaleString()}) >= 2M. Using batchSize of ${batchSize.toLocaleString()}.`);
|
|
382
382
|
}
|
|
383
383
|
|
|
384
384
|
// Resolve soup presets (intra-week / intra-day shape — must happen after numDays is computed)
|
|
@@ -422,7 +422,7 @@ export function validateDungeonConfig(config) {
|
|
|
422
422
|
throw new Error('Hook string did not evaluate to a function');
|
|
423
423
|
}
|
|
424
424
|
} catch (error) {
|
|
425
|
-
if (
|
|
425
|
+
if (verbose) {
|
|
426
426
|
console.warn(`\u26a0\ufe0f Failed to convert hook string to function: ${error.message}`);
|
|
427
427
|
console.warn('Using default pass-through hook');
|
|
428
428
|
}
|
|
@@ -432,7 +432,7 @@ export function validateDungeonConfig(config) {
|
|
|
432
432
|
|
|
433
433
|
// Ensure hook is a function
|
|
434
434
|
if (typeof hook !== 'function') {
|
|
435
|
-
if (
|
|
435
|
+
if (verbose) console.warn('\u26a0\ufe0f Hook is not a function, using default pass-through hook');
|
|
436
436
|
hook = (record) => record;
|
|
437
437
|
}
|
|
438
438
|
|
|
@@ -555,7 +555,7 @@ export function validateDungeonConfig(config) {
|
|
|
555
555
|
|
|
556
556
|
// Warn if isAuthEvent is set but avgDevicePerUser=0 — pre-auth device_only
|
|
557
557
|
// stamping degrades to user_id via the floor guard, defeating the identity model.
|
|
558
|
-
if (avgDevicePerUser === 0 && validatedEvents.some(e => e.isAuthEvent)) {
|
|
558
|
+
if (verbose && avgDevicePerUser === 0 && validatedEvents.some(e => e.isAuthEvent)) {
|
|
559
559
|
console.warn(
|
|
560
560
|
`⚠️ isAuthEvent requires avgDevicePerUser >= 1 to produce pre-auth anonymous events. ` +
|
|
561
561
|
`Set avgDevicePerUser or hasAnonIds: true.`
|
package/lib/core/storage.js
CHANGED
|
@@ -50,6 +50,7 @@ export async function createHookArray(arr = [], opts) {
|
|
|
50
50
|
let writeDir;
|
|
51
51
|
let isBatchMode = runtime.isBatchMode || false;
|
|
52
52
|
let isWriting = false; // Prevent concurrent writes
|
|
53
|
+
const writtenFiles = [];
|
|
53
54
|
|
|
54
55
|
// Determine write directory
|
|
55
56
|
const dataFolder = path.resolve("./data");
|
|
@@ -175,6 +176,7 @@ export async function createHookArray(arr = [], opts) {
|
|
|
175
176
|
|
|
176
177
|
// Write to disk/cloud - always blocking to prevent OOM
|
|
177
178
|
const writeResult = await FILE_CONN(() => writeToDisk(dataToWrite, { writePath }));
|
|
179
|
+
writtenFiles.push(writePath);
|
|
178
180
|
return writeResult;
|
|
179
181
|
} finally {
|
|
180
182
|
isWriting = false; // Release the lock
|
|
@@ -231,6 +233,7 @@ export async function createHookArray(arr = [], opts) {
|
|
|
231
233
|
const dataToWrite = [...arr];
|
|
232
234
|
arr.length = 0; // Clear array after copying data
|
|
233
235
|
await FILE_CONN(() => writeToDisk(dataToWrite, { writePath }));
|
|
236
|
+
writtenFiles.push(writePath);
|
|
234
237
|
// Data now lives on disk, not in arr — mirror what transformThenPush
|
|
235
238
|
// does when crossing BATCH_SIZE so the Mixpanel sender knows to read
|
|
236
239
|
// from disk instead of from the (now empty) in-memory array.
|
|
@@ -249,6 +252,7 @@ export async function createHookArray(arr = [], opts) {
|
|
|
249
252
|
enrichedArray.flush = flush;
|
|
250
253
|
enrichedArray.getWriteDir = getWriteDir;
|
|
251
254
|
enrichedArray.getWritePath = getWritePath;
|
|
255
|
+
enrichedArray.getWrittenFiles = () => [...writtenFiles];
|
|
252
256
|
|
|
253
257
|
// Add additional properties from rest
|
|
254
258
|
for (const key in rest) {
|
|
@@ -384,7 +388,7 @@ export class StorageManager {
|
|
|
384
388
|
const batchSize = config.batchSize || 1_000_000;
|
|
385
389
|
const numEvents = config.numEvents || 0;
|
|
386
390
|
|
|
387
|
-
if (batchSize < numEvents) {
|
|
391
|
+
if (batchSize < numEvents && config.verbose) {
|
|
388
392
|
console.warn(
|
|
389
393
|
`⚠️ writeToDisk is false but batchSize (${batchSize.toLocaleString()}) < numEvents (${numEvents.toLocaleString()}). ` +
|
|
390
394
|
`Batch files will be written to disk temporarily to avoid OOM. ` +
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
/** @typedef {import('../../types').Context} Context */
|
|
7
7
|
|
|
8
8
|
import dayjs from "dayjs";
|
|
9
|
-
import { comma,
|
|
9
|
+
import { comma, rm } from "ak-tools";
|
|
10
10
|
import * as u from "../utils/utils.js";
|
|
11
11
|
import mp from "mixpanel-import";
|
|
12
12
|
|
|
@@ -15,6 +15,8 @@ import mp from "mixpanel-import";
|
|
|
15
15
|
* @param {Context} context - Context object containing config, storage, etc.
|
|
16
16
|
* @returns {Promise<Object>} Import results for all data types
|
|
17
17
|
*/
|
|
18
|
+
export { collectWrittenFiles };
|
|
19
|
+
|
|
18
20
|
export async function sendToMixpanel(context) {
|
|
19
21
|
const { config, storage } = context;
|
|
20
22
|
const {
|
|
@@ -70,11 +72,9 @@ export async function sendToMixpanel(context) {
|
|
|
70
72
|
log(` Events`);
|
|
71
73
|
let eventDataToImport = u.deepClone(eventData);
|
|
72
74
|
const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && eventData && eventData.length === 0);
|
|
73
|
-
if (shouldReadFromFiles && eventData?.
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
// @ts-ignore
|
|
77
|
-
eventDataToImport = files.filter(f => f.includes('-EVENTS'));
|
|
75
|
+
if (shouldReadFromFiles && eventData?.getWrittenFiles) {
|
|
76
|
+
const files = eventData.getWrittenFiles();
|
|
77
|
+
if (files.length > 0) eventDataToImport = files;
|
|
78
78
|
}
|
|
79
79
|
const imported = await mp(creds, eventDataToImport, {
|
|
80
80
|
recordType: "event",
|
|
@@ -89,11 +89,9 @@ export async function sendToMixpanel(context) {
|
|
|
89
89
|
log(` User Profiles`);
|
|
90
90
|
let userProfilesToImport = u.deepClone(userProfilesData);
|
|
91
91
|
const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && userProfilesData && userProfilesData.length === 0);
|
|
92
|
-
if (shouldReadFromFiles && userProfilesData?.
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
// @ts-ignore
|
|
96
|
-
userProfilesToImport = files.filter(f => f.includes('-USERS'));
|
|
92
|
+
if (shouldReadFromFiles && userProfilesData?.getWrittenFiles) {
|
|
93
|
+
const files = userProfilesData.getWrittenFiles();
|
|
94
|
+
if (files.length > 0) userProfilesToImport = files;
|
|
97
95
|
}
|
|
98
96
|
const imported = await mp(creds, userProfilesToImport, {
|
|
99
97
|
recordType: "user",
|
|
@@ -108,11 +106,9 @@ export async function sendToMixpanel(context) {
|
|
|
108
106
|
log(` Ad Spend`);
|
|
109
107
|
let adSpendDataToImport = u.deepClone(adSpendData);
|
|
110
108
|
const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && adSpendData && adSpendData.length === 0);
|
|
111
|
-
if (shouldReadFromFiles && adSpendData?.
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
// @ts-ignore
|
|
115
|
-
adSpendDataToImport = files.filter(f => f.includes('-ADSPEND'));
|
|
109
|
+
if (shouldReadFromFiles && adSpendData?.getWrittenFiles) {
|
|
110
|
+
const files = adSpendData.getWrittenFiles();
|
|
111
|
+
if (files.length > 0) adSpendDataToImport = files;
|
|
116
112
|
}
|
|
117
113
|
const imported = await mp(creds, adSpendDataToImport, {
|
|
118
114
|
recordType: "event",
|
|
@@ -130,11 +126,9 @@ export async function sendToMixpanel(context) {
|
|
|
130
126
|
log(` Group Profiles (${groupKey})`);
|
|
131
127
|
let groupProfilesToImport = u.deepClone(groupEntity);
|
|
132
128
|
const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && groupEntity.length === 0);
|
|
133
|
-
if (shouldReadFromFiles && groupEntity?.
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
// @ts-ignore
|
|
137
|
-
groupProfilesToImport = files.filter(f => f.includes(`-${groupKey}-GROUPS`));
|
|
129
|
+
if (shouldReadFromFiles && groupEntity?.getWrittenFiles) {
|
|
130
|
+
const files = groupEntity.getWrittenFiles();
|
|
131
|
+
if (files.length > 0) groupProfilesToImport = files;
|
|
138
132
|
}
|
|
139
133
|
const imported = await mp({ token, groupKey }, groupProfilesToImport, {
|
|
140
134
|
recordType: "group",
|
|
@@ -151,11 +145,9 @@ export async function sendToMixpanel(context) {
|
|
|
151
145
|
log(` Group Events`);
|
|
152
146
|
let groupEventDataToImport = u.deepClone(groupEventData);
|
|
153
147
|
const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && groupEventData.length === 0);
|
|
154
|
-
if (shouldReadFromFiles && groupEventData?.
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
// @ts-ignore
|
|
158
|
-
groupEventDataToImport = files.filter(f => f.includes('-GROUP-EVENTS'));
|
|
148
|
+
if (shouldReadFromFiles && groupEventData?.getWrittenFiles) {
|
|
149
|
+
const files = groupEventData.getWrittenFiles();
|
|
150
|
+
if (files.length > 0) groupEventDataToImport = files;
|
|
159
151
|
}
|
|
160
152
|
const imported = await mp(creds, groupEventDataToImport, {
|
|
161
153
|
recordType: "event",
|
|
@@ -174,11 +166,9 @@ export async function sendToMixpanel(context) {
|
|
|
174
166
|
log(` SCD: ${scdKey}`);
|
|
175
167
|
let scdDataToImport = u.deepClone(scdEntity);
|
|
176
168
|
const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && scdEntity && scdEntity.length === 0);
|
|
177
|
-
if (shouldReadFromFiles && scdEntity?.
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
// @ts-ignore
|
|
181
|
-
scdDataToImport = files.filter(f => f.includes(`-${scdKey}-SCD`))?.pop();
|
|
169
|
+
if (shouldReadFromFiles && scdEntity?.getWrittenFiles) {
|
|
170
|
+
const files = scdEntity.getWrittenFiles();
|
|
171
|
+
if (files.length > 0) scdDataToImport = files;
|
|
182
172
|
}
|
|
183
173
|
|
|
184
174
|
/** @type {"string" | "number" | "boolean"} */
|
|
@@ -231,27 +221,11 @@ export async function sendToMixpanel(context) {
|
|
|
231
221
|
|
|
232
222
|
log(`${'─'.repeat(50)}\n`);
|
|
233
223
|
|
|
234
|
-
// Clean up batch files if needed
|
|
224
|
+
// Clean up batch files if needed (writeToDisk=false but batch mode wrote temp files)
|
|
235
225
|
if (!writeToDisk && isBATCH_MODE) {
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const listDir = await ls(writeDir);
|
|
240
|
-
// @ts-ignore
|
|
241
|
-
const files = listDir.filter(f => {
|
|
242
|
-
if (configName && !f.includes(configName)) return false;
|
|
243
|
-
return f.includes('-EVENTS') ||
|
|
244
|
-
f.includes('-USERS') ||
|
|
245
|
-
f.includes('-ADSPEND') ||
|
|
246
|
-
f.includes('-GROUPS') ||
|
|
247
|
-
f.includes('-GROUP-EVENTS') ||
|
|
248
|
-
f.includes('-SCD') ||
|
|
249
|
-
f.includes('-MIRROR') ||
|
|
250
|
-
f.includes('-LOOKUP');
|
|
251
|
-
});
|
|
252
|
-
for (const file of files) {
|
|
253
|
-
await rm(file);
|
|
254
|
-
}
|
|
226
|
+
const allFiles = collectWrittenFiles(storage);
|
|
227
|
+
for (const file of allFiles) {
|
|
228
|
+
await rm(file);
|
|
255
229
|
}
|
|
256
230
|
}
|
|
257
231
|
|
|
@@ -324,4 +298,25 @@ function logProblems(problems) {
|
|
|
324
298
|
}
|
|
325
299
|
}
|
|
326
300
|
log('');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Collect all written file paths from every storage container.
|
|
305
|
+
* @param {import('../../types').Storage} storage
|
|
306
|
+
* @returns {string[]}
|
|
307
|
+
*/
|
|
308
|
+
function collectWrittenFiles(storage) {
|
|
309
|
+
const files = [];
|
|
310
|
+
for (const container of [storage.eventData, storage.userProfilesData, storage.adSpendData,
|
|
311
|
+
storage.mirrorEventData, storage.groupEventData]) {
|
|
312
|
+
if (container?.getWrittenFiles) files.push(...container.getWrittenFiles());
|
|
313
|
+
}
|
|
314
|
+
for (const arr of [storage.groupProfilesData, storage.scdTableData, storage.lookupTableData]) {
|
|
315
|
+
if (Array.isArray(arr)) {
|
|
316
|
+
for (const c of arr) {
|
|
317
|
+
if (c?.getWrittenFiles) files.push(...c.getWrittenFiles());
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return files;
|
|
327
322
|
}
|
|
@@ -31,7 +31,7 @@ export const MACRO_PRESETS = {
|
|
|
31
31
|
*/
|
|
32
32
|
flat: {
|
|
33
33
|
bornRecentBias: 0,
|
|
34
|
-
percentUsersBornInDataset:
|
|
34
|
+
percentUsersBornInDataset: 50,
|
|
35
35
|
preExistingSpread: 'uniform',
|
|
36
36
|
},
|
|
37
37
|
|
|
@@ -41,7 +41,7 @@ export const MACRO_PRESETS = {
|
|
|
41
41
|
*/
|
|
42
42
|
steady: {
|
|
43
43
|
bornRecentBias: 0.1,
|
|
44
|
-
percentUsersBornInDataset:
|
|
44
|
+
percentUsersBornInDataset: 35,
|
|
45
45
|
preExistingSpread: 'uniform',
|
|
46
46
|
},
|
|
47
47
|
|
|
@@ -51,7 +51,7 @@ export const MACRO_PRESETS = {
|
|
|
51
51
|
*/
|
|
52
52
|
growth: {
|
|
53
53
|
bornRecentBias: 0.3,
|
|
54
|
-
percentUsersBornInDataset:
|
|
54
|
+
percentUsersBornInDataset: 60,
|
|
55
55
|
preExistingSpread: 'pinned',
|
|
56
56
|
},
|
|
57
57
|
|
|
@@ -61,7 +61,7 @@ export const MACRO_PRESETS = {
|
|
|
61
61
|
*/
|
|
62
62
|
viral: {
|
|
63
63
|
bornRecentBias: 0.6,
|
|
64
|
-
percentUsersBornInDataset:
|
|
64
|
+
percentUsersBornInDataset: 95,
|
|
65
65
|
preExistingSpread: 'pinned',
|
|
66
66
|
},
|
|
67
67
|
|
|
@@ -71,7 +71,7 @@ export const MACRO_PRESETS = {
|
|
|
71
71
|
*/
|
|
72
72
|
decline: {
|
|
73
73
|
bornRecentBias: -0.3,
|
|
74
|
-
percentUsersBornInDataset:
|
|
74
|
+
percentUsersBornInDataset: 25,
|
|
75
75
|
preExistingSpread: 'uniform',
|
|
76
76
|
},
|
|
77
77
|
};
|
package/lib/utils/utils.js
CHANGED
|
@@ -1597,6 +1597,15 @@ function assignSessionIds(events, timeoutMinutes = 30) {
|
|
|
1597
1597
|
return events;
|
|
1598
1598
|
}
|
|
1599
1599
|
|
|
1600
|
+
async function deleteFile(filePath) {
|
|
1601
|
+
if (filePath.startsWith('gs://')) {
|
|
1602
|
+
const { bucket, file } = parseGCSUri(filePath);
|
|
1603
|
+
await storage.bucket(bucket).file(file).delete({ ignoreNotFound: true });
|
|
1604
|
+
} else {
|
|
1605
|
+
await fs.promises.unlink(filePath).catch(() => {});
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1600
1609
|
export {
|
|
1601
1610
|
pick,
|
|
1602
1611
|
date,
|
|
@@ -1651,4 +1660,5 @@ export {
|
|
|
1651
1660
|
assignSessionIds,
|
|
1652
1661
|
bunchIntoSessions,
|
|
1653
1662
|
setDatasetNow,
|
|
1663
|
+
deleteFile,
|
|
1654
1664
|
};
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface Dungeon {
|
|
|
19
19
|
// ── Core Parameters ──
|
|
20
20
|
/** Optional dungeon version. Not used by the engine — serves as metadata for tracking revisions when configs are saved/shared. */
|
|
21
21
|
version?: string | number;
|
|
22
|
+
/** Optional app or dataset name. Not used by the engine — available for logging and metadata when set. */
|
|
23
|
+
appName?: string;
|
|
22
24
|
/** Mixpanel project token. If provided, data will be imported to Mixpanel after generation. */
|
|
23
25
|
token?: string;
|
|
24
26
|
/** RNG seed for reproducible output. Same seed + concurrency=1 = identical data. */
|
|
@@ -109,8 +111,10 @@ export interface Dungeon {
|
|
|
109
111
|
hasDesktopDevices?: boolean;
|
|
110
112
|
/** If true, events include browser properties. */
|
|
111
113
|
hasBrowser?: boolean;
|
|
112
|
-
/** If true (default), writes output files to ./data/. Can also be a directory path string. */
|
|
114
|
+
/** If true (default), writes output files to ./data/. Can also be a directory path string or gs:// URI. */
|
|
113
115
|
writeToDisk?: boolean | string;
|
|
116
|
+
/** If true, deletes all written files (local and GCS) at end of run regardless of import success/failure. Default: false. */
|
|
117
|
+
cleanup?: boolean;
|
|
114
118
|
/** If true, gzip-compresses output files. */
|
|
115
119
|
gzip?: boolean;
|
|
116
120
|
/** If true, prints progress to stdout during generation. */
|
|
@@ -220,7 +224,7 @@ export interface Dungeon {
|
|
|
220
224
|
// ── Distribution Controls ──
|
|
221
225
|
// These three knobs are normally set by the `macro` preset (default "flat").
|
|
222
226
|
// Setting them on the dungeon config directly overrides the preset's value.
|
|
223
|
-
/** Percentage of users whose account creation falls within the dataset window (vs. pre-existing). Default (from macro: "flat"):
|
|
227
|
+
/** Percentage of users whose account creation falls within the dataset window (vs. pre-existing). Default (from macro: "flat"): 50 */
|
|
224
228
|
percentUsersBornInDataset?: number;
|
|
225
229
|
/** Bias for birth dates of users born in dataset. -1..1; negative = early skew, positive = recent skew, 0 = uniform. Default (from macro: "flat"): 0 */
|
|
226
230
|
bornRecentBias?: number;
|
|
@@ -541,6 +545,8 @@ export interface HookedArray<T> extends Array<T> {
|
|
|
541
545
|
getWriteDir: () => string;
|
|
542
546
|
/** Absolute path (with extension) of the next batch file. */
|
|
543
547
|
getWritePath: () => string;
|
|
548
|
+
/** Returns all file paths written by this container during the current run. */
|
|
549
|
+
getWrittenFiles: () => string[];
|
|
544
550
|
/** SCD prop name this array carries (only set on SCD HookedArrays). */
|
|
545
551
|
scdKey?: string;
|
|
546
552
|
/** Entity type for SCDs ("user" or a group key). */
|