pointfreeco / swift-composable-architecture

A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
https://www.pointfree.co/collections/composable-architecture
MIT License
12.22k stars 1.42k forks source link

Make PersistenceKeyDefault load its default value lazily #3057

Closed seanmrich closed 4 months ago

seanmrich commented 4 months ago

PersistenceKeyDefault eagerly evaluates its defaultValue. This means every declaration of a Shared value creates a new copy of this value when it's evaluated. Since the normal pattern for persisting shared values is to successfully load from its source, the default value is generated and thrown away without being used.

Additionally, creating this default value can have side effects. Consider a testing context where the default value uses the uuid dependency. Here's a persistence key whose default value creates a Folder value whose initializer generates an id from the current uuid dependency.

extension PersistenceKey where Self == PersistenceKeyDefault<FileStorageKey<Folder>> {
  public static var rootFolder: Self {
    PersistenceKeyDefault(
      .fileStorage(.documentsDirectory.appendingPathComponent("rootFolder", conformingTo: .json)), 
      Folder()
    )
  }
}

Now when running a test that overrides the default with a predetermined value, the creation of the default value has incremented the uuid dependency, even though the value isn't used.

func testAddFolder() async {
  await withDependencies {
    $0.uuid = .incrementing
  } operation: {
    @Shared(.rootFolder) var root = Folder(…)
    let store = TestStore(
      initialState: Feature.State(),
      reducer: { Feature() }
    )
    await store.send(.addFolderButtonTapped) {
      $0.root.subfolders = [
        Folder(id: UUID(???))
      ]
    }
  }
}

This request modifies PersistenceKeyDefault.defaultValue to lazily evaluate only when the load action fails. It also includes a couple tests to verify the change.