Skip to content

Commit 9afbc6d

Browse files
Added .collectInstances(upTo:) helper method to collect all instances into an array from a ranged read returning an AsyncInstances
1 parent 4497aa6 commit 9afbc6d

3 files changed

Lines changed: 67 additions & 31 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ for try await book in bookDatastore.load(dimitri.id, from: \.author) {
220220
print("Book written by Dimitri: \(book.title)")
221221
}
222222

223-
let allShelves = try await shelfDatastore.load(...).reduce(into: []) { $0.append($1) }
223+
let allShelves = try await shelfDatastore.load(...).collectInstances(upTo: .infinity)
224224
```
225225

226226
Writing or deleteing is equally as straight-forward:

Sources/CodableDatastore/Datastore/AsyncInstances.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,42 @@ extension AsyncInstances {
1515
public var firstInstance: Element? {
1616
get async throws { try await first { _ in true } }
1717
}
18+
19+
/// Copy and collect all instances in a given load operation into a single array.
20+
///
21+
/// Do not use this method if you need to know if more instances exist past the proposed collection limit, and use a `for try await ... in ...` loop instead to properly handle that case.
22+
///
23+
/// - Warning: This method is only safe to use from sequences vended by a Datastore ranged ``Datastore/load(range:order:)`` operation as they guarantee that the returned sequence won't stall due to unavailable instances. Do not use it when collecting observations as there is no guarantee observations will be returned!
24+
/// - Parameter collectionLimit: The maximum amount of entries to collect. Specify `.infinity` to _questionably_ collect all instances.
25+
/// - Returns: An array of instances up to the collection limit.
26+
public func collectInstances(upTo collectionLimit: Int) async throws -> [Element] {
27+
var instances: [Element] = []
28+
for try await instance in self {
29+
instances.append(instance)
30+
31+
guard instances.count <= collectionLimit
32+
else { return instances }
33+
}
34+
return instances
35+
}
36+
37+
/// Copy and collect all instances in a given load operation into a single array.
38+
///
39+
/// Do not use this method if you need to know if more instances exist past the proposed collection limit, and use a `for try await ... in ...` loop instead to properly handle that case.
40+
///
41+
/// - Warning: This method is only safe to use from sequences vended by a Datastore ranged ``Datastore/load(range:order:)`` operation as they guarantee that the returned sequence won't stall due to unavailable instances. Do not use it when collecting observations as there is no guarantee observations will be returned!
42+
/// - Parameter collectionLimit: The maximum amount of entries to collect. Specify `.infinity` to _questionably_ collect all instances.
43+
/// - Returns: An array of instances up to the collection limit.
44+
public func collectInstances(upTo collectionLimit: AsyncInstancesLimit) async throws -> [Element] {
45+
try await collectInstances(upTo: .max)
46+
}
47+
}
48+
49+
public enum AsyncInstancesLimit {
50+
/// Load all the instances in a given range.
51+
///
52+
/// - Warning: Unless you know in advance that the amount of entries absolutely fits in working memory, collecting all instances is extremently ill-advised.
53+
case infinity
1854
}
1955

2056
// MARK: - Standard Library Conformances

Tests/CodableDatastoreTests/DiskPersistenceDatastoreTests.swift

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ final class DiskPersistenceDatastoreTests: XCTestCase, @unchecked Sendable {
316316
let count = try await datastore.count
317317
XCTAssertEqual(count, 3)
318318

319-
let values = try await datastore.load("A"..."Z", order: .descending, from: \.value).map { $0.id }.reduce(into: []) { $0.append($1) }
319+
let values = try await datastore.load("A"..."Z", order: .descending, from: \.value).map { $0.id }.collectInstances(upTo: .infinity)
320320
XCTAssertEqual(values, ["2", "3", "1"])
321321
}
322322

@@ -356,7 +356,7 @@ final class DiskPersistenceDatastoreTests: XCTestCase, @unchecked Sendable {
356356
let count = try await datastore.count
357357
XCTAssertEqual(count, 3)
358358

359-
let values = try await datastore.load("A"..."Z", order: .descending, from: \.value).map { $0.id }.reduce(into: []) { $0.append($1) }
359+
let values = try await datastore.load("A"..."Z", order: .descending, from: \.value).map { $0.id }.collectInstances(upTo: .infinity)
360360
XCTAssertEqual(values, ["2", "3", "1"])
361361
let value3 = try await datastore.load("My name is Dimitri", from: \.value).map { $0.id }
362362
XCTAssertEqual(value3, "3")
@@ -404,15 +404,15 @@ final class DiskPersistenceDatastoreTests: XCTestCase, @unchecked Sendable {
404404
let count = try await datastore.count
405405
XCTAssertEqual(count, 3)
406406

407-
let values = try await datastore.load("A"..."Z", order: .descending, from: \.value).map { $0.id }.reduce(into: []) { $0.append($1) }
407+
let values = try await datastore.load("A"..."Z", order: .descending, from: \.value).map { $0.id }.collectInstances(upTo: .infinity)
408408
XCTAssertEqual(values, ["2", "3", "1", "2", "1", "3", "1", "3"])
409-
let valuesA = try await datastore.load("A", order: .descending, from: \.value).map { $0.id }.reduce(into: []) { $0.append($1) }
409+
let valuesA = try await datastore.load("A", order: .descending, from: \.value).map { $0.id }.collectInstances(upTo: .infinity)
410410
XCTAssertEqual(valuesA, ["3"])
411-
let valuesB = try await datastore.load("B", order: .descending, from: \.value).map { $0.id }.reduce(into: []) { $0.append($1) }
411+
let valuesB = try await datastore.load("B", order: .descending, from: \.value).map { $0.id }.collectInstances(upTo: .infinity)
412412
XCTAssertEqual(valuesB, ["3", "1"])
413-
let valuesC = try await datastore.load("C", order: .ascending, from: \.value).map { $0.id }.reduce(into: []) { $0.append($1) }
413+
let valuesC = try await datastore.load("C", order: .ascending, from: \.value).map { $0.id }.collectInstances(upTo: .infinity)
414414
XCTAssertEqual(valuesC, ["1", "2"])
415-
let valuesD = try await datastore.load("D", order: .descending, from: \.value).map { $0.id }.reduce(into: []) { $0.append($1) }
415+
let valuesD = try await datastore.load("D", order: .descending, from: \.value).map { $0.id }.collectInstances(upTo: .infinity)
416416
XCTAssertEqual(valuesD, [])
417417
}
418418

@@ -452,21 +452,21 @@ final class DiskPersistenceDatastoreTests: XCTestCase, @unchecked Sendable {
452452
let count = try await datastore.count
453453
XCTAssertEqual(count, 3)
454454

455-
let values = try await datastore.load("A"..."Z", order: .descending, from: \.value).map { $0.id }.reduce(into: []) { $0.append($1) }
455+
let values = try await datastore.load("A"..."Z", order: .descending, from: \.value).map { $0.id }.collectInstances(upTo: .infinity)
456456
XCTAssertEqual(values, ["2", "3", "1", "2", "1", "3", "1", "3"])
457-
let valuesA = try await datastore.load("A", order: .descending, from: \.value).map { $0.id }.reduce(into: []) { $0.append($1) }
457+
let valuesA = try await datastore.load("A", order: .descending, from: \.value).map { $0.id }.collectInstances(upTo: .infinity)
458458
XCTAssertEqual(valuesA, ["3"])
459459
let valueA = try await datastore.load("A", from: \.value).map { $0.id }
460460
XCTAssertEqual(valueA, "3")
461-
let valuesB = try await datastore.load("B", order: .descending, from: \.value).map { $0.id }.reduce(into: []) { $0.append($1) }
461+
let valuesB = try await datastore.load("B", order: .descending, from: \.value).map { $0.id }.collectInstances(upTo: .infinity)
462462
XCTAssertEqual(valuesB, ["3", "1"])
463463
let valueB = try await datastore.load("B", from: \.value).map { $0.id }
464464
XCTAssertEqual(valueB, "1")
465-
let valuesC = try await datastore.load("C", order: .ascending, from: \.value).map { $0.id }.reduce(into: []) { $0.append($1) }
465+
let valuesC = try await datastore.load("C", order: .ascending, from: \.value).map { $0.id }.collectInstances(upTo: .infinity)
466466
XCTAssertEqual(valuesC, ["1", "2"])
467467
let valueC = try await datastore.load("C", from: \.value).map { $0.id }
468468
XCTAssertEqual(valueC, "1")
469-
let valuesD = try await datastore.load("D", order: .descending, from: \.value).map { $0.id }.reduce(into: []) { $0.append($1) }
469+
let valuesD = try await datastore.load("D", order: .descending, from: \.value).map { $0.id }.collectInstances(upTo: .infinity)
470470
XCTAssertEqual(valuesD, [])
471471
let valueNil = try await datastore.load("D", from: \.value).map { $0.id }
472472
XCTAssertNil(valueNil)
@@ -558,7 +558,7 @@ final class DiskPersistenceDatastoreTests: XCTestCase, @unchecked Sendable {
558558
)
559559

560560
/// Read before persisting anything
561-
var values = try await datastore.load(...).map { $0.value }.reduce(into: []) { $0.append($1) }
561+
var values = try await datastore.load(...).map { $0.value }.collectInstances(upTo: .infinity)
562562
XCTAssertEqual(values, [])
563563

564564
for n in 0..<200 {
@@ -572,57 +572,57 @@ final class DiskPersistenceDatastoreTests: XCTestCase, @unchecked Sendable {
572572
XCTAssertEqual(iteratedCount, 200)
573573

574574
/// Simple ranges
575-
values = try await datastore.load(5..<9).map { $0.value }.reduce(into: []) { $0.append($1) }
575+
values = try await datastore.load(5..<9).map { $0.value }.collectInstances(upTo: .infinity)
576576
XCTAssertEqual(values, ["6", "8"])
577577

578-
values = try await datastore.load((5..<9).reversed).map { $0.value }.reduce(into: []) { $0.append($1) }
578+
values = try await datastore.load((5..<9).reversed).map { $0.value }.collectInstances(upTo: .infinity)
579579
XCTAssertEqual(values, ["8", "6"])
580580

581581
/// Larger ranges
582-
values = try await datastore.load(221..<241).map { $0.value }.reduce(into: []) { $0.append($1) }
582+
values = try await datastore.load(221..<241).map { $0.value }.collectInstances(upTo: .infinity)
583583
XCTAssertEqual(values, ["222", "224", "226", "228", "230", "232", "234", "236", "238", "240"])
584584

585-
values = try await datastore.load((221..<241).reversed).map { $0.value }.reduce(into: []) { $0.append($1) }
585+
values = try await datastore.load((221..<241).reversed).map { $0.value }.collectInstances(upTo: .infinity)
586586
XCTAssertEqual(values, ["240", "238", "236", "234", "232", "230", "228", "226", "224", "222"])
587587

588588
/// Across page boudries
589-
values = try await datastore.load(209...217).map { $0.value }.reduce(into: []) { $0.append($1) }
589+
values = try await datastore.load(209...217).map { $0.value }.collectInstances(upTo: .infinity)
590590
XCTAssertEqual(values, ["210", "212", "214", "216"])
591591

592-
values = try await datastore.load((209...217).reversed).map { $0.value }.reduce(into: []) { $0.append($1) }
592+
values = try await datastore.load((209...217).reversed).map { $0.value }.collectInstances(upTo: .infinity)
593593
XCTAssertEqual(values, ["216", "214", "212", "210"])
594594

595595
/// Unbounded ranges
596-
values = Array(try await datastore.load(.unbounded).map { $0.value }.reduce(into: []) { $0.append($1) }.prefix(5))
596+
values = Array(try await datastore.load(.unbounded).map { $0.value }.collectInstances(upTo: .infinity).prefix(5))
597597
XCTAssertEqual(values, ["0", "2", "4", "6", "8"])
598598

599-
values = Array(try await datastore.load(...).map { $0.value }.reduce(into: []) { $0.append($1) }.prefix(5))
599+
values = Array(try await datastore.load(...).map { $0.value }.collectInstances(upTo: .infinity).prefix(5))
600600
XCTAssertEqual(values, ["0", "2", "4", "6", "8"])
601601

602-
values = Array(try await datastore.load(.unbounded.reversed).map { $0.value }.reduce(into: []) { $0.append($1) }.prefix(5))
602+
values = Array(try await datastore.load(.unbounded.reversed).map { $0.value }.collectInstances(upTo: .infinity).prefix(5))
603603
XCTAssertEqual(values, ["398", "396", "394", "392", "390"])
604604

605-
values = Array(try await datastore.load(..., order: .descending).map { $0.value }.reduce(into: []) { $0.append($1) }.prefix(5))
605+
values = Array(try await datastore.load(..., order: .descending).map { $0.value }.collectInstances(upTo: .infinity).prefix(5))
606606
XCTAssertEqual(values, ["398", "396", "394", "392", "390"])
607607

608608
/// Inclusive ranges
609-
values = try await datastore.load(6...10).map { $0.value }.reduce(into: []) { $0.append($1) }
609+
values = try await datastore.load(6...10).map { $0.value }.collectInstances(upTo: .infinity)
610610
XCTAssertEqual(values, ["6", "8", "10"])
611611

612-
values = try await datastore.load(6...10, order: .descending).map { $0.value }.reduce(into: []) { $0.append($1) }
612+
values = try await datastore.load(6...10, order: .descending).map { $0.value }.collectInstances(upTo: .infinity)
613613
XCTAssertEqual(values, ["10", "8", "6"])
614614

615615
/// Exclusive ranges
616-
values = try await datastore.load(6..<10).map { $0.value }.reduce(into: []) { $0.append($1) }
616+
values = try await datastore.load(6..<10).map { $0.value }.collectInstances(upTo: .infinity)
617617
XCTAssertEqual(values, ["6", "8"])
618618

619-
values = try await datastore.load(6..<10, order: .descending).map { $0.value }.reduce(into: []) { $0.append($1) }
619+
values = try await datastore.load(6..<10, order: .descending).map { $0.value }.collectInstances(upTo: .infinity)
620620
XCTAssertEqual(values, ["8", "6"])
621621

622-
values = try await datastore.load(6..>10).map { $0.value }.reduce(into: []) { $0.append($1) }
622+
values = try await datastore.load(6..>10).map { $0.value }.collectInstances(upTo: .infinity)
623623
XCTAssertEqual(values, ["8", "10"])
624624

625-
values = try await datastore.load(6..>10, order: .descending).map { $0.value }.reduce(into: []) { $0.append($1) }
625+
values = try await datastore.load(6..>10, order: .descending).map { $0.value }.collectInstances(upTo: .infinity)
626626
XCTAssertEqual(values, ["10", "8"])
627627
}
628628

@@ -902,7 +902,7 @@ final class DiskPersistenceDatastoreTests: XCTestCase, @unchecked Sendable {
902902
try await datastore.persist(.init(id: 10, value: valueBank.randomElement()!))
903903
try await persistence.perform {
904904
/// Resolve and close out the previous child transaction, which should not corrupt the parent.
905-
let resolvedInstances = try await allInstances.reduce(into: []) { $0.append($1) }
905+
let resolvedInstances = try await allInstances.collectInstances(upTo: .infinity)
906906
XCTAssertEqual(resolvedInstances.count, 10)
907907

908908
/// Allow corruption to occur if they will.

0 commit comments

Comments
 (0)