Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@temporalio/interceptors-opentelemetry": "file:packages/interceptors-opentelemetry",
"@temporalio/nexus": "file:packages/nexus",
"@temporalio/nyc-test-coverage": "file:packages/nyc-test-coverage",
"@temporalio/plugin": "file:packages/plugin",
"@temporalio/proto": "file:packages/proto",
"@temporalio/test": "file:packages/test",
"@temporalio/testing": "file:packages/testing",
Expand Down Expand Up @@ -90,6 +91,7 @@
"packages/interceptors-opentelemetry",
"packages/nexus",
"packages/nyc-test-coverage",
"packages/plugin",
"packages/proto",
"packages/test",
"packages/testing",
Expand Down
25 changes: 24 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 { ClientPlugin } 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?: ClientPlugin[];

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,21 @@ 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 ?? []);

// Process plugins first to allow them to modify connect configuration
for (const plugin of options?.plugins ?? []) {
if (plugin.configureClient !== undefined) {
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 +117,7 @@ export class Client extends BaseClient {
workflow: {
queryRejectCondition: this.workflow.options.queryRejectCondition,
},
plugins: plugins ?? [],
};
}

Expand Down
42 changes: 38 additions & 4 deletions packages/client/src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ export interface ConnectionOptions {
* @default 10 seconds
*/
connectTimeout?: Duration;

/**
* List of plugins to register with the connection.
*
* Plugins allow you to configure the connection options.
*
* Any plugins provided will also be passed to any client built from this connection.
*/
plugins?: ConnectionPlugin[];
}

export type ConnectionOptionsWithDefaults = Required<
Expand Down Expand Up @@ -172,6 +181,7 @@ function addDefaults(options: ConnectionOptions): ConnectionOptionsWithDefaults
interceptors: interceptors ?? [makeGrpcRetryInterceptor(defaultGrpcRetryOptions())],
metadata: {},
connectTimeoutMs: msOptionalToNumber(connectTimeout) ?? 10_000,
plugins: [],
...filterNullAndUndefined(rest),
};
}
Expand All @@ -182,8 +192,8 @@ function addDefaults(options: ConnectionOptions): ConnectionOptionsWithDefaults
* - Add default port to address if port not specified
* - Set `Authorization` header based on {@link ConnectionOptions.apiKey}
*/
function normalizeGRPCConfig(options?: ConnectionOptions): ConnectionOptions {
const { tls: tlsFromConfig, credentials, callCredentials, ...rest } = options || {};
function normalizeGRPCConfig(options: ConnectionOptions): ConnectionOptions {
const { tls: tlsFromConfig, credentials, callCredentials, ...rest } = options;
if (rest.apiKey) {
if (rest.metadata?.['Authorization']) {
throw new TypeError(
Expand Down Expand Up @@ -322,10 +332,12 @@ export class Connection {
*/
public readonly healthService: HealthService;

public readonly plugins: ConnectionPlugin[];

readonly callContextStorage: AsyncLocalStorage<CallContext>;
private readonly apiKeyFnRef: { fn?: () => string };

protected static createCtorOptions(options?: ConnectionOptions): ConnectionCtorOptions {
protected static createCtorOptions(options: ConnectionOptions): ConnectionCtorOptions {
const normalizedOptions = normalizeGRPCConfig(options);
const apiKeyFnRef: { fn?: () => string } = {};
if (normalizedOptions.apiKey) {
Expand Down Expand Up @@ -441,6 +453,12 @@ export class Connection {
* This method does not verify connectivity with the server. We recommend using {@link connect} instead.
*/
static lazy(options?: ConnectionOptions): Connection {
options = options ?? {};
for (const plugin of options.plugins ?? []) {
if (plugin.configureConnection !== undefined) {
options = plugin.configureConnection(options);
}
}
return new this(this.createCtorOptions(options));
}

Expand Down Expand Up @@ -474,6 +492,7 @@ export class Connection {
this.healthService = healthService;
this.callContextStorage = callContextStorage;
this.apiKeyFnRef = apiKeyFnRef;
this.plugins = options.plugins ?? [];
}

protected static generateRPCImplementation({
Expand Down Expand Up @@ -529,7 +548,7 @@ export class Connection {
* this will locally result in the request call throwing a {@link grpc.ServiceError|ServiceError}
* with code {@link grpc.status.DEADLINE_EXCEEDED|DEADLINE_EXCEEDED}; see {@link isGrpcDeadlineError}.
*
* It is stronly recommended to explicitly set deadlines. If no deadline is set, then it is
* It is strongly recommended to explicitly set deadlines. If no deadline is set, then it is
* possible for the client to end up waiting forever for a response.
*
* @param deadline a point in time after which the request will be considered as failed; either a
Expand Down Expand Up @@ -685,3 +704,18 @@ export class Connection {
return wrapper as WorkflowService;
}
}

/**
* Plugin to control the configuration of a connection.
*/
export interface ConnectionPlugin {
/**
* Gets the name of this plugin.
*/
get name(): string;

/**
* Hook called when creating a connection to allow modification of configuration.
*/
configureConnection?(options: ConnectionOptions): ConnectionOptions;
}
9 changes: 8 additions & 1 deletion packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,19 @@ export * from '@temporalio/common/lib/interfaces';
export * from '@temporalio/common/lib/workflow-handle';
export * from './async-completion-client';
export * from './client';
export { Connection, ConnectionOptions, ConnectionOptionsWithDefaults, LOCAL_TARGET } from './connection';
export {
Connection,
ConnectionOptions,
ConnectionOptionsWithDefaults,
ConnectionPlugin,
LOCAL_TARGET,
} from './connection';
export * from './errors';
export * from './grpc-retry';
export * from './interceptors';
export * from './types';
export * from './workflow-client';
export { ClientPlugin } from './plugin';
export * from './workflow-options';
export * from './schedule-types';
export * from './schedule-client';
Expand Down
19 changes: 19 additions & 0 deletions packages/client/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { ClientOptions } from './client';

/**
* Plugin to control the configuration of a native connection.
*/
export interface ClientPlugin {
/**
* Gets the name of this plugin.
*/
get name(): string;

/**
* Hook called when creating a client to allow modification of configuration.
*
* This method is called during client creation and allows plugins to modify
* the client configuration before the client is fully initialized.
*/
configureClient?(options: ClientOptions): ClientOptions;
}
1 change: 1 addition & 0 deletions packages/client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export interface CallContext {
*/
export interface ConnectionLike {
workflowService: WorkflowService;
plugins: any[];
close(): Promise<void>;
ensureConnected(): Promise<void>;

Expand Down
35 changes: 35 additions & 0 deletions packages/plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "@temporalio/plugin",
"version": "1.13.0",
"description": "Library for plugin creation",
"main": "lib/index.js",
"types": "./lib/index.d.ts",
"keywords": [
"temporal",
"workflow",
"worker",
"plugin"
],
"author": "Temporal Technologies Inc. <sdk@temporal.io>",
"license": "MIT",
"dependencies": {},
"bugs": {
"url": "https://github.com/temporalio/sdk-typescript/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/temporalio/sdk-typescript.git",
"directory": "packages/plugin"
},
"homepage": "https://github.com/temporalio/sdk-typescript/tree/main/packages/plugin",
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">= 18.0.0"
},
"files": [
"src",
"lib"
]
}
1 change: 1 addition & 0 deletions packages/plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SimplePlugin, SimplePluginOptions } from './plugin';
Loading
Loading