Skip to content
This repository was archived by the owner on Mar 5, 2025. It is now read-only.
Draft
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
19 changes: 10 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,19 @@
"tslib": "^2.3.0"
},
"devDependencies": {
"@types/jest": "^26.0.23",
"@types/node": "^15.14.0",
"@typescript-eslint/eslint-plugin": "^4.28.1",
"@typescript-eslint/parser": "^4.28.1",
"eslint": "^7.29.0",
"@types/jest": "^27.0.2",
"@types/node": "^16.10.9",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"jest": "^27.0.6",
"event-target-shim": "^6.0.2",
"jest": "^27.2.5",
"jest-date-mock": "^1.0.8",
"npm-run-all": "^4.1.5",
"prettier": "^2.3.2",
"ts-jest": "^27.0.3",
"typescript": "^4.3.5"
"prettier": "^2.4.1",
"ts-jest": "^27.0.5",
"typescript": "^4.4.4"
},
"scripts": {
"check": "tsc --noEmit",
Expand Down
22 changes: 22 additions & 0 deletions src/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { StorageInterface } from './types';

export class Cache {
constructor(
protected readonly storage: StorageInterface | Cache,
protected readonly namespace: string
) {}

async setItem(key: string, value: string): Promise<void> {
return this.storage.setItem(`${this.namespace}:${key}`, value);
}

async getItem(key: string): Promise<string | null> {
return this.storage.getItem(`${this.namespace}:${key}`);
}

async removeItem(...keys: readonly string[]): Promise<void> {
await Promise.all(
keys.map((key) => this.storage.removeItem(`${this.namespace}:${key}`))
);
}
}
42 changes: 42 additions & 0 deletions src/channel/channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
ChannelOptions,
ClientInterface,
CreateLogStreamPayload,
LogStreamNameResolver,
NonEmptyString,
PutLogEventsPayload,
} from '../types';
import { InputLogEvent } from '@aws-sdk/client-cloudwatch-logs';

export class Channel<N extends string = string> {
readonly client: ClientInterface;
readonly logGroupName: string;
readonly logStreamNameResolver: LogStreamNameResolver;
readonly interval: number;

constructor(readonly name: NonEmptyString<N>, options: ChannelOptions) {
this.client = options.client;
this.logGroupName = options.logGroupName;
this.logStreamNameResolver = options.logStreamNameResolver;
this.interval = options.interval;
}

async createPutLogEventsPayload(
events: readonly InputLogEvent[],
sequenceToken?: string | null
): Promise<PutLogEventsPayload> {
return {
logGroupName: this.logGroupName,
logStreamName: await this.logStreamNameResolver(),
logEvents: events,
...(sequenceToken ? { sequenceToken } : undefined),
};
}

async createCreateLogStreamPayload(): Promise<CreateLogStreamPayload> {
return {
logGroupName: this.logGroupName,
logStreamName: await this.logStreamNameResolver(),
};
}
}
45 changes: 45 additions & 0 deletions src/channel/channelCollection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Channel } from './channel';
import { wrapAsArray } from '../util';
import { Collection } from '../collection';

export class ChannelCollection extends Collection<Channel> {
static wrap(
channels:
| Channel
| readonly Channel[]
| Collection<Channel>
| ChannelCollection
): ChannelCollection {
return channels instanceof ChannelCollection
? channels
: new ChannelCollection(
channels instanceof Collection
? channels.items
: wrapAsArray(channels)
);
}

constructor(readonly items: readonly Channel[]) {
super('Channel', items);
}

filterByNamePrefix(
namePrefix: string | readonly string[]
): ChannelCollection {
return ChannelCollection.wrap(this.filterByPrefix('name', namePrefix));
}

filterByLogGroupName(
logGroupName: string | readonly string[]
): ChannelCollection {
return ChannelCollection.wrap(this.filterBy('logGroupName', logGroupName));
}

findByName(name: string): Channel | undefined {
return this.findBy('name', name);
}

findByLogGroupName(logGroupName: string): Channel | undefined {
return this.findBy('logGroupName', logGroupName);
}
}
51 changes: 51 additions & 0 deletions src/channel/channelFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
ChannelFactoryOptions,
ClientInterface,
Exact,
LogStreamNameResolver,
NonEmptyString,
RequireMissing,
} from '../types';
import { Channel } from './channel';

export class ChannelFactory<O extends Partial<ChannelFactoryOptions>> {
protected readonly client: ClientInterface | null;
protected readonly logGroupName: string | null;
protected readonly logStreamNameResolver: LogStreamNameResolver;
protected readonly interval: number;

// Any essential parameters can be omitted in constructor.
constructor(options?: Exact<O, Partial<ChannelFactoryOptions>>) {
this.client = options?.client ?? null;
this.logGroupName = options?.logGroupName ?? null;
this.logStreamNameResolver =
options?.logStreamNameResolver ?? (() => 'anonymous');
this.interval = options?.interval ?? 3000;
}

// Omitted parameters in constructor must be passed here.
createChannel<N extends string>(
name: NonEmptyString<N>,
...args: RequireMissing<ChannelFactoryOptions, O>
): Channel;
createChannel(
name: string,
options?: Partial<ChannelFactoryOptions>
): Channel {
const client = options?.client ?? this.client;
if (!client) {
throw new Error('Missing channel config: client');
}
const logGroupName = options?.logGroupName ?? this.logGroupName;
if (!logGroupName) {
throw new Error('Missing channel config: logGroupName');
}
return new Channel(name, {
client,
logGroupName,
logStreamNameResolver:
options?.logStreamNameResolver ?? this.logStreamNameResolver,
interval: options?.interval ?? this.interval,
});
}
}
3 changes: 3 additions & 0 deletions src/channel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './channel';
export * from './channelCollection';
export * from './channelFactory';
39 changes: 39 additions & 0 deletions src/collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { wrapAsArray } from './util';
import { KeyOfType } from './types';

