@you-agent-factory/factory-emulator 0.0.0 → 0.0.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/LICENSE.md +19 -1
- package/README.md +305 -286
- package/dist/compatibility.d.ts +19 -0
- package/dist/compatibility.js +156 -0
- package/dist/event-sink.d.ts +29 -0
- package/dist/event-sink.js +58 -0
- package/dist/generated/scenario-schema.d.ts +268 -0
- package/dist/generated/scenario-schema.js +319 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/logical-move.d.ts +10 -0
- package/dist/logical-move.js +18 -0
- package/dist/recording-sink.d.ts +22 -0
- package/dist/recording-sink.js +122 -0
- package/dist/runtime-reference-conformance.d.ts +20 -0
- package/dist/runtime-reference-conformance.js +138 -0
- package/dist/runtime-reference-evidence.d.ts +9 -0
- package/dist/runtime-reference-evidence.js +193 -0
- package/dist/runtime-reference-fixtures.d.ts +7 -0
- package/dist/runtime-reference-fixtures.js +308 -0
- package/dist/runtime-reference.d.ts +48 -0
- package/dist/runtime-reference.js +143 -0
- package/dist/scenario-contracts.d.ts +81 -0
- package/dist/scenario-contracts.js +11 -0
- package/dist/scenario.d.ts +271 -0
- package/dist/scenario.js +333 -0
- package/dist/scheduler.d.ts +24 -0
- package/dist/scheduler.js +104 -0
- package/dist/session-contracts.d.ts +262 -0
- package/dist/session-contracts.js +74 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +1490 -0
- package/dist/submission-replay.d.ts +14 -0
- package/dist/submission-replay.js +96 -0
- package/dist/submission-validation.d.ts +3 -0
- package/dist/submission-validation.js +113 -0
- package/examples/customer-support.scenario.v1.json +45 -0
- package/package.json +44 -22
- package/schema/scenario.schema.json +171 -0
- package/generated/factory-emulator-scenario.schema.json +0 -191
- package/generated/factory.schema.json +0 -2606
- package/src/contracts.ts +0 -41
- package/src/data-error.js +0 -21
- package/src/data-only.js +0 -208
- package/src/emulator.js +0 -195
- package/src/emulator.ts +0 -86
- package/src/examples.js +0 -86
- package/src/examples.ts +0 -11
- package/src/generated/factory-schema.d.ts +0 -3
- package/src/generated/factory-schema.js +0 -5
- package/src/generated/scenario-schema.d.ts +0 -4
- package/src/generated/scenario-schema.js +0 -6
- package/src/generated/scenario.ts +0 -103
- package/src/identity.js +0 -55
- package/src/index.js +0 -34
- package/src/index.ts +0 -102
- package/src/limits.js +0 -125
- package/src/parser.js +0 -113
- package/src/parser.ts +0 -56
- package/src/scheduler.js +0 -374
- package/src/semantics.js +0 -354
- package/src/semantics.ts +0 -38
- package/src/session.js +0 -1201
- package/src/session.ts +0 -324
- package/src/sinks.js +0 -118
- package/src/sinks.ts +0 -56
- package/src/support.js +0 -334
- package/src/support.ts +0 -15
- package/src/virtual-time.js +0 -36
package/src/session.js
DELETED
|
@@ -1,1201 +0,0 @@
|
|
|
1
|
-
import { parseEmulatorScenario } from "./parser.js";
|
|
2
|
-
import {
|
|
3
|
-
canonicalStringify,
|
|
4
|
-
deriveFactoryEmulatorIdentity,
|
|
5
|
-
} from "./identity.js";
|
|
6
|
-
import {
|
|
7
|
-
calculateNextSchedulerBatch,
|
|
8
|
-
inspectNextSchedulerBatch,
|
|
9
|
-
} from "./scheduler.js";
|
|
10
|
-
import {
|
|
11
|
-
DEFAULT_FACTORY_EMULATOR_LIMITS,
|
|
12
|
-
FACTORY_EMULATOR_LIMIT_HARD_CAPS,
|
|
13
|
-
budgetExceededDiagnostic,
|
|
14
|
-
normalizeFactoryEmulatorLimits,
|
|
15
|
-
synchronousWorkLimitDiagnostic,
|
|
16
|
-
zeroDurationCycleDiagnostic,
|
|
17
|
-
} from "./limits.js";
|
|
18
|
-
import { selectEmulatorRule } from "./semantics.js";
|
|
19
|
-
import {
|
|
20
|
-
FactoryEmulatorDurationError,
|
|
21
|
-
stateAtElapsed,
|
|
22
|
-
validateDuration,
|
|
23
|
-
virtualTimeAt,
|
|
24
|
-
} from "./virtual-time.js";
|
|
25
|
-
import { defineDataError } from "./data-error.js";
|
|
26
|
-
import { dataOnlyDiagnostics } from "./data-only.js";
|
|
27
|
-
|
|
28
|
-
export { FactoryEmulatorDurationError } from "./virtual-time.js";
|
|
29
|
-
export {
|
|
30
|
-
DEFAULT_FACTORY_EMULATOR_LIMITS,
|
|
31
|
-
FACTORY_EMULATOR_LIMIT_HARD_CAPS,
|
|
32
|
-
} from "./limits.js";
|
|
33
|
-
|
|
34
|
-
const EVENT_SCHEMA_VERSION = "agent-factory.event.v1";
|
|
35
|
-
const WORK_REQUEST_TYPE = "FACTORY_REQUEST_BATCH";
|
|
36
|
-
const UNPRINTABLE_SINK_ERROR_MESSAGE = "Sink rejected with an unprintable value";
|
|
37
|
-
|
|
38
|
-
export const FactoryEmulatorConfigurationError = defineDataError(
|
|
39
|
-
"FactoryEmulatorConfigurationError",
|
|
40
|
-
"INVALID_CONFIGURATION",
|
|
41
|
-
{
|
|
42
|
-
message: () => "Factory emulator configuration is not valid or supported",
|
|
43
|
-
details: (diagnostics) => ({ diagnostics: copy(diagnostics) }),
|
|
44
|
-
},
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
export const FactoryEmulatorLifecycleError = defineDataError(
|
|
48
|
-
"FactoryEmulatorLifecycleError",
|
|
49
|
-
"INVALID_LIFECYCLE",
|
|
50
|
-
{
|
|
51
|
-
message: (command, phase) =>
|
|
52
|
-
`Factory emulator cannot ${command} while lifecycle phase is ${phase}`,
|
|
53
|
-
details: (command, phase) => ({ command, phase }),
|
|
54
|
-
},
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
export const FactoryEmulatorSubmissionError = defineDataError(
|
|
58
|
-
"FactoryEmulatorSubmissionError",
|
|
59
|
-
"INVALID_SUBMISSION",
|
|
60
|
-
{
|
|
61
|
-
message: () => "Factory emulator submission is not valid",
|
|
62
|
-
details: (diagnostics) => ({ diagnostics: copy(diagnostics) }),
|
|
63
|
-
},
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
export const FactoryEmulatorPendingCommandError = defineDataError(
|
|
67
|
-
"FactoryEmulatorPendingCommandError",
|
|
68
|
-
"PENDING_TRANSACTION",
|
|
69
|
-
{
|
|
70
|
-
message: (attemptedCommand, pendingCommand) =>
|
|
71
|
-
`Factory emulator cannot ${attemptedCommand} while ${pendingCommand} has a rejected transaction`,
|
|
72
|
-
details: (attemptedCommand, pendingCommand) => ({ attemptedCommand, pendingCommand }),
|
|
73
|
-
},
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
export const FactoryEmulatorExecutionPausedError = defineDataError(
|
|
77
|
-
"FactoryEmulatorExecutionPausedError",
|
|
78
|
-
"EXECUTION_PAUSED",
|
|
79
|
-
{
|
|
80
|
-
message: (diagnostic) => `Factory emulator execution paused: ${diagnostic.kind}`,
|
|
81
|
-
details: (diagnostic) => ({ diagnostic: copy(diagnostic) }),
|
|
82
|
-
},
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Creates one caller-owned deterministic Factory emulator session.
|
|
87
|
-
*
|
|
88
|
-
* Validation and event calculation happen before the first sink write. The
|
|
89
|
-
* immutable inputs are detached at construction, and all runtime state is
|
|
90
|
-
* local to the returned session.
|
|
91
|
-
*/
|
|
92
|
-
export function createFactoryEmulatorSession({
|
|
93
|
-
factory,
|
|
94
|
-
scenario,
|
|
95
|
-
sink,
|
|
96
|
-
limits,
|
|
97
|
-
yieldControl = defaultYieldControl,
|
|
98
|
-
}) {
|
|
99
|
-
if (!sink || typeof sink.write !== "function" || typeof sink.close !== "function") {
|
|
100
|
-
throw new TypeError("sink must provide write and close functions");
|
|
101
|
-
}
|
|
102
|
-
if (typeof yieldControl !== "function") {
|
|
103
|
-
throw new TypeError("yieldControl must be a function");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const configurationDiagnostics = [
|
|
107
|
-
...(limits === undefined
|
|
108
|
-
? []
|
|
109
|
-
: dataOnlyDiagnostics(limits, {
|
|
110
|
-
code: "INVALID_LIMIT_CONFIGURATION",
|
|
111
|
-
rootPath: "/limits",
|
|
112
|
-
})),
|
|
113
|
-
];
|
|
114
|
-
if (configurationDiagnostics.length > 0) {
|
|
115
|
-
throw new FactoryEmulatorConfigurationError(configurationDiagnostics);
|
|
116
|
-
}
|
|
117
|
-
const configuredLimitInput = limits === undefined
|
|
118
|
-
? undefined
|
|
119
|
-
: copyConfiguration(limits, "INVALID_LIMIT_CONFIGURATION", "/limits");
|
|
120
|
-
const normalizedLimits = normalizeFactoryEmulatorLimits(configuredLimitInput);
|
|
121
|
-
if (!normalizedLimits.success) {
|
|
122
|
-
throw new FactoryEmulatorConfigurationError(normalizedLimits.diagnostics);
|
|
123
|
-
}
|
|
124
|
-
const configuredLimits = normalizedLimits.limits;
|
|
125
|
-
const initialSubmissionCount = submissionBatchLengthFromScenario(scenario);
|
|
126
|
-
if (initialSubmissionCount > configuredLimits.maxSynchronousWorkItems) {
|
|
127
|
-
throw new FactoryEmulatorConfigurationError([
|
|
128
|
-
synchronousInputConfigurationDiagnostic(
|
|
129
|
-
configuredLimits.maxSynchronousWorkItems,
|
|
130
|
-
initialSubmissionCount,
|
|
131
|
-
),
|
|
132
|
-
]);
|
|
133
|
-
}
|
|
134
|
-
configurationDiagnostics.push(
|
|
135
|
-
...dataOnlyDiagnostics(scenario, { code: "INVALID_SCENARIO_SHAPE" }),
|
|
136
|
-
...dataOnlyDiagnostics(factory, { code: "INVALID_FACTORY_DEFINITION" }),
|
|
137
|
-
);
|
|
138
|
-
if (configurationDiagnostics.length > 0) {
|
|
139
|
-
throw new FactoryEmulatorConfigurationError(configurationDiagnostics);
|
|
140
|
-
}
|
|
141
|
-
const configuredFactory = copyConfiguration(
|
|
142
|
-
factory,
|
|
143
|
-
"INVALID_FACTORY_DEFINITION",
|
|
144
|
-
"/",
|
|
145
|
-
);
|
|
146
|
-
const configuredScenario = copyConfiguration(
|
|
147
|
-
scenario,
|
|
148
|
-
"INVALID_SCENARIO_SHAPE",
|
|
149
|
-
"/",
|
|
150
|
-
);
|
|
151
|
-
let committedState = preStartState();
|
|
152
|
-
let commandInProgress;
|
|
153
|
-
let pendingTransaction;
|
|
154
|
-
let runtimeError;
|
|
155
|
-
|
|
156
|
-
function assertCommandAvailable(command) {
|
|
157
|
-
if (commandInProgress !== undefined) {
|
|
158
|
-
throw new FactoryEmulatorLifecycleError(command, commandInProgress);
|
|
159
|
-
}
|
|
160
|
-
if (pendingTransaction !== undefined && pendingTransaction.command !== command) {
|
|
161
|
-
throw new FactoryEmulatorPendingCommandError(command, pendingTransaction.command);
|
|
162
|
-
}
|
|
163
|
-
if (runtimeError?.code === "EXECUTION_PAUSED" && command !== "close") {
|
|
164
|
-
throw new FactoryEmulatorExecutionPausedError(runtimeError.diagnostic);
|
|
165
|
-
}
|
|
166
|
-
if (
|
|
167
|
-
pendingTransaction === undefined &&
|
|
168
|
-
committedState.lifecycle !== (command === "start" ? "pre-start" : "started")
|
|
169
|
-
) {
|
|
170
|
-
throw new FactoryEmulatorLifecycleError(command, committedState.lifecycle);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function beginCommand(command, key, phase) {
|
|
175
|
-
assertCommandAvailable(command);
|
|
176
|
-
if (pendingTransaction !== undefined) {
|
|
177
|
-
if (pendingTransaction.command !== command || pendingTransaction.key !== key) {
|
|
178
|
-
throw new FactoryEmulatorPendingCommandError(command, pendingTransaction.command);
|
|
179
|
-
}
|
|
180
|
-
commandInProgress = phase;
|
|
181
|
-
return pendingTransaction;
|
|
182
|
-
}
|
|
183
|
-
if (committedState.lifecycle !== (command === "start" ? "pre-start" : "started")) {
|
|
184
|
-
throw new FactoryEmulatorLifecycleError(command, committedState.lifecycle);
|
|
185
|
-
}
|
|
186
|
-
commandInProgress = phase;
|
|
187
|
-
return undefined;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
async function writeCalculation(command, key, calculation, progress = {}) {
|
|
191
|
-
assertCalculationWithinBudgets(command, calculation);
|
|
192
|
-
try {
|
|
193
|
-
const receipt = await sink.write(copy(calculation.batch));
|
|
194
|
-
if (receipt?.status !== "accepted") {
|
|
195
|
-
throw new TypeError("sink.write must return an accepted receipt");
|
|
196
|
-
}
|
|
197
|
-
committedState = calculation.state;
|
|
198
|
-
pendingTransaction = undefined;
|
|
199
|
-
runtimeError = undefined;
|
|
200
|
-
} catch (error) {
|
|
201
|
-
pendingTransaction = {
|
|
202
|
-
command,
|
|
203
|
-
key,
|
|
204
|
-
batch: calculation.batch,
|
|
205
|
-
state: calculation.state,
|
|
206
|
-
progress,
|
|
207
|
-
};
|
|
208
|
-
runtimeError = sinkError("SINK_WRITE_REJECTED", "write", command, error);
|
|
209
|
-
throw error;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function assertCalculationWithinBudgets(command, calculation) {
|
|
214
|
-
// The terminal lifecycle event is always available so a paused session can
|
|
215
|
-
// release its sink. All preceding canonical events still count as usage.
|
|
216
|
-
if (command === "close") {
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
const checks = [
|
|
220
|
-
{
|
|
221
|
-
configured: configuredLimits.maxCompletedDispatches,
|
|
222
|
-
limit: "completedDispatches",
|
|
223
|
-
observed: calculation.state.counters.completions,
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
configured: configuredLimits.maxEvents,
|
|
227
|
-
limit: "events",
|
|
228
|
-
observed: calculation.state.counters.events,
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
configured: configuredLimits.maxVirtualElapsedMs,
|
|
232
|
-
limit: "virtualElapsedMs",
|
|
233
|
-
observed: calculation.state.virtualElapsedMs,
|
|
234
|
-
},
|
|
235
|
-
];
|
|
236
|
-
const exceeded = checks.find((check) => check.observed > check.configured);
|
|
237
|
-
if (exceeded !== undefined) {
|
|
238
|
-
pauseExecution(budgetExceededDiagnostic({
|
|
239
|
-
...exceeded,
|
|
240
|
-
virtualTime: committedState.virtualTime,
|
|
241
|
-
virtualElapsedMs: committedState.virtualElapsedMs,
|
|
242
|
-
}));
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function pauseExecution(diagnostic) {
|
|
247
|
-
runtimeError = {
|
|
248
|
-
code: "EXECUTION_PAUSED",
|
|
249
|
-
diagnostic: copy(diagnostic),
|
|
250
|
-
};
|
|
251
|
-
throw new FactoryEmulatorExecutionPausedError(diagnostic);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function assertSynchronousWorkWithinLimit(observed) {
|
|
255
|
-
if (observed > configuredLimits.maxSynchronousWorkItems) {
|
|
256
|
-
pauseExecution(synchronousWorkLimitDiagnostic({
|
|
257
|
-
configured: configuredLimits.maxSynchronousWorkItems,
|
|
258
|
-
observed,
|
|
259
|
-
virtualTime: committedState.virtualTime,
|
|
260
|
-
virtualElapsedMs: committedState.virtualElapsedMs,
|
|
261
|
-
}));
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async function inspectSchedulerBatchWithinLimit() {
|
|
266
|
-
let continuation;
|
|
267
|
-
while (true) {
|
|
268
|
-
const inspection = inspectNextSchedulerBatch({
|
|
269
|
-
continuation,
|
|
270
|
-
maximumWorkItems: configuredLimits.maxSynchronousWorkItems,
|
|
271
|
-
state: committedState,
|
|
272
|
-
});
|
|
273
|
-
if (inspection.done) {
|
|
274
|
-
if (inspection.batch !== undefined) {
|
|
275
|
-
assertSynchronousWorkWithinLimit(inspection.batch.workItems);
|
|
276
|
-
}
|
|
277
|
-
return {
|
|
278
|
-
plan: inspection.plan,
|
|
279
|
-
hasWaitingWork: inspection.hasWaitingWork,
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
continuation = inspection.continuation;
|
|
283
|
-
await yieldControl();
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async function finishAdvance(command, fromVirtualTime, batches, madeProgress) {
|
|
288
|
-
if (madeProgress || committedState.virtualTime !== fromVirtualTime) {
|
|
289
|
-
committedState = {
|
|
290
|
-
...committedState,
|
|
291
|
-
counters: {
|
|
292
|
-
...committedState.counters,
|
|
293
|
-
commands: committedState.counters.commands + 1,
|
|
294
|
-
},
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
return advanceReceipt(
|
|
298
|
-
command,
|
|
299
|
-
fromVirtualTime,
|
|
300
|
-
batches,
|
|
301
|
-
madeProgress,
|
|
302
|
-
await detachState(committedState),
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
async function runAdvance(command, key, targetElapsedMs, retry) {
|
|
307
|
-
const progress = retry?.progress ?? {
|
|
308
|
-
fromVirtualTime: committedState.virtualTime,
|
|
309
|
-
targetElapsedMs,
|
|
310
|
-
batches: [],
|
|
311
|
-
};
|
|
312
|
-
const fromVirtualTime = progress.fromVirtualTime;
|
|
313
|
-
targetElapsedMs = progress.targetElapsedMs;
|
|
314
|
-
const batches = await detachBatches(progress.batches);
|
|
315
|
-
let zeroDurationBatches = progress.zeroDurationBatches ?? 0;
|
|
316
|
-
let synchronousBatches = 0;
|
|
317
|
-
let madeProgress = batches.length > 0;
|
|
318
|
-
|
|
319
|
-
if (retry !== undefined) {
|
|
320
|
-
const beforeRetryElapsedMs = committedState.virtualElapsedMs;
|
|
321
|
-
await writeCalculation(command, retry.key, retry, progress);
|
|
322
|
-
batches.push(copy(retry.batch));
|
|
323
|
-
zeroDurationBatches = committedState.virtualElapsedMs === beforeRetryElapsedMs
|
|
324
|
-
? zeroDurationBatches + 1
|
|
325
|
-
: 0;
|
|
326
|
-
madeProgress = true;
|
|
327
|
-
if (command === "advanceToNext") {
|
|
328
|
-
return await finishAdvance(command, fromVirtualTime, batches, true);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
while (true) {
|
|
333
|
-
const inspection = await inspectSchedulerBatchWithinLimit();
|
|
334
|
-
const plan = inspection.plan;
|
|
335
|
-
if (
|
|
336
|
-
plan === undefined
|
|
337
|
-
&& inspection.hasWaitingWork !== true
|
|
338
|
-
&& batches.length === 0
|
|
339
|
-
) {
|
|
340
|
-
targetElapsedMs = committedState.virtualElapsedMs;
|
|
341
|
-
}
|
|
342
|
-
const calculation = calculateNextSchedulerBatch({
|
|
343
|
-
createEvent,
|
|
344
|
-
identityCoordinates: sessionIdentityCoordinates(
|
|
345
|
-
configuredFactory,
|
|
346
|
-
configuredScenario,
|
|
347
|
-
committedState.sessionId,
|
|
348
|
-
),
|
|
349
|
-
plan,
|
|
350
|
-
scenario: configuredScenario,
|
|
351
|
-
state: committedState,
|
|
352
|
-
});
|
|
353
|
-
if (
|
|
354
|
-
calculation === undefined ||
|
|
355
|
-
(targetElapsedMs !== undefined && calculation.elapsedMs > targetElapsedMs)
|
|
356
|
-
) {
|
|
357
|
-
break;
|
|
358
|
-
}
|
|
359
|
-
const nextZeroDurationBatches = calculation.elapsedMs === committedState.virtualElapsedMs
|
|
360
|
-
? zeroDurationBatches + 1
|
|
361
|
-
: 0;
|
|
362
|
-
if (nextZeroDurationBatches > configuredLimits.maxZeroDurationBatches) {
|
|
363
|
-
pauseExecution(zeroDurationCycleDiagnostic({
|
|
364
|
-
configured: configuredLimits.maxZeroDurationBatches,
|
|
365
|
-
observed: nextZeroDurationBatches,
|
|
366
|
-
virtualTime: committedState.virtualTime,
|
|
367
|
-
virtualElapsedMs: committedState.virtualElapsedMs,
|
|
368
|
-
}));
|
|
369
|
-
}
|
|
370
|
-
const materialized = await materializeSchedulerCalculation(calculation);
|
|
371
|
-
if (materialized.batch !== undefined) {
|
|
372
|
-
await writeCalculation(command, key, materialized, {
|
|
373
|
-
fromVirtualTime,
|
|
374
|
-
targetElapsedMs,
|
|
375
|
-
batches,
|
|
376
|
-
zeroDurationBatches,
|
|
377
|
-
});
|
|
378
|
-
batches.push(copy(materialized.batch));
|
|
379
|
-
} else {
|
|
380
|
-
committedState = materialized.state;
|
|
381
|
-
}
|
|
382
|
-
zeroDurationBatches = nextZeroDurationBatches;
|
|
383
|
-
madeProgress = true;
|
|
384
|
-
if (command === "advanceToNext" && calculation.batch !== undefined) {
|
|
385
|
-
break;
|
|
386
|
-
}
|
|
387
|
-
synchronousBatches += 1;
|
|
388
|
-
if (synchronousBatches >= configuredLimits.maxSynchronousBatches) {
|
|
389
|
-
synchronousBatches = 0;
|
|
390
|
-
await yieldControl();
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (
|
|
395
|
-
targetElapsedMs !== undefined &&
|
|
396
|
-
committedState.virtualElapsedMs !== targetElapsedMs
|
|
397
|
-
) {
|
|
398
|
-
const targetState = stateAtElapsed(configuredScenario, committedState, targetElapsedMs);
|
|
399
|
-
assertCalculationWithinBudgets(command, { state: targetState });
|
|
400
|
-
committedState = targetState;
|
|
401
|
-
}
|
|
402
|
-
return await finishAdvance(command, fromVirtualTime, batches, madeProgress);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
async function materializeSchedulerCalculation(calculation) {
|
|
406
|
-
if (calculation === undefined || calculation.workReplacements === undefined) {
|
|
407
|
-
return calculation;
|
|
408
|
-
}
|
|
409
|
-
const works = await materializeWorks(
|
|
410
|
-
calculation.state.works,
|
|
411
|
-
calculation.workReplacements,
|
|
412
|
-
);
|
|
413
|
-
return {
|
|
414
|
-
...calculation,
|
|
415
|
-
state: { ...calculation.state, works },
|
|
416
|
-
workReplacements: undefined,
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
async function materializeWorks(sourceWorks, replacements, appendedWorks = []) {
|
|
421
|
-
const works = [];
|
|
422
|
-
const total = sourceWorks.length + appendedWorks.length;
|
|
423
|
-
for (let index = 0; index < total; index += 1) {
|
|
424
|
-
if (index < sourceWorks.length) {
|
|
425
|
-
works.push(replacements?.get(index) ?? sourceWorks[index]);
|
|
426
|
-
} else {
|
|
427
|
-
works.push(appendedWorks[index - sourceWorks.length]);
|
|
428
|
-
}
|
|
429
|
-
if (
|
|
430
|
-
(index + 1) % configuredLimits.maxSynchronousWorkItems === 0
|
|
431
|
-
&& index + 1 < total
|
|
432
|
-
) {
|
|
433
|
-
await yieldControl();
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
return works;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
async function detachState(state) {
|
|
440
|
-
const works = [];
|
|
441
|
-
for (let index = 0; index < state.works.length; index += 1) {
|
|
442
|
-
works.push(copy(state.works[index]));
|
|
443
|
-
if (
|
|
444
|
-
(index + 1) % configuredLimits.maxSynchronousWorkItems === 0
|
|
445
|
-
&& index + 1 < state.works.length
|
|
446
|
-
) {
|
|
447
|
-
await yieldControl();
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
const stateWithoutWorks = { ...state, works: undefined };
|
|
451
|
-
const detached = copy(stateWithoutWorks);
|
|
452
|
-
delete detached.works;
|
|
453
|
-
return { ...detached, works };
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
async function detachBatches(sourceBatches) {
|
|
457
|
-
const batches = [];
|
|
458
|
-
for (let index = 0; index < sourceBatches.length; index += 1) {
|
|
459
|
-
batches.push(copy(sourceBatches[index]));
|
|
460
|
-
if (
|
|
461
|
-
(index + 1) % configuredLimits.maxSynchronousBatches === 0
|
|
462
|
-
&& index + 1 < sourceBatches.length
|
|
463
|
-
) {
|
|
464
|
-
await yieldControl();
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
return batches;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
return Object.freeze({
|
|
471
|
-
async start() {
|
|
472
|
-
const retry = beginCommand("start", "start", "starting");
|
|
473
|
-
try {
|
|
474
|
-
let batches;
|
|
475
|
-
if (retry !== undefined) {
|
|
476
|
-
batches = copy(retry.progress.batches);
|
|
477
|
-
await writeCalculation("start", "start", retry, retry.progress);
|
|
478
|
-
batches.push(copy(retry.batch));
|
|
479
|
-
} else {
|
|
480
|
-
const parsed = parseEmulatorScenario(configuredScenario, configuredFactory);
|
|
481
|
-
if (!parsed.success) {
|
|
482
|
-
throw new FactoryEmulatorConfigurationError(parsed.diagnostics);
|
|
483
|
-
}
|
|
484
|
-
const transaction = calculateStart(parsed.factory, parsed.scenario);
|
|
485
|
-
batches = [];
|
|
486
|
-
await writeCalculation("start", "start", transaction, {
|
|
487
|
-
batches,
|
|
488
|
-
stage: "bootstrap",
|
|
489
|
-
});
|
|
490
|
-
batches.push(copy(transaction.batch));
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if (committedState.counters.commands === 0) {
|
|
494
|
-
assertSynchronousWorkWithinLimit(
|
|
495
|
-
configuredScenario.initialSubmissions?.length ?? 0,
|
|
496
|
-
);
|
|
497
|
-
const initial = calculateInitialSubmission(
|
|
498
|
-
configuredFactory,
|
|
499
|
-
configuredScenario,
|
|
500
|
-
committedState,
|
|
501
|
-
);
|
|
502
|
-
await writeCalculation("start", "start", initial, {
|
|
503
|
-
batches,
|
|
504
|
-
stage: "initial-submission",
|
|
505
|
-
});
|
|
506
|
-
batches.push(copy(initial.batch));
|
|
507
|
-
}
|
|
508
|
-
return {
|
|
509
|
-
status: "started",
|
|
510
|
-
batches: copy(batches),
|
|
511
|
-
state: await detachState(committedState),
|
|
512
|
-
};
|
|
513
|
-
} finally {
|
|
514
|
-
commandInProgress = undefined;
|
|
515
|
-
}
|
|
516
|
-
},
|
|
517
|
-
async submit(submissionOrBatch) {
|
|
518
|
-
assertCommandAvailable("submit");
|
|
519
|
-
const batchInspection = inspectSubmissionBatch(submissionOrBatch);
|
|
520
|
-
if (!batchInspection.success) {
|
|
521
|
-
throw new FactoryEmulatorSubmissionError([
|
|
522
|
-
invalidDataDiagnostic("INVALID_SCENARIO_SHAPE", "/submissions"),
|
|
523
|
-
]);
|
|
524
|
-
}
|
|
525
|
-
assertSynchronousWorkWithinLimit(batchInspection.length);
|
|
526
|
-
const submissions = normalizeSubmissions(submissionOrBatch, batchInspection.isBatch);
|
|
527
|
-
const key = commandKey("submit", canonicalStringify(submissions));
|
|
528
|
-
const retry = beginCommand("submit", key, "submitting");
|
|
529
|
-
try {
|
|
530
|
-
if (retry !== undefined) {
|
|
531
|
-
await writeCalculation("submit", key, retry);
|
|
532
|
-
return {
|
|
533
|
-
status: "submitted",
|
|
534
|
-
batch: copy(retry.batch),
|
|
535
|
-
state: await detachState(committedState),
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
const parsed = parseEmulatorScenario(
|
|
539
|
-
{
|
|
540
|
-
...configuredScenario,
|
|
541
|
-
initialSubmissions: submissions,
|
|
542
|
-
rules: [],
|
|
543
|
-
unmatchedBehavior: { kind: "ignore" },
|
|
544
|
-
},
|
|
545
|
-
configuredFactory,
|
|
546
|
-
);
|
|
547
|
-
if (!parsed.success) {
|
|
548
|
-
throw new FactoryEmulatorSubmissionError(
|
|
549
|
-
parsed.diagnostics.map((diagnostic) => ({
|
|
550
|
-
...diagnostic,
|
|
551
|
-
path: diagnostic.path.replace(/^\/initialSubmissions/, "/submissions"),
|
|
552
|
-
})),
|
|
553
|
-
);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
const calculated = calculateSubmit(
|
|
557
|
-
parsed.factory,
|
|
558
|
-
configuredScenario,
|
|
559
|
-
committedState,
|
|
560
|
-
submissions,
|
|
561
|
-
);
|
|
562
|
-
const calculation = {
|
|
563
|
-
...calculated,
|
|
564
|
-
state: {
|
|
565
|
-
...calculated.state,
|
|
566
|
-
works: await materializeWorks(
|
|
567
|
-
calculated.state.works,
|
|
568
|
-
undefined,
|
|
569
|
-
calculated.appendedWorks,
|
|
570
|
-
),
|
|
571
|
-
},
|
|
572
|
-
appendedWorks: undefined,
|
|
573
|
-
};
|
|
574
|
-
await writeCalculation("submit", key, calculation);
|
|
575
|
-
return {
|
|
576
|
-
status: "submitted",
|
|
577
|
-
batch: copy(calculation.batch),
|
|
578
|
-
state: await detachState(committedState),
|
|
579
|
-
};
|
|
580
|
-
} finally {
|
|
581
|
-
commandInProgress = undefined;
|
|
582
|
-
}
|
|
583
|
-
},
|
|
584
|
-
async advanceBy(durationMs) {
|
|
585
|
-
assertCommandAvailable("advanceBy");
|
|
586
|
-
validateDuration(durationMs);
|
|
587
|
-
const key = commandKey("advanceBy", durationMs);
|
|
588
|
-
const retry = beginCommand("advanceBy", key, "advancing");
|
|
589
|
-
const targetElapsedMs = retry?.progress.targetElapsedMs
|
|
590
|
-
?? committedState.virtualElapsedMs + durationMs;
|
|
591
|
-
if (!Number.isSafeInteger(targetElapsedMs)) {
|
|
592
|
-
commandInProgress = undefined;
|
|
593
|
-
throw new FactoryEmulatorDurationError(durationMs);
|
|
594
|
-
}
|
|
595
|
-
try {
|
|
596
|
-
virtualTimeAt(configuredScenario, targetElapsedMs);
|
|
597
|
-
return await runAdvance("advanceBy", key, targetElapsedMs, retry);
|
|
598
|
-
} finally {
|
|
599
|
-
commandInProgress = undefined;
|
|
600
|
-
}
|
|
601
|
-
},
|
|
602
|
-
async advanceToNext() {
|
|
603
|
-
const retry = beginCommand("advanceToNext", "advanceToNext", "advancing");
|
|
604
|
-
try {
|
|
605
|
-
return await runAdvance("advanceToNext", "advanceToNext", undefined, retry);
|
|
606
|
-
} finally {
|
|
607
|
-
commandInProgress = undefined;
|
|
608
|
-
}
|
|
609
|
-
},
|
|
610
|
-
async close() {
|
|
611
|
-
const retry = beginCommand("close", "close", "closing");
|
|
612
|
-
let terminal;
|
|
613
|
-
try {
|
|
614
|
-
terminal = retry ?? calculateClose(committedState, configuredFactory, configuredScenario);
|
|
615
|
-
if (retry?.progress.terminalAccepted !== true) {
|
|
616
|
-
await writeCalculation("close", "close", terminal, { terminalAccepted: false });
|
|
617
|
-
}
|
|
618
|
-
try {
|
|
619
|
-
const receipt = await sink.close();
|
|
620
|
-
if (receipt?.status !== "closed") {
|
|
621
|
-
throw new TypeError("sink.close must return a closed receipt");
|
|
622
|
-
}
|
|
623
|
-
pendingTransaction = undefined;
|
|
624
|
-
runtimeError = undefined;
|
|
625
|
-
return {
|
|
626
|
-
status: "closed",
|
|
627
|
-
batch: copy(terminal.batch),
|
|
628
|
-
state: await detachState(committedState),
|
|
629
|
-
};
|
|
630
|
-
} catch (error) {
|
|
631
|
-
pendingTransaction = {
|
|
632
|
-
command: "close",
|
|
633
|
-
key: "close",
|
|
634
|
-
batch: terminal.batch,
|
|
635
|
-
state: committedState,
|
|
636
|
-
progress: { terminalAccepted: true },
|
|
637
|
-
};
|
|
638
|
-
runtimeError = sinkError("SINK_CLOSE_REJECTED", "close", "close", error);
|
|
639
|
-
throw error;
|
|
640
|
-
}
|
|
641
|
-
} finally {
|
|
642
|
-
commandInProgress = undefined;
|
|
643
|
-
}
|
|
644
|
-
},
|
|
645
|
-
reset() {
|
|
646
|
-
const phase = commandInProgress ?? committedState.lifecycle;
|
|
647
|
-
if (
|
|
648
|
-
commandInProgress !== undefined ||
|
|
649
|
-
committedState.lifecycle === "closed" ||
|
|
650
|
-
(pendingTransaction === undefined &&
|
|
651
|
-
committedState.lifecycle !== "started" &&
|
|
652
|
-
runtimeError?.code !== "EXECUTION_PAUSED")
|
|
653
|
-
) {
|
|
654
|
-
throw new FactoryEmulatorLifecycleError("reset", phase);
|
|
655
|
-
}
|
|
656
|
-
committedState = preStartState();
|
|
657
|
-
pendingTransaction = undefined;
|
|
658
|
-
runtimeError = undefined;
|
|
659
|
-
return copy({ status: "reset", state: committedState });
|
|
660
|
-
},
|
|
661
|
-
pending() {
|
|
662
|
-
return pendingTransaction === undefined ? undefined : copy(pendingTransaction.batch);
|
|
663
|
-
},
|
|
664
|
-
state() {
|
|
665
|
-
return copy(committedState);
|
|
666
|
-
},
|
|
667
|
-
status() {
|
|
668
|
-
return copy(sessionStatus({
|
|
669
|
-
configuredLimits,
|
|
670
|
-
commandInProgress,
|
|
671
|
-
pendingTransaction,
|
|
672
|
-
runtimeError,
|
|
673
|
-
state: committedState,
|
|
674
|
-
}));
|
|
675
|
-
},
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function calculateSubmit(factory, scenario, state, submissions) {
|
|
680
|
-
const identityCoordinates = sessionIdentityCoordinates(
|
|
681
|
-
factory,
|
|
682
|
-
scenario,
|
|
683
|
-
state.sessionId,
|
|
684
|
-
);
|
|
685
|
-
const commandSequence = state.counters.commands;
|
|
686
|
-
const { event, works } = calculateWorkRequest({
|
|
687
|
-
command: "submit",
|
|
688
|
-
commandSequence,
|
|
689
|
-
eventSequence: state.counters.events,
|
|
690
|
-
eventTime: state.virtualTime,
|
|
691
|
-
identityCoordinates,
|
|
692
|
-
scenario,
|
|
693
|
-
sessionId: state.sessionId,
|
|
694
|
-
submissions,
|
|
695
|
-
});
|
|
696
|
-
return {
|
|
697
|
-
batch: { events: [event] },
|
|
698
|
-
state: {
|
|
699
|
-
...state,
|
|
700
|
-
counters: {
|
|
701
|
-
...state.counters,
|
|
702
|
-
commands: state.counters.commands + 1,
|
|
703
|
-
events: state.counters.events + 1,
|
|
704
|
-
requests: state.counters.requests + 1,
|
|
705
|
-
works: state.counters.works + works.length,
|
|
706
|
-
},
|
|
707
|
-
},
|
|
708
|
-
appendedWorks: works,
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
function calculateStart(factory, scenario) {
|
|
713
|
-
const normalizedStartAt = normalizeStartAt(scenario.startAt);
|
|
714
|
-
const factoryIdentity = canonicalStringify(factory);
|
|
715
|
-
const sessionId = deriveFactoryEmulatorIdentity("session", {
|
|
716
|
-
factory: factoryIdentity,
|
|
717
|
-
scenarioId: scenario.id,
|
|
718
|
-
scenarioVersion: scenario.version,
|
|
719
|
-
seed: scenario.seed,
|
|
720
|
-
startAt: normalizedStartAt,
|
|
721
|
-
});
|
|
722
|
-
const identityCoordinates = sessionIdentityCoordinates(
|
|
723
|
-
factory,
|
|
724
|
-
scenario,
|
|
725
|
-
sessionId,
|
|
726
|
-
);
|
|
727
|
-
|
|
728
|
-
const bootstrapEvents = [
|
|
729
|
-
createEvent({
|
|
730
|
-
identityCoordinates,
|
|
731
|
-
sequence: 0,
|
|
732
|
-
sessionId,
|
|
733
|
-
eventTime: normalizedStartAt,
|
|
734
|
-
type: "INITIAL_STRUCTURE_REQUEST",
|
|
735
|
-
payload: {
|
|
736
|
-
factory,
|
|
737
|
-
metadata: { emulatorScenarioId: scenario.id },
|
|
738
|
-
},
|
|
739
|
-
}),
|
|
740
|
-
createEvent({
|
|
741
|
-
identityCoordinates,
|
|
742
|
-
sequence: 1,
|
|
743
|
-
sessionId,
|
|
744
|
-
eventTime: normalizedStartAt,
|
|
745
|
-
type: "RUN_REQUEST",
|
|
746
|
-
payload: { recordedAt: normalizedStartAt, factory },
|
|
747
|
-
}),
|
|
748
|
-
];
|
|
749
|
-
|
|
750
|
-
const initialSubmissions = scenario.initialSubmissions ?? [];
|
|
751
|
-
const bootstrapState = {
|
|
752
|
-
lifecycle: "started",
|
|
753
|
-
sessionId,
|
|
754
|
-
virtualTime: normalizedStartAt,
|
|
755
|
-
virtualElapsedMs: 0,
|
|
756
|
-
works: [],
|
|
757
|
-
ruleCursors: {},
|
|
758
|
-
counters: {
|
|
759
|
-
commands: initialSubmissions.length === 0 ? 1 : 0,
|
|
760
|
-
events: bootstrapEvents.length,
|
|
761
|
-
requests: 0,
|
|
762
|
-
works: 0,
|
|
763
|
-
dispatches: 0,
|
|
764
|
-
completions: 0,
|
|
765
|
-
},
|
|
766
|
-
};
|
|
767
|
-
return copy({
|
|
768
|
-
batch: { events: bootstrapEvents },
|
|
769
|
-
state: bootstrapState,
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
function calculateInitialSubmission(factory, scenario, state) {
|
|
774
|
-
const submissions = scenario.initialSubmissions ?? [];
|
|
775
|
-
const { event, works } = calculateWorkRequest({
|
|
776
|
-
command: "start",
|
|
777
|
-
commandSequence: 0,
|
|
778
|
-
eventSequence: state.counters.events,
|
|
779
|
-
eventTime: state.virtualTime,
|
|
780
|
-
identityCoordinates: sessionIdentityCoordinates(factory, scenario, state.sessionId),
|
|
781
|
-
scenario,
|
|
782
|
-
sessionId: state.sessionId,
|
|
783
|
-
submissions,
|
|
784
|
-
});
|
|
785
|
-
return copy({
|
|
786
|
-
batch: { events: [event] },
|
|
787
|
-
state: {
|
|
788
|
-
...state,
|
|
789
|
-
works,
|
|
790
|
-
counters: {
|
|
791
|
-
...state.counters,
|
|
792
|
-
commands: 1,
|
|
793
|
-
events: state.counters.events + 1,
|
|
794
|
-
requests: 1,
|
|
795
|
-
works: works.length,
|
|
796
|
-
},
|
|
797
|
-
},
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
function calculateWorkRequest({
|
|
802
|
-
command,
|
|
803
|
-
commandSequence,
|
|
804
|
-
eventSequence,
|
|
805
|
-
eventTime,
|
|
806
|
-
identityCoordinates,
|
|
807
|
-
scenario,
|
|
808
|
-
sessionId,
|
|
809
|
-
submissions,
|
|
810
|
-
}) {
|
|
811
|
-
const requestId = deriveFactoryEmulatorIdentity("request", {
|
|
812
|
-
...identityCoordinates,
|
|
813
|
-
command,
|
|
814
|
-
sequence: commandSequence,
|
|
815
|
-
});
|
|
816
|
-
const works = submissions.map((submission, ordinal) => {
|
|
817
|
-
const workCoordinates = {
|
|
818
|
-
...identityCoordinates,
|
|
819
|
-
command,
|
|
820
|
-
...(command === "start" ? {} : { sequence: commandSequence }),
|
|
821
|
-
ordinal,
|
|
822
|
-
submissionId: submission.id,
|
|
823
|
-
workType: submission.workType,
|
|
824
|
-
};
|
|
825
|
-
const traceId = deriveFactoryEmulatorIdentity("trace", workCoordinates);
|
|
826
|
-
const tokenId = deriveFactoryEmulatorIdentity("token", {
|
|
827
|
-
...workCoordinates,
|
|
828
|
-
lineage: { kind: "submission", submissionId: submission.id },
|
|
829
|
-
});
|
|
830
|
-
return {
|
|
831
|
-
submissionId: submission.id,
|
|
832
|
-
requestId,
|
|
833
|
-
traceId,
|
|
834
|
-
workId: deriveFactoryEmulatorIdentity("work", workCoordinates),
|
|
835
|
-
tokenId,
|
|
836
|
-
workType: submission.workType,
|
|
837
|
-
phase: submissionPhase(scenario, submission),
|
|
838
|
-
...(submission.input === undefined ? {} : { input: submission.input }),
|
|
839
|
-
};
|
|
840
|
-
});
|
|
841
|
-
return {
|
|
842
|
-
works,
|
|
843
|
-
event: createEvent({
|
|
844
|
-
identityCoordinates,
|
|
845
|
-
sequence: eventSequence,
|
|
846
|
-
sessionId,
|
|
847
|
-
eventTime,
|
|
848
|
-
type: "WORK_REQUEST",
|
|
849
|
-
context: {
|
|
850
|
-
requestId,
|
|
851
|
-
traceIds: works.map((work) => work.traceId),
|
|
852
|
-
workIds: works.map((work) => work.workId),
|
|
853
|
-
},
|
|
854
|
-
payload: {
|
|
855
|
-
type: WORK_REQUEST_TYPE,
|
|
856
|
-
source: "emulator",
|
|
857
|
-
works: works.map((work) => ({
|
|
858
|
-
name: work.submissionId,
|
|
859
|
-
workId: work.workId,
|
|
860
|
-
requestId: work.requestId,
|
|
861
|
-
workTypeName: work.workType,
|
|
862
|
-
currentChainingTraceId: work.traceId,
|
|
863
|
-
traceId: work.traceId,
|
|
864
|
-
tags: { emulatorTokenId: work.tokenId },
|
|
865
|
-
...(work.input === undefined ? {} : { payload: work.input }),
|
|
866
|
-
})),
|
|
867
|
-
},
|
|
868
|
-
}),
|
|
869
|
-
};
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
function submissionPhase(scenario, submission) {
|
|
873
|
-
if (selectEmulatorRule(scenario, submission) !== undefined) {
|
|
874
|
-
return "ready";
|
|
875
|
-
}
|
|
876
|
-
return scenario.unmatchedBehavior.kind === "ignore" ? "waiting" : "ready";
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
function sessionIdentityCoordinates(factory, scenario, sessionId) {
|
|
880
|
-
return {
|
|
881
|
-
factory: canonicalStringify(factory),
|
|
882
|
-
scenarioId: scenario.id,
|
|
883
|
-
seed: scenario.seed,
|
|
884
|
-
sessionId,
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
function normalizeSubmissions(submissionOrBatch, isBatch) {
|
|
889
|
-
const submissionValues = isBatch
|
|
890
|
-
? submissionOrBatch
|
|
891
|
-
: [submissionOrBatch];
|
|
892
|
-
const diagnostics = dataOnlyDiagnostics(submissionValues, {
|
|
893
|
-
code: "INVALID_SCENARIO_SHAPE",
|
|
894
|
-
rootPath: "/submissions",
|
|
895
|
-
});
|
|
896
|
-
if (diagnostics.length > 0) {
|
|
897
|
-
throw new FactoryEmulatorSubmissionError(diagnostics);
|
|
898
|
-
}
|
|
899
|
-
let submissions;
|
|
900
|
-
try {
|
|
901
|
-
submissions = copy(submissionValues);
|
|
902
|
-
} catch {
|
|
903
|
-
throw new FactoryEmulatorSubmissionError([
|
|
904
|
-
invalidDataDiagnostic("INVALID_SCENARIO_SHAPE", "/submissions"),
|
|
905
|
-
]);
|
|
906
|
-
}
|
|
907
|
-
if (submissions.length === 0) {
|
|
908
|
-
throw new FactoryEmulatorSubmissionError([{
|
|
909
|
-
code: "INVALID_SCENARIO_SHAPE",
|
|
910
|
-
path: "/submissions",
|
|
911
|
-
message: "must contain at least one Work request",
|
|
912
|
-
expectation: "at least one Work request",
|
|
913
|
-
}]);
|
|
914
|
-
}
|
|
915
|
-
return submissions;
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
function inspectSubmissionBatch(submissionOrBatch) {
|
|
919
|
-
try {
|
|
920
|
-
if (!Array.isArray(submissionOrBatch)) {
|
|
921
|
-
return { success: true, isBatch: false, length: 1 };
|
|
922
|
-
}
|
|
923
|
-
const descriptor = Object.getOwnPropertyDescriptor(submissionOrBatch, "length");
|
|
924
|
-
if (
|
|
925
|
-
descriptor === undefined
|
|
926
|
-
|| !("value" in descriptor)
|
|
927
|
-
|| !Number.isSafeInteger(descriptor.value)
|
|
928
|
-
|| descriptor.value < 0
|
|
929
|
-
) {
|
|
930
|
-
return { success: false };
|
|
931
|
-
}
|
|
932
|
-
return { success: true, isBatch: true, length: descriptor.value };
|
|
933
|
-
} catch {
|
|
934
|
-
return { success: false };
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function submissionBatchLengthFromScenario(scenario) {
|
|
939
|
-
if (scenario === null || typeof scenario !== "object") {
|
|
940
|
-
return 0;
|
|
941
|
-
}
|
|
942
|
-
try {
|
|
943
|
-
const descriptor = Object.getOwnPropertyDescriptor(scenario, "initialSubmissions");
|
|
944
|
-
if (descriptor === undefined || !("value" in descriptor)) {
|
|
945
|
-
return 0;
|
|
946
|
-
}
|
|
947
|
-
const inspection = inspectSubmissionBatch(descriptor.value);
|
|
948
|
-
return inspection.success ? inspection.length : 0;
|
|
949
|
-
} catch {
|
|
950
|
-
return 0;
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
function synchronousInputConfigurationDiagnostic(configured, observed) {
|
|
955
|
-
return {
|
|
956
|
-
code: "SYNCHRONOUS_WORK_LIMIT_EXCEEDED",
|
|
957
|
-
path: "/initialSubmissions",
|
|
958
|
-
message:
|
|
959
|
-
`/initialSubmissions contains ${observed} Work requests; the configured maximum is ${configured}.`,
|
|
960
|
-
expectation: `at most ${configured} initial Work requests`,
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
function copyConfiguration(value, code, path) {
|
|
965
|
-
try {
|
|
966
|
-
return copy(value);
|
|
967
|
-
} catch {
|
|
968
|
-
throw new FactoryEmulatorConfigurationError([
|
|
969
|
-
invalidDataDiagnostic(code, path),
|
|
970
|
-
]);
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
function invalidDataDiagnostic(code, path) {
|
|
975
|
-
return {
|
|
976
|
-
code,
|
|
977
|
-
path,
|
|
978
|
-
message: `${path} must contain structured-cloneable data values only`,
|
|
979
|
-
expectation: "plain objects, dense arrays, null, booleans, strings, or finite numbers",
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
function sessionStatus({
|
|
984
|
-
configuredLimits,
|
|
985
|
-
commandInProgress,
|
|
986
|
-
pendingTransaction,
|
|
987
|
-
runtimeError,
|
|
988
|
-
state,
|
|
989
|
-
}) {
|
|
990
|
-
const status = executionStatus(state, commandInProgress, runtimeError);
|
|
991
|
-
return {
|
|
992
|
-
...status,
|
|
993
|
-
...(state.virtualTime === undefined ? {} : { virtualTime: state.virtualTime }),
|
|
994
|
-
virtualElapsedMs: state.virtualElapsedMs,
|
|
995
|
-
budgetUsage: {
|
|
996
|
-
completedDispatches: {
|
|
997
|
-
used: state.counters.completions,
|
|
998
|
-
limit: configuredLimits.maxCompletedDispatches,
|
|
999
|
-
},
|
|
1000
|
-
events: {
|
|
1001
|
-
used: state.counters.events,
|
|
1002
|
-
limit: configuredLimits.maxEvents,
|
|
1003
|
-
},
|
|
1004
|
-
virtualElapsedMs: {
|
|
1005
|
-
used: state.virtualElapsedMs,
|
|
1006
|
-
limit: configuredLimits.maxVirtualElapsedMs,
|
|
1007
|
-
},
|
|
1008
|
-
},
|
|
1009
|
-
...(pendingTransaction === undefined
|
|
1010
|
-
? {}
|
|
1011
|
-
: { pendingTransaction: pendingTransactionStatus(pendingTransaction) }),
|
|
1012
|
-
...(runtimeError === undefined ? {} : { error: runtimeError }),
|
|
1013
|
-
};
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
function executionStatus(state, commandInProgress, runtimeError) {
|
|
1017
|
-
if (runtimeError !== undefined) {
|
|
1018
|
-
return runtimeError.code === "EXECUTION_PAUSED"
|
|
1019
|
-
? {
|
|
1020
|
-
phase: "error",
|
|
1021
|
-
reason: runtimeError.diagnostic.kind,
|
|
1022
|
-
diagnostic: runtimeError.diagnostic,
|
|
1023
|
-
}
|
|
1024
|
-
: { phase: "error", reason: runtimeError.code };
|
|
1025
|
-
}
|
|
1026
|
-
if (state.lifecycle === "closed") {
|
|
1027
|
-
return { phase: "closed", reason: "session-closed" };
|
|
1028
|
-
}
|
|
1029
|
-
if (commandInProgress !== undefined) {
|
|
1030
|
-
return { phase: "active", reason: commandInProgress };
|
|
1031
|
-
}
|
|
1032
|
-
if (state.lifecycle === "pre-start") {
|
|
1033
|
-
return { phase: "idle", reason: "not-started" };
|
|
1034
|
-
}
|
|
1035
|
-
if (state.works.some((work) => work.phase === "active")) {
|
|
1036
|
-
return { phase: "active", reason: "work-active" };
|
|
1037
|
-
}
|
|
1038
|
-
if (state.works.some((work) => work.phase === undefined || work.phase === "ready")) {
|
|
1039
|
-
return { phase: "ready", reason: "work-ready" };
|
|
1040
|
-
}
|
|
1041
|
-
if (state.works.some((work) => work.phase === "waiting")) {
|
|
1042
|
-
return { phase: "waiting", reason: "work-waiting" };
|
|
1043
|
-
}
|
|
1044
|
-
return { phase: "idle", reason: "no-unfinished-work" };
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
function pendingTransactionStatus(transaction) {
|
|
1048
|
-
return {
|
|
1049
|
-
command: transaction.command,
|
|
1050
|
-
phase: transaction.progress.terminalAccepted === true
|
|
1051
|
-
? "sink-close"
|
|
1052
|
-
: "sink-write",
|
|
1053
|
-
eventCount: transaction.batch.events.length,
|
|
1054
|
-
};
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
function calculateClose(state, factory, scenario) {
|
|
1058
|
-
const identityCoordinates = sessionIdentityCoordinates(factory, scenario, state.sessionId);
|
|
1059
|
-
const event = createEvent({
|
|
1060
|
-
identityCoordinates,
|
|
1061
|
-
sequence: state.counters.events,
|
|
1062
|
-
tick: state.counters.events,
|
|
1063
|
-
sessionId: state.sessionId,
|
|
1064
|
-
eventTime: state.virtualTime,
|
|
1065
|
-
type: "RUN_RESPONSE",
|
|
1066
|
-
payload: {
|
|
1067
|
-
state: "COMPLETED",
|
|
1068
|
-
reason: "emulator session closed",
|
|
1069
|
-
},
|
|
1070
|
-
});
|
|
1071
|
-
return {
|
|
1072
|
-
batch: { events: [event] },
|
|
1073
|
-
state: {
|
|
1074
|
-
...state,
|
|
1075
|
-
lifecycle: "closed",
|
|
1076
|
-
counters: {
|
|
1077
|
-
...state.counters,
|
|
1078
|
-
commands: state.counters.commands + 1,
|
|
1079
|
-
events: state.counters.events + 1,
|
|
1080
|
-
},
|
|
1081
|
-
},
|
|
1082
|
-
};
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
function advanceReceipt(command, fromVirtualTime, batches, madeProgress, state) {
|
|
1086
|
-
return {
|
|
1087
|
-
status: !madeProgress && batches.length === 0 && state.virtualTime === fromVirtualTime
|
|
1088
|
-
? "idle"
|
|
1089
|
-
: "advanced",
|
|
1090
|
-
command,
|
|
1091
|
-
fromVirtualTime,
|
|
1092
|
-
virtualTime: state.virtualTime,
|
|
1093
|
-
virtualElapsedMs: state.virtualElapsedMs,
|
|
1094
|
-
batches,
|
|
1095
|
-
state,
|
|
1096
|
-
};
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
function commandKey(command, value) {
|
|
1100
|
-
return value === undefined ? command : `${command}:${canonicalStringify(value)}`;
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
function sinkError(code, operation, command, error) {
|
|
1104
|
-
return {
|
|
1105
|
-
code,
|
|
1106
|
-
operation,
|
|
1107
|
-
command,
|
|
1108
|
-
message: sinkErrorMessage(error),
|
|
1109
|
-
};
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
function sinkErrorMessage(error) {
|
|
1113
|
-
let message = error;
|
|
1114
|
-
try {
|
|
1115
|
-
if (error instanceof Error) {
|
|
1116
|
-
message = error.message;
|
|
1117
|
-
}
|
|
1118
|
-
return typeof message === "string" ? message : String(message);
|
|
1119
|
-
} catch {
|
|
1120
|
-
return UNPRINTABLE_SINK_ERROR_MESSAGE;
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
function createEvent({
|
|
1125
|
-
identityCoordinates,
|
|
1126
|
-
sequence,
|
|
1127
|
-
tick = 0,
|
|
1128
|
-
sessionId,
|
|
1129
|
-
eventTime,
|
|
1130
|
-
type,
|
|
1131
|
-
context = {},
|
|
1132
|
-
payload,
|
|
1133
|
-
}) {
|
|
1134
|
-
return {
|
|
1135
|
-
schemaVersion: EVENT_SCHEMA_VERSION,
|
|
1136
|
-
id: deriveFactoryEmulatorIdentity("event", {
|
|
1137
|
-
...identityCoordinates,
|
|
1138
|
-
sequence,
|
|
1139
|
-
type,
|
|
1140
|
-
context,
|
|
1141
|
-
}),
|
|
1142
|
-
type,
|
|
1143
|
-
context: {
|
|
1144
|
-
sequence,
|
|
1145
|
-
tick,
|
|
1146
|
-
eventTime,
|
|
1147
|
-
sessionId,
|
|
1148
|
-
sessionSequence: sequence,
|
|
1149
|
-
source: "emulator",
|
|
1150
|
-
...context,
|
|
1151
|
-
},
|
|
1152
|
-
payload,
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
function preStartState() {
|
|
1157
|
-
return {
|
|
1158
|
-
lifecycle: "pre-start",
|
|
1159
|
-
virtualElapsedMs: 0,
|
|
1160
|
-
works: [],
|
|
1161
|
-
ruleCursors: {},
|
|
1162
|
-
counters: {
|
|
1163
|
-
commands: 0,
|
|
1164
|
-
events: 0,
|
|
1165
|
-
requests: 0,
|
|
1166
|
-
works: 0,
|
|
1167
|
-
dispatches: 0,
|
|
1168
|
-
completions: 0,
|
|
1169
|
-
},
|
|
1170
|
-
};
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
function normalizeStartAt(startAt) {
|
|
1174
|
-
const instant = new Date(startAt);
|
|
1175
|
-
if (!Number.isFinite(instant.getTime())) {
|
|
1176
|
-
throw new TypeError("scenario startAt must identify a finite UTC instant");
|
|
1177
|
-
}
|
|
1178
|
-
return instant.toISOString();
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
function copy(value) {
|
|
1182
|
-
return structuredClone(value);
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
function defaultYieldControl() {
|
|
1186
|
-
if (typeof MessageChannel === "function") {
|
|
1187
|
-
return new Promise((resolve) => {
|
|
1188
|
-
const channel = new MessageChannel();
|
|
1189
|
-
channel.port1.onmessage = () => {
|
|
1190
|
-
channel.port1.close();
|
|
1191
|
-
channel.port2.close();
|
|
1192
|
-
resolve();
|
|
1193
|
-
};
|
|
1194
|
-
channel.port2.postMessage(undefined);
|
|
1195
|
-
});
|
|
1196
|
-
}
|
|
1197
|
-
if (typeof setImmediate === "function") {
|
|
1198
|
-
return new Promise((resolve) => setImmediate(resolve));
|
|
1199
|
-
}
|
|
1200
|
-
throw new TypeError("yieldControl is required when the host has no task scheduler");
|
|
1201
|
-
}
|