Compare commits

..

7 Commits

11 changed files with 373 additions and 140 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@kevisual/api", "name": "@kevisual/api",
"version": "0.0.58", "version": "0.0.64",
"description": "", "description": "",
"main": "mod.ts", "main": "mod.ts",
"scripts": { "scripts": {
@@ -18,18 +18,18 @@
"keywords": [], "keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)", "author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT", "license": "MIT",
"packageManager": "pnpm@10.30.1", "packageManager": "pnpm@10.32.1",
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@kevisual/cache": "^0.0.5", "@kevisual/cache": "^0.0.5",
"@kevisual/code-builder": "^0.0.6", "@kevisual/code-builder": "^0.0.6",
"@kevisual/query": "^0.0.49", "@kevisual/query": "^0.0.53",
"@kevisual/remote-app": "^0.0.4", "@kevisual/remote-app": "^0.0.6",
"@kevisual/router": "^0.0.83", "@kevisual/router": "^0.1.1",
"@kevisual/types": "^0.0.12", "@kevisual/types": "^0.0.12",
"@kevisual/use-config": "^1.0.30", "@kevisual/use-config": "^1.0.30",
"@types/bun": "^1.3.9", "@types/bun": "^1.3.10",
"@types/node": "^25.3.0", "@types/node": "^25.4.0",
"@types/spark-md5": "^3.0.5", "@types/spark-md5": "^3.0.5",
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"fast-glob": "^3.3.3", "fast-glob": "^3.3.3",
@@ -37,10 +37,10 @@
}, },
"dependencies": { "dependencies": {
"@kevisual/context": "^0.0.8", "@kevisual/context": "^0.0.8",
"@kevisual/js-filter": "^0.0.5", "@kevisual/js-filter": "^0.0.6",
"@kevisual/load": "^0.0.6", "@kevisual/load": "^0.0.6",
"@paralleldrive/cuid2": "^3.3.0", "@paralleldrive/cuid2": "^3.3.0",
"es-toolkit": "^1.44.0", "es-toolkit": "^1.45.1",
"eventemitter3": "^5.0.4", "eventemitter3": "^5.0.4",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"nanoid": "^5.1.6", "nanoid": "^5.1.6",

98
pnpm-lock.yaml generated
View File

@@ -9,11 +9,11 @@ importers:
.: .:
dependencies: dependencies:
'@kevisual/context': '@kevisual/context':
specifier: ^0.0.8
version: 0.0.8
'@kevisual/js-filter':
specifier: ^0.0.6 specifier: ^0.0.6
version: 0.0.6 version: 0.0.6
'@kevisual/js-filter':
specifier: ^0.0.5
version: 0.0.5
'@kevisual/load': '@kevisual/load':
specifier: ^0.0.6 specifier: ^0.0.6
version: 0.0.6 version: 0.0.6
@@ -21,8 +21,8 @@ importers:
specifier: ^3.3.0 specifier: ^3.3.0
version: 3.3.0 version: 3.3.0
es-toolkit: es-toolkit:
specifier: ^1.44.0 specifier: ^1.45.1
version: 1.44.0 version: 1.45.1
eventemitter3: eventemitter3:
specifier: ^5.0.4 specifier: ^5.0.4
version: 5.0.4 version: 5.0.4
@@ -52,14 +52,14 @@ importers:
specifier: ^0.0.6 specifier: ^0.0.6
version: 0.0.6 version: 0.0.6
'@kevisual/query': '@kevisual/query':
specifier: ^0.0.47 specifier: ^0.0.53
version: 0.0.47 version: 0.0.53
'@kevisual/remote-app': '@kevisual/remote-app':
specifier: ^0.0.4 specifier: ^0.0.6
version: 0.0.4 version: 0.0.6
'@kevisual/router': '@kevisual/router':
specifier: ^0.0.75 specifier: ^0.1.1
version: 0.0.75 version: 0.1.1
'@kevisual/types': '@kevisual/types':
specifier: ^0.0.12 specifier: ^0.0.12
version: 0.0.12 version: 0.0.12
@@ -67,11 +67,11 @@ importers:
specifier: ^1.0.30 specifier: ^1.0.30
version: 1.0.30(dotenv@17.3.1) version: 1.0.30(dotenv@17.3.1)
'@types/bun': '@types/bun':
specifier: ^1.3.9 specifier: ^1.3.10
version: 1.3.9 version: 1.3.10
'@types/node': '@types/node':
specifier: ^25.2.3 specifier: ^25.4.0
version: 25.2.3 version: 25.4.0
'@types/spark-md5': '@types/spark-md5':
specifier: ^3.0.5 specifier: ^3.0.5
version: 3.0.5 version: 3.0.5
@@ -114,11 +114,11 @@ packages:
resolution: {integrity: sha512-0aqATB31/yw4k4s5/xKnfr4DKbUnx8e3Z3BmKbiXTrc+CqWiWTdlGe9bKI9dZ2Df+xNp6g11W4xM2NICNyyCCw==} resolution: {integrity: sha512-0aqATB31/yw4k4s5/xKnfr4DKbUnx8e3Z3BmKbiXTrc+CqWiWTdlGe9bKI9dZ2Df+xNp6g11W4xM2NICNyyCCw==}
hasBin: true hasBin: true
'@kevisual/context@0.0.6': '@kevisual/context@0.0.8':
resolution: {integrity: sha512-w7HBOuO3JH37n6xT6W3FD7ykqHTwtyxOQzTzfEcKDCbsvGB1wVreSxFm2bvoFnnFLuxT/5QMpKlnPrwvmcTGnw==} resolution: {integrity: sha512-DTJpyHI34NE76B7g6f+QlIqiCCyqI2qkBMQE736dzeRDGxOjnbe2iQY9W+Rt2PE6kmymM3qyOmSfNovyWyWrkA==}
'@kevisual/js-filter@0.0.5': '@kevisual/js-filter@0.0.6':
resolution: {integrity: sha512-+S+Sf3K/aP6XtZI2s7TgKOr35UuvUvtpJ9YDW30a+mY0/N8gRuzyKhieBzQN7Ykayzz70uoMavBXut2rUlLgzw==} resolution: {integrity: sha512-FcbOsmS1inhwrfgXMM/XLFTGTHUxBCss32JEMYdEFWQDYCar5rN8cxD1W8FuKDTVRlpA+zBpQ/BE6XT4UaeljA==}
'@kevisual/load@0.0.6': '@kevisual/load@0.0.6':
resolution: {integrity: sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA==} resolution: {integrity: sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA==}
@@ -126,17 +126,17 @@ packages:
'@kevisual/query@0.0.18': '@kevisual/query@0.0.18':
resolution: {integrity: sha512-I2vHTu0I6AyD9PJyr+vxyp9jIJ6rd2EZqLVHTv/+zrVKVc2SS76Tg7aGNkmAFqqLSCB8kLLsmMGtSJU1Qb8VVg==} resolution: {integrity: sha512-I2vHTu0I6AyD9PJyr+vxyp9jIJ6rd2EZqLVHTv/+zrVKVc2SS76Tg7aGNkmAFqqLSCB8kLLsmMGtSJU1Qb8VVg==}
'@kevisual/query@0.0.47': '@kevisual/query@0.0.53':
resolution: {integrity: sha512-ZR7WXeDDGUSzBtcGVU3J173sA0hCqrGTw5ybGbdNGlM0VyJV/XQIovCcSoZh1YpnciLRRqJvzXUgTnCkam+M3g==} resolution: {integrity: sha512-PAhpCLBr0emz0lGNlTVHMbJiC5wrtGLbInPddRzgKE35fiyNt+SWSsUWABiD0DeNrLN/OxWyAFobt880Z/e5MQ==}
'@kevisual/remote-app@0.0.4': '@kevisual/remote-app@0.0.6':
resolution: {integrity: sha512-2yIlWY98pLCcxG+DJsqXXkd5YYEgymuOsyElH+31AoEPb7mlNREnYS81zN0KM9nvdSmU2G51vV4UVirJlYBZCQ==} resolution: {integrity: sha512-yc3BKAhtY+SzrvQSebeyR/QR93nPctndNMnW6ne1YPK+Kfpuf8gi7W4zlg18EJh7FEpDuDVHKqVp1klsWjESqQ==}
'@kevisual/router@0.0.20': '@kevisual/router@0.0.20':
resolution: {integrity: sha512-uSwDYWh+kvAu6i0m0SJVgcLR/CYz7WvIWGz0nSF8Vg6smJuAgI+laHR4ESO8Fbz+Xn8bPHuSwmM//HHLMLx2FA==} resolution: {integrity: sha512-uSwDYWh+kvAu6i0m0SJVgcLR/CYz7WvIWGz0nSF8Vg6smJuAgI+laHR4ESO8Fbz+Xn8bPHuSwmM//HHLMLx2FA==}
'@kevisual/router@0.0.75': '@kevisual/router@0.1.1':
resolution: {integrity: sha512-WBDRKMjNYTP7ymkUUtiQwWYIcqnc+TGo3rFuRze8ovYV2UN5cQxIkIfsDbgWOdV1/v9b57gtiJvJRqWjCBWKRg==} resolution: {integrity: sha512-+uaJc+Bf/T1mfxyfy9PmwuxJGPOLhVqrmsli2xUPqkkFvizrFIGB1vBTITuo5XP/FnwGqxgbjsitG57AMubm3w==}
'@kevisual/types@0.0.10': '@kevisual/types@0.0.10':
resolution: {integrity: sha512-Q73uzzjk9UidumnmCvOpgzqDDvQxsblz22bIFuoiioUFJWwaparx8bpd8ArRyFojicYL1YJoFDzDZ9j9NN8grA==} resolution: {integrity: sha512-Q73uzzjk9UidumnmCvOpgzqDDvQxsblz22bIFuoiioUFJWwaparx8bpd8ArRyFojicYL1YJoFDzDZ9j9NN8grA==}
@@ -173,8 +173,8 @@ packages:
resolution: {integrity: sha512-OqiFvSOF0dBSesELYY2CAMa4YINvlLpvKOz/rv6NeZEqiyttlHgv98Juwv4Ch+GrEV7IZ8jfI2VcEoYUjXXCjw==} resolution: {integrity: sha512-OqiFvSOF0dBSesELYY2CAMa4YINvlLpvKOz/rv6NeZEqiyttlHgv98Juwv4Ch+GrEV7IZ8jfI2VcEoYUjXXCjw==}
hasBin: true hasBin: true
'@types/bun@1.3.9': '@types/bun@1.3.10':
resolution: {integrity: sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw==} resolution: {integrity: sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ==}
'@types/node-fetch@2.6.12': '@types/node-fetch@2.6.12':
resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
@@ -188,8 +188,8 @@ packages:
'@types/node@22.15.27': '@types/node@22.15.27':
resolution: {integrity: sha512-5fF+eu5mwihV2BeVtX5vijhdaZOfkQTATrePEaXTcKqI16LhJ7gi2/Vhd9OZM0UojcdmiOCVg5rrax+i1MdoQQ==} resolution: {integrity: sha512-5fF+eu5mwihV2BeVtX5vijhdaZOfkQTATrePEaXTcKqI16LhJ7gi2/Vhd9OZM0UojcdmiOCVg5rrax+i1MdoQQ==}
'@types/node@25.2.3': '@types/node@25.4.0':
resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==} resolution: {integrity: sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==}
'@types/spark-md5@3.0.5': '@types/spark-md5@3.0.5':
resolution: {integrity: sha512-lWf05dnD42DLVKQJZrDHtWFidcLrHuip01CtnC2/S6AMhX4t9ZlEUj4iuRlAnts0PQk7KESOqKxeGE/b6sIPGg==} resolution: {integrity: sha512-lWf05dnD42DLVKQJZrDHtWFidcLrHuip01CtnC2/S6AMhX4t9ZlEUj4iuRlAnts0PQk7KESOqKxeGE/b6sIPGg==}
@@ -212,8 +212,8 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'} engines: {node: '>=8'}
bun-types@1.3.9: bun-types@1.3.10:
resolution: {integrity: sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg==} resolution: {integrity: sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg==}
call-bind-apply-helpers@1.0.2: call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
@@ -254,8 +254,8 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
es-toolkit@1.44.0: es-toolkit@1.45.1:
resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==} resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==}
event-target-shim@5.0.1: event-target-shim@5.0.1:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
@@ -460,8 +460,8 @@ packages:
undici-types@6.21.0: undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
undici-types@7.16.0: undici-types@7.18.2:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
web-streams-polyfill@4.0.0-beta.3: web-streams-polyfill@4.0.0-beta.3:
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
@@ -501,9 +501,9 @@ snapshots:
'@kevisual/code-builder@0.0.6': {} '@kevisual/code-builder@0.0.6': {}
'@kevisual/context@0.0.6': {} '@kevisual/context@0.0.8': {}
'@kevisual/js-filter@0.0.5': {} '@kevisual/js-filter@0.0.6': {}
'@kevisual/load@0.0.6': '@kevisual/load@0.0.6':
dependencies: dependencies:
@@ -517,18 +517,18 @@ snapshots:
- ws - ws
- zod - zod
'@kevisual/query@0.0.47': {} '@kevisual/query@0.0.53': {}
'@kevisual/remote-app@0.0.4': {} '@kevisual/remote-app@0.0.6': {}
'@kevisual/router@0.0.20': '@kevisual/router@0.0.20':
dependencies: dependencies:
path-to-regexp: 8.2.0 path-to-regexp: 8.2.0
selfsigned: 2.4.1 selfsigned: 2.4.1
'@kevisual/router@0.0.75': '@kevisual/router@0.1.1':
dependencies: dependencies:
es-toolkit: 1.44.0 es-toolkit: 1.45.1
'@kevisual/types@0.0.10': {} '@kevisual/types@0.0.10': {}
@@ -561,9 +561,9 @@ snapshots:
bignumber.js: 9.3.1 bignumber.js: 9.3.1
error-causes: 3.0.2 error-causes: 3.0.2
'@types/bun@1.3.9': '@types/bun@1.3.10':
dependencies: dependencies:
bun-types: 1.3.9 bun-types: 1.3.10
'@types/node-fetch@2.6.12': '@types/node-fetch@2.6.12':
dependencies: dependencies:
@@ -582,9 +582,9 @@ snapshots:
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
'@types/node@25.2.3': '@types/node@25.4.0':
dependencies: dependencies:
undici-types: 7.16.0 undici-types: 7.18.2
'@types/spark-md5@3.0.5': {} '@types/spark-md5@3.0.5': {}
@@ -604,9 +604,9 @@ snapshots:
dependencies: dependencies:
fill-range: 7.1.1 fill-range: 7.1.1
bun-types@1.3.9: bun-types@1.3.10:
dependencies: dependencies:
'@types/node': 25.2.3 '@types/node': 25.4.0
call-bind-apply-helpers@1.0.2: call-bind-apply-helpers@1.0.2:
dependencies: dependencies:
@@ -644,7 +644,7 @@ snapshots:
has-tostringtag: 1.0.2 has-tostringtag: 1.0.2
hasown: 2.0.2 hasown: 2.0.2
es-toolkit@1.44.0: {} es-toolkit@1.45.1: {}
event-target-shim@5.0.1: {} event-target-shim@5.0.1: {}
@@ -818,7 +818,7 @@ snapshots:
undici-types@6.21.0: {} undici-types@6.21.0: {}
undici-types@7.16.0: {} undici-types@7.18.2: {}
web-streams-polyfill@4.0.0-beta.3: {} web-streams-polyfill@4.0.0-beta.3: {}

