From a81afc9221d8c0d1e3ae3b16acb3a2a123e66385 Mon Sep 17 00:00:00 2001 From: Morten Olsen Date: Wed, 6 Sep 2023 22:58:51 +0200 Subject: [PATCH] feat: improved plugin structure --- packages/algorithm/package.json | 5 +- .../algorithm/src/algorithm/calulation.ts | 164 ++++++----- .../algorithm/src/algorithm/expand-node.ts | 24 +- packages/algorithm/src/index.ts | 2 +- packages/algorithm/src/plugins/create.ts | 20 ++ packages/algorithm/src/plugins/index.ts | 3 +- packages/algorithm/src/plugins/transport.ts | 114 ++++---- packages/algorithm/src/types/node.ts | 7 +- packages/algorithm/src/types/planable.ts | 1 - packages/algorithm/src/types/plugin.ts | 30 ++- packages/configs/tsconfig.esm.json | 8 +- packages/playground/index.html | 3 +- .../playground/src/features/runner/block.tsx | 4 +- packages/playground/src/pages/hello/script.ts | 254 +++++++++--------- packages/playground/src/pages/hello/worker.ts | 1 + pnpm-lock.yaml | 8 + 16 files changed, 366 insertions(+), 282 deletions(-) create mode 100644 packages/algorithm/src/plugins/create.ts diff --git a/packages/algorithm/package.json b/packages/algorithm/package.json index b007667..4cb8ace 100644 --- a/packages/algorithm/package.json +++ b/packages/algorithm/package.json @@ -25,5 +25,8 @@ "build": "tsc --build configs/tsconfig.libs.json" }, "types": "./dist/cjs/types/index.d.ts", - "version": "0.1.8" + "version": "0.1.8", + "dependencies": { + "@sinclair/typebox": "^0.31.14" + } } diff --git a/packages/algorithm/src/algorithm/calulation.ts b/packages/algorithm/src/algorithm/calulation.ts index cecf472..61dd9aa 100644 --- a/packages/algorithm/src/algorithm/calulation.ts +++ b/packages/algorithm/src/algorithm/calulation.ts @@ -1,87 +1,103 @@ -import { Attributes, GraphNode } from '../types/node'; +import { GraphNode } from '../types/node'; import { Planable } from '../types/planable'; -import { PluginAttributes, Plugins } from '../types/plugin'; +import { PluginAttributes, PluginContext, Plugins } from '../types/plugin'; import { expandNode } from './expand-node'; type CalulationOptions = { - location: string; - time: number; - planables: Planable>[]; plugins: TPlugins; +}; + +type CalulationResult = { + root: GraphNode, PluginContext>; + nodes: GraphNode, PluginContext>[]; + completed: GraphNode, PluginContext>[]; + planables: Planable>[]; +}; + +type RunOptions = { + start: number; + context: PluginContext; + planables: Planable>[]; heuristic?: (result: any) => boolean; - onUpdated?: (result: any) => void; }; -type CalulationResult = { - root: GraphNode; - nodes: GraphNode[]; - completed: GraphNode[]; - planables: Planable[]; -}; +class Bob { + #options: CalulationOptions; + #id: number = 0; -const idGen = () => { - let id = 0; - return () => { - id += 1; - return id.toString(); - }; -}; - -const calulation = async ({ - location, - time, - planables, - plugins, - heuristic, - onUpdated, -}: CalulationOptions): Promise< - CalulationResult> -> => { - const generateId = idGen(); - let exploreId = 1; - const root: GraphNode> = { - id: generateId(), - type: 'root', - score: 0, - parent: null, - duration: 0, - time, - location, - exploreId: 0, - remaining: planables, - }; - - const nodes: GraphNode>[] = [root]; - const leafNodes: GraphNode>[] = [root]; - const completed: GraphNode>[] = []; - - const popHighestScore = () => { - const highestScore = Math.max(...leafNodes.map((n) => n.score)); - const highestScoreNode = leafNodes.find((n) => n.score === highestScore); - if (!highestScoreNode) { - throw new Error('No highest score node'); - } - leafNodes.splice(leafNodes.indexOf(highestScoreNode), 1); - return highestScoreNode; - }; - - while (leafNodes.length > 0) { - const node = popHighestScore(); - node.exploreId = exploreId++; - const expanded = await expandNode({ node, generateId, plugins }); - nodes.push(...expanded); - completed.push(...expanded.filter((n) => n.remaining.length === 0)); - leafNodes.push(...expanded.filter((n) => n.remaining.length > 0)); - if (heuristic && heuristic({ root, nodes, completed, planables })) { - break; - } - if (onUpdated) { - onUpdated({ root, nodes, completed, planables }); - } + constructor(options: CalulationOptions) { + this.#options = options; } - return { root, nodes, completed, planables }; -}; + #getNextId = (): string => { + return (this.#id++).toString(); + }; + + public run = async ({ + planables, + start, + context, + heuristic, + }: RunOptions): Promise> => { + const { plugins } = this.#options; + let exploreId = 1; + const root: GraphNode< + PluginAttributes, + PluginContext + > = { + id: this.#getNextId(), + context, + type: 'root', + score: 0, + parent: null, + duration: 0, + time: start, + exploreId: 0, + remaining: planables, + }; + + const nodes: GraphNode< + PluginAttributes, + PluginContext + >[] = [root]; + const leafNodes: GraphNode< + PluginAttributes, + PluginContext + >[] = [root]; + const completed: GraphNode< + PluginAttributes, + PluginContext + >[] = []; + + const popHighestScore = () => { + const highestScore = Math.max(...leafNodes.map((n) => n.score)); + const highestScoreNode = leafNodes.find((n) => n.score === highestScore); + if (!highestScoreNode) { + throw new Error('No highest score node'); + } + leafNodes.splice(leafNodes.indexOf(highestScoreNode), 1); + return highestScoreNode; + }; + + while (leafNodes.length > 0) { + const node = popHighestScore(); + node.exploreId = exploreId++; + const expanded = await expandNode({ + node, + generateId: this.#getNextId, + plugins, + }); + nodes.push(...expanded); + completed.push(...expanded.filter((n) => n.remaining.length === 0)); + leafNodes.push(...expanded.filter((n) => n.remaining.length > 0)); + if (heuristic && heuristic({ root, nodes, completed, planables })) { + break; + } + } + + return { root, nodes, completed, planables }; + }; +} export type { CalulationOptions, CalulationResult }; -export { calulation }; +export { Bob }; diff --git a/packages/algorithm/src/algorithm/expand-node.ts b/packages/algorithm/src/algorithm/expand-node.ts index ad1a7ef..bbd7e24 100644 --- a/packages/algorithm/src/algorithm/expand-node.ts +++ b/packages/algorithm/src/algorithm/expand-node.ts @@ -1,18 +1,26 @@ import { Attributes, GraphNode } from '../types/node'; -import { Plugin } from '../types/plugin'; +import { Plugins } from '../types/plugin'; import { hasImpossible } from './is-impossible'; -type ExpandOptions = { - node: GraphNode; +type ExpandOptions< + TAttributes extends Attributes, + TContext extends Attributes, +> = { + node: GraphNode; generateId: () => string; - plugins: Plugin[]; + plugins: Plugins; }; -const expandNode = async ({ +const expandNode = async < + TAttributes extends Attributes, + TContext extends Attributes, +>({ node, generateId, plugins, -}: ExpandOptions): Promise[]> => { +}: ExpandOptions): Promise< + GraphNode[] +> => { const isImpossible = hasImpossible({ node }); if (isImpossible) { @@ -21,7 +29,7 @@ const expandNode = async ({ } const metaNodes = await Promise.all( - plugins.map(async (plugin) => { + Object.values(plugins).map(async (plugin) => { if (!plugin.getMetaNodes) { return []; } @@ -39,7 +47,7 @@ const expandNode = async ({ ); const planables = node.remaining.filter((planable) => { - const hasNonPlanable = plugins.some( + const hasNonPlanable = Object.values(plugins).some( (plugin) => plugin.isPlanable && !plugin.isPlanable(node, planable), ); return !hasNonPlanable; diff --git a/packages/algorithm/src/index.ts b/packages/algorithm/src/index.ts index c52f400..8f5dc1d 100644 --- a/packages/algorithm/src/index.ts +++ b/packages/algorithm/src/index.ts @@ -1,5 +1,5 @@ export type { GraphNode } from './types/node'; export type { Planable } from './types/planable'; export { expandNode } from './algorithm/expand-node'; -export { calulation, type CalulationResult } from './algorithm/calulation'; +export { Bob, type CalulationResult } from './algorithm/calulation'; export { plugins } from './plugins/index'; diff --git a/packages/algorithm/src/plugins/create.ts b/packages/algorithm/src/plugins/create.ts new file mode 100644 index 0000000..ea99111 --- /dev/null +++ b/packages/algorithm/src/plugins/create.ts @@ -0,0 +1,20 @@ +import type { TSchema, Static } from '@sinclair/typebox'; +import type { Plugin } from '../types/plugin'; + +const createPlugin = < + TAttribuesSchema extends TSchema, + TContextSchema extends TSchema, +>( + attributes: TAttribuesSchema, + context: TContextSchema, + plugin: Omit< + Plugin, Static>, + 'attributes' | 'context' + >, +): Plugin, Static> => ({ + ...plugin, + attributes, + context, +}); + +export { createPlugin }; diff --git a/packages/algorithm/src/plugins/index.ts b/packages/algorithm/src/plugins/index.ts index 53199a0..9bf3dce 100644 --- a/packages/algorithm/src/plugins/index.ts +++ b/packages/algorithm/src/plugins/index.ts @@ -1,9 +1,8 @@ -import { Attributes } from '../types/node'; import { Plugin } from '../types/plugin'; import { transport } from './transport'; const plugins = { transport, -} satisfies Record Plugin>; +} satisfies Record Plugin>; export { plugins }; diff --git a/packages/algorithm/src/plugins/transport.ts b/packages/algorithm/src/plugins/transport.ts index e43ed68..207cd1c 100644 --- a/packages/algorithm/src/plugins/transport.ts +++ b/packages/algorithm/src/plugins/transport.ts @@ -1,5 +1,5 @@ -import { GraphNode } from '../types/node'; -import { Plugin } from '../types/plugin'; +import { Type } from '@sinclair/typebox'; +import { createPlugin } from './create'; type GetTravelTime = (from: string, to: string) => Promise; @@ -7,58 +7,66 @@ type TransportOptions = { getTravelTime: GetTravelTime; }; -type TransportAttributes = { - locations?: string[]; -}; +const transport = ({ getTravelTime }: TransportOptions) => + createPlugin( + Type.Object({ + locations: Type.Optional(Type.Array(Type.String())), + }), + Type.Object({ + location: Type.String(), + }), + { + getMetaNodes: async (node) => { + const locations = + (node.type !== 'travel' && + [ + ...new Set( + node.remaining + .map((planable) => planable.attributes?.locations) + .flat(), + ), + ] + .filter((location) => location !== node.context.location) + .filter(Boolean) + .map((l) => l!)) || + []; -const transport = ({ - getTravelTime, -}: TransportOptions): Plugin => ({ - getMetaNodes: async (node) => { - const locations = - (node.type !== 'travel' && - [ - ...new Set( - node.remaining - .map((planable) => planable.attributes?.locations) - .flat(), - ), - ] - .filter((location) => location !== node.location) - .filter(Boolean) - .map((l) => l!)) || - []; + const travelNodes = await Promise.all( + locations.map(async (location) => { + const travelTime = await getTravelTime( + node.context.location, + location, + ); + return { + ...node, + type: 'travel' as const, + context: { + ...node.context, + location, + }, + planable: undefined, + location, + exploreId: 0, + score: node.score - 10, + time: node.time + node.duration, + duration: travelTime, + parent: node.id, + }; + }), + ); - const travelNodes = await Promise.all( - locations.map>>( - async (location) => { - const travelTime = await getTravelTime(node.location, location); - return { - ...node, - type: 'travel', - planable: undefined, - location, - exploreId: 0, - score: node.score - 20, - time: node.time + node.duration, - duration: travelTime, - parent: node.id, - }; - }, - ), - ); - - return travelNodes; - }, - isPlanable: (node, planable) => { - if ( - planable.attributes?.locations && - !planable.attributes?.locations.includes(node.location) - ) { - return false; - } - return true; - }, -}); + return travelNodes; + }, + isPlanable: (node, planable) => { + if ( + planable.attributes?.locations && + !planable.attributes?.locations.includes(node.context.location) + ) { + return false; + } + return true; + }, + }, + ); export { transport }; diff --git a/packages/algorithm/src/types/node.ts b/packages/algorithm/src/types/node.ts index 76fcb38..a758d62 100644 --- a/packages/algorithm/src/types/node.ts +++ b/packages/algorithm/src/types/node.ts @@ -2,11 +2,14 @@ import type { Planable } from './planable'; type Attributes = any; -type GraphNode = { +type GraphNode< + TAttributes extends Attributes = Attributes, + TContext extends Attributes = Attributes, +> = { id: string; type: 'root' | 'planable' | 'travel'; score: number; - location: string; + context: TContext; parent: string | null; time: number; exploreId: number; diff --git a/packages/algorithm/src/types/planable.ts b/packages/algorithm/src/types/planable.ts index 63563cc..4aa9b75 100644 --- a/packages/algorithm/src/types/planable.ts +++ b/packages/algorithm/src/types/planable.ts @@ -11,7 +11,6 @@ type Planable = { }; attributes: TAttributes; required?: boolean; - locations?: string[]; }; export type { Planable }; diff --git a/packages/algorithm/src/types/plugin.ts b/packages/algorithm/src/types/plugin.ts index 42f869d..90ce418 100644 --- a/packages/algorithm/src/types/plugin.ts +++ b/packages/algorithm/src/types/plugin.ts @@ -1,25 +1,31 @@ -import { Attributes, GraphNode } from './node'; +import { GraphNode } from './node'; import { Planable } from './planable'; -type Plugin = { +type Plugin = { + context: any; + attributes: any; getMetaNodes?: ( - node: GraphNode, - ) => Promise[]>; - isImpossible?: (node: GraphNode) => Promise; + node: GraphNode, + ) => Promise[]>; + isImpossible?: (node: GraphNode) => Promise; isPlanable?: ( - node: GraphNode, + node: GraphNode, planable: Planable, ) => boolean; }; -type Plugins = Plugin[]; +type Plugins = Record; type PluginAttributes = { - [K in keyof TPlugins]: TPlugins[K] extends Plugin - ? TAttributes extends Attributes + [K in keyof TPlugins]: TPlugins[K] extends Plugin ? TAttributes - : never : never; -}[number]; +}[keyof TPlugins]; -export type { Plugin, Plugins, PluginAttributes }; +type PluginContext = { + [K in keyof TPlugins]: TPlugins[K] extends Plugin + ? TContext + : never; +}[keyof TPlugins]; + +export type { Plugin, Plugins, PluginAttributes, PluginContext }; diff --git a/packages/configs/tsconfig.esm.json b/packages/configs/tsconfig.esm.json index 7c59caa..77d2b9a 100644 --- a/packages/configs/tsconfig.esm.json +++ b/packages/configs/tsconfig.esm.json @@ -1,8 +1,12 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { - "lib": ["ES2022", "DOM"], + "lib": [ + "ES2022", + "DOM" + ], "target": "ES2022", - "module": "ESNext" + "module": "ES2022", + "moduleResolution": "node" } } diff --git a/packages/playground/index.html b/packages/playground/index.html index e4b78ea..7e6449b 100644 --- a/packages/playground/index.html +++ b/packages/playground/index.html @@ -1,8 +1,7 @@ - + - Vite + React + TS diff --git a/packages/playground/src/features/runner/block.tsx b/packages/playground/src/features/runner/block.tsx index a9c3abd..f701b8b 100644 --- a/packages/playground/src/features/runner/block.tsx +++ b/packages/playground/src/features/runner/block.tsx @@ -14,7 +14,7 @@ type BlockProps = { presenter?: React.FC; }; -const id = (function* () { +const id = (function*() { let i = 0; while (true) { yield i++; @@ -57,6 +57,7 @@ const Block: React.FC = ({ }); } catch (error) { setError(error); + console.error(error); } setRunning(false); @@ -75,6 +76,7 @@ const Block: React.FC = ({ } if (type === 'error') { setError(payload); + console.error(payload); } }; worker.addEventListener('message', listener); diff --git a/packages/playground/src/pages/hello/script.ts b/packages/playground/src/pages/hello/script.ts index 2f78d36..9d8bf9c 100644 --- a/packages/playground/src/pages/hello/script.ts +++ b/packages/playground/src/pages/hello/script.ts @@ -1,4 +1,4 @@ -import { calulation, plugins } from '@bob-the-algorithm/core'; +import { Bob, plugins } from '@bob-the-algorithm/core'; import { createWorker } from '../../features/runner/worker'; import { convertResult } from '../../utils/graph'; @@ -6,141 +6,149 @@ const MIN = 1000 * 60; const HOUR = 1000 * 60 * 60; const getTravelTime = async () => 30 * MIN; +const transport = plugins.transport({ + getTravelTime, +}); const realistic = async () => { - const result = await calulation({ - location: 'home', - time: 0, - heuristic: ({ completed }) => completed.length >= 3, - plugins: [ - plugins.transport({ - getTravelTime, - }), - ], - planables: [ - { - id: `Brush teeth`, - duration: 2 * MIN, - start: { - min: 7 * HOUR, - max: 8 * HOUR, - }, - attributes: { - locations: ['home'], - }, - score: 1, + try { + const bob = new Bob({ + plugins: { transport }, + }); + const result = await bob.run({ + context: { + location: 'home', }, - { - id: 'Drop off kids', - duration: 30 * MIN, - attributes: { - locations: ['daycare'], + start: 0, + heuristic: ({ completed }) => completed.length >= 3, + planables: [ + { + id: `Brush teeth`, + duration: 2 * MIN, + start: { + min: 7 * HOUR, + max: 8 * HOUR, + }, + attributes: { + locations: ['home'], + }, + score: 1, }, - score: 1, - start: { - min: 7 * HOUR, - max: 9 * HOUR, + { + id: 'Drop off kids', + duration: 30 * MIN, + attributes: { + locations: ['daycare'], + }, + score: 1, + start: { + min: 7 * HOUR, + max: 9 * HOUR, + }, }, - }, - { - id: 'Pickup the kids', - duration: 30 * MIN, - attributes: { - locations: ['daycare'], + { + id: 'Pickup the kids', + duration: 30 * MIN, + attributes: { + locations: ['daycare'], + }, + score: 1, + start: { + min: 15 * HOUR, + max: 15.5 * HOUR, + }, }, - score: 1, - start: { - min: 15 * HOUR, - max: 15.5 * HOUR, + { + id: `Eat breakfast`, + duration: 15 * MIN, + start: { + min: 7 * HOUR, + max: 9 * HOUR, + }, + attributes: { + locations: ['home'], + }, + score: 1, }, - }, - { - id: `Eat breakfast`, - duration: 15 * MIN, - start: { - min: 7 * HOUR, - max: 9 * HOUR, + { + id: 'Do work', + duration: 1 * HOUR, + count: 5, + attributes: { + locations: ['work'], + }, + score: 10, + start: { + min: 8 * HOUR, + max: 18 * HOUR, + }, }, - attributes: { - locations: ['home'], + { + id: 'Read book', + duration: 0.5 * HOUR, + attributes: { + locations: ['home', 'work'], + }, + score: 3, + count: 2, + start: { + min: 8 * HOUR, + max: 22 * HOUR, + }, }, - score: 1, - }, - { - id: 'Do work', - duration: 1 * HOUR, - count: 5, - attributes: { - locations: ['work'], + { + id: 'Meditate', + duration: 10 * MIN, + score: 1, + attributes: {}, + start: { + min: 8 * HOUR, + max: 22 * HOUR, + }, }, - score: 10, - start: { - min: 8 * HOUR, - max: 18 * HOUR, + { + id: 'Meeting 1', + duration: 1 * HOUR, + attributes: { + locations: ['work', 'work'], + }, + score: 10, + start: { + min: 10 * HOUR, + max: 10 * HOUR, + }, }, - }, - { - id: 'Read book', - duration: 0.5 * HOUR, - attributes: { - locations: ['home', 'work'], + { + id: 'Meeting 2', + duration: 1 * HOUR, + attributes: { + locations: ['work', 'work'], + }, + score: 10, + start: { + min: 12 * HOUR, + max: 12 * HOUR, + }, }, - score: 3, - count: 2, - start: { - min: 8 * HOUR, - max: 22 * HOUR, + { + id: 'Play playstation', + duration: 1 * HOUR, + attributes: { + locations: ['home'], + }, + score: 10, + start: { + min: 16 * HOUR, + max: 24 * HOUR, + }, }, - }, - { - id: 'Meditate', - duration: 10 * MIN, - score: 1, - attributes: {}, - start: { - min: 8 * HOUR, - max: 22 * HOUR, - }, - }, - { - id: 'Meeting 1', - duration: 1 * HOUR, - attributes: { - locations: ['work', 'work'], - }, - score: 10, - start: { - min: 10 * HOUR, - max: 10 * HOUR, - }, - }, - { - id: 'Meeting 2', - duration: 1 * HOUR, - attributes: { - locations: ['work', 'work'], - }, - score: 10, - start: { - min: 12 * HOUR, - max: 12 * HOUR, - }, - }, - { - id: 'Play playstation', - duration: 1 * HOUR, - attributes: { - locations: ['home'], - }, - score: 10, - start: { - min: 16 * HOUR, - max: 24 * HOUR, - }, - }, - ], - }); - return convertResult(result); + ], + }); + return convertResult(result); + } catch (e) { + console.error(e); + throw e; + } }; createWorker({ diff --git a/packages/playground/src/pages/hello/worker.ts b/packages/playground/src/pages/hello/worker.ts index c2f077d..bc9cc9c 100644 --- a/packages/playground/src/pages/hello/worker.ts +++ b/packages/playground/src/pages/hello/worker.ts @@ -1,3 +1,4 @@ +import './script.ts'; const worker = new Worker(new URL('./script.ts', import.meta.url), { type: 'module', }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c831194..07cf239 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,6 +31,10 @@ importers: version: 20.5.9 packages/algorithm: + dependencies: + '@sinclair/typebox': + specifier: ^0.31.14 + version: 0.31.14 devDependencies: '@bob-the-algorithm/config': specifier: workspace:^ @@ -1234,6 +1238,10 @@ packages: rollup: 3.28.1 dev: true + /@sinclair/typebox@0.31.14: + resolution: {integrity: sha512-2spk0ie6J/4r+nwb55OtBXUn5cZLF9S98fopIjuutBVoq8yLRNh+h8QvMkCjMu5gWBMnnZ/PUSXeHE3xGBPKLQ==} + dev: false + /@swc/core-darwin-arm64@1.3.82: resolution: {integrity: sha512-JfsyDW34gVKD3uE0OUpUqYvAD3yseEaicnFP6pB292THtLJb0IKBBnK50vV/RzEJtc1bR3g1kNfxo2PeurZTrA==} engines: {node: '>=10'}