Compare commits
141 Commits
Author | SHA1 | Date | |
---|---|---|---|
a966b0735c | |||
6fed2cdc5c | |||
9fd6bbfc89 | |||
e7a7e1c903 | |||
5e5f4003e0 | |||
23c03fd623 | |||
cab2d8aa2e | |||
1f57f1397c | |||
8a9c2c1505 | |||
b003da7f59 | |||
735890bc3d | |||
69035f49c8 | |||
5ec20ee526 | |||
8b811ffd6b | |||
35df3697c7 | |||
8b4befc828 | |||
77dddd9157 | |||
737f413324 | |||
e613937c43 | |||
9c66752f8b | |||
5c6922c710 | |||
c8e4343ac7 | |||
924bc2c5a7 | |||
2274afcd38 | |||
23aab2adf8 | |||
90311ad65e | |||
407e1383f8 | |||
ad106909e2 | |||
b346da01f1 | |||
51fedb270b | |||
fd26b48ff6 | |||
1bfe10691a | |||
bf81c34dbc | |||
f837bb5230 | |||
1b76e6882f | |||
83b43f501d | |||
9b626a562d | |||
c216f97bfd | |||
71453877d7 | |||
d1a601d006 | |||
0a9236a605 | |||
3a384307ee | |||
c215356b31 | |||
96f37dd470 | |||
10fac39d30 | |||
912572cba5 | |||
b001ebaab8 | |||
aa15da6b92 | |||
f144f27daa | |||
a58c9a0541 | |||
649db1059c | |||
e0c75716d7 | |||
009985c226 | |||
f3f2f8e3bf | |||
cf304ceccd | |||
6f075132c4 | |||
4dca98e81d | |||
1e022d6c68 | |||
f20d737ecf | |||
d791eca5e8 | |||
63c6dac8fa | |||
c2c1dee427 | |||
4ae90a5cf6 | |||
803d4d2894 | |||
fcc75af1ff | |||
0e3bb07a69 | |||
c90aa07ace | |||
362f3f1bd0 | |||
12f7348fec | |||
7d478c400e | |||
ab75cf8720 | |||
f7ef8a6828 | |||
ece2803d12 | |||
2384fc1b76 | |||
309c282379 | |||
90c616ca41 | |||
57177074d0 | |||
d3b5c802cd | |||
8e64353026 | |||
290746c191 | |||
abefef8d7c | |||
81b042e670 | |||
6e3ee011a9 | |||
9b5ff4b1b5 | |||
556ba6cb30 | |||
7321ac680d | |||
2fd8219849 | |||
ea56e2218f | |||
9a07817914 | |||
9bc83b0d1e | |||
98c638e1ab | |||
575ddd36a0 | |||
52b731ce68 | |||
3f6e81b2aa | |||
adad99f6bf | |||
2771c92e85 | |||
440ea9ff3a | |||
51bb8dfa90 | |||
ce3bfa01b4 | |||
265109fca6 | |||
8bfd4d8866 | |||
785f247027 | |||
3f3f488dc4 | |||
0241eda296 | |||
66722759af | |||
e9fad241ee | |||
34face164f | |||
f7bf366962 | |||
046059d228 | |||
78e8171a6a | |||
c97a535035 | |||
dcf198787a | |||
a1e0ebd658 | |||
8bf0a71266 | |||
e499612ecb | |||
e049899599 | |||
37f4d34e7a | |||
ca2e6895ce | |||
ccc5c33656 | |||
546f7f4fc7 | |||
8536060ce4 | |||
e57c332d82 | |||
df87c6c75e | |||
53a4375545 | |||
88022ea14b | |||
2a681644eb | |||
e2708c5bb3 | |||
8c7a71ddce | |||
5acbf420ae | |||
5d673799cc | |||
bb0271e021 | |||
718feb74ae | |||
82aa80d7d9 | |||
67831cd37f | |||
777817b588 | |||
c7dd378eb3 | |||
9d9f67c91a | |||
82cc8c29e1 | |||
625b1c6871 | |||
e149b5a5f1 | |||
2879fb8cee |
@ -12,30 +12,35 @@ stages:
|
||||
- release
|
||||
- metadata
|
||||
|
||||
before_script:
|
||||
- npm install -g @shipzone/npmci
|
||||
|
||||
# ====================
|
||||
# security stage
|
||||
# ====================
|
||||
mirror:
|
||||
auditProductionDependencies:
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
stage: security
|
||||
script:
|
||||
- npmci git mirror
|
||||
- npmci npm prepare
|
||||
- npmci command npm install --production --ignore-scripts
|
||||
- npmci command npm config set registry https://registry.npmjs.org
|
||||
- npmci command npm audit --audit-level=high --only=prod --production
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
allow_failure: true
|
||||
|
||||
audit:
|
||||
auditDevDependencies:
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
stage: security
|
||||
script:
|
||||
- npmci npm prepare
|
||||
- npmci command npm install --ignore-scripts
|
||||
- npmci command npm config set registry https://registry.npmjs.org
|
||||
- npmci command npm audit --audit-level=high
|
||||
- npmci command npm audit --audit-level=high --only=dev
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
allow_failure: true
|
||||
|
||||
# ====================
|
||||
# test stage
|
||||
@ -50,9 +55,7 @@ testStable:
|
||||
- npmci npm test
|
||||
coverage: /\d+.?\d+?\%\s*coverage/
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- priv
|
||||
|
||||
testBuild:
|
||||
stage: test
|
||||
@ -63,9 +66,7 @@ testBuild:
|
||||
- npmci command npm run build
|
||||
coverage: /\d+.?\d+?\%\s*coverage/
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
release:
|
||||
stage: release
|
||||
@ -85,11 +86,12 @@ release:
|
||||
codequality:
|
||||
stage: metadata
|
||||
allow_failure: true
|
||||
only:
|
||||
- tags
|
||||
script:
|
||||
- npmci command npm install -g tslint typescript
|
||||
- npmci command npm install -g typescript
|
||||
- npmci npm prepare
|
||||
- npmci npm install
|
||||
- npmci command "tslint -c tslint.json ./ts/**/*.ts"
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
@ -109,11 +111,10 @@ trigger:
|
||||
pages:
|
||||
stage: metadata
|
||||
script:
|
||||
- npmci node install lts
|
||||
- npmci command npm install -g @gitzone/tsdoc
|
||||
- npmci node install stable
|
||||
- npmci npm prepare
|
||||
- npmci npm install
|
||||
- npmci command tsdoc
|
||||
- npmci command npm run buildDocs
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
|
24
.vscode/launch.json
vendored
24
.vscode/launch.json
vendored
@ -2,28 +2,10 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "current file",
|
||||
"type": "node",
|
||||
"command": "npm test",
|
||||
"name": "Run npm test",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"${relativeFile}"
|
||||
],
|
||||
"runtimeArgs": ["-r", "@gitzone/tsrun"],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"protocol": "inspector",
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
},
|
||||
{
|
||||
"name": "test.ts",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"test/test.ts"
|
||||
],
|
||||
"runtimeArgs": ["-r", "@gitzone/tsrun"],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"protocol": "inspector",
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -15,7 +15,7 @@
|
||||
"properties": {
|
||||
"projectType": {
|
||||
"type": "string",
|
||||
"enum": ["website", "element", "service", "npm"]
|
||||
"enum": ["website", "element", "service", "npm", "wcc"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,17 +2,33 @@
|
||||
"gitzone": {
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "gitlab.com",
|
||||
"gitscope": "apiglobal",
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "api.global",
|
||||
"gitrepo": "typedrequest",
|
||||
"shortDescription": "make typed requests towards apis",
|
||||
"npmPackagename": "@apiglobal/typedrequest",
|
||||
"description": "A TypeScript library for making typed requests towards APIs, including facilities for handling requests, routing, and virtual stream handling.",
|
||||
"npmPackagename": "@api.global/typedrequest",
|
||||
"license": "MIT",
|
||||
"projectDomain": "api.global"
|
||||
"projectDomain": "api.global",
|
||||
"keywords": [
|
||||
"TypeScript",
|
||||
"API Requests",
|
||||
"HTTP",
|
||||
"Typed Programming",
|
||||
"Request Routing",
|
||||
"Response Handling",
|
||||
"Virtual Streams",
|
||||
"Asynchronous",
|
||||
"Network Communication",
|
||||
"Server Communication",
|
||||
"API Integration"
|
||||
]
|
||||
}
|
||||
},
|
||||
"npmci": {
|
||||
"npmGlobalTools": [],
|
||||
"npmAccessLevel": "public"
|
||||
},
|
||||
"tsdoc": {
|
||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||
}
|
||||
}
|
5259
package-lock.json
generated
5259
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
55
package.json
55
package.json
@ -1,32 +1,37 @@
|
||||
{
|
||||
"name": "@apiglobal/typedrequest",
|
||||
"version": "1.0.31",
|
||||
"name": "@api.global/typedrequest",
|
||||
"version": "3.0.21",
|
||||
"private": false,
|
||||
"description": "make typed requests towards apis",
|
||||
"description": "A TypeScript library for making typed requests towards APIs, including facilities for handling requests, routing, and virtual stream handling.",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"type": "module",
|
||||
"author": "Lossless GmbH",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "(tstest test/)",
|
||||
"build": "(tsbuild --web)",
|
||||
"format": "(gitzone format)"
|
||||
"build": "(tsbuild --web --allowimplicitany && tsbundle npm)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gitzone/tsbuild": "^2.1.24",
|
||||
"@gitzone/tstest": "^1.0.33",
|
||||
"@pushrocks/smartexpress": "^3.0.69",
|
||||
"@pushrocks/tapbundle": "^3.2.1",
|
||||
"@types/node": "^14.0.13",
|
||||
"tslint": "^6.1.2",
|
||||
"tslint-config-prettier": "^1.18.0"
|
||||
"@api.global/typedserver": "^3.0.27",
|
||||
"@git.zone/tsbuild": "^2.1.72",
|
||||
"@git.zone/tsbundle": "^2.0.15",
|
||||
"@git.zone/tsrun": "^1.2.44",
|
||||
"@git.zone/tstest": "^1.0.88",
|
||||
"@push.rocks/smartenv": "^5.0.12",
|
||||
"@push.rocks/tapbundle": "^5.0.23",
|
||||
"@types/node": "^20.12.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apiglobal/typedrequest-interfaces": "^1.0.13",
|
||||
"@pushrocks/lik": "^4.0.13",
|
||||
"@pushrocks/smartdelay": "^2.0.9",
|
||||
"@pushrocks/smartjson": "^3.0.10",
|
||||
"@pushrocks/smartrequest": "^1.1.47"
|
||||
"@api.global/typedrequest-interfaces": "^3.0.18",
|
||||
"@push.rocks/isounique": "^1.0.5",
|
||||
"@push.rocks/lik": "^6.0.14",
|
||||
"@push.rocks/smartbuffer": "^3.0.2",
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"@push.rocks/smartpromise": "^4.0.3",
|
||||
"@push.rocks/webrequest": "^3.0.37",
|
||||
"@push.rocks/webstream": "^1.0.8"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@ -39,5 +44,21 @@
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
"readme.md"
|
||||
],
|
||||
"browserslist": [
|
||||
"last 1 chrome versions"
|
||||
],
|
||||
"keywords": [
|
||||
"TypeScript",
|
||||
"API Requests",
|
||||
"HTTP",
|
||||
"Typed Programming",
|
||||
"Request Routing",
|
||||
"Response Handling",
|
||||
"Virtual Streams",
|
||||
"Asynchronous",
|
||||
"Network Communication",
|
||||
"Server Communication",
|
||||
"API Integration"
|
||||
]
|
||||
}
|
||||
|
4751
pnpm-lock.yaml
generated
Normal file
4751
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
readme.hints.md
Normal file
1
readme.hints.md
Normal file
@ -0,0 +1 @@
|
||||
|
152
readme.md
152
readme.md
@ -1,39 +1,135 @@
|
||||
# @apiglobal/typedrequest
|
||||
make typed requests towards apis
|
||||
# @api.global/typedrequest
|
||||
|
||||
## Availabililty and Links
|
||||
* [npmjs.org (npm package)](https://www.npmjs.com/package/@apiglobal/typedrequest)
|
||||
* [gitlab.com (source)](https://gitlab.com/apiglobal/typedrequest)
|
||||
* [github.com (source mirror)](https://github.com/apiglobal/typedrequest)
|
||||
* [docs (typedoc)](https://apiglobal.gitlab.io/typedrequest/)
|
||||
A TypeScript library for making typed requests towards APIs, providing interfaces and classes to handle request/response cycles using typed definitions. It supports building and handling requests, routing based on request types, and virtual stream handling over networks.
|
||||
|
||||
## Status for master
|
||||
## Install
|
||||
|
||||
Status Category | Status Badge
|
||||
-- | --
|
||||
GitLab Pipelines | [](https://lossless.cloud)
|
||||
GitLab Pipline Test Coverage | [](https://lossless.cloud)
|
||||
npm | [](https://lossless.cloud)
|
||||
Snyk | [](https://lossless.cloud)
|
||||
TypeScript Support | [](https://lossless.cloud)
|
||||
node Support | [](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
||||
Code Style | [](https://lossless.cloud)
|
||||
PackagePhobia (total standalone install weight) | [](https://lossless.cloud)
|
||||
PackagePhobia (package size on registry) | [](https://lossless.cloud)
|
||||
BundlePhobia (total size when bundled) | [](https://lossless.cloud)
|
||||
Platform support | [](https://lossless.cloud) [](https://lossless.cloud)
|
||||
To install `@api.global/typedrequest`, you can use npm or yarn. Run the following command in your project directory:
|
||||
|
||||
```
|
||||
npm install @api.global/typedrequest --save
|
||||
```
|
||||
|
||||
Or, if you prefer yarn:
|
||||
|
||||
```
|
||||
yarn add @api.global/typedrequest
|
||||
```
|
||||
|
||||
This package is designed to facilitate making typed requests to APIs, ensuring that both requests and responses adhere to predefined interfaces. This enhances the predictability and reliability of network communication in applications that interact with APIs.
|
||||
|
||||
## Usage
|
||||
|
||||
Use TypeScript for best in class intellisense.
|
||||
Utilizing `@api.global/typedrequest` involves several core concepts, including the creation of typed requests, handling virtual streams, defining request handlers, and routing requests based on their type. Below are comprehensive examples and explanations of how to use these features in your applications.
|
||||
|
||||
## Contribution
|
||||
### Setting Up a Typed Request
|
||||
|
||||
We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :)
|
||||
The foundation of `@api.global/typedrequest` is the `TypedRequest` class, which allows you to define requests that are strictly typed. This ensures that both the request sent and the response received match the expected structures.
|
||||
|
||||
For further information read the linked docs at the top of this readme.
|
||||
First, define an interface that represents your request and response data structure:
|
||||
|
||||
> MIT licensed | **©** [Lossless GmbH](https://lossless.gmbh)
|
||||
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
|
||||
```typescript
|
||||
// Define an interface for your request/response structure
|
||||
interface IUserRequest {
|
||||
method: 'getUser';
|
||||
request: { userId: string };
|
||||
response: { username: string; email: string; };
|
||||
}
|
||||
```
|
||||
|
||||
[](https://maintainedby.lossless.com)
|
||||
Next, create an instance of `TypedRequest` using this interface, and use it to make an API call:
|
||||
|
||||
```typescript
|
||||
import { TypedRequest } from '@api.global/typedrequest';
|
||||
|
||||
// Construct a TypedRequest instance for fetching user data
|
||||
const getUserRequest = new TypedRequest<IUserRequest>('https://your-api.com/users', 'getUser');
|
||||
|
||||
// Execute the request with a specific userId and log the response
|
||||
const userResponse = await getUserRequest.fire({ userId: 'user-123' });
|
||||
console.log(userResponse.username);
|
||||
```
|
||||
|
||||
### Handling Virtual Streams
|
||||
|
||||
`@api.global/typedrequest` introduces the concept of virtual streams, allowing you to manage real-time data transfer over the network as streams. You can create a virtual stream, send data, and read data from it as shown below:
|
||||
|
||||
```typescript
|
||||
import { VirtualStream } from '@api.global/typedrequest';
|
||||
|
||||
// Instantiate a VirtualStream for handling real-time data
|
||||
const myStream = new VirtualStream<ArrayBufferLike>();
|
||||
|
||||
// Use the stream to send data
|
||||
await myStream.sendData(new TextEncoder().encode("Hello, World!"));
|
||||
|
||||
// Retrieve data from the stream
|
||||
const receivedData = await myStream.fetchData();
|
||||
console.log(new TextDecoder().decode(receivedData));
|
||||
```
|
||||
|
||||
### Defining and Using Handlers
|
||||
|
||||
To process requests, you can define handlers that are responsible for specific types of requests:
|
||||
|
||||
```typescript
|
||||
import { TypedHandler } from '@api.global/typedrequest';
|
||||
|
||||
interface IAdditionRequest {
|
||||
method: 'add';
|
||||
request: { a: number; b: number; };
|
||||
response: { result: number; };
|
||||
}
|
||||
|
||||
// Create a handler for processing addition requests
|
||||
const additionHandler = new TypedHandler<IAdditionRequest>('add', async (req) => {
|
||||
return { result: req.a + req.b };
|
||||
});
|
||||
```
|
||||
|
||||
### Routing Requests
|
||||
|
||||
`TypedRouter` is used to route requests to their appropriate handlers based on the request type:
|
||||
|
||||
```typescript
|
||||
import { TypedRouter } from '@api.global/typedrequest';
|
||||
|
||||
const router = new TypedRouter();
|
||||
|
||||
// Associate the additionHandler with the router
|
||||
router.addTypedHandler(additionHandler);
|
||||
|
||||
// The router now directs 'add' requests to the additionHandler
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
`@api.global/typedrequest` provides a `TypedResponseError` class to facilitate error handling in typed requests:
|
||||
|
||||
```typescript
|
||||
import { TypedResponseError } from '@api.global/typedrequest';
|
||||
|
||||
// Throw a TypedResponseError with details about the error
|
||||
throw new TypedResponseError('An error occurred', { detail: 'Error details' });
|
||||
```
|
||||
|
||||
This comprehensive overview covers the essential features provided by `@api.global/typedrequest`. By adhering to defined request and response structures, you can create more reliable and maintainable applications that interact seamlessly with APIs.
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
|
47
test/test.browser.ts
Normal file
47
test/test.browser.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
|
||||
import * as typedrequest from '../ts/index.js';
|
||||
|
||||
let testTypedHandler: typedrequest.TypedHandler<ITestReqRes>;
|
||||
|
||||
// lets define an interface
|
||||
interface ITestReqRes {
|
||||
method: 'hi';
|
||||
request: {
|
||||
name: string;
|
||||
};
|
||||
response: {
|
||||
surname: string;
|
||||
};
|
||||
}
|
||||
|
||||
tap.test('should create a typedHandler', async () => {
|
||||
// lets use the interface in a TypedHandler
|
||||
testTypedHandler = new typedrequest.TypedHandler<ITestReqRes>('hi', async (reqArg) => {
|
||||
return {
|
||||
surname: 'wow',
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('should define a testHandler', async () => {
|
||||
const testTypedRouter = new typedrequest.TypedRouter(); // typed routers can broker typedrequests between handlers
|
||||
testTypedRouter.addTypedHandler(testTypedHandler);
|
||||
});
|
||||
|
||||
tap.skip.test('should fire a request', async () => {
|
||||
let response = await fetch('http://localhost:3000/typedrequest', {
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"body": "{\"correlation\":{\"id\":\"uni_aefe56c1a0f61a3e91082209\",\"phase\":\"request\"},\"method\":\"hi\",\"request\":{\"name\":\"yes\"},\"response\":null}"
|
||||
})
|
||||
console.log(await response.text());
|
||||
});
|
||||
|
||||
tap.skip.test('test', async (tools) => {
|
||||
await tools.delayFor(5000);
|
||||
})
|
||||
|
||||
tap.start();
|
74
test/test.ts
74
test/test.ts
@ -1,11 +1,14 @@
|
||||
import { expect, tap } from '@pushrocks/tapbundle';
|
||||
import * as smartexpress from '@pushrocks/smartexpress';
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import * as typedserver from '@api.global/typedserver';
|
||||
|
||||
import * as typedrequest from '../ts/index';
|
||||
import * as typedrequest from '../ts/index.js';
|
||||
import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
|
||||
|
||||
let testServer: smartexpress.Server;
|
||||
let testServer: typedserver.servertools.Server;
|
||||
let testTypedRouter: typedrequest.TypedRouter;
|
||||
let testTypedHandler: typedrequest.TypedHandler<ITestReqRes>;
|
||||
|
||||
// lets define an interface
|
||||
interface ITestReqRes {
|
||||
method: 'hi';
|
||||
request: {
|
||||
@ -16,29 +19,39 @@ interface ITestReqRes {
|
||||
};
|
||||
}
|
||||
|
||||
interface ITestStream {
|
||||
method: 'handleStream';
|
||||
request: {
|
||||
requestStream: typedrequestInterfaces.IVirtualStream;
|
||||
};
|
||||
response: {
|
||||
responseStream: typedrequestInterfaces.IVirtualStream;
|
||||
};
|
||||
}
|
||||
|
||||
tap.test('should create a typedHandler', async () => {
|
||||
testTypedHandler = new typedrequest.TypedHandler<ITestReqRes>('hi', async reqArg => {
|
||||
// lets use the interface in a TypedHandler
|
||||
testTypedHandler = new typedrequest.TypedHandler<ITestReqRes>('hi', async (reqArg) => {
|
||||
return {
|
||||
surname: 'wow'
|
||||
surname: 'wow',
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('should spawn a server to test with', async () => {
|
||||
testServer = new smartexpress.Server({
|
||||
testServer = new typedserver.servertools.Server({
|
||||
cors: true,
|
||||
forceSsl: false,
|
||||
port: 3000
|
||||
port: 3000,
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('should define a testHandler', async () => {
|
||||
testTypedRouter = new typedrequest.TypedRouter(); // typed routers can broker typedrequests between handlers
|
||||
testTypedRouter.addTypedHandler(testTypedHandler);
|
||||
testServer.addRoute(
|
||||
'/testroute',
|
||||
new smartexpress.Handler('POST', async (req, res) => {
|
||||
console.log(req.body);
|
||||
res.json(await testTypedHandler.addResponse(req.body));
|
||||
})
|
||||
new typedserver.servertools.HandlerTypedRouter(testTypedRouter as any) // the "any" is testspecific, since smartexpress ships with its own version of typedrequest.
|
||||
);
|
||||
});
|
||||
|
||||
@ -52,14 +65,45 @@ tap.test('should fire a request', async () => {
|
||||
'hi'
|
||||
);
|
||||
const response = await typedRequest.fire({
|
||||
name: 'really'
|
||||
name: 'really',
|
||||
});
|
||||
console.log('this is the response:');
|
||||
console.log(response);
|
||||
expect(response.surname).to.equal('wow');
|
||||
expect(response.surname).toEqual('wow');
|
||||
});
|
||||
|
||||
tap.test('should end the server', async () => {
|
||||
tap.test('should allow VirtualStreams', async () => {
|
||||
const newRequestingVS = new typedrequest.VirtualStream();
|
||||
const newRespondingVS = new typedrequest.VirtualStream();
|
||||
let generatedRequestingVS: typedrequestInterfaces.IVirtualStream;
|
||||
let generatedRespondingVS: typedrequestInterfaces.IVirtualStream;
|
||||
testTypedRouter.addTypedHandler(new typedrequest.TypedHandler<ITestStream>('handleStream', async (reqArg) => {
|
||||
console.log('hey there');
|
||||
console.log(reqArg.requestStream);
|
||||
generatedRequestingVS = reqArg.requestStream;
|
||||
return {
|
||||
responseStream: newRespondingVS,
|
||||
};
|
||||
}));
|
||||
const typedRequest = new typedrequest.TypedRequest<ITestStream>(
|
||||
'http://localhost:3000/testroute',
|
||||
'handleStream'
|
||||
);
|
||||
const response = await typedRequest.fire({
|
||||
requestStream: newRequestingVS,
|
||||
});
|
||||
console.log(response.responseStream);
|
||||
|
||||
newRequestingVS.sendData(Buffer.from('hello'));
|
||||
const data = await generatedRequestingVS.fetchData();
|
||||
const decodedData = new TextDecoder().decode(data);
|
||||
expect(decodedData).toEqual('hello');
|
||||
});
|
||||
|
||||
tap.test('should end the server', async (toolsArg) => {
|
||||
await toolsArg.delayFor(1000);
|
||||
await testServer.stop();
|
||||
setTimeout(() => process.exit(0), 100);
|
||||
});
|
||||
|
||||
tap.start();
|
||||
|
8
ts/00_commitinfo_data.ts
Normal file
8
ts/00_commitinfo_data.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@api.global/typedrequest',
|
||||
version: '3.0.21',
|
||||
description: 'A TypeScript library for making typed requests towards APIs, including facilities for handling requests, routing, and virtual stream handling.'
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
export * from './typedrequest.classes.typedrequest';
|
||||
export * from './typedrequest.classes.typedhandler';
|
||||
export * from './typedrequest.classes.typedrouter';
|
||||
export * from './typedrequest.classes.typedrequest.js';
|
||||
export * from './typedrequest.classes.typedhandler.js';
|
||||
export * from './typedrequest.classes.typedrouter.js';
|
||||
export * from './typedrequest.classes.typedresponseerror.js';
|
||||
export * from './typedrequest.classes.typedtarget.js';
|
||||
export * from './typedrequest.classes.virtualstream.js';
|
15
ts/plugins.ts
Normal file
15
ts/plugins.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// apiglobal scope
|
||||
import * as typedRequestInterfaces from '@api.global/typedrequest-interfaces';
|
||||
|
||||
export { typedRequestInterfaces };
|
||||
|
||||
// pushrocks scope
|
||||
import * as isounique from '@push.rocks/isounique';
|
||||
import * as lik from '@push.rocks/lik';
|
||||
import * as smartbuffer from '@push.rocks/smartbuffer';
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as webrequest from '@push.rocks/webrequest';
|
||||
|
||||
|
||||
export { isounique, lik, smartbuffer, smartdelay, smartpromise, webrequest };
|
@ -1,6 +1,7 @@
|
||||
import * as plugins from './typedrequest.plugins';
|
||||
import * as plugins from './plugins.js';
|
||||
import { TypedResponseError } from './typedrequest.classes.typedresponseerror.js';
|
||||
|
||||
type THandlerFunction<T extends plugins.typedRequestInterfaces.ITypedRequest> = (
|
||||
export type THandlerFunction<T extends plugins.typedRequestInterfaces.ITypedRequest> = (
|
||||
requestArg: T['request']
|
||||
) => Promise<T['response']>;
|
||||
|
||||
@ -26,8 +27,28 @@ export class TypedHandler<T extends plugins.typedRequestInterfaces.ITypedRequest
|
||||
'this handler has been given a wrong method to answer to. Please use a TypedRouter to filter requests'
|
||||
);
|
||||
}
|
||||
const response = await this.handlerFunction(typedRequestArg.request);
|
||||
typedRequestArg.response = response;
|
||||
let typedResponseError: TypedResponseError;
|
||||
const response = await this.handlerFunction(typedRequestArg.request).catch((e) => {
|
||||
if (e instanceof TypedResponseError) {
|
||||
typedResponseError = e;
|
||||
} else {
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
|
||||
if (typedResponseError) {
|
||||
typedRequestArg.error = {
|
||||
text: typedResponseError.errorText,
|
||||
data: typedResponseError.errorData,
|
||||
};
|
||||
}
|
||||
|
||||
if (response) {
|
||||
typedRequestArg.response = response;
|
||||
}
|
||||
|
||||
typedRequestArg?.correlation?.phase ? (typedRequestArg.correlation.phase = 'response') : null;
|
||||
|
||||
return typedRequestArg;
|
||||
}
|
||||
}
|
||||
|
@ -1,38 +1,103 @@
|
||||
import * as plugins from './typedrequest.plugins';
|
||||
import * as plugins from './plugins.js';
|
||||
import { VirtualStream } from './typedrequest.classes.virtualstream.js';
|
||||
import { TypedResponseError } from './typedrequest.classes.typedresponseerror.js';
|
||||
import { TypedRouter } from './typedrequest.classes.typedrouter.js';
|
||||
import { TypedTarget } from './typedrequest.classes.typedtarget.js';
|
||||
|
||||
const webrequestInstance = new plugins.webrequest.WebRequest();
|
||||
|
||||
export class TypedRequest<T extends plugins.typedRequestInterfaces.ITypedRequest> {
|
||||
public urlEndPoint: string;
|
||||
/**
|
||||
* in case we post against a url endpoint
|
||||
*/
|
||||
public urlEndPoint?: string;
|
||||
|
||||
/**
|
||||
* in case we post against a TypedTarget
|
||||
*/
|
||||
typedTarget: TypedTarget;
|
||||
|
||||
public method: string;
|
||||
|
||||
// STATIC
|
||||
constructor(urlEndPointArg: string, methodArg: T['method']) {
|
||||
this.urlEndPoint = urlEndPointArg;
|
||||
/**
|
||||
* @param postEndPointArg
|
||||
* @param methodArg
|
||||
*/
|
||||
constructor(postTarget: string | TypedTarget, methodArg: T['method']) {
|
||||
if (typeof postTarget === 'string') {
|
||||
this.urlEndPoint = postTarget;
|
||||
} else {
|
||||
this.typedTarget = postTarget;
|
||||
}
|
||||
this.method = methodArg;
|
||||
}
|
||||
|
||||
/**
|
||||
* firest the request
|
||||
* fires the request
|
||||
*/
|
||||
public async fire(fireArg: T['request']): Promise<T['response']> {
|
||||
const response = await plugins.smartrequest.postJson(this.urlEndPoint, {
|
||||
requestBody: {
|
||||
method: this.method,
|
||||
request: fireArg,
|
||||
response: null
|
||||
public async fire(fireArg: T['request'], useCacheArg: boolean = false): Promise<T['response']> {
|
||||
let payloadSending: plugins.typedRequestInterfaces.ITypedRequest = {
|
||||
method: this.method,
|
||||
request: fireArg,
|
||||
response: null,
|
||||
correlation: {
|
||||
id: plugins.isounique.uni(),
|
||||
phase: 'request',
|
||||
},
|
||||
};
|
||||
|
||||
// lets preprocess the payload
|
||||
payloadSending = VirtualStream.encodePayloadForNetwork(payloadSending, {
|
||||
sendMethod: (payloadArg: plugins.typedRequestInterfaces.IStreamRequest) => {
|
||||
return this.postTrObject(payloadArg) as Promise<plugins.typedRequestInterfaces.IStreamRequest>;
|
||||
}
|
||||
});
|
||||
const responseBody: T = response.body;
|
||||
if (responseBody.error) {
|
||||
console.log(responseBody.error.text);
|
||||
console.log(responseBody.error.data);
|
||||
|
||||
let payloadReceiving: plugins.typedRequestInterfaces.ITypedRequest;
|
||||
payloadReceiving = await this.postTrObject(payloadSending, useCacheArg);
|
||||
|
||||
// lets preprocess the response
|
||||
payloadReceiving = VirtualStream.decodePayloadFromNetwork(payloadReceiving, {
|
||||
sendMethod: (payloadArg: plugins.typedRequestInterfaces.IStreamRequest) => {
|
||||
return this.postTrObject(payloadArg) as Promise<plugins.typedRequestInterfaces.IStreamRequest>;
|
||||
}
|
||||
});
|
||||
return payloadReceiving.response;
|
||||
}
|
||||
|
||||
private async postTrObject(payloadSendingArg: plugins.typedRequestInterfaces.ITypedRequest, useCacheArg: boolean = false) {
|
||||
let payloadReceiving: plugins.typedRequestInterfaces.ITypedRequest;
|
||||
if (this.urlEndPoint) {
|
||||
const response = await webrequestInstance.postJson(
|
||||
this.urlEndPoint,
|
||||
payloadSendingArg,
|
||||
useCacheArg
|
||||
);
|
||||
payloadReceiving = response;
|
||||
} else {
|
||||
payloadReceiving = await this.typedTarget.post(payloadSendingArg);
|
||||
}
|
||||
if (payloadReceiving.error) {
|
||||
console.error(
|
||||
`Got an error ${payloadReceiving.error.text} with data ${JSON.stringify(
|
||||
payloadReceiving.error.data,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
if (!payloadReceiving.retry) {
|
||||
throw new TypedResponseError(payloadReceiving.error.text, payloadReceiving.error.data);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (responseBody.retry) {
|
||||
console.log(`server requested retry for the following reason: ${responseBody.retry.reason}`);
|
||||
await plugins.smartdelay.delayFor(responseBody.retry.waitForMs);
|
||||
if (payloadReceiving.retry) {
|
||||
console.log(
|
||||
`server requested retry for the following reason: ${payloadReceiving.retry.reason}`
|
||||
);
|
||||
await plugins.smartdelay.delayFor(payloadReceiving.retry.waitForMs);
|
||||
// tslint:disable-next-line: no-return-await
|
||||
return await this.fire(fireArg);
|
||||
payloadReceiving = await this.postTrObject(payloadSendingArg, useCacheArg);
|
||||
}
|
||||
return responseBody.response;
|
||||
return payloadReceiving;
|
||||
}
|
||||
}
|
||||
|
10
ts/typedrequest.classes.typedresponseerror.ts
Normal file
10
ts/typedrequest.classes.typedresponseerror.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export class TypedResponseError {
|
||||
public errorText: string;
|
||||
public errorData: any;
|
||||
constructor(errorTextArg: string, errorDataArg?: any) {
|
||||
this.errorText = errorTextArg;
|
||||
this.errorData = errorDataArg;
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import * as plugins from './typedrequest.plugins';
|
||||
import * as plugins from './plugins.js';
|
||||
import { VirtualStream } from './typedrequest.classes.virtualstream.js';
|
||||
|
||||
import { TypedHandler } from './typedrequest.classes.typedhandler';
|
||||
import { TypedHandler } from './typedrequest.classes.typedhandler.js';
|
||||
import { TypedRequest } from './typedrequest.classes.typedrequest.js';
|
||||
|
||||
/**
|
||||
* A typed router decides on which typed handler to call based on the method
|
||||
@ -8,13 +10,16 @@ import { TypedHandler } from './typedrequest.classes.typedhandler';
|
||||
* This is thought for reusing the same url endpoint for different methods
|
||||
*/
|
||||
export class TypedRouter {
|
||||
public upstreamTypedRouter: TypedRouter;
|
||||
|
||||
public routerMap = new plugins.lik.ObjectMap<TypedRouter>();
|
||||
|
||||
public handlerMap = new plugins.lik.ObjectMap<
|
||||
TypedHandler<plugins.typedRequestInterfaces.ITypedRequest>
|
||||
TypedHandler<any & plugins.typedRequestInterfaces.ITypedRequest>
|
||||
>();
|
||||
public registeredVirtualStreams = new plugins.lik.ObjectMap<VirtualStream<any>>();
|
||||
|
||||
public fireEventInterestMap = new plugins.lik.InterestMap<
|
||||
string,
|
||||
plugins.typedRequestInterfaces.ITypedRequest
|
||||
>((correlationId: string) => correlationId);
|
||||
|
||||
/**
|
||||
* adds the handler to the routing map
|
||||
@ -39,11 +44,11 @@ export class TypedRouter {
|
||||
* @param typedRequest
|
||||
*/
|
||||
public addTypedRouter(typedRouterArg: TypedRouter) {
|
||||
this.routerMap.add(typedRouterArg);
|
||||
}
|
||||
|
||||
public setUpstreamTypedRouter(typedRouterArg: TypedRouter) {
|
||||
this.upstreamTypedRouter = typedRouterArg;
|
||||
const routerExists = this.routerMap.findSync((routerArg) => routerArg === typedRouterArg);
|
||||
if (!routerExists) {
|
||||
this.routerMap.add(typedRouterArg);
|
||||
typedRouterArg.addTypedRouter(this);
|
||||
}
|
||||
}
|
||||
|
||||
public checkForTypedHandler(methodArg: string): boolean {
|
||||
@ -57,52 +62,106 @@ export class TypedRouter {
|
||||
*/
|
||||
public getTypedHandlerForMethod(
|
||||
methodArg: string,
|
||||
checkUpstreamRouter = true
|
||||
checkedRouters: TypedRouter[] = []
|
||||
): TypedHandler<any> {
|
||||
checkedRouters.push(this);
|
||||
|
||||
let typedHandler: TypedHandler<any>;
|
||||
|
||||
if (this.upstreamTypedRouter && checkUpstreamRouter) {
|
||||
typedHandler = this.upstreamTypedRouter.getTypedHandlerForMethod(methodArg);
|
||||
} else {
|
||||
typedHandler = this.handlerMap.find(handler => {
|
||||
return handler.method === methodArg;
|
||||
});
|
||||
typedHandler = this.handlerMap.findSync((handler) => {
|
||||
return handler.method === methodArg;
|
||||
});
|
||||
|
||||
if (!typedHandler) {
|
||||
this.routerMap.getArray().forEach(typedRouter => {
|
||||
if (!typedHandler) {
|
||||
typedHandler = typedRouter.getTypedHandlerForMethod(methodArg, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!typedHandler) {
|
||||
this.routerMap.getArray().forEach((typedRouterArg) => {
|
||||
if (!typedHandler && !checkedRouters.includes(typedRouterArg)) {
|
||||
typedHandler = typedRouterArg.getTypedHandlerForMethod(methodArg, checkedRouters);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return typedHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* routes a typed request to a handler
|
||||
* if typedrequest object has correlation.phase === 'request' -> routes a typed request object to a handler
|
||||
* if typedrequest object has correlation.phase === 'response' -> routes a typed request object to request fire event
|
||||
* @param typedRequestArg
|
||||
*/
|
||||
public async routeAndAddResponse(typedRequestArg: plugins.typedRequestInterfaces.ITypedRequest) {
|
||||
const typedHandler = this.getTypedHandlerForMethod(typedRequestArg.method);
|
||||
public async routeAndAddResponse<
|
||||
T extends plugins.typedRequestInterfaces.ITypedRequest = plugins.typedRequestInterfaces.ITypedRequest
|
||||
>(typedRequestArg: T, localRequestArg = false): Promise<T> {
|
||||
// decoding first
|
||||
typedRequestArg = VirtualStream.decodePayloadFromNetwork(typedRequestArg, {
|
||||
typedrouter: this,
|
||||
});
|
||||
|
||||
if (!typedHandler) {
|
||||
const availableMethods: string[] = [];
|
||||
await this.handlerMap.forEach(async handler => {
|
||||
availableMethods.push(handler.method);
|
||||
});
|
||||
console.log(`Cannot find method for ${typedHandler}`);
|
||||
console.log(`Available methods are:`);
|
||||
console.log(availableMethods);
|
||||
typedRequestArg.error = {
|
||||
text: 'There is no available method for this call on the server side',
|
||||
data: {}
|
||||
};
|
||||
return typedRequestArg;
|
||||
// localdata second
|
||||
typedRequestArg.localData = typedRequestArg.localData || {};
|
||||
typedRequestArg.localData.firstTypedrouter = this;
|
||||
|
||||
// lets do stream processing
|
||||
if (typedRequestArg.method === '##VirtualStream##') {
|
||||
const result: any = await this.handleStreamTypedRequest(typedRequestArg as plugins.typedRequestInterfaces.IStreamRequest);
|
||||
result.localData = null;
|
||||
return result as T;
|
||||
}
|
||||
|
||||
typedRequestArg = await typedHandler.addResponse(typedRequestArg);
|
||||
return typedRequestArg;
|
||||
// lets do normal routing
|
||||
if (typedRequestArg?.correlation?.phase === 'request' || localRequestArg) {
|
||||
const typedHandler = this.getTypedHandlerForMethod(typedRequestArg.method);
|
||||
|
||||
if (!typedHandler) {
|
||||
console.log(`Cannot find handler for methodname ${typedRequestArg.method}`);
|
||||
typedRequestArg.error = {
|
||||
text: 'There is no available method for this call on the server side',
|
||||
data: {},
|
||||
};
|
||||
typedRequestArg.correlation.phase = 'response';
|
||||
|
||||
// encode again before handing back
|
||||
typedRequestArg.localData = null;
|
||||
typedRequestArg = VirtualStream.encodePayloadForNetwork(typedRequestArg, {
|
||||
typedrouter: this,
|
||||
});
|
||||
return typedRequestArg;
|
||||
}
|
||||
|
||||
typedRequestArg = await typedHandler.addResponse(typedRequestArg);
|
||||
typedRequestArg.localData = null;
|
||||
// encode again before handing back
|
||||
typedRequestArg = VirtualStream.encodePayloadForNetwork(typedRequestArg, {
|
||||
typedrouter: this,
|
||||
});
|
||||
return typedRequestArg;
|
||||
} else if (typedRequestArg?.correlation?.phase === 'response') {
|
||||
this.fireEventInterestMap
|
||||
.findInterest(typedRequestArg.correlation.id)
|
||||
?.fullfillInterest(typedRequestArg);
|
||||
return null;
|
||||
} else {
|
||||
console.log('received weirdly shaped request');
|
||||
console.log(typedRequestArg);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handle streaming
|
||||
* @param streamTrArg
|
||||
*/
|
||||
public async handleStreamTypedRequest(streamTrArg: plugins.typedRequestInterfaces.IStreamRequest) {
|
||||
const relevantVirtualStream = await this.registeredVirtualStreams.find(async virtualStreamArg => {
|
||||
return virtualStreamArg.streamId === streamTrArg.request.streamId;
|
||||
});
|
||||
if (!relevantVirtualStream) {
|
||||
console.log(`no relevant virtual stream found for stream with id ${streamTrArg.request.streamId}`);
|
||||
console.log(this.registeredVirtualStreams.getArray());
|
||||
return streamTrArg;
|
||||
} else {
|
||||
console.log(`success: found relevant virtual stream with id ${streamTrArg.request.streamId}`);
|
||||
}
|
||||
const result = await relevantVirtualStream.handleStreamTr(streamTrArg);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
81
ts/typedrequest.classes.typedtarget.ts
Normal file
81
ts/typedrequest.classes.typedtarget.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { TypedRouter } from './typedrequest.classes.typedrouter.js';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export type IPostMethod = (
|
||||
typedRequestPostObject: plugins.typedRequestInterfaces.ITypedRequest
|
||||
) => Promise<plugins.typedRequestInterfaces.ITypedRequest>;
|
||||
|
||||
/**
|
||||
* this is an alternative to a post url supplied in `new Typedrequest(new TypedTarget(...), 'someMethodName')`
|
||||
* enables the use of custom post functions
|
||||
* used for things like broadcast channels
|
||||
* e.g. @designestate/dees-comms
|
||||
* the main difference here is, that the response comes back async and is routed by interest through typedrouter
|
||||
*/
|
||||
export type IPostMethodWithTypedRouter = (
|
||||
typedRequestPostObject: plugins.typedRequestInterfaces.ITypedRequest
|
||||
) => Promise<void> | Promise<plugins.typedRequestInterfaces.ITypedRequest>;
|
||||
|
||||
export interface ITypedTargetConstructorOptions {
|
||||
url?: string;
|
||||
postMethod?: IPostMethod;
|
||||
/**
|
||||
* a post method that does not return the answer
|
||||
*/
|
||||
postMethodWithTypedRouter?: IPostMethodWithTypedRouter;
|
||||
/**
|
||||
* this typedrouter allows us to have easy async request response cycles
|
||||
*/
|
||||
typedRouterRef?: TypedRouter;
|
||||
}
|
||||
|
||||
/**
|
||||
* a typed target defines a target for requests
|
||||
*/
|
||||
export class TypedTarget {
|
||||
url: string;
|
||||
type: 'rest' | 'socket';
|
||||
options: ITypedTargetConstructorOptions;
|
||||
|
||||
constructor(optionsArg: ITypedTargetConstructorOptions) {
|
||||
if (optionsArg.postMethodWithTypedRouter && !optionsArg.typedRouterRef) {
|
||||
throw new Error('you have to specify a typedrouter when using postmethod with typedrouter');
|
||||
}
|
||||
this.options = optionsArg;
|
||||
}
|
||||
|
||||
/**
|
||||
* wether calls to this target are bound to the request/response cycle
|
||||
* if false, always delivers response as result of a call
|
||||
* if true, delivers response in a separate call
|
||||
* can only be async when type is 'socket'
|
||||
*/
|
||||
public isAsync: boolean;
|
||||
|
||||
public async post<T extends plugins.typedRequestInterfaces.ITypedRequest>(
|
||||
payloadArg: T
|
||||
): Promise<T> {
|
||||
let responseInterest: plugins.lik.Interest<
|
||||
string,
|
||||
plugins.typedRequestInterfaces.ITypedRequest
|
||||
>;
|
||||
// having a typedrouter allows us to work with async request response cycles.
|
||||
if (this.options.typedRouterRef) {
|
||||
responseInterest = await this.options.typedRouterRef.fireEventInterestMap.addInterest(
|
||||
payloadArg.correlation.id,
|
||||
payloadArg
|
||||
);
|
||||
}
|
||||
const postMethod = this.options.postMethod || this.options.postMethodWithTypedRouter;
|
||||
const postMethodReturnValue = await postMethod(payloadArg);
|
||||
let responseBody: T;
|
||||
if (responseInterest) {
|
||||
responseBody = (await responseInterest.interestFullfilled) as T;
|
||||
} else if (postMethodReturnValue) {
|
||||
responseBody = postMethodReturnValue as T;
|
||||
} else {
|
||||
responseBody = payloadArg;
|
||||
}
|
||||
return responseBody;
|
||||
}
|
||||
}
|
376
ts/typedrequest.classes.virtualstream.ts
Normal file
376
ts/typedrequest.classes.virtualstream.ts
Normal file
@ -0,0 +1,376 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import type { TypedRouter } from './typedrequest.classes.typedrouter.js';
|
||||
|
||||
export interface ICommFunctions {
|
||||
sendMethod?: (
|
||||
sendPayload: plugins.typedRequestInterfaces.IStreamRequest
|
||||
) => Promise<plugins.typedRequestInterfaces.IStreamRequest>;
|
||||
typedrouter?: TypedRouter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. A VirtualStream connects over the network
|
||||
* 2. It is always paired to one other VirtualStream
|
||||
* on the other side with the same streamId.
|
||||
* 3. It has a Readable and Writable side.
|
||||
* 4. The Writable side is Readable on the other side and vice versa.
|
||||
*/
|
||||
export class VirtualStream<T = ArrayBufferLike> implements plugins.typedRequestInterfaces.IVirtualStream<T> {
|
||||
// STATIC
|
||||
public static encodePayloadForNetwork(
|
||||
objectPayload: any,
|
||||
commFunctions: ICommFunctions,
|
||||
originalPayload?: any,
|
||||
path = []
|
||||
): any {
|
||||
if (!objectPayload) {
|
||||
return objectPayload;
|
||||
}
|
||||
if (plugins.smartbuffer.isBufferLike(objectPayload)) {
|
||||
return objectPayload;
|
||||
}
|
||||
if (objectPayload instanceof VirtualStream) {
|
||||
if (!objectPayload.side && commFunctions.sendMethod) {
|
||||
objectPayload.side = 'requesting';
|
||||
objectPayload.sendMethod = commFunctions.sendMethod;
|
||||
}
|
||||
if (!objectPayload.side && commFunctions.typedrouter) {
|
||||
objectPayload.side = 'responding';
|
||||
objectPayload.typedrouter = commFunctions.typedrouter;
|
||||
commFunctions.typedrouter.registeredVirtualStreams.add(objectPayload);
|
||||
}
|
||||
if (!originalPayload.response || path.includes('response')) {
|
||||
objectPayload.startKeepAliveLoop();
|
||||
return {
|
||||
_isVirtualStream: true,
|
||||
streamId: objectPayload.streamId,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
_OBMITTED_VIRTUAL_STREAM: true,
|
||||
reason: 'path is under .request: obmitted for deduplication reasons in response cycle.',
|
||||
};
|
||||
}
|
||||
} else if (Array.isArray(objectPayload)) {
|
||||
// For arrays, we recurse over each item.
|
||||
return objectPayload.map((item, index) =>
|
||||
VirtualStream.encodePayloadForNetwork(
|
||||
item,
|
||||
commFunctions,
|
||||
originalPayload || objectPayload,
|
||||
path.concat(String(index)) // Convert index to string and concatenate to path
|
||||
)
|
||||
);
|
||||
} else if (objectPayload !== null && typeof objectPayload === 'object') {
|
||||
// For objects, we recurse over each key-value pair.
|
||||
return Object.entries(objectPayload).reduce((acc, [key, value]) => {
|
||||
const newPath = path.concat(key); // Concatenate the new key to the path
|
||||
acc[key] = VirtualStream.encodePayloadForNetwork(
|
||||
value,
|
||||
commFunctions,
|
||||
originalPayload || objectPayload,
|
||||
newPath
|
||||
);
|
||||
return acc;
|
||||
}, {});
|
||||
} else {
|
||||
return objectPayload;
|
||||
}
|
||||
}
|
||||
|
||||
public static decodePayloadFromNetwork(objectPayload: any, commFunctions: ICommFunctions): any {
|
||||
if (plugins.smartbuffer.isBufferLike(objectPayload)) {
|
||||
return objectPayload;
|
||||
}
|
||||
if (objectPayload !== null && typeof objectPayload === 'object') {
|
||||
if (objectPayload._isVirtualStream) {
|
||||
const virtualStream = new VirtualStream();
|
||||
virtualStream.streamId = objectPayload.streamId;
|
||||
if (!virtualStream.side && commFunctions.sendMethod) {
|
||||
virtualStream.side = 'requesting';
|
||||
virtualStream.sendMethod = commFunctions.sendMethod;
|
||||
}
|
||||
if (!virtualStream.side && commFunctions.typedrouter) {
|
||||
virtualStream.side = 'responding';
|
||||
virtualStream.typedrouter = commFunctions.typedrouter;
|
||||
commFunctions.typedrouter.registeredVirtualStreams.add(virtualStream);
|
||||
}
|
||||
virtualStream.startKeepAliveLoop();
|
||||
return virtualStream;
|
||||
} else if (Array.isArray(objectPayload)) {
|
||||
const returnArray = [];
|
||||
for (const item of objectPayload) {
|
||||
returnArray.push(VirtualStream.decodePayloadFromNetwork(item, commFunctions));
|
||||
}
|
||||
return returnArray;
|
||||
} else {
|
||||
return Object.keys(objectPayload).reduce((acc, key) => {
|
||||
acc[key] = VirtualStream.decodePayloadFromNetwork(objectPayload[key], commFunctions);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
} else {
|
||||
return objectPayload;
|
||||
}
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
|
||||
public side: 'requesting' | 'responding';
|
||||
public streamId: string = plugins.isounique.uni();
|
||||
|
||||
// integration with typedrequest mechanics
|
||||
public sendMethod: ICommFunctions['sendMethod'];
|
||||
public typedrouter: TypedRouter;
|
||||
|
||||
// wether to keep the stream alive
|
||||
private keepAlive = true;
|
||||
private lastKeepAliveEvent: number;
|
||||
|
||||
// backpressured arrays
|
||||
private sendBackpressuredArray =
|
||||
new plugins.lik.BackpressuredArray<T>(
|
||||
16
|
||||
);
|
||||
private receiveBackpressuredArray =
|
||||
new plugins.lik.BackpressuredArray<T>(
|
||||
16
|
||||
);
|
||||
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* takes care of sending
|
||||
*/
|
||||
private async workOnQueue() {
|
||||
if(this.side === 'requesting') {
|
||||
let thisSideIsBackpressured = !this.receiveBackpressuredArray.checkSpaceAvailable();
|
||||
let otherSideHasNext = false;
|
||||
let otherSideIsBackpressured = false;
|
||||
|
||||
// helper functions
|
||||
const getFeedback = async () => {
|
||||
const streamTr = await this.sendMethod({
|
||||
method: '##VirtualStream##',
|
||||
request: {
|
||||
streamId: this.streamId,
|
||||
cycleId: plugins.isounique.uni(),
|
||||
cycle: 'request',
|
||||
mainPurpose: 'feedback',
|
||||
next: this.sendBackpressuredArray.data.length > 0,
|
||||
backpressure: this.receiveBackpressuredArray.checkSpaceAvailable(),
|
||||
},
|
||||
response: null,
|
||||
}).catch(() => {
|
||||
console.log('stream ended immaturely');
|
||||
this.keepAlive = false;
|
||||
});
|
||||
if (streamTr && streamTr.response) {
|
||||
otherSideIsBackpressured = streamTr.response.backpressure
|
||||
otherSideHasNext = streamTr.response.next;
|
||||
}
|
||||
}
|
||||
await getFeedback();
|
||||
|
||||
// do work loop
|
||||
while (this.sendBackpressuredArray.data.length > 0 || otherSideHasNext) {
|
||||
let dataArg: typeof this.sendBackpressuredArray.data[0];
|
||||
if (this.sendBackpressuredArray.data.length > 0) {
|
||||
dataArg = this.sendBackpressuredArray.shift();
|
||||
}
|
||||
let streamTr: plugins.typedRequestInterfaces.IStreamRequest;
|
||||
streamTr = await this.sendMethod({
|
||||
method: '##VirtualStream##',
|
||||
request: {
|
||||
streamId: this.streamId,
|
||||
cycleId: plugins.isounique.uni(),
|
||||
cycle: 'request',
|
||||
mainPurpose: dataArg ? 'chunk' : 'read',
|
||||
backpressure: thisSideIsBackpressured,
|
||||
next: this.sendBackpressuredArray.data.length > 0,
|
||||
...dataArg ? { chunkData: dataArg } : {},
|
||||
},
|
||||
response: null,
|
||||
}).catch(() => {
|
||||
console.log('stream ended immaturely');
|
||||
this.keepAlive = false;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (streamTr && streamTr.response && streamTr.response.chunkData) {
|
||||
this.receiveBackpressuredArray.push(streamTr.response.chunkData);
|
||||
}
|
||||
thisSideIsBackpressured = this.receiveBackpressuredArray.checkSpaceAvailable();
|
||||
|
||||
// lets care about looping
|
||||
otherSideHasNext = streamTr && streamTr.response && streamTr.response.next;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method handles the stream only on the responding side
|
||||
* @param streamTrArg
|
||||
* @returns
|
||||
*/
|
||||
public async handleStreamTr(streamTrArg: plugins.typedRequestInterfaces.IStreamRequest) {
|
||||
if (streamTrArg.request.keepAlive === true && this.keepAlive === true) {
|
||||
this.lastKeepAliveEvent = Date.now();
|
||||
} else if (streamTrArg.request.keepAlive === false) {
|
||||
this.keepAlive = false;
|
||||
}
|
||||
|
||||
// keepAlive handling
|
||||
if (streamTrArg.request.mainPurpose === 'keepAlive') {
|
||||
// if the main purpose is keepAlive, we answer with a keepAlive
|
||||
streamTrArg.response = {
|
||||
streamId: this.streamId,
|
||||
cycleId: streamTrArg.request.cycleId,
|
||||
cycle: 'response',
|
||||
mainPurpose: 'keepAlive',
|
||||
keepAlive: this.keepAlive,
|
||||
next: this.sendBackpressuredArray.data.length > 0,
|
||||
backpressure: this.receiveBackpressuredArray.checkSpaceAvailable(),
|
||||
};
|
||||
}
|
||||
|
||||
// feedback handling
|
||||
if (streamTrArg.request.mainPurpose === 'feedback') {
|
||||
streamTrArg.response = {
|
||||
streamId: this.streamId,
|
||||
cycleId: streamTrArg.request.cycleId,
|
||||
cycle: 'response',
|
||||
mainPurpose: 'feedback',
|
||||
next: this.sendBackpressuredArray.data.length > 0,
|
||||
backpressure: this.receiveBackpressuredArray.checkSpaceAvailable(),
|
||||
};
|
||||
}
|
||||
|
||||
// chunk handling
|
||||
if (streamTrArg.request.mainPurpose === 'chunk') {
|
||||
this.receiveBackpressuredArray.push(streamTrArg.request.chunkData);
|
||||
if (this.sendBackpressuredArray.data.length > 0 && streamTrArg.response.backpressure === false) {
|
||||
const dataArg = this.sendBackpressuredArray.shift();
|
||||
streamTrArg.response = {
|
||||
streamId: this.streamId,
|
||||
cycleId: streamTrArg.request.cycleId,
|
||||
cycle: 'response',
|
||||
mainPurpose: 'chunk',
|
||||
next: this.sendBackpressuredArray.data.length > 1,
|
||||
backpressure: this.receiveBackpressuredArray.checkSpaceAvailable(),
|
||||
chunkData: this.sendBackpressuredArray.shift(),
|
||||
};
|
||||
} else {
|
||||
streamTrArg.response = {
|
||||
streamId: this.streamId,
|
||||
cycleId: streamTrArg.request.cycleId,
|
||||
cycle: 'response',
|
||||
mainPurpose: 'feedback',
|
||||
next: this.sendBackpressuredArray.data.length > 0,
|
||||
};
|
||||
}
|
||||
streamTrArg.request = null;
|
||||
}
|
||||
|
||||
return streamTrArg;
|
||||
}
|
||||
|
||||
// lifecycle methods
|
||||
/**
|
||||
* closes the virtual stream
|
||||
*/
|
||||
public async cleanup() {
|
||||
if (this.typedrouter) {
|
||||
this.typedrouter.registeredVirtualStreams.remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* a keepAlive loop that works across technologies
|
||||
*/
|
||||
private async startKeepAliveLoop() {
|
||||
// initially wait for a second
|
||||
await plugins.smartdelay.delayFor(0);
|
||||
let counter = 0;
|
||||
keepAliveLoop: while (this.keepAlive) {
|
||||
const triggerResult = await this.triggerKeepAlive();
|
||||
await plugins.smartdelay.delayFor(1000);
|
||||
}
|
||||
await plugins.smartdelay.delayFor(1000);
|
||||
await this.cleanup();
|
||||
console.log(`cleaned up for stream ${this.streamId}`);
|
||||
}
|
||||
|
||||
private async triggerKeepAlive() {
|
||||
if (this.side === 'requesting') {
|
||||
console.log(`keepalive sent.`);
|
||||
const streamTr = await this.sendMethod({
|
||||
method: '##VirtualStream##',
|
||||
request: {
|
||||
streamId: this.streamId,
|
||||
cycleId: plugins.isounique.uni(),
|
||||
cycle: 'request',
|
||||
mainPurpose: 'keepAlive',
|
||||
keepAlive: true,
|
||||
},
|
||||
response: null,
|
||||
}).catch(() => {
|
||||
this.keepAlive = false;
|
||||
});
|
||||
|
||||
// lets handle keepAlive
|
||||
if (streamTr && streamTr.response && streamTr.response.keepAlive === false) {
|
||||
this.keepAlive = false;
|
||||
} else {
|
||||
this.lastKeepAliveEvent = Date.now();
|
||||
}
|
||||
if (streamTr && streamTr.response && streamTr.response.next) {
|
||||
this.workOnQueue();
|
||||
}
|
||||
}
|
||||
if (Date.now() - this.lastKeepAliveEvent > 10000) {
|
||||
console.log(`closing stream for ${this.streamId}`);
|
||||
this.keepAlive = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Data sending and receiving
|
||||
public async sendData(dataArg: T): Promise<void> {
|
||||
this.sendBackpressuredArray.push(dataArg);
|
||||
this.workOnQueue();
|
||||
await this.sendBackpressuredArray.waitForSpace();
|
||||
}
|
||||
|
||||
public async fetchData(): Promise<T> {
|
||||
if (this.receiveBackpressuredArray.hasSpace) {
|
||||
// do something maybe?
|
||||
}
|
||||
await this.receiveBackpressuredArray.waitForItems();
|
||||
const dataPackage = this.receiveBackpressuredArray.shift();
|
||||
return dataPackage;
|
||||
}
|
||||
|
||||
/**
|
||||
* reads from a Readable and sends it to the other side
|
||||
* @param readableStreamArg
|
||||
*/
|
||||
public async readFromWebstream(readableStreamArg: ReadableStream<T>) {
|
||||
const reader = readableStreamArg.getReader();
|
||||
let streamIsDone = false;
|
||||
while(!streamIsDone) {
|
||||
const { value, done } = await reader.read();
|
||||
if(value) {
|
||||
await this.sendData(value);
|
||||
}
|
||||
streamIsDone = done;
|
||||
}
|
||||
}
|
||||
|
||||
public async writeToWebstream(writableStreamArg: WritableStream<T>) {
|
||||
const writer = writableStreamArg.getWriter();
|
||||
while(this.keepAlive) {
|
||||
await writer.write(await this.fetchData());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
// apiglobal scope
|
||||
import * as typedRequestInterfaces from '@apiglobal/typedrequest-interfaces';
|
||||
|
||||
export { typedRequestInterfaces };
|
||||
|
||||
// pushrocks scope
|
||||
import * as lik from '@pushrocks/lik';
|
||||
import * as smartdelay from '@pushrocks/smartdelay';
|
||||
import * as smartrequest from '@pushrocks/smartrequest';
|
||||
import * as smartjson from '@pushrocks/smartjson';
|
||||
|
||||
export { lik, smartdelay, smartrequest, smartjson };
|
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
]
|
||||
}
|
17
tslint.json
17
tslint.json
@ -1,17 +0,0 @@
|
||||
{
|
||||
"extends": ["tslint:latest", "tslint-config-prettier"],
|
||||
"rules": {
|
||||
"semicolon": [true, "always"],
|
||||
"no-console": false,
|
||||
"ordered-imports": false,
|
||||
"object-literal-sort-keys": false,
|
||||
"member-ordering": {
|
||||
"options":{
|
||||
"order": [
|
||||
"static-method"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultSeverity": "warning"
|
||||
}
|
Reference in New Issue
Block a user