View File

@@ -0,0 +1,123 @@
import { createStore, UseStore, get, set, del, clear, keys, values, entries, update, setMany, getMany, delMany } from 'idb-keyval';
/**
* 缓存存储选项
*/
export type CacheStoreOpts = {
/**
* 数据库名称
*/
dbName?: string;
/**
* 存储空间名称
*/
storeName?: string;
};
export class BaseCacheStore {
store: UseStore;
constructor(opts?: CacheStoreOpts) {
this.store = createStore(opts?.dbName || 'default-db', opts?.storeName || 'cache-store');
}
async get(key: string) {
return get(key, this.store);
}
async set(key: string, value: any) {
return set(key, value, this.store);
}
async del(key: string) {
return del(key, this.store);
}
async clear() {
return clear(this.store);
}
async keys() {
return keys(this.store);
}
async values() {
return values(this.store);
}
async entries() {
return entries(this.store);
}
async update(key: string, updater: (value: any) => any) {
return update(key, updater, this.store);
}
async setMany(entries: [string, any][]) {
return setMany(entries, this.store);
}
async getMany(keys: string[]) {
return getMany(keys, this.store);
}
async delMany(keys: string[]) {
return delMany(keys, this.store);
}
}
/**
* 缓存存储
*/
export class CacheStore extends BaseCacheStore {
constructor(opts?: CacheStoreOpts) {
super(opts);
}
async getData<T = any>(key: string) {
const data = await this.get(key);
return data.data as T;
}
async setData(key: string, data: any) {
return this.set(key, data);
}
/**
* 获取缓存数据,并检查是否过期
* @param key 缓存键
* @returns 缓存数据
*/
async getCheckData<T = any>(key: string) {
const data = await this.get(key);
if (data.expireTime && data.expireTime < Date.now()) {
await super.del(key);
return null;
}
return data.data as T;
}
/**
* 设置缓存数据,并检查是否过期
* @param key 缓存键
* @param data 缓存数据
* @param opts 缓存选项
* @returns 缓存数据
*/
async setCheckData(key: string, data: any, opts?: { expireTime?: number; updatedAt?: number }) {
const now = Date.now();
const expireTime = now + (opts?.expireTime || 1000 * 60 * 60 * 24 * 10);
const newData = {
data,
updatedAt: opts?.updatedAt || Date.now(),
expireTime,
};
await this.set(key, newData);
return data;
}
async checkNew(key: string, data: any): Promise<boolean> {
const existing = await this.get(key);
if (!existing) {
return true;
}
if (!data?.updatedAt) {
return false;
}
const updatedAt = new Date(data.updatedAt).getTime();
if (isNaN(updatedAt)) {
return false;
}
return updatedAt > existing.updatedAt;
}
/**
* 删除缓存数据
* @param key 缓存键
* @returns 缓存数据
*/
async delCheckData(key: string) {
return this.del(key);
}
}

