import { QueryClient as Query, Result } from '@kevisual/query'; import { QueryRouterServer, type App, type Route, fromJSONSchema, toJSONSchema } from '@kevisual/router/browser'; import { filter } from '@kevisual/js-filter' import { EventEmitter } from 'eventemitter3'; import { initApi } from './router-api-proxy.ts'; import Fuse from 'fuse.js'; import { cloneDeep } from 'es-toolkit'; export const RouteTypeList = ['api', 'context', 'worker', 'page'] as const; export type RouterViewItemInfo = RouterViewApi | RouterViewContext | RouterViewWorker | RouteViewPage; export type RouterViewItem = RouterViewItemInfo & T; type RouteViewBase = { /** * _id 用于纯本地存储标识 */ _id?: string; id?: string; title?: string; description?: string; enabled?: boolean; /** * 响应数据 */ response?: any; /** * 默认动作配置 */ action?: { path?: string; key?: string; id?: string; payload?: any;[key: string]: any }; /** * 本地状态,loading、active、error等 */ routerStatus?: 'loading' | 'active' | 'inactive' | 'error'; } export type RouterViewApi = { type: 'api', api: { url: string, // 已初始化的query实例,不需要编辑配置 query?: Query } } & RouteViewBase; export type RouterViewContext = { type: 'context', context: { key: string, // 从context中获取router,不需要编辑配置 router?: QueryRouterServer } } & RouteViewBase; export type RouterViewWorker = { type: 'worker', worker: { type: 'Worker' | 'SharedWorker' | 'serviceWorker', url: string, // 已初始化的worker实例,不需要编辑配置 worker?: Worker | SharedWorker | ServiceWorker, /** * worker选项 * default: { type: 'module' } */ workerOptions?: { type: 'module' | 'classic' } } } & RouteViewBase; /** * 去掉不需要保存的服务器的数据 * @param item * @returns */ export const pickRouterViewData = (item: RouterViewItem) => { const { action, response, _id, ...rest } = cloneDeep(item); if (rest.type === 'api') { if (rest.api) { delete rest.api.query; } } if (rest.type === 'worker') { if (rest.worker) { delete rest.worker.worker; } } if (rest.type === 'context') { if (rest.context) { delete rest.context.router; } } delete rest.routerStatus; return rest } /** * 注入 js 的url地址,使用 importScripts 加载 */ export type RouteViewPage = { type: 'page', page: { url: string, } } & RouteViewBase; export type RouterViewQuery = { id: string, query: string, title: string, } /** * 后端存储结构 */ export type RouterViewData = { data: { items: RouterViewItem[]; } views: RouterViewQuery[]; viewId?: string; [key: string]: any; } export class QueryProxy { router: QueryRouterServer; token?: string; routerViewItems: RouterViewItem[]; views: RouterViewQuery[]; emitter: EventEmitter; constructor(opts?: { router?: QueryRouterServer, token?: string, routerViewData?: RouterViewData }) { this.router = opts?.router || new QueryRouterServer(); this.token = opts?.token || this.getDefulatToken(); this.routerViewItems = opts?.routerViewData?.data?.items || []; this.views = opts?.routerViewData?.views || []; this.initRouterViewQuery(); this.emitter = new EventEmitter(); } private getDefulatToken() { try { if (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined') { return localStorage.getItem('token') || undefined; } } catch (e) { return undefined; } } initRouterViewQuery() { this.routerViewItems = this.routerViewItems.map(item => { return this.initRouterView(item); }).filter(item => { const enabled = item.enabled ?? true; return enabled; }); } private initRouterView(item: RouterViewItem) { item.routerStatus = 'loading'; if (item.type === 'api' && item.api?.url) { const url = item.api.url; if (item?.api?.query) return item; const query = new Query({ url: url }); item['api'] = { url: url, query: query }; } if (item.type === 'worker' && item.worker?.url) { let viewItem = item as RouterViewWorker; if (!item.worker?.workerOptions?.type) { item.worker.workerOptions = { ...item.worker.workerOptions, type: 'module' }; } if (item.worker.worker) { return item; } let worker: Worker | SharedWorker | ServiceWorker | undefined = undefined; if (item.worker.type === 'SharedWorker') { worker = new SharedWorker(item.worker.url, item.worker.workerOptions); worker.port.start(); } else if (viewItem.worker.type === 'serviceWorker') { if ('serviceWorker' in navigator) { navigator.serviceWorker.register(viewItem.worker.url, item.worker.workerOptions).then(function (registration) { console.debug('注册serviceWorker成功 ', registration.scope); }, function (err) { console.debug('注册 serviceWorker 失败: ', err); }); } else { console.warn('当前浏览器不支持serviceWorker'); } } else { worker = new Worker(viewItem.worker.url, item.worker.workerOptions); } viewItem['worker']['worker'] = worker; } if (item.type === 'context' && item.context?.key) { if (item.context?.router) { return item; } // @ts-ignore const context = globalThis['context'] || {} const router = context[item.context.key] as QueryRouterServer; if (router) { item['context']['router'] = router; } } return item; } /** * 初始化路由 * main * @returns */ async init() { const routerViewItems = this.routerViewItems || []; if (routerViewItems.length === 0) { // 默认初始化api类型路由 await this.initApi(); return; } for (const item of routerViewItems) { switch (item.type) { case 'api': await this.initApi(item); break; case 'context': await this.initContext(item); break; case 'worker': await this.initWorker(item); break; case 'page': await this.initPage(item); break; } } this.emitter.emit('initComplete'); } /** * 监听初始化完成 * @returns */ async listenInitComplete(): Promise { return new Promise((resolve) => { const timer = setTimeout(() => { this.emitter.removeAllListeners('initComplete'); resolve(false); }, 3 * 60000); // 3分钟超时 const func = () => { clearTimeout(timer); resolve(true); } this.emitter.once('initComplete', func); }); } async initApi(item?: RouterViewApi) { initApi({ item: item, router: this.router, token: this.token }); } async initContext(item?: RouterViewContext) { // @ts-ignore const context = globalThis['context'] || {} const router = item?.context?.router || context[item?.context?.key] as QueryRouterServer; if (item) { item.routerStatus = router ? 'active' : 'error'; } if (!router) { console.warn(`未发现Context router ${item?.context?.key}`); return } const routes = router.getList(); // TODO: args // const args = fromJSONSchema(r); for (const r of routes) { console.debug(`注册路由: [${r.path}] ${r?.key}`, 'Context'); let metadata = r.metadata || {}; metadata.viewItem = item; metadata.source = 'query-proxy-context'; this.router.route({ path: r.path, key: r.key || '', id: r.id, description: r.description, metadata: metadata, }).define(async (ctx) => { const res = await router.run({ path: r.path, key: r.key, ...ctx.query }); ctx.forward(res) }).addTo(this.router); } } generateId() { return 'route_' + Math.random().toString(36).substring(2, 9); } private async callWorker(msg: any, viewItem: RouterViewWorker['worker']): Promise { const that = this; const requestId = this.generateId(); const worker = viewItem?.worker; if (!worker) { return { code: 500, message: 'Worker未初始化' }; } let port: MessagePort | Worker | ServiceWorker; if (viewItem.type === 'SharedWorker') { port = (worker as SharedWorker).port; } else { port = worker as Worker | ServiceWorker; } port.postMessage({ ...msg, requestId: requestId, }) return new Promise((resolve) => { const timer = setTimeout(() => { that.emitter.removeAllListeners(requestId); resolve({ code: 500, message: '请求超时' }); }, 3 * 60 * 1000); // 3分钟超时 that.emitter.once(requestId, (res: any) => { clearTimeout(timer); resolve(res); }); }); } private async initWorker(item?: RouterViewWorker, initRoutes: boolean = true) { const that = this; if (!item?.worker?.url) { console.warn('Worker URL not provided'); return; } const viewItem = item.worker; const worker = viewItem?.worker; if (item) { item.routerStatus = worker ? 'active' : 'error'; } if (!worker) { console.warn('Worker not initialized'); return; } const callResponse = (e: MessageEvent) => { const msg = e.data; if (msg.requestId) { const requestId = msg.requestId; that.emitter.emit(requestId, msg); } else { that.router.run(msg); } } if (item.worker.type === 'SharedWorker') { const port = (worker as SharedWorker).port; port.onmessage = callResponse; port.start(); } else if (item.worker.type === 'serviceWorker') { navigator.serviceWorker.addEventListener('message', callResponse); } else { (worker as Worker).onmessage = callResponse; } if (!initRoutes) { return; } const callWorker = this.callWorker.bind(this); const res = await callWorker({ path: "router", key: 'list', token: this.token, }, viewItem); if (res.code !== 200) { console.error('Failed to init query proxy router:', res.message); return; } const _list = res.data?.list || [] for (const r of _list) { if (r.path || r.id) { console.debug(`注册路由: [${r.path}] ${r?.key}`, 'API'); let metadata = r.metadata || {}; metadata.viewItem = item; metadata.source = 'query-proxy-worker'; this.router.route({ path: r.path, key: r.key || '', id: r.id, description: r.description, metadata: metadata, }).define(async (ctx) => { const msg = { ...ctx.query }; if (msg.token === undefined && that.token !== undefined) { msg.token = that.token; } const res = await callWorker({ path: r.path, key: r.key, ...msg }, viewItem); ctx.forward(res) }).addTo(that.router); } } } private async initPage(item?: RouteViewPage) { if (!item?.page?.url) { console.warn('Page地址未提供'); return; } const url = item.page.url; try { if (typeof window !== 'undefined') { await import(url) if (item) { item.routerStatus = 'active'; } } } catch (e) { if (item) { item.routerStatus = 'error'; } console.warn('引入Page脚本失败:', url, e); return; } } private getQueryByViewId(viewId: string): string | undefined { const view = this.views.find(v => v.id === viewId); if (view) { return view.query; } return undefined; } /** * 列出路由 * @param filter * @param query WHERE metadata.tags CONTAINS 'premium' * @returns */ async listRoutes(filterFn?: (item: Route) => boolean, opts?: { viewId?: string, query?: string }) { let query = opts?.query; if (opts?.viewId && !query) { query = this.getQueryByViewId(opts.viewId); } const routes = this.router.routes.filter(filterFn || (() => true)); if (query) { if (query.toLocaleUpperCase().startsWith('WHERE')) { return filter(routes, query); } else { const fuse = new Fuse(routes, { keys: ['path', 'key', 'description'], threshold: 0.4, }); let findsRoutes = fuse.search(query); const resultRoutes = findsRoutes.map(r => r.item); return resultRoutes; } } return routes; } async getViewQuery(viewId: string) { const view = this.views.find(v => v.id === viewId); if (view) { return view.query; } return undefined; } /** * 运行路由 * @param msg * @returns */ async run(msg: { id?: string, path?: string, key?: string }) { return await this.router.run(msg); } async runByRouteView(routeView: RouterViewItem) { if (routeView.response) { return routeView; } const item = this.initRouterView(routeView); if (item.type === 'api' && item.api?.url) { const query = item.api.query!; const res = await query.post(item.action || {}); item.response = res; return item; } else if (item.type === 'api') { item.response = { code: 500, message: 'API URL未配置' }; return item; } if (item.type === 'context' && item.context?.router) { const router = item.context.router; const res = await router.run(item.action || {}); item.response = res; return item; } if (item.type === 'page') { await this.initPage(item); const res = await this.router.run(item.action || {}); item.response = res; return item; } if (item.type === 'worker' && item.worker?.worker) { await this.initWorker(item, false); const callWorker = this.callWorker.bind(this); const res = await callWorker(item.action || {}, item.worker); item.response = res; return item; } item.response = { code: 500, message: '无法处理的路由类型', data: item }; return item; } } export type RouterItem = { id?: string; path?: string; key?: string; description?: string; middleware?: string[]; metadata?: Record; } export const createViewData = (routerViewData: RouterViewItem | RouterViewItem[], data?: RouterViewData): RouterViewData => { return { views: [], viewId: undefined, data: { items: Array.isArray(routerViewData) ? routerViewData : [routerViewData], }, ...data } }