@yopdev/dev-server 3.0.2-RC → 3.0.2-RC1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ import { Logger, LoggerFactory } from '@yopdev/logging';
2
+ import { clearInterval } from 'timers';
3
+ import { Lifecycle, Service } from './services';
4
+
5
+ export const newScheduledTasks = (
6
+ name: string,
7
+ schedules: Rate[],
8
+ ) => new Service(new ScheduledTasks(name, schedules))
9
+
10
+ class ScheduledTasks implements Lifecycle<void> {
11
+ private LOGGER: Logger
12
+
13
+ constructor(readonly name: string, private readonly schedules: Rate[]) {
14
+ this.LOGGER = LoggerFactory.create(`SCHEDULER[${name}]`)
15
+ this.LOGGER.info('registered %i scheduled tasks', schedules.length);
16
+ }
17
+
18
+ intervals: NodeJS.Timeout[] = [];
19
+
20
+ start = async () => Promise.resolve(
21
+ this.schedules.forEach((schedule) => this.intervals.push(setInterval(schedule.task, schedule.frequency * 1000)))
22
+ )
23
+ .then(() => undefined)
24
+
25
+ stop = async () => Promise.resolve(this.intervals.forEach((interval) => clearInterval(interval)))
26
+ }
27
+
28
+ export type Rate = {
29
+ frequency: number;
30
+ task: () => Promise<unknown>;
31
+ }
@@ -0,0 +1,46 @@
1
+ import { Logger, LoggerFactory } from "@yopdev/logging";
2
+ import { DevServerConfig } from "./config";
3
+
4
+ export interface Lifecycle<I> extends Startable<I>, Stoppable {
5
+ name: string
6
+ }
7
+
8
+ export interface Startable<T> {
9
+ start(config: DevServerConfig): Promise<T>
10
+ }
11
+ export interface Stoppable {
12
+ stop(): Promise<void>;
13
+ }
14
+
15
+ export type Callback<I> = (instance: I) => Promise<void>
16
+
17
+ export class Service<I> implements Lifecycle<I> {
18
+ private readonly DEFAULT_CALLBACK: Callback<unknown> = () => Promise.resolve(this.LOGGER.debug('no callback'));
19
+
20
+ readonly name: string
21
+ readonly start: (config: DevServerConfig) => Promise<I>
22
+ readonly stop: () => Promise<void>
23
+ private doStop: () => Promise<void>
24
+ private readonly LOGGER: Logger
25
+
26
+ constructor(service: Lifecycle<I>, private readonly callback?: Callback<I>) {
27
+ this.name = service.name
28
+ this.LOGGER = LoggerFactory.create(`SERVICE[${this.name}]`)
29
+ const stop = async () => service.stop()
30
+ this.start = (config: DevServerConfig) => service.start(config)
31
+ .catch((error) => stop()
32
+ .then(() => this.doStop = () => Promise.resolve(this.LOGGER.error(error, 'failed')))
33
+ .then(() => Promise.reject(error))
34
+ )
35
+ .then((started) => this
36
+ .callbackOrDefault(started)
37
+ .then(() => this.LOGGER.info('started'))
38
+ .then(() => started)
39
+ )
40
+ this.doStop = () => stop()
41
+ .then(() => this.LOGGER.info('stopped'))
42
+ this.stop = () => this.doStop();
43
+ }
44
+
45
+ private callbackOrDefault = async (instance: I) => (this.callback ?? this.DEFAULT_CALLBACK)(instance)
46
+ }
@@ -0,0 +1,109 @@
1
+ import { Logger, LoggerFactory } from "@yopdev/logging";
2
+ import { HttpServer, HttpSettings } from "./http-server";
3
+ import { Sns } from "./sns";
4
+ import { IncomingMessage, ServerResponse } from "http";
5
+ import { MessageAttributeValue } from "@aws-sdk/client-sns";
6
+ import { internalServerError, writeResponse } from "./responses";
7
+ import { Lifecycle, Service, Callback } from "./services";
8
+ import { DevServerConfig } from "./config";
9
+ import { assertNotUndefined } from "./assert";
10
+
11
+ export const newSnsHttpProxy = (
12
+ name: string,
13
+ config: {
14
+ settings: HttpSettings,
15
+ method: string,
16
+ subject?: string,
17
+ topic?: string,
18
+ },
19
+ callback?: Callback<string>,
20
+ ) => new Service(new SnsHttpProxy(name, config.settings, config.method, config.subject, config.topic), callback)
21
+
22
+ export class SnsHttpProxy implements Lifecycle<string> {
23
+ private LOGGER: Logger
24
+ private server: HttpServer
25
+ private config: {
26
+ topic: string
27
+ sns: Sns
28
+ } | undefined
29
+
30
+ constructor(
31
+ readonly name: string,
32
+ private readonly settings: HttpSettings,
33
+ private readonly method: string,
34
+ private readonly subject?: string,
35
+ private readonly topic?: string,
36
+ ) {
37
+ this.server = new HttpServer(
38
+ this.name,
39
+ this.settings,
40
+ this.handler(this.method, this.subject)
41
+ )
42
+ this.LOGGER = LoggerFactory.create(`HTTP->SNS[${name}]`)
43
+ this.LOGGER.info(
44
+ 'forwarding %s %s events to %s',
45
+ subject ? `events with ${subject} subject` : 'all',
46
+ method,
47
+ topic ?? 'the event bus',
48
+ );
49
+ }
50
+
51
+ start = (config: DevServerConfig) => Promise.resolve(this.server)
52
+ .then(server => server.start())
53
+ .tap(() => {
54
+ this.config = {
55
+ topic: this.topic ?? config.eventsProxy.topic.arn,
56
+ sns: config.sns
57
+ }
58
+ })
59
+ .then((endpoint) => endpoint)
60
+
61
+ stop = () => this.onlyWhenStarted(this.server).stop()
62
+ .then(() => this.LOGGER.debug('stopped'))
63
+
64
+ handler = (method: string, subject?: string) =>
65
+ (request: IncomingMessage, body: string, response: ServerResponse) =>
66
+ Promise.resolve((assertNotUndefined(this.config)))
67
+ .then((config) =>
68
+ config.sns
69
+ .publish(
70
+ { arn: config.topic },
71
+ this.extractMessage(method, request, body),
72
+ subject,
73
+ this.extractMessageAttributes(request)
74
+ )
75
+ .then(() => writeResponse(response, 200, ''))
76
+ .catch((e) => {
77
+ this.LOGGER.error(e, 'request failed to execute');
78
+ internalServerError(response, e.body);
79
+ })
80
+ )
81
+
82
+ extractMessage = (method: string, request: IncomingMessage, body: string) => {
83
+ switch (method) {
84
+ case 'GET':
85
+ return this.extractQueryString(request) || '';
86
+ case 'POST':
87
+ return body;
88
+ default:
89
+ throw new Error();
90
+ }
91
+ }
92
+
93
+ private onlyWhenStarted = (server: HttpServer | undefined) => assertNotUndefined(server, 'not started')
94
+
95
+ private extractQueryString = (request: IncomingMessage) => request.url?.substring(request.url?.indexOf('?') + 1)
96
+
97
+ private extractMessageAttributes = (request: IncomingMessage): Record<string, MessageAttributeValue> => {
98
+ const initialValue = {};
99
+ return Object.keys(request.headers).reduce((obj, item) => {
100
+ return {
101
+ ...obj,
102
+ [item]: {
103
+ DataType: 'String',
104
+ StringValue: request.headers[item],
105
+ },
106
+ };
107
+ }, initialValue);
108
+ }
109
+ }
package/src/sns.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { CreateTopicCommand, MessageAttributeValue, PublishCommand, SNSClient, SubscribeCommand } from "@aws-sdk/client-sns";
2
+ import { AwsConfig } from "./config";
3
+ import { assertNotUndefined } from "./assert";
4
+ import { LoggerFactory } from "@yopdev/logging";
5
+
6
+ const LOGGER = LoggerFactory.create('SNS')
7
+ export class Sns {
8
+ readonly client: SNSClient;
9
+
10
+ constructor(aws: AwsConfig) {
11
+ this.client = new SNSClient(aws);
12
+ }
13
+
14
+ createTopic = async (topic: string) => this.client
15
+ .send(
16
+ new CreateTopicCommand({
17
+ Name: topic,
18
+ }),
19
+ )
20
+ .then((o) => assertNotUndefined(o.TopicArn, 'TopicArn is not present'))
21
+ .tap((arn) => LOGGER.debug('topic %o created', arn))
22
+
23
+ createSubscription = async (topic: { arn: string }, queue: { arn: string }) => this.client
24
+ .send(
25
+ new SubscribeCommand({
26
+ TopicArn: topic.arn,
27
+ Protocol: 'sqs',
28
+ Endpoint: queue.arn,
29
+ }),
30
+ )
31
+ .then(() => LOGGER.debug('subscription %s->%s created', topic.arn, queue.arn))
32
+
33
+ publish = async (
34
+ topic: { arn: string },
35
+ body: string,
36
+ subject?: string,
37
+ attributes?: Record<string, MessageAttributeValue>,
38
+ ) => this.client
39
+ .send(
40
+ new PublishCommand({
41
+ TopicArn: topic.arn,
42
+ Message: body,
43
+ MessageAttributes: attributes,
44
+ Subject: subject,
45
+ }),
46
+ )
47
+ .then(() => LOGGER.trace('published %s[%s]', topic.arn, subject))
48
+ .then()
49
+ }
package/src/sqs.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { CreateQueueCommand, GetQueueAttributesCommand, SQSClient } from "@aws-sdk/client-sqs";
2
+ import { assertNotUndefined } from "./assert";
3
+ import { AwsConfig } from "./config";
4
+ import { LoggerFactory } from "@yopdev/logging";
5
+
6
+ const QUEUE_NAME_ATTRIBUTE_NAME = 'QueueArn'
7
+ const LOGGER = LoggerFactory.create('SQS')
8
+ export class Sqs {
9
+ readonly client: SQSClient;
10
+
11
+ constructor(config: AwsConfig) {
12
+ this.client = new SQSClient(config)
13
+ }
14
+
15
+ createStandardQueue = async (name: string) => this.createNamedQueue(name, false);
16
+ createFifoQueue = async (name: string) => this.createNamedQueue(name, true);
17
+
18
+ private createNamedQueue = async (name: string, fifo: boolean): Promise<Queue> => this.client
19
+ .send(
20
+ new CreateQueueCommand({
21
+ QueueName: fifo ? `${name}.fifo` : name,
22
+ Attributes: {
23
+ FifoQueue: fifo ? 'true' : undefined,
24
+ }
25
+ }),
26
+ )
27
+ .then(
28
+ (create) => this.client
29
+ .send(
30
+ new GetQueueAttributesCommand({
31
+ QueueUrl: create.QueueUrl,
32
+ AttributeNames: [QUEUE_NAME_ATTRIBUTE_NAME],
33
+ }),
34
+ )
35
+ .then((attributes) => ({
36
+ url: assertNotUndefined(create.QueueUrl),
37
+ arn: assertNotUndefined(attributes.Attributes)[QUEUE_NAME_ATTRIBUTE_NAME]!,
38
+ }))
39
+ )
40
+ .tap((queue) => LOGGER.debug('created %s', queue.arn))
41
+ }
42
+
43
+ export type Queue = {
44
+ arn: string;
45
+ url: string;
46
+ };
@@ -0,0 +1,10 @@
1
+ import { Consumer } from "sqs-consumer";
2
+
3
+ export const stopConsumer = async (queueWaitTimeSecs: number, target?: Consumer): Promise<void> => {
4
+ if (target) {
5
+ target.stop();
6
+ return new Promise((resolve) => {
7
+ setTimeout(resolve, queueWaitTimeSecs * 1000 + 1);
8
+ });
9
+ } else { return Promise.resolve() }
10
+ }
package/src/tunnel.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { Lifecycle, Service } from './services';
2
+ import { Logger, LoggerFactory } from '@yopdev/logging';
3
+ import * as ngrok from '@ngrok/ngrok';
4
+ import { assertNotUndefined } from './assert';
5
+
6
+ export const terminate = async () => ngrok.kill();
7
+ export const newTunnel = (config: { name: string; port: number; fixed?: { name: string; token: string } }) =>
8
+ new Service(new Tunnel(config.name, config.port, config.fixed));
9
+ class Tunnel implements Lifecycle<string> {
10
+ private logger: Logger;
11
+ constructor(
12
+ readonly name: string,
13
+ private readonly port: number,
14
+ private readonly domain?: {
15
+ name: string,
16
+ token: string,
17
+ },
18
+ ) {
19
+ this.logger = LoggerFactory.create(`TUNNEL[${name}]`);
20
+ }
21
+ start = async () =>
22
+ ngrok
23
+ .forward({
24
+ proto: 'http',
25
+ domain: this.domain?.name,
26
+ addr: this.port,
27
+ authtoken: this.domain?.token,
28
+ })
29
+ .then((listener) => assertNotUndefined(listener.url()))
30
+ .tap((url) => this.logger.debug(url))
31
+ stop = async () => ngrok.disconnect();
32
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "NodeNext",
4
+ "target": "ES2021",
5
+ "moduleResolution": "NodeNext",
6
+ "declaration": true,
7
+ "outDir": "./dist"
8
+ }
9
+ }