Last active
September 4, 2025 06:29
-
-
Save arivictor/47ec87b4a32ee048b0c38e9ece72290d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import SwiftUI | |
| import Combine | |
| import CoreData | |
| // MARK: - Domain | |
| protocol EventBus { | |
| func publish<T: DomainEvent>(_ event: T) | |
| func subscribe<T: DomainEvent>(_ type: T.Type, handler: @escaping (T) -> Void) -> AnyCancellable | |
| } | |
| struct NoteEntity: Identifiable { | |
| let id: UUID | |
| var preview: String | |
| var body: String | |
| var pinned: Bool = false | |
| var updatedAt: Date | |
| var createdAt: Date | |
| init(id: UUID = UUID(), body: String, pinned: Bool = false, createdAt: Date = Date(), updatedAt: Date = Date(), preview: String = "") { | |
| self.id = id | |
| self.body = body | |
| self.preview = preview ?? NotePreviewGenerator.generate(from: body) | |
| self.pinned = pinned | |
| self.createdAt = createdAt | |
| self.updatedAt = updatedAt | |
| } | |
| } | |
| protocol NoteRepository { | |
| func create(body: String) -> NoteEntity | |
| func update(id: UUID, body: String) -> NoteEntity? | |
| func pin(id: UUID, pinned: Bool) -> NoteEntity? | |
| func get(id: UUID) -> NoteEntity? | |
| func getAll() -> [NoteEntity] | |
| func delete(id: UUID) | |
| } | |
| struct NotePreviewGenerator { | |
| static func generate(from body: String) -> String { | |
| let lines = body.components(separatedBy: .newlines) | |
| return lines | |
| .filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } | |
| .prefix(3) | |
| .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } | |
| .joined(separator: "\n") | |
| } | |
| } | |
| protocol DomainEvent { | |
| var eventId: UUID { get } | |
| var timestamp: Date { get } | |
| var eventType: String { get } | |
| } | |
| struct NotePinnedEvent: DomainEvent { | |
| let eventId = UUID() | |
| let timestamp = Date() | |
| let eventType = "NotePinned" | |
| let noteId: UUID | |
| let pinned: Bool | |
| } | |
| struct NoteDeletedEvent: DomainEvent { | |
| let eventId = UUID() | |
| let timestamp = Date() | |
| let eventType = "NoteDeleted" | |
| let noteId: UUID | |
| } | |
| protocol EventHandler { | |
| associatedtype EventType: DomainEvent | |
| func handle(_ event: EventType) | |
| } | |
| final class NotePinnedEventHandler: EventHandler { | |
| func handle(_ event: NotePinnedEvent) { | |
| print("➡️ [Event Handler] NotePinnedEventHandler reacting to event for note \(event.noteId)") | |
| // Do something.. | |
| } | |
| } | |
| final class NoteDeletedEventHandler: EventHandler { | |
| func handle(_ event: NoteDeletedEvent) { | |
| print("➡️ [Event Handler] NoteDeletedEventHandler reacting to event for note \(event.noteId)") | |
| // Do something.. | |
| } | |
| } | |
| protocol Command { | |
| var commandId: UUID { get } | |
| } | |
| struct PinNoteCommand: Command { | |
| let commandId = UUID() | |
| let noteId: UUID | |
| let pinned: Bool | |
| } | |
| struct DeleteNoteCommand: Command { | |
| let commandId = UUID() | |
| let noteId: UUID | |
| } | |
| protocol CommandHandler { | |
| associatedtype CommandType: Command | |
| func handle(_ command: CommandType) | |
| } | |
| class PinNoteCommandHandler: CommandHandler { | |
| typealias CommandType = PinNoteCommand | |
| private let eventBus: EventBus | |
| private let database: NoteRepository | |
| init(eventBus: EventBus, database: NoteRepository) { | |
| self.eventBus = eventBus | |
| self.database = database | |
| } | |
| func handle(_ command: PinNoteCommand) { | |
| print("🔨 [Command Handler] PinNoteCommand send for note \(command.noteId)") | |
| if let note = database.pin(id: command.noteId, pinned: command.pinned) { | |
| let event = NotePinnedEvent(noteId: note.id, pinned: command.pinned) | |
| eventBus.publish(event) | |
| } else { | |
| print("❗ [Command Handler] Note with ID \(command.noteId) not found.") | |
| } | |
| } | |
| } | |
| class DeleteNoteCommandHandler: CommandHandler { | |
| typealias CommandType = DeleteNoteCommand | |
| private let eventBus: EventBus | |
| private let database: NoteRepository | |
| init(eventBus: EventBus, database: NoteRepository) { | |
| self.eventBus = eventBus | |
| self.database = database | |
| } | |
| func handle(_ command: DeleteNoteCommand) { | |
| print("🔨 [Command Handler] DeleteNoteCommand send for note \(command.noteId)") | |
| database.delete(id: command.noteId) | |
| let event = NoteDeletedEvent(noteId: command.noteId) | |
| eventBus.publish(event) | |
| } | |
| } | |
| // MARK: Infrastructure | |
| final class InMemoryNoteRepository: NoteRepository { | |
| private var notes: [UUID: NoteEntity] = [:] | |
| func create(body: String) -> NoteEntity { | |
| var note = NoteEntity(body: body) | |
| let now = Date() | |
| note.createdAt = now | |
| note.updatedAt = now | |
| note.preview = NotePreviewGenerator.generate(from: body) | |
| notes[note.id] = note | |
| return note | |
| } | |
| func update(id: UUID, body: String) -> NoteEntity? { | |
| guard var note = notes[id] else { return nil } | |
| note.body = body | |
| note.preview = NotePreviewGenerator.generate(from: body) | |
| note.updatedAt = Date() | |
| notes[id] = note | |
| return note | |
| } | |
| func pin(id: UUID, pinned: Bool) -> NoteEntity? { | |
| guard var note = notes[id] else { return nil } | |
| note.pinned = pinned | |
| note.updatedAt = Date() | |
| notes[id] = note | |
| return note | |
| } | |
| func get(id: UUID) -> NoteEntity? { | |
| notes[id] | |
| } | |
| func getAll() -> [NoteEntity] { | |
| Array(notes.values) | |
| } | |
| func delete(id: UUID) { | |
| notes.removeValue(forKey: id) | |
| } | |
| } | |
| class InMemoryEventBus: EventBus, ObservableObject { | |
| private let subject = PassthroughSubject<DomainEvent, Never>() | |
| func publish<T: DomainEvent>(_ event: T) { | |
| print("📢 [Event Bus]: \(event.eventType) at \(event.timestamp)") | |
| subject.send(event) | |
| } | |
| func subscribe<T: DomainEvent>(_ type: T.Type, handler: @escaping (T) -> Void) -> AnyCancellable { | |
| subject | |
| .compactMap { $0 as? T } | |
| .sink(receiveValue: handler) | |
| } | |
| } | |
| // MARK: - Presentation | |
| extension Date { | |
| func relativeDescription() -> String { | |
| let now = Date() | |
| let interval = now.timeIntervalSince(self) | |
| if interval < 60 { | |
| return "Just now" | |
| } | |
| let formatter = RelativeDateTimeFormatter() | |
| formatter.unitsStyle = .full | |
| return formatter.localizedString(for: self, relativeTo: now) | |
| } | |
| static func randomWithin7Days() -> Date { | |
| let now = Date() | |
| let randomSeconds = TimeInterval(Int.random(in: 0..<(7 * 24 * 60 * 60))) | |
| return now.addingTimeInterval(-randomSeconds) | |
| } | |
| } | |
| struct ContentView: View { | |
| let database: NoteRepository | |
| let eventBus: EventBus | |
| @State private var notes: [NoteEntity] = [] | |
| @State private var cancellables = Set<AnyCancellable>() | |
| init(database: NoteRepository, eventBus: EventBus) { | |
| self.database = database | |
| self.eventBus = eventBus | |
| } | |
| private func toggleSidebar() { | |
| NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil) | |
| } | |
| private func subscribeToPinned() { | |
| let cancellable = eventBus.subscribe(NotePinnedEvent.self) { event in | |
| DispatchQueue.main.async { | |
| if let idx = self.notes.firstIndex(where: { $0.id == event.noteId }) { | |
| self.notes[idx].pinned = event.pinned | |
| } | |
| } | |
| } | |
| cancellables.insert(cancellable) | |
| } | |
| private func subscribeToDeleted() { | |
| let cancellable = eventBus.subscribe(NoteDeletedEvent.self) { event in | |
| DispatchQueue.main.async { | |
| self.notes.removeAll { $0.id == event.noteId } | |
| } | |
| } | |
| cancellables.insert(cancellable) | |
| } | |
| private func actionPinNote(noteId: UUID, pinned: Bool) { | |
| print("👤 [User Action] Pin note \(noteId) set to \(pinned)") | |
| let cmd = PinNoteCommand(noteId: noteId, pinned: pinned) | |
| let pinHandler = PinNoteCommandHandler(eventBus: eventBus, database: database) | |
| pinHandler.handle(cmd) | |
| } | |
| private func actionDeleteNote(noteId: UUID) { | |
| print("👤 [User Action] Delete note \(noteId)") | |
| let cmd = DeleteNoteCommand(noteId: noteId) | |
| let deleteHandler = DeleteNoteCommandHandler(eventBus: eventBus, database: database) | |
| deleteHandler.handle(cmd) | |
| } | |
| private func get_notes() -> [NoteEntity] { | |
| return database.getAll().sorted { $0.updatedAt > $1.updatedAt } | |
| } | |
| private func bootstrap() { | |
| notes = get_notes() | |
| subscribeToPinned() | |
| subscribeToDeleted() | |
| } | |
| var body: some View { | |
| NavigationView { | |
| VStack { | |
| List(notes) { note in | |
| NavigationLink( | |
| destination: Text(note.body) | |
| .padding() | |
| ) { | |
| Text(note.preview) | |
| .lineLimit(1) | |
| if note.pinned { | |
| Image(systemName: "pin.fill") | |
| .font(.caption) | |
| .foregroundColor(.primary) | |
| } | |
| } | |
| .contextMenu { | |
| Button(action: { | |
| actionPinNote(noteId: note.id, pinned: !note.pinned) | |
| }) { | |
| Text("Pin") | |
| Image(systemName: "pin") | |
| } | |
| Button(action: { | |
| actionDeleteNote(noteId: note.id) | |
| }) { | |
| Text("Delete") | |
| Image(systemName: "trash") | |
| } | |
| } | |
| } | |
| .onAppear { | |
| bootstrap() | |
| } | |
| } | |
| .toolbar { | |
| ToolbarItem(placement: .navigation) { | |
| Button(action: toggleSidebar) { | |
| Image(systemName: "sidebar.leading") | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // MARK: - App | |
| func BootstrappedContentView() -> some View { | |
| let database = InMemoryNoteRepository() | |
| let eventBus = InMemoryEventBus() | |
| // Seed database with initial notes | |
| _ = database.create(body: "Sample note") | |
| // Global event handlers | |
| _ = eventBus.subscribe(NotePinnedEvent.self) { event in | |
| NotePinnedEventHandler().handle(event) // logging / background work | |
| } | |
| _ = eventBus.subscribe(NoteDeletedEvent.self) { event in | |
| NoteDeletedEventHandler().handle(event) // logging / background work | |
| } | |
| return ContentView( | |
| database: database, | |
| eventBus: eventBus | |
| ) | |
| } | |
| #Preview { | |
| return BootstrappedContentView() | |
| } | |
| @main | |
| struct HyperNoteApp: App { | |
| var body: some Scene { | |
| WindowGroup { | |
| BootstrappedContentView() | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment