commit e1463eb256ea46becfe3a041f704e19b25c4726c Author: Signal Date: Mon Aug 3 15:05:47 2026 -0400 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..5320a6b --- /dev/null +++ b/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version: 6.1 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "ArtifactState", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "ArtifactState", + targets: ["ArtifactState"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "ArtifactState"), + .testTarget( + name: "ArtifactStateTests", + dependencies: ["ArtifactState"] + ), + ] +) diff --git a/Sources/ArtifactState/State.swift b/Sources/ArtifactState/State.swift new file mode 100644 index 0000000..637d196 --- /dev/null +++ b/Sources/ArtifactState/State.swift @@ -0,0 +1,102 @@ + +actor ObservationState { + public static var observers: [[any BindingProtocol]] = [] +} + +public func observing(into: inout [any BindingProtocol], body: () -> T) -> T { + ObservationState.observers.append([]) + defer { into = ObservationState.observers.popLast()! } + return body() +} + +public protocol BindingProtocol { + func notify() + func subscribe(key: ObjectIdentifier, handler: @escaping () -> Void) + func unsubscribe(key: ObjectIdentifier) +} + +public class Binding: BindingProtocol { + public private(set) var listeners: [ObjectIdentifier: () -> Void] = [:] + public var wrappedValue: T { fatalError() } + + public func notify() { + listeners.forEach { $1() } + } + + public func subscribe(key: ObjectIdentifier, handler: @escaping () -> Void) { + listeners[key] = handler + } + + public func unsubscribe(key: ObjectIdentifier) { + listeners.removeValue(forKey: key) + } +} + +@propertyWrapper +public class State: Binding { + public var projectedValue: State { self } + private var value: T + public override var wrappedValue: T { + get { + if ObservationState.observers.count > 0 { + ObservationState.observers[ObservationState.observers.count - 1].append(self) + } + return value + } + set { + if value != newValue { + value = newValue + notify() + } + } + } + + public init(wrappedValue: T) { + self.value = wrappedValue + } +} + +public class DerivedState: Binding { + private var value: T + public override var wrappedValue: T { + get { + if ObservationState.observers.count > 0 { + ObservationState.observers[ObservationState.observers.count - 1].append(self) + } + return value + } + } + var deps: [any BindingProtocol] = [] + var derivation: () -> T + private var id: ObjectIdentifier! + + public init(_ derivation: @escaping () -> T) { + self.derivation = derivation + value = observing(into: &deps) { + derivation() + } + super.init() + id = ObjectIdentifier(self) + deps.forEach { + $0.subscribe(key: id) { [self] in + recompute() + } + } + } + + deinit { + deps.forEach { $0.unsubscribe(key: id) } + } + + func recompute() { + deps.forEach { $0.unsubscribe(key: id) } + value = observing(into: &deps) { + derivation() + } + deps.forEach { + $0.subscribe(key: id) { [self] in + recompute() + } + } + } +} diff --git a/Tests/ArtifactStateTests/ArtifactStateTests.swift b/Tests/ArtifactStateTests/ArtifactStateTests.swift new file mode 100644 index 0000000..ce23e70 --- /dev/null +++ b/Tests/ArtifactStateTests/ArtifactStateTests.swift @@ -0,0 +1,12 @@ +import XCTest +@testable import ArtifactState + +final class ArtifactStateTests: XCTestCase { + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest + + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } +}