{"id":16529328,"url":"https://github.com/glebec/batching-toposort","last_synced_at":"2025-03-21T09:31:50.024Z","repository":{"id":74731423,"uuid":"172175180","full_name":"glebec/batching-toposort","owner":"glebec","description":"Efficiently sort interdependent tasks into a sequence of concurrently-executable batches","archived":false,"fork":false,"pushed_at":"2021-11-12T03:15:56.000Z","size":343,"stargazers_count":27,"open_issues_count":1,"forks_count":3,"subscribers_count":0,"default_branch":"master","last_synced_at":"2024-10-12T17:44:26.424Z","etag":null,"topics":["algorithm","concurrency","dag","digraph","directed-acyclic-graph","graph","sort","toposort"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/glebec.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-02-23T05:26:21.000Z","updated_at":"2024-06-12T05:06:07.000Z","dependencies_parsed_at":"2023-03-02T12:15:24.372Z","dependency_job_id":null,"html_url":"https://github.com/glebec/batching-toposort","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/glebec%2Fbatching-toposort","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/glebec%2Fbatching-toposort/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/glebec%2Fbatching-toposort/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/glebec%2Fbatching-toposort/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/glebec","download_url":"https://codeload.github.com/glebec/batching-toposort/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":221813790,"owners_count":16884910,"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","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":["algorithm","concurrency","dag","digraph","directed-acyclic-graph","graph","sort","toposort"],"created_at":"2024-10-11T17:44:30.761Z","updated_at":"2024-10-28T09:46:56.933Z","avatar_url":"https://github.com/glebec.png","language":"JavaScript","readme":"# Batching-Toposort\n\n[![npm version](https://img.shields.io/npm/v/batching-toposort.svg?maxAge=3600)](https://www.npmjs.com/package/batching-toposort)\n[![Build Status](https://travis-ci.org/glebec/batching-toposort.svg?branch=master)](https://travis-ci.org/glebec/batching-toposort)\n[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)\n\nEfficiently sort interdependent tasks into a sequence of concurrently-executable batches.\n\n```hs\nbatchingToposort :: { DependencyId : [DependentId] } -\u003e [[TaskId]]\n```\n\n*   `O(t + d)` time complexity (for `t` tasks and `d` dependency relationships)\n*   `O(t)` space complexity\n*   Zero package dependencies\n*   Thoroughly tested (including [invariant tests](http://jsverify.github.io/#property-based-testing))\n*   Errors on cyclic graphs\n\n## Motivation\n\nOften one needs to schedule interdependent tasks. In order to determine task order, the classic solution is to use [topological sort](https://en.wikipedia.org/wiki/Topological_sorting). However, toposort typically outputs a list of individual tasks, without grouping those that can be executed concurrently. Batching-Toposort takes this additional consideration into account, producing a list of lists of tasks. The outer list is ordered; each inner list is unordered.\n\n## Usage\n\n```sh\nnpm install batching-toposort\n```\n\n\u003cimg width=\"250\" src=\"images/graph.png\"\u003e\n\nBatching-Toposort expects a [directed acyclic graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph) (DAG) implemented via [adjacency list](https://en.wikipedia.org/wiki/Adjacency_list). In other words, construct an object whose keys are dependency IDs, and whose values are lists of dependent IDs.\n\n```js\nconst batchingToposort = require('batching-toposort')\n\n// DAG :: { DependencyId : [DependentId] }\nconst DAG = {\n    a: ['c', 'f'], // `a` is a dependency of `c` and `f`\n    b: ['d', 'e'],\n    c: ['f'],\n    d: ['f', 'g'],\n    e: ['h'],\n    f: ['i'],\n    g: ['j'],\n    h: ['j'],\n    i: [],\n    j: [],\n}\n\n// batchingToposort :: DAG -\u003e [[TaskId]]\nconst taskBatches = batchingToposort(DAG)\n// [['a', 'b'], ['c', 'd', 'e'], ['f', 'g', 'h'], ['i', 'j']]\n```\n\n(If there is demand, Batching-Toposort may one day include a small DAG API and/or [DOT](\u003chttps://en.wikipedia.org/wiki/DOT_(graph_description_language)\u003e) support for convenience, but as of now it is the developer's role to construct the graph.)\n\n## Implementation\n\nIn short, Batching-Toposort adapts [Kahn's Algorithm](https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm) by inserting each round of root tasks into sublists rather than appending tasks directly to the main output list.\n\nThe classic DAG toposort keeps track of each task's in-degree (number of dependencies). As root tasks (those with no dependencies) are added to the output list, their dependents' in-degree counts are decremented. For a task to become a root, all of its dependencies must have been accounted for. The core algorithm is illustrated below in pseudocode (the actual implementation is in [`src/index.js`](src/index.js)).\n\n```\ngiven G = adjacency list of tasks and dependents (~O(1) lookup):\n\nlet N = map from tasks to in-degree counts (~O(1) lookup / update)\nlet L = [] (empty output list) (~O(1) append)\nlet R1 = list of root tasks (~O(1) addition, ~O(n) iteration)\n\nwhile R1 is nonempty\n    append R1 to L\n    let R2 = [] (empty list for next batch) (~O(1) append)\n    for each task T in R1\n        for each dependent D of T (as per G)\n            decrement in-degree count for D (in N)\n            if D's in-degree (as per N) is 0\n                add D to R2\n    R1 = R2\n\nreturn L\n```\n\n### Performance\n\nThe time complexity is `O(|V| + |E|)` for `V` task vertices and `E` dependency edges.\n\n*   The algorithm loops through rounds of roots, and every task is only a root only once, contributing to `O(|V|)` rounds (worst case is a linked list of tasks).\n*   Each round handles a disjoint set of dependency edges (those rooted in that round's tasks), so the `O(|E|)` handling of all edges is effectively distributed across rounds.\n*   Other operations, e.g. querying a node's in-degree (average case `O(1)`), are carefully managed to preserve the time complexity.\n\nThe space complexity is slightly better at `O(|V|)`.\n\n*   The in-degree map size is proportional to the number of vertices `|V|`, but not edges, as those are folded into an integer count during map construction.\n*   The output by definition contains `|V|` tasks (distributed among as many or fewer lists).\n*   Again, other operations are controlled to keep space complexity low.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fglebec%2Fbatching-toposort","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fglebec%2Fbatching-toposort","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fglebec%2Fbatching-toposort/lists"}