Files
query-awesome/query/query-proxy/proxy.ts

480 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
export const RouteTypeList = ['api', 'context', 'worker', 'page'] as const;
export type RouterViewItemInfo = RouterViewApi | RouterViewContext | RouterViewWorker | RouteViewPage;
export type RouterViewItem<T = {}> = 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 };
}
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 } = 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;
}
}
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();
}
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;
});
}
initRouterView(item: RouterViewItem) {
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<boolean> {
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 (!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;
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);
}
async callWorker(msg: any, viewItem: RouterViewWorker['worker']): Promise<Result> {
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);
});
});
}
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 (!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;
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);
}
}
}
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).then((module) => { }).catch((err) => {
console.error('引入Page脚本失败:', url, err);
});
}
} catch (e) {
console.warn('引入Page脚本失败:', url, e);
return;
}
}
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<any>(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: '无法处理的路由类型' };
return item;
}
}
export type RouterItem = {
id?: string;
path?: string;
key?: string;
description?: string;
middleware?: string[];
metadata?: Record<string, any>;
}