Skip to content

Commit 3d50d78

Browse files
committed
Refactored plugin class design
1 parent a533e53 commit 3d50d78

6 files changed

Lines changed: 88 additions & 84 deletions

File tree

codify-core/src/commands/plan/orchestrator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ export const PlanOrchestrator = {
1111
const resourceDefinitions = await pluginCollection.getAllResourceDefinitions();
1212
console.log(resourceDefinitions);
1313

14-
await pluginCollection.killPlugins();
14+
await pluginCollection.destroy();
1515
},
1616
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ResourceDefinition } from '../../entities/resource-definition';
2+
import { RemoveMethods } from '../../utils/types';
3+
import { validateTypeArray, validateTypeRecordStringUnknown } from '../../utils/validator';
4+
5+
type ResourceName = string;
6+
7+
export class PluginData {
8+
9+
// Plugin names should be globally unique
10+
name!: string;
11+
directory!: string;
12+
resourceDefinitions!: Map<ResourceName, ResourceDefinition>;
13+
14+
constructor(props: RemoveMethods<PluginData>) {
15+
Object.assign(this, props);
16+
}
17+
18+
static create(name: string, directory: string, resourceDefinitions: unknown): PluginData {
19+
if (this.validate(resourceDefinitions)) {
20+
const entries = resourceDefinitions.map((u) => {
21+
const resourceDefinition = ResourceDefinition.fromJson(u);
22+
23+
// Append the plugin name to all resources to prevent conflicts across plugins
24+
return [`${this.name}_${resourceDefinition.name}`, resourceDefinition] as const;
25+
})
26+
27+
return new PluginData({ directory, name, resourceDefinitions: new Map(entries) });
28+
}
29+
30+
throw new Error('Unable to parse plugin definition')
31+
}
32+
33+
34+
private static validate(resourceDefinitions: unknown): resourceDefinitions is Array<Record<string, unknown>> {
35+
if (!validateTypeArray(resourceDefinitions)) {
36+
throw new Error('Resource definitions is not of type array')
37+
}
38+
39+
if (!resourceDefinitions.every((u) => validateTypeRecordStringUnknown(u))) {
40+
throw new Error('Type definitions is not of Record<string, unknown>')
41+
}
42+
43+
return true;
44+
}
45+
46+
}
Lines changed: 15 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,30 @@
11
import type { ChildProcess } from 'node:child_process';
22

3-
import { ResourceDefinition } from '../../entities/resource-definition';
4-
import { validateTypeArray, validateTypeRecordStringUnknown } from '../../utils/validator';
5-
6-
type ResourceName = string;
3+
import { PluginIpcBridge } from '../ipc-bridge';
4+
import { PluginData } from './plugin-data';
75

