{"id":20176556,"url":"https://github.com/algoan/pubsub","last_synced_at":"2026-04-01T17:56:59.480Z","repository":{"id":36977470,"uuid":"255924216","full_name":"algoan/pubsub","owner":"algoan","description":"A simple PubSub factory method exposing a listen-emit pattern","archived":false,"fork":false,"pushed_at":"2026-03-26T13:28:55.000Z","size":4804,"stargazers_count":17,"open_issues_count":5,"forks_count":16,"subscribers_count":4,"default_branch":"master","last_synced_at":"2026-03-27T05:27:43.404Z","etag":null,"topics":["factory","google-pubsub","nodejs","typescript"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","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/algoan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2020-04-15T13:24:53.000Z","updated_at":"2026-03-26T13:27:19.000Z","dependencies_parsed_at":"2023-01-17T11:01:07.322Z","dependency_job_id":"9ac85edc-e2f9-4475-a9d5-48987d707ba7","html_url":"https://github.com/algoan/pubsub","commit_stats":{"total_commits":225,"total_committers":9,"mean_commits":25.0,"dds":0.6755555555555556,"last_synced_commit":"c58809f09c51f13c6ce0f9f2f658149d1304f05e"},"previous_names":[],"tags_count":41,"template":false,"template_full_name":null,"purl":"pkg:github/algoan/pubsub","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/algoan%2Fpubsub","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/algoan%2Fpubsub/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/algoan%2Fpubsub/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/algoan%2Fpubsub/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/algoan","download_url":"https://codeload.github.com/algoan/pubsub/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/algoan%2Fpubsub/sbom","scorecard":{"id":183201,"data":{"date":"2025-08-11","repo":{"name":"github.com/algoan/pubsub","commit":"1e08a5a5aa0961da83f63a7fd7b622744374edc0"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":4.9,"checks":[{"name":"Code-Review","score":-1,"reason":"Found no human activity in the last 15 changesets","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":"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":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/test-and-publish.yaml: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":"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":"Maintained","score":10,"reason":"26 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Pinned-Dependencies","score":1,"reason":"dependency not pinned by hash detected -- score normalized to 1","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test-and-publish.yaml:34: update your workflow using https://app.stepsecurity.io/secureworkflow/algoan/pubsub/test-and-publish.yaml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test-and-publish.yaml:36: update your workflow using https://app.stepsecurity.io/secureworkflow/algoan/pubsub/test-and-publish.yaml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/test-and-publish.yaml:41: update your workflow using https://app.stepsecurity.io/secureworkflow/algoan/pubsub/test-and-publish.yaml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test-and-publish.yaml:59: update your workflow using https://app.stepsecurity.io/secureworkflow/algoan/pubsub/test-and-publish.yaml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test-and-publish.yaml:64: update your workflow using https://app.stepsecurity.io/secureworkflow/algoan/pubsub/test-and-publish.yaml/master?enable=pin","Warn: npmCommand not pinned by hash: .github/workflows/test-and-publish.yaml:49","Warn: npmCommand not pinned by hash: .github/workflows/test-and-publish.yaml:50","Warn: npmCommand not pinned by hash: .github/workflows/test-and-publish.yaml:69","Info:   0 out of   4 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   1 third-party GitHubAction dependencies pinned","Info:   1 out of   4 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":"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":"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":"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":"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 'master'"],"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":10,"reason":"SAST tool is run on all commits","details":["Info: all commits (30) 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":7,"reason":"3 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-52f5-9888-hmc6"],"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-16T19:16:03.777Z","repository_id":36977470,"created_at":"2025-08-16T19:16:03.777Z","updated_at":"2025-08-16T19:16:03.777Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31290709,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["factory","google-pubsub","nodejs","typescript"],"created_at":"2024-11-14T02:09:59.500Z","updated_at":"2026-04-01T17:56:59.452Z","avatar_url":"https://github.com/algoan.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PubSub\n\nThis is a generic PubSub Factory exposing a listen and a emit method.\n\n_NOTE_: Today, only [Google Cloud PubSub](https://cloud.google.com/pubsub/docs/overview) has been added.\n\n## Installation\n\n```bash\nnpm install --save @algoan/pubsub\n```\n\n## Usage\n\n### Google Cloud PubSub\n\n#### Run tests\n\nTo run tests or to try the PubSubFactory class, you need to have a google account and have installed gcloud sdk.\n\nThen, to install the [Google PubSub simulator](https://cloud.google.com/pubsub/docs/emulator), run:\n\n```shell\ngcloud components install pubsub-emulator\ngcloud components install beta\ngcloud components update\n```\n\nStart tests running:\n\n```shell\nnpm test\n```\n\nIt will launch a Google PubSub emulator thanks to the [google-pubsub-emulator](https://github.com/ert78gb/google-pubsub-emulator) library.\n\n\n#### Example\n\nTo create a PubSub instance using Google Cloud:\n\n```typescript\nimport { EmittedMessage, GCPubSub, PubSubFactory, Transport } from '@algoan/pubsub'\n\nconst pubsub: GCPubSub = PubSubFactory.create({\n  transport: Transport.GOOGLE_PUBSUB,\n  options: {\n    projectId: 'test',\n    // And all other Google PubSub properties\n  }\n});\nconst topicName: string = 'some_topic';\n\nawait pubsub.listen(topicName, {\n  autoAck: true,\n  onMessage: (data: EmittedMessage\u003c{foo: string}\u003e) =\u003e {\n    console.log(data.parsedData); // { foo: 'bar', time: {Date.now}, _eventName: 'some_topic' }\n    // do whatever you want. The message has already been acknowledged\n  },\n  onError: (error: Error) =\u003e {\n    // Handle error as you wish\n  }\n});\n\nawait pubsub.emit(topicName, { foo: 'bar' });\n```\n\n### Contribution\n\nThank you for your future contribution 😁 Please follow [these instructions](CONTRIBUTING.md) before opening a pull request!\n\n## API\n\n### `PubSubFactory.create({ transport, options })`\n\nThe only static method from the `PubSubFactory` class. It initiates a new PubSub instance depending on the `transport`. By default, it connects to [Google Cloud PubSub](https://googleapis.dev/nodejs/pubsub/latest/index.html).\n\n- `transport`: PubSub technology to use. Only `GOOGLE_PUBSUB` is available for now.\n- `options`: Options related to the transport.\n  - If `transport === Transport.GOOGLE_PUBSUB`, then have a look at the [Google Cloud PubSub config client](https://googleapis.dev/nodejs/pubsub/latest/global.html#ClientConfig).\n  - `debug`: Display logs if it is set to true. It uses a [pino logger](https://getpino.io/#/) and [pino-pretty](https://github.com/pinojs/pino-pretty) if `NODE_ENV` is not equal to `production`.\n  - `pinoOptions`: If `debug` is set to true, set the [pino logger options](https://getpino.io/#/docs/api?id=options). Default to `level: debug` and `prettyPrint: true` if `NODE_ENV` is not equal to `production`.\n  - `topicsPrefix`: Add a prefix to all created topics. Example: `topicsPrefix: 'my-topic'`, all topics will begin with `my-topic+{your topic name}`.\n  - `topicsSeparator`: Customize separator between topicsPrefix and topic name. Example: `topicsSeparator: '-'`, all topics will be  `{topic prefix}-{your topic name} (default to '+')`.\n  - `subscriptionsPrefix`: Add a prefix to all created subscriptions. Example: `subscriptionsPrefix: 'my-sub'`, all subscriptions will begin with `my-sub%{your topic name}`.\n  - `subscriptionsSeparator`: Customize separator between subscriptionsPrefix and topic name. Example: `subscriptionsSeparator: '-'`, all subscriptions will be  `{subscription prefix}-{your topic name} (default to '%')`.\n  - `namespace`: Add a namespace property to [Message attributes](https://googleapis.dev/nodejs/pubsub/latest/google.pubsub.v1.html#.PubsubMessage) when publishing on a topic.\n  - `environment`: Add a environment property to [Message attributes](https://googleapis.dev/nodejs/pubsub/latest/google.pubsub.v1.html#.PubsubMessage) when publishing on a topic.\n\n### `pubsub.listen(event, opts)`\n\nListen to a specific event.\n\n_NOTE_: It only uses the [Google Cloud subscription pull](https://cloud.google.com/pubsub/docs/pull) delivery for now.\n\n- `event`: Name of the event.\n- `opts`: Options related to the Listener method\n  - `onMessage`: Method called when receiving a message\n  - `onError`: Method called when an error occurs\n  - `options`: Option related to the chosen transport\n\nIf the chosen transport is Google Cloud PubSub, then `options` would be:\n\n- `autoAck`: Automatically ACK an event as soon as it is received (default to `true`)\n- `subscriptionOptions`: Options related to the created Subscription:\n  - `name`: Custom name for the subscription. Default: `event` (also equal to the topic name)\n  - `get`: Options applied to the `getSubscription` method (have a look at [Subscription options](https://googleapis.dev/nodejs/pubsub/latest/Subscription.html#get))\n  - `sub`: Options applied to the subscription instance (see also [`setOptions` method](https://googleapis.dev/nodejs/pubsub/latest/Subscription.html#setOptions))\n  - `create`: Options applied to the `createSubscription` method (have a look at [Create Subscription options](https://googleapis.dev/nodejs/pubsub/latest/Topic.html#createSubscription))\n  - `deadLetterTopicName`: Per-subscription override for the dead-letter topic name (see [Dead Letter Topics](#dead-letter-topics))\n- `topicOptions`: Options applied to the created topic (have a look at [Topic options](https://googleapis.dev/nodejs/pubsub/latest/Topic.html#get))\n- `topicName`: Set the topic name. By default, it uses the default name with a prefix.\n\n### `pubsub.emit(event, payload, opts)`\n\nEmit a specific event with a payload. It added attributes in the message if you have added a namespace or an environment when setting the `PubSubFactory` class. It also adds an `_eventName` and a `time` property in the emitted `payload`.\n\n- `event`: Name of the event to emit.\n- `payload`: Payload to send. It will be buffered by Google, and then parsed by the [listen](#pubsublistenevent-options) method.\n- `opts`: Options related to the Emit method\n  - `metadata`: Custom metadata added to the message\n  - `options`: Option related to the chosen transport\n\nIf the chosen transport is Google Cloud PubSub, then `options` would be:\n\n- `topicOptions`: Options applied to the created topic (have a look at [Topic options](https://googleapis.dev/nodejs/pubsub/latest/Topic.html#get))\n- `publishOptions`: Publish options set to the topic after its creation. Refer to [Publish Options](https://googleapis.dev/nodejs/pubsub/latest/global.html#PublishOptions)\n- `messageOptions`: Additional message options added to the message. Refer to [Message Options](https://googleapis.dev/nodejs/pubsub/latest/google.pubsub.v1.IPubsubMessage.html)\n\n### `pubsub.unsubscribe(event)`\n\nStop the server connection for a given subscription.\n\n- `event`: Name of of the event to stop listening for.\n\n## Dead Letter Topics\n\nWhen `deadLetterOptions` is set in the constructor options, the library automatically:\n\n1. Creates the dead-letter topic (if it does not exist)\n2. Creates a drain subscription on the dead-letter topic (to prevent GCP from discarding undelivered messages)\n3. Grants `roles/pubsub.publisher` on the dead-letter topic to the GCP Pub/Sub service account\n4. Grants `roles/pubsub.subscriber` on the source subscription to the GCP Pub/Sub service account\n5. Applies the `deadLetterPolicy` to the created subscription\n\nThis setup only happens **once per new subscription** — reconnecting to an already-existing subscription incurs zero overhead.\n\n### Mode 1: Per-subscription isolated dead-letter topics (recommended for multi-consumer systems)\n\nOmit `deadLetterTopicName`. Each subscription automatically gets its own `\u003csubscriptionName\u003e-deadletter` topic, so failed messages from different consumers are isolated and can be replayed independently.\n\n```typescript\nconst pubsub: GCPubSub = PubSubFactory.create({\n  transport: Transport.GOOGLE_PUBSUB,\n  options: {\n    projectId: 'my-project',\n    deadLetterOptions: {\n      maxDeliveryAttempts: 10, // optional, defaults to 5\n    },\n  },\n});\n\n// Each listen() call creates its own isolated dead-letter topic:\n// - \"my-orders-sub-deadletter\" for the first subscription\n// - \"my-payments-sub-deadletter\" for the second\nawait pubsub.listen('my-orders-sub');\nawait pubsub.listen('my-payments-sub');\n```\n\n### Mode 2: Shared dead-letter topic (single DLT for all subscriptions)\n\nProvide `deadLetterTopicName` to route all failed messages to one shared topic. The topic must already exist in GCP.\n\n```typescript\nconst pubsub: GCPubSub = PubSubFactory.create({\n  transport: Transport.GOOGLE_PUBSUB,\n  options: {\n    projectId: 'my-project',\n    deadLetterOptions: {\n      deadLetterTopicName: 'projects/my-project/topics/my-dead-letter', // fully-qualified or short name\n      maxDeliveryAttempts: 5,\n    },\n  },\n});\n\nawait pubsub.listen('my-orders-sub');   // routes to \"my-dead-letter\"\nawait pubsub.listen('my-payments-sub'); // routes to \"my-dead-letter\"\n```\n\n### Mode 3: Per-subscription override\n\nOverride the dead-letter topic for a specific subscription via `subscriptionOptions.deadLetterTopicName`. Falls back to the instance-level `deadLetterTopicName`, then auto-derives if neither is set.\n\n```typescript\nawait pubsub.listen('my-special-sub', {\n  options: {\n    subscriptionOptions: {\n      deadLetterTopicName: 'projects/my-project/topics/special-dead-letter',\n    },\n  },\n});\n```\n\n### No dead-letter topic\n\nIf `deadLetterOptions` is not set, no dead-letter resources are created and no IAM calls are made — fully backward compatible.\n\n```typescript\nconst pubsub: GCPubSub = PubSubFactory.create({\n  transport: Transport.GOOGLE_PUBSUB,\n  options: {\n    projectId: 'my-project',\n    // no deadLetterOptions — existing behavior unchanged\n  },\n});\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falgoan%2Fpubsub","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falgoan%2Fpubsub","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falgoan%2Fpubsub/lists"}