Skip to content

Commit

Permalink
Merge pull request #409 from wuespace/TELESTION-465
Browse files Browse the repository at this point in the history
TELESTION-465: Deno samples
  • Loading branch information
pklaschka authored Jan 10, 2024
2 parents 163f3f3 + ef9aa1d commit cc4060e
Show file tree
Hide file tree
Showing 15 changed files with 392 additions and 0 deletions.
3 changes: 3 additions & 0 deletions backend-deno/samples/.common.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Common sample service environment variables
NATS_URL=nats:4222
DATA_DIR=/data
57 changes: 57 additions & 0 deletions backend-deno/samples/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Deno Backend Service Samples

This directory contains samples for Deno backend service.

## Adding a new sample

To add a new sample, create a new directory under this directory. The directory
name should be the name of the sample. For example, if you want to create a
sample named `hello_world`, create a directory named `hello_world`.

Inside the directory, create a `README.md` file that describes the sample.

Most samples should also have a `mod.ts` file that contains the sample code.

### Importing the Telestion Library

To import the Telestion library, use the following import statement:

```typescript
import { startService } from "https://deno.land/x/telestion/mod.ts";
```

This ensures that the import gets aliases to the repository's library files.

### Running the sample

All samples get packaged into a single docker image, based on the [`Dockerfile`](./Dockerfile) in this directory.

The `Dockerfile` gets built from the context of the [`backend-deno`](../) directory. This is done to ensure it has access to the repository's library `mod.ts` file.

To add your sample to the `docker-compose.yml` file, add the following to the `services` section:

```yaml
<service-name>:
build:
context: ../
dockerfile: samples/Dockerfile
command: ["<service-name>/mod.ts"]
depends_on:
- nats
env_file:
- .common.env
```
Replace `<service-name>` with the name of your sample. For example, if your sample is named `hello_world`, replace `<service-name>` with `hello_world`.

You then have to rebuild the docker image by running the following command from the `samples` directory:

```bash
docker compose up --build
```

## Docker Image publishing

The docker image may, in the future, be published to a docker registry.

This way, projects can easily include the samples in their `docker-compose.yml` file without having to download them locally in their projects for useful services.
13 changes: 13 additions & 0 deletions backend-deno/samples/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM denoland/deno:alpine-1.39.0
LABEL version="1.0.0-alpha.3" maintainer="WüSpace e. V. <[email protected]>"

# Add files
COPY ./mod.ts /app/mod.ts
COPY ./samples /app/samples

# Cache dependencies in container layer
RUN deno cache /app/samples/**/*.ts

# Run
WORKDIR /app/samples
ENTRYPOINT [ "deno", "run", "--allow-all" ]
19 changes: 19 additions & 0 deletions backend-deno/samples/config/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Service Configuration Sample

This sample shows how to use the configuration system.

In addition to parsing the config for further use,
all configuration options are printed to the console.

## Configuration Options

The following configuration options get printed to the console:

### Standard Configuration Options

- `SERVICE_NAME`
- `DATA_DIR`

### Custom Configuration Options

- `MY_STRING`
24 changes: 24 additions & 0 deletions backend-deno/samples/config/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {startService} from "https://deno.land/x/telestion/mod.ts";
import {z} from "https://deno.land/x/[email protected]/mod.ts";

// Start the service
const {config, serviceName, dataDir} = await startService({
nats: false // we don't need NATS to work with the config
});

// Standard config options
console.log(`Service "${serviceName}" started!`);
console.log(`Data directory: ${dataDir}`);

// Custom config options
const customConfig = z.object({
MY_STRING: z.string(),
}).parse(config);

console.log(`My string: ${customConfig.MY_STRING}`);

// Everything
console.log("Complete config: ", config);

