Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1
feat: Typescript implementation#6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base:main
Are you sure you want to change the base?
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
8864382a9e3016c936623aecaab1b9cc8bf4cf047a5903fa2ad6a3e18cc9f4631e49fa38abf1127dbbe4c83ce64df14920ab258edf3ef3a453898fae8e66210d47a847c8f136807ac30f2ea9b6a74cf9f47073f19cf8ffb7c3bc71dfff3c8cb766e5601f8afFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,7 @@ | ||
| node_modules | ||
| coverage | ||
| dist | ||
| dist | ||
| .github/copilot-instructions.md | ||
| .gitconfig |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "trailingComma": "all", | ||
| "tabWidth": 2, | ||
| "singleQuote": true, | ||
| "semi": false | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,12 @@ | ||
| // @ts-check | ||
| import eslint from '@eslint/js'; | ||
| import tseslint from 'typescript-eslint'; | ||
| import eslint from '@eslint/js' | ||
| import tseslint from 'typescript-eslint' | ||
| export default tseslint.config( | ||
| eslint.configs.recommended, | ||
| ...tseslint.configs.recommended, | ||
| ); | ||
| { | ||
| ignores: ['node_modules/**', 'dist/**'], | ||
| }, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,206 +1,3 @@ | ||
| import EventEmitter from 'events' | ||
| import { AckPolicy } from '@nats-io/jetstream' | ||
| import { nanos, NatsError } from '@nats-io/nats-core' | ||
| import type { JetStreamClient, Consumer, JsMsg} from '@nats-io/jetstream' | ||
| import type { MsgHdrs } from '@nats-io/nats-core' | ||
| import { createSubject, sleep } from './utils' | ||
| import { FixedWindowLimiter, IntervalLimiter, type Limiter } from './limiter' | ||
| export type QueueOpts | ||
| = { | ||
| client: JetStreamClient | ||
| name: string | ||
| /** | ||
| * ms | ||
| */ | ||
| deduplicateWindow?: number | ||
| } | ||
| export type Job = { | ||
| id?: number | string | ||
| name: string | ||
| data: unknown | ||
| } | ||
| export type AddOptions = { | ||
| id?: string | ||
| headers?: MsgHdrs | ||
| } | ||
| export const DEFAULT_DEDUPLICATE_WINDOW = 2_000 | ||
| export class Queue { | ||
| protected readonly client: JetStreamClient | ||
| protected readonly name: string | ||
| protected readonly deduplicateWindow: number | ||
| constructor(opts: QueueOpts) { | ||
| this.client = opts.client | ||
| this.name = opts.name | ||
| this.deduplicateWindow = opts.deduplicateWindow || DEFAULT_DEDUPLICATE_WINDOW | ||
| } | ||
| async setup() { | ||
| const manager = await this.client.jetstreamManager() | ||
| try { | ||
| await manager.streams.add({ | ||
| name: this.name, | ||
| subjects: [`${this.name}.*`], | ||
| duplicate_window: nanos(this.deduplicateWindow) | ||
| }) | ||
| } catch (e) { | ||
| // TODO smart error handling | ||
| if (!(e instanceof NatsError)) { | ||
| throw e | ||
| } | ||
| await manager.streams.update( | ||
| this.name, | ||
| { | ||
| subjects: [`${this.name}.*`], | ||
| duplicate_window: nanos(this.deduplicateWindow) | ||
| } | ||
| ) | ||
| } | ||
| } | ||
| async add(name: string, data?: unknown, options?: AddOptions) { | ||
| const payload = JSON.stringify(data) | ||
| return this.client.publish(`${this.name}.${name}`, payload, options && { | ||
| msgID: options.id, | ||
| headers: options.headers | ||
| }) | ||
| } | ||
| } | ||
| export type RateLimit = { | ||
| duration: number | ||
| max: number | ||
| } | ||
| export type WorkerOpts = { | ||
| client: JetStreamClient | ||
| name: string | ||
| processor: (job: JsMsg) => Promise<void> | ||
| concurrency?: number | ||
| rateLimit?: RateLimit | ||
| } | ||
| export class Worker extends EventEmitter { | ||
| protected readonly client: JetStreamClient | ||
| protected readonly name: string | ||
| protected readonly processor: (job: JsMsg) => Promise<void> | ||
| protected readonly concurrency: number | ||
| protected readonly limiter: Limiter | ||
| protected readonly fetchInterval: number | ||
| protected readonly fetchTimeout: number | ||
| protected consumer: Consumer | null = null | ||
| protected running = false | ||
| protected processingNow = 0 | ||
| protected loopPromise: Promise<void> | null = null | ||
| constructor(opts: WorkerOpts) { | ||
| super() | ||
| this.client = opts.client | ||
| this.name = opts.name | ||
| this.processor = opts.processor | ||
| this.concurrency = opts.concurrency || 1 | ||
| this.fetchInterval = 150 | ||
| this.fetchTimeout = 3_000 | ||
| this.limiter = opts.rateLimit ? | ||
| new FixedWindowLimiter(opts.rateLimit.max, opts.rateLimit.duration, this.fetchInterval) : | ||
| new IntervalLimiter(this.fetchInterval) | ||
| } | ||
| async setup() { | ||
| const manager = await this.client.jetstreamManager() | ||
| try { | ||
| await manager.streams.add({ | ||
| name: this.name, | ||
| subjects: [createSubject(this.name)], | ||
| }) | ||
| } catch (e) { | ||
| if (!(e instanceof NatsError && e.api_error?.err_code === 10058)) { | ||
| throw e | ||
| } | ||
| } | ||
| await manager.consumers.add(this.name, { | ||
| durable_name: this.name, | ||
| ack_policy: AckPolicy.All, | ||
| }) | ||
| this.consumer = await this.client.consumers.get(this.name, this.name) | ||
| } | ||
| async stop() { | ||
| this.running = false | ||
| if (this.loopPromise) { | ||
| await this.loopPromise | ||
| } | ||
| while (this.processingNow > 0) { | ||
| await sleep(this.fetchInterval) | ||
| } | ||
| } | ||
| start() { | ||
| if (!this.consumer) { | ||
| throw new Error('call setup() before start()') | ||
| } | ||
| if (!this.loopPromise) { | ||
| this.running = true | ||
| this.loopPromise = this.loop() | ||
| } | ||
| } | ||
| protected async loop() { | ||
| while (this.running) { | ||
| const max = this.limiter.get(this.concurrency - this.processingNow) | ||
| const jobs = await this.fetch(max) | ||
| for await (const j of jobs) { | ||
| this.limiter.inc() | ||
| this.process(j) // without await! | ||
| } | ||
| await sleep(this.limiter.timeout()) | ||
| } | ||
| } | ||
| protected async process(j: JsMsg) { | ||
| this.processingNow += 1 | ||
| try { | ||
| this.process(j) | ||
| await j.ackAck() | ||
| } catch (e) { | ||
| await j.term() | ||
| } finally { | ||
| this.processingNow -= 1 | ||
| } | ||
| } | ||
| protected async fetch(count: number) { | ||
| try { | ||
| return this.consumer!.fetch({ | ||
| max_messages: count, | ||
| expires: this.fetchTimeout | ||
| }) | ||
| } catch (e) { | ||
| // TODO | ||
| return [] | ||
| } | ||
| } | ||
| } | ||
| export * from './queue' | ||
| export * from './job' | ||
| export * from './worker' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { JobCreateData } from './types' | ||
| export class Job { | ||
Collaborator There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Надо джобу иметь для консьюмера. | ||
| id: string | ||
| name: string | ||
| meta: { | ||
| failed: boolean | ||
| startTime: number | ||
| retryCount: number | ||
| timeout: number | ||
| // parentId?: string | ||
| } | ||
| data: unknown | ||
| queueName: string | ||
| constructor(data: JobCreateData) { | ||
| this.id = data.id ?? `${crypto.randomUUID()}_${Date.now()}` | ||
| this.queueName = data.queueName | ||
| this.name = data.name | ||
| this.data = data.data | ||
| this.meta = { | ||
| retryCount: 0, | ||
| startTime: Date.now() + (data.delay ?? 0), | ||
Collaborator There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Лучше вообще не писать свойство, если это можно сделать. | ||
| failed: false, | ||
Collaborator There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Это по принципу отсутствие failed === false | ||
| // TODO: Is this correct? | ||
| timeout: data.timeout ?? 0, | ||
Collaborator There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Это нафиг | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| type Event<T, D> = { | ||
| event: T | ||
| data: D | ||
| } | ||
| export type JobCompletedEvent = Event<'JOB_COMPLETED', { jobId: string }> | ||
| export type JobFailedEvent = Event<'JOB_FAILED', { jobId: string }> | ||
| // export type JobChildCompletedEvent = Event< | ||
| // 'JOB_CHILD_COMPLETED', | ||
| // { childId: string; parentId: string } | ||
| // > | ||
| // export type JobChildFailedEvent = Event< | ||
| // 'JOB_CHILD_FAILED', | ||
| // { childId: string; parentId: string } | ||
| // > | ||
| export type JobEvent = | ||
| // | JobChildFailedEvent | ||
| // | JobChildCompletedEvent | ||
| JobCompletedEvent | JobFailedEvent |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Вот эти зависимости надо указывать в peerDependecies.
И в дев вернуть.