feat: 更新版本至 0.0.61,重构登录缓存逻辑,添加浏览器缓存支持
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/api",
|
"name": "@kevisual/api",
|
||||||
"version": "0.0.60",
|
"version": "0.0.61",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "mod.ts",
|
"main": "mod.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
123
query/query-login/browser-cache/cache-store.ts
Normal file
123
query/query-login/browser-cache/cache-store.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
query/query-login/browser-cache/cache.ts
Normal file
29
query/query-login/browser-cache/cache.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -146,8 +146,9 @@ export class LoginCacheStore<T extends Cache = Cache> implements CacheStore<T> {
|
|||||||
/**
|
/**
|
||||||
* 初始化,设置默认值
|
* 初始化,设置默认值
|
||||||
*/
|
*/
|
||||||
async init() {
|
async init(): Promise<CacheLogin> {
|
||||||
const defaultData: CacheLogin = { ...this.cacheData };
|
const defaultData: CacheLogin = { ...this.cacheData };
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
if (this.cache.init) {
|
if (this.cache.init) {
|
||||||
try {
|
try {
|
||||||
const cacheData = await this.cache.init();
|
const cacheData = await this.cache.init();
|
||||||
@@ -158,7 +159,8 @@ export class LoginCacheStore<T extends Cache = Cache> implements CacheStore<T> {
|
|||||||
} else {
|
} else {
|
||||||
this.cacheData = (await this.getValue()) || defaultData;
|
this.cacheData = (await this.getValue()) || defaultData;
|
||||||
}
|
}
|
||||||
return this.cacheData;
|
resolve(this.cacheData);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 设置当前用户
|
* 设置当前用户
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user