{"id":26730812,"url":"https://github.com/cevr/ftld","last_synced_at":"2025-08-02T06:10:47.689Z","repository":{"id":154788697,"uuid":"625713868","full_name":"cevr/ftld","owner":"cevr","description":"A pragmatic entry into a functional fantasy land.","archived":false,"fork":false,"pushed_at":"2025-01-14T16:15:52.000Z","size":847,"stargazers_count":57,"open_issues_count":0,"forks_count":4,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-06-27T23:15:55.973Z","etag":null,"topics":["functional","functional-programming","javascript","monad","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","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/cevr.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"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":"2023-04-10T00:06:15.000Z","updated_at":"2025-04-14T17:55:03.000Z","dependencies_parsed_at":"2024-05-08T23:31:21.447Z","dependency_job_id":"72a08d30-ab99-46bd-a709-124c4aa678e5","html_url":"https://github.com/cevr/ftld","commit_stats":null,"previous_names":[],"tags_count":84,"template":false,"template_full_name":null,"purl":"pkg:github/cevr/ftld","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cevr%2Fftld","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cevr%2Fftld/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cevr%2Fftld/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cevr%2Fftld/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cevr","download_url":"https://codeload.github.com/cevr/ftld/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cevr%2Fftld/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268340272,"owners_count":24234699,"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":["functional","functional-programming","javascript","monad","typescript"],"created_at":"2025-03-27T23:32:02.692Z","updated_at":"2025-08-02T06:10:47.663Z","avatar_url":"https://github.com/cevr.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"`ftld` is a small, focused, library that provides a set of functional primitives for building robust and resilient applications in TypeScript.\n\n[![ftld's badge](https://deno.bundlejs.com/?q=ftld\u0026badge=simple)](https://bundlejs.com/?q=ftld)\n\n# Why\n\nFunctional programming is a style of programming that emphasizes safety and composability. It's a powerful paradigm that can help you write more concise, readable, and maintainable code. However, it can be difficult to get started with functional programming in TypeScript. There are many libraries that provide functional programming primitives, but they often have a large API surface area and can be difficult to learn.\n\n`ftld` on the other hand is:\n\n- 🟢 tiny (4kb minified and gzipped)\n- 📦 tree-shakeable\n- 🕺 pragmatic\n- 🔍 focused (it provides a small set of primitives)\n- 🧠 easy to learn (it has a small API surface area)\n- 🎯 easy to use (it's written in TypeScript and has first-class support for TypeScript)\n- 🤝 easy to integrate\n- 🎉 provides all around great DX\n\n# What it's not\n\n`ftld` is not a replacement for a full-featured library like [Effect](https://www.effect.website/). I highly recommend checking out Effect if you're looking for a more comprehensive library. Many of the ideas in `ftld` were inspired directly by Effect.\n\n# Installation\n\n`ftld` is available as an npm package.\n\n```bash\nnpm install ftld\n```\n\n```bash\npnpm install ftld\n```\n\n# Usage\n\n`ftld` exports the following:\n\n- `Do`\n- `Option`\n- `Result`\n- `Task`\n\n## Option\n\nThe `Option` type is a useful way to handle values that might be absent. Instead of using `null` or `undefined`, which can lead to runtime errors, the `Option` type enforces handling the absence of a value at the type level. It provides a set of useful methods for working with optional values.\n\n`Option` can have one of two variants: `Some` and `None`. `Some` represents a value that exists, while `None` represents an absence of value.\n\n### Methods\n\n- `Option.from` - Creates an `Option` from a value that might be `null` or `undefined`.\n- `Option.fromPredicate` - Creates an `Option` from a predicate. Can narrow the type of the value.\n- `Option.tryCatch` - Creates an `Option` from a function that might throw an error.\n- `Option.Some` - Creates an `Option` from a value that exists.\n- `Option.None` - Creates an `Option` from a value that doesn't exist.\n- `Option.isSome` - Checks if an `Option` is `Some`.\n- `Option.isNone` - Checks if an `Option` is `None`.\n- `option.map` - Maps an `Option` to a new `Option` by applying a function to the value.\n- `option.flatMap` - Maps an `Option` to a new `Option` by applying a function to the value and flattening the result.\n- `option.tap` - Applies a side effect to the value of an `Option` if the it is a `Some` and returns the original `Option`.\n- `option.unwrap` - Unwraps an `Option` and returns the value, or throws an error if the `Option` is `None`.\n- `option.unwrapOr` - Unwraps an `Option` and returns the value, or returns a default value if the `Option` is `None`.\n- `option.match` - Matches an `Option` to a value based on whether it is `Some` or `None`.\n\n```ts\nconst someValue: Option\u003cnumber\u003e = Option.Some(42);\n\n// Map a value\nconst doubled: Option\u003cnumber\u003e = someValue.map((x) =\u003e x * 2);\nconsole.log(doubled.unwrap()); // 84\n\n// FlatMap a value\nconst flatMapped: Option\u003cnumber\u003e = someValue.flatMap((x) =\u003e Option.Some(x * 2));\nconsole.log(flatMapped.unwrap()); // 84\n\n// Unwrap a value, or provide a default\nconst defaultValue = 0;\nconst unwrappedOr: number = someValue.unwrapOr(defaultValue);\nconsole.log(unwrappedOr); // 42\n\n// better yet - pattern match it!\nconst value: number = someValue.match({\n  Some: (x) =\u003e x,\n  None: () =\u003e 0,\n});\n```\n\n### Collection Methods\n\nThe `Option` type also provides a set of collection methods that can be used to work with arrays of `Option` values.\n\n- `traverse`\n- `all`\n- `any`\n\n#### Traverse\n\n`traverse` is used when you have a collection of values and a function that transforms each value into an `Option`. It applies the function to each element of the array and combines the resulting `Option` values into a single `Option` containing an array of the transformed values, if all the values were `Some`. If any of the values are `None`, the result will be a `None`.\n\nHere's an example using traverse:\n\n```ts\nimport { Option } from \"./option\";\n\nconst values = [1, 2, 3, 4, 5];\n\nconst isEven = (x) =\u003e x % 2 === 0;\nconst toEvenOption = (x) =\u003e (isEven(x) ? Option.Some(x) : Option.None());\n\nconst traversed: Option\u003cnumber[]\u003e = Option.traverse(values, toEvenOption);\n\nconsole.log(traversed); // None, since not all values are even\n```\n\nIn this example, we use the traverse function to apply toEvenOption to each value in the values array. Since not all values are even, the result is `None`.\n\n#### all\n\n`all` is used when you have an array of `Option` values and you want to combine them into a single `Option` containing an array of the unwrapped values, if all the values are `Some`. If any of the values are `None`, the result will be a `None`.\n\nHere's an example using all:\n\n```ts\nimport { Option } from \"./option\";\n\nconst options = [\n  Option.Some(1),\n  Option.Some(2),\n  Option.None(),\n  Option.Some(4),\n  Option.Some(5),\n];\n\nconst option: Option\u003cnumber[]\u003e = Option.all(options);\n\nconsole.log(option); // None, since there's a None value in the array\n```\n\nIn this example, we use the `all` function to combine the options array into a single `Option`. Since there's a `None` value in the array, the result is `None`.\n\nIn summary, `traverse` is used when you have an array of values and a function that turns each value into an `Option`, whereas `all` is used when you already have an array of `Option` values. Both functions return an `Option` containing an array of unwrapped values if all values are `Some`, or a `None` if any of the values are None.\n\n#### Any\n\n`any` is used when you have an array of `Option` values and you want to check if any of the values are `Some`. It returns the first `Some` value it finds, or `None` if none of the values are `Some`.\n\nHere's an example using `any`:\n\n```ts\nimport { Option } from \"ftld\";\n\nconst options = [\n  Option.Some(1),\n  Option.Some(2),\n  Option.None(),\n  Option.Some(4),\n  Option.Some(5),\n];\n\nconst any: Option\u003cnumber\u003e = Option.any(options);\n\nconsole.log(any); // Some(1)\n```\n\n### Error Handling\n\nThe `tryCatch` function allows you to safely execute a function that might throw an error, converting the result into an `Option`.\n\n```ts\nlet someCondition = true;\nlet value = 42;\ntype Value = number;\nconst tryCatchResult: Option\u003cValue\u003e = Option.tryCatch(() =\u003e {\n  if (someCondition) throw new Error(\"Error message\");\n  return value;\n});\nconsole.log(tryCatchResult.isNone()); // true\n```\n\n## Result\n\nThe `Result` type is a useful way to handle computations that may error. Instead of callbacks or throw expressions, which are indirect and can cause confusion, the `Result` type enforces handling the presence of an error at the type level. It provides a set of useful methods for working with this form of branching logic.\n\n`Result` can have one of two variants: `Ok` and `Err`. `Ok` represents the result of a computation that has succeeded, while `Err` represents the result of a computation that has failed.\n\n### Methods\n\n- `Result.from` - Converts value to a `Result`.\n- `Result.fromPredicate` - Creates a `Result` from a predicate. Can narrow the type of the value.\n- `Result.tryCatch` - Converts a value based on a computation that may throw.\n- `Result.isOk` - Returns true if the result is `Ok`.\n- `Result.isErr` - Returns true if the result is `Err`.\n- `Result.Ok` - Creates an `Ok` instance.\n- `Result.Err` - Creates an `Err` instance.\n- `result.map` - Maps a value.\n- `result.flatMap` - Maps the value over a function returning a new Result.\n- `result.recover` - Maps the error over a function returning a new Result.\n- `result.unwrap` - Unwraps a value. Throws if the result is `Err`.\n- `result.unwrapOr` - Unwraps a value, or provides a default.\n- `result.unwrapErr` - Unwraps an error. Throws if the result is `Ok`.\n- `result.tap` - Executes a side effect.\n- `result.tapErr` - Executes a side effect if the result is `Err`.\n- `result.settle` - converts a result to a object representing the result of a computation.\n\n```ts\nconst result: Result\u003cstring, number\u003e = Result.Ok\u003cstring, number\u003e(42);\n\n// Map a value\nconst doubled: Result\u003cstring, number\u003e = result.map((x) =\u003e x * 2);\nconsole.log(doubled.unwrap()); // 84\n\n// FlatMap a value\nconst flatMapped: Result\u003cstring, number\u003e = result.flatMap((x) =\u003e\n  Result.Ok(x * 2)\n);\nconsole.log(flatMapped.unwrap()); // 84\n\n// Unwrap a value, or provide a default\nconst defaultValue = 0;\nconst unwrappedOr: number = result.unwrapOr(defaultValue);\nconsole.log(unwrappedOr); // 42\n\n// better yet - pattern match\nconst value: number = result.match({\n  Ok: (x) =\u003e x,\n  Err: (x) =\u003e 0,\n});\n```\n\n### Collection Methods\n\nThe `Result` type also provides a set of methods for working with arrays of `Result` values:\n\n- `traverse`\n- `all`\n- `any`\n- `coalesce`\n- `validate`\n- `settle`\n\n#### Traverse\n\n```ts\nconst values = [1, 2, 3, 4, 5];\n\nconst isEven = (x) =\u003e x % 2 === 0;\nconst toEvenResult = (x) =\u003e\n  isEven(x)\n    ? Result.Ok\u003cstring, number\u003e(x)\n    : Result.Err\u003cstring, number\u003e(\"Value is not even\");\n\nconst traversed: Result\u003cstring, number[]\u003e = Result.traverse(\n  values,\n  toEvenResult\n);\n\nconsole.log(traversed); // Err('Value is not even'), since not all values are even\n```\n\nIn this example, we use the traverse function to apply `toEvenResult` to each value in the values array. Since not all values are even, the result is `Err`.\n\n#### all\n\n```ts\nconst results = [\n  Result.Ok\u003cstring, number\u003e(1),\n  Result.Ok\u003cstring, number\u003e(2),\n  Result.Err\u003cstring, number\u003e(\"oops!\"),\n  Result.Ok\u003cstring, number\u003e(4),\n  Result.Ok\u003cstring, number\u003e(5),\n];\n\nconst result: Result\u003cstring, number[]\u003e = Result.all(results);\n\nconsole.log(result); // Err('oops!'), since there's an Err value in the array\n```\n\n#### Any\n\n`any` is used when you have an array of `Result` values and you want to check if any of the values are `Ok`. It returns the first `Ok` value it finds, or `Err` if none of the values are `Ok`.\n\nHere's an example using `any`:\n\n```ts\nimport { Result } from \"ftld\";\n\nconst results = [\n  Result.Ok\u003cstring, number\u003e(1),\n  Result.Ok\u003cstring, number\u003e(2),\n  Result.Err\u003cstring, number\u003e(\"oops!\"),\n  Result.Ok\u003cstring, number\u003e(4),\n  Result.Ok\u003cstring, number\u003e(5),\n];\n\nconst any: Result\u003cstring, number\u003e = Result.any(results);\n\nconsole.log(any); // Ok(1)\n```\n\n#### Coalesce\n\n`coalesce` is used when you have an array of `Result` values and you want to convert them into a single `Result` value while also keeping each error. It aggregates both the errors and the values into a single `Result` value.\n\nHere's an example using `coalesce`:\n\n```ts\nimport { Result } from \"ftld\";\n\nconst results = [\n  Result.Ok\u003cstring, number\u003e(1),\n  Result.Err\u003cSomeError, number\u003e(new SomeError()),\n  Result.Err\u003cOtherError, number\u003e(new OtherError()),\n  Result.Ok\u003cstring, number\u003e(4),\n  Result.Ok\u003cstring, number\u003e(5),\n];\n\nconst coalesced: Result\u003c(SomeError | OtherError | string)[], number[]\u003e =\n  Result.coalesce(results);\n\nconsole.log(coalesced); // Err([new SomeError(), new OtherError()])\n```\n\n#### Validate\n\n`validate` is used when you have an array of results with the same Ok value and you want to convert them into a single `Result` value. It aggregates the errors and the first Ok value into a single `Result` value.\n\nIt's similar to `coalesce`, but it only returns the first Ok value if there are no errors, rather than aggregating all of them.\n\nHere's an example using `validate`:\n\n```ts\nimport { Result } from \"ftld\";\n\nconst value = 2;\n\nconst isEven = (x) =\u003e x % 2 === 0;\nconst isPositive = (x) =\u003e x \u003e 0;\n\nconst validations = [\n  Result.fromPredicate(value, isEven, (value) =\u003e new NotEvenError(value)),\n  Result.fromPredicate(\n    value,\n    isPositive,\n    (value) =\u003e new NotPositiveError(value)\n  ),\n];\n\nconst validated: Result\u003c(NotEvenError | NotPositiveError)[], number\u003e =\n  Result.validate(validations);\n\nconsole.log(validated); // Ok(2)\n```\n\n#### Settle\n\n`settle` is special in that it does not return a Result. Instead it returns a collection of `SettledResult` values, which are either `{type: \"Ok\", value: T}` or `{type: \"Error\", error: E}`. This is useful when you want to handle both the Ok and Err cases, but don't want to aggregate them into a single `Result` value.\n\n```ts\nimport { Result, SettledResult } from \"ftld\";\n\nconst results = [\n  Result.Ok\u003cstring, number\u003e(1),\n  Result.Err\u003cSomeError, number\u003e(new SomeError()),\n  Result.Err\u003cOtherError, number\u003e(new OtherError()),\n  Result.Ok\u003cstring, number\u003e(4),\n  Result.Ok\u003cstring, number\u003e(5),\n];\n\nconst settled: SettledResult\u003cSomeError | OtherError, number\u003e[] =\n  Result.settle(results); // [{type: \"Ok\", value: 1}, {type: \"Err\", error: new SomeError()}, {type: \"Err\", error: new OtherError()}, {type: \"Ok\", value: 4}, {type: \"Ok\", value: 5}]\n```\n\n### Error Handling\n\nThe `tryCatch` function allows you to safely execute a function that might throw an error, converting the result into an `Result`.\n\n```ts\nconst tryCatchResult: Result\u003cError, never\u003e = Result.tryCatch(() =\u003e {\n  throw new Error('Error message');\n}, (error) =\u003e error as Error));\nconsole.log(tryCatchResult.isErr()); // true\n```\n\n## Task\n\n`Task` represents a lazy computation that may fail. It will always return a `Result` value, either `Ok` or `Err`. If the computation is asynchronous, running it (`.run`) will return a `Promise` that resolves to a `Result` value. This means a task can be synchronous or asynchronous.\n\n\u003e Key differences to `Promise`:\n\u003e\n\u003e - `Task` is lazy, meaning it won't start executing until you call `run` or await it.\n\u003e - `Task` will never throw an error, instead it will return an `Err` value.\n\n### Usage\n\nHere are some examples of how to use the `Task` type and its utility functions:\n\n```typescript\nimport { Task, unknown } from \"ftld\";\n\nconst task: AsyncTask\u003cunknown, number\u003e = Task.from(async () =\u003e {\n  return 42;\n});\nconsole.log(await task.run()); // Result.Ok(42)\n\nconst errTask: SyncTask\u003cstring, never\u003e = Result.Err(\"oops\");\n\nconst res: Result\u003cstring, never\u003e = errTask.run();\n\nconsole.log(res.isErr()); // true\n```\n\n### Methods\n\n- `Task.from` - Creates a `Task` from a `Promise` or a function that returns a `Promise`.\n- `Task.fromPredicate` - Creates a `Task` from a predicate function. Can narrow the type of the value.\n- Task.sleep - Creates a `Task` that resolves after a specified number of milliseconds.\n- `task.map` - Maps the value of a `Task` to a new value.\n- `task.mapErr` - Maps the error of a `Task` to a new error.\n- `task.flatMap` - Maps the value of a `Task` to a new `Task`.\n- `task.recover` - Maps the error of a `Task` to a new `Task`.\n- `task.tap` - Runs a function on the value of a `Task` without changing the value.\n- `task.tapErr` - Runs a function on the error of a `Task` without changing the error.\n- `task.run` - Runs the `Task` and returns a `Promise` that resolves to a `Result`.\n- `task.match` - Runs an object of cases against the `Result` value of a `Task`.\n- `task.schedule` - Schedules the `Task` by the provided options. This always returns an asynchronous `Task`.\n\n```ts\nconst someValue: Result\u003cunknown, number\u003e = await Task.from(\n  async () =\u003e 42\n).run();\nconst someOtherValue: Result\u003cunknown, number\u003e = await Task.from(\n  async () =\u003e 84\n).run();\n\n// Map a value\nconst doubled: SyncTask\u003cunknown, number\u003e = Task.from(42).map((x) =\u003e x * 2);\n// you can also call .run() to get the Promise as well\nconsole.log(doubled.run()); // Result.Ok(84)\n\nconst flatMapped: SyncTask\u003cunknown, number\u003e = Task.from(42).flatMap((x) =\u003e\n  Task.from(x * 2)\n);\nconsole.log(flatMapped.run()); // 84\n\n// if the task is syncronous - you can use unwrap like you would with a Result\nconst result: SyncTask\u003cunknown, number\u003e = Task.from(42);\nconsole.log(result); // Result.Ok(42)\nconsole.log(result.unwrap()); // 42\n```\n\n### Scheduling\n\nThe `Task` instance also allows for managing the scheduling of the computation.\nThe result of a scheduled task is always asynchronous.\nIt provides the following options:\n\n- `timeout`: The number of milliseconds to wait before timing out the task.\n- `delay`: The number of milliseconds to delay the execution of the task.\n- `retry`: The number of times to retry the task if it fails.\n- `repeat`: The number of times to repeat the task if it succeeds.\n\nEach option (except `timeout`) can be a number, boolean, or a function that returns a number or boolean or even a promise that resolves to a number or boolean.\n\n```ts\nimport { Task, TaskTimeoutError, TaskSchedulingError } from \"ftld\";\n\nconst task: SyncTask\u003cError, number\u003e = Task.from(() =\u003e {\n  if (Math.random() \u003e 0.5) {\n    return 42;\n  } else {\n    throw new Error(\"oops\");\n  }\n});\n\nconst delayed: AsyncTask\u003cError, number\u003e = task.schedule({\n  delay: 1000,\n});\n\nconst timedOut: AsyncTask\u003cError | TaskTimeoutError, number\u003e = task.schedule({\n  timeout: 1000,\n});\n\nconst retried: AsyncTask\u003cError, number\u003e = task.schedule({\n  retry: 3,\n});\n\nconst customRetry: AsyncTask\u003cError | TaskSchedulingError, number\u003e = task.schedule({\n  retry: (attempt, err) =\u003e {\n    if (err instanceof Error) {\n      return 3;\n    }\n    return 0;\n  },\n});\n\nconst exponentialBackoff: AsyncTask\u003cError | TaskSchedulingError, number\u003e = task.schedule({\n  retry: 5,\n  delay: (retryAttempt) =\u003e 2 ** retryAttempt * 1000,\n});\n\nconst repeated: AsyncTask\u003cError, number\u003e = task.schedule({\n  repeat: 3,\n});\n\nconst customRepeat: AsyncTask\u003cError | TaskSchedulingError, number\u003e = task.schedule({\n  repeat: (attempt, value) =\u003e {\n    if (value === 42) {\n      return 3;\n    }\n    return false;\n  },\n});\n\n// both repeat/retry can take a promise as well\nconst repeatUntil: AsyncTask\u003cError | TaskSchedulingError, number\u003e = task.schedule({\n  retry: async (attempt, err) =\u003e {\n    retrun await shouldRetry();\n  },\n  repeat: async (attempt, value) =\u003e {\n    return await jobIsDone();\n  },\n});\n```\n\n### Collection Methods\n\nThe `Task` type provides several methods for working with arrays of `Task` values:\n\n- `traverse`\n- `traversePar`\n- `any`\n- `sequential`\n- `parallel`\n- `race`\n- `coalesce`\n- `coalescePar`\n- `settle`\n- `settlePar`\n\n#### Parallel\n\n`parallel` allows you to run multiple tasks in parallel and combine the results into a single `Task` containing an array of the unwrapped values, if all the tasks were successful. If any of the tasks fail, the result will be a `Err`. This is always asynchronous.\n\nHere's an example using parallel:\n\n```ts\nconst tasks = [\n  Task.sleep(1000).map(() =\u003e 1),\n  Task.sleep(1000).map(() =\u003e 2),\n  Task.sleep(1000).map(() =\u003e 3),\n  Task.sleep(1000).map(() =\u003e 4),\n  Task.sleep(1000).map(() =\u003e 5),\n];\n\nconst parallel: AsyncTask\u003cunknown, number[]\u003e = Task.parallel(tasks);\n\nconsole.log(await parallel.run()); // Result.Ok([1, 2, 3, 4, 5])\n```\n\nin this example, we use the `parallel` function to run all tasks in parallel and combine the results into a single `Task`. Since all tasks are successful, the result is `Ok`.\n\n#### Sequential\n\n`sequential` allows you to run multiple tasks sequentially and combine the results into a single `Task` containing an array of the unwrapped values, if all the tasks were successful. If any of the tasks fail, the result will be a `Err`. This is synchronous if all tasks are synchronous.\n\nHere's an example using sequential:\n\n```ts\nconst syncTasks = [\n  Task.from(() =\u003e 1),\n  Task.from(() =\u003e 2),\n  Task.from(() =\u003e 3),\n  Task.from(() =\u003e 4),\n  Task.from(() =\u003e 5),\n];\nconst asyncTasks = [\n  Task.sleep(1000).map(() =\u003e 1),\n  Task.sleep(1000).map(() =\u003e 2),\n  Task.sleep(1000).map(() =\u003e 3),\n  Task.sleep(1000).map(() =\u003e 4),\n  Task.sleep(1000).map(() =\u003e 5),\n];\n\nconst sync: SyncTask\u003cunknown, number[]\u003e = Task.sequential(syncTasks);\nconst async: AsyncTask\u003cunknown, number[]\u003e = Task.sequential(asyncTasks);\n\nconsole.log(sync.run()); // Result.Ok([1, 2, 3, 4, 5])\nconsole.log(await async.run()); // Result.Ok([1, 2, 3, 4, 5])\n```\n\n#### Race\n\n`race` allows you to run multiple tasks in parallel and combine the results into a single `Task` containing the unwrapped value of the first settled task. This is always asynchronous.\n\n```ts\nconst tasks = [\n  Task.sleep(1000).map(() =\u003e 1),\n  Task.sleep(500).map(() =\u003e 2),\n  Task.sleep(2000).map(() =\u003e 3),\n  Task.sleep(10).flatMap(() =\u003e Result.Err(new Error(\"oops\"))),\n];\n\nconst res: AsycTask\u003cError, number\u003e = Task.race(tasks);\n\nconsole.log(await res.run()); // Result.Err(Error('oops!'))\n```\n\n#### Traverse\n\n`traverse` allows you convert items in a collection into a collection of tasks sequentially and combine the results into a single `Task` containing an array of the unwrapped values, if all the tasks were successful. If any of the tasks fail, the result will be a `Err`. This is synchronous if all tasks are synchronous.\n\n```ts\nconst makeAsyncTask = (x: number) =\u003e Task.sleep(x * 2).map(() =\u003e x * 2);\nconst makeSyncTask = (x: number) =\u003e Task.from(() =\u003e x * 2);\nconst async: AsyncTask\u003cunknown, number[]\u003e = Task.traverse(\n  [1, 2, 3, 4, 5],\n  makeAsyncTask\n);\nconst sync: SyncTask\u003cunknown, number[]\u003e = Task.traverse(\n  [1, 2, 3, 4, 5],\n  makeSyncTask\n);\n\nconsole.log(await async.run()); // Result.Ok([2, 4, 6, 8, 10])\nconsole.log(sync.run()); // Result.Ok([2, 4, 6, 8, 10])\n```\n\n#### TraversePar\n\nThe parallel version of `traverse`. This is always asynchronous.\n\n```ts\nconst traversePar: AsyncTask\u003cunknown, number[]\u003e = Task.traversePar(\n  [1, 2, 3, 4, 5],\n  (x) =\u003e Task.sleep(x * 2).map(() =\u003e x * 2)\n);\n\nconsole.log(await traversePar.run()); // Result.Ok([2, 4, 6, 8, 10])\n```\n\n#### Any\n\n`any` allows you to take a collection of tasks and find the first successful task. If all tasks fail, the result will be a `Err`. This is synchronous if all tasks are synchronous.\n\n```ts\nconst syncTasks = [\n  Task.from(() =\u003e Result.Err(new Error(\"oops\"))),\n  Task.from(() =\u003e Result.Err(new Error(\"oops\"))),\n  Task.from(() =\u003e 3),\n  Task.from(() =\u003e 4),\n  Task.from(() =\u003e 5),\n];\nconst asyncTasks = [\n  Task.sleep(1000).flatMap(() =\u003e Result.Err(new Error(\"oops\"))),\n  Task.sleep(1000).flatMap(() =\u003e Result.Err(new Error(\"oops\"))),\n  Task.sleep(1000).map(() =\u003e 3),\n  Task.sleep(1000).map(() =\u003e 4),\n  Task.sleep(1000).map(() =\u003e 5),\n];\n\nconst asyncAny: AsyncTask\u003cError, number\u003e = Task.any(asyncTasks);\nconst syncAny: SyncTask\u003cError, number\u003e = Task.any(syncTasks);\n\nconsole.log(await asyncAny.run()); // Result.Ok(3)\nconsole.log(sync.run()); // Result.Ok(3)\n```\n\n#### Coalesce\n\n`coalesce` allows you to take a collection of tasks and aggregate the results into a single Task. If any tasks fail, the result will be a `Err`, with a collection of all the errors. This is synchronous if all tasks are synchronous.\n\n```ts\nconst syncTasks = [\n  Task.from(() =\u003e Result.Err(new SomeError())),\n  Task.from(() =\u003e Result.Err(new OtherError())),\n  Task.from(() =\u003e 3),\n  Task.from(() =\u003e 4),\n  Task.from(() =\u003e 5),\n];\nconst asyncTasks = [\n  Task.sleep(1000).flatMap(() =\u003e Result.Err(new SomeError())),\n  Task.sleep(1000).flatMap(() =\u003e Result.Err(new OtherError())),\n  Task.sleep(1000).map(() =\u003e 3),\n  Task.sleep(1000).map(() =\u003e 4),\n  Task.sleep(1000).map(() =\u003e 5),\n];\n\nconst asyncCoalesce: AsyncTask\u003c(SomeError | OtherError)[], number[]\u003e =\n  Task.coalesce(asyncTasks);\nconst syncCoalesce: SyncTask\u003c(SomeError | OtherError)[], number[]\u003e =\n  Task.coalesce(syncTasks);\n\nconsole.log(await coalesce.run()); // Result.Err([SomeError, OtherError])\nconsole.log(sync.run()); // Result.Err([SomeError, OtherError])\n```\n\n#### CoalescePar\n\nThe parallel version of `coalesce`. This is always asynchronous.\n\n```ts\nconst tasks = [\n  Task.sleep(1000).flatMap(() =\u003e Result.Err(new SomeError())),\n  Task.sleep(1000).flatMap(() =\u003e Result.Err(new OtherError())),\n  Task.sleep(1000).map(() =\u003e 3),\n  Task.sleep(1000).map(() =\u003e 4),\n  Task.sleep(1000).map(() =\u003e 5),\n];\n\nconst coalescePar: AsyncTask\u003c(SomeError | OtherError)[], number[]\u003e =\n  Task.coalescePar(tasks);\n\nconsole.log(await coalescePar.run()); // Result.Err([SomeError, OtherError])\n```\n\n#### Settle\n\n`settle` allows you to take a collection of tasks and aggregate the results into a `SettledTask`, similar to the `Result` type. This is synchronous if all tasks are synchronous.\n\n```ts\nimport { Task, SettledResult } from \"ftld\";\n\nconst syncTasks = [\n  Task.from(() =\u003e Result.Err(new SomeError())),\n  Task.from(() =\u003e Result.Err(new OtherError())),\n  Task.from(() =\u003e 3),\n  Task.from(() =\u003e 4),\n  Task.from(() =\u003e 5),\n];\nconst asyncTasks = [\n  Task.sleep(1000).flatMap(() =\u003e Result.Err(new SomeError())),\n  Task.sleep(1000).flatMap(() =\u003e Result.Err(new OtherError())),\n  Task.sleep(1000).map(() =\u003e 3),\n  Task.sleep(1000).map(() =\u003e 4),\n  Task.sleep(1000).map(() =\u003e 5),\n];\n\nconst asyncSettled: SettledResult\u003cSomeError | OtherError | Error, number\u003e[] =\n  await Task.settle(asyncTasks);\nconst syncSettled: SettledResult\u003cSomeError | OtherError | Error, number\u003e[] =\n  Task.settle(syncTasks);\n```\n\n#### SettlePar\n\nThe parallel version of `settle`. This is always asynchronous.\n\n```ts\nimport { Task, SettledResult } from \"ftld\";\n\nconst tasks = [\n  Task.sleep(1000).flatMap(() =\u003e Result.Err(new SomeError())),\n  Task.sleep(1000).flatMap(() =\u003e Result.Err(new OtherError())),\n  Task.sleep(1000).map(() =\u003e 3),\n  Task.sleep(1000).map(() =\u003e 4),\n  Task.sleep(1000).map(() =\u003e 5),\n];\n\nconst settle: SettledResult\u003cSomeError | OtherError | Error, number\u003e[] =\n  await Task.settlePar(tasks);\n```\n\n## Do\n\n`Do` is a utility that allows you to unwrap monadic values in a synchronous manner. Provides the same benefits as async/await but for all types in ftld, albeit with a more cumbersome syntax.\n\nIt handles `Task`, `Result`, `Option`, and even `Promise` types. It always returns a `Task`, which will be synchronous if all the computations are synchronous, or asynchronous if any of the computations are asynchronous.\n\n```ts\nimport { Do, Task, Result, UnwrapNoneError, unknown } from \"ftld\";\n\n// without Do you get nesting hell\nfunction doSomething(): SyncTask\u003cunknown, unknown\u003e {\n  return Task.from(() =\u003e {\n    //...\n  }).flatMap(() =\u003e {\n    //...\n    return Task.from().flatMap(() =\u003e {\n      //...\n      return Task.flatMap(() =\u003e {\n        //...\n        return Task.from().flatMap(() =\u003e {\n          //...\n        });\n      });\n    });\n  });\n}\n\n// if there are any async computations, it will return an Async Task\nfunction doSomething(): AsyncTask\u003c\n  SomeError | OtherError | UnwrapNoneError, // \u003c-- notice how the Option type has an error type\n  number\n\u003e {\n  return Do(function* () {\n    const a: number = yield* Result.from(\n      () =\u003e 1,\n      () =\u003e new SomeError()\n    );\n\n    // async!\n    const b: number = yield* Task.from(\n      async () =\u003e 2,\n      () =\u003e new OtherError()\n    );\n\n    const c: number = yield* Option.from(3 as number | null);\n\n    return a + b + c;\n  });\n}\n\n// if there are no async computations, it will return a sync Task\nfunction doSomething(): SyncTask\u003c\n  SomeError | OtherError | UnwrapNoneError,\n  number\n\u003e {\n  return Do(function* ($) {\n    const a: number = yield* Result.from(\n      () =\u003e 1,\n      () =\u003e new SomeError()\n    );\n\n    const b: number = yield* Result.from(\n      () =\u003e 2,\n      () =\u003e new OtherError()\n    );\n\n    const c: number = yield* Option.from(3 as number | null);\n\n    return a + b + c;\n  });\n}\n```\n\n## Recipes\n\nHere's a list of useful utilities, but don't justify an increase in bundle size.\n\n### Wrapping Zod\n\nIt's common to want to wrap a validation library like Zod in a Result type. Here's an example of how to do that:\n\n```ts\nimport { Result } from \"ftld\";\nimport { z } from \"zod\";\n\nexport const wrapZod =\n  \u003cT extends z.Schema\u003e(schema: T) =\u003e\n  \u003cE = z.ZodIssue[]\u003e(\n    value: unknown,\n    onErr: (issues: z.ZodIssue[]) =\u003e E = (issues) =\u003e issues as E\n  ): Result\u003cE, z.infer\u003cT\u003e\u003e =\u003e {\n    const res = schema.safeParse(value);\n    if (res.success) {\n      return Result.Ok(res.data);\n    }\n    return Result.Err(onErr(res.error.errors));\n  };\n\nconst emailSchema = wrapZod(z.string().email());\n\nconst email: Result\u003cz.ZodIssue[], string\u003e = emailSchema(\"test\");\nconst emailWithCustomError: Result\u003cCustomError, string\u003e = emailSchema(\n  \"test\",\n  () =\u003e new CustomError()\n);\n```\n\n### Taskify\n\nYou might have an API (like node:fs) that uses promises, but you want to use Tasks instead. You can create a `taskify` function to convert a promise-based API into a Task-based API.\n\n```ts\nimport { Task } from \"ftld\";\nimport * as fs from \"fs/promises\";\n\ntype Taskify = {\n  // this is so we preserve the types of the original api if it includes overloads\n  \u003cA extends Record\u003cstring, unknown\u003e\u003e(obj: A): {\n    [K in keyof A]: A[K] extends {\n      (...args: infer P1): infer R1;\n      (...args: infer P2): infer R2;\n      (...args: infer P3): infer R3;\n    }\n      ? {\n          (...args: P1): R1 extends Promise\u003cinfer RP1\u003e\n            ? AsyncTask\u003cunknown, RP1\u003e\n            : SyncTask\u003cunknown, R1\u003e;\n          (...args: P2): R2 extends Promise\u003cinfer RP2\u003e\n            ? AsyncTask\u003cunknown, RP2\u003e\n            : SyncTask\u003cunknown, R2\u003e;\n          (...args: P3): R3 extends Promise\u003cinfer RP3\u003e\n            ? AsyncTask\u003cunknown, RP3\u003e\n            : SyncTask\u003cunknown, R3\u003e;\n        }\n      : A[K] extends {\n          (...args: infer P1): infer R1;\n          (...args: infer P2): infer R2;\n        }\n      ? {\n          (...args: P1): R1 extends Promise\u003cinfer RP1\u003e\n            ? AsyncTask\u003cunknown, RP1\u003e\n            : SyncTask\u003cunknown, R1\u003e;\n          (...args: P2): R2 extends Promise\u003cinfer RP2\u003e\n            ? AsyncTask\u003cunknown, RP2\u003e\n            : SyncTask\u003cunknown, R2\u003e;\n        }\n      : A[K] extends { (...args: infer P1): infer R1 }\n      ? {\n          (...args: P1): R1 extends Promise\u003cinfer RP1\u003e\n            ? AsyncTask\u003cunknown, RP1\u003e\n            : SyncTask\u003cunknown, R1\u003e;\n        }\n      : A[K];\n  } \u0026 {};\n\n  \u003c\n    A extends {\n      (...args: any[]): any;\n      (...args: any[]): any;\n      (...args: any[]): any;\n    }\n  \u003e(\n    fn: A\n  ): A extends {\n    (...args: infer P1): infer R1;\n    (...args: infer P2): infer R2;\n    (...args: infer P3): infer R3;\n  }\n    ? {\n        (...args: P1): R1 extends Promise\u003cinfer RP1\u003e\n          ? AsyncTask\u003cunknown, RP1\u003e\n          : SyncTask\u003cunknown, R1\u003e;\n        (...args: P2): R2 extends Promise\u003cinfer RP2\u003e\n          ? AsyncTask\u003cunknown, RP2\u003e\n          : SyncTask\u003cunknown, R2\u003e;\n        (...args: P3): R3 extends Promise\u003cinfer RP3\u003e\n          ? AsyncTask\u003cunknown, RP3\u003e\n          : SyncTask\u003cunknown, R3\u003e;\n      }\n    : never;\n  \u003c\n    A extends {\n      (...args: any[]): any;\n      (...args: any[]): any;\n    }\n  \u003e(\n    fn: A\n  ): A extends {\n    (...args: infer P1): infer R1;\n    (...args: infer P2): infer R2;\n  }\n    ? {\n        (...args: P1): R1 extends Promise\u003cinfer RP1\u003e\n          ? AsyncTask\u003cunknown, RP1\u003e\n          : SyncTask\u003cunknown, R1\u003e;\n        (...args: P2): R2 extends Promise\u003cinfer RP2\u003e\n          ? AsyncTask\u003cunknown, RP2\u003e\n          : SyncTask\u003cunknown, R2\u003e;\n      }\n    : never;\n  \u003c\n    A extends {\n      (...args: any[]): any;\n    }\n  \u003e(\n    fn: A\n  ): A extends {\n    (...args: infer P1): infer R1;\n  }\n    ? {\n        (...args: P1): R1 extends Promise\u003cinfer RP1\u003e\n          ? AsyncTask\u003cunknown, RP1\u003e\n          : SyncTask\u003cunknown, R1\u003e;\n      }\n    : never;\n};\n\nconst taskify: Taskify = (fnOrRecord: any): any =\u003e {\n  if (fnOrRecord instanceof Function) {\n    return (...args: any[]) =\u003e {\n      return Task.from(() =\u003e fnOrRecord(...args));\n    };\n  }\n\n  return Object.fromEntries(\n    Object.entries(fnOrRecord).map(([key, value]) =\u003e {\n      if (value instanceof Function) {\n        return [\n          key,\n          (...args: any[]) =\u003e {\n            return Task.from(() =\u003e value(...args));\n          },\n        ];\n      }\n      return [key, value];\n    })\n  );\n};\n\n// usage\nconst readFile = taskify(fs.readFile);\n// overloads preserved!\nreadFile(\"path\", \"utf8\")\n  .map((content) =\u003e {\n    return content.toUpperCase();\n  })\n  .run();\nconst taskFs = taskify(fs);\n// overloads preserved!\ntaskFs\n  .readFile(\"path\", \"utf8\")\n  .map((string) =\u003e string.toUpperCase())\n  .run();\n```\n\n### Brand\n\nThe `Brand` type is a wrapper around a value that allows you to create a new type from an existing type. It's useful for creating new types that are more specific than the original type, such as `Email` or `Password`.\n\n```ts\n// credit to EffectTs/Data/Brand\nimport { Result } from \"./result\";\n\n// @ts-expect-error\nexport const Brand: {\n  /**\n   * Create a validated brand constructor that checks the value using the provided validation function.\n   */\n  \u003cE, TBrand\u003e(\n    validate: (value: Unbrand\u003cTBrand\u003e) =\u003e boolean,\n    onErr: (value: Unbrand\u003cTBrand\u003e) =\u003e E\n  ): ValidatedBrandConstructor\u003cE, TBrand\u003e;\n\n  /**\n   * Create a nominal brand constructor.\n   */\n  \u003cTBrand\u003e(): NominalBrandConstructor\u003cTBrand\u003e;\n\n  /**\n   * Compose multiple brand constructors into a single brand constructor.\n   */\n  compose\u003c\n    TBrands extends readonly [\n      BrandConstructor\u003cany, any\u003e,\n      ...BrandConstructor\u003cany, any\u003e[]\n    ]\n  \u003e(\n    ...brands: EnsureCommonBase\u003cTBrands\u003e\n  ): ComposedBrandConstructor\u003c\n    {\n      [B in keyof TBrands]: PickErrorFromBrandConstructor\u003cTBrands[B]\u003e;\n    }[number],\n    UnionToIntersection\u003c\n      { [B in keyof TBrands]: PickBrandFromConstructor\u003cTBrands[B]\u003e }[number]\n    \u003e extends infer X extends Brand\u003cany\u003e\n      ? X\n      : Brand\u003cany\u003e\n  \u003e;\n} = (validate, onErr) =\u003e (value) =\u003e {\n  if (validate) {\n    return Result.fromPredicate(value, validate, onErr);\n  }\n  return value;\n};\n\nBrand.compose =\n  (...brands) =\u003e\n  (value) =\u003e {\n    const results = brands.map((brand) =\u003e brand(value));\n\n    return Result.validate(results as any) as any;\n  };\n\ntype EnsureCommonBase\u003c\n  TBrands extends readonly [\n    BrandConstructor\u003cany, any\u003e,\n    ...BrandConstructor\u003cany, any\u003e[]\n  ]\n\u003e = {\n  [B in keyof TBrands]: Unbrand\u003c\n    PickBrandFromConstructor\u003cTBrands[0]\u003e\n  \u003e extends Unbrand\u003cPickBrandFromConstructor\u003cTBrands[B]\u003e\u003e\n    ? Unbrand\u003cPickBrandFromConstructor\u003cTBrands[B]\u003e\u003e extends Unbrand\u003c\n        PickBrandFromConstructor\u003cTBrands[0]\u003e\n      \u003e\n      ? TBrands[B]\n      : TBrands[B]\n    : \"ERROR: All brands should have the same base type\";\n};\n\ndeclare const BrandSymbol: unique symbol;\n\ntype BrandId = typeof BrandSymbol;\n\ntype UnionToIntersection\u003cT\u003e = (T extends any ? (x: T) =\u003e any : never) extends (\n  x: infer R\n) =\u003e any\n  ? R\n  : never;\n\ntype Brands\u003cP\u003e = P extends Brander\u003cany\u003e\n  ? UnionToIntersection\u003c\n      {\n        [k in keyof P[BrandId]]: k extends string | symbol ? Brander\u003ck\u003e : never;\n      }[keyof P[BrandId]]\n    \u003e\n  : never;\n\nexport type Unbrand\u003cP\u003e = P extends infer Q \u0026 Brands\u003cP\u003e ? Q : P;\n\nexport type Brand\u003cA, K extends string | symbol = typeof BrandSymbol\u003e = A \u0026\n  Brander\u003cK\u003e;\n\nexport namespace Brand {\n  export type Infer\u003cA\u003e = A extends Brand\u003cinfer B\u003e\n    ? B\n    : A extends BrandConstructor\u003cunknown, infer B\u003e\n    ? B\n    : never;\n}\n\ninterface Brander\u003cin out K extends string | symbol\u003e {\n  readonly [BrandSymbol]: {\n    readonly [k in K]: K;\n  };\n}\n\ntype NominalBrandConstructor\u003cA\u003e = (value: Unbrand\u003cA\u003e) =\u003e A;\n\ntype ValidatedBrandConstructor\u003cE, A\u003e = (value: Unbrand\u003cA\u003e) =\u003e Result\u003cE, A\u003e;\n\ntype ComposedBrandConstructor\u003cE, A\u003e = (value: Unbrand\u003cA\u003e) =\u003e Result\u003cE[], A\u003e;\n\ntype BrandConstructor\u003cE, A\u003e =\n  | NominalBrandConstructor\u003cA\u003e\n  | ValidatedBrandConstructor\u003cE, A\u003e\n  | ComposedBrandConstructor\u003cE, A\u003e;\n\ntype PickErrorFromBrandConstructor\u003cBC\u003e = BC extends BrandConstructor\u003c\n  infer E,\n  infer _A\n\u003e\n  ? E\n  : never;\n\ntype PickBrandFromConstructor\u003cBC\u003e = BC extends BrandConstructor\u003c\n  infer _E,\n  infer A\n\u003e\n  ? A\n  : never;\n```\n\n```ts\nimport { Brand } from \"./brand\";\n\ntype Email = Brand\u003cstring, \"Email\"\u003e;\n\nconst Email = Brand\u003cEmail\u003e();\n\nconst email: Email = Email(\"email@provider.com\");\n```\n\nYou can go further by refining the type to only allow valid email addresses:\n\n```ts\ntype Email = Brand\u003cstring, \"Email\"\u003e;\n\nconst Email = Brand\u003cError, Email\u003e(\n  (value) =\u003e {\n    return value.includes(\"@\");\n  },\n  (value) =\u003e {\n    return new Error(`Invalid email address: ${value}`);\n  }\n);\n\nconst email: Result\u003cError, Email\u003e = Email(\"test@provider.com\");\n```\n\nIt is also composable, meaning you can create brands as the result of other brands:\n\n```ts\ntype Int = Brand\u003cnumber, \"Int\"\u003e;\ntype PositiveNumber = Brand\u003cnumber, \"PositiveNumber\"\u003e;\n\nclass InvalidIntegerError extends Error {\n  constructor(value: number) {\n    super(`Invalid integer: ${value}`);\n  }\n}\n\nconst Int = Brand\u003cInvalidIntegerError, Int\u003e(\n  (value) =\u003e {\n    return Number.isInteger(value);\n  },\n  (value) =\u003e {\n    return new InvalidIntegerError(value);\n  }\n);\n\nclass InvalidPositiveNumberError extends Error {\n  constructor(value: number) {\n    super(`Invalid positive number: ${value}`);\n  }\n}\n\nconst PositiveNumber = Brand\u003cInvalidPositiveNumberError, PositiveNumber\u003e(\n  (value) =\u003e {\n    return value \u003e 0;\n  },\n  (value) =\u003e {\n    return new InvalidPositiveNumberError(value);\n  }\n);\n\ntype PositiveInt = Int \u0026 PositiveNumber;\n\nconst PositiveInt = Brand.compose(Int, PositiveNumber);\n\nconst positiveInt: Result\u003c\n  (InvalidIntegerError | InvalidPositiveNumberError)[],\n  PositiveInt\n\u003e = PositiveInt(42);\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcevr%2Fftld","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcevr%2Fftld","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcevr%2Fftld/lists"}