From 1fad52994d7e3dd860a9f9ffb6bd9fcfae803f4d Mon Sep 17 00:00:00 2001 From: dangreen Date: Sun, 18 Oct 2026 16:56:25 +0400 Subject: [PATCH] feat(kida,store): move `latest` from `kida` to `@nano_kit/store` `latest` now sits in `@nano_kit/store` next to `previous`, its pair, and the store exports it as before. `kida` no longer exports it, and neither does `nanoviews/store`, which re-exports `kida`: the core keeps what views and the core itself use. No stable `kida` had `latest`, so only the 2.0.0 prereleases lose it. --- packages/kida/.size-limit.js | 4 +- packages/kida/src/utils.spec.ts | 114 ------------------------------- packages/kida/src/utils.ts | 33 --------- packages/store/src/utils.spec.ts | 100 +++++++++++++++++++++++++++ packages/store/src/utils.ts | 33 +++++++++ 5 files changed, 135 insertions(+), 149 deletions(-) delete mode 100644 packages/kida/src/utils.spec.ts diff --git a/packages/kida/.size-limit.js b/packages/kida/.size-limit.js index 87871ed4..5e4ee18f 100644 --- a/packages/kida/.size-limit.js +++ b/packages/kida/.size-limit.js @@ -15,14 +15,14 @@ export default [ gzip: true, path: 'dist/index.production.js', import: '*', - limit: '5.5 kB', + limit: '5.45 kB', modifyRolldownConfig }, { name: 'All publics (Brotli)', path: 'dist/index.production.js', import: '*', - limit: '5.1 kB', + limit: '5.05 kB', modifyRolldownConfig }, { diff --git a/packages/kida/src/utils.spec.ts b/packages/kida/src/utils.spec.ts deleted file mode 100644 index c4b7f1c4..00000000 --- a/packages/kida/src/utils.spec.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { - vi, - describe, - it, - expect -} from 'vitest' -import { - signal, - effect, - batch -} from 'agera' -import { latest } from './utils.js' - -describe('kida', () => { - describe('utils', () => { - describe('latest', () => { - it('should return the last source value on the first read', () => { - const $a = signal('a') - const $b = signal('b') - const $latest = latest($a, $b) - - expect($latest()).toBe('b') - }) - - it('should return the last source value on the first read when it is undefined', () => { - const $a = signal('a') - const $b = signal(undefined) - const $latest = latest($a, $b) - - expect($latest()).toBeUndefined() - }) - - it('should return the value of the source that changed last', () => { - const $a = signal('a') - const $b = signal('b') - const $latest = latest($a, $b) - const listener = vi.fn() - const off = effect(() => { - listener($latest()) - }) - - $a('a1') - $b('b1') - $a('a2') - - expect(listener.mock.calls).toEqual([['b'], ['a1'], ['b1'], ['a2']]) - - off() - }) - - it('should return the last changed source when several sources changed in a batch', () => { - const $a = signal('a') - const $b = signal('b') - const $latest = latest($a, $b) - const off = effect(() => { - $latest() - }) - - batch(() => { - $b('b1') - $a('a1') - }) - - expect($latest()).toBe('b1') - - off() - }) - - it('should return the last changed source when several sources changed between reads', () => { - const $a = signal('a') - const $b = signal('b') - const $latest = latest($a, $b) - - expect($latest()).toBe('b') - - $b('b1') - $a('a1') - - expect($latest()).toBe('b1') - }) - - it('should keep the value when a source is written with an equal value', () => { - const $a = signal('a') - const $b = signal('b') - const $latest = latest($a, $b) - - expect($latest()).toBe('b') - - $a('a') - - expect($latest()).toBe('b') - }) - - it('should keep the value when nothing changed since the last read', () => { - const $a = signal('a') - const $b = signal('b') - const $latest = latest($a, $b) - const off = effect(() => { - $latest() - }) - - $a('a1') - - expect($latest()).toBe('a1') - - // Losing the last subscriber purges the deps and forces the next read - // to recompute with no source changed - off() - - expect($latest()).toBe('a1') - }) - }) - }) -}) diff --git a/packages/kida/src/utils.ts b/packages/kida/src/utils.ts index 1964a7f0..9a6ffe80 100644 --- a/packages/kida/src/utils.ts +++ b/packages/kida/src/utils.ts @@ -39,39 +39,6 @@ export function boolean($signal: Accessor) { return computed(() => Boolean($signal())) } -/** - * Create a signal with the value of the source that changed last. - * A change is a change of value: writing a value equal to the current one - * changes nothing. The last source wins on the first read and whenever - * several sources changed since the previous read, which is what reading - * without a subscription or writing inside a `batch` amounts to. - * @param sources - Sources to merge. - * @returns A signal with the value of the most recently changed source. - */ -/* @__NO_SIDE_EFFECTS__ */ -export function latest(...sources: Accessor[]) { - const prev: T[] = [] - let value: T - - return computed(() => { - // The first run has nothing to compare against, so every source counts as - // changed and the last one wins - const init = !prev.length - - // Every source is read on every run: skipping one drops its subscription - for (let i = 0, len = sources.length; i < len; i++) { - const next = sources[i]() - - if (init || next !== prev[i]) { - prev[i] = next - value = next - } - } - - return value - }) -} - /** * Compose multiple destroy functions into a single destroy function. * @param destroys - Destroy functions to compose. diff --git a/packages/store/src/utils.spec.ts b/packages/store/src/utils.spec.ts index 0d0ac463..d988a07c 100644 --- a/packages/store/src/utils.spec.ts +++ b/packages/store/src/utils.spec.ts @@ -7,11 +7,13 @@ import { expect } from 'vitest' import { + batch, effect, signal } from 'kida' import { interval, + latest, previous, signalNodeAssign } from './utils.js' @@ -54,6 +56,104 @@ describe('store', () => { }) }) + describe('latest', () => { + it('should return the last source value on the first read', () => { + const $a = signal('a') + const $b = signal('b') + const $latest = latest($a, $b) + + expect($latest()).toBe('b') + }) + + it('should return the last source value on the first read when it is undefined', () => { + const $a = signal('a') + const $b = signal(undefined) + const $latest = latest($a, $b) + + expect($latest()).toBeUndefined() + }) + + it('should return the value of the source that changed last', () => { + const $a = signal('a') + const $b = signal('b') + const $latest = latest($a, $b) + const listener = vi.fn() + const off = effect(() => { + listener($latest()) + }) + + $a('a1') + $b('b1') + $a('a2') + + expect(listener.mock.calls).toEqual([['b'], ['a1'], ['b1'], ['a2']]) + + off() + }) + + it('should return the last changed source when several sources changed in a batch', () => { + const $a = signal('a') + const $b = signal('b') + const $latest = latest($a, $b) + const off = effect(() => { + $latest() + }) + + batch(() => { + $b('b1') + $a('a1') + }) + + expect($latest()).toBe('b1') + + off() + }) + + it('should return the last changed source when several sources changed between reads', () => { + const $a = signal('a') + const $b = signal('b') + const $latest = latest($a, $b) + + expect($latest()).toBe('b') + + $b('b1') + $a('a1') + + expect($latest()).toBe('b1') + }) + + it('should keep the value when a source is written with an equal value', () => { + const $a = signal('a') + const $b = signal('b') + const $latest = latest($a, $b) + + expect($latest()).toBe('b') + + $a('a') + + expect($latest()).toBe('b') + }) + + it('should keep the value when nothing changed since the last read', () => { + const $a = signal('a') + const $b = signal('b') + const $latest = latest($a, $b) + const off = effect(() => { + $latest() + }) + + $a('a1') + + expect($latest()).toBe('a1') + + // Losing the last subscriber purges the deps and forces the next read + // to recompute with no source changed + off() + + expect($latest()).toBe('a1') + }) + }) + describe('interval', () => { beforeEach(() => { vi.useFakeTimers() diff --git a/packages/store/src/utils.ts b/packages/store/src/utils.ts index 37395d17..dccf68db 100644 --- a/packages/store/src/utils.ts +++ b/packages/store/src/utils.ts @@ -28,6 +28,39 @@ export function previous($signal: Accessor): Accessor { }) } +/** + * Create a signal with the value of the source that changed last. + * A change is a change of value: writing a value equal to the current one + * changes nothing. The last source wins on the first read and whenever + * several sources changed since the previous read, which is what reading + * without a subscription or writing inside a `batch` amounts to. + * @param sources - Sources to merge. + * @returns A signal with the value of the most recently changed source. + */ +/* @__NO_SIDE_EFFECTS__ */ +export function latest(...sources: Accessor[]) { + const prev: T[] = [] + let value: T + + return computed(() => { + // The first run has nothing to compare against, so every source counts as + // changed and the last one wins + const init = !prev.length + + // Every source is read on every run: skipping one drops its subscription + for (let i = 0, len = sources.length; i < len; i++) { + const next = sources[i]() + + if (init || next !== prev[i]) { + prev[i] = next + value = next + } + } + + return value + }) +} + /** * Create a signal that increments on an interval while mounted. * @param ms - Interval duration in milliseconds.