make-mp-data 2.0.19 → 2.0.22
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/dungeons/big.js +7 -6
- package/dungeons/business.js +21 -3
- package/dungeons/experiments.js +8 -7
- package/dungeons/media.js +7 -7
- package/dungeons/sanity.js +8 -14
- package/dungeons/simple.js +1 -0
- package/dungeons/student-teacher.js +426 -0
- package/dungeons/userAgent.js +7 -7
- package/entry.js +19 -3
- package/index.js +107 -7
- package/lib/cli/cli.js +8 -0
- package/lib/core/config-validator.js +244 -218
- package/lib/core/context.js +31 -16
- package/lib/core/storage.js +61 -27
- package/lib/generators/events.js +41 -18
- package/lib/orchestrators/mixpanel-sender.js +5 -2
- package/lib/orchestrators/user-loop.js +212 -181
- package/lib/orchestrators/worker-manager.js +5 -2
- package/lib/templates/abbreviated.d.ts +159 -0
- package/lib/{data → templates}/defaults.js +2 -2
- package/lib/templates/instructions.txt +78 -0
- package/lib/templates/scratch-dungeon-template.js +116 -0
- package/lib/templates/verbose-schema.js +338 -0
- package/lib/utils/ai.js +42 -64
- package/lib/utils/chart.js +5 -0
- package/lib/utils/utils.js +116 -55
- package/package.json +9 -10
- package/types.d.ts +138 -125
- package/lib/cloud-function.js +0 -20
- /package/lib/{utils/prompt.txt → templates/prompt (old).txt} +0 -0
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
* Extracted from index.js validateDungeonConfig function
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/** @typedef {import('../../types.js').Dungeon} Dungeon */
|
|
7
|
+
/** @typedef {import('../../types.js').EventConfig} EventConfig */
|
|
8
|
+
/** @typedef {import('../../types.js').Context} Context */
|
|
9
|
+
/** @typedef {import('../../types.js').Funnel} Funnel */
|
|
10
|
+
|
|
6
11
|
import dayjs from "dayjs";
|
|
7
12
|
import { makeName } from "ak-tools";
|
|
8
13
|
import * as u from "../utils/utils.js";
|
|
@@ -10,222 +15,238 @@ import os from "os";
|
|
|
10
15
|
|
|
11
16
|
/**
|
|
12
17
|
* Infers funnels from the provided events
|
|
13
|
-
* @param {
|
|
14
|
-
* @returns {
|
|
18
|
+
* @param {EventConfig[]} events - Array of event configurations
|
|
19
|
+
* @returns {Funnel[]} Array of inferred funnel configurations
|
|
15
20
|
*/
|
|
16
21
|
function inferFunnels(events) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
22
|
+
const createdFunnels = [];
|
|
23
|
+
const firstEvents = events.filter((e) => e.isFirstEvent).map((e) => e.event);
|
|
24
|
+
const usageEvents = events.filter((e) => !e.isFirstEvent).map((e) => e.event);
|
|
25
|
+
const numFunnelsToCreate = Math.ceil(usageEvents.length);
|
|
26
|
+
|
|
27
|
+
/** @type {import('../../types.js').Funnel} */
|
|
28
|
+
const funnelTemplate = {
|
|
29
|
+
sequence: [],
|
|
30
|
+
conversionRate: 50,
|
|
31
|
+
order: 'sequential',
|
|
32
|
+
requireRepeats: false,
|
|
33
|
+
props: {},
|
|
34
|
+
timeToConvert: 1,
|
|
35
|
+
isFirstFunnel: false,
|
|
36
|
+
weight: 1
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Create funnels for first events
|
|
40
|
+
if (firstEvents.length) {
|
|
41
|
+
for (const event of firstEvents) {
|
|
42
|
+
createdFunnels.push({
|
|
43
|
+
...u.deepClone(funnelTemplate),
|
|
44
|
+
sequence: [event],
|
|
45
|
+
isFirstFunnel: true,
|
|
46
|
+
conversionRate: 100
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// At least one funnel with all usage events
|
|
52
|
+
createdFunnels.push({ ...u.deepClone(funnelTemplate), sequence: usageEvents });
|
|
53
|
+
|
|
54
|
+
// Create random funnels for the rest
|
|
55
|
+
for (let i = 1; i < numFunnelsToCreate; i++) {
|
|
56
|
+
/** @type {import('../../types.js').Funnel} */
|
|
57
|
+
const funnel = { ...u.deepClone(funnelTemplate) };
|
|
58
|
+
funnel.conversionRate = u.integer(25, 75);
|
|
59
|
+
funnel.timeToConvert = u.integer(1, 10);
|
|
60
|
+
funnel.weight = u.integer(1, 10);
|
|
61
|
+
const sequence = u.shuffleArray(usageEvents).slice(0, u.integer(2, usageEvents.length));
|
|
62
|
+
funnel.sequence = sequence;
|
|
63
|
+
funnel.order = 'random';
|
|
64
|
+
createdFunnels.push(funnel);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return createdFunnels;
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
/**
|
|
66
71
|
* Validates and enriches a dungeon configuration object
|
|
67
|
-
* @param {
|
|
68
|
-
* @returns {
|
|
72
|
+
* @param {Partial<Dungeon>} config - Raw configuration object
|
|
73
|
+
* @returns {Dungeon} Validated and enriched configuration
|
|
69
74
|
*/
|
|
70
75
|
export function validateDungeonConfig(config) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
76
|
+
const chance = u.getChance();
|
|
77
|
+
|
|
78
|
+
// Extract configuration with defaults
|
|
79
|
+
let {
|
|
80
|
+
seed,
|
|
81
|
+
numEvents = 100_000,
|
|
82
|
+
numUsers = 1000,
|
|
83
|
+
numDays = 30,
|
|
84
|
+
epochStart = 0,
|
|
85
|
+
epochEnd = dayjs().unix(),
|
|
86
|
+
events = [{ event: "foo" }, { event: "bar" }, { event: "baz" }],
|
|
87
|
+
superProps = { luckyNumber: [2, 2, 4, 4, 42, 42, 42, 2, 2, 4, 4, 42, 42, 42, 420] },
|
|
88
|
+
funnels = [],
|
|
89
|
+
userProps = {
|
|
90
|
+
spiritAnimal: chance.animal.bind(chance),
|
|
91
|
+
},
|
|
92
|
+
scdProps = {},
|
|
93
|
+
mirrorProps = {},
|
|
94
|
+
groupKeys = [],
|
|
95
|
+
groupProps = {},
|
|
96
|
+
lookupTables = [],
|
|
97
|
+
hasAnonIds = false,
|
|
98
|
+
hasSessionIds = false,
|
|
99
|
+
format = "csv",
|
|
100
|
+
token = null,
|
|
101
|
+
region = "US",
|
|
102
|
+
writeToDisk = false,
|
|
103
|
+
verbose = true,
|
|
104
|
+
makeChart = false,
|
|
105
|
+
soup = {},
|
|
106
|
+
hook = (record) => record,
|
|
107
|
+
hasAdSpend = false,
|
|
108
|
+
hasCampaigns = false,
|
|
109
|
+
hasLocation = false,
|
|
110
|
+
hasAvatar = false,
|
|
111
|
+
isAnonymous = false,
|
|
112
|
+
hasBrowser = false,
|
|
113
|
+
hasAndroidDevices = false,
|
|
114
|
+
hasDesktopDevices = false,
|
|
115
|
+
hasIOSDevices = false,
|
|
116
|
+
alsoInferFunnels = false,
|
|
117
|
+
name = "",
|
|
118
|
+
batchSize = 500_000,
|
|
119
|
+
concurrency
|
|
120
|
+
} = config;
|
|
121
|
+
|
|
122
|
+
// Set concurrency default only if not provided
|
|
123
|
+
if (concurrency === undefined || concurrency === null) {
|
|
124
|
+
concurrency = Math.min(os.cpus().length * 2, 16);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Ensure defaults for deep objects
|
|
128
|
+
if (!config.superProps) config.superProps = superProps;
|
|
129
|
+
if (!config.userProps || Object.keys(config?.userProps || {})) config.userProps = userProps;
|
|
130
|
+
|
|
131
|
+
// Setting up "TIME"
|
|
132
|
+
if (epochStart && !numDays) numDays = dayjs.unix(epochEnd).diff(dayjs.unix(epochStart), "day");
|
|
133
|
+
if (!epochStart && numDays) epochStart = dayjs.unix(epochEnd).subtract(numDays, "day").unix();
|
|
134
|
+
if (epochStart && numDays) { } // noop
|
|
135
|
+
if (!epochStart && !numDays) {
|
|
136
|
+
throw new Error("Either epochStart or numDays must be provided");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Generate simulation name
|
|
140
|
+
config.simulationName = name || makeName();
|
|
141
|
+
config.name = config.simulationName;
|
|
142
|
+
|
|
143
|
+
// Validate events
|
|
144
|
+
if (!events || !events.length) events = [{ event: "foo" }, { event: "bar" }, { event: "baz" }];
|
|
145
|
+
|
|
146
|
+
// Convert string events to objects
|
|
147
|
+
if (typeof events[0] === "string") {
|
|
148
|
+
events = events.map(e => ({ event: /** @type {string} */ (e) }));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Handle funnel inference
|
|
152
|
+
if (alsoInferFunnels) {
|
|
153
|
+
const inferredFunnels = inferFunnels(events);
|
|
154
|
+
funnels = [...funnels, ...inferredFunnels];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Create funnel for events not in other funnels
|
|
158
|
+
const eventContainedInFunnels = Array.from(funnels.reduce((acc, f) => {
|
|
159
|
+
const events = f.sequence;
|
|
160
|
+
events.forEach(event => acc.add(event));
|
|
161
|
+
return acc;
|
|
162
|
+
}, new Set()));
|
|
163
|
+
|
|
164
|
+
const eventsNotInFunnels = events
|
|
165
|
+
.filter(e => !e.isFirstEvent)
|
|
166
|
+
.filter(e => !eventContainedInFunnels.includes(e.event))
|
|
167
|
+
.map(e => e.event);
|
|
168
|
+
|
|
169
|
+
if (eventsNotInFunnels.length) {
|
|
170
|
+
const sequence = u.shuffleArray(eventsNotInFunnels.flatMap(event => {
|
|
171
|
+
let evWeight;
|
|
172
|
+
// First check the config
|
|
173
|
+
if (config.events) {
|
|
174
|
+
evWeight = config.events.find(e => e.event === event)?.weight || 1;
|
|
175
|
+
}
|
|
176
|
+
// Fallback on default
|
|
177
|
+
else {
|
|
178
|
+
evWeight = 1;
|
|
179
|
+
}
|
|
180
|
+
return Array(evWeight).fill(event);
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
funnels.push({
|
|
184
|
+
sequence,
|
|
185
|
+
conversionRate: 50,
|
|
186
|
+
order: 'random',
|
|
187
|
+
timeToConvert: 24 * 14,
|
|
188
|
+
requireRepeats: false,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ensure every event in funnel sequence exists in our eventConfig
|
|
193
|
+
const eventInFunnels = Array.from(new Set(funnels.map(funnel => funnel.sequence).flat()));
|
|
194
|
+
|
|
195
|
+
const definedEvents = events.map(e => e.event);
|
|
196
|
+
const missingEvents = eventInFunnels.filter(event => !definedEvents.includes(event));
|
|
197
|
+
if (missingEvents.length) {
|
|
198
|
+
throw new Error(`Funnel sequences contain events that are not defined in the events config:\n${missingEvents.join(', ')}\nPlease ensure all events in funnel sequences are defined in the events array.`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
// Event validation
|
|
204
|
+
const validatedEvents = u.validateEventConfig(events);
|
|
205
|
+
|
|
206
|
+
// Build final config object
|
|
207
|
+
const validatedConfig = {
|
|
208
|
+
...config,
|
|
209
|
+
concurrency,
|
|
210
|
+
funnels,
|
|
211
|
+
batchSize,
|
|
212
|
+
seed,
|
|
213
|
+
numEvents,
|
|
214
|
+
numUsers,
|
|
215
|
+
numDays,
|
|
216
|
+
epochStart,
|
|
217
|
+
epochEnd,
|
|
218
|
+
events: validatedEvents,
|
|
219
|
+
superProps,
|
|
220
|
+
userProps,
|
|
221
|
+
scdProps,
|
|
222
|
+
mirrorProps,
|
|
223
|
+
groupKeys,
|
|
224
|
+
groupProps,
|
|
225
|
+
lookupTables,
|
|
226
|
+
hasAnonIds,
|
|
227
|
+
hasSessionIds,
|
|
228
|
+
format,
|
|
229
|
+
token,
|
|
230
|
+
region,
|
|
231
|
+
writeToDisk,
|
|
232
|
+
verbose,
|
|
233
|
+
makeChart,
|
|
234
|
+
soup,
|
|
235
|
+
hook,
|
|
236
|
+
hasAdSpend,
|
|
237
|
+
hasCampaigns,
|
|
238
|
+
hasLocation,
|
|
239
|
+
hasAvatar,
|
|
240
|
+
isAnonymous,
|
|
241
|
+
hasBrowser,
|
|
242
|
+
hasAndroidDevices,
|
|
243
|
+
hasDesktopDevices,
|
|
244
|
+
hasIOSDevices,
|
|
245
|
+
simulationName: config.simulationName,
|
|
246
|
+
name: config.name
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
return validatedConfig;
|
|
229
250
|
}
|
|
230
251
|
|
|
231
252
|
/**
|
|
@@ -233,17 +254,22 @@ export function validateDungeonConfig(config) {
|
|
|
233
254
|
* @param {Object} config - Configuration to validate
|
|
234
255
|
* @throws {Error} If required fields are missing
|
|
235
256
|
*/
|
|
257
|
+
/**
|
|
258
|
+
* Validates required configuration parameters
|
|
259
|
+
* @param {Dungeon} config - Configuration object to validate
|
|
260
|
+
* @returns {boolean} True if validation passes
|
|
261
|
+
*/
|
|
236
262
|
export function validateRequiredConfig(config) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
263
|
+
if (!config) {
|
|
264
|
+
throw new Error("Configuration is required");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (typeof config !== 'object') {
|
|
268
|
+
throw new Error("Configuration must be an object");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Could add more specific validation here
|
|
272
|
+
return true;
|
|
247
273
|
}
|
|
248
274
|
|
|
249
275
|
export { inferFunnels };
|
package/lib/core/context.js
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
* Provides centralized state management and dependency injection
|
|
4
4
|
*/
|
|
5
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 */
|
|
6
|
+
/** @typedef {import('../../types.js').Dungeon} Dungeon */
|
|
7
|
+
/** @typedef {import('../../types.js').Storage} Storage */
|
|
8
|
+
/** @typedef {import('../../types.js').Context} Context */
|
|
9
|
+
/** @typedef {import('../../types.js').RuntimeState} RuntimeState */
|
|
10
|
+
/** @typedef {import('../../types.js').Defaults} Defaults */
|
|
11
11
|
|
|
12
12
|
import dayjs from "dayjs";
|
|
13
|
-
import { campaigns, devices, locations } from '../
|
|
13
|
+
import { campaigns, devices, locations } from '../templates/defaults.js';
|
|
14
14
|
import * as u from '../utils/utils.js';
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -31,14 +31,23 @@ function createDefaults(config, campaignData) {
|
|
|
31
31
|
locations.filter(l => l.country === singleCountry) :
|
|
32
32
|
locations;
|
|
33
33
|
|
|
34
|
+
// PERFORMANCE: Pre-calculate weighted arrays to avoid repeated weighArray calls
|
|
35
|
+
const weighedLocationsUsers = u.weighArray(locationsUsers);
|
|
36
|
+
const weighedLocationsEvents = u.weighArray(locationsEvents);
|
|
37
|
+
const weighedIOSDevices = u.weighArray(devices.iosDevices);
|
|
38
|
+
const weighedAndroidDevices = u.weighArray(devices.androidDevices);
|
|
39
|
+
const weighedDesktopDevices = u.weighArray(devices.desktopDevices);
|
|
40
|
+
const weighedBrowsers = u.weighArray(devices.browsers);
|
|
41
|
+
const weighedCampaigns = u.weighArray(campaignData);
|
|
42
|
+
|
|
34
43
|
return {
|
|
35
|
-
locationsUsers: () =>
|
|
36
|
-
locationsEvents: () =>
|
|
37
|
-
iOSDevices: () =>
|
|
38
|
-
androidDevices: () =>
|
|
39
|
-
desktopDevices: () =>
|
|
40
|
-
browsers: () =>
|
|
41
|
-
campaigns: () =>
|
|
44
|
+
locationsUsers: () => weighedLocationsUsers,
|
|
45
|
+
locationsEvents: () => weighedLocationsEvents,
|
|
46
|
+
iOSDevices: () => weighedIOSDevices,
|
|
47
|
+
androidDevices: () => weighedAndroidDevices,
|
|
48
|
+
desktopDevices: () => weighedDesktopDevices,
|
|
49
|
+
browsers: () => weighedBrowsers,
|
|
50
|
+
campaigns: () => weighedCampaigns
|
|
42
51
|
};
|
|
43
52
|
}
|
|
44
53
|
|
|
@@ -136,17 +145,23 @@ export function createContext(config, storage = null, isCliMode = null) {
|
|
|
136
145
|
// Time helper methods
|
|
137
146
|
getTimeShift() {
|
|
138
147
|
const actualNow = dayjs().add(2, "day");
|
|
139
|
-
return actualNow.diff(dayjs.unix(
|
|
148
|
+
return actualNow.diff(dayjs.unix(this.FIXED_NOW), "seconds");
|
|
140
149
|
},
|
|
141
150
|
|
|
142
151
|
getDaysShift() {
|
|
143
152
|
const actualNow = dayjs().add(2, "day");
|
|
144
|
-
return actualNow.diff(dayjs.unix(
|
|
153
|
+
return actualNow.diff(dayjs.unix(this.FIXED_NOW), "days");
|
|
145
154
|
},
|
|
146
155
|
|
|
147
156
|
// Time constants (previously globals)
|
|
148
157
|
FIXED_NOW: global.FIXED_NOW,
|
|
149
|
-
FIXED_BEGIN: global.FIXED_BEGIN
|
|
158
|
+
FIXED_BEGIN: global.FIXED_BEGIN,
|
|
159
|
+
|
|
160
|
+
// PERFORMANCE: Pre-calculated time shift (instead of calculating per-event)
|
|
161
|
+
TIME_SHIFT_SECONDS: (() => {
|
|
162
|
+
const actualNow = dayjs().add(2, "day");
|
|
163
|
+
return actualNow.diff(dayjs.unix(global.FIXED_NOW), "seconds");
|
|
164
|
+
})(),
|
|
150
165
|
};
|
|
151
166
|
|
|
152
167
|
return context;
|