├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── fabricbot.json └── workflows │ └── codeQL.yml ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── azure-pipelines ├── code-mirror.yml ├── official-build.yml ├── public-build.yml ├── release.yml └── templates │ ├── build.yml │ └── test.yml ├── package-lock.json ├── package.json ├── samples-js ├── .funcignore ├── .gitignore ├── functions │ ├── backupSiteContent.js │ ├── callActivityWithRetry.js │ ├── callSubOrchestratorWithRetry.js │ ├── cancelTimer.js │ ├── continueAsNewCounter.js │ ├── counter.js │ ├── httpStart.js │ ├── httpSyncStart.js │ ├── listAzureSubscriptions.js │ ├── sayHello.js │ ├── smsPhoneVerification.js │ └── weatherMonitor.js ├── host.json ├── package-lock.json └── package.json ├── samples-ts ├── .funcignore ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── functions │ ├── backupSiteContent.ts │ ├── callActivityWithRetry.ts │ ├── callSubOrchestratorWithRetry.ts │ ├── cancelTimer.ts │ ├── continueAsNewCounter.ts │ ├── counter.ts │ ├── httpStart.ts │ ├── httpSyncStart.ts │ ├── listAzureSubscriptions.ts │ ├── sayHello.ts │ ├── smsPhoneVerification.ts │ └── weatherMonitor.ts ├── host.json ├── package-lock.json ├── package.json └── tsconfig.json ├── src ├── Constants.ts ├── ManagedIdentityTokenSource.ts ├── RetryOptions.ts ├── actions │ ├── ActionType.ts │ ├── CallActivityAction.ts │ ├── CallActivityWithRetryAction.ts │ ├── CallEntityAction.ts │ ├── CallHttpAction.ts │ ├── CallSubOrchestratorAction.ts │ ├── CallSubOrchestratorWithRetryAction.ts │ ├── ContinueAsNewAction.ts │ ├── CreateTimerAction.ts │ ├── ExternalEventType.ts │ ├── IAction.ts │ ├── SignalEntityAction.ts │ ├── WaitForExternalEventAction.ts │ ├── WhenAllAction.ts │ └── WhenAnyAction.ts ├── app.ts ├── client.ts ├── durableClient │ ├── DurableClient.ts │ ├── OrchestrationClientInputData.ts │ ├── PurgeHistoryResult.ts │ └── getClient.ts ├── entities │ ├── DurableEntityBindingInfo.ts │ ├── DurableLock.ts │ ├── Entity.ts │ ├── EntityId.ts │ ├── EntityState.ts │ ├── EntityStateResponse.ts │ ├── LockState.ts │ ├── OperationResult.ts │ ├── RequestMessage.ts │ ├── ResponseMessage.ts │ └── Signal.ts ├── error │ ├── AggregatedError.ts │ ├── DurableError.ts │ └── OrchestrationFailureError.ts ├── history │ ├── EventRaisedEvent.ts │ ├── EventSentEvent.ts │ ├── ExecutionStartedEvent.ts │ ├── HistoryEvent.ts │ ├── HistoryEventOptions.ts │ ├── HistoryEventType.ts │ ├── OrchestratorCompletedEvent.ts │ ├── OrchestratorStartedEvent.ts │ ├── SubOrchestrationInstanceCompletedEvent.ts │ ├── SubOrchestrationInstanceCreatedEvent.ts │ ├── SubOrchestrationInstanceFailedEvent.ts │ ├── TaskCompletedEvent.ts │ ├── TaskFailedEvent.ts │ ├── TaskScheduledEvent.ts │ ├── TimerCreatedEvent.ts │ └── TimerFiredEvent.ts ├── http │ ├── DurableHttpRequest.ts │ ├── DurableHttpResponse.ts │ ├── HttpCreationPayload.ts │ └── HttpManagementPayload.ts ├── index.ts ├── input.ts ├── orchestrations │ ├── DurableOrchestrationBindingInfo.ts │ ├── DurableOrchestrationContext.ts │ ├── DurableOrchestrationStatus.ts │ ├── IOrchestratorState.ts │ ├── OrchestrationRuntimeStatus.ts │ ├── Orchestrator.ts │ ├── OrchestratorState.ts │ ├── ReplaySchema.ts │ └── TaskOrchestrationExecutor.ts ├── task │ ├── AtomicTask.ts │ ├── CallHttpWithPollingTask.ts │ ├── CompoundTask.ts │ ├── DFTask.ts │ ├── DFTimerTask.ts │ ├── LongTimerTask.ts │ ├── NoOpTask.ts │ ├── RegisteredActivityTask.ts │ ├── RegisteredOrchestrationTask.ts │ ├── RetryableTask.ts │ ├── TaskBase.ts │ ├── WhenAllTask.ts │ ├── WhenAnyTask.ts │ └── index.ts ├── trigger.ts └── util │ ├── GuidManager.ts │ ├── Utils.ts │ ├── WebhookUtils.ts │ └── testingUtils.ts ├── test ├── integration │ ├── entity-spec.ts │ └── orchestrator-spec.ts ├── test-app │ ├── .funcignore │ ├── .gitignore │ ├── .vscode │ │ ├── extensions.json │ │ ├── launch.json │ │ ├── settings.json │ │ └── tasks.json │ ├── host.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── functions │ │ │ ├── counter1.ts │ │ │ └── hello.ts │ └── tsconfig.json ├── testobjects │ ├── TestOrchestrations.ts │ ├── testconstants.ts │ ├── testentities.ts │ ├── testentitybatches.ts │ ├── testentityoperations.ts │ ├── testhistories.ts │ └── testutils.ts └── unit │ ├── durableclient-spec.ts │ ├── entityid-spec.ts │ ├── getclient-spec.ts │ ├── guidmanager-spec.ts │ ├── orchestrationclient-spec.ts │ ├── retryoptions-spec.ts │ ├── shim-spec.ts │ ├── timertask-spec.ts │ └── utils-spec.ts ├── tsconfig.json ├── tsconfig.nocomments └── types ├── activity.d.ts ├── app.client.d.ts ├── app.d.ts ├── durableClient.d.ts ├── entity.d.ts ├── index.d.ts ├── input.d.ts ├── orchestration.d.ts ├── task.d.ts └── trigger.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editor configuration powered by http://editorconfig.org/ 2 | ; Top-most EditorConfig file 3 | root = true 4 | 5 | [*] 6 | trim_trailing_whitespace = true 7 | end_of_line = crlf 8 | indent_style = space 9 | indent_size = 4 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | lib/** 3 | samples/node_modules/** 4 | test/test-app/node_modules/** 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier/@typescript-eslint", 6 | "plugin:prettier/recommended" 7 | ], 8 | "rules": { 9 | "@typescript-eslint/no-use-before-define": "off", 10 | "@typescript-eslint/no-explicit-any": "off", 11 | "@typescript-eslint/no-namespace": "off", 12 | "@typescript-eslint/interface-name-prefix": "off", 13 | "@typescript-eslint/ban-types": "off" 14 | }, 15 | "overrides": [ 16 | { 17 | "files": ["*.js"], 18 | "rules": { 19 | "@typescript-eslint/no-var-requires": "off" 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. Please make an effort to fill in all the sections below; the information will help us investigate your issue. 12 | 13 | **Investigative information** 14 | 15 | - Durable Functions extension version: 16 | - durable-functions npm module version: 17 | - Language (JavaScript/TypeScript) and version: 18 | - Node.js version: 19 | 20 | ***If deployed to Azure App Service*** 21 | 22 | - Timeframe issue observed: 23 | - Function App name: 24 | - Function name(s): 25 | - Region: 26 | - Orchestration instance ID(s): 27 | 28 | > If you don't want to share your Function App name or Functions names on GitHub, please be sure to provide your Invocation ID, Timestamp, and Region - we can use this to look up your Function App/Function. Provide an invocation id per Function. See the [Functions Host wiki](https://github.com/Azure/azure-webjobs-sdk-script/wiki/Sharing-Your-Function-App-name-privately) for more details. 29 | 30 | **To Reproduce** 31 | Steps to reproduce the behavior: 32 | 33 | 1. Go to '...' 34 | 2. Click on '....' 35 | 3. Scroll down to '....' 36 | 4. See error 37 | 38 | > While not required, providing your orchestrator's source code in anonymized form is often very helpful when investigating unexpected orchestrator behavior. 39 | 40 | **Expected behavior** 41 | A clear and concise description of what you expected to happen. 42 | 43 | **Actual behavior** 44 | A clear and concise description of what actually happened. 45 | 46 | **Screenshots** 47 | If applicable, add screenshots to help explain your problem. 48 | 49 | **Known workarounds** 50 | Provide a description of any known workarounds you used. 51 | 52 | **Additional context** 53 | 54 | - Development environment (ex. Visual Studio) 55 | - Links to source 56 | - Additional bindings used 57 | - Function invocation IDs 58 | -------------------------------------------------------------------------------- /.github/workflows/codeQL.yml: -------------------------------------------------------------------------------- 1 | # This workflow generates weekly CodeQL reports for this repo, a security requirements. 2 | # The workflow is adapted from the following reference: https://github.com/Azure-Samples/azure-functions-python-stream-openai/pull/2/files 3 | # Generic comments on how to modify these file are left intactfor future maintenance. 4 | 5 | name: "CodeQL" 6 | 7 | on: 8 | push: 9 | branches: [ "v3.x", "v2.x" ] 10 | pull_request: 11 | branches: [ "v3.x", "v2.x"] 12 | schedule: 13 | - cron: '0 0 * * 1' # Weekly Monday run, needed for weekly reports 14 | workflow_call: # allows to be invoked as part of a larger workflow 15 | workflow_dispatch: # allows for the workflow to run manually see: https://docs.github.com/en/actions/using-workflows/manually-running-a-workflow 16 | 17 | jobs: 18 | 19 | analyze: 20 | name: Analyze 21 | runs-on: windows-latest 22 | permissions: 23 | actions: read 24 | contents: read 25 | security-events: write 26 | 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: ['typescript'] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 33 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 34 | 35 | steps: 36 | # Initializes the CodeQL tools for scanning. 37 | - name: Initialize CodeQL 38 | uses: github/codeql-action/init@v3 39 | with: 40 | languages: ${{ matrix.language }} 41 | # If you wish to specify custom queries, you can do so here or in a config file. 42 | # By default, queries listed here will override any specified in a config file. 43 | # Prefix the list here with "+" to use these queries and those in the config file. 44 | 45 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 46 | # queries: security-extended,security-and-quality 47 | 48 | - uses: actions/checkout@v3 49 | with: 50 | submodules: true 51 | 52 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 53 | # If this step fails, then you should remove it and run the build manually (see below) 54 | - name: Autobuild 55 | uses: github/codeql-action/autobuild@v2 56 | 57 | # Run CodeQL analysis 58 | - name: Perform CodeQL Analysis 59 | uses: github/codeql-action/analyze@v3 60 | with: 61 | category: "/language:${{matrix.language}}" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | npm-debug.log 4 | lib/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "true": false, 5 | "singleQuote": false, 6 | "printWidth": 100, 7 | "endOfLine": "auto" 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "editorconfig.editorconfig", 5 | "eg2.vscode-npm-script", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceRoot}\\test\\test.ts", 12 | "sourceMaps": true, 13 | "cwd": "${workspaceRoot}\\lib\\test", 14 | "outFiles": [ 15 | "${workspaceRoot}/lib/**/*.js" 16 | ], 17 | "env": { 18 | "DEBUG": "*" 19 | }, 20 | "preLaunchTask": "build" 21 | }, 22 | { 23 | "type": "node", 24 | "request": "attach", 25 | "name": "Attach by Process ID", 26 | "outFiles": [ 27 | "${workspaceRoot}/lib/**/*.js" 28 | ], 29 | "processId": "${command:PickProcess}" 30 | }, 31 | { 32 | "type": "node", 33 | "request": "attach", 34 | "name": "Attach", 35 | "outFiles": [ 36 | "${workspaceRoot}/lib/**/*.js" 37 | ], 38 | "port": 5858 39 | }, 40 | { 41 | "type": "node", 42 | "request": "launch", 43 | "name": "Mocha Tests", 44 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 45 | "args": [ 46 | "--colors", 47 | "${workspaceFolder}/lib/test/**/**-spec.js", 48 | "-g", 49 | ".*" 50 | ], 51 | "internalConsoleOptions": "openOnSessionStart", 52 | "sourceMaps": true, 53 | "outFiles": [ 54 | "${workspaceFolder}/lib/**" 55 | ], 56 | "preLaunchTask": "build" 57 | }, 58 | { 59 | "type": "node", 60 | "request": "launch", 61 | "name": "Mocha Tests Debug", 62 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 63 | "args": [ 64 | "--colors", 65 | "${workspaceFolder}/lib/test/**/**-spec.js", 66 | "-g", 67 | ".*", 68 | "--timeout", 69 | "300000" 70 | ], 71 | "internalConsoleOptions": "openOnSessionStart", 72 | "sourceMaps": true, 73 | "outFiles": [ 74 | "${workspaceFolder}/lib/**" 75 | ], 76 | "preLaunchTask": "build" 77 | }, 78 | { 79 | "name": "Attach to Node Functions", 80 | "type": "node", 81 | "request": "attach", 82 | "port": 9229, 83 | "preLaunchTask": "func: host start" 84 | } 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib", 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.formatOnSave": true, 5 | "files.exclude": { 6 | "obj": true, 7 | "bin": true 8 | }, 9 | "azureFunctions.deploySubpath": "samples", 10 | "azureFunctions.postDeployTask": "npm install", 11 | "azureFunctions.projectLanguage": "TypeScript", 12 | "azureFunctions.projectRuntime": "~3", 13 | "debug.internalConsoleOptions": "neverOpen", 14 | "azureFunctions.preDeployTask": "npm prune", 15 | "mochaExplorer.require": "ts-node/register", 16 | "mochaExplorer.files": "test/**/*.ts" 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "command": "npm", 6 | "tasks": [ 7 | { 8 | "label": "install", 9 | "type": "shell", 10 | "args": [ 11 | "install" 12 | ], 13 | "problemMatcher": [] 14 | }, 15 | { 16 | "label": "update", 17 | "type": "shell", 18 | "args": [ 19 | "update" 20 | ], 21 | "problemMatcher": [] 22 | }, 23 | { 24 | "label": "test", 25 | "type": "shell", 26 | "args": [ 27 | "run", 28 | "test" 29 | ], 30 | "problemMatcher": [], 31 | "group": "test" 32 | }, 33 | { 34 | "label": "build", 35 | "type": "shell", 36 | "args": [ 37 | "run", 38 | "build" 39 | ], 40 | "problemMatcher": [], 41 | "group": "build" 42 | }, 43 | { 44 | "type": "func", 45 | "command": "host start", 46 | "problemMatcher": "$func-node-watch", 47 | "isBackground": true, 48 | "dependsOn": "npm build", 49 | "options": { 50 | "cwd": "${workspaceFolder}/samples" 51 | } 52 | }, 53 | { 54 | "type": "shell", 55 | "label": "npm build", 56 | "command": "npm run build", 57 | "dependsOn": [ 58 | "func: extensions install", 59 | "npm install" 60 | ], 61 | "problemMatcher": "$tsc", 62 | "options": { 63 | "cwd": "${workspaceFolder}/samples" 64 | } 65 | }, 66 | { 67 | "type": "shell", 68 | "label": "npm install", 69 | "command": "npm install", 70 | "options": { 71 | "cwd": "${workspaceFolder}/samples" 72 | } 73 | }, 74 | { 75 | "type": "shell", 76 | "label": "npm prune", 77 | "command": "npm prune --production", 78 | "dependsOn": "npm build", 79 | "problemMatcher": [], 80 | "options": { 81 | "cwd": "${workspaceFolder}/samples" 82 | } 83 | } 84 | ] 85 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Microsoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /azure-pipelines/code-mirror.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - v2.x 5 | - v3.x 6 | 7 | resources: 8 | repositories: 9 | - repository: eng 10 | type: git 11 | name: engineering 12 | ref: refs/tags/release 13 | 14 | variables: 15 | - template: ci/variables/cfs.yml@eng 16 | 17 | extends: 18 | template: ci/code-mirror.yml@eng 19 | -------------------------------------------------------------------------------- /azure-pipelines/official-build.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - v3.x 5 | batch: true 6 | 7 | # CI only 8 | pr: none 9 | 10 | schedules: 11 | # Build nightly to catch any new CVEs and report SDL often. 12 | # We are also required to generated CodeQL reports weekly, so this 13 | # helps us meet that. 14 | - cron: "0 0 * * *" 15 | displayName: Nightly Build 16 | branches: 17 | include: 18 | - v3.x 19 | always: true 20 | 21 | resources: 22 | repositories: 23 | - repository: 1esPipelines 24 | type: git 25 | name: 1ESPipelineTemplates/1ESPipelineTemplates 26 | ref: refs/tags/release 27 | 28 | extends: 29 | template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines 30 | parameters: 31 | pool: 32 | name: 1es-pool-azfunc 33 | image: 1es-windows-2022 34 | os: windows 35 | 36 | stages: 37 | - stage: WindowsUnitTests 38 | dependsOn: [] 39 | jobs: 40 | - template: /azure-pipelines/templates/test.yml@self 41 | 42 | - stage: LinuxUnitTests 43 | dependsOn: [] 44 | jobs: 45 | - template: /azure-pipelines/templates/test.yml@self 46 | pool: 47 | name: 1es-pool-azfunc 48 | image: 1es-ubuntu-22.04 49 | os: linux 50 | 51 | - stage: Build 52 | dependsOn: [] 53 | jobs: 54 | - template: /azure-pipelines/templates/build.yml@self 55 | -------------------------------------------------------------------------------- /azure-pipelines/public-build.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - v3.x 5 | batch: true 6 | 7 | pr: 8 | branches: 9 | include: 10 | - v3.x 11 | 12 | resources: 13 | repositories: 14 | - repository: 1esPipelines 15 | type: git 16 | name: 1ESPipelineTemplates/1ESPipelineTemplates 17 | ref: refs/tags/release 18 | 19 | extends: 20 | template: v1/1ES.Unofficial.PipelineTemplate.yml@1esPipelines 21 | parameters: 22 | pool: 23 | name: 1es-pool-azfunc-public 24 | image: 1es-windows-2022 25 | os: windows 26 | 27 | settings: 28 | # PR's from forks do not have sufficient permissions to set tags. 29 | skipBuildTagsForGitHubPullRequests: ${{ variables['System.PullRequest.IsFork'] }} 30 | 31 | stages: 32 | - stage: WindowsUnitTests 33 | dependsOn: [] 34 | jobs: 35 | - template: /azure-pipelines/templates/test.yml@self 36 | 37 | - stage: LinuxUnitTests 38 | dependsOn: [] 39 | jobs: 40 | - template: /azure-pipelines/templates/test.yml@self 41 | pool: 42 | name: 1es-pool-azfunc-public 43 | image: 1es-ubuntu-22.04 44 | os: linux 45 | 46 | - stage: Build 47 | dependsOn: [] 48 | jobs: 49 | - template: /azure-pipelines/templates/build.yml@self 50 | -------------------------------------------------------------------------------- /azure-pipelines/release.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: NpmPublishTag 3 | displayName: "Tag" 4 | type: string 5 | default: "latest" 6 | - name: NpmPublishDryRun 7 | displayName: "Dry Run" 8 | type: boolean 9 | default: true 10 | 11 | trigger: none 12 | pr: none 13 | 14 | resources: 15 | repositories: 16 | - repository: 1es 17 | type: git 18 | name: 1ESPipelineTemplates/1ESPipelineTemplates 19 | ref: refs/tags/release 20 | pipelines: 21 | - pipeline: DurableJSCI 22 | project: internal 23 | source: durable-js.official 24 | branch: v3.x 25 | 26 | extends: 27 | template: v1/1ES.Official.PipelineTemplate.yml@1es 28 | parameters: 29 | sdl: 30 | sourceAnalysisPool: 31 | name: 1es-pool-azfunc 32 | image: 1es-windows-2022 33 | os: windows 34 | codeql: 35 | runSourceLanguagesInSourceAnalysis: true 36 | 37 | stages: 38 | - stage: Release 39 | pool: 40 | name: 1es-pool-azfunc 41 | image: 1es-ubuntu-22.04 42 | os: linux 43 | jobs: 44 | - job: Release 45 | steps: 46 | - task: NodeTool@0 47 | displayName: "Install Node.js" 48 | inputs: 49 | versionSpec: 14.x 50 | - download: DurableJSCI 51 | - script: mv *.tgz package.tgz 52 | displayName: "Rename tgz file" # because the publish command below requires an exact path 53 | workingDirectory: "$(Pipeline.Workspace)/DurableJSCI/drop" 54 | - task: Npm@1 55 | displayName: "npm publish" 56 | inputs: 57 | command: custom 58 | workingDir: "$(Pipeline.Workspace)/DurableJSCI/drop" 59 | verbose: true 60 | customCommand: "publish package.tgz --tag ${{ parameters.NpmPublishTag }} --dry-run ${{ lower(parameters.NpmPublishDryRun) }}" 61 | customEndpoint: "durable-functions npm (valid until Jan 1st 2025)" 62 | -------------------------------------------------------------------------------- /azure-pipelines/templates/build.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - job: 3 | templateContext: 4 | outputs: 5 | - output: pipelineArtifact 6 | path: $(Build.ArtifactStagingDirectory) 7 | artifact: drop 8 | sbomBuildDropPath: "$(System.DefaultWorkingDirectory)" 9 | sbomPackageName: "Durable Functions for Node.js" 10 | # The list of components can't be determined from the webpacked file in the staging dir, so reference the original node_modules folder 11 | sbomBuildComponentPath: "$(Build.SourcesDirectory)/node_modules" 12 | steps: 13 | - task: NodeTool@0 14 | inputs: 15 | versionSpec: 20.x 16 | displayName: "Install Node.js" 17 | - script: npm ci 18 | displayName: "npm ci" 19 | - script: npm run-script build 20 | displayName: "npm run-script build" 21 | - script: npm prune --production 22 | displayName: "npm prune --production" # so that only production dependencies are included in SBOM 23 | - script: npm pack 24 | displayName: "pack npm package" 25 | - task: CopyFiles@2 26 | displayName: "Copy package to staging" 27 | inputs: 28 | SourceFolder: $(System.DefaultWorkingDirectory) 29 | Contents: "*.tgz" 30 | TargetFolder: $(Build.ArtifactStagingDirectory) 31 | -------------------------------------------------------------------------------- /azure-pipelines/templates/test.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - job: UnitTests 3 | 4 | strategy: 5 | matrix: 6 | Node18: 7 | NODE_VERSION: "18.x" 8 | Node20: 9 | NODE_VERSION: "20.x" 10 | 11 | steps: 12 | - task: NodeTool@0 13 | inputs: 14 | versionSpec: $(NODE_VERSION) 15 | displayName: "Install Node dependencies" 16 | - script: npm ci 17 | displayName: "npm ci" 18 | - script: npm run test 19 | displayName: "npm build and test" 20 | - script: npm run test:nolint 21 | displayName: "npm build and test (no linting)" 22 | - script: npm run build 23 | displayName: "npm run build" 24 | - script: npm pack 25 | displayName: "npm pack" 26 | - script: mv durable-functions-*.tgz package.tgz 27 | displayName: "Rename package file" 28 | - task: CopyFiles@2 29 | displayName: "Create smoke test app" 30 | inputs: 31 | SourceFolder: "$(System.DefaultWorkingDirectory)/test/test-app" 32 | Contents: "**" 33 | TargetFolder: "$(Agent.BuildDirectory)/test-app" 34 | CleanTargetFolder: true 35 | - script: npm install $(System.DefaultWorkingDirectory)/package.tgz 36 | displayName: "Install packed durable-functions module (test app)" 37 | workingDirectory: $(Agent.BuildDirectory)/test-app 38 | - script: npm install 39 | displayName: "npm install (test app)" 40 | workingDirectory: $(Agent.BuildDirectory)/test-app 41 | - script: npm run build 42 | displayName: "Build smoke test app" 43 | workingDirectory: "$(Agent.BuildDirectory)/test-app" 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "durable-functions", 3 | "version": "3.1.0", 4 | "description": "Durable Functions library for Node.js Azure Functions", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/Azure/azure-functions-durable-js.git" 9 | }, 10 | "author": "Microsoft Corporation", 11 | "keywords": [ 12 | "azure-functions" 13 | ], 14 | "files": [ 15 | "lib/src", 16 | "types/" 17 | ], 18 | "main": "lib/src/index.js", 19 | "types": "types/index.d.ts", 20 | "engines": { 21 | "node": ">=18.0" 22 | }, 23 | "scripts": { 24 | "clean": "rimraf lib", 25 | "lint": "eslint --ext .ts,.json src/", 26 | "lint:test": "eslint --ext .ts,.json test/", 27 | "lint:samples": "eslint --ext .ts,.js samples/", 28 | "lint:srcfix": "eslint --ext .ts,.json src/ --fix", 29 | "lint:testfix": "eslint --ext .ts,.json test/ --fix", 30 | "lint:samplesfix": "eslint --ext .ts,.js samples/ --fix", 31 | "build": "npm install && npm run clean && npm run lint && npm run lint:test && npx tsc && npm run stripInternalDocs && echo Done", 32 | "build:samples": "npm --prefix samples run build", 33 | "build:prod": "npm install --production", 34 | "validate:samples": "npm run build && npm --prefix samples install && npm run build:samples", 35 | "build:nolint": "npm run clean && npm run stripInternalDocs && echo Done", 36 | "stripInternalDocs": "tsc --pretty -p tsconfig.nocomments", 37 | "test": "npm run build && mocha --recursive ./lib/test/**/*-spec.js", 38 | "test:nolint": "npm run build:nolint && mocha --recursive ./lib/test/**/*-spec.js", 39 | "watch": "tsc --watch", 40 | "watch:test": "npm run test -- --watch", 41 | "docs": "typedoc --excludePrivate --mode file --out ./lib/docs ./src", 42 | "e2etst": "npm run" 43 | }, 44 | "dependencies": { 45 | "@azure/functions": "^4.0.0", 46 | "@opentelemetry/api": "^1.9.0", 47 | "axios": "^1.6.1", 48 | "debug": "~2.6.9", 49 | "lodash": "^4.17.15", 50 | "moment": "^2.29.2", 51 | "uuid": "^9.0.1", 52 | "validator": "~13.7.0" 53 | }, 54 | "devDependencies": { 55 | "@types/chai": "~4.1.6", 56 | "@types/chai-as-promised": "~7.1.0", 57 | "@types/chai-string": "~1.4.1", 58 | "@types/debug": "0.0.29", 59 | "@types/lodash": "^4.14.119", 60 | "@types/mocha": "^7.0.2", 61 | "@types/nock": "^9.3.0", 62 | "@types/node": "^18.0.0", 63 | "@types/rimraf": "0.0.28", 64 | "@types/sinon": "~5.0.5", 65 | "@types/uuid": "^9.0.7", 66 | "@types/validator": "^9.4.3", 67 | "@typescript-eslint/eslint-plugin": "^5.4.0", 68 | "@typescript-eslint/parser": "^5.4.0", 69 | "chai": "~4.2.0", 70 | "chai-as-promised": "~7.1.1", 71 | "chai-string": "~1.5.0", 72 | "eslint": "^7.32.0", 73 | "eslint-config-prettier": "^6.15.0", 74 | "eslint-plugin-prettier": "^3.4.1", 75 | "mocha": "^11.2.2", 76 | "nock": "^10.0.6", 77 | "prettier": "^2.0.5", 78 | "rimraf": "~2.5.4", 79 | "sinon": "~7.1.1", 80 | "ts-node": "^10.0.0", 81 | "typedoc": "^0.22.11", 82 | "typescript": "~4.5.0" 83 | }, 84 | "bugs": { 85 | "url": "https://github.com/Azure/azure-functions-durable-js/issues" 86 | }, 87 | "homepage": "https://github.com/Azure/azure-functions-durable-js#readme", 88 | "directories": { 89 | "lib": "lib", 90 | "test": "test" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /samples-js/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | local.settings.json 6 | test 7 | tsconfig.json -------------------------------------------------------------------------------- /samples-js/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | .env.test 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # DynamoDB Local files 84 | .dynamodb/ 85 | 86 | # TypeScript output 87 | dist 88 | out 89 | 90 | # Azure Functions artifacts 91 | bin 92 | obj 93 | appsettings.json 94 | local.settings.json 95 | 96 | # Azurite artifacts 97 | __blobstorage__ 98 | __queuestorage__ 99 | __azurite_db*__.json -------------------------------------------------------------------------------- /samples-js/functions/backupSiteContent.js: -------------------------------------------------------------------------------- 1 | const df = require("durable-functions"); 2 | const fs = require("fs/promises"); 3 | const readdirp = require("readdirp"); 4 | const path = require("path"); 5 | const { output } = require("@azure/functions"); 6 | 7 | const getFileListActivityName = "getFileList"; 8 | const copyFileToBlobActivityName = "copyFileToBlob"; 9 | 10 | df.app.orchestration("backupSiteContent", function* (context) { 11 | const rootDir = context.df.getInput(); 12 | if (!rootDir) { 13 | throw new Error("A directory path is required as an input."); 14 | } 15 | 16 | const rootDirAbs = path.resolve(rootDir); 17 | const files = yield context.df.callActivity(getFileListActivityName, rootDirAbs); 18 | 19 | // Backup Files and save Tasks into array 20 | const tasks = []; 21 | for (const file of files) { 22 | const input = { 23 | backupPath: path.relative(rootDirAbs, file).replace("\\", "/"), 24 | filePath: file, 25 | }; 26 | tasks.push(context.df.callActivity(copyFileToBlobActivityName, input)); 27 | } 28 | 29 | // wait for all the Backup Files Activities to complete, sum total bytes 30 | const results = yield context.df.Task.all(tasks); 31 | const totalBytes = results ? results.reduce((prev, curr) => prev + curr, 0) : 0; 32 | 33 | // return results; 34 | return totalBytes; 35 | }); 36 | 37 | df.app.activity(getFileListActivityName, { 38 | handler: async function (rootDirectory, context) { 39 | context.log(`Searching for files under '${rootDirectory}'...`); 40 | 41 | const allFilePaths = []; 42 | for await (const entry of readdirp(rootDirectory, { type: "files" })) { 43 | allFilePaths.push(entry.fullPath); 44 | } 45 | context.log(`Found ${allFilePaths.length} under ${rootDirectory}.`); 46 | return allFilePaths; 47 | }, 48 | }); 49 | 50 | const blobOutput = output.storageBlob({ 51 | path: "backups/{backupPath}", 52 | connection: "StorageConnString", 53 | }); 54 | 55 | df.app.activity(copyFileToBlobActivityName, { 56 | extraOutputs: [blobOutput], 57 | handler: async function ({ backupPath, filePath }, context) { 58 | const outputLocation = `backups/${backupPath}`; 59 | const stats = await fs.stat(filePath); 60 | context.log(`Copying '${filePath}' to '${outputLocation}'. Total bytes = ${stats.size}.`); 61 | 62 | const fileContents = await fs.readFile(filePath); 63 | 64 | context.extraOutputs.set(blobOutput, fileContents); 65 | 66 | return stats.size; 67 | }, 68 | }); 69 | -------------------------------------------------------------------------------- /samples-js/functions/callActivityWithRetry.js: -------------------------------------------------------------------------------- 1 | const df = require("durable-functions"); 2 | 3 | df.app.orchestration("callActivityWithRetry", function* (context) { 4 | const retryOptions = new df.RetryOptions(1000, 2); 5 | let returnValue; 6 | 7 | try { 8 | returnValue = yield context.df.callActivityWithRetry("flakyFunction", retryOptions); 9 | } catch (e) { 10 | context.log("Orchestrator caught exception. Flaky function is extremely flaky."); 11 | } 12 | 13 | return returnValue; 14 | }); 15 | 16 | df.app.activity("flakyFunction", { 17 | handler: function (_input, context) { 18 | context.log("Flaky Function Flaking!"); 19 | throw Error("FlakyFunction flaked"); 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /samples-js/functions/callSubOrchestratorWithRetry.js: -------------------------------------------------------------------------------- 1 | const df = require("durable-functions"); 2 | 3 | const subOrchestratorName = "throwsErrorInLine"; 4 | 5 | df.app.orchestration("callSubOrchestratorWithRetry", function* (context) { 6 | const retryOptions = new df.RetryOptions(10000, 2); 7 | const childId = `${context.df.instanceId}:0`; 8 | 9 | let returnValue; 10 | 11 | try { 12 | returnValue = yield context.df.callSubOrchestratorWithRetry( 13 | subOrchestratorName, 14 | retryOptions, 15 | "Matter", 16 | childId 17 | ); 18 | } catch (e) { 19 | context.log("Orchestrator caught exception. Sub-orchestrator failed."); 20 | } 21 | 22 | return returnValue; 23 | }); 24 | 25 | df.app.orchestration(subOrchestratorName, function* () { 26 | throw Error(`${subOrchestratorName} does what it says on the tin.`); 27 | }); 28 | -------------------------------------------------------------------------------- /samples-js/functions/cancelTimer.js: -------------------------------------------------------------------------------- 1 | const df = require("durable-functions"); 2 | const { DateTime } = require("luxon"); 3 | 4 | df.app.orchestration("cancelTimer", function* (context) { 5 | const expiration = DateTime.fromJSDate(context.df.currentUtcDateTime).plus({ minutes: 2 }); 6 | const timeoutTask = context.df.createTimer(expiration.toJSDate()); 7 | 8 | const hello = yield context.df.callActivity("sayHello", "from the other side"); 9 | 10 | if (!timeoutTask.isCompleted) { 11 | timeoutTask.cancel(); 12 | } 13 | 14 | return hello; 15 | }); 16 | -------------------------------------------------------------------------------- /samples-js/functions/continueAsNewCounter.js: -------------------------------------------------------------------------------- 1 | const df = require("durable-functions"); 2 | const { DateTime } = require("luxon"); 3 | 4 | df.app.orchestration("continueAsNewCounter", function* (context) { 5 | let currentValue = context.df.getInput() || 0; 6 | context.log(`Value is ${currentValue}`); 7 | currentValue++; 8 | 9 | const wait = DateTime.fromJSDate(context.df.currentUtcDateTime).plus({ seconds: 30 }); 10 | context.log("Counting up at" + wait.toString()); 11 | yield context.df.createTimer(wait.toJSDate()); 12 | 13 | if (currentValue < 10) { 14 | context.df.continueAsNew(currentValue); 15 | } 16 | 17 | return currentValue; 18 | }); 19 | -------------------------------------------------------------------------------- /samples-js/functions/counter.js: -------------------------------------------------------------------------------- 1 | const df = require("durable-functions"); 2 | 3 | const counterEntityName = "counterEntity"; 4 | 5 | df.app.entity(counterEntityName, async function (context) { 6 | await Promise.resolve(); 7 | let currentValue = context.df.getState(() => 0); 8 | 9 | switch (context.df.operationName) { 10 | case "add": 11 | const amount = context.df.getInput(); 12 | currentValue += amount; 13 | break; 14 | case "reset": 15 | currentValue = 0; 16 | break; 17 | case "get": 18 | context.df.return(currentValue); 19 | break; 20 | } 21 | 22 | context.df.setState(currentValue); 23 | }); 24 | 25 | df.app.orchestration("counterOrchestration", function* (context) { 26 | const entityId = new df.EntityId(counterEntityName, "myCounter"); 27 | 28 | currentValue = yield context.df.callEntity(entityId, "get"); 29 | if (currentValue < 10) { 30 | yield context.df.callEntity(entityId, "add", 1); 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /samples-js/functions/httpStart.js: -------------------------------------------------------------------------------- 1 | const df = require("durable-functions"); 2 | const { app } = require("@azure/functions"); 3 | 4 | app.http("httpStart", { 5 | route: "orchestrators/{orchestratorName}", 6 | extraInputs: [df.input.durableClient()], 7 | handler: async (request, context) => { 8 | const client = df.getClient(context); 9 | const body = await request.json(); 10 | const instanceId = await client.startNew(request.params.orchestratorName, { input: body }); 11 | 12 | context.log(`Started orchestration with ID = '${instanceId}'.`); 13 | 14 | return client.createCheckStatusResponse(request, instanceId); 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /samples-js/functions/httpSyncStart.js: -------------------------------------------------------------------------------- 1 | const { app } = require("@azure/functions"); 2 | const df = require("durable-functions"); 3 | 4 | const timeout = "timeout"; 5 | const retryInterval = "retryInterval"; 6 | 7 | app.http("httpSyncStart", { 8 | methods: ["POST"], 9 | route: "orchestrators/wait/{orchestratorName}", 10 | authLevel: "anonymous", 11 | extraInputs: [df.input.durableClient()], 12 | handler: async function (request, context) { 13 | const client = df.getClient(context); 14 | const body = await request.json(); 15 | const instanceId = await client.startNew(request.params.orchestratorName, { input: body }); 16 | 17 | context.log(`Started orchestration with ID = '${instanceId}'.`); 18 | 19 | const timeoutInMilliseconds = getTimeInMilliseconds(request, timeout) || 30000; 20 | const retryIntervalInMilliseconds = getTimeInMilliseconds(request, retryInterval) || 1000; 21 | 22 | const response = await client.waitForCompletionOrCreateCheckStatusResponse( 23 | request, 24 | instanceId, 25 | { 26 | timeoutInMilliseconds, 27 | retryIntervalInMilliseconds, 28 | } 29 | ); 30 | return response; 31 | }, 32 | }); 33 | 34 | function getTimeInMilliseconds(req, queryParameterName) { 35 | // parameters are passed in as seconds 36 | const queryValue = req.query.get(queryParameterName); 37 | // return as milliseconds 38 | return queryValue ? queryValue * 1000 : undefined; 39 | } 40 | -------------------------------------------------------------------------------- /samples-js/functions/listAzureSubscriptions.js: -------------------------------------------------------------------------------- 1 | const df = require("durable-functions"); 2 | 3 | df.app.orchestration("listAzureSubscriptions", function* (context) { 4 | // More information on the use of managed identities in the callHttp API: 5 | // https://docs.microsoft.com/azure/azure-functions/durable/durable-functions-http-features#managed-identities 6 | const res = yield context.df.callHttp({ 7 | method: "GET", 8 | url: "https://management.azure.com/subscriptions?api-version=2019-06-01", 9 | tokenSource: new df.ManagedIdentityTokenSource("https://management.core.windows.net"), 10 | }); 11 | return res; 12 | }); 13 | -------------------------------------------------------------------------------- /samples-js/functions/sayHello.js: -------------------------------------------------------------------------------- 1 | const df = require("durable-functions"); 2 | 3 | const helloActivityName = "sayHello"; 4 | 5 | df.app.orchestration("helloSequence", function* (context) { 6 | context.log("Starting chain sample"); 7 | 8 | const output = []; 9 | output.push(yield context.df.callActivity(helloActivityName, "Tokyo")); 10 | output.push(yield context.df.callActivity(helloActivityName, "Seattle")); 11 | output.push(yield context.df.callActivity(helloActivityName, "Cairo")); 12 | 13 | return output; 14 | }); 15 | 16 | df.app.orchestration("sayHelloWithActivity", function* (context) { 17 | const input = context.df.getInput(); 18 | 19 | const output = yield context.df.callActivity(helloActivityName, input); 20 | return output; 21 | }); 22 | 23 | df.app.orchestration("sayHelloWithCustomStatus", function* (context) { 24 | const input = context.df.getInput(); 25 | const output = yield context.df.callActivity(helloActivityName, input); 26 | context.df.setCustomStatus(output); 27 | return output; 28 | }); 29 | 30 | df.app.orchestration("sayHelloWithSubOrchestrator", function* (context) { 31 | const input = context.df.getInput(); 32 | 33 | const output = yield context.df.callSubOrchestrator("sayHelloWithActivity", input); 34 | return output; 35 | }); 36 | 37 | df.app.activity(helloActivityName, { 38 | handler: function (input) { 39 | return `Hello ${input}`; 40 | }, 41 | }); 42 | -------------------------------------------------------------------------------- /samples-js/functions/smsPhoneVerification.js: -------------------------------------------------------------------------------- 1 | const { output } = require("@azure/functions"); 2 | const df = require("durable-functions"); 3 | const { DateTime } = require("luxon"); 4 | 5 | const sendSmsChallengeActivityName = "sendSmsChallenge"; 6 | 7 | df.app.orchestration("smsPhoneVerification", function* (context) { 8 | const phoneNumber = context.df.getInput(); 9 | if (!phoneNumber) { 10 | throw new Error("A phone number input is required."); 11 | } 12 | 13 | const challengeCode = yield context.df.callActivity(sendSmsChallengeActivityName, phoneNumber); 14 | 15 | // The user has 90 seconds to respond with the code they received in the SMS message. 16 | const expiration = DateTime.fromJSDate(context.df.currentUtcDateTime).plus({ seconds: 90 }); 17 | const timeoutTask = context.df.createTimer(expiration.toJSDate()); 18 | 19 | let authorized = false; 20 | for (let i = 0; i <= 3; i++) { 21 | const challengeResponseTask = context.df.waitForExternalEvent("SmsChallengeResponse"); 22 | 23 | const winner = yield context.df.Task.any([challengeResponseTask, timeoutTask]); 24 | 25 | if (winner === timeoutTask) { 26 | // Timeout expired 27 | break; 28 | } 29 | 30 | // We got back a response! Compare it to the challenge code. 31 | if (challengeResponseTask.result === challengeCode) { 32 | authorized = true; 33 | break; 34 | } 35 | } 36 | 37 | if (!timeoutTask.isCompleted) { 38 | // All pending timers must be complete or canceled before the function exits. 39 | timeoutTask.cancel(); 40 | } 41 | 42 | return authorized; 43 | }); 44 | 45 | const twilioOutput = output.generic({ 46 | type: "twilioSms", 47 | from: "%TwilioPhoneNumber%", 48 | accountSidSetting: "TwilioAccountSid", 49 | authTokenSetting: "TwilioAuthToken", 50 | }); 51 | 52 | df.app.activity(sendSmsChallengeActivityName, { 53 | extraOutputs: [twilioOutput], 54 | handler: function (phoneNumber, context) { 55 | // Get a random challenge code 56 | const challengeCode = Math.floor(Math.random() * 10000); 57 | 58 | context.log(`Sending verification code ${challengeCode} to ${phoneNumber}.`); 59 | 60 | context.extraOutputs.set(twilioOutput, { 61 | body: `Your verification code is ${challengeCode.toPrecision(4)}`, 62 | to: phoneNumber, 63 | }); 64 | 65 | return challengeCode; 66 | }, 67 | }); 68 | -------------------------------------------------------------------------------- /samples-js/functions/weatherMonitor.js: -------------------------------------------------------------------------------- 1 | const { output } = require("@azure/functions"); 2 | const df = require("durable-functions"); 3 | const { DateTime } = require("luxon"); 4 | const axios = require("axios").default; 5 | 6 | const clearWeatherConditions = ["clear sky", "few clouds", "scattered clouds", "broken clouds"]; 7 | 8 | const getIsClearActivity = "getIsClear"; 9 | const sendGoodWeatherAlertActivity = "sendGoodWeatherAlert"; 10 | 11 | df.app.orchestration("weatherMonitor", function* (context) { 12 | // get input 13 | const input = context.df.getInput(); 14 | context.log(`Received monitor request. Location: ${input.location}. Phone: ${input.phone}`); 15 | verifyRequest(input); 16 | 17 | // set expiry time to 6 hours from now 18 | const endTime = DateTime.fromJSDate(context.df.currentUtcDateTime).plus({ hours: 6 }); 19 | const locationString = `${input.location.city}${ 20 | input.location.state ? `, ${input.location.state}` : "" 21 | }, ${input.location.country}`; 22 | context.log(`Instantiating monitor for ${locationString}. Expires: ${endTime.toString()}.`); 23 | 24 | // until the expiry time 25 | while (DateTime.fromJSDate(context.df.currentUtcDateTime) < endTime) { 26 | // Check the weather 27 | context.log( 28 | `Checking current weather conditions for ${locationString} at ${context.df.currentUtcDateTime}.` 29 | ); 30 | const isClear = yield context.df.callActivity(getIsClearActivity, input.location); 31 | 32 | if (isClear) { 33 | // It's not raining! Or snowing. Or misting. Tell our user to take advantage of it. 34 | context.log(`Detected clear weather for ${locationString}. Notifying ${input.phone}.`); 35 | yield context.df.callActivity(sendGoodWeatherAlertActivity, input.phone); 36 | break; 37 | } else { 38 | // Wait for the next checkpoint 39 | const nextCheckpoint = DateTime.fromJSDate(context.df.currentUtcDateTime).plus({ 40 | seconds: 30, 41 | }); 42 | 43 | context.log(`Next check for ${locationString} at ${nextCheckpoint.toString()}`); 44 | 45 | yield context.df.createTimer(nextCheckpoint.toJSDate()); 46 | } 47 | } 48 | 49 | context.log("Monitor expiring."); 50 | }); 51 | 52 | function verifyRequest(request) { 53 | if (!request) { 54 | throw new Error("An input object is required."); 55 | } 56 | if (!request.location) { 57 | throw new Error("A location input is required."); 58 | } 59 | if (!request.location.city) { 60 | throw new Error("A city is required on the location input."); 61 | } 62 | if (!request.location.country) { 63 | throw new Error("A country code is required on location input."); 64 | } 65 | if ( 66 | (request.location.country === "USA" || request.location.country === "US") && 67 | !request.location.state 68 | ) { 69 | throw new Error("A state code is required on location input for US locations."); 70 | } 71 | if (!request.phone) { 72 | throw new Error("A phone number input is required."); 73 | } 74 | } 75 | 76 | df.app.activity(getIsClearActivity, { 77 | handler: async function (location, context) { 78 | try { 79 | // get current conditions from OpenWeatherMap API 80 | const currentConditions = await getCurrentConditions(location); 81 | // compare against known clear conditions 82 | return clearWeatherConditions.includes(currentConditions.description); 83 | } catch (err) { 84 | context.log(`${getIsClearActivity} encountered an error: ${err}`); 85 | throw err; 86 | } 87 | }, 88 | }); 89 | 90 | async function getCurrentConditions(location) { 91 | try { 92 | // get current conditions from OpenWeatherMap API 93 | const url = location.state 94 | ? `https://api.openweathermap.org/data/2.5/weather?q=${location.city},${location.state},${location.country}&appid=${process.env.OpenWeatherMapApiKey}` 95 | : `https://api.openweathermap.org/data/2.5/weather?q=${location.city},${location.country}&appid=${process.env.OpenWeatherMapApiKey}`; 96 | 97 | const response = await axios.get(url); 98 | return response.data.weather[0]; 99 | } catch (err) { 100 | throw err; 101 | } 102 | } 103 | 104 | // configure twilio output 105 | const twilioOutput = output.generic({ 106 | type: "twilioSms", 107 | from: "%TwilioPhoneNumber%", 108 | accountSidSetting: "TwilioAccountSid", 109 | authTokenSetting: "TwilioAuthToken", 110 | }); 111 | 112 | df.app.activity(sendGoodWeatherAlertActivity, { 113 | extraOutputs: [twilioOutput], // register twilio output 114 | handler: function (phoneNumber, context) { 115 | // send message to phone number 116 | context.extraOutputs.set(twilioOutput, { 117 | body: "The weather's clear outside! Go talk a walk!", 118 | to: phoneNumber, 119 | }); 120 | }, 121 | }); 122 | -------------------------------------------------------------------------------- /samples-js/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[3.15.0, 4.0.0)" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "samples-js", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "functions/*.js", 6 | "scripts": { 7 | "start": "func start", 8 | "test": "echo \"No tests yet...\"" 9 | }, 10 | "dependencies": { 11 | "@azure/functions": "^4.7.0", 12 | "axios": "^1.6.0", 13 | "durable-functions": "^3.0.0", 14 | "luxon": "^3.2.1", 15 | "readdirp": "^3.6.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /samples-ts/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | local.settings.json 6 | test 7 | tsconfig.json -------------------------------------------------------------------------------- /samples-ts/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | .env.test 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # DynamoDB Local files 84 | .dynamodb/ 85 | 86 | # TypeScript output 87 | dist 88 | out 89 | 90 | # Azure Functions artifacts 91 | bin 92 | obj 93 | appsettings.json 94 | local.settings.json 95 | 96 | # Azurite artifacts 97 | __blobstorage__ 98 | __queuestorage__ 99 | __azurite_db*__.json -------------------------------------------------------------------------------- /samples-ts/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions" 4 | ] 5 | } -------------------------------------------------------------------------------- /samples-ts/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to Node Functions", 6 | "type": "node", 7 | "request": "attach", 8 | "port": 9229, 9 | "preLaunchTask": "func: host start" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /samples-ts/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": ".", 3 | "azureFunctions.postDeployTask": "npm install (functions)", 4 | "azureFunctions.projectLanguage": "TypeScript", 5 | "azureFunctions.projectRuntime": "~4", 6 | "debug.internalConsoleOptions": "neverOpen", 7 | "azureFunctions.preDeployTask": "npm prune (functions)" 8 | } 9 | -------------------------------------------------------------------------------- /samples-ts/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "func", 6 | "label": "func: host start", 7 | "command": "host start", 8 | "problemMatcher": "$func-node-watch", 9 | "isBackground": true, 10 | "dependsOn": "npm build (functions)" 11 | }, 12 | { 13 | "type": "shell", 14 | "label": "npm build (functions)", 15 | "command": "npm run build", 16 | "dependsOn": "npm install (functions)", 17 | "problemMatcher": "$tsc" 18 | }, 19 | { 20 | "type": "shell", 21 | "label": "npm install (functions)", 22 | "command": "npm install" 23 | }, 24 | { 25 | "type": "shell", 26 | "label": "npm prune (functions)", 27 | "command": "npm prune --production", 28 | "dependsOn": "npm build (functions)", 29 | "problemMatcher": [] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /samples-ts/functions/backupSiteContent.ts: -------------------------------------------------------------------------------- 1 | import * as df from "durable-functions"; 2 | import * as fs from "fs/promises"; 3 | import * as readdirp from "readdirp"; 4 | import * as path from "path"; 5 | import { InvocationContext, output } from "@azure/functions"; 6 | import { 7 | OrchestrationHandler, 8 | OrchestrationContext, 9 | ActivityHandler, 10 | Task, 11 | } from "durable-functions"; 12 | import { Stats } from "fs"; 13 | 14 | const getFileListActivityName = "getFileList"; 15 | const copyFileToBlobActivityName = "copyFileToBlob"; 16 | 17 | const backupSiteContentOrchestration: OrchestrationHandler = function* ( 18 | context: OrchestrationContext 19 | ) { 20 | const rootDir: string = context.df.getInput(); 21 | if (!rootDir) { 22 | throw new Error("A directory path is required as an input."); 23 | } 24 | 25 | const rootDirAbs: string = path.resolve(rootDir); 26 | const files: string[] = yield context.df.callActivity(getFileListActivityName, rootDirAbs); 27 | 28 | // Backup Files and save Tasks into array 29 | const tasks: Task[] = []; 30 | for (const file of files) { 31 | const input = { 32 | backupPath: path.relative(rootDirAbs, file).replace("\\", "/"), 33 | filePath: file, 34 | }; 35 | tasks.push(context.df.callActivity(copyFileToBlobActivityName, input)); 36 | } 37 | 38 | // wait for all the Backup Files Activities to complete, sum total bytes 39 | const results: number[] = yield context.df.Task.all(tasks); 40 | const totalBytes: number = results ? results.reduce((prev, curr) => prev + curr, 0) : 0; 41 | 42 | // return results; 43 | return totalBytes; 44 | }; 45 | df.app.orchestration("backupSiteContent", backupSiteContentOrchestration); 46 | 47 | const getFileListActivity: ActivityHandler = async function ( 48 | rootDirectory: string, 49 | context: InvocationContext 50 | ): Promise { 51 | context.log(`Searching for files under '${rootDirectory}'...`); 52 | 53 | const allFilePaths = []; 54 | for await (const entry of readdirp(rootDirectory, { type: "files" })) { 55 | allFilePaths.push(entry.fullPath); 56 | } 57 | context.log(`Found ${allFilePaths.length} under ${rootDirectory}.`); 58 | return allFilePaths; 59 | }; 60 | 61 | df.app.activity(getFileListActivityName, { 62 | handler: getFileListActivity, 63 | }); 64 | 65 | const blobOutput = output.storageBlob({ 66 | path: "backups/{backupPath}", 67 | connection: "StorageConnString", 68 | }); 69 | 70 | const copyFileToBlobActivity: ActivityHandler = async function ( 71 | { backupPath, filePath }: { backupPath: string; filePath: string }, 72 | context: InvocationContext 73 | ): Promise { 74 | const outputLocation = `backups/${backupPath}`; 75 | const stats: Stats = await fs.stat(filePath); 76 | context.log(`Copying '${filePath}' to '${outputLocation}'. Total bytes = ${stats.size}.`); 77 | 78 | const fileContents = await fs.readFile(filePath); 79 | 80 | context.extraOutputs.set(blobOutput, fileContents); 81 | 82 | return stats.size; 83 | }; 84 | df.app.activity(copyFileToBlobActivityName, { 85 | extraOutputs: [blobOutput], 86 | handler: copyFileToBlobActivity, 87 | }); 88 | -------------------------------------------------------------------------------- /samples-ts/functions/callActivityWithRetry.ts: -------------------------------------------------------------------------------- 1 | import { InvocationContext } from "@azure/functions"; 2 | import * as df from "durable-functions"; 3 | import { 4 | ActivityHandler, 5 | OrchestrationContext, 6 | OrchestrationHandler, 7 | RetryOptions, 8 | } from "durable-functions"; 9 | 10 | const callActivityWithRetry: OrchestrationHandler = function* (context: OrchestrationContext) { 11 | const retryOptions: RetryOptions = new df.RetryOptions(1000, 2); 12 | 13 | let returnValue: any; 14 | try { 15 | returnValue = yield context.df.callActivityWithRetry("flakyFunction", retryOptions); 16 | } catch (e) { 17 | context.log("Orchestrator caught exception. Flaky function is extremely flaky."); 18 | } 19 | 20 | return returnValue; 21 | }; 22 | df.app.orchestration("callActivityWithRetry", callActivityWithRetry); 23 | 24 | const flakyFunction: ActivityHandler = function (_input: any, context: InvocationContext): void { 25 | context.log("Flaky Function Flaking!"); 26 | throw new Error("FlakyFunction flaked"); 27 | }; 28 | df.app.activity("flakyFunction", { 29 | handler: flakyFunction, 30 | }); 31 | -------------------------------------------------------------------------------- /samples-ts/functions/callSubOrchestratorWithRetry.ts: -------------------------------------------------------------------------------- 1 | import * as df from "durable-functions"; 2 | import { OrchestrationContext, OrchestrationHandler } from "durable-functions"; 3 | 4 | const subOrchestratorName = "throwsErrorInLine"; 5 | 6 | const callSubOrchestratorWithRetry: OrchestrationHandler = function* ( 7 | context: OrchestrationContext 8 | ) { 9 | const retryOptions: df.RetryOptions = new df.RetryOptions(10000, 2); 10 | const childId = `${context.df.instanceId}:0`; 11 | 12 | let returnValue: any; 13 | 14 | try { 15 | returnValue = yield context.df.callSubOrchestratorWithRetry( 16 | subOrchestratorName, 17 | retryOptions, 18 | { 19 | input: "Input", 20 | instanceId: childId, 21 | } 22 | ); 23 | } catch (e) { 24 | context.log("Orchestrator caught exception. Sub-orchestrator failed."); 25 | } 26 | 27 | return returnValue; 28 | }; 29 | df.app.orchestration("callSubOrchestratorWithRetry", callSubOrchestratorWithRetry); 30 | 31 | const flakyOrchestrator: OrchestrationHandler = function* () { 32 | throw new Error(`${subOrchestratorName} does what it says on the tin.`); 33 | }; 34 | df.app.orchestration(subOrchestratorName, flakyOrchestrator); 35 | -------------------------------------------------------------------------------- /samples-ts/functions/cancelTimer.ts: -------------------------------------------------------------------------------- 1 | import * as df from "durable-functions"; 2 | import { OrchestrationContext, OrchestrationHandler, TimerTask } from "durable-functions"; 3 | import { DateTime } from "luxon"; 4 | 5 | const cancelTimer: OrchestrationHandler = function* (context: OrchestrationContext) { 6 | const expiration: DateTime = DateTime.fromJSDate(context.df.currentUtcDateTime).plus({ 7 | minutes: 2, 8 | }); 9 | const timeoutTask: TimerTask = context.df.createTimer(expiration.toJSDate()); 10 | 11 | const hello: string = yield context.df.callActivity("sayHello", "from the other side"); 12 | 13 | if (!timeoutTask.isCompleted) { 14 | timeoutTask.cancel(); 15 | } 16 | 17 | return hello; 18 | }; 19 | df.app.orchestration("cancelTimer", cancelTimer); 20 | -------------------------------------------------------------------------------- /samples-ts/functions/continueAsNewCounter.ts: -------------------------------------------------------------------------------- 1 | import * as df from "durable-functions"; 2 | import { OrchestrationContext, OrchestrationHandler } from "durable-functions"; 3 | import { DateTime } from "luxon"; 4 | 5 | const continueAsNewCounter: OrchestrationHandler = function* (context: OrchestrationContext) { 6 | let currentValue: number = context.df.getInput() || 0; 7 | context.log(`Value is ${currentValue}`); 8 | currentValue++; 9 | 10 | const wait: DateTime = DateTime.fromJSDate(context.df.currentUtcDateTime).plus({ seconds: 30 }); 11 | context.log("Counting up at" + wait.toString()); 12 | yield context.df.createTimer(wait.toJSDate()); 13 | 14 | if (currentValue < 10) { 15 | context.df.continueAsNew(currentValue); 16 | } 17 | 18 | return currentValue; 19 | }; 20 | 21 | df.app.orchestration("continueAsNewCounter", continueAsNewCounter); 22 | -------------------------------------------------------------------------------- /samples-ts/functions/counter.ts: -------------------------------------------------------------------------------- 1 | import * as df from "durable-functions"; 2 | import { 3 | EntityHandler, 4 | EntityContext, 5 | OrchestrationContext, 6 | OrchestrationHandler, 7 | EntityId, 8 | } from "durable-functions"; 9 | 10 | const counterEntityName = "counterEntity"; 11 | 12 | const counterEntity: EntityHandler = async function ( 13 | context: EntityContext 14 | ): Promise { 15 | await Promise.resolve(); 16 | let currentValue: number = context.df.getState(() => 0); 17 | 18 | switch (context.df.operationName) { 19 | case "add": 20 | const amount: number = context.df.getInput(); 21 | currentValue += amount; 22 | break; 23 | case "reset": 24 | currentValue = 0; 25 | break; 26 | case "get": 27 | context.df.return(currentValue); 28 | break; 29 | } 30 | 31 | context.df.setState(currentValue); 32 | }; 33 | df.app.entity(counterEntityName, counterEntity); 34 | 35 | const counterOrchestration: OrchestrationHandler = function* (context: OrchestrationContext) { 36 | const entityId: EntityId = new df.EntityId(counterEntityName, "myCounter"); 37 | 38 | const currentValue: number = yield context.df.callEntity(entityId, "get"); 39 | if (currentValue < 10) { 40 | yield context.df.callEntity(entityId, "add", 1); 41 | } 42 | }; 43 | df.app.orchestration("counterOrchestration", counterOrchestration); 44 | -------------------------------------------------------------------------------- /samples-ts/functions/httpStart.ts: -------------------------------------------------------------------------------- 1 | import * as df from "durable-functions"; 2 | import { app, HttpHandler, HttpRequest, HttpResponse, InvocationContext } from "@azure/functions"; 3 | 4 | const httpStart: HttpHandler = async ( 5 | request: HttpRequest, 6 | context: InvocationContext 7 | ): Promise => { 8 | const client = df.getClient(context); 9 | const body: unknown = await request.json(); 10 | const instanceId: string = await client.startNew(request.params.orchestratorName, { 11 | input: body, 12 | }); 13 | 14 | context.log(`Started orchestration with ID = '${instanceId}'.`); 15 | 16 | return client.createCheckStatusResponse(request, instanceId); 17 | }; 18 | app.http("httpStart", { 19 | route: "orchestrators/{orchestratorName}", 20 | extraInputs: [df.input.durableClient()], 21 | handler: httpStart, 22 | }); 23 | -------------------------------------------------------------------------------- /samples-ts/functions/httpSyncStart.ts: -------------------------------------------------------------------------------- 1 | import { 2 | app, 3 | HttpHandler, 4 | HttpRequest, 5 | HttpResponse, 6 | HttpResponseInit, 7 | InvocationContext, 8 | } from "@azure/functions"; 9 | import * as df from "durable-functions"; 10 | 11 | const timeout = "timeout"; 12 | const retryInterval = "retryInterval"; 13 | 14 | const httpSyncStart: HttpHandler = async function ( 15 | request: HttpRequest, 16 | context: InvocationContext 17 | ): Promise { 18 | const client = df.getClient(context); 19 | const body: unknown = await request.json(); 20 | const instanceId: string = await client.startNew(request.params.orchestratorName, { 21 | input: body, 22 | }); 23 | 24 | context.log(`Started orchestration with ID = '${instanceId}'.`); 25 | 26 | const timeoutInMilliseconds: number = getTimeInMilliseconds(request, timeout) || 30000; 27 | const retryIntervalInMilliseconds: number = 28 | getTimeInMilliseconds(request, retryInterval) || 1000; 29 | 30 | const response = await client.waitForCompletionOrCreateCheckStatusResponse( 31 | request, 32 | instanceId, 33 | { 34 | timeoutInMilliseconds, 35 | retryIntervalInMilliseconds, 36 | } 37 | ); 38 | return response; 39 | }; 40 | 41 | app.http("httpSyncStart", { 42 | methods: ["POST"], 43 | route: "orchestrators/wait/{orchestratorName}", 44 | authLevel: "anonymous", 45 | extraInputs: [df.input.durableClient()], 46 | handler: httpSyncStart, 47 | }); 48 | 49 | function getTimeInMilliseconds(req: HttpRequest, queryParameterName: string): number { 50 | // parameters are passed in as seconds 51 | const queryValue: number = parseInt(req.query.get(queryParameterName)); 52 | // return as milliseconds 53 | return queryValue ? queryValue * 1000 : undefined; 54 | } 55 | -------------------------------------------------------------------------------- /samples-ts/functions/listAzureSubscriptions.ts: -------------------------------------------------------------------------------- 1 | import * as df from "durable-functions"; 2 | import { OrchestrationContext, OrchestrationHandler } from "durable-functions"; 3 | 4 | const listAzureSubscriptions: OrchestrationHandler = function* (context: OrchestrationContext) { 5 | // More information on the use of managed identities in the callHttp API: 6 | // https://docs.microsoft.com/azure/azure-functions/durable/durable-functions-http-features#managed-identities 7 | const res = yield context.df.callHttp({ 8 | method: "GET", 9 | url: "https://management.azure.com/subscriptions?api-version=2019-06-01", 10 | tokenSource: new df.ManagedIdentityTokenSource("https://management.core.windows.net"), 11 | }); 12 | return res; 13 | }; 14 | df.app.orchestration("listAzureSubscriptions", listAzureSubscriptions); 15 | -------------------------------------------------------------------------------- /samples-ts/functions/sayHello.ts: -------------------------------------------------------------------------------- 1 | import * as df from "durable-functions"; 2 | import { ActivityHandler, OrchestrationContext, OrchestrationHandler } from "durable-functions"; 3 | 4 | const helloActivityName = "sayHello"; 5 | 6 | const helloSequence: OrchestrationHandler = function* (context: OrchestrationContext) { 7 | context.log("Starting chain sample"); 8 | 9 | const output: string[] = []; 10 | output.push(yield context.df.callActivity(helloActivityName, "Tokyo")); 11 | output.push(yield context.df.callActivity(helloActivityName, "Seattle")); 12 | output.push(yield context.df.callActivity(helloActivityName, "Cairo")); 13 | 14 | return output; 15 | }; 16 | df.app.orchestration("helloSequence", helloSequence); 17 | 18 | const sayHelloWithActivity: OrchestrationHandler = function* (context: OrchestrationContext) { 19 | const input: unknown = context.df.getInput(); 20 | 21 | const output: string = yield context.df.callActivity(helloActivityName, input); 22 | return output; 23 | }; 24 | df.app.orchestration("sayHelloWithActivity", sayHelloWithActivity); 25 | 26 | const sayHelloWithCustomStatus: OrchestrationHandler = function* (context: OrchestrationContext) { 27 | const input: unknown = context.df.getInput(); 28 | const output: string = yield context.df.callActivity(helloActivityName, input); 29 | context.df.setCustomStatus(output); 30 | return output; 31 | }; 32 | df.app.orchestration("sayHelloWithCustomStatus", sayHelloWithCustomStatus); 33 | 34 | const sayHelloWithSubOrchestrator: OrchestrationHandler = function* ( 35 | context: OrchestrationContext 36 | ) { 37 | const input: unknown = context.df.getInput(); 38 | 39 | const output: string = yield context.df.callSubOrchestrator("sayHelloWithActivity", { input }); 40 | return output; 41 | }; 42 | df.app.orchestration("sayHelloWithSubOrchestrator", sayHelloWithSubOrchestrator); 43 | 44 | const helloActivity: ActivityHandler = function (input: unknown): string { 45 | return `Hello ${input}`; 46 | }; 47 | df.app.activity(helloActivityName, { 48 | handler: helloActivity, 49 | }); 50 | -------------------------------------------------------------------------------- /samples-ts/functions/smsPhoneVerification.ts: -------------------------------------------------------------------------------- 1 | import { InvocationContext, output } from "@azure/functions"; 2 | import * as df from "durable-functions"; 3 | import { 4 | ActivityHandler, 5 | OrchestrationContext, 6 | OrchestrationHandler, 7 | Task, 8 | TimerTask, 9 | } from "durable-functions"; 10 | import { DateTime } from "luxon"; 11 | 12 | const sendSmsChallengeActivityName = "sendSmsChallenge"; 13 | 14 | const smsPhoneVerification: OrchestrationHandler = function* (context: OrchestrationContext) { 15 | const phoneNumber: string = context.df.getInput(); 16 | if (!phoneNumber) { 17 | throw new Error("A phone number input is required."); 18 | } 19 | 20 | const challengeCode: number = yield context.df.callActivity( 21 | sendSmsChallengeActivityName, 22 | phoneNumber 23 | ); 24 | 25 | // The user has 90 seconds to respond with the code they received in the SMS message. 26 | const expiration: DateTime = DateTime.fromJSDate(context.df.currentUtcDateTime).plus({ 27 | seconds: 90, 28 | }); 29 | const timeoutTask: TimerTask = context.df.createTimer(expiration.toJSDate()); 30 | 31 | let authorized = false; 32 | for (let i = 0; i <= 3; i++) { 33 | const challengeResponseTask: Task = context.df.waitForExternalEvent("SmsChallengeResponse"); 34 | 35 | const winner: Task = yield context.df.Task.any([challengeResponseTask, timeoutTask]); 36 | 37 | if (winner === timeoutTask) { 38 | // Timeout expired 39 | break; 40 | } 41 | 42 | // We got back a response! Compare it to the challenge code. 43 | if (challengeResponseTask.result === challengeCode) { 44 | authorized = true; 45 | break; 46 | } 47 | } 48 | 49 | if (!timeoutTask.isCompleted) { 50 | // All pending timers must be complete or canceled before the function exits. 51 | timeoutTask.cancel(); 52 | } 53 | 54 | return authorized; 55 | }; 56 | df.app.orchestration("smsPhoneVerification", smsPhoneVerification); 57 | 58 | const twilioOutput = output.generic({ 59 | type: "twilioSms", 60 | from: "%TwilioPhoneNumber%", 61 | accountSidSetting: "TwilioAccountSid", 62 | authTokenSetting: "TwilioAuthToken", 63 | }); 64 | 65 | const sendSmsChallenge: ActivityHandler = function ( 66 | phoneNumber: string, 67 | context: InvocationContext 68 | ): number { 69 | // Get a random challenge code 70 | const challengeCode: number = Math.floor(Math.random() * 10000); 71 | 72 | context.log(`Sending verification code ${challengeCode} to ${phoneNumber}.`); 73 | 74 | context.extraOutputs.set(twilioOutput, { 75 | body: `Your verification code is ${challengeCode.toPrecision(4)}`, 76 | to: phoneNumber, 77 | }); 78 | 79 | return challengeCode; 80 | }; 81 | 82 | df.app.activity(sendSmsChallengeActivityName, { 83 | extraOutputs: [twilioOutput], 84 | handler: sendSmsChallenge, 85 | }); 86 | -------------------------------------------------------------------------------- /samples-ts/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[3.15.0, 4.0.0)" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "samples-ts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/functions/*.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "watch": "tsc -w", 9 | "prestart": "npm run build", 10 | "start": "func start", 11 | "test": "echo \"No tests yet...\"" 12 | }, 13 | "devDependencies": { 14 | "@types/luxon": "^3.2.0", 15 | "@types/node": "16.x", 16 | "typescript": "^4.0.0" 17 | }, 18 | "dependencies": { 19 | "@azure/functions": "^4.0.0", 20 | "axios": "^1.8.4", 21 | "durable-functions": "^3.0.0", 22 | "luxon": "^3.2.1", 23 | "readdirp": "^3.6.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "dist", 6 | "rootDir": ".", 7 | "sourceMap": true, 8 | "strict": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Constants.ts: -------------------------------------------------------------------------------- 1 | /** @hidden */ 2 | export class Constants { 3 | public static readonly DefaultLocalHost: string = "localhost:7071"; 4 | public static readonly DefaultLocalOrigin: string = `http://${Constants.DefaultLocalHost}`; 5 | } 6 | -------------------------------------------------------------------------------- /src/ManagedIdentityTokenSource.ts: -------------------------------------------------------------------------------- 1 | import * as types from "durable-functions"; 2 | 3 | export class ManagedIdentityTokenSource implements types.ManagedIdentityTokenSource { 4 | /** @hidden */ 5 | public readonly kind: string = "AzureManagedIdentity"; 6 | 7 | constructor(public readonly resource: string) {} 8 | } 9 | -------------------------------------------------------------------------------- /src/RetryOptions.ts: -------------------------------------------------------------------------------- 1 | import { Utils } from "./util/Utils"; 2 | import * as types from "durable-functions"; 3 | 4 | export class RetryOptions implements types.RetryOptions { 5 | public backoffCoefficient: number; 6 | public maxRetryIntervalInMilliseconds: number; 7 | public retryTimeoutInMilliseconds: number; 8 | 9 | constructor( 10 | public readonly firstRetryIntervalInMilliseconds: number, 11 | public readonly maxNumberOfAttempts: number 12 | ) { 13 | Utils.throwIfNotNumber( 14 | firstRetryIntervalInMilliseconds, 15 | "firstRetryIntervalInMilliseconds" 16 | ); 17 | Utils.throwIfNotNumber(maxNumberOfAttempts, "maxNumberOfAttempts"); 18 | 19 | if (firstRetryIntervalInMilliseconds <= 0) { 20 | throw new RangeError("firstRetryIntervalInMilliseconds value must be greater than 0."); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/actions/ActionType.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @hidden 3 | * 4 | * The type of asynchronous behavior the Durable Functions extension should 5 | * perform on behalf of the language shim. For internal use only as part of the 6 | * [out-of-proc execution schema.](https://github.com/Azure/azure-functions-durable-extension/wiki/Out-of-Proc-Orchestrator-Execution-Schema-Reference) 7 | * 8 | * Corresponds to internal type AsyncActionType in [Durable Functions extension.](https://github.com/Azure/azure-functions-durable-extension) 9 | */ 10 | export enum ActionType { 11 | CallActivity = 0, 12 | CallActivityWithRetry = 1, 13 | CallSubOrchestrator = 2, 14 | CallSubOrchestratorWithRetry = 3, 15 | ContinueAsNew = 4, 16 | CreateTimer = 5, 17 | WaitForExternalEvent = 6, 18 | CallEntity = 7, 19 | CallHttp = 8, 20 | SignalEntity = 9, 21 | // ActionType 10 corresponds to ScheduledSignalEntity, which is not supported yet 22 | WhenAny = 11, 23 | WhenAll = 12, 24 | } 25 | -------------------------------------------------------------------------------- /src/actions/CallActivityAction.ts: -------------------------------------------------------------------------------- 1 | import { Utils } from "../util/Utils"; 2 | import { ActionType } from "./ActionType"; 3 | import { IAction } from "./IAction"; 4 | 5 | /** @hidden */ 6 | export class CallActivityAction implements IAction { 7 | public readonly actionType: ActionType = ActionType.CallActivity; 8 | public readonly input: unknown; 9 | 10 | constructor(public readonly functionName: string, input?: unknown) { 11 | this.input = Utils.processInput(input); 12 | Utils.throwIfEmpty(functionName, "functionName"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/actions/CallActivityWithRetryAction.ts: -------------------------------------------------------------------------------- 1 | import { Utils } from "../util/Utils"; 2 | import { RetryOptions } from "../RetryOptions"; 3 | import { ActionType } from "./ActionType"; 4 | import { IAction } from "./IAction"; 5 | 6 | /** @hidden */ 7 | export class CallActivityWithRetryAction implements IAction { 8 | public readonly actionType: ActionType = ActionType.CallActivityWithRetry; 9 | public readonly input: unknown; 10 | 11 | constructor( 12 | public readonly functionName: string, 13 | public readonly retryOptions: RetryOptions, 14 | input?: unknown 15 | ) { 16 | this.input = Utils.processInput(input); 17 | Utils.throwIfEmpty(functionName, "functionName"); 18 | 19 | Utils.throwIfNotInstanceOf( 20 | retryOptions, 21 | "retryOptions", 22 | new RetryOptions(1, 1), 23 | "RetryOptions" 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/actions/CallEntityAction.ts: -------------------------------------------------------------------------------- 1 | import { EntityId } from "../entities/EntityId"; 2 | import { Utils } from "../util/Utils"; 3 | import { ActionType } from "./ActionType"; 4 | import { IAction } from "./IAction"; 5 | 6 | /** @hidden */ 7 | export class CallEntityAction implements IAction { 8 | public readonly actionType: ActionType = ActionType.CallEntity; 9 | public readonly instanceId: string; 10 | public readonly input: unknown; 11 | 12 | constructor(entityId: EntityId, public readonly operation: string, input?: unknown) { 13 | if (!entityId) { 14 | throw new Error("Must provide EntityId to CallEntityAction constructor"); 15 | } 16 | this.input = input; 17 | Utils.throwIfEmpty(operation, "operation"); 18 | this.instanceId = EntityId.getSchedulerIdFromEntityId(entityId); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/actions/CallHttpAction.ts: -------------------------------------------------------------------------------- 1 | import { DurableHttpRequest } from "../http/DurableHttpRequest"; 2 | import { ActionType } from "./ActionType"; 3 | import { IAction } from "./IAction"; 4 | 5 | /** @hidden */ 6 | export class CallHttpAction implements IAction { 7 | public readonly actionType: ActionType = ActionType.CallHttp; 8 | 9 | constructor(public readonly httpRequest: DurableHttpRequest) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/actions/CallSubOrchestratorAction.ts: -------------------------------------------------------------------------------- 1 | import { Utils } from "../util/Utils"; 2 | import { ActionType } from "./ActionType"; 3 | import { IAction } from "./IAction"; 4 | 5 | /** @hidden */ 6 | export class CallSubOrchestratorAction implements IAction { 7 | public readonly actionType: ActionType = ActionType.CallSubOrchestrator; 8 | public readonly input: unknown; 9 | 10 | constructor( 11 | public readonly functionName: string, 12 | public readonly instanceId?: string, 13 | input?: unknown 14 | ) { 15 | this.input = input; 16 | Utils.throwIfEmpty(functionName, "functionName"); 17 | 18 | if (instanceId) { 19 | Utils.throwIfEmpty(instanceId, "instanceId"); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/actions/CallSubOrchestratorWithRetryAction.ts: -------------------------------------------------------------------------------- 1 | import { RetryOptions } from "../RetryOptions"; 2 | import { Utils } from "../util/Utils"; 3 | import { ActionType } from "./ActionType"; 4 | import { IAction } from "./IAction"; 5 | 6 | /** @hidden */ 7 | export class CallSubOrchestratorWithRetryAction implements IAction { 8 | public readonly actionType: ActionType = ActionType.CallSubOrchestratorWithRetry; 9 | public readonly input: unknown; 10 | 11 | constructor( 12 | public readonly functionName: string, 13 | public readonly retryOptions: RetryOptions, 14 | input?: unknown, 15 | public readonly instanceId?: string 16 | ) { 17 | this.input = input; 18 | Utils.throwIfEmpty(functionName, "functionName"); 19 | 20 | Utils.throwIfNotInstanceOf( 21 | retryOptions, 22 | "retryOptions", 23 | new RetryOptions(1, 1), 24 | "RetryOptions" 25 | ); 26 | 27 | if (instanceId) { 28 | Utils.throwIfEmpty(instanceId, "instanceId"); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/actions/ContinueAsNewAction.ts: -------------------------------------------------------------------------------- 1 | import { Utils } from "../util/Utils"; 2 | import { ActionType } from "./ActionType"; 3 | import { IAction } from "./IAction"; 4 | 5 | /** @hidden */ 6 | export class ContinueAsNewAction implements IAction { 7 | public readonly actionType: ActionType = ActionType.ContinueAsNew; 8 | public readonly input: unknown; 9 | 10 | constructor(input: unknown) { 11 | this.input = Utils.processInput(input); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/actions/CreateTimerAction.ts: -------------------------------------------------------------------------------- 1 | import { isDate } from "util"; 2 | import { ActionType } from "./ActionType"; 3 | import { IAction } from "./IAction"; 4 | 5 | /** @hidden */ 6 | export class CreateTimerAction implements IAction { 7 | public readonly actionType: ActionType = ActionType.CreateTimer; 8 | 9 | constructor(public readonly fireAt: Date, public isCanceled: boolean = false) { 10 | if (!isDate(fireAt)) { 11 | throw new TypeError(`fireAt: Expected valid Date object but got ${fireAt}`); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/actions/ExternalEventType.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @hidden 3 | * Represents the options that can be provided for the "reason" field of events in 4 | * Durable Functions 2.0. 5 | */ 6 | export enum ExternalEventType { 7 | ExternalEvent = "ExternalEvent", 8 | LockAcquisitionCompleted = "LockAcquisitionCompleted", 9 | EntityResponse = "EntityResponse", 10 | } 11 | -------------------------------------------------------------------------------- /src/actions/IAction.ts: -------------------------------------------------------------------------------- 1 | import { ActionType } from "./ActionType"; 2 | 3 | /** @hidden */ 4 | export interface IAction { 5 | actionType: ActionType; 6 | } 7 | -------------------------------------------------------------------------------- /src/actions/SignalEntityAction.ts: -------------------------------------------------------------------------------- 1 | import { EntityId } from "../entities/EntityId"; 2 | import { Utils } from "../util/Utils"; 3 | import { ActionType } from "./ActionType"; 4 | import { IAction } from "./IAction"; 5 | 6 | /** @hidden */ 7 | export class SignalEntityAction implements IAction { 8 | public readonly actionType: ActionType = ActionType.SignalEntity; 9 | public readonly instanceId: string; 10 | public readonly input: unknown; 11 | 12 | constructor(entityId: EntityId, public readonly operation: string, input?: unknown) { 13 | if (!entityId) { 14 | throw new Error("Must provide EntityId to SignalEntityAction constructor"); 15 | } 16 | this.input = input; 17 | Utils.throwIfEmpty(operation, "operation"); 18 | this.instanceId = EntityId.getSchedulerIdFromEntityId(entityId); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/actions/WaitForExternalEventAction.ts: -------------------------------------------------------------------------------- 1 | import { Utils } from "../util/Utils"; 2 | import { ActionType } from "./ActionType"; 3 | import { ExternalEventType } from "./ExternalEventType"; 4 | import { IAction } from "./IAction"; 5 | 6 | /** @hidden */ 7 | export class WaitForExternalEventAction implements IAction { 8 | public readonly actionType: ActionType = ActionType.WaitForExternalEvent; 9 | 10 | constructor( 11 | public readonly externalEventName: string, 12 | public readonly reason = ExternalEventType.ExternalEvent 13 | ) { 14 | Utils.throwIfEmpty(externalEventName, "externalEventName"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/actions/WhenAllAction.ts: -------------------------------------------------------------------------------- 1 | import { DFTask } from "../task"; 2 | import { ActionType } from "./ActionType"; 3 | import { IAction } from "./IAction"; 4 | 5 | /** @hidden */ 6 | export class WhenAllAction implements IAction { 7 | public readonly actionType: ActionType = ActionType.WhenAll; 8 | public readonly compoundActions: IAction[]; 9 | 10 | constructor(tasks: DFTask[]) { 11 | this.compoundActions = tasks.map((t) => t.actionObj); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/actions/WhenAnyAction.ts: -------------------------------------------------------------------------------- 1 | import { DFTask } from "../task"; 2 | import { ActionType } from "./ActionType"; 3 | import { IAction } from "./IAction"; 4 | 5 | /** @hidden */ 6 | export class WhenAnyAction implements IAction { 7 | public readonly actionType: ActionType = ActionType.WhenAny; 8 | public readonly compoundActions: IAction[]; 9 | 10 | constructor(tasks: DFTask[]) { 11 | this.compoundActions = tasks.map((t) => t.actionObj); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActivityOptions, 3 | EntityHandler, 4 | EntityOptions, 5 | OrchestrationHandler, 6 | OrchestrationOptions, 7 | RegisteredActivity, 8 | RegisteredOrchestration, 9 | } from "durable-functions"; 10 | import * as trigger from "./trigger"; 11 | import { createOrchestrator, createEntityFunction } from "./util/testingUtils"; 12 | import { app as azFuncApp } from "@azure/functions"; 13 | import { RegisteredOrchestrationTask } from "./task/RegisteredOrchestrationTask"; 14 | import { RegisteredActivityTask } from "./task/RegisteredActivityTask"; 15 | 16 | export function orchestration( 17 | functionName: string, 18 | handlerOrOptions: OrchestrationHandler | OrchestrationOptions 19 | ): RegisteredOrchestration { 20 | const options: OrchestrationOptions = 21 | typeof handlerOrOptions === "function" ? { handler: handlerOrOptions } : handlerOrOptions; 22 | 23 | azFuncApp.generic(functionName, { 24 | trigger: trigger.orchestration(), 25 | ...options, 26 | handler: createOrchestrator(options.handler), 27 | }); 28 | 29 | const result: RegisteredOrchestration = ( 30 | input?: unknown, 31 | instanceId?: string 32 | ): RegisteredOrchestrationTask => { 33 | return new RegisteredOrchestrationTask(functionName, input, instanceId); 34 | }; 35 | 36 | return result; 37 | } 38 | 39 | export function entity( 40 | functionName: string, 41 | handlerOrOptions: EntityHandler | EntityOptions 42 | ): void { 43 | const options: EntityOptions = 44 | typeof handlerOrOptions === "function" ? { handler: handlerOrOptions } : handlerOrOptions; 45 | 46 | azFuncApp.generic(functionName, { 47 | trigger: trigger.entity(), 48 | ...options, 49 | handler: createEntityFunction(options.handler), 50 | }); 51 | } 52 | 53 | export function activity(functionName: string, options: ActivityOptions): RegisteredActivity { 54 | azFuncApp.generic(functionName, { 55 | trigger: trigger.activity(), 56 | ...options, 57 | }); 58 | 59 | const result: RegisteredActivity = (input?: unknown): RegisteredActivityTask => { 60 | return new RegisteredActivityTask(functionName, input); 61 | }; 62 | 63 | return result; 64 | } 65 | 66 | export * as client from "./client"; 67 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import * as input from "./input"; 2 | import { 3 | CosmosDBDurableClientOptions, 4 | DurableClientHandler, 5 | DurableClientOptions, 6 | EventGridDurableClientOptions, 7 | EventHubDurableClientOptions, 8 | HttpDurableClientOptions, 9 | ServiceBusQueueDurableClientOptions, 10 | ServiceBusTopicDurableClientOptions, 11 | StorageBlobDurableClientOptions, 12 | StorageQueueDurableClientOptions, 13 | TimerDurableClientOptions, 14 | } from "durable-functions"; 15 | import { 16 | FunctionHandler, 17 | FunctionResult, 18 | InvocationContext, 19 | app as azFuncApp, 20 | } from "@azure/functions"; 21 | import { DurableClient } from "./durableClient/DurableClient"; 22 | import { getClient, isDurableClientInput } from "./durableClient/getClient"; 23 | 24 | export function http(functionName: string, options: HttpDurableClientOptions): void { 25 | addClientInput(options); 26 | azFuncApp.http(functionName, { 27 | ...options, 28 | handler: convertToFunctionHandler(options.handler), 29 | }); 30 | } 31 | 32 | export function timer(functionName: string, options: TimerDurableClientOptions): void { 33 | addClientInput(options); 34 | azFuncApp.timer(functionName, { 35 | ...options, 36 | handler: convertToFunctionHandler(options.handler), 37 | }); 38 | } 39 | 40 | export function storageBlob(functionName: string, options: StorageBlobDurableClientOptions): void { 41 | addClientInput(options); 42 | azFuncApp.storageBlob(functionName, { 43 | ...options, 44 | handler: convertToFunctionHandler(options.handler), 45 | }); 46 | } 47 | 48 | export function storageQueue( 49 | functionName: string, 50 | options: StorageQueueDurableClientOptions 51 | ): void { 52 | addClientInput(options); 53 | azFuncApp.storageQueue(functionName, { 54 | ...options, 55 | handler: convertToFunctionHandler(options.handler), 56 | }); 57 | } 58 | 59 | export function serviceBusQueue( 60 | functionName: string, 61 | options: ServiceBusQueueDurableClientOptions 62 | ): void { 63 | addClientInput(options); 64 | azFuncApp.serviceBusQueue(functionName, { 65 | ...options, 66 | handler: convertToFunctionHandler(options.handler), 67 | }); 68 | } 69 | 70 | export function serviceBusTopic( 71 | functionName: string, 72 | options: ServiceBusTopicDurableClientOptions 73 | ): void { 74 | addClientInput(options); 75 | azFuncApp.serviceBusTopic(functionName, { 76 | ...options, 77 | handler: convertToFunctionHandler(options.handler), 78 | }); 79 | } 80 | 81 | export function eventHub(functionName: string, options: EventHubDurableClientOptions): void { 82 | addClientInput(options); 83 | azFuncApp.eventHub(functionName, { 84 | ...options, 85 | handler: convertToFunctionHandler(options.handler), 86 | }); 87 | } 88 | 89 | export function eventGrid(functionName: string, options: EventGridDurableClientOptions): void { 90 | addClientInput(options); 91 | azFuncApp.eventGrid(functionName, { 92 | ...options, 93 | handler: convertToFunctionHandler(options.handler), 94 | }); 95 | } 96 | 97 | export function cosmosDB(functionName: string, options: CosmosDBDurableClientOptions): void { 98 | addClientInput(options); 99 | azFuncApp.cosmosDB(functionName, { 100 | ...options, 101 | handler: convertToFunctionHandler(options.handler), 102 | }); 103 | } 104 | 105 | export function generic(functionName: string, options: DurableClientOptions): void { 106 | addClientInput(options); 107 | azFuncApp.generic(functionName, { 108 | ...options, 109 | handler: convertToFunctionHandler(options.handler), 110 | }); 111 | } 112 | 113 | function addClientInput(options: Partial): void { 114 | options.extraInputs = options.extraInputs ?? []; 115 | if (!options.extraInputs.find(isDurableClientInput)) { 116 | options.extraInputs.push(input.durableClient()); 117 | } 118 | } 119 | 120 | function convertToFunctionHandler(clientHandler: DurableClientHandler): FunctionHandler { 121 | return (trigger: unknown, context: InvocationContext): FunctionResult => { 122 | const client: DurableClient = getClient(context); 123 | return clientHandler(trigger, client, context); 124 | }; 125 | } 126 | -------------------------------------------------------------------------------- /src/durableClient/OrchestrationClientInputData.ts: -------------------------------------------------------------------------------- 1 | import { HttpCreationPayload } from "../http/HttpCreationPayload"; 2 | import { HttpManagementPayload } from "../http/HttpManagementPayload"; 3 | 4 | /** @hidden */ 5 | export class OrchestrationClientInputData { 6 | public static isOrchestrationClientInputData(obj: unknown): boolean { 7 | const typedInstance = obj as { [index: string]: unknown }; 8 | if (typedInstance) { 9 | // Only check for required fields. 10 | if ( 11 | typedInstance.taskHubName !== undefined && 12 | typedInstance.creationUrls !== undefined && 13 | typedInstance.managementUrls !== undefined 14 | ) { 15 | return true; 16 | } 17 | return false; 18 | } 19 | return false; 20 | } 21 | 22 | constructor( 23 | public taskHubName: string, 24 | public creationUrls: HttpCreationPayload, 25 | public managementUrls: HttpManagementPayload, 26 | public baseUrl?: string, 27 | public requiredQueryStringParameters?: string, 28 | public rpcBaseUrl?: string 29 | ) {} 30 | } 31 | -------------------------------------------------------------------------------- /src/durableClient/PurgeHistoryResult.ts: -------------------------------------------------------------------------------- 1 | import * as types from "durable-functions"; 2 | 3 | export class PurgeHistoryResult implements types.PurgeHistoryResult { 4 | constructor(public readonly instancesDeleted: number) {} 5 | } 6 | -------------------------------------------------------------------------------- /src/durableClient/getClient.ts: -------------------------------------------------------------------------------- 1 | import { FunctionInput, InvocationContext } from "@azure/functions"; 2 | import { DurableClientInput } from "durable-functions"; 3 | import { DurableClient } from "./DurableClient"; 4 | import { OrchestrationClientInputData } from "./OrchestrationClientInputData"; 5 | /** @hidden */ 6 | import cloneDeep = require("lodash/cloneDeep"); 7 | /** @hidden */ 8 | import url = require("url"); 9 | import { HttpCreationPayload } from "../http/HttpCreationPayload"; 10 | import { HttpManagementPayload } from "../http/HttpManagementPayload"; 11 | import { isURL } from "validator"; 12 | import { Constants } from "../Constants"; 13 | 14 | export function getClient(context: InvocationContext): DurableClient { 15 | const foundInput: FunctionInput | undefined = context.options.extraInputs.find( 16 | isDurableClientInput 17 | ); 18 | if (!foundInput) { 19 | throw new Error( 20 | "Could not find a registered durable client input binding. Check your extraInputs definition when registering your function." 21 | ); 22 | } 23 | 24 | const clientInputOptions = foundInput as DurableClientInput; 25 | let clientData = getClientData(context, clientInputOptions); 26 | 27 | if (!process.env.WEBSITE_HOSTNAME || process.env.WEBSITE_HOSTNAME.includes("0.0.0.0")) { 28 | clientData = correctClientData(clientData); 29 | } 30 | 31 | return new DurableClient(clientData); 32 | } 33 | 34 | /** @hidden */ 35 | export function isDurableClientInput(input: FunctionInput): boolean { 36 | return input.type === "durableClient" || input.type === "orchestrationClient"; 37 | } 38 | 39 | /** @hidden */ 40 | function getClientData( 41 | context: InvocationContext, 42 | clientInput: DurableClientInput 43 | ): OrchestrationClientInputData { 44 | const clientData: unknown = context.extraInputs.get(clientInput); 45 | if (clientData && OrchestrationClientInputData.isOrchestrationClientInputData(clientData)) { 46 | return clientData as OrchestrationClientInputData; 47 | } 48 | 49 | throw new Error( 50 | "Received input is not a valid durable client input. Check your extraInputs definition when registering your function." 51 | ); 52 | } 53 | 54 | /** @hidden */ 55 | function correctClientData(clientData: OrchestrationClientInputData): OrchestrationClientInputData { 56 | const returnValue = cloneDeep(clientData); 57 | 58 | returnValue.creationUrls = correctUrls(clientData.creationUrls) as HttpCreationPayload; 59 | returnValue.managementUrls = correctUrls(clientData.managementUrls) as HttpManagementPayload; 60 | 61 | return returnValue; 62 | } 63 | 64 | function correctUrls(obj: { [key: string]: string }): { [key: string]: string } { 65 | const returnValue = cloneDeep(obj); 66 | 67 | const keys = Object.getOwnPropertyNames(obj); 68 | keys.forEach((key) => { 69 | const value = obj[key]; 70 | 71 | if ( 72 | isURL(value, { 73 | protocols: ["http", "https"], 74 | require_tld: false, 75 | require_protocol: true, 76 | }) 77 | ) { 78 | const valueAsUrl = new url.URL(value); 79 | returnValue[key] = value.replace(valueAsUrl.origin, Constants.DefaultLocalOrigin); 80 | } 81 | }); 82 | 83 | return returnValue; 84 | } 85 | -------------------------------------------------------------------------------- /src/entities/DurableEntityBindingInfo.ts: -------------------------------------------------------------------------------- 1 | import { EntityId } from "./EntityId"; 2 | import { RequestMessage } from "./RequestMessage"; 3 | 4 | /** @hidden */ 5 | export class DurableEntityBindingInfoReqFields { 6 | constructor( 7 | public readonly self: EntityId, 8 | public readonly exists: boolean, 9 | public readonly batch: RequestMessage[] 10 | ) {} 11 | } 12 | 13 | /** @hidden */ 14 | export class DurableEntityBindingInfo extends DurableEntityBindingInfoReqFields { 15 | constructor( 16 | public readonly self: EntityId, 17 | public readonly exists: boolean, 18 | public readonly state: string | undefined, 19 | public readonly batch: RequestMessage[] 20 | ) { 21 | super(self, exists, batch); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/entities/DurableLock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @hidden 3 | * For use with locking operations. 4 | * TODO: improve this 5 | */ 6 | export class DurableLock {} 7 | -------------------------------------------------------------------------------- /src/entities/EntityId.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:member-access 2 | 3 | import { Utils } from "../util/Utils"; 4 | import * as types from "durable-functions"; 5 | 6 | export class EntityId implements types.EntityId { 7 | /** @hidden */ 8 | static getEntityIdFromSchedulerId(schedulerId: string): EntityId { 9 | const pos = schedulerId.indexOf("@", 1); 10 | const entityName = schedulerId.substring(1, pos); 11 | const entityKey = schedulerId.substring(pos + 1); 12 | return new EntityId(entityName, entityKey); 13 | } 14 | 15 | /** @hidden */ 16 | static getSchedulerIdFromEntityId(entityId: EntityId): string { 17 | return `@${entityId.name.toLowerCase()}@${entityId.key}`; 18 | } 19 | 20 | constructor( 21 | // TODO: consider how to name these fields more accurately without interfering with JSON serialization 22 | public readonly name: string, 23 | public readonly key: string 24 | ) { 25 | Utils.throwIfEmpty(name, "name"); 26 | Utils.throwIfEmpty(key, "key"); 27 | } 28 | 29 | public toString(): string { 30 | return EntityId.getSchedulerIdFromEntityId(this); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/entities/EntityState.ts: -------------------------------------------------------------------------------- 1 | import { OperationResult } from "./OperationResult"; 2 | import { Signal } from "./Signal"; 3 | 4 | /** @hidden */ 5 | export class EntityState { 6 | public entityExists: boolean; 7 | public entityState: string | undefined; 8 | public readonly results: OperationResult[]; 9 | public readonly signals: Signal[]; 10 | 11 | constructor(results: OperationResult[], signals: Signal[]) { 12 | this.results = results; 13 | this.signals = signals; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/entities/EntityStateResponse.ts: -------------------------------------------------------------------------------- 1 | import * as types from "durable-functions"; 2 | 3 | export class EntityStateResponse implements types.EntityStateResponse { 4 | constructor(public entityExists: boolean, public entityState?: T) {} 5 | } 6 | -------------------------------------------------------------------------------- /src/entities/LockState.ts: -------------------------------------------------------------------------------- 1 | import { EntityId } from "./EntityId"; 2 | 3 | /** 4 | * @hidden 5 | * Returned by [DurableOrchestrationContext].[isLocked] and 6 | * [DurableEntityContext].[isLocked]. 7 | */ 8 | export class LockState { 9 | constructor( 10 | /** Whether the context already holds some locks. */ 11 | public readonly isLocked: boolean, 12 | /** The locks held by the context. */ 13 | public readonly ownedLocks: EntityId[] 14 | ) {} 15 | } 16 | -------------------------------------------------------------------------------- /src/entities/OperationResult.ts: -------------------------------------------------------------------------------- 1 | /** @hidden */ 2 | export class OperationResult { 3 | constructor( 4 | readonly isError: boolean, 5 | readonly duration: number, 6 | readonly startTime: number, 7 | readonly result?: string 8 | ) {} 9 | } 10 | -------------------------------------------------------------------------------- /src/entities/RequestMessage.ts: -------------------------------------------------------------------------------- 1 | import { EntityId } from "./EntityId"; 2 | 3 | /** @hidden */ 4 | export class RequestMessage { 5 | /** A unique identifier for this operation. */ 6 | public id: string; // Id 7 | 8 | /** 9 | * The name of the operation being called (if this is an operation message) 10 | * or undefined (if this is a lock request). 11 | */ 12 | public name?: string; // Operation 13 | 14 | /** Whether or not this is a one-way message. */ 15 | public signal?: boolean; // IsSignal 16 | 17 | /** The operation input. */ 18 | public input?: string; // Input 19 | 20 | /** The content the operation was called with. */ 21 | public arg?: unknown; // Content 22 | 23 | /** The parent instance that called this operation. */ 24 | public parent?: string; // ParentInstanceId 25 | 26 | /** 27 | * For lock requests, the set of locks being acquired. Is sorted, 28 | * contains at least one element, and has no repetitions. 29 | */ 30 | public lockset?: EntityId[]; // LockSet 31 | 32 | /** For lock requests involving multiple locks, the message number. */ 33 | public pos?: number; // Position 34 | } 35 | -------------------------------------------------------------------------------- /src/entities/ResponseMessage.ts: -------------------------------------------------------------------------------- 1 | import { Utils } from "../util/Utils"; 2 | 3 | /** @hidden */ 4 | export class ResponseMessage { 5 | public result?: string; // Result 6 | public exceptionType?: string; // ExceptionType 7 | 8 | public constructor(event: unknown) { 9 | if (typeof event === "object" && event !== null) { 10 | if (Utils.hasStringProperty(event, "result")) { 11 | this.result = event.result; 12 | } 13 | if (Utils.hasStringProperty(event, "exceptionType")) { 14 | this.exceptionType = event.exceptionType; 15 | } 16 | } else { 17 | throw Error( 18 | "Attempted to construct ResponseMessage event from incompatible History event. " + 19 | "This is probably a bug in History-replay. Please file a bug report." 20 | ); 21 | } 22 | } 23 | } 24 | 25 | // TODO: error deserialization 26 | -------------------------------------------------------------------------------- /src/entities/Signal.ts: -------------------------------------------------------------------------------- 1 | import { EntityId } from "./EntityId"; 2 | 3 | /** @hidden */ 4 | export class Signal { 5 | constructor( 6 | public readonly target: EntityId, 7 | public readonly name: string, 8 | public readonly input: string, 9 | public readonly requestId: string, 10 | public readonly requestTime: number 11 | ) {} 12 | } 13 | -------------------------------------------------------------------------------- /src/error/AggregatedError.ts: -------------------------------------------------------------------------------- 1 | import * as types from "durable-functions"; 2 | 3 | /** @hidden */ 4 | const separator = "-----------------------------------"; 5 | 6 | export class AggregatedError extends Error implements types.AggregatedError { 7 | public errors: Error[]; 8 | 9 | constructor(errors: Error[]) { 10 | const errorStrings = errors.map( 11 | (error) => `Name: ${error.name}\nMessage: ${error.message}\nStackTrace: ${error.stack}` 12 | ); 13 | const message = `context.df.Task.all() encountered the below error messages:\n\n${errorStrings.join( 14 | `\n${separator}\n` 15 | )}`; 16 | super(message); 17 | this.errors = errors; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/error/DurableError.ts: -------------------------------------------------------------------------------- 1 | import * as types from "durable-functions"; 2 | 3 | export class DurableError extends Error implements types.DurableError { 4 | constructor(message: string | undefined) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/error/OrchestrationFailureError.ts: -------------------------------------------------------------------------------- 1 | import { OrchestratorState } from "../orchestrations/OrchestratorState"; 2 | 3 | /** @hidden */ 4 | const outOfProcDataLabel = "\n\n$OutOfProcData$:"; 5 | 6 | /** 7 | * @hidden 8 | * A wrapper for all errors thrown within an orchestrator function. This exception will embed 9 | * the orchestrator state in a way that the C# extension knows how to read so that it can replay the 10 | * actions scheduled before throwing an exception. This prevents non-determinism errors in Durable Task. 11 | * 12 | * Note that making any changes to the following schema to OrchestrationFailureError.message could be considered a breaking change: 13 | * 14 | * "\n\n$OutOfProcData$" 15 | */ 16 | export class OrchestrationFailureError extends Error { 17 | constructor(error: any, state: OrchestratorState) { 18 | let errorMessage: string; 19 | if (error instanceof Error) { 20 | errorMessage = error.message; 21 | } else if (typeof error === "string") { 22 | errorMessage = error; 23 | } else { 24 | errorMessage = JSON.stringify(error); 25 | } 26 | 27 | const message = `${errorMessage}${outOfProcDataLabel}${JSON.stringify(state)}`; 28 | super(message); 29 | this.stack = error.stack; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/history/EventRaisedEvent.ts: -------------------------------------------------------------------------------- 1 | import { HistoryEvent } from "./HistoryEvent"; 2 | import { HistoryEventOptions } from "./HistoryEventOptions"; 3 | import { HistoryEventType } from "./HistoryEventType"; 4 | 5 | /** @hidden */ 6 | export class EventRaisedEvent extends HistoryEvent { 7 | public Name: string; 8 | public Input: string | undefined; 9 | 10 | constructor(options: HistoryEventOptions) { 11 | super(HistoryEventType.EventRaised, options.eventId, options.isPlayed, options.timestamp); 12 | 13 | if (options.name === undefined) { 14 | throw new Error("EventRaisedEvent needs a name provided."); 15 | } else { 16 | this.Name = options.name; 17 | } 18 | 19 | this.Input = options.input; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/history/EventSentEvent.ts: -------------------------------------------------------------------------------- 1 | import { HistoryEvent } from "./HistoryEvent"; 2 | import { HistoryEventOptions } from "./HistoryEventOptions"; 3 | import { HistoryEventType } from "./HistoryEventType"; 4 | 5 | /** @hidden */ 6 | export class EventSentEvent extends HistoryEvent { 7 | public Name: string; 8 | public Input: string | undefined; 9 | public InstanceId: string; 10 | 11 | constructor(options: HistoryEventOptions) { 12 | super(HistoryEventType.EventSent, options.eventId, options.isPlayed, options.timestamp); 13 | 14 | if (options.name === undefined) { 15 | throw new Error("EventSentEvent needs a name provided."); 16 | } 17 | 18 | if (options.instanceId === undefined) { 19 | throw new Error("EventSentEvent needs an instance id provided."); 20 | } 21 | 22 | this.Input = options.input; 23 | this.Name = options.name; 24 | this.InstanceId = options.instanceId; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/history/ExecutionStartedEvent.ts: -------------------------------------------------------------------------------- 1 | import { HistoryEvent } from "./HistoryEvent"; 2 | import { HistoryEventOptions } from "./HistoryEventOptions"; 3 | import { HistoryEventType } from "./HistoryEventType"; 4 | 5 | /** @hidden */ 6 | export class ExecutionStartedEvent extends HistoryEvent { 7 | public Name: string; 8 | public Input: string | undefined; 9 | 10 | constructor(options: HistoryEventOptions) { 11 | super( 12 | HistoryEventType.ExecutionStarted, 13 | options.eventId, 14 | options.isPlayed, 15 | options.timestamp 16 | ); 17 | 18 | if (options.name === undefined) { 19 | throw new Error("ExecutionStartedEvent needs a name provided."); 20 | } else { 21 | this.Name = options.name; 22 | } 23 | 24 | this.Input = options.input; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/history/HistoryEvent.ts: -------------------------------------------------------------------------------- 1 | import { HistoryEventType } from "./HistoryEventType"; 2 | 3 | /** @hidden */ 4 | export abstract class HistoryEvent { 5 | constructor( 6 | public EventType: HistoryEventType, 7 | public EventId: number, 8 | public IsPlayed: boolean, 9 | public Timestamp: Date, 10 | public IsProcessed: boolean = false 11 | ) {} 12 | } 13 | -------------------------------------------------------------------------------- /src/history/HistoryEventOptions.ts: -------------------------------------------------------------------------------- 1 | /** @hidden */ 2 | export class HistoryEventOptions { 3 | public details?: string; 4 | public fireAt?: Date; 5 | public input?: string; 6 | public instanceId?: string; 7 | public name?: string; 8 | public reason?: string; 9 | public result?: string; 10 | public taskScheduledId?: number; 11 | public timerId?: number; 12 | 13 | constructor(public eventId: number, public timestamp: Date, public isPlayed: boolean = false) {} 14 | } 15 | -------------------------------------------------------------------------------- /src/history/HistoryEventType.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @hidden 3 | * Corresponds to subclasses of HistoryEvent type in [Durable Task framework.](https://github.com/Azure/durabletask) 4 | */ 5 | export enum HistoryEventType { 6 | ExecutionStarted = 0, 7 | ExecutionCompleted = 1, 8 | ExecutionFailed = 2, 9 | ExecutionTerminated = 3, 10 | TaskScheduled = 4, 11 | TaskCompleted = 5, 12 | TaskFailed = 6, 13 | SubOrchestrationInstanceCreated = 7, 14 | SubOrchestrationInstanceCompleted = 8, 15 | SubOrchestrationInstanceFailed = 9, 16 | TimerCreated = 10, 17 | TimerFired = 11, 18 | OrchestratorStarted = 12, 19 | OrchestratorCompleted = 13, 20 | EventSent = 14, 21 | EventRaised = 15, 22 | ContinueAsNew = 16, 23 | GenericEvent = 17, 24 | HistoryState = 18, 25 | ExecutionSuspended = 19, 26 | ExecutionResumed = 20, 27 | } 28 | -------------------------------------------------------------------------------- /src/history/OrchestratorCompletedEvent.ts: -------------------------------------------------------------------------------- 1 | import { HistoryEvent } from "./HistoryEvent"; 2 | import { HistoryEventOptions } from "./HistoryEventOptions"; 3 | import { HistoryEventType } from "./HistoryEventType"; 4 | 5 | /** @hidden */ 6 | export class OrchestratorCompletedEvent extends HistoryEvent { 7 | constructor(options: HistoryEventOptions) { 8 | super( 9 | HistoryEventType.OrchestratorCompleted, 10 | options.eventId, 11 | options.isPlayed, 12 | options.timestamp 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/history/OrchestratorStartedEvent.ts: -------------------------------------------------------------------------------- 1 | import { HistoryEvent } from "./HistoryEvent"; 2 | import { HistoryEventOptions } from "./HistoryEventOptions"; 3 | import { HistoryEventType } from "./HistoryEventType"; 4 | 5 | /** @hidden */ 6 | export class OrchestratorStartedEvent extends HistoryEvent { 7 | constructor(options: HistoryEventOptions) { 8 | super( 9 | HistoryEventType.OrchestratorStarted, 10 | options.eventId, 11 | options.isPlayed, 12 | options.timestamp 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/history/SubOrchestrationInstanceCompletedEvent.ts: -------------------------------------------------------------------------------- 1 | import { HistoryEvent } from "./HistoryEvent"; 2 | import { HistoryEventOptions } from "./HistoryEventOptions"; 3 | import { HistoryEventType } from "./HistoryEventType"; 4 | 5 | /** @hidden */ 6 | export class SubOrchestrationInstanceCompletedEvent extends HistoryEvent { 7 | public TaskScheduledId: number; 8 | public Result: string; 9 | 10 | constructor(options: HistoryEventOptions) { 11 | super( 12 | HistoryEventType.SubOrchestrationInstanceCompleted, 13 | options.eventId, 14 | options.isPlayed, 15 | options.timestamp 16 | ); 17 | 18 | if (options.taskScheduledId === undefined) { 19 | throw new Error( 20 | "SubOrchestrationInstanceCompletedEvent needs a task scheduled id provided." 21 | ); 22 | } 23 | 24 | if (options.result === undefined) { 25 | throw new Error("SubOrchestrationInstanceCompletedEvent needs an result provided."); 26 | } 27 | 28 | this.TaskScheduledId = options.taskScheduledId; 29 | this.Result = options.result; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/history/SubOrchestrationInstanceCreatedEvent.ts: -------------------------------------------------------------------------------- 1 | import { HistoryEvent } from "./HistoryEvent"; 2 | import { HistoryEventOptions } from "./HistoryEventOptions"; 3 | import { HistoryEventType } from "./HistoryEventType"; 4 | 5 | /** @hidden */ 6 | export class SubOrchestrationInstanceCreatedEvent extends HistoryEvent { 7 | public Name: string; 8 | public InstanceId: string; 9 | public Input: string | undefined; 10 | 11 | constructor(options: HistoryEventOptions) { 12 | super( 13 | HistoryEventType.SubOrchestrationInstanceCreated, 14 | options.eventId, 15 | options.isPlayed, 16 | options.timestamp 17 | ); 18 | 19 | if (options.name === undefined) { 20 | throw new Error("SubOrchestrationInstanceCreatedEvent needs a name provided."); 21 | } 22 | 23 | if (options.instanceId === undefined) { 24 | throw new Error("SubOrchestrationInstanceCreatedEvent needs an instance id provided."); 25 | } 26 | 27 | this.Input = options.input; 28 | this.Name = options.name; 29 | this.InstanceId = options.instanceId; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/history/SubOrchestrationInstanceFailedEvent.ts: -------------------------------------------------------------------------------- 1 | import { HistoryEvent } from "./HistoryEvent"; 2 | import { HistoryEventOptions } from "./HistoryEventOptions"; 3 | import { HistoryEventType } from "./HistoryEventType"; 4 | 5 | /** @hidden */ 6 | export class SubOrchestrationInstanceFailedEvent extends HistoryEvent { 7 | public TaskScheduledId: number; 8 | public Reason: string | undefined; 9 | public Details: string | undefined; 10 | 11 | constructor(options: HistoryEventOptions) { 12 | super( 13 | HistoryEventType.SubOrchestrationInstanceFailed, 14 | options.eventId, 15 | options.isPlayed, 16 | options.timestamp 17 | ); 18 | 19 | if (options.taskScheduledId === undefined) { 20 | throw new Error( 21 | "SubOrchestrationInstanceFailedEvent needs a task scheduled id provided." 22 | ); 23 | } 24 | 25 | this.TaskScheduledId = options.taskScheduledId; 26 | this.Reason = options.reason; 27 | this.Details = options.details; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/history/TaskCompletedEvent.ts: -------------------------------------------------------------------------------- 1 | import { HistoryEvent } from "./HistoryEvent"; 2 | import { HistoryEventOptions } from "./HistoryEventOptions"; 3 | import { HistoryEventType } from "./HistoryEventType"; 4 | 5 | /** @hidden */ 6 | export class TaskCompletedEvent extends HistoryEvent { 7 | public TaskScheduledId: number; 8 | public Result: string; 9 | 10 | constructor(options: HistoryEventOptions) { 11 | super(HistoryEventType.TaskCompleted, options.eventId, options.isPlayed, options.timestamp); 12 | 13 | if (options.taskScheduledId === undefined) { 14 | throw new Error("TaskCompletedEvent needs a task scheduled id provided."); 15 | } 16 | 17 | if (options.result === undefined) { 18 | throw new Error("TaskCompletedEvent needs a result provided."); 19 | } 20 | 21 | this.TaskScheduledId = options.taskScheduledId; 22 | this.Result = options.result; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/history/TaskFailedEvent.ts: -------------------------------------------------------------------------------- 1 | import { HistoryEvent } from "./HistoryEvent"; 2 | import { HistoryEventOptions } from "./HistoryEventOptions"; 3 | import { HistoryEventType } from "./HistoryEventType"; 4 | 5 | /** @hidden */ 6 | export class TaskFailedEvent extends HistoryEvent { 7 | public TaskScheduledId: number; 8 | public Reason: string | undefined; 9 | public Details: string | undefined; 10 | 11 | constructor(options: HistoryEventOptions) { 12 | super(HistoryEventType.TaskFailed, options.eventId, options.isPlayed, options.timestamp); 13 | 14 | if (options.taskScheduledId === undefined) { 15 | throw new Error("TaskFailedEvent needs a task scheduled id provided."); 16 | } 17 | 18 | this.TaskScheduledId = options.taskScheduledId; 19 | this.Reason = options.reason; 20 | this.Details = options.details; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/history/TaskScheduledEvent.ts: -------------------------------------------------------------------------------- 1 | import { HistoryEvent } from "./HistoryEvent"; 2 | import { HistoryEventOptions } from "./HistoryEventOptions"; 3 | import { HistoryEventType } from "./HistoryEventType"; 4 | 5 | /** @hidden */ 6 | export class TaskScheduledEvent extends HistoryEvent { 7 | public Name: string; 8 | public Input: string | undefined; 9 | 10 | constructor(options: HistoryEventOptions) { 11 | super(HistoryEventType.TaskScheduled, options.eventId, options.isPlayed, options.timestamp); 12 | 13 | if (options.name === undefined) { 14 | throw new Error("TaskScheduledEvent needs a name provided."); 15 | } 16 | 17 | this.Input = options.input; 18 | this.Name = options.name; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/history/TimerCreatedEvent.ts: -------------------------------------------------------------------------------- 1 | import { HistoryEvent } from "./HistoryEvent"; 2 | import { HistoryEventOptions } from "./HistoryEventOptions"; 3 | import { HistoryEventType } from "./HistoryEventType"; 4 | 5 | /** @hidden */ 6 | export class TimerCreatedEvent extends HistoryEvent { 7 | public FireAt: Date; 8 | 9 | constructor(options: HistoryEventOptions) { 10 | super(HistoryEventType.TimerCreated, options.eventId, options.isPlayed, options.timestamp); 11 | 12 | if (options.fireAt === undefined) { 13 | throw new Error("TimerCreatedEvent needs a fireAt time provided."); 14 | } 15 | 16 | this.FireAt = options.fireAt; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/history/TimerFiredEvent.ts: -------------------------------------------------------------------------------- 1 | import { HistoryEvent } from "./HistoryEvent"; 2 | import { HistoryEventOptions } from "./HistoryEventOptions"; 3 | import { HistoryEventType } from "./HistoryEventType"; 4 | 5 | /** @hidden */ 6 | export class TimerFiredEvent extends HistoryEvent { 7 | public TimerId: number; 8 | public FireAt: Date; 9 | 10 | constructor(options: HistoryEventOptions) { 11 | super(HistoryEventType.TimerFired, options.eventId, options.isPlayed, options.timestamp); 12 | 13 | if (options.timerId === undefined) { 14 | throw new Error("TimerFiredEvent needs a timer id provided."); 15 | } 16 | 17 | if (options.fireAt === undefined) { 18 | throw new Error("TimerFiredEvent needs a fireAt time provided."); 19 | } 20 | 21 | this.TimerId = options.timerId; 22 | this.FireAt = options.fireAt; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/http/DurableHttpRequest.ts: -------------------------------------------------------------------------------- 1 | import { TokenSource } from "durable-functions"; 2 | 3 | /** 4 | * Data structure representing a durable HTTP request. 5 | */ 6 | export class DurableHttpRequest { 7 | /** 8 | * Creates a new instance of DurableHttpRequest with the 9 | * specified parameters. 10 | * 11 | * @param method The HTTP request method. 12 | * @param uri The HTTP request URL. 13 | * @param content The HTTP request content. 14 | * @param headers The HTTP request headers. 15 | * @param tokenSource The source of OAuth tokens to add to the request. 16 | * @param asynchronousPatternEnabled Specifies whether the DurableHttpRequest should handle the asynchronous pattern. 17 | */ 18 | constructor( 19 | /** The HTTP request method. */ 20 | public readonly method: string, 21 | /** The HTTP request URL. */ 22 | public readonly uri: string, 23 | /** The HTTP request content. */ 24 | public readonly content?: string, 25 | /** The HTTP request headers. */ 26 | public readonly headers?: { 27 | [key: string]: string; 28 | }, 29 | /** The source of OAuth token to add to the request. */ 30 | public readonly tokenSource?: TokenSource, 31 | /** Whether the DurableHttpRequest should handle the asynchronous pattern. **/ 32 | public readonly asynchronousPatternEnabled: boolean = true 33 | ) {} 34 | } 35 | -------------------------------------------------------------------------------- /src/http/DurableHttpResponse.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Data structure representing a durable HTTP response. 3 | */ 4 | export class DurableHttpResponse { 5 | /** 6 | * Creates a new instance of DurableHttpResponse with the 7 | * specified parameters. 8 | * 9 | * @param statusCode The HTTP response status code. 10 | * @param content The HTTP response content. 11 | * @param headers The HTTP response headers. 12 | */ 13 | constructor( 14 | /** The HTTP response status code. */ 15 | public statusCode: number, 16 | 17 | /** The HTTP response content. */ 18 | public content?: string, 19 | 20 | /** The HTTP response headers. */ 21 | public headers?: { 22 | [key: string]: string; 23 | } 24 | ) {} 25 | 26 | // returns the specified header, case insensitively 27 | // returns undefined if the header is not set 28 | public getHeader(name: string): string | undefined { 29 | if (this.headers) { 30 | const lowerCaseName = name.toLowerCase(); 31 | const foundKey = Object.keys(this.headers).find( 32 | (key) => key.toLowerCase() === lowerCaseName 33 | ); 34 | if (foundKey) { 35 | return this.headers[foundKey]; 36 | } 37 | } 38 | return undefined; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/http/HttpCreationPayload.ts: -------------------------------------------------------------------------------- 1 | /** @hidden */ 2 | export class HttpCreationPayload { 3 | [key: string]: string; 4 | 5 | constructor(public createNewInstancePostUri: string, public waitOnNewInstancePostUri: string) {} 6 | } 7 | -------------------------------------------------------------------------------- /src/http/HttpManagementPayload.ts: -------------------------------------------------------------------------------- 1 | import * as types from "durable-functions"; 2 | 3 | export class HttpManagementPayload implements types.HttpManagementPayload { 4 | /** @hidden */ 5 | [key: string]: string; 6 | 7 | /** @hidden */ 8 | constructor( 9 | public readonly id: string, 10 | public readonly statusQueryGetUri: string, 11 | public readonly sendEventPostUri: string, 12 | public readonly terminatePostUri: string, 13 | public readonly rewindPostUri: string, 14 | public readonly purgeHistoryDeleteUri: string, 15 | public readonly suspendPostUri: string, 16 | public readonly resumePostUri: string 17 | ) {} 18 | } 19 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { DummyEntityContext, DummyOrchestrationContext } from "./util/testingUtils"; 2 | import { ManagedIdentityTokenSource } from "./ManagedIdentityTokenSource"; 3 | import { EntityId } from "./entities/EntityId"; 4 | import { EntityStateResponse } from "./entities/EntityStateResponse"; 5 | import { OrchestrationRuntimeStatus } from "./orchestrations/OrchestrationRuntimeStatus"; 6 | import { RetryOptions } from "./RetryOptions"; 7 | import { getClient } from "./durableClient/getClient"; 8 | 9 | export * as app from "./app"; 10 | export * as trigger from "./trigger"; 11 | export * as input from "./input"; 12 | 13 | export { 14 | EntityId, 15 | EntityStateResponse, 16 | getClient, 17 | ManagedIdentityTokenSource, 18 | OrchestrationRuntimeStatus, 19 | RetryOptions, 20 | DummyOrchestrationContext, 21 | DummyEntityContext, 22 | }; 23 | -------------------------------------------------------------------------------- /src/input.ts: -------------------------------------------------------------------------------- 1 | import { DurableClientInput } from "durable-functions"; 2 | import { input as azFuncInput } from "@azure/functions"; 3 | 4 | export function durableClient(): DurableClientInput { 5 | return azFuncInput.generic({ 6 | type: "durableClient", 7 | }) as DurableClientInput; 8 | } 9 | -------------------------------------------------------------------------------- /src/orchestrations/DurableOrchestrationBindingInfo.ts: -------------------------------------------------------------------------------- 1 | import { HistoryEvent } from "../history/HistoryEvent"; 2 | import { ReplaySchema } from "./ReplaySchema"; 3 | 4 | /** @hidden */ 5 | export class DurableOrchestrationBindingInfoReqFields { 6 | constructor( 7 | public readonly history: HistoryEvent[] = [], 8 | public readonly instanceId: string = "", 9 | public readonly isReplaying: boolean = false, 10 | public readonly upperSchemaVersion: ReplaySchema = ReplaySchema.V1 // TODO: Implement entity locking // public readonly contextLocks?: EntityId[], 11 | ) {} 12 | } 13 | 14 | /** @hidden */ 15 | export class DurableOrchestrationBindingInfo extends DurableOrchestrationBindingInfoReqFields { 16 | public readonly upperSchemaVersionNew?: ReplaySchema; 17 | 18 | constructor( 19 | public readonly history: HistoryEvent[] = [], 20 | public readonly input?: unknown, 21 | public readonly instanceId: string = "", 22 | public readonly isReplaying: boolean = false, 23 | public readonly parentInstanceId?: string, 24 | public readonly maximumShortTimerDuration?: string, 25 | public readonly longRunningTimerIntervalDuration?: string, 26 | public readonly defaultHttpAsyncRequestSleepTimeMillseconds?: number, 27 | public readonly upperSchemaVersion: ReplaySchema = ReplaySchema.V1 // TODO: Implement entity locking // public readonly contextLocks?: EntityId[], 28 | ) { 29 | super(history, instanceId, isReplaying, upperSchemaVersion); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/orchestrations/DurableOrchestrationStatus.ts: -------------------------------------------------------------------------------- 1 | import * as types from "durable-functions"; 2 | import { OrchestrationRuntimeStatus } from "./OrchestrationRuntimeStatus"; 3 | 4 | export class DurableOrchestrationStatus implements types.DurableOrchestrationStatus { 5 | public readonly name: string; 6 | public readonly instanceId: string; 7 | public readonly createdTime: Date; 8 | public readonly lastUpdatedTime: Date; 9 | public readonly input: unknown; 10 | public readonly output: unknown; 11 | public readonly runtimeStatus: OrchestrationRuntimeStatus; 12 | public readonly customStatus?: unknown; 13 | public readonly history?: Array; 14 | 15 | /** @hidden */ 16 | constructor(init: unknown) { 17 | if (!this.isDurableOrchestrationStatusInit(init)) { 18 | throw new TypeError( 19 | `Failed to construct a DurableOrchestrationStatus object because the initializer had invalid types or missing fields. Initializer received: ${JSON.stringify( 20 | init 21 | )}` 22 | ); 23 | } 24 | 25 | this.name = init.name; 26 | this.instanceId = init.instanceId; 27 | this.createdTime = new Date(init.createdTime); 28 | this.lastUpdatedTime = new Date(init.lastUpdatedTime); 29 | this.input = init.input; 30 | this.output = init.output; 31 | this.runtimeStatus = init.runtimeStatus as OrchestrationRuntimeStatus; 32 | this.customStatus = init.customStatus; 33 | this.history = init.history; 34 | } 35 | 36 | private isDurableOrchestrationStatusInit(obj: unknown): obj is DurableOrchestrationStatusInit { 37 | const objAsInit = obj as DurableOrchestrationStatusInit; 38 | return ( 39 | objAsInit !== undefined && 40 | typeof objAsInit.name === "string" && 41 | typeof objAsInit.instanceId === "string" && 42 | (typeof objAsInit.createdTime === "string" || objAsInit.createdTime instanceof Date) && 43 | (typeof objAsInit.lastUpdatedTime === "string" || 44 | objAsInit.lastUpdatedTime instanceof Date) && 45 | objAsInit.input !== undefined && 46 | objAsInit.output !== undefined && 47 | typeof objAsInit.runtimeStatus === "string" 48 | ); 49 | } 50 | } 51 | 52 | interface DurableOrchestrationStatusInit { 53 | name: string; 54 | instanceId: string; 55 | createdTime: string | Date; 56 | lastUpdatedTime: string | Date; 57 | input: unknown; 58 | output: unknown; 59 | runtimeStatus: string; 60 | customStatus?: unknown; 61 | history?: Array; 62 | } 63 | -------------------------------------------------------------------------------- /src/orchestrations/IOrchestratorState.ts: -------------------------------------------------------------------------------- 1 | import { IAction } from "../actions/IAction"; 2 | import { ReplaySchema } from "./ReplaySchema"; 3 | 4 | /** @hidden */ 5 | export interface IOrchestratorState { 6 | isDone: boolean; 7 | actions: IAction[][]; 8 | output: unknown; 9 | error?: string; 10 | customStatus?: unknown; 11 | schemaVersion: ReplaySchema; 12 | } 13 | -------------------------------------------------------------------------------- /src/orchestrations/OrchestrationRuntimeStatus.ts: -------------------------------------------------------------------------------- 1 | export enum OrchestrationRuntimeStatus { 2 | Running = "Running", 3 | Completed = "Completed", 4 | ContinuedAsNew = "ContinuedAsNew", 5 | Failed = "Failed", 6 | Canceled = "Canceled", 7 | Terminated = "Terminated", 8 | Pending = "Pending", 9 | Suspended = "Suspended", 10 | } 11 | -------------------------------------------------------------------------------- /src/orchestrations/OrchestratorState.ts: -------------------------------------------------------------------------------- 1 | import { IAction } from "../actions/IAction"; 2 | import { WhenAllAction } from "../actions/WhenAllAction"; 3 | import { WhenAnyAction } from "../actions/WhenAnyAction"; 4 | import { IOrchestratorState } from "./IOrchestratorState"; 5 | import { ReplaySchema } from "./ReplaySchema"; 6 | 7 | /** @hidden */ 8 | export class OrchestratorState implements IOrchestratorState { 9 | public readonly isDone: boolean; 10 | public readonly actions: IAction[][]; 11 | public readonly output: unknown; 12 | public readonly error?: string; 13 | public readonly customStatus?: unknown; 14 | public readonly schemaVersion: ReplaySchema; 15 | 16 | /** 17 | * @hidden 18 | * 19 | * It flattens an array of actions. 20 | * By flatten, we mean that, in the presence of a compound action (WhenAll/WhenAny), 21 | * we recursively extract all of its sub-actions into a flat sequence which is then 22 | * put in-place of the original compound action. 23 | * 24 | * For example, given the array: 25 | * [Activity, Activity, WhenAll(Activity, WhenAny(ExternalEvent, Activity))] 26 | * We obtain: 27 | * [Activity, Activity, Activity, ExternalEvent, Activity] 28 | * 29 | * This is helpful in translating the representation of user actions in 30 | * the DF extension replay protocol V2 to V1. 31 | * 32 | * @param actions 33 | * The array of actions to flatten 34 | * @returns 35 | * The flattened array of actions. 36 | */ 37 | private flattenCompoundActions(actions: IAction[]): IAction[] { 38 | const flatActions: IAction[] = []; 39 | for (const action of actions) { 40 | // Given any compound action 41 | if (action instanceof WhenAllAction || action instanceof WhenAnyAction) { 42 | // We obtain its inner actions as a flat array 43 | const innerActionArr = this.flattenCompoundActions(action.compoundActions); 44 | // we concatenate the inner actions to the flat array we're building 45 | flatActions.push(...innerActionArr); 46 | } else { 47 | // The action wasn't compound, so it's left intact 48 | flatActions.push(action); 49 | } 50 | } 51 | return flatActions; 52 | } 53 | 54 | // literal actions is used exclusively to facilitate testing. If true, the action representation is to be left intact 55 | constructor(options: IOrchestratorState, _literalActions = false) { 56 | this.isDone = options.isDone; 57 | this.actions = options.actions; 58 | this.output = options.output; 59 | this.schemaVersion = options.schemaVersion; 60 | 61 | if (options.error) { 62 | this.error = options.error; 63 | } 64 | 65 | if (options.customStatus) { 66 | this.customStatus = options.customStatus; 67 | } 68 | // Under replay protocol V1, compound actions are treated as lists of actions and 69 | // atomic actions are represented as a 1-element lists. 70 | // For example, given actions list: [Activity, WhenAny(ExternalEvent, WhenAll(Timer, Activity))] 71 | // The V1 protocol expects: [[Activity], [ExternalEvent, Timer, Activity]] 72 | if (options.schemaVersion === ReplaySchema.V1 && !_literalActions) { 73 | // We need to transform our V2 action representation to V1. 74 | // In V2, actions are represented as 2D arrays (for legacy reasons) with a singular element: an array of actions. 75 | const actions = this.actions[0]; 76 | const newActions: IAction[][] = []; 77 | // guard against empty array, meaning no user actions were scheduled 78 | if (actions !== undefined) { 79 | for (const action of actions) { 80 | // Each action is represented as an array in V1 81 | const newEntry: IAction[] = []; 82 | if (action instanceof WhenAllAction || action instanceof WhenAnyAction) { 83 | const innerActionArr = this.flattenCompoundActions(action.compoundActions); 84 | newEntry.push(...innerActionArr); 85 | } else { 86 | newEntry.push(action); 87 | } 88 | newActions.push(newEntry); 89 | } 90 | this.actions = newActions; 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/orchestrations/ReplaySchema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @hidden 3 | * Supported OOProc DF extension protocols 4 | */ 5 | export enum ReplaySchema { 6 | V1 = 0, 7 | V2 = 1, 8 | V3 = 2, 9 | } 10 | 11 | export const LatestReplaySchema: ReplaySchema = ReplaySchema.V3; 12 | -------------------------------------------------------------------------------- /src/task/AtomicTask.ts: -------------------------------------------------------------------------------- 1 | import { DFTask } from "./DFTask"; 2 | 3 | export class AtomicTask extends DFTask {} 4 | -------------------------------------------------------------------------------- /src/task/CallHttpWithPollingTask.ts: -------------------------------------------------------------------------------- 1 | import { CompoundTask } from "./CompoundTask"; 2 | import { AtomicTask, TaskBase, TaskID, TaskState } from "."; 3 | import { DurableOrchestrationContext } from "../orchestrations/DurableOrchestrationContext"; 4 | import { TaskOrchestrationExecutor } from "src/orchestrations/TaskOrchestrationExecutor"; 5 | import moment = require("moment"); 6 | import { CallHttpAction } from "../actions/CallHttpAction"; 7 | import { DurableHttpResponse } from "../http/DurableHttpResponse"; 8 | 9 | /** 10 | * @hidden 11 | * 12 | * CallHttp Task with polling logic 13 | * 14 | * If the HTTP requests returns a 202 status code with a 'Location' header, 15 | * then a timer task is created, after which another HTTP request is made, 16 | * until a different status code is returned. 17 | * 18 | * Any other result from the HTTP requests is the result of the whole task. 19 | * 20 | * The duration of the timer is specified by the 'Retry-After' header (in seconds) 21 | * of the 202 response, or a default value specified by the durable extension is used. 22 | * 23 | */ 24 | export class CallHttpWithPollingTask extends CompoundTask { 25 | protected action: CallHttpAction; 26 | private readonly defaultHttpAsyncRequestSleepDuration: moment.Duration; 27 | 28 | public constructor( 29 | id: TaskID, 30 | action: CallHttpAction, 31 | private readonly orchestrationContext: DurableOrchestrationContext, 32 | private readonly executor: TaskOrchestrationExecutor, 33 | defaultHttpAsyncRequestSleepTimeMillseconds: number 34 | ) { 35 | super([new AtomicTask(id, action)], action); 36 | this.id = id; 37 | this.action = action; 38 | this.defaultHttpAsyncRequestSleepDuration = moment.duration( 39 | defaultHttpAsyncRequestSleepTimeMillseconds, 40 | "ms" 41 | ); 42 | } 43 | 44 | public trySetValue(child: TaskBase): void { 45 | if (child.stateObj === TaskState.Completed) { 46 | if (child.actionObj instanceof CallHttpAction) { 47 | const resultObj = child.result as DurableHttpResponse; 48 | const result = new DurableHttpResponse( 49 | resultObj.statusCode, 50 | resultObj.content, 51 | resultObj.headers 52 | ); 53 | if (result.statusCode === 202 && result.getHeader("Location")) { 54 | const retryAfterHeaderValue = result.getHeader("Retry-After"); 55 | const delay: moment.Duration = retryAfterHeaderValue 56 | ? moment.duration(retryAfterHeaderValue, "s") 57 | : this.defaultHttpAsyncRequestSleepDuration; 58 | 59 | const currentTime = this.orchestrationContext.currentUtcDateTime; 60 | const timerFireTime = moment(currentTime).add(delay).toDate(); 61 | 62 | // this should be safe since both types returned by this call 63 | // (DFTimerTask and LongTimerTask) are TaskBase-conforming 64 | const timerTask = (this.orchestrationContext.createTimer( 65 | timerFireTime 66 | ) as unknown) as TaskBase; 67 | const callHttpTask = new AtomicTask( 68 | false, 69 | new CallHttpAction(this.action.httpRequest) 70 | ); 71 | 72 | this.addNewChildren([timerTask, callHttpTask]); 73 | } else { 74 | // Set the value of a non-redirect HTTP response as the value of the entire 75 | // compound task 76 | this.setValue(false, result); 77 | } 78 | } 79 | } else { 80 | // If any subtask failed, we fail the entire compound task 81 | if (this.firstError === undefined) { 82 | this.firstError = child.result as Error; 83 | this.setValue(true, this.firstError); 84 | } 85 | } 86 | } 87 | 88 | private addNewChildren(children: TaskBase[]): void { 89 | children.map((child) => { 90 | child.parent = this; 91 | this.children.push(child); 92 | this.executor.trackOpenTask(child); 93 | }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/task/CompoundTask.ts: -------------------------------------------------------------------------------- 1 | import { TaskOrchestrationExecutor } from "src/orchestrations/TaskOrchestrationExecutor"; 2 | import { IAction } from "../actions/IAction"; 3 | import { DFTask } from "./DFTask"; 4 | import { TaskBase } from "./TaskBase"; 5 | 6 | /** 7 | * @hidden 8 | * 9 | * A task that depends on the completion of other (sub-) tasks. 10 | */ 11 | export abstract class CompoundTask extends DFTask { 12 | protected firstError: Error | undefined; 13 | 14 | /** 15 | * @hidden 16 | * Construct a Compound Task. 17 | * Primarily sets the parent pointer of each sub-task to be `this`. 18 | * 19 | * @param children 20 | * The sub-tasks that this task depends on 21 | * @param action 22 | * An action representing this compound task 23 | */ 24 | constructor(public children: TaskBase[], protected action: IAction) { 25 | super(false, action); 26 | children.map((c) => (c.parent = this)); 27 | this.firstError = undefined; 28 | 29 | // If the task has no children, throw an error 30 | // See issue here for why this isn't allowed: https://github.com/Azure/azure-functions-durable-js/issues/424 31 | if (children.length == 0) { 32 | const message = 33 | "When constructing a CompoundTask (such as Task.all() or Task.any()), you must specify at least one Task."; 34 | throw new Error(message); 35 | } 36 | } 37 | 38 | /** 39 | * @hidden 40 | * Tries to set this task's result based on the completion of a sub-task 41 | * @param child 42 | * @param executor The TaskOrchestrationExecutor instance that is managing the replay 43 | * This argument is optional, and mostly passed to the RetryableTask trySetValue() method 44 | * A sub-task of this task. 45 | */ 46 | public handleCompletion(child: TaskBase, executor?: TaskOrchestrationExecutor): void { 47 | if (!this.isPlayed) { 48 | this.isPlayed = child.isPlayed; 49 | } 50 | this.trySetValue(child, executor); 51 | } 52 | 53 | /** 54 | * @hidden 55 | * 56 | * Task-internal logic for attempting to set this tasks' result 57 | * after any of its sub-tasks completes. 58 | * @param child 59 | * A sub-task 60 | */ 61 | abstract trySetValue(child: TaskBase, executor?: TaskOrchestrationExecutor): void; 62 | } 63 | -------------------------------------------------------------------------------- /src/task/DFTask.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "durable-functions"; 2 | import { TaskBase } from "./TaskBase"; 3 | import { IAction } from "../actions/IAction"; 4 | 5 | /** 6 | * @hidden 7 | * A task that should result in an Action being communicated to the DF extension. 8 | */ 9 | export class DFTask extends TaskBase implements Task { 10 | protected action: IAction; 11 | public alreadyScheduled = false; 12 | 13 | /** Get this task's backing action */ 14 | get actionObj(): IAction { 15 | return this.action; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/task/DFTimerTask.ts: -------------------------------------------------------------------------------- 1 | import { TimerTask } from "durable-functions"; 2 | import { AtomicTask } from "./AtomicTask"; 3 | import { TaskID } from "."; 4 | import { CreateTimerAction } from "../actions/CreateTimerAction"; 5 | 6 | /** 7 | * @hidden 8 | * A timer task. This is the internal implementation to the user-exposed TimerTask interface, which 9 | * has a more restricted API. 10 | */ 11 | export class DFTimerTask extends AtomicTask implements TimerTask { 12 | /** 13 | * @hidden 14 | * Construct a Timer Task. 15 | * 16 | * @param id 17 | * The task's ID 18 | * @param action 19 | * The backing action of this task 20 | */ 21 | constructor(public id: TaskID, public action: CreateTimerAction) { 22 | super(id, action); 23 | } 24 | 25 | /** Whether this timer task is canceled */ 26 | get isCanceled(): boolean { 27 | return this.action.isCanceled; 28 | } 29 | 30 | /** 31 | * @hidden 32 | * Cancel this timer task. 33 | * It errors out if the task has already completed. 34 | */ 35 | public cancel(): void { 36 | if (this.hasResult) { 37 | throw Error("Cannot cancel a completed task."); 38 | } 39 | this.action.isCanceled = true; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/task/LongTimerTask.ts: -------------------------------------------------------------------------------- 1 | import { DurableOrchestrationContext, TimerTask } from "durable-functions"; 2 | import { WhenAllTask } from "./WhenAllTask"; 3 | import moment = require("moment"); 4 | import { TaskOrchestrationExecutor } from "../orchestrations/TaskOrchestrationExecutor"; 5 | import { TaskID } from "."; 6 | import { DFTimerTask } from "./DFTimerTask"; 7 | import { CreateTimerAction } from "../actions/CreateTimerAction"; 8 | 9 | /** 10 | * @hidden 11 | * 12 | * A long Timer Task. 13 | * 14 | * This Task is created when a timer is created with a duration 15 | * longer than the maximum timer duration supported by storage infrastructure. 16 | * 17 | * It extends `WhenAllTask` because it decomposes into 18 | * several smaller sub-`TimerTask`s 19 | */ 20 | export class LongTimerTask extends WhenAllTask implements TimerTask { 21 | public id: TaskID; 22 | public action: CreateTimerAction; 23 | private readonly executor: TaskOrchestrationExecutor; 24 | private readonly maximumTimerDuration: moment.Duration; 25 | private readonly orchestrationContext: DurableOrchestrationContext; 26 | private readonly longRunningTimerIntervalDuration: moment.Duration; 27 | 28 | public constructor( 29 | id: TaskID, 30 | action: CreateTimerAction, 31 | orchestrationContext: DurableOrchestrationContext, 32 | executor: TaskOrchestrationExecutor, 33 | maximumTimerLength: string, 34 | longRunningTimerIntervalLength: string 35 | ) { 36 | const maximumTimerDuration = moment.duration(maximumTimerLength); 37 | const longRunningTimerIntervalDuration = moment.duration(longRunningTimerIntervalLength); 38 | const currentTime = orchestrationContext.currentUtcDateTime; 39 | const finalFireTime = action.fireAt; 40 | const durationUntilFire = moment.duration(moment(finalFireTime).diff(currentTime)); 41 | 42 | const nextFireTime: Date = 43 | durationUntilFire > maximumTimerDuration 44 | ? moment(currentTime).add(longRunningTimerIntervalDuration).toDate() 45 | : finalFireTime; 46 | 47 | const nextTimerAction = new CreateTimerAction(nextFireTime); 48 | const nextTimerTask = new DFTimerTask(false, nextTimerAction); 49 | super([nextTimerTask], action); 50 | 51 | this.id = id; 52 | this.action = action; 53 | this.orchestrationContext = orchestrationContext; 54 | this.executor = executor; 55 | this.maximumTimerDuration = maximumTimerDuration; 56 | this.longRunningTimerIntervalDuration = longRunningTimerIntervalDuration; 57 | } 58 | 59 | get isCanceled(): boolean { 60 | return this.action.isCanceled; 61 | } 62 | 63 | /** 64 | * @hidden 65 | * Cancel this timer task. 66 | * It errors out if the task has already completed. 67 | */ 68 | public cancel(): void { 69 | if (this.hasResult) { 70 | throw Error("Cannot cancel a completed task."); 71 | } 72 | this.action.isCanceled = true; 73 | } 74 | 75 | /** 76 | * @hidden 77 | * Attempts to set a value to this timer, given a completed sub-timer 78 | * 79 | * @param child 80 | * The sub-timer that just completed 81 | */ 82 | public trySetValue(child: DFTimerTask): void { 83 | const currentTime = this.orchestrationContext.currentUtcDateTime; 84 | const finalFireTime = this.action.fireAt; 85 | if (finalFireTime > currentTime) { 86 | const nextTimer: DFTimerTask = this.getNextTimerTask(finalFireTime, currentTime); 87 | this.addNewChild(nextTimer); 88 | } 89 | super.trySetValue(child); 90 | } 91 | 92 | private getNextTimerTask(finalFireTime: Date, currentTime: Date): DFTimerTask { 93 | const durationUntilFire = moment.duration(moment(finalFireTime).diff(currentTime)); 94 | const nextFireTime: Date = 95 | durationUntilFire > this.maximumTimerDuration 96 | ? moment(currentTime).add(this.longRunningTimerIntervalDuration).toDate() 97 | : finalFireTime; 98 | return new DFTimerTask(false, new CreateTimerAction(nextFireTime)); 99 | } 100 | 101 | private addNewChild(childTimer: DFTimerTask): void { 102 | childTimer.parent = this; 103 | this.children.push(childTimer); 104 | this.executor.trackOpenTask(childTimer); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/task/NoOpTask.ts: -------------------------------------------------------------------------------- 1 | import { TaskBase } from "./TaskBase"; 2 | 3 | /** 4 | * @hidden 5 | * 6 | * A task created only to facilitate replay, it should not communicate any 7 | * actions to the DF extension. 8 | * 9 | * We internally track these kinds of tasks to reason over the completion of 10 | * DF APIs that decompose into smaller DF APIs that the user didn't explicitly 11 | * schedule. 12 | */ 13 | export class NoOpTask extends TaskBase { 14 | constructor() { 15 | super(false, "noOp"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/task/RegisteredActivityTask.ts: -------------------------------------------------------------------------------- 1 | import * as types from "durable-functions"; 2 | import { AtomicTask } from "./AtomicTask"; 3 | import { RetryOptions } from "../RetryOptions"; 4 | import { CallActivityAction } from "../actions/CallActivityAction"; 5 | import { CallActivityWithRetryAction } from "../actions/CallActivityWithRetryAction"; 6 | import { RetryableTask } from "./RetryableTask"; 7 | 8 | export class RegisteredActivityTask extends AtomicTask implements types.RegisteredActivityTask { 9 | withRetry: (retryOptions: RetryOptions) => RetryableTask; 10 | 11 | constructor(activityName: string, input?: unknown) { 12 | super(false, new CallActivityAction(activityName, input)); 13 | 14 | this.withRetry = (retryOptions: RetryOptions): RetryableTask => { 15 | if (this.alreadyScheduled) { 16 | throw new Error( 17 | "Invalid use of `.withRetry`: attempted to create a retriable task from an already scheduled task. " + 18 | `A task with ID ${this.id} to call activity ${activityName} has already been scheduled. ` + 19 | "Make sure to only invoke `.withRetry` on tasks that have not previously been yielded." 20 | ); 21 | } 22 | 23 | const callActivityWithRetryAction = new CallActivityWithRetryAction( 24 | activityName, 25 | retryOptions, 26 | input 27 | ); 28 | const backingTask = new AtomicTask(false, callActivityWithRetryAction); 29 | return new RetryableTask(backingTask, retryOptions); 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/task/RegisteredOrchestrationTask.ts: -------------------------------------------------------------------------------- 1 | import * as types from "durable-functions"; 2 | import { CallSubOrchestratorAction } from "../actions/CallSubOrchestratorAction"; 3 | import { CallSubOrchestratorWithRetryAction } from "../actions/CallSubOrchestratorWithRetryAction"; 4 | import { AtomicTask } from "./AtomicTask"; 5 | import { RetryableTask } from "./RetryableTask"; 6 | import { RetryOptions } from "../RetryOptions"; 7 | 8 | export class RegisteredOrchestrationTask extends AtomicTask 9 | implements types.RegisteredOrchestrationTask { 10 | withRetry: (retryOptions: RetryOptions) => RetryableTask; 11 | 12 | constructor(orchestrationName: string, input?: unknown, instanceId?: string) { 13 | super(false, new CallSubOrchestratorAction(orchestrationName, instanceId, input)); 14 | 15 | this.withRetry = (retryOptions: RetryOptions): RetryableTask => { 16 | if (this.alreadyScheduled) { 17 | throw new Error( 18 | "Invalid use of `.withRetry`: attempted to create a retriable task from an already scheduled task. " + 19 | `A task with ID ${this.id} to call subOrchestrator ${orchestrationName} has already been scheduled. ` + 20 | "Make sure to only invoke `.withRetry` on tasks that have not previously been yielded." 21 | ); 22 | } 23 | 24 | const callSubOrchestratorWithRetryAction = new CallSubOrchestratorWithRetryAction( 25 | orchestrationName, 26 | retryOptions, 27 | input, 28 | instanceId 29 | ); 30 | const backingTask = new AtomicTask(false, callSubOrchestratorWithRetryAction); 31 | return new RetryableTask(backingTask, retryOptions); 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/task/RetryableTask.ts: -------------------------------------------------------------------------------- 1 | import { RetryOptions } from "durable-functions"; 2 | import { TaskOrchestrationExecutor } from "../orchestrations/TaskOrchestrationExecutor"; 3 | import { TaskState } from "."; 4 | import { DFTask } from "./DFTask"; 5 | import { NoOpTask } from "./NoOpTask"; 6 | import { TaskBase } from "./TaskBase"; 7 | import { WhenAllTask } from "./WhenAllTask"; 8 | import { DurableError } from "../error/DurableError"; 9 | 10 | /** 11 | * @hidden 12 | * 13 | * A `-WithRetry` Task. 14 | * It is modeled after a `WhenAllTask` because it decomposes 15 | * into several sub-tasks (a growing sequence of timers and atomic tasks) 16 | * that all need to complete before this task reaches an end-value. 17 | */ 18 | export class RetryableTask extends WhenAllTask { 19 | private isWaitingOnTimer: boolean; 20 | private attemptNumber: number; 21 | private error: any; 22 | 23 | /** 24 | * @hidden 25 | * Construct a retriable task. 26 | * 27 | * @param innerTask 28 | * The task representing the work to retry 29 | * @param retryOptions 30 | * The retrying settings 31 | * @param executor 32 | * The taskOrchestrationExecutor managing the replay, 33 | * we use to to scheduling new tasks (timers and retries) 34 | */ 35 | constructor(public innerTask: DFTask, private retryOptions: RetryOptions) { 36 | super([innerTask], innerTask.actionObj); 37 | this.attemptNumber = 1; 38 | this.isWaitingOnTimer = false; 39 | } 40 | 41 | /** 42 | * @hidden 43 | * Attempts to set a value to this task, given a completed sub-task 44 | * 45 | * @param child 46 | * The sub-task that just completed 47 | */ 48 | public trySetValue(child: TaskBase, executor?: TaskOrchestrationExecutor): void { 49 | if (!executor) { 50 | throw new DurableError( 51 | "A framework-internal error was detected: " + 52 | "No executor passed to RetryableTask.trySetValue. " + 53 | "A TaskOrchestrationExecutor is required to schedule new tasks. " + 54 | "If this issue persists, please report it here: " + 55 | "https://github.com/Azure/azure-functions-durable-js/issues" 56 | ); 57 | } 58 | 59 | // Case 1 - child is a timer task 60 | if (this.isWaitingOnTimer) { 61 | this.isWaitingOnTimer = false; 62 | 63 | // If we're out of retry attempts, we can set the output value 64 | // of this task to be that of the last error we encountered 65 | if (this.attemptNumber > this.retryOptions.maxNumberOfAttempts) { 66 | this.setValue(true, this.error); 67 | } else { 68 | // If we still have more attempts available, we re-schedule the 69 | // original task. Since these sub-tasks are not user-managed, 70 | // they are declared as internal tasks. 71 | const rescheduledTask = new NoOpTask(); 72 | rescheduledTask.parent = this; 73 | this.children.push(rescheduledTask); 74 | executor.trackOpenTask(rescheduledTask); 75 | } 76 | } // Case 2 - child is the API to retry, and it succeeded 77 | else if (child.stateObj === TaskState.Completed) { 78 | // If we have a successful non-timer task, we accept its result 79 | this.setValue(false, child.result); 80 | } // Case 3 - child is the API to retry, and it failed 81 | else { 82 | // If the sub-task failed, schedule timer to retry again. 83 | // Since these sub-tasks are not user-managed, they are declared as internal tasks. 84 | const rescheduledTask = new NoOpTask(); 85 | rescheduledTask.parent = this; 86 | this.children.push(rescheduledTask); 87 | executor.trackOpenTask(rescheduledTask); 88 | this.isWaitingOnTimer = true; 89 | this.error = child.result; 90 | this.attemptNumber++; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/task/TaskBase.ts: -------------------------------------------------------------------------------- 1 | import { TaskOrchestrationExecutor } from "src/orchestrations/TaskOrchestrationExecutor"; 2 | import { BackingAction, TaskID, TaskState } from "."; 3 | import { CompoundTask } from "./CompoundTask"; 4 | 5 | /** 6 | * @hidden 7 | * Base class for all Tasks, defines the basic state transitions for all tasks. 8 | */ 9 | export abstract class TaskBase { 10 | public state: TaskState; 11 | public parent: CompoundTask | undefined; 12 | public isPlayed: boolean; 13 | public result: unknown; 14 | 15 | /** 16 | * @hidden 17 | * 18 | * Construct a task. 19 | * @param id 20 | * The task's ID 21 | * @param action 22 | * The task's backing action 23 | */ 24 | constructor(public id: TaskID, protected action: BackingAction) { 25 | this.state = TaskState.Running; 26 | } 27 | 28 | /** Get this task's backing action */ 29 | get actionObj(): BackingAction { 30 | return this.action; 31 | } 32 | 33 | /** Get this task's current state */ 34 | get stateObj(): TaskState { 35 | return this.state; 36 | } 37 | 38 | /** Whether this task is not in the Running state */ 39 | get hasResult(): boolean { 40 | return this.state !== TaskState.Running; 41 | } 42 | 43 | get isFaulted(): boolean { 44 | return this.state === TaskState.Failed; 45 | } 46 | 47 | get isCompleted(): boolean { 48 | return this.state === TaskState.Completed; 49 | } 50 | 51 | /** Change this task from the Running state to a completed state */ 52 | private changeState(state: TaskState): void { 53 | if (state === TaskState.Running) { 54 | throw Error("Cannot change Task to the RUNNING state."); 55 | } 56 | this.state = state; 57 | } 58 | 59 | /** Attempt to set a result for this task, and notifies parents, if any */ 60 | public setValue(isError: boolean, value: unknown, executor?: TaskOrchestrationExecutor): void { 61 | let newState: TaskState; 62 | 63 | if (isError) { 64 | if (!(value instanceof Error)) { 65 | const errMessage = `Task ID ${this.id} failed but it's value was not an Exception`; 66 | throw new Error(errMessage); 67 | } 68 | newState = TaskState.Failed; 69 | } else { 70 | newState = TaskState.Completed; 71 | } 72 | 73 | this.changeState(newState); 74 | this.result = value; 75 | this.propagate(executor); 76 | } 77 | 78 | /** 79 | * @hidden 80 | * Notifies this task's parents about its state change. 81 | */ 82 | private propagate(executor?: TaskOrchestrationExecutor): void { 83 | const hasCompleted = this.state !== TaskState.Running; 84 | if (hasCompleted && this.parent !== undefined) { 85 | this.parent.handleCompletion(this, executor); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/task/WhenAllTask.ts: -------------------------------------------------------------------------------- 1 | import { CompoundTask } from "./CompoundTask"; 2 | import { TaskBase } from "./TaskBase"; 3 | import { AtomicTask } from "./AtomicTask"; 4 | import { TaskState } from "."; 5 | import { IAction } from "../actions/IAction"; 6 | 7 | /** 8 | * @hidden 9 | * 10 | * A WhenAll task. 11 | */ 12 | export class WhenAllTask extends CompoundTask { 13 | /** 14 | * @hidden 15 | * Construct a WhenAll task. 16 | * 17 | * @param children 18 | * Sub-tasks to wait on. 19 | * @param action 20 | * A the backing action representing this task. 21 | */ 22 | constructor(public children: TaskBase[], protected action: IAction) { 23 | super(children, action); 24 | } 25 | 26 | /** 27 | * @hidden 28 | * Attempts to set a value to this task, given a completed sub-task 29 | * 30 | * @param child 31 | * The sub-task that just completed 32 | */ 33 | public trySetValue(child: AtomicTask): void { 34 | if (child.stateObj === TaskState.Completed) { 35 | // We set the result only after all sub-tasks have completed 36 | if (this.children.every((c) => c.stateObj === TaskState.Completed)) { 37 | // The result is a list of all sub-task's results 38 | const results = this.children.map((c) => c.result); 39 | this.setValue(false, results); 40 | } 41 | } else { 42 | // If any task failed, we fail the entire compound task 43 | if (this.firstError === undefined) { 44 | this.firstError = child.result as Error; 45 | this.setValue(true, this.firstError); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/task/WhenAnyTask.ts: -------------------------------------------------------------------------------- 1 | import { TaskState } from "."; 2 | import { CompoundTask } from "./CompoundTask"; 3 | import { TaskBase } from "./TaskBase"; 4 | 5 | /** 6 | * @hidden 7 | * 8 | * A WhenAny task. 9 | */ 10 | export class WhenAnyTask extends CompoundTask { 11 | /** 12 | * @hidden 13 | * Attempts to set a value to this task, given a completed sub-task 14 | * 15 | * @param child 16 | * The sub-task that just completed 17 | */ 18 | public trySetValue(child: TaskBase): void { 19 | // For a Task to have isError = true, it needs to contain within an Exception/Error datatype. 20 | // However, WhenAny only contains Tasks as its result, so technically it "never errors out". 21 | // The isError flag is used simply to determine if the result of the task should be fed in 22 | // as a value, or as a raised exception to the generator code. For WhenAny, we always feed 23 | // in the result as a value. 24 | if (this.state === TaskState.Running) { 25 | this.setValue(false, child); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/task/index.ts: -------------------------------------------------------------------------------- 1 | import { IAction } from "../actions/IAction"; 2 | 3 | /** 4 | * @hidden 5 | * The states a task can be in 6 | */ 7 | export enum TaskState { 8 | Running, 9 | Failed, 10 | Completed, 11 | } 12 | 13 | /** 14 | * @hidden 15 | * A taskID, either a `string` for external events, 16 | * or either `false` or a `number` for un-awaited 17 | * and awaited tasks respectively. 18 | */ 19 | export type TaskID = number | string | false; 20 | 21 | /** 22 | * @hidden 23 | * A backing action, either a proper action or "noOp" for an internal-only task 24 | */ 25 | export type BackingAction = IAction | "noOp"; 26 | 27 | export * from "./TaskBase"; 28 | export * from "./DFTask"; 29 | export * from "./CompoundTask"; 30 | export * from "./AtomicTask"; 31 | export * from "./NoOpTask"; 32 | export * from "./DFTimerTask"; 33 | export * from "./CallHttpWithPollingTask"; 34 | export * from "./LongTimerTask"; 35 | export * from "./RetryableTask"; 36 | export * from "./WhenAllTask"; 37 | export * from "./WhenAnyTask"; 38 | -------------------------------------------------------------------------------- /src/trigger.ts: -------------------------------------------------------------------------------- 1 | import { ActivityTrigger, EntityTrigger, OrchestrationTrigger } from "durable-functions"; 2 | import { trigger as azFuncTrigger } from "@azure/functions"; 3 | 4 | export function activity(): ActivityTrigger { 5 | return azFuncTrigger.generic({ 6 | type: "activityTrigger", 7 | }) as ActivityTrigger; 8 | } 9 | 10 | export function orchestration(): OrchestrationTrigger { 11 | return azFuncTrigger.generic({ 12 | type: "orchestrationTrigger", 13 | }) as OrchestrationTrigger; 14 | } 15 | 16 | export function entity(): EntityTrigger { 17 | return azFuncTrigger.generic({ 18 | type: "entityTrigger", 19 | }) as EntityTrigger; 20 | } 21 | -------------------------------------------------------------------------------- /src/util/GuidManager.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | /** @hidden */ 3 | import { v5 as uuidv5 } from "uuid"; 4 | import { Utils } from "./Utils"; 5 | 6 | /** @hidden */ 7 | export class GuidManager { 8 | // I don't anticipate these changing often. 9 | public static DnsNamespaceValue = "9e952958-5e33-4daf-827f-2fa12937b875"; 10 | public static UrlNamespaceValue = "9e952958-5e33-4daf-827f-2fa12937b875"; 11 | public static IsoOidNamespaceValue = "9e952958-5e33-4daf-827f-2fa12937b875"; 12 | 13 | public static createDeterministicGuid(namespaceValue: string, name: string): string { 14 | return this.createDeterministicGuidCore(namespaceValue, name); 15 | } 16 | 17 | private static createDeterministicGuidCore(namespaceValue: string, name: string): string { 18 | Utils.throwIfEmpty(namespaceValue, "namespaceValue"); 19 | Utils.throwIfEmpty(name, "name"); 20 | 21 | const hash = crypto.createHash("sha1"); 22 | hash.update(name); 23 | const bytes: number[] = Array.prototype.slice.call(hash.digest(), 0, 16); 24 | 25 | return uuidv5(namespaceValue, bytes); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/util/Utils.ts: -------------------------------------------------------------------------------- 1 | /** @hidden */ 2 | export class Utils { 3 | public static processInput(input: string | T): string | T { 4 | // If we fail to stringify inputs, they may get deserialized incorrectly. 5 | // For instance: "13131" might get interpreted as a number. 6 | // Somehow this doesn't appear to occur with other datatypes, but we should 7 | // investigate that further. 8 | if (typeof input === "string") { 9 | input = JSON.stringify(input); 10 | } 11 | return input; 12 | } 13 | public static getInstancesOf( 14 | collection: { [index: string]: unknown }, 15 | abstractClass: T // this should be an abstract class that contains only the _required_ properties 16 | ): T[] { 17 | if (collection && abstractClass) { 18 | const candidateObjects = Object.values(collection).filter((value) => 19 | this.hasAllPropertiesOf(value, abstractClass) 20 | ); 21 | 22 | this.parseTimestampsAsDates(candidateObjects); 23 | 24 | return candidateObjects as T[]; 25 | } 26 | return []; 27 | } 28 | 29 | public static getHrMilliseconds(times: number[]): number { 30 | return times[0] * 1000 + times[1] / 1e6; 31 | } 32 | 33 | public static hasStringProperty( 34 | obj: X, 35 | prop: Y 36 | ): obj is X & Record { 37 | if (Utils.hasOwnProperty(obj, prop)) { 38 | const propKey = prop as keyof typeof obj; 39 | const property = obj[propKey]; 40 | const propertyIsString = typeof property === "string"; 41 | return propertyIsString; 42 | } 43 | return false; 44 | } 45 | 46 | public static hasOwnProperty( 47 | obj: X, 48 | prop: Y 49 | ): obj is X & Record { 50 | // informs TS that an object has a property 51 | return obj.hasOwnProperty(prop); 52 | } 53 | 54 | public static parseTimestampsAsDates(obj: unknown): void { 55 | if (typeof obj === "object" && obj != null) { 56 | if (this.hasOwnProperty(obj, "Timestamp") && typeof obj.Timestamp === "string") { 57 | obj.Timestamp = new Date(obj.Timestamp); 58 | } 59 | Object.values(obj).map((value) => { 60 | // This recursive step ensures _all_ Timestamp properties are converted properly 61 | // For example, a payload can contain the history as a property, so if we want to 62 | // parse each HistoryEvent's Timestamp, we need to traverse the payload recursively 63 | this.parseTimestampsAsDates(value); 64 | }); 65 | } 66 | } 67 | 68 | public static hasAllPropertiesOf(obj: unknown, refInstance: T): boolean { 69 | return ( 70 | typeof refInstance === "object" && 71 | typeof obj === "object" && 72 | obj !== null && 73 | Object.keys(refInstance).every((key: string) => { 74 | return obj.hasOwnProperty(key); 75 | }) 76 | ); 77 | } 78 | 79 | public static ensureNonNull(argument: T | undefined, message: string): T { 80 | if (argument === undefined) { 81 | throw new TypeError(message); 82 | } 83 | 84 | return argument; 85 | } 86 | 87 | public static async sleep(delayInMilliseconds: number): Promise { 88 | await new Promise((resolve) => setTimeout(resolve, delayInMilliseconds)); 89 | } 90 | 91 | public static throwIfNotInstanceOf( 92 | value: unknown, 93 | name: string, 94 | refInstance: T, 95 | type: string 96 | ): void { 97 | if (!this.hasAllPropertiesOf(value, refInstance)) { 98 | throw new TypeError( 99 | `${name}: Expected object of type ${type} but got ${typeof value}; are you missing properties?` 100 | ); 101 | } 102 | } 103 | 104 | public static throwIfEmpty(value: unknown, name: string): void { 105 | if (typeof value !== "string") { 106 | throw new TypeError( 107 | `${name}: Expected non-empty, non-whitespace string but got ${typeof value}` 108 | ); 109 | } else if (value.trim().length < 1) { 110 | throw new Error( 111 | `${name}: Expected non-empty, non-whitespace string but got empty string` 112 | ); 113 | } 114 | } 115 | 116 | public static throwIfNotNumber(value: unknown, name: string): void { 117 | if (typeof value !== "number") { 118 | throw new TypeError(`${name}: Expected number but got ${typeof value}`); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/util/WebhookUtils.ts: -------------------------------------------------------------------------------- 1 | /** @hidden */ 2 | export class WebhookUtils { 3 | public static getReadEntityUrl( 4 | baseUrl: string, 5 | requiredQueryStrings: string, 6 | entityName: string, 7 | entityKey: string, 8 | taskHubName?: string, 9 | connectionName?: string 10 | ): string { 11 | let requestUrl = baseUrl + "/entities/" + entityName + "/" + entityKey + "?"; 12 | 13 | const queryStrings: string[] = []; 14 | if (taskHubName) { 15 | queryStrings.push("taskHub=" + taskHubName); 16 | } 17 | 18 | if (connectionName) { 19 | queryStrings.push("connection=" + connectionName); 20 | } 21 | 22 | queryStrings.push(requiredQueryStrings); 23 | requestUrl += queryStrings.join("&"); 24 | return requestUrl; 25 | } 26 | 27 | public static getSignalEntityUrl( 28 | baseUrl: string, 29 | requiredQueryStrings: string, 30 | entityName: string, 31 | entityKey: string, 32 | operationName?: string, 33 | taskHubName?: string, 34 | connectionName?: string 35 | ): string { 36 | let requestUrl = baseUrl + "/entities/" + entityName + "/" + entityKey + "?"; 37 | 38 | const queryStrings: string[] = []; 39 | if (operationName) { 40 | queryStrings.push("op=" + operationName); 41 | } 42 | 43 | if (taskHubName) { 44 | queryStrings.push("taskHub=" + taskHubName); 45 | } 46 | 47 | if (connectionName) { 48 | queryStrings.push("connection=" + connectionName); 49 | } 50 | 51 | queryStrings.push(requiredQueryStrings); 52 | requestUrl += queryStrings.join("&"); 53 | return requestUrl; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/integration/entity-spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import "mocha"; 3 | import { DummyEntityContext } from "../../src/util/testingUtils"; 4 | import { TestEntities } from "../testobjects/testentities"; 5 | import { TestEntityBatches } from "../testobjects/testentitybatches"; 6 | import { StringStoreOperation } from "../testobjects/testentityoperations"; 7 | import { EntityState } from "../../src/entities/EntityState"; 8 | 9 | describe("Entity", () => { 10 | it("StringStore entity with no initial state.", async () => { 11 | const entity = TestEntities.StringStore; 12 | const operations: StringStoreOperation[] = []; 13 | operations.push({ kind: "set", value: "hello" }); 14 | operations.push({ kind: "get" }); 15 | operations.push({ kind: "set", value: "hello world" }); 16 | operations.push({ kind: "get" }); 17 | const testData = TestEntityBatches.GetStringStoreBatch(operations, undefined); 18 | const mockContext = new DummyEntityContext(); 19 | const result = await entity(testData.input, mockContext); 20 | expect(result).to.not.be.undefined; 21 | if (result) { 22 | entityStateMatchesExpected(result, testData.output); 23 | } 24 | }); 25 | it("StringStore entity with initial state.", async () => { 26 | const entity = TestEntities.StringStore; 27 | const operations: StringStoreOperation[] = []; 28 | operations.push({ kind: "get" }); 29 | const testData = TestEntityBatches.GetStringStoreBatch(operations, "Hello world"); 30 | const mockContext = new DummyEntityContext(); 31 | const result = await entity(testData.input, mockContext); 32 | expect(result).to.not.be.undefined; 33 | if (result) { 34 | entityStateMatchesExpected(result, testData.output); 35 | } 36 | }); 37 | it("AsyncStringStore entity with no initial state.", async () => { 38 | const entity = TestEntities.AsyncStringStore; 39 | const operations: StringStoreOperation[] = []; 40 | operations.push({ kind: "set", value: "set 1" }); 41 | operations.push({ kind: "get" }); 42 | operations.push({ kind: "set", value: "set 2" }); 43 | operations.push({ kind: "get" }); 44 | const testData = TestEntityBatches.GetAsyncStringStoreBatch(operations, undefined); 45 | const mockContext = new DummyEntityContext(); 46 | const result = await entity(testData.input, mockContext); 47 | expect(result).to.not.be.undefined; 48 | if (result) { 49 | entityStateMatchesExpected(result, testData.output); 50 | } 51 | }); 52 | }); 53 | 54 | // Have to compare on an element by element basis as elapsed time is not deterministic. 55 | function entityStateMatchesExpected(actual: EntityState, expected: EntityState): void { 56 | expect(actual.entityExists).to.be.equal(expected.entityExists); 57 | expect(actual.entityState).to.be.deep.equal(expected.entityState); 58 | expect(actual.signals).to.be.deep.equal(expected.signals); 59 | for (let i = 0; i < actual.results.length; i++) { 60 | expect(actual.results[i].isError).to.be.equal(expected.results[i].isError); 61 | expect(actual.results[i].result).to.be.deep.equal(expected.results[i].result); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/test-app/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | __azurite_db*__.json 6 | __blobstorage__ 7 | __queuestorage__ 8 | local.settings.json 9 | test 10 | tsconfig.json -------------------------------------------------------------------------------- /test/test-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | .env.test 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # DynamoDB Local files 84 | .dynamodb/ 85 | 86 | # TypeScript output 87 | dist 88 | out 89 | 90 | # Azure Functions artifacts 91 | bin 92 | obj 93 | appsettings.json 94 | local.settings.json 95 | 96 | # Azurite artifacts 97 | __blobstorage__ 98 | __queuestorage__ 99 | __azurite_db*__.json -------------------------------------------------------------------------------- /test/test-app/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions" 4 | ] 5 | } -------------------------------------------------------------------------------- /test/test-app/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to Node Functions", 6 | "type": "node", 7 | "request": "attach", 8 | "port": 9229, 9 | "preLaunchTask": "func: host start" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /test/test-app/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": ".", 3 | "azureFunctions.postDeployTask": "npm install (functions)", 4 | "azureFunctions.projectLanguage": "TypeScript", 5 | "azureFunctions.projectRuntime": "~4", 6 | "debug.internalConsoleOptions": "neverOpen", 7 | "azureFunctions.projectLanguageModel": 4, 8 | "azureFunctions.preDeployTask": "npm prune (functions)" 9 | } -------------------------------------------------------------------------------- /test/test-app/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "func", 6 | "label": "func: host start", 7 | "command": "host start", 8 | "problemMatcher": "$func-node-watch", 9 | "isBackground": true, 10 | "dependsOn": "npm build (functions)" 11 | }, 12 | { 13 | "type": "shell", 14 | "label": "npm build (functions)", 15 | "command": "npm run build", 16 | "dependsOn": "npm install (functions)", 17 | "problemMatcher": "$tsc" 18 | }, 19 | { 20 | "type": "shell", 21 | "label": "npm install (functions)", 22 | "command": "npm install" 23 | }, 24 | { 25 | "type": "shell", 26 | "label": "npm prune (functions)", 27 | "command": "npm prune --production", 28 | "dependsOn": "npm build (functions)", 29 | "problemMatcher": [] 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /test/test-app/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[3.15.0, 4.0.0)" 14 | }, 15 | "extensions": { 16 | "durableTask": { 17 | "storageProvider": { 18 | "type": "AzureStorage" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/test-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build": "tsc", 7 | "watch": "tsc -w", 8 | "prestart": "npm run build", 9 | "start": "func start", 10 | "test": "echo \"No tests yet...\"" 11 | }, 12 | "dependencies": { 13 | "@azure/functions": "^4.0.0-alpha.7", 14 | "durable-functions": "^3.0.0-alpha.2" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^18.x", 18 | "typescript": "^4.0.0" 19 | }, 20 | "main": "dist/src/functions/*.js" 21 | } 22 | -------------------------------------------------------------------------------- /test/test-app/src/functions/counter1.ts: -------------------------------------------------------------------------------- 1 | import { app, HttpHandler, HttpRequest, HttpResponse, InvocationContext } from "@azure/functions"; 2 | import * as df from "durable-functions"; 3 | import { EntityContext, EntityHandler } from "durable-functions"; 4 | 5 | const entityName = "counter1"; 6 | 7 | const counter1: EntityHandler = (context: EntityContext) => { 8 | const currentValue: number = context.df.getState(() => 0); 9 | switch (context.df.operationName) { 10 | case "add": 11 | const amount: number = context.df.getInput(); 12 | context.df.setState(currentValue + amount); 13 | break; 14 | case "reset": 15 | context.df.setState(0); 16 | break; 17 | case "get": 18 | context.df.return(currentValue); 19 | break; 20 | } 21 | }; 22 | df.app.entity(entityName, counter1); 23 | 24 | const counter1HttpStart: HttpHandler = async ( 25 | req: HttpRequest, 26 | context: InvocationContext 27 | ): Promise => { 28 | const id: string = req.params.id; 29 | const entityId = new df.EntityId(entityName, id); 30 | const client = df.getClient(context); 31 | 32 | if (req.method === "POST") { 33 | // increment value 34 | await client.signalEntity(entityId, "add", 1); 35 | } else { 36 | // read current state of entity 37 | const stateResponse = await client.readEntityState(entityId); 38 | return new HttpResponse({ 39 | jsonBody: stateResponse.entityState, 40 | }); 41 | } 42 | }; 43 | app.http("counter1HttpStart", { 44 | route: `${entityName}/{id}`, 45 | extraInputs: [df.input.durableClient()], 46 | handler: counter1HttpStart, 47 | }); 48 | -------------------------------------------------------------------------------- /test/test-app/src/functions/hello.ts: -------------------------------------------------------------------------------- 1 | import { app, HttpHandler, HttpRequest, HttpResponse, InvocationContext } from "@azure/functions"; 2 | import * as df from "durable-functions"; 3 | import { ActivityHandler, OrchestrationContext, OrchestrationHandler } from "durable-functions"; 4 | 5 | const activityName = "hello"; 6 | 7 | const helloOrchestrator: OrchestrationHandler = function* (context: OrchestrationContext) { 8 | const outputs = []; 9 | outputs.push(yield context.df.callActivity(activityName, "Tokyo")); 10 | outputs.push(yield context.df.callActivity(activityName, "Seattle")); 11 | outputs.push(yield context.df.callActivity(activityName, "Cairo")); 12 | 13 | return outputs; 14 | }; 15 | df.app.orchestration("helloOrchestrator", helloOrchestrator); 16 | 17 | const hello: ActivityHandler = (input: string): string => { 18 | return `Hello, ${input}`; 19 | }; 20 | df.app.activity(activityName, { handler: hello }); 21 | 22 | const helloHttpStart: HttpHandler = async ( 23 | request: HttpRequest, 24 | context: InvocationContext 25 | ): Promise => { 26 | const client = df.getClient(context); 27 | const body: unknown = await request.text(); 28 | const instanceId: string = await client.startNew(request.params.orchestratorName, { 29 | input: body, 30 | }); 31 | 32 | context.log(`Started orchestration with ID = '${instanceId}'.`); 33 | 34 | return client.createCheckStatusResponse(request, instanceId); 35 | }; 36 | 37 | app.http("helloHttpStart", { 38 | route: "orchestrators/{orchestratorName}", 39 | extraInputs: [df.input.durableClient()], 40 | handler: helloHttpStart, 41 | }); 42 | -------------------------------------------------------------------------------- /test/test-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "dist", 6 | "rootDir": ".", 7 | "sourceMap": true, 8 | "strict": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/testobjects/testconstants.ts: -------------------------------------------------------------------------------- 1 | export class TestConstants { 2 | public static readonly connectionPlaceholder: string = "CONNECTION"; 3 | public static readonly taskHubPlaceholder: string = "TASK-HUB"; 4 | public static readonly hostPlaceholder: string = "HOST-PLACEHOLDER"; 5 | 6 | public static readonly testCode: string = "code=base64string"; 7 | 8 | public static readonly entityNamePlaceholder: string = "{entityName}"; 9 | public static readonly entityKeyPlaceholder: string = "{entityKey?}"; 10 | public static readonly eventNamePlaceholder: string = "{eventName}"; 11 | public static readonly functionPlaceholder: string = "{functionName}"; 12 | public static readonly idPlaceholder: string = "[/{instanceId}]"; 13 | public static readonly intervalPlaceholder: string = "{intervalInSeconds}"; 14 | public static readonly operationPlaceholder: string = "{operation}"; 15 | public static readonly reasonPlaceholder: string = "{text}"; 16 | public static readonly timeoutPlaceholder: string = "{timeoutInSeconds}"; 17 | 18 | public static readonly uriSuffix: string = `taskHub=${TestConstants.taskHubPlaceholder}&connection=${TestConstants.connectionPlaceholder}&${TestConstants.testCode}`; // tslint:disable-line max-line-length 19 | public static readonly webhookPath: string = "/runtime/webhooks/durabletask/"; 20 | 21 | public static readonly statusQueryGetUriTemplate: string = `${TestConstants.hostPlaceholder}${TestConstants.webhookPath}instances/${TestConstants.idPlaceholder}?${TestConstants.uriSuffix}`; // tslint:disable-line max-line-length 22 | public static readonly sendEventPostUriTemplate: string = `${TestConstants.hostPlaceholder}${TestConstants.webhookPath}instances/${TestConstants.idPlaceholder}/raiseEvent/${TestConstants.eventNamePlaceholder}?${TestConstants.uriSuffix}`; // tslint:disable-line max-line-length 23 | public static readonly terminatePostUriTemplate: string = `${TestConstants.hostPlaceholder}${TestConstants.webhookPath}instances/${TestConstants.idPlaceholder}/terminate?reason=${TestConstants.reasonPlaceholder}&${TestConstants.uriSuffix}`; // tslint:disable-line max-line-length 24 | public static readonly rewindPostUriTemplate: string = `${TestConstants.hostPlaceholder}${TestConstants.webhookPath}instances/${TestConstants.idPlaceholder}/rewind?reason=${TestConstants.reasonPlaceholder}&${TestConstants.uriSuffix}`; // tslint:disable-line max-line-length 25 | public static readonly purgeDeleteUriTemplate: string = `${TestConstants.hostPlaceholder}${TestConstants.webhookPath}instances/${TestConstants.idPlaceholder}?${TestConstants.uriSuffix}`; // tslint:disable-line max-line-length 26 | public static readonly suspendPostUriTemplate: string = `${TestConstants.hostPlaceholder}${TestConstants.webhookPath}instances/${TestConstants.idPlaceholder}/suspend?reason=${TestConstants.reasonPlaceholder}&${TestConstants.uriSuffix}`; // tslint:disable-line max-line-length 27 | public static readonly resumePostUriTemplate: string = `${TestConstants.hostPlaceholder}${TestConstants.webhookPath}instances/${TestConstants.idPlaceholder}/resume?reason=${TestConstants.reasonPlaceholder}&${TestConstants.uriSuffix}`; // tslint:disable-line max-line-length 28 | 29 | public static readonly createPostUriTemplate: string = `${TestConstants.hostPlaceholder}${TestConstants.webhookPath}orchestrators/${TestConstants.functionPlaceholder}${TestConstants.idPlaceholder}?${TestConstants.testCode}`; // tslint:disable-line max-line-length 30 | public static readonly waitOnPostUriTemplate: string = `${TestConstants.hostPlaceholder}${TestConstants.webhookPath}orchestrators/${TestConstants.functionPlaceholder}${TestConstants.idPlaceholder}?timeout=${TestConstants.timeoutPlaceholder}&pollingInterval=${TestConstants.intervalPlaceholder}&${TestConstants.testCode}`; // tslint:disable-line max-line-length 31 | 32 | public static readonly entityGetUriTemplate: string = `${TestConstants.hostPlaceholder}${TestConstants.webhookPath}entities/${TestConstants.entityNamePlaceholder}/${TestConstants.entityKeyPlaceholder}&${TestConstants.uriSuffix}`; // tslint:disable-line max-line-length 33 | public static readonly entityPostUriTemplate: string = `${TestConstants.hostPlaceholder}${TestConstants.webhookPath}entities/${TestConstants.entityNamePlaceholder}/${TestConstants.entityKeyPlaceholder}&op=${TestConstants.operationPlaceholder}&${TestConstants.uriSuffix}`; // tslint:disable-line max-line-length 34 | } 35 | -------------------------------------------------------------------------------- /test/testobjects/testentities.ts: -------------------------------------------------------------------------------- 1 | import { createEntityFunction } from "../../src/util/testingUtils"; 2 | 3 | export class TestEntities { 4 | public static StringStore = createEntityFunction((context): void => { 5 | switch (context.df.operationName) { 6 | case "set": 7 | context.df.setState(context.df.getInput() || ""); 8 | break; 9 | case "get": 10 | context.df.return(context.df.getState() || ""); 11 | break; 12 | default: 13 | throw new Error("No such operation exists"); 14 | } 15 | }); 16 | 17 | public static Counter = createEntityFunction((context): void => { 18 | const input = context.df.getInput(); 19 | const state = context.df.getState(); 20 | 21 | if (input === undefined || state === undefined) { 22 | throw new Error("Input or state not set"); 23 | } 24 | 25 | switch (context.df.operationName) { 26 | case "increment": 27 | context.df.setState(input + 1); 28 | break; 29 | case "add": 30 | context.df.setState(state + input); 31 | break; 32 | case "get": 33 | context.df.return(state); 34 | break; 35 | case "set": 36 | context.df.setState(input); 37 | break; 38 | case "delete": 39 | context.df.destructOnExit(); 40 | break; 41 | default: 42 | throw Error("Invalid operation"); 43 | } 44 | }); 45 | 46 | public static AsyncStringStore = createEntityFunction(async (context) => { 47 | await new Promise((resolve) => setTimeout(() => resolve(), 0)); // force onto the event loop and result in a no-op delay 48 | switch (context.df.operationName) { 49 | case "set": 50 | context.df.setState(context.df.getInput() || ""); 51 | break; 52 | case "get": 53 | context.df.return(context.df.getState() || ""); 54 | break; 55 | default: 56 | throw new Error("No such operation exists"); 57 | } 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /test/testobjects/testentityoperations.ts: -------------------------------------------------------------------------------- 1 | import { DurableEntityBindingInfo } from "../../src/entities/DurableEntityBindingInfo"; 2 | import { EntityState } from "../../src/entities/EntityState"; 3 | 4 | export interface EntityInputsAndOutputs { 5 | input: DurableEntityBindingInfo; 6 | output: EntityState; 7 | } 8 | 9 | export interface Get { 10 | kind: "get"; 11 | } 12 | 13 | export interface Set { 14 | kind: "set"; 15 | value: T; 16 | } 17 | 18 | export interface Increment { 19 | kind: "increment"; 20 | } 21 | 22 | export interface Add { 23 | kind: "add"; 24 | value: T; 25 | } 26 | 27 | export interface Delete { 28 | kind: "delete"; 29 | } 30 | 31 | export type StringStoreOperation = Get | Set; 32 | 33 | export type CounterOperation = Get | Set | Increment | Add | Delete; 34 | -------------------------------------------------------------------------------- /test/unit/entityid-spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import "mocha"; 3 | import { EntityId } from "../../src/entities/EntityId"; 4 | 5 | describe("EntityId", () => { 6 | const defaultEntityName = "entity"; 7 | const defaultEntityKey = "123"; 8 | const defaultEntityId = new EntityId(defaultEntityName, defaultEntityKey); 9 | 10 | it("returns correct toString", () => { 11 | const expectedToString = `@${defaultEntityName}@${defaultEntityKey}`; 12 | 13 | const result = defaultEntityId.toString(); 14 | 15 | expect(result).to.equal(expectedToString); 16 | }); 17 | 18 | describe("getEntityIdFromSchedulerId", () => { 19 | it("constructs correct entity ID from scheduler ID", () => { 20 | const schedulerId = `@${defaultEntityName}@${defaultEntityKey}`; 21 | 22 | const expectedEntityId = new EntityId(defaultEntityName, defaultEntityKey); 23 | 24 | const result = EntityId.getEntityIdFromSchedulerId(schedulerId); 25 | 26 | expect(result).to.deep.equal(expectedEntityId); 27 | }); 28 | }); 29 | 30 | describe("getSchedulerIdFromEntityId", () => { 31 | it("constructs correct scheduler ID from entity ID", () => { 32 | const expectedSchedulerId = `@${defaultEntityName}@${defaultEntityKey}`; 33 | 34 | const result = EntityId.getSchedulerIdFromEntityId(defaultEntityId); 35 | 36 | expect(result).to.equal(expectedSchedulerId); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/unit/guidmanager-spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import "mocha"; 3 | import moment = require("moment"); 4 | import { GuidManager } from "../../src/util/GuidManager"; 5 | import { v1 as uuidv1 } from "uuid"; 6 | import { isUUID } from "validator"; 7 | 8 | describe("GuidManager", () => { 9 | describe("createDeterministicGuid()", async () => { 10 | it("throws if namespaceValue is empty", () => { 11 | expect(() => GuidManager.createDeterministicGuid("", "name")).to.throw( 12 | "namespaceValue: Expected non-empty, non-whitespace string but got empty string" 13 | ); 14 | }); 15 | 16 | it("throws if name is empty", () => { 17 | expect(() => GuidManager.createDeterministicGuid("namespaceValue", "")).to.throw( 18 | "name: Expected non-empty, non-whitespace string but got empty string" 19 | ); 20 | }); 21 | 22 | it("returns consistent GUID for namespace and name", () => { 23 | const namespace = GuidManager.UrlNamespaceValue; 24 | const instanceId = uuidv1(); 25 | const currentUtcDateTime = moment.utc().toDate().valueOf(); 26 | 27 | const name1 = `${instanceId}_${currentUtcDateTime}_0`; 28 | const name2 = `${instanceId}_${currentUtcDateTime}_12`; 29 | 30 | const result1a = GuidManager.createDeterministicGuid(namespace, name1); 31 | const result1b = GuidManager.createDeterministicGuid(namespace, name1); 32 | 33 | const result2a = GuidManager.createDeterministicGuid(namespace, name2); 34 | const result2b = GuidManager.createDeterministicGuid(namespace, name2); 35 | 36 | expect(isUUID(result1a, "5")).to.equal(true); 37 | expect(isUUID(result2a, "5")).to.equal(true); 38 | expect(result1a).to.equal(result1b); 39 | expect(result2a).to.equal(result2b); 40 | expect(result1a).to.not.equal(result2a); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/unit/retryoptions-spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import "mocha"; 3 | import { RetryOptions } from "../../src/RetryOptions"; 4 | 5 | describe("RetryOptions", () => { 6 | it("throws if firstRetryIntervalInMilliseconds less than or equal to zero", async () => { 7 | expect(() => { 8 | new RetryOptions(0, 1); 9 | }).to.throw("firstRetryIntervalInMilliseconds value must be greater than 0."); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/unit/shim-spec.ts: -------------------------------------------------------------------------------- 1 | import { app as AzFuncApp, FunctionInput } from "@azure/functions"; 2 | import { expect } from "chai"; 3 | import sinon = require("sinon"); 4 | import { app, input, trigger } from "../../src"; 5 | import { 6 | ActivityHandler, 7 | EntityContext, 8 | EntityHandler, 9 | OrchestrationHandler, 10 | } from "durable-functions"; 11 | 12 | describe("APIs to register functions", () => { 13 | const appStub = sinon.stub(AzFuncApp, "generic"); 14 | const defaultOrchestrationHandler: OrchestrationHandler = function* () { 15 | return "hello world"; 16 | }; 17 | const defaultEntityHandler: EntityHandler = function (context: EntityContext) { 18 | context.df.return("Hello world"); 19 | }; 20 | const defaultActivityFunction: ActivityHandler = function () { 21 | return "hello world"; 22 | }; 23 | 24 | afterEach(() => { 25 | appStub.reset(); 26 | }); 27 | 28 | describe("app.orchestration", () => { 29 | it("registers an orchestration function with handler directly", () => { 30 | const expectedFunctionName = "testFunc"; 31 | app.orchestration(expectedFunctionName, defaultOrchestrationHandler); 32 | 33 | expect(appStub.callCount).to.equal(1); 34 | expect(appStub.args[0][0]).to.equal(expectedFunctionName); 35 | expect(appStub.args[0][1].trigger.type).equal("orchestrationTrigger"); 36 | expect(appStub.args[0][1].handler).to.be.a("function"); 37 | }); 38 | 39 | it("registers an orchestration function with options object", () => { 40 | const expectedFunctionName = "testFunc"; 41 | app.orchestration(expectedFunctionName, { handler: defaultOrchestrationHandler }); 42 | 43 | expect(appStub.callCount).to.equal(1); 44 | expect(appStub.args[0][0]).to.equal(expectedFunctionName); 45 | expect(appStub.args[0][1].trigger.type).equal("orchestrationTrigger"); 46 | expect(appStub.args[0][1].handler).to.be.a("function"); 47 | }); 48 | }); 49 | 50 | describe("app.entity", () => { 51 | it("registers an entity function with handler directly", () => { 52 | const expectedFunctionName = "testFunc"; 53 | app.entity(expectedFunctionName, defaultEntityHandler); 54 | 55 | expect(appStub.callCount).to.equal(1); 56 | expect(appStub.args[0][0]).to.equal(expectedFunctionName); 57 | expect(appStub.args[0][1].trigger.type).equal("entityTrigger"); 58 | expect(appStub.args[0][1].handler).to.be.a("function"); 59 | }); 60 | 61 | it("registers an entity function with options object", () => { 62 | const expectedFunctionName = "testFunc"; 63 | app.entity(expectedFunctionName, { handler: defaultEntityHandler }); 64 | 65 | expect(appStub.callCount).to.equal(1); 66 | expect(appStub.args[0][0]).to.equal(expectedFunctionName); 67 | expect(appStub.args[0][1].trigger.type).equal("entityTrigger"); 68 | expect(appStub.args[0][1].handler).to.be.a("function"); 69 | }); 70 | }); 71 | 72 | describe("app.activity", () => { 73 | it("registers an activity function with options object", () => { 74 | const expectedFunctionName = "testFunc"; 75 | app.activity(expectedFunctionName, { handler: defaultActivityFunction }); 76 | 77 | expect(appStub.callCount).to.equal(1); 78 | expect(appStub.args[0][0]).to.equal(expectedFunctionName); 79 | expect(appStub.args[0][1].trigger.type).equal("activityTrigger"); 80 | expect(appStub.args[0][1].handler).to.be.a("function"); 81 | }); 82 | 83 | it("passes along extra options", () => { 84 | const extraInput: FunctionInput = { 85 | type: "someType", 86 | name: "someName", 87 | }; 88 | 89 | app.activity("testFunc", { 90 | handler: defaultActivityFunction, 91 | extraInputs: [extraInput], 92 | }); 93 | 94 | expect(appStub.args[0][1].extraInputs).to.deep.equal([extraInput]); 95 | }); 96 | }); 97 | 98 | describe("trigger", () => { 99 | it("returns orchestration trigger object", () => { 100 | const options = trigger.orchestration(); 101 | expect(options.type).to.equal("orchestrationTrigger"); 102 | expect(options.name).to.be.a("string"); 103 | }); 104 | 105 | it("returns entity trigger object", () => { 106 | const options = trigger.entity(); 107 | expect(options.type).to.equal("entityTrigger"); 108 | expect(options.name).to.be.a("string"); 109 | }); 110 | 111 | it("returns activity trigger object", () => { 112 | const options = trigger.activity(); 113 | expect(options.type).to.equal("activityTrigger"); 114 | expect(options.name).to.be.a("string"); 115 | }); 116 | }); 117 | 118 | describe("input", () => { 119 | it("returns a durable client input object", () => { 120 | const options = input.durableClient(); 121 | expect(options.type).to.equal("durableClient"); 122 | expect(options.name).to.be.a("string"); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /test/unit/timertask-spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import "mocha"; 3 | import { DFTimerTask } from "../../src/task"; 4 | import { CreateTimerAction } from "../../src/actions/CreateTimerAction"; 5 | 6 | describe("TimerTask", () => { 7 | it("throws cannot cancel a completed task", async () => { 8 | const isCanceled = false; 9 | const date = new Date(); 10 | const action = new CreateTimerAction(date, isCanceled); 11 | const task = new DFTimerTask(0, action); 12 | task.setValue(false, undefined); // set value to complete task 13 | 14 | expect(() => { 15 | task.cancel(); 16 | }).to.throw("Cannot cancel a completed task."); 17 | }); 18 | 19 | it("cancels an incomplete task", async () => { 20 | const isCanceled = false; 21 | const date = new Date(); 22 | const action = new CreateTimerAction(date, isCanceled); 23 | const task = new DFTimerTask(0, action); 24 | 25 | task.cancel(); 26 | expect(task.isCanceled).to.equal(true); 27 | }); 28 | 29 | it("is canceled when its action is canceled", async () => { 30 | const isCanceled = true; 31 | const date = new Date(); 32 | const action = new CreateTimerAction(date, isCanceled); 33 | const task = new DFTimerTask(0, action); 34 | 35 | expect(task.isCanceled).to.equal(true); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictNullChecks": true, 4 | "declaration": false, 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "noImplicitAny": true, 8 | "outDir": "./lib", 9 | "preserveConstEnums": true, 10 | "removeComments": false, 11 | "target": "es6", 12 | "sourceMap": true, 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "strictBindCallApply": true, 16 | "baseUrl": "./", 17 | "paths": { 18 | "durable-functions": ["types"] 19 | } 20 | }, 21 | "include": ["src/**/*", "test/**/*", "types/*"], 22 | "exclude": ["node_modules", "test/test-app"] 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.nocomments: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "removeComments": true 6 | } 7 | } -------------------------------------------------------------------------------- /types/activity.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FunctionHandler, 3 | FunctionInput, 4 | FunctionOptions, 5 | FunctionOutput, 6 | FunctionTrigger, 7 | } from "@azure/functions"; 8 | import { RetryOptions, Task } from "durable-functions"; 9 | 10 | export type ActivityHandler = FunctionHandler; 11 | 12 | export interface ActivityOptions extends Partial { 13 | handler: ActivityHandler; 14 | extraInputs?: FunctionInput[]; 15 | extraOutputs?: FunctionOutput[]; 16 | } 17 | 18 | export interface ActivityTrigger extends FunctionTrigger { 19 | type: "activityTrigger"; 20 | } 21 | 22 | export type RegisteredActivity = (input?: unknown) => RegisteredActivityTask; 23 | 24 | export interface RegisteredActivityTask extends Task { 25 | withRetry: (retryOptions: RetryOptions) => Task; 26 | } 27 | -------------------------------------------------------------------------------- /types/app.d.ts: -------------------------------------------------------------------------------- 1 | import { ActivityOptions, RegisteredActivity } from "./activity"; 2 | import { EntityHandler, EntityOptions } from "./entity"; 3 | import { 4 | OrchestrationHandler, 5 | OrchestrationOptions, 6 | RegisteredOrchestration, 7 | } from "./orchestration"; 8 | 9 | /** 10 | * Registers a generator function as a Durable Orchestrator for your Function App. 11 | * 12 | * @param functionName the name of your new durable orchestrator 13 | * @param handler the generator function that should act as an orchestrator 14 | * 15 | */ 16 | export function orchestration( 17 | functionName: string, 18 | handler: OrchestrationHandler 19 | ): RegisteredOrchestration; 20 | 21 | /** 22 | * Registers a generator function as a Durable Orchestrator for your Function App. 23 | * 24 | * @param functionName the name of your new durable orchestrator 25 | * @param options the configuration options object describing the handler for this orchestrator 26 | * 27 | */ 28 | export function orchestration( 29 | functionName: string, 30 | options: OrchestrationOptions 31 | ): RegisteredOrchestration; 32 | 33 | /** 34 | * Registers a function as a Durable Entity for your Function App. 35 | * 36 | * @param functionName the name of your new durable entity 37 | * @param handler the function that should act as an entity 38 | * 39 | */ 40 | export function entity(functionName: string, handler: EntityHandler): void; 41 | 42 | /** 43 | * Registers a function as a Durable Entity for your Function App. 44 | * 45 | * @param functionName the name of your new durable entity 46 | * @param options the configuration options object describing the handler for this entity 47 | * 48 | */ 49 | export function entity(functionName: string, options: EntityOptions): void; 50 | 51 | /** 52 | * Registers a function as an Activity Function for your Function App 53 | * 54 | * @param functionName the name of your new activity function 55 | * @param options the configuration options for this activity, specifying the handler and the inputs and outputs 56 | */ 57 | export function activity(functionName: string, options: ActivityOptions): RegisteredActivity; 58 | 59 | export * as client from "./app.client"; 60 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { InvocationContext } from "@azure/functions"; 2 | import { DurableClient } from "./durableClient"; 3 | 4 | export * from "./activity"; 5 | export * from "./durableClient"; 6 | export * from "./entity"; 7 | export * from "./orchestration"; 8 | export * from "./task"; 9 | 10 | export * as app from "./app"; 11 | export * as trigger from "./trigger"; 12 | export * as input from "./input"; 13 | 14 | /** 15 | * Returns an OrchestrationClient instance. 16 | * @param context The context object of the Azure function whose body 17 | * calls this method. 18 | * 19 | */ 20 | export function getClient(context: InvocationContext): DurableClient; 21 | 22 | /** 23 | * Token Source implementation for [Azure Managed Identities](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview). 24 | * 25 | * @example Get a list of Azure Subscriptions by calling the Azure Resource Manager HTTP API. 26 | * ```javascript 27 | * const df = require("durable-functions"); 28 | * 29 | * df.app.orchestration(function* (context) { 30 | * return yield context.df.callHttp({ 31 | * method: "GET", 32 | * url: "https://management.azure.com/subscriptions?api-version=2019-06-01", 33 | * tokenSource: df.ManagedIdentityTokenSource("https://management.core.windows.net"), 34 | * }); 35 | * }); 36 | * ``` 37 | */ 38 | export declare class ManagedIdentityTokenSource { 39 | /** 40 | * Returns a `ManagedIdentityTokenSource` object. 41 | * @param resource The Azure Active Directory resource identifier of the web API being invoked. 42 | */ 43 | constructor(resource: string); 44 | 45 | /** 46 | * The Azure Active Directory resource identifier of the web API being invoked. 47 | * For example, `https://management.core.windows.net/` or `https://graph.microsoft.com/`. 48 | */ 49 | readonly resource: string; 50 | } 51 | 52 | // Over time we will likely add more implementations 53 | export type TokenSource = ManagedIdentityTokenSource; 54 | 55 | /** 56 | * Defines retry policies that can be passed as parameters to various 57 | * operations. 58 | */ 59 | export declare class RetryOptions { 60 | /** 61 | * Creates a new instance of RetryOptions with the supplied first retry and 62 | * max attempts. 63 | * @param firstRetryIntervalInMilliseconds The first retry interval (ms). Must be greater than 0. 64 | * @param maxNumberOfAttempts The maximum number of attempts. 65 | */ 66 | constructor(firstRetryIntervalInMilliseconds: number, maxNumberOfAttempts: number); 67 | /** 68 | * The retry backoff coefficient 69 | */ 70 | backoffCoefficient: number; 71 | /** 72 | * The max retry interval (ms). 73 | */ 74 | maxRetryIntervalInMilliseconds: number; 75 | /** 76 | * The timeout for retries (ms). 77 | */ 78 | retryTimeoutInMilliseconds: number; 79 | /** 80 | * The first retry interval (ms). Must be greater than 0. 81 | */ 82 | readonly firstRetryIntervalInMilliseconds: number; 83 | /** 84 | * The maximum number of attempts. 85 | */ 86 | readonly maxNumberOfAttempts: number; 87 | } 88 | 89 | /** 90 | * A specfic error thrown when a scheduled activity or suborchestrator has failed. 91 | * This error can be checked for via `instanceof` guards to catch only exceptions thrown 92 | * by the DurableJS library. 93 | */ 94 | export declare class DurableError extends Error { 95 | /** 96 | * Constructs a `DurableError` instance. 97 | * @param message error message. 98 | */ 99 | constructor(message: string | undefined); 100 | } 101 | -------------------------------------------------------------------------------- /types/input.d.ts: -------------------------------------------------------------------------------- 1 | import { DurableClientInput } from "./durableClient"; 2 | 3 | /** 4 | * @returns a durable client input configuration object 5 | */ 6 | export function durableClient(): DurableClientInput; 7 | -------------------------------------------------------------------------------- /types/task.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A Durable Functions Task. 3 | */ 4 | export interface Task { 5 | /** 6 | * Whether the task has completed. Note that completion is not 7 | * equivalent to success. 8 | */ 9 | isCompleted: boolean; 10 | /** 11 | * Whether the task faulted in some way due to error. 12 | */ 13 | isFaulted: boolean; 14 | /** 15 | * The result of the task, if completed. Otherwise `undefined`. 16 | */ 17 | result?: unknown; 18 | } 19 | 20 | /** 21 | * Returned from [[DurableClient]].[[createTimer]] if the call is 22 | * not `yield`-ed. Represents a pending timer. See documentation on [[Task]] 23 | * for more information. 24 | * 25 | * All pending timers must be completed or canceled for an orchestration to 26 | * complete. 27 | * 28 | * @example Cancel a timer 29 | * ```javascript 30 | * // calculate expiration date 31 | * const timeoutTask = context.df.createTimer(expirationDate); 32 | * 33 | * // do some work 34 | * 35 | * if (!timeoutTask.isCompleted) { 36 | * // An orchestration won't get marked as completed until all its scheduled 37 | * // tasks have returned, or been cancelled. Therefore, it is important 38 | * // to cancel timers when they're no longer needed 39 | * timeoutTask.cancel(); 40 | * } 41 | * ``` 42 | * 43 | * @example Create a timeout 44 | * ```javascript 45 | * const now = Date.now(); 46 | * const expiration = new Date(now.valueOf()).setMinutes(now.getMinutes() + 30); 47 | * 48 | * const timeoutTask = context.df.createTimer(expirationDate); 49 | * const otherTask = context.df.callActivity("DoWork"); 50 | * 51 | * const winner = yield context.df.Task.any([timeoutTask, otherTask]); 52 | * 53 | * if (winner === otherTask) { 54 | * // do some more work 55 | * } 56 | * 57 | * if (!timeoutTask.isCompleted) { 58 | * // An orchestration won't get marked as completed until all its scheduled 59 | * // tasks have returned, or been cancelled. Therefore, it is important 60 | * // to cancel timers when they're no longer needed 61 | * timeoutTask.cancel(); 62 | * } 63 | * ``` 64 | */ 65 | export interface TimerTask extends Task { 66 | /** 67 | * @returns Whether or not the timer has been canceled. 68 | */ 69 | isCanceled: boolean; 70 | /** 71 | * Indicates the timer should be canceled. This request will execute on the 72 | * next `yield` or `return` statement. 73 | */ 74 | cancel: () => void; 75 | } 76 | 77 | /** 78 | * A specific error thrown when context.df.Task.all() fails. Its message 79 | * contains an aggregation of all the exceptions that failed. It should follow the 80 | * below format: 81 | * 82 | * context.df.Task.all() encountered the below error messages: 83 | * 84 | * Name: DurableError 85 | * Message: The activity function "ActivityA" failed. 86 | * StackTrace: 87 | * ----------------------------------- 88 | * Name: DurableError 89 | * Message: The activity function "ActivityB" failed. 90 | * StackTrace: 91 | */ 92 | export declare class AggregatedError extends Error { 93 | /** 94 | * The list of errors nested inside this `AggregatedError` 95 | */ 96 | errors: Error[]; 97 | 98 | /** 99 | * Construct an `AggregatedError` using a list of errors 100 | * @param errors List of errors. 101 | */ 102 | constructor(errors: Error[]); 103 | } 104 | -------------------------------------------------------------------------------- /types/trigger.d.ts: -------------------------------------------------------------------------------- 1 | import { ActivityTrigger } from "./activity"; 2 | import { EntityTrigger } from "./entity"; 3 | import { OrchestrationTrigger } from "./orchestration"; 4 | 5 | /** 6 | * @returns a durable activity trigger 7 | */ 8 | export function activity(): ActivityTrigger; 9 | 10 | /** 11 | * @returns a durable orchestration trigger 12 | */ 13 | export function orchestration(): OrchestrationTrigger; 14 | 15 | /** 16 | * @returns a durable entity trigger 17 | */ 18 | export function entity(): EntityTrigger; 19 | --------------------------------------------------------------------------------