Sources/Compose/Introspection/Introspection.swift
import Foundation
import Combine
@inlinable
func withIntrospection(_ action : () -> Void) {
#if DEBUG
if Introspection.shared.isEnabled == true {
action()
}
#endif
}
public class Introspection {
///Shared introspection instnace.
public static let shared = Introspection()
///Whether the introspection as a whole is enabled or not.
///Introspection can only be enabled in DEBUG builds.
#if DEBUG
public var isEnabled = false {
didSet {
if isEnabled == true {
advertise()
}
}
}
#else
public let isEnabled = false
#endif
///Whether component allocation/deallocation tracking is enabled or disabled.
///When enabled, all component logs are printed out in Xcode.
#if DEBUG
public var isComponentAllocationTrackingEnabled = false
#else
public let isComponentAllocationTrackingEnabled = false
#endif
///Client to send changes to.
fileprivate var client : IntrospectionClient? = nil
///Queue to operate on.
fileprivate var queue : OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
///App descriptor.
let app = AppDescriptor()
///Observation scope contrext.
fileprivate var observationScopeIds = [UUID]()
///Services ids.
fileprivate var servicesIds = [ObjectIdentifier : UUID]()
///Managing cancellables in here.
fileprivate var cancellables = Set<AnyCancellable>()
}
extension Introspection {
///Launchs the client to advertise changes to the app.
fileprivate func advertise() {
guard client == nil else {
return
}
client = IntrospectionClient()
client?.$connectionState
.sink { [unowned self] state in
guard state == .connected else {
return
}
self.advertiseAppDescriptor()
}
.store(in: &cancellables)
app.objectWillChange
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.global())
.sink { [unowned self] _ in
self.advertiseAppDescriptor()
}
.store(in: &cancellables)
}
fileprivate func advertiseAppDescriptor() {
queue.addOperation { [weak self] in
guard let self = self else {
return
}
do {
try self.client?.send(self.app)
}
catch let error {
print("[Compose] Error advertising introspection changes: \(error)")
}
}
}
}
extension Introspection {
///Registers a component with the introspection.
func register(component : Component) {
var lifecycle : ComponentDescriptor.Lifecycle = .static
switch component {
case is AnyContainerComponent:
lifecycle = .container
default:
break
}
app.components[component.id] = ComponentDescriptor(id: component.id,
name: String(describing: component.type),
lifecycle: lifecycle)
if component is StartupComponent {
app.startupComponentId = component.id
}
}
///Unregisters a component and removes it from the introspection.
func unregister(component id : UUID) {
guard let descriptor = app.components[id] else {
return
}
descriptor.emitters.forEach {
unregister(emitter: $0)
}
}
func updateDescriptor(forComponent id : UUID, update : (inout ComponentDescriptor?) -> Void) {
update(&app.components[id])
}
func descriptor(forComponent id : UUID) -> ComponentDescriptor? {
app.components[id]
}
}
extension Introspection {
func register(emitter : AnyEmitter, named name : String?) {
app.emitters[emitter.id] = EmitterDescriptor(id: emitter.id,
name: name ?? "Unnamed",
description: "",
valueDescription: "")
}
func unregister(emitter id : UUID) {
guard let descriptor = app.emitters[id] else {
return
}
app.emitters[id] = nil
descriptor.observers.forEach {
unregister(observer: $0)
}
}
func updateDescriptor(forEmitter id : UUID, update : (inout EmitterDescriptor?) -> Void) {
update(&app.emitters[id])
}
}
extension Introspection {
func register<V>(observer : Observer<V>, emitterId : UUID) {
app.observers[observer.id] = ObserverDescriptor(id: observer.id,
description: "",
emitterId: emitterId)
}
func unregister(observer id : UUID) {
if let observer = app.observers[id] {
updateDescriptor(forEmitter: observer.emitterId) {
$0?.observers.remove(id)
}
if let componentId = observer.componentId {
updateDescriptor(forComponent: componentId) {
$0?.observers.remove(id)
}
}
}
app.observers[id] = nil
}
func updateDescriptor(forObserver id : UUID, update : (inout ObserverDescriptor?) -> Void) {
update(&app.observers[id])
}
func descriptor(forObserver id : UUID) -> ObserverDescriptor? {
app.observers[id]
}
func pushObservationScope(id : UUID) {
observationScopeIds.append(id)
}
func popObservationScope() {
observationScopeIds.removeLast()
}
var observationScopeId : UUID {
observationScopeIds.last ?? UUID()
}
}
extension Introspection {
func register(store : AnyStore, named name : String?) {
app.stores[store.id] = StoreDescriptor(id: store.id,
name: name ?? "Unknown",
isMapped: store.isMapped)
}
func updateDescriptor(forStore id : UUID, update : (inout StoreDescriptor?) -> Void) {
update(&app.stores[id])
}
}
extension Introspection {
func register(router : Router, named name : String?) {
guard app.routers[router.id] == nil else {
return
}
app.routers[router.id] = RouterDescriptor(id: router.id,
name: name ?? "Unknown",
hasDefaultContent: router.start == nil)
app.routers[router.id]?.routes = router.routes.map { $0.id }
}
func updateDescriptor(forRouter id : UUID, update : (inout RouterDescriptor?) -> Void) {
update(&app.routers[id])
}
}
extension Introspection {
func register<S : Service>(service : S) {
var id = UUID()
if let existingId = servicesIds[ObjectIdentifier(S.self)] {
id = existingId
}
else {
servicesIds[ObjectIdentifier(S.self)] = id
}
var descriptor = ComponentDescriptor(id: id,
name: String(describing: S.self),
lifecycle: .service)
let mirror = Mirror(reflecting: service)
for (name, value) in mirror.children {
if let emitter = value as? AnyEmitter {
Introspection.shared.register(emitter: emitter, named: name)
descriptor.add(emitter: emitter)
}
if let store = value as? AnyStore {
let name = name?.replacingOccurrences(of: "_", with: "") ?? "state"
Introspection.shared.register(store: store, named: name)
descriptor.add(store: store)
Introspection.shared.register(emitter: store.willChange, named: "\(name).willChange")
Introspection.shared.updateDescriptor(forEmitter: store.willChange.id) {
$0?.componentId = id
}
}
}
app.components[id] = descriptor
}
}