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

View File

@@ -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<TAttributes extends Attributes> = {
node: GraphNode<TAttributes>;
type ExpandOptions<
TAttributes extends Attributes,
TContext extends Attributes,
> = {
node: GraphNode<TAttributes, TContext>;
generateId: () => string;
plugins: Plugin[];
plugins: Plugins;
};
const expandNode = async <TAttributes extends Attributes>({
const expandNode = async <
TAttributes extends Attributes,
TContext extends Attributes,
>({
node,
generateId,
plugins,
}: ExpandOptions<TAttributes>): Promise<GraphNode<TAttributes>[]> => {
}: ExpandOptions<TAttributes, TContext>): Promise<
GraphNode<TAttributes, TContext>[]
> => {
const isImpossible = hasImpossible({ node });
if (isImpossible) {
@@ -21,7 +29,7 @@ const expandNode = async <TAttributes extends Attributes>({
}
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 <TAttributes extends Attributes>({
);
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;

View File

@@ -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';

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 { transport } from './transport';
const plugins = {
transport,
} satisfies Record<string, (...args: any[]) => Plugin<Attributes>>;
} satisfies Record<string, (...args: any[]) => Plugin>;
export { plugins };

View File

@@ -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<number>;
@@ -7,13 +7,15 @@ type TransportOptions = {
getTravelTime: GetTravelTime;
};
type TransportAttributes = {
locations?: string[];
};
const transport = ({
getTravelTime,
}: TransportOptions): Plugin<TransportAttributes> => ({
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' &&
@@ -24,28 +26,33 @@ const transport = ({
.flat(),
),
]
.filter((location) => location !== node.location)
.filter((location) => location !== node.context.location)
.filter(Boolean)
.map((l) => l!)) ||
[];
const travelNodes = await Promise.all(
locations.map<Promise<GraphNode<TransportAttributes>>>(
async (location) => {
const travelTime = await getTravelTime(node.location, location);
locations.map(async (location) => {
const travelTime = await getTravelTime(
node.context.location,
location,
);
return {
...node,
type: 'travel',
type: 'travel' as const,
context: {
...node.context,
location,
},
planable: undefined,
location,
exploreId: 0,
score: node.score - 20,
score: node.score - 10,
time: node.time + node.duration,
duration: travelTime,
parent: node.id,
};
},
),
}),
);
return travelNodes;
@@ -53,12 +60,13 @@ const transport = ({
isPlanable: (node, planable) => {
if (
planable.attributes?.locations &&
!planable.attributes?.locations.includes(node.location)
!planable.attributes?.locations.includes(node.context.location)
) {
return false;
}
return true;
},
});
},
);
export { transport };

View File

@@ -2,11 +2,14 @@ import type { Planable } from './planable';
type Attributes = any;
type GraphNode<TAttributes extends Attributes = Attributes> = {
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;

View File

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

View File

@@ -1,25 +1,31 @@
import { Attributes, GraphNode } from './node';
import { GraphNode } from './node';
import { Planable } from './planable';
type Plugin<TAttributes extends Attributes = Attributes> = {
type Plugin<TAttributes = any, TContext = any> = {
context: any;
attributes: any;
getMetaNodes?: (
node: GraphNode<TAttributes>,
) => Promise<GraphNode<TAttributes>[]>;
isImpossible?: (node: GraphNode<TAttributes>) => Promise<boolean>;
node: GraphNode<TAttributes, TContext>,
) => Promise<GraphNode<TAttributes, TContext>[]>;
isImpossible?: (node: GraphNode<TAttributes, TContext>) => Promise<boolean>;
isPlanable?: (
node: GraphNode<TAttributes>,
node: GraphNode<TAttributes, TContext>,
planable: Planable<TAttributes>,
) => boolean;
};
type Plugins = Plugin[];
type Plugins = Record<string, Plugin>;
type PluginAttributes<TPlugins extends Plugins> = {
[K in keyof TPlugins]: TPlugins[K] extends Plugin<infer TAttributes>
? TAttributes extends Attributes
[K in keyof TPlugins]: TPlugins[K] extends Plugin<infer TAttributes, any>
? TAttributes
: 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",
"compilerOptions": {
"lib": ["ES2022", "DOM"],
"lib": [
"ES2022",
"DOM"
],
"target": "ES2022",
"module": "ESNext"
"module": "ES2022",
"moduleResolution": "node"
}
}

View File

@@ -1,8 +1,7 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<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" />
<title>Vite + React + TS</title>
</head>

View File

@@ -57,6 +57,7 @@ const Block: React.FC<BlockProps> = ({
});
} catch (error) {
setError(error);
console.error(error);
}
setRunning(false);
@@ -75,6 +76,7 @@ const Block: React.FC<BlockProps> = ({
}
if (type === 'error') {
setError(payload);
console.error(payload);
}
};
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 { convertResult } from '../../utils/graph';
@@ -6,17 +6,21 @@ 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({
try {
const bob = new Bob({
plugins: { transport },
});
const result = await bob.run({
context: {
location: 'home',
time: 0,
},
start: 0,
heuristic: ({ completed }) => completed.length >= 3,
plugins: [
plugins.transport({
getTravelTime,
}),
],
planables: [
{
id: `Brush teeth`,
@@ -141,6 +145,10 @@ const realistic = async () => {
],
});
return convertResult(result);
} catch (e) {
console.error(e);
throw e;
}
};
createWorker({

View File

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

8
pnpm-lock.yaml generated
View File

@@ -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'}