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,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<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;
const idGen = () => {
let id = 0;
return () => {
id += 1;
return id.toString();
};
};
const calulation = async <TPlugins extends Plugins>({
location,
time,
planables,
plugins,
heuristic,
onUpdated,
}: CalulationOptions<TPlugins>): Promise<
CalulationResult<PluginAttributes<TPlugins>>
> => {
const generateId = idGen();
let exploreId = 1;
const root: GraphNode<PluginAttributes<TPlugins>> = {
id: generateId(),
type: 'root',
score: 0,
parent: null,
duration: 0,
time,
location,
exploreId: 0,
remaining: planables,
};
const nodes: GraphNode<PluginAttributes<TPlugins>>[] = [root];
const leafNodes: GraphNode<PluginAttributes<TPlugins>>[] = [root];
const completed: GraphNode<PluginAttributes<TPlugins>>[] = [];
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<TPlugins>) {
this.#options = options;
}
return { root, nodes, completed, planables };
};
#getNextId = (): string => {
return (this.#id++).toString();
};
public run = async ({
planables,
start,
context,
heuristic,
}: RunOptions<TPlugins>): Promise<CalulationResult<TPlugins>> => {
const { plugins } = this.#options;
let exploreId = 1;
const root: GraphNode<
PluginAttributes<TPlugins>,
PluginContext<TPlugins>
> = {
id: this.#getNextId(),
context,
type: 'root',
score: 0,
parent: null,
duration: 0,
time: start,
exploreId: 0,
remaining: planables,
};
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));
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 };

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,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<TransportAttributes> => ({
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<Promise<GraphNode<TransportAttributes>>>(
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 };

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 };