{"id":15467837,"url":"https://github.com/quaelin/particl","last_synced_at":"2025-11-17T13:38:53.135Z","repository":{"id":39704210,"uuid":"302212007","full_name":"quaelin/particl","owner":"quaelin","description":"A small, easy to use JavaScript module that provides asynchronous control flow, event/property listeners, barriers, and more.","archived":false,"fork":false,"pushed_at":"2023-01-04T04:51:12.000Z","size":1006,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-09-23T06:30:11.754Z","etag":null,"topics":["async","asynchronous","callback","control-flow","promise","promises","trigger-listeners"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/quaelin.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-10-08T02:28:52.000Z","updated_at":"2021-10-21T23:55:39.000Z","dependencies_parsed_at":"2023-02-01T21:45:49.012Z","dependency_job_id":null,"html_url":"https://github.com/quaelin/particl","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/quaelin/particl","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quaelin%2Fparticl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quaelin%2Fparticl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quaelin%2Fparticl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quaelin%2Fparticl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/quaelin","download_url":"https://codeload.github.com/quaelin/particl/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quaelin%2Fparticl/sbom","scorecard":{"id":754838,"data":{"date":"2025-08-11","repo":{"name":"github.com/quaelin/particl","commit":"26869d90fc3a2b39b6e066119aa246f5fea437f6"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.4,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Code-Review","score":0,"reason":"Found 0/3 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Pinned-Dependencies","score":3,"reason":"dependency not pinned by hash detected -- score normalized to 3","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/node.js.yml:22: update your workflow using https://app.stepsecurity.io/secureworkflow/quaelin/particl/node.js.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/node.js.yml:24: update your workflow using https://app.stepsecurity.io/secureworkflow/quaelin/particl/node.js.yml/main?enable=pin","Info:   0 out of   2 GitHub-owned GitHubAction dependencies pinned","Info:   1 out of   1 npmCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/node.js.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":0,"reason":"license file not detected","details":["Warn: project does not have a license file"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 28 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":0,"reason":"13 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-67hx-6x53-jw92","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-gxpj-cx7g-858c","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw","Warn: Project is vulnerable to: GHSA-52f5-9888-hmc6","Warn: Project is vulnerable to: GHSA-72xf-g2v4-qvf3","Warn: Project is vulnerable to: GHSA-j8xg-fqg3-53r7","Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-22T21:20:38.368Z","repository_id":39704210,"created_at":"2025-08-22T21:20:38.368Z","updated_at":"2025-08-22T21:20:38.368Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":284893574,"owners_count":27080531,"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-11-17T02:00:06.431Z","response_time":55,"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":["async","asynchronous","callback","control-flow","promise","promises","trigger-listeners"],"created_at":"2024-10-02T01:27:53.434Z","updated_at":"2025-11-17T13:38:53.109Z","avatar_url":"https://github.com/quaelin.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# particl\n\nA small, easy to use JavaScript module that provides asynchronous control flow,\nevent/property listeners, barriers, and more.\n\n`particl` is the spiritual successor to [atom](https://github.com/quaelin/atom),\nusing some more modern language features and adding Promise support and more\nthorough tests.\n\n## Features\n\n* Small, depending only on core-js for older browser support.\n* Allows you to _clearly_ code using a variety of async control flow patterns,\n  even ones that would be cumbersome to implement clearly with just Promises.\n\n## Install\n\n```\n  npm install particl\n```\n\n## Tutorial\n\n### Constructor\n\nYou can call the `particl()` constructor without arguments, but it's also\npossible to pass in initial properties as an object, and/or a callback function\nwhich will be immediately invoked.\n\n```js\n  // no arguments\n  let p = particl();\n\n  // initial values\n  p = particl({ prop1: 'value1', prop2: 'value2' });\n\n  // exploded api view\n  p = particl(({ get, set, need, provide, ... }) =\u003e {\n    ...\n  });\n  p = particl({ prop1: 'value1' }, ({ get, set, need, provide, ... }) =\u003e {\n    ...\n  });\n```\n\nThe exploded api form simply makes the entire instance API available as the\nfirst argument to the callback, so that you can easily select the portion of the\napi you want to interact with using argument destructuring.\n\nSo the following are identical:\n\n```js\n  const p = particl();\n  p.set('foo', 'bar');\n\n  const p = particl({ foo: 'bar' });\n\n  const p = particl(({ set }) =\u003e {\n    set({ foo: 'bar' });\n  });\n```\n\n### Properties\n\nA particl has properties.  The `get()` and `set()` methods may be employed to\nread and write values of any type:\n\n```js\n  particl(({ get, set }) =\u003e {\n\n    set('key', 'value');\n    console.log('Value of key:', get('key'));\n\n    set({\n      pi: 3.141592653,\n      r: 5,\n      circumference: () =\u003e 2 * get('pi') * get('r'),\n    });\n    console.log('Circumference:', get('circumference')());\n  });\n```\n\nIf an object is passed in to the constructor, it will initialize properties:\n\n```js\n  p = particl({ p: 3.141592653, r: 5 });\n```\n\nUse `has()` to query for existence of a property, and `keys()` to get a list of\nall properties that have been set.\n\n```js\n  if (p.has('skills')) {\n    console.log('What \"p\" brings to the table:', p.keys());\n  }\n```\n\nThe `each()` method lets you execute a function on a series of properties.\n\n```js\n  p.set({ r: 0xBA, g: 0xDA, b: 0x55 });\n  p.each(['r', 'g', 'b'], (key, value) =\u003e {\n    console.log(`${key}:${value}`);\n  });\n```\n\n### Listeners\n\nListeners may be attached to particls in a variety of ways.\n\nTo be notified as soon as a property is set, use the `once()` method.  The\ncallback will be called immediately if the property is already set.\n\n```js\n  p.once('user', (user) =\u003e {\n    alert(`Welcome, ${user.name}!`);\n  });\n```\n\nor\n\n```js\n  p.once('user').then((user) =\u003e {\n    alert(`Welcome, ${user.name}!`);\n  });\n```\n\nMany particl methods can work with more than one property at a time.\n\n```js\n  p.once(['app', 'user'], (app, user) =\u003e {\n    alert(`Welcome to ${app.name}, ${user.name}!`);\n  });\n```\n\nor\n\n```js\n  p.once(['app', 'user']).then(({ app, user }) =\u003e {\n    alert(`Welcome to ${app.name}, ${user.name}!`);\n  });\n```\n\nWhen you just want to know about the next change, even if the property is\nalready set, use `next()`.\n\n```js\n  p.next('click', (click) =\u003e {\n    alert(`Are you done clicking on ${click.button} yet?`);\n  });\n```\n\nTo watch for any future changes to a property, use the `on()` method.\n\n```js\n  function myErrorHandler(error) {\n    console.log(`There was a grievous calamity of code in ${p.get('module')}`);\n    console.log(error);\n  }\n  p.on('error', myErrorHandler);\n```\n\nNote that setting a property with a primitive (string/number/boolean) value will\nonly trigger listeners if the value is *different*.  On the other hand, setting\nan array or object value will *always* trigger listeners.\n\nYou can unregister any listener using `off()`.\n\n```js\n  p.off(myErrorHandler);\n```\n\n### Needs and Providers\n\nYou can register a provider for a property.\n\n```js\n  particl(({ need, on, provide }) =\u003e {\n    provide('privacyPolicy', () =\u003e fetch(`${baseUrl}/privacy.txt`));\n\n    on('clickPrivacy', async () =\u003e {\n      element.innerText = await need('privacyPolicy');\n    });\n  });\n```\n\nProviders only get invoked if there is a need, and if the property is not\nalready set.  Use the `need()` method to declare a need for a particular\nproperty.  If a corresponding provider is registered, it will be invoked.\nOtherwise, `need()` behaves just like `once()`.\n\n### Exploded view\n\nIf you call `explode(func)` or else just pass in a function to the constructor,\nit will be called with the entire api object, allowing you to easily select just\nindividual methods you want with argument destructuring:\n\n```js\n  particl(async ({ get, need, on, provide, set }) =\u003e {\n    set({ logDefaults: { appName: 'myApp' }});\n\n    on('log', (event) =\u003e {\n      console.log({\n        ...get('logDefaults'),\n        ...event,\n        timestamp: Date.now(),\n      });\n    });\n    const log = event =\u003e set('log', event);\n\n    provide('data', () =\u003e fetch('${baseUrl}/api/data'));\n\n    const data = await need('data');\n    log({ msg: 'data loaded '});\n\n    ...\n  });\n```\n\n### Extending functionality with mixins\n\nThe particl constructor provides a way to incorporate mixins that extend the\nparticl's api.  Use it like this:\n\n```js\n  particl(\n    [ ...mixins ],\n    ({ on, set, get, need, customMethodFromMixin, ... }) =\u003e {\n\n    }\n  );\n```\n\nSome helpful mixins are included in the [mixins/ directory](./src/mixins).  Fox\nexample, use the `customListeners` mixin to create customized method names for\nspecific properties:\n\n```js\n  const customListenersMixin = require('particl/dist/mixins/customListeners');\n\n  particl(\n    [customListenersMixin('event')],\n    ({ onEvent, onceEvent, setEvent }) =\u003e {\n      // This is a convenient shorthand for on('event', (evt) =\u003e { ... })\n      onEvent((evt) =\u003e {\n        console.log('Something happened:', evt);\n      });\n    }\n  );\n```\n\nOr use the `matchers` mixin to add _validation_ to your property listeners:\n\n```js\n  const matchersMixin = require('particl/dist/mixins/matchers');\n\n  particl(\n    [matchersMixin],\n    async ({ onceMatch, set }) =\u003e {\n      set('authStatus', { authenticated: false });\n\n      ...\n\n      const isAuthenticated = (authStatus) =\u003e (authStatus?.authenticated);\n      await onceMatch('authStatus', isAuthenticated);\n      // We only get here once the user is authenticated\n    }\n  );\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquaelin%2Fparticl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fquaelin%2Fparticl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquaelin%2Fparticl/lists"}