Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
323 changes: 323 additions & 0 deletions PLUGIN_FEATURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
# Temporal TypeScript SDK Plugin Support

This implements a Plugin system similar to the Python SDK's Plugin feature, allowing you to extend and customize the behavior of Temporal clients and workers through a chain of responsibility pattern.

## Overview

Plugins provide a way to intercept and modify:
- Client creation and configuration
- Service connections
- Worker configuration and execution
- Activities, workflows, and interceptors

## Architecture

The plugin system uses a **chain of responsibility pattern** where each plugin can:
1. Modify configuration
2. Pass control to the next plugin in the chain
3. Perform custom logic before/after delegation

## Client Plugin Support

### ClientOptions Extension

```typescript
export interface ClientOptions extends BaseClientOptions {
// ... existing options ...

/**
* List of plugins to register with the client.
*/
plugins?: Plugin[];
}
```

### Plugin Base Class

```typescript
export abstract class Plugin {
/**
* Gets the fully qualified name of this plugin.
*/
get name(): string;

/**
* Initialize this plugin in the plugin chain.
*/
initClientPlugin(next: Plugin): Plugin;

/**
* Hook called when creating a client to allow modification of configuration.
*/
configureClient(config: ClientOptions): ClientOptions;
}
```

## Worker Plugin Support

### WorkerOptions Extension

```typescript
export interface WorkerOptions {
// ... existing options ...

/**
* List of plugins to register with the worker.
*/
plugins?: Plugin[];
}
```

### Worker Plugin Methods

```typescript
export abstract class Plugin extends ClientPlugin {
/**
* Initialize this plugin in the worker plugin chain.
*/
initWorkerPlugin(next: Plugin): Plugin;

/**
* Hook called when creating a worker to allow modification of configuration.
*/
configureWorker(config: WorkerOptions): WorkerOptions;
}
```

## Usage Examples

### Basic Client Plugin

```typescript
import { Plugin, Client } from '@temporalio/client';

class CustomClientPlugin extends Plugin {
configureClient(config: ClientOptions): ClientOptions {
// Add custom metadata
console.log('Configuring client with custom settings');

// Modify configuration
const modifiedConfig = {
...config,
// Add custom properties
};

// Chain to next plugin
return super.configureClient(modifiedConfig);
}
}

// Use with client
const client = new Client({
plugins: [new CustomClientPlugin()],
namespace: 'default',
});
```

### Basic Worker Plugin

```typescript
import { Plugin, Worker } from '@temporalio/worker';

class CustomWorkerPlugin extends Plugin {
configureWorker(config: WorkerOptions): WorkerOptions {
// Modify task queue name
const taskQueue = config.taskQueue ? `custom-${config.taskQueue}` : config.taskQueue;

const modifiedConfig = {
...config,
taskQueue,
identity: `${config.identity}-with-plugin`,
};

console.log(`Modified task queue to: ${taskQueue}`);
return super.configureWorker(modifiedConfig);
}
}

// Use with worker
const worker = await Worker.create({
plugins: [new CustomWorkerPlugin()],
taskQueue: 'my-task-queue',
workflowsPath: './workflows',
});
```

### Activity Plugin Example

```typescript
class ActivityPlugin extends Plugin {
private activities: Record<string, Function>;

constructor(activities: Record<string, Function>) {
super();
this.activities = activities;
}

configureWorker(config: WorkerConfig): WorkerConfig {
// Merge custom activities with existing ones
const existingActivities = config.activities || {};
const mergedActivities = {
...existingActivities,
...this.activities,
};

return super.configureWorker({
...config,
activities: mergedActivities,
});
}
}

// Custom activities
const customActivities = {
async logMessage(message: string): Promise<void> {
console.log(`Custom activity: ${message}`);
},

async processData(data: any): Promise<any> {
return { processed: true, data };
},
};

// Use the plugin
const worker = await Worker.create({
plugins: [new ActivityPlugin(customActivities)],
taskQueue: 'my-task-queue',
workflowsPath: './workflows',
});
```

### Multiple Plugin Chain

```typescript
class LoggingPlugin extends Plugin {
configureClient(config: ClientOptions): ClientOptions {
console.log('LoggingPlugin: Client configuration');
return super.configureClient(config);
}

configureWorker(config: WorkerOptions): WorkerOptions {
console.log('LoggingPlugin: Worker configuration');
return super.configureWorker(config);
}
}

class MetricsPlugin extends Plugin {
configureClient(config: ClientOptions): ClientOptions {
console.log('MetricsPlugin: Adding metrics interceptors');
// Add metrics interceptors
return super.configureClient(config);
}
}

// Chain multiple plugins
const client = new Client({
plugins: [
new LoggingPlugin(),
new MetricsPlugin(),
new CustomClientPlugin(),
],
namespace: 'default',
});
```

## Implementation Details

### Plugin Chain Building

The `buildPluginChain()` function creates a chain of responsibility:

