{"id":1819,"url":"https://github.com/hyperoslo/Aftermath","last_synced_at":"2025-08-02T04:32:40.971Z","repository":{"id":56901653,"uuid":"66086829","full_name":"hyperoslo/Aftermath","owner":"hyperoslo","description":":crystal_ball: Stateless message-driven micro-framework in Swift.","archived":false,"fork":false,"pushed_at":"2017-03-28T14:15:25.000Z","size":457,"stargazers_count":71,"open_issues_count":0,"forks_count":1,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-07-13T13:04:51.825Z","etag":null,"topics":["command","handler","stateless","unidirectional-data-flow"],"latest_commit_sha":null,"homepage":"http://hyper.no","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hyperoslo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-08-19T14:07:42.000Z","updated_at":"2023-04-11T13:33:27.000Z","dependencies_parsed_at":"2022-08-20T18:20:33.467Z","dependency_job_id":null,"html_url":"https://github.com/hyperoslo/Aftermath","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/hyperoslo/Aftermath","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyperoslo%2FAftermath","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyperoslo%2FAftermath/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyperoslo%2FAftermath/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyperoslo%2FAftermath/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hyperoslo","download_url":"https://codeload.github.com/hyperoslo/Aftermath/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyperoslo%2FAftermath/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268334618,"owners_count":24233793,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-02T02:00:12.353Z","response_time":74,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["command","handler","stateless","unidirectional-data-flow"],"created_at":"2024-01-05T20:15:56.565Z","updated_at":"2025-08-02T04:32:40.379Z","avatar_url":"https://github.com/hyperoslo.png","language":"Swift","funding_links":[],"categories":["Uncategorized","Reactive Programming","HarmonyOS"],"sub_categories":["Uncategorized","Other free courses","Windows Manager","Prototyping","Other Parsing"],"readme":"⚠️ **DEPRECATED, NO LONGER MAINTAINED**\n\n![Aftermath](https://github.com/hyperoslo/Aftermath/blob/master/Images/cover.png)\n\n[![CI Status](http://img.shields.io/travis/hyperoslo/Aftermath.svg?style=flat)](https://travis-ci.org/hyperoslo/Aftermath)\n[![Version](https://img.shields.io/cocoapods/v/Aftermath.svg?style=flat)](http://cocoadocs.org/docsets/Aftermath)\n[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)\n![Swift](https://img.shields.io/badge/%20in-swift%203.0-orange.svg)\n[![License](https://img.shields.io/cocoapods/l/Aftermath.svg?style=flat)](http://cocoadocs.org/docsets/Aftermath)\n[![Platform](https://img.shields.io/cocoapods/p/Aftermath.svg?style=flat)](http://cocoadocs.org/docsets/Aftermath)\n\n## Description\n\n**Aftermath** is a stateless message-driven micro-framework in Swift, which is\nbased on the concept of the unidirectional data flow architecture.\n\nAt first sight **Aftermath** may seem to be just a type-safe implementation of\nthe publish-subscribe messaging pattern, but actually it could be considered as\na distinct mental model in application design, different from familiar MVC,\nMVVM or MVP approaches. Utilizing the ideas behind\n[Event Sourcing](http://martinfowler.com/eaaDev/EventSourcing.html)\nand [Flux](https://facebook.github.io/flux/) patterns it helps to separate\nconcerns, reduce code dependencies and make data flow more predictable.\n\nThe following diagram demonstrates the data flow in **Aftermath** architecture\nin details:\n\n\u003cdiv align=\"center\"\u003e\n\u003cimg src=\"https://github.com/hyperoslo/Aftermath/blob/master/Images/detailed_flow.png\" /\u003e\n\u003c/div\u003e\u003cbr/\u003e\n\n## Table of Contents\n\n* [Core components](#core-components)\n* [The flow](#the-flow)\n* [Extra](#extra)\n* [Engine](#engine)\n* [Life hacks](#life-hacks)\n* [Tools](#tools)\n* [Summary](#summary)\n* [Installation](#installation)\n* [Examples](#examples)\n* [Extensions](#extensions)\n* [Alternatives](#alternatives)\n* [Author](#author)\n* [Influences](#influences)\n* [Contributing](#contributing)\n* [License](#License)\n\n## Core components\n\n### Command\n\n**Command** is a message with a set of instructions describing an intention to\nexecute the corresponding behavior. Command could lead to data fetching,\ndata mutation and any sort of sync or async operation that\nproduces desirable output needed to update application/view state.\n\nEvery command can produce only one **Output** type.\n\n### Command Handler\n\n**Command Handler** layer is responsible for business logic in the application.\nThe submission of a command is received by a command handler, which usually\nperforms short- or long-term operation, such as network request, database query,\ncache read/white process, etc. Command handler can be sync and publish the\nresult immediately. On the other hand it's the best place in the\napplication to write asynchronous code.\n\nThe restriction is to create only one command handler per command.\n\n### Event\n\nCommand Handler is responsible for publishing **events** that will be\nconsumed by reactions. There are 3 types of events:\n\n- `progress` event indicates that the operation triggered by command has been\nstarted and is in the pending state at the moment.\n- `data` event holds the output produced by the command execution.\n- `error` notifies that an error has been occurred during the command\nexecution.\n\n### Reaction\n\n**Reaction** responds to event published by command handler. It is supposed\nto handle 3 possible event types by describing the desired behavior in the\neach scenario:\n\n- `wait` function reacts on `progress` type of the event\n- `consume` function reacts on `data` type of the event.\n- `rescue` function is a fallback for the case when `error` event has been\nreceived.\n\nNormally reaction performs UI updates, but could also be used for other kinds\nof output processing.\n\n## The flow\n\nTaking 4 core components described before, we can build a simplified version\nof the data flow:\n\n\u003cdiv align=\"center\"\u003e\n\u003cimg src=\"https://github.com/hyperoslo/Aftermath/blob/master/Images/simplified_flow.png\" /\u003e\n\u003c/div\u003e\u003cbr/\u003e\n\n### Command execution\n\nThe first step is to declare a command. Your command type has to conform to the\n`Aftermath.Command` protocol and the `Output` type must be implicitly specified.\n\nLet's say we want to fetch a list of books from some untrusted resource and\ncorrect typos in titles and author names 🤓.\n\n```swift\n// This is our model we are going to work with.\nstruct Book {\n  let id: Int\n  let title: String\n  let author: String\n}\n\nstruct BookListCommand: Command {\n  // Result of this command will be a list of books.\n  typealias Output = [Book]\n}\n\nstruct BookUpdateCommand: Command {\n  // Result of this command will be an updated book.\n  typealias Output = Book\n\n  // Let's pass the entire model to the command to simplify this example.\n  // Ideally we wouldn't do that because a command is supposed to be as simple\n  // as possible, only with attributes that are needed for handler.\n  let book: Book\n}\n```\n\n**Note** that any type can play the role of `Output`, so if we want to add a\ndate to our `BookUpdateCommand` it could look like the following:\n\n```swift\ntypealias Output = (Book, Date)\n```\n\nIn order to execute a command you have to conform to `CommandProducer` protocol:\n\n```swift\nclass ViewController: UITableViewController, CommandProducer {\n\n  // Fetch a list of books.\n  func load() {\n    execute(command: BookListCommand())\n  }\n\n  // Update a single book with corrected title and/or author name.\n  func update(book: Book) {\n    execute(command: BookUpdateCommand(book: book))\n  }\n}\n```\n\n### Command handling\n\nCommand is an intention that needs to be translated into an action by a handler.\nThe command handler is responsible for publishing events to notify about results of\nthe operation it performs. The command handlers type has to conform to\n`Aftermath.CommandHandler` protocol, that needs to know about the command type\nit will work with:\n\n```swift\nstruct BookListCommandHandler: CommandHandler {\n\n  func handle(command: BookListCommand) throws -\u003e Event\u003cBooksCommand\u003e {\n    // Start network request to fetch data.\n    fetchBooks { books, error in\n      if let error = error {\n        // Publish error.\n        self.publish(error: error)\n        return\n      }\n\n      // Publish fetched data.\n      self.publish(data: books)\n    }\n\n    // Load data from local database/cache.\n    let localBooks = loadLocalBooks()\n\n    // If the list is empty let the listeners know that operation is in the process.\n    return Book.list.isEmpty ? Event.progress : Event.data(localBooks)\n  }\n}\n```\n\n**Note** that every command handler needs to be registered on\n[Aftermath Engine](#engine).\n\n```swift\nEngine.shared.use(handler: BookListCommandHandler())\n```\n\n### Reacting to events\n\nThe last step, but not the least, is to react to events published by the command\nhandlers. Just conform to `ReactionProducer` protocol, implement reaction\nbehavior and you're ready to go:\n\n```swift\nclass ViewController: UITableViewController, CommandProducer, ReactionProducer {\n\n  var books = [Book]()\n\n  deinit {\n    // Don't forget to dispose all reaction tokens.\n    disposeAll()\n  }\n\n  override func viewDidLoad() {\n    super.viewDidLoad()\n\n    // React to events.\n    react(to: BookListCommand.self, with: Reaction(\n      wait: { [weak self] in\n        // Wait for results to come.\n        self?.refreshControl?.beginRefreshing()\n      },\n      consume: { [weak self] books in\n        // We're lucky, there are some books to display.\n        self?.books = books\n        self?.refreshControl?.endRefreshing()\n        self?.tableView.reloadData()\n      },\n      rescue: { [weak self] error in\n        // Well, seems like something went wrong.\n        self?.refreshControl?.endRefreshing()\n        print(error)\n      }))\n  }\n\n  // ...\n}\n```\n\n**It's important** to dispose all reaction tokens when your `ReactionProducer`\ninstance is about to be deallocated or reaction needs to be unsubscribed from\nevents.\n\n```swift\n// Disposes all reaction tokens for the current `ReactionProducer`.\ndisposeAll()\n\n// Disposes a specified reaction token.\nlet token = react(to: BookListCommand.self, with: reaction)\ndispose(token: token)\n```\n\n## Extra\n\n### Action\n\n**Action** is a variation of command that handles itself. It's a possibility to\nsimplify the code when command itself or business logic are super tiny. There\nis no need to register an action, it will be automatically added to the list of\nactive command handlers on the fly, when it's executed as a command.\n\n```swift\nimport Sugar\n\nstruct WelcomeAction: Action {\n  typealias Output = String\n\n  let userId: String\n\n  func handle(command: WelcomeAction) throws -\u003e Event\u003cWelcomeAction\u003e {\n    fetchUser(id: userId) { user in\n      self.publish(data: \"Hello \\(user.name)\")\n    }\n    return Event.progress\n  }\n}\n\n// Execute action\n\nstruct WelcomeManager: CommandProducer {\n\n  func salute() {\n    execute(action: WelcomeAction(userId: 11))\n  }\n}\n```\n\n### Fact\n\n**Fact** works like notification, with no async operations involved. It can\nbe used when there is no need for a handler to generate an output. Fact is an\noutput itself, so the only thing you want to do is notify all\nsubscribers that something happened in the system, and they will react\naccordingly. In this sense it's closer to a type-safe alternative to\n`Notification`.\n\n```swift\nstruct LoginFact: Fact {\n  let username: String\n}\n\nclass ProfileController: UIViewController, ReactionProducer {\n\n  override func viewDidLoad() {\n    super.viewDidLoad()\n\n    // React\n    next { (fact: LoginFact) in\n      title = fact.username\n    }\n  }\n}\n\nstruct AuthService: FactProducer {\n\n  func login() {\n    let fact = LoginFact(username: \"John Doe\")\n    // Publish\n    post(fact: fact)  \n  }\n}\n```\n\n### Middleware\n\n**Middleware** is a layer where commands and events can be intercepted before\nthey reach their listeners.\n\nIt means you can modify/cancel/extend the executed command in\n**Command Middleware** before it's processed by the command handler:\n\n\u003cdiv align=\"center\"\u003e\n\u003cimg src=\"https://github.com/hyperoslo/Aftermath/blob/master/Images/command_middleware.png\" /\u003e\n\u003c/div\u003e\u003cbr/\u003e\n\nOr you can do appropriate operation in **Event Middleware** before the\npublished event is received by its reactions.\n\n\u003cdiv align=\"center\"\u003e\n\u003cimg src=\"https://github.com/hyperoslo/Aftermath/blob/master/Images/event_middleware.png\" /\u003e\n\u003c/div\u003e\u003cbr/\u003e\n\nIt's handy for logging, crash reporting, aborting particular commands or\nevents, etc.\n\n```swift\n// Command middleware\nstruct ErrorCommandMiddleware: CommandMiddleware {\n\n  func intercept(command: AnyCommand, execute: Execute, next: Execute) throws {\n    do {\n      // Don't forget to call `next` to invoke the next function in the chain.\n      try next(command)\n    } catch {\n      print(\"Command failed with error -\u003e \\(command)\")\n      throw error\n    }\n  }\n}\n\nEngine.shared.pipeCommands(through: [ErrorCommandMiddleware()])\n\n// Event middleware\nstruct LogEventMiddleware: EventMiddleware {\n\n  // Don't forget to call `next` to invoke the next function in the chain.\n  func intercept(event: AnyEvent, publish: Publish, next: Publish) throws {\n    print(\"Event published -\u003e \\(event)\")\n    try next(event)\n  }\n}\n\nEngine.shared.pipeEvents(through: [LogEventMiddleware()])\n```\n\n**Note** that it's necessary to call `next` to invoke the next function in the\nchain while building your custom middleware.\n\n`AnyCommand` and `AnyEvent` are special protocols that every `Command` or\n`Event` conform to. They are used mostly in [middleware](#middleware) to\nworkaround restrictions of working with Swift generic protocols that have\n`associatedtype`.\n\n## Engine\n\n**Engine** is the main entry point for **Aftermath** configuration:\n\n- Register command handlers:\n\n```swift\nEngine.shared.use(handler: BookListCommandHandler())\n```\n\n- Add command and event middleware:\n\n```swift\n// Commands\nEngine.shared.pipeCommands(through: [LogCommandMiddleware(), ErrorCommandMiddleware()])\n// Events\nEngine.shared.pipeEvents(through: [LogEventMiddleware(), ErrorEventMiddleware()])\n```\n\n- Set global error handler to catch all unexpected errors and framework\nwarnings:\n\n```swift\nstruct EngineErrorHandler: ErrorHandler {\n\n  func handleError(error: Error) {\n    if let error = error as? Failure {\n      print(\"Engine error -\u003e \\(error)\")\n    } else if let warning = error as? Warning {\n      print(\"Engine warning -\u003e \\(warning)\")\n    } else {\n      print(\"Unknown error -\u003e \\(error)\")\n    }\n  }\n}\n\nEngine.shared.errorHandler = EngineErrorHandler()\n```\n\n- Dispose all registered command handlers and event listeners (reactions):\n\n```swift\nEngine.shared.invalidate()\n```\n\n## Life hacks\n\n### Stories\n\nNaming is hard. It doesn't feel right to have names like `BookListCommand`,\n`BookListCommandHandler` and `BookListWhatever`, does it? If you agree, then\nyou can work around this issue by introducing a new idea into the mix.\nYou can group all related types into stories, which make the flow more concrete.\n\n```swift\nstruct BookListStory {\n\n  struct Command: Aftermath.Command {\n    // ...\n  }\n\n  struct Handler: Aftermath.CommandHandler {\n    // ...\n  }\n}\n```\n\nIn this sense, it's close to user stories used in agile software development\nmethodologies.\n\nYou can find more detailed example in [AftermathNotes](https://github.com/hyperoslo/Aftermath/blob/master/Example/Aftermath)\ndemo project.\n\n### Features\n\nSome of the stories may seem very similar. Then in makes sense to make them\nmore generic and reusable according to the [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)\nprinciple. For example, let's say we have the flow to fetch a single resource\nby id.\n\n```swift\nimport Aftermath\nimport Malibu\n\n// Generic feature\nprotocol DetailFeature {\n  associatedtype Model: Entity\n  var resource: String { get }\n}\n\n// Command\nstruct DetailCommand\u003cFeature: DetailFeature\u003e: Aftermath.Command {\n  typealias Output = Feature.Model\n  let id: Int\n}\n\n// Command handler\nstruct DetailCommandHandler\u003cFeature: DetailFeature\u003e: Aftermath.CommandHandler {\n  typealias Command = DetailCommand\u003cFeature\u003e\n\n  let feature: Feature\n\n  func handle(command: Command) throws -\u003e Event\u003cCommand\u003e {\n    fetchDetail(\"\\(feature.resource)/\\(command.id)\") { json, error in\n      if let error = error {\n        self.publish(error: error)\n        return\n      }\n\n      do {\n        self.publish(data: try Feature.Model(json))\n      } catch {\n        self.publish(error: error)\n      }\n    }\n\n    return Event.progress\n  }\n}\n\n// Concrete feature\nstruct BookFeature: ListFeature, DeleteFeature, CommandProducer {\n  typealias Model = Todo\n  var resource = \"books\"\n}\n\n// Execute command to load a single resource.\nexecute(command: DetailCommand\u003cBookFeature\u003e(id: id))\n\n// Register reaction listener.\nreact(to: DetailCommand\u003cBookFeature\u003e.self, with: reaction)\n```\n\nYou can find more detailed example in [AftermathNotesPlus](https://github.com/hyperoslo/Aftermath/blob/master/Example/AftermathNotes)\ndemo project.\n\n## Summary\n\nWe believe that in iOS applications, in most of the cases, there is no real need\nfor single global state (single source of truth) or multiple sub-states\ndistributed between stores. Data is stored on disc in local persistence layer,\nsuch as database and cache, or it's fetched from network. Then this content,\nassembled piece by piece from different sources, is translated into the\n\"view state\", which is readable by the view to render it on the screen. This\n\"view state\" is kept in memory and valid at a given instant in time until we\nswitch the context and the current view is deallocated.\n\nKeeping that in mind, it makes more sense to dispose the \"view state\" together\nwith the view it belongs to, rather than retain no longer used replication in\nany sort of global state.\n\nIt should be enough to restore a state by re-playing previous events from the\nhistory.\n\n**Advantages of Aftermath**\n\n- Separation of concerns\n- Code reusability\n- Unidirectional data flow\n- Type safety\n\n**Disadvantages of **Aftermath**\n\n- No state (?)\n- Focusing on command output instead of actual data\n- Async command handler could confuse the flow\n\n**P.S.** Even though **Aftermath** is a stateless framework at the moment, we\nhave plans to introduce some sort of optional store(s) for better state\nmanagement. It might be a new feature in v2, keep watching.\n\n## Tools\n\n- **Aftermath** comes with a set of development tools, such as additional\nhelpers, useful command and event middleware for logging, error handling, etc.\n\n```swift\n// Commands\nEngine.sharedInstance.pipeCommands(through: [LogCommandMiddleware(), ErrorCommandMiddleware()])\n// Events\nEngine.sharedInstance.pipeEvents(through: [LogEventMiddleware(), ErrorEventMiddleware()])\n```\n\n## Installation\n\n**Aftermath** is available through [CocoaPods](http://cocoapods.org). To install\nit, simply add the following line to your Podfile:\n\n```ruby\npod 'Aftermath'\n```\n\n**Aftermath** is also available through [Carthage](https://github.com/Carthage/Carthage).\nTo install just write into your Cartfile:\n\n```ruby\ngithub \"hyperoslo/Aftermath\"\n```\n\n**Aftermath** can also be installed manually. Just download and drop `Sources`\nfolders in your project.\n\n## Examples\n\n- [iOS Playground](https://github.com/hyperoslo/Aftermath/blob/master/Playground-iOS.playground/Content.swift)\nuses live view of interactive playground to show how to fetch data from network\nand display it in the `UITableView`.\n\n- [AftermathNotes](https://github.com/hyperoslo/Aftermath/blob/master/Example/AftermathNotes)\nis a simple application that demonstrates how to setup networking stack and\ndata cache layer using **Aftermath**. It uses the concept of [stories](#stories)\nto group related types and make the `command -\u003e event` flow more readable.\n\n- [AftermathNotesPlus](https://github.com/hyperoslo/Aftermath/blob/master/Example/AftermathNotes)\nis a more advanced example that extends [AftermathNotes](https://github.com/hyperoslo/Aftermath/blob/master/Example/AftermathNotes)\ndemo. It plays with generics and introduces the concept of [features](#features)\nin order to reuse view controllers and RESTful network requests.\n\n## Extensions\n\nThis repository aims to be the core implementation of framework, but there are\nalso a range of extensions that integrate **Aftermath** with other libraries\nand extend it with more features:\n\n- [AftermathCompass](https://github.com/hyperoslo/AftermathCompass) is a\nmessage-driven routing system built on top of **Aftermath** and\n[Compass](https://github.com/hyperoslo/Compass).\n\n- [AftermathSpots](https://github.com/hyperoslo/AftermathSpots) is made to\nimprove development routines of building component-based UIs using\n[Spots](https://github.com/hyperoslo/Spots) cross-platform view controller\nframework. It comes with custom reactions and injectable behaviors that move\ncode reusability to the next level and make your application even more\ndecoupled and flexible.\n\n## Alternatives\n\nStill not sure about state management? It's not that easy to cover all\nscenarios and find a silver bullet for all occasions. But if you think it's\ntime to break conventions and try new architecture in your next application,\nthere are some links for further reading and research:\n\n- [ReSwift](https://github.com/ReSwift/ReSwift) - Unidirectional data flow in\nSwift, inspired by [Redux](http://redux.js.org).\n\n- [SwiftFlux](https://github.com/yonekawa/SwiftFlux) - A type-safe\n[Flux](https://facebook.github.io/flux/) implementation in Swift.\n\n- [fantastic-ios-architecture](https://github.com/onmyway133/fantastic-ios-architecture) -\nA list of resources related to iOS architecture topic.\n\n## Author\n\nHyper Interaktiv AS, ios@hyper.no\n\n## Influences\n\n**Aftermath** is inspired by the idea of unidirectional data flow in\n[Flux](https://facebook.github.io/flux/) and utilizes some concepts like\nsequence of commands and events from\n[Event Sourcing](http://martinfowler.com/eaaDev/EventSourcing.html).\n\n## Contributing\n\nWe would love you to contribute to **Aftermath**, check the [CONTRIBUTING](https://github.com/hyperoslo/Aftermath/blob/master/CONTRIBUTING.md)\nfile for more info.\n\n## License\n\n**Aftermath** is available under the MIT license. See the [LICENSE](https://github.com/hyperoslo/Aftermath/blob/master/LICENSE.md)\nfile for more info.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhyperoslo%2FAftermath","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhyperoslo%2FAftermath","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhyperoslo%2FAftermath/lists"}