{"_id":"@effectionx/converge","_rev":"6-215a1f3d35163718a07d5cb74422b933","name":"@effectionx/converge","dist-tags":{"latest":"0.1.4"},"versions":{"0.1.0":{"name":"@effectionx/converge","version":"0.1.0","author":{"name":"engineering@frontside.com"},"license":"MIT","_id":"@effectionx/converge@0.1.0","maintainers":[{"name":"frontsidejack","email":"jack@frontside.io"}],"homepage":"https://github.com/thefrontside/effectionx#readme","bugs":{"url":"https://github.com/thefrontside/effectionx/issues"},"dist":{"shasum":"1f585346ba9455817a8c4de42de6d4114bf673ae","tarball":"https://registry.npmjs.org/@effectionx/converge/-/converge-0.1.0.tgz","fileCount":8,"integrity":"sha512-+iaXndeINVQCmymoY1hgTnqDda9dy5VVtWF1LU/gZV1mw2gOooy9QOx7S/LYa5ftj1dAZqIquMRVbyOXGeqarg==","signatures":[{"sig":"MEYCIQD8NW9TfmjIM0Uap11PXoyd4DncLxSXNoTtA0oz3JZETAIhAN5lhhof62XZXlc0Sg5/JYCQV8vzr+1V94+LPslhlhUD","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@effectionx%2fconverge@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":77020},"main":"./dist/mod.js","type":"module","types":"./dist/mod.d.ts","engines":{"node":">= 22"},"exports":{".":{"default":"./dist/mod.js","development":"./mod.ts"}},"gitHead":"9bf9488da2d41cf8541684dfdc1bdd9257232483","_npmUser":{"name":"frontsidejack","email":"jack@frontside.io"},"repository":{"url":"git+https://github.com/thefrontside/effectionx.git","type":"git"},"_npmVersion":"10.9.4","directories":{},"sideEffects":false,"_nodeVersion":"22.21.1","dependencies":{"@effectionx/timebox":"workspace:*"},"_hasShrinkwrap":false,"devDependencies":{"@effectionx/bdd":"workspace:*"},"peerDependencies":{"effection":"^3 || ^4"},"_npmOperationalInternal":{"tmp":"tmp/converge_0.1.0_1768765594985_0.5511032589236096","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@effectionx/converge","version":"0.1.1","author":{"name":"engineering@frontside.com"},"license":"MIT","_id":"@effectionx/converge@0.1.1","maintainers":[{"name":"frontsidejack","email":"jack@frontside.io"}],"homepage":"https://github.com/thefrontside/effectionx#readme","bugs":{"url":"https://github.com/thefrontside/effectionx/issues"},"dist":{"shasum":"61d5b04aba419b479aba90edf0b47b476d06e515","tarball":"https://registry.npmjs.org/@effectionx/converge/-/converge-0.1.1.tgz","fileCount":13,"integrity":"sha512-xLQ6YRlwUBljBEfBy3X1FpIFQ+YjA8rwinE0BuQAtJ9GlWIdmJ7O7qVTJ9YkDab0ZcKfaOFSATMtTpTIGIe7sA==","signatures":[{"sig":"MEYCIQCjBk+QIifPU6ARtDZNhPixiWaqfxZUWZDCKB9EhqPkmgIhALchtACMqSeYCJUEoWHghQH2OXbTSXcNVO11D4oI0PRH","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@effectionx%2fconverge@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":196356},"main":"./dist/mod.js","type":"module","_from":"file:effectionx-converge-0.1.1.tgz","types":"./dist/mod.d.ts","engines":{"node":">= 22"},"exports":{".":{"default":"./dist/mod.js","development":"./mod.ts"}},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:05309104-9ab1-4191-af03-f3bbb932a413"}},"_resolved":"/tmp/7c7f2ffbe9ab2d7b0a45b440d5c957e6/effectionx-converge-0.1.1.tgz","_integrity":"sha512-xLQ6YRlwUBljBEfBy3X1FpIFQ+YjA8rwinE0BuQAtJ9GlWIdmJ7O7qVTJ9YkDab0ZcKfaOFSATMtTpTIGIe7sA==","repository":{"url":"git+https://github.com/thefrontside/effectionx.git","type":"git"},"_npmVersion":"11.6.2","description":"Recognize a desired state and synchronize on when that state has been achieved.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","dependencies":{"@effectionx/timebox":"0.4.0"},"_hasShrinkwrap":false,"devDependencies":{"@effectionx/bdd":"0.4.1"},"peerDependencies":{"effection":"^3 || ^4"},"_npmOperationalInternal":{"tmp":"tmp/converge_0.1.1_1768978795224_0.42270903702056817","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@effectionx/converge","version":"0.1.2","author":{"name":"engineering@frontside.com"},"license":"MIT","_id":"@effectionx/converge@0.1.2","maintainers":[{"name":"frontsidejack","email":"jack@frontside.io"}],"homepage":"https://github.com/thefrontside/effectionx#readme","bugs":{"url":"https://github.com/thefrontside/effectionx/issues"},"dist":{"shasum":"d0bb8963ab21b848686a77b4c9bef48e542c45cf","tarball":"https://registry.npmjs.org/@effectionx/converge/-/converge-0.1.2.tgz","fileCount":13,"integrity":"sha512-Hg7jY7bcOrU1CDf8oU+VrlZaXJ47cv703vj9iSjic7i9SdljkKDznk2BtEeXLR+xsjdtE972g06agOGaunXMRA==","signatures":[{"sig":"MEUCIGBrbWlV0nCjUmcCsUJGDelhP9vA6iqS06wC9F8DPEn8AiEA/XjVxC4G6T7zgUJSLV/B2Eg+aTp7k8ZzixbQKCKhS7k=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@effectionx%2fconverge@0.1.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":196448},"main":"./dist/mod.js","type":"module","_from":"file:effectionx-converge-0.1.2.tgz","types":"./dist/mod.d.ts","engines":{"node":">= 22"},"exports":{".":{"default":"./dist/mod.js","development":"./mod.ts"}},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:05309104-9ab1-4191-af03-f3bbb932a413"}},"_resolved":"/tmp/3ef9ef80ab39496f7a539a7c1798492e/effectionx-converge-0.1.2.tgz","_integrity":"sha512-Hg7jY7bcOrU1CDf8oU+VrlZaXJ47cv703vj9iSjic7i9SdljkKDznk2BtEeXLR+xsjdtE972g06agOGaunXMRA==","repository":{"url":"git+https://github.com/thefrontside/effectionx.git","type":"git"},"_npmVersion":"11.6.2","description":"Poll and wait for conditions to be met with automatic retry and timeout","directories":{},"sideEffects":false,"_nodeVersion":"24.13.0","dependencies":{"@effectionx/timebox":"0.4.1"},"_hasShrinkwrap":false,"devDependencies":{"@effectionx/bdd":"0.4.2"},"peerDependencies":{"effection":"^3 || ^4"},"_npmOperationalInternal":{"tmp":"tmp/converge_0.1.2_1770864032048_0.23756907458895538","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@effectionx/converge","version":"0.1.3","author":{"name":"engineering@frontside.com"},"license":"MIT","_id":"@effectionx/converge@0.1.3","maintainers":[{"name":"frontsidejack","email":"jack@frontside.io"}],"homepage":"https://github.com/thefrontside/effectionx#readme","bugs":{"url":"https://github.com/thefrontside/effectionx/issues"},"dist":{"shasum":"64a4c512bf83d2d3f84f0fee7b785d69c21c8720","tarball":"https://registry.npmjs.org/@effectionx/converge/-/converge-0.1.3.tgz","fileCount":13,"integrity":"sha512-LPuDslL53kA8etLgO/sMmqv9v66txOAO5d6DUWLnwZa6CwA2MTYkHEnZAoIGCnQYbjKTbPxlKatHgF15ihB4zA==","signatures":[{"sig":"MEUCIDPPpUBp39sElzrdMEj1fCqPNW9Ht1xVsW9m4NfddsdoAiEAwx2UzEsxw0m5s1uWM2hgBRAyrT+1vnp1h+E/6Z584zw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@effectionx%2fconverge@0.1.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":196538},"main":"./dist/mod.js","type":"module","_from":"file:effectionx-converge-0.1.3.tgz","types":"./dist/mod.d.ts","engines":{"node":">= 22"},"exports":{".":{"types":"./dist/mod.d.ts","import":"./dist/mod.js","default":"./dist/mod.js","development":"./mod.ts"}},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:05309104-9ab1-4191-af03-f3bbb932a413"}},"_resolved":"/tmp/9f8ce681a995c9f56d5125dcdfc92dc2/effectionx-converge-0.1.3.tgz","_integrity":"sha512-LPuDslL53kA8etLgO/sMmqv9v66txOAO5d6DUWLnwZa6CwA2MTYkHEnZAoIGCnQYbjKTbPxlKatHgF15ihB4zA==","repository":{"url":"git+https://github.com/thefrontside/effectionx.git","type":"git"},"_npmVersion":"11.6.2","description":"Poll and wait for conditions to be met with automatic retry and timeout","directories":{},"sideEffects":false,"_nodeVersion":"24.13.0","dependencies":{"@effectionx/timebox":"0.4.2"},"_hasShrinkwrap":false,"devDependencies":{"effection":"^4","@effectionx/bdd":"0.4.3"},"peerDependencies":{"effection":"^3 || ^4"},"_npmOperationalInternal":{"tmp":"tmp/converge_0.1.3_1771868053060_0.13795738765440846","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"@effectionx/converge","version":"0.1.4","keywords":["effection","effectionx","testing","concurrency","polling","retry"],"author":{"name":"engineering@frontside.com"},"license":"MIT","_id":"@effectionx/converge@0.1.4","maintainers":[{"name":"frontsidejack","email":"jack@frontside.io"}],"homepage":"https://github.com/thefrontside/effectionx#readme","bugs":{"url":"https://github.com/thefrontside/effectionx/issues"},"dist":{"shasum":"ab4400c2550ba09bcf6389fa728659f2fe7ffa67","tarball":"https://registry.npmjs.org/@effectionx/converge/-/converge-0.1.4.tgz","fileCount":13,"integrity":"sha512-M11KU7jK3gOFGI4RT+Bf6ITMJntzzoGEO2KPx9r8uBoR+Rgwiyeuq+OeIHkuiHLUC+bB1g93KOj7aKR/G0F1/Q==","signatures":[{"sig":"MEQCIEXVl9/9job0JN0WU93x5/d8vDLhx3CHiGanXUZ52m1MAiAtC18j6oUokGLtJOdLpoR/zltW5CWMwwg7iZGKnulxUw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@effectionx%2fconverge@0.1.4","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":196582},"main":"./dist/mod.js","type":"module","_from":"file:effectionx-converge-0.1.4.tgz","types":"./dist/mod.d.ts","exports":{".":{"types":"./dist/mod.d.ts","import":"./dist/mod.js","default":"./dist/mod.js"}},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:05309104-9ab1-4191-af03-f3bbb932a413"}},"_resolved":"/tmp/738bb2103438986e4e6fe3d896f79308/effectionx-converge-0.1.4.tgz","_integrity":"sha512-M11KU7jK3gOFGI4RT+Bf6ITMJntzzoGEO2KPx9r8uBoR+Rgwiyeuq+OeIHkuiHLUC+bB1g93KOj7aKR/G0F1/Q==","repository":{"url":"git+https://github.com/thefrontside/effectionx.git","type":"git"},"_npmVersion":"11.9.0","description":"Poll and wait for conditions to be met with automatic retry and timeout","directories":{},"sideEffects":false,"_nodeVersion":"24.14.0","dependencies":{"@effectionx/timebox":"0.4.3"},"_hasShrinkwrap":false,"devDependencies":{"effection":"^4","@effectionx/bdd":"0.5.2"},"peerDependencies":{"effection":"^3 || ^4"},"_npmOperationalInternal":{"tmp":"tmp/converge_0.1.4_1774215705754_0.3126225043323032","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2026-01-18T19:46:34.849Z","modified":"2026-09-30T21:08:15.582Z","0.1.0":"2026-01-18T19:46:35.142Z","0.1.1":"2026-01-21T06:59:55.437Z","0.1.2":"2026-02-12T02:40:32.250Z","0.1.3":"2026-02-23T17:34:13.207Z","0.1.4":"2026-03-22T21:41:45.920Z"},"bugs":{"url":"https://github.com/thefrontside/effectionx/issues"},"author":{"name":"engineering@frontside.com"},"license":"MIT","homepage":"https://github.com/thefrontside/effectionx#readme","keywords":["effection","effectionx","testing","concurrency","polling","retry"],"repository":{"url":"git+https://github.com/thefrontside/effectionx.git","type":"git"},"description":"Poll and wait for conditions to be met with automatic retry and timeout","maintainers":[{"email":"me@jacobbolda.com","name":"jbolda"},{"email":"jack@frontside.io","name":"frontsidejack"}],"readme":"# @effectionx/converge\n\nRecognize a desired state and synchronize on when that state has been achieved.\n\n> This package is a port of [@bigtest/convergence](https://github.com/bigtestjs/convergence) adapted for [Effection](https://frontside.com/effection) structured concurrency.\n\n---\n\n## Why Convergence?\n\nLet's say you want to write an assertion to verify a simple cause and\neffect: when a certain button is clicked, a dialog appears containing\nsome text that gets loaded from the network.\n\nIn order to do this, you have to make sure that your assertion runs\n_after_ the effect you're testing has been realized.\n\n![Image of assertion after an effect](https://raw.githubusercontent.com/thefrontside/effectionx/main/converge/images/assertion-after.png)\n\nIf not, then you could end up with a false negative, or \"flaky test\"\nbecause you ran the assertion too early. If you'd only waited a little\nbit longer, then your test would have passed. So sad!\n\n![Image of false negative test](https://raw.githubusercontent.com/thefrontside/effectionx/main/converge/images/false-negative.png)\n\nIn fact, test flakiness is the reason most people shy away from\nwriting big tests in JavaScript in the first place. It seems almost\nimpossible to write robust tests without having visibility into the\ninternals of your runtime so that you can manually synchronize on\nthings like rendering and data loading. Unfortunately, those can be a\nmoving target, and worse, they couple you to your framework.\n\nBut what if instead of trying to run our assertions at just the right\ntime, we ran them _many_ times until they either pass or we decide to\ngive up?\n\n![Image of convergent assertion](https://raw.githubusercontent.com/thefrontside/effectionx/main/converge/images/convergent-assertion.png)\n\nThis is the essence of what `@effectionx/converge` provides:\nrepeatedly testing for a condition and then allowing code to run when\nthat condition has been met.\n\nAnd it isn't just for assertions either. Because it is a general\nmechanism for synchronizing on any observed state, it can be used to\nproperly time test setup and teardown as well.\n\n---\n\n## Installation\n\n```bash\nnpm install @effectionx/converge\n```\n\n---\n\n## Usage\n\n### `when(assertion, options?)`\n\nConverges when the assertion passes _within_ the timeout period. The\nassertion runs repeatedly (every 10ms by default) and is considered\npassing when it does not throw or return `false`. If it never passes\nwithin the timeout, the operation throws with the last error.\n\n```typescript\nimport { when } from \"@effectionx/converge\";\n\n// Wait for a condition to become true\nlet { value } = yield* when(function* () {\n  if (total !== 100) throw new Error(\"not ready\");\n  return total;\n});\n\n// With custom timeout\nyield* when(\n  function* () {\n    if (!element.isVisible) throw new Error(\"not visible\");\n  },\n  { timeout: 5000 },\n);\n```\n\n### `always(assertion, options?)`\n\nConverges when the assertion passes _throughout_ the timeout period.\nLike `when()`, the assertion runs repeatedly, but it must pass\nconsistently for the entire duration. If it fails at any point, the\noperation throws immediately.\n\n```typescript\nimport { always } from \"@effectionx/converge\";\n\n// Verify a condition remains true\nyield* always(function* () {\n  if (counter >= 100) throw new Error(\"counter exceeded limit\");\n});\n\n// With custom timeout\nyield* always(\n  function* () {\n    if (!connection.isAlive) throw new Error(\"connection lost\");\n  },\n  { timeout: 5000 },\n);\n```\n\n---\n\n## Options\n\nBoth `when` and `always` accept an options object:\n\n| Option     | Type     | Default                        | Description                                   |\n| ---------- | -------- | ------------------------------ | --------------------------------------------- |\n| `timeout`  | `number` | `2000` (when) / `200` (always) | Maximum time to wait in milliseconds          |\n| `interval` | `number` | `10`                           | Time between assertion retries in milliseconds |\n\n---\n\n## Stats Object\n\nBoth functions return a `ConvergeStats` object with timing and execution info:\n\n```typescript\ninterface ConvergeStats<T> {\n  start: number; // Timestamp when convergence started\n  end: number; // Timestamp when convergence ended\n  elapsed: number; // Milliseconds the convergence took\n  runs: number; // Number of times the assertion was executed\n  timeout: number; // The timeout that was configured\n  interval: number; // The interval that was configured\n  value: T; // The return value from the assertion\n}\n```\n\nExample:\n\n```typescript\nlet stats = yield* when(\n  function* () {\n    return yield* fetchData();\n  },\n  { timeout: 5000 },\n);\n\nconsole.log(`Converged in ${stats.elapsed}ms after ${stats.runs} attempts`);\nconsole.log(stats.value); // the fetched data\n```\n\n---\n\n## Examples\n\n### Waiting for an element to appear\n\n```typescript\nyield* when(function* () {\n  let element = document.querySelector('[data-test-id=\"dialog\"]');\n  if (!element) throw new Error(\"dialog not found\");\n  return element;\n});\n```\n\n### Verifying a value remains stable\n\n```typescript\nyield* always(\n  function* () {\n    if (connection.status !== \"connected\") {\n      throw new Error(\"connection dropped\");\n    }\n  },\n  { timeout: 1000 },\n);\n```\n\n### Using with file system operations\n\n```typescript\nimport { when } from \"@effectionx/converge\";\nimport { access } from \"node:fs/promises\";\nimport { until } from \"effection\";\n\n// Wait for a file to exist\nyield* when(\n  function* () {\n    let exists = yield* until(\n      access(filePath).then(\n        () => true,\n        () => false,\n      ),\n    );\n    if (!exists) throw new Error(\"file not found\");\n    return true;\n  },\n  { timeout: 10000 },\n);\n```\n","readmeFilename":"README.md"}