@typeonce/ritmo 0.6.0 → 0.7.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 +157 -33
- package/dist/Command.d.ts.map +1 -1
- package/dist/Command.js +49 -15
- package/dist/Command.js.map +1 -1
- package/dist/Debug.d.ts +104 -12
- 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 +31 -21
- package/dist/Descriptor.d.ts.map +1 -1
- package/dist/Descriptor.js +21 -12
- package/dist/Descriptor.js.map +1 -1
- package/dist/Entity.d.ts +14 -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 +6 -0
- package/dist/EntityScope.d.ts.map +1 -1
- package/dist/EntityScope.js +6 -0
- 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 +12 -3
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +4 -1
- package/dist/Machine.js.map +1 -1
- package/dist/Query.d.ts +28 -24
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +0 -24
- package/dist/Query.js.map +1 -1
- package/dist/Runtime.d.ts +36 -16
- package/dist/Runtime.d.ts.map +1 -1
- package/dist/Runtime.js +230 -67
- 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 +61 -25
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +0 -8
- package/dist/Schema.js.map +1 -1
- package/dist/System.d.ts +149 -52
- package/dist/System.d.ts.map +1 -1
- package/dist/System.js +52 -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 +6 -0
- package/dist/internal/debug.d.ts.map +1 -1
- package/dist/internal/debug.js +95 -1
- package/dist/internal/debug.js.map +1 -1
- package/dist/internal/game.d.ts.map +1 -1
- package/dist/internal/game.js +9 -2
- 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 +120 -0
- package/dist/internal/order.js.map +1 -0
- package/dist/internal/queries.js +2 -2
- package/dist/internal/queries.js.map +1 -1
- package/package.json +1 -1
- package/src/Brand.ts +9 -4
- package/src/Clock.ts +311 -0
- package/src/Command.ts +271 -73
- package/src/Debug.ts +111 -10
- package/src/Decode.ts +1 -1
- package/src/Descriptor.ts +37 -27
- package/src/Entity.ts +15 -5
- package/src/EntityScope.ts +6 -0
- package/src/HostClock.ts +64 -0
- package/src/Inspector.ts +7 -2
- package/src/Machine.ts +13 -3
- package/src/Query.ts +41 -33
- package/src/Runtime.ts +273 -91
- package/src/Schedule.ts +380 -13
- package/src/Schema.ts +70 -41
- package/src/System.ts +174 -88
- package/src/index.ts +8 -7
- package/src/internal/cells.ts +10 -12
- package/src/internal/debug.ts +104 -4
- package/src/internal/game.ts +11 -3
- package/src/internal/order.ts +140 -0
- package/src/internal/queries.ts +3 -3
- 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,6 +55,7 @@
|
|
|
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";
|
|
@@ -66,6 +67,7 @@ import * as Query from "./Query.js";
|
|
|
66
67
|
import * as Relation from "./Relation.js";
|
|
67
68
|
import * as Result from "./Result.js";
|
|
68
69
|
import * as Schedule from "./Schedule.js";
|
|
70
|
+
import * as OrderInternal from "./internal/order.js";
|
|
69
71
|
import * as EntityScopeModule from "./EntityScope.js";
|
|
70
72
|
import * as Snapshot from "./Snapshot.js";
|
|
71
73
|
/**
|
|
@@ -194,12 +196,12 @@ const makeValidatedRuntime = (options) => {
|
|
|
194
196
|
*/
|
|
195
197
|
const pendingCommands = [];
|
|
196
198
|
/**
|
|
197
|
-
* Debug state. With `debug
|
|
199
|
+
* Debug state. With `debug`, `pendingOrigins` names the system that
|
|
198
200
|
* queued each pending command, in step with `pendingCommands`. Tracing is
|
|
199
201
|
* on only while `observe` listeners are registered; untraced runs pay one
|
|
200
202
|
* boolean check at each system, schedule, and marker boundary.
|
|
201
203
|
*/
|
|
202
|
-
const debugEnabled = options.debug;
|
|
204
|
+
const debugEnabled = options.debug !== undefined;
|
|
203
205
|
const pendingOrigins = [];
|
|
204
206
|
const traceListeners = new Set();
|
|
205
207
|
let tracing = false;
|
|
@@ -211,7 +213,9 @@ const makeValidatedRuntime = (options) => {
|
|
|
211
213
|
const scheduleNames = new Map();
|
|
212
214
|
const anonymousScheduleNames = new WeakMap();
|
|
213
215
|
let anonymousScheduleCount = 0;
|
|
214
|
-
|
|
216
|
+
// Only a debug runtime times anything; the clock is the one it was given.
|
|
217
|
+
const hostClock = options.debug?.hostClock ?? { now: () => 0 };
|
|
218
|
+
const now = () => hostClock.now();
|
|
215
219
|
const emit = (event) => {
|
|
216
220
|
for (const listener of traceListeners)
|
|
217
221
|
listener(event);
|
|
@@ -247,29 +251,81 @@ const makeValidatedRuntime = (options) => {
|
|
|
247
251
|
Relation.mutationFailure(relation, operation, world.entityIdOf(sourceId), world.entityIdOf(targetId), error)
|
|
248
252
|
]);
|
|
249
253
|
};
|
|
254
|
+
/**
|
|
255
|
+
* With `debug`, the relations commands have tried to relate or
|
|
256
|
+
* reorder, with the systems that queued those commands, for the
|
|
257
|
+
* `relation-failure-never-read` lint. `commandOrigin` names the system whose
|
|
258
|
+
* command is being applied.
|
|
259
|
+
*/
|
|
260
|
+
const relationMutations = new Map();
|
|
261
|
+
let commandOrigin = "(unknown)";
|
|
262
|
+
/**
|
|
263
|
+
* With `debug`, what each system's applied commands did, as
|
|
264
|
+
* `kind:name` (`spawns:Enemy`, `despawns:Enemy` for every component the
|
|
265
|
+
* despawned entity had), and the systems that ran: for the
|
|
266
|
+
* `command-permission-unused` lint.
|
|
267
|
+
*/
|
|
268
|
+
const commandUses = new Map();
|
|
269
|
+
const ranSystems = new Set();
|
|
270
|
+
const noteCommand = (kind, name) => {
|
|
271
|
+
let uses = commandUses.get(commandOrigin);
|
|
272
|
+
if (uses === undefined) {
|
|
273
|
+
uses = new Set();
|
|
274
|
+
commandUses.set(commandOrigin, uses);
|
|
275
|
+
}
|
|
276
|
+
uses.add(`${kind}:${name}`);
|
|
277
|
+
};
|
|
278
|
+
const noteRelationMutation = (relation) => {
|
|
279
|
+
let systems = relationMutations.get(relation.name);
|
|
280
|
+
if (systems === undefined) {
|
|
281
|
+
systems = new Set();
|
|
282
|
+
relationMutations.set(relation.name, systems);
|
|
283
|
+
}
|
|
284
|
+
systems.add(commandOrigin);
|
|
285
|
+
};
|
|
250
286
|
/**
|
|
251
287
|
* World adapter used by deferred commands.
|
|
252
288
|
*/
|
|
253
289
|
const commandWorld = {
|
|
254
290
|
spawnEntity(id, components) {
|
|
291
|
+
if (debugEnabled)
|
|
292
|
+
for (const [descriptor] of components)
|
|
293
|
+
noteCommand("spawns", descriptor.name);
|
|
255
294
|
world.spawnEntity(id, components);
|
|
256
295
|
},
|
|
257
296
|
destroyEntity(id) {
|
|
297
|
+
if (debugEnabled) {
|
|
298
|
+
const record = world.records.get(id);
|
|
299
|
+
record?.values.forEach((value, ordinal) => {
|
|
300
|
+
if (value !== ABSENT)
|
|
301
|
+
noteCommand("despawns", world.descriptorAt(ordinal).name);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
258
304
|
world.destroyEntity(id);
|
|
259
305
|
},
|
|
260
306
|
assignEntityScope(id, scope) {
|
|
261
307
|
world.assignEntityScope(id, scope.key);
|
|
262
308
|
},
|
|
263
309
|
destroyEntityScope(scope) {
|
|
310
|
+
if (debugEnabled)
|
|
311
|
+
noteCommand("despawnsScope", scope.name);
|
|
264
312
|
world.destroyEntityScope(scope.key);
|
|
265
313
|
},
|
|
266
314
|
removeComponent(id, descriptor) {
|
|
315
|
+
if (debugEnabled)
|
|
316
|
+
noteCommand("removes", descriptor.name);
|
|
267
317
|
world.removeComponent(id, descriptor);
|
|
268
318
|
},
|
|
269
319
|
writeComponent(id, descriptor, value) {
|
|
320
|
+
if (debugEnabled)
|
|
321
|
+
noteCommand("inserts", descriptor.name);
|
|
270
322
|
world.writeComponent(id, descriptor, value);
|
|
271
323
|
},
|
|
272
324
|
tryRelate(id, relation, target) {
|
|
325
|
+
if (debugEnabled) {
|
|
326
|
+
noteRelationMutation(relation);
|
|
327
|
+
noteCommand("relates", relation.name);
|
|
328
|
+
}
|
|
273
329
|
const result = world.tryRelate(id, relation, target);
|
|
274
330
|
if (!result.ok) {
|
|
275
331
|
appendRelationFailure(relation, "relate", id, target, result.error);
|
|
@@ -277,9 +333,15 @@ const makeValidatedRuntime = (options) => {
|
|
|
277
333
|
return result;
|
|
278
334
|
},
|
|
279
335
|
unrelate(id, relation) {
|
|
336
|
+
if (debugEnabled)
|
|
337
|
+
noteCommand("relates", relation.name);
|
|
280
338
|
world.unrelate(id, relation);
|
|
281
339
|
},
|
|
282
340
|
reorderChildren(id, relation, children) {
|
|
341
|
+
if (debugEnabled) {
|
|
342
|
+
noteRelationMutation(relation);
|
|
343
|
+
noteCommand("relates", relation.name);
|
|
344
|
+
}
|
|
283
345
|
const result = world.reorderChildren(id, relation, children.map((child) => child));
|
|
284
346
|
if (!result.ok) {
|
|
285
347
|
const targetId = result.error._tag === "MissingChildEntity"
|
|
@@ -306,12 +368,6 @@ const makeValidatedRuntime = (options) => {
|
|
|
306
368
|
};
|
|
307
369
|
const makeLookup = (reader) => {
|
|
308
370
|
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
371
|
related: relatedTarget,
|
|
316
372
|
relatedSources(target, relation) {
|
|
317
373
|
if (!world.records.has(target)) {
|
|
@@ -319,19 +375,6 @@ const makeValidatedRuntime = (options) => {
|
|
|
319
375
|
}
|
|
320
376
|
return Result.success(world.relatedSourceIds(relation, target).map(entityId));
|
|
321
377
|
},
|
|
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
378
|
parent: relatedTarget,
|
|
336
379
|
ancestors(target, relation) {
|
|
337
380
|
if (!world.records.has(target)) {
|
|
@@ -371,20 +414,6 @@ const makeValidatedRuntime = (options) => {
|
|
|
371
414
|
}
|
|
372
415
|
return Result.success(descendants);
|
|
373
416
|
},
|
|
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
417
|
root(target, relation) {
|
|
389
418
|
if (!world.records.has(target)) {
|
|
390
419
|
return Result.failure(Relation.missingEntityError(target));
|
|
@@ -400,13 +429,34 @@ const makeValidatedRuntime = (options) => {
|
|
|
400
429
|
};
|
|
401
430
|
return lookup;
|
|
402
431
|
};
|
|
403
|
-
const makeQueryHandle = (query, reader) => {
|
|
432
|
+
const makeQueryHandle = (query, reader, lookup) => {
|
|
404
433
|
const each = () => queries.each(query, reader.since);
|
|
434
|
+
const matching = (ids) => {
|
|
435
|
+
const matches = [];
|
|
436
|
+
for (const id of ids) {
|
|
437
|
+
const resolved = resolve(id, query, reader);
|
|
438
|
+
if (resolved.ok)
|
|
439
|
+
matches.push(resolved.value);
|
|
440
|
+
}
|
|
441
|
+
return matches;
|
|
442
|
+
};
|
|
405
443
|
return {
|
|
406
444
|
each,
|
|
407
445
|
get(target) {
|
|
408
446
|
return resolve(target, query, reader);
|
|
409
447
|
},
|
|
448
|
+
getHandle(handle) {
|
|
449
|
+
return resolve(handle, query, reader);
|
|
450
|
+
},
|
|
451
|
+
children(target, relation) {
|
|
452
|
+
if (!world.records.has(target))
|
|
453
|
+
return Result.failure(Relation.missingEntityError(target));
|
|
454
|
+
return Result.success(matching(world.relatedSourceIds(relation, target).map(entityId)));
|
|
455
|
+
},
|
|
456
|
+
descendants(target, relation, options) {
|
|
457
|
+
const ids = lookup.descendants(target, relation, options);
|
|
458
|
+
return ids.ok ? Result.success(matching(ids.value)) : ids;
|
|
459
|
+
},
|
|
410
460
|
single() {
|
|
411
461
|
const matches = each();
|
|
412
462
|
if (matches.length === 0) {
|
|
@@ -522,12 +572,18 @@ const makeValidatedRuntime = (options) => {
|
|
|
522
572
|
return {
|
|
523
573
|
all() {
|
|
524
574
|
return world.removedSince(ordinal, reader.since).map(entityId);
|
|
575
|
+
},
|
|
576
|
+
lagged() {
|
|
577
|
+
return world.removedLagged(ordinal, reader.since, reader.registeredAt);
|
|
525
578
|
}
|
|
526
579
|
};
|
|
527
580
|
};
|
|
528
581
|
const makeDespawnedReadView = (reader) => ({
|
|
529
582
|
all() {
|
|
530
583
|
return world.despawnedSince(reader.since).map(entityId);
|
|
584
|
+
},
|
|
585
|
+
lagged() {
|
|
586
|
+
return world.despawnedLagged(reader.since, reader.registeredAt);
|
|
531
587
|
}
|
|
532
588
|
});
|
|
533
589
|
const makeRelationFailureReadView = (relation, reader) => ({
|
|
@@ -595,12 +651,13 @@ const makeValidatedRuntime = (options) => {
|
|
|
595
651
|
* views the implementation is allowed to see. Views read live storage, so
|
|
596
652
|
* one context is built per system and reused for every run.
|
|
597
653
|
*/
|
|
598
|
-
const makeContext = (system, reader) => {
|
|
654
|
+
const makeContext = (system, reader, queue) => {
|
|
599
655
|
const spec = system.spec;
|
|
656
|
+
const lookup = makeLookup(reader);
|
|
600
657
|
return {
|
|
601
658
|
local: spec.local?.(),
|
|
602
|
-
queries: mapRecord(spec.queries, (query) => makeQueryHandle(query, reader)),
|
|
603
|
-
lookup
|
|
659
|
+
queries: mapRecord(spec.queries, (query) => makeQueryHandle(query, reader, lookup)),
|
|
660
|
+
lookup,
|
|
604
661
|
resources: mapRecord(spec.resources, (access) => access.mode === "read"
|
|
605
662
|
? Cells.storeRead(resources, access.descriptor.key)
|
|
606
663
|
: makeResourceWriteView(access.descriptor, resources, writeResource)),
|
|
@@ -615,7 +672,8 @@ const makeValidatedRuntime = (options) => {
|
|
|
615
672
|
despawned: mapRecord(spec.despawned, () => makeDespawnedReadView(reader)),
|
|
616
673
|
relationFailures: mapRecord(spec.relationFailures, (access) => makeRelationFailureReadView(access.relation, reader)),
|
|
617
674
|
services: mapRecord(spec.services, (access) => providedServices[access.descriptor.name]),
|
|
618
|
-
|
|
675
|
+
// The queue serves every declared permission set; the spec's types narrow it.
|
|
676
|
+
commands: queue
|
|
619
677
|
};
|
|
620
678
|
};
|
|
621
679
|
const slots = new WeakMap();
|
|
@@ -686,7 +744,7 @@ const makeValidatedRuntime = (options) => {
|
|
|
686
744
|
system: system.name,
|
|
687
745
|
outcome,
|
|
688
746
|
...(outcome === "ok" ? {} : { error }),
|
|
689
|
-
ms: now()
|
|
747
|
+
ms: HostClockModule.elapsed(started, now()),
|
|
690
748
|
writes,
|
|
691
749
|
resources: resourceWrites,
|
|
692
750
|
events: emitted,
|
|
@@ -748,7 +806,8 @@ const makeValidatedRuntime = (options) => {
|
|
|
748
806
|
let slot = slots.get(stateKey(system));
|
|
749
807
|
if (!slot) {
|
|
750
808
|
const reader = { since: 0, lastRun: 0, streamSince: 0, streamLastRun: 0, registeredAt: world.currentTick() };
|
|
751
|
-
|
|
809
|
+
const commands = Command.makeCommands(() => world.allocateEntity());
|
|
810
|
+
slot = { context: makeContext(system, reader, commands), commands, reader };
|
|
752
811
|
slots.set(stateKey(system), slot);
|
|
753
812
|
if (holdsStreams)
|
|
754
813
|
registerStreamReader(system, slot.reader);
|
|
@@ -789,7 +848,9 @@ const makeValidatedRuntime = (options) => {
|
|
|
789
848
|
return succeeded;
|
|
790
849
|
}
|
|
791
850
|
}
|
|
792
|
-
const { context, reader } = slotOf(system, true);
|
|
851
|
+
const { context, commands, reader } = slotOf(system, true);
|
|
852
|
+
if (debugEnabled)
|
|
853
|
+
ranSystems.add(system.name);
|
|
793
854
|
// Changes stamped after the previous completed run are visible to this run.
|
|
794
855
|
reader.since = reader.lastRun;
|
|
795
856
|
reader.streamSince = reader.streamLastRun;
|
|
@@ -798,26 +859,24 @@ const makeValidatedRuntime = (options) => {
|
|
|
798
859
|
beginSystemTransaction();
|
|
799
860
|
let outcome;
|
|
800
861
|
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);
|
|
862
|
+
// A system that returns nothing cannot fail; otherwise it returned a `Result`.
|
|
863
|
+
outcome = system.run(context) ?? succeeded;
|
|
805
864
|
}
|
|
806
865
|
catch (defect) {
|
|
807
866
|
if (tracing)
|
|
808
|
-
emit(traceSystem(system, reader, thisRun, started, "defect", defect,
|
|
867
|
+
emit(traceSystem(system, reader, thisRun, started, "defect", defect, commands.flush()));
|
|
809
868
|
rollbackSystemTransaction();
|
|
810
|
-
|
|
869
|
+
commands.flush();
|
|
811
870
|
throw defect;
|
|
812
871
|
}
|
|
813
872
|
if (!outcome.ok) {
|
|
814
873
|
if (tracing)
|
|
815
|
-
emit(traceSystem(system, reader, thisRun, started, "failed", outcome.error,
|
|
874
|
+
emit(traceSystem(system, reader, thisRun, started, "failed", outcome.error, commands.flush()));
|
|
816
875
|
rollbackSystemTransaction();
|
|
817
|
-
|
|
876
|
+
commands.flush();
|
|
818
877
|
return Result.failure({ kind: "SystemFailure", system: system.name, error: outcome.error });
|
|
819
878
|
}
|
|
820
|
-
const queued =
|
|
879
|
+
const queued = commands.flush();
|
|
821
880
|
const traced = tracing ? traceSystem(system, reader, thisRun, started, "ok", undefined, queued) : undefined;
|
|
822
881
|
commitSystemTransaction();
|
|
823
882
|
// A failed run leaves `lastRun` unchanged, so the next run sees the same changes again.
|
|
@@ -840,8 +899,15 @@ const makeValidatedRuntime = (options) => {
|
|
|
840
899
|
const origins = pendingOrigins.splice(0, pendingOrigins.length);
|
|
841
900
|
world.advanceTick();
|
|
842
901
|
if (!tracing) {
|
|
843
|
-
|
|
844
|
-
command
|
|
902
|
+
if (!debugEnabled) {
|
|
903
|
+
for (const command of commands) {
|
|
904
|
+
command.apply(commandWorld);
|
|
905
|
+
}
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
for (let index = 0; index < commands.length; index++) {
|
|
909
|
+
commandOrigin = origins[index] ?? "(unknown)";
|
|
910
|
+
commands[index].apply(commandWorld);
|
|
845
911
|
}
|
|
846
912
|
return;
|
|
847
913
|
}
|
|
@@ -850,6 +916,7 @@ const makeValidatedRuntime = (options) => {
|
|
|
850
916
|
const command = commands[index];
|
|
851
917
|
const effects = [];
|
|
852
918
|
commandEffects = effects;
|
|
919
|
+
commandOrigin = origins[index] ?? "(unknown)";
|
|
853
920
|
try {
|
|
854
921
|
command.apply(commandWorld);
|
|
855
922
|
}
|
|
@@ -875,7 +942,7 @@ const makeValidatedRuntime = (options) => {
|
|
|
875
942
|
finally {
|
|
876
943
|
schedulePath.pop();
|
|
877
944
|
}
|
|
878
|
-
emit({ type: "schedule.end", frame: frameCount, schedule: scheduleLabel, ok: result.ok, ms: now()
|
|
945
|
+
emit({ type: "schedule.end", frame: frameCount, schedule: scheduleLabel, ok: result.ok, ms: HostClockModule.elapsed(started, now()) });
|
|
879
946
|
return result;
|
|
880
947
|
};
|
|
881
948
|
const traceTransition = (machineKey, snapshot, outcome) => {
|
|
@@ -1023,6 +1090,14 @@ const makeValidatedRuntime = (options) => {
|
|
|
1023
1090
|
};
|
|
1024
1091
|
const eventsByKey = new Set(Object.values(options.schema.events).map((descriptor) => descriptor.key));
|
|
1025
1092
|
const externallyWrittenEvents = new Set();
|
|
1093
|
+
/** `debug.observeHost` listeners: ticks and sends, without tracing. Only a debug runtime registers any. */
|
|
1094
|
+
const hostListeners = new Set();
|
|
1095
|
+
const emitHost = (event) => {
|
|
1096
|
+
for (const listener of hostListeners)
|
|
1097
|
+
listener(event);
|
|
1098
|
+
};
|
|
1099
|
+
/** Events declared as sent by the host (`debug.declareHostEvents`). */
|
|
1100
|
+
let hostEvents = new Set();
|
|
1026
1101
|
const send = (descriptor, value) => {
|
|
1027
1102
|
if (ticking)
|
|
1028
1103
|
throw new Error(`runtime.send(${descriptor.name}) was called while a schedule is running: emit from a system instead`);
|
|
@@ -1033,6 +1108,8 @@ const makeValidatedRuntime = (options) => {
|
|
|
1033
1108
|
// Bound host traffic even when it sends repeatedly without ticking.
|
|
1034
1109
|
events.trim(0);
|
|
1035
1110
|
externallyWrittenEvents.add(descriptor.name);
|
|
1111
|
+
if (hostListeners.size > 0)
|
|
1112
|
+
emitHost({ type: "send", event: descriptor.name, value });
|
|
1036
1113
|
if (tracing)
|
|
1037
1114
|
emit({ type: "runtime.sent", frame: frameCount, tick: published, event: descriptor.name, value });
|
|
1038
1115
|
};
|
|
@@ -1045,23 +1122,90 @@ const makeValidatedRuntime = (options) => {
|
|
|
1045
1122
|
transitionEvents.trim(boundary);
|
|
1046
1123
|
relationFailures.trim(boundary);
|
|
1047
1124
|
};
|
|
1125
|
+
/**
|
|
1126
|
+
* With debug: the last failed tick result, until its `ok` is read. The
|
|
1127
|
+
* result handed back is a copy whose `ok` getter marks it read.
|
|
1128
|
+
*/
|
|
1129
|
+
let unread;
|
|
1130
|
+
const reportUnread = (when) => {
|
|
1131
|
+
const pending = unread;
|
|
1132
|
+
unread = undefined;
|
|
1133
|
+
if (pending === undefined || pending.read)
|
|
1134
|
+
return;
|
|
1135
|
+
const event = { type: "failure.ignored", ...pending.event, when };
|
|
1136
|
+
emit(event);
|
|
1137
|
+
emitHost(event);
|
|
1138
|
+
};
|
|
1139
|
+
const watchResult = (result, schedules) => {
|
|
1140
|
+
if (!debugEnabled || result.ok)
|
|
1141
|
+
return result;
|
|
1142
|
+
const error = result.error;
|
|
1143
|
+
const system = typeof error === "object" && error !== null && error.kind === "SystemFailure"
|
|
1144
|
+
? error.system
|
|
1145
|
+
: undefined;
|
|
1146
|
+
const names = schedules.flatMap((schedule) => schedule.steps.filter(Schedule.isSystemStep).map((step) => step.name));
|
|
1147
|
+
const at = system === undefined ? -1 : names.indexOf(system);
|
|
1148
|
+
const holder = system === undefined
|
|
1149
|
+
? schedules[0]
|
|
1150
|
+
: schedules.find((schedule) => schedule.steps.some((step) => Schedule.isSystemStep(step) && step.name === system)) ?? schedules[0];
|
|
1151
|
+
const entry = {
|
|
1152
|
+
read: false,
|
|
1153
|
+
event: {
|
|
1154
|
+
frame: frameCount,
|
|
1155
|
+
schedule: holder === undefined ? "(none)" : scheduleName(holder),
|
|
1156
|
+
system,
|
|
1157
|
+
error,
|
|
1158
|
+
skipped: [...new Set(names.slice(at + 1))]
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
unread = entry;
|
|
1162
|
+
return {
|
|
1163
|
+
get ok() {
|
|
1164
|
+
entry.read = true;
|
|
1165
|
+
return false;
|
|
1166
|
+
},
|
|
1167
|
+
error
|
|
1168
|
+
};
|
|
1169
|
+
};
|
|
1048
1170
|
const tickUnsafe = (schedules) => {
|
|
1049
1171
|
if (ticking)
|
|
1050
1172
|
throw new Error("runtime.tick cannot run inside another tick");
|
|
1173
|
+
if (debugEnabled)
|
|
1174
|
+
reportUnread("next-tick");
|
|
1051
1175
|
ticking = true;
|
|
1052
1176
|
try {
|
|
1053
1177
|
advanceFrame();
|
|
1054
|
-
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1178
|
+
return watchResult(observedTick(schedules, () => {
|
|
1179
|
+
for (const schedule of schedules) {
|
|
1180
|
+
const result = runScheduleUnsafe(schedule);
|
|
1181
|
+
if (!result.ok)
|
|
1182
|
+
return result;
|
|
1183
|
+
}
|
|
1184
|
+
return succeeded;
|
|
1185
|
+
}), schedules);
|
|
1060
1186
|
}
|
|
1061
1187
|
finally {
|
|
1062
1188
|
ticking = false;
|
|
1063
1189
|
}
|
|
1064
1190
|
};
|
|
1191
|
+
/** Runs one tick's schedules, telling `observeHost` listeners when it starts and how it ends. */
|
|
1192
|
+
const observedTick = (schedules, run) => {
|
|
1193
|
+
if (hostListeners.size === 0)
|
|
1194
|
+
return run();
|
|
1195
|
+
emitHost({ type: "tick.start", schedules, frame: frameCount });
|
|
1196
|
+
let result;
|
|
1197
|
+
try {
|
|
1198
|
+
result = run();
|
|
1199
|
+
}
|
|
1200
|
+
catch (defect) {
|
|
1201
|
+
emitHost({ type: "tick.end", schedules, frame: frameCount, outcome: "defect", error: defect });
|
|
1202
|
+
throw defect;
|
|
1203
|
+
}
|
|
1204
|
+
emitHost(result.ok
|
|
1205
|
+
? { type: "tick.end", schedules, frame: frameCount, outcome: "ok" }
|
|
1206
|
+
: { type: "tick.end", schedules, frame: frameCount, outcome: "failed", error: result.error });
|
|
1207
|
+
return result;
|
|
1208
|
+
};
|
|
1065
1209
|
const hasRequirement = (requirement) => {
|
|
1066
1210
|
switch (requirement.kind) {
|
|
1067
1211
|
case "service":
|
|
@@ -1073,17 +1217,23 @@ const makeValidatedRuntime = (options) => {
|
|
|
1073
1217
|
}
|
|
1074
1218
|
};
|
|
1075
1219
|
const tryRunSchedule = (schedule) => {
|
|
1220
|
+
if (debugEnabled)
|
|
1221
|
+
reportUnread("next-tick");
|
|
1076
1222
|
const missing = schedule.requirements
|
|
1077
1223
|
.filter((requirement) => !hasRequirement(requirement))
|
|
1078
1224
|
.map(({ kind, name }) => ({ kind, name }));
|
|
1079
1225
|
if (missing.length > 0) {
|
|
1080
|
-
return Result.failure({
|
|
1226
|
+
return watchResult(Result.failure({
|
|
1081
1227
|
kind: "MissingRuntimeRequirements",
|
|
1082
1228
|
requirements: missing
|
|
1083
|
-
});
|
|
1229
|
+
}), [schedule]);
|
|
1230
|
+
}
|
|
1231
|
+
const issues = OrderInternal.tickOrderIssues(schedule.steps);
|
|
1232
|
+
if (issues.length > 0) {
|
|
1233
|
+
return watchResult(Result.failure({ kind: "TickOrderViolation", issues }), [schedule]);
|
|
1084
1234
|
}
|
|
1085
1235
|
advanceFrame();
|
|
1086
|
-
return runScheduleUnsafe(schedule);
|
|
1236
|
+
return watchResult(observedTick([schedule], () => runScheduleUnsafe(schedule)), [schedule]);
|
|
1087
1237
|
};
|
|
1088
1238
|
/**
|
|
1089
1239
|
* Evaluates a declared read-only projection without running a schedule.
|
|
@@ -1389,12 +1539,18 @@ const makeValidatedRuntime = (options) => {
|
|
|
1389
1539
|
for (const [name, schedule] of Object.entries(named))
|
|
1390
1540
|
scheduleNames.set(schedule, name);
|
|
1391
1541
|
},
|
|
1542
|
+
declareHostEvents(declared) {
|
|
1543
|
+
hostEvents = new Set(declared.map((descriptor) => descriptor.name));
|
|
1544
|
+
},
|
|
1392
1545
|
describe: () => DebugInternal.describe({
|
|
1393
1546
|
schema: options.schema,
|
|
1394
1547
|
machines: [...new Set(machinesByName.values())],
|
|
1395
1548
|
currentMachine: (machine) => currentMachines.get(machine.key),
|
|
1396
1549
|
providedServices: Object.keys(providedServices),
|
|
1397
|
-
externallyWrittenEvents,
|
|
1550
|
+
externallyWrittenEvents: new Set([...externallyWrittenEvents, ...hostEvents]),
|
|
1551
|
+
relationMutations,
|
|
1552
|
+
commandUses,
|
|
1553
|
+
ranSystems,
|
|
1398
1554
|
hasResource: (descriptor) => resources.has(descriptor.key),
|
|
1399
1555
|
schedules: [...scheduleNames].map(([schedule, name]) => [name, schedule])
|
|
1400
1556
|
}),
|
|
@@ -1411,6 +1567,13 @@ const makeValidatedRuntime = (options) => {
|
|
|
1411
1567
|
}
|
|
1412
1568
|
};
|
|
1413
1569
|
},
|
|
1570
|
+
observeHost(listener) {
|
|
1571
|
+
hostListeners.add(listener);
|
|
1572
|
+
return () => {
|
|
1573
|
+
hostListeners.delete(listener);
|
|
1574
|
+
};
|
|
1575
|
+
},
|
|
1576
|
+
checkIgnoredFailures: () => reportUnread("session-end"),
|
|
1414
1577
|
streams,
|
|
1415
1578
|
population: () => {
|
|
1416
1579
|
const components = {};
|
|
@@ -1458,7 +1621,7 @@ export const make = (options) => {
|
|
|
1458
1621
|
resources: validated.value,
|
|
1459
1622
|
machines: options.machines,
|
|
1460
1623
|
machineDefinitions: options.machineDefinitions ?? [],
|
|
1461
|
-
debug: options.debug
|
|
1624
|
+
debug: options.debug
|
|
1462
1625
|
});
|
|
1463
1626
|
return (fallible ? Result.success(runtime) : runtime);
|
|
1464
1627
|
};
|