diff --git a/libs/ngxtension/inject-rate-limited/README.md b/libs/ngxtension/inject-rate-limited/README.md new file mode 100644 index 000000000..3bdf5eadf --- /dev/null +++ b/libs/ngxtension/inject-rate-limited/README.md @@ -0,0 +1,3 @@ +# ngxtension/inject-rate-limited + +Secondary entry point of `ngxtension`. It can be used by importing from `ngxtension/inject-rate-limited`. diff --git a/libs/ngxtension/inject-rate-limited/ng-package.json b/libs/ngxtension/inject-rate-limited/ng-package.json new file mode 100644 index 000000000..b3e53d699 --- /dev/null +++ b/libs/ngxtension/inject-rate-limited/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/ngxtension/inject-rate-limited/src/index.ts b/libs/ngxtension/inject-rate-limited/src/index.ts new file mode 100644 index 000000000..3d6495c60 --- /dev/null +++ b/libs/ngxtension/inject-rate-limited/src/index.ts @@ -0,0 +1 @@ +export * from './inject-rate-limited'; diff --git a/libs/ngxtension/inject-rate-limited/src/inject-rate-limited.ts b/libs/ngxtension/inject-rate-limited/src/inject-rate-limited.ts new file mode 100644 index 000000000..bcf38bc0c --- /dev/null +++ b/libs/ngxtension/inject-rate-limited/src/inject-rate-limited.ts @@ -0,0 +1,95 @@ +import { + inject, + InjectionToken, + Injector, + Signal, + signal, +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { assertInjector } from 'ngxtension/assert-injector'; +import { + asyncScheduler, + auditTime, + debounceTime, + sampleTime, + Subject, + ThrottleConfig, + throttleTime, +} from 'rxjs'; + +const NGXTENSION_RATE_LIMIT = new InjectionToken('NGXTENSION_RATE_LIMIT', { + providedIn: 'root', + factory: () => 200, +}); + +export type RateLimitedSignal = Signal & { + next: (value: T) => void; +}; + +export interface RateLimitedSignalOptions { + /** + * Time window in milliseconds used by the operator. + * Defaults to the NGXTENSION_RATE_LIMIT injection token. + */ + durationMs?: number; + + /** + * RxJS operator used to rate-limit updates. + * @default debounceTime + */ + operator?: + | typeof debounceTime + | typeof throttleTime + | typeof sampleTime + | typeof auditTime; + + /** + * Optional injector override. + */ + injector?: Injector; + + /** + * Configuration for `throttleTime`, if used. + * @default { leading: true, trailing: false } + */ + config?: ThrottleConfig; +} + +/** + * Creates a signal that buffers and emits updates using a rate-limiting RxJS operator. + */ +export function injectRateLimited( + initialValue: T, + options: RateLimitedSignalOptions = {}, +): RateLimitedSignal { + return assertInjector(injectRateLimited, options?.injector, () => { + const defaultRateLimit = inject(NGXTENSION_RATE_LIMIT); + + const { + durationMs: delayMs = defaultRateLimit, + operator = debounceTime, + config = { + leading: true, + trailing: false, + }, + } = options; + + const subject = new Subject(); + const inner = signal(initialValue); + + subject + .pipe(operator(delayMs, asyncScheduler, config), takeUntilDestroyed()) + .subscribe((value) => inner.set(value)); + + return new Proxy(inner.asReadonly(), { + get(target, prop, receiver) { + switch (prop) { + case 'next': + return (value: T) => subject.next(value); + default: + return Reflect.get(target, prop, receiver); + } + }, + }); + }); +} diff --git a/tsconfig.base.json b/tsconfig.base.json index ff52593c4..2efe9bde7 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -108,6 +108,9 @@ "ngxtension/inject-query-params": [ "libs/ngxtension/inject-query-params/src/index.ts" ], + "ngxtension/inject-rate-limited": [ + "libs/ngxtension/inject-rate-limited/src/index.ts" + ], "ngxtension/inject-route-data": [ "libs/ngxtension/inject-route-data/src/index.ts" ],