86
export class Plugin {
97

10-
// Plugin names should be globally unique
11-
name: string;
12-
directory: string;
13-
148
process: ChildProcess;
15-
resourceDefinitions?: Map<ResourceName, ResourceDefinition>;
9+
ipcBridge: PluginIpcBridge;
1610

17-
constructor(directory: string, name: string, process: ChildProcess) {
18-
this.name = name;
19-
this.directory = directory;
11+
// Separate out data so that the validation logic is testable.
12+
data: PluginData;
13+
14+
constructor(data: PluginData, process: ChildProcess, ipcBridge: PluginIpcBridge = new PluginIpcBridge()) {
2015
this.process = process;
16+
this.ipcBridge = ipcBridge;
17+
this.data = data;
2118
}
2219

23-
setResourceDefinitions(definitions: unknown): void {
24-
if (this.validate(definitions)) {
25-
const entries = definitions.map((u) => {
26-
const resourceDefinition = ResourceDefinition.fromJson(u);
27-
28-
// Append the plugin name to all resources to prevent conflicts across plugins
29-
return [`${this.name}_${resourceDefinition.name}`, resourceDefinition] as const;
30-
})
20+
static async create(directory: string, name: string, ipcBridge: PluginIpcBridge = new PluginIpcBridge()): Promise<Plugin> {
21+
const process = await ipcBridge.createProcess(directory, name);
22+
const resourceDefinitions = await ipcBridge.sendMessageForResult(process, { cmd: 'getResourceDefinitions' });
3123

32-
this.resourceDefinitions = new Map(entries);
33-
return;
34-
}
35-
36-
throw new Error('Unable to parse resource definition');
24+
return new Plugin(PluginData.create(directory, name, resourceDefinitions), process);
3725
}
3826

39-
private validate(definitions: unknown): definitions is Array<Record<string, unknown>> {
40-
if (!validateTypeArray(definitions)) {
41-
throw new Error('Definitions is not of type array')
42-
}
43-
44-
if (!definitions.every((u) => validateTypeRecordStringUnknown(u))) {
45-
throw new Error('Type definitions is not of Record<string, unknown>')
46-
}
47-
48-
return true;
27+
destroy() {
28+
this.ipcBridge.killPlugin(this.process);
4929
}
5030
}

codify-core/src/plugins/ipc-bridge.ts

Lines changed: 17 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,61 @@
1-
import * as ChildProcess from 'node:child_process';
1+
import { ChildProcess, fork } from 'node:child_process';
22

33
import { config } from '../project-configs';
44
import { validateTypeRecordStringUnknown } from '../utils/validator';
55
import { PluginMessage } from './entities/message';
6-
import { Plugin } from './entities/plugin';
76

87
export class PluginIpcBridge {
98

10-
static async initializePlugin(directory: string, name: string): Promise<Plugin> {
11-
const childProcess = ChildProcess.fork(
9+
async createProcess(directory: string, name: string): Promise<ChildProcess> {
10+
return fork(
1211
directory + '/' + name + config.defaultPluginEntryPoint,
1312
[],
1413
{ execArgv: ['-r', 'ts-node/register'], silent: true },
1514
);
16-
const plugin = new Plugin(directory, name, childProcess);
17-
18-
childProcess.stdout!.on('data', (data) => {
19-
console.log(data.toString());
20-
});
21-
22-
childProcess.stderr!.on('data', (data) => {
23-
console.log(data.toString());
24-
})
25-
26-
await this.initializeResourceDefinitions(plugin);
27-
28-
return plugin;
29-
}
30-
31-
static async initializeResourceDefinitions(plugin: Plugin): Promise<void> {
32-
const resourceDefinitions = await this.sendMessageForResult(plugin, { cmd: 'getResourceDefinitions' });
33-
plugin.setResourceDefinitions(resourceDefinitions);
3415
}
3516

36-
static killPlugin(plugin: Plugin): void {
37-
plugin.process.kill();
17+
killPlugin(process: ChildProcess): void {
18+
process.kill();
3819
}
3920

40-
private static async sendMessageForResult(plugin: Plugin, message: PluginMessage): Promise<unknown> {
21+
async sendMessageForResult(process: ChildProcess, message: PluginMessage): Promise<unknown> {
4122
return new Promise((resolve, reject) => {
4223
const timer = setTimeout(() => {
43-
plugin.process.kill();
24+
process.kill();
4425
reject(new Error(`Plugin did not respond in 10s to call: ${message.cmd}`))
4526
}, 10_000);
4627

4728
const errorListener = (error: Buffer) => {
48-
plugin.process.kill();
29+
process.kill();
4930
reject(error.toString());
5031
}
5132

5233
const messageListener = (incomingMessage: unknown) => {
5334
console.log(incomingMessage);
5435

5536
if (!validateTypeRecordStringUnknown(incomingMessage)) {
56-
return reject(new Error(`Bad message from plugin ${plugin.name}. ${JSON.stringify(incomingMessage, null, 2)}`))
37+
return reject(new Error(`Bad message from plugin ${name}. ${JSON.stringify(incomingMessage, null, 2)}`))
5738
}
5839

5940
if (incomingMessage.cmd === this.getResultFunctionName(message.cmd)) {
6041
clearTimeout(timer);
61-
plugin.process.removeListener('message', messageListener);
62-
plugin.process.removeListener('error', errorListener);
42+
process.removeListener('message', messageListener);
43+
process.removeListener('error', errorListener);
6344
resolve(incomingMessage.data);
6445
}
6546
};
6647

67-
plugin.process.on('message', messageListener);
68-
plugin.process.stderr!.on('data', errorListener);
69-
plugin.process.send(message);
48+
process.on('message', messageListener);
49+
process.stderr!.on('data', errorListener);
50+
process.send(message);
7051
});
7152
}
7253

73-
private sendMessage(plugin: Plugin, message: PluginMessage): void {
74-
plugin.process.send(message);
54+
sendMessage(process: ChildProcess, message: PluginMessage): void {
55+
process.send(message);
7556
}
7657

77-
private static getResultFunctionName(rpcFunctionName: string): string {
58+
private getResultFunctionName(rpcFunctionName: string): string {
7859
return rpcFunctionName + 'Result';
7960
}
8061

codify-core/src/plugins/plugin-collection.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { ParsedProject } from '../config-compiler/parser/entities';
22
import { ResourceDefinition } from '../entities/resource-definition';
33
import { Plugin } from './entities/plugin';
4-
import { PluginIpcBridge } from './ipc-bridge';
54
import { PluginResolver } from './resolver';
65

76
type PluginName = string;
@@ -21,20 +20,19 @@ export class PluginCollection {
2120
...project.projectConfig.plugins,
2221
};
2322

24-
const pluginResolver = new PluginResolver();
2523
const plugins = await Promise.all(Object.entries(pluginDefinitions).map(([name, version]) =>
26-
pluginResolver.resolve(name, version)
24+
PluginResolver.resolve(name, version)
2725
));
2826

29-
for (const u of plugins) {
30-
this.plugins.set(u.name, u);
27+
for (const plugin of plugins) {
28+
this.plugins.set(plugin.data.name, plugin);
3129
}
3230
}
3331

3432
async getAllResourceDefinitions(): Promise<Map<string, ResourceDefinition>> {
3533
const result = new Map<string, ResourceDefinition>();
3634
for (const plugin of this.plugins.values()) {
37-
const { resourceDefinitions } = plugin;
35+
const { resourceDefinitions } = plugin.data;
3836
if (!resourceDefinitions) {
3937
continue;
4038
}
@@ -54,9 +52,9 @@ export class PluginCollection {
5452
return result;
5553
}
5654

57-
async killPlugins(): Promise<void> {
55+
async destroy(): Promise<void> {
5856
for (const plugin of this.plugins.values()) {
59-
PluginIpcBridge.killPlugin(plugin);
57+
plugin.destroy();
6058
}
6159
}
6260

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import * as fs from 'node:fs/promises';
22

33
import { Plugin } from './entities/plugin';
4-
import { PluginIpcBridge } from './ipc-bridge';
54

65
const DEFAULT_PLUGIN_REGEX = /(?<=default:).*$/g
76

87
export class PluginResolver {
98

10-
async resolve(name: string, version: string): Promise<Plugin> {
9+
static async resolve(name: string, version: string): Promise<Plugin> {
1110

1211
if (DEFAULT_PLUGIN_REGEX.test(name)) {
1312
return this.resolveDefaultPlugin(name, version)
@@ -17,7 +16,7 @@ export class PluginResolver {
1716
}
1817

1918
// TODO: update this method to resolve default plugins from github in the future.
20-
private async resolveDefaultPlugin(name: string, _version: string): Promise<Plugin> {
19+
private static async resolveDefaultPlugin(name: string, _version: string): Promise<Plugin> {
2120
const pluginName = name.match(DEFAULT_PLUGIN_REGEX)![0]
2221

2322
const defaultPluginDir = '../plugins';
@@ -26,7 +25,7 @@ export class PluginResolver {
2625
throw new Error(`Unable to find default plugin: ${name}`)
2726
}
2827

29-
return PluginIpcBridge.initializePlugin(defaultPluginDir, pluginName);
28+
return Plugin.create(defaultPluginDir, pluginName);
3029
}
3130

3231
}

0 commit comments

Comments
 (0)