Skip to content
Merged
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
27 changes: 0 additions & 27 deletions .eslintrc

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/typescript.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: 18
node-version: 24

- name: Install protoc
if: ${{ inputs.version-is-repo-ref }}
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ node_modules
__pycache__
pyrightconfig.json

# .NET stuff
features.sln

# Build stuff
bin
obj
Expand Down
64 changes: 64 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';
import prettierConfig from 'eslint-config-prettier';

export default tseslint.config(
{ ignores: ['**/node_modules/**', '**/lib/**', '**/*.js', '**/*.mjs', '**/*.cjs'] },
{
files: ['features/**/*.ts', 'harness/ts/**/*.ts'],
extends: [js.configs.recommended, ...tseslint.configs.recommended, prettierConfig],
plugins: { import: importPlugin },
languageOptions: {
parserOptions: { project: ['./tsconfig.json'] },
},
settings: {
'import/parsers': { '@typescript-eslint/parser': ['.ts'] },
'import/resolver': {
// Resolve types under `<root>@types` even for packages without source code, like `@types/unist`
typescript: { alwaysTryTypes: true, project: ['tsconfig.json'] },
},
'import/internal-regex': '^@temporalio/',
},
rules: {
eqeqeq: ['error', 'always', { null: 'ignore' }],
'no-duplicate-imports': 'error',
'object-shorthand': ['error', 'always'],
'no-restricted-imports': ['error', { patterns: ['@temporalio/*/src/*'] }],
// TypeScript rules
'@typescript-eslint/no-deprecated': 'warn',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'error',
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
// Import rules
'import/no-unresolved': ['error', { ignore: ['^__temporal_', '^@temporalio/harness'] }],
// TypeScript compilation already ensures that named imports exist in the referenced module
'import/named': 'off',
'import/default': 'error',
'import/namespace': 'error',
'import/no-absolute-path': 'error',
'import/no-self-import': 'error',
'import/no-cycle': 'error',
'import/no-useless-path-segments': 'error',
'import/no-relative-packages': 'error',
'import/export': 'error',
'import/no-named-as-default': 'error',
'import/no-extraneous-dependencies': 'error',
'import/no-mutable-exports': 'error',
'import/unambiguous': 'error',
'import/first': 'error',
'import/order': ['error', { groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'] }],
'import/newline-after-import': 'error',
'import/no-unassigned-import': 'error',
'import/no-named-default': 'error',
},
}
);
5 changes: 2 additions & 3 deletions features/activity/cancel_try_cancel/feature.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Context } from '@temporalio/activity';
import { CancelledFailure } from '@temporalio/common';
import { ActivityFailure, ApplicationFailure, CancelledFailure } from '@temporalio/common';
import { Feature, getClient } from '@temporalio/harness';
import { ActivityFailure, ApplicationFailure } from '@temporalio/common';
import * as wf from '@temporalio/workflow';

// Allow 4 retries with no backoff
Expand Down Expand Up @@ -39,7 +38,7 @@ export async function workflow(): Promise<void> {

// Confirm signal is received saying the activity got the cancel
const activityResult = await new Promise<string>((resolve) => wf.setHandler(activityResultSignal, resolve));
if (activityResult != 'cancelled') {
if (activityResult !== 'cancelled') {
throw ApplicationFailure.nonRetryable(`Expected cancelled, got ${activityResult}`);
}
}
Expand Down
4 changes: 2 additions & 2 deletions features/activity/retry_on_error/feature.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as assert from 'assert';
import { Context } from '@temporalio/activity';
import * as wf from '@temporalio/workflow';
import { WorkflowFailedError } from '@temporalio/client';
import { TemporalFailure } from '@temporalio/common';
import { Feature } from '@temporalio/harness';
import * as assert from 'assert';

// Allow 4 retries with no backoff
const activities = wf.proxyActivities<typeof activitiesImpl>({
Expand Down Expand Up @@ -36,7 +36,7 @@ export const feature = new Feature({
await assert.rejects(runner.waitForRunResult(handle), (err) => {
assert.ok(
err instanceof WorkflowFailedError,
`expected WorkflowFailedError, got ${typeof err}, message: ${(err as any).message}`
`expected WorkflowFailedError, got ${typeof err}, message: ${(err as any).message}`,
);
assert.ok(err.cause instanceof TemporalFailure);
assert.equal(err.cause.cause?.message, 'activity attempt 5 failed');
Expand Down
8 changes: 4 additions & 4 deletions features/activity/shutdown/feature.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as assert from 'assert';
import { Feature } from '@temporalio/harness';
import * as wf from '@temporalio/workflow';
import { ApplicationFailure, TimeoutFailure, TimeoutType } from '@temporalio/common';
import * as assert from 'assert';

// Promise and helper used for activities to detect worker shutdown
let shutdownRequested = false;
Expand Down Expand Up @@ -47,8 +47,8 @@ export async function workflow(): Promise<string> {
const fut1 = gracefulActivities.cancelFailure();
const fut2 = ignoringActivities.cancelIgnore();
// Register rejection handlers eagerly in case harness is slow seeing first activity scheduled event
fut1.catch(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function
fut2.catch(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function
void fut1.catch(() => {});
void fut2.catch(() => {});

await fut;

Expand Down Expand Up @@ -95,7 +95,7 @@ export const feature = new Feature({
() => runner.getHistoryEvents(handle),
(event) => !!event.activityTaskScheduledEventAttributes,
5000, // 5 second timeout
100 // 100ms poll interval
100, // 100ms poll interval
);

notifyShutdown();
Expand Down
2 changes: 1 addition & 1 deletion features/child_workflow/result/feature.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as assert from 'assert';
import * as wf from '@temporalio/workflow';
import { Feature } from '@temporalio/harness';
import * as assert from 'assert';

const ChildWorkflowInput = 'test';

Expand Down
56 changes: 32 additions & 24 deletions features/client/http_proxy/feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import assert from 'assert';
import { fork } from 'child_process';
import { randomUUID } from 'crypto';
import { Client, ClientOptions, Connection, ConnectionOptions } from '@temporalio/client';
import { Feature } from '@temporalio/harness';
import { ReplaceNested, Feature } from '@temporalio/harness';

export async function workflow(): Promise<void> {
return;
}

interface SubprocessOpts {
connectionOpts: ConnectionOptions;
connectionOpts: ReplaceNested<ConnectionOptions, Uint8Array<ArrayBufferLike>, string>;
clientOpts: ClientOptions;
taskQueue: string;
}
Expand All @@ -23,27 +23,29 @@ export const feature = new Feature({
const url = new URL(runner.options.proxyUrl);
const proxyUrl = url.toString();

const { tls, metadata, ...connectionOpts } = runner.connectionOpts;

// Proxying config is internally passed to @grpc/grpc-js using an environment variable.
// We test in a subprocess to not infect other things in this process. The
// subprocess will make the client call to run the workflow, this will just
// return the run.
const subprocessOpts: SubprocessOpts = {
connectionOpts: {
...runner.connectionOpts,
...(typeof runner.connectionOpts?.tls === 'object'
...connectionOpts,
...(metadata && typeof metadata === 'object'
? { metadata: Object.fromEntries(Object.entries(metadata).map(([key, value]) => [key, value.toString()])) }
: undefined),
...(tls && typeof tls === 'object'
? {
tls: {
...runner.connectionOpts.tls,
serverNameOverride: tls.serverNameOverride,
serverRootCACertificate: tls.serverRootCACertificate
? Buffer.from(tls.serverRootCACertificate).toString('base64')
: undefined,
clientCertPair: {
// Can't serialize Buffers safely to JSON, so let's cheat a bit
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
crt: Buffer.from(runner.connectionOpts.tls!.clientCertPair!.crt).toString(
'base64'
) as unknown as Buffer,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
key: Buffer.from(runner.connectionOpts.tls!.clientCertPair!.key).toString(
'base64'
) as unknown as Buffer,
crt: Buffer.from(tls.clientCertPair!.crt).toString('base64'),
key: Buffer.from(tls.clientCertPair!.key).toString('base64'),
},
},
}
Expand Down Expand Up @@ -76,26 +78,32 @@ export const feature = new Feature({
async function subprocess() {
if (typeof process.env.subprocess_opts !== 'string')
throw new Error('Expected process.env.subprocess_opts to be a string');
const { connectionOpts, clientOpts, taskQueue } = JSON.parse(process.env.subprocess_opts) as SubprocessOpts;
const {
connectionOpts: { tls, ...connectionOpts },
clientOpts,
taskQueue,
} = JSON.parse(process.env.subprocess_opts) as SubprocessOpts;
const connection = await Connection.connect({
...connectionOpts,
...(typeof connectionOpts?.tls === 'object'
...(tls && typeof tls === 'object'
? {
tls: {
...connectionOpts.tls,
// Can't serialize Buffers safely to JSON, so let's cheat a bit
clientCertPair: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
crt: Buffer.from(connectionOpts.tls!.clientCertPair!.crt as unknown as string, 'base64'),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
key: Buffer.from(connectionOpts.tls!.clientCertPair!.key as unknown as string, 'base64'),
},
serverNameOverride: tls.serverNameOverride,
serverRootCACertificate: tls.serverRootCACertificate
? new Uint8Array(Buffer.from(tls.serverRootCACertificate as string, 'base64'))
: undefined,
clientCertPair: tls.clientCertPair
? {
crt: new Uint8Array(Buffer.from(tls.clientCertPair!.crt, 'base64')),
key: new Uint8Array(Buffer.from(tls.clientCertPair!.key, 'base64')),
}
: undefined,
},
}
: undefined),
});
try {
const client = await new Client({
const client = new Client({
...clientOpts,
connection,
});
Expand Down
63 changes: 36 additions & 27 deletions features/client/http_proxy_auth/feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import assert from 'assert';
import { fork } from 'child_process';
import { randomUUID } from 'crypto';
import { Client, ClientOptions, Connection, ConnectionOptions } from '@temporalio/client';
import { Feature } from '@temporalio/harness';
import { ReplaceNested, Feature } from '@temporalio/harness';

export async function workflow(): Promise<void> {
return;
}

interface SubprocessOpts {
connectionOpts: ConnectionOptions;
connectionOpts: ReplaceNested<ConnectionOptions, Uint8Array<ArrayBufferLike>, string>;
clientOpts: ClientOptions;
taskQueue: string;
}
Expand All @@ -29,24 +29,29 @@ export const feature = new Feature({
// We test in a subprocess to not infect other things in this process. The
// subprocess will make the client call to run the workflow, this will just
// return the run.
const { tls, metadata, ...connectionOpts } = runner.connectionOpts;
const subprocessOpts: SubprocessOpts = {
connectionOpts: {
...runner.connectionOpts,
...(typeof runner.connectionOpts?.tls === 'object'
...connectionOpts,
...(metadata && typeof metadata === 'object'
? { metadata: Object.fromEntries(Object.entries(metadata).map(([key, value]) => [key, value.toString()])) }
: undefined),
...(tls && typeof tls === 'object'
? {
tls: {
...runner.connectionOpts.tls,
clientCertPair: {
// Can't serialize Buffers safely to JSON, so let's cheat a bit
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
crt: Buffer.from(runner.connectionOpts.tls!.clientCertPair!.crt).toString(
'base64'
) as unknown as Buffer,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
key: Buffer.from(runner.connectionOpts.tls!.clientCertPair!.key).toString(
'base64'
) as unknown as Buffer,
},
serverNameOverride: tls.serverNameOverride,
serverRootCACertificate: tls.serverRootCACertificate
? Buffer.from(tls.serverRootCACertificate).toString('base64')
: undefined,
clientCertPair: tls.clientCertPair
? {
// Can't serialize Buffers safely to JSON, so let's cheat a bit

crt: Buffer.from(tls.clientCertPair!.crt).toString('base64'),

key: Buffer.from(tls.clientCertPair!.key).toString('base64'),
}
: undefined,
},
}
: undefined),
Expand Down Expand Up @@ -79,25 +84,29 @@ async function subprocess() {
if (typeof process.env.subprocess_opts !== 'string')
throw new Error('Expected process.env.subprocess_opts to be a string');
const { connectionOpts, clientOpts, taskQueue } = JSON.parse(process.env.subprocess_opts) as SubprocessOpts;
const { tls, ...connectionOptsRest } = connectionOpts;
const connection = await Connection.connect({
...connectionOpts,
...(typeof connectionOpts?.tls === 'object'
...connectionOptsRest,
...(tls && typeof tls === 'object'
? {
tls: {
...connectionOpts.tls,
// Can't serialize Buffers safely to JSON, so let's cheat a bit
clientCertPair: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
crt: Buffer.from(connectionOpts.tls!.clientCertPair!.crt as unknown as string, 'base64'),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
key: Buffer.from(connectionOpts.tls!.clientCertPair!.key as unknown as string, 'base64'),
},
serverNameOverride: tls.serverNameOverride,
serverRootCACertificate: tls.serverRootCACertificate
? new Uint8Array(Buffer.from(tls.serverRootCACertificate as string, 'base64'))
: undefined,
clientCertPair: tls.clientCertPair
? {
crt: new Uint8Array(Buffer.from(tls.clientCertPair!.crt as string, 'base64')),

key: new Uint8Array(Buffer.from(tls.clientCertPair!.key as string, 'base64')),
}
: undefined,
},
}
: undefined),
});
try {
const client = await new Client({
const client = new Client({
...clientOpts,
connection,
});
Expand Down
Loading
Loading