diff --git a/src/app/app.js b/src/app/app.js index 6c678f7e..06e627ed 100644 --- a/src/app/app.js +++ b/src/app/app.js @@ -1,9 +1,8 @@ -/** @import { SystemFunc } from '../ecs/index.js' */ /** @import { SystemConfig, SystemGroupConfig } from '../schedule/index.js' */ /** @import { Constructor,TypeId } from '../type/index.js'*/ import { World, ComponentHooks } from '../ecs/index.js' -import { Scheduler, SchedulerBuilder, Executable } from '../schedule/index.js' +import { Scheduler, SchedulerBuilder } from '../schedule/index.js' import { assert } from '../logger/index.js' import { typeid } from '../type/index.js' @@ -84,12 +83,6 @@ export class App { */ initialized = false - /** - * @private - * @type {SchedulerBuilder} - */ - systemBuilder = new SchedulerBuilder() - /** * Return the world of the app. * @@ -103,7 +96,7 @@ export class App { * @param {{label: import('../type/index.js').Constructor, delay?: number, repeat?: boolean, errorHandler?: (error: Error, world: World) => void, defaultSystemGroup?: import('../type/index.js').Constructor}} config */ createSchedule(config) { - this.scheduler.set(new Executable(config)) + SchedulerBuilder.Instance.addSchedule(config) return this } @@ -127,7 +120,7 @@ export class App { run() { this.plugins.register(this) - this.systemBuilder.pushToScheduler(this.scheduler) + SchedulerBuilder.Instance.pushToScheduler(this.scheduler) assert(this.runner, 'App runner is not set. Call `app.setRunner(...)` before `app.run()`.') this.runner(this.scheduler, this.world) this.initialized = true @@ -155,7 +148,7 @@ export class App { * @param {SystemConfig} config */ registerSystem(config) { - this.systemBuilder.add(config) + SchedulerBuilder.Instance.add(config) return this } @@ -164,7 +157,7 @@ export class App { * @param {SystemGroupConfig} config */ registerSystemGroup(config) { - this.systemBuilder.addGroup(config) + SchedulerBuilder.Instance.addGroup(config) return this } diff --git a/src/core/plugin.js b/src/core/plugin.js index 59c86c60..7c8d43e6 100644 --- a/src/core/plugin.js +++ b/src/core/plugin.js @@ -1,5 +1,6 @@ import { App, Plugin } from '../app/index.js' import { AppSchedule, CoreSystems, defaultRunner } from './core/index.js' +import { SchedulerBuilder } from '../schedule/index.js' import { registerCoreTypes } from './systems/index.js' export class CorePlugin extends Plugin { @@ -9,6 +10,7 @@ export class CorePlugin extends Plugin { */ register(app) { app + .setResource(SchedulerBuilder.Instance) .setRunner(defaultRunner) .createSchedule({ label: AppSchedule.Startup, diff --git a/src/schedule/core/index.js b/src/schedule/core/index.js index f36432e8..c10e4112 100644 --- a/src/schedule/core/index.js +++ b/src/schedule/core/index.js @@ -1,6 +1,6 @@ export * from './schedule.js' export * from './scheduler.js' export * from './executable.js' -export * from './systembuilder.js' +export * from './schedulerbuilder.js' export * from './systemconfig.js' export * from './runner.js' diff --git a/src/schedule/core/systembuilder.js b/src/schedule/core/schedulerbuilder.js similarity index 50% rename from src/schedule/core/systembuilder.js rename to src/schedule/core/schedulerbuilder.js index b2210b2c..9df35302 100644 --- a/src/schedule/core/systembuilder.js +++ b/src/schedule/core/schedulerbuilder.js @@ -4,226 +4,172 @@ import { Graph, kahnTopologySort } from 'vifaa' import { assert, throws } from '../../logger/index.js' import { typeid } from '../../type/index.js' +import { Executable } from './executable.js' -export class SchedulerBuilder { +export class ScheduleContext { /** - * @private - * @type {SystemConfig[]} + * @param {Constructor} label */ - systems = [] + constructor(label) { + this.label = label + } /** - * @private - * @type {SystemGroupConfig[]} + * @type {Constructor} */ - systemGroups = [] + label /** - * @param {SystemConfig} config + * @type {SystemRegistration[]} */ - add(config) { - this.systems.push(config) - } + systems = [] /** - * @param {SystemGroupConfig} config + * @type {SystemGroupRegistration[]} */ - addGroup(config) { - this.systemGroups.push(config) - } + groups = [] /** - * @param {Scheduler} scheduler + * @type {Map} */ - pushToScheduler(scheduler) { - - /** @type {Map} */ - const defaultGroupsBySchedule = new Map() - - for (const executable of scheduler.values()) { - defaultGroupsBySchedule.set(typeid(executable.label), executable.defaultSystemGroup) - } - - const schedules = this.createScheduleContexts(defaultGroupsBySchedule) - - for (const [, context] of schedules) { - const schedule = scheduler.get(context.label) - - assert(schedule, `The schedule label "${context.label.name}" is not set in the provided \`Scheduler\`.`) - - for (const system of this.sortScheduleSystems(context)) { - schedule.add(system.config.system) - } - } - } + nodesByLabel = new Map() /** - * @private - * @param {Map} defaultGroupsBySchedule - * @returns {Map} + * @type {Map} */ - createScheduleContexts(defaultGroupsBySchedule) { - - /** @type {Map} */ - const schedules = new Map() - - for (let i = 0; i < this.systemGroups.length; i++) { - const config = this.systemGroups[i] - const context = getOrCreateScheduleContext(schedules, config.schedule) - const groupTypeId = typeid(config.label) - - if (context.groupIdsByTypeId.has(groupTypeId)) { - throws(`Duplicate system group label "${config.label.name}" on schedule "${config.schedule.name}".`) - } + groupIdsByTypeId = new Map() - /** @type {SystemGroupRegistration} */ - const group = { - id: context.groups.length, - config, - parentId: undefined, - systems: [] - } - - context.groups.push(group) - context.groupIdsByTypeId.set(groupTypeId, group.id) - - const groupLabel = config.label.name + /** + * @type {Map | undefined} + */ + graphIdsByGroupId = undefined - if (groupLabel !== '') { - const existing = context.nodesByLabel.get(groupLabel) + /** + * @type {Constructor | undefined} + */ + defaultSystemGroup = undefined - if (existing) { - throws(`Duplicate system group label "${groupLabel}" on schedule "${config.schedule.name}". Use a unique label or direct function references in ordering.`) - } + /** + * @param {SystemGroupConfig} config + */ + addGroup(config) { + const groupTypeId = typeid(config.label) - context.nodesByLabel.set(groupLabel, { kind: ScheduleNodeKind.Group, id: group.id }) - } + if (this.groupIdsByTypeId.has(groupTypeId)) { + throws(`Duplicate system group label "${config.label.name}" on schedule "${config.schedule.name}".`) } - for (let i = 0; i < this.systems.length; i++) { - const config = this.systems[i] - const context = getOrCreateScheduleContext(schedules, config.schedule) - const systemLabel = config.label || config.system.name - const system = { - id: context.systems.length, - config - } + /** @type {SystemGroupRegistration} */ + const group = { + id: this.groups.length, + config, + parentId: undefined, + systems: [] + } - context.systems.push(system) + this.groups.push(group) + this.groupIdsByTypeId.set(groupTypeId, group.id) - if (systemLabel !== '') { - const existing = context.nodesByLabel.get(systemLabel) + const groupLabel = config.label.name - if (existing) { - throws(`Duplicate system label "${systemLabel}" on schedule "${config.schedule.name}". Use a unique label or direct function references in ordering.`) - } + if (groupLabel !== '') { + const existing = this.nodesByLabel.get(groupLabel) - context.nodesByLabel.set(systemLabel, { kind: ScheduleNodeKind.System, id: system.id }) + if (existing) { + throws(`Duplicate system group label "${groupLabel}" on schedule "${config.schedule.name}". Use a unique label or direct function references in ordering.`) } - } - for (const [, context] of schedules) { - context.defaultSystemGroup = defaultGroupsBySchedule.get(typeid(context.label)) + this.nodesByLabel.set(groupLabel, { kind: ScheduleNodeKind.Group, id: group.id }) + } - this.resolveGroupParents(context, context.label) + return group + } - for (let i = 0; i < context.systems.length; i++) { - const system = context.systems[i] - const groupLabel = system.config.systemGroup ?? context.defaultSystemGroup + /** + * @param {SystemConfig} config + */ + addSystem(config) { + const systemLabel = config.label || config.system.name - if (!groupLabel) continue + const system = { + id: this.systems.length, + config + } - const groupId = context.groupIdsByTypeId.get(typeid(groupLabel)) + this.systems.push(system) - if (groupId === undefined) { - throws(`The system group "${groupLabel.name}" must be registered explicitly before it can be used on schedule "${context.label.name}".`) - } + if (systemLabel !== '') { + const existing = this.nodesByLabel.get(systemLabel) - context.groups[groupId].systems.push(system.id) + if (existing) { + throws(`Duplicate system label "${systemLabel}" on schedule "${config.schedule.name}". Use a unique label or direct function references in ordering.`) } + + this.nodesByLabel.set(systemLabel, { kind: ScheduleNodeKind.System, id: system.id }) } - return schedules + return system } /** - * @private - * @param {ScheduleContext} context - * @param {Constructor} scheduleLabel + * @param {Constructor | undefined} defaultSystemGroup */ - resolveGroupParents(context, scheduleLabel) { - for (let i = 0; i < context.groups.length; i++) { - const group = context.groups[i] + setDefaultSystemGroup(defaultSystemGroup) { + this.defaultSystemGroup = defaultSystemGroup + } + + /** + * Resolves group parents and validates that the hierarchy is acyclic. + */ + resolveGroupParents() { + for (let i = 0; i < this.groups.length; i++) { + const group = this.groups[i] const parentLabel = group.config.parent if (!parentLabel) continue - const parentId = context.groupIdsByTypeId.get(typeid(parentLabel)) + const parentId = this.groupIdsByTypeId.get(typeid(parentLabel)) if (parentId === undefined) { - throws(`The parent system group "${parentLabel.name}" must be registered explicitly before it can be used on schedule "${scheduleLabel.name}".`) + throws(`The parent system group "${parentLabel.name}" must be registered explicitly before it can be used on schedule "${this.label.name}".`) } group.parentId = parentId } - this.assertNoGroupCycles(context) + this.assertNoGroupCycles() } /** - * @private - * @param {ScheduleContext} context + * Assigns systems to their configured groups. */ - assertNoGroupCycles(context) { - - /** @type {GroupVisitState[]} */ - const state = new Array(context.groups.length).fill(GroupVisitState.Unvisited) - - for (let i = 0; i < context.groups.length; i++) { - this.visitGroupHierarchy(context, i, state) - } - } - - /** - * @private - * @param {ScheduleContext} context - * @param {number} groupId - * @param {GroupVisitState[]} state - */ - visitGroupHierarchy(context, groupId, state) { - const visitState = state[groupId] - - if (visitState === GroupVisitState.Visiting) { - const group = context.groups[groupId] + assignSystemsToGroups() { + for (let i = 0; i < this.systems.length; i++) { + const system = this.systems[i] + const groupLabel = system.config.systemGroup ?? this.defaultSystemGroup - throws(`Schedule "${context.label.name}" contains cyclic system group nesting involving "${group.config.label.name}".`) - } + if (!groupLabel) continue - if (visitState === GroupVisitState.Visited) return + const groupId = this.groupIdsByTypeId.get(typeid(groupLabel)) - state[groupId] = GroupVisitState.Visiting - - const { parentId } = context.groups[groupId] + if (groupId === undefined) { + throws(`The system group "${groupLabel.name}" must be registered explicitly before it can be used on schedule "${this.label.name}".`) + } - if (parentId !== undefined) { - this.visitGroupHierarchy(context, parentId, state) + this.groups[groupId].systems.push(system.id) } - - state[groupId] = GroupVisitState.Visited } /** - * @private - * @param {ScheduleContext} context * @returns {SystemRegistration[]} */ - sortScheduleSystems(context) { - const { graph, systemsByGraphId } = this.expandScheduleGraph(context) + sortSystems() { + const { graph, systemsByGraphId } = this.expandScheduleGraph() const sorted = kahnTopologySort(graph) if (!sorted) { - throws(`Schedule "${context.label.name}" contains cyclic system ordering constraints.`) + throws(`Schedule "${this.label.name}" contains cyclic system ordering constraints.`) } /** @type {SystemRegistration[]} */ @@ -241,11 +187,9 @@ export class SchedulerBuilder { } /** - * @private - * @param {ScheduleContext} context * @returns {{ graph: Graph, systemsByGraphId: Map }} */ - expandScheduleGraph(context) { + expandScheduleGraph() { const graph = /** @type {Graph} */ (new Graph(true)) /** @type {Map} */ @@ -254,7 +198,7 @@ export class SchedulerBuilder { /** @type {Map} */ const graphIdsByGroupId = new Map() - context.graphIdsByGroupId = graphIdsByGroupId + this.graphIdsByGroupId = graphIdsByGroupId /** @type {Map} */ const systemsByGraphId = new Map() @@ -265,34 +209,34 @@ export class SchedulerBuilder { /** @type {Set} */ const edges = new Set() - for (let i = 0; i < context.groups.length; i++) { - const group = context.groups[i] + for (let i = 0; i < this.groups.length; i++) { + const group = this.groups[i] const graphId = graph.addNode(group) graphIdsByGroupId.set(group.id, graphId) } - for (let i = 0; i < context.systems.length; i++) { - const system = context.systems[i] + for (let i = 0; i < this.systems.length; i++) { + const system = this.systems[i] const graphId = graph.addNode(system) graphIdsBySystemId.set(system.id, graphId) systemsByGraphId.set(graphId, system) } - for (let i = 0; i < context.systems.length; i++) { - const system = context.systems[i] + for (let i = 0; i < this.systems.length; i++) { + const system = this.systems[i] - this.addNodeOrdering(context, graph, graphIdsBySystemId, edges, { + this.addNodeOrdering(graph, graphIdsBySystemId, edges, { kind: ScheduleNodeKind.System, id: system.id }, system.config.before, system.config.after, groupSystemsCache) } - for (let i = 0; i < context.groups.length; i++) { - const group = context.groups[i] + for (let i = 0; i < this.groups.length; i++) { + const group = this.groups[i] - this.addNodeOrdering(context, graph, graphIdsBySystemId, edges, { + this.addNodeOrdering(graph, graphIdsBySystemId, edges, { kind: ScheduleNodeKind.Group, id: group.id }, group.config.before, group.config.after, groupSystemsCache) @@ -305,8 +249,6 @@ export class SchedulerBuilder { } /** - * @private - * @param {ScheduleContext} context * @param {Graph} graph * @param {Map} graphIdsBySystemId * @param {Set} edges @@ -315,14 +257,14 @@ export class SchedulerBuilder { * @param {(SystemFunc | Constructor | string)[] | undefined} after * @param {Map} groupSystemsCache */ - addNodeOrdering(context, graph, graphIdsBySystemId, edges, source, before, after, groupSystemsCache) { + addNodeOrdering(graph, graphIdsBySystemId, edges, source, before, after, groupSystemsCache) { if (before) { for (let i = 0; i < before.length; i++) { const label = describeReference(before[i]) - this.addExpandedEdge(context, graph, graphIdsBySystemId, edges, source, this.resolveNode(context, label), before[i], groupSystemsCache) - this.addExpandedEdge(context, graph, graphIdsBySystemId, edges, source, this.resolveNode(context, label), label, groupSystemsCache) + this.addExpandedEdge(graph, graphIdsBySystemId, edges, source, this.resolveNode(label), before[i], groupSystemsCache) + this.addExpandedEdge(graph, graphIdsBySystemId, edges, source, this.resolveNode(label), label, groupSystemsCache) } } @@ -330,32 +272,28 @@ export class SchedulerBuilder { for (let i = 0; i < after.length; i++) { const label = describeReference(after[i]) - this.addExpandedEdge(context, graph, graphIdsBySystemId, edges, this.resolveNode(context, label), source, after[i], groupSystemsCache) - this.addExpandedEdge(context, graph, graphIdsBySystemId, edges, this.resolveNode(context, label), source, label, groupSystemsCache) + this.addExpandedEdge(graph, graphIdsBySystemId, edges, this.resolveNode(label), source, after[i], groupSystemsCache) + this.addExpandedEdge(graph, graphIdsBySystemId, edges, this.resolveNode(label), source, label, groupSystemsCache) } } } /** - * @private - * @param {ScheduleContext} context * @param {string} label * @returns {ScheduleNodeRef} */ - resolveNode(context, label) { + resolveNode(label) { const stringLabel = /** @type {string} */ (label) - const node = context.nodesByLabel.get(stringLabel) + const node = this.nodesByLabel.get(stringLabel) if (!node) { - throws(`Could not resolve the system or system group label "${stringLabel}" on schedule "${context.label.name}".`) + throws(`Could not resolve the system or system group label "${stringLabel}" on schedule "${this.label.name}".`) } return node } /** - * @private - * @param {ScheduleContext} context * @param {Graph} graph * @param {Map} graphIdsBySystemId * @param {Set} edges @@ -364,9 +302,9 @@ export class SchedulerBuilder { * @param {SystemFunc | Constructor | string} targetLabel * @param {Map} groupSystemsCache */ - addExpandedEdge(context, graph, graphIdsBySystemId, edges, from, to, targetLabel, groupSystemsCache) { - const fromNodes = this.expandNodeToOrderingNodes(context, from, graphIdsBySystemId, groupSystemsCache) - const toNodes = this.expandNodeToOrderingNodes(context, to, graphIdsBySystemId, groupSystemsCache) + addExpandedEdge(graph, graphIdsBySystemId, edges, from, to, targetLabel, groupSystemsCache) { + const fromNodes = this.expandNodeToOrderingNodes(from, graphIdsBySystemId, groupSystemsCache) + const toNodes = this.expandNodeToOrderingNodes(to, graphIdsBySystemId, groupSystemsCache) for (let i = 0; i < fromNodes.length; i++) { for (let j = 0; j < toNodes.length; j++) { @@ -374,7 +312,7 @@ export class SchedulerBuilder { const toNodeId = toNodes[j] if (fromNodeId === toNodeId) { - throws(`The reference "${describeReference(targetLabel)}" creates a self-referential system ordering on schedule "${context.label.name}".`) + throws(`The reference "${describeReference(targetLabel)}" creates a self-referential system ordering on schedule "${this.label.name}".`) } const key = `${fromNodeId}:${toNodeId}` @@ -388,23 +326,21 @@ export class SchedulerBuilder { } /** - * @private - * @param {ScheduleContext} context * @param {ScheduleNodeRef} node * @param {Map} graphIdsBySystemId * @param {Map} groupSystemsCache * @returns {number[]} */ - expandNodeToOrderingNodes(context, node, graphIdsBySystemId, groupSystemsCache) { + expandNodeToOrderingNodes(node, graphIdsBySystemId, groupSystemsCache) { if (node.kind === ScheduleNodeKind.System) { const graphId = graphIdsBySystemId.get(node.id) - assert(graphId !== undefined, `Internal error: Could not resolve graph node for system ${node.id} on schedule "${context.label.name}".`) + assert(graphId !== undefined, `Internal error: Could not resolve graph node for system ${node.id} on schedule "${this.label.name}".`) return [graphId] } - const systems = this.expandGroupToSystems(context, node.id, groupSystemsCache, new Set()) + const systems = this.expandGroupToSystems(node.id, groupSystemsCache, new Set()) if (systems.length > 0) { @@ -414,75 +350,69 @@ export class SchedulerBuilder { for (let i = 0; i < systems.length; i++) { const graphId = graphIdsBySystemId.get(systems[i]) - assert(graphId !== undefined, `Internal error: Could not resolve graph node for system ${systems[i]} on schedule "${context.label.name}".`) + assert(graphId !== undefined, `Internal error: Could not resolve graph node for system ${systems[i]} on schedule "${this.label.name}".`) graphIds.push(graphId) } return graphIds } - const graphId = this.expandGroupToGraphNode(context, node.id) + const graphId = this.expandGroupToGraphNode(node.id) return [graphId] } /** - * @private - * @param {ScheduleContext} context * @param {number} groupId * @returns {number} */ - expandGroupToGraphNode(context, groupId) { - const graphId = context.graphIdsByGroupId?.get(groupId) + expandGroupToGraphNode(groupId) { + const graphId = this.graphIdsByGroupId?.get(groupId) - assert(graphId !== undefined, `Internal error: Could not resolve graph node for system group ${groupId} on schedule "${context.label.name}".`) + assert(graphId !== undefined, `Internal error: Could not resolve graph node for system group ${groupId} on schedule "${this.label.name}".`) return graphId } /** - * @private - * @param {ScheduleContext} context * @param {ScheduleNodeRef} node * @param {Map} groupSystemsCache * @returns {number[]} */ - expandNodeToSystems(context, node, groupSystemsCache) { + expandNodeToSystems(node, groupSystemsCache) { if (node.kind === ScheduleNodeKind.System) return [node.id] - return this.expandGroupToSystems(context, node.id, groupSystemsCache, new Set()) + return this.expandGroupToSystems(node.id, groupSystemsCache, new Set()) } /** - * @private - * @param {ScheduleContext} context * @param {number} groupId * @param {Map} cache * @param {Set} visiting * @returns {number[]} */ - expandGroupToSystems(context, groupId, cache, visiting) { + expandGroupToSystems(groupId, cache, visiting) { const cached = cache.get(groupId) if (cached) return cached if (visiting.has(groupId)) { - const group = context.groups[groupId] + const group = this.groups[groupId] - throws(`Schedule "${context.label.name}" contains cyclic system group nesting involving "${group.config.label.name}".`) + throws(`Schedule "${this.label.name}" contains cyclic system group nesting involving "${group.config.label.name}".`) } visiting.add(groupId) /** @type {number[]} */ - const systems = [...context.groups[groupId].systems] + const systems = [...this.groups[groupId].systems] - for (let i = 0; i < context.groups.length; i++) { - const child = context.groups[i] + for (let i = 0; i < this.groups.length; i++) { + const child = this.groups[i] if (child.parentId !== groupId) continue - const childSystems = this.expandGroupToSystems(context, child.id, cache, visiting) + const childSystems = this.expandGroupToSystems(child.id, cache, visiting) for (let j = 0; j < childSystems.length; j++) { systems.push(childSystems[j]) @@ -494,6 +424,166 @@ export class SchedulerBuilder { return systems } + + /** + * Validates that group nesting is acyclic. + */ + assertNoGroupCycles() { + + /** @type {GroupVisitState[]} */ + const state = new Array(this.groups.length).fill(GroupVisitState.Unvisited) + + for (let i = 0; i < this.groups.length; i++) { + this.visitGroupHierarchy(i, state) + } + } + + /** + * @param {number} groupId + * @param {GroupVisitState[]} state + */ + visitGroupHierarchy(groupId, state) { + const visitState = state[groupId] + + if (visitState === GroupVisitState.Visiting) { + const group = this.groups[groupId] + + throws(`Schedule "${this.label.name}" contains cyclic system group nesting involving "${group.config.label.name}".`) + } + + if (visitState === GroupVisitState.Visited) return + + state[groupId] = GroupVisitState.Visiting + + const { parentId } = this.groups[groupId] + + if (parentId !== undefined) { + this.visitGroupHierarchy(parentId, state) + } + + state[groupId] = GroupVisitState.Visited + } +} + +export class SchedulerBuilder { + + /** + * @private + * @type {Map} + */ + schedules = new Map() + + /** + * @private + * @type {{label: Constructor, delay?: number, repeat?: boolean, errorHandler?: (error: Error, world: import('../../ecs/index.js').World) => void, defaultSystemGroup?: Constructor}[]} + */ + scheduleConfigs = [] + + /** + * @private + * @type {SystemConfig[]} + */ + systems = [] + + /** + * @private + * @type {SystemGroupConfig[]} + */ + systemGroups = [] + + /** + * Clears the collected build state. + */ + clear() { + this.scheduleConfigs = [] + this.systems = [] + this.systemGroups = [] + this.schedules = new Map() + + return this + } + + /** + * @param {{label: Constructor, delay?: number, repeat?: boolean, errorHandler?: (error: Error, world: import('../../ecs/index.js').World) => void, defaultSystemGroup?: Constructor}} config + */ + addSchedule(config) { + this.scheduleConfigs.push(config) + } + + /** + * @param {SystemConfig} config + */ + add(config) { + this.systems.push(config) + } + + /** + * @param {SystemGroupConfig} config + */ + addGroup(config) { + this.systemGroups.push(config) + } + + /** + * @param {Scheduler} scheduler + */ + pushToScheduler(scheduler) { + for (let i = 0; i < this.scheduleConfigs.length; i++) { + scheduler.set(new Executable(this.scheduleConfigs[i])) + } + + /** @type {Map} */ + const defaultGroupsBySchedule = new Map() + + for (let i = 0; i < this.scheduleConfigs.length; i++) { + const config = this.scheduleConfigs[i] + + defaultGroupsBySchedule.set(typeid(config.label), config.defaultSystemGroup) + } + + const schedules = this.createScheduleContexts(defaultGroupsBySchedule) + + for (const [, context] of schedules) { + const schedule = scheduler.get(context.label) + + assert(schedule, `The schedule label "${context.label.name}" is not set in the provided \`Scheduler\`.`) + + for (const system of context.sortSystems()) { + schedule.add(system.config.system) + } + } + } + + /** + * @private + * @param {Map} defaultGroupsBySchedule + * @returns {Map} + */ + createScheduleContexts(defaultGroupsBySchedule) { + for (let i = 0; i < this.systemGroups.length; i++) { + const config = this.systemGroups[i] + const context = getOrCreateScheduleContext(this.schedules, config.schedule) + + context.addGroup(config) + } + + for (let i = 0; i < this.systems.length; i++) { + const config = this.systems[i] + const context = getOrCreateScheduleContext(this.schedules, config.schedule) + + context.addSystem(config) + } + + for (const [, context] of this.schedules) { + context.setDefaultSystemGroup(defaultGroupsBySchedule.get(typeid(context.label))) + context.resolveGroupParents() + context.assignSystemsToGroups() + } + + return this.schedules + } + + static Instance = new SchedulerBuilder() } /** @@ -507,15 +597,7 @@ function getOrCreateScheduleContext(schedules, label) { if (existing) return existing - const created = /** @type {ScheduleContext} */ ({ - label, - systems: [], - groups: [], - nodesByLabel: new Map(), - groupIdsByTypeId: new Map(), - graphIdsByGroupId: undefined, - defaultSystemGroup: undefined - }) + const created = new ScheduleContext(label) schedules.set(scheduleTypeId, created) @@ -537,17 +619,6 @@ const ScheduleNodeKind = Object.freeze({ Group: 1 }) -/** - * @typedef ScheduleContext - * @property {Constructor} label - * @property {SystemRegistration[]} systems - * @property {SystemGroupRegistration[]} groups - * @property {Map} nodesByLabel - * @property {Map} groupIdsByTypeId - * @property {Map | undefined} graphIdsByGroupId - * @property {Constructor | undefined} defaultSystemGroup - */ - /** * @typedef SystemRegistration * @property {number} id diff --git a/src/schedule/tests/scheduleBuilder.test.js b/src/schedule/tests/scheduleBuilder.test.js index 5c7df5ac..656338e7 100644 --- a/src/schedule/tests/scheduleBuilder.test.js +++ b/src/schedule/tests/scheduleBuilder.test.js @@ -14,6 +14,30 @@ class AlternatePhase { } class FencePhase { } describe('Testing `SchedulerBuilder`', () => { + test('creates schedule executables from builder configs', () => { + const builder = new SchedulerBuilder() + const scheduler = new Scheduler() + const world = new World() + /** @type {string[]} */ + const order = [] + + function tick() { order.push('tick') } + + builder.addSchedule({ + label: Update, + repeat: false + }) + builder.add({ + schedule: Update, + system: tick + }) + + builder.pushToScheduler(scheduler) + scheduler.get(Update)?.run(world) + + deepStrictEqual(order, ['tick']) + }) + test('sorts systems topologically from their `before` and `after` labels', () => { const builder = new SchedulerBuilder() const scheduler = new Scheduler()