Skip to content

Commit

Permalink
[CHANGE] examples for multiple endpoints
Browse files Browse the repository at this point in the history
[FEAT] support multiple endpoints

[FEAT] [SRV] addGroup() to group endpoints under a subject token [CHANGE] [SRV] endpoint is now optional

[CHANGE] [SRV] service info now reports multiple subjects - one per endpoint, so `subject` change to `subjects` and is an array.

[CHANGE] [SRV] stats now have `endpoints` which is an array of endpoint stats.

[CHANGE] [SRV] ServiceStats no longer contain metric information at the root level, they are nested under endpoints as the service can now have one or more endpoints.

[CHANGE] addGroup can have contain subject tokens, but addEndpoint(name) must be a simple string - name can be {name: string, subject: string} to allow the service have a simple name but specify a complex subject
  • Loading branch information
aricart committed Jan 6, 2023
1 parent 03accc4 commit f26302f
Show file tree
Hide file tree
Showing 6 changed files with 719 additions and 282 deletions.
6 changes: 3 additions & 3 deletions examples/services/01_services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ service.stopped.then((err: Error | null) => {
// we were able to parse an array of numbers from the request
// with at least one entry, we sort in reverse order
a.sort((a, b) => b - a);
// and first entry has the larget number
// and first entry has the largest number
const max = a[0];
// since our service also tracks the largest number ever seen
// we update our largest number
Expand Down Expand Up @@ -217,8 +217,8 @@ const stats = JSONCodec<ServiceStats>().decode(
(await nc.request(`$SRV.STATS.max.${found[0].id}`)).data,
);
assertEquals(stats.name, "max");
assertEquals(stats.num_requests, 3);
assertEquals((stats.data as { max: number }).max, 100);
assertEquals(stats.endpoints?.[0].num_requests, 3);
assertEquals((stats.endpoints?.[0].data as { max: number }).max, 100);

// stop the service
await service.stop();
Expand Down
153 changes: 153 additions & 0 deletions examples/services/02_multiple_endpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright 2022 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {
connect,
JSONCodec,
SchemaInfo,
ServiceError,
ServiceMsg,
} from "../../src/mod.ts";

const jc = JSONCodec();

// connect to NATS on demo.nats.io
const nc = await connect({ servers: ["demo.nats.io"] });

// this is a pseudo JSON schema - current requirements is that it is a string
// so more conveniently return an URL to your schemas.
const schema: SchemaInfo = {
request: JSON.stringify({
type: "array",
minItems: 1,
items: { type: "number" },
}),
response: JSON.stringify({ type: "number" }),
};

// create a service - using the statsHandler and decoder
const calc = await nc.services.add({
name: "calc",
version: "0.0.1",
description: "example calculator service",
schema,
});

// For this example the thing we want to showcase is how you can
// create service that has multiple endpoints.
// The service will have `sum`, `max`, `average` and `min` operations.
// While we could create a service that listens on `sum`, `max`, etc.,
// creating a complex hierarchy will allow you to carve the subject
// name space to allow for better access control, and organize your
// services better.
// In the service API, you `addGroup()` to the service, which will
// introduce a prefix for the calculator's endpoints. The group name
// can be any valid subject that can be prefixed into another.
const g = calc.addGroup("calc");

// We can now add endpoints under this which will augment the subject
// space, adding an endpoint. Endpoints can only have a simple name
// and can specify an optional callback:
(async () => {
// this endpoint is accessible as `calc.sum`
for await (const m of g.addEndpoint("sum")) {
const numbers = decode(m);
const s = numbers.reduce((sum, v) => {
return sum + v;
});
m.respond(jc.encode(s));
}
})().then();

// Here's another implemented using a callback, will be accessible by `calc.average`:
g.addEndpoint("average", (err, m) => {
if (err) {
calc.stop(err);
return;
}
const numbers = decode(m);
const sum = numbers.reduce((sum, v) => {
return sum + v;
});
m.respond(jc.encode(sum / numbers.length));
});

g.addEndpoint("min", (err, m) => {
if (err) {
calc.stop(err);
return;
}
const numbers = decode(m);
const min = numbers.reduce((n, v) => {
return Math.min(n, v);
});
m.respond(jc.encode(min));
});

g.addEndpoint("max", (err, m) => {
if (err) {
calc.stop(err);
return;
}
const numbers = decode(m);
const max = numbers.reduce((n, v) => {
return Math.max(n, v);
});
m.respond(jc.encode(max));
});

calc.stopped.then((err: Error | null) => {
console.log(`calc stopped ${err ? "because: " + err.message : ""}`);
});

// Now we switch gears and look at a client making a request
async function calculate(op: string, a: number[]): Promise<void> {
const r = await nc.request(`calc.${op}`, jc.encode(a));
if (ServiceError.isServiceError(r)) {
console.log(ServiceError.toServiceError(r));
return;
}
const ans = jc.decode<number>(r.data);
console.log(`${op} ${a.join(", ")} = ${ans}`);
}

await Promise.all([
calculate("sum", [5, 10, 15]),
calculate("average", [5, 10, 15]),
calculate("min", [5, 10, 15]),
calculate("max", [5, 10, 15]),
]);

// stop the service
await calc.stop();
// and close the connection
await nc.close();

// a simple decoder that tosses a ServiceError if the input is not what we want.
function decode(m: ServiceMsg): number[] {
const a = jc.decode<number[]>(m.data);
if (!Array.isArray(a)) {
throw new ServiceError(400, "input requires array");
}
if (a.length === 0) {
throw new ServiceError(400, "array must have at least one number");
}
a.forEach((v) => {
if (typeof v !== "number") {
throw new ServiceError(400, "array elements must be numbers");
}
});
return a;
}
Loading

0 comments on commit f26302f

Please sign in to comment.