mirror of
https://github.com/Athou/commafeed.git
synced 2026-03-21 21:37:29 +00:00
Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc76d7e609 | ||
|
|
1b24bf33ed | ||
|
|
58ff378735 | ||
|
|
1cee04a233 | ||
|
|
ac11a0efb8 | ||
|
|
f2ea1e3f7a | ||
|
|
153970c146 | ||
|
|
c21287e642 | ||
|
|
284942a82e | ||
|
|
e796916e73 | ||
|
|
004db1762c | ||
|
|
8e05ba8820 | ||
|
|
f4f386b5e5 | ||
|
|
ecbf8bec23 | ||
|
|
5d8c09ccda | ||
|
|
47c5c3d8a0 | ||
|
|
3ddce16d5b | ||
|
|
acefdc44d9 | ||
|
|
d05c5b9d7f | ||
|
|
5d1237d1f4 | ||
|
|
6bac8631ac | ||
|
|
2c803327ad | ||
|
|
451ae5bc51 | ||
|
|
f41ce8c878 | ||
|
|
177c54c813 | ||
|
|
3765e32fd4 | ||
|
|
aab7a16d18 | ||
|
|
f2463af63c | ||
|
|
212c2c3b56 | ||
|
|
3ab00b0cdd | ||
|
|
fad85e9299 | ||
|
|
3cd5e203e2 | ||
|
|
7b081fa870 | ||
|
|
5ce22051d4 | ||
|
|
1e59489fa5 | ||
|
|
1e691b1255 | ||
|
|
61e1bef63f | ||
|
|
46dbb78fbf | ||
|
|
99890707b3 | ||
|
|
f54c841fdc | ||
|
|
e2a6009ee9 | ||
|
|
a34dd15040 | ||
|
|
24c934003d | ||
|
|
5300e1a245 | ||
|
|
90381c670b | ||
|
|
23edea93db | ||
|
|
a51712e363 | ||
|
|
4310e979e1 | ||
|
|
f690b76d87 | ||
|
|
ac29594b67 | ||
|
|
93f535bb87 | ||
|
|
076eb3cf42 | ||
|
|
7b2e0fffbd | ||
|
|
8eaab0dbc3 | ||
|
|
eaa5bc896e | ||
|
|
42d1db5fc3 | ||
|
|
78c017ddaf | ||
|
|
231551d743 | ||
|
|
d0984eaba7 | ||
|
|
15854a72d1 | ||
|
|
7fd2bf0eda | ||
|
|
6a10a2167e | ||
|
|
1ba99d255c | ||
|
|
ff1c2947b6 | ||
|
|
a690d2e0db | ||
|
|
a3df327396 | ||
|
|
ede0016d8e | ||
|
|
c19b091795 | ||
|
|
9e62c8b9f3 | ||
|
|
9f30dc181c | ||
|
|
37fe44f860 | ||
|
|
68aaab8467 | ||
|
|
b951ed1fcd | ||
|
|
9bd9dc568a | ||
|
|
29e4356fee | ||
|
|
0ed31eaa99 | ||
|
|
1e9869b217 | ||
|
|
78cfc2c827 | ||
|
|
a6e5a0d125 | ||
|
|
ea13aecd27 | ||
|
|
d838e8f28f | ||
|
|
c9a7b9e17c | ||
|
|
8fe2d0bc0e | ||
|
|
71e2f1e1e6 | ||
|
|
1ce9d1b9b2 | ||
|
|
b3d6ae467f | ||
|
|
da8d720dc4 | ||
|
|
824c38f8ce | ||
|
|
b0579a70d8 | ||
|
|
f9fe2d0976 |
1
.github/stale.yml
vendored
1
.github/stale.yml
vendored
@@ -7,6 +7,7 @@ exemptLabels:
|
|||||||
- pinned
|
- pinned
|
||||||
- security
|
- security
|
||||||
- enhancement
|
- enhancement
|
||||||
|
- feature-request
|
||||||
- bug
|
- bug
|
||||||
# Label to use when marking an issue as stale
|
# Label to use when marking an issue as stale
|
||||||
staleLabel: wontfix
|
staleLabel: wontfix
|
||||||
|
|||||||
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
|||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
- name: Set up GraalVM
|
- name: Set up GraalVM
|
||||||
uses: graalvm/setup-graalvm@54b4f5a65c1a84b2fdfdc2078fe43df32819e4b1 # v1
|
uses: graalvm/setup-graalvm@03e8abf916fd0e281b2efe7b2da3378bb0a1d085 # v1
|
||||||
with:
|
with:
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
distribution: "graalvm"
|
distribution: "graalvm"
|
||||||
@@ -67,14 +67,14 @@ jobs:
|
|||||||
|
|
||||||
# Upload artifacts
|
# Upload artifacts
|
||||||
- name: Upload cross-platform app
|
- name: Upload cross-platform app
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||||
if: matrix.os == 'ubuntu-latest' # we only need to upload the cross-platform artifact once per database
|
if: matrix.os == 'ubuntu-latest' # we only need to upload the cross-platform artifact once per database
|
||||||
with:
|
with:
|
||||||
name: commafeed-${{ matrix.database }}-jvm
|
name: commafeed-${{ matrix.database }}-jvm
|
||||||
path: commafeed-server/target/commafeed-*.zip
|
path: commafeed-server/target/commafeed-*.zip
|
||||||
|
|
||||||
- name: Upload native executable
|
- name: Upload native executable
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||||
with:
|
with:
|
||||||
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
|
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
|
||||||
path: commafeed-server/target/commafeed-*-runner*
|
path: commafeed-server/target/commafeed-*-runner*
|
||||||
@@ -104,17 +104,17 @@ jobs:
|
|||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
|
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||||
|
|
||||||
- name: Install required packages
|
- name: Install required packages
|
||||||
run: sudo apt-get install -y rename unzip
|
run: sudo apt-get install -y rename unzip
|
||||||
|
|
||||||
# Prepare artifacts
|
# Prepare artifacts
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
pattern: commafeed-${{ matrix.database }}-*
|
pattern: commafeed-${{ matrix.database }}-*
|
||||||
path: ./artifacts
|
path: ./artifacts
|
||||||
@@ -135,7 +135,7 @@ jobs:
|
|||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
- name: Login to Container Registry
|
- name: Login to Container Registry
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
|
||||||
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
@@ -143,7 +143,7 @@ jobs:
|
|||||||
|
|
||||||
## build but don't push for PRs and renovate
|
## build but don't push for PRs and renovate
|
||||||
- name: Docker build - native
|
- name: Docker build - native
|
||||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: commafeed-server/src/main/docker/Dockerfile.native
|
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||||
@@ -151,7 +151,7 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
|
||||||
- name: Docker build - jvm
|
- name: Docker build - jvm
|
||||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||||
@@ -160,7 +160,7 @@ jobs:
|
|||||||
|
|
||||||
## build and push tag
|
## build and push tag
|
||||||
- name: Docker build and push tag - native
|
- name: Docker build and push tag - native
|
||||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
|
||||||
if: ${{ github.ref_type == 'tag' }}
|
if: ${{ github.ref_type == 'tag' }}
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -172,7 +172,7 @@ jobs:
|
|||||||
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}
|
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}
|
||||||
|
|
||||||
- name: Docker build and push tag - jvm
|
- name: Docker build and push tag - jvm
|
||||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
|
||||||
if: ${{ github.ref_type == 'tag' }}
|
if: ${{ github.ref_type == 'tag' }}
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -185,7 +185,7 @@ jobs:
|
|||||||
|
|
||||||
## build and push master
|
## build and push master
|
||||||
- name: Docker build and push master - native
|
- name: Docker build and push master - native
|
||||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
|
||||||
if: ${{ github.ref_name == 'master' }}
|
if: ${{ github.ref_name == 'master' }}
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -195,7 +195,7 @@ jobs:
|
|||||||
tags: athou/commafeed:master-${{ matrix.database }}
|
tags: athou/commafeed:master-${{ matrix.database }}
|
||||||
|
|
||||||
- name: Docker build and push master - jvm
|
- name: Docker build and push master - jvm
|
||||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
|
||||||
if: ${{ github.ref_name == 'master' }}
|
if: ${{ github.ref_name == 'master' }}
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -220,7 +220,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
pattern: commafeed-*
|
pattern: commafeed-*
|
||||||
path: ./artifacts
|
path: ./artifacts
|
||||||
@@ -236,7 +236,7 @@ jobs:
|
|||||||
version: ${{ github.ref_name }}
|
version: ${{ github.ref_name }}
|
||||||
|
|
||||||
- name: Create GitHub release
|
- name: Create GitHub release
|
||||||
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1
|
uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1
|
||||||
with:
|
with:
|
||||||
name: CommaFeed ${{ github.ref_name }}
|
name: CommaFeed ${{ github.ref_name }}
|
||||||
body: ${{ steps.changelog_reader.outputs.changes }}
|
body: ${{ steps.changelog_reader.outputs.changes }}
|
||||||
|
|||||||
2
.mvn/wrapper/maven-wrapper.properties
vendored
2
.mvn/wrapper/maven-wrapper.properties
vendored
@@ -1,3 +1,3 @@
|
|||||||
wrapperVersion=3.3.4
|
wrapperVersion=3.3.4
|
||||||
distributionType=only-script
|
distributionType=only-script
|
||||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
|
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.14/apache-maven-3.9.14-bin.zip
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.4.2/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.4.7/schema.json",
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"indentStyle": "space",
|
"indentStyle": "space",
|
||||||
"indentWidth": 4,
|
"indentWidth": 4,
|
||||||
|
|||||||
4040
commafeed-client/package-lock.json
generated
4040
commafeed-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -18,19 +18,20 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@fontsource/open-sans": "^5.2.7",
|
"@fontsource/open-sans": "^5.2.7",
|
||||||
"@lingui/core": "^5.9.1",
|
"@lingui/core": "^5.9.3",
|
||||||
"@lingui/react": "^5.9.1",
|
"@lingui/react": "^5.9.3",
|
||||||
"@mantine/core": "^8.3.15",
|
"@mantine/core": "^8.3.16",
|
||||||
"@mantine/form": "^8.3.15",
|
"@mantine/form": "^8.3.16",
|
||||||
"@mantine/hooks": "^8.3.15",
|
"@mantine/hooks": "^8.3.16",
|
||||||
"@mantine/modals": "^8.3.15",
|
"@mantine/modals": "^8.3.16",
|
||||||
"@mantine/notifications": "^8.3.15",
|
"@mantine/notifications": "^8.3.16",
|
||||||
"@mantine/spotlight": "^8.3.15",
|
"@mantine/spotlight": "^8.3.16",
|
||||||
"@monaco-editor/react": "^4.7.0",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@react-querybuilder/mantine": "^8.14.0",
|
"@react-querybuilder/mantine": "^8.14.0",
|
||||||
"@reduxjs/toolkit": "^2.11.2",
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
"axios": "^1.13.5",
|
"@rolldown/plugin-babel": "^0.2.2",
|
||||||
"dayjs": "^1.11.19",
|
"axios": "^1.13.6",
|
||||||
|
"dayjs": "^1.11.20",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
"interweave": "^13.1.1",
|
"interweave": "^13.1.1",
|
||||||
"monaco-editor": "^0.55.1",
|
"monaco-editor": "^0.55.1",
|
||||||
@@ -40,11 +41,11 @@
|
|||||||
"react-contexify": "^6.0.0",
|
"react-contexify": "^6.0.0",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-draggable": "^4.5.0",
|
"react-draggable": "^4.5.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.6.0",
|
||||||
"react-infinite-scroller": "^1.2.6",
|
"react-infinite-scroller": "^1.2.6",
|
||||||
"react-querybuilder": "^8.14.0",
|
"react-querybuilder": "^8.14.0",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^7.13.0",
|
"react-router-dom": "^7.13.1",
|
||||||
"react-swipeable": "^7.0.2",
|
"react-swipeable": "^7.0.2",
|
||||||
"style-to-object": "^1.0.14",
|
"style-to-object": "^1.0.14",
|
||||||
"throttle-debounce": "^5.0.2",
|
"throttle-debounce": "^5.0.2",
|
||||||
@@ -53,10 +54,10 @@
|
|||||||
"websocket-heartbeat-js": "^1.1.3"
|
"websocket-heartbeat-js": "^1.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.4.2",
|
"@biomejs/biome": "^2.4.7",
|
||||||
"@lingui/babel-plugin-lingui-macro": "^5.9.1",
|
"@lingui/babel-plugin-lingui-macro": "^5.9.3",
|
||||||
"@lingui/cli": "^5.9.1",
|
"@lingui/cli": "^5.9.3",
|
||||||
"@lingui/vite-plugin": "^5.9.1",
|
"@lingui/vite-plugin": "^5.9.3",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
@@ -66,16 +67,15 @@
|
|||||||
"@types/react-infinite-scroller": "^1.2.5",
|
"@types/react-infinite-scroller": "^1.2.5",
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@types/tinycon": "^0.6.7",
|
"@types/tinycon": "^0.6.7",
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"babel-plugin-react-compiler": "1.0.0",
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jsdom": "^28.1.0",
|
"jsdom": "^29.0.0",
|
||||||
"lint-staged": "^16.2.7",
|
"lint-staged": "^16.4.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^8.0.0",
|
||||||
"vite-plugin-checker": "^0.12.0",
|
"vite-plugin-checker": "^0.12.0",
|
||||||
"vite-tsconfig-paths": "^6.1.1",
|
"vitest": "^4.1.0",
|
||||||
"vitest": "^4.0.18",
|
|
||||||
"yaml": "^2.8.2"
|
"yaml": "^2.8.2"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|||||||
@@ -13,9 +13,9 @@
|
|||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<!-- renovate: datasource=node-version depName=node -->
|
<!-- renovate: datasource=node-version depName=node -->
|
||||||
<node.version>v24.13.1</node.version>
|
<node.version>v24.14.0</node.version>
|
||||||
<!-- renovate: datasource=npm depName=npm -->
|
<!-- renovate: datasource=npm depName=npm -->
|
||||||
<npm.version>11.10.0</npm.version>
|
<npm.version>11.11.1</npm.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-resources-plugin</artifactId>
|
<artifactId>maven-resources-plugin</artifactId>
|
||||||
<version>3.4.0</version>
|
<version>3.5.0</version>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<id>copy web interface to resources</id>
|
<id>copy web interface to resources</id>
|
||||||
@@ -94,4 +94,49 @@
|
|||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
<profiles>
|
||||||
|
<!-- This profile is used to kill the Biome process on Windows -->
|
||||||
|
<!-- npm ci can fail if Biome is running (e.g., in the IDE) because it locks some files -->
|
||||||
|
<profile>
|
||||||
|
<id>kill-biome</id>
|
||||||
|
<activation>
|
||||||
|
<os>
|
||||||
|
<family>Windows</family>
|
||||||
|
</os>
|
||||||
|
</activation>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
|
<artifactId>exec-maven-plugin</artifactId>
|
||||||
|
<version>3.6.3</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>kill-biome</id>
|
||||||
|
<phase>initialize</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>exec</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<executable>taskkill</executable>
|
||||||
|
<arguments>
|
||||||
|
<argument>/F</argument>
|
||||||
|
<argument>/IM</argument>
|
||||||
|
<argument>biome.exe</argument>
|
||||||
|
</arguments>
|
||||||
|
<successCodes>
|
||||||
|
<successCode>0</successCode>
|
||||||
|
<!-- taskkill returns 128 if the process is not found, which is fine in this case -->
|
||||||
|
<successCode>128</successCode>
|
||||||
|
</successCodes>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</profile>
|
||||||
|
</profiles>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { configureStore } from "@reduxjs/toolkit"
|
import { configureStore } from "@reduxjs/toolkit"
|
||||||
import { type TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
|
import { shallowEqual, type TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
|
||||||
import { entriesSlice } from "@/app/entries/slice"
|
import { entriesSlice } from "@/app/entries/slice"
|
||||||
import { redirectSlice } from "@/app/redirect/slice"
|
import { redirectSlice } from "@/app/redirect/slice"
|
||||||
import { serverSlice } from "@/app/server/slice"
|
import { serverSlice } from "@/app/server/slice"
|
||||||
@@ -41,3 +41,4 @@ export type AppDispatch = typeof store.dispatch
|
|||||||
|
|
||||||
export const useAppDispatch: () => AppDispatch = useDispatch
|
export const useAppDispatch: () => AppDispatch = useDispatch
|
||||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||||
|
export const useShallowEqualAppSelector: TypedUseSelectorHook<RootState> = selector => useSelector(selector, shallowEqual)
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ const useStyles = tss.create(() => ({
|
|||||||
badge: {
|
badge: {
|
||||||
width: "3.2rem",
|
width: "3.2rem",
|
||||||
// for some reason, mantine Badge has "cursor: 'default'"
|
// for some reason, mantine Badge has "cursor: 'default'"
|
||||||
cursor: "pointer",
|
cursor: "inherit",
|
||||||
|
},
|
||||||
|
indicator: {
|
||||||
|
// ensure the indicator is not shown above the app header
|
||||||
|
zIndex: 0,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -23,7 +27,15 @@ export function UnreadCount(
|
|||||||
const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount
|
const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount
|
||||||
return (
|
return (
|
||||||
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count} openDelay={Constants.tooltip.delay}>
|
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count} openDelay={Constants.tooltip.delay}>
|
||||||
<Indicator disabled={!props.showIndicator} size={4} offset={10} position="middle-start">
|
<Indicator
|
||||||
|
disabled={!props.showIndicator}
|
||||||
|
size={4}
|
||||||
|
offset={10}
|
||||||
|
position="middle-start"
|
||||||
|
classNames={{
|
||||||
|
indicator: classes.indicator,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Badge className={`${classes.badge} cf-badge`} variant="light" fullWidth>
|
<Badge className={`${classes.badge} cf-badge`} variant="light" fullWidth>
|
||||||
{count}
|
{count}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import { Constants } from "@/app/constants"
|
|||||||
import type { EntrySourceType } from "@/app/entries/slice"
|
import type { EntrySourceType } from "@/app/entries/slice"
|
||||||
import { loadEntries } from "@/app/entries/thunks"
|
import { loadEntries } from "@/app/entries/thunks"
|
||||||
import { redirectToCategoryDetails, redirectToFeedDetails, redirectToTagDetails } from "@/app/redirect/thunks"
|
import { redirectToCategoryDetails, redirectToFeedDetails, redirectToTagDetails } from "@/app/redirect/thunks"
|
||||||
import { useAppDispatch, useAppSelector } from "@/app/store"
|
import { useAppDispatch, useAppSelector, useShallowEqualAppSelector } from "@/app/store"
|
||||||
import { flattenCategoryTree } from "@/app/utils"
|
import { categoryHasNewEntries, categoryUnreadCount, flattenCategoryTree } from "@/app/utils"
|
||||||
import { FeedEntries } from "@/components/content/FeedEntries"
|
import { FeedEntries } from "@/components/content/FeedEntries"
|
||||||
|
import { UnreadCount } from "@/components/sidebar/UnreadCount"
|
||||||
|
import { useMobile } from "@/hooks/useMobile"
|
||||||
import { tss } from "@/tss"
|
import { tss } from "@/tss"
|
||||||
|
|
||||||
function NoSubscriptionHelp() {
|
function NoSubscriptionHelp() {
|
||||||
@@ -33,6 +35,12 @@ const useStyles = tss.create(() => ({
|
|||||||
sourceWebsiteLink: {
|
sourceWebsiteLink: {
|
||||||
color: "inherit",
|
color: "inherit",
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
titleText: {
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -48,6 +56,33 @@ export function FeedEntriesPage(props: Readonly<FeedEntriesPageProps>) {
|
|||||||
const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
|
const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
|
||||||
const sourceWebsiteUrl = useAppSelector(state => state.entries.sourceWebsiteUrl)
|
const sourceWebsiteUrl = useAppSelector(state => state.entries.sourceWebsiteUrl)
|
||||||
const hasMore = useAppSelector(state => state.entries.hasMore)
|
const hasMore = useAppSelector(state => state.entries.hasMore)
|
||||||
|
const mobile = useMobile()
|
||||||
|
const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible)
|
||||||
|
const { unreadCount, hasNewEntries } = useShallowEqualAppSelector(state => {
|
||||||
|
const root = state.tree.rootCategory
|
||||||
|
if (!root) return { unreadCount: 0, hasNewEntries: false }
|
||||||
|
|
||||||
|
if (props.sourceType === "category") {
|
||||||
|
const category = id === Constants.categories.all.id ? root : flattenCategoryTree(root).find(c => c.id === id)
|
||||||
|
return {
|
||||||
|
unreadCount: categoryUnreadCount(category),
|
||||||
|
hasNewEntries: categoryHasNewEntries(category),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.sourceType === "feed") {
|
||||||
|
const feed = flattenCategoryTree(root)
|
||||||
|
.flatMap(c => c.feeds)
|
||||||
|
.find(f => f.id === +id)
|
||||||
|
return {
|
||||||
|
unreadCount: feed?.unread ?? 0,
|
||||||
|
hasNewEntries: !!feed?.hasNewEntries,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { unreadCount: 0, hasNewEntries: false }
|
||||||
|
})
|
||||||
|
const showUnreadCount = mobile || !sidebarVisible
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
let title: React.ReactNode = sourceLabel
|
let title: React.ReactNode = sourceLabel
|
||||||
@@ -89,16 +124,23 @@ export function FeedEntriesPage(props: Readonly<FeedEntriesPageProps>) {
|
|||||||
return (
|
return (
|
||||||
// add some room at the bottom of the page in order to be able to scroll the current entry at the top of the page when expanding
|
// add some room at the bottom of the page in order to be able to scroll the current entry at the top of the page when expanding
|
||||||
<Box mb={viewport.height * 0.7}>
|
<Box mb={viewport.height * 0.7}>
|
||||||
<Group gap="xl" className="cf-entries-title">
|
<Group className="cf-entries-title" wrap="nowrap">
|
||||||
{sourceWebsiteUrl && (
|
{sourceWebsiteUrl && (
|
||||||
<a href={sourceWebsiteUrl} target="_blank" rel="noreferrer" className={classes.sourceWebsiteLink}>
|
<a href={sourceWebsiteUrl} target="_blank" rel="noreferrer" className={classes.sourceWebsiteLink}>
|
||||||
<Title order={3}>{title}</Title>
|
<Title order={3} className={classes.titleText}>
|
||||||
|
{title}
|
||||||
|
</Title>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{!sourceWebsiteUrl && <Title order={3}>{title}</Title>}
|
{!sourceWebsiteUrl && (
|
||||||
|
<Title order={3} className={classes.titleText}>
|
||||||
|
{title}
|
||||||
|
</Title>
|
||||||
|
)}
|
||||||
<ActionIcon onClick={titleClicked} variant="subtle" color={theme.primaryColor}>
|
<ActionIcon onClick={titleClicked} variant="subtle" color={theme.primaryColor}>
|
||||||
<TbEdit size={18} />
|
<TbEdit size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
{showUnreadCount && <UnreadCount unreadCount={unreadCount} showIndicator={hasNewEntries} />}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<FeedEntries />
|
<FeedEntries />
|
||||||
|
|||||||
@@ -1,25 +1,17 @@
|
|||||||
import { lingui } from "@lingui/vite-plugin"
|
import { lingui } from "@lingui/vite-plugin"
|
||||||
import react from "@vitejs/plugin-react"
|
import babel from "@rolldown/plugin-babel"
|
||||||
|
import react, { reactCompilerPreset } from "@vitejs/plugin-react"
|
||||||
import { defineConfig } from "vite"
|
import { defineConfig } from "vite"
|
||||||
import checker from "vite-plugin-checker"
|
import checker from "vite-plugin-checker"
|
||||||
import tsconfigPaths from "vite-tsconfig-paths"
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig(() => ({
|
export default defineConfig(() => ({
|
||||||
plugins: [
|
plugins: [
|
||||||
react({
|
react(),
|
||||||
babel: {
|
babel({
|
||||||
plugins: [
|
presets: [reactCompilerPreset()],
|
||||||
// support for lingui macros
|
plugins: ["@lingui/babel-plugin-lingui-macro"],
|
||||||
// needs to be before the react compiler plugin
|
|
||||||
"@lingui/babel-plugin-lingui-macro",
|
|
||||||
// react compiler
|
|
||||||
["babel-plugin-react-compiler", { target: "19" }],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
lingui(),
|
lingui(),
|
||||||
tsconfigPaths(),
|
|
||||||
checker({
|
checker({
|
||||||
typescript: true,
|
typescript: true,
|
||||||
biome: {
|
biome: {
|
||||||
@@ -43,22 +35,32 @@ export default defineConfig(() => ({
|
|||||||
"/logout": "http://localhost:8083",
|
"/logout": "http://localhost:8083",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
resolve: {
|
||||||
chunkSizeWarningLimit: 3500,
|
tsconfigPaths: true,
|
||||||
rollupOptions: {
|
},
|
||||||
output: {
|
legacy: {
|
||||||
manualChunks: id => {
|
// required for websocket-heartbeat-js
|
||||||
// output mantine as its own chunk because it is quite large
|
inconsistentCjsInterop: true,
|
||||||
if (id.includes("@mantine")) {
|
|
||||||
return "mantine"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
globals: true,
|
globals: true,
|
||||||
setupFiles: "./src/setupTests.ts",
|
setupFiles: "./src/setupTests.ts",
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
chunkSizeWarningLimit: 4000,
|
||||||
|
rolldownOptions: {
|
||||||
|
output: {
|
||||||
|
codeSplitting: {
|
||||||
|
groups: [
|
||||||
|
// output mantine as its own chunk because it is quite large
|
||||||
|
{
|
||||||
|
name: "mantine",
|
||||||
|
test: "@mantine",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<packaging>quarkus</packaging>
|
<packaging>quarkus</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<quarkus.version>3.31.4</quarkus.version>
|
<quarkus.version>3.32.4</quarkus.version>
|
||||||
<querydsl.version>7.1</querydsl.version>
|
<querydsl.version>7.1</querydsl.version>
|
||||||
<rome.version>2.1.0</rome.version>
|
<rome.version>2.1.0</rome.version>
|
||||||
|
|
||||||
@@ -29,6 +29,12 @@
|
|||||||
<type>pom</type>
|
<type>pom</type>
|
||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- the quarkus bom declares a dependency on an old version of protobuf, we need to override it for cel-java -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.protobuf</groupId>
|
||||||
|
<artifactId>protobuf-java</artifactId>
|
||||||
|
<version>4.34.1</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
@@ -237,7 +243,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.puppycrawl.tools</groupId>
|
<groupId>com.puppycrawl.tools</groupId>
|
||||||
<artifactId>checkstyle</artifactId>
|
<artifactId>checkstyle</artifactId>
|
||||||
<version>13.2.0</version>
|
<version>13.3.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<executions>
|
<executions>
|
||||||
@@ -266,7 +272,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>com.diffplug.spotless</groupId>
|
<groupId>com.diffplug.spotless</groupId>
|
||||||
<artifactId>spotless-maven-plugin</artifactId>
|
<artifactId>spotless-maven-plugin</artifactId>
|
||||||
<version>3.2.1</version>
|
<version>3.4.0</version>
|
||||||
<?m2e ignore?>
|
<?m2e ignore?>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
@@ -302,7 +308,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>1.18.42</version>
|
<version>1.18.44</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -393,7 +399,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.cel</groupId>
|
<groupId>dev.cel</groupId>
|
||||||
<artifactId>cel</artifactId>
|
<artifactId>cel</artifactId>
|
||||||
<version>0.11.1</version>
|
<version>0.12.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.passay</groupId>
|
<groupId>org.passay</groupId>
|
||||||
@@ -428,7 +434,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.ibm.icu</groupId>
|
<groupId>com.ibm.icu</groupId>
|
||||||
<artifactId>icu4j</artifactId>
|
<artifactId>icu4j</artifactId>
|
||||||
<version>78.2</version>
|
<version>78.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.sourceforge.cssparser</groupId>
|
<groupId>net.sourceforge.cssparser</groupId>
|
||||||
@@ -448,7 +454,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.github.hakky54</groupId>
|
<groupId>io.github.hakky54</groupId>
|
||||||
<artifactId>ayza-for-apache5</artifactId>
|
<artifactId>ayza-for-apache5</artifactId>
|
||||||
<version>10.0.3</version>
|
<version>10.0.4</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.brotli</groupId>
|
<groupId>org.brotli</groupId>
|
||||||
@@ -465,7 +471,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.quarkiverse.playwright</groupId>
|
<groupId>io.quarkiverse.playwright</groupId>
|
||||||
<artifactId>quarkus-playwright</artifactId>
|
<artifactId>quarkus-playwright</artifactId>
|
||||||
<version>2.3.2</version>
|
<version>2.3.3</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM debian:13.3@sha256:2c91e484d93f0830a7e05a2b9d92a7b102be7cab562198b984a84fdbc7806d91
|
FROM debian:13.4@sha256:55a15a112b42be10bfc8092fcc40b6748dc236f7ef46a358d9392b339e9d60e8
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
EXPOSE 8082
|
EXPOSE 8082
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import com.commafeed.security.password.PasswordConstraintValidator;
|
|||||||
import io.quarkus.runtime.ShutdownEvent;
|
import io.quarkus.runtime.ShutdownEvent;
|
||||||
import io.quarkus.runtime.StartupEvent;
|
import io.quarkus.runtime.StartupEvent;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Singleton
|
@Singleton
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class CommaFeedApplication {
|
public class CommaFeedApplication {
|
||||||
@@ -20,6 +22,8 @@ public class CommaFeedApplication {
|
|||||||
private final CommaFeedConfiguration config;
|
private final CommaFeedConfiguration config;
|
||||||
|
|
||||||
public void start(@Observes StartupEvent ev) {
|
public void start(@Observes StartupEvent ev) {
|
||||||
|
log.info("starting up...");
|
||||||
|
|
||||||
PasswordConstraintValidator.setMinimumPasswordLength(config.users().minimumPasswordLength());
|
PasswordConstraintValidator.setMinimumPasswordLength(config.users().minimumPasswordLength());
|
||||||
|
|
||||||
feedRefreshEngine.start();
|
feedRefreshEngine.start();
|
||||||
@@ -27,6 +31,8 @@ public class CommaFeedApplication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void stop(@Observes ShutdownEvent ev) {
|
public void stop(@Observes ShutdownEvent ev) {
|
||||||
|
log.info("shutting down...");
|
||||||
|
|
||||||
feedRefreshEngine.stop();
|
feedRefreshEngine.stop();
|
||||||
taskScheduler.stop();
|
taskScheduler.stop();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,12 @@ public interface CommaFeedConfiguration {
|
|||||||
@ConfigDocSection
|
@ConfigDocSection
|
||||||
Websocket websocket();
|
Websocket websocket();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duration to wait for the feed refresh engine and the task scheduler to stop when the application is shutting down.
|
||||||
|
*/
|
||||||
|
@WithDefault("2s")
|
||||||
|
Duration shutdownTimeout();
|
||||||
|
|
||||||
interface HttpClient {
|
interface HttpClient {
|
||||||
/**
|
/**
|
||||||
* User-Agent string that will be used by the http client, leave empty for the default one.
|
* User-Agent string that will be used by the http client, leave empty for the default one.
|
||||||
@@ -144,7 +150,7 @@ public interface CommaFeedConfiguration {
|
|||||||
* Prevent access to local addresses to mitigate server-side request forgery (SSRF) attacks, which could potentially expose internal
|
* Prevent access to local addresses to mitigate server-side request forgery (SSRF) attacks, which could potentially expose internal
|
||||||
* resources.
|
* resources.
|
||||||
*
|
*
|
||||||
* You may want to enable this if you host a public instance of CommaFeed with regisration open.
|
* You may want to enable this if you host a public instance of CommaFeed with registrations open.
|
||||||
*/
|
*/
|
||||||
@WithDefault("false")
|
@WithDefault("false")
|
||||||
boolean blockLocalAddresses();
|
boolean blockLocalAddresses();
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import com.commafeed.backend.model.AbstractModel;
|
|||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
import com.commafeed.backend.model.FeedSubscription;
|
import com.commafeed.backend.model.FeedSubscription;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -41,12 +42,12 @@ public class FeedRefreshEngine {
|
|||||||
|
|
||||||
private final BlockingDeque<Feed> queue;
|
private final BlockingDeque<Feed> queue;
|
||||||
|
|
||||||
private final ExecutorService feedProcessingLoopExecutor;
|
private ExecutorService feedProcessingLoopExecutor;
|
||||||
private final ExecutorService refillLoopExecutor;
|
private ExecutorService refillLoopExecutor;
|
||||||
private final ExecutorService refillExecutor;
|
private ThreadPoolExecutor refillExecutor;
|
||||||
private final ThreadPoolExecutor workerExecutor;
|
private ThreadPoolExecutor workerExecutor;
|
||||||
private final ThreadPoolExecutor databaseUpdaterExecutor;
|
private ThreadPoolExecutor databaseUpdaterExecutor;
|
||||||
private final ThreadPoolExecutor notifierExecutor;
|
private ThreadPoolExecutor notifierExecutor;
|
||||||
|
|
||||||
public FeedRefreshEngine(UnitOfWork unitOfWork, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater,
|
public FeedRefreshEngine(UnitOfWork unitOfWork, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater,
|
||||||
FeedUpdateNotifier notifier, CommaFeedConfiguration config, MetricRegistry metrics) {
|
FeedUpdateNotifier notifier, CommaFeedConfiguration config, MetricRegistry metrics) {
|
||||||
@@ -60,6 +61,15 @@ public class FeedRefreshEngine {
|
|||||||
|
|
||||||
this.queue = new LinkedBlockingDeque<>();
|
this.queue = new LinkedBlockingDeque<>();
|
||||||
|
|
||||||
|
metrics.register(MetricRegistry.name(getClass(), "queue", "size"), (Gauge<Integer>) queue::size);
|
||||||
|
metrics.register(MetricRegistry.name(getClass(), "worker", "active"), (Gauge<Integer>) () -> workerExecutor.getActiveCount());
|
||||||
|
metrics.register(MetricRegistry.name(getClass(), "updater", "active"),
|
||||||
|
(Gauge<Integer>) () -> databaseUpdaterExecutor.getActiveCount());
|
||||||
|
metrics.register(MetricRegistry.name(getClass(), "notifier", "active"), (Gauge<Integer>) () -> notifierExecutor.getActiveCount());
|
||||||
|
metrics.register(MetricRegistry.name(getClass(), "notifier", "queue"), (Gauge<Integer>) () -> notifierExecutor.getQueue().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createExecutors() {
|
||||||
this.feedProcessingLoopExecutor = Executors.newSingleThreadExecutor();
|
this.feedProcessingLoopExecutor = Executors.newSingleThreadExecutor();
|
||||||
this.refillLoopExecutor = Executors.newSingleThreadExecutor();
|
this.refillLoopExecutor = Executors.newSingleThreadExecutor();
|
||||||
this.refillExecutor = newDiscardingSingleThreadExecutorService();
|
this.refillExecutor = newDiscardingSingleThreadExecutorService();
|
||||||
@@ -67,15 +77,10 @@ public class FeedRefreshEngine {
|
|||||||
this.databaseUpdaterExecutor = newBlockingExecutorService(config.feedRefresh().databaseThreads());
|
this.databaseUpdaterExecutor = newBlockingExecutorService(config.feedRefresh().databaseThreads());
|
||||||
this.notifierExecutor = newDiscardingExecutorService(config.pushNotifications().threads(),
|
this.notifierExecutor = newDiscardingExecutorService(config.pushNotifications().threads(),
|
||||||
config.pushNotifications().queueCapacity());
|
config.pushNotifications().queueCapacity());
|
||||||
|
|
||||||
metrics.register(MetricRegistry.name(getClass(), "queue", "size"), (Gauge<Integer>) queue::size);
|
|
||||||
metrics.register(MetricRegistry.name(getClass(), "worker", "active"), (Gauge<Integer>) workerExecutor::getActiveCount);
|
|
||||||
metrics.register(MetricRegistry.name(getClass(), "updater", "active"), (Gauge<Integer>) databaseUpdaterExecutor::getActiveCount);
|
|
||||||
metrics.register(MetricRegistry.name(getClass(), "notifier", "active"), (Gauge<Integer>) notifierExecutor::getActiveCount);
|
|
||||||
metrics.register(MetricRegistry.name(getClass(), "notifier", "queue"), (Gauge<Integer>) () -> notifierExecutor.getQueue().size());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
|
createExecutors();
|
||||||
startFeedProcessingLoop();
|
startFeedProcessingLoop();
|
||||||
startRefillLoop();
|
startRefillLoop();
|
||||||
}
|
}
|
||||||
@@ -197,12 +202,14 @@ public class FeedRefreshEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
this.feedProcessingLoopExecutor.shutdownNow();
|
MoreExecutors.shutdownAndAwaitTermination(this.feedProcessingLoopExecutor, config.shutdownTimeout());
|
||||||
this.refillLoopExecutor.shutdownNow();
|
MoreExecutors.shutdownAndAwaitTermination(this.refillLoopExecutor, config.shutdownTimeout());
|
||||||
this.refillExecutor.shutdownNow();
|
MoreExecutors.shutdownAndAwaitTermination(this.refillExecutor, config.shutdownTimeout());
|
||||||
this.workerExecutor.shutdownNow();
|
MoreExecutors.shutdownAndAwaitTermination(this.workerExecutor, config.shutdownTimeout());
|
||||||
this.databaseUpdaterExecutor.shutdownNow();
|
MoreExecutors.shutdownAndAwaitTermination(this.databaseUpdaterExecutor, config.shutdownTimeout());
|
||||||
this.notifierExecutor.shutdownNow();
|
MoreExecutors.shutdownAndAwaitTermination(this.notifierExecutor, config.shutdownTimeout());
|
||||||
|
|
||||||
|
queue.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -54,10 +54,20 @@ public class DatabaseCleaningService {
|
|||||||
int deleted;
|
int deleted;
|
||||||
long entriesTotal = 0;
|
long entriesTotal = 0;
|
||||||
do {
|
do {
|
||||||
|
if (Thread.currentThread().isInterrupted()) {
|
||||||
|
log.info("interrupted, stopping cleanup of feeds without subscriptions");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
List<Feed> feeds = unitOfWork.call(() -> feedDAO.findWithoutSubscriptions(1));
|
List<Feed> feeds = unitOfWork.call(() -> feedDAO.findWithoutSubscriptions(1));
|
||||||
for (Feed feed : feeds) {
|
for (Feed feed : feeds) {
|
||||||
long entriesDeleted;
|
long entriesDeleted;
|
||||||
do {
|
do {
|
||||||
|
if (Thread.currentThread().isInterrupted()) {
|
||||||
|
log.info("interrupted, stopping cleanup of feeds without subscriptions");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
entriesDeleted = unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), batchSize));
|
entriesDeleted = unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), batchSize));
|
||||||
entriesDeletedMeter.mark(entriesDeleted);
|
entriesDeletedMeter.mark(entriesDeleted);
|
||||||
entriesTotal += entriesDeleted;
|
entriesTotal += entriesDeleted;
|
||||||
@@ -76,6 +86,11 @@ public class DatabaseCleaningService {
|
|||||||
long total = 0;
|
long total = 0;
|
||||||
long deleted;
|
long deleted;
|
||||||
do {
|
do {
|
||||||
|
if (Thread.currentThread().isInterrupted()) {
|
||||||
|
log.info("interrupted, stopping cleanup of contents without entries");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
deleted = unitOfWork.call(() -> feedEntryContentDAO.deleteWithoutEntries(batchSize));
|
deleted = unitOfWork.call(() -> feedEntryContentDAO.deleteWithoutEntries(batchSize));
|
||||||
total += deleted;
|
total += deleted;
|
||||||
log.debug("removed {} contents without entries", total);
|
log.debug("removed {} contents without entries", total);
|
||||||
@@ -87,6 +102,11 @@ public class DatabaseCleaningService {
|
|||||||
log.info("cleaning entries exceeding feed capacity");
|
log.info("cleaning entries exceeding feed capacity");
|
||||||
long total = 0;
|
long total = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (Thread.currentThread().isInterrupted()) {
|
||||||
|
log.info("interrupted, stopping cleanup of entries exceeding feed capacity");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
List<FeedCapacity> feeds = unitOfWork
|
List<FeedCapacity> feeds = unitOfWork
|
||||||
.call(() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, batchSize, keepStarredEntries));
|
.call(() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, batchSize, keepStarredEntries));
|
||||||
if (feeds.isEmpty()) {
|
if (feeds.isEmpty()) {
|
||||||
@@ -97,6 +117,11 @@ public class DatabaseCleaningService {
|
|||||||
long remaining = feed.capacity() - maxFeedCapacity;
|
long remaining = feed.capacity() - maxFeedCapacity;
|
||||||
int deleted;
|
int deleted;
|
||||||
do {
|
do {
|
||||||
|
if (Thread.currentThread().isInterrupted()) {
|
||||||
|
log.info("interrupted, stopping cleanup of entries exceeding feed capacity");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final long rem = remaining;
|
final long rem = remaining;
|
||||||
deleted = unitOfWork.call(() -> feedEntryDAO.deleteOldEntries(feed.id(), Math.min(batchSize, rem), keepStarredEntries));
|
deleted = unitOfWork.call(() -> feedEntryDAO.deleteOldEntries(feed.id(), Math.min(batchSize, rem), keepStarredEntries));
|
||||||
entriesDeletedMeter.mark(deleted);
|
entriesDeletedMeter.mark(deleted);
|
||||||
@@ -114,6 +139,11 @@ public class DatabaseCleaningService {
|
|||||||
long total = 0;
|
long total = 0;
|
||||||
long deleted;
|
long deleted;
|
||||||
do {
|
do {
|
||||||
|
if (Thread.currentThread().isInterrupted()) {
|
||||||
|
log.info("interrupted, stopping cleanup of old entries");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
deleted = unitOfWork.call(() -> feedEntryDAO.deleteEntriesOlderThan(olderThan, batchSize, keepStarredEntries));
|
deleted = unitOfWork.call(() -> feedEntryDAO.deleteEntriesOlderThan(olderThan, batchSize, keepStarredEntries));
|
||||||
entriesDeletedMeter.mark(deleted);
|
entriesDeletedMeter.mark(deleted);
|
||||||
total += deleted;
|
total += deleted;
|
||||||
@@ -127,6 +157,11 @@ public class DatabaseCleaningService {
|
|||||||
long total = 0;
|
long total = 0;
|
||||||
long deleted;
|
long deleted;
|
||||||
do {
|
do {
|
||||||
|
if (Thread.currentThread().isInterrupted()) {
|
||||||
|
log.info("interrupted, stopping cleanup of old read statuses");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
deleted = unitOfWork.call(() -> feedEntryStatusDAO.deleteOldStatuses(olderThan, batchSize));
|
deleted = unitOfWork.call(() -> feedEntryStatusDAO.deleteOldStatuses(olderThan, batchSize));
|
||||||
total += deleted;
|
total += deleted;
|
||||||
log.debug("removed {} old read statuses", total);
|
log.debug("removed {} old read statuses", total);
|
||||||
@@ -139,6 +174,11 @@ public class DatabaseCleaningService {
|
|||||||
long total = 0;
|
long total = 0;
|
||||||
long marked;
|
long marked;
|
||||||
do {
|
do {
|
||||||
|
if (Thread.currentThread().isInterrupted()) {
|
||||||
|
log.info("interrupted, stopping marking entries as read");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
marked = unitOfWork.call(() -> feedEntryStatusDAO.autoMarkAsRead(batchSize));
|
marked = unitOfWork.call(() -> feedEntryStatusDAO.autoMarkAsRead(batchSize));
|
||||||
total += marked;
|
total += marked;
|
||||||
log.debug("marked {} entries as read", total);
|
log.debug("marked {} entries as read", total);
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ public abstract class ScheduledTask {
|
|||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
log.info("registering task {} for execution every {} {}, starting in {} {}", getClass().getSimpleName(), getPeriod(), getTimeUnit(),
|
log.debug("registering task {} for execution every {} {}, starting in {} {}", getClass().getSimpleName(), getPeriod(),
|
||||||
getInitialDelay(), getTimeUnit());
|
getTimeUnit(), getInitialDelay(), getTimeUnit());
|
||||||
executor.scheduleWithFixedDelay(runnable, getInitialDelay(), getPeriod(), getTimeUnit());
|
executor.scheduleWithFixedDelay(runnable, getInitialDelay(), getPeriod(), getTimeUnit());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,24 +6,32 @@ import java.util.concurrent.ScheduledExecutorService;
|
|||||||
|
|
||||||
import jakarta.inject.Singleton;
|
import jakarta.inject.Singleton;
|
||||||
|
|
||||||
import io.quarkus.arc.All;
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
||||||
|
import io.quarkus.arc.All;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Singleton
|
@Singleton
|
||||||
public class TaskScheduler {
|
public class TaskScheduler {
|
||||||
|
|
||||||
private final List<ScheduledTask> tasks;
|
private final List<ScheduledTask> tasks;
|
||||||
private final ScheduledExecutorService executor;
|
private final CommaFeedConfiguration config;
|
||||||
|
|
||||||
public TaskScheduler(@All List<ScheduledTask> tasks) {
|
private ScheduledExecutorService executor;
|
||||||
|
|
||||||
|
public TaskScheduler(@All List<ScheduledTask> tasks, CommaFeedConfiguration config) {
|
||||||
this.tasks = tasks;
|
this.tasks = tasks;
|
||||||
this.executor = Executors.newScheduledThreadPool(tasks.size());
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
tasks.forEach(task -> task.register(executor));
|
this.executor = Executors.newScheduledThreadPool(tasks.size());
|
||||||
|
this.tasks.forEach(task -> task.register(executor));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
executor.shutdownNow();
|
MoreExecutors.shutdownAndAwaitTermination(executor, config.shutdownTimeout());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ quarkus.native.additional-build-args=-H:PageSize=65536
|
|||||||
# test profile overrides
|
# test profile overrides
|
||||||
%test.quarkus.log.category."org.mockserver".level=WARN
|
%test.quarkus.log.category."org.mockserver".level=WARN
|
||||||
%test.quarkus.log.category."liquibase".level=WARN
|
%test.quarkus.log.category."liquibase".level=WARN
|
||||||
|
%test.commafeed.shutdown-timeout=100ms
|
||||||
%test.commafeed.users.create-demo-account=true
|
%test.commafeed.users.create-demo-account=true
|
||||||
%test.commafeed.users.allow-registrations=true
|
%test.commafeed.users.allow-registrations=true
|
||||||
%test.commafeed.password-recovery-enabled=true
|
%test.commafeed.password-recovery-enabled=true
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import jakarta.persistence.EntityManager;
|
|||||||
import org.hibernate.Session;
|
import org.hibernate.Session;
|
||||||
import org.kohsuke.MetaInfServices;
|
import org.kohsuke.MetaInfServices;
|
||||||
|
|
||||||
|
import io.quarkus.runtime.ShutdownEvent;
|
||||||
|
import io.quarkus.runtime.StartupEvent;
|
||||||
import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback;
|
import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback;
|
||||||
import io.quarkus.test.junit.callback.QuarkusTestMethodContext;
|
import io.quarkus.test.junit.callback.QuarkusTestMethodContext;
|
||||||
|
|
||||||
@@ -17,12 +19,17 @@ public class DatabaseReset implements QuarkusTestBeforeEachCallback {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beforeEach(QuarkusTestMethodContext context) {
|
public void beforeEach(QuarkusTestMethodContext context) {
|
||||||
CDI.current()
|
// stop the application to make sure that there are no active transactions when we truncate the tables
|
||||||
.select(EntityManager.class)
|
getBean(CommaFeedApplication.class).stop(new ShutdownEvent());
|
||||||
.get()
|
|
||||||
.unwrap(Session.class)
|
// truncate all tables so that we have a clean slate for the next test
|
||||||
.getSessionFactory()
|
getBean(EntityManager.class).unwrap(Session.class).getSessionFactory().getSchemaManager().truncateMappedObjects();
|
||||||
.getSchemaManager()
|
|
||||||
.truncateMappedObjects();
|
// restart the application
|
||||||
|
getBean(CommaFeedApplication.class).start(new StartupEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T getBean(Class<T> clazz) {
|
||||||
|
return CDI.current().select(clazz).get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user