Compare commits
No commits in common. "main" and "v0.0.6" have entirely different histories.
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,9 +1,2 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
build
|
|
||||||
.cache
|
|
||||||
.DS_Store
|
|
||||||
*.log
|
|
||||||
|
|
||||||
|
|
||||||
.turbo
|
|
3
.npmrc
3
.npmrc
@ -1,3 +0,0 @@
|
|||||||
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
|
||||||
@abearxiong:registry=https://npm.pkg.github.com
|
|
||||||
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
|
@ -1,24 +0,0 @@
|
|||||||
import { App } from '@kevisual/router';
|
|
||||||
|
|
||||||
const app = new App({
|
|
||||||
io: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
app
|
|
||||||
.route({
|
|
||||||
path: 'test',
|
|
||||||
key: 'test',
|
|
||||||
})
|
|
||||||
.define(async (ctx) => {
|
|
||||||
ctx.body = 'test';
|
|
||||||
})
|
|
||||||
.addTo(app);
|
|
||||||
|
|
||||||
app.listen(4000, () => {
|
|
||||||
console.log('Server is running at http://localhost:4000');
|
|
||||||
});
|
|
||||||
|
|
||||||
app.io.addListener('subscribe', async ({ data, end, ws }) => {
|
|
||||||
console.log('A user connected', data);
|
|
||||||
ws.send('Hello World');
|
|
||||||
});
|
|
@ -8,8 +8,6 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- Your content goes here -->
|
<!-- Your content goes here -->
|
||||||
<!-- <script type="module" src="src/index.ts"></script> -->
|
<script type="module" src="src/index.ts"></script>
|
||||||
<!-- 测试io -->
|
|
||||||
<script type="module" src="src/test2.ts"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -10,10 +10,9 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@abearxiong/query": "file:..",
|
"@abearxiong/query": "file:.."
|
||||||
"@kevisual/router": "0.0.6-alpha-4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.5"
|
"vite": "^5.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
// @ts-ignore
|
|
||||||
import { QueryClient } from '@kevisual/query';
|
|
||||||
|
|
||||||
const query = new QueryClient({ url: '/api/router', io: true });
|
|
||||||
|
|
||||||
query.qws.listenConnect(() => {
|
|
||||||
console.log('Connected');
|
|
||||||
// query.qws.send({
|
|
||||||
// type: 'subscribe',
|
|
||||||
// });
|
|
||||||
// query.qws.connect().then((res) => {
|
|
||||||
// console.log('Connected', res);
|
|
||||||
// query.qws.send({
|
|
||||||
// type: 'subscribe',
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
});
|
|
||||||
|
|
||||||
query.qws.connect().then((res) => {
|
|
||||||
console.log('Connected', res);
|
|
||||||
query.qws.send({
|
|
||||||
type: 'subscribe',
|
|
||||||
});
|
|
||||||
query.qws.ws.addEventListener('message', (event) => {
|
|
||||||
console.log('get', event.data);
|
|
||||||
});
|
|
||||||
});
|
|
213
jest.config.ts
Normal file
213
jest.config.ts
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
// For a detailed explanation regarding each configuration property, visit:
|
||||||
|
// https://jestjs.io/docs/en/configuration.html
|
||||||
|
import type { Config } from 'jest';
|
||||||
|
import { defaults } from 'jest-config';
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
// All imported modules in your tests should be mocked automatically
|
||||||
|
// automock: false,
|
||||||
|
|
||||||
|
// Stop running tests after `n` failures
|
||||||
|
// bail: 0,
|
||||||
|
globals: {
|
||||||
|
window: {},
|
||||||
|
},
|
||||||
|
// The directory where Jest should store its cached dependency information
|
||||||
|
// cacheDirectory: "/private/var/folders/y3/bq7hhg1x02x_3xqrx11sql840000gn/T/jest_dx",
|
||||||
|
|
||||||
|
// Automatically clear mock calls and instances between every test
|
||||||
|
clearMocks: true,
|
||||||
|
|
||||||
|
// Indicates whether the coverage information should be collected while executing the test
|
||||||
|
// collectCoverage: false,
|
||||||
|
|
||||||
|
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||||
|
// collectCoverageFrom: undefined,
|
||||||
|
|
||||||
|
// The directory where Jest should output its coverage files
|
||||||
|
coverageDirectory: 'coverage',
|
||||||
|
|
||||||
|
// An array of regexp pattern strings used to skip coverage collection
|
||||||
|
// coveragePathIgnorePatterns: [
|
||||||
|
// "/node_modules/"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// Indicates which provider should be used to instrument code for coverage
|
||||||
|
// coverageProvider: "babel",
|
||||||
|
|
||||||
|
// A list of reporter names that Jest uses when writing coverage reports
|
||||||
|
// coverageReporters: [
|
||||||
|
// "json",
|
||||||
|
// "text",
|
||||||
|
// "lcov",
|
||||||
|
// "clover"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An object that configures minimum threshold enforcement for coverage results
|
||||||
|
// coverageThreshold: undefined,
|
||||||
|
|
||||||
|
// A path to a custom dependency extractor
|
||||||
|
// dependencyExtractor: undefined,
|
||||||
|
|
||||||
|
// Make calling deprecated APIs throw helpful error messages
|
||||||
|
// errorOnDeprecated: false,
|
||||||
|
|
||||||
|
// Force coverage collection from ignored files using an array of glob patterns
|
||||||
|
// forceCoverageMatch: [],
|
||||||
|
|
||||||
|
// A path to a module which exports an async function that is triggered once before all test suites
|
||||||
|
// globalSetup: undefined,
|
||||||
|
|
||||||
|
// A path to a module which exports an async function that is triggered once after all test suites
|
||||||
|
// globalTeardown: undefined,
|
||||||
|
|
||||||
|
// A set of global variables that need to be available in all test environments
|
||||||
|
// globals: {},
|
||||||
|
|
||||||
|
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||||
|
// maxWorkers: "50%",
|
||||||
|
|
||||||
|
// An array of directory names to be searched recursively up from the requiring module's location
|
||||||
|
// moduleDirectories: [
|
||||||
|
// "node_modules"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An array of file extensions your modules use
|
||||||
|
// moduleFileExtensions: [
|
||||||
|
// "js",
|
||||||
|
// "json",
|
||||||
|
// "jsx",
|
||||||
|
// "ts",
|
||||||
|
// "tsx",
|
||||||
|
// "node"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||||
|
// moduleNameMapper: {},
|
||||||
|
// moduleNameMapper: {
|
||||||
|
// '(.*)': ['<rootDir>/$1', '<rootDir>/$1', '<rootDir>/$1'],
|
||||||
|
// },
|
||||||
|
// moduleNameMapper: {
|
||||||
|
// '@/(.*)': ['<rootDir>/$1', '<rootDir>/$1', '<rootDir>/$1'],
|
||||||
|
// },
|
||||||
|
|
||||||
|
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||||
|
// modulePathIgnorePatterns: [],
|
||||||
|
|
||||||
|
// Activates notifications for test results
|
||||||
|
// notify: false,
|
||||||
|
|
||||||
|
// An enum that specifies notification mode. Requires { notify: true }
|
||||||
|
// notifyMode: "failure-change",
|
||||||
|
|
||||||
|
// A preset that is used as a base for Jest's configuration
|
||||||
|
// preset: undefined,
|
||||||
|
// preset: 'ts-jest',
|
||||||
|
preset: 'ts-jest/presets/default-esm',
|
||||||
|
// transform: {
|
||||||
|
// '^.+\\.ts?$': 'babel-jest',
|
||||||
|
// },
|
||||||
|
transform: {
|
||||||
|
// '^.+\\.(ts|js)x?$': 'ts-jest',
|
||||||
|
'^.+\\.(ts|js)x?$': ['ts-jest', { useESM: true }],
|
||||||
|
},
|
||||||
|
|
||||||
|
// "testRegex": "/test/.*\\.(test|spec)\\.(ts)$",
|
||||||
|
|
||||||
|
// Run tests from one or more projects
|
||||||
|
// projects: undefined,
|
||||||
|
|
||||||
|
// Use this configuration option to add custom reporters to Jest
|
||||||
|
// reporters: undefined,
|
||||||
|
|
||||||
|
// Automatically reset mock state between every test
|
||||||
|
// resetMocks: false,
|
||||||
|
|
||||||
|
// Reset the module registry before running each individual test
|
||||||
|
// resetModules: false,
|
||||||
|
|
||||||
|
// A path to a custom resolver
|
||||||
|
// resolver: undefined,
|
||||||
|
|
||||||
|
// Automatically restore mock state between every test
|
||||||
|
// restoreMocks: false,
|
||||||
|
|
||||||
|
// The root directory that Jest should scan for tests and modules within
|
||||||
|
// rootDir: undefined,
|
||||||
|
|
||||||
|
// A list of paths to directories that Jest should use to search for files in
|
||||||
|
// roots: [
|
||||||
|
// "<rootDir>"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// Allows you to use a custom runner instead of Jest's default test runner
|
||||||
|
// runner: "jest-runner",
|
||||||
|
|
||||||
|
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||||
|
// setupFiles: [],
|
||||||
|
|
||||||
|
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||||
|
// setupFilesAfterEnv: [],
|
||||||
|
|
||||||
|
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
||||||
|
// slowTestThreshold: 5,
|
||||||
|
|
||||||
|
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||||
|
// snapshotSerializers: [],
|
||||||
|
|
||||||
|
// The test environment that will be used for testing
|
||||||
|
// testEnvironment: 'node',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
|
||||||
|
// Options that will be passed to the testEnvironment
|
||||||
|
// testEnvironmentOptions: {},
|
||||||
|
|
||||||
|
// Adds a location field to test results
|
||||||
|
// testLocationInResults: false,
|
||||||
|
|
||||||
|
// The glob patterns Jest uses to detect test files
|
||||||
|
testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'],
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||||
|
testPathIgnorePatterns: [
|
||||||
|
'/node_modules/',
|
||||||
|
'<rootDir>/node_modules/(?!(quill-mention)/)',
|
||||||
|
],
|
||||||
|
|
||||||
|
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||||
|
// testRegex: [],
|
||||||
|
|
||||||
|
// This option allows the use of a custom results processor
|
||||||
|
// testResultsProcessor: undefined,
|
||||||
|
|
||||||
|
// This option allows use of a custom test runner
|
||||||
|
// testRunner: "jasmine2",
|
||||||
|
|
||||||
|
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||||
|
// testURL: "http://localhost",
|
||||||
|
|
||||||
|
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||||
|
// timers: "real",
|
||||||
|
|
||||||
|
// A map from regular expressions to paths to transformers
|
||||||
|
// transform: undefined,
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||||
|
// transformIgnorePatterns: [
|
||||||
|
// "/node_modules/",
|
||||||
|
// "\\.pnp\\.[^\\/]+$"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||||
|
// unmockedModulePathPatterns: undefined,
|
||||||
|
|
||||||
|
// Indicates whether each individual test should be reported during the run
|
||||||
|
// verbose: undefined,
|
||||||
|
|
||||||
|
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||||
|
// watchPathIgnorePatterns: [],
|
||||||
|
|
||||||
|
// Whether to use watchman for file crawling
|
||||||
|
// watchman: true,
|
||||||
|
};
|
||||||
|
module.exports = config;
|
41
package.json
41
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/query",
|
"name": "@kevisual/query",
|
||||||
"version": "0.0.29",
|
"version": "0.0.6",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
@ -8,8 +8,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run clean && rollup -c",
|
"build": "npm run clean && rollup -c",
|
||||||
"build:app": "npm run build && rsync dist/* ../deploy/dist",
|
"test": "NODE_ENV=development node --experimental-vm-modules node_modules/jest/bin/jest.js --detectOpenHandles",
|
||||||
"dev:lib": "rollup -c -w",
|
|
||||||
"clean": "rm -rf dist"
|
"clean": "rm -rf dist"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@ -23,16 +22,15 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
"@rollup/plugin-typescript": "^12.1.2",
|
"@rollup/plugin-typescript": "^11.1.6",
|
||||||
"rollup": "^4.41.1",
|
"rollup": "^4.21.2",
|
||||||
"rollup-plugin-dts": "^6.2.1",
|
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.7.0",
|
||||||
"typescript": "^5.8.3",
|
"zustand": "^4.5.5",
|
||||||
"zustand": "^5.0.5"
|
"typescript": "^5.5.4"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.22",
|
"packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
@ -42,23 +40,16 @@
|
|||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/query-browser.js",
|
"import": "./dist/index.js",
|
||||||
"require": "./dist/query-browser.js"
|
"require": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"./query": {
|
"./node": {
|
||||||
"import": "./dist/query.js",
|
"import": "./dist/node-adapter.js",
|
||||||
"require": "./dist/query.js"
|
"require": "./dist/node-adapter.js"
|
||||||
},
|
},
|
||||||
"./ws": {
|
"./ws": {
|
||||||
"import": "./dist/query-ws.js",
|
"import": "./dist/ws.js",
|
||||||
"require": "./dist/query-ws.js"
|
"require": "./dist/ws.js"
|
||||||
},
|
|
||||||
"./query-ai": {
|
|
||||||
"import": "./dist/query-ai.js",
|
|
||||||
"require": "./dist/query-ai.js"
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"openai": "^5.0.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
24
readme.md
24
readme.md
@ -1,13 +1,12 @@
|
|||||||
# query
|
# query
|
||||||
|
|
||||||
对 fetch 功能的的一部分功能的封装。主要处理header的token的提交和response中的处理json。
|
对应的 fetch 内容的一部分功能的封装。
|
||||||
|
|
||||||
主要目的:请求路径默认`/api/router`,使用`post`,`post`的数据分流使用`path`和`key`.
|
主要目的:请求路径默认`/api/router`,使用`post`,`post`的数据分流使用`path`和`key`.
|
||||||
|
|
||||||
适配后端的项目 [@kevisual/router](https://git.xiongxiao.me/kevisual/router)
|
|
||||||
|
|
||||||
## query
|
## query
|
||||||
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const query = new Query();
|
const query = new Query();
|
||||||
const res = await query.post({
|
const res = await query.post({
|
||||||
@ -34,21 +33,4 @@ type Data = {
|
|||||||
type DataOpts = Partial<QueryOpts> & {
|
type DataOpts = Partial<QueryOpts> & {
|
||||||
beforeRequest?: Fn;
|
beforeRequest?: Fn;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
## 适配的@kevisual/router的代码
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { App } from '@kevisual/router';
|
|
||||||
const app = new App();
|
|
||||||
|
|
||||||
app
|
|
||||||
.route({
|
|
||||||
path: 'demo',
|
|
||||||
key: '1',
|
|
||||||
})
|
|
||||||
.define(async (ctx) => {
|
|
||||||
ctx.body = 234;
|
|
||||||
})
|
|
||||||
.addTo(app);
|
|
||||||
```
|
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import typescript from '@rollup/plugin-typescript';
|
import typescript from '@rollup/plugin-typescript';
|
||||||
import resolve from '@rollup/plugin-node-resolve';
|
import resolve from '@rollup/plugin-node-resolve';
|
||||||
import { dts } from 'rollup-plugin-dts';
|
|
||||||
/**
|
/**
|
||||||
* @type {import('rollup').RollupOptions}
|
* @type {import('rollup').RollupOptions}
|
||||||
*/
|
*/
|
||||||
@ -10,91 +10,43 @@ export default [
|
|||||||
{
|
{
|
||||||
input: 'src/index.ts', // TypeScript 入口文件
|
input: 'src/index.ts', // TypeScript 入口文件
|
||||||
output: {
|
output: {
|
||||||
file: 'dist/query-browser.js', // 输出文件
|
file: 'dist/index.js', // 输出文件
|
||||||
format: 'es', // 输出格式设置为 ES 模块
|
format: 'es', // 输出格式设置为 ES 模块
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
resolve(), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
|
resolve(), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
|
||||||
typescript(), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
|
typescript({
|
||||||
|
allowImportingTsExtensions: true,
|
||||||
|
noEmit: true,
|
||||||
|
}), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: 'src/index.ts', // TypeScript 入口文件
|
input: 'src/node-adapter.ts', // TypeScript 入口文件
|
||||||
output: {
|
output: {
|
||||||
file: 'dist/query-browser.d.ts', // 输出文件
|
file: 'dist/node-adapter.js', // 输出文件
|
||||||
format: 'es', // 输出格式设置为 ES 模块
|
|
||||||
},
|
|
||||||
plugins: [dts()],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'src/query.ts',
|
|
||||||
output: {
|
|
||||||
file: 'dist/query.js',
|
|
||||||
format: 'es',
|
|
||||||
},
|
|
||||||
moduleSideEffects: false, // 确保无副作用的模块能被完全 tree-shake
|
|
||||||
propertyReadSideEffects: false,
|
|
||||||
|
|
||||||
plugins: [resolve(), typescript()],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'src/query.ts',
|
|
||||||
output: {
|
|
||||||
file: 'dist/query.d.ts',
|
|
||||||
format: 'es',
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins: [dts()],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'src/ws.ts', // TypeScript 入口文件
|
|
||||||
output: {
|
|
||||||
file: 'dist/query-ws.js', // 输出文件
|
|
||||||
format: 'es', // 输出格式设置为 ES 模块
|
format: 'es', // 输出格式设置为 ES 模块
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
resolve(), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
|
resolve(), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
|
||||||
typescript(), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
|
typescript({
|
||||||
|
allowImportingTsExtensions: true,
|
||||||
|
noEmit: true,
|
||||||
|
}), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: 'src/ws.ts', // TypeScript 入口文件
|
input: 'src/ws.ts', // TypeScript 入口文件
|
||||||
output: {
|
output: {
|
||||||
file: 'dist/query-ws.d.ts', // 输出文件
|
file: 'dist/ws.js', // 输出文件
|
||||||
format: 'es', // 输出格式设置为 ES 模块
|
format: 'es', // 输出格式设置为 ES 模块
|
||||||
},
|
},
|
||||||
plugins: [dts()],
|
plugins: [
|
||||||
},
|
resolve(), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
|
||||||
{
|
typescript({
|
||||||
input: 'src/adapter.ts',
|
allowImportingTsExtensions: true,
|
||||||
output: {
|
noEmit: true,
|
||||||
file: 'dist/query-adapter.js',
|
}), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
|
||||||
format: 'es',
|
],
|
||||||
},
|
|
||||||
plugins: [resolve(), typescript()],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'src/adapter.ts', // TypeScript 入口文件
|
|
||||||
output: {
|
|
||||||
file: 'dist/query-adapter.d.ts', // 输出文件
|
|
||||||
format: 'es', // 输出格式设置为 ES 模块
|
|
||||||
},
|
|
||||||
plugins: [dts()],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'src/query-ai.ts',
|
|
||||||
output: {
|
|
||||||
file: 'dist/query-ai.js',
|
|
||||||
format: 'es',
|
|
||||||
},
|
|
||||||
plugins: [resolve(), typescript()],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'src/query-ai.ts', // TypeScript 入口文件
|
|
||||||
output: {
|
|
||||||
file: 'dist/query-ai.d.ts', // 输出文件
|
|
||||||
format: 'es', // 输出格式设置为 ES 模块
|
|
||||||
},
|
|
||||||
plugins: [dts()],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -1,89 +1,34 @@
|
|||||||
export const methods = ['GET', 'POST'] as const;
|
type AdapterOpts = {
|
||||||
export type Method = (typeof methods)[number];
|
url: string;
|
||||||
|
|
||||||
type SimpleObject = Record<string, any>;
|
|
||||||
export type AdapterOpts = {
|
|
||||||
url?: string;
|
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
body?: Record<string, any> | FormData; // body 可以是对象、字符串或 FormData
|
body?: Record<string, any>;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
method?: Method;
|
|
||||||
isBlob?: boolean; // 是否返回 Blob 对象, 第一优先
|
|
||||||
isText?: boolean; // 是否返回文本内容, 第二优先
|
|
||||||
isPostFile?: boolean; // 是否为文件上传
|
|
||||||
};
|
};
|
||||||
export const isTextForContentType = (contentType: string | null) => {
|
export const adapter = async (opts: AdapterOpts) => {
|
||||||
if (!contentType) return false;
|
|
||||||
const textTypes = ['text/', 'xml', 'html', 'javascript', 'css', 'csv', 'plain', 'x-www-form-urlencoded'];
|
|
||||||
return textTypes.some((type) => contentType.includes(type));
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param opts
|
|
||||||
* @param overloadOpts 覆盖fetch的默认配置
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const adapter = async (opts: AdapterOpts = {}, overloadOpts?: RequestInit) => {
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const signal = controller.signal;
|
const signal = controller.signal;
|
||||||
const isBlob = opts.isBlob || false; // 是否返回 Blob 对象
|
const timeout = opts.timeout || 60000; // 默认超时时间为 60s
|
||||||
const isText = opts.isText || false; // 是否返回文本内容
|
|
||||||
const isPostFile = opts.isPostFile || false; // 是否为文件上传
|
|
||||||
const timeout = opts.timeout || 60000 * 3; // 默认超时时间为 60s * 3
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
controller.abort();
|
controller.abort();
|
||||||
}, timeout);
|
}, timeout);
|
||||||
let method = overloadOpts?.method || opts?.method || 'POST';
|
|
||||||
let headers = { ...opts?.headers, ...overloadOpts?.headers };
|
return fetch(opts.url, {
|
||||||
let origin = '';
|
method: 'POST',
|
||||||
let url: URL;
|
headers: {
|
||||||
if (opts?.url?.startsWith('http')) {
|
|
||||||
url = new URL(opts.url);
|
|
||||||
} else {
|
|
||||||
origin = window?.location?.origin || 'http://localhost:51015';
|
|
||||||
url = new URL(opts.url, origin);
|
|
||||||
}
|
|
||||||
const isGet = method === 'GET';
|
|
||||||
if (isGet) {
|
|
||||||
url.search = new URLSearchParams(opts.body as SimpleObject).toString();
|
|
||||||
}
|
|
||||||
let body: string | FormData | undefined = undefined;
|
|
||||||
if (isGet) {
|
|
||||||
body = undefined;
|
|
||||||
} else if (isPostFile) {
|
|
||||||
body = opts.body as FormData; // 如果是文件上传,直接使用 FormData
|
|
||||||
} else {
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...headers,
|
...opts.headers,
|
||||||
};
|
},
|
||||||
body = JSON.stringify(opts.body); // 否则将对象转换为 JSON 字符串
|
body: JSON.stringify(opts.body),
|
||||||
}
|
|
||||||
return fetch(url, {
|
|
||||||
method: method.toUpperCase(),
|
|
||||||
signal,
|
signal,
|
||||||
body: body,
|
|
||||||
...overloadOpts,
|
|
||||||
headers: headers,
|
|
||||||
})
|
})
|
||||||
.then(async (response) => {
|
.then((response) => {
|
||||||
// 获取 Content-Type 头部信息
|
// 获取 Content-Type 头部信息
|
||||||
const contentType = response.headers.get('Content-Type');
|
const contentType = response.headers.get('Content-Type');
|
||||||
if (isBlob) {
|
|
||||||
return await response.blob(); // 直接返回 Blob 对象
|
|
||||||
}
|
|
||||||
const isJson = contentType && contentType.includes('application/json');
|
|
||||||
// 判断返回的数据类型
|
// 判断返回的数据类型
|
||||||
if (isJson && !isText) {
|
if (contentType && contentType.includes('application/json')) {
|
||||||
return await response.json(); // 解析为 JSON
|
return response.json(); // 解析为 JSON
|
||||||
} else if (isTextForContentType(contentType)) {
|
|
||||||
return {
|
|
||||||
code: 200,
|
|
||||||
status: response.status,
|
|
||||||
data: await response.text(), // 直接返回文本内容
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
return response;
|
return response.text(); // 解析为文本
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -99,7 +44,3 @@ export const adapter = async (opts: AdapterOpts = {}, overloadOpts?: RequestInit
|
|||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* adapter
|
|
||||||
*/
|
|
||||||
export const queryFetch = adapter;
|
|
||||||
|
129
src/index.ts
129
src/index.ts
@ -1 +1,128 @@
|
|||||||
export * from './query-browser.ts'
|
import { adapter } from './adapter.ts';
|
||||||
|
import { QueryWs } from './ws.ts';
|
||||||
|
export { QueryOpts };
|
||||||
|
type Fn = (opts: {
|
||||||
|
url?: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
body?: Record<string, any>;
|
||||||
|
[key: string]: any;
|
||||||
|
timeout?: number;
|
||||||
|
}) => Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
type QueryOpts = {
|
||||||
|
url?: string;
|
||||||
|
adapter?: typeof adapter;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
timeout?: number;
|
||||||
|
};
|
||||||
|
type Data = {
|
||||||
|
path?: string;
|
||||||
|
key?: string;
|
||||||
|
payload?: Record<string, any>;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
type Result<S = any> = {
|
||||||
|
code: number;
|
||||||
|
data?: S;
|
||||||
|
message?: string;
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
// 额外功能
|
||||||
|
type DataOpts = Partial<QueryOpts> & {
|
||||||
|
beforeRequest?: Fn;
|
||||||
|
afterResponse?: (result: Result) => Promise<any>;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* const query = new Query();
|
||||||
|
* const res = await query.post({
|
||||||
|
* path: 'demo',
|
||||||
|
* key: '1',
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* U是参数 V是返回值
|
||||||
|
*/
|
||||||
|
export class Query<U = any, V = any> {
|
||||||
|
adapter: typeof adapter;
|
||||||
|
url: string;
|
||||||
|
beforeRequest?: Fn;
|
||||||
|
afterResponse?: (result: Result) => Promise<any>;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
timeout?: number;
|
||||||
|
constructor(opts?: QueryOpts) {
|
||||||
|
this.adapter = opts?.adapter || adapter;
|
||||||
|
this.url = opts?.url || '/api/router';
|
||||||
|
this.headers = opts?.headers || {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
this.timeout = opts?.timeout || 60000; // 默认超时时间为 60s
|
||||||
|
}
|
||||||
|
async get<T = any, S = any>(params: Record<string, any> & Data & U & T, options?: DataOpts): Promise<Result<V & S>> {
|
||||||
|
return this.post(params, options);
|
||||||
|
}
|
||||||
|
async post<T = any, S = any>(body: Record<string, any> & Data & T, options?: DataOpts): Promise<Result<S>> {
|
||||||
|
const url = options?.url || this.url;
|
||||||
|
const headers = { ...this.headers, ...options?.headers };
|
||||||
|
const adapter = options?.adapter || this.adapter;
|
||||||
|
const beforeRequest = options?.beforeRequest || this.beforeRequest;
|
||||||
|
const afterResponse = options?.afterResponse || this.afterResponse;
|
||||||
|
const timeout = options?.timeout || this.timeout;
|
||||||
|
const req = {
|
||||||
|
url: url,
|
||||||
|
headers: headers,
|
||||||
|
body,
|
||||||
|
timeout,
|
||||||
|
};
|
||||||
|
if (beforeRequest) {
|
||||||
|
await beforeRequest(req);
|
||||||
|
}
|
||||||
|
return adapter(req).then(async (res) => {
|
||||||
|
res.success = res.code === 200;
|
||||||
|
if (afterResponse) {
|
||||||
|
return await afterResponse(res);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
before(fn: Fn) {
|
||||||
|
this.beforeRequest = fn;
|
||||||
|
}
|
||||||
|
after(fn: (result: Result) => Promise<any>) {
|
||||||
|
this.afterResponse = fn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 前端调用后端QueryRouter
|
||||||
|
*/
|
||||||
|
export class QueryClient<U = any, V = any> extends Query<U, V> {
|
||||||
|
tokenName: string;
|
||||||
|
storage: Storage;
|
||||||
|
token: string;
|
||||||
|
qws: QueryWs;
|
||||||
|
constructor(opts?: QueryOpts & { tokenName?: string; storage?: Storage }) {
|
||||||
|
super(opts);
|
||||||
|
this.tokenName = opts?.tokenName || 'token';
|
||||||
|
this.storage = opts?.storage || localStorage;
|
||||||
|
this.beforeRequest = async (opts) => {
|
||||||
|
const token = this.token || this.getToken();
|
||||||
|
if (token) {
|
||||||
|
opts.headers = {
|
||||||
|
...opts.headers,
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return opts;
|
||||||
|
};
|
||||||
|
this.qws = new QueryWs({ url: opts?.url });
|
||||||
|
}
|
||||||
|
getToken() {
|
||||||
|
return this.storage.getItem(this.tokenName);
|
||||||
|
}
|
||||||
|
saveToken(token: string) {
|
||||||
|
this.storage.setItem(this.tokenName, token);
|
||||||
|
}
|
||||||
|
removeToken() {
|
||||||
|
this.storage.removeItem(this.tokenName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { adapter };
|
||||||
|
57
src/node-adapter.ts
Normal file
57
src/node-adapter.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import http from 'http';
|
||||||
|
|
||||||
|
type AdapterOpts = {
|
||||||
|
url: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
body?: Record<string, any>;
|
||||||
|
};
|
||||||
|
export const nodeAdapter = async (opts: AdapterOpts): Promise<any> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const postData = JSON.stringify(opts.body || '');
|
||||||
|
const _url = new URL(opts.url);
|
||||||
|
const { hostname, port, pathname } = _url;
|
||||||
|
const options = {
|
||||||
|
hostname: hostname,
|
||||||
|
port: port,
|
||||||
|
path: pathname || '/api/router',
|
||||||
|
method: 'POST', // Assuming it's a POST request
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(postData),
|
||||||
|
...opts.headers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = http.request(options, (res) => {
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
// Collect data chunks
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resolve when the response is complete
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(data);
|
||||||
|
resolve(parsedData);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle request errors
|
||||||
|
req.on('error', (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write the request body and end the request
|
||||||
|
if (opts.body) {
|
||||||
|
req.write(postData);
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const adapter = nodeAdapter;
|
@ -1,58 +0,0 @@
|
|||||||
import OpenAI, { ClientOptions } from 'openai';
|
|
||||||
import type { RequestOptions } from 'openai/core.mjs';
|
|
||||||
|
|
||||||
type QueryOpts = {
|
|
||||||
/**
|
|
||||||
* OpenAI model name, example: deepseek-chat
|
|
||||||
*/
|
|
||||||
model: string;
|
|
||||||
/**
|
|
||||||
* OpenAI client options
|
|
||||||
* QueryAi.init() will be called with these options
|
|
||||||
*/
|
|
||||||
openAiOpts?: ClientOptions;
|
|
||||||
openai?: OpenAI;
|
|
||||||
};
|
|
||||||
export class QueryAI {
|
|
||||||
private openai: OpenAI;
|
|
||||||
model?: string;
|
|
||||||
constructor(opts?: QueryOpts) {
|
|
||||||
this.model = opts?.model;
|
|
||||||
if (opts?.openai) {
|
|
||||||
this.openai = opts.openai;
|
|
||||||
} else if (opts?.openAiOpts) {
|
|
||||||
this.init(opts?.openAiOpts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
init(opts: ClientOptions) {
|
|
||||||
this.openai = new OpenAI(opts);
|
|
||||||
}
|
|
||||||
async query(prompt: string, opts?: RequestOptions) {
|
|
||||||
return this.openai.chat.completions.create({
|
|
||||||
model: this.model,
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: prompt,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stream: false,
|
|
||||||
...opts,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async queryAsync(prompt: string, opts?: RequestOptions) {
|
|
||||||
return this.openai.chat.completions.create({
|
|
||||||
model: this.model,
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: prompt,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stream: true,
|
|
||||||
...opts,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { OpenAI };
|
|
@ -1,56 +0,0 @@
|
|||||||
import { adapter } from './adapter.ts';
|
|
||||||
import { QueryWs, QueryWsOpts } from './ws.ts';
|
|
||||||
import { Query, ClientQuery } from './query.ts';
|
|
||||||
import { BaseQuery, wrapperError } from './query.ts';
|
|
||||||
|
|
||||||
export { QueryOpts, QueryWs, ClientQuery, Query, QueryWsOpts, adapter, BaseQuery, wrapperError };
|
|
||||||
|
|
||||||
export type { DataOpts, Result, Data } from './query.ts';
|
|
||||||
|
|
||||||
type QueryOpts = {
|
|
||||||
url?: string;
|
|
||||||
adapter?: typeof adapter;
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
timeout?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 前端调用后端QueryRouter, 封装 beforeRequest 和 wss
|
|
||||||
*/
|
|
||||||
export class QueryClient extends Query {
|
|
||||||
tokenName: string;
|
|
||||||
storage: Storage;
|
|
||||||
token: string;
|
|
||||||
constructor(opts?: QueryOpts & { tokenName?: string; storage?: Storage; io?: boolean }) {
|
|
||||||
super(opts);
|
|
||||||
this.tokenName = opts?.tokenName || 'token';
|
|
||||||
this.storage = opts?.storage || localStorage;
|
|
||||||
this.beforeRequest = async (opts) => {
|
|
||||||
const token = this.token || this.getToken();
|
|
||||||
if (token) {
|
|
||||||
opts.headers = {
|
|
||||||
...opts.headers,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return opts;
|
|
||||||
};
|
|
||||||
if (opts?.io) {
|
|
||||||
this.createWs();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
createWs(opts?: QueryWsOpts) {
|
|
||||||
this.qws = new QueryWs({ url: this.url, ...opts });
|
|
||||||
}
|
|
||||||
getToken() {
|
|
||||||
return this.storage.getItem(this.tokenName);
|
|
||||||
}
|
|
||||||
saveToken(token: string) {
|
|
||||||
this.storage.setItem(this.tokenName, token);
|
|
||||||
}
|
|
||||||
removeToken() {
|
|
||||||
this.storage.removeItem(this.tokenName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 移除默认生成的实例
|
|
||||||
// export const client = new QueryClient();
|
|
282
src/query.ts
282
src/query.ts
@ -1,282 +0,0 @@
|
|||||||
import { adapter, isTextForContentType, Method, AdapterOpts } from './adapter.ts';
|
|
||||||
import type { QueryWs } from './ws.ts';
|
|
||||||
/**
|
|
||||||
* 请求前处理函数
|
|
||||||
* @param opts 请求配置
|
|
||||||
* @returns 请求配置
|
|
||||||
*/
|
|
||||||
export type Fn = (opts: {
|
|
||||||
url?: string;
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
body?: Record<string, any>;
|
|
||||||
[key: string]: any;
|
|
||||||
timeout?: number;
|
|
||||||
}) => Promise<Record<string, any> | false>;
|
|
||||||
|
|
||||||
export type QueryOpts = {
|
|
||||||
adapter?: typeof adapter;
|
|
||||||
[key: string]: any;
|
|
||||||
} & AdapterOpts;
|
|
||||||
export type Data = {
|
|
||||||
path?: string;
|
|
||||||
key?: string;
|
|
||||||
payload?: Record<string, any>;
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
export type Result<S = any> = {
|
|
||||||
code: number;
|
|
||||||
data?: S;
|
|
||||||
message?: string;
|
|
||||||
success: boolean;
|
|
||||||
/**
|
|
||||||
* 是否不返回 message
|
|
||||||
*/
|
|
||||||
noMsg?: boolean;
|
|
||||||
/**
|
|
||||||
* 显示错误, 当 handle的信息被处理的时候,如果不是success,同时自己设置了noMsg,那么就不显示错误信息了,因为被处理。
|
|
||||||
*
|
|
||||||
* 日常: fetch().then(res=>if(res.sucess){message.error('error')})的时候,比如401被处理过了,就不再提示401错误了。
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
showError: (fn?: () => void) => void;
|
|
||||||
};
|
|
||||||
// 额外功能
|
|
||||||
export type DataOpts = Partial<QueryOpts> & {
|
|
||||||
beforeRequest?: Fn;
|
|
||||||
afterResponse?: <S = any>(result: Result<S>, ctx?: { req?: any; res?: any; fetch?: any }) => Promise<Result<S>>;
|
|
||||||
/**
|
|
||||||
* 是否在stop的时候不请求
|
|
||||||
*/
|
|
||||||
noStop?: boolean;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* 设置基础响应, 设置 success 和 showError,
|
|
||||||
* success 是 code 是否等于 200
|
|
||||||
* showError 是 如果 success 为 false 且 noMsg 为 false, 则调用 showError
|
|
||||||
* @param res 响应
|
|
||||||
*/
|
|
||||||
export const setBaseResponse = (res: Partial<Result>) => {
|
|
||||||
res.success = res.code === 200;
|
|
||||||
/**
|
|
||||||
* 显示错误
|
|
||||||
* @param fn 错误处理函数
|
|
||||||
*/
|
|
||||||
res.showError = (fn?: () => void) => {
|
|
||||||
if (!res.success && !res.noMsg) {
|
|
||||||
fn?.();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return res as Result;
|
|
||||||
};
|
|
||||||
export const wrapperError = ({ code, message }: { code?: number; message?: string }) => {
|
|
||||||
const result = {
|
|
||||||
code: code || 500,
|
|
||||||
success: false,
|
|
||||||
message: message || 'api request error',
|
|
||||||
showError: (fn?: () => void) => {
|
|
||||||
//
|
|
||||||
},
|
|
||||||
noMsg: true,
|
|
||||||
};
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* const query = new Query();
|
|
||||||
* const res = await query.post({
|
|
||||||
* path: 'demo',
|
|
||||||
* key: '1',
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* U是参数 V是返回值
|
|
||||||
*/
|
|
||||||
export class Query {
|
|
||||||
adapter: typeof adapter;
|
|
||||||
url: string;
|
|
||||||
beforeRequest?: DataOpts['beforeRequest'];
|
|
||||||
afterResponse?: DataOpts['afterResponse'];
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
timeout?: number;
|
|
||||||
/**
|
|
||||||
* 需要突然停止请求,比如401的时候
|
|
||||||
*/
|
|
||||||
stop?: boolean;
|
|
||||||
// 默认不使用ws
|
|
||||||
qws: QueryWs;
|
|
||||||
|
|
||||||
constructor(opts?: QueryOpts) {
|
|
||||||
this.adapter = opts?.adapter || adapter;
|
|
||||||
this.url = opts?.url || '/api/router';
|
|
||||||
this.headers = opts?.headers || {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
};
|
|
||||||
this.timeout = opts?.timeout || 60000 * 3; // 默认超时时间为 60s * 3
|
|
||||||
}
|
|
||||||
setQueryWs(qws: QueryWs) {
|
|
||||||
this.qws = qws;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 突然停止请求
|
|
||||||
*/
|
|
||||||
setStop(stop: boolean) {
|
|
||||||
this.stop = stop;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 发送 get 请求,转到 post 请求
|
|
||||||
* T是请求类型自定义
|
|
||||||
* S是返回类型自定义
|
|
||||||
* @param params 请求参数
|
|
||||||
* @param options 请求配置
|
|
||||||
* @returns 请求结果
|
|
||||||
*/
|
|
||||||
async get<R = any, P = any>(params: Data & P, options?: DataOpts): Promise<Result<R>> {
|
|
||||||
return this.post(params, options);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 发送 post 请求
|
|
||||||
* T是请求类型自定义
|
|
||||||
* S是返回类型自定义
|
|
||||||
* @param body 请求体
|
|
||||||
* @param options 请求配置
|
|
||||||
* @returns 请求结果
|
|
||||||
*/
|
|
||||||
async post<R = any, P = any>(body: Data & P, options?: DataOpts): Promise<Result<R>> {
|
|
||||||
const url = options?.url || this.url;
|
|
||||||
const { headers, adapter, beforeRequest, afterResponse, timeout, ...rest } = options || {};
|
|
||||||
const _headers = { ...this.headers, ...headers };
|
|
||||||
const _adapter = adapter || this.adapter;
|
|
||||||
const _beforeRequest = beforeRequest || this.beforeRequest;
|
|
||||||
const _afterResponse = afterResponse || this.afterResponse;
|
|
||||||
const _timeout = timeout || this.timeout;
|
|
||||||
const req = {
|
|
||||||
url: url,
|
|
||||||
headers: _headers,
|
|
||||||
body,
|
|
||||||
timeout: _timeout,
|
|
||||||
...rest,
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
if (_beforeRequest) {
|
|
||||||
const res = await _beforeRequest(req);
|
|
||||||
if (res === false) {
|
|
||||||
return wrapperError({
|
|
||||||
code: 500,
|
|
||||||
message: 'request is cancel',
|
|
||||||
// @ts-ignore
|
|
||||||
req: req,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('request beforeFn error', e, req);
|
|
||||||
return wrapperError({
|
|
||||||
code: 500,
|
|
||||||
message: 'api request beforeFn error',
|
|
||||||
// @ts-ignore
|
|
||||||
req: req,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (this.stop && !options?.noStop) {
|
|
||||||
const that = this;
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
let timer = 0;
|
|
||||||
const detect = setInterval(() => {
|
|
||||||
if (!that.stop) {
|
|
||||||
clearInterval(detect);
|
|
||||||
resolve(true);
|
|
||||||
}
|
|
||||||
timer++;
|
|
||||||
if (timer > 30) {
|
|
||||||
console.error('request stop: timeout', req.url, timer);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return _adapter(req).then(async (res) => {
|
|
||||||
try {
|
|
||||||
setBaseResponse(res);
|
|
||||||
if (_afterResponse) {
|
|
||||||
return await _afterResponse(res, {
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
fetch: adapter,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('request afterFn error', e, req);
|
|
||||||
return wrapperError({
|
|
||||||
code: 500,
|
|
||||||
message: 'api request afterFn error',
|
|
||||||
// @ts-ignore
|
|
||||||
req: req,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 请求前处理,设置请求前处理函数
|
|
||||||
* @param fn 处理函数
|
|
||||||
*/
|
|
||||||
before(fn: DataOpts['beforeRequest']) {
|
|
||||||
this.beforeRequest = fn;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 请求后处理,设置请求后处理函数
|
|
||||||
* @param fn 处理函数
|
|
||||||
*/
|
|
||||||
after(fn: DataOpts['afterResponse']) {
|
|
||||||
this.afterResponse = fn;
|
|
||||||
}
|
|
||||||
async fetchText(urlOrOptions?: string | QueryOpts, options?: QueryOpts): Promise<Result<any>> {
|
|
||||||
let _options = { ...options };
|
|
||||||
if (typeof urlOrOptions === 'string' && !_options.url) {
|
|
||||||
_options.url = urlOrOptions;
|
|
||||||
}
|
|
||||||
if (typeof urlOrOptions === 'object') {
|
|
||||||
_options = { ...urlOrOptions, ..._options };
|
|
||||||
}
|
|
||||||
const res = await adapter({
|
|
||||||
method: 'GET',
|
|
||||||
..._options,
|
|
||||||
headers: {
|
|
||||||
...this.headers,
|
|
||||||
...(_options?.headers || {}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return setBaseResponse(res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { adapter };
|
|
||||||
|
|
||||||
export class BaseQuery<T extends Query = Query, R extends { queryChain?: any; query?: any } = { queryChain: any; query?: T }> {
|
|
||||||
query: T;
|
|
||||||
queryDefine: R;
|
|
||||||
constructor(opts?: { query?: T; queryDefine?: R; clientQuery?: T }) {
|
|
||||||
if (opts?.clientQuery) {
|
|
||||||
this.query = opts.clientQuery;
|
|
||||||
} else {
|
|
||||||
this.query = opts?.query;
|
|
||||||
}
|
|
||||||
if (opts.queryDefine) {
|
|
||||||
this.queryDefine = opts.queryDefine;
|
|
||||||
this.queryDefine.query = this.query;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
get chain(): R['queryChain'] {
|
|
||||||
return this.queryDefine.queryChain;
|
|
||||||
}
|
|
||||||
post<R = any, P = any>(data: P, options?: DataOpts): Promise<Result<R>> {
|
|
||||||
return this.query.post(data, options);
|
|
||||||
}
|
|
||||||
get<R = any, P = any>(data: P, options?: DataOpts): Promise<Result<R>> {
|
|
||||||
return this.query.get(data, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ClientQuery extends Query {
|
|
||||||
constructor(opts?: QueryOpts) {
|
|
||||||
super({ ...opts, url: opts?.url || '/client/router' });
|
|
||||||
}
|
|
||||||
}
|
|
106
src/ws.ts
106
src/ws.ts
@ -9,7 +9,7 @@ type QueryWsStore = {
|
|||||||
};
|
};
|
||||||
export type QuerySelectState = 'connecting' | 'connected' | 'disconnected';
|
export type QuerySelectState = 'connecting' | 'connected' | 'disconnected';
|
||||||
export type QueryWsStoreListener = (newState: QueryWsStore, oldState: QueryWsStore) => void;
|
export type QueryWsStoreListener = (newState: QueryWsStore, oldState: QueryWsStore) => void;
|
||||||
export type QueryWsOpts = {
|
type QueryWsOpts = {
|
||||||
url?: string;
|
url?: string;
|
||||||
store?: StoreApi<QueryWsStore>;
|
store?: StoreApi<QueryWsStore>;
|
||||||
ws?: WebSocket;
|
ws?: WebSocket;
|
||||||
@ -45,44 +45,24 @@ export class QueryWs {
|
|||||||
/**
|
/**
|
||||||
* 连接 WebSocket
|
* 连接 WebSocket
|
||||||
*/
|
*/
|
||||||
async connect(opts?: { timeout?: number }) {
|
connect() {
|
||||||
const store = this.store;
|
const store = this.store;
|
||||||
const that = this;
|
|
||||||
const connected = store.getState().connected;
|
const connected = store.getState().connected;
|
||||||
if (connected) {
|
if (connected) {
|
||||||
return Promise.resolve(true);
|
return;
|
||||||
}
|
}
|
||||||
return new Promise((resolve, reject) => {
|
const ws = this.ws || new WebSocket(this.url);
|
||||||
const ws = that.ws || new WebSocket(that.url);
|
ws.onopen = () => {
|
||||||
const timeout = opts?.timeout || 5 * 60 * 1000; // 默认 5 分钟
|
store.getState().setConnected(true);
|
||||||
let timer = setTimeout(() => {
|
store.getState().setStatus('connected');
|
||||||
const isOpen = ws.readyState === WebSocket.OPEN;
|
};
|
||||||
if (isOpen) {
|
ws.onclose = () => {
|
||||||
console.log('WebSocket 连接成功 in timer');
|
store.getState().setConnected(false);
|
||||||
resolve(true);
|
store.getState().setStatus('disconnected');
|
||||||
return;
|
this.ws = null;
|
||||||
}
|
};
|
||||||
console.error('WebSocket 连接超时', that.url);
|
|
||||||
resolve(false);
|
|
||||||
}, timeout);
|
|
||||||
ws.onopen = (ev) => {
|
|
||||||
store.getState().setConnected(true);
|
|
||||||
store.getState().setStatus('connected');
|
|
||||||
resolve(true);
|
|
||||||
clearTimeout(timer);
|
|
||||||
};
|
|
||||||
ws.onclose = (ev) => {
|
|
||||||
store.getState().setConnected(false);
|
|
||||||
store.getState().setStatus('disconnected');
|
|
||||||
this.ws = null;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* ws.onopen 必须用这个去获取,否者会丢失链接信息
|
|
||||||
* @param callback
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
listenConnect(callback: () => void) {
|
listenConnect(callback: () => void) {
|
||||||
const store = this.store;
|
const store = this.store;
|
||||||
const { connected } = store.getState();
|
const { connected } = store.getState();
|
||||||
@ -107,41 +87,10 @@ export class QueryWs {
|
|||||||
);
|
);
|
||||||
return cancel;
|
return cancel;
|
||||||
}
|
}
|
||||||
listenClose(callback: () => void) {
|
|
||||||
const store = this.store;
|
|
||||||
const { status } = store.getState();
|
|
||||||
if (status === 'disconnected') {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
const subscriptionOne = (selector: (state: QueryWsStore) => QueryWsStore['status'], listener: QueryWsStoreListener) => {
|
|
||||||
const unsubscribe = store.subscribe((newState: any, oldState: any) => {
|
|
||||||
if (selector(newState) !== selector(oldState)) {
|
|
||||||
listener(newState, oldState);
|
|
||||||
unsubscribe();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return unsubscribe;
|
|
||||||
};
|
|
||||||
const cancel = subscriptionOne(
|
|
||||||
(state) => state.status,
|
|
||||||
(newState, oldState) => {
|
|
||||||
if (newState.status === 'disconnected') {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return cancel;
|
|
||||||
}
|
|
||||||
onMessage<T = any, U = any>(
|
onMessage<T = any, U = any>(
|
||||||
fn: (data: U, event: MessageEvent) => void,
|
fn: (data: U, event: MessageEvent) => void,
|
||||||
opts?: {
|
opts?: {
|
||||||
/**
|
|
||||||
* 是否将数据转换为 JSON
|
|
||||||
*/
|
|
||||||
isJson?: boolean;
|
isJson?: boolean;
|
||||||
/**
|
|
||||||
* 选择器
|
|
||||||
*/
|
|
||||||
selector?: (data: T) => U;
|
selector?: (data: T) => U;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@ -178,26 +127,7 @@ export class QueryWs {
|
|||||||
store.getState().setConnected(false);
|
store.getState().setConnected(false);
|
||||||
store.getState().setStatus('disconnected');
|
store.getState().setStatus('disconnected');
|
||||||
}
|
}
|
||||||
/**
|
send<T = any, U = any>(data: T, opts?: { isJson?: boolean; wrapper?: (data: T) => U }) {
|
||||||
* 发送消息
|
|
||||||
*
|
|
||||||
* @param data
|
|
||||||
* @param opts
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
send<T = any, U = any>(
|
|
||||||
data: T,
|
|
||||||
opts?: {
|
|
||||||
/**
|
|
||||||
* 是否将数据转换为 JSON
|
|
||||||
*/
|
|
||||||
isJson?: boolean;
|
|
||||||
/**
|
|
||||||
* 包装数据
|
|
||||||
*/
|
|
||||||
wrapper?: (data: T) => U;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const ws = this.ws;
|
const ws = this.ws;
|
||||||
const isJson = opts?.isJson ?? true;
|
const isJson = opts?.isJson ?? true;
|
||||||
const wrapper = opts?.wrapper;
|
const wrapper = opts?.wrapper;
|
||||||
@ -211,10 +141,4 @@ export class QueryWs {
|
|||||||
ws.send(data as string);
|
ws.send(data as string);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getOpen() {
|
|
||||||
if (!this.ws) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.ws.readyState === WebSocket.OPEN;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
12
test/adapter.ts
Normal file
12
test/adapter.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { adapter } from '../src/adapter';
|
||||||
|
const hostname = 'localhost:3002';
|
||||||
|
|
||||||
|
describe('Adapter', () => {
|
||||||
|
// 编写一个测试用例
|
||||||
|
// yarn test --testNamePattern='Adapter'
|
||||||
|
test('Adapter:First', () => {
|
||||||
|
adapter({ url: hostname + '/api/router' }).then((res) => {
|
||||||
|
expect(res).toEqual({ id: 1 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
7
test/hello.test.ts
Normal file
7
test/hello.test.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
describe('Hello', () => {
|
||||||
|
// 编写一个测试用例
|
||||||
|
// yarn test --testNamePattern='Hello'
|
||||||
|
test('Hello World', () => {
|
||||||
|
console.log('Hello World');
|
||||||
|
});
|
||||||
|
});
|
17
test/node-adapter.ts
Normal file
17
test/node-adapter.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { nodeAdapter } from "../src/node-adapter";
|
||||||
|
|
||||||
|
// tsx test/node-adapter.ts
|
||||||
|
const main = async () => {
|
||||||
|
const res = await nodeAdapter({
|
||||||
|
url: 'http://127.0.0.1/api/router',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
path: 'demo',
|
||||||
|
key: '1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(res);
|
||||||
|
};
|
||||||
|
main();
|
25
test/query.test.ts
Normal file
25
test/query.test.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Query } from './../src/index';
|
||||||
|
|
||||||
|
const query = new Query({ url: '/api/router' });
|
||||||
|
|
||||||
|
describe('Query', () => {
|
||||||
|
// 编写一个测试用例
|
||||||
|
// yarn test --testNamePattern='Query'
|
||||||
|
test('Query:First', async () => {
|
||||||
|
console.log('Query');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// test('query', async () => {
|
||||||
|
// query.get({ id: 1 }).then((res) => {
|
||||||
|
// expect(res).toEqual({ id: 1 });
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// describe('Hello', () => {
|
||||||
|
// // 编写一个测试用例
|
||||||
|
// // yarn test --testNamePattern='Hello'
|
||||||
|
// test('Hello World', () => {
|
||||||
|
// console.log('Hello World');
|
||||||
|
// });
|
||||||
|
// });
|
13
test/ws.test.ts
Normal file
13
test/ws.test.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { QueryWs } from '../src/ws';
|
||||||
|
|
||||||
|
const queryWs = new QueryWs({ url: '/api/ws' });
|
||||||
|
|
||||||
|
queryWs.listenConnect(() => {
|
||||||
|
console.log('Connected');
|
||||||
|
});
|
||||||
|
|
||||||
|
queryWs.store.getState().setConnected(true);
|
||||||
|
queryWs.store.getState().setConnected(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
queryWs.store.getState().setConnected(true);
|
||||||
|
}, 1000);
|
@ -8,10 +8,10 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"newLine": "LF",
|
"newLine": "LF",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"declaration": false,
|
|
||||||
"typeRoots": [
|
"typeRoots": [
|
||||||
"node_modules/@types",
|
"node_modules/@types",
|
||||||
],
|
],
|
||||||
|
"declaration": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user