View File

@@ -0,0 +1,29 @@
export { CacheStore, BaseCacheStore } from './cache-store.ts'
import { CacheStore } from './cache-store.ts'
/**
* 一个简单的缓存类,用于存储字符串。
* 对数据进行添加对比内容。
*/
export class MyCache<T = any> extends CacheStore {
key: string;
constructor(opts?: { key?: string }) {
const { key, ...rest } = opts || {};
super(rest);
this.key = key || 'my-cache';
}
async getData<U = T>(key: string = this.key): Promise<U> {
return super.getCheckData<U>(key) as any;
}
/**
* 设置缓存数据默认过期时间为10天
* @param data
* @param opts
*/
async setData<U = T>(data: U, opts?: { expireTime?: number, updatedAt?: number }) {
super.setCheckData(this.key, data, opts);
}
async del(): Promise<void> {
await super.del(this.key);
}
}

View File

@@ -90,6 +90,15 @@ export type LoginCacheStoreOpts<T extends Cache = Cache> = {
name: string; name: string;
cache: T; cache: T;
}; };
const defaultCacheData: CacheLogin = {
loginUsers: [],
user: undefined,
id: undefined,
accessToken: undefined,
refreshToken: undefined,
accessTokenExpiresIn: undefined,
createdAt: undefined,
}
export class LoginCacheStore<T extends Cache = Cache> implements CacheStore<T> { export class LoginCacheStore<T extends Cache = Cache> implements CacheStore<T> {
cache: T; cache: T;
name: string; name: string;
@@ -100,12 +109,16 @@ export class LoginCacheStore<T extends Cache = Cache> implements CacheStore<T> {
} }
// @ts-ignore // @ts-ignore
this.cache = opts.cache; this.cache = opts.cache;
this.cacheData = { this.cacheData = { ...defaultCacheData };
loginUsers: [], this.name = opts.name;
user: undefined, }
id: undefined, /**
accessToken: undefined, * 设置缓存
refreshToken: undefined, * @param key
* @param value
* @returns
accessTokenExpiresIn: undefined,
createdAt: undefined,
}; };
this.name = opts.name; this.name = opts.name;
} }
@@ -125,13 +138,7 @@ export class LoginCacheStore<T extends Cache = Cache> implements CacheStore<T> {
*/ */
async delValue() { async delValue() {
await this.cache.del(); await this.cache.del();
this.cacheData = { this.cacheData = { ...defaultCacheData };
loginUsers: [],
user: undefined,
id: undefined,
accessToken: undefined,
refreshToken: undefined,
};
} }
getValue(): Promise<CacheLogin> { getValue(): Promise<CacheLogin> {
return this.cache.get(this.name); return this.cache.get(this.name);
@@ -139,44 +146,38 @@ export class LoginCacheStore<T extends Cache = Cache> implements CacheStore<T> {
/** /**
* 初始化,设置默认值 * 初始化,设置默认值
*/ */
async init() { async init(): Promise<CacheLogin> {
const defaultData: CacheLogin = { const defaultData: CacheLogin = { ...this.cacheData };
loginUsers: [], return new Promise(async (resolve) => {
user: undefined, if (this.cache.init) {
id: undefined, try {
accessToken: undefined, const cacheData = await this.cache.init();
refreshToken: undefined, this.cacheData = cacheData || defaultData;
accessTokenExpiresIn: undefined, } catch (error) {
createdAt: undefined, console.log('cacheInit error', error);
}; }
if (this.cache.init) { } else {
try { this.cacheData = (await this.getValue()) || defaultData;
const cacheData = await this.cache.init();
this.cacheData = cacheData || defaultData;
} catch (error) {
console.log('cacheInit error', error);
} }
} else { resolve(this.cacheData);
this.cacheData = (await this.getValue()) || defaultData; });
}
return this.cacheData;
} }
/** /**
* 设置当前用户 * 设置当前用户
* @param user * @param user
*/ */
async setLoginUser(user: CacheLoginUser) { async setLoginUser(loginUser: CacheLoginUser) {
const has = this.cacheData.loginUsers.find((u) => u.id === user.id); const has = this.cacheData.loginUsers.find((u) => u.id === loginUser.id);
if (has) { if (has) {
this.cacheData.loginUsers = this.cacheData?.loginUsers?.filter((u) => u?.id && u.id !== user.id); this.cacheData.loginUsers = this.cacheData?.loginUsers?.filter((u) => u?.id && u.id !== loginUser.id);
} }
this.cacheData.loginUsers.push(user); this.cacheData.loginUsers.push(loginUser);
this.cacheData.user = user.user; this.cacheData.user = loginUser.user;
this.cacheData.id = user.id; this.cacheData.id = loginUser.id;
this.cacheData.accessToken = user.accessToken; this.cacheData.accessToken = loginUser.accessToken;
this.cacheData.refreshToken = user.refreshToken; this.cacheData.refreshToken = loginUser.refreshToken;
this.cacheData.accessTokenExpiresIn = user.accessTokenExpiresIn; this.cacheData.accessTokenExpiresIn = loginUser.accessTokenExpiresIn;
this.cacheData.createdAt = user.createdAt; this.cacheData.createdAt = loginUser.createdAt;
await this.setValue(this.cacheData); await this.setValue(this.cacheData);
} }
@@ -214,22 +215,22 @@ export class LoginCacheStore<T extends Cache = Cache> implements CacheStore<T> {
if (has) { if (has) {
this.cacheData.loginUsers = this.cacheData?.loginUsers?.filter((u) => u?.id && u.id !== user.id); this.cacheData.loginUsers = this.cacheData?.loginUsers?.filter((u) => u?.id && u.id !== user.id);
} }
this.cacheData.user = undefined; const hasOther = this.cacheData.loginUsers.length > 0;
this.cacheData.id = undefined; const current = this.cacheData.loginUsers[this.cacheData.loginUsers.length - 1];
this.cacheData.accessToken = undefined; if (hasOther && current) {
this.cacheData.refreshToken = undefined; this.cacheData.user = current.user;
this.cacheData.accessTokenExpiresIn = undefined; this.cacheData.id = current.id;
this.cacheData.createdAt = undefined; this.cacheData.accessToken = current.accessToken;
this.cacheData.refreshToken = current.refreshToken;
this.cacheData.accessTokenExpiresIn = current.accessTokenExpiresIn;
this.cacheData.createdAt = current.createdAt;
} else {
this.cacheData = { ...defaultCacheData };
}
await this.setValue(this.cacheData); await this.setValue(this.cacheData);
} }
async clearAll() { async clearAll() {
this.cacheData.loginUsers = []; this.cacheData = { ...defaultCacheData };
this.cacheData.user = undefined;
this.cacheData.id = undefined;
this.cacheData.accessToken = undefined;
this.cacheData.refreshToken = undefined;
this.cacheData.accessTokenExpiresIn = undefined;
this.cacheData.createdAt = undefined;
await this.setValue(this.cacheData); await this.setValue(this.cacheData);
} }
} }

View File

@@ -1,5 +1,5 @@
import { QueryLogin, QueryLoginOpts } from './query-login.ts'; import { QueryLogin, QueryLoginOpts } from './query-login.ts';
import { MyCache } from '@kevisual/cache'; import { MyCache } from './browser-cache/cache.ts';
type QueryLoginNodeOptsWithoutCache = Omit<QueryLoginOpts, 'cache'>; type QueryLoginNodeOptsWithoutCache = Omit<QueryLoginOpts, 'cache'>;
export class QueryLoginBrowser extends QueryLogin { export class QueryLoginBrowser extends QueryLogin {

View File

@@ -3,6 +3,7 @@ import type { Result, DataOpts } from '@kevisual/query/query';
import { LoginCacheStore, CacheStore, User } from './login-cache.ts'; import { LoginCacheStore, CacheStore, User } from './login-cache.ts';
import { Cache } from './login-cache.ts'; import { Cache } from './login-cache.ts';
import { BaseLoad } from '@kevisual/load'; import { BaseLoad } from '@kevisual/load';
import { EventEmitter } from 'eventemitter3'
export type QueryLoginOpts<T extends Cache = Cache> = { export type QueryLoginOpts<T extends Cache = Cache> = {
query?: Query; query?: Query;
isBrowser?: boolean; isBrowser?: boolean;
@@ -26,9 +27,11 @@ export class QueryLogin<T extends Cache = Cache> extends BaseQuery {
*/ */
cacheStore: CacheStore<T>; cacheStore: CacheStore<T>;
isBrowser: boolean; isBrowser: boolean;
load?: boolean;
storage: Storage; storage: Storage;
load: boolean = false;
status: 'init' | 'logining' | 'loginSuccess' | 'loginError' = 'init';
onLoad?: () => void; onLoad?: () => void;
emitter = new EventEmitter();
constructor(opts?: QueryLoginOpts<T>) { constructor(opts?: QueryLoginOpts<T>) {
super({ super({
@@ -42,14 +45,29 @@ export class QueryLogin<T extends Cache = Cache> extends BaseQuery {
if (!this.storage) { if (!this.storage) {
throw new Error('storage is required'); throw new Error('storage is required');
} }
this.cacheStore.init().then(() => {
this.onLoad?.();
this.load = true;
this.emitter.emit('load');
});
} }
setQuery(query: Query) { setQuery(query: Query) {
this.query = query; this.query = query;
} }
private async init() { async init() {
await this.cacheStore.init(); if (this.load) {
this.load = true; return this.cacheStore.cacheData;
this.onLoad?.(); }
return new Promise(async (resolve) => {
const timer = setTimeout(() => {
resolve(this.cacheStore.cacheData);
}, 1000 * 20); // 20秒超时避免一直等待
const listener = () => {
clearTimeout(timer);
resolve(this.cacheStore.cacheData);
}
this.emitter.once('load', listener);
});
} }
async post<T = any>(data: any, opts?: DataOpts) { async post<T = any>(data: any, opts?: DataOpts) {
try { try {
@@ -179,8 +197,8 @@ export class QueryLogin<T extends Cache = Cache> extends BaseQuery {
* @param refreshToken 刷新token如果不传则从缓存中获取 * @param refreshToken 刷新token如果不传则从缓存中获取
* @returns * @returns
*/ */
async refreshLoginUser(refreshToken?: string) { async refreshLoginUser(opts?: { refreshToken?: string, accessToken?: string }) {
const res = await this.queryRefreshToken(refreshToken); const res = await this.queryRefreshToken(opts);
if (res.code === 200) { if (res.code === 200) {
const { accessToken, refreshToken, accessTokenExpiresIn } = res?.data || {}; const { accessToken, refreshToken, accessTokenExpiresIn } = res?.data || {};
this.storage.setItem('token', accessToken || ''); this.storage.setItem('token', accessToken || '');
@@ -193,9 +211,17 @@ export class QueryLogin<T extends Cache = Cache> extends BaseQuery {
* @param refreshToken * @param refreshToken
* @returns * @returns
*/ */
async queryRefreshToken(refreshToken?: string) { async queryRefreshToken(opts?: { refreshToken?: string, accessToken?: string }) {
const _refreshToken = refreshToken || (await this.cacheStore.getRefreshToken()); const refreshToken = opts?.refreshToken;
let data = { refreshToken: _refreshToken }; let accessToken = opts?.accessToken;
const _refreshToken = refreshToken ?? (await this.cacheStore.getRefreshToken());
let data: any = {};
if (accessToken) {
data.accessToken = accessToken;
}
if (_refreshToken) {
data.refreshToken = _refreshToken;
}
if (!_refreshToken) { if (!_refreshToken) {
await this.cacheStore.clearCurrentUser(); await this.cacheStore.clearCurrentUser();
return { return {
@@ -226,7 +252,7 @@ export class QueryLogin<T extends Cache = Cache> extends BaseQuery {
if (response?.code === 401) { if (response?.code === 401) {
const hasRefreshToken = await that.cacheStore.getRefreshToken(); const hasRefreshToken = await that.cacheStore.getRefreshToken();
if (hasRefreshToken) { if (hasRefreshToken) {
const res = await that.queryRefreshToken(hasRefreshToken); const res = await that.queryRefreshToken({ refreshToken: hasRefreshToken });
if (res.code === 200) { if (res.code === 200) {
const { accessToken, refreshToken, accessTokenExpiresIn } = res?.data || {}; const { accessToken, refreshToken, accessTokenExpiresIn } = res?.data || {};
that.storage.setItem('token', accessToken || ''); that.storage.setItem('token', accessToken || '');
@@ -352,6 +378,7 @@ export class QueryLogin<T extends Cache = Cache> extends BaseQuery {
} }
const isExpired = await this.cacheStore.getIsExpired(); const isExpired = await this.cacheStore.getIsExpired();
if (isExpired) { if (isExpired) {
console.log('token过期正在刷新token', this.cacheStore.cacheData);
const res = await this.refreshLoginUser() const res = await this.refreshLoginUser()
if (res.code === 200) { if (res.code === 200) {
// 刷新成功返回新的token // 刷新成功返回新的token

View File

@@ -0,0 +1,10 @@
const cacheData = {
accessTokenExpiresIn: 604800,
createdAt: 1771926793545
};
const expiresIn = cacheData.createdAt + cacheData.accessTokenExpiresIn * 1000;
console.log('expiresIn', expiresIn);
const now = Date.now();
console.log('now', now);
console.log('isExpired', now >= expiresIn);

View File

@@ -4,6 +4,7 @@ import { filter } from '@kevisual/js-filter'
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import { initApi } from './router-api-proxy.ts'; import { initApi } from './router-api-proxy.ts';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { cloneDeep } from 'es-toolkit';
export const RouteTypeList = ['api', 'context', 'worker', 'page'] as const; export const RouteTypeList = ['api', 'context', 'worker', 'page'] as const;
export type RouterViewItemInfo = RouterViewApi | RouterViewContext | RouterViewWorker | RouteViewPage; export type RouterViewItemInfo = RouterViewApi | RouterViewContext | RouterViewWorker | RouteViewPage;
@@ -26,6 +27,10 @@ type RouteViewBase = {
* 默认动作配置 * 默认动作配置
*/ */
action?: { path?: string; key?: string; id?: string; payload?: any;[key: string]: any }; action?: { path?: string; key?: string; id?: string; payload?: any;[key: string]: any };
/**
* 本地状态loading、active、error等
*/
routerStatus?: 'loading' | 'active' | 'inactive' | 'error';
} }
export type RouterViewApi = { export type RouterViewApi = {
type: 'api', type: 'api',
@@ -67,7 +72,7 @@ export type RouterViewWorker = {
* @returns * @returns
*/ */
export const pickRouterViewData = (item: RouterViewItem) => { export const pickRouterViewData = (item: RouterViewItem) => {
const { action, response, _id, ...rest } = item; const { action, response, _id, ...rest } = cloneDeep(item);
if (rest.type === 'api') { if (rest.type === 'api') {
if (rest.api) { if (rest.api) {
delete rest.api.query; delete rest.api.query;
@@ -83,6 +88,7 @@ export const pickRouterViewData = (item: RouterViewItem) => {
delete rest.context.router; delete rest.context.router;
} }
} }
delete rest.routerStatus;
return rest return rest
} }
/** /**
@@ -98,7 +104,7 @@ export type RouteViewPage = {
export type RouterViewQuery = { export type RouterViewQuery = {
id: string, id: string,
query: string, query: string,
title: string title: string,
} }
/** /**
* 后端存储结构 * 后端存储结构
@@ -143,6 +149,7 @@ export class QueryProxy {
} }
private initRouterView(item: RouterViewItem) { private initRouterView(item: RouterViewItem) {
item.routerStatus = 'loading';
if (item.type === 'api' && item.api?.url) { if (item.type === 'api' && item.api?.url) {
const url = item.api.url; const url = item.api.url;
if (item?.api?.query) return item; if (item?.api?.query) return item;
@@ -245,10 +252,14 @@ export class QueryProxy {
// @ts-ignore // @ts-ignore
const context = globalThis['context'] || {} const context = globalThis['context'] || {}
const router = item?.context?.router || context[item?.context?.key] as QueryRouterServer; const router = item?.context?.router || context[item?.context?.key] as QueryRouterServer;
if (item) {
item.routerStatus = router ? 'active' : 'error';
}
if (!router) { if (!router) {
console.warn(`未发现Context router ${item?.context?.key}`); console.warn(`未发现Context router ${item?.context?.key}`);
return return
} }
const routes = router.getList(); const routes = router.getList();
// TODO: args // TODO: args
// const args = fromJSONSchema(r); // const args = fromJSONSchema(r);
@@ -308,6 +319,9 @@ export class QueryProxy {
} }
const viewItem = item.worker; const viewItem = item.worker;
const worker = viewItem?.worker; const worker = viewItem?.worker;
if (item) {
item.routerStatus = worker ? 'active' : 'error';
}
if (!worker) { if (!worker) {
console.warn('Worker not initialized'); console.warn('Worker not initialized');
return; return;
@@ -377,11 +391,15 @@ export class QueryProxy {
const url = item.page.url; const url = item.page.url;
try { try {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
await import(url).then((module) => { }).catch((err) => { await import(url)
console.error('引入Page脚本失败:', url, err); if (item) {
}); item.routerStatus = 'active';
}
} }
} catch (e) { } catch (e) {
if (item) {
item.routerStatus = 'error';
}
console.warn('引入Page脚本失败:', url, e); console.warn('引入Page脚本失败:', url, e);
return; return;
} }

View File

@@ -17,12 +17,16 @@ export const initApi = async (opts: {
const token = opts?.token; const token = opts?.token;
const query = item?.api?.query || new Query({ url: item?.api?.url || '/api/router' }) const query = item?.api?.query || new Query({ url: item?.api?.url || '/api/router' })
const res = await query.post<{ list: RouterItem[] }>({ path: "router", key: 'list', token: token }); const res = await query.post<{ list: RouterItem[] }>({ path: "router", key: 'list', token: token });
if (item) {
item.routerStatus = res?.code === 200 ? 'active' : 'error';
}
if (res.code !== 200) { if (res.code !== 200) {
return { return {
code: res.code, code: res.code,
message: `初始化路由失败: ${res.message}, url: ${query.url}` message: `初始化路由失败: ${res.message}, url: ${query.url}`
} }
} }
let _list = res.data?.list || [] let _list = res.data?.list || []
if (opts?.exclude) { if (opts?.exclude) {
if (opts?.exclude) { if (opts?.exclude) {

View File

@@ -26,6 +26,10 @@ export class QueryResources {
setUsername(username: string) { setUsername(username: string) {
this.prefix = `/${username}/resources/`; this.prefix = `/${username}/resources/`;
} }
/**
* 设置prefix类似 /{username}/resources/
* @param prefix
*/
setPrefix(prefix: string) { setPrefix(prefix: string) {
this.prefix = prefix; this.prefix = prefix;
} }
@@ -55,19 +59,25 @@ export class QueryResources {
headers: this.header(opts?.headers), headers: this.header(opts?.headers),
}); });
} }
getUrl(prefix: string): string {
if (prefix.startsWith('http')) {
return prefix;
}
return `${this.prefix}${prefix}`;
}
async getList(prefix: string, data?: { recursive?: boolean }, opts?: DataOpts): Promise<Result<any[]>> { async getList(prefix: string, data?: { recursive?: boolean }, opts?: DataOpts): Promise<Result<any[]>> {
return this.get(data, { return this.get(data, {
url: `${this.prefix}${prefix}`, url: this.getUrl(prefix),
body: data, body: data,
...opts, ...opts,
}); });
} }
async fetchFile(filepath: string, opts?: DataOpts): Promise<Result<any>> { async fetchFile(filepath: string, opts?: DataOpts): Promise<Result<any>> {
const url = `${this.prefix}${filepath}`; const url = this.getUrl(filepath);
return this.get({}, { url, method: 'GET', ...opts, headers: this.header(opts?.headers, false), isText: true }); return this.get({}, { url, method: 'GET', ...opts, headers: this.header(opts?.headers, false), isText: true });
} }
async uploadFile(filepath: string, content: string | Blob, opts?: DataOpts & { chunkSize?: number, maxSize?: number }): Promise<Result<any>> { async uploadFile(filepath: string, content: string | Blob, opts?: DataOpts & { chunkSize?: number, maxSize?: number }): Promise<Result<any>> {
const pathname = `${this.prefix}${filepath}`; const pathname = this.getUrl(filepath);
const filename = path.basename(pathname); const filename = path.basename(pathname);
const type = getContentType(filename); const type = getContentType(filename);
const url = new URL(pathname, window.location.origin); const url = new URL(pathname, window.location.origin);
@@ -110,12 +120,12 @@ export class QueryResources {
return res; return res;
} }
async uploadChunkedFile(filepath: string, file: Blob, hash: string, opts?: DataOpts & { chunkSize?: number }): Promise<Result<any>> { async uploadChunkedFile(filepath: string, file: Blob, hash: string, opts?: DataOpts & { chunkSize?: number }): Promise<Result<any>> {
const pathname = `${this.prefix}${filepath}`; const pathname = this.getUrl(filepath);
const filename = path.basename(pathname); const filename = path.basename(pathname);
const url = new URL(pathname, window.location.origin); const url = new URL(pathname, window.location.origin);
url.searchParams.set('hash', hash); url.searchParams.set('hash', hash);
url.searchParams.set('chunk', '1'); url.searchParams.set('chunk', '1');
console.log(`url,`, url, hash); // console.log(`url,`, url, hash);
// 预留 eventSource 支持(暂不处理) // 预留 eventSource 支持(暂不处理)
// const createEventSource = opts?.createEventSource; // const createEventSource = opts?.createEventSource;
const { chunkSize: _chunkSize, ...restOpts } = opts || {}; const { chunkSize: _chunkSize, ...restOpts } = opts || {};
@@ -166,9 +176,20 @@ export class QueryResources {
this.onProcess?.({ type: 'uploadFinish', filename, size: file.size, process: 100 }); this.onProcess?.({ type: 'uploadFinish', filename, size: file.size, process: 100 });
return { code: 200, message: '上传成功' }; return { code: 200, message: '上传成功' };
} }
/**
* 移除 prefix获取相对路径
* @param filepath
* @returns
*/
getRelativePath(filepath: string): string {
if (filepath.startsWith(this.prefix)) {
return filepath.slice(this.prefix.length);
}
return filepath;
}
async getStat(filepath: string, opts?: DataOpts): Promise<Result<Stat>> { async getStat(filepath: string, opts?: DataOpts): Promise<Result<Stat>> {
const url = `${this.prefix}${filepath}`; const url = this.getUrl(filepath);
return adapter({ return adapter({
url, url,
params: { params: {
@@ -192,8 +213,8 @@ export class QueryResources {
return this.uploadFile(filepath, '文件夹占位,其他文件不存在,文件夹不存在,如果有其他文件夹,删除当前文件夹占位文件即可', opts); return this.uploadFile(filepath, '文件夹占位,其他文件不存在,文件夹不存在,如果有其他文件夹,删除当前文件夹占位文件即可', opts);
} }
async rename(oldpath: string, newpath: string, opts?: DataOpts): Promise<Result<any>> { async rename(oldpath: string, newpath: string, opts?: DataOpts): Promise<Result<any>> {
const pathname = `${this.prefix}${oldpath}`; const pathname = this.getUrl(oldpath);
const newName = `${this.prefix}${newpath}`; const newName = this.getUrl(newpath);
const params = { const params = {
newName: newName, newName: newName,
}; };
@@ -206,7 +227,7 @@ export class QueryResources {
}); });
} }
async deleteFile(filepath: string, opts?: DataOpts): Promise<Result<any>> { async deleteFile(filepath: string, opts?: DataOpts): Promise<Result<any>> {
const url = `${this.prefix}${filepath}`; const url = this.getUrl(filepath);
return adapter({ return adapter({
url, url,
method: 'DELETE' as any, method: 'DELETE' as any,