diff --git a/.changeset/queued-scalar-inputs.md b/.changeset/queued-scalar-inputs.md new file mode 100644 index 000000000..b540b6801 --- /dev/null +++ b/.changeset/queued-scalar-inputs.md @@ -0,0 +1,5 @@ +--- +'@tanstack/angular-pacer': minor +--- + +Return the processed scalar from `injectQueuedValue` and defer source reads until Angular inputs are available. Support primitive initial values and explicit options for object or function initial values. Replace uses of the previous `queued()` pending-items array with `queued.queuer.state().items` or `injectQueuedSignal`. diff --git a/docs/framework/angular/guides/queuing.md b/docs/framework/angular/guides/queuing.md index 1bcf9f03c..07775034e 100644 --- a/docs/framework/angular/guides/queuing.md +++ b/docs/framework/angular/guides/queuing.md @@ -69,6 +69,40 @@ The focused snippets later in this guide use `injectQueuer` and assume they run Pass `initialItems` when work is already available at creation time. The queue applies its normal insertion and capacity rules, and automatic processing can begin immediately unless `started: false` is set. +## Queue a source signal + +`injectQueuedValue` returns a `QueuedValueSignal`: `queued()` reads the most recently processed value, `queued.addItem(value)` enqueues a value, and `queued.queuer` exposes the queue controls. Its default selector exposes pending items at `queued.queuer.state().items`. + +```ts +readonly source = input.required() +readonly queued = injectQueuedValue(this.source, { wait: 500 }) +``` + +Construction does not read the source. Angular's effect reads it after inputs are bound and enqueues the latest value. Each later effect run enqueues the latest observed source value. Several source writes before an effect runs are coalesced; use `queued.addItem` for every value that must enter the queue individually. + +Before processing begins, `queued()` reads the initial source value. Reading it before a required input is bound throws Angular's required-input error. Supply an explicit initial value if the output must be readable earlier: + +```ts +const text = injectQueuedValue(source, '') +const object = injectQueuedValue(objectSource, { label: 'Loading' }, {}) +const callback = injectQueuedValue(callbackSource, () => 'Loading', {}) +``` + +Two-argument primitive initial values, including `0`, `false`, `''`, and `null`, are supported. Object and function initial values require a third options object, such as `{}`. A function in the options position is an options factory. + +Pass a fourth selector argument, which can be `undefined`, when an explicit initial value uses factory or undefined options: + +```ts +const queued = injectQueuedValue(source, '', () => ({ wait: wait() }), undefined) +const initialObject = injectQueuedValue(objectSource, { label: 'Loading' }, undefined, undefined) +``` + +`injectQueuedValue(source, options, undefined)` keeps the options-only meaning. An explicit `undefined` initial value requires an options object or a fourth argument. + +### Migrate from the previous return shape + +Earlier versions returned the pending-items array from `queued()`. It now returns the processed scalar value. Replace `queued().length` with `queued.queuer.state().items.length` using the default selector, or use `injectQueuedSignal` when the queue contents are the desired signal value. Custom selectors must include `items` to access that list. + ## Ordering items Automatic processing uses `addItemsTo` to choose where new items enter and `getItemsFrom` to choose where items leave. diff --git a/docs/framework/angular/reference/functions/injectQueuedValue.md b/docs/framework/angular/reference/functions/injectQueuedValue.md index 9b341d252..49be825d6 100644 --- a/docs/framework/angular/reference/functions/injectQueuedValue.md +++ b/docs/framework/angular/reference/functions/injectQueuedValue.md @@ -9,10 +9,10 @@ title: injectQueuedValue function injectQueuedValue( value, options?, -selector?): QueuedSignal; +selector?): QueuedValueSignal; ``` -Defined in: [queuer/injectQueuedValue.ts:32](https://github.com/TanStack/pacer/blob/main/packages/angular-pacer/src/queuer/injectQueuedValue.ts#L32) +Defined in: [queuer/injectQueuedValue.ts:48](https://github.com/TanStack/pacer/blob/main/packages/angular-pacer/src/queuer/injectQueuedValue.ts#L48) An Angular function that creates a queued value that processes state changes in order with an optional delay. This function uses injectQueuedSignal internally to manage a queue of state changes and apply them sequentially. @@ -21,9 +21,17 @@ The queued value will process changes in the order they are received, with optio processing each change. This is useful for handling state updates that need to be processed in a specific order, like animations or sequential UI updates. -The function returns a tuple containing: -- A Signal that provides the current queued value -- The queuer instance with control methods +The returned signal provides the most recently processed value. Before the queue processes +an item, it provides the explicit initial value or the source's initial value. +Source reads are deferred until the signal is read or Angular runs its effect, so required +inputs can be bound before their values are read. + +Use `queued.addItem(...)` to add a value and `queued.queuer` to control the queue. +Pending items are available through `queued.queuer.state().items` with the default selector. + +Pass an options object as the third argument to supply an object or function as the initial +value. When those options are undefined or a factory, pass a fourth selector argument +(undefined is allowed) to distinguish the initial-value form from options plus a selector. ### Type Parameters @@ -43,7 +51,7 @@ The function returns a tuple containing: #### options? -[`AngularPacerOptions`](../type-aliases/AngularPacerOptions.md)\<`QueuerOptions`\<`TValue`\>\> +[`AngularPacerOptions`](../type-aliases/AngularPacerOptions.md)\<[`AngularQueuerOptions`](../interfaces/AngularQueuerOptions.md)\<`TValue`, `TSelected`\>\> #### selector? @@ -51,7 +59,7 @@ The function returns a tuple containing: ### Returns -[`QueuedSignal`](../interfaces/QueuedSignal.md)\<`TValue`, `TSelected`\> +[`QueuedValueSignal`](../type-aliases/QueuedValueSignal.md)\<`TValue`, `TSelected`\> ### Example @@ -73,10 +81,10 @@ function injectQueuedValue( value, initialValue, options?, -selector?): QueuedSignal; +selector?): QueuedValueSignal; ``` -Defined in: [queuer/injectQueuedValue.ts:43](https://github.com/TanStack/pacer/blob/main/packages/angular-pacer/src/queuer/injectQueuedValue.ts#L43) +Defined in: [queuer/injectQueuedValue.ts:59](https://github.com/TanStack/pacer/blob/main/packages/angular-pacer/src/queuer/injectQueuedValue.ts#L59) An Angular function that creates a queued value that processes state changes in order with an optional delay. This function uses injectQueuedSignal internally to manage a queue of state changes and apply them sequentially. @@ -85,9 +93,17 @@ The queued value will process changes in the order they are received, with optio processing each change. This is useful for handling state updates that need to be processed in a specific order, like animations or sequential UI updates. -The function returns a tuple containing: -- A Signal that provides the current queued value -- The queuer instance with control methods +The returned signal provides the most recently processed value. Before the queue processes +an item, it provides the explicit initial value or the source's initial value. +Source reads are deferred until the signal is read or Angular runs its effect, so required +inputs can be bound before their values are read. + +Use `queued.addItem(...)` to add a value and `queued.queuer` to control the queue. +Pending items are available through `queued.queuer.state().items` with the default selector. + +Pass an options object as the third argument to supply an object or function as the initial +value. When those options are undefined or a factory, pass a fourth selector argument +(undefined is allowed) to distinguish the initial-value form from options plus a selector. ### Type Parameters @@ -107,11 +123,87 @@ The function returns a tuple containing: #### initialValue -`TValue` +`Extract`\<`TValue`, `PrimitiveInitialValue`\> #### options? -`QueuerOptions`\<`TValue`\> +[`AngularQueuerOptions`](../interfaces/AngularQueuerOptions.md)\<`TValue`, `TSelected`\> + +#### selector? + +(`state`) => `TSelected` + +### Returns + +[`QueuedValueSignal`](../type-aliases/QueuedValueSignal.md)\<`TValue`, `TSelected`\> + +### Example + +```ts +const initialValue = signal('initial') +const queued = injectQueuedValue(initialValue, { + wait: 500, + started: true, +}) + +// Add changes to the queue +queued.addItem('new value') +``` + +## Call Signature + +```ts +function injectQueuedValue( + value, + initialValue, + options, +selector?): QueuedValueSignal; +``` + +Defined in: [queuer/injectQueuedValue.ts:71](https://github.com/TanStack/pacer/blob/main/packages/angular-pacer/src/queuer/injectQueuedValue.ts#L71) + +An Angular function that creates a queued value that processes state changes in order with an optional delay. +This function uses injectQueuedSignal internally to manage a queue of state changes and apply them sequentially. + +The queued value will process changes in the order they are received, with optional delays between +processing each change. This is useful for handling state updates that need to be processed +in a specific order, like animations or sequential UI updates. + +The returned signal provides the most recently processed value. Before the queue processes +an item, it provides the explicit initial value or the source's initial value. +Source reads are deferred until the signal is read or Angular runs its effect, so required +inputs can be bound before their values are read. + +Use `queued.addItem(...)` to add a value and `queued.queuer` to control the queue. +Pending items are available through `queued.queuer.state().items` with the default selector. + +Pass an options object as the third argument to supply an object or function as the initial +value. When those options are undefined or a factory, pass a fourth selector argument +(undefined is allowed) to distinguish the initial-value form from options plus a selector. + +### Type Parameters + +#### TValue + +`TValue` + +#### TSelected + +`TSelected` *extends* `Pick`\<`QueuerState`\<`TValue`\>, `"items"`\> = `Pick`\<`QueuerState`\<`TValue`\>, `"items"`\> + +### Parameters + +#### value + +`Signal`\<`TValue`\> + +#### initialValue + +`TValue` + +#### options + +[`AngularQueuerOptions`](../interfaces/AngularQueuerOptions.md)\<`TValue`, `TSelected`\> #### selector? @@ -119,7 +211,7 @@ The function returns a tuple containing: ### Returns -[`QueuedSignal`](../interfaces/QueuedSignal.md)\<`TValue`, `TSelected`\> +[`QueuedValueSignal`](../type-aliases/QueuedValueSignal.md)\<`TValue`, `TSelected`\> ### Example @@ -141,10 +233,10 @@ function injectQueuedValue( value, initialValue, options, -selector): QueuedSignal; +selector): QueuedValueSignal; ``` -Defined in: [queuer/injectQueuedValue.ts:55](https://github.com/TanStack/pacer/blob/main/packages/angular-pacer/src/queuer/injectQueuedValue.ts#L55) +Defined in: [queuer/injectQueuedValue.ts:83](https://github.com/TanStack/pacer/blob/main/packages/angular-pacer/src/queuer/injectQueuedValue.ts#L83) An Angular function that creates a queued value that processes state changes in order with an optional delay. This function uses injectQueuedSignal internally to manage a queue of state changes and apply them sequentially. @@ -153,9 +245,17 @@ The queued value will process changes in the order they are received, with optio processing each change. This is useful for handling state updates that need to be processed in a specific order, like animations or sequential UI updates. -The function returns a tuple containing: -- A Signal that provides the current queued value -- The queuer instance with control methods +The returned signal provides the most recently processed value. Before the queue processes +an item, it provides the explicit initial value or the source's initial value. +Source reads are deferred until the signal is read or Angular runs its effect, so required +inputs can be bound before their values are read. + +Use `queued.addItem(...)` to add a value and `queued.queuer` to control the queue. +Pending items are available through `queued.queuer.state().items` with the default selector. + +Pass an options object as the third argument to supply an object or function as the initial +value. When those options are undefined or a factory, pass a fourth selector argument +(undefined is allowed) to distinguish the initial-value form from options plus a selector. ### Type Parameters @@ -179,7 +279,8 @@ The function returns a tuple containing: #### options -() => `QueuerOptions`\<`TValue`\> + \| [`AngularPacerOptions`](../type-aliases/AngularPacerOptions.md)\<[`AngularQueuerOptions`](../interfaces/AngularQueuerOptions.md)\<`TValue`, `TSelected`\>\> + \| `undefined` #### selector @@ -187,7 +288,7 @@ The function returns a tuple containing: ### Returns -[`QueuedSignal`](../interfaces/QueuedSignal.md)\<`TValue`, `TSelected`\> +[`QueuedValueSignal`](../type-aliases/QueuedValueSignal.md)\<`TValue`, `TSelected`\> ### Example diff --git a/docs/framework/angular/reference/index.md b/docs/framework/angular/reference/index.md index 93045bc57..74f656e84 100644 --- a/docs/framework/angular/reference/index.md +++ b/docs/framework/angular/reference/index.md @@ -34,6 +34,7 @@ title: "@tanstack/angular-pacer" ## Type Aliases - [AngularPacerOptions](type-aliases/AngularPacerOptions.md) +- [QueuedValueSignal](type-aliases/QueuedValueSignal.md) ## Functions diff --git a/docs/framework/angular/reference/type-aliases/QueuedValueSignal.md b/docs/framework/angular/reference/type-aliases/QueuedValueSignal.md new file mode 100644 index 000000000..1621461d1 --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/QueuedValueSignal.md @@ -0,0 +1,37 @@ +--- +id: QueuedValueSignal +title: QueuedValueSignal +--- + +```ts +type QueuedValueSignal = Signal & object; +``` + +Defined in: [queuer/injectQueuedValue.ts:9](https://github.com/TanStack/pacer/blob/main/packages/angular-pacer/src/queuer/injectQueuedValue.ts#L9) + +A processed value signal with methods for adding and controlling queued values. + +## Type Declaration + +### addItem + +```ts +addItem: AngularQueuer["addItem"]; +``` + +### queuer + +```ts +queuer: AngularQueuer; +``` + +## Type Parameters + +### TValue + +`TValue` + +### TSelected + +`TSelected` = \{ +\} diff --git a/examples/angular/injectQueuedValue/src/app/app.html b/examples/angular/injectQueuedValue/src/app/app.html index ad483d978..b445f9c8f 100644 --- a/examples/angular/injectQueuedValue/src/app/app.html +++ b/examples/angular/injectQueuedValue/src/app/app.html @@ -4,7 +4,7 @@

