├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── Nightly.yml │ ├── ci.yml │ ├── plan-release.yml │ ├── publish.yml │ └── push-dist.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.cjs ├── .release-plan.json ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── RELEASE.md ├── config └── ember-cli-update.json ├── ember-async-data ├── .gitignore ├── .prettierignore ├── .prettierrc.cjs ├── .template-lintrc.cjs ├── addon-main.cjs ├── babel.config.json ├── eslint.config.mjs ├── package.json ├── rollup.config.mjs ├── src │ ├── helpers │ │ └── load.ts │ ├── index.ts │ ├── template-registry.ts │ └── tracked-async-data.ts ├── tsconfig.json ├── type-tests │ ├── index.ts │ ├── load-test.ts │ ├── tracked-async-data-test.ts │ └── tsconfig.json └── unpublished-development-types │ └── index.d.ts ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── test-app ├── .editorconfig ├── .ember-cli ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .stylelintignore ├── .stylelintrc.js ├── .template-lintrc.js ├── .watchmanconfig ├── app ├── app.ts ├── components │ └── .gitkeep ├── config │ └── environment.d.ts ├── controllers │ └── .gitkeep ├── helpers │ └── .gitkeep ├── index.html ├── models │ └── .gitkeep ├── router.ts ├── routes │ └── .gitkeep ├── styles │ └── app.css └── templates │ └── application.gjs ├── config ├── ember-cli-update.json ├── ember-try.js ├── environment.js ├── optional-features.json └── targets.js ├── ember-cli-build.js ├── eslint.config.mjs ├── package.json ├── public └── robots.txt ├── testem.js ├── tests ├── defer.ts ├── helpers │ └── index.ts ├── index.html ├── integration │ └── helpers │ │ └── load-test.gts ├── test-helper.ts └── unit │ ├── helpers │ └── load-test.ts │ └── tracked-async-data-test.ts ├── tsconfig.json ├── types ├── global.d.ts └── test-app │ └── index.d.ts └── vendor └── .gitkeep /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: 'daily' 7 | -------------------------------------------------------------------------------- /.github/workflows/Nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly TypeScript Run 2 | 3 | # run the typescript@next Ember Try job... 4 | jobs: 5 | ts-next: 6 | name: typescript@next 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: pnpm/action-setup@v4 11 | - name: Install Node 12 | uses: actions/setup-node@v4 13 | with: 14 | node-version: 20 15 | cache: pnpm 16 | 17 | - name: Install Dependencies 18 | run: pnpm install --frozen-lockfile 19 | 20 | - run: pnpm add -D typescript@next 21 | working-directory: ember-async-data 22 | 23 | - run: ./node_modules/.bin/tsc --version && ./node_modules/.bin/tsc --noEmit true --emitDeclarationOnly false 24 | working-directory: ember-async-data 25 | 26 | - run: ./node_modules/.bin/tsc --noEmit true --emitDeclarationOnly false --project type-tests --rootDir "." 27 | working-directory: ember-async-data 28 | 29 | # ...nightly at midnight 30 | on: 31 | schedule: 32 | - cron: 0 0 * * * 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | pull_request: {} 9 | 10 | concurrency: 11 | group: ci-${{ github.head_ref || github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | test: 16 | name: "Tests" 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 10 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: pnpm/action-setup@v4 23 | - name: Install Node 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | cache: pnpm 28 | 29 | - name: Install Dependencies 30 | run: pnpm install --frozen-lockfile 31 | 32 | - name: Lint Addon 33 | run: pnpm run --filter=ember-async-data lint 34 | 35 | - name: Lint Test App 36 | run: pnpm run --filter=test-app lint 37 | 38 | - name: Run Tests 39 | run: pnpm run test:ember 40 | 41 | floating: 42 | name: "Floating Dependencies" 43 | runs-on: ubuntu-latest 44 | timeout-minutes: 10 45 | 46 | steps: 47 | - uses: actions/checkout@v4 48 | - uses: pnpm/action-setup@v4 49 | - name: Install Node 50 | uses: actions/setup-node@v4 51 | with: 52 | node-version: 20 53 | cache: pnpm 54 | 55 | - name: Install Dependencies 56 | run: pnpm install --no-lockfile 57 | 58 | - name: Run Tests 59 | run: pnpm test:ember 60 | 61 | try-scenarios: 62 | name: ${{ matrix.try-scenario }} 63 | runs-on: ubuntu-latest 64 | needs: 'test' 65 | timeout-minutes: 10 66 | 67 | strategy: 68 | fail-fast: false 69 | matrix: 70 | try-scenario: 71 | - ember-lts-4.8 72 | - ember-lts-4.12 73 | - ember-lts-5.4 74 | - ember-lts-5.8 75 | - ember-lts-5.12 76 | - ember-release 77 | - ember-beta 78 | - ember-canary 79 | - embroider-safe 80 | - embroider-optimized 81 | 82 | steps: 83 | - uses: actions/checkout@v4 84 | - uses: pnpm/action-setup@v4 85 | - name: Install Node 86 | uses: actions/setup-node@v4 87 | with: 88 | node-version: 20 89 | cache: pnpm 90 | 91 | - name: Install Dependencies 92 | run: pnpm install --frozen-lockfile 93 | 94 | - name: Run Tests 95 | run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }} --skip-cleanup 96 | working-directory: test-app 97 | 98 | tests_ts: 99 | name: "Type checking: TS ${{ matrix.ts-version }}" 100 | runs-on: ubuntu-latest 101 | needs: test 102 | timeout-minutes: 10 103 | 104 | strategy: 105 | fail-fast: false 106 | matrix: 107 | ts-version: 108 | - 5.5 109 | - 5.6 110 | - 5.7 111 | - next 112 | 113 | steps: 114 | - uses: actions/checkout@v4 115 | - uses: pnpm/action-setup@v4 116 | - name: Install Node 117 | uses: actions/setup-node@v4 118 | with: 119 | node-version: 20 120 | cache: pnpm 121 | 122 | - name: Install Dependencies 123 | run: pnpm install --frozen-lockfile 124 | 125 | - run: pnpm add -D typescript@${{ matrix.ts-version }} 126 | working-directory: ember-async-data 127 | 128 | - run: ./node_modules/.bin/tsc --version && ./node_modules/.bin/tsc --noEmit true --emitDeclarationOnly false 129 | working-directory: ember-async-data 130 | 131 | - run: ./node_modules/.bin/tsc --noEmit true --emitDeclarationOnly false --project type-tests --rootDir "." 132 | working-directory: ember-async-data 133 | -------------------------------------------------------------------------------- /.github/workflows/plan-release.yml: -------------------------------------------------------------------------------- 1 | name: Plan Release 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | pull_request_target: # This workflow has permissions on the repo, do NOT run code from PRs in this workflow. See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ 9 | types: 10 | - labeled 11 | - unlabeled 12 | 13 | concurrency: 14 | group: plan-release # only the latest one of these should ever be running 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | is-this-a-release: 19 | name: "Is this a release?" 20 | runs-on: ubuntu-latest 21 | outputs: 22 | command: ${{ steps.check-release.outputs.command }} 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 2 28 | ref: 'main' 29 | # This will only cause the `is-this-a-release` job to have a "command" of `release` 30 | # when the .release-plan.json file was changed on the last commit. 31 | - id: check-release 32 | run: if git diff --name-only HEAD HEAD~1 | grep -w -q ".release-plan.json"; then echo "command=release"; fi >> $GITHUB_OUTPUT 33 | 34 | create-prepare-release-pr: 35 | name: Create Prepare Release PR 36 | runs-on: ubuntu-latest 37 | timeout-minutes: 5 38 | needs: is-this-a-release 39 | permissions: 40 | contents: write 41 | issues: read 42 | pull-requests: write 43 | # only run on push event or workflow dispatch if plan wasn't updated (don't create a release plan when we're releasing) 44 | # only run on labeled event if the PR has already been merged 45 | if: ((github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.is-this-a-release.outputs.command != 'release') || (github.event_name == 'pull_request_target' && github.event.pull_request.merged == true) 46 | 47 | steps: 48 | - uses: actions/checkout@v4 49 | # We need to download lots of history so that 50 | # github-changelog can discover what's changed since the last release 51 | with: 52 | fetch-depth: 0 53 | ref: 'main' 54 | - uses: pnpm/action-setup@v4 55 | - uses: actions/setup-node@v4 56 | with: 57 | node-version: 20 58 | cache: pnpm 59 | - run: pnpm install --frozen-lockfile 60 | - name: "Generate Explanation and Prep Changelogs" 61 | id: explanation 62 | run: | 63 | set +e 64 | pnpm release-plan prepare 2> >(tee -a release-plan-stderr.txt >&2) 65 | 66 | if [ $? -ne 0 ]; then 67 | release_plan_output=$(cat release-plan-stderr.txt) 68 | else 69 | release_plan_output=$(jq .description .release-plan.json -r) 70 | rm release-plan-stderr.txt 71 | 72 | if [ $(jq '.solution | length' .release-plan.json) -eq 1 ]; then 73 | new_version=$(jq -r '.solution[].newVersion' .release-plan.json) 74 | echo "new_version=v$new_version" >> $GITHUB_OUTPUT 75 | fi 76 | fi 77 | echo 'text<> $GITHUB_OUTPUT 78 | echo "$release_plan_output" >> $GITHUB_OUTPUT 79 | echo 'EOF' >> $GITHUB_OUTPUT 80 | env: 81 | GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} 82 | 83 | - uses: peter-evans/create-pull-request@v7 84 | with: 85 | commit-message: "Prepare Release ${{ steps.explanation.outputs.new_version}} using 'release-plan'" 86 | labels: "internal" 87 | branch: release-preview 88 | title: Prepare Release ${{ steps.explanation.outputs.new_version }} 89 | body: | 90 | This PR is a preview of the release that [release-plan](https://github.com/embroider-build/release-plan) has prepared. To release you should just merge this PR 👍 91 | 92 | ----------------------------------------- 93 | 94 | ${{ steps.explanation.outputs.text }} 95 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # For every push to the primary branch with .release-plan.json modified, 2 | # runs release-plan. 3 | 4 | name: Publish Stable 5 | 6 | on: 7 | workflow_dispatch: 8 | push: 9 | branches: 10 | - main 11 | - master 12 | paths: 13 | - '.release-plan.json' 14 | 15 | concurrency: 16 | group: publish-${{ github.head_ref || github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | publish: 21 | name: "NPM Publish" 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: write 25 | pull-requests: write 26 | id-token: write 27 | attestations: write 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: pnpm/action-setup@v4 32 | - uses: actions/setup-node@v4 33 | with: 34 | node-version: 20 35 | # This creates an .npmrc that reads the NODE_AUTH_TOKEN environment variable 36 | registry-url: 'https://registry.npmjs.org' 37 | cache: pnpm 38 | - run: pnpm install --frozen-lockfile 39 | - name: Publish to NPM 40 | run: NPM_CONFIG_PROVENANCE=true pnpm release-plan publish 41 | env: 42 | GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} 43 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | -------------------------------------------------------------------------------- /.github/workflows/push-dist.yml: -------------------------------------------------------------------------------- 1 | # Because this library needs to be built, 2 | # we can't easily point package.json files at the git repo for easy cross-repo testing. 3 | # 4 | # This workflow brings back that capability by placing the compiled assets on a "dist" branch 5 | # (configurable via the "branch" option below) 6 | name: Push dist 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | - master 13 | 14 | jobs: 15 | push-dist: 16 | name: Push dist 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 10 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: pnpm/action-setup@v4 23 | - name: Install Node 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | cache: pnpm 28 | 29 | - name: Install Dependencies 30 | run: pnpm install --frozen-lockfile 31 | 32 | - uses: kategengler/put-built-npm-package-contents-on-branch@v2.0.0 33 | with: 34 | branch: dist 35 | token: ${{ secrets.GITHUB_TOKEN }} 36 | working-directory: 'ember-async-data' 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # misc 7 | .env* 8 | .pnp* 9 | .pnpm-debug.log 10 | .sass-cache 11 | .eslintcache 12 | coverage/ 13 | npm-debug.log* 14 | yarn-error.log 15 | 16 | # ember-try 17 | /.node_modules.ember-try/ 18 | /package.json.ember-try 19 | /package-lock.json.ember-try 20 | /yarn.lock.ember-try 21 | /pnpm-lock.ember-try.yaml 22 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # Docs: https://pnpm.io/npmrc 2 | # https://github.com/emberjs/rfcs/pull/907 3 | 4 | # we don't want addons to be bad citizens of the ecosystem 5 | auto-install-peers=false 6 | strict-peer-dependencies=true 7 | 8 | # we want true isolation, 9 | # if a dependency is not declared, we want an error 10 | resolve-peers-from-workspace-root=false 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Prettier is also run from each package, so the ignores here 2 | # protect against files that may not be within a package 3 | 4 | # misc 5 | !.* 6 | .lint-todo/ 7 | 8 | # ember-try 9 | /.node_modules.ember-try/ 10 | /pnpm-lock.ember-try.yaml 11 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | plugins: ['prettier-plugin-ember-template-tag'], 5 | singleQuote: true, 6 | }; 7 | -------------------------------------------------------------------------------- /.release-plan.json: -------------------------------------------------------------------------------- 1 | { 2 | "solution": { 3 | "ember-async-data": { 4 | "impact": "patch", 5 | "oldVersion": "2.0.0", 6 | "newVersion": "2.0.1", 7 | "tagName": "latest", 8 | "constraints": [ 9 | { 10 | "impact": "patch", 11 | "reason": "Appears in changelog section :bug: Bug Fix" 12 | }, 13 | { 14 | "impact": "patch", 15 | "reason": "Appears in changelog section :house: Internal" 16 | } 17 | ], 18 | "pkgJSONPath": "./ember-async-data/package.json" 19 | } 20 | }, 21 | "description": "## Release (2025-05-12)\n\n* ember-async-data 2.0.1 (patch)\n\n#### :bug: Bug Fix\n* `ember-async-data`\n * [#854](https://github.com/tracked-tools/ember-async-data/pull/854) Remove unneeded peer dependency declaration ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n\n#### :memo: Documentation\n* [#821](https://github.com/tracked-tools/ember-async-data/pull/821) Update TypeScript support badge ([@SergeAstapov](https://github.com/SergeAstapov))\n\n#### :house: Internal\n* `ember-async-data`\n * [#855](https://github.com/tracked-tools/ember-async-data/pull/855) chore(deps-dev): bump pnpm from 10.6.3 to 10.10.0 ([@SergeAstapov](https://github.com/SergeAstapov))\n * [#834](https://github.com/tracked-tools/ember-async-data/pull/834) Bump release-plan from v0.13 to v0.16 ([@SergeAstapov](https://github.com/SergeAstapov))\n* Other\n * [#829](https://github.com/tracked-tools/ember-async-data/pull/829) Update Nightly TypeScript run job ([@SergeAstapov](https://github.com/SergeAstapov))\n\n#### Committers: 2\n- Sergey Astapov ([@SergeAstapov](https://github.com/SergeAstapov))\n- [@NullVoxPopuli](https://github.com/NullVoxPopuli)\n" 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Release (2025-05-12) 4 | 5 | * ember-async-data 2.0.1 (patch) 6 | 7 | #### :bug: Bug Fix 8 | * `ember-async-data` 9 | * [#854](https://github.com/tracked-tools/ember-async-data/pull/854) Remove unneeded peer dependency declaration ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 10 | 11 | #### :memo: Documentation 12 | * [#821](https://github.com/tracked-tools/ember-async-data/pull/821) Update TypeScript support badge ([@SergeAstapov](https://github.com/SergeAstapov)) 13 | 14 | #### :house: Internal 15 | * `ember-async-data` 16 | * [#855](https://github.com/tracked-tools/ember-async-data/pull/855) chore(deps-dev): bump pnpm from 10.6.3 to 10.10.0 ([@SergeAstapov](https://github.com/SergeAstapov)) 17 | * [#834](https://github.com/tracked-tools/ember-async-data/pull/834) Bump release-plan from v0.13 to v0.16 ([@SergeAstapov](https://github.com/SergeAstapov)) 18 | * Other 19 | * [#829](https://github.com/tracked-tools/ember-async-data/pull/829) Update Nightly TypeScript run job ([@SergeAstapov](https://github.com/SergeAstapov)) 20 | 21 | #### Committers: 2 22 | - Sergey Astapov ([@SergeAstapov](https://github.com/SergeAstapov)) 23 | - [@NullVoxPopuli](https://github.com/NullVoxPopuli) 24 | 25 | ## Release (2025-03-02) 26 | 27 | ember-async-data 2.0.0 (major) 28 | 29 | #### :boom: Breaking Change 30 | * `ember-async-data`, `test-app` 31 | * [#819](https://github.com/tracked-tools/ember-async-data/pull/819) Throw an error when accessing `.value` or `.error` unsafely ([@SergeAstapov](https://github.com/SergeAstapov)) 32 | * `ember-async-data` 33 | * [#818](https://github.com/tracked-tools/ember-async-data/pull/818) Drop @dependentKeyCompat from addon ([@SergeAstapov](https://github.com/SergeAstapov)) 34 | * Other 35 | * [#816](https://github.com/tracked-tools/ember-async-data/pull/816) Drop support for TypeScript < 5.5 ([@SergeAstapov](https://github.com/SergeAstapov)) 36 | 37 | #### :rocket: Enhancement 38 | * `ember-async-data` 39 | * [#814](https://github.com/tracked-tools/ember-async-data/pull/814) Expand @ember/test-waiters dependency range to allow v4 ([@SergeAstapov](https://github.com/SergeAstapov)) 40 | 41 | #### :memo: Documentation 42 | * `ember-async-data`, `test-app` 43 | * [#820](https://github.com/tracked-tools/ember-async-data/pull/820) Update documentation and tests to use template tag ([@SergeAstapov](https://github.com/SergeAstapov)) 44 | * Other 45 | * [#773](https://github.com/tracked-tools/ember-async-data/pull/773) Fix Loader Example ([@devotox](https://github.com/devotox)) 46 | 47 | #### :house: Internal 48 | * `ember-async-data`, `test-app` 49 | * [#815](https://github.com/tracked-tools/ember-async-data/pull/815) Sync addon with latest addon-blueprint ([@SergeAstapov](https://github.com/SergeAstapov)) 50 | * [#809](https://github.com/tracked-tools/ember-async-data/pull/809) Switch from yarn to pnpm ([@SergeAstapov](https://github.com/SergeAstapov)) 51 | * [#715](https://github.com/tracked-tools/ember-async-data/pull/715) Add TypeScript 5.1 and 5.2 to support matrix ([@SergeAstapov](https://github.com/SergeAstapov)) 52 | * `ember-async-data` 53 | * [#813](https://github.com/tracked-tools/ember-async-data/pull/813) Replace release-it with release-plan ([@SergeAstapov](https://github.com/SergeAstapov)) 54 | * [#806](https://github.com/tracked-tools/ember-async-data/pull/806) chore(deps-dev): Bump actions/checkout from v3 to v4 ([@SergeAstapov](https://github.com/SergeAstapov)) 55 | * [#735](https://github.com/tracked-tools/ember-async-data/pull/735) Bump prettier and eslint-plugin-prettier ([@nlfurniss](https://github.com/nlfurniss)) 56 | * Other 57 | * [#812](https://github.com/tracked-tools/ember-async-data/pull/812) Update TypeScript testing matrix to include >=5.0 ([@SergeAstapov](https://github.com/SergeAstapov)) 58 | * [#810](https://github.com/tracked-tools/ember-async-data/pull/810) Print tsc version to ensure proper TypeScript version usage ([@SergeAstapov](https://github.com/SergeAstapov)) 59 | * [#736](https://github.com/tracked-tools/ember-async-data/pull/736) Bump browserlist ([@nlfurniss](https://github.com/nlfurniss)) 60 | * `test-app` 61 | * [#808](https://github.com/tracked-tools/ember-async-data/pull/808) Bump ember-cli from 5.7 to 5.12 ([@SergeAstapov](https://github.com/SergeAstapov)) 62 | 63 | #### Committers: 3 64 | - Devonte ([@devotox](https://github.com/devotox)) 65 | - Nathaniel Furniss ([@nlfurniss](https://github.com/nlfurniss)) 66 | - Sergey Astapov ([@SergeAstapov](https://github.com/SergeAstapov)) 67 | 68 | All notable changes to this project will be documented in this file. 69 | 70 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | ## v1.0.3 (2023-10-01) 79 | 80 | #### :bug: Bug Fix 81 | * [#704](https://github.com/tracked-tools/ember-async-data/pull/704) Fix triple-slash reference in complied output, breaking TS consumers ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 82 | 83 | #### :memo: Documentation 84 | * [#707](https://github.com/tracked-tools/ember-async-data/pull/707) Update minimum supported Ember.js version in README.md ([@SergeAstapov](https://github.com/SergeAstapov)) 85 | * [#693](https://github.com/tracked-tools/ember-async-data/pull/693) Update repository field in package.json ([@SergeAstapov](https://github.com/SergeAstapov)) 86 | 87 | #### :house: Internal 88 | * [#681](https://github.com/tracked-tools/ember-async-data/pull/681) Replace publish-unstable workflow with push-dist ([@SergeAstapov](https://github.com/SergeAstapov)) 89 | 90 | #### Committers: 2 91 | - Sergey Astapov ([@SergeAstapov](https://github.com/SergeAstapov)) 92 | - [@NullVoxPopuli](https://github.com/NullVoxPopuli) 93 | 94 | ## v1.0.2 (2023-08-02) 95 | 96 | #### :rocket: Enhancement 97 | * [#625](https://github.com/tracked-tools/ember-async-data/pull/625) Add ember v5 to peerDependencies ([@SergeAstapov](https://github.com/SergeAstapov)) 98 | 99 | #### :memo: Documentation 100 | * [#666](https://github.com/tracked-tools/ember-async-data/pull/666) Add brackets for test selector ([@jrjohnson](https://github.com/jrjohnson)) 101 | 102 | #### :house: Internal 103 | * [#679](https://github.com/tracked-tools/ember-async-data/pull/679) Sync with embroider-addon blueprint ([@SergeAstapov](https://github.com/SergeAstapov)) 104 | 105 | #### Committers: 2 106 | - Jon Johnson ([@jrjohnson](https://github.com/jrjohnson)) 107 | - Sergey Astapov ([@SergeAstapov](https://github.com/SergeAstapov)) 108 | 109 | 110 | ## v1.0.1 (2023-03-15) 111 | 112 | **Note:** This is a significant bug fix which *does* (very mildly) break the public API, but necessarily so for the sake of fixing a bug. 113 | 114 | #### :bug: Bug Fix 115 | * [#578](https://github.com/tracked-tools/ember-async-data/pull/578) Bugfix: drop `context` and stop caching `TrackedAsyncData` ([@chriskrycho](https://github.com/chriskrycho)) 116 | 117 | Previously, `TrackedAsyncData` and the `load` function accepted a `context` parameter as their second argument, to use with Ember's destroyables API. However, that was (a) unnecessary and (b) could actually cause memory leaks, literally the opposite of what it was designed to do. To account for this change, simply remove that call from all call sites. 118 | 119 | Additionally, note that this means that you will no longer get a single instance of `TrackedAsyncData` for the same promise. In most cases, this is irrelevant, and it is likely that removing our cache which attempted to be helpful that way will *improve* your performance. 120 | 121 | #### Committers: 1 122 | - Chris Krycho ([@chriskrycho](https://github.com/chriskrycho)) 123 | 124 | ## v1.0.0 (2023-03-15) 125 | 126 | #### :boom: Breaking Change 127 | * [#527](https://github.com/tracked-tools/ember-async-data/pull/527) Drop TypeScript support for <= 4.7 ([@SergeAstapov](https://github.com/SergeAstapov)) 128 | * [#575](https://github.com/tracked-tools/ember-async-data/pull/575) Add hard error and deprecations for 1.0 ([@chriskrycho](https://github.com/chriskrycho)) 129 | * [#473](https://github.com/tracked-tools/ember-async-data/pull/473) Use the types published from Ember itself; require Ember.js v4.8.4 or above ([@chriskrycho](https://github.com/chriskrycho)) 130 | 131 | #### :rocket: Enhancement 132 | * [#575](https://github.com/tracked-tools/ember-async-data/pull/575) Add hard error and deprecations for 1.0 ([@chriskrycho](https://github.com/chriskrycho)) 133 | * [#473](https://github.com/tracked-tools/ember-async-data/pull/473) Use the types published from Ember itself ([@chriskrycho](https://github.com/chriskrycho)) 134 | 135 | #### :memo: Documentation 136 | * [#546](https://github.com/tracked-tools/ember-async-data/pull/546) Fix wrong Readme example ([@simonihmig](https://github.com/simonihmig)) 137 | * [#545](https://github.com/tracked-tools/ember-async-data/pull/545) Fix reference to non-existing `TrackedPromise` in Readme ([@simonihmig](https://github.com/simonihmig)) 138 | * [#528](https://github.com/tracked-tools/ember-async-data/pull/528) Fix typo in docs ([@SergeAstapov](https://github.com/SergeAstapov)) 139 | * [#508](https://github.com/tracked-tools/ember-async-data/pull/508) Replace /endif with /if ([@kennstenicht](https://github.com/kennstenicht)) 140 | * [#456](https://github.com/tracked-tools/ember-async-data/pull/456) Update links in README.md after migration to tracked-tools org ([@SergeAstapov](https://github.com/SergeAstapov)) 141 | * [#404](https://github.com/tracked-tools/ember-async-data/pull/404) Add Glint usage docs ([@SergeAstapov](https://github.com/SergeAstapov)) 142 | * [#407](https://github.com/tracked-tools/ember-async-data/pull/407) Add note about TypeScript 4.9 support ([@SergeAstapov](https://github.com/SergeAstapov)) 143 | * [#406](https://github.com/tracked-tools/ember-async-data/pull/406) Minor tweaks in README.md ([@SergeAstapov](https://github.com/SergeAstapov)) 144 | 145 | #### :house: Internal 146 | * [#576](https://github.com/tracked-tools/ember-async-data/pull/576) Use `16 || >= 18` in test app engines ([@chriskrycho](https://github.com/chriskrycho)) 147 | * [#572](https://github.com/tracked-tools/ember-async-data/pull/572) [BREAKING] Drop support for non-active versions of Node ([@nlfurniss](https://github.com/nlfurniss)) 148 | * [#547](https://github.com/tracked-tools/ember-async-data/pull/547) Disable publish-unstable workflow ([@SergeAstapov](https://github.com/SergeAstapov)) 149 | * [#474](https://github.com/tracked-tools/ember-async-data/pull/474) Skip publish-unstable for dependabot PRs ([@SergeAstapov](https://github.com/SergeAstapov)) 150 | * [#401](https://github.com/tracked-tools/ember-async-data/pull/401) add publish-unstable workflow ([@SergeAstapov](https://github.com/SergeAstapov)) 151 | * [#397](https://github.com/tracked-tools/ember-async-data/pull/397) update v2 addon setup ([@SergeAstapov](https://github.com/SergeAstapov)) 152 | * [#396](https://github.com/tracked-tools/ember-async-data/pull/396) Remove eslint-plugin-qunit from addon .eslintrc.js ([@SergeAstapov](https://github.com/SergeAstapov)) 153 | 154 | #### Committers: 5 155 | - Chris Krycho ([@chriskrycho](https://github.com/chriskrycho)) 156 | - Christoph Wiedenmann ([@kennstenicht](https://github.com/kennstenicht)) 157 | - Nathaniel Furniss ([@nlfurniss](https://github.com/nlfurniss)) 158 | - Sergey Astapov ([@SergeAstapov](https://github.com/SergeAstapov)) 159 | - Simon Ihmig ([@simonihmig](https://github.com/simonihmig)) 160 | 161 | ## v0.7.0 (2022-11-10) 162 | 163 | #### :boom: Breaking Change 164 | * [#384](https://github.com/chriskrycho/ember-async-data/pull/384) convert addon to v2 format ([@SergeAstapov](https://github.com/SergeAstapov)) 165 | * [#300](https://github.com/chriskrycho/ember-async-data/pull/300) Set minimum Ember version to 3.28 ([@chriskrycho](https://github.com/chriskrycho)) 166 | * [#301](https://github.com/chriskrycho/ember-async-data/pull/301) Require Node 14, support Node 16 and 18 ([@chriskrycho](https://github.com/chriskrycho)) 167 | * [#298](https://github.com/chriskrycho/ember-async-data/pull/298) Set minimum TS version to 4.5, use Ember v4 types ([@chriskrycho](https://github.com/chriskrycho)) 168 | 169 | #### :rocket: Enhancement 170 | * [#301](https://github.com/chriskrycho/ember-async-data/pull/301) Require Node 14, support Node 16 and 18 ([@chriskrycho](https://github.com/chriskrycho)) 171 | * [#299](https://github.com/chriskrycho/ember-async-data/pull/299) Add support for TS 4.6 and 4.7 ([@chriskrycho](https://github.com/chriskrycho)) 172 | * [#213](https://github.com/chriskrycho/ember-async-data/pull/213) Add support for TS 4.5 ([@chriskrycho](https://github.com/chriskrycho)) 173 | 174 | #### :bug: Bug Fix 175 | * [#183](https://github.com/chriskrycho/ember-async-data/pull/183) Fix `paths` location in type tests tsconfig.json ([@chriskrycho](https://github.com/chriskrycho)) 176 | 177 | #### :memo: Documentation 178 | * [#359](https://github.com/chriskrycho/ember-async-data/pull/359) update hbs import from ember-cli-htmlbars ([@SergeAstapov](https://github.com/SergeAstapov)) 179 | * [#213](https://github.com/chriskrycho/ember-async-data/pull/213) Add support for TS 4.5 ([@chriskrycho](https://github.com/chriskrycho)) 180 | * [#153](https://github.com/chriskrycho/ember-async-data/pull/153) Docs: improve and fix issues in the README ([@chriskrycho](https://github.com/chriskrycho)) 181 | 182 | #### :house: Internal 183 | * [#382](https://github.com/chriskrycho/ember-async-data/pull/382) convert to monorepo ([@SergeAstapov](https://github.com/SergeAstapov)) 184 | * [#364](https://github.com/chriskrycho/ember-async-data/pull/364) run `npx ember-cli-update --to=4.8.0` to align with the latest blueprint ([@SergeAstapov](https://github.com/SergeAstapov)) 185 | * [#380](https://github.com/chriskrycho/ember-async-data/pull/380) Update prettier setup per latest addon blueprint ([@SergeAstapov](https://github.com/SergeAstapov)) 186 | * [#243](https://github.com/chriskrycho/ember-async-data/pull/243) Use `finally` for `waiter.endAsync` ([@chriskrycho](https://github.com/chriskrycho)) 187 | 188 | #### Committers: 3 189 | - Chris Krycho ([@chriskrycho](https://github.com/chriskrycho)) 190 | - Nathaniel Furniss ([@nlfurniss](https://github.com/nlfurniss)) 191 | - Sergey Astapov ([@SergeAstapov](https://github.com/SergeAstapov)) 192 | 193 | ## v0.6.0 (2021-09-04) 194 | 195 | #### :boom: Breaking Change 196 | * [#152](https://github.com/chriskrycho/ember-async-data/pull/152) Breaking: drop support for versions of Ember before 3.24 LTS ([@chriskrycho](https://github.com/chriskrycho)) 197 | * [#149](https://github.com/chriskrycho/ember-async-data/pull/149) Breaking: require destroyable context ([@chriskrycho](https://github.com/chriskrycho)) 198 | 199 | #### Committers: 1 200 | - Chris Krycho ([@chriskrycho](https://github.com/chriskrycho)) 201 | 202 | ## v0.5.1 (2021-09-04) 203 | 204 | #### :house: Internal 205 | * [#71](https://github.com/chriskrycho/ember-async-data/pull/71) Configure release-it for future releases ([@chriskrycho](https://github.com/chriskrycho)) 206 | 207 | #### Committers: 2 208 | - Chris Krycho ([@chriskrycho](https://github.com/chriskrycho)) 209 | - Nathaniel Furniss ([@nlfurniss](https://github.com/nlfurniss)) 210 | 211 | ## [v0.5.0] (2021-06-01) 212 | 213 | ### Added :star: 214 | 215 | - Add support for [TypeScript 4.3](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-3.html) (#63) 216 | - Add re-exports from the index (#70): you can now `import { TrackedAsyncData, load } from 'ember-async-data'; 217 | 218 | ### Docs 📖 219 | 220 | - Add a section on testing (#69) 221 | 222 | ## [v0.4.0] (2021-05-13) 223 | 224 | ### Fixed :hammer_and_wrench: 225 | 226 | `@ember/test-waiters` is now a direct dependency as it's used by app code. 227 | 228 | ## [v0.3.0] (2021-04-12) 229 | 230 | ### Added :star: 231 | 232 | Following on from 0.2.0's support for narrowing with `.isPending`, `.isResolved`, and `.isRejected`, `TrackedAsyncData` instances cab now be "narrowed" by checking the `.state` property ([#6]): 233 | 234 | ```ts 235 | import TrackedAsyncData from 'ember-async-data/tracked-async-data'; 236 | 237 | let data = new TrackedAsyncData(Promise.resolve('string')); 238 | switch (data.state) { 239 | case 'PENDING'; 240 | data.value; // null (and a warning for accessing in an invalid state!) 241 | data.error; // null (and a warning for accessing in an invalid state!) 242 | break; 243 | case 'RESOLVED': 244 | data.value; // string 245 | data.error; // null (and a warning for accessing in an invalid state!) 246 | break; 247 | case 'REJECTED': 248 | data.value; // null (and a warning for accessing in an invalid state!) 249 | data.error; // unknown 250 | break; 251 | default: 252 | break; 253 | } 254 | ``` 255 | 256 | ### Fixed :hammer_and_wrench: 257 | 258 | Decorated `.state` with `@dependentKeyCompat` so it can be used as a dependent key with classic computed properties. 259 | 260 | [#6]: https://github.com/chriskrycho/ember-async-data/pull/6 261 | 262 | ## [v0.2.0] (2021-03-27) 263 | 264 | This is a wholly backwards-compatible change, which just adds one new feature and improves some docs. 265 | 266 | ### Added :star: 267 | 268 | `TrackedAsyncData` now has the ability to use TypeScript’s type-narrowing functionality via the `.isPending`, `.isResolved`, and `.isRejected` ([#2]) checks: 269 | 270 | ```ts 271 | import TrackedAsyncData from 'ember-async-data/tracked-async-data'; 272 | 273 | let data = new TrackedAsyncData(Promise.resolve('string')); 274 | if (data.isPending) { 275 | data.value; // null (and a warning for accessing in an invalid state!) 276 | data.error; // null (and a warning for accessing in an invalid state!) 277 | } else if (data.isResolved) { 278 | data.value; // string 279 | data.error; // null (and a warning for accessing in an invalid state!) 280 | } else if (data.isRejected) { 281 | data.value; // null (and a warning for accessing in an invalid state!) 282 | data.error; // unknown 283 | } 284 | ``` 285 | 286 | (Remember that the `null` fallbacks for `.value` and `.error` will be removed in a future version which drops support for Ember Classic computed properties.) 287 | 288 | [#2]: https://github.com/chriskrycho/ember-async-data/pull/2 289 | 290 | ## [v0.1.0] (2021-03-18) 291 | 292 | Initial release, with `TrackedAsyncData` and a `load` helper! 293 | 294 | [v0.5.0]: https://github.com/chriskrycho/ember-async-data/compare/v0.4.0...v0.5.0 295 | [v0.4.0]: https://github.com/chriskrycho/ember-async-data/compare/v0.3.0...v0.4.0 296 | [v0.3.0]: https://github.com/chriskrycho/ember-async-data/compare/v0.2.0...v0.3.0 297 | [v0.2.0]: https://github.com/chriskrycho/ember-async-data/compare/v0.1.0...v0.2.0 298 | [v0.1.0]: https://github.com/chriskrycho/ember-async-data/compare/b1d0dbf...v0.1.0 299 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | This repo is divided into multiple packages using pnpm workspaces: 4 | 5 | - `ember-async-data` is the actual ember-async-data addon 6 | - `test-app` is its test suite 7 | 8 | ## Installation 9 | 10 | - `git clone https://github.com/chriskrycho/ember-async-data.git` 11 | - `cd ember-async-data` 12 | - `pnpm install` 13 | 14 | ## Linting 15 | 16 | - `pnpm lint` 17 | - `pnpm lint:fix` 18 | 19 | ## Building the addon 20 | 21 | - `cd ember-async-data` 22 | - `pnpm build` 23 | 24 | ## Running tests 25 | 26 | - `cd test-app` 27 | - `pnpm test` – Runs the test suite on the current Ember version 28 | - `pnpm test:watch` – Runs the test suite in "watch mode" 29 | 30 | ## Running the test application 31 | 32 | - `cd test-app` 33 | - `pnpm start` 34 | - Visit the test application at [http://localhost:4200](http://localhost:4200). 35 | 36 | For more information on using ember-cli, visit [https://cli.emberjs.com/release/](https://cli.emberjs.com/release/). 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | CI 4 | 5 | 6 | npm 7 | 8 | 9 | supported TypeScript versions 10 | 11 | 12 | Nightly TypeScript Run 13 | 14 | Stability: Active 15 | 16 | 17 | 18 |

