Skip to content

Commit 2e80f91

Browse files
authored
Plugins Implementation (#1794)
1 parent 7cc47fc commit 2e80f91

26 files changed

+924
-23
lines changed

package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"@temporalio/interceptors-opentelemetry": "file:packages/interceptors-opentelemetry",
4747
"@temporalio/nexus": "file:packages/nexus",
4848
"@temporalio/nyc-test-coverage": "file:packages/nyc-test-coverage",
49+
"@temporalio/plugin": "file:packages/plugin",
4950
"@temporalio/proto": "file:packages/proto",
5051
"@temporalio/test": "file:packages/test",
5152
"@temporalio/testing": "file:packages/testing",
@@ -92,6 +93,7 @@
9293
"packages/interceptors-opentelemetry",
9394
"packages/nexus",
9495
"packages/nyc-test-coverage",
96+
"packages/plugin",
9597
"packages/proto",
9698
"packages/test",
9799
"packages/testing",

packages/client/src/client.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ export interface ClientOptions extends BaseClientOptions {
1515
*/
1616
interceptors?: ClientInterceptors;
1717

18+
/**
19+
* List of plugins to register with the client.
20+
*
21+
* Plugins allow you to extend and customize the behavior of Temporal clients.
22+
* They can intercept and modify client creation.
23+
*
24+
* @experimental Plugins is an experimental feature; APIs may change without notice.
25+
*/
26+
plugins?: ClientPlugin[];
27+
1828
workflow?: {
1929
/**
2030
* Should a query be rejected by closed and failed workflows
@@ -32,6 +42,7 @@ export type LoadedClientOptions = LoadedWithDefaults<ClientOptions>;
3242
*/
3343
export class Client extends BaseClient {
3444
public readonly options: LoadedClientOptions;
45+
3546
/**
3647
* Workflow sub-client - use to start and interact with Workflows
3748
*/
@@ -52,9 +63,21 @@ export class Client extends BaseClient {
5263
public readonly taskQueue: TaskQueueClient;
5364

5465
constructor(options?: ClientOptions) {
66+
options = options ?? {};
67+
68+
// Add client plugins from the connection
69+
options.plugins = (options.plugins ?? []).concat(options.connection?.plugins ?? []);
70+
71+
// Process plugins first to allow them to modify connect configuration
72+
for (const plugin of options.plugins) {
73+
if (plugin.configureClient !== undefined) {
74+
options = plugin.configureClient(options);
75+
}
76+
}
77+
5578
super(options);
5679

57-
const { interceptors, workflow, ...commonOptions } = options ?? {};
80+
const { interceptors, workflow, plugins, ...commonOptions } = options;
5881

5982
this.workflow = new WorkflowClient({
6083
...commonOptions,
@@ -95,6 +118,7 @@ export class Client extends BaseClient {
95118
workflow: {
96119
queryRejectCondition: this.workflow.options.queryRejectCondition,
97120
},
121+
plugins: plugins ?? [],
98122
};
99123
}
100124

@@ -108,3 +132,23 @@ export class Client extends BaseClient {
108132
return this.connection.workflowService;
109133
}
110134
}
135+
136+
/**
137+
* Plugin to control the configuration of a native connection.
138+
*
139+
* @experimental Plugins is an experimental feature; APIs may change without notice.
140+
*/
141+
export interface ClientPlugin {
142+
/**
143+
* Gets the name of this plugin.
144+
*/
145+
get name(): string;
146+
147+
/**
148+
* Hook called when creating a client to allow modification of configuration.
149+
*
150+
* This method is called during client creation and allows plugins to modify
151+
* the client configuration before the client is fully initialized.
152+
*/
153+
configureClient?(options: Omit<ClientOptions, 'plugins'>): Omit<ClientOptions, 'plugins'>;
154+
}

packages/client/src/connection.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,16 @@ export interface ConnectionOptions {
130130
* @default 10 seconds
131131
*/
132132
connectTimeout?: Duration;
133+
134+
/**
135+
* List of plugins to register with the connection.
136+
*
137+
* Plugins allow you to configure the connection options.
138+
* Any plugins provided will also be passed to any client built from this connection.
139+
*
140+
* @experimental Plugins is an experimental feature; APIs may change without notice.
141+
*/
142+
plugins?: ConnectionPlugin[];
133143
}
134144

135145
export type ConnectionOptionsWithDefaults = Required<
@@ -172,6 +182,7 @@ function addDefaults(options: ConnectionOptions): ConnectionOptionsWithDefaults
172182
interceptors: interceptors ?? [makeGrpcRetryInterceptor(defaultGrpcRetryOptions())],
173183
metadata: {},
174184
connectTimeoutMs: msOptionalToNumber(connectTimeout) ?? 10_000,
185+
plugins: [],
175186
...filterNullAndUndefined(rest),
176187
};
177188
}
@@ -182,8 +193,8 @@ function addDefaults(options: ConnectionOptions): ConnectionOptionsWithDefaults
182193
* - Add default port to address if port not specified
183194
* - Set `Authorization` header based on {@link ConnectionOptions.apiKey}
184195
*/
185-
function normalizeGRPCConfig(options?: ConnectionOptions): ConnectionOptions {
186-
const { tls: tlsFromConfig, credentials, callCredentials, ...rest } = options || {};
196+
function normalizeGRPCConfig(options: ConnectionOptions): ConnectionOptions {
197+
const { tls: tlsFromConfig, credentials, callCredentials, ...rest } = options;
187198
if (rest.apiKey) {
188199
if (rest.metadata?.['Authorization']) {
189200
throw new TypeError(
@@ -325,10 +336,12 @@ export class Connection {
325336
*/
326337
public readonly healthService: HealthService;
327338

339+
public readonly plugins: ConnectionPlugin[];
340+
328341
readonly callContextStorage: AsyncLocalStorage<CallContext>;
329342
private readonly apiKeyFnRef: { fn?: () => string };
330343

331-
protected static createCtorOptions(options?: ConnectionOptions): ConnectionCtorOptions {
344+
protected static createCtorOptions(options: ConnectionOptions): ConnectionCtorOptions {
332345
const normalizedOptions = normalizeGRPCConfig(options);
333346
const apiKeyFnRef: { fn?: () => string } = {};
334347
if (normalizedOptions.apiKey) {
@@ -444,6 +457,12 @@ export class Connection {
444457
* This method does not verify connectivity with the server. We recommend using {@link connect} instead.
445458
*/
446459
static lazy(options?: ConnectionOptions): Connection {
460+
options = options ?? {};
461+
for (const plugin of options.plugins ?? []) {
462+
if (plugin.configureConnection !== undefined) {
463+
options = plugin.configureConnection(options);
464+
}
465+
}
447466
return new this(this.createCtorOptions(options));
448467
}
449468

@@ -477,6 +496,7 @@ export class Connection {
477496
this.healthService = healthService;
478497
this.callContextStorage = callContextStorage;
479498
this.apiKeyFnRef = apiKeyFnRef;
499+
this.plugins = options.plugins ?? [];
480500
}
481501

482502
protected static generateRPCImplementation({
@@ -532,7 +552,7 @@ export class Connection {
532552
* this will locally result in the request call throwing a {@link grpc.ServiceError|ServiceError}
533553
* with code {@link grpc.status.DEADLINE_EXCEEDED|DEADLINE_EXCEEDED}; see {@link isGrpcDeadlineError}.
534554
*
535-
* It is stronly recommended to explicitly set deadlines. If no deadline is set, then it is
555+
* It is strongly recommended to explicitly set deadlines. If no deadline is set, then it is
536556
* possible for the client to end up waiting forever for a response.
537557
*
538558
* @param deadline a point in time after which the request will be considered as failed; either a
@@ -688,3 +708,20 @@ export class Connection {
688708
return wrapper as WorkflowService;
689709
}
690710
}
711+
712+
/**
713+
* Plugin to control the configuration of a connection.
714+
*
715+
* @experimental Plugins is an experimental feature; APIs may change without notice.
716+
*/
717+
export interface ConnectionPlugin {
718+
/**
719+
* Gets the name of this plugin.
720+
*/
721+
get name(): string;
722+
723+
/**
724+
* Hook called when creating a connection to allow modification of configuration.
725+
*/
726+
configureConnection?(options: ConnectionOptions): ConnectionOptions;
727+
}

packages/client/src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ export * from '@temporalio/common/lib/interfaces';
3030
export * from '@temporalio/common/lib/workflow-handle';
3131
export * from './async-completion-client';
3232
export * from './client';
33-
export { Connection, ConnectionOptions, ConnectionOptionsWithDefaults, LOCAL_TARGET } from './connection';
33+
export {
34+
Connection,
35+
ConnectionOptions,
36+
ConnectionOptionsWithDefaults,
37+
ConnectionPlugin,
38+
LOCAL_TARGET,
39+
} from './connection';
3440
export * from './errors';
3541
export * from './grpc-retry';
3642
export * from './interceptors';

packages/client/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { TypedSearchAttributes, SearchAttributes, SearchAttributeValue, Pri
33
import { makeProtoEnumConverters } from '@temporalio/common/lib/internal-workflow';
44
import * as proto from '@temporalio/proto';
55
import { Replace } from '@temporalio/common/lib/type-helpers';
6+
import type { ConnectionPlugin } from './connection';
67

78
export interface WorkflowExecution {
89
workflowId: string;
@@ -122,6 +123,7 @@ export interface CallContext {
122123
*/
123124
export interface ConnectionLike {
124125
workflowService: WorkflowService;
126+
plugins: ConnectionPlugin[];
125127
close(): Promise<void>;
126128
ensureConnected(): Promise<void>;
127129

packages/meta/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"@temporalio/envconfig": "file:../envconfig",
99
"@temporalio/interceptors-opentelemetry": "file:../interceptors-opentelemetry",
1010
"@temporalio/nexus": "file:../nexus",
11+
"@temporalio/plugin": "file:../plugin",
1112
"@temporalio/proto": "file:../proto",
1213
"@temporalio/testing": "file:../testing",
1314
"@temporalio/worker": "file:../worker",

packages/meta/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export * as nexus from '@temporalio/nexus';
1111
export * as testing from '@temporalio/testing';
1212
export * as opentelemetry from '@temporalio/interceptors-opentelemetry';
1313
export * as envconfig from '@temporalio/envconfig';
14+
export * as plugin from '@temporalio/plugin';

packages/meta/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
{ "path": "../activity" },
1010
{ "path": "../client" },
1111
{ "path": "../common" },
12+
{ "path": "../plugin" },
1213
{ "path": "../worker" },
1314
{ "path": "../workflow" }
1415
],

packages/plugin/package.json

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "@temporalio/plugin",
3+
"version": "1.13.0",
4+
"description": "Library for plugin creation",
5+
"main": "lib/index.js",
6+
"types": "./lib/index.d.ts",
7+
"keywords": [
8+
"temporal",
9+
"workflow",
10+
"worker",
11+
"plugin"
12+
],
13+
"author": "Temporal Technologies Inc. <[email protected]>",
14+
"license": "MIT",
15+
"dependencies": {},
16+
"devDependencies": {
17+
"@temporalio/common": "file:../common",
18+
"@temporalio/client": "file:../client",
19+
"@temporalio/worker": "file:../worker",
20+
"nexus-rpc": "^0.0.1"
21+
},
22+
"bugs": {
23+
"url": "https://github.com/temporalio/sdk-typescript/issues"
24+
},
25+
"repository": {
26+
"type": "git",
27+
"url": "git+https://github.com/temporalio/sdk-typescript.git",
28+
"directory": "packages/plugin"
29+
},
30+
"homepage": "https://github.com/temporalio/sdk-typescript/tree/main/packages/plugin",
31+
"publishConfig": {
32+
"access": "public"
33+
},
34+
"engines": {
35+
"node": ">= 18.0.0"
36+
},
37+
"files": [
38+
"src",
39+
"lib"
40+
]
41+
}

0 commit comments

Comments
 (0)