From 0d377ba222edefe71a668d6b4249f318362b386c Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Sat, 21 Aug 2021 08:56:18 +0300 Subject: [PATCH] example: add full ood example with new version --- examples/ood.ts | 215 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 examples/ood.ts diff --git a/examples/ood.ts b/examples/ood.ts new file mode 100644 index 0000000..2903944 --- /dev/null +++ b/examples/ood.ts @@ -0,0 +1,215 @@ +import { StateMachine, StateMachineBuilder } from "../fsm.ts"; + +enum ProjectStatus { + Pending = "pending", + Active = "active", + Completed = "completed", + Archived = "archived", +} + +interface ProjectStateMachineContext { + projectService: ProjectService; + projectId: Project["id"]; +} + +const smbProject = new StateMachineBuilder< + ProjectStateMachineContext, + ProjectStatus +>() + .withStates( + [ProjectStatus.Pending, ProjectStatus.Archived, ProjectStatus.Completed], + { + async onEntry(_from, to, { projectService, projectId }) { + await projectService.updateProject(projectId, { status: to.name }); + }, + }, + ) + .withState( + ProjectStatus.Active, + { + async onEntry(_from, _to, { projectService, projectId }) { + await projectService.activateProject(projectId); + }, + }, + ) + .withTransitions([ + [ProjectStatus.Pending, [ProjectStatus.Active, ProjectStatus.Archived]], + [ProjectStatus.Active, [ProjectStatus.Completed]], + ]); + +async function main() { + await prepareExampleEnv(); + + const projectService = new ProjectService(); + + async function firstProjectPath() { + const project = await projectService.getProject(1); + const smProject = smbProject.build(project.status); + + const projectId = project.id; + const context = { projectService, projectId }; + + await logProject("[project 1] initial", projectId); + logAllowedStates(smProject); + + await smProject.tryChangeState(ProjectStatus.Active, context); + await logProject("[project 1] after activation", projectId); + logAllowedStates(smProject); + + await smProject.tryChangeState(ProjectStatus.Completed, context); + await logProject("[project 1] after completed", projectId); + logAllowedStates(smProject); + } + + async function secondProjectPath() { + const project = await projectService.getProject(2); + const smProject = smbProject.build(project.status); + + const projectId = project.id; + const context = { projectService, projectId }; + + await logProject("[project 2] initial", projectId); + logAllowedStates(smProject); + + await smProject.tryChangeState(ProjectStatus.Archived, context); + await logProject("[project 2] after activation", projectId); + logAllowedStates(smProject); + } + + async function logProject(message: string, projectId: number) { + console.log(message, await projectService.getProject(projectId)); + } + + function logAllowedStates(sm: StateMachine) { + console.log("[fsm] transitions", sm.allowedTransitionStateNames()); + } + + await firstProjectPath(); + console.log("---------"); + await secondProjectPath(); +} + +async function prepareExampleEnv() { + const projectService = new ProjectService(); + + await projectService.createProject({ name: "my first project" }); + await projectService.createProject({ name: "my second project" }); +} + +class ProjectService { + private readonly projectStorage = new ProjectStorage(); + + createProject(insertData: CreateProjectData) { + return this.projectStorage.createProject(insertData); + } + + updateProject(id: Project["id"], patchData: UpdateProjectData) { + return this.projectStorage.updateProject(id, patchData); + } + + async activateProject(id: Project["id"]) { + const project = await this.getProject(id); + + await this.projectStorage.updateProject( + project.id, + { status: ProjectStatus.Active }, + ); + } + + async getProject(id: Project["id"]) { + const project = await this.getProjectUnchecked(id); + if (!project) throw new Error("Project does not exist"); + return project; + } + + getProjectUnchecked(id: Project["id"]) { + return this.projectStorage.getProject(id); + } +} + +interface CreateProjectData { + name: string; +} + +interface UpdateProjectData { + name?: string; + status?: ProjectStatus; +} + +class ProjectStorage { + createProject(insertData: CreateProjectData) { + return randomDebounced(() => { + const createdData: Project = { + ...insertData, + id: genProjectId(), + status: ProjectStatus.Pending, + createdAt: new Date(), + updatedAt: new Date(), + }; + _projectStore.set(createdData.id, createdData); + return createdData; + }); + } + + updateProject(id: Project["id"], patchData: UpdateProjectData) { + return randomDebounced(() => { + const currentData = _projectStore.get(id); + if (!currentData) return null; + + const updatedData: Project = { + ...currentData, + ...patchData, + updatedAt: new Date(), + }; + + if ( + currentData.activatedAt == null && + patchData.status === ProjectStatus.Active + ) { + updatedData.activatedAt = new Date(); + } + + _projectStore.set(id, updatedData); + return updatedData; + }); + } + + getProject(id: Project["id"]) { + return randomDebounced(() => _projectStore.get(id)); + } +} + +interface Project { + id: number; + name: string; + status: ProjectStatus; + createdAt: Date; + updatedAt: Date; + activatedAt?: Date; +} + +function genProjectId() { + return _projectStore.size + 1; +} + +const _projectStore = new Map(); + +function randomDebounced(fn: () => T): Promise { + return debounced(fn, randomWaitMs()); +} + +function debounced(fn: () => T, wait: number): Promise { + return delay(wait).then(fn); +} + +function delay(wait: number) { + return new Promise((resolve) => setTimeout(resolve, wait)); +} + +function randomWaitMs() { + return Math.round(Math.random() * 1000) + 1000; +} + +if (import.meta.main) { + await main(); +}