From efbba8dd00a17d1aa781035f33c950a268999744 Mon Sep 17 00:00:00 2001 From: Mary Gao Date: Wed, 24 Apr 2024 10:26:15 +0800 Subject: [PATCH] Support V3 SimplePollerLike in RLC LRO (#2443) * Fix typo issues * Update the reference property * Remove the useLegacyLro options * Update the template to adopt with v3 changes * Update the lro flag and enable useLegacyV2Lro for load test * Update the changes * Update the swagger side with this options * Update the flag in smoke test * Update the integration testing in swagger repo * Adopt the option useLegacyV2Lro for swagger way * Revert change in smoke test * Update packages/autorest.typescript/test/commands/smoke-test-list.ts * Update the smoke test in typespec * Update the test case for lro rpcClient * Add test cases for lro rpc rlc * Change to lro rpc * Update the modular poller in rlc and disable legacy code in modular * Add a warning * Testing in local env * Fix the exception issue during polling initial process * regen lro code * regen smoke-test code * Update packages/typespec-ts/package.json * Update packages/typespec-ts/package.json * Update the lock file * Remove the option for useLegacyV2Lro * Revert testing changes * Regenerate rlc lro with v3 version * Update the modular test * Regenerate smoke testing in swagger * Refresh the smoke test in tsp * Update the content * regen smoke test * regen integration code * Should report the server message * Update the package.json for rlc integration * update the commands * Update the smoke git diff * update the rlc path * Add extra steps to install dependencies * Update the dependencies for browser testing * Revert changes * Update the install commands * Remove the option useLegacyV2Lro * Update the documents * Update the swagger generation * regen integration code * regen smoke test * remove the un-used command * Update the scripts to install deps * Update the RLC simplepollerlike with comments * regen smoke test and integration * regen smoke test tag rlc * Update the namings * Avoid breakings for toString * Update the integration for swagger * update changes * regen test * Update the lro template * Support abort signal for modular lro * regen integration test * regen smoke test * regen rlc swagger test * Update the node version * Update the smoke test * update the ut name * update the test cases * Regen smoke testing * update the smoke testing --------- Co-authored-by: Jiao Di (MSFT) <80496810+v-jiaodi@users.noreply.github.com> Co-authored-by: Di Jiao --- .scripts/build.yml | 2 +- .scripts/ci.yml | 20 +- .scripts/release.yml | 24 +- .scripts/smoke-test.yml | 4 +- common/config/rush/pnpm-lock.yaml | 14 +- packages/autorest.typescript/package.json | 9 +- .../transforms/transformPaths.ts | 2 +- .../test/commands/browser.package.json | 10 + .../test/commands/prepare-deps.ts | 85 ++++++ .../autorest.typescript/test/commands/run.ts | 2 +- .../test/commands/test-swagger-gen.ts | 2 +- .../generated/dpgCustomization/package.json | 4 +- .../dpgCustomization/src/pollingHelper.ts | 148 +++++++++- .../generated/lroRest/package.json | 4 +- .../generated/lroRest/src/pollingHelper.ts | 148 +++++++++- .../generated/pagingRest/package.json | 4 +- .../generated/pagingRest/src/pollingHelper.ts | 148 +++++++++- .../test/rlcIntegration/lroRest.spec.ts | 16 ++ .../test/rlcIntegration/package.json | 5 + .../agrifood-data-plane/package.json | 4 +- .../review/agrifood-data-plane.api.md | 24 +- .../agrifood-data-plane/src/pollingHelper.ts | 148 +++++++++- .../anomaly-detector-rest/package.json | 4 +- .../review/anomaly-detector-rest.api.md | 24 +- .../src/pollingHelper.ts | 148 +++++++++- .../src/pollingHelper.ts | 148 +++++++++- .../webpack.config.test.js | 43 ++- packages/rlc-common/src/buildIndexFile.ts | 8 +- packages/rlc-common/src/buildPollingHelper.ts | 23 +- packages/rlc-common/src/interfaces.ts | 2 +- .../src/metadata/buildPackageFile.ts | 4 +- .../packageJson/azurePackageCommon.ts | 8 +- .../rlc-common/src/static/pollingContent.ts | 182 ++++++++++--- .../rlc-common/src/transformSampleGroups.ts | 4 +- .../generated/typespec-ts/package.json | 4 +- .../typespec-ts/review/authoring.api.md | 24 +- .../typespec-ts/src/pollingHelper.ts | 148 +++++++++- .../generated/typespec-ts/package.json | 4 +- .../review/contosowidgetmanager-rest.api.md | 24 +- .../typespec-ts/src/pollingHelper.ts | 148 +++++++++- .../generated/typespec-ts/package.json | 4 +- .../health-insights-radiologyinsights.api.md | 24 +- .../typespec-ts/src/pollingHelper.ts | 148 +++++++++- .../generated/typespec-ts/package.json | 4 +- .../health-insights-clinicalmatching.api.md | 24 +- .../typespec-ts/src/pollingHelper.ts | 148 +++++++++- .../generated/typespec-ts/package.json | 4 +- .../typespec-ts/review/load-testing.api.md | 24 +- .../typespec-ts/src/pollingHelper.ts | 148 +++++++++- .../generated/typespec-ts/src/rest/index.ts | 1 + .../typespec-ts/src/rest/pollingHelper.ts | 221 +++++++++++++++ .../testRunOperations/api/pollingHelpers.ts | 33 ++- .../openai/generated/typespec-ts/package.json | 4 +- .../typespec-ts/review/openai.api.md | 24 +- .../typespec-ts/src/pollingHelper.ts | 148 +++++++++- .../generated/typespec-ts/src/rest/index.ts | 1 + .../typespec-ts/src/rest/pollingHelper.ts | 219 +++++++++++++++ .../generated/src/api/pollingHelpers.ts | 33 ++- .../sources/generated/src/rest/index.ts | 1 + .../generated/src/rest/pollingHelper.ts | 253 ++++++++++++++++++ .../typespec-ts/src/api/pollingHelpers.ts | 33 ++- .../generated/typespec-ts/src/rest/index.ts | 1 + .../typespec-ts/src/rest/pollingHelper.ts | 253 ++++++++++++++++++ packages/typespec-ts/package.json | 4 +- .../typespec-ts/src/modular/buildLroFiles.ts | 41 ++- .../src/transform/transformPaths.ts | 2 +- .../test/commands/cadl-ranch-list.ts | 2 +- .../typespec-ts/test/commands/prepare-deps.ts | 35 --- .../generated/lro/lroCore/package.json | 4 +- .../lro/lroCore/src/pollingHelper.ts | 148 +++++++++- .../generated/lro/lroRPC/README.md | 8 +- .../generated/lro/lroRPC/package.json | 4 +- .../lro/lroRPC/src/clientDefinitions.ts | 38 +-- .../generated/lro/lroRPC/src/index.ts | 6 +- .../generated/lro/lroRPC/src/isUnexpected.ts | 37 +-- .../generated/lro/lroRPC/src/models.ts | 8 +- .../generated/lro/lroRPC/src/outputModels.ts | 48 ++-- .../generated/lro/lroRPC/src/parameters.ts | 11 +- .../generated/lro/lroRPC/src/pollingHelper.ts | 158 +++++++++-- .../generated/lro/lroRPC/src/responses.ts | 37 +-- .../src/{legacyClient.ts => rpcClient.ts} | 10 +- .../test/integration/lroCore.spec.ts | 18 +- .../test/integration/lroRpc.spec.ts | 38 +++ .../rpc/generated/src/api/pollingHelpers.ts | 33 ++- .../lro/rpc/generated/src/rest/index.ts | 1 + .../rpc/generated/src/rest/pollingHelper.ts | 215 +++++++++++++++ .../generated/src/api/pollingHelpers.ts | 33 ++- .../lro/standard/generated/src/rest/index.ts | 1 + .../generated/src/rest/pollingHelper.ts | 241 +++++++++++++++++ .../modularIntegration/lroStardard.spec.ts | 87 ++++-- 90 files changed, 4262 insertions(+), 536 deletions(-) create mode 100644 packages/autorest.typescript/test/commands/browser.package.json create mode 100644 packages/autorest.typescript/test/commands/prepare-deps.ts create mode 100644 packages/autorest.typescript/test/rlcIntegration/package.json create mode 100644 packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/rest/pollingHelper.ts create mode 100644 packages/typespec-test/test/openai_modular/generated/typespec-ts/src/rest/pollingHelper.ts create mode 100644 packages/typespec-test/test/widget_dpg/generated/typespec-ts/sources/generated/src/rest/pollingHelper.ts create mode 100644 packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/rest/pollingHelper.ts delete mode 100644 packages/typespec-ts/test/commands/prepare-deps.ts rename packages/typespec-ts/test/integration/generated/lro/lroRPC/src/{legacyClient.ts => rpcClient.ts} (77%) create mode 100644 packages/typespec-ts/test/integration/lroRpc.spec.ts create mode 100644 packages/typespec-ts/test/modularIntegration/generated/lro/rpc/generated/src/rest/pollingHelper.ts create mode 100644 packages/typespec-ts/test/modularIntegration/generated/lro/standard/generated/src/rest/pollingHelper.ts diff --git a/.scripts/build.yml b/.scripts/build.yml index f3e8f705e9..cc582f4b04 100644 --- a/.scripts/build.yml +++ b/.scripts/build.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "18.x" + versionSpec: "18.19.x" displayName: "Use Node 18" - script: | npm install -g @microsoft/rush diff --git a/.scripts/ci.yml b/.scripts/ci.yml index 9853ad54ec..623081eb56 100644 --- a/.scripts/ci.yml +++ b/.scripts/ci.yml @@ -104,11 +104,11 @@ stages: macOS_Node18: Pool: OSVmImage: "macOS-latest" - NodeTestVersion: "18.x" + NodeTestVersion: "18.19.x" Linux_Node18: Pool: ${{ parameters.LinuxPool }} OSVmImage: "ubuntu-20.04" - NodeTestVersion: "18.x" + NodeTestVersion: "18.19.x" Windows_Latest: Pool: OSVmImage: "Windows-latest" @@ -134,11 +134,11 @@ stages: macOS_Node18: Pool: OSVmImage: "macOS-latest" - NodeTestVersion: "18.x" + NodeTestVersion: "18.19.x" Linux_Node18: Pool: ${{ parameters.LinuxPool }} OSVmImage: "ubuntu-20.04" - NodeTestVersion: "18.x" + NodeTestVersion: "18.19.x" Windows_Latest: Pool: OSVmImage: "Windows-latest" @@ -169,11 +169,11 @@ stages: macOS_Node18: Pool: OSVmImage: "macOS-latest" - NodeTestVersion: "18.x" + NodeTestVersion: "18.19.x" Linux_Node18: Pool: ${{ parameters.LinuxPool }} OSVmImage: "ubuntu-20.04" - NodeTestVersion: "18.x" + NodeTestVersion: "18.19.x" pool: name: $[coalesce(variables['Pool'], '')] vmImage: $[coalesce(variables['OSVmImage'], '')] @@ -231,11 +231,11 @@ stages: macOS_Node18: Pool: OSVmImage: "macOS-latest" - NodeTestVersion: "18.x" + NodeTestVersion: "18.19.x" Linux_Node18: Pool: ${{ parameters.LinuxPool }} OSVmImage: "ubuntu-20.04" - NodeTestVersion: "18.x" + NodeTestVersion: "18.19.x" Windows_Latest: Pool: OSVmImage: "Windows-latest" @@ -261,11 +261,11 @@ stages: macOS_Node18: Pool: OSVmImage: "macOS-latest" - NodeTestVersion: "18.x" + NodeTestVersion: "18.19.x" Linux_Node18: Pool: ${{ parameters.LinuxPool }} OSVmImage: "ubuntu-20.04" - NodeTestVersion: "18.x" + NodeTestVersion: "18.19.x" # Windows_Latest: # Pool: # OSVmImage: "Windows-latest" diff --git a/.scripts/release.yml b/.scripts/release.yml index d25188456b..33cef268fc 100644 --- a/.scripts/release.yml +++ b/.scripts/release.yml @@ -36,7 +36,7 @@ stages: steps: - task: NodeTool@0 inputs: - versionSpec: "18.x" + versionSpec: "18.19.x" displayName: "Install Node.js" - script: | tar zxvf $(Pipeline.Workspace)/packages/$(TAR_NAME) @@ -59,7 +59,7 @@ stages: steps: - task: NodeTool@0 inputs: - versionSpec: "18.x" + versionSpec: "18.19.x" displayName: "Install Node.js" - script: | cd $(Pipeline.Workspace)/packages @@ -90,7 +90,7 @@ stages: steps: - task: NodeTool@0 inputs: - versionSpec: "18.x" + versionSpec: "18.19.x" displayName: "Install Node.js" - script: | tar zxvf $(Pipeline.Workspace)/packages/$(TAR_NAME) @@ -113,7 +113,7 @@ stages: steps: - task: NodeTool@0 inputs: - versionSpec: "18.x" + versionSpec: "18.19.x" displayName: "Install Node.js" - script: | cd $(Pipeline.Workspace)/packages @@ -145,7 +145,7 @@ stages: steps: - task: NodeTool@0 inputs: - versionSpec: "18.x" + versionSpec: "18.19.x" displayName: "Install Node.js" - script: | tar zxvf $(Pipeline.Workspace)/packages/$(TAR_NAME) @@ -168,7 +168,7 @@ stages: steps: - task: NodeTool@0 inputs: - versionSpec: "18.x" + versionSpec: "18.19.x" displayName: "Install Node.js" - script: | cd $(Pipeline.Workspace)/packages @@ -196,7 +196,7 @@ stages: steps: - task: NodeTool@0 inputs: - versionSpec: "18.x" + versionSpec: "18.19.x" displayName: "Install Node.js" - script: | tar zxvf $(Pipeline.Workspace)/packages/$(TAR_NAME) @@ -221,7 +221,7 @@ stages: steps: - task: NodeTool@0 inputs: - versionSpec: "18.x" + versionSpec: "18.19.x" displayName: "Install Node.js" - script: | tar zxvf $(Pipeline.Workspace)/packages/$(TAR_NAME) @@ -246,7 +246,7 @@ stages: steps: - task: NodeTool@0 inputs: - versionSpec: "18.x" + versionSpec: "18.19.x" displayName: "Install Node.js" - script: | tar zxvf $(Pipeline.Workspace)/packages/$(TAR_NAME) @@ -270,7 +270,7 @@ stages: steps: - task: NodeTool@0 inputs: - versionSpec: "18.x" + versionSpec: "18.19.x" displayName: "Install Node.js" - script: | cd $(Pipeline.Workspace)/packages @@ -293,7 +293,7 @@ stages: steps: - task: NodeTool@0 inputs: - versionSpec: "18.x" + versionSpec: "18.19.x" displayName: "Install Node.js" - script: | cd $(Pipeline.Workspace)/packages @@ -316,7 +316,7 @@ stages: steps: - task: NodeTool@0 inputs: - versionSpec: "18.x" + versionSpec: "18.19.x" displayName: "Install Node.js" - script: | cd $(Pipeline.Workspace)/packages diff --git a/.scripts/smoke-test.yml b/.scripts/smoke-test.yml index 6f2f6138b0..13cde6caa2 100644 --- a/.scripts/smoke-test.yml +++ b/.scripts/smoke-test.yml @@ -11,11 +11,11 @@ jobs: macOS_Node18: Pool: OSVmImage: "macOS-latest" - NodeTestVersion: "18.x" + NodeTestVersion: "18.19.x" Linux_Node18: Pool: ${{ parameters.LinuxPool }} OSVmImage: "ubuntu-20.04" - NodeTestVersion: "18.x" + NodeTestVersion: "18.19.x" Windows_Latest: Pool: OSVmImage: "Windows-latest" diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 8148b3b4a5..8008c0eae1 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -235,7 +235,7 @@ importers: '@azure-tools/typespec-client-generator-core': '>=0.40.0 <1.0.0' '@azure/abort-controller': ^2.0.0 '@azure/core-auth': ^1.6.0 - '@azure/core-lro': ^2.5.4 + '@azure/core-lro': 3.0.0-beta.1 '@azure/core-paging': ^1.5.0 '@azure/core-rest-pipeline': ^1.14.0 '@azure/core-util': ^1.4.0 @@ -280,7 +280,7 @@ importers: '@azure-tools/typespec-client-generator-core': 0.40.0_7d65p6cqvelxcgmavq4zy7blpy '@azure/abort-controller': 2.0.0 '@azure/core-auth': 1.6.0 - '@azure/core-lro': 2.5.4 + '@azure/core-lro': 3.0.0-beta.1 '@azure/core-paging': 1.5.0 '@azure/core-rest-pipeline': 1.14.0 '@azure/core-util': 1.5.0 @@ -627,6 +627,16 @@ packages: '@azure/logger': 1.0.4 tslib: 2.6.2 + /@azure/core-lro/3.0.0-beta.1: + resolution: {integrity: sha512-KbkVHWhLnlDQRtIZGCPQFB6y2xgeL7p5iICRYTCDjHBpWX9W/I3HkkvbvK4SordNsKwWbLKfPLrOJKJdsJocEQ==} + engines: {node: '>=18.0.0'} + dependencies: + '@azure/abort-controller': 2.0.0 + '@azure/core-util': 1.5.0 + '@azure/logger': 1.0.4 + tslib: 2.6.2 + dev: true + /@azure/core-paging/1.5.0: resolution: {integrity: sha512-zqWdVIt+2Z+3wqxEOGzR5hXFZ8MGKK52x4vFLw8n58pR6ZfKRx3EXYTxTaYxYHc/PexPUTyimcTWFJbji9Z6Iw==} engines: {node: '>=14.0.0'} diff --git a/packages/autorest.typescript/package.json b/packages/autorest.typescript/package.json index fd0afce969..fce4273d4b 100644 --- a/packages/autorest.typescript/package.json +++ b/packages/autorest.typescript/package.json @@ -3,7 +3,8 @@ "version": "6.0.18", "scripts": { "build": "tsc -p . && npm run copyFiles", - "build:test:browser": "tsc -p tsconfig.browser-test.json && webpack --config webpack.config.test.js", + "build:test:browser:rlc": "tsc -p tsconfig.browser-test.json && ts-node test/commands/prepare-deps.ts --browser && webpack --config webpack.config.test.js --env mode=rlc", + "build:test:browser": "tsc -p tsconfig.browser-test.json && ts-node test/commands/prepare-deps.ts --removal && webpack --config webpack.config.test.js --env mode=hlc", "check:tree": "ts-node ./test/commands/check-clean-tree.ts", "pack": "npm pack 2>&1", "clean": "rimraf --glob test-browser test/**/node_modules", @@ -12,7 +13,7 @@ "test:node": "npm run unit-test && npm run start-test-server:v2 & npm run integration-test:alone & npm run rlc-integration-test:alone & npm run test-version-tolerance & npm run stop-test-server", "test:node:alone": "npm run unit-test && npm run integration-test:alone && npm run rlc-integration-test:alone && npm run test-version-tolerance", "test:browser": "npm run start-test-server:v2 & npm run integration-test:browser & npm run rlc-integration-test:browser & npm run test-version-tolerance & npm run stop-test-server", - "test:browser:alone": "npm run integration-test:browser && npm run rlc-integration-test:browser && npm run test-version-tolerance", + "test:browser:alone": "npm run integration-test:browser && npm run rlc-integration-test:browser && npm run test-version-tolerance", "rlc-test:node": "npm run unit-test && npm run start-test-server:v2 & npm run rlc-integration-test:alone && npm run stop-test-server", "rlc-test:browser": "npm run start-test-server:v2 & npm run rlc-integration-test:browser && npm run stop-test-server", "unit-test": "mocha -r ts-node/register \"./test/unit/**/*spec.ts\"", @@ -23,9 +24,9 @@ "integration-test:alone": "mocha -r ts-node/register --timeout 2000 \"./test/integration/**/!(sampleTest).spec.ts\"", "rlc-integration-test": "npm run start-test-server:v2 & npm run rlc-generate-and-test && npm run stop-test-server", "rlc-integration-test:new": "npm-run-all start-test-server rlc-generate-and-test rlc-integration-test:alone stop-test-server", - "rlc-integration-test:browser": "npm run build:test:browser && karma start karma.conf.js", + "rlc-integration-test:browser": "npm run build:test:browser:rlc && karma start karma.conf.js", "rlc-generate-and-test": "npm-run-all -s build rlc-generate-swaggers rlc-integration-test:alone rlc-integration-test:browser", - "rlc-integration-test:alone": "mocha -r ts-node/register --timeout 2000 \"./test/rlcIntegration/**/!(sampleTest).spec.ts\"", + "rlc-integration-test:alone": "ts-node test/commands/prepare-deps.ts && mocha -r ts-node/register --timeout 2000 \"./test/rlcIntegration/!(sampleTest).spec.ts\"", "start-test-server": "ts-node test/commands/start-server.ts", "start-test-server:v2": "autorest-testserver run --appendCoverage", "stop-test-server": "autorest-testserver stop", diff --git a/packages/autorest.typescript/src/restLevelClient/transforms/transformPaths.ts b/packages/autorest.typescript/src/restLevelClient/transforms/transformPaths.ts index 5055fe1d1d..8e83785d83 100644 --- a/packages/autorest.typescript/src/restLevelClient/transforms/transformPaths.ts +++ b/packages/autorest.typescript/src/restLevelClient/transforms/transformPaths.ts @@ -94,7 +94,7 @@ export function transformPaths(model: CodeModel): Paths { lroDetails: { isLongRunning: isLongRunningOperation(operation) }, - isPageable: isPagingOperation(operation) + isPaging: isPagingOperation(operation) } }; diff --git a/packages/autorest.typescript/test/commands/browser.package.json b/packages/autorest.typescript/test/commands/browser.package.json new file mode 100644 index 0000000000..88277586e5 --- /dev/null +++ b/packages/autorest.typescript/test/commands/browser.package.json @@ -0,0 +1,10 @@ +{ + "browser": { + "./utils/stream-helpers.js": "./utils/stream-helpers.browser.js", + "./utils/fileSystem.js": "./utils/fileSystem.browser.js", + "./utils/path.js": "./utils/path.browser.js" + }, + "dependencies": { + "@azure/core-lro": "3.0.0-beta.1" + } +} diff --git a/packages/autorest.typescript/test/commands/prepare-deps.ts b/packages/autorest.typescript/test/commands/prepare-deps.ts new file mode 100644 index 0000000000..9af533d5da --- /dev/null +++ b/packages/autorest.typescript/test/commands/prepare-deps.ts @@ -0,0 +1,85 @@ +import { spawn } from "child_process"; +import { existsSync } from "fs"; +import { join } from "path"; + +async function main() { + const isBrowserTest = process.argv.includes("--browser"); + const isRemoval = process.argv.includes("--removal"); + if (isBrowserTest) { + await copyPackageJson(); + await installDependencies(join(`${__dirname}`, "..", "..", "test-browser")); + } else if (isRemoval) { + await removeFiles([ + join(`${__dirname}`, "..", "..", "test-browser", "package.json"), + join(`${__dirname}`, "..", "..", "test-browser", "node_modules") + ]); + } else { + await installDependencies( + join(`${__dirname}`, "..", "..", "test", "rlcIntegration") + ); + } +} + +async function removeFiles(files: string[]) { + const existing = files.filter((file) => existsSync(file)); + if (existing.length === 0) { + console.log("No dependencies to remove"); + return; + } + runCommand("rm", ["-rf", ...existing]); + console.log("Removed dependencies for hlc browser tests", existing); +} + +async function copyPackageJson() { + const srcPath = join( + `${__dirname}`, + "..", + "..", + "test", + "commands", + "browser.package.json" + ); + const destPath = join( + `${__dirname}`, + "..", + "..", + "test-browser", + "package.json" + ); + await runCommand("cp", [srcPath, destPath]); +} + +async function installDependencies(path: string) { + await runCommand( + `npm${/^win/.test(process.platform) ? ".cmd" : ""}`, + ["install"], + path + ); + console.log("Installed dependencies for rlc browser tests", path); +} + +async function runCommand(command: string, args: string[] = [], cwd = ".") { + return new Promise((resolve, reject) => { + const process = spawn(command, args, { cwd, shell: true }); + + let stdout = ""; + let stderr = ""; + + process.stdout.on("data", (data) => (stdout += data.toString())); + process.stderr.on("data", (data) => (stderr += data.toString())); + + process.on("close", () => { + resolve(stdout ?? stderr); + }); + + process.on("error", (error: any) => { + console.log(stdout, stderr, error); + reject(new Error(error)); + }); + }); +} + +main().catch((error) => { + console.error(error); + process.exit(-1); +}); diff --git a/packages/autorest.typescript/test/commands/run.ts b/packages/autorest.typescript/test/commands/run.ts index ecde75291c..338b0ca6f5 100644 --- a/packages/autorest.typescript/test/commands/run.ts +++ b/packages/autorest.typescript/test/commands/run.ts @@ -185,4 +185,4 @@ export async function runAutorest( console.error(error); throw error; } -} +} \ No newline at end of file diff --git a/packages/autorest.typescript/test/commands/test-swagger-gen.ts b/packages/autorest.typescript/test/commands/test-swagger-gen.ts index 09b8e5ac37..74a35f27d1 100644 --- a/packages/autorest.typescript/test/commands/test-swagger-gen.ts +++ b/packages/autorest.typescript/test/commands/test-swagger-gen.ts @@ -1290,4 +1290,4 @@ const run = async () => { run().catch(error => { console.error(error); process.exit(-1000); -}); +}); \ No newline at end of file diff --git a/packages/autorest.typescript/test/rlcIntegration/generated/dpgCustomization/package.json b/packages/autorest.typescript/test/rlcIntegration/generated/dpgCustomization/package.json index 50db508a7a..eff97a7952 100644 --- a/packages/autorest.typescript/test/rlcIntegration/generated/dpgCustomization/package.json +++ b/packages/autorest.typescript/test/rlcIntegration/generated/dpgCustomization/package.json @@ -45,8 +45,8 @@ "@azure/core-rest-pipeline": "^1.5.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2", - "@azure/core-lro": "^2.5.4", - "@azure/abort-controller": "^1.0.0", + "@azure/core-lro": "3.0.0-beta.1", + "@azure/abort-controller": "^2.0.0", "@azure/core-paging": "^1.5.0" }, "devDependencies": { diff --git a/packages/autorest.typescript/test/rlcIntegration/generated/dpgCustomization/src/pollingHelper.ts b/packages/autorest.typescript/test/rlcIntegration/generated/dpgCustomization/src/pollingHelper.ts index 366c6527c4..8cc2131521 100644 --- a/packages/autorest.typescript/test/rlcIntegration/generated/dpgCustomization/src/pollingHelper.ts +++ b/packages/autorest.typescript/test/rlcIntegration/generated/dpgCustomization/src/pollingHelper.ts @@ -2,14 +2,85 @@ // Licensed under the MIT license. import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; import { + CancelOnProgress, CreateHttpPollerOptions, LongRunningOperation, - LroResponse, + OperationResponse, OperationState, - SimplePollerLike, createHttpPoller, } from "@azure/core-lro"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + /** * Helper function that builds a Poller object to help polling a long running operation. * @param client - Client to use for sending the request to get additional pages. @@ -22,23 +93,42 @@ export async function getLongRunningPoller( initialResponse: TResult, options: CreateHttpPollerOptions> = {}, ): Promise, TResult>> { + const abortController = new AbortController(); const poller: LongRunningOperation = { - requestMethod: initialResponse.request.method, - requestPath: initialResponse.request.url, sendInitialRequest: async () => { // In the case of Rest Clients we are building the LRO poller object from a response that's the reason // we are not triggering the initial request here, just extracting the information from the // response we were provided. return getLroResponse(initialResponse); }, - sendPollRequest: async (path) => { + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { // This is the callback that is going to be called to poll the service // to get the latest status. We use the client provided and the polling path // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location // depending on the lro pattern that the service implements. If non is provided we default to the initial path. - const response = await client - .pathUnchecked(path ?? initialResponse.request.url) - .get(); + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } const lroResponse = getLroResponse(response as TResult); lroResponse.rawResponse.headers["x-ms-original-url"] = initialResponse.request.url; @@ -47,7 +137,45 @@ export async function getLongRunningPoller( }; options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; - return createHttpPoller(poller, options); + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; } /** @@ -57,7 +185,7 @@ export async function getLongRunningPoller( */ function getLroResponse( response: TResult, -): LroResponse { +): OperationResponse { if (Number.isNaN(response.status)) { throw new TypeError( `Status code of the response is not a number. Value: ${response.status}`, diff --git a/packages/autorest.typescript/test/rlcIntegration/generated/lroRest/package.json b/packages/autorest.typescript/test/rlcIntegration/generated/lroRest/package.json index 2ff3e3b1c5..357f0c0df3 100644 --- a/packages/autorest.typescript/test/rlcIntegration/generated/lroRest/package.json +++ b/packages/autorest.typescript/test/rlcIntegration/generated/lroRest/package.json @@ -27,8 +27,8 @@ "@azure/core-rest-pipeline": "^1.5.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2", - "@azure/core-lro": "^2.5.4", - "@azure/abort-controller": "^1.0.0" + "@azure/core-lro": "3.0.0-beta.1", + "@azure/abort-controller": "^2.0.0" }, "devDependencies": { "dotenv": "^16.0.0", diff --git a/packages/autorest.typescript/test/rlcIntegration/generated/lroRest/src/pollingHelper.ts b/packages/autorest.typescript/test/rlcIntegration/generated/lroRest/src/pollingHelper.ts index 366c6527c4..8cc2131521 100644 --- a/packages/autorest.typescript/test/rlcIntegration/generated/lroRest/src/pollingHelper.ts +++ b/packages/autorest.typescript/test/rlcIntegration/generated/lroRest/src/pollingHelper.ts @@ -2,14 +2,85 @@ // Licensed under the MIT license. import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; import { + CancelOnProgress, CreateHttpPollerOptions, LongRunningOperation, - LroResponse, + OperationResponse, OperationState, - SimplePollerLike, createHttpPoller, } from "@azure/core-lro"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + /** * Helper function that builds a Poller object to help polling a long running operation. * @param client - Client to use for sending the request to get additional pages. @@ -22,23 +93,42 @@ export async function getLongRunningPoller( initialResponse: TResult, options: CreateHttpPollerOptions> = {}, ): Promise, TResult>> { + const abortController = new AbortController(); const poller: LongRunningOperation = { - requestMethod: initialResponse.request.method, - requestPath: initialResponse.request.url, sendInitialRequest: async () => { // In the case of Rest Clients we are building the LRO poller object from a response that's the reason // we are not triggering the initial request here, just extracting the information from the // response we were provided. return getLroResponse(initialResponse); }, - sendPollRequest: async (path) => { + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { // This is the callback that is going to be called to poll the service // to get the latest status. We use the client provided and the polling path // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location // depending on the lro pattern that the service implements. If non is provided we default to the initial path. - const response = await client - .pathUnchecked(path ?? initialResponse.request.url) - .get(); + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } const lroResponse = getLroResponse(response as TResult); lroResponse.rawResponse.headers["x-ms-original-url"] = initialResponse.request.url; @@ -47,7 +137,45 @@ export async function getLongRunningPoller( }; options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; - return createHttpPoller(poller, options); + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; } /** @@ -57,7 +185,7 @@ export async function getLongRunningPoller( */ function getLroResponse( response: TResult, -): LroResponse { +): OperationResponse { if (Number.isNaN(response.status)) { throw new TypeError( `Status code of the response is not a number. Value: ${response.status}`, diff --git a/packages/autorest.typescript/test/rlcIntegration/generated/pagingRest/package.json b/packages/autorest.typescript/test/rlcIntegration/generated/pagingRest/package.json index ea7f30178b..e8ae5aa7cf 100644 --- a/packages/autorest.typescript/test/rlcIntegration/generated/pagingRest/package.json +++ b/packages/autorest.typescript/test/rlcIntegration/generated/pagingRest/package.json @@ -27,8 +27,8 @@ "@azure/core-rest-pipeline": "^1.5.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2", - "@azure/core-lro": "^2.5.4", - "@azure/abort-controller": "^1.0.0", + "@azure/core-lro": "3.0.0-beta.1", + "@azure/abort-controller": "^2.0.0", "@azure/core-paging": "^1.5.0" }, "devDependencies": { diff --git a/packages/autorest.typescript/test/rlcIntegration/generated/pagingRest/src/pollingHelper.ts b/packages/autorest.typescript/test/rlcIntegration/generated/pagingRest/src/pollingHelper.ts index 366c6527c4..8cc2131521 100644 --- a/packages/autorest.typescript/test/rlcIntegration/generated/pagingRest/src/pollingHelper.ts +++ b/packages/autorest.typescript/test/rlcIntegration/generated/pagingRest/src/pollingHelper.ts @@ -2,14 +2,85 @@ // Licensed under the MIT license. import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; import { + CancelOnProgress, CreateHttpPollerOptions, LongRunningOperation, - LroResponse, + OperationResponse, OperationState, - SimplePollerLike, createHttpPoller, } from "@azure/core-lro"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + /** * Helper function that builds a Poller object to help polling a long running operation. * @param client - Client to use for sending the request to get additional pages. @@ -22,23 +93,42 @@ export async function getLongRunningPoller( initialResponse: TResult, options: CreateHttpPollerOptions> = {}, ): Promise, TResult>> { + const abortController = new AbortController(); const poller: LongRunningOperation = { - requestMethod: initialResponse.request.method, - requestPath: initialResponse.request.url, sendInitialRequest: async () => { // In the case of Rest Clients we are building the LRO poller object from a response that's the reason // we are not triggering the initial request here, just extracting the information from the // response we were provided. return getLroResponse(initialResponse); }, - sendPollRequest: async (path) => { + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { // This is the callback that is going to be called to poll the service // to get the latest status. We use the client provided and the polling path // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location // depending on the lro pattern that the service implements. If non is provided we default to the initial path. - const response = await client - .pathUnchecked(path ?? initialResponse.request.url) - .get(); + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } const lroResponse = getLroResponse(response as TResult); lroResponse.rawResponse.headers["x-ms-original-url"] = initialResponse.request.url; @@ -47,7 +137,45 @@ export async function getLongRunningPoller( }; options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; - return createHttpPoller(poller, options); + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; } /** @@ -57,7 +185,7 @@ export async function getLongRunningPoller( */ function getLroResponse( response: TResult, -): LroResponse { +): OperationResponse { if (Number.isNaN(response.status)) { throw new TypeError( `Status code of the response is not a number. Value: ${response.status}`, diff --git a/packages/autorest.typescript/test/rlcIntegration/lroRest.spec.ts b/packages/autorest.typescript/test/rlcIntegration/lroRest.spec.ts index de21c7eb23..2771450449 100644 --- a/packages/autorest.typescript/test/rlcIntegration/lroRest.spec.ts +++ b/packages/autorest.typescript/test/rlcIntegration/lroRest.spec.ts @@ -192,6 +192,22 @@ describe("LRO Rest Client", () => { assert.deepEqual(result.body, { id: "100", name: "foo" }); }); + it("should handle put202Retry200", async () => { + const initialResponse = await client.path("/lro/put/202/retry/200").put(); + const poller = await getLongRunningPoller(client, initialResponse, { + intervalInMs: 0 + }); + + try { + const promise = poller.pollUntilDone(); + poller.stopPolling(); + await promise; + assert.fail("Should be aborted by stopPolling"); + } catch (e) { + assert.equal(e.message, "The operation was aborted."); + } + }); + it("should handle putNoHeaderInRetry", async () => { const initialResponse = await client .path("/lro/put/noheader/202/200") diff --git a/packages/autorest.typescript/test/rlcIntegration/package.json b/packages/autorest.typescript/test/rlcIntegration/package.json new file mode 100644 index 0000000000..7db45ccfdc --- /dev/null +++ b/packages/autorest.typescript/test/rlcIntegration/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@azure/core-lro": "3.0.0-beta.1" + } +} diff --git a/packages/autorest.typescript/test/smoke/generated/agrifood-data-plane/package.json b/packages/autorest.typescript/test/smoke/generated/agrifood-data-plane/package.json index 04616d205d..32434b7b95 100644 --- a/packages/autorest.typescript/test/smoke/generated/agrifood-data-plane/package.json +++ b/packages/autorest.typescript/test/smoke/generated/agrifood-data-plane/package.json @@ -27,8 +27,8 @@ "@azure/core-rest-pipeline": "^1.5.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2", - "@azure/core-lro": "^2.5.4", - "@azure/abort-controller": "^1.0.0", + "@azure/core-lro": "3.0.0-beta.1", + "@azure/abort-controller": "^2.0.0", "@azure/core-paging": "^1.5.0" }, "devDependencies": { diff --git a/packages/autorest.typescript/test/smoke/generated/agrifood-data-plane/review/agrifood-data-plane.api.md b/packages/autorest.typescript/test/smoke/generated/agrifood-data-plane/review/agrifood-data-plane.api.md index bdb5be790b..06b24cf2ad 100644 --- a/packages/autorest.typescript/test/smoke/generated/agrifood-data-plane/review/agrifood-data-plane.api.md +++ b/packages/autorest.typescript/test/smoke/generated/agrifood-data-plane/review/agrifood-data-plane.api.md @@ -6,6 +6,8 @@ /// +import { AbortSignalLike } from '@azure/abort-controller'; +import { CancelOnProgress } from '@azure/core-lro'; import { Client } from '@azure-rest/core-client'; import { ClientOptions } from '@azure-rest/core-client'; import { CreateHttpPollerOptions } from '@azure/core-lro'; @@ -15,7 +17,6 @@ import { OperationState } from '@azure/core-lro'; import { PagedAsyncIterableIterator } from '@azure/core-paging'; import { PathUncheckedResponse } from '@azure-rest/core-client'; import { RequestParameters } from '@azure-rest/core-client'; -import { SimplePollerLike } from '@azure/core-lro'; import { StreamableMethod } from '@azure-rest/core-client'; // @public @@ -4209,6 +4210,27 @@ export interface SeasonsListQueryParamProperties { years?: string; } +// @public +export interface SimplePollerLike, TResult> { + getOperationState(): TState; + getResult(): TResult | undefined; + isDone(): boolean; + isStopped(): boolean; + onProgress(callback: (state: TState) => void): CancelOnProgress; + poll(options?: { + abortSignal?: AbortSignalLike; + }): Promise; + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + serialize(): Promise; + // @deprecated + stopPolling(): void; + submitted(): Promise; + // @deprecated + toString(): string; +} + // @public export interface TillageData { area?: Measure; diff --git a/packages/autorest.typescript/test/smoke/generated/agrifood-data-plane/src/pollingHelper.ts b/packages/autorest.typescript/test/smoke/generated/agrifood-data-plane/src/pollingHelper.ts index 366c6527c4..8cc2131521 100644 --- a/packages/autorest.typescript/test/smoke/generated/agrifood-data-plane/src/pollingHelper.ts +++ b/packages/autorest.typescript/test/smoke/generated/agrifood-data-plane/src/pollingHelper.ts @@ -2,14 +2,85 @@ // Licensed under the MIT license. import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; import { + CancelOnProgress, CreateHttpPollerOptions, LongRunningOperation, - LroResponse, + OperationResponse, OperationState, - SimplePollerLike, createHttpPoller, } from "@azure/core-lro"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + /** * Helper function that builds a Poller object to help polling a long running operation. * @param client - Client to use for sending the request to get additional pages. @@ -22,23 +93,42 @@ export async function getLongRunningPoller( initialResponse: TResult, options: CreateHttpPollerOptions> = {}, ): Promise, TResult>> { + const abortController = new AbortController(); const poller: LongRunningOperation = { - requestMethod: initialResponse.request.method, - requestPath: initialResponse.request.url, sendInitialRequest: async () => { // In the case of Rest Clients we are building the LRO poller object from a response that's the reason // we are not triggering the initial request here, just extracting the information from the // response we were provided. return getLroResponse(initialResponse); }, - sendPollRequest: async (path) => { + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { // This is the callback that is going to be called to poll the service // to get the latest status. We use the client provided and the polling path // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location // depending on the lro pattern that the service implements. If non is provided we default to the initial path. - const response = await client - .pathUnchecked(path ?? initialResponse.request.url) - .get(); + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } const lroResponse = getLroResponse(response as TResult); lroResponse.rawResponse.headers["x-ms-original-url"] = initialResponse.request.url; @@ -47,7 +137,45 @@ export async function getLongRunningPoller( }; options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; - return createHttpPoller(poller, options); + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; } /** @@ -57,7 +185,7 @@ export async function getLongRunningPoller( */ function getLroResponse( response: TResult, -): LroResponse { +): OperationResponse { if (Number.isNaN(response.status)) { throw new TypeError( `Status code of the response is not a number. Value: ${response.status}`, diff --git a/packages/autorest.typescript/test/smoke/generated/anomaly-detector-rest/package.json b/packages/autorest.typescript/test/smoke/generated/anomaly-detector-rest/package.json index 9584254d02..047bc7b259 100644 --- a/packages/autorest.typescript/test/smoke/generated/anomaly-detector-rest/package.json +++ b/packages/autorest.typescript/test/smoke/generated/anomaly-detector-rest/package.json @@ -27,8 +27,8 @@ "@azure/core-rest-pipeline": "^1.5.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2", - "@azure/core-lro": "^2.5.4", - "@azure/abort-controller": "^1.0.0", + "@azure/core-lro": "3.0.0-beta.1", + "@azure/abort-controller": "^2.0.0", "@azure/core-paging": "^1.5.0" }, "devDependencies": { diff --git a/packages/autorest.typescript/test/smoke/generated/anomaly-detector-rest/review/anomaly-detector-rest.api.md b/packages/autorest.typescript/test/smoke/generated/anomaly-detector-rest/review/anomaly-detector-rest.api.md index 4b0aa1ffae..aca154dc1d 100644 --- a/packages/autorest.typescript/test/smoke/generated/anomaly-detector-rest/review/anomaly-detector-rest.api.md +++ b/packages/autorest.typescript/test/smoke/generated/anomaly-detector-rest/review/anomaly-detector-rest.api.md @@ -4,6 +4,8 @@ ```ts +import { AbortSignalLike } from '@azure/abort-controller'; +import { CancelOnProgress } from '@azure/core-lro'; import { Client } from '@azure-rest/core-client'; import { ClientOptions } from '@azure-rest/core-client'; import { CreateHttpPollerOptions } from '@azure/core-lro'; @@ -14,7 +16,6 @@ import { PagedAsyncIterableIterator } from '@azure/core-paging'; import { PathUncheckedResponse } from '@azure-rest/core-client'; import { RawHttpHeaders } from '@azure/core-rest-pipeline'; import { RequestParameters } from '@azure-rest/core-client'; -import { SimplePollerLike } from '@azure/core-lro'; import { StreamableMethod } from '@azure-rest/core-client'; // @public @@ -728,6 +729,27 @@ export interface Routes { (path: "/multivariate/models/{modelId}:detect-last", modelId: string): LastDetectAnomaly; } +// @public +export interface SimplePollerLike, TResult> { + getOperationState(): TState; + getResult(): TResult | undefined; + isDone(): boolean; + isStopped(): boolean; + onProgress(callback: (state: TState) => void): CancelOnProgress; + poll(options?: { + abortSignal?: AbortSignalLike; + }): Promise; + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + serialize(): Promise; + // @deprecated + stopPolling(): void; + submitted(): Promise; + // @deprecated + toString(): string; +} + // @public export interface TimeSeriesPoint { timestamp?: Date | string; diff --git a/packages/autorest.typescript/test/smoke/generated/anomaly-detector-rest/src/pollingHelper.ts b/packages/autorest.typescript/test/smoke/generated/anomaly-detector-rest/src/pollingHelper.ts index 366c6527c4..8cc2131521 100644 --- a/packages/autorest.typescript/test/smoke/generated/anomaly-detector-rest/src/pollingHelper.ts +++ b/packages/autorest.typescript/test/smoke/generated/anomaly-detector-rest/src/pollingHelper.ts @@ -2,14 +2,85 @@ // Licensed under the MIT license. import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; import { + CancelOnProgress, CreateHttpPollerOptions, LongRunningOperation, - LroResponse, + OperationResponse, OperationState, - SimplePollerLike, createHttpPoller, } from "@azure/core-lro"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + /** * Helper function that builds a Poller object to help polling a long running operation. * @param client - Client to use for sending the request to get additional pages. @@ -22,23 +93,42 @@ export async function getLongRunningPoller( initialResponse: TResult, options: CreateHttpPollerOptions> = {}, ): Promise, TResult>> { + const abortController = new AbortController(); const poller: LongRunningOperation = { - requestMethod: initialResponse.request.method, - requestPath: initialResponse.request.url, sendInitialRequest: async () => { // In the case of Rest Clients we are building the LRO poller object from a response that's the reason // we are not triggering the initial request here, just extracting the information from the // response we were provided. return getLroResponse(initialResponse); }, - sendPollRequest: async (path) => { + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { // This is the callback that is going to be called to poll the service // to get the latest status. We use the client provided and the polling path // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location // depending on the lro pattern that the service implements. If non is provided we default to the initial path. - const response = await client - .pathUnchecked(path ?? initialResponse.request.url) - .get(); + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } const lroResponse = getLroResponse(response as TResult); lroResponse.rawResponse.headers["x-ms-original-url"] = initialResponse.request.url; @@ -47,7 +137,45 @@ export async function getLongRunningPoller( }; options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; - return createHttpPoller(poller, options); + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; } /** @@ -57,7 +185,7 @@ export async function getLongRunningPoller( */ function getLroResponse( response: TResult, -): LroResponse { +): OperationResponse { if (Number.isNaN(response.status)) { throw new TypeError( `Status code of the response is not a number. Value: ${response.status}`, diff --git a/packages/autorest.typescript/test/smoke/generated/synapse-artifacts-rest/src/pollingHelper.ts b/packages/autorest.typescript/test/smoke/generated/synapse-artifacts-rest/src/pollingHelper.ts index 366c6527c4..8cc2131521 100644 --- a/packages/autorest.typescript/test/smoke/generated/synapse-artifacts-rest/src/pollingHelper.ts +++ b/packages/autorest.typescript/test/smoke/generated/synapse-artifacts-rest/src/pollingHelper.ts @@ -2,14 +2,85 @@ // Licensed under the MIT license. import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; import { + CancelOnProgress, CreateHttpPollerOptions, LongRunningOperation, - LroResponse, + OperationResponse, OperationState, - SimplePollerLike, createHttpPoller, } from "@azure/core-lro"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + /** * Helper function that builds a Poller object to help polling a long running operation. * @param client - Client to use for sending the request to get additional pages. @@ -22,23 +93,42 @@ export async function getLongRunningPoller( initialResponse: TResult, options: CreateHttpPollerOptions> = {}, ): Promise, TResult>> { + const abortController = new AbortController(); const poller: LongRunningOperation = { - requestMethod: initialResponse.request.method, - requestPath: initialResponse.request.url, sendInitialRequest: async () => { // In the case of Rest Clients we are building the LRO poller object from a response that's the reason // we are not triggering the initial request here, just extracting the information from the // response we were provided. return getLroResponse(initialResponse); }, - sendPollRequest: async (path) => { + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { // This is the callback that is going to be called to poll the service // to get the latest status. We use the client provided and the polling path // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location // depending on the lro pattern that the service implements. If non is provided we default to the initial path. - const response = await client - .pathUnchecked(path ?? initialResponse.request.url) - .get(); + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } const lroResponse = getLroResponse(response as TResult); lroResponse.rawResponse.headers["x-ms-original-url"] = initialResponse.request.url; @@ -47,7 +137,45 @@ export async function getLongRunningPoller( }; options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; - return createHttpPoller(poller, options); + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; } /** @@ -57,7 +185,7 @@ export async function getLongRunningPoller( */ function getLroResponse( response: TResult, -): LroResponse { +): OperationResponse { if (Number.isNaN(response.status)) { throw new TypeError( `Status code of the response is not a number. Value: ${response.status}`, diff --git a/packages/autorest.typescript/webpack.config.test.js b/packages/autorest.typescript/webpack.config.test.js index cbf5016964..8d46883404 100644 --- a/packages/autorest.typescript/webpack.config.test.js +++ b/packages/autorest.typescript/webpack.config.test.js @@ -1,36 +1,27 @@ const { readdirSync, statSync } = require("fs"); -const { join: joinPath, sep, extname } = require("path"); +const { join: joinPath, sep, extname, resolve } = require("path"); const webpack = require("webpack"); -function getIntegrationTestFiles() { - let hlcDirPath = joinPath(__dirname, "test-browser", "integration"); - let hlcFiles = readdirSync(hlcDirPath); - let rlcDirPath = joinPath(__dirname, "test-browser", "rlcIntegration"); - let rlcFiles = readdirSync(rlcDirPath); - hlcFiles = hlcFiles +function getIntegrationTestFiles(env) { + const mode = env.mode ?? "hlc"; + const dirPath = joinPath( + __dirname, + "test-browser", + mode === "hlc" ? "integration" : "rlcIntegration" + ); + let files = readdirSync(dirPath); + files = files .filter( - name => - extname(name) === ".js" && - statSync(`${hlcDirPath}${sep}${name}`).isFile() + (name) => + extname(name) === ".js" && statSync(`${dirPath}${sep}${name}`).isFile() ) - .map(filename => `${hlcDirPath}${sep}${filename}`); + .map((filename) => `${dirPath}${sep}${filename}`); - rlcFiles = rlcFiles - .filter( - name => - extname(name) === ".js" && - statSync(`${rlcDirPath}${sep}${name}`).isFile() - ) - .map(filename => `${rlcDirPath}${sep}${filename}`); - - return [...hlcFiles, ...rlcFiles]; + return [...files]; } - -const entry = getIntegrationTestFiles(); - -module.exports = { +module.exports = (env) => ({ target: "web", - entry, + entry: getIntegrationTestFiles(env), output: { filename: "index.js", path: joinPath(__dirname, "test-browser") @@ -64,4 +55,4 @@ module.exports = { Buffer: ["buffer", "Buffer"] }) ] -}; +}); diff --git a/packages/rlc-common/src/buildIndexFile.ts b/packages/rlc-common/src/buildIndexFile.ts index 406198d3b0..7f8aa93c01 100644 --- a/packages/rlc-common/src/buildIndexFile.ts +++ b/packages/rlc-common/src/buildIndexFile.ts @@ -133,9 +133,7 @@ function generateRLCIndexForMultiClient(file: SourceFile, model: RLCModel) { exports.push("UnexpectedHelper"); } - // TODO: Disable LRO poller tentatively and remember to remove this when new LRO in RLC is ready - // https://github.com/Azure/autorest.typescript/issues/2230 - if (hasPollingOperations(model) && !model.options?.isModularLibrary) { + if (hasPollingOperations(model)) { file.addImportDeclaration({ namespaceImport: "PollingHelper", moduleSpecifier: getImportModuleName( @@ -299,9 +297,7 @@ function generateRLCIndex(file: SourceFile, model: RLCModel) { ]); } - // TODO: Disable LRO poller tentatively - // https://github.com/Azure/autorest.typescript/issues/2230 - if (hasPollingOperations(model) && !model.options?.isModularLibrary) { + if (hasPollingOperations(model)) { file.addExportDeclarations([ { moduleSpecifier: getImportModuleName( diff --git a/packages/rlc-common/src/buildPollingHelper.ts b/packages/rlc-common/src/buildPollingHelper.ts index 698b381982..137a2c641e 100644 --- a/packages/rlc-common/src/buildPollingHelper.ts +++ b/packages/rlc-common/src/buildPollingHelper.ts @@ -14,7 +14,7 @@ interface LroDetail { } interface ResponseMap { - initalResponses: string; + initialResponses: string; finalResponses: string; precedence?: number; } @@ -24,11 +24,6 @@ export function buildPollingHelper(model: RLCModel) { return; } - // TODO: Disable LRO poller tentatively and remember to remove this when new LRO in RLC is ready - // https://github.com/Azure/autorest.typescript/issues/2230 - if (model.options?.isModularLibrary) { - return; - } const lroDetail: LroDetail = buildLroHelperDetail(model); const readmeFileContents = hbs.compile(pollingContent, { noEscape: true }); const { srcPath } = model; @@ -52,20 +47,20 @@ function buildLroHelperDetail(model: RLCModel): LroDetail { for (const methodDetails of Object.values(details.methods)) { const lroDetail = methodDetails[0].operationHelperDetail?.lroDetails; if (lroDetail?.isLongRunning) { - const initalResponses = methodDetails[0].responseTypes.success.concat( + const initialResponses = methodDetails[0].responseTypes.success.concat( methodDetails[0].responseTypes.error ); - const finalRespoonse = lroDetail.logicalResponseTypes?.success.concat( + const finalResponse = lroDetail.logicalResponseTypes?.success.concat( methodDetails[0].responseTypes.error ); - if (initalResponses && finalRespoonse) { - initalResponses.forEach((n) => responses.add(n)); - finalRespoonse.forEach((n) => responses.add(n)); + if (initialResponses && finalResponse) { + initialResponses.forEach((n) => responses.add(n)); + finalResponse.forEach((n) => responses.add(n)); mapDetail!.push({ - initalResponses: initalResponses.join("|"), - finalResponses: finalRespoonse.join("|"), + initialResponses: initialResponses.join("|"), + finalResponses: finalResponse.join("|"), precedence: lroDetail.precedence ?? OPERATION_LRO_HIGH_PRIORITY }); } @@ -73,7 +68,7 @@ function buildLroHelperDetail(model: RLCModel): LroDetail { } } - // Sorted by the precedecne + // Sorted by the precedence mapDetail.sort((d1, d2) => d1.precedence - d2.precedence); return { clientOverload: responses.size > 0 && mapDetail.length > 0, diff --git a/packages/rlc-common/src/interfaces.ts b/packages/rlc-common/src/interfaces.ts index e2eeb53365..ba254b4a7e 100644 --- a/packages/rlc-common/src/interfaces.ts +++ b/packages/rlc-common/src/interfaces.ts @@ -172,7 +172,7 @@ export type PathParameter = { export interface OperationHelperDetail { lroDetails?: OperationLroDetail; - isPageable?: boolean; + isPaging?: boolean; } export const OPERATION_LRO_HIGH_PRIORITY = 0, diff --git a/packages/rlc-common/src/metadata/buildPackageFile.ts b/packages/rlc-common/src/metadata/buildPackageFile.ts index 95a7f210c8..7d71cc54dd 100644 --- a/packages/rlc-common/src/metadata/buildPackageFile.ts +++ b/packages/rlc-common/src/metadata/buildPackageFile.ts @@ -37,9 +37,7 @@ export function buildPackageFile( hasLro: hasPollingOperations(model), hasPaging: hasPagingOperations(model), monorepoPackageDirectory: model.options?.azureOutputDirectory, - specSource: model.options?.sourceFrom ?? "TypeSpec", - // Currently we would use v3 LRO for modular libraries - useV3Lro: model.options?.isModularLibrary ?? false + specSource: model.options?.sourceFrom ?? "TypeSpec" }; if (isAzureMonorepoPackage(model)) { diff --git a/packages/rlc-common/src/metadata/packageJson/azurePackageCommon.ts b/packages/rlc-common/src/metadata/packageJson/azurePackageCommon.ts index f84aa6dd88..b275994d10 100644 --- a/packages/rlc-common/src/metadata/packageJson/azurePackageCommon.ts +++ b/packages/rlc-common/src/metadata/packageJson/azurePackageCommon.ts @@ -10,7 +10,6 @@ export interface AzurePackageInfoConfig extends PackageCommonInfoConfig { hasLro: boolean; hasPaging: boolean; specSource: "Swagger" | "TypeSpec"; - useV3Lro: boolean; } /** @@ -31,8 +30,7 @@ export function getAzureCommonPackageInfo(config: AzurePackageInfoConfig) { */ export function getAzurePackageDependencies({ hasLro, - hasPaging, - useV3Lro + hasPaging }: AzurePackageInfoConfig) { let dependencies: Record = { "@azure-rest/core-client": "^1.2.0", @@ -45,8 +43,8 @@ export function getAzurePackageDependencies({ if (hasLro) { dependencies = { ...dependencies, - "@azure/core-lro": useV3Lro ? "3.0.0-beta.1" : "^2.5.4", - "@azure/abort-controller": useV3Lro ? "^2.0.0" : "^1.0.0" + "@azure/core-lro": "3.0.0-beta.1", + "@azure/abort-controller": "^2.0.0" }; } diff --git a/packages/rlc-common/src/static/pollingContent.ts b/packages/rlc-common/src/static/pollingContent.ts index 25e094d19f..0b16e0cbea 100644 --- a/packages/rlc-common/src/static/pollingContent.ts +++ b/packages/rlc-common/src/static/pollingContent.ts @@ -3,23 +3,14 @@ export const pollingContent = ` import { Client, HttpResponse } from "@azure-rest/core-client"; -{{#if useLegacyLro}} -import { - LongRunningOperation, - LroEngine, - LroEngineOptions, - LroResponse, - PollerLike, - PollOperationState -} from "@azure/core-lro"; -{{else}} +import { AbortSignalLike } from "@azure/abort-controller"; import { + CancelOnProgress, CreateHttpPollerOptions, LongRunningOperation, - LroResponse, + OperationResponse, OperationState, - SimplePollerLike, - createHttpPoller + createHttpPoller, } from "@azure/core-lro"; {{#if clientOverload}} import { @@ -28,7 +19,76 @@ import { {{/each}} } from "./responses{{#if isEsm}}.js{{/if}}"; {{/if}} -{{/if}} + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + /** * Helper function that builds a Poller object to help polling a long running operation. * @param client - Client to use for sending the request to get additional pages. @@ -42,38 +102,52 @@ export async function getLongRunningPoller< TResult extends {{ this.finalResponses }} >( client: Client, - initialResponse: {{ this.initalResponses }}, + initialResponse: {{ this.initialResponses }}, options?: CreateHttpPollerOptions> ): Promise, TResult>>; {{/each}} {{/if}} -export {{#unless useLegacyLro}}async {{/unless}}function getLongRunningPoller( - client: Client, - initialResponse: TResult, - {{#if useLegacyLro}} - options: LroEngineOptions> = {} - ): PollerLike, TResult> { - {{else}} +export async function getLongRunningPoller( + client: Client, + initialResponse: TResult, options: CreateHttpPollerOptions> = {} - ): Promise, TResult>> { - {{/if}} - const poller: LongRunningOperation = { - requestMethod: initialResponse.request.method, - requestPath: initialResponse.request.url, + ): Promise, TResult>> { + const abortController = new AbortController(); + const poller: LongRunningOperation = { sendInitialRequest: async () => { // In the case of Rest Clients we are building the LRO poller object from a response that's the reason // we are not triggering the initial request here, just extracting the information from the // response we were provided. return getLroResponse(initialResponse); }, - sendPollRequest: async path => { + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike } + ) => { // This is the callback that is going to be called to poll the service // to get the latest status. We use the client provided and the polling path // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location // depending on the lro pattern that the service implements. If non is provided we default to the initial path. - const response = await client - .pathUnchecked(path ?? initialResponse.request.url) - .get(); + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } const lroResponse = getLroResponse(response as TResult); lroResponse.rawResponse.headers["x-ms-original-url"] = initialResponse.request.url; @@ -81,12 +155,46 @@ export {{#unless useLegacyLro}}async {{/unless}}function getLongRunningPoller, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState()." + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState()." + ); + } + return JSON.stringify({ + state: httpPoller.operationState + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted + }; + return simplePoller; } /** @@ -96,7 +204,7 @@ export {{#unless useLegacyLro}}async {{/unless}}function getLongRunningPoller( response: TResult -): LroResponse { +): OperationResponse { if (Number.isNaN(response.status)) { throw new TypeError( \`Status code of the response is not a number. Value: \${response.status}\` diff --git a/packages/rlc-common/src/transformSampleGroups.ts b/packages/rlc-common/src/transformSampleGroups.ts index 96ba9cbb12..08916f2236 100644 --- a/packages/rlc-common/src/transformSampleGroups.ts +++ b/packages/rlc-common/src/transformSampleGroups.ts @@ -85,7 +85,7 @@ export function transformSampleGroups(model: RLCModel, allowMockValue = true) { methodParamNames: "", method, isLRO: detail.operationHelperDetail?.lroDetails?.isLongRunning ?? false, - isPaging: detail.operationHelperDetail?.isPageable ?? false, + isPaging: detail.operationHelperDetail?.isPaging ?? false, useLegacyLro: false }; // client-level, path-level and method-level parameter preparation @@ -126,7 +126,7 @@ function enrichLROAndPagingInSample( ) { const isLRO = operation.operationHelperDetail?.lroDetails?.isLongRunning ?? false, - isPaging = operation.operationHelperDetail?.isPageable ?? false; + isPaging = operation.operationHelperDetail?.isPaging ?? false; if (isPaging) { if (isLRO) { // TODO: report warning this is not supported diff --git a/packages/typespec-test/test/authoring/generated/typespec-ts/package.json b/packages/typespec-test/test/authoring/generated/typespec-ts/package.json index 8d8ae34443..290ef6a203 100644 --- a/packages/typespec-test/test/authoring/generated/typespec-ts/package.json +++ b/packages/typespec-test/test/authoring/generated/typespec-ts/package.json @@ -45,8 +45,8 @@ "@azure/core-rest-pipeline": "^1.5.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2", - "@azure/core-lro": "^2.5.4", - "@azure/abort-controller": "^1.0.0", + "@azure/core-lro": "3.0.0-beta.1", + "@azure/abort-controller": "^2.0.0", "@azure/core-paging": "^1.5.0" }, "devDependencies": { diff --git a/packages/typespec-test/test/authoring/generated/typespec-ts/review/authoring.api.md b/packages/typespec-test/test/authoring/generated/typespec-ts/review/authoring.api.md index 9a9c2d4496..f29d491579 100644 --- a/packages/typespec-test/test/authoring/generated/typespec-ts/review/authoring.api.md +++ b/packages/typespec-test/test/authoring/generated/typespec-ts/review/authoring.api.md @@ -4,6 +4,8 @@ ```ts +import { AbortSignalLike } from '@azure/abort-controller'; +import { CancelOnProgress } from '@azure/core-lro'; import { Client } from '@azure-rest/core-client'; import { ClientOptions } from '@azure-rest/core-client'; import { CreateHttpPollerOptions } from '@azure/core-lro'; @@ -17,7 +19,6 @@ import { PagedAsyncIterableIterator } from '@azure/core-paging'; import { PathUncheckedResponse } from '@azure-rest/core-client'; import { RawHttpHeaders } from '@azure/core-rest-pipeline'; import { RequestParameters } from '@azure-rest/core-client'; -import { SimplePollerLike } from '@azure/core-lro'; import { StreamableMethod } from '@azure-rest/core-client'; // @public (undocumented) @@ -805,6 +806,27 @@ export interface Routes { (path: "/authoring/analyze-text/projects/global/training-config-versions"): ListTrainingConfigVersions; } +// @public +export interface SimplePollerLike, TResult> { + getOperationState(): TState; + getResult(): TResult | undefined; + isDone(): boolean; + isStopped(): boolean; + onProgress(callback: (state: TState) => void): CancelOnProgress; + poll(options?: { + abortSignal?: AbortSignalLike; + }): Promise; + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + serialize(): Promise; + // @deprecated + stopPolling(): void; + submitted(): Promise; + // @deprecated + toString(): string; +} + // @public export interface SupportedLanguageOutput { languageCode: string; diff --git a/packages/typespec-test/test/authoring/generated/typespec-ts/src/pollingHelper.ts b/packages/typespec-test/test/authoring/generated/typespec-ts/src/pollingHelper.ts index baec0d8cf0..659586aae4 100644 --- a/packages/typespec-test/test/authoring/generated/typespec-ts/src/pollingHelper.ts +++ b/packages/typespec-test/test/authoring/generated/typespec-ts/src/pollingHelper.ts @@ -2,12 +2,13 @@ // Licensed under the MIT license. import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; import { + CancelOnProgress, CreateHttpPollerOptions, LongRunningOperation, - LroResponse, + OperationResponse, OperationState, - SimplePollerLike, createHttpPoller, } from "@azure/core-lro"; import { @@ -38,6 +39,76 @@ import { SwapDeploymentsDefaultResponse, SwapDeploymentsLogicalResponse, } from "./responses.js"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + /** * Helper function that builds a Poller object to help polling a long running operation. * @param client - Client to use for sending the request to get additional pages. @@ -118,23 +189,42 @@ export async function getLongRunningPoller( initialResponse: TResult, options: CreateHttpPollerOptions> = {}, ): Promise, TResult>> { + const abortController = new AbortController(); const poller: LongRunningOperation = { - requestMethod: initialResponse.request.method, - requestPath: initialResponse.request.url, sendInitialRequest: async () => { // In the case of Rest Clients we are building the LRO poller object from a response that's the reason // we are not triggering the initial request here, just extracting the information from the // response we were provided. return getLroResponse(initialResponse); }, - sendPollRequest: async (path) => { + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { // This is the callback that is going to be called to poll the service // to get the latest status. We use the client provided and the polling path // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location // depending on the lro pattern that the service implements. If non is provided we default to the initial path. - const response = await client - .pathUnchecked(path ?? initialResponse.request.url) - .get(); + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } const lroResponse = getLroResponse(response as TResult); lroResponse.rawResponse.headers["x-ms-original-url"] = initialResponse.request.url; @@ -143,7 +233,45 @@ export async function getLongRunningPoller( }; options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; - return createHttpPoller(poller, options); + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; } /** @@ -153,7 +281,7 @@ export async function getLongRunningPoller( */ function getLroResponse( response: TResult, -): LroResponse { +): OperationResponse { if (Number.isNaN(response.status)) { throw new TypeError( `Status code of the response is not a number. Value: ${response.status}`, diff --git a/packages/typespec-test/test/contoso/generated/typespec-ts/package.json b/packages/typespec-test/test/contoso/generated/typespec-ts/package.json index 161db217f3..93f1212bf5 100644 --- a/packages/typespec-test/test/contoso/generated/typespec-ts/package.json +++ b/packages/typespec-test/test/contoso/generated/typespec-ts/package.json @@ -45,8 +45,8 @@ "@azure/core-rest-pipeline": "^1.5.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2", - "@azure/core-lro": "^2.5.4", - "@azure/abort-controller": "^1.0.0", + "@azure/core-lro": "3.0.0-beta.1", + "@azure/abort-controller": "^2.0.0", "@azure/core-paging": "^1.5.0" }, "devDependencies": { diff --git a/packages/typespec-test/test/contoso/generated/typespec-ts/review/contosowidgetmanager-rest.api.md b/packages/typespec-test/test/contoso/generated/typespec-ts/review/contosowidgetmanager-rest.api.md index 0d5c560024..3c706f0e1d 100644 --- a/packages/typespec-test/test/contoso/generated/typespec-ts/review/contosowidgetmanager-rest.api.md +++ b/packages/typespec-test/test/contoso/generated/typespec-ts/review/contosowidgetmanager-rest.api.md @@ -4,6 +4,8 @@ ```ts +import { AbortSignalLike } from '@azure/abort-controller'; +import { CancelOnProgress } from '@azure/core-lro'; import { Client } from '@azure-rest/core-client'; import { ClientOptions } from '@azure-rest/core-client'; import { CreateHttpPollerOptions } from '@azure/core-lro'; @@ -16,7 +18,6 @@ import { PagedAsyncIterableIterator } from '@azure/core-paging'; import { PathUncheckedResponse } from '@azure-rest/core-client'; import { RawHttpHeaders } from '@azure/core-rest-pipeline'; import { RequestParameters } from '@azure-rest/core-client'; -import { SimplePollerLike } from '@azure/core-lro'; import { StreamableMethod } from '@azure-rest/core-client'; // @public @@ -307,6 +308,27 @@ export interface Routes { (path: "/widgets"): ListWidgets; } +// @public +export interface SimplePollerLike, TResult> { + getOperationState(): TState; + getResult(): TResult | undefined; + isDone(): boolean; + isStopped(): boolean; + onProgress(callback: (state: TState) => void): CancelOnProgress; + poll(options?: { + abortSignal?: AbortSignalLike; + }): Promise; + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + serialize(): Promise; + // @deprecated + stopPolling(): void; + submitted(): Promise; + // @deprecated + toString(): string; +} + // @public export interface Widget { manufacturerId: string; diff --git a/packages/typespec-test/test/contoso/generated/typespec-ts/src/pollingHelper.ts b/packages/typespec-test/test/contoso/generated/typespec-ts/src/pollingHelper.ts index df33f5b9a1..b380b928a8 100644 --- a/packages/typespec-test/test/contoso/generated/typespec-ts/src/pollingHelper.ts +++ b/packages/typespec-test/test/contoso/generated/typespec-ts/src/pollingHelper.ts @@ -2,12 +2,13 @@ // Licensed under the MIT license. import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; import { + CancelOnProgress, CreateHttpPollerOptions, LongRunningOperation, - LroResponse, + OperationResponse, OperationState, - SimplePollerLike, createHttpPoller, } from "@azure/core-lro"; import { @@ -19,6 +20,76 @@ import { DeleteWidgetDefaultResponse, DeleteWidgetLogicalResponse, } from "./responses.js"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + /** * Helper function that builds a Poller object to help polling a long running operation. * @param client - Client to use for sending the request to get additional pages. @@ -50,23 +121,42 @@ export async function getLongRunningPoller( initialResponse: TResult, options: CreateHttpPollerOptions> = {}, ): Promise, TResult>> { + const abortController = new AbortController(); const poller: LongRunningOperation = { - requestMethod: initialResponse.request.method, - requestPath: initialResponse.request.url, sendInitialRequest: async () => { // In the case of Rest Clients we are building the LRO poller object from a response that's the reason // we are not triggering the initial request here, just extracting the information from the // response we were provided. return getLroResponse(initialResponse); }, - sendPollRequest: async (path) => { + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { // This is the callback that is going to be called to poll the service // to get the latest status. We use the client provided and the polling path // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location // depending on the lro pattern that the service implements. If non is provided we default to the initial path. - const response = await client - .pathUnchecked(path ?? initialResponse.request.url) - .get(); + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } const lroResponse = getLroResponse(response as TResult); lroResponse.rawResponse.headers["x-ms-original-url"] = initialResponse.request.url; @@ -75,7 +165,45 @@ export async function getLongRunningPoller( }; options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; - return createHttpPoller(poller, options); + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; } /** @@ -85,7 +213,7 @@ export async function getLongRunningPoller( */ function getLroResponse( response: TResult, -): LroResponse { +): OperationResponse { if (Number.isNaN(response.status)) { throw new TypeError( `Status code of the response is not a number. Value: ${response.status}`, diff --git a/packages/typespec-test/test/healthInsights_radiologyinsights/generated/typespec-ts/package.json b/packages/typespec-test/test/healthInsights_radiologyinsights/generated/typespec-ts/package.json index 6858e348ea..6f36554a5a 100644 --- a/packages/typespec-test/test/healthInsights_radiologyinsights/generated/typespec-ts/package.json +++ b/packages/typespec-test/test/healthInsights_radiologyinsights/generated/typespec-ts/package.json @@ -45,8 +45,8 @@ "@azure/core-rest-pipeline": "^1.5.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2", - "@azure/core-lro": "^2.5.4", - "@azure/abort-controller": "^1.0.0" + "@azure/core-lro": "3.0.0-beta.1", + "@azure/abort-controller": "^2.0.0" }, "devDependencies": { "dotenv": "^16.0.0", diff --git a/packages/typespec-test/test/healthInsights_radiologyinsights/generated/typespec-ts/review/health-insights-radiologyinsights.api.md b/packages/typespec-test/test/healthInsights_radiologyinsights/generated/typespec-ts/review/health-insights-radiologyinsights.api.md index 3dde4833ad..88bb942053 100644 --- a/packages/typespec-test/test/healthInsights_radiologyinsights/generated/typespec-ts/review/health-insights-radiologyinsights.api.md +++ b/packages/typespec-test/test/healthInsights_radiologyinsights/generated/typespec-ts/review/health-insights-radiologyinsights.api.md @@ -4,6 +4,8 @@ ```ts +import { AbortSignalLike } from '@azure/abort-controller'; +import { CancelOnProgress } from '@azure/core-lro'; import { Client } from '@azure-rest/core-client'; import { ClientOptions } from '@azure-rest/core-client'; import { CreateHttpPollerOptions } from '@azure/core-lro'; @@ -15,7 +17,6 @@ import { OperationState } from '@azure/core-lro'; import { RawHttpHeaders } from '@azure/core-rest-pipeline'; import { RawHttpHeadersInput } from '@azure/core-rest-pipeline'; import { RequestParameters } from '@azure-rest/core-client'; -import { SimplePollerLike } from '@azure/core-lro'; import { StreamableMethod } from '@azure-rest/core-client'; // @public @@ -1216,6 +1217,27 @@ export interface SexMismatchInferenceOutput extends RadiologyInsightsInferenceOu sexIndication: CodeableConceptOutput; } +// @public +export interface SimplePollerLike, TResult> { + getOperationState(): TState; + getResult(): TResult | undefined; + isDone(): boolean; + isStopped(): boolean; + onProgress(callback: (state: TState) => void): CancelOnProgress; + poll(options?: { + abortSignal?: AbortSignalLike; + }): Promise; + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + serialize(): Promise; + // @deprecated + stopPolling(): void; + submitted(): Promise; + // @deprecated + toString(): string; +} + // @public export interface TimePeriod { end?: Date | string; diff --git a/packages/typespec-test/test/healthInsights_radiologyinsights/generated/typespec-ts/src/pollingHelper.ts b/packages/typespec-test/test/healthInsights_radiologyinsights/generated/typespec-ts/src/pollingHelper.ts index 61e152a43e..68e7e3f5e2 100644 --- a/packages/typespec-test/test/healthInsights_radiologyinsights/generated/typespec-ts/src/pollingHelper.ts +++ b/packages/typespec-test/test/healthInsights_radiologyinsights/generated/typespec-ts/src/pollingHelper.ts @@ -2,12 +2,13 @@ // Licensed under the MIT license. import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; import { + CancelOnProgress, CreateHttpPollerOptions, LongRunningOperation, - LroResponse, + OperationResponse, OperationState, - SimplePollerLike, createHttpPoller, } from "@azure/core-lro"; import { @@ -15,6 +16,76 @@ import { CreateJobDefaultResponse, CreateJobLogicalResponse, } from "./responses.js"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + /** * Helper function that builds a Poller object to help polling a long running operation. * @param client - Client to use for sending the request to get additional pages. @@ -34,23 +105,42 @@ export async function getLongRunningPoller( initialResponse: TResult, options: CreateHttpPollerOptions> = {}, ): Promise, TResult>> { + const abortController = new AbortController(); const poller: LongRunningOperation = { - requestMethod: initialResponse.request.method, - requestPath: initialResponse.request.url, sendInitialRequest: async () => { // In the case of Rest Clients we are building the LRO poller object from a response that's the reason // we are not triggering the initial request here, just extracting the information from the // response we were provided. return getLroResponse(initialResponse); }, - sendPollRequest: async (path) => { + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { // This is the callback that is going to be called to poll the service // to get the latest status. We use the client provided and the polling path // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location // depending on the lro pattern that the service implements. If non is provided we default to the initial path. - const response = await client - .pathUnchecked(path ?? initialResponse.request.url) - .get(); + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } const lroResponse = getLroResponse(response as TResult); lroResponse.rawResponse.headers["x-ms-original-url"] = initialResponse.request.url; @@ -59,7 +149,45 @@ export async function getLongRunningPoller( }; options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; - return createHttpPoller(poller, options); + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; } /** @@ -69,7 +197,7 @@ export async function getLongRunningPoller( */ function getLroResponse( response: TResult, -): LroResponse { +): OperationResponse { if (Number.isNaN(response.status)) { throw new TypeError( `Status code of the response is not a number. Value: ${response.status}`, diff --git a/packages/typespec-test/test/healthInsights_trialmatcher/generated/typespec-ts/package.json b/packages/typespec-test/test/healthInsights_trialmatcher/generated/typespec-ts/package.json index 6a061fbc35..48c43cd362 100644 --- a/packages/typespec-test/test/healthInsights_trialmatcher/generated/typespec-ts/package.json +++ b/packages/typespec-test/test/healthInsights_trialmatcher/generated/typespec-ts/package.json @@ -45,8 +45,8 @@ "@azure/core-rest-pipeline": "^1.5.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2", - "@azure/core-lro": "^2.5.4", - "@azure/abort-controller": "^1.0.0" + "@azure/core-lro": "3.0.0-beta.1", + "@azure/abort-controller": "^2.0.0" }, "devDependencies": { "dotenv": "^16.0.0", diff --git a/packages/typespec-test/test/healthInsights_trialmatcher/generated/typespec-ts/review/health-insights-clinicalmatching.api.md b/packages/typespec-test/test/healthInsights_trialmatcher/generated/typespec-ts/review/health-insights-clinicalmatching.api.md index ac9696bc3c..355cf011cb 100644 --- a/packages/typespec-test/test/healthInsights_trialmatcher/generated/typespec-ts/review/health-insights-clinicalmatching.api.md +++ b/packages/typespec-test/test/healthInsights_trialmatcher/generated/typespec-ts/review/health-insights-clinicalmatching.api.md @@ -4,6 +4,8 @@ ```ts +import { AbortSignalLike } from '@azure/abort-controller'; +import { CancelOnProgress } from '@azure/core-lro'; import { Client } from '@azure-rest/core-client'; import { ClientOptions } from '@azure-rest/core-client'; import { CreateHttpPollerOptions } from '@azure/core-lro'; @@ -15,7 +17,6 @@ import { OperationState } from '@azure/core-lro'; import { RawHttpHeaders } from '@azure/core-rest-pipeline'; import { RawHttpHeadersInput } from '@azure/core-rest-pipeline'; import { RequestParameters } from '@azure-rest/core-client'; -import { SimplePollerLike } from '@azure/core-lro'; import { StreamableMethod } from '@azure-rest/core-client'; // @public @@ -337,6 +338,27 @@ export interface Routes { (path: "/trialmatcher/jobs"): CreateJob; } +// @public +export interface SimplePollerLike, TResult> { + getOperationState(): TState; + getResult(): TResult | undefined; + isDone(): boolean; + isStopped(): boolean; + onProgress(callback: (state: TState) => void): CancelOnProgress; + poll(options?: { + abortSignal?: AbortSignalLike; + }): Promise; + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + serialize(): Promise; + // @deprecated + stopPolling(): void; + submitted(): Promise; + // @deprecated + toString(): string; +} + // @public export interface TrialMatcherData { configuration?: TrialMatcherModelConfiguration; diff --git a/packages/typespec-test/test/healthInsights_trialmatcher/generated/typespec-ts/src/pollingHelper.ts b/packages/typespec-test/test/healthInsights_trialmatcher/generated/typespec-ts/src/pollingHelper.ts index 0fe67aff70..aec5bdc1d9 100644 --- a/packages/typespec-test/test/healthInsights_trialmatcher/generated/typespec-ts/src/pollingHelper.ts +++ b/packages/typespec-test/test/healthInsights_trialmatcher/generated/typespec-ts/src/pollingHelper.ts @@ -2,12 +2,13 @@ // Licensed under the MIT license. import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; import { + CancelOnProgress, CreateHttpPollerOptions, LongRunningOperation, - LroResponse, + OperationResponse, OperationState, - SimplePollerLike, createHttpPoller, } from "@azure/core-lro"; import { @@ -16,6 +17,76 @@ import { CreateJobDefaultResponse, CreateJobLogicalResponse, } from "./responses.js"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + /** * Helper function that builds a Poller object to help polling a long running operation. * @param client - Client to use for sending the request to get additional pages. @@ -38,23 +109,42 @@ export async function getLongRunningPoller( initialResponse: TResult, options: CreateHttpPollerOptions> = {}, ): Promise, TResult>> { + const abortController = new AbortController(); const poller: LongRunningOperation = { - requestMethod: initialResponse.request.method, - requestPath: initialResponse.request.url, sendInitialRequest: async () => { // In the case of Rest Clients we are building the LRO poller object from a response that's the reason // we are not triggering the initial request here, just extracting the information from the // response we were provided. return getLroResponse(initialResponse); }, - sendPollRequest: async (path) => { + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { // This is the callback that is going to be called to poll the service // to get the latest status. We use the client provided and the polling path // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location // depending on the lro pattern that the service implements. If non is provided we default to the initial path. - const response = await client - .pathUnchecked(path ?? initialResponse.request.url) - .get(); + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } const lroResponse = getLroResponse(response as TResult); lroResponse.rawResponse.headers["x-ms-original-url"] = initialResponse.request.url; @@ -63,7 +153,45 @@ export async function getLongRunningPoller( }; options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; - return createHttpPoller(poller, options); + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; } /** @@ -73,7 +201,7 @@ export async function getLongRunningPoller( */ function getLroResponse( response: TResult, -): LroResponse { +): OperationResponse { if (Number.isNaN(response.status)) { throw new TypeError( `Status code of the response is not a number. Value: ${response.status}`, diff --git a/packages/typespec-test/test/loadTest/generated/typespec-ts/package.json b/packages/typespec-test/test/loadTest/generated/typespec-ts/package.json index 97438caabf..c3fb810620 100644 --- a/packages/typespec-test/test/loadTest/generated/typespec-ts/package.json +++ b/packages/typespec-test/test/loadTest/generated/typespec-ts/package.json @@ -45,8 +45,8 @@ "@azure/core-rest-pipeline": "^1.5.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2", - "@azure/core-lro": "^2.5.4", - "@azure/abort-controller": "^1.0.0", + "@azure/core-lro": "3.0.0-beta.1", + "@azure/abort-controller": "^2.0.0", "@azure/core-paging": "^1.5.0" }, "devDependencies": { diff --git a/packages/typespec-test/test/loadTest/generated/typespec-ts/review/load-testing.api.md b/packages/typespec-test/test/loadTest/generated/typespec-ts/review/load-testing.api.md index 8fb3e2692c..5c1f144134 100644 --- a/packages/typespec-test/test/loadTest/generated/typespec-ts/review/load-testing.api.md +++ b/packages/typespec-test/test/loadTest/generated/typespec-ts/review/load-testing.api.md @@ -6,6 +6,8 @@ /// +import { AbortSignalLike } from '@azure/abort-controller'; +import { CancelOnProgress } from '@azure/core-lro'; import { Client } from '@azure-rest/core-client'; import { ClientOptions } from '@azure-rest/core-client'; import { CreateHttpPollerOptions } from '@azure/core-lro'; @@ -17,7 +19,6 @@ import { PagedAsyncIterableIterator } from '@azure/core-paging'; import { PathUncheckedResponse } from '@azure-rest/core-client'; import { RawHttpHeaders } from '@azure/core-rest-pipeline'; import { RequestParameters } from '@azure-rest/core-client'; -import { SimplePollerLike } from '@azure/core-lro'; import { StreamableMethod } from '@azure-rest/core-client'; import { TokenCredential } from '@azure/core-auth'; @@ -1404,6 +1405,27 @@ export interface SecretOutput { value?: string; } +// @public +export interface SimplePollerLike, TResult> { + getOperationState(): TState; + getResult(): TResult | undefined; + isDone(): boolean; + isStopped(): boolean; + onProgress(callback: (state: TState) => void): CancelOnProgress; + poll(options?: { + abortSignal?: AbortSignalLike; + }): Promise; + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + serialize(): Promise; + // @deprecated + stopPolling(): void; + submitted(): Promise; + // @deprecated + toString(): string; +} + // @public export interface Test { certificate?: CertificateMetadata; diff --git a/packages/typespec-test/test/loadTest/generated/typespec-ts/src/pollingHelper.ts b/packages/typespec-test/test/loadTest/generated/typespec-ts/src/pollingHelper.ts index 1121707427..7247a02cbe 100644 --- a/packages/typespec-test/test/loadTest/generated/typespec-ts/src/pollingHelper.ts +++ b/packages/typespec-test/test/loadTest/generated/typespec-ts/src/pollingHelper.ts @@ -2,12 +2,13 @@ // Licensed under the MIT license. import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; import { + CancelOnProgress, CreateHttpPollerOptions, LongRunningOperation, - LroResponse, + OperationResponse, OperationState, - SimplePollerLike, createHttpPoller, } from "@azure/core-lro"; import { @@ -16,6 +17,76 @@ import { LoadTestRunCreateOrUpdateTestRunDefaultResponse, LoadTestRunCreateOrUpdateTestRunLogicalResponse, } from "./responses.js"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + /** * Helper function that builds a Poller object to help polling a long running operation. * @param client - Client to use for sending the request to get additional pages. @@ -40,23 +111,42 @@ export async function getLongRunningPoller( initialResponse: TResult, options: CreateHttpPollerOptions> = {}, ): Promise, TResult>> { + const abortController = new AbortController(); const poller: LongRunningOperation = { - requestMethod: initialResponse.request.method, - requestPath: initialResponse.request.url, sendInitialRequest: async () => { // In the case of Rest Clients we are building the LRO poller object from a response that's the reason // we are not triggering the initial request here, just extracting the information from the // response we were provided. return getLroResponse(initialResponse); }, - sendPollRequest: async (path) => { + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { // This is the callback that is going to be called to poll the service // to get the latest status. We use the client provided and the polling path // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location // depending on the lro pattern that the service implements. If non is provided we default to the initial path. - const response = await client - .pathUnchecked(path ?? initialResponse.request.url) - .get(); + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } const lroResponse = getLroResponse(response as TResult); lroResponse.rawResponse.headers["x-ms-original-url"] = initialResponse.request.url; @@ -65,7 +155,45 @@ export async function getLongRunningPoller( }; options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; - return createHttpPoller(poller, options); + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; } /** @@ -75,7 +203,7 @@ export async function getLongRunningPoller( */ function getLroResponse( response: TResult, -): LroResponse { +): OperationResponse { if (Number.isNaN(response.status)) { throw new TypeError( `Status code of the response is not a number. Value: ${response.status}`, diff --git a/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/rest/index.ts b/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/rest/index.ts index a0d5934c88..a22d574c4f 100644 --- a/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/rest/index.ts +++ b/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/rest/index.ts @@ -11,5 +11,6 @@ export * from "./isUnexpected.js"; export * from "./models.js"; export * from "./outputModels.js"; export * from "./paginateHelper.js"; +export * from "./pollingHelper.js"; export default AzureLoadTestingClient; diff --git a/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/rest/pollingHelper.ts b/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/rest/pollingHelper.ts new file mode 100644 index 0000000000..7247a02cbe --- /dev/null +++ b/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/rest/pollingHelper.ts @@ -0,0 +1,221 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; +import { + CancelOnProgress, + CreateHttpPollerOptions, + LongRunningOperation, + OperationResponse, + OperationState, + createHttpPoller, +} from "@azure/core-lro"; +import { + LoadTestRunCreateOrUpdateTestRun200Response, + LoadTestRunCreateOrUpdateTestRun201Response, + LoadTestRunCreateOrUpdateTestRunDefaultResponse, + LoadTestRunCreateOrUpdateTestRunLogicalResponse, +} from "./responses.js"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + +/** + * Helper function that builds a Poller object to help polling a long running operation. + * @param client - Client to use for sending the request to get additional pages. + * @param initialResponse - The initial response. + * @param options - Options to set a resume state or custom polling interval. + * @returns - A poller object to poll for operation state updates and eventually get the final response. + */ +export async function getLongRunningPoller< + TResult extends + | LoadTestRunCreateOrUpdateTestRunLogicalResponse + | LoadTestRunCreateOrUpdateTestRunDefaultResponse, +>( + client: Client, + initialResponse: + | LoadTestRunCreateOrUpdateTestRun200Response + | LoadTestRunCreateOrUpdateTestRun201Response + | LoadTestRunCreateOrUpdateTestRunDefaultResponse, + options?: CreateHttpPollerOptions>, +): Promise, TResult>>; +export async function getLongRunningPoller( + client: Client, + initialResponse: TResult, + options: CreateHttpPollerOptions> = {}, +): Promise, TResult>> { + const abortController = new AbortController(); + const poller: LongRunningOperation = { + sendInitialRequest: async () => { + // In the case of Rest Clients we are building the LRO poller object from a response that's the reason + // we are not triggering the initial request here, just extracting the information from the + // response we were provided. + return getLroResponse(initialResponse); + }, + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { + // This is the callback that is going to be called to poll the service + // to get the latest status. We use the client provided and the polling path + // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location + // depending on the lro pattern that the service implements. If non is provided we default to the initial path. + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } + const lroResponse = getLroResponse(response as TResult); + lroResponse.rawResponse.headers["x-ms-original-url"] = + initialResponse.request.url; + return lroResponse; + }, + }; + + options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; +} + +/** + * Converts a Rest Client response to a response that the LRO implementation understands + * @param response - a rest client http response + * @returns - An LRO response that the LRO implementation understands + */ +function getLroResponse( + response: TResult, +): OperationResponse { + if (Number.isNaN(response.status)) { + throw new TypeError( + `Status code of the response is not a number. Value: ${response.status}`, + ); + } + + return { + flatResponse: response, + rawResponse: { + ...response, + statusCode: Number.parseInt(response.status), + body: response.body, + }, + }; +} diff --git a/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/testRunOperations/api/pollingHelpers.ts b/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/testRunOperations/api/pollingHelpers.ts index 4d7227d796..8aeb480ba7 100644 --- a/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/testRunOperations/api/pollingHelpers.ts +++ b/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/testRunOperations/api/pollingHelpers.ts @@ -58,6 +58,7 @@ export function getLongRunningPoller< ); } let initialResponse: TResponse | undefined = undefined; + const pollAbortController = new AbortController(); const poller: LongRunningOperation = { sendInitialRequest: async () => { if (!getInitialResponse) { @@ -74,9 +75,30 @@ export function getLongRunningPoller< abortSignal?: AbortSignalLike; }, ) => { - const response = await client - .pathUnchecked(path) - .get({ abortSignal: options.abortSignal ?? pollOptions?.abortSignal }); + // The poll request would both listen to the user provided abort signal and the poller's own abort signal + function abortListener(): void { + pollAbortController.abort(); + } + const abortSignal = pollAbortController.signal; + if (options.abortSignal?.aborted) { + pollAbortController.abort(); + } else if (pollOptions?.abortSignal?.aborted) { + pollAbortController.abort(); + } else if (!abortSignal.aborted) { + options.abortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + pollOptions?.abortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client.pathUnchecked(path).get({ abortSignal }); + } finally { + options.abortSignal?.removeEventListener("abort", abortListener); + pollOptions?.abortSignal?.removeEventListener("abort", abortListener); + } if (options.initialUrl || initialResponse) { response.headers["x-ms-original-url"] = options.initialUrl ?? initialResponse!.request.url; @@ -104,10 +126,7 @@ function getLroResponse( response: TResponse, ): OperationResponse { if (isUnexpected(response as PathUncheckedResponse)) { - createRestError( - `Status code of the response is not a number. Value: ${response.status}`, - response, - ); + throw createRestError(response); } return { flatResponse: response, diff --git a/packages/typespec-test/test/openai/generated/typespec-ts/package.json b/packages/typespec-test/test/openai/generated/typespec-ts/package.json index b78755a23a..d4f197cf3d 100644 --- a/packages/typespec-test/test/openai/generated/typespec-ts/package.json +++ b/packages/typespec-test/test/openai/generated/typespec-ts/package.json @@ -45,8 +45,8 @@ "@azure/core-rest-pipeline": "^1.5.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2", - "@azure/core-lro": "^2.5.4", - "@azure/abort-controller": "^1.0.0" + "@azure/core-lro": "3.0.0-beta.1", + "@azure/abort-controller": "^2.0.0" }, "devDependencies": { "dotenv": "^16.0.0", diff --git a/packages/typespec-test/test/openai/generated/typespec-ts/review/openai.api.md b/packages/typespec-test/test/openai/generated/typespec-ts/review/openai.api.md index fbbbcb2744..33f67900e0 100644 --- a/packages/typespec-test/test/openai/generated/typespec-ts/review/openai.api.md +++ b/packages/typespec-test/test/openai/generated/typespec-ts/review/openai.api.md @@ -4,6 +4,8 @@ ```ts +import { AbortSignalLike } from '@azure/abort-controller'; +import { CancelOnProgress } from '@azure/core-lro'; import { Client } from '@azure-rest/core-client'; import { ClientOptions } from '@azure-rest/core-client'; import { CreateHttpPollerOptions } from '@azure/core-lro'; @@ -14,7 +16,6 @@ import { KeyCredential } from '@azure/core-auth'; import { OperationState } from '@azure/core-lro'; import { RawHttpHeaders } from '@azure/core-rest-pipeline'; import { RequestParameters } from '@azure-rest/core-client'; -import { SimplePollerLike } from '@azure/core-lro'; import { StreamableMethod } from '@azure-rest/core-client'; import { TokenCredential } from '@azure/core-auth'; @@ -530,6 +531,27 @@ export interface Routes { (path: "/images/generations:submit"): BeginAzureBatchImageGeneration; } +// @public +export interface SimplePollerLike, TResult> { + getOperationState(): TState; + getResult(): TResult | undefined; + isDone(): boolean; + isStopped(): boolean; + onProgress(callback: (state: TState) => void): CancelOnProgress; + poll(options?: { + abortSignal?: AbortSignalLike; + }): Promise; + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + serialize(): Promise; + // @deprecated + stopPolling(): void; + submitted(): Promise; + // @deprecated + toString(): string; +} + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/typespec-test/test/openai/generated/typespec-ts/src/pollingHelper.ts b/packages/typespec-test/test/openai/generated/typespec-ts/src/pollingHelper.ts index d8759dd9c9..f48f9d79c5 100644 --- a/packages/typespec-test/test/openai/generated/typespec-ts/src/pollingHelper.ts +++ b/packages/typespec-test/test/openai/generated/typespec-ts/src/pollingHelper.ts @@ -2,12 +2,13 @@ // Licensed under the MIT license. import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; import { + CancelOnProgress, CreateHttpPollerOptions, LongRunningOperation, - LroResponse, + OperationResponse, OperationState, - SimplePollerLike, createHttpPoller, } from "@azure/core-lro"; import { @@ -15,6 +16,76 @@ import { BeginAzureBatchImageGenerationDefaultResponse, BeginAzureBatchImageGenerationLogicalResponse, } from "./responses.js"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + /** * Helper function that builds a Poller object to help polling a long running operation. * @param client - Client to use for sending the request to get additional pages. @@ -38,23 +109,42 @@ export async function getLongRunningPoller( initialResponse: TResult, options: CreateHttpPollerOptions> = {}, ): Promise, TResult>> { + const abortController = new AbortController(); const poller: LongRunningOperation = { - requestMethod: initialResponse.request.method, - requestPath: initialResponse.request.url, sendInitialRequest: async () => { // In the case of Rest Clients we are building the LRO poller object from a response that's the reason // we are not triggering the initial request here, just extracting the information from the // response we were provided. return getLroResponse(initialResponse); }, - sendPollRequest: async (path) => { + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { // This is the callback that is going to be called to poll the service // to get the latest status. We use the client provided and the polling path // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location // depending on the lro pattern that the service implements. If non is provided we default to the initial path. - const response = await client - .pathUnchecked(path ?? initialResponse.request.url) - .get(); + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } const lroResponse = getLroResponse(response as TResult); lroResponse.rawResponse.headers["x-ms-original-url"] = initialResponse.request.url; @@ -63,7 +153,45 @@ export async function getLongRunningPoller( }; options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; - return createHttpPoller(poller, options); + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; } /** @@ -73,7 +201,7 @@ export async function getLongRunningPoller( */ function getLroResponse( response: TResult, -): LroResponse { +): OperationResponse { if (Number.isNaN(response.status)) { throw new TypeError( `Status code of the response is not a number. Value: ${response.status}`, diff --git a/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/rest/index.ts b/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/rest/index.ts index a1de6ffe97..938c70667f 100644 --- a/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/rest/index.ts +++ b/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/rest/index.ts @@ -10,6 +10,7 @@ export * from "./clientDefinitions.js"; export * from "./isUnexpected.js"; export * from "./models.js"; export * from "./outputModels.js"; +export * from "./pollingHelper.js"; export { createFile, createFileFromStream, diff --git a/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/rest/pollingHelper.ts b/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/rest/pollingHelper.ts new file mode 100644 index 0000000000..f48f9d79c5 --- /dev/null +++ b/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/rest/pollingHelper.ts @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; +import { + CancelOnProgress, + CreateHttpPollerOptions, + LongRunningOperation, + OperationResponse, + OperationState, + createHttpPoller, +} from "@azure/core-lro"; +import { + BeginAzureBatchImageGeneration202Response, + BeginAzureBatchImageGenerationDefaultResponse, + BeginAzureBatchImageGenerationLogicalResponse, +} from "./responses.js"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + +/** + * Helper function that builds a Poller object to help polling a long running operation. + * @param client - Client to use for sending the request to get additional pages. + * @param initialResponse - The initial response. + * @param options - Options to set a resume state or custom polling interval. + * @returns - A poller object to poll for operation state updates and eventually get the final response. + */ +export async function getLongRunningPoller< + TResult extends + | BeginAzureBatchImageGenerationLogicalResponse + | BeginAzureBatchImageGenerationDefaultResponse, +>( + client: Client, + initialResponse: + | BeginAzureBatchImageGeneration202Response + | BeginAzureBatchImageGenerationDefaultResponse, + options?: CreateHttpPollerOptions>, +): Promise, TResult>>; +export async function getLongRunningPoller( + client: Client, + initialResponse: TResult, + options: CreateHttpPollerOptions> = {}, +): Promise, TResult>> { + const abortController = new AbortController(); + const poller: LongRunningOperation = { + sendInitialRequest: async () => { + // In the case of Rest Clients we are building the LRO poller object from a response that's the reason + // we are not triggering the initial request here, just extracting the information from the + // response we were provided. + return getLroResponse(initialResponse); + }, + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { + // This is the callback that is going to be called to poll the service + // to get the latest status. We use the client provided and the polling path + // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location + // depending on the lro pattern that the service implements. If non is provided we default to the initial path. + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } + const lroResponse = getLroResponse(response as TResult); + lroResponse.rawResponse.headers["x-ms-original-url"] = + initialResponse.request.url; + return lroResponse; + }, + }; + + options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; +} + +/** + * Converts a Rest Client response to a response that the LRO implementation understands + * @param response - a rest client http response + * @returns - An LRO response that the LRO implementation understands + */ +function getLroResponse( + response: TResult, +): OperationResponse { + if (Number.isNaN(response.status)) { + throw new TypeError( + `Status code of the response is not a number. Value: ${response.status}`, + ); + } + + return { + flatResponse: response, + rawResponse: { + ...response, + statusCode: Number.parseInt(response.status), + body: response.body, + }, + }; +} diff --git a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/sources/generated/src/api/pollingHelpers.ts b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/sources/generated/src/api/pollingHelpers.ts index 6e5c921476..6e3a0a93bc 100644 --- a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/sources/generated/src/api/pollingHelpers.ts +++ b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/sources/generated/src/api/pollingHelpers.ts @@ -58,6 +58,7 @@ export function getLongRunningPoller< ); } let initialResponse: TResponse | undefined = undefined; + const pollAbortController = new AbortController(); const poller: LongRunningOperation = { sendInitialRequest: async () => { if (!getInitialResponse) { @@ -74,9 +75,30 @@ export function getLongRunningPoller< abortSignal?: AbortSignalLike; }, ) => { - const response = await client - .pathUnchecked(path) - .get({ abortSignal: options.abortSignal ?? pollOptions?.abortSignal }); + // The poll request would both listen to the user provided abort signal and the poller's own abort signal + function abortListener(): void { + pollAbortController.abort(); + } + const abortSignal = pollAbortController.signal; + if (options.abortSignal?.aborted) { + pollAbortController.abort(); + } else if (pollOptions?.abortSignal?.aborted) { + pollAbortController.abort(); + } else if (!abortSignal.aborted) { + options.abortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + pollOptions?.abortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client.pathUnchecked(path).get({ abortSignal }); + } finally { + options.abortSignal?.removeEventListener("abort", abortListener); + pollOptions?.abortSignal?.removeEventListener("abort", abortListener); + } if (options.initialUrl || initialResponse) { response.headers["x-ms-original-url"] = options.initialUrl ?? initialResponse!.request.url; @@ -104,10 +126,7 @@ function getLroResponse( response: TResponse, ): OperationResponse { if (isUnexpected(response as PathUncheckedResponse)) { - createRestError( - `Status code of the response is not a number. Value: ${response.status}`, - response, - ); + throw createRestError(response); } return { flatResponse: response, diff --git a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/sources/generated/src/rest/index.ts b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/sources/generated/src/rest/index.ts index 6a101accd4..1a092c7640 100644 --- a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/sources/generated/src/rest/index.ts +++ b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/sources/generated/src/rest/index.ts @@ -11,6 +11,7 @@ export * from "./isUnexpected.js"; export * from "./models.js"; export * from "./outputModels.js"; export * from "./paginateHelper.js"; +export * from "./pollingHelper.js"; export * from "./serializeHelper.js"; export default WidgetServiceClient; diff --git a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/sources/generated/src/rest/pollingHelper.ts b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/sources/generated/src/rest/pollingHelper.ts new file mode 100644 index 0000000000..fcf2871540 --- /dev/null +++ b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/sources/generated/src/rest/pollingHelper.ts @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; +import { + CancelOnProgress, + CreateHttpPollerOptions, + LongRunningOperation, + OperationResponse, + OperationState, + createHttpPoller, +} from "@azure/core-lro"; +import { + WidgetsCreateOrReplace200Response, + WidgetsCreateOrReplace201Response, + WidgetsCreateOrReplaceDefaultResponse, + WidgetsCreateOrReplaceLogicalResponse, + BudgetsCreateOrReplace200Response, + BudgetsCreateOrReplace201Response, + BudgetsCreateOrReplaceDefaultResponse, + BudgetsCreateOrReplaceLogicalResponse, + BudgetsCreateOrUpdate200Response, + BudgetsCreateOrUpdate201Response, + BudgetsCreateOrUpdateDefaultResponse, + BudgetsCreateOrUpdateLogicalResponse, +} from "./responses.js"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + +/** + * Helper function that builds a Poller object to help polling a long running operation. + * @param client - Client to use for sending the request to get additional pages. + * @param initialResponse - The initial response. + * @param options - Options to set a resume state or custom polling interval. + * @returns - A poller object to poll for operation state updates and eventually get the final response. + */ +export async function getLongRunningPoller< + TResult extends + | BudgetsCreateOrUpdateLogicalResponse + | BudgetsCreateOrUpdateDefaultResponse, +>( + client: Client, + initialResponse: + | BudgetsCreateOrUpdate200Response + | BudgetsCreateOrUpdate201Response + | BudgetsCreateOrUpdateDefaultResponse, + options?: CreateHttpPollerOptions>, +): Promise, TResult>>; +export async function getLongRunningPoller< + TResult extends + | WidgetsCreateOrReplaceLogicalResponse + | WidgetsCreateOrReplaceDefaultResponse, +>( + client: Client, + initialResponse: + | WidgetsCreateOrReplace200Response + | WidgetsCreateOrReplace201Response + | WidgetsCreateOrReplaceDefaultResponse, + options?: CreateHttpPollerOptions>, +): Promise, TResult>>; +export async function getLongRunningPoller< + TResult extends + | BudgetsCreateOrReplaceLogicalResponse + | BudgetsCreateOrReplaceDefaultResponse, +>( + client: Client, + initialResponse: + | BudgetsCreateOrReplace200Response + | BudgetsCreateOrReplace201Response + | BudgetsCreateOrReplaceDefaultResponse, + options?: CreateHttpPollerOptions>, +): Promise, TResult>>; +export async function getLongRunningPoller( + client: Client, + initialResponse: TResult, + options: CreateHttpPollerOptions> = {}, +): Promise, TResult>> { + const abortController = new AbortController(); + const poller: LongRunningOperation = { + sendInitialRequest: async () => { + // In the case of Rest Clients we are building the LRO poller object from a response that's the reason + // we are not triggering the initial request here, just extracting the information from the + // response we were provided. + return getLroResponse(initialResponse); + }, + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { + // This is the callback that is going to be called to poll the service + // to get the latest status. We use the client provided and the polling path + // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location + // depending on the lro pattern that the service implements. If non is provided we default to the initial path. + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } + const lroResponse = getLroResponse(response as TResult); + lroResponse.rawResponse.headers["x-ms-original-url"] = + initialResponse.request.url; + return lroResponse; + }, + }; + + options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; +} + +/** + * Converts a Rest Client response to a response that the LRO implementation understands + * @param response - a rest client http response + * @returns - An LRO response that the LRO implementation understands + */ +function getLroResponse( + response: TResult, +): OperationResponse { + if (Number.isNaN(response.status)) { + throw new TypeError( + `Status code of the response is not a number. Value: ${response.status}`, + ); + } + + return { + flatResponse: response, + rawResponse: { + ...response, + statusCode: Number.parseInt(response.status), + body: response.body, + }, + }; +} diff --git a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/api/pollingHelpers.ts b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/api/pollingHelpers.ts index 6e5c921476..6e3a0a93bc 100644 --- a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/api/pollingHelpers.ts +++ b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/api/pollingHelpers.ts @@ -58,6 +58,7 @@ export function getLongRunningPoller< ); } let initialResponse: TResponse | undefined = undefined; + const pollAbortController = new AbortController(); const poller: LongRunningOperation = { sendInitialRequest: async () => { if (!getInitialResponse) { @@ -74,9 +75,30 @@ export function getLongRunningPoller< abortSignal?: AbortSignalLike; }, ) => { - const response = await client - .pathUnchecked(path) - .get({ abortSignal: options.abortSignal ?? pollOptions?.abortSignal }); + // The poll request would both listen to the user provided abort signal and the poller's own abort signal + function abortListener(): void { + pollAbortController.abort(); + } + const abortSignal = pollAbortController.signal; + if (options.abortSignal?.aborted) { + pollAbortController.abort(); + } else if (pollOptions?.abortSignal?.aborted) { + pollAbortController.abort(); + } else if (!abortSignal.aborted) { + options.abortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + pollOptions?.abortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client.pathUnchecked(path).get({ abortSignal }); + } finally { + options.abortSignal?.removeEventListener("abort", abortListener); + pollOptions?.abortSignal?.removeEventListener("abort", abortListener); + } if (options.initialUrl || initialResponse) { response.headers["x-ms-original-url"] = options.initialUrl ?? initialResponse!.request.url; @@ -104,10 +126,7 @@ function getLroResponse( response: TResponse, ): OperationResponse { if (isUnexpected(response as PathUncheckedResponse)) { - createRestError( - `Status code of the response is not a number. Value: ${response.status}`, - response, - ); + throw createRestError(response); } return { flatResponse: response, diff --git a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/rest/index.ts b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/rest/index.ts index 6a101accd4..1a092c7640 100644 --- a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/rest/index.ts +++ b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/rest/index.ts @@ -11,6 +11,7 @@ export * from "./isUnexpected.js"; export * from "./models.js"; export * from "./outputModels.js"; export * from "./paginateHelper.js"; +export * from "./pollingHelper.js"; export * from "./serializeHelper.js"; export default WidgetServiceClient; diff --git a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/rest/pollingHelper.ts b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/rest/pollingHelper.ts new file mode 100644 index 0000000000..fcf2871540 --- /dev/null +++ b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/rest/pollingHelper.ts @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; +import { + CancelOnProgress, + CreateHttpPollerOptions, + LongRunningOperation, + OperationResponse, + OperationState, + createHttpPoller, +} from "@azure/core-lro"; +import { + WidgetsCreateOrReplace200Response, + WidgetsCreateOrReplace201Response, + WidgetsCreateOrReplaceDefaultResponse, + WidgetsCreateOrReplaceLogicalResponse, + BudgetsCreateOrReplace200Response, + BudgetsCreateOrReplace201Response, + BudgetsCreateOrReplaceDefaultResponse, + BudgetsCreateOrReplaceLogicalResponse, + BudgetsCreateOrUpdate200Response, + BudgetsCreateOrUpdate201Response, + BudgetsCreateOrUpdateDefaultResponse, + BudgetsCreateOrUpdateLogicalResponse, +} from "./responses.js"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + +/** + * Helper function that builds a Poller object to help polling a long running operation. + * @param client - Client to use for sending the request to get additional pages. + * @param initialResponse - The initial response. + * @param options - Options to set a resume state or custom polling interval. + * @returns - A poller object to poll for operation state updates and eventually get the final response. + */ +export async function getLongRunningPoller< + TResult extends + | BudgetsCreateOrUpdateLogicalResponse + | BudgetsCreateOrUpdateDefaultResponse, +>( + client: Client, + initialResponse: + | BudgetsCreateOrUpdate200Response + | BudgetsCreateOrUpdate201Response + | BudgetsCreateOrUpdateDefaultResponse, + options?: CreateHttpPollerOptions>, +): Promise, TResult>>; +export async function getLongRunningPoller< + TResult extends + | WidgetsCreateOrReplaceLogicalResponse + | WidgetsCreateOrReplaceDefaultResponse, +>( + client: Client, + initialResponse: + | WidgetsCreateOrReplace200Response + | WidgetsCreateOrReplace201Response + | WidgetsCreateOrReplaceDefaultResponse, + options?: CreateHttpPollerOptions>, +): Promise, TResult>>; +export async function getLongRunningPoller< + TResult extends + | BudgetsCreateOrReplaceLogicalResponse + | BudgetsCreateOrReplaceDefaultResponse, +>( + client: Client, + initialResponse: + | BudgetsCreateOrReplace200Response + | BudgetsCreateOrReplace201Response + | BudgetsCreateOrReplaceDefaultResponse, + options?: CreateHttpPollerOptions>, +): Promise, TResult>>; +export async function getLongRunningPoller( + client: Client, + initialResponse: TResult, + options: CreateHttpPollerOptions> = {}, +): Promise, TResult>> { + const abortController = new AbortController(); + const poller: LongRunningOperation = { + sendInitialRequest: async () => { + // In the case of Rest Clients we are building the LRO poller object from a response that's the reason + // we are not triggering the initial request here, just extracting the information from the + // response we were provided. + return getLroResponse(initialResponse); + }, + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { + // This is the callback that is going to be called to poll the service + // to get the latest status. We use the client provided and the polling path + // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location + // depending on the lro pattern that the service implements. If non is provided we default to the initial path. + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } + const lroResponse = getLroResponse(response as TResult); + lroResponse.rawResponse.headers["x-ms-original-url"] = + initialResponse.request.url; + return lroResponse; + }, + }; + + options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; +} + +/** + * Converts a Rest Client response to a response that the LRO implementation understands + * @param response - a rest client http response + * @returns - An LRO response that the LRO implementation understands + */ +function getLroResponse( + response: TResult, +): OperationResponse { + if (Number.isNaN(response.status)) { + throw new TypeError( + `Status code of the response is not a number. Value: ${response.status}`, + ); + } + + return { + flatResponse: response, + rawResponse: { + ...response, + statusCode: Number.parseInt(response.status), + body: response.body, + }, + }; +} diff --git a/packages/typespec-ts/package.json b/packages/typespec-ts/package.json index 37ca1eed93..0149749668 100644 --- a/packages/typespec-ts/package.json +++ b/packages/typespec-ts/package.json @@ -36,7 +36,7 @@ "regen-test-baselines": "npm run generate-tsp-only && npm run generate-tsp-only:non-branded", "integration-test:alone": "npm run integration-test:alone:rlc && npm run integration-test:alone:modular", "integration-test:alone:rlc": "cross-env TS_NODE_PROJECT=tsconfig.integration.json mocha -r ts-node/register --experimental-specifier-resolution=node --timeout 36000 ./test/integration/*.spec.ts", - "integration-test:alone:modular": "node --loader ts-node/esm ./test/commands/prepare-deps.ts && cross-env TS_NODE_PROJECT=tsconfig.integration.json mocha -r ts-node/register --experimental-specifier-resolution=node --timeout 36000 ./test/modularIntegration/*.spec.ts", + "integration-test:alone:modular": "cross-env TS_NODE_PROJECT=tsconfig.integration.json mocha -r ts-node/register --experimental-specifier-resolution=node --timeout 36000 ./test/modularIntegration/*.spec.ts", "integration-test:alone:non-branded-rlc": "cross-env TS_NODE_PROJECT=tsconfig.integration.json mocha -r ts-node/register --experimental-specifier-resolution=node --timeout 36000 ./test/nonBrandedIntegration/rlc/*.spec.ts", "integration-test:alone:non-branded-modular": "cross-env TS_NODE_PROJECT=tsconfig.integration.json mocha -r ts-node/register --experimental-specifier-resolution=node --timeout 36000 ./test/nonBrandedIntegration/modular/*.spec.ts", "stop-test-server": "npx cadl-ranch server stop", @@ -69,7 +69,7 @@ "@azure/core-auth": "^1.6.0", "cross-env": "^7.0.3", "@azure/core-paging": "^1.5.0", - "@azure/core-lro": "^2.5.4", + "@azure/core-lro": "3.0.0-beta.1", "@azure/core-rest-pipeline": "^1.14.0", "@azure/abort-controller": "^2.0.0", "@azure/logger": "^1.0.4", diff --git a/packages/typespec-ts/src/modular/buildLroFiles.ts b/packages/typespec-ts/src/modular/buildLroFiles.ts index c8c2e89717..877defaf8c 100644 --- a/packages/typespec-ts/src/modular/buildLroFiles.ts +++ b/packages/typespec-ts/src/modular/buildLroFiles.ts @@ -275,13 +275,10 @@ export function buildGetPollerHelper( } const checkResponseStatus = needUnexpectedHelper ? `if (isUnexpected(response as PathUncheckedResponse)) { - createRestError( - \`Status code of the response is not a number. Value: \${response.status}\`, - response - ); + throw createRestError(response); }` : `if (Number.isNaN(response.status)) { - createRestError( + throw createRestError( \`Status code of the response is not a number. Value: \${response.status}\`, response ); @@ -341,10 +338,13 @@ export function buildGetPollerHelper( ); } let initialResponse: TResponse | undefined = undefined; + const pollAbortController = new AbortController(); const poller: LongRunningOperation = { sendInitialRequest: async () => { if (!getInitialResponse) { - throw new Error("getInitialResponse is required when initializing a new poller"); + throw new Error( + "getInitialResponse is required when initializing a new poller" + ); } initialResponse = await getInitialResponse(); return getLroResponse(initialResponse); @@ -355,14 +355,35 @@ export function buildGetPollerHelper( abortSignal?: AbortSignalLike; } ) => { - const response = await client - .pathUnchecked(path) - .get({ abortSignal: options.abortSignal ?? pollOptions?.abortSignal }); + // The poll request would both listen to the user provided abort signal and the poller's own abort signal + function abortListener(): void { + pollAbortController.abort(); + } + const abortSignal = pollAbortController.signal; + if (options.abortSignal?.aborted) { + pollAbortController.abort(); + } else if (pollOptions?.abortSignal?.aborted) { + pollAbortController.abort(); + } else if (!abortSignal.aborted) { + options.abortSignal?.addEventListener("abort", abortListener, { + once: true + }); + pollOptions?.abortSignal?.addEventListener("abort", abortListener, { + once: true + }); + } + let response; + try { + response = await client.pathUnchecked(path).get({ abortSignal }); + } finally { + options.abortSignal?.removeEventListener("abort", abortListener); + pollOptions?.abortSignal?.removeEventListener("abort", abortListener); + } if (options.initialUrl || initialResponse) { response.headers["x-ms-original-url"] = options.initialUrl ?? initialResponse!.request.url; } - + return getLroResponse(response as TResponse); } }; diff --git a/packages/typespec-ts/src/transform/transformPaths.ts b/packages/typespec-ts/src/transform/transformPaths.ts index 29d1146e81..a1b28ef20b 100644 --- a/packages/typespec-ts/src/transform/transformPaths.ts +++ b/packages/typespec-ts/src/transform/transformPaths.ts @@ -102,7 +102,7 @@ function transformOperation( responseTypes, operationGroupName ), - isPageable: isPagingOperation(program, route) + isPaging: isPagingOperation(program, route) } }; if ( diff --git a/packages/typespec-ts/test/commands/cadl-ranch-list.ts b/packages/typespec-ts/test/commands/cadl-ranch-list.ts index 7c6b06593d..cc9a8df68d 100644 --- a/packages/typespec-ts/test/commands/cadl-ranch-list.ts +++ b/packages/typespec-ts/test/commands/cadl-ranch-list.ts @@ -87,7 +87,7 @@ export const rlcTsps: TypeSpecRanchConfig[] = [ }, { outputPath: "lro/lroRPC", - inputPath: "azure/core/lro/rpc-legacy" + inputPath: "azure/core/lro/rpc" }, { outputPath: "models/inheritance", diff --git a/packages/typespec-ts/test/commands/prepare-deps.ts b/packages/typespec-ts/test/commands/prepare-deps.ts deleted file mode 100644 index da99388184..0000000000 --- a/packages/typespec-ts/test/commands/prepare-deps.ts +++ /dev/null @@ -1,35 +0,0 @@ -import pkg from "chalk"; -import { join, dirname } from "path"; -import { execSync } from "child_process"; -import { fileURLToPath } from "url"; -const { bold } = pkg; -const logError = (str: string) => console.error(bold.red(str)); -export async function installDependencies(path: string) { - const command = `cd ${path} && npm install`; - console.log(`Running command: ${command}`); - const result = execSync(command); - console.log("Command output:", result.toString()); -} - -async function main() { - const paths = ["lro/rpc", "lro/standard"]; - const __filename = fileURLToPath(import.meta.url); - const __dirname = dirname(__filename); - for (const path of paths) { - await installDependencies( - join( - `${__dirname}`, - "..", - "modularIntegration", - "generated", - path, - "generated" - ) - ); - } -} - -main().catch((error) => { - logError(error); - process.exit(-1); -}); diff --git a/packages/typespec-ts/test/integration/generated/lro/lroCore/package.json b/packages/typespec-ts/test/integration/generated/lro/lroCore/package.json index 7e0935de1c..dbd8e8a08a 100644 --- a/packages/typespec-ts/test/integration/generated/lro/lroCore/package.json +++ b/packages/typespec-ts/test/integration/generated/lro/lroCore/package.json @@ -27,8 +27,8 @@ "@azure/core-rest-pipeline": "^1.5.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2", - "@azure/core-lro": "^2.5.4", - "@azure/abort-controller": "^1.0.0" + "@azure/core-lro": "3.0.0-beta.1", + "@azure/abort-controller": "^2.0.0" }, "devDependencies": { "dotenv": "^16.0.0", diff --git a/packages/typespec-ts/test/integration/generated/lro/lroCore/src/pollingHelper.ts b/packages/typespec-ts/test/integration/generated/lro/lroCore/src/pollingHelper.ts index 6f6b8f9a42..5771f8b2f6 100644 --- a/packages/typespec-ts/test/integration/generated/lro/lroCore/src/pollingHelper.ts +++ b/packages/typespec-ts/test/integration/generated/lro/lroCore/src/pollingHelper.ts @@ -2,12 +2,13 @@ // Licensed under the MIT license. import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; import { + CancelOnProgress, CreateHttpPollerOptions, LongRunningOperation, - LroResponse, + OperationResponse, OperationState, - SimplePollerLike, createHttpPoller, } from "@azure/core-lro"; import { @@ -22,6 +23,76 @@ import { ExportOperationDefaultResponse, ExportLogicalResponse, } from "./responses.js"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + /** * Helper function that builds a Poller object to help polling a long running operation. * @param client - Client to use for sending the request to get additional pages. @@ -60,23 +131,42 @@ export async function getLongRunningPoller( initialResponse: TResult, options: CreateHttpPollerOptions> = {}, ): Promise, TResult>> { + const abortController = new AbortController(); const poller: LongRunningOperation = { - requestMethod: initialResponse.request.method, - requestPath: initialResponse.request.url, sendInitialRequest: async () => { // In the case of Rest Clients we are building the LRO poller object from a response that's the reason // we are not triggering the initial request here, just extracting the information from the // response we were provided. return getLroResponse(initialResponse); }, - sendPollRequest: async (path) => { + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { // This is the callback that is going to be called to poll the service // to get the latest status. We use the client provided and the polling path // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location // depending on the lro pattern that the service implements. If non is provided we default to the initial path. - const response = await client - .pathUnchecked(path ?? initialResponse.request.url) - .get(); + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } const lroResponse = getLroResponse(response as TResult); lroResponse.rawResponse.headers["x-ms-original-url"] = initialResponse.request.url; @@ -85,7 +175,45 @@ export async function getLongRunningPoller( }; options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; - return createHttpPoller(poller, options); + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; } /** @@ -95,7 +223,7 @@ export async function getLongRunningPoller( */ function getLroResponse( response: TResult, -): LroResponse { +): OperationResponse { if (Number.isNaN(response.status)) { throw new TypeError( `Status code of the response is not a number. Value: ${response.status}`, diff --git a/packages/typespec-ts/test/integration/generated/lro/lroRPC/README.md b/packages/typespec-ts/test/integration/generated/lro/lroRPC/README.md index 7c2474a420..13494db6ae 100644 --- a/packages/typespec-ts/test/integration/generated/lro/lroRPC/README.md +++ b/packages/typespec-ts/test/integration/generated/lro/lroRPC/README.md @@ -1,6 +1,6 @@ -# Legacy REST client library for JavaScript +# Rpc REST client library for JavaScript -Illustrates bodies templated with Azure Core with long-running operation +Illustrates bodies templated with Azure Core with long-running RPC operation **Please rely heavily on our [REST client docs](https://github.com/Azure/azure-sdk-for-js/blob/main/documentation/rest-clients.md) to use this library** @@ -20,13 +20,13 @@ Key links: ### Install the `@msinternal/lro-rpc` package -Install the Legacy REST client REST client library for JavaScript with `npm`: +Install the Rpc REST client REST client library for JavaScript with `npm`: ```bash npm install @msinternal/lro-rpc ``` -### Create and authenticate a `LegacyClient` +### Create and authenticate a `RpcClient` To use an [Azure Active Directory (AAD) token credential](https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/identity/identity/samples/AzureIdentityExamples.md#authenticating-with-a-pre-fetched-access-token), provide an instance of the desired credential type obtained from the diff --git a/packages/typespec-ts/test/integration/generated/lro/lroRPC/package.json b/packages/typespec-ts/test/integration/generated/lro/lroRPC/package.json index a444e96b06..9334ad464d 100644 --- a/packages/typespec-ts/test/integration/generated/lro/lroRPC/package.json +++ b/packages/typespec-ts/test/integration/generated/lro/lroRPC/package.json @@ -27,8 +27,8 @@ "@azure/core-rest-pipeline": "^1.5.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2", - "@azure/core-lro": "^2.5.4", - "@azure/abort-controller": "^1.0.0" + "@azure/core-lro": "3.0.0-beta.1", + "@azure/abort-controller": "^2.0.0" }, "devDependencies": { "dotenv": "^16.0.0", diff --git a/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/clientDefinitions.ts b/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/clientDefinitions.ts index 2494f79dd3..dc55b5935a 100644 --- a/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/clientDefinitions.ts +++ b/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/clientDefinitions.ts @@ -1,41 +1,27 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { GetJobParameters, CreateJobParameters } from "./parameters.js"; +import { LongRunningRpcParameters } from "./parameters.js"; import { - GetJob200Response, - GetJobDefaultResponse, - CreateJob202Response, - CreateJobDefaultResponse, + LongRunningRpc202Response, + LongRunningRpcDefaultResponse, } from "./responses.js"; import { Client, StreamableMethod } from "@azure-rest/core-client"; -export interface GetJob { - /** Poll a Job */ - get( - options?: GetJobParameters, - ): StreamableMethod; -} - -export interface CreateJob { - /** Creates a Job */ +export interface LongRunningRpc { + /** Generate data. */ post( - options?: CreateJobParameters, - ): StreamableMethod; + options?: LongRunningRpcParameters, + ): StreamableMethod< + LongRunningRpc202Response | LongRunningRpcDefaultResponse + >; } export interface Routes { - /** Resource for '/azure/core/lro/rpc/legacy/create-resource-poll-via-operation-location/jobs/\{jobId\}' has methods for the following verbs: get */ - ( - path: "/azure/core/lro/rpc/legacy/create-resource-poll-via-operation-location/jobs/{jobId}", - jobId: string, - ): GetJob; - /** Resource for '/azure/core/lro/rpc/legacy/create-resource-poll-via-operation-location/jobs' has methods for the following verbs: post */ - ( - path: "/azure/core/lro/rpc/legacy/create-resource-poll-via-operation-location/jobs", - ): CreateJob; + /** Resource for '/azure/core/lro/rpc/generations:submit' has methods for the following verbs: post */ + (path: "/azure/core/lro/rpc/generations:submit"): LongRunningRpc; } -export type LegacyClient = Client & { +export type RpcClient = Client & { path: Routes; }; diff --git a/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/index.ts b/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/index.ts index bdbcf820dc..5646815ff5 100644 --- a/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/index.ts +++ b/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/index.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import LegacyClient from "./legacyClient.js"; +import RpcClient from "./rpcClient.js"; -export * from "./legacyClient.js"; +export * from "./rpcClient.js"; export * from "./parameters.js"; export * from "./responses.js"; export * from "./clientDefinitions.js"; @@ -12,4 +12,4 @@ export * from "./models.js"; export * from "./outputModels.js"; export * from "./pollingHelper.js"; -export default LegacyClient; +export default RpcClient; diff --git a/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/isUnexpected.ts b/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/isUnexpected.ts index f78308470f..d5ca295551 100644 --- a/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/isUnexpected.ts +++ b/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/isUnexpected.ts @@ -2,39 +2,28 @@ // Licensed under the MIT license. import { - GetJob200Response, - GetJobDefaultResponse, - CreateJob202Response, - CreateJobLogicalResponse, - CreateJobDefaultResponse, + LongRunningRpc202Response, + LongRunningRpcLogicalResponse, + LongRunningRpcDefaultResponse, } from "./responses.js"; const responseMap: Record = { - "GET /azure/core/lro/rpc/legacy/create-resource-poll-via-operation-location/jobs/{jobId}": - ["200"], - "POST /azure/core/lro/rpc/legacy/create-resource-poll-via-operation-location/jobs": - ["202"], - "GET /azure/core/lro/rpc/legacy/create-resource-poll-via-operation-location/jobs": - ["200", "202"], + "POST /azure/core/lro/rpc/generations:submit": ["202"], + "GET /azure/core/lro/rpc/generations:submit": ["200", "202"], }; -export function isUnexpected( - response: GetJob200Response | GetJobDefaultResponse, -): response is GetJobDefaultResponse; export function isUnexpected( response: - | CreateJob202Response - | CreateJobLogicalResponse - | CreateJobDefaultResponse, -): response is CreateJobDefaultResponse; + | LongRunningRpc202Response + | LongRunningRpcLogicalResponse + | LongRunningRpcDefaultResponse, +): response is LongRunningRpcDefaultResponse; export function isUnexpected( response: - | GetJob200Response - | GetJobDefaultResponse - | CreateJob202Response - | CreateJobLogicalResponse - | CreateJobDefaultResponse, -): response is GetJobDefaultResponse | CreateJobDefaultResponse { + | LongRunningRpc202Response + | LongRunningRpcLogicalResponse + | LongRunningRpcDefaultResponse, +): response is LongRunningRpcDefaultResponse { const lroOriginal = response.headers["x-ms-original-url"]; const url = new URL(lroOriginal ?? response.request.url); const method = response.request.method; diff --git a/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/models.ts b/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/models.ts index 152b76d0ba..b6a47226cc 100644 --- a/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/models.ts +++ b/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/models.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -/** Data of the job */ -export interface JobData { - /** Comment. */ - comment: string; +/** Options for the generation. */ +export interface GenerationOptions { + /** Prompt. */ + prompt: string; } diff --git a/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/outputModels.ts b/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/outputModels.ts index 1fe890834c..09021f6a53 100644 --- a/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/outputModels.ts +++ b/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/outputModels.ts @@ -1,28 +1,32 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { ErrorResponse } from "@azure-rest/core-client"; +import { ErrorModel } from "@azure-rest/core-client"; -/** Result of the job */ -export interface JobResultOutput { - /** A processing job identifier. */ - readonly jobId: string; - /** Comment. */ - readonly comment: string; - /** The status of the processing job. */ - readonly status: JobStatusOutput; - /** Error objects that describes the error when status is "Failed". */ - readonly errors?: Array; - /** The results. */ - readonly results?: string[]; +/** Options for the generation. */ +export interface GenerationOptionsOutput { + /** Prompt. */ + prompt: string; } -/** Alias for JobStatusOutput */ -export type JobStatusOutput = - | string - | "notStarted" - | "running" - | "Succeeded" - | "Failed" - | "canceled" - | "partiallyCompleted"; +/** Provides status details for long running operations. */ +export interface ResourceOperationStatusOutput { + /** The unique ID of the operation. */ + readonly id: string; + /** + * The status of the operation + * + * Possible values: "NotStarted", "Running", "Succeeded", "Failed", "Canceled" + */ + status: string; + /** Error object that describes the error when status is "Failed". */ + error?: ErrorModel; + /** The result of the operation. */ + result?: GenerationResultOutput; +} + +/** Result of the generation. */ +export interface GenerationResultOutput { + /** The data. */ + data: string; +} diff --git a/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/parameters.ts b/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/parameters.ts index 9edaa6f79b..830060a823 100644 --- a/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/parameters.ts +++ b/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/parameters.ts @@ -2,12 +2,11 @@ // Licensed under the MIT license. import { RequestParameters } from "@azure-rest/core-client"; -import { JobData } from "./models.js"; +import { GenerationOptions } from "./models.js"; -export type GetJobParameters = RequestParameters; - -export interface CreateJobBodyParam { - body?: JobData; +export interface LongRunningRpcBodyParam { + body?: GenerationOptions; } -export type CreateJobParameters = CreateJobBodyParam & RequestParameters; +export type LongRunningRpcParameters = LongRunningRpcBodyParam & + RequestParameters; diff --git a/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/pollingHelper.ts b/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/pollingHelper.ts index 61e152a43e..a8790253d0 100644 --- a/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/pollingHelper.ts +++ b/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/pollingHelper.ts @@ -2,19 +2,90 @@ // Licensed under the MIT license. import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; import { + CancelOnProgress, CreateHttpPollerOptions, LongRunningOperation, - LroResponse, + OperationResponse, OperationState, - SimplePollerLike, createHttpPoller, } from "@azure/core-lro"; import { - CreateJob202Response, - CreateJobDefaultResponse, - CreateJobLogicalResponse, + LongRunningRpc202Response, + LongRunningRpcDefaultResponse, + LongRunningRpcLogicalResponse, } from "./responses.js"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + /** * Helper function that builds a Poller object to help polling a long running operation. * @param client - Client to use for sending the request to get additional pages. @@ -23,10 +94,10 @@ import { * @returns - A poller object to poll for operation state updates and eventually get the final response. */ export async function getLongRunningPoller< - TResult extends CreateJobLogicalResponse | CreateJobDefaultResponse, + TResult extends LongRunningRpcLogicalResponse | LongRunningRpcDefaultResponse, >( client: Client, - initialResponse: CreateJob202Response | CreateJobDefaultResponse, + initialResponse: LongRunningRpc202Response | LongRunningRpcDefaultResponse, options?: CreateHttpPollerOptions>, ): Promise, TResult>>; export async function getLongRunningPoller( @@ -34,23 +105,42 @@ export async function getLongRunningPoller( initialResponse: TResult, options: CreateHttpPollerOptions> = {}, ): Promise, TResult>> { + const abortController = new AbortController(); const poller: LongRunningOperation = { - requestMethod: initialResponse.request.method, - requestPath: initialResponse.request.url, sendInitialRequest: async () => { // In the case of Rest Clients we are building the LRO poller object from a response that's the reason // we are not triggering the initial request here, just extracting the information from the // response we were provided. return getLroResponse(initialResponse); }, - sendPollRequest: async (path) => { + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { // This is the callback that is going to be called to poll the service // to get the latest status. We use the client provided and the polling path // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location // depending on the lro pattern that the service implements. If non is provided we default to the initial path. - const response = await client - .pathUnchecked(path ?? initialResponse.request.url) - .get(); + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } const lroResponse = getLroResponse(response as TResult); lroResponse.rawResponse.headers["x-ms-original-url"] = initialResponse.request.url; @@ -59,7 +149,45 @@ export async function getLongRunningPoller( }; options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; - return createHttpPoller(poller, options); + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; } /** @@ -69,7 +197,7 @@ export async function getLongRunningPoller( */ function getLroResponse( response: TResult, -): LroResponse { +): OperationResponse { if (Number.isNaN(response.status)) { throw new TypeError( `Status code of the response is not a number. Value: ${response.status}`, diff --git a/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/responses.ts b/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/responses.ts index c120c467f9..dd0ecbb92f 100644 --- a/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/responses.ts +++ b/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/responses.ts @@ -3,48 +3,33 @@ import { RawHttpHeaders } from "@azure/core-rest-pipeline"; import { HttpResponse, ErrorResponse } from "@azure-rest/core-client"; -import { JobResultOutput } from "./outputModels.js"; +import { ResourceOperationStatusOutput } from "./outputModels.js"; -/** The request has succeeded. */ -export interface GetJob200Response extends HttpResponse { - status: "200"; - body: JobResultOutput; -} - -export interface GetJobDefaultHeaders { - /** String error code indicating what went wrong. */ - "x-ms-error-code"?: string; -} - -export interface GetJobDefaultResponse extends HttpResponse { - status: string; - body: ErrorResponse; - headers: RawHttpHeaders & GetJobDefaultHeaders; -} - -export interface CreateJob202Headers { +export interface LongRunningRpc202Headers { /** The location for monitoring the operation state. */ "operation-location": string; } /** The request has been accepted for processing, but processing has not yet completed. */ -export interface CreateJob202Response extends HttpResponse { +export interface LongRunningRpc202Response extends HttpResponse { status: "202"; - headers: RawHttpHeaders & CreateJob202Headers; + body: ResourceOperationStatusOutput; + headers: RawHttpHeaders & LongRunningRpc202Headers; } -export interface CreateJobDefaultHeaders { +export interface LongRunningRpcDefaultHeaders { /** String error code indicating what went wrong. */ "x-ms-error-code"?: string; } -export interface CreateJobDefaultResponse extends HttpResponse { +export interface LongRunningRpcDefaultResponse extends HttpResponse { status: string; body: ErrorResponse; - headers: RawHttpHeaders & CreateJobDefaultHeaders; + headers: RawHttpHeaders & LongRunningRpcDefaultHeaders; } -/** The final response for long-running createJob operation */ -export interface CreateJobLogicalResponse extends HttpResponse { +/** The final response for long-running longRunningRpc operation */ +export interface LongRunningRpcLogicalResponse extends HttpResponse { status: "200"; + body: ResourceOperationStatusOutput; } diff --git a/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/legacyClient.ts b/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/rpcClient.ts similarity index 77% rename from packages/typespec-ts/test/integration/generated/lro/lroRPC/src/legacyClient.ts rename to packages/typespec-ts/test/integration/generated/lro/lroRPC/src/rpcClient.ts index af6b810cd3..93bf644f68 100644 --- a/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/legacyClient.ts +++ b/packages/typespec-ts/test/integration/generated/lro/lroRPC/src/rpcClient.ts @@ -3,15 +3,13 @@ import { getClient, ClientOptions } from "@azure-rest/core-client"; import { logger } from "./logger.js"; -import { LegacyClient } from "./clientDefinitions.js"; +import { RpcClient } from "./clientDefinitions.js"; /** - * Initialize a new instance of `LegacyClient` + * Initialize a new instance of `RpcClient` * @param options - the parameter for all optional parameters */ -export default function createClient( - options: ClientOptions = {}, -): LegacyClient { +export default function createClient(options: ClientOptions = {}): RpcClient { const endpointUrl = options.endpoint ?? options.baseUrl ?? `http://localhost:3000`; options.apiVersion = options.apiVersion ?? "2022-12-01-preview"; @@ -30,7 +28,7 @@ export default function createClient( }, }; - const client = getClient(endpointUrl, options) as LegacyClient; + const client = getClient(endpointUrl, options) as RpcClient; return client; } diff --git a/packages/typespec-ts/test/integration/lroCore.spec.ts b/packages/typespec-ts/test/integration/lroCore.spec.ts index 1a1ffd5ab6..a3b82dfa85 100644 --- a/packages/typespec-ts/test/integration/lroCore.spec.ts +++ b/packages/typespec-ts/test/integration/lroCore.spec.ts @@ -15,17 +15,17 @@ describe("AzureLroCoreClient Rest Client", () => { it("should put LRO response", async () => { try { - const initalResponse = await client + const initialResponse = await client .path("/azure/core/lro/standard/users/{name}", "madge") .put({ body: { role: "contributor" } }); - const poller = await getLongRunningPoller(client, initalResponse); + const poller = await getLongRunningPoller(client, initialResponse); const result = await poller.pollUntilDone(); assert.equal(result.status, "200"); - assert.strictEqual(initalResponse.status, "201"); + assert.strictEqual(initialResponse.status, "201"); if (isUnexpected(result)) { throw Error("Unexpected status code"); } @@ -40,17 +40,17 @@ describe("AzureLroCoreClient Rest Client", () => { it("should delete LRO response", async () => { try { - const initalResponse = await client + const initialResponse = await client .path("/azure/core/lro/standard/users/{name}", "madge") .delete({ body: { role: "contributor" } }); - const poller = await getLongRunningPoller(client, initalResponse); + const poller = await getLongRunningPoller(client, initialResponse); const result = await poller.pollUntilDone(); assert.equal(result.status, "200"); - assert.strictEqual(initalResponse.status, "202"); + assert.strictEqual(initialResponse.status, "202"); if (isUnexpected(result)) { throw Error("Unexpected status code"); } @@ -64,17 +64,17 @@ describe("AzureLroCoreClient Rest Client", () => { it("should export LRO response", async () => { try { - const initalResponse = await client + const initialResponse = await client .path("/azure/core/lro/standard/users/{name}:export", "madge") .post({ queryParameters: { format: "json" } }); - const poller = await getLongRunningPoller(client, initalResponse); + const poller = await getLongRunningPoller(client, initialResponse); const result = await poller.pollUntilDone(); assert.equal(result.status, "200"); - assert.strictEqual(initalResponse.status, "202"); + assert.strictEqual(initialResponse.status, "202"); if (isUnexpected(result)) { throw Error("Unexpected status code"); } diff --git a/packages/typespec-ts/test/integration/lroRpc.spec.ts b/packages/typespec-ts/test/integration/lroRpc.spec.ts new file mode 100644 index 0000000000..bc6b36824c --- /dev/null +++ b/packages/typespec-ts/test/integration/lroRpc.spec.ts @@ -0,0 +1,38 @@ +import SpecsAzureCoreLroStandardClientFactory, { + RpcClient, + getLongRunningPoller, + isUnexpected +} from "./generated/lro/lroRPC/src/index.js"; +import { assert } from "chai"; +describe("RpcClient Rest Client", () => { + let client: RpcClient; + + beforeEach(() => { + client = SpecsAzureCoreLroStandardClientFactory({ + allowInsecureConnection: true + }); + }); + + it("should post LRO response", async () => { + try { + const initialResponse = await client + .path("/azure/core/lro/rpc/generations:submit") + .post({ + body: { prompt: "text" } + }); + const poller = await getLongRunningPoller(client, initialResponse); + const result = await poller.pollUntilDone(); + assert.equal(result.status, "200"); + assert.strictEqual(initialResponse.status, "202"); + if (isUnexpected(result)) { + throw Error("Unexpected status code"); + } + assert.equal(result.status, "200"); + if (result.status === "200") { + assert.equal(result.body.result?.data, "text data"); + } + } catch (err) { + assert.fail(err as string); + } + }); +}); diff --git a/packages/typespec-ts/test/modularIntegration/generated/lro/rpc/generated/src/api/pollingHelpers.ts b/packages/typespec-ts/test/modularIntegration/generated/lro/rpc/generated/src/api/pollingHelpers.ts index 6e5c921476..6e3a0a93bc 100644 --- a/packages/typespec-ts/test/modularIntegration/generated/lro/rpc/generated/src/api/pollingHelpers.ts +++ b/packages/typespec-ts/test/modularIntegration/generated/lro/rpc/generated/src/api/pollingHelpers.ts @@ -58,6 +58,7 @@ export function getLongRunningPoller< ); } let initialResponse: TResponse | undefined = undefined; + const pollAbortController = new AbortController(); const poller: LongRunningOperation = { sendInitialRequest: async () => { if (!getInitialResponse) { @@ -74,9 +75,30 @@ export function getLongRunningPoller< abortSignal?: AbortSignalLike; }, ) => { - const response = await client - .pathUnchecked(path) - .get({ abortSignal: options.abortSignal ?? pollOptions?.abortSignal }); + // The poll request would both listen to the user provided abort signal and the poller's own abort signal + function abortListener(): void { + pollAbortController.abort(); + } + const abortSignal = pollAbortController.signal; + if (options.abortSignal?.aborted) { + pollAbortController.abort(); + } else if (pollOptions?.abortSignal?.aborted) { + pollAbortController.abort(); + } else if (!abortSignal.aborted) { + options.abortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + pollOptions?.abortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client.pathUnchecked(path).get({ abortSignal }); + } finally { + options.abortSignal?.removeEventListener("abort", abortListener); + pollOptions?.abortSignal?.removeEventListener("abort", abortListener); + } if (options.initialUrl || initialResponse) { response.headers["x-ms-original-url"] = options.initialUrl ?? initialResponse!.request.url; @@ -104,10 +126,7 @@ function getLroResponse( response: TResponse, ): OperationResponse { if (isUnexpected(response as PathUncheckedResponse)) { - createRestError( - `Status code of the response is not a number. Value: ${response.status}`, - response, - ); + throw createRestError(response); } return { flatResponse: response, diff --git a/packages/typespec-ts/test/modularIntegration/generated/lro/rpc/generated/src/rest/index.ts b/packages/typespec-ts/test/modularIntegration/generated/lro/rpc/generated/src/rest/index.ts index 9d4e905e6f..5646815ff5 100644 --- a/packages/typespec-ts/test/modularIntegration/generated/lro/rpc/generated/src/rest/index.ts +++ b/packages/typespec-ts/test/modularIntegration/generated/lro/rpc/generated/src/rest/index.ts @@ -10,5 +10,6 @@ export * from "./clientDefinitions.js"; export * from "./isUnexpected.js"; export * from "./models.js"; export * from "./outputModels.js"; +export * from "./pollingHelper.js"; export default RpcClient; diff --git a/packages/typespec-ts/test/modularIntegration/generated/lro/rpc/generated/src/rest/pollingHelper.ts b/packages/typespec-ts/test/modularIntegration/generated/lro/rpc/generated/src/rest/pollingHelper.ts new file mode 100644 index 0000000000..a8790253d0 --- /dev/null +++ b/packages/typespec-ts/test/modularIntegration/generated/lro/rpc/generated/src/rest/pollingHelper.ts @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; +import { + CancelOnProgress, + CreateHttpPollerOptions, + LongRunningOperation, + OperationResponse, + OperationState, + createHttpPoller, +} from "@azure/core-lro"; +import { + LongRunningRpc202Response, + LongRunningRpcDefaultResponse, + LongRunningRpcLogicalResponse, +} from "./responses.js"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + +/** + * Helper function that builds a Poller object to help polling a long running operation. + * @param client - Client to use for sending the request to get additional pages. + * @param initialResponse - The initial response. + * @param options - Options to set a resume state or custom polling interval. + * @returns - A poller object to poll for operation state updates and eventually get the final response. + */ +export async function getLongRunningPoller< + TResult extends LongRunningRpcLogicalResponse | LongRunningRpcDefaultResponse, +>( + client: Client, + initialResponse: LongRunningRpc202Response | LongRunningRpcDefaultResponse, + options?: CreateHttpPollerOptions>, +): Promise, TResult>>; +export async function getLongRunningPoller( + client: Client, + initialResponse: TResult, + options: CreateHttpPollerOptions> = {}, +): Promise, TResult>> { + const abortController = new AbortController(); + const poller: LongRunningOperation = { + sendInitialRequest: async () => { + // In the case of Rest Clients we are building the LRO poller object from a response that's the reason + // we are not triggering the initial request here, just extracting the information from the + // response we were provided. + return getLroResponse(initialResponse); + }, + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { + // This is the callback that is going to be called to poll the service + // to get the latest status. We use the client provided and the polling path + // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location + // depending on the lro pattern that the service implements. If non is provided we default to the initial path. + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } + const lroResponse = getLroResponse(response as TResult); + lroResponse.rawResponse.headers["x-ms-original-url"] = + initialResponse.request.url; + return lroResponse; + }, + }; + + options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; +} + +/** + * Converts a Rest Client response to a response that the LRO implementation understands + * @param response - a rest client http response + * @returns - An LRO response that the LRO implementation understands + */ +function getLroResponse( + response: TResult, +): OperationResponse { + if (Number.isNaN(response.status)) { + throw new TypeError( + `Status code of the response is not a number. Value: ${response.status}`, + ); + } + + return { + flatResponse: response, + rawResponse: { + ...response, + statusCode: Number.parseInt(response.status), + body: response.body, + }, + }; +} diff --git a/packages/typespec-ts/test/modularIntegration/generated/lro/standard/generated/src/api/pollingHelpers.ts b/packages/typespec-ts/test/modularIntegration/generated/lro/standard/generated/src/api/pollingHelpers.ts index 6e5c921476..6e3a0a93bc 100644 --- a/packages/typespec-ts/test/modularIntegration/generated/lro/standard/generated/src/api/pollingHelpers.ts +++ b/packages/typespec-ts/test/modularIntegration/generated/lro/standard/generated/src/api/pollingHelpers.ts @@ -58,6 +58,7 @@ export function getLongRunningPoller< ); } let initialResponse: TResponse | undefined = undefined; + const pollAbortController = new AbortController(); const poller: LongRunningOperation = { sendInitialRequest: async () => { if (!getInitialResponse) { @@ -74,9 +75,30 @@ export function getLongRunningPoller< abortSignal?: AbortSignalLike; }, ) => { - const response = await client - .pathUnchecked(path) - .get({ abortSignal: options.abortSignal ?? pollOptions?.abortSignal }); + // The poll request would both listen to the user provided abort signal and the poller's own abort signal + function abortListener(): void { + pollAbortController.abort(); + } + const abortSignal = pollAbortController.signal; + if (options.abortSignal?.aborted) { + pollAbortController.abort(); + } else if (pollOptions?.abortSignal?.aborted) { + pollAbortController.abort(); + } else if (!abortSignal.aborted) { + options.abortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + pollOptions?.abortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client.pathUnchecked(path).get({ abortSignal }); + } finally { + options.abortSignal?.removeEventListener("abort", abortListener); + pollOptions?.abortSignal?.removeEventListener("abort", abortListener); + } if (options.initialUrl || initialResponse) { response.headers["x-ms-original-url"] = options.initialUrl ?? initialResponse!.request.url; @@ -104,10 +126,7 @@ function getLroResponse( response: TResponse, ): OperationResponse { if (isUnexpected(response as PathUncheckedResponse)) { - createRestError( - `Status code of the response is not a number. Value: ${response.status}`, - response, - ); + throw createRestError(response); } return { flatResponse: response, diff --git a/packages/typespec-ts/test/modularIntegration/generated/lro/standard/generated/src/rest/index.ts b/packages/typespec-ts/test/modularIntegration/generated/lro/standard/generated/src/rest/index.ts index 4864f3887d..45a9625d19 100644 --- a/packages/typespec-ts/test/modularIntegration/generated/lro/standard/generated/src/rest/index.ts +++ b/packages/typespec-ts/test/modularIntegration/generated/lro/standard/generated/src/rest/index.ts @@ -10,5 +10,6 @@ export * from "./clientDefinitions.js"; export * from "./isUnexpected.js"; export * from "./models.js"; export * from "./outputModels.js"; +export * from "./pollingHelper.js"; export default StandardClient; diff --git a/packages/typespec-ts/test/modularIntegration/generated/lro/standard/generated/src/rest/pollingHelper.ts b/packages/typespec-ts/test/modularIntegration/generated/lro/standard/generated/src/rest/pollingHelper.ts new file mode 100644 index 0000000000..5771f8b2f6 --- /dev/null +++ b/packages/typespec-ts/test/modularIntegration/generated/lro/standard/generated/src/rest/pollingHelper.ts @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Client, HttpResponse } from "@azure-rest/core-client"; +import { AbortSignalLike } from "@azure/abort-controller"; +import { + CancelOnProgress, + CreateHttpPollerOptions, + LongRunningOperation, + OperationResponse, + OperationState, + createHttpPoller, +} from "@azure/core-lro"; +import { + CreateOrReplace200Response, + CreateOrReplace201Response, + CreateOrReplaceDefaultResponse, + CreateOrReplaceLogicalResponse, + DeleteOperation202Response, + DeleteOperationDefaultResponse, + DeleteLogicalResponse, + ExportOperation202Response, + ExportOperationDefaultResponse, + ExportLogicalResponse, +} from "./responses.js"; + +/** + * A simple poller that can be used to poll a long running operation. + */ +export interface SimplePollerLike< + TState extends OperationState, + TResult, +> { + /** + * Returns true if the poller has finished polling. + */ + isDone(): boolean; + /** + * Returns true if the poller is stopped. + */ + isStopped(): boolean; + /** + * Returns the state of the operation. + */ + getOperationState(): TState; + /** + * Returns the result value of the operation, + * regardless of the state of the poller. + * It can return undefined or an incomplete form of the final TResult value + * depending on the implementation. + */ + getResult(): TResult | undefined; + /** + * Returns a promise that will resolve once a single polling request finishes. + * It does this by calling the update method of the Poller's operation. + */ + poll(options?: { abortSignal?: AbortSignalLike }): Promise; + /** + * Returns a promise that will resolve once the underlying operation is completed. + */ + pollUntilDone(pollOptions?: { + abortSignal?: AbortSignalLike; + }): Promise; + /** + * Invokes the provided callback after each polling is completed, + * sending the current state of the poller's operation. + * + * It returns a method that can be used to stop receiving updates on the given callback function. + */ + onProgress(callback: (state: TState) => void): CancelOnProgress; + + /** + * Returns a promise that could be used for serialized version of the poller's operation + * by invoking the operation's serialize method. + */ + serialize(): Promise; + + /** + * Wait the poller to be submitted. + */ + submitted(): Promise; + + /** + * Returns a string representation of the poller's operation. Similar to serialize but returns a string. + * @deprecated Use serialize() instead. + */ + toString(): string; + + /** + * Stops the poller from continuing to poll. Please note this will only stop the client-side polling + * @deprecated Use abortSignal to stop polling instead. + */ + stopPolling(): void; +} + +/** + * Helper function that builds a Poller object to help polling a long running operation. + * @param client - Client to use for sending the request to get additional pages. + * @param initialResponse - The initial response. + * @param options - Options to set a resume state or custom polling interval. + * @returns - A poller object to poll for operation state updates and eventually get the final response. + */ +export async function getLongRunningPoller< + TResult extends ExportLogicalResponse | ExportOperationDefaultResponse, +>( + client: Client, + initialResponse: ExportOperation202Response | ExportOperationDefaultResponse, + options?: CreateHttpPollerOptions>, +): Promise, TResult>>; +export async function getLongRunningPoller< + TResult extends + | CreateOrReplaceLogicalResponse + | CreateOrReplaceDefaultResponse, +>( + client: Client, + initialResponse: + | CreateOrReplace200Response + | CreateOrReplace201Response + | CreateOrReplaceDefaultResponse, + options?: CreateHttpPollerOptions>, +): Promise, TResult>>; +export async function getLongRunningPoller< + TResult extends DeleteLogicalResponse | DeleteOperationDefaultResponse, +>( + client: Client, + initialResponse: DeleteOperation202Response | DeleteOperationDefaultResponse, + options?: CreateHttpPollerOptions>, +): Promise, TResult>>; +export async function getLongRunningPoller( + client: Client, + initialResponse: TResult, + options: CreateHttpPollerOptions> = {}, +): Promise, TResult>> { + const abortController = new AbortController(); + const poller: LongRunningOperation = { + sendInitialRequest: async () => { + // In the case of Rest Clients we are building the LRO poller object from a response that's the reason + // we are not triggering the initial request here, just extracting the information from the + // response we were provided. + return getLroResponse(initialResponse); + }, + sendPollRequest: async ( + path, + options?: { abortSignal?: AbortSignalLike }, + ) => { + // This is the callback that is going to be called to poll the service + // to get the latest status. We use the client provided and the polling path + // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location + // depending on the lro pattern that the service implements. If non is provided we default to the initial path. + function abortListener(): void { + abortController.abort(); + } + const inputAbortSignal = options?.abortSignal; + const abortSignal = abortController.signal; + if (inputAbortSignal?.aborted) { + abortController.abort(); + } else if (!abortSignal.aborted) { + inputAbortSignal?.addEventListener("abort", abortListener, { + once: true, + }); + } + let response; + try { + response = await client + .pathUnchecked(path ?? initialResponse.request.url) + .get({ abortSignal }); + } finally { + inputAbortSignal?.removeEventListener("abort", abortListener); + } + const lroResponse = getLroResponse(response as TResult); + lroResponse.rawResponse.headers["x-ms-original-url"] = + initialResponse.request.url; + return lroResponse; + }, + }; + + options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true; + const httpPoller = createHttpPoller(poller, options); + const simplePoller: SimplePollerLike, TResult> = { + isDone() { + return httpPoller.isDone; + }, + isStopped() { + return httpPoller.isStopped; + }, + getOperationState() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return httpPoller.operationState; + }, + getResult() { + return httpPoller.result; + }, + toString() { + if (!httpPoller.operationState) { + throw new Error( + "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().", + ); + } + return JSON.stringify({ + state: httpPoller.operationState, + }); + }, + stopPolling() { + abortController.abort(); + }, + onProgress: httpPoller.onProgress, + poll: httpPoller.poll, + pollUntilDone: httpPoller.pollUntilDone, + serialize: httpPoller.serialize, + submitted: httpPoller.submitted, + }; + return simplePoller; +} + +/** + * Converts a Rest Client response to a response that the LRO implementation understands + * @param response - a rest client http response + * @returns - An LRO response that the LRO implementation understands + */ +function getLroResponse( + response: TResult, +): OperationResponse { + if (Number.isNaN(response.status)) { + throw new TypeError( + `Status code of the response is not a number. Value: ${response.status}`, + ); + } + + return { + flatResponse: response, + rawResponse: { + ...response, + statusCode: Number.parseInt(response.status), + body: response.body, + }, + }; +} diff --git a/packages/typespec-ts/test/modularIntegration/lroStardard.spec.ts b/packages/typespec-ts/test/modularIntegration/lroStardard.spec.ts index c2b98b14ec..83d78b0a27 100644 --- a/packages/typespec-ts/test/modularIntegration/lroStardard.spec.ts +++ b/packages/typespec-ts/test/modularIntegration/lroStardard.spec.ts @@ -61,8 +61,7 @@ describe("LROStandardClient Classical Client", () => { assert.deepEqual(states, ["running", "succeeded"]); }); - // Skip this case: https://github.com/Azure/autorest.typescript/issues/2316 - it.skip("should abort signal", async () => { + it("should abort initial request", async () => { const abortController = new AbortController(); const poller = client.createOrReplace( "madge", @@ -73,15 +72,73 @@ describe("LROStandardClient Classical Client", () => { abortSignal: abortController.signal } ); - await poller.submitted(); - assert.strictEqual(poller.operationState?.status, "running"); abortController.abort(); - poller.poll(); - assert.strictEqual(poller.operationState?.status, "cancelled"); + try { + await poller.submitted(); + assert.fail("Should throw an AbortError"); + } catch (err: any) { + assert.strictEqual(err.message, "The operation was aborted."); + } + }); + + it("should abort polling request", async () => { + const abortController = new AbortController(); + const poller = client.createOrReplace( + "madge", + { + role: "contributor" + } as any, + { + abortSignal: abortController.signal + } + ); + try { + await poller.submitted(); + abortController.abort(); + await poller.poll(); + assert.fail("Should throw an AbortError"); + } catch (err: any) { + assert.strictEqual(err.message, "The operation was aborted."); + } + }); + + it("should abort pollUntilDone request", async () => { + const abortController = new AbortController(); + const poller = client.createOrReplace("madge", { + role: "contributor" + } as any); + abortController.abort(); + try { + await poller.pollUntilDone({ abortSignal: abortController.signal }); + assert.fail("Should throw an AbortError"); + } catch (err: any) { + assert.strictEqual(err.message, "The operation was aborted."); + } + }); + + it("should abort polling request if both method and polling abort is set", async () => { + const methodAbort = new AbortController(); + const pollAbort = new AbortController(); + const poller = client.createOrReplace( + "madge", + { + role: "contributor" + } as any, + { + abortSignal: methodAbort.signal + } + ); + + try { + pollAbort.abort(); + await poller.pollUntilDone({ abortSignal: pollAbort.signal }); + assert.fail("Should throw an AbortError"); + } catch (err: any) { + assert.strictEqual(err.message, "The operation was aborted."); + } }); - // Skip this case: https://github.com/Azure/azure-sdk-for-js/issues/28694 - it.skip("submitted should catch the initial error", async () => { + it("submitted should catch the initial error", async () => { try { const poller = client.createOrReplace("madge", { role: "foo" @@ -92,12 +149,12 @@ describe("LROStandardClient Classical Client", () => { } catch (err: any) { assert.strictEqual( err.message, - "The long-running operation has failed" + "Body provided doesn't match expected body" ); } }); - it("poll should catch the inital error", async () => { + it("poll should catch the initial error", async () => { try { const poller = client.createOrReplace("madge", { role: "foo" @@ -108,12 +165,12 @@ describe("LROStandardClient Classical Client", () => { } catch (err: any) { assert.strictEqual( err.message, - "The long-running operation has failed" + "Body provided doesn't match expected body" ); } }); - it("pollUntilDone should catch the inital error", async () => { + it("pollUntilDone should catch the initial error", async () => { try { const poller = client.createOrReplace("madge", { role: "foo" @@ -124,12 +181,12 @@ describe("LROStandardClient Classical Client", () => { } catch (err: any) { assert.strictEqual( err.message, - "The long-running operation has failed" + "Body provided doesn't match expected body" ); } }); - it("await should catch inital exception", async () => { + it("await should catch initial exception", async () => { try { await client.createOrReplace("madge", { role: "foo" @@ -138,7 +195,7 @@ describe("LROStandardClient Classical Client", () => { } catch (err: any) { assert.strictEqual( err.message, - "The long-running operation has failed" + "Body provided doesn't match expected body" ); } });