feat: improved plugin structure

This commit is contained in:
Morten Olsen
2023-09-06 22:58:51 +02:00
parent 89c0271fc5
commit a81afc9221
16 changed files with 366 additions and 282 deletions

View File

@@ -25,5 +25,8 @@
"build": "tsc --build configs/tsconfig.libs.json" "build": "tsc --build configs/tsconfig.libs.json"
}, },
"types": "./dist/cjs/types/index.d.ts", "types": "./dist/cjs/types/index.d.ts",
"version": "0.1.8" "version": "0.1.8",
"dependencies": {
"@sinclair/typebox": "^0.31.14"
}
} }

View File

@@ -1,59 +1,73 @@
import { Attributes, GraphNode } from '../types/node'; import { GraphNode } from '../types/node';
import { Planable } from '../types/planable'; import { Planable } from '../types/planable';
import { PluginAttributes, Plugins } from '../types/plugin'; import { PluginAttributes, PluginContext, Plugins } from '../types/plugin';
import { expandNode } from './expand-node'; import { expandNode } from './expand-node';
type CalulationOptions<TPlugins extends Plugins> = { type CalulationOptions<TPlugins extends Plugins> = {
location: string;
time: number;
planables: Planable<PluginAttributes<TPlugins>>[];
plugins: TPlugins; plugins: TPlugins;
};
type CalulationResult<TPlugins extends Plugins> = {
root: GraphNode<PluginAttributes<TPlugins>, PluginContext<TPlugins>>;
nodes: GraphNode<PluginAttributes<TPlugins>, PluginContext<TPlugins>>[];
completed: GraphNode<PluginAttributes<TPlugins>, PluginContext<TPlugins>>[];
planables: Planable<PluginAttributes<TPlugins>>[];
};
type RunOptions<TPlugins extends Plugins> = {
start: number;
context: PluginContext<TPlugins>;
planables: Planable<PluginAttributes<TPlugins>>[];
heuristic?: (result: any) => boolean; heuristic?: (result: any) => boolean;
onUpdated?: (result: any) => void;
}; };
type CalulationResult<TAttributes extends Attributes> = { class Bob<TPlugins extends Plugins> {
root: GraphNode<TAttributes>; #options: CalulationOptions<TPlugins>;
nodes: GraphNode<TAttributes>[]; #id: number = 0;
completed: GraphNode<TAttributes>[];
planables: Planable<TAttributes>[];
};
const idGen = () => { constructor(options: CalulationOptions<TPlugins>) {
let id = 0; this.#options = options;
return () => { }
id += 1;
return id.toString(); #getNextId = (): string => {
return (this.#id++).toString();
}; };
};
const calulation = async <TPlugins extends Plugins>({ public run = async ({
location,
time,
planables, planables,
plugins, start,
context,
heuristic, heuristic,
onUpdated, }: RunOptions<TPlugins>): Promise<CalulationResult<TPlugins>> => {
}: CalulationOptions<TPlugins>): Promise< const { plugins } = this.#options;
CalulationResult<PluginAttributes<TPlugins>>
> => {
const generateId = idGen();
let exploreId = 1; let exploreId = 1;
const root: GraphNode<PluginAttributes<TPlugins>> = { const root: GraphNode<
id: generateId(), PluginAttributes<TPlugins>,
PluginContext<TPlugins>
> = {
id: this.#getNextId(),
context,
type: 'root', type: 'root',
score: 0, score: 0,
parent: null, parent: null,
duration: 0, duration: 0,
time, time: start,
location,
exploreId: 0, exploreId: 0,
remaining: planables, remaining: planables,
}; };
const nodes: GraphNode<PluginAttributes<TPlugins>>[] = [root]; const nodes: GraphNode<
const leafNodes: GraphNode<PluginAttributes<TPlugins>>[] = [root]; PluginAttributes<TPlugins>,
const completed: GraphNode<PluginAttributes<TPlugins>>[] = []; PluginContext<TPlugins>
>[] = [root];
const leafNodes: GraphNode<
PluginAttributes<TPlugins>,
PluginContext<TPlugins>
>[] = [root];
const completed: GraphNode<
PluginAttributes<TPlugins>,
PluginContext<TPlugins>
>[] = [];
const popHighestScore = () => { const popHighestScore = () => {
const highestScore = Math.max(...leafNodes.map((n) => n.score)); const highestScore = Math.max(...leafNodes.map((n) => n.score));
@@ -68,20 +82,22 @@ const calulation = async <TPlugins extends Plugins>({
while (leafNodes.length > 0) { while (leafNodes.length > 0) {
const node = popHighestScore(); const node = popHighestScore();
node.exploreId = exploreId++; node.exploreId = exploreId++;
const expanded = await expandNode({ node, generateId, plugins }); const expanded = await expandNode({
node,
generateId: this.#getNextId,
plugins,
});
nodes.push(...expanded); nodes.push(...expanded);
completed.push(...expanded.filter((n) => n.remaining.length === 0)); completed.push(...expanded.filter((n) => n.remaining.length === 0));
leafNodes.push(...expanded.filter((n) => n.remaining.length > 0)); leafNodes.push(...expanded.filter((n) => n.remaining.length > 0));
if (heuristic && heuristic({ root, nodes, completed, planables })) { if (heuristic && heuristic({ root, nodes, completed, planables })) {
break; break;
} }
if (onUpdated) {
onUpdated({ root, nodes, completed, planables });
}
} }
return { root, nodes, completed, planables }; return { root, nodes, completed, planables };
}; };
}
export type { CalulationOptions, CalulationResult }; export type { CalulationOptions, CalulationResult };
export { calulation }; export { Bob };

View File

@@ -1,18 +1,26 @@
import { Attributes, GraphNode } from '../types/node'; import { Attributes, GraphNode } from '../types/node';
import { Plugin } from '../types/plugin'; import { Plugins } from '../types/plugin';
import { hasImpossible } from './is-impossible'; import { hasImpossible } from './is-impossible';
type ExpandOptions<TAttributes extends Attributes> = { type ExpandOptions<
node: GraphNode<TAttributes>; TAttributes extends Attributes,
TContext extends Attributes,
> = {
node: GraphNode<TAttributes, TContext>;
generateId: () => string; generateId: () => string;
plugins: Plugin[]; plugins: Plugins;
}; };
const expandNode = async <TAttributes extends Attributes>({ const expandNode = async <
TAttributes extends Attributes,
TContext extends Attributes,
>({
node, node,
generateId, generateId,
plugins, plugins,
}: ExpandOptions<TAttributes>): Promise<GraphNode<TAttributes>[]> => { }: ExpandOptions<TAttributes, TContext>): Promise<
GraphNode<TAttributes, TContext>[]
> => {
const isImpossible = hasImpossible({ node }); const isImpossible = hasImpossible({ node });
if (isImpossible) { if (isImpossible) {
@@ -21,7 +29,7 @@ const expandNode = async <TAttributes extends Attributes>({
} }
const metaNodes = await Promise.all( const metaNodes = await Promise.all(
plugins.map(async (plugin) => { Object.values(plugins).map(async (plugin) => {
if (!plugin.getMetaNodes) { if (!plugin.getMetaNodes) {
return []; return [];
} }
@@ -39,7 +47,7 @@ const expandNode = async <TAttributes extends Attributes>({
); );
const planables = node.remaining.filter((planable) => { const planables = node.remaining.filter((planable) => {
const hasNonPlanable = plugins.some( const hasNonPlanable = Object.values(plugins).some(
(plugin) => plugin.isPlanable && !plugin.isPlanable(node, planable), (plugin) => plugin.isPlanable && !plugin.isPlanable(node, planable),
); );
return !hasNonPlanable; return !hasNonPlanable;

View File

@@ -1,5 +1,5 @@
export type { GraphNode } from './types/node'; export type { GraphNode } from './types/node';
export type { Planable } from './types/planable'; export type { Planable } from './types/planable';
export { expandNode } from './algorithm/expand-node'; 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'; export { plugins } from './plugins/index';

View File

@@ -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<TAttribuesSchema>, Static<TContextSchema>>,
'attributes' | 'context'
>,
): Plugin<Static<TAttribuesSchema>, Static<TContextSchema>> => ({
...plugin,
attributes,
context,
});
export { createPlugin };

View File

@@ -1,9 +1,8 @@
import { Attributes } from '../types/node';
import { Plugin } from '../types/plugin'; import { Plugin } from '../types/plugin';
import { transport } from './transport'; import { transport } from './transport';
const plugins = { const plugins = {
transport, transport,
} satisfies Record<string, (...args: any[]) => Plugin<Attributes>>; } satisfies Record<string, (...args: any[]) => Plugin>;
export { plugins }; export { plugins };

View File

@@ -1,5 +1,5 @@
import { GraphNode } from '../types/node'; import { Type } from '@sinclair/typebox';
import { Plugin } from '../types/plugin'; import { createPlugin } from './create';
type GetTravelTime = (from: string, to: string) => Promise<number>; type GetTravelTime = (from: string, to: string) => Promise<number>;
@@ -7,13 +7,15 @@ type TransportOptions = {
getTravelTime: GetTravelTime; getTravelTime: GetTravelTime;
}; };
type TransportAttributes = { const transport = ({ getTravelTime }: TransportOptions) =>
locations?: string[]; createPlugin(
}; Type.Object({
locations: Type.Optional(Type.Array(Type.String())),
const transport = ({ }),
getTravelTime, Type.Object({
}: TransportOptions): Plugin<TransportAttributes> => ({ location: Type.String(),
}),
{
getMetaNodes: async (node) => { getMetaNodes: async (node) => {
const locations = const locations =
(node.type !== 'travel' && (node.type !== 'travel' &&
@@ -24,28 +26,33 @@ const transport = ({
.flat(), .flat(),
), ),
] ]
.filter((location) => location !== node.location) .filter((location) => location !== node.context.location)
.filter(Boolean) .filter(Boolean)
.map((l) => l!)) || .map((l) => l!)) ||
[]; [];
const travelNodes = await Promise.all( const travelNodes = await Promise.all(
locations.map<Promise<GraphNode<TransportAttributes>>>( locations.map(async (location) => {
async (location) => { const travelTime = await getTravelTime(
const travelTime = await getTravelTime(node.location, location); node.context.location,
location,
);
return { return {
...node, ...node,
type: 'travel', type: 'travel' as const,
context: {
...node.context,
location,
},
planable: undefined, planable: undefined,
location, location,
exploreId: 0, exploreId: 0,
score: node.score - 20, score: node.score - 10,
time: node.time + node.duration, time: node.time + node.duration,
duration: travelTime, duration: travelTime,
parent: node.id, parent: node.id,
}; };
}, }),
),
); );
return travelNodes; return travelNodes;
@@ -53,12 +60,13 @@ const transport = ({
isPlanable: (node, planable) => { isPlanable: (node, planable) => {
if ( if (
planable.attributes?.locations && planable.attributes?.locations &&
!planable.attributes?.locations.includes(node.location) !planable.attributes?.locations.includes(node.context.location)
) { ) {
return false; return false;
} }
return true; return true;
}, },
}); },
);
export { transport }; export { transport };

View File

@@ -2,11 +2,14 @@ import type { Planable } from './planable';
type Attributes = any; type Attributes = any;
type GraphNode<TAttributes extends Attributes = Attributes> = { type GraphNode<
TAttributes extends Attributes = Attributes,
TContext extends Attributes = Attributes,
> = {
id: string; id: string;
type: 'root' | 'planable' | 'travel'; type: 'root' | 'planable' | 'travel';
score: number; score: number;
location: string; context: TContext;
parent: string | null; parent: string | null;
time: number; time: number;
exploreId: number; exploreId: number;

View File

@@ -11,7 +11,6 @@ type Planable<TAttributes extends Attributes = Attributes> = {
}; };
attributes: TAttributes; attributes: TAttributes;
required?: boolean; required?: boolean;
locations?: string[];
}; };
export type { Planable }; export type { Planable };

View File

@@ -1,25 +1,31 @@
import { Attributes, GraphNode } from './node'; import { GraphNode } from './node';
import { Planable } from './planable'; import { Planable } from './planable';
type Plugin<TAttributes extends Attributes = Attributes> = { type Plugin<TAttributes = any, TContext = any> = {
context: any;
attributes: any;
getMetaNodes?: ( getMetaNodes?: (
node: GraphNode<TAttributes>, node: GraphNode<TAttributes, TContext>,
) => Promise<GraphNode<TAttributes>[]>; ) => Promise<GraphNode<TAttributes, TContext>[]>;
isImpossible?: (node: GraphNode<TAttributes>) => Promise<boolean>; isImpossible?: (node: GraphNode<TAttributes, TContext>) => Promise<boolean>;
isPlanable?: ( isPlanable?: (
node: GraphNode<TAttributes>, node: GraphNode<TAttributes, TContext>,
planable: Planable<TAttributes>, planable: Planable<TAttributes>,
) => boolean; ) => boolean;
}; };
type Plugins = Plugin[]; type Plugins = Record<string, Plugin>;
type PluginAttributes<TPlugins extends Plugins> = { type PluginAttributes<TPlugins extends Plugins> = {
[K in keyof TPlugins]: TPlugins[K] extends Plugin<infer TAttributes> [K in keyof TPlugins]: TPlugins[K] extends Plugin<infer TAttributes, any>
? TAttributes extends Attributes
? TAttributes ? TAttributes
: never
: never; : never;
}[number]; }[keyof TPlugins];
export type { Plugin, Plugins, PluginAttributes }; type PluginContext<TPlugins extends Plugins> = {
[K in keyof TPlugins]: TPlugins[K] extends Plugin<any, infer TContext>
? TContext
: never;
}[keyof TPlugins];
export type { Plugin, Plugins, PluginAttributes, PluginContext };

View File

@@ -1,8 +1,12 @@
{ {
"extends": "./tsconfig.base.json", "extends": "./tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"lib": ["ES2022", "DOM"], "lib": [
"ES2022",
"DOM"
],
"target": "ES2022", "target": "ES2022",
"module": "ESNext" "module": "ES2022",
"moduleResolution": "node"
} }
} }

View File

@@ -1,8 +1,7 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title> <title>Vite + React + TS</title>
</head> </head>

View File

@@ -14,7 +14,7 @@ type BlockProps = {
presenter?: React.FC<any>; presenter?: React.FC<any>;
}; };
const id = (function* () { const id = (function*() {
let i = 0; let i = 0;
while (true) { while (true) {
yield i++; yield i++;
@@ -57,6 +57,7 @@ const Block: React.FC<BlockProps> = ({
}); });
} catch (error) { } catch (error) {
setError(error); setError(error);
console.error(error);
} }
setRunning(false); setRunning(false);
@@ -75,6 +76,7 @@ const Block: React.FC<BlockProps> = ({
} }
if (type === 'error') { if (type === 'error') {
setError(payload); setError(payload);
console.error(payload);
} }
}; };
worker.addEventListener('message', listener); worker.addEventListener('message', listener);

View File

@@ -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 { createWorker } from '../../features/runner/worker';
import { convertResult } from '../../utils/graph'; import { convertResult } from '../../utils/graph';
@@ -6,17 +6,21 @@ const MIN = 1000 * 60;
const HOUR = 1000 * 60 * 60; const HOUR = 1000 * 60 * 60;
const getTravelTime = async () => 30 * MIN; const getTravelTime = async () => 30 * MIN;
const transport = plugins.transport({
getTravelTime,
});
const realistic = async () => { const realistic = async () => {
const result = await calulation({ try {
const bob = new Bob({
plugins: { transport },
});
const result = await bob.run({
context: {
location: 'home', location: 'home',
time: 0, },
start: 0,
heuristic: ({ completed }) => completed.length >= 3, heuristic: ({ completed }) => completed.length >= 3,
plugins: [
plugins.transport({
getTravelTime,
}),
],
planables: [ planables: [
{ {
id: `Brush teeth`, id: `Brush teeth`,
@@ -141,6 +145,10 @@ const realistic = async () => {
], ],
}); });
return convertResult(result); return convertResult(result);
} catch (e) {
console.error(e);
throw e;
}
}; };
createWorker({ createWorker({

View File

@@ -1,3 +1,4 @@
import './script.ts';
const worker = new Worker(new URL('./script.ts', import.meta.url), { const worker = new Worker(new URL('./script.ts', import.meta.url), {
type: 'module', type: 'module',
}); });

8
pnpm-lock.yaml generated
View File

@@ -31,6 +31,10 @@ importers:
version: 20.5.9 version: 20.5.9
packages/algorithm: packages/algorithm:
dependencies:
'@sinclair/typebox':
specifier: ^0.31.14
version: 0.31.14
devDependencies: devDependencies:
'@bob-the-algorithm/config': '@bob-the-algorithm/config':
specifier: workspace:^ specifier: workspace:^
@@ -1234,6 +1238,10 @@ packages:
rollup: 3.28.1 rollup: 3.28.1
dev: true dev: true
/@sinclair/typebox@0.31.14:
resolution: {integrity: sha512-2spk0ie6J/4r+nwb55OtBXUn5cZLF9S98fopIjuutBVoq8yLRNh+h8QvMkCjMu5gWBMnnZ/PUSXeHE3xGBPKLQ==}
dev: false
/@swc/core-darwin-arm64@1.3.82: /@swc/core-darwin-arm64@1.3.82:
resolution: {integrity: sha512-JfsyDW34gVKD3uE0OUpUqYvAD3yseEaicnFP6pB292THtLJb0IKBBnK50vV/RzEJtc1bR3g1kNfxo2PeurZTrA==} resolution: {integrity: sha512-JfsyDW34gVKD3uE0OUpUqYvAD3yseEaicnFP6pB292THtLJb0IKBBnK50vV/RzEJtc1bR3g1kNfxo2PeurZTrA==}
engines: {node: '>=10'} engines: {node: '>=10'}