@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.
Files changed (125) hide show
  1. package/README.md +2 -0
  2. package/dist/Brand.d.ts +9 -4
  3. package/dist/Brand.d.ts.map +1 -1
  4. package/dist/Brand.js +9 -4
  5. package/dist/Brand.js.map +1 -1
  6. package/dist/Clock.d.ts +186 -0
  7. package/dist/Clock.d.ts.map +1 -0
  8. package/dist/Clock.js +111 -0
  9. package/dist/Clock.js.map +1 -0
  10. package/dist/Command.d.ts +157 -33
  11. package/dist/Command.d.ts.map +1 -1
  12. package/dist/Command.js +49 -15
  13. package/dist/Command.js.map +1 -1
  14. package/dist/Debug.d.ts +104 -12
  15. package/dist/Debug.d.ts.map +1 -1
  16. package/dist/Debug.js.map +1 -1
  17. package/dist/Decode.d.ts +1 -1
  18. package/dist/Decode.js +1 -1
  19. package/dist/Descriptor.d.ts +31 -21
  20. package/dist/Descriptor.d.ts.map +1 -1
  21. package/dist/Descriptor.js +21 -12
  22. package/dist/Descriptor.js.map +1 -1
  23. package/dist/Entity.d.ts +14 -5
  24. package/dist/Entity.d.ts.map +1 -1
  25. package/dist/Entity.js +2 -2
  26. package/dist/Entity.js.map +1 -1
  27. package/dist/EntityScope.d.ts +6 -0
  28. package/dist/EntityScope.d.ts.map +1 -1
  29. package/dist/EntityScope.js +6 -0
  30. package/dist/EntityScope.js.map +1 -1
  31. package/dist/HostClock.d.ts +49 -0
  32. package/dist/HostClock.d.ts.map +1 -0
  33. package/dist/HostClock.js +46 -0
  34. package/dist/HostClock.js.map +1 -0
  35. package/dist/Inspector.d.ts +7 -1
  36. package/dist/Inspector.d.ts.map +1 -1
  37. package/dist/Inspector.js.map +1 -1
  38. package/dist/Machine.d.ts +12 -3
  39. package/dist/Machine.d.ts.map +1 -1
  40. package/dist/Machine.js +4 -1
  41. package/dist/Machine.js.map +1 -1
  42. package/dist/Query.d.ts +28 -24
  43. package/dist/Query.d.ts.map +1 -1
  44. package/dist/Query.js +0 -24
  45. package/dist/Query.js.map +1 -1
  46. package/dist/Runtime.d.ts +36 -16
  47. package/dist/Runtime.d.ts.map +1 -1
  48. package/dist/Runtime.js +230 -67
  49. package/dist/Runtime.js.map +1 -1
  50. package/dist/Schedule.d.ts +217 -10
  51. package/dist/Schedule.d.ts.map +1 -1
  52. package/dist/Schedule.js +5 -0
  53. package/dist/Schedule.js.map +1 -1
  54. package/dist/Schema.d.ts +61 -25
  55. package/dist/Schema.d.ts.map +1 -1
  56. package/dist/Schema.js +0 -8
  57. package/dist/Schema.js.map +1 -1
  58. package/dist/System.d.ts +149 -52
  59. package/dist/System.d.ts.map +1 -1
  60. package/dist/System.js +52 -6
  61. package/dist/System.js.map +1 -1
  62. package/dist/index.d.ts +8 -7
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +8 -7
  65. package/dist/index.js.map +1 -1
  66. package/dist/internal/cells.d.ts +3 -3
  67. package/dist/internal/cells.d.ts.map +1 -1
  68. package/dist/internal/cells.js +9 -11
  69. package/dist/internal/cells.js.map +1 -1
  70. package/dist/internal/debug.d.ts +6 -0
  71. package/dist/internal/debug.d.ts.map +1 -1
  72. package/dist/internal/debug.js +95 -1
  73. package/dist/internal/debug.js.map +1 -1
  74. package/dist/internal/game.d.ts.map +1 -1
  75. package/dist/internal/game.js +9 -2
  76. package/dist/internal/game.js.map +1 -1
  77. package/dist/internal/order.d.ts +10 -0
  78. package/dist/internal/order.d.ts.map +1 -0
  79. package/dist/internal/order.js +120 -0
  80. package/dist/internal/order.js.map +1 -0
  81. package/dist/internal/queries.js +2 -2
  82. package/dist/internal/queries.js.map +1 -1
  83. package/package.json +1 -1
  84. package/src/Brand.ts +9 -4
  85. package/src/Clock.ts +311 -0
  86. package/src/Command.ts +271 -73
  87. package/src/Debug.ts +111 -10
  88. package/src/Decode.ts +1 -1
  89. package/src/Descriptor.ts +37 -27
  90. package/src/Entity.ts +15 -5
  91. package/src/EntityScope.ts +6 -0
  92. package/src/HostClock.ts +64 -0
  93. package/src/Inspector.ts +7 -2
  94. package/src/Machine.ts +13 -3
  95. package/src/Query.ts +41 -33
  96. package/src/Runtime.ts +273 -91
  97. package/src/Schedule.ts +380 -13
  98. package/src/Schema.ts +70 -41
  99. package/src/System.ts +174 -88
  100. package/src/index.ts +8 -7
  101. package/src/internal/cells.ts +10 -12
  102. package/src/internal/debug.ts +104 -4
  103. package/src/internal/game.ts +11 -3
  104. package/src/internal/order.ts +140 -0
  105. package/src/internal/queries.ts +3 -3
  106. package/dist/Definition.d.ts +0 -99
  107. package/dist/Definition.d.ts.map +0 -1
  108. package/dist/Definition.js +0 -68
  109. package/dist/Definition.js.map +0 -1
  110. package/dist/Feature.d.ts +0 -106
  111. package/dist/Feature.d.ts.map +0 -1
  112. package/dist/Feature.js +0 -87
  113. package/dist/Feature.js.map +0 -1
  114. package/dist/Fx.d.ts +0 -128
  115. package/dist/Fx.d.ts.map +0 -1
  116. package/dist/Fx.js +0 -112
  117. package/dist/Fx.js.map +0 -1
  118. package/dist/internal/definition.d.ts +0 -17
  119. package/dist/internal/definition.d.ts.map +0 -1
  120. package/dist/internal/definition.js +0 -25
  121. package/dist/internal/definition.js.map +0 -1
  122. package/src/Definition.ts +0 -106
  123. package/src/Feature.ts +0 -241
  124. package/src/Fx.ts +0 -176
  125. 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: true`, `pendingOrigins` names the system that
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
- const now = () => performance.now();
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: makeLookup(reader),
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
- commands: Command.makeCommands(() => world.allocateEntity())
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() - started,
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
- slot = { context: makeContext(system, reader), reader };
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. Otherwise run its effect,
802
- // equivalent to `Fx.runSync(Fx.provide(effect, services))` without the wrapper allocations.
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, context.commands.flush()));
867
+ emit(traceSystem(system, reader, thisRun, started, "defect", defect, commands.flush()));
809
868
  rollbackSystemTransaction();
810
- context.commands.flush();
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, context.commands.flush()));
874
+ emit(traceSystem(system, reader, thisRun, started, "failed", outcome.error, commands.flush()));
816
875
  rollbackSystemTransaction();
817
- context.commands.flush();
876
+ commands.flush();
818
877
  return Result.failure({ kind: "SystemFailure", system: system.name, error: outcome.error });
819
878
  }
820
- const queued = context.commands.flush();
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
- for (const command of commands) {
844
- command.apply(commandWorld);
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() - started });
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
- for (const schedule of schedules) {
1055
- const result = runScheduleUnsafe(schedule);
1056
- if (!result.ok)
1057
- return result;
1058
- }
1059
- return succeeded;
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 === true
1624
+ debug: options.debug
1462
1625
  });
1463
1626
  return (fallible ? Result.success(runtime) : runtime);
1464
1627
  };