From 01988361fd64b3b10977a47f3ee6be2c5fed7341 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 8 Dec 2025 18:18:03 +0100 Subject: [PATCH 1/2] feat(llc): Add `insertAt` to `upsert` for controlling insertion position --- packages/stream_core/CHANGELOG.md | 6 + .../lib/src/utils/list_extensions.dart | 21 +++- .../test/query/list_extensions_test.dart | 116 ++++++++++++++++++ 3 files changed, 140 insertions(+), 3 deletions(-) diff --git a/packages/stream_core/CHANGELOG.md b/packages/stream_core/CHANGELOG.md index 07bd3b2..1caf04d 100644 --- a/packages/stream_core/CHANGELOG.md +++ b/packages/stream_core/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +### ✨ Features + +- Added `insertAt` parameter to `upsert` for controlling insertion position of new elements + ## 0.3.1 ### ✨ Features diff --git a/packages/stream_core/lib/src/utils/list_extensions.dart b/packages/stream_core/lib/src/utils/list_extensions.dart index 3fee5ce..98546f8 100644 --- a/packages/stream_core/lib/src/utils/list_extensions.dart +++ b/packages/stream_core/lib/src/utils/list_extensions.dart @@ -53,7 +53,8 @@ extension ListExtensions on List { /// Inserts or replaces an element in the list based on a key. /// /// If an element with the same key already exists, it will be replaced. - /// Otherwise, the new element will be appended to the end of the list. + /// Otherwise, the new element will be inserted at the position determined by + /// [insertAt] (defaults to appending at the end). /// Time complexity: O(n) for search, O(n) for list creation. /// /// ```dart @@ -64,23 +65,37 @@ extension ListExtensions on List { /// ); /// // Result: [User(id: '1', name: 'Alice Updated'), User(id: '2', name: 'Bob')] /// - /// // Adding new element + /// // Adding new element (appends to end) /// final withNew = users.upsert( /// User(id: '3', name: 'Charlie'), /// key: (user) => user.id, /// ); /// // Result: [User(id: '1', name: 'Alice'), User(id: '2', name: 'Bob'), User(id: '3', name: 'Charlie')] + /// + /// // Insert at specific position + /// final inserted = users.upsert( + /// User(id: '3', name: 'Charlie'), + /// key: (user) => user.id, + /// insertAt: (list) => 0, // Insert at beginning + /// ); + /// // Result: [User(id: '3', name: 'Charlie'), User(id: '1', name: 'Alice'), User(id: '2', name: 'Bob')] /// ``` List upsert( T element, { required K Function(T item) key, + int Function(List list)? insertAt, T Function(T original, T updated)? update, }) { final elementKey = key(element); final index = indexWhere((e) => key(e) == elementKey); // Add the element if it does not exist - if (index == -1) return [...this, element]; + if (index == -1) { + final insertionIndex = insertAt?.call(this) ?? length; + // Clamp index to valid range [0, length] + final validIndex = insertionIndex.clamp(0, length); + return [...this].apply((it) => it.insert(validIndex, element)); + } T handleUpdate(T original, T updated) { if (update != null) return update(original, updated); diff --git a/packages/stream_core/test/query/list_extensions_test.dart b/packages/stream_core/test/query/list_extensions_test.dart index da401c5..1133bd6 100644 --- a/packages/stream_core/test/query/list_extensions_test.dart +++ b/packages/stream_core/test/query/list_extensions_test.dart @@ -135,6 +135,122 @@ void main() { expect(result.first.content, 'Hello Updated'); expect(result.last.content, 'World'); }); + + test('should insert at beginning when insertAt returns 0', () { + final users = [ + const _TestUser(id: '1', name: 'Alice'), + const _TestUser(id: '2', name: 'Bob'), + ]; + + final result = users.upsert( + const _TestUser(id: '3', name: 'Charlie'), + key: (user) => user.id, + insertAt: (list) => 0, + ); + + expect(result.length, 3); + expect(result[0].name, 'Charlie'); + expect(result[1].name, 'Alice'); + expect(result[2].name, 'Bob'); + }); + + test('should insert at middle position when insertAt specifies', () { + final users = [ + const _TestUser(id: '1', name: 'Alice'), + const _TestUser(id: '2', name: 'Bob'), + const _TestUser(id: '3', name: 'Charlie'), + ]; + + final result = users.upsert( + const _TestUser(id: '4', name: 'David'), + key: (user) => user.id, + insertAt: (list) => 1, + ); + + expect(result.length, 4); + expect(result[0].name, 'Alice'); + expect(result[1].name, 'David'); + expect(result[2].name, 'Bob'); + expect(result[3].name, 'Charlie'); + }); + + test('should clamp insertAt index to valid range', () { + final users = [ + const _TestUser(id: '1', name: 'Alice'), + const _TestUser(id: '2', name: 'Bob'), + ]; + + // Index too large + final resultLarge = users.upsert( + const _TestUser(id: '3', name: 'Charlie'), + key: (user) => user.id, + insertAt: (list) => 100, // Way beyond list length + ); + + expect(resultLarge.length, 3); + expect(resultLarge.last.name, 'Charlie'); // Should be clamped to end + + // Negative index + final resultNegative = users.upsert( + const _TestUser(id: '4', name: 'David'), + key: (user) => user.id, + insertAt: (list) => -5, + ); + + expect(resultNegative.length, 3); + expect( + resultNegative.first.name, 'David'); // Should be clamped to start + }); + + test('should use insertAt with list information', () { + final scores = [ + const _TestScore(userId: 1, points: 100), + const _TestScore(userId: 2, points: 200), + const _TestScore(userId: 3, points: 150), + ]; + + // Insert at position based on list length + final result = scores.upsert( + const _TestScore(userId: 4, points: 175), + key: (score) => score.userId, + insertAt: (list) => list.length ~/ 2, // Insert at middle + ); + + expect(result.length, 4); + expect(result[1].userId, 4); // Inserted at index 1 (3 ~/ 2 = 1) + }); + + test('should not use insertAt when replacing existing element', () { + final users = [ + const _TestUser(id: '1', name: 'Alice'), + const _TestUser(id: '2', name: 'Bob'), + const _TestUser(id: '3', name: 'Charlie'), + ]; + + final result = users.upsert( + const _TestUser(id: '2', name: 'Bob Updated'), + key: (user) => user.id, + insertAt: (list) => 0, // Should be ignored when replacing + ); + + expect(result.length, 3); + expect(result[0].name, 'Alice'); + expect(result[1].name, 'Bob Updated'); // Replaced in place + expect(result[2].name, 'Charlie'); + }); + + test('should work with insertAt on empty list', () { + final users = <_TestUser>[]; + + final result = users.upsert( + const _TestUser(id: '1', name: 'Alice'), + key: (user) => user.id, + insertAt: (list) => 0, + ); + + expect(result.length, 1); + expect(result.first.name, 'Alice'); + }); }); group('updateWhere', () { From 3d3d40d337d27827b695648d5cb9d45e0fcedcdf Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 9 Dec 2025 15:38:34 +0100 Subject: [PATCH 2/2] chore: fix lints --- packages/stream_core/test/query/list_extensions_test.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/stream_core/test/query/list_extensions_test.dart b/packages/stream_core/test/query/list_extensions_test.dart index 1133bd6..3eb01f9 100644 --- a/packages/stream_core/test/query/list_extensions_test.dart +++ b/packages/stream_core/test/query/list_extensions_test.dart @@ -199,7 +199,9 @@ void main() { expect(resultNegative.length, 3); expect( - resultNegative.first.name, 'David'); // Should be clamped to start + resultNegative.first.name, + 'David', + ); // Should be clamped to start }); test('should use insertAt with list information', () {