@synode/core 1.0.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/LICENSE +35 -0
- package/dist/engine-CgLY6SKJ.cjs +593 -0
- package/dist/engine-CgLY6SKJ.cjs.map +1 -0
- package/dist/engine-SRByMZvP.mjs +515 -0
- package/dist/engine-SRByMZvP.mjs.map +1 -0
- package/dist/execution/worker.cjs +125 -0
- package/dist/execution/worker.cjs.map +1 -0
- package/dist/execution/worker.d.cts +1 -0
- package/dist/execution/worker.d.mts +1 -0
- package/dist/execution/worker.mjs +125 -0
- package/dist/execution/worker.mjs.map +1 -0
- package/dist/index.cjs +1163 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1142 -0
- package/dist/index.d.mts +1142 -0
- package/dist/index.mjs +1093 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Synode Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Digitl Cloud GmbH. All rights reserved.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person or organization
|
|
6
|
+
obtaining a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to use the Software for personal, internal, and commercial
|
|
8
|
+
purposes, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
1. PERMITTED USE. You may use, copy, and modify the Software for your own
|
|
11
|
+
personal, internal, or commercial purposes.
|
|
12
|
+
|
|
13
|
+
2. NO REDISTRIBUTION. You may not distribute, publish, sublicense, or
|
|
14
|
+
otherwise make the Software or any derivative works available to third
|
|
15
|
+
parties, whether in source code or compiled form, free of charge or for
|
|
16
|
+
a fee.
|
|
17
|
+
|
|
18
|
+
3. NO RESALE. You may not sell, rent, lease, or otherwise commercially
|
|
19
|
+
exploit the Software itself as a standalone product or as part of a
|
|
20
|
+
software distribution.
|
|
21
|
+
|
|
22
|
+
4. NO HOSTING AS A SERVICE. You may not offer the Software to third parties
|
|
23
|
+
as a hosted, managed, or software-as-a-service product where the primary
|
|
24
|
+
value derives from the Software.
|
|
25
|
+
|
|
26
|
+
5. ATTRIBUTION. You must retain this license notice and copyright notice in
|
|
27
|
+
all copies or substantial portions of the Software.
|
|
28
|
+
|
|
29
|
+
6. NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
|
30
|
+
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
31
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
32
|
+
IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES
|
|
33
|
+
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
34
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
35
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
const require_index = require('./index.cjs');
|
|
2
|
+
let __faker_js_faker = require("@faker-js/faker");
|
|
3
|
+
|
|
4
|
+
//#region src/errors.ts
|
|
5
|
+
const PATH_LABELS = [
|
|
6
|
+
"Journey",
|
|
7
|
+
"Adventure",
|
|
8
|
+
"Action",
|
|
9
|
+
"Step"
|
|
10
|
+
];
|
|
11
|
+
/**
|
|
12
|
+
* Formats an error message with a hierarchical path prefix and optional suggestion.
|
|
13
|
+
*
|
|
14
|
+
* @param message - The base error message
|
|
15
|
+
* @param path - Hierarchical path segments labeled Journey/Adventure/Action/Step
|
|
16
|
+
* @param suggestion - Optional suggestion appended on a new line
|
|
17
|
+
* @returns The formatted message string
|
|
18
|
+
*/
|
|
19
|
+
function formatErrorMessage(message, path, suggestion) {
|
|
20
|
+
let formatted = message;
|
|
21
|
+
if (path.length > 0) formatted = `[${path.map((segment, i) => `${PATH_LABELS[Math.min(i, PATH_LABELS.length - 1)]} "${segment}"`).join(" > ")}] ${message}`;
|
|
22
|
+
if (suggestion) formatted += `\n${suggestion}`;
|
|
23
|
+
return formatted;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Structured error class for all Synode validation and runtime errors.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* throw new SynodeError({
|
|
31
|
+
* code: 'INVALID_BOUNCE_CHANCE',
|
|
32
|
+
* message: 'Bounce chance must be between 0 and 1',
|
|
33
|
+
* path: ['Purchase Flow', 'Checkout', 'Submit Order'],
|
|
34
|
+
* expected: 'number between 0 and 1',
|
|
35
|
+
* received: '1.5',
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
var SynodeError = class extends Error {
|
|
40
|
+
/** Structured error code identifying the error type. */
|
|
41
|
+
code;
|
|
42
|
+
/** Hierarchical path where the error occurred. */
|
|
43
|
+
path;
|
|
44
|
+
/** Optional suggestion for fixing the error. */
|
|
45
|
+
suggestion;
|
|
46
|
+
/** The original unformatted error message. */
|
|
47
|
+
rawMessage;
|
|
48
|
+
/** Optional description of the expected value. */
|
|
49
|
+
expected;
|
|
50
|
+
/** Optional description of the actual value received. */
|
|
51
|
+
received;
|
|
52
|
+
constructor(options) {
|
|
53
|
+
const formatted = formatErrorMessage(options.message, options.path, options.suggestion);
|
|
54
|
+
super(formatted, { cause: options.cause });
|
|
55
|
+
this.name = "SynodeError";
|
|
56
|
+
this.code = options.code;
|
|
57
|
+
this.path = options.path;
|
|
58
|
+
this.suggestion = options.suggestion;
|
|
59
|
+
this.rawMessage = options.message;
|
|
60
|
+
this.expected = options.expected;
|
|
61
|
+
this.received = options.received;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Produces a structured multi-line representation of the error for human-readable output.
|
|
65
|
+
*
|
|
66
|
+
* Format:
|
|
67
|
+
* ```
|
|
68
|
+
* [ERROR_CODE] rawMessage
|
|
69
|
+
* Path: Journey 'x' > Adventure 'y' > Action 'z'
|
|
70
|
+
* Expected: what was expected
|
|
71
|
+
* Received: what was provided
|
|
72
|
+
* Fix: suggestion
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* Path is omitted when path is empty. Expected/Received are omitted when not provided.
|
|
76
|
+
* Fix is omitted when there is no suggestion.
|
|
77
|
+
*
|
|
78
|
+
* @returns A formatted string representation of the error.
|
|
79
|
+
*/
|
|
80
|
+
format() {
|
|
81
|
+
const lines = [`[${this.code}] ${this.rawMessage}`];
|
|
82
|
+
if (this.path.length > 0) {
|
|
83
|
+
const segments = this.path.map((segment, i) => `${PATH_LABELS[Math.min(i, PATH_LABELS.length - 1)]} '${segment}'`);
|
|
84
|
+
lines.push(` Path: ${segments.join(" > ")}`);
|
|
85
|
+
}
|
|
86
|
+
if (this.expected !== void 0) lines.push(` Expected: ${this.expected}`);
|
|
87
|
+
if (this.received !== void 0) lines.push(` Received: ${this.received}`);
|
|
88
|
+
if (this.suggestion) lines.push(` Fix: ${this.suggestion}`);
|
|
89
|
+
return lines.join("\n");
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Computes the Levenshtein edit distance between two strings using a space-efficient
|
|
94
|
+
* two-row approach.
|
|
95
|
+
*
|
|
96
|
+
* @param a - First string
|
|
97
|
+
* @param b - Second string
|
|
98
|
+
* @returns The minimum number of single-character edits (insertions, deletions, substitutions)
|
|
99
|
+
*/
|
|
100
|
+
function levenshtein(a, b) {
|
|
101
|
+
if (a === b) return 0;
|
|
102
|
+
if (a.length === 0) return b.length;
|
|
103
|
+
if (b.length === 0) return a.length;
|
|
104
|
+
let prev = new Array(b.length + 1);
|
|
105
|
+
let curr = new Array(b.length + 1);
|
|
106
|
+
for (let j = 0; j <= b.length; j++) prev[j] = j;
|
|
107
|
+
for (let i = 1; i <= a.length; i++) {
|
|
108
|
+
curr[0] = i;
|
|
109
|
+
for (let j = 1; j <= b.length; j++) {
|
|
110
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
111
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
112
|
+
}
|
|
113
|
+
[prev, curr] = [curr, prev];
|
|
114
|
+
}
|
|
115
|
+
return prev[b.length];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Finds the closest matching string from a list of candidates using Levenshtein distance.
|
|
119
|
+
* Returns undefined if no candidate is within the dynamic threshold.
|
|
120
|
+
*
|
|
121
|
+
* The threshold is `max(3, floor(maxLen * 0.4))` where maxLen is the longer of the
|
|
122
|
+
* input and candidate strings.
|
|
123
|
+
*
|
|
124
|
+
* @param input - The string to match against candidates
|
|
125
|
+
* @param candidates - List of valid strings to compare against
|
|
126
|
+
* @returns The closest matching candidate, or undefined if none is close enough
|
|
127
|
+
*/
|
|
128
|
+
function suggestClosest(input, candidates) {
|
|
129
|
+
if (candidates.length === 0) return void 0;
|
|
130
|
+
let bestMatch;
|
|
131
|
+
let bestDistance = Infinity;
|
|
132
|
+
for (const candidate of candidates) {
|
|
133
|
+
const distance = levenshtein(input, candidate);
|
|
134
|
+
const maxLen = Math.max(input.length, candidate.length);
|
|
135
|
+
if (distance <= Math.max(3, Math.floor(maxLen * .4)) && distance < bestDistance) {
|
|
136
|
+
bestDistance = distance;
|
|
137
|
+
bestMatch = candidate;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return bestMatch;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Builds a human-readable suggestion string for a not-found error, including
|
|
144
|
+
* the closest match (if any) and the full list of available options.
|
|
145
|
+
*
|
|
146
|
+
* @param kind - The kind of entity (e.g., "dataset", "journey")
|
|
147
|
+
* @param requested - The name that was requested but not found
|
|
148
|
+
* @param available - List of valid names
|
|
149
|
+
* @returns A suggestion string with "Did you mean..." and/or available options
|
|
150
|
+
*/
|
|
151
|
+
function buildNotFoundSuggestion(kind, requested, available) {
|
|
152
|
+
const closest = suggestClosest(requested, available);
|
|
153
|
+
const parts = [];
|
|
154
|
+
if (closest) parts.push(`Did you mean '${closest}'?`);
|
|
155
|
+
parts.push(`Available ${kind}s: [${available.join(", ")}]`);
|
|
156
|
+
return parts.join(" ");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/state/ids.ts
|
|
161
|
+
/**
|
|
162
|
+
* Default ID generator using crypto.randomUUID.
|
|
163
|
+
*/
|
|
164
|
+
const defaultIdGenerator = { generate(prefix) {
|
|
165
|
+
const uuid = crypto.randomUUID();
|
|
166
|
+
return prefix ? `${prefix}_${uuid}` : uuid;
|
|
167
|
+
} };
|
|
168
|
+
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region src/state/context.ts
|
|
171
|
+
var SynodeContext = class {
|
|
172
|
+
state = /* @__PURE__ */ new Map();
|
|
173
|
+
fieldMetadata = /* @__PURE__ */ new Map();
|
|
174
|
+
currentTime;
|
|
175
|
+
_userId;
|
|
176
|
+
_sessionId;
|
|
177
|
+
_faker;
|
|
178
|
+
completedJourneys = /* @__PURE__ */ new Set();
|
|
179
|
+
datasets = /* @__PURE__ */ new Map();
|
|
180
|
+
constructor(startTime = /* @__PURE__ */ new Date(), idGenerator = defaultIdGenerator, locale = "en") {
|
|
181
|
+
this.idGenerator = idGenerator;
|
|
182
|
+
this.locale = locale;
|
|
183
|
+
this.currentTime = new Date(startTime);
|
|
184
|
+
this._userId = idGenerator.generate("user");
|
|
185
|
+
this._sessionId = idGenerator.generate("session");
|
|
186
|
+
const localeDef = __faker_js_faker.allLocales[locale];
|
|
187
|
+
this._faker = new __faker_js_faker.Faker({ locale: localeDef });
|
|
188
|
+
}
|
|
189
|
+
get faker() {
|
|
190
|
+
return this._faker;
|
|
191
|
+
}
|
|
192
|
+
get userId() {
|
|
193
|
+
return this._userId;
|
|
194
|
+
}
|
|
195
|
+
get sessionId() {
|
|
196
|
+
return this._sessionId;
|
|
197
|
+
}
|
|
198
|
+
get(key) {
|
|
199
|
+
return this.state.get(key);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Set a context field value with optional scope for automatic cleanup.
|
|
203
|
+
*
|
|
204
|
+
* @param key - The field name
|
|
205
|
+
* @param value - The field value
|
|
206
|
+
* @param options - Optional settings including lifecycle scope
|
|
207
|
+
*
|
|
208
|
+
* @remarks
|
|
209
|
+
* When calling `set()` multiple times on the same field with different scopes,
|
|
210
|
+
* the most recent scope setting will be used. For example, if a field is first
|
|
211
|
+
* set with `scope: 'adventure'` and later set again with `scope: 'action'`,
|
|
212
|
+
* it will be cleared at the end of the action rather than the adventure.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```typescript
|
|
216
|
+
* ctx.set('cartItems', [], { scope: 'adventure' });
|
|
217
|
+
* // Later in the same adventure...
|
|
218
|
+
* ctx.set('cartItems', ['item1'], { scope: 'action' }); // Now scoped to action
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
set(key, value, options) {
|
|
222
|
+
this.state.set(key, value);
|
|
223
|
+
if (options?.scope) this.fieldMetadata.set(key, { scope: options.scope });
|
|
224
|
+
else this.fieldMetadata.delete(key);
|
|
225
|
+
}
|
|
226
|
+
now() {
|
|
227
|
+
return new Date(this.currentTime);
|
|
228
|
+
}
|
|
229
|
+
generateId(prefix) {
|
|
230
|
+
return this.idGenerator.generate(prefix);
|
|
231
|
+
}
|
|
232
|
+
hasCompletedJourney(journeyId) {
|
|
233
|
+
return this.completedJourneys.has(journeyId);
|
|
234
|
+
}
|
|
235
|
+
markJourneyComplete(journeyId) {
|
|
236
|
+
this.completedJourneys.add(journeyId);
|
|
237
|
+
}
|
|
238
|
+
dataset(id) {
|
|
239
|
+
const ds = this.datasets.get(id);
|
|
240
|
+
if (!ds) {
|
|
241
|
+
const available = [...this.datasets.keys()];
|
|
242
|
+
const suggestion = available.length > 0 ? buildNotFoundSuggestion("dataset", id, available) : void 0;
|
|
243
|
+
throw new SynodeError({
|
|
244
|
+
code: "DATASET_NOT_FOUND",
|
|
245
|
+
message: `Dataset '${id}' not found in context`,
|
|
246
|
+
path: [],
|
|
247
|
+
suggestion
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return new DatasetHandleImpl(ds);
|
|
251
|
+
}
|
|
252
|
+
typedDataset(id) {
|
|
253
|
+
const ds = this.datasets.get(id);
|
|
254
|
+
if (!ds) {
|
|
255
|
+
const available = [...this.datasets.keys()];
|
|
256
|
+
const suggestion = available.length > 0 ? buildNotFoundSuggestion("dataset", id, available) : void 0;
|
|
257
|
+
throw new SynodeError({
|
|
258
|
+
code: "DATASET_NOT_FOUND",
|
|
259
|
+
message: `Dataset '${id}' not found in context`,
|
|
260
|
+
path: [],
|
|
261
|
+
suggestion
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return new DatasetHandleImpl(ds);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Internal: Register a dataset for use in this context.
|
|
268
|
+
*/
|
|
269
|
+
registerDataset(dataset) {
|
|
270
|
+
this.datasets.set(dataset.id, dataset);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Internal: Advance the simulation time.
|
|
274
|
+
*/
|
|
275
|
+
advanceTime(ms) {
|
|
276
|
+
this.currentTime = new Date(this.currentTime.getTime() + ms);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Internal: Rotate the session ID.
|
|
280
|
+
*/
|
|
281
|
+
rotateSession() {
|
|
282
|
+
this._sessionId = this.idGenerator.generate("session");
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Internal: Clear all fields with the specified scope.
|
|
286
|
+
* Called by the engine when a scope ends.
|
|
287
|
+
*/
|
|
288
|
+
clearScope(scope) {
|
|
289
|
+
const keysToDelete = [];
|
|
290
|
+
for (const [key, metadata] of this.fieldMetadata.entries()) if (metadata.scope === scope) keysToDelete.push(key);
|
|
291
|
+
for (const key of keysToDelete) {
|
|
292
|
+
this.state.delete(key);
|
|
293
|
+
this.fieldMetadata.delete(key);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
/**
|
|
298
|
+
* Internal implementation of DatasetHandle.
|
|
299
|
+
*/
|
|
300
|
+
var DatasetHandleImpl = class {
|
|
301
|
+
constructor(dataset) {
|
|
302
|
+
this.dataset = dataset;
|
|
303
|
+
}
|
|
304
|
+
randomRow() {
|
|
305
|
+
if (this.dataset.rows.length === 0) throw new Error(`Dataset '${this.dataset.id}' is empty.`);
|
|
306
|
+
const index = Math.floor(Math.random() * this.dataset.rows.length);
|
|
307
|
+
return this.dataset.rows[index];
|
|
308
|
+
}
|
|
309
|
+
getRowById(id) {
|
|
310
|
+
return this.dataset.rows.find((row) => {
|
|
311
|
+
for (const key of Object.keys(row)) if (row[key] === id) return true;
|
|
312
|
+
return false;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
getRowByIndex(index) {
|
|
316
|
+
return this.dataset.rows[index];
|
|
317
|
+
}
|
|
318
|
+
getAllRows() {
|
|
319
|
+
return [...this.dataset.rows];
|
|
320
|
+
}
|
|
321
|
+
size() {
|
|
322
|
+
return this.dataset.rows.length;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
//#endregion
|
|
327
|
+
//#region src/generators/persona.ts
|
|
328
|
+
/**
|
|
329
|
+
* Defines a new persona.
|
|
330
|
+
*/
|
|
331
|
+
function definePersona(config) {
|
|
332
|
+
return config;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Generates a concrete persona instance from a definition.
|
|
336
|
+
* This resolves all dynamic fields and weighted distributions.
|
|
337
|
+
*/
|
|
338
|
+
async function generatePersona(definition, baseContext) {
|
|
339
|
+
const attributes = {};
|
|
340
|
+
for (const [key, generator] of Object.entries(definition.attributes)) if (typeof generator === "function") attributes[key] = await generator(baseContext, attributes);
|
|
341
|
+
else attributes[key] = generator;
|
|
342
|
+
return {
|
|
343
|
+
id: definition.id,
|
|
344
|
+
name: definition.name,
|
|
345
|
+
attributes
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
//#endregion
|
|
350
|
+
//#region src/generators/dataset.ts
|
|
351
|
+
/**
|
|
352
|
+
* Defines a new dataset with type inference support.
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* ```typescript
|
|
356
|
+
* const products = defineDataset({
|
|
357
|
+
* id: 'products',
|
|
358
|
+
* name: 'Products',
|
|
359
|
+
* count: 100,
|
|
360
|
+
* fields: {
|
|
361
|
+
* id: (ctx, row) => `prod-${row.index}`,
|
|
362
|
+
* name: (ctx) => ctx.faker.commerce.productName(),
|
|
363
|
+
* price: (ctx) => ctx.faker.number.float({ min: 10, max: 500 }),
|
|
364
|
+
* },
|
|
365
|
+
* });
|
|
366
|
+
*
|
|
367
|
+
* // Type inference works automatically
|
|
368
|
+
* type Product = InferDatasetRow<typeof products>;
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
371
|
+
function defineDataset(config) {
|
|
372
|
+
return config;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Generates a concrete dataset from a definition.
|
|
376
|
+
* @param definition The dataset definition.
|
|
377
|
+
* @param context Context providing access to faker, other datasets, and user data.
|
|
378
|
+
*/
|
|
379
|
+
async function generateDataset(definition, context) {
|
|
380
|
+
if (!Number.isFinite(definition.count) || definition.count < 0 || definition.count > 1e7) throw new Error(`Dataset '${definition.id}' count must be a finite number between 0 and 10,000,000, got ${String(definition.count)}`);
|
|
381
|
+
const rows = [];
|
|
382
|
+
for (let index = 0; index < definition.count; index++) {
|
|
383
|
+
const row = {};
|
|
384
|
+
for (const [key, generator] of Object.entries(definition.fields)) if (typeof generator === "function") row[key] = await generator(context, {
|
|
385
|
+
index,
|
|
386
|
+
data: row
|
|
387
|
+
});
|
|
388
|
+
else row[key] = generator;
|
|
389
|
+
rows.push(row);
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
id: definition.id,
|
|
393
|
+
name: definition.name,
|
|
394
|
+
rows
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
//#endregion
|
|
399
|
+
//#region src/monitoring/timing.ts
|
|
400
|
+
/**
|
|
401
|
+
* Generates a delay in milliseconds based on a time span configuration.
|
|
402
|
+
*/
|
|
403
|
+
function generateDelay(timeSpan) {
|
|
404
|
+
const { min, max, distribution = "uniform" } = timeSpan;
|
|
405
|
+
switch (distribution) {
|
|
406
|
+
case "uniform": return min + Math.random() * (max - min);
|
|
407
|
+
case "gaussian": {
|
|
408
|
+
const mean = (min + max) / 2;
|
|
409
|
+
const stdDev = (max - min) / 6;
|
|
410
|
+
const u1 = Math.random();
|
|
411
|
+
const u2 = Math.random();
|
|
412
|
+
const value = mean + Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2) * stdDev;
|
|
413
|
+
return Math.max(min, Math.min(max, value));
|
|
414
|
+
}
|
|
415
|
+
case "exponential": {
|
|
416
|
+
const lambda = 1 / (max - min);
|
|
417
|
+
const value = min - Math.log(1 - Math.random()) / lambda;
|
|
418
|
+
return Math.min(max, value);
|
|
419
|
+
}
|
|
420
|
+
default: return min + Math.random() * (max - min);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Checks if a bounce should occur based on probability.
|
|
425
|
+
*/
|
|
426
|
+
function shouldBounce(bounceChance) {
|
|
427
|
+
if (!bounceChance) return false;
|
|
428
|
+
return Math.random() < bounceChance;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
//#endregion
|
|
432
|
+
//#region src/execution/engine.ts
|
|
433
|
+
var Engine = class {
|
|
434
|
+
constructor(journey) {
|
|
435
|
+
this.journey = journey;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Executes the journey and yields events.
|
|
439
|
+
* @param context Optional context to use. If not provided, a new one is created.
|
|
440
|
+
*/
|
|
441
|
+
async *run(context) {
|
|
442
|
+
const ctx = context ?? new SynodeContext();
|
|
443
|
+
ctx.rotateSession();
|
|
444
|
+
if (this.journey.requires) {
|
|
445
|
+
for (const requiredJourneyId of this.journey.requires) if (!ctx.hasCompletedJourney(requiredJourneyId)) return;
|
|
446
|
+
}
|
|
447
|
+
if (shouldBounce(this.journey.bounceChance)) {
|
|
448
|
+
if (this.journey.suppressionPeriod) {
|
|
449
|
+
const suppressionDelay = generateDelay(this.journey.suppressionPeriod);
|
|
450
|
+
ctx.advanceTime(suppressionDelay);
|
|
451
|
+
}
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
for (const adventure of this.journey.adventures) {
|
|
455
|
+
if (shouldBounce(adventure.bounceChance)) if (adventure.onBounce === "skip") {
|
|
456
|
+
ctx.clearScope("adventure");
|
|
457
|
+
continue;
|
|
458
|
+
} else {
|
|
459
|
+
ctx.clearScope("adventure");
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
for (const action of adventure.actions) {
|
|
463
|
+
if (shouldBounce(action.bounceChance)) {
|
|
464
|
+
ctx.clearScope("action");
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
if (adventure.timeSpan) {
|
|
468
|
+
const delay = generateDelay(adventure.timeSpan);
|
|
469
|
+
ctx.advanceTime(delay);
|
|
470
|
+
}
|
|
471
|
+
const actionPath = [
|
|
472
|
+
this.journey.id,
|
|
473
|
+
adventure.id,
|
|
474
|
+
action.id
|
|
475
|
+
];
|
|
476
|
+
let events;
|
|
477
|
+
try {
|
|
478
|
+
events = await action.handler(ctx);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
if (err instanceof SynodeError) throw err;
|
|
481
|
+
throw new SynodeError({
|
|
482
|
+
code: "HANDLER_ERROR",
|
|
483
|
+
message: `${err instanceof Error ? err.constructor.name : "UnknownError"}: ${err instanceof Error ? err.message : String(err)} (userId: ${ctx.userId}, sessionId: ${ctx.sessionId})`,
|
|
484
|
+
path: actionPath,
|
|
485
|
+
cause: err
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
if (!Array.isArray(events)) throw new SynodeError({
|
|
489
|
+
code: "INVALID_HANDLER_RETURN",
|
|
490
|
+
message: `Action handler must return an array of Events, got ${typeof events}`,
|
|
491
|
+
path: actionPath
|
|
492
|
+
});
|
|
493
|
+
for (let i = 0; i < events.length; i++) {
|
|
494
|
+
if (i > 0 && action.timeSpan) {
|
|
495
|
+
const delay = generateDelay(action.timeSpan);
|
|
496
|
+
ctx.advanceTime(delay);
|
|
497
|
+
events[i].timestamp = ctx.now();
|
|
498
|
+
}
|
|
499
|
+
yield events[i];
|
|
500
|
+
}
|
|
501
|
+
ctx.clearScope("action");
|
|
502
|
+
}
|
|
503
|
+
ctx.clearScope("adventure");
|
|
504
|
+
}
|
|
505
|
+
ctx.markJourneyComplete(this.journey.id);
|
|
506
|
+
ctx.clearScope("journey");
|
|
507
|
+
if (this.journey.suppressionPeriod) {
|
|
508
|
+
const suppressionDelay = generateDelay(this.journey.suppressionPeriod);
|
|
509
|
+
ctx.advanceTime(suppressionDelay);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
//#endregion
|
|
515
|
+
Object.defineProperty(exports, 'Engine', {
|
|
516
|
+
enumerable: true,
|
|
517
|
+
get: function () {
|
|
518
|
+
return Engine;
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
Object.defineProperty(exports, 'SynodeContext', {
|
|
522
|
+
enumerable: true,
|
|
523
|
+
get: function () {
|
|
524
|
+
return SynodeContext;
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
Object.defineProperty(exports, 'SynodeError', {
|
|
528
|
+
enumerable: true,
|
|
529
|
+
get: function () {
|
|
530
|
+
return SynodeError;
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
Object.defineProperty(exports, 'buildNotFoundSuggestion', {
|
|
534
|
+
enumerable: true,
|
|
535
|
+
get: function () {
|
|
536
|
+
return buildNotFoundSuggestion;
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
Object.defineProperty(exports, 'defineDataset', {
|
|
540
|
+
enumerable: true,
|
|
541
|
+
get: function () {
|
|
542
|
+
return defineDataset;
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
Object.defineProperty(exports, 'definePersona', {
|
|
546
|
+
enumerable: true,
|
|
547
|
+
get: function () {
|
|
548
|
+
return definePersona;
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
Object.defineProperty(exports, 'formatErrorMessage', {
|
|
552
|
+
enumerable: true,
|
|
553
|
+
get: function () {
|
|
554
|
+
return formatErrorMessage;
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
Object.defineProperty(exports, 'generateDataset', {
|
|
558
|
+
enumerable: true,
|
|
559
|
+
get: function () {
|
|
560
|
+
return generateDataset;
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
Object.defineProperty(exports, 'generateDelay', {
|
|
564
|
+
enumerable: true,
|
|
565
|
+
get: function () {
|
|
566
|
+
return generateDelay;
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
Object.defineProperty(exports, 'generatePersona', {
|
|
570
|
+
enumerable: true,
|
|
571
|
+
get: function () {
|
|
572
|
+
return generatePersona;
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
Object.defineProperty(exports, 'levenshtein', {
|
|
576
|
+
enumerable: true,
|
|
577
|
+
get: function () {
|
|
578
|
+
return levenshtein;
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
Object.defineProperty(exports, 'shouldBounce', {
|
|
582
|
+
enumerable: true,
|
|
583
|
+
get: function () {
|
|
584
|
+
return shouldBounce;
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
Object.defineProperty(exports, 'suggestClosest', {
|
|
588
|
+
enumerable: true,
|
|
589
|
+
get: function () {
|
|
590
|
+
return suggestClosest;
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
//# sourceMappingURL=engine-CgLY6SKJ.cjs.map
|