make-mp-data 1.5.56 → 2.0.1
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/.claude/settings.local.json +21 -0
- package/.gcloudignore +2 -1
- package/.vscode/launch.json +6 -17
- package/.vscode/settings.json +31 -2
- package/dungeons/media.js +371 -0
- package/index.js +353 -1766
- package/{components → lib/cli}/cli.js +25 -6
- package/lib/cloud-function.js +20 -0
- package/lib/core/config-validator.js +248 -0
- package/lib/core/context.js +180 -0
- package/lib/core/storage.js +268 -0
- package/{components → lib/data}/defaults.js +17 -14
- package/lib/generators/adspend.js +133 -0
- package/lib/generators/events.js +242 -0
- package/lib/generators/funnels.js +330 -0
- package/lib/generators/mirror.js +168 -0
- package/lib/generators/profiles.js +93 -0
- package/lib/generators/scd.js +102 -0
- package/lib/orchestrators/mixpanel-sender.js +222 -0
- package/lib/orchestrators/user-loop.js +194 -0
- package/lib/orchestrators/worker-manager.js +200 -0
- package/{components → lib/utils}/ai.js +8 -36
- package/{components → lib/utils}/chart.js +9 -9
- package/{components → lib/utils}/project.js +4 -4
- package/{components → lib/utils}/utils.js +35 -23
- package/package.json +15 -15
- package/scripts/dana.mjs +137 -0
- package/scripts/new-dungeon.sh +7 -6
- package/scripts/update-deps.sh +2 -1
- package/tests/cli.test.js +28 -25
- package/tests/e2e.test.js +38 -36
- package/tests/int.test.js +151 -56
- package/tests/testSoup.mjs +1 -1
- package/tests/unit.test.js +15 -14
- package/tsconfig.json +1 -1
- package/types.d.ts +68 -11
- package/vitest.config.js +47 -0
- package/log.json +0 -1678
- package/tests/jest.config.js +0 -47
- /package/{components → lib/utils}/prompt.txt +0 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import yargs from 'yargs';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
/** @typedef {import('../../types').Dungeon} Config */
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const packageJsonPath = path.join(__dirname, '../../package.json');
|
|
10
|
+
const { version } = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
3
11
|
|
|
4
12
|
const hero = String.raw`
|
|
5
13
|
|
|
@@ -47,7 +55,7 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
|
|
|
47
55
|
type: 'string'
|
|
48
56
|
})
|
|
49
57
|
.option("format", {
|
|
50
|
-
demandOption: false,
|
|
58
|
+
demandOption: false,
|
|
51
59
|
alias: 'f',
|
|
52
60
|
describe: 'csv or json',
|
|
53
61
|
type: 'string'
|
|
@@ -104,6 +112,14 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
|
|
|
104
112
|
type: 'boolean',
|
|
105
113
|
coerce: boolCoerce
|
|
106
114
|
})
|
|
115
|
+
.options("simple", {
|
|
116
|
+
demandOption: false,
|
|
117
|
+
default: false,
|
|
118
|
+
describe: 'use simple data model (basic events and users)',
|
|
119
|
+
alias: 'simp',
|
|
120
|
+
type: 'boolean',
|
|
121
|
+
coerce: boolCoerce
|
|
122
|
+
})
|
|
107
123
|
.option("writeToDisk", {
|
|
108
124
|
demandOption: false,
|
|
109
125
|
default: true,
|
|
@@ -141,7 +157,7 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
|
|
|
141
157
|
describe: 'create a PNG chart from data',
|
|
142
158
|
type: 'boolean',
|
|
143
159
|
coerce: boolCoerce
|
|
144
|
-
})
|
|
160
|
+
})
|
|
145
161
|
.option("hasAdSpend", {
|
|
146
162
|
alias: 'ads',
|
|
147
163
|
demandOption: false,
|
|
@@ -198,11 +214,14 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
|
|
|
198
214
|
type: 'boolean',
|
|
199
215
|
coerce: boolCoerce
|
|
200
216
|
})
|
|
201
|
-
|
|
217
|
+
|
|
202
218
|
.help()
|
|
203
219
|
.wrap(null)
|
|
204
220
|
.argv;
|
|
205
221
|
|
|
222
|
+
//cli is always verbose mode:
|
|
223
|
+
args.verbose = true;
|
|
224
|
+
|
|
206
225
|
return args;
|
|
207
226
|
|
|
208
227
|
}
|
|
@@ -218,4 +237,4 @@ function boolCoerce(value, foo) {
|
|
|
218
237
|
}
|
|
219
238
|
|
|
220
239
|
|
|
221
|
-
|
|
240
|
+
export default cliParams;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud Function Entry Point
|
|
3
|
+
* Provides a clean interface for Google Cloud Functions deployment
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @typedef {import('../types').Dungeon} Config */
|
|
7
|
+
|
|
8
|
+
import functions from '@google-cloud/functions-framework';
|
|
9
|
+
import { handleCloudFunctionEntry } from './orchestrators/worker-manager.js';
|
|
10
|
+
import main from '../index.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Cloud Function HTTP entry point
|
|
14
|
+
* Handles distributed data generation across multiple workers
|
|
15
|
+
*/
|
|
16
|
+
functions.http('entry', async (req, res) => {
|
|
17
|
+
await handleCloudFunctionEntry(req, res, main);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export { handleCloudFunctionEntry, main };
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration validation and enrichment module
|
|
3
|
+
* Extracted from index.js validateDungeonConfig function
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import dayjs from "dayjs";
|
|
7
|
+
import { makeName, clone } from "ak-tools";
|
|
8
|
+
import * as u from "../utils/utils.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Infers funnels from the provided events
|
|
12
|
+
* @param {Array} events - Array of event configurations
|
|
13
|
+
* @returns {Array} Array of inferred funnel configurations
|
|
14
|
+
*/
|
|
15
|
+
function inferFunnels(events) {
|
|
16
|
+
const createdFunnels = [];
|
|
17
|
+
const firstEvents = events.filter((e) => e.isFirstEvent).map((e) => e.event);
|
|
18
|
+
const usageEvents = events.filter((e) => !e.isFirstEvent).map((e) => e.event);
|
|
19
|
+
const numFunnelsToCreate = Math.ceil(usageEvents.length);
|
|
20
|
+
|
|
21
|
+
/** @type {import('../../types.js').Funnel} */
|
|
22
|
+
const funnelTemplate = {
|
|
23
|
+
sequence: [],
|
|
24
|
+
conversionRate: 50,
|
|
25
|
+
order: 'sequential',
|
|
26
|
+
requireRepeats: false,
|
|
27
|
+
props: {},
|
|
28
|
+
timeToConvert: 1,
|
|
29
|
+
isFirstFunnel: false,
|
|
30
|
+
weight: 1
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Create funnels for first events
|
|
34
|
+
if (firstEvents.length) {
|
|
35
|
+
for (const event of firstEvents) {
|
|
36
|
+
createdFunnels.push({
|
|
37
|
+
...clone(funnelTemplate),
|
|
38
|
+
sequence: [event],
|
|
39
|
+
isFirstFunnel: true,
|
|
40
|
+
conversionRate: 100
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// At least one funnel with all usage events
|
|
46
|
+
createdFunnels.push({ ...clone(funnelTemplate), sequence: usageEvents });
|
|
47
|
+
|
|
48
|
+
// Create random funnels for the rest
|
|
49
|
+
for (let i = 1; i < numFunnelsToCreate; i++) {
|
|
50
|
+
/** @type {import('../../types.js').Funnel} */
|
|
51
|
+
const funnel = { ...clone(funnelTemplate) };
|
|
52
|
+
funnel.conversionRate = u.integer(25, 75);
|
|
53
|
+
funnel.timeToConvert = u.integer(1, 10);
|
|
54
|
+
funnel.weight = u.integer(1, 10);
|
|
55
|
+
const sequence = u.shuffleArray(usageEvents).slice(0, u.integer(2, usageEvents.length));
|
|
56
|
+
funnel.sequence = sequence;
|
|
57
|
+
funnel.order = 'random';
|
|
58
|
+
createdFunnels.push(funnel);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return createdFunnels;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Validates and enriches a dungeon configuration object
|
|
66
|
+
* @param {Object} config - Raw configuration object
|
|
67
|
+
* @returns {Object} Validated and enriched configuration
|
|
68
|
+
*/
|
|
69
|
+
export function validateDungeonConfig(config) {
|
|
70
|
+
const chance = u.getChance();
|
|
71
|
+
|
|
72
|
+
// Extract configuration with defaults
|
|
73
|
+
let {
|
|
74
|
+
seed,
|
|
75
|
+
numEvents = 100_000,
|
|
76
|
+
numUsers = 1000,
|
|
77
|
+
numDays = 30,
|
|
78
|
+
epochStart = 0,
|
|
79
|
+
epochEnd = dayjs().unix(),
|
|
80
|
+
events = [{ event: "foo" }, { event: "bar" }, { event: "baz" }],
|
|
81
|
+
superProps = { luckyNumber: [2, 2, 4, 4, 42, 42, 42, 2, 2, 4, 4, 42, 42, 42, 420] },
|
|
82
|
+
funnels = [],
|
|
83
|
+
userProps = {
|
|
84
|
+
spiritAnimal: chance.animal.bind(chance),
|
|
85
|
+
},
|
|
86
|
+
scdProps = {},
|
|
87
|
+
mirrorProps = {},
|
|
88
|
+
groupKeys = [],
|
|
89
|
+
groupProps = {},
|
|
90
|
+
lookupTables = [],
|
|
91
|
+
hasAnonIds = false,
|
|
92
|
+
hasSessionIds = false,
|
|
93
|
+
format = "csv",
|
|
94
|
+
token = null,
|
|
95
|
+
region = "US",
|
|
96
|
+
writeToDisk = false,
|
|
97
|
+
verbose = false,
|
|
98
|
+
makeChart = false,
|
|
99
|
+
soup = {},
|
|
100
|
+
hook = (record) => record,
|
|
101
|
+
hasAdSpend = false,
|
|
102
|
+
hasCampaigns = false,
|
|
103
|
+
hasLocation = false,
|
|
104
|
+
hasAvatar = false,
|
|
105
|
+
isAnonymous = false,
|
|
106
|
+
hasBrowser = false,
|
|
107
|
+
hasAndroidDevices = false,
|
|
108
|
+
hasDesktopDevices = false,
|
|
109
|
+
hasIOSDevices = false,
|
|
110
|
+
alsoInferFunnels = false,
|
|
111
|
+
name = "",
|
|
112
|
+
batchSize = 500_000,
|
|
113
|
+
concurrency = 500
|
|
114
|
+
} = config;
|
|
115
|
+
|
|
116
|
+
// Ensure defaults for deep objects
|
|
117
|
+
if (!config.superProps) config.superProps = superProps;
|
|
118
|
+
if (!config.userProps || Object.keys(config?.userProps || {})) config.userProps = userProps;
|
|
119
|
+
|
|
120
|
+
// Setting up "TIME"
|
|
121
|
+
if (epochStart && !numDays) numDays = dayjs.unix(epochEnd).diff(dayjs.unix(epochStart), "day");
|
|
122
|
+
if (!epochStart && numDays) epochStart = dayjs.unix(epochEnd).subtract(numDays, "day").unix();
|
|
123
|
+
if (epochStart && numDays) { } // noop
|
|
124
|
+
if (!epochStart && !numDays) {
|
|
125
|
+
throw new Error("Either epochStart or numDays must be provided");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Generate simulation name
|
|
129
|
+
config.simulationName = name || makeName();
|
|
130
|
+
config.name = config.simulationName;
|
|
131
|
+
|
|
132
|
+
// Validate events
|
|
133
|
+
if (!events || !events.length) events = [{ event: "foo" }, { event: "bar" }, { event: "baz" }];
|
|
134
|
+
|
|
135
|
+
// Convert string events to objects
|
|
136
|
+
if (typeof events[0] === "string") {
|
|
137
|
+
events = events.map(e => ({ event: e }));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Handle funnel inference
|
|
141
|
+
if (alsoInferFunnels) {
|
|
142
|
+
const inferredFunnels = inferFunnels(events);
|
|
143
|
+
funnels = [...funnels, ...inferredFunnels];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Create funnel for events not in other funnels
|
|
147
|
+
const eventContainedInFunnels = Array.from(funnels.reduce((acc, f) => {
|
|
148
|
+
const events = f.sequence;
|
|
149
|
+
events.forEach(event => acc.add(event));
|
|
150
|
+
return acc;
|
|
151
|
+
}, new Set()));
|
|
152
|
+
|
|
153
|
+
const eventsNotInFunnels = events
|
|
154
|
+
.filter(e => !e.isFirstEvent)
|
|
155
|
+
.filter(e => !eventContainedInFunnels.includes(e.event))
|
|
156
|
+
.map(e => e.event);
|
|
157
|
+
|
|
158
|
+
if (eventsNotInFunnels.length) {
|
|
159
|
+
const sequence = u.shuffleArray(eventsNotInFunnels.flatMap(event => {
|
|
160
|
+
let evWeight;
|
|
161
|
+
// First check the config
|
|
162
|
+
if (config.events) {
|
|
163
|
+
evWeight = config.events.find(e => e.event === event)?.weight || 1;
|
|
164
|
+
}
|
|
165
|
+
// Fallback on default
|
|
166
|
+
else {
|
|
167
|
+
evWeight = 1;
|
|
168
|
+
}
|
|
169
|
+
return Array(evWeight).fill(event);
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
funnels.push({
|
|
173
|
+
sequence,
|
|
174
|
+
conversionRate: 50,
|
|
175
|
+
order: 'random',
|
|
176
|
+
timeToConvert: 24 * 14,
|
|
177
|
+
requireRepeats: false,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Event validation
|
|
182
|
+
const validatedEvents = u.validateEventConfig(events);
|
|
183
|
+
|
|
184
|
+
// Build final config object
|
|
185
|
+
const validatedConfig = {
|
|
186
|
+
...config,
|
|
187
|
+
concurrency,
|
|
188
|
+
funnels,
|
|
189
|
+
batchSize,
|
|
190
|
+
seed,
|
|
191
|
+
numEvents,
|
|
192
|
+
numUsers,
|
|
193
|
+
numDays,
|
|
194
|
+
epochStart,
|
|
195
|
+
epochEnd,
|
|
196
|
+
events: validatedEvents,
|
|
197
|
+
superProps,
|
|
198
|
+
userProps,
|
|
199
|
+
scdProps,
|
|
200
|
+
mirrorProps,
|
|
201
|
+
groupKeys,
|
|
202
|
+
groupProps,
|
|
203
|
+
lookupTables,
|
|
204
|
+
hasAnonIds,
|
|
205
|
+
hasSessionIds,
|
|
206
|
+
format,
|
|
207
|
+
token,
|
|
208
|
+
region,
|
|
209
|
+
writeToDisk,
|
|
210
|
+
verbose,
|
|
211
|
+
makeChart,
|
|
212
|
+
soup,
|
|
213
|
+
hook,
|
|
214
|
+
hasAdSpend,
|
|
215
|
+
hasCampaigns,
|
|
216
|
+
hasLocation,
|
|
217
|
+
hasAvatar,
|
|
218
|
+
isAnonymous,
|
|
219
|
+
hasBrowser,
|
|
220
|
+
hasAndroidDevices,
|
|
221
|
+
hasDesktopDevices,
|
|
222
|
+
hasIOSDevices,
|
|
223
|
+
simulationName: config.simulationName,
|
|
224
|
+
name: config.name
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
return validatedConfig;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Validates configuration for required fields
|
|
232
|
+
* @param {Object} config - Configuration to validate
|
|
233
|
+
* @throws {Error} If required fields are missing
|
|
234
|
+
*/
|
|
235
|
+
export function validateRequiredConfig(config) {
|
|
236
|
+
if (!config) {
|
|
237
|
+
throw new Error("Configuration is required");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (typeof config !== 'object') {
|
|
241
|
+
throw new Error("Configuration must be an object");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Could add more specific validation here
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export { inferFunnels };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context module - replaces global variables with a context object
|
|
3
|
+
* Provides centralized state management and dependency injection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @typedef {import('../../types').Dungeon} Dungeon */
|
|
7
|
+
/** @typedef {import('../../types').Storage} Storage */
|
|
8
|
+
/** @typedef {import('../../types').Context} Context */
|
|
9
|
+
/** @typedef {import('../../types').RuntimeState} RuntimeState */
|
|
10
|
+
/** @typedef {import('../../types').Defaults} Defaults */
|
|
11
|
+
|
|
12
|
+
import dayjs from "dayjs";
|
|
13
|
+
import { campaigns, devices, locations } from '../data/defaults.js';
|
|
14
|
+
import * as u from '../utils/utils.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a defaults factory function that computes weighted defaults
|
|
18
|
+
* @param {Dungeon} config - Configuration object
|
|
19
|
+
* @param {Array} campaignData - Campaign data array
|
|
20
|
+
* @returns {Defaults} Defaults object with factory functions
|
|
21
|
+
*/
|
|
22
|
+
function createDefaults(config, campaignData) {
|
|
23
|
+
const { singleCountry } = config;
|
|
24
|
+
|
|
25
|
+
// Pre-compute weighted arrays based on configuration
|
|
26
|
+
const locationsUsers = singleCountry ?
|
|
27
|
+
locations.filter(l => l.country === singleCountry) :
|
|
28
|
+
locations;
|
|
29
|
+
|
|
30
|
+
const locationsEvents = singleCountry ?
|
|
31
|
+
locations.filter(l => l.country === singleCountry) :
|
|
32
|
+
locations;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
locationsUsers: () => u.weighArray(locationsUsers),
|
|
36
|
+
locationsEvents: () => u.weighArray(locationsEvents),
|
|
37
|
+
iOSDevices: () => u.weighArray(devices.iosDevices),
|
|
38
|
+
androidDevices: () => u.weighArray(devices.androidDevices),
|
|
39
|
+
desktopDevices: () => u.weighArray(devices.desktopDevices),
|
|
40
|
+
browsers: () => u.weighArray(devices.browsers),
|
|
41
|
+
campaigns: () => u.weighArray(campaignData)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Creates a runtime state object for tracking execution state
|
|
47
|
+
* @returns {RuntimeState} Runtime state with counters and flags
|
|
48
|
+
*/
|
|
49
|
+
function createRuntimeState() {
|
|
50
|
+
return {
|
|
51
|
+
operations: 0,
|
|
52
|
+
eventCount: 0,
|
|
53
|
+
userCount: 0,
|
|
54
|
+
isBatchMode: false,
|
|
55
|
+
verbose: false,
|
|
56
|
+
isCLI: false
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Context factory that creates a complete context object for data generation
|
|
62
|
+
* @param {Dungeon} config - Validated configuration object
|
|
63
|
+
* @param {Storage|null} storage - Storage containers (optional, can be set later)
|
|
64
|
+
* @returns {Context} Context object containing all state and dependencies
|
|
65
|
+
*/
|
|
66
|
+
export function createContext(config, storage = null) {
|
|
67
|
+
// Import campaign data (could be made configurable)
|
|
68
|
+
const campaignData = campaigns;
|
|
69
|
+
|
|
70
|
+
// Create computed defaults based on config
|
|
71
|
+
const defaults = createDefaults(config, campaignData);
|
|
72
|
+
|
|
73
|
+
// Create runtime state
|
|
74
|
+
const runtime = createRuntimeState();
|
|
75
|
+
|
|
76
|
+
// Set runtime flags from config
|
|
77
|
+
runtime.verbose = config.verbose || false;
|
|
78
|
+
runtime.isBatchMode = config.batchSize && config.batchSize < config.numEvents;
|
|
79
|
+
runtime.isCLI = process.argv[1].endsWith('index.js') || process.argv[1].endsWith('cli.js');
|
|
80
|
+
|
|
81
|
+
const context = {
|
|
82
|
+
config,
|
|
83
|
+
storage,
|
|
84
|
+
defaults,
|
|
85
|
+
campaigns: campaignData,
|
|
86
|
+
runtime,
|
|
87
|
+
|
|
88
|
+
// Helper methods for updating state
|
|
89
|
+
incrementOperations() {
|
|
90
|
+
runtime.operations++;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
incrementEvents() {
|
|
94
|
+
runtime.eventCount++;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
incrementUsers() {
|
|
98
|
+
runtime.userCount++;
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
setStorage(storageObj) {
|
|
102
|
+
this.storage = storageObj;
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
// Getter methods for runtime state
|
|
106
|
+
getOperations() {
|
|
107
|
+
return runtime.operations;
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
getEventCount() {
|
|
111
|
+
return runtime.eventCount;
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
getUserCount() {
|
|
115
|
+
return runtime.userCount;
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
incrementUserCount() {
|
|
119
|
+
runtime.userCount++;
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
incrementEventCount() {
|
|
123
|
+
runtime.eventCount++;
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
isBatchMode() {
|
|
127
|
+
return runtime.isBatchMode;
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
isCLI() {
|
|
131
|
+
return runtime.isCLI;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// Time helper methods
|
|
135
|
+
getTimeShift() {
|
|
136
|
+
const actualNow = dayjs().add(2, "day");
|
|
137
|
+
return actualNow.diff(dayjs.unix(global.FIXED_NOW), "seconds");
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
getDaysShift() {
|
|
141
|
+
const actualNow = dayjs().add(2, "day");
|
|
142
|
+
return actualNow.diff(dayjs.unix(global.FIXED_NOW), "days");
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
// Time constants (previously globals)
|
|
146
|
+
FIXED_NOW: global.FIXED_NOW,
|
|
147
|
+
FIXED_BEGIN: global.FIXED_BEGIN
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return context;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Updates an existing context with new storage containers
|
|
155
|
+
* @param {Context} context - Existing context object
|
|
156
|
+
* @param {Storage} storage - New storage containers
|
|
157
|
+
* @returns {Context} Updated context object
|
|
158
|
+
*/
|
|
159
|
+
export function updateContextWithStorage(context, storage) {
|
|
160
|
+
context.storage = storage;
|
|
161
|
+
return context;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Validates that a context object has all required properties
|
|
166
|
+
* @param {Context} context - Context to validate
|
|
167
|
+
* @throws {Error} If context is missing required properties
|
|
168
|
+
*/
|
|
169
|
+
export function validateContext(context) {
|
|
170
|
+
const required = ['config', 'defaults', 'campaigns', 'runtime'];
|
|
171
|
+
const missing = required.filter(prop => !context[prop]);
|
|
172
|
+
|
|
173
|
+
if (missing.length > 0) {
|
|
174
|
+
throw new Error(`Context is missing required properties: ${missing.join(', ')}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!context.config.numUsers || !context.config.numEvents) {
|
|
178
|
+
throw new Error('Context config must have numUsers and numEvents');
|
|
179
|
+
}
|
|
180
|
+
}
|