export class Collection<T> {
constructor(readonly type: string, readonly items: readonly T[]) {}

protected filterBy<B extends keyof T>(
by: B,
values: T[B] | readonly T[B][]
): Collection<T> {
const search = wrapAsArray(values);
return new Collection<T>(
this.type,
this.items.filter((item) => search.includes(item[by]))
);
}

protected filterByPrefix<B extends KeyOfType<T, string>>(
by: B,
values: string | readonly string[]
): Collection<T> {
const search = wrapAsArray(values);
return new Collection<T>(
this.type,
this.items.filter((item) =>
search.some((prefix) => {
const value = item[by];
return (
typeof value === 'string' && (value as string).startsWith(prefix)
);
})
)
);
}

protected findBy<B extends keyof T>(by: B, value: T[B]): T | undefined {
return this.filterBy(by, value).items[0];
}
}
15 changes: 15 additions & 0 deletions src/collector/collector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { SourceCollection } from '../source';
import { Channel } from '../channel';
import { Sender } from '../sender';

export class Collector {
constructor(readonly channel: Channel, readonly sources: SourceCollection) {}

get channelName(): string {
return this.channel.name;
}

async collect(sender: Sender): Promise<void> {
return sender.send(this.sources.flush());
}
}
53 changes: 53 additions & 0 deletions src/collector/collectorCollection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Collector } from './collector';
import { wrapAsArray } from '../util';
import { Collection } from '../collection';
import { Source, SourceCollection } from '../source';
import { ChannelCollection } from '../channel';

export class CollectorCollection extends Collection<Collector> {
static wrap(
collectors:
| Collector
| readonly Collector[]
| Collection<Collector>
| CollectorCollection
): CollectorCollection {
return collectors instanceof CollectorCollection
? collectors
: new CollectorCollection(
collectors instanceof Collection
? collectors.items
: wrapAsArray(collectors)
);
}

constructor(readonly items: readonly Collector[]) {
super('Collector', items);
}

get channels(): ChannelCollection {
return ChannelCollection.wrap(
this.items.map((collector) => collector.channel)
);
}

get sources(): SourceCollection {
return SourceCollection.wrap(
([] as readonly Source[]).concat(
...this.items.map((collector) => collector.sources.items)
)
);
}

filterByChannelNamePrefix(
namePrefix: string | readonly string[]
): CollectorCollection {
return CollectorCollection.wrap(
this.filterByPrefix('channelName', namePrefix)
);
}

findByChannelName(name: string): Collector | undefined {
return this.findBy('channelName', name);
}
}
12 changes: 12 additions & 0 deletions src/collector/collectorFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Source, SourceCollection } from '../source';
import { Channel } from '../channel';
import { Collector } from './collector';

export class CollectorFactory {
createCollector(
channel: Channel,
sources: Source | readonly Source[] | SourceCollection
): Collector {
return new Collector(channel, SourceCollection.wrap(sources));
}
}
3 changes: 3 additions & 0 deletions src/collector/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './collector';
export * from './collectorCollection';
export * from './collectorFactory';
48 changes: 48 additions & 0 deletions src/environment/environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
ConsoleInterface,
EnvironmentalOptions,
StorageInterface,
} from '../types';
import { BrowserPolyfillRequiredError } from '../util';

export class Environment implements EnvironmentalOptions {
readonly storage: StorageInterface;
readonly console: ConsoleInterface;
readonly setInterval: typeof setInterval;
readonly clearInterval: typeof clearInterval;
readonly setTimeout: typeof setTimeout;
readonly eventTarget: EventTarget;

constructor(options?: EnvironmentalOptions) {
this.storage = getStorageImpl(options);
this.console = options?.console ?? console;
this.setInterval = options?.setInterval ?? setInterval;
this.clearInterval = options?.clearInterval ?? clearInterval;
this.setTimeout = options?.setTimeout ?? setTimeout;
this.eventTarget = getEventTargetImpl(options);
}
}

const getStorageImpl = (options?: EnvironmentalOptions): StorageInterface => {
return (
options?.storage ??
(typeof localStorage !== 'undefined'
? localStorage
: needsPolyfill(['localStorage']))
);
};

const getEventTargetImpl = (options?: EnvironmentalOptions): EventTarget => {
if (options?.eventTarget) {
return options?.eventTarget;
}
const top = typeof window !== 'undefined' ? window : globalThis;
if (!top.addEventListener || !top.dispatchEvent) {
needsPolyfill(['addEventListener', 'dispatchEvent']);
}
return top;
};

const needsPolyfill = (components: readonly (keyof Window)[]): never => {
throw new BrowserPolyfillRequiredError(components);
};
2 changes: 2 additions & 0 deletions src/environment/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './environment';
export * from './installedEnvironment';
11 changes: 11 additions & 0 deletions src/environment/installedEnvironment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ConsoleInterface } from '../types';
import { Environment } from './environment';

export class InstalledEnvironment extends Environment {
constructor(
readonly originalConsole: ConsoleInterface,
environment: Environment
) {
super(environment);
}
}
Loading