injectQueuedValue

Source (instant): {{ source() }}

Value (queued): {{ queued() }}

-

Queue length: {{ queued().length }}

+

Queue length: {{ queued.queuer.state().items.length }}

diff --git a/examples/angular/injectQueuedValue/src/app/app.ts b/examples/angular/injectQueuedValue/src/app/app.ts index c1e04c46d..81280f403 100644 --- a/examples/angular/injectQueuedValue/src/app/app.ts +++ b/examples/angular/injectQueuedValue/src/app/app.ts @@ -12,7 +12,7 @@ export class App { protected readonly source = signal('') // A queued value: changes are applied in-order, with an optional delay between items. - // `value()` is the current processed value, and `items()` exposes the pending queue. + // `queued()` is the processed value; `queued.queuer.state().items` contains pending values. protected readonly queued = injectQueuedValue(this.source, { wait: 500 }, (state) => ({ items: state.items, })) diff --git a/examples/angular/injectQueuedValue/src/app/inputapp.ts b/examples/angular/injectQueuedValue/src/app/inputapp.ts index 978d1cb7f..d17b52bcb 100644 --- a/examples/angular/injectQueuedValue/src/app/inputapp.ts +++ b/examples/angular/injectQueuedValue/src/app/inputapp.ts @@ -5,17 +5,17 @@ import { injectQueuedValue } from '@tanstack/angular-pacer' selector: 'app-input', standalone: true, template: ` -

NG0950

+

Required input

value: {{ value() }}

Value (queued): {{ queued() }}

-

Queue length: {{ queued().length }}

+

Queue length: {{ queued.queuer.state().items.length }}

`, }) export class InputApp { readonly value = input.required() - protected readonly queued = injectQueuedValue(this.value, null, { wait: 500 }, (state) => ({ + protected readonly queued = injectQueuedValue(this.value, { wait: 500 }, (state) => ({ items: state.items, })) diff --git a/packages/angular-pacer/src/queuer/injectQueuedValue.ts b/packages/angular-pacer/src/queuer/injectQueuedValue.ts index 7f938b77b..5b45cc529 100644 --- a/packages/angular-pacer/src/queuer/injectQueuedValue.ts +++ b/packages/angular-pacer/src/queuer/injectQueuedValue.ts @@ -1,9 +1,17 @@ -import { effect, linkedSignal, signal } from '@angular/core' +import { computed, effect, signal, untracked } from '@angular/core' import { injectQueuedSignal } from './injectQueuedSignal' import type { AngularPacerOptions } from '../types' -import type { QueuedSignal } from './injectQueuedSignal' +import type { AngularQueuer, AngularQueuerOptions } from './injectQueuer' import type { Signal } from '@angular/core' -import type { QueuerOptions, QueuerState } from '@tanstack/pacer/queuer' +import type { QueuerState } from '@tanstack/pacer/queuer' + +/** A processed value signal with methods for adding and controlling queued values. */ +export type QueuedValueSignal = Signal & { + addItem: AngularQueuer['addItem'] + queuer: AngularQueuer +} + +type PrimitiveInitialValue = string | number | boolean | bigint | symbol | null /** * An Angular function that creates a queued value that processes state changes in order with an optional delay. @@ -13,9 +21,17 @@ import type { QueuerOptions, QueuerState } from '@tanstack/pacer/queuer' * processing each change. This is useful for handling state updates that need to be processed * in a specific order, like animations or sequential UI updates. * - * The function returns a tuple containing: - * - A Signal that provides the current queued value - * - The queuer instance with control methods + * The returned signal provides the most recently processed value. Before the queue processes + * an item, it provides the explicit initial value or the source's initial value. + * Source reads are deferred until the signal is read or Angular runs its effect, so required + * inputs can be bound before their values are read. + * + * Use `queued.addItem(...)` to add a value and `queued.queuer` to control the queue. + * Pending items are available through `queued.queuer.state().items` with the default selector. + * + * Pass an options object as the third argument to supply an object or function as the initial + * value. When those options are undefined or a factory, pass a fourth selector argument + * (undefined is allowed) to distinguish the initial-value form from options plus a selector. * * @example * ```ts @@ -37,9 +53,21 @@ export function injectQueuedValue< >, >( value: Signal, - options?: AngularPacerOptions>, + options?: AngularPacerOptions>, + selector?: (state: QueuerState) => TSelected, +): QueuedValueSignal +export function injectQueuedValue< + TValue, + TSelected extends Pick, 'items'> = Pick< + QueuerState, + 'items' + >, +>( + value: Signal, + initialValue: Extract, + options?: AngularQueuerOptions, selector?: (state: QueuerState) => TSelected, -): QueuedSignal +): QueuedValueSignal export function injectQueuedValue< TValue, TSelected extends Pick, 'items'> = Pick< @@ -49,9 +77,9 @@ export function injectQueuedValue< >( value: Signal, initialValue: TValue, - options?: QueuerOptions, + options: AngularQueuerOptions, selector?: (state: QueuerState) => TSelected, -): QueuedSignal +): QueuedValueSignal export function injectQueuedValue< TValue, TSelected extends Pick, 'items'> = Pick< @@ -61,9 +89,10 @@ export function injectQueuedValue< >( value: Signal, initialValue: TValue, - options: () => QueuerOptions, + options: + AngularPacerOptions> | undefined, selector: ((state: QueuerState) => TSelected) | undefined, -): QueuedSignal +): QueuedValueSignal export function injectQueuedValue< TValue, TSelected extends Pick, 'items'> = Pick< @@ -72,42 +101,62 @@ export function injectQueuedValue< >, >( value: Signal, - initialValueOrOptions?: TValue | AngularPacerOptions>, + initialValueOrOptions?: + TValue | AngularPacerOptions>, initialOptionsOrSelector?: - | AngularPacerOptions> + | AngularPacerOptions> | ((state: QueuerState) => TSelected), maybeSelector?: (state: QueuerState) => TSelected, -): QueuedSignal { - const hasSelector = typeof initialOptionsOrSelector === 'function' +): QueuedValueSignal { + const hasPrimitiveInitialValue = + initialValueOrOptions === null || + (initialValueOrOptions !== undefined && + typeof initialValueOrOptions !== 'object' && + typeof initialValueOrOptions !== 'function') const hasInitialValue = - (initialOptionsOrSelector !== undefined && !hasSelector) || + hasPrimitiveInitialValue || + (initialOptionsOrSelector !== undefined && + typeof initialOptionsOrSelector !== 'function') || arguments.length >= 4 - - const initialValue = hasInitialValue - ? (initialValueOrOptions as TValue) - : value() const initialOptions = hasInitialValue - ? (initialOptionsOrSelector as AngularPacerOptions>) - : (initialValueOrOptions as AngularPacerOptions>) + ? (initialOptionsOrSelector as AngularPacerOptions< + AngularQueuerOptions + >) + : (initialValueOrOptions as AngularPacerOptions< + AngularQueuerOptions + >) const selector = hasInitialValue ? maybeSelector : (initialOptionsOrSelector as ((state: QueuerState) => TSelected) | undefined) - const linkedValue = linkedSignal(() => value()) - const queuedValue = signal(initialValue) + // Wrap values so an explicit undefined remains distinct from uninitialized state. + const processed = signal<{ value: TValue } | undefined>( + hasInitialValue ? { value: initialValueOrOptions as TValue } : undefined, + ) + const queuedValue = computed(() => { + const current = processed() + return current ? current.value : value() + }) const queued = injectQueuedSignal( (item) => { - queuedValue.set(item) + processed.set({ value: item }) }, initialOptions, selector, ) effect(() => { - queued.addItem(linkedValue()) + const nextValue = value() + untracked(() => { + if (!processed()) processed.set({ value: nextValue }) + queued.addItem(nextValue) + }) }) - return queued + return Object.assign(queuedValue, { + addItem: queued.addItem, + queuer: queued.queuer, + }) } diff --git a/packages/angular-pacer/tests/options.test.ts b/packages/angular-pacer/tests/options.test.ts index 388365836..fc2b9c9ad 100644 --- a/packages/angular-pacer/tests/options.test.ts +++ b/packages/angular-pacer/tests/options.test.ts @@ -49,6 +49,32 @@ function checkOptionOverloads() { undefined, ) injectQueuedValue(source, 'initial', () => ({ wait: 100 }), undefined) + expectTypeOf(injectQueuedValue(source)()).toEqualTypeOf() + const objectSource = signal({ label: 'source' }) + const objectInitial = { label: 'fallback' } + const functionInitial = () => 'fallback' + const functionSource = signal(() => 'source') + injectQueuedValue(objectSource, objectInitial, {}) + injectQueuedValue(objectSource, objectInitial, undefined, undefined) + injectQueuedValue( + objectSource, + objectInitial, + () => ({ wait: 100 }), + undefined, + ) + injectQueuedValue(functionSource, functionInitial, {}) + injectQueuedValue(functionSource, functionInitial, undefined, undefined) + injectQueuedValue(source, 'fallback') + injectQueuedValue(source, 'fallback', undefined) + injectQueuedValue(source, { wait: 100 }, undefined) + // @ts-expect-error Object fallbacks require explicit options to distinguish them from options-only calls. + injectQueuedValue(objectSource, objectInitial) + // @ts-expect-error Undefined third arguments are reserved for options plus an undefined selector. + injectQueuedValue(objectSource, objectInitial, undefined) + // @ts-expect-error Function fallbacks require explicit options to distinguish them from factories. + injectQueuedValue(functionSource, functionInitial) + // @ts-expect-error Undefined options for a function fallback require a fourth argument. + injectQueuedValue(functionSource, functionInitial, undefined) // @ts-expect-error A fourth argument distinguishes a factory from a selector. injectDebouncedValue(source, 'initial', () => ({ wait: 100 })) // @ts-expect-error A fourth argument distinguishes a factory from a selector. diff --git a/packages/angular-pacer/tests/queued-value.test.ts b/packages/angular-pacer/tests/queued-value.test.ts new file mode 100644 index 000000000..b3d6dc7b2 --- /dev/null +++ b/packages/angular-pacer/tests/queued-value.test.ts @@ -0,0 +1,201 @@ +import '@angular/compiler' +import { Component, Input, input, isSignal, signal } from '@angular/core' +import { TestBed } from '@angular/core/testing' +import { + BrowserTestingModule, + platformBrowserTesting, +} from '@angular/platform-browser/testing' +import { afterEach, beforeAll, beforeEach, expect, it, vi } from 'vitest' +import { injectQueuedValue } from '../src/queuer/injectQueuedValue' + +beforeAll(() => + TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting()), +) +beforeEach(() => vi.useFakeTimers()) +afterEach(() => { + TestBed.resetTestingModule() + vi.useRealTimers() +}) + +it('returns the processed scalar while retaining pending queue items', () => { + const source = signal('initial') + const queued = TestBed.runInInjectionContext(() => + injectQueuedValue(source, { started: false }), + ) + TestBed.tick() + expect(isSignal(queued)).toBe(true) + expect(queued()).toBe('initial') + source.set('next') + TestBed.tick() + expect(queued()).toBe('initial') + expect(queued.queuer.peekAllItems()).toEqual(['initial', 'next']) + queued.queuer.flush() + expect(queued()).toBe('next') +}) + +it('constructs before required source inputs are bound', () => { + class RequiredSource { + source = input.required() + queued = injectQueuedValue(this.source, { started: false }) + } + Input({ required: true, isSignal: true } as Parameters[0])( + RequiredSource.prototype, + 'source', + ) + Component({ standalone: true, template: '' })(RequiredSource) + const fixture = TestBed.createComponent(RequiredSource) + expect(() => fixture.componentInstance.queued()).toThrow(/NG0950/) + fixture.componentRef.setInput('source', 'bound') + fixture.detectChanges() + expect(fixture.componentInstance.queued()).toBe('bound') +}) + +it('recognizes a two-argument primitive initial value without reading the source', () => { + const queued = TestBed.runInInjectionContext(() => + injectQueuedValue(input.required(), 'fallback'), + ) + expect(queued()).toBe('fallback') +}) + +it.each([0, false, '', null, 0n, Symbol('initial')])( + 'preserves the primitive fallback %s', + (initial) => { + const queued = TestBed.runInInjectionContext(() => + injectQueuedValue(input.required(), initial), + ) + expect(queued()).toBe(initial) + }, +) + +it('supports object and function initial values with explicit options', () => { + const initialObject = { label: 'fallback' } + const initialFunction = vi.fn(() => 'fallback') + const [objectValue, functionValue] = TestBed.runInInjectionContext(() => [ + injectQueuedValue( + input.required(), + initialObject, + {}, + ), + injectQueuedValue( + input.required(), + initialFunction, + {}, + ), + ]) + expect(objectValue()).toBe(initialObject) + expect(functionValue()).toBe(initialFunction) + expect(initialFunction).not.toHaveBeenCalled() +}) + +it('distinguishes options with an undefined selector from explicit initial values', () => { + const source = signal({ label: 'source' }) + const fallback = { label: 'fallback' } + const [optionsOnly, explicitFallback, explicitUndefined] = + TestBed.runInInjectionContext(() => [ + injectQueuedValue(source, { started: false }, undefined), + injectQueuedValue(source, fallback, undefined, undefined), + injectQueuedValue( + signal('source'), + undefined, + {}, + undefined, + ), + ]) + expect(optionsOnly()).toBe(source()) + expect(optionsOnly.queuer.options.started).toBe(false) + expect(explicitFallback()).toBe(fallback) + expect(explicitUndefined()).toBeUndefined() +}) + +it('preserves selectors and processes manual values in queue order', () => { + const source = signal('source') + const queued = TestBed.runInInjectionContext(() => + injectQueuedValue( + source, + 'fallback', + { started: false, wait: 100 }, + (state) => ({ items: state.items, size: state.size }), + ), + ) + expect(queued()).toBe('fallback') + TestBed.tick() + queued.addItem('manual') + expect(queued.queuer.state().size).toBe(2) + queued.queuer.start() + expect(queued()).toBe('source') + vi.advanceTimersByTime(99) + expect(queued()).toBe('source') + vi.advanceTimersByTime(1) + expect(queued()).toBe('manual') + expect(queued.queuer.state().items).toEqual([]) +}) + +it('observes only source changes when enqueuing values', () => { + const source = signal('initial'), + wait = signal(100) + const queued = TestBed.runInInjectionContext(() => + injectQueuedValue(source, () => ({ wait: wait(), started: false })), + ) + TestBed.tick() + expect(queued.queuer.peekAllItems()).toEqual(['initial']) + wait.set(200) + TestBed.tick() + queued.queuer.execute() + TestBed.tick() + expect(queued.queuer.peekAllItems()).toEqual([]) + source.set('intermediate') + source.set('latest') + TestBed.tick() + expect(queued.queuer.peekAllItems()).toEqual(['latest']) + expect(queued()).toBe('initial') + queued.queuer.execute() + expect(queued()).toBe('latest') +}) + +it('supports required source and option inputs across the supported forms', () => { + class RequiredInputs { + source = input.required() + wait = input.required() + defaultOptions = injectQueuedValue(this.source) + factoryOptions = injectQueuedValue(this.source, () => ({ + wait: this.wait(), + started: false, + })) + explicitFactory = injectQueuedValue( + this.source, + 'fallback', + () => ({ wait: this.wait(), started: false }), + undefined, + ) + } + for (const property of ['source', 'wait']) { + Input({ required: true, isSignal: true } as Parameters[0])( + RequiredInputs.prototype, + property, + ) + } + Component({ standalone: true, template: '' })(RequiredInputs) + const fixture = TestBed.createComponent(RequiredInputs) + expect(fixture.componentInstance.explicitFactory()).toBe('fallback') + fixture.componentRef.setInput('source', 'bound') + fixture.componentRef.setInput('wait', 100) + fixture.detectChanges() + expect(fixture.componentInstance.defaultOptions()).toBe('bound') + expect(fixture.componentInstance.factoryOptions()).toBe('bound') + expect(fixture.componentInstance.explicitFactory()).toBe('fallback') + fixture.componentInstance.explicitFactory.queuer.flush() + expect(fixture.componentInstance.explicitFactory()).toBe('bound') +}) + +it('stops pending work when its injection context is destroyed', () => { + const processed = vi.fn() + const queued = TestBed.runInInjectionContext(() => + injectQueuedValue(signal('source'), { wait: 100, onExecute: processed }), + ) + TestBed.tick() + queued.addItem('later') + TestBed.resetTestingModule() + vi.advanceTimersByTime(100) + expect(queued()).toBe('source') + expect(processed).toHaveBeenCalledOnce() +})