19 | 20 | # ember-async-data 21 | 22 | A utility/helper and data structure for representing a `Promise` in a declarative, reactive way in Glimmer.js and Ember Octane. 23 | 24 | - Render a promise in a declarative way in template-only code: 25 | 26 | ```hbs 27 | {{#let (load @somePromise) as |result|}} 28 | {{#if result.isResolved}} 29 | 30 | {{else if result.isPending}} 31 | 32 | {{else if result.isRejected}} 33 |

34 | Whoops! Looks like something went wrong! 35 | {{result.error.message}} 36 |

37 | {{/if}} 38 | {{/let}} 39 | ``` 40 | 41 | - Create declarative data fetches based on arguments to a component in a backing class: 42 | 43 | ```gjs 44 | import Component from '@glimmer/component'; 45 | import { cached } from '@glimmer/tracking'; 46 | import { service } from '@ember/service'; 47 | import { TrackedAsyncData } from 'ember-async-data'; 48 | 49 | export default class SmartProfile extends Component { 50 | @service store; 51 | 52 | @cached 53 | get someData() { 54 | let recordPromise = this.store.findRecord('user', this.args.id); 55 | return new TrackedAsyncData(recordPromise); 56 | } 57 | 58 | 70 | } 71 | ``` 72 | 73 | (See the guide [below](#in-javascript) for why this uses `@cached`!) 74 | 75 | 76 | ## Contents 77 | 78 | - [Compatibility](#compatibility) 79 | - [TypeScript](#typescript) 80 | - [Installation](#installation) 81 | - [Motivation](#motivation) 82 | - [Usage](#usage) 83 | - [In JavaScript](#in-javascript) 84 | - [With TypeScript](#with-typescript) 85 | - [Note on Usage with API Calls](#note-on-usage-with-api-calls) 86 | - [`load` function](#load-function) 87 | - [Subclassing](#subclassing) 88 | - [In templates](#in-templates) 89 | - [Testing](#testing) 90 | - [Unit testing](#unit-testing) 91 | - [Integration testing](#integration-testing) 92 | - [API](#api) 93 | - [`TrackedAsyncData`](#trackedasyncdata) 94 | - [Notes](#notes) 95 | - [`load` function](#load-function-1) 96 | - [In templates](#in-templates-1) 97 | - [Explanation](#explanation) 98 | - [Background and history](#background-and-history) 99 | - [Contributing](#contributing) 100 | - [License](#license) 101 | - [Credit](#credit) 102 | 103 | --- 104 | 105 | ## Compatibility 106 | 107 | - Ember.js v4.8.4 or above (requires Octane Edition) 108 | - Embroider or ember-auto-import v2.0.0 or above (this is [v2 addon](https://emberjs.github.io/rfcs/0507-embroider-v2-package-format.html) 109 | 110 | ### TypeScript 111 | 112 | This project follows the current draft of [the Semantic Versioning for TypeScript Types][semver] proposal. 113 | 114 | - **Currently supported TypeScript versions:** v5.5 - v5.8 115 | - **Compiler support policy:** [simple majors][sm] 116 | - **Public API:** all published types not in a `-private` module are public 117 | 118 | [semver]: https://www.semver-ts.org 119 | [sm]: https://www.semver-ts.org/#simple-majors 120 | 121 | 122 | ## Installation 123 | 124 | ``` 125 | ember install ember-async-data 126 | ``` 127 | 128 | ## Motivation 129 | 130 | Sometimes it doesn't make sense for a route to fetch the data required by a given component. This can be the case for a number of reasons: 131 | 132 | - The data for the component is not essential for the route and is not driven by a URL. 133 | - In some contexts, it is impossible to know which data to load ahead of time. In these cases the data must be loaded at the point of user interaction. 134 | - Having a component drive its own consumption of data can make the component easier to reuse and increase readability where the data fetching would otherwise be far removed from the actual component usage. 135 | 136 | Additionally, it's often valuable to load data only when required, because it factors into network speed and CPU cost and often goes unused entirely. Though these concerns are present everywhere, they are especially important in rural areas and emerging markets where: 137 | 138 | - Networks are slower, so it costs more CPU and therefore energy/battery to accomplish the same task 139 | - Devices are slower, so both the network call and the response parsing further degrade page performance 140 | - Data loading often has a 1:1 bytes-used-to-amount-paid cost to the user 141 | 142 | 143 | ## Usage 144 | 145 | You can use `TrackedAsyncData` either directly in JavaScript or via the `{{load}}` helper in templates. 146 | 147 | - [In JavaScript](#in-javascript) 148 | - [With TypeScript](#with-typescript) 149 | - [Note on Usage with API Calls](#note-on-usage-with-api-calls) 150 | - [`load` function](#load-function) 151 | - [Subclassing](#subclassing) 152 | - [In Templates](#in-templates) 153 | - [Testing](#testing) 154 | 155 | 156 | ### In JavaScript 157 | 158 | To create a `TrackedAsyncData`, simply import it from the library and call its constructor with a `Promise`. 159 | 160 | First, a small utility function for being able to resolve or reject a `Promise` at will (so we can see how the lifecycle behaves): 161 | 162 | ```ts 163 | function defer() { 164 | let deferred!: { 165 | resolve: (value: unknown) => void; 166 | reject: (reason?: unknown) => void; 167 | } = {}; 168 | 169 | deferred.promise = new Promise((resolve, reject) => { 170 | deferred.resolve = resolve; 171 | deferred.reject = reject; 172 | }); 173 | 174 | return deferred; 175 | } 176 | ``` 177 | 178 | Now we can create promises to resolve or reject and pass them to `TrackedAsyncData`: 179 | 180 | ```ts 181 | import { TrackedAsyncData } from 'ember-async-data'; 182 | 183 | let firstDeferred = defer(); 184 | let willResolve = new TrackedAsyncData(firstDeferred.promise); 185 | 186 | console.log(willResolve); 187 | /* 188 | TrackedAsyncData: { 189 | "isPending": true, 190 | "isResolved": false, 191 | "isRejected": false 192 | } 193 | */ 194 | 195 | await firstDeferred.resolve('potato'); 196 | console.log(willResolve); 197 | /* 198 | TrackedAsyncData: { 199 | "isPending": false, 200 | "isResolved": true, 201 | "value": "potato", 202 | "isRejected": false 203 | } 204 | */ 205 | 206 | // create another promise, this time to reject 207 | let secondDeferred = defer(); 208 | let willReject = new TrackedAsyncData(secondDeferred.promise); 209 | 210 | console.log(willReject); 211 | /* 212 | TrackedAsyncData: { 213 | "isPending": true, 214 | "isResolved": false, 215 | "isRejected": false 216 | } 217 | */ 218 | 219 | await secondDeferred.reject('wat'); 220 | console.log(willReject); 221 | /* 222 | TrackedAsyncData: { 223 | "isPending": false, 224 | "isResolved": false, 225 | "isRejected": true, 226 | "error": "wat" 227 | } 228 | */ 229 | ``` 230 | 231 | You can use `TrackedAsyncData` with *any* value, not just a `Promise`, which is convenient when working with data which may or may not already be in a `Promise`. 232 | 233 | 234 | #### With TypeScript 235 | 236 | This library provides full type safety for `TrackedAsyncData`; see [**API**](#api) below for details. The resulting `value` will always be of the same type as the `Promise` you pass in. Type narrowing works correctly: if you check the `.state` property or any of the `.isPending`, `.isResolved`, or `.isRejected` properties, the resulting type will have the correct corresponding types for `.value` and `.error`. 237 | 238 | - With `.state`: 239 | 240 | ```ts 241 | let example = new TrackedAsyncData(Promise.resolve({ theAnswer: 42 })); 242 | switch (example.state) { 243 | case 'PENDING': 244 | console.log(example.value?.theAnswer); // 🛑 WARN; type is `number | null` 245 | console.log(example.error); // 🛑 WARN 246 | break; 247 | case 'RESOLVED': 248 | console.log(example.value.theAnswer); // ✅ 249 | console.log(example.error); // 🛑 WARN 250 | break; 251 | case 'REJECTED': 252 | console.log(example.value?.theAnswer); // 🛑 WARN; type is `number | 253 | console.log(example.error); // ✅ 254 | break; 255 | default: 256 | assertUnreachable(example); // ✅ as long as all cases covered 257 | } 258 | ``` 259 | 260 | 261 | - With the boolean property checks `.isPending`, `.isResolved`, or `.isRejected`: 262 | 263 | ```ts 264 | let example = new TrackedAsyncData(Promise.resolve("a string")); 265 | if (example.isResolved) { 266 | console.log(example.value.length); // ✅ type is `string` 267 | } 268 | ``` 269 | 270 | 271 | #### Note on Usage with API Calls 272 | 273 | When using `TrackedAsyncData` with an API call in a getter, it is important to use [`@cached`](https://api.emberjs.com/ember/release/functions/@glimmer%2Ftracking/cached) (for Ember.js < 4.5 via the [ember-cached-decorator-polyfill](https://github.com/ember-polyfills/ember-cached-decorator-polyfill)) with the getter. Otherwise, you can end up triggering the creation of multiple API calls. For example, given a component like this: 274 | 275 | ```gts 276 | import Component from '@glimmer/component'; 277 | import { service } from '@ember/service'; 278 | import { TrackedAsyncData } from 'ember-async-data'; 279 | 280 | import type Store from '@ember-data/store'; 281 | 282 | interface ProfileSignature { 283 | Args: { 284 | userId: string; 285 | }; 286 | } 287 | 288 | export default class Profile extends Component { 289 | @service store: Store; 290 | 291 | get fullProfile() { 292 | return new TrackedAsyncData(this.store.findRecord('user', this.args.userId)); 293 | } 294 | 295 | 313 | } 314 | ``` 315 | 316 | If the template checks the `fullProfile` state in multiple places, it will invoke the getter multiple times per render: 317 | 318 | This code would invoke the getter twice on first render, which would therefore trigger two separate calls to the store, one of which would effectively be thrown away. Then, once the second call *did* resolve, it would invoke the getter multiple *more* times, and the result would be a sort of ping-ponging back and forth between pending and resolved states as a cascade of new API calls are triggered by each invocation of the getter. 319 | 320 | This is the *correct* default behavior, even though it might be surprising at first: 321 | 322 | - For getters and templates: in Octane, caching is something we layer onto getters where it makes sense to pay for them, rather than paying for them *everywhere* (as in Ember classic) even when that's far more costly than just rerunning the getter a couple times. For API calls, it always makes sense! 323 | - For the `TrackedAsyncData` API, this similarly means we don't pay for extra caching of arguments in the many cases we don't need it. 324 | 325 | _**Note:** in the future, we will make a set of [Resources](https://www.pzuraq.com/introducing-use/) layered on top of the core data types here, which will allow us to build in caching for API calls._ 326 | 327 | 328 | #### `load` function 329 | 330 | For symmetry with templates, you can also use `load` in JavaScript; it has the exact same semantics as calling `new TrackedAsyncData`. Using `load`, the example from the top of the README would look like this: 331 | 332 | ```ts 333 | import Component from '@glimmer/component'; 334 | import { cached } from '@glimmer/tracking'; 335 | import { inject as service } from '@ember/service'; 336 | import { load } from 'ember-async-data'; 337 | 338 | export default class SmartProfile extends Component { 339 | @service store; 340 | 341 | @cached 342 | get someData() { 343 | return load(this.store.findRecord('user', this.args.id)); 344 | } 345 | } 346 | ``` 347 | 348 | Note that this has the exact same requirements around API calls as the direct use of the constructor. 349 | 350 | #### Subclassing 351 | 352 | It is illegal to subclass `TrackedAsyncData`; trying to invoke a subclass will throw an error. 353 | 354 | 355 | ### In templates 356 | 357 | To use a `TrackedAsyncData` in templates, we provide the `load` helper. You can pass it any value, and it will return a `TrackedAsyncData` for that value. You can then use the `.isPending`, `.isResolved`, and `.isRejected` properties to conditionally render content based on the state of the promise. 358 | 359 | You could use this to build a component which uses named blocks to provide a nice API for end users: 360 | 361 | ```hbs 362 |
363 | {{#let (load @promise) as |result|}} 364 | {{#if result.isPending}} 365 |
366 | {{#if (has-block "pending")}} 367 | {{yield to="pending"}} 368 | {{else}} 369 | Loading... 370 | {{/if}} 371 |
372 | {{else if result.isResolved}} 373 |
374 | {{#if (has-block "resolved")}} 375 | {{yield result.value to="resolved"}} 376 | {{else}} 377 | {{result.value}} 378 | {{/if}} 379 |
380 | {{else if result.isRejected}} 381 |
382 | {{#if (has-block "rejected")}} 383 | {{yield result.error to="rejected"}} 384 | {{else}} 385 | {{result.error}} 386 | {{/if}} 387 |
388 | {{/if}} 389 | {{/let}} 390 |
391 | ``` 392 | 393 | Then callers could use it like this: 394 | 395 | ```hbs 396 | 397 | <:pending>Hang on, we’ll get that data for you! 398 | 399 | <:resolved as |value|> 400 | Cool! The value you asked for was: {{value}}. 401 | <:/resolve> 402 | 403 | <:rejected as |error|> 404 | Oh no, we couldn't get that data for you. Here's what we know: {{error}} 405 | <:/rejected> 406 | 407 | ``` 408 | 409 | This project ships [Glint](https://github.com/typed-ember/glint) types, 410 | which allow you when using TypeScript to get strict type checking in your templates. 411 | 412 | Unless you are using [strict mode](http://emberjs.github.io/rfcs/0496-handlebars-strict-mode.html) templates 413 | (via [first class component templates](http://emberjs.github.io/rfcs/0779-first-class-component-templates.html)), 414 | Glint needs a [Template Registry](https://typed-ember.gitbook.io/glint/using-glint/ember/template-registry) 415 | that contains entries for the template helper provided by this addon. 416 | To add these registry entries automatically to your app, you just need to import `ember-async-data/template-registry` 417 | from somewhere in your app. When using Glint already, you will likely have a file like 418 | `types/glint.d.ts` where you already import glint types, so just add the import there: 419 | 420 | ```ts 421 | import '@glint/environment-ember-loose'; 422 | import type EmberAsyncDataRegistry from 'ember-async-data/template-registry'; 423 | 424 | declare module '@glint/environment-ember-loose/registry' { 425 | export default interface Registry extends EmberAsyncDataRegistry, /* other addon registries */ { 426 | // local entries 427 | } 428 | } 429 | ``` 430 | 431 | > **Note:** Glint itself is still under active development, and as such breaking changes might occur. 432 | > Therefore, Glint support by this addon is also considered experimental, and not covered by our SemVer contract! 433 | 434 | ### Testing 435 | 436 | Working with the full range of behavior from `TrackedAsyncData` in tests will require you to manage the inherent asynchrony of the system much more explicitly than you may be used to. This is unavoidable: the point of the helper and data type *is* dealing with asynchrony in an explicit way. 437 | 438 | As a starting point, you can and should ask: "Is testing the a loading spinner when `someTrackedAsyncData.isLoading` something I actually need an automated test for, or can I verify it manually just once?" Depending on your app, your answer might be that just verifying it manually is a better use of your time! In other cases, testing that behavior might be essential: for example, if you are building an abstraction on top of `TrackedAsyncData` for *others* to consume. 439 | 440 | #### Unit testing 441 | 442 | Unit testing is straightforward. Using a tool like [`RSVP.defer()`](https://github.com/tildeio/rsvp.js/blob/master/lib/rsvp/defer.js), you can create a promise and control its resolution and verify that your use of `TrackedAsyncData` does what you need it to. Whenever you trigger a change in the state of the underlying promise, you will need to wait for promise resolution in the test. There are two ways to do this: 443 | 444 | 1. If you are working with a promise directly, you can simply `await` the promise itself. 445 | 2. If you are working with things which might or might not be promises, or working at a higher level of abstraction where you don’t have direct access to the promise, you can `await` the `settled()` helper from `@ember/test-helpers`, since `TrackedAsyncData` integrates into Ember’s test waiter system. 446 | 447 | ```js 448 | import { TrackedAsyncData } from 'ember-async-data'; 449 | import { defer } from 'rsvp'; 450 | import { module, test } from 'qunit'; 451 | import { settled } from '@ember/test-helpers'; 452 | 453 | module('my very own tests', function (hooks) { 454 | test('directly awaiting the promise works', async function (assert) { 455 | let { promise, resolve } = defer(); 456 | let asyncData = new TrackedAsyncData(promise); 457 | assert.true(asyncData.isPending); 458 | 459 | let expectedValue = "cool"; 460 | resolve(expectedValue); 461 | await promise; 462 | assert.true(asyncData.isResolved); 463 | asset.equal(asyncData.value, expectedValue); 464 | }); 465 | 466 | test('awaiting `settled` also works', async function (assert) { 467 | let { promise, resolve } = defer(); 468 | let asyncData = new TrackedAsyncData(promise); 469 | assert.true(asyncData.isPending); 470 | 471 | let expectedValue = "cool"; 472 | resolve(expectedValue); 473 | await settled(); 474 | assert.true(asyncData.isResolved); 475 | asset.equal(asyncData.value, expectedValue); 476 | }); 477 | }); 478 | ``` 479 | 480 | Handling errors is slightly more complicated: `TrackedAsyncData` “re-throws” the promises it works with when they have errors, to avoid silently swallowing them in a way that prevents you from using them with logging infrastructure or otherwise dealing with them in your app’s infrastructure. However, this means you must also account for them in your testing: 481 | 482 | ```js 483 | test('it handles errors', async function (assert) { 484 | assert.expect(2); 485 | 486 | let { promise, reject } = defer(); 487 | let asyncData = new TrackedAsyncData(promise); 488 | 489 | reject(new Error("this is the error!")); 490 | await promise.catch((error) => { 491 | assert.equal(error.message, "this is the error!"); 492 | }); 493 | 494 | assert.true(asyncData.isRejected); 495 | assert.equal(asyncData.error.message, "this is the error!"); 496 | }); 497 | ``` 498 | 499 | #### Integration testing 500 | 501 | Integration/render tests are similar to those with unit testing, but with an additional wrinkle: all of Ember’s integration test helpers are *also* asynchronous, and the asynchrony of those test helpers and the `TrackedAsyncData` become “entangled” when they interact. That is: when you render something which depends on the state of the `TrackedAsyncData`, the promise which `TrackedAsyncData` is handling and the promise for rendering are effectively tied together. 502 | 503 | So this code, which you might write if you haven’t dealt with this before, ***WILL NOT WORK***: 504 | 505 | ```gjs 506 | import { TrackedAsyncData } from 'ember-async-data'; 507 | import { defer } from 'rsvp'; 508 | import { module, test } from 'qunit'; 509 | import { setupRenderingTest } from '@ember/test-helpers'; 510 | import { render } from "@ember/test-helpers"; 511 | 512 | module('my very own tests', function (hooks) { 513 | setupRenderingTest(hooks); 514 | 515 | test('THIS DOES NOT WORK', async function (assert) { 516 | const { promise, resolve } = defer(); 517 | const data = new TrackedAsyncData(promise); 518 | 519 | await render( 520 | 531 | `); 532 | 533 | assert.dom('[data-test-data]').hasText('Loading...'); 534 | }); 535 | }); 536 | ``` 537 | 538 | This test will simply time out without ever having finished, because the test waiters in the render promise and the test waiters in `TrackedAsyncData` are tangled up together. Instead, we need to separate the rendering promise from the promise in `TrackedAsyncData`. We can instead render the template and, instead of waiting for *that* promise to resolve, use Ember’s `waitFor` helper to wait for the *results* of rendering to happen. Then when we are done dealing with the promise, we can resolve it and *then* `await` the result of the render promise. This will let the test clean up correctly: 539 | 540 | ```js 541 | import { TrackedAsyncData } from 'ember-async-data'; 542 | import { defer } from 'rsvp'; 543 | import { module, test } from 'qunit'; 544 | import { setupRenderingTest } from '@ember/test-helpers'; 545 | import { render, waitFor } from "@ember/test-helpers"; 546 | 547 | module('my very own tests', function (hooks) { 548 | setupRenderingTest(hooks); 549 | 550 | test('this actually works', async function (assert) { 551 | const { promise, resolve } = defer(); 552 | const data = new TrackedAsyncData(promise); 553 | 554 | const renderPromise = render( 555 | 566 | ); 567 | 568 | // Here we waits for the *result* of rendering, rather than the render 569 | // promise itself. Once we have rendered, we can make assertions about 570 | // what rendered: 571 | await waitFor('[data-test-data]'); 572 | assert.dom('[data-test-data]').hasText('Loading...'); 573 | 574 | // Then to clean up the test, we need the original promise to resolve 575 | // so the test waiter system isn't just stuck waiting for it forever. 576 | resolve(); 577 | // Finally, we 578 | await renderPromise; 579 | }); 580 | }); 581 | ``` 582 | 583 | While this might seem a bit annoying, it means that we actually *can* control all the related bits of asynchronous code that we need, and—more just as importantly—it means we avoid leaking promises across tests (a common cause of test instability) and means that in general tests following the “happy path” *don’t* have to worry about managing this asynchrony themselves. 584 | 585 | For that happy path, you can use a *resolved* `TrackedAsyncData` and everything will always “just work” as you’d expect: 586 | 587 | ```gjs 588 | test('the "happy path" works easily', async function (assert) { 589 | const data = new TrackedAsyncData(Promise.resolve("a value")); 590 | 591 | await render( 592 | 603 | ); 604 | 605 | assert.dom('[data-test-data]').hasText('Loaded: a value'); 606 | }); 607 | ``` 608 | 609 | In other words, the only time you have to care about the details of handling async in your tests is when you want to render and step through the different async states explicitly. 610 | 611 | ## API 612 | 613 | You can currently use this in three distinct ways: 614 | 615 | 1. By using the `TrackedAsyncData` class directly in JavaScript. 616 | 2. With the `load` utility function exported from the helper file. (This is not preferred, but exists for backwards compatibility and symmetry with the helper, until we have a `Resource`-style API available.) 617 | 3. With the `{{load}}` helper in templates. 618 | 619 | 620 | ### `TrackedAsyncData` 621 | 622 | The public API for `TrackedAsyncData`: 623 | 624 | ```ts 625 | class TrackedAsyncData { 626 | constructor(data: T | Promise); 627 | 628 | get state(): "PENDING" | "RESOLVED" | "REJECTED"; 629 | get isPending(): boolean; 630 | get isResolved(): boolean; 631 | get isRejected(): boolean; 632 | 633 | // Only available if `isResolved`. 634 | get value(): T | null; 635 | 636 | // Only available if `isRejected`. 637 | get error(): unknown; 638 | } 639 | ``` 640 | 641 | 642 | #### Notes 643 | 644 | - `value` is `T | null` today, but only for the sake of safe interop with Ember Classic computed properties (which eagerly evaluate getters for the sake of). You *should not* rely on the `null` fallback, as accessing `value` when `isResolved` is false throws a hard error starting with v2.0. The same is true of `error`. 645 | - The class is *not* intended for subclassing, and will in fact throw in the constructor if you try to subclass it! 646 | - The `value` and `error` getters will *throw an error* if you access them when the underlying promise is in the wrong state. 647 | 648 | 649 | ### `load` function 650 | 651 | The `load` helper function is basically just a static constructor for `TrackedAsyncData`: 652 | 653 | ```ts 654 | function load(data: T | Promise): TrackedAsyncData; 655 | ``` 656 | 657 | 658 | ### In templates 659 | 660 | The `{{load}}` helper is identical to the `load` function but in template space: it accepts a single positional parameter of a promise as its only argument, and yields a `TrackedAsyncData` for that promise. (See usage examples [above](#in-templates).) 661 | 662 | ## Explanation 663 | 664 | For a deep dive, see this pair of blog posts (the API has changed slightly since these were authored, but the fundamental ideas are the same): 665 | 666 | - [Migrating Off of `PromiseProxyMixin` in Ember Octane](https://v5.chriskrycho.com/journal/migrating-off-of-promiseproxymixin-in-ember-octane/) 667 | - [Async Data and Autotracking in Ember Octane](https://v5.chriskrycho.com/journal/async-data-and-autotracking-in-ember-octane/) 668 | 669 | You can think of this as an autotracked, Ember-friendly implementation of the idea in [How Elm Slays a UI Antipattern](http://blog.jenkster.com/2016/06/how-elm-slays-a-ui-antipattern.html). 670 | 671 | ### Background and history 672 | 673 | In the past, Ember developers tended to reach for `PromiseProxyMixin` to solve this problem. However, `PromiseProxyMixin` has two significant issues: 674 | 675 | 1. Because it directly proxies to the underlying promise values, it is possible to misuse by accessing the properties of the resolved type synchronously, instead of properly awaiting the value of the promise (either with `await` or with `.then()`). 676 | 2. Because it is a mixin, it cannot be used with Glimmer components, and in general is not compatible with the future direction of Ember, which is moving *away* from mixins and *toward* appropriate use of delegates (whether services or non-singleton regular classes), composition, and pure functions. 677 | 678 | The `load` helper is a fully-featured replacement for `PromiseProxyMixin`, with none of these pitfalls. This takes a promise as a parameter and returns a `TrackedAsyncData` object which handles the pending/resolved/rejected state as well as value/error data. The associated `load` helper provides a consistent way for developers to load data in their component templates. 679 | 680 | 681 | ## Contributing 682 | 683 | See the [Contributing](CONTRIBUTING.md) guide for details. 684 | 685 | ## License 686 | 687 | This project is licensed under the [MIT License](LICENSE.md). 688 | 689 | ## Credit 690 | 691 | Research into the ideas behind this code happened at [LinkedIn](https://www.linkedin.com), with key work done by [Yaobin Dong](https://www.linkedin.com/in/yaobin-dong-8a881481/), [Lucy Lin](https://www.linkedin.com/in/lucylylin/), [Lewis Miller](https://www.linkedin.com/in/lewis-miller/), and [Chris Krycho](https://www.linkedin.com/in/chriskrycho/). While this new implementation is distinct from that one in many ways, it would have been impossible without those initial efforts! 692 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | Releases in this repo are mostly automated using [release-plan](https://github.com/embroider-build/release-plan/). Once you label all your PRs correctly (see below) you will have an automatically generated PR that updates your CHANGELOG.md file and a `.release-plan.json` that is used to prepare the release once the PR is merged. 4 | 5 | ## Preparation 6 | 7 | Since the majority of the actual release process is automated, the remaining tasks before releasing are: 8 | 9 | - correctly labeling **all** pull requests that have been merged since the last release 10 | - updating pull request titles so they make sense to our users 11 | 12 | Some great information on why this is important can be found at [keepachangelog.com](https://keepachangelog.com/en/1.1.0/), but the overall 13 | guiding principle here is that changelogs are for humans, not machines. 14 | 15 | When reviewing merged PR's the labels to be used are: 16 | 17 | - breaking - Used when the PR is considered a breaking change. 18 | - enhancement - Used when the PR adds a new feature or enhancement. 19 | - bug - Used when the PR fixes a bug included in a previous release. 20 | - documentation - Used when the PR adds or updates documentation. 21 | - internal - Internal changes or things that don't fit in any other category. 22 | 23 | **Note:** `release-plan` requires that **all** PRs are labeled. If a PR doesn't fit in a category it's fine to label it as `internal` 24 | 25 | ## Release 26 | 27 | Once the prep work is completed, the actual release is straight forward: you just need to merge the open [Plan Release](https://github.com/tracked-tools/ember-async-data/pulls?q=is%3Apr+is%3Aopen+%22Prepare+Release%22+in%3Atitle) PR 28 | -------------------------------------------------------------------------------- /config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "@embroider/addon-blueprint", 6 | "version": "4.1.0", 7 | "blueprints": [ 8 | { 9 | "name": "@embroider/addon-blueprint", 10 | "isBaseBlueprint": true, 11 | "options": [ 12 | "--pnpm", 13 | "--ci-provider=github", 14 | "--addon-location=ember-async-data", 15 | "--test-app-location=test-app", 16 | "--typescript" 17 | ] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /ember-async-data/.gitignore: -------------------------------------------------------------------------------- 1 | # The authoritative copies of these live in the monorepo root (because they're 2 | # more useful on github that way), but the build copies them into here so they 3 | # will also appear in published NPM packages. 4 | /README.md 5 | /LICENSE.md 6 | 7 | # compiled output 8 | dist/ 9 | declarations/ 10 | 11 | # npm/pnpm/yarn pack output 12 | *.tgz 13 | 14 | # deps & caches 15 | node_modules/ 16 | .eslintcache 17 | .prettiercache 18 | -------------------------------------------------------------------------------- /ember-async-data/.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /dist/ 6 | /declarations/ 7 | 8 | # misc 9 | /coverage/ 10 | -------------------------------------------------------------------------------- /ember-async-data/.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | plugins: ['prettier-plugin-ember-template-tag'], 5 | overrides: [ 6 | { 7 | files: '*.{js,gjs,ts,gts,mjs,mts,cjs,cts}', 8 | options: { 9 | singleQuote: true, 10 | templateSingleQuote: false, 11 | }, 12 | }, 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /ember-async-data/.template-lintrc.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended', 5 | }; 6 | -------------------------------------------------------------------------------- /ember-async-data/addon-main.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { addonV1Shim } = require('@embroider/addon-shim'); 4 | module.exports = addonV1Shim(__dirname); 5 | -------------------------------------------------------------------------------- /ember-async-data/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "@babel/plugin-transform-typescript", 5 | { 6 | "allExtensions": true, 7 | "onlyRemoveTypeImports": true, 8 | "allowDeclareFields": true 9 | } 10 | ], 11 | "@embroider/addon-dev/template-colocation-plugin", 12 | [ 13 | "babel-plugin-ember-template-compilation", 14 | { 15 | "targetFormat": "hbs", 16 | "transforms": [] 17 | } 18 | ], 19 | [ 20 | "module:decorator-transforms", 21 | { "runtime": { "import": "decorator-transforms/runtime" } } 22 | ] 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /ember-async-data/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Debugging: 3 | * https://eslint.org/docs/latest/use/configure/debug 4 | * ---------------------------------------------------- 5 | * 6 | * Print a file's calculated configuration 7 | * 8 | * npx eslint --print-config path/to/file.js 9 | * 10 | * Inspecting the config 11 | * 12 | * npx eslint --inspect-config 13 | * 14 | */ 15 | import babelParser from '@babel/eslint-parser'; 16 | import js from '@eslint/js'; 17 | import prettier from 'eslint-config-prettier'; 18 | import ember from 'eslint-plugin-ember/recommended'; 19 | import importPlugin from 'eslint-plugin-import'; 20 | import n from 'eslint-plugin-n'; 21 | import globals from 'globals'; 22 | import ts from 'typescript-eslint'; 23 | 24 | const parserOptions = { 25 | esm: { 26 | js: { 27 | ecmaFeatures: { modules: true }, 28 | ecmaVersion: 'latest', 29 | }, 30 | ts: { 31 | projectService: true, 32 | project: true, 33 | tsconfigRootDir: import.meta.dirname, 34 | }, 35 | }, 36 | }; 37 | 38 | export default ts.config( 39 | js.configs.recommended, 40 | ember.configs.base, 41 | ember.configs.gjs, 42 | ember.configs.gts, 43 | prettier, 44 | /** 45 | * Ignores must be in their own object 46 | * https://eslint.org/docs/latest/use/configure/ignore 47 | */ 48 | { 49 | ignores: ['dist/', 'declarations/', 'node_modules/', 'coverage/', '!**/.*'], 50 | }, 51 | /** 52 | * https://eslint.org/docs/latest/use/configure/configuration-files#configuring-linter-options 53 | */ 54 | { 55 | linterOptions: { 56 | reportUnusedDisableDirectives: 'error', 57 | }, 58 | }, 59 | { 60 | files: ['**/*.js'], 61 | languageOptions: { 62 | parser: babelParser, 63 | }, 64 | }, 65 | { 66 | files: ['**/*.{js,gjs}'], 67 | languageOptions: { 68 | parserOptions: parserOptions.esm.js, 69 | globals: { 70 | ...globals.browser, 71 | }, 72 | }, 73 | }, 74 | { 75 | files: ['**/*.{ts,gts}'], 76 | languageOptions: { 77 | parser: ember.parser, 78 | parserOptions: parserOptions.esm.ts, 79 | }, 80 | extends: [...ts.configs.recommendedTypeChecked, ember.configs.gts], 81 | }, 82 | { 83 | files: ['src/**/*'], 84 | plugins: { 85 | import: importPlugin, 86 | }, 87 | rules: { 88 | // require relative imports use full extensions 89 | 'import/extensions': ['error', 'always', { ignorePackages: true }], 90 | }, 91 | }, 92 | /** 93 | * CJS node files 94 | */ 95 | { 96 | files: [ 97 | '**/*.cjs', 98 | '.prettierrc.js', 99 | '.stylelintrc.js', 100 | '.template-lintrc.js', 101 | 'addon-main.cjs', 102 | ], 103 | plugins: { 104 | n, 105 | }, 106 | 107 | languageOptions: { 108 | sourceType: 'script', 109 | ecmaVersion: 'latest', 110 | globals: { 111 | ...globals.node, 112 | }, 113 | }, 114 | }, 115 | /** 116 | * ESM node files 117 | */ 118 | { 119 | files: ['**/*.mjs'], 120 | plugins: { 121 | n, 122 | }, 123 | 124 | languageOptions: { 125 | sourceType: 'module', 126 | ecmaVersion: 'latest', 127 | parserOptions: parserOptions.esm.js, 128 | globals: { 129 | ...globals.node, 130 | }, 131 | }, 132 | }, 133 | ); 134 | -------------------------------------------------------------------------------- /ember-async-data/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-async-data", 3 | "version": "2.0.1", 4 | "description": "A utility/helper and data structure for representing a `Promise` in a declarative, reactive way in Glimmer.js and Ember Octane.", 5 | "keywords": [ 6 | "ember-addon" 7 | ], 8 | "repository": "https://github.com/tracked-tools/ember-async-data", 9 | "license": "MIT", 10 | "author": { 11 | "name": "Chris Krycho", 12 | "email": "hello@chriskrycho.com", 13 | "url": "https://v5.chriskrycho.com/" 14 | }, 15 | "exports": { 16 | ".": { 17 | "types": "./declarations/index.d.ts", 18 | "default": "./dist/index.js" 19 | }, 20 | "./*": { 21 | "types": "./declarations/*.d.ts", 22 | "default": "./dist/*.js" 23 | }, 24 | "./addon-main.js": "./addon-main.cjs" 25 | }, 26 | "typesVersions": { 27 | "*": { 28 | "*": [ 29 | "declarations/*" 30 | ] 31 | } 32 | }, 33 | "files": [ 34 | "addon-main.cjs", 35 | "declarations", 36 | "dist" 37 | ], 38 | "scripts": { 39 | "build": "rollup --config", 40 | "format": "prettier . --cache --write", 41 | "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", 42 | "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" --prefixColors auto && pnpm run format", 43 | "lint:format": "prettier . --cache --check", 44 | "lint:hbs": "ember-template-lint . --no-error-on-unmatched-pattern", 45 | "lint:hbs:fix": "ember-template-lint . --fix --no-error-on-unmatched-pattern", 46 | "lint:js": "eslint . --cache", 47 | "lint:js:fix": "eslint . --fix", 48 | "lint:types": "glint", 49 | "prepack": "rollup --config", 50 | "start": "rollup --config --watch", 51 | "test": "echo 'A v2 addon does not have tests, run tests in test-app'" 52 | }, 53 | "dependencies": { 54 | "@ember/test-waiters": "^4.1.0", 55 | "@embroider/addon-shim": "^1.10.0", 56 | "decorator-transforms": "^2.2.2" 57 | }, 58 | "devDependencies": { 59 | "@babel/core": "^7.26.10", 60 | "@babel/eslint-parser": "^7.26.8", 61 | "@babel/plugin-transform-typescript": "^7.26.8", 62 | "@babel/runtime": "^7.26.9", 63 | "@embroider/addon-dev": "^7.1.4", 64 | "@eslint/js": "^9.21.0", 65 | "@glint/core": "^1.5.2", 66 | "@glint/environment-ember-loose": "^1.5.2", 67 | "@glint/environment-ember-template-imports": "^1.5.2", 68 | "@glint/template": "^1.5.2", 69 | "@rollup/plugin-babel": "^6.0.4", 70 | "@tsconfig/ember": "^3.0.8", 71 | "babel-plugin-ember-template-compilation": "^2.2.5", 72 | "concurrently": "^9.0.1", 73 | "ember-source": "^6.3.0", 74 | "ember-template-lint": "^7.0.1", 75 | "eslint": "^9.22.0", 76 | "eslint-config-prettier": "^10.1.1", 77 | "eslint-plugin-ember": "^12.3.3", 78 | "eslint-plugin-import": "^2.31.0", 79 | "eslint-plugin-n": "^17.15.1", 80 | "expect-type": "^0.18.0", 81 | "globals": "^16.0.0", 82 | "prettier": "^3.5.3", 83 | "prettier-plugin-ember-template-tag": "^2.0.4", 84 | "rollup": "^4.35.0", 85 | "rollup-plugin-copy": "^3.5.0", 86 | "typescript": "~5.7.3", 87 | "typescript-eslint": "^8.25.0", 88 | "webpack": "^5.90.3" 89 | }, 90 | "volta": { 91 | "extends": "../package.json" 92 | }, 93 | "ember": { 94 | "edition": "octane" 95 | }, 96 | "ember-addon": { 97 | "version": 2, 98 | "type": "addon", 99 | "main": "addon-main.cjs", 100 | "versionCompatibility": { 101 | "ember": "3.28 || >=4.0" 102 | }, 103 | "app-js": { 104 | "./helpers/load.js": "./dist/_app_/helpers/load.js" 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /ember-async-data/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { babel } from '@rollup/plugin-babel'; 2 | import copy from 'rollup-plugin-copy'; 3 | import { Addon } from '@embroider/addon-dev/rollup'; 4 | 5 | const addon = new Addon({ 6 | srcDir: 'src', 7 | destDir: 'dist', 8 | }); 9 | 10 | export default { 11 | // This provides defaults that work well alongside `publicEntrypoints` below. 12 | // You can augment this if you need to. 13 | output: addon.output(), 14 | 15 | plugins: [ 16 | // These are the modules that users should be able to import from your 17 | // addon. Anything not listed here may get optimized away. 18 | // By default, all your JavaScript modules (**/*.js) will be importable. 19 | // But you are encouraged to tweak this to only cover the modules that make 20 | // up your addon's public API. Also make sure your package.json#exports 21 | // is aligned to the config here. 22 | // See https://github.com/embroider-build/embroider/blob/main/docs/v2-faq.md#how-can-i-define-the-public-exports-of-my-addon 23 | addon.publicEntrypoints([ 24 | 'helpers/**/*.js', 25 | 'index.js', 26 | 'template-registry.js', 27 | 'tracked-async-data.js', 28 | ]), 29 | 30 | // These are the modules that should get reexported into the traditional 31 | // "app" tree. Things in here should also be in publicEntrypoints above, but 32 | // not everything in publicEntrypoints necessarily needs to go here. 33 | addon.appReexports(['helpers/**/*.js']), 34 | 35 | // Follow the V2 Addon rules about dependencies. Your code can import from 36 | // `dependencies` and `peerDependencies` as well as standard Ember-provided 37 | // package names. 38 | addon.dependencies(), 39 | 40 | // This babel config should *not* apply presets or compile away ES modules. 41 | // It exists only to provide development niceties for you, like automatic 42 | // template colocation. 43 | // 44 | // By default, this will load the actual babel config from the file 45 | // babel.config.json. 46 | babel({ 47 | extensions: ['.js', '.gjs', '.ts', '.gts'], 48 | babelHelpers: 'bundled', 49 | }), 50 | 51 | // Ensure that standalone .hbs files are properly integrated as Javascript. 52 | addon.hbs(), 53 | 54 | // Ensure that .gjs files are properly integrated as Javascript 55 | addon.gjs(), 56 | 57 | // Emit .d.ts declaration files 58 | addon.declarations('declarations'), 59 | 60 | // addons are allowed to contain imports of .css files, which we want rollup 61 | // to leave alone and keep in the published output. 62 | addon.keepAssets(['**/*.css']), 63 | 64 | // Remove leftover build artifacts when starting a new build. 65 | addon.clean(), 66 | 67 | // Copy Readme and License into published package 68 | copy({ 69 | targets: [ 70 | { src: '../README.md', dest: '.' }, 71 | { src: '../LICENSE.md', dest: '.' }, 72 | ], 73 | }), 74 | ], 75 | }; 76 | -------------------------------------------------------------------------------- /ember-async-data/src/helpers/load.ts: -------------------------------------------------------------------------------- 1 | import TrackedAsyncData from '../tracked-async-data.ts'; 2 | 3 | /** 4 | Given a `Promise`, return a `TrackedAsyncData` object which exposes the state 5 | of the promise, as well as the resolved value or thrown error once the promise 6 | resolves or fails. 7 | 8 | The function and helper accept any data, so you may use it freely in contexts 9 | where you are receiving data which may or may not be a `Promise`. 10 | 11 | ## Example 12 | 13 | Given a component class like this, you can use the result in your template: 14 | 15 | ```gjs 16 | import Component from '@glimmer/component'; 17 | import { cached } from 'ember-cached-decorator-polyfill'; 18 | import { load } from 'ember-tracked-data/helpers/load'; 19 | 20 | export default class ExtraInfo extends Component { 21 | @cached 22 | get someData() { 23 | return load(fetch('some-url', this.args.someArg)); 24 | } 25 | 26 | 35 | } 36 | ``` 37 | 38 | You can also use the helper directly in your template: 39 | 40 | ```hbs 41 | {{#let (load @somePromise) as |data|}} 42 | {{#if data.isLoading}} 43 | 44 | {{else if data.isLoaded}} 45 | 46 | {{else if data.isError}} 47 | 48 | {{/if}} 49 | {{/let}} 50 | ``` 51 | 52 | @param data The (async) data we want to operate on: a value or a `Promise` of 53 | a value. 54 | @returns An object containing the state(, value, and error). 55 | @note Prefer to use `TrackedAsyncData` directly! This function is provided 56 | simply for symmetry with the helper and backwards compatibility. 57 | */ 58 | function load(data: T | Promise): TrackedAsyncData { 59 | return new TrackedAsyncData(data); 60 | } 61 | export { load, load as default }; 62 | -------------------------------------------------------------------------------- /ember-async-data/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as TrackedAsyncData } from './tracked-async-data.ts'; 2 | export { load } from './helpers/load.ts'; 3 | -------------------------------------------------------------------------------- /ember-async-data/src/template-registry.ts: -------------------------------------------------------------------------------- 1 | // Easily allow apps, which are not yet using strict mode templates, to consume your Glint types, by importing this file. 2 | // Add all your components, helpers and modifiers to the template registry here, so apps don't have to do this. 3 | // See https://typed-ember.gitbook.io/glint/environments/ember/authoring-addons 4 | 5 | import type Load from './helpers/load.ts'; 6 | 7 | export default interface Registry { 8 | load: typeof Load; 9 | } 10 | -------------------------------------------------------------------------------- /ember-async-data/src/tracked-async-data.ts: -------------------------------------------------------------------------------- 1 | import { tracked } from '@glimmer/tracking'; 2 | import { assert } from '@ember/debug'; 3 | import { buildWaiter } from '@ember/test-waiters'; 4 | 5 | const waiter = buildWaiter('ember-async-data'); 6 | 7 | /** A very cheap representation of the of a promise. */ 8 | type StateRepr = 9 | | [tag: 'PENDING'] 10 | | [tag: 'RESOLVED', value: T] 11 | | [tag: 'REJECTED', error: unknown]; 12 | 13 | // We only need a single instance of the pending state in our system, since it 14 | // is otherwise unparameterized (unlike the resolved and rejected states). 15 | const PENDING = ['PENDING'] as [tag: 'PENDING']; 16 | 17 | // This class exists so that the state can be *wholly* private to outside 18 | // consumers, but its tracked internals can be both read and written directly by 19 | // `TrackedAsyncData` itself. The initial state of every `TrackedAsyncData` is 20 | // `PENDING`, though it may immediately become resolved for some `Promise` 21 | // instances (e.g. with a `Promise.resolve`). 22 | class State { 23 | @tracked data: StateRepr = PENDING; 24 | } 25 | 26 | // NOTE: this class is the implementation behind the types; the public types 27 | // layer on additional safety. See below! Additionally, the docs for the class 28 | // itself are applied to the export, not to the class, so that they will appear 29 | // when users refer to *that*. 30 | class _TrackedAsyncData { 31 | #token: unknown; 32 | 33 | /** 34 | @param promise The promise to load. 35 | */ 36 | constructor(data: T | Promise) { 37 | if (this.constructor !== _TrackedAsyncData) { 38 | throw new Error('tracked-async-data cannot be subclassed'); 39 | } 40 | 41 | if (!isPromiseLike(data)) { 42 | this.#state.data = ['RESOLVED', data]; 43 | return; 44 | } 45 | 46 | const promise = data; 47 | this.#token = waiter.beginAsync(); 48 | 49 | // Otherwise, we know that haven't yet handled that promise anywhere in the 50 | // system, so we continue creating a new instance. 51 | promise.then( 52 | (value) => { 53 | this.#state.data = ['RESOLVED', value]; 54 | waiter.endAsync(this.#token); 55 | }, 56 | (error) => { 57 | this.#state.data = ['REJECTED', error]; 58 | waiter.endAsync(this.#token); 59 | }, 60 | ); 61 | } 62 | 63 | /** 64 | The internal state management for the promise. 65 | 66 | - `readonly` so it cannot be mutated by the class itself after instantiation 67 | - uses true native privacy so it cannot even be read (and therefore *cannot* 68 | be depended upon) by consumers. 69 | */ 70 | readonly #state = new State(); 71 | 72 | /** 73 | * The resolution state of the promise. 74 | */ 75 | get state(): State['data'][0] { 76 | return this.#state.data[0]; 77 | } 78 | 79 | /** 80 | The value of the resolved promise. 81 | 82 | @note It is only valid to access `error` when `.isError` is true, that is, 83 | when `TrackedAsyncData.state` is `"ERROR"`. 84 | @warning You should not rely on this returning `T | null`! 85 | */ 86 | get value(): T | null { 87 | assert( 88 | "Accessing `value` when TrackedAsyncData is not in the resolved state is not supported. Always check that `.state` is `'RESOLVED'` or that `.isResolved` is `true` before accessing this property.", 89 | this.#state.data[0] === 'RESOLVED', 90 | ); 91 | 92 | return this.#state.data[0] === 'RESOLVED' ? this.#state.data[1] : null; 93 | } 94 | 95 | /** 96 | The error of the rejected promise. 97 | 98 | @note It is only valid to access `error` when `.isError` is true, that is, 99 | when `TrackedAsyncData.state` is `"ERROR"`. 100 | @warning You should not rely on this returning `null` when the state is not 101 | `"ERROR"`! 102 | */ 103 | get error(): unknown { 104 | assert( 105 | "Accessing `error` when TrackedAsyncData is not in the rejected state is not supported. Always check that `.state` is `'REJECTED'` or that `.isRejected` is `true` before accessing this property.", 106 | this.#state.data[0] === 'REJECTED', 107 | ); 108 | 109 | return this.#state.data[0] === 'REJECTED' ? this.#state.data[1] : null; 110 | } 111 | 112 | /** 113 | Is the state `"PENDING"`. 114 | */ 115 | get isPending(): boolean { 116 | return this.state === 'PENDING'; 117 | } 118 | 119 | /** Is the state `"RESOLVED"`? */ 120 | get isResolved(): boolean { 121 | return this.state === 'RESOLVED'; 122 | } 123 | 124 | /** Is the state `"REJECTED"`? */ 125 | get isRejected(): boolean { 126 | return this.state === 'REJECTED'; 127 | } 128 | 129 | // SAFETY: casts are safe because we uphold these invariants elsewhere in the 130 | // class. It would be great if we could guarantee them statically, but getters 131 | // do not return information about the state of the class well. 132 | toJSON(): JSONRepr { 133 | const { isPending, isResolved, isRejected } = this; 134 | if (isPending) { 135 | return { isPending, isResolved, isRejected } as JSONRepr; 136 | } else if (isResolved) { 137 | return { 138 | isPending, 139 | isResolved, 140 | value: this.value, 141 | isRejected, 142 | } as JSONRepr; 143 | } else { 144 | return { 145 | isPending, 146 | isResolved, 147 | isRejected, 148 | error: this.error, 149 | } as JSONRepr; 150 | } 151 | } 152 | 153 | toString(): string { 154 | return JSON.stringify(this.toJSON(), null, 2); 155 | } 156 | } 157 | 158 | /** 159 | The JSON representation of a `TrackedAsyncData`, useful for e.g. logging. 160 | 161 | Note that you cannot reconstruct a `TrackedAsyncData` *from* this, because it 162 | is impossible to get the original promise when in a pending state! 163 | */ 164 | export type JSONRepr = 165 | | { isPending: true; isResolved: false; isRejected: false } 166 | | { isPending: false; isResolved: true; isRejected: false; value: T } 167 | | { isPending: false; isResolved: false; isRejected: true; error: unknown }; 168 | 169 | // The exported type is the intersection of three narrowed interfaces. Doing it 170 | // this way has two nice benefits: 171 | // 172 | // 1. It allows narrowing to work. For example: 173 | // 174 | // ```ts 175 | // let data = new TrackedAsyncData(Promise.resolve("hello")); 176 | // if (data.isPending) { 177 | // data.value; // null 178 | // data.error; // null 179 | // } else if (data.isPending) { 180 | // data.value; // null 181 | // data.error; // null 182 | // } else if (data.isRejected) { 183 | // data.value; // null 184 | // data.error; // unknown, can now be narrowed 185 | // } 186 | // ``` 187 | // 188 | // This dramatically improves the usability of the type in type-aware 189 | // contexts (including with templates when using Glint!) 190 | // 191 | // 2. Using `interface extends` means that (a) it is guaranteed to be a subtype 192 | // of the `_TrackedAsyncData` type, (b) that the docstrings applied to the 193 | // base type still work, and (c) that the types which are *common* to the 194 | // shared implementations (i.e. `.toJSON()` and `.toString()`) are shared 195 | // automatically. 196 | 197 | interface Pending extends _TrackedAsyncData { 198 | state: 'PENDING'; 199 | isPending: true; 200 | isResolved: false; 201 | isRejected: false; 202 | value: null; 203 | error: null; 204 | } 205 | 206 | interface Resolved extends _TrackedAsyncData { 207 | state: 'RESOLVED'; 208 | isPending: false; 209 | isResolved: true; 210 | isRejected: false; 211 | value: T; 212 | error: null; 213 | } 214 | 215 | interface Rejected extends _TrackedAsyncData { 216 | state: 'REJECTED'; 217 | isPending: false; 218 | isResolved: false; 219 | isRejected: true; 220 | value: null; 221 | error: unknown; 222 | } 223 | 224 | /** 225 | An autotracked `Promise` handler, representing asynchronous data. 226 | 227 | Given a `Promise` instance, a `TrackedAsyncData` behaves exactly lik the 228 | original `Promise`, except that it makes the state of the `Promise` visible 229 | via tracked state, so you can check whether the promise is pending, resolved, 230 | or rejected; and so you can get the value if it has resolved or the error if 231 | it has rejected. 232 | 233 | Every `Promise` in the system is guaranteed to be associated with at most a 234 | single `TrackedAsyncData`. 235 | 236 | ## Example 237 | 238 | ```ts 239 | import Component from '@glimmer/component'; 240 | import { cached } from '@glimmer/tracking'; 241 | import { inject as service } from '@ember/service'; 242 | import TrackedAsyncData from 'ember-async-data/tracked-async-data'; 243 | 244 | export default class SmartProfile extends Component<{ id: number }> { 245 | @service store; 246 | 247 | @cached 248 | get someData() { 249 | let recordPromise = this.store.findRecord('user', this.args.id); 250 | return new TrackedAsyncData(recordPromise); 251 | } 252 | } 253 | ``` 254 | 255 | And a corresponding template: 256 | 257 | ```hbs 258 | {{#if this.someData.isResolved}} 259 | 260 | {{else if this.someData.isPending}} 261 | 262 | {{else if this.someData.isRejected}} 263 |

264 | Whoops! Looks like something went wrong! 265 | {{this.someData.error.message}} 266 |

267 | {{/if}} 268 | ``` 269 | */ 270 | type TrackedAsyncData = Pending | Resolved | Rejected; 271 | const TrackedAsyncData = _TrackedAsyncData as new ( 272 | data: T | Promise, 273 | ) => TrackedAsyncData; 274 | export default TrackedAsyncData; 275 | 276 | /** Utility type to check whether the string `key` is a property on an object */ 277 | function has( 278 | key: K, 279 | t: T, 280 | ): t is T & Record { 281 | return key in t; 282 | } 283 | 284 | function isPromiseLike(data: unknown): data is PromiseLike { 285 | return ( 286 | typeof data === 'object' && 287 | data !== null && 288 | has('then', data) && 289 | typeof data.then === 'function' 290 | ); 291 | } 292 | -------------------------------------------------------------------------------- /ember-async-data/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/ember/tsconfig.json", 3 | "include": ["src/**/*", "unpublished-development-types/**/*"], 4 | "glint": { 5 | "environment": ["ember-loose", "ember-template-imports"] 6 | }, 7 | "compilerOptions": { 8 | "allowJs": true, 9 | "declarationDir": "declarations", 10 | 11 | /** 12 | https://www.typescriptlang.org/tsconfig#noEmit 13 | 14 | We want to emit declarations, so this option must be set to `false`. 15 | @tsconfig/ember sets this to `true`, which is incompatible with our need to set `emitDeclarationOnly`. 16 | @tsconfig/ember is more optimized for apps, which wouldn't emit anything, only type check. 17 | */ 18 | "noEmit": false, 19 | 20 | /** 21 | https://www.typescriptlang.org/tsconfig#emitDeclarationOnly 22 | We want to only emit declarations as we use Rollup to emit JavaScript. 23 | */ 24 | "emitDeclarationOnly": true, 25 | 26 | /** 27 | https://www.typescriptlang.org/tsconfig#noEmitOnError 28 | Do not block emit on TS errors. 29 | */ 30 | "noEmitOnError": false, 31 | 32 | /** 33 | https://www.typescriptlang.org/tsconfig#allowImportingTsExtensions 34 | 35 | We want our tooling to know how to resolve our custom files so the appropriate plugins 36 | can do the proper transformations on those files. 37 | */ 38 | "allowImportingTsExtensions": true, 39 | 40 | "types": ["ember-source/types"] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ember-async-data/type-tests/index.ts: -------------------------------------------------------------------------------- 1 | import 'ember-source/types'; 2 | import 'ember-source/types/preview'; 3 | -------------------------------------------------------------------------------- /ember-async-data/type-tests/load-test.ts: -------------------------------------------------------------------------------- 1 | import TrackedAsyncData from '../src/tracked-async-data'; 2 | import loadHelper, { load } from '../src/helpers/load'; 3 | import { expectTypeOf } from 'expect-type'; 4 | 5 | expectTypeOf(load).toEqualTypeOf< 6 | (data: T | Promise) => TrackedAsyncData 7 | >(); 8 | 9 | expectTypeOf(load(true)).toEqualTypeOf(new TrackedAsyncData(true)); 10 | 11 | expectTypeOf>>().toEqualTypeOf< 12 | TrackedAsyncData 13 | >(); 14 | -------------------------------------------------------------------------------- /ember-async-data/type-tests/tracked-async-data-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/prefer-promise-reject-errors,@typescript-eslint/unbound-method */ 2 | import TrackedAsyncData, { type JSONRepr } from '../src/tracked-async-data.ts'; 3 | import { expectTypeOf } from 'expect-type'; 4 | 5 | declare function unreachable(x: never): never; 6 | 7 | declare class PublicAPI { 8 | constructor(data: T | Promise); 9 | get state(): 'PENDING' | 'RESOLVED' | 'REJECTED'; 10 | get value(): T | null; 11 | get error(): unknown; 12 | get isPending(): boolean; 13 | get isResolved(): boolean; 14 | get isRejected(): boolean; 15 | toJSON(): 16 | | { isPending: true; isResolved: false; isRejected: false } 17 | | { isPending: false; isResolved: true; value: T; isRejected: false } 18 | | { isPending: false; isResolved: false; isRejected: true; error: unknown }; 19 | toString(): string; 20 | } 21 | 22 | expectTypeOf(TrackedAsyncData).toBeConstructibleWith(12); 23 | expectTypeOf(TrackedAsyncData).toBeConstructibleWith('hello'); 24 | expectTypeOf(TrackedAsyncData).toBeConstructibleWith(true); 25 | expectTypeOf(TrackedAsyncData).toBeConstructibleWith(null); 26 | expectTypeOf(TrackedAsyncData).toBeConstructibleWith(undefined); 27 | expectTypeOf(TrackedAsyncData).toBeConstructibleWith({ cool: 'story' }); 28 | expectTypeOf(TrackedAsyncData).toBeConstructibleWith(['neat']); 29 | expectTypeOf(TrackedAsyncData).toBeConstructibleWith(Promise.resolve()); 30 | expectTypeOf(TrackedAsyncData).toBeConstructibleWith(Promise.resolve(12)); 31 | expectTypeOf(TrackedAsyncData).toBeConstructibleWith(Promise.reject()); 32 | expectTypeOf(TrackedAsyncData).toBeConstructibleWith(Promise.reject('gah')); 33 | 34 | // We use `toMatchTypeOf` here to confirm the union type which makes up 35 | // `TrackedAsyncData` is structurally compatible with the desired public 36 | // interface, but then use explicit `toEqualTypeOf` checks on each individual 37 | // property below to guarantee they don't accidentally widen the actual type 38 | // of each property. 39 | declare let data: TrackedAsyncData; 40 | declare let expected: PublicAPI; 41 | expectTypeOf(data).toMatchTypeOf(expected); 42 | 43 | expectTypeOf(data.state).toEqualTypeOf<'PENDING' | 'RESOLVED' | 'REJECTED'>(); 44 | expectTypeOf(data.value).toEqualTypeOf(); 45 | expectTypeOf(data.error).toEqualTypeOf(); 46 | expectTypeOf(data.isPending).toEqualTypeOf(); 47 | expectTypeOf(data.isResolved).toEqualTypeOf(); 48 | expectTypeOf(data.isRejected).toEqualTypeOf(); 49 | expectTypeOf(data.toJSON).toBeFunction(); 50 | expectTypeOf(data.toJSON()).toEqualTypeOf>(); 51 | expectTypeOf(data.toString).toBeFunction(); 52 | expectTypeOf(data.toString()).toEqualTypeOf(); 53 | 54 | if (data.isPending) { 55 | expectTypeOf(data.value).toEqualTypeOf(null); 56 | expectTypeOf(data.error).toEqualTypeOf(null); 57 | } else if (data.isResolved) { 58 | expectTypeOf(data.value).toEqualTypeOf(); 59 | expectTypeOf(data.error).toEqualTypeOf(null); 60 | } else if (data.isRejected) { 61 | expectTypeOf(data.value).toEqualTypeOf(null); 62 | expectTypeOf(data.error).toEqualTypeOf(); 63 | } else { 64 | unreachable(data); 65 | } 66 | 67 | if (data.state === 'PENDING') { 68 | expectTypeOf(data.value).toEqualTypeOf(null); 69 | expectTypeOf(data.error).toEqualTypeOf(null); 70 | } else if (data.state === 'RESOLVED') { 71 | expectTypeOf(data.value).toEqualTypeOf(); 72 | expectTypeOf(data.error).toEqualTypeOf(null); 73 | } else if (data.state === 'REJECTED') { 74 | expectTypeOf(data.value).toEqualTypeOf(null); 75 | expectTypeOf(data.error).toEqualTypeOf(); 76 | } else { 77 | unreachable(data); 78 | } 79 | -------------------------------------------------------------------------------- /ember-async-data/type-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["**/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /ember-async-data/unpublished-development-types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Add any types here that you need for local development only. 2 | // These will *not* be published as part of your addon, so be careful that your published code does not rely on them! 3 | 4 | import 'ember-source/types'; 5 | import 'ember-source/types/preview'; 6 | 7 | import '@glint/environment-ember-loose'; 8 | import '@glint/environment-ember-template-imports'; 9 | 10 | // Uncomment if you need to support consuming projects in loose mode 11 | // 12 | // declare module '@glint/environment-ember-loose/registry' { 13 | // export default interface Registry { 14 | // // Add any registry entries from other addons here that your addon itself uses (in non-strict mode templates) 15 | // // See https://typed-ember.gitbook.io/glint/using-glint/ember/using-addons 16 | // } 17 | // } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "repository": "https://github.com/tracked-tools/ember-async-data", 4 | "license": "MIT", 5 | "author": { 6 | "name": "Chris Krycho", 7 | "email": "hello@chriskrycho.com", 8 | "url": "https://v5.chriskrycho.com/" 9 | }, 10 | "workspaces": [ 11 | "ember-async-data", 12 | "test-app" 13 | ], 14 | "scripts": { 15 | "build": "pnpm run --filter=ember-async-data build", 16 | "format": "pnpm run --filter=\"*\" format", 17 | "lint": "pnpm run --filter=\"*\" lint", 18 | "lint:fix": "pnpm run --filter=\"*\" lint:fix", 19 | "prepare": "pnpm run build", 20 | "start": "concurrently 'pnpm:start:*' --restart-after 5000 --prefixColors auto", 21 | "start:addon": "pnpm run --filter=ember-async-data start", 22 | "start:test-app": "pnpm run --filter=test-app start", 23 | "test": "pnpm run --filter=test-app test", 24 | "test:ember": "pnpm --filter=test-app test:ember" 25 | }, 26 | "devDependencies": { 27 | "@glint/core": "^1.2.1", 28 | "concurrently": "^9.1.2", 29 | "prettier-plugin-ember-template-tag": "^2.0.2", 30 | "release-plan": "^0.16.0" 31 | }, 32 | "packageManager": "pnpm@10.10.0", 33 | "volta": { 34 | "node": "20.19.1", 35 | "pnpm": "10.10.0" 36 | }, 37 | "publishConfig": { 38 | "registry": "https://registry.npmjs.org" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - ember-async-data 3 | - test-app 4 | onlyBuiltDependencies: 5 | - core-js 6 | -------------------------------------------------------------------------------- /test-app/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /test-app/.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false, 9 | 10 | /** 11 | Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript 12 | rather than JavaScript by default, when a TypeScript version of a given blueprint is available. 13 | */ 14 | "isTypeScriptProject": true 15 | } 16 | -------------------------------------------------------------------------------- /test-app/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | pull_request: {} 9 | 10 | concurrency: 11 | group: ci-${{ github.head_ref || github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | lint: 16 | name: "Lint" 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 10 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: pnpm/action-setup@v4 23 | with: 24 | version: 9 25 | - name: Install Node 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: 18 29 | cache: pnpm 30 | - name: Install Dependencies 31 | run: pnpm install --frozen-lockfile 32 | - name: Lint 33 | run: pnpm lint 34 | 35 | test: 36 | name: "Test" 37 | runs-on: ubuntu-latest 38 | timeout-minutes: 10 39 | 40 | steps: 41 | - uses: actions/checkout@v3 42 | - uses: pnpm/action-setup@v4 43 | with: 44 | version: 9 45 | - name: Install Node 46 | uses: actions/setup-node@v3 47 | with: 48 | node-version: 18 49 | cache: pnpm 50 | - name: Install Dependencies 51 | run: pnpm install --frozen-lockfile 52 | - name: Run Tests 53 | run: pnpm test 54 | -------------------------------------------------------------------------------- /test-app/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /declarations/ 4 | 5 | # dependencies 6 | /node_modules/ 7 | 8 | # misc 9 | /.env* 10 | /.pnp* 11 | /.eslintcache 12 | /coverage/ 13 | /npm-debug.log* 14 | /testem.log 15 | /yarn-error.log 16 | 17 | # ember-try 18 | /.node_modules.ember-try/ 19 | /npm-shrinkwrap.json.ember-try 20 | /package.json.ember-try 21 | /package-lock.json.ember-try 22 | /yarn.lock.ember-try 23 | 24 | # broccoli-debug 25 | /DEBUG/ 26 | -------------------------------------------------------------------------------- /test-app/.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /dist/ 6 | 7 | # misc 8 | /coverage/ 9 | !.* 10 | .*/ 11 | 12 | # ember-try 13 | /.node_modules.ember-try/ 14 | -------------------------------------------------------------------------------- /test-app/.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | overrides: [ 5 | { 6 | files: '*.{js,ts}', 7 | options: { 8 | singleQuote: true, 9 | }, 10 | }, 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /test-app/.stylelintignore: -------------------------------------------------------------------------------- 1 | # unconventional files 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /dist/ 6 | 7 | # addons 8 | /.node_modules.ember-try/ 9 | -------------------------------------------------------------------------------- /test-app/.stylelintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: ['stylelint-config-standard', 'stylelint-prettier/recommended'], 5 | }; 6 | -------------------------------------------------------------------------------- /test-app/.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended', 5 | }; 6 | -------------------------------------------------------------------------------- /test-app/.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["dist"] 3 | } 4 | -------------------------------------------------------------------------------- /test-app/app/app.ts: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from 'test-app/config/environment'; 5 | 6 | export default class App extends Application { 7 | modulePrefix = config.modulePrefix; 8 | podModulePrefix = config.podModulePrefix; 9 | Resolver = Resolver; 10 | } 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | -------------------------------------------------------------------------------- /test-app/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracked-tools/ember-async-data/5b5f4d4b6c0df6dd0c003da938be3bc32f244382/test-app/app/components/.gitkeep -------------------------------------------------------------------------------- /test-app/app/config/environment.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type declarations for 3 | * import config from 'test-app/config/environment' 4 | */ 5 | declare const config: { 6 | environment: string; 7 | modulePrefix: string; 8 | podModulePrefix: string; 9 | locationType: 'history' | 'hash' | 'none' | 'auto'; 10 | rootURL: string; 11 | APP: Record; 12 | }; 13 | 14 | export default config; 15 | -------------------------------------------------------------------------------- /test-app/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracked-tools/ember-async-data/5b5f4d4b6c0df6dd0c003da938be3bc32f244382/test-app/app/controllers/.gitkeep -------------------------------------------------------------------------------- /test-app/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracked-tools/ember-async-data/5b5f4d4b6c0df6dd0c003da938be3bc32f244382/test-app/app/helpers/.gitkeep -------------------------------------------------------------------------------- /test-app/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TestApp 6 | 7 | 8 | 9 | {{content-for "head"}} 10 | 11 | 12 | 13 | 14 | {{content-for "head-footer"}} 15 | 16 | 17 | {{content-for "body"}} 18 | 19 | 20 | 21 | 22 | {{content-for "body-footer"}} 23 | 24 | 25 | -------------------------------------------------------------------------------- /test-app/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracked-tools/ember-async-data/5b5f4d4b6c0df6dd0c003da938be3bc32f244382/test-app/app/models/.gitkeep -------------------------------------------------------------------------------- /test-app/app/router.ts: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from 'test-app/config/environment'; 3 | 4 | export default class Router extends EmberRouter { 5 | location = config.locationType; 6 | rootURL = config.rootURL; 7 | } 8 | 9 | Router.map(function () { 10 | // Add route declarations here 11 | }); 12 | -------------------------------------------------------------------------------- /test-app/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracked-tools/ember-async-data/5b5f4d4b6c0df6dd0c003da938be3bc32f244382/test-app/app/routes/.gitkeep -------------------------------------------------------------------------------- /test-app/app/styles/app.css: -------------------------------------------------------------------------------- 1 | /* Ember supports plain CSS out of the box. More info: https://cli.emberjs.com/release/advanced-use/stylesheets/ */ 2 | -------------------------------------------------------------------------------- /test-app/app/templates/application.gjs: -------------------------------------------------------------------------------- 1 | import RouteTemplate from 'ember-route-template'; 2 | 3 | export default RouteTemplate( 4 | 9 | ); 10 | -------------------------------------------------------------------------------- /test-app/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "6.2.2", 7 | "blueprints": [ 8 | { 9 | "name": "app", 10 | "outputRepo": "https://github.com/ember-cli/ember-new-output", 11 | "codemodsSource": "ember-app-codemods-manifest@1", 12 | "isBaseBlueprint": true, 13 | "options": [ 14 | "--no-welcome", 15 | "--pnpm", 16 | "--ci-provider=github", 17 | "--typescript" 18 | ] 19 | } 20 | ] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /test-app/config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup'); 5 | 6 | module.exports = async function () { 7 | return { 8 | usePnpm: true, 9 | scenarios: [ 10 | { 11 | name: 'ember-lts-4.8', 12 | npm: { 13 | devDependencies: { 14 | 'ember-load-initializers': '^2.0.0', 15 | 'ember-resolver': '^11.0.1', 16 | 'ember-source': '~4.8.0', 17 | }, 18 | }, 19 | }, 20 | { 21 | name: 'ember-lts-4.12', 22 | npm: { 23 | devDependencies: { 24 | 'ember-source': '~4.12.0', 25 | 'ember-load-initializers': '^2.0.0', 26 | }, 27 | }, 28 | }, 29 | { 30 | name: 'ember-lts-5.4', 31 | npm: { 32 | devDependencies: { 33 | 'ember-source': '~5.4.0', 34 | }, 35 | }, 36 | }, 37 | { 38 | name: 'ember-lts-5.8', 39 | npm: { 40 | devDependencies: { 41 | 'ember-source': '~5.8.0', 42 | }, 43 | }, 44 | }, 45 | { 46 | name: 'ember-lts-5.12', 47 | npm: { 48 | devDependencies: { 49 | 'ember-source': '~5.12.0', 50 | }, 51 | }, 52 | }, 53 | { 54 | name: 'ember-release', 55 | npm: { 56 | devDependencies: { 57 | 'ember-source': await getChannelURL('release'), 58 | }, 59 | }, 60 | }, 61 | { 62 | name: 'ember-beta', 63 | allowedToFail: true, 64 | npm: { 65 | devDependencies: { 66 | 'ember-source': await getChannelURL('beta'), 67 | }, 68 | }, 69 | }, 70 | { 71 | name: 'ember-canary', 72 | allowedToFail: true, 73 | npm: { 74 | devDependencies: { 75 | 'ember-source': await getChannelURL('canary'), 76 | }, 77 | }, 78 | }, 79 | embroiderSafe(), 80 | embroiderOptimized(), 81 | ], 82 | }; 83 | }; 84 | -------------------------------------------------------------------------------- /test-app/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (environment) { 4 | const ENV = { 5 | modulePrefix: 'test-app', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'history', 9 | EmberENV: { 10 | EXTEND_PROTOTYPES: false, 11 | FEATURES: { 12 | // Here you can enable experimental features on an ember canary build 13 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 14 | }, 15 | }, 16 | 17 | APP: { 18 | // Here you can pass flags/options to your application instance 19 | // when it is created 20 | }, 21 | }; 22 | 23 | if (environment === 'development') { 24 | // ENV.APP.LOG_RESOLVER = true; 25 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 26 | // ENV.APP.LOG_TRANSITIONS = true; 27 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 28 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 29 | } 30 | 31 | if (environment === 'test') { 32 | // Testem prefers this... 33 | ENV.locationType = 'none'; 34 | 35 | // keep test console output quieter 36 | ENV.APP.LOG_ACTIVE_GENERATION = false; 37 | ENV.APP.LOG_VIEW_LOOKUPS = false; 38 | 39 | ENV.APP.rootElement = '#ember-testing'; 40 | ENV.APP.autoboot = false; 41 | } 42 | 43 | if (environment === 'production') { 44 | // here you can enable a production-specific feature 45 | } 46 | 47 | return ENV; 48 | }; 49 | -------------------------------------------------------------------------------- /test-app/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /test-app/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions', 7 | ]; 8 | 9 | module.exports = { 10 | browsers, 11 | }; 12 | -------------------------------------------------------------------------------- /test-app/ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberApp = require('ember-cli/lib/broccoli/ember-app'); 4 | 5 | module.exports = function (defaults) { 6 | const app = new EmberApp(defaults, { 7 | 'ember-cli-babel': { enableTypeScriptTransform: true }, 8 | autoImport: { 9 | watchDependencies: ['ember-async-data'], 10 | }, 11 | }); 12 | 13 | const { maybeEmbroider } = require('@embroider/test-setup'); 14 | return maybeEmbroider(app); 15 | }; 16 | -------------------------------------------------------------------------------- /test-app/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Debugging: 3 | * https://eslint.org/docs/latest/use/configure/debug 4 | * ---------------------------------------------------- 5 | * 6 | * Print a file's calculated configuration 7 | * 8 | * npx eslint --print-config path/to/file.js 9 | * 10 | * Inspecting the config 11 | * 12 | * npx eslint --inspect-config 13 | * 14 | */ 15 | import globals from "globals"; 16 | import js from "@eslint/js"; 17 | 18 | import ts from "typescript-eslint"; 19 | 20 | import ember from "eslint-plugin-ember/recommended"; 21 | 22 | import eslintConfigPrettier from "eslint-config-prettier"; 23 | import qunit from "eslint-plugin-qunit"; 24 | import n from "eslint-plugin-n"; 25 | 26 | import babelParser from "@babel/eslint-parser"; 27 | 28 | const parserOptions = { 29 | esm: { 30 | js: { 31 | ecmaFeatures: { modules: true }, 32 | ecmaVersion: "latest", 33 | requireConfigFile: false, 34 | babelOptions: { 35 | plugins: [ 36 | [ 37 | "@babel/plugin-proposal-decorators", 38 | { decoratorsBeforeExport: true }, 39 | ], 40 | ], 41 | }, 42 | }, 43 | ts: { 44 | projectService: true, 45 | tsconfigRootDir: import.meta.dirname, 46 | }, 47 | }, 48 | }; 49 | 50 | export default ts.config( 51 | js.configs.recommended, 52 | ember.configs.base, 53 | ember.configs.gjs, 54 | ember.configs.gts, 55 | eslintConfigPrettier, 56 | /** 57 | * Ignores must be in their own object 58 | * https://eslint.org/docs/latest/use/configure/ignore 59 | */ 60 | { 61 | ignores: ["dist/", "node_modules/", "coverage/", "!**/.*"], 62 | }, 63 | /** 64 | * https://eslint.org/docs/latest/use/configure/configuration-files#configuring-linter-options 65 | */ 66 | { 67 | linterOptions: { 68 | reportUnusedDisableDirectives: "error", 69 | }, 70 | }, 71 | { 72 | files: ["**/*.js"], 73 | languageOptions: { 74 | parser: babelParser, 75 | }, 76 | }, 77 | { 78 | files: ["**/*.{js,gjs}"], 79 | languageOptions: { 80 | parserOptions: parserOptions.esm.js, 81 | globals: { 82 | ...globals.browser, 83 | }, 84 | }, 85 | }, 86 | { 87 | files: ["**/*.{ts,gts}"], 88 | languageOptions: { 89 | parser: ember.parser, 90 | parserOptions: parserOptions.esm.ts, 91 | }, 92 | extends: [...ts.configs.recommendedTypeChecked, ember.configs.gts], 93 | }, 94 | { 95 | files: ["tests/**/*-test.{js,gjs,ts,gts}"], 96 | plugins: { 97 | qunit, 98 | }, 99 | rules: { 100 | "@typescript-eslint/no-floating-promises": "off", 101 | }, 102 | }, 103 | /** 104 | * CJS node files 105 | */ 106 | { 107 | files: [ 108 | "**/*.cjs", 109 | "config/**/*.js", 110 | "tests/dummy/config/**/*.js", 111 | "testem.js", 112 | "testem*.js", 113 | "index.js", 114 | ".prettierrc.js", 115 | ".stylelintrc.js", 116 | ".template-lintrc.js", 117 | "ember-cli-build.js", 118 | ], 119 | plugins: { 120 | n, 121 | }, 122 | 123 | languageOptions: { 124 | sourceType: "script", 125 | ecmaVersion: "latest", 126 | globals: { 127 | ...globals.node, 128 | }, 129 | }, 130 | }, 131 | /** 132 | * ESM node files 133 | */ 134 | { 135 | files: ["**/*.mjs"], 136 | plugins: { 137 | n, 138 | }, 139 | 140 | languageOptions: { 141 | sourceType: "module", 142 | ecmaVersion: "latest", 143 | parserOptions: parserOptions.esm.js, 144 | globals: { 145 | ...globals.node, 146 | }, 147 | }, 148 | }, 149 | ); 150 | -------------------------------------------------------------------------------- /test-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-app", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Test app for ember-async-data addon", 6 | "repository": "https://github.com/chriskrycho/ember-async-data", 7 | "license": "MIT", 8 | "author": "", 9 | "directories": { 10 | "doc": "doc", 11 | "test": "tests" 12 | }, 13 | "scripts": { 14 | "build": "ember build --environment=production", 15 | "format": "prettier . --cache --write", 16 | "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", 17 | "lint:css": "stylelint \"**/*.css\"", 18 | "lint:css:fix": "concurrently \"pnpm:lint:css -- --fix\"", 19 | "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" --prefixColors auto", 20 | "lint:format": "prettier . --cache --check", 21 | "lint:hbs": "ember-template-lint .", 22 | "lint:hbs:fix": "ember-template-lint . --fix", 23 | "lint:js": "eslint . --cache", 24 | "lint:js:fix": "eslint . --fix", 25 | "lint:types": "tsc --noEmit", 26 | "start": "ember serve", 27 | "test": "concurrently \"pnpm:lint\" \"pnpm:test:*\" --names \"lint,test:\" --prefixColors auto", 28 | "test:ember": "ember test" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.26.10", 32 | "@babel/eslint-parser": "^7.26.8", 33 | "@ember/optional-features": "^2.2.0", 34 | "@ember/test-helpers": "^4.0.5", 35 | "@embroider/test-setup": "^4.0.0", 36 | "@eslint/js": "^9.21.0", 37 | "@glimmer/component": "^1.1.2", 38 | "@glimmer/tracking": "^1.1.2", 39 | "@glint/environment-ember-loose": "^1.5.2", 40 | "@glint/environment-ember-template-imports": "^1.5.2", 41 | "@glint/template": "^1.5.2", 42 | "@tsconfig/ember": "^3.0.9", 43 | "@types/eslint__js": "^8.42.3", 44 | "@types/qunit": "^2.19.12", 45 | "@types/rsvp": "^4.0.9", 46 | "broccoli-asset-rev": "^3.0.0", 47 | "concurrently": "^9.1.2", 48 | "ember-async-data": "workspace:*", 49 | "ember-auto-import": "^2.10.0", 50 | "ember-cli": "~6.2.3", 51 | "ember-cli-app-version": "^7.0.0", 52 | "ember-cli-babel": "^8.2.0", 53 | "ember-cli-clean-css": "^3.0.0", 54 | "ember-cli-dependency-checker": "^3.3.3", 55 | "ember-cli-htmlbars": "^6.3.0", 56 | "ember-cli-inject-live-reload": "^2.1.0", 57 | "ember-cli-sri": "^2.1.1", 58 | "ember-cli-terser": "^4.0.2", 59 | "ember-load-initializers": "^3.0.1", 60 | "ember-page-title": "^8.2.3", 61 | "ember-qunit": "^9.0.1", 62 | "ember-resolver": "^13.1.0", 63 | "ember-route-template": "^1.0.3", 64 | "ember-source": "~6.3.0", 65 | "ember-source-channel-url": "^3.0.0", 66 | "ember-template-imports": "^4.3.0", 67 | "ember-template-lint": "^7.0.1", 68 | "ember-try": "^4.0.0", 69 | "eslint": "^9.22.0", 70 | "eslint-config-prettier": "^10.1.1", 71 | "eslint-plugin-ember": "^12.5.0", 72 | "eslint-plugin-n": "^17.15.1", 73 | "eslint-plugin-prettier": "^5.2.3", 74 | "eslint-plugin-qunit": "^8.1.2", 75 | "globals": "^16.0.0", 76 | "loader.js": "^4.7.0", 77 | "prettier": "^3.5.3", 78 | "prettier-plugin-ember-template-tag": "^2.0.4", 79 | "qunit": "^2.24.1", 80 | "qunit-dom": "^3.4.0", 81 | "stylelint": "^16.16.0", 82 | "stylelint-config-standard": "^37.0.0", 83 | "stylelint-prettier": "^5.0.3", 84 | "tracked-built-ins": "^4.0.0", 85 | "typescript": "~5.7.3", 86 | "typescript-eslint": "^8.25.0", 87 | "webpack": "^5.98.0" 88 | }, 89 | "engines": { 90 | "node": ">= 18" 91 | }, 92 | "volta": { 93 | "extends": "../package.json" 94 | }, 95 | "ember": { 96 | "edition": "octane" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /test-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /test-app/testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: ['Chrome'], 7 | launch_in_dev: ['Chrome'], 8 | browser_start_timeout: 120, 9 | browser_args: { 10 | Chrome: { 11 | ci: [ 12 | // --no-sandbox is needed when running Chrome inside a container 13 | process.env.CI ? '--no-sandbox' : null, 14 | '--headless', 15 | '--disable-dev-shm-usage', 16 | '--disable-software-rasterizer', 17 | '--mute-audio', 18 | '--remote-debugging-port=0', 19 | '--window-size=1440,900', 20 | ].filter(Boolean), 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /test-app/tests/defer.ts: -------------------------------------------------------------------------------- 1 | interface Deferred { 2 | resolve: (value?: unknown) => void; 3 | reject: (value?: unknown) => void; 4 | promise: Promise; 5 | } 6 | 7 | export function defer(): Deferred { 8 | const deferred = { 9 | resolve: undefined, 10 | reject: undefined, 11 | promise: undefined, 12 | } as Partial; 13 | 14 | deferred.promise = new Promise((resolve, reject) => { 15 | deferred.resolve = resolve; 16 | deferred.reject = reject; 17 | }); 18 | 19 | return deferred as Deferred; 20 | } 21 | -------------------------------------------------------------------------------- /test-app/tests/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | setupApplicationTest as upstreamSetupApplicationTest, 3 | setupRenderingTest as upstreamSetupRenderingTest, 4 | setupTest as upstreamSetupTest, 5 | type SetupTestOptions, 6 | } from 'ember-qunit'; 7 | 8 | // This file exists to provide wrappers around ember-qunit's / ember-mocha's 9 | // test setup functions. This way, you can easily extend the setup that is 10 | // needed per test type. 11 | 12 | function setupApplicationTest(hooks: NestedHooks, options?: SetupTestOptions) { 13 | upstreamSetupApplicationTest(hooks, options); 14 | 15 | // Additional setup for application tests can be done here. 16 | // 17 | // For example, if you need an authenticated session for each 18 | // application test, you could do: 19 | // 20 | // hooks.beforeEach(async function () { 21 | // await authenticateSession(); // ember-simple-auth 22 | // }); 23 | // 24 | // This is also a good place to call test setup functions coming 25 | // from other addons: 26 | // 27 | // setupIntl(hooks); // ember-intl 28 | // setupMirage(hooks); // ember-cli-mirage 29 | } 30 | 31 | function setupRenderingTest(hooks: NestedHooks, options?: SetupTestOptions) { 32 | upstreamSetupRenderingTest(hooks, options); 33 | 34 | // Additional setup for rendering tests can be done here. 35 | } 36 | 37 | function setupTest(hooks: NestedHooks, options?: SetupTestOptions) { 38 | upstreamSetupTest(hooks, options); 39 | 40 | // Additional setup for unit tests can be done here. 41 | } 42 | 43 | export { setupApplicationTest, setupRenderingTest, setupTest }; 44 | -------------------------------------------------------------------------------- /test-app/tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TestApp Tests 6 | 7 | 8 | 9 | {{content-for "head"}} {{content-for "test-head"}} 10 | 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} {{content-for "test-head-footer"}} 16 | 17 | 18 | {{content-for "body"}} {{content-for "test-body"}} 19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {{content-for "body-footer"}} {{content-for "test-body-footer"}} 34 | 35 | 36 | -------------------------------------------------------------------------------- /test-app/tests/integration/helpers/load-test.gts: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render, waitFor } from '@ember/test-helpers'; 4 | import { tracked } from '@glimmer/tracking'; 5 | import load from 'ember-async-data/helpers/load'; 6 | import { defer } from '../../defer'; 7 | 8 | class LocalState { 9 | @tracked promise: Promise; 10 | 11 | constructor(promise: Promise) { 12 | this.promise = promise; 13 | } 14 | } 15 | 16 | module('Integration | Helper | load', function (hooks) { 17 | setupRenderingTest(hooks); 18 | 19 | test('it renders loading state', async function (assert) { 20 | const { promise, resolve } = defer(); 21 | 22 | const renderPromise = render(); 35 | 36 | await waitFor('[data-test-load-helper]'); 37 | 38 | assert.dom('[data-test-load-helper]').hasText('PENDING'); 39 | 40 | resolve(); 41 | await renderPromise; 42 | }); 43 | 44 | test('it renders loaded state', async function (assert) { 45 | const deferred = defer(); 46 | deferred.resolve('foobar'); 47 | await deferred.promise; 48 | 49 | const renderPromise = render(); 62 | 63 | await waitFor('[data-test-load-helper]'); 64 | assert.dom('[data-test-load-helper]').containsText('RESOLVED'); 65 | assert.dom('[data-test-load-helper]').containsText('foobar'); 66 | 67 | await renderPromise; 68 | }); 69 | 70 | test('it renders error state', async function (assert) { 71 | assert.expect(3); 72 | 73 | const { promise, reject } = defer(); 74 | 75 | // This handles the error throw from rendering a rejected promise 76 | promise.catch((error: Error) => { 77 | assert.ok(error instanceof Error); 78 | assert.strictEqual(error.message, 'foobar'); 79 | }); 80 | 81 | reject(new Error('foobar')); 82 | 83 | const renderPromise = render(); 96 | 97 | await waitFor('[data-test-load-helper]'); 98 | assert.dom('[data-test-load-helper]').hasText('REJECTED'); 99 | 100 | await renderPromise; 101 | }); 102 | 103 | test('it renders loading state and then loaded state', async function (assert) { 104 | const { promise, resolve } = defer(); 105 | 106 | const renderPromise = render(); 119 | 120 | await waitFor('[data-test-load-helper]'); 121 | assert.dom('[data-test-load-helper]').hasText('PENDING'); 122 | 123 | resolve(); 124 | 125 | await waitFor('[data-test-state="RESOLVED"]'); 126 | assert.dom('[data-test-load-helper]').hasText('RESOLVED'); 127 | 128 | await renderPromise; 129 | }); 130 | 131 | test('it renders loading state and then error state', async function (assert) { 132 | assert.expect(3); 133 | 134 | const { promise, reject } = defer(); 135 | 136 | const renderPromise = render(); 149 | 150 | await waitFor('[data-test-load-helper]'); 151 | assert.dom('[data-test-load-helper]').hasText('PENDING'); 152 | 153 | reject(new Error('foobar')); 154 | 155 | // This handles the error thrown at the top level 156 | await promise.catch(() => { 157 | assert.ok(true, 'things are neat!'); 158 | }); 159 | 160 | await waitFor('[data-test-state="REJECTED"]'); 161 | assert.dom('[data-test-load-helper]').hasText('REJECTED', 'rejected'); 162 | 163 | await renderPromise; 164 | }); 165 | 166 | test('it renders the state for the new promise if a new promise is sent and resolves before the old promise is done loading', async function (assert) { 167 | assert.expect(4); 168 | 169 | const { promise: oldPromise, reject: rejectOld } = defer(); 170 | const localState = new LocalState(oldPromise); 171 | 172 | // This handles the error throw from rendering a rejected promise 173 | oldPromise.catch((error) => { 174 | assert.ok(error instanceof Error); 175 | assert.strictEqual((error as Error).message, 'foobar'); 176 | }); 177 | 178 | const renderPromise = render(); 191 | 192 | await waitFor('[data-test-load-helper]'); 193 | assert.dom('[data-test-load-helper]').hasText('PENDING'); 194 | 195 | const { promise: newPromise, resolve: resolveNew } = defer(); 196 | localState.promise = newPromise; 197 | 198 | resolveNew(); 199 | await newPromise; 200 | rejectOld(new Error('foobar')); 201 | 202 | await waitFor('[data-test-state="RESOLVED"]'); 203 | assert.dom('[data-test-load-helper]').hasText('RESOLVED'); 204 | 205 | await renderPromise; 206 | }); 207 | 208 | test('it renders the state and value for the new promise if a new promise with a different value is sent before the old promise is done loading', async function (assert) { 209 | const { promise: oldPromise, resolve: resolveOld } = defer(); 210 | const localState = new LocalState(oldPromise); 211 | 212 | const renderPromise = render(); 225 | 226 | await waitFor('[data-test-load-helper]'); 227 | assert.dom('[data-test-load-helper]').hasText('PENDING'); 228 | 229 | const { promise: newPromise, resolve: resolveNew } = defer(); 230 | localState.promise = newPromise; 231 | 232 | resolveNew('New'); 233 | await newPromise; 234 | resolveOld('Old'); 235 | await oldPromise; 236 | 237 | await waitFor('[data-test-state="RESOLVED"]'); 238 | assert.dom('[data-test-load-helper]').containsText('RESOLVED'); 239 | assert.dom('[data-test-load-helper]').containsText('New'); 240 | 241 | await renderPromise; 242 | }); 243 | 244 | test('it renders error state and then loading state for a retried promise', async function (assert) { 245 | assert.expect(4); 246 | 247 | const deferred = defer(); 248 | const promise = deferred.promise; 249 | const localState = new LocalState(promise); 250 | 251 | // This handles the error throw from rendering a rejected promise 252 | deferred.promise.catch((error: Error) => { 253 | assert.ok(error instanceof Error); 254 | assert.strictEqual(error.message, 'foobar'); 255 | }); 256 | 257 | deferred.reject(new Error('foobar')); 258 | 259 | const renderPromise = render(); 272 | 273 | await waitFor('[data-test-load-helper]'); 274 | assert.dom('[data-test-load-helper]').hasText('REJECTED'); 275 | 276 | const retryDeferred = defer(); 277 | localState.promise = retryDeferred.promise; 278 | 279 | await waitFor('[data-test-state="PENDING"]'); 280 | assert.dom('[data-test-load-helper]').hasText('PENDING'); 281 | 282 | retryDeferred.resolve(); 283 | await retryDeferred.promise; 284 | await renderPromise; 285 | }); 286 | }); 287 | -------------------------------------------------------------------------------- /test-app/tests/test-helper.ts: -------------------------------------------------------------------------------- 1 | import Application from 'test-app/app'; 2 | import config from 'test-app/config/environment'; 3 | import * as QUnit from 'qunit'; 4 | import { setApplication } from '@ember/test-helpers'; 5 | import { setup } from 'qunit-dom'; 6 | import { loadTests } from 'ember-qunit/test-loader'; 7 | import { start, setupEmberOnerrorValidation } from 'ember-qunit'; 8 | 9 | setApplication(Application.create(config.APP)); 10 | 11 | setup(QUnit.assert); 12 | QUnit.config.testTimeout = 1000; 13 | setupEmberOnerrorValidation(); 14 | loadTests(); 15 | start(); 16 | -------------------------------------------------------------------------------- /test-app/tests/unit/helpers/load-test.ts: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | import { load } from 'ember-async-data/helpers/load'; 4 | import TrackedAsyncData from 'ember-async-data/tracked-async-data'; 5 | import { defer } from 'test-app/tests/defer'; 6 | 7 | // As ember-async-data is now v2 addon and bundled via webpack, 8 | // we have separate entrypoints for the app and tests. 9 | // As a result, 'ember-async-data/tracked-async-data' gets evaluated twice, 10 | // and we get *another* class, not the same one the addon itself gets. 11 | // For some background info see https://github.com/webpack/webpack/issues/7556 12 | 13 | // We can unskip these tests once ember-auto-import gets fixed, 14 | // so we would have single entry point for app and tests. 15 | // Link to track status: https://github.com/ef4/ember-auto-import/issues/503 16 | module.skip('Unit | load', function (hooks) { 17 | setupTest(hooks); 18 | 19 | test('given a promise', async function (assert) { 20 | const { promise, resolve } = defer(); 21 | const result = load(promise); 22 | assert.ok( 23 | result instanceof TrackedAsyncData, 24 | 'it returns a TrackedAsyncData instance', 25 | ); 26 | resolve(); 27 | await promise; 28 | }); 29 | 30 | test('given a plain value', function (assert) { 31 | const result = load(12); 32 | assert.ok( 33 | result instanceof TrackedAsyncData, 34 | 'it returns a TrackedAsyncData instance', 35 | ); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test-app/tests/unit/tracked-async-data-test.ts: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { defer } from '../defer'; 3 | import TrackedAsyncData from 'ember-async-data/tracked-async-data'; 4 | import { settled } from '@ember/test-helpers'; 5 | import { setupTest } from 'test-app/tests/helpers'; 6 | 7 | const ERROR_NOT_REJECTED_STATE = 8 | "Error: Assertion Failed: Accessing `error` when TrackedAsyncData is not in the rejected state is not supported. Always check that `.state` is `'REJECTED'` or that `.isRejected` is `true` before accessing this property."; 9 | 10 | const VALUE_NOT_RESOLVED_STATE = 11 | "Error: Assertion Failed: Accessing `value` when TrackedAsyncData is not in the resolved state is not supported. Always check that `.state` is `'RESOLVED'` or that `.isResolved` is `true` before accessing this property."; 12 | 13 | module('Unit | TrackedAsyncData', function (hooks) { 14 | setupTest(hooks); 15 | 16 | test('it cannot be subclassed', function (assert) { 17 | // @ts-expect-error: The type is not statically subclassable, either, so 18 | // this fails both at the type-checking level and dynamically at runtime. 19 | class Subclass extends TrackedAsyncData {} 20 | 21 | assert.throws(() => new Subclass(Promise.resolve('nope'))); 22 | }); 23 | 24 | test('is initially PENDING', async function (assert) { 25 | const deferred = defer(); 26 | 27 | const result = new TrackedAsyncData(deferred.promise); 28 | assert.strictEqual(result.state, 'PENDING'); 29 | assert.true(result.isPending); 30 | assert.false(result.isResolved); 31 | assert.false(result.isRejected); 32 | assert.throws( 33 | () => result.value === null, 34 | (err: Error) => err.toString() === VALUE_NOT_RESOLVED_STATE, 35 | ); 36 | assert.throws( 37 | () => result.error === null, 38 | (err: Error) => err.toString() === ERROR_NOT_REJECTED_STATE, 39 | ); 40 | 41 | deferred.resolve(); 42 | await deferred.promise; 43 | }); 44 | 45 | test('it updates to resolved state', async function (assert) { 46 | const deferred = defer(); 47 | const result = new TrackedAsyncData(deferred.promise); 48 | 49 | deferred.resolve('foobar'); 50 | await settled(); 51 | 52 | assert.strictEqual(result.state, 'RESOLVED'); 53 | assert.false(result.isPending); 54 | assert.true(result.isResolved); 55 | assert.false(result.isRejected); 56 | assert.strictEqual(result.value, 'foobar'); 57 | assert.throws( 58 | () => result.error === null, 59 | (err: Error) => err.toString() === ERROR_NOT_REJECTED_STATE, 60 | ); 61 | }); 62 | 63 | module('it returns resolved state for non-thenable input', function () { 64 | test('undefined', async function (assert) { 65 | const loadUndefined = new TrackedAsyncData(undefined); 66 | await settled(); 67 | 68 | assert.strictEqual(loadUndefined.state, 'RESOLVED'); 69 | assert.false(loadUndefined.isPending); 70 | assert.true(loadUndefined.isResolved); 71 | assert.false(loadUndefined.isRejected); 72 | assert.strictEqual(loadUndefined.value, undefined); 73 | assert.throws( 74 | () => loadUndefined.error === null, 75 | (err: Error) => err.toString() === ERROR_NOT_REJECTED_STATE, 76 | ); 77 | }); 78 | 79 | test('null', async function (assert) { 80 | const loadNull = new TrackedAsyncData(null); 81 | await settled(); 82 | 83 | assert.strictEqual(loadNull.state, 'RESOLVED'); 84 | assert.false(loadNull.isPending); 85 | assert.true(loadNull.isResolved); 86 | assert.false(loadNull.isRejected); 87 | assert.strictEqual(loadNull.value, null); 88 | assert.throws( 89 | () => loadNull.error === null, 90 | (err: Error) => err.toString() === ERROR_NOT_REJECTED_STATE, 91 | ); 92 | }); 93 | 94 | test('non-thenable object', async function (assert) { 95 | const notAThenableObject = { notAThenable: true }; 96 | const loadObject = new TrackedAsyncData(notAThenableObject); 97 | await settled(); 98 | 99 | assert.strictEqual(loadObject.state, 'RESOLVED'); 100 | assert.false(loadObject.isPending); 101 | assert.true(loadObject.isResolved); 102 | assert.false(loadObject.isRejected); 103 | assert.strictEqual(loadObject.value, notAThenableObject); 104 | assert.throws( 105 | () => loadObject.error === null, 106 | (err: Error) => err.toString() === ERROR_NOT_REJECTED_STATE, 107 | ); 108 | }); 109 | 110 | test('boolean: true', async function (assert) { 111 | const loadTrue = new TrackedAsyncData(true); 112 | await settled(); 113 | 114 | assert.strictEqual(loadTrue.state, 'RESOLVED'); 115 | assert.false(loadTrue.isPending); 116 | assert.true(loadTrue.isResolved); 117 | assert.false(loadTrue.isRejected); 118 | assert.true(loadTrue.value); 119 | assert.throws( 120 | () => loadTrue.error === null, 121 | (err: Error) => err.toString() === ERROR_NOT_REJECTED_STATE, 122 | ); 123 | }); 124 | 125 | test('boolean: false', async function (assert) { 126 | const loadFalse = new TrackedAsyncData(false); 127 | await settled(); 128 | 129 | assert.strictEqual(loadFalse.state, 'RESOLVED'); 130 | assert.false(loadFalse.isPending); 131 | assert.true(loadFalse.isResolved); 132 | assert.false(loadFalse.isRejected); 133 | assert.false(loadFalse.value); 134 | assert.throws( 135 | () => loadFalse.error === null, 136 | (err: Error) => err.toString() === ERROR_NOT_REJECTED_STATE, 137 | ); 138 | }); 139 | 140 | test('number', async function (assert) { 141 | const loadNumber = new TrackedAsyncData(5); 142 | await settled(); 143 | 144 | assert.strictEqual(loadNumber.state, 'RESOLVED'); 145 | assert.false(loadNumber.isPending); 146 | assert.true(loadNumber.isResolved); 147 | assert.false(loadNumber.isRejected); 148 | assert.strictEqual(loadNumber.value, 5); 149 | assert.throws( 150 | () => loadNumber.error === null, 151 | (err: Error) => err.toString() === ERROR_NOT_REJECTED_STATE, 152 | ); 153 | }); 154 | 155 | test('string', async function (assert) { 156 | const loadString = new TrackedAsyncData('js'); 157 | await settled(); 158 | 159 | // loadString 160 | assert.strictEqual(loadString.state, 'RESOLVED'); 161 | assert.false(loadString.isPending); 162 | assert.true(loadString.isResolved); 163 | assert.false(loadString.isRejected); 164 | assert.strictEqual(loadString.value, 'js'); 165 | assert.throws( 166 | () => loadString.error === null, 167 | (err: Error) => err.toString() === ERROR_NOT_REJECTED_STATE, 168 | ); 169 | }); 170 | }); 171 | 172 | test('it returns error state', async function (assert) { 173 | assert.expect(8); 174 | 175 | // This handles the error throw from rendering a rejected promise 176 | const deferred = defer(); 177 | const result = new TrackedAsyncData(deferred.promise); 178 | 179 | deferred.reject(new Error('foobar')); 180 | await deferred.promise.catch((error) => { 181 | assert.true(error instanceof Error); 182 | assert.strictEqual( 183 | (error as Error).message, 184 | 'foobar', 185 | 'thrown promise rejection', 186 | ); 187 | }); 188 | 189 | assert.strictEqual(result.state, 'REJECTED'); 190 | assert.false(result.isPending); 191 | assert.false(result.isResolved); 192 | assert.true(result.isRejected); 193 | assert.throws( 194 | () => result.value === null, 195 | (err: Error) => err.toString() === VALUE_NOT_RESOLVED_STATE, 196 | ); 197 | assert.strictEqual((result.error as Error).message, 'foobar'); 198 | }); 199 | 200 | test('it returns loading state and then loaded state', async function (assert) { 201 | const deferred = defer(); 202 | const result = new TrackedAsyncData(deferred.promise); 203 | assert.strictEqual(result.state, 'PENDING'); 204 | 205 | deferred.resolve(); 206 | await deferred.promise; 207 | 208 | assert.strictEqual(result.state, 'RESOLVED'); 209 | }); 210 | 211 | test('it returns loading state and then error state', async function (assert) { 212 | assert.expect(4); 213 | 214 | const deferred = defer(); 215 | const result = new TrackedAsyncData(deferred.promise); 216 | assert.strictEqual(result.state, 'PENDING'); 217 | 218 | deferred.reject(new Error('foobar')); 219 | await deferred.promise.catch((err: Error) => { 220 | assert.true(err instanceof Error); 221 | assert.strictEqual(err.message, 'foobar'); 222 | }); 223 | 224 | assert.strictEqual(result.state, 'REJECTED'); 225 | }); 226 | 227 | test('it returns loaded state for already-resolved promises', async function (assert) { 228 | const promise = Promise.resolve('hello'); 229 | const result = new TrackedAsyncData(promise); 230 | await promise; 231 | assert.strictEqual(result.state, 'RESOLVED'); 232 | }); 233 | 234 | test('it returns error state for already-rejected promises', async function (assert) { 235 | assert.expect(3); 236 | 237 | const promise = Promise.reject(new Error('foobar')); 238 | const result = new TrackedAsyncData(promise); 239 | 240 | // This handles the error thrown *locally*. 241 | await promise.catch((error: Error) => { 242 | assert.true(error instanceof Error); 243 | assert.strictEqual(error.message, 'foobar'); 244 | }); 245 | 246 | assert.strictEqual(result.state, 'REJECTED'); 247 | }); 248 | }); 249 | -------------------------------------------------------------------------------- /test-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/ember/tsconfig.json", 3 | "glint": { 4 | "environment": ["ember-loose", "ember-template-imports"] 5 | }, 6 | "compilerOptions": { 7 | "skipLibCheck": true, 8 | "noEmit": true, 9 | "noEmitOnError": false, 10 | "declaration": false, 11 | "declarationMap": false, 12 | // The combination of `baseUrl` with `paths` allows Ember's classic package 13 | // layout, which is not resolvable with the Node resolution algorithm, to 14 | // work with TypeScript. 15 | "baseUrl": ".", 16 | "paths": { 17 | "test-app/tests/*": ["tests/*"], 18 | "test-app/*": ["app/*"], 19 | "*": ["types/*"] 20 | }, 21 | "types": ["ember-source/types"] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test-app/types/global.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracked-tools/ember-async-data/5b5f4d4b6c0df6dd0c003da938be3bc32f244382/test-app/types/global.d.ts -------------------------------------------------------------------------------- /test-app/types/test-app/index.d.ts: -------------------------------------------------------------------------------- 1 | import 'ember-source/types'; 2 | import 'ember-source/types/preview'; 3 | 4 | import '@glint/environment-ember-loose'; 5 | -------------------------------------------------------------------------------- /test-app/vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracked-tools/ember-async-data/5b5f4d4b6c0df6dd0c003da938be3bc32f244382/test-app/vendor/.gitkeep --------------------------------------------------------------------------------