// The end
console.log("Stopping service...")
5 changes: 5 additions & 0 deletions backend-deno/samples/deno.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"imports": {
"https://deno.land/x/telestion/mod.ts": "../mod.ts"
}
}
70 changes: 70 additions & 0 deletions backend-deno/samples/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
services:
nats:
image: nats:latest
ports:
- "4222:4222"
- "8222:8222"
# volumes:
# - ./nats.conf:/etc/nats.conf
config:
build:
context: ../
dockerfile: samples/Dockerfile
command: ["config/mod.ts", "--MY_STRING", "Overwritten Hello World"]
depends_on:
- nats
env_file:
- .common.env
environment:
- SERVICE_NAME=config
- MY_STRING=Hello World that gets overwritten
latest-value-cache:
build:
context: ../
dockerfile: samples/Dockerfile
command: ["latest-value-cache/mod.ts"]
depends_on:
- nats
env_file:
- .common.env
environment:
- SERVICE_NAME=latest-value-cache
- DATA_SUBJECT=data.>
- REQUEST_SUBJECT=latest.data.>
sample-publisher:
build:
context: ../
dockerfile: samples/Dockerfile
command: ["publisher/mod.ts"]
depends_on:
- nats
env_file:
- .common.env
environment:
- SERVICE_NAME=sample-publisher
- DATA_SUBJECT=data.sample
forward-to-logger:
build:
context: ../
dockerfile: samples/Dockerfile
command: ["requester/mod.ts"]
depends_on:
- nats
env_file:
- .common.env
environment:
- SERVICE_NAME=forward-to-logger
- REQUEST_SUBJECT=latest.data.sample
- FREQUENCY=3000
- OUTPUT_SUBJECT=log.sample-data
logger:
build:
context: ../
dockerfile: samples/Dockerfile
command: ["logger/mod.ts"]
depends_on:
- nats
env_file:
- .common.env
environment:
- SERVICE_NAME=logger
20 changes: 20 additions & 0 deletions backend-deno/samples/latest-value-cache/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Latest Value Cache

A cache that stores the latest value for a given subject namespace (`[prefix].>`) and enables other services to retrieve the latest value for a given subject.

## Configuration Options

| Option | Description | Default |
| ------ | ----------- | ------- |
| `DATA_SUBJECT` | The subject namespace to cache values for. Must be of the pattern `[data-prefix].>` | (required) |
| `REQUEST_SUBJECT` | The subject to listen for requests on. Must be of the pattern `[request-prefix].>` | (required) |

Note that the `DATA_SUBJECT` and `REQUEST_SUBJECT` must be different. For each subject in the specified `DATA_SUBJECT` namespace, the latest value will be stored in the cache. Other services can then request the latest value for a given subject by publishing a message to the `REQUEST_SUBJECT` namespace with the subject to request as the message payload.

## Limitations

The cache is not persistent, so if the service is restarted, the cache will be empty.

While the service can be scaled horizontally (and requests will be load balanced across all instances), new data gets handled by all instances.

This could be improved by using a distributed cache (e.g. Redis).
65 changes: 65 additions & 0 deletions backend-deno/samples/latest-value-cache/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { startService } from "https://deno.land/x/telestion/mod.ts";
import { z } from "https://deno.land/x/[email protected]/mod.ts";

const { messageBus, config, serviceName } = await startService();

const { DATA_SUBJECT, REQUEST_SUBJECT } = z.object({
DATA_SUBJECT: z.string().endsWith(">"),
REQUEST_SUBJECT: z.string().endsWith(">"),
}).parse(config);

console.log("Latest value cache started with config", {
DATA_SUBJECT,
REQUEST_SUBJECT,
});

const dataStore = new Map<string, Uint8Array>();

console.log("Listening for new data on", DATA_SUBJECT);

// listen to the data subject
const data = messageBus.subscribe(DATA_SUBJECT);
(async () => {
for await (const msg of data) {
// data key is the ">" in the subject
const dataKey = msg.subject.substring(DATA_SUBJECT.length - 1);
// store the data
dataStore.set(dataKey, msg.data);

console.log(
"Latest value cache received data",
msg.data,
"for key",
dataKey,
);
}
})();

