mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-06 17:43:51 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd676611ce | ||
|
|
f3bb2be5ef | ||
|
|
8535b3dc41 | ||
|
|
89cc9b072b | ||
|
|
e4b5b24fdd | ||
|
|
9904f50e0b | ||
|
|
b7ac7caab4 | ||
|
|
d74087fdc5 | ||
|
|
875565d08a | ||
|
|
0ecd915245 | ||
|
|
b5ebd20314 | ||
|
|
c102e2454f | ||
|
|
5fc3258a3d | ||
|
|
1a9863d36f | ||
|
|
35c2ad262f | ||
|
|
a0613b6306 | ||
|
|
c18840038f | ||
|
|
e2de0e0e3d | ||
|
|
6fb0ff9177 | ||
|
|
c2fb6adfd8 | ||
|
|
8aeb8bd0ca | ||
|
|
a47042cbd5 | ||
|
|
2c44d3abc6 | ||
|
|
01c164a78a |
@@ -2,6 +2,8 @@ version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
image: mcr.microsoft.com/devcontainers/base:bookworm
|
||||
volumes:
|
||||
- ../..:/workspaces:cached
|
||||
|
||||
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.22.4"
|
||||
appVersion: "0.25.2"
|
||||
|
||||
2
.github/helm/affine/charts/doc/Chart.yaml
vendored
2
.github/helm/affine/charts/doc/Chart.yaml
vendored
@@ -3,7 +3,7 @@ name: doc
|
||||
description: AFFiNE doc server
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.22.4"
|
||||
appVersion: "0.25.2"
|
||||
dependencies:
|
||||
- name: gcloud-sql-proxy
|
||||
version: 0.0.0
|
||||
|
||||
@@ -3,7 +3,7 @@ name: graphql
|
||||
description: AFFiNE GraphQL server
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.22.4"
|
||||
appVersion: "0.25.2"
|
||||
dependencies:
|
||||
- name: gcloud-sql-proxy
|
||||
version: 0.0.0
|
||||
|
||||
@@ -3,7 +3,7 @@ name: renderer
|
||||
description: AFFiNE renderer server
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.22.4"
|
||||
appVersion: "0.25.2"
|
||||
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.22.4"
|
||||
appVersion: "0.25.2"
|
||||
dependencies:
|
||||
- name: gcloud-sql-proxy
|
||||
version: 0.0.0
|
||||
|
||||
42
Cargo.lock
generated
42
Cargo.lock
generated
@@ -163,6 +163,7 @@ dependencies = [
|
||||
"file-format",
|
||||
"infer",
|
||||
"mimalloc",
|
||||
"mp4parse",
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
@@ -574,6 +575,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitreader"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "886559b1e163d56c765bc3a985febb4eee8009f625244511d8ee3c432e08c066"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
version = "1.0.1"
|
||||
@@ -1483,6 +1493,15 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
|
||||
|
||||
[[package]]
|
||||
name = "fallible_collections"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a88c69768c0a15262df21899142bc6df9b9b823546d4b4b9a7bc2d6c448ec6fd"
|
||||
dependencies = [
|
||||
"hashbrown 0.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.13.0"
|
||||
@@ -1801,6 +1820,15 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
@@ -2494,6 +2522,20 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mp4parse"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63a35203d3c6ce92d5251c77520acb2e57108c88728695aa883f70023624c570"
|
||||
dependencies = [
|
||||
"bitreader",
|
||||
"byteorder",
|
||||
"fallible_collections",
|
||||
"log",
|
||||
"num-traits",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nanoid"
|
||||
version = "0.4.0"
|
||||
|
||||
@@ -48,6 +48,7 @@ libc = "0.2"
|
||||
log = "0.4"
|
||||
loom = { version = "0.7", features = ["checkpoint"] }
|
||||
mimalloc = "0.1"
|
||||
mp4parse = "0.17"
|
||||
nanoid = "0.4"
|
||||
napi = { version = "3.0.0-beta.3", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
||||
napi-build = { version = "2" }
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<br>
|
||||
</h1>
|
||||
<a href="https://affine.pro/download">
|
||||
<img alt="affine logo" src="https://cdn.affine.pro/Github_hero_image1.png" style="width: 100%">
|
||||
<img alt="affine logo" src="https://cdn.affine.pro/Github_hero_image2.png" style="width: 100%">
|
||||
</a>
|
||||
<br/>
|
||||
<p align="center">
|
||||
|
||||
@@ -296,7 +296,7 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4",
|
||||
"version": "0.25.2",
|
||||
"devDependencies": {
|
||||
"@vanilla-extract/vite-plugin": "^5.0.0",
|
||||
"msw": "^2.8.4",
|
||||
|
||||
@@ -41,5 +41,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -45,5 +45,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -45,5 +45,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -48,5 +48,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -42,5 +42,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -48,5 +48,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -39,5 +39,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -43,5 +43,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -49,5 +49,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -323,7 +323,8 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
|
||||
private readonly _renderEmbedView = () => {
|
||||
const linkedDoc = this.linkedDoc;
|
||||
const isDeleted = !linkedDoc;
|
||||
const trash = linkedDoc?.meta?.trash;
|
||||
const isDeleted = trash || !linkedDoc;
|
||||
const isLoading = this._loading;
|
||||
const isError = this.isError;
|
||||
const isEmpty = this._isDocEmpty() && this.isBannerEmpty;
|
||||
@@ -521,11 +522,6 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
);
|
||||
|
||||
this._setDocUpdatedAt();
|
||||
this.disposables.add(
|
||||
this.store.workspace.slots.docListUpdated.subscribe(() => {
|
||||
this._setDocUpdatedAt();
|
||||
})
|
||||
);
|
||||
|
||||
if (this._referenceToNode) {
|
||||
this._linkedDocMode = this.model.props.params?.mode ?? 'page';
|
||||
@@ -554,6 +550,13 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.add(
|
||||
this.store.workspace.slots.docListUpdated.subscribe(() => {
|
||||
this._setDocUpdatedAt();
|
||||
this.refreshData();
|
||||
})
|
||||
);
|
||||
|
||||
this._trackCitationDeleteEvent();
|
||||
}
|
||||
|
||||
|
||||
@@ -357,10 +357,14 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
|
||||
};
|
||||
|
||||
refreshData = () => {
|
||||
this._load().catch(e => {
|
||||
console.error(e);
|
||||
this._error = true;
|
||||
});
|
||||
this._load()
|
||||
.then(() => {
|
||||
this._isEmptySyncedDoc = isEmptyDoc(this.syncedDoc, this.editorMode);
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
this._error = true;
|
||||
});
|
||||
};
|
||||
|
||||
title$ = computed(() => {
|
||||
@@ -445,7 +449,8 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
|
||||
this._cycle = false;
|
||||
|
||||
const syncedDoc = this.syncedDoc;
|
||||
if (!syncedDoc) {
|
||||
const trash = syncedDoc?.meta?.trash;
|
||||
if (trash || !syncedDoc) {
|
||||
this._deleted = true;
|
||||
this._loading = false;
|
||||
return;
|
||||
@@ -521,6 +526,7 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
|
||||
this.disposables.add(
|
||||
this.store.workspace.slots.docListUpdated.subscribe(() => {
|
||||
this._setDocUpdatedAt();
|
||||
this.refreshData();
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -49,5 +49,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -44,5 +44,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -44,5 +44,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -46,5 +46,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -46,5 +46,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -49,5 +49,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -42,5 +42,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -67,5 +67,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -45,5 +45,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -46,5 +46,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -42,5 +42,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -82,5 +82,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -46,5 +46,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -26,5 +26,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -42,5 +42,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -35,5 +35,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -40,5 +40,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -42,5 +42,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -41,5 +41,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -43,5 +43,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -44,5 +44,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -44,5 +44,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -45,5 +45,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -51,5 +51,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -45,5 +45,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -42,5 +42,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -44,5 +44,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -44,5 +44,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -43,5 +43,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -25,5 +25,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -42,5 +42,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.21.0"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -47,5 +47,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -50,5 +50,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -44,5 +44,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -42,5 +42,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -56,5 +56,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -43,5 +43,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -30,5 +30,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -41,5 +41,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -75,5 +75,5 @@
|
||||
"devDependencies": {
|
||||
"vitest": "3.1.3"
|
||||
},
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -45,5 +45,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -34,5 +34,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -36,5 +36,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -40,5 +40,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -38,5 +38,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -36,5 +36,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -34,5 +34,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -41,5 +41,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export class ImportDoc extends WithDisposable(LitElement) {
|
||||
);
|
||||
return;
|
||||
}
|
||||
this._onImportSuccess([entryId], {
|
||||
this._onImportSuccess(entryId ? [entryId] : [], {
|
||||
isWorkspaceFile,
|
||||
importedCount: pageIds.length,
|
||||
});
|
||||
|
||||
@@ -21,6 +21,28 @@ type ImportNotionZipOptions = {
|
||||
extensions: ExtensionType[];
|
||||
};
|
||||
|
||||
type PageIcon = {
|
||||
type: 'emoji' | 'image';
|
||||
content: string; // emoji unicode or image URL/data
|
||||
};
|
||||
|
||||
type FolderHierarchy = {
|
||||
name: string;
|
||||
path: string;
|
||||
children: Map<string, FolderHierarchy>;
|
||||
pageId?: string;
|
||||
parentPath?: string;
|
||||
icon?: PageIcon;
|
||||
};
|
||||
|
||||
type ImportNotionZipResult = {
|
||||
entryId: string | undefined;
|
||||
pageIds: string[];
|
||||
isWorkspaceFile: boolean;
|
||||
hasMarkdown: boolean;
|
||||
folderHierarchy?: FolderHierarchy;
|
||||
};
|
||||
|
||||
function getProvider(extensions: ExtensionType[]) {
|
||||
const container = new Container();
|
||||
extensions.forEach(ext => {
|
||||
@@ -29,6 +51,197 @@ function getProvider(extensions: ExtensionType[]) {
|
||||
return container.provider();
|
||||
}
|
||||
|
||||
function parseFolderPath(filePath: string): {
|
||||
folderParts: string[];
|
||||
fileName: string;
|
||||
} {
|
||||
const parts = filePath.split('/');
|
||||
const fileName = parts.pop() || '';
|
||||
return { folderParts: parts.filter(part => part.length > 0), fileName };
|
||||
}
|
||||
|
||||
function extractPageIcon(doc: Document): PageIcon | undefined {
|
||||
// Look for Notion page icon in the HTML
|
||||
// Notion export format: <div class="page-header-icon undefined"><span class="icon">✅</span></div>
|
||||
|
||||
console.log('=== Extracting page icon ===');
|
||||
|
||||
// Check if there's a head section with title for debugging
|
||||
const headTitle = doc.querySelector('head title');
|
||||
if (headTitle) {
|
||||
console.log('Page title from head:', headTitle.textContent);
|
||||
}
|
||||
|
||||
// Look for the exact Notion export structure: .page-header-icon .icon
|
||||
const notionIconSpan = doc.querySelector('.page-header-icon .icon');
|
||||
if (notionIconSpan && notionIconSpan.textContent) {
|
||||
const iconContent = notionIconSpan.textContent.trim();
|
||||
console.log('Found Notion icon (.page-header-icon .icon):', iconContent);
|
||||
if (/\p{Emoji}/u.test(iconContent)) {
|
||||
return {
|
||||
type: 'emoji',
|
||||
content: iconContent,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Look for page header area for debugging
|
||||
const pageHeader = doc.querySelector('.page-header-icon');
|
||||
if (pageHeader) {
|
||||
console.log(
|
||||
'Found .page-header-icon:',
|
||||
pageHeader.outerHTML.substring(0, 300) + '...'
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback: try to find emoji icons with older selectors
|
||||
const emojiIcon = doc.querySelector('.page-header-icon .notion-emoji');
|
||||
if (emojiIcon && emojiIcon.textContent) {
|
||||
console.log(
|
||||
'Found emoji icon (.page-header-icon .notion-emoji):',
|
||||
emojiIcon.textContent
|
||||
);
|
||||
return {
|
||||
type: 'emoji',
|
||||
content: emojiIcon.textContent.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
// Try alternative emoji selectors
|
||||
const altEmojiIcon = doc.querySelector('[role="img"][aria-label]');
|
||||
if (
|
||||
altEmojiIcon &&
|
||||
altEmojiIcon.textContent &&
|
||||
/\p{Emoji}/u.test(altEmojiIcon.textContent)
|
||||
) {
|
||||
console.log(
|
||||
'Found emoji icon ([role="img"][aria-label]):',
|
||||
altEmojiIcon.textContent
|
||||
);
|
||||
return {
|
||||
type: 'emoji',
|
||||
content: altEmojiIcon.textContent.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
// Look for image icons in the page header
|
||||
const imageIcon = doc.querySelector('.page-header-icon img');
|
||||
if (imageIcon) {
|
||||
const src = imageIcon.getAttribute('src');
|
||||
console.log('Found image icon (.page-header-icon img):', src);
|
||||
if (src) {
|
||||
return {
|
||||
type: 'image',
|
||||
content: src,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Look for any span with emoji class "icon" in page header area
|
||||
const iconSpans = doc.querySelectorAll('span.icon');
|
||||
for (const span of iconSpans) {
|
||||
if (span.textContent && /\p{Emoji}/u.test(span.textContent.trim())) {
|
||||
const parent = span.parentElement;
|
||||
console.log(
|
||||
'Found emoji in span.icon:',
|
||||
span.textContent,
|
||||
'parent classes:',
|
||||
parent?.className
|
||||
);
|
||||
// Check if this is in a page header context
|
||||
if (
|
||||
parent &&
|
||||
(parent.classList.contains('page-header-icon') ||
|
||||
parent.closest('.page-header-icon'))
|
||||
) {
|
||||
console.log(
|
||||
'Using emoji from span.icon in page header:',
|
||||
span.textContent
|
||||
);
|
||||
return {
|
||||
type: 'emoji',
|
||||
content: span.textContent.trim(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Try to find icons in the page title area that might contain emoji
|
||||
const pageTitle = doc.querySelector('.page-title, h1');
|
||||
if (pageTitle && pageTitle.textContent) {
|
||||
console.log('Page title element found:', pageTitle.textContent);
|
||||
const text = pageTitle.textContent.trim();
|
||||
// Check if the title starts with an emoji
|
||||
const emojiMatch = text.match(/^(\p{Emoji}+)/u);
|
||||
if (emojiMatch) {
|
||||
console.log('Found emoji in title:', emojiMatch[1]);
|
||||
return {
|
||||
type: 'emoji',
|
||||
content: emojiMatch[1],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
console.log('No page icon found');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildFolderHierarchy(
|
||||
pagePaths: Array<{ path: string; pageId: string; icon?: PageIcon }>
|
||||
): FolderHierarchy {
|
||||
const root: FolderHierarchy = {
|
||||
name: '',
|
||||
path: '',
|
||||
children: new Map(),
|
||||
};
|
||||
|
||||
for (const { path, pageId, icon } of pagePaths) {
|
||||
const { folderParts, fileName } = parseFolderPath(path);
|
||||
let current = root;
|
||||
let currentPath = '';
|
||||
|
||||
// Navigate/create folder structure
|
||||
for (const folderName of folderParts) {
|
||||
const parentPath = currentPath;
|
||||
currentPath = currentPath ? `${currentPath}/${folderName}` : folderName;
|
||||
|
||||
if (!current.children.has(folderName)) {
|
||||
current.children.set(folderName, {
|
||||
name: folderName,
|
||||
path: currentPath,
|
||||
parentPath: parentPath || undefined,
|
||||
children: new Map(),
|
||||
});
|
||||
}
|
||||
current = current.children.get(folderName)!;
|
||||
}
|
||||
|
||||
// If this is a page file, associate it with the current folder
|
||||
if (fileName.endsWith('.html') && !fileName.startsWith('index.html')) {
|
||||
const pageName = fileName.replace(/\.html$/, '');
|
||||
if (!current.children.has(pageName)) {
|
||||
current.children.set(pageName, {
|
||||
name: pageName,
|
||||
path: path,
|
||||
parentPath: current.path || undefined,
|
||||
children: new Map(),
|
||||
pageId: pageId,
|
||||
icon: icon,
|
||||
});
|
||||
} else {
|
||||
// Update existing entry with pageId and icon
|
||||
const existingPage = current.children.get(pageName)!;
|
||||
existingPage.pageId = pageId;
|
||||
if (icon) {
|
||||
existingPage.icon = icon;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a Notion zip file into the BlockSuite collection.
|
||||
*
|
||||
@@ -42,18 +255,24 @@ function getProvider(extensions: ExtensionType[]) {
|
||||
* - pageIds: An array of imported page IDs.
|
||||
* - isWorkspaceFile: Whether the imported file is a workspace file.
|
||||
* - hasMarkdown: Whether the zip contains markdown files.
|
||||
* - folderHierarchy: The parsed folder hierarchy from the Notion export.
|
||||
*/
|
||||
async function importNotionZip({
|
||||
collection,
|
||||
schema,
|
||||
imported,
|
||||
extensions,
|
||||
}: ImportNotionZipOptions) {
|
||||
}: ImportNotionZipOptions): Promise<ImportNotionZipResult> {
|
||||
const provider = getProvider(extensions);
|
||||
const pageIds: string[] = [];
|
||||
let isWorkspaceFile = false;
|
||||
let hasMarkdown = false;
|
||||
let entryId: string | undefined;
|
||||
const pagePathsWithIds: Array<{
|
||||
path: string;
|
||||
pageId: string;
|
||||
icon?: PageIcon;
|
||||
}> = [];
|
||||
const parseZipFile = async (path: File | Blob) => {
|
||||
const unzip = new Unzip();
|
||||
await unzip.load(path);
|
||||
@@ -80,6 +299,8 @@ async function importNotionZip({
|
||||
isWorkspaceFile = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
let pageIcon: PageIcon | undefined;
|
||||
if (lastSplitIndex !== -1) {
|
||||
const text = await content.text();
|
||||
const doc = new DOMParser().parseFromString(text, 'text/html');
|
||||
@@ -88,7 +309,10 @@ async function importNotionZip({
|
||||
// Skip empty pages
|
||||
continue;
|
||||
}
|
||||
// Extract page icon from the HTML
|
||||
pageIcon = extractPageIcon(doc);
|
||||
}
|
||||
|
||||
const id = collection.idGenerator();
|
||||
const splitPath = path.split('/');
|
||||
while (splitPath.length > 0) {
|
||||
@@ -96,6 +320,7 @@ async function importNotionZip({
|
||||
splitPath.shift();
|
||||
}
|
||||
pagePaths.push(path);
|
||||
pagePathsWithIds.push({ path, pageId: id, icon: pageIcon });
|
||||
if (entryId === undefined && lastSplitIndex === -1) {
|
||||
entryId = id;
|
||||
}
|
||||
@@ -166,7 +391,14 @@ async function importNotionZip({
|
||||
const allPromises = await parseZipFile(imported);
|
||||
await Promise.all(allPromises.flat());
|
||||
entryId = entryId ?? pageIds[0];
|
||||
return { entryId, pageIds, isWorkspaceFile, hasMarkdown };
|
||||
|
||||
// Build folder hierarchy from collected paths
|
||||
const folderHierarchy =
|
||||
pagePathsWithIds.length > 0
|
||||
? buildFolderHierarchy(pagePathsWithIds)
|
||||
: undefined;
|
||||
|
||||
return { entryId, pageIds, isWorkspaceFile, hasMarkdown, folderHierarchy };
|
||||
}
|
||||
|
||||
export const NotionHtmlTransformer = {
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -35,5 +35,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -30,5 +30,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -36,5 +36,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -38,5 +38,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -35,5 +35,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -17,5 +17,5 @@
|
||||
"dependencies": {
|
||||
"@blocksuite/affine": "workspace:*"
|
||||
},
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -64,5 +64,5 @@
|
||||
"devDependencies": {
|
||||
"vitest": "3.1.3"
|
||||
},
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -47,5 +47,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ export class Clipboard extends LifeCycleWatcher {
|
||||
const byPriority = Array.from(this._adapters).sort(
|
||||
(a, b) => b.priority - a.priority
|
||||
);
|
||||
|
||||
for (const { adapter, mimeType } of byPriority) {
|
||||
const item = getItem(mimeType);
|
||||
if (Array.isArray(item)) {
|
||||
@@ -170,7 +171,9 @@ export class Clipboard extends LifeCycleWatcher {
|
||||
index?: number
|
||||
) => {
|
||||
const data = event.clipboardData;
|
||||
if (!data) return;
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const json = this.readFromClipboard(data);
|
||||
@@ -187,7 +190,7 @@ export class Clipboard extends LifeCycleWatcher {
|
||||
);
|
||||
}
|
||||
return slice;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
const getDataByType = this._getDataByType(data);
|
||||
const slice = await this._getSnapshotByPriority(
|
||||
type => getDataByType(type),
|
||||
@@ -195,7 +198,6 @@ export class Clipboard extends LifeCycleWatcher {
|
||||
parent,
|
||||
index
|
||||
);
|
||||
|
||||
return slice;
|
||||
}
|
||||
};
|
||||
@@ -292,9 +294,7 @@ export class Clipboard extends LifeCycleWatcher {
|
||||
|
||||
if (image) {
|
||||
const type = 'image/png';
|
||||
|
||||
delete items[type];
|
||||
|
||||
if (typeof image === 'string') {
|
||||
clipboardItems[type] = new Blob([image], { type });
|
||||
} else if (image instanceof Blob) {
|
||||
@@ -314,7 +314,7 @@ export class Clipboard extends LifeCycleWatcher {
|
||||
if (hasInnerHTML || isEmpty) {
|
||||
const type = 'text/html';
|
||||
const snapshot = lz.compressToEncodedURIComponent(JSON.stringify(items));
|
||||
const html = `<div data-blocksuite-snapshot='${snapshot}'>${innerHTML}</div>`;
|
||||
const html = `<div data-blocksuite-snapshot="${snapshot}">${innerHTML}</div>`;
|
||||
clipboardItems[type] = new Blob([html], { type });
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ export class ClipboardControl {
|
||||
const clipboardEventState = new ClipboardEventState({
|
||||
event,
|
||||
});
|
||||
|
||||
this._dispatcher.run(
|
||||
'paste',
|
||||
this._createContext(event, clipboardEventState)
|
||||
|
||||
@@ -42,5 +42,5 @@
|
||||
"!dist/__tests__",
|
||||
"shim.d.ts"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface DocMeta {
|
||||
createDate: number;
|
||||
updatedDate?: number;
|
||||
favorite?: boolean;
|
||||
trash?: boolean;
|
||||
}
|
||||
|
||||
export interface WorkspaceMeta {
|
||||
|
||||
@@ -33,5 +33,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -41,10 +41,10 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@vanilla-extract/vite-plugin": "^5.0.0",
|
||||
"vite": "^6.1.0",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-istanbul": "^7.0.0",
|
||||
"vite-plugin-wasm": "^3.4.1",
|
||||
"vitest": "3.1.3"
|
||||
},
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -41,10 +41,10 @@
|
||||
"@vanilla-extract/vite-plugin": "^5.0.0",
|
||||
"graphql": "^16.9.0",
|
||||
"magic-string": "^0.30.11",
|
||||
"vite": "^6.0.3",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-istanbul": "^7.0.0",
|
||||
"vite-plugin-wasm": "^3.3.0",
|
||||
"vite-plugin-web-components-hmr": "^0.1.3"
|
||||
},
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
],
|
||||
"ext": "ts,md,json"
|
||||
},
|
||||
"version": "0.22.4"
|
||||
"version": "0.25.2"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/monorepo",
|
||||
"version": "0.22.4",
|
||||
"version": "0.25.2",
|
||||
"private": true,
|
||||
"author": "toeverything",
|
||||
"license": "MIT",
|
||||
@@ -89,7 +89,7 @@
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.18.0",
|
||||
"unplugin-swc": "^1.5.1",
|
||||
"vite": "^6.0.3",
|
||||
"vite": "^7.0.0",
|
||||
"vitest": "3.1.3"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1",
|
||||
|
||||
@@ -11,6 +11,7 @@ affine_common = { workspace = true, features = ["doc-loader"] }
|
||||
chrono = { workspace = true }
|
||||
file-format = { workspace = true }
|
||||
infer = { workspace = true }
|
||||
mp4parse = { workspace = true }
|
||||
napi = { workspace = true, features = ["async"] }
|
||||
napi-derive = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/server-native",
|
||||
"version": "0.22.4",
|
||||
"version": "0.25.2",
|
||||
"engines": {
|
||||
"node": ">= 10.16.0 < 11 || >= 11.8.0"
|
||||
},
|
||||
|
||||
@@ -1,12 +1,41 @@
|
||||
use mp4parse::{read_mp4, TrackType};
|
||||
use napi_derive::napi;
|
||||
|
||||
#[napi]
|
||||
pub fn get_mime(input: &[u8]) -> String {
|
||||
if let Some(kind) = infer::get(&input[..4096.min(input.len())]) {
|
||||
let mimetype = if let Some(kind) = infer::get(&input[..4096.min(input.len())]) {
|
||||
kind.mime_type().to_string()
|
||||
} else {
|
||||
file_format::FileFormat::from_bytes(input)
|
||||
.media_type()
|
||||
.to_string()
|
||||
};
|
||||
if mimetype == "video/mp4" {
|
||||
detect_mp4_flavor(input)
|
||||
} else {
|
||||
mimetype
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_mp4_flavor(input: &[u8]) -> String {
|
||||
let mut cursor = std::io::Cursor::new(input);
|
||||
match read_mp4(&mut cursor) {
|
||||
Ok(ctx) => {
|
||||
let mut has_video = false;
|
||||
let mut has_audio = false;
|
||||
for track in ctx.tracks.iter() {
|
||||
match track.track_type {
|
||||
TrackType::Video | TrackType::AuxiliaryVideo | TrackType::Picture => has_video = true,
|
||||
TrackType::Audio => has_audio = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if !has_video && has_audio {
|
||||
"audio/m4a".to_string()
|
||||
} else {
|
||||
"video/mp4".to_string()
|
||||
}
|
||||
}
|
||||
Err(_) => "video/mp4".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/server",
|
||||
"private": true,
|
||||
"version": "0.22.4",
|
||||
"version": "0.25.2",
|
||||
"description": "Affine Node.js server",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -28,12 +28,12 @@
|
||||
"dependencies": {
|
||||
"@affine/reader": "workspace:*",
|
||||
"@affine/server-native": "workspace:*",
|
||||
"@ai-sdk/anthropic": "^2.0.1",
|
||||
"@ai-sdk/google": "^2.0.4",
|
||||
"@ai-sdk/google-vertex": "^3.0.5",
|
||||
"@ai-sdk/openai": "^2.0.10",
|
||||
"@ai-sdk/openai-compatible": "^1.0.5",
|
||||
"@ai-sdk/perplexity": "^2.0.1",
|
||||
"@ai-sdk/anthropic": "^2.0.38",
|
||||
"@ai-sdk/google": "^2.0.24",
|
||||
"@ai-sdk/google-vertex": "^3.0.54",
|
||||
"@ai-sdk/openai": "^2.0.56",
|
||||
"@ai-sdk/openai-compatible": "^1.0.23",
|
||||
"@ai-sdk/perplexity": "^2.0.14",
|
||||
"@apollo/server": "^4.11.3",
|
||||
"@aws-sdk/client-s3": "^3.779.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.779.0",
|
||||
@@ -56,26 +56,26 @@
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
"@node-rs/crc32": "^1.10.6",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/core": "^1.29.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.57.0",
|
||||
"@opentelemetry/exporter-zipkin": "^1.29.0",
|
||||
"@opentelemetry/host-metrics": "^0.35.4",
|
||||
"@opentelemetry/instrumentation": "^0.57.0",
|
||||
"@opentelemetry/instrumentation-graphql": "^0.47.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.57.0",
|
||||
"@opentelemetry/instrumentation-ioredis": "^0.47.0",
|
||||
"@opentelemetry/instrumentation-nestjs-core": "^0.44.0",
|
||||
"@opentelemetry/instrumentation-socket.io": "^0.46.0",
|
||||
"@opentelemetry/resources": "^1.29.0",
|
||||
"@opentelemetry/sdk-metrics": "^1.29.0",
|
||||
"@opentelemetry/sdk-node": "^0.57.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.29.0",
|
||||
"@opentelemetry/core": "^1.30.1",
|
||||
"@opentelemetry/exporter-prometheus": "^0.57.2",
|
||||
"@opentelemetry/exporter-zipkin": "^1.30.1",
|
||||
"@opentelemetry/host-metrics": "^0.36.0",
|
||||
"@opentelemetry/instrumentation": "^0.57.2",
|
||||
"@opentelemetry/instrumentation-graphql": "^0.55.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.57.2",
|
||||
"@opentelemetry/instrumentation-ioredis": "^0.55.0",
|
||||
"@opentelemetry/instrumentation-nestjs-core": "^0.54.0",
|
||||
"@opentelemetry/instrumentation-socket.io": "^0.54.0",
|
||||
"@opentelemetry/resources": "^1.30.1",
|
||||
"@opentelemetry/sdk-metrics": "^1.30.1",
|
||||
"@opentelemetry/sdk-node": "^0.57.2",
|
||||
"@opentelemetry/sdk-trace-node": "^1.30.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.28.0",
|
||||
"@prisma/client": "^6.6.0",
|
||||
"@prisma/instrumentation": "^6.7.0",
|
||||
"@react-email/components": "0.0.38",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"ai": "^5.0.10",
|
||||
"ai": "^5.0.81",
|
||||
"bullmq": "^5.40.2",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cross-env": "^7.0.3",
|
||||
|
||||
@@ -42,6 +42,7 @@ type Ctx = {
|
||||
controller: RevenueCatWebhookController;
|
||||
subResolver: UserSubscriptionResolver;
|
||||
|
||||
mockAlias: (appUserId: string) => Sinon.SinonStub;
|
||||
mockSub: (subs: Subscription[]) => Sinon.SinonStub;
|
||||
mockSubSeq: (sequences: Subscription[][]) => Sinon.SinonStub;
|
||||
triggerWebhook: (
|
||||
@@ -100,13 +101,20 @@ test.beforeEach(async t => {
|
||||
t.context.controller = controller;
|
||||
t.context.subResolver = subResolver;
|
||||
|
||||
t.context.mockSub = subs => Sinon.stub(rc, 'getSubscriptions').resolves(subs);
|
||||
const customerId = 'cust';
|
||||
t.context.mockAlias = appUserId =>
|
||||
Sinon.stub(rc, 'getCustomerAlias').resolves([appUserId]);
|
||||
t.context.mockSub = subs =>
|
||||
Sinon.stub(rc, 'getSubscriptions').resolves(
|
||||
subs.map(s => ({ ...s, customerId: customerId }))
|
||||
);
|
||||
t.context.mockSubSeq = sequences => {
|
||||
const stub = Sinon.stub(rc, 'getSubscriptions');
|
||||
sequences.forEach((seq, idx) => {
|
||||
if (idx === 0) stub.onFirstCall().resolves(seq);
|
||||
else if (idx === 1) stub.onSecondCall().resolves(seq);
|
||||
else stub.onCall(idx).resolves(seq);
|
||||
const subs = seq.map(s => ({ ...s, customerId: customerId }));
|
||||
if (idx === 0) stub.onFirstCall().resolves(subs);
|
||||
else if (idx === 1) stub.onSecondCall().resolves(subs);
|
||||
else stub.onCall(idx).resolves(subs);
|
||||
});
|
||||
return stub;
|
||||
};
|
||||
@@ -178,8 +186,9 @@ test('should resolve product mapping consistently (whitelist, override, unknown)
|
||||
});
|
||||
|
||||
test('should standardize RC subscriber response and upsert subscription with observability fields', async t => {
|
||||
const { webhook, collectEvents, mockSub } = t.context;
|
||||
const { webhook, collectEvents, mockAlias, mockSub } = t.context;
|
||||
|
||||
mockAlias(user.id);
|
||||
const subscriber = mockSub([
|
||||
{
|
||||
identifier: 'Pro',
|
||||
@@ -234,8 +243,9 @@ test('should standardize RC subscriber response and upsert subscription with obs
|
||||
});
|
||||
|
||||
test('should process expiration/refund by deleting subscription and emitting canceled', async t => {
|
||||
const { db, collectEvents, mockSub, triggerWebhook } = t.context;
|
||||
const { db, collectEvents, mockAlias, mockSub, triggerWebhook } = t.context;
|
||||
|
||||
mockAlias(user.id);
|
||||
await db.subscription.create({
|
||||
data: {
|
||||
targetId: user.id,
|
||||
@@ -339,8 +349,10 @@ test('should enqueue per-user reconciliation jobs for existing RC active/trialin
|
||||
});
|
||||
|
||||
test('should activate subscriptions via webhook for whitelisted products across stores (iOS/Android)', async t => {
|
||||
const { db, event, collectEvents, mockSubSeq, triggerWebhook } = t.context;
|
||||
const { db, event, collectEvents, mockAlias, mockSubSeq, triggerWebhook } =
|
||||
t.context;
|
||||
|
||||
mockAlias(user.id);
|
||||
const scenarios = [
|
||||
{
|
||||
name: 'Pro monthly on iOS',
|
||||
@@ -422,7 +434,9 @@ test('should activate subscriptions via webhook for whitelisted products across
|
||||
});
|
||||
|
||||
test('should keep active and advance period dates when a trialing subscription renews', async t => {
|
||||
const { db, collectEvents, mockSubSeq, triggerWebhook } = t.context;
|
||||
const { db, collectEvents, mockAlias, mockSubSeq, triggerWebhook } =
|
||||
t.context;
|
||||
mockAlias(user.id);
|
||||
mockSubSeq([
|
||||
[
|
||||
{
|
||||
@@ -476,7 +490,9 @@ test('should keep active and advance period dates when a trialing subscription r
|
||||
});
|
||||
|
||||
test('should remove or cancel the record and revoke entitlement when a trialing subscription expires', async t => {
|
||||
const { db, collectEvents, mockSubSeq, triggerWebhook } = t.context;
|
||||
const { db, collectEvents, mockAlias, mockSubSeq, triggerWebhook } =
|
||||
t.context;
|
||||
mockAlias(user.id);
|
||||
mockSubSeq([
|
||||
[
|
||||
{
|
||||
@@ -497,7 +513,7 @@ test('should remove or cancel the record and revoke entitlement when a trialing
|
||||
isTrial: false,
|
||||
isActive: false,
|
||||
latestPurchaseDate: new Date('2025-04-01T00:00:00.000Z'),
|
||||
expirationDate: new Date('2024-01-01T00:00:00.000Z'),
|
||||
expirationDate: new Date('2025-04-08T00:00:00.000Z'),
|
||||
productId: 'app.affine.pro.Annual',
|
||||
store: 'app_store',
|
||||
willRenew: false,
|
||||
@@ -526,7 +542,8 @@ test('should remove or cancel the record and revoke entitlement when a trialing
|
||||
});
|
||||
|
||||
test('should set canceledAt and keep active until expiration when will_renew is false (cancellation before period end)', async t => {
|
||||
const { db, collectEvents, mockSub, triggerWebhook } = t.context;
|
||||
const { db, collectEvents, mockAlias, mockSub, triggerWebhook } = t.context;
|
||||
mockAlias(user.id);
|
||||
mockSub([
|
||||
{
|
||||
identifier: 'Pro',
|
||||
@@ -563,7 +580,8 @@ test('should set canceledAt and keep active until expiration when will_renew is
|
||||
});
|
||||
|
||||
test('should retain record as past_due (inactive but not expired) and NOT emit canceled event', async t => {
|
||||
const { db, collectEvents, mockSub, triggerWebhook } = t.context;
|
||||
const { db, collectEvents, mockAlias, mockSub, triggerWebhook } = t.context;
|
||||
mockAlias(user.id);
|
||||
mockSub([
|
||||
{
|
||||
identifier: 'Pro',
|
||||
@@ -656,7 +674,8 @@ test('should block checkout when an existing subscription of the same plan is ac
|
||||
});
|
||||
|
||||
test('should skip RC upsert when Stripe active already exists for same plan', async t => {
|
||||
const { db, collectEvents, mockSub, triggerWebhook } = t.context;
|
||||
const { db, collectEvents, mockAlias, mockSub, triggerWebhook } = t.context;
|
||||
mockAlias(user.id);
|
||||
await db.subscription.create({
|
||||
data: {
|
||||
targetId: user.id,
|
||||
@@ -732,8 +751,9 @@ test('should block read-write ops on revenuecat-managed record (cancel/resume/up
|
||||
});
|
||||
|
||||
test('should reconcile and fix missing or out-of-order states for revenuecat Active/Trialing/PastDue records', async t => {
|
||||
const { webhook, collectEvents, mockSub } = t.context;
|
||||
const { webhook, collectEvents, mockAlias, mockSub } = t.context;
|
||||
|
||||
mockAlias(user.id);
|
||||
const subscriber = mockSub([
|
||||
{
|
||||
identifier: 'Pro',
|
||||
@@ -759,8 +779,9 @@ test('should reconcile and fix missing or out-of-order states for revenuecat Act
|
||||
});
|
||||
|
||||
test('should treat refund as early expiration and revoke immediately', async t => {
|
||||
const { db, collectEvents, mockSub, triggerWebhook } = t.context;
|
||||
const { db, collectEvents, mockAlias, mockSub, triggerWebhook } = t.context;
|
||||
|
||||
mockAlias(user.id);
|
||||
await db.subscription.create({
|
||||
data: {
|
||||
targetId: user.id,
|
||||
@@ -803,7 +824,9 @@ test('should treat refund as early expiration and revoke immediately', async t =
|
||||
});
|
||||
|
||||
test('should ignore non-whitelisted productId and not write to DB', async t => {
|
||||
const { db, collectEvents, mockSub, triggerWebhook } = t.context;
|
||||
const { db, collectEvents, mockAlias, mockSub, triggerWebhook } = t.context;
|
||||
|
||||
mockAlias(user.id);
|
||||
mockSub([
|
||||
{
|
||||
identifier: 'Weird',
|
||||
@@ -831,8 +854,10 @@ test('should ignore non-whitelisted productId and not write to DB', async t => {
|
||||
});
|
||||
|
||||
test('should map via entitlement+duration when productId not whitelisted (P1M/P1Y only)', async t => {
|
||||
const { db, collectEvents, mockSubSeq, triggerWebhook } = t.context;
|
||||
const { db, collectEvents, mockAlias, mockSubSeq, triggerWebhook } =
|
||||
t.context;
|
||||
|
||||
mockAlias(user.id);
|
||||
mockSubSeq([
|
||||
[
|
||||
{
|
||||
@@ -933,8 +958,9 @@ test('should not dispatch webhook event when authorization header is missing or
|
||||
});
|
||||
|
||||
test('should refresh user subscriptions (empty / revenuecat / stripe-only)', async t => {
|
||||
const { subResolver, db, mockSubSeq } = t.context;
|
||||
const { subResolver, db, mockAlias, mockSubSeq } = t.context;
|
||||
|
||||
mockAlias(user.id);
|
||||
const currentUser = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
|
||||
@@ -2,13 +2,17 @@ import {
|
||||
createCipheriv,
|
||||
createDecipheriv,
|
||||
createHash,
|
||||
createPrivateKey,
|
||||
createPublicKey,
|
||||
createSign,
|
||||
createVerify,
|
||||
generateKeyPairSync,
|
||||
type KeyObject,
|
||||
randomBytes,
|
||||
randomInt,
|
||||
sign,
|
||||
timingSafeEqual,
|
||||
verify,
|
||||
} from 'node:crypto';
|
||||
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
@@ -32,20 +36,31 @@ function generatePrivateKey(): string {
|
||||
namedCurve: 'prime256v1',
|
||||
});
|
||||
|
||||
// Export EC private key as PKCS#8 PEM. This avoids OpenSSL 3.x decoder issues
|
||||
// in Node.js 22 when later deriving the public key via createPublicKey.
|
||||
const key = privateKey.export({
|
||||
type: 'sec1',
|
||||
type: 'pkcs8',
|
||||
format: 'pem',
|
||||
});
|
||||
|
||||
return key.toString('utf8');
|
||||
}
|
||||
|
||||
function generatePublicKey(privateKey: string) {
|
||||
return createPublicKey({
|
||||
key: Buffer.from(privateKey),
|
||||
})
|
||||
.export({ format: 'pem', type: 'spki' })
|
||||
.toString('utf8');
|
||||
function parseKey(privateKey: string) {
|
||||
const keyBuf = Buffer.from(privateKey);
|
||||
let priv: KeyObject;
|
||||
try {
|
||||
priv = createPrivateKey({ key: keyBuf, format: 'pem', type: 'pkcs8' });
|
||||
} catch (e1) {
|
||||
try {
|
||||
priv = createPrivateKey({ key: keyBuf, format: 'pem', type: 'sec1' });
|
||||
} catch (e2) {
|
||||
// As a last resort rely on auto-detection
|
||||
priv = createPrivateKey(keyBuf);
|
||||
}
|
||||
}
|
||||
const pub = createPublicKey(priv);
|
||||
return { priv, pub };
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -53,8 +68,8 @@ export class CryptoHelper implements OnModuleInit {
|
||||
logger = new Logger(CryptoHelper.name);
|
||||
|
||||
keyPair!: {
|
||||
publicKey: Buffer;
|
||||
privateKey: Buffer;
|
||||
publicKey: KeyObject;
|
||||
privateKey: KeyObject;
|
||||
sha256: {
|
||||
publicKey: Buffer;
|
||||
privateKey: Buffer;
|
||||
@@ -87,11 +102,14 @@ export class CryptoHelper implements OnModuleInit {
|
||||
|
||||
private setup() {
|
||||
const privateKey = this.config.crypto.privateKey || generatePrivateKey();
|
||||
const publicKey = generatePublicKey(privateKey);
|
||||
const { priv, pub } = parseKey(privateKey);
|
||||
const publicKey = pub
|
||||
.export({ format: 'pem', type: 'spki' })
|
||||
.toString('utf8');
|
||||
|
||||
this.keyPair = {
|
||||
publicKey: Buffer.from(publicKey),
|
||||
privateKey: Buffer.from(privateKey),
|
||||
publicKey: pub,
|
||||
privateKey: priv,
|
||||
sha256: {
|
||||
publicKey: this.sha256(publicKey),
|
||||
privateKey: this.sha256(privateKey),
|
||||
@@ -99,11 +117,23 @@ export class CryptoHelper implements OnModuleInit {
|
||||
};
|
||||
}
|
||||
|
||||
private get keyType() {
|
||||
return (this.keyPair.privateKey.asymmetricKeyType as string) || 'ec';
|
||||
}
|
||||
|
||||
sign(data: string) {
|
||||
const sign = createSign('rsa-sha256');
|
||||
sign.update(data, 'utf-8');
|
||||
sign.end();
|
||||
return `${data},${sign.sign(this.keyPair.privateKey, 'base64')}`;
|
||||
const input = Buffer.from(data, 'utf-8');
|
||||
if (this.keyType === 'ed25519') {
|
||||
// Ed25519 signs the message directly (no pre-hash)
|
||||
const sig = sign(null, input, this.keyPair.privateKey);
|
||||
return `${data},${sig.toString('base64')}`;
|
||||
} else {
|
||||
// ECDSA with SHA-256 for EC keys
|
||||
const sign = createSign('sha256');
|
||||
sign.update(input);
|
||||
sign.end();
|
||||
return `${data},${sign.sign(this.keyPair.privateKey, 'base64')}`;
|
||||
}
|
||||
}
|
||||
|
||||
verify(signatureWithData: string) {
|
||||
@@ -111,10 +141,18 @@ export class CryptoHelper implements OnModuleInit {
|
||||
if (!signature) {
|
||||
return false;
|
||||
}
|
||||
const verify = createVerify('rsa-sha256');
|
||||
verify.update(data, 'utf-8');
|
||||
verify.end();
|
||||
return verify.verify(this.keyPair.privateKey, signature, 'base64');
|
||||
const input = Buffer.from(data, 'utf-8');
|
||||
const sigBuf = Buffer.from(signature, 'base64');
|
||||
if (this.keyType === 'ed25519') {
|
||||
// Ed25519 verifies the message directly
|
||||
return verify(null, input, this.keyPair.publicKey, sigBuf);
|
||||
} else {
|
||||
// ECDSA with SHA-256
|
||||
const verify = createVerify('sha256');
|
||||
verify.update(input);
|
||||
verify.end();
|
||||
return verify.verify(this.keyPair.publicKey, sigBuf);
|
||||
}
|
||||
}
|
||||
|
||||
encrypt(data: string) {
|
||||
@@ -179,7 +217,7 @@ export class CryptoHelper implements OnModuleInit {
|
||||
let otp = '';
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
otp += this.randomInt(0, 9).toString();
|
||||
otp += this.randomInt(0, 10).toString();
|
||||
}
|
||||
|
||||
return otp;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const OneKB = 1024;
|
||||
export const OneMB = OneKB * OneKB;
|
||||
export const OneGB = OneKB * OneMB;
|
||||
export const OneDay = 1000 * 60 * 60 * 24;
|
||||
export const OneMinute = 1000 * 60;
|
||||
export const OneDay = OneMinute * 60 * 24;
|
||||
|
||||
@@ -270,14 +270,12 @@ export class AuthController {
|
||||
validators.assertValidEmail(email);
|
||||
|
||||
const cacheKey = OTP_CACHE_KEY(otp);
|
||||
const cachedToken = await this.cache.get<
|
||||
{ token: string; clientNonce: string } | string
|
||||
>(cacheKey);
|
||||
const cachedToken = await this.cache.get<{
|
||||
token: string;
|
||||
clientNonce: string;
|
||||
}>(cacheKey);
|
||||
let token: string | undefined;
|
||||
// TODO(@fengmk2): this is a temporary compatible with cache token is string value, should be removed in 0.22
|
||||
if (typeof cachedToken === 'string') {
|
||||
token = cachedToken;
|
||||
} else if (cachedToken) {
|
||||
if (cachedToken && typeof cachedToken === 'object') {
|
||||
token = cachedToken.token;
|
||||
if (cachedToken.clientNonce && cachedToken.clientNonce !== clientNonce) {
|
||||
throw new InvalidAuthState();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { getStreamAsBuffer } from 'get-stream';
|
||||
|
||||
import { JOB_SIGNAL, OnJob, sleep } from '../../base';
|
||||
import { Cache, JOB_SIGNAL, JobQueue, OnJob, sleep } from '../../base';
|
||||
import { type MailName, MailProps, Renderers } from '../../mails';
|
||||
import { UserProps, WorkspaceProps } from '../../mails/components';
|
||||
import { Models } from '../../models';
|
||||
@@ -40,17 +41,32 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const sendMailKey = 'mailjob:sendMail';
|
||||
const retryMailKey = 'mailjob:sendMail:retry';
|
||||
const sendMailCacheKey = (name: string, to: string) =>
|
||||
`${sendMailKey}:${name}:${to}`;
|
||||
const retryMaxPerTick = 20;
|
||||
const retryFirstTime = 3;
|
||||
|
||||
@Injectable()
|
||||
export class MailJob {
|
||||
private readonly logger = new Logger('MailJob');
|
||||
|
||||
constructor(
|
||||
private readonly cache: Cache,
|
||||
private readonly queue: JobQueue,
|
||||
private readonly sender: MailSender,
|
||||
private readonly doc: DocReader,
|
||||
private readonly workspaceBlob: WorkspaceBlobStorage,
|
||||
private readonly models: Models
|
||||
) {}
|
||||
|
||||
@OnJob('notification.sendMail')
|
||||
async sendMail({
|
||||
private calculateRetryDelay(startTime: number) {
|
||||
const elapsed = Date.now() - startTime;
|
||||
return Math.min(30 * 1000, Math.round(elapsed / 2000) * 1000);
|
||||
}
|
||||
|
||||
private async sendMailInternal({
|
||||
startTime,
|
||||
name,
|
||||
to,
|
||||
@@ -97,23 +113,29 @@ export class MailJob {
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.sender.send(name, {
|
||||
to,
|
||||
...(await Renderers[name](
|
||||
// @ts-expect-error the job trigger part has been typechecked
|
||||
props
|
||||
)),
|
||||
...options,
|
||||
});
|
||||
if (result === false) {
|
||||
try {
|
||||
const result = await this.sender.send(name, {
|
||||
to,
|
||||
...(await Renderers[name](
|
||||
// @ts-expect-error the job trigger part has been typechecked
|
||||
props
|
||||
)),
|
||||
...options,
|
||||
});
|
||||
if (!result) {
|
||||
// wait for a while before retrying
|
||||
const retryDelay = this.calculateRetryDelay(startTime);
|
||||
await sleep(retryDelay);
|
||||
return JOB_SIGNAL.Retry;
|
||||
}
|
||||
return undefined;
|
||||
} catch (e) {
|
||||
this.logger.error(`Failed to send mail [${name}] to [${to}]`, e);
|
||||
// wait for a while before retrying
|
||||
const elapsed = Date.now() - startTime;
|
||||
const retryDelay = Math.min(30 * 1000, Math.round(elapsed / 2000) * 1000);
|
||||
const retryDelay = this.calculateRetryDelay(startTime);
|
||||
await sleep(retryDelay);
|
||||
return JOB_SIGNAL.Retry;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async fetchWorkspaceProps(workspaceId: string) {
|
||||
@@ -151,4 +173,45 @@ export class MailJob {
|
||||
|
||||
return { email: user.email } satisfies UserProps;
|
||||
}
|
||||
|
||||
@OnJob('notification.sendMail')
|
||||
async sendMail(job: Jobs['notification.sendMail']) {
|
||||
const cacheKey = sendMailCacheKey(job.name, job.to);
|
||||
const retried = await this.cache.mapIncrease(sendMailKey, cacheKey, 1);
|
||||
if (retried <= retryFirstTime) {
|
||||
const ret = await this.sendMailInternal(job);
|
||||
if (!ret) await this.cache.mapDelete(sendMailKey, cacheKey);
|
||||
return ret;
|
||||
}
|
||||
await this.cache.mapSet(retryMailKey, cacheKey, JSON.stringify(job));
|
||||
await this.cache.mapDelete(sendMailKey, cacheKey);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@Cron(CronExpression.EVERY_MINUTE)
|
||||
async sendRetryMails() {
|
||||
// pick random one from the retry map
|
||||
let processed = 0;
|
||||
let key = await this.cache.mapRandomKey(retryMailKey);
|
||||
while (key && processed < retryMaxPerTick) {
|
||||
try {
|
||||
const job = await this.cache.mapGet<string>(retryMailKey, key);
|
||||
if (job) {
|
||||
const jobData = JSON.parse(job) as Jobs['notification.sendMail'];
|
||||
await this.queue.add('notification.sendMail', jobData);
|
||||
// wait for a while before retrying
|
||||
const retryDelay = this.calculateRetryDelay(jobData.startTime);
|
||||
await sleep(retryDelay);
|
||||
}
|
||||
await this.cache.mapDelete(retryMailKey, key);
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
`Failed to re-queue retry mail job for key [${key}]`,
|
||||
e
|
||||
);
|
||||
}
|
||||
key = await this.cache.mapRandomKey(retryMailKey);
|
||||
processed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user