```typescript
export function buildPluginChain(plugins: Plugin[]): Plugin {
if (plugins.length === 0) {
return new _RootPlugin();
}

// Start with the root plugin at the end
let chain: Plugin = new _RootPlugin();

// Build the chain in reverse order
for (let i = plugins.length - 1; i >= 0; i--) {
const plugin = plugins[i];
plugin.initClientPlugin(chain);
chain = plugin;
}

return chain;
}
```

### Client Integration

The Client constructor processes plugins before initialization:

```typescript
constructor(options?: ClientOptions) {
// Process plugins first to allow them to modify configuration
const processedOptions = Client.applyPlugins(options);

super(processedOptions);
// ... rest of constructor
}

private static applyPlugins(options?: ClientOptions): ClientOptions {
if (!options?.plugins?.length) {
return options ?? {};
}

const pluginChain = buildPluginChain(options.plugins);
const clientConfig: ClientOptions = { ...options };
const processedConfig = pluginChain.configureClient(clientConfig);

return { ...processedConfig };
}
```

### Worker Integration

Similarly, the Worker.create() method would process plugins:

```typescript
public static async create(options: WorkerOptions): Promise<Worker> {
// Apply plugins to modify configuration
const processedOptions = Worker.applyPlugins(options);

// ... rest of worker creation with processed options
}
```

## Files Modified/Added

### Client Package (`packages/client/`)
- **NEW**: `src/plugin.ts` - Base Plugin class and client plugin support
- **MODIFIED**: `src/client.ts` - Added plugins field to ClientOptions and plugin processing
- **MODIFIED**: `src/index.ts` - Export Plugin and related types

### Worker Package (`packages/worker/`)
- **NEW**: `src/plugin.ts` - Worker plugin extension and chain building
- **MODIFIED**: `src/worker-options.ts` - Added plugins field to WorkerOptions
- **MODIFIED**: `src/index.ts` - Export worker Plugin and related types

### Test Package (`packages/test/`)
- **NEW**: `src/example-plugin.ts` - Comprehensive examples and usage patterns

## Benefits

1. **Extensibility**: Easily extend client and worker functionality without modifying core SDK
2. **Composability**: Chain multiple plugins together for complex customizations
3. **Consistency**: Similar pattern to Python SDK for cross-language familiarity
4. **Separation of Concerns**: Keep custom logic separate from core application code
5. **Reusability**: Plugins can be shared across projects and teams

## Common Use Cases

- **Authentication**: Add custom auth headers or credentials
- **Observability**: Inject custom metrics, logging, or tracing
- **Data Transformation**: Custom data converters or payload codecs
- **Environment Configuration**: Different settings per environment
- **Activity/Workflow Registration**: Dynamically add activities or workflows
- **Connection Customization**: Modify connection parameters or retry policies
- **Namespace Management**: Automatic namespace prefixing or routing

This plugin system provides a powerful, flexible way to customize Temporal SDK behavior while maintaining clean separation of concerns and enabling code reuse across projects.
24 changes: 23 additions & 1 deletion packages/client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ScheduleClient } from './schedule-client';
import { QueryRejectCondition, WorkflowService } from './types';
import { WorkflowClient } from './workflow-client';
import { TaskQueueClient } from './task-queue-client';
import { isClientPlugin, Plugin } from './plugin';

export interface ClientOptions extends BaseClientOptions {
/**
Expand All @@ -15,6 +16,14 @@ export interface ClientOptions extends BaseClientOptions {
*/
interceptors?: ClientInterceptors;

/**
* List of plugins to register with the client.
*
* Plugins allow you to extend and customize the behavior of Temporal clients through a chain of
* responsibility pattern. They can intercept and modify client creation.
*/
plugins?: Plugin[];

workflow?: {
/**
* Should a query be rejected by closed and failed workflows
Expand All @@ -32,6 +41,7 @@ export type LoadedClientOptions = LoadedWithDefaults<ClientOptions>;
*/
export class Client extends BaseClient {
public readonly options: LoadedClientOptions;

/**
* Workflow sub-client - use to start and interact with Workflows
*/
Expand All @@ -52,9 +62,20 @@ export class Client extends BaseClient {
public readonly taskQueue: TaskQueueClient;

constructor(options?: ClientOptions) {
options = options ?? {};

// Add client plugins from the connection
options.plugins = (options.plugins || []).concat(
(options.connection?.plugins || []).filter(p => isClientPlugin(p)).map(p => p as Plugin));

// Process plugins first to allow them to modify connect configuration
for (const plugin of options?.plugins ?? []) {
options = plugin.configureClient(options)
}

super(options);

const { interceptors, workflow, ...commonOptions } = options ?? {};
const { interceptors, workflow, plugins, ...commonOptions } = options;

this.workflow = new WorkflowClient({
...commonOptions,
Expand Down Expand Up @@ -95,6 +116,7 @@ export class Client extends BaseClient {
workflow: {
queryRejectCondition: this.workflow.options.queryRejectCondition,
},
plugins: plugins ?? [],
};
}

Expand Down
Loading
Loading