diff --git a/packages/atoms/src/classes/Signal.ts b/packages/atoms/src/classes/Signal.ts index 32a168f6..a79be3f8 100644 --- a/packages/atoms/src/classes/Signal.ts +++ b/packages/atoms/src/classes/Signal.ts @@ -80,13 +80,11 @@ export const doMutate = ( const transactions: Transaction[] = [] - if (isMutatable(newState)) { + if (Array.isArray(newState)) { + transactions.push({ k: [], v: newState }) + } else if (isMutatable(newState)) { const empty = ( - newState instanceof Set - ? new Set() - : Array.isArray(newState) - ? [] - : {} + newState instanceof Set ? new Set() : {} ) as G['State'] recursivelyMutate( @@ -138,7 +136,10 @@ export const doMutate = ( // applied as mutations; non-mutatable values (primitives, class instances, // etc.) are set as the entire new state. if (!transactions.length && result !== undefined) { - if (isMutatable(result)) { + if (Array.isArray(result)) { + newState = result + transactions.push({ k: [], v: result }) + } else if (isMutatable(result)) { recursivelyMutate(proxyWrapper.p, result) } else { node.set(result as Settable, { @@ -149,6 +150,9 @@ export const doMutate = ( return } } + } else if (Array.isArray(mutatable)) { + newState = mutatable as G['State'] + transactions.push({ k: [], v: mutatable }) } else { recursivelyMutate(proxyWrapper.p, mutatable) } diff --git a/packages/react/test/integrations/__snapshots__/proxies.test.tsx.snap b/packages/react/test/integrations/__snapshots__/proxies.test.tsx.snap index 15b8acce..afcf6ced 100644 --- a/packages/react/test/integrations/__snapshots__/proxies.test.tsx.snap +++ b/packages/react/test/integrations/__snapshots__/proxies.test.tsx.snap @@ -5,13 +5,14 @@ exports[`proxies array operations 1`] = ` [ [ { - "k": [ - "0", - "a", - ], + "k": [], "v": [ - 11, - 33, + { + "a": [ + 11, + 33, + ], + }, ], }, ], diff --git a/packages/react/test/integrations/proxies.test.tsx b/packages/react/test/integrations/proxies.test.tsx index 56458a20..d49bb22c 100644 --- a/packages/react/test/integrations/proxies.test.tsx +++ b/packages/react/test/integrations/proxies.test.tsx @@ -1,4 +1,11 @@ -import { api, As, atom, injectSignal, Transaction } from '@zedux/atoms' +import { + api, + As, + atom, + injectMappedSignal, + injectSignal, + Transaction, +} from '@zedux/atoms' import { ecosystem } from '../utils/ecosystem' describe('proxies', () => { @@ -317,7 +324,21 @@ describe('proxies', () => { state[3] = 40 }) - expect(signal.get()).toEqual([2, 3, 30, 40, 5]) + expect(signal.get()).toEqual([2, 3, 30, 40]) + }) + + test('function-form returning an array generates a single transaction', () => { + const calls: any[] = [] + const signal = ecosystem.signal([1, 2, 3]) + + signal.on('mutate', transactions => { + calls.push(transactions) + }) + + signal.mutate(draft => [draft[0] + 10, draft[1] + 10]) + + expect(signal.get()).toEqual([11, 12]) + expect(calls).toEqual([[{ k: [], v: [11, 12] }]]) }) describe('recursivelyMutate optimizations', () => { @@ -382,6 +403,44 @@ describe('proxies', () => { expect(signal.get()).toEqual({ arr: [11, 21] }) expect(calls).toEqual([[{ k: 'arr', v: [11, 21] }]]) }) + + test('top-level array replaces instead of deep merging', () => { + const calls: any[] = [] + const signal = ecosystem.signal([{ a: 1, b: 2 }, { c: 3 }]) + + signal.on('mutate', transactions => { + calls.push(transactions) + }) + + signal.mutate([{ a: 5 }]) + + // should replace entire array, not deep merge { a: 5 } into { a: 1, b: 2 } + expect(signal.get()).toEqual([{ a: 5 }]) + expect(calls).toEqual([[{ k: [], v: [{ a: 5 }] }]]) + }) + }) + + test('mapped signal prefixes array replacement transaction with inner key', () => { + const testAtom = atom('mappedArr', () => { + const arrSignal = injectSignal([1, 2, 3]) + const signal = injectMappedSignal({ arr: arrSignal }) + + return api(signal).setExports({ arrSignal }) + }) + + const node = ecosystem.getNode(testAtom) + const calls: any[] = [] + + node.on('mutate', transactions => { + calls.push(transactions) + }) + + node.exports.arrSignal.mutate([10, 20]) + + expect(node.get()).toEqual({ arr: [10, 20] }) + expect(calls).toEqual([[{ k: ['arr'], v: [10, 20] }]]) + + ecosystem.dehydrate({ exclude: [testAtom] }) }) describe('non-plain object handling', () => { diff --git a/packages/react/test/integrations/signals.test.tsx b/packages/react/test/integrations/signals.test.tsx index 31c1fff0..546838d1 100644 --- a/packages/react/test/integrations/signals.test.tsx +++ b/packages/react/test/integrations/signals.test.tsx @@ -528,7 +528,7 @@ describe('signals', () => { ]) }) - test('mutate generates add transactions for each element of an array', () => { + test('mutate replaces array state directly with a single transaction', () => { const signal = ecosystem.signal(null) let transactions: Transaction[] | undefined @@ -538,10 +538,8 @@ describe('signals', () => { signal.mutate(['a', 'b']) - expect(transactions).toEqual([ - { k: '0', v: 'a' }, - { k: '1', v: 'b' }, - ]) + expect(transactions).toEqual([{ k: [], v: ['a', 'b'] }]) + expect(signal.get()).toEqual(['a', 'b']) }) test('mutate generates add transactions for each item in a set', () => {