@nestjs/workflows 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +50 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +8 -0
- package/dist/core/codecs/aes-gcm-payload.codec.d.ts +42 -0
- package/dist/core/codecs/aes-gcm-payload.codec.js +119 -0
- package/dist/core/codecs/index.d.ts +2 -0
- package/dist/core/codecs/index.js +2 -0
- package/dist/core/codecs/payload-codecs.d.ts +59 -0
- package/dist/core/codecs/payload-codecs.js +130 -0
- package/dist/core/index.d.ts +16 -0
- package/dist/core/index.js +21 -0
- package/dist/core/interfaces/clock.interface.d.ts +18 -0
- package/dist/core/interfaces/clock.interface.js +1 -0
- package/dist/core/interfaces/index.d.ts +9 -0
- package/dist/core/interfaces/index.js +9 -0
- package/dist/core/interfaces/leased-worker-options.interface.d.ts +94 -0
- package/dist/core/interfaces/leased-worker-options.interface.js +1 -0
- package/dist/core/interfaces/limits.interface.d.ts +92 -0
- package/dist/core/interfaces/limits.interface.js +1 -0
- package/dist/core/interfaces/payload-codec.interface.d.ts +64 -0
- package/dist/core/interfaces/payload-codec.interface.js +1 -0
- package/dist/core/interfaces/result-waiter-options.interface.d.ts +52 -0
- package/dist/core/interfaces/result-waiter-options.interface.js +1 -0
- package/dist/core/interfaces/retry-settings.interface.d.ts +71 -0
- package/dist/core/interfaces/retry-settings.interface.js +1 -0
- package/dist/core/interfaces/schedule-store.interface.d.ts +143 -0
- package/dist/core/interfaces/schedule-store.interface.js +1 -0
- package/dist/core/interfaces/schedule.interface.d.ts +106 -0
- package/dist/core/interfaces/schedule.interface.js +1 -0
- package/dist/core/interfaces/scheduler-options.interface.d.ts +152 -0
- package/dist/core/interfaces/scheduler-options.interface.js +1 -0
- package/dist/core/limits/index.d.ts +1 -0
- package/dist/core/limits/index.js +1 -0
- package/dist/core/limits/limits.d.ts +61 -0
- package/dist/core/limits/limits.js +130 -0
- package/dist/core/results/index.d.ts +1 -0
- package/dist/core/results/index.js +1 -0
- package/dist/core/results/result-waiter.d.ts +54 -0
- package/dist/core/results/result-waiter.js +140 -0
- package/dist/core/retries/index.d.ts +1 -0
- package/dist/core/retries/index.js +1 -0
- package/dist/core/retries/retry.d.ts +60 -0
- package/dist/core/retries/retry.js +107 -0
- package/dist/core/scheduling/cron.util.d.ts +52 -0
- package/dist/core/scheduling/cron.util.js +262 -0
- package/dist/core/scheduling/encoded-schedule.store.d.ts +26 -0
- package/dist/core/scheduling/encoded-schedule.store.js +54 -0
- package/dist/core/scheduling/in-memory-schedule.store.d.ts +20 -0
- package/dist/core/scheduling/in-memory-schedule.store.js +90 -0
- package/dist/core/scheduling/index.d.ts +3 -0
- package/dist/core/scheduling/index.js +3 -0
- package/dist/core/scheduling/rrule.util.d.ts +55 -0
- package/dist/core/scheduling/rrule.util.js +371 -0
- package/dist/core/scheduling/schedule-spec.d.ts +57 -0
- package/dist/core/scheduling/schedule-spec.js +233 -0
- package/dist/core/scheduling/scheduler.d.ts +134 -0
- package/dist/core/scheduling/scheduler.js +450 -0
- package/dist/core/scheduling/time-zone.util.d.ts +18 -0
- package/dist/core/scheduling/time-zone.util.js +88 -0
- package/dist/core/time/clock.d.ts +28 -0
- package/dist/core/time/clock.js +35 -0
- package/dist/core/time/duration.d.ts +21 -0
- package/dist/core/time/duration.js +30 -0
- package/dist/core/time/index.d.ts +2 -0
- package/dist/core/time/index.js +2 -0
- package/dist/core/utils/canonical.util.d.ts +2 -0
- package/dist/core/utils/canonical.util.js +4 -0
- package/dist/core/utils/maybe.util.d.ts +8 -0
- package/dist/core/utils/maybe.util.js +14 -0
- package/dist/core/utils/payload-place.util.d.ts +10 -0
- package/dist/core/utils/payload-place.util.js +32 -0
- package/dist/core/workers/index.d.ts +1 -0
- package/dist/core/workers/index.js +1 -0
- package/dist/core/workers/leased-worker.d.ts +86 -0
- package/dist/core/workers/leased-worker.js +280 -0
- package/dist/cqrs/decorators/index.d.ts +2 -0
- package/dist/cqrs/decorators/index.js +2 -0
- package/dist/cqrs/decorators/signal-on.decorator.d.ts +18 -0
- package/dist/cqrs/decorators/signal-on.decorator.js +34 -0
- package/dist/cqrs/decorators/start-on.decorator.d.ts +18 -0
- package/dist/cqrs/decorators/start-on.decorator.js +49 -0
- package/dist/cqrs/index.d.ts +3 -0
- package/dist/cqrs/index.js +5 -0
- package/dist/cqrs/interfaces/index.d.ts +4 -0
- package/dist/cqrs/interfaces/index.js +4 -0
- package/dist/cqrs/interfaces/signal-on-options.interface.d.ts +22 -0
- package/dist/cqrs/interfaces/signal-on-options.interface.js +1 -0
- package/dist/cqrs/interfaces/start-on-options.interface.d.ts +23 -0
- package/dist/cqrs/interfaces/start-on-options.interface.js +1 -0
- package/dist/cqrs/interfaces/workflow-dispatcher-context.interface.d.ts +10 -0
- package/dist/cqrs/interfaces/workflow-dispatcher-context.interface.js +1 -0
- package/dist/cqrs/interfaces/workflow-event-route.interface.d.ts +20 -0
- package/dist/cqrs/interfaces/workflow-event-route.interface.js +1 -0
- package/dist/cqrs/utils/event-routes.util.d.ts +7 -0
- package/dist/cqrs/utils/event-routes.util.js +22 -0
- package/dist/cqrs/workflow-event.publisher.d.ts +52 -0
- package/dist/cqrs/workflow-event.publisher.js +257 -0
- package/dist/cqrs/workflow-events.explorer.d.ts +27 -0
- package/dist/cqrs/workflow-events.explorer.js +92 -0
- package/dist/cqrs/workflows-cqrs.module.d.ts +10 -0
- package/dist/cqrs/workflows-cqrs.module.js +27 -0
- package/dist/decorators/index.d.ts +1 -0
- package/dist/decorators/index.js +1 -0
- package/dist/decorators/workflow.decorator.d.ts +7 -0
- package/dist/decorators/workflow.decorator.js +52 -0
- package/dist/errors/child-workflow-failed.error.d.ts +13 -0
- package/dist/errors/child-workflow-failed.error.js +14 -0
- package/dist/errors/index.d.ts +13 -0
- package/dist/errors/index.js +13 -0
- package/dist/errors/non-retryable-step.error.d.ts +4 -0
- package/dist/errors/non-retryable-step.error.js +7 -0
- package/dist/errors/step-failed.error.d.ts +15 -0
- package/dist/errors/step-failed.error.js +16 -0
- package/dist/errors/step-timeout.error.d.ts +5 -0
- package/dist/errors/step-timeout.error.js +8 -0
- package/dist/errors/workflow-definition.error.d.ts +10 -0
- package/dist/errors/workflow-definition.error.js +13 -0
- package/dist/errors/workflow-failed.error.d.ts +20 -0
- package/dist/errors/workflow-failed.error.js +17 -0
- package/dist/errors/workflow-id-conflict.error.d.ts +6 -0
- package/dist/errors/workflow-id-conflict.error.js +9 -0
- package/dist/errors/workflow-interrupt.error.d.ts +32 -0
- package/dist/errors/workflow-interrupt.error.js +19 -0
- package/dist/errors/workflow-non-determinism.error.d.ts +4 -0
- package/dist/errors/workflow-non-determinism.error.js +7 -0
- package/dist/errors/workflow-not-found.error.d.ts +10 -0
- package/dist/errors/workflow-not-found.error.js +13 -0
- package/dist/errors/workflow-result-timeout.error.d.ts +11 -0
- package/dist/errors/workflow-result-timeout.error.js +13 -0
- package/dist/errors/workflow-state.error.d.ts +11 -0
- package/dist/errors/workflow-state.error.js +14 -0
- package/dist/errors/workflow.error.d.ts +7 -0
- package/dist/errors/workflow.error.js +7 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.js +3 -0
- package/dist/events/workflow-events.interface.d.ts +145 -0
- package/dist/events/workflow-events.interface.js +1 -0
- package/dist/events/workflow-events.service.d.ts +17 -0
- package/dist/events/workflow-events.service.js +38 -0
- package/dist/events/workflows.channels.d.ts +3 -0
- package/dist/events/workflows.channels.js +9 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +23 -0
- package/dist/interfaces/index.d.ts +13 -0
- package/dist/interfaces/index.js +13 -0
- package/dist/interfaces/serialized-workflow-error.interface.d.ts +6 -0
- package/dist/interfaces/serialized-workflow-error.interface.js +1 -0
- package/dist/interfaces/workflow-client.interface.d.ts +156 -0
- package/dist/interfaces/workflow-client.interface.js +1 -0
- package/dist/interfaces/workflow-clock.interface.d.ts +7 -0
- package/dist/interfaces/workflow-clock.interface.js +1 -0
- package/dist/interfaces/workflow-context.interface.d.ts +241 -0
- package/dist/interfaces/workflow-context.interface.js +1 -0
- package/dist/interfaces/workflow-decorator-options.interface.d.ts +101 -0
- package/dist/interfaces/workflow-decorator-options.interface.js +1 -0
- package/dist/interfaces/workflow-instance.interface.d.ts +83 -0
- package/dist/interfaces/workflow-instance.interface.js +1 -0
- package/dist/interfaces/workflow-payload-codec.interface.d.ts +32 -0
- package/dist/interfaces/workflow-payload-codec.interface.js +1 -0
- package/dist/interfaces/workflow-retry-options.interface.d.ts +9 -0
- package/dist/interfaces/workflow-retry-options.interface.js +1 -0
- package/dist/interfaces/workflow-runner.interface.d.ts +11 -0
- package/dist/interfaces/workflow-runner.interface.js +1 -0
- package/dist/interfaces/workflow-schedule.interface.d.ts +104 -0
- package/dist/interfaces/workflow-schedule.interface.js +1 -0
- package/dist/interfaces/workflow-storage.interface.d.ts +4 -0
- package/dist/interfaces/workflow-storage.interface.js +1 -0
- package/dist/interfaces/workflow-store.interface.d.ts +488 -0
- package/dist/interfaces/workflow-store.interface.js +1 -0
- package/dist/interfaces/workflows-module-options.interface.d.ts +80 -0
- package/dist/interfaces/workflows-module-options.interface.js +1 -0
- package/dist/mysql/index.d.ts +5 -0
- package/dist/mysql/index.js +8 -0
- package/dist/mysql/interfaces/index.d.ts +1 -0
- package/dist/mysql/interfaces/index.js +1 -0
- package/dist/mysql/interfaces/mysql-workflow-store-options.interface.d.ts +25 -0
- package/dist/mysql/interfaces/mysql-workflow-store-options.interface.js +1 -0
- package/dist/mysql/key-limits.d.ts +30 -0
- package/dist/mysql/key-limits.js +41 -0
- package/dist/mysql/migrations/index.d.ts +7 -0
- package/dist/mysql/migrations/index.js +16 -0
- package/dist/mysql/migrations/initial.migration.d.ts +9 -0
- package/dist/mysql/migrations/initial.migration.js +104 -0
- package/dist/mysql/mysql-workflow.store.d.ts +197 -0
- package/dist/mysql/mysql-workflow.store.js +828 -0
- package/dist/postgres/errors/index.d.ts +1 -0
- package/dist/postgres/errors/index.js +2 -0
- package/dist/postgres/index.d.ts +5 -0
- package/dist/postgres/index.js +8 -0
- package/dist/postgres/interfaces/index.d.ts +1 -0
- package/dist/postgres/interfaces/index.js +1 -0
- package/dist/postgres/interfaces/postgres-workflow-store-options.interface.d.ts +22 -0
- package/dist/postgres/interfaces/postgres-workflow-store-options.interface.js +1 -0
- package/dist/postgres/migrations/index.d.ts +6 -0
- package/dist/postgres/migrations/index.js +15 -0
- package/dist/postgres/migrations/initial.migration.d.ts +6 -0
- package/dist/postgres/migrations/initial.migration.js +106 -0
- package/dist/postgres/postgres-workflow.store.d.ts +132 -0
- package/dist/postgres/postgres-workflow.store.js +586 -0
- package/dist/services/index.d.ts +6 -0
- package/dist/services/index.js +6 -0
- package/dist/services/workflow-client.service.d.ts +146 -0
- package/dist/services/workflow-client.service.js +436 -0
- package/dist/services/workflow-execution.service.d.ts +267 -0
- package/dist/services/workflow-execution.service.js +1076 -0
- package/dist/services/workflow-registry.service.d.ts +56 -0
- package/dist/services/workflow-registry.service.js +172 -0
- package/dist/services/workflow-scheduler.service.d.ts +54 -0
- package/dist/services/workflow-scheduler.service.js +173 -0
- package/dist/services/workflow-schedules.service.d.ts +60 -0
- package/dist/services/workflow-schedules.service.js +193 -0
- package/dist/services/workflow-worker.service.d.ts +92 -0
- package/dist/services/workflow-worker.service.js +468 -0
- package/dist/signals/index.d.ts +1 -0
- package/dist/signals/index.js +1 -0
- package/dist/signals/workflow.signal.d.ts +17 -0
- package/dist/signals/workflow.signal.js +28 -0
- package/dist/sql/rate-windows.d.ts +41 -0
- package/dist/sql/rate-windows.js +53 -0
- package/dist/sql/workflow-rows.d.ts +16 -0
- package/dist/sql/workflow-rows.js +102 -0
- package/dist/sql/workflow-schema.error.d.ts +21 -0
- package/dist/sql/workflow-schema.error.js +15 -0
- package/dist/storage/encoded-workflow.store.d.ts +71 -0
- package/dist/storage/encoded-workflow.store.js +201 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/workflow.storage.d.ts +55 -0
- package/dist/storage/workflow.storage.js +159 -0
- package/dist/stores/in-memory-workflow.store.d.ts +75 -0
- package/dist/stores/in-memory-workflow.store.js +380 -0
- package/dist/stores/index.d.ts +1 -0
- package/dist/stores/index.js +1 -0
- package/dist/testing/assertions.d.ts +10 -0
- package/dist/testing/assertions.js +72 -0
- package/dist/testing/index.d.ts +38 -0
- package/dist/testing/index.js +1089 -0
- package/dist/testing/schedule-store.contract.d.ts +33 -0
- package/dist/testing/schedule-store.contract.js +161 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/journal-limits.util.d.ts +6 -0
- package/dist/utils/journal-limits.util.js +25 -0
- package/dist/utils/new-instance.util.d.ts +26 -0
- package/dist/utils/new-instance.util.js +87 -0
- package/dist/utils/normalize.util.d.ts +5 -0
- package/dist/utils/normalize.util.js +11 -0
- package/dist/utils/run-timeout.util.d.ts +3 -0
- package/dist/utils/run-timeout.util.js +15 -0
- package/dist/utils/schedule-spec.util.d.ts +16 -0
- package/dist/utils/schedule-spec.util.js +9 -0
- package/dist/utils/schedule-store.util.d.ts +15 -0
- package/dist/utils/schedule-store.util.js +44 -0
- package/dist/utils/serialize-error.util.d.ts +7 -0
- package/dist/utils/serialize-error.util.js +22 -0
- package/dist/utils/step-scope.util.d.ts +16 -0
- package/dist/utils/step-scope.util.js +38 -0
- package/dist/workflows.constants.d.ts +10 -0
- package/dist/workflows.constants.js +10 -0
- package/dist/workflows.module-definition.d.ts +4 -0
- package/dist/workflows.module-definition.js +6 -0
- package/dist/workflows.module.d.ts +24 -0
- package/dist/workflows.module.js +68 -0
- package/package.json +123 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
(The MIT License)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kamil Mysliwiec
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
'Software'), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
[travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master
|
|
6
|
+
[travis-url]: https://travis-ci.org/nestjs/nest
|
|
7
|
+
[linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux
|
|
8
|
+
[linux-url]: https://travis-ci.org/nestjs/nest
|
|
9
|
+
|
|
10
|
+
<p align="center">A progressive <a href="http://nodejs.org" target="blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
|
13
|
+
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
|
14
|
+
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/dm/@nestjs/core.svg" alt="NPM Downloads" /></a>
|
|
15
|
+
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
|
16
|
+
<a href="https://opencollective.com/nest#backer"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
|
17
|
+
<a href="https://opencollective.com/nest#sponsor"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
|
18
|
+
<a href="https://paypal.me/kamilmysliwiec"><img src="https://img.shields.io/badge/Donate-PayPal-dc3d53.svg"/></a>
|
|
19
|
+
<a href="https://twitter.com/nestframework"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
|
|
20
|
+
</p>
|
|
21
|
+
<!--[](https://opencollective.com/nest#backer)
|
|
22
|
+
[](https://opencollective.com/nest#sponsor)-->
|
|
23
|
+
|
|
24
|
+
## Description
|
|
25
|
+
|
|
26
|
+
Durable workflows module for [Nest](https://github.com/nestjs/nest): long-running, multi-step processes written as plain async code that survive restarts, with journaled steps, durable timers, signals and compensations, backed by your own database and with no third-party dependencies.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
$ npm i --save @nestjs/workflows
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
[Overview & Tutorial](https://docs.nestjs.com/reliability/workflows)
|
|
37
|
+
|
|
38
|
+
## Support
|
|
39
|
+
|
|
40
|
+
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
|
41
|
+
|
|
42
|
+
## Stay in touch
|
|
43
|
+
|
|
44
|
+
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
|
45
|
+
- Website - [https://nestjs.com](https://nestjs.com/)
|
|
46
|
+
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
Nest is [MIT licensed](LICENSE).
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The `nest-workflows` command: the first-party stores' migrations from a shell or a CI step. `migrate` and `status`
|
|
3
|
+
// pick the store by the URL (postgres:// or postgresql://: PostgresWorkflowStore; mysql://: MySqlWorkflowStore), and
|
|
4
|
+
// `sql` prints PostgreSQL's unless `--dialect mysql` says otherwise.
|
|
5
|
+
import { runStoreCli } from '@nestjs/store-kit';
|
|
6
|
+
import { mysqlWorkflowStoreSchema } from './mysql/migrations/index.js';
|
|
7
|
+
import { workflowStoreSchema } from './postgres/migrations/index.js';
|
|
8
|
+
process.exitCode = await runStoreCli([workflowStoreSchema, mysqlWorkflowStoreSchema], process.argv.slice(2));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { PayloadCodec, PayloadContext } from '../interfaces/payload-codec.interface.js';
|
|
2
|
+
/**
|
|
3
|
+
* What `AesGcmPayloadCodec` takes.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* const options: AesGcmPayloadCodecOptions = { keys: { '2026-09': process.env.PAYLOADS_KEY! }, current: '2026-09' };
|
|
7
|
+
* ```
|
|
8
|
+
*/
|
|
9
|
+
export interface AesGcmPayloadCodecOptions {
|
|
10
|
+
/**
|
|
11
|
+
* The keys by id (letters, digits, `_` and `-`): 32 random bytes each (AES-256), as a `Buffer` or `Uint8Array`,
|
|
12
|
+
* or as base64 (`openssl rand -base64 32`). Keep a key here while payloads it encrypted may still be read.
|
|
13
|
+
*/
|
|
14
|
+
keys: Record<string, string | Uint8Array>;
|
|
15
|
+
/** The id of the key that encrypts. The others only decrypt: to rotate, add a key and make it `current`. */
|
|
16
|
+
current: string;
|
|
17
|
+
/** Deflate a payload of at least this many bytes (as JSON) before encrypting it; `false` never. Default 1024. */
|
|
18
|
+
compress?: number | false;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Encrypts payloads with AES-256-GCM (`node:crypto`), under a key derived for each payload from the configured one
|
|
22
|
+
* (HKDF-SHA256 with a random salt) and a random IV, so no key nears the 2^32 encryptions random IVs allow, and with
|
|
23
|
+
* the payload's context as additional authenticated data, so a payload that was changed, or moved to another place
|
|
24
|
+
* (another instance, journal entry, job or field), fails to decrypt instead of being read. The key's id is stored with
|
|
25
|
+
* each payload, so rotating keys needs no migration: payloads are decrypted with the key that encrypted them, and
|
|
26
|
+
* written with the current one. Payloads of 1 KiB or more are deflated first. Its `id` is `'aes-256-gcm'`.
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* WorkflowsModule.forRoot({
|
|
30
|
+
* codec: new AesGcmPayloadCodec({ keys: { '2026-09': process.env.WORKFLOWS_KEY! }, current: '2026-09' }),
|
|
31
|
+
* })
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare class AesGcmPayloadCodec implements PayloadCodec {
|
|
35
|
+
readonly id = "aes-256-gcm";
|
|
36
|
+
private readonly keys;
|
|
37
|
+
private readonly current;
|
|
38
|
+
private readonly threshold;
|
|
39
|
+
constructor(options: AesGcmPayloadCodecOptions);
|
|
40
|
+
encode(value: unknown, context: PayloadContext): string;
|
|
41
|
+
decode(data: string, context: PayloadContext): unknown;
|
|
42
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from 'node:crypto';
|
|
2
|
+
import { deflateRawSync, inflateRawSync } from 'node:zlib';
|
|
3
|
+
import { describePlace, otherPlaces, WORKFLOW_PLACES } from '../utils/payload-place.util.js';
|
|
4
|
+
const SALT_BYTES = 16;
|
|
5
|
+
const IV_BYTES = 12;
|
|
6
|
+
const TAG_BYTES = 16;
|
|
7
|
+
const HEADER_BYTES = SALT_BYTES + IV_BYTES;
|
|
8
|
+
/** The first byte of every plaintext: its format. Bit 0: deflated. */
|
|
9
|
+
const DEFLATED = 1;
|
|
10
|
+
/**
|
|
11
|
+
* Encrypts payloads with AES-256-GCM (`node:crypto`), under a key derived for each payload from the configured one
|
|
12
|
+
* (HKDF-SHA256 with a random salt) and a random IV, so no key nears the 2^32 encryptions random IVs allow, and with
|
|
13
|
+
* the payload's context as additional authenticated data, so a payload that was changed, or moved to another place
|
|
14
|
+
* (another instance, journal entry, job or field), fails to decrypt instead of being read. The key's id is stored with
|
|
15
|
+
* each payload, so rotating keys needs no migration: payloads are decrypted with the key that encrypted them, and
|
|
16
|
+
* written with the current one. Payloads of 1 KiB or more are deflated first. Its `id` is `'aes-256-gcm'`.
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* WorkflowsModule.forRoot({
|
|
20
|
+
* codec: new AesGcmPayloadCodec({ keys: { '2026-09': process.env.WORKFLOWS_KEY! }, current: '2026-09' }),
|
|
21
|
+
* })
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export class AesGcmPayloadCodec {
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.id = 'aes-256-gcm';
|
|
27
|
+
this.keys = new Map();
|
|
28
|
+
if (options === null || typeof options !== 'object' || options.keys === null || typeof options.keys !== 'object') {
|
|
29
|
+
throw new TypeError('AesGcmPayloadCodec: pass { keys: { [keyId]: key }, current: keyId }.');
|
|
30
|
+
}
|
|
31
|
+
for (const [id, key] of Object.entries(options.keys)) {
|
|
32
|
+
if (!/^[\w-]+$/.test(id)) {
|
|
33
|
+
throw new TypeError(`AesGcmPayloadCodec: invalid key id ${JSON.stringify(id)}. Use letters, digits, "_" and "-".`);
|
|
34
|
+
}
|
|
35
|
+
this.keys.set(id, keyBytes(id, key));
|
|
36
|
+
}
|
|
37
|
+
if (!this.keys.has(options.current)) {
|
|
38
|
+
throw new TypeError(`AesGcmPayloadCodec: current (${JSON.stringify(options.current)}) must be one of the keys' ids (${[...this.keys.keys()].join(', ') || 'none'}).`);
|
|
39
|
+
}
|
|
40
|
+
this.current = options.current;
|
|
41
|
+
const compress = options.compress ?? 1_024;
|
|
42
|
+
if (compress !== false && (!Number.isSafeInteger(compress) || compress < 0)) {
|
|
43
|
+
throw new TypeError(`AesGcmPayloadCodec: compress (${JSON.stringify(compress)}) must be a number of bytes, or false.`);
|
|
44
|
+
}
|
|
45
|
+
this.threshold = compress === false ? Infinity : compress;
|
|
46
|
+
}
|
|
47
|
+
encode(value, context) {
|
|
48
|
+
const json = Buffer.from(JSON.stringify(value), 'utf8');
|
|
49
|
+
const deflated = json.length >= this.threshold;
|
|
50
|
+
const plaintext = Buffer.concat([Buffer.of(deflated ? DEFLATED : 0), deflated ? deflateRawSync(json) : json]);
|
|
51
|
+
const header = randomBytes(HEADER_BYTES);
|
|
52
|
+
const cipher = createCipheriv('aes-256-gcm', derived(this.keys.get(this.current), header), header.subarray(SALT_BYTES));
|
|
53
|
+
cipher.setAAD(associated(this.current, context));
|
|
54
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
55
|
+
return `${this.current}.${Buffer.concat([header, ciphertext, cipher.getAuthTag()]).toString('base64url')}`;
|
|
56
|
+
}
|
|
57
|
+
decode(data, context) {
|
|
58
|
+
const dot = data.indexOf('.');
|
|
59
|
+
const id = data.slice(0, dot);
|
|
60
|
+
const key = this.keys.get(id);
|
|
61
|
+
if (dot < 0 || !key) {
|
|
62
|
+
throw new Error(`AesGcmPayloadCodec: a payload of ${describePlace(context)} was encrypted with key ${JSON.stringify(id)}, which isn't in keys. ` +
|
|
63
|
+
'Keep a key in keys while payloads it encrypted may still be read.');
|
|
64
|
+
}
|
|
65
|
+
const bytes = Buffer.from(data.slice(dot + 1), 'base64url');
|
|
66
|
+
let plaintext;
|
|
67
|
+
try {
|
|
68
|
+
const decipher = createDecipheriv('aes-256-gcm', derived(key, bytes), bytes.subarray(SALT_BYTES, HEADER_BYTES));
|
|
69
|
+
decipher.setAAD(associated(id, context));
|
|
70
|
+
decipher.setAuthTag(bytes.subarray(bytes.length - TAG_BYTES));
|
|
71
|
+
plaintext = Buffer.concat([decipher.update(bytes.subarray(HEADER_BYTES, bytes.length - TAG_BYTES)), decipher.final()]);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new Error(`AesGcmPayloadCodec: a payload of ${describePlace(context)} failed authentication: it was changed, or encrypted for another place.`);
|
|
75
|
+
}
|
|
76
|
+
const body = plaintext.subarray(1);
|
|
77
|
+
return JSON.parse(((plaintext[0] & DEFLATED) === DEFLATED ? inflateRawSync(body) : body).toString('utf8'));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function keyBytes(id, key) {
|
|
81
|
+
const advice = 'Give it 32 random bytes, as a Buffer, a Uint8Array, or base64 (`openssl rand -base64 32`).';
|
|
82
|
+
let bytes;
|
|
83
|
+
if (key instanceof Uint8Array) {
|
|
84
|
+
bytes = Buffer.from(key);
|
|
85
|
+
}
|
|
86
|
+
else if (typeof key === 'string') {
|
|
87
|
+
// Buffer.from() skips what isn't base64, so a key with a typo would decode to other bytes instead of failing.
|
|
88
|
+
const text = key.trim();
|
|
89
|
+
if (text.length === 0) {
|
|
90
|
+
throw new TypeError(`AesGcmPayloadCodec: key "${id}" is an empty string. ${advice}`);
|
|
91
|
+
}
|
|
92
|
+
if (!/^[A-Za-z0-9+/_-]+={0,2}$/.test(text)) {
|
|
93
|
+
throw new TypeError(`AesGcmPayloadCodec: key "${id}" isn't base64. ${advice}`);
|
|
94
|
+
}
|
|
95
|
+
bytes = Buffer.from(text, 'base64');
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const what = key === undefined ? "undefined (an environment variable that isn't set?)" : key === null ? 'null' : typeof key === 'object' ? 'an object' : `a ${typeof key}`;
|
|
99
|
+
throw new TypeError(`AesGcmPayloadCodec: key "${id}" is ${what}. ${advice}`);
|
|
100
|
+
}
|
|
101
|
+
if (bytes.length !== 32) {
|
|
102
|
+
throw new TypeError(`AesGcmPayloadCodec: key "${id}" is ${bytes.length} bytes; AES-256 takes 32 (as a Buffer, or base64: \`openssl rand -base64 32\`).`);
|
|
103
|
+
}
|
|
104
|
+
return bytes;
|
|
105
|
+
}
|
|
106
|
+
/** The payload's own key: HKDF-SHA256 of the configured key, with the salt that starts the payload's bytes. */
|
|
107
|
+
function derived(key, bytes) {
|
|
108
|
+
return Buffer.from(hkdfSync('sha256', key, bytes.subarray(0, SALT_BYTES), 'nestjs-workflows payload', 32));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The additional authenticated data: the key's id, then the context: its `field` and workflows' places (`null` where
|
|
112
|
+
* absent) first, in the order this codec was built with, so what it encrypted then still decrypts, then any other
|
|
113
|
+
* key, by name, with its value.
|
|
114
|
+
*/
|
|
115
|
+
function associated(key, context) {
|
|
116
|
+
const places = WORKFLOW_PLACES.map((place) => context[place] ?? null);
|
|
117
|
+
const others = otherPlaces(context).flatMap((place) => [place, context[place]]);
|
|
118
|
+
return Buffer.from(JSON.stringify([key, context.field, ...places, ...others]), 'utf8');
|
|
119
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { PayloadCodec, PayloadContext, SerializedError } from '../interfaces/payload-codec.interface.js';
|
|
2
|
+
import { type Maybe } from '../utils/maybe.util.js';
|
|
3
|
+
/**
|
|
4
|
+
* What `PayloadCodecs` names in its errors.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* new PayloadCodecs(codecs, { name: "QueuesModule's codec", type: 'QueuePayloadCodec' });
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
export interface PayloadCodecsOptions {
|
|
11
|
+
/** The option the codecs come from, such as `"WorkflowsModule's codec"`. Default `"the module's codec"`. */
|
|
12
|
+
name?: string;
|
|
13
|
+
/** What a codec implements, such as `'WorkflowPayloadCodec'`. Default `'PayloadCodec'`. */
|
|
14
|
+
type?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A package's payload codecs and the family's envelope, which stores what they encode as
|
|
18
|
+
* `$wf1:<codec id>:<encoded>`, so every package encodes alike and a codec reads what another package wrote. The
|
|
19
|
+
* first codec encodes; each payload is decoded by the codec whose id it carries, so a codec that was replaced keeps
|
|
20
|
+
* decoding what it wrote (list it after the new one). `null` and `undefined` stay as they are. Payloads stored before
|
|
21
|
+
* any codec was set are read as they are, and without codecs nothing is encoded, but a string that looks like an
|
|
22
|
+
* envelope is wrapped as one (`plain`) rather than stored as it is, and an encoded payload no listed codec knows fails
|
|
23
|
+
* to decode instead of being read as a string.
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* const codecs = new PayloadCodecs([new AesGcmPayloadCodec({ keys, current: '2026-09' })], { name: "QueuesModule's codec" });
|
|
27
|
+
* const stored = await codecs.encode(job.data, { field: 'data', queue: 'emails', job: job.id });
|
|
28
|
+
* const data = await codecs.decode(row.data, { field: 'data', queue: 'emails', job: row.id });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare class PayloadCodecs<C extends PayloadContext = PayloadContext> {
|
|
32
|
+
private readonly writer;
|
|
33
|
+
private readonly byId;
|
|
34
|
+
private readonly name;
|
|
35
|
+
constructor(codecs: PayloadCodec<C>[], options?: PayloadCodecsOptions);
|
|
36
|
+
/** Whether a codec encodes what is written (else payloads are stored as they are). */
|
|
37
|
+
get encodes(): boolean;
|
|
38
|
+
/** `value` in an envelope, or as it is (no codec, `null`, `undefined`). Synchronous when the codec is. */
|
|
39
|
+
encode(value: unknown, context: C): Maybe<unknown>;
|
|
40
|
+
/** The value in an envelope, or `stored` as it is when it isn't one. Throws for a codec that isn't listed. */
|
|
41
|
+
decode(stored: unknown, context: C): Maybe<unknown>;
|
|
42
|
+
/**
|
|
43
|
+
* An error with its `message` and `stack` encoded together into `message`, and its `name` as it is (the error's
|
|
44
|
+
* class, which the store may filter on, not the application's data); a nested error (a field holding another
|
|
45
|
+
* `SerializedError`) likewise, and nothing else. Without a codec, as it is, unless a message starts like an envelope.
|
|
46
|
+
*/
|
|
47
|
+
encodeError<E extends SerializedError | null | undefined>(error: E, context: C): Maybe<E>;
|
|
48
|
+
/** What `encodeError()` encoded, decoded; an error stored as it is comes back as it is. */
|
|
49
|
+
decodeError<E extends SerializedError | null | undefined>(error: E, context: C): Promise<E>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Whether a stored value is an encoded payload, in the family's envelope, rather than one stored as it is: a string
|
|
53
|
+
* starting with `$wf1:`. For stores that skip decoding what needs none.
|
|
54
|
+
*
|
|
55
|
+
* ```ts
|
|
56
|
+
* const payload = isEncodedPayload(row.data) ? await codecs.decode(row.data, context) : row.data;
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare function isEncodedPayload(value: unknown): value is string;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { andThen } from '../utils/maybe.util.js';
|
|
2
|
+
import { describePlace } from '../utils/payload-place.util.js';
|
|
3
|
+
/**
|
|
4
|
+
* An encoded payload as stored: `$wf1:<codec id>:<what the codec returned>`. The prefix tells it from a payload
|
|
5
|
+
* stored as it is (without a codec), and the codec's id picks the codec that decodes it. A payload stored as it is
|
|
6
|
+
* can't start with the prefix: a string that does (the application's data) is stored as a `plain` envelope of its
|
|
7
|
+
* JSON instead, so no payload is ever mistaken for another's envelope.
|
|
8
|
+
*/
|
|
9
|
+
const ENVELOPE = '$wf1:';
|
|
10
|
+
/** The envelope's own codec, for a string that starts like one when no codec encodes it: its JSON. */
|
|
11
|
+
const PLAIN = {
|
|
12
|
+
id: 'plain',
|
|
13
|
+
encode: (value) => JSON.stringify(value),
|
|
14
|
+
decode: (data) => JSON.parse(data),
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* A package's payload codecs and the family's envelope, which stores what they encode as
|
|
18
|
+
* `$wf1:<codec id>:<encoded>`, so every package encodes alike and a codec reads what another package wrote. The
|
|
19
|
+
* first codec encodes; each payload is decoded by the codec whose id it carries, so a codec that was replaced keeps
|
|
20
|
+
* decoding what it wrote (list it after the new one). `null` and `undefined` stay as they are. Payloads stored before
|
|
21
|
+
* any codec was set are read as they are, and without codecs nothing is encoded, but a string that looks like an
|
|
22
|
+
* envelope is wrapped as one (`plain`) rather than stored as it is, and an encoded payload no listed codec knows fails
|
|
23
|
+
* to decode instead of being read as a string.
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* const codecs = new PayloadCodecs([new AesGcmPayloadCodec({ keys, current: '2026-09' })], { name: "QueuesModule's codec" });
|
|
27
|
+
* const stored = await codecs.encode(job.data, { field: 'data', queue: 'emails', job: job.id });
|
|
28
|
+
* const data = await codecs.decode(row.data, { field: 'data', queue: 'emails', job: row.id });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export class PayloadCodecs {
|
|
32
|
+
constructor(codecs, options = {}) {
|
|
33
|
+
this.name = options.name ?? "the module's codec";
|
|
34
|
+
const ids = new Set();
|
|
35
|
+
for (const codec of codecs) {
|
|
36
|
+
if (codec === null || typeof codec !== 'object' || typeof codec.encode !== 'function' || typeof codec.decode !== 'function') {
|
|
37
|
+
throw new TypeError(`${this.name}: expected a ${options.type ?? 'PayloadCodec'} (an object with id, encode() and decode()), got ${String(codec)}.`);
|
|
38
|
+
}
|
|
39
|
+
if (typeof codec.id !== 'string' || !/^[\w.-]+$/.test(codec.id) || codec.id === PLAIN.id) {
|
|
40
|
+
throw new TypeError(`${this.name}: ${codec.constructor.name} has an invalid id ${JSON.stringify(codec.id)}. Use letters, digits, ".", "_" and "-" ` +
|
|
41
|
+
`("${PLAIN.id}" is the envelope's).`);
|
|
42
|
+
}
|
|
43
|
+
if (ids.has(codec.id)) {
|
|
44
|
+
throw new TypeError(`${this.name}: two codecs have the id "${codec.id}". Give each its own.`);
|
|
45
|
+
}
|
|
46
|
+
ids.add(codec.id);
|
|
47
|
+
}
|
|
48
|
+
this.writer = codecs[0];
|
|
49
|
+
this.byId = new Map([...codecs, PLAIN].map((codec) => [codec.id, codec]));
|
|
50
|
+
}
|
|
51
|
+
/** Whether a codec encodes what is written (else payloads are stored as they are). */
|
|
52
|
+
get encodes() {
|
|
53
|
+
return this.writer !== undefined;
|
|
54
|
+
}
|
|
55
|
+
/** `value` in an envelope, or as it is (no codec, `null`, `undefined`). Synchronous when the codec is. */
|
|
56
|
+
encode(value, context) {
|
|
57
|
+
const writer = this.writer ?? (isEncodedPayload(value) ? PLAIN : undefined);
|
|
58
|
+
if (value === undefined || value === null || !writer) {
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
return andThen(writer.encode(value, context), (data) => `${ENVELOPE}${writer.id}:${data}`);
|
|
62
|
+
}
|
|
63
|
+
/** The value in an envelope, or `stored` as it is when it isn't one. Throws for a codec that isn't listed. */
|
|
64
|
+
decode(stored, context) {
|
|
65
|
+
if (!isEncodedPayload(stored)) {
|
|
66
|
+
return stored;
|
|
67
|
+
}
|
|
68
|
+
const end = stored.indexOf(':', ENVELOPE.length);
|
|
69
|
+
const id = stored.slice(ENVELOPE.length, end);
|
|
70
|
+
const codec = this.byId.get(id);
|
|
71
|
+
if (end < 0 || !codec) {
|
|
72
|
+
throw new Error(`A payload of ${describePlace(context)} was encoded by the codec "${id}", which ${this.name} option doesn't list. ` +
|
|
73
|
+
'Keep a codec listed (codec: [current, previous]) while payloads it encoded may still be read.');
|
|
74
|
+
}
|
|
75
|
+
return codec.decode(stored.slice(end + 1), context);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* An error with its `message` and `stack` encoded together into `message`, and its `name` as it is (the error's
|
|
79
|
+
* class, which the store may filter on, not the application's data); a nested error (a field holding another
|
|
80
|
+
* `SerializedError`) likewise, and nothing else. Without a codec, as it is, unless a message starts like an envelope.
|
|
81
|
+
*/
|
|
82
|
+
encodeError(error, context) {
|
|
83
|
+
if (error === null || error === undefined || (!this.writer && !messages(error).some(isEncodedPayload))) {
|
|
84
|
+
return error;
|
|
85
|
+
}
|
|
86
|
+
const secret = { message: error.message, ...(error.stack === undefined ? {} : { stack: error.stack }) };
|
|
87
|
+
const writer = this.writer ?? PLAIN;
|
|
88
|
+
const nested = nestedErrors(error);
|
|
89
|
+
return andThen(andThen(writer.encode(secret, context), (data) => `${ENVELOPE}${writer.id}:${data}`), (message) => andThen(all(nested.map(([, inner]) => this.encodeError(inner, context))), (encoded) => ({
|
|
90
|
+
name: error.name,
|
|
91
|
+
message,
|
|
92
|
+
...Object.fromEntries(nested.map(([field], i) => [field, encoded[i]])),
|
|
93
|
+
})));
|
|
94
|
+
}
|
|
95
|
+
/** What `encodeError()` encoded, decoded; an error stored as it is comes back as it is. */
|
|
96
|
+
async decodeError(error, context) {
|
|
97
|
+
if (!isEncodedPayload(error?.message)) {
|
|
98
|
+
return error;
|
|
99
|
+
}
|
|
100
|
+
const { message, stack } = (await this.decode(error.message, context));
|
|
101
|
+
const nested = await Promise.all(nestedErrors(error).map(async ([field, inner]) => [field, await this.decodeError(inner, context)]));
|
|
102
|
+
return { name: error.name, message, ...(stack === undefined ? {} : { stack }), ...Object.fromEntries(nested) };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Whether a stored value is an encoded payload, in the family's envelope, rather than one stored as it is: a string
|
|
107
|
+
* starting with `$wf1:`. For stores that skip decoding what needs none.
|
|
108
|
+
*
|
|
109
|
+
* ```ts
|
|
110
|
+
* const payload = isEncodedPayload(row.data) ? await codecs.decode(row.data, context) : row.data;
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export function isEncodedPayload(value) {
|
|
114
|
+
return typeof value === 'string' && value.startsWith(ENVELOPE);
|
|
115
|
+
}
|
|
116
|
+
/** The fields of `error` that hold nested errors (workflows' `compensation`), in their order. */
|
|
117
|
+
function nestedErrors(error) {
|
|
118
|
+
return Object.entries(error).filter((entry) => entry[0] !== 'name' && entry[0] !== 'message' && entry[0] !== 'stack' && isSerializedError(entry[1]));
|
|
119
|
+
}
|
|
120
|
+
/** The messages of `error` and of its nested errors. */
|
|
121
|
+
function messages(error) {
|
|
122
|
+
return [error.message, ...nestedErrors(error).flatMap(([, inner]) => messages(inner))];
|
|
123
|
+
}
|
|
124
|
+
function isSerializedError(value) {
|
|
125
|
+
return typeof value === 'object' && value !== null && typeof value.name === 'string' && typeof value.message === 'string';
|
|
126
|
+
}
|
|
127
|
+
/** Every value, synchronously when none is a promise. */
|
|
128
|
+
function all(values) {
|
|
129
|
+
return values.reduce((done, value) => andThen(done, (list) => andThen(value, (item) => [...list, item])), []);
|
|
130
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { ManualClock, parseDuration, systemClock, type Duration } from './time/index.js';
|
|
2
|
+
export type { Clock } from './interfaces/index.js';
|
|
3
|
+
export { backoffDelay, nextRetry, resolveRetry, type RetryDecision } from './retries/index.js';
|
|
4
|
+
export type { BackoffSettings, ResolvedBackoff, ResolvedRetry, RetrySettings } from './interfaces/index.js';
|
|
5
|
+
export { AesGcmPayloadCodec, isEncodedPayload, PayloadCodecs, type AesGcmPayloadCodecOptions, type PayloadCodecsOptions } from './codecs/index.js';
|
|
6
|
+
export type { PayloadCodec, PayloadContext, SerializedError } from './interfaces/index.js';
|
|
7
|
+
export { assertPriority, MAX_PRIORITY, rateWindowRoom, resolveConcurrency, resolveRateLimit, takeRateWindow } from './limits/index.js';
|
|
8
|
+
export type { ConcurrencyLimit, RateLimit, RateWindow, RateWindowState, ResolvedConcurrency, ResolvedRateLimit } from './interfaces/index.js';
|
|
9
|
+
export { ResultWaiter } from './results/index.js';
|
|
10
|
+
export type { ResultOutcome, ResultWaiterOptions, ResultWaitOptions } from './interfaces/index.js';
|
|
11
|
+
export { assertScheduleId, nextOccurrences, occurrenceId, parseSchedule } from './scheduling/index.js';
|
|
12
|
+
export type { ScheduleMissed, ScheduleOccurrence, ScheduleOptions, ScheduleOverlap, ScheduleSpec, ScheduleTiming } from './interfaces/index.js';
|
|
13
|
+
export { InMemoryScheduleStore, Scheduler, type ScheduleFields } from './scheduling/index.js';
|
|
14
|
+
export type { DeclaredSchedule, DueOccurrence, ScheduleClaimRequest, ScheduleProduction, ScheduleQuery, ScheduleRecord, ScheduleSave, ScheduleSkip, ScheduleStore, ScheduleWrite, SchedulerLabels, SchedulerOptions, } from './interfaces/index.js';
|
|
15
|
+
export { LeasedWorker } from './workers/index.js';
|
|
16
|
+
export type { LeasedRun, LeasedWorkerOptions, LeaseRequest } from './interfaces/index.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// The `@nestjs/workflows/core` entry: the engine's storage-agnostic pieces, for building packages on them (a queue,
|
|
2
|
+
// say) so they behave and read like workflows. Applications import `@nestjs/workflows`; nothing here is needed to
|
|
3
|
+
// write or run workflows. Nothing here imports a driver.
|
|
4
|
+
// Time: durations, and the clock every timestamp comes from
|
|
5
|
+
export { ManualClock, parseDuration, systemClock } from './time/index.js';
|
|
6
|
+
// Retries: the family's retry settings, resolved, and whether and when to retry after a failure
|
|
7
|
+
export { backoffDelay, nextRetry, resolveRetry } from './retries/index.js';
|
|
8
|
+
// Payloads: codecs, the AES-GCM one, and the family's envelope that stores what they encode
|
|
9
|
+
export { AesGcmPayloadCodec, isEncodedPayload, PayloadCodecs } from './codecs/index.js';
|
|
10
|
+
// Limits: concurrency and rate limits, priorities, and rate-limit window arithmetic (the claims themselves are a store's)
|
|
11
|
+
export { assertPriority, MAX_PRIORITY, rateWindowRoom, resolveConcurrency, resolveRateLimit, takeRateWindow } from './limits/index.js';
|
|
12
|
+
// Results: waiting for an outcome, settled at once by this process or read from the store with a backoff
|
|
13
|
+
export { ResultWaiter } from './results/index.js';
|
|
14
|
+
// Scheduling: schedule specs (cron with seconds, every, rrule, time zones, bounds, missed and overlap policies),
|
|
15
|
+
// their occurrences, and the ids of the runs they start
|
|
16
|
+
export { assertScheduleId, nextOccurrences, occurrenceId, parseSchedule } from './scheduling/index.js';
|
|
17
|
+
// Scheduling: the engine (one run per occurrence, declared schedules in step with the code) and the store it runs
|
|
18
|
+
// on, which `scheduleStoreContract()` from `@nestjs/workflows/testing` checks
|
|
19
|
+
export { InMemoryScheduleStore, Scheduler } from './scheduling/index.js';
|
|
20
|
+
// Workers: a claim-execute loop under leases, renewed on a heartbeat, drained at shutdown
|
|
21
|
+
export { LeasedWorker } from './workers/index.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where every timestamp comes from (deadlines, retry times, lease expiry, schedule occurrences), so tests can move
|
|
3
|
+
* time without fake timers: `systemClock`, or a `ManualClock`.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* class Deadline {
|
|
7
|
+
* constructor(private readonly clock: Clock = systemClock) {}
|
|
8
|
+
*
|
|
9
|
+
* passed(at: number): boolean {
|
|
10
|
+
* return this.clock.now() >= at;
|
|
11
|
+
* }
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export interface Clock {
|
|
16
|
+
/** Epoch milliseconds. */
|
|
17
|
+
now(): number;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './clock.interface.js';
|
|
2
|
+
export * from './leased-worker-options.interface.js';
|
|
3
|
+
export * from './limits.interface.js';
|
|
4
|
+
export * from './payload-codec.interface.js';
|
|
5
|
+
export * from './result-waiter-options.interface.js';
|
|
6
|
+
export * from './retry-settings.interface.js';
|
|
7
|
+
export * from './schedule-store.interface.js';
|
|
8
|
+
export * from './schedule.interface.js';
|
|
9
|
+
export * from './scheduler-options.interface.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './clock.interface.js';
|
|
2
|
+
export * from './leased-worker-options.interface.js';
|
|
3
|
+
export * from './limits.interface.js';
|
|
4
|
+
export * from './payload-codec.interface.js';
|
|
5
|
+
export * from './result-waiter-options.interface.js';
|
|
6
|
+
export * from './retry-settings.interface.js';
|
|
7
|
+
export * from './schedule-store.interface.js';
|
|
8
|
+
export * from './schedule.interface.js';
|
|
9
|
+
export * from './scheduler-options.interface.js';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Duration } from '../time/duration.js';
|
|
2
|
+
import type { Clock } from './clock.interface.js';
|
|
3
|
+
/**
|
|
4
|
+
* The lease `LeasedWorker` asks its `claim` to take: every item the claim returns is leased to `owner` under `token`
|
|
5
|
+
* until `until`.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* claim: ({ owner, token, now, until }, limit) => store.claim({ owner, token, now, leaseUntil: until, limit }),
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export interface LeaseRequest {
|
|
12
|
+
/** The worker's id (`LeasedWorker.owner`), for the store's `leaseOwner`. */
|
|
13
|
+
owner: string;
|
|
14
|
+
/** A new token for this claim: the store fences every write under the lease with it. */
|
|
15
|
+
token: string;
|
|
16
|
+
/** The clock's time of the claim. */
|
|
17
|
+
now: number;
|
|
18
|
+
/** When the lease ends unless renewed: `now` plus the lease duration. */
|
|
19
|
+
until: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* One claimed item's run, which `execute` receives: its `signal` aborts when the lease is lost (a renewal found it
|
|
23
|
+
* gone) or the worker shuts down, and the worker renews its lease every `heartbeatInterval` until `execute` settles.
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* execute: async (job, run) => {
|
|
27
|
+
* const result = await handler(job.data, { signal: run.signal });
|
|
28
|
+
* if (!(await store.complete(job.id, job.token, result))) {
|
|
29
|
+
* run.loseLease(); // another worker took the job over
|
|
30
|
+
* }
|
|
31
|
+
* },
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export interface LeasedRun {
|
|
35
|
+
/** Aborts when the lease is lost or the worker shuts down: `leaseLost` tells which. */
|
|
36
|
+
readonly signal: AbortSignal;
|
|
37
|
+
/** The lease is gone: another worker may run the item now, so write nothing more under it. */
|
|
38
|
+
readonly leaseLost: boolean;
|
|
39
|
+
/** The worker stopped waiting for this run at shutdown (`shutdownTimeout`): it no longer renews, and the run should write nothing more. */
|
|
40
|
+
readonly detached: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Renews the lease now, as the heartbeat does (a long step that proves it's alive): `false` once it's lost. A renewal
|
|
43
|
+
* that fails (the store is unreachable) keeps the lease, which the next heartbeat renews before it runs out.
|
|
44
|
+
*/
|
|
45
|
+
renew(): Promise<boolean>;
|
|
46
|
+
/** Marks the lease lost, when a fenced write found it gone: aborts `signal` and stops renewing. */
|
|
47
|
+
loseLease(): void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* What `LeasedWorker` takes: how to claim, renew and execute items (the store's side), and the loop's settings, which
|
|
51
|
+
* default to workflows' (10 at once, a poll a second, 30 s leases renewed every third of that, a 10 s drain).
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* const worker = new LeasedWorker<Job>({
|
|
55
|
+
* concurrency: 5,
|
|
56
|
+
* claim: (lease, limit) => store.claimJobs(lease, limit),
|
|
57
|
+
* renew: (job, until) => store.renewJob(job.id, job.token, until),
|
|
58
|
+
* execute: (job, run) => runJob(job, run),
|
|
59
|
+
* });
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export interface LeasedWorkerOptions<T> {
|
|
63
|
+
/** Leases up to `limit` due items under `lease`; `[]` when none is due. */
|
|
64
|
+
claim(lease: LeaseRequest, limit: number): Promise<T[]>;
|
|
65
|
+
/** Extends the lease of `item` to `until`; `false` when it is gone (its token isn't the item's any more). */
|
|
66
|
+
renew(item: T, until: number): Promise<boolean>;
|
|
67
|
+
/** Runs one claimed item. It may find `run.signal` already aborted (claimed as the worker shut down): hand it back. */
|
|
68
|
+
execute(item: T, run: LeasedRun): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Called before claiming, in each poll (at most every `produceInterval`) and each round of `drain()`: work that makes
|
|
71
|
+
* items due (a schedule's occurrences). Resolves to how many it made: `drain()` goes on while it makes some.
|
|
72
|
+
*/
|
|
73
|
+
produce?(): Promise<number>;
|
|
74
|
+
/** Names the worker in errors about its options, as their prefix: `worker.concurrency`. Default `'worker'`. */
|
|
75
|
+
name?: string;
|
|
76
|
+
/** The worker's id, shown as the lease owner. Default `hostname:pid:random`. */
|
|
77
|
+
owner?: string;
|
|
78
|
+
/** Items executed at once by this worker. Default 10. */
|
|
79
|
+
concurrency?: number;
|
|
80
|
+
/** How often to look for due items when not kicked. Default `'1s'`. */
|
|
81
|
+
pollInterval?: Duration;
|
|
82
|
+
/** How long a claim is valid without a renewal. Default `'30s'`. */
|
|
83
|
+
leaseDuration?: Duration;
|
|
84
|
+
/** How often a running item's lease is renewed. Default a third of `leaseDuration`. */
|
|
85
|
+
heartbeatInterval?: Duration;
|
|
86
|
+
/** How long `shutdown()` waits for running items before detaching them. Default `'10s'`. */
|
|
87
|
+
shutdownTimeout?: Duration;
|
|
88
|
+
/** The least time between two `produce()` calls of the polling loop (wall-clock). Default 0: every poll. */
|
|
89
|
+
produceInterval?: Duration;
|
|
90
|
+
/** Where lease times come from. Default `systemClock`. */
|
|
91
|
+
clock?: Clock;
|
|
92
|
+
/** Where the loop reports what failed; it carries on. Default: logged. `drain()` throws instead. */
|
|
93
|
+
onError?(error: unknown, stage: 'claim' | 'produce' | 'execute'): void;
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|