Repository: muukii/Verge Branch: main Commit: 5c8bcd829d57 Files: 199 Total size: 498.0 KB Directory structure: gitextract_zegsmnq1/ ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── CommitChecks.yml ├── .gitignore ├── .spi.yml ├── Development/ │ ├── .gitignore │ ├── Development/ │ │ ├── Resources/ │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── AccentColor.colorset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ └── Preview Content/ │ │ │ └── Preview Assets.xcassets/ │ │ │ └── Contents.json │ │ ├── Sources/ │ │ │ ├── BookBinding.swift │ │ │ ├── BookLongList.swift │ │ │ ├── BookReadingPropertyWrapper.swift │ │ │ ├── BookStoreReader.swift │ │ │ ├── ContentView.swift │ │ │ └── DevelopmentApp.swift │ │ ├── Tests/ │ │ │ └── DevelopmentTests.swift │ │ └── UITests/ │ │ ├── ReadingTests.swift │ │ └── StoreReaderTests.swift │ ├── DevelopmentUITests/ │ │ ├── DevelopmentUITests.swift │ │ └── DevelopmentUITestsLaunchTests.swift │ ├── Project.swift │ ├── Tuist/ │ │ ├── Package.resolved │ │ └── Package.swift │ ├── Tuist.swift │ └── mise.toml ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources/ │ ├── Verge/ │ │ ├── Documentation.docc/ │ │ │ ├── Activity.md │ │ │ ├── Changes.md │ │ │ ├── ComputedProperty.md │ │ │ ├── Derived.md │ │ │ ├── Dispatcher.md │ │ │ ├── Essentials/ │ │ │ │ └── Motivation.md │ │ │ ├── Guides/ │ │ │ │ ├── Advanced Usage.md │ │ │ │ ├── Basic Usage.md │ │ │ │ └── Migration Guide v9.md │ │ │ ├── Mutation.md │ │ │ ├── Resources/ │ │ │ │ └── Tiny.md │ │ │ ├── State.md │ │ │ ├── Verge.Store.md │ │ │ └── Verge.md │ │ ├── Library/ │ │ │ ├── BackgroundDeallocationQueue.swift │ │ │ ├── CachedMap.swift │ │ │ ├── EventEmitter.swift │ │ │ ├── InoutRef.swift │ │ │ ├── Log.swift │ │ │ ├── Signpost.swift │ │ │ ├── StoreActivitySubscription.swift │ │ │ ├── StoreStateSubscription.swift │ │ │ ├── StoreSubscriptionBase.swift │ │ │ ├── VergeAnyCancellable.swift │ │ │ ├── VergeConcurrency+SynchronizationTracker.swift │ │ │ ├── VergeConcurrency.swift │ │ │ └── _BackingStorage+.swift │ │ ├── Logging/ │ │ │ ├── ActivityTrace.swift │ │ │ ├── DefaultStoreLogger.swift │ │ │ ├── MutationTrace.swift │ │ │ ├── RuntimeError.swift │ │ │ ├── RuntimeSanitizer.swift │ │ │ └── StoreLogger.swift │ │ ├── Sendable.swift │ │ ├── Store/ │ │ │ ├── AnyTargetQueue.swift │ │ │ ├── Changes.swift │ │ │ ├── DetachedDispatcher.swift │ │ │ ├── KeyObject.swift │ │ │ ├── NonAtomicCounter.swift │ │ │ ├── Pipeline.swift │ │ │ ├── Scan.swift │ │ │ ├── StateType.swift │ │ │ ├── Store+Combine.swift │ │ │ ├── Store+RunLoop.swift │ │ │ ├── Store.swift │ │ │ ├── StoreDriverType+Accumulator.swift │ │ │ ├── StoreDriverType.swift │ │ │ ├── StoreMiddleware.swift │ │ │ ├── StoreOperation.swift │ │ │ ├── StoreType+Assignee.swift │ │ │ ├── StoreType+BindingDerived.swift │ │ │ ├── StoreType+Derived.swift │ │ │ ├── StoreWrapperType.swift │ │ │ ├── Transaction.swift │ │ │ └── UIStateStore.swift │ │ ├── SwiftUI/ │ │ │ ├── .swift │ │ │ ├── OnReceive.swift │ │ │ ├── Reading.swift │ │ │ ├── StoreObject.swift │ │ │ └── StoreReader.swift │ │ ├── Utility/ │ │ │ ├── Edge.swift │ │ │ ├── ReferenceEdge.swift │ │ │ └── ThunkToMainActor.swift │ │ ├── Verge.swift │ │ └── macros.swift │ ├── VergeClassic/ │ │ ├── Emitter.swift │ │ ├── Extensions.swift │ │ ├── Info.plist │ │ ├── Storage+Rx.swift │ │ ├── Storage.swift │ │ ├── Verge+Extension.swift │ │ ├── Verge.h │ │ └── VergeClassic.swift │ ├── VergeMacros/ │ │ └── Source.swift │ ├── VergeMacrosPlugin/ │ │ ├── KeyPathMap.swift │ │ ├── MacroError.swift │ │ └── Plugin.swift │ ├── VergeNormalizationDerived/ │ │ ├── DerivedMaking+.swift │ │ ├── DerivedResult.swift │ │ ├── EntityType+Typealias.swift │ │ ├── EntityWrapper.swift │ │ ├── NonNullEntityWrapper.swift │ │ ├── Pipelines.swift │ │ ├── StoreType+.swift │ │ └── VergeNormalizationDerived.swift │ ├── VergeRx/ │ │ ├── Extensions.swift │ │ └── Store+Rx.swift │ └── VergeTiny/ │ └── Source.swift ├── StoreReaderDemo/ │ └── StoreReaderDemo/ │ ├── Assets.xcassets/ │ │ ├── AccentColor.colorset/ │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── ContentView.swift │ ├── Preview Content/ │ │ └── Preview Assets.xcassets/ │ │ └── Contents.json │ └── StoreReaderDemoApp.swift ├── TaskManagerPlayground/ │ └── TaskManagerPlayground/ │ ├── Assets.xcassets/ │ │ ├── AccentColor.colorset/ │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Book.swift │ ├── ContentView.swift │ ├── Info.plist │ ├── Preview Content/ │ │ └── Preview Assets.xcassets/ │ │ └── Contents.json │ └── TaskManagerPlaygroundApp.swift ├── Tests/ │ ├── All.xctestplan │ ├── DemoState.swift │ ├── VergeMacrosTests/ │ │ └── KeyPathMapTests.swift │ ├── VergeNormalizationDerivedTests/ │ │ ├── CombiningTests.swift │ │ ├── DemoState.swift │ │ └── VergeNormalizationDerivedTests.swift │ ├── VergeRxTests/ │ │ ├── ChangedOperatorTests.swift │ │ ├── DemoState.swift │ │ ├── ReproduceDeadlockTests.swift │ │ ├── SubjectCompletionTests.swift │ │ └── VergeRxTests.swift │ ├── VergeTests/ │ │ ├── AccumulationTests.swift │ │ ├── ActivityTests.swift │ │ ├── BindingDerivedTests.swift │ │ ├── CachedMapTests.swift │ │ ├── ChangesTests.swift │ │ ├── ComparerTests.swift │ │ ├── ConcurrencyTests.swift │ │ ├── CopyPerformance.swift │ │ ├── CounterTests.swift │ │ ├── DemoState.swift │ │ ├── DerivedTests.swift │ │ ├── EdgeTests.swift │ │ ├── EventEmitterTests.swift │ │ ├── FilterTests.swift │ │ ├── IsolatedContextTests.swift │ │ ├── OldComparer.swift │ │ ├── PerformanceTests.swift │ │ ├── PipelineTests.swift │ │ ├── ReferenceEdgeTests.swift │ │ ├── Retain/ │ │ │ ├── PublisherCompletionTests.swift │ │ │ └── StoreSinkSusbscriptionTests.swift │ │ ├── RunLoopTests.swift │ │ ├── Sample.swift │ │ ├── StateTypeTests.swift │ │ ├── StoreAndDerivedTests.swift │ │ ├── StoreInitTests.swift │ │ ├── StoreMiddlewareTests.swift │ │ ├── StoreSinkTests.swift │ │ ├── StoreTaskTests.swift │ │ ├── SynchronizeDisplayValueTests.swift │ │ ├── SyntaxTests.swift │ │ ├── TransactionTests.swift │ │ ├── Usage.swift │ │ └── VergeStoreTests.swift │ └── VergeTinyTests/ │ └── VergeTinyTests.swift ├── Verge.playground/ │ ├── Pages/ │ │ ├── Memo.xcplaygroundpage/ │ │ │ └── Contents.swift │ │ ├── PlainStorePattern1.xcplaygroundpage/ │ │ │ └── Contents.swift │ │ ├── PlainStorePattern2.xcplaygroundpage/ │ │ │ └── Contents.swift │ │ ├── PlainStorePattern3.xcplaygroundpage/ │ │ │ └── Contents.swift │ │ ├── PlainStorePattern4.xcplaygroundpage/ │ │ │ └── Contents.swift │ │ ├── PlainStorePattern5.xcplaygroundpage/ │ │ │ └── Contents.swift │ │ ├── VergeStorePartern2.xcplaygroundpage/ │ │ │ └── Contents.swift │ │ └── VergeStorePattern.xcplaygroundpage/ │ │ └── Contents.swift │ ├── Sources/ │ │ └── Wire.swift │ └── contents.xcplayground ├── mise.toml └── playgrounds/ ├── .gitignore └── PlaySwiftUI/ ├── PlaySwiftUI/ │ ├── Assets.xcassets/ │ │ ├── AccentColor.colorset/ │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── ContentView.swift │ ├── PlaySwiftUIApp.swift │ ├── Preview Content/ │ │ └── Preview Assets.xcassets/ │ │ └── Contents.json │ └── Simple.swift └── PlaySwiftUI.xcodeproj/ ├── project.pbxproj └── project.xcworkspace/ ├── contents.xcworkspacedata └── xcshareddata/ └── swiftpm/ └── Package.resolved ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: [muukii] patreon: muukii ko_fi: muukii ================================================ FILE: .github/workflows/CommitChecks.yml ================================================ name: CommitChecks on: push: branches: - "**" jobs: test: runs-on: macos-15 steps: - uses: maxim-lobanov/setup-xcode@v1.1 with: xcode-version: "26" - uses: actions/checkout@v2 - name: Run Test run: set -o pipefail && xcodebuild -scheme Verge-Package test -destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.0' -skipMacroValidation -skipPackagePluginValidation | xcbeautify ui-test: runs-on: macos-15 defaults: run: working-directory: ./Development steps: - uses: maxim-lobanov/setup-xcode@v1.1 with: xcode-version: "26" - uses: actions/checkout@v2 - uses: jdx/mise-action@v2 with: install: true cache: true experimental: true - run: tuist install - run: tuist generate --no-open - run: tuist test ================================================ FILE: .gitignore ================================================ # Created by https://www.gitignore.io/api/swift ### Swift ### # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore /worktree ## Build generated build/ DerivedData/ .swiftpm ## Various settings *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata/ ## Other *.moved-aside *.xccheckout *.xcscmblueprint ## Obj-C/Swift specific *.hmap *.ipa *.dSYM.zip *.dSYM ## Playgrounds timeline.xctimeline playground.xcworkspace # Swift Package Manager # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ # Package.pins .build/ # CocoaPods - Refactored to standalone file Pods # Carthage - Refactored to standalone file Carthage # fastlane # # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the # screenshots whenever they are needed. # For more information about the recommended setup visit: # https://docs.fastlane.tools/best-practices/source-control/#source-control fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output .DS_Store # Tuist Derived *.xcodeproj Workspace.xcworkspace/ settings.local.json # End of https://www.gitignore.io/api/swift ================================================ FILE: .spi.yml ================================================ version: 1 builder: configs: - documentation_targets: [Verge, VergeNormalizationDerived] ================================================ FILE: Development/.gitignore ================================================ ### macOS ### # General .DS_Store .AppleDouble .LSOverride # Icon must end with two Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### Xcode ### # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore ## User settings xcuserdata/ ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) *.xcscmblueprint *.xccheckout ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) build/ DerivedData/ *.moved-aside *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 ### Xcode Patch ### *.xcodeproj/* !*.xcodeproj/project.pbxproj !*.xcodeproj/xcshareddata/ !*.xcworkspace/contents.xcworkspacedata /*.gcno ### Projects ### *.xcodeproj *.xcworkspace ### Tuist derived files ### graph.dot Derived/ ### Tuist managed dependencies ### Tuist/.build ================================================ FILE: Development/Development/Resources/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Development/Development/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { "idiom" : "ipad", "scale" : "1x", "size" : "20x20" }, { "idiom" : "ipad", "scale" : "2x", "size" : "20x20" }, { "idiom" : "ipad", "scale" : "1x", "size" : "29x29" }, { "idiom" : "ipad", "scale" : "2x", "size" : "29x29" }, { "idiom" : "ipad", "scale" : "1x", "size" : "40x40" }, { "idiom" : "ipad", "scale" : "2x", "size" : "40x40" }, { "idiom" : "ipad", "scale" : "1x", "size" : "76x76" }, { "idiom" : "ipad", "scale" : "2x", "size" : "76x76" }, { "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" }, { "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Development/Development/Resources/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Development/Development/Resources/Preview Content/Preview Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Development/Development/Sources/BookBinding.swift ================================================ import SwiftUI import Verge struct BookBindingUsingReading: View { @ReadingObject> var state: BookBindingState init() { self._state = .init({ .init(initialState: .init()) }) } var body: some View { Counter(value: $state.value) } struct Counter: View { @Binding var value: Int var body: some View { VStack { Text("\(value)") Button { value += 1 } label: { Text("Increment") } } } } } struct BookBindingUsingStoreReader: View { let store: Store = .init(initialState: .init()) init() { } var body: some View { StoreReader(store) { $state in Counter(value: $state.value) } } struct Counter: View { @Binding var value: Int var body: some View { VStack { Text("\(value)") Button { value += 1 } label: { Text("Increment") } } } } } @Tracking struct BookBindingState { var value: Int = 0 } #Preview("Binding Reading") { BookBindingUsingReading() } #Preview("Binding StoreReader") { BookBindingUsingStoreReader() } ================================================ FILE: Development/Development/Sources/BookLongList.swift ================================================ // // BookLongList.swift // Development // // Created by Muukii on 2025/03/26. // import SwiftUI import Verge @Tracking struct BookState { var items: [BookItem] = [] init(items: [BookItem]) { self.items = items } } struct BookItem: Identifiable { var id: some Hashable { cellStore } let cellStore: Store } @Tracking struct BookCellState { let id: Int var title: String var isSelected: Bool = false } struct BookCellContent: View { let store: Store var body: some View { StoreReader(store) { $state in RoundedRectangle(cornerRadius: 8) .fill(state.isSelected ? Color.red : Color.blue.opacity(0.2)) .aspectRatio(1, contentMode: .fit) .overlay( Text("\(state.id + 1)") .font(.system(size: 16)) ) } } } struct BookCell: View { let store: Store var body: some View { Button { store.commit { state in state.isSelected.toggle() } } label: { BookCellContent(store: store) } } } struct BookLongList: View { private let columns = [ GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), ] @ReadingObject>({ .init(initialState: .init(items: (0..<500).map { index in BookItem( cellStore: Store( initialState: BookCellState( id: index, title: UUID().uuidString ) ) ) })) }) var state: BookState init() { } var body: some View { ScrollView { LazyVGrid(columns: columns, spacing: 16) { ForEach(state.items) { item in BookCell(store: item.cellStore) } } .padding() } } } #Preview { BookLongList() } ================================================ FILE: Development/Development/Sources/BookReadingPropertyWrapper.swift ================================================ import Verge import SwiftUI struct BookReading: View { var body: some View { ReadingSolution() } } @Tracking struct MyState { var value: Int = 0 var a: Int = 0 var b: Int = 0 var c: Int = 0 } extension Store where State == MyState { func backgroundUpdate() { Task { await self.backgroundCommit { $0.value += 1 $0.a += 1 $0.b += 1 $0.c += 1 } } } } enum ItemKind: Identifiable { case first case second case third var id: String { switch self { case .first: return "1" case .second: return "2" case .third: return "3" } } } struct ItemDetail: View { let items: [Item] let content: (Item) -> Content @State var selectedItem: Item? private let name: String init( name: String, items: [Item], @ViewBuilder content: @escaping (Item) -> Content ) { self.name = name self.items = items self.content = content } var body: some View { VStack { ForEach(items) { item in Button("\(name).\(item.id)") { selectedItem = item } } if let item = selectedItem { content(item) .padding() .background(Color.orange) } } } } private struct ReadingSolution: View { @State var id: Int = 0 @State var outerValue: Int = 0 init() { print("init") } var body: some View { VStack { Button("New") { id += 1 } Button("Up Outer") { outerValue += 1 } Solution(outerValue: outerValue) .id(id) .padding() .background(Color.green) } } struct Solution: View { let items: [ItemKind] = [ .first, .second, .third, ] @ReadingObject> var state: MyState private let outerValue: Int init(outerValue: Int) { self._state = .init( label: "A", { Store<_, Never>.init(initialState: MyState()) } ) self.outerValue = outerValue } var body: some View { HStack { VStack { Text("Using Store holding") Button("async up") { $state.driver.backgroundUpdate() } Button("A Up") { $state.driver.commit { $0.value += 1 } } Text("A Value: \(state.value)") Text("Outer: \(outerValue)") ItemDetail(name: "A", items: items) { item in switch item { case .first: Button.init("A.1.a: \(state.a)") { $state.driver.commit { $0.a += 1 } } case .second: Button.init("A.1.b: \(state.b)") { $state.driver.commit { $0.b += 1 } } case .third: Button.init("A.1.c: \(state.c)") { $state.driver.commit { $0.c += 1 } } } } .padding() .background(Color.yellow) } Passed(store: $state.driver) } } } struct Passed: View { let items: [ItemKind] = [ .first, .second, .third, ] @Reading> var state: MyState init( store: Store ) { self._state = .init(label: "B", store) } var body: some View { VStack { Text("Using Store passed") Button("B Up") { $state.driver.commit { $0.value += 1 } } Text("B Value: \(state.value)") ItemDetail(name: "B",items: items) { item in switch item { case .first: Button.init("B.1.a: \(state.a)") { $state.driver.commit { $0.a += 1 } } case .second: Button.init("B.1.b: \(state.b)") { $state.driver.commit { $0.b += 1 } } case .third: Button.init("B.1.c: \(state.c)") { $state.driver.commit { $0.c += 1 } } } } .padding() .background(Color.yellow) } } } } struct PassedContainer: View { private let store: Store = .init(initialState: MyState()) @State var count: Int = 0 var body: some View { VStack { Button("Outer Up \(count)") { count += 1 } ReadingSolution.Passed(store: store) } } } #Preview("Reading solution") { ReadingSolution() } #Preview("Passed solution") { PassedContainer() } ================================================ FILE: Development/Development/Sources/BookStoreReader.swift ================================================ import SwiftUI import Verge struct StoreReaderSolution: View { @State var id: Int = 0 @State var outerValue: Int = 0 init() { print("init") } var body: some View { VStack { Button("New") { id += 1 } Button("Up Outer") { outerValue += 1 } Solution(outerValue: outerValue) .id(id) .padding() .background(Color.green) } } struct Solution: View { let items: [ItemKind] = [ .first, .second, .third, ] @StoreObject var store = Store(initialState: .init()) private let outerValue: Int init(outerValue: Int) { self.outerValue = outerValue } var body: some View { HStack { VStack { Text("Using Store holding") StoreReader(store) { $state in VStack { Button("A Up") { store.commit { $0.value += 1 } } Text("A Value: \(state.value)") Text("Outer: \(outerValue)") ItemDetail(name: "A", items: items) { item in switch item { case .first: Button("A.1.a: \(state.a)") { store.commit { $0.a += 1 } } case .second: Button("A.1.b: \(state.b)") { store.commit { $0.b += 1 } } case .third: Button("A.1.c: \(state.c)") { store.commit { $0.c += 1 } } } } .padding() .background(Color.yellow) } } } Passed(store: store) } } } struct Passed: View { let items: [ItemKind] = [ .first, .second, .third, ] private let store: Store init(store: Store) { self.store = store } var body: some View { VStack { Text("Using Store passed") StoreReader(store) { $state in VStack { Button("B Up") { store.commit { $0.value += 1 } } Text("B Value: \(state.value)") ItemDetail(name: "B", items: items) { item in switch item { case .first: Button("B.1.a: \(state.a)") { store.commit { $0.a += 1 } } case .second: Button("B.1.b: \(state.b)") { store.commit { $0.b += 1 } } case .third: Button("B.1.c: \(state.c)") { store.commit { $0.c += 1 } } } } .padding() .background(Color.yellow) } } } } } } #Preview("Reading solution") { StoreReaderSolution() } ================================================ FILE: Development/Development/Sources/ContentView.swift ================================================ import SwiftUI public struct ContentView: View { public init() {} public var body: some View { NavigationStack { List { NavigationLink { BookReading() } label: { Text("@Reading") } NavigationLink { PassedContainer() } label: { Text("@Reading - passed") } NavigationLink { StoreReaderSolution() } label: { Text("StoreReader") } NavigationLink { BookLongList() } label: { Text("Long List") } NavigationLink { BookBindingUsingReading() } label: { Text("Binding @Reading") } NavigationLink { BookBindingUsingStoreReader() } label: { Text("Binding StoreReader") } } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } ================================================ FILE: Development/Development/Sources/DevelopmentApp.swift ================================================ import SwiftUI @main struct DevelopmentApp: App { var body: some Scene { WindowGroup { ContentView() } } } ================================================ FILE: Development/Development/Tests/DevelopmentTests.swift ================================================ import Foundation import XCTest final class DevelopmentTests: XCTestCase { func test_twoPlusTwo_isFour() { XCTAssertEqual(2+2, 4) } } ================================================ FILE: Development/Development/UITests/ReadingTests.swift ================================================ import XCTest final class ReadingTests: XCTestCase { var app: XCUIApplication! override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication() app.launch() } override func tearDownWithError() throws { app = nil } func testA_up() { let app = XCUIApplication() app.collectionViews.buttons[ "@Reading" ] .tap() app.buttons["A Up"].tap() XCTAssertTrue(app.staticTexts["A Value: 1"].exists) XCTAssertTrue(app.staticTexts["B Value: 1"].exists) } func test_sync() { let app = XCUIApplication() app.collectionViews.buttons[ "@Reading" ] .tap() app.buttons["A.1"].tap() app.buttons["A.1.a: 0"].tap() XCTAssertTrue(app.buttons["A.1.a: 1"].exists) app.buttons["B.1"].tap() XCTAssertTrue(app.buttons["B.1.a: 1"].exists) app.buttons["B.1.a: 1"].tap() XCTAssertTrue(app.buttons["A.1.a: 2"].exists) XCTAssertTrue(app.buttons["B.1.a: 2"].exists) } func test_binding_reading() { let app = XCUIApplication() app.collectionViews/*@START_MENU_TOKEN@*/.buttons["Binding @Reading"]/*[[".cells.buttons[\"Binding @Reading\"]",".buttons[\"Binding @Reading\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap() app.buttons["Increment"].tap() XCTAssertTrue(app.staticTexts["1"].exists) } func test_binding_storeReader() { let app = XCUIApplication() app.collectionViews.buttons["Binding StoreReader"].tap() app.buttons["Increment"].tap() XCTAssertTrue(app.staticTexts["1"].exists) } } ================================================ FILE: Development/Development/UITests/StoreReaderTests.swift ================================================ import XCTest final class StoreReaderTests: XCTestCase { var app: XCUIApplication! override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication() app.launch() } override func tearDownWithError() throws { app = nil } func testA_up() { let app = XCUIApplication() app.collectionViews.buttons[ "StoreReader" ] .tap() app.buttons["A Up"].tap() XCTAssertTrue(app.staticTexts["A Value: 1"].exists) XCTAssertTrue(app.staticTexts["B Value: 1"].exists) } func test_sync() { let app = XCUIApplication() app.collectionViews.buttons[ "StoreReader" ] .tap() app.buttons["A.1"].tap() app.buttons["A.1.a: 0"].tap() XCTAssertTrue(app.buttons["A.1.a: 1"].exists) app.buttons["B.1"].tap() XCTAssertTrue(app.buttons["B.1.a: 1"].exists) app.buttons["B.1.a: 1"].tap() XCTAssertTrue(app.buttons["A.1.a: 2"].exists) XCTAssertTrue(app.buttons["B.1.a: 2"].exists) } } ================================================ FILE: Development/DevelopmentUITests/DevelopmentUITests.swift ================================================ // // DevelopmentUITests.swift // DevelopmentUITests // // Created by Muukii on 2025/03/22. // import XCTest final class DevelopmentUITests: XCTestCase { override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. // In UI tests it is usually best to stop immediately when a failure occurs. continueAfterFailure = false // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. } override func tearDownWithError() throws { // Put teardown code here. This method is called after the invocation of each test method in the class. } @MainActor func testExample() throws { // UI tests must launch the application that they test. let app = XCUIApplication() app.launch() // Use XCTAssert and related functions to verify your tests produce the correct results. } @MainActor func testLaunchPerformance() throws { if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { // This measures how long it takes to launch your application. measure(metrics: [XCTApplicationLaunchMetric()]) { XCUIApplication().launch() } } } } ================================================ FILE: Development/DevelopmentUITests/DevelopmentUITestsLaunchTests.swift ================================================ // // DevelopmentUITestsLaunchTests.swift // DevelopmentUITests // // Created by Muukii on 2025/03/22. // import XCTest final class DevelopmentUITestsLaunchTests: XCTestCase { override class var runsForEachTargetApplicationUIConfiguration: Bool { true } override func setUpWithError() throws { continueAfterFailure = false } @MainActor func testLaunch() throws { let app = XCUIApplication() app.launch() // Insert steps here to perform after app launch but before taking a screenshot, // such as logging into a test account or navigating somewhere in the app let attachment = XCTAttachment(screenshot: app.screenshot()) attachment.name = "Launch Screen" attachment.lifetime = .keepAlways add(attachment) } } ================================================ FILE: Development/Project.swift ================================================ import ProjectDescription let project = Project( name: "Development", targets: [ .target( name: "Development", destinations: .iOS, product: .app, bundleId: "io.tuist.Development", infoPlist: .extendingDefault( with: [ "UILaunchScreen": [ "UIColorName": "", "UIImageName": "", ] ] ), sources: ["Development/Sources/**"], resources: ["Development/Resources/**"], dependencies: [ .external(name: "Verge") ] ), .target( name: "DevelopmentUITests", destinations: .iOS, product: .uiTests, bundleId: "io.tuist.DevelopmentUITests", infoPlist: .default, sources: ["Development/UITests/**"], resources: [], dependencies: [.target(name: "Development")] ), .target( name: "DevelopmentTests", destinations: .iOS, product: .unitTests, bundleId: "io.tuist.DevelopmentTests", infoPlist: .default, sources: ["Development/Tests/**"], resources: [], dependencies: [.target(name: "Development")] ), ] ) ================================================ FILE: Development/Tuist/Package.resolved ================================================ { "originHash" : "fb710b9bf77d01a2d48fc3e3b210bfb0e949769f26d1ca1c351ac86995dca6fb", "pins" : [ { "identity" : "normalization", "kind" : "remoteSourceControl", "location" : "https://github.com/VergeGroup/Normalization", "state" : { "revision" : "6e7cb1ddeda4d0f1d2fbf8ca6d25ecd8ed6ba917", "version" : "1.1.0" } }, { "identity" : "rxswift", "kind" : "remoteSourceControl", "location" : "https://github.com/ReactiveX/RxSwift.git", "state" : { "revision" : "5dd1907d64f0d36f158f61a466bab75067224893", "version" : "6.9.0" } }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-atomics.git", "state" : { "revision" : "cd142fd2f64be2100422d658e7411e39489da985", "version" : "1.2.0" } }, { "identity" : "swift-collections", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", "version" : "1.1.4" } }, { "identity" : "swift-concurrency-task-manager", "kind" : "remoteSourceControl", "location" : "https://github.com/VergeGroup/swift-concurrency-task-manager", "state" : { "revision" : "340cf14e0282977deeeb436605d1810ce4f4fbc9", "version" : "1.4.0" } }, { "identity" : "swift-macro-state-struct", "kind" : "remoteSourceControl", "location" : "https://github.com/VergeGroup/swift-macro-state-struct", "state" : { "revision" : "b6ade33024ae04699fa6a4885be70c0299eb3cec", "version" : "2.0.0" } }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { "revision" : "0687f71944021d616d34d922343dcef086855920", "version" : "600.0.1" } }, { "identity" : "typedcomparator", "kind" : "remoteSourceControl", "location" : "https://github.com/VergeGroup/TypedComparator", "state" : { "revision" : "337ce0e573e7637ddd29392cb88c3b852f042c8e", "version" : "1.0.0" } }, { "identity" : "typedidentifier", "kind" : "remoteSourceControl", "location" : "https://github.com/VergeGroup/TypedIdentifier", "state" : { "revision" : "284340409ba47858a1b3c353dcef9c21303953db", "version" : "2.0.3" } } ], "version" : 3 } ================================================ FILE: Development/Tuist/Package.swift ================================================ // swift-tools-version: 6.0 import PackageDescription #if TUIST import struct ProjectDescription.PackageSettings let packageSettings = PackageSettings( // Customize the product types for specific package product // Default is .staticFramework // productTypes: ["Alamofire": .framework,] productTypes: [:] ) #endif let package = Package( name: "Development", dependencies: [ // Add your own dependencies here: // .package(url: "https://github.com/Alamofire/Alamofire", from: "5.0.0"), // You can read more about dependencies here: https://docs.tuist.io/documentation/tuist/dependencies .package(path: "../../") ] ) ================================================ FILE: Development/Tuist.swift ================================================ import ProjectDescription let tuist = Tuist(project: .tuist()) ================================================ FILE: Development/mise.toml ================================================ [tools] tuist = "4.44.3" ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 muukii Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Package.resolved ================================================ { "originHash" : "e0239f7d7b3b817d553cd88ff38b3bf2eaf6a4d2aa61a19fb57aed0c303e01eb", "pins" : [ { "identity" : "normalization", "kind" : "remoteSourceControl", "location" : "https://github.com/VergeGroup/Normalization", "state" : { "revision" : "0d6adaadd3dcf2aa4c65db8edcf1ea94c73d4a10", "version" : "2.0.0" } }, { "identity" : "rxswift", "kind" : "remoteSourceControl", "location" : "https://github.com/ReactiveX/RxSwift.git", "state" : { "revision" : "5949cbd3d4f3f97968bb40b6cd232f8315c6341c", "version" : "6.7.0" } }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-atomics.git", "state" : { "revision" : "cd142fd2f64be2100422d658e7411e39489da985", "version" : "1.2.0" } }, { "identity" : "swift-collections", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", "version" : "1.1.0" } }, { "identity" : "swift-concurrency-task-manager", "kind" : "remoteSourceControl", "location" : "https://github.com/VergeGroup/swift-concurrency-task-manager", "state" : { "revision" : "87605b623fa1a02c657534251ae238a56bc0d15c", "version" : "3.0.0" } }, { "identity" : "swift-custom-dump", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", "version" : "1.3.3" } }, { "identity" : "swift-macro-state-struct", "kind" : "remoteSourceControl", "location" : "https://github.com/VergeGroup/swift-macro-state-struct", "state" : { "revision" : "aabd53cd8be48f71bb33e5fe66e38accc0438660", "version" : "3.0.0" } }, { "identity" : "swift-macro-testing", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-macro-testing.git", "state" : { "revision" : "9ab11325daa51c7c5c10fcf16c92bac906717c7e", "version" : "0.6.4" } }, { "identity" : "swift-snapshot-testing", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { "revision" : "a8b7c5e0ed33d8ab8887d1654d9b59f2cbad529b", "version" : "1.18.7" } }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax.git", "state" : { "revision" : "4799286537280063c85a32f09884cfbca301b1a1", "version" : "602.0.0" } }, { "identity" : "swift-typed-identifier", "kind" : "remoteSourceControl", "location" : "https://github.com/VergeGroup/swift-typed-identifier", "state" : { "revision" : "ea7afa2ce943c6bf3ef87d385c8a6e9f67fea4f3", "version" : "2.0.4" } }, { "identity" : "typedcomparator", "kind" : "remoteSourceControl", "location" : "https://github.com/VergeGroup/TypedComparator", "state" : { "revision" : "337ce0e573e7637ddd29392cb88c3b852f042c8e", "version" : "1.0.0" } }, { "identity" : "viewinspector", "kind" : "remoteSourceControl", "location" : "https://github.com/nalexn/ViewInspector.git", "state" : { "revision" : "5acfa0a3c095ac9ad050abe51c60d1831e8321da", "version" : "0.10.0" } }, { "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { "revision" : "b2ed9eabefe56202ee4939dd9fc46b6241c88317", "version" : "1.6.1" } } ], "version" : 3 } ================================================ FILE: Package.swift ================================================ // swift-tools-version: 6.0 import CompilerPluginSupport import PackageDescription let package = Package( name: "Verge", platforms: [ .macOS(.v13), .iOS(.v16), .tvOS(.v13), .watchOS(.v6), ], products: [ .library(name: "Verge", targets: ["Verge"]), .library(name: "VergeTiny", targets: ["VergeTiny"]), .library(name: "VergeNormalizationDerived", targets: ["VergeNormalizationDerived"]), .library(name: "VergeRx", targets: ["VergeRx"]), .library(name: "VergeClassic", targets: ["VergeClassic"]), .library(name: "VergeMacros", targets: ["VergeMacros"]), ], dependencies: [ .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.0.0"), .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"), .package(url: "https://github.com/apple/swift-collections", from: "1.1.0"), .package(url: "https://github.com/VergeGroup/swift-concurrency-task-manager", from: "3.0.0"), .package(url: "https://github.com/VergeGroup/TypedComparator", from: "1.0.0"), .package(url: "https://github.com/VergeGroup/Normalization", from: "2.0.0"), .package(url: "https://github.com/VergeGroup/swift-macro-state-struct", from: "3.0.0"), /// for testing .package(url: "https://github.com/nalexn/ViewInspector.git", from: "0.10.0"), .package(url: "https://github.com/swiftlang/swift-syntax.git", "600.0.0"..<"603.0.0"), .package(url: "https://github.com/pointfreeco/swift-macro-testing.git", from: "0.2.1") ], targets: [ // compiler plugin .macro( name: "VergeMacrosPlugin", dependencies: [ .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), ] ), // macro exports .target(name: "VergeMacros", dependencies: ["VergeMacrosPlugin"]), .target(name: "VergeTiny", dependencies: []), .target( name: "Verge", dependencies: [ "VergeMacros", .product(name: "StateStruct", package: "swift-macro-state-struct"), .product(name: "TypedComparator", package: "TypedComparator"), .product(name: "Atomics", package: "swift-atomics"), .product(name: "DequeModule", package: "swift-collections"), .product(name: "ConcurrencyTaskManager", package: "swift-concurrency-task-manager"), ] ), .target( name: "VergeClassic", dependencies: [ "VergeRx" ] ), .target( name: "VergeNormalizationDerived", dependencies: [ "Verge", .product(name: "Normalization", package: "Normalization"), .product(name: "HashTreeCollections", package: "swift-collections"), ] ), .target( name: "VergeRx", dependencies: [ "Verge", .product(name: "RxSwift", package: "RxSwift"), .product(name: "RxCocoa", package: "RxSwift"), ] ), .testTarget( name: "VergeNormalizationDerivedTests", dependencies: ["VergeNormalizationDerived"] ), .testTarget( name: "VergeRxTests", dependencies: ["VergeRx", "VergeClassic"] ), .testTarget( name: "VergeTests", dependencies: ["Verge", "ViewInspector"], swiftSettings: [ .enableExperimentalFeature("StrictConcurrency") ] ), .testTarget( name: "VergeTinyTests", dependencies: ["VergeTiny"] ), .testTarget( name: "VergeMacrosTests", dependencies: [ "VergeMacrosPlugin", .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), .product(name: "MacroTesting", package: "swift-macro-testing"), ]), ], swiftLanguageModes: [.v6] ) ================================================ FILE: README.md ================================================ > [!WARNING] > Verge is stabilizing and transitioning to [swift-state-graph](https://github.com/VergeGroup/swift-state-graph) as its next-generation foundation. > Please check this out > https://medium.com/p/8c7423692992

VergeIcon

Verge.swift

📍An effective state management architecture for iOS - UIKit, SwiftUI📍
_ An easier way to get unidirectional data flow _
_ Supports concurrent processing _

[My development note](https://www.notion.so/muukii/Verge-93813aec77d44bef802a2d92f565c7bb?pvs=4) ## Using StoreReader or @Reading in SwiftUI In SwiftUI, there are two ways to observe a Store: using the `StoreReader` view or the `@Reading` property wrapper. Both efficiently track state changes and only re-render the view when tracked values change. First, define your state with `@Tracking` macro: ```swift @Tracking struct State { var count: Int = 0 @Tracking struct NestedState { var isActive: Bool = false var message: String = "Hello, Verge!" } var nestedState: NestedState = NestedState() } ``` ### StoreReader Example ```swift struct MyView: View { let store = Store(initialState: .init()) var body: some View { StoreReader(store) { $state in VStack { Text("Count: \(state.count)") Button("Increment") { store.commit { $0.count += 1 } } Text("Is Active: \(state.nestedState.isActive)") Text("Message: \(state.nestedState.message)") Button("Toggle Active") { store.commit { $0.nestedState.isActive.toggle() } } } } } } ``` ### @Reading Example ```swift struct MyView: View { @Reading> var state: State init() { self._state = .init( label: "MyView", { Store(initialState: .init()) } ) } var body: some View { VStack { Text("Count: \(state.count)") Button("Increment") { $state.driver.commit { $0.count += 1 } } Text("Is Active: \(state.nestedState.isActive)") Text("Message: \(state.nestedState.message)") Button("Toggle Active") { $state.driver.commit { $0.nestedState.isActive.toggle() } } } } } ``` --- - Dependencies - [TypedIdentifier](https://github.com/VergeGroup/TypedIdentifier) - [TypedComparator](https://github.com/VergeGroup/TypedComparator) - [Normalization](https://github.com/VergeGroup/Normalization) - Docs - [Verge](https://swiftpackageindex.com/VergeGroup/swift-Verge/main/documentation/verge) - [VergeNormalizationDerived](https://swiftpackageindex.com/VergeGroup/swift-Verge/main/documentation/vergenormalizationderived) ## Support this projects yellow-button # Verge: A High-Performance, Scalable State Management Library for SwiftUI and UIKit Verge is a high-performance, scalable state management library for Swift, designed with real-world use cases in mind. It offers a lightweight and easy-to-use approach to managing your application state without the need for complex actions and reducers. This guide will walk you through the basics of using Verge in your Swift projects. ## Key Concepts and Motivations Verge was designed with the following concepts in mind: - Inspired by the Flux library, but with a focus on providing a store-pattern as the core concept. - The store-pattern is a primitive concept found in Flux and Redux, focusing on sharing state between components using a single source of truth. - Verge does not dictate how to manage actions to modify the state. Instead, it provides a simple `commit` function that accepts a closure describing how to change the state. - Users can build additional layers on top of Verge, such as implementing enum-based actions for more structured state management. - Verge supports multi-threading, ensuring fast, safe, and efficient operation. - Compatible with both UIKit and SwiftUI. - Includes APIs for handling real-world application development use cases, such as managing asynchronous operations. - Addresses the complexity of updating state in large and complex applications. - Provides an ORM for efficient management of a large number of entities. - Designed for use in business-focused applications. ## Getting Started To use Verge, follow these steps: 1. Define a state struct with `@Tracking` macro 2. Instantiate a `Store` with your initial state 3. Update the state using the `commit` method on the store instance 4. Subscribe to state updates using the `sinkState` method ## Defining Your State Create a state struct that represents the state of your application. Use the `@Tracking` macro to make your state trackable by Verge. This allows Verge to detect changes in your state and trigger updates as necessary. ```swift @Tracking struct MyState { var count: Int = 0 } ``` ## Instantiating a Store Create a `Store` instance with the initial state of your application. The `Store` class takes two type parameters: - The first type parameter represents the state of your application. - The second type parameter represents any activity you want to use with your store. If you don't need any activity, use `Never`. ```swift let store = Store<_, Never>(initialState: MyState()) ``` ## Updating the State To update your application state, use the `commit` method on your `Store` instance. The `commit` method takes a closure with a single parameter, which is a mutable reference to your state. Inside the closure, modify the state as needed. ```swift store.commit { $0.count += 1 } ``` ## Subscribing to State Updates To receive updates when the state changes, use the `sinkState` method on your `Store` instance. This method takes a closure that receives the updated state as its parameter. The closure will be called whenever the state changes. ```swift store.sinkState { state in // Receives updates of the state } .storeWhileSourceActive() ``` The `storeWhileSourceActive()` call at the end is a method provided by Verge to automatically manage the lifetime of the subscription. It retains the subscription as long as the source (in this case, the `store` instance) is alive. ## Using Activity of Store for Event-Driven Programming In certain scenarios, event-driven programming is essential for creating responsive and efficient applications. The Verge library's Activity of Store feature is designed to cater to this need, allowing developers to handle events seamlessly within their projects. The Activity of Store comes into play when your application requires event-driven programming. It enables you to manage events and associated logic independently from the main store management, promoting a clean and organized code structure. This separation of concerns simplifies the overall development process and makes it easier to maintain and extend your application over time. By leveraging the Activity of Store functionality, you can efficiently handle events within your application while keeping the store management intact. This ensures that your application remains performant and scalable, enabling you to build robust and reliable Swift applications using the Verge library. Here's an example of using Activity of Store: ```swift let store: Store store.send(MyActivity.somethingHappened) ``` ```swift store.sinkActivity { (activity: MyActivity) in // handle activities. } .storeWhileSourceActive() ``` ## Using Verge with SwiftUI To use Verge in SwiftUI, you can utilize the `StoreReader` to subscribe to state updates within your SwiftUI views. Here's an example of how to do this: ```swift import SwiftUI import Verge struct ContentView: View { @StoreObject private var viewModel = CounterViewModel() var body: some View { VStack { StoreReader(viewModel) { state in Text("Count: \(state.count)") .font(.largeTitle) } Button(action: { viewModel.increment() }) { Text("Increment") } } } } final class CounterViewModel: StoreComponentType { @Tracking struct State { var count: Int = 0 } let store: Store = .init(initialState: .init()) func increment() { commit { $0.count += 1 } } } ``` In this example, `StoreReader` is used to read the state from the `MyViewModel` store. This allows you to access and display the state within your SwiftUI view. Additionally, you can perform actions by calling methods on the store directly, as demonstrated with the button in the example. This new section will help users understand how to use Verge with SwiftUI, allowing them to manage state effectively within their SwiftUI views. Let me know if you have any further suggestions or changes! **StoreObject** property wrapper: SwiftUI provides the `@StateObject` property wrapper to create and manage a persistent instance of a given object that adheres to the ObservableObject protocol. However, StateObject will cause the view to be refreshed whenever the ObservableObject is updated. In Verge, we introduce the StoreObject property wrapper, which instantiates a Store object for the duration of the view's lifecycle but does not cause the view to refresh when the Store updates. This is beneficial when you want to manage the Store in a more granular way, without causing the entire view to refresh when the Store changes. Instead, Store updates can be handled through the StoreReader. ## Using Verge with UIKit Here's a simple usage example of Verge with a UIViewController: ```swift class MyViewController: UIViewController { @Tracking private struct State { var count: Int = 0 } private let store: Store = .init(initialState: .init()) private let label: UILabel = .init() override func viewDidLoad() { super.viewDidLoad() setupUI() // Subscribe to the store's state updates store.sinkState { [weak self] state in guard let self = self else { return } // Check if the value has been updated using ifChanged state.ifChanged(\.count) { count in self.label.text = "Count: \(count)" } } .storeWhileSourceActive() } private func setupUI() { // Omitted for brevity } private func incrementCount() { store.commit { $0.count += 1 } } } ``` ## Efficient State Updates in UIKit using `sinkState`, `Changed`, and `ifChanged` In UIKit, which is event-driven, it's crucial to update components efficiently by only updating them as needed. The Verge library provides a way to achieve this using the `sinkState` method, the `Changed` type, and the `ifChanged` method. When you use the `sinkState` method, the closure you provide receives the latest state wrapped in a `Changed` type. This wrapper also includes the previous state, allowing you to determine which properties have been updated using the `ifChanged` method. Here's an example of using `sinkState` and `ifChanged` in UIKit to efficiently update components: ```swift store.sinkState { $0.ifChanged(\.myProperty) { newValue in // Update the component only when myProperty has changed } } ``` In this example, the component is updated only when `myProperty` has changed, ensuring efficient updates in the UIKit-based application. Compared to UIKit, SwiftUI works with a declarative view structure, which means that there is less need to check for state changes to update the view. However, when working with UIKit, using `sinkState`, `Changed`, and `ifChanged` helps maintain a performant and responsive application. ## Using TaskManager for Asynchronous Operations Verge's Store includes a TaskManager that allows you to dispatch and manage asynchronous operations. This feature simplifies handling async tasks while keeping them associated with your Store. ### Basic usage To use TaskManager, simply call the `task` method on your Store instance, and provide a closure that contains the asynchronous operation: ```swift store.task { await runMyOperation() } ``` ### Task management with keys and modes TaskManager also enables you to manage tasks based on keys and modes. You can assign a unique key to each task and specify a mode for its execution. This allows you to control the execution behavior of tasks based on their keys. For example, you can use the `.dropCurrent` mode to drop any currently running tasks with the same key and run the new task immediately: ```swift store.task(key: .init("MyOperation"), mode: .dropCurrent) { // } ``` This functionality provides you with fine-grained control over how tasks are executed, ensuring that your application remains responsive and efficient, even when handling multiple asynchronous operations. ## Advanced Usage: Managing Multiple Stores for Complex Applications In theory, managing your entire application state in a single store is ideal. However, in large and complex applications, the computational complexity can become significant, leading to performance issues and slow application responsiveness. In such cases, it's recommended to separate your state into multiple stores and integrate them as needed. By dividing your state into multiple stores, you can reduce the complexity and overhead associated with state updates. Each store can manage a specific part of your application state, ensuring that updates are performed efficiently and quickly. This approach also promotes better organization and separation of concerns in your code, making it easier to maintain and extend your application over time. To use multiple stores, create separate Store instances for different parts of your application state, and then connect them as needed. This may involve passing store instances to child components or sharing stores between sibling components. By structuring your application this way, you can ensure that each part of your application state is managed efficiently and effectively. ### Copying State Between Stores To copy state between stores, you can use the `sinkState` method along with the `ifChanged` function to only trigger updates when the state has changed. Here's an example: ```swift store.sinkState { $0.ifChanged(\.myState) { value in otherStore.commit { $0.myState = value } } } ``` In this example, when the state of `myState` changes in `store`, the new value is committed to `otherStore`. This approach allows you to synchronize state between multiple stores efficiently. ## Using Derived for Efficient Computed Properties Verge's `Derived` feature allows you to create computed properties based on your store's state and efficiently subscribe to updates. This feature can help you optimize your application by reducing unnecessary computations and updates. Derived is inspired by the [reselect](https://github.com/reduxjs/reselect) library and provides similar functionality. ### Creating a Derived Property To create a derived property, you'll use the `store.derived` method. This method takes a `Pipeline` object that describes how the derived data is generated: ```swift let derived: Derived = store.derived(.select(\\.count)) ``` You can use `select` or `map` to generate derived data. `select` is used to take a value directly from the state, while `map` can be used to generate new values based on the state, similar to a map function: ```swift let derived: Derived = store.derived(.map { $0.count * 2 }) ``` The `Pipeline` checks if the derived data has been updated from the previous value. If it hasn't changed, `Derived` won't publish any changes. ### Chaining Derived Instances You can create another Derived instance from an existing Derived instance, effectively chaining them together: ```swift let anotherDerived: Derived = derived.derived(.map { $0.description }) ``` ### Subscribing to Derived Property Updates To subscribe to updates of a derived property, you can use the `sinkState` method, just like with a store: ```swift derived.sinkState { value in // Handle updates of the derived property } .storeWhileSourceActive() ``` By using `Derived` for computed properties and subscribing to updates, you can ensure that your application remains efficient and performant, avoiding unnecessary computations and state updates. # Normalization VergeGroup and Verge package provide a library for normalization techniques to handle entities in an efficient way. [Normalization](https://github.com/VergeGroup/Normalization) is a library that makes tables in a struct and manages value-type entities in copy-efficient tables. It can be used in the same way as handling value types. Which means we can use it with Verge bringing them into store-pattern. `VergeNormalizationDerived` target provides functionalities for subscribing entity updates when it's using with Verge. # Installation ## SwiftPM Verge supports SwiftPM. ## Thanks - [Redux](https://redux.js.org/) - [Vuex](https://vuex.vuejs.org/) - [ReSwift](https://github.com/ReSwift/ReSwift) - [swift-composable-architecture](https://github.com/pointfreeco/swift-composable-architecture) ## Author [🇯🇵 Muukii (Hiroshi Kimura)](https://github.com/muukii) ## License Verge is released under the MIT license. ================================================ FILE: Sources/Verge/Documentation.docc/Activity.md ================================================ # Activity ## What Activity brings to us Activity enables Event-driven partially. Verge supports to send any events that won’t be stored persistently. Even if an application runs with State-Driven, it might have some issues that not easy to something with State-Driven. For example, something that would happen with the timer’s trigger. It’s probably not easy to expressing that as a state. In this case, Activity helps that can do easily. This means Verge can use Event-Driven from Data-Driven partially. We think it’s not so special concept. SwiftUI supports these use cases as well that using Combine’s Publisher. ```swift func onReceive

(_ publisher: P, perform action: @escaping (P.Output) -> Void) -> some View where P : Publisher, P.Failure == Never ``` [Apple’s SwiftUI Ref](https://developer.apple.com/documentation/swiftui/view/3365935-onreceive) ## Add Activity to the Store In sample code following this: ```swift final class MyStore: StoreComponentType { struct State { ... } } ``` To enable using Activity, we add new decralation just like this: ```swift final class MyStore: StoreComponentType { struct State { ... } /// 👇 enum Activity { case didSendMessage } } ``` ## Send an Activity And finally, that Store now can emit an activity that we created. ```swift extension MyStore { func sendMessage() { send(.didSendMessage) } } ``` --- ## Subscribe the Activity **Normal** ```swift store.sinkActivity { activity in ... } .store(in: &subscriptions) ``` **Using Combine** ```swift store .activityPublisher .sink { event in // do something } .store(in: &subscriptions) ``` ================================================ FILE: Sources/Verge/Documentation.docc/Changes.md ================================================ # ``Verge/Changes`` `Changes` wraps primitive value inside and previous one. This data structure enables to get differences between now one and previous one. It helps us to prevent duplicated operation with the same value from the state. ![Changes structure](changes) ## How we update UI with the state uniquely in UIKit In subscribing the state and binding UI, it’s most important to reduce the meaningless time to update UI. What things are the meaningless? that is the update UI operations which contains no updates. Basically, we can prevent this with like followings: ```swift func updateUI(newState: State) { if self.label.text != newState.name { self.label.text = newState.name } } ``` Although, this approach make the code a little bit complicated by increasing the code that updates UI. Especially, same words come up a lot. ## Changes simplify the code Actually, state property of Store returns `Changes` implicitly. From the power of `dynamicMemberLookup`, we can use the property of the State with we don’t know that is actually Changes object. How to know there is differences is using `ifChanged`. ```swift let store: MyStore let state: Changes = store.state state.ifChanged(\.name) { name in // called when `name` changed only } ``` ## Patterns of `ifChanged` `ifChanged` has a bunch of overloads. Let’s take a look of patterns. ```swift state.ifChanged(\.aaa, \.bbb) { aaa, bbb in // Executes every two properties change. } ``` ```swift state.ifChanged({ "Mr." + $0.name }) { composedName in } ``` ```swift state.ifChanged({ "Mr." + $0.name }, { $0 == $1 }) { aaa, bbb in } ``` ```swift state.ifChanged({ ($0.aaa, $0.bbb, "Mr" + $0.name)}, ==) { composedName in // Returning tuple that contains composed value enables to do anything with the value you want. // It releases us from complicated streaming mixing. // Current Swift version does not support tuple conforming with Equatable, // You need passing `==` function in the second argument. } ``` ## Subscribing the state ```swift class ViewController: UIViewController { var subscriptions = Set() let store: MyStore override func viewDidLoad() { super.viewDidLoad() store.sinkChanges { [weak self] (changes) in // it will be called on the thread which committed self?.update(changes: changes) } .store(in: &subscriptions) } private func update(changes: Changes { changes.ifChanged(\.name) { name in // called only name changed } ... } } ``` ## Make Changes object the first-time value If you have a `Changes` from anywhere, it might have previous value, Using `ifChanged` might return false because compared with the previous one. You can create the Changed object that always returns true from `ifChanged` with followings: ```swift let changes: Changes let firstTimeChanges: Changes = changes.droppedPrevious() ```# <#Title#> ================================================ FILE: Sources/Verge/Documentation.docc/ComputedProperty.md ================================================ # Computed Property ## Overview A declaration to add a computed-property into the state. It helps to add a property that does not need to be stored-property. It’s like Swift’s computed property like following: ```swift struct State { var items: [Item] = [] { var itemsCount: Int { items.count } } ``` However, this Swift’s computed-property will compute the value every state changed. It might become a serious issue on performance. Compared with Swift’s computed property and this, this does not compute the value every state changes, It does compute depend on specified rules. That rules mainly come from the concept of Memoization. Example code: ```swift struct State: ExtendedStateType { var name: String = ... var items: [Int] = [] struct Extended: ExtendedType { static let instance = Extended() let filteredArray = Field.Computed<[Int]> { $0.items.filter { $0 > 300 } } .dropsInput { $0.noChanges(\.items) } } } ``` ```swift let store: MyStore = ... let state = store.state let result: [Int] = state.computed.filteredArray ``` ## Instructions ### Computed Property on State States may have a property that actually does not need to be stored property. In that case, we can use computed property. Although, we should take care of the cost of the computing to return value in that. What is that case? Followings explains that.

For example, there is itemsCount. ```swift struct State { var items: [Item] = [] var itemsCount: Int = 0 } ``` In order to become itemsCount dynamic value, it needs to be updated with updating items like this. ```swift struct State { var items: [Item] = [] { didSet { itemsCount = items.count } } var itemsCount: Int = 0 } ``` We got it, but we don’t think it’s pretty simple. Actually we can do this like this. ```swift struct State { var items: [Item] = [] { var itemsCount: Int { items.count } } ``` With this, it did get to be more simple. ```swift struct State { var items: [Item] = [] var processedItems: [ProcessedItem] { items.map { $0.doSomeExpensiveProcessing() } } } ``` As an example, Item can be processed with the something operation that takes expensive cost. We can replace this example with filter function. This code looks is very simple and it has got data from source of truth. Every time we can get correct data. However we can look this takes a lot of the computing resources. In this case, it would be better to use didSet and update data. ```swift struct State { var items: [Item] = [] { didSet { processedItems = items.map { $0.doSomeExpensiveProcessing() } } } var processedItems: [ProcessedItem] = [] } ``` However, as we said, this approach is not simple. And this can not handle easily a case that combining from multiple stored property. Next introduces one of the solutions. ## Extended Computed Properties VergeStore has a way of providing computed property with caching to reduce taking computing resource. Keywords are: - `ExtendedStateType` - `ExtendedType` - `Field.Computed` Above State code can be improved like following. ```swift struct State: ExtendedStateType { var name: String = ... var items: [Int] = [] struct Extended: ExtendedType { static let instance = Extended() let filteredArray = Field.Computed<[Int]> { $0.items.filter { $0 > 300 } } .dropsInput { $0.noChanges(\.items) } } } ``` To access that computed property, we can do the followings: ```swift let store: MyStore = ... let state = store.state let result: [Int] = state.computed.filteredArray ``` `store.computed.filteredArray` will be updated only when items updated. Since the results are stored as a cache, we can take value without computing. Followings are the steps describes when it computes while paying the cost. ```swift let store: MyStore = ... // It computes store.state.computed.filteredArray // no computes because results cached with first-time access store.state.computed.filteredArray // State will change but no affects items store.commit { $0.name = "Muukii" } // no computes because results cached with first-time access store.state.computed.filteredArray // State will change with it affects items store.commit { $0.items.append(...) } // It computes new value store.state.computed.filteredArray ```# <#Title#> ================================================ FILE: Sources/Verge/Documentation.docc/Derived.md ================================================ # ``Verge/Derived`` Derived’s functions are: - Computes the derived data from the state tree - Emit the updated data with updating Store - Supports subscribe the data - Supports Memoization - Compatible with SwiftUI’s observableObject and `StateReader` ## Overview ### Create a Derived object from the Store **Select a tree from the state** ```swift let derived: Derived = store.derived(.map(\.count)) ``` ```swift // we can write also this. // However, we recommend do above way as possible // because it enables cache. let derived: Derived = store.derived(.map { $0.count }) ``` **Technically, above method callings are produced from below declaration.** ```swift extension StoreType { public func derived( _ memoizeMap: MemoizeMap, NewState>, dropsOutput: ((Changes) -> Bool)? = nil, queue: TargetQueue? = nil ) -> Derived } ``` `MemoizeMap` manages to transform value from the state and keep performance that way of drops transform operations if the input value no changes. **Compute a value from the state** Derived can create any type of value what we need. MemoizeMap ```swift let derived = store.derived( .map(derive: { ($0.name, $0.age) }, dropsDerived: == ) { args in let (name, age) = args ... return ... }) ``` **Most manually way of creating a Derived object** We can create fully tuned up Derived object with using custom initialized `MemoizedMap`. Most of the cases, we don’t need to do this. Because several overloaded methods enable optimizations automatically that depending on doing things. Verge shows current optimization status from the Complexity column of Xcode documentation. ![https://user-images.githubusercontent.com/1888355/83332811-41df2480-a2d8-11ea-8da0-d86c127fc926.png](https://user-images.githubusercontent.com/1888355/83332811-41df2480-a2d8-11ea-8da0-d86c127fc926.png) ## Take a value Derived is an object (reference type). It provides a latest value from a store. This supports getting the value ad-hoc or subscribing the value updating. Derived allows us to take the latest value at the time. ``` let value: Changes = derived.value ``` ## Subscribe the latest value Derived provides Derived allows us to subscribe to the updated value. ```swift let cancellable = derived.sinkValue { (changes: Changes) in } ``` Please, carefully handle a cancellable object. A concealable object that returns that subscribe method is similar to AnyCancellable of Combine.framework. We need to retain that until we don’t need to get the update event. ## Supports other Reactive Frameworks We might need to use some Reactive framework to integrate other sequence. Derived allows us to make to a sequence from itself. Currently, it supports Combine.framework and RxSwift.framework. ### + Combine ```swift derived .valuePublisher() .sink { (changes: Changes) in } ``` ### + RxSwift 💡You need to install VergeRx module to use this. ```swift derived.rx .changesObservable() .subscribe(onNext: { (changes: Changes) in }) ``` ## Memoization to keep good performance Mostly Derived is used for projecting the specified shape from the source object. And some cases may contain an expensive operation. In that case, we can consider to tune Memoization up. We can see the detail of Memoization from below link. [Wiki - Memoization](https://en.wikipedia.org/wiki/Memoization) ## Skips the map operation if the source state has no changes In create Derived method, we can get the detail that how we suppress the no need updating and updated event. ```swift extension StoreType { public func derived( _ memoizeMap: MemoizeMap, NewState>, dropsOutput: @escaping (Changes) -> Bool = { _ in false } ) -> Derived } ``` ================================================ FILE: Sources/Verge/Documentation.docc/Dispatcher.md ================================================ # ``Verge/DispatcherType`` Dispatcher allows us to update the state of the Store from away the store and to manage dependencies to create Mutation. - Dispatcher is - it is an object. - it does not have its own state. - it can run commit with specified store’s state. - it can have a temporary dependency to commit the mutation. - it can focus on specified tree of the state. Here is an example store, in this section we create a dispatcher to commit the mutation into this Store: ```swift struct State: StateType { var count: Int = 0 } enum Activity { case happen } final class MyStore: Store { ... } ``` MyStore has a typealias to define a dispatcher: ```swift MyStore.Dispatcher ``` ## Define a dispatcher Let’s take a look how we create a dispatcher with using the typealias: ```swift final class MyDispatcher: MyStore.Dispatcher { } ``` Now we can create an instance of `MyDispatcher`: ```swift let store = MyStore() let dispatcher = MyDispatcher(targetStore: store) ``` ## Add an action to the dispatcher Next, we add an action that commits a mutation: ```swift final class MyDispatcher: MyStore.Dispatcher { func doSomething() { commit { $0.count = 100 } } } ``` ```swift let store: MyStore let dispatcher: MyDispatcher dispatcher.doSomething() store.state.count == 100 // true ``` ## Add a dependency to dispatch an action In the case of large applications, we need to handle many dependencies to run the application. For example, if we use multiple HTTP clients. ```swift final class MyDispatcher: MyStore.Dispatcher { let apiClient: APIClient init(apiClient: APIClient, targetStore: Store) { self.apiClient = apiClient super.init(targetStore: targetStore) } // an example of fetching data and commit func fetchData() { apiClient.fetchData { [weak self] result in switch result { case .success(let data): let items = data.encode(...) self?.commit { $0.fetchedItems = items } case .failure(let error): // handles error } } } } ``` To use this: ```swift let store = MyStore() let apiClient = APIClient() let dispatcher = MyDispatcher(apiClient: apiClient, target: store) dispatcher.fetchData() ``` Now we can handle multiple kinds of dependencies each it fits itself life-time. For example, if you have a restriction that some dependencies can be created only the user’s logging-in, you can create a dispatcher what is for. Next section explains the detail. ## Create multiple Dispatcher ![https://user-images.githubusercontent.com/1888355/82821486-28586a00-9edf-11ea-8c98-062eafcc4f16.png](https://user-images.githubusercontent.com/1888355/82821486-28586a00-9edf-11ea-8c98-062eafcc4f16.png) Creating a dispatcher does not have the restriction of the number of instances or types. This means that it allows us to define a dispatcher and instantiate an instance of the dispatcher to fill the use-case. For example, In case the timing of getting dependencies that to be needed by run Action or Mutation is different, it’s not easy to have them in the one dispatcher with type-safety. they must be optional types. Using creating multiple dispatchers techniques solves this case by defines the dispatcher each the timing of getting dependencies. ```swift class LoggedInDispatcher: MyStore.Dispatcher { let apiClientNeedsAuthToken: APIClient = ... ... } class LoggedOutDispatcher: MyStore.Dispatcher { let apiClientWithoutAuthToken: APIClient = ... ... } ``` ```swift let store = MyStore() let loggedInDispatcher = LoggedInDispatcher(...) let loggedOutDispatcher = LoggedOutDispatcher(...) ``` The application will create `LoggedInDispatcher` when the user is logged-in and deinitialize `LoggedOutDispatcher`. ## Create scoped dispatcher As another feature what Dispatcher provides, It supports to commit specified scope of the state which helps to mutate with focus on a part of the large state tree. Here is a sample state that assuming a large app. - AppState (MyStore) - db: Database - loggedIn: LoggedInState - myInfo: MyInfoState - loggedOut: LoggedOutState We have `database`, `logged-in` and `logged-out state`. `database` means normalized state shape to manage many entities. In previous section, that explained we can multiple dispatchers each the user’s state. However, it needs to the full path to mutate where we need to mutate. ```swift class LoggedInDispatcher: MyStore.Dispatcher { func performA() { commit { $0.loggedIn.xxx } } func performB() { commit { $0.loggedIn.xxx } } func performC() { commit { $0.loggedIn.xxx } } } ``` LoggedInDispatcher will often dispatch some action for logged-in-state. But it calls everytime `$0.loggedIn`, it seems a little bit verbosity. That will be solved by `ScopedDispatcher`. It will move on target tree of the state when it dispatch the action. The following code shows how we could create a `ScopedDispatcher`: ```swift final class LoggedInService: MyStore.ScopedDispatcher { init(store: Store) { super.init(targetStore: store, scope: \.loggedIn) } func someOperation() { commit { (state: LoggedInState) in ... } } } ``` In LoggedInService, commit mutates `LoggedInState` directly. Like this, we can create a dispatcher each use-cases. ### Detaching to other tree Just in case, `ScopedDispatcher` supports also mutating on other state tree. **Moving on more deeper** ```swift final class LoggedInService: MyStore.ScopedDispatcher { func detachingOperation() { let myInfo = detached(by: \.myInfo) myInfo.commit { (state: MyInfo) in } } } ``` **Detaches from root** ```swift final class LoggedInService: MyStore.ScopedDispatcher { func detachingOperation() { let db = detached(from: \.db) db.commit { (state: Database) in } } } ``` ================================================ FILE: Sources/Verge/Documentation.docc/Essentials/Motivation.md ================================================ # Motivation ## Verge focuses use-cases in the real-world Recently, we could say the unidirectional data flow is a popular architecture such as flux. ## Does flux architecture have a good performance? It depends. The performance will be the worst depends on how it is used. However, most of the cases, we don't know the app we're creating how it will grow and scales.While the application is scaling up, the performance might decrease by getting complexity.To keep performance, we need to tune it up with several approaches.Considering the performance takes time from the beginning.it will make us be annoying to use flux architecture. ## Verge is designed for use from small and supports to scale. Setting Verge up quickly, and tune-up when we need it. Verge automatically tune-up and shows us what makes performance badly while development from Xcode's documentation. For example, Verge provides these stuff to tune performance up. - Derived (Similar to [facebookexperimental/Recoil](https://github.com/facebookexperimental/Recoil)'s Selector) - ORM ## Supports volatile events - Activity We use an event as `Activity` that won't be stored in the state.This concept would help us to describe something that is not easy to describe as a state in the client application. ================================================ FILE: Sources/Verge/Documentation.docc/Guides/Advanced Usage.md ================================================ # Advanced Usage ## To keep performance and scalability ## Adding a cachable computed property in a State We can add a computed property in a state to get a derived value with stored property, and that computed property works fine as well other stored property. ```swift struct MyState { var items: [Item] = [] { var itemsCount: Int { items.count } } ``` However, this patterns might cause an expensive cost of operation depends on how they computes. To solve it, Verge arrows us to define the computed property with another approach. ```swift struct MyState: ExtendedStateType { var name: String = ... var items: [Int] = [] struct Extended: ExtendedType { let filteredArray = Field.Computed<[Int]> { $0.items.filter { $0 > 300 } } .ifChanged(selector: \.largeArray) } } ``` ```swift let store: MyStore store.changes.computed.filteredArray ``` This defined computed array calculates only if changed specified value. That condition to re-calculate is defined with `.ifChanged` method in the example code. And finally, it caches the result by first-time access and it returns cached value until if the source value changed. ## Making a slice of the state (Selector) We can create a slice object that derives a data from the state. ```swift let derived: Derived = store.derived(.map(\.count)) // take a value derived.value // subscribe a value changes derived.sinkChanges { (changes: Changes) in } ``` ## Creating a Dispatcher Store arrows us to define an action in itself, that might cause gain complexity in supporting a large application. To solve this, Verge offers us to create an object that dispatches an action to the store. We can separate the code of actions to keep maintainability. that also help us to manage a different type of dependencies. For example, the case of those dependencies different between logged-in and logged-out. ```swift class MyDispatcher: MyStore.Dispatcher { func moreOperation() { commit { ... } } } ``` ```swift let store: MyStore let dispatcher = MyDispatcher(target: store) dispatcher.moreOperation() ``` ================================================ FILE: Sources/Verge/Documentation.docc/Guides/Basic Usage.md ================================================ # Basic Usage This section shows you how we start to use Verge. It’s very basic usage. You need to read Advanced Usage section if you’re considering to use in production. To understand smoothly about Verge, we need to figure the following domains out. ## Domains ### Store - A storage object to manage a state and emit activities by the action. - Store can dispatch actions to itself. ### State - A type of state-tree that describes the data our feature needs. ### Activity (Optional) - A type that describes an activity that happens during performs the action. - This instance won’t be stored in anywhere. It would help us to perform something by event-driven. - Consider to use this depends on that if can be represented as a state. - For example, to present alert or notifcitaions by the action. ### Action - Action runs any operations (sync / async) and commits any mutations to the state of the store. - Action is described by Swift’s method in Store or Dispatcher. ### Dispatcher (Optional) - A type to dispatch an action to specific store. - For a large application, to separate the logics each domain. ## Setup a Store ### Define a state ```swift struct MyState { var count = 0 } ``` ### Define an activity ```swift enum MyActivity { case countWasIncremented } ``` `Activity` is not required type. If you don’t need to use `Activity`, you can set`Never` in Store’s type parameter. ### Define a store ```swift class MyStore: Store { init(dependency: Dependency) { super.init(initialState: .init(), logger: nil) } } ``` In example, it created a subclass of `Store`. Of course, we can also create an instance from `Store` without subclassing. But we can put some dependencies (e.g. API client) with creating a sub-class of `Store`. ## Add an action Next, add an action to mutate the state. Essentially, Verge uses Swift’s method to describe an action against enum or struct based action descriptor other Flux library has. This approach has advantages that adding an action faster and call it naturally and dispatches with a faster way by Swift’s native method dispatching system. - **Better Performance** - Swift can perform this action with Swift’s method dispatching instead switch-case computing. - **Returns anything we need** - the action can return anything from that action (e.g. state or result) - If that action dispatch async operation, it can return `Future` object. (such as Vuex action) As a future direction, Verge might get a dispatching action system with describing with enum or struct based to run action. However, the current approach would be the base system for it. We’re currently researching that needs. ```swift class MyStore: Store { func incrementCount() { commit { $0.count += 1 } } ``` Yes, this point is most different with Redux. we could say it close to Vuex. Store knows what the application’s needs. To mutate the state, we use `commit` method. the argument inside commit’s closure is `inout State`, you can modify it anything but you can’t put the asynchronous operations. If you need to do this, call `commit` from the asynchronous operation. like this: ```swift func incrementCount() { DispatchQueue.main.async { commit { $0.count += 1 } } } ``` ### Run the action For example, call that action. ```swift let store = MyStore(...) store.incrementCount() ``` ## Send an activity from the action ```swift func incrementCount() { ... send(.countWasIncremented) } ``` ## Binding the state with UI ### Use the store in SwiftUI To bind the state with `View`, it uses `StateReader`. Since `Store` is also compatible with `ObservableObject`, we can declare `@ObservedObject` or `@EnviromentObject`. `StateReader` provides several options to reduce no changes updates. Please check it out from Xcode. ```swift struct MyView: View { let store: MyStore var body: some View { StateReader(store).content { state in Text(state.name) } .onReceive(session.store.activityPublisher) { (activity) in ... } } } ``` ### Use the store in UIKit In UIKit, UIKit doesn’t work with differentiating. To keep better performance, we need to set a value if it’s changed. Verge publishes an object that contains previous state and latest state, `Changes` object would be so helpful to check if a value changed. ```swift class ViewController: UIViewController { let store: MyStore var cancellable: VergeAnyCancellable? init(store: MyStore) { ... self.cancellable = store.sinkState { [weak self] state in self?.update(state: state) } } private func update(state: Changes) { state.ifChanged(\.name) { (name) in nameLabel.text = name } state.ifChanged(\.age) { (age) in ageLabel.text = age.description } } } ``` ================================================ FILE: Sources/Verge/Documentation.docc/Guides/Migration Guide v9.md ================================================ # Changes in v9 ## Store requires Equatable State From v8 complex implementations, v9 becomes it requires Equatable to State associated with Store, Changes, Derived and what else related. Then Verge v9 now dropped lots of implementations and overloads covering cases if the state don’t have Equatable. ## Store can have multiple databases Now, Store has `databases` accessor that allows us to read database. `databases` is `DatabaseDynamicMembers` which provides property following to State shape. This looks up member only type of `DatabaseType` ```swift @dynamicMemberLookup public struct DatabaseDynamicMembers { unowned let store: Store init(store: Store) { self.store = store } public subscript(dynamicMember keyPath: KeyPath) -> DatabaseContext { .init(keyPath: keyPath, store: store) } } ``` ## Use new syntax for creating Field.Computed As you know, Changes supports memoized-computed-property. That can be done writing `Field.Computed` Now its writing syntax will change. ```swift let filteredArray = Field.Computed( .map( using: { $0.largeArray }, transform: { $0.filter { $0 > 300 } } ) ) ``` `using` specifies the dependencies which used from `transform` function. `transform` function will create a new value from given dependencies provided from `using` function. ## Detail changes - Dropped complex implementations related to performance tunings - Stopped using cache to return Derived internally. - Deleted `batchCommit` From v8 complex implementations, v9 becomes it requires Equatable to State associated with Store, Changes, Derived and what else related. Then Verge v9 now dropped lots of implementations and overloads covering cases if the state don’t have Equatable. ================================================ FILE: Sources/Verge/Documentation.docc/Mutation.md ================================================ # Mutation ## What Mutation is The only way to actually change state in a Store is by committing a mutation. Define a function that returns Mutation object. That expresses that function is Mutation Mutation does **NOT** allow to run asynchronous operation. ### Define mutations in the Store ```swift struct MyState { var todos: [TODO] = [] } class MyStore: Store { func addNewTodo(title: String) { commit { (state: inout InoutRef) in state.todos.append(Todo(title: title, hasCompleted: false)) } } } ``` ### Run Mutation ```swift let store = MyStore() store.addNewTodo(title: "Create SwiftUI App") print(store.state.todos) // store.state.todos => [Todo(title: "Create SwiftUI App", hasCompleted: false)] ``` ## Batches multiple commtis Committing multiple mutations in a short time might decrease performance. Because the subscribers around the store derive a state many times. Like this, ```swift class MyStore: Store { func myMutation() { if ... { commit { ... } // emits updated event } if ... { commit { ... } // emits updated event } if ... { commit { ... } // emits updated event } } } ``` To keep better performance, we need to keep using fewer commits in a short time. We have 2 ways. ### Using `commit` ``DispatcherType/commit(_:_:_:_:mutation:)`` provides ``InoutRef``, that can detect how the wrapped state will change. If there is no change, `commit` does nothing and no emitting the events from the Store. However, you should attention `commit` is atomically operation which means, the Store getting lock while committing. ```swift func myMutation() { commit { (state: inout InoutRef) in if ... { state.aaa = ... } if ... { state.bbb = ... } if ... { state.ccc = ... } } } ``` ================================================ FILE: Sources/Verge/Documentation.docc/Resources/Tiny.md ================================================ # Yet another super tiny store pattern with Verge/Tiny In fact, `store-pattern` doesn't need something library to run. The actually necessary thing is **the changing detection in UIKit.** Without the changing detection, the code is here. There is no dependencies. ```swift class MyView: UIView { private struct State { var count: Int = 0 } private var state: State { didSet { update(with: state) } } private func update(with state: State) { ... } } ``` Next, we focus on `update(with:)` method. Try to simulate updating the label's value. ```swift private func update(with state: State) { myLabel.text = "\(state.count)" } ``` As you can see, you will think you want to prevent updating the value until the value changed. ## Use Verge.Tiny module to prevent the duplicated updating. With installing `Verge/Tiny` module, we can write up like followings. ```swift private func update(with state: State) { associatedProperties.doIfChanged(state.count) { count in myLabel.text = "\(count)" } } ``` `associatedProperties` is a storage of the values that associated with its owner object(NSObject). `doIfChanged` gets the location of the code that would be a unique key by composition in the storage. With this functions, we can get a filter anywhere in the object. However, this function might affect code readabilities in Swift. Please carefully using this. We recommend you gather those operations into one place. ================================================ FILE: Sources/Verge/Documentation.docc/State.md ================================================ # Thinking in single state tree (Not enforced) VergeStore uses a **single state-tree. (Recommended)** That means an object contains all of the application’s state. With this, we can get to achieve **“single source of truth”** That state is managed by ``Store``. It process updating the state and notify updated events to the subscribers. > Tip: Store DOES support multiple state-tree as well. Depending on the case, we can create another Store instance. ## Add a computed property ``` struct State: Equatable { var count: Int = 0 var countText: String { return count.description } } ``` Extending properties that computes a value from stored property. Although in some of cases, the cost of computing might be higher which depends on how it create the value from stored properties. There is ``ExtendedStateType``. This provies us to get more stuff that **increases performance** and productivity. ## Attention to Normalization **If you put the data that has relation-ship or complicated structure into state tree, it would be needed normalization to keep performance. Please check VergeORM module** [About more Normalization and why we need to do this](https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape/) ================================================ FILE: Sources/Verge/Documentation.docc/Verge.Store.md ================================================ # ``Verge/Store`` - Store: - Should be a reference type object, - To share the state they manage to multiple subscribers. - Receives **Mutation** to update the state with thread-safety - Compatible with SwiftUI’s observableObject and we can use `StateReader` to read the state partially. ## Ways to creating a store Verge provides 2 ways to create a store. 1. Declare a class that conforms with `StoreComponentType` - It’s a protocol that indicates the class wraps a store inside and behaves like a store. 2. Subclassing from `Store` - a most basic way, but we need to define State and Activity outside Now, we recommend using No.1 in order to manage the source code with better portability. ## Declare a class that conforms with `StoreComponentType` ```swift final class MyStore: StoreComponentType { struct State: StateType { var count: Int = 0 } /// This means wrapping store inside. (Probably it should be renamed as like `innerStore` or `wrappedStore`) /// `DefaultStore` is a typealias that declared by `StoreComponentType`. /// You can use any class that inherited from `Store` for your use-cases. let store: DefaultStore init() { self.store = .init(initialState: .init()) } } ``` ### Add a Mutation ```swift extension MyStore { func increment() { commit { $0.count += 0 } } } ``` ### Commit the mutation ```swift let store = MyStore() store.increment() ``` ## Subclassing from `Store` ```swift struct State: StateType { var count: Int = 0 } enum Activity { case happen } final class MyStore: Store { init() { super.init( initialState: .init(), logger: DefaultStoreLogger.shared ) } } ``` ### Add a Mutation ```swift extension MyStore { func increment() { commit { $0.count += 0 } } } ``` ### Commit the mutation ```swift let store = MyStore() store.increment() ``` ================================================ FILE: Sources/Verge/Documentation.docc/Verge.md ================================================ # ``Verge`` ## 🛹 Small start unidirectional data flow Verge is designed for use from small and supports to scale. Setting Verge up quickly, and tune-up when we need it. ## 🏎 Focus on performance Does flux have a good performance?The performance will be the worst depends on how it is used.Verge automatically tune-up and shows us how we could gain a performant. ## ⛱ Available in **UIKit** and **SwiftUI** Verge supports both of UI framework. Especially, it highly supports to update partially UI on UIKit. ## Verge supports small start and scaling up Verge is a state management library for iOS (UIKit / SwiftUI).Mostly it's based on Flux architecture.Flux architecture is so beautiful and simplified thinking.Although, we need to do several tuning to bring it into a real product.In fact, Flux needs to consider about computing resources. Verge contains several ideas to do this from Web technologies such as Redux, Vuex, and Recoil.They have very useful techniques to be successful in real-world productions based on the core-concept of Flux. Verge can be setting it up quickly, and tune performance up when we need it.Verge automatically tune-up as possible and shows us what makes performance badly while development from Xcode's documentation. ## At a glance A way to create a ViewModel ```swift final class MyViewModel: StoreComponentType { struct State: Equatable { var name: String = "" var count: Int = 0 } let store: DefaultStore = .init(initialState: .init()) func myAction() { commit { $0.name = "Hello, Verge" } } func increment() { commit { $0.count += 1 } } } ``` A way to create a customized store ```swift struct MyState: Equatable { var name: String = "" var count: Int = 0 } final class MyStore: Store { func myAction() { commit { $0.name = "Hello, Verge" } } func increment() { commit { $0.count += 1 } } } ``` ### SwiftUI ```swift struct MyView: View { let store: MyViewModel var body: some View { StateReader(store) { state in Text(state.name) Button(action: { self.store.myAction() }) { Text("Action") } } } } ``` ### UIKit ```swift final class MyViewController: UIViewController { let viewModel: MyViewModel ... var cancellable: VergeAnyCancellable? init(viewModel: MyViewModel) { self.viewModel = viewModel self.cancellable = viewModel.sinkState { [weak self] state in self?.update(state: state) } } private func update(state: Changes) { state.ifChanged(\.name) { (name) in nameLabel.text = name } state.ifChanged(\.count) { (age) in countLabel.text = age.description } ... } } ``` ## Prepare moving to SwiftUI from now with Verge SwiftUI's concept is similar to the concept of React, Vue, and Elm.Therefore, the concept of state management will become to be similar as well. That is Redux or Vuex and more. Now, almost of iOS Applications are developed on top of UIKit.And We can't say SwiftUI is ready for top production.However, it would change. It's better to use the state management that fits SwiftUI from now. It's not only for that, current UIKit based applications can get more productivity as well. ## Questions? We accept your questions about usage of Verge and something else in GitHub Issues. 日本語での質問も大丈夫です ## Topics ### Guides - - - ### Extras - ### Migrations - ### Essentials - ``Verge/Store`` - ``Verge/Changes`` - ``Verge/Derived`` - ``Verge/DispatcherType`` ================================================ FILE: Sources/Verge/Library/BackgroundDeallocationQueue.swift ================================================ // // Copyright (c) 2020 Hiroshi Kimura(Muukii) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import DequeModule actor BackgroundDeallocationQueue { private var buffer: Deque> = .init() func releaseObjectInBackground(object: AnyObject) { let innerCurrentRef = Unmanaged.passRetained(object) let isFirstEntry = buffer.isEmpty buffer.append(innerCurrentRef) if isFirstEntry { Task { // accumulate objects to dealloc for batching try? await Task.sleep(nanoseconds: 1_000_000) await self.drain() } } } func drain() async { guard buffer.isEmpty == false else { return } while let pointer = buffer.popFirst() { pointer.release() await Task.yield() } await drain() } } ================================================ FILE: Sources/Verge/Library/CachedMap.swift ================================================ // // Copyright (c) 2020 muukii // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation @available(*, deprecated, renamed: "InstancePool") public typealias CachedMapStorage = InstancePool /// A storage object that retains projected instance from source by identified key. public final class InstancePool: @unchecked Sendable { struct Artifact { let source: Source var value: Target } private let generateKey: @Sendable (Source) -> AnyHashable private let updateCondition: @Sendable (Source, Source) -> Bool private var innerStorage: [AnyHashable: Artifact] = [:] private let outerLock = NSLock() /// Creates an instance /// /// - Parameters: /// - keySelector: A closure that gives key to identify the mapped instance. /// - shouldUpdate: A closure that indicates whether cached one replace with new mapped instance. public init( keySelector: @escaping @Sendable (Source) -> Key, shouldUpdate: @escaping @Sendable (_ cached: Source, _ new: Source) -> Bool = { _, _ in false } ) { self.updateCondition = shouldUpdate self.generateKey = { AnyHashable(keySelector($0)) } } public func purgeCache() { outerLock.lock() defer { outerLock.unlock() } innerStorage = [:] } public func map( from collection: C, sweepsUnused: Bool, makeNew: (C.Element) throws -> Target, update: (C.Element, inout Target) -> Void ) rethrows -> [Target] where C.Element == Source { outerLock.lock() defer { outerLock.unlock() } let keys = collection.map(generateKey) let result = try zip(collection, keys).map { (element, key) -> Target in if let cached = innerStorage[key], !updateCondition(cached.source, element) { update(element, &innerStorage[key]!.value) return innerStorage[key]!.value } let newObject = try makeNew(element) innerStorage[key] = Artifact(source: element, value: newObject) return newObject } if sweepsUnused { let unusedKeys = Set(innerStorage.keys).subtracting(keys) for key in unusedKeys { innerStorage.removeValue(forKey: key) } } return result } public func compactMap( from collection: C, sweepsUnused: Bool, makeNew: (C.Element) throws -> Target?, update: (C.Element, inout Target) -> Void ) rethrows -> [Target] where C.Element == Source { outerLock.lock() defer { outerLock.unlock() } let keys = collection.map(generateKey) let result = try zip(collection, keys).compactMap { (element, key) -> Target? in if let cached = innerStorage[key], !updateCondition(cached.source, element) { update(element, &innerStorage[key]!.value) return innerStorage[key]!.value } guard let newObject = try makeNew(element) else { return nil } innerStorage[key] = Artifact(source: element, value: newObject) return newObject } if sweepsUnused { let unusedKeys = Set(innerStorage.keys).subtracting(keys) for key in unusedKeys { innerStorage.removeValue(forKey: key) } } return result } } extension Collection { /** Returns an array containing the results of mapping the given closure over the sequence’s elements. Especially, it uses a cached instance to return. You can set your expectation on how it caches from creating `CachedMapStorage`. It helps to create a store object or view model from immutable data. - Author: Verge */ public func cachedMap( using pool: InstancePool, sweepsUnused: Bool = false, makeNew: (Self.Element) throws -> U, update: (Self.Element, inout U) -> Void = { _, _ in } ) rethrows -> [U] { return try pool.map(from: self, sweepsUnused: sweepsUnused, makeNew: makeNew, update: update) } /** Returns an array containing the results of mapping the given closure over the sequence’s elements. Especially, it uses a cached instance to return. You can set your expectation on how it caches from creating `CachedMapStorage`. It helps to create a store object or view model from immutable data. - Author: Verge */ public func cachedCompactMap( using pool: InstancePool, sweepsUnused: Bool = false, makeNew: (Self.Element) throws -> U?, update: (Self.Element, inout U) -> Void = { _, _ in } ) rethrows -> [U] { return try pool.compactMap(from: self, sweepsUnused: sweepsUnused, makeNew: makeNew, update: update) } } ================================================ FILE: Sources/Verge/Library/EventEmitter.swift ================================================ // // Copyright (c) 2019 muukii // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Atomics import Combine import DequeModule import Foundation import os public final class EventEmitterCancellable: Hashable, Cancellable, @unchecked Sendable { public static func == (lhs: EventEmitterCancellable, rhs: EventEmitterCancellable) -> Bool { lhs === rhs } private struct State { weak var owner: EventEmitterType? var wasCancelled: Bool = false } private let state: VergeConcurrency.ManagedCriticalState fileprivate init(owner: EventEmitterType) { self.state = VergeConcurrency.ManagedCriticalState(State(owner: owner)) } public func hash(into hasher: inout Hasher) { ObjectIdentifier(self).hash(into: &hasher) } public func cancel() { let continues: EventEmitterType? = state.withCriticalRegion { guard $0.wasCancelled == false else { return nil } $0.wasCancelled = true return $0.owner } continues?.removeEventHandler(self) } } protocol EventEmitterType: AnyObject, Sendable { func removeEventHandler(_ token: EventEmitterCancellable) } public protocol EventEmitterEventType { func onComsume() } /// Instead of Combine open class EventEmitter: EventEmitterType, @unchecked Sendable { public var publisher: Publisher { return .init(eventEmitter: self) } private var subscribers: VergeConcurrency.UnfairLockAtomic<[EventEmitterCancellable: (Event) -> Void]> = .init([:]) private let queue: VergeConcurrency.UnfairLockAtomic> = .init(.init()) private let flag = ManagedAtomic.init(false) private var deinitHandlers: VergeConcurrency.UnfairLockAtomic<[() -> Void]> = .init([]) public init() { } deinit { deinitHandlers.value.forEach { $0() } } @_spi(EventEmitter) public func accept(_ event: consuming Event) { /** https://github.com/VergeGroup/Verge/pull/220 https://github.com/VergeGroup/Verge/issues/221 https://github.com/VergeGroup/Verge/pull/222 */ // delivers a given event for subscribers at this point. let capturedSubscribers = subscribers.value queue.modify { $0.append(event) } if flag.compareExchange(expected: false, desired: true, ordering: .sequentiallyConsistent) .exchanged { while let event: Event = queue.modify({ if $0.isEmpty == false { return $0.removeFirst() } else { return nil } }) { // Emits receiveEvent(event) for subscriber in capturedSubscribers { vergeSignpostEvent("EventEmitter.emitForSubscriber") subscriber.1(event) } event.onComsume() } /** might contain a bug in here? a conjunction of enqueue and dequeue */ _ = flag.compareExchange(expected: true, desired: false, ordering: .sequentiallyConsistent) } else { // enqueue only } } open func receiveEvent(_ event: consuming Event) { } @_spi(EventEmitter) @discardableResult public func addEventHandler(_ eventReceiver: @escaping (Event) -> Void) -> EventEmitterCancellable { let token = EventEmitterCancellable(owner: self) subscribers.modify { $0[token] = eventReceiver } return token } func removeEventHandler(_ token: EventEmitterCancellable) { var itemToRemove: ((Event) -> Void)? = nil subscribers.modify { itemToRemove = $0[token] $0.removeValue(forKey: token) } // To avoid triggering deinit inside removing operation // At this point, deallocation will happen, then ``EventEmitterCancellable` runs operations. // subscribers is using unfair-lock means it's not recursive lock. // if removes the item then deallocated inside locking, onDeinit handler runs then entering this method recursively potentially by some others. // then unfair-lock raises runtime error. withExtendedLifetime(itemToRemove, {}) } public func onDeinit(_ onDeinit: @escaping () -> Void) { deinitHandlers.modify { $0.append(onDeinit) } } } extension EventEmitter { @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public struct Publisher: Combine.Publisher { public typealias Output = Event public typealias Failure = Never private weak var eventEmitter: EventEmitter? public init(eventEmitter: EventEmitter) { self.eventEmitter = eventEmitter } public func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input { let subscription = Subscription(subscriber: subscriber, eventEmitter: eventEmitter) subscriber.receive(subscription: subscription) } } @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public struct Subscription: Combine.Subscription where S.Input == Event { public let combineIdentifier: CombineIdentifier = .init() private let subscriber: S private let eventEmitterSubscription: EventEmitterCancellable? private weak var eventEmitter: EventEmitter? init(subscriber: S, eventEmitter: EventEmitter?) { self.subscriber = subscriber self.eventEmitter = eventEmitter self.eventEmitterSubscription = eventEmitter? .addEventHandler { (event) in _ = subscriber.receive(event) } } public func request(_ demand: Subscribers.Demand) { // TODO: implement } public func cancel() { guard let eventEmitterSubscription else { return } eventEmitter?.removeEventHandler(eventEmitterSubscription) } } } ================================================ FILE: Sources/Verge/Library/InoutRef.swift ================================================ // // Copyright (c) 2020 muukii // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import StateStruct public enum Modification { case graph(PropertyNode) case indeterminate } public struct InoutRef: ~Copyable { // MARK: - Properties nonisolated(unsafe) private let pointer: UnsafeMutablePointer /// A wrapped value /// You may use this property to call the mutating method which `Wrapped` has. public var wrapped: Wrapped { _read { yield pointer.pointee } _modify { yield &pointer.pointee } } private(set) var modification: Modification? public var hasModified: Bool { guard let modification else { return false } switch modification { case .graph(let graph): return !graph.isEmpty case .indeterminate: return true } } // MARK: - Initializers /** Creates an instance You should take care of using the pointer of value. Using always `withUnsafeMutablePointer` to pass it, otherwise Swift might crash with Memory error. */ init(_ pointer: UnsafeMutablePointer) { self.pointer = pointer } deinit { (pointer.pointee as? TrackingObject)?.endTracking() } // MARK: - Functions public mutating func modify(_ perform: (inout Wrapped) throws -> T) rethrows -> T { if pointer.pointee is TrackingObject { let identifier = Token() (pointer.pointee as! TrackingObject)._tracking_context.identifier = identifier let result = try perform(&pointer.pointee) let resultIdentifier = (pointer.pointee as! TrackingObject)._tracking_context.identifier (pointer.pointee as! TrackingObject)._tracking_context.identifier = nil guard Optional(identifier) == resultIdentifier else { // replacing instance itself happened. modification = .indeterminate return result } if var modifyingResult = (pointer.pointee as! TrackingObject).trackingResult { modifyingResult.graph.shakeAsWrite() let graph = modifyingResult.graph self.modification = .graph(graph) //#if DEBUG // Log.writeGraph.debug("Modified: \(graph.prettyPrint())") //#endif } else { modification = .indeterminate } return result } else { let r = try perform(&pointer.pointee) modification = .indeterminate return r } } } private final class Token: Hashable { static func == (lhs: Token, rhs: Token) -> Bool { lhs === rhs } func hash(into hasher: inout Hasher) { ObjectIdentifier(self).hash(into: &hasher) } } /// Do not retain on anywhere. @dynamicMemberLookup public final class ReadRef { private let pointer: UnsafePointer /// A wrapped value /// You may use this property to call the mutating method which `Wrapped` has. public var wrapped: Wrapped { _read { yield pointer.pointee } } // MARK: - Initializers /** Creates an instance You should take care of using the pointer of value. Using always `withUnsafeMutablePointer` to pass it, otherwise Swift might crash with Memory error. */ public init(_ pointer: UnsafePointer) { self.pointer = pointer } // MARK: - Functions public subscript(dynamicMember keyPath: KeyPath) -> T { _read { yield pointer.pointee[keyPath: keyPath] } } public subscript(dynamicMember keyPath: KeyPath) -> T? { _read { yield pointer.pointee[keyPath: keyPath] } } public subscript(keyPath keyPath: KeyPath) -> T { _read { yield pointer.pointee[keyPath: keyPath] } } public subscript(keyPath keyPath: KeyPath) -> T? { _read { yield pointer.pointee[keyPath: keyPath] } } func map(keyPath: KeyPath, perform: (inout ReadRef) throws -> Result) rethrows -> Result { try withUnsafePointer(to: pointer.pointee[keyPath: keyPath]) { (pointer) in var ref = ReadRef.init(pointer) return try perform(&ref) } } func map(keyPath: KeyPath, perform: (inout ReadRef?) throws -> Result) rethrows -> Result { guard pointer.pointee[keyPath: keyPath] != nil else { var _nil: ReadRef! = .none return try perform(&_nil) } return try withUnsafePointer(to: pointer.pointee[keyPath: keyPath]!) { (pointer) in var ref: ReadRef! = ReadRef.init(pointer) return try perform(&ref) } } } ================================================ FILE: Sources/Verge/Library/Log.swift ================================================ // // Log.swift // VergeCore // // Created by muukii on 2020/02/24. // Copyright © 2020 muukii. All rights reserved. // import Foundation import os.log enum Log { static let store = Logger(OSLog.makeOSLogInDebug { OSLog.init(subsystem: "Verge", category: "store") }) static let storeCommit = Logger(OSLog.makeOSLogInDebug { OSLog.init(subsystem: "Verge", category: "store.commit") }) static let storeReader = Logger(OSLog.makeOSLogInDebug { OSLog.init(subsystem: "Verge", category: "storeReader") }) static let reading = Logger(OSLog.makeOSLogInDebug { OSLog.init(subsystem: "Verge", category: "reading") }) static let writeGraph = Logger(OSLog.makeOSLogInDebug { OSLog.init(subsystem: "Verge", category: "writeGraph") }) } extension OSLog { @inline(__always) fileprivate static func makeOSLogInDebug(isEnabled: Bool = true, _ factory: () -> OSLog) -> OSLog { #if DEBUG return factory() #else return .disabled #endif } } ================================================ FILE: Sources/Verge/Library/Signpost.swift ================================================ import os import Foundation nonisolated(unsafe) public var _verge_signpost_enabled = ProcessInfo.processInfo.environment["VERGE_SIGNPOST_ENABLED"] != nil @usableFromInline enum SignpostConstants { @usableFromInline static let performanceLog = { () -> OSLog in if #available(iOSApplicationExtension 13.0, *) { return OSLog(subsystem: "lib.verge", category: "performance") } else { return OSLog(subsystem: "lib.verge", category: "performance") } }() @usableFromInline static let pointOfInterestLog = OSLog(subsystem: "lib.verge", category: .pointsOfInterest) } @inlinable public func vergeSignpostEvent(_ event: StaticString) { #if DEBUG if _verge_signpost_enabled { let id = OSSignpostID(log: SignpostConstants.pointOfInterestLog) os_signpost(.event, log: SignpostConstants.pointOfInterestLog, name: event, signpostID: id) } #endif } @inlinable public func vergeSignpostEvent(_ event: StaticString, label: @autoclosure () -> String) { #if DEBUG if _verge_signpost_enabled { let id = OSSignpostID(log: SignpostConstants.pointOfInterestLog) os_signpost(.event, log: SignpostConstants.pointOfInterestLog, name: event, signpostID: id, "%@", label()) } #endif } public struct VergeSignpostTransaction { #if DEBUG @usableFromInline let _end: () -> Void public let rawID: os_signpost_id_t #endif public init(_ name: StaticString) { #if DEBUG if _verge_signpost_enabled { let id = OSSignpostID(log: SignpostConstants.performanceLog) self.rawID = id.rawValue os_signpost(.begin, log: SignpostConstants.performanceLog, name: name, signpostID: id) _end = { os_signpost(.end, log: SignpostConstants.performanceLog, name: name, signpostID: id) } } else { rawID = 0 _end = {} } #else #endif } public init(_ name: StaticString, label: @autoclosure () -> String) { #if DEBUG if _verge_signpost_enabled { let id = OSSignpostID(log: SignpostConstants.performanceLog) self.rawID = id.rawValue let _label = label() os_signpost(.begin, log: SignpostConstants.performanceLog, name: name, signpostID: id, "Begin: %@", _label) _end = { os_signpost(.end, log: SignpostConstants.performanceLog, name: name, signpostID: id, "End: %@", _label) } } else { rawID = 0 _end = {} } #else #endif } public func event(name: StaticString, label: @autoclosure () -> String) { #if DEBUG if _verge_signpost_enabled { let id = OSSignpostID(rawID) os_signpost(.event, log: SignpostConstants.pointOfInterestLog, name: name, signpostID: id, "%@", label()) } #endif } @inlinable @inline(__always) public func event(name: StaticString) { #if DEBUG if _verge_signpost_enabled { let id = OSSignpostID(rawID) os_signpost(.event, log: SignpostConstants.pointOfInterestLog, name: name, signpostID: id) } #endif } @inlinable @inline(__always) public func end() { #if DEBUG _end() #endif } } ================================================ FILE: Sources/Verge/Library/StoreActivitySubscription.swift ================================================ import Combine import Atomics /** A subscription that is compatible with Combine’s Cancellable. You can manage asynchronous tasks either call the ``cancel()`` to halt the subscription, or allow it to terminate upon instance deallocation, and by implementing the ``storeWhileSourceActive()`` technique, the subscription’s active status is maintained until the source store is released. */ public final class StoreActivitySubscription: StoreSubscriptionBase, @unchecked Sendable { } ================================================ FILE: Sources/Verge/Library/StoreStateSubscription.swift ================================================ import Combine import Atomics /** A subscription that is compatible with Combine's Cancellable. You can manage asynchronous tasks either call the ``cancel()`` to halt the subscription, or allow it to terminate upon instance deallocation, and by implementing the ``storeWhileSourceActive()`` technique, the subscription's active status is maintained until the source store is released. */ public final class StoreStateSubscription: StoreSubscriptionBase, @unchecked Sendable { } ================================================ FILE: Sources/Verge/Library/StoreSubscriptionBase.swift ================================================ public class StoreSubscriptionBase: Hashable, Cancellable { public static func == (lhs: StoreSubscriptionBase, rhs: StoreSubscriptionBase) -> Bool { lhs === rhs } private struct State { var wasCancelled: Bool = false weak var storeCancellable: VergeAnyCancellable? var associatedStore: (any StoreType)? } private let state: VergeConcurrency.ManagedCriticalState public func hash(into hasher: inout Hasher) { ObjectIdentifier(self).hash(into: &hasher) } private let source: EventEmitterCancellable init( _ eventEmitterCancellable: EventEmitterCancellable, storeCancellable: VergeAnyCancellable ) { self.source = eventEmitterCancellable self.state = .init(State(storeCancellable: storeCancellable)) } public func cancel() { let continues = state.withCriticalRegion { state -> Bool in guard state.wasCancelled == false else { return false } state.wasCancelled = true // if it's associated as storeWhileSourceActive. state.storeCancellable?.dissociate(self) state.associatedStore = nil return true } guard continues else { return } source.cancel() } /** Make this subscription alive while the source is active. the source means a root data store which is Store. In case of Derived, the source will be Derived's upstream. If the upstream invalidated, this subscription will stop. */ @discardableResult public func storeWhileSourceActive() -> Self { state.withCriticalRegion { state in assert(state.wasCancelled == false) assert(state.storeCancellable != nil) state.storeCancellable?.associate(self) } return self } func associate(store: any StoreType) -> Self { state.withCriticalRegion { state in assert(state.wasCancelled == false) assert(state.storeCancellable != nil) state.associatedStore = store } return self } /** Converts to Combine.AnyCancellable to make it auto cancellable. */ public func asAny() -> AnyCancellable { return .init { [self] in self.cancel() } } deinit { cancel() } } ================================================ FILE: Sources/Verge/Library/VergeAnyCancellable.swift ================================================ import Combine import Atomics /// A typealias to `Set`. public typealias VergeAnyCancellables = Set /// A type-erasing cancellable object that executes a provided closure when canceled. /// An AnyCancellable instance automatically calls cancel() when deinitialized. /// To cancel depending owner, can be written following /// /// ``` /// class ViewController { /// /// var subscriptions = VergeAnyCancellables() /// /// func something() { /// /// let derived = store.derived(...) /// /// derived /// .subscribeStateChanges { ... } /// .store(in: &subscriptions) /// } /// /// } /// ``` public final class VergeAnyCancellable: Hashable, Cancellable, Sendable { private struct State { var wasCancelled: Bool = false var actions: ContiguousArray<() -> Void>? = .init() var retainObjects: [AnyObject] = [] } private let state: VergeConcurrency.ManagedCriticalState = .init(State()) public static func == (lhs: VergeAnyCancellable, rhs: VergeAnyCancellable) -> Bool { lhs === rhs } public func hash(into hasher: inout Hasher) { ObjectIdentifier(self).hash(into: &hasher) } public init() { } public convenience init(onDeinit: @escaping () -> Void) { self.init() state.withCriticalRegion { $0.actions = [onDeinit] } } public convenience init(_ cancellable: C) where C : Cancellable { self.init { cancellable.cancel() } } @discardableResult public func associate(_ object: AnyObject) -> VergeAnyCancellable { state.withCriticalRegion { state in assert(!state.wasCancelled) state.retainObjects.append(object) } return self } public func dissociate(_ object: AnyObject) { let targets = state.withCriticalRegion { state -> [AnyObject] in var targets: [AnyObject] = .init() targets.reserveCapacity(state.retainObjects.count) state.retainObjects.removeAll { let remove = $0 === object if remove { targets.append($0) } return remove } return targets } guard targets.isEmpty == false else { return } withExtendedLifetime(targets, {}) } public func insert(_ cancellable: Cancellable) { state.withCriticalRegion { state in assert(!state.wasCancelled) state.actions?.append { cancellable.cancel() } } } public func insert(onDeinit: @escaping () -> Void) { state.withCriticalRegion { state in assert(!state.wasCancelled) state.actions?.append(onDeinit) } } deinit { cancel() } public func cancel() { let result = state.withCriticalRegion { state -> ( retainObjects: [AnyObject], actions: ContiguousArray<() -> Void>? )? in guard !state.wasCancelled else { return nil } state.wasCancelled = true let retainObjects = state.retainObjects state.retainObjects.removeAll() let actions = state.actions state.actions = nil return (retainObjects, actions) } guard let result else { return } withExtendedLifetime(result.retainObjects, {}) result.actions?.forEach { $0() } } } ================================================ FILE: Sources/Verge/Library/VergeConcurrency+SynchronizationTracker.swift ================================================ // // Copyright (c) 2020 muukii // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation extension VergeConcurrency { /// /// /// Modified based on RxSwift's original implementations. public final class SynchronizationTracker: @unchecked Sendable { public enum Warning: Hashable { case reentrancyAnomaly case synchronizationAnomaly } private let _lock = VergeConcurrency.RecursiveLock() private var _threads = [UnsafeMutableRawPointer: Int]() private let _isEnabled: Bool public init(debugOnly: Bool = false) { if debugOnly { #if DEBUG self._isEnabled = true #else self._isEnabled = false #endif } else { self._isEnabled = true } } /** Marks as entering a synchronized operation. */ @discardableResult public func register( _ file: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line, printsConsole: Bool = false ) -> Set { guard _isEnabled else { return .init() } self._lock.lock(); defer { self._lock.unlock() } var flags = Set() let pointer = Unmanaged.passUnretained(Thread.current).toOpaque() let count = (self._threads[pointer] ?? 0) + 1 if count > 1 { flags.insert(.reentrancyAnomaly) } self._threads[pointer] = count if self._threads.count > 1 { flags.insert(.synchronizationAnomaly) } if printsConsole, flags.isEmpty == false { print("⚠️[SynchronizationTracker] Found issues \(flags) in \(file):\(function):\(line)") } return flags } /** Marks as exited a synchronized operation. */ public func unregister() { guard _isEnabled else { return } self._lock.lock(); defer { self._lock.unlock() } let pointer = Unmanaged.passUnretained(Thread.current).toOpaque() self._threads[pointer] = (self._threads[pointer] ?? 1) - 1 if self._threads[pointer] == 0 { self._threads[pointer] = nil } } } } ================================================ FILE: Sources/Verge/Library/VergeConcurrency.swift ================================================ // // Copyright (c) 2020 muukii // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation public enum VergeConcurrency { public final class RecursiveLock: NSRecursiveLock, @unchecked Sendable { } public struct UnfairLock: ~Copyable, @unchecked Sendable { private let _lock: os_unfair_lock_t public init() { _lock = .allocate(capacity: 1) _lock.initialize(to: os_unfair_lock()) } public func lock() { os_unfair_lock_lock(_lock) } public func unlock() { os_unfair_lock_unlock(_lock) } public func `try`() -> Bool { return os_unfair_lock_trylock(_lock) } deinit { _lock.deinitialize(count: 1) _lock.deallocate() } } /// An atomic variable. public final class RecursiveLockAtomic: @unchecked Sendable { public var unsafelyWrappedValue: Value { _read { yield _value } } private let lock: RecursiveLock private var _value: Value /// Atomically get or set the value of the variable. public var value: Value { get { return withValue { $0 } } set(newValue) { swap(newValue) } } /// Initialize the variable with the given initial value. /// /// - parameters: /// - value: Initial value for `self`. public init(_ value: Value) { _value = value lock = .init() } /// Atomically modifies the variable. /// /// - parameters: /// - action: A closure that takes the current value. /// /// - returns: The result of the action. @discardableResult public func modify(_ action: (inout Value) throws -> Result) rethrows -> Result { lock.lock() defer { lock.unlock() } return try action(&_value) } /// Atomically perform an arbitrary action using the current value of the /// variable. /// /// - parameters: /// - action: A closure that takes the current value. /// /// - returns: The result of the action. @discardableResult public func withValue(_ action: (Value) throws -> Result) rethrows -> Result { lock.lock() defer { lock.unlock() } return try action(_value) } /// Atomically replace the contents of the variable. /// /// - parameters: /// - newValue: A new value for the variable. /// /// - returns: The old value. @discardableResult public func swap(_ newValue: Value) -> Value { return modify { (value: inout Value) in let oldValue = value value = newValue return oldValue } } } /// An atomic variable. @propertyWrapper public final class UnfairLockAtomic: @unchecked Sendable { public var unsafelyWrappedValue: Value { _read { yield _value } } private let lock: UnfairLock private var _value: Value /// Atomically get or set the value of the variable. public var value: Value { get { return withValue { $0 } } set(newValue) { swap(newValue) } } public var wrappedValue: Value { get { return withValue { $0 } } set(newValue) { swap(newValue) } } /// Initialize the variable with the given initial value. /// /// - parameters: /// - value: Initial value for `self`. public init(_ wrappedValue: Value) { _value = wrappedValue lock = .init() } public init(wrappedValue: Value) { _value = wrappedValue lock = .init() } public var projectedValue: UnfairLockAtomic { self } /// Atomically modifies the variable. /// /// - parameters: /// - action: A closure that takes the current value. /// /// - returns: The result of the action. @discardableResult public func modify(_ action: (inout Value) throws -> Result) rethrows -> Result { lock.lock() defer { lock.unlock() } return try action(&_value) } /// Atomically perform an arbitrary action using the current value of the /// variable. /// /// - parameters: /// - action: A closure that takes the current value. /// /// - returns: The result of the action. @discardableResult public func withValue(_ action: (Value) throws -> Result) rethrows -> Result { lock.lock() defer { lock.unlock() } return try action(_value) } /// Atomically replace the contents of the variable. /// /// - parameters: /// - newValue: A new value for the variable. /// /// - returns: The old value. @discardableResult public func swap(_ newValue: Value) -> Value { return modify { (value: inout Value) in let oldValue = value value = newValue return oldValue } } } /// A container that initializes value when it needs. /// /// Supports multi-threading. @propertyWrapper public final class AtomicLazy: @unchecked Sendable { private enum State { case initialized(T) case notInitialized } public typealias Initializer = () -> T private var _onInitialized: (T) -> Void = { _ in } private let lock: UnfairLock = .init() public var wrappedValue: T { lock.lock() defer { lock.unlock() } return unsafeValue } public var projectedValue: AtomicLazy { self } @discardableResult public func modify(_ action: (inout T) throws -> Result) rethrows -> Result { lock.lock() defer { lock.unlock() } var new = unsafeValue let result = try action(&new) self._synchronized_state = .initialized(new) return consume result } private var _synchronized_state: State = .notInitialized private var unsafeValue: T { get { switch _synchronized_state { case .notInitialized: let value = initializer() _onInitialized(value) self._synchronized_state = .initialized(value) self.initializer = nil return value case .initialized(let value): return value } } } private var initializer: Initializer! public init(_ initializer: @escaping Initializer) { self.initializer = initializer } public init(wrappedValue initializer: @autoclosure @escaping Initializer) { self.initializer = initializer } /// Set closure on value initialized. /// the closure would be called on thread which value initialized. @discardableResult public func onInitialized(_ perform: @escaping (T) -> Void) -> Self { _onInitialized = perform return self } } // From: https://github.com/apple/swift-async-algorithms/blob/4c3ea81f81f0a25d0470188459c6d4bf20cf2f97/Sources/AsyncAlgorithms/Locking.swift#L131 struct ManagedCriticalState: @unchecked Sendable { private final class LockedBuffer: ManagedBuffer { deinit { withUnsafeMutablePointerToElements { Lock.deinitialize($0) } } } private let buffer: ManagedBuffer init(_ initial: State) { buffer = LockedBuffer.create(minimumCapacity: 1) { buffer in buffer.withUnsafeMutablePointerToElements { Lock.initialize($0) } return initial } } func withCriticalRegion(_ critical: (inout State) throws -> R) rethrows -> R { try buffer.withUnsafeMutablePointers { header, lock in Lock.lock(lock) defer { Lock.unlock(lock) } return try critical(&header.pointee) } } } internal struct Lock { #if canImport(Darwin) typealias Primitive = os_unfair_lock #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) typealias Primitive = pthread_mutex_t #elseif canImport(WinSDK) typealias Primitive = SRWLOCK #else #error("Unsupported platform") #endif typealias PlatformLock = UnsafeMutablePointer let platformLock: PlatformLock private init(_ platformLock: PlatformLock) { self.platformLock = platformLock } fileprivate static func initialize(_ platformLock: PlatformLock) { #if canImport(Darwin) platformLock.initialize(to: os_unfair_lock()) #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) let result = pthread_mutex_init(platformLock, nil) precondition(result == 0, "pthread_mutex_init failed") #elseif canImport(WinSDK) InitializeSRWLock(platformLock) #else #error("Unsupported platform") #endif } fileprivate static func deinitialize(_ platformLock: PlatformLock) { #if canImport(Glibc) || canImport(Musl) || canImport(Bionic) let result = pthread_mutex_destroy(platformLock) precondition(result == 0, "pthread_mutex_destroy failed") #endif platformLock.deinitialize(count: 1) } fileprivate static func lock(_ platformLock: PlatformLock) { #if canImport(Darwin) os_unfair_lock_lock(platformLock) #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) pthread_mutex_lock(platformLock) #elseif canImport(WinSDK) AcquireSRWLockExclusive(platformLock) #else #error("Unsupported platform") #endif } fileprivate static func unlock(_ platformLock: PlatformLock) { #if canImport(Darwin) os_unfair_lock_unlock(platformLock) #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) let result = pthread_mutex_unlock(platformLock) precondition(result == 0, "pthread_mutex_unlock failed") #elseif canImport(WinSDK) ReleaseSRWLockExclusive(platformLock) #else #error("Unsupported platform") #endif } static func allocate() -> Lock { let platformLock = PlatformLock.allocate(capacity: 1) initialize(platformLock) return Lock(platformLock) } func deinitialize() { Lock.deinitialize(platformLock) platformLock.deallocate() } func lock() { Lock.lock(platformLock) } func unlock() { Lock.unlock(platformLock) } /// Acquire the lock for the duration of the given block. /// /// This convenience method should be preferred to `lock` and `unlock` in /// most situations, as it ensures that the lock will be released regardless /// of how `body` exits. /// /// - Parameter body: The block to execute while holding the lock. /// - Returns: The value returned by the block. func withLock(_ body: () throws -> T) rethrows -> T { self.lock() defer { self.unlock() } return try body() } // specialise Void return (for performance) func withLockVoid(_ body: () throws -> Void) rethrows -> Void { try self.withLock(body) } } } @inline(__always) func withUncheckedSendable(_ body: () throws -> T) rethrows -> T { try body() } ================================================ FILE: Sources/Verge/Library/_BackingStorage+.swift ================================================ extension _BackingStorage { func map(_ transform: (borrowing Value) throws -> U) rethrows -> _BackingStorage { return .init( try transform(value) ) } } ================================================ FILE: Sources/Verge/Logging/ActivityTrace.swift ================================================ // // Copyright (c) 2020 muukii // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// A trace that indicates the activity where comes from. public struct ActivityTrace: Encodable, Sendable { public let createdAt: Date = .init() public let name: String public let file: String public let function: String public let line: UInt } ================================================ FILE: Sources/Verge/Logging/DefaultStoreLogger.swift ================================================ // // Copyright (c) 2019 muukii // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import os public struct CommitLog: Encodable, Sendable { public let type: String = "commit" public let tookMilliseconds: Double public let traces: [MutationTrace] public let store: String public init(storeName: String, traces: [MutationTrace], time: CFTimeInterval) { self.store = storeName self.tookMilliseconds = time * 1000 self.traces = traces } } public struct ActivityLog: Encodable, Sendable { public let type: String = "activity" public let trace: ActivityTrace public let store: String public init(storeName: String, trace: ActivityTrace) { self.store = storeName self.trace = trace } } public struct DidCreateDispatcherLog: Encodable, Sendable { public let type: String = "did_create_dispatcher" public let store: String public let dispatcher: String public init(storeName: String, dispatcherName: String) { self.store = storeName self.dispatcher = dispatcherName } } public struct DidDestroyDispatcherLog: Encodable, Sendable { public let type: String = "did_destroy_dispatcher" public let store: String public let dispatcher: String public init(storeName: String, dispatcherName: String) { self.store = storeName self.dispatcher = dispatcherName } } /// An object default implementation of VergeStoreLogger. /// It uses `os_log` to print inside. /// There are OSLog object each type of action. /// You can turn off logging each OSLog object. public struct DefaultStoreLogger: StoreLogger { public static var `default`: Self { .init() } public let commitLog = OSLog(subsystem: "VergeStore", category: "Commit") public let activityLog = OSLog(subsystem: "VergeStore", category: "Activity") public let dispatcherCreationLog = OSLog(subsystem: "VergeStore", category: "Dispatcher_Creation") public let dispatcherDestructionLog = OSLog(subsystem: "VergeStore", category: "Dispatcher_Destruction") public init() { } private static let encoder: JSONEncoder = { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 if #available(iOS 11.0, macOS 10.13, tvOS 11, watchOS 4, *) { encoder.outputFormatting = [.prettyPrinted, .sortedKeys] } else { encoder.outputFormatting = [.prettyPrinted] } return encoder }() public func didCommit(log: CommitLog, sender: AnyObject) { Task { [commitLog]in let string = String(data: try! DefaultStoreLogger.encoder.encode(log), encoding: .utf8)! os_log("%@", log: commitLog, type: .default, string) } } public func didSendActivity(log: ActivityLog, sender: AnyObject) { Task { [activityLog] in let string = String(data: try! DefaultStoreLogger.encoder.encode(log), encoding: .utf8)! os_log("%@", log: activityLog, type: .default, string) } } public func didCreateDispatcher(log: DidCreateDispatcherLog, sender: AnyObject) { Task { [dispatcherCreationLog] in let string = String(data: try! DefaultStoreLogger.encoder.encode(log), encoding: .utf8)! os_log("%@", log: dispatcherCreationLog, type: .default, string) } } public func didDestroyDispatcher(log: DidDestroyDispatcherLog, sender: AnyObject) { Task { [dispatcherDestructionLog] in let string = String(data: try! DefaultStoreLogger.encoder.encode(log), encoding: .utf8)! os_log("%@", log: dispatcherDestructionLog, type: .default, string) } } } ================================================ FILE: Sources/Verge/Logging/MutationTrace.swift ================================================ // // Copyright (c) 2020 muukii // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation public protocol HasTraces { var traces: [MutationTrace] { get } } /// A trace that indicates the mutation where comes from. public struct MutationTrace: Encodable, Equatable, Sendable { public static func == (lhs: MutationTrace, rhs: MutationTrace) -> Bool { lhs.createdAt == rhs.createdAt && lhs.name == rhs.name && lhs.file.description == rhs.file.description && lhs.function.description == rhs.function.description && lhs.line == rhs.line } public let createdAt: Date = .init() public let name: String public let file: StaticString public let function: StaticString public let line: UInt public init( name: String = "", file: StaticString = #file, function: StaticString = #function, line: UInt = #line ) { self.name = name self.file = file self.function = function self.line = line } } extension StaticString: @retroactive Encodable { public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(description) } } ================================================ FILE: Sources/Verge/Logging/RuntimeError.swift ================================================ // // Copyright (c) 2020 Hiroshi Kimura(muukii) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. public enum RuntimeError: Swift.Error { case recoveredStateFromReceivingOlderVersion(latestState: AnyChangesType, receivedState: AnyChangesType) case recursiveleyCommit(storeName: String, traces: [MutationTrace]) } ================================================ FILE: Sources/Verge/Logging/RuntimeSanitizer.swift ================================================ // // Copyright (c) 2020 Hiroshi Kimura(muukii) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. public struct RuntimeSanitizer: Sendable { nonisolated(unsafe) public static var global = RuntimeSanitizer() public var isSanitizerStateReceivingByCorrectOrder: Bool = false public var isRecursivelyCommitDetectionEnabled: Bool = false public var onDidFindRuntimeError: @Sendable (RuntimeError) -> Void = { _ in } public init() { } } ================================================ FILE: Sources/Verge/Logging/StoreLogger.swift ================================================ // // Copyright (c) 2020 Hiroshi Kimura(muukii) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// A protocol to register logger and get the event VergeStore emits. public protocol StoreLogger { func didCommit(log: CommitLog, sender: AnyObject) func didSendActivity(log: ActivityLog, sender: AnyObject) func didCreateDispatcher(log: DidCreateDispatcherLog, sender: AnyObject) func didDestroyDispatcher(log: DidDestroyDispatcherLog, sender: AnyObject) } ================================================ FILE: Sources/Verge/Sendable.swift ================================================ final class UnsafeSendableClass: @unchecked Sendable { var value: T init(_ value: T) { self.value = value } } struct UnsafeSendableWeak: @unchecked Sendable { weak var value: T? init(_ value: T) { self.value = value } } struct UnsafeSendableStruct: ~Copyable, @unchecked Sendable { var value: T init(_ value: consuming T) { self.value = value } consuming func send() -> sending T { return value } consuming func with(_ mutation: (inout sending T) throws -> sending Return) rethrows -> sending Return { try mutation(&value) } } func withUnsafeSending(_ value: consuming T) -> sending T { UnsafeSendableStruct(value).send() } ================================================ FILE: Sources/Verge/Store/AnyTargetQueue.swift ================================================ // // Copyright (c) 2020 Hiroshi Kimura(Muukii) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import Atomics public protocol TargetQueueType { func execute(_ workItem: sending @escaping @Sendable () -> Void) } /// Describes queue to dispatch event /// Currently light-weight impl /// A reason why class is to take an object identifier. public final class AnyTargetQueue: TargetQueueType { private let _execute: (@escaping @Sendable () -> Void) -> Void fileprivate init( _execute: @escaping (@escaping @Sendable () -> Void) -> Void ) { self._execute = _execute } public func execute(_ workItem: @escaping @Sendable () -> Void) { _execute(workItem) } } public protocol MainActorTargetQueueType { func execute(_ workItem: @escaping @MainActor () -> Void) } extension MainActorTargetQueueType where Self == ImmediateMainActorTargetQueue { public static func mainIsolated() -> Self { .init() } public static var main: Self { .shared } } extension MainActorTargetQueueType where Self == HoppingMainActorTargetQueue { /// It dispatches to main-queue asynchronously always. public static var asyncMain: Self { return .init() } } /// always dispatches to main-queue asynchronously public struct HoppingMainActorTargetQueue: MainActorTargetQueueType { init() { } public func execute(_ workItem: @escaping @MainActor () -> Void) { DispatchQueue.main.async { workItem() } } } /// It dispatches to main-queue as possible as synchronously. Otherwise, it dispatches asynchronously. public struct ImmediateMainActorTargetQueue: Sendable, MainActorTargetQueueType { public static let shared = Self() private let numberEnqueued = ManagedAtomic.init(0) init() { } public func execute(_ workItem: @escaping @MainActor () -> Void) { let previousNumberEnqueued = numberEnqueued.loadThenWrappingIncrement(ordering: .sequentiallyConsistent) if Thread.isMainThread && previousNumberEnqueued == 0 { MainActor.assumeIsolated { workItem() } numberEnqueued.wrappingDecrement(ordering: .sequentiallyConsistent) } else { DispatchQueue.main.async { workItem() self.numberEnqueued.wrappingDecrement(ordering: .sequentiallyConsistent) } } } } private enum StaticMember { static let serialBackgroundDispatchQueue: DispatchQueue = .init( label: "org.verge.background", qos: .default, attributes: [], autoreleaseFrequency: .workItem, target: nil ) } extension TargetQueueType where Self == Queues.Passthrough { /// Returns a instance that never dispatches. /// The Sink use this targetQueue performs in the queue which the upstream commit dispatched. public static var passthrough: Queues.Passthrough { .init() } } extension TargetQueueType where Self == Queues.AsyncBackground { /// It dispatches to the serial background queue asynchronously. public static var asyncSerialBackground: Self { self.init() } } extension TargetQueueType where Self == AnyTargetQueue { /// Use specified queue, always dispatches public static func specific(_ targetQueue: DispatchQueue) -> AnyTargetQueue { return .init { workItem in targetQueue.async(execute: workItem) } } /// Enqueue first item on current-thread(synchronously). /// From then, using specified queue. public static func startsFromCurrentThread(andUse queue: some TargetQueueType) -> AnyTargetQueue { let numberEnqueued = ManagedAtomic.init(true) let execute = queue.execute return .init { workItem in let isFirst = numberEnqueued.loadThenLogicalAnd(with: false, ordering: .relaxed) if isFirst { workItem() } else { execute(workItem) } } } /// Enqueue first item on current-thread(synchronously). /// From then, using specified queue. public static func startsFromCurrentThread(andUse queue: some MainActorTargetQueueType) -> AnyTargetQueue { return startsFromCurrentThread(andUse: Queues.MainActor(queue)) } } extension HoppingMainActorTargetQueue { /// It dispatches to main-queue asynchronously always. public static var asyncMain: HoppingMainActorTargetQueue { return .init() } /// It dispatches to main-queue as possible as synchronously. Otherwise, it dispatches asynchronously from other background-thread. public static var main: ImmediateMainActorTargetQueue { return .shared } /// It dispatches to main-queue as possible as synchronously. Otherwise, it dispatches asynchronously from other background-thread. /// This create isolated queue against using `.main`. public static func mainIsolated() -> ImmediateMainActorTargetQueue { return .init() } } public enum Queues { struct MainActor: TargetQueueType { let underlying: Underlying init(_ underlying: Underlying) { self.underlying = underlying } public func execute(_ workItem: sending @escaping @Sendable () -> Void) { underlying.execute(workItem) } } public struct Passthrough: TargetQueueType { public func execute(_ workItem: sending @escaping @Sendable () -> Void) { workItem() } } public struct AsyncBackground: TargetQueueType { private let executor: BackgroundActor = .init() public func execute(_ workItem: sending @escaping @Sendable () -> Void) { Task { [executor, workItem] in await executor.perform(workItem) } } private actor BackgroundActor: Actor { init() { } func perform(_ operation: sending @Sendable () throws -> R) rethrows -> R { try operation() } } } } ================================================ FILE: Sources/Verge/Store/Changes.swift ================================================ // // Copyright (c) 2020 Hiroshi Kimura(Muukii) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation @_spi(Internal) import TypedComparator import StateStruct private let _shared_changesDeallocationQueue = BackgroundDeallocationQueue() public protocol AnyChangesType: AnyObject, Sendable { var traces: [MutationTrace] { get } var version: UInt64 { get } } public protocol ChangesType: AnyChangesType { associatedtype Value var previousPrimitive: Value? { get } var primitive: Value { get } var previous: Self? { get } var modification: Modification? { get } func asChanges() -> Changes } /// An immutable data object to achieve followings: /// - To know a property has been modified. (It contains 2 instances (old, new)) /// - To avoid copying cost with wrapping reference type - So, you can embed this object on the other state. /// /// ```swift /// struct MyState: Equatable { /// var name: String /// var age: String /// var height: String /// } /// ``` /// /// ```swift /// let changes: Changes /// ``` /// /// It can be accessed with properties of MyState by dynamicMemberLookup /// ```swift /// changes.name /// ``` /// /// It would be helpful to update UI partially /// ```swift /// func updateUI(changes: Changes) { /// /// changes.ifChanged(\.name) { name in /// // update UI /// } /// /// changes.ifChanged(\.age) { age in /// // update UI /// } /// /// changes.ifChanged(\.height) { height in /// // update UI /// } /// } /// ``` /// /// - Attention: Equalities calculates with pointer-personality basically, if the Value type compatibles `Equatable`, it does using also Value's equalities. /// This means Changes will return equals if different pointer but the value is the same. @dynamicMemberLookup public final class Changes: @unchecked Sendable, ChangesType, Equatable, HasTraces { public typealias ChangesKeyPath = KeyPath public static func == (lhs: Changes, rhs: Changes) -> Bool { lhs === rhs } // MARK: - Stored Properties public let previous: Changes? private let innerBox: InnerBox public private(set) var version: UInt64 // MARK: - Computed Properties @available(*, deprecated, renamed: "previousPrimitive") public var old: Value? { previousPrimitive } @available(*, deprecated, renamed: "primitive") public var current: Value { primitive } /// Returns a previous value as primitive /// - Important: a returns value won't change against pointer-personality public var previousPrimitive: Value? { _read { yield previous?.primitive } } /// Returns a value as primitive /// - Important: a returns value won't change against pointer-personality public var primitive: Value { _read { yield innerBox.value } } public var primitiveBox: InnerBox { innerBox } /// Returns a value as primitive /// - Important: a returns value won't change against pointer-personality public var root: Value { _read { yield innerBox.value } } public let traces: [MutationTrace] public let modification: Modification? public let _transaction: Transaction // MARK: - Initializers public convenience init( old: consuming Value?, new: consuming Value ) { self.init( previous: old.map { .init(old: nil, new: $0) }, innerBox: .init(consume new), version: 0, traces: [], modification: nil, transaction: .init() ) } private init( previous: Changes?, innerBox: InnerBox, version: UInt64, traces: [MutationTrace], modification: consuming Modification?, transaction: Transaction ) { self.previous = previous self.innerBox = innerBox self.version = version self.traces = traces self.modification = modification self._transaction = transaction vergeSignpostEvent("Changes.init", label: "\(type(of: self))") } deinit { vergeSignpostEvent("Changes.deinit", label: "\(type(of: self))") let unsafeBox = UnsafeSendableStruct(innerBox) Task { await _shared_changesDeallocationQueue.releaseObjectInBackground(object: unsafeBox.value) } } @inline(__always) private func cloneWithDropsPrevious() -> Changes { return .init( previous: nil, innerBox: innerBox, version: version, traces: traces, modification: nil, transaction: _transaction ) } func replacePrevious(_ previous: Changes) -> Changes { return .init( previous: previous, innerBox: innerBox, version: version, traces: traces, modification: nil, transaction: _transaction ) } @inlinable public func asChanges() -> Changes { self } /// Returns a Changes object that dropped previous value. /// It returns always true in `ifChanged` public func droppedPrevious() -> Changes { cloneWithDropsPrevious() } @inlinable public subscript(dynamicMember keyPath: KeyPath) -> T { _read { yield primitive[keyPath: keyPath] } } /// Returns a new instance that projects value by transform closure. /// /// - Warning: modification would be dropped. public func map(_ transform: (borrowing Value) throws -> U) rethrows -> Changes { let signpost = VergeSignpostTransaction("Changes.map") defer { signpost.end() } return Changes( previous: try previous.map { try $0.map(transform) }, innerBox: try innerBox.map(transform), version: version, traces: traces, modification: modification, transaction: _transaction ) } /** Returns an ``Changes`` containing the value of mapping the given key path over the root value if value present. */ public func mapIfPresent(_ keyPath: KeyPath) -> Changes? { guard self[dynamicMember: keyPath] != nil else { return nil } return Changes( previous: previous.flatMap { $0.mapIfPresent(keyPath) }, innerBox: innerBox.map { $0[keyPath: keyPath]! }, version: version, traces: traces, modification: nil, transaction: _transaction ) } /** Returns an ``Changes`` containing the value of mapping the given key path over the root value if value present. */ @available(*, deprecated, renamed: "mapIfPresent") public func _beta_map(_ keyPath: KeyPath) -> Changes? { mapIfPresent(keyPath) } public func makeNextChanges( with nextNewValue: Value, modification: Modification?, transaction: Transaction ) -> Changes { let previous = cloneWithDropsPrevious() let nextVersion = previous.version &+ 1 return Changes.init( previous: previous, innerBox: .init(nextNewValue), version: nextVersion, traces: traces, modification: modification, transaction: transaction ) } @discardableResult public func _read(perform: (borrowing Value) -> Return) -> Return { perform(innerBox.value) } } // MARK: - Primitive methods extension Changes { /// Takes a composed value if it's changed from old value. @inline(__always) public func takeIfChanged( _ compose: (Value) throws -> Composed, _ comparer: some TypedComparator ) rethrows -> Composed? { try _takeIfChanged(compose, comparer) } @inline(__always) fileprivate func _takeIfChanged( _ compose: (Value) throws -> (repeat each Element), _ comparer: consuming some TypedComparator<(repeat each Element)> ) rethrows -> (repeat each Element)? { let current = self.primitive guard let previousValue = previous else { return try compose(consume current) } let old = previousValue.primitive let composedFromCurrent = try compose(consume current) guard !comparer(try compose(consume old), composedFromCurrent) else { return nil } return composedFromCurrent } @inline(__always) fileprivate func _takeIfChanged_packed( _ compose: (Value) throws -> (repeat each Element) ) rethrows -> (repeat each Element)? { let current = self.primitive guard let previousValue = previous else { return try compose(consume current) } let old = previousValue.primitive let composedFromCurrent = try compose(consume current) let composedFromOld = try compose(old) guard !areEqual( (repeat each composedFromOld), (repeat each composedFromCurrent) ) else { return nil } return composedFromCurrent } @inline(__always) fileprivate func _takeIfChanged_packed_nonEquatable( _ compose: (Value) throws -> (repeat each Element), comparator: some TypedComparator<(repeat each Element)> ) rethrows -> (repeat each Element)? { let current = self.primitive guard let previousValue = previous else { return try compose(consume current) } let old = previousValue.primitive let composedFromCurrent = try compose(consume current) let composedFromOld = try compose(old) let isEqual = comparator(composedFromOld, composedFromCurrent) guard isEqual == false else { return nil } return composedFromCurrent } /// Performs a closure if the selected value changed from the previous one. /// /// - Parameters: /// - compose: A closure that projects a composed value from self. that executes with both of value(new and old). /// - comparer: A Comparer that checks if the composed values are different between current and old. /// - perform: A closure that executes any operation if composed value changed. /// /// - Returns: An instance that returned from the perform closure if performed. @inline(__always) public func ifChanged( _ compose: (Value) -> Composed, _ comparer: some TypedComparator, _ perform: (Composed) throws -> Result ) rethrows -> Result? { guard let result = takeIfChanged(compose, comparer) else { return nil } return try perform(result) } } // MARK: - Convenience methods extension Changes { /// Takes a composed value if it's changed from old value. @inline(__always) public func takeIfChanged( _ compose: (Value) throws -> Composed ) rethrows -> Composed? { try takeIfChanged(compose, .equality()) } public func ifChanged( _ compose: (Value) -> Composed ) -> IfChangedBox { guard let result = takeIfChanged(compose) else { return .init() } return .init(value: consume result, source: innerBox) } /** Packed ``` state.ifChanged({ $0.name }).do { value in ... } ``` */ public borrowing func ifChanged( _ compose: (borrowing Value) -> (repeat each Element) ) -> IfChangedBox { guard let result = _takeIfChanged_packed(compose) else { return .init() } return .init(value: (repeat each result), source: innerBox) } public borrowing func ifChanged( _ compose: (borrowing Value) -> (repeat each Element), comparator: some TypedComparator<(repeat each Element)> ) -> IfChangedBox { guard let result = _takeIfChanged_packed_nonEquatable(compose, comparator: comparator) else { return .init() } return .init(value: (repeat each result), source: innerBox) } /** singular variant */ public func ifChanged( _ keyPath: KeyPath ) -> IfChangedBox { ifChanged({ $0[keyPath: keyPath] }) } /** multiple variant */ public func ifChanged( _ keyPaths: repeat KeyPath ) -> IfChangedBox { ifChanged({ (repeat $0[keyPath: each keyPaths]) }) } } // MARK: - Has changes extension Changes { /// Returns boolean that indicates value specified by keyPath contains changes with compared old and new. @inline(__always) public func hasChanges(_ keyPath: ChangesKeyPath) -> Bool { hasChanges(keyPath, EqualityComparator()) } /// Returns boolean that indicates value specified by keyPath contains changes with compared old and new. @inline(__always) public func hasChanges( _ keyPath: ChangesKeyPath, _ comparer: some TypedComparator ) -> Bool { hasChanges({ $0[keyPath: keyPath] }, comparer) } @inline(__always) public func hasChanges( _ compose: (Value) -> Composed ) -> Bool { hasChanges(compose, EqualityComparator()) } @inline(__always) public func hasChanges( _ compose: (Value) -> Composed, _ comparer: some TypedComparator ) -> Bool { takeIfChanged(compose, comparer) != nil } } // MARK: - NoChanges extension Changes { /// Returns boolean that indicates value specified by keyPath contains **NO** changes with compared old and new. @inline(__always) public func noChanges(_ keyPath: ChangesKeyPath) -> Bool { !hasChanges(keyPath, EqualityComparator()) } /// Returns boolean that indicates value specified by keyPath contains **NO** changes with compared old and new. @inline(__always) public func noChanges(_ keyPath: ChangesKeyPath, _ comparer: some TypedComparator) -> Bool { !hasChanges(keyPath, comparer) } } extension Changes: CustomReflectable { public var customMirror: Mirror { Mirror( self, children: [ "version": version, "previous": previous as Any, "primitive": primitive, "transaction": _transaction, "traces": traces, "modification": modification as Any, ], displayStyle: .struct, ancestorRepresentation: .generated ) } } // MARK: - Nested Types extension Changes { public typealias InnerBox = _BackingStorage } extension Changes where Value: Equatable { public var hasChanges: Bool { previousPrimitive != primitive } public func ifChanged() -> IfChangedBox { ifChanged({ $0 }) } } public struct IfChangedBox: ~Copyable { public let value: T? @usableFromInline let source: Changes.InnerBox? init(value: consuming T, source: Changes.InnerBox) { self.value = consume value self.source = source } init() { self.value = nil self.source = nil } @discardableResult @inlinable public consuming func `do`(_ perform: (consuming T) throws -> Return) rethrows -> Return? { if let value { return try perform(consume value) } return nil } @discardableResult @inlinable public consuming func doWithSource(_ perform: (consuming Changes.InnerBox) throws -> Return) rethrows -> Return? { if let source { return try perform(source) } return nil } } ================================================ FILE: Sources/Verge/Store/DetachedDispatcher.swift ================================================ // // Copyright (c) 2019 muukii // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. public final class DetachedDispatcher: StoreDriverType { public let store: Store public let scope: WritableKeyPath & Sendable init( store: Store, scope: WritableKeyPath & Sendable ) { self.store = store self.scope = scope } } ================================================ FILE: Sources/Verge/Store/KeyObject.swift ================================================ import Foundation @_spi(Internal) public final class KeyObject: NSObject, NSCopying { public func copy(with zone: NSZone? = nil) -> Any { return KeyObject(content: content) } public let content: Content public init(content: consuming Content) { self.content = content } public override var hash: Int { content.hashValue } public override func isEqual(_ object: Any?) -> Bool { guard let other = object as? KeyObject else { return false } guard content == other.content else { return false } return true } } ================================================ FILE: Sources/Verge/Store/NonAtomicCounter.swift ================================================ // // Copyright (c) 2020 Hiroshi Kimura(Muukii) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// A container that manages raw value to describe mark as updated. public struct NonAtomicCounter: Hashable, Sendable { private(set) public var value: UInt64 = 0 public init() {} public mutating func increment() { value &+= 1 } } ================================================ FILE: Sources/Verge/Store/Pipeline.swift ================================================ // // Copyright (c) 2020 Hiroshi Kimura(Muukii) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import TypedComparator public enum ContinuousResult { case new(Output) case noUpdates } extension ContinuousResult: Equatable where Output: Equatable { } /// A filter object that yields the output produced from the input. public protocol PipelineType: Sendable { associatedtype Input associatedtype Output associatedtype Storage = Void func makeStorage() -> Storage /// Yields the output from the input. func yield(_ input: Input, storage: inout Storage) -> Output /// Yields the output from the input if it's needed func yieldContinuously(_ input: Input, storage: inout Storage) -> ContinuousResult } extension PipelineType where Storage == Void { public func makeStorage() { () } } /// It produces outputs from inputs with their own conditions. /// /// Against just using map closure, it can drop output if there are no changes. /// This will be helpful in performance. Therefore most type parameters require Equatable. public enum Pipelines { /// KeyPath based pipeline, light weight operation just take value from source. public struct ChangesSelectPassthroughPipeline: PipelineType { public typealias Storage = Void public typealias Input = Changes public let selector: @Sendable (borrowing Input.Value) -> Output public init( selector: @escaping @Sendable (borrowing Input.Value) -> Output ) { self.selector = selector } public func yieldContinuously(_ input: Input, storage: inout Storage) -> ContinuousResult { let target = input._read(perform: selector) return .new(consume target) } public func yield(_ input: Input, storage: inout Storage) -> Output { input._read(perform: selector) } } /// KeyPath based pipeline, light weight operation just take value from source. public struct ChangesSelectPipeline: PipelineType { public typealias Input = Changes public let selector: @Sendable (borrowing Input.Value) -> Output public let additionalDropCondition: (@Sendable (Input) -> Bool)? public init( selector: @escaping @Sendable (borrowing Input.Value) -> Output, additionalDropCondition: (@Sendable (Input) -> Bool)? ) { self.selector = selector self.additionalDropCondition = additionalDropCondition } public func yieldContinuously(_ input: Input, storage: inout Storage) -> ContinuousResult { guard let previous = input.previous else { return .new(input._read(perform: selector)) } let target = input._read(perform: selector) guard previous._read(perform: selector) == target else { guard let additionalDropCondition = additionalDropCondition, additionalDropCondition(input) else { return .new(consume target) } return .noUpdates } return .noUpdates } public func yield(_ input: Input, storage: inout Storage) -> Output { input._read(perform: selector) } public func drop(while predicate: @escaping @Sendable (Input) -> Bool) -> Self { return .init( selector: selector, additionalDropCondition: additionalDropCondition.map { currentCondition in { input in currentCondition(input) || predicate(input) } } ?? predicate ) } } /// Closure based pipeline, public struct ChangesMapPipeline: PipelineType { public typealias Storage = Void public typealias Input = Changes // MARK: - Properties public let intermediate: @Sendable (Input.Value) -> PipelineIntermediate public let transform: @Sendable (Intermediate) -> Output public let additionalDropCondition: (@Sendable (Input) -> Bool)? public init( @PipelineIntermediateBuilder intermediate: @escaping @Sendable ( Input.Value ) -> PipelineIntermediate, transform: @escaping @Sendable (Intermediate) -> Output, additionalDropCondition: (@Sendable (Input) -> Bool)? ) { self.intermediate = intermediate self.transform = transform self.additionalDropCondition = additionalDropCondition } // MARK: - Functions public func yieldContinuously(_ input: Input, storage: inout Storage) -> ContinuousResult { guard let previous = input.previous else { return .new(yield(input, storage: &storage)) } let previousIntermediate = intermediate(previous.primitive) let newIntermediate = intermediate(input.primitive) guard previousIntermediate == newIntermediate else { let previousMapped = transform(previousIntermediate.value) let newMapped = transform(newIntermediate.value) guard previousMapped == newMapped else { guard let additionalDropCondition = additionalDropCondition, additionalDropCondition(input) else { return .new(newMapped) } return .noUpdates } return .noUpdates } return .noUpdates } public func yield(_ input: Input, storage: inout Storage) -> Output { transform(intermediate(input.primitive).value) } public func drop(while predicate: @escaping @Sendable (Input) -> Bool) -> Self { return .init( intermediate: intermediate, transform: transform, additionalDropCondition: additionalDropCondition.map { currentCondition in { input in currentCondition(input) || predicate(input) } } ?? predicate ) } } public struct UniqueFilterEquatable: PipelineType where Map.Output: Equatable { public typealias Input = Map.Input public typealias Output = Map.Output private let map: Map public init(map: Map) { self.map = map } public func makeStorage() -> VergeConcurrency.UnfairLockAtomic { .init(nil) } public func yield(_ input: Input, storage: inout Storage) -> Output { let result = map.perform(input) storage.swap(result) return result } public func yieldContinuously(_ input: Input, storage: inout Storage) -> ContinuousResult { // not to check if input has changed because storing the input may cause performance issue by copying. let result = map.perform(input) return storage.modify { value in if value != result { value = result return .new(result) } else { return .noUpdates } } } } public struct UniqueFilter: PipelineType where OutputComparator.Input == Map.Output? { public typealias Input = Map.Input public typealias Output = Map.Output private let map: Map private let outputComparator: OutputComparator public init(map: Map, outputComparator: OutputComparator) { self.map = map self.outputComparator = outputComparator } public func makeStorage() -> VergeConcurrency.UnfairLockAtomic { .init(nil) } public func yield(_ input: Input, storage: inout Storage) -> Output { let result = map.perform(input) storage.swap(result) return result } public func yieldContinuously(_ input: Input, storage: inout Storage) -> ContinuousResult { // not to check if input has changed because storing the input may cause performance issue by copying. let result = map.perform(input) return storage.modify { value in if !outputComparator(value, result) { value = result return .new(result) } else { return .noUpdates } } } } } public protocol MapFunction: Sendable { associatedtype Input associatedtype Output func perform(_ input: Input) -> Output } public struct AnyMapFunction: MapFunction { private let _perform: @Sendable (Input) -> Output public init(_ perform: @escaping @Sendable (Input) -> Output) { self._perform = perform } public func perform(_ input: Input) -> Output { _perform(input) } } extension PipelineType { public static func uniqueMap(_ mapFunction: Map) -> Self where Map.Output: Equatable, Self == Pipelines.UniqueFilterEquatable { return .init(map: mapFunction) } public static func uniqueMap( _ map: @escaping @Sendable (Input) -> Output ) -> Self where Output: Equatable, Self == Pipelines.UniqueFilterEquatable> { return uniqueMap(.init(map)) } public static func uniqueMap( _ mapFunction: Map, _ outputComparator: OutputComparator ) -> Self where Self == Pipelines.UniqueFilter { return .init(map: mapFunction, outputComparator: outputComparator) } public static func uniqueMap( _ map: @escaping @Sendable (Input) -> Output, _ outputComparator: OutputComparator ) -> Self where Self == Pipelines.UniqueFilter, OutputComparator> { return .init(map: .init(map), outputComparator: outputComparator) } } extension PipelineType { /** For Changes input Produces output values using KeyPath-based projection. exactly same with ``PipelineType/select(_:)`` */ public static func map( _ selector: @escaping @Sendable ( borrowing Input ) -> Output ) -> Self where Output: Equatable, Self == Pipelines.ChangesSelectPipeline { self.init(selector: selector, additionalDropCondition: nil) } /** For Changes input Produces output values using KeyPath-based projection. exactly same with ``PipelineType/select(_:)`` */ public static func map( _ selector: @escaping @Sendable ( borrowing Input ) -> Output ) -> Self where Self == Pipelines.ChangesSelectPassthroughPipeline { self.init(selector: selector) } /** For Changes input Produces output values using closure based projection. exactly same with ``PipelineType/map(_:)-7xvom`` */ // needs this overload as making closure from keyPath will not make sendable closure. public static func map( _ selector: KeyPath & Sendable ) -> Self where Output: Equatable, Self == Pipelines.ChangesSelectPipeline { self.init(selector: { $0[keyPath: selector] }, additionalDropCondition: nil) } /** For Changes input Produces output values using closure based projection. exactly same with ``PipelineType/map(_:)-7xvom`` */ public static func select( _ selector: KeyPath & Sendable ) -> Self where Output: Equatable, Self == Pipelines.ChangesSelectPipeline { self.init(selector: { $0[keyPath: selector] }, additionalDropCondition: nil) } /** For Changes input Produces output values using closure based projection. exactly same with ``PipelineType/map(_:)-7xvom`` */ public static func select( _ selector: KeyPath & Sendable ) -> Self where Self == Pipelines.ChangesSelectPassthroughPipeline { self.init(selector: { $0[keyPath: selector] }) } } extension PipelineType { /** For Changes input Produces output values using closure-based projection. `map` closure takes the value projected from `using` closure which is intermediate value. If the intermediate value is not changed, map closure won't perform. - Parameters: - using: Specifies values for transforming. This function is annotated ``PipelineIntermediateBuilder`` - transform: Transforms the given value from `using` function ```swift `.map(using: { $0.a; $0.b;}, transform: { a, b in ... }` ``` */ public static func map( @PipelineIntermediateBuilder using intermediate: @escaping @Sendable ( Input ) -> PipelineIntermediate, transform: @escaping @Sendable (Intermediate) -> Output ) -> Self where Input: Equatable, Output: Equatable, Self == Pipelines.ChangesMapPipeline { self.init( intermediate: intermediate, transform: transform, additionalDropCondition: nil ) } /** For Changes input Produces output values using closure-based projection. Using Edge as intermediate, output value will be unwrapped value from the Edge. */ public static func map( @PipelineIntermediateBuilder using intermediate: @escaping @Sendable (Input) -> PipelineIntermediate> ) -> Self where Input: Equatable, Output: Equatable, Self == Pipelines.ChangesMapPipeline, EdgeIntermediate> { self.init( intermediate: intermediate, transform: { $0.wrappedValue }, additionalDropCondition: nil ) } } public struct PipelineIntermediate: Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { lhs.comparer(lhs.value, rhs.value) } public var value: T public let comparer: ((T, T) -> Bool) @inlinable public init(value: T, comparer: @escaping (T, T) -> Bool) { self.value = value self.comparer = comparer } @inlinable public init(value: T) where T: Equatable { self.value = value self.comparer = (==) // this won't be called } } extension PipelineIntermediate where T: Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { lhs.value == rhs.value } } /// A result-builder that builds ``PipelineIntermediate``. /// It converts tuple into ``PipelineIntermediate`` implementing Equatable. /// /// projects: /// ``` /// { /// $0.a /// $0.b /// } /// /// // alternative syntax. /// { $0.a; $0.b; } /// ``` /// /// into: /// ``` /// PipelineIntermediate<(A, B)> /// ``` @resultBuilder public enum PipelineIntermediateBuilder { public static func buildBlock(_ i: PipelineIntermediate) -> PipelineIntermediate { return i } public static func buildBlock(_ s1: S1) -> PipelineIntermediate { .init(value: s1) } public static func buildBlock(_ s1: S1, _ s2: S2) -> PipelineIntermediate<(S1, S2)> { .init(value: (s1, s2), comparer: ==) } public static func buildBlock( _ s1: S1, _ s2: S2, _ s3: S3 ) -> PipelineIntermediate<(S1, S2, S3)> { .init(value: (s1, s2, s3), comparer: ==) } public static func buildBlock( _ s1: S1, _ s2: S2, _ s3: S3, _ s4: S4 ) -> PipelineIntermediate<(S1, S2, S3, S4)> { .init(value: (s1, s2, s3, s4), comparer: ==) } public static func buildBlock< S1: Equatable, S2: Equatable, S3: Equatable, S4: Equatable, S5: Equatable >(_ s1: S1, _ s2: S2, _ s3: S3, _ s4: S4, _ s5: S5) -> PipelineIntermediate<(S1, S2, S3, S4, S5)> { .init(value: (s1, s2, s3, s4, s5), comparer: ==) } public static func buildBlock< S1: Equatable, S2: Equatable, S3: Equatable, S4: Equatable, S5: Equatable, S6: Equatable >(_ s1: S1, _ s2: S2, _ s3: S3, _ s4: S4, _ s5: S5, _ s6: S6) -> PipelineIntermediate< (S1, S2, S3, S4, S5, S6) > { .init(value: (s1, s2, s3, s4, s5, s6), comparer: ==) } } ================================================ FILE: Sources/Verge/Store/Scan.swift ================================================ // // Copyright (c) 2020 Hiroshi Kimura(Muukii) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation public final class Scan: Sendable { public typealias Accumulator = @Sendable (inout Accumulate, Element) -> Void public var value: Accumulate { _value.value } private let _value: VergeConcurrency.UnfairLockAtomic private let _accumulator: Accumulator public init(seed: Accumulate, accumulator: @escaping Accumulator) { self._value = .init(seed) self._accumulator = accumulator } public func accumulate(_ element: Element) -> Accumulate { _value.modify { _v -> Accumulate in _accumulator(&_v, element) return _v } } } extension Scan { /// A Scan instance that increments itself integer value each event received public static func counter() -> Scan { .init(seed: 0, accumulator: { n, _ in n += 1 }) } } ================================================ FILE: Sources/Verge/Store/StateType.swift ================================================ // // Copyright (c) 2019 muukii // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import StateStruct /** An opt-in protocol that indicates it's used as a state of the State. */ public protocol StateType { /** A chance of modifying state alongside the commit. You may use this to make another value that consists of the state's self. It's better to use it for better performance to get the value rather than using computed property. */ @Sendable static func reduce( modifying: inout Self, transaction: inout Transaction, current: Changes ) } ================================================ FILE: Sources/Verge/Store/Store+Combine.swift ================================================ // // Copyright (c) 2019 muukii // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Combine @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension Store { /// A publisher that repeatedly emits the changes when state updated /// /// Guarantees to emit the first event on started subscribing. /// /// - Parameter startsFromInitial: Make the first changes object's hasChanges always return true. /// - Returns: @_spi(Package) public func _statePublisher() -> some Combine.Publisher, Never> { return publisher .associate(resource: self, retains: keepsAliveForSubscribers) .flatMap { event in guard case .state(.didUpdate(let state)) = event else { return Empty, Never>().eraseToAnyPublisher() } return Just>(state) .eraseToAnyPublisher() } .merge(with: Just(state.droppedPrevious())) } // @_spi(Package) public func _activityPublisher() -> some Combine.Publisher { return publisher .associate(resource: self, retains: keepsAliveForSubscribers) .flatMap { event in guard case .activity(let a) = event else { return Empty().eraseToAnyPublisher() } return Just(a).eraseToAnyPublisher() } } } extension Publisher { func associate(resource: AnyObject, retains: Bool) -> some Publisher { let box = ResourceBox(object: resource, retains: retains) return handleEvents(receiveCancel: { // retain self until subscription finsihed withExtendedLifetime(box) {} }) } } private final class ResourceBox { private let object: AnyObject? init(object: AnyObject, retains: Bool) { if retains { self.object = object } else { self.object = nil } } } ================================================ FILE: Sources/Verge/Store/Store+RunLoop.swift ================================================ import Foundation extension Store { /// Push an event to run event loop. public func updateMainLoop() { RunLoop.main.perform(inModes: [.common]) {} } /** Subscribes state updates in given run-loop. */ @MainActor public func pollMainLoop(receive: @escaping @MainActor (Changes) -> Void) -> VergeAnyCancellable { var latestState: Changes? = nil let subscription = RunLoopActivityObserver.addObserver(acitivity: .beforeWaiting, in: .main) { MainActor.assumeIsolated { let newState = self.state guard (latestState?.version ?? 0) < newState.version else { return } let state: Changes if let latestState { state = newState.replacePrevious(latestState) } else { state = newState.droppedPrevious() } latestState = newState receive(state) } } let firstState = state.droppedPrevious() latestState = firstState receive(firstState) return .init { RunLoopActivityObserver.remove(subscription) } } } enum RunLoopActivityObserver { struct Subscription { let mode: CFRunLoopMode let observer: CFRunLoopObserver? weak var targetRunLoop: RunLoop? } static func addObserver(acitivity: CFRunLoopActivity, in runLoop: RunLoop, callback: @escaping () -> Void) -> Subscription { let o = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, acitivity.rawValue, true, Int.max, { observer, activity in callback() }); assert(o != nil) let mode = CFRunLoopMode.commonModes! let cfRunLoop = runLoop.getCFRunLoop() CFRunLoopAddObserver(cfRunLoop, o, mode); return .init(mode: mode, observer: o, targetRunLoop: runLoop) } static func remove(_ subscription: consuming Subscription) { guard let observer = subscription.observer, let targetRunLoop = subscription.targetRunLoop else { return } CFRunLoopRemoveObserver(targetRunLoop.getCFRunLoop(), observer, subscription.mode); } } #if DEBUG && canImport(SwiftUI) import SwiftUI #Preview { Content() } @Tracking private struct StoreState { var count: Int = 0 } private struct Content: View { let store = Store<_, Never>(initialState: StoreState()) @State var subscription: VergeAnyCancellable? @State var timer: Timer? var body: some View { VStack { Button("Up") { store.commit { $0.count += 1 } } Button("Background Up") { for _ in 0..<10 { store.commit { $0.count += 1 } } } Button("Run") { RunLoop.main.run(until: Date(timeIntervalSinceNow: 19)) } } .onAppear { subscription = store.pollMainLoop { state in print(state.count) } } } } #endif ================================================ FILE: Sources/Verge/Store/Store.swift ================================================ // // Copyright (c) 2019 muukii // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Atomics import ConcurrencyTaskManager import Foundation import os.log import StateStruct #if canImport(Combine) import Combine #endif /// A protocol that indicates itself is a reference-type and can convert to concrete Store type. public protocol StoreType: AnyObject, Sendable { associatedtype State associatedtype Activity: Sendable = Never func asStore() -> Store var state: Changes { get } func lock() func unlock() } public typealias NoActivityStoreBase = Store private let sanitizerQueue = DispatchQueue.init(label: "org.vergegroup.verge.sanitizer") public enum _StoreEvent: EventEmitterEventType { public enum StateEvent { case willUpdate case didUpdate(Changes) } case state(StateEvent) case activity(Activity) case waiter(() -> Void) public func onComsume() { switch self { case .state: break case .activity: break case .waiter(let closure): closure() } } } actor Writer { init() { } func perform(_ operation: () throws -> sending R) rethrows -> sending R { try operation() } } /// An object that retains a latest state value and receives mutations that modify itself state. /// Those updates would be shared all of the subscribers these are sink(s), Derived(s) /// /// You may create subclass of VergeDefaultStore /// ``` /// final class MyStore: Store { /// init() { /// super.init(initialState: .init(), logger: nil) /// } /// } /// ``` /// You may use also `StoreWrapperType` to define State and Activity as inner types. /// open class Store: EventEmitter<_StoreEvent>, CustomReflectable, StoreType, StoreDriverType, DerivedMaking, @unchecked Sendable, Hashable { // MARK: Equatable public static func == (lhs: Store, rhs: Store) -> Bool { lhs === rhs } // MARK: Hashable public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } public let scope: any WritableKeyPath & Sendable = \State.self private let tracker = VergeConcurrency.SynchronizationTracker() /// A name of the store. /// Specified or generated automatically from file and line. public let name: String public let logger: StoreLogger? public let sanitizer: RuntimeSanitizer private var middlewares: [AnyStoreMiddleware] = [] private let externalOperation: @Sendable (inout InoutRef, Changes, inout Transaction) -> Void private(set) var nonatomicValue: Changes private let _lock: StoreOperation /** Holds subscriptions for sink State and Activity to finish them with its store life-cycle. */ private let storeLifeCycleCancellable: VergeAnyCancellable = .init() open var keepsAliveForSubscribers: Bool { false } private let wasInvalidated = Atomics.ManagedAtomic(false) // MARK: - Deinit deinit { invalidate() } // MARK: - Task public let taskManager: TaskManager = .init() let writer: Writer = .init() // MARK: - Initializers /// An initializer /// - Parameters: /// - initialState: A state instance that will be modified by the first commit. /// - backingStorageRecursiveLock: A lock instance for mutual exclusion. /// - logger: You can also use `DefaultLogger.shared`. public nonisolated init( name: String? = nil, initialState: State, storeOperation: StoreOperation = .atomic, logger: StoreLogger? = nil, sanitizer: RuntimeSanitizer? = nil, _ file: StaticString = #file, _ line: UInt = #line ) { self.nonatomicValue = .init(old: nil, new: initialState) self._lock = storeOperation self.logger = logger self.sanitizer = sanitizer ?? RuntimeSanitizer.global self.name = name ?? "\(file):\(line)" self.externalOperation = { @Sendable _, _, _ in } super.init() } public nonisolated init( name: String? = nil, initialState: State, storeOperation: StoreOperation = .atomic, logger: StoreLogger? = nil, sanitizer: RuntimeSanitizer? = nil, _ file: StaticString = #file, _ line: UInt = #line ) where State: StateType { // making reduced state var _initialState = initialState let reduced = withUnsafeMutablePointer(to: &_initialState) { pointer in var inoutRef = InoutRef<_>.init(pointer) inoutRef.modify { state in var transaction = Transaction() State.reduce(modifying: &state, transaction: &transaction, current: .init(old: nil, new: initialState)) } return inoutRef.wrapped } self.nonatomicValue = .init(old: nil, new: reduced) self._lock = storeOperation self.logger = logger self.sanitizer = sanitizer ?? RuntimeSanitizer.global self.name = name ?? "\(file):\(line)" self.externalOperation = { @Sendable inoutRef, state, transaction in let intermediate = state.makeNextChanges( with: inoutRef.wrapped, modification: inoutRef.modification, transaction: transaction ) inoutRef.modify { state in State.reduce( modifying: &state, transaction: &transaction, current: intermediate ) } } super.init() } @_spi(Internal) public final override func receiveEvent(_ event: consuming _StoreEvent) { switch event { case .state(let stateEvent): switch stateEvent { case .willUpdate: break case .didUpdate(let state): stateDidUpdate(newState: state) } case .activity: break case .waiter: break } } open func stateDidUpdate(newState: Changes) { } final func invalidate() { guard wasInvalidated.compareExchange(expected: false, desired: true, ordering: .relaxed).exchanged else { // already invalidated return } performInvalidation() } func performInvalidation() { storeLifeCycleCancellable.cancel() taskManager.cancelAll() } } // MARK: - Typealias extension Store { public typealias Scope = State public typealias Value = State } // MARK: - Computed Properties extension Store { public var store: Store { self } /// Returns a current state with thread-safety. /// /// It causes locking and unlocking with a bit cost. /// It may cause blocking if any other is doing mutation or reading. public var state: Changes { _lock.lock() defer { _lock.unlock() } return nonatomicValue } } // MARK: - Convenience Initializers extension Store { /// An initializer for preventing using the refence type as a state. @available( *, deprecated, message: "Using the reference type for the state is restricted. it must be a value type to run correctly." ) public convenience init( name: String? = nil, initialState: State, storeOperation: StoreOperation = .atomic, logger: StoreLogger? = nil, _ file: StaticString = #file, _ line: UInt = #line ) where State: AnyObject { preconditionFailure( "Using the reference type for the state is restricted. it must be a value type to run correctly." ) } } // MARK: - Wait extension Store { /** Commit operation does not mean that emitting latest state for all of subscribers synchronously. Updating state of the store will be updated immediately. To wait until all of the subscribers get the latest state, you can use this method. */ public func waitUntilAllEventConsumed() async { await withCheckedContinuation { c in accept( .waiter({ c.resume() })) } } } // MARK: - Middleware extension Store { /// Registers a middleware. /// MIddleware can execute additional operations unified with mutations. /// public func add(middleware: some StoreMiddlewareType) { // use lock lock() defer { unlock() } middlewares.append(.init(modify: middleware.modify)) } } extension Store { // MARK: - CustomReflectable public var customMirror: Mirror { return Mirror( self, children: KeyValuePairs.init( dictionaryLiteral: ("stateVersion", state.version), ("middlewares", middlewares) ), displayStyle: .class ) } @inline(__always) public func asStore() -> Store { self } /** Adds an asynchronous task to perform. Use this function to perform an asynchronous task with a lifetime that matches that of this store. If this store is deallocated ealier than the given task finished, that asynchronous task will be cancelled. Carefully use this function - If the task retains this store, it will continue to live until the task is finished. - Parameters: - key: - mode: - priority: - action - Returns: A Task for tracking given async operation's completion. */ @discardableResult public func task( key: ConcurrencyTaskManager.TaskKey = .distinct(), mode: ConcurrencyTaskManager.TaskManager.Mode = .dropCurrent, priority: TaskPriority = .userInitiated, @_inheritActorContext _ action: @Sendable @escaping () async throws -> Return ) -> Task { return taskManager.task(key: key, mode: mode, priority: priority, action) } /** Adds an asynchronous task to perform. Use this function to perform an asynchronous task with a lifetime that matches that of this store. If this store is deallocated ealier than the given task finished, that asynchronous task will be cancelled. Carefully use this function - If the task retains this store, it will continue to live until the task is finished. - Parameters: - key: - mode: - priority: - action - Returns: A Task for tracking given async operation's completion. */ @discardableResult public func taskDetached( key: ConcurrencyTaskManager.TaskKey = .distinct(), mode: ConcurrencyTaskManager.TaskManager.Mode = .dropCurrent, priority: TaskPriority = .userInitiated, _ action: @Sendable @escaping () async throws -> Return ) -> Task { return taskManager.taskDetached(key: key, mode: mode, priority: priority, action) } // MARK: - Internal /// Receives mutation /// /// - Parameters: /// - mutation: (`inout` attributes to prevent escaping `Inout` inside the closure.) @inline(__always) func _receive_sending( mutation: (inout State, inout Transaction) throws -> Result ) rethrows -> Result { let signpost = VergeSignpostTransaction("Store.commit") defer { signpost.end() } let warnings: Set if RuntimeSanitizer.global.isRecursivelyCommitDetectionEnabled { warnings = tracker.register() } else { warnings = .init() } defer { if RuntimeSanitizer.global.isRecursivelyCommitDetectionEnabled { tracker.unregister() } } var valueFromMutation: Result! var elapsed: CFTimeInterval = 0 var commitLog: CommitLog? let __sanitizer__ = sanitizer /** a ciritical session */ try _update { (state) -> UpdateResult in let startedTime = CFAbsoluteTimeGetCurrent() defer { elapsed = CFAbsoluteTimeGetCurrent() - startedTime } var modifying = state.primitive // TODO: better performant way if var trackingObject = modifying as? TrackingObject { trackingObject.startNewTracking() modifying = trackingObject as! State } let updateResult = try withUnsafeMutablePointer(to: &modifying) { (stateMutablePointer) -> UpdateResult in var transaction = Transaction() var inoutRef = InoutRef<_>.init(stateMutablePointer) let result = try inoutRef.modify { modifying in try mutation(&modifying, &transaction) } valueFromMutation = consume result /** Step-1: Checks if the state has been modified */ guard inoutRef.hasModified else { // No emits update event // Log.storeCommit.debug("\(type(of: self)) No updates") return .nothingUpdates } /** Step-2: Reduce modifying state with externalOperation */ externalOperation(&inoutRef, state, &transaction) /** Step-3 Applying by middlewares */ self.middlewares.forEach { middleware in let intermediate = state.makeNextChanges( with: stateMutablePointer.pointee, modification: inoutRef.modification, transaction: transaction ) inoutRef.modify { modifying in middleware.modify( modifyingState: &modifying, transaction: &transaction, current: intermediate ) } } /** Make a new state */ state = state.makeNextChanges( with: stateMutablePointer.pointee, modification: inoutRef.modification, transaction: transaction ) commitLog = CommitLog(storeName: self.name, traces: transaction.traces, time: elapsed) return .updated } return updateResult } if let logger = logger, let _commitLog = commitLog { logger.didCommit(log: _commitLog, sender: self) } return UnsafeSendableStruct(valueFromMutation).send() } @inline(__always) func _send( activity: Activity, trace: ActivityTrace ) { accept(.activity(activity)) let log = ActivityLog(storeName: self.name, trace: trace) logger?.didSendActivity(log: log, sender: self) } func _mainActor_sinkState( keepsAliveSource: Bool? = nil, dropsFirst: Bool = false, queue: some MainActorTargetQueueType, receive: @escaping @MainActor (Changes) -> Void ) -> StoreStateSubscription { return _primitive_sinkState( dropsFirst: dropsFirst, queue: Queues.MainActor(queue), receive: { e in MainActor.assumeIsolated { receive(e) } } ) } func _primitive_sinkState( dropsFirst: Bool = false, queue: some TargetQueueType, receive: @escaping @Sendable (Changes) -> Void ) -> StoreStateSubscription { let cancellable = _base_primitive_sinkState( dropsFirst: dropsFirst, queue: queue, receive: receive ) if keepsAliveForSubscribers { return .init(cancellable, storeCancellable: storeLifeCycleCancellable) .associate(store: self) // while subscribing its Store will be alive } else { return .init(cancellable, storeCancellable: storeLifeCycleCancellable) } } private func _base_primitive_sinkState( dropsFirst: Bool = false, queue: some TargetQueueType, receive: @escaping @Sendable (Changes) -> Void ) -> EventEmitterCancellable { let executor = queue.execute nonisolated(unsafe) var latestStateWrapper: Changes? = nil let __sanitizer__ = sanitizer let lock = VergeConcurrency.UnfairLock() /// Firstly, it registers a closure to make sure that it receives all of the updates, even updates inside the first call. /// To get recursive updates that comes from first call receive closure. let cancellable = _sinkStateEvent { (event) in switch event { case .willUpdate: break case .didUpdate(let receivedState): executor { lock.lock() var resolvedReceivedState = receivedState // To escaping from critical issue if let latestState = latestStateWrapper { if latestState.version <= receivedState.version { /* No issues case: It has received newer version than previous version */ latestStateWrapper = receivedState } else { /* Serious problem case: Received an older version than the state received before. To recover this case, send latest version state with dropping previous value in order to make `ifChanged` returns always true. */ resolvedReceivedState = latestState.droppedPrevious() if __sanitizer__.isSanitizerStateReceivingByCorrectOrder { sanitizerQueue.async { __sanitizer__.onDidFindRuntimeError( .recoveredStateFromReceivingOlderVersion( latestState: latestState, receivedState: receivedState ) ) Log.store.warning( """ ⚠️ [Verge Error] Received older version(\(receivedState.version)) value rather than latest received version(\(latestState.version)). The root cause might be from the following things: - Committed concurrently from multiple threads. To solve, make sure to commit in series, for example using DispatchQueue. Verge can't use a lock to process serially because the dead-lock will happen in some of the cases. RxSwift's BehaviorSubject takes the same deal. Regarding: Extra commit was dispatched inside sink synchronously This issue has been fixed by https://github.com/VergeGroup/Verge/pull/222 --- Received older version (\(receivedState.version)): (\(String(describing: receivedState.traces))) Latest Version (\(latestState.version)): (\(String(describing: latestState.traces))) === """ ) } } } } else { // first item latestStateWrapper = receivedState } lock.unlock() receive(resolvedReceivedState) } } } if !dropsFirst { let value = state.droppedPrevious() executor { lock.lock() latestStateWrapper = value lock.unlock() // this closure might contains some mutations. // It depends outside usages. receive(value) } } return cancellable } func _mainActor_scan_sinkState( scan: Scan, Accumulate>, dropsFirst: Bool = false, queue: some MainActorTargetQueueType, receive: @escaping @MainActor (Changes, Accumulate) -> Void ) -> StoreStateSubscription { _mainActor_sinkState(dropsFirst: dropsFirst, queue: queue) { (changes) in let accumulate = scan.accumulate(changes) receive(changes, accumulate) } } func _primitive_scan_sinkState( scan: Scan, Accumulate>, dropsFirst: Bool = false, queue: some TargetQueueType, receive: @escaping @Sendable (Changes, Accumulate) -> Void ) -> StoreStateSubscription { _primitive_sinkState(dropsFirst: dropsFirst, queue: queue) { (changes) in let accumulate = scan.accumulate(changes) receive(changes, accumulate) } } func _mainActor_sinkActivity( queue: some MainActorTargetQueueType, receive: @escaping @MainActor (sending Activity) -> Void ) -> StoreActivitySubscription { return _primitive_sinkActivity( queue: Queues.MainActor(queue), receive: { e in MainActor.assumeIsolated { receive(e) } } ) } func _primitive_sinkActivity( queue: some TargetQueueType, receive: @escaping @Sendable (sending Activity) -> Void ) -> StoreActivitySubscription { let execute = queue.execute let cancellable = self._sinkActivityEvent { activity in execute { receive(activity) } } return .init(cancellable, storeCancellable: storeLifeCycleCancellable) } } // MARK: - Storage Implementation extension Store { public func lock() { _lock.lock() } public func unlock() { _lock.unlock() } final func _sinkStateEvent( subscriber: @escaping (_StoreEvent.StateEvent) -> Void ) -> EventEmitterCancellable { addEventHandler { event in guard case .state(let stateEvent) = event else { return } subscriber(stateEvent) } } final func _sinkActivityEvent(subscriber: @escaping (sending Activity) -> Void) -> EventEmitterCancellable { addEventHandler { event in guard case .activity(let activity) = event else { return } subscriber(activity) } } enum UpdateResult { case updated case nothingUpdates } @inline(__always) final func _update(_ update: (inout Changes) throws -> UpdateResult) rethrows { let signpost = VergeSignpostTransaction("Storage.update") defer { signpost.end() } lock() do { let result = try update(&nonatomicValue) switch result { case .nothingUpdates: unlock() case .updated: let afterValue = nonatomicValue /** Unlocks lock before emitting event to avoid dead-locking. But it causes cracking the order of event. SeeAlso: testOrderOfEvents */ unlock() // it's not actual `will` 👨🏻❓ accept(.state(.willUpdate)) accept(.state(.didUpdate(afterValue))) } } catch { unlock() throw error } } } extension Store { /// [Experimental] public func stateStream() -> AsyncStream> { return .init(Changes.self, bufferingPolicy: .unbounded) { continuation in let subscription = self.sinkState(queue: .passthrough) { state in continuation.yield(state) } continuation.onTermination = { termination in subscription.cancel() } } } } ================================================ FILE: Sources/Verge/Store/StoreDriverType+Accumulator.swift ================================================ import StateStruct extension StoreDriverType where TargetStore.State : TrackingObject { /** Subscribes states and accumulates into components. Against sink method, it does not use Changes object. It allows to check if values has changed in the unit of accumulation, not Changes view. */ public func accumulate( queue: some MainActorTargetQueueType = .mainIsolated(), @AccumulationSinkComponentBuilder _ buildSubscription: @escaping @MainActor (consuming AccumulationBuilder) -> _AccumulationSinkGroup ) -> StoreStateSubscription { var previousBox: ReferenceEdge<_AccumulationSinkGroup?> = .init(wrappedValue: nil) let scope = self.scope return store.asStore().sinkState(dropsFirst: false, queue: queue) { state in let builder = AccumulationBuilder(previousLoader: { previousBox.wrappedValue }) var group = buildSubscription(consume builder) // sets the previous value if let previous = previousBox.wrappedValue { group = group.receive(previous: previous) } // sets the latest value group = group.receive( source: { state.primitive.tracked()[keyPath: scope] }, modification: state.modification ?? .indeterminate ) // runs sink group = group.consume() previousBox.wrappedValue = group } } /** Subscribes states and accumulates into components. Against sink method, it does not use Changes object. It allows to check if values has changed in the unit of accumulation, not Changes view. */ @_disfavoredOverload public func accumulate( queue: some TargetQueueType, @AccumulationSinkComponentBuilder _ buildSubscription: @escaping @Sendable (consuming AccumulationBuilder) -> _AccumulationSinkGroup ) -> StoreStateSubscription { let previousBox: UnsafeSendableClass?>> = .init( .init(wrappedValue: nil) ) let lock = VergeConcurrency.UnfairLock() let scope = self.scope return store.asStore().sinkState(dropsFirst: false, queue: queue) { @Sendable state in lock.lock() defer { lock.unlock() } withUncheckedSendable { let builder = AccumulationBuilder(previousLoader: { previousBox.value.wrappedValue }) var group = buildSubscription(consume builder) // sets the previous value if let previous = previousBox.value.wrappedValue { group = group.receive(previous: previous) } // sets the latest value group = group.receive( source: { state.primitive.tracked()[keyPath: scope] }, modification: state.modification ?? .indeterminate ) // runs sink group = group.consume() previousBox.value.wrappedValue = group } } } } public protocol AccumulationSink { associatedtype Source consuming func receive(source: () -> Source, modification: Modification) -> Self consuming func receive(previous: consuming Self) -> Self consuming func consume() -> Self } public struct AccumulationBuilder: ~Copyable { public var previous: (any AccumulationSink)? { previousLoader() } private let previousLoader: () -> (any AccumulationSink)? init(previousLoader: @escaping () -> (any AccumulationSink)?) { self.previousLoader = previousLoader } public func ifChanged(_ selector: @escaping (borrowing Source) -> Value) -> AccumulationSinkIfChanged { .init( selector: selector ) } } public struct AccumulationSinkIfChanged: AccumulationSink { private let selector: (borrowing Source) -> Target private var latestValue: Target? private var previousValue: Target? private var readingGraph: PropertyNode? private var source: Source? private var counter: UInt64 = 0 private var countToEmit: UInt64 = 0 private var handlerWithSelectedValue: ((consuming Target) -> Void)? private var handlerWithSource: ((Source) -> Void)? init( selector: @escaping (borrowing Source) -> Target ) { self.selector = selector } public consuming func dropFirst(_ k: UInt64 = 1) -> Self { countToEmit = k return self } /** the closure will be released after consumed. */ public consuming func `do`(@_inheritActorContext @_implicitSelfCapture _ perform: @escaping (consuming Target) -> Void) -> Self { self.handlerWithSelectedValue = perform return self } public consuming func `doWithSource`(@_inheritActorContext @_implicitSelfCapture _ perform: @escaping (Source) -> Void) -> Self { self.handlerWithSource = perform return self } public consuming func receive( source: () -> Source, modification: Modification ) -> Self { switch modification { case .graph(let propertyNode): guard let readingGraph, PropertyNode.hasChanges( writeGraph: propertyNode, readGraph: readingGraph ) else { // no changes self.latestValue = self.previousValue return self } break case .indeterminate: break } let tracked = source() self.latestValue = selector(tracked) self.readingGraph = tracked.trackingResult?.graph tracked.endTracking() if handlerWithSource != nil { self.source = tracked } return self } public consuming func receive(previous: consuming Self) -> Self { self.previousValue = previous.latestValue self.readingGraph = previous.readingGraph self.counter = previous.counter return self } public consuming func consume() -> Self { if latestValue != previousValue { if counter >= countToEmit { handlerWithSelectedValue?(latestValue!) handlerWithSource?(source!) } counter &+= 1 } self.handlerWithSource = nil self.handlerWithSelectedValue = nil return self } } public struct _AccumulationSinkGroup: AccumulationSink { private var component: Component private var _receiveSource: ( () -> Source, Modification, Component ) -> Component private var _receiveOther: (Component, Component) -> Component private var _consume: (Component) -> Component init( component: Component, receiveSource: @escaping (() -> Source, Modification, Component) -> Component, receiveOther: @escaping (Component, Component) -> Component, consume: @escaping (Component) -> Component ) { self.component = component self._receiveSource = receiveSource self._receiveOther = receiveOther self._consume = consume } init() where Component == Void { self.component = () self._receiveSource = { _, _, component in component } self._receiveOther = { _, component in component } self._consume = { component in component } } public consuming func receive(source: () -> Source, modification: Modification) -> Self { component = _receiveSource(source, modification, component) return self } public consuming func receive(previous: Self) -> Self { component = _receiveOther(previous.component, component) return self } public consuming func consume() -> Self { component = _consume(component) return self } } public struct _AccumulationSinkCondition: AccumulationSink where TrueComponent.Source == Source, FalseComponent.Source == Source { private var trueComponent: TrueComponent? private var falseComponent: FalseComponent? init( trueComponent: TrueComponent?, falseComponent: FalseComponent? ) { self.trueComponent = trueComponent self.falseComponent = falseComponent } public consuming func receive(source: () -> Source, modification: Modification) -> Self { if let trueComponent = trueComponent { self.trueComponent = trueComponent.receive(source: source, modification: modification) } else if let falseComponent = falseComponent { self.falseComponent = falseComponent.receive(source: source, modification: modification) } return self } public consuming func receive(previous: Self) -> Self { if let trueComponent = trueComponent, let previousTrueComponent = previous.trueComponent { self.trueComponent = trueComponent.receive(previous: previousTrueComponent) } else if let falseComponent = falseComponent, let previousFalseComponent = previous.falseComponent { self.falseComponent = falseComponent.receive(previous: previousFalseComponent) } return self } public consuming func consume() -> Self { if let trueComponent = trueComponent { self.trueComponent = trueComponent.consume() } else if let falseComponent = falseComponent { self.falseComponent = falseComponent.consume() } return self } } struct _AccumulationSinkOptional: AccumulationSink where Component.Source == Source { private var component: Component? init( component: Component? ) { self.component = component } consuming func receive(source: () -> Source, modification: Modification) -> Self { if let component = component { self.component = component.receive(source: source, modification: modification) } return self } consuming func receive(previous: Self) -> Self { if let component = component, let previousComponent = previous.component { self.component = component.receive(previous: previousComponent) } return self } consuming func consume() -> Self { if let component = component { self.component = component.consume() } return self } } @resultBuilder public struct AccumulationSinkComponentBuilder { public static func buildExpression(_ expression: S) -> S where S.Source == Source { expression } public static func buildBlock() -> some AccumulationSink { return _AccumulationSinkGroup() } public static func buildEither(first component: TrueComponent) -> _AccumulationSinkCondition where TrueComponent.Source == Source, FalseComponent.Source == Source { return _AccumulationSinkCondition( trueComponent: component, falseComponent: nil ) } public static func buildEither(second component: FalseComponent) -> _AccumulationSinkCondition where TrueComponent.Source == Source, FalseComponent.Source == Source { return _AccumulationSinkCondition( trueComponent: nil, falseComponent: component ) } public static func buildOptional(_ component: Component?) -> some AccumulationSink where Component : AccumulationSink { return _AccumulationSinkOptional.init(component: component) } // FIXME: add `where repeat (each S).Source == Source` public static func buildBlock(_ sinks: repeat each S) -> _AccumulationSinkGroup { return _AccumulationSinkGroup( component: (repeat each sinks), receiveSource: { source, modification, component in // Waiting https://www.swift.org/blog/pack-iteration/ func iterate(_ left: T) -> T { return left.receive( source: { unsafeBitCast(source(), to: T.Source.self) }, modification: modification ) } let modified = (repeat iterate(each component)) return modified }, receiveOther: { other, current in // Waiting https://www.swift.org/blog/pack-iteration/ func iterate(previous: consuming T, current: consuming T) -> T { return current.receive(previous: previous) } let modified = (repeat iterate(previous: each other, current: each current)) return modified }, consume: { component in // Waiting https://www.swift.org/blog/pack-iteration/ func iterate(_ component: consuming T) -> T { return component.consume() } let modified = (repeat iterate(each component)) return modified } ) } } ================================================ FILE: Sources/Verge/Store/StoreDriverType.swift ================================================ // // Copyright (c) 2019 muukii // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation @available(*, deprecated, renamed: "StoreDriverType") public typealias DispatcherType = StoreDriverType /// A protocol that uses external Store inside and provides the functions. /// ``` /// final class MyViewModel: StoreDriverType { /// /// struct State { /// ... /// } /// /// // If you don't need Activity, you can remove it. /// enum Activity { /// ... /// } /// /// let store: Store /// /// init() { /// self.store = .init(initialState: .init(), logger: nil) /// } /// /// } /// ``` public protocol StoreDriverType: AnyObject where Activity == TargetStore.Activity { associatedtype TargetStore: StoreType associatedtype Scope = TargetStore.State var store: TargetStore { get } var scope: WritableKeyPath & Sendable { get } var state: Changes { get } // WORKAROUND: for activityPublisher() associatedtype Activity: Sendable = TargetStore.Activity } extension StoreDriverType { public func statePublisher() -> some Combine.Publisher, Never> { store.asStore()._statePublisher() } public func activityPublisher() -> some Combine.Publisher { store.asStore()._activityPublisher() } /// A state that cut out from root-state with the scope key path. public nonisolated var state: Changes { store.state.map { $0[keyPath: scope] } } public nonisolated var rootState: Changes { return store.state } } extension StoreDriverType where Scope == TargetStore.State { public var scope: WritableKeyPath & Sendable { \TargetStore.State.self } // MARK: - Subscribings /// Subscribe the state changes /// /// First object always returns true from ifChanged / hasChanges / noChanges unless dropsFirst is true. /// /// - Parameters: /// - dropsFirst: Drops the latest value on started. if true, receive closure will call from next state updated. /// - queue: Specify a queue to receive changes object. /// - Returns: A subscriber that performs the provided closure upon receiving values. public func sinkState( dropsFirst: Bool = false, queue: some TargetQueueType, receive: @escaping @Sendable (Changes) -> Void ) -> StoreStateSubscription { store.asStore()._primitive_sinkState(dropsFirst: dropsFirst, queue: queue, receive: receive) } /// Subscribe the state changes /// /// First object always returns true from ifChanged / hasChanges / noChanges unless dropsFirst is true. /// /// - Parameters: /// - dropsFirst: Drops the latest value on started. if true, receive closure will call from next state updated. /// - queue: Specify a queue to receive changes object. /// - Returns: A subscriber that performs the provided closure upon receiving values. public func sinkState( dropsFirst: Bool = false, queue: some MainActorTargetQueueType = .mainIsolated(), receive: @escaping @MainActor (Changes) -> Void ) -> StoreStateSubscription { store.asStore()._mainActor_sinkState(dropsFirst: dropsFirst, queue: queue, receive: receive) } /// Subscribe the state changes /// /// First object always returns true from ifChanged / hasChanges / noChanges unless dropsFirst is true. /// /// - Parameters: /// - scan: Accumulates a specified type of value over receiving updates. /// - dropsFirst: Drops the latest value on started. if true, receive closure will call from next state updated. /// - queue: Specify a queue to receive changes object. /// - Returns: A subscriber that performs the provided closure upon receiving values. @_disfavoredOverload public func sinkState( scan: Scan, Accumulate>, dropsFirst: Bool = false, queue: some TargetQueueType, receive: @escaping @Sendable (Changes, Accumulate) -> Void ) -> StoreStateSubscription { store.asStore()._primitive_scan_sinkState( scan: scan, dropsFirst: dropsFirst, queue: queue, receive: receive ) } /// Subscribe the state changes /// /// First object always returns true from ifChanged / hasChanges / noChanges unless dropsFirst is true. /// /// - Parameters: /// - scan: Accumulates a specified type of value over receiving updates. /// - dropsFirst: Drops the latest value on started. if true, receive closure will call from next state updated. /// - queue: Specify a queue to receive changes object. /// - Returns: A subscriber that performs the provided closure upon receiving values. @discardableResult public func sinkState( scan: Scan, Accumulate>, dropsFirst: Bool = false, queue: some MainActorTargetQueueType = .mainIsolated(), receive: @escaping @MainActor (Changes, Accumulate) -> Void ) -> StoreStateSubscription { store.asStore()._mainActor_scan_sinkState( scan: scan, dropsFirst: dropsFirst, queue: queue, receive: receive) } /// Subscribe the activity /// /// - Returns: A subscriber that performs the provided closure upon receiving values. @_disfavoredOverload public func sinkActivity( queue: some TargetQueueType, receive: @escaping @Sendable (sending TargetStore.Activity) -> Void ) -> StoreActivitySubscription { store.asStore()._primitive_sinkActivity(queue: queue, receive: receive) } /// Subscribe the activity /// /// - Returns: A subscriber that performs the provided closure upon receiving values. public func sinkActivity( queue: some MainActorTargetQueueType = .mainIsolated(), receive: @escaping @MainActor (sending TargetStore.Activity) -> Void ) -> StoreActivitySubscription { store.asStore()._mainActor_sinkActivity(queue: queue) { activity in MainActor.assumeIsolated { receive(activity) } } } } extension StoreDriverType { /** Commit operation does not mean that emitting latest state for all of subscribers synchronously. Updating state of the store will be updated immediately. To wait until all of the subscribers get the latest state, you can use this method. */ public func waitUntilAllEventConsumed() async { await store.asStore().waitUntilAllEventConsumed() } } extension StoreDriverType { /** Subscribe the state that scoped First object always returns true from ifChanged / hasChanges / noChanges unless dropsFirst is true. - Parameters: - dropsFirst: Drops the latest value on started. if true, receive closure will call from next state updated. - queue: Specify a queue to receive changes object. - Returns: A subscriber that performs the provided closure upon receiving values. */ @_disfavoredOverload public func sinkState( dropsFirst: Bool = false, queue: some TargetQueueType, receive: @escaping @Sendable (Changes) -> Void ) -> StoreStateSubscription { let _scope = scope return store.asStore().sinkState(dropsFirst: dropsFirst, queue: queue) { state in receive(state.map { $0[keyPath: _scope] }) } } /** Subscribe the state that scoped First object always returns true from ifChanged / hasChanges / noChanges unless dropsFirst is true. - Parameters: - dropsFirst: Drops the latest value on started. if true, receive closure will call from next state updated. - queue: Specify a queue to receive changes object. - Returns: A subscriber that performs the provided closure upon receiving values. */ @_disfavoredOverload public func sinkState( dropsFirst: Bool = false, queue: some MainActorTargetQueueType = .mainIsolated(), receive: @escaping @MainActor (Changes) -> Void ) -> StoreStateSubscription { let _scope = scope return store.asStore().sinkState(dropsFirst: dropsFirst, queue: queue) { @MainActor state in receive(state.map { $0[keyPath: _scope] }) } } /// Subscribe the state changes /// /// First object always returns true from ifChanged / hasChanges / noChanges unless dropsFirst is true. /// /// - Parameters: /// - scan: Accumulates a specified type of value over receiving updates. /// - dropsFirst: Drops the latest value on started. if true, receive closure will call from next state updated. /// - queue: Specify a queue to receive changes object. /// - Returns: A subscriber that performs the provided closure upon receiving values. @_disfavoredOverload public func sinkState( scan: Scan, Accumulate>, dropsFirst: Bool = false, queue: some TargetQueueType, receive: @escaping @Sendable (Changes, Accumulate) -> Void ) -> StoreStateSubscription { sinkState(dropsFirst: dropsFirst, queue: queue) { (changes) in let accumulate = scan.accumulate(changes) receive(changes, accumulate) } } /// Subscribe the state changes /// /// First object always returns true from ifChanged / hasChanges / noChanges unless dropsFirst is true. /// /// - Parameters: /// - scan: Accumulates a specified type of value over receiving updates. /// - dropsFirst: Drops the latest value on started. if true, receive closure will call from next state updated. /// - queue: Specify a queue to receive changes object. /// - Returns: A subscriber that performs the provided closure upon receiving values. @_disfavoredOverload public func sinkState( scan: Scan, Accumulate>, dropsFirst: Bool = false, queue: some MainActorTargetQueueType = .mainIsolated(), receive: @escaping @MainActor (Changes, Accumulate) -> Void ) -> StoreStateSubscription { sinkState(dropsFirst: dropsFirst, queue: queue) { @MainActor changes in let accumulate = scan.accumulate(changes) receive(changes, accumulate) } } /// Send activity /// - Parameter activity: public func send( _ name: String = "", _ activity: TargetStore.Activity, _ file: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line ) { let trace = ActivityTrace( name: name, file: file.description, function: function.description, line: line ) store.asStore()._send(activity: activity, trace: trace) } /// Send activity /// - Parameter activity: public func send( _ activity: TargetStore.Activity, _ file: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line ) { send("", activity, file, function, line) } /// Run Mutation that created inline /// /// Throwable public func commit( _ name: String = "", _ file: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line, mutation: (inout Scope) throws -> Result ) rethrows -> Result { let trace = MutationTrace( name: name, file: file, function: function, line: line ) return try store.asStore()._receive_sending( mutation: { [scope] state, _ -> Result in try mutation(&state[keyPath: scope]) } ) } /// Run Mutation that created inline /// /// Throwable public func commit( _ name: String = "", _ file: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line, mutation: (inout Scope, inout Transaction) throws -> Result ) rethrows -> Result { let trace = MutationTrace( name: name, file: file, function: function, line: line ) return try store.asStore()._receive_sending( mutation: { [scope] state, transaction -> Result in transaction.append(trace: trace) return try mutation(&state[keyPath: scope], &transaction) } ) } /// Run Mutation that created inline /// /// Throwable public func commit( _ name: String = "", _ file: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line, mutation: (inout Scope) throws -> Result ) rethrows -> Result where Scope == TargetStore.State { let trace = MutationTrace( name: name, file: file, function: function, line: line ) return try store.asStore()._receive_sending( mutation: { state, transaction -> Result in transaction.append(trace: trace) return try mutation(&state) } ) } /// Run Mutation that created inline /// /// Throwable public func commit( _ name: String = "", _ file: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line, mutation: (inout Scope, inout Transaction) throws -> Result ) rethrows -> Result where Scope == TargetStore.State { let trace = MutationTrace( name: name, file: file, function: function, line: line ) return try store.asStore()._receive_sending( mutation: { state, transaction -> Result in transaction.append(trace: trace) return try mutation(&state, &transaction) } ) } /// Run Mutation that created inline /// /// Throwable public func backgroundCommit( _ name: String = "", _ file: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line, mutation: sending (inout Scope) throws -> sending Result ) async rethrows -> Result { let trace = MutationTrace( name: name, file: file, function: function, line: line ) let result = try await store.asStore().writer.perform { [store = self.store, scope] in let r = try store.asStore()._receive_sending { state, transaction in transaction.append(trace: trace) let r = try mutation(&state[keyPath: scope]) return r } let workaround = { r } return workaround() } await self.waitUntilAllEventConsumed() return result } /// Run Mutation that created inline /// /// Throwable public func backgroundCommit( _ name: String = "", _ file: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line, mutation: sending (inout Scope, inout Transaction) throws -> sending Result ) async rethrows -> Result { let trace = MutationTrace( name: name, file: file, function: function, line: line ) let result = try await store.asStore().writer.perform { [store = self.store, scope] in let r = try store.asStore()._receive_sending { state, transaction in transaction.append(trace: trace) let r = try mutation(&state[keyPath: scope], &transaction) return r } let workaround = { r } return workaround() } await self.waitUntilAllEventConsumed() return result } /// Run Mutation that created inline /// /// Throwable public func backgroundCommit( _ name: String = "", _ file: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line, mutation: sending (inout Scope) throws -> sending Result ) async rethrows -> Result where Scope == TargetStore.State { let trace = MutationTrace( name: name, file: file, function: function, line: line ) let result = try await store.asStore().writer.perform { [store = self.store] in let r = try store.asStore()._receive_sending { state, transaction in transaction.append(trace: trace) let r = try mutation(&state) return r } let workaround = { r } return workaround() } await self.waitUntilAllEventConsumed() return result } /// Run Mutation that created inline /// /// Throwable public func backgroundCommit( _ name: String = "", _ file: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line, mutation: sending (inout Scope, inout Transaction) throws -> Result ) async rethrows -> Result where Scope == TargetStore.State, Self: Sendable { let trace = MutationTrace( name: name, file: file, function: function, line: line ) let result = try await store.asStore().writer.perform { [store = self.store] in let r = try store.asStore()._receive_sending { state, transaction in transaction.append(trace: trace) let r = try mutation(&state, &transaction) return r } let workaround = { r } return workaround() } await self.waitUntilAllEventConsumed() return result } @available(*, deprecated, message: "A detached scope does not support TrackingObject.") public func detached( from newScope: WritableKeyPath & Sendable ) -> DetachedDispatcher { .init(store: store.asStore(), scope: newScope) } public func detached( from newScope: WritableKeyPath & Sendable ) -> DetachedDispatcher { .init(store: store.asStore(), scope: newScope) } // https://muukii.notion.site/Appending-Sendable-WritableKeyPath-makes-non-sendable-KeyPath-10618017d4c1800a8835fce9d6bffeba?pvs=4 /* public func detached( by appendingScope: WritableKeyPath & Sendable ) -> DetachedDispatcher { return .init( store: store.asStore(), scope: scope.appending(path: appendingScope) ) } */ } ================================================ FILE: Sources/Verge/Store/StoreMiddleware.swift ================================================ // // Copyright (c) 2019 muukii // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation public protocol StoreMiddlewareType: Sendable { associatedtype State @Sendable func modify(modifyingState: inout State, transaction: inout Transaction, current: Changes) } public struct AnyStoreMiddleware: StoreMiddlewareType, Sendable { private let closure: @Sendable (_ modifyingState: inout State, _ transaction: inout Transaction, _ current: Changes) -> Void init( modify: @escaping @Sendable ( _ modifyingState: inout State, _ transaction: inout Transaction, _ current: Changes ) -> Void ) { self.closure = modify } public func modify( modifyingState: inout State, transaction: inout Transaction, current: Changes ) { self.closure(&modifyingState, &transaction, current) } } extension StoreMiddlewareType { /** Creates an instance that commits mutations according to the original committing. */ public static func modify( modify: @escaping @Sendable ( _ modifyingState: inout State, _ transaction: inout Transaction, _ current: Changes ) -> Void ) -> Self where Self == AnyStoreMiddleware { return .init(modify: modify) } } ================================================ FILE: Sources/Verge/Store/StoreOperation.swift ================================================ import class Foundation.NSRecursiveLock public enum StoreOperation: Sendable { case nonAtomic case atomic(NSRecursiveLock) func lock() { switch self { case .nonAtomic: break case .atomic(let lock): lock.lock() } } func unlock() { switch self { case .nonAtomic: break case .atomic(let lock): lock.unlock() } } public static var atomic: Self { return .atomic(.init()) } } ================================================ FILE: Sources/Verge/Store/StoreType+Assignee.swift ================================================ // // Copyright (c) 2020 Hiroshi Kimura(Muukii) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation extension StoreDriverType { public typealias Assignee = @Sendable (Value) -> Void /** Returns an asignee function to asign ``` let store1 = Store() let store2 = Store() store1 .derived(.map(\.count)) .assign(to: store2.assignee(\.count)) ``` */ public func assignee( _ keyPath: WritableKeyPath & Sendable ) -> Assignee> { return { [weak store] value in store?.asStore().commit { $0[keyPath: keyPath] = value.primitive } } } /** Returns an asignee function to asign ``` let store1 = Store() let store2 = Store() store1 .derived(.map(\.count)) .assign(to: store2.assignee(\.count)) ``` */ public func assignee( _ keyPath: WritableKeyPath & Sendable ) -> Assignee> { return { [weak store] value in store?.asStore().commit { $0[keyPath: keyPath] = value.primitive } } } /** Returns an asignee function to asign ``` let store1 = Store() let store2 = Store() store1 .derived(.map(\.count)) .assign(to: store2.assignee(\.count)) ``` */ public func assignee( _ keyPath: WritableKeyPath & Sendable ) -> Assignee> { return { [weak store] value in let changes = value.map { Optional.some($0) } store?.asStore().commit { $0[keyPath: keyPath] = .some(value.primitive) } } } /** Assignee to asign Changes object directly. */ public func assignee( _ keyPath: WritableKeyPath & Sendable ) -> Assignee { return { [weak store] value in store?.asStore().commit { $0[keyPath: keyPath] = value } } } /** Assignee to asign Changes object directly. */ public func assignee( _ keyPath: WritableKeyPath & Sendable ) -> Assignee { return { [weak store] value in store?.asStore().commit { [value] a in a[keyPath: keyPath] = value } } } /** Assignee to asign Changes object directly. */ public func assignee( _ keyPath: WritableKeyPath & Sendable ) -> Assignee { return { [weak store] value in store?.asStore().commit { $0[keyPath: keyPath] = .some(value) } } } } ================================================ FILE: Sources/Verge/Store/StoreType+BindingDerived.swift ================================================ // // Copyright (c) 2020 Hiroshi Kimura(Muukii) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import class Foundation.NSString private enum CommitFromBindingDerived: TransactionKey { static var defaultValue: Bool { false } } extension Transaction { var isFromBindingDerived: Bool { get { self[CommitFromBindingDerived.self] } set { self[CommitFromBindingDerived.self] = newValue } } } private struct BindingDerivedPipeline: PipelineType where BackingPipeline.Input == Changes, BackingPipeline.Output == Output { typealias Input = Changes private let backingPipeline: BackingPipeline init(backingPipeline: BackingPipeline) { self.backingPipeline = backingPipeline } func makeStorage() -> BackingPipeline.Storage { backingPipeline.makeStorage() } func yield(_ input: Changes, storage: inout Storage) -> Output { backingPipeline.yield(input, storage: &storage) } func yieldContinuously(_ input: Changes, storage: inout Storage) -> ContinuousResult { if input._transaction.isFromBindingDerived { return .noUpdates } return backingPipeline.yieldContinuously(input, storage: &storage) } } extension StoreDriverType { /// Returns Binding Derived object /// /// - Complexity: 💡 It's better to set `dropsOutput` predicate. /// - Parameters: /// - name: /// - get: /// - dropsOutput: Predicate to drops object if found a duplicated output /// - set: /// - Returns: public func bindingDerived( _ name: String = "", _ file: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line, get pipeline: Pipeline, set: sending @escaping @Sendable (inout TargetStore.State, Pipeline.Output) -> Void, queue: some TargetQueueType = .passthrough ) -> BindingDerived where Pipeline.Input == Changes { let derived = BindingDerived.init( get: BindingDerivedPipeline(backingPipeline: pipeline), set: { [weak self] state in self?.store.asStore() ._receive_sending { inoutRef, transaction in transaction.isFromBindingDerived = true set(&inoutRef, state) } }, initialUpstreamState: store.asStore().state, subscribeUpstreamState: { callback in store.asStore()._primitive_sinkState( dropsFirst: true, queue: queue, receive: callback ) }, retainsUpstream: nil ) store.asStore().onDeinit { [weak derived] in derived?.invalidate() } return derived } public func bindingDerived { bindingDerived( name, file, function, line, get: .select(select), set: { state, newValue in state[keyPath: select] = newValue } ) } public func bindingDerived( _ name: String = "", _ file: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line, select: WritableKeyPath & Sendable, queue: some TargetQueueType = .passthrough ) -> BindingDerived { public static var alwaysFalse: Self { .init { _, _ in false } } private let _equals: (Input, Input) -> Bool /// Creates an instance /// /// - Parameter equals: Return true if two inputs are equal. public init( _ equals: @escaping (Input, Input) -> Bool ) { self._equals = equals } /// It compares the value selected from passed selector closure /// - Parameter selector: public init(selector: @escaping (Input) -> T) { self.init { a, b in selector(a) == selector(b) } } public init(selector: @escaping (Input) -> T, equals: @escaping (T, T) -> Bool) { self.init { a, b in equals(selector(a), selector(b)) } } public init(selector: @escaping (Input) -> T, comparer: Comparer) { self.init { a, b in comparer._equals(selector(a), selector(b)) } } /// Make Combined comparer /// - Parameter comparers: public init(and comparers: [Comparer]) { self.init { pre, new in for filter in comparers { guard filter._equals(pre, new) else { return false } } return true } } /// Make Combined comparer /// - Parameter comparers: public init(or comparers: [Comparer]) { self.init { pre, new in for filter in comparers { if filter._equals(pre, new) { return true } } return false } } public func equals(_ lhs: Input, _ rhs: Input) -> Bool { _equals(lhs, rhs) } /// Returns an curried closure public func curried() -> (_ lhs: Input, _ rhs: Input) -> Bool { _equals } } extension Comparer where Input : Equatable { public init() { self.init(==) } public static var usingEquatable: Self { return .init(==) } } extension Comparer { public func and(_ otherComparer: () -> Comparer) -> Comparer { .init(and: [ self, otherComparer() ]) } public func or(_ otherComparer: () -> Comparer) -> Comparer { .init(or: [ self, otherComparer() ]) } } ================================================ FILE: Tests/VergeTests/PerformanceTests.swift ================================================ import Foundation import XCTest import Verge /** Store's state contains a huge dictionary. This test-case tests to commit mutations: 1. Mutates property beside of huge dictionary. 2. Mutates a huge dictionary. It compares that performance. It would be good if the first 1 test-case is fast without unaffected from a huge dictionary. */ class PerformanceTests: XCTestCase { func testMutationOnAnotherProperty() { let store = DemoStore() measure(metrics: [XCTMemoryMetric(), XCTCPUMetric(), XCTClockMetric()]) { store.increment() } } } ================================================ FILE: Tests/VergeTests/PipelineTests.swift ================================================ import XCTest import Verge final class PipelineTests: XCTestCase { func test_MapPipeline() { let mapCounter = VergeConcurrency.UnfairLockAtomic.init(0) let pipeline = Pipelines.ChangesMapPipeline( intermediate: { $0.count }, transform: { mapCounter.modify { $0 += 1 } return $0 }, additionalDropCondition: nil ) var storage: Void = () do { let s = DemoState() XCTAssertEqual( pipeline.yieldContinuously( Changes.init( old: s, new: s ), storage: &storage ), .noUpdates ) XCTAssertEqual(mapCounter.value, 0) } do { XCTAssertEqual( pipeline.yieldContinuously( Changes.init( old: .init(name: "A", count: 1), new: .init(name: "A", count: 2) ), storage: &storage ), .new(2) ) XCTAssertEqual(mapCounter.value , 2) } } func test_MapPipeline_Intermediate() { let mapCounter = VergeConcurrency.UnfairLockAtomic.init(0) let pipeline = Pipelines.ChangesMapPipeline( intermediate: { $0.name }, transform: { mapCounter.modify { $0 += 1 } return $0.count }, additionalDropCondition: nil ) var storage: Void = () do { let s = DemoState() XCTAssertEqual( pipeline.yieldContinuously( Changes.init( old: s, new: s ), storage: &storage ), .noUpdates ) XCTAssertEqual(mapCounter.value, 0) } do { XCTAssertEqual( pipeline.yieldContinuously( Changes.init( old: .init(name: "A", count: 1), new: .init(name: "A", count: 2) ), storage: &storage ), .noUpdates ) XCTAssertEqual(mapCounter.value, 0) } } func testSelect() { do { let store = Verge.Store(initialState: .init()) // do { // let d = store.derived(.map(\.nonEquatable)) // XCTAssert((d as Any) is Derived>) // } // // do { // let d = store.derived(.map(\.nonEquatable)) // XCTAssert((d as Any) is Derived>) // } do { let d = store.derived(.map(\.onEquatable)) XCTAssert((d as Any) is Derived) } do { let d = store.derived(.map(\.count)) XCTAssert((d as Any) is Derived) } do { let d = store.derived(.map { @Sendable in $0.count }) XCTAssert((d as Any) is Derived) } } } } ================================================ FILE: Tests/VergeTests/ReferenceEdgeTests.swift ================================================ import XCTest import Verge final class FragmentTests: XCTestCase { struct State { var name: String = "" @ReferenceEdge var number = 0 } func testFragment() { var state = State() XCTAssertEqual(state.number, 0) state.number += 1 XCTAssertEqual(state.number, 1) } func testFragmentWithCopy() { let state = State() XCTAssertEqual(state.number, 0) var anotherState = state anotherState.number += 1 XCTAssertEqual(anotherState.number, 1) XCTAssertNotEqual(state.$number._storagePointer, anotherState.$number._storagePointer) } func testReference() { var state = State() state.number += 1 state.name = "A" var state2 = state state2.name = "B" XCTAssert(state.$number._storagePointer == state2.$number._storagePointer) } func testCast() { let source = ReferenceEdge(wrappedValue: State()) var binded = unsafeBitCast(source, to: ReferenceEdge.self) print(binded.name) binded.name = "hiroshi" XCTAssertEqual(binded.name, "hiroshi") } } ================================================ FILE: Tests/VergeTests/Retain/PublisherCompletionTests.swift ================================================ import Foundation import Combine import Verge import XCTest final class SubjectCompletionTests: XCTestCase { func testStatePublisherCompletion1() { var bag = Set() var store: DemoStore? = DemoStore() weak var weakStore: DemoStore? = store store?.statePublisher() .sink( receiveCompletion: { _ in XCTFail() }, receiveValue: { _ in } ) .store(in: &bag) XCTAssertNotNil(weakStore) store = nil XCTAssertNil(weakStore) } func testActivityPublisherCompletion1() { var bag = Set() var store: DemoStore? = DemoStore() weak var weakStore: DemoStore? = store store? .activityPublisher() .sink( receiveCompletion: { _ in XCTFail() }, receiveValue: { _ in } ) .store(in: &bag) XCTAssertNotNil(weakStore) store = nil XCTAssertNil(weakStore) bag.forEach { $0.cancel() } XCTAssertNil(weakStore) } func testActivityPublisherCompletion2() { var bag = Set() var store: DemoStore? = DemoStore() weak var weakStore: DemoStore? = store store!.activityPublisher() .sink( receiveCompletion: { _ in XCTFail("It should not be called, as the subscription was canceled before publishing completion.") }, receiveValue: { [store] _ in withExtendedLifetime(store) {} } ) .store(in: &bag) XCTAssertNotNil(weakStore) store = nil XCTAssertNotNil(weakStore) bag.forEach { $0.cancel() } XCTAssertNil(weakStore) } func testActivityPublisherCompletion_deallocated() { var bag = Set() var store: DemoStore? = DemoStore() weak var weakStore: DemoStore? = store var strongRef: Ref? = Ref() weak var weakRef = strongRef store!.activityPublisher() .sink( receiveCompletion: { _ in XCTFail() }, receiveValue: { _ in print("") withExtendedLifetime(strongRef) {} } ) .store(in: &bag) strongRef = nil store = nil XCTAssertNil(weakStore) XCTAssertNil(weakRef) } func testDerived_publisher_retains_derived() { var c: AnyCancellable? let ref = withReference(DemoStore()) let derivedRef = withReference(ref.value!.derived(.map(\.count))) c = derivedRef.value! .statePublisher() .sink( receiveCompletion: { _ in XCTFail() }, receiveValue: { _ in } ) derivedRef.release() XCTAssertNotNil(derivedRef.value) c?.cancel() XCTAssertNil(derivedRef.value) } func testDerived_stream_stops_on_store_deallocated() async { var c: AnyCancellable? let storeRef = withReference(DemoStore()) let derivedRef = withReference(storeRef.value!.derived(.map(\.count))) storeRef.release() await Task.yield() XCTAssertNil(storeRef.value) c = derivedRef.value! .statePublisher() .sink( receiveCompletion: { _ in XCTFail() }, receiveValue: { _ in } ) derivedRef.release() await Task.yield() // still not nil as the subscription is still on going. XCTAssertNotNil(derivedRef.value) XCTAssertNil(storeRef.value) withExtendedLifetime(c, {}) c?.cancel() XCTAssertNil(derivedRef.value) } class Ref {} } final class Reference: @unchecked Sendable, CustomDebugStringConvertible { weak var value: T? private var strong: T? init(_ object: T){ self.value = object self.strong = object } func release() { strong = nil } var debugDescription: String { "\(value)" } } func withReference(_ object: T) -> Reference { return .init(object) } ================================================ FILE: Tests/VergeTests/Retain/StoreSinkSusbscriptionTests.swift ================================================ import Verge import XCTest import Atomics final class StoreSinkSubscriptionTests: XCTestCase { func test_Store_sinkState_stops_on_store_deallocated() { let storeRef = withReference(DemoStore()) let resourceRef = withReference(Resource()) storeRef.value! .sinkState { [ref = resourceRef.value!] _ in withExtendedLifetime(ref) {} } .storeWhileSourceActive() resourceRef.release() XCTAssertNotNil(storeRef.value) XCTAssertNotNil(resourceRef.value) storeRef.release() XCTAssertNil(storeRef.value) XCTAssertNil(resourceRef.value) } func test_Store_sinkState_canceled_by_others() { var cancellable: StoreStateSubscription? let storeRef = withReference(DemoStore()) let resourceRef = withReference(Resource()) cancellable = storeRef.value! .sinkState { [ref = resourceRef.value!] _ in withExtendedLifetime(ref) {} } .storeWhileSourceActive() resourceRef.release() XCTAssertNotNil(storeRef.value) XCTAssertNotNil(resourceRef.value) cancellable?.cancel() XCTAssertNotNil(storeRef.value) XCTAssertNil(resourceRef.value) } func test_Store_sinkActivity_stops_on_store_deallocated() { let storeRef = withReference(DemoStore()) let resourceRef = withReference(Resource()) storeRef.value! .sinkActivity { [ref = resourceRef.value!] _ in withExtendedLifetime(ref) {} } .storeWhileSourceActive() resourceRef.release() XCTAssertNotNil(storeRef.value) XCTAssertNotNil(resourceRef.value) storeRef.release() XCTAssertNil(storeRef.value) XCTAssertNil(resourceRef.value) } func test_Derived_lifeCycle_with_source() { let storeRef = withReference(DemoStore()) let resourceRef = withReference(Resource()) let derivedRef = withReference(storeRef.value!.derived(.select(\.count))) derivedRef.value! .sinkState { [ref = resourceRef.value!] _ in withExtendedLifetime(ref) {} } .storeWhileSourceActive() resourceRef.release() XCTAssertNotNil(storeRef.value) XCTAssertNotNil(resourceRef.value) storeRef.release() XCTAssertNil(storeRef.value) XCTAssertNil(resourceRef.value) } func test_Derived_lifeCycle_with_source_release_store_earlier() { let storeRef = withReference(DemoStore()) let resourceRef = withReference(Resource()) // make derived let derivedRef = withReference(storeRef.value!.derived(.select(\.count))) // start subscribe derivedRef.value! .sinkState { [ref = resourceRef.value!] _ in withExtendedLifetime(ref) {} } .storeWhileSourceActive() // make the resource is captured only from the closure resourceRef.release() XCTAssertNotNil(resourceRef.value) // release store earlier than stop derived storeRef.release() // should be nil because derived won't retain store as upstream XCTAssertNil(storeRef.value) // released all resouces XCTAssertNil(resourceRef.value) } func testCompare() { let wasInvalidated = Atomics.ManagedAtomic(false) XCTAssertTrue(wasInvalidated.compareExchange(expected: false, desired: true, ordering: .relaxed).exchanged) XCTAssertFalse(wasInvalidated.compareExchange(expected: false, desired: true, ordering: .relaxed).exchanged) } /** Store won't retain the Derived */ func testRelease() { let wrapper = DemoStore() let ref = withReference(wrapper.derived(.map { @Sendable in $0.count }, queue: .passthrough)) ref.release() XCTAssertNil(ref.value) } func testRetain() { let store = DemoStore() let derivedRef = withReference(store.derived(.map { @Sendable in $0.count }, queue: .passthrough)) let expectation = XCTestExpectation(description: "receive changes") expectation.expectedFulfillmentCount = 1 expectation.assertForOverFulfill = true let subscription = derivedRef .value! .sinkState( dropsFirst: true, queue: .passthrough ) { (changes) in expectation.fulfill() } XCTAssertNotNil(derivedRef.value) derivedRef.release() XCTAssertNotNil(derivedRef.value, "as still subscribing") store.commit { _ in } store.commit { $0.count += 1 } subscription.cancel() XCTAssertNil(derivedRef.value) wait(for: [expectation], timeout: 1) } func test_derived_chain() { let wrapper = DemoStore() var baseSlice: Derived! = wrapper.derived(.map { @Sendable in $0.count }, queue: .passthrough) weak var weakBaseSlice = baseSlice var slice: Derived! = baseSlice.derived(.map { @Sendable in $0.self }, queue: .passthrough) baseSlice = nil weak var weakSlice = slice XCTAssertEqual(slice.state.primitive, 0) XCTAssertEqual(slice.state.version, 0) XCTAssertEqual(slice.state.hasChanges(\.self), true) XCTAssertNotNil(weakBaseSlice) wrapper.increment() XCTAssertEqual(slice.state.primitive, 1) XCTAssertEqual(slice.state.version, 1) XCTAssertEqual(slice.state.hasChanges(\.self), true) XCTAssertNotNil(weakBaseSlice) wrapper.empty() XCTAssertEqual(slice.state.primitive, 1) XCTAssertEqual(slice.state.version, 1) // with memoized, version not changed XCTAssertEqual(slice.state.hasChanges(\.self), true) XCTAssertNotNil(weakBaseSlice) wrapper.increment() XCTAssertEqual(slice.state.primitive, 2) XCTAssertEqual(slice.state.version, 2) XCTAssertEqual(slice.state.hasChanges(\.self), true) XCTAssertNotNil(weakBaseSlice) slice = nil XCTAssertNil(weakSlice) XCTAssertNil(weakBaseSlice) } final class Resource { deinit { print("d") } } } ================================================ FILE: Tests/VergeTests/RunLoopTests.swift ================================================ import XCTest @testable import Verge final class RunLoopTests: XCTestCase { func test_performance_adding() { measure { for _ in 0..<1000 { RunLoopActivityObserver.addObserver(acitivity: .beforeWaiting, in: .main) { } } } } } ================================================ FILE: Tests/VergeTests/Sample.swift ================================================ // // Sample.swift // VergeStoreTests // // Created by muukii on 2020/04/18. // Copyright © 2020 muukii. All rights reserved. // import Foundation #if canImport(UIKIt) import UIKit import Verge enum Sample { struct State: StateType { var name: String = "" var age: Int = 0 } enum Activity { case somethingHappen } class ViewController: UIViewController { private let nameLabel: UILabel = .init() private let ageLabel: UILabel = .init() let store = Store(initialState: .init(), logger: nil) var subscriptions = Set() override func viewDidLoad() { super.viewDidLoad() store.sinkState { [weak self] (changes) in self?.update(changes: changes) } .store(in: &subscriptions) } private func update(changes: Changes) { changes.ifChanged(\.name) { (name) in nameLabel.text = name } changes.ifChanged(\.age) { (age) in ageLabel.text = age.description } } } } #endif ================================================ FILE: Tests/VergeTests/StateTypeTests.swift ================================================ import Foundation import Verge import XCTest final class StateTypeTests: XCTestCase { func test_init() { let store = Store(initialState: .init()) XCTAssertEqual(store.state.count2, 1) } func test_store() { let store = Store(initialState: .init()) store.commit { $0.count = 1 } XCTAssertEqual(store.state.count2, 2) store.commit { $0.name = "a" } XCTAssertEqual(store.state.count2, 2) } struct State: StateType { var name = "" var count = 0 var count2 = 0 static func reduce( modifying: inout StateTypeTests.State, transaction: inout Transaction, current: Changes ) { current.ifChanged(\.count).do { _ in modifying.count2 += 1 } } } } ================================================ FILE: Tests/VergeTests/StoreAndDerivedTests.swift ================================================ import Verge import XCTest final class StoreAndDerivedTests: XCTestCase { @MainActor func test() async { let store = Store<_, Never>(initialState: DemoState()) let _ = store.derived(.select(\.name)) let countDerived = store.derived(.select(\.count)) await withTaskGroup(of: Void.self) { group in for i in 0..<1000 { group.addTask { await withBackground { store.commit { $0.name = "\(i)" } } } } group.addTask { await withBackground { store.commit { $0.count = 100 } } XCTAssertEqual(store.state.count, 100) await store.waitUntilAllEventConsumed() // potentially it fails as EventEmitter's behavior // If EventEmitter's buffer is not empty, commit function escape from the stack by only adding. XCTAssertEqual(countDerived.state.primitive, 100) } } print("end") } } /** Performs the given task in background */ public nonisolated func withBackground( _ thunk: @escaping @Sendable () async throws -> Return ) async rethrows -> Return { // for now we will keep this until Swift6. assert(Thread.isMainThread == false) // here is the background as it's nonisolated // to inherit current actor context, use @_unsafeInheritExecutor // thunk closure runs on the background as it's sendable // if it's not sendable, inherit current actor context but it's already background. // @_inheritActorContext makes closure runs on current actor context even if it's sendable. return try await thunk() } ================================================ FILE: Tests/VergeTests/StoreInitTests.swift ================================================ import XCTest import Verge final class StoreInitTests: XCTestCase { class RefState { } /* class RefViewModel: StoreComponentType { class State: Equatable { static func == (lhs: StoreInitTests.RefViewModel.State, rhs: StoreInitTests.RefViewModel.State) -> Bool { true } init() {} } let store: DefaultStore init() { // it raises a warning self.store = DefaultStore(initialState: .init()) } } */ class StructViewModel: StoreDriverType { struct State: Equatable { init() {} } let store: Store init() { self.store = .init(initialState: .init()) } } } ================================================ FILE: Tests/VergeTests/StoreMiddlewareTests.swift ================================================ import Testing @Suite("StoreMiddlewareTests") struct StoreMiddlewareTests { @Test("Commit Hook") func testCommitHook() { let store = DemoStore() store.add( middleware: .modify { @Sendable modifyingState, transaction, current in current.ifChanged(\.count).do { _ in modifyingState.count += 1 } }) store.add( middleware: .modify { @Sendable modifyingState, transaction, current in current.ifChanged(\.name).do { _ in modifyingState.count = 100 } }) #expect(store.state.count == 0) store.commit { $0.count += 1 } #expect(store.state.count == 2) store.commit { $0.name = "A" } if case .graph(let graph) = store.state.modification { graph.prettyPrint() #expect( graph.prettyPrint() == """ VergeTests.DemoState { name-(1)+(1) count-(1)+(1) } """) } #expect(store.state.count == 100) } } ================================================ FILE: Tests/VergeTests/StoreSinkTests.swift ================================================ import Verge import XCTest @MainActor fileprivate func UI() { } final class StoreSinkTests: XCTestCase { func testMainActorSubscription() { let store = DemoStore() _ = store.sinkState { _ in UI() } _ = store.sinkState(queue: .main) { @MainActor _ in UI() } } func testNonActorSubscription() { let store = DemoStore() _ = store.sinkState(queue: .passthrough) { _ in Task { await UI() } } } } ================================================ FILE: Tests/VergeTests/StoreTaskTests.swift ================================================ import Verge import XCTest import Atomics final class StoreTaskTests: XCTestCase { func test() { let atomic = ManagedAtomic.init(false) do { let r = atomic.compareExchange(expected: true, desired: true, ordering: .sequentiallyConsistent).exchanged print(r) } do { let r = atomic.compareExchange(expected: true, desired: false, ordering: .sequentiallyConsistent).exchanged print(r) } do { let r = atomic.compareExchange(expected: false, desired: true, ordering: .sequentiallyConsistent).exchanged print(r) } } @MainActor func testActorContext_onMainActor() async throws { let store = DemoStore() try await store.task { XCTAssertTrue(Thread.isMainThread) } .value try await store.taskDetached { XCTAssertFalse(Thread.isMainThread) } .value } func testActorContext() async throws { let store = DemoStore() try await store.task { XCTAssertFalse(Thread.isMainThread) } .value try await store.taskDetached { XCTAssertFalse(Thread.isMainThread) } .value } } ================================================ FILE: Tests/VergeTests/SynchronizeDisplayValueTests.swift ================================================ // // SynchronizeDisplayValueTests.swift // VergeStoreTests // // Created by muukii on 2019/11/09. // Copyright © 2019 muukii. All rights reserved. // import Foundation import XCTest import Verge ================================================ FILE: Tests/VergeTests/SyntaxTests.swift ================================================ // // SyntaxTests.swift // VergeStoreTests // // Created by muukii on 2020/05/24. // Copyright © 2020 muukii. All rights reserved. // import Foundation import Verge import VergeMacros enum SyntaxTests { static func code() { let changes: Changes = .init(old: nil, new: .init()) changes.ifChanged(\.name).do { name in } // changes.ifChanged({ ($0.name, $0.name) }) { args in // } changes.ifChanged({ $0.nonEquatable }, comparator: .alwaysFalse()).do { name in } changes.ifChanged({ $0.name }).do { name in } changes.ifChanged(\.name, \.count).do { name, count in } } } ================================================ FILE: Tests/VergeTests/TransactionTests.swift ================================================ import Verge import XCTest final class TransactionTests: XCTestCase { func testTransaction() async { struct MyKey: TransactionKey { static var defaultValue: String? { nil } } let store = Store(initialState: .init()) await store.commit { $0.count += 1 $1[MyKey.self] = "first commit" } XCTAssertEqual(store.state._transaction[MyKey.self], "first commit") } } ================================================ FILE: Tests/VergeTests/Usage.swift ================================================ import Verge struct RootState: Equatable { struct Nested: Equatable {} var nested: Nested = .init() } final class RootStore: Store, @unchecked Sendable { } final class Service: StoreDriverType { var store: RootStore { fatalError() } init() { } } final class SpecificService: StoreDriverType { var scope: WritableKeyPath & Sendable { \.nested } var store: RootStore { fatalError() } init() { _ = sinkState { state in let _: Changes = state } } } final class ViewModel: StoreDriverType { struct State: Equatable {} let store: Store = .init(initialState: .init()) init() { commit { _ in } _ = sinkState { state in let _: Changes = state } } } // MARK: - Abstraction protocol MyViewModelType: StoreDriverType where TargetStore.State == String, TargetStore.Activity == Int { } final class Concrete: MyViewModelType { let store: Verge.Store init() { store = .init(initialState: .init()) _ = sinkState { _ in } commit { _ in } } } final class Controller where ViewModel.Scope == ViewModel.TargetStore.State { init(viewModel: ViewModel) { let _: Changes = viewModel.state viewModel.send(1) } } ================================================ FILE: Tests/VergeTests/VergeStoreTests.swift ================================================ // // VergeStoreTests.swift // VergeStoreTests // // Created by muukii on 2019/11/04. // Copyright © 2019 muukii. All rights reserved. // import XCTest import Verge import os.lock import Combine @available(iOS 13.0, *) final class VergeStoreTests: XCTestCase { @Tracking struct _State { @Tracking struct TreeA { } @Tracking struct TreeB { } @Tracking struct TreeC { } struct NestedState: Equatable { var myName: String = "" } struct OptionalNestedState: Equatable { var myName: String = "" } var count: Int = 0 var optionalNested: OptionalNestedState? var nested: NestedState = .init() var treeA: TreeA = TreeA() var treeB: TreeB = TreeB() var treeC: TreeC = TreeC() } final class _Store: Verge.Store<_State, Never>, @unchecked Sendable { init() { super.init(initialState: .init(), logger: DefaultStoreLogger.default) } } class RootDispatcher: StoreDriverType { enum Error: Swift.Error { case something } var scope: WritableKeyPath { \.self } let store: _Store init(store: _Store) { self.store = store } func resetCount() { return commit { s in s.count = 0 } } func increment() { commit { $0.count += 1 } } func setNestedState() { commit { $0.optionalNested = .init() } } func setMyName() { commit { if $0.optionalNested != nil { $0.optionalNested?.myName = "Muuk" } } } func returnSomeValue() -> String { return commit { _ in return "Hello, Verge" } } func continuousIncrement() { increment() increment() } func failableIncrement() throws { try commit { state in throw Error.something } } func hoge() { let _detached = detached(from: \.nested) let _: Changes = _detached.state _detached.commit { (state: inout TargetStore.State.NestedState) in } let optionalNestedTarget = detached(from: \.optionalNested) let _: Changes = optionalNestedTarget.state optionalNestedTarget.commit { (state: inout TargetStore.State.OptionalNestedState?) in } } } /** Use Edge due to TreeA does not have Equatable. */ final class TreeADispatcher: StoreDriverType { let store: _Store let scope: WritableKeyPath & Sendable = \.treeA init(store: _Store) { self.store = store } func operation() { let _: Changes<_State.TreeA> = state commit { (state: inout _State.TreeA) in } let treeB = detached(from: \.treeB) let _: Changes<_State.TreeB> = treeB.state treeB.commit { (state: inout _State.TreeB) in } } } let store = _Store() lazy var dispatcher = RootDispatcher(store: self.store) var subs = Set() override func setUp() { // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. } func testCommit() { let store = DemoStore() store.commit { $0.count = 100 } XCTAssertEqual(store.state.count, 100) store.commit { $0.inner.name = "mmm" } XCTAssertEqual(store.state.inner.name, "mmm") let exp = expectation(description: "async") DispatchQueue.global().async { store.commit { $0.inner.name = "xxx" } XCTAssertEqual(store.state.inner.name, "xxx") exp.fulfill() } wait(for: [exp], timeout: 1) } func testEmptyCommit() { let store = DemoStore() let count = VergeConcurrency.UnfairLockAtomic.init(0) let subs = store.sinkState(queue: .passthrough) { (_) in count.modify { $0 += 1 } } XCTAssertEqual(store.state.version, 0) store.commit { $0.count = 100 } XCTAssertEqual(store.state.version, 1) store.commit { _ in } // no changes XCTAssertEqual(store.state.version, 1) store.commit { $0.count = 100 } // many times calling empty commits for _ in 0..<3 { store.commit { _ in } } // no affects from read a value store.commit { if $0.count > 100 { $0.count = 0 XCTFail() } } XCTAssertEqual(store.state.version, 2) XCTAssertEqual(count.value, 3) withExtendedLifetime(subs, {}) } func testDispatch() { dispatcher.resetCount() dispatcher.resetCount() dispatcher.resetCount() dispatcher.continuousIncrement() XCTAssert(store.state.primitive.count == 2) } func testTryMutation() { do { try dispatcher.failableIncrement() XCTFail() } catch { } } func testIncrement() { dispatcher.increment() XCTAssertEqual(store.state.primitive.count, 1) } func testTargetingCommit() { dispatcher.setNestedState() dispatcher.setMyName() XCTAssertEqual(store.state.primitive.optionalNested?.myName, "Muuk") } func testReturnAnyValueFromMutation() { let r = dispatcher.returnSomeValue() XCTAssertEqual(r, "Hello, Verge") } func testSubscription() { var subscriptions = Set() let count = VergeConcurrency.UnfairLockAtomic.init(0) store.sinkState(queue: .passthrough) { (changes) in count.modify { $0 += 1 } } .store(in: &subscriptions) store.commit { $0.count += 1 } // stop subscribing subscriptions = .init() store.commit { $0.count += 1 } XCTAssertEqual(count.value, 2) } @MainActor func testChangesPublisher() { let store = DemoStore() XCTContext.runActivity(named: "Premise") { (activity) in XCTAssertEqual(store.state.hasChanges(\.count), true) store.commit { $0.count = $0.count } XCTAssertEqual(store.state.hasChanges(\.count), false) } XCTContext.runActivity(named: "startsFromInitial: true") { (activity) in let exp1 = expectation(description: "") _ = store.statePublisher() .sink { changes in exp1.fulfill() XCTAssertEqual(changes.hasChanges(\.count), true) } XCTAssertEqual(exp1.expectedFulfillmentCount, 1) wait(for: [exp1], timeout: 1) } } func testAsigneeFromStore() { let store1 = DemoStore() let store2 = DemoStore() let sub = store1 .assign(to: store2.assignee(\.recursive)) store1.commit { $0.count += 1 } XCTAssertEqual(store1.state.primitive.count, store2.state.primitive.recursive?.count) store1.commit { $0.count += 1 } XCTAssertEqual(store1.state.primitive.count, store2.state.primitive.recursive?.count) withExtendedLifetime(sub, {}) } enum MyError: Error { case something } func testThrowing() { let store = DemoStore() do { try store.commit { _ in throw MyError.something } } catch { print(error) } } func testAsigneeFromDerived() { let store1 = DemoStore() let store2 = DemoStore() let sub = store1 .derived(.map(\.count), queue: .passthrough) .assign(to: store2.assignee(\.count)) store1.commit { $0.count += 1 } XCTAssertEqual(store1.state.primitive.count, store2.state.primitive.count) store1.commit { $0.count += 1 } XCTAssertEqual(store1.state.primitive.count, store2.state.primitive.count) withExtendedLifetime(sub, {}) } final class DemoStoreWrapper2: StoreDriverType { struct State: Equatable { var source: Changes } let store: Verge.Store var sub: StoreStateSubscription? = nil init(sourceStore: DemoStore) { let d = sourceStore .derived(.map(\.count), queue: .passthrough) self.store = .init(initialState: .init(source: d.state), logger: nil) sub = d.assign(to: store.assignee(\.source)) } } func testAsignee2() { let store1 = DemoStore() let store2 = DemoStoreWrapper2(sourceStore: store1) store1.commit { $0.count += 1 } XCTAssertEqual(store1.state.primitive.count, store2.state.primitive.source.root) store1.commit { $0.count += 1 } XCTAssertEqual(store1.state.primitive.count, store2.state.primitive.source.root) } func testScan() { let store1 = DemoStore() let expect = expectation(description: "") let subscription = store1.sinkState(scan: Scan(seed: 0, accumulator: { v, c in v += 1 })) { changes, accumulated in XCTAssertEqual(accumulated, 1) expect.fulfill() } withExtendedLifetime(subscription) {} wait(for: [expect], timeout: 1) } func testMapIfPresent() { let store = _Store() XCTAssert(store.state.optionalNested == nil) do { let state = store.state if let _ = state.mapIfPresent(\.optionalNested) { XCTFail() } } store.commit { $0.optionalNested = .init() } do { let state = store.state if let nested = state.mapIfPresent(\.optionalNested) { XCTAssert(nested.previous == nil) } else { XCTFail() } } store.commit { $0.optionalNested!.myName = "hello" } do { let state = store.state if let nested = state.mapIfPresent(\.optionalNested) { XCTAssert(nested.previous != nil) } else { XCTFail() } } } } ================================================ FILE: Tests/VergeTinyTests/VergeTinyTests.swift ================================================ // // Copyright (c) 2021 Hiroshi Kimura(Muukii) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import VergeTiny import XCTest final class VergeTinyTests: XCTestCase { func testDefineProperty() { let filter = DynamicPropertyStorage() let property = filter.defineProperty(Int.self) XCTAssertEqual(property.value, nil) property.value = 1 XCTAssertEqual(property.value, 1) } func testDistinct() { let filter = DynamicPropertyStorage() let property = filter.defineProperty(Int.self) let exp = expectation(description: "") exp.assertForOverFulfill = true exp.expectedFulfillmentCount = 2 property.doIfChanged(1) { value in XCTAssertEqual(value, 1) exp.fulfill() } property.doIfChanged(1) { value in XCTFail() } property.doIfChanged(2) { value in XCTAssertEqual(value, 2) exp.fulfill() } wait(for: [exp], timeout: 0) } func testDistinctOptional() { let filter = DynamicPropertyStorage() let property = filter.defineProperty(Int?.self) let exp = expectation(description: "") exp.assertForOverFulfill = true exp.expectedFulfillmentCount = 4 property.doIfChanged(nil) { value in XCTAssertEqual(value, nil) exp.fulfill() } property.doIfChanged(nil) { value in XCTFail() } property.doIfChanged(1) { value in XCTAssertEqual(value, 1) exp.fulfill() } property.doIfChanged(1) { value in XCTFail() } property.doIfChanged(2) { value in XCTAssertEqual(value, 2) exp.fulfill() } property.doIfChanged(nil) { value in XCTAssertEqual(value, nil) exp.fulfill() } property.doIfChanged(nil) { value in XCTFail() } wait(for: [exp], timeout: 0) } func testPerformanceCreatingProperty() { let filter = DynamicPropertyStorage() measure { let _ = filter.defineProperty(Int?.self) } } func testPerformanceAccessingProperty() { let filter = DynamicPropertyStorage() let property = filter.defineProperty(Int.self) property.value = 1 measure { property.value? += 1 } } } ================================================ FILE: Verge.playground/Pages/Memo.xcplaygroundpage/Contents.swift ================================================ enum Action { case increment case decrement } class Store { var state: State } ================================================ FILE: Verge.playground/Pages/PlainStorePattern1.xcplaygroundpage/Contents.swift ================================================ import UIKit class ViewA: DemoUIView { // MARK: Components let nameLabel = UILabel() // MARK: State var name: String = "" { didSet { self.nameLabel.text = name } } } //: [Next](@next) ================================================ FILE: Verge.playground/Pages/PlainStorePattern2.xcplaygroundpage/Contents.swift ================================================ //: [Previous](@previous) import UIKit class ViewA: DemoUIView { // MARK: Components let nameLabel = UILabel() let ageLabel = UILabel() // MARK: State var name: String = "" { didSet { self.nameLabel.text = name } } var age: Int = 0 { didSet { self.ageLabel.text = name } } } //: [Next](@next) ================================================ FILE: Verge.playground/Pages/PlainStorePattern3.xcplaygroundpage/Contents.swift ================================================ //: [Previous](@previous) import UIKit class ViewA: DemoUIView { // MARK: Components let nameLabel = UILabel() let ageLabel = UILabel() // MARK: State struct State { var name: String = "" var age: Int = 0 } var state: State { didSet { self.nameLabel.text = state.name self.ageLabel.text = state.age.description } } } //: [Next](@next) ================================================ FILE: Verge.playground/Pages/PlainStorePattern4.xcplaygroundpage/Contents.swift ================================================ //: [Previous](@previous) import Foundation import UIKit struct State { var name: String = "" var age: Int = 0 } class Store { // State type should be struct type to call didSet var state: State { didSet { // notify a update to subscribers } } init(_ state: State) { self.state = state } func onDidUpdate(_ closure: @escaping (State) -> Void) { // implement to notfy updates } } class ViewA: DemoUIView { // MARK: Components let nameLabel = UILabel() let ageLabel = UILabel() // MARK: State let store: Store // MARK: - Initializers init(store: Store) { self.store = store super.init() store.onDidUpdate { [weak self] state in guard let self = self else { return } self.ageLabel.text = state.age.description self.nameLabel.text = state.name } } private func updateName() { store.state.name = "Muukii" // then, nameLabel updates to "Muukii" by store.onDidUpdate. } } class ViewB: DemoUIView { // MARK: Components let nameLabel = UILabel() let ageLabel = UILabel() // MARK: State let store: Store // MARK: - Initializers init(store: Store) { self.store = store super.init() store.onDidUpdate { [weak self] state in guard let self = self else { return } self.ageLabel.text = state.age.description self.nameLabel.text = state.name } } } //: [Next](@next) ================================================ FILE: Verge.playground/Pages/PlainStorePattern5.xcplaygroundpage/Contents.swift ================================================ //: [Previous](@previous) import Foundation import UIKit struct State { var name: String = "" var age: Int = 0 } class Store { // State type should be struct type to call didSet var state: State { didSet { // notify a update to subscribers let old = oldValue let new = state } } init(_ state: State) { self.state = state } func onDidUpdate(_ closure: @escaping (_ old: State, _ new: State) -> Void) { // implement to notfy updates } } class ViewA: DemoUIView { // MARK: Components let nameLabel = UILabel() let ageLabel = UILabel() // MARK: State let store: Store // MARK: - Initializers init(store: Store) { self.store = store super.init() store.onDidUpdate { [weak self] old, new in guard let self = self else { return } if old.name != new.name { self.nameLabel.text = new.name } if old.age != new.age { self.ageLabel.text = new.age.description } } } private func updateName() { store.state.name = "Muukii" // then, nameLabel updates to "Muukii" by store.onDidUpdate. } } class ViewB: DemoUIView { // MARK: Components let nameLabel = UILabel() let ageLabel = UILabel() // MARK: State let store: Store // MARK: - Initializers init(store: Store) { self.store = store super.init() store.onDidUpdate { [weak self] old, new in guard let self = self else { return } if old.name != new.name { self.nameLabel.text = new.name } if old.age != new.age { self.ageLabel.text = new.age.description } } } } //: [Next](@next) ================================================ FILE: Verge.playground/Pages/VergeStorePartern2.xcplaygroundpage/Contents.swift ================================================ //: [Previous](@previous) import UIKit import Verge struct State { var name: String = "" var age: Int = 0 } class Store: VergeStore.Store { init() { super.init(initialState: State(), logger: nil) } func incrementAge() { commit { $0.age += 1 } } func updateName(_ name: String) { commit { $0.name = name } } } class ViewA: DemoUIView { let nameLabel = UILabel() let ageLabel = UILabel() let store: Store private var cancellable: VergeAnyCancellable? init(store: Store) { self.store = store super.init() cancellable = store.sinkChanges { [weak self] (changes) in self?.updateUI(changes: changes) } } private func updateUI(changes: Changes) { changes.ifChanged(\.name) { name in nameLabel.text = name } changes.ifChanged(\.age) { age in ageLabel.text = age.description } } } //: [Next](@next) ================================================ FILE: Verge.playground/Pages/VergeStorePattern.xcplaygroundpage/Contents.swift ================================================ //: [Previous](@previous) import Foundation import UIKit import Verge struct State { var name: String = "" var age: Int = 0 } class Store: VergeStore.Store { init() { super.init(initialState: State(), logger: nil) } } class ViewA: DemoUIView { // MARK: Components let nameLabel = UILabel() let ageLabel = UILabel() // MARK: State let store: Store private var cancellable: VergeAnyCancellable? // MARK: - Initializers init(store: Store) { self.store = store super.init() cancellable = store.sinkChanges { [weak self] (changes) in guard let self = self else { return } changes.ifChanged(\.name) { name in self.nameLabel.text = name } changes.ifChanged(\.age) { age in self.ageLabel.text = age.description } } } private func updateName() { store.commit { $0.name = "Muukii" } } } //: [Next](@next) ================================================ FILE: Verge.playground/Sources/Wire.swift ================================================ import UIKit open class DemoUIView: UIView { public init() { super.init(frame: .zero) } @available(*, unavailable) public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } ================================================ FILE: Verge.playground/contents.xcplayground ================================================ ================================================ FILE: mise.toml ================================================ [tools] tuist = "4.44.3" ================================================ FILE: playgrounds/.gitignore ================================================ !*.xcodeproj ================================================ FILE: playgrounds/PlaySwiftUI/PlaySwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: playgrounds/PlaySwiftUI/PlaySwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: playgrounds/PlaySwiftUI/PlaySwiftUI/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: playgrounds/PlaySwiftUI/PlaySwiftUI/ContentView.swift ================================================ // // ContentView.swift // PlaySwiftUI // // Created by Muukii on 2025/02/13. // import SwiftUI import Verge @Tracking struct ContentState { @Tracking struct Info { let constant: String var name: String init(name: String = "Init") { self.constant = name self.name = name } } var info = Info() var count: Int = 0 } enum AppContainer { static let store = Store<_, Never>(initialState: ContentState()) } struct ContentView: View { let store = AppContainer.store var body: some View { VStack { Button("Update Name") { store.commit { $0.info.name = UUID().uuidString } } Button("Increment") { store.commit { $0.count += 1 } } Button("Replace info") { store.commit { $0.info = .init(name: "Replaced") } } StoreReader(store) { (state: ContentState) in Text(state.info.name) Text(state.count.description) } StoreReader(store) { (state: ContentState) in Text(state.info.constant) } @StoreBindable var storeBindable = store TextField("Name", text: $storeBindable.info.name) } .padding() } } #Preview { ContentView() } ================================================ FILE: playgrounds/PlaySwiftUI/PlaySwiftUI/PlaySwiftUIApp.swift ================================================ // // PlaySwiftUIApp.swift // PlaySwiftUI // // Created by Muukii on 2025/02/13. // import SwiftUI @main struct PlaySwiftUIApp: App { var body: some Scene { WindowGroup { ContentView() } } } ================================================ FILE: playgrounds/PlaySwiftUI/PlaySwiftUI/Preview Content/Preview Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: playgrounds/PlaySwiftUI/PlaySwiftUI/Simple.swift ================================================ import SwiftUI import Verge private struct _Book: View { @Tracking struct State { var count: Int = 0 @Tracking struct NestedState { var isActive: Bool = false var message: String = "Hello, Verge!" } var nestedState: NestedState = NestedState() } let store = Store<_, Never>(initialState: State()) var body: some View { StoreReader(store) { state in VStack { Text("Count: \(state.count)") Button("Increment") { store.commit { $0.count += 1 } } Text("Is Active: \(state.nestedState.isActive)") Text("Message: \(state.nestedState.message)") Button("Toggle Active") { store.commit { $0.nestedState.isActive.toggle() } } } } } } #Preview("Simple") { _Book() } ================================================ FILE: playgrounds/PlaySwiftUI/PlaySwiftUI.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 77; objects = { /* Begin PBXBuildFile section */ 4B8F11472D60E10B00FBCA80 /* Verge in Frameworks */ = {isa = PBXBuildFile; productRef = 4B8F11462D60E10B00FBCA80 /* Verge */; }; 4B8F114A2D60E13900FBCA80 /* Verge in Frameworks */ = {isa = PBXBuildFile; productRef = 4B8F11492D60E13900FBCA80 /* Verge */; }; 4B8F11632D60E1C900FBCA80 /* Verge in Frameworks */ = {isa = PBXBuildFile; productRef = 4B8F11622D60E1C900FBCA80 /* Verge */; }; 4BB8698B2D5E213200B2B2FB /* Verge in Frameworks */ = {isa = PBXBuildFile; productRef = 4BB8698A2D5E213200B2B2FB /* Verge */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 4BB869782D5E205E00B2B2FB /* PlaySwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PlaySwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ 4BB8697A2D5E205E00B2B2FB /* PlaySwiftUI */ = { isa = PBXFileSystemSynchronizedRootGroup; path = PlaySwiftUI; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ 4BB869752D5E205E00B2B2FB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4B8F114A2D60E13900FBCA80 /* Verge in Frameworks */, 4B8F11472D60E10B00FBCA80 /* Verge in Frameworks */, 4B8F11632D60E1C900FBCA80 /* Verge in Frameworks */, 4BB8698B2D5E213200B2B2FB /* Verge in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 4BB8696F2D5E205E00B2B2FB = { isa = PBXGroup; children = ( 4BB8697A2D5E205E00B2B2FB /* PlaySwiftUI */, 4BB869792D5E205E00B2B2FB /* Products */, ); sourceTree = ""; }; 4BB869792D5E205E00B2B2FB /* Products */ = { isa = PBXGroup; children = ( 4BB869782D5E205E00B2B2FB /* PlaySwiftUI.app */, ); name = Products; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 4BB869772D5E205E00B2B2FB /* PlaySwiftUI */ = { isa = PBXNativeTarget; buildConfigurationList = 4BB869862D5E205F00B2B2FB /* Build configuration list for PBXNativeTarget "PlaySwiftUI" */; buildPhases = ( 4BB869742D5E205E00B2B2FB /* Sources */, 4BB869752D5E205E00B2B2FB /* Frameworks */, 4BB869762D5E205E00B2B2FB /* Resources */, ); buildRules = ( ); dependencies = ( ); fileSystemSynchronizedGroups = ( 4BB8697A2D5E205E00B2B2FB /* PlaySwiftUI */, ); name = PlaySwiftUI; packageProductDependencies = ( 4BB8698A2D5E213200B2B2FB /* Verge */, 4B8F11462D60E10B00FBCA80 /* Verge */, 4B8F11492D60E13900FBCA80 /* Verge */, 4B8F11622D60E1C900FBCA80 /* Verge */, ); productName = PlaySwiftUI; productReference = 4BB869782D5E205E00B2B2FB /* PlaySwiftUI.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 4BB869702D5E205E00B2B2FB /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1620; LastUpgradeCheck = 1620; TargetAttributes = { 4BB869772D5E205E00B2B2FB = { CreatedOnToolsVersion = 16.2; }; }; }; buildConfigurationList = 4BB869732D5E205E00B2B2FB /* Build configuration list for PBXProject "PlaySwiftUI" */; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 4BB8696F2D5E205E00B2B2FB; minimizedProjectReferenceProxies = 1; packageReferences = ( 4B8F11612D60E1C900FBCA80 /* XCLocalSwiftPackageReference "../../../Verge" */, ); preferredProjectObjectVersion = 77; productRefGroup = 4BB869792D5E205E00B2B2FB /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 4BB869772D5E205E00B2B2FB /* PlaySwiftUI */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 4BB869762D5E205E00B2B2FB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 4BB869742D5E205E00B2B2FB /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ 4BB869842D5E205F00B2B2FB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.2; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 4BB869852D5E205F00B2B2FB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.2; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; VALIDATE_PRODUCT = YES; }; name = Release; }; 4BB869872D5E205F00B2B2FB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"PlaySwiftUI/Preview Content\""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = app.muukii.PlaySwiftUI; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 4BB869882D5E205F00B2B2FB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"PlaySwiftUI/Preview Content\""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = app.muukii.PlaySwiftUI; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 4BB869732D5E205E00B2B2FB /* Build configuration list for PBXProject "PlaySwiftUI" */ = { isa = XCConfigurationList; buildConfigurations = ( 4BB869842D5E205F00B2B2FB /* Debug */, 4BB869852D5E205F00B2B2FB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 4BB869862D5E205F00B2B2FB /* Build configuration list for PBXNativeTarget "PlaySwiftUI" */ = { isa = XCConfigurationList; buildConfigurations = ( 4BB869872D5E205F00B2B2FB /* Debug */, 4BB869882D5E205F00B2B2FB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ 4B8F11612D60E1C900FBCA80 /* XCLocalSwiftPackageReference "../../../Verge" */ = { isa = XCLocalSwiftPackageReference; relativePath = ../../../Verge; }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ 4B8F11462D60E10B00FBCA80 /* Verge */ = { isa = XCSwiftPackageProductDependency; productName = Verge; }; 4B8F11492D60E13900FBCA80 /* Verge */ = { isa = XCSwiftPackageProductDependency; productName = Verge; }; 4B8F11622D60E1C900FBCA80 /* Verge */ = { isa = XCSwiftPackageProductDependency; productName = Verge; }; 4BB8698A2D5E213200B2B2FB /* Verge */ = { isa = XCSwiftPackageProductDependency; productName = Verge; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 4BB869702D5E205E00B2B2FB /* Project object */; } ================================================ FILE: playgrounds/PlaySwiftUI/PlaySwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: playgrounds/PlaySwiftUI/PlaySwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved ================================================ { "originHash" : "91b2bd4d42ac7a19338699d5fcae987277fc6461718b80e90459a6235bafabb2", "pins" : [ { "identity" : "normalization", "kind" : "remoteSourceControl", "location" : "https://github.com/VergeGroup/Normalization", "state" : { "revision" : "6e7cb1ddeda4d0f1d2fbf8ca6d25ecd8ed6ba917", "version" : "1.1.0" } }, { "identity" : "rxswift", "kind" : "remoteSourceControl", "location" : "https://github.com/ReactiveX/RxSwift.git", "state" : { "revision" : "c7c7d2cf50a3211fe2843f76869c698e4e417930", "version" : "6.8.0" } }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-atomics.git", "state" : { "revision" : "cd142fd2f64be2100422d658e7411e39489da985", "version" : "1.2.0" } }, { "identity" : "swift-collections", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", "version" : "1.1.4" } }, { "identity" : "swift-concurrency-task-manager", "kind" : "remoteSourceControl", "location" : "https://github.com/VergeGroup/swift-concurrency-task-manager", "state" : { "revision" : "340cf14e0282977deeeb436605d1810ce4f4fbc9", "version" : "1.4.0" } }, { "identity" : "swift-macro-state-struct", "kind" : "remoteSourceControl", "location" : "https://github.com/VergeGroup/swift-macro-state-struct", "state" : { "branch" : "main", "revision" : "88e3937abd26b76bafe9027938538966457f156f" } }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { "revision" : "0687f71944021d616d34d922343dcef086855920", "version" : "600.0.1" } }, { "identity" : "typedcomparator", "kind" : "remoteSourceControl", "location" : "https://github.com/VergeGroup/TypedComparator", "state" : { "revision" : "337ce0e573e7637ddd29392cb88c3b852f042c8e", "version" : "1.0.0" } }, { "identity" : "typedidentifier", "kind" : "remoteSourceControl", "location" : "https://github.com/VergeGroup/TypedIdentifier", "state" : { "revision" : "284340409ba47858a1b3c353dcef9c21303953db", "version" : "2.0.3" } } ], "version" : 3 }