├── .babelrc ├── .codecov.yml ├── .eslintrc.js ├── .github ├── dependabot.yml ├── label-commenter-dependabot.yml ├── labels.yml └── workflows │ ├── ci.yml │ ├── close-milestone.yml │ ├── dependabot-merge.yml │ ├── draft-release.yml │ ├── publish.yml │ ├── sync-labels.yml │ └── update-milestone.yml ├── .gitignore ├── .mergify.yml ├── .npmignore ├── .prettierrc ├── .vscode ├── settings.json └── tasks.json ├── GitReleaseManager.yaml ├── GitVersion.yml ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── binding.ts ├── binding │ └── NotifyPropertyChanged.ts ├── cache.ts ├── cache │ ├── AnonymousQuery.ts │ ├── Cache.ts │ ├── CacheUpdater.ts │ ├── Change.ts │ ├── ChangeAwareCache.ts │ ├── ChangeReason.ts │ ├── ChangeSet.ts │ ├── ChangeSetOperatorFunction.ts │ ├── Comparer.ts │ ├── DistinctChangeSet.ts │ ├── DynamicDataError.ts │ ├── ExpirableItem.ts │ ├── ICache.ts │ ├── ICacheUpdater.ts │ ├── IChangeSet.ts │ ├── IConnectableCache.ts │ ├── IGroupChangeSet.ts │ ├── IIntermediateCache.ts │ ├── IKey.ts │ ├── IKeyValueCollection.ts │ ├── IObservableCache.ts │ ├── IPagedChangeSet.ts │ ├── IQuery.ts │ ├── ISortedChangeSet.ts │ ├── ISourceCache.ts │ ├── ISourceUpdater.ts │ ├── IndexCalculator.ts │ ├── IntermediateCache.ts │ ├── KeyValueCollection.ts │ ├── Node.ts │ ├── ObservableCache.ts │ ├── PageRequest.ts │ ├── PageResponse.ts │ ├── ReaderWriter.ts │ ├── SortOptimizations.ts │ ├── SortedChangeSet.ts │ ├── SourceCache.ts │ ├── operators.ts │ └── operators │ │ ├── and.ts │ │ ├── asObservableCache.ts │ │ ├── autoRefresh.ts │ │ ├── autoRefreshOnObservable.ts │ │ ├── bind.ts │ │ ├── changeKey.ts │ │ ├── clone.ts │ │ ├── combineCache.ts │ │ ├── deferUntilLoaded.ts │ │ ├── disposeMany.ts │ │ ├── distinctValues.ts │ │ ├── except.ts │ │ ├── expireAfter.ts │ │ ├── filter.ts │ │ ├── forEachChange.ts │ │ ├── forExpiry.ts │ │ ├── groupOn.ts │ │ ├── groupOnDistinct.ts │ │ ├── groupOnProperty.ts │ │ ├── ignoreUpdateWhen.ts │ │ ├── includeUpdateWhen.ts │ │ ├── limitSizeTo.ts │ │ ├── mergeMany.ts │ │ ├── mergeManyItems.ts │ │ ├── notEmpty.ts │ │ ├── onItemAdded.ts │ │ ├── onItemRemoved.ts │ │ ├── onItemUpdated.ts │ │ ├── or.ts │ │ ├── other.ts │ │ ├── queryWhenChanged.ts │ │ ├── refCount.ts │ │ ├── skipInitial.ts │ │ ├── sort.ts │ │ ├── startWithEmpty.ts │ │ ├── statusMonitor.ts │ │ ├── subscribeMany.ts │ │ ├── suppressRefresh.ts │ │ ├── toCollection.ts │ │ ├── toObservableChangeSet.ts │ │ ├── toSortedCollection.ts │ │ ├── transform.ts │ │ ├── transformForced.ts │ │ ├── transformMany.ts │ │ ├── transformPromise.ts │ │ ├── transformToTree.ts │ │ ├── treatMovesAsRemoveAdd.ts │ │ ├── trueFor.ts │ │ ├── trueForAll.ts │ │ ├── trueForAny.ts │ │ ├── watch.ts │ │ ├── watchValue.ts │ │ ├── whenAnyPropertyChanged.ts │ │ ├── whenChanged.ts │ │ ├── whenPropertyChanged.ts │ │ ├── whenValueChanged.ts │ │ ├── whereReasonsAre.ts │ │ ├── whereReasonsAreNot.ts │ │ └── xor.ts ├── diagnostics.ts ├── diagnostics │ ├── ChangeStatistics.ts │ ├── ChangeSummary.ts │ ├── operators.ts │ └── operators │ │ └── CollectUpdateStats.ts ├── index.ts ├── notify.ts ├── notify │ ├── index.ts │ └── notifyPropertyChangedSymbol.ts ├── util.ts └── util │ ├── ArrayOrIterable.ts │ ├── CompositeDisposable.ts │ ├── Disposable.ts │ ├── DisposableBase.ts │ ├── Lazy.ts │ ├── RefCountDisposable.ts │ ├── SerialDisposable.ts │ ├── SingleAssignmentDisposable.ts │ ├── deepEqualMapAdapter.ts │ ├── emptyIterator.ts │ ├── index.ts │ ├── is.ts │ ├── isEqualityComparer.ts │ ├── isIterable.ts │ ├── isMap.ts │ ├── isSet.ts │ ├── isWeakMap.ts │ ├── isWeakSet.ts │ ├── tryGetValue.ts │ └── using.ts ├── test ├── binding │ ├── BindCacheFixture.spec.ts │ ├── BindEqualityTests.spec.ts │ ├── BindingListBindCacheSortedFixture.spec.ts │ ├── Clone.spec.ts │ ├── DeeplyNestedNotifyPropertyChangedFixture.cs │ └── NotifyPropertyChangedExFixture.cs ├── cache │ ├── And.spec.ts │ ├── AutoRefresh.spec.ts │ ├── BatchFixture.cs │ ├── BatchIfFixture.cs │ ├── BatchIfWithTimeOutFixture.cs │ ├── BufferInitialFixture.cs │ ├── DeferUntilLoadedFixture.spec.ts │ ├── DisposeManyFixture.spec.ts │ ├── DistinctFixture.spec.ts │ ├── EditDiffFixture.cs │ ├── ExceptFixture.spec.ts │ ├── ExpireAfterFixture.spec.ts │ ├── FilterControllerFixture.spec.ts │ ├── FilterFixture.spec.ts │ ├── ForEachChangeFixture.spec.ts │ ├── FullJoinFixture.cs │ ├── FullJoinManyFixture.cs │ ├── GroupControllerFixture.spec.ts │ ├── GroupControllerForFilteredItemsFixture.spec.ts │ ├── GroupFixture.spec.ts │ ├── GroupFromDistinctFixture.spec.ts │ ├── GroupOnPropertyFixture.spec.ts │ ├── IgnoreUpdateFixture.spec.ts │ ├── IncludeUpdateFixture.spec.ts │ ├── InnerJoinFixture.cs │ ├── InnerJoinManyFixture.cs │ ├── LeftJoinFixture.cs │ ├── LeftJoinManyFixture.cs │ ├── MergeManyFixture.spec.ts │ ├── MergeManyItemsFixture.spec.ts │ ├── MergeManyWithKeyOverloadFixture.spec.ts │ ├── MonitorStatusFixture.spec.ts │ ├── ObservableCachePreviewFixture.spec.ts │ ├── ObservableChangeSetFixture.cs │ ├── ObservableToObservableChangeSetFixture.spec.ts │ ├── OnItemFixture.spec.ts │ ├── Or.spec.ts │ ├── PageFixture.cs │ ├── QueryWhenChangedFixture.spec.ts │ ├── RefCountFixture.cs │ ├── RightJoinFixture.cs │ ├── RightJoinManyFixture.cs │ ├── SizeLimitFixture.cs │ ├── SortFixture.spec.ts │ ├── SortObservableFixtureFixture.spec.ts │ ├── SourceCache.spec.ts │ ├── SubscribeManyFixture.spec.ts │ ├── SwitchFixture.cs │ ├── TimeExpiryFixture.spec.ts │ ├── ToObservableChangeSetFixtureWithCompletion.spec.ts │ ├── ToSortedCollectionFixture.spec.ts │ ├── TransformFixture.spec.ts │ ├── TransformManyFixture.spec.ts │ ├── TransformManyRefreshFixture.spec.ts │ ├── TransformManySimpleFixture.spec.ts │ ├── TransformTreeFixture.spec.ts │ ├── TransformTreeWithRefreshFixture.spec.ts │ ├── TrueForAllFixture.spec.ts │ ├── TrueForAnyFixture.spec.ts │ ├── WatchFixture.spec.ts │ ├── XorFixture.spec.ts │ ├── regex.txt │ └── tsconfig.json ├── domain │ ├── Animal.ts │ ├── ParentAndChildren.ts │ ├── ParentChild.ts │ ├── Person.ts │ ├── PersonEmployment.ts │ ├── PersonObs.ts │ ├── PersonWithChildren.ts │ ├── PersonWithEmployment.ts │ ├── PersonWithFriends.ts │ ├── PersonWithGender.ts │ ├── PersonWithRelations.ts │ ├── Pet.ts │ ├── RandomPersonGenerator.ts │ └── SelfObservingPerson.cs └── util │ ├── aggregator.ts │ └── indexed.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── tsconfig.json └── wallaby.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-transform-for-of" 4 | ] 5 | } -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: no 3 | strict_yaml_branch: master 4 | 5 | coverage: 6 | round: down 7 | precision: 1 8 | 9 | status: 10 | project: yes 11 | patch: yes 12 | changes: no 13 | 14 | comment: 15 | branch: master 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:promise/recommended', 11 | 'plugin:unicorn/recommended', 12 | 'plugin:prettier/recommended', 13 | 'prettier/babel', 14 | 'prettier/standard', 15 | 'prettier/unicorn', 16 | 'prettier/@typescript-eslint', 17 | ], 18 | plugins: [], 19 | globals: { 20 | Atomics: 'readonly', 21 | SharedArrayBuffer: 'readonly', 22 | }, 23 | parser: '@typescript-eslint/parser', 24 | parserOptions: { 25 | ecmaVersion: 2018, 26 | sourceType: 'module', 27 | }, 28 | rules: { 29 | quotes: ['error', 'single'], 30 | semi: ['error', 'always'], 31 | 'no-undef': 'off', 32 | 'no-unused-vars': 'off', 33 | // '@typescript-eslint/no-unused-vars': 'off', 34 | 'unicorn/filename-case': 'off', 35 | 'no-nested-ternary': 'off', 36 | 'unicorn/no-null': 'off', 37 | 'unicorn/no-nested-ternary': 'off', 38 | 'unicorn/no-useless-undefined': 'off', 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'daily' 7 | assignees: 8 | - 'david-driscoll' 9 | open-pull-requests-limit: 100 10 | 11 | - package-ecosystem: 'npm' 12 | directory: '/' 13 | schedule: 14 | interval: 'daily' 15 | assignees: 16 | - 'david-driscoll' 17 | open-pull-requests-limit: 100 -------------------------------------------------------------------------------- /.github/label-commenter-dependabot.yml: -------------------------------------------------------------------------------- 1 | labels: 2 | - name: ':shipit: merge' 3 | labeled: 4 | pr: 5 | body: '@dependabot squash and merge' 6 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | - name: ":beetle: bug" 2 | color: "d73a4a" 3 | description: "Something isn't working" 4 | - name: ":blue_book: documentation" 5 | color: 0075ca 6 | description: "Improvements or additions to documentation" 7 | - name: ":family: duplicate" 8 | color: "cfd3d7" 9 | description: "This issue or pull request already exists" 10 | - name: ":fire: enhancement" 11 | color: "a2eeef" 12 | description: "New feature or request" 13 | - name: "help wanted" 14 | color: "008672" 15 | description: "Extra attention is needed" 16 | - name: "good first issue" 17 | color: "7057ff" 18 | description: "Good for newcomers" 19 | - name: ":x: invalid" 20 | color: "e4e669" 21 | description: "This doesn't seem right" 22 | - name: ":grey_question: question" 23 | color: "d876e3" 24 | description: "Further information is requested" 25 | - name: ":lock: wontfix" 26 | color: "ffffff" 27 | description: "This will not be worked on" 28 | - name: ":rocket: feature" 29 | color: "ccf5ff" 30 | description: "This adds some form of new functionality" 31 | - name: ":boom: breaking change" 32 | color: "efa7ae" 33 | description: "This breaks existing behavior" 34 | - name: ":sparkles: mysterious" 35 | color: "cccccc" 36 | description: "We forgot to label this" 37 | - name: ":hammer: chore" 38 | color: "27127c" 39 | description: "Just keeping things neat and tidy" 40 | - name: ":package: dependencies" 41 | color: "edc397" 42 | description: "Pull requests that update a dependency file" 43 | - name: ":shipit: merge" 44 | color: "98ed98" 45 | description: "Shipit!" 46 | - name: ":construction: deprecated" 47 | color: "dd824d" 48 | description: "Deprecated functionality" 49 | - name: ":wastebasket: removed" 50 | color: "fce99f" 51 | description: "Removed functionality" 52 | - name: ":old_key: security" 53 | color: "cbce1e" 54 | description: "Security related issue" -------------------------------------------------------------------------------- /.github/workflows/close-milestone.yml: -------------------------------------------------------------------------------- 1 | name: Close Milestone 2 | on: 3 | release: 4 | types: 5 | - released 6 | jobs: 7 | close_milestone: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | 15 | - name: Install GitVersion 16 | uses: gittools/actions/gitversion/setup@master 17 | with: 18 | versionSpec: '5.x' 19 | 20 | - name: Install GitReleaseManager 21 | uses: gittools/actions/gitreleasemanager/setup@master 22 | with: 23 | versionSpec: '0.11.x' 24 | 25 | - name: Use GitVersion 26 | id: gitversion 27 | uses: gittools/actions/gitversion/execute@master 28 | 29 | # Ensure the milestone exists 30 | - name: Create Milestone 31 | uses: WyriHaximus/github-action-create-milestone@0.1.0 32 | with: 33 | title: v${{ steps.gitversion.outputs.majorMinorPatch }} 34 | env: 35 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 36 | continue-on-error: true 37 | 38 | # move any issues to that milestone in the event the release is renamed 39 | - name: sync milestones 40 | uses: RocketSurgeonsGuild/actions/sync-milestone@v0.2.4 41 | with: 42 | default-label: ':sparkles: mysterious' 43 | github-token: ${{ secrets.BOT_TOKEN }} 44 | 45 | - name: Get Repo and Owner 46 | shell: pwsh 47 | id: repository 48 | if: ${{ !github.event.release.prerelease && steps.gitversion.outputs.preReleaseTag == '' }} 49 | run: | 50 | $parts = $ENV:GITHUB_REPOSITORY.Split('/') 51 | echo "::set-output name=owner::$($parts[0])" 52 | echo "::set-output name=repository::$($parts[1])" 53 | 54 | - name: Close Milestone 55 | shell: pwsh 56 | if: ${{ !github.event.release.prerelease && steps.gitversion.outputs.preReleaseTag == '' }} 57 | run: | 58 | dotnet gitreleasemanager close ` 59 | -o "${{ steps.repository.outputs.owner }}" ` 60 | -r "${{ steps.repository.outputs.repository }}" ` 61 | --token "${{ secrets.GITHUB_TOKEN }}" ` 62 | -m "v${{ steps.gitversion.outputs.majorMinorPatch }}" 63 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Commenter 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - labeled 7 | 8 | jobs: 9 | comment: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Dump GitHub context 13 | env: 14 | GITHUB_CONTEXT: ${{ toJson(github) }} 15 | run: echo "$GITHUB_CONTEXT" 16 | - name: Dump job context 17 | env: 18 | JOB_CONTEXT: ${{ toJson(job) }} 19 | run: echo "$JOB_CONTEXT" 20 | - name: Dump steps context 21 | env: 22 | STEPS_CONTEXT: ${{ toJson(steps) }} 23 | run: echo "$STEPS_CONTEXT" 24 | - name: Dump runner context 25 | env: 26 | RUNNER_CONTEXT: ${{ toJson(runner) }} 27 | run: echo "$RUNNER_CONTEXT" 28 | - uses: actions/checkout@v2 29 | with: 30 | ref: master 31 | - name: Dependabot Commenter 32 | if: | 33 | (github.event.label.name == ':shipit: merge') && (github.event.pull_request.user.login == 'dependabot[bot]' || github.event.pull_request.user.login == 'dependabot-preview[bot]') 34 | uses: peaceiris/actions-label-commenter@v1.6.0 35 | with: 36 | github_token: ${{ secrets.BOT_TOKEN }} 37 | config_file: .github/label-commenter-dependabot.yml 38 | -------------------------------------------------------------------------------- /.github/workflows/draft-release.yml: -------------------------------------------------------------------------------- 1 | name: Create Milestone and Draft Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths-ignore: 7 | - '**/*.md' 8 | jobs: 9 | create_milestone_and_draft_release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Fetch all history for all tags and branches 18 | run: git fetch --prune 19 | 20 | - name: Install GitVersion 21 | uses: gittools/actions/gitversion/setup@master 22 | with: 23 | versionSpec: '5.x' 24 | 25 | - name: Install GitReleaseManager 26 | uses: gittools/actions/gitreleasemanager/setup@master 27 | with: 28 | versionSpec: '0.11.x' 29 | 30 | - name: Use GitVersion 31 | id: gitversion 32 | uses: gittools/actions/gitversion/execute@master 33 | 34 | - name: Create Milestone 35 | uses: WyriHaximus/github-action-create-milestone@0.1.0 36 | with: 37 | title: v${{ steps.gitversion.outputs.majorMinorPatch }} 38 | env: 39 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 40 | continue-on-error: true 41 | 42 | - name: Get Repo and Owner 43 | shell: pwsh 44 | id: repository 45 | run: | 46 | $parts = $ENV:GITHUB_REPOSITORY.Split('/') 47 | echo "::set-output name=owner::$($parts[0])" 48 | echo "::set-output name=repository::$($parts[1])" 49 | 50 | - name: sync milestones 51 | uses: RocketSurgeonsGuild/actions/sync-milestone@v0.2.4 52 | with: 53 | default-label: ':sparkles: mysterious' 54 | github-token: ${{ secrets.GITHUB_TOKEN }} 55 | 56 | - name: Create Draft Release 57 | shell: pwsh 58 | run: | 59 | dotnet gitreleasemanager create ` 60 | -o "${{ steps.repository.outputs.owner }}" ` 61 | -r "${{ steps.repository.outputs.repository }}" ` 62 | --token "${{ secrets.BOT_TOKEN }}" ` 63 | -m "v${{ steps.gitversion.outputs.majorMinorPatch }}" 64 | 65 | - name: Export Changelog 66 | shell: pwsh 67 | run: | 68 | dotnet gitreleasemanager export ` 69 | -o "${{ steps.repository.outputs.owner }}" ` 70 | -r "${{ steps.repository.outputs.repository }}" ` 71 | --token "${{ secrets.GITHUB_TOKEN }}" ` 72 | -f CHANGELOG.md 73 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Publish release 3 | 4 | on: 5 | release: 6 | types: [created] 7 | 8 | jobs: 9 | publish: 10 | needs: build 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | clean: 'false' 16 | fetch-depth: '0' 17 | 18 | - uses: actions/setup-node@v1 19 | with: 20 | always-auth: true 21 | node-version: '12.x' 22 | registry-url: https://registry.npmjs.org 23 | 24 | - name: 💰 cache modules 25 | uses: actions/cache@v2 26 | with: 27 | # npm cache files are stored in `~/.npm` on Linux/macOS 28 | path: ~/.npm 29 | key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }} 30 | restore-keys: | 31 | ${{ runner.OS }}-node- 32 | ${{ runner.OS }}- 33 | 34 | - name: Fetch all history for all tags and branches 35 | run: | 36 | git fetch --prune 37 | 38 | - name: Install GitVersion 39 | uses: gittools/actions/gitversion/setup@master 40 | with: 41 | versionSpec: '5.x' 42 | 43 | - name: Use GitVersion 44 | id: gitversion 45 | uses: gittools/actions/gitversion/execute@master 46 | 47 | - name: 🆚 npm version 48 | shell: pwsh 49 | run: | 50 | $data = gc .\package.json -Raw | ConvertFrom-Json; 51 | $data.version = '${{ steps.gitversion.outputs.fullSemVer }}'; 52 | set-content .\package.json ($data|ConvertTo-Json) 53 | 54 | - name: 🎁 npm install 55 | run: npm ci 56 | 57 | - name: 📦 npm publish (latest) 58 | run: npm publish --tag latest 59 | env: 60 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 61 | -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yml: -------------------------------------------------------------------------------- 1 | name: Sync Labels 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - .github/workflows/sync-labels.yml 8 | - .github/labels.yml 9 | schedule: 10 | - cron: '0 0 * * 4' 11 | 12 | jobs: 13 | sync_labels: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | 19 | - name: Checkout tools repo 20 | uses: actions/checkout@v2 21 | with: 22 | repository: RocketSurgeonsGuild/.github 23 | path: .rsg 24 | 25 | - name: merge files 26 | uses: RocketSurgeonsGuild/actions/merge-labels@v0.2.4 27 | with: 28 | files: '.rsg/.github/labels.yml,.github/labels.yml' 29 | output: .github/labels.yml 30 | 31 | - name: Run Labeler 32 | if: success() 33 | uses: crazy-max/ghaction-github-labeler@v3.0.0 34 | with: 35 | yaml_file: .github/labels.yml 36 | skip_delete: false 37 | dry_run: false 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/update-milestone.yml: -------------------------------------------------------------------------------- 1 | name: Update Milestone 2 | on: 3 | pull_request_target: 4 | types: 5 | - closed 6 | - opened 7 | - reopened 8 | - synchronize 9 | 10 | jobs: 11 | update_milestone: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: sync milestones 18 | uses: RocketSurgeonsGuild/actions/sync-milestone@v0.2.4 19 | with: 20 | default-label: ':sparkles: mysterious' 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and not Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Stores VSCode versions used for testing VSCode extensions 107 | .vscode-test 108 | .idea/ 109 | docs/ 110 | **/*.js 111 | !./*.js 112 | src/**/*.d.ts 113 | test/**/*.d.ts 114 | *.d.ts 115 | src/**/*.js.map 116 | test/**/*.js.map 117 | *.js.map -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: automatic merge when GitHub branch protection passes (others) 3 | conditions: 4 | - base=master 5 | - -author~=^dependabot(|-preview)\[bot\]$ 6 | - 'label=:shipit: merge' 7 | actions: 8 | merge: 9 | method: squash 10 | strict: smart+fasttrack 11 | - name: auto merge github-actions 12 | conditions: 13 | - 'label=github-actions' 14 | - author~=^dependabot(|-preview)\[bot\]$ 15 | actions: 16 | label: 17 | add: 18 | - ':shipit: merge' 19 | - name: automatic merge when GitHub branch protection passes 20 | conditions: 21 | - merged 22 | - 'label=:shipit: merge' 23 | actions: 24 | label: 25 | remove: 26 | - ':shipit: merge' 27 | - name: delete head branch after merge 28 | conditions: 29 | - merged 30 | actions: 31 | label: 32 | remove: 33 | - ':shipit: merge' 34 | delete_head_branch: {} 35 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | !**/*.d.ts 2 | !**/*.js* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 180, 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "arrowParens": "avoid", 8 | "useTabs": false 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true, 8 | "binding": true, 9 | "cache": true, 10 | "diagnostics": true, 11 | "notify": true, 12 | "util": true, 13 | "*.d.ts": true, 14 | "*.js": { 15 | "when": "$(basename).js.map" 16 | }, 17 | "*.js.map": true 18 | } 19 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "typescript", 6 | "tsconfig": "tsconfig.json", 7 | "option": "watch", 8 | "problemMatcher": [ 9 | "$tsc-watch" 10 | ], 11 | "group": "build", 12 | "label": "tsc: watch - tsconfig.json" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /GitReleaseManager.yaml: -------------------------------------------------------------------------------- 1 | # create: 2 | # include-footer: false 3 | # footer-heading: '' 4 | # footer-content: '' 5 | # footer-includes-milestone: true 6 | export: 7 | include-created-date-in-title: true 8 | created-date-string-format: MMMM dd, yyyy 9 | perform-regex-removal: true 10 | regex-text: '([a-f\d]{40}\s|:boom:|:rocket:|:fire:|:blue_book:|:beetle:|:squirrel:|:raised_hand:|:sparkles:|:package:|:old_key:|:wastebasket:|:construction:)' 11 | issue-labels-include: 12 | - ':boom: breaking change' 13 | - ':rocket: feature' 14 | - ':fire: enhancement' 15 | - ':old_key: security' 16 | - ':blue_book: documentation' 17 | - ':beetle: bug' 18 | - ':hammer: chore' 19 | - 'good first issue' 20 | - 'help wanted' 21 | - ':sparkles: mysterious' 22 | - ':package: dependencies' 23 | - ':construction: deprecated' 24 | - ':wastebasket: removed' 25 | issue-labels-exclude: 26 | - ':family: duplicate' 27 | - ':grey_question: question' 28 | - ':lock: wontfix' 29 | - ':shipit: merge' 30 | - ':x: invalid' 31 | -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | assembly-versioning-scheme: MajorMinorPatch 2 | mode: ContinuousDeployment 3 | continuous-delivery-fallback-tag: beta 4 | next-version: 0.1.0 5 | branches: 6 | next: 7 | regex: ^next$ 8 | mode: ContinuousDeployment 9 | tag: next 10 | increment: Major 11 | prevent-increment-of-merged-branch-version: false 12 | track-merge-target: false 13 | tracks-release-branches: false 14 | is-release-branch: false 15 | source-branches: ['master'] 16 | ignore: 17 | sha: [] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dynamicdata 2 | 3 | A port of DynamicData from C# to TypeScript / JavaScript 4 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | collectCoverage: true, 5 | globals: { 6 | 'ts-jest': { 7 | compiler: 'typescript', 8 | tsConfig: { 9 | importHelpers: true, 10 | target: 'ES6', 11 | module: 'commonjs', 12 | }, 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynamicdatajs", 3 | "version": "0.1.0", 4 | "description": "", 5 | "author": "David Driscoll", 6 | "module": "esm/index.js", 7 | "main": "index.js", 8 | "typings": "index.d.ts", 9 | "files": [ 10 | "esm", 11 | "binding", 12 | "cache", 13 | "diagnostics", 14 | "notify", 15 | "util", 16 | "src", 17 | "*.js*", 18 | "*.d.ts" 19 | ], 20 | "scripts": { 21 | "build": "tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json", 22 | "test": "jest", 23 | "lint": "eslint --fix src/**/*.ts test/**/*.ts", 24 | "prepare": "npm run build" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/david-driscoll/DynamicDataJs.git" 29 | }, 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/david-driscoll/DynamicDataJs/issues" 33 | }, 34 | "homepage": "https://github.com/david-driscoll/DynamicDataJs#readme", 35 | "devDependencies": { 36 | "@babel/core": "^7.11.6", 37 | "@babel/plugin-transform-for-of": "^7.10.4", 38 | "@babel/preset-env": "^7.11.5", 39 | "@babel/preset-typescript": "^7.10.4", 40 | "@types/faker": "^5.1.0", 41 | "@types/jest": "^26.0.13", 42 | "@types/node": "^14.10.1", 43 | "@types/webpack": "^4.41.22", 44 | "@typescript-eslint/eslint-plugin": "^4.1.0", 45 | "@typescript-eslint/parser": "^4.1.0", 46 | "@vue/reactivity": "^3.0.0-rc.10", 47 | "babel-jest": "^26.3.0", 48 | "cross-env": "^7.0.2", 49 | "eslint": "^7.9.0", 50 | "eslint-config-prettier": "^6.11.0", 51 | "eslint-config-standard": "^14.1.1", 52 | "eslint-plugin-import": "^2.22.0", 53 | "eslint-plugin-node": "^11.1.0", 54 | "eslint-plugin-prettier": "^3.1.4", 55 | "eslint-plugin-promise": "^4.2.1", 56 | "eslint-plugin-standard": "^4.0.1", 57 | "eslint-plugin-unicorn": "^21.0.0", 58 | "faker": "^5.1.0", 59 | "husky": "^4.3.0", 60 | "ix": "^4.0.0", 61 | "jest": "^26.4.2", 62 | "lint-staged": "^10.3.0", 63 | "prettier": "^2.1.1", 64 | "rxjs": "^6.6.3", 65 | "ts-jest": "^26.3.0", 66 | "ts-node": "^9.0.0", 67 | "tsdx": "^0.13.3", 68 | "typedoc": "^0.19.1", 69 | "typescript": "^4.0.2" 70 | }, 71 | "dependencies": { 72 | "binary-search": "^1.3.6", 73 | "fast-deep-equal": "^3.1.3" 74 | }, 75 | "peerDependencies": { 76 | "ix": "^4.0.0", 77 | "rxjs": "^6.0.0" 78 | }, 79 | "lint-staged": { 80 | "*.{ts}": [ 81 | "eslint --fix", 82 | "prettier --fix" 83 | ] 84 | }, 85 | "husky": { 86 | "hooks": { 87 | "pre-commit": "lint-staged" 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/binding.ts: -------------------------------------------------------------------------------- 1 | export * from './binding/NotifyPropertyChanged'; 2 | -------------------------------------------------------------------------------- /src/cache.ts: -------------------------------------------------------------------------------- 1 | export * from './cache/Cache'; 2 | export * from './cache/CacheUpdater'; 3 | export * from './cache/Change'; 4 | export * from './cache/ChangeAwareCache'; 5 | export * from './cache/ChangeReason'; 6 | export * from './cache/ChangeSet'; 7 | export * from './cache/ChangeSetOperatorFunction'; 8 | export * from './cache/Comparer'; 9 | export * from './cache/DistinctChangeSet'; 10 | export * from './cache/DynamicDataError'; 11 | export * from './cache/ExpirableItem'; 12 | export * from './cache/ICache'; 13 | export * from './cache/ICacheUpdater'; 14 | export * from './cache/IChangeSet'; 15 | export * from './cache/IConnectableCache'; 16 | export * from './cache/IGroupChangeSet'; 17 | export * from './cache/IKey'; 18 | export * from './cache/IKeyValueCollection'; 19 | export * from './cache/IndexCalculator'; 20 | export * from './cache/IObservableCache'; 21 | export * from './cache/IPagedChangeSet'; 22 | export * from './cache/IQuery'; 23 | export * from './cache/ISortedChangeSet'; 24 | export * from './cache/ISourceCache'; 25 | export * from './cache/ISourceUpdater'; 26 | export * from './cache/KeyValueCollection'; 27 | export * from './cache/Node'; 28 | export * from './cache/ObservableCache'; 29 | export * from './cache/PageRequest'; 30 | export * from './cache/PageResponse'; 31 | export * from './cache/SortedChangeSet'; 32 | export * from './cache/SortOptimizations'; 33 | export * from './cache/SourceCache'; 34 | -------------------------------------------------------------------------------- /src/cache/AnonymousQuery.ts: -------------------------------------------------------------------------------- 1 | import { IQuery } from './IQuery'; 2 | import { Cache } from './Cache'; 3 | 4 | export class AnonymousQuery implements IQuery { 5 | private readonly _cache: Cache; 6 | 7 | public constructor(cache: Cache) { 8 | this._cache = cache; 9 | } 10 | 11 | get size() { 12 | return this._cache.size; 13 | } 14 | 15 | [Symbol.iterator](): IterableIterator<[TKey, TObject]> { 16 | return this._cache[Symbol.iterator](); 17 | } 18 | 19 | entries(): IterableIterator<[TKey, TObject]> { 20 | return this._cache.entries(); 21 | } 22 | 23 | keys(): IterableIterator { 24 | return this._cache.keys(); 25 | } 26 | 27 | lookup(key: TKey): TObject | undefined { 28 | return this._cache.lookup(key); 29 | } 30 | 31 | values(): IterableIterator { 32 | return this._cache.values(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/cache/ChangeReason.ts: -------------------------------------------------------------------------------- 1 | export type ChangeReason = 'add' | 'update' | 'remove' | 'refresh' | 'moved'; 2 | 3 | export function isChangeReason(value: any) { 4 | return typeof value === 'string' && (value == 'add' || value == 'update' || value == 'remove' || value == 'refresh' || value == 'moved'); 5 | } 6 | -------------------------------------------------------------------------------- /src/cache/ChangeSet.ts: -------------------------------------------------------------------------------- 1 | import { Change } from './Change'; 2 | import { IChangeSet } from './IChangeSet'; 3 | import { ArrayOrIterable } from '../util/ArrayOrIterable'; 4 | import { ChangeReason } from './ChangeReason'; 5 | 6 | /** 7 | * A collection of changes 8 | */ 9 | export class ChangeSet extends Set> implements IChangeSet { 10 | /** 11 | * An empty change set 12 | */ 13 | public static empty() { 14 | return new ChangeSet(); 15 | } 16 | 17 | public constructor(collection?: ArrayOrIterable>) { 18 | super(collection); 19 | } 20 | 21 | public forReason(reason: ChangeReason) { 22 | let count = 0; 23 | for (const item of this.values()) { 24 | if (item.reason == reason) count++; 25 | } 26 | return count; 27 | } 28 | 29 | public get adds() { 30 | return this.forReason('add'); 31 | } 32 | 33 | public get updates() { 34 | return this.forReason('update'); 35 | } 36 | 37 | public get removes() { 38 | return this.forReason('remove'); 39 | } 40 | 41 | public get refreshes() { 42 | return this.forReason('refresh'); 43 | } 44 | 45 | public get moves() { 46 | return this.forReason('moved'); 47 | } 48 | 49 | public toString() { 50 | return `ChangeSet. Count=${this.size}`; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/cache/ChangeSetOperatorFunction.ts: -------------------------------------------------------------------------------- 1 | import { IChangeSet } from './IChangeSet'; 2 | import { MonoTypeOperatorFunction, Observable, OperatorFunction } from 'rxjs'; 3 | import { Group } from './IGroupChangeSet'; 4 | import { ISortedChangeSet } from './ISortedChangeSet'; 5 | import { IPagedChangeSet } from './IPagedChangeSet'; 6 | import { DistinctChangeSet } from './DistinctChangeSet'; 7 | 8 | export type MonoTypeChangeSetOperatorFunction = MonoTypeOperatorFunction>; 9 | export type ChangeSetOperatorFunction = OperatorFunction< 10 | IChangeSet, 11 | IChangeSet 12 | >; 13 | -------------------------------------------------------------------------------- /src/cache/Comparer.ts: -------------------------------------------------------------------------------- 1 | export type Comparer = (a: T, b: T) => number /* (-1 | 0 | 1) */; 2 | export type KeyValueComparer = Comparer; 3 | export function keyValueComparer(keyComparer: Comparer, valueComparer?: Comparer): KeyValueComparer { 4 | return function innerKeyValueComparer([aKey, aValue]: readonly [TKey, TObject], [bKey, bValue]: readonly [TKey, TObject]) { 5 | if (valueComparer) { 6 | const result = valueComparer(aValue, bValue); 7 | if (result !== 0) { 8 | return result; 9 | } 10 | } 11 | return keyComparer(aKey, bKey); 12 | }; 13 | } 14 | 15 | export function defaultComparer(a: T, b: T) { 16 | return a === b ? 0 : a > b ? 1 : -1; 17 | } 18 | -------------------------------------------------------------------------------- /src/cache/DistinctChangeSet.ts: -------------------------------------------------------------------------------- 1 | import { IChangeSet } from './IChangeSet'; 2 | export type DistinctChangeSet = IChangeSet; 3 | -------------------------------------------------------------------------------- /src/cache/DynamicDataError.ts: -------------------------------------------------------------------------------- 1 | export type DynamicDataError = { key: TKey; value: TObject; error: Error }; 2 | -------------------------------------------------------------------------------- /src/cache/ExpirableItem.ts: -------------------------------------------------------------------------------- 1 | export type ExpirableItem = { 2 | readonly value: TObject; 3 | readonly key: TKey; 4 | readonly expireAt: number; 5 | readonly index?: number; 6 | }; 7 | -------------------------------------------------------------------------------- /src/cache/ICache.ts: -------------------------------------------------------------------------------- 1 | import { IQuery } from './IQuery'; 2 | import { IChangeSet } from './IChangeSet'; 3 | import { ArrayOrIterable } from '../util/ArrayOrIterable'; 4 | 5 | /** 6 | * A cache which captures all changes which are made to it. These changes are recorded until CaptureChanges() at which point thw changes are cleared. 7 | * 8 | * Used for creating custom operators 9 | * @typeparam TObject The type of the object. 10 | * @typeparam TKey The type of the key. 11 | */ 12 | export interface ICache extends IQuery { 13 | /** 14 | * Clones the cache from the specified changes 15 | * @param changes The changes. 16 | */ 17 | clone(changes: IChangeSet): void; 18 | 19 | /** 20 | * Adds or updates the item using the specified key 21 | * @param item The item. 22 | * @param key The key. 23 | */ 24 | addOrUpdate(item: TObject, key: TKey): void; 25 | 26 | /** 27 | * Removes the item matching the specified key. 28 | * @param key The key. 29 | */ 30 | removeKey(key: TKey): void; 31 | 32 | /** 33 | * Removes all items matching the specified keys 34 | * @param keys The keys. 35 | */ 36 | removeKeys(keys: ArrayOrIterable): void; 37 | 38 | /** 39 | * Clears all items 40 | */ 41 | clear(): void; 42 | 43 | /** 44 | * Sends a signal for operators to recalculate it's state 45 | */ 46 | refresh(): void; 47 | 48 | /** 49 | * Refreshes the items matching the specified keys 50 | * @param keys The keys. 51 | */ 52 | refreshKeys(keys: ArrayOrIterable): void; 53 | 54 | /** 55 | * Refreshes the item matching the specified key 56 | */ 57 | refreshKey(key: TKey): void; 58 | 59 | /** 60 | * The tag of this class, should return `Cache` 61 | */ 62 | readonly [Symbol.toStringTag]: 'Cache'; 63 | } 64 | -------------------------------------------------------------------------------- /src/cache/ICacheUpdater.ts: -------------------------------------------------------------------------------- 1 | import { IQuery } from './IQuery'; 2 | import { IChangeSet } from './IChangeSet'; 3 | import { ArrayOrIterable } from '../util/ArrayOrIterable'; 4 | 5 | /** 6 | * Api for updating an intermediate cache 7 | * 8 | * Use edit to produce singular changeset. 9 | * 10 | * NB:The evaluate method is used to signal to any observing operators 11 | * to reevaluate whether the the object still matches downstream operators. 12 | * This is primarily targeted to inline object changes such as datetime and calculated fields. 13 | * 14 | * @typeparam TObject The type of the object. 15 | */ 16 | export interface ICacheUpdater extends IQuery { 17 | /** 18 | * Adds or updates the specified key value pairs 19 | */ 20 | addOrUpdatePairs(entries: ArrayOrIterable<[TKey, TObject]>): void; 21 | 22 | /** 23 | * Adds or updates the specified item / key pair 24 | */ 25 | addOrUpdate(item: TObject, key: TKey): void; 26 | 27 | /** 28 | * Sends a signal for operators to recalculate it's state 29 | */ 30 | refresh(): void; 31 | 32 | /** 33 | * Refreshes the items matching the specified keys 34 | * @param keys The keys. 35 | */ 36 | refreshKeys(keys: ArrayOrIterable): void; 37 | 38 | /** 39 | * Refreshes the item matching the specified key 40 | * @param key The key. 41 | */ 42 | refreshKey(key: TKey): void; 43 | 44 | /** 45 | * Removes the specified keys 46 | * @param key The key. 47 | */ 48 | removeKeys(key: ArrayOrIterable): void; 49 | 50 | /** 51 | * Remove the specified key 52 | * @param key The key. 53 | */ 54 | removeKey(key: TKey): void; 55 | 56 | /** 57 | * Removes the specified key value pairs 58 | * @param entries The entries. 59 | */ 60 | removePairs(entries: ArrayOrIterable<[TKey, TObject]>): void; 61 | 62 | /** 63 | * Clones the change set to the cache 64 | */ 65 | clone(changes: IChangeSet): void; 66 | 67 | /** 68 | * Clears all items from the underlying cache. 69 | */ 70 | clear(): void; 71 | 72 | /** 73 | * Gets the key associated with the object 74 | * @param item The item. 75 | */ 76 | getKey(item: TObject): TKey; 77 | 78 | /** 79 | * Gets the key values for the specified items 80 | * @param values The values. 81 | */ 82 | entries(values?: ArrayOrIterable): IterableIterator<[TKey, TObject]>; 83 | } 84 | -------------------------------------------------------------------------------- /src/cache/IChangeSet.ts: -------------------------------------------------------------------------------- 1 | import { Change } from './Change'; 2 | 3 | /** 4 | * A collection of changes. 5 | * 6 | * Changes are always published in the order. 7 | * @typeparam TObject The type of the object 8 | * @typeparam TKey The type of the key 9 | */ 10 | export interface IChangeSet { 11 | /** 12 | * Gets the number of additions 13 | */ 14 | readonly adds: number; 15 | 16 | /** 17 | * Gets the number of removes 18 | */ 19 | readonly removes: number; 20 | 21 | /** 22 | * The number of refreshes 23 | */ 24 | readonly refreshes: number; 25 | 26 | /** 27 | * Gets the number of moves 28 | */ 29 | readonly moves: number; 30 | 31 | /** 32 | * The total update count 33 | */ 34 | readonly size: number; 35 | 36 | /** 37 | * The number of updates 38 | */ 39 | readonly updates: number; 40 | 41 | /** Iterator of values in the changeset. */ 42 | [Symbol.iterator](): IterableIterator>; 43 | 44 | /** 45 | * Returns an iterable of key, value pairs for every entry in the changeset 46 | */ 47 | values(): IterableIterator>; 48 | 49 | /** 50 | * foreach item in the changeset 51 | * @param callbackfn 52 | * @param thisArg 53 | */ 54 | forEach(callbackfn: (value: Change) => void, thisArgument?: any): void; 55 | } 56 | -------------------------------------------------------------------------------- /src/cache/IConnectableCache.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { Change } from './Change'; 3 | import { IChangeSet } from './IChangeSet'; 4 | 5 | /** 6 | * A cache for observing and querying in memory data 7 | * @typeparam TObject The type of the object 8 | * @typeparam TKey The type of the key 9 | */ 10 | export interface IConnectableCache { 11 | /** 12 | * Returns an observable of any changes which match the specified key. The sequence starts with the initial item in the cache (if there is one). 13 | * @param key The key 14 | */ 15 | watch(key: TKey): Observable>; 16 | 17 | /** 18 | * Returns a filtered stream of cache changes preceded with the initial filtered state 19 | * @param predicate The result will be filtered using the specified predicate 20 | */ 21 | connect(predicate?: (object: TObject) => boolean): Observable>; 22 | 23 | /** 24 | * Returns a filtered stream of cache changes. 25 | * Unlike Connect(), the returned observable is not prepended with the caches initial items. 26 | * 27 | * @param predicate The result will be filtered using the specified predicate 28 | */ 29 | preview(predicate?: (object: TObject) => boolean): Observable>; 30 | 31 | /** 32 | * A count changed observable starting with the current count 33 | */ 34 | readonly countChanged: Observable; 35 | } 36 | -------------------------------------------------------------------------------- /src/cache/IGroupChangeSet.ts: -------------------------------------------------------------------------------- 1 | import { IChangeSet } from './IChangeSet'; 2 | import { ChangeSet } from './ChangeSet'; 3 | import { ArrayOrIterable } from '../util/ArrayOrIterable'; 4 | import { Change } from './Change'; 5 | import { IObservableCache } from './IObservableCache'; 6 | import { IntermediateCache } from './IntermediateCache'; 7 | import { ICacheUpdater } from './ICacheUpdater'; 8 | 9 | export type Group = Iterable & { key: TGroupKey; cache: IObservableCache }; 10 | 11 | // export interface IGroupChangeSet extends IChangeSet, TGroupKey> {} 12 | 13 | export class ManagedGroup implements Group { 14 | private readonly _cache = new IntermediateCache(); 15 | 16 | constructor(public readonly key: TGroupKey) {} 17 | 18 | [Symbol.iterator]() { 19 | return this._cache.values(); 20 | } 21 | 22 | get cache() { 23 | return this._cache; 24 | } 25 | 26 | /** 27 | * @internal 28 | */ 29 | update(updateAction: (updater: ICacheUpdater) => void) { 30 | this._cache.edit(updateAction); 31 | } 32 | 33 | get size() { 34 | return this._cache.size; 35 | } 36 | 37 | /** 38 | * @internal 39 | */ 40 | getInitialUpdates() { 41 | return this._cache.getInitialUpdates(); 42 | } 43 | } 44 | 45 | export class GroupChangeSet 46 | extends ChangeSet, TGroupKey> 47 | implements IChangeSet, TGroupKey> { 48 | constructor(collection?: ArrayOrIterable, TGroupKey>>) { 49 | super(collection); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/cache/IIntermediateCache.ts: -------------------------------------------------------------------------------- 1 | import { ICacheUpdater } from './ICacheUpdater'; 2 | import { IObservableCache } from './IObservableCache'; 3 | /** 4 | * An observable cache which exposes an update API. 5 | * 6 | * Intended to be used as a helper for creating custom operators. 7 | * @typeparam TObject The type of the object 8 | * @typeparam TKey The type of the key 9 | */ 10 | export interface IIntermediateCache extends IObservableCache { 11 | /** 12 | * Action to apply a batch update to a cache. Multiple update methods can be invoked within a single batch operation. 13 | * These operations are invoked within the cache's lock and is therefore thread safe. 14 | * The result of the action will produce a single changeset 15 | * @param updateAction The update action 16 | */ 17 | edit(updateAction: (updater: ICacheUpdater) => void): void; 18 | } 19 | -------------------------------------------------------------------------------- /src/cache/IKey.ts: -------------------------------------------------------------------------------- 1 | import { IChangeSet } from './IChangeSet'; 2 | import { MonoTypeOperatorFunction, Observable } from 'rxjs'; 3 | import { ISortedChangeSet } from './ISortedChangeSet'; 4 | import { IPagedChangeSet } from './IPagedChangeSet'; 5 | import { DistinctChangeSet } from './DistinctChangeSet'; 6 | 7 | /** 8 | * Represents the key of an object 9 | * @typeparam T the key type 10 | */ 11 | export interface IKey { 12 | /** 13 | * The key 14 | */ 15 | readonly key: T; 16 | } 17 | -------------------------------------------------------------------------------- /src/cache/IKeyValueCollection.ts: -------------------------------------------------------------------------------- 1 | import { SortReason } from './ISortedChangeSet'; 2 | import { SortOptimizations } from './SortOptimizations'; 3 | import { Comparer, KeyValueComparer } from './Comparer'; 4 | 5 | /** 6 | * A key collection which contains sorting information. 7 | * @typeparam TObject The type of the object 8 | * @typeparam TKey The type of the key 9 | */ 10 | export interface IKeyValueCollection extends Iterable<[TKey, TObject]> { 11 | /** 12 | * Gets the comparer used to peform the sort 13 | */ 14 | readonly comparer: KeyValueComparer; 15 | 16 | /** 17 | * The count of items. 18 | */ 19 | readonly size: number; 20 | 21 | /** 22 | * Gets the reason for a sort being applied. 23 | */ 24 | readonly sortReason: SortReason; 25 | 26 | /** 27 | * Gets the optimisations used to produce the sort 28 | */ 29 | readonly optimizations: SortOptimizations; 30 | 31 | /** 32 | * Iterator of values in the array. 33 | */ 34 | [Symbol.iterator](): IterableIterator<[TKey, TObject]>; 35 | 36 | /** 37 | * Returns an iterable of key, value pairs for every entry in the array 38 | */ 39 | entries(): IterableIterator<[number, [TKey, TObject]]>; 40 | 41 | /** 42 | * Returns an iterable of keys in the array 43 | */ 44 | keys(): IterableIterator; 45 | 46 | /** 47 | * Returns an iterable of values in the array 48 | */ 49 | values(): IterableIterator<[TKey, TObject]>; 50 | } 51 | -------------------------------------------------------------------------------- /src/cache/IObservableCache.ts: -------------------------------------------------------------------------------- 1 | import { IDisposable } from '../util/Disposable'; 2 | import { IConnectableCache } from './IConnectableCache'; 3 | import { IQuery } from './IQuery'; 4 | 5 | /** 6 | * A cache for observing and querying in memory data. With additional data access operatorsObservableCache 7 | */ 8 | export interface IObservableCache extends IConnectableCache, IQuery, IDisposable { 9 | /** 10 | * Gets the key associated with the object 11 | * @param item The item. 12 | */ 13 | getKey(item: TObject): TKey; 14 | 15 | /** 16 | * The symbol name of the cache 17 | */ 18 | readonly [Symbol.toStringTag]: 'ObservableCache'; 19 | } 20 | 21 | export function isObservableCache(value: any): value is IObservableCache { 22 | return value && value[Symbol.toStringTag] === 'ObservableCache'; 23 | } 24 | -------------------------------------------------------------------------------- /src/cache/IPagedChangeSet.ts: -------------------------------------------------------------------------------- 1 | import { PageResponse } from './PageResponse'; 2 | import { ISortedChangeSet } from './ISortedChangeSet'; 3 | export interface IPagedChangeSet extends ISortedChangeSet { 4 | /** 5 | * The parameters used to virtualize the stream 6 | */ 7 | readonly response: PageResponse; 8 | } 9 | -------------------------------------------------------------------------------- /src/cache/IQuery.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Exposes internal cache state to enable querying 3 | * @typeparam TObject The type of the object. 4 | * @typeparam TKey The type of the key. 5 | */ 6 | export interface IQuery { 7 | /** 8 | * Gets the keys 9 | */ 10 | keys(): IterableIterator; 11 | 12 | /** 13 | * Gets the Items 14 | */ 15 | values(): IterableIterator; 16 | 17 | /** 18 | * Gets the key value pairs 19 | */ 20 | entries(): IterableIterator<[TKey, TObject]>; 21 | 22 | /** 23 | * Lookup a single item using the specified key. 24 | * @summary Fast indexed lookup 25 | * @param key The key 26 | */ 27 | lookup(key: TKey): TObject | undefined; 28 | 29 | /** 30 | * The total count of cached items 31 | */ 32 | readonly size: number; 33 | 34 | [Symbol.iterator](): IterableIterator<[TKey, TObject]>; 35 | } 36 | -------------------------------------------------------------------------------- /src/cache/ISortedChangeSet.ts: -------------------------------------------------------------------------------- 1 | import { IChangeSet } from './IChangeSet'; 2 | import { IKeyValueCollection } from './IKeyValueCollection'; 3 | 4 | export interface ISortedChangeSet extends IChangeSet { 5 | /** 6 | * All cached items in sort order 7 | */ 8 | readonly sortedItems: IKeyValueCollection; 9 | } 10 | 11 | export type SortReason = 'initialLoad' | 'comparerChanged' | 'dataChanged' | 'reorder' | 'reset'; 12 | 13 | export function isSortedChangeSet(changeSet: IChangeSet): changeSet is ISortedChangeSet { 14 | return !!(changeSet as any).sortedItems; 15 | } 16 | -------------------------------------------------------------------------------- /src/cache/ISourceCache.ts: -------------------------------------------------------------------------------- 1 | import { IObservableCache } from './IObservableCache'; 2 | import { ISourceUpdater } from './ISourceUpdater'; 3 | 4 | /** 5 | * An observable cache which exposes an update API. Used at the root 6 | * of all observable chains 7 | * @typeparam TObject The type of the object 8 | * @typeparam TKey The type of the key 9 | */ 10 | export interface ISourceCache extends IObservableCache { 11 | /** 12 | * Action to apply a batch update to a cache. Multiple update methods can be invoked within a single batch operation. 13 | * These operations are invoked within the cache's lock and is therefore thread safe. 14 | * The result of the action will produce a single changeset 15 | * @param updateAction The update action 16 | */ 17 | edit(updateAction: (updater: ISourceUpdater) => void): void; 18 | } 19 | 20 | export function isSourceCache(value: any): value is ISourceCache { 21 | return value && value[Symbol.toStringTag] === 'ObservableCache' && value.edit !== undefined; 22 | } 23 | -------------------------------------------------------------------------------- /src/cache/ISourceUpdater.ts: -------------------------------------------------------------------------------- 1 | import { ArrayOrIterable } from '../util/ArrayOrIterable'; 2 | import { EqualityComparer } from '../util/isEqualityComparer'; 3 | import { ICacheUpdater } from './ICacheUpdater'; 4 | 5 | /** 6 | * Api for updating a source cache 7 | * 8 | * Use edit to produce singular changeset. 9 | * 10 | * NB:The evaluate method is used to signal to any observing operators 11 | * to reevaluate whether the the object still matches downstream operators. 12 | * This is primarily targeted to inline object changes such as datetime and calculated fields. 13 | * 14 | * @typeparam TObject The type of the object. 15 | * @typeparam TKey The type of the key. 16 | */ 17 | export interface ISourceUpdater extends ICacheUpdater { 18 | /** 19 | * Clears existing values and loads the specified items 20 | * @param entries The entries. 21 | */ 22 | load(entries: ArrayOrIterable): void; 23 | 24 | /** 25 | * Adds or changes the specified items. 26 | * @param entries The entries. 27 | */ 28 | addOrUpdateValues(entries: ArrayOrIterable): void; 29 | 30 | /** 31 | * Adds or update the item, 32 | * @param item The item. 33 | */ 34 | addOrUpdate(item: TObject): void; 35 | 36 | /** 37 | * Adds or update the item using a comparer 38 | * @param item The item. 39 | * @param comparer The comparer 40 | */ 41 | addOrUpdate(item: TObject, comparer: EqualityComparer): void; 42 | 43 | /** 44 | * Refreshes the specified items. 45 | * @param entries The entries. 46 | */ 47 | refreshValues(entries: ArrayOrIterable): void; 48 | 49 | /** 50 | * Refreshes the specified item 51 | * @param item The item. 52 | */ 53 | refresh(): void; 54 | 55 | /** 56 | * Refreshes the specified item 57 | * @param item The item. 58 | */ 59 | refresh(item: TObject): void; 60 | 61 | /** 62 | * Removes the specified items 63 | * @param entries The entries. 64 | */ 65 | removeValues(entries: ArrayOrIterable): void; 66 | 67 | /** 68 | * Removes the specified item. 69 | * @param item The item. 70 | */ 71 | remove(item: TObject): void; 72 | } 73 | -------------------------------------------------------------------------------- /src/cache/KeyValueCollection.ts: -------------------------------------------------------------------------------- 1 | import { IKeyValueCollection } from './IKeyValueCollection'; 2 | import { SortOptimizations } from './SortOptimizations'; 3 | import { count } from 'ix/iterable'; 4 | import { Comparer, KeyValueComparer, keyValueComparer } from './Comparer'; 5 | import { SortReason } from './ISortedChangeSet'; 6 | 7 | export class KeyValueCollection implements IKeyValueCollection { 8 | private readonly _items: ReadonlyArray<[TKey, TObject]>; 9 | 10 | public constructor(); 11 | public constructor(items: ReadonlyArray<[TKey, TObject]>, comparer: KeyValueComparer, sortReason: SortReason, optimizations: SortOptimizations); 12 | public constructor(items?: ReadonlyArray<[TKey, TObject]>, comparer?: KeyValueComparer, sortReason?: SortReason, optimizations?: SortOptimizations) { 13 | if (items === undefined) { 14 | this._items = []; 15 | this.size = 0; 16 | this.sortReason = 'reset'; 17 | this.optimizations = 'none'; 18 | this.comparer = keyValueComparer((a, b) => { 19 | if ((a)?.valueOf() > (b)?.valueOf()) { 20 | return 1; 21 | } 22 | if ((a)?.valueOf() < (a)?.valueOf()) { 23 | return -1; 24 | } 25 | return 0; 26 | }); 27 | } else { 28 | this._items = items; 29 | this.size = count(items); 30 | this.comparer = comparer!; 31 | this.sortReason = sortReason!; 32 | this.optimizations = optimizations!; 33 | } 34 | } 35 | 36 | /** 37 | * Gets the comparer used to peform the sort 38 | */ 39 | public readonly comparer: KeyValueComparer; 40 | 41 | public readonly size: number; 42 | 43 | public sortReason: SortReason; 44 | 45 | public readonly optimizations: SortOptimizations; 46 | 47 | [Symbol.iterator]() { 48 | return this._items[Symbol.iterator](); 49 | } 50 | 51 | entries() { 52 | return this._items.entries(); 53 | } 54 | 55 | keys() { 56 | return this._items.keys(); 57 | } 58 | 59 | values() { 60 | return this._items.values(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/cache/Node.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, IDisposable } from '../util'; 2 | import { SourceCache } from './SourceCache'; 3 | import { asObservableCache } from './operators/asObservableCache'; 4 | import { IObservableCache } from './IObservableCache'; 5 | import { ISourceUpdater } from './ISourceUpdater'; 6 | 7 | /** 8 | * Node describing the relationship between and item and it's ancestors and descendent 9 | * @typeparam TObject The type of the object 10 | * @typeparam TKey The type of the key 11 | */ 12 | export class Node implements IDisposable { 13 | private readonly _children = new SourceCache, TKey>(n => n.key); 14 | private readonly _cleanUp: IDisposable; 15 | private _parent?: Node; 16 | 17 | /** 18 | * Initializes a new instance of the class. 19 | * @param item The item 20 | * @param key The key 21 | * @param parent The parent 22 | */ 23 | public constructor(item: TObject, key: TKey, parent?: Node) { 24 | this.item = item; 25 | this.key = key; 26 | this._parent = parent; 27 | this.children = asObservableCache(this._children); 28 | this._cleanUp = new CompositeDisposable(this.children, this._children); 29 | } 30 | 31 | /** 32 | * The item 33 | */ 34 | public readonly item: TObject; 35 | 36 | /** 37 | * The key 38 | */ 39 | public readonly key: TKey; 40 | 41 | /** 42 | * Gets the parent if it has one 43 | */ 44 | public get parent() { 45 | return this._parent; 46 | } 47 | 48 | /** 49 | * The child nodes 50 | */ 51 | public readonly children: IObservableCache, TKey>; 52 | 53 | /** 54 | * Gets or sets a value indicating whether this instance is root. 55 | */ 56 | public get isRoot() { 57 | return this.parent === undefined; 58 | } 59 | 60 | /** 61 | * Gets the depth i.e. how many degrees of separation from the parent 62 | */ 63 | public get depth() { 64 | let i = 0; 65 | let parent = this.parent; 66 | do { 67 | if (parent === undefined) { 68 | break; 69 | } 70 | 71 | i++; 72 | parent = parent.parent; 73 | // eslint-disable-next-line no-constant-condition 74 | } while (true); 75 | return i; 76 | } 77 | 78 | /** 79 | * @internal 80 | */ 81 | public update(updateAction: (updater: ISourceUpdater, TKey>) => void) { 82 | this._children.edit(updateAction); 83 | } 84 | 85 | /** 86 | * @internal 87 | */ 88 | public setParent(parent?: Node | undefined) { 89 | this._parent = parent; 90 | } 91 | 92 | public toString() { 93 | const count = this.children.size === 0 ? '' : ` (${this.children.size} children)`; 94 | return `${this.item}${count}`; 95 | } 96 | 97 | public dispose() { 98 | this._cleanUp.dispose(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/cache/PageRequest.ts: -------------------------------------------------------------------------------- 1 | export class PageRequest { 2 | public constructor(public readonly page = 1, public readonly size = 25) {} 3 | public static readonly default = new PageRequest(); 4 | public static readonly empty = new PageRequest(0, 0); 5 | } 6 | -------------------------------------------------------------------------------- /src/cache/PageResponse.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Response from the pagination operator 3 | */ 4 | export type PageResponse = { 5 | /** 6 | * The size of the page. 7 | */ 8 | readonly pageSize: number; 9 | /** 10 | * The current page 11 | */ 12 | readonly page: number; 13 | /** 14 | * Total number of pages. 15 | */ 16 | readonly pages: number; 17 | /** 18 | * The total number of records in the underlying cache 19 | */ 20 | readonly totalSize: number; 21 | }; 22 | -------------------------------------------------------------------------------- /src/cache/SortOptimizations.ts: -------------------------------------------------------------------------------- 1 | // export type = 2 | export type SortOptimizations = 'none'; // TODO: add more? 3 | -------------------------------------------------------------------------------- /src/cache/SortedChangeSet.ts: -------------------------------------------------------------------------------- 1 | import { ChangeSet } from './ChangeSet'; 2 | import { IKeyValueCollection } from './IKeyValueCollection'; 3 | import { ArrayOrIterable } from '../util/ArrayOrIterable'; 4 | import { Change } from './Change'; 5 | import { KeyValueCollection } from './KeyValueCollection'; 6 | import { IChangeSet } from './IChangeSet'; 7 | import { ISortedChangeSet } from './ISortedChangeSet'; 8 | 9 | export class SortedChangeSet extends ChangeSet { 10 | public readonly sortedItems: IKeyValueCollection; 11 | 12 | constructor(sortedItems?: IKeyValueCollection, collection?: ArrayOrIterable>) { 13 | super(collection); 14 | this.sortedItems = sortedItems ?? new KeyValueCollection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/cache/operators.ts: -------------------------------------------------------------------------------- 1 | export * from './operators/and'; 2 | export * from './operators/asObservableCache'; 3 | export * from './operators/autoRefresh'; 4 | export * from './operators/autoRefreshOnObservable'; 5 | export * from './operators/bind'; 6 | export * from './operators/changeKey'; 7 | export * from './operators/clone'; 8 | export * from './operators/combineCache'; 9 | export * from './operators/deferUntilLoaded'; 10 | export * from './operators/disposeMany'; 11 | export * from './operators/distinctValues'; 12 | export * from './operators/except'; 13 | export * from './operators/expireAfter'; 14 | export * from './operators/filter'; 15 | export * from './operators/forEachChange'; 16 | export * from './operators/forExpiry'; 17 | export * from './operators/groupOn'; 18 | export * from './operators/groupOnDistinct'; 19 | export * from './operators/groupOnProperty'; 20 | export * from './operators/ignoreUpdateWhen'; 21 | export * from './operators/includeUpdateWhen'; 22 | export * from './operators/limitSizeTo'; 23 | export * from './operators/mergeMany'; 24 | export * from './operators/mergeManyItems'; 25 | export * from './operators/notEmpty'; 26 | export * from './operators/onItemAdded'; 27 | export * from './operators/onItemRemoved'; 28 | export * from './operators/onItemUpdated'; 29 | export * from './operators/or'; 30 | export * from './operators/other'; 31 | export * from './operators/queryWhenChanged'; 32 | export * from './operators/refCount'; 33 | export * from './operators/skipInitial'; 34 | export * from './operators/sort'; 35 | export * from './operators/startWithEmpty'; 36 | export * from './operators/statusMonitor'; 37 | export * from './operators/subscribeMany'; 38 | export * from './operators/suppressRefresh'; 39 | export * from './operators/toCollection'; 40 | export * from './operators/toObservableChangeSet'; 41 | export * from './operators/toSortedCollection'; 42 | export * from './operators/transform'; 43 | export * from './operators/transformForced'; 44 | export * from './operators/transformMany'; 45 | export * from './operators/transformPromise'; 46 | export * from './operators/transformToTree'; 47 | export * from './operators/treatMovesAsRemoveAdd'; 48 | export * from './operators/trueFor'; 49 | export * from './operators/trueForAll'; 50 | export * from './operators/trueForAny'; 51 | export * from './operators/watch'; 52 | export * from './operators/watchValue'; 53 | export * from './operators/whenAnyPropertyChanged'; 54 | export * from './operators/whenChanged'; 55 | export * from './operators/whenPropertyChanged'; 56 | export * from './operators/whenValueChanged'; 57 | export * from './operators/whereReasonsAre'; 58 | export * from './operators/whereReasonsAreNot'; 59 | export * from './operators/xor'; 60 | -------------------------------------------------------------------------------- /src/cache/operators/and.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { combineCache } from './combineCache'; 4 | import { ArrayOrIterable } from '../../util/ArrayOrIterable'; 5 | 6 | /** 7 | * Applied a logical And operator between the collections i.e items which are in all of the sources are included 8 | * @category Operator 9 | * @typeparam TObject The type of the object. 10 | * @typeparam TKey The type of the key. 11 | * @param items The items 12 | */ 13 | export function and(items: ArrayOrIterable>>) { 14 | return combineCache('and', items); 15 | } 16 | -------------------------------------------------------------------------------- /src/cache/operators/asObservableCache.ts: -------------------------------------------------------------------------------- 1 | import { ObservableCache } from '../ObservableCache'; 2 | import { IObservableCache } from '../IObservableCache'; 3 | import { isObservable, Observable } from 'rxjs'; 4 | import { IChangeSet } from '../IChangeSet'; 5 | 6 | /** 7 | * Converts the source to an read only observable cache 8 | * @category Operator 9 | * @typeparam TObject The type of the object 10 | * @typeparam TKey The type of the key 11 | * @param source The source 12 | */ 13 | export function asObservableCache(source: IObservableCache): IObservableCache; 14 | /** 15 | * Converts the source to an read only observable cache 16 | * @category Operator 17 | * @typeparam TObject The type of the object 18 | * @typeparam TKey The type of the key 19 | * @param source The source 20 | * @param deepEqual Use deep equality with the cache 21 | */ 22 | export function asObservableCache(source: Observable>, deepEqual?: boolean): IObservableCache; 23 | export function asObservableCache( 24 | source: IObservableCache | Observable>, 25 | deepEqual = false, 26 | ): IObservableCache { 27 | if (isObservable(source)) { 28 | return new ObservableCache(source, deepEqual); 29 | } else { 30 | return source; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/cache/operators/autoRefresh.ts: -------------------------------------------------------------------------------- 1 | import { NotifyPropertyChangedType } from '../../notify/notifyPropertyChangedSymbol'; 2 | import { MonoTypeOperatorFunction, OperatorFunction, SchedulerLike } from 'rxjs'; 3 | import { IChangeSet } from '../IChangeSet'; 4 | import { autoRefreshOnObservable } from './autoRefreshOnObservable'; 5 | import { whenAnyPropertyChanged } from './whenAnyPropertyChanged'; 6 | import { throttleTime } from 'rxjs/operators'; 7 | import { ChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 8 | 9 | /** 10 | * Automatically refresh downstream operators when any properties change. 11 | * @category Operator 12 | * @param changeSetBuffer Batch up changes by specifying the buffer. This greatly increases performance when many elements have sucessive property changes 13 | * @param propertyChangeThrottle When observing on multiple property changes, apply a throttle to prevent excessive refesh invocations 14 | * @param scheduler The scheduler 15 | */ 16 | export function autoRefresh( 17 | changeSetBuffer?: number, 18 | propertyChangeThrottle?: number, 19 | scheduler?: SchedulerLike, 20 | ): ChangeSetOperatorFunction>; 21 | /** 22 | * Automatically refresh downstream operators when any properties change. 23 | * @category Operator 24 | * @param key the property to watch 25 | * @param changeSetBuffer Batch up changes by specifying the buffer. This greatly increases performance when many elements have sucessive property changes 26 | * @param propertyChangeThrottle When observing on multiple property changes, apply a throttle to prevent excessive refesh invocations 27 | * @param scheduler The scheduler 28 | */ 29 | export function autoRefresh( 30 | key: keyof TObject, 31 | changeSetBuffer?: number, 32 | propertyChangeThrottle?: number, 33 | scheduler?: SchedulerLike, 34 | ): ChangeSetOperatorFunction>; 35 | export function autoRefresh( 36 | key?: number | keyof TObject, 37 | changeSetBuffer?: number, 38 | propertyChangeThrottle?: number | SchedulerLike, 39 | scheduler?: SchedulerLike, 40 | ): ChangeSetOperatorFunction> { 41 | const properties: string[] = []; 42 | if (typeof key === 'string' || typeof key === 'symbol') { 43 | properties.push(key as any); 44 | } else { 45 | scheduler = propertyChangeThrottle as any; 46 | propertyChangeThrottle = changeSetBuffer as any; 47 | changeSetBuffer = key as any; 48 | } 49 | 50 | return function autoRefreshOperator(source) { 51 | return source.pipe( 52 | autoRefreshOnObservable( 53 | (t, v) => { 54 | if (propertyChangeThrottle) { 55 | return whenAnyPropertyChanged(t, ...(properties as any[])).pipe(throttleTime(propertyChangeThrottle as number, scheduler)); 56 | } else { 57 | return whenAnyPropertyChanged(t, ...(properties as any[])); 58 | } 59 | }, 60 | changeSetBuffer as number | undefined, 61 | scheduler, 62 | ), 63 | ); 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /src/cache/operators/changeKey.ts: -------------------------------------------------------------------------------- 1 | import { OperatorFunction } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { map } from 'rxjs/operators'; 4 | import { from as ixFrom } from 'ix/Ix.dom.iterable'; 5 | import { map as ixMap } from 'ix/Ix.dom.iterable.operators'; 6 | import { Change } from '../Change'; 7 | import { ChangeSet } from '../ChangeSet'; 8 | import { ChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 9 | 10 | /** 11 | * Changes the primary key. 12 | * @category Operator 13 | * @typeparam TObject The type of the object. 14 | * @typeparam TSourceKey The type of the source key. 15 | * @typeparam TDestinationKey The type of the destination key. 16 | * @param selector The key selector eg. (item) => newKey; 17 | */ 18 | export function changeKey( 19 | selector: (value: TObject, sourceKey: TSourceKey) => TDestinationKey, 20 | ): ChangeSetOperatorFunction { 21 | return function changeKeyOperator(source) { 22 | return source.pipe( 23 | map(updates => { 24 | const changed = ixFrom(updates).pipe(ixMap(u => Change.create({ ...u, key: selector(u.current, u.key) }))); 25 | return new ChangeSet(changed); 26 | }), 27 | ); 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/cache/operators/deferUntilLoaded.ts: -------------------------------------------------------------------------------- 1 | import { IObservableCache } from '../IObservableCache'; 2 | import { concat, MonoTypeOperatorFunction, Observable } from 'rxjs'; 3 | import { filter, map, take } from 'rxjs/operators'; 4 | import { ChangeSet } from '../ChangeSet'; 5 | import { notEmpty } from './notEmpty'; 6 | import { IChangeSet } from '../IChangeSet'; 7 | import { statusMonitor } from './statusMonitor'; 8 | import { MonoTypeChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 9 | 10 | /** 11 | * Defer the subscription until the stream has been inflated with data 12 | * @category Operator 13 | * @typeparam TObject The type of the object. 14 | * @typeparam TKey The type of the key. 15 | * @param source The source cache 16 | */ 17 | export function deferUntilLoaded(source: IObservableCache): Observable>; 18 | /** 19 | * Defer the subscription until the stream has been inflated with data 20 | * @category Operator 21 | * @typeparam TObject The type of the object. 22 | * @typeparam TKey The type of the key. 23 | */ 24 | export function deferUntilLoaded(): MonoTypeChangeSetOperatorFunction; 25 | export function deferUntilLoaded(source?: IObservableCache) { 26 | if (source !== undefined) { 27 | return concat( 28 | source.countChanged.pipe( 29 | filter(count => count != 0), 30 | take(1), 31 | map(_ => new ChangeSet()), 32 | ), 33 | source.connect(), 34 | ).pipe(notEmpty()); 35 | } 36 | return function deferUntilLoadedOperator(source: Observable>) { 37 | return concat( 38 | source.pipe( 39 | statusMonitor(), 40 | filter(status => status == 'loaded'), 41 | take(1), 42 | map(_ => new ChangeSet()), 43 | ), 44 | source, 45 | ).pipe(notEmpty()); 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/cache/operators/disposeMany.ts: -------------------------------------------------------------------------------- 1 | import { MonoTypeOperatorFunction, Observable, OperatorFunction } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { Disposable, IDisposableOrSubscription, isDisposable, isSubscription } from '../../util'; 4 | import { Cache } from '../Cache'; 5 | import { tap } from 'rxjs/operators'; 6 | import { from as ixFrom } from 'ix/Ix.dom.iterable'; 7 | import { MonoTypeChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 8 | 9 | /** 10 | * Disposes each item when no longer required. 11 | * Individual items are disposed when removed or replaced. All items 12 | * are disposed when the stream is disposed 13 | * @category Operator 14 | * @typeparam TObject The type of the object. 15 | * @typeparam TKey The type of the key. 16 | * @param removeAction the action to take when an item is removed 17 | */ 18 | export function disposeMany(removeAction?: (value: TObject) => void): MonoTypeChangeSetOperatorFunction { 19 | if (!removeAction) { 20 | removeAction = function (value: any) { 21 | if (isDisposable(value)) value.dispose(); 22 | if (isSubscription(value)) value.unsubscribe(); 23 | }; 24 | } 25 | 26 | return function disposeManyOperator(source: Observable>) { 27 | return new Observable>(observer => { 28 | const cache = new Cache(); 29 | const subscriber = source 30 | .pipe( 31 | tap( 32 | changes => registerForRemoval(changes as any, cache), 33 | error => observer.error(error), 34 | ), 35 | ) 36 | .subscribe(observer); 37 | 38 | return () => { 39 | subscriber.unsubscribe(); 40 | 41 | ixFrom(cache.values()).forEach(t => removeAction!(t)); 42 | cache.clear(); 43 | }; 44 | }); 45 | }; 46 | 47 | function registerForRemoval(changes: IChangeSet, cache: Cache) { 48 | changes.forEach(change => { 49 | switch (change.reason) { 50 | case 'update': 51 | if (change.previous) removeAction!(change.previous); 52 | break; 53 | case 'remove': 54 | removeAction!(change.current); 55 | break; 56 | } 57 | }); 58 | cache.clone(changes); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/cache/operators/except.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { combineCache } from './combineCache'; 4 | import { ArrayOrIterable } from '../../util/ArrayOrIterable'; 5 | 6 | /** 7 | * Dynamically apply a logical Except operator between the collections 8 | * Items from the first collection in the outer list are included unless contained in any of the other lists 9 | * @category Operator 10 | * @typeparam TObject The type of the object. 11 | * @typeparam TKey The type of the key. 12 | * @param items The items 13 | */ 14 | export function except(items: ArrayOrIterable>>) { 15 | return combineCache('except', items); 16 | } 17 | -------------------------------------------------------------------------------- /src/cache/operators/forEachChange.ts: -------------------------------------------------------------------------------- 1 | import { Change } from '../Change'; 2 | import { MonoTypeOperatorFunction } from 'rxjs'; 3 | import { IChangeSet } from '../IChangeSet'; 4 | import { tap } from 'rxjs/operators'; 5 | import { MonoTypeChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 6 | 7 | /** 8 | * Provides a call back for each change 9 | * @category Operator 10 | * @typeparam TObject The type of the object. 11 | * @typeparam TKey The type of the key. 12 | * @param action The action. 13 | */ 14 | export function forEachChange(action: (change: Change) => void): MonoTypeChangeSetOperatorFunction { 15 | return function forEachChangeOperator(source) { 16 | return source.pipe(tap(changes => changes.forEach(element => action(element)))); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/cache/operators/groupOnProperty.ts: -------------------------------------------------------------------------------- 1 | import { Observable, OperatorFunction, queueScheduler, SchedulerLike } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { NotifyPropertyChangedType } from '../../notify/notifyPropertyChangedSymbol'; 4 | import { publish, throttleTime } from 'rxjs/operators'; 5 | import { whenValueChanged } from './whenValueChanged'; 6 | import { groupOn } from './groupOn'; 7 | import { Group } from '../IGroupChangeSet'; 8 | import { ChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 9 | 10 | /** 11 | * Groups the source using the property specified by the property selector. Groups are re-applied when the property value changed. 12 | * When there are likely to be a large number of group property changes specify a throttle to improve performance 13 | * @category Operator 14 | * @typeparam TObject The type of the object. 15 | * @typeparam TKey The type of the key. 16 | * @typeparam TGroupKey The type of the group key. 17 | * @param key The key to watch 18 | * @param propertyChangedThrottle 19 | * @param scheduler The scheduler. 20 | */ 21 | export function groupOnProperty( 22 | key: TProperty, 23 | propertyChangedThrottle?: number, 24 | scheduler: SchedulerLike = queueScheduler, 25 | ): ChangeSetOperatorFunction, TKey, TObject[TProperty]>, TObject[TProperty]> { 26 | return function groupOnKeyOperator(source) { 27 | return source.pipe( 28 | publish(shared => { 29 | // Monitor explicit property changes 30 | let regrouper = shared.pipe(whenValueChanged(key, false)); 31 | 32 | //add a throttle if specified 33 | if (propertyChangedThrottle) { 34 | regrouper = regrouper.pipe(throttleTime(propertyChangedThrottle, scheduler ?? queueScheduler)); 35 | } 36 | 37 | // Use property changes as a trigger to re-evaluate Grouping 38 | return (shared.pipe(groupOn(x => x[key], regrouper)) as any) as Observable< 39 | IChangeSet, TKey, TObject[TProperty]>, TObject[TProperty]> 40 | >; 41 | }), 42 | ); 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /src/cache/operators/ignoreUpdateWhen.ts: -------------------------------------------------------------------------------- 1 | import { MonoTypeOperatorFunction } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { map } from 'rxjs/operators'; 4 | import { from as ixFrom } from 'ix/Ix.dom.iterable'; 5 | import { filter as ixFilter } from 'ix/iterable/operators'; 6 | import { ChangeSet } from '../ChangeSet'; 7 | import { notEmpty } from './notEmpty'; 8 | import { MonoTypeChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 9 | 10 | /** 11 | * Ignores the update when the condition is met. 12 | * The first parameter in the ignore function is the current value and the second parameter is the previous value 13 | * @category Operator 14 | * @typeparam TObject The type of the object. 15 | * @typeparam TKey The type of the key. 16 | * @param ignoreFunction The ignore function (current,previous)=>{ return true to ignore }. 17 | */ 18 | export function ignoreUpdateWhen(ignoreFunction: (current: TObject, previous: TObject) => boolean): MonoTypeChangeSetOperatorFunction { 19 | return function ignoreUpdateWhenOperator(source) { 20 | return source.pipe( 21 | map(updates => { 22 | const result = ixFrom(updates).pipe( 23 | ixFilter(u => { 24 | if (u.reason !== 'update') return true; 25 | return !ignoreFunction(u.current, u.previous!); 26 | }), 27 | ); 28 | return new ChangeSet(result); 29 | }), 30 | notEmpty(), 31 | ); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/cache/operators/includeUpdateWhen.ts: -------------------------------------------------------------------------------- 1 | import { MonoTypeOperatorFunction } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { map } from 'rxjs/operators'; 4 | import { from as ixFrom } from 'ix/Ix.dom.iterable'; 5 | import { filter as ixFilter } from 'ix/iterable/operators'; 6 | import { ChangeSet } from '../ChangeSet'; 7 | import { notEmpty } from './notEmpty'; 8 | import { MonoTypeChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 9 | 10 | /** 11 | * Only includes the update when the condition is met. 12 | * The first parameter in the ignore function is the current value and the second parameter is the previous value 13 | * @category Operator 14 | * @typeparam TObject The type of the object. 15 | * @typeparam TKey The type of the key. 16 | * @param includeFunction The include function (current,previous)=>{ return true to include }. 17 | */ 18 | export function includeUpdateWhen(includeFunction: (current: TObject, previous: TObject) => boolean): MonoTypeChangeSetOperatorFunction { 19 | return function includeUpdateWhenOperator(source) { 20 | return source.pipe( 21 | map(updates => { 22 | const result = ixFrom(updates).pipe( 23 | ixFilter(u => { 24 | return u.reason !== 'update' || includeFunction(u.current, u.previous!); 25 | }), 26 | ); 27 | return new ChangeSet(result); 28 | }), 29 | notEmpty(), 30 | ); 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/cache/operators/mergeMany.ts: -------------------------------------------------------------------------------- 1 | import { Observable, OperatorFunction } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { subscribeMany } from './subscribeMany'; 4 | 5 | /** 6 | * Dynamically merges the observable which is selected from each item in the stream, and unmerges the item 7 | * when it is no longer part of the stream. 8 | * @category Operator 9 | * @typeparam TObject The type of the object. 10 | * @typeparam TKey The type of the key. 11 | * @typeparam TDestination The type of the destination. 12 | * @param observableSelector The observable selector. 13 | */ 14 | export function mergeMany( 15 | observableSelector: (value: TObject, key: TKey) => Observable, 16 | ): OperatorFunction, TDestination> { 17 | return function mergeManyOperator(source) { 18 | return new Observable(observer => { 19 | return source.pipe(subscribeMany((t, v) => observableSelector(t, v).subscribe(x => observer.next(x)))).subscribe( 20 | x => {}, 21 | ex => observer.error(ex), 22 | // TODO: Is this needed 23 | () => observer.complete(), 24 | ); 25 | }); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/cache/operators/mergeManyItems.ts: -------------------------------------------------------------------------------- 1 | import { Observable, OperatorFunction } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { subscribeMany } from './subscribeMany'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | export type ItemWithValue = { item: TObject; value: TValue }; 7 | 8 | /** 9 | * Dynamically merges the observable which is selected from each item in the stream, and unmerges the item 10 | * when it is no longer part of the stream. 11 | * @category Operator 12 | * @typeparam TObject The type of the object. 13 | * @typeparam TKey The type of the key. 14 | * @typeparam TDestination The type of the destination. 15 | * @param observableSelector The observable selector. 16 | */ 17 | export function mergeManyItems( 18 | observableSelector: (value: TObject, key: TKey) => Observable, 19 | ): OperatorFunction, ItemWithValue> { 20 | return function mergeManyItemsOperator(source) { 21 | return new Observable>(observer => { 22 | return source 23 | .pipe( 24 | subscribeMany((t, v) => 25 | observableSelector(t, v) 26 | .pipe(map(z => ({ item: t, value: z }))) 27 | .subscribe(observer), 28 | ), 29 | ) 30 | .subscribe(); 31 | }); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/cache/operators/notEmpty.ts: -------------------------------------------------------------------------------- 1 | import { MonoTypeOperatorFunction, Observable } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { filter } from 'rxjs/operators'; 4 | import { MonoTypeChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 5 | 6 | /** 7 | * Suppresses updates which are empty 8 | * @category Operator 9 | * @typeparam TObject The type of the object. 10 | * @typeparam TKey The type of the key. 11 | */ 12 | export function notEmpty(): MonoTypeChangeSetOperatorFunction { 13 | return function notEmptyOperator(source: Observable>) { 14 | return source.pipe(filter(changes => changes.size !== 0)); 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/cache/operators/onItemAdded.ts: -------------------------------------------------------------------------------- 1 | import { MonoTypeOperatorFunction } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { tap } from 'rxjs/operators'; 4 | import { from as ixFrom } from 'ix/Ix.dom.iterable'; 5 | import { filter as ixFilter } from 'ix/iterable/operators'; 6 | import { MonoTypeChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 7 | 8 | /** 9 | * Callback for each item as and when it is being added to the stream 10 | * @category Operator 11 | * @typeparam TObject The type of the object. 12 | * @typeparam TKey The type of the key. 13 | * @param addAction The add action. 14 | */ 15 | export function onItemAdded(action: (value: TObject) => void): MonoTypeChangeSetOperatorFunction { 16 | return function onItemAddedOperator(source) { 17 | return source.pipe( 18 | tap(changes => 19 | ixFrom(changes) 20 | .pipe(ixFilter(x => x.reason === 'add')) 21 | .forEach(change => action(change.current)), 22 | ), 23 | ); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/cache/operators/onItemRemoved.ts: -------------------------------------------------------------------------------- 1 | import { MonoTypeOperatorFunction } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { tap } from 'rxjs/operators'; 4 | import { from as ixFrom } from 'ix/Ix.dom.iterable'; 5 | import { filter as ixFilter } from 'ix/iterable/operators'; 6 | import { MonoTypeChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 7 | 8 | /** 9 | * Callback for each item as and when it is being removed from the stream 10 | * @category Operator 11 | * @typeparam TObject The type of the object. 12 | * @typeparam TKey The type of the key. 13 | * @param action The remove action. 14 | */ 15 | export function onItemRemoved(action: (value: TObject) => void): MonoTypeChangeSetOperatorFunction { 16 | return function onItemRemovedOperator(source) { 17 | return source.pipe( 18 | tap(changes => 19 | ixFrom(changes) 20 | .pipe(ixFilter(x => x.reason === 'remove')) 21 | .forEach(change => action(change.current)), 22 | ), 23 | ); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/cache/operators/onItemUpdated.ts: -------------------------------------------------------------------------------- 1 | import { MonoTypeOperatorFunction } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { tap } from 'rxjs/operators'; 4 | import { from as ixFrom } from 'ix/Ix.dom.iterable'; 5 | import { filter as ixFilter } from 'ix/iterable/operators'; 6 | import { MonoTypeChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 7 | 8 | /** 9 | * Callback when an item has been updated eg. (current, previous)=>{} 10 | * @category Operator 11 | * @typeparam TObject The type of the object. 12 | * @typeparam TKey The type of the key. 13 | * @param action The update action. 14 | */ 15 | export function onItemUpdated(action: (current: TObject, previous: TObject) => void): MonoTypeChangeSetOperatorFunction { 16 | return function onItemUpdatedOperator(source) { 17 | return source.pipe( 18 | tap(changes => 19 | ixFrom(changes) 20 | .pipe(ixFilter(x => x.reason === 'update')) 21 | .forEach(change => action(change.current, change.previous!)), 22 | ), 23 | ); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/cache/operators/or.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { combineCache } from './combineCache'; 4 | import { ArrayOrIterable } from '../../util/ArrayOrIterable'; 5 | 6 | /** 7 | * Apply a logical Or operator between the collections i.e items which are in any of the sources are included 8 | * @category Operator 9 | * @typeparam TObject The type of the object. 10 | * @typeparam TKey The type of the key. 11 | * @param items The items 12 | */ 13 | export function or(items: ArrayOrIterable>>) { 14 | return combineCache('or', items); 15 | } 16 | -------------------------------------------------------------------------------- /src/cache/operators/queryWhenChanged.ts: -------------------------------------------------------------------------------- 1 | import { merge, Observable, OperatorFunction } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { IQuery } from '../IQuery'; 4 | import { map, publish, scan } from 'rxjs/operators'; 5 | import { Cache } from '../Cache'; 6 | import { AnonymousQuery } from '../AnonymousQuery'; 7 | import { mergeMany } from './mergeMany'; 8 | 9 | /** 10 | * The latest copy of the cache is exposed for querying i) after each modification to the underlying data ii) on subscription 11 | * @category Operator 12 | * @typeparam TObject The type of the object. 13 | * @typeparam TKey The type of the key. 14 | * @typeparam TValue The type of the value. 15 | * @param itemChangedTrigger Should the query be triggered for observables on individual items 16 | */ 17 | export function queryWhenChanged(itemChangedTrigger?: (value: TObject) => Observable): OperatorFunction, IQuery> { 18 | return function queryWhenChangedBaseOperator(source) { 19 | if (itemChangedTrigger == undefined) { 20 | return source.pipe( 21 | scan((cache, changes) => { 22 | cache.clone(changes); 23 | return cache; 24 | }, new Cache()), 25 | map(list => new AnonymousQuery(list)), 26 | ); 27 | } 28 | 29 | return source.pipe( 30 | publish(shared => { 31 | const state = new Cache(); 32 | 33 | const inlineChange = shared.pipe( 34 | mergeMany(itemChangedTrigger), 35 | map(_ => new AnonymousQuery(state)), 36 | ); 37 | 38 | const sourceChanged = shared.pipe( 39 | scan((list, changes) => { 40 | list.clone(changes); 41 | return list; 42 | }, state), 43 | map(list => new AnonymousQuery(list)), 44 | ); 45 | 46 | return merge(sourceChanged, inlineChange); 47 | }), 48 | ); 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /src/cache/operators/refCount.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/prevent-abbreviations */ 2 | import { Observable } from 'rxjs'; 3 | import { IChangeSet } from '../IChangeSet'; 4 | import { IObservableCache } from '../IObservableCache'; 5 | import { asObservableCache } from './asObservableCache'; 6 | import { IDisposable } from '../../util'; 7 | import { MonoTypeChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 8 | 9 | /** 10 | * Cache equivalent to Publish().RefCount(). The source is cached so long as there is at least 1 subscriber. 11 | * @category Operator 12 | * @typeparam TObject The type of the object. 13 | * @typeparam TKey The type of the destination key. 14 | */ 15 | export function refCount(): MonoTypeChangeSetOperatorFunction { 16 | return function referenceCountOperator(source: Observable>) { 17 | let _referenceCount = 0; 18 | let _cache: IObservableCache; 19 | return new Observable>(observer => { 20 | if (++_referenceCount === 1) { 21 | _cache = asObservableCache(source); 22 | } 23 | 24 | const subscriber = _cache.connect().subscribe(observer); 25 | 26 | return () => { 27 | subscriber.unsubscribe(); 28 | let cacheToDispose: IDisposable | undefined; 29 | if (--_referenceCount == 0) { 30 | cacheToDispose = _cache; 31 | _cache = undefined!; 32 | } 33 | 34 | cacheToDispose?.dispose(); 35 | }; 36 | }); 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/cache/operators/skipInitial.ts: -------------------------------------------------------------------------------- 1 | import { MonoTypeOperatorFunction } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { deferUntilLoaded } from './deferUntilLoaded'; 4 | import { skip } from 'rxjs/operators'; 5 | import { MonoTypeChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 6 | 7 | /** 8 | * Defer the subscription until loaded and skip initial changeset 9 | * @category Operator 10 | * @typeparam TObject The type of the object. 11 | * @typeparam TKey The type of the key. 12 | */ 13 | export function skipInitial(): MonoTypeChangeSetOperatorFunction { 14 | return function skipInitialOperator(source) { 15 | return source.pipe(deferUntilLoaded(), skip(1)); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/cache/operators/startWithEmpty.ts: -------------------------------------------------------------------------------- 1 | import { IChangeSet } from '../IChangeSet'; 2 | import { MonoTypeOperatorFunction } from 'rxjs'; 3 | import { startWith } from 'rxjs/operators'; 4 | import { MonoTypeChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 5 | 6 | /** 7 | * Prepends an empty changeset to the source 8 | * @category Operator 9 | * @typeparam TObject The type of the object. 10 | * @typeparam TKey The type of the key. 11 | * @param changesSetClass the class that defines the changeset 12 | */ 13 | export function startWithEmpty(changesSetClass: { empty(): IChangeSet }): MonoTypeChangeSetOperatorFunction { 14 | return function startWithEmptyOperator(source) { 15 | return source.pipe(startWith(changesSetClass.empty())); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/cache/operators/statusMonitor.ts: -------------------------------------------------------------------------------- 1 | import { Observable, OperatorFunction, Subject } from 'rxjs'; 2 | import { distinctUntilChanged, startWith } from 'rxjs/operators'; 3 | 4 | export type ConnectionStatus = 'pending' | 'loaded' | 'errored' | 'completed'; 5 | 6 | /** 7 | * Monitors the status of a stream 8 | * @category Operator 9 | * @typeparam T 10 | */ 11 | export function statusMonitor(): OperatorFunction { 12 | return function statusMonitorOperator(source) { 13 | return new Observable(observer => { 14 | const statusSubject = new Subject(); 15 | let status: ConnectionStatus = 'pending'; 16 | 17 | function error(ex: Error) { 18 | status = 'errored'; 19 | statusSubject.next(status); 20 | observer.error(ex); 21 | } 22 | 23 | function completion() { 24 | if (status === 'errored') { 25 | return; 26 | } 27 | 28 | status = 'completed'; 29 | statusSubject.next(status); 30 | } 31 | 32 | function updated() { 33 | if (status !== 'pending') { 34 | return; 35 | } 36 | 37 | status = 'loaded'; 38 | statusSubject.next(status); 39 | } 40 | 41 | const monitor = source.subscribe(updated, error, completion); 42 | 43 | const subscriber = statusSubject.pipe(startWith(status), distinctUntilChanged()).subscribe(observer); 44 | 45 | return () => { 46 | statusSubject.complete(); 47 | monitor.unsubscribe(); 48 | subscriber.unsubscribe(); 49 | }; 50 | }); 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /src/cache/operators/subscribeMany.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, IDisposableOrSubscription } from '../../util'; 2 | import { ConnectableObservable, MonoTypeOperatorFunction, Observable } from 'rxjs'; 3 | import { IChangeSet } from '../IChangeSet'; 4 | import { publish } from 'rxjs/operators'; 5 | import { transform } from './transform'; 6 | import { disposeMany } from './disposeMany'; 7 | import { IPagedChangeSet } from '../IPagedChangeSet'; 8 | import { ISortedChangeSet } from '../ISortedChangeSet'; 9 | import { DistinctChangeSet } from '../DistinctChangeSet'; 10 | import { MonoTypeChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 11 | 12 | /** 13 | * Subscribes to each item when it is added to the stream and unsubscribes when it is removed. All items will be unsubscribed when the stream is disposed 14 | * @category Operator 15 | * @typeparam TObject The type of the object. 16 | * @typeparam TKey The type of the key. 17 | * @param subscriptionFactory The subscription function 18 | */ 19 | export function subscribeMany(subscriptionFactory: (value: TObject, key: TKey) => IDisposableOrSubscription): MonoTypeChangeSetOperatorFunction { 20 | return function subscribeManyOperator(source: Observable>) { 21 | return new Observable>(observer => { 22 | const published = publish>()(source); 23 | const subscriptions = published.pipe(transform(subscriptionFactory), disposeMany()).subscribe(); 24 | 25 | return new CompositeDisposable(subscriptions, published.subscribe(observer), published.connect()); 26 | }); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/cache/operators/suppressRefresh.ts: -------------------------------------------------------------------------------- 1 | import { MonoTypeOperatorFunction } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { whereReasonsAreNot } from './whereReasonsAreNot'; 4 | import { MonoTypeChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 5 | 6 | /** 7 | * Suppress refresh notifications 8 | * @category Operator 9 | * @typeparam TObject The type of the object. 10 | * @typeparam TKey The type of the key. 11 | */ 12 | export function suppressRefresh(): MonoTypeChangeSetOperatorFunction { 13 | return function suppressRefreshOperator(source) { 14 | return source.pipe(whereReasonsAreNot('refresh')); 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/cache/operators/toCollection.ts: -------------------------------------------------------------------------------- 1 | import { OperatorFunction } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { queryWhenChanged } from './queryWhenChanged'; 4 | import { map } from 'rxjs/operators'; 5 | import { toArray as ixToArray } from 'ix/iterable'; 6 | 7 | /** 8 | * Converts the changeset into a fully formed collection. Each change in the source results in a new collection 9 | * @category Operator 10 | * @typeparam TObject The type of the object. 11 | * @typeparam TKey The type of the key. 12 | */ 13 | export function toCollection(): OperatorFunction, TObject[]> { 14 | return function toCollectionOperator(source) { 15 | return source.pipe( 16 | queryWhenChanged(), 17 | map(query => ixToArray(query.values())), 18 | ); 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/cache/operators/toSortedCollection.ts: -------------------------------------------------------------------------------- 1 | import { OperatorFunction } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { queryWhenChanged } from './queryWhenChanged'; 4 | import { map } from 'rxjs/operators'; 5 | import { from as ixFrom, toArray as ixToArray } from 'ix/Ix.dom.iterable'; 6 | import { orderBy, orderByDescending } from 'ix/iterable/operators'; 7 | 8 | /** 9 | * Converts the changeset into a fully formed sorted collection. Each change in the source results in a new sorted collection 10 | * @category Operator 11 | * @typeparam TObject The type of the object. 12 | * @typeparam TKey The type of the key. 13 | * @typeparam TSortKey The sort key 14 | * @param sortSelector The sort function 15 | * @param sortOrder The sort order. Defaults to ascending 16 | */ 17 | export function toSortedCollection( 18 | sortSelector: (value: TObject) => TSortKey, 19 | sortOrder?: 'asc' | 'desc', 20 | ): OperatorFunction, TObject[]>; 21 | /** 22 | * Converts the changeset into a fully formed sorted collection. Each change in the source results in a new sorted collection 23 | * @typeparam TObject The type of the object. 24 | * @typeparam TKey The type of the key. 25 | * @typeparam TSortKey The sort key 26 | * @param sortSelector The sort function 27 | * @param comparer The sort comparer 28 | * @param sortOrder The sort order. Defaults to ascending 29 | */ 30 | export function toSortedCollection( 31 | sortSelector: (value: TObject) => TSortKey, 32 | comparer: (a: TSortKey, b: TSortKey) => number, 33 | sortOrder?: 'asc' | 'desc', 34 | ): OperatorFunction, TObject[]>; 35 | export function toSortedCollection( 36 | sortSelector: (value: TObject) => TSortKey, 37 | comparerOrSortOrder?: ((a: TSortKey, b: TSortKey) => number) | 'asc' | 'desc', 38 | sortOrder: 'asc' | 'desc' = 'asc', 39 | ): OperatorFunction, TObject[]> { 40 | let comparer: ((a: TSortKey, b: TSortKey) => number) | undefined; 41 | if (typeof comparerOrSortOrder === 'string') { 42 | sortOrder = comparerOrSortOrder; 43 | } else { 44 | comparer = comparerOrSortOrder; 45 | } 46 | return function toSortedCollectionOperator(source) { 47 | return source.pipe( 48 | queryWhenChanged(), 49 | map(query => ixToArray(ixFrom(query.values()).pipe(sortOrder === 'asc' ? orderBy(sortSelector, comparer) : orderByDescending(sortSelector, comparer)))), 50 | ); 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /src/cache/operators/treatMovesAsRemoveAdd.ts: -------------------------------------------------------------------------------- 1 | import { MonoTypeOperatorFunction } from 'rxjs'; 2 | import { ISortedChangeSet } from '../ISortedChangeSet'; 3 | import { IChangeSet } from '../IChangeSet'; 4 | import { Change } from '../Change'; 5 | import { map } from 'rxjs/operators'; 6 | import { SortedChangeSet } from '../SortedChangeSet'; 7 | 8 | /** 9 | * Converts moves changes to remove + add 10 | * @category Operator 11 | * @typeparam TObject The type of the object. 12 | * @typeparam TKey The type of the key. 13 | */ 14 | export function treatMovesAsRemoveAdd(): MonoTypeOperatorFunction> { 15 | function* replaceMoves(items: IChangeSet): Iterable> { 16 | for (const change of items) { 17 | if (change.reason === 'moved') { 18 | yield Change.remove(change.key, change.current, change.previousIndex); 19 | yield Change.add(change.key, change.current, change.currentIndex); 20 | } else { 21 | yield change; 22 | } 23 | } 24 | } 25 | 26 | return function treatMovesAsRemoveAddOperator(source) { 27 | return source.pipe(map(changes => new SortedChangeSet(changes.sortedItems, replaceMoves(changes)))); 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/cache/operators/trueFor.ts: -------------------------------------------------------------------------------- 1 | import { combineLatest, ConnectableObservable, Observable, OperatorFunction } from 'rxjs'; 2 | import { ArrayOrIterable } from '../../util/ArrayOrIterable'; 3 | import { IChangeSet } from '../IChangeSet'; 4 | import { transform } from './transform'; 5 | import { distinctUntilChanged, map, publish, tap } from 'rxjs/operators'; 6 | import { mergeMany } from './mergeMany'; 7 | import { CompositeDisposable } from '../../util'; 8 | import { toCollection } from './toCollection'; 9 | 10 | /** 11 | * @ignore 12 | * Used for trueForAll and trueForAny 13 | */ 14 | export function trueFor( 15 | observableSelector: (value: TObject) => Observable, 16 | collectionMatcher: (data: ArrayOrIterable>) => boolean, 17 | ): OperatorFunction, boolean> { 18 | return function trueForOperator(source) { 19 | return new Observable(observer => { 20 | const transformed = publish, TKey>>()( 21 | source.pipe(transform(t => new ObservableWithValue(t, observableSelector(t)))), 22 | ); 23 | const inlineChanges = transformed.pipe(mergeMany(t => t.observable)); 24 | const queried = transformed.pipe(toCollection()); 25 | 26 | //nb: we do not care about the inline change because we are only monitoring it to cause a re-evaluation of all items 27 | const publisher = combineLatest([queried, inlineChanges]) 28 | .pipe( 29 | map(([items, _]) => collectionMatcher(items)), 30 | distinctUntilChanged(), 31 | ) 32 | .subscribe(observer); 33 | 34 | return new CompositeDisposable(publisher, transformed.connect()); 35 | }); 36 | }; 37 | } 38 | 39 | class ObservableWithValue { 40 | private _latestValue?: TValue; 41 | 42 | public constructor(item: TObject, source: Observable) { 43 | this.item = item; 44 | this.observable = source.pipe(tap(value => (this._latestValue = value))); 45 | } 46 | 47 | public readonly item: TObject; 48 | 49 | public get latestValue() { 50 | return this._latestValue; 51 | } 52 | 53 | public readonly observable: Observable; 54 | } 55 | -------------------------------------------------------------------------------- /src/cache/operators/trueForAll.ts: -------------------------------------------------------------------------------- 1 | import { Observable, OperatorFunction } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { trueFor } from './trueFor'; 4 | import { every } from 'ix/iterable'; 5 | 6 | /** 7 | * Produces a boolean observable indicating whether the latest resulting value from all of the specified observables matches 8 | * the equality condition. The observable is re-evaluated whenever 9 | * i) The cache changes 10 | * or ii) The inner observable changes 11 | * @category Operator 12 | * @typeparam TObject The type of the object. 13 | * @typeparam TKey The type of the key. 14 | * @typeparam TValue The type of the value. 15 | * @param observableSelector Selector which returns the target observable 16 | * @param equalityCondition The equality condition. 17 | */ 18 | export function trueForAll( 19 | observableSelector: (value: TObject) => Observable, 20 | equalityCondition: (value: TObject, item: TValue) => boolean, 21 | ): OperatorFunction, boolean> { 22 | return function trueForAllOperator(source) { 23 | return source.pipe( 24 | trueFor(observableSelector, items => { 25 | items; //? 26 | return every(items, { 27 | predicate: o => { 28 | o; //? 29 | return (o.latestValue !== undefined && equalityCondition(o.item, o.latestValue)) ?? false; 30 | }, 31 | }); 32 | }), 33 | ); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/cache/operators/trueForAny.ts: -------------------------------------------------------------------------------- 1 | import { Observable, OperatorFunction } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { trueFor } from './trueFor'; 4 | import { some } from 'ix/iterable'; 5 | 6 | /** 7 | * Produces a boolean observable indicating whether the resulting value of whether any of the specified observables matches 8 | * the equality condition. The observable is re-evaluated whenever 9 | * i) The cache changes. 10 | * or ii) The inner observable changes. 11 | * @category Operator 12 | * @typeparam TObject The type of the object. 13 | * @typeparam TKey The type of the key. 14 | * @typeparam TValue The type of the value. 15 | * @param observableSelector The observable selector. 16 | * @param equalityCondition The equality condition. 17 | */ 18 | export function trueForAny( 19 | observableSelector: (value: TObject) => Observable, 20 | equalityCondition: (value: TObject, item: TValue) => boolean, 21 | ): OperatorFunction, boolean> { 22 | return function trueForAllOperator(source) { 23 | return source.pipe( 24 | trueFor(observableSelector, items => { 25 | return some(items, { 26 | predicate: o => { 27 | return (o.latestValue !== undefined && equalityCondition(o.item, o.latestValue)) ?? false; 28 | }, 29 | }); 30 | }), 31 | ); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/cache/operators/watch.ts: -------------------------------------------------------------------------------- 1 | import { OperatorFunction } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { Change } from '../Change'; 4 | import { filter, mergeMap } from 'rxjs/operators'; 5 | 6 | /** 7 | * Returns an observable of any updates which match the specified key, preceded with the initial cache state 8 | * @category Operator 9 | * @typeparam TObject The type of the object. 10 | * @typeparam TKey The type of the key. 11 | * @param key The key. 12 | */ 13 | export function watch(key: TKey): OperatorFunction, Change> { 14 | return function watchOperator(source) { 15 | return source.pipe( 16 | mergeMap(updates => updates), 17 | filter(update => update.key === key), 18 | ); 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/cache/operators/watchValue.ts: -------------------------------------------------------------------------------- 1 | import { IObservableCache, isObservableCache } from '../IObservableCache'; 2 | import { map } from 'rxjs/operators'; 3 | import { Observable, OperatorFunction } from 'rxjs'; 4 | import { IChangeSet } from '../IChangeSet'; 5 | import { watch } from './watch'; 6 | 7 | /** 8 | * Watches updates for a single value matching the specified key 9 | * @category Operator 10 | * @typeparam TObject The type of the object. 11 | * @typeparam TKey The type of the key. 12 | * @param cache The cache. 13 | * @param key The key. 14 | */ 15 | export function watchValue(cache: IObservableCache, key: TKey): Observable; 16 | /** 17 | * Watches updates for a single value matching the specified key 18 | * @category Operator 19 | * @typeparam TObject The type of the object. 20 | * @typeparam TKey The type of the key. 21 | * @param key The key. 22 | */ 23 | export function watchValue(key: TKey): OperatorFunction, TObject>; 24 | export function watchValue(cache: IObservableCache | TKey, key?: TKey) { 25 | if (isObservableCache(cache)) { 26 | return cache.watch(key!).pipe(map(z => z.current)); 27 | } else { 28 | key = cache; 29 | return function watchValueOperator(source: Observable>) { 30 | return source.pipe( 31 | watch(key), 32 | map(z => z.current), 33 | ); 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/cache/operators/whenAnyPropertyChanged.ts: -------------------------------------------------------------------------------- 1 | import { isNotifyPropertyChanged, notificationsFor, NotifyPropertyChangedType } from '../../notify/notifyPropertyChangedSymbol'; 2 | import { filter, map } from 'rxjs/operators'; 3 | import { MonoTypeOperatorFunction, Observable } from 'rxjs'; 4 | import { IChangeSet } from '../IChangeSet'; 5 | import { mergeMany } from './mergeMany'; 6 | 7 | /** 8 | * Notifies when any any property on the object has changed 9 | * @category Operator 10 | * @typeparam TObject The type of the object 11 | * @param value The object to observe 12 | * @param propertiesToMonitor specify properties to Monitor, or omit to monitor all property changes 13 | */ 14 | export function whenAnyPropertyChanged(value: TObject, ...propertiesToMonitor: (keyof TObject)[]): Observable>; 15 | /** 16 | * Watches each item in the collection and notifies when any of them has changed 17 | * @category Operator 18 | * @typeparam TObject The type of the object. 19 | * @typeparam TKey The type of the key. 20 | * @param propertiesToMonitor specify properties to Monitor, or omit to monitor all property changes 21 | */ 22 | export function whenAnyPropertyChanged( 23 | ...propertiesToMonitor: (keyof TObject)[] 24 | ): MonoTypeOperatorFunction, TProperty>>; 25 | export function whenAnyPropertyChanged(...value: (TObject | keyof TObject)[]) { 26 | if (value.length > 0 && typeof value[0] !== 'string' && typeof value[0] !== 'symbol') { 27 | if (!isNotifyPropertyChanged(value[0])) { 28 | throw new Error( 29 | 'Object must implement the notifyPropertyChangedSymbol or inherit from the NotifyPropertyChangedBase class or be wrapped by the proxy method observePropertyChanges', 30 | ); 31 | } 32 | const propertiesToMonitor = value.slice(1) as (keyof TObject)[]; 33 | return (propertiesToMonitor.length > 0 34 | ? notificationsFor(value[0] as any).pipe(filter(property => propertiesToMonitor.includes(property as any))) 35 | : notificationsFor(value[0] as any) 36 | ).pipe(map(z => value)); 37 | } 38 | return function whenAnyPropertyChangedOperator(source: Observable>) { 39 | const propertiesToMonitor = value as (keyof TObject)[]; 40 | return source.pipe(mergeMany(value => whenAnyPropertyChanged(value, ...propertiesToMonitor))); 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/cache/operators/whenChanged.ts: -------------------------------------------------------------------------------- 1 | import { isNotifyPropertyChanged, notificationsFor, NotifyPropertyChangedType } from '../../notify/notifyPropertyChangedSymbol'; 2 | import { filter, map } from 'rxjs/operators'; 3 | import { concat, defer, Observable, of } from 'rxjs'; 4 | 5 | /** 6 | * Used for whenAnyChanged 7 | * @ignore 8 | */ 9 | export type PropertyValue = { sender: TObject; value: TObject[TProperty] }; 10 | 11 | /** 12 | * Used for whenAny operators 13 | * @ignore 14 | */ 15 | export function whenChanged(value: TObject, key: TProperty, notifyInitial = true, fallbackValue?: () => TObject[TProperty]) { 16 | if (!isNotifyPropertyChanged(value)) { 17 | throw new Error( 18 | 'Object must implement the notifyPropertyChangedSymbol or inherit from the NotifyPropertyChangedBase class or be wrapped by the proxy method observePropertyChanges', 19 | ); 20 | } 21 | return whenChangedValues(value, key, notifyInitial, fallbackValue).pipe( 22 | filter(x => !!x.value), 23 | map(z => z.value), 24 | ); 25 | } 26 | 27 | function whenChangedValues( 28 | value: TObject, 29 | key: TProperty, 30 | notifyInitial = true, 31 | fallbackValue?: () => TObject[TProperty], 32 | ): Observable, TProperty>> { 33 | if (!isNotifyPropertyChanged(value)) { 34 | throw new Error( 35 | 'Object must implement the notifyPropertyChangedSymbol or inherit from the NotifyPropertyChangedBase class or be wrapped by the proxy method observePropertyChanges', 36 | ); 37 | } 38 | const propertyChanged = notificationsFor(value).pipe( 39 | filter(x => x === key), 40 | map(t => ({ sender: value, value: value[key] } as PropertyValue, TProperty>)), 41 | ); 42 | return notifyInitial 43 | ? concat( 44 | defer(() => 45 | of({ 46 | sender: value, 47 | value: value[key] || fallbackValue?.(), 48 | } as PropertyValue, TProperty>), 49 | ), 50 | propertyChanged, 51 | ) 52 | : propertyChanged; 53 | } 54 | -------------------------------------------------------------------------------- /src/cache/operators/whenPropertyChanged.ts: -------------------------------------------------------------------------------- 1 | import { isNotifyPropertyChanged, NotifyPropertyChangedType } from '../../notify/notifyPropertyChangedSymbol'; 2 | import { Observable, OperatorFunction } from 'rxjs'; 3 | import { IChangeSet } from '../IChangeSet'; 4 | import { mergeMany } from './mergeMany'; 5 | import { whenChanged } from './whenChanged'; 6 | 7 | /** 8 | * Observes property changes for the specified property, starting with the current value 9 | * @category Operator 10 | * @param value The source 11 | * @param key The key to observe 12 | * @param notifyInitial If true the resulting observable includes the initial value 13 | * @param fallbackValue A fallback value may be specified to ensure a notification is received when a value is unobtainable. 14 | * For example when observing Parent.Child.Age, if Child == null the value is unobtainable as Age is a struct and cannot be set to Null. 15 | * For an object like Parent.Child.Sibling, sibling is an object so if Child == null, the value null and obtainable and is returned as null. 16 | */ 17 | export function whenPropertyChanged( 18 | value: NotifyPropertyChangedType, 19 | key: TProperty, 20 | notifyInitial?: boolean, 21 | fallbackValue?: () => TObject[TProperty], 22 | ): Observable; 23 | /** 24 | * Watches each item in the collection and notifies when any of them has changed 25 | * @category Operator 26 | * @typeparam TObject The type of the object. 27 | * @typeparam TKey The type of the key. 28 | * @typeparam TValue The type of the value. 29 | * @param key The key to watch 30 | * @param notifyInitial if set to true [notify on initial value]. 31 | */ 32 | export function whenPropertyChanged( 33 | key: TProperty, 34 | notifyInitial?: boolean, 35 | ): OperatorFunction, TProperty>, TObject[TProperty]>; 36 | export function whenPropertyChanged( 37 | value: NotifyPropertyChangedType | TProperty, 38 | key: TProperty | boolean, 39 | notifyInitial?: boolean, 40 | fallbackValue?: () => TObject[TProperty], 41 | ) { 42 | if (isNotifyPropertyChanged(value)) { 43 | return whenChanged(value as any, key as TProperty, notifyInitial, fallbackValue); 44 | } else { 45 | return function whenValueChangedOperator(source: Observable, TProperty>>) { 46 | return source.pipe(mergeMany(v => whenChanged(v, value as TProperty, key as boolean | undefined))); 47 | }; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/cache/operators/whenValueChanged.ts: -------------------------------------------------------------------------------- 1 | import { isNotifyPropertyChanged, NotifyPropertyChangedType } from '../../notify/notifyPropertyChangedSymbol'; 2 | import { Observable, OperatorFunction } from 'rxjs'; 3 | import { IChangeSet } from '../IChangeSet'; 4 | import { mergeMany } from './mergeMany'; 5 | import { whenChanged } from './whenChanged'; 6 | 7 | /** 8 | * Observes property changes for the specified property, starting with the current value 9 | * @category Operator 10 | * @param value The source 11 | * @param key The key to observe 12 | * @param notifyInitial If true the resulting observable includes the initial value 13 | * @param fallbackValue A fallback value may be specified to ensure a notification is received when a value is unobtainable. 14 | * For example when observing Parent.Child.Age, if Child == null the value is unobtainable as Age is a struct and cannot be set to Null. 15 | * For an object like Parent.Child.Sibling, sibling is an object so if Child == null, the value null and obtainable and is returned as null. 16 | */ 17 | export function whenValueChanged( 18 | value: TObject, 19 | key: TProperty, 20 | notifyInitial?: boolean, 21 | fallbackValue?: () => TObject[TProperty], 22 | ): Observable; 23 | /** 24 | * Watches each item in the collection and notifies when any of them has changed 25 | * @category Operator 26 | * @typeparam TObject The type of the object. 27 | * @typeparam TKey The type of the key. 28 | * @typeparam TValue The type of the value. 29 | * @param key The key to watch 30 | * @param notifyInitial if set to true [notify on initial value]. 31 | */ 32 | export function whenValueChanged( 33 | key: TProperty, 34 | notifyInitial?: boolean, 35 | ): OperatorFunction, TObject[TProperty]>; 36 | export function whenValueChanged( 37 | value: NotifyPropertyChangedType | TProperty, 38 | key: TProperty | boolean, 39 | notifyInitial?: boolean, 40 | fallbackValue?: () => TObject[TProperty], 41 | ) { 42 | if (typeof value !== 'string' && typeof value !== 'symbol') { 43 | if (!isNotifyPropertyChanged(value)) { 44 | throw new Error( 45 | 'Object must implement the notifyPropertyChangedSymbol or inherit from the NotifyPropertyChangedBase class or be wrapped by the proxy method observePropertyChanges', 46 | ); 47 | } 48 | return whenChanged(value as any, key as TProperty, notifyInitial, fallbackValue); 49 | } else { 50 | return function whenValueChangedOperator(source: Observable>) { 51 | return source.pipe(mergeMany(v => whenChanged(v, value as TProperty, key as boolean | undefined))); 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/cache/operators/whereReasonsAre.ts: -------------------------------------------------------------------------------- 1 | import { ChangeReason } from '../ChangeReason'; 2 | import { MonoTypeOperatorFunction } from 'rxjs'; 3 | import { IChangeSet } from '../IChangeSet'; 4 | import { map } from 'rxjs/operators'; 5 | import { ChangeSet } from '../ChangeSet'; 6 | import { from as ixFrom } from 'ix/Ix.dom.iterable'; 7 | import { filter as ixFilter } from 'ix/iterable/operators'; 8 | import { notEmpty } from './notEmpty'; 9 | import { MonoTypeChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 10 | 11 | /** 12 | * Includes changes for the specified reasons only 13 | * @category Operator 14 | * @typeparam TObject The type of the object. 15 | * @typeparam TKey The type of the key. 16 | * @param reasons The reasons. 17 | */ 18 | export function whereReasonsAre(...reasons: ChangeReason[]): MonoTypeChangeSetOperatorFunction { 19 | return function onItemUpdatedOperator(source) { 20 | return source.pipe( 21 | map(updates => new ChangeSet(ixFrom(updates).pipe(ixFilter(x => reasons.includes(x.reason))))), 22 | notEmpty(), 23 | ); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/cache/operators/whereReasonsAreNot.ts: -------------------------------------------------------------------------------- 1 | import { ChangeReason } from '../ChangeReason'; 2 | import { MonoTypeOperatorFunction } from 'rxjs'; 3 | import { IChangeSet } from '../IChangeSet'; 4 | import { map } from 'rxjs/operators'; 5 | import { ChangeSet } from '../ChangeSet'; 6 | import { from as ixFrom } from 'ix/Ix.dom.iterable'; 7 | import { filter as ixFilter } from 'ix/iterable/operators'; 8 | import { notEmpty } from './notEmpty'; 9 | import { MonoTypeChangeSetOperatorFunction } from '../ChangeSetOperatorFunction'; 10 | 11 | /** 12 | * Excludes updates for the specified reasons 13 | * @category Operator 14 | * @typeparam TObject The type of the object. 15 | * @typeparam TKey The type of the key. 16 | * @param reasons The reasons. 17 | */ 18 | export function whereReasonsAreNot(...reasons: ChangeReason[]): MonoTypeChangeSetOperatorFunction { 19 | return function onItemUpdatedOperator(source) { 20 | return source.pipe( 21 | map(updates => new ChangeSet(ixFrom(updates).pipe(ixFilter(x => !reasons.includes(x.reason))))), 22 | notEmpty(), 23 | ); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/cache/operators/xor.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { IChangeSet } from '../IChangeSet'; 3 | import { combineCache } from './combineCache'; 4 | import { ArrayOrIterable } from '../../util/ArrayOrIterable'; 5 | 6 | /** 7 | * Apply a logical Xor operator between the collections. 8 | * Items which are only in one of the sources are included in the result 9 | * @category Operator 10 | * @typeparam TObject The type of the object. 11 | * @typeparam TKey The type of the key. 12 | * @param items The items 13 | */ 14 | export function xor(items: ArrayOrIterable>>) { 15 | return combineCache('xor', items); 16 | } 17 | -------------------------------------------------------------------------------- /src/diagnostics.ts: -------------------------------------------------------------------------------- 1 | export * from './diagnostics/ChangeStatistics'; 2 | export * from './diagnostics/ChangeSummary'; 3 | -------------------------------------------------------------------------------- /src/diagnostics/ChangeStatistics.ts: -------------------------------------------------------------------------------- 1 | /** 2 | /// Object used to capture accumulated changes 3 | */ 4 | export class ChangeStatistics { 5 | /** 6 | /// Initializes a new instance of the class. 7 | */ 8 | public constructor(index?: number, adds?: number, updates?: number, removes?: number, refreshes?: number, moves?: number, count?: number) { 9 | this.index = index ?? -1; 10 | this.adds = adds ?? 0; 11 | this.updates = updates ?? 0; 12 | this.removes = removes ?? 0; 13 | this.refreshes = refreshes ?? 0; 14 | this.moves = moves ?? 0; 15 | this.count = count ?? 0; 16 | this.lastUpdated = new Date(); 17 | } 18 | 19 | /** 20 | /// Gets the adds. 21 | */ 22 | /// 23 | /// The adds. 24 | /// 25 | public readonly adds: number; 26 | 27 | /** 28 | /// Gets the updates. 29 | */ 30 | /// 31 | /// The updates. 32 | /// 33 | public readonly updates: number; 34 | 35 | /** 36 | /// Gets the removes. 37 | */ 38 | /// 39 | /// The removes. 40 | /// 41 | public readonly removes: number; 42 | 43 | /** 44 | /// Gets the refreshes. 45 | */ 46 | /// 47 | /// The refreshes. 48 | /// 49 | public readonly refreshes: number; 50 | 51 | /** 52 | /// Gets the count. 53 | */ 54 | /// 55 | /// The count. 56 | /// 57 | public readonly count: number; 58 | 59 | /** 60 | /// Gets the index. 61 | */ 62 | /// 63 | /// The index. 64 | /// 65 | public readonly index: number; 66 | 67 | /** 68 | /// Gets the moves. 69 | */ 70 | /// 71 | /// The moves. 72 | /// 73 | public readonly moves: number; 74 | 75 | /** 76 | /// Gets the last updated. 77 | */ 78 | /// 79 | /// The last updated. 80 | /// 81 | public readonly lastUpdated: Date; 82 | 83 | /// 84 | public toString() { 85 | return `CurrentIndex: ${this.index}, Adds: ${this.adds}, Updates: ${this.updates}, Removes: ${this.removes}, Refreshes: ${this.refreshes}, Size: ${this.count}, Timestamp: ${this.lastUpdated}`; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/diagnostics/ChangeSummary.ts: -------------------------------------------------------------------------------- 1 | /** 2 | /// Accumulates change statics 3 | */ 4 | import { ChangeStatistics } from './ChangeStatistics'; 5 | 6 | export class ChangeSummary { 7 | private readonly _index: number; 8 | 9 | /** 10 | /// An empty instance of change summary 11 | */ 12 | public static readonly empty = new ChangeSummary(); 13 | 14 | /** 15 | /// Initializes a new instance of the class. 16 | */ 17 | public constructor(index?: number, latest?: ChangeStatistics, overall?: ChangeStatistics) { 18 | this.latest = latest ?? new ChangeStatistics(); 19 | this.overall = overall ?? new ChangeStatistics(); 20 | this._index = index ?? -1; 21 | } 22 | 23 | /** 24 | /// Gets the latest change 25 | */ 26 | /// 27 | /// The latest. 28 | /// 29 | public readonly latest: ChangeStatistics; 30 | 31 | /** 32 | /// Gets the overall change count 33 | */ 34 | /// 35 | /// The overall. 36 | /// 37 | public readonly overall: ChangeStatistics; 38 | 39 | /// 40 | public toString() { 41 | return `CurrentIndex: ${this._index}, Latest Size: ${this.latest.count}, Overall Size: ${this.overall.count}`; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/diagnostics/operators.ts: -------------------------------------------------------------------------------- 1 | export * from './operators/CollectUpdateStats'; 2 | -------------------------------------------------------------------------------- /src/diagnostics/operators/CollectUpdateStats.ts: -------------------------------------------------------------------------------- 1 | import { OperatorFunction, Observable } from 'rxjs'; 2 | import { ChangeSummary } from '../ChangeSummary'; 3 | import { IChangeSet } from '../../cache/IChangeSet'; 4 | import { scan } from 'rxjs/operators'; 5 | import { ChangeStatistics } from '../ChangeStatistics'; 6 | 7 | /** 8 | * Accumulates update statistics 9 | * @category Diagnostic Operator 10 | * @typeparam TSource The type of the source. 11 | * @typeparam TKey The type of the key. 12 | * @param source The source. 13 | */ 14 | export function collectUpdateStats(): OperatorFunction, ChangeSummary> { 15 | return function collectUpdateStatsOperator(source: Observable>) { 16 | return source.pipe( 17 | scan((seed, next) => { 18 | const index = seed.overall.index + 1; 19 | const adds = seed.overall.adds + next.adds; 20 | const updates = seed.overall.updates + next.updates; 21 | const removes = seed.overall.removes + next.removes; 22 | const evaluates = seed.overall.refreshes + next.refreshes; 23 | const moves = seed.overall.moves + next.moves; 24 | const total = seed.overall.count + next.size; 25 | 26 | const latest = new ChangeStatistics(index, next.adds, next.updates, next.removes, next.refreshes, next.moves, next.size); 27 | const overall = new ChangeStatistics(index, adds, updates, removes, evaluates, moves, total); 28 | return new ChangeSummary(index, latest, overall); 29 | }, ChangeSummary.empty), 30 | ); 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './binding'; 2 | export * from './cache'; 3 | export * from './cache/operators'; 4 | export * from './diagnostics'; 5 | export * from './diagnostics/operators'; 6 | export * from './notify'; 7 | export * from './util'; 8 | -------------------------------------------------------------------------------- /src/notify.ts: -------------------------------------------------------------------------------- 1 | import { observePropertyChanges, notificationsFor, toRaw } from './notify/notifyPropertyChangedSymbol'; 2 | export { observePropertyChanges, notificationsFor, toRaw }; 3 | -------------------------------------------------------------------------------- /src/notify/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | observePropertyChanges, 3 | notificationsFor, 4 | toRaw, 5 | NotifyCollectionChangedType, 6 | NotifyPropertyChangedType, 7 | isNotifyCollectionChanged, 8 | isNotifyPropertyChanged, 9 | observeCollectionChanges, 10 | } from './notifyPropertyChangedSymbol'; 11 | export { 12 | observePropertyChanges, 13 | notificationsFor, 14 | toRaw, 15 | NotifyCollectionChangedType, 16 | NotifyPropertyChangedType, 17 | isNotifyCollectionChanged, 18 | isNotifyPropertyChanged, 19 | observeCollectionChanges, 20 | }; 21 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | export * from './util/CompositeDisposable'; 2 | export * from './util/DisposableBase'; 3 | export * from './util/Disposable'; 4 | export * from './util/isEqualityComparer'; 5 | export * from './util/RefCountDisposable'; 6 | export * from './util/SerialDisposable'; 7 | export * from './util/SingleAssignmentDisposable'; 8 | export * from './util/Lazy'; 9 | export { using } from './util/using'; 10 | -------------------------------------------------------------------------------- /src/util/ArrayOrIterable.ts: -------------------------------------------------------------------------------- 1 | export type ArrayOrIterable = Array | Iterable; 2 | -------------------------------------------------------------------------------- /src/util/CompositeDisposable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | import { Disposable, IDisposable, IDisposableOrSubscription, ISubscription } from './Disposable'; 5 | export class CompositeDisposable extends Set implements IDisposable, ISubscription { 6 | private _isDisposed = false; 7 | 8 | constructor(...disposables: IDisposableOrSubscription[]) { 9 | super(disposables); 10 | } 11 | 12 | public get isDisposed() { 13 | return this._isDisposed; 14 | } 15 | 16 | public dispose() { 17 | if (this._isDisposed) return; 18 | this._isDisposed = true; 19 | if (this.size) { 20 | this.forEach(disposable => Disposable.of(disposable).dispose()); 21 | this.clear(); 22 | } 23 | } 24 | 25 | public unsubscribe(): void { 26 | this.dispose(); 27 | } 28 | 29 | public add(...disposables: IDisposableOrSubscription[]) { 30 | if (this.isDisposed) { 31 | disposables.forEach(item => Disposable.of(item).dispose()); 32 | } else { 33 | disposables.forEach(item => super.add(item)); 34 | } 35 | return this; 36 | } 37 | 38 | public remove(disposable: IDisposableOrSubscription) { 39 | this.delete(disposable); 40 | return this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/util/Disposable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | export interface IDisposable { 5 | dispose(): void; 6 | } 7 | 8 | export interface ISubscription { 9 | unsubscribe(): void; 10 | } 11 | 12 | export function isDisposable(value: any): value is IDisposable { 13 | return !!value.dispose; 14 | } 15 | 16 | export function isSubscription(value: any): value is ISubscription { 17 | return !!value.unsubscribe; 18 | } 19 | 20 | export function canBeDisposed(value: any): value is IDisposableOrSubscription { 21 | return !!(value.dispose || value.unsubscribe) || typeof value === 'function'; 22 | } 23 | 24 | export type IDisposableOrSubscription = IDisposable | ISubscription | (() => void); 25 | // eslint-disable-next-line prefer-const 26 | let empty: Disposable; 27 | 28 | export class Disposable implements IDisposable, ISubscription { 29 | public static get empty() { 30 | return empty; 31 | } 32 | 33 | /* tslint:disable-next-line:no-reserved-keywords no-any */ 34 | public static of(value: any) { 35 | if (!value) { 36 | return empty; 37 | } 38 | 39 | if (value.dispose) { 40 | return value; 41 | } 42 | return new Disposable(value); 43 | } 44 | 45 | public static create(value: IDisposableOrSubscription): IDisposable & ISubscription; 46 | public static create(action: () => void): IDisposable & ISubscription; 47 | public static create(action: IDisposableOrSubscription | (() => void)) { 48 | return new Disposable(action); 49 | } 50 | 51 | private _action: () => void = () => {}; 52 | private _isDisposed = false; 53 | 54 | constructor(value: IDisposableOrSubscription); 55 | /* tslint:disable-next-line:no-any */ 56 | constructor(value: any) { 57 | if (!value) { 58 | return empty; 59 | } 60 | 61 | if (typeof value === 'function') { 62 | this._action = value; 63 | } else if (value.unsubscribe) { 64 | this._action = () => (value).unsubscribe(); 65 | } else if (value.dispose) { 66 | this._action = () => (value).dispose(); 67 | } 68 | } 69 | 70 | public get isDisposed() { 71 | return this._isDisposed; 72 | } 73 | 74 | public dispose() { 75 | if (this._isDisposed) return; 76 | this._isDisposed = true; 77 | this._action(); 78 | } 79 | 80 | public unsubscribe(): void { 81 | this.dispose(); 82 | } 83 | } 84 | 85 | empty = new Disposable(() => { 86 | /* */ 87 | }); 88 | -------------------------------------------------------------------------------- /src/util/DisposableBase.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | import { CompositeDisposable } from './CompositeDisposable'; 5 | import { IDisposable } from './Disposable'; 6 | export abstract class DisposableBase implements IDisposable { 7 | protected _disposable: CompositeDisposable; 8 | constructor() { 9 | this._disposable = new CompositeDisposable(); 10 | } 11 | 12 | public dispose() { 13 | this._disposable.dispose(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/util/Lazy.ts: -------------------------------------------------------------------------------- 1 | export class Lazy { 2 | /** 3 | * 4 | */ 5 | private _value?: T; 6 | private _isValueCreated = false; 7 | constructor(private _factory: () => T) {} 8 | 9 | public get value() { 10 | if (this._isValueCreated) { 11 | return this._value; 12 | } 13 | this._value = this._factory(); 14 | this._isValueCreated = true; 15 | return this._value; 16 | } 17 | 18 | public get isValueCreated() { 19 | return this._isValueCreated; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/util/RefCountDisposable.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/prevent-abbreviations */ 2 | /** 3 | * 4 | */ 5 | import { Disposable, IDisposable, IDisposableOrSubscription, ISubscription } from './Disposable'; 6 | 7 | export class RefCountDisposable implements IDisposable { 8 | private _underlyingDisposable: IDisposable; 9 | private _isDisposed = false; 10 | private _isPrimaryDisposed = false; 11 | private _count = 0; 12 | 13 | constructor(underlyingDisposable: IDisposableOrSubscription) { 14 | this._underlyingDisposable = Disposable.of(underlyingDisposable); 15 | } 16 | 17 | public get isDisposed() { 18 | return this._isDisposed; 19 | } 20 | 21 | public dispose() { 22 | if (!this.isDisposed && !this._isPrimaryDisposed) { 23 | this._isPrimaryDisposed = true; 24 | if (this._count === 0) { 25 | this._isDisposed = true; 26 | this._underlyingDisposable.dispose(); 27 | } 28 | } 29 | } 30 | 31 | public unsubscribe(): void { 32 | this.dispose(); 33 | } 34 | 35 | public getDisposable() { 36 | if (this.isDisposed) { 37 | return Disposable.empty; 38 | } 39 | 40 | this._count += 1; 41 | return new InnerDisposable(this, () => { 42 | this._count -= 1; 43 | if (this._count === 0 && this._isPrimaryDisposed) { 44 | this._isDisposed = true; 45 | this._underlyingDisposable.dispose(); 46 | } 47 | }); 48 | } 49 | } 50 | 51 | class InnerDisposable extends Disposable { 52 | constructor(private _reference: RefCountDisposable, action: () => void) { 53 | super(action); 54 | } 55 | 56 | public dispose() { 57 | if (!this._reference.isDisposed && !this.isDisposed) { 58 | super.dispose(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/util/SerialDisposable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | import { IDisposable, ISubscription } from './Disposable'; 5 | 6 | export class SerialDisposable implements IDisposable { 7 | private _currentDisposable?: IDisposable | null; 8 | private _isDisposed = false; 9 | 10 | public get isDisposed() { 11 | return this._isDisposed; 12 | } 13 | 14 | public get disposable() { 15 | return this._currentDisposable; 16 | } 17 | 18 | public set disposable(value) { 19 | const shouldDispose = this.isDisposed; 20 | if (!shouldDispose) { 21 | this._currentDisposable = value; 22 | } 23 | if (!this.isDisposed) { 24 | this._currentDisposable = value; 25 | } 26 | if (this.isDisposed && value) { 27 | value.dispose(); 28 | } 29 | } 30 | 31 | public unsubscribe(): void { 32 | this.dispose(); 33 | } 34 | 35 | public dispose() { 36 | if (this.isDisposed) return; 37 | this._isDisposed = true; 38 | const old = this._currentDisposable; 39 | this._currentDisposable = null; 40 | if (old) { 41 | old.dispose(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/util/SingleAssignmentDisposable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | import { Disposable, IDisposable, IDisposableOrSubscription, ISubscription } from './Disposable'; 5 | 6 | export class SingleAssignmentDisposable implements IDisposable { 7 | private _currentDisposable?: Disposable | null; 8 | private _isDisposed = false; 9 | 10 | public get isDisposed() { 11 | return this._isDisposed; 12 | } 13 | 14 | public get disposable() { 15 | return this._currentDisposable; 16 | } 17 | public set disposable(value: IDisposableOrSubscription | null | undefined) { 18 | if (this._currentDisposable) { 19 | throw new Error('Disposable has already been assigned'); 20 | } 21 | if (!this.isDisposed) { 22 | this._currentDisposable = value ? new Disposable(value) : null; 23 | } 24 | if (this.isDisposed && value) { 25 | new Disposable(value).dispose(); 26 | } 27 | } 28 | 29 | public unsubscribe(): void { 30 | this.dispose(); 31 | } 32 | 33 | public dispose() { 34 | if (!this.isDisposed) { 35 | this._isDisposed = true; 36 | const old = this._currentDisposable; 37 | this._currentDisposable = null; 38 | if (old) { 39 | old.dispose(); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/util/deepEqualMapAdapter.ts: -------------------------------------------------------------------------------- 1 | import { find } from 'ix/iterable'; 2 | import equal from 'fast-deep-equal'; 3 | 4 | export function deepEqualMapAdapter( 5 | map: Map, 6 | ): { 7 | get: typeof Map.prototype.get; 8 | set: typeof Map.prototype.set; 9 | has: typeof Map.prototype.has; 10 | delete: typeof Map.prototype.delete; 11 | } { 12 | return { 13 | get(key) { 14 | return map.get(key) ?? find(map, { predicate: f => equal(f[0], key) }); 15 | }, 16 | set(key, value) { 17 | const foundKey = find(map, { predicate: ([k]) => equal(k, key) }); 18 | if (foundKey !== undefined) { 19 | map.delete(foundKey[0]); 20 | } 21 | return map.set(key, value); 22 | }, 23 | delete(key) { 24 | const foundKey = find(map, { predicate: ([k]) => equal(k, key) }); 25 | if (foundKey !== undefined) { 26 | return map.delete(foundKey[0]); 27 | } else { 28 | return map.delete(key); 29 | } 30 | }, 31 | has(key) { 32 | const foundKey = find(map, { predicate: ([k]) => equal(k, key) }); 33 | if (foundKey !== undefined) return true; 34 | return map.has(key); 35 | }, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/util/emptyIterator.ts: -------------------------------------------------------------------------------- 1 | function* emptyIterator(): IterableIterator {} 2 | -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | export * from './CompositeDisposable'; 5 | export * from './DisposableBase'; 6 | export * from './Disposable'; 7 | export * from './RefCountDisposable'; 8 | export * from './SerialDisposable'; 9 | export * from './SingleAssignmentDisposable'; 10 | export * from './Lazy'; 11 | export { using } from './using'; 12 | -------------------------------------------------------------------------------- /src/util/is.ts: -------------------------------------------------------------------------------- 1 | const hasOwnProperty = Object.prototype.hasOwnProperty; 2 | /** @ignore */ 3 | export const hasOwn = (value: unknown, key: string | symbol): key is keyof typeof value => hasOwnProperty.call(value, key); 4 | /** @ignore */ 5 | export const isArray = Array.isArray; 6 | /** @ignore */ 7 | export const isFunction = (value: any): value is Function => typeof value === 'function'; 8 | /** @ignore */ 9 | export const isString = (value: any): value is string => typeof value === 'string'; 10 | /** @ignore */ 11 | export const isSymbol = (value: any): value is symbol => typeof value === 'symbol'; 12 | /** @ignore */ 13 | export const isObject = (value: any): value is Record => value !== null && typeof value === 'object'; 14 | /** @ignore */ 15 | export const isPromise = (value: any): value is Promise => { 16 | return isObject(value) && isFunction(value.then) && isFunction(value.catch); 17 | }; 18 | 19 | // compare whether a value has changed, accounting for NaN. 20 | /** @ignore */ 21 | export const hasChanged = (value: any, oldValue: any): boolean => value !== oldValue && (value === value || oldValue === oldValue); 22 | -------------------------------------------------------------------------------- /src/util/isEqualityComparer.ts: -------------------------------------------------------------------------------- 1 | export type EqualityComparer = (a: T, b: T) => boolean; 2 | 3 | export function isEqualityComparer(value: any): value is Function { 4 | return !!value?.equals; 5 | } 6 | -------------------------------------------------------------------------------- /src/util/isIterable.ts: -------------------------------------------------------------------------------- 1 | export function isIterable(value: any): value is Iterable { 2 | return value && value[Symbol.iterator]; 3 | } 4 | -------------------------------------------------------------------------------- /src/util/isMap.ts: -------------------------------------------------------------------------------- 1 | export function isMap(value: any): value is Map { 2 | return value && value[Symbol.toStringTag] === 'Map'; 3 | } 4 | -------------------------------------------------------------------------------- /src/util/isSet.ts: -------------------------------------------------------------------------------- 1 | export function isSet(value: any): value is Set { 2 | return value && value[Symbol.toStringTag] === 'Set'; 3 | } 4 | -------------------------------------------------------------------------------- /src/util/isWeakMap.ts: -------------------------------------------------------------------------------- 1 | export function isWeakMap(value: any): value is WeakMap { 2 | return value && value[Symbol.toStringTag] === 'WeakMap'; 3 | } 4 | -------------------------------------------------------------------------------- /src/util/isWeakSet.ts: -------------------------------------------------------------------------------- 1 | export function isWeakSet(value: any): value is WeakSet { 2 | return value && value[Symbol.toStringTag] === 'WeakSet'; 3 | } 4 | -------------------------------------------------------------------------------- /src/util/tryGetValue.ts: -------------------------------------------------------------------------------- 1 | export function tryGetValue( 2 | data: { 3 | get(key: K): V | undefined; 4 | has(key: K): boolean; 5 | }, 6 | key: K, 7 | ): { value: V; found: true } | { found: false } { 8 | if (data.has(key)) return { found: true, value: data.get(key)! }; 9 | return { found: false }; 10 | } 11 | -------------------------------------------------------------------------------- /src/util/using.ts: -------------------------------------------------------------------------------- 1 | import { canBeDisposed, Disposable, IDisposableOrSubscription, isDisposable } from './Disposable'; 2 | 3 | export function using(resource: TDisposable, func: (resource: TDisposable) => TResult): TResult { 4 | const disposable = canBeDisposed(resource) && !isDisposable(resource) ? Disposable.create(resource) : resource; 5 | let result: TResult | undefined; 6 | try { 7 | result = func(resource); 8 | } finally { 9 | disposable.dispose(); 10 | } 11 | 12 | return result!; 13 | } 14 | -------------------------------------------------------------------------------- /test/binding/BindCacheFixture.spec.ts: -------------------------------------------------------------------------------- 1 | import { Person } from '../domain/Person'; 2 | import { ISourceCache } from '../../src/cache/ISourceCache'; 3 | import { ISourceUpdater } from '../../src/cache/ISourceUpdater'; 4 | import { Subject, Subscription } from 'rxjs'; 5 | import { SortComparer } from '../../src/cache/operators/sort'; 6 | import { SourceCache, updateable } from '../../src/cache/SourceCache'; 7 | import { bind } from '../../src/cache/operators/bind'; 8 | import { first, toArray } from 'ix/iterable'; 9 | import { randomPersonGenerator } from '../domain/RandomPersonGenerator'; 10 | 11 | describe('BindingListCacheFixture', () => { 12 | let _collection: Person[]; 13 | let _source: ISourceCache & ISourceUpdater; 14 | let _binder: Subscription; 15 | const _comparer = SortComparer.ascending('name'); 16 | 17 | beforeEach(() => { 18 | _collection = []; 19 | _source = updateable(new SourceCache(p => p.name)); 20 | _binder = _source.connect().pipe(bind(_collection)).subscribe(); 21 | }); 22 | 23 | afterEach(() => { 24 | _binder.unsubscribe(); 25 | _source.dispose(); 26 | }); 27 | 28 | it('AddToSourceAddsToDestination', () => { 29 | const person = new Person('Adult1', 50); 30 | _source.addOrUpdate(person); 31 | 32 | expect(_collection.length).toBe(1); 33 | expect(first(_collection)).toBe(person); 34 | }); 35 | 36 | it('UpdateToSourceUpdatesTheDestination', () => { 37 | const person = new Person('Adult1', 50); 38 | const personUpdated = new Person('Adult1', 51); 39 | _source.addOrUpdate(person); 40 | _source.addOrUpdate(personUpdated); 41 | 42 | expect(_collection.length).toBe(1); 43 | expect(first(_collection)).toEqual(personUpdated); 44 | }); 45 | 46 | it('RemoveSourceRemovesFromTheDestination', () => { 47 | const person = new Person('Adult1', 50); 48 | _source.addOrUpdate(person); 49 | _source.remove(person); 50 | 51 | expect(_collection.length).toBe(0); 52 | }); 53 | 54 | it('BatchAdd', () => { 55 | const people = toArray(randomPersonGenerator(100)); 56 | _source.addOrUpdateValues(people); 57 | 58 | expect(_collection.length).toBe(100); 59 | expect(_collection).toEqual(_collection); 60 | }); 61 | 62 | it('BatchRemove', () => { 63 | const people = toArray(randomPersonGenerator(100)); 64 | _source.addOrUpdateValues(people); 65 | _source.clear(); 66 | expect(_collection.length).toBe(0); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/binding/BindEqualityTests.spec.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from 'rxjs'; 2 | import { IChangeSet } from '../../src/cache/IChangeSet'; 3 | import { Person } from '../domain/Person'; 4 | import { bind } from '../../src/cache/operators/bind'; 5 | import { ChangeSet } from '../../src/cache/ChangeSet'; 6 | import { Change } from '../../src/cache/Change'; 7 | 8 | describe('BindEqualityTests', () => { 9 | it('should not remove person when given the same person as two different references when using reference equality', () => { 10 | const subject = new Subject>(); 11 | const values: Person[] = []; 12 | subject.pipe(bind(values, bind.indexOfAdapter(values))).subscribe(); 13 | 14 | const person = new Person('Adult1', 50); 15 | subject.next(new ChangeSet([Change.add(person.name, person)])); 16 | 17 | expect(values.length).toBe(1); 18 | 19 | subject.next(new ChangeSet([Change.remove(person.name, new Person('Adult1', 50))])); 20 | 21 | expect(values.length).toBe(1); 22 | }); 23 | it('should not remove person when given the same person as two different references using deep equal equality', () => { 24 | const subject = new Subject>(); 25 | const values: Person[] = []; 26 | subject.pipe(bind(values, bind.deepEqualAdapter(values))).subscribe(); 27 | 28 | const person = new Person('Adult1', 50); 29 | subject.next(new ChangeSet([Change.add(person.name, person)])); 30 | 31 | expect(values.length).toBe(1); 32 | 33 | subject.next(new ChangeSet([Change.remove(person.name, new Person('Adult1', 50))])); 34 | 35 | expect(values.length).toBe(0); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/cache/BatchFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DynamicData.Tests.Domain; 3 | using FluentAssertions; 4 | using Microsoft.Reactive.Testing; 5 | using Xunit; 6 | 7 | namespace DynamicData.Tests.Cache 8 | { 9 | 10 | public class BatchFixture: IDisposable 11 | { 12 | let _source: ISourceCache & ISourceUpdater; 13 | let _results: ChangeSetAggregator; 14 | let _scheduler: TestScheduler; 15 | 16 | public BatchFixture() 17 | { 18 | _scheduler = new TestScheduler(); 19 | _source = updateable(new SourceCache((p => p.Key))); 20 | _results = _source.Connect().Batch(TimeSpan.FromMinutes(1), _scheduler).AsAggregator(); 21 | } 22 | 23 | afterEach(() => { 24 | _results.Dispose(); 25 | _source.Dispose(); 26 | }); 27 | 28 | it('NoResultsWillBeReceivedBeforeClosingBuffer', () => { 29 | _source.AddOrUpdate(new Person("A", 1)); 30 | expect(_results.messages.length).toBe(0); 31 | }); it('ResultsWillBeReceivedAfterClosingBuffer', () => { 32 | _source.AddOrUpdate(new Person("A", 1)); 33 | 34 | //go forward an arbitary amount of time 35 | _scheduler.AdvanceBy(TimeSpan.FromSeconds(61).Ticks); 36 | expect(_results.messages.length).toBe(1); 37 | }); } 38 | } 39 | -------------------------------------------------------------------------------- /test/cache/BufferInitialFixture.cs: -------------------------------------------------------------------------------- 1 | describe('BufferInitialFixture', () => { 2 | { 3 | private static readonly ICollection People = Enumerable.Range(1, 10_000).Select(i => new Person(i.ToString(), i)).ToList(); 4 | 5 | it('BufferInitial', () => { 6 | var scheduler = new TestScheduler(); 7 | 8 | var cache = new SourceCache(i => i.Name)//dispose var cache = new SourceCache(i => i.Name) 9 | var aggregator = cache.Connect().BufferInitial(TimeSpan.FromSeconds(1), scheduler).AsAggregator()//dispose 10 | 11 | foreach (var item in People) 12 | { 13 | cache.AddOrUpdate(item); 14 | // dispose var aggregator = cache.Connect().BufferInitial(TimeSpan.FromSeconds(1), scheduler).AsAggregator() 15 | 16 | expect(aggregator.data.size).toBe(0); 17 | expect(aggregator.messages.length).toBe(0); 18 | 19 | scheduler.Start(); 20 | 21 | expect(aggregator.data.size).toBe(10_000); 22 | expect(aggregator.messages.length).toBe(1); 23 | 24 | cache.AddOrUpdate(new Person("_New",1)); 25 | 26 | expect(aggregator.data.size).toBe(10_001); 27 | expect(aggregator.messages.length).toBe(2); 28 | }); } 29 | } 30 | }); -------------------------------------------------------------------------------- /test/cache/DeferUntilLoadedFixture.spec.ts: -------------------------------------------------------------------------------- 1 | import { IChangeSet } from '../../src/cache/IChangeSet'; 2 | import { SourceCache, updateable } from '../../src/cache/SourceCache'; 3 | import { deferUntilLoaded } from '../../src/cache/operators/deferUntilLoaded'; 4 | import { first } from 'ix/iterable'; 5 | import { skipInitial } from '../../src/cache/operators/skipInitial'; 6 | import { Person } from '../domain/Person'; 7 | 8 | describe('DeferAndSkipFixture', () => { 9 | it('DeferUntilLoadedDoesNothingUntilDataHasBeenReceived', () => { 10 | let updateReceived = false; 11 | let result: IChangeSet | undefined = undefined; 12 | 13 | const cache = updateable(new SourceCache(p => p.name)); 14 | 15 | const deferStream = cache 16 | .connect() 17 | .pipe(deferUntilLoaded()) 18 | .subscribe(changes => { 19 | updateReceived = true; 20 | result = changes; 21 | }); 22 | 23 | const person = new Person('Test', 1); 24 | expect(updateReceived).toBe(false); 25 | cache.addOrUpdate(person); 26 | 27 | expect(updateReceived).toBe(true); 28 | expect(result!.adds).toBe(1); 29 | expect(first(result!)!.current).toBe(person); 30 | deferStream.unsubscribe(); 31 | }); 32 | 33 | it('SkipInitialDoesNotReturnTheFirstBatchOfData', () => { 34 | let updateReceived = false; 35 | 36 | const cache = updateable(new SourceCache(p => p.name)); 37 | 38 | const deferStream = cache 39 | .connect() 40 | .pipe(skipInitial()) 41 | .subscribe(changes => (updateReceived = true)); 42 | 43 | expect(updateReceived).toBe(false); 44 | 45 | cache.addOrUpdate(new Person('P1', 1)); 46 | 47 | expect(updateReceived).toBe(false); 48 | 49 | cache.addOrUpdate(new Person('P2', 2)); 50 | expect(updateReceived).toBe(true); 51 | deferStream.unsubscribe(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/cache/ExceptFixture.spec.ts: -------------------------------------------------------------------------------- 1 | import { ISourceCache } from '../../src/cache/ISourceCache'; 2 | import { ISourceUpdater } from '../../src/cache/ISourceUpdater'; 3 | import { asAggregator, ChangeSetAggregator } from '../util/aggregator'; 4 | import { SourceCache, updateable } from '../../src/cache/SourceCache'; 5 | import { except } from '../../src/cache/operators/except'; 6 | import { Person } from '../domain/Person'; 7 | 8 | describe('ExceptFixture', () => { 9 | let _targetSource: ISourceCache & ISourceUpdater; 10 | let _exceptSource: ISourceCache & ISourceUpdater; 11 | let _results: ChangeSetAggregator; 12 | 13 | beforeEach(() => { 14 | _targetSource = updateable(new SourceCache(p => p.name)); 15 | _exceptSource = updateable(new SourceCache(p => p.name)); 16 | _results = asAggregator(except([_targetSource.connect(), _exceptSource.connect()])); 17 | }); 18 | 19 | afterEach(() => { 20 | _targetSource.dispose(); 21 | _exceptSource.dispose(); 22 | _results.dispose(); 23 | }); 24 | 25 | it('UpdatingOneSourceOnlyProducesResult', () => { 26 | const person = new Person('Adult1', 50); 27 | _targetSource.addOrUpdate(person); 28 | 29 | expect(_results.messages.length).toBe(1); 30 | expect(_results.data.size).toBe(1); 31 | }); 32 | it('DoNotIncludeExceptListItems', () => { 33 | const person = new Person('Adult1', 50); 34 | _exceptSource.addOrUpdate(person); 35 | _targetSource.addOrUpdate(person); 36 | 37 | expect(_results.messages.length).toBe(0); 38 | expect(_results.data.size).toBe(0); 39 | }); 40 | it('RemovedAnItemFromExceptThenIncludesTheItem', () => { 41 | const person = new Person('Adult1', 50); 42 | _exceptSource.addOrUpdate(person); 43 | _targetSource.addOrUpdate(person); 44 | 45 | _exceptSource.remove(person); 46 | expect(_results.messages.length).toBe(1); 47 | expect(_results.data.size).toBe(1); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/cache/ForEachChangeFixture.spec.ts: -------------------------------------------------------------------------------- 1 | import { SourceCache, updateable } from '../../src/cache/SourceCache'; 2 | import { Change } from '../../src/cache/Change'; 3 | import { forEachChange } from '../../src/cache/operators/forEachChange'; 4 | import { ISourceUpdater } from '../../src/cache/ISourceUpdater'; 5 | import { range } from 'ix/iterable'; 6 | import { map } from 'ix/iterable/operators'; 7 | import { ISourceCache } from '../../src/cache/ISourceCache'; 8 | import { randomPersonGenerator } from '../domain/RandomPersonGenerator'; 9 | import { Person } from '../domain/Person'; 10 | 11 | describe('ForEachChangeFixture', () => { 12 | let _source: ISourceCache & ISourceUpdater; 13 | beforeEach(() => { 14 | _source = updateable(new SourceCache(p => p.name)); 15 | }); 16 | 17 | afterEach(() => { 18 | _source.dispose(); 19 | }); 20 | 21 | it('Test', () => { 22 | const messages: Change[] = []; 23 | const messageWriter = _source 24 | .connect() 25 | .pipe(forEachChange(x => messages.push(x))) 26 | .subscribe(); 27 | 28 | _source.addOrUpdateValues(randomPersonGenerator(100)); 29 | messageWriter.unsubscribe(); 30 | 31 | expect(messages.length).toBe(100); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/cache/IgnoreUpdateFixture.spec.ts: -------------------------------------------------------------------------------- 1 | import { ISourceUpdater } from '../../src/cache/ISourceUpdater'; 2 | import { Person } from '../domain/Person'; 3 | import { ChangeSetAggregator } from '../util/aggregator'; 4 | import { ISourceCache } from '../../src/cache/ISourceCache'; 5 | import { SourceCache, updateable } from '../../src/cache/SourceCache'; 6 | import { ignoreUpdateWhen } from '../../src/cache/operators/ignoreUpdateWhen'; 7 | 8 | describe('IgnoreUpdateFixture', () => { 9 | let _source: ISourceCache & ISourceUpdater; 10 | let _results: ChangeSetAggregator; 11 | 12 | beforeEach(() => { 13 | _source = updateable(new SourceCache(p => p.key)); 14 | _results = new ChangeSetAggregator(_source.connect().pipe(ignoreUpdateWhen((current, previous) => current === previous))); 15 | }); 16 | 17 | afterEach(() => { 18 | _source.dispose(); 19 | }); 20 | 21 | it('IgnoreFunctionWillIgnoreSubsequentUpdatesOfAnItem', () => { 22 | const person = new Person('Person', 10); 23 | _source.addOrUpdate(person); 24 | _source.addOrUpdate(person); 25 | _source.addOrUpdate(person); 26 | 27 | expect(_results.messages.length).toBe(1); 28 | expect(_results.data.size).toBe(1); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/cache/IncludeUpdateFixture.spec.ts: -------------------------------------------------------------------------------- 1 | import { ISourceCache } from '../../src/cache/ISourceCache'; 2 | import { Person } from '../domain/Person'; 3 | import { ISourceUpdater } from '../../src/cache/ISourceUpdater'; 4 | import { ChangeSetAggregator } from '../util/aggregator'; 5 | import { SourceCache, updateable } from '../../src/cache/SourceCache'; 6 | import { includeUpdateWhen } from '../../src/cache/operators/includeUpdateWhen'; 7 | 8 | describe('IncludeUpdateFixture', () => { 9 | let _source: ISourceCache & ISourceUpdater; 10 | let _results: ChangeSetAggregator; 11 | 12 | beforeEach(() => { 13 | _source = updateable(new SourceCache(p => p.key)); 14 | _results = new ChangeSetAggregator(_source.connect().pipe(includeUpdateWhen((current, previous) => current !== previous))); 15 | }); 16 | 17 | afterEach(() => { 18 | _source.dispose(); 19 | }); 20 | 21 | it('IgnoreFunctionWillIgnoreSubsequentUpdatesOfAnItem', () => { 22 | const person = new Person('Person', 10); 23 | _source.addOrUpdate(person); 24 | _source.addOrUpdate(person); 25 | _source.addOrUpdate(person); 26 | 27 | expect(_results.messages.length).toBe(1); 28 | expect(_results.data.size).toBe(1); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/cache/MergeManyFixture.spec.ts: -------------------------------------------------------------------------------- 1 | import { ISourceUpdater } from '../../src/cache/ISourceUpdater'; 2 | import { ISourceCache } from '../../src/cache/ISourceCache'; 3 | import { SourceCache, updateable } from '../../src/cache/SourceCache'; 4 | import { Subject } from 'rxjs'; 5 | import { mergeMap } from 'rxjs/operators'; 6 | import { mergeMany } from '../../src/cache/operators/mergeMany'; 7 | 8 | describe('MergeManyFixture', function () { 9 | let _source: ISourceCache & ISourceUpdater; 10 | 11 | beforeEach(() => { 12 | _source = updateable(new SourceCache(p => p.id)); 13 | }); 14 | 15 | afterEach(() => { 16 | _source.dispose(); 17 | }); 18 | 19 | class ObjectWithObservable { 20 | private readonly _changed = new Subject(); 21 | private _value: boolean = false; 22 | 23 | public constructor(id: number) { 24 | this.id = id; 25 | } 26 | 27 | public invokeObservable(value: boolean) { 28 | this._value = value; 29 | this._changed.next(value); 30 | } 31 | 32 | public observable = this._changed.asObservable(); 33 | public id: number; 34 | } 35 | 36 | // Invocations the only when child is invoked. 37 | it('InvocationOnlyWhenChildIsInvoked', () => { 38 | let invoked = false; 39 | 40 | const stream = _source 41 | .connect() 42 | .pipe(mergeMany(x => x.observable)) 43 | .subscribe(o => { 44 | invoked = true; 45 | }); 46 | 47 | const item = new ObjectWithObservable(1); 48 | _source.addOrUpdate(item); 49 | 50 | expect(invoked).toBe(false); 51 | 52 | item.invokeObservable(true); 53 | expect(invoked).toBe(true); 54 | stream.unsubscribe(); 55 | }); 56 | 57 | it('RemovedItemWillNotCauseInvocation', () => { 58 | let invoked = false; 59 | 60 | const stream = _source 61 | .connect() 62 | .pipe(mergeMany(x => x.observable)) 63 | .subscribe(o => { 64 | invoked = true; 65 | }); 66 | 67 | const item = new ObjectWithObservable(1); 68 | _source.addOrUpdate(item); 69 | _source.remove(item); 70 | expect(invoked).toBe(false); 71 | 72 | item.invokeObservable(true); 73 | expect(invoked).toBe(false); 74 | stream.unsubscribe(); 75 | }); 76 | 77 | it('EverythingIsUnsubscribedWhenStreamIsDisposed', () => { 78 | let invoked = false; 79 | 80 | const stream = _source 81 | .connect() 82 | .pipe(mergeMany(x => x.observable)) 83 | .subscribe(o => { 84 | invoked = true; 85 | }); 86 | 87 | const item = new ObjectWithObservable(1); 88 | _source.addOrUpdate(item); 89 | 90 | stream.unsubscribe(); 91 | 92 | item.invokeObservable(true); 93 | expect(invoked).toBe(false); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /test/cache/MergeManyItemsFixture.spec.ts: -------------------------------------------------------------------------------- 1 | import { ISourceCache } from '../../src/cache/ISourceCache'; 2 | import { ISourceUpdater } from '../../src/cache/ISourceUpdater'; 3 | import { SourceCache, updateable } from '../../src/cache/SourceCache'; 4 | import { Subject } from 'rxjs'; 5 | import { mergeMany } from '../../src/cache/operators/mergeMany'; 6 | import { mergeManyItems } from '../../src/cache/operators/mergeManyItems'; 7 | 8 | describe('MergeManyItemsFixture', function () { 9 | let _source: ISourceCache & ISourceUpdater; 10 | 11 | beforeEach(() => { 12 | _source = updateable(new SourceCache(p => p.id)); 13 | }); 14 | 15 | afterEach(() => { 16 | _source.dispose(); 17 | }); 18 | 19 | class ObjectWithObservable { 20 | private readonly _changed = new Subject(); 21 | private _value: boolean = false; 22 | 23 | public constructor(id: number) { 24 | this.id = id; 25 | } 26 | 27 | public invokeObservable(value: boolean) { 28 | this._value = value; 29 | this._changed.next(value); 30 | } 31 | 32 | public observable = this._changed.asObservable(); 33 | public id: number; 34 | } 35 | 36 | it('InvocationOnlyWhenChildIsInvoked', () => { 37 | let invoked = false; 38 | 39 | const stream = _source 40 | .connect() 41 | .pipe(mergeManyItems(x => x.observable)) 42 | .subscribe(o => { 43 | invoked = true; 44 | expect(o.item.id == 1).toBe(true); 45 | }); 46 | 47 | const item = new ObjectWithObservable(1); 48 | _source.addOrUpdate(item); 49 | 50 | expect(invoked).toBe(false); 51 | 52 | item.invokeObservable(true); 53 | expect(invoked).toBe(true); 54 | stream.unsubscribe(); 55 | }); 56 | 57 | it('RemovedItemWillNotCauseInvocation', () => { 58 | let invoked = false; 59 | 60 | const stream = _source 61 | .connect() 62 | .pipe(mergeManyItems(x => x.observable)) 63 | .subscribe(o => { 64 | invoked = true; 65 | expect(o.item.id == 1).toBe(true); 66 | }); 67 | 68 | const item = new ObjectWithObservable(1); 69 | _source.addOrUpdate(item); 70 | _source.remove(item); 71 | expect(invoked).toBe(false); 72 | 73 | item.invokeObservable(true); 74 | expect(invoked).toBe(false); 75 | stream.unsubscribe(); 76 | }); 77 | 78 | it('EverythingIsUnsubscribedWhenStreamIsDisposed', () => { 79 | let invoked = false; 80 | 81 | const stream = _source 82 | .connect() 83 | .pipe(mergeManyItems(x => x.observable)) 84 | .subscribe(o => { 85 | invoked = true; 86 | expect(o.item.id == 1).toBe(true); 87 | }); 88 | 89 | const item = new ObjectWithObservable(1); 90 | _source.addOrUpdate(item); 91 | 92 | stream.unsubscribe(); 93 | 94 | item.invokeObservable(true); 95 | expect(invoked).toBe(false); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/cache/MonitorStatusFixture.spec.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionStatus, statusMonitor } from '../../src/cache/operators/statusMonitor'; 2 | import { Subject } from 'rxjs'; 3 | import { filter } from 'rxjs/operators'; 4 | 5 | describe('MonitorStatusFixture', () => { 6 | it('InitialiStatusIsLoadding', () => { 7 | let invoked = false; 8 | let status: ConnectionStatus = 'pending'; 9 | const subscription = new Subject().pipe(statusMonitor()).subscribe(s => { 10 | invoked = true; 11 | status = s; 12 | }); 13 | expect(invoked).toBe(true); 14 | expect(status).toBe('pending'); 15 | subscription.unsubscribe(); 16 | }); 17 | 18 | it('SetToLoaded', () => { 19 | let invoked = false; 20 | let status: ConnectionStatus = 'pending'; 21 | const subject = new Subject(); 22 | const subscription = subject.pipe(statusMonitor()).subscribe(s => { 23 | invoked = true; 24 | status = s; 25 | }); 26 | 27 | subject.next(1); 28 | expect(invoked).toBe(true); 29 | expect(status).toBe('loaded'); 30 | subscription.unsubscribe(); 31 | }); 32 | 33 | it('SetToError', () => { 34 | let invoked = false; 35 | let status: ConnectionStatus = 'pending'; 36 | const subject = new Subject(); 37 | let exception: Error; 38 | 39 | const subscription = subject.pipe(statusMonitor()).subscribe( 40 | s => { 41 | invoked = true; 42 | status = s; 43 | }, 44 | ex => { 45 | exception = ex; 46 | }, 47 | ); 48 | 49 | subject.error(new Error('Test')); 50 | subscription.unsubscribe(); 51 | 52 | expect(invoked).toBe(true); 53 | expect(status).toBe('errored'); 54 | }); 55 | 56 | it('MultipleInvokesDoNotCallLoadedAgain', () => { 57 | let invoked = false; 58 | let invocations = 0; 59 | const subject = new Subject(); 60 | 61 | const subscription = subject 62 | .pipe( 63 | statusMonitor(), 64 | filter(status => status === 'loaded'), 65 | ) 66 | .subscribe(s => { 67 | invoked = true; 68 | invocations++; 69 | }); 70 | 71 | subject.next(1); 72 | subject.next(1); 73 | subject.next(1); 74 | 75 | expect(invoked).toBe(true); 76 | expect(invocations).toBe(1); 77 | subscription.unsubscribe(); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/cache/OnItemFixture.spec.ts: -------------------------------------------------------------------------------- 1 | import { Person } from '../domain/Person'; 2 | import { SourceCache, updateable } from '../../src/cache/SourceCache'; 3 | import { onItemAdded } from '../../src/cache/operators/onItemAdded'; 4 | import { onItemUpdated } from '../../src/cache/operators/onItemUpdated'; 5 | import { onItemRemoved } from '../../src/cache/operators/onItemRemoved'; 6 | 7 | describe('OnItemFixture', function () { 8 | it('OnItemAddCalled', () => { 9 | let called = false; 10 | const source = updateable(new SourceCache(x => x.age)); 11 | 12 | source 13 | .connect() 14 | .pipe(onItemAdded(_ => (called = true))) 15 | .subscribe(); 16 | 17 | const person = new Person('A', 1); 18 | 19 | source.addOrUpdate(person); 20 | expect(called).toBe(true); 21 | }); 22 | 23 | it('OnItemRemovedCalled', () => { 24 | let called = false; 25 | const source = updateable(new SourceCache(x => x.age)); 26 | 27 | source 28 | .connect() 29 | .pipe(onItemRemoved(_ => (called = true))) 30 | .subscribe(); 31 | 32 | const person = new Person('A', 1); 33 | source.addOrUpdate(person); 34 | source.remove(person); 35 | expect(called).toBe(true); 36 | }); 37 | 38 | it('OnItemUpdatedCalled', () => { 39 | let called = false; 40 | const source = updateable(new SourceCache(x => x.age)); 41 | 42 | source 43 | .connect() 44 | .pipe(onItemUpdated((x, y) => (called = true))) 45 | .subscribe(); 46 | 47 | const person = new Person('A', 1); 48 | source.addOrUpdate(person); 49 | const update = new Person('B', 1); 50 | source.addOrUpdate(update); 51 | expect(called).toBe(true); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/cache/Or.spec.ts: -------------------------------------------------------------------------------- 1 | import { ISourceCache } from '../../src/cache/ISourceCache'; 2 | import { ISourceUpdater } from '../../src/cache/ISourceUpdater'; 3 | import { asAggregator, ChangeSetAggregator } from '../util/aggregator'; 4 | import { IChangeSet } from '../../src/cache/IChangeSet'; 5 | import { SourceCache, updateable } from '../../src/cache/SourceCache'; 6 | import { or } from '../../src/cache/operators/or'; 7 | import { first } from 'ix/iterable'; 8 | import { Person } from '../domain/Person'; 9 | 10 | describe('OrFixture', () => { 11 | let _source1: ISourceCache & ISourceUpdater; 12 | let _source2: ISourceCache & ISourceUpdater; 13 | let _results: ChangeSetAggregator; 14 | beforeEach(() => { 15 | _source1 = updateable(new SourceCache(p => p.name)); 16 | _source2 = updateable(new SourceCache(p => p.name)); 17 | _results = asAggregator(or([_source1.connect(), _source2.connect()])); 18 | }); 19 | afterEach(() => { 20 | _source1.dispose(); 21 | _source2.dispose(); 22 | _results.dispose(); 23 | }); 24 | 25 | it('UpdatingOneSourceOnlyProducesResult', () => { 26 | const person = new Person('Adult1', 50); 27 | _source1.addOrUpdate(person); 28 | 29 | expect(_results.messages.length).toBe(1); 30 | expect(_results.data.size).toBe(1); 31 | }); 32 | 33 | it('UpdatingBothProducesResultsAndDoesNotDuplicateTheMessage', () => { 34 | const person = new Person('Adult1', 50); 35 | _source1.addOrUpdate(person); 36 | _source2.addOrUpdate(person); 37 | expect(_results.messages.length).toBe(1); 38 | expect(_results.data.size).toBe(1); 39 | expect(first(_results.data.values())).toBe(person); 40 | }); 41 | 42 | it('RemovingFromOneDoesNotFromResult', () => { 43 | const person = new Person('Adult1', 50); 44 | _source1.addOrUpdate(person); 45 | _source2.addOrUpdate(person); 46 | 47 | _source2.remove(person); 48 | expect(_results.messages.length).toBe(1); 49 | expect(_results.data.size).toBe(1); 50 | }); 51 | 52 | it('UpdatingOneProducesOnlyOneUpdate', () => { 53 | const person = new Person('Adult1', 50); 54 | _source1.addOrUpdate(person); 55 | _source2.addOrUpdate(person); 56 | 57 | const personUpdated = new Person('Adult1', 51); 58 | _source2.addOrUpdate(personUpdated); 59 | expect(_results.messages.length).toBe(2); 60 | expect(_results.data.size).toBe(1); 61 | expect(first(_results.data.values())).toBe(personUpdated); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/cache/QueryWhenChangedFixture.spec.ts: -------------------------------------------------------------------------------- 1 | import { ISourceCache } from '../../src/cache/ISourceCache'; 2 | import { ChangeSetAggregator } from '../util/aggregator'; 3 | import { SourceCache, updateable } from '../../src/cache/SourceCache'; 4 | import { Person } from '../domain/Person'; 5 | import { ISourceUpdater } from '../../src/cache/ISourceUpdater'; 6 | import { queryWhenChanged } from '../../src/cache/operators/queryWhenChanged'; 7 | 8 | describe('QueryWhenChangedFixture', () => { 9 | let _source: ISourceCache & ISourceUpdater; 10 | let _results: ChangeSetAggregator; 11 | 12 | beforeEach(() => { 13 | _source = updateable(new SourceCache(p => p.name)); 14 | _results = new ChangeSetAggregator(_source.connect(p => p.age > 20)); 15 | }); 16 | 17 | afterEach(() => { 18 | _source.dispose(); 19 | _results.dispose(); 20 | }); 21 | 22 | it('ChangeInvokedOnSubscriptionIfItHasData', () => { 23 | let invoked = false; 24 | _source.addOrUpdate(new Person('A', 1)); 25 | const subscription = _source 26 | .connect() 27 | .pipe(queryWhenChanged()) 28 | .subscribe(x => (invoked = true)); 29 | expect(invoked).toBe(true); 30 | subscription.unsubscribe(); 31 | }); 32 | 33 | it('ChangeInvokedOnNext', () => { 34 | let invoked = false; 35 | 36 | const subscription = _source 37 | .connect() 38 | .pipe(queryWhenChanged()) 39 | .subscribe(x => (invoked = true)); 40 | 41 | expect(invoked).toBe(false); 42 | 43 | _source.addOrUpdate(new Person('A', 1)); 44 | expect(invoked).toBe(true); 45 | 46 | subscription.unsubscribe(); 47 | }); 48 | 49 | it('ChangeInvokedOnSubscriptionIfItHasData_WithSelector', () => { 50 | let invoked = false; 51 | _source.addOrUpdate(new Person('A', 1)); 52 | const subscription = _source 53 | .connect() 54 | .pipe(queryWhenChanged()) 55 | .subscribe(x => (invoked = true)); 56 | expect(invoked).toBe(true); 57 | subscription.unsubscribe(); 58 | }); 59 | 60 | it('ChangeInvokedOnNext_WithSelector', () => { 61 | let invoked = false; 62 | 63 | const subscription = _source 64 | .connect() 65 | .pipe(queryWhenChanged()) 66 | .subscribe(x => (invoked = true)); 67 | 68 | expect(invoked).toBe(false); 69 | 70 | _source.addOrUpdate(new Person('A', 1)); 71 | expect(invoked).toBe(true); 72 | 73 | subscription.unsubscribe(); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/cache/RefCountFixture.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Linq; 2 | using DynamicData.Tests.Domain; 3 | using Xunit; 4 | using System; 5 | using System.Threading.Tasks; 6 | using System.Linq; 7 | using FluentAssertions; 8 | 9 | namespace DynamicData.Tests.Cache 10 | { 11 | 12 | public class RefCountFixture: IDisposable 13 | { 14 | let _source: ISourceCache & ISourceUpdater; 15 | 16 | public RefCountFixture() 17 | { 18 | _source = updateable(new SourceCache((p => p.Key))); 19 | } 20 | 21 | afterEach(() => { 22 | _source.Dispose(); 23 | }); 24 | 25 | it('ChainIsInvokedOnceForMultipleSubscribers', () => { 26 | int created = 0; 27 | int disposals = 0; 28 | 29 | //Some expensive transform (or chain of operations) 30 | var longChain = _source.Connect() 31 | .Transform(p => p) 32 | .Do(_ => created++) 33 | .Finally(() => disposals++) 34 | .RefCount(); 35 | 36 | var suscriber1 = longChain.Subscribe(); 37 | var suscriber2 = longChain.Subscribe(); 38 | var suscriber3 = longChain.Subscribe(); 39 | 40 | _source.AddOrUpdate(new Person("Name", 10)); 41 | suscriber1.Dispose(); 42 | suscriber2.Dispose(); 43 | suscriber3.Dispose(); 44 | 45 | expect(created).toBe(1); 46 | expect(disposals).toBe(1); 47 | }); it('CanResubscribe', () => { 48 | int created = 0; 49 | int disposals = 0; 50 | 51 | //must have data so transform is invoked 52 | _source.AddOrUpdate(new Person("Name", 10)); 53 | 54 | //Some expensive transform (or chain of operations) 55 | var longChain = _source.Connect() 56 | .Transform(p => p) 57 | .Do(_ => created++) 58 | .Finally(() => disposals++) 59 | .RefCount(); 60 | 61 | var subscriber = longChain.Subscribe(); 62 | subscriber.Dispose(); 63 | 64 | subscriber = longChain.Subscribe(); 65 | subscriber.Dispose(); 66 | 67 | expect(created).toBe(2); 68 | expect(disposals).toBe(2); 69 | }); // This test is probabilistic, it could be cool to be able to prove RefCount's thread-safety 70 | // more accurately but I don't think that there is an easy way to do this. 71 | // At least this test can catch some bugs in the old implementation. 72 | // [Fact] 73 | private async Task IsHopefullyThreadSafe() 74 | { 75 | var refCount = _source.Connect().RefCount(); 76 | 77 | await Task.WhenAll(Enumerable.Range(0, 100).Select(_ => 78 | Task.Run(() => 79 | { 80 | for (int i = 0; i < 1000; ++i) 81 | { 82 | var subscription = refCount.Subscribe(); 83 | subscription.Dispose(); 84 | } 85 | }))); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/cache/SubscribeManyFixture.spec.ts: -------------------------------------------------------------------------------- 1 | import { Disposable } from '../../src/util'; 2 | import { SourceCache, updateable } from '../../src/cache/SourceCache'; 3 | import { ISourceUpdater } from '../../src/cache/ISourceUpdater'; 4 | import { ChangeSetAggregator } from '../util/aggregator'; 5 | import { ISourceCache } from '../../src/cache/ISourceCache'; 6 | import { subscribeMany } from '../../src/cache/operators/subscribeMany'; 7 | import { every, first, range } from 'ix/iterable'; 8 | import { map } from 'ix/iterable/operators'; 9 | import construct = Reflect.construct; 10 | 11 | describe('SubscribeManyFixture', function () { 12 | let _source: ISourceCache & ISourceUpdater; 13 | let _results: ChangeSetAggregator; 14 | 15 | beforeEach(() => { 16 | _source = updateable(new SourceCache(p => p.id)); 17 | _results = new ChangeSetAggregator( 18 | _source.connect().pipe( 19 | subscribeMany(subscribeable => { 20 | subscribeable.subscribe(); 21 | return Disposable.create(subscribeable); 22 | }), 23 | ), 24 | ); 25 | }); 26 | 27 | afterEach(() => { 28 | _source.dispose(); 29 | _results.dispose(); 30 | }); 31 | 32 | it('AddedItemWillbeSubscribed', () => { 33 | _source.addOrUpdate(new SubscribeableObject(1)); 34 | 35 | expect(_results.messages.length).toBe(1); 36 | expect(_results.data.size).toBe(1); 37 | expect(first(_results.data.values())!.isSubscribed).toBe(true); 38 | }); 39 | 40 | it('RemoveIsUnsubscribed', () => { 41 | _source.addOrUpdate(new SubscribeableObject(1)); 42 | _source.removeKey(1); 43 | 44 | expect(_results.messages.length).toBe(2); 45 | expect(_results.data.size).toBe(0); 46 | expect(first(_results.messages[1])!.current.isSubscribed).toBe(false); 47 | }); 48 | 49 | it('UpdateUnsubscribesPrevious', () => { 50 | _source.addOrUpdate(new SubscribeableObject(1)); 51 | _source.addOrUpdate(new SubscribeableObject(1)); 52 | 53 | expect(_results.messages.length).toBe(2); 54 | expect(_results.data.size).toBe(1); 55 | expect(first(_results.messages[1])!.current.isSubscribed).toBe(true); 56 | expect(first(_results.messages[1])!.previous!.isSubscribed).toBe(false); 57 | }); 58 | 59 | it('EverythingIsUnsubscribedWhenStreamIsDisposed', () => { 60 | _source.addOrUpdateValues(range(1, 10).pipe(map(i => new SubscribeableObject(i)))); 61 | _source.clear(); 62 | 63 | expect(_results.messages.length).toBe(2); 64 | expect(every(_results.messages[1], { predicate: d => !d.current.isSubscribed })).toBe(true); 65 | }); 66 | 67 | class SubscribeableObject { 68 | public isSubscribed: boolean = false; 69 | public readonly id: number; 70 | 71 | public subscribe() { 72 | this.isSubscribed = true; 73 | } 74 | 75 | public unsubscribe() { 76 | this.isSubscribed = false; 77 | } 78 | 79 | public constructor(id: number) { 80 | this.id = id; 81 | } 82 | } 83 | }); 84 | -------------------------------------------------------------------------------- /test/cache/SwitchFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reactive.Subjects; 4 | using DynamicData.Tests.Domain; 5 | using FluentAssertions; 6 | using Xunit; 7 | 8 | namespace DynamicData.Tests.Cache 9 | { 10 | 11 | public class SwitchFixture: IDisposable 12 | { 13 | let _switchable: ISubject>; 14 | let _source: ISourceCache & ISourceUpdater; 15 | let _results: ChangeSetAggregator; 16 | 17 | public SwitchFixture() 18 | { 19 | _source = updateable(new SourceCache((p => p.Name))); 20 | _switchable = new BehaviorSubject>(_source); 21 | _results = _switchable.Switch().AsAggregator(); 22 | } 23 | 24 | afterEach(() => { 25 | _source.Dispose(); 26 | _results.Dispose(); 27 | }); 28 | 29 | it('PoulatesFirstSource', () => { 30 | var inital = Enumerable.Range(1, 100).Select(i => new Person("Person" + i, i)).ToArray(); 31 | _source.AddOrUpdate(inital); 32 | 33 | expect(_results.data.size).toBe(100); 34 | }); it('ClearsForNewSource', () => { 35 | var inital = Enumerable.Range(1, 100).Select(i => new Person("Person" + i, i)).ToArray(); 36 | _source.AddOrUpdate(inital); 37 | 38 | expect(_results.data.size).toBe(100); 39 | 40 | var newSource = new SourceCache(p => p.Name); 41 | _switchable.OnNext(newSource); 42 | 43 | expect(_results.data.size).toBe(0); 44 | 45 | newSource.AddOrUpdate(inital); 46 | expect(_results.data.size).toBe(100); 47 | 48 | var nextUpdates = Enumerable.Range(101, 100).Select(i => new Person("Person" + i, i)).ToArray(); 49 | newSource.AddOrUpdate(nextUpdates); 50 | expect(_results.data.size).toBe(200); 51 | 52 | }); } 53 | } 54 | -------------------------------------------------------------------------------- /test/cache/ToObservableChangeSetFixtureWithCompletion.spec.ts: -------------------------------------------------------------------------------- 1 | import { Subject, Subscription } from 'rxjs'; 2 | import { Person } from '../domain/Person'; 3 | import { IDisposable } from '../../src/util'; 4 | import { toObservableChangeSet } from '../../src/cache/operators/toObservableChangeSet'; 5 | import { clone } from '../../src/cache/operators/clone'; 6 | import { map } from 'rxjs/operators'; 7 | 8 | describe('ToObservableChangeSetFixtureWithCompletion', () => { 9 | let _observable: Subject; 10 | let _disposable: Subscription; 11 | let _target: Person[]; 12 | let _hasCompleted = false; 13 | 14 | beforeEach(() => { 15 | _observable = new Subject(); 16 | 17 | _target = []; 18 | 19 | _disposable = toObservableChangeSet(_observable.pipe(map(z => [z])), p => p.key) 20 | .pipe(clone(_target)) 21 | .subscribe({ 22 | complete() { 23 | _hasCompleted = true; 24 | }, 25 | }); 26 | }); 27 | 28 | afterEach(() => { 29 | _disposable.unsubscribe(); 30 | }); 31 | 32 | it('ShouldReceiveUpdatesThenComplete', () => { 33 | _observable.next(new Person('One', 1)); 34 | _observable.next(new Person('Two', 2)); 35 | 36 | expect(_target.length).toBe(2); 37 | 38 | _observable.complete(); 39 | expect(_hasCompleted).toBe(true); 40 | 41 | _observable.next(new Person('Three', 3)); 42 | expect(_target.length).toBe(2); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/cache/TransformManyFixture.spec.ts: -------------------------------------------------------------------------------- 1 | import { ISourceCache } from '../../src/cache/ISourceCache'; 2 | import { ISourceUpdater } from '../../src/cache/ISourceUpdater'; 3 | import { PersonWithRelations } from '../domain/PersonWithRelations'; 4 | import { asAggregator, ChangeSetAggregator } from '../util/aggregator'; 5 | import { SourceCache, updateable } from '../../src/cache/SourceCache'; 6 | import { transformMany } from '../../src/cache/operators/transformMany'; 7 | import { from } from 'ix/iterable'; 8 | import { expand } from 'ix/iterable/operators'; 9 | import { ignoreUpdateWhen } from '../../src/cache/operators/ignoreUpdateWhen'; 10 | 11 | describe('TransformManyFixture', () => { 12 | let _source: ISourceCache & ISourceUpdater; 13 | let _results: ChangeSetAggregator; 14 | 15 | beforeEach(() => { 16 | _source = updateable(new SourceCache(p => p.key)); 17 | _results = asAggregator( 18 | _source.connect().pipe( 19 | transformMany( 20 | p => from(p.relations).pipe(expand(r => r.relations)), 21 | p => p.name, 22 | ), 23 | ignoreUpdateWhen((current, previous) => current.name == previous.name), 24 | ), 25 | ); 26 | }); 27 | 28 | afterEach(() => { 29 | _source.dispose(); 30 | _results.dispose(); 31 | }); 32 | 33 | it('RecursiveChildrenCanBeAdded', () => { 34 | const frientofchild1 = new PersonWithRelations('Friend1', 10); 35 | const child1 = new PersonWithRelations('Child1', 10, [frientofchild1]); 36 | const child2 = new PersonWithRelations('Child2', 8); 37 | const child3 = new PersonWithRelations('Child3', 8); 38 | const mother = new PersonWithRelations('Mother', 35, [child1, child2, child3]); 39 | // const father = new PersonWithRelations("Father", 35, new[] {child1, child2, child3, mother}); 40 | 41 | _source.addOrUpdate(mother); 42 | 43 | expect(_results.data.size).toBe(4); 44 | expect(_results.data.lookup('Child1')).toBeDefined(); 45 | expect(_results.data.lookup('Child2')).toBeDefined(); 46 | expect(_results.data.lookup('Child3')).toBeDefined(); 47 | expect(_results.data.lookup('Friend1')).toBeDefined(); 48 | }); 49 | 50 | it('ChildrenAreRemovedWhenParentIsRemoved', () => { 51 | const frientofchild1 = new PersonWithRelations('Friend1', 10); 52 | const child1 = new PersonWithRelations('Child1', 10, [frientofchild1]); 53 | const child2 = new PersonWithRelations('Child2', 8); 54 | const child3 = new PersonWithRelations('Child3', 8); 55 | const mother = new PersonWithRelations('Mother', 35, [child1, child2, child3]); 56 | // const father = new PersonWithRelations("Father", 35, new[] {child1, child2, child3, mother}); 57 | 58 | _source.addOrUpdate(mother); 59 | _source.remove(mother); 60 | expect(_results.data.size).toBe(0); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/cache/TransformManyRefreshFixture.spec.ts: -------------------------------------------------------------------------------- 1 | import { ISourceUpdater } from '../../src/cache/ISourceUpdater'; 2 | import { PersonWithFriends } from '../domain/PersonWithFriends'; 3 | import { ISourceCache } from '../../src/cache/ISourceCache'; 4 | import { asAggregator, ChangeSetAggregator } from '../util/aggregator'; 5 | import { SourceCache, updateable } from '../../src/cache/SourceCache'; 6 | import { autoRefresh } from '../../src/cache/operators/autoRefresh'; 7 | import { transformMany } from '../../src/cache/operators/transformMany'; 8 | 9 | describe('TransformManyRefreshFixture', () => { 10 | let _source: ISourceCache & ISourceUpdater; 11 | let _results: ChangeSetAggregator; 12 | 13 | beforeEach(() => { 14 | _source = updateable(new SourceCache(p => p.key)); 15 | _results = asAggregator( 16 | _source.connect().pipe( 17 | autoRefresh(), 18 | transformMany( 19 | p => p.friends, 20 | p => p.name, 21 | ), 22 | ), 23 | ); 24 | }); 25 | 26 | afterEach(() => { 27 | _source.dispose(); 28 | _results.dispose(); 29 | }); 30 | 31 | it('AutoRefresh', () => { 32 | const person = new PersonWithFriends('Person', 50); 33 | _source.addOrUpdate(person); 34 | 35 | person.friends = [new PersonWithFriends('Friend1', 40), new PersonWithFriends('Friend2', 45)]; 36 | expect(_results.data.size).toBe(2); 37 | expect(_results.data.lookup('Friend1')).toBeDefined(); 38 | expect(_results.data.lookup('Friend2')).toBeDefined(); 39 | }); 40 | 41 | it('AutoRefreshOnOtherProperty', () => { 42 | const friends = [new PersonWithFriends('Friend1', 40)]; 43 | const person = new PersonWithFriends('Person', 50, friends); 44 | _source.addOrUpdate(person); 45 | 46 | friends.push(new PersonWithFriends('Friend2', 45)); 47 | person.age = 55; 48 | 49 | expect(_results.data.size).toBe(2); 50 | expect(_results.data.lookup('Friend1')).toBeDefined(); 51 | expect(_results.data.lookup('Friend2')).toBeDefined(); 52 | }); 53 | 54 | it('DirectRefresh', () => { 55 | const friends = [new PersonWithFriends('Friend1', 40)]; 56 | const person = new PersonWithFriends('Person', 50, friends); 57 | _source.addOrUpdate(person); 58 | 59 | friends.push(new PersonWithFriends('Friend2', 45)); 60 | _source.refresh(person); 61 | 62 | expect(_results.data.size).toBe(2); 63 | expect(_results.data.lookup('Friend1')).toBeDefined(); 64 | expect(_results.data.lookup('Friend2')).toBeDefined(); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/cache/WatchFixture.spec.ts: -------------------------------------------------------------------------------- 1 | import { IDisposable } from '../../src/util'; 2 | import { ISourceCache } from '../../src/cache/ISourceCache'; 3 | import { SourceCache, updateable } from '../../src/cache/SourceCache'; 4 | import { ISourceUpdater } from '../../src/cache/ISourceUpdater'; 5 | import { ChangeSetAggregator } from '../util/aggregator'; 6 | import { disposeMany } from '../../src/cache/operators/disposeMany'; 7 | import { first, every, range } from 'ix/iterable'; 8 | import { map } from 'ix/iterable/operators'; 9 | 10 | describe('WatchFixture', () => { 11 | class DisposableObject implements IDisposable { 12 | private _isDisposed = false; 13 | public get isDisposed() { 14 | return this._isDisposed; 15 | } 16 | 17 | public constructor(public readonly id: number) {} 18 | 19 | public dispose() { 20 | this._isDisposed = true; 21 | } 22 | } 23 | 24 | let _source: ISourceCache & ISourceUpdater; 25 | let _results: ChangeSetAggregator; 26 | 27 | beforeEach(() => { 28 | _source = updateable(new SourceCache(p => p.id)); 29 | _results = new ChangeSetAggregator(_source.connect().pipe(disposeMany())); 30 | }); 31 | 32 | afterEach(() => { 33 | _source.dispose(); 34 | _results.dispose(); 35 | }); 36 | 37 | it('AddWillNotCallDispose', () => { 38 | _source.addOrUpdate(new DisposableObject(1)); 39 | 40 | expect(_results.messages.length).toBe(1); 41 | expect(_results.data.size).toBe(1); 42 | expect(first(_results.data.values())!.isDisposed).toBe(false); 43 | }); 44 | 45 | it('RemoveWillCallDispose', () => { 46 | _source.addOrUpdate(new DisposableObject(1)); 47 | _source.edit(updater => updater.removeKey(1)); 48 | 49 | expect(_results.messages.length).toBe(2); 50 | expect(_results.data.size).toBe(0); 51 | expect(first(_results.messages[1])!.current.isDisposed).toBe(true); 52 | }); 53 | 54 | it('UpdateWillCallDispose', () => { 55 | _source.addOrUpdate(new DisposableObject(1)); 56 | _source.addOrUpdate(new DisposableObject(1)); 57 | 58 | expect(_results.messages.length).toBe(2); 59 | expect(_results.data.size).toBe(1); 60 | expect(first(_results.messages[1])!.current.isDisposed).toBe(false); 61 | expect(first(_results.messages[1])!.previous!.isDisposed).toBe(true); 62 | }); 63 | 64 | it('EverythingIsDisposedWhenStreamIsDisposed', () => { 65 | _source.addOrUpdateValues(range(1, 10).pipe(map(i => new DisposableObject(i)))); 66 | _source.clear(); 67 | 68 | expect(_results.messages.length).toBe(2); 69 | expect(every(_results.messages[1], { predicate: d => d.current.isDisposed })).toBe(true); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/cache/XorFixture.spec.ts: -------------------------------------------------------------------------------- 1 | import { asAggregator, ChangeSetAggregator } from '../util/aggregator'; 2 | import { ISourceCache } from '../../src/cache/ISourceCache'; 3 | import { SourceCache, updateable } from '../../src/cache/SourceCache'; 4 | import { or } from '../../src/cache/operators/or'; 5 | import { ISourceUpdater } from '../../src/cache/ISourceUpdater'; 6 | import { xor } from '../../src/cache/operators/xor'; 7 | import { Person } from '../domain/Person'; 8 | 9 | describe('XOrFixture', () => { 10 | let _source1: ISourceCache & ISourceUpdater; 11 | let _source2: ISourceCache & ISourceUpdater; 12 | let _results: ChangeSetAggregator; 13 | 14 | beforeEach(() => { 15 | _source1 = updateable(new SourceCache(p => p.name)); 16 | _source2 = updateable(new SourceCache(p => p.name)); 17 | _results = asAggregator(xor([_source1.connect(), _source2.connect()])); 18 | }); 19 | 20 | afterEach(() => { 21 | _source1.dispose(); 22 | _source2.dispose(); 23 | _results.dispose(); 24 | }); 25 | 26 | it('UpdatingOneSourceOnlyProducesResult', () => { 27 | const person = new Person('Adult1', 50); 28 | _source1.addOrUpdate(person); 29 | 30 | expect(_results.messages.length).toBe(1); 31 | expect(_results.data.size).toBe(1); 32 | }); 33 | it('UpdatingBothDoeNotProducesResult', () => { 34 | const person = new Person('Adult1', 50); 35 | _source1.addOrUpdate(person); 36 | _source2.addOrUpdate(person); 37 | expect(_results.data.size).toBe(0); 38 | }); 39 | it('RemovingFromOneDoesNotFromResult', () => { 40 | const person = new Person('Adult1', 50); 41 | _source1.addOrUpdate(person); 42 | _source2.addOrUpdate(person); 43 | 44 | _source2.remove(person); 45 | expect(_results.messages.length).toBe(3); 46 | expect(_results.data.size).toBe(1); 47 | }); 48 | it('UpdatingOneProducesOnlyOneUpdate', () => { 49 | const person = new Person('Adult1', 50); 50 | _source1.addOrUpdate(person); 51 | _source2.addOrUpdate(person); 52 | 53 | const personUpdated = new Person('Adult1', 51); 54 | _source2.addOrUpdate(personUpdated); 55 | expect(_results.messages.length).toBe(2); 56 | expect(_results.data.size).toBe(0); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/cache/regex.txt: -------------------------------------------------------------------------------- 1 | usings to dispose (with blocks) 2 | using \((.*)\)\s+{([\s|\S]+?)\} 3 | $1//dispose\n$2 // dispose $1 4 | 5 | usings to dispose (without blocks) 6 | using \((.*)\) 7 | $1//dispose $1 8 | 9 | facts to it 10 | \[Fact\]\s+public void (\w+)\(\)\s+\{([\s|\S]+?)\}\s 11 | it\('$1', \(\) => {$2}\); 12 | 13 | fluent assertions to jest 14 | (\w.*?)\.Should\(\).Be\((.*?)(,.*?)?\); 15 | expect\($1\).toBe\($2\); -------------------------------------------------------------------------------- /test/cache/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": false, 4 | "target": "ES6", 5 | "module": "ES2015", 6 | "lib": [ 7 | "ESNext" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "sourceMap": true, 12 | "moduleResolution": "node", 13 | "allowSyntheticDefaultImports": true, 14 | "esModuleInterop": true, 15 | "experimentalDecorators": true, 16 | "emitDecoratorMetadata": true 17 | } 18 | } -------------------------------------------------------------------------------- /test/domain/Animal.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { NotifyChanged, NotifyPropertyChanged } from '../../src/binding/NotifyPropertyChanged'; 3 | import { notifyPropertyChangedSymbol } from '../../src/notify/notifyPropertyChangedSymbol'; 4 | 5 | export enum AnimalFamily { 6 | Mammal, 7 | Reptile, 8 | Fish, 9 | Amphibian, 10 | Bird, 11 | } 12 | 13 | @NotifyPropertyChanged 14 | export class Animal { 15 | @NotifyChanged() 16 | public includeInResults: boolean; 17 | 18 | public constructor(public readonly name: string, public readonly type: string, public readonly family: AnimalFamily) { 19 | this.includeInResults = false; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/domain/ParentAndChildren.ts: -------------------------------------------------------------------------------- 1 | import { Person } from './Person'; 2 | 3 | export class ParentAndChildren { 4 | public readonly parent: Person | undefined; 5 | public readonly parentId: string | undefined; 6 | public readonly children: Person[]; 7 | 8 | public get count() { 9 | return this.children.length; 10 | } 11 | public constructor(parentId: string, parent: Person | undefined, children: Person[]); 12 | public constructor(parent: Person, children: Person[]); 13 | public constructor(parent: Person | string, parentOrChildren: Person[] | Person | undefined, children?: Person[]) { 14 | if (typeof parent === 'string') { 15 | this.parent = children as any; 16 | this.parentId = parent; 17 | this.children = children ?? []; 18 | } else { 19 | this.parent = parent; 20 | this.children = parentOrChildren as any; 21 | } 22 | } 23 | 24 | public toString() { 25 | return `Parent: ${this.parent}, (${this.count} children)`; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/domain/ParentChild.ts: -------------------------------------------------------------------------------- 1 | import { Person } from './Person'; 2 | 3 | export class ParentChild { 4 | public child: Person; 5 | public parent: Person; 6 | 7 | public constructor(child: Person, parent: Person) { 8 | this.child = child; 9 | this.parent = parent; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/domain/Person.ts: -------------------------------------------------------------------------------- 1 | import { NotifyChanged, NotifyPropertyChanged } from '../../src/binding/NotifyPropertyChanged'; 2 | import { notifyPropertyChangedSymbol } from '../../src/notify/notifyPropertyChangedSymbol'; 3 | 4 | @NotifyPropertyChanged 5 | export class Person { 6 | public readonly parentName: string; 7 | public readonly name: string; 8 | public readonly gender: string; 9 | 10 | public get key() { 11 | return this.name; 12 | } 13 | 14 | public constructor(name: string, age: number, gender: string = 'F', parentName?: string) { 15 | this.name = name; 16 | this.age = age; 17 | this.gender = gender; 18 | this.parentName = parentName ?? ''; 19 | } 20 | 21 | @NotifyChanged() 22 | public age: number; 23 | 24 | public static AgeEqualityComparer(x: Person, y: Person) { 25 | if (x === y) return true; 26 | if (x == null) return false; 27 | if (y == null) return false; 28 | return x.age == y.age; 29 | } 30 | 31 | public static NameAgeGenderEqualityComparer(x: Person, y: Person) { 32 | if (x === y) return true; 33 | if (x == null) return false; 34 | if (y == null) return false; 35 | return x.name === y.name && x.age === y.age && x.gender === y.gender; 36 | } 37 | 38 | public toString() { 39 | return `${this.name}. ${this.age}`; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/domain/PersonEmployment.ts: -------------------------------------------------------------------------------- 1 | export class PersonEmpKey { 2 | private readonly _name: string; 3 | private readonly _company: string; 4 | 5 | public constructor(name: string, company: string) { 6 | this._name = name; 7 | this._company = company; 8 | } 9 | 10 | public static create(personEmployment: PersonEmployment) { 11 | return new PersonEmpKey(personEmployment.name, personEmployment.company); 12 | } 13 | } 14 | 15 | export class PersonEmployment { 16 | public readonly name: string; 17 | public readonly company: string; 18 | public readonly key: PersonEmpKey; 19 | 20 | public constructor(name: string, company: string) { 21 | this.name = name; 22 | this.company = company; 23 | this.key = PersonEmpKey.create(this); 24 | } 25 | 26 | public toString() { 27 | return `Name: ${this.name}, Company: ${this.company}`; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/domain/PersonObs.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from 'rxjs'; 2 | 3 | export class PersonObs { 4 | private readonly _age: BehaviorSubject; 5 | public readonly parentName: string; 6 | public readonly name: string; 7 | public readonly gender: string; 8 | 9 | public get key() { 10 | return this.name; 11 | } 12 | 13 | public constructor(name: string, age: number, gender: string = 'F', parentName?: string) { 14 | this.name = name; 15 | this._age = new BehaviorSubject(age); 16 | this.gender = gender; 17 | this.parentName = parentName ?? ''; 18 | } 19 | 20 | public age() { 21 | return this._age.asObservable(); 22 | } 23 | 24 | public setAge(age: number) { 25 | this._age.next(age); 26 | } 27 | 28 | public static AgeEqualityComparer(x: PersonObs, y: PersonObs) { 29 | if (x === y) return true; 30 | if (x == null) return false; 31 | if (y == null) return false; 32 | return x.age == y.age; 33 | } 34 | 35 | public static NameAgeGenderEqualityComparer(x: PersonObs, y: PersonObs) { 36 | if (x === y) return true; 37 | if (x == null) return false; 38 | if (y == null) return false; 39 | return x.name === y.name && x._age.value === y._age.value && x.gender === y.gender; 40 | } 41 | 42 | public toString() { 43 | return `${this.name}. ${this._age.value}`; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/domain/PersonWithChildren.ts: -------------------------------------------------------------------------------- 1 | import { Person } from './Person'; 2 | 3 | export class PersonWithChildren { 4 | public constructor(name: string, age: number, relations: Person[] = []) { 5 | this.name = name; 6 | this.age = age; 7 | this.keyValue = name; 8 | this.relations = relations; 9 | this.key = name; 10 | } 11 | 12 | public readonly keyValue: string; 13 | public readonly key: string; 14 | public readonly name: string; 15 | public readonly age: number; 16 | public readonly relations: Person[]; 17 | 18 | public toString() { 19 | return `${this.name}. ${this.age}`; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/domain/PersonWithEmployment.ts: -------------------------------------------------------------------------------- 1 | import { IObservableCache } from '../../src/cache/IObservableCache'; 2 | import { Group } from '../../src/cache/IGroupChangeSet'; 3 | import { PersonEmpKey, PersonEmployment } from './PersonEmployment'; 4 | import { IDisposable } from '../../src/util'; 5 | 6 | export class PersonWithEmployment implements IDisposable { 7 | private readonly _source: Group; 8 | 9 | public constructor(source: Group) { 10 | this._source = source; 11 | this.employmentData = source.cache; 12 | } 13 | 14 | public get person() { 15 | return this._source.key; 16 | } 17 | 18 | public readonly employmentData: IObservableCache; 19 | 20 | public get employmentCount() { 21 | return this.employmentData.size; 22 | } 23 | 24 | public dispose() { 25 | this.employmentData.dispose(); 26 | } 27 | 28 | public toString() { 29 | return `Person: ${this.person}. Count ${this.employmentCount}`; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/domain/PersonWithFriends.ts: -------------------------------------------------------------------------------- 1 | import { NotifyChanged, NotifyPropertyChanged } from '../../src/binding/NotifyPropertyChanged'; 2 | import { IKey } from '../../src/cache/IKey'; 3 | 4 | @NotifyPropertyChanged 5 | export class PersonWithFriends implements IKey { 6 | public constructor(name: string, age: number, friends: PersonWithFriends[] = []) { 7 | this.name = name; 8 | this.age = age; 9 | this.friends = friends; 10 | this.key = name; 11 | } 12 | 13 | public readonly name: string; 14 | @NotifyChanged() 15 | public age: number; 16 | 17 | @NotifyChanged() 18 | public friends: PersonWithFriends[]; 19 | 20 | public toString() { 21 | return `${this.name}. ${this.age}`; 22 | } 23 | 24 | public readonly key: string; 25 | } 26 | -------------------------------------------------------------------------------- /test/domain/PersonWithGender.ts: -------------------------------------------------------------------------------- 1 | import { Person } from './Person'; 2 | 3 | export class PersonWithGender { 4 | public static create(person: Person, gender: string) { 5 | return new PersonWithGender(person.name, person.age, gender); 6 | } 7 | 8 | public readonly name: string; 9 | public readonly age: number; 10 | public readonly gender: string; 11 | 12 | public constructor(name: string, age: number, gender: string) { 13 | this.name = name; 14 | this.age = age; 15 | this.gender = gender; 16 | } 17 | 18 | public toString() { 19 | return `${this.name}. ${this.age} (${this.gender})`; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/domain/PersonWithRelations.ts: -------------------------------------------------------------------------------- 1 | import { IKey } from '../../src/cache/IKey'; 2 | import { Pet } from './Pet'; 3 | 4 | export class PersonWithRelations implements IKey { 5 | public constructor(name: string, age: number, relations: PersonWithRelations[] = []) { 6 | this.name = name; 7 | this.age = age; 8 | this.keyValue = name; 9 | this.relations = relations; 10 | this.key = name; 11 | } 12 | 13 | public readonly name: string; 14 | 15 | public readonly age: number; 16 | 17 | public readonly keyValue: string; 18 | 19 | public readonly relations: PersonWithRelations[]; 20 | public pet: Pet[] = []; 21 | 22 | public toString() { 23 | return `${this.name}. ${this.age}`; 24 | } 25 | 26 | public readonly key: string; 27 | } 28 | -------------------------------------------------------------------------------- /test/domain/Pet.ts: -------------------------------------------------------------------------------- 1 | export class Pet { 2 | public name: string | undefined; 3 | public animal: string | undefined; 4 | } 5 | -------------------------------------------------------------------------------- /test/domain/RandomPersonGenerator.ts: -------------------------------------------------------------------------------- 1 | import { range } from 'ix/iterable'; 2 | import { map } from 'ix/iterable/operators'; 3 | import faker from 'faker'; 4 | import { Person } from './Person'; 5 | 6 | export function randomPersonGenerator(number = 10000, seed?: number) { 7 | if (seed !== undefined) { 8 | faker.seed(seed); 9 | } 10 | return range(0, number).pipe( 11 | map(() => { 12 | const gender = faker.random.number(1); 13 | return new Person(faker.name.firstName() + ' ' + faker.name.lastName(), faker.random.number({ min: 1, max: 100 }), gender ? 'F' : 'M'); 14 | }), 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /test/domain/SelfObservingPerson.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | 4 | namespace DynamicData.Tests.Domain 5 | { 6 | public class SelfObservingPerson : IDisposable 7 | { 8 | private bool _completed; 9 | private int _updateCount; 10 | private Person _person; 11 | private readonly IDisposable _cleanUp; 12 | 13 | public SelfObservingPerson(IObservable observable) 14 | { 15 | _cleanUp = observable.Finally(() => _completed = true).Subscribe(p => 16 | { 17 | _person = p; 18 | _updateCount++; 19 | }); 20 | } 21 | 22 | public Person Person { get { return _person; } } 23 | 24 | public int UpdateCount { get { return _updateCount; } } 25 | 26 | public bool Completed { get { return _completed; } } 27 | 28 | #region Overrides of IDisposable 29 | 30 | /// 31 | ///put here the code to dispose all managed and unmanaged resources 32 | /// 33 | public void Dispose() 34 | { 35 | _cleanUp.Dispose(); 36 | } 37 | 38 | #endregion 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/util/aggregator.ts: -------------------------------------------------------------------------------- 1 | import { IObservableCache } from '../../src/cache/IObservableCache'; 2 | import { Disposable, IDisposable } from '../../src/util'; 3 | import { publish } from 'rxjs/operators'; 4 | import { IChangeSet } from '../../src/cache/IChangeSet'; 5 | import { ConnectableObservable, Observable } from 'rxjs'; 6 | import { ChangeSummary } from '../../src/diagnostics/ChangeSummary'; 7 | import { collectUpdateStats } from '../../src/diagnostics/operators/CollectUpdateStats'; 8 | import { asObservableCache } from '../../src/cache/operators/asObservableCache'; 9 | 10 | export class ChangeSetAggregator = IChangeSet> implements IDisposable { 11 | private readonly _disposer: IDisposable; 12 | private _summary: ChangeSummary = ChangeSummary.empty; 13 | private _error?: Error; 14 | 15 | /** 16 | * Initializes a new instance of the class. 17 | * @param source The source. 18 | */ 19 | public constructor(source: Observable) { 20 | const published = publish()(source); 21 | 22 | const error = published.subscribe( 23 | updates => {}, 24 | ex => (this._error = ex), 25 | ); 26 | const results = published.subscribe(updates => this.messages.push(updates)); 27 | this.data = asObservableCache(published); 28 | const summariser = published.pipe(collectUpdateStats()).subscribe(summary => (this._summary = summary)); 29 | 30 | const connected = published.connect(); 31 | this._disposer = Disposable.create(() => { 32 | connected.unsubscribe(); 33 | summariser.unsubscribe(); 34 | results.unsubscribe(); 35 | error.unsubscribe(); 36 | }); 37 | } 38 | 39 | /** 40 | * Gets the data. 41 | */ 42 | public readonly data: IObservableCache; 43 | 44 | /** 45 | * Gets the messages. 46 | */ 47 | public readonly messages: TChangeSet[] = []; 48 | 49 | /** 50 | * Gets the summary. 51 | */ 52 | public get summary() { 53 | return this._summary; 54 | } 55 | 56 | /** 57 | * Gets the error. 58 | */ 59 | public get error() { 60 | return this._error; 61 | } 62 | 63 | /** 64 | * Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 65 | */ 66 | public dispose() { 67 | this._disposer?.dispose(); 68 | } 69 | } 70 | 71 | export function asAggregator = IChangeSet>(source: Observable) { 72 | return new ChangeSetAggregator(source); 73 | } 74 | -------------------------------------------------------------------------------- /test/util/indexed.ts: -------------------------------------------------------------------------------- 1 | import { IKeyValueCollection } from '../../src/cache/IKeyValueCollection'; 2 | import { from, reduce, toMap } from 'ix/iterable'; 3 | import { map } from 'ix/iterable/operators'; 4 | 5 | export function indexed(source: IKeyValueCollection) { 6 | return toMap<{ key: TKey; value: TObject; index: number }, TKey, { key: TKey; value: TObject; index: number }>( 7 | from(source).pipe(map(([key, value], index) => ({ key, value, index }))), 8 | { 9 | keySelector: x => x.key as any, // typing issue 10 | }, 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "." 6 | } 7 | } -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "outDir": "./esm" 6 | } 7 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "ES6", 5 | "module": "ES2020", 6 | "lib": [ 7 | "ESNext", 8 | "DOM" 9 | ], 10 | "rootDir": "./src", 11 | "outDir": "./esm", 12 | "declaration": true, 13 | "strict": true, 14 | "sourceMap": true, 15 | "moduleResolution": "node", 16 | "allowSyntheticDefaultImports": true, 17 | "esModuleInterop": true, 18 | "experimentalDecorators": true, 19 | "emitDecoratorMetadata": true 20 | }, 21 | "typedocOptions": { 22 | "mode": "file", 23 | "out": "docs" 24 | }, 25 | "exclude": [ 26 | "./dist", 27 | "./test", 28 | "./binding/**/*.d.ts", 29 | "./diagnostics/**/*.d.ts", 30 | "./cache/**/*.d.ts", 31 | "./notify/**/*.d.ts", 32 | "./util/**/*.d.ts", 33 | "./esm/**/*.d.ts", 34 | "./*.d.ts" 35 | ] 36 | } -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return { 3 | // tell wallaby to use automatic configuration 4 | autoDetect: true 5 | } 6 | }; --------------------------------------------------------------------------------