From 373c60b52173e6ed8bcc01b3c4f1c6ac4734398a Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 24 May 2025 18:28:43 -0400 Subject: [PATCH] add diff-arrays utility for efficient array diffs --- packages/backend/src/misc/diff-arrays.ts | 56 +++++++++++++++++++ .../backend/test/unit/misc/diff-arrays.ts | 53 ++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 packages/backend/src/misc/diff-arrays.ts create mode 100644 packages/backend/test/unit/misc/diff-arrays.ts diff --git a/packages/backend/src/misc/diff-arrays.ts b/packages/backend/src/misc/diff-arrays.ts new file mode 100644 index 0000000000..1f6820481b --- /dev/null +++ b/packages/backend/src/misc/diff-arrays.ts @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export interface DiffResult { + added: T[]; + removed: T[]; +} + +/** + * Calculates the difference between two snapshots of data. + * Null, undefined, and empty arrays are supported, and values do not have to be unique. + * Result sets are de-duplicated, and will be empty if no data was added or removed (respectively). + * @param dataBefore Array containing data before the change + * @param dataAfter Array containing data after the change + */ +export function diffArrays(dataBefore: T[] | null | undefined, dataAfter: T[] | null | undefined): DiffResult { + const before = dataBefore ? new Set(dataBefore) : null; + const after = dataAfter ? new Set(dataAfter) : null; + + // data before AND after => changed + if (before?.size && after?.size) { + const added: T[] = []; + const removed: T[] = []; + + for (const host of before) { + // before and NOT after => removed + if (!after.has(host)) { + removed.push(host); + } + } + + for (const host of after) { + // after and NOT before => added + if (!before.has(host)) { + added.push(host); + } + } + + return { added, removed }; + } + + // data ONLY before => all removed + if (before?.size) { + return { added: [], removed: Array.from(before) }; + } + + // data ONLY after => all added + if (after?.size) { + return { added: Array.from(after), removed: [] }; + } + + // data NEITHER before nor after => no change + return { added: [], removed: [] }; +} diff --git a/packages/backend/test/unit/misc/diff-arrays.ts b/packages/backend/test/unit/misc/diff-arrays.ts new file mode 100644 index 0000000000..3eed5a8913 --- /dev/null +++ b/packages/backend/test/unit/misc/diff-arrays.ts @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { diffArrays } from '@/misc/diff-arrays.js'; + +describe(diffArrays, () => { + it('should return empty result when both inputs are null', () => { + const result = diffArrays(null, null); + expect(result.added).toHaveLength(0); + expect(result.removed).toHaveLength(0); + }); + + it('should return empty result when both inputs are empty', () => { + const result = diffArrays([], []); + expect(result.added).toHaveLength(0); + expect(result.removed).toHaveLength(0); + }); + + it('should remove before when added is empty', () => { + const result = diffArrays([1, 2, 3], []); + expect(result.added).toHaveLength(0); + expect(result.removed).toEqual([1, 2, 3]); + }); + + it('should deduplicate before when added is empty', () => { + const result = diffArrays([1, 1, 2, 2, 3], []); + expect(result.added).toHaveLength(0); + expect(result.removed).toEqual([1, 2, 3]); + }); + + it('should remove after when before is empty', () => { + const result = diffArrays([], [1, 2, 3]); + expect(result.added).toEqual([1, 2, 3]); + expect(result.removed).toHaveLength(0); + }); + + it('should deduplicate after when before is empty', () => { + const result = diffArrays([], [1, 1, 2, 2, 3]); + expect(result.added).toEqual([1, 2, 3]); + expect(result.removed).toHaveLength(0); + }); + + it('should return diff when both have values', () => { + const result = diffArrays( + ['a', 'b', 'c', 'd'], + ['a', 'c', 'e', 'f'], + ); + expect(result.added).toEqual(['e', 'f']); + expect(result.removed).toEqual(['b', 'd']); + }); +});