diff --git a/package.json b/package.json index 598dd80..0f68aad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kevisual/api", - "version": "0.0.10", + "version": "0.0.11", "description": "", "main": "mod.ts", "scripts": { @@ -35,6 +35,7 @@ "@kevisual/js-filter": "^0.0.2", "@kevisual/load": "^0.0.6", "es-toolkit": "^1.43.0", + "eventemitter3": "^5.0.1", "nanoid": "^5.1.6" }, "exports": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a08654..d1c3ffb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: es-toolkit: specifier: ^1.43.0 version: 1.43.0 + eventemitter3: + specifier: ^5.0.1 + version: 5.0.1 nanoid: specifier: ^5.1.6 version: 5.1.6 diff --git a/query/query-proxy/index.ts b/query/query-proxy/index.ts index 70dbc36..63b9265 100644 --- a/query/query-proxy/index.ts +++ b/query/query-proxy/index.ts @@ -1,67 +1,268 @@ -import { Query } from '@kevisual/query/query'; +import { Query, Result } from '@kevisual/query/query'; import { QueryRouterServer, Route } from '@kevisual/router/src/route.ts'; import { filter } from '@kevisual/js-filter' -export type ProxyItem = { - title?: string; - type?: 'api' | 'context' | 'page'; - description?: string; - api?: { - url: string; - }, - context?: { - key: string; - }, - page?: {}, - where?: string; - whereList?: Array<{ title: string; where: string }>; +import { EventEmitter } from 'eventemitter3'; + +export type RouterViewItem = RouterViewApi | RouterViewContext | RouterViewWorker; +export type RouterViewApi = { + title: string; + description: string; + type: 'api', + api: { + url: string, + // 已初始化的query实例 + query?: Query + } +} + +export type RouterViewContext = { + title: string; + description: string; + type: 'context', + context: { + key: string, + // 从context中获取router + router?: QueryRouterServer + } +} +export type RouterViewWorker = { + title: string; + description: string; + type: 'worker', + worker: { + type: 'Worker' | 'SharedWorker' | 'serviceWorker', + url: string, + // 已初始化的worker实例 + worker?: Worker | SharedWorker | ServiceWorker + } +} +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 { - query: Query; router: QueryRouterServer; token?: string; - constructor(opts?: { query: Query, router?: QueryRouterServer, token?: string }) { - this.query = opts?.query || new Query(); + routeViewItems: RouterViewItem[]; + views: RouterViewQuery[]; + emitter: EventEmitter; + constructor(opts?: { query: Query, router?: QueryRouterServer, token?: string, routeViewData?: RouterViewData }) { this.router = opts?.router || new QueryRouterServer(); this.token = opts?.token || this.getDefulatToken(); + this.routeViewItems = opts?.routeViewData?.data?.items || []; + this.views = opts?.routeViewData?.views || []; + this.initRouteViewQuery(); + this.emitter = new EventEmitter(); } getDefulatToken() { try { - if (localStorage) { + if (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined') { return localStorage.getItem('token') || undefined; } } catch (e) { return undefined; } } + async initRouteViewQuery() { + this.routeViewItems = this.routeViewItems?.map(item => { + if (item.type === 'api' && item.api?.url) { + const url = item.api.url; + item['api'] = { url: url, query: new Query({ url: url }) }; + } + if (item.type === 'worker' && item.worker?.url) { + let viewItem = item as RouterViewWorker; + let worker: Worker | SharedWorker | ServiceWorker | undefined = undefined; + if (item.worker.type === 'SharedWorker') { + worker = new SharedWorker(item.worker.url); + worker.port.start(); + } else if (viewItem.worker.type === 'serviceWorker') { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register(viewItem.worker.url).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); + } + viewItem['worker']['worker'] = worker; + } + if (item.type === 'context' && item.context?.key) { + // @ts-ignore + const context = globalThis['context'] || {} + const router = context[item.context.key] as QueryRouterServer; + if (router) { + item['context']['router'] = router; + } + } + return item; + }); + } + /** * 初始化路由 * @returns */ - async init() { + async init(routeViewData?: RouterViewData) { + const routes = routeViewData?.data?.items || []; + if (routes.length === 0) { + await this.initApi(); + return; + } + for (const item of routes) { + switch (item.type) { + case 'api': + this.initApi(item); + break; + case 'context': + break; + case 'worker': + this.initWorker(item); + break; + } + } + } + async initApi(item?: RouterViewApi) { const that = this; - const res = await this.query.post<{ list: RouterItem[] }>({ path: "router", key: 'list', token: this.token }); + 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: this.token }); if (res.code !== 200) { console.error('Failed to init query proxy router:', res.message); return } const _list = res.data?.list || [] - for (const item of _list) { - if (item.path || item.id) { - console.log(`Register route: [${item.path}] ${item?.key}`); + for (const r of _list) { + if (r.path || r.id) { + console.debug(`注册路由: [${r.path}] ${r?.key}`, 'API'); this.router.route({ - path: item.path, - key: item.key || '', - id: item.id, - description: item.description, - metadata: item.metadata, + path: r.path, + key: r.key || '', + id: r.id, + description: r.description, + metadata: r.metadata, }).define(async (ctx) => { const msg = { ...ctx.query }; if (msg.token === undefined && that.token !== undefined) { msg.token = that.token; } - const r = await that.query.post({ path: item.path, key: item.key, ...msg }); - ctx.forward(r) + const res = await query.post({ path: r.path, key: r.key, ...msg }); + ctx.forward(res) + }).addTo(that.router); + } + } + } + async initContext(item?: RouterViewContext) { + // @ts-ignore + const context = globalThis['context'] || {} + const router = item?.context?.router || context[item?.context?.key] as QueryRouterServer; + if (!router) { + console.warn(`未发现Context router ${item?.context?.key}`); + return + } + const routes = router.getList(); + for (const r of routes) { + console.debug(`注册路由: [${r.path}] ${r?.key}`, 'Context'); + this.router.route({ + path: r.path, + key: r.key || '', + id: r.id, + description: r.description, + metadata: r.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); + } + async initWorker(item?: RouterViewWorker) { + const that = this; + if (!item?.worker?.url) { + console.warn('Worker URL not provided'); + return; + } + const viewItem = item.worker; + const worker = viewItem?.worker; + if (!worker) { + console.warn('Worker not initialized'); + return; + } + if (item.worker.type === 'SharedWorker') { + const port = (worker as SharedWorker).port; + port.onmessage = function (e) { + const msg = e.data; + const requestId = msg.requestId; + that.emitter.emit(requestId, msg); + }; + port.start(); + } + const callWorker = async (msg: any, viewItem: RouterViewWorker['worker']): Promise => { + 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); + }); + }); + } + 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'); + this.router.route({ + path: r.path, + key: r.key || '', + id: r.id, + description: r.description, + metadata: r.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); } } @@ -72,7 +273,16 @@ export class QueryProxy { * @param query WHERE metadata.tags CONTAINS 'premium' * @returns */ - async listRoutes(filterFn?: (item: Route) => boolean, query?: string) { + async listRoutes(filterFn?: (item: Route) => boolean, opts?: { viewId?: string, query?: string }) { + let query = opts?.query; + if (opts?.viewId) { + const view = this.views.find(v => v.id === opts.viewId); + if (view) { + query = view.query; + } + } if (opts?.query) { + query = opts.query; + } const routes = this.router.routes.filter(filterFn || (() => true)); if (query) { return filter(routes, query);