// listen to the request subject
const request = messageBus.subscribe(REQUEST_SUBJECT, {
queue: serviceName,
});
(async () => {
for await (const msg of request) {
console.log("Latest value cache received request on", msg.subject);

// request key is the ">" in the subject
const requestKey = msg.subject.substring(REQUEST_SUBJECT.length - 1);
// get the data from the store
const data = dataStore.get(requestKey);
// respond with the data or null if there is no data
msg.respond(data);

console.log(
"Latest value cache responded with",
data,
"for key",
requestKey,
);
}
})();

await Promise.any([data.closed, request.closed]);
console.log("Latest value cache stopped");
data.drain();
request.drain();
11 changes: 11 additions & 0 deletions backend-deno/samples/logger/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Logger Sample Service

This sample service demonstrates how to use the Telestion library to create a simple logger service.

It's equivalent to the [example in the documentation](https://docs.telestion.wuespace.de/Backend%20Development/typescript/e2e-log-service/).

## Functionality Description

Logs all messages received on any "sub-subject" of `log.>`. For example, if a message is received on `log.info`, it will be logged. If a message is received on `log.debug`, it will also be logged.

Messages get logged on both the console and to a local file.
27 changes: 27 additions & 0 deletions backend-deno/samples/logger/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { startService } from "https://deno.land/x/telestion/mod.ts";
import { encode } from "https://deno.land/[email protected]/encoding/hex.ts";

const encoder = new TextEncoder();

const { messageBus } = await startService();

const logMessages = messageBus.subscribe("log.>");

console.log("Logger started");

for await (const msg of logMessages) {
try {
const currentTime = new Date().toISOString();
const logMessage = encode(msg.data).toString();
const subject = msg.subject.split(".")[1];

console.log(`${currentTime} [${subject}] ${logMessage}`);
await Deno.writeFile(
"log.txt",
encoder.encode(`${currentTime} [${subject}] ${logMessage}\n`),
{ append: true },
);
} catch (error) {
console.error(error);
}
}
12 changes: 12 additions & 0 deletions backend-deno/samples/publisher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Publisher Sample

This sample publishes a random JSON message in a given frequency.

## Configuration Options

The following configuration options are available:

| Option | Description | Default |
| ------ | ----------- | ------- |
| `DATA_SUBJECT` | The data subject to publish messages to | (required) |
| `FREQUENCY` | The frequency in which to publish messages (in milliseconds) | `3000` |
26 changes: 26 additions & 0 deletions backend-deno/samples/publisher/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { JSONCodec, startService } from "https://deno.land/x/telestion/mod.ts";
import { z } from "https://deno.land/x/[email protected]/mod.ts";

const { messageBus, config } = await startService();

const { DATA_SUBJECT, FREQUENCY } = z.object({
DATA_SUBJECT: z.string(),
FREQUENCY: z.coerce.number().positive().default(3000),
}).parse(config);

console.log("Publisher started with config", {
DATA_SUBJECT,
});

setInterval(() => {
const value = {
value: Math.random(),
time: new Date().toISOString(),
};
console.log("Publishing", value, "to", DATA_SUBJECT);
messageBus.publish(
DATA_SUBJECT,
JSONCodec().encode(value),
);
console.log("Published", value, "to", DATA_SUBJECT);
}, FREQUENCY);
13 changes: 13 additions & 0 deletions backend-deno/samples/requester/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Requester Sample

This sample shows how to request data from another service.

In a given frequency, the service will request the value from a given subject (with an empty request) and re-publish it on a different subject.

## Configuration Options

| Option | Description | Default |
| ------ | ----------- | ------- |
| `REQUEST_SUBJECT` | The subject to request data from. | (required) |
| `PUBLISH_SUBJECT` | The subject to publish the requested data on. | (required) |
| `FREQUENCY` | The frequency to request data from the `REQUEST_SUBJECT` in milliseconds. | `5000` |
Loading

0 comments on commit cc4060e

Please sign in to comment.