@typeonce/ritmo 0.6.0 → 0.8.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/README.md +2 -0
- package/dist/Brand.d.ts +9 -4
- package/dist/Brand.d.ts.map +1 -1
- package/dist/Brand.js +9 -4
- package/dist/Brand.js.map +1 -1
- package/dist/Clock.d.ts +186 -0
- package/dist/Clock.d.ts.map +1 -0
- package/dist/Clock.js +111 -0
- package/dist/Clock.js.map +1 -0
- package/dist/Command.d.ts +250 -41
- package/dist/Command.d.ts.map +1 -1
- package/dist/Command.js +59 -23
- package/dist/Command.js.map +1 -1
- package/dist/Condition.d.ts +12 -1
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js.map +1 -1
- package/dist/Debug.d.ts +163 -15
- package/dist/Debug.d.ts.map +1 -1
- package/dist/Debug.js.map +1 -1
- package/dist/Decode.d.ts +1 -1
- package/dist/Decode.js +1 -1
- package/dist/Descriptor.d.ts +67 -24
- package/dist/Descriptor.d.ts.map +1 -1
- package/dist/Descriptor.js +32 -15
- package/dist/Descriptor.js.map +1 -1
- package/dist/Entity.d.ts +22 -5
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js +2 -2
- package/dist/Entity.js.map +1 -1
- package/dist/EntityScope.d.ts +26 -1
- package/dist/EntityScope.d.ts.map +1 -1
- package/dist/EntityScope.js +14 -2
- package/dist/EntityScope.js.map +1 -1
- package/dist/HostClock.d.ts +49 -0
- package/dist/HostClock.d.ts.map +1 -0
- package/dist/HostClock.js +46 -0
- package/dist/HostClock.js.map +1 -0
- package/dist/Inspector.d.ts +7 -1
- package/dist/Inspector.d.ts.map +1 -1
- package/dist/Inspector.js.map +1 -1
- package/dist/Machine.d.ts +67 -36
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +10 -3
- package/dist/Machine.js.map +1 -1
- package/dist/Query.d.ts +74 -26
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +32 -28
- package/dist/Query.js.map +1 -1
- package/dist/Runtime.d.ts +39 -16
- package/dist/Runtime.d.ts.map +1 -1
- package/dist/Runtime.js +556 -87
- package/dist/Runtime.js.map +1 -1
- package/dist/Schedule.d.ts +217 -10
- package/dist/Schedule.d.ts.map +1 -1
- package/dist/Schedule.js +5 -0
- package/dist/Schedule.js.map +1 -1
- package/dist/Schema.d.ts +196 -44
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +29 -11
- package/dist/Schema.js.map +1 -1
- package/dist/Snapshot.d.ts +14 -0
- package/dist/Snapshot.d.ts.map +1 -1
- package/dist/Snapshot.js.map +1 -1
- package/dist/System.d.ts +212 -58
- package/dist/System.d.ts.map +1 -1
- package/dist/System.js +84 -6
- package/dist/System.js.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -7
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +3 -3
- package/dist/internal/cells.d.ts.map +1 -1
- package/dist/internal/cells.js +9 -11
- package/dist/internal/cells.js.map +1 -1
- package/dist/internal/debug.d.ts +13 -0
- package/dist/internal/debug.d.ts.map +1 -1
- package/dist/internal/debug.js +192 -7
- package/dist/internal/debug.js.map +1 -1
- package/dist/internal/equal.d.ts +8 -0
- package/dist/internal/equal.d.ts.map +1 -0
- package/dist/internal/equal.js +38 -0
- package/dist/internal/equal.js.map +1 -0
- package/dist/internal/fragments.d.ts.map +1 -1
- package/dist/internal/fragments.js +11 -1
- package/dist/internal/fragments.js.map +1 -1
- package/dist/internal/game.d.ts.map +1 -1
- package/dist/internal/game.js +41 -6
- package/dist/internal/game.js.map +1 -1
- package/dist/internal/order.d.ts +10 -0
- package/dist/internal/order.d.ts.map +1 -0
- package/dist/internal/order.js +122 -0
- package/dist/internal/order.js.map +1 -0
- package/dist/internal/queries.d.ts.map +1 -1
- package/dist/internal/queries.js +32 -10
- package/dist/internal/queries.js.map +1 -1
- package/dist/internal/requirements.d.ts +26 -0
- package/dist/internal/requirements.d.ts.map +1 -0
- package/dist/internal/requirements.js +46 -0
- package/dist/internal/requirements.js.map +1 -0
- package/package.json +1 -1
- package/src/Brand.ts +9 -4
- package/src/Clock.ts +311 -0
- package/src/Command.ts +407 -89
- package/src/Condition.ts +17 -1
- package/src/Debug.ts +172 -14
- package/src/Decode.ts +1 -1
- package/src/Descriptor.ts +92 -37
- package/src/Entity.ts +24 -5
- package/src/EntityScope.ts +27 -2
- package/src/HostClock.ts +64 -0
- package/src/Inspector.ts +7 -2
- package/src/Machine.ts +81 -37
- package/src/Query.ts +110 -38
- package/src/Runtime.ts +607 -111
- package/src/Schedule.ts +380 -13
- package/src/Schema.ts +249 -63
- package/src/Snapshot.ts +4 -0
- package/src/System.ts +248 -93
- package/src/index.ts +8 -7
- package/src/internal/cells.ts +10 -12
- package/src/internal/debug.ts +208 -12
- package/src/internal/equal.ts +31 -0
- package/src/internal/fragments.ts +12 -2
- package/src/internal/game.ts +61 -7
- package/src/internal/order.ts +144 -0
- package/src/internal/queries.ts +37 -13
- package/src/internal/requirements.ts +71 -0
- package/dist/Definition.d.ts +0 -99
- package/dist/Definition.d.ts.map +0 -1
- package/dist/Definition.js +0 -68
- package/dist/Definition.js.map +0 -1
- package/dist/Feature.d.ts +0 -106
- package/dist/Feature.d.ts.map +0 -1
- package/dist/Feature.js +0 -87
- package/dist/Feature.js.map +0 -1
- package/dist/Fx.d.ts +0 -128
- package/dist/Fx.d.ts.map +0 -1
- package/dist/Fx.js +0 -112
- package/dist/Fx.js.map +0 -1
- package/dist/internal/definition.d.ts +0 -17
- package/dist/internal/definition.d.ts.map +0 -1
- package/dist/internal/definition.js +0 -25
- package/dist/internal/definition.js.map +0 -1
- package/src/Definition.ts +0 -106
- package/src/Feature.ts +0 -241
- package/src/Fx.ts +0 -176
- package/src/internal/definition.ts +0 -56
package/dist/Runtime.js
CHANGED
|
@@ -55,17 +55,21 @@
|
|
|
55
55
|
* @groupDescription Functions
|
|
56
56
|
* Public runtime constructors and helpers for services, machines, and bootstrap input.
|
|
57
57
|
*/
|
|
58
|
+
import * as HostClockModule from "./HostClock.js";
|
|
58
59
|
import * as Command from "./Command.js";
|
|
59
60
|
import * as DescriptorModule from "./Descriptor.js";
|
|
60
61
|
import * as Cells from "./internal/cells.js";
|
|
62
|
+
import { sameValue } from "./internal/equal.js";
|
|
61
63
|
import * as DebugInternal from "./internal/debug.js";
|
|
62
64
|
import { makeQueryEngine } from "./internal/queries.js";
|
|
65
|
+
import { requirementIndex } from "./internal/requirements.js";
|
|
63
66
|
import * as Streams from "./internal/streams.js";
|
|
64
67
|
import { ABSENT, makeWorld } from "./internal/world.js";
|
|
65
68
|
import * as Query from "./Query.js";
|
|
66
69
|
import * as Relation from "./Relation.js";
|
|
67
70
|
import * as Result from "./Result.js";
|
|
68
71
|
import * as Schedule from "./Schedule.js";
|
|
72
|
+
import * as OrderInternal from "./internal/order.js";
|
|
69
73
|
import * as EntityScopeModule from "./EntityScope.js";
|
|
70
74
|
import * as Snapshot from "./Snapshot.js";
|
|
71
75
|
/**
|
|
@@ -194,12 +198,12 @@ const makeValidatedRuntime = (options) => {
|
|
|
194
198
|
*/
|
|
195
199
|
const pendingCommands = [];
|
|
196
200
|
/**
|
|
197
|
-
* Debug state. With `debug
|
|
201
|
+
* Debug state. With `debug`, `pendingOrigins` names the system that
|
|
198
202
|
* queued each pending command, in step with `pendingCommands`. Tracing is
|
|
199
203
|
* on only while `observe` listeners are registered; untraced runs pay one
|
|
200
204
|
* boolean check at each system, schedule, and marker boundary.
|
|
201
205
|
*/
|
|
202
|
-
const debugEnabled = options.debug;
|
|
206
|
+
const debugEnabled = options.debug !== undefined;
|
|
203
207
|
const pendingOrigins = [];
|
|
204
208
|
const traceListeners = new Set();
|
|
205
209
|
let tracing = false;
|
|
@@ -211,7 +215,9 @@ const makeValidatedRuntime = (options) => {
|
|
|
211
215
|
const scheduleNames = new Map();
|
|
212
216
|
const anonymousScheduleNames = new WeakMap();
|
|
213
217
|
let anonymousScheduleCount = 0;
|
|
214
|
-
|
|
218
|
+
// Only a debug runtime times anything; the clock is the one it was given.
|
|
219
|
+
const hostClock = options.debug?.hostClock ?? { now: () => 0 };
|
|
220
|
+
const now = () => hostClock.now();
|
|
215
221
|
const emit = (event) => {
|
|
216
222
|
for (const listener of traceListeners)
|
|
217
223
|
listener(event);
|
|
@@ -236,8 +242,52 @@ const makeValidatedRuntime = (options) => {
|
|
|
236
242
|
const providedMachines = (options.machines ?? machines());
|
|
237
243
|
const machineEntries = (options.machines ?? machines())[runtimeMachinesEntries];
|
|
238
244
|
const machineDefinitionOrder = new Map(options.machineDefinitions.map((machine, index) => [machine.key, index]));
|
|
245
|
+
/** Machines the runtime was given an initial value for. */
|
|
246
|
+
const providedMachineKeys = new Set();
|
|
239
247
|
for (const provision of machineEntries) {
|
|
240
248
|
currentMachines.set(provision.machine.key, provision.initial);
|
|
249
|
+
providedMachineKeys.add(provision.machine.key);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Sub-states by parent machine key, in definition order, and the value
|
|
253
|
+
* each enters whenever its parent enters the state it exists in (its
|
|
254
|
+
* runtime-provided value). A sub-state has no current value while its
|
|
255
|
+
* parent is elsewhere, and never one when the runtime does not provide it.
|
|
256
|
+
*/
|
|
257
|
+
const subStates = new Map();
|
|
258
|
+
const subStateEntry = new Map();
|
|
259
|
+
for (const machine of options.machineDefinitions) {
|
|
260
|
+
if (machine.within === undefined)
|
|
261
|
+
continue;
|
|
262
|
+
const siblings = subStates.get(machine.within.machine.key);
|
|
263
|
+
if (siblings === undefined)
|
|
264
|
+
subStates.set(machine.within.machine.key, [machine]);
|
|
265
|
+
else
|
|
266
|
+
siblings.push(machine);
|
|
267
|
+
if (!currentMachines.has(machine.key))
|
|
268
|
+
continue;
|
|
269
|
+
subStateEntry.set(machine.key, currentMachines.get(machine.key));
|
|
270
|
+
}
|
|
271
|
+
// Definition order puts a parent before its sub-states, so a sub-state of
|
|
272
|
+
// an inactive sub-state is cleared too.
|
|
273
|
+
for (const machine of options.machineDefinitions) {
|
|
274
|
+
if (machine.within !== undefined && currentMachines.get(machine.within.machine.key) !== machine.within.state) {
|
|
275
|
+
currentMachines.delete(machine.key);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Scope despawn triggers by machine key, in scope definition order
|
|
280
|
+
* (`Game.EntityScope(name, { despawnOnExit, despawnOnEnter })`).
|
|
281
|
+
*/
|
|
282
|
+
const scopeTriggers = new Map();
|
|
283
|
+
for (const scope of options.scopeDefinitions) {
|
|
284
|
+
for (const trigger of scope.despawnOn) {
|
|
285
|
+
const triggers = scopeTriggers.get(trigger.machine.key);
|
|
286
|
+
if (triggers === undefined)
|
|
287
|
+
scopeTriggers.set(trigger.machine.key, [{ trigger, scope }]);
|
|
288
|
+
else
|
|
289
|
+
triggers.push({ trigger, scope });
|
|
290
|
+
}
|
|
241
291
|
}
|
|
242
292
|
const appendRelationFailure = (relation, operation, sourceId, targetId, error) => {
|
|
243
293
|
if (tracing) {
|
|
@@ -247,29 +297,145 @@ const makeValidatedRuntime = (options) => {
|
|
|
247
297
|
Relation.mutationFailure(relation, operation, world.entityIdOf(sourceId), world.entityIdOf(targetId), error)
|
|
248
298
|
]);
|
|
249
299
|
};
|
|
300
|
+
/**
|
|
301
|
+
* With `debug`, the relations commands have tried to relate or
|
|
302
|
+
* reorder, with the systems that queued those commands, for the
|
|
303
|
+
* `relation-failure-never-read` lint. `commandOrigin` names the system whose
|
|
304
|
+
* command is being applied.
|
|
305
|
+
*/
|
|
306
|
+
const relationMutations = new Map();
|
|
307
|
+
let commandOrigin = "(unknown)";
|
|
308
|
+
/**
|
|
309
|
+
* With `debug`, what each system's applied commands did, as
|
|
310
|
+
* `kind:name` (`spawns:Enemy`, `despawns:Enemy` for every component the
|
|
311
|
+
* despawned entity had), and the systems that ran: for the
|
|
312
|
+
* `command-permission-unused` lint.
|
|
313
|
+
*/
|
|
314
|
+
const commandUses = new Map();
|
|
315
|
+
const ranSystems = new Set();
|
|
316
|
+
const noteCommand = (kind, name) => {
|
|
317
|
+
let uses = commandUses.get(commandOrigin);
|
|
318
|
+
if (uses === undefined) {
|
|
319
|
+
uses = new Set();
|
|
320
|
+
commandUses.set(commandOrigin, uses);
|
|
321
|
+
}
|
|
322
|
+
uses.add(`${kind}:${name}`);
|
|
323
|
+
};
|
|
324
|
+
const noteRelationMutation = (relation) => {
|
|
325
|
+
let systems = relationMutations.get(relation.name);
|
|
326
|
+
if (systems === undefined) {
|
|
327
|
+
systems = new Set();
|
|
328
|
+
relationMutations.set(relation.name, systems);
|
|
329
|
+
}
|
|
330
|
+
systems.add(commandOrigin);
|
|
331
|
+
};
|
|
332
|
+
/**
|
|
333
|
+
* `Schema.requires`, checked when commands are applied. Types reject a
|
|
334
|
+
* breaking command when they can see it; this catches the rest: a query
|
|
335
|
+
* proof gone stale within one flush (another system's command applied in
|
|
336
|
+
* between), erased types, and raw ids. A breaking command is skipped
|
|
337
|
+
* whole, traced, and reported by the `requirement-violated` lint.
|
|
338
|
+
*/
|
|
339
|
+
const requirements = requirementIndex(options.schema);
|
|
340
|
+
/** With `debug`, skipped commands by system, command, and requirement, for the lint. */
|
|
341
|
+
const requirementViolations = new Map();
|
|
342
|
+
const violateRequirement = (command, entity, [component, missing]) => {
|
|
343
|
+
if (tracing)
|
|
344
|
+
recordEffect({ kind: "requirementViolated", entity, command, component, missing });
|
|
345
|
+
if (debugEnabled) {
|
|
346
|
+
const key = `${commandOrigin}/${command}/${component}/${missing}`;
|
|
347
|
+
const seen = requirementViolations.get(key);
|
|
348
|
+
requirementViolations.set(key, { system: commandOrigin, command, component, missing, entity, count: (seen?.count ?? 0) + 1 });
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
const holds = (id) => {
|
|
352
|
+
const record = world.records.get(id);
|
|
353
|
+
return (descriptor) => record !== undefined && world.has(record, world.ordinalOf(descriptor));
|
|
354
|
+
};
|
|
355
|
+
/** The first requirement `components` break, as `[requirer, missing]`. */
|
|
356
|
+
const missingIn = (components, alsoHeld) => {
|
|
357
|
+
for (const [descriptor] of components) {
|
|
358
|
+
for (const required of requirements.requires.get(descriptor.name) ?? []) {
|
|
359
|
+
if (!alsoHeld(required) && !components.some(([staged]) => staged.name === required.name)) {
|
|
360
|
+
return [descriptor.name, required.name];
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return undefined;
|
|
365
|
+
};
|
|
250
366
|
/**
|
|
251
367
|
* World adapter used by deferred commands.
|
|
252
368
|
*/
|
|
253
369
|
const commandWorld = {
|
|
254
370
|
spawnEntity(id, components) {
|
|
371
|
+
if (!requirements.empty) {
|
|
372
|
+
const missing = missingIn(components, () => false);
|
|
373
|
+
if (missing !== undefined) {
|
|
374
|
+
violateRequirement("spawn", id, missing);
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (debugEnabled)
|
|
379
|
+
for (const [descriptor] of components)
|
|
380
|
+
noteCommand("spawns", descriptor.name);
|
|
255
381
|
world.spawnEntity(id, components);
|
|
382
|
+
return true;
|
|
256
383
|
},
|
|
257
384
|
destroyEntity(id) {
|
|
385
|
+
if (debugEnabled) {
|
|
386
|
+
const record = world.records.get(id);
|
|
387
|
+
record?.values.forEach((value, ordinal) => {
|
|
388
|
+
if (value !== ABSENT)
|
|
389
|
+
noteCommand("despawns", world.descriptorAt(ordinal).name);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
258
392
|
world.destroyEntity(id);
|
|
259
393
|
},
|
|
260
394
|
assignEntityScope(id, scope) {
|
|
261
395
|
world.assignEntityScope(id, scope.key);
|
|
262
396
|
},
|
|
263
397
|
destroyEntityScope(scope) {
|
|
398
|
+
if (debugEnabled)
|
|
399
|
+
noteCommand("despawnsScope", scope.name);
|
|
264
400
|
world.destroyEntityScope(scope.key);
|
|
265
401
|
},
|
|
266
|
-
|
|
267
|
-
world.
|
|
402
|
+
removeComponents(id, descriptors) {
|
|
403
|
+
if (!requirements.empty && world.records.has(id)) {
|
|
404
|
+
const held = holds(id);
|
|
405
|
+
for (const descriptor of descriptors) {
|
|
406
|
+
for (const requirer of requirements.requiredBy.get(descriptor.name) ?? []) {
|
|
407
|
+
if (held(requirer) && !descriptors.some((removed) => removed.name === requirer.name)) {
|
|
408
|
+
violateRequirement("remove", id, [requirer.name, descriptor.name]);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
for (const descriptor of descriptors) {
|
|
415
|
+
if (debugEnabled)
|
|
416
|
+
noteCommand("removes", descriptor.name);
|
|
417
|
+
world.removeComponent(id, descriptor);
|
|
418
|
+
}
|
|
268
419
|
},
|
|
269
|
-
|
|
270
|
-
world.
|
|
420
|
+
insertComponents(id, entries) {
|
|
421
|
+
if (!requirements.empty && world.records.has(id)) {
|
|
422
|
+
const missing = missingIn(entries, holds(id));
|
|
423
|
+
if (missing !== undefined) {
|
|
424
|
+
violateRequirement("insert", id, missing);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
for (const [descriptor, value] of entries) {
|
|
429
|
+
if (debugEnabled)
|
|
430
|
+
noteCommand("inserts", descriptor.name);
|
|
431
|
+
world.writeComponent(id, descriptor, value);
|
|
432
|
+
}
|
|
271
433
|
},
|
|
272
434
|
tryRelate(id, relation, target) {
|
|
435
|
+
if (debugEnabled) {
|
|
436
|
+
noteRelationMutation(relation);
|
|
437
|
+
noteCommand("relates", relation.name);
|
|
438
|
+
}
|
|
273
439
|
const result = world.tryRelate(id, relation, target);
|
|
274
440
|
if (!result.ok) {
|
|
275
441
|
appendRelationFailure(relation, "relate", id, target, result.error);
|
|
@@ -277,9 +443,15 @@ const makeValidatedRuntime = (options) => {
|
|
|
277
443
|
return result;
|
|
278
444
|
},
|
|
279
445
|
unrelate(id, relation) {
|
|
446
|
+
if (debugEnabled)
|
|
447
|
+
noteCommand("relates", relation.name);
|
|
280
448
|
world.unrelate(id, relation);
|
|
281
449
|
},
|
|
282
450
|
reorderChildren(id, relation, children) {
|
|
451
|
+
if (debugEnabled) {
|
|
452
|
+
noteRelationMutation(relation);
|
|
453
|
+
noteCommand("relates", relation.name);
|
|
454
|
+
}
|
|
283
455
|
const result = world.reorderChildren(id, relation, children.map((child) => child));
|
|
284
456
|
if (!result.ok) {
|
|
285
457
|
const targetId = result.error._tag === "MissingChildEntity"
|
|
@@ -306,12 +478,6 @@ const makeValidatedRuntime = (options) => {
|
|
|
306
478
|
};
|
|
307
479
|
const makeLookup = (reader) => {
|
|
308
480
|
const lookup = {
|
|
309
|
-
get(target, query) {
|
|
310
|
-
return resolve(target, query, reader);
|
|
311
|
-
},
|
|
312
|
-
getHandle(handle, query) {
|
|
313
|
-
return resolve(handle, query, reader);
|
|
314
|
-
},
|
|
315
481
|
related: relatedTarget,
|
|
316
482
|
relatedSources(target, relation) {
|
|
317
483
|
if (!world.records.has(target)) {
|
|
@@ -319,19 +485,6 @@ const makeValidatedRuntime = (options) => {
|
|
|
319
485
|
}
|
|
320
486
|
return Result.success(world.relatedSourceIds(relation, target).map(entityId));
|
|
321
487
|
},
|
|
322
|
-
childMatches(target, relation, query) {
|
|
323
|
-
if (!world.records.has(target)) {
|
|
324
|
-
return Result.failure(Relation.missingEntityError(target));
|
|
325
|
-
}
|
|
326
|
-
const matches = [];
|
|
327
|
-
for (const sourceId of world.relatedSourceIds(relation, target)) {
|
|
328
|
-
const resolved = resolve(sourceId, query, reader);
|
|
329
|
-
if (resolved.ok) {
|
|
330
|
-
matches.push(resolved.value);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
return Result.success(matches);
|
|
334
|
-
},
|
|
335
488
|
parent: relatedTarget,
|
|
336
489
|
ancestors(target, relation) {
|
|
337
490
|
if (!world.records.has(target)) {
|
|
@@ -371,20 +524,6 @@ const makeValidatedRuntime = (options) => {
|
|
|
371
524
|
}
|
|
372
525
|
return Result.success(descendants);
|
|
373
526
|
},
|
|
374
|
-
descendantMatches(target, relation, query, options) {
|
|
375
|
-
const descendants = lookup.descendants(target, relation, options);
|
|
376
|
-
if (!descendants.ok) {
|
|
377
|
-
return descendants;
|
|
378
|
-
}
|
|
379
|
-
const matches = [];
|
|
380
|
-
for (const descendantId of descendants.value) {
|
|
381
|
-
const resolved = resolve(descendantId, query, reader);
|
|
382
|
-
if (resolved.ok) {
|
|
383
|
-
matches.push(resolved.value);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
return Result.success(matches);
|
|
387
|
-
},
|
|
388
527
|
root(target, relation) {
|
|
389
528
|
if (!world.records.has(target)) {
|
|
390
529
|
return Result.failure(Relation.missingEntityError(target));
|
|
@@ -400,13 +539,34 @@ const makeValidatedRuntime = (options) => {
|
|
|
400
539
|
};
|
|
401
540
|
return lookup;
|
|
402
541
|
};
|
|
403
|
-
const makeQueryHandle = (query, reader) => {
|
|
542
|
+
const makeQueryHandle = (query, reader, lookup) => {
|
|
404
543
|
const each = () => queries.each(query, reader.since);
|
|
544
|
+
const matching = (ids) => {
|
|
545
|
+
const matches = [];
|
|
546
|
+
for (const id of ids) {
|
|
547
|
+
const resolved = resolve(id, query, reader);
|
|
548
|
+
if (resolved.ok)
|
|
549
|
+
matches.push(resolved.value);
|
|
550
|
+
}
|
|
551
|
+
return matches;
|
|
552
|
+
};
|
|
405
553
|
return {
|
|
406
554
|
each,
|
|
407
555
|
get(target) {
|
|
408
556
|
return resolve(target, query, reader);
|
|
409
557
|
},
|
|
558
|
+
getHandle(handle) {
|
|
559
|
+
return resolve(handle, query, reader);
|
|
560
|
+
},
|
|
561
|
+
children(target, relation) {
|
|
562
|
+
if (!world.records.has(target))
|
|
563
|
+
return Result.failure(Relation.missingEntityError(target));
|
|
564
|
+
return Result.success(matching(world.relatedSourceIds(relation, target).map(entityId)));
|
|
565
|
+
},
|
|
566
|
+
descendants(target, relation, options) {
|
|
567
|
+
const ids = lookup.descendants(target, relation, options);
|
|
568
|
+
return ids.ok ? Result.success(matching(ids.value)) : ids;
|
|
569
|
+
},
|
|
410
570
|
single() {
|
|
411
571
|
const matches = each();
|
|
412
572
|
if (matches.length === 0) {
|
|
@@ -444,6 +604,14 @@ const makeValidatedRuntime = (options) => {
|
|
|
444
604
|
store.set(key, value);
|
|
445
605
|
};
|
|
446
606
|
const writeResource = journalStoreWrite(resources, resourceOriginals);
|
|
607
|
+
/**
|
|
608
|
+
* Resource change ticks for `changedResource` readers. Only resources some
|
|
609
|
+
* system reads with `changedResource` are compared at commit; a reader's
|
|
610
|
+
* first run reports a change anyway, so writes before it registers are not
|
|
611
|
+
* needed.
|
|
612
|
+
*/
|
|
613
|
+
const watchedResources = new Set();
|
|
614
|
+
const resourceChangedAt = new Map();
|
|
447
615
|
const journalMachine = (key) => {
|
|
448
616
|
if (!machineOriginals.has(key)) {
|
|
449
617
|
machineOriginals.set(key, pendingMachines.get(key) ?? absentValue);
|
|
@@ -454,6 +622,15 @@ const makeValidatedRuntime = (options) => {
|
|
|
454
622
|
};
|
|
455
623
|
const commitSystemTransaction = () => {
|
|
456
624
|
world.commitTransaction();
|
|
625
|
+
if (watchedResources.size > 0) {
|
|
626
|
+
for (const [key, before] of resourceOriginals) {
|
|
627
|
+
if (!watchedResources.has(key))
|
|
628
|
+
continue;
|
|
629
|
+
if (before === absentValue || !sameValue(before, resources.get(key))) {
|
|
630
|
+
resourceChangedAt.set(key, world.currentTick());
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
457
634
|
if (emittedEvents.size > 0) {
|
|
458
635
|
const published = world.advanceTick();
|
|
459
636
|
for (const [key, values] of emittedEvents) {
|
|
@@ -490,6 +667,16 @@ const makeValidatedRuntime = (options) => {
|
|
|
490
667
|
emittedEvents.clear();
|
|
491
668
|
};
|
|
492
669
|
const makeResourceWriteView = (descriptor, store, write) => Cells.storeWrite(store, descriptor.key, write, DescriptorModule.constructorOf(descriptor));
|
|
670
|
+
const makeResourceChangedView = (descriptor, reader) => {
|
|
671
|
+
const key = descriptor.key;
|
|
672
|
+
watchedResources.add(key);
|
|
673
|
+
const read = Cells.storeRead(resources, key);
|
|
674
|
+
return {
|
|
675
|
+
get: () => read.get(),
|
|
676
|
+
// `since` is the previous completed run; 0 before the first one.
|
|
677
|
+
changed: () => reader.since === 0 || (resourceChangedAt.get(key) ?? 0) > reader.since
|
|
678
|
+
};
|
|
679
|
+
};
|
|
493
680
|
const makeEventReadView = (descriptorKey, reader) => ({
|
|
494
681
|
all() {
|
|
495
682
|
return events.since(descriptorKey, reader.streamSince);
|
|
@@ -522,12 +709,18 @@ const makeValidatedRuntime = (options) => {
|
|
|
522
709
|
return {
|
|
523
710
|
all() {
|
|
524
711
|
return world.removedSince(ordinal, reader.since).map(entityId);
|
|
712
|
+
},
|
|
713
|
+
lagged() {
|
|
714
|
+
return world.removedLagged(ordinal, reader.since, reader.registeredAt);
|
|
525
715
|
}
|
|
526
716
|
};
|
|
527
717
|
};
|
|
528
718
|
const makeDespawnedReadView = (reader) => ({
|
|
529
719
|
all() {
|
|
530
720
|
return world.despawnedSince(reader.since).map(entityId);
|
|
721
|
+
},
|
|
722
|
+
lagged() {
|
|
723
|
+
return world.despawnedLagged(reader.since, reader.registeredAt);
|
|
531
724
|
}
|
|
532
725
|
});
|
|
533
726
|
const makeRelationFailureReadView = (relation, reader) => ({
|
|
@@ -542,15 +735,28 @@ const makeValidatedRuntime = (options) => {
|
|
|
542
735
|
get: () => currentMachines.get(stateMachine.key),
|
|
543
736
|
is: (value) => currentMachines.get(stateMachine.key) === value
|
|
544
737
|
});
|
|
738
|
+
/** Queues a next value; a sub-state whose parent is in another state queues nothing and fails. */
|
|
739
|
+
const queueMachine = (stateMachine, value, skipIfSame) => {
|
|
740
|
+
const within = stateMachine.within;
|
|
741
|
+
if (within !== undefined && !currentMachines.has(stateMachine.key)) {
|
|
742
|
+
return Result.failure({
|
|
743
|
+
_tag: "SubStateInactive",
|
|
744
|
+
machine: stateMachine.name,
|
|
745
|
+
parent: within.machine.name,
|
|
746
|
+
requires: within.state
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
journalMachine(stateMachine.key);
|
|
750
|
+
pendingMachines.set(stateMachine.key, { value, skipIfSame });
|
|
751
|
+
return (within === undefined ? undefined : succeeded);
|
|
752
|
+
};
|
|
545
753
|
const makeNextMachineWriteView = (stateMachine) => ({
|
|
546
754
|
getPending: () => pendingMachines.get(stateMachine.key)?.value,
|
|
547
755
|
set(value) {
|
|
548
|
-
|
|
549
|
-
pendingMachines.set(stateMachine.key, { value, skipIfSame: false });
|
|
756
|
+
return queueMachine(stateMachine, value, false);
|
|
550
757
|
},
|
|
551
758
|
setIfChanged(value) {
|
|
552
|
-
|
|
553
|
-
pendingMachines.set(stateMachine.key, { value, skipIfSame: true });
|
|
759
|
+
return queueMachine(stateMachine, value, true);
|
|
554
760
|
},
|
|
555
761
|
reset() {
|
|
556
762
|
journalMachine(stateMachine.key);
|
|
@@ -595,15 +801,18 @@ const makeValidatedRuntime = (options) => {
|
|
|
595
801
|
* views the implementation is allowed to see. Views read live storage, so
|
|
596
802
|
* one context is built per system and reused for every run.
|
|
597
803
|
*/
|
|
598
|
-
const makeContext = (system, reader) => {
|
|
804
|
+
const makeContext = (system, reader, queue) => {
|
|
599
805
|
const spec = system.spec;
|
|
806
|
+
const lookup = makeLookup(reader);
|
|
600
807
|
return {
|
|
601
808
|
local: spec.local?.(),
|
|
602
|
-
queries: mapRecord(spec.queries, (query) => makeQueryHandle(query, reader)),
|
|
603
|
-
lookup
|
|
809
|
+
queries: mapRecord(spec.queries, (query) => makeQueryHandle(query, reader, lookup)),
|
|
810
|
+
lookup,
|
|
604
811
|
resources: mapRecord(spec.resources, (access) => access.mode === "read"
|
|
605
812
|
? Cells.storeRead(resources, access.descriptor.key)
|
|
606
|
-
:
|
|
813
|
+
: access.mode === "changed"
|
|
814
|
+
? makeResourceChangedView(access.descriptor, reader)
|
|
815
|
+
: makeResourceWriteView(access.descriptor, resources, writeResource)),
|
|
607
816
|
events: mapRecord(spec.events, (access) => access.mode === "read"
|
|
608
817
|
? makeEventReadView(access.descriptor.key, reader)
|
|
609
818
|
: makeEventWriteView(access.descriptor.key)),
|
|
@@ -615,7 +824,8 @@ const makeValidatedRuntime = (options) => {
|
|
|
615
824
|
despawned: mapRecord(spec.despawned, () => makeDespawnedReadView(reader)),
|
|
616
825
|
relationFailures: mapRecord(spec.relationFailures, (access) => makeRelationFailureReadView(access.relation, reader)),
|
|
617
826
|
services: mapRecord(spec.services, (access) => providedServices[access.descriptor.name]),
|
|
618
|
-
|
|
827
|
+
// The queue serves every declared permission set; the spec's types narrow it.
|
|
828
|
+
commands: queue
|
|
619
829
|
};
|
|
620
830
|
};
|
|
621
831
|
const slots = new WeakMap();
|
|
@@ -686,7 +896,7 @@ const makeValidatedRuntime = (options) => {
|
|
|
686
896
|
system: system.name,
|
|
687
897
|
outcome,
|
|
688
898
|
...(outcome === "ok" ? {} : { error }),
|
|
689
|
-
ms: now()
|
|
899
|
+
ms: HostClockModule.elapsed(started, now()),
|
|
690
900
|
writes,
|
|
691
901
|
resources: resourceWrites,
|
|
692
902
|
events: emitted,
|
|
@@ -748,7 +958,8 @@ const makeValidatedRuntime = (options) => {
|
|
|
748
958
|
let slot = slots.get(stateKey(system));
|
|
749
959
|
if (!slot) {
|
|
750
960
|
const reader = { since: 0, lastRun: 0, streamSince: 0, streamLastRun: 0, registeredAt: world.currentTick() };
|
|
751
|
-
|
|
961
|
+
const commands = Command.makeCommands(() => world.allocateEntity());
|
|
962
|
+
slot = { context: makeContext(system, reader, commands), commands, reader };
|
|
752
963
|
slots.set(stateKey(system), slot);
|
|
753
964
|
if (holdsStreams)
|
|
754
965
|
registerStreamReader(system, slot.reader);
|
|
@@ -789,7 +1000,9 @@ const makeValidatedRuntime = (options) => {
|
|
|
789
1000
|
return succeeded;
|
|
790
1001
|
}
|
|
791
1002
|
}
|
|
792
|
-
const { context, reader } = slotOf(system, true);
|
|
1003
|
+
const { context, commands, reader } = slotOf(system, true);
|
|
1004
|
+
if (debugEnabled)
|
|
1005
|
+
ranSystems.add(system.name);
|
|
793
1006
|
// Changes stamped after the previous completed run are visible to this run.
|
|
794
1007
|
reader.since = reader.lastRun;
|
|
795
1008
|
reader.streamSince = reader.streamLastRun;
|
|
@@ -798,26 +1011,24 @@ const makeValidatedRuntime = (options) => {
|
|
|
798
1011
|
beginSystemTransaction();
|
|
799
1012
|
let outcome;
|
|
800
1013
|
try {
|
|
801
|
-
// A system that returns nothing cannot fail
|
|
802
|
-
|
|
803
|
-
const effect = system.run(context);
|
|
804
|
-
outcome = effect === undefined ? succeeded : effect.run(context.services);
|
|
1014
|
+
// A system that returns nothing cannot fail; otherwise it returned a `Result`.
|
|
1015
|
+
outcome = system.run(context) ?? succeeded;
|
|
805
1016
|
}
|
|
806
1017
|
catch (defect) {
|
|
807
1018
|
if (tracing)
|
|
808
|
-
emit(traceSystem(system, reader, thisRun, started, "defect", defect,
|
|
1019
|
+
emit(traceSystem(system, reader, thisRun, started, "defect", defect, commands.flush()));
|
|
809
1020
|
rollbackSystemTransaction();
|
|
810
|
-
|
|
1021
|
+
commands.flush();
|
|
811
1022
|
throw defect;
|
|
812
1023
|
}
|
|
813
1024
|
if (!outcome.ok) {
|
|
814
1025
|
if (tracing)
|
|
815
|
-
emit(traceSystem(system, reader, thisRun, started, "failed", outcome.error,
|
|
1026
|
+
emit(traceSystem(system, reader, thisRun, started, "failed", outcome.error, commands.flush()));
|
|
816
1027
|
rollbackSystemTransaction();
|
|
817
|
-
|
|
1028
|
+
commands.flush();
|
|
818
1029
|
return Result.failure({ kind: "SystemFailure", system: system.name, error: outcome.error });
|
|
819
1030
|
}
|
|
820
|
-
const queued =
|
|
1031
|
+
const queued = commands.flush();
|
|
821
1032
|
const traced = tracing ? traceSystem(system, reader, thisRun, started, "ok", undefined, queued) : undefined;
|
|
822
1033
|
commitSystemTransaction();
|
|
823
1034
|
// A failed run leaves `lastRun` unchanged, so the next run sees the same changes again.
|
|
@@ -840,8 +1051,15 @@ const makeValidatedRuntime = (options) => {
|
|
|
840
1051
|
const origins = pendingOrigins.splice(0, pendingOrigins.length);
|
|
841
1052
|
world.advanceTick();
|
|
842
1053
|
if (!tracing) {
|
|
843
|
-
|
|
844
|
-
command
|
|
1054
|
+
if (!debugEnabled) {
|
|
1055
|
+
for (const command of commands) {
|
|
1056
|
+
command.apply(commandWorld);
|
|
1057
|
+
}
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
for (let index = 0; index < commands.length; index++) {
|
|
1061
|
+
commandOrigin = origins[index] ?? "(unknown)";
|
|
1062
|
+
commands[index].apply(commandWorld);
|
|
845
1063
|
}
|
|
846
1064
|
return;
|
|
847
1065
|
}
|
|
@@ -850,6 +1068,7 @@ const makeValidatedRuntime = (options) => {
|
|
|
850
1068
|
const command = commands[index];
|
|
851
1069
|
const effects = [];
|
|
852
1070
|
commandEffects = effects;
|
|
1071
|
+
commandOrigin = origins[index] ?? "(unknown)";
|
|
853
1072
|
try {
|
|
854
1073
|
command.apply(commandWorld);
|
|
855
1074
|
}
|
|
@@ -875,7 +1094,7 @@ const makeValidatedRuntime = (options) => {
|
|
|
875
1094
|
finally {
|
|
876
1095
|
schedulePath.pop();
|
|
877
1096
|
}
|
|
878
|
-
emit({ type: "schedule.end", frame: frameCount, schedule: scheduleLabel, ok: result.ok, ms: now()
|
|
1097
|
+
emit({ type: "schedule.end", frame: frameCount, schedule: scheduleLabel, ok: result.ok, ms: HostClockModule.elapsed(started, now()) });
|
|
879
1098
|
return result;
|
|
880
1099
|
};
|
|
881
1100
|
const traceTransition = (machineKey, snapshot, outcome) => {
|
|
@@ -904,6 +1123,39 @@ const makeValidatedRuntime = (options) => {
|
|
|
904
1123
|
activeTransitions.delete(schedule.transition.machine.key);
|
|
905
1124
|
}
|
|
906
1125
|
};
|
|
1126
|
+
/**
|
|
1127
|
+
* Despawns the scopes whose trigger matches a committed transition: leaving
|
|
1128
|
+
* `snapshot.from` or entering `snapshot.to`. It runs right after the
|
|
1129
|
+
* commit, so `onExit` systems still saw the members and entities spawned by
|
|
1130
|
+
* `onEnter` survive. Members are the entities in the scope at that moment:
|
|
1131
|
+
* commands queued by `onExit` systems and not yet applied are not.
|
|
1132
|
+
*/
|
|
1133
|
+
const despawnTriggeredScopes = (machineKey, snapshot) => {
|
|
1134
|
+
const triggers = scopeTriggers.get(machineKey);
|
|
1135
|
+
if (triggers === undefined)
|
|
1136
|
+
return;
|
|
1137
|
+
const applied = [];
|
|
1138
|
+
for (const { trigger, scope } of triggers) {
|
|
1139
|
+
const matches = trigger.phase === "exit" ? trigger.state === snapshot.from : trigger.state === snapshot.to;
|
|
1140
|
+
if (!matches)
|
|
1141
|
+
continue;
|
|
1142
|
+
const origin = `${trigger.phase === "exit" ? "onExit" : "onEnter"}(${trigger.machine.name}=${String(trigger.state)})`;
|
|
1143
|
+
const effects = [];
|
|
1144
|
+
commandEffects = tracing ? effects : undefined;
|
|
1145
|
+
commandOrigin = origin;
|
|
1146
|
+
try {
|
|
1147
|
+
commandWorld.destroyEntityScope(scope);
|
|
1148
|
+
}
|
|
1149
|
+
finally {
|
|
1150
|
+
commandEffects = undefined;
|
|
1151
|
+
}
|
|
1152
|
+
if (tracing)
|
|
1153
|
+
applied.push({ tag: `despawnScope ${scope.name}`, system: origin, effects });
|
|
1154
|
+
}
|
|
1155
|
+
if (applied.length > 0) {
|
|
1156
|
+
emit({ type: "deferred", frame: frameCount, tick: world.currentTick(), schedule: currentSchedule(), marker: "applyStateTransitions", commands: applied });
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
907
1159
|
/**
|
|
908
1160
|
* Applies queued commands, then queued machine transitions, running the
|
|
909
1161
|
* matching exit, transition, and enter schedules from the bundle.
|
|
@@ -916,6 +1168,10 @@ const makeValidatedRuntime = (options) => {
|
|
|
916
1168
|
- (machineDefinitionOrder.get(rightKey) ?? Number.MAX_SAFE_INTEGER));
|
|
917
1169
|
const schedules = bundle?.entries ?? [];
|
|
918
1170
|
for (const [machineKey, pending] of scheduledTransitions) {
|
|
1171
|
+
if (pendingMachines.get(machineKey) !== pending) {
|
|
1172
|
+
// Discarded earlier in this marker: its parent left the state it exists in.
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
919
1175
|
pendingMachines.delete(machineKey);
|
|
920
1176
|
const current = currentMachines.get(machineKey);
|
|
921
1177
|
if (current === undefined) {
|
|
@@ -926,11 +1182,24 @@ const makeValidatedRuntime = (options) => {
|
|
|
926
1182
|
traceTransition(machineKey, { from: current, to: current }, "unchanged");
|
|
927
1183
|
continue;
|
|
928
1184
|
}
|
|
929
|
-
previousMachines.set(machineKey, current);
|
|
930
1185
|
const snapshot = {
|
|
931
1186
|
from: current,
|
|
932
1187
|
to: pending.value
|
|
933
1188
|
};
|
|
1189
|
+
// Sub-states that exist in the state being left exit first, deepest
|
|
1190
|
+
// first: their exit schedules run before the parent's, while they
|
|
1191
|
+
// still have their values, and they lose them at the parent's commit.
|
|
1192
|
+
const leaving = activeSubStatesIn(machineKey, snapshot.from);
|
|
1193
|
+
for (const sub of leaving) {
|
|
1194
|
+
const value = currentMachines.get(sub.key);
|
|
1195
|
+
const result = runPhase(sub.key, "exit", value, { from: value, to: undefined }, schedules);
|
|
1196
|
+
if (!result.ok) {
|
|
1197
|
+
pendingMachines.set(machineKey, pending);
|
|
1198
|
+
if (tracing)
|
|
1199
|
+
traceTransition(machineKey, snapshot, "failed");
|
|
1200
|
+
return result;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
934
1203
|
for (const schedule of schedules) {
|
|
935
1204
|
const transition = schedule.transition;
|
|
936
1205
|
if (transition.phase === "exit" && transition.machine.key === machineKey && transition.state === snapshot.from) {
|
|
@@ -959,11 +1228,9 @@ const makeValidatedRuntime = (options) => {
|
|
|
959
1228
|
}
|
|
960
1229
|
}
|
|
961
1230
|
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
if (tracing)
|
|
966
|
-
traceTransition(machineKey, snapshot, "applied");
|
|
1231
|
+
for (const sub of leaving)
|
|
1232
|
+
commitMachine(sub.key, currentMachines.get(sub.key), undefined);
|
|
1233
|
+
commitMachine(machineKey, current, pending.value);
|
|
967
1234
|
for (const schedule of schedules) {
|
|
968
1235
|
const transition = schedule.transition;
|
|
969
1236
|
if (transition.phase === "enter" && transition.machine.key === machineKey && transition.state === snapshot.to) {
|
|
@@ -975,6 +1242,79 @@ const makeValidatedRuntime = (options) => {
|
|
|
975
1242
|
}
|
|
976
1243
|
}
|
|
977
1244
|
}
|
|
1245
|
+
// Sub-states that exist in the entered state enter their value, parents first.
|
|
1246
|
+
const entered = enterSubStates(machineKey, snapshot.to, schedules);
|
|
1247
|
+
if (!entered.ok)
|
|
1248
|
+
return entered;
|
|
1249
|
+
}
|
|
1250
|
+
return succeeded;
|
|
1251
|
+
};
|
|
1252
|
+
/**
|
|
1253
|
+
* Commits one machine's value (`undefined` for a sub-state that stops
|
|
1254
|
+
* existing): records the change and its transition event, traces it, and
|
|
1255
|
+
* despawns the scopes it triggers.
|
|
1256
|
+
*/
|
|
1257
|
+
const commitMachine = (machineKey, from, to) => {
|
|
1258
|
+
const snapshot = { from, to };
|
|
1259
|
+
if (from !== undefined)
|
|
1260
|
+
previousMachines.set(machineKey, from);
|
|
1261
|
+
if (to === undefined) {
|
|
1262
|
+
currentMachines.delete(machineKey);
|
|
1263
|
+
pendingMachines.delete(machineKey);
|
|
1264
|
+
}
|
|
1265
|
+
else {
|
|
1266
|
+
currentMachines.set(machineKey, to);
|
|
1267
|
+
}
|
|
1268
|
+
changedMachines.add(machineKey);
|
|
1269
|
+
transitionEvents.append(machineKey, world.advanceTick(), [snapshot]);
|
|
1270
|
+
if (tracing)
|
|
1271
|
+
traceTransition(machineKey, snapshot, "applied");
|
|
1272
|
+
despawnTriggeredScopes(machineKey, snapshot);
|
|
1273
|
+
};
|
|
1274
|
+
/** Runs every schedule of `phase` for `machineKey` in `state`, in bundle order. */
|
|
1275
|
+
const runPhase = (machineKey, phase, state, snapshot, schedules) => {
|
|
1276
|
+
for (const schedule of schedules) {
|
|
1277
|
+
const transition = schedule.transition;
|
|
1278
|
+
if (transition.phase === phase && transition.machine.key === machineKey && transition.state === state) {
|
|
1279
|
+
const result = runTransitionSchedule(schedule, snapshot);
|
|
1280
|
+
if (!result.ok)
|
|
1281
|
+
return result;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return succeeded;
|
|
1285
|
+
};
|
|
1286
|
+
/** The sub-states with a value that exist in `state` of `parentKey`, and theirs, deepest first. */
|
|
1287
|
+
const activeSubStatesIn = (parentKey, state) => {
|
|
1288
|
+
const result = [];
|
|
1289
|
+
for (const sub of subStates.get(parentKey) ?? []) {
|
|
1290
|
+
const value = currentMachines.get(sub.key);
|
|
1291
|
+
if (sub.within.state !== state || value === undefined)
|
|
1292
|
+
continue;
|
|
1293
|
+
result.push(...activeSubStatesIn(sub.key, value), sub);
|
|
1294
|
+
}
|
|
1295
|
+
return result;
|
|
1296
|
+
};
|
|
1297
|
+
/**
|
|
1298
|
+
* Gives the sub-states of `parentKey` that exist in `state` their entry
|
|
1299
|
+
* value, then runs their `onEnter` schedules and enters their own
|
|
1300
|
+
* sub-states.
|
|
1301
|
+
*/
|
|
1302
|
+
const enterSubStates = (parentKey, state, schedules) => {
|
|
1303
|
+
for (const sub of subStates.get(parentKey) ?? []) {
|
|
1304
|
+
if (sub.within.state !== state || currentMachines.has(sub.key) || !subStateEntry.has(sub.key))
|
|
1305
|
+
continue;
|
|
1306
|
+
const value = subStateEntry.get(sub.key);
|
|
1307
|
+
commitMachine(sub.key, undefined, value);
|
|
1308
|
+
const snapshot = { from: undefined, to: value };
|
|
1309
|
+
const result = runPhase(sub.key, "enter", value, snapshot, schedules);
|
|
1310
|
+
if (!result.ok) {
|
|
1311
|
+
if (tracing)
|
|
1312
|
+
traceTransition(sub.key, snapshot, "enterFailed");
|
|
1313
|
+
return result;
|
|
1314
|
+
}
|
|
1315
|
+
const nested = enterSubStates(sub.key, value, schedules);
|
|
1316
|
+
if (!nested.ok)
|
|
1317
|
+
return nested;
|
|
978
1318
|
}
|
|
979
1319
|
return succeeded;
|
|
980
1320
|
};
|
|
@@ -1023,6 +1363,14 @@ const makeValidatedRuntime = (options) => {
|
|
|
1023
1363
|
};
|
|
1024
1364
|
const eventsByKey = new Set(Object.values(options.schema.events).map((descriptor) => descriptor.key));
|
|
1025
1365
|
const externallyWrittenEvents = new Set();
|
|
1366
|
+
/** `debug.observeHost` listeners: ticks and sends, without tracing. Only a debug runtime registers any. */
|
|
1367
|
+
const hostListeners = new Set();
|
|
1368
|
+
const emitHost = (event) => {
|
|
1369
|
+
for (const listener of hostListeners)
|
|
1370
|
+
listener(event);
|
|
1371
|
+
};
|
|
1372
|
+
/** Events declared as sent by the host (`debug.declareHostEvents`). */
|
|
1373
|
+
let hostEvents = new Set();
|
|
1026
1374
|
const send = (descriptor, value) => {
|
|
1027
1375
|
if (ticking)
|
|
1028
1376
|
throw new Error(`runtime.send(${descriptor.name}) was called while a schedule is running: emit from a system instead`);
|
|
@@ -1033,6 +1381,8 @@ const makeValidatedRuntime = (options) => {
|
|
|
1033
1381
|
// Bound host traffic even when it sends repeatedly without ticking.
|
|
1034
1382
|
events.trim(0);
|
|
1035
1383
|
externallyWrittenEvents.add(descriptor.name);
|
|
1384
|
+
if (hostListeners.size > 0)
|
|
1385
|
+
emitHost({ type: "send", event: descriptor.name, value });
|
|
1036
1386
|
if (tracing)
|
|
1037
1387
|
emit({ type: "runtime.sent", frame: frameCount, tick: published, event: descriptor.name, value });
|
|
1038
1388
|
};
|
|
@@ -1045,23 +1395,90 @@ const makeValidatedRuntime = (options) => {
|
|
|
1045
1395
|
transitionEvents.trim(boundary);
|
|
1046
1396
|
relationFailures.trim(boundary);
|
|
1047
1397
|
};
|
|
1398
|
+
/**
|
|
1399
|
+
* With debug: the last failed tick result, until its `ok` is read. The
|
|
1400
|
+
* result handed back is a copy whose `ok` getter marks it read.
|
|
1401
|
+
*/
|
|
1402
|
+
let unread;
|
|
1403
|
+
const reportUnread = (when) => {
|
|
1404
|
+
const pending = unread;
|
|
1405
|
+
unread = undefined;
|
|
1406
|
+
if (pending === undefined || pending.read)
|
|
1407
|
+
return;
|
|
1408
|
+
const event = { type: "failure.ignored", ...pending.event, when };
|
|
1409
|
+
emit(event);
|
|
1410
|
+
emitHost(event);
|
|
1411
|
+
};
|
|
1412
|
+
const watchResult = (result, schedules) => {
|
|
1413
|
+
if (!debugEnabled || result.ok)
|
|
1414
|
+
return result;
|
|
1415
|
+
const error = result.error;
|
|
1416
|
+
const system = typeof error === "object" && error !== null && error.kind === "SystemFailure"
|
|
1417
|
+
? error.system
|
|
1418
|
+
: undefined;
|
|
1419
|
+
const names = schedules.flatMap((schedule) => schedule.steps.filter(Schedule.isSystemStep).map((step) => step.name));
|
|
1420
|
+
const at = system === undefined ? -1 : names.indexOf(system);
|
|
1421
|
+
const holder = system === undefined
|
|
1422
|
+
? schedules[0]
|
|
1423
|
+
: schedules.find((schedule) => schedule.steps.some((step) => Schedule.isSystemStep(step) && step.name === system)) ?? schedules[0];
|
|
1424
|
+
const entry = {
|
|
1425
|
+
read: false,
|
|
1426
|
+
event: {
|
|
1427
|
+
frame: frameCount,
|
|
1428
|
+
schedule: holder === undefined ? "(none)" : scheduleName(holder),
|
|
1429
|
+
system,
|
|
1430
|
+
error,
|
|
1431
|
+
skipped: [...new Set(names.slice(at + 1))]
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
unread = entry;
|
|
1435
|
+
return {
|
|
1436
|
+
get ok() {
|
|
1437
|
+
entry.read = true;
|
|
1438
|
+
return false;
|
|
1439
|
+
},
|
|
1440
|
+
error
|
|
1441
|
+
};
|
|
1442
|
+
};
|
|
1048
1443
|
const tickUnsafe = (schedules) => {
|
|
1049
1444
|
if (ticking)
|
|
1050
1445
|
throw new Error("runtime.tick cannot run inside another tick");
|
|
1446
|
+
if (debugEnabled)
|
|
1447
|
+
reportUnread("next-tick");
|
|
1051
1448
|
ticking = true;
|
|
1052
1449
|
try {
|
|
1053
1450
|
advanceFrame();
|
|
1054
|
-
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1451
|
+
return watchResult(observedTick(schedules, () => {
|
|
1452
|
+
for (const schedule of schedules) {
|
|
1453
|
+
const result = runScheduleUnsafe(schedule);
|
|
1454
|
+
if (!result.ok)
|
|
1455
|
+
return result;
|
|
1456
|
+
}
|
|
1457
|
+
return succeeded;
|
|
1458
|
+
}), schedules);
|
|
1060
1459
|
}
|
|
1061
1460
|
finally {
|
|
1062
1461
|
ticking = false;
|
|
1063
1462
|
}
|
|
1064
1463
|
};
|
|
1464
|
+
/** Runs one tick's schedules, telling `observeHost` listeners when it starts and how it ends. */
|
|
1465
|
+
const observedTick = (schedules, run) => {
|
|
1466
|
+
if (hostListeners.size === 0)
|
|
1467
|
+
return run();
|
|
1468
|
+
emitHost({ type: "tick.start", schedules, frame: frameCount });
|
|
1469
|
+
let result;
|
|
1470
|
+
try {
|
|
1471
|
+
result = run();
|
|
1472
|
+
}
|
|
1473
|
+
catch (defect) {
|
|
1474
|
+
emitHost({ type: "tick.end", schedules, frame: frameCount, outcome: "defect", error: defect });
|
|
1475
|
+
throw defect;
|
|
1476
|
+
}
|
|
1477
|
+
emitHost(result.ok
|
|
1478
|
+
? { type: "tick.end", schedules, frame: frameCount, outcome: "ok" }
|
|
1479
|
+
: { type: "tick.end", schedules, frame: frameCount, outcome: "failed", error: result.error });
|
|
1480
|
+
return result;
|
|
1481
|
+
};
|
|
1065
1482
|
const hasRequirement = (requirement) => {
|
|
1066
1483
|
switch (requirement.kind) {
|
|
1067
1484
|
case "service":
|
|
@@ -1069,21 +1486,28 @@ const makeValidatedRuntime = (options) => {
|
|
|
1069
1486
|
case "resource":
|
|
1070
1487
|
return resources.has(requirement.key);
|
|
1071
1488
|
case "stateMachine":
|
|
1072
|
-
|
|
1489
|
+
// An inactive sub-state has no value but is provided.
|
|
1490
|
+
return providedMachineKeys.has(requirement.key) || currentMachines.has(requirement.key);
|
|
1073
1491
|
}
|
|
1074
1492
|
};
|
|
1075
1493
|
const tryRunSchedule = (schedule) => {
|
|
1494
|
+
if (debugEnabled)
|
|
1495
|
+
reportUnread("next-tick");
|
|
1076
1496
|
const missing = schedule.requirements
|
|
1077
1497
|
.filter((requirement) => !hasRequirement(requirement))
|
|
1078
1498
|
.map(({ kind, name }) => ({ kind, name }));
|
|
1079
1499
|
if (missing.length > 0) {
|
|
1080
|
-
return Result.failure({
|
|
1500
|
+
return watchResult(Result.failure({
|
|
1081
1501
|
kind: "MissingRuntimeRequirements",
|
|
1082
1502
|
requirements: missing
|
|
1083
|
-
});
|
|
1503
|
+
}), [schedule]);
|
|
1504
|
+
}
|
|
1505
|
+
const issues = OrderInternal.tickOrderIssues(schedule.steps);
|
|
1506
|
+
if (issues.length > 0) {
|
|
1507
|
+
return watchResult(Result.failure({ kind: "TickOrderViolation", issues }), [schedule]);
|
|
1084
1508
|
}
|
|
1085
1509
|
advanceFrame();
|
|
1086
|
-
return runScheduleUnsafe(schedule);
|
|
1510
|
+
return watchResult(observedTick([schedule], () => runScheduleUnsafe(schedule)), [schedule]);
|
|
1087
1511
|
};
|
|
1088
1512
|
/**
|
|
1089
1513
|
* Evaluates a declared read-only projection without running a schedule.
|
|
@@ -1167,6 +1591,11 @@ const makeValidatedRuntime = (options) => {
|
|
|
1167
1591
|
return Result.failure({ _tag: "InvalidComponent", entityId: entity.id, name, error: value.error });
|
|
1168
1592
|
components.push([descriptor, value.value]);
|
|
1169
1593
|
}
|
|
1594
|
+
// Transient components are never saved, so a requirement on one is not checked here.
|
|
1595
|
+
const missing = missingIn(components, DescriptorModule.isTransient);
|
|
1596
|
+
if (missing !== undefined) {
|
|
1597
|
+
return Result.failure({ _tag: "MissingRequiredComponent", entityId: entity.id, name: missing[1], requiredBy: missing[0] });
|
|
1598
|
+
}
|
|
1170
1599
|
entities.push({ id: entity.id, components });
|
|
1171
1600
|
}
|
|
1172
1601
|
const validatedResources = [];
|
|
@@ -1208,14 +1637,34 @@ const makeValidatedRuntime = (options) => {
|
|
|
1208
1637
|
}
|
|
1209
1638
|
}
|
|
1210
1639
|
}
|
|
1211
|
-
const
|
|
1640
|
+
const restoredMachines = new Map(currentMachines);
|
|
1212
1641
|
for (const [name, value] of Object.entries(saved.machines)) {
|
|
1213
1642
|
const machine = machinesByName.get(name);
|
|
1214
1643
|
if (!machine)
|
|
1215
1644
|
return Result.failure({ _tag: "UnknownMachine", name });
|
|
1216
1645
|
if (!machine.values.includes(value))
|
|
1217
1646
|
return Result.failure({ _tag: "InvalidMachineState", name, value });
|
|
1218
|
-
|
|
1647
|
+
restoredMachines.set(machine.key, value);
|
|
1648
|
+
}
|
|
1649
|
+
// A save has no value for a sub-state whose parent is elsewhere; one that
|
|
1650
|
+
// has a value must have its parent in the state it exists in. A sub-state
|
|
1651
|
+
// missing while its parent is there enters its entry value.
|
|
1652
|
+
for (const machine of options.machineDefinitions) {
|
|
1653
|
+
const within = machine.within;
|
|
1654
|
+
if (within === undefined)
|
|
1655
|
+
continue;
|
|
1656
|
+
const active = restoredMachines.get(within.machine.key) === within.state;
|
|
1657
|
+
if (Object.hasOwn(saved.machines, machine.name)) {
|
|
1658
|
+
if (!active) {
|
|
1659
|
+
return Result.failure({ _tag: "InactiveSubState", name: machine.name, parent: within.machine.name, requires: within.state });
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
else if (active && subStateEntry.has(machine.key)) {
|
|
1663
|
+
restoredMachines.set(machine.key, subStateEntry.get(machine.key));
|
|
1664
|
+
}
|
|
1665
|
+
else {
|
|
1666
|
+
restoredMachines.delete(machine.key);
|
|
1667
|
+
}
|
|
1219
1668
|
}
|
|
1220
1669
|
const scoped = new Set();
|
|
1221
1670
|
const memberships = [];
|
|
@@ -1247,9 +1696,12 @@ const makeValidatedRuntime = (options) => {
|
|
|
1247
1696
|
}
|
|
1248
1697
|
for (const [id, key] of memberships)
|
|
1249
1698
|
world.assignEntityScope(id, key);
|
|
1250
|
-
for (const [key, value] of validatedResources)
|
|
1699
|
+
for (const [key, value] of validatedResources) {
|
|
1251
1700
|
resources.set(key, value);
|
|
1252
|
-
|
|
1701
|
+
resourceChangedAt.set(key, world.currentTick());
|
|
1702
|
+
}
|
|
1703
|
+
currentMachines.clear();
|
|
1704
|
+
for (const [key, value] of restoredMachines)
|
|
1253
1705
|
currentMachines.set(key, value);
|
|
1254
1706
|
// Locals are not saved: each system starts over from its factory.
|
|
1255
1707
|
localSlots.forEach(({ slot }, index) => { slot.context.local = rebuiltLocals[index]; });
|
|
@@ -1389,12 +1841,21 @@ const makeValidatedRuntime = (options) => {
|
|
|
1389
1841
|
for (const [name, schedule] of Object.entries(named))
|
|
1390
1842
|
scheduleNames.set(schedule, name);
|
|
1391
1843
|
},
|
|
1844
|
+
declareHostEvents(declared) {
|
|
1845
|
+
hostEvents = new Set(declared.map((descriptor) => descriptor.name));
|
|
1846
|
+
},
|
|
1392
1847
|
describe: () => DebugInternal.describe({
|
|
1393
1848
|
schema: options.schema,
|
|
1394
1849
|
machines: [...new Set(machinesByName.values())],
|
|
1395
1850
|
currentMachine: (machine) => currentMachines.get(machine.key),
|
|
1851
|
+
providesMachine: (machine) => providedMachineKeys.has(machine.key),
|
|
1852
|
+
scopes: options.scopeDefinitions,
|
|
1396
1853
|
providedServices: Object.keys(providedServices),
|
|
1397
|
-
externallyWrittenEvents,
|
|
1854
|
+
externallyWrittenEvents: new Set([...externallyWrittenEvents, ...hostEvents]),
|
|
1855
|
+
relationMutations,
|
|
1856
|
+
commandUses,
|
|
1857
|
+
ranSystems,
|
|
1858
|
+
requirementViolations: [...requirementViolations.values()],
|
|
1398
1859
|
hasResource: (descriptor) => resources.has(descriptor.key),
|
|
1399
1860
|
schedules: [...scheduleNames].map(([schedule, name]) => [name, schedule])
|
|
1400
1861
|
}),
|
|
@@ -1411,6 +1872,13 @@ const makeValidatedRuntime = (options) => {
|
|
|
1411
1872
|
}
|
|
1412
1873
|
};
|
|
1413
1874
|
},
|
|
1875
|
+
observeHost(listener) {
|
|
1876
|
+
hostListeners.add(listener);
|
|
1877
|
+
return () => {
|
|
1878
|
+
hostListeners.delete(listener);
|
|
1879
|
+
};
|
|
1880
|
+
},
|
|
1881
|
+
checkIgnoredFailures: () => reportUnread("session-end"),
|
|
1414
1882
|
streams,
|
|
1415
1883
|
population: () => {
|
|
1416
1884
|
const components = {};
|
|
@@ -1458,7 +1926,8 @@ export const make = (options) => {
|
|
|
1458
1926
|
resources: validated.value,
|
|
1459
1927
|
machines: options.machines,
|
|
1460
1928
|
machineDefinitions: options.machineDefinitions ?? [],
|
|
1461
|
-
|
|
1929
|
+
scopeDefinitions: options.scopeDefinitions ?? [],
|
|
1930
|
+
debug: options.debug
|
|
1462
1931
|
});
|
|
1463
1932
|
return (fallible ? Result.success(runtime) : runtime);
|
|
1464
1933
|
};
|