diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 000000000..817c42208
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,15 @@
+coverage:
+ range: 70..80
+ round: down
+ precision: 2
+
+comment:
+ layout: diff, files
+
+ignore:
+ - "**/fake"
+ - "**/commonTest"
+ - "**/androidTest"
+ - "**/iOSTest"
+ - "**/jsTest"
+ - "**/jvmTest"
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..c9758e3bd
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,32 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: "[BUG]"
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. Pixel 3]
+ - OS: [e.g. Android 10]
+ - Store Version [e.g. 4.0.0]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..d0c33e9ef
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: "[Feature Request] "
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/proposal.md b/.github/ISSUE_TEMPLATE/proposal.md
new file mode 100644
index 000000000..98227a2df
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/proposal.md
@@ -0,0 +1,39 @@
+---
+name: Proposal
+about: Propose an API change
+title: "[Proposal] "
+---
+
+# Proposal: [Title]
+
+Author(s): [GitHub username]
+
+Last updated: [Date]
+
+## Abstract
+
+[A short summary of the proposal.]
+
+## Background
+
+[An introduction of the necessary background and the problem being solved by the proposed change.]
+
+## Proposal
+
+[A precise statement of the proposed change.]
+
+## Rationale
+
+[A discussion of alternate approaches and the trade offs, advantages, and disadvantages of the specified approach.]
+
+## Compatibility
+
+[A discussion of the change with regard to the current version of Store.]
+
+## Implementation
+
+[A description of the steps in the implementation, who will do them, and when.]
+
+## Open issues
+
+[A discussion of open issues relating to this proposal. This section may be omitted if there are none.]
\ No newline at end of file
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..f66d5b7c5
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,9 @@
+version: 2
+updates:
+ - package-ecosystem: gradle
+ directory: "/"
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 1
+ ignore:
+ update-types: ["version-update:semver-major"]
diff --git a/.github/images/hero-light.svg b/.github/images/hero-light.svg
new file mode 100644
index 000000000..6ba3359c4
--- /dev/null
+++ b/.github/images/hero-light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.github/images/kotlin-foundation.png b/.github/images/kotlin-foundation.png
new file mode 100644
index 000000000..904066f24
Binary files /dev/null and b/.github/images/kotlin-foundation.png differ
diff --git a/.github/images/mobile-native-foundation.png b/.github/images/mobile-native-foundation.png
new file mode 100644
index 000000000..5b87ffb8e
Binary files /dev/null and b/.github/images/mobile-native-foundation.png differ
diff --git a/.github/workflows/add_issue_to_project.yml b/.github/workflows/add_issue_to_project.yml
new file mode 100644
index 000000000..425c590dc
--- /dev/null
+++ b/.github/workflows/add_issue_to_project.yml
@@ -0,0 +1,16 @@
+name: Add Issue To Project
+
+on:
+ issues:
+ types:
+ - opened
+
+jobs:
+ add-issue-to-project:
+ name: Add issue to project
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/add-to-project@main
+ with:
+ project-url: https://github.com/orgs/MobileNativeFoundation/projects/1
+ github-token: ${{ secrets.ADD_ISSUE_TO_PROJECT }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..52e6c3eb3
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,94 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ build-and-test:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ api-level:
+ - 29
+ steps:
+
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ ref: ${{ github.head_ref || github.ref }}
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'zulu'
+ java-version: '11'
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v2
+
+ - name: Grant execute permission for Gradlew
+ run: chmod +x gradlew
+
+ - name: Check Kotlin formatting
+ run: ./gradlew spotlessCheck
+
+ - name: Run Kotlin static analysis
+ run: ./gradlew detekt
+
+ - name: Build and Test with Coverage
+ run: ./gradlew clean build koverXmlReport --stacktrace
+
+ - name: Upload Coverage to Codecov
+ uses: codecov/codecov-action@v4
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: build/reports/kover/coverage.xml
+ flags: unittests
+ name: codecov-umbrella
+ fail_ci_if_error: true
+ verbose: true
+
+ publish:
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main' && github.repository == 'MobileNativeFoundation/Store'
+ runs-on: macos-latest
+ needs: build-and-test
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'zulu'
+ java-version: '11'
+
+ - name: Grant execute permission for Gradlew
+ run: chmod +x gradlew
+
+ - name: Upload Artifacts to Maven Central
+ run: ./gradlew publishAllPublicationsToMavenCentralRepository --no-daemon --no-parallel
+ env:
+ ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }}
+ ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }}
+ ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
+ ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}
+
+ - name: Retrieve Version
+ run: |
+ echo "VERSION_NAME=$(cat gradle.properties | grep -w "VERSION_NAME" | cut -d'=' -f2)" >> $GITHUB_ENV
+
+ - name: Publish Release
+ run: ./gradlew closeAndReleaseRepository --no-daemon --no-parallel
+ if: "!endsWith(env.VERSION_NAME, '-SNAPSHOT')"
+ env:
+ ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }}
+ ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }}
\ No newline at end of file
diff --git a/.github/workflows/create_swift_package.yml b/.github/workflows/create_swift_package.yml
new file mode 100644
index 000000000..dca0c1f99
--- /dev/null
+++ b/.github/workflows/create_swift_package.yml
@@ -0,0 +1,7 @@
+name: Create Swift Package
+
+on:
+ workflow_dispatch:
+jobs:
+ publish:
+ uses: touchlab/KMMBridgeGithubWorkflow/.github/workflows/faktorybuildbranches.yml@v0.6
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 6824847ca..397360e4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,7 +24,6 @@ build/
# Local configuration file (sdk path, etc)
local.properties
-gradle.properties
# Proguard folder generated by Eclipse
proguard/
@@ -41,6 +40,19 @@ captures/
# Intellij
*.iml
.idea/
+.classpath
+.project
+.settings
# Keystore files
*.jks
+
+**/kover/html/
+*.podspec
+.kotlin/
+yarn.lock
+
+# Ignore coverage reports
+**/*/coverage.xml
+**/*/build/kover/
+**/*/build/reports/kover/
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index c6b631d7e..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-language: android
-jdk:
-- oraclejdk8
-android:
- components:
- - tools
- - platform-tools
- - build-tools-26.0.1
- - android-25
- - extra-android-m2repository
- licenses:
- - 'android-sdk-license-.+'
-script:
-- ./gradlew check --stacktrace
-after_success:
- - gradle/deploy_snapshot.sh
-branches:
- except:
- - gh-pages
-notifications:
- email: false
-sudo: false
-cache:
- directories:
- - $HOME/.m2
- - $HOME/.gradle
-env:
- global:
- - secure: dKC1kpEFHhnCqt9+txDKxGcrHdFGUUM46KkKGvQXGDkvXFa2RcDiLCQ0qXfJvmdHTRAxvp7xpQeN1Vr2t+792YvWj90z9gPBDcPc3V+5D7MBXKB1PPPO4Kvq15Z8mRmNy64R7YIm35/MPz6TySi/VUmJ8QuoKlkVd3CsQM6GI77YztKyyA4Vrp078SSOCJLbZfQQ+KE/s3C9mIuTqHca8hBuNs8u9kZy8+znc+86NAnpt3aMEwICPHAUZD1AmPSL2WoOgtkVnHn6ShgmL0+vCZI0xesHHH256QCFY6nWA760hWMcD3VtG6WUGv8rjOIrDBAbl50Gp/6MU61OiSqstJKCVGS1ZjKP2pFLcKQTXvRCN5L2v8Q+kwRD9bT4Q06BxOj5JwG8kWBjy3uwlgSmlmh1iGXmlppt8jscpot5ErkevyBaq1w4zjnazR2NGXF04CeT6f8tfrfIEwPvlPNRLYWrA2ybyQMzlLAxQSNqc08FNef2q+djm+FKqN470nKp1bjI737fdrlAnVzSTEYGgn+2VCu1h/2aFldI6R9we3X3XgJo50+W6bQSU7Xj4LX1FjH4r8iZ1C9sfLQeenpwViFbLFb6uhNLd3OF6siTr6SaYYXRgVzStFtIuGT2LJl8a99cx7gXE3kMwo6Tu3Q1aOw1jVMiO5rxyBCwXzNLrBs=
- - secure: 0MV6JaE+32cPY5qr+nC1UmshP7iwuwL9rbfhY2atJkyl+a31T0O+/u+76b8aNJTU95UZ4+3ZR6tXz/c0W+tEBT2A6JbH1vxHLuAVQPcYEjruTF4QgcMd/6Jm4bjIbaM5M3H6oUNcqNVyTBvpJm1Azc1KjwLWTtrhnnwa6bXTQvB/MHQ5NPMCp+4kolk7iACGP55WfS6thcFjZf+KaHWyN0WPOeSL5eneGIn+sS3ag72KXSxqgZgORJqbVpcOEPTu9Y71MNjcWLKLwZ0USYFNVVVgdY/nfDz61xLKydcQB9f7jvt+QgyncebarVlOQsisV3I/vnzmI2DDTAWz0eOT+4zFJCitQFfFoA+iUh0O8n++H47frEAkJTb01Nrbx0ZPc95iQ+JrKEkw5GgAAuBWJ8zgITFGHi1foBYTPjP8YhEMl3loepSXJmAZ2sdBnBXnwky5Tet8gHOaTATWaSAR15i1KKAjHmQ+pHscN6IQN2uZFDDAbZXjneKCCuUvCIwbO2tebTKRxP5idTgYkmUMN21aPtU3SZyewpBA69+NDwkp5y+1KQbYRVq+DpdR0mKtz4SMp+jLSRxSSl94wFADAaYsoPxr97pVmQQaIK8s5Q9LQBK24JgpE6Ed8fTQIZyf1SszLN98SxCkVFE4q8CS9vskHJ5lQH9/EkexV9L9e9g=
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0069d3cb1..d47cb26e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,237 @@
-Change Log
-==========
+# Changelog
-The change log for Store version 1.x can be found [here](https://github.com/NYTimes/Store/blob/develop/CHANGELOG.md).
+### Thank you to all our wonderful contributors and users
-Version 3.0.0-beta *(2017-07-26)*
-----------------------------
+## [5.0.0] (2023-09-14 )
+### Stable release of Store 5, major additions since Store 4 (no breaking changes)
+* MutableStore
+* Validator
+* Fallback Mechanism
+* KMP support
+* Conflict Resolution for store writes
+* Removal of experimental duration APIs
+* StoreResult.NoNewData
+
+## [5.0.0-beta03] (2023-08-11)
+
+* Fix validator regression https://github.com/MobileNativeFoundation/Store/pull/573
+
+## [5.0.0-beta02] (2023-07-21)
+
+* Fix breaking changes with Source of
+ Truth [#560](https://github.com/MobileNativeFoundation/Store/pull/560)
+
+## [5.0.0-beta01] (2023-05-19)
+
+* Delegate memory cache implementation and provide a hybrid cache with automatic list decomposition
+ as a separate
+ artifact [#548](https://github.com/MobileNativeFoundation/Store/pull/548)
+
+## [5.0.0-alpha06] (2023-05-08)
+
+* Separate MutableStoreBuilder from
+ StoreBuilder [#542](https://github.com/MobileNativeFoundation/Store/commit/e050a15afc21c22ffea10a6a7d5f1b436ee34a6a)
+* Support
+ Rx2 [#531](https://github.com/MobileNativeFoundation/Store/commit/7d73f08cc07294d00b176325af792b51874dfeff)
+* Introduce Fallback
+ Mechanisms [#545](https://github.com/MobileNativeFoundation/Store/commit/d1e46a9d02703c798738bc5fb645344fefb90dd4)
+
+## [5.0.0-alpha05] (2023-03-15)
+
+* Target iOS Simulator
+* Target Linux
+* Make Bookkeeper optional
+
+## [5.0.0-alpha04] (2023-02-24)
+
+* Introduce MutableStore
+* Implement RealMutableStore with Store delegate
+* Extract Store and MutableStore methods to use cases
+
+## [5.0.0-alpha03] (2022-12-18)
+
+This release adds support for Store on iOS, JVM, and JS. Concepts and usage are unchanged from
+Store4. In a future release we will reintroduce support for local and remote writes with conflict
+resolution based on Google's offline first guidance.
+
+* Target Android, iOS, JVM, JS
+* Remove concept of Market
+* Remove support for local and remote writes (temporary)
+
+## [5.0.0-alpha02] (2022-12-04)
+
+* Target iOS and JS
+* Rename packages
+
+## [5.0.0-alpha1] (2022-12-04)
+
+* Introduce Market
+* Support local and remote writes with conflict resolution based on Google's offline-first guidance
+* Target Android and JVM
+
+## [4.0.5] (2021-03-30)
+
+* Update to Kotlin 1.6.10
+ * Store `4.0.4-KT15` is the last version supporting Kotlin 1.5
+ * Store `4.0.1` is the last version supporting Kotlin 1.4
+
+## [4.0.4-KT15] (2021-12-08)
+
+* Bug fixes and documentation updates
+
+## [4.0.3-KT15] (2021-11-18)
+
+* Update to Kotlin 1.5.31 and Coroutines 1.5.2
+* Bug fixes and documentation updates
+
+## [4.0.2-KT15] (2021-05-06)
+
+**Kotlin 1.5 introduced breaking changes in the experimental Duration apis we used**
+**4.0.2-KT15 is a duplicate of 4.0.1 but compiled for kotlin 1.5**
+**Version 4.0.1 is the last version compatible with Kotlin 1.4**
+
+* Fire off kotlin 1.5 compatible snapshot (#273)
+
+## [4.0.1] (2021-05-06)
+
+* Fix issues when upgrading to kotlin 1.5 (Deprecated duration api)
+* Add piggyback to all stores
+
+## [4.0.0] (2020-11-30)
+
+* Update coroutines to 1.4.0, kotlin to 1.4.10 [#242](https://github.com/dropbox/Store/pull/242)
+
+## [4.0.0-beta] (2020-09-21)
+
+**API change**
+
+* Remove need for generics with `Error` type (#220)
+
+**Bug Fixes and Stability Improvements**
+
+* Revert cache implementation to guava, rather than rolling our own (#200)
+* Sample App improvements (#227)
+
+## [4.0.0-alpha07] (2020-08-19)
+
+**New Features**
+
+* Add `StoreResult.NoNewData` to represent when a fetcher didn't return data. (#194)
+* Move `Fetcher`-factories into `Companion` of `Fetcher` interface (#168)
+
+**Bug Fixes and Stability Improvements**
+
+* Fix a leak of non-global coroutine contexts. (#199)
+* Update to Kotlin 1.4.0 and Coroutines 1.3.9 (#195)
+* Update to Coroutines 1.3.5 and remove `@FlowPreview` and `@ExperimentalCoroutinesApi`
+ annotations. (#166)
+
+## [4.0.0-alpha06] (2020-04-29)
+
+**Major API change!** (#123)
+
+This release introduces a major change to `StoreBuilder`'s API. This should be the LAST major API
+change to store before
+we'll move to beta.
+
+* The typealias `Fetcher` was added to standardize the input type for a `StoreBuilder`
+* `SourceOfTruth` in now a top level interface and part of `Store`'s public API
+* `StoreBuilder` can now only be created using a `Fetcher` and optionally a `SourceOfTruth`
+* All the overloads for creating a `StoreBuilder` were moved to `Fetcher` and `SourceOfTruth` as
+ appropriate.
+* Rx artifacts were updated accordingly to match main artifacts.
+
+## [4.0.0-alpha05] (2020-04-03)
+
+**Bug Fixes and Stability Improvements**
+
+* Contain @ExperimentalStdlibApi within relevant scope. (#154)
+* Use AtomicFu to replace Java's AtomicBoolean and ReentrantLock (#147)
+* migrate Multicast to Kotlin Test (#146)
+* Remove Collections.unmodifiableMap (#145)
+* Update AGP version (#143)
+* Remove some unneeded java.util packages (#141)
+
+## [4.0.0-alpha04] (2020-04-03)
+
+**New Features**
+
+* Add `asMap` function to Cache for backward compat (#136)
+* Migrate filesystem library to use kotlin.time APIs (#133)
+* Rx get fresh bindings (#130)
+* Migrate cache library to use kotlin.time APIs (#129)
+* Update sample app (#117)
+
+**Bug Fixes and Stability Improvements**
+
+* Use Kotlin version of ArrayDeque in ChannelManager (#134)
+* Kotlin 1.3.70 and other dependencies updates (#125)
+* Make SharedFlowProducer APIs safe (#121)
+* Ensure network starts after disk is established (#115)
+* Update to Gradle 6.2 (#111)
+
+## [4.0.0-alpha03] (2020-02-13)
+
+**New Features**
+
+* Added Rx bindings, available as store-rx2 artifact (#93)
+* Bug fixes (#90)
+* Add ability to delete all entries in the store (#79)
+
+## [4.0.0-alpha02] (2020-01-29)
+
+**New Features**
+
+* Introduce piggyback only downstreams to multicaster and fix #59 (#75)
+* Change flow collection util to drain the flow (#64)
+* Readme improvements (#70, #72)
+* Avoid illegal cast in RealStore.stream (#69)
+* Added docs to MemoryPolicy.setMemorySize (#67) (#68)
+
+## [4.0.0-alpha01] (2020-01-08)
+
+**New Features**
+
+* Store has been rewritten using Kotlin Coroutines instead of RxJava
+
+## [3.1.0] (2018-06-07)
+
+**New Features**
+
+* (#319) Store can now be used in Java (non-Android) projects
+* (#338) Room integration for Store
+
+**Bug Fixes and Stability Improvements**
+
+* (#315) Add missing reading of expire-after-policy when creating a NoopPersister
+* (#311) Update Kotlin & AGP versions
+* (#328) Fix memory policy default size
+* (#329) Adding docs to README for setting 1.8 compatibility
+* (#273) Adds comments to the sample app
+* (#336) Fixes errors in README
+
+## [3.0.1] (2018-03-20)
+
+**Bug Fixes and Stability Improvements**
+
+* (#311) Update Kotlin & AGP versions
+* (#314) Fix issues occured from RxJava1 dependency
+
+## [3.0.0] (2018-02-01)
+
+**New Features**
+
+* (#275) Add ParsingFetcher that wraps Raw type Parser and Fetcher
+
+**Bug Fixes and Stability Improvements**
+
+* (#267) Kotlin 1.1.4 for store-kotlin
+* (#290) Remove @Experimental from store-kotlin API
+* (#283) Update build tools to 26.0.2
+* (#259, #261, #272, #289, #303) README + documentation updates
+* (#310) Sample app fixes
+
+## [3.0.0-beta] (2017-07-26)
**New Features**
@@ -21,29 +248,93 @@ Version 3.0.0-beta *(2017-07-26)*
* (#246) Update to Moshi 1.5.0
* (#252) Fix stream for a single barcode
-Version 3.0.0-alpha *(2017-05-23)*
-----------------------------
+## [3.0.0-alpha] (2017-05-23)
This is a first alpha release of Store ported to RxJava 2.
**New Features**
* (#155) Port to RxJava 2
-* (#220) Packages have been renamed to store3 to allow use of this artifact alongside the original Store
+* (#220) Packages have been renamed to store3 to allow use of this artifact alongside the original
+ Store
* (#185) Return Single/Maybe where appropriate
* (#189) Add lambdas to Store and Filesystem modules
* (#214) expireAfterAccess added to MemoryPolicy
-* (#214) Deprecate setExpireAfter and getExpireAfter -- use new expireAfterWrite or expireAfterAccess, see #199 for
-MemoryPolicy changes
+* (#214) Deprecate setExpireAfter and getExpireAfter -- use new expireAfterWrite or
+ expireAfterAccess, see #199 for
+ MemoryPolicy changes
* (#214) Add Raw to BufferedSource transformer
-
**Bug Fixes and Stability Improvements**
* (#214) Fix networkBeforeStale on cold start with no connectivity
* (#214) Add a missing source.close() call
-* (#164) FileSystemPersister.persisterIsStale() should return false if record is missing or policy is unspecified
+* (#164) FileSystemPersister.persisterIsStale() should return false if record is missing or policy
+ is unspecified
* (#166) Remove apt dependency and use annotationProcessor instead
* (#214) Standardize store.stream() to emit only new items
* (#214) Fix typos
-* (#214) Close source after write to filesystem
\ No newline at end of file
+* (#214) Close source after write to filesystem
+
+## [1.x]
+
+* The change log for Store version 1.x can be
+ found [here](https://github.com/NYTimes/Store/blob/develop/CHANGELOG.md).
+
+[Unreleased]: https://github.com/MobileNativeFoundation/Store/compare/v5.0.0-beta02...HEAD
+
+[5.0.0-beta02]: https://github.com/MobileNativeFoundation/Store/releases/tag/5.0.0-beta02
+
+[5.0.0-beta01]: https://github.com/MobileNativeFoundation/Store/releases/tag/5.0.0-beta01
+
+[5.0.0-alpha06]: https://github.com/MobileNativeFoundation/Store/releases/tag/5.0.0-alpha06
+
+[5.0.0-alpha05]: https://github.com/MobileNativeFoundation/Store/releases/tag/5.0.0-alpha05
+
+[5.0.0-alpha04]: https://github.com/MobileNativeFoundation/Store/releases/tag/5.0.0-alpha04
+
+[5.0.0-alpha03]: https://github.com/MobileNativeFoundation/Store/releases/tag/5.0.0-alpha03
+
+[5.0.0-alpha02]: https://github.com/MobileNativeFoundation/Store/releases/tag/5.0.0-alpha02
+
+[5.0.0-alpha1]: https://github.com/MobileNativeFoundation/Store/releases/tag/5.0.0-alpha1
+
+[4.0.5]: https://github.com/MobileNativeFoundation/Store/releases/tag/4.0.5
+
+[4.0.4-KT15]: https://github.com/MobileNativeFoundation/Store/releases/tag/4.0.4-KT15
+
+[4.0.3-KT15]: https://github.com/MobileNativeFoundation/Store/releases/tag/4.0.3-KT15
+
+[4.0.2-KT15]: https://github.com/MobileNativeFoundation/Store/releases/tag/4.0.2-KT15
+
+[4.0.1]: https://github.com/MobileNativeFoundation/Store/releases/tag/4.0.1
+
+[4.0.0]: https://github.com/MobileNativeFoundation/Store/releases/tag/4.0.0
+
+[4.0.0-beta]: https://github.com/MobileNativeFoundation/Store/releases/tag/4.0.0-beta
+
+[4.0.0-alpha07]: https://github.com/MobileNativeFoundation/Store/releases/tag/4.0.0-alpha07
+
+[4.0.0-alpha06]: https://github.com/MobileNativeFoundation/Store/releases/tag/4.0.0-alpha06
+
+[4.0.0-alpha05]: https://github.com/MobileNativeFoundation/Store/releases/tag/4.0.0-alpha05
+
+[4.0.0-alpha04]: https://github.com/MobileNativeFoundation/Store/releases/tag/4.0.0-alpha04
+
+[4.0.0-alpha03]: https://github.com/MobileNativeFoundation/Store/releases/tag/4.0.0-alpha03
+
+[4.0.0-alpha02]: https://github.com/MobileNativeFoundation/Store/releases/tag/4.0.0-alpha02
+
+[4.0.0-alpha01]: https://github.com/MobileNativeFoundation/Store/releases/tag/4.0.0-alpha01
+
+[3.1.0]: https://github.com/MobileNativeFoundation/Store/releases/tag/3.1.0
+
+[3.0.1]: https://github.com/MobileNativeFoundation/Store/releases/tag/3.0.1
+
+[3.0.0]: https://github.com/MobileNativeFoundation/Store/releases/tag/3.0.0
+
+[3.0.0-beta]: https://github.com/MobileNativeFoundation/Store/releases/tag/3.0.0-beta
+
+[3.0.0-alpha]: https://github.com/MobileNativeFoundation/Store/releases/tag/3.0.0-alpha
+
+[1.x]: https://github.com/NYTimes/Store/blob/develop/CHANGELOG.md
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 46bfe401e..b0c254b9c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,38 +1,28 @@
-Contributing to Store
-=======================
-
-The New York Times team welcomes contributions of all kinds, from simple bug reports through documentation, test cases,
-bugfixes, and features.
-
-DOs and DON'Ts
---------------
-
-* DO follow our coding style (as described below)
-* DO give priority to the current style of the project or file you're changing even if it diverges from the general guidelines.
-* DO include tests when adding new features. When fixing bugs, start with adding a test that highlights how the current behavior is broken.
-* DO keep the discussions focused. When a new or related topic comes up it's often better to create new issue than to side track the discussion.
-* DO run all Gradle verification tasks (`./gradlew check`) before submitting a pull request
-
-* DO NOT send PRs for style changes.
-* DON'T surprise us with big pull requests. Instead, file an issue and start a discussion so we can agree on a direction before you invest a large amount of time.
-* DON'T commit code that you didn't write. If you find code that you think is a good fit, file an issue and start a discussion before proceeding.
-* DON'T submit PRs that alter licensing related files or headers. If you believe there's a problem with them, file an issue and we'll be happy to discuss it.
-
-
-Coding Style
-------------
-
-The coding style employed here is fairly conventional Java - indentations are four spaces, class
-names are PascalCased, identifiers and methods are camelCased.
-
-Workflow
---------
-
-We love Github issues! Before working on any new features, please open an issue so that we can agree on the
-direction, and hopefully avoid investing a lot of time on a feature that might need reworking.
-
-Small pull requests for things like typos, bugfixes, etc are always welcome.
-
-Please note that we will not accept pull requests for style changes.
-
-
+# Contributing to Store
+Thanks for considering contributing to Store. This document provides guidelines and information about how you can contribute.
+
+## Getting Started
+- **Fork the Repository**: Start by forking the [MobileNativeFoundation/Store](https://github.com/MobileNativeFoundation/Store) repository.
+- **Clone the Fork**: Clone your fork to your machine to start working on the changes.
+
+## Contribution Workflow
+### Reporting Issues
+- **Search Existing Issues**: Before creating a new issue, please do a search in existing issues to see if it has been reported or fixed.
+- **Create a Detailed Issue**: If you find a bug or have a feature request, please create an issue with a clear title and a detailed description.
+### Submitting Changes
+- **Create a Branch**: Create a branch in your fork for your contribution.
+- **Make Your Changes**: Make your changes and commit them to your branch. Make sure to write clear, concise commit messages.
+- **Write Tests**: If you are adding new features or fixing bugs, write tests that cover your changes.
+- **Run the Tests**: Run the project's existing tests to ensure nothing is broken.
+- **Create a Pull Request**: Submit a PR to the main repository for review. Include a clear description of the changes and any relevant issue numbers.
+### Code Review Process
+- **Wait for Review**: Maintainers will review your PR and might request changes.
+- **Make Requested Changes**: If changes are requested, make them and update your PR.
+- **Merge**: Once your PR is approved, a maintainer will merge it into the main codebase.
+
+## Community Guidelines
+- **Be Respectful**: Treat everyone with respect. We strive to create a welcoming and inclusive environment.
+- **Follow the Code of Conduct**: Familiarize yourself with our [Code of Conduct](https://github.com/MobileNativeFoundation/Store/blob/main/CODE_OF_CONDUCT.md).
+
+## Getting Help
+- **Join the Community**: If you have questions or need help, join our [Slack channel](https://kotlinlang.slack.com/archives/C06007Z01HU).
diff --git a/Images/friendly_robot.png b/Images/friendly_robot.png
new file mode 100644
index 000000000..424a3f78c
Binary files /dev/null and b/Images/friendly_robot.png differ
diff --git a/Images/friendly_robot_icon.png b/Images/friendly_robot_icon.png
new file mode 100755
index 000000000..558096a81
Binary files /dev/null and b/Images/friendly_robot_icon.png differ
diff --git a/Images/store-logo.png b/Images/store-logo.png
deleted file mode 100644
index 226184ada..000000000
Binary files a/Images/store-logo.png and /dev/null differ
diff --git a/LICENSE b/LICENSE
index df6192d36..eef1e28f1 100644
--- a/LICENSE
+++ b/LICENSE
@@ -188,6 +188,8 @@
Copyright 2016-2017 The New York Times Company
+ Copyright (c) 2019 Dropbox, Inc.
+
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
diff --git a/README.md b/README.md
index d2e87e495..f67ae8b45 100644
--- a/README.md
+++ b/README.md
@@ -1,335 +1,38 @@
-[](https://travis-ci.org/NYTimes/Store)
+
-
+# Store5
-Store is an Android library for effortless, reactive data loading.
+[](https://codecov.io/gh/MobileNativeFoundation/Store)
-### The Problems:
+#### Documentation
-+ Modern Android Apps need their data representations to be fluid and always available.
-+ Users expect their UI experience to never be compromised (blocked) by new data loads. Whether an application is social, news, or business-to-business, users expect a seamless experience both online and offline.
-+ International users expect minimal data downloads as many megabytes of downloaded data can quickly result in astronomical phone bills.
+Comprehensive guides, tutorials, and API reference: [store.mobilenativefoundation.org](https://store.mobilenativefoundation.org).
-A Store is a class that simplifies fetching, parsing, storage, and retrieval of data in your application. A Store is similar to the Repository pattern [[https://msdn.microsoft.com/en-us/library/ff649690.aspx](https://msdn.microsoft.com/en-us/library/ff649690.aspx)] while exposing a Reactive API built with RxJava that adheres to a unidirectional data flow.
+#### Getting Started
-Store provides a level of abstraction between UI elements and data operations.
+1. Start with the [Quickstart](https://store.mobilenativefoundation.org/docs/quickstart) to build your first Store.
+2. Dive into [Store Foundations](https://store.mobilenativefoundation.org/docs/concepts) to learn how Store works.
+3. Check out [Handling CRUD](https://store.mobilenativefoundation.org/docs/use-cases/store5/setting-up-store-for-crud-operations) for an advanced guide on supporting create, read, update, and delete operations.
-### Overview
+#### Getting Help
-A Store is responsible for managing a particular data request. When you create an implementation of a Store, you provide it with a `Fetcher`, a function that defines how data will be fetched over network. You can also define how your Store will cache data in-memory and on-disk, as well as how to parse it. Since Store returns your data as an Observable, threading is a breeze! Once a Store is built, it handles the logic around data flow, allowing your views to use the best data source and ensuring that the newest data is always available for later offline use. Stores can be customized to work with your own implementations or use our included middleware.
+Join our community in the [#store](https://kotlinlang.slack.com/archives/C06007Z01HU) channel on the official Kotlin Slack.
-Store leverages RxJava and multiple request throttling to prevent excessive calls to the network and disk cache. By utilizing Store, you eliminate the possibility of flooding your network with the same request while adding two layers of caching (memory and disk).
-
-### Fully Configured Store
-Let's start by looking at what a fully configured Store looks like. We will then walk through simpler examples showing each piece:
-```java
-Store articleStore = StoreBuilder.parsedWithKey()
- .fetcher(articleId -> api.getArticleAsBufferedSource(articleId)) //OkHttp responseBody.source()
- .persister(FileSystemPersister.create(FileSystemFactory.create(context.getFilesDir()),pathResolver))
- .parser(GsonParserFactory.createSourceParser(gson, ArticleAsset.Article.class))
- .open();
-
-```
-
-With the above setup you have:
-+ In Memory Caching for rotation
-+ Disk caching for when users are offline
-+ Parsing through streaming API to limit memory consumption
-+ Rich API to ask for data whether you want cached/new or a stream of future data updates.
-
-And now for the details:
-
-### Creating a Store
-
-You create a Store using a builder. The only requirement is to include a `.Fetcher` that returns an Single and has a single method `fetch(key)`
-
-
-``` java
- Store store = StoreBuilder.<>key()
- .fetcher(articleId -> api.getArticle(articleId)) //OkHttp responseBody.source()
- .open();
-```
-Stores use generic keys as identifiers for data. A key can be any value object that properly implements toString and equals and hashCode. When your Fetcher function is called, it will be passed a particular Key value. Similarly, the key will be used as a primary identifier within caches (Make sure to have a proper hashCode!!)
-
-### Our Key implementation - Barcodes
-For convenience we included our own key implementation called a BarCode. Barcode has two fields `String key and String type`
-``` java
-BarCode barcode = new BarCode("Article", "42");
-```
-When using a Barcode as your key, you can use a StoreBuilder convenience method
-``` java
- Store store = StoreBuilder.barcode()
- .fetcher(articleBarcode -> api.getAsset(articleBarcode.getKey(),articleBarcode.getType()))
- .open();
-```
-
-
-
-### Public Interface - Get, Fetch, Stream, GetRefreshing
-
-```java
-Single article = store.get(barCode);
-```
-
-The first time you subscribe to `store.get(barCode)`, the response will be stored in an in-memory cache. All subsequent calls to `store.get(barCode)` with the same Key will retrieve the cached version of the data, minimizing unnecessary data calls. This prevents your app from fetching fresh data over the network (or from another external data source) in situations when doing so would unnecessarily waste bandwidth and battery. A great use case is any time your views are recreated after a rotation, they will be able to request the cached data from your Store. Having this data available can help you avoid the need to retain this in the view layer.
-
-
-So far our Store’s data flow looks like this:
-
-
-
-By default, 100 items will be cached in memory for 24 hours. You may pass in your own instance of a Guava Cache to override the default policy.
-
-
-### Busting through the cache
-
-Alternatively you can call `store.fetch(barCode)` to get an Observable that skips the memory (and optional disk cache).
+#### Getting Involved
+Store has a vibrant community of contributors. We welcome contributions of all kinds. Please see our [Contributing Guidelines](CONTRIBUTING.md) for more information on how to get involved.
-Fresh data call will look like: `store.fetch()`
-
+#### Backed By
+
+
+
+
-In the New York Times app, overnight background updates use `fetch` to make sure that calls to `store.get()` will not have to hit the network during normal usage. Another good use case for `fetch` is when a user wants to pull to refresh.
+#### License
-
-Calls to both `fetch()` and `get()` emit one value and then call `onCompleted()` or throw an error.
-
-
-### Stream
-For real-time updates, you may also call `store.stream()` which returns an Observable that emits each time a new item is added to the Store. You can think of stream as an Event Bus-like feature that allows you to know when any new network hits happen for a particular Store. You can leverage the Rx operator `filter()` to only subscribe to a subset of emissions.
-
-### Get Refreshing
-There is another special way to subscribe to a Store: getRefreshing(key). Get Refreshing will subscribe to get() which returns a single response, but unlike Get, Get Refreshing will stay subscribed. Anytime you call store.clear(key) anyone subscribed to getRefreshing(key) will resubscribe and force a new network response.
-
-
-### Inflight Debouncer
-
-To prevent duplicative requests for the same data, Store offers an inflight debouncer. If the same request is made within a minute of a previous identical request, the same response will be returned. This is useful for situations when your app needs to make many async calls for the same data at startup or for when users are obsessively pulling to refresh. As an example, The New York Times news app asynchronously calls `ConfigStore.get()` from 12 different places on startup. The first call blocks while all others wait for the data to arrive. We have seen a dramatic decrease in the app's the data usage after implementing this in flight logic.
-
-
-### Adding a Parser
-
-Since it is rare for data to arrive from the network in the format that your views need, Stores can delegate to a parser by using a `StoreBuilder.parsedWithKey()
-
-```java
-Store store = StoreBuilder.parsedWithKey()
- .fetcher(articleId -> api.getArticle(articleId))
- .parser(source -> {
- try (InputStreamReader reader = new InputStreamReader(source.inputStream())) {
- return gson.fromJson(reader, Article.class);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- })
- .open();
+```text
+Copyright (c) 2024 Mobile Native Foundation.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
```
-
-Our updated data flow now looks like this:
-
-`store.get()` -> 
-
-
-
-### Middleware - GsonSourceParser
-
-There are also separate middleware libraries with parsers to help in cases where your fetcher is a Reader, BufferedSource or String and your parser is Gson:
-- GsonReaderParser
-- GsonSourceParser
-- GsonStringParser
-
-These can be accessed via a Factory class (GsonParserFactory).
-
-Our example can now be rewritten as:
-```java
-Store store = StoreBuilder.parsedWithKey()
- .fetcher(articleId -> api.getArticle(articleId))
- .parser(GsonParserFactory.createSourceParser(gson, Article.class))
- .open();
-```
-
-In some cases you may need to parse a top level JSONArray, in which case you can provide a TypeToken.
-```java
-Store,Integer> store = StoreBuilder.>parsedWithKey()
- .fetcher(articleId -> api.getArticles())
- .parser(GsonParserFactory.createSourceParser(gson, new TypeToken>() {}))
- .open();
-
-
-```
-
-Similarly we have a middleware artifact for Moshi & Jackson too!
-
-
-### Disk Caching
-
-Stores can enable disk caching by passing a Persister into the builder. Whenever a new network request is made, the Store will first write to the disk cache and then read from the disk cache.
-
-
-Now our data flow looks like:
-`store.get()` -> 
-
-
-
- Ideally, data will be streamed from network to disk using either a BufferedSource or Reader as your network raw type (rather than String).
-
-```java
-Store store = StoreBuilder.parsedWithKey()
- .fetcher(articleId -> api.getArticles())
- .persister(new Persister() {
- @Override
- public Maybe read(Integer key) {
- if (dataIsCached) {
- return Observable.fromCallable(() -> userImplementedCache.get(key));
- } else {
- return Observable.empty();
- }
- }
-
- @Override
- public Single write(BarCode barCode, BufferedSource source) {
- userImplementedCache.save(key, source);
- return Single.just(true);
- }
- })
- .parser(GsonParserFactory.createSourceParser(gson, Article.class))
- .open();
-```
-
-Stores don’t care how you’re storing or retrieving your data from disk. As a result, you can use Stores with object storage or any database (Realm, SQLite, CouchDB, Firebase etc). The only requirement is that data must be the same type when stored and retrieved as it was when received from your Fetcher. Technically there is nothing stopping you from implementing an in memory cache for the “persister” implementation and instead have two levels of in memory caching--one with inflated and one with deflated models, allowing for sharing of the “persister” cache data between stores.
-
-
-**Note**: When using a Parser and a disk cache, the Parser will be called AFTER fetching from disk and not between the network and disk. This allows your persister to work on the network stream directly.
-
-
-If using SQLite we recommend working with SqlBrite. If you are not using SqlBrite, an Observable can be created rather simply with `Observable.fromCallable(() -> getDBValue())`
-
-### Middleware - SourcePersister & FileSystem
-
-We've found the fastest form of persistence is streaming network responses directly to disk. As a result, we have included a separate library with a reactive FileSystem which depends on Okio BufferedSources. We have also included a FileSystemPersister which will give you disk caching and works beautifully with GsonSourceParser. When using the FileSystemPersister you must pass in a `PathResolver` which will tell the file system how to name the paths to cache entries.
-
-Now back to our first example:
-
-```java
-Store store = StoreBuilder.parsedWithKey()
- .fetcher(articleId -> api.getArticles(articleId))
- .persister(FileSystemPersister.create(FileSystemFactory.create(context.getFilesDir()),pathResolver))
- .parser(GsonParserFactory.createSourceParser(gson, String.class))
- .open();
-```
-
-As mentioned, the above builder is how we work with network operations at the New York Times. With the above setup you have:
-+ Memory caching with Guava Cache
-+ Disk caching with FileSystem (you can reuse the same file system implementation for all stores)
-+ Parsing from a BufferedSource to a (Article in our case) with Gson
-+ In-flight request management
-+ Ability to get cached data or bust through your caches (get vs. fresh)
-+ Ability to listen for any new emissions from network (stream)
-+ Ability to be notified and resubscribed when caches are cleared (helpful for times when you need to do a post request and update another screen, such as with `getRefreshing`)
-
-We recommend using the above builder setup for most Stores. The SourcePersister implementation has a tiny memory footprint because it will stream bytes from network to disk and then from disk to parser. The streaming nature of Stores allows us to download dozens of 1mb+ json responses without worrying about OOM on low-memory devices. As mentioned above, Stores allow us to do things like calling `configStore.get()` a dozen times asynchronously before our Main Activity finishes loading without blocking the main thread or flooding our network.
-
-### RecordProvider
-If you'd like your Store to know about disk data staleness, you can have your `Persister` implement `RecordProvider`. After doing so you can configure your Store to work in one of two ways:
-
-```java
-store = StoreBuilder.barcode()
- .fetcher(fetcher)
- .persister(persister)
- .refreshOnStale()
- .open();
-
-```
-
-`refreshOnStale` will backfill the disk cache anytime a record is stale. The user will still get the stale record returned to them.
-
-Or alternatively:
-
-```java
- store = StoreBuilder.barcode()
- .fetcher(fetcher)
- .persister(persister)
- .networkBeforeStale()
- .open();
-```
-`networkBeforeStale` - Store will try to get network source when disk data is stale. If the network source throws an error or is empty, stale disk data will be returned.
-
-
-### Subclassing a Store
-
-We can also subclass a Store implementation (RealStore):
-
-```java
-public class SampleStore extends RealStore {
- public SampleStore(Fetcher fetcher, Persister persister) {
- super(fetcher, persister);
- }
-}
-```
-
-Subclassing is useful when you’d like to inject Store dependencies or add a few helper methods to a store:
-
-```java
-public class SampleStore extends RealStore {
- @Inject
- public SampleStore(Fetcher fetcher, Persister persister) {
- super(fetcher, persister);
- }
-}
-```
-
-
-### Artifacts
-
-**CurrentVersion = 3.0.0-beta**
-
-+ **Cache** Cache extracted from Guava (keeps method count to a minimum)
-
- ```groovy
- compile 'com.nytimes.android:cache3:CurrentVersion'
- ```
-+ **Store** This contains only Store classes and has a dependecy on RxJava + the above cache.
-
- ```groovy
- compile 'com.nytimes.android:store3:CurrentVersion'
- ```
-+ **Store-Kotlin** Store plus a couple of added Kotlin classes for more idiomatic usage.
-
- ```groovy
- compile 'com.nytimes.android:store-kotlin3:CurrentVersion'
- ```
-+ **Middleware** Sample Gson parsers, (feel free to create more and open PRs)
-
- ```groovy
- compile 'com.nytimes.android:middleware3:CurrentVersion'
- ```
-+ **Middleware-Jackson** Sample Jackon parsers, (feel free to create more and open PRs)
-
- ```groovy
- compile 'com.nytimes.android:middleware-jackson3:CurrentVersion'
- ```
-+ **Middleware-Moshi** Sample Moshi parsers, (feel free to create more and open PRs)
-
- ```groovy
- compile 'com.nytimes.android:middleware-moshi3:CurrentVersion'
- ```
-+ **File System** Persistence Library built using OKIO Source/Sink + Middleware for streaming from Network to FileSystem
-
- ```groovy
- compile 'com.nytimes.android:filesystem3:CurrentVersion'
- ```
-
-### Sample Project
-
-See the app for example usage of Store. Alternatively, the Wiki contains a set of recipes for common use cases
-+ Simple Example: Retrofit + Store
-+ Complex Example: BufferedSource from Retrofit (Can be OKHTTP too) + our FileSystem + our GsonSourceParser
-
-### Talks
-[DroidCon Italy](https://youtu.be/TvsOsgd0--c)
-[Android Makers](https://www.youtube.com/watch?time_continue=170&v=G1MebI2k9aA)
-
-### Community projects
-
-+ https://github.com/stoyicker/master-slave-clean-store: An offline-first Master-Slave project with scroll-driven pagination using Store for the data layer.
-+ https://github.com/benoberkfell/cat-rates: [Ben Oberkfell's 360AnDev talk, "Android Architecture for the Subway"](https://academy.realm.io/posts/360-andev-2017-ben-oberkfell-android-architecture-offline-first/) illustrates using Store for caching server responses
diff --git a/RELEASING.md b/RELEASING.md
index 736bfe4bf..4d42171d9 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -1,16 +1,28 @@
Releasing
========
- 1. Change the version in top level `build.gradle` to a non-SNAPSHOT verson.
- 2. Update the `CHANGELOG.md` for the impending release.
- 3. Update the `README.md` with the new version.
- 4. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version)
- 5. `./gradlew clean uploadArchives`.
- 6. Visit [Sonatype Nexus](https://oss.sonatype.org/) and promote the artifact.
- 7. `git tag -a X.Y.X -m "Version X.Y.Z"` (where X.Y.Z is the new version)
- 8. Update the top level `build.gradle` to the next SNAPSHOT version.
- 9. `git commit -am "Prepare next development version."`
- 10. `git push && git push --tags`
- 11. Update the sample module to point to the newly released version. (May take 2 hours)
-
-If step 5 or 6 fails, drop the Sonatype repo, fix the problem, commit, and start again at step 5.
+1. Change the version in top level `gradle.properties` to a non-SNAPSHOT version.
+2. Update the `cocoapods` version in `build.gradle.kts` in `:store`.
+3. Modify `create_swift_package.yml` workflow.
+ * https://github.com/MobileNativeFoundation/Store/blob/e526400cdf51aa2f78b6b7e9e87f4a6845e6dcea/.github/workflows/create_swift_package.yml
+4. Update the `CHANGELOG.md` for the impending release.
+5. Update the `README.md` with the new version.
+6. `git commit -sam "Prepare for release X.Y.Z."` (where X.Y.Z is the new version)
+7. `git tag -a X.Y.X -m "Version X.Y.Z"` (where X.Y.Z is the new version)
+ * Run `git tag` to verify it.
+8. `git push && git push --tags`
+ * This should be pushed to your fork.
+9. Create a PR with this commit and merge it.
+10. Update the top level `build.gradle` to the next SNAPSHOT version.
+11. Modify `create_swift_package.yml` workflow to only run manually.
+ * https://github.com/MobileNativeFoundation/Store/blob/de9ed1764408eeaafe5e58fe602205c875a8b0b0/.github/workflows/create_swift_package.yml
+12. `git commit -am "Prepare next development version."`
+13. Create a PR with this commit and merge it.
+14. Login to Sonatype to promote the artifacts https://central.sonatype.org/pages/releasing-the-deployment.html
+ * This part is automated. If it fails in CI, follow the steps below.
+ * Click on Staging Repositories under Build Promotion
+ * Select all the Repositories that contain the content you want to release
+ * Click on Close and refresh until the Release button is active
+ * Click Release and submit
+15. Update the sample module's `build.gradle` to point to the newly released version. (It may take ~2 hours for artifact to be available after release)
+
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
deleted file mode 100644
index 796b96d1c..000000000
--- a/app/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index 6e0a3a6ad..000000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,56 +0,0 @@
-apply plugin: 'com.android.application'
-apply plugin: 'com.getkeepsafe.dexcount'
-
-android {
- compileSdkVersion versions.compileSdk
- buildToolsVersion versions.buildTools
- defaultConfig {
- applicationId "com.nytimes.android.store.sample"
- minSdkVersion 19
- compileSdkVersion versions.compileSdk
- targetSdkVersion versions.targetSdk
- versionCode 1
- versionName "1.0"
- testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
- }
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
- lintOptions {
- abortOnError false
- disable 'InvalidPackage'
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
- packagingOptions {
- exclude 'META-INF/rxjava.properties'
- }
-}
-
-dependencies {
-
- testImplementation libraries.junit
-
- implementation libraries.supportRecyclerView
- implementation libraries.supportAppCompat
- implementation libraries.supportCardView
- implementation libraries.supportDesign
- implementation libraries.retrofit
- implementation libraries.retrofitGsonConverter
- implementation libraries.retrofitRx2
- implementation libraries.picasso
- implementation libraries.guava
- annotationProcessor libraries.immutablesValue // <-- for annotation processor
- compileOnly libraries.immutablesValue // <-- for annotation API
- compileOnly libraries.immutablesGson // for annotations
- implementation 'com.nytimes.android:store3:3.0.0-beta'
- implementation 'com.nytimes.android:cache3:3.0.0-beta'
- implementation 'com.nytimes.android:middleware3:3.0.0-beta'
- implementation 'com.nytimes.android:filesystem3:3.0.0-beta'
- implementation libraries.rxAndroid2
-}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
deleted file mode 100644
index a565a62f7..000000000
--- a/app/proguard-rules.pro
+++ /dev/null
@@ -1,17 +0,0 @@
-# Add project specific ProGuard rules here.
-# By default, the flags in this file are appended to flags specified
-# in /Users/206847/Library/Android/sdk/tools/proguard/proguard-android.txt
-# You can edit the include path and order by changing the proguardFiles
-# directive in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# Add any project specific keep options here:
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
deleted file mode 100644
index af1966639..000000000
--- a/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/java/com/nytimes/android/sample/SampleApp.java b/app/src/main/java/com/nytimes/android/sample/SampleApp.java
deleted file mode 100644
index 6f9150664..000000000
--- a/app/src/main/java/com/nytimes/android/sample/SampleApp.java
+++ /dev/null
@@ -1,104 +0,0 @@
-package com.nytimes.android.sample;
-
-import android.app.Application;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.nytimes.android.external.fs3.SourcePersisterFactory;
-import com.nytimes.android.external.store3.base.Persister;
-import com.nytimes.android.external.store3.base.impl.BarCode;
-import com.nytimes.android.external.store3.base.impl.MemoryPolicy;
-import com.nytimes.android.external.store3.base.impl.Store;
-import com.nytimes.android.external.store3.base.impl.StoreBuilder;
-import com.nytimes.android.external.store3.middleware.GsonParserFactory;
-import com.nytimes.android.sample.data.model.GsonAdaptersModel;
-import com.nytimes.android.sample.data.model.RedditData;
-import com.nytimes.android.sample.data.remote.Api;
-
-import java.io.IOException;
-import java.util.concurrent.TimeUnit;
-
-import io.reactivex.Single;
-import okhttp3.ResponseBody;
-import okio.BufferedSource;
-import retrofit2.Retrofit;
-import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
-import retrofit2.converter.gson.GsonConverterFactory;
-
-public class SampleApp extends Application {
-
- private Store nonPersistedStore;
- private Store persistedStore;
- private Persister persister;
-
- @Override
- public void onCreate() {
- super.onCreate();
-
- initPersister();
- this.nonPersistedStore = provideRedditStore();
- this.persistedStore = providePersistedRedditStore();
- }
-
- private void initPersister() {
- try {
- persister = newPersister();
- } catch (IOException exception) {
- throw new RuntimeException(exception);
- }
- }
-
- public Store getNonPersistedStore() {
- return this.nonPersistedStore;
- }
-
- public Store getPersistedStore() {
- return this.persistedStore;
- }
-
- private Store provideRedditStore() {
- return StoreBuilder.barcode()
- .fetcher(barCode -> provideRetrofit().fetchSubreddit(barCode.getKey(), "10"))
- .memoryPolicy(
- MemoryPolicy
- .builder()
- .setExpireAfter(10)
- .setExpireAfterTimeUnit(TimeUnit.SECONDS)
- .build()
- )
- .open();
- }
-
- private Store providePersistedRedditStore() {
- return StoreBuilder.parsedWithKey()
- .fetcher(this::fetcher)
- .persister(persister)
- .parser(GsonParserFactory.createSourceParser(provideGson(), RedditData.class))
- .open();
- }
-
- private Persister newPersister() throws IOException {
- return SourcePersisterFactory.create(getApplicationContext().getCacheDir());
- }
-
- private Single fetcher(BarCode barCode) {
- return provideRetrofit().fetchSubredditForPersister(barCode.getKey(), "10")
- .map(ResponseBody::source);
- }
-
- private Api provideRetrofit() {
- return new Retrofit.Builder()
- .baseUrl("http://reddit.com/")
- .addConverterFactory(GsonConverterFactory.create(provideGson()))
- .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
- .validateEagerly(BuildConfig.DEBUG) // Fail early: check Retrofit configuration at creation time in Debug build.
- .build()
- .create(Api.class);
- }
-
- Gson provideGson() {
- return new GsonBuilder()
- .registerTypeAdapterFactory(new GsonAdaptersModel())
- .create();
- }
-}
diff --git a/app/src/main/java/com/nytimes/android/sample/activity/PersistingStoreActivity.java b/app/src/main/java/com/nytimes/android/sample/activity/PersistingStoreActivity.java
deleted file mode 100644
index 6d41ce3d6..000000000
--- a/app/src/main/java/com/nytimes/android/sample/activity/PersistingStoreActivity.java
+++ /dev/null
@@ -1,90 +0,0 @@
-package com.nytimes.android.sample.activity;
-
-import android.os.Bundle;
-import android.support.v7.app.AppCompatActivity;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.Toolbar;
-import android.util.Log;
-import android.widget.Toast;
-
-import com.nytimes.android.external.store3.base.impl.BarCode;
-import com.nytimes.android.external.store3.base.impl.Store;
-import com.nytimes.android.sample.R;
-import com.nytimes.android.sample.SampleApp;
-import com.nytimes.android.sample.data.model.Children;
-import com.nytimes.android.sample.data.model.Post;
-import com.nytimes.android.sample.data.model.RedditData;
-import com.nytimes.android.sample.reddit.PostAdapter;
-
-import java.util.List;
-
-import io.reactivex.Observable;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.schedulers.Schedulers;
-
-import static android.widget.Toast.makeText;
-
-
-public class PersistingStoreActivity extends AppCompatActivity {
-
- private RecyclerView recyclerView;
- private PostAdapter postAdapter;
- private Store persistedStore;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.activity_store);
- setSupportActionBar((Toolbar) findViewById(R.id.toolbar));
-
- postAdapter = new PostAdapter();
- recyclerView = (RecyclerView) findViewById(R.id.postRecyclerView);
- LinearLayoutManager layoutManager = new LinearLayoutManager(this);
- layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
- recyclerView.setLayoutManager(layoutManager);
- recyclerView.setAdapter(postAdapter);
- }
-
- private void initStore() {
- if (this.persistedStore == null) {
- this.persistedStore = ((SampleApp) getApplicationContext()).getPersistedStore();
- }
- }
-
- @SuppressWarnings("CheckReturnValue")
- public void loadPosts() {
- BarCode awwRequest = new BarCode(RedditData.class.getSimpleName(), "aww");
-
- this.persistedStore
- .get(awwRequest)
- .flatMapObservable(this::sanitizeData)
- .toList()
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(this::showPosts, throwable ->
- Log.e(StoreActivity.class.getSimpleName(), throwable.getMessage(),
- throwable));
- }
-
- private void showPosts(List posts) {
- postAdapter.setPosts(posts);
- makeText(PersistingStoreActivity.this,
- "Loaded " + posts.size() + " posts",
- Toast.LENGTH_SHORT)
- .show();
- }
-
- private Observable sanitizeData(RedditData redditData) {
- return Observable.fromIterable(redditData.data().children())
- .map(Children::data);
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- initStore();
- loadPosts();
- }
-}
diff --git a/app/src/main/java/com/nytimes/android/sample/activity/StoreActivity.java b/app/src/main/java/com/nytimes/android/sample/activity/StoreActivity.java
deleted file mode 100644
index 4edcb6ff4..000000000
--- a/app/src/main/java/com/nytimes/android/sample/activity/StoreActivity.java
+++ /dev/null
@@ -1,96 +0,0 @@
-package com.nytimes.android.sample.activity;
-
-import android.os.Bundle;
-import android.support.v7.app.AppCompatActivity;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.Toolbar;
-import android.util.Log;
-import android.widget.Toast;
-
-import com.nytimes.android.external.store3.base.impl.BarCode;
-import com.nytimes.android.external.store3.base.impl.Store;
-import com.nytimes.android.sample.R;
-import com.nytimes.android.sample.SampleApp;
-import com.nytimes.android.sample.data.model.Children;
-import com.nytimes.android.sample.data.model.Post;
-import com.nytimes.android.sample.data.model.RedditData;
-import com.nytimes.android.sample.reddit.PostAdapter;
-
-import java.util.List;
-
-import io.reactivex.Observable;
-import io.reactivex.ObservableSource;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.annotations.NonNull;
-import io.reactivex.functions.Function;
-import io.reactivex.schedulers.Schedulers;
-
-import static android.widget.Toast.makeText;
-
-
-public class StoreActivity extends AppCompatActivity {
-
- private RecyclerView recyclerView;
- private PostAdapter postAdapter;
- private Store nonPersistedStore;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_store);
- setSupportActionBar((Toolbar) findViewById(R.id.toolbar));
- postAdapter = new PostAdapter();
- recyclerView = (RecyclerView) findViewById(R.id.postRecyclerView);
- LinearLayoutManager layoutManager = new LinearLayoutManager(this);
- layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
- recyclerView.setLayoutManager(layoutManager);
- recyclerView.setAdapter(postAdapter);
- }
-
- private void initStore() {
- if (this.nonPersistedStore == null) {
- this.nonPersistedStore = ((SampleApp) getApplicationContext()).getNonPersistedStore();
- }
- }
-
- @SuppressWarnings("CheckReturnValue")
- public void loadPosts() {
- BarCode awwRequest = new BarCode(RedditData.class.getSimpleName(), "aww");
-
- this.nonPersistedStore
- .get(awwRequest)
- .flatMapObservable(new Function>() {
- @Override
- public ObservableSource apply(@NonNull RedditData redditData) throws Exception {
- return sanitizeData(redditData);
- }
- })
- .toList()
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(this::showPosts, throwable -> {
- Log.e(StoreActivity.class.getSimpleName(), throwable.getMessage(), throwable);
- });
- }
-
- private void showPosts(List posts) {
- postAdapter.setPosts(posts);
- makeText(StoreActivity.this,
- "Loaded " + posts.size() + " posts",
- Toast.LENGTH_SHORT)
- .show();
- }
-
- private Observable sanitizeData(RedditData redditData) {
- return Observable.fromIterable(redditData.data().children())
- .map(Children::data);
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- initStore();
- loadPosts();
- }
-}
diff --git a/app/src/main/java/com/nytimes/android/sample/data/model/Children.java b/app/src/main/java/com/nytimes/android/sample/data/model/Children.java
deleted file mode 100644
index 74183d952..000000000
--- a/app/src/main/java/com/nytimes/android/sample/data/model/Children.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.nytimes.android.sample.data.model;
-
-import org.immutables.value.Value;
-
-@Value.Immutable
-public abstract class Children {
- public abstract Post data();
-}
diff --git a/app/src/main/java/com/nytimes/android/sample/data/model/Data.java b/app/src/main/java/com/nytimes/android/sample/data/model/Data.java
deleted file mode 100644
index 4e0fd2cf1..000000000
--- a/app/src/main/java/com/nytimes/android/sample/data/model/Data.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.nytimes.android.sample.data.model;
-
-import org.immutables.value.Value;
-
-import java.util.List;
-
-@Value.Immutable
-public abstract class Data {
- public abstract List children();
-}
diff --git a/app/src/main/java/com/nytimes/android/sample/data/model/Image.java b/app/src/main/java/com/nytimes/android/sample/data/model/Image.java
deleted file mode 100644
index 786406399..000000000
--- a/app/src/main/java/com/nytimes/android/sample/data/model/Image.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.nytimes.android.sample.data.model;
-
-import org.immutables.value.Value;
-
-@Value.Immutable
-public abstract class Image {
- public abstract String url();
- public abstract int height();
- public abstract int width();
-}
diff --git a/app/src/main/java/com/nytimes/android/sample/data/model/Images.java b/app/src/main/java/com/nytimes/android/sample/data/model/Images.java
deleted file mode 100644
index 06ab2cbab..000000000
--- a/app/src/main/java/com/nytimes/android/sample/data/model/Images.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.nytimes.android.sample.data.model;
-
-import org.immutables.value.Value;
-
-@Value.Immutable
-public abstract class Images {
- public abstract Image source();
-}
diff --git a/app/src/main/java/com/nytimes/android/sample/data/model/Post.java b/app/src/main/java/com/nytimes/android/sample/data/model/Post.java
deleted file mode 100644
index 9d408fca4..000000000
--- a/app/src/main/java/com/nytimes/android/sample/data/model/Post.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.nytimes.android.sample.data.model;
-
-import android.support.annotation.Nullable;
-
-import com.google.common.base.Optional;
-
-import org.immutables.value.Value;
-
-@Value.Immutable
-public abstract class Post {
- @Nullable
- public abstract Preview preview();
-
- public abstract String title();
-
- public abstract String url();
-
- @Nullable
- public abstract Integer height();
-
- @Nullable
- public abstract Integer width();
-
- @Value.Derived
- public Optional nestedThumbnail() {
- if (preview() == null || preview().images() == null || preview().images().get(0).source() == null)
- return Optional.absent();
- return Optional.of(preview().images().get(0).source());
- }
-
-}
diff --git a/app/src/main/java/com/nytimes/android/sample/data/model/Preview.java b/app/src/main/java/com/nytimes/android/sample/data/model/Preview.java
deleted file mode 100644
index 95c7f952d..000000000
--- a/app/src/main/java/com/nytimes/android/sample/data/model/Preview.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.nytimes.android.sample.data.model;
-
-import org.immutables.value.Value;
-
-import java.util.List;
-
-@Value.Immutable
-@Value.Style(allParameters = true)
-public abstract class Preview {
- @Value.Parameter(false)
- public abstract List images();
-}
diff --git a/app/src/main/java/com/nytimes/android/sample/data/model/RedditData.java b/app/src/main/java/com/nytimes/android/sample/data/model/RedditData.java
deleted file mode 100644
index 686faf8de..000000000
--- a/app/src/main/java/com/nytimes/android/sample/data/model/RedditData.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.nytimes.android.sample.data.model;
-
-import org.immutables.value.Value;
-
-@Value.Immutable
-public abstract class RedditData {
- public abstract Data data();
- public abstract String kind();
-}
diff --git a/app/src/main/java/com/nytimes/android/sample/data/model/package-info.java b/app/src/main/java/com/nytimes/android/sample/data/model/package-info.java
deleted file mode 100644
index 9417bd0fe..000000000
--- a/app/src/main/java/com/nytimes/android/sample/data/model/package-info.java
+++ /dev/null
@@ -1,4 +0,0 @@
-@Gson.TypeAdapters
-package com.nytimes.android.sample.data.model;
-
-import org.immutables.gson.Gson;
diff --git a/app/src/main/java/com/nytimes/android/sample/data/remote/Api.java b/app/src/main/java/com/nytimes/android/sample/data/remote/Api.java
deleted file mode 100644
index 65e8d5a83..000000000
--- a/app/src/main/java/com/nytimes/android/sample/data/remote/Api.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.nytimes.android.sample.data.remote;
-
-import com.nytimes.android.sample.data.model.RedditData;
-
-import io.reactivex.Single;
-import okhttp3.ResponseBody;
-import retrofit2.http.GET;
-import retrofit2.http.Path;
-import retrofit2.http.Query;
-
-public interface Api {
-
- @GET("r/{subredditName}/new/.json")
- Single fetchSubreddit(@Path("subredditName") String subredditName,
- @Query("limit") String limit);
-
- @GET("r/{subredditName}/new/.json")
- Single fetchSubredditForPersister(@Path("subredditName") String subredditName,
- @Query("limit") String limit);
-}
diff --git a/app/src/main/java/com/nytimes/android/sample/reddit/PostAdapter.java b/app/src/main/java/com/nytimes/android/sample/reddit/PostAdapter.java
deleted file mode 100644
index 93989228b..000000000
--- a/app/src/main/java/com/nytimes/android/sample/reddit/PostAdapter.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.nytimes.android.sample.reddit;
-
-import android.support.v7.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.nytimes.android.sample.R;
-import com.nytimes.android.sample.data.model.Post;
-
-import java.util.ArrayList;
-import java.util.List;
-
-
-
-public class PostAdapter extends RecyclerView.Adapter {
-
- private final List articles = new ArrayList<>();
-
- @Override
- public PostViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- View itemView = LayoutInflater.from(
- parent.getContext()).inflate(R.layout.article_item, parent, false);
- return new PostViewHolder(itemView);
- }
-
- @Override
- public void onBindViewHolder(PostViewHolder holder, int position) {
- holder.onBind(articles.get(position));
- }
-
- @Override
- public int getItemCount() {
- return articles.size();
- }
-
- public void setPosts(List articlesToAdd) {
- articles.clear();
- articles.addAll(articlesToAdd);
- notifyDataSetChanged();
- }
-}
diff --git a/app/src/main/java/com/nytimes/android/sample/reddit/PostViewHolder.java b/app/src/main/java/com/nytimes/android/sample/reddit/PostViewHolder.java
deleted file mode 100644
index 72e3dd025..000000000
--- a/app/src/main/java/com/nytimes/android/sample/reddit/PostViewHolder.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package com.nytimes.android.sample.reddit;
-
-import android.app.Application;
-import android.support.v7.widget.RecyclerView;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-
-import com.nytimes.android.sample.R;
-import com.nytimes.android.sample.data.model.Image;
-import com.nytimes.android.sample.data.model.ImmutableImage;
-import com.nytimes.android.sample.data.model.Post;
-import com.nytimes.android.sample.util.BitmapTransform;
-import com.nytimes.android.sample.util.DeviceUtils;
-import com.squareup.picasso.Picasso;
-
-
-public class PostViewHolder extends RecyclerView.ViewHolder {
-
- private int maxHeight;
- private int maxWidth;
- private TextView title;
- private ImageView thumbnail;
- private View topSpacer;
- private final DeviceUtils deviceUtils;
-
- public PostViewHolder(View itemView) {
- super(itemView);
- deviceUtils = new DeviceUtils((Application) itemView.getContext().getApplicationContext());
- findViews(itemView);
- setMaxDimensions(itemView);
- }
-
- public void onBind(Post article) {
- title.setText(article.title());
- if (article.nestedThumbnail().isPresent()) {
- showImage(article);
- }
- }
-
- private void showImage(Post article) {
- Image nestedImage = article.nestedThumbnail().get();
- Image image = ImmutableImage
- .builder()
- .height(nestedImage.height())
- .width(nestedImage.width())
- .url(nestedImage.url())
- .build();
- BitmapTransform bitmapTransform = new BitmapTransform(maxWidth, maxHeight, image);
-
- int targetWidth = bitmapTransform.targetWidth;
- int targetHeight = bitmapTransform.targetHeight;
-
- setSpacer(targetWidth, targetHeight);
-
- setupThumbnail(targetWidth, targetHeight);
-
- Picasso.with(itemView.getContext())
- .load(image.url())
- .transform(bitmapTransform)
- .resize(targetWidth, targetHeight)
- .centerInside()
- .placeholder(R.color.gray80)
- .into(thumbnail);
- }
-
- private void setSpacer(int targetWidth, int targetHeight) {
- if (targetWidth >= targetHeight) {
- topSpacer.setVisibility(View.GONE);
- } else {
- topSpacer.setVisibility(View.VISIBLE);
- }
- }
-
- private void setupThumbnail(int targetWidth, int targetHeight) {
- thumbnail.setMaxWidth(targetWidth);
- thumbnail.setMaxHeight(targetHeight);
- thumbnail.setMinimumWidth(targetWidth);
- thumbnail.setMinimumHeight(targetHeight);
- thumbnail.requestLayout();
- }
-
- private void setMaxDimensions(View itemView) {
- int screenWidth;
- int screenHeight;
- screenWidth = deviceUtils.getScreenWidth();
- screenHeight = deviceUtils.getScreenHeight();
-
- if (screenWidth > screenHeight) {
- screenHeight = deviceUtils.getScreenWidth();
- screenWidth = deviceUtils.getScreenHeight();
- }
-
- maxHeight = (int) (screenHeight * .55f);
- int margin = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.post_horizontal_margin);
- maxWidth = screenWidth - (2 * margin);
- }
-
- private void findViews(View itemView) {
- title = (TextView) itemView.findViewById(R.id.title);
- thumbnail = (ImageView) itemView.findViewById(R.id.thumbnail);
- topSpacer = itemView.findViewById(R.id.topSpacer);
- }
-}
diff --git a/app/src/main/java/com/nytimes/android/sample/util/BitmapTransform.java b/app/src/main/java/com/nytimes/android/sample/util/BitmapTransform.java
deleted file mode 100644
index 54b878b1b..000000000
--- a/app/src/main/java/com/nytimes/android/sample/util/BitmapTransform.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package com.nytimes.android.sample.util;
-
-import android.graphics.Bitmap;
-
-import com.nytimes.android.sample.data.model.Image;
-import com.squareup.picasso.Transformation;
-
-
-public class BitmapTransform implements Transformation
-{
- int maxWidth, maxHeight;
- Image key;
- public int targetWidth;
- public int targetHeight;
- private final String picassoKey;
-
- public BitmapTransform(int maxWidth, int maxHeight, Image image) {
- this.maxWidth = maxWidth;
- this.maxHeight = maxHeight;
- this.key = image;
- this.picassoKey = key.url() + "_" + targetWidth + ":" + targetHeight;
-
- double aspectRatio;
- if (image.width() >= image.height()) {
- targetWidth = maxWidth;
- aspectRatio = (double) image.height() / (double) image.width();
- targetHeight = (int) (targetWidth * aspectRatio);
- } else {
- targetHeight = maxHeight;
- aspectRatio = (double) image.width() / (double) image.height();
- targetWidth = (int) (targetHeight * aspectRatio);
- }
- }
-
- @Override
- public Bitmap transform(Bitmap source) {
- Bitmap result = Bitmap.createScaledBitmap(source, targetWidth,
- targetHeight, true);
- if (result != source) {
- source.recycle();
- }
- return result;
- }
-
- @Override
- public String key() {
- return picassoKey;
- }
-}
diff --git a/app/src/main/java/com/nytimes/android/sample/util/DeviceUtils.java b/app/src/main/java/com/nytimes/android/sample/util/DeviceUtils.java
deleted file mode 100644
index 7a9424d7c..000000000
--- a/app/src/main/java/com/nytimes/android/sample/util/DeviceUtils.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.nytimes.android.sample.util;
-
-import android.app.Application;
-import android.content.Context;
-import android.graphics.Point;
-import android.view.Display;
-import android.view.WindowManager;
-
-public class DeviceUtils {
-
- private WindowManager windowManager;
-
- public DeviceUtils(Application context) {
- windowManager =
- (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
- }
-
- public int getScreenWidth() {
- return getScreenSize().x;
- }
-
- public int getScreenHeight() {
- return getScreenSize().y;
- }
-
- public Point getScreenSize() {
- Display display = windowManager.getDefaultDisplay();
- Point result = new Point();
- display.getSize(result);
- return result;
- }
-}
diff --git a/app/src/main/res/layout/activity_store.xml b/app/src/main/res/layout/activity_store.xml
deleted file mode 100644
index 9454f3d33..000000000
--- a/app/src/main/res/layout/activity_store.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/article_item.xml b/app/src/main/res/layout/article_item.xml
deleted file mode 100644
index dffb1d1c5..000000000
--- a/app/src/main/res/layout/article_item.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index cde69bccc..000000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index c133a0cbd..000000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index bfa42f0e7..000000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index 324e72cdd..000000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index aee44e138..000000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
deleted file mode 100644
index 1bcf5fba3..000000000
--- a/app/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
- #3F51B5
- #303F9F
- #FF4081
-
- #222222
- #000000
- #FFFFFF
- #ffdfdfdf
-
- @android:color/transparent
- #000
- #1A000000
- #26000000
- #66000000
- #80000000
- #BF000000
- #CC000000
- #CF000000
- #E6000000
-
-
- #1a1a1a
- #E61a1a1a
- #222
- #CF222222
- #333
- #505050
- #666
- #808080
- #999
- #b3b3b3
- #ccc
- #e3e3e3
- #ebebeb
- #f3f3f3
- #f7f7f7
- #fafafa
- #a5a5a5
- #f6f6f6
- #cccccc
-
- #fff
- #80ffffff
-
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
deleted file mode 100644
index 52fdd43c5..000000000
--- a/app/src/main/res/values/dimens.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
- 16dp
- 16dp
-
- 24sp
- 22sp
- 20sp
- 18sp
- 16sp
- 14sp
- 16dp
- 16dp
- 16dp
- 15dp
- 5dp
-
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
deleted file mode 100644
index 1ad8c299e..000000000
--- a/app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
- Sample
-
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
deleted file mode 100644
index fa3d07cf5..000000000
--- a/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/app/src/test/java/com/nytimes/android/sample/StoreIntegrationTest.java b/app/src/test/java/com/nytimes/android/sample/StoreIntegrationTest.java
deleted file mode 100644
index 1afca4b23..000000000
--- a/app/src/test/java/com/nytimes/android/sample/StoreIntegrationTest.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.nytimes.android.sample;
-
-import com.nytimes.android.external.store3.base.impl.Store;
-import com.nytimes.android.external.store3.base.impl.BarCode;
-import com.nytimes.android.external.store3.base.impl.StoreBuilder;
-
-import org.junit.Before;
-import org.junit.Test;
-
-import io.reactivex.Single;
-
-import static junit.framework.Assert.assertEquals;
-
-public class StoreIntegrationTest {
-
- private Store testStore;
-
- @Before
- public void setUp() throws Exception {
- testStore = StoreBuilder.barcode()
- .fetcher(barCode -> Single.just("hello"))
- .open();
- }
-
- @Test
- public void testRepeatedGet() throws Exception {
- String first = testStore.get(BarCode.empty()).blockingGet();
- assertEquals(first, "hello");
-
- }
-}
diff --git a/app/version.properties b/app/version.properties
deleted file mode 100644
index 6c66f5620..000000000
--- a/app/version.properties
+++ /dev/null
@@ -1 +0,0 @@
-version=0.1
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index 2e29bb48a..000000000
--- a/build.gradle
+++ /dev/null
@@ -1,96 +0,0 @@
-apply from: 'buildsystem/dependencies.gradle'
-
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
-buildscript {
- repositories {
- mavenLocal()
- maven {
- url 'https://plugins.gradle.org/m2/'
- }
-
- maven {
- url = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/'
- }
-
- jcenter()
-
- google()
- }
-
- rootProject.ext.versions = [
- kotlin: '1.1.4'
- ]
-
- dependencies {
- classpath 'com.android.tools.build:gradle:3.0.0-beta2'
- classpath 'com.google.gms:google-services:3.0.0'
- classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.5.6'
- classpath 'net.ltgt.gradle:gradle-errorprone-plugin:0.0.11'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$rootProject.ext.versions.kotlin"
- classpath 'org.jetbrains.dokka:dokka-gradle-plugin:0.9.14'
- }
-}
-
-allprojects {
- buildscript {
-
- }
-
- repositories {
- maven {
- url 'https://oss.sonatype.org/content/repositories/snapshots/'
- }
-
- mavenCentral()
- }
-
- // Workaround to prevent Gradle from stealing focus from other apps during tests run/etc.
- // https://gist.github.com/artem-zinnatullin/4c250e04636e25797165
- tasks.withType(JavaForkOptions) {
- jvmArgs '-Djava.awt.headless=true'
- }
-}
-
-ext {
- // POM file
- GROUP = "com.nytimes.android"
- VERSION_NAME = "3.0.0-SNAPSHOT"
- POM_PACKAGING = "pom"
- POM_DESCRIPTION = "Store3 is built with RxJava2"
-
- POM_URL = "https://github.com/nytimes/Store/"
- POM_SCM_URL = "https://github.com/nytimes/Store/"
- POM_SCM_CONNECTION = "scm:git:https://github.com/nytm/Store.git"
- POM_SCM_DEV_CONNECTION = "scm:git:git@github.com:nytm/Store.git"
-
- POM_LICENCE_NAME = "Apache License"
- POM_LICENCE_URL = "http://www.apache.org/licenses/"
- POM_LICENCE_DIST = "repo"
-
- POM_DEVELOPER_ID = "nytimesandroid"
- POM_DEVELOPER_NAME = "New York Times"
-}
-
-
-// From command line use: -PdisablePreDex to disable it: primarily for jenkins
-project.ext.preDexLibs = !project.hasProperty('disablePreDex')
-
-subprojects {
- project.plugins.whenPluginAdded { plugin ->
- if ("com.android.build.gradle.AppPlugin".equals(plugin.class.name)) {
- project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs
- } else if ("com.android.build.gradle.LibraryPlugin".equals(plugin.class.name)) {
- project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs
- }
- }
- project.plugins.apply('net.ltgt.errorprone')
-
- configurations.errorprone {
- resolutionStrategy.force 'com.google.errorprone:error_prone_core:2.0.15'
- }
-}
-
-task gitHooksInit (type:Exec) {
- workingDir "$projectDir"
- commandLine './init-git-hooks'
-}
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 000000000..3d149aadd
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,75 @@
+plugins {
+ alias(libs.plugins.spotless)
+ alias(libs.plugins.detekt)
+}
+
+buildscript {
+ repositories {
+ mavenCentral()
+ gradlePluginPortal()
+ google()
+ }
+
+ dependencies {
+ classpath(libs.android.gradle.plugin)
+ classpath(libs.kotlin.gradle.plugin)
+ classpath(libs.kotlin.serialization.plugin)
+ classpath(libs.dokka.gradle.plugin)
+ classpath(libs.jacoco.gradle.plugin)
+ classpath(libs.maven.publish.plugin)
+ classpath(libs.atomic.fu.gradle.plugin)
+ classpath(libs.kmmBridge.gradle.plugin)
+ classpath(libs.binary.compatibility.validator)
+ }
+}
+
+allprojects {
+ repositories {
+ mavenCentral()
+ google()
+ }
+}
+
+subprojects {
+ apply(plugin = "com.diffplug.spotless")
+ apply(plugin = "io.gitlab.arturbosch.detekt")
+
+ spotless {
+ kotlin {
+ ktfmt(libs.versions.ktfmt.get()).googleStyle()
+ target("src/**/*.kt")
+ trimTrailingWhitespace()
+ endWithNewline()
+ }
+
+ kotlinGradle {
+ ktfmt(libs.versions.ktfmt.get()).googleStyle()
+ target("*.kts")
+ trimTrailingWhitespace()
+ endWithNewline()
+ }
+ }
+
+ detekt {
+ buildUponDefaultConfig = true
+ baseline = file("${projectDir}/config/detekt/baseline.xml")
+ config.setFrom("$rootDir/config/detekt/rules.yml")
+ source.setFrom("src")
+ }
+}
+
+tasks {
+ withType {
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ }
+
+ withType().configureEach {
+ sourceCompatibility = JavaVersion.VERSION_11.name
+ targetCompatibility = JavaVersion.VERSION_11.name
+ }
+}
+
+// Workaround for https://youtrack.jetbrains.com/issue/KT-62040
+tasks.getByName("wrapper")
diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle
deleted file mode 100644
index 529369268..000000000
--- a/buildsystem/dependencies.gradle
+++ /dev/null
@@ -1,137 +0,0 @@
-allprojects {
- repositories {
- mavenLocal()
- maven {
- url 'https://plugins.gradle.org/m2/'
- }
-
- maven {
- url = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/'
- }
- jcenter()
- }
-}
-
-ext.versions = [
- minSdk : 16,
- targetSdk : 25,
- compileSdk : 25,
- buildTools : '26.0.1',
- kotlin : '1.1.2-5',
-
- // UI libs.
- supportLibs : '25.1.1',
- picasso : '2.5.2',
- rxBinding : '0.3.0',
- butterKnife : '7.0.1',
-
- // Reactive.
- rxJava : '1.3.0',
- rxJava2 : '2.1.2',
- rxJavaProGuardRules : '1.1.6.0',
- rxJavaAsyncUtil : '0.21.0',
- rxAndroid : '1.3.0',
- rxAndroid2 : '2.0.1',
-
- // Others.
- retrofit : '2.2.0',
- dagger : '2.9',
- jsr305 : '3.0.1',
- okHttp : '2.7.5',
- okio : '1.13.0',
- gson : '2.8.1',
- moshi : '1.5.0',
- jackson : '2.8.8',
- guava : '19.0',
- javapoet : '1.8.0',
- immutables : '2.2.1',
- supportMultiDex : '1.0.1',
- javaxInject : '1',
- autoFactory : '1.0-beta3',
- autoService : '1.0-rc2',
- autoCommon : '0.6',
- javaWriter : '2.5.1',
- commonsLang3 : '3.0',
- javax : '1',
-
- // Debugging & Inspecting.
- slf4j : '1.7.19',
-
- // Testing.
- junit : '4.12',
- assertJ : '1.7.1',
- mockito : '1.9.5',
- robolectric : '3.1.2',
- supportTestRunner : '0.4.1',
- espresso : '2.2.1',
- compileTesting : '0.8',
-]
-
-ext.libraries = [
- // UI libs.
- supportAppCompat : "com.android.support:appcompat-v7:$versions.supportLibs",
- supportCardView : "com.android.support:cardview-v7:$versions.supportLibs",
- supportDesign : "com.android.support:design:$versions.supportLibs",
- supportPercent : "com.android.support:percent:$versions.supportLibs",
- supportV13 : "com.android.support:support-v13:$versions.supportLibs",
- supportRecyclerView : "com.android.support:recyclerview-v7:$versions.supportLibs",
- supportVectorDrawable : "com.android.support:support-vector-drawable:$versions.supportLibs",
- supportAnimatedDrawable : "com.android.support:animated-vector-drawable:$versions.supportLibs",
- picasso : "com.squareup.picasso:picasso:$versions.picasso",
- rxBinding : "com.jakewharton.rxbinding:rxbinding:$versions.rxBinding",
- butterKnife : "com.jakewharton:butterknife:$versions.butterKnife",
-
- // Reactive.
- rxJava : "io.reactivex:rxjava:$versions.rxJava",
- rxJava2 : "io.reactivex.rxjava2:rxjava:$versions.rxJava2",
- rxJavaAsyncUtil : "io.reactivex:rxjava-async-util:$versions.rxJavaAsyncUtil",
- rxJavaProGuardRules : "com.artemzin.rxjava:proguard-rules:$versions.rxJavaProGuardRules",
- rxAndroid : "io.reactivex:rxandroid:$versions.rxAndroid",
- rxAndroid2 : "io.reactivex.rxjava2:rxandroid:$versions.rxAndroid2",
-
- // Others.
- retrofit : "com.squareup.retrofit2:retrofit:$versions.retrofit",
- retrofitGsonConverter : "com.squareup.retrofit2:converter-gson:$versions.retrofit",
- retrofitRx2 : "com.squareup.retrofit2:adapter-rxjava2:$versions.retrofit",
- dagger : "com.google.dagger:dagger:$versions.dagger",
- daggerCompiler : "com.google.dagger:dagger-compiler:$versions.dagger",
- jsr305 : "com.google.code.findbugs:jsr305:$versions.jsr305",
- okHttp : "com.squareup.okhttp:okhttp:$versions.okHttp",
- okio : "com.squareup.okio:okio:$versions.okio",
- gson : "com.google.code.gson:gson:$versions.gson",
- moshi : "com.squareup.moshi:moshi:$versions.moshi",
- jacksonCore : "com.fasterxml.jackson.core:jackson-core:$versions.jackson",
- jacksonDatabind : "com.fasterxml.jackson.core:jackson-databind:$versions.jackson",
- guava : "com.google.guava:guava:$versions.guava",
- javapoet : "com.squareup:javapoet:$versions.javapoet",
- immutablesValue : "org.immutables:value:$versions.immutables",
- immutablesValueProcessor: "org.immutables:value-processor:$versions.immutables",
- immutablesGson : "org.immutables:gson:$versions.immutables",
- supportAnnotations : "com.android.support:support-annotations:$versions.supportLibs",
- supportMultiDex : "com.android.support:multidex:$versions.supportMultiDex",
- javaxInject : "javax.inject:javax.inject:$versions.javaxInject",
- autoFactory : "com.google.auto.factory:auto-factory:$versions.autoFactory",
- autoService : "com.google.auto.service:auto-service:$versions.autoService",
- autoCommon : "com.google.auto:auto-common:$versions.autoCommon",
- javaWriter : "com.squareup:javawriter:$versions.javaWriter",
- javax : "javax.inject:javax.inject:$versions.javax",
- kotlinStdLib : "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin",
- commonsLang3 : "org.apache.commons:commons-lang3:$versions.commonsLang3",
- // Debugging & Inspecting.
- slf4jNoOp : "org.slf4j:slf4j-api:$versions.slf4j",
- slf4jTesting : "org.slf4j:slf4j-simple:$versions.slf4j",
- slf4jAndroid : "org.slf4j:slf4j-android:$versions.slf4j",
-
- // Testing.
- junit : "junit:junit:$versions.junit",
- assertJ : "org.assertj:assertj-core:$versions.assertJ",
- mockito : "org.mockito:mockito-core:$versions.mockito",
- robolectric : "org.robolectric:robolectric:$versions.robolectric",
- robolectricMultiDex : "org.robolectric:shadows-multidex:$versions.robolectric",
- supportTestRunner : "com.android.support.test:runner:$versions.supportTestRunner",
- supportTestRules : "com.android.support.test:rules:$versions.supportTestRunner",
- espressoCore : "com.android.support.test.espresso:espresso-core:$versions.espresso",
- espressoContrib : "com.android.support.test.espresso:espresso-contrib:$versions.espresso",
- mockWebserver : "com.squareup.okhttp:mockwebserver:$versions.okHttp",
- compileTesting : "com.google.testing.compile:compile-testing:$versions.compileTesting",
-]
diff --git a/cache/.gitignore b/cache/.gitignore
deleted file mode 100644
index 796b96d1c..000000000
--- a/cache/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
diff --git a/cache/README.md b/cache/README.md
index 33ae76621..e994c7589 100644
--- a/cache/README.md
+++ b/cache/README.md
@@ -1,37 +1,48 @@
-Cache
-===================
+# Cache
-Store depends on a subset of Guava, we have extracted these parts into a shaded Cache artifact.
-Feel free to use Cache anytime you need an in memory cache implementation optimized for Android.
+Store depends on a subset of [Guava](https://github.com/google/guava).
+This is a shaded artifact that is Kotlin Multiplatform compatible.
+## Usage
-To use, first build a cache instance.
+```kotlin
+implementation("org.mobilenativefoundation.store:cache:${STORE_VERSION}")
+```
+
+## Implementation
+
+### Model the key
-```java
- memCache = CacheBuilder.newBuilder()
- .maximumSize(getCacheSize())
- .expireAfterAccess(getCacheTTL(), TimeUnit.SECONDS)
- .build();
+```kotlin
+data class Key(
+ val id: String
+)
```
-You can then use your cache as regular cache or one that knows how to load itself (with blocking) when empty
-```java
-memCache.get(key, new Callable() {
- @Override
- public Observable call() throws Exception {
- return getCachedValue(key);
- }
- });
- ```
-
- Please refer to Guava's Cache documentation for additional features/configurations
- https://github.com/google/guava/wiki/CachesExplained
-
-```groovy
- compile 'com.nytimes.android:cache3:3.0.0-beta'
+### Model the value
+
+```kotlin
+data class Post(
+ val title: String
+)
```
+### Build the cache
+
+```kotlin
+ val cache = CacheBuilder()
+ .maximumSize(100)
+ .expireAfterWrite(1.day)
+ .build()
```
+
+## See Also
+
+https://github.com/google/guava/wiki/CachesExplained
+
+## License
+
+```text
Copyright (c) 2017 The New York Times Company
Copyright (c) 2010 The Guava Authors
diff --git a/cache/api/android/cache.api b/cache/api/android/cache.api
new file mode 100644
index 000000000..e64d0ebd9
--- /dev/null
+++ b/cache/api/android/cache.api
@@ -0,0 +1,76 @@
+public final class org/mobilenativefoundation/store/cache/BuildConfig {
+ public static final field BUILD_TYPE Ljava/lang/String;
+ public static final field DEBUG Z
+ public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
+ public fun ()V
+}
+
+public abstract interface class org/mobilenativefoundation/store/cache5/Cache {
+ public abstract fun getAllPresent ()Ljava/util/Map;
+ public abstract fun getAllPresent (Ljava/util/List;)Ljava/util/Map;
+ public abstract fun getIfPresent (Ljava/lang/Object;)Ljava/lang/Object;
+ public abstract fun getOrPut (Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
+ public abstract fun invalidate (Ljava/lang/Object;)V
+ public abstract fun invalidateAll ()V
+ public abstract fun invalidateAll (Ljava/util/List;)V
+ public abstract fun put (Ljava/lang/Object;Ljava/lang/Object;)V
+ public abstract fun putAll (Ljava/util/Map;)V
+ public abstract fun size ()J
+}
+
+public final class org/mobilenativefoundation/store/cache5/Cache$DefaultImpls {
+ public static fun getAllPresent (Lorg/mobilenativefoundation/store/cache5/Cache;)Ljava/util/Map;
+}
+
+public final class org/mobilenativefoundation/store/cache5/CacheBuilder {
+ public static final field Companion Lorg/mobilenativefoundation/store/cache5/CacheBuilder$Companion;
+ public fun ()V
+ public final fun build ()Lorg/mobilenativefoundation/store/cache5/Cache;
+ public final fun concurrencyLevel (Lkotlin/jvm/functions/Function0;)Lorg/mobilenativefoundation/store/cache5/CacheBuilder;
+ public final fun expireAfterAccess-LRDsOJo (J)Lorg/mobilenativefoundation/store/cache5/CacheBuilder;
+ public final fun expireAfterWrite-LRDsOJo (J)Lorg/mobilenativefoundation/store/cache5/CacheBuilder;
+ public final fun maximumSize (J)Lorg/mobilenativefoundation/store/cache5/CacheBuilder;
+ public final fun ticker (Lkotlin/jvm/functions/Function0;)Lorg/mobilenativefoundation/store/cache5/CacheBuilder;
+ public final fun weigher (JLkotlin/jvm/functions/Function2;)Lorg/mobilenativefoundation/store/cache5/CacheBuilder;
+}
+
+public final class org/mobilenativefoundation/store/cache5/CacheBuilder$Companion {
+}
+
+public final class org/mobilenativefoundation/store/cache5/StoreMultiCache : org/mobilenativefoundation/store/cache5/Cache {
+ public static final field Companion Lorg/mobilenativefoundation/store/cache5/StoreMultiCache$Companion;
+ public fun (Lorg/mobilenativefoundation/store/core5/KeyProvider;Lorg/mobilenativefoundation/store/cache5/Cache;Lorg/mobilenativefoundation/store/cache5/Cache;)V
+ public synthetic fun (Lorg/mobilenativefoundation/store/core5/KeyProvider;Lorg/mobilenativefoundation/store/cache5/Cache;Lorg/mobilenativefoundation/store/cache5/Cache;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun getAllPresent ()Ljava/util/Map;
+ public fun getAllPresent (Ljava/util/List;)Ljava/util/Map;
+ public synthetic fun getIfPresent (Ljava/lang/Object;)Ljava/lang/Object;
+ public fun getIfPresent (Lorg/mobilenativefoundation/store/core5/StoreKey;)Lorg/mobilenativefoundation/store/core5/StoreData;
+ public synthetic fun getOrPut (Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
+ public fun getOrPut (Lorg/mobilenativefoundation/store/core5/StoreKey;Lkotlin/jvm/functions/Function0;)Lorg/mobilenativefoundation/store/core5/StoreData;
+ public synthetic fun invalidate (Ljava/lang/Object;)V
+ public fun invalidate (Lorg/mobilenativefoundation/store/core5/StoreKey;)V
+ public fun invalidateAll ()V
+ public fun invalidateAll (Ljava/util/List;)V
+ public synthetic fun put (Ljava/lang/Object;Ljava/lang/Object;)V
+ public fun put (Lorg/mobilenativefoundation/store/core5/StoreKey;Lorg/mobilenativefoundation/store/core5/StoreData;)V
+ public fun putAll (Ljava/util/Map;)V
+ public fun size ()J
+}
+
+public final class org/mobilenativefoundation/store/cache5/StoreMultiCache$Companion {
+ public final fun invalidKeyErrorMessage (Ljava/lang/Object;)Ljava/lang/String;
+}
+
+public final class org/mobilenativefoundation/store/cache5/StoreMultiCacheAccessor {
+ public fun (Lorg/mobilenativefoundation/store/cache5/Cache;Lorg/mobilenativefoundation/store/cache5/Cache;)V
+ public final fun getAllPresent ()Ljava/util/Map;
+ public final fun getCollection (Lorg/mobilenativefoundation/store/core5/StoreKey$Collection;)Lorg/mobilenativefoundation/store/core5/StoreData$Collection;
+ public final fun getSingle (Lorg/mobilenativefoundation/store/core5/StoreKey$Single;)Lorg/mobilenativefoundation/store/core5/StoreData$Single;
+ public final fun invalidateAll ()V
+ public final fun invalidateCollection (Lorg/mobilenativefoundation/store/core5/StoreKey$Collection;)Z
+ public final fun invalidateSingle (Lorg/mobilenativefoundation/store/core5/StoreKey$Single;)Z
+ public final fun putCollection (Lorg/mobilenativefoundation/store/core5/StoreKey$Collection;Lorg/mobilenativefoundation/store/core5/StoreData$Collection;)Z
+ public final fun putSingle (Lorg/mobilenativefoundation/store/core5/StoreKey$Single;Lorg/mobilenativefoundation/store/core5/StoreData$Single;)Z
+ public final fun size ()J
+}
+
diff --git a/cache/api/jvm/cache.api b/cache/api/jvm/cache.api
new file mode 100644
index 000000000..c7cdabcb3
--- /dev/null
+++ b/cache/api/jvm/cache.api
@@ -0,0 +1,69 @@
+public abstract interface class org/mobilenativefoundation/store/cache5/Cache {
+ public abstract fun getAllPresent ()Ljava/util/Map;
+ public abstract fun getAllPresent (Ljava/util/List;)Ljava/util/Map;
+ public abstract fun getIfPresent (Ljava/lang/Object;)Ljava/lang/Object;
+ public abstract fun getOrPut (Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
+ public abstract fun invalidate (Ljava/lang/Object;)V
+ public abstract fun invalidateAll ()V
+ public abstract fun invalidateAll (Ljava/util/List;)V
+ public abstract fun put (Ljava/lang/Object;Ljava/lang/Object;)V
+ public abstract fun putAll (Ljava/util/Map;)V
+ public abstract fun size ()J
+}
+
+public final class org/mobilenativefoundation/store/cache5/Cache$DefaultImpls {
+ public static fun getAllPresent (Lorg/mobilenativefoundation/store/cache5/Cache;)Ljava/util/Map;
+}
+
+public final class org/mobilenativefoundation/store/cache5/CacheBuilder {
+ public static final field Companion Lorg/mobilenativefoundation/store/cache5/CacheBuilder$Companion;
+ public fun ()V
+ public final fun build ()Lorg/mobilenativefoundation/store/cache5/Cache;
+ public final fun concurrencyLevel (Lkotlin/jvm/functions/Function0;)Lorg/mobilenativefoundation/store/cache5/CacheBuilder;
+ public final fun expireAfterAccess-LRDsOJo (J)Lorg/mobilenativefoundation/store/cache5/CacheBuilder;
+ public final fun expireAfterWrite-LRDsOJo (J)Lorg/mobilenativefoundation/store/cache5/CacheBuilder;
+ public final fun maximumSize (J)Lorg/mobilenativefoundation/store/cache5/CacheBuilder;
+ public final fun ticker (Lkotlin/jvm/functions/Function0;)Lorg/mobilenativefoundation/store/cache5/CacheBuilder;
+ public final fun weigher (JLkotlin/jvm/functions/Function2;)Lorg/mobilenativefoundation/store/cache5/CacheBuilder;
+}
+
+public final class org/mobilenativefoundation/store/cache5/CacheBuilder$Companion {
+}
+
+public final class org/mobilenativefoundation/store/cache5/StoreMultiCache : org/mobilenativefoundation/store/cache5/Cache {
+ public static final field Companion Lorg/mobilenativefoundation/store/cache5/StoreMultiCache$Companion;
+ public fun (Lorg/mobilenativefoundation/store/core5/KeyProvider;Lorg/mobilenativefoundation/store/cache5/Cache;Lorg/mobilenativefoundation/store/cache5/Cache;)V
+ public synthetic fun (Lorg/mobilenativefoundation/store/core5/KeyProvider;Lorg/mobilenativefoundation/store/cache5/Cache;Lorg/mobilenativefoundation/store/cache5/Cache;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun getAllPresent ()Ljava/util/Map;
+ public fun getAllPresent (Ljava/util/List;)Ljava/util/Map;
+ public synthetic fun getIfPresent (Ljava/lang/Object;)Ljava/lang/Object;
+ public fun getIfPresent (Lorg/mobilenativefoundation/store/core5/StoreKey;)Lorg/mobilenativefoundation/store/core5/StoreData;
+ public synthetic fun getOrPut (Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
+ public fun getOrPut (Lorg/mobilenativefoundation/store/core5/StoreKey;Lkotlin/jvm/functions/Function0;)Lorg/mobilenativefoundation/store/core5/StoreData;
+ public synthetic fun invalidate (Ljava/lang/Object;)V
+ public fun invalidate (Lorg/mobilenativefoundation/store/core5/StoreKey;)V
+ public fun invalidateAll ()V
+ public fun invalidateAll (Ljava/util/List;)V
+ public synthetic fun put (Ljava/lang/Object;Ljava/lang/Object;)V
+ public fun put (Lorg/mobilenativefoundation/store/core5/StoreKey;Lorg/mobilenativefoundation/store/core5/StoreData;)V
+ public fun putAll (Ljava/util/Map;)V
+ public fun size ()J
+}
+
+public final class org/mobilenativefoundation/store/cache5/StoreMultiCache$Companion {
+ public final fun invalidKeyErrorMessage (Ljava/lang/Object;)Ljava/lang/String;
+}
+
+public final class org/mobilenativefoundation/store/cache5/StoreMultiCacheAccessor {
+ public fun (Lorg/mobilenativefoundation/store/cache5/Cache;Lorg/mobilenativefoundation/store/cache5/Cache;)V
+ public final fun getAllPresent ()Ljava/util/Map;
+ public final fun getCollection (Lorg/mobilenativefoundation/store/core5/StoreKey$Collection;)Lorg/mobilenativefoundation/store/core5/StoreData$Collection;
+ public final fun getSingle (Lorg/mobilenativefoundation/store/core5/StoreKey$Single;)Lorg/mobilenativefoundation/store/core5/StoreData$Single;
+ public final fun invalidateAll ()V
+ public final fun invalidateCollection (Lorg/mobilenativefoundation/store/core5/StoreKey$Collection;)Z
+ public final fun invalidateSingle (Lorg/mobilenativefoundation/store/core5/StoreKey$Single;)Z
+ public final fun putCollection (Lorg/mobilenativefoundation/store/core5/StoreKey$Collection;Lorg/mobilenativefoundation/store/core5/StoreData$Collection;)Z
+ public final fun putSingle (Lorg/mobilenativefoundation/store/core5/StoreKey$Single;Lorg/mobilenativefoundation/store/core5/StoreData$Single;)Z
+ public final fun size ()J
+}
+
diff --git a/cache/build.gradle b/cache/build.gradle
deleted file mode 100644
index 762abda7c..000000000
--- a/cache/build.gradle
+++ /dev/null
@@ -1,17 +0,0 @@
-apply plugin: 'java'
-
-group = GROUP
-version = VERSION_NAME
-
-dependencies {
- compileOnly libraries.jsr305
-}
-
-buildscript {
- tasks.withType(JavaCompile) {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
- }
-}
-
-apply from: rootProject.file("gradle/maven-push.gradle")
diff --git a/cache/build.gradle.kts b/cache/build.gradle.kts
new file mode 100644
index 000000000..811b513df
--- /dev/null
+++ b/cache/build.gradle.kts
@@ -0,0 +1,21 @@
+plugins { id("org.mobilenativefoundation.store.multiplatform") }
+
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ api(libs.kotlinx.atomic.fu)
+ api(projects.core)
+ implementation(libs.kotlinx.coroutines.core)
+ }
+ }
+ val commonTest by getting {
+ dependencies {
+ implementation(libs.junit)
+ implementation(libs.kotlinx.coroutines.test)
+ }
+ }
+ }
+}
+
+android { namespace = "org.mobilenativefoundation.store.cache" }
diff --git a/cache/config/detekt/baseline.xml b/cache/config/detekt/baseline.xml
new file mode 100644
index 000000000..9ff4cde95
--- /dev/null
+++ b/cache/config/detekt/baseline.xml
@@ -0,0 +1,42 @@
+
+
+
+
+ EmptyFunctionBlock:LocalCache.kt$LocalCache.Companion.<no name provided>${}
+ EmptyFunctionBlock:LocalCache.kt$LocalCache.StrongValueReference${}
+ LongMethod:LocalCache.kt$LocalCache.Segment$fun put(key: K, hash: Int, value: V, onlyIfAbsent: Boolean): V?
+ LongParameterList:LocalCache.kt$LocalCache.Segment$( first: ReferenceEntry<K, V>, entry: ReferenceEntry<K, V>, key: K, hash: Int, valueReference: ValueReference<K, V>, cause: RemovalCause?, )
+ MagicNumber:CacheBuilder.kt$CacheBuilder$16
+ MagicNumber:CacheBuilder.kt$CacheBuilder$4
+ MagicNumber:LocalCache.kt$LocalCache$20
+ MagicNumber:LocalCache.kt$LocalCache$32
+ MagicNumber:LocalCache.kt$LocalCache.Companion$10
+ MagicNumber:LocalCache.kt$LocalCache.Companion$14
+ MagicNumber:LocalCache.kt$LocalCache.Companion$15
+ MagicNumber:LocalCache.kt$LocalCache.Companion$16
+ MagicNumber:LocalCache.kt$LocalCache.Companion$3
+ MagicNumber:LocalCache.kt$LocalCache.Companion$6
+ MagicNumber:LocalCache.kt$LocalCache.Segment$3
+ MagicNumber:LocalCache.kt$LocalCache.Segment$4
+ NestedBlockDepth:LocalCache.kt$LocalCache.Segment$fun activeEntries(): Map<K, V>
+ NestedBlockDepth:LocalCache.kt$LocalCache.Segment$fun clear()
+ NestedBlockDepth:LocalCache.kt$LocalCache.Segment$fun put(key: K, hash: Int, value: V, onlyIfAbsent: Boolean): V?
+ NestedBlockDepth:LocalCache.kt$LocalCache.Segment$fun remove(key: K, hash: Int): V?
+ NestedBlockDepth:LocalCache.kt$LocalCache.Segment$private fun expand()
+ ReturnCount:LocalCache.kt$LocalCache.Segment$fun get(key: K, hash: Int): V?
+ ReturnCount:LocalCache.kt$LocalCache.Segment$fun remove(key: K, hash: Int): V?
+ ReturnCount:LocalCache.kt$LocalCache.Segment$private fun getLiveEntry(key: K, hash: Int, now: Long): ReferenceEntry<K, V>?
+ TooManyFunctions:LocalCache.kt$LocalCache$Segment<K : Any, V : Any>
+ TooManyFunctions:LocalCache.kt$LocalCache<K : Any, V : Any>
+ TooManyFunctions:StoreMultiCache.kt$StoreMultiCache<Id : Any, Key : StoreKey<Id>, Single : StoreData.Single<Id>, Collection : StoreData.Collection<Id, Single>, Output : StoreData<Id>> : Cache
+ UnusedParameter:LocalCache.kt$LocalCache.Segment$cause: RemovalCause?
+ UnusedParameter:LocalCache.kt$LocalCache.Segment$hash: Int
+ UnusedParameter:LocalCache.kt$LocalCache.Segment$key: K?
+ UnusedPrivateMember:LocalCache.kt$LocalCache$private fun newValueReference( entry: ReferenceEntry<K, V>, value: V, weight: Int, ): ValueReference<K, V>
+ UseCheckOrError:CacheBuilder.kt$CacheBuilder$throw IllegalStateException("Maximum size cannot be combined with weigher.")
+ UseCheckOrError:LocalCache.kt$LocalCache.Segment$throw IllegalStateException("Weights must be non-negative")
+ UseRequire:CacheBuilder.kt$CacheBuilder$throw IllegalArgumentException("Duration must be non-negative.")
+ UseRequire:CacheBuilder.kt$CacheBuilder$throw IllegalArgumentException("Maximum size must be non-negative.")
+ UseRequire:CacheBuilder.kt$CacheBuilder$throw IllegalArgumentException("Maximum weight must be non-negative.")
+
+
diff --git a/cache/config/ktlint/baseline.xml b/cache/config/ktlint/baseline.xml
new file mode 100644
index 000000000..7d1ab2676
--- /dev/null
+++ b/cache/config/ktlint/baseline.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cache/gradle.properties b/cache/gradle.properties
index 93b6bc00b..ac546f2a1 100644
--- a/cache/gradle.properties
+++ b/cache/gradle.properties
@@ -1,3 +1,3 @@
-POM_NAME=com.nytimes.android
-POM_ARTIFACT_ID=cache3
-POM_PACKAGING=aar
+POM_NAME=org.mobilenativefoundation.store
+POM_ARTIFACT_ID=cache5
+POM_PACKAGING=jar
\ No newline at end of file
diff --git a/cache/src/androidMain/AndroidManifest.xml b/cache/src/androidMain/AndroidManifest.xml
new file mode 100644
index 000000000..8072ee00d
--- /dev/null
+++ b/cache/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/Cache.kt b/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/Cache.kt
new file mode 100644
index 000000000..8bc0202dc
--- /dev/null
+++ b/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/Cache.kt
@@ -0,0 +1,55 @@
+package org.mobilenativefoundation.store.cache5
+
+interface Cache {
+ /** @return [Value] associated with [key] or `null` if there is no cached value for [key]. */
+ fun getIfPresent(key: Key): Value?
+
+ /**
+ * @param [valueProducer] Must not return `null`. It may either return a non-null value or throw
+ * an exception.
+ * @return [Value] associated with [key], obtaining the value from [valueProducer] if necessary.
+ * No observable state associated with this cache is modified until loading completes.
+ * @throws ExecutionExeption If a checked exception was thrown while loading the value.
+ * @throws UncheckedExecutionException If an unchecked exception was thrown while loading the
+ * value.
+ * @throws ExecutionError If an error was thrown while loading the value.
+ */
+ fun getOrPut(key: Key, valueProducer: () -> Value): Value
+
+ /**
+ * @return Map of the [Value] associated with each [Key] in [keys]. Returned map only contains
+ * entries already present in the cache. The default implementation provided here throws a
+ * [NotImplementedError] to maintain backward compatibility for existing implementations.
+ */
+ fun getAllPresent(keys: List<*>): Map
+
+ /** @return Map of the [Value] associated with each [Key] in the cache. */
+ fun getAllPresent(): Map = throw NotImplementedError()
+
+ /**
+ * Associates [value] with [key]. If the cache previously contained a value associated with [key],
+ * the old value is replaced by [value]. Prefer [getOrPut] when using the conventional "If cached,
+ * then return. Otherwise create, cache, and then return" pattern.
+ */
+ fun put(key: Key, value: Value)
+
+ /**
+ * Copies all of the mappings from the specified map to the cache. The effect of this call is
+ * equivalent to that of calling [put] on this map once for each mapping from [Key] to [Value] in
+ * the specified map. The behavior of this operation is undefined if the specified map is modified
+ * while the operation is in progress.
+ */
+ fun putAll(map: Map)
+
+ /** Discards any cached value associated with [key]. */
+ fun invalidate(key: Key)
+
+ /** Discards any cached value associated for [keys]. */
+ fun invalidateAll(keys: List)
+
+ /** Discards all entries in the cache. */
+ fun invalidateAll()
+
+ /** @return Approximate number of entries in the cache. */
+ fun size(): Long
+}
diff --git a/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/CacheBuilder.kt b/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/CacheBuilder.kt
new file mode 100644
index 000000000..b915a2b0a
--- /dev/null
+++ b/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/CacheBuilder.kt
@@ -0,0 +1,75 @@
+package org.mobilenativefoundation.store.cache5
+
+import kotlin.time.Duration
+
+class CacheBuilder {
+ internal var concurrencyLevel = 4
+ private set
+
+ internal val initialCapacity = 16
+ internal var maximumSize = UNSET
+ private set
+
+ internal var maximumWeight = UNSET
+ private set
+
+ internal var expireAfterAccess: Duration = Duration.INFINITE
+ private set
+
+ internal var expireAfterWrite: Duration = Duration.INFINITE
+ private set
+
+ internal var weigher: Weigher? = null
+ private set
+
+ internal var ticker: Ticker? = null
+ private set
+
+ fun concurrencyLevel(producer: () -> Int): CacheBuilder = apply {
+ concurrencyLevel = producer.invoke()
+ }
+
+ fun maximumSize(maximumSize: Long): CacheBuilder = apply {
+ if (maximumSize < 0) {
+ throw IllegalArgumentException("Maximum size must be non-negative.")
+ }
+ this.maximumSize = maximumSize
+ }
+
+ fun expireAfterAccess(duration: Duration): CacheBuilder = apply {
+ if (duration.isNegative()) {
+ throw IllegalArgumentException("Duration must be non-negative.")
+ }
+ expireAfterAccess = duration
+ }
+
+ fun expireAfterWrite(duration: Duration): CacheBuilder = apply {
+ if (duration.isNegative()) {
+ throw IllegalArgumentException("Duration must be non-negative.")
+ }
+ expireAfterWrite = duration
+ }
+
+ fun ticker(ticker: Ticker): CacheBuilder = apply { this.ticker = ticker }
+
+ fun weigher(maximumWeight: Long, weigher: Weigher): CacheBuilder =
+ apply {
+ if (maximumWeight < 0) {
+ throw IllegalArgumentException("Maximum weight must be non-negative.")
+ }
+
+ this.maximumWeight = maximumWeight
+ this.weigher = weigher
+ }
+
+ fun build(): Cache {
+ if (maximumSize != -1L && weigher != null) {
+ throw IllegalStateException("Maximum size cannot be combined with weigher.")
+ }
+ return LocalCache.LocalManualCache(this)
+ }
+
+ companion object {
+ private const val UNSET = -1L
+ }
+}
diff --git a/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/LocalCache.kt b/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/LocalCache.kt
new file mode 100644
index 000000000..0be801964
--- /dev/null
+++ b/cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/LocalCache.kt
@@ -0,0 +1,1862 @@
+/*
+ * Copyright (C) 2009 The Guava Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * KMP conversion
+ * Copyright (C) 2022 André Claßen
+ */
+package org.mobilenativefoundation.store.cache5
+
+import kotlin.math.min
+import kotlin.time.Duration
+import kotlinx.atomicfu.AtomicArray
+import kotlinx.atomicfu.AtomicRef
+import kotlinx.atomicfu.atomic
+import kotlinx.atomicfu.atomicArrayOfNulls
+import kotlinx.atomicfu.locks.reentrantLock
+import kotlinx.atomicfu.loop
+
+internal class LocalCache(builder: CacheBuilder) {
+ /**
+ * Mask value for indexing into segments. The upper bits of a key's hash code are used to choose
+ * the segment.
+ */
+ private val segmentMask: Int
+
+ /**
+ * Shift value for indexing within segments. Helps prevent entries that end up in the same segment
+ * from also ending up in the same bucket.
+ */
+ private val segmentShift: Int
+
+ /** The segments, each of which is a specialized hash table. */
+ private val segments: Array?>
+
+ /** Strategy for referencing values. */
+ private val valueStrength: Strength = Strength.Strong
+
+ /** The maximum weight of this map. UNSET_LONG if there is no maximum. */
+ private val maxWeight: Long
+
+ /** Weigher to weigh cache entries. */
+ private val weigher: Weigher
+
+ /** How long after the last access to an entry the map will retain that entry. */
+ private val expireAfterAccessNanos: Long
+
+ /** How long after the last write to an entry the map will retain that entry. */
+ private val expireAfterWriteNanos: Long
+
+ /** Measures time in a testable way. */
+ private val ticker: Ticker
+
+ /** Factory used to create new entries. */
+ private val entryFactory: EntryFactory
+
+ private val evictsBySize: Boolean
+ get() = maxWeight >= 0
+
+ private val customWeigher: Boolean
+ get() = weigher !== OneWeigher
+
+ private val expiresAfterWrite: Boolean
+ get() = expireAfterWriteNanos > 0
+
+ private val expiresAfterAccess: Boolean
+ get() = expireAfterAccessNanos > 0
+
+ private val usesAccessQueue: Boolean
+ get() = expiresAfterAccess || evictsBySize
+
+ private val usesWriteQueue: Boolean
+ get() = expiresAfterWrite
+
+ private val recordsWrite: Boolean
+ get() = expiresAfterWrite
+
+ private val recordsAccess: Boolean
+ get() = expiresAfterAccess
+
+ private val recordsTime: Boolean
+ get() = recordsWrite || recordsAccess
+
+ private val usesWriteEntries: Boolean
+ get() = usesWriteQueue || recordsWrite
+
+ private val usesAccessEntries: Boolean
+ get() = usesAccessQueue || recordsAccess
+
+ private sealed class Strength {
+ /*
+ * TODO(kevinb): If we strongly reference the value and aren't loading, we needn't wrap the
+ * value. This could save ~8 bytes per entry.
+ */
+ object Strong : Strength() {
+ override fun referenceValue(
+ segment: Segment?,
+ entry: ReferenceEntry?,
+ value: V,
+ weight: Int,
+ ): ValueReference {
+ return if (weight == 1) {
+ StrongValueReference(value)
+ } else {
+ WeightedStrongValueReference(value, weight)
+ }
+ }
+ }
+
+ /** Creates a reference for the given value according to this value strength. */
+ abstract fun referenceValue(
+ segment: Segment?,
+ entry: ReferenceEntry?,
+ value: V,
+ weight: Int,
+ ): ValueReference
+ }
+
+ /** Creates new entries. */
+ private sealed class EntryFactory {
+ object Strong : EntryFactory() {
+ override fun newEntry(
+ segment: Segment?,
+ key: K,
+ hash: Int,
+ next: ReferenceEntry?,
+ ): ReferenceEntry {
+ return StrongEntry(key, hash, next)
+ }
+ }
+
+ object StrongAccess : EntryFactory() {
+ override fun newEntry(
+ segment: Segment?,
+ key: K,
+ hash: Int,
+ next: ReferenceEntry?,
+ ): ReferenceEntry {
+ return StrongAccessEntry(key, hash, next)
+ }
+
+ override fun copyEntry(
+ segment: Segment?,
+ original: ReferenceEntry,
+ newNext: ReferenceEntry?,
+ ): ReferenceEntry {
+ val newEntry = super.copyEntry(segment, original, newNext)
+ copyAccessEntry(original, newEntry)
+ return newEntry
+ }
+ }
+
+ object StrongWrite : EntryFactory() {
+ override fun newEntry(
+ segment: Segment?,
+ key: K,
+ hash: Int,
+ next: ReferenceEntry?,
+ ): ReferenceEntry {
+ return StrongWriteEntry(key, hash, next)
+ }
+
+ override fun copyEntry(
+ segment: Segment?,
+ original: ReferenceEntry,
+ newNext: ReferenceEntry?,
+ ): ReferenceEntry {
+ val newEntry = super.copyEntry(segment, original, newNext)
+ copyWriteEntry(original, newEntry)
+ return newEntry
+ }
+ }
+
+ object StrongAccessWrite : EntryFactory() {
+ override fun newEntry(
+ segment: Segment?,
+ key: K,
+ hash: Int,
+ next: ReferenceEntry?,
+ ): ReferenceEntry {
+ return StrongAccessWriteEntry(key, hash, next)
+ }
+
+ override fun copyEntry(
+ segment: Segment?,
+ original: ReferenceEntry,
+ newNext: ReferenceEntry?,
+ ): ReferenceEntry {
+ val newEntry = super.copyEntry(segment, original, newNext)
+ copyAccessEntry(original, newEntry)
+ copyWriteEntry(original, newEntry)
+ return newEntry
+ }
+ }
+
+ /**
+ * Creates a new entry.
+ *
+ * @param segment to create the entry for
+ * @param key of the entry
+ * @param hash of the key
+ * @param next entry in the same bucket
+ */
+ abstract fun newEntry(
+ segment: Segment?,
+ key: K,
+ hash: Int,
+ next: ReferenceEntry?,
+ ): ReferenceEntry
+
+ /**
+ * Copies an entry, assigning it a new `next` entry.
+ *
+ * @param original the entry to copy
+ * @param newNext entry in the same bucket
+ */
+ // Guarded By Segment.this
+ open fun copyEntry(
+ segment: Segment?,
+ original: ReferenceEntry,
+ newNext: ReferenceEntry?,
+ ): ReferenceEntry {
+ return newEntry(segment, original.key, original.hash, newNext)
+ }
+
+ // Guarded By Segment.this
+ fun copyAccessEntry(
+ original: ReferenceEntry,
+ newEntry: ReferenceEntry,
+ ) {
+ // TODO(fry): when we link values instead of entries this method can go
+ // away, as can connectAccessOrder, nullifyAccessOrder.
+ newEntry.accessTime = original.accessTime
+ connectAccessOrder(original.previousInAccessQueue, newEntry)
+ connectAccessOrder(newEntry, original.nextInAccessQueue)
+ nullifyAccessOrder(original)
+ }
+
+ // Guarded By Segment.this
+ fun copyWriteEntry(
+ original: ReferenceEntry,
+ newEntry: ReferenceEntry,
+ ) {
+ // TODO(fry): when we link values instead of entries this method can go
+ // away, as can connectWriteOrder, nullifyWriteOrder.
+ newEntry.writeTime = original.writeTime
+ connectWriteOrder(original.previousInWriteQueue, newEntry)
+ connectWriteOrder(newEntry, original.nextInWriteQueue)
+ nullifyWriteOrder(original)
+ }
+
+ companion object {
+ /** Masks used to compute indices in the following table. */
+ private const val ACCESS_MASK = 1
+ private const val WRITE_MASK = 2
+
+ /** Look-up table for factories. */
+ private val factories = arrayOf(Strong, StrongAccess, StrongWrite, StrongAccessWrite)
+
+ fun getFactory(usesAccessQueue: Boolean, usesWriteQueue: Boolean): EntryFactory {
+ val flags =
+ ((if (usesAccessQueue) ACCESS_MASK else 0) or if (usesWriteQueue) WRITE_MASK else 0)
+ return factories[flags]
+ }
+ }
+ }
+
+ /** A reference to a value. */
+ private interface ValueReference {
+ /** Returns the value. Does not block or throw exceptions. */
+ fun get(): V?
+
+ /** Returns the weight of this entry. This is assumed to be static between calls to setValue. */
+ val weight: Int
+
+ /**
+ * Returns the entry associated with this value reference, or `null` if this value reference is
+ * independent of any entry.
+ */
+ val entry: ReferenceEntry?
+
+ /**
+ * Creates a copy of this reference for the given entry.
+ *
+ * `value` may be null only for a loading reference.
+ */
+ fun copyFor(value: V?, entry: ReferenceEntry?): ValueReference
+
+ /**
+ * Notifify pending loads that a new value was set. This is only relevant to loading value
+ * references.
+ */
+ fun notifyNewValue(newValue: V)
+
+ /**
+ * Returns true if this reference contains an active value, meaning one that is still considered
+ * present in the cache. Active values consist of live values, which are returned by cache
+ * lookups, and dead values, which have been evicted but awaiting removal. Non-active values
+ * consist strictly of loading values, though during refresh a value may be both active and
+ * loading.
+ */
+ val isActive: Boolean
+ }
+
+ /**
+ * An entry in a reference map.
+ *
+ * Entries in the map can be in the following states:
+ *
+ * Valid:
+ * - Live: valid key/value are set
+ * - Loading: loading is pending
+ *
+ * Invalid:
+ * - Expired: time expired (key/value may still be set)
+ * - Collected: key/value was partially collected, but not yet cleaned up
+ * - Unset: marked as unset, awaiting cleanup or reuse
+ */
+ private interface ReferenceEntry {
+ /** Returns the value reference from this entry. */
+ /** Sets the value reference for this entry. */
+ var valueReference: ValueReference?
+ get() = throw UnsupportedOperationException()
+ set(_) = throw UnsupportedOperationException()
+
+ /** Returns the next entry in the chain. */
+ val next: ReferenceEntry?
+ get() = throw UnsupportedOperationException()
+
+ /** Returns the entry's hash. */
+ val hash: Int
+ get() = throw UnsupportedOperationException()
+
+ /** Returns the key for this entry. */
+ val key: K
+ get() = throw UnsupportedOperationException()
+
+ /*
+ * Used by entries that use access order. Access entries are maintained in a doubly-linked list.
+ * New entries are added at the tail of the list at write time; stale entries are expired from
+ * the head of the list.
+ */
+ /** Returns the time that this entry was last accessed, in ns. */
+ /** Sets the entry access time in ns. */
+ var accessTime: Long
+ get() = throw UnsupportedOperationException()
+ set(_) = throw UnsupportedOperationException()
+
+ /** Returns the next entry in the access queue. */
+ /** Sets the next entry in the access queue. */
+ var nextInAccessQueue: ReferenceEntry
+ get() = throw UnsupportedOperationException()
+ set(_) = throw UnsupportedOperationException()
+
+ /** Returns the previous entry in the access queue. */
+ /** Sets the previous entry in the access queue. */
+ var previousInAccessQueue: ReferenceEntry
+ get() = throw UnsupportedOperationException()
+ set(_) = throw UnsupportedOperationException()
+
+ /*
+ * Implemented by entries that use write order. Write entries are maintained in a
+ * doubly-linked list. New entries are added at the tail of the list at write time and stale
+ * entries are expired from the head of the list.
+ */
+ /** Returns the time that this entry was last written, in ns. */
+ /** Sets the entry write time in ns. */
+ var writeTime: Long
+ get() = throw UnsupportedOperationException()
+ set(_) = throw UnsupportedOperationException()
+
+ /** Returns the next entry in the write queue. */
+ /** Sets the next entry in the write queue. */
+ var nextInWriteQueue: ReferenceEntry
+ get() = throw UnsupportedOperationException()
+ set(_) = throw UnsupportedOperationException()
+
+ /** Returns the previous entry in the write queue. */
+ /** Sets the previous entry in the write queue. */
+ var previousInWriteQueue: ReferenceEntry
+ get() = throw UnsupportedOperationException()
+ set(_) = throw UnsupportedOperationException()
+ }
+
+ private object NullEntry : ReferenceEntry {
+ override var valueReference: ValueReference?
+ get() = null
+ set(_) {}
+
+ override val next: ReferenceEntry?
+ get() = null
+
+ override val hash: Int
+ get() = 0
+
+ override val key: Any
+ get() = Unit
+
+ override var accessTime: Long
+ get() = 0
+ set(_) {}
+
+ override var nextInAccessQueue: ReferenceEntry
+ get() = this
+ set(_) {}
+
+ override var previousInAccessQueue: ReferenceEntry
+ get() = this
+ set(_) {}
+
+ override var writeTime: Long
+ get() = 0
+ set(_) {}
+
+ override var nextInWriteQueue: ReferenceEntry
+ get() = this
+ set(_) {}
+
+ override var previousInWriteQueue: ReferenceEntry
+ get() = this
+ set(_) {}
+ }
+
+ /*
+ * Note: All of this duplicate code sucks, but it saves a lot of memory. If only Java had mixins!
+ * To maintain this code, make a change for the strong reference type. Then, cut and paste, and
+ * replace "Strong" with "Soft" or "Weak" within the pasted text. The primary difference is that
+ * strong entries store the key reference directly while soft and weak entries delegate to their
+ * respective superclasses.
+ */
+
+ /** Used for strongly-referenced keys. */
+ private open class StrongEntry(
+ override val key: K, // The code below is exactly the same for each entry type.
+ override val hash: Int,
+ override val next: ReferenceEntry?,
+ ) : ReferenceEntry {
+ private val _valueReference = atomic?>(unset())
+ override var valueReference: ValueReference? = _valueReference.value
+ }
+
+ private class StrongAccessEntry(
+ key: K,
+ hash: Int,
+ next: ReferenceEntry?,
+ ) : StrongEntry(key, hash, next) {
+ // The code below is exactly the same for each access entry type.
+
+ private val _accessTime = atomic(Long.MAX_VALUE)
+ override var accessTime = _accessTime.value
+
+ // Guarded By Segment.this
+ override var nextInAccessQueue: ReferenceEntry = nullEntry()
+
+ // Guarded By Segment.this
+ override var previousInAccessQueue: ReferenceEntry = nullEntry()
+ }
+
+ private class StrongWriteEntry(key: K, hash: Int, next: ReferenceEntry?) :
+ StrongEntry(key, hash, next) {
+ // The code below is exactly the same for each write entry type.
+ private val _writeTime = atomic(Long.MAX_VALUE)
+ override var writeTime = _writeTime.value
+
+ // Guarded By Segment.this
+ override var nextInWriteQueue: ReferenceEntry = nullEntry()
+
+ // Guarded By Segment.this
+ override var previousInWriteQueue: ReferenceEntry = nullEntry()
+ }
+
+ private class StrongAccessWriteEntry(
+ key: K,
+ hash: Int,
+ next: ReferenceEntry?,
+ ) : StrongEntry(key, hash, next) {
+ // The code below is exactly the same for each access entry type.
+ private val _accessTime = atomic(Long.MAX_VALUE)
+ override var accessTime: Long = _accessTime.value
+
+ // Guarded By Segment.this
+ override var nextInAccessQueue: ReferenceEntry = nullEntry()
+
+ // Guarded By Segment.this
+ override var previousInAccessQueue: ReferenceEntry = nullEntry()
+
+ // The code below is exactly the same for each write entry type.
+ private val _writeTime = atomic(Long.MAX_VALUE)
+ override var writeTime: Long = _writeTime.value
+
+ // Guarded By Segment.this
+ override var nextInWriteQueue: ReferenceEntry = nullEntry()
+
+ // Guarded By Segment.this
+ override var previousInWriteQueue: ReferenceEntry = nullEntry()
+ }
+
+ /** References a strong value. */
+ private open class StrongValueReference(private val referent: V) :
+ ValueReference {
+ override fun get(): V = referent
+
+ override val weight: Int = 1
+ override val entry: ReferenceEntry? = null
+
+ override fun copyFor(value: V?, entry: ReferenceEntry?): ValueReference = this
+
+ override val isActive: Boolean = true
+
+ override fun notifyNewValue(newValue: V) {}
+ }
+
+ /** References a strong value. */
+ private class WeightedStrongValueReference(
+ referent: V,
+ override val weight: Int,
+ ) : StrongValueReference(referent)
+
+ /** This method is a convenience for testing. Code should call [Segment.newEntry] directly. */
+ private fun newEntry(key: K, hash: Int, next: ReferenceEntry?): ReferenceEntry {
+ val segment = segmentFor(hash)
+ segment.reentrantLock.lock()
+ return try {
+ segment.newEntry(key, hash, next)
+ } finally {
+ segment.reentrantLock.unlock()
+ }
+ }
+
+ /** This method is a convenience for testing. Code should call [Segment.copyEntry] directly. */
+ // Guarded By Segment.this
+ private fun copyEntry(
+ original: ReferenceEntry,
+ newNext: ReferenceEntry?,
+ ): ReferenceEntry? {
+ val hash = original.hash
+ return segmentFor(hash).copyEntry(original, newNext)
+ }
+
+ /** This method is a convenience for testing. Code should call [Segment.setValue] instead. */
+ // Guarded By Segment.this
+ private fun newValueReference(
+ entry: ReferenceEntry,
+ value: V,
+ weight: Int,
+ ): ValueReference {
+ val hash = entry.hash
+ return valueStrength.referenceValue(segmentFor(hash), entry, value, weight)
+ }
+
+ private fun hash(key: K): Int = rehash(key.hashCode())
+
+ /**
+ * Returns the segment that should be used for a key with the given hash.
+ *
+ * @param hash the hash code for the key
+ * @return the segment
+ */
+ private fun segmentFor(hash: Int): Segment =
+ // TODO(fry): Lazily create segments?
+ segments[hash ushr segmentShift and segmentMask] as Segment
+
+ private fun createSegment(initialCapacity: Int, maxSegmentWeight: Long): Segment =
+ Segment(this, initialCapacity, maxSegmentWeight)
+
+ // expiration
+
+ /** Returns true if the entry has expired. */
+ private fun isExpired(entry: ReferenceEntry, now: Long): Boolean =
+ if (expiresAfterAccess && now - entry.accessTime >= expireAfterAccessNanos) {
+ true
+ } else {
+ expiresAfterWrite && now - entry.writeTime >= expireAfterWriteNanos
+ }
+
+ // Inner Classes
+
+ private class SegmentTable(val size: Int) {
+ private val table: AtomicArray?> = atomicArrayOfNulls(size)
+
+ operator fun get(idx: Int) = table[idx].value
+
+ operator fun set(idx: Int, value: ReferenceEntry?) {
+ table[idx].value = value
+ }
+ }
+
+ /** Segments are specialized versions of hash tables. */
+ private class Segment(
+ private val map: LocalCache,
+ initialCapacity: Int,
+ private val maxSegmentWeight: Long,
+ ) {
+ /*
+ * TODO(fry): Consider copying variables (like evictsBySize) from outer class into this class.
+ * It will require more memory but will reduce indirection.
+ */
+ /*
+ * Segments maintain a table of entry lists that are ALWAYS kept in a consistent state, so can
+ * be read without locking. Next fields of nodes are immutable (final). All list additions are
+ * performed at the front of each bin. This makes it easy to check changes, and also fast to
+ * traverse. When nodes would otherwise be changed, new nodes are created to replace them. This
+ * works well for hash tables since the bin lists tend to be short. (The average length is less
+ * than two.)
+ *
+ * Read operations can thus proceed without locking, but rely on selected uses of volatiles to
+ * ensure that completed write operations performed by other threads are noticed. For most
+ * purposes, the "count" field, tracking the number of elements, serves as that volatile
+ * variable ensuring visibility. This is convenient because this field needs to be read in many
+ * read operations anyway:
+ *
+ * - All (unsynchronized) read operations must first read the "count" field, and should not
+ * look at table entries if it is 0.
+ *
+ * - All (synchronized) write operations should write to the "count" field after structurally
+ * changing any bin. The operations must not take any action that could even momentarily
+ * cause a concurrent read operation to see inconsistent data. This is made easier by the
+ * nature of the read operations in Map. For example, no operation can reveal that the table
+ * has grown but the threshold has not yet been updated, so there are no atomicity requirements
+ * for this with respect to reads.
+ *
+ * As a guide, all critical volatile reads and writes to the count field are marked in code
+ * comments.
+ */
+
+ val reentrantLock = reentrantLock()
+
+ /** The number of live elements in this segment's region. */
+ private val count = atomic(0)
+
+ /** The weight of the live elements in this segment's region. */
+ private var totalWeight: Long = 0
+
+ /**
+ * Number of updates that alter the size of the table. This is used during bulk-read methods to
+ * make sure they see a consistent snapshot: If modCounts change during a traversal of segments
+ * loading size or checking containsValue, then we might have an inconsistent view of state so
+ * (usually) must retry.
+ */
+ private var modCount = 0
+
+ /**
+ * The table is expanded when its size exceeds this threshold. (The value of this field is
+ * always `(int) (capacity * 0.75)`.)
+ */
+ private var threshold = 0
+
+ /** The per-segment table. */
+ private val table: AtomicRef>
+
+ /**
+ * The recency queue is used to record which entries were accessed for updating the access
+ * list's ordering. It is drained as a batch operation when either the DRAIN_THRESHOLD is
+ * crossed or a write occurs on the segment.
+ */
+ private val recencyQueue: Queue>
+
+ /**
+ * A counter of the number of reads since the last write, used to drain queues on a small
+ * fraction of read operations.
+ */
+ private val readCount = atomic(0)
+
+ /**
+ * A queue of elements currently in the map, ordered by write time. Elements are added to the
+ * tail of the queue on write.
+ */
+ private val writeQueue: MutableQueue>
+
+ /**
+ * A queue of elements currently in the map, ordered by access time. Elements are added to the
+ * tail of the queue on access (note that writes count as accesses).
+ */
+ private val accessQueue: MutableQueue>
+
+ fun newEntry(key: K, hash: Int, next: ReferenceEntry?): ReferenceEntry =
+ map.entryFactory.newEntry(this, key, hash, next)
+
+ /**
+ * Copies `original` into a new entry chained to `newNext`. Returns the new entry, or `null` if
+ * `original` was already garbage collected.
+ */
+ fun copyEntry(
+ original: ReferenceEntry,
+ newNext: ReferenceEntry?,
+ ): ReferenceEntry? {
+ val valueReference = original.valueReference
+ val value = valueReference!!.get()
+ if (value == null && valueReference.isActive) {
+ // value collected
+ return null
+ }
+ val newEntry = map.entryFactory.copyEntry(this, original, newNext)
+ newEntry.valueReference = valueReference.copyFor(value, newEntry)
+ return newEntry
+ }
+
+ /** Sets a new value of an entry. Adds newly created entries at the end of the access queue. */
+ fun setValue(entry: ReferenceEntry, key: K, value: V, now: Long) {
+ val previous = entry.valueReference
+ val weight = map.weigher(key, value)
+ if (weight < 0) throw IllegalStateException("Weights must be non-negative")
+ entry.valueReference = map.valueStrength.referenceValue(this, entry, value, weight)
+ recordWrite(entry, weight, now)
+ previous?.notifyNewValue(value)
+ }
+
+ // recency queue, shared by expiration and eviction
+
+ /**
+ * Records the relative order in which this read was performed by adding `entry` to the recency
+ * queue. At write-time, or when the queue is full past the threshold, the queue will be drained
+ * and the entries therein processed.
+ *
+ * Note: locked reads should use [.recordLockedRead].
+ */
+ private fun recordRead(entry: ReferenceEntry, now: Long) {
+ if (map.recordsAccess) {
+ entry.accessTime = now
+ }
+ recencyQueue.add(entry)
+ }
+
+ /**
+ * Updates the eviction metadata that `entry` was just read. This currently amounts to adding
+ * `entry` to relevant eviction lists.
+ *
+ * Note: this method should only be called under lock, as it directly manipulates the eviction
+ * queues. Unlocked reads should use [.recordRead].
+ */
+ private fun recordLockedRead(entry: ReferenceEntry, now: Long) {
+ if (map.recordsAccess) {
+ entry.accessTime = now
+ }
+ accessQueue.add(entry)
+ }
+
+ /**
+ * Updates eviction metadata that `entry` was just written. This currently amounts to adding
+ * `entry` to relevant eviction lists.
+ */
+ private fun recordWrite(entry: ReferenceEntry, weight: Int, now: Long) {
+ // we are already under lock, so drain the recency queue immediately
+ drainRecencyQueue()
+ totalWeight += weight.toLong()
+ if (map.recordsAccess) {
+ entry.accessTime = now
+ }
+ if (map.recordsWrite) {
+ entry.writeTime = now
+ }
+ accessQueue.add(entry)
+ writeQueue.add(entry)
+ }
+
+ /**
+ * Drains the recency queue, updating eviction metadata that the entries therein were read in
+ * the specified relative order. This currently amounts to adding them to relevant eviction
+ * lists (accounting for the fact that they could have been removed from the map since being
+ * added to the recency queue).
+ */
+ private fun drainRecencyQueue() {
+ while (true) {
+ val e = recencyQueue.poll() ?: break
+ // An entry may be in the recency queue despite it being removed from
+ // the map . This can occur when the entry was concurrently read while a
+ // writer is removing it from the segment or after a clear has removed
+ // all of the segment's entries.
+ if (accessQueue.contains(e)) {
+ accessQueue.add(e)
+ }
+ }
+ }
+
+ // expiration
+
+ /** Cleanup expired entries when the lock is available. */
+ private fun tryExpireEntries(now: Long) {
+ if (reentrantLock.tryLock()) {
+ try {
+ expireEntries(now)
+ } finally {
+ reentrantLock.unlock()
+ // don't call postWriteCleanup as we're in a read
+ }
+ }
+ }
+
+ private fun expireEntries(now: Long) {
+ drainRecencyQueue()
+ while (true) {
+ val e = writeQueue.peek()?.takeIf { map.isExpired(it, now) } ?: break
+ if (!removeEntry(e, e.hash, RemovalCause.EXPIRED)) {
+ throw AssertionError()
+ }
+ }
+
+ while (true) {
+ val e = accessQueue.peek()?.takeIf { map.isExpired(it, now) } ?: break
+ if (!removeEntry(e, e.hash, RemovalCause.EXPIRED)) {
+ throw AssertionError()
+ }
+ }
+ }
+
+ // eviction
+ private fun enqueueNotification(entry: ReferenceEntry, cause: RemovalCause?) {
+ enqueueNotification(entry.key, entry.hash, entry.valueReference, cause)
+ }
+
+ private fun enqueueNotification(
+ key: K?,
+ hash: Int,
+ valueReference: ValueReference?,
+ cause: RemovalCause?,
+ ) {
+ valueReference?.weight?.toLong()?.apply { totalWeight -= this }
+ }
+
+ /**
+ * Performs eviction if the segment is over capacity. Avoids flushing the entire cache if the
+ * newest entry exceeds the maximum weight all on its own.
+ *
+ * @param newest the most recently added entry
+ */
+ private fun evictEntries(newest: ReferenceEntry) {
+ if (!map.evictsBySize) {
+ return
+ }
+ drainRecencyQueue()
+
+ // If the newest entry by itself is too heavy for the segment, don't bother evicting
+ // anything else, just that
+ if (newest.valueReference!!.weight > maxSegmentWeight) {
+ if (!removeEntry(newest, newest.hash, RemovalCause.SIZE)) {
+ throw AssertionError()
+ }
+ }
+ while (totalWeight > maxSegmentWeight) {
+ val e = nextEvictable
+ if (!removeEntry(e, e.hash, RemovalCause.SIZE)) {
+ throw AssertionError()
+ }
+ }
+ }
+
+ // TODO(fry): instead implement this with an eviction head
+
+ private val nextEvictable: ReferenceEntry
+ get() {
+ for (e in accessQueue) {
+ val weight = e.valueReference!!.weight
+ if (weight > 0) {
+ return e
+ }
+ }
+ throw AssertionError()
+ }
+
+ /** Returns first entry of bin for given hash. */
+ private fun getFirst(hash: Int): ReferenceEntry? {
+ // read this volatile field only once
+ val table = table.value
+ return table[hash and table.size - 1]
+ }
+
+ // Specialized implementations of map methods
+ private fun getEntry(key: K, hash: Int): ReferenceEntry? {
+ var e = getFirst(hash)
+ while (e != null) {
+ if (e.hash != hash) {
+ e = e.next
+ continue
+ }
+ val entryKey = e.key
+ if (key == entryKey) {
+ return e
+ }
+ e = e.next
+ }
+ return null
+ }
+
+ private fun getLiveEntry(key: K, hash: Int, now: Long): ReferenceEntry? {
+ val e = getEntry(key, hash)
+ if (e == null) {
+ return null
+ } else if (map.isExpired(e, now)) {
+ tryExpireEntries(now)
+ return null
+ }
+ return e
+ }
+
+ /**
+ * Gets the value from an entry. Returns null if the entry is invalid, partially-collected,
+ * loading, or expired.
+ */
+ fun get(key: K, hash: Int): V? {
+ return try {
+ if (count.value != 0) { // read-volatile
+ val now = map.ticker()
+ val e = getLiveEntry(key, hash, now) ?: return null
+ val value = e.valueReference?.get()
+ if (value != null) {
+ recordRead(e, now)
+ return value
+ }
+ }
+ null
+ } finally {
+ postReadCleanup()
+ }
+ }
+
+ fun getOrPut(key: K, hash: Int, defaultValue: () -> V): V {
+ reentrantLock.lock()
+ return try {
+ if (count.value != 0) { // read-volatile
+ val now = map.ticker()
+ val e = getLiveEntry(key, hash, now)
+ val value = e?.valueReference?.get()
+ if (value != null) {
+ recordRead(e, now)
+ return value
+ }
+ }
+ val default = defaultValue()
+ put(key, hash, default, false)
+ default
+ } finally {
+ reentrantLock.unlock()
+ postReadCleanup()
+ }
+ }
+
+ fun put(key: K, hash: Int, value: V, onlyIfAbsent: Boolean): V? {
+ reentrantLock.lock()
+ return try {
+ val now = map.ticker()
+ preWriteCleanup(now)
+ if (count.value + 1 > threshold) { // ensure capacity
+ expand()
+ }
+ val table = table.value
+ val index = hash and table.size - 1
+ val first = table[index]
+
+ // Look for an existing entry.
+ var e: ReferenceEntry? = first
+ while (e != null) {
+ val entryKey = e.key
+ if (e.hash == hash && key == entryKey) {
+ // We found an existing entry.
+ val valueReference = e.valueReference
+ val entryValue = valueReference!!.get()
+ return when {
+ entryValue == null -> {
+ ++modCount
+ val newCount =
+ if (valueReference.isActive) {
+ enqueueNotification(key, hash, valueReference, RemovalCause.COLLECTED)
+ setValue(e, key, value, now)
+ count.value // count remains unchanged
+ } else {
+ setValue(e, key, value, now)
+ count.value + 1
+ }
+ count.value = newCount // write-volatile
+ evictEntries(e)
+ null
+ }
+
+ onlyIfAbsent -> {
+ // Mimic
+ // "if (!map.containsKey(key)) ...
+ // else return map.get(key);
+ recordLockedRead(e, now)
+ entryValue
+ }
+
+ else -> {
+ // clobber existing entry, count remains unchanged
+ ++modCount
+ enqueueNotification(key, hash, valueReference, RemovalCause.REPLACED)
+ setValue(e, key, value, now)
+ evictEntries(e)
+ entryValue
+ }
+ }
+ }
+ e = e.next
+ }
+
+ // Create a new entry.
+ ++modCount
+ val newEntry = newEntry(key, hash, first)
+ setValue(newEntry, key, value, now)
+ table[index] = newEntry
+ count.plusAssign(1)
+ evictEntries(newEntry)
+ null
+ } finally {
+ reentrantLock.unlock()
+ postWriteCleanup()
+ }
+ }
+
+ fun remove(key: K, hash: Int): V? {
+ reentrantLock.lock()
+ return try {
+ val now = map.ticker()
+ preWriteCleanup(now)
+ val table = table.value
+ val index = hash and table.size - 1
+ val first = table[index]
+ var e = first
+ while (e != null) {
+ val entryKey = e.key
+ if (e.hash == hash && key == entryKey) {
+ val valueReference = e.valueReference
+ val entryValue = valueReference!!.get()
+ val cause: RemovalCause =
+ when {
+ entryValue != null -> {
+ RemovalCause.EXPLICIT
+ }
+
+ valueReference.isActive -> {
+ RemovalCause.COLLECTED
+ }
+
+ else -> {
+ // currently loading
+ return null
+ }
+ }
+ ++modCount
+ val newFirst = removeValueFromChain(first!!, e, entryKey, hash, valueReference, cause)
+ val newCount = count.value - 1
+ table[index] = newFirst
+ count.value = newCount // write-volatile
+ return entryValue
+ }
+ e = e.next
+ }
+ null
+ } finally {
+ reentrantLock.unlock()
+ postWriteCleanup()
+ }
+ }
+
+ fun clear() {
+ if (count.value != 0) { // read-volatile
+ reentrantLock.lock()
+ try {
+ val table = table.value
+ for (i in 0 until table.size) {
+ var e = table[i]
+ while (e != null) {
+ // Loading references aren't actually in the map yet.
+ if (e.valueReference!!.isActive) {
+ enqueueNotification(e, RemovalCause.EXPLICIT)
+ }
+ e = e.next
+ }
+ }
+ for (i in 0 until table.size) {
+ table[i] = null
+ }
+ writeQueue.clear()
+ accessQueue.clear()
+ readCount.value = 0
+ ++modCount
+ count.value = 0 // write-volatile
+ } finally {
+ reentrantLock.unlock()
+ postWriteCleanup()
+ }
+ }
+ }
+
+ /** Expands the table if possible. */
+ private fun expand() {
+ val oldTable = table.value
+ val oldCapacity = oldTable.size
+ if (oldCapacity >= MAXIMUM_CAPACITY) {
+ return
+ }
+
+ /*
+ * Reclassify nodes in each list to new Map. Because we are using power-of-two expansion, the
+ * elements from each bin must either stay at same index, or move with a power of two offset.
+ * We eliminate unnecessary node creation by catching cases where old nodes can be reused
+ * because their next fields won't change. Statistically, at the default threshold, only
+ * about one-sixth of them need cloning when a table doubles. The nodes they replace will be
+ * garbage collectable as soon as they are no longer referenced by any reader thread that may
+ * be in the midst of traversing table right now.
+ */
+ var newCount = count.value
+ val newTable = SegmentTable(oldCapacity shl 1)
+ threshold = newTable.size * 3 / 4
+ val newMask = newTable.size - 1
+ for (oldIndex in 0 until oldCapacity) {
+ // We need to guarantee that any existing reads of old Map can
+ // proceed. So we cannot yet null out each bin.
+ val head = oldTable[oldIndex] ?: continue
+
+ val next = head.next
+ val headIndex = head.hash and newMask
+
+ // Single node on list
+ if (next == null) {
+ newTable[headIndex] = head
+ } else {
+ // Reuse the consecutive sequence of nodes with the same target
+ // index from the end of the list. tail points to the first
+ // entry in the reusable list.
+ var tail = head
+ var tailIndex = headIndex
+ var entry = next
+ while (entry != null) {
+ val newIndex = entry.hash and newMask
+ if (newIndex != tailIndex) {
+ // The index changed. We'll need to copy the previous entry.
+ tailIndex = newIndex
+ tail = entry
+ }
+ entry = entry.next
+ }
+ newTable[tailIndex] = tail
+
+ // Clone nodes leading up to the tail.
+ var headEntry = head
+ while (headEntry !== tail) {
+ val newIndex = headEntry.hash and newMask
+ val newNext = newTable[newIndex]
+ val newFirst = copyEntry(headEntry, newNext)
+ if (newFirst != null) {
+ newTable[newIndex] = newFirst
+ } else {
+ removeCollectedEntry(headEntry)
+ newCount--
+ }
+ headEntry = headEntry.next ?: break
+ }
+ }
+ }
+ table.value = newTable
+ count.value = newCount
+ }
+
+ private fun removeValueFromChain(
+ first: ReferenceEntry,
+ entry: ReferenceEntry,
+ key: K,
+ hash: Int,
+ valueReference: ValueReference,
+ cause: RemovalCause?,
+ ): ReferenceEntry? {
+ enqueueNotification(key, hash, valueReference, cause)
+ writeQueue.remove(entry)
+ accessQueue.remove(entry)
+ return removeEntryFromChain(first, entry)
+ }
+
+ private fun removeEntryFromChain(
+ first: ReferenceEntry,
+ entry: ReferenceEntry,
+ ): ReferenceEntry? {
+ var newCount = count.value
+ var newFirst = entry.next
+ var e = first
+ while (e !== entry) {
+ val next = copyEntry(e, newFirst)
+ if (next != null) {
+ newFirst = next
+ } else {
+ removeCollectedEntry(e)
+ newCount--
+ }
+ e = e.next ?: break
+ }
+ count.value = newCount
+ return newFirst
+ }
+
+ private fun removeCollectedEntry(entry: ReferenceEntry) {
+ enqueueNotification(entry, RemovalCause.COLLECTED)
+ writeQueue.remove(entry)
+ accessQueue.remove(entry)
+ }
+
+ private fun removeEntry(entry: ReferenceEntry, hash: Int, cause: RemovalCause?): Boolean {
+ val table = table.value
+ val index = hash and table.size - 1
+ val first = table[index]
+ var e = first
+
+ while (e != null) {
+ if (e === entry) {
+ ++modCount
+ val newFirst = removeValueFromChain(first!!, e, e.key, hash, e.valueReference!!, cause)
+ val newCount = count.value - 1
+ table[index] = newFirst
+ count.value = newCount // write-volatile
+ return true
+ }
+ e = e.next
+ }
+ return false
+ }
+
+ /**
+ * Performs routine cleanup following a read. Normally cleanup happens during writes. If cleanup
+ * is not observed after a sufficient number of reads, try cleaning up from the read thread.
+ */
+ private fun postReadCleanup() {
+ if (readCount.incrementAndGet() and DRAIN_THRESHOLD == 0) {
+ cleanUp()
+ }
+ }
+
+ /**
+ * Performs routine cleanup prior to executing a write. This should be called every time a write
+ * thread acquires the segment lock, immediately after acquiring the lock.
+ *
+ * Post-condition: expireEntries has been run.
+ */
+ private fun preWriteCleanup(now: Long) {
+ runLockedCleanup(now)
+ }
+
+ /** Performs routine cleanup following a write. */
+ private fun postWriteCleanup() {
+ runUnlockedCleanup()
+ }
+
+ fun cleanUp() {
+ val now = map.ticker()
+ runLockedCleanup(now)
+ runUnlockedCleanup()
+ }
+
+ private fun runLockedCleanup(now: Long) {
+ if (reentrantLock.tryLock()) {
+ try {
+ expireEntries(now) // calls drainRecencyQueue
+ readCount.value = 0
+ } finally {
+ reentrantLock.unlock()
+ }
+ }
+ }
+
+ private fun runUnlockedCleanup() {
+ // locked cleanup may generate notifications we can send unlocked
+ /*if (!isHeldByCurrentThread) {
+ map.processPendingNotifications()
+ }*/
+ }
+
+ fun activeEntries(): Map {
+ // read-volatile
+ if (count.value == 0) return emptyMap()
+ reentrantLock.lock()
+ return try {
+ val activeMap = mutableMapOf()
+ val table = table.value
+ for (i in 0 until table.size) {
+ var e = table[i]
+ while (e != null) {
+ if (e.valueReference?.isActive == true) {
+ activeMap[e.key] = e.valueReference?.get()!!
+ }
+ e = e.next
+ }
+ }
+ activeMap.ifEmpty { emptyMap() }
+ } finally {
+ reentrantLock.unlock()
+ }
+ }
+
+ init {
+ threshold = initialCapacity * 3 / 4 // 0.75
+ if (!map.customWeigher && threshold.toLong() == maxSegmentWeight) {
+ // prevent spurious expansion before eviction
+ threshold++
+ }
+ table = atomic(SegmentTable(initialCapacity))
+ recencyQueue = if (map.usesAccessQueue) AtomicLinkedQueue() else discardingQueue()
+ writeQueue = if (map.usesWriteQueue) WriteQueue() else discardingQueue()
+ accessQueue = if (map.usesAccessQueue) AccessQueue() else discardingQueue()
+ }
+ }
+
+ // Queues
+
+ private interface Queue {
+ fun poll(): T?
+
+ fun add(value: T)
+ }
+
+ private interface MutableQueue : Queue, Iterable {
+ fun peek(): E?
+
+ fun isEmpty(): Boolean
+
+ val size: Int
+
+ fun clear()
+
+ fun remove(element: E): Boolean
+
+ fun contains(element: E): Boolean
+ }
+
+ private class AtomicLinkedQueue : Queue {
+ private val head: AtomicRef> = atomic(Node(null))
+ private val tail: AtomicRef> = atomic(head.value)
+
+ private class Node(val value: T) {
+ val next = atomic