├── Example ├── BGSwift │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── ViewController.swift │ ├── AppDelegate.swift │ └── LoginExtent.swift ├── BGSwift.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── BGSwift-Example.xcscheme └── Tests │ ├── TestSupport.swift │ ├── ImpulseTests.swift │ ├── AssertionTests.swift │ ├── DependenciesTests.swift │ ├── BGGraphTests.swift │ ├── BGExtentTests.swift │ ├── EventTests.swift │ ├── BGMomentTests.swift │ ├── ConcurrencyTests.swift │ └── BGStateTests.swift ├── BGSwift └── Classes │ ├── BGAction.swift │ ├── BGSideEffect.swift │ ├── BGEvent.swift │ ├── BGLink.swift │ ├── Mutex.swift │ ├── PriorityQueue.swift │ ├── BGBehavior.swift │ ├── BGResource.swift │ ├── BGExtent.swift │ └── BGGraph.swift ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── PULL_REQUEST_TEMPLATE.md ├── Package.swift ├── README.md ├── CONTRIBUTING.md ├── .gitignore ├── Code_of_Conduct.md └── LICENSE.txt /Example/BGSwift/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /BGSwift/Classes/BGAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import Foundation 6 | 7 | struct BGAction { 8 | let impulse: String? 9 | let action: () -> Void 10 | } 11 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/BGSwift/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Example/BGSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/BGSwift/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | I confirm that this contribution is made under the terms of the license found in the root directory of this repository's source tree and that I have the authority necessary to make this contribution on behalf of its copyright owner. 4 | -------------------------------------------------------------------------------- /Example/BGSwift.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BGSwift/Classes/BGSideEffect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import Foundation 6 | 7 | struct BGSideEffect { 8 | let label: String? 9 | let event: BGEvent 10 | let run: () -> Void 11 | 12 | init(label: String? = nil, event: BGEvent, run: @escaping () -> Void) { 13 | self.label = label 14 | self.event = event 15 | self.run = run 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BGSwift/Classes/BGEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | 6 | import Foundation 7 | 8 | public struct BGEvent: Equatable { 9 | public let sequence: UInt 10 | public let timestamp: Date 11 | public let impulse: String? 12 | 13 | public static let unknownPast: BGEvent = .init(sequence: 0, timestamp: Date(timeIntervalSince1970: 0), impulse: nil) 14 | 15 | init(sequence: UInt, timestamp: Date, impulse: String?) { 16 | self.sequence = sequence 17 | self.timestamp = timestamp 18 | self.impulse = impulse 19 | } 20 | 21 | public func happenedSince(sequence: UInt) -> Bool { 22 | self.sequence > 0 && self.sequence >= sequence 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Example/Tests/TestSupport.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import Foundation 6 | import XCTest 7 | @testable import BGSwift 8 | 9 | func TestAssertionHit(graph: BGGraph, _ code: () -> (), _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) { 10 | let assertionHit = CheckAssertionHit(graph: graph, code) 11 | 12 | if !assertionHit { 13 | XCTFail(message(), file: file, line: line) 14 | } 15 | } 16 | 17 | func CheckAssertionHit(graph: BGGraph, _ code: () -> ()) -> Bool { 18 | var assertionHit = false 19 | graph.debugOnAssertionFailure = { _, _, _ in 20 | assertionHit = true 21 | } 22 | 23 | defer { 24 | graph.debugOnAssertionFailure = nil 25 | } 26 | 27 | code() 28 | 29 | return assertionHit 30 | } 31 | -------------------------------------------------------------------------------- /Example/Tests/ImpulseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import XCTest 6 | @testable import BGSwift 7 | 8 | class ImpulseTests: XCTestCase { 9 | 10 | let g = BGGraph() 11 | 12 | func testActionWithUnspecifiedImpulse() { 13 | var impulse: String! 14 | 15 | let expectedLine = String(#line + 1) 16 | g.action { [g] in 17 | impulse = g.currentEvent!.impulse! 18 | } 19 | 20 | XCTAssertTrue(impulse.contains(#fileID)) 21 | XCTAssertTrue(impulse.contains(#function)) 22 | XCTAssertTrue(impulse.contains(expectedLine)) 23 | } 24 | 25 | func testActionWithImpulseString() { 26 | var impulse: String! 27 | 28 | g.action(impulse: "foo") { [g] in 29 | impulse = g.currentEvent!.impulse! 30 | } 31 | 32 | XCTAssertEqual(impulse, "foo") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "BGSwift", 8 | platforms: [ 9 | .macOS(.v10_15), // These are where Combine becomes available which we will use in SwiftPM compatibility 10 | .iOS(.v13) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "BGSwift", 16 | targets: ["BGSwift"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 24 | .target( 25 | name: "BGSwift", 26 | path: "BGSwift/Classes"), 27 | .testTarget( 28 | name: "BGSwiftTests", 29 | dependencies: ["BGSwift"], 30 | path: "Example/Tests"), 31 | ], 32 | swiftLanguageVersions: [.v5] 33 | ) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BehaviorGraph 2 | 3 | **Behavior Graph** is a software library that greatly enhances our ability to program **user facing software** and **control systems**. Programs of this type quickly scale up in complexity as features are added. Behavior Graph directly addresses this complexity by shifting more of the burden to the computer. It works by offering the programmer a new unit of code organization called a **behavior**. Behaviors are blocks of code enriched with additional information about their stateful relationships. Using this information, Behavior Graph enforces _safe use of mutable state_, arguably the primary source of complexity in this class of software. It does this by taking on the responsibility of control flow between behaviors, ensuring they are are _run at the correct time and in the correct order_. 4 | 5 | ## Is it any good? 6 | 7 | Yes 8 | 9 | ## Documentation 10 | 11 | Coming Soon. See Objective-C Docs Here 12 | 13 | [Introduction to Behavior Graph](/bgdocs/objc/intro.html) 14 | 15 | [Behavior Graph Programming Guide](/bgdocs/objc/guide.html) 16 | 17 | ## Example 18 | 19 | To run the example project, clone the repo, and run `pod install` from the Example directory first. 20 | 21 | ## Requirements 22 | 23 | iOS or MacOS 24 | 25 | ## Installation 26 | 27 | Work In Progress: 28 | 29 | Simply add the following line to your Podfile: 30 | 31 | ```ruby 32 | pod 'BGSwift', :git => '???' 33 | ``` 34 | 35 | ## License 36 | 37 | BehaviorGraph is available under the Apache 2.0 license. See the LICENSE file for more info. 38 | -------------------------------------------------------------------------------- /Example/Tests/AssertionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import Foundation 6 | import XCTest 7 | import BGSwift 8 | 9 | class AssertionTests: XCTestCase { 10 | 11 | func testAssertsUndeclaredDemandWithSupplier() { 12 | var assertionHit = false 13 | 14 | let g = BGGraph() 15 | g.checkUndeclaredDemands = true 16 | 17 | let b = BGExtentBuilder(graph: g) 18 | let r = b.moment() 19 | 20 | b.behavior().supplies([r]).runs { _ in 21 | // do nothing 22 | } 23 | 24 | b.behavior().demands([b.added]).runs { extent in 25 | assertionHit = CheckAssertionHit(graph: g) { 26 | _ = r.justUpdated() 27 | } 28 | } 29 | 30 | let e = BGExtent(builder: b) 31 | e.addToGraphWithAction() 32 | 33 | XCTAssertTrue(assertionHit) 34 | } 35 | 36 | func testAssertsUndeclaredDemandWithNoSupplier() { 37 | var assertionHit = false 38 | 39 | let g = BGGraph() 40 | g.checkUndeclaredDemands = true 41 | 42 | let b = BGExtentBuilder(graph: g) 43 | let r = b.moment() 44 | 45 | b.behavior().demands([b.added]).runs { extent in 46 | assertionHit = CheckAssertionHit(graph: g) { 47 | _ = r.justUpdated() 48 | } 49 | } 50 | 51 | let e = BGExtent(builder: b) 52 | e.addToGraphWithAction() 53 | 54 | XCTAssertTrue(assertionHit) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Example/BGSwift/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/BGSwift/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import UIKit 6 | import BGSwift 7 | 8 | class ViewController: UIViewController { 9 | 10 | @IBOutlet var emailField: UITextField! 11 | @IBOutlet var passwordField: UITextField! 12 | @IBOutlet var loginButton: UIButton! 13 | @IBOutlet var emailFeedback: UILabel! 14 | @IBOutlet var passwordFeedback: UILabel! 15 | @IBOutlet var loginStatus: UILabel! 16 | @IBOutlet var loginSuccess: UIButton! 17 | @IBOutlet var loginFail: UIButton! 18 | 19 | let graph: BGGraph 20 | let loginExtent: LoginExtent 21 | 22 | required init?(coder: NSCoder) { 23 | graph = BGGraph() 24 | loginExtent = LoginExtent(graph: graph) 25 | super.init(coder: coder) 26 | loginExtent.loginForm = self 27 | } 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | loginExtent.addToGraphWithAction() 32 | } 33 | 34 | override func didReceiveMemoryWarning() { 35 | super.didReceiveMemoryWarning() 36 | } 37 | 38 | @IBAction func didUpdateEmailField(sender: UITextField) { 39 | graph.action { 40 | self.loginExtent.email.update(self.emailField.text ?? "") 41 | } 42 | } 43 | 44 | @IBAction func didUpdatePasswordField(sender: UITextField) { 45 | graph.action { 46 | self.loginExtent.password.update(self.passwordField.text ?? "") 47 | } 48 | } 49 | 50 | @IBAction func loginButtonClicked(sender: UIButton) { 51 | graph.action { 52 | self.loginExtent.loginClick.update() 53 | } 54 | } 55 | 56 | @IBAction func loginSucceeded(sender: UIButton) { 57 | loginExtent.completeLogin(success: true) 58 | } 59 | 60 | @IBAction func loginFailed(sender: UIButton) { 61 | loginExtent.completeLogin(success: false) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /BGSwift/Classes/BGLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import Foundation 6 | 7 | enum LinkType { 8 | case reactive 9 | case order 10 | } 11 | 12 | struct BGSubsequentLink: Equatable, Hashable { 13 | weak private(set) var behavior: BGBehavior? 14 | let type: LinkType 15 | let behaviorPtr: ObjectIdentifier 16 | 17 | init(behavior: BGBehavior, type: LinkType) { 18 | self.behavior = behavior 19 | self.type = type 20 | behaviorPtr = ObjectIdentifier(behavior) 21 | } 22 | 23 | // MARK: Equatable 24 | 25 | static func == (lhs: BGSubsequentLink, rhs: BGSubsequentLink) -> Bool { 26 | if lhs.type == rhs.type, 27 | let bl = lhs.behavior, let br = rhs.behavior, 28 | bl === br { 29 | return true 30 | } 31 | return false 32 | } 33 | 34 | // MARK: Hashable 35 | 36 | func hash(into hasher: inout Hasher) { 37 | hasher.combine(behaviorPtr) 38 | hasher.combine(type) 39 | } 40 | } 41 | 42 | struct BGDemandLink: BGDemandable, Equatable, Hashable { 43 | weak private(set) var resource: (any BGResourceInternal)? 44 | let type: LinkType 45 | let resourcePtr: ObjectIdentifier 46 | 47 | init(resource: any BGResourceInternal, type: LinkType) { 48 | self.resource = resource 49 | self.type = type 50 | resourcePtr = ObjectIdentifier(resource) 51 | } 52 | 53 | // MARK: Equatable 54 | 55 | static func == (lhs: BGDemandLink, rhs: BGDemandLink) -> Bool { 56 | if lhs.type == rhs.type, 57 | let rl = lhs.resource, let rr = rhs.resource, 58 | rl === rr { 59 | return true 60 | } 61 | return false 62 | } 63 | 64 | // MARK: Hashable 65 | 66 | func hash(into hasher: inout Hasher) { 67 | hasher.combine(resourcePtr) 68 | hasher.combine(type) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Example/BGSwift/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Example/BGSwift/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | 6 | import UIKit 7 | 8 | @UIApplicationMain 9 | class AppDelegate: UIResponder, UIApplicationDelegate { 10 | 11 | var window: UIWindow? 12 | 13 | 14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 15 | // Override point for customization after application launch. 16 | return true 17 | } 18 | 19 | func applicationWillResignActive(_ application: UIApplication) { 20 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 21 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 22 | } 23 | 24 | func applicationDidEnterBackground(_ application: UIApplication) { 25 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 26 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 27 | } 28 | 29 | func applicationWillEnterForeground(_ application: UIApplication) { 30 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 31 | } 32 | 33 | func applicationDidBecomeActive(_ application: UIApplication) { 34 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 35 | } 36 | 37 | func applicationWillTerminate(_ application: UIApplication) { 38 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 39 | } 40 | 41 | 42 | } 43 | 44 | -------------------------------------------------------------------------------- /BGSwift/Classes/Mutex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import Foundation 6 | 7 | class Mutex { 8 | let mutex: UnsafeMutablePointer 9 | 10 | init(recursive: Bool = true) { 11 | let attributes = UnsafeMutablePointer.allocate(capacity: 1) 12 | attributes.initialize(to: pthread_mutexattr_t()) 13 | 14 | guard pthread_mutexattr_init(attributes) == 0 else { 15 | preconditionFailure() 16 | } 17 | 18 | defer { 19 | pthread_mutexattr_destroy(attributes) 20 | attributes.deallocate() 21 | } 22 | 23 | pthread_mutexattr_settype(attributes, recursive ? PTHREAD_MUTEX_RECURSIVE : PTHREAD_MUTEX_NORMAL) 24 | 25 | mutex = .allocate(capacity: 1) 26 | mutex.initialize(to: pthread_mutex_t()) 27 | 28 | guard pthread_mutex_init(mutex, attributes) == 0 else { 29 | preconditionFailure() 30 | } 31 | } 32 | 33 | deinit { 34 | pthread_mutex_destroy(mutex) 35 | mutex.deallocate() 36 | } 37 | 38 | // jlou 2/18/21 - Avoid concurrent execution by surrounding code with mutex lock/unlock. I believe Swift 39 | // compiler should be able to inline the non-escaping closure and avoid heap allocation for captured variables 40 | // but I will need to test this. If not, we can pass variables into the closure with a generic input argument. 41 | 42 | @inline(__always) 43 | func balancedUnlock(_ code: () throws -> Void) rethrows { 44 | lock() 45 | defer { 46 | unlock() 47 | } 48 | try code() 49 | } 50 | 51 | @inline(__always) 52 | func balancedUnlock(_ code: () throws -> T) rethrows -> T { 53 | lock() 54 | defer { 55 | unlock() 56 | } 57 | return try code() 58 | } 59 | 60 | @inline(__always) 61 | func lock() { 62 | pthread_mutex_lock(mutex) 63 | } 64 | 65 | @inline(__always) 66 | func tryLock() -> Bool { 67 | return pthread_mutex_trylock(mutex) == 0 68 | } 69 | 70 | @inline(__always) 71 | func unlock() { 72 | pthread_mutex_unlock(mutex) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | First, thanks for taking the time to contribute to our project! There are many ways you can help out. 3 | 4 | ### Questions 5 | 6 | If you have a question that needs an answer, create an issue, and label it as a question. 7 | 8 | ### Issues for bugs or feature requests 9 | 10 | If you encounter any bugs in the code, or want to request a new feature or enhancement, please create an issue to report it. Kindly add a label to indicate what type of issue it is. 11 | 12 | ### Contribute Code 13 | We welcome your pull requests for bug fixes. To implement something new, please create an issue first so we can discuss it together. 14 | 15 | ***Creating a Pull Request*** 16 | Please follow [best practices](https://github.com/trein/dev-best-practices/wiki/Git-Commit-Best-Practices) for creating git commits. In addition: 17 | 18 | - Make sure your code respects existing formatting conventions. In general, follow 19 | the same coding style as the code that you are modifying. 20 | - Bugfixes must include a unit test or integration test reproducing the issue. 21 | - Try to keep pull requests short and submit separate ones for unrelated 22 | features, but feel free to combine simple bugfixes/tests into one pull request. 23 | - Keep the number of commits small and combine commits for related changes. 24 | - Keep formatting changes in separate commits to make code reviews easier and 25 | distinguish them from actual code changes. 26 | 27 | When your code is ready to be submitted, submit a pull request to begin the code review process. 28 | 29 | We only seek to accept code that you are authorized to contribute to the project. We have added a pull request template on our projects so that your contributions are made with the following confirmation: 30 | 31 | > I confirm that this contribution is made under the terms of the license found in the root directory of this repository's source tree and that I have the authority necessary to make this contribution on behalf of its copyright owner. 32 | 33 | ## Code of Conduct 34 | 35 | We encourage inclusive and professional interactions on our project. We welcome everyone to open an issue, improve the documentation, report bug or submit a pull request. By participating in this project, you agree to abide by the [Code of Conduct](Code-Of-Conduct.md). If you feel there is a conduct issue related to this project, please raise it per the Code of Conduct process and we will address it. 36 | 37 | -------------------------------------------------------------------------------- /BGSwift/Classes/PriorityQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import Foundation 6 | 7 | class PriorityQueue { 8 | private let heap: CFBinaryHeap 9 | private var unheapedElements = [BGBehavior]() 10 | 11 | init() { 12 | var callbacks = CFBinaryHeapCallBacks(version: 0, retain: nil, release: nil, copyDescription: nil) { ptr1, ptr2, context in 13 | 14 | let lhs = Unmanaged.fromOpaque(ptr1!).takeUnretainedValue().order 15 | let rhs = Unmanaged.fromOpaque(ptr2!).takeUnretainedValue().order 16 | 17 | return ( 18 | lhs < rhs ? .compareLessThan : 19 | lhs > rhs ? .compareGreaterThan : 20 | .compareEqualTo 21 | ) 22 | } 23 | 24 | heap = CFBinaryHeapCreate(kCFAllocatorDefault, 0, &callbacks, nil) 25 | } 26 | 27 | var count: Int { 28 | unheapedElements.count + CFBinaryHeapGetCount(heap) 29 | } 30 | 31 | var isEmpty: Bool { 32 | unheapedElements.isEmpty && CFBinaryHeapGetCount(heap) == 0 33 | } 34 | 35 | func setNeedsReheap() { 36 | for _ in 0 ..< CFBinaryHeapGetCount(heap) { 37 | let value = popHeap() 38 | unheapedElements.append(value) 39 | } 40 | } 41 | 42 | private func reheapIfNeeded() { 43 | unheapedElements.forEach { element in 44 | let ptr = Unmanaged.passRetained(element).toOpaque() 45 | CFBinaryHeapAddValue(heap, ptr) 46 | } 47 | unheapedElements.removeAll() 48 | } 49 | 50 | func push(_ value: BGBehavior) { 51 | unheapedElements.append(value) 52 | } 53 | 54 | func pop() -> BGBehavior { 55 | reheapIfNeeded() 56 | 57 | guard count > 0 else { 58 | preconditionFailure() 59 | } 60 | 61 | return popHeap() 62 | } 63 | 64 | func peek() -> BGBehavior { 65 | reheapIfNeeded() 66 | 67 | guard count > 0 else { 68 | preconditionFailure() 69 | } 70 | 71 | return Unmanaged.fromOpaque(CFBinaryHeapGetMinimum(heap)).takeUnretainedValue() 72 | } 73 | 74 | private func popHeap() -> BGBehavior { 75 | let value = Unmanaged.fromOpaque(CFBinaryHeapGetMinimum(heap)).takeRetainedValue() 76 | CFBinaryHeapRemoveMinimumValue(heap) 77 | return value 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | # macOS 6 | .DS_Store 7 | 8 | ## User settings 9 | xcuserdata/ 10 | 11 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 12 | *.xcscmblueprint 13 | *.xccheckout 14 | 15 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 16 | build/ 17 | DerivedData/ 18 | *.moved-aside 19 | *.pbxuser 20 | !default.pbxuser 21 | *.mode1v3 22 | !default.mode1v3 23 | *.mode2v3 24 | !default.mode2v3 25 | *.perspectivev3 26 | !default.perspectivev3 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | 31 | ## App packaging 32 | *.ipa 33 | *.dSYM.zip 34 | *.dSYM 35 | 36 | ## Playgrounds 37 | timeline.xctimeline 38 | playground.xcworkspace 39 | 40 | # Swift Package Manager 41 | # 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | # Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | # *.xcodeproj 47 | # 48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 49 | # hence it is not needed unless you have added a package configuration file to your project 50 | # .swiftpm 51 | 52 | .build/ 53 | 54 | # CocoaPods 55 | # 56 | # We recommend against adding the Pods directory to your .gitignore. However 57 | # you should judge for yourself, the pros and cons are mentioned at: 58 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 59 | 60 | Pods/ 61 | 62 | # Add this line if you want to avoid checking in source code from the Xcode workspace 63 | # *.xcworkspace 64 | 65 | # Carthage 66 | # 67 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 68 | # Carthage/Checkouts 69 | 70 | Carthage/Build/ 71 | 72 | # Accio dependency management 73 | Dependencies/ 74 | .accio/ 75 | 76 | # fastlane 77 | # 78 | # It is recommended to not store the screenshots in the git repo. 79 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 80 | # For more information about the recommended setup visit: 81 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 82 | 83 | fastlane/report.xml 84 | fastlane/Preview.html 85 | fastlane/screenshots/**/*.png 86 | fastlane/test_output 87 | 88 | # Code Injection 89 | # 90 | # After new code Injection tools there's a generated folder /iOSInjectionProject 91 | # https://github.com/johnno1962/injectionforxcode 92 | 93 | iOSInjectionProject/ 94 | -------------------------------------------------------------------------------- /Example/Tests/DependenciesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | 6 | import Foundation 7 | import XCTest 8 | @testable import BGSwift 9 | 10 | class DependenciesTest : XCTestCase { 11 | 12 | var g: BGGraph! 13 | var r_a, r_b, r_c: BGState! 14 | var bld: BGExtentBuilder! 15 | var ext: BGExtent! 16 | 17 | override func setUp() { 18 | super.setUp() 19 | g = BGGraph() 20 | bld = BGExtentBuilder(graph: g) 21 | r_a = bld.state(0) 22 | r_b = bld.state(0) 23 | r_c = bld.state(0) 24 | } 25 | 26 | func testAActivatesB() { 27 | // |> Given a behavior with supplies and demands 28 | bld.behavior().supplies([r_b]).demands([r_a]).runs { extent in 29 | self.r_b.update(2 * self.r_a.value) 30 | } 31 | ext = BGExtent(builder: bld) 32 | ext.addToGraphWithAction(); 33 | 34 | // |> When the demand is updated 35 | r_a.updateWithAction(1) 36 | 37 | // |> Then the demanding behavior will run and update its supplied resource 38 | XCTAssertEqual(r_b.value, 2) 39 | XCTAssertEqual(r_b.event, r_a.event) 40 | } 41 | 42 | func testActivatesBehaviorsOncePerEvent() { 43 | // |> Given a behavior that demands multiple resources 44 | var called = 0 45 | bld.behavior().supplies([r_c]).demands([r_a, r_b]).runs { extent in 46 | called += 1 47 | } 48 | ext = BGExtent(builder: bld) 49 | ext.addToGraphWithAction() 50 | 51 | // |> When both resources are updated in same event 52 | self.g.action { 53 | self.r_a.update(1) 54 | self.r_b.update(2) 55 | } 56 | 57 | // |> Then the demanding behavior is activated only once 58 | XCTAssertEqual(called, 1) 59 | } 60 | } 61 | 62 | class DependenciesTest2: XCTestCase { 63 | 64 | func testBehaviorNotActivatedFromOrderingDemandUpdate() { 65 | let g = BGGraph() 66 | let b = BGExtentBuilder(graph: g) 67 | let r = b.moment() 68 | 69 | var reactiveLinkActivated = false 70 | var orderLinkActivated = false 71 | 72 | b.behavior().demands([r]).runs { extent in 73 | reactiveLinkActivated = true 74 | } 75 | 76 | b.behavior().demands([r.order]).runs { extent in 77 | orderLinkActivated = true 78 | } 79 | 80 | let ext = BGExtent(builder: b) 81 | g.action { 82 | ext.addToGraph() 83 | r.update() 84 | } 85 | 86 | XCTAssertTrue(reactiveLinkActivated) 87 | XCTAssertFalse(orderLinkActivated) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Example/Tests/BGGraphTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import XCTest 6 | import BGSwift 7 | 8 | class BGGraphTests: XCTestCase { 9 | 10 | var g: BGGraph! 11 | var b: BGExtentBuilder! 12 | var e: BGExtent! 13 | var rA: BGState! 14 | var rB: BGState! 15 | var rC: BGState! 16 | 17 | override func setUpWithError() throws { 18 | g = BGGraph() 19 | b = BGExtentBuilder(graph: g) 20 | rA = b.state(0) 21 | rB = b.state(0) 22 | rC = b.state(0) 23 | } 24 | 25 | override func tearDownWithError() throws { 26 | // Put teardown code here. This method is called after the invocation of each test method in the class. 27 | } 28 | 29 | func testDependencyCyclesCaught() { 30 | // |> Given a graph with dependency cycles 31 | b.behavior().supplies([rA]).demands([rB]).runs { extent in 32 | // nothing 33 | } 34 | b.behavior().supplies([rB]).demands([rA]).runs { extent in 35 | // nothing 36 | } 37 | e = BGExtent(builder: b) 38 | 39 | // |> When it is added to the graph 40 | // |> Then it will raise an error 41 | TestAssertionHit(graph: g) { 42 | e.addToGraphWithAction() 43 | } 44 | } 45 | 46 | func testResourceCanOnlyBeSuppliedByOneBehavior() { 47 | // |> Given an a resource that is supplied by a behavior 48 | b.behavior().supplies([rA]).runs { extent in 49 | // nothing 50 | } 51 | 52 | // |> When a behavior sets a static supply that is already supplied by another behavior 53 | // |> Then it will raise an error 54 | TestAssertionHit(graph: g) { 55 | b.behavior().supplies([rA]).runs { extent in 56 | // nothing 57 | } 58 | } 59 | 60 | let bhv = b.behavior().runs { extent in 61 | // nothing 62 | } 63 | 64 | e = BGExtent(builder: b) 65 | e.addToGraphWithAction() 66 | 67 | // |> When a behavior sets a dynamic supply that is already supplied by another behavior 68 | // |> Then it will raise an error 69 | TestAssertionHit(graph: g) { 70 | g.action { 71 | bhv.setDynamicSupplies([self.rA]) 72 | } 73 | } 74 | } 75 | 76 | func testLinksCanOnlyBeUpdatedDuringAnEvent() { 77 | // |> Given a behavior in a graph 78 | let bhv = b.behavior().runs { extent in 79 | // nothing 80 | } 81 | e = BGExtent(builder: b) 82 | e.addToGraphWithAction() 83 | 84 | // |> When updating demands outside of event 85 | // |> Then there is an error 86 | TestAssertionHit(graph: g) { 87 | bhv.setDynamicDemands([rA]) 88 | } 89 | 90 | // |> And when updating supplies outside of event 91 | // |> Then there is an error 92 | TestAssertionHit(graph: g) { 93 | bhv.setDynamicSupplies([rB]) 94 | } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /BGSwift/Classes/BGBehavior.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import Foundation 6 | 7 | enum OrderingState: Int { 8 | case ordered 9 | case ordering 10 | case unordered 11 | } 12 | 13 | public class BGBehavior { 14 | var orderingState = OrderingState.ordered 15 | var order = UInt(0) 16 | var enqueuedSequence = UInt(0) 17 | var lastUpdateSequence = UInt(0) 18 | var removedSequence = UInt(0) 19 | 20 | var debugName: String? 21 | 22 | var staticSupplies = Set() 23 | var staticDemands = Set() 24 | 25 | var supplies = Set() 26 | var demands = Set() 27 | 28 | var uncommittedDynamicSupplies: [any BGResourceInternal]? 29 | var uncommittedDynamicDemands: [BGDemandable]? 30 | 31 | var uncommittedSupplies: Bool 32 | var uncommittedDemands: Bool 33 | 34 | let requiresMainThread: Bool 35 | 36 | let runBlock: (BGExtent) -> Void 37 | 38 | weak var owner: BGExtent? 39 | var graph: BGGraph? { owner?.graph } 40 | 41 | 42 | internal init(graph: BGGraph, supplies: [BGResource], demands: [BGDemandable], requiresMainThread: Bool, body: @escaping (BGExtent) -> Void) { 43 | self.requiresMainThread = requiresMainThread 44 | self.runBlock = body 45 | 46 | uncommittedSupplies = !supplies.isEmpty 47 | uncommittedDemands = !demands.isEmpty 48 | 49 | supplies.forEach { 50 | let resource = $0.asInternal 51 | guard resource.supplier == nil else { 52 | graph.assertionFailure("Resource is already supplied by a different behavior.") 53 | return 54 | } 55 | staticSupplies.insert(resource.weakReference) 56 | resource.supplier = self 57 | } 58 | 59 | demands.forEach { staticDemands.insert($0.link) } 60 | } 61 | 62 | func run() { 63 | guard let owner = self.owner else { 64 | return 65 | } 66 | runBlock(owner) 67 | } 68 | 69 | public func setDynamicSupplies(_ supplies: [BGResource]) { 70 | uncommittedDynamicSupplies = (supplies.map { $0.asInternal} ) 71 | uncommittedSupplies = true 72 | 73 | if let owner = owner, owner.status == .added { 74 | owner.graph.updateSupplies(behavior: self) 75 | } 76 | } 77 | 78 | public func setDynamicSupplies(_ supplies: BGResource...) { 79 | setDynamicSupplies(supplies as [BGResource]) 80 | } 81 | 82 | public func setDynamicDemands(_ demands: [BGDemandable]) { 83 | uncommittedDynamicDemands = demands 84 | uncommittedDemands = true 85 | 86 | if let owner = owner, owner.status == .added { 87 | owner.graph.updateDemands(behavior: self) 88 | } 89 | } 90 | 91 | public func setDynamicDemands(_ demands: BGDemandable...) { 92 | setDynamicDemands(demands) 93 | } 94 | } 95 | 96 | extension BGBehavior: CustomDebugStringConvertible { 97 | public var debugDescription: String { 98 | "<\(String(describing: Self.self)):\(String(format: "%018p", unsafeBitCast(self, to: Int64.self))) (\(debugName ?? "Unlabeled"))>" 99 | } 100 | } 101 | 102 | extension BGBehavior: Equatable { 103 | public static func == (lhs: BGBehavior, rhs: BGBehavior) -> Bool { lhs === rhs } 104 | 105 | } 106 | 107 | extension BGBehavior: Hashable { 108 | public func hash(into hasher: inout Hasher) { 109 | hasher.combine(ObjectIdentifier(self)) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Example/BGSwift.xcodeproj/xcshareddata/xcschemes/BGSwift-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 35 | 36 | 37 | 38 | 40 | 46 | 47 | 48 | 49 | 50 | 60 | 62 | 68 | 69 | 70 | 71 | 74 | 75 | 76 | 77 | 81 | 82 | 83 | 84 | 90 | 92 | 98 | 99 | 100 | 101 | 103 | 104 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /Example/BGSwift/LoginExtent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import Foundation 6 | import BGSwift 7 | 8 | class LoginExtent: BGExtent { 9 | let email: BGState 10 | let password: BGState 11 | let loginClick: BGMoment 12 | let emailValid: BGState 13 | let passwordValid: BGState 14 | let loginEnabled: BGState 15 | let loggingIn: BGState 16 | let loginComplete: BGTypedMoment 17 | 18 | weak var loginForm: ViewController? 19 | var savedLoginBlock: ((Bool) -> Void)? 20 | 21 | init(graph: BGGraph) { 22 | let bld: BGExtentBuilder = BGExtentBuilder(graph: graph) 23 | email = bld.state("") 24 | password = bld.state("") 25 | loginClick = bld.moment() 26 | emailValid = bld.state(false) 27 | passwordValid = bld.state(false) 28 | loginEnabled = bld.state(false) 29 | loggingIn = bld.state(false) 30 | loginComplete = bld.typedMoment() 31 | 32 | 33 | bld.behavior() 34 | .supplies([emailValid]) 35 | .demands([email, bld.added]) 36 | .runs { extent in 37 | extent.emailValid.update(LoginExtent.validEmailAddress(extent.email.value)) 38 | extent.sideEffect { 39 | extent.loginForm?.emailFeedback.text = extent.emailValid.value ? "✅" : "❌" 40 | } 41 | } 42 | 43 | bld.behavior() 44 | .supplies([passwordValid]) 45 | .demands([password, bld.added]) 46 | .runs { extent in 47 | extent.passwordValid.update(extent.password.value.count > 0) 48 | extent.sideEffect { 49 | extent.loginForm?.passwordFeedback.text = extent.passwordValid.value ? "✅" : "❌" 50 | } 51 | } 52 | 53 | bld.behavior() 54 | .supplies([loginEnabled]) 55 | .demands([emailValid, passwordValid, loggingIn, bld.added]) 56 | .runs { extent in 57 | let enabled = extent.emailValid.value && extent.passwordValid.value && !extent.loggingIn.value; 58 | extent.loginEnabled.update(enabled) 59 | extent.sideEffect { 60 | extent.loginForm?.loginButton.isEnabled = extent.loginEnabled.value 61 | } 62 | } 63 | 64 | bld.behavior() 65 | .supplies([loggingIn]) 66 | .demands([loginClick, loginComplete, bld.added]) 67 | .runs { extent in 68 | if extent.loginClick.justUpdated() && extent.loginEnabled.traceValue { 69 | extent.loggingIn.update(true) 70 | } else if extent.loginComplete.justUpdated() && extent.loggingIn.value { 71 | extent.loggingIn.update(false) 72 | } 73 | 74 | if extent.loggingIn.justUpdated(to: true) { 75 | extent.sideEffect { 76 | extent.loginCall(email: extent.email.value, password: extent.password.value) { success in 77 | 78 | extent.graph.action { 79 | extent.loginComplete.update(success) 80 | } 81 | 82 | } 83 | } 84 | } 85 | } 86 | 87 | bld.behavior() 88 | .demands([loggingIn, loginComplete, bld.added]) 89 | .runs { extent in 90 | extent.sideEffect { 91 | var status = "" 92 | if extent.loggingIn.value { 93 | status = "Logging in..."; 94 | } else if let loginComplete = extent.loginComplete.updatedValue { 95 | if loginComplete { 96 | status = "Login Success" 97 | } else { 98 | status = "Login Failed" 99 | } 100 | } 101 | extent.loginForm?.loginStatus.text = status; 102 | } 103 | } 104 | 105 | super.init(builder: bld) 106 | 107 | } 108 | 109 | static func validEmailAddress(_ email: String) -> Bool { 110 | let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" 111 | let pred = NSPredicate(format: "SELF matches %@", regex) 112 | return pred.evaluate(with: email) 113 | } 114 | 115 | func loginCall(email: String, password: String, complete: @escaping (Bool) -> Void) { 116 | self.savedLoginBlock = complete 117 | } 118 | 119 | func completeLogin(success: Bool) { 120 | self.savedLoginBlock?(success) 121 | self.savedLoginBlock = nil 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /Example/Tests/BGExtentTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import XCTest 6 | @testable import BGSwift 7 | 8 | class BGExtentTests: XCTestCase { 9 | 10 | var g: BGGraph! 11 | var b: BGExtentBuilder! 12 | var e: BGExtent! 13 | var rA: BGState! 14 | var rB: BGState! 15 | var rC: BGState! 16 | 17 | 18 | override func setUpWithError() throws { 19 | g = BGGraph() 20 | b = BGExtentBuilder(graph: g) 21 | rA = b.state(0) 22 | rB = b.state(0) 23 | rC = b.state(0) 24 | } 25 | 26 | override func tearDownWithError() throws { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | } 29 | 30 | func testAddedResourceIsUpdatedOnAdding() { 31 | // |> Given an extent 32 | var run = false 33 | var nonAddedRun = false 34 | b.behavior().demands([b.added]).runs { extent in 35 | run = true 36 | } 37 | b.behavior().demands([b.added]).runs { extent in 38 | run = true 39 | } 40 | b.behavior().runs { extent in 41 | nonAddedRun = true 42 | } 43 | e = BGExtent(builder: b) 44 | 45 | // |> When it is added 46 | e.addToGraphWithAction() 47 | 48 | // |> The added resource is updated 49 | XCTAssertTrue(run) 50 | XCTAssertFalse(nonAddedRun) 51 | } 52 | 53 | func testCanGetResourcesAndBehaviors() { 54 | b.behavior().runs { _ in } 55 | b.behavior().runs { _ in } 56 | e = BGExtent(builder: b) 57 | 58 | // |> When it is added 59 | e.addToGraphWithAction() 60 | 61 | // |> Then we can get the resources and behaviors 62 | XCTAssertEqual(4, e.allResources.count) // _added is a resource also 63 | XCTAssertEqual(2, e.allBehaviors.count) 64 | } 65 | 66 | func testCheckCannotAddExtentToGraphMultipleTimes() { 67 | // NOTE: This is primarily to prevent user error. 68 | // Perhaps it makes sense to remove/add extents in the future. 69 | 70 | // |> Given an extent added to a graph 71 | e = BGExtent(builder: b) 72 | e.addToGraphWithAction() 73 | 74 | // |> When it is added again 75 | // |> Then there is an error 76 | TestAssertionHit(graph: g) { 77 | e.addToGraphWithAction() 78 | } 79 | } 80 | 81 | func testCheckExtentCanOnlyBeAddedDuringEvent() { 82 | // |> Given an extent 83 | e = BGExtent(builder: b) 84 | 85 | // |> When added outside an event 86 | // |> Then there is an error 87 | TestAssertionHit(graph: g) { 88 | e.addToGraph() 89 | } 90 | } 91 | 92 | func testCheckExtentCanOnlyBeRemovedDuringEvent() { 93 | // |> Given an extent 94 | e = BGExtent(builder: b) 95 | e.addToGraphWithAction() 96 | 97 | // |> When added outside an event 98 | // |> Then there is an error 99 | TestAssertionHit(graph: g) { 100 | e.removeFromGraph() 101 | } 102 | } 103 | 104 | class MyExtent: BGExtent { 105 | let r1: BGMoment 106 | 107 | init(graph: BGGraph) { 108 | let b = BGExtentBuilder(graph: graph) 109 | r1 = b.moment() 110 | super.init(builder: b) 111 | } 112 | } 113 | 114 | func testResourcePropertiesGetNames() { 115 | let e2 = MyExtent(graph: self.g) 116 | 117 | XCTAssertEqual(e2.r1.debugName, "MyExtent.r1") 118 | } 119 | 120 | func testCanTrackWhenExtentsAreAdded() { 121 | // |> Given a graph with debugOnExtentAdded defined 122 | var addedExtents: [BGExtent] = [] 123 | var addedResources: [any BGResource] = [] 124 | g.debugOnExtentAdded = { 125 | addedExtents.append($0) 126 | addedResources.append(contentsOf: $0.allResources) 127 | } 128 | 129 | // |> When an extent is added 130 | let b1 = BGExtentBuilder(graph: g) 131 | let _ = b1.moment() 132 | let e1 = BGExtent(builder: b1) 133 | 134 | e1.addToGraphWithAction() 135 | 136 | // |> Then callback is called 137 | XCTAssertEqual(e1, addedExtents[0]) 138 | XCTAssertEqual(1, addedExtents.count) 139 | XCTAssertEqual(2, addedResources.count) // _added is a default resource 140 | 141 | // |> And when callback is undefined 142 | g.debugOnExtentAdded = nil 143 | 144 | // and another extent is added 145 | let b2 = BGExtentBuilder(graph: g) 146 | let _ = b2.moment() 147 | let e2 = BGExtent(builder: b2) 148 | 149 | e2.addToGraphWithAction() 150 | 151 | // |> Then callback is not called 152 | XCTAssertEqual(1, addedExtents.count) 153 | XCTAssertEqual(2, addedResources.count) 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /Example/Tests/EventTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import XCTest 6 | import BGSwift 7 | 8 | class EventTests: XCTestCase { 9 | 10 | var g: BGGraph! 11 | var b: BGExtentBuilder! 12 | var e: BGExtent! 13 | var rA: BGState! 14 | var rB: BGState! 15 | var rC: BGState! 16 | 17 | override func setUpWithError() throws { 18 | g = BGGraph() 19 | b = BGExtentBuilder(graph: g) 20 | rA = b.state(0) 21 | rB = b.state(0) 22 | rC = b.state(0) 23 | } 24 | 25 | override func tearDownWithError() throws { 26 | // Put teardown code here. This method is called after the invocation of each test method in the class. 27 | } 28 | 29 | func testSideEffectsHappenAfterAllBehaviors() { 30 | // |> Given a behavior in the graph 31 | var counter = 0; 32 | var sideEffectCount: Int? = nil; 33 | b.behavior().demands([rA]).runs { extent in 34 | counter = counter + 1 35 | } 36 | e = BGExtent(builder: b) 37 | e.addToGraphWithAction() 38 | 39 | // |> When a sideEffect is created 40 | g.action { 41 | self.rA.update(1) 42 | self.g.sideEffect { 43 | sideEffectCount = counter 44 | } 45 | } 46 | 47 | // |> Then it will be run after all behaviors 48 | XCTAssertEqual(sideEffectCount, 1) 49 | } 50 | 51 | func testSideEffectsHappenInOrderTheyAreCreated() { 52 | // |> Given behaviors with side effects 53 | var runs: [Int] = [] 54 | b.behavior().supplies([rB]).demands([rA]).runs { extent in 55 | self.rB.update(1) 56 | extent.sideEffect { 57 | runs.append(2) 58 | } 59 | } 60 | b.behavior().demands([rB]).runs { extent in 61 | extent.sideEffect { 62 | runs.append(1) 63 | } 64 | } 65 | e = BGExtent(builder: b) 66 | e.addToGraphWithAction() 67 | 68 | // |> When those behaviors are run 69 | rA.updateWithAction(1) 70 | 71 | // |> Then the sideEffects are run in the order they are created 72 | XCTAssertEqual(runs[0], 2) 73 | XCTAssertEqual(runs[1], 1) 74 | } 75 | 76 | func testTransientValuesAreAvailableDuringEffects() { 77 | // |> Given a behavior with side effects and a transient resource 78 | var valueAvailable = false 79 | var updatedAvailable = false 80 | let rX: BGTypedMoment = b.typedMoment() 81 | b.behavior().supplies([rX]).demands([rA]).runs { extent in 82 | rX.update(2) 83 | extent.sideEffect { 84 | valueAvailable = rX.updatedValue == 2 85 | updatedAvailable = rX.justUpdated() 86 | } 87 | } 88 | e = BGExtent(builder: b) 89 | e.addToGraphWithAction() 90 | 91 | // |> When that side effect is run 92 | g.action { 93 | self.rA.update(1) 94 | } 95 | 96 | // |> Then the value and updated state of that transient resource will be available 97 | XCTAssertTrue(valueAvailable) 98 | XCTAssertTrue(updatedAvailable) 99 | // and the value/updated state will not be available outside that event 100 | XCTAssertNil(rX.updatedValue) 101 | XCTAssertFalse(rX.justUpdated()) 102 | } 103 | 104 | func testNestedActionsRunAfterSideEffects() { 105 | // |> Given a behavior with a side effect that creates a new event 106 | var counter = 0 107 | var effectCount: Int? 108 | var actionCount: Int? 109 | b.behavior().demands([rA]).runs { extent in 110 | self.e.sideEffect { 111 | self.g.action { 112 | actionCount = counter 113 | counter = counter + 1 114 | } 115 | } 116 | self.e.sideEffect { 117 | effectCount = counter 118 | counter = counter + 1 119 | } 120 | } 121 | e = BGExtent(builder: b) 122 | e.addToGraphWithAction() 123 | 124 | // |> When a nested chain of sideEffects is started 125 | rA.updateWithAction(1) 126 | 127 | // |> Then side effects are still run in order they were created 128 | XCTAssertEqual(effectCount, 0) 129 | XCTAssertEqual(actionCount, 1) 130 | } 131 | 132 | func testActionsAreSynchronousByDefault() { 133 | // |> Given a graph 134 | var counter = 0 135 | 136 | // |> When an action runs by default 137 | g.action { 138 | counter = counter + 1 139 | } 140 | 141 | // |> It will be run synchronously 142 | XCTAssertEqual(counter, 1) 143 | } 144 | 145 | func testSideEffectsMustBeCreatedInsideEvent() { 146 | // |> When a side effect is created outside of an event 147 | // |> Then an error will be raised 148 | TestAssertionHit(graph: g) { 149 | self.g.sideEffect { 150 | // nothing 151 | } 152 | } 153 | } 154 | 155 | func testDateProviderGivesAlternateTime() { 156 | g.dateProvider = { 157 | Date(timeIntervalSinceReferenceDate: 123) 158 | } 159 | var timeStamp: Date? 160 | g.action { 161 | timeStamp = self.g.currentEvent?.timestamp 162 | } 163 | XCTAssertEqual(timeStamp, Date(timeIntervalSinceReferenceDate: 123)) 164 | } 165 | } 166 | 167 | -------------------------------------------------------------------------------- /Example/Tests/BGMomentTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import XCTest 6 | import BGSwift 7 | 8 | class BGMomentTests: XCTestCase { 9 | 10 | var g: BGGraph! 11 | var bld: BGExtentBuilder! 12 | var ext: BGExtent! 13 | 14 | override func setUpWithError() throws { 15 | g = BGGraph() 16 | bld = BGExtentBuilder(graph: g) 17 | } 18 | 19 | override func tearDownWithError() throws { 20 | } 21 | 22 | func testMomentUpdates() throws { 23 | // |> Given a moment in the graph 24 | let mr1 = bld.moment() 25 | var afterUpdate = false; 26 | bld.behavior().demands([mr1]).runs { extent in 27 | if mr1.justUpdated() { 28 | afterUpdate = true 29 | } 30 | } 31 | ext = BGExtent(builder: bld) 32 | ext.addToGraphWithAction() 33 | 34 | // |> When it is read in the graph (and was not updated) 35 | var beforeUpdate = false 36 | var updateEvent: BGEvent? = nil 37 | g.action { 38 | beforeUpdate = mr1.justUpdated() 39 | mr1.update() 40 | updateEvent = self.g.currentEvent 41 | } 42 | 43 | // |> Then it didn't justUpdate before updating 44 | XCTAssertFalse(beforeUpdate) 45 | 46 | // |> And after 47 | // |> Then it did 48 | XCTAssertTrue(afterUpdate) 49 | 50 | // |> And outside an event 51 | // |> It is not just Updated 52 | XCTAssertFalse(mr1.justUpdated()) 53 | 54 | // |> And event stays the same from when it last happened 55 | XCTAssertEqual(mr1.event, updateEvent) 56 | } 57 | 58 | func testTypedMomentsHaveInformation() { 59 | // Given a moment with data 60 | let mr1: BGTypedMoment = bld.typedMoment() 61 | var afterUpdate: Int? = nil 62 | bld.behavior().demands([mr1]).runs { extent in 63 | if mr1.justUpdated() { 64 | afterUpdate = mr1.updatedValue 65 | } 66 | } 67 | ext = BGExtent(builder: bld) 68 | ext.addToGraphWithAction() 69 | 70 | // |> When it happens 71 | mr1.updateWithAction(1) 72 | 73 | // |> Then the data is visible in subsequent behaviors 74 | XCTAssertEqual(afterUpdate, 1) 75 | } 76 | 77 | func testTypedMomentsAreTransient() { 78 | // NOTE this default prevents retaining data that no longer is needed 79 | 80 | class ClassType { } 81 | 82 | // |> Given a moment with data 83 | let mr1: BGTypedMoment = bld.typedMoment() 84 | ext = BGExtent(builder: bld) 85 | ext.addToGraphWithAction() 86 | 87 | // |> When current event is over 88 | weak var weakValue: ClassType? 89 | autoreleasepool { 90 | let value = ClassType() 91 | weakValue = value 92 | mr1.updateWithAction(value) 93 | } 94 | 95 | // |> Then value nils out and is not retained 96 | XCTAssertNil(mr1.updatedValue) 97 | XCTAssertNil(weakValue) 98 | } 99 | 100 | func testNonSuppliedMomentsCanUpdateBeforeAdding() { 101 | // |> Given a moment 102 | let mr1 = bld.moment() 103 | var didRun = false; 104 | bld.behavior().demands([mr1]).runs { extent in 105 | if mr1.justUpdated() { 106 | didRun = true 107 | } 108 | } 109 | ext = BGExtent(builder: bld) 110 | 111 | // |> When it is updated in the same event as adding to graph 112 | g.action { 113 | mr1.update() 114 | self.ext.addToGraph() 115 | } 116 | 117 | // |> Then it runs and demanding behavior is run 118 | XCTAssertTrue(didRun) 119 | } 120 | 121 | func testCheckUpdatingMomentNotInGraphIsANullOp() { 122 | // NOTE: Extent can be deallocated and removed from graph while 123 | // some pending resource update may exist. Doing so should just 124 | // do nothing. 125 | 126 | // |> Given a moment resource not part of the graph 127 | let mr1 = bld.moment() 128 | 129 | // |> When it is updated 130 | var errorHappened = false 131 | g.action { 132 | errorHappened = CheckAssertionHit(graph: self.g) { 133 | mr1.update() 134 | } 135 | } 136 | 137 | // |> Then nothing happens 138 | XCTAssertFalse(errorHappened) 139 | XCTAssertEqual(mr1.event, BGEvent.unknownPast) 140 | } 141 | 142 | func testCheckMomentUpdatesOnlyHappenDuringEvent() { 143 | // |> Given a moment in the graph 144 | let mr1 = bld.moment() 145 | ext = BGExtent(builder: bld) 146 | ext.addToGraphWithAction() 147 | 148 | // |> When updating outside of an event 149 | // |> Then it should fail 150 | TestAssertionHit(graph: g) { 151 | mr1.update() 152 | } 153 | } 154 | 155 | func testCheckMomentOnlyUpdatedBySupplier() { 156 | // |> Given a supplied moment 157 | let mr1 = bld.moment() 158 | let mr2 = bld.moment() 159 | bld.behavior().supplies([mr2]).demands([mr1]).runs { extent in 160 | } 161 | bld.behavior().demands([mr1]).runs { extent in 162 | if mr1.justUpdated() { 163 | mr2.update() 164 | } 165 | } 166 | ext = BGExtent(builder: bld) 167 | ext.addToGraphWithAction() 168 | 169 | // |> When it is updated by the wrong behavior 170 | // |> Then it should throw 171 | TestAssertionHit(graph: g) { 172 | self.g.action { 173 | mr1.update() 174 | } 175 | } 176 | } 177 | 178 | func testCheckUnsuppliedMomentOnlyUpdatedInAction() { 179 | // |> Given a supplied moment and unsupplied moment 180 | let mr1 = bld.moment() 181 | let mr2 = bld.moment() 182 | var updateFailed = false 183 | bld.behavior().demands([mr1]).runs { extent in 184 | updateFailed = CheckAssertionHit(graph: self.g) { 185 | mr2.update() 186 | } 187 | } 188 | ext = BGExtent(builder: bld) 189 | ext.addToGraphWithAction() 190 | 191 | // |> When the unsupplied moment is updated by a behavior 192 | g.action { 193 | mr1.update() 194 | } 195 | 196 | // |> Then it should throw 197 | XCTAssertTrue(updateFailed) 198 | } 199 | 200 | func testCheckSuppliedMomentCannotBeUpdatedInAction() { 201 | // |> Given a supplied moment 202 | let mr1 = bld.moment() 203 | bld.behavior().supplies([mr1]).demands([]).runs { extent in 204 | } 205 | ext = BGExtent(builder: bld) 206 | ext.addToGraphWithAction() 207 | 208 | // |> When we try updating that moment in an action 209 | var updateFailed = false 210 | self.g.action { 211 | updateFailed = CheckAssertionHit(graph: self.g) { 212 | mr1.update() 213 | } 214 | } 215 | 216 | // |> Then the updating chould throw 217 | XCTAssertTrue(updateFailed) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /Code_of_Conduct.md: -------------------------------------------------------------------------------- 1 | # Yahoo Inc Open Source Code of Conduct 2 | 3 | ## Summary 4 | This Code of Conduct is our way to encourage good behavior and discourage bad behavior in our open source projects. We invite participation from many people to bring different perspectives to our projects. We will do our part to foster a welcoming and professional environment free of harassment. We expect participants to communicate professionally and thoughtfully during their involvement with this project. 5 | 6 | Participants may lose their good standing by engaging in misconduct. For example: insulting, threatening, or conveying unwelcome sexual content. We ask participants who observe conduct issues to report the incident directly to the project's Response Team at opensource-conduct@yahooinc.com. Yahoo will assign a respondent to address the issue. We may remove harassers from this project. 7 | 8 | This code does not replace the terms of service or acceptable use policies of the websites used to support this project. We acknowledge that participants may be subject to additional conduct terms based on their employment which may govern their online expressions. 9 | 10 | ## Details 11 | This Code of Conduct makes our expectations of participants in this community explicit. 12 | * We forbid harassment and abusive speech within this community. 13 | * We request participants to report misconduct to the project’s Response Team. 14 | * We urge participants to refrain from using discussion forums to play out a fight. 15 | 16 | ### Expected Behaviors 17 | We expect participants in this community to conduct themselves professionally. Since our primary mode of communication is text on an online forum (e.g. issues, pull requests, comments, emails, or chats) devoid of vocal tone, gestures, or other context that is often vital to understanding, it is important that participants are attentive to their interaction style. 18 | 19 | * **Assume positive intent.** We ask community members to assume positive intent on the part of other people’s communications. We may disagree on details, but we expect all suggestions to be supportive of the community goals. 20 | * **Respect participants.** We expect occasional disagreements. Open Source projects are learning experiences. Ask, explore, challenge, and then _respectfully_ state if you agree or disagree. If your idea is rejected, be more persuasive not bitter. 21 | * **Welcoming to new members.** New members bring new perspectives. Some ask questions that have been addressed before. _Kindly_ point to existing discussions. Everyone is new to every project once. 22 | * **Be kind to beginners.** Beginners use open source projects to get experience. They might not be talented coders yet, and projects should not accept poor quality code. But we were all beginners once, and we need to engage kindly. 23 | * **Consider your impact on others.** Your work will be used by others, and you depend on the work of others. We expect community members to be considerate and establish a balance their self-interest with communal interest. 24 | * **Use words carefully.** We may not understand intent when you say something ironic. Often, people will misinterpret sarcasm in online communications. We ask community members to communicate plainly. 25 | * **Leave with class.** When you wish to resign from participating in this project for any reason, you are free to fork the code and create a competitive project. Open Source explicitly allows this. Your exit should not be dramatic or bitter. 26 | 27 | ### Unacceptable Behaviors 28 | Participants remain in good standing when they do not engage in misconduct or harassment (some examples follow). We do not list all forms of harassment, nor imply some forms of harassment are not worthy of action. Any participant who *feels* harassed or *observes* harassment, should report the incident to the Response Team. 29 | * **Don't be a bigot.** Calling out project members by their identity or background in a negative or insulting manner. This includes, but is not limited to, slurs or insinuations related to protected or suspect classes e.g. race, color, citizenship, national origin, political belief, religion, sexual orientation, gender identity and expression, age, size, culture, ethnicity, genetic features, language, profession, national minority status, mental or physical ability. 30 | * **Don't insult.** Insulting remarks about a person’s lifestyle practices. 31 | * **Don't dox.** Revealing private information about other participants without explicit permission. 32 | * **Don't intimidate.** Threats of violence or intimidation of any project member. 33 | * **Don't creep.** Unwanted sexual attention or content unsuited for the subject of this project. 34 | * **Don't inflame.** We ask that victim of harassment not address their grievances in the public forum, as this often intensifies the problem. Report it, and let us address it off-line. 35 | * **Don't disrupt.** Sustained disruptions in a discussion. 36 | 37 | ### Reporting Issues 38 | If you experience or witness misconduct, or have any other concerns about the conduct of members of this project, please report it by contacting our Response Team at opensource-conduct@yahooinc.com who will handle your report with discretion. Your report should include: 39 | * Your preferred contact information. We cannot process anonymous reports. 40 | * Names (real or usernames) of those involved in the incident. 41 | * Your account of what occurred, and if the incident is ongoing. Please provide links to or transcripts of the publicly available records (e.g. a mailing list archive or a public IRC logger), so that we can review it. 42 | * Any additional information that may be helpful to achieve resolution. 43 | 44 | After filing a report, a representative will contact you directly to review the incident and ask additional questions. If a member of the Yahoo Response Team is named in an incident report, that member will be recused from handling your incident. If the complaint originates from a member of the Response Team, it will be addressed by a different member of the Response Team. We will consider reports to be confidential for the purpose of protecting victims of abuse. 45 | 46 | ### Scope 47 | Yahoo will assign a Response Team member with admin rights on the project and legal rights on the project copyright. The Response Team is empowered to restrict some privileges to the project as needed. Since this project is governed by an open source license, any participant may fork the code under the terms of the project license. The Response Team’s goal is to preserve the project if possible, and will restrict or remove participation from those who disrupt the project. 48 | 49 | This code does not replace the terms of service or acceptable use policies that are provided by the websites used to support this community. Nor does this code apply to communications or actions that take place outside of the context of this community. Many participants in this project are also subject to codes of conduct based on their employment. This code is a social-contract that informs participants of our social expectations. It is not a terms of service or legal contract. 50 | 51 | ## License and Acknowledgment. 52 | This text is shared under the [CC-BY-4.0 license](https://creativecommons.org/licenses/by/4.0/). This code is based on a study conducted by the [TODO Group](https://todogroup.org/) of many codes used in the open source community. If you have feedback about this code, contact our Response Team at the address listed above. 53 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Example/Tests/ConcurrencyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import XCTest 6 | @testable import BGSwift 7 | 8 | class ConcurrencyTests: XCTestCase { 9 | 10 | let SYNC_EXPECTATION_TIMEOUT = TimeInterval(0) 11 | let ASYNC_EXPECTATION_TIMEOUT = TimeInterval(0.1) 12 | 13 | func runSyncOnMain(_ code: () throws -> Void) rethrows { 14 | if Thread.isMainThread { 15 | try code() 16 | } else { 17 | try DispatchQueue.main.sync(execute: code) 18 | } 19 | } 20 | 21 | func testSyncActionRunInSideEffectRunsSynchronously() { 22 | let exp = XCTestExpectation() 23 | 24 | let g = BGGraph() 25 | 26 | DispatchQueue.global().async { 27 | XCTAssertFalse(Thread.isMainThread) 28 | 29 | g.action(syncStrategy: .sync) { 30 | g.sideEffect { 31 | var actionRan = false 32 | g.action(syncStrategy: .sync) { 33 | actionRan = true 34 | } 35 | if actionRan { 36 | exp.fulfill() 37 | } 38 | } 39 | } 40 | } 41 | 42 | wait(for: [exp], timeout: ASYNC_EXPECTATION_TIMEOUT) 43 | } 44 | 45 | func testSyncStrategyUnspecifiedActionRunInSideEffectRunsSynchronously() { 46 | let exp = XCTestExpectation() 47 | 48 | let g = BGGraph() 49 | 50 | DispatchQueue.global().async { 51 | XCTAssertFalse(Thread.isMainThread) 52 | 53 | g.action(syncStrategy: .sync) { 54 | g.sideEffect { 55 | var actionRan = false 56 | g.action(syncStrategy: nil) { 57 | actionRan = true 58 | } 59 | if actionRan { 60 | exp.fulfill() 61 | } 62 | } 63 | } 64 | } 65 | 66 | wait(for: [exp], timeout: ASYNC_EXPECTATION_TIMEOUT) 67 | } 68 | 69 | func testSyncActionRunWhenOtherActionInProgressWaitsAndRunsSynchronously() { 70 | //@SAL 9/6/2023 -- I'm having a hard time figuring out what scenario this test is trying to cover 71 | let innerActionRan = XCTestExpectation() 72 | 73 | let g = BGGraph() 74 | 75 | let sleepInterval = TimeInterval(0.1) 76 | 77 | var waitedForInnerAction: Bool? = nil 78 | 79 | // Begin execution onto a background thread so that both actions' threads are equal priortity 80 | DispatchQueue.global().async { 81 | g.action(syncStrategy: .sync) { 82 | // Begin second action on another thread 83 | DispatchQueue.global().async { 84 | var actionRan = false 85 | 86 | // Note that it is possible that the outer action completes before the inner 87 | // action attempts to run. In that case, the test will succeed even 88 | // if there was never any contention for the graph's mutex. Hopefully this 89 | // won't be the case if we choose a sufficient sleep interval. 90 | g.action(syncStrategy: .sync) { 91 | actionRan = true 92 | } 93 | waitedForInnerAction = actionRan 94 | // @SAL 9/6/2023 -- Moved this here because the action was running syncrhronously(as expected) 95 | // but as soon as innerActionRan was fulfilled, the test would end and the assertion 96 | // this may be because there are now always side effects (because deferred release clearing is a side effect) 97 | // So it now always synchronizes onto the main thread which gives it a chance for the wait below to be satisfied 98 | innerActionRan.fulfill() 99 | } 100 | } 101 | } 102 | 103 | wait(for: [innerActionRan], timeout: ASYNC_EXPECTATION_TIMEOUT + sleepInterval) 104 | XCTAssertEqual(waitedForInnerAction, true) 105 | } 106 | 107 | func testSyncStrategyUnspecifiedActionRunWhenOtherActionInProgressReturnsSynchronouslyAndSchedulesAsyncAction() { 108 | let innerActionRan = XCTestExpectation() 109 | 110 | let g = BGGraph() 111 | 112 | var waitedForInnerAction: Bool? = nil 113 | 114 | // Begin execution onto a background thread so that both actions' threads are equal priortity 115 | DispatchQueue.global().async { 116 | g.action(syncStrategy: .sync) { 117 | let semaphore = DispatchSemaphore(value: 0) 118 | 119 | // Begin second action on another thread 120 | DispatchQueue.global().async { 121 | var actionRan = false 122 | g.action(syncStrategy: nil) { 123 | actionRan = true 124 | innerActionRan.fulfill() 125 | } 126 | waitedForInnerAction = actionRan 127 | 128 | semaphore.signal() 129 | } 130 | 131 | // Wait until other action is scheduled 132 | semaphore.wait() 133 | } 134 | } 135 | 136 | wait(for: [innerActionRan], timeout: ASYNC_EXPECTATION_TIMEOUT) 137 | XCTAssertEqual(waitedForInnerAction, false) 138 | } 139 | 140 | func testSyncStrategyUnspecifiedActionCalledInAnotherActionIsRunAsyncOnSameDispatchQueueLoop() { 141 | let g = BGGraph() 142 | 143 | var actionOrder = [String]() 144 | g.action(impulse: "outer", syncStrategy: .sync) { 145 | g.action(impulse: "inner", syncStrategy: nil) { 146 | actionOrder.append(g.currentEvent!.impulse!) 147 | } 148 | actionOrder.append(g.currentEvent!.impulse!) 149 | } 150 | XCTAssertEqual(actionOrder, ["outer", "inner"]) 151 | } 152 | 153 | func testSyncStrategyUnspecifiedActionCalledInBehaviorIsRunAsyncOnSameDispatchQueueLoop() { 154 | let g = BGGraph() 155 | 156 | var execOrder = [String]() 157 | 158 | let b = BGExtentBuilder(graph: g) 159 | b.behavior().demands([b.added]).runs { extent in 160 | g.action(impulse: "inner") { 161 | execOrder.append(g.currentEvent!.impulse!) 162 | } 163 | execOrder.append(g.currentEvent!.impulse!) 164 | } 165 | 166 | let e = BGExtent(builder: b) 167 | g.action(impulse: "outer") { 168 | e.addToGraph() 169 | } 170 | XCTAssertEqual(execOrder, ["outer", "inner"]) 171 | } 172 | 173 | func testUpdatingResourceOfDeallocedExtentIsANoOp() { 174 | let g = BGGraph() 175 | 176 | let asyncActionFinished = XCTestExpectation() 177 | 178 | autoreleasepool { 179 | let b = BGExtentBuilder(graph: g) 180 | let r = b.state(false) 181 | let e = BGExtent(builder: b) 182 | 183 | g.action { 184 | e.addToGraph() 185 | } 186 | 187 | g.action(syncStrategy: .async(queue: DispatchQueue.global(qos: .background))) { [weak e] in 188 | r.update(true) 189 | 190 | XCTAssertNil(e) 191 | XCTAssertFalse(r.value) 192 | asyncActionFinished.fulfill() 193 | } 194 | 195 | // e deallocs before the async action is executed 196 | } 197 | 198 | wait(for: [asyncActionFinished], timeout: ASYNC_EXPECTATION_TIMEOUT) 199 | } 200 | 201 | func testSyncActionOnMainThreadWhileBackgroundActionWithSideEffectIsRunningCompletesSynchronously() { 202 | let g = BGGraph() 203 | 204 | var execOrder = [String]() 205 | 206 | let mutex = Mutex(recursive: false) 207 | func appendExecOrder(_ str: String) { 208 | mutex.balancedUnlock { 209 | execOrder.append(str) 210 | } 211 | } 212 | 213 | let workTime: TimeInterval = 1 214 | 215 | let backgroundActionRunning = XCTestExpectation() 216 | let backgroundActionCompleted = XCTestExpectation() 217 | 218 | g.action(syncStrategy: .async(queue: DispatchQueue.global(qos: .background))) { 219 | appendExecOrder("background-action-start") 220 | backgroundActionRunning.fulfill() 221 | 222 | // do work 223 | Thread.sleep(forTimeInterval: workTime) 224 | appendExecOrder("background-work-complete") 225 | 226 | g.sideEffect { 227 | appendExecOrder("se-background") 228 | } 229 | 230 | backgroundActionCompleted.fulfill() 231 | } 232 | 233 | wait(for: [backgroundActionRunning], timeout: ASYNC_EXPECTATION_TIMEOUT) 234 | appendExecOrder("creating-main-action") 235 | g.action(syncStrategy: .sync) { 236 | appendExecOrder("main-action-start") 237 | g.sideEffect { 238 | appendExecOrder("se-main") 239 | } 240 | } 241 | XCTAssertEqual(execOrder, ["background-action-start", "creating-main-action", "background-work-complete", "se-background", "main-action-start", "se-main"]) 242 | } 243 | 244 | func testSyncNestedActionsDisallowed() { 245 | let g = BGGraph() 246 | TestAssertionHit(graph: g) { 247 | g.action { 248 | g.action(syncStrategy:.sync) { 249 | } 250 | } 251 | } 252 | } 253 | 254 | func testSyncActionsInBehaviorsDisallowed() { 255 | // NOTE: an action inside a behavior can essentially be considered a side effect by default 256 | // as long as we can delay that action until after the current event which is impossible 257 | // with a sync actions 258 | 259 | // |> Given a behavior in the graph that creates an action outside of a side effect 260 | let g = BGGraph() 261 | let b = BGExtentBuilder(graph: g) 262 | let r1 = b.moment() 263 | b.behavior().demands([r1]).runs { extent in 264 | g.action(syncStrategy:.sync) { 265 | // do something 266 | } 267 | // cant run because I may have code here that expected it to run 268 | } 269 | let e = BGExtent(builder: b) 270 | e.addToGraphWithAction() 271 | 272 | // |> When that behavior runs 273 | // |> Then it should throw an error 274 | TestAssertionHit(graph: g) { 275 | r1.updateWithAction() 276 | } 277 | } 278 | 279 | func testSyncActionsInSideEffectsAreAllowed() { 280 | // NOTE: This is currently allowed because its where one would expect actions to be created 281 | // however, technically the synchronous action could cut ahead of some pending side effects from 282 | // the existing event. So maybe that could be disallowed and forced into optional asynchrony or fixed? 283 | 284 | // |> Given a behavior in the graph that creates an action outside of a side effect 285 | let g = BGGraph() 286 | let b = BGExtentBuilder(graph: g) 287 | let r1 = b.moment() 288 | b.behavior().demands([r1]).runs { extent in 289 | extent.sideEffect { 290 | g.action(syncStrategy:.sync) { 291 | // do something 292 | } 293 | // cant run because I may have code here that expected it to run 294 | } 295 | extent.sideEffect { 296 | // run something else 297 | // technicallthe the action above is cutting ahead in line and changing state on us 298 | } 299 | } 300 | let e = BGExtent(builder: b) 301 | e.addToGraphWithAction() 302 | 303 | // |> When that behavior runs 304 | // |> Then is should be allowed? 305 | let failed = CheckAssertionHit(graph: g) { 306 | r1.updateWithAction() 307 | } 308 | XCTAssertFalse(failed) 309 | } 310 | 311 | func testBehaviorsRunOnMainThreadWhenConfigured() { 312 | let g = BGGraph() 313 | let b = BGExtentBuilder(graph: g) 314 | let r1 = b.moment() 315 | 316 | let backgroundWorkRan = XCTestExpectation() 317 | 318 | var behavior1RanOnMain: Bool? 319 | var behavior2RanOnMain: Bool? 320 | 321 | b.behavior() 322 | .demands(r1) 323 | .requiresMainThread() 324 | .runs { extent in 325 | behavior1RanOnMain = Thread.isMainThread 326 | } 327 | 328 | b.behavior() 329 | .demands(r1) 330 | .runs { extent in 331 | behavior2RanOnMain = Thread.isMainThread 332 | } 333 | 334 | let e = BGExtent(builder: b) 335 | e.addToGraphWithAction() 336 | 337 | DispatchQueue.global(qos: .default).async { 338 | g.action(syncStrategy: .sync) { 339 | r1.update() 340 | } 341 | backgroundWorkRan.fulfill() 342 | } 343 | 344 | wait(for: [backgroundWorkRan], timeout: ASYNC_EXPECTATION_TIMEOUT) 345 | XCTAssertEqual(behavior1RanOnMain, true) 346 | XCTAssertEqual(behavior2RanOnMain, false) 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /Example/BGSwift/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 30 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 66 | 74 | 82 | 90 | 97 | 98 | 99 | 100 | 101 | 102 | 109 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /BGSwift/Classes/BGResource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import Foundation 6 | 7 | public protocol BGOptionalObject { 8 | associatedtype Wrapped: AnyObject 9 | var bg_unwrapped: Wrapped? { get } 10 | } 11 | 12 | extension Optional: BGOptionalObject where Wrapped: AnyObject { 13 | public var bg_unwrapped: Wrapped? { 14 | return self 15 | } 16 | } 17 | 18 | public protocol BGResource: AnyObject, BGDemandable, CustomDebugStringConvertible { 19 | var _event: BGEvent { get } 20 | var debugName: String? { get set } 21 | } 22 | 23 | protocol BGResourceInternal: AnyObject, BGResource { 24 | associatedtype ValueType 25 | 26 | var subsequents: Set { get set } 27 | var supplier: BGBehavior? { get set } 28 | var owner: BGExtent? { get set } 29 | var _prevEvent: BGEvent { get set } 30 | var _order: BGDemandable? { get set } 31 | var _event: BGEvent { get set } 32 | 33 | var _value: ValueType { get set } 34 | func shouldSkipUpdate(_ other: ValueType) -> Bool 35 | 36 | var deferClearingTransientValue: Bool { get } 37 | func clearTransientValue() 38 | } 39 | 40 | public extension BGResource { 41 | var event: BGEvent { 42 | asInternal.verifyDemands() 43 | return _event 44 | } 45 | 46 | var traceEvent: BGEvent { 47 | let asInternal = asInternal 48 | return _event.sequence == asInternal.graph?.currentEvent?.sequence ? asInternal._prevEvent : _event 49 | } 50 | 51 | var order: BGDemandable { 52 | let asInternal = asInternal 53 | if let order = asInternal._order { 54 | return order 55 | } else { 56 | let order = BGDemandLink(resource: asInternal, type: .order) 57 | asInternal._order = order 58 | return order 59 | } 60 | } 61 | 62 | func justUpdated() -> Bool { 63 | guard let currentEvent = asInternal.graph?.currentEvent else { 64 | return false 65 | } 66 | 67 | return currentEvent.sequence == event.sequence 68 | } 69 | 70 | func hasUpdated() -> Bool { 71 | event.sequence > BGEvent.unknownPast.sequence 72 | } 73 | 74 | func hasUpdatedSince(_ resource: BGResource?) -> Bool { 75 | event.happenedSince(sequence: resource?.event.sequence ?? 0) 76 | } 77 | 78 | func happenedSince(sequence: UInt) -> Bool { 79 | event.happenedSince(sequence: sequence) 80 | } 81 | 82 | var subsequentBehaviors: [BGBehavior] { 83 | asInternal.subsequents.compactMap { $0.behavior } 84 | } 85 | } 86 | 87 | internal extension BGResource { 88 | var asInternal: any BGResourceInternal { self as! (any BGResourceInternal) } 89 | } 90 | 91 | extension BGResourceInternal { 92 | var graph: BGGraph? { owner?.graph } 93 | 94 | func verifyDemands() { 95 | // TODO: compile out checks with build flag 96 | guard let graph = graph, graph.checkUndeclaredDemands, graph.currentThread == Thread.current else { 97 | return 98 | } 99 | 100 | if let currentBehavior = graph.currentRunningBehavior, currentBehavior !== supplier { 101 | graph.assert(currentBehavior.demands.first(where: { $0.resource === self }) != nil, 102 | "Accessed a resource in a behavior that was not declared as a demand.") 103 | } 104 | } 105 | 106 | var updateable: BGResourceUpdatable { 107 | guard let graph = self.graph else { 108 | // If graph is nil, then weak extent has been deallocated and resource updates are no-ops. 109 | return .notUpdatable 110 | } 111 | 112 | guard let currentEvent = graph.currentEvent else { 113 | graph.assertionFailure("Can only update a resource during an event.") 114 | return .notUpdatable 115 | } 116 | 117 | guard let owner = owner else { 118 | graph.assertionFailure("Cannot update a resource that does not belong to an extent.") 119 | return .notUpdatable 120 | } 121 | 122 | if let behavior = supplier { 123 | 124 | guard graph.currentRunningBehavior === behavior else { 125 | graph.assertionFailure("Cannot update a resource with a supplying behavior during an action.") 126 | return .notUpdatable 127 | } 128 | } else { 129 | if self !== owner._added { 130 | guard graph.processingAction else { 131 | graph.assertionFailure("Cannot update a resource with no supplying behaviour outside an action.") 132 | return .notUpdatable 133 | } 134 | } 135 | } 136 | 137 | guard _event.sequence < currentEvent.sequence else { 138 | // assert or fail? 139 | graph.assertionFailure("Can only update a resource once per event.") 140 | return .notUpdatable 141 | } 142 | 143 | return .updateable(graph: graph, currentEvent: currentEvent) 144 | } 145 | 146 | func _update(_ newValue: ValueType) { 147 | guard case .updateable(let graph, let event) = updateable else { 148 | return 149 | } 150 | 151 | if shouldSkipUpdate(newValue) { 152 | return 153 | } 154 | 155 | _prevEvent = _event; 156 | 157 | _value = newValue 158 | _event = event 159 | 160 | graph.updatedResources.append(self) 161 | 162 | if deferClearingTransientValue { 163 | graph.updatedTransientResources.append(self) 164 | } else { 165 | clearTransientValue() 166 | } 167 | } 168 | 169 | func _updateWithAction(_ newValue: ValueType, file: String, line: Int, function: String, syncStrategy: BGGraph.SynchronizationStrategy?) { 170 | graph?.action(file: file, line: line, function: function, syncStrategy: syncStrategy) { 171 | self._update(newValue) 172 | } 173 | } 174 | 175 | func _updateWithAction(_ newValue: ValueType, impulse: String, syncStrategy: BGGraph.SynchronizationStrategy?) { 176 | graph?.action(impulse: impulse, syncStrategy: syncStrategy) { 177 | self._update(newValue) 178 | } 179 | } 180 | 181 | var weakReference: WeakResource { 182 | WeakResource(self) 183 | } 184 | 185 | public func hash(into hasher: inout Hasher) { 186 | hasher.combine(ObjectIdentifier(self)) 187 | } 188 | } 189 | 190 | struct WeakResource: Equatable, Hashable { 191 | weak var resource: (any BGResourceInternal)? 192 | let resourcePtr: ObjectIdentifier 193 | 194 | init(_ resource: any BGResourceInternal) { 195 | self.resource = resource 196 | resourcePtr = ObjectIdentifier(resource) 197 | } 198 | 199 | // MARK: Equatable 200 | 201 | static func == (lhs: WeakResource, rhs: WeakResource) -> Bool { 202 | guard let l = lhs.resource, let r = rhs.resource, l === r else { 203 | return false 204 | } 205 | return true 206 | } 207 | 208 | // MARK: Hashable 209 | 210 | func hash(into hasher: inout Hasher) { 211 | hasher.combine(resourcePtr) 212 | } 213 | } 214 | 215 | enum BGResourceUpdatable { 216 | case notUpdatable 217 | case updateable(graph: BGGraph, currentEvent: BGEvent) 218 | } 219 | 220 | public class BGMoment: BGResource, BGResourceInternal { 221 | typealias ValueType = Void 222 | var _value: ValueType = Void() 223 | 224 | public func update() { 225 | _update(Void()) 226 | } 227 | 228 | public func updateWithAction(file: String = #fileID, line: Int = #line, function: String = #function, syncStrategy: BGGraph.SynchronizationStrategy? = nil) { 229 | _updateWithAction(Void(), file: file, line: line, function: function, syncStrategy: syncStrategy) 230 | } 231 | 232 | public func updateWithAction(impulse: String, syncStrategy: BGGraph.SynchronizationStrategy? = nil) { 233 | _updateWithAction(Void(), impulse: impulse, syncStrategy: syncStrategy) 234 | } 235 | 236 | // MARK: BGResource 237 | 238 | var _debugName: String? 239 | public var debugName: String? { 240 | get { 241 | if _debugName != nil { 242 | return _debugName 243 | } else { 244 | self.owner?.loadDebugNames() 245 | return _debugName 246 | } 247 | } 248 | set { _debugName = newValue } 249 | } 250 | public var _event: BGEvent = .unknownPast 251 | 252 | // MARK: BGResourceInternal 253 | 254 | var subsequents = Set() 255 | weak var supplier: BGBehavior? 256 | weak var owner: BGExtent? 257 | var _prevEvent: BGEvent = .unknownPast 258 | var _order: BGDemandable? 259 | 260 | func shouldSkipUpdate(_ other: Void) -> Bool { false } 261 | var deferClearingTransientValue: Bool { false } 262 | func clearTransientValue() {} 263 | 264 | // MARK: CustomDebugStringConvertible 265 | 266 | public var debugDescription: String { 267 | var updated = false 268 | if let currentEvent = graph?.currentEvent, 269 | currentEvent.sequence == _event.sequence { 270 | updated = true 271 | } 272 | 273 | return "<\(String(describing: Self.self)):\(String(format: "%018p", unsafeBitCast(self, to: Int64.self))) (\(debugName ?? "Unlabeled")), updated=\(updated)>" 274 | } 275 | } 276 | 277 | public class BGTypedMoment: BGResource, BGResourceInternal { 278 | public var updatedValue: Type? { 279 | verifyDemands() 280 | return _value 281 | } 282 | 283 | public func update(_ newValue: Type) { 284 | _update(newValue) 285 | } 286 | 287 | public func updateWithAction(_ newValue: Type, file: String = #fileID, line: Int = #line, function: String = #function, syncStrategy: BGGraph.SynchronizationStrategy? = nil) { 288 | _updateWithAction(newValue, file: file, line: line, function: function, syncStrategy: syncStrategy) 289 | } 290 | 291 | public func updateWithAction(_ newValue: Type, impulse: String, syncStrategy: BGGraph.SynchronizationStrategy? = nil) { 292 | _updateWithAction(newValue, impulse: impulse, syncStrategy: syncStrategy) 293 | } 294 | 295 | func shouldSkipUpdate(_ other: Type?) -> Bool { false } 296 | var deferClearingTransientValue: Bool { true } 297 | 298 | func clearTransientValue() { 299 | _value = nil 300 | } 301 | 302 | // MARK: BGResource 303 | 304 | var _debugName: String? 305 | public var debugName: String? { 306 | get { 307 | if _debugName != nil { 308 | return _debugName 309 | } else { 310 | self.owner?.loadDebugNames() 311 | return _debugName 312 | } 313 | } 314 | set { _debugName = newValue } 315 | } 316 | public var _event: BGEvent = .unknownPast 317 | 318 | // MARK: BGResourceInternal 319 | 320 | typealias ValueType = Type? 321 | var subsequents = Set() 322 | weak var supplier: BGBehavior? 323 | weak var owner: BGExtent? 324 | var _prevEvent: BGEvent = .unknownPast 325 | var _order: BGDemandable? 326 | public var _value: Type? 327 | 328 | // MARK: CustomDebugStringConvertible 329 | 330 | public var debugDescription: String { 331 | let updated: String 332 | if let value = _value { 333 | updated = " updatedValue=\(String(describing: value))" 334 | } else { 335 | updated = "" 336 | } 337 | 338 | return "<\(String(describing: Self.self)):\(String(format: "%018p", unsafeBitCast(self, to: Int64.self))) (\(debugName ?? "Unlabeled")\(updated)>" 339 | } 340 | } 341 | 342 | public enum BGStateComparison { 343 | public enum None { case none } 344 | public enum Equal { case equal } 345 | public enum Identical { case identical } 346 | } 347 | 348 | public class BGState: BGResource, BGResourceInternal { 349 | var _prevValue: Type? 350 | private var comparison: ((_ lhs: Type, _ rhs: Type) -> Bool)? 351 | 352 | internal init(_ value: Type, comparison: ((_ lhs: Type, _ rhs: Type) -> Bool)?) { 353 | self.comparison = comparison 354 | self._value = value 355 | self._prevValue = value 356 | } 357 | 358 | public var value: Type { 359 | verifyDemands() 360 | return _value 361 | } 362 | 363 | public var traceValue: Type { 364 | if let value = _prevValue { 365 | return value 366 | } else { 367 | return _value 368 | } 369 | } 370 | 371 | public var updatedValue: Type? { 372 | verifyDemands() 373 | return justUpdated() ? _value : nil 374 | } 375 | 376 | func valueEquals(_ other: Type) -> Bool { 377 | if let comparison = comparison { 378 | return comparison(_value, other) 379 | } else { 380 | return false 381 | } 382 | } 383 | 384 | func setInitialValue(_ value: Type) { 385 | graph?.assert(owner == nil || owner?.status == .inactive) 386 | _prevValue = value 387 | _value = value 388 | } 389 | 390 | public func update(_ newValue: Type, forced: Bool = false) { 391 | guard case .updateable(let graph, let event) = updateable else { 392 | return 393 | } 394 | 395 | if forced || !valueEquals(newValue) { 396 | _prevValue = _value; 397 | _prevEvent = _event; 398 | 399 | _value = newValue 400 | _event = event 401 | 402 | graph.updatedResources.append(self) 403 | graph.updatedTransientResources.append(self) 404 | } 405 | } 406 | 407 | public func updateWithAction(_ newValue: Type, file: String = #fileID, line: Int = #line, function: String = #function, syncStrategy: BGGraph.SynchronizationStrategy? = nil) { 408 | graph?.action(file: file, line: line, function: function, syncStrategy: syncStrategy) { 409 | self.update(newValue) 410 | } 411 | } 412 | 413 | public func updateWithAction(_ newValue: Type, impulse: String, syncStrategy: BGGraph.SynchronizationStrategy? = nil) { 414 | graph?.action(impulse: impulse, syncStrategy: syncStrategy) { 415 | self.update(newValue) 416 | } 417 | } 418 | 419 | public func justUpdated(to: Type) -> Bool { 420 | return justUpdated() && valueEquals(to) 421 | } 422 | 423 | public func justUpdated(from: Type) -> Bool { 424 | guard justUpdated(), let comparison = comparison else { 425 | return false 426 | } 427 | return comparison(traceValue, from) 428 | } 429 | 430 | public func justUpdated(to: Type, from: Type) -> Bool { 431 | guard justUpdated(), let comparison = comparison else { 432 | return false 433 | } 434 | return comparison(_value, to) && comparison(traceValue, from) 435 | } 436 | 437 | // MARK: BGResource 438 | 439 | var _debugName: String? 440 | public var debugName: String? { 441 | get { 442 | if _debugName != nil { 443 | return _debugName 444 | } else { 445 | self.owner?.loadDebugNames() 446 | return _debugName 447 | } 448 | } 449 | set { _debugName = newValue } 450 | } 451 | public var _event: BGEvent = .unknownPast 452 | 453 | // MARK: BGResourceInternal 454 | 455 | var subsequents = Set() 456 | weak var supplier: BGBehavior? 457 | weak var owner: BGExtent? 458 | var _prevEvent: BGEvent = .unknownPast 459 | var _order: BGDemandable? 460 | public var _value: Type 461 | 462 | func shouldSkipUpdate(_ other: Type) -> Bool { 463 | valueEquals(other) 464 | } 465 | 466 | var deferClearingTransientValue: Bool { _prevValue != nil } 467 | 468 | func clearTransientValue() { 469 | // Temporarily extend the lifetime of the value on the stack to avoid a crash 470 | // if clearing this value triggers dealloc code that in turn accesses 471 | // this value. 472 | withExtendedLifetime(_prevValue) { 473 | _prevValue = nil 474 | } 475 | } 476 | 477 | // MARK: CustomDebugStringConvertible 478 | 479 | public var debugDescription: String { 480 | let traceValue: String 481 | if let prevValue = _prevValue { 482 | traceValue = ", traceValue=\(String(describing: prevValue))" 483 | } else { 484 | traceValue = "" 485 | } 486 | 487 | return "<\(String(describing: Self.self)):\(String(format: "%018p", unsafeBitCast(self, to: Int64.self))) (\(debugName ?? "Unlabeled") value=\(String(describing: _value))\(traceValue)>" 488 | } 489 | } 490 | 491 | public protocol BGDemandable {} 492 | 493 | extension BGDemandable { 494 | 495 | var link: BGDemandLink { 496 | switch self { 497 | case let link as BGDemandLink: 498 | return link 499 | case let resource as any BGResourceInternal: 500 | return BGDemandLink(resource: resource, type: .reactive) 501 | default: 502 | preconditionFailure("Unknown `BGDemandable` type.") 503 | } 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /Example/Tests/BGStateTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import Foundation 6 | import XCTest 7 | @testable import BGSwift 8 | 9 | class BGStateTests : XCTestCase { 10 | 11 | var g: BGGraph! 12 | var r_a, r_b: BGState! 13 | var bld: BGExtentBuilder! 14 | var ext: BGExtent! 15 | 16 | override func setUp() { 17 | g = BGGraph() 18 | bld = BGExtentBuilder(graph: g) 19 | r_a = bld.state(0) 20 | r_b = bld.state(0) 21 | } 22 | 23 | func testHasInitialState() { 24 | // |> When we create a new state resource 25 | // |> It has an initial value 26 | XCTAssertEqual(r_a.value, 0) 27 | } 28 | 29 | func testUpdatesWhenAddedToGraph() { 30 | ext = BGExtent(builder: bld) 31 | ext.addToGraphWithAction() 32 | 33 | // |> When it is updated 34 | r_a.updateWithAction(2) 35 | 36 | // |> Then it has new value and event 37 | XCTAssertEqual(r_a.value, 2) 38 | XCTAssertEqual(r_a.event, g.lastEvent) 39 | } 40 | 41 | func testCanHandleNullNilValues() { 42 | // Motivation: nullable states are useful for modeling false/true with data 43 | 44 | // |> Given a nullable state 45 | let r_n: BGState = bld.state(nil) 46 | ext = BGExtent(builder: bld) 47 | ext.addToGraphWithAction() 48 | 49 | // |> When updated 50 | r_n.updateWithAction(1) 51 | 52 | // |> Then it will have that new state 53 | XCTAssertEqual(r_n.value, 1) 54 | 55 | // |> And when updated to nil 56 | r_n.updateWithAction(nil) 57 | 58 | // |> Then it will have nil state 59 | XCTAssertNil(r_n.value) 60 | } 61 | 62 | func testWorksAsDemandAndSupply() { 63 | // |> Given state resources and behaviors 64 | var ran = false; 65 | bld.behavior().supplies([r_b]).demands([r_a]).runs { extent in 66 | self.r_b.update(self.r_a.value) 67 | } 68 | 69 | bld.behavior().demands([r_b]).runs { extent in 70 | ran = true 71 | } 72 | ext = BGExtent(builder: bld) 73 | ext.addToGraphWithAction() 74 | 75 | // |> When event is started 76 | r_a.updateWithAction(1) 77 | 78 | // |> Then subsequent behaviors are run 79 | XCTAssertEqual(r_b.value, 1) 80 | XCTAssertEqual(ran, true) 81 | } 82 | 83 | func testJustUpdatedChecksDuringEvent() { 84 | var updatedA = false 85 | var updatedB = false 86 | var updatedToA = false 87 | var updatedToWrongToA = false 88 | var updatedToB = false 89 | var updatedToFromA = false 90 | var updatedToFromWrongToA = false 91 | var updatedToFromWrongFromA = false 92 | var updatedToFromB = false 93 | var updatedFromA = false 94 | var updatedFromWrongFromA = false 95 | var updatedFromB = false 96 | 97 | // |> Given a behavior that tracks updated methods 98 | bld.behavior().demands([r_a, r_b]).runs { extent in 99 | updatedA = self.r_a.justUpdated() 100 | updatedB = self.r_b.justUpdated() 101 | updatedToA = self.r_a.justUpdated(to: 1) 102 | updatedToWrongToA = self.r_a.justUpdated(to: 2) 103 | updatedToB = self.r_b.justUpdated(to: 1) 104 | updatedToFromA = self.r_a.justUpdated(to: 1, from: 0) 105 | updatedToFromWrongToA = self.r_a.justUpdated(to: 2, from: 0) 106 | updatedToFromWrongFromA = self.r_a.justUpdated(to: 1, from: 2) 107 | updatedToFromB = self.r_b.justUpdated(to: 1, from: 0) 108 | updatedFromA = self.r_a.justUpdated(from: 0) 109 | updatedFromWrongFromA = self.r_a.justUpdated(from: 2) 110 | updatedFromB = self.r_b.justUpdated(from: 0) 111 | } 112 | ext = BGExtent(builder: bld) 113 | ext.addToGraphWithAction() 114 | 115 | // |> When r_a updates 116 | r_a.updateWithAction(1) 117 | 118 | // |> Then updates are tracked inside behavior 119 | XCTAssertEqual(updatedA, true) 120 | XCTAssertEqual(r_a.justUpdated(), false) // false outside event 121 | XCTAssertEqual(updatedB, false) // not updated 122 | XCTAssertEqual(updatedToA, true) 123 | XCTAssertEqual(r_a.justUpdated(to: 1), false) 124 | XCTAssertEqual(updatedToWrongToA, false) 125 | XCTAssertEqual(updatedToB, false) 126 | XCTAssertEqual(updatedToFromA, true) 127 | XCTAssertEqual(r_a.justUpdated(to: 1, from: 0), false) 128 | XCTAssertEqual(updatedToFromWrongToA, false) 129 | XCTAssertEqual(updatedToFromWrongFromA, false) 130 | XCTAssertEqual(updatedToFromB, false) 131 | XCTAssertEqual(updatedFromA, true) 132 | XCTAssertEqual(r_a.justUpdated(from: 0), false) 133 | XCTAssertEqual(updatedFromWrongFromA, false) 134 | XCTAssertEqual(updatedFromB, false) 135 | } 136 | 137 | func testCanAccessTraceValuesAndEvents() { 138 | var beforeValue: Int? = nil 139 | var beforeEvent: BGEvent? = nil 140 | 141 | // |> Given a behavior that accesses trace 142 | bld.behavior().demands([r_a]).runs { extent in 143 | beforeValue = self.r_a.traceValue 144 | beforeEvent = self.r_a.traceEvent 145 | } 146 | ext = BGExtent(builder: bld) 147 | ext.addToGraphWithAction() 148 | 149 | // |> When resource is updated 150 | r_a.updateWithAction(1) 151 | 152 | // |> Trace captures original state during 153 | XCTAssertEqual(beforeValue, 0) 154 | XCTAssertEqual(beforeEvent, BGEvent.unknownPast) 155 | // and current state outside event 156 | XCTAssertEqual(r_a.traceValue, 1) 157 | XCTAssertEqual(r_a.traceEvent, g.lastEvent) 158 | } 159 | 160 | func testCanUpdateResourceDuringSameEventAsAdding() { 161 | // @SAL this doesn't work yet, can't add and update in the same event 162 | var didRun = false 163 | bld.behavior().demands([r_a]).runs { extent in 164 | didRun = true 165 | } 166 | ext = BGExtent(builder: bld) 167 | 168 | self.g.action { 169 | self.r_a.update(1) 170 | self.ext.addToGraph() 171 | } 172 | 173 | XCTAssertEqual(r_a.value, 1) 174 | XCTAssertEqual(didRun, true) 175 | } 176 | 177 | // @SAL 8/16/2025 -- these quickspec tests werent running under xcodebuild, so this 178 | // wasn't failing. Moving to spm tests caused it to run and fail. 179 | // An ability to update a resource before extent is added to graph may be a desired 180 | // behavior, but it is not currently supported. 181 | // it("can update resource before extent is added to graph") { 182 | // //ext = BGExtent(builder: bld) 183 | // 184 | // r_a.update(1) 185 | // 186 | // expect(ext.status) == .inactive 187 | // expect(r_a.value) == 1 188 | // } 189 | 190 | func testCanUpdateStateBeforeAdding() { 191 | // |> Given a state resource that hasn't been added 192 | let r = bld.state(0) 193 | ext = BGExtent(builder: bld) 194 | 195 | // |> When we update 196 | r.setInitialValue(1) 197 | // |> Then it's value is the new initial value 198 | XCTAssertEqual(r.value, 1) 199 | XCTAssertEqual(r._prevValue, 1) 200 | } 201 | 202 | func testCanUpdateStateBeforeBuildingExtent() { 203 | // |> Given a state resource that hasn't been added 204 | let r = bld.state(0) 205 | 206 | // |> When we update 207 | r.setInitialValue(1) 208 | // |> Then it's value is the new initial value 209 | XCTAssertEqual(r.value, 1) 210 | XCTAssertEqual(r._prevValue, 1) 211 | } 212 | 213 | func testCannotSetInitialValueAfterAdding() { 214 | // |> Given a state that has been added 215 | let r = bld.state(0) 216 | ext = BGExtent(builder: bld) 217 | ext.addToGraphWithAction() 218 | 219 | // |> when we try to set initial value 220 | // |> then it will assert 221 | TestAssertionHit(graph: g) { 222 | r.setInitialValue(1) 223 | } 224 | } 225 | 226 | func testEnsuresCanUpdateChecksAreRun() { 227 | // NOTE: there are multiple canUpdate checks, this just ensures that 228 | // code path is followed 229 | bld.behavior().demands([r_a]).runs { extent in 230 | self.r_b.update(self.r_a.value) 231 | } 232 | ext = BGExtent(builder: bld) 233 | ext.addToGraphWithAction() 234 | 235 | TestAssertionHit(graph: g) { 236 | r_a.updateWithAction(1) 237 | } 238 | } 239 | 240 | } 241 | 242 | fileprivate struct Struct {} 243 | fileprivate class Object {} 244 | 245 | fileprivate class EquatableObject: Equatable { 246 | var failEquality = false 247 | 248 | static func == (lhs: EquatableObject, rhs: EquatableObject) -> Bool { 249 | !lhs.failEquality && !rhs.failEquality 250 | } 251 | } 252 | 253 | class BGStateUpdateTests: XCTestCase { 254 | 255 | let g = BGGraph() 256 | var b: BGExtentBuilder! 257 | 258 | override func setUp() { 259 | b = BGExtentBuilder(graph: g) 260 | } 261 | 262 | override func tearDown() { 263 | g.debugOnAssertionFailure = nil 264 | } 265 | 266 | // MARK: Non-Equatable, Non-Object 267 | 268 | func testNonEquatableStruct_default() { 269 | // default is .none 270 | 271 | let s = b.state(Struct()) 272 | 273 | let extent = BGExtent(builder: b) 274 | 275 | self.g.action { 276 | extent.addToGraph() 277 | 278 | s.update(s.value) 279 | XCTAssertTrue(s.justUpdated()) 280 | } 281 | } 282 | 283 | func testNonEquatableStruct_noCheck() { 284 | let s = b.state(Struct(), comparison: .none) 285 | 286 | let extent = BGExtent(builder: b) 287 | 288 | self.g.action { 289 | extent.addToGraph() 290 | 291 | s.update(s.value) 292 | XCTAssertTrue(s.justUpdated()) 293 | } 294 | } 295 | 296 | // MARK: Equatable, Non-Object 297 | 298 | func testEquatable_default() { 299 | // default is .equal 300 | 301 | let s = b.state("foo") 302 | 303 | let extent = BGExtent(builder: b) 304 | 305 | self.g.action { 306 | extent.addToGraph() 307 | 308 | s.update("foo") 309 | XCTAssertFalse(s.justUpdated()) 310 | } 311 | 312 | self.g.action { 313 | s.update("bar") 314 | XCTAssertTrue(s.justUpdated()) 315 | } 316 | } 317 | 318 | func testEquatable_noCheck() { 319 | let s = b.state("foo", comparison: .none) 320 | 321 | let extent = BGExtent(builder: b) 322 | 323 | self.g.action { 324 | extent.addToGraph() 325 | 326 | s.update(s.value) 327 | XCTAssertTrue(s.justUpdated()) 328 | } 329 | } 330 | 331 | func testEquatable_equals() { 332 | let s = b.state("foo", comparison: .equal) 333 | 334 | let extent = BGExtent(builder: b) 335 | 336 | self.g.action { 337 | extent.addToGraph() 338 | 339 | // s.valueEquality("foo", equalityCheck: .equal) 340 | s.update("foo") 341 | XCTAssertFalse(s.justUpdated()) 342 | } 343 | 344 | self.g.action { 345 | s.update("bar") 346 | XCTAssertTrue(s.justUpdated()) 347 | } 348 | } 349 | 350 | func testEquatable_forced() { 351 | let s = b.state("foo", comparison: .equal) 352 | 353 | let extent = BGExtent(builder: b) 354 | 355 | self.g.action { 356 | extent.addToGraph() 357 | 358 | s.update("foo") 359 | XCTAssertFalse(s.justUpdated()) 360 | } 361 | 362 | self.g.action { 363 | s.update("foo", forced: true) 364 | XCTAssertTrue(s.justUpdated()) 365 | } 366 | } 367 | 368 | // MARK: Non-Equatable, Object 369 | 370 | func testObject_default() { 371 | // default is .identical 372 | 373 | let s = b.state(Object()) 374 | 375 | let extent = BGExtent(builder: b) 376 | 377 | self.g.action { 378 | extent.addToGraph() 379 | 380 | s.update(s.value) 381 | XCTAssertFalse(s.justUpdated()) 382 | } 383 | 384 | self.g.action { 385 | s.update(Object()) 386 | XCTAssertTrue(s.justUpdated()) 387 | } 388 | } 389 | 390 | func testObject_noCheck() { 391 | let s = b.state(Object(), comparison: .none) 392 | 393 | let extent = BGExtent(builder: b) 394 | 395 | self.g.action { 396 | extent.addToGraph() 397 | 398 | s.update(s.value) 399 | XCTAssertTrue(s.justUpdated()) 400 | } 401 | } 402 | 403 | func testObject_indentical() { 404 | let s = b.state(Object(), comparison: .identical) 405 | 406 | let extent = BGExtent(builder: b) 407 | 408 | self.g.action { 409 | extent.addToGraph() 410 | 411 | s.update(s.value) 412 | XCTAssertFalse(s.justUpdated()) 413 | } 414 | 415 | self.g.action { 416 | s.update(Object()) 417 | XCTAssertTrue(s.justUpdated()) 418 | } 419 | } 420 | 421 | // MARK: Equatable, Object 422 | 423 | func testEquatableObject_default() { 424 | // default is .equal 425 | 426 | let obj = EquatableObject() 427 | 428 | let s = b.state(obj) 429 | 430 | let extent = BGExtent(builder: b) 431 | 432 | self.g.action { 433 | extent.addToGraph() 434 | 435 | s.update(obj) 436 | XCTAssertFalse(s.justUpdated()) 437 | } 438 | 439 | obj.failEquality = true 440 | self.g.action { 441 | s.update(obj) 442 | XCTAssertTrue(s.justUpdated()) 443 | } 444 | } 445 | 446 | func testEquatableObject_noCheck() { 447 | let s = b.state(EquatableObject(), comparison: .none) 448 | 449 | let extent = BGExtent(builder: b) 450 | 451 | self.g.action { 452 | extent.addToGraph() 453 | 454 | s.update(s.value) 455 | XCTAssertTrue(s.justUpdated()) 456 | } 457 | } 458 | 459 | func testEquatableObject_equal() { 460 | let obj = EquatableObject() 461 | 462 | let s = b.state(obj, comparison: .equal) 463 | 464 | let extent = BGExtent(builder: b) 465 | 466 | self.g.action { 467 | extent.addToGraph() 468 | 469 | s.update(obj) 470 | XCTAssertFalse(s.justUpdated()) 471 | } 472 | 473 | obj.failEquality = true 474 | self.g.action { 475 | s.update(obj) 476 | XCTAssertTrue(s.justUpdated()) 477 | } 478 | } 479 | 480 | func testEquatableObject_indentical() { 481 | let s = b.state(EquatableObject(), comparison: .identical) 482 | 483 | let extent = BGExtent(builder: b) 484 | 485 | self.g.action { 486 | extent.addToGraph() 487 | 488 | s.update(s.value) 489 | XCTAssertFalse(s.justUpdated()) 490 | } 491 | 492 | self.g.action { 493 | s.update(EquatableObject()) 494 | XCTAssertTrue(s.justUpdated()) 495 | } 496 | } 497 | 498 | func testEquatableObject_forced() { 499 | let obj = EquatableObject() 500 | obj.failEquality = false 501 | 502 | let s = b.state(obj, comparison: .equal) 503 | 504 | let extent = BGExtent(builder: b) 505 | 506 | self.g.action { 507 | extent.addToGraph() 508 | 509 | s.update(obj) 510 | XCTAssertFalse(s.justUpdated()) 511 | } 512 | 513 | self.g.action { 514 | s.update(obj, forced: true) 515 | XCTAssertTrue(s.justUpdated()) 516 | } 517 | } 518 | 519 | // MARK: Optional, Non-Equatable, Object 520 | 521 | func testOptionalObject_default() { 522 | // default is .identical 523 | 524 | let s: BGState = b.state(Object()) 525 | 526 | let extent = BGExtent(builder: b) 527 | 528 | self.g.action { 529 | extent.addToGraph() 530 | 531 | s.update(s.value) 532 | XCTAssertFalse(s.justUpdated()) 533 | } 534 | 535 | self.g.action { 536 | s.update(Object()) 537 | XCTAssertTrue(s.justUpdated()) 538 | } 539 | } 540 | 541 | func testOptionalObject_noCheck() { 542 | let s: BGState = b.state(Object(), comparison: .none) 543 | 544 | let extent = BGExtent(builder: b) 545 | 546 | self.g.action { 547 | extent.addToGraph() 548 | 549 | s.update(s.value) 550 | XCTAssertTrue(s.justUpdated()) 551 | } 552 | } 553 | 554 | func testOptionalObject_indentical() { 555 | let s: BGState = b.state(Object(), comparison: .identical) 556 | 557 | let extent = BGExtent(builder: b) 558 | 559 | self.g.action { 560 | extent.addToGraph() 561 | 562 | s.update(s.value) 563 | XCTAssertFalse(s.justUpdated()) 564 | } 565 | 566 | self.g.action { 567 | s.update(Object()) 568 | XCTAssertTrue(s.justUpdated()) 569 | } 570 | } 571 | 572 | // MARK: Optional Equatable, Object 573 | 574 | func testOptionalEquatableObject_default() { 575 | let obj = EquatableObject() 576 | 577 | let s: BGState = b.state(EquatableObject()) 578 | 579 | let extent = BGExtent(builder: b) 580 | 581 | self.g.action { 582 | extent.addToGraph() 583 | 584 | s.update(obj) 585 | XCTAssertFalse(s.justUpdated()) 586 | } 587 | 588 | obj.failEquality = true 589 | 590 | self.g.action { 591 | s.update(obj) 592 | XCTAssertTrue(s.justUpdated()) 593 | } 594 | } 595 | 596 | func testOptionalEquatableObject_noCheck() { 597 | let s: BGState = b.state(EquatableObject(), comparison: .none) 598 | 599 | let extent = BGExtent(builder: b) 600 | 601 | self.g.action { 602 | extent.addToGraph() 603 | 604 | s.update(s.value) 605 | XCTAssertTrue(s.justUpdated()) 606 | } 607 | } 608 | 609 | func testOptionalEquatableObject_equal() { 610 | let obj = EquatableObject() 611 | 612 | let s: BGState = b.state(EquatableObject(), comparison: .equal) 613 | 614 | let extent = BGExtent(builder: b) 615 | 616 | self.g.action { 617 | extent.addToGraph() 618 | 619 | s.update(obj) 620 | XCTAssertFalse(s.justUpdated()) 621 | } 622 | 623 | obj.failEquality = true 624 | 625 | self.g.action { 626 | s.update(obj) 627 | XCTAssertTrue(s.justUpdated()) 628 | } 629 | } 630 | 631 | func testOptionalEquatableObject_indentical() { 632 | let obj1 = EquatableObject() 633 | obj1.failEquality = false 634 | 635 | let obj2 = EquatableObject() 636 | obj2.failEquality = false 637 | 638 | let s: BGState = b.state(obj1, comparison: .identical) 639 | 640 | let extent = BGExtent(builder: b) 641 | 642 | self.g.action { 643 | extent.addToGraph() 644 | 645 | s.update(obj1) 646 | XCTAssertFalse(s.justUpdated()) 647 | } 648 | 649 | self.g.action { 650 | s.update(obj2) 651 | XCTAssertTrue(s.justUpdated()) 652 | } 653 | } 654 | 655 | func testPreviousValueIsNotRetainedAfterAction() { 656 | class ClassType {} 657 | 658 | weak var weakValue: ClassType? 659 | let state: BGState = b.state(ClassType()) 660 | let extent = BGExtent(builder: b) 661 | extent.addToGraphWithAction() 662 | 663 | weakValue = state.value 664 | XCTAssertNotNil(weakValue) 665 | 666 | autoreleasepool { 667 | state.updateWithAction(ClassType()) 668 | } 669 | 670 | XCTAssertNil(weakValue) 671 | } 672 | } 673 | -------------------------------------------------------------------------------- /BGSwift/Classes/BGExtent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import Foundation 6 | 7 | open class BGExtent: Hashable, CustomDebugStringConvertible { 8 | public let graph: BGGraph 9 | 10 | var resources = [any BGResourceInternal]() 11 | var behaviors = [BGBehavior]() 12 | /// Return snapshot of all the resources on the extent. 13 | public var allResources: [any BGResource] { resources } 14 | /// Return snapshot of all the behaviors on the extent. 15 | public var allBehaviors: [BGBehavior] { behaviors } 16 | 17 | var _added: BGMoment 18 | public var added: BGResource { _added } 19 | var _mirror: Mirror? 20 | 21 | public enum Status { 22 | case inactive 23 | case added 24 | case removed 25 | } 26 | public var status: Status = .inactive 27 | 28 | public var debugName: String? 29 | 30 | public init(builder: BGExtentBuilderGeneric) { 31 | builder.graph.assert(builder.extent == nil) 32 | 33 | graph = builder.graph 34 | _added = builder._added 35 | 36 | builder.extent = self 37 | 38 | self.addComponents(from: builder) 39 | 40 | } 41 | 42 | deinit { 43 | guard status == .added else { 44 | return 45 | } 46 | status = .removed 47 | 48 | let resources = resources 49 | let behaviors = behaviors 50 | let graph = graph 51 | graph.action { [weak graph] in 52 | graph?.removeExtent(resources: resources, behaviors: behaviors) 53 | } 54 | } 55 | 56 | func addComponents(from builder: BGExtentBuilderGeneric) { 57 | guard builder.graph === graph, 58 | status == .inactive 59 | else { 60 | graph.assertionFailure() 61 | return 62 | } 63 | 64 | builder.resources.forEach { 65 | $0.owner = self 66 | } 67 | 68 | builder.behaviors.forEach { 69 | $0.owner = self 70 | } 71 | 72 | resources.append(contentsOf: builder.resources) 73 | behaviors.append(contentsOf: builder.behaviors) 74 | 75 | builder.resources.removeAll() 76 | builder.behaviors.removeAll() 77 | } 78 | 79 | open func addToGraph() { 80 | graph.addExtent(self) 81 | } 82 | 83 | public func addToGraphWithAction() { 84 | graph.action { 85 | self.addToGraph() 86 | } 87 | } 88 | 89 | open func removeFromGraph() { 90 | guard status == .added else { 91 | // assertionFailure("Extents can only be removed once after adding.") 92 | return 93 | } 94 | 95 | guard graph.processingChanges else { 96 | graph.assertionFailure("Can only remove behaviors during an event.") 97 | return 98 | } 99 | 100 | status = .removed 101 | 102 | let resources = resources 103 | let behaviors = behaviors 104 | self.resources.removeAll() 105 | self.behaviors.removeAll() 106 | 107 | graph.removeExtent(resources: resources, behaviors: behaviors) 108 | } 109 | 110 | internal func loadDebugNames() { 111 | if (_mirror == nil) { 112 | _mirror = Mirror(reflecting: self) 113 | while let mirror = _mirror, mirror.subjectType is BGExtent.Type { 114 | mirror.children.forEach { child in 115 | if let resource = child.value as? (any BGResourceInternal) { 116 | if resource.debugName == nil { 117 | resource.debugName = "\(String(describing: Self.self)).\(child.label ?? "Anonymous_Resource")" 118 | } 119 | } else if let behavior = child.value as? BGBehavior { 120 | if behavior.debugName == nil { 121 | behavior.debugName = "\(String(describing: Self.self)).\(child.label ?? "Anonymous_Behavior")" 122 | } 123 | } 124 | } 125 | _mirror = mirror.superclassMirror 126 | } 127 | } 128 | } 129 | 130 | public func sideEffect(file: String = #fileID, line: Int = #line, function: String = #function, _ body: @escaping () -> Void) { 131 | let impulse = BGGraph.impulseString(file: file, line: line, function: function) 132 | sideEffect(impulse, body) 133 | } 134 | 135 | public func sideEffect(_ label: String?, _ body: @escaping () -> Void) { 136 | graph.sideEffect(label, body: body) 137 | } 138 | 139 | // MARK: CustomDebugStringConvertible 140 | 141 | public var debugDescription: String { 142 | "<\(String(describing: Self.self)):\(String(format: "%018p", unsafeBitCast(self, to: Int64.self))) (\(debugName ?? "Unlabeled"))>" 143 | } 144 | 145 | // MARK: Equatable 146 | 147 | public static func == (lhs: BGExtent, rhs: BGExtent) -> Bool { lhs === rhs } 148 | 149 | // MARK: Hashable 150 | 151 | public func hash(into hasher: inout Hasher) { 152 | hasher.combine(ObjectIdentifier(self)) 153 | } 154 | } 155 | 156 | public class BGExtentBuilderGeneric: NSObject { 157 | var resources = [any BGResourceInternal]() 158 | var behaviors = [BGBehavior]() 159 | let graph: BGGraph 160 | let _added: BGMoment 161 | public var added: BGResource { _added } 162 | 163 | var extent: BGExtent? 164 | 165 | init(graph: BGGraph) { 166 | self.graph = graph 167 | 168 | let added = BGMoment() 169 | self._added = added 170 | resources.append(added) 171 | } 172 | 173 | init(extent: BGExtent) { 174 | self.graph = extent.graph 175 | self._added = extent._added 176 | self.extent = extent 177 | } 178 | 179 | public func moment() -> BGMoment { 180 | let moment = BGMoment() 181 | 182 | if let extent = extent { 183 | graph.assert(extent.status == .inactive) 184 | moment.owner = extent 185 | extent.resources.append(moment) 186 | } else { 187 | resources.append(moment) 188 | } 189 | 190 | return moment 191 | } 192 | 193 | public func typedMoment() -> BGTypedMoment { 194 | let moment = BGTypedMoment() 195 | 196 | if let extent = extent { 197 | graph.assert(extent.status == .inactive) 198 | moment.owner = extent 199 | extent.resources.append(moment) 200 | } else { 201 | resources.append(moment) 202 | } 203 | 204 | return moment 205 | } 206 | 207 | public func state(_ value: T, comparison: @escaping (_ lhs: T, _ rhs: T) -> Bool) -> BGState { 208 | let state = BGState(value, comparison: comparison) 209 | 210 | if let extent = extent { 211 | graph.assert(extent.status == .inactive) 212 | state.owner = extent 213 | extent.resources.append(state) 214 | } else { 215 | resources.append(state) 216 | } 217 | 218 | return state 219 | } 220 | 221 | @_disfavoredOverload 222 | public func state(_ value: T, comparison: BGStateComparison.None = .none) -> BGState { 223 | return state(value) { _, _ in 224 | false 225 | } 226 | } 227 | 228 | public func state(_ value: T, comparison: BGStateComparison.Equal = .equal) -> BGState { 229 | return state(value) { lhs, rhs in 230 | lhs == rhs 231 | } 232 | } 233 | 234 | public func state(_ value: T, comparison: BGStateComparison.Identical = .identical) -> BGState { 235 | return state(value) { lhs, rhs in 236 | lhs === rhs 237 | } 238 | } 239 | 240 | @_disfavoredOverload 241 | public func state(_ value: T, comparison: BGStateComparison.Identical = .identical) -> BGState { 242 | return state(value) { lhs, rhs in 243 | lhs.bg_unwrapped === rhs.bg_unwrapped 244 | } 245 | } 246 | 247 | public func generic_behavior() -> BGBehaviorBuilderGeneric { BGBehaviorBuilderGeneric(self) } 248 | 249 | func behavior(supplies mainBehaviorStaticSupplies: [BGResource], 250 | demands mainBehaviorStaticDemands: [BGDemandable], 251 | preDynamicSupplies: BGDynamicSupplyBuilderGeneric?, 252 | postDynamicSupplies: BGDynamicSupplyBuilderGeneric?, 253 | preDynamicDemands: BGDynamicDemandBuilderGeneric?, 254 | postDynamicDemands: BGDynamicDemandBuilderGeneric?, 255 | requiresMainThread: Bool, 256 | body: @escaping (_ extent: Extent) -> Void) -> BGBehavior { 257 | weak var weakMainBehavior: BGBehavior? 258 | 259 | var mainBehaviorStaticSupplies = mainBehaviorStaticSupplies 260 | var mainBehaviorStaticDemands = mainBehaviorStaticDemands 261 | 262 | if postDynamicSupplies != nil || postDynamicDemands != nil { 263 | let orderingResource = moment() 264 | orderingResource.debugName = "DynamicPostOrdering" 265 | mainBehaviorStaticSupplies.append(orderingResource) 266 | 267 | if let postDynamicSupplies = postDynamicSupplies { 268 | var demands = postDynamicSupplies.demands 269 | demands.append(orderingResource) 270 | let dynamicDemands = postDynamicSupplies._dynamicDemands 271 | let resolver = postDynamicSupplies.resolver 272 | generic_behavior() 273 | .demands(demands) 274 | .generic_dynamicDemands(dynamicDemands) 275 | .generic_runs { extent in 276 | guard let mainBehavior = weakMainBehavior else { 277 | return 278 | } 279 | mainBehavior.setDynamicSupplies(resolver(extent).compactMap { $0 }) 280 | } 281 | } 282 | 283 | if let postDynamicDemands = postDynamicDemands { 284 | var demands = postDynamicDemands.demands 285 | demands.append(orderingResource) 286 | let dynamicDemands = postDynamicDemands._dynamicDemands 287 | let resolver = postDynamicDemands.resolver 288 | generic_behavior() 289 | .demands(demands) 290 | .generic_dynamicDemands(dynamicDemands) 291 | .generic_runs { extent in 292 | guard let mainBehavior = weakMainBehavior else { 293 | return 294 | } 295 | mainBehavior.setDynamicDemands(resolver(extent).compactMap { $0 }) 296 | } 297 | } 298 | } 299 | 300 | if let preDynamicSupplies = preDynamicSupplies { 301 | let orderingResource = moment() 302 | orderingResource.debugName = "DynamicSuppliesPreOrdering" 303 | mainBehaviorStaticDemands.append(orderingResource) 304 | 305 | let supplies = [orderingResource] 306 | let demands = preDynamicSupplies.demands 307 | let dynamicDemands = preDynamicSupplies._dynamicDemands 308 | let resolver = preDynamicSupplies.resolver 309 | generic_behavior() 310 | .supplies(supplies) 311 | .demands(demands) 312 | .generic_dynamicDemands(dynamicDemands) 313 | .generic_runs { extent in 314 | guard let mainBehavior = weakMainBehavior else { 315 | return 316 | } 317 | mainBehavior.setDynamicSupplies(resolver(extent).compactMap { $0 }) 318 | } 319 | } 320 | 321 | if let preDynamicDemands = preDynamicDemands { 322 | let orderingResource = moment() 323 | orderingResource.debugName = "DynamicDemandsPreOrdering" 324 | mainBehaviorStaticDemands.append(orderingResource) 325 | 326 | let supplies = [orderingResource] 327 | let demands = preDynamicDemands.demands 328 | let dynamicDemands = preDynamicDemands._dynamicDemands 329 | let resolver = preDynamicDemands.resolver 330 | generic_behavior() 331 | .supplies(supplies) 332 | .demands(demands) 333 | .generic_dynamicDemands(dynamicDemands) 334 | .generic_runs { extent in 335 | guard let mainBehavior = weakMainBehavior else { 336 | return 337 | } 338 | mainBehavior.setDynamicDemands(resolver(extent).compactMap { $0 }) 339 | } 340 | } 341 | 342 | let mainBehavior = BGBehavior(graph: graph, 343 | supplies: mainBehaviorStaticSupplies, 344 | demands: mainBehaviorStaticDemands, 345 | requiresMainThread: requiresMainThread) { extent in 346 | body(extent as! Extent) 347 | } 348 | weakMainBehavior = mainBehavior 349 | 350 | if let extent = extent { 351 | graph.assert(extent.status == .inactive) 352 | mainBehavior.owner = extent 353 | extent.behaviors.append(mainBehavior) 354 | } else { 355 | behaviors.append(mainBehavior) 356 | } 357 | 358 | return mainBehavior 359 | } 360 | } 361 | 362 | public class BGExtentBuilder: BGExtentBuilderGeneric { 363 | public override init(graph: BGGraph) { 364 | super.init(graph: graph) 365 | } 366 | 367 | public override init(extent: BGExtent) { 368 | super.init(extent: extent) 369 | } 370 | 371 | public func behavior() -> BGBehaviorBuilder { BGBehaviorBuilder(self) } 372 | } 373 | 374 | public class BGParameterizedExtentBuilder: BGExtentBuilderGeneric { 375 | let paramsBlock: (_ extent: Extent) -> Params? 376 | 377 | public init(graph: BGGraph, _ paramsBlock: @escaping (_ extent: Extent) -> Params?) { 378 | self.paramsBlock = paramsBlock 379 | super.init(graph: graph) 380 | } 381 | 382 | public init(extent: Extent, _ paramsBlock: @escaping (_ extent: Extent) -> Params?) { 383 | self.paramsBlock = paramsBlock 384 | super.init(extent: extent) 385 | } 386 | 387 | public func behavior() -> BGParameterizedBehaviorBuilder { BGParameterizedBehaviorBuilder(self) } 388 | } 389 | 390 | public class BGBehaviorBuilderGeneric: NSObject { 391 | let builder: BGExtentBuilderGeneric 392 | 393 | var _supplies = [BGResource]() 394 | var _demands = [BGDemandable]() 395 | var _preDynamicSupplies: BGDynamicSupplyBuilderGeneric? 396 | var _postDynamicSupplies: BGDynamicSupplyBuilderGeneric? 397 | var _preDynamicDemands: BGDynamicDemandBuilderGeneric? 398 | var _postDynamicDemands: BGDynamicDemandBuilderGeneric? 399 | var _requiresMainThread: Bool = false 400 | 401 | public init(_ builder: BGExtentBuilderGeneric) { 402 | self.builder = builder 403 | } 404 | 405 | @discardableResult 406 | public func supplies(_ supplies: [BGResource]) -> Self { 407 | _supplies = supplies 408 | return self 409 | } 410 | 411 | @discardableResult 412 | public func supplies(_ supplies: BGResource...) -> Self { 413 | self.supplies(supplies as [BGResource]) 414 | return self 415 | } 416 | 417 | @discardableResult 418 | public func demands(_ demands: [BGDemandable]) -> Self { 419 | _demands = demands 420 | return self 421 | } 422 | 423 | @discardableResult 424 | public func demands(_ demands: BGDemandable...) -> Self { 425 | self.demands(demands as [BGDemandable]) 426 | } 427 | 428 | @discardableResult 429 | public func requiresMainThread(_ requiresMainThread: Bool = true) -> Self { 430 | self._requiresMainThread = requiresMainThread 431 | return self 432 | } 433 | 434 | @discardableResult 435 | func generic_dynamicSupplies(_ dynamicSupplies: BGDynamicSupplyBuilderGeneric?) -> BGBehaviorBuilderGeneric { 436 | if let dynamicSupplies = dynamicSupplies { 437 | switch dynamicSupplies.order { 438 | case .pre: 439 | _preDynamicSupplies = dynamicSupplies 440 | case .post: 441 | _postDynamicSupplies = dynamicSupplies 442 | } 443 | } 444 | return self 445 | } 446 | 447 | @discardableResult 448 | func generic_dynamicDemands(_ dynamicDemands: BGDynamicDemandBuilderGeneric?) -> BGBehaviorBuilderGeneric { 449 | if let dynamicDemands = dynamicDemands { 450 | switch dynamicDemands.order { 451 | case .pre: 452 | _preDynamicDemands = dynamicDemands 453 | case .post: 454 | _postDynamicDemands = dynamicDemands 455 | } 456 | } 457 | return self 458 | } 459 | 460 | @discardableResult 461 | func generic_runs(_ body: @escaping (_ extent: BGExtent) -> Void) -> BGBehavior { 462 | builder.behavior(supplies: _supplies, 463 | demands: _demands, 464 | preDynamicSupplies: _preDynamicSupplies, 465 | postDynamicSupplies: _postDynamicSupplies, 466 | preDynamicDemands: _preDynamicDemands, 467 | postDynamicDemands: _postDynamicDemands, 468 | requiresMainThread: _requiresMainThread, 469 | body: body) 470 | } 471 | } 472 | 473 | public class BGBehaviorBuilder : BGBehaviorBuilderGeneric { 474 | 475 | @discardableResult 476 | public func dynamicSupplies(_ dynamicSupplies: BGDynamicSupplyBuilder) -> Self { 477 | generic_dynamicSupplies(dynamicSupplies) 478 | return self 479 | } 480 | 481 | @discardableResult 482 | public func dynamicDemands(_ dynamicDemands: BGDynamicDemandBuilder) -> Self { 483 | generic_dynamicDemands(dynamicDemands) 484 | return self 485 | } 486 | 487 | @discardableResult 488 | public func runs(_ body: @escaping (_ extent: Extent) -> Void) -> BGBehavior { 489 | builder.behavior(supplies: _supplies, 490 | demands: _demands, 491 | preDynamicSupplies: _preDynamicSupplies, 492 | postDynamicSupplies: _postDynamicSupplies, 493 | preDynamicDemands: _preDynamicDemands, 494 | postDynamicDemands: _postDynamicDemands, 495 | requiresMainThread: _requiresMainThread) { extent in 496 | body(extent as! Extent) 497 | } 498 | } 499 | } 500 | 501 | public class BGParameterizedBehaviorBuilder : BGBehaviorBuilderGeneric { 502 | let paramsBlock: (_ extent: Extent) -> Params? 503 | public init(_ builder: BGParameterizedExtentBuilder) { 504 | paramsBlock = builder.paramsBlock 505 | super.init(builder) 506 | } 507 | 508 | @discardableResult 509 | public func dynamicSupplies(_ dynamicSupplies: BGDynamicSupplyBuilder) -> Self { 510 | generic_dynamicSupplies(dynamicSupplies) 511 | return self 512 | } 513 | 514 | @discardableResult 515 | public func dynamicDemands(_ dynamicDemands: BGDynamicDemandBuilder) -> Self { 516 | generic_dynamicDemands(dynamicDemands) 517 | return self 518 | } 519 | 520 | @discardableResult 521 | public func runs(_ body: @escaping (_ p: Params) -> Void) -> BGBehavior { 522 | let paramsBlock = self.paramsBlock 523 | return builder.behavior(supplies: _supplies, 524 | demands: _demands, 525 | preDynamicSupplies: _preDynamicSupplies, 526 | postDynamicSupplies: _postDynamicSupplies, 527 | preDynamicDemands: _preDynamicDemands, 528 | postDynamicDemands: _postDynamicDemands, 529 | requiresMainThread: _requiresMainThread) { extent in 530 | if let params = paramsBlock(extent as! Extent) { 531 | body(params) 532 | } 533 | } 534 | } 535 | } 536 | 537 | public enum BGDynamicsOrderingType: Int { 538 | case pre 539 | case post 540 | } 541 | 542 | public class BGDynamicSupplyBuilderGeneric { 543 | let order: BGDynamicsOrderingType 544 | let demands: [BGDemandable] 545 | var _dynamicDemands: BGDynamicDemandBuilderGeneric? 546 | let resolver: (_ extent: BGExtent) -> [BGResource?] 547 | 548 | init(_ order: BGDynamicsOrderingType, 549 | demands: [BGDemandable], 550 | _ resolver: @escaping (_ extent: BGExtent) -> [BGResource?]) { 551 | self.order = order 552 | self.demands = demands 553 | self.resolver = resolver 554 | } 555 | 556 | @discardableResult 557 | func generic_withDynamicDemands(_ dynamicDemands: BGDynamicDemandBuilderGeneric?) -> BGDynamicSupplyBuilderGeneric { 558 | _dynamicDemands = dynamicDemands 559 | return self 560 | } 561 | } 562 | 563 | public class BGDynamicSupplyBuilder: BGDynamicSupplyBuilderGeneric { 564 | public init(_ order: BGDynamicsOrderingType, 565 | demands: [BGDemandable], 566 | _ resolver: @escaping (_ extent: Extent) -> [BGResource?]) { 567 | super.init(order, demands: demands) { extent in 568 | resolver(extent as! Extent) 569 | } 570 | } 571 | 572 | public func withDynamicDemands(_ dynamicDemands: BGDynamicDemandBuilder?) -> BGDynamicSupplyBuilder { 573 | self.generic_withDynamicDemands(dynamicDemands) 574 | return self 575 | } 576 | } 577 | 578 | public class BGDynamicDemandBuilderGeneric { 579 | let order: BGDynamicsOrderingType 580 | let demands: [BGDemandable] 581 | var _dynamicDemands: BGDynamicDemandBuilderGeneric? 582 | let resolver: (_ extent: BGExtent) -> [BGDemandable?] 583 | 584 | init(_ order: BGDynamicsOrderingType, 585 | demands: [BGDemandable], 586 | _ resolver: @escaping (_ extent: BGExtent) -> [BGDemandable?]) { 587 | self.order = order 588 | self.demands = demands 589 | self.resolver = resolver 590 | } 591 | 592 | @discardableResult 593 | func generic_withDynamicDemands(_ dynamicDemands: BGDynamicDemandBuilderGeneric?) -> BGDynamicDemandBuilderGeneric { 594 | _dynamicDemands = dynamicDemands 595 | return self 596 | } 597 | } 598 | 599 | public class BGDynamicDemandBuilder: BGDynamicDemandBuilderGeneric { 600 | public init(_ order: BGDynamicsOrderingType, 601 | demands: [BGDemandable], 602 | _ resolver: @escaping (_ extent: Extent) -> [BGDemandable?]) { 603 | super.init(order, demands: demands) { extent in 604 | resolver(extent as! Extent) 605 | } 606 | } 607 | 608 | public func withDynamicDemands(_ dynamicDemands: BGDynamicDemandBuilder?) -> BGDynamicDemandBuilder { 609 | generic_withDynamicDemands(dynamicDemands) 610 | return self 611 | } 612 | } 613 | -------------------------------------------------------------------------------- /BGSwift/Classes/BGGraph.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 Yahoo 3 | // 4 | 5 | import Foundation 6 | 7 | public class BGGraph { 8 | public enum SynchronizationStrategy { 9 | case sync 10 | case async(queue: DispatchQueue? = nil) 11 | } 12 | 13 | public var dateProvider: (() -> Date)? 14 | public var onAction: (() -> Void)? 15 | 16 | private let behaviorQueue = PriorityQueue() 17 | private var mainThreadBehaviorsToRun = [BGBehavior]() 18 | private var actionQueue = [BGAction]() 19 | private var eventLoopState: EventLoopState? 20 | private var sequence: UInt = 0 21 | private var deferredRelease = [Any]() 22 | private var sideEffectQueue = [BGSideEffect]() 23 | var updatedTransientResources = [any BGResourceInternal]() 24 | 25 | private let mutex = Mutex(recursive: true) 26 | 27 | private let defaultQueue: DispatchQueue 28 | 29 | var behaviorsWithModifiedSupplies = [BGBehavior]() 30 | var behaviorsWithModifiedDemands = [BGBehavior]() 31 | var updatedResources = [any BGResourceInternal]() 32 | private var untrackedBehaviors = [BGBehavior]() 33 | private var untrackedExtents = [BGExtent]() 34 | private var needsOrdering = [BGBehavior]() 35 | 36 | var currentRunningBehavior: BGBehavior? 37 | var currentThread: Thread? 38 | var lockCount: UInt = 0 39 | 40 | public var checkUndeclaredDemands: Bool = false 41 | 42 | public var currentEvent: BGEvent? { eventLoopState?.event } 43 | 44 | public var debugOnExtentAdded: ((BGExtent) -> Void)? 45 | public var debugOnAssertionFailure: ((@autoclosure () -> String, StaticString, UInt) -> Void)? 46 | 47 | var processingChanges: Bool { 48 | eventLoopState?.processingChanges ?? false 49 | } 50 | 51 | var processingAction: Bool { 52 | eventLoopState?.processingAction ?? false 53 | } 54 | 55 | private var _lastEvent: BGEvent 56 | public var lastEvent: BGEvent { _lastEvent } 57 | 58 | public init() { 59 | self._lastEvent = BGEvent.unknownPast 60 | 61 | defaultQueue = DispatchQueue(label: "BGGraph.default", qos: .userInteractive) 62 | } 63 | 64 | @available(*, deprecated, message: "Use `BGGraph.init()` and `BGGraph.dateProvider` instead.") 65 | public convenience init(dateProvider: @escaping () -> Date = { Date() }) { 66 | self.init() 67 | self.dateProvider = dateProvider 68 | } 69 | 70 | public func action(file: String = #fileID, line: Int = #line, function: String = #function, syncStrategy: SynchronizationStrategy? = nil, body: @escaping (() -> Void)) { 71 | let impulse = BGGraph.impulseString(file: file, line: line, function: function) 72 | action(impulse: impulse, syncStrategy: syncStrategy, body: body) 73 | } 74 | 75 | public func action(impulse: String?, syncStrategy: SynchronizationStrategy? = nil, body: @escaping (() -> Void)) { 76 | switch syncStrategy { 77 | case .sync: 78 | mutex.balancedUnlock { 79 | mutexLocked { 80 | guard !processingAction else { 81 | self.assertionFailure("Nested actions cannot be executed synchronously.") 82 | return 83 | } 84 | guard !processingChanges else { 85 | self.assertionFailure("Actions originating from behavior closures cannot be executed synchronously.") 86 | return 87 | } 88 | 89 | actionQueue.append(BGAction(impulse: impulse, action: body)) 90 | eventLoop() 91 | } 92 | } 93 | case .async(let queue): 94 | (queue ?? defaultQueue).async { 95 | self.action(impulse: impulse, syncStrategy: .sync, body: body) 96 | } 97 | case .none: 98 | if mutex.tryLock() { 99 | mutexLocked { 100 | let action = BGAction(impulse: impulse, action: body) 101 | actionQueue.append(action) 102 | 103 | // Run sync when the graph is idle or when called from a side-effect. 104 | if !processingChanges { 105 | eventLoop() 106 | } 107 | } 108 | mutex.unlock() 109 | } else { 110 | // Cannot acquire the lock, so dispatch async 111 | action(impulse: impulse, syncStrategy: .async(queue: nil), body: body) 112 | } 113 | } 114 | } 115 | 116 | func mutexLocked(_ execute: () -> Void) { 117 | if lockCount == 0 { 118 | currentThread = Thread.current 119 | } 120 | lockCount += 1 121 | 122 | execute() 123 | 124 | lockCount -= 1 125 | if lockCount == 0 { 126 | currentThread = nil 127 | } 128 | } 129 | 130 | public func sideEffect(_ label: String? = nil, body: @escaping () -> Void) { 131 | guard let event = currentEvent else { 132 | self.assertionFailure("Side effects must be created inside actions or behaviors.") 133 | return 134 | } 135 | 136 | let sideEffect = BGSideEffect(label: label, event: event, run: body) 137 | self.sideEffectQueue.append(sideEffect) 138 | } 139 | 140 | private func eventLoop() { 141 | var finished = false 142 | while !finished { 143 | autoreleasepool { 144 | if let eventLoopState = self.eventLoopState { 145 | 146 | if eventLoopState.processingChanges { 147 | 148 | // Continue to process any behaviors before any processing any graph structure changes 149 | if !mainThreadBehaviorsToRun.isEmpty { 150 | executeOnMainFromRunloop { 151 | guard !mainThreadBehaviorsToRun.isEmpty else { 152 | return 153 | } 154 | 155 | let behaviorsToRun = mainThreadBehaviorsToRun 156 | mainThreadBehaviorsToRun.removeAll() 157 | 158 | behaviorsToRun.forEach { 159 | runBehavior($0, sequence: eventLoopState.event.sequence) 160 | } 161 | } 162 | } 163 | 164 | if !untrackedBehaviors.isEmpty { 165 | commitUntrackedBehaviors() 166 | } 167 | 168 | if !behaviorsWithModifiedSupplies.isEmpty { 169 | commitModifiedSupplies() 170 | } 171 | 172 | if !behaviorsWithModifiedDemands.isEmpty { 173 | commitModifiedDemands() 174 | } 175 | 176 | if !needsOrdering.isEmpty { 177 | orderBehaviors() 178 | } 179 | 180 | if !untrackedExtents.isEmpty { 181 | if let debugOnExtentAdded { 182 | for extent in untrackedExtents { 183 | debugOnExtentAdded(extent) 184 | } 185 | } 186 | untrackedExtents.removeAll() 187 | } 188 | 189 | if !updatedResources.isEmpty { 190 | updatedResources.forEach { 191 | for subsequent in $0.subsequents { 192 | switch subsequent.type { 193 | case .reactive: 194 | if let behavior = subsequent.behavior { 195 | submitToQueue(behavior) 196 | } 197 | case .order: 198 | break 199 | } 200 | } 201 | } 202 | updatedResources.removeAll() 203 | } 204 | 205 | if !behaviorQueue.isEmpty { 206 | // We want to run all behaviors of the same order together before processing any changes 207 | // to the graph structure (e.g. behavior, supply/demand changes, added/removed extents, etc) 208 | // to potentially reduce the amount of graph sorts we need to perform 209 | 210 | let order = behaviorQueue.peek().order 211 | var behaviorsToRun = [BGBehavior]() 212 | 213 | while !behaviorQueue.isEmpty, behaviorQueue.peek().order == order { 214 | let behavior = behaviorQueue.pop() 215 | if behavior.requiresMainThread { 216 | mainThreadBehaviorsToRun.append(behavior) 217 | } else { 218 | behaviorsToRun.append(behavior) 219 | } 220 | } 221 | 222 | behaviorsToRun.forEach { 223 | runBehavior($0, sequence: eventLoopState.event.sequence) 224 | } 225 | 226 | return 227 | } 228 | } 229 | 230 | eventLoopState.processingChanges = false 231 | 232 | // clearing transient resources is a side effect because it may cause deallocs 233 | if !updatedTransientResources.isEmpty { 234 | // make copy so we just add side effect once per event loop 235 | var localTransientResources = updatedTransientResources 236 | self.sideEffect { 237 | while !localTransientResources.isEmpty { 238 | localTransientResources.removeFirst().clearTransientValue() 239 | } 240 | } 241 | updatedTransientResources.removeAll() 242 | } 243 | 244 | // releasing deferred objects is a side effect because it may cause deallocs 245 | if !deferredRelease.isEmpty { 246 | // make copy so we just add side effect once per event loop 247 | var localDeferredRelease = deferredRelease 248 | self.sideEffect { 249 | if !localDeferredRelease.isEmpty { 250 | autoreleasepool { 251 | // Temporarily retain values on the stack so that `removeAll()` is completed before any object 252 | // is released to avoid a crash for calling `deferredRelease.isEmpty` above while `deferredRelease` is 253 | // being modified (in the case where object's dealloc triggers a synchronous action) 254 | localDeferredRelease.removeAll() 255 | } 256 | } 257 | } 258 | deferredRelease.removeAll() 259 | } 260 | 261 | if !sideEffectQueue.isEmpty { 262 | executeOnMainFromRunloop { 263 | while !sideEffectQueue.isEmpty { 264 | let sideEffect = sideEffectQueue.removeFirst() 265 | sideEffect.run() 266 | } 267 | } 268 | return 269 | } 270 | 271 | self._lastEvent = eventLoopState.event 272 | 273 | self.eventLoopState = nil 274 | } 275 | 276 | if let action = actionQueue.first { 277 | actionQueue.removeFirst() 278 | 279 | let currentDate = self.dateProvider?() ?? Date() 280 | sequence += 1 281 | let event = BGEvent(sequence: sequence, timestamp: currentDate, impulse: action.impulse) 282 | 283 | let eventLoopState = EventLoopState(event: event) 284 | self.eventLoopState = eventLoopState 285 | 286 | action.action() 287 | onAction?() 288 | eventLoopState.processingAction = false 289 | 290 | // NOTE: We keep the action block around because it may capture capture and retain some external objects 291 | // If it were to go away right after running then that might cause a dealloc to be called as it goes out of scope internal 292 | // to the event loop and thus create a side effect during the update phase. 293 | // So we keep it around until after all updates are processed. 294 | deferredRelease.append(action) 295 | 296 | return 297 | } 298 | 299 | finished = true 300 | } 301 | } 302 | } 303 | 304 | private func runBehavior(_ behavior: BGBehavior, sequence: UInt) { 305 | if behavior.removedSequence != sequence { 306 | behavior.lastUpdateSequence = sequence 307 | 308 | if let extent = behavior.owner { 309 | currentRunningBehavior = behavior 310 | behavior.runBlock(extent) 311 | currentRunningBehavior = nil 312 | } 313 | } 314 | } 315 | 316 | private func commitUntrackedBehaviors() { 317 | for behavior in untrackedBehaviors { 318 | if behavior.uncommittedSupplies { 319 | behaviorsWithModifiedSupplies.append(behavior) 320 | } 321 | 322 | if behavior.uncommittedDemands { 323 | behaviorsWithModifiedDemands.append(behavior) 324 | } 325 | } 326 | untrackedBehaviors.removeAll() 327 | } 328 | 329 | private func commitModifiedSupplies() { 330 | for behavior in behaviorsWithModifiedSupplies { 331 | if behavior.uncommittedSupplies { 332 | let oldSupplies = behavior.supplies 333 | var newSupplies = behavior.staticSupplies.filter { 334 | guard let _ = $0.resource else { 335 | return false 336 | } 337 | return true 338 | } 339 | behavior.uncommittedDynamicSupplies?.forEach { 340 | let supplier = $0.supplier 341 | guard supplier === behavior || supplier == nil else { 342 | self.assertionFailure("Resource is already supplied by a different behavior.") 343 | return 344 | } 345 | newSupplies.insert($0.weakReference) 346 | } 347 | behavior.supplies = newSupplies 348 | 349 | let removedSupplies = oldSupplies.subtracting(newSupplies) 350 | let addedSupplies = newSupplies.subtracting(oldSupplies) 351 | 352 | for supply in removedSupplies { 353 | supply.resource?.supplier = nil 354 | behavior.supplies.remove(supply) 355 | } 356 | 357 | if !addedSupplies.isEmpty { 358 | for supply in addedSupplies { 359 | guard let resource = supply.resource else { 360 | continue 361 | } 362 | resource.supplier = behavior 363 | behavior.supplies.insert(supply) 364 | 365 | var deadLinks = [BGSubsequentLink]() 366 | for link in resource.subsequents { 367 | guard let subsequent = link.behavior else { 368 | deadLinks.append(link) 369 | continue 370 | } 371 | 372 | if subsequent.order <= behavior.order { 373 | needsOrdering.append(subsequent) 374 | } 375 | } 376 | deadLinks.forEach { 377 | resource.subsequents.remove($0) 378 | } 379 | } 380 | } 381 | 382 | behavior.uncommittedDynamicSupplies = nil 383 | behavior.uncommittedSupplies = false 384 | } 385 | } 386 | behaviorsWithModifiedSupplies.removeAll() 387 | } 388 | 389 | private func commitModifiedDemands() { 390 | for behavior in behaviorsWithModifiedDemands { 391 | if behavior.uncommittedDemands { 392 | let oldDemands = behavior.demands 393 | 394 | var newDemands = behavior.staticDemands.filter { 395 | guard let _ = $0.resource else { 396 | return false 397 | } 398 | return true 399 | } 400 | behavior.uncommittedDynamicDemands?.forEach { 401 | let link = $0.link 402 | guard let _ = link.resource else { 403 | return 404 | } 405 | newDemands.insert(link) 406 | } 407 | behavior.demands = newDemands 408 | 409 | let removedDemands = oldDemands.subtracting(newDemands) 410 | let addedDemands = newDemands.subtracting(oldDemands) 411 | 412 | removedDemands.forEach { 413 | guard let resource = $0.resource else { 414 | return 415 | } 416 | resource.subsequents.remove(.init(behavior: behavior, type: $0.type)) 417 | } 418 | 419 | if !addedDemands.isEmpty { 420 | var needsOrdering: Bool = false 421 | var reactiveDemandJustUpdated: Bool = false 422 | 423 | for demand in addedDemands { 424 | guard let resource = demand.resource else { 425 | continue 426 | } 427 | resource.subsequents.insert(.init(behavior: behavior, type: demand.type)) 428 | 429 | if demand.type == .reactive && resource.justUpdated() { 430 | reactiveDemandJustUpdated = true 431 | } 432 | 433 | if !needsOrdering, 434 | let prior = resource.supplier, 435 | prior.order >= behavior.order { 436 | needsOrdering = true 437 | } 438 | } 439 | 440 | if needsOrdering { 441 | self.needsOrdering.append(behavior) 442 | } 443 | 444 | if reactiveDemandJustUpdated { 445 | self.submitToQueue(behavior) 446 | } 447 | } 448 | 449 | behavior.uncommittedDynamicDemands = nil 450 | behavior.uncommittedDemands = false 451 | } 452 | } 453 | behaviorsWithModifiedDemands.removeAll() 454 | } 455 | 456 | private func orderBehaviors() { 457 | var traversalQueue = needsOrdering 458 | needsOrdering.removeAll() 459 | 460 | var needsOrdering = [BGBehavior]() 461 | while !traversalQueue.isEmpty { 462 | let behavior = traversalQueue.removeFirst() 463 | if behavior.orderingState != .unordered { 464 | behavior.orderingState = .unordered 465 | needsOrdering.append(behavior) 466 | 467 | for supply in behavior.supplies { 468 | (supply.resource?.subsequents.compactMap({ $0.behavior })).map { traversalQueue.append(contentsOf: $0) } 469 | } 470 | } 471 | } 472 | 473 | var needsReheap = false 474 | needsOrdering.forEach { 475 | sortDFS(behavior: $0, needsReheap: &needsReheap) 476 | } 477 | 478 | if needsReheap { 479 | behaviorQueue.setNeedsReheap() 480 | } 481 | } 482 | 483 | private func sortDFS(behavior: BGBehavior, needsReheap: inout Bool) { 484 | guard behavior.orderingState != .ordering else { 485 | // assert or fail? 486 | self.assert(behavior.orderingState != .ordering, "Dependency cycle detected") 487 | return 488 | } 489 | 490 | if behavior.orderingState == .unordered { 491 | behavior.orderingState = .ordering 492 | 493 | var order = UInt(1) 494 | var deadLinks = [BGDemandLink]() 495 | for demand in behavior.demands { 496 | guard let demandedResource = demand.resource else { 497 | deadLinks.append(demand) 498 | continue 499 | } 500 | 501 | if let prior = demandedResource.supplier { 502 | if prior.orderingState != .ordered { 503 | sortDFS(behavior: prior, needsReheap: &needsReheap) 504 | } 505 | order = max(order, prior.order + 1) 506 | } 507 | } 508 | 509 | deadLinks.forEach { behavior.demands.remove($0) } 510 | 511 | behavior.orderingState = .ordered 512 | if order != behavior.order { 513 | behavior.order = order 514 | needsReheap = true 515 | } 516 | } 517 | } 518 | 519 | func submitToQueue(_ behavior: BGBehavior) { 520 | // @SAL 8/26/2019-- I'm not sure how either of these would trigger, it seems they are both a result of a broken 521 | // algorithm, not a misconfigured graph 522 | // jlou 2/5/19 - These asserts are checking for graph implementation bugs, not for user error. 523 | self.assert(eventLoopState?.processingChanges ?? false, "Should not be activating behaviors in current phase.") 524 | self.assert((behavior.graph?.lastEvent.sequence).map { $0 < sequence } ?? true, "Behavior already ran in this event.") 525 | 526 | if behavior.enqueuedSequence < sequence { 527 | behavior.enqueuedSequence = sequence 528 | 529 | behaviorQueue.push(behavior) 530 | } 531 | } 532 | 533 | func addExtent(_ extent: BGExtent) { 534 | guard let eventLoopState = self.eventLoopState, eventLoopState.processingChanges else { 535 | self.assertionFailure("Extents must be added during an event.") 536 | return 537 | } 538 | guard extent.status == .inactive else { 539 | self.assertionFailure("Extent can only be added once.") 540 | return 541 | } 542 | 543 | extent._added.update() 544 | extent.status = .added 545 | untrackedExtents.append(extent) 546 | untrackedBehaviors.append(contentsOf: extent.behaviors) 547 | } 548 | 549 | func removeExtent(resources: [any BGResourceInternal], behaviors: [BGBehavior]) { 550 | guard let eventLoopState = eventLoopState, eventLoopState.processingChanges else { 551 | self.assertionFailure("Can only remove extents during an event.") 552 | return 553 | } 554 | 555 | resources.forEach { resource in 556 | resource.subsequents.forEach { subsequentLink in 557 | subsequentLink.behavior?.demands.remove(.init(resource: resource, type: subsequentLink.type)) 558 | } 559 | resource.subsequents.removeAll() 560 | 561 | if let supplier = resource.supplier { 562 | supplier.supplies.remove(resource.weakReference) 563 | resource.supplier = nil 564 | } 565 | 566 | resource.owner = nil 567 | } 568 | 569 | behaviors.forEach { behavior in 570 | for supply in behavior.supplies { 571 | supply.resource?.supplier = nil 572 | } 573 | behavior.supplies.removeAll() 574 | 575 | behavior.demands.forEach { demandLink in 576 | demandLink.resource?.subsequents.remove(.init(behavior: behavior, type: demandLink.type)) 577 | } 578 | behavior.demands.removeAll() 579 | 580 | behavior.removedSequence = eventLoopState.event.sequence 581 | 582 | behavior.owner = nil 583 | } 584 | } 585 | 586 | func updateDemands(behavior: BGBehavior) { 587 | guard let eventLoopState = self.eventLoopState, eventLoopState.processingChanges else { 588 | self.assertionFailure("Can only update demands during an event.") 589 | return 590 | } 591 | behaviorsWithModifiedDemands.append(behavior) 592 | } 593 | 594 | func updateSupplies(behavior: BGBehavior) { 595 | guard let eventLoopState = self.eventLoopState, eventLoopState.processingChanges else { 596 | self.assertionFailure("Can only update supplies during an event.") 597 | return 598 | } 599 | behaviorsWithModifiedSupplies.append(behavior) 600 | } 601 | 602 | func executeOnMainFromRunloop(_ work: () -> Void) { 603 | if Thread.current.isMainThread { 604 | work() 605 | } else { 606 | mutex.unlock() 607 | DispatchQueue.main.sync { 608 | mutex.lock() 609 | work() 610 | mutex.unlock() 611 | } 612 | mutex.lock() 613 | } 614 | } 615 | 616 | static func impulseString(file: String, line: Int, function: String) -> String { 617 | "\(function)@\(file):\(line)" 618 | } 619 | 620 | func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line) { 621 | if !condition() { 622 | (debugOnAssertionFailure ?? Swift.assertionFailure)(message(), file, line) 623 | } 624 | } 625 | 626 | func assertionFailure(_ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line) { 627 | (debugOnAssertionFailure ?? Swift.assertionFailure)(message(), file, line) 628 | } 629 | 630 | } 631 | 632 | fileprivate class EventLoopState { 633 | let event: BGEvent 634 | var processingAction: Bool = true 635 | var processingChanges: Bool = true 636 | init(event: BGEvent) { 637 | self.event = event 638 | } 639 | } 640 | --------------------------------------------------------------------------------