mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2026-06-27 19:35:07 +00:00
Compare commits
No commits in common. "28a3e878b73f52a052cbbfad9181f60c009feeb6" and "5d0b0bb1758c6c0ab67edc8621e8ab8ecb6497e0" have entirely different histories.
28a3e878b7
...
5d0b0bb175
@ -29,9 +29,7 @@
|
|||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"vue": "^3.5.25",
|
"vue": "^3.5.25",
|
||||||
"vue-sonner": "^2.0.9",
|
"vue-sonner": "^2.0.9",
|
||||||
"vuetify": "^3.11.0",
|
"vuetify": "^3.11.0"
|
||||||
"vite": "^5.4.11"
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
@ -50,6 +48,7 @@
|
|||||||
"unplugin-fonts": "^1.4.0",
|
"unplugin-fonts": "^1.4.0",
|
||||||
"unplugin-vue-components": "^30.0.0",
|
"unplugin-vue-components": "^30.0.0",
|
||||||
"unplugin-vue-router": "^0.18.0",
|
"unplugin-vue-router": "^0.18.0",
|
||||||
|
"vite": "^5.4.11",
|
||||||
"vite-plugin-pwa": "^1.2.0",
|
"vite-plugin-pwa": "^1.2.0",
|
||||||
"vite-plugin-vue-devtools": "^7.6.8",
|
"vite-plugin-vue-devtools": "^7.6.8",
|
||||||
"vite-plugin-vue-layouts": "^0.11.0",
|
"vite-plugin-vue-layouts": "^0.11.0",
|
||||||
|
|||||||
43
pnpm-lock.yaml
generated
43
pnpm-lock.yaml
generated
@ -56,9 +56,6 @@ importers:
|
|||||||
uuid:
|
uuid:
|
||||||
specifier: ^13.0.0
|
specifier: ^13.0.0
|
||||||
version: 13.0.0
|
version: 13.0.0
|
||||||
vite:
|
|
||||||
specifier: ^5.4.11
|
|
||||||
version: 5.4.21(sass-embedded@1.93.3)(sass@1.94.2)(terser@5.44.1)
|
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.25
|
specifier: ^3.5.25
|
||||||
version: 3.5.25(typescript@5.9.3)
|
version: 3.5.25(typescript@5.9.3)
|
||||||
@ -117,6 +114,9 @@ importers:
|
|||||||
unplugin-vue-router:
|
unplugin-vue-router:
|
||||||
specifier: ^0.18.0
|
specifier: ^0.18.0
|
||||||
version: 0.18.0(@vue/compiler-sfc@3.5.25)(typescript@5.9.3)(vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
|
version: 0.18.0(@vue/compiler-sfc@3.5.25)(typescript@5.9.3)(vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
|
||||||
|
vite:
|
||||||
|
specifier: ^5.4.11
|
||||||
|
version: 5.4.21(sass-embedded@1.93.3)(sass@1.94.2)(terser@5.44.1)
|
||||||
vite-plugin-pwa:
|
vite-plugin-pwa:
|
||||||
specifier: ^1.2.0
|
specifier: ^1.2.0
|
||||||
version: 1.2.0(@vite-pwa/assets-generator@1.0.2)(vite@5.4.21(sass-embedded@1.93.3)(sass@1.94.2)(terser@5.44.1))(workbox-build@7.4.0)(workbox-window@7.4.0)
|
version: 1.2.0(@vite-pwa/assets-generator@1.0.2)(vite@5.4.21(sass-embedded@1.93.3)(sass@1.94.2)(terser@5.44.1))(workbox-build@7.4.0)(workbox-window@7.4.0)
|
||||||
@ -904,79 +904,67 @@ packages:
|
|||||||
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
|
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-arm@1.0.5':
|
'@img/sharp-libvips-linux-arm@1.0.5':
|
||||||
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
|
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-s390x@1.0.4':
|
'@img/sharp-libvips-linux-s390x@1.0.4':
|
||||||
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
|
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-x64@1.0.4':
|
'@img/sharp-libvips-linux-x64@1.0.4':
|
||||||
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
|
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
|
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
|
||||||
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
|
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
|
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
|
||||||
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
|
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@img/sharp-linux-arm64@0.33.5':
|
'@img/sharp-linux-arm64@0.33.5':
|
||||||
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
|
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-linux-arm@0.33.5':
|
'@img/sharp-linux-arm@0.33.5':
|
||||||
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
|
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-linux-s390x@0.33.5':
|
'@img/sharp-linux-s390x@0.33.5':
|
||||||
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
|
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-linux-x64@0.33.5':
|
'@img/sharp-linux-x64@0.33.5':
|
||||||
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
|
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-arm64@0.33.5':
|
'@img/sharp-linuxmusl-arm64@0.33.5':
|
||||||
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
|
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-x64@0.33.5':
|
'@img/sharp-linuxmusl-x64@0.33.5':
|
||||||
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
|
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@img/sharp-wasm32@0.33.5':
|
'@img/sharp-wasm32@0.33.5':
|
||||||
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
|
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
|
||||||
@ -1073,42 +1061,36 @@ packages:
|
|||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@parcel/watcher-linux-arm-musl@2.5.1':
|
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||||
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
|
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||||
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
|
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||||
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
|
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||||
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
|
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@parcel/watcher-linux-x64-musl@2.5.1':
|
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||||
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
|
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@parcel/watcher-win32-arm64@2.5.1':
|
'@parcel/watcher-win32-arm64@2.5.1':
|
||||||
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
|
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
|
||||||
@ -1224,67 +1206,56 @@ packages:
|
|||||||
resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==}
|
resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm-musleabihf@4.53.3':
|
'@rollup/rollup-linux-arm-musleabihf@4.53.3':
|
||||||
resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==}
|
resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-gnu@4.53.3':
|
'@rollup/rollup-linux-arm64-gnu@4.53.3':
|
||||||
resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==}
|
resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-musl@4.53.3':
|
'@rollup/rollup-linux-arm64-musl@4.53.3':
|
||||||
resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==}
|
resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-loong64-gnu@4.53.3':
|
'@rollup/rollup-linux-loong64-gnu@4.53.3':
|
||||||
resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==}
|
resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-gnu@4.53.3':
|
'@rollup/rollup-linux-ppc64-gnu@4.53.3':
|
||||||
resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==}
|
resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-gnu@4.53.3':
|
'@rollup/rollup-linux-riscv64-gnu@4.53.3':
|
||||||
resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==}
|
resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-musl@4.53.3':
|
'@rollup/rollup-linux-riscv64-musl@4.53.3':
|
||||||
resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==}
|
resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-s390x-gnu@4.53.3':
|
'@rollup/rollup-linux-s390x-gnu@4.53.3':
|
||||||
resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==}
|
resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-gnu@4.53.3':
|
'@rollup/rollup-linux-x64-gnu@4.53.3':
|
||||||
resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==}
|
resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-musl@4.53.3':
|
'@rollup/rollup-linux-x64-musl@4.53.3':
|
||||||
resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==}
|
resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-openharmony-arm64@4.53.3':
|
'@rollup/rollup-openharmony-arm64@4.53.3':
|
||||||
resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==}
|
resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==}
|
||||||
@ -2986,56 +2957,48 @@ packages:
|
|||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: glibc
|
|
||||||
|
|
||||||
sass-embedded-linux-arm@1.93.3:
|
sass-embedded-linux-arm@1.93.3:
|
||||||
resolution: {integrity: sha512-yeiv2y+dp8B4wNpd3+JsHYD0mvpXSfov7IGyQ1tMIR40qv+ROkRqYiqQvAOXf76Qwh4Y9OaYZtLpnsPjfeq6mA==}
|
resolution: {integrity: sha512-yeiv2y+dp8B4wNpd3+JsHYD0mvpXSfov7IGyQ1tMIR40qv+ROkRqYiqQvAOXf76Qwh4Y9OaYZtLpnsPjfeq6mA==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: glibc
|
|
||||||
|
|
||||||
sass-embedded-linux-musl-arm64@1.93.3:
|
sass-embedded-linux-musl-arm64@1.93.3:
|
||||||
resolution: {integrity: sha512-PS829l+eUng+9W4PFclXGb4uA2+965NHV3/Sa5U7qTywjeeUUYTZg70dJHSqvhrBEfCc2XJABeW3adLJbyQYkw==}
|
resolution: {integrity: sha512-PS829l+eUng+9W4PFclXGb4uA2+965NHV3/Sa5U7qTywjeeUUYTZg70dJHSqvhrBEfCc2XJABeW3adLJbyQYkw==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: musl
|
|
||||||
|
|
||||||
sass-embedded-linux-musl-arm@1.93.3:
|
sass-embedded-linux-musl-arm@1.93.3:
|
||||||
resolution: {integrity: sha512-fU0fwAwbp7sBE3h5DVU5UPzvaLg7a4yONfFWkkcCp6ZrOiPuGRHXXYriWQ0TUnWy4wE+svsVuWhwWgvlb/tkKg==}
|
resolution: {integrity: sha512-fU0fwAwbp7sBE3h5DVU5UPzvaLg7a4yONfFWkkcCp6ZrOiPuGRHXXYriWQ0TUnWy4wE+svsVuWhwWgvlb/tkKg==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: musl
|
|
||||||
|
|
||||||
sass-embedded-linux-musl-riscv64@1.93.3:
|
sass-embedded-linux-musl-riscv64@1.93.3:
|
||||||
resolution: {integrity: sha512-cK1oBY+FWQquaIGEeQ5H74KTO8cWsSWwXb/WaildOO9U6wmUypTgUYKQ0o5o/29nZbWWlM1PHuwVYTSnT23Jjg==}
|
resolution: {integrity: sha512-cK1oBY+FWQquaIGEeQ5H74KTO8cWsSWwXb/WaildOO9U6wmUypTgUYKQ0o5o/29nZbWWlM1PHuwVYTSnT23Jjg==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: musl
|
|
||||||
|
|
||||||
sass-embedded-linux-musl-x64@1.93.3:
|
sass-embedded-linux-musl-x64@1.93.3:
|
||||||
resolution: {integrity: sha512-A7wkrsHu2/I4Zpa0NMuPGkWDVV7QGGytxGyUq3opSXgAexHo/vBPlGoDXoRlSdex0cV+aTMRPjoGIfdmNlHwyg==}
|
resolution: {integrity: sha512-A7wkrsHu2/I4Zpa0NMuPGkWDVV7QGGytxGyUq3opSXgAexHo/vBPlGoDXoRlSdex0cV+aTMRPjoGIfdmNlHwyg==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: musl
|
|
||||||
|
|
||||||
sass-embedded-linux-riscv64@1.93.3:
|
sass-embedded-linux-riscv64@1.93.3:
|
||||||
resolution: {integrity: sha512-vWkW1+HTF5qcaHa6hO80gx/QfB6GGjJUP0xLbnAoY4pwEnw5ulGv6RM8qYr8IDhWfVt/KH+lhJ2ZFxnJareisQ==}
|
resolution: {integrity: sha512-vWkW1+HTF5qcaHa6hO80gx/QfB6GGjJUP0xLbnAoY4pwEnw5ulGv6RM8qYr8IDhWfVt/KH+lhJ2ZFxnJareisQ==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: glibc
|
|
||||||
|
|
||||||
sass-embedded-linux-x64@1.93.3:
|
sass-embedded-linux-x64@1.93.3:
|
||||||
resolution: {integrity: sha512-k6uFxs+e5jSuk1Y0niCwuq42F9ZC5UEP7P+RIOurIm8w/5QFa0+YqeW+BPWEW5M1FqVOsNZH3qGn4ahqvAEjPA==}
|
resolution: {integrity: sha512-k6uFxs+e5jSuk1Y0niCwuq42F9ZC5UEP7P+RIOurIm8w/5QFa0+YqeW+BPWEW5M1FqVOsNZH3qGn4ahqvAEjPA==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: glibc
|
|
||||||
|
|
||||||
sass-embedded-unknown-all@1.93.3:
|
sass-embedded-unknown-all@1.93.3:
|
||||||
resolution: {integrity: sha512-o5wj2rLpXH0C+GJKt/VpWp6AnMsCCbfFmnMAttcrsa+U3yrs/guhZ3x55KAqqUsE8F47e3frbsDL+1OuQM5DAA==}
|
resolution: {integrity: sha512-o5wj2rLpXH0C+GJKt/VpWp6AnMsCCbfFmnMAttcrsa+U3yrs/guhZ3x55KAqqUsE8F47e3frbsDL+1OuQM5DAA==}
|
||||||
|
|||||||
@ -1,270 +0,0 @@
|
|||||||
/**
|
|
||||||
* Cache Manager — 带 TTL 的缓存管理器
|
|
||||||
*
|
|
||||||
* 封装 IndexedDB 缓存读写 + 过期逻辑。
|
|
||||||
* 缓存条目格式: { data, meta, cacheTimestamp, cacheTTL }
|
|
||||||
* 存储在 ClassworksDB 的 kv store 中,key 前缀为 _cache:
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { openDB } from "idb";
|
|
||||||
import { createEmptyMeta } from "./crdtEngine";
|
|
||||||
import { getSetting } from "./settings";
|
|
||||||
|
|
||||||
// Cache key prefix (与旧代码保持一致)
|
|
||||||
const CACHE_PREFIX = "_cache:";
|
|
||||||
|
|
||||||
// 数据库信息
|
|
||||||
const DB_NAME = "ClassworksDB";
|
|
||||||
|
|
||||||
// 默认 TTL: 7 天
|
|
||||||
const DEFAULT_TTL = 7 * 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TTL 配置 — 按 key 模式匹配
|
|
||||||
* 顺序匹配,第一个命中的生效
|
|
||||||
* 通配符 * 匹配任意字符
|
|
||||||
*/
|
|
||||||
const TTL_CONFIG = [
|
|
||||||
{ pattern: "*", ttl: DEFAULT_TTL },
|
|
||||||
];
|
|
||||||
|
|
||||||
// --- 内部辅助 ---
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化数据库连接
|
|
||||||
*/
|
|
||||||
async function getDB() {
|
|
||||||
return openDB(DB_NAME, undefined, {
|
|
||||||
upgrade(db) {
|
|
||||||
if (!db.objectStoreNames.contains("kv")) {
|
|
||||||
db.createObjectStore("kv");
|
|
||||||
}
|
|
||||||
if (!db.objectStoreNames.contains("system")) {
|
|
||||||
db.createObjectStore("system");
|
|
||||||
}
|
|
||||||
if (!db.objectStoreNames.contains("syncQueue")) {
|
|
||||||
db.createObjectStore("syncQueue");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 简单 glob 匹配 (* 匹配任意字符)
|
|
||||||
* @param {string} pattern
|
|
||||||
* @param {string} str
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
function globMatch(pattern, str) {
|
|
||||||
const regexStr = "^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$";
|
|
||||||
return new RegExp(regexStr).test(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断缓存条目是否为旧格式 (无 meta 字段)
|
|
||||||
* @param {*} entry
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
function isLegacyEntry(entry) {
|
|
||||||
return entry && typeof entry === "object" && !("meta" in entry) && !("cacheTimestamp" in entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断缓存条目是否为新格式
|
|
||||||
* @param {*} entry
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
function isNewFormatEntry(entry) {
|
|
||||||
return entry && typeof entry === "object" && "meta" in entry && "cacheTimestamp" in entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 导出 API ---
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据 key 匹配 TTL 配置
|
|
||||||
* @param {string} key — 数据 key (不含 _cache: 前缀)
|
|
||||||
* @returns {number} TTL 毫秒数
|
|
||||||
*/
|
|
||||||
export function getTTLForKey(key) {
|
|
||||||
for (const { pattern, ttl } of TTL_CONFIG) {
|
|
||||||
if (globMatch(pattern, key)) {
|
|
||||||
return ttl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return DEFAULT_TTL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 读取缓存条目
|
|
||||||
* - 过期则自动删除并返回 null
|
|
||||||
* - 旧格式自动迁移到新格式
|
|
||||||
*
|
|
||||||
* @param {string} key — 数据 key (不含前缀)
|
|
||||||
* @returns {Promise<{ data: *, meta: Object }|null>}
|
|
||||||
*/
|
|
||||||
export async function getCacheEntry(key) {
|
|
||||||
try {
|
|
||||||
const db = await getDB();
|
|
||||||
const cacheKey = CACHE_PREFIX + key;
|
|
||||||
const raw = await db.get("kv", cacheKey);
|
|
||||||
|
|
||||||
if (!raw) return null;
|
|
||||||
|
|
||||||
// 尝试解析 JSON
|
|
||||||
let entry;
|
|
||||||
try {
|
|
||||||
entry = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 旧格式迁移
|
|
||||||
if (isLegacyEntry(entry)) {
|
|
||||||
const deviceId = getSetting("device.uuid") || "unknown";
|
|
||||||
const meta = createEmptyMeta(deviceId);
|
|
||||||
meta.ts = Date.now();
|
|
||||||
meta.lastSyncedData = entry;
|
|
||||||
|
|
||||||
const migrated = {
|
|
||||||
data: entry,
|
|
||||||
meta,
|
|
||||||
cacheTimestamp: Date.now(),
|
|
||||||
cacheTTL: getTTLForKey(key),
|
|
||||||
};
|
|
||||||
|
|
||||||
// 写回迁移后的格式
|
|
||||||
await db.put("kv", JSON.stringify(migrated), cacheKey);
|
|
||||||
return { data: migrated.data, meta: migrated.meta };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新格式 — 检查是否过期
|
|
||||||
if (isNewFormatEntry(entry)) {
|
|
||||||
const age = Date.now() - entry.cacheTimestamp;
|
|
||||||
if (age > entry.cacheTTL) {
|
|
||||||
// 过期,删除
|
|
||||||
await db.delete("kv", cacheKey);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return { data: entry.data, meta: entry.meta };
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("cacheManager.getCacheEntry 失败:", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 写入缓存条目
|
|
||||||
* @param {string} key — 数据 key (不含前缀)
|
|
||||||
* @param {*} data — 用户数据
|
|
||||||
* @param {Object} meta — CRDT metadata
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
export async function setCacheEntry(key, data, meta) {
|
|
||||||
try {
|
|
||||||
const db = await getDB();
|
|
||||||
const cacheKey = CACHE_PREFIX + key;
|
|
||||||
const entry = {
|
|
||||||
data,
|
|
||||||
meta,
|
|
||||||
cacheTimestamp: Date.now(),
|
|
||||||
cacheTTL: getTTLForKey(key),
|
|
||||||
};
|
|
||||||
await db.put("kv", JSON.stringify(entry), cacheKey);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("cacheManager.setCacheEntry 失败:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除缓存条目
|
|
||||||
* @param {string} key — 数据 key (不含前缀)
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
export async function deleteCacheEntry(key) {
|
|
||||||
try {
|
|
||||||
const db = await getDB();
|
|
||||||
await db.delete("kv", CACHE_PREFIX + key);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("cacheManager.deleteCacheEntry 失败:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查缓存是否未过期
|
|
||||||
* @param {string} key — 数据 key (不含前缀)
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
export async function isCacheFresh(key) {
|
|
||||||
try {
|
|
||||||
const db = await getDB();
|
|
||||||
const raw = await db.get("kv", CACHE_PREFIX + key);
|
|
||||||
if (!raw) return false;
|
|
||||||
|
|
||||||
let entry;
|
|
||||||
try {
|
|
||||||
entry = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isNewFormatEntry(entry)) return false;
|
|
||||||
|
|
||||||
return Date.now() - entry.cacheTimestamp <= entry.cacheTTL;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理所有过期的 _cache: 条目
|
|
||||||
* 在启动时和同步完成后调用
|
|
||||||
* @returns {Promise<number>} 删除的条目数
|
|
||||||
*/
|
|
||||||
export async function cleanupExpiredEntries() {
|
|
||||||
let cleaned = 0;
|
|
||||||
try {
|
|
||||||
const db = await getDB();
|
|
||||||
const tx = db.transaction("kv", "readwrite");
|
|
||||||
const store = tx.objectStore("kv");
|
|
||||||
const allKeys = await store.getAllKeys();
|
|
||||||
|
|
||||||
for (const storeKey of allKeys) {
|
|
||||||
if (!storeKey.startsWith(CACHE_PREFIX)) continue;
|
|
||||||
|
|
||||||
const raw = await store.get(storeKey);
|
|
||||||
if (!raw) continue;
|
|
||||||
|
|
||||||
let entry;
|
|
||||||
try {
|
|
||||||
entry = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNewFormatEntry(entry)) {
|
|
||||||
const age = Date.now() - entry.cacheTimestamp;
|
|
||||||
if (age > entry.cacheTTL) {
|
|
||||||
await store.delete(storeKey);
|
|
||||||
cleaned++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await tx.done;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("cacheManager.cleanupExpiredEntries 失败:", error);
|
|
||||||
}
|
|
||||||
return cleaned;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取缓存前缀 (供外部使用)
|
|
||||||
*/
|
|
||||||
export { CACHE_PREFIX };
|
|
||||||
@ -1,305 +0,0 @@
|
|||||||
/**
|
|
||||||
* CRDT Engine — 纯函数冲突解决模块
|
|
||||||
*
|
|
||||||
* 基于向量时钟的无冲突复制数据类型 (CRDT) 实现。
|
|
||||||
* 所有函数无副作用,不依赖 IndexedDB 或网络。
|
|
||||||
*
|
|
||||||
* 合并策略:
|
|
||||||
* - 对象: 按字段 LWW (Last-Writer-Wins),使用 _fieldTs 字段级时间戳
|
|
||||||
* - 数组: 按 identity 合并,union 语义
|
|
||||||
* - 原始类型: LWW,时间戳相同时 deviceId 字典序决定
|
|
||||||
*/
|
|
||||||
|
|
||||||
// --- 向量时钟操作 ---
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建空的 metadata 对象
|
|
||||||
* @param {string} deviceId — 本设备标识符
|
|
||||||
* @returns {Object} 初始 metadata
|
|
||||||
*/
|
|
||||||
export function createEmptyMeta(deviceId) {
|
|
||||||
return {
|
|
||||||
vc: { [deviceId]: 0 },
|
|
||||||
ts: 0,
|
|
||||||
deviceId,
|
|
||||||
_fieldTs: {},
|
|
||||||
lastSyncedData: null,
|
|
||||||
lastSyncedTs: 0,
|
|
||||||
lastSyncedVc: { [deviceId]: 0 },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 递增设备时钟,返回新的 metadata (不可变)
|
|
||||||
* @param {Object} meta — 当前 metadata
|
|
||||||
* @param {string} deviceId — 本设备标识符
|
|
||||||
* @returns {Object} 递增后的新 metadata
|
|
||||||
*/
|
|
||||||
export function bumpClock(meta, deviceId) {
|
|
||||||
const newVc = { ...meta.vc };
|
|
||||||
newVc[deviceId] = (newVc[deviceId] || 0) + 1;
|
|
||||||
return {
|
|
||||||
...meta,
|
|
||||||
vc: newVc,
|
|
||||||
ts: Date.now(),
|
|
||||||
deviceId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 合并两个向量时钟,取各分量的 max
|
|
||||||
* @param {Object} vcA
|
|
||||||
* @param {Object} vcB
|
|
||||||
* @returns {Object} 合并后的向量时钟
|
|
||||||
*/
|
|
||||||
export function mergeClocks(vcA, vcB) {
|
|
||||||
const result = { ...vcA };
|
|
||||||
for (const [node, count] of Object.entries(vcB)) {
|
|
||||||
result[node] = Math.max(result[node] || 0, count);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 比较两个版本的向量时钟
|
|
||||||
* @param {Object} metaA
|
|
||||||
* @param {Object} metaB
|
|
||||||
* @returns {"A_NEWER"|"B_NEWER"|"CONCURRENT"|"EQUAL"}
|
|
||||||
*/
|
|
||||||
export function compareVersions(metaA, metaB) {
|
|
||||||
const vcA = metaA.vc || {};
|
|
||||||
const vcB = metaB.vc || {};
|
|
||||||
|
|
||||||
// 收集所有节点
|
|
||||||
const allNodes = new Set([...Object.keys(vcA), ...Object.keys(vcB)]);
|
|
||||||
|
|
||||||
let aGreater = false;
|
|
||||||
let bGreater = false;
|
|
||||||
|
|
||||||
for (const node of allNodes) {
|
|
||||||
const a = vcA[node] || 0;
|
|
||||||
const b = vcB[node] || 0;
|
|
||||||
if (a > b) aGreater = true;
|
|
||||||
if (b > a) bGreater = true;
|
|
||||||
if (aGreater && bGreater) return "CONCURRENT";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!aGreater && !bGreater) return "EQUAL";
|
|
||||||
if (aGreater) return "A_NEWER";
|
|
||||||
return "B_NEWER";
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 数据合并 ---
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断是否为纯对象 (非数组、非 null)
|
|
||||||
*/
|
|
||||||
function isPlainObject(val) {
|
|
||||||
return val !== null && typeof val === "object" && !Array.isArray(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DJB2 哈希,用于轻量数据比较
|
|
||||||
* @param {*} data — JSON 可序列化数据
|
|
||||||
* @returns {string} 十六进制哈希字符串
|
|
||||||
*/
|
|
||||||
export function computeDataHash(data) {
|
|
||||||
const str = typeof data === "string" ? data : JSON.stringify(data);
|
|
||||||
let hash = 5381;
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0;
|
|
||||||
}
|
|
||||||
return (hash >>> 0).toString(16);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 合并字段级时间戳
|
|
||||||
* @param {Object} fieldTsA
|
|
||||||
* @param {Object} fieldTsB
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
function mergeFieldTimestamps(fieldTsA, fieldTsB) {
|
|
||||||
if (!fieldTsA && !fieldTsB) return {};
|
|
||||||
if (!fieldTsA) return { ...fieldTsB };
|
|
||||||
if (!fieldTsB) return { ...fieldTsA };
|
|
||||||
|
|
||||||
const result = { ...fieldTsA };
|
|
||||||
for (const [path, infoB] of Object.entries(fieldTsB)) {
|
|
||||||
const infoA = result[path];
|
|
||||||
if (!infoA || infoB.ts > infoA.ts || (infoB.ts === infoA.ts && infoB.deviceId < infoA.deviceId)) {
|
|
||||||
result[path] = infoB;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检测数组的 identity 函数
|
|
||||||
* 优先级: id > name > key > JSON.stringify
|
|
||||||
*/
|
|
||||||
function detectIdentityFn(arr) {
|
|
||||||
if (!arr || arr.length === 0) return null;
|
|
||||||
const first = arr[0];
|
|
||||||
if (isPlainObject(first)) {
|
|
||||||
if ("id" in first) return (item) => (isPlainObject(item) ? item.id : JSON.stringify(item));
|
|
||||||
if ("name" in first) return (item) => (isPlainObject(item) ? item.name : JSON.stringify(item));
|
|
||||||
if ("key" in first) return (item) => (isPlainObject(item) ? item.key : JSON.stringify(item));
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 合并两个数组 — union 语义,按 identity 去重
|
|
||||||
* @param {Array} local
|
|
||||||
* @param {Object} localMeta
|
|
||||||
* @param {Array} remote
|
|
||||||
* @returns {Array} 合并后的数组
|
|
||||||
*/
|
|
||||||
function mergeArrays(local, localMeta, remote) {
|
|
||||||
const identityFn =
|
|
||||||
detectIdentityFn(local) ||
|
|
||||||
detectIdentityFn(remote) ||
|
|
||||||
((item) => JSON.stringify(item));
|
|
||||||
|
|
||||||
const localMap = new Map();
|
|
||||||
const remoteMap = new Map();
|
|
||||||
|
|
||||||
local.forEach((item) => {
|
|
||||||
const id = identityFn(item);
|
|
||||||
if (!localMap.has(id)) localMap.set(id, item);
|
|
||||||
});
|
|
||||||
remote.forEach((item) => {
|
|
||||||
const id = identityFn(item);
|
|
||||||
if (!remoteMap.has(id)) remoteMap.set(id, item);
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = [];
|
|
||||||
const seen = new Set();
|
|
||||||
|
|
||||||
// 保持本地顺序,本地有的以本地为准
|
|
||||||
for (const [id, item] of localMap) {
|
|
||||||
if (seen.has(id)) continue;
|
|
||||||
seen.add(id);
|
|
||||||
result.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 追加远程独有的
|
|
||||||
for (const [id, item] of remoteMap) {
|
|
||||||
if (!seen.has(id)) {
|
|
||||||
seen.add(id);
|
|
||||||
result.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 合并两个对象 — 按字段 LWW
|
|
||||||
* @param {Object} local
|
|
||||||
* @param {Object} localMeta
|
|
||||||
* @param {Object} remote
|
|
||||||
* @param {Object} remoteMeta
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
function mergeObjects(local, localMeta, remote, remoteMeta) {
|
|
||||||
const result = {};
|
|
||||||
const allKeys = new Set([...Object.keys(local), ...Object.keys(remote)]);
|
|
||||||
|
|
||||||
for (const key of allKeys) {
|
|
||||||
const localHas = key in local;
|
|
||||||
const remoteHas = key in remote;
|
|
||||||
|
|
||||||
if (localHas && !remoteHas) {
|
|
||||||
result[key] = local[key];
|
|
||||||
} else if (!localHas && remoteHas) {
|
|
||||||
result[key] = remote[key];
|
|
||||||
} else {
|
|
||||||
// 两边都有 — 用字段级时间戳决定
|
|
||||||
const localFieldInfo = localMeta._fieldTs?.[key] || {
|
|
||||||
ts: localMeta.ts,
|
|
||||||
deviceId: localMeta.deviceId,
|
|
||||||
};
|
|
||||||
const remoteFieldInfo = remoteMeta._fieldTs?.[key] || {
|
|
||||||
ts: remoteMeta.ts,
|
|
||||||
deviceId: remoteMeta.deviceId,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (localFieldInfo.ts > remoteFieldInfo.ts) {
|
|
||||||
result[key] = local[key];
|
|
||||||
} else if (remoteFieldInfo.ts > localFieldInfo.ts) {
|
|
||||||
result[key] = remote[key];
|
|
||||||
} else {
|
|
||||||
// 时间戳相同 — deviceId 字典序决定
|
|
||||||
result[key] =
|
|
||||||
localFieldInfo.deviceId <= remoteFieldInfo.deviceId
|
|
||||||
? local[key]
|
|
||||||
: remote[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CRDT 合并入口 — 根据数据类型选择合并策略
|
|
||||||
*
|
|
||||||
* @param {*} localData — 本地数据
|
|
||||||
* @param {Object} localMeta — 本地 metadata (含 vc, ts, deviceId, _fieldTs)
|
|
||||||
* @param {*} remoteData — 远程数据
|
|
||||||
* @param {Object} remoteMeta — 远程 metadata
|
|
||||||
* @returns {{ data: *, meta: Object }} 合并后的数据和 metadata
|
|
||||||
*/
|
|
||||||
export function mergeValues(localData, localMeta, remoteData, remoteMeta) {
|
|
||||||
const mergedVc = mergeClocks(localMeta.vc || {}, remoteMeta.vc || {});
|
|
||||||
const mergedTs = Math.max(localMeta.ts || 0, remoteMeta.ts || 0);
|
|
||||||
const mergedFieldTs = mergeFieldTimestamps(
|
|
||||||
localMeta._fieldTs,
|
|
||||||
remoteMeta._fieldTs,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mergedData;
|
|
||||||
|
|
||||||
if (isPlainObject(localData) && isPlainObject(remoteData)) {
|
|
||||||
mergedData = mergeObjects(
|
|
||||||
localData,
|
|
||||||
localMeta,
|
|
||||||
remoteData,
|
|
||||||
remoteMeta,
|
|
||||||
);
|
|
||||||
} else if (Array.isArray(localData) && Array.isArray(remoteData)) {
|
|
||||||
mergedData = mergeArrays(localData, localMeta, remoteData);
|
|
||||||
} else if (
|
|
||||||
typeof localData === typeof remoteData &&
|
|
||||||
typeof localData !== "object"
|
|
||||||
) {
|
|
||||||
// 原始类型: LWW
|
|
||||||
if ((localMeta.ts || 0) > (remoteMeta.ts || 0)) {
|
|
||||||
mergedData = localData;
|
|
||||||
} else if ((remoteMeta.ts || 0) > (localMeta.ts || 0)) {
|
|
||||||
mergedData = remoteData;
|
|
||||||
} else {
|
|
||||||
mergedData =
|
|
||||||
(localMeta.deviceId || "") <= (remoteMeta.deviceId || "")
|
|
||||||
? localData
|
|
||||||
: remoteData;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 类型不同 — LWW
|
|
||||||
mergedData = (localMeta.ts || 0) >= (remoteMeta.ts || 0) ? localData : remoteData;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: mergedData,
|
|
||||||
meta: {
|
|
||||||
vc: mergedVc,
|
|
||||||
ts: mergedTs,
|
|
||||||
deviceId: mergedTs === (localMeta.ts || 0) ? localMeta.deviceId : remoteMeta.deviceId,
|
|
||||||
_fieldTs: mergedFieldTs,
|
|
||||||
lastSyncedData: remoteMeta.lastSyncedData ?? localMeta.lastSyncedData ?? null,
|
|
||||||
lastSyncedTs: Math.max(localMeta.lastSyncedTs || 0, remoteMeta.lastSyncedTs || 0),
|
|
||||||
lastSyncedVc: mergedVc,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,31 +1,18 @@
|
|||||||
import { kvLocalProvider } from "./providers/kvLocalProvider";
|
import {kvLocalProvider} from "./providers/kvLocalProvider";
|
||||||
import { kvServerProvider } from "./providers/kvServerProvider";
|
import {kvServerProvider} from "./providers/kvServerProvider";
|
||||||
import { getSetting, setSetting } from "./settings";
|
import {getSetting, setSetting} from "./settings";
|
||||||
import { getEffectiveServerUrl } from "./serverRotation";
|
import {getEffectiveServerUrl} from "./serverRotation";
|
||||||
import {
|
|
||||||
createEmptyMeta,
|
|
||||||
bumpClock,
|
|
||||||
mergeValues,
|
|
||||||
} from "./crdtEngine";
|
|
||||||
import {
|
|
||||||
getCacheEntry,
|
|
||||||
setCacheEntry,
|
|
||||||
CACHE_PREFIX,
|
|
||||||
} from "./cacheManager";
|
|
||||||
import {
|
|
||||||
initSmartSync,
|
|
||||||
destroySmartSync,
|
|
||||||
flushAll,
|
|
||||||
triggerSyncAfterSuccess,
|
|
||||||
} from "./smartSyncManager";
|
|
||||||
|
|
||||||
export const formatResponse = (data) => data;
|
export const formatResponse = (data) => data;
|
||||||
|
|
||||||
export const formatError = (message, code = "UNKNOWN_ERROR") => ({
|
export const formatError = (message, code = "UNKNOWN_ERROR") => ({
|
||||||
success: false,
|
success: false,
|
||||||
error: { code, message },
|
error: {code, message},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Cache key prefix to avoid collision with kv-local mode data
|
||||||
|
const CACHE_PREFIX = "_cache:";
|
||||||
|
|
||||||
function isServerError(result) {
|
function isServerError(result) {
|
||||||
return result && result.success === false;
|
return result && result.success === false;
|
||||||
}
|
}
|
||||||
@ -34,32 +21,50 @@ function isNetworkError(result) {
|
|||||||
return isServerError(result) && result.error?.code === "NETWORK_ERROR";
|
return isServerError(result) && result.error?.code === "NETWORK_ERROR";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// --- Sync manager: flushes queued writes when back online ---
|
||||||
* 获取设备 ID,用于 CRDT 向量时钟节点标识
|
|
||||||
*/
|
|
||||||
function getDeviceId() {
|
|
||||||
return getSetting("device.uuid") || "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
let _onlineHandler = null;
|
||||||
* 规范化服务器返回的数据
|
let _flushing = false;
|
||||||
* 某些端点 (如 Bearer token 认证) 返回 {value: [...]} 包装格式,
|
|
||||||
* 统一解包为原始数据,保证缓存比较的一致性。
|
async function flushSyncQueue() {
|
||||||
* @param {*} data — 服务器返回的原始数据
|
if (_flushing) return;
|
||||||
* @returns {*} 规范化后的数据
|
_flushing = true;
|
||||||
*/
|
try {
|
||||||
function normalizeServerData(data) {
|
const queueResult = await kvLocalProvider.getSyncQueue();
|
||||||
if (data && typeof data === "object" && !Array.isArray(data) && "value" in data) {
|
if (queueResult.success === false || !Array.isArray(queueResult) || queueResult.length === 0) {
|
||||||
return data.value;
|
return;
|
||||||
|
}
|
||||||
|
for (const entry of queueResult) {
|
||||||
|
try {
|
||||||
|
const result = await kvServerProvider.saveData(entry.key, entry.data);
|
||||||
|
if (result.success !== false) {
|
||||||
|
await kvLocalProvider.removeFromSyncQueue(entry.key);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If a single item fails, stop — will retry on next online event
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_flushing = false;
|
||||||
}
|
}
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Sync manager: 向后兼容的导出 ---
|
|
||||||
export const syncManager = {
|
export const syncManager = {
|
||||||
init: initSmartSync,
|
init() {
|
||||||
destroy: destroySmartSync,
|
if (_onlineHandler) return;
|
||||||
flushNow: flushAll,
|
_onlineHandler = () => flushSyncQueue();
|
||||||
|
window.addEventListener("online", _onlineHandler);
|
||||||
|
// Attempt flush on startup in case items were queued before last exit
|
||||||
|
if (navigator.onLine) flushSyncQueue();
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
if (_onlineHandler) {
|
||||||
|
window.removeEventListener("online", _onlineHandler);
|
||||||
|
_onlineHandler = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
flushNow: flushSyncQueue,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper: check if we should use the server provider
|
// Helper: check if we should use the server provider
|
||||||
@ -76,95 +81,21 @@ export default {
|
|||||||
return kvLocalProvider.loadData(key);
|
return kvLocalProvider.loadData(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server mode: network-first with CRDT-aware cache
|
// Server mode: network-first with cache fallback
|
||||||
const rawResult = await kvServerProvider.loadData(key);
|
const result = await kvServerProvider.loadData(key);
|
||||||
// 规范化: 某些端点返回 {value: [...]} 包装格式,统一解包
|
|
||||||
const result = normalizeServerData(rawResult);
|
|
||||||
|
|
||||||
if (!isNetworkError(result)) {
|
if (!isNetworkError(result)) {
|
||||||
// 服务器返回成功或非网络错误 (如 NOT_FOUND)
|
// Success or non-network error (e.g. NOT_FOUND) — cache on success
|
||||||
if (result.success !== false) {
|
if (result.success !== false) {
|
||||||
// 有效数据 — 与本地缓存进行 CRDT 比较
|
kvLocalProvider.saveData(CACHE_PREFIX + key, result);
|
||||||
const cacheEntry = await getCacheEntry(key);
|
|
||||||
const deviceId = getDeviceId();
|
|
||||||
|
|
||||||
if (cacheEntry) {
|
|
||||||
const cachedData = normalizeServerData(cacheEntry.data);
|
|
||||||
const localDataStr = JSON.stringify(cachedData);
|
|
||||||
const serverDataStr = JSON.stringify(result);
|
|
||||||
const lastSyncedStr = JSON.stringify(normalizeServerData(cacheEntry.meta.lastSyncedData) ?? null);
|
|
||||||
|
|
||||||
if (serverDataStr === localDataStr) {
|
|
||||||
// 数据完全相同 — 无冲突,直接返回本地
|
|
||||||
return cachedData;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serverDataStr !== lastSyncedStr) {
|
|
||||||
// 服务器数据与上次同步快照不同 — 另一台设备写入了新数据
|
|
||||||
const localVc = cacheEntry.meta.vc || {};
|
|
||||||
const lastSyncedVc = cacheEntry.meta.lastSyncedVc || {};
|
|
||||||
const hasLocalChanges = (localVc[deviceId] || 0) > (lastSyncedVc[deviceId] || 0);
|
|
||||||
|
|
||||||
if (hasLocalChanges) {
|
|
||||||
// 本地也有未同步的更改 — CRDT 合并
|
|
||||||
const serverMeta = createEmptyMeta("server");
|
|
||||||
serverMeta.ts = Date.now();
|
|
||||||
|
|
||||||
const merged = mergeValues(
|
|
||||||
cachedData,
|
|
||||||
cacheEntry.meta,
|
|
||||||
result,
|
|
||||||
serverMeta,
|
|
||||||
);
|
|
||||||
|
|
||||||
await setCacheEntry(key, merged.data, merged.meta);
|
|
||||||
// 推送合并结果到服务器 (fire-and-forget)
|
|
||||||
kvServerProvider.saveData(key, merged.data);
|
|
||||||
return merged.data;
|
|
||||||
} else {
|
|
||||||
// 本地无未同步更改 — 采用服务器版本
|
|
||||||
const meta = createEmptyMeta(deviceId);
|
|
||||||
meta.ts = Date.now();
|
|
||||||
meta.lastSyncedData = result;
|
|
||||||
meta.lastSyncedTs = Date.now();
|
|
||||||
meta.lastSyncedVc = { ...meta.vc };
|
|
||||||
await setCacheEntry(key, result, meta);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 服务器数据 === 上次同步快照,但 ≠ 本地数据
|
|
||||||
// 说明本地有未推送的更改,返回本地数据
|
|
||||||
return cachedData;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 无本地缓存 — 首次获取
|
|
||||||
const meta = createEmptyMeta(deviceId);
|
|
||||||
meta.ts = Date.now();
|
|
||||||
meta.lastSyncedData = result;
|
|
||||||
meta.lastSyncedTs = Date.now();
|
|
||||||
meta.lastSyncedVc = { ...meta.vc };
|
|
||||||
await setCacheEntry(key, result, meta);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 网络错误 — 从缓存兜底
|
// Network error — try local cache
|
||||||
const cached = await getCacheEntry(key);
|
const cached = await kvLocalProvider.loadData(CACHE_PREFIX + key);
|
||||||
if (cached) {
|
if (cached.success !== false) {
|
||||||
const data = normalizeServerData(cached.data);
|
return {...cached, fromCache: true};
|
||||||
// 直接在数据对象上添加 fromCache 标记 (保持数组类型不变)
|
|
||||||
if (typeof data === "object" && data !== null) {
|
|
||||||
data.fromCache = true;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 兼容旧格式缓存
|
|
||||||
const legacyCached = await kvLocalProvider.loadData(CACHE_PREFIX + key);
|
|
||||||
if (legacyCached.success !== false) {
|
|
||||||
return { ...legacyCached, fromCache: true };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -175,45 +106,20 @@ export default {
|
|||||||
return kvLocalProvider.saveData(key, data);
|
return kvLocalProvider.saveData(key, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceId = getDeviceId();
|
// Server mode: write-through — persist locally first
|
||||||
|
await kvLocalProvider.saveData(CACHE_PREFIX + key, data);
|
||||||
// 读取现有缓存条目获取当前向量时钟
|
|
||||||
const existingEntry = await getCacheEntry(key);
|
|
||||||
let meta;
|
|
||||||
|
|
||||||
if (existingEntry) {
|
|
||||||
meta = bumpClock(existingEntry.meta, deviceId);
|
|
||||||
} else {
|
|
||||||
meta = bumpClock(createEmptyMeta(deviceId), deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write-through: 先写入本地缓存 (含 CRDT metadata)
|
|
||||||
await setCacheEntry(key, data, meta);
|
|
||||||
|
|
||||||
const result = await kvServerProvider.saveData(key, data);
|
const result = await kvServerProvider.saveData(key, data);
|
||||||
|
|
||||||
if (result.success !== false) {
|
if (result.success !== false) {
|
||||||
// 服务器保存成功 — 更新 lastSynced 快照
|
// Server save succeeded — remove from sync queue if present
|
||||||
meta.lastSyncedData = data;
|
|
||||||
meta.lastSyncedTs = Date.now();
|
|
||||||
meta.lastSyncedVc = { ...meta.vc };
|
|
||||||
await setCacheEntry(key, data, meta);
|
|
||||||
await kvLocalProvider.removeFromSyncQueue(key);
|
await kvLocalProvider.removeFromSyncQueue(key);
|
||||||
|
|
||||||
// 智能同步: 刷新其他队列中的更改
|
|
||||||
triggerSyncAfterSuccess();
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 服务器保存失败 — 加入同步队列 (含 CRDT metadata)
|
// Server save failed — queue for later sync
|
||||||
await kvLocalProvider.addToSyncQueue({
|
await kvLocalProvider.addToSyncQueue({key, data, timestamp: Date.now()});
|
||||||
key,
|
return {success: true, queuedForSync: true};
|
||||||
data,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
meta,
|
|
||||||
});
|
|
||||||
return { success: true, queuedForSync: true };
|
|
||||||
},
|
},
|
||||||
|
|
||||||
loadKeys: async (options = {}) => {
|
loadKeys: async (options = {}) => {
|
||||||
@ -234,7 +140,7 @@ export default {
|
|||||||
async getKeyCloudUrl(key, options = {}) {
|
async getKeyCloudUrl(key, options = {}) {
|
||||||
const {
|
const {
|
||||||
migrateFromLocal = true,
|
migrateFromLocal = true,
|
||||||
autoConfigureCloud = true,
|
autoConfigureCloud = true
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -311,24 +217,27 @@ export default {
|
|||||||
// 获取认证token
|
// 获取认证token
|
||||||
const authtoken = getSetting("server.kvToken");
|
const authtoken = getSetting("server.kvToken");
|
||||||
// 构建云端访问URL
|
// 构建云端访问URL
|
||||||
const url = `${serverUrl}/kv/${key}?token=${authtoken}`;
|
let url = `${serverUrl}/kv/${key}?token=${authtoken}`;
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
url,
|
url,
|
||||||
migrated,
|
migrated,
|
||||||
configured,
|
configured
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取键云端地址时出错:", error);
|
console.error('获取键云端地址时出错:', error);
|
||||||
return formatError(
|
return formatError(
|
||||||
error.message || "获取键云端地址失败",
|
error.message || "获取键云端地址失败",
|
||||||
"CLOUD_URL_ERROR",
|
"CLOUD_URL_ERROR"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const ErrorCodes = {
|
export const ErrorCodes = {
|
||||||
NOT_FOUND: "数据不存在",
|
NOT_FOUND: "数据不存在",
|
||||||
NETWORK_ERROR: "网络连接失败",
|
NETWORK_ERROR: "网络连接失败",
|
||||||
|
|||||||
@ -155,32 +155,4 @@ export const kvLocalProvider = {
|
|||||||
return formatError("删除同步队列项失败:" + error);
|
return formatError("删除同步队列项失败:" + error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// --- Prefix-based operations for cache management ---
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除指定前缀的所有键
|
|
||||||
* @param {string} prefix — 键名前缀
|
|
||||||
* @returns {Promise<number>} 删除的键数
|
|
||||||
*/
|
|
||||||
async deleteByPrefix(prefix) {
|
|
||||||
try {
|
|
||||||
const db = await initDB();
|
|
||||||
const tx = db.transaction("kv", "readwrite");
|
|
||||||
const store = tx.objectStore("kv");
|
|
||||||
const allKeys = await store.getAllKeys();
|
|
||||||
let deleted = 0;
|
|
||||||
for (const key of allKeys) {
|
|
||||||
if (key.startsWith(prefix)) {
|
|
||||||
await store.delete(key);
|
|
||||||
deleted++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await tx.done;
|
|
||||||
return deleted;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("kvLocalProvider.deleteByPrefix 失败:", error);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,148 +0,0 @@
|
|||||||
/**
|
|
||||||
* Smart Sync Manager — 增强同步管理器
|
|
||||||
*
|
|
||||||
* 替代 dataProvider.js 中内联的 flushSyncQueue。
|
|
||||||
* 功能:
|
|
||||||
* - 网络恢复时自动刷新同步队列
|
|
||||||
* - 任意云端操作成功后,若队列有未同步条目,自动触发全部同步
|
|
||||||
* - 同步成功后更新本地缓存的 lastSyncedData/lastSyncedVc 快照
|
|
||||||
* - 启动时清理过期缓存
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { kvLocalProvider } from "./providers/kvLocalProvider";
|
|
||||||
import { kvServerProvider } from "./providers/kvServerProvider";
|
|
||||||
import { cleanupExpiredEntries, getCacheEntry, setCacheEntry } from "./cacheManager";
|
|
||||||
import { createEmptyMeta } from "./crdtEngine";
|
|
||||||
import { getSetting } from "./settings";
|
|
||||||
|
|
||||||
let _onlineHandler = null;
|
|
||||||
let _flushing = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新同步队列 — 逐条重放,失败则停止
|
|
||||||
* @param {Object} [options]
|
|
||||||
* @param {boolean} [options.silent=false] — 静默模式,不输出日志
|
|
||||||
* @returns {Promise<{ synced: number, failed: number }>}
|
|
||||||
*/
|
|
||||||
export async function flushAll(options = {}) {
|
|
||||||
if (_flushing) return { synced: 0, failed: 0 };
|
|
||||||
_flushing = true;
|
|
||||||
|
|
||||||
let synced = 0;
|
|
||||||
let failed = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const queueResult = await kvLocalProvider.getSyncQueue();
|
|
||||||
|
|
||||||
if (queueResult.success === false || !Array.isArray(queueResult) || queueResult.length === 0) {
|
|
||||||
return { synced: 0, failed: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of queueResult) {
|
|
||||||
try {
|
|
||||||
const result = await kvServerProvider.saveData(entry.key, entry.data);
|
|
||||||
|
|
||||||
if (result.success !== false) {
|
|
||||||
// 同步成功 — 从队列移除
|
|
||||||
await kvLocalProvider.removeFromSyncQueue(entry.key);
|
|
||||||
|
|
||||||
// 更新本地缓存的 lastSynced 快照
|
|
||||||
if (entry.meta) {
|
|
||||||
const deviceId = getSetting("device.uuid") || "unknown";
|
|
||||||
const existingEntry = await getCacheEntry(entry.key);
|
|
||||||
|
|
||||||
if (existingEntry) {
|
|
||||||
const meta = { ...existingEntry.meta };
|
|
||||||
meta.lastSyncedData = entry.data;
|
|
||||||
meta.lastSyncedTs = Date.now();
|
|
||||||
meta.lastSyncedVc = { ...meta.vc };
|
|
||||||
await setCacheEntry(entry.key, existingEntry.data, meta);
|
|
||||||
} else {
|
|
||||||
// 缓存条目已过期或不存在,重建
|
|
||||||
const meta = entry.meta || createEmptyMeta(deviceId);
|
|
||||||
meta.lastSyncedData = entry.data;
|
|
||||||
meta.lastSyncedTs = Date.now();
|
|
||||||
meta.lastSyncedVc = { ...meta.vc };
|
|
||||||
await setCacheEntry(entry.key, entry.data, meta);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
synced++;
|
|
||||||
} else {
|
|
||||||
// 服务器返回错误 (非网络错误) — 跳过此项继续
|
|
||||||
failed++;
|
|
||||||
if (!options.silent) {
|
|
||||||
console.warn(`smartSync: 跳过 key=${entry.key}, 服务器错误:`, result.error?.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// 网络错误 — 停止处理,下次重试
|
|
||||||
failed++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理过期缓存
|
|
||||||
if (synced > 0) {
|
|
||||||
cleanupExpiredEntries();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
_flushing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options.silent && synced > 0) {
|
|
||||||
console.log(`smartSync: 同步完成,成功 ${synced} 条,失败 ${failed} 条`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { synced, failed };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 触发条件同步 — 任意云端操作成功后调用
|
|
||||||
* 仅在队列中有条目时执行
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
export async function triggerSyncAfterSuccess() {
|
|
||||||
try {
|
|
||||||
const queueResult = await kvLocalProvider.getSyncQueue();
|
|
||||||
if (queueResult.success === false || !Array.isArray(queueResult)) return;
|
|
||||||
if (queueResult.length === 0) return;
|
|
||||||
|
|
||||||
// 有未同步条目,触发全部同步
|
|
||||||
flushAll({ silent: true });
|
|
||||||
} catch {
|
|
||||||
// 静默失败
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化智能同步管理器
|
|
||||||
* - 注册 online 事件监听
|
|
||||||
* - 启动时清理过期缓存
|
|
||||||
* - 启动时尝试刷新队列
|
|
||||||
*/
|
|
||||||
export function initSmartSync() {
|
|
||||||
if (_onlineHandler) return;
|
|
||||||
|
|
||||||
_onlineHandler = () => flushAll();
|
|
||||||
window.addEventListener("online", _onlineHandler);
|
|
||||||
|
|
||||||
// 启动时清理过期缓存
|
|
||||||
cleanupExpiredEntries();
|
|
||||||
|
|
||||||
// 启动时尝试刷新队列
|
|
||||||
if (navigator.onLine) {
|
|
||||||
flushAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 销毁智能同步管理器
|
|
||||||
* 移除事件监听
|
|
||||||
*/
|
|
||||||
export function destroySmartSync() {
|
|
||||||
if (_onlineHandler) {
|
|
||||||
window.removeEventListener("online", _onlineHandler);
|
|
||||||
_onlineHandler = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user