Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
strategy:
matrix:
os-version: [ubuntu-latest, macos-latest]
node-version: ["18", "22"]
node-version: ["20", "22", "24"]
redis-version: [6]

steps:
Expand Down Expand Up @@ -38,7 +38,7 @@ jobs:
strategy:
matrix:
os-version: [windows-latest]
node-version: ["18", "22"]
node-version: ["20", "22", "24"]

steps:
- name: Git checkout
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18
24
21 changes: 9 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ npm i allserver express
Optionally, you can use Allserver's built-in client:

```shell
npm i allserver node-fetch
npm i allserver
```

Or do HTTP requests using any module you like.
Expand Down Expand Up @@ -390,10 +390,8 @@ exports.handler = Allserver({

#### Using built-in client

You'd need to install `node-fetch` optional dependency.

```shell
npm i allserver node-fetch
npm i allserver
```

Note, that this code is **same** as the gRPC client code example below!
Expand Down Expand Up @@ -618,13 +616,7 @@ assert(success === true);

### Bare AWS Lambda invocation

First you need to install any of the AWS SDK versions.

```shell
npm i allserver aws-sdk
```

or
First you need to install the AWS SDK v3.

```shell
npm i allserver @aws-sdk/client-lambda
Expand Down Expand Up @@ -681,6 +673,9 @@ aws lambda invoke --function-name my-lambda-name --payload '{"_":{"procedureName
- `transport`<br>
The transport implementation object. The `uri` is ignored if this option provided. If not given then it will be automatically created based on the `uri` schema. E.g. if it starts with `http://` or `https://` then `HttpClientTransport` will be used. If starts with `grpc://` then `GrpcClientTransport` will be used. If starts with `bullmq://` then `BullmqClientTransport` is used.

- `timeout=60_000`<br>
Set it to `0` if you don't need a timeout. If the procedure call takes longer than this value then the `AllserverClient` will return `success=false` and `code=ALLSERVER_CLIENT_TIMEOUT`.

- `neverThrow=true`<br>
Set it to `false` if you want to get exceptions when there are a network, or a server errors during a procedure call. Otherwise, the standard `{success,code,message}` object is returned from method calls. The Allserver error `code`s are always start with `"ALLSERVER_"`. E.g. `"ALLSERVER_CLIENT_MALFORMED_INTROSPECTION"`.

Expand Down Expand Up @@ -709,6 +704,7 @@ You can change the above mentioned options default values like this:
```js
AllseverClient = AllserverClient.defaults({
transport,
timeout,
neverThrow,
dynamicMethods,
autoIntrospect,
Expand Down Expand Up @@ -912,7 +908,8 @@ const client = AllserverClient({
ctx.http.headers.authorization = "Basic my-token";
},
async after(ctx) {
if (ctx.error) console.error(ctx.error); else console.log(ctx.result);
if (ctx.error) console.error(ctx.error);
else console.log(ctx.result);
},
});
```
Expand Down
1 change: 0 additions & 1 deletion example/index.test.js → example/example.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ setTimeout(async () => {
const httpServer = Allserver({ procedures, transport: HttpTransport({ port: 40000 }) });
httpServer.start();

// const fetch = require("node-fetch");
// const response = await (
// await fetch("http://localhost:40000/sayHello", { method: "POST", body: JSON.stringify({ name: user }) })
// ).json();
Expand Down
64 changes: 24 additions & 40 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "allserver",
"version": "2.5.0",
"description": "Multi-protocol simple RPC server and [optional] client. Boilerplate-less. Opinionated. Minimalistic. DX-first.",
"version": "2.6.0",
"description": "Multi-protocol RPC server and [optional] client. DX-first. Minimalistic. Boilerplate-less. Opinionated.",
"main": "src/index.js",
"scripts": {
"lint": "eslint ./",
"test": "mocha",
"cov": "nyc --reporter=html npm run test"
"lint": "oxlint ./",
"test": "node --test --test-force-exit --trace-deprecation",
"cov": "nyc --reporter=html node --run test"
},
"keywords": [
"simple",
Expand All @@ -25,7 +25,7 @@
],
"repository": {
"type": "git",
"url": "http://github.com/flash-oss/allserver"
"url": "https://github.com/flash-oss/allserver"
},
"bugs": {
"url": "https://github.com/flash-oss/allserver/issues"
Expand All @@ -34,14 +34,17 @@
"author": "Vasyl Boroviak",
"license": "MIT",
"peerDependencies": {
"@grpc/grpc-js": "^1.1.7",
"@grpc/proto-loader": "^0.7.5",
"bullmq": "^3.10.1",
"express": "^4.18.2",
"micro": "^10.0.1",
"node-fetch": "^2.6.9"
"@aws-sdk/client-lambda": "3",
"@grpc/grpc-js": "1",
"@grpc/proto-loader": "0",
"bullmq": "3 - 5",
"express": "4 - 6",
"micro": "10"
},
"peerDependenciesMeta": {
"@aws-sdk/client-lambda": {
"optional": true
},
"@grpc/grpc-js": {
"optional": true
},
Expand All @@ -56,41 +59,22 @@
},
"micro": {
"optional": true
},
"node-fetch": {
"optional": true
}
},
"devDependencies": {
"@grpc/grpc-js": "^1.1.7",
"@grpc/proto-loader": "^0.7.5",
"bullmq": "^3.10.1",
"@aws-sdk/client-lambda": "^3.921.0",
"@grpc/grpc-js": "^1.13.4",
"@grpc/proto-loader": "^0.8.0",
"bullmq": "^5.56.9",
"cls-hooked": "^4.2.2",
"eslint": "^8.55.0",
"express": "^4.18.2",
"lambda-local": "^1.7.3",
"express": "^4.21.2",
"lambda-local": "^2.2.0",
"micro": "^10.0.1",
"mocha": "^10.2.0",
"node-fetch": "^2.6.9",
"nyc": "^15.1.0",
"nyc": "^17.1.0",
"oxlint": "^1.25.0",
"prettier": "^2.1.1"
},
"dependencies": {
"stampit": "^4.3.1"
},
"eslintConfig": {
"parserOptions": {
"ecmaVersion": 2022
},
"env": {
"es6": true,
"node": true,
"mocha": true
},
"extends": "eslint:recommended"
},
"mocha": {
"recursive": true,
"exit": true
"stampit": "^5.0.1"
}
}
45 changes: 38 additions & 7 deletions src/client/AllserverClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ module.exports = require("stampit")({
[p]: {
// The protocol implementation strategy.
transport: null,
// The maximum time to wait until returning the ALLSERVER_CLIENT_TIMEOUT error. 0 means - no timeout.
timeout: 60_000,
// Disable any exception throwing when calling any methods. Otherwise, throws network and server errors.
neverThrow: true,
// Automatically find (introspect) and call corresponding remote procedures. Use only the methods defined in client side.
Expand Down Expand Up @@ -173,6 +175,7 @@ module.exports = require("stampit")({
{
uri,
transport,
timeout,
neverThrow,
dynamicMethods,
autoIntrospect,
Expand All @@ -183,6 +186,7 @@ module.exports = require("stampit")({
},
{ stamp }
) {
this[p].timeout = timeout != null ? timeout : this[p].timeout;
this[p].neverThrow = neverThrow != null ? neverThrow : this[p].neverThrow;
this[p].dynamicMethods = dynamicMethods != null ? dynamicMethods : this[p].dynamicMethods;
this[p].autoIntrospect = autoIntrospect != null ? autoIntrospect : this[p].autoIntrospect;
Expand All @@ -200,7 +204,7 @@ module.exports = require("stampit")({
const getTransport = stamp.compose.deepConfiguration.transports[schema.toLowerCase()];
if (!getTransport) throw new Error(`Schema not supported: ${uri}`);

this[p].transport = getTransport()({ uri });
this[p].transport = getTransport()({ uri, timeout: this[p].timeout });
}

if (before) this[p].before = [].concat(this[p].before).concat(before).filter(isFunction);
Expand All @@ -218,10 +222,10 @@ module.exports = require("stampit")({
try {
// This is supposed to be executed only once (per uri) unless it throws.
// There are only 3 situations when this throws:
// * the "introspect" method not found on server,
// * the network request is malformed,
// * the "introspect" method not found on server, (GRPC)
// * the network request is malformed, (HTTP-like)
// * couldn't connect to the remote host.
ctx.result = await transport.introspect(ctx);
ctx.result = await this._callTransport(ctx);
} catch (err) {
ctx.result = {
success: false,
Expand Down Expand Up @@ -281,19 +285,44 @@ module.exports = require("stampit")({
return await runMiddlewares(middlewares);
},

_callTransport(ctx) {
const transportMethod = ctx.isIntrospection ? "introspect" : "call";
// In JavaScript if the `timeout` is null or undefined or some other object this condition will return `false`
if (this[p].timeout > 0) {
let timeout = this[p].timeout;
// Let's give a chance to the Transport to return its native timeout response before returning Client's timeout response.
if (timeout >= 100) timeout = Math.round(timeout / 100 + timeout);
return Promise.race([
this[p].transport[transportMethod](ctx),
new Promise((resolve) =>
setTimeout(
() =>
resolve({
success: false,
code: "ALLSERVER_CLIENT_TIMEOUT",
message: `The remote procedure ${ctx.procedureName} timed out in ${this[p].timeout} ms`,
}),
timeout
)
),
]);
}

return this[p].transport[transportMethod](ctx);
},

async call(procedureName, arg) {
if (!arg) arg = {};
if (!arg._) arg._ = {};
arg._.procedureName = procedureName;

const transport = this[p].transport;
const defaultCtx = { procedureName, arg, client: this };
const ctx = transport.createCallContext(defaultCtx);
const ctx = this[p].transport.createCallContext(defaultCtx);

await this._callMiddlewares(ctx, "before", async () => {
if (!ctx.result) {
try {
ctx.result = await transport.call(ctx);
ctx.result = await this._callTransport(ctx);
} catch (err) {
if (!this[p].neverThrow) throw err;

Expand Down Expand Up @@ -327,6 +356,7 @@ module.exports = require("stampit")({
statics: {
defaults({
transport,
timeout,
neverThrow,
dynamicMethods,
autoIntrospect,
Expand All @@ -341,6 +371,7 @@ module.exports = require("stampit")({
return this.deepProps({
[p]: {
transport,
timeout,
neverThrow,
dynamicMethods,
callIntrospectedProceduresOnly,
Expand Down
4 changes: 1 addition & 3 deletions src/client/BullmqClientTransport.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ module.exports = require("./ClientTransport").compose({
props: {
Queue: require("bullmq").Queue,
QueueEvents: require("bullmq").QueueEvents,
_timeout: 60000,
_queue: null,
_queueEvents: null,
_jobsOptions: null,
Expand All @@ -22,7 +21,6 @@ module.exports = require("./ClientTransport").compose({
retryStrategy: null, // only one attempt to connect
};
}
this._timeout = timeout || this._timeout;

this._queue = new this.Queue(queueName, { connection: connectionOptions });
this._queue.on("error", () => {}); // The only reason we subscribe is to avoid bullmq to print errors to console
Expand All @@ -49,7 +47,7 @@ module.exports = require("./ClientTransport").compose({
try {
await this._queue.waitUntilReady();
const job = await bullmq.queue.add(procedureName, bullmq.data, bullmq.jobsOptions);
return await job.waitUntilFinished(bullmq.queueEvents, this._timeout);
return await job.waitUntilFinished(bullmq.queueEvents, this.timeout); // this.timeout is a property of the parent ClientTransport
} catch (err) {
if (err.code === "ECONNREFUSED") err.noNetToServer = true;
throw err;
Expand Down
5 changes: 4 additions & 1 deletion src/client/ClientTransport.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ module.exports = require("stampit")({

props: {
uri: null,
timeout: 60_000,
},

init({ uri }) {
init({ uri, timeout }) {
if (!isFunction(this.introspect)) throw new Error("ClientTransport must implement introspect()");
if (!isFunction(this.call)) throw new Error("ClientTransport must implement call()");

this.uri = uri || this.uri;
if (!isString(this.uri)) throw new Error("`uri` connection string is required");

this.timeout = timeout != null ? timeout : this.timeout;
},
});
2 changes: 1 addition & 1 deletion src/client/GrpcClientTransport.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module.exports = require("./ClientTransport").compose({
name: "GrpcClientTransport",

props: {
_fs: require("fs"),
_fs: require("node:fs"),
_grpc: require("@grpc/grpc-js"),
_protoLoader: require("@grpc/proto-loader"),
_grpcClientForIntrospection: null,
Expand Down
5 changes: 2 additions & 3 deletions src/client/HttpClientTransport.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ module.exports = require("./ClientTransport").compose({
name: "HttpClientTransport",

props: {
// eslint-disable-next-line no-undef
fetch: (typeof globalThis !== "undefined" && globalThis.fetch) || require("node-fetch"),
fetch: globalThis.fetch,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
Expand Down Expand Up @@ -51,7 +50,7 @@ module.exports = require("./ClientTransport").compose({
const json = JSON.parse(text);
error = new Error((json && json.message) || text);
if (json && json.code) error.code = json.code;
} catch (err) {
} catch {
// ignoring. Not a JSON
}
}
Expand Down
Loading
Loading