mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-06 17:43:51 +00:00
Compare commits
131 Commits
v0.18.0-be
...
v0.18.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b066a4b39 | ||
|
|
cc0462e7fe | ||
|
|
b87c3840f3 | ||
|
|
47243247b9 | ||
|
|
8689465e00 | ||
|
|
b0ca3c6d58 | ||
|
|
2857568f03 | ||
|
|
2e94944d2b | ||
|
|
afa0e31ecd | ||
|
|
3390fbc5db | ||
|
|
cd2c2b7fdb | ||
|
|
3f5dadb4f5 | ||
|
|
401106203c | ||
|
|
e200e0a1a0 | ||
|
|
56a3f054f9 | ||
|
|
abaea9e605 | ||
|
|
1c2b23b160 | ||
|
|
9642566086 | ||
|
|
bd7c422c46 | ||
|
|
bf093710b7 | ||
|
|
ffa4d5422d | ||
|
|
a97ee60502 | ||
|
|
84cfcb193f | ||
|
|
1f71e87460 | ||
|
|
54c51225ed | ||
|
|
5fade7aaf5 | ||
|
|
df99e2ca97 | ||
|
|
4610f1e934 | ||
|
|
78ef9fee34 | ||
|
|
18089c7369 | ||
|
|
991e0b9b63 | ||
|
|
129cceade9 | ||
|
|
055fa0a8b4 | ||
|
|
9f3dceb220 | ||
|
|
6a64055886 | ||
|
|
c712e87114 | ||
|
|
343152e162 | ||
|
|
97d6f53932 | ||
|
|
44e00f67c4 | ||
|
|
39cb1b7714 | ||
|
|
6f5c61b8b6 | ||
|
|
87520e9bf9 | ||
|
|
181b213a3e | ||
|
|
a8938ab403 | ||
|
|
ca8bb6dc90 | ||
|
|
21c7d1810d | ||
|
|
2fa843b960 | ||
|
|
9469b135c5 | ||
|
|
c0f6a60a66 | ||
|
|
c32b29a293 | ||
|
|
f85dfae63b | ||
|
|
b3b1ea2f33 | ||
|
|
01d1631fe8 | ||
|
|
b5fa8472d9 | ||
|
|
17c247af53 | ||
|
|
f4abe39689 | ||
|
|
73283df3e1 | ||
|
|
a5bcfb0b14 | ||
|
|
68573aa35e | ||
|
|
a8d664a03e | ||
|
|
e2b221a451 | ||
|
|
98bdf25844 | ||
|
|
fa82842cd7 | ||
|
|
2ee2cbfe36 | ||
|
|
51b00c476c | ||
|
|
e6a4fc7210 | ||
|
|
bfc8b93a96 | ||
|
|
5cde590a4f | ||
|
|
cc9a23e424 | ||
|
|
6fe2e42490 | ||
|
|
713551fbf1 | ||
|
|
81fc9e1aa1 | ||
|
|
35f3fc7b5d | ||
|
|
b3749246f6 | ||
|
|
64674a539f | ||
|
|
5605185a00 | ||
|
|
846544d887 | ||
|
|
c4e65c754e | ||
|
|
50a04f6443 | ||
|
|
9239eed6a7 | ||
|
|
7bdad2dc4b | ||
|
|
1284f33a4b | ||
|
|
39c65051ac | ||
|
|
e6ef1dea51 | ||
|
|
2aceed8824 | ||
|
|
e2f281ac18 | ||
|
|
d6618b6891 | ||
|
|
571e25a7a1 | ||
|
|
1cdc7d5592 | ||
|
|
5f40fbc69c | ||
|
|
c1ece15560 | ||
|
|
1a1041712f | ||
|
|
02dbe135d4 | ||
|
|
73d0e64c20 | ||
|
|
14d2214248 | ||
|
|
099b5d5aa0 | ||
|
|
5823353733 | ||
|
|
e988be2f86 | ||
|
|
2a3e81de3e | ||
|
|
c323e5ae93 | ||
|
|
d8eda5e42d | ||
|
|
06591db8d9 | ||
|
|
add8c56c69 | ||
|
|
483a6d8034 | ||
|
|
727130ec97 | ||
|
|
c79b93bc01 | ||
|
|
50891ad9eb | ||
|
|
59264b9996 | ||
|
|
584d095895 | ||
|
|
fcd4f8c4ff | ||
|
|
ed06e6b72c | ||
|
|
029654f45e | ||
|
|
ef82b9d3e7 | ||
|
|
4977055a2e | ||
|
|
c0d802a169 | ||
|
|
01228117e3 | ||
|
|
c70318735c | ||
|
|
ca6e8c380b | ||
|
|
684b676028 | ||
|
|
aae71a23eb | ||
|
|
9e903fe909 | ||
|
|
e7732d0e18 | ||
|
|
8c650f7b43 | ||
|
|
c41646be7f | ||
|
|
9e41918a1a | ||
|
|
15749def2a | ||
|
|
902635e60f | ||
|
|
89d09fd5e9 | ||
|
|
fe04ab35cc | ||
|
|
5b5dc26abf | ||
|
|
de7b1ff516 |
@@ -1,2 +1,4 @@
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
[target.aarch64-pc-windows-msvc]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
2
.github/actions/deploy/deploy.mjs
vendored
2
.github/actions/deploy/deploy.mjs
vendored
@@ -41,7 +41,7 @@ const isBeta = buildType === 'beta';
|
||||
const isInternal = buildType === 'internal';
|
||||
|
||||
const replicaConfig = {
|
||||
production: {
|
||||
stable: {
|
||||
web: 3,
|
||||
graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 3,
|
||||
sync: Number(process.env.PRODUCTION_SYNC_REPLICA) || 3,
|
||||
|
||||
28
.github/actions/setup-rust/action.yml
vendored
Normal file
28
.github/actions/setup-rust/action.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: 'Rust setup'
|
||||
description: 'Rust setup, including cache configuration'
|
||||
inputs:
|
||||
components:
|
||||
description: 'Cargo components'
|
||||
required: false
|
||||
targets:
|
||||
description: 'Cargo target'
|
||||
required: false
|
||||
toolchain:
|
||||
description: 'Rustup toolchain'
|
||||
required: false
|
||||
default: 'stable'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: ${{ inputs.toolchain }}
|
||||
targets: ${{ inputs.targets }}
|
||||
components: ${{ inputs.components }}
|
||||
- name: Add Targets
|
||||
if: ${{ inputs.targets }}
|
||||
run: rustup target add ${{ inputs.targets }}
|
||||
shell: bash
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
2
.github/deployment/front/Dockerfile
vendored
2
.github/deployment/front/Dockerfile
vendored
@@ -1,4 +1,4 @@
|
||||
FROM openresty/openresty:1.25.3.2-0-buster
|
||||
FROM openresty/openresty:1.27.1.1-0-buster
|
||||
WORKDIR /app
|
||||
COPY ./packages/frontend/apps/web/dist ./dist
|
||||
COPY ./packages/frontend/admin/dist ./admin
|
||||
|
||||
2
.github/helm/affine/Chart.yaml
vendored
2
.github/helm/affine/Chart.yaml
vendored
@@ -3,4 +3,4 @@ name: affine
|
||||
description: AFFiNE cloud chart
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.17.0"
|
||||
appVersion: "0.18.0"
|
||||
|
||||
@@ -3,7 +3,7 @@ name: graphql
|
||||
description: AFFiNE GraphQL server
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.17.0"
|
||||
appVersion: "0.18.0"
|
||||
dependencies:
|
||||
- name: gcloud-sql-proxy
|
||||
version: 0.0.0
|
||||
|
||||
2
.github/helm/affine/charts/sync/Chart.yaml
vendored
2
.github/helm/affine/charts/sync/Chart.yaml
vendored
@@ -3,7 +3,7 @@ name: sync
|
||||
description: AFFiNE Sync Server
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.17.0"
|
||||
appVersion: "0.18.0"
|
||||
dependencies:
|
||||
- name: gcloud-sql-proxy
|
||||
version: 0.0.0
|
||||
|
||||
54
.github/workflows/build-test.yml
vendored
54
.github/workflows/build-test.yml
vendored
@@ -223,7 +223,7 @@ jobs:
|
||||
run: yarn nx test:coverage @affine/monorepo
|
||||
|
||||
- name: Upload unit test coverage results
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./.coverage/store/lcov.info
|
||||
@@ -371,7 +371,7 @@ jobs:
|
||||
COPILOT_OPENAI_API_KEY: 'use_fake_openai_api_key'
|
||||
|
||||
- name: Upload server test coverage results
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/backend/server/.coverage/lcov.info
|
||||
@@ -379,6 +379,23 @@ jobs:
|
||||
name: affine
|
||||
fail_ci_if_error: false
|
||||
|
||||
server-native-test:
|
||||
name: Run server native tests
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
CARGO_TERM_COLOR: always
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Rust
|
||||
uses: ./.github/actions/setup-rust
|
||||
|
||||
- name: Install latest nextest release
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Run tests
|
||||
run: cargo nextest run --release
|
||||
|
||||
copilot-api-test:
|
||||
name: Server Copilot Api Test
|
||||
runs-on: ubuntu-latest
|
||||
@@ -408,22 +425,44 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check blocksuite update
|
||||
id: check-blocksuite-update
|
||||
env:
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
run: |
|
||||
if node ./scripts/detect-blocksuite-update.mjs "$BASE_REF"; then
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
backend:
|
||||
- 'packages/backend/server/src/**'
|
||||
|
||||
- name: Setup Node.js
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.filter.outputs.backend == 'true' }}
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
electron-install: false
|
||||
full-cache: true
|
||||
|
||||
- name: Download server-native.node
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.filter.outputs.backend == 'true' }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: server-native.node
|
||||
path: ./packages/backend/server
|
||||
|
||||
- name: Prepare Server Test Environment
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.filter.outputs.backend == 'true' }}
|
||||
uses: ./.github/actions/server-test-env
|
||||
|
||||
- name: Run server tests
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.filter.outputs.backend == 'true' }}
|
||||
run: yarn workspace @affine/server test:copilot:coverage --forbid-only
|
||||
env:
|
||||
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
|
||||
@@ -431,7 +470,8 @@ jobs:
|
||||
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
|
||||
|
||||
- name: Upload server test coverage results
|
||||
uses: codecov/codecov-action@v4
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.filter.outputs.backend == 'true' }}
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/backend/server/.coverage/lcov.info
|
||||
@@ -480,7 +520,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip == 'true' }}
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' }}
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
playwright-install: true
|
||||
@@ -488,14 +528,14 @@ jobs:
|
||||
hard-link-nm: false
|
||||
|
||||
- name: Download server-native.node
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip == 'true' }}
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: server-native.node
|
||||
path: ./packages/backend/server
|
||||
|
||||
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip == 'true' }}
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' }}
|
||||
uses: ./.github/actions/copilot-test
|
||||
with:
|
||||
script: yarn workspace @affine-test/affine-cloud-copilot e2e --forbid-only --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
@@ -732,6 +772,8 @@ jobs:
|
||||
- build-server-native
|
||||
- build-electron-renderer
|
||||
- server-test
|
||||
- server-native-test
|
||||
- copilot-api-test
|
||||
- copilot-e2e-test
|
||||
- server-e2e-test
|
||||
- desktop-test
|
||||
|
||||
2
.github/workflows/copilot-test.yml
vendored
2
.github/workflows/copilot-test.yml
vendored
@@ -86,7 +86,7 @@ jobs:
|
||||
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
|
||||
|
||||
- name: Upload server test coverage results
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/backend/server/.coverage/lcov.info
|
||||
|
||||
50
.github/workflows/deploy.yml
vendored
50
.github/workflows/deploy.yml
vendored
@@ -173,41 +173,31 @@ jobs:
|
||||
BLOCKSUITE_REPO_PATH: ${{ github.workspace }}/blocksuite
|
||||
- name: Post Failed event to a Slack channel
|
||||
id: failed-slack
|
||||
uses: slackapi/slack-github-action@v1.27.0
|
||||
uses: slackapi/slack-github-action@v2.0.0
|
||||
if: ${{ always() && contains(needs.*.result, 'failure') }}
|
||||
with:
|
||||
channel-id: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
|
||||
method: chat.postMessage
|
||||
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload: |
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"text": "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy failed `${{ github.event.inputs.flavor }}`>",
|
||||
"type": "mrkdwn"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
channel: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
|
||||
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy failed `${{ github.event.inputs.flavor }}`>"
|
||||
blocks:
|
||||
- type: section
|
||||
text:
|
||||
type: mrkdwn
|
||||
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy failed `${{ github.event.inputs.flavor }}`>"
|
||||
- name: Post Cancel event to a Slack channel
|
||||
id: cancel-slack
|
||||
uses: slackapi/slack-github-action@v1.27.0
|
||||
uses: slackapi/slack-github-action@v2.0.0
|
||||
if: ${{ always() && contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'failure') }}
|
||||
with:
|
||||
channel-id: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
|
||||
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
method: chat.postMessage
|
||||
payload: |
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"text": "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy cancelled `${{ github.event.inputs.flavor }}`>",
|
||||
"type": "mrkdwn"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
channel: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
|
||||
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy cancelled `${{ github.event.inputs.flavor }}`>"
|
||||
blocks:
|
||||
- type: section
|
||||
text:
|
||||
type: mrkdwn
|
||||
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy cancelled `${{ github.event.inputs.flavor }}`>"
|
||||
|
||||
92
.github/workflows/release-desktop.yml
vendored
92
.github/workflows/release-desktop.yml
vendored
@@ -32,7 +32,7 @@ permissions:
|
||||
|
||||
env:
|
||||
BUILD_TYPE: ${{ github.event.inputs.build-type }}
|
||||
DEBUG: napi:*
|
||||
DEBUG: 'affine:*,napi:*'
|
||||
APP_NAME: affine
|
||||
MACOSX_DEPLOYMENT_TARGET: '10.13'
|
||||
|
||||
@@ -87,6 +87,7 @@ jobs:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.spec.runner }}
|
||||
needs: before-make
|
||||
environment: ${{ github.event.inputs.build-type }}
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
@@ -96,6 +97,7 @@ jobs:
|
||||
SENTRY_PROJECT: 'affine'
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_RELEASE: ${{ needs.before-make.outputs.RELEASE_VERSION }}
|
||||
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -163,10 +165,10 @@ jobs:
|
||||
if: ${{ matrix.spec.platform == 'linux' }}
|
||||
run: |
|
||||
mkdir -p builds
|
||||
mv packages/frontend/apps/electron/out/*/make/zip/linux/x64/*.zip ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.zip
|
||||
mv packages/frontend/apps/electron/out/*/make/*.AppImage ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.appimage
|
||||
mv packages/frontend/apps/electron/out/*/make/deb/x64/*.deb ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.deb
|
||||
mv packages/frontend/apps/electron/out/*/make/flatpak/*/*.flatpak ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.flatpak
|
||||
mv packages/frontend/apps/electron/out/*/make/zip/linux/${{ matrix.spec.arch }}/*.zip ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.zip
|
||||
mv packages/frontend/apps/electron/out/*/make/*.AppImage ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.appimage
|
||||
mv packages/frontend/apps/electron/out/*/make/deb/${{ matrix.spec.arch }}/*.deb ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.deb
|
||||
mv packages/frontend/apps/electron/out/*/make/flatpak/*/*.flatpak ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.flatpak
|
||||
|
||||
- uses: actions/attest-build-provenance@v1
|
||||
if: ${{ matrix.spec.platform == 'darwin' }}
|
||||
@@ -189,6 +191,7 @@ jobs:
|
||||
path: builds
|
||||
|
||||
package-distribution-windows:
|
||||
environment: ${{ github.event.inputs.build-type }}
|
||||
strategy:
|
||||
matrix:
|
||||
spec:
|
||||
@@ -196,16 +199,22 @@ jobs:
|
||||
platform: win32
|
||||
arch: x64
|
||||
target: x86_64-pc-windows-msvc
|
||||
- runner: windows-latest
|
||||
platform: win32
|
||||
arch: arm64
|
||||
target: aarch64-pc-windows-msvc
|
||||
runs-on: ${{ matrix.spec.runner }}
|
||||
needs: before-make
|
||||
outputs:
|
||||
FILES_TO_BE_SIGNED: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED }}
|
||||
FILES_TO_BE_SIGNED_x64: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED_x64 }}
|
||||
FILES_TO_BE_SIGNED_arm64: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED_arm64 }}
|
||||
env:
|
||||
SKIP_GENERATE_ASSETS: 1
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: 'affine'
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_RELEASE: ${{ needs.before-make.outputs.RELEASE_VERSION }}
|
||||
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -243,7 +252,7 @@ jobs:
|
||||
id: get_files_to_be_signed
|
||||
run: |
|
||||
Set-Variable -Name FILES_TO_BE_SIGNED -Value ((Get-ChildItem -Path packages/frontend/apps/electron/out -Recurse -File | Where-Object { $_.Extension -in @(".exe", ".node", ".dll", ".msi") } | ForEach-Object { '"' + $_.FullName.Replace((Get-Location).Path + '\packages\frontend\apps\electron\out\', '') + '"' }) -join ' ')
|
||||
"FILES_TO_BE_SIGNED=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT
|
||||
"FILES_TO_BE_SIGNED_${{ matrix.spec.arch }}=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT
|
||||
echo $FILES_TO_BE_SIGNED
|
||||
|
||||
- name: Zip artifacts for faster upload
|
||||
@@ -257,25 +266,35 @@ jobs:
|
||||
archive.zip
|
||||
!**/*.map
|
||||
|
||||
sign-packaged-artifacts-windows:
|
||||
sign-packaged-artifacts-windows_x64:
|
||||
needs: package-distribution-windows
|
||||
uses: ./.github/workflows/windows-signer.yml
|
||||
with:
|
||||
files: ${{ needs.package-distribution-windows.outputs.FILES_TO_BE_SIGNED }}
|
||||
files: ${{ needs.package-distribution-windows.outputs.FILES_TO_BE_SIGNED_x64 }}
|
||||
artifact-name: packaged-win32-x64
|
||||
|
||||
sign-packaged-artifacts-windows_arm64:
|
||||
needs: package-distribution-windows
|
||||
uses: ./.github/workflows/windows-signer.yml
|
||||
with:
|
||||
files: ${{ needs.package-distribution-windows.outputs.FILES_TO_BE_SIGNED_arm64 }}
|
||||
artifact-name: packaged-win32-arm64
|
||||
|
||||
make-windows-installer:
|
||||
needs: sign-packaged-artifacts-windows
|
||||
needs:
|
||||
- sign-packaged-artifacts-windows_x64
|
||||
- sign-packaged-artifacts-windows_arm64
|
||||
strategy:
|
||||
matrix:
|
||||
spec:
|
||||
- runner: windows-latest
|
||||
platform: win32
|
||||
- platform: win32
|
||||
arch: x64
|
||||
target: x86_64-pc-windows-msvc
|
||||
runs-on: ${{ matrix.spec.runner }}
|
||||
- platform: win32
|
||||
arch: arm64
|
||||
runs-on: windows-latest
|
||||
outputs:
|
||||
FILES_TO_BE_SIGNED: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED }}
|
||||
FILES_TO_BE_SIGNED_x64: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED_x64 }}
|
||||
FILES_TO_BE_SIGNED_arm64: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED_arm64 }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Version
|
||||
@@ -288,6 +307,8 @@ jobs:
|
||||
extra-flags: workspaces focus @affine/electron @affine/monorepo
|
||||
hard-link-nm: false
|
||||
nmHoistingLimits: workspaces
|
||||
env:
|
||||
npm_config_arch: ${{ matrix.spec.arch }}
|
||||
- name: Download and overwrite packaged artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -309,7 +330,7 @@ jobs:
|
||||
id: get_files_to_be_signed
|
||||
run: |
|
||||
Set-Variable -Name FILES_TO_BE_SIGNED -Value ((Get-ChildItem -Path packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make -Recurse -File | Where-Object { $_.Extension -in @(".exe", ".node", ".dll", ".msi") } | ForEach-Object { '"' + $_.FullName.Replace((Get-Location).Path + '\packages\frontend\apps\electron\out\${{ env.BUILD_TYPE }}\make\', '') + '"' }) -join ' ')
|
||||
"FILES_TO_BE_SIGNED=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT
|
||||
"FILES_TO_BE_SIGNED_${{ matrix.spec.arch }}=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT
|
||||
echo $FILES_TO_BE_SIGNED
|
||||
|
||||
- name: Save installer for signing
|
||||
@@ -318,22 +339,36 @@ jobs:
|
||||
name: installer-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
||||
path: archive.zip
|
||||
|
||||
sign-installer-artifacts-windows:
|
||||
sign-installer-artifacts-windows-x64:
|
||||
needs: make-windows-installer
|
||||
uses: ./.github/workflows/windows-signer.yml
|
||||
with:
|
||||
files: ${{ needs.make-windows-installer.outputs.FILES_TO_BE_SIGNED }}
|
||||
files: ${{ needs.make-windows-installer.outputs.FILES_TO_BE_SIGNED_x64 }}
|
||||
artifact-name: installer-win32-x64
|
||||
|
||||
sign-installer-artifacts-windows-arm64:
|
||||
needs: make-windows-installer
|
||||
uses: ./.github/workflows/windows-signer.yml
|
||||
with:
|
||||
files: ${{ needs.make-windows-installer.outputs.FILES_TO_BE_SIGNED_arm64 }}
|
||||
artifact-name: installer-win32-arm64
|
||||
|
||||
finalize-installer-windows:
|
||||
needs: [sign-installer-artifacts-windows, before-make]
|
||||
needs:
|
||||
[
|
||||
sign-installer-artifacts-windows-x64,
|
||||
sign-installer-artifacts-windows-arm64,
|
||||
before-make,
|
||||
]
|
||||
strategy:
|
||||
matrix:
|
||||
spec:
|
||||
- runner: windows-latest
|
||||
platform: win32
|
||||
arch: x64
|
||||
target: x86_64-pc-windows-msvc
|
||||
- runner: windows-latest
|
||||
platform: win32
|
||||
arch: arm64
|
||||
runs-on: ${{ matrix.spec.runner }}
|
||||
steps:
|
||||
- name: Download and overwrite installer artifacts
|
||||
@@ -347,16 +382,16 @@ jobs:
|
||||
- name: Save artifacts
|
||||
run: |
|
||||
mkdir -p builds
|
||||
mv packages/frontend/apps/electron/out/*/make/zip/win32/x64/AFFiNE*-win32-x64-*.zip ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-x64.zip
|
||||
mv packages/frontend/apps/electron/out/*/make/squirrel.windows/x64/*.exe ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-x64.exe
|
||||
mv packages/frontend/apps/electron/out/*/make/nsis.windows/x64/*.exe ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-x64.nsis.exe
|
||||
mv packages/frontend/apps/electron/out/*/make/zip/win32/${{ matrix.spec.arch }}/AFFiNE*-win32-${{ matrix.spec.arch }}-*.zip ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.zip
|
||||
mv packages/frontend/apps/electron/out/*/make/squirrel.windows/${{ matrix.spec.arch }}/*.exe ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.exe
|
||||
mv packages/frontend/apps/electron/out/*/make/nsis.windows/${{ matrix.spec.arch }}/*.exe ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.nsis.exe
|
||||
|
||||
- uses: actions/attest-build-provenance@v1
|
||||
with:
|
||||
subject-path: |
|
||||
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-x64.zip
|
||||
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-x64.exe
|
||||
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-x64.nsis.exe
|
||||
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.zip
|
||||
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.exe
|
||||
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.nsis.exe
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -391,6 +426,11 @@ jobs:
|
||||
with:
|
||||
name: affine-win32-x64-builds
|
||||
path: ./
|
||||
- name: Download Artifacts (windows-arm64)
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: affine-win32-arm64-builds
|
||||
path: ./
|
||||
- name: Download Artifacts (linux-x64)
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
|
||||
2
.github/workflows/workers.yml
vendored
2
.github/workflows/workers.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Publish
|
||||
uses: cloudflare/wrangler-action@v3.7.0
|
||||
uses: cloudflare/wrangler-action@v3.12.1
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -12,4 +12,4 @@ npmPublishAccess: public
|
||||
|
||||
npmPublishRegistry: "https://registry.npmjs.org"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.5.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.5.1.cjs
|
||||
|
||||
581
Cargo.lock
generated
581
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
51
Cargo.toml
51
Cargo.toml
@@ -1,29 +1,36 @@
|
||||
[workspace]
|
||||
members = ["./packages/backend/native", "./packages/frontend/native", "./packages/frontend/native/schema"]
|
||||
members = [
|
||||
"./packages/backend/native",
|
||||
"./packages/common/native",
|
||||
"./packages/frontend/native",
|
||||
"./packages/frontend/native/schema"
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
chrono = "0.4"
|
||||
dotenv = "0.15"
|
||||
file-format = { version = "0.25", features = ["reader"] }
|
||||
mimalloc = "0.1"
|
||||
napi = { version = "3.0.0-alpha.12", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
||||
napi-build = { version = "2" }
|
||||
napi-derive = { version = "3.0.0-alpha.12" }
|
||||
notify = { version = "7", features = ["serde"] }
|
||||
once_cell = "1"
|
||||
parking_lot = "0.12"
|
||||
rand = "0.8"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
sha3 = "0.10"
|
||||
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
|
||||
tiktoken-rs = "0.6"
|
||||
tokio = "1.37"
|
||||
uuid = "1.8"
|
||||
v_htmlescape = "0.15"
|
||||
y-octo = { git = "https://github.com/y-crdt/y-octo.git", branch = "main" }
|
||||
affine_common = { path = "./packages/common/native" }
|
||||
anyhow = "1"
|
||||
chrono = "0.4"
|
||||
dotenv = "0.15"
|
||||
file-format = { version = "0.26", features = ["reader"] }
|
||||
mimalloc = "0.1"
|
||||
napi = { version = "3.0.0-alpha.12", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
||||
napi-build = { version = "2" }
|
||||
napi-derive = { version = "3.0.0-alpha.12" }
|
||||
notify = { version = "7", features = ["serde"] }
|
||||
once_cell = "1"
|
||||
parking_lot = "0.12"
|
||||
rand = "0.8"
|
||||
rayon = "1.10"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
sha3 = "0.10"
|
||||
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
|
||||
tiktoken-rs = "0.6"
|
||||
tokio = "1.37"
|
||||
uuid = "1.8"
|
||||
v_htmlescape = "0.15"
|
||||
y-octo = { git = "https://github.com/y-crdt/y-octo.git", branch = "main" }
|
||||
|
||||
[profile.dev.package.sqlx-macros]
|
||||
opt-level = 3
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
],
|
||||
"ext": "ts,md,json"
|
||||
},
|
||||
"version": "0.17.0"
|
||||
"version": "0.18.0"
|
||||
}
|
||||
|
||||
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/monorepo",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"private": true,
|
||||
"author": "toeverything",
|
||||
"license": "MIT",
|
||||
@@ -58,7 +58,8 @@
|
||||
"@faker-js/faker": "^9.0.0",
|
||||
"@istanbuljs/schema": "^0.1.3",
|
||||
"@magic-works/i18n-codegen": "^0.6.0",
|
||||
"@playwright/test": "=1.47.2",
|
||||
"@playwright/test": "=1.48.2",
|
||||
"@swc/core": "^1.9.1",
|
||||
"@taplo/cli": "^0.7.0",
|
||||
"@toeverything/infra": "workspace:*",
|
||||
"@types/affine__env": "workspace:*",
|
||||
@@ -67,8 +68,8 @@
|
||||
"@typescript-eslint/eslint-plugin": "^7.6.0",
|
||||
"@typescript-eslint/parser": "^7.6.0",
|
||||
"@vanilla-extract/vite-plugin": "^4.0.7",
|
||||
"@vitest/coverage-istanbul": "2.1.1",
|
||||
"@vitest/ui": "2.1.1",
|
||||
"@vitest/coverage-istanbul": "2.1.4",
|
||||
"@vitest/ui": "2.1.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "^33.0.0",
|
||||
"eslint": "^8.57.0",
|
||||
@@ -87,16 +88,16 @@
|
||||
"msw": "^2.3.0",
|
||||
"nx": "^20.0.3",
|
||||
"nx-cloud": "^19.1.0",
|
||||
"oxlint": "0.11.0",
|
||||
"oxlint": "0.11.1",
|
||||
"prettier": "^3.3.3",
|
||||
"semver": "^7.6.0",
|
||||
"serve": "^14.2.1",
|
||||
"typescript": "^5.6.3",
|
||||
"unplugin-swc": "^1.4.5",
|
||||
"vite": "^5.2.8",
|
||||
"vitest": "2.1.1"
|
||||
"vitest": "2.1.4"
|
||||
},
|
||||
"packageManager": "yarn@4.5.0",
|
||||
"packageManager": "yarn@4.5.1",
|
||||
"resolutions": {
|
||||
"array-buffer-byte-length": "npm:@nolyfill/array-buffer-byte-length@latest",
|
||||
"array-includes": "npm:@nolyfill/array-includes@latest",
|
||||
|
||||
@@ -7,15 +7,16 @@ version = "1.0.0"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true }
|
||||
file-format = { workspace = true }
|
||||
napi = { workspace = true }
|
||||
napi-derive = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
sha3 = { workspace = true }
|
||||
tiktoken-rs = { workspace = true }
|
||||
v_htmlescape = { workspace = true }
|
||||
y-octo = { workspace = true }
|
||||
affine_common = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
file-format = { workspace = true }
|
||||
napi = { workspace = true }
|
||||
napi-derive = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
sha3 = { workspace = true }
|
||||
tiktoken-rs = { workspace = true }
|
||||
v_htmlescape = { workspace = true }
|
||||
y-octo = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||
mimalloc = { workspace = true }
|
||||
@@ -24,7 +25,8 @@ mimalloc = { workspace = true }
|
||||
mimalloc = { workspace = true, features = ["local_dynamic_tls"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = "1"
|
||||
rayon = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/server-native",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"engines": {
|
||||
"node": ">= 10.16.0 < 11 || >= 11.8.0"
|
||||
},
|
||||
@@ -33,7 +33,7 @@
|
||||
"build:debug": "napi build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "3.0.0-alpha.62",
|
||||
"@napi-rs/cli": "3.0.0-alpha.64",
|
||||
"lib0": "^0.2.93",
|
||||
"nx": "^20.0.3",
|
||||
"nx-cloud": "^19.1.0",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../../frontend/native/src/hashcash.rs
|
||||
69
packages/backend/native/src/hashcash.rs
Normal file
69
packages/backend/native/src/hashcash.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use affine_common::hashcash::Stamp;
|
||||
use napi::{bindgen_prelude::AsyncTask, Env, JsBoolean, JsString, Result as NapiResult, Task};
|
||||
use napi_derive::napi;
|
||||
|
||||
pub struct AsyncVerifyChallengeResponse {
|
||||
response: String,
|
||||
bits: u32,
|
||||
resource: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl Task for AsyncVerifyChallengeResponse {
|
||||
type Output = bool;
|
||||
type JsValue = JsBoolean;
|
||||
|
||||
fn compute(&mut self) -> NapiResult<Self::Output> {
|
||||
Ok(if let Ok(stamp) = Stamp::try_from(self.response.as_str()) {
|
||||
stamp.check(self.bits, &self.resource)
|
||||
} else {
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve(&mut self, env: Env, output: bool) -> NapiResult<Self::JsValue> {
|
||||
env.get_boolean(output)
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn verify_challenge_response(
|
||||
response: String,
|
||||
bits: u32,
|
||||
resource: String,
|
||||
) -> AsyncTask<AsyncVerifyChallengeResponse> {
|
||||
AsyncTask::new(AsyncVerifyChallengeResponse {
|
||||
response,
|
||||
bits,
|
||||
resource,
|
||||
})
|
||||
}
|
||||
|
||||
pub struct AsyncMintChallengeResponse {
|
||||
bits: Option<u32>,
|
||||
resource: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl Task for AsyncMintChallengeResponse {
|
||||
type Output = String;
|
||||
type JsValue = JsString;
|
||||
|
||||
fn compute(&mut self) -> NapiResult<Self::Output> {
|
||||
Ok(Stamp::mint(self.resource.clone(), self.bits).format())
|
||||
}
|
||||
|
||||
fn resolve(&mut self, env: Env, output: String) -> NapiResult<Self::JsValue> {
|
||||
env.create_string(&output)
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn mint_challenge_response(
|
||||
resource: String,
|
||||
bits: Option<u32>,
|
||||
) -> AsyncTask<AsyncMintChallengeResponse> {
|
||||
AsyncTask::new(AsyncMintChallengeResponse { bits, resource })
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
provider = "postgresql"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/server",
|
||||
"private": true,
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"description": "Affine Node.js server",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -12,9 +12,9 @@
|
||||
"start": "node --loader ts-node/esm/transpile-only.mjs ./src/index.ts",
|
||||
"dev": "nodemon ./src/index.ts",
|
||||
"test": "ava --concurrency 1 --serial",
|
||||
"test:copilot": "ava --concurrency 1 --serial \"tests/**/copilot-*.e2e.ts\"",
|
||||
"test:copilot": "ava \"tests/**/copilot-*.spec.ts\"",
|
||||
"test:coverage": "c8 ava --concurrency 1 --serial",
|
||||
"test:copilot:coverage": "c8 ava --timeout=5m --concurrency 1 --serial \"tests/**/copilot-*.e2e.ts\"",
|
||||
"test:copilot:coverage": "c8 ava --timeout=5m \"tests/**/copilot-*.spec.ts\"",
|
||||
"postinstall": "prisma generate",
|
||||
"data-migration": "node --loader ts-node/esm/transpile-only.mjs ./src/data/index.ts",
|
||||
"predeploy": "yarn prisma migrate deploy && node --import ./scripts/register.js ./dist/data/index.js run",
|
||||
@@ -23,8 +23,8 @@
|
||||
"dependencies": {
|
||||
"@apollo/server": "^4.10.2",
|
||||
"@aws-sdk/client-s3": "^3.620.0",
|
||||
"@fal-ai/serverless-client": "^0.14.0",
|
||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.19.0",
|
||||
"@fal-ai/serverless-client": "^0.15.0",
|
||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.20.0",
|
||||
"@google-cloud/opentelemetry-cloud-trace-exporter": "^2.2.0",
|
||||
"@google-cloud/opentelemetry-resource-util": "^2.2.0",
|
||||
"@nestjs/apollo": "^12.1.0",
|
||||
@@ -41,18 +41,18 @@
|
||||
"@node-rs/crc32": "^1.10.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/core": "^1.25.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.53.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.54.0",
|
||||
"@opentelemetry/exporter-zipkin": "^1.25.0",
|
||||
"@opentelemetry/host-metrics": "^0.35.2",
|
||||
"@opentelemetry/instrumentation": "^0.53.0",
|
||||
"@opentelemetry/instrumentation-graphql": "^0.43.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.53.0",
|
||||
"@opentelemetry/instrumentation-ioredis": "^0.43.0",
|
||||
"@opentelemetry/instrumentation-nestjs-core": "^0.40.0",
|
||||
"@opentelemetry/instrumentation-socket.io": "^0.42.0",
|
||||
"@opentelemetry/instrumentation": "^0.54.0",
|
||||
"@opentelemetry/instrumentation-graphql": "^0.44.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.54.0",
|
||||
"@opentelemetry/instrumentation-ioredis": "^0.44.0",
|
||||
"@opentelemetry/instrumentation-nestjs-core": "^0.41.0",
|
||||
"@opentelemetry/instrumentation-socket.io": "^0.43.0",
|
||||
"@opentelemetry/resources": "^1.25.0",
|
||||
"@opentelemetry/sdk-metrics": "^1.25.0",
|
||||
"@opentelemetry/sdk-node": "^0.53.0",
|
||||
"@opentelemetry/sdk-node": "^0.54.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.25.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.25.0",
|
||||
"@prisma/client": "^5.15.0",
|
||||
|
||||
@@ -147,9 +147,11 @@ export class SelfhostModule implements OnModuleInit {
|
||||
|
||||
// fallback all unknown routes
|
||||
app.get([basePath, basePath + '/*'], this.check.use, (req, res) => {
|
||||
const mobile = isMobile({
|
||||
ua: req.headers['user-agent'] ?? undefined,
|
||||
});
|
||||
const mobile =
|
||||
this.config.AFFINE_ENV === 'dev' &&
|
||||
isMobile({
|
||||
ua: req.headers['user-agent'] ?? undefined,
|
||||
});
|
||||
|
||||
return res.sendFile(
|
||||
join(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import pkg from '../../../package.json' assert { type: 'json' };
|
||||
import pkg from '../../../package.json' with { type: 'json' };
|
||||
import {
|
||||
AFFINE_ENV,
|
||||
AFFiNEConfig,
|
||||
|
||||
@@ -26,21 +26,20 @@ export const CallMetric = (
|
||||
return desc;
|
||||
}
|
||||
|
||||
desc.value = async function (...args: any[]) {
|
||||
const timer = metrics[scope].histogram(name, {
|
||||
description: `function call time costs of ${name}`,
|
||||
unit: 'ms',
|
||||
});
|
||||
const count = metrics[scope].counter(`${name}_calls`, {
|
||||
description: `function call counter of ${name}`,
|
||||
});
|
||||
const errorCount = metrics[scope].counter(`${name}_errors`, {
|
||||
description: `function call error counter of ${name}`,
|
||||
});
|
||||
const timer = metrics[scope].histogram('function_timer', {
|
||||
description: 'function call time costs',
|
||||
unit: 'ms',
|
||||
});
|
||||
const count = metrics[scope].counter('function_calls', {
|
||||
description: 'function call counter',
|
||||
});
|
||||
|
||||
desc.value = async function (...args: any[]) {
|
||||
const start = Date.now();
|
||||
let error = false;
|
||||
|
||||
const end = () => {
|
||||
timer?.record(Date.now() - start, attrs);
|
||||
timer?.record(Date.now() - start, { ...attrs, name, error });
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -50,10 +49,11 @@ export const CallMetric = (
|
||||
return await originalMethod.apply(this, args);
|
||||
} catch (err) {
|
||||
if (!record || !!record.error) {
|
||||
errorCount.add(1, attrs);
|
||||
error = true;
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
count.add(1, { ...attrs, name, error });
|
||||
if (!record || !!record.timer) {
|
||||
end();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { defineStartupConfig, ModuleConfig } from '../../fundamentals/config';
|
||||
import {
|
||||
defineRuntimeConfig,
|
||||
defineStartupConfig,
|
||||
ModuleConfig,
|
||||
} from '../../fundamentals/config';
|
||||
import { CaptchaConfig } from './types';
|
||||
|
||||
declare module '../config' {
|
||||
interface PluginsConfig {
|
||||
captcha: ModuleConfig<CaptchaConfig>;
|
||||
captcha: ModuleConfig<
|
||||
CaptchaConfig,
|
||||
{
|
||||
enable: boolean;
|
||||
}
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,3 +30,10 @@ defineStartupConfig('plugins.captcha', {
|
||||
bits: 20,
|
||||
},
|
||||
});
|
||||
|
||||
defineRuntimeConfig('plugins.captcha', {
|
||||
enable: {
|
||||
desc: 'Check captcha challenge when user authenticating the app.',
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
Config,
|
||||
getRequestResponseFromContext,
|
||||
GuardProvider,
|
||||
} from '../../fundamentals';
|
||||
@@ -18,11 +19,18 @@ export class CaptchaGuardProvider
|
||||
{
|
||||
name = 'captcha' as const;
|
||||
|
||||
constructor(private readonly captcha: CaptchaService) {
|
||||
constructor(
|
||||
private readonly captcha: CaptchaService,
|
||||
private readonly config: Config
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
if (!(await this.config.runtime.fetch('plugins.captcha/enable'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { req } = getRequestResponseFromContext(context);
|
||||
|
||||
// require headers, old client send through query string
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import './config';
|
||||
|
||||
import { AuthModule } from '../../core/auth';
|
||||
import { ServerFeature } from '../../core/config';
|
||||
import { Plugin } from '../registry';
|
||||
|
||||
@@ -88,15 +88,14 @@ export class CaptchaService {
|
||||
|
||||
async verifyRequest(credential: Credential, req: Request) {
|
||||
const challenge = credential.challenge;
|
||||
let resource: string | null = null;
|
||||
if (typeof challenge === 'string' && challenge) {
|
||||
const resource = await this.token
|
||||
resource = await this.token
|
||||
.getToken(TokenType.Challenge, challenge)
|
||||
.then(token => token?.credential);
|
||||
|
||||
if (!resource) {
|
||||
throw new CaptchaVerificationFailed('Invalid Challenge');
|
||||
}
|
||||
.then(token => token?.credential || null);
|
||||
}
|
||||
|
||||
if (resource) {
|
||||
const isChallengeVerified = await this.verifyChallengeResponse(
|
||||
credential.token,
|
||||
resource
|
||||
|
||||
@@ -126,7 +126,7 @@ export class OpenAIProvider
|
||||
});
|
||||
}
|
||||
|
||||
protected checkParams({
|
||||
protected async checkParams({
|
||||
messages,
|
||||
embeddings,
|
||||
model,
|
||||
@@ -137,7 +137,7 @@ export class OpenAIProvider
|
||||
model: string;
|
||||
options: CopilotChatOptions;
|
||||
}) {
|
||||
if (!this.availableModels.includes(model)) {
|
||||
if (!(await this.isModelAvailable(model))) {
|
||||
throw new CopilotPromptInvalid(`Invalid model: ${model}`);
|
||||
}
|
||||
if (Array.isArray(messages) && messages.length > 0) {
|
||||
@@ -219,7 +219,7 @@ export class OpenAIProvider
|
||||
model: string = 'gpt-4o-mini',
|
||||
options: CopilotChatOptions = {}
|
||||
): Promise<string> {
|
||||
this.checkParams({ messages, model, options });
|
||||
await this.checkParams({ messages, model, options });
|
||||
|
||||
try {
|
||||
metrics.ai.counter('chat_text_calls').add(1, { model });
|
||||
@@ -250,7 +250,7 @@ export class OpenAIProvider
|
||||
model: string = 'gpt-4o-mini',
|
||||
options: CopilotChatOptions = {}
|
||||
): AsyncIterable<string> {
|
||||
this.checkParams({ messages, model, options });
|
||||
await this.checkParams({ messages, model, options });
|
||||
|
||||
try {
|
||||
metrics.ai.counter('chat_text_stream_calls').add(1, { model });
|
||||
@@ -300,7 +300,7 @@ export class OpenAIProvider
|
||||
options: CopilotEmbeddingOptions = { dimensions: DEFAULT_DIMENSIONS }
|
||||
): Promise<number[][]> {
|
||||
messages = Array.isArray(messages) ? messages : [messages];
|
||||
this.checkParams({ embeddings: messages, model, options });
|
||||
await this.checkParams({ embeddings: messages, model, options });
|
||||
|
||||
try {
|
||||
metrics.ai.counter('generate_embedding_calls').add(1, { model });
|
||||
|
||||
@@ -14,7 +14,7 @@ const OIDCTokenSchema = z.object({
|
||||
access_token: z.string(),
|
||||
expires_in: z.number(),
|
||||
refresh_token: z.string(),
|
||||
scope: z.string(),
|
||||
scope: z.string().optional(),
|
||||
token_type: z.string(),
|
||||
});
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ const runIfCopilotConfigured = test.macro(
|
||||
}
|
||||
);
|
||||
|
||||
test.beforeEach(async t => {
|
||||
test.serial.before(async t => {
|
||||
const module = await createTestingModule({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
@@ -101,7 +101,7 @@ test.beforeEach(async t => {
|
||||
};
|
||||
});
|
||||
|
||||
test.beforeEach(async t => {
|
||||
test.serial.before(async t => {
|
||||
const { prompt, executors } = t.context;
|
||||
|
||||
executors.image.register();
|
||||
@@ -121,12 +121,12 @@ test.beforeEach(async t => {
|
||||
}
|
||||
});
|
||||
|
||||
test.afterEach.always(async _ => {
|
||||
test.after(async _ => {
|
||||
unregisterCopilotProvider(OpenAIProvider.type);
|
||||
unregisterCopilotProvider(FalProvider.type);
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
test.after(async t => {
|
||||
await t.context.module.close();
|
||||
});
|
||||
|
||||
@@ -143,7 +143,7 @@ const assertNotWrappedInCodeBlock = (
|
||||
|
||||
const checkMDList = (text: string) => {
|
||||
const lines = text.split('\n');
|
||||
const listItemRegex = /^( {2})*(-|\*|\+) .+$/;
|
||||
const listItemRegex = /^( {2})*(-|\u2010-\u2015|\*|\+)? .+$/;
|
||||
let prevIndent = null;
|
||||
|
||||
for (const line of lines) {
|
||||
@@ -166,7 +166,9 @@ const checkMDList = (text: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
prevIndent = currentIndent;
|
||||
if (line.trim().startsWith('-')) {
|
||||
prevIndent = currentIndent;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -190,14 +192,14 @@ const retry = async (
|
||||
while (i--) {
|
||||
const ret = await t.try(callback);
|
||||
if (ret.passed) {
|
||||
ret.commit();
|
||||
break;
|
||||
return ret.commit();
|
||||
} else {
|
||||
ret.discard();
|
||||
t.log(ret.errors.map(e => e.message).join('\n'));
|
||||
t.log(`retrying ${action} ${3 - i}/3 ...`);
|
||||
}
|
||||
}
|
||||
t.fail(`failed to run ${action}`);
|
||||
};
|
||||
|
||||
// ==================== utils ====================
|
||||
@@ -248,6 +250,16 @@ test('should validate markdown list', t => {
|
||||
- item 1.1.1.1
|
||||
`)
|
||||
);
|
||||
t.true(
|
||||
checkMDList(`
|
||||
- item 1
|
||||
- item 1.1
|
||||
- item 1.1.1.1
|
||||
item 1.1.1.1 line breaks
|
||||
- item 1.1.1.2
|
||||
`),
|
||||
'should allow line breaks'
|
||||
);
|
||||
});
|
||||
|
||||
// ==================== action ====================
|
||||
@@ -447,14 +459,14 @@ const workflows = [
|
||||
{
|
||||
name: 'brainstorm',
|
||||
content: 'apple company',
|
||||
verifier: (t: ExecutionContext<Tester>, result: string) => {
|
||||
verifier: (t: ExecutionContext, result: string) => {
|
||||
t.assert(checkMDList(result), 'should be a markdown list');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'presentation',
|
||||
content: 'apple company',
|
||||
verifier: (t: ExecutionContext<Tester>, result: string) => {
|
||||
verifier: (t: ExecutionContext, result: string) => {
|
||||
for (const l of result.split('\n')) {
|
||||
t.notThrows(() => {
|
||||
JSON.parse(l.trim());
|
||||
@@ -475,11 +487,11 @@ for (const { name, content, verifier } of workflows) {
|
||||
let result = '';
|
||||
for await (const ret of workflow.runGraph({ content }, name)) {
|
||||
if (ret.status === GraphExecutorState.EnterNode) {
|
||||
console.log('enter node:', ret.node.name);
|
||||
t.log('enter node:', ret.node.name);
|
||||
} else if (ret.status === GraphExecutorState.ExitNode) {
|
||||
console.log('exit node:', ret.node.name);
|
||||
t.log('exit node:', ret.node.name);
|
||||
} else if (ret.status === GraphExecutorState.EmitAttachment) {
|
||||
console.log('stream attachment:', ret);
|
||||
t.log('stream attachment:', ret);
|
||||
} else {
|
||||
result += ret.content;
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.12",
|
||||
"vitest": "2.1.1"
|
||||
"vitest": "2.1.4"
|
||||
},
|
||||
"version": "0.17.0"
|
||||
"version": "0.18.0"
|
||||
}
|
||||
|
||||
6
packages/common/env/package.json
vendored
6
packages/common/env/package.json
vendored
@@ -3,8 +3,8 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@blocksuite/affine": "0.17.25",
|
||||
"vitest": "2.1.1"
|
||||
"@blocksuite/affine": "0.17.33",
|
||||
"vitest": "2.1.4"
|
||||
},
|
||||
"exports": {
|
||||
"./automation": "./src/automation.ts",
|
||||
@@ -21,5 +21,5 @@
|
||||
"dependencies": {
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"version": "0.17.0"
|
||||
"version": "0.18.0"
|
||||
}
|
||||
|
||||
@@ -8,14 +8,16 @@
|
||||
"./storage": "./src/storage/index.ts",
|
||||
"./utils": "./src/utils/index.ts",
|
||||
"./app-config-storage": "./src/app-config-storage.ts",
|
||||
"./op": "./src/op/index.ts",
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/debug": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@blocksuite/affine": "0.17.25",
|
||||
"@blocksuite/affine": "0.17.33",
|
||||
"@datastructures-js/binary-search-tree": "^5.3.2",
|
||||
"eventemitter2": "^6.4.9",
|
||||
"foxact": "^0.2.33",
|
||||
"fractional-indexing": "^3.2.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
@@ -36,7 +38,7 @@
|
||||
"fake-indexeddb": "^6.0.0",
|
||||
"react": "^18.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"vitest": "2.1.1"
|
||||
"vitest": "2.1.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@affine/templates": "*",
|
||||
@@ -58,5 +60,5 @@
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"version": "0.17.0"
|
||||
"version": "0.18.0"
|
||||
}
|
||||
|
||||
@@ -4,9 +4,6 @@ import type { FrameworkProvider, Scope, Service } from '../core';
|
||||
import { ComponentNotFoundError, Framework } from '../core';
|
||||
import { parseIdentifier } from '../core/identifier';
|
||||
import type { GeneralIdentifier, IdentifierType, Type } from '../core/types';
|
||||
import { MountPoint } from './scope-root-components';
|
||||
|
||||
export { useMount } from './scope-root-components';
|
||||
|
||||
export const FrameworkStackContext = React.createContext<FrameworkProvider[]>([
|
||||
Framework.EMPTY.provider(),
|
||||
@@ -129,7 +126,7 @@ export const FrameworkScope = ({
|
||||
|
||||
return (
|
||||
<FrameworkStackContext.Provider value={nextStack}>
|
||||
<MountPoint>{children}</MountPoint>
|
||||
{children}
|
||||
</FrameworkStackContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
type NodesMap = Map<
|
||||
number,
|
||||
{
|
||||
node: React.ReactNode;
|
||||
debugKey?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
const ScopeRootComponentsContext = React.createContext<{
|
||||
nodes: NodesMap;
|
||||
setNodes: React.Dispatch<React.SetStateAction<NodesMap>>;
|
||||
}>({ nodes: new Map(), setNodes: () => {} });
|
||||
|
||||
let _id = 0;
|
||||
/**
|
||||
* A hook to add nodes to the nearest scope's root
|
||||
*/
|
||||
export const useMount = (debugKey?: string) => {
|
||||
const [id] = React.useState(_id++);
|
||||
const { setNodes } = React.useContext(ScopeRootComponentsContext);
|
||||
|
||||
const unmount = React.useCallback(() => {
|
||||
setNodes(prev => {
|
||||
if (!prev.has(id)) {
|
||||
return prev;
|
||||
}
|
||||
const next = new Map(prev);
|
||||
next.delete(id);
|
||||
return next;
|
||||
});
|
||||
}, [id, setNodes]);
|
||||
|
||||
const mount = React.useCallback(
|
||||
(node: React.ReactNode) => {
|
||||
setNodes(prev => new Map(prev).set(id, { node, debugKey }));
|
||||
return unmount;
|
||||
},
|
||||
[setNodes, id, debugKey, unmount]
|
||||
);
|
||||
|
||||
return React.useMemo(() => {
|
||||
return {
|
||||
/**
|
||||
* Add a node to the nearest scope root
|
||||
* ```tsx
|
||||
* const { mount } = useMount();
|
||||
* useEffect(() => {
|
||||
* const unmount = mount(<div>Node</div>);
|
||||
* return unmount;
|
||||
* }, [])
|
||||
* ```
|
||||
* @return A function to unmount the added node.
|
||||
*/
|
||||
mount,
|
||||
};
|
||||
}, [mount]);
|
||||
};
|
||||
|
||||
export const MountPoint = ({ children }: React.PropsWithChildren) => {
|
||||
const [nodes, setNodes] = React.useState<NodesMap>(new Map());
|
||||
|
||||
return (
|
||||
<ScopeRootComponentsContext.Provider value={{ nodes, setNodes }}>
|
||||
{children}
|
||||
{Array.from(nodes.entries()).map(([id, { node, debugKey }]) => (
|
||||
<div data-testid={debugKey} key={id} style={{ display: 'contents' }}>
|
||||
{node}
|
||||
</div>
|
||||
))}
|
||||
</ScopeRootComponentsContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -16,6 +16,7 @@ export const AFFiNE_WORKSPACE_DB_SCHEMA = {
|
||||
primaryMode: f.string().optional(),
|
||||
edgelessColorTheme: f.string().optional(),
|
||||
journal: f.string().optional(),
|
||||
pageWidth: f.string().optional(),
|
||||
}),
|
||||
docCustomPropertyInfo: {
|
||||
id: f.string().primaryKey().optional().default(nanoid),
|
||||
|
||||
@@ -45,4 +45,10 @@ export const BUILT_IN_CUSTOM_PROPERTY_TYPE = [
|
||||
show: 'always-hide',
|
||||
index: 'a0000007',
|
||||
},
|
||||
{
|
||||
id: 'pageWidth',
|
||||
type: 'pageWidth',
|
||||
show: 'always-hide',
|
||||
index: 'a0000008',
|
||||
},
|
||||
] as DocCustomPropertyInfo[];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import {
|
||||
type AffineTextAttributes,
|
||||
@@ -16,6 +17,8 @@ import { DocScope } from '../scopes/doc';
|
||||
import type { DocsStore } from '../stores/docs';
|
||||
import { DocService } from './doc';
|
||||
|
||||
const logger = new DebugLogger('DocsService');
|
||||
|
||||
export class DocsService extends Service {
|
||||
list = this.framework.createEntity(DocRecordList);
|
||||
|
||||
@@ -52,6 +55,15 @@ export class DocsService extends Service {
|
||||
record: docRecord,
|
||||
});
|
||||
|
||||
try {
|
||||
blockSuiteDoc.load();
|
||||
} catch (e) {
|
||||
logger.error('Failed to load doc', {
|
||||
docId,
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
|
||||
const doc = docScope.get(DocService).doc;
|
||||
|
||||
const { obj, release } = this.pool.put(docId, doc);
|
||||
|
||||
@@ -64,7 +64,7 @@ export class DocPropertiesStore extends Store {
|
||||
createDocPropertyInfo(
|
||||
config: Omit<DocCustomPropertyInfo, 'id'> & { id?: string }
|
||||
) {
|
||||
return this.dbService.db.docCustomPropertyInfo.create(config).id;
|
||||
return this.dbService.db.docCustomPropertyInfo.create(config);
|
||||
}
|
||||
|
||||
removeDocPropertyInfo(id: string) {
|
||||
|
||||
@@ -8,8 +8,10 @@ const isMobile = BUILD_CONFIG.isMobileEdition;
|
||||
export const AFFINE_FLAGS = {
|
||||
enable_ai: {
|
||||
category: 'affine',
|
||||
displayName: 'Enable AI',
|
||||
description: 'Enable or disable ALL AI features.',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-ai.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-ai.description',
|
||||
hide: true,
|
||||
configurable: true,
|
||||
defaultState: true,
|
||||
@@ -17,77 +19,96 @@ export const AFFINE_FLAGS = {
|
||||
enable_database_full_width: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_database_full_width',
|
||||
displayName: 'Database Full Width',
|
||||
description: 'The database will be displayed in full-width mode.',
|
||||
configurable: isNotStableBuild,
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-database-full-width.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-database-full-width.description',
|
||||
configurable: isCanaryBuild,
|
||||
},
|
||||
enable_database_attachment_note: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_database_attachment_note',
|
||||
displayName: 'Database Attachment Note',
|
||||
description: 'Allows adding notes to database attachments.',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-database-attachment-note.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-database-attachment-note.description',
|
||||
configurable: isNotStableBuild,
|
||||
},
|
||||
enable_block_query: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_block_query',
|
||||
displayName: 'Todo Block Query',
|
||||
description: 'Enables querying of todo blocks.',
|
||||
configurable: isNotStableBuild,
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-block-query.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-block-query.description',
|
||||
configurable: isCanaryBuild,
|
||||
},
|
||||
enable_synced_doc_block: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_synced_doc_block',
|
||||
displayName: 'Synced Doc Block',
|
||||
description: 'Enables syncing of doc blocks.',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-synced-doc-block.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-synced-doc-block.description',
|
||||
configurable: false,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_edgeless_text: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_edgeless_text',
|
||||
displayName: 'Edgeless Text',
|
||||
description: 'Enables edgeless text blocks.',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-edgeless-text.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-edgeless-text.description',
|
||||
configurable: false,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_color_picker: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_color_picker',
|
||||
displayName: 'Color Picker',
|
||||
description: 'Enables color picker blocks.',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-color-picker.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-color-picker.description',
|
||||
configurable: false,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_ai_chat_block: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_ai_chat_block',
|
||||
displayName: 'AI Chat Block',
|
||||
description: 'Enables AI chat blocks.',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-ai-chat-block.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-ai-chat-block.description',
|
||||
configurable: false,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_ai_onboarding: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_ai_onboarding',
|
||||
displayName: 'AI Onboarding',
|
||||
description: 'Enables AI onboarding.',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-ai-onboarding.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-ai-onboarding.description',
|
||||
configurable: false,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_mind_map_import: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_mind_map_import',
|
||||
displayName: 'Mind Map Import',
|
||||
description: 'Enables mind map import.',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-mind-map-import.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-mind-map-import.description',
|
||||
configurable: false,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_multi_view: {
|
||||
category: 'affine',
|
||||
displayName: 'Split View',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-multi-view.name',
|
||||
description:
|
||||
'The Split View feature enables you to divide your tab into multiple sections for simultaneous viewing and editing of different documents.',
|
||||
'com.affine.settings.workspace.experimental-features.enable-multi-view.description',
|
||||
feedbackType: 'discord',
|
||||
feedbackLink:
|
||||
'https://discord.com/channels/959027316334407691/1280009690004324405',
|
||||
@@ -96,9 +117,11 @@ export const AFFINE_FLAGS = {
|
||||
},
|
||||
enable_emoji_folder_icon: {
|
||||
category: 'affine',
|
||||
displayName: 'Emoji Folder Icon',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-emoji-folder-icon.name',
|
||||
description:
|
||||
'Once enabled, you can use an emoji as the folder icon. When the first character of the folder name is an emoji, it will be extracted and used as its icon.',
|
||||
'com.affine.settings.workspace.experimental-features.enable-emoji-folder-icon.description',
|
||||
|
||||
feedbackType: 'discord',
|
||||
feedbackLink:
|
||||
'https://discord.com/channels/959027316334407691/1280014319865696351/1280014319865696351',
|
||||
@@ -107,9 +130,10 @@ export const AFFINE_FLAGS = {
|
||||
},
|
||||
enable_emoji_doc_icon: {
|
||||
category: 'affine',
|
||||
displayName: 'Emoji Doc Icon',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-emoji-doc-icon.name',
|
||||
description:
|
||||
'Once enabled, you can use an emoji as the page icon. When the first character of the folder name is an emoji, it will be extracted and used as its icon.',
|
||||
'com.affine.settings.workspace.experimental-features.enable-emoji-doc-icon.description',
|
||||
feedbackType: 'discord',
|
||||
feedbackLink:
|
||||
'https://discord.com/channels/959027316334407691/1280014319865696351',
|
||||
@@ -118,57 +142,69 @@ export const AFFINE_FLAGS = {
|
||||
},
|
||||
enable_editor_settings: {
|
||||
category: 'affine',
|
||||
displayName: 'Editor Settings',
|
||||
description: 'Enables editor settings.',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-editor-settings.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-editor-settings.description',
|
||||
configurable: false,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_offline_mode: {
|
||||
category: 'affine',
|
||||
displayName: 'Offline Mode',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-offline-mode.name',
|
||||
description:
|
||||
'Stop Connecting to the Internet. Even with AFFiNE Cloud, enabling this toggle stops internet connection and keeps everything local, but syncing will be disabled.',
|
||||
'com.affine.settings.workspace.experimental-features.enable-offline-mode.description',
|
||||
configurable: isDesktopEnvironment,
|
||||
defaultState: false,
|
||||
},
|
||||
enable_theme_editor: {
|
||||
category: 'affine',
|
||||
displayName: 'Theme Editor',
|
||||
description: 'Enables theme editor.',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-theme-editor.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-theme-editor.description',
|
||||
configurable: isCanaryBuild,
|
||||
defaultState: isCanaryBuild,
|
||||
},
|
||||
enable_local_workspace: {
|
||||
category: 'affine',
|
||||
displayName: 'Allow create local workspace',
|
||||
description: 'Allow create local workspace.',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-local-workspace.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-local-workspace.description',
|
||||
configurable: isCanaryBuild,
|
||||
defaultState: isDesktopEnvironment || isCanaryBuild,
|
||||
},
|
||||
enable_advanced_block_visibility: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_advanced_block_visibility',
|
||||
displayName: 'Advanced block visibility control',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-advanced-block-visibility.name',
|
||||
description:
|
||||
'To provide detailed control over which edgeless blocks are visible in page mode.',
|
||||
'com.affine.settings.workspace.experimental-features.enable-advanced-block-visibility.description',
|
||||
configurable: true,
|
||||
defaultState: false,
|
||||
},
|
||||
enable_mobile_keyboard_toolbar: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_mobile_keyboard_toolbar',
|
||||
displayName: 'Mobile Keyboard Toolbar',
|
||||
description: 'Enables the mobile keyboard toolbar.',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-mobile-keyboard-toolbar.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-mobile-keyboard-toolbar.description',
|
||||
configurable: false,
|
||||
defaultState: isMobile,
|
||||
},
|
||||
enable_snapshot_import_export: {
|
||||
category: 'affine',
|
||||
displayName: 'Enable Snapshot Import Export',
|
||||
enable_mobile_linked_doc_menu: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_mobile_linked_doc_menu',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-mobile-linked-doc-menu.name',
|
||||
description:
|
||||
'Once enabled, users can import and export blocksuite snapshots',
|
||||
configurable: true,
|
||||
defaultState: false,
|
||||
'com.affine.settings.workspace.experimental-features.enable-mobile-linked-doc-menu.description',
|
||||
configurable: false,
|
||||
defaultState: isMobile,
|
||||
},
|
||||
} satisfies { [key in string]: FlagInfo };
|
||||
|
||||
|
||||
159
packages/common/infra/src/op/README.md
Normal file
159
packages/common/infra/src/op/README.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Introduction
|
||||
|
||||
Operation Pattern is a tiny `RPC` framework available both in frontend and backend.
|
||||
|
||||
It introduces super simple call and listen signatures to make Worker, cross tabs SharedWorker or BroadcastChannel easier to use and reduce boilerplate.
|
||||
|
||||
# usage
|
||||
|
||||
## Register Op Handlers
|
||||
|
||||
### Function call handler
|
||||
|
||||
```ts
|
||||
interface Ops extends OpSchema {
|
||||
add: [{ a: number; b: number }, number]
|
||||
}
|
||||
|
||||
// register
|
||||
const consumer: OpConsumer<Ops>;
|
||||
consumer.register('add', ({ a, b }) => a + b);
|
||||
|
||||
// call
|
||||
const client: OpClient<Ops>;
|
||||
const ret = client.call('add', { a: 1, b: 2 })); // Promise<3>
|
||||
```
|
||||
|
||||
### Stream call handler
|
||||
|
||||
```ts
|
||||
interface Ops extends OpSchema {
|
||||
subscribeStatus: [number, string];
|
||||
}
|
||||
|
||||
// register
|
||||
const consumer: OpConsumer<Ops>;
|
||||
consumer.register('subscribeStatus', (id: number) => {
|
||||
return interval(3000).pipe(map(() => 'connected'));
|
||||
});
|
||||
|
||||
// subscribe
|
||||
const client: OpClient<Ops>;
|
||||
client.ob$('subscribeStatus', 123).subscribe({
|
||||
next: status => {
|
||||
ui.setServerStatus(status);
|
||||
},
|
||||
error: error => {
|
||||
ui.setServerError(error);
|
||||
},
|
||||
complete: () => {
|
||||
//
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Transfer variables
|
||||
|
||||
> [Transferable Objects](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects)
|
||||
|
||||
#### Client transferables
|
||||
|
||||
```ts
|
||||
interface Ops extends OpSchema {
|
||||
heavyWork: [{ name: string; data: Uint8Array; data2: Uint8Array }, void];
|
||||
}
|
||||
|
||||
const client: OpClient<Ops>;
|
||||
const data = new Uint8Array([1, 2, 3]);
|
||||
const nonTransferredData = new Uint8Array([1, 2, 3]);
|
||||
client.call(
|
||||
'heavyWork',
|
||||
transfer(
|
||||
{
|
||||
name: '',
|
||||
data: data,
|
||||
data2: nonTransferredData,
|
||||
},
|
||||
[data.buffer]
|
||||
)
|
||||
);
|
||||
|
||||
// after transferring, you can not use the transferred variables anymore!!!
|
||||
// moved
|
||||
assertEq(data.byteLength, 0);
|
||||
// copied
|
||||
assertEq(nonTransferredData.byteLength, 3);
|
||||
```
|
||||
|
||||
#### Consumer transferables
|
||||
|
||||
```ts
|
||||
interface Ops extends OpSchema {
|
||||
job: [{ id: string }, Uint8Array];
|
||||
}
|
||||
|
||||
const consumer: OpConsumer<Ops>;
|
||||
consumer.register('ops', ({ id }) => {
|
||||
return interval(3000).pipe(
|
||||
map(() => {
|
||||
const data = new Uint8Array([1, 2, 3]);
|
||||
transfer(data, [data.buffer]);
|
||||
})
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## Communication
|
||||
|
||||
### BroadcastChannel
|
||||
|
||||
:::CAUTION
|
||||
|
||||
BroadcastChannel doesn't support transfer transferable objects. All data passed through it's `postMessage` api would be structured cloned
|
||||
|
||||
see [Structured_clone_algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)
|
||||
|
||||
:::
|
||||
|
||||
```ts
|
||||
const channel = new BroadcastChannel('domain');
|
||||
const consumer = new OpConsumer(channel);
|
||||
consumer.listen();
|
||||
|
||||
const client = new OpClient(channel);
|
||||
client.listen();
|
||||
```
|
||||
|
||||
### MessageChannel
|
||||
|
||||
```ts
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
|
||||
const client = new OpClient(port1);
|
||||
const consumer = new OpConsumer(port2);
|
||||
```
|
||||
|
||||
### Worker
|
||||
|
||||
```ts
|
||||
const worker = new Worker('./xxx-worker');
|
||||
const client = new OpClient(worker);
|
||||
|
||||
// in worker
|
||||
const consumer = new OpConsumer(globalThis);
|
||||
consumer.listen();
|
||||
```
|
||||
|
||||
### SharedWorker
|
||||
|
||||
```ts
|
||||
const worker = new SharedWorker('./xxx-worker');
|
||||
const client = new OpClient(worker.port);
|
||||
|
||||
// in worker
|
||||
globalThis.addEventListener('connect', event => {
|
||||
const port = event.ports[0];
|
||||
const consumer = new OpConsumer(port);
|
||||
consumer.listen();
|
||||
});
|
||||
```
|
||||
209
packages/common/infra/src/op/__tests__/client.spec.ts
Normal file
209
packages/common/infra/src/op/__tests__/client.spec.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { afterEach } from 'node:test';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { OpClient } from '../client';
|
||||
import { type MessageHandlers, transfer } from '../message';
|
||||
import type { OpSchema } from '../types';
|
||||
|
||||
interface TestOps extends OpSchema {
|
||||
add: [{ a: number; b: number }, number];
|
||||
bin: [Uint8Array, Uint8Array];
|
||||
sub: [Uint8Array, number];
|
||||
}
|
||||
|
||||
declare module 'vitest' {
|
||||
interface TestContext {
|
||||
producer: OpClient<TestOps>;
|
||||
handlers: MessageHandlers;
|
||||
postMessage: ReturnType<typeof vi.fn>;
|
||||
}
|
||||
}
|
||||
|
||||
describe('op client', () => {
|
||||
beforeEach(ctx => {
|
||||
const { port1 } = new MessageChannel();
|
||||
// @ts-expect-error patch postMessage
|
||||
port1.postMessage = vi.fn(port1.postMessage);
|
||||
// @ts-expect-error patch postMessage
|
||||
ctx.postMessage = port1.postMessage;
|
||||
ctx.producer = new OpClient(port1);
|
||||
// @ts-expect-error internal api
|
||||
ctx.handlers = ctx.producer.handlers;
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should send call op', async ctx => {
|
||||
// @ts-expect-error internal api
|
||||
const pendingCalls = ctx.producer.pendingCalls;
|
||||
const result = ctx.producer.call('add', { a: 1, b: 2 });
|
||||
|
||||
expect(ctx.postMessage.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": "add:1",
|
||||
"name": "add",
|
||||
"payload": {
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
},
|
||||
"type": "call",
|
||||
}
|
||||
`);
|
||||
expect(pendingCalls.has('add:1')).toBe(true);
|
||||
|
||||
// fake consumer return
|
||||
ctx.handlers.return({ type: 'return', id: 'add:1', data: 3 });
|
||||
|
||||
await expect(result).resolves.toBe(3);
|
||||
|
||||
expect(pendingCalls.has('add:1')).toBe(false);
|
||||
});
|
||||
|
||||
it('should transfer transferables with call op', async ctx => {
|
||||
const data = new Uint8Array([1, 2, 3]);
|
||||
const result = ctx.producer.call('bin', transfer(data, [data.buffer]));
|
||||
|
||||
expect(ctx.postMessage.mock.calls[0][1].transfer[0]).toBeInstanceOf(
|
||||
ArrayBuffer
|
||||
);
|
||||
|
||||
// fake consumer return
|
||||
ctx.handlers.return({
|
||||
type: 'return',
|
||||
id: 'bin:1',
|
||||
data: new Uint8Array([3, 2, 1]),
|
||||
});
|
||||
|
||||
await expect(result).resolves.toEqual(new Uint8Array([3, 2, 1]));
|
||||
expect(data.byteLength).toBe(0);
|
||||
});
|
||||
|
||||
it('should cancel call', async ctx => {
|
||||
const promise = ctx.producer.call('add', { a: 1, b: 2 });
|
||||
|
||||
promise.cancel();
|
||||
|
||||
expect(ctx.postMessage.mock.lastCall).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"id": "add:1",
|
||||
"type": "cancel",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
await expect(promise).rejects.toThrow('canceled');
|
||||
});
|
||||
|
||||
it('should timeout call', async ctx => {
|
||||
const promise = ctx.producer.call('add', { a: 1, b: 2 });
|
||||
|
||||
vi.advanceTimersByTime(4000);
|
||||
|
||||
await expect(promise).rejects.toThrow('timeout');
|
||||
});
|
||||
|
||||
it('should send subscribe op', async ctx => {
|
||||
let ob = {
|
||||
next: vi.fn(),
|
||||
error: vi.fn(),
|
||||
complete: vi.fn(),
|
||||
};
|
||||
|
||||
// @ts-expect-error internal api
|
||||
const subscriptions = ctx.producer.obs;
|
||||
ctx.producer.ob$('sub', new Uint8Array([1, 2, 3])).subscribe(ob);
|
||||
|
||||
expect(ctx.postMessage.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": "sub:1",
|
||||
"name": "sub",
|
||||
"payload": Uint8Array [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
],
|
||||
"type": "subscribe",
|
||||
}
|
||||
`);
|
||||
expect(subscriptions.has('sub:1')).toBe(true);
|
||||
|
||||
// fake consumer return
|
||||
ctx.handlers.next({ type: 'next', id: 'sub:1', data: 1 });
|
||||
ctx.handlers.next({ type: 'next', id: 'sub:1', data: 2 });
|
||||
ctx.handlers.next({ type: 'next', id: 'sub:1', data: 3 });
|
||||
|
||||
expect(subscriptions.has('sub:1')).toBe(true);
|
||||
|
||||
ctx.handlers.complete({ type: 'complete', id: 'sub:1' });
|
||||
|
||||
expect(ob.next).toHaveBeenCalledTimes(3);
|
||||
expect(ob.complete).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(subscriptions.has('sub:1')).toBe(false);
|
||||
expect(ctx.postMessage.mock.lastCall).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"id": "sub:1",
|
||||
"type": "unsubscribe",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
// smoking
|
||||
ob = {
|
||||
next: vi.fn(),
|
||||
error: vi.fn(),
|
||||
complete: vi.fn(),
|
||||
};
|
||||
ctx.producer.ob$('sub', new Uint8Array([1, 2, 3])).subscribe(ob);
|
||||
|
||||
expect(subscriptions.has('sub:2')).toBe(true);
|
||||
|
||||
ctx.handlers.next({ type: 'next', id: 'sub:2', data: 1 });
|
||||
ctx.handlers.error({
|
||||
type: 'error',
|
||||
id: 'sub:2',
|
||||
error: new Error('test'),
|
||||
});
|
||||
|
||||
expect(ob.next).toHaveBeenCalledTimes(1);
|
||||
expect(ob.error).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(subscriptions.has('sub')).toBe(false);
|
||||
});
|
||||
|
||||
it('should transfer transferables with subscribe op', async ctx => {
|
||||
const data = new Uint8Array([1, 2, 3]);
|
||||
const sub = ctx.producer
|
||||
.ob$('bin', transfer(data, [data.buffer]))
|
||||
.subscribe({
|
||||
next: vi.fn(),
|
||||
});
|
||||
|
||||
expect(data.byteLength).toBe(0);
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('should unsubscribe subscription op', ctx => {
|
||||
const sub = ctx.producer.ob$('sub', new Uint8Array([1, 2, 3])).subscribe({
|
||||
next: vi.fn(),
|
||||
});
|
||||
|
||||
sub.unsubscribe();
|
||||
|
||||
expect(ctx.postMessage.mock.lastCall).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"id": "sub:1",
|
||||
"type": "unsubscribe",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
197
packages/common/infra/src/op/__tests__/consumer.spec.ts
Normal file
197
packages/common/infra/src/op/__tests__/consumer.spec.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import { afterEach } from 'node:test';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { OpConsumer } from '../consumer';
|
||||
import { type MessageHandlers, transfer } from '../message';
|
||||
import type { OpSchema } from '../types';
|
||||
|
||||
interface TestOps extends OpSchema {
|
||||
add: [{ a: number; b: number }, number];
|
||||
any: [any, any];
|
||||
}
|
||||
|
||||
declare module 'vitest' {
|
||||
interface TestContext {
|
||||
consumer: OpConsumer<TestOps>;
|
||||
handlers: MessageHandlers;
|
||||
postMessage: ReturnType<typeof vi.fn>;
|
||||
}
|
||||
}
|
||||
|
||||
describe('op consumer', () => {
|
||||
beforeEach(ctx => {
|
||||
const { port2 } = new MessageChannel();
|
||||
// @ts-expect-error patch postMessage
|
||||
port2.postMessage = vi.fn(port2.postMessage);
|
||||
// @ts-expect-error patch postMessage
|
||||
ctx.postMessage = port2.postMessage;
|
||||
ctx.consumer = new OpConsumer(port2);
|
||||
// @ts-expect-error internal api
|
||||
ctx.handlers = ctx.consumer.handlers;
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should throw if no handler registered', async ctx => {
|
||||
ctx.handlers.call({ type: 'call', id: 'add:1', name: 'add', payload: {} });
|
||||
await vi.advanceTimersToNextTimerAsync();
|
||||
expect(ctx.postMessage.mock.lastCall).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"error": [Error: Handler for operation [add] is not registered.],
|
||||
"id": "add:1",
|
||||
"type": "return",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle call message', async ctx => {
|
||||
ctx.consumer.register('add', ({ a, b }) => a + b);
|
||||
|
||||
ctx.handlers.call({
|
||||
type: 'call',
|
||||
id: 'add:1',
|
||||
name: 'add',
|
||||
payload: { a: 1, b: 2 },
|
||||
});
|
||||
await vi.advanceTimersToNextTimerAsync();
|
||||
expect(ctx.postMessage.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
{
|
||||
"data": 3,
|
||||
"id": "add:1",
|
||||
"type": "return",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle cancel message', async ctx => {
|
||||
ctx.consumer.register('add', ({ a, b }, { signal }) => {
|
||||
const { reject, resolve, promise } = Promise.withResolvers<number>();
|
||||
|
||||
signal?.addEventListener('abort', () => {
|
||||
reject(new Error('canceled'));
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
resolve(a + b);
|
||||
}, Number.MAX_SAFE_INTEGER);
|
||||
|
||||
return promise;
|
||||
});
|
||||
|
||||
ctx.handlers.call({
|
||||
type: 'call',
|
||||
id: 'add:1',
|
||||
name: 'add',
|
||||
payload: { a: 1, b: 2 },
|
||||
});
|
||||
ctx.handlers.cancel({ type: 'cancel', id: 'add:1' });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
|
||||
expect(ctx.postMessage).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should transfer transferables in return', async ctx => {
|
||||
const data = new Uint8Array([1, 2, 3]);
|
||||
const nonTransferred = new Uint8Array([4, 5, 6]);
|
||||
|
||||
ctx.consumer.register('any', () => {
|
||||
return transfer({ data: { data, nonTransferred } }, [data.buffer]);
|
||||
});
|
||||
|
||||
ctx.handlers.call({ type: 'call', id: 'any:1', name: 'any', payload: {} });
|
||||
await vi.advanceTimersToNextTimerAsync();
|
||||
expect(ctx.postMessage).toHaveBeenCalledOnce();
|
||||
|
||||
expect(data.byteLength).toBe(0);
|
||||
expect(nonTransferred.byteLength).toBe(3);
|
||||
});
|
||||
|
||||
it('should handle subscribe message', async ctx => {
|
||||
ctx.consumer.register('any', data => {
|
||||
return new Observable(observer => {
|
||||
data.forEach((v: number) => observer.next(v));
|
||||
observer.complete();
|
||||
});
|
||||
});
|
||||
|
||||
ctx.handlers.subscribe({
|
||||
type: 'subscribe',
|
||||
id: 'any:1',
|
||||
name: 'any',
|
||||
payload: transfer(new Uint8Array([1, 2, 3]), [
|
||||
new Uint8Array([1, 2, 3]).buffer,
|
||||
]),
|
||||
});
|
||||
await vi.advanceTimersToNextTimerAsync();
|
||||
expect(ctx.postMessage.mock.calls.map(call => call[0]))
|
||||
.toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"data": 1,
|
||||
"id": "any:1",
|
||||
"type": "next",
|
||||
},
|
||||
{
|
||||
"data": 2,
|
||||
"id": "any:1",
|
||||
"type": "next",
|
||||
},
|
||||
{
|
||||
"data": 3,
|
||||
"id": "any:1",
|
||||
"type": "next",
|
||||
},
|
||||
{
|
||||
"id": "any:1",
|
||||
"type": "complete",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle unsubscribe message', async ctx => {
|
||||
ctx.consumer.register('any', data => {
|
||||
return new Observable(observer => {
|
||||
data.forEach((v: number) => {
|
||||
setTimeout(() => {
|
||||
observer.next(v);
|
||||
}, 1);
|
||||
});
|
||||
setTimeout(() => {
|
||||
observer.complete();
|
||||
}, 1);
|
||||
});
|
||||
});
|
||||
|
||||
ctx.handlers.subscribe({
|
||||
type: 'subscribe',
|
||||
id: 'any:1',
|
||||
name: 'any',
|
||||
payload: transfer(new Uint8Array([1, 2, 3]), [
|
||||
new Uint8Array([1, 2, 3]).buffer,
|
||||
]),
|
||||
});
|
||||
|
||||
ctx.handlers.unsubscribe({ type: 'unsubscribe', id: 'any:1' });
|
||||
|
||||
await vi.advanceTimersToNextTimerAsync();
|
||||
expect(ctx.postMessage.mock.calls).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
{
|
||||
"id": "any:1",
|
||||
"type": "complete",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
76
packages/common/infra/src/op/__tests__/message.spec.ts
Normal file
76
packages/common/infra/src/op/__tests__/message.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
AutoMessageHandler,
|
||||
ignoreUnknownEvent,
|
||||
KNOWN_MESSAGE_TYPES,
|
||||
type MessageCommunicapable,
|
||||
type MessageHandlers,
|
||||
} from '../message';
|
||||
|
||||
class CustomMessageHandler extends AutoMessageHandler {
|
||||
public handlers: Partial<MessageHandlers> = {
|
||||
call: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
subscribe: vi.fn(),
|
||||
unsubscribe: vi.fn(),
|
||||
return: vi.fn(),
|
||||
next: vi.fn(),
|
||||
error: vi.fn(),
|
||||
complete: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
declare module 'vitest' {
|
||||
interface TestContext {
|
||||
sendPort: MessageCommunicapable;
|
||||
receivePort: MessageCommunicapable;
|
||||
handler: CustomMessageHandler;
|
||||
}
|
||||
}
|
||||
|
||||
describe('message', () => {
|
||||
beforeEach(ctx => {
|
||||
const listeners: ((event: MessageEvent) => void)[] = [];
|
||||
ctx.sendPort = {
|
||||
postMessage: (msg: any) => {
|
||||
listeners.forEach(listener => {
|
||||
listener(new MessageEvent('message', { data: msg }));
|
||||
});
|
||||
},
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
};
|
||||
|
||||
ctx.receivePort = {
|
||||
postMessage: vi.fn(),
|
||||
addEventListener: vi.fn((_event, handler) => {
|
||||
listeners.push(handler);
|
||||
}),
|
||||
removeEventListener: vi.fn(),
|
||||
};
|
||||
ctx.handler = new CustomMessageHandler(ctx.receivePort);
|
||||
ctx.handler.listen();
|
||||
});
|
||||
|
||||
it('should ignore unknown message type', ctx => {
|
||||
const handler = vi.fn();
|
||||
// @ts-expect-error internal api
|
||||
ctx.handler.handleMessage = ignoreUnknownEvent(handler);
|
||||
|
||||
ctx.sendPort.postMessage('connected');
|
||||
ctx.sendPort.postMessage({ type: 'call1' });
|
||||
ctx.sendPort.postMessage(new Uint8Array());
|
||||
ctx.sendPort.postMessage(null);
|
||||
ctx.sendPort.postMessage(undefined);
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle known message type', async ctx => {
|
||||
for (const type of KNOWN_MESSAGE_TYPES) {
|
||||
ctx.sendPort.postMessage({ type });
|
||||
expect(ctx.handler.handlers[type]).toBeCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
201
packages/common/infra/src/op/client.ts
Normal file
201
packages/common/infra/src/op/client.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { merge } from 'lodash-es';
|
||||
import { Observable, type Observer } from 'rxjs';
|
||||
|
||||
import {
|
||||
AutoMessageHandler,
|
||||
type CallMessage,
|
||||
type CancelMessage,
|
||||
fetchTransferables,
|
||||
type MessageCommunicapable,
|
||||
type MessageHandlers,
|
||||
type SubscribeMessage,
|
||||
type UnsubscribeMessage,
|
||||
} from './message';
|
||||
import type { OpInput, OpNames, OpOutput, OpSchema } from './types';
|
||||
|
||||
export interface CancelablePromise<T> extends Promise<T> {
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
interface PendingCall extends PromiseWithResolvers<any> {
|
||||
id: string;
|
||||
timeout: number | NodeJS.Timeout;
|
||||
}
|
||||
|
||||
export interface OpClientOptions {
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export class OpClient<Ops extends OpSchema> extends AutoMessageHandler {
|
||||
private readonly callIds = new Map<OpNames<Ops>, number>();
|
||||
private readonly pendingCalls = new Map<string, PendingCall>();
|
||||
private readonly obs = new Map<string, Observer<any>>();
|
||||
private readonly options: OpClientOptions = {
|
||||
timeout: 3000,
|
||||
};
|
||||
|
||||
constructor(port: MessageCommunicapable, options: OpClientOptions = {}) {
|
||||
super(port);
|
||||
merge(this.options, options);
|
||||
}
|
||||
|
||||
protected override get handlers() {
|
||||
return {
|
||||
return: this.handleReturnMessage,
|
||||
next: this.handleSubscriptionNextMessage,
|
||||
error: this.handleSubscriptionErrorMessage,
|
||||
complete: this.handleSubscriptionCompleteMessage,
|
||||
};
|
||||
}
|
||||
|
||||
private readonly handleReturnMessage: MessageHandlers['return'] = msg => {
|
||||
const pending = this.pendingCalls.get(msg.id);
|
||||
if (!pending) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ('error' in msg) {
|
||||
pending.reject(msg.error);
|
||||
} else {
|
||||
pending.resolve(msg.data);
|
||||
}
|
||||
clearTimeout(pending.timeout);
|
||||
this.pendingCalls.delete(msg.id);
|
||||
};
|
||||
|
||||
private readonly handleSubscriptionNextMessage: MessageHandlers['next'] =
|
||||
msg => {
|
||||
const ob = this.obs.get(msg.id);
|
||||
if (!ob) {
|
||||
return;
|
||||
}
|
||||
|
||||
ob.next(msg.data);
|
||||
};
|
||||
|
||||
private readonly handleSubscriptionErrorMessage: MessageHandlers['error'] =
|
||||
msg => {
|
||||
const ob = this.obs.get(msg.id);
|
||||
if (!ob) {
|
||||
return;
|
||||
}
|
||||
|
||||
ob.error(msg.error);
|
||||
};
|
||||
|
||||
private readonly handleSubscriptionCompleteMessage: MessageHandlers['complete'] =
|
||||
msg => {
|
||||
const ob = this.obs.get(msg.id);
|
||||
if (!ob) {
|
||||
return;
|
||||
}
|
||||
|
||||
ob.complete();
|
||||
};
|
||||
|
||||
protected nextCallId(op: OpNames<Ops>) {
|
||||
let id = this.callIds.get(op) ?? 0;
|
||||
id++;
|
||||
this.callIds.set(op, id);
|
||||
|
||||
return `${op}:${id}`;
|
||||
}
|
||||
|
||||
protected currentCallId(op: OpNames<Ops>) {
|
||||
return this.callIds.get(op) ?? 0;
|
||||
}
|
||||
|
||||
call<Op extends OpNames<Ops>>(
|
||||
op: Op,
|
||||
...args: OpInput<Ops, Op>
|
||||
): CancelablePromise<OpOutput<Ops, Op>> {
|
||||
const promiseWithResolvers = Promise.withResolvers<any>();
|
||||
const payload = args[0];
|
||||
|
||||
const msg = {
|
||||
type: 'call',
|
||||
id: this.nextCallId(op),
|
||||
name: op as string,
|
||||
payload,
|
||||
} satisfies CallMessage;
|
||||
|
||||
const promise = promiseWithResolvers.promise as CancelablePromise<any>;
|
||||
|
||||
const raise = (reason: string) => {
|
||||
const pending = this.pendingCalls.get(msg.id);
|
||||
if (!pending) {
|
||||
return;
|
||||
}
|
||||
this.port.postMessage({
|
||||
type: 'cancel',
|
||||
id: msg.id,
|
||||
} satisfies CancelMessage);
|
||||
promiseWithResolvers.reject(new Error(reason));
|
||||
clearTimeout(pending.timeout);
|
||||
this.pendingCalls.delete(msg.id);
|
||||
};
|
||||
|
||||
promise.cancel = () => {
|
||||
raise('canceled');
|
||||
};
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
raise('timeout');
|
||||
}, this.options.timeout);
|
||||
|
||||
const transferables = fetchTransferables(payload);
|
||||
|
||||
this.port.postMessage(msg, { transfer: transferables });
|
||||
this.pendingCalls.set(msg.id, {
|
||||
...promiseWithResolvers,
|
||||
timeout,
|
||||
id: msg.id,
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
ob$<Op extends OpNames<Ops>, Out extends OpOutput<Ops, Op>>(
|
||||
op: Op,
|
||||
...args: OpInput<Ops, Op>
|
||||
): Observable<Out> {
|
||||
const payload = args[0];
|
||||
|
||||
const msg = {
|
||||
type: 'subscribe',
|
||||
id: this.nextCallId(op),
|
||||
name: op as string,
|
||||
payload,
|
||||
} satisfies SubscribeMessage;
|
||||
|
||||
const sub$ = new Observable<Out>(ob => {
|
||||
this.obs.set(msg.id, ob);
|
||||
|
||||
return () => {
|
||||
ob.complete();
|
||||
this.obs.delete(msg.id);
|
||||
this.port.postMessage({
|
||||
type: 'unsubscribe',
|
||||
id: msg.id,
|
||||
} satisfies UnsubscribeMessage);
|
||||
};
|
||||
});
|
||||
|
||||
const transferables = fetchTransferables(payload);
|
||||
this.port.postMessage(msg, { transfer: transferables });
|
||||
|
||||
return sub$;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.close();
|
||||
this.pendingCalls.forEach(call => {
|
||||
call.reject(new Error('client destroyed'));
|
||||
});
|
||||
this.pendingCalls.clear();
|
||||
this.obs.forEach(ob => {
|
||||
ob.complete();
|
||||
});
|
||||
this.obs.clear();
|
||||
}
|
||||
}
|
||||
179
packages/common/infra/src/op/consumer.ts
Normal file
179
packages/common/infra/src/op/consumer.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import EventEmitter2 from 'eventemitter2';
|
||||
import { defer, from, fromEvent, Observable, of, take, takeUntil } from 'rxjs';
|
||||
|
||||
import {
|
||||
AutoMessageHandler,
|
||||
type CallMessage,
|
||||
fetchTransferables,
|
||||
type MessageHandlers,
|
||||
type ReturnMessage,
|
||||
type SubscribeMessage,
|
||||
type SubscriptionCompleteMessage,
|
||||
type SubscriptionErrorMessage,
|
||||
type SubscriptionNextMessage,
|
||||
} from './message';
|
||||
import type { OpInput, OpNames, OpOutput, OpSchema } from './types';
|
||||
|
||||
interface OpCallContext {
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export type OpHandler<Ops extends OpSchema, Op extends OpNames<Ops>> = (
|
||||
payload: OpInput<Ops, Op>[0],
|
||||
ctx: OpCallContext
|
||||
) =>
|
||||
| OpOutput<Ops, Op>
|
||||
| Promise<OpOutput<Ops, Op>>
|
||||
| Observable<OpOutput<Ops, Op>>;
|
||||
|
||||
export class OpConsumer<Ops extends OpSchema> extends AutoMessageHandler {
|
||||
private readonly eventBus = new EventEmitter2();
|
||||
|
||||
private readonly registeredOpHandlers = new Map<
|
||||
OpNames<Ops>,
|
||||
OpHandler<Ops, any>
|
||||
>();
|
||||
|
||||
private readonly processing = new Map<string, AbortController>();
|
||||
|
||||
override get handlers() {
|
||||
return {
|
||||
call: this.handleCallMessage,
|
||||
cancel: this.handleCancelMessage,
|
||||
subscribe: this.handleSubscribeMessage,
|
||||
unsubscribe: this.handleCancelMessage,
|
||||
};
|
||||
}
|
||||
|
||||
private readonly handleCallMessage: MessageHandlers['call'] = async msg => {
|
||||
const abortController = new AbortController();
|
||||
this.processing.set(msg.id, abortController);
|
||||
|
||||
this.eventBus.emit(`before:${msg.name}`, msg.payload);
|
||||
this.ob$(msg, abortController.signal)
|
||||
.pipe(take(1))
|
||||
.subscribe({
|
||||
next: data => {
|
||||
this.eventBus.emit(`after:${msg.name}`, msg.payload, data);
|
||||
const transferables = fetchTransferables(data);
|
||||
this.port.postMessage(
|
||||
{
|
||||
type: 'return',
|
||||
id: msg.id,
|
||||
data,
|
||||
} satisfies ReturnMessage,
|
||||
{ transfer: transferables }
|
||||
);
|
||||
},
|
||||
error: error => {
|
||||
this.port.postMessage({
|
||||
type: 'return',
|
||||
id: msg.id,
|
||||
error: error as Error,
|
||||
} satisfies ReturnMessage);
|
||||
},
|
||||
complete: () => {
|
||||
this.processing.delete(msg.id);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private readonly handleSubscribeMessage: MessageHandlers['subscribe'] =
|
||||
msg => {
|
||||
const abortController = new AbortController();
|
||||
this.processing.set(msg.id, abortController);
|
||||
|
||||
this.ob$(msg, abortController.signal).subscribe({
|
||||
next: data => {
|
||||
const transferables = fetchTransferables(data);
|
||||
this.port.postMessage(
|
||||
{
|
||||
type: 'next',
|
||||
id: msg.id,
|
||||
data,
|
||||
} satisfies SubscriptionNextMessage,
|
||||
{ transfer: transferables }
|
||||
);
|
||||
},
|
||||
error: error => {
|
||||
this.port.postMessage({
|
||||
type: 'error',
|
||||
id: msg.id,
|
||||
error: error as Error,
|
||||
} satisfies SubscriptionErrorMessage);
|
||||
},
|
||||
complete: () => {
|
||||
this.port.postMessage({
|
||||
type: 'complete',
|
||||
id: msg.id,
|
||||
} satisfies SubscriptionCompleteMessage);
|
||||
this.processing.delete(msg.id);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private readonly handleCancelMessage: MessageHandlers['cancel'] &
|
||||
MessageHandlers['unsubscribe'] = msg => {
|
||||
const abortController = this.processing.get(msg.id);
|
||||
if (!abortController) {
|
||||
return;
|
||||
}
|
||||
|
||||
abortController.abort();
|
||||
};
|
||||
|
||||
register<Op extends OpNames<Ops>>(op: Op, handler: OpHandler<Ops, Op>) {
|
||||
this.registeredOpHandlers.set(op, handler);
|
||||
}
|
||||
|
||||
before<Op extends OpNames<Ops>>(
|
||||
op: Op,
|
||||
handler: (...input: OpInput<Ops, Op>) => void
|
||||
) {
|
||||
this.eventBus.on(`before:${op}`, handler);
|
||||
}
|
||||
|
||||
after<Op extends OpNames<Ops>>(
|
||||
op: Op,
|
||||
handler: (...args: [...OpInput<Ops, Op>, OpOutput<Ops, Op>]) => void
|
||||
) {
|
||||
this.eventBus.on(`after:${op}`, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
ob$(op: CallMessage | SubscribeMessage, signal: AbortSignal) {
|
||||
return defer(() => {
|
||||
const handler = this.registeredOpHandlers.get(op.name as any);
|
||||
if (!handler) {
|
||||
throw new Error(
|
||||
`Handler for operation [${op.name}] is not registered.`
|
||||
);
|
||||
}
|
||||
|
||||
const ret$ = handler(op.payload, { signal });
|
||||
|
||||
let ob$: Observable<any>;
|
||||
if (ret$ instanceof Promise) {
|
||||
ob$ = from(ret$);
|
||||
} else if (ret$ instanceof Observable) {
|
||||
ob$ = ret$;
|
||||
} else {
|
||||
ob$ = of(ret$);
|
||||
}
|
||||
|
||||
return ob$.pipe(takeUntil(fromEvent(signal, 'abort')));
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.close();
|
||||
this.registeredOpHandlers.clear();
|
||||
this.processing.forEach(controller => {
|
||||
controller.abort();
|
||||
});
|
||||
this.processing.clear();
|
||||
this.eventBus.removeAllListeners();
|
||||
}
|
||||
}
|
||||
4
packages/common/infra/src/op/index.ts
Normal file
4
packages/common/infra/src/op/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './client';
|
||||
export * from './consumer';
|
||||
export { type MessageCommunicapable, transfer } from './message';
|
||||
export type { OpSchema } from './types';
|
||||
166
packages/common/infra/src/op/message.ts
Normal file
166
packages/common/infra/src/op/message.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
const PRODUCER_MESSAGE_TYPES = [
|
||||
'call',
|
||||
'cancel',
|
||||
'subscribe',
|
||||
'unsubscribe',
|
||||
] as const;
|
||||
const CONSUMER_MESSAGE_TYPES = ['return', 'next', 'error', 'complete'] as const;
|
||||
export const KNOWN_MESSAGE_TYPES = new Set([
|
||||
...PRODUCER_MESSAGE_TYPES,
|
||||
...CONSUMER_MESSAGE_TYPES,
|
||||
]);
|
||||
|
||||
type MessageType =
|
||||
| (typeof PRODUCER_MESSAGE_TYPES)[number]
|
||||
| (typeof CONSUMER_MESSAGE_TYPES)[number];
|
||||
|
||||
export interface Message {
|
||||
type: MessageType;
|
||||
}
|
||||
|
||||
// in
|
||||
export interface CallMessage extends Message {
|
||||
type: 'call';
|
||||
id: string;
|
||||
name: string;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
export interface CancelMessage extends Message {
|
||||
type: 'cancel';
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface SubscribeMessage extends Message {
|
||||
type: 'subscribe';
|
||||
id: string;
|
||||
name: string;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
export interface UnsubscribeMessage extends Message {
|
||||
type: 'unsubscribe';
|
||||
id: string;
|
||||
}
|
||||
|
||||
// out
|
||||
export type ReturnMessage = {
|
||||
type: 'return';
|
||||
id: string;
|
||||
} & (
|
||||
| {
|
||||
data: any;
|
||||
}
|
||||
| {
|
||||
error: Error;
|
||||
}
|
||||
);
|
||||
|
||||
export interface SubscriptionNextMessage extends Message {
|
||||
type: 'next';
|
||||
id: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
export interface SubscriptionErrorMessage extends Message {
|
||||
type: 'error';
|
||||
id: string;
|
||||
error: Error;
|
||||
}
|
||||
|
||||
export type SubscriptionCompleteMessage = {
|
||||
type: 'complete';
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type Messages =
|
||||
| CallMessage
|
||||
| CancelMessage
|
||||
| SubscribeMessage
|
||||
| UnsubscribeMessage
|
||||
| ReturnMessage
|
||||
| SubscriptionNextMessage
|
||||
| SubscriptionErrorMessage
|
||||
| SubscriptionCompleteMessage;
|
||||
|
||||
export type MessageHandlers = {
|
||||
[Type in Messages['type']]: (
|
||||
message: Extract<Messages, { type: Type }>
|
||||
) => void;
|
||||
};
|
||||
|
||||
export type MessageCommunicapable = Pick<
|
||||
MessagePort,
|
||||
'postMessage' | 'addEventListener' | 'removeEventListener'
|
||||
> & {
|
||||
start?(): void;
|
||||
close?(): void;
|
||||
terminate?(): void; // For Worker
|
||||
};
|
||||
|
||||
export function ignoreUnknownEvent(handler: (data: Messages) => void) {
|
||||
return (event: MessageEvent<Message>) => {
|
||||
const data = event.data;
|
||||
|
||||
if (
|
||||
!data ||
|
||||
typeof data !== 'object' ||
|
||||
typeof data.type !== 'string' ||
|
||||
!KNOWN_MESSAGE_TYPES.has(data.type)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
handler(data as any);
|
||||
};
|
||||
}
|
||||
|
||||
const TRANSFERABLES_CACHE = new Map<any, Transferable[]>();
|
||||
export function transfer<T>(data: T, transferables: Transferable[]): T {
|
||||
TRANSFERABLES_CACHE.set(data, transferables);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function fetchTransferables(data: any): Transferable[] | undefined {
|
||||
const transferables = TRANSFERABLES_CACHE.get(data);
|
||||
if (transferables) {
|
||||
TRANSFERABLES_CACHE.delete(data);
|
||||
}
|
||||
|
||||
return transferables;
|
||||
}
|
||||
|
||||
export abstract class AutoMessageHandler {
|
||||
private listening = false;
|
||||
protected abstract handlers: Partial<MessageHandlers>;
|
||||
|
||||
constructor(protected readonly port: MessageCommunicapable) {}
|
||||
|
||||
protected handleMessage = ignoreUnknownEvent((msg: Messages) => {
|
||||
const handler = this.handlers[msg.type];
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
|
||||
handler(msg as any);
|
||||
});
|
||||
|
||||
listen() {
|
||||
if (this.listening) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.port.addEventListener('message', this.handleMessage);
|
||||
this.port.addEventListener('messageerror', console.error);
|
||||
this.port.start?.();
|
||||
this.listening = true;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.port.close?.();
|
||||
this.port.terminate?.(); // For Worker
|
||||
this.port.removeEventListener('message', this.handleMessage);
|
||||
this.port.removeEventListener('messageerror', console.error);
|
||||
this.listening = false;
|
||||
}
|
||||
}
|
||||
36
packages/common/infra/src/op/types.ts
Normal file
36
packages/common/infra/src/op/types.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
type KeyToKey<T extends OpSchema> = {
|
||||
[K in keyof T]: string extends K ? never : K;
|
||||
};
|
||||
|
||||
declare type ValuesOf<T> = T extends {
|
||||
[K in keyof T]: infer _U;
|
||||
}
|
||||
? _U
|
||||
: never;
|
||||
|
||||
export interface OpSchema {
|
||||
[key: string]: [any, any?];
|
||||
}
|
||||
|
||||
type RequiredInput<In> = In extends void ? [] : In extends never ? [] : [In];
|
||||
|
||||
export type OpNames<T extends OpSchema> = ValuesOf<KeyToKey<T>>;
|
||||
export type OpInput<
|
||||
Ops extends OpSchema,
|
||||
Type extends OpNames<Ops>,
|
||||
> = Type extends keyof Ops
|
||||
? Ops[Type] extends [infer In]
|
||||
? RequiredInput<In>
|
||||
: Ops[Type] extends [infer In, infer _Out]
|
||||
? RequiredInput<In>
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type OpOutput<
|
||||
Ops extends OpSchema,
|
||||
Type extends OpNames<Ops>,
|
||||
> = Type extends keyof Ops
|
||||
? Ops[Type] extends [infer _In, infer Out]
|
||||
? Out
|
||||
: never
|
||||
: never;
|
||||
@@ -1,6 +1,8 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { from, merge, of, Subject, throttleTime } from 'rxjs';
|
||||
import { merge, of, Subject, throttleTime } from 'rxjs';
|
||||
|
||||
import { backoffRetry, fromPromise } from '../../../../livedata';
|
||||
import { exhaustMapWithTrailing } from '../../../../utils/';
|
||||
import {
|
||||
type AggregateOptions,
|
||||
@@ -16,6 +18,8 @@ import {
|
||||
} from '../../';
|
||||
import { DataStruct, type DataStructRWTransaction } from './data-struct';
|
||||
|
||||
const logger = new DebugLogger('IndexedDBIndex');
|
||||
|
||||
export class IndexedDBIndex<S extends Schema> implements Index<S> {
|
||||
data: DataStruct = new DataStruct(this.databaseName, this.schema);
|
||||
broadcast$ = new Subject();
|
||||
@@ -63,12 +67,15 @@ export class IndexedDBIndex<S extends Schema> implements Index<S> {
|
||||
return merge(of(1), this.broadcast$).pipe(
|
||||
throttleTime(3000, undefined, { leading: true, trailing: true }),
|
||||
exhaustMapWithTrailing(() => {
|
||||
return from(
|
||||
(async () => {
|
||||
return fromPromise(async () => {
|
||||
try {
|
||||
const trx = await this.data.readonly();
|
||||
return this.data.search(trx, query, options);
|
||||
})()
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('search error', error);
|
||||
throw error;
|
||||
}
|
||||
}).pipe(backoffRetry());
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -90,12 +97,15 @@ export class IndexedDBIndex<S extends Schema> implements Index<S> {
|
||||
return merge(of(1), this.broadcast$).pipe(
|
||||
throttleTime(3000, undefined, { leading: true, trailing: true }),
|
||||
exhaustMapWithTrailing(() => {
|
||||
return from(
|
||||
(async () => {
|
||||
return fromPromise(async () => {
|
||||
try {
|
||||
const trx = await this.data.readonly();
|
||||
return this.data.aggregate(trx, query, field, options);
|
||||
})()
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('aggregate error', error);
|
||||
throw error;
|
||||
}
|
||||
}).pipe(backoffRetry());
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,150 +1,5 @@
|
||||
import { generateKeyBetween } from 'fractional-indexing';
|
||||
|
||||
export interface SortableProvider<T, K extends string | number> {
|
||||
getItems(): T[];
|
||||
getItemId(item: T): K;
|
||||
getItemOrder(item: T): string;
|
||||
setItemOrder(item: T, order: string): void;
|
||||
}
|
||||
|
||||
// Using fractional-indexing managing orders of items in a list
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export function createFractionalIndexingSortableHelper<
|
||||
T,
|
||||
K extends string | number,
|
||||
>(provider: SortableProvider<T, K>) {
|
||||
function getOrderedItems() {
|
||||
return provider.getItems().sort((a, b) => {
|
||||
const oa = provider.getItemOrder(a);
|
||||
const ob = provider.getItemOrder(b);
|
||||
return oa > ob ? 1 : oa < ob ? -1 : 0;
|
||||
});
|
||||
}
|
||||
|
||||
function getLargestOrder() {
|
||||
const lastItem = getOrderedItems().at(-1);
|
||||
return lastItem ? provider.getItemOrder(lastItem) : null;
|
||||
}
|
||||
|
||||
function getSmallestOrder() {
|
||||
const firstItem = getOrderedItems().at(0);
|
||||
return firstItem ? provider.getItemOrder(firstItem) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new order at the end of the list
|
||||
*/
|
||||
function getNewItemOrder() {
|
||||
return generateKeyBetween(getLargestOrder(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move item from one position to another
|
||||
*
|
||||
* in the most common sorting case, moving over will visually place the dragging item to the target position
|
||||
* the original item in the target position will either move up or down, depending on the direction of the drag
|
||||
*
|
||||
* @param fromId
|
||||
* @param toId
|
||||
*/
|
||||
function move(fromId: K, toId: K) {
|
||||
const items = getOrderedItems();
|
||||
const from = items.findIndex(i => provider.getItemId(i) === fromId);
|
||||
const to = items.findIndex(i => provider.getItemId(i) === toId);
|
||||
const fromItem = items[from];
|
||||
const toItem = items[to];
|
||||
const toNextItem = items[from < to ? to + 1 : to - 1];
|
||||
const toOrder = toItem ? provider.getItemOrder(toItem) : null;
|
||||
const toNextOrder = toNextItem ? provider.getItemOrder(toNextItem) : null;
|
||||
const args: [string | null, string | null] =
|
||||
from < to ? [toOrder, toNextOrder] : [toNextOrder, toOrder];
|
||||
provider.setItemOrder(fromItem, generateKeyBetween(...args));
|
||||
}
|
||||
|
||||
function moveTo(fromId: K, toId: K, position: 'before' | 'after') {
|
||||
const items = getOrderedItems();
|
||||
const from = items.findIndex(i => provider.getItemId(i) === fromId);
|
||||
const to = items.findIndex(i => provider.getItemId(i) === toId);
|
||||
const fromItem = items[from] as T | undefined;
|
||||
if (fromItem === undefined) return;
|
||||
const toItem = items[to] as T | undefined;
|
||||
const toItemPrev = items[to - 1] as T | undefined;
|
||||
const toItemNext = items[to + 1] as T | undefined;
|
||||
const toItemOrder = toItem ? provider.getItemOrder(toItem) : null;
|
||||
const toItemPrevOrder = toItemPrev
|
||||
? provider.getItemOrder(toItemPrev)
|
||||
: null;
|
||||
const toItemNextOrder = toItemNext
|
||||
? provider.getItemOrder(toItemNext)
|
||||
: null;
|
||||
if (position === 'before') {
|
||||
provider.setItemOrder(
|
||||
fromItem,
|
||||
generateKeyBetween(toItemPrevOrder, toItemOrder)
|
||||
);
|
||||
} else {
|
||||
provider.setItemOrder(
|
||||
fromItem,
|
||||
generateKeyBetween(toItemOrder, toItemNextOrder)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cases example:
|
||||
* Imagine we have the following items, | a | b | c |
|
||||
* 1. insertBefore('b', undefined). before is not provided, which means insert b after c
|
||||
* | a | c |
|
||||
* ▴
|
||||
* b
|
||||
* result: | a | c | b |
|
||||
*
|
||||
* 2. insertBefore('b', 'a'). insert b before a
|
||||
* | a | c |
|
||||
* ▴
|
||||
* b
|
||||
*
|
||||
* result: | b | a | c |
|
||||
*/
|
||||
function insertBefore(
|
||||
id: string | number,
|
||||
beforeId: string | number | undefined
|
||||
) {
|
||||
const items = getOrderedItems();
|
||||
// assert id is in the list
|
||||
const item = items.find(i => provider.getItemId(i) === id);
|
||||
if (!item) return;
|
||||
|
||||
const beforeItemIndex = items.findIndex(
|
||||
i => provider.getItemId(i) === beforeId
|
||||
);
|
||||
const beforeItem = beforeItemIndex !== -1 ? items[beforeItemIndex] : null;
|
||||
const beforeItemPrev = beforeItem ? items[beforeItemIndex - 1] : null;
|
||||
|
||||
const beforeOrder = beforeItem ? provider.getItemOrder(beforeItem) : null;
|
||||
const beforePrevOrder = beforeItemPrev
|
||||
? provider.getItemOrder(beforeItemPrev)
|
||||
: null;
|
||||
|
||||
provider.setItemOrder(
|
||||
item,
|
||||
generateKeyBetween(beforePrevOrder, beforeOrder)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
getOrderedItems,
|
||||
getLargestOrder,
|
||||
getSmallestOrder,
|
||||
getNewItemOrder,
|
||||
move,
|
||||
moveTo,
|
||||
insertBefore,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* generate a key between a and b, the result key is always satisfied with a < result < b.
|
||||
* the key always has a random suffix, so there is no need to worry about collision.
|
||||
|
||||
12
packages/common/native/Cargo.toml
Normal file
12
packages/common/native/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
edition = "2021"
|
||||
name = "affine_common"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
sha3 = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rayon = { workspace = true }
|
||||
204
packages/common/native/src/hashcash.rs
Normal file
204
packages/common/native/src/hashcash.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
|
||||
use rand::{
|
||||
distributions::{Alphanumeric, Distribution},
|
||||
thread_rng,
|
||||
};
|
||||
use sha3::{Digest, Sha3_256};
|
||||
|
||||
const SALT_LENGTH: usize = 16;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Stamp {
|
||||
version: String,
|
||||
claim: u32,
|
||||
ts: String,
|
||||
resource: String,
|
||||
ext: String,
|
||||
rand: String,
|
||||
counter: String,
|
||||
}
|
||||
|
||||
impl Stamp {
|
||||
fn check_expiration(&self) -> bool {
|
||||
NaiveDateTime::parse_from_str(&self.ts, "%Y%m%d%H%M%S")
|
||||
.ok()
|
||||
.map(|ts| DateTime::<Utc>::from_naive_utc_and_offset(ts, Utc))
|
||||
.and_then(|utc| {
|
||||
utc
|
||||
.checked_add_signed(Duration::minutes(5))
|
||||
.map(|utc| Utc::now() <= utc)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn check<S: AsRef<str>>(&self, bits: u32, resource: S) -> bool {
|
||||
if self.version == "1"
|
||||
&& bits <= self.claim
|
||||
&& self.check_expiration()
|
||||
&& self.resource == resource.as_ref()
|
||||
{
|
||||
let hex_digits = ((self.claim as f32) / 4.).floor() as usize;
|
||||
|
||||
// check challenge
|
||||
let mut hasher = Sha3_256::new();
|
||||
hasher.update(self.format().as_bytes());
|
||||
let result = format!("{:x}", hasher.finalize());
|
||||
result[..hex_digits] == String::from_utf8(vec![b'0'; hex_digits]).unwrap()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format(&self) -> String {
|
||||
format!(
|
||||
"{}:{}:{}:{}:{}:{}:{}",
|
||||
self.version, self.claim, self.ts, self.resource, self.ext, self.rand, self.counter
|
||||
)
|
||||
}
|
||||
|
||||
/// Mint a new hashcash stamp.
|
||||
pub fn mint(resource: String, bits: Option<u32>) -> Self {
|
||||
let version = "1";
|
||||
let now = Utc::now();
|
||||
let ts = now.format("%Y%m%d%H%M%S");
|
||||
let bits = bits.unwrap_or(20);
|
||||
let rand = String::from_iter(
|
||||
Alphanumeric
|
||||
.sample_iter(thread_rng())
|
||||
.take(SALT_LENGTH)
|
||||
.map(char::from),
|
||||
);
|
||||
let challenge = format!("{}:{}:{}:{}:{}:{}", version, bits, ts, &resource, "", rand);
|
||||
|
||||
Stamp {
|
||||
version: version.to_string(),
|
||||
claim: bits,
|
||||
ts: ts.to_string(),
|
||||
resource,
|
||||
ext: "".to_string(),
|
||||
rand,
|
||||
counter: {
|
||||
let mut hasher = Sha3_256::new();
|
||||
let mut counter = 0;
|
||||
let hex_digits = ((bits as f32) / 4.).ceil() as usize;
|
||||
let zeros = String::from_utf8(vec![b'0'; hex_digits]).unwrap();
|
||||
loop {
|
||||
hasher.update(format!("{}:{:x}", challenge, counter).as_bytes());
|
||||
let result = format!("{:x}", hasher.finalize_reset());
|
||||
if result[..hex_digits] == zeros {
|
||||
break format!("{:x}", counter);
|
||||
};
|
||||
counter += 1
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Stamp {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let stamp_vec = value.split(':').collect::<Vec<&str>>();
|
||||
if stamp_vec.len() != 7
|
||||
|| stamp_vec
|
||||
.iter()
|
||||
.enumerate()
|
||||
.any(|(i, s)| i != 4 && s.is_empty())
|
||||
{
|
||||
return Err(format!(
|
||||
"Malformed stamp, expected 6 parts, got {}",
|
||||
stamp_vec.len()
|
||||
));
|
||||
}
|
||||
Ok(Stamp {
|
||||
version: stamp_vec[0].to_string(),
|
||||
claim: stamp_vec[1]
|
||||
.parse()
|
||||
.map_err(|_| "Malformed stamp".to_string())?,
|
||||
ts: stamp_vec[2].to_string(),
|
||||
resource: stamp_vec[3].to_string(),
|
||||
ext: stamp_vec[4].to_string(),
|
||||
rand: stamp_vec[5].to_string(),
|
||||
counter: stamp_vec[6].to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Stamp;
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use rayon::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn test_mint() {
|
||||
{
|
||||
let response = Stamp::mint("test".into(), Some(20)).format();
|
||||
assert!(
|
||||
Stamp::try_from(response.as_str())
|
||||
.unwrap()
|
||||
.check(20, "test"),
|
||||
"should pass"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let response = Stamp::mint("test".into(), Some(19)).format();
|
||||
assert!(
|
||||
!Stamp::try_from(response.as_str())
|
||||
.unwrap()
|
||||
.check(20, "test"),
|
||||
"should fail with lower bits"
|
||||
);
|
||||
}
|
||||
{
|
||||
let response = Stamp::mint("test".into(), Some(20)).format();
|
||||
assert!(
|
||||
!Stamp::try_from(response.as_str())
|
||||
.unwrap()
|
||||
.check(20, "test2"),
|
||||
"should fail with different resource"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_expiration() {
|
||||
let response = Stamp::mint("test".into(), Some(20));
|
||||
assert!(response.check_expiration());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format() {
|
||||
let response = Stamp::mint("test".into(), Some(20));
|
||||
assert_eq!(
|
||||
response.format(),
|
||||
format!(
|
||||
"1:20:{}:test::{}:{}",
|
||||
response.ts, response.rand, response.counter
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz() {
|
||||
(0..1000).into_par_iter().for_each(|_| {
|
||||
let bit = rand::random::<u32>() % 20 + 1;
|
||||
let resource = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(7)
|
||||
.map(char::from)
|
||||
.collect::<String>();
|
||||
let response = Stamp::mint(resource.clone(), Some(bit)).format();
|
||||
assert!(
|
||||
Stamp::try_from(response.as_str())
|
||||
.unwrap()
|
||||
.check(bit, resource),
|
||||
"should pass"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
1
packages/common/native/src/lib.rs
Normal file
1
packages/common/native/src/lib.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod hashcash;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/admin",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@affine/core": "workspace:*",
|
||||
@@ -37,8 +37,8 @@
|
||||
"cmdk": "^1.0.0",
|
||||
"embla-carousel-react": "^8.1.5",
|
||||
"input-otp": "^1.2.4",
|
||||
"lucide-react": "^0.445.0",
|
||||
"next-themes": "^0.3.0",
|
||||
"lucide-react": "^0.456.0",
|
||||
"next-themes": "^0.4.0",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^9.0.0",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
@@ -7,8 +7,8 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.7.1'
|
||||
classpath 'com.google.gms:google-services:4.4.0'
|
||||
classpath 'com.android.tools.build:gradle:8.7.2'
|
||||
classpath 'com.google.gms:google-services:4.4.2'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
22
packages/frontend/apps/android/App/gradlew
vendored
22
packages/frontend/apps/android/App/gradlew
vendored
@@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -83,7 +85,9 @@ done
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -201,11 +205,11 @@ fi
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
|
||||
22
packages/frontend/apps/android/App/gradlew.bat
vendored
22
packages/frontend/apps/android/App/gradlew.bat
vendored
@@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ ext {
|
||||
compileSdkVersion = 34
|
||||
targetSdkVersion = 34
|
||||
androidxActivityVersion = '1.8.0'
|
||||
androidxAppCompatVersion = '1.6.1'
|
||||
androidxAppCompatVersion = '1.7.0'
|
||||
androidxCoordinatorLayoutVersion = '1.2.0'
|
||||
androidxCoreVersion = '1.12.0'
|
||||
androidxFragmentVersion = '1.6.2'
|
||||
coreSplashScreenVersion = '1.0.1'
|
||||
androidxWebkitVersion = '1.9.0'
|
||||
junitVersion = '4.13.2'
|
||||
androidxJunitVersion = '1.1.5'
|
||||
androidxEspressoCoreVersion = '3.5.1'
|
||||
androidxJunitVersion = '1.2.1'
|
||||
androidxEspressoCoreVersion = '3.6.1'
|
||||
cordovaAndroidVersion = '10.1.1'
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/android",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"description": "AFFiNE Desktop Web application",
|
||||
"private": true,
|
||||
"browser": "src/index.tsx",
|
||||
@@ -13,8 +13,8 @@
|
||||
"@affine/component": "workspace:*",
|
||||
"@affine/core": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@blocksuite/affine": "0.17.25",
|
||||
"@blocksuite/icons": "^2.1.67",
|
||||
"@blocksuite/affine": "0.17.33",
|
||||
"@blocksuite/icons": "^2.1.70",
|
||||
"@capacitor/android": "^6.1.2",
|
||||
"@capacitor/core": "^6.1.2",
|
||||
"@sentry/react": "^8.0.0",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||
import { AffineContext } from '@affine/core/components/context';
|
||||
import { Telemetry } from '@affine/core/components/telemetry';
|
||||
import { AppContainer } from '@affine/core/desktop/components/app-container';
|
||||
import { configureMobileModules } from '@affine/core/mobile/modules';
|
||||
import { router } from '@affine/core/mobile/router';
|
||||
import { configureCommonModules } from '@affine/core/modules';
|
||||
@@ -47,9 +46,8 @@ export function App() {
|
||||
<FrameworkRoot framework={frameworkProvider}>
|
||||
<I18nProvider>
|
||||
<AffineContext store={getCurrentStore()}>
|
||||
<Telemetry />
|
||||
<RouterProvider
|
||||
fallbackElement={<AppFallback />}
|
||||
fallbackElement={<AppContainer fallback />}
|
||||
router={router}
|
||||
future={future}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/electron",
|
||||
"private": true,
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"author": "toeverything",
|
||||
"repository": {
|
||||
"url": "https://github.com/toeverything/AFFiNE",
|
||||
@@ -28,7 +28,7 @@
|
||||
"@affine/core": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@affine/native": "workspace:*",
|
||||
"@blocksuite/affine": "0.17.25",
|
||||
"@blocksuite/affine": "0.17.33",
|
||||
"@electron-forge/cli": "^7.3.0",
|
||||
"@electron-forge/core": "^7.3.0",
|
||||
"@electron-forge/core-utils": "^7.3.0",
|
||||
@@ -66,14 +66,14 @@
|
||||
"tree-kill": "^1.2.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"uuid": "^11.0.0",
|
||||
"vitest": "2.1.1",
|
||||
"vitest": "2.1.4",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"async-call-rpc": "^6.4.2",
|
||||
"electron-updater": "^6.2.1",
|
||||
"link-preview-js": "^3.0.5",
|
||||
"next-themes": "^0.3.0",
|
||||
"next-themes": "^0.4.0",
|
||||
"yjs": "patch:yjs@npm%3A13.6.18#~/.yarn/patches/yjs-npm-13.6.18-ad0d5f7c43.patch"
|
||||
},
|
||||
"build": {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { GlobalLoading } from '@affine/component/global-loading';
|
||||
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||
import { AffineContext } from '@affine/core/components/context';
|
||||
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
|
||||
import { Telemetry } from '@affine/core/components/telemetry';
|
||||
import { AppContainer } from '@affine/core/desktop/components/app-container';
|
||||
import { router } from '@affine/core/desktop/router';
|
||||
import { configureCommonModules } from '@affine/core/modules';
|
||||
import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header';
|
||||
@@ -11,27 +9,38 @@ import {
|
||||
configureDesktopApiModule,
|
||||
DesktopApiService,
|
||||
} from '@affine/core/modules/desktop-api';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import {
|
||||
configureSpellCheckSettingModule,
|
||||
EditorSettingService,
|
||||
} from '@affine/core/modules/editor-setting';
|
||||
import { configureFindInPageModule } from '@affine/core/modules/find-in-page';
|
||||
import { I18nProvider } from '@affine/core/modules/i18n';
|
||||
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
|
||||
import { CustomThemeModifier } from '@affine/core/modules/theme-editor';
|
||||
import {
|
||||
ClientSchemeProvider,
|
||||
PopupWindowProvider,
|
||||
} from '@affine/core/modules/url';
|
||||
import { configureSqliteUserspaceStorageProvider } from '@affine/core/modules/userspace';
|
||||
import { configureDesktopWorkbenchModule } from '@affine/core/modules/workbench';
|
||||
import {
|
||||
configureDesktopWorkbenchModule,
|
||||
WorkbenchService,
|
||||
} from '@affine/core/modules/workbench';
|
||||
import {
|
||||
configureBrowserWorkspaceFlavours,
|
||||
configureSqliteWorkspaceEngineStorageProvider,
|
||||
} from '@affine/core/modules/workspace-engine';
|
||||
import createEmotionCache from '@affine/core/utils/create-emotion-cache';
|
||||
import { apis, events } from '@affine/electron-api';
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import {
|
||||
DocsService,
|
||||
Framework,
|
||||
FrameworkRoot,
|
||||
getCurrentStore,
|
||||
GlobalContextService,
|
||||
LifecycleService,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { Suspense } from 'react';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
@@ -72,6 +81,7 @@ configureDesktopWorkbenchModule(framework);
|
||||
configureAppTabsHeaderModule(framework);
|
||||
configureFindInPageModule(framework);
|
||||
configureDesktopApiModule(framework);
|
||||
configureSpellCheckSettingModule(framework);
|
||||
|
||||
framework.impl(PopupWindowProvider, p => {
|
||||
const apis = p.get(DesktopApiService).api;
|
||||
@@ -110,6 +120,55 @@ window.addEventListener('focus', () => {
|
||||
frameworkProvider.get(LifecycleService).applicationFocus();
|
||||
});
|
||||
frameworkProvider.get(LifecycleService).applicationStart();
|
||||
window.addEventListener('unload', () => {
|
||||
frameworkProvider
|
||||
.get(DesktopApiService)
|
||||
.api.handler.ui.pingAppLayoutReady(false)
|
||||
.catch(console.error);
|
||||
});
|
||||
|
||||
events?.applicationMenu.openAboutPageInSettingModal(() =>
|
||||
frameworkProvider.get(GlobalDialogService).open('setting', {
|
||||
activeTab: 'about',
|
||||
})
|
||||
);
|
||||
events?.applicationMenu.onNewPageAction(() => {
|
||||
const currentWorkspaceId = frameworkProvider
|
||||
.get(GlobalContextService)
|
||||
.globalContext.workspaceId.get();
|
||||
const workspacesService = frameworkProvider.get(WorkspacesService);
|
||||
const workspaceMetadata = currentWorkspaceId
|
||||
? workspacesService.list.workspace$(currentWorkspaceId).value
|
||||
: null;
|
||||
const workspaceRef =
|
||||
workspaceMetadata &&
|
||||
workspacesService.open({ metadata: workspaceMetadata });
|
||||
if (!workspaceRef) {
|
||||
return;
|
||||
}
|
||||
const { workspace, dispose } = workspaceRef;
|
||||
const editorSettingService = frameworkProvider.get(EditorSettingService);
|
||||
const docsService = workspace.scope.get(DocsService);
|
||||
const editorSetting = editorSettingService.editorSetting;
|
||||
|
||||
const docProps = {
|
||||
note: editorSetting.get('affine:note'),
|
||||
};
|
||||
apis?.ui
|
||||
.isActiveTab()
|
||||
.then(isActive => {
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
const page = docsService.createDoc({ docProps });
|
||||
workspace.scope.get(WorkbenchService).workbench.openDoc(page.id);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
dispose();
|
||||
});
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
@@ -119,11 +178,8 @@ export function App() {
|
||||
<I18nProvider>
|
||||
<AffineContext store={getCurrentStore()}>
|
||||
<DesktopThemeSync />
|
||||
<Telemetry />
|
||||
<CustomThemeModifier />
|
||||
<GlobalLoading />
|
||||
<RouterProvider
|
||||
fallbackElement={<AppFallback />}
|
||||
fallbackElement={<AppContainer fallback />}
|
||||
router={router}
|
||||
future={future}
|
||||
/>
|
||||
|
||||
@@ -16,18 +16,17 @@ export const root = style({
|
||||
},
|
||||
});
|
||||
|
||||
export const body = style({
|
||||
flex: 1,
|
||||
paddingTop: 52,
|
||||
});
|
||||
|
||||
export const appTabsHeader = style({
|
||||
zIndex: 1,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
});
|
||||
|
||||
export const fallbackRoot = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
paddingTop: 52,
|
||||
});
|
||||
|
||||
export const splitViewFallback = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ShellAppFallback } from '@affine/core/components/affine/app-container';
|
||||
import { useAppSettingHelper } from '@affine/core/components/hooks/affine/use-app-setting-helper';
|
||||
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
|
||||
import { ThemeProvider } from '@affine/core/components/theme-provider';
|
||||
import { configureAppSidebarModule } from '@affine/core/modules/app-sidebar';
|
||||
import { ShellAppSidebarFallback } from '@affine/core/modules/app-sidebar/views';
|
||||
import {
|
||||
AppTabsHeader,
|
||||
configureAppTabsHeaderModule,
|
||||
@@ -10,7 +11,6 @@ import { configureDesktopApiModule } from '@affine/core/modules/desktop-api';
|
||||
import { configureI18nModule, I18nProvider } from '@affine/core/modules/i18n';
|
||||
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
|
||||
import { configureAppThemeModule } from '@affine/core/modules/theme';
|
||||
import { SplitViewFallback } from '@affine/core/modules/workbench/view/split-view/split-view';
|
||||
import {
|
||||
configureGlobalStorageModule,
|
||||
Framework,
|
||||
@@ -41,9 +41,14 @@ export function App() {
|
||||
<I18nProvider>
|
||||
<div className={styles.root} data-translucent={translucent}>
|
||||
<AppTabsHeader mode="shell" className={styles.appTabsHeader} />
|
||||
<ShellAppFallback className={styles.fallbackRoot}>
|
||||
<SplitViewFallback className={styles.splitViewFallback} />
|
||||
</ShellAppFallback>
|
||||
<div className={styles.body}>
|
||||
<ShellAppSidebarFallback />
|
||||
</div>
|
||||
{environment.isWindows && (
|
||||
<div style={{ position: 'fixed', right: 0, top: 0, zIndex: 5 }}>
|
||||
<WindowsAppControls />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</I18nProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DesktopApiService } from '@affine/core/modules/desktop-api/service';
|
||||
import { DesktopApiService } from '@affine/core/modules/desktop-api';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useRef } from 'react';
|
||||
|
||||
@@ -3,17 +3,25 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const filenamesMapping = {
|
||||
windows: 'latest.yml',
|
||||
all: 'latest.yml',
|
||||
macos: 'latest-mac.yml',
|
||||
linux: 'latest-linux.yml',
|
||||
};
|
||||
|
||||
const releaseFiles = ['zip', 'exe', 'dmg', 'appimage', 'deb', 'flatpak'];
|
||||
|
||||
const generateYml = platform => {
|
||||
const yml = {
|
||||
version: process.env.RELEASE_VERSION ?? '0.0.0',
|
||||
files: [],
|
||||
};
|
||||
const regex = new RegExp(`^affine-.*-${platform}-.*.(exe|zip|dmg|appimage)$`);
|
||||
|
||||
const regex =
|
||||
// we involves all distribution files in one release file to enforce we handle auto updater correctly
|
||||
platform === 'all'
|
||||
? new RegExp(`.(${releaseFiles.join('|')})$`)
|
||||
: new RegExp(`.+-${platform}-.+.(${releaseFiles.join('|')})$`);
|
||||
|
||||
const files = fs.readdirSync(process.cwd()).filter(file => regex.test(file));
|
||||
const outputFileName = filenamesMapping[platform];
|
||||
|
||||
@@ -34,11 +42,14 @@ const generateYml = platform => {
|
||||
});
|
||||
} catch {}
|
||||
});
|
||||
// path & sha512 are deprecated
|
||||
yml.path = yml.files[0].url;
|
||||
yml.sha512 = yml.files[0].sha512;
|
||||
yml.releaseDate = new Date().toISOString();
|
||||
|
||||
// NOTE(@forehalo): make sure old windows x64 won't fetch windows arm64 by default
|
||||
// maybe we need to separate arm64 builds to separated yml file `latest-arm64.yml`, `latest-linux-arm64.yml`
|
||||
// check https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/src/providers/Provider.ts#L30
|
||||
// and packages/frontend/apps/electron/src/main/updater/affine-update-provider.ts#L100
|
||||
yml.files.sort(a => (a.url.includes('windows-arm64') ? 1 : -1));
|
||||
|
||||
const ymlStr =
|
||||
`version: ${yml.version}\n` +
|
||||
`files:\n` +
|
||||
@@ -51,13 +62,11 @@ const generateYml = platform => {
|
||||
);
|
||||
})
|
||||
.join('') +
|
||||
`path: ${yml.path}\n` +
|
||||
`sha512: ${yml.sha512}\n` +
|
||||
`releaseDate: ${yml.releaseDate}\n`;
|
||||
|
||||
fs.writeFileSync(outputFileName, ymlStr);
|
||||
};
|
||||
|
||||
generateYml('windows');
|
||||
generateYml('macos');
|
||||
generateYml('linux');
|
||||
generateYml('all');
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseArgs } from 'node:util';
|
||||
|
||||
import debug from 'debug';
|
||||
import { z } from 'zod';
|
||||
|
||||
const log = debug('affine:make-env');
|
||||
|
||||
const ReleaseTypeSchema = z.enum(['stable', 'beta', 'canary', 'internal']);
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
@@ -36,15 +40,27 @@ const icnsPath = path.join(
|
||||
const iconPngPath = path.join(ROOT, './resources/icons/icon.png');
|
||||
|
||||
const iconUrl = `https://cdn.affine.pro/app-icons/icon_${buildType}.ico`;
|
||||
const arch =
|
||||
process.argv.indexOf('--arch') > 0
|
||||
? process.argv[process.argv.indexOf('--arch') + 1]
|
||||
: process.arch;
|
||||
|
||||
const platform =
|
||||
process.argv.indexOf('--platform') > 0
|
||||
? process.argv[process.argv.indexOf('--platform') + 1]
|
||||
: process.platform;
|
||||
log(`buildType=${buildType}, productName=${productName}, icoPath=${icoPath}`);
|
||||
|
||||
const {
|
||||
values: { arch, platform },
|
||||
} = parseArgs({
|
||||
options: {
|
||||
arch: {
|
||||
type: 'string',
|
||||
description: 'The architecture to build for',
|
||||
default: process.arch,
|
||||
},
|
||||
platform: {
|
||||
type: 'string',
|
||||
description: 'The platform to build for',
|
||||
default: process.platform,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
log(`parsed args: arch=${arch}, platform=${platform}`);
|
||||
|
||||
const appIdMap = {
|
||||
internal: 'pro.affine.internal',
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
ROOT,
|
||||
} from './make-env.js';
|
||||
|
||||
const log = debug('make-nsis');
|
||||
const log = debug('affine:make-nsis');
|
||||
|
||||
async function make() {
|
||||
const appName = productName;
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
ROOT,
|
||||
} from './make-env.js';
|
||||
|
||||
const log = debug('make-squirrel');
|
||||
const log = debug('affine:make-squirrel');
|
||||
|
||||
// taking from https://github.com/electron/forge/blob/main/packages/maker/squirrel/src/MakerSquirrel.ts
|
||||
// it was for forge's maker, but can be used standalone as well
|
||||
@@ -29,6 +29,7 @@ async function make() {
|
||||
buildType,
|
||||
`${appName}-${platform}-${arch}`
|
||||
);
|
||||
log('making squirrel.windows: appDirectory', appDirectory);
|
||||
await fs.ensureDir(outPath);
|
||||
|
||||
const packageJSON = await fs.readJson(path.resolve(ROOT, 'package.json'));
|
||||
@@ -40,7 +41,7 @@ async function make() {
|
||||
exe: `${appName}.exe`,
|
||||
setupExe: `${appName}-${packageJSON.version} Setup.exe`,
|
||||
version: packageJSON.version,
|
||||
appDirectory: appDirectory,
|
||||
appDirectory,
|
||||
outputDirectory: outPath,
|
||||
iconUrl: iconUrl,
|
||||
setupIcon: icoPath,
|
||||
|
||||
@@ -9,11 +9,8 @@ import { storeWorkspaceMeta } from '../workspace';
|
||||
import { getWorkspaceDBPath, getWorkspacesBasePath } from '../workspace/meta';
|
||||
|
||||
export type ErrorMessage =
|
||||
| 'DB_FILE_ALREADY_LOADED'
|
||||
| 'DB_FILE_PATH_INVALID'
|
||||
| 'DB_FILE_INVALID'
|
||||
| 'DB_FILE_MIGRATION_FAILED'
|
||||
| 'FILE_ALREADY_EXISTS'
|
||||
| 'UNKNOWN_ERROR';
|
||||
|
||||
export interface LoadDBFileResult {
|
||||
|
||||
20
packages/frontend/apps/electron/src/main/cleanup.ts
Normal file
20
packages/frontend/apps/electron/src/main/cleanup.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { app } from 'electron';
|
||||
|
||||
import { logger } from './logger';
|
||||
|
||||
const cleanupRegistry: (() => void)[] = [];
|
||||
|
||||
export function beforeAppQuit(fn: () => void) {
|
||||
cleanupRegistry.push(fn);
|
||||
}
|
||||
|
||||
app.on('before-quit', () => {
|
||||
cleanupRegistry.forEach(fn => {
|
||||
// some cleanup functions might throw on quit and crash the app
|
||||
try {
|
||||
fn();
|
||||
} catch (err) {
|
||||
logger.warn('cleanup error on quit', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -30,11 +30,15 @@ export function setupDeepLink(app: App) {
|
||||
}
|
||||
|
||||
app.on('open-url', (event, url) => {
|
||||
logger.log('open-url', url);
|
||||
if (url.startsWith(`${protocol}://`)) {
|
||||
event.preventDefault();
|
||||
handleAffineUrl(url).catch(e => {
|
||||
logger.error('failed to handle affine url', e);
|
||||
});
|
||||
app
|
||||
.whenReady()
|
||||
.then(() => handleAffineUrl(url))
|
||||
.catch(e => {
|
||||
logger.error('failed to handle affine url', e);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { app, BrowserWindow, WebContentsView } from 'electron';
|
||||
import { BrowserWindow, WebContentsView } from 'electron';
|
||||
|
||||
import { AFFINE_EVENT_CHANNEL_NAME } from '../shared/type';
|
||||
import { applicationMenuEvents } from './application-menu';
|
||||
import { beforeAppQuit } from './cleanup';
|
||||
import { logger } from './logger';
|
||||
import { sharedStorageEvents } from './shared-storage';
|
||||
import { uiEvents } from './ui/events';
|
||||
@@ -56,14 +57,10 @@ export function registerEvents() {
|
||||
unsubs.push(unsubscribe);
|
||||
}
|
||||
}
|
||||
app.on('before-quit', () => {
|
||||
// subscription on quit sometimes crashes the app
|
||||
unsubs.forEach(unsub => {
|
||||
try {
|
||||
unsub();
|
||||
} catch (err) {
|
||||
logger.warn('unsubscribe error on quit', err);
|
||||
}
|
||||
|
||||
unsubs.forEach(unsub => {
|
||||
beforeAppQuit(() => {
|
||||
unsub();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { NamespaceHandlers } from '../type';
|
||||
import { savePDFFileAs } from './pdf';
|
||||
|
||||
export const exportHandlers = {
|
||||
savePDFFileAs: async (_, title: string) => {
|
||||
return savePDFFileAs(title);
|
||||
},
|
||||
} satisfies NamespaceHandlers;
|
||||
|
||||
export * from './pdf';
|
||||
@@ -1,90 +0,0 @@
|
||||
import { BrowserWindow, dialog } from 'electron';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { logger } from '../logger';
|
||||
import type { ErrorMessage } from './utils';
|
||||
import { getFakedResult } from './utils';
|
||||
|
||||
export interface SavePDFFileResult {
|
||||
filePath?: string;
|
||||
canceled?: boolean;
|
||||
error?: ErrorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when the user clicks the "Export to PDF" button in the electron.
|
||||
*
|
||||
* It will just copy the file to the given path
|
||||
*/
|
||||
export async function savePDFFileAs(
|
||||
pageTitle: string
|
||||
): Promise<SavePDFFileResult> {
|
||||
try {
|
||||
const ret =
|
||||
getFakedResult() ??
|
||||
(await dialog.showSaveDialog({
|
||||
properties: ['showOverwriteConfirmation'],
|
||||
title: 'Save PDF',
|
||||
showsTagField: false,
|
||||
buttonLabel: 'Save',
|
||||
defaultPath: `${pageTitle}.pdf`,
|
||||
message: 'Save Page as a PDF file',
|
||||
filters: [{ name: 'PDF Files', extensions: ['pdf'] }],
|
||||
}));
|
||||
const filePath = ret.filePath;
|
||||
if (ret.canceled || !filePath) {
|
||||
return {
|
||||
canceled: true,
|
||||
};
|
||||
}
|
||||
|
||||
await BrowserWindow.getFocusedWindow()
|
||||
?.webContents.printToPDF({
|
||||
pageSize: 'A4',
|
||||
margins: {
|
||||
bottom: 0.5,
|
||||
},
|
||||
printBackground: true,
|
||||
landscape: false,
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: '<div></div>',
|
||||
footerTemplate: getFootTemple(),
|
||||
})
|
||||
.then(data => {
|
||||
fs.writeFile(filePath, data, error => {
|
||||
if (error) throw error;
|
||||
logger.log(`Wrote PDF successfully to ${filePath}`);
|
||||
});
|
||||
});
|
||||
return { filePath };
|
||||
} catch (err) {
|
||||
logger.error('savePDFFileAs', err);
|
||||
return {
|
||||
error: 'UNKNOWN_ERROR',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getFootTemple(): string {
|
||||
const logo = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="53" height="12" viewBox="0 0 53 12" fill="none">
|
||||
<path d="M18.9256 0.709372C18.8749 0.504937 18.6912 0.361572 18.4807 0.361572H17.77C17.5595 0.361572 17.3758 0.504937 17.3252 0.709372L14.9153 10.4283C14.8438 10.7172 15.0621 10.9965 15.3601 10.9965H15.6052C15.8183 10.9965 16.0033 10.8497 16.0513 10.6423L16.5646 8.43721C16.6127 8.22974 16.7976 8.08291 17.0107 8.08291H19.2396C19.4527 8.08291 19.6376 8.22974 19.6857 8.43721L20.199 10.6423C20.247 10.8497 20.432 10.9965 20.6451 10.9965H20.8902C21.1878 10.9965 21.4065 10.7172 21.335 10.4283L18.9251 0.709372H18.9256ZM18.7891 7.0629H17.4616C17.1666 7.0629 16.9483 6.7883 17.0155 6.50113L17.9025 2.23181C17.9575 1.99576 18.2936 1.99576 18.3486 2.23181L19.2357 6.50113C19.3024 6.7883 19.0845 7.0629 18.7896 7.0629H18.7891Z" fill="black" fill-opacity="0.1"/>
|
||||
<path d="M36.2654 5.00861H30.766C30.5131 5.00861 30.3078 4.8033 30.3078 4.55036V2.25132C30.3078 1.77055 30.6976 1.38074 31.1783 1.38074H33.8997C34.1526 1.38074 34.3579 1.17544 34.3579 0.922494V0.818977C34.3579 0.566031 34.1526 0.36073 33.8997 0.36073H30.8539C29.8924 0.36073 29.1132 1.14036 29.1132 2.10146V5.00774H24.2171C23.9642 5.00774 23.7589 4.80244 23.7589 4.54949V2.25046C23.7589 1.76969 24.1487 1.37988 24.6295 1.37988H27.3508C27.6038 1.37988 27.8091 1.17457 27.8091 0.921628V0.818111C27.8091 0.565165 27.6038 0.359863 27.3508 0.359863H24.3051C23.3435 0.359863 22.5643 1.13949 22.5643 2.1006V10.5366C22.5643 10.7895 22.7696 10.9948 23.0226 10.9948H23.3011C23.554 10.9948 23.7593 10.7895 23.7593 10.5366V6.48513C23.7593 6.23219 23.9646 6.02689 24.2176 6.02689H29.1136V10.5366C29.1136 10.7895 29.3189 10.9948 29.5719 10.9948H29.8504C30.1033 10.9948 30.3086 10.7895 30.3086 10.5366V6.48513C30.3086 6.23219 30.5139 6.02689 30.7669 6.02689H35.9713C36.4521 6.02689 36.8419 6.4167 36.8419 6.89747V10.5418C36.8419 10.7947 37.0472 11 37.3001 11H37.5492C37.8021 11 38.0074 10.7947 38.0074 10.5418V6.74804C38.0074 5.7865 37.2278 5.00731 36.2667 5.00731L36.2654 5.00861Z" fill="black" fill-opacity="0.1"/>
|
||||
<path d="M45.2871 0.361517H45.0363C44.7838 0.361517 44.5789 0.565953 44.5781 0.818032L44.5504 9.53946L42.0512 0.695024C41.9954 0.497519 41.8156 0.361517 41.6103 0.361517H40.521C40.268 0.361517 40.0627 0.566819 40.0627 0.819765V10.5387C40.0627 10.7916 40.268 10.9969 40.521 10.9969H40.7718C41.0243 10.9969 41.2292 10.7925 41.23 10.5404L41.2577 1.81899L43.7569 10.6634C43.8128 10.8609 43.9925 10.9969 44.1978 10.9969H45.2871C45.5401 10.9969 45.7454 10.7916 45.7454 10.5387V0.819331C45.7454 0.566386 45.5401 0.361084 45.2871 0.361084V0.361517Z" fill="black" fill-opacity="0.1"/>
|
||||
<path d="M49.2307 1.3811H51.8212C52.0741 1.3811 52.2794 1.17579 52.2794 0.922849V0.819331C52.2794 0.566386 52.0741 0.361084 51.8212 0.361084H48.9214C47.9599 0.361084 47.1807 1.14071 47.1807 2.10182V9.25489C47.1807 10.2164 47.9603 10.9956 48.9214 10.9956H51.8212C52.0741 10.9956 52.2794 10.7903 52.2794 10.5374V10.4339C52.2794 10.1809 52.0741 9.97562 51.8212 9.97562H49.2307C48.7499 9.97562 48.3601 9.5858 48.3601 9.10503V6.33996C48.3601 6.08701 48.5654 5.88171 48.8183 5.88171H51.6752C51.9282 5.88171 52.1335 5.67641 52.1335 5.42346V5.31994C52.1335 5.067 51.9282 4.8617 51.6752 4.8617H48.8183C48.5654 4.8617 48.3601 4.65639 48.3601 4.40345V2.24995C48.3601 1.76918 48.7499 1.37936 49.2307 1.37936V1.3811Z" fill="black" fill-opacity="0.1"/>
|
||||
<path d="M37.3088 1.65787C37.1052 1.4543 36.7583 1.54742 36.6838 1.82549L36.3473 3.08199C36.2728 3.35962 36.527 3.61387 36.8051 3.5398L38.0616 3.20326C38.3396 3.12876 38.4323 2.7814 38.2292 2.57826L37.3097 1.65873L37.3088 1.65787Z" fill="black" fill-opacity="0.1"/>
|
||||
<path d="M11.9043 9.92891C11.8624 9.85125 11.3139 8.91339 11.2674 8.82602C9.92288 6.49855 7.98407 3.13718 6.64195 0.814557C6.37775 0.29657 5.6448 0.286862 5.36882 0.795141C3.92685 3.2932 1.71414 7.12436 0.274242 9.6193C0.214607 9.72505 0.118915 9.88037 0.0617076 9.99687C-0.035025 10.2063 -0.0163025 10.4677 0.10574 10.6608C0.238877 10.8858 0.502032 11.0165 0.759985 11.0027H0.8269C2.06986 11.0009 9.86983 11.0048 11.2844 11.0027C11.8329 11.0041 12.1779 10.4022 11.904 9.92926L11.9043 9.92891ZM6.09068 1.66053C6.91793 3.09661 7.8967 4.79099 8.85535 6.45036C8.47016 6.21355 8.05792 6.02875 7.6169 5.91711C7.49763 5.89007 7.04136 5.83529 6.94289 5.83113C6.9207 5.82939 6.89851 5.82835 6.87598 5.82835H5.12474C4.97288 5.82835 4.82622 5.86753 4.69655 5.93757C4.53325 5.14845 4.68199 4.26468 4.8914 3.51683C4.90978 3.45269 4.92954 3.38889 4.9493 3.3251C5.29636 2.7239 5.62366 2.15737 5.91073 1.65984C5.95095 1.5905 6.0508 1.59084 6.09068 1.65984V1.66053ZM6.15412 8.25707C6.15412 8.25707 6.12327 8.30492 6.09692 8.34583C6.07161 8.37079 6.03728 8.38535 5.99984 8.38535C5.94956 8.38535 5.90484 8.35935 5.87988 8.31601L5.03841 6.85809C5.03841 6.85809 5.0124 6.80747 4.98987 6.76413C4.98085 6.72946 4.98571 6.69271 5.00408 6.66046C5.02905 6.61712 5.07412 6.59112 5.12404 6.59112H6.80733C6.80733 6.59112 6.86419 6.59389 6.91308 6.59632C6.9474 6.60568 6.97722 6.62822 6.99559 6.66046C7.02056 6.7038 7.02056 6.75581 6.99559 6.79915L6.15378 8.25707H6.15412ZM1.12681 9.94521C1.30502 9.63733 1.53766 9.23653 1.5914 9.14084C2.1971 8.09169 3.04585 6.62163 3.89252 5.15539C3.88004 5.60715 3.92616 6.05684 4.04993 6.49473C4.08599 6.61158 4.26697 7.03422 4.31274 7.12159C4.32591 7.14967 4.34256 7.17914 4.35851 7.20619L5.21939 8.69739C5.29532 8.8288 5.40245 8.93628 5.52796 9.01359C4.92607 9.54961 4.08634 9.86269 3.33397 10.0551C3.27191 10.0707 3.20916 10.0849 3.14675 10.0988C2.34827 10.0988 1.66837 10.0995 1.21695 10.1009C1.13651 10.1009 1.08659 10.0142 1.12681 9.94486V9.94521ZM10.7834 10.1016C9.65661 10.1026 7.37212 10.1005 5.25475 10.0995C5.65139 9.88453 6.01683 9.62034 6.33337 9.29478C6.41624 9.20498 6.69222 8.83712 6.74492 8.75391C6.7626 8.72825 6.77994 8.69913 6.79519 8.67208L7.65608 7.18088C7.73201 7.04947 7.77153 6.90281 7.77569 6.75546C8.54089 7.00856 9.23188 7.57925 9.77449 8.13468C9.82129 8.18322 9.86706 8.23245 9.91248 8.28203C10.2464 8.86035 10.5695 9.41994 10.8729 9.9459C10.9127 10.0152 10.8632 10.1019 10.7831 10.1019L10.7834 10.1016Z" fill="black" fill-opacity="0.1"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
const footerTemp = `
|
||||
<div style="font-size: 14px; width: 100%; display: flex; justify-content: flex-end; margin-right: 40px;">
|
||||
<a href="https://affine.pro" style="display: flex; text-decoration: none; color: rgba(0, 0, 0, 0.1);">
|
||||
<span>Created with</span>
|
||||
<div style="display: flex; align-items: center;">${logo}</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return footerTemp;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// provide a backdoor to set dialog path for testing in playwright
|
||||
interface FakeDialogResult {
|
||||
canceled?: boolean;
|
||||
filePath?: string;
|
||||
filePaths?: string[];
|
||||
}
|
||||
// result will be used in the next call to showOpenDialog
|
||||
// if it is being read once, it will be reset to undefined
|
||||
let fakeDialogResult: FakeDialogResult | undefined = undefined;
|
||||
export function getFakedResult() {
|
||||
const result = fakeDialogResult;
|
||||
fakeDialogResult = undefined;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function setFakeDialogResult(result: FakeDialogResult | undefined) {
|
||||
fakeDialogResult = result;
|
||||
// for convenience, we will fill filePaths with filePath if it is not set
|
||||
if (result?.filePaths === undefined && result?.filePath !== undefined) {
|
||||
result.filePaths = [result.filePath];
|
||||
}
|
||||
}
|
||||
const ErrorMessages = ['FILE_ALREADY_EXISTS', 'UNKNOWN_ERROR'] as const;
|
||||
export type ErrorMessage = (typeof ErrorMessages)[number];
|
||||
@@ -3,7 +3,6 @@ import { ipcMain } from 'electron';
|
||||
import { AFFINE_API_CHANNEL_NAME } from '../shared/type';
|
||||
import { clipboardHandlers } from './clipboard';
|
||||
import { configStorageHandlers } from './config-storage';
|
||||
import { exportHandlers } from './export';
|
||||
import { findInPageHandlers } from './find-in-page';
|
||||
import { getLogFilePath, logger, revealLogFile } from './logger';
|
||||
import { sharedStorageHandlers } from './shared-storage';
|
||||
@@ -24,7 +23,6 @@ export const allHandlers = {
|
||||
debug: debugHandlers,
|
||||
ui: uiHandlers,
|
||||
clipboard: clipboardHandlers,
|
||||
export: exportHandlers,
|
||||
updater: updaterHandlers,
|
||||
configStorage: configStorageHandlers,
|
||||
findInPage: findInPageHandlers,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
|
||||
import type { HelperToMain, MainToHelper } from '../shared/type';
|
||||
import { MessageEventChannel } from '../shared/utils';
|
||||
import { beforeAppQuit } from './cleanup';
|
||||
import { logger } from './logger';
|
||||
|
||||
const HELPER_PROCESS_PATH = path.join(__dirname, './helper.js');
|
||||
@@ -65,7 +66,7 @@ class HelperProcessManager {
|
||||
});
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
beforeAppQuit(() => {
|
||||
this.#process.kill();
|
||||
});
|
||||
}
|
||||
@@ -78,8 +79,12 @@ class HelperProcessManager {
|
||||
renderer.postMessage('helper-connection', null, [rendererPort]);
|
||||
|
||||
return () => {
|
||||
helperPort.close();
|
||||
rendererPort.close();
|
||||
try {
|
||||
helperPort.close();
|
||||
rendererPort.close();
|
||||
} catch (err) {
|
||||
logger.error('[helper] close port error', err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,9 @@ if (process.env.SKIP_ONBOARDING) {
|
||||
*/
|
||||
const isSingleInstance = app.requestSingleInstanceLock();
|
||||
if (!isSingleInstance) {
|
||||
logger.info('Another instance is running, exiting...');
|
||||
logger.info(
|
||||
'Another instance is running or responding deep link, exiting...'
|
||||
);
|
||||
app.quit();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ export const workbenchViewIconNameSchema = z.enum([
|
||||
'page',
|
||||
'edgeless',
|
||||
'journal',
|
||||
'attachment',
|
||||
'pdf',
|
||||
]);
|
||||
|
||||
export const workbenchViewMetaSchema = z.object({
|
||||
@@ -43,3 +45,11 @@ export type TabViewsMetaSchema = z.infer<typeof tabViewsMetaSchema>;
|
||||
export type WorkbenchMeta = z.infer<typeof workbenchMetaSchema>;
|
||||
export type WorkbenchViewMeta = z.infer<typeof workbenchViewMetaSchema>;
|
||||
export type WorkbenchViewModule = z.infer<typeof workbenchViewIconNameSchema>;
|
||||
|
||||
export const SpellCheckStateSchema = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const SpellCheckStateKey = 'spellCheckState';
|
||||
export type SpellCheckStateKey = typeof SpellCheckStateKey;
|
||||
export type SpellCheckStateSchema = z.infer<typeof SpellCheckStateSchema>;
|
||||
@@ -1,8 +1,10 @@
|
||||
import { app, nativeTheme, shell } from 'electron';
|
||||
import { getLinkPreview } from 'link-preview-js';
|
||||
|
||||
import { isMacOS } from '../../shared/utils';
|
||||
import { persistentConfig } from '../config-storage/persist';
|
||||
import { logger } from '../logger';
|
||||
import type { WorkbenchViewMeta } from '../shared-state-schema';
|
||||
import type { NamespaceHandlers } from '../type';
|
||||
import {
|
||||
activateView,
|
||||
@@ -21,6 +23,7 @@ import {
|
||||
pingAppLayoutReady,
|
||||
showDevTools,
|
||||
showTab,
|
||||
updateActiveViewMeta,
|
||||
updateWorkbenchMeta,
|
||||
updateWorkbenchViewMeta,
|
||||
} from '../windows-manager';
|
||||
@@ -173,6 +176,9 @@ export const uiHandlers = {
|
||||
getTabViewsMeta: async () => {
|
||||
return getTabViewsMeta();
|
||||
},
|
||||
updateActiveViewMeta: async (e, meta: Partial<WorkbenchViewMeta>) => {
|
||||
return updateActiveViewMeta(e.sender, meta);
|
||||
},
|
||||
getTabsStatus: async () => {
|
||||
return getTabsStatus();
|
||||
},
|
||||
@@ -197,8 +203,8 @@ export const uiHandlers = {
|
||||
uiSubjects.onToggleRightSidebar$.next(tabId);
|
||||
}
|
||||
},
|
||||
pingAppLayoutReady: async e => {
|
||||
pingAppLayoutReady(e.sender);
|
||||
pingAppLayoutReady: async (e, ready = true) => {
|
||||
pingAppLayoutReady(e.sender, ready);
|
||||
},
|
||||
showDevTools: async (_, ...args: Parameters<typeof showDevTools>) => {
|
||||
return showDevTools(...args);
|
||||
@@ -211,4 +217,19 @@ export const uiHandlers = {
|
||||
win.show();
|
||||
win.focus();
|
||||
},
|
||||
restartApp: async () => {
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
},
|
||||
onLanguageChange: async (e, language: string) => {
|
||||
// only works for win/linux
|
||||
// see https://www.electronjs.org/docs/latest/tutorial/spellchecker#how-to-set-the-languages-the-spellchecker-uses
|
||||
if (isMacOS()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.sender.session.availableSpellCheckerLanguages.includes(language)) {
|
||||
e.sender.session.setSpellCheckerLanguages([language, 'en-US']);
|
||||
}
|
||||
},
|
||||
} satisfies NamespaceHandlers;
|
||||
|
||||
@@ -5,7 +5,6 @@ import { newError } from 'builder-util-runtime';
|
||||
import type {
|
||||
AppUpdater,
|
||||
ResolvedUpdateFileInfo,
|
||||
UpdateFileInfo,
|
||||
UpdateInfo,
|
||||
} from 'electron-updater';
|
||||
import { CancellationToken, Provider } from 'electron-updater';
|
||||
@@ -23,12 +22,18 @@ interface GithubUpdateInfo extends UpdateInfo {
|
||||
}
|
||||
|
||||
interface GithubRelease {
|
||||
url: string;
|
||||
name: string;
|
||||
tag_name: string;
|
||||
body: string;
|
||||
draft: boolean;
|
||||
prerelease: boolean;
|
||||
created_at: string;
|
||||
published_at: string;
|
||||
assets: Array<{
|
||||
name: string;
|
||||
url: string;
|
||||
size: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -92,11 +97,15 @@ export class AFFiNEUpdateProvider extends Provider<GithubUpdateInfo> {
|
||||
const latestRelease = releases[0] as GithubRelease;
|
||||
const tag = latestRelease.tag_name;
|
||||
|
||||
const channelFileName = getChannelFilename(this.getDefaultChannelName());
|
||||
const channelFileAsset = latestRelease.assets.find(({ url }) =>
|
||||
url.endsWith(channelFileName)
|
||||
const channelFileName = 'latest.yml';
|
||||
const channelFileAsset = latestRelease.assets.find(
|
||||
({ name }) => name === channelFileName
|
||||
);
|
||||
|
||||
// TODO(@forehalo):
|
||||
// we need a way to let UI thread prompt user to manually install the latest version,
|
||||
// if we introduce breaking changes on auto updater in the future.
|
||||
// for example we rename the release file from `latest.yml` to `release.yml`
|
||||
if (!channelFileAsset) {
|
||||
throw newError(
|
||||
`Cannot find ${channelFileName} in the latest release artifacts.`,
|
||||
@@ -113,32 +122,30 @@ export class AFFiNEUpdateProvider extends Provider<GithubUpdateInfo> {
|
||||
channelFileUrl
|
||||
);
|
||||
|
||||
const files: UpdateFileInfo[] = [];
|
||||
|
||||
result.files.forEach(file => {
|
||||
const asset = latestRelease.assets.find(({ name }) => name === file.url);
|
||||
if (asset) {
|
||||
file.url = asset.url;
|
||||
}
|
||||
|
||||
// for windows, we need to determine its installer type (nsis or squirrel)
|
||||
if (process.platform === 'win32') {
|
||||
const isSquirrel = isSquirrelBuild();
|
||||
if (isSquirrel && file.url.endsWith('.nsis.exe')) {
|
||||
return;
|
||||
result.files
|
||||
.filter(({ url }) =>
|
||||
availableForMyPlatformAndInstaller(
|
||||
url,
|
||||
process.platform,
|
||||
process.arch,
|
||||
isSquirrelBuild()
|
||||
)
|
||||
)
|
||||
.forEach(file => {
|
||||
const asset = latestRelease.assets.find(
|
||||
({ name }) => name === file.url
|
||||
);
|
||||
if (asset) {
|
||||
file.url = asset.url;
|
||||
}
|
||||
}
|
||||
|
||||
files.push(file);
|
||||
});
|
||||
});
|
||||
|
||||
if (result.releaseName == null) {
|
||||
result.releaseName = latestRelease.name;
|
||||
}
|
||||
|
||||
if (result.releaseNotes == null) {
|
||||
// TODO(@forehalo): add release notes
|
||||
result.releaseNotes = '';
|
||||
result.releaseNotes = latestRelease.body;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -150,13 +157,130 @@ export class AFFiNEUpdateProvider extends Provider<GithubUpdateInfo> {
|
||||
resolveFiles(updateInfo: GithubUpdateInfo): Array<ResolvedUpdateFileInfo> {
|
||||
const files = getFileList(updateInfo);
|
||||
|
||||
return files.map(file => ({
|
||||
url: new URL(file.url),
|
||||
info: file,
|
||||
}));
|
||||
return files
|
||||
.filter(({ url }) =>
|
||||
availableForMyPlatformAndInstaller(
|
||||
url,
|
||||
process.platform,
|
||||
process.arch,
|
||||
isSquirrelBuild()
|
||||
)
|
||||
)
|
||||
.map(file => ({
|
||||
url: new URL(file.url),
|
||||
info: file,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function getChannelFilename(channel: string): string {
|
||||
return `${channel}.yml`;
|
||||
type VersionDistribution = 'canary' | 'beta' | 'stable';
|
||||
type VersionPlatform = 'windows' | 'macos' | 'linux';
|
||||
type VersionArch = 'x64' | 'arm64';
|
||||
type FileParts =
|
||||
| ['affine', string, VersionDistribution, VersionPlatform, VersionArch]
|
||||
| [
|
||||
'affine',
|
||||
string,
|
||||
`${'canary' | 'beta'}.${number}`,
|
||||
VersionDistribution,
|
||||
VersionPlatform,
|
||||
VersionArch,
|
||||
];
|
||||
|
||||
export function availableForMyPlatformAndInstaller(
|
||||
file: string,
|
||||
platform: NodeJS.Platform,
|
||||
arch: NodeJS.Architecture,
|
||||
// moved to parameter to make test coverage easier
|
||||
imWindowsSquirrelPkg: boolean
|
||||
): boolean {
|
||||
const imArm64 = arch === 'arm64';
|
||||
const imX64 = arch === 'x64';
|
||||
const imMacos = platform === 'darwin';
|
||||
const imWindows = platform === 'win32';
|
||||
const imLinux = platform === 'linux';
|
||||
|
||||
// in form of:
|
||||
// affine-${build}-${buildSuffix}-${distribution}-${platform}-${arch}.${installer}
|
||||
// ^ 1.0.0 ^canary.1 ^ canary ^windows ^ x64 ^.nsis.exe
|
||||
const filename = file.split('/').pop();
|
||||
|
||||
if (!filename) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parts = filename.split('-') as FileParts;
|
||||
|
||||
// fix -${arch}.${installer}
|
||||
const archDotInstaller = parts[parts.length - 1];
|
||||
const installerIdx = archDotInstaller.indexOf('.');
|
||||
if (installerIdx === -1) {
|
||||
return false;
|
||||
}
|
||||
const installer = archDotInstaller.substring(installerIdx + 1);
|
||||
parts[parts.length - 1] = archDotInstaller.substring(0, installerIdx);
|
||||
|
||||
let version: {
|
||||
build: string;
|
||||
suffix?: string;
|
||||
distribution: VersionDistribution;
|
||||
platform: VersionPlatform;
|
||||
arch: VersionArch;
|
||||
installer: string;
|
||||
};
|
||||
|
||||
if (parts.length === 5) {
|
||||
version = {
|
||||
build: parts[1],
|
||||
distribution: parts[2],
|
||||
platform: parts[3],
|
||||
arch: parts[4],
|
||||
installer,
|
||||
};
|
||||
} else if (parts.length === 6) {
|
||||
version = {
|
||||
build: parts[1],
|
||||
suffix: parts[2],
|
||||
distribution: parts[3],
|
||||
platform: parts[4],
|
||||
arch: parts[5],
|
||||
installer,
|
||||
};
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
function matchPlatform(platform: VersionPlatform) {
|
||||
return (
|
||||
(platform === 'windows' && imWindows) ||
|
||||
(platform === 'macos' && imMacos) ||
|
||||
(platform === 'linux' && imLinux)
|
||||
);
|
||||
}
|
||||
|
||||
function matchArch(arch: VersionArch) {
|
||||
return (
|
||||
// off course we can install x64 on x64
|
||||
(imX64 && arch === 'x64') ||
|
||||
// arm64 macos can install arm64 or x64 in rosetta2
|
||||
(imArm64 && (arch === 'arm64' || imMacos))
|
||||
);
|
||||
}
|
||||
|
||||
function matchInstaller(installer: string) {
|
||||
// do not allow squirrel or nsis installer to cross download each other on windows
|
||||
if (!imWindows) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return imWindowsSquirrelPkg
|
||||
? installer === 'exe'
|
||||
: installer === 'nsis.exe';
|
||||
}
|
||||
|
||||
return (
|
||||
matchPlatform(version.platform) &&
|
||||
matchArch(version.arch) &&
|
||||
matchInstaller(version.installer)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import electronWindowState from 'electron-window-state';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { isLinux, isMacOS, isWindows } from '../../shared/utils';
|
||||
import { beforeAppQuit } from '../cleanup';
|
||||
import { buildType } from '../config';
|
||||
import { mainWindowOrigin } from '../constants';
|
||||
import { ensureHelperProcess } from '../helper-process';
|
||||
@@ -116,11 +117,17 @@ export class MainWindowManager {
|
||||
uiSubjects.onFullScreen$.next(mainWindow.isFullScreen());
|
||||
});
|
||||
|
||||
beforeAppQuit(() => {
|
||||
this.cleanupWindows();
|
||||
});
|
||||
|
||||
mainWindow.on('close', e => {
|
||||
// TODO(@pengx17): gracefully close the app, for example, ask user to save unsaved changes
|
||||
e.preventDefault();
|
||||
if (!isMacOS()) {
|
||||
closeAllWindows();
|
||||
this.mainWindowReady = undefined;
|
||||
this.mainWindow$.next(undefined);
|
||||
} else {
|
||||
// hide window on macOS
|
||||
// application quit will be handled by closing the hidden window
|
||||
|
||||
@@ -2,6 +2,9 @@ import { join } from 'node:path';
|
||||
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
Menu,
|
||||
MenuItem,
|
||||
session,
|
||||
type View,
|
||||
type WebContents,
|
||||
@@ -22,20 +25,22 @@ import {
|
||||
} from 'rxjs';
|
||||
|
||||
import { isMacOS } from '../../shared/utils';
|
||||
import { beforeAppQuit } from '../cleanup';
|
||||
import { CLOUD_BASE_URL, isDev } from '../config';
|
||||
import { mainWindowOrigin, shellViewUrl } from '../constants';
|
||||
import { ensureHelperProcess } from '../helper-process';
|
||||
import { logger } from '../logger';
|
||||
import { globalStateStorage } from '../shared-storage/storage';
|
||||
import { getCustomThemeWindow } from './custom-theme-window';
|
||||
import { getMainWindow, MainWindowManager } from './main-window';
|
||||
import {
|
||||
SpellCheckStateKey,
|
||||
SpellCheckStateSchema,
|
||||
TabViewsMetaKey,
|
||||
type TabViewsMetaSchema,
|
||||
tabViewsMetaSchema,
|
||||
type WorkbenchMeta,
|
||||
type WorkbenchViewMeta,
|
||||
} from './tab-views-meta-schema';
|
||||
} from '../shared-state-schema';
|
||||
import { globalStateStorage } from '../shared-storage/storage';
|
||||
import { getMainWindow, MainWindowManager } from './main-window';
|
||||
|
||||
async function getAdditionalArguments() {
|
||||
const { getExposedMeta } = await import('../exposed');
|
||||
@@ -74,6 +79,10 @@ const TabViewsMetaState = {
|
||||
},
|
||||
};
|
||||
|
||||
const spellCheckSettings = SpellCheckStateSchema.parse(
|
||||
globalStateStorage.get(SpellCheckStateKey) ?? {}
|
||||
);
|
||||
|
||||
type AddTabAction = {
|
||||
type: 'add-tab';
|
||||
payload: WorkbenchMeta;
|
||||
@@ -270,7 +279,14 @@ export class WebContentViewsManager {
|
||||
}
|
||||
};
|
||||
|
||||
getViewIdFromWebContentsId = (id: number) => {
|
||||
setTabUIUnready = (tabId: string) => {
|
||||
this.appTabsUIReady$.next(
|
||||
new Set([...this.appTabsUIReady$.value].filter(key => key !== tabId))
|
||||
);
|
||||
this.reorderViews();
|
||||
};
|
||||
|
||||
getWorkbenchIdFromWebContentsId = (id: number) => {
|
||||
return Array.from(this.tabViewsMap.entries()).find(
|
||||
([, view]) => view.webContents.id === id
|
||||
)?.[0];
|
||||
@@ -303,7 +319,7 @@ export class WebContentViewsManager {
|
||||
|
||||
updateWorkbenchViewMeta = (
|
||||
workbenchId: string,
|
||||
viewId: string,
|
||||
viewId: string | number,
|
||||
patch: Partial<WorkbenchViewMeta>
|
||||
) => {
|
||||
const workbench = this.tabViewsMeta.workbenches.find(
|
||||
@@ -313,7 +329,10 @@ export class WebContentViewsManager {
|
||||
return;
|
||||
}
|
||||
const views = workbench.views;
|
||||
const viewIndex = views.findIndex(v => v.id === viewId);
|
||||
const viewIndex =
|
||||
typeof viewId === 'string'
|
||||
? views.findIndex(v => v.id === viewId)
|
||||
: viewId;
|
||||
if (viewIndex === -1) {
|
||||
return;
|
||||
}
|
||||
@@ -731,8 +750,10 @@ export class WebContentViewsManager {
|
||||
})
|
||||
);
|
||||
|
||||
app.on('before-quit', () => {
|
||||
disposables.forEach(d => d.unsubscribe());
|
||||
disposables.forEach(d => {
|
||||
beforeAppQuit(() => {
|
||||
d.unsubscribe();
|
||||
});
|
||||
});
|
||||
|
||||
const focusActiveView = () => {
|
||||
@@ -806,13 +827,44 @@ export class WebContentViewsManager {
|
||||
transparent: true,
|
||||
contextIsolation: true,
|
||||
sandbox: false,
|
||||
spellcheck: false, // TODO(@pengx17): enable?
|
||||
spellcheck: spellCheckSettings.enabled,
|
||||
preload: join(__dirname, './preload.js'), // this points to the bundled preload module
|
||||
// serialize exposed meta that to be used in preload
|
||||
additionalArguments: additionalArguments,
|
||||
},
|
||||
});
|
||||
|
||||
if (spellCheckSettings.enabled) {
|
||||
view.webContents.on('context-menu', (_event, params) => {
|
||||
const menu = new Menu();
|
||||
|
||||
// Add each spelling suggestion
|
||||
for (const suggestion of params.dictionarySuggestions) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: suggestion,
|
||||
click: () => view.webContents.replaceMisspelling(suggestion),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Allow users to add the misspelled word to the dictionary
|
||||
if (params.misspelledWord) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: 'Add to dictionary', // TODO: i18n
|
||||
click: () =>
|
||||
view.webContents.session.addWordToSpellCheckerDictionary(
|
||||
params.misspelledWord
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
menu.popup();
|
||||
});
|
||||
}
|
||||
|
||||
this.webViewsMap$.next(this.tabViewsMap.set(viewId, view));
|
||||
let unsub = () => {};
|
||||
|
||||
@@ -821,12 +873,6 @@ export class WebContentViewsManager {
|
||||
view.webContents.on('did-finish-load', () => {
|
||||
unsub = helperProcessManager.connectRenderer(view.webContents);
|
||||
});
|
||||
view.webContents.on('will-navigate', () => {
|
||||
// means the view is reloaded
|
||||
this.appTabsUIReady$.next(
|
||||
new Set([...this.appTabsUIReady$.value].filter(key => key !== viewId))
|
||||
);
|
||||
});
|
||||
} else {
|
||||
view.webContents.on('focus', () => {
|
||||
globalThis.setTimeout(() => {
|
||||
@@ -943,7 +989,7 @@ export const updateWorkbenchMeta = (
|
||||
|
||||
export const updateWorkbenchViewMeta = (
|
||||
workbenchId: string,
|
||||
viewId: string,
|
||||
viewId: string | number,
|
||||
meta: Partial<WorkbenchViewMeta>
|
||||
) => {
|
||||
WebContentViewsManager.instance.updateWorkbenchViewMeta(
|
||||
@@ -956,6 +1002,24 @@ export const updateWorkbenchViewMeta = (
|
||||
export const getWorkbenchMeta = (id: string) => {
|
||||
return TabViewsMetaState.value.workbenches.find(w => w.id === id);
|
||||
};
|
||||
|
||||
export const updateActiveViewMeta = (
|
||||
wc: WebContents,
|
||||
meta: Partial<WorkbenchViewMeta>
|
||||
) => {
|
||||
const workbenchId =
|
||||
WebContentViewsManager.instance.getWorkbenchIdFromWebContentsId(wc.id);
|
||||
const workbench = workbenchId ? getWorkbenchMeta(workbenchId) : undefined;
|
||||
|
||||
if (workbench && workbenchId) {
|
||||
return WebContentViewsManager.instance.updateWorkbenchViewMeta(
|
||||
workbenchId,
|
||||
workbench.activeViewIndex,
|
||||
meta
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getTabViewsMeta = () => TabViewsMetaState.value;
|
||||
export const isActiveTab = (wc: WebContents) => {
|
||||
return (
|
||||
@@ -1022,32 +1086,30 @@ export const onActiveTabChanged = (fn: (tabId: string) => void) => {
|
||||
|
||||
export const showDevTools = (id?: string) => {
|
||||
// use focusedWindow?
|
||||
// const focusedWindow = BrowserWindow.getFocusedWindow()
|
||||
|
||||
// workaround for opening devtools for theme-editor window
|
||||
// there should be some strategy like windows manager, so we can know which window is active
|
||||
getCustomThemeWindow()
|
||||
.then(w => {
|
||||
if (w && w.isFocused()) {
|
||||
w.webContents.openDevTools();
|
||||
} else {
|
||||
const view = id
|
||||
? WebContentViewsManager.instance.getViewById(id)
|
||||
: WebContentViewsManager.instance.activeWorkbenchView;
|
||||
if (view) {
|
||||
view.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
// check if focused window is main window
|
||||
const mainWindow = WebContentViewsManager.instance.mainWindow;
|
||||
if (focusedWindow && focusedWindow.id !== mainWindow?.id) {
|
||||
focusedWindow.webContents.openDevTools();
|
||||
} else {
|
||||
const view = id
|
||||
? WebContentViewsManager.instance.getViewById(id)
|
||||
: WebContentViewsManager.instance.activeWorkbenchView;
|
||||
if (view) {
|
||||
view.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const pingAppLayoutReady = (wc: WebContents) => {
|
||||
const viewId = WebContentViewsManager.instance.getViewIdFromWebContentsId(
|
||||
wc.id
|
||||
);
|
||||
export const pingAppLayoutReady = (wc: WebContents, ready: boolean) => {
|
||||
const viewId =
|
||||
WebContentViewsManager.instance.getWorkbenchIdFromWebContentsId(wc.id);
|
||||
if (viewId) {
|
||||
WebContentViewsManager.instance.setTabUIReady(viewId);
|
||||
if (ready) {
|
||||
WebContentViewsManager.instance.setTabUIReady(viewId);
|
||||
} else {
|
||||
WebContentViewsManager.instance.setTabUIUnready(viewId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -157,7 +157,11 @@ const createMessagePortChannel = (port: MessagePort): EventBasedChannel => {
|
||||
port.start();
|
||||
return () => {
|
||||
port.onmessage = null;
|
||||
port.close();
|
||||
try {
|
||||
port.close();
|
||||
} catch (err) {
|
||||
console.error('[helper] close port error', err);
|
||||
}
|
||||
};
|
||||
},
|
||||
send(data) {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`testing for client update > filter valid installer files > filter for platform [darwin] arch [arm64] 1`] = `
|
||||
[
|
||||
"affine-0.18.0-stable-macos-arm64.dmg",
|
||||
"affine-0.18.0-stable-macos-arm64.zip",
|
||||
"affine-0.18.0-stable-macos-x64.dmg",
|
||||
"affine-0.18.0-stable-macos-x64.zip",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`testing for client update > filter valid installer files > filter for platform [darwin] arch [x64] 1`] = `
|
||||
[
|
||||
"affine-0.18.0-stable-macos-x64.dmg",
|
||||
"affine-0.18.0-stable-macos-x64.zip",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`testing for client update > filter valid installer files > filter for platform [linux] arch [x64] 1`] = `
|
||||
[
|
||||
"affine-0.18.0-stable-linux-x64.appimage",
|
||||
"affine-0.18.0-stable-linux-x64.deb",
|
||||
"affine-0.18.0-stable-linux-x64.flatpak",
|
||||
"affine-0.18.0-stable-linux-x64.zip",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`testing for client update > filter valid installer files > filter for platform [win32] arch [arm64] 1`] = `
|
||||
[
|
||||
"affine-0.18.0-stable-windows-arm64.nsis.exe",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`testing for client update > filter valid installer files > filter for platform [win32] arch [arm64] and is squirrel installer 1`] = `
|
||||
[
|
||||
"affine-0.18.0-stable-windows-arm64.exe",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`testing for client update > filter valid installer files > filter for platform [win32] arch [x64] 1`] = `
|
||||
[
|
||||
"affine-0.18.0-stable-windows-x64.nsis.exe",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`testing for client update > filter valid installer files > filter for platform [win32] arch [x64] and is squirrel installer 1`] = `
|
||||
[
|
||||
"affine-0.18.0-stable-windows-x64.exe",
|
||||
]
|
||||
`;
|
||||
@@ -45,6 +45,16 @@
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.17.0-canary.7/affine-0.17.0-canary.7-canary-windows-x64.nsis.exe",
|
||||
"size": 133493672
|
||||
},
|
||||
{
|
||||
"name": "affine-0.17.0-canary.7-canary-windows-arm64.exe",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.17.0-canary.7/affine-0.17.0-canary.7-canary-windows-arm64.exe",
|
||||
"size": 182557416
|
||||
},
|
||||
{
|
||||
"name": "affine-0.17.0-canary.7-canary-windows-arm64.nsis.exe",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.17.0-canary.7/affine-0.17.0-canary.7-canary-windows-arm64.nsis.exe",
|
||||
"size": 133493672
|
||||
},
|
||||
{
|
||||
"name": "codecov.yml",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.17.0-canary.7/codecov.yml",
|
||||
|
||||
@@ -1,73 +1,73 @@
|
||||
[
|
||||
{
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/tag/v0.16.3",
|
||||
"name": "0.16.3",
|
||||
"tag_name": "v0.16.3",
|
||||
"published_at": "2024-08-14T07:43:22Z",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/tag/v0.18.0",
|
||||
"name": "0.18.0",
|
||||
"tag_name": "v0.18.0",
|
||||
"published_at": "2024-11-13T07:43:22Z",
|
||||
"assets": [
|
||||
{
|
||||
"name": "affine-0.16.3-stable-linux-x64.appimage",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.16.3/affine-0.16.3-stable-linux-x64.appimage",
|
||||
"name": "affine-0.18.0-stable-linux-x64.appimage",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.18.0/affine-0.18.0-stable-linux-x64.appimage",
|
||||
"size": 178308288
|
||||
},
|
||||
{
|
||||
"name": "affine-0.16.3-stable-linux-x64.zip",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.16.3/affine-0.16.3-stable-linux-x64.zip",
|
||||
"name": "affine-0.18.0-stable-linux-x64.zip",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.18.0/affine-0.18.0-stable-linux-x64.zip",
|
||||
"size": 176405078
|
||||
},
|
||||
{
|
||||
"name": "affine-0.16.3-stable-macos-arm64.dmg",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.16.3/affine-0.16.3-stable-macos-arm64.dmg",
|
||||
"name": "affine-0.18.0-stable-macos-arm64.dmg",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.18.0/affine-0.18.0-stable-macos-arm64.dmg",
|
||||
"size": 168093091
|
||||
},
|
||||
{
|
||||
"name": "affine-0.16.3-stable-macos-arm64.zip",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.16.3/affine-0.16.3-stable-macos-arm64.zip",
|
||||
"name": "affine-0.18.0-stable-macos-arm64.zip",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.18.0/affine-0.18.0-stable-macos-arm64.zip",
|
||||
"size": 167540517
|
||||
},
|
||||
{
|
||||
"name": "affine-0.16.3-stable-macos-x64.dmg",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.16.3/affine-0.16.3-stable-macos-x64.dmg",
|
||||
"name": "affine-0.18.0-stable-macos-x64.dmg",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.18.0/affine-0.18.0-stable-macos-x64.dmg",
|
||||
"size": 175029125
|
||||
},
|
||||
{
|
||||
"name": "affine-0.16.3-stable-macos-x64.zip",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.16.3/affine-0.16.3-stable-macos-x64.zip",
|
||||
"name": "affine-0.18.0-stable-macos-x64.zip",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.18.0/affine-0.18.0-stable-macos-x64.zip",
|
||||
"size": 174752343
|
||||
},
|
||||
{
|
||||
"name": "affine-0.16.3-stable-windows-x64.exe",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.16.3/affine-0.16.3-stable-windows-x64.exe",
|
||||
"name": "affine-0.18.0-stable-windows-x64.exe",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.18.0/affine-0.18.0-stable-windows-x64.exe",
|
||||
"size": 177757416
|
||||
},
|
||||
{
|
||||
"name": "affine-0.16.3-stable-windows-x64.nsis.exe",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.16.3/affine-0.16.3-stable-windows-x64.nsis.exe",
|
||||
"name": "affine-0.18.0-stable-windows-x64.nsis.exe",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.18.0/affine-0.18.0-stable-windows-x64.nsis.exe",
|
||||
"size": 130302976
|
||||
},
|
||||
{
|
||||
"name": "codecov.yml",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.16.3/codecov.yml",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.18.0/codecov.yml",
|
||||
"size": 91
|
||||
},
|
||||
{
|
||||
"name": "latest-linux.yml",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.16.3/latest-linux.yml",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.18.0/latest-linux.yml",
|
||||
"size": 539
|
||||
},
|
||||
{
|
||||
"name": "latest-mac.yml",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.16.3/latest-mac.yml",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.18.0/latest-mac.yml",
|
||||
"size": 865
|
||||
},
|
||||
{
|
||||
"name": "latest.yml",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.16.3/latest.yml",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.18.0/latest.yml",
|
||||
"size": 540
|
||||
},
|
||||
{
|
||||
"name": "web-static.zip",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.16.3/web-static.zip",
|
||||
"url": "https://github.com/toeverything/AFFiNE/releases/download/v0.18.0/web-static.zip",
|
||||
"size": 61989498
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
version: 0.16.3
|
||||
files:
|
||||
- url: affine-0.16.3-stable-linux-x64.appimage
|
||||
sha512: nmID71T7jq9yKCdujVUeL71TLXmwIdaaWZB0ouDX13Np1vahS1+1A5uJbHUzTH0N/sN0W+LKUg9L29wNgi42gw==
|
||||
size: 178308288
|
||||
- url: affine-0.16.3-stable-linux-x64.zip
|
||||
sha512: fsHTT0fUeU/uLGdlRiuddzSuJWIOcaUTgUj7DB5XSQJ4qA5blAcpij8zOil0ww3Ea7Kwe7qcIe4SSCtNFu31sQ==
|
||||
size: 176405078
|
||||
path: affine-0.16.3-stable-linux-x64.appimage
|
||||
sha512: nmID71T7jq9yKCdujVUeL71TLXmwIdaaWZB0ouDX13Np1vahS1+1A5uJbHUzTH0N/sN0W+LKUg9L29wNgi42gw==
|
||||
releaseDate: 2024-08-14T07:11:42.171Z
|
||||
@@ -1,17 +0,0 @@
|
||||
version: 0.16.3
|
||||
files:
|
||||
- url: affine-0.16.3-stable-macos-arm64.dmg
|
||||
sha512: fmJWpi45gVYYUavb0Cd6Y9DR2nxBc3wMagHOiMF1PPg+4tEyHGmVIhRIwY/QaJ5TAR+3tRAENZwen2gvja0UtQ==
|
||||
size: 168093091
|
||||
- url: affine-0.16.3-stable-macos-arm64.zip
|
||||
sha512: u1ud8pJ613A5Oqh3fbcnUUOA4hNoURWBdtAMJoeZ6EIAUvZzV0tsDcAqLiEP89LKbitaH0IdrW3D8EFSsZ9kRw==
|
||||
size: 167540517
|
||||
- url: affine-0.16.3-stable-macos-x64.dmg
|
||||
sha512: Ou1W6/xHyM+ZN9BLYvc+8qCB8wR9F3jLQP5m3oG0uIDDw7wwoR+ny3gcWbDzalfxoOR84CvM74LIfc7BQf69Uw==
|
||||
size: 175029125
|
||||
- url: affine-0.16.3-stable-macos-x64.zip
|
||||
sha512: oot098M9qqdRbw+znnuLjVedZ1U59p4m+gzSxRtpCuYdfvumvu5/RN1jvY2cHssqstJj/Ybh4eBTlREZMgKyyg==
|
||||
size: 174752343
|
||||
path: affine-0.16.3-stable-macos-arm64.dmg
|
||||
sha512: fmJWpi45gVYYUavb0Cd6Y9DR2nxBc3wMagHOiMF1PPg+4tEyHGmVIhRIwY/QaJ5TAR+3tRAENZwen2gvja0UtQ==
|
||||
releaseDate: 2024-08-14T07:11:41.503Z
|
||||
@@ -1,11 +0,0 @@
|
||||
version: 0.16.3
|
||||
files:
|
||||
- url: affine-0.16.3-stable-windows-x64.exe
|
||||
sha512: 47zaLkAhSxPuWsKq01dSEt8GusXqK1rmSaiOTBLe32lmUiXPhUqYO5JhzbrjJKx7/TFcic4UDJ/Zir3wf9fKRA==
|
||||
size: 177757416
|
||||
- url: affine-0.16.3-stable-windows-x64.nsis.exe
|
||||
sha512: G3Rxa3onqlJTGQIcz7Rz6ZQ/6rAwjzjYnW/HB5yzXkjN6e5yfW2JBk765+AyiPFV5Mn4Rloj7V6GM6m4q7WfWg==
|
||||
size: 130302976
|
||||
path: affine-0.16.3-stable-windows-x64.exe
|
||||
sha512: 47zaLkAhSxPuWsKq01dSEt8GusXqK1rmSaiOTBLe32lmUiXPhUqYO5JhzbrjJKx7/TFcic4UDJ/Zir3wf9fKRA==
|
||||
releaseDate: 2024-08-14T07:11:40.285Z
|
||||
@@ -3,9 +3,13 @@ files:
|
||||
- url: affine-0.17.0-canary.7-canary-linux-x64.appimage
|
||||
sha512: qspZkDlItrHu02vSItbjc3I+t4FcOiHOzGt0Ap6IeZEFKal+hoOh4WIcUN16dlS/OoFm+is8yPBHqN/70xhWKA==
|
||||
size: 181990592
|
||||
- url: affine-0.17.0-canary.7-canary-linux-x64.deb
|
||||
sha512: fom2iuMiPUlnHAGJhQdAnWJwMggK4rloNkiWqH8ZHF1Q09oturgSMGgkUEWZWXsZPpORt545eYNv5Zg9aff8yQ==
|
||||
size: 180105256
|
||||
- url: affine-0.17.0-canary.7-canary-linux-x64.flatpak
|
||||
sha512: fom2iuMiPUlnHAGJhQdAnWJwMggK4rloNkiWqH8ZHF1Q09oturgSMGgkUEWZWXsZPpORt545eYNv5Zg9aff8yQ==
|
||||
size: 180105256
|
||||
- url: affine-0.17.0-canary.7-canary-linux-x64.zip
|
||||
sha512: fom2iuMiPUlnHAGJhQdAnWJwMggK4rloNkiWqH8ZHF1Q09oturgSMGgkUEWZWXsZPpORt545eYNv5Zg9aff8yQ==
|
||||
size: 180105256
|
||||
path: affine-0.17.0-canary.7-canary-linux-x64.appimage
|
||||
sha512: qspZkDlItrHu02vSItbjc3I+t4FcOiHOzGt0Ap6IeZEFKal+hoOh4WIcUN16dlS/OoFm+is8yPBHqN/70xhWKA==
|
||||
releaseDate: 2024-08-29T08:20:53.453Z
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user