475 lines
19 KiB
JavaScript
475 lines
19 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.Connection = exports.LOCAL_TARGET = exports.InternalConnectionOptionsSymbol = void 0;
|
|
const node_async_hooks_1 = require("node:async_hooks");
|
|
const grpc = __importStar(require("@grpc/grpc-js"));
|
|
const internal_non_workflow_1 = require("@temporalio/common/lib/internal-non-workflow");
|
|
const internal_workflow_1 = require("@temporalio/common/lib/internal-workflow");
|
|
const time_1 = require("@temporalio/common/lib/time");
|
|
const errors_1 = require("./errors");
|
|
const grpc_retry_1 = require("./grpc-retry");
|
|
const pkg_1 = __importDefault(require("./pkg"));
|
|
const types_1 = require("./types");
|
|
/**
|
|
* The default Temporal Server's TCP port for public gRPC connections.
|
|
*/
|
|
const DEFAULT_TEMPORAL_GRPC_PORT = 7233;
|
|
/**
|
|
* A symbol used to attach extra, SDK-internal connection options.
|
|
*
|
|
* @internal
|
|
* @hidden
|
|
*/
|
|
exports.InternalConnectionOptionsSymbol = Symbol('__temporal_internal_connection_options');
|
|
exports.LOCAL_TARGET = 'localhost:7233';
|
|
function addDefaults(options) {
|
|
const { channelArgs, interceptors, connectTimeout, ...rest } = options;
|
|
return {
|
|
address: exports.LOCAL_TARGET,
|
|
credentials: grpc.credentials.createInsecure(),
|
|
channelArgs: {
|
|
'grpc.keepalive_permit_without_calls': 1,
|
|
'grpc.keepalive_time_ms': 30_000,
|
|
'grpc.keepalive_timeout_ms': 15_000,
|
|
max_receive_message_length: 128 * 1024 * 1024, // 128 MB
|
|
...channelArgs,
|
|
},
|
|
interceptors: interceptors ?? [(0, grpc_retry_1.makeGrpcRetryInterceptor)((0, grpc_retry_1.defaultGrpcRetryOptions)())],
|
|
metadata: {},
|
|
connectTimeoutMs: (0, time_1.msOptionalToNumber)(connectTimeout) ?? 10_000,
|
|
plugins: [],
|
|
...(0, internal_workflow_1.filterNullAndUndefined)(rest),
|
|
};
|
|
}
|
|
/**
|
|
* - Convert {@link ConnectionOptions.tls} to {@link grpc.ChannelCredentials}
|
|
* - Add the grpc.ssl_target_name_override GRPC {@link ConnectionOptions.channelArgs | channel arg}
|
|
* - Add default port to address if port not specified
|
|
* - Set `Authorization` header based on {@link ConnectionOptions.apiKey}
|
|
*/
|
|
function normalizeGRPCConfig(options) {
|
|
const { tls: tlsFromConfig, credentials, callCredentials, ...rest } = options;
|
|
if (rest.apiKey) {
|
|
if (rest.metadata?.['Authorization']) {
|
|
throw new TypeError('Both `apiKey` option and `Authorization` header were provided, but only one makes sense to use at a time.');
|
|
}
|
|
if (credentials !== undefined) {
|
|
throw new TypeError('Both `apiKey` and `credentials` ConnectionOptions were provided, but only one makes sense to use at a time');
|
|
}
|
|
}
|
|
if (rest.address) {
|
|
rest.address = (0, internal_non_workflow_1.normalizeGrpcEndpointAddress)(rest.address, DEFAULT_TEMPORAL_GRPC_PORT);
|
|
}
|
|
const tls = (0, internal_non_workflow_1.normalizeTlsConfig)(tlsFromConfig, options.apiKey);
|
|
if (tls) {
|
|
if (credentials) {
|
|
throw new TypeError('Both `tls` and `credentials` ConnectionOptions were provided');
|
|
}
|
|
const serverRootCert = tls.serverRootCACertificate && Buffer.from(tls.serverRootCACertificate);
|
|
const clientCertKey = tls.clientCertPair?.key && Buffer.from(tls.clientCertPair?.key);
|
|
const clientCertCrt = tls.clientCertPair?.crt && Buffer.from(tls.clientCertPair?.crt);
|
|
return {
|
|
...rest,
|
|
credentials: grpc.credentials.combineChannelCredentials(grpc.credentials.createSsl(serverRootCert, clientCertKey, clientCertCrt), ...(callCredentials ?? [])),
|
|
channelArgs: {
|
|
...rest.channelArgs,
|
|
...(tls.serverNameOverride
|
|
? {
|
|
'grpc.ssl_target_name_override': tls.serverNameOverride,
|
|
'grpc.default_authority': tls.serverNameOverride,
|
|
}
|
|
: undefined),
|
|
},
|
|
};
|
|
}
|
|
else {
|
|
return {
|
|
...rest,
|
|
credentials: grpc.credentials.combineChannelCredentials(credentials ?? grpc.credentials.createInsecure(), ...(callCredentials ?? [])),
|
|
};
|
|
}
|
|
}
|
|
/**
|
|
* Client connection to the Temporal Server
|
|
*
|
|
* ⚠️ Connections are expensive to construct and should be reused.
|
|
* Make sure to {@link close} any unused connections to avoid leaking resources.
|
|
*/
|
|
class Connection {
|
|
/**
|
|
* @internal
|
|
*/
|
|
static Client = grpc.makeGenericClientConstructor({}, 'WorkflowService', {});
|
|
options;
|
|
client;
|
|
/**
|
|
* Used to ensure `ensureConnected` is called once.
|
|
*/
|
|
connectPromise;
|
|
/**
|
|
* Raw gRPC access to Temporal Server's {@link
|
|
* https://github.com/temporalio/api/blob/master/temporal/api/workflowservice/v1/service.proto | Workflow service}
|
|
*/
|
|
workflowService;
|
|
/**
|
|
* Raw gRPC access to Temporal Server's
|
|
* {@link https://github.com/temporalio/api/blob/master/temporal/api/operatorservice/v1/service.proto | Operator service}
|
|
*
|
|
* The Operator Service API defines how Temporal SDKs and other clients interact with the Temporal
|
|
* server to perform administrative functions like registering a search attribute or a namespace.
|
|
*
|
|
* This Service API is NOT compatible with Temporal Cloud. Attempt to use it against a Temporal
|
|
* Cloud namespace will result in gRPC `unauthorized` error.
|
|
*/
|
|
operatorService;
|
|
/**
|
|
* Raw gRPC access to the Temporal test service.
|
|
*
|
|
* Will be `undefined` if connected to a server that does not support the test service.
|
|
*/
|
|
testService;
|
|
/**
|
|
* Raw gRPC access to the standard gRPC {@link https://github.com/grpc/grpc/blob/92f58c18a8da2728f571138c37760a721c8915a2/doc/health-checking.md | health service}.
|
|
*/
|
|
healthService;
|
|
plugins;
|
|
callContextStorage;
|
|
apiKeyFnRef;
|
|
static createCtorOptions(options) {
|
|
const normalizedOptions = normalizeGRPCConfig(options);
|
|
const apiKeyFnRef = {};
|
|
if (normalizedOptions.apiKey) {
|
|
if (typeof normalizedOptions.apiKey === 'string') {
|
|
const apiKey = normalizedOptions.apiKey;
|
|
apiKeyFnRef.fn = () => apiKey;
|
|
}
|
|
else {
|
|
apiKeyFnRef.fn = normalizedOptions.apiKey;
|
|
}
|
|
}
|
|
const optionsWithDefaults = addDefaults(normalizedOptions);
|
|
// Allow overriding this
|
|
optionsWithDefaults.metadata['client-name'] ??= 'temporal-typescript';
|
|
optionsWithDefaults.metadata['client-version'] ??= pkg_1.default.version;
|
|
const client = new this.Client(optionsWithDefaults.address, optionsWithDefaults.credentials, optionsWithDefaults.channelArgs);
|
|
const callContextStorage = new node_async_hooks_1.AsyncLocalStorage();
|
|
const workflowRpcImpl = this.generateRPCImplementation({
|
|
serviceName: 'temporal.api.workflowservice.v1.WorkflowService',
|
|
client,
|
|
callContextStorage,
|
|
interceptors: optionsWithDefaults?.interceptors,
|
|
staticMetadata: optionsWithDefaults.metadata,
|
|
apiKeyFnRef,
|
|
});
|
|
const workflowService = types_1.WorkflowService.create(workflowRpcImpl, false, false);
|
|
const operatorRpcImpl = this.generateRPCImplementation({
|
|
serviceName: 'temporal.api.operatorservice.v1.OperatorService',
|
|
client,
|
|
callContextStorage,
|
|
interceptors: optionsWithDefaults?.interceptors,
|
|
staticMetadata: optionsWithDefaults.metadata,
|
|
apiKeyFnRef,
|
|
});
|
|
const operatorService = types_1.OperatorService.create(operatorRpcImpl, false, false);
|
|
let testService = undefined;
|
|
if (options?.[exports.InternalConnectionOptionsSymbol]?.supportsTestService) {
|
|
const testRpcImpl = this.generateRPCImplementation({
|
|
serviceName: 'temporal.api.testservice.v1.TestService',
|
|
client,
|
|
callContextStorage,
|
|
interceptors: optionsWithDefaults?.interceptors,
|
|
staticMetadata: optionsWithDefaults.metadata,
|
|
apiKeyFnRef,
|
|
});
|
|
testService = types_1.TestService.create(testRpcImpl, false, false);
|
|
}
|
|
const healthRpcImpl = this.generateRPCImplementation({
|
|
serviceName: 'grpc.health.v1.Health',
|
|
client,
|
|
callContextStorage,
|
|
interceptors: optionsWithDefaults?.interceptors,
|
|
staticMetadata: optionsWithDefaults.metadata,
|
|
apiKeyFnRef,
|
|
});
|
|
const healthService = types_1.HealthService.create(healthRpcImpl, false, false);
|
|
return {
|
|
client,
|
|
callContextStorage,
|
|
workflowService,
|
|
operatorService,
|
|
testService,
|
|
healthService,
|
|
options: optionsWithDefaults,
|
|
apiKeyFnRef,
|
|
};
|
|
}
|
|
/**
|
|
* Ensure connection can be established.
|
|
*
|
|
* Does not need to be called if you use {@link connect}.
|
|
*
|
|
* This method's result is memoized to ensure it runs only once.
|
|
*
|
|
* Calls {@link proto.temporal.api.workflowservice.v1.WorkflowService.getSystemInfo} internally.
|
|
*/
|
|
async ensureConnected() {
|
|
if (this.connectPromise == null) {
|
|
const deadline = Date.now() + this.options.connectTimeoutMs;
|
|
this.connectPromise = (async () => {
|
|
await this.untilReady(deadline);
|
|
try {
|
|
await this.withDeadline(deadline, () => this.workflowService.getSystemInfo({}));
|
|
}
|
|
catch (err) {
|
|
if ((0, errors_1.isGrpcServiceError)(err)) {
|
|
// Ignore old servers
|
|
if (err.code !== grpc.status.UNIMPLEMENTED) {
|
|
throw new errors_1.ServiceError('Failed to connect to Temporal server', { cause: err });
|
|
}
|
|
}
|
|
else {
|
|
throw err;
|
|
}
|
|
}
|
|
})();
|
|
}
|
|
return this.connectPromise;
|
|
}
|
|
/**
|
|
* Create a lazy `Connection` instance.
|
|
*
|
|
* This method does not verify connectivity with the server. We recommend using {@link connect} instead.
|
|
*/
|
|
static lazy(options) {
|
|
options = options ?? {};
|
|
for (const plugin of options.plugins ?? []) {
|
|
if (plugin.configureConnection !== undefined) {
|
|
options = plugin.configureConnection(options);
|
|
}
|
|
}
|
|
return new this(this.createCtorOptions(options));
|
|
}
|
|
/**
|
|
* Establish a connection with the server and return a `Connection` instance.
|
|
*
|
|
* This is the preferred method of creating connections as it verifies connectivity by calling
|
|
* {@link ensureConnected}.
|
|
*/
|
|
static async connect(options) {
|
|
const conn = this.lazy(options);
|
|
await conn.ensureConnected();
|
|
return conn;
|
|
}
|
|
constructor({ options, client, workflowService, operatorService, testService, healthService, callContextStorage, apiKeyFnRef, }) {
|
|
this.options = options;
|
|
this.client = client;
|
|
this.workflowService = this.withNamespaceHeaderInjector(workflowService);
|
|
this.operatorService = operatorService;
|
|
this.testService = testService;
|
|
this.healthService = healthService;
|
|
this.callContextStorage = callContextStorage;
|
|
this.apiKeyFnRef = apiKeyFnRef;
|
|
this.plugins = options.plugins ?? [];
|
|
}
|
|
static generateRPCImplementation({ serviceName, client, callContextStorage, interceptors, staticMetadata, apiKeyFnRef, }) {
|
|
return (method, requestData, callback) => {
|
|
const metadataContainer = new grpc.Metadata();
|
|
const { metadata, deadline, abortSignal } = callContextStorage.getStore() ?? {};
|
|
if (apiKeyFnRef.fn) {
|
|
const apiKey = apiKeyFnRef.fn();
|
|
if (apiKey)
|
|
metadataContainer.set('Authorization', `Bearer ${apiKey}`);
|
|
}
|
|
for (const [k, v] of Object.entries(staticMetadata)) {
|
|
metadataContainer.set(k, v);
|
|
}
|
|
if (metadata != null) {
|
|
for (const [k, v] of Object.entries(metadata)) {
|
|
metadataContainer.set(k, v);
|
|
}
|
|
}
|
|
const call = client.makeUnaryRequest(`/${serviceName}/${method.name}`, (arg) => arg, (arg) => arg, requestData, metadataContainer, { interceptors, deadline }, callback);
|
|
if (abortSignal != null) {
|
|
abortSignal.addEventListener('abort', () => call.cancel());
|
|
}
|
|
return call;
|
|
};
|
|
}
|
|
/**
|
|
* Set a deadline for any service requests executed in `fn`'s scope.
|
|
*
|
|
* The deadline is a point in time after which any pending gRPC request will be considered as failed;
|
|
* 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 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
|
|
* Date object, or a number of milliseconds since the Unix epoch (UTC).
|
|
* @returns the value returned from `fn`
|
|
*
|
|
* @see https://grpc.io/docs/guides/deadlines/
|
|
*/
|
|
async withDeadline(deadline, fn) {
|
|
const cc = this.callContextStorage.getStore();
|
|
return await this.callContextStorage.run({ ...cc, deadline }, fn);
|
|
}
|
|
/**
|
|
* Set an {@link AbortSignal} that, when aborted, cancels any ongoing service requests executed in
|
|
* `fn`'s scope. This will locally result in the request call throwing a {@link grpc.ServiceError|ServiceError}
|
|
* with code {@link grpc.status.CANCELLED|CANCELLED}; see {@link isGrpcCancelledError}.
|
|
*
|
|
* This method is only a convenience wrapper around {@link Connection.withAbortSignal}.
|
|
*
|
|
* @example
|
|
*
|
|
* ```ts
|
|
* const ctrl = new AbortController();
|
|
* setTimeout(() => ctrl.abort(), 10_000);
|
|
* // 👇 throws if incomplete by the timeout.
|
|
* await conn.withAbortSignal(ctrl.signal, () => client.workflow.execute(myWorkflow, options));
|
|
* ```
|
|
*
|
|
* @returns value returned from `fn`
|
|
*
|
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
|
|
*/
|
|
// FIXME: `abortSignal` should be cumulative, i.e. if a signal is already set, it should be added
|
|
// to the list of signals, and both the new and existing signal should abort the request.
|
|
async withAbortSignal(abortSignal, fn) {
|
|
const cc = this.callContextStorage.getStore();
|
|
return await this.callContextStorage.run({ ...cc, abortSignal }, fn);
|
|
}
|
|
/**
|
|
* Set metadata for any service requests executed in `fn`'s scope.
|
|
*
|
|
* The provided metadata is merged on top of any existing metadata in current scope, including metadata provided in
|
|
* {@link ConnectionOptions.metadata}.
|
|
*
|
|
* @returns value returned from `fn`
|
|
*
|
|
* @example
|
|
*
|
|
* ```ts
|
|
* const workflowHandle = await conn.withMetadata({ apiKey: 'secret' }, () =>
|
|
* conn.withMetadata({ otherKey: 'set' }, () => client.start(options)))
|
|
* );
|
|
* ```
|
|
*/
|
|
async withMetadata(metadata, fn) {
|
|
const cc = this.callContextStorage.getStore();
|
|
return await this.callContextStorage.run({
|
|
...cc,
|
|
metadata: { ...cc?.metadata, ...metadata },
|
|
}, fn);
|
|
}
|
|
/**
|
|
* Set the apiKey for any service requests executed in `fn`'s scope (thus changing the `Authorization` header).
|
|
*
|
|
* @returns value returned from `fn`
|
|
*
|
|
* @example
|
|
*
|
|
* ```ts
|
|
* const workflowHandle = await conn.withApiKey('secret', () =>
|
|
* conn.withMetadata({ otherKey: 'set' }, () => client.start(options)))
|
|
* );
|
|
* ```
|
|
*/
|
|
async withApiKey(apiKey, fn) {
|
|
const cc = this.callContextStorage.getStore();
|
|
return await this.callContextStorage.run({
|
|
...cc,
|
|
metadata: { ...cc?.metadata, Authorization: `Bearer ${apiKey}` },
|
|
}, fn);
|
|
}
|
|
/**
|
|
* Set the {@link ConnectionOptions.apiKey} for all subsequent requests. A static string or a
|
|
* callback function may be provided.
|
|
*/
|
|
setApiKey(apiKey) {
|
|
if (typeof apiKey === 'string') {
|
|
if (apiKey === '') {
|
|
throw new TypeError('`apiKey` must not be an empty string');
|
|
}
|
|
this.apiKeyFnRef.fn = () => apiKey;
|
|
}
|
|
else {
|
|
this.apiKeyFnRef.fn = apiKey;
|
|
}
|
|
}
|
|
/**
|
|
* Wait for successful connection to the server.
|
|
*
|
|
* @see https://grpc.github.io/grpc/node/grpc.Client.html#waitForReady__anchor
|
|
*/
|
|
async untilReady(deadline) {
|
|
return new Promise((resolve, reject) => {
|
|
this.client.waitForReady(deadline, (err) => {
|
|
if (err) {
|
|
reject(err);
|
|
}
|
|
else {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
// This method is async for uniformity with NativeConnection which could be used in the future to power clients
|
|
/**
|
|
* Close the underlying gRPC client.
|
|
*
|
|
* Make sure to call this method to ensure proper resource cleanup.
|
|
*/
|
|
async close() {
|
|
this.client.close();
|
|
this.callContextStorage.disable();
|
|
}
|
|
withNamespaceHeaderInjector(workflowService) {
|
|
const wrapper = {};
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
for (const [methodName, methodImpl] of Object.entries(workflowService)) {
|
|
if (typeof methodImpl !== 'function')
|
|
continue;
|
|
wrapper[methodName] = (...args) => {
|
|
const namespace = args[0]?.namespace;
|
|
if (namespace) {
|
|
return this.withMetadata({ 'temporal-namespace': namespace }, () => methodImpl.apply(workflowService, args));
|
|
}
|
|
else {
|
|
return methodImpl.apply(workflowService, args);
|
|
}
|
|
};
|
|
}
|
|
return wrapper;
|
|
}
|
|
}
|
|
exports.Connection = Connection;
|
|
//# sourceMappingURL=connection.js.map
|