@satorio/machinery 0.2.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 +21 -0
- package/README.md +71 -0
- package/dictionary/all.json +4656 -0
- package/dictionary/all.toon +639 -0
- package/dictionary/core.json +221 -0
- package/dictionary/core.toon +33 -0
- package/dictionary/graphql.json +212 -0
- package/dictionary/graphql.toon +32 -0
- package/dictionary/http.json +113 -0
- package/dictionary/http.toon +21 -0
- package/dictionary/identifiers.json +266 -0
- package/dictionary/identifiers.toon +38 -0
- package/dictionary/index.json +96 -0
- package/dictionary/index.toon +18 -0
- package/dictionary/integration.json +131 -0
- package/dictionary/integration.toon +23 -0
- package/dictionary/money.json +383 -0
- package/dictionary/money.toon +51 -0
- package/dictionary/ports.json +392 -0
- package/dictionary/ports.toon +52 -0
- package/dictionary/postgres.json +473 -0
- package/dictionary/postgres.toon +61 -0
- package/dictionary/publishing.json +131 -0
- package/dictionary/publishing.toon +23 -0
- package/dictionary/queue.json +365 -0
- package/dictionary/queue.toon +49 -0
- package/dictionary/reference/api.mdx +843 -0
- package/dictionary/reference/uk/api.mdx +844 -0
- package/dictionary/storage.json +194 -0
- package/dictionary/storage.toon +30 -0
- package/dictionary/testing.json +707 -0
- package/dictionary/testing.toon +87 -0
- package/dictionary/time.json +311 -0
- package/dictionary/time.toon +43 -0
- package/dictionary/vitest.json +356 -0
- package/dictionary/vitest.toon +48 -0
- package/dictionary/workflow.json +185 -0
- package/dictionary/workflow.toon +29 -0
- package/dist/bin/command.d.ts +45 -0
- package/dist/bin/command.js +113 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +11 -0
- package/dist/core/bigint-json.d.ts +6 -0
- package/dist/core/bigint-json.js +6 -0
- package/dist/core/closed-set.d.ts +14 -0
- package/dist/core/closed-set.js +8 -0
- package/dist/core/domain-error.d.ts +8 -0
- package/dist/core/domain-error.js +11 -0
- package/dist/core/environment.d.ts +124 -0
- package/dist/core/environment.js +115 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +13 -0
- package/dist/core/ulid.d.ts +27 -0
- package/dist/core/ulid.js +27 -0
- package/dist/graphql/builder.d.ts +36 -0
- package/dist/graphql/builder.js +35 -0
- package/dist/graphql/context.d.ts +36 -0
- package/dist/graphql/context.js +69 -0
- package/dist/graphql/failure-classification.d.ts +11 -0
- package/dist/graphql/failure-classification.js +1 -0
- package/dist/graphql/failures.d.ts +62 -0
- package/dist/graphql/failures.js +30 -0
- package/dist/graphql/graphiql.d.ts +45 -0
- package/dist/graphql/graphiql.js +56 -0
- package/dist/graphql/index.d.ts +15 -0
- package/dist/graphql/index.js +14 -0
- package/dist/graphql/layer.d.ts +15 -0
- package/dist/graphql/layer.js +23 -0
- package/dist/graphql/request-handler.d.ts +26 -0
- package/dist/graphql/request-handler.js +33 -0
- package/dist/graphql/server.d.ts +19 -0
- package/dist/graphql/server.js +16 -0
- package/dist/graphql/settings.d.ts +55 -0
- package/dist/graphql/settings.js +82 -0
- package/dist/http/health.d.ts +70 -0
- package/dist/http/health.js +60 -0
- package/dist/http/index.d.ts +5 -0
- package/dist/http/index.js +5 -0
- package/dist/identifiers/client-code.d.ts +37 -0
- package/dist/identifiers/client-code.js +41 -0
- package/dist/identifiers/creditor-reference.d.ts +38 -0
- package/dist/identifiers/creditor-reference.js +32 -0
- package/dist/identifiers/damm.d.ts +22 -0
- package/dist/identifiers/damm.js +50 -0
- package/dist/identifiers/document-number.d.ts +68 -0
- package/dist/identifiers/document-number.js +73 -0
- package/dist/identifiers/document-numbers.d.ts +27 -0
- package/dist/identifiers/document-numbers.js +16 -0
- package/dist/identifiers/failures.d.ts +46 -0
- package/dist/identifiers/failures.js +48 -0
- package/dist/identifiers/index.d.ts +14 -0
- package/dist/identifiers/index.js +13 -0
- package/dist/identifiers/next-document-number.d.ts +26 -0
- package/dist/identifiers/next-document-number.js +31 -0
- package/dist/identifiers/ulid.d.ts +28 -0
- package/dist/identifiers/ulid.js +71 -0
- package/dist/integration/external-http-client.d.ts +22 -0
- package/dist/integration/external-http-client.js +74 -0
- package/dist/integration/index.d.ts +12 -0
- package/dist/integration/index.js +12 -0
- package/dist/integration/layer.d.ts +19 -0
- package/dist/integration/layer.js +30 -0
- package/dist/integration/retry.d.ts +65 -0
- package/dist/integration/retry.js +54 -0
- package/dist/integration/settings.d.ts +73 -0
- package/dist/integration/settings.js +92 -0
- package/dist/money/amount.d.ts +60 -0
- package/dist/money/amount.js +67 -0
- package/dist/money/arithmetic.d.ts +60 -0
- package/dist/money/arithmetic.js +73 -0
- package/dist/money/currency.d.ts +25 -0
- package/dist/money/currency.js +24 -0
- package/dist/money/decimal.d.ts +29 -0
- package/dist/money/decimal.js +39 -0
- package/dist/money/exchange-rate.d.ts +32 -0
- package/dist/money/exchange-rate.js +28 -0
- package/dist/money/failures.d.ts +86 -0
- package/dist/money/failures.js +97 -0
- package/dist/money/index.d.ts +15 -0
- package/dist/money/index.js +15 -0
- package/dist/money/percent.d.ts +35 -0
- package/dist/money/percent.js +50 -0
- package/dist/money/schema.d.ts +25 -0
- package/dist/money/schema.js +41 -0
- package/dist/ports/blob-store.d.ts +42 -0
- package/dist/ports/blob-store.js +13 -0
- package/dist/ports/document-number-counter.d.ts +36 -0
- package/dist/ports/document-number-counter.js +12 -0
- package/dist/ports/event-publisher.d.ts +29 -0
- package/dist/ports/event-publisher.js +13 -0
- package/dist/ports/external-http-client.d.ts +60 -0
- package/dist/ports/external-http-client.js +25 -0
- package/dist/ports/index.d.ts +17 -0
- package/dist/ports/index.js +17 -0
- package/dist/ports/integration-event-outbox.d.ts +44 -0
- package/dist/ports/integration-event-outbox.js +13 -0
- package/dist/ports/port-unavailable.d.ts +21 -0
- package/dist/ports/port-unavailable.js +30 -0
- package/dist/ports/processed-event-registry.d.ts +57 -0
- package/dist/ports/processed-event-registry.js +24 -0
- package/dist/ports/queue-message-producer.d.ts +26 -0
- package/dist/ports/queue-message-producer.js +12 -0
- package/dist/ports/queue-message-source.d.ts +39 -0
- package/dist/ports/queue-message-source.js +13 -0
- package/dist/ports/readiness-probe.d.ts +27 -0
- package/dist/ports/readiness-probe.js +13 -0
- package/dist/ports/transaction-boundary.d.ts +28 -0
- package/dist/ports/transaction-boundary.js +13 -0
- package/dist/postgres/client.d.ts +51 -0
- package/dist/postgres/client.js +81 -0
- package/dist/postgres/document-number-counter.d.ts +14 -0
- package/dist/postgres/document-number-counter.js +27 -0
- package/dist/postgres/failures.d.ts +28 -0
- package/dist/postgres/failures.js +18 -0
- package/dist/postgres/index.d.ts +18 -0
- package/dist/postgres/index.js +18 -0
- package/dist/postgres/integration-event-outbox.d.ts +15 -0
- package/dist/postgres/integration-event-outbox.js +60 -0
- package/dist/postgres/jsonb-value.d.ts +10 -0
- package/dist/postgres/jsonb-value.js +9 -0
- package/dist/postgres/layer.d.ts +57 -0
- package/dist/postgres/layer.js +72 -0
- package/dist/postgres/migrations/advisory-lock.d.ts +47 -0
- package/dist/postgres/migrations/advisory-lock.js +59 -0
- package/dist/postgres/migrations/runner.d.ts +56 -0
- package/dist/postgres/migrations/runner.js +69 -0
- package/dist/postgres/migrations/sql-file-loader.d.ts +59 -0
- package/dist/postgres/migrations/sql-file-loader.js +99 -0
- package/dist/postgres/processed-event-registry.d.ts +13 -0
- package/dist/postgres/processed-event-registry.js +44 -0
- package/dist/postgres/readiness-probe.d.ts +27 -0
- package/dist/postgres/readiness-probe.js +33 -0
- package/dist/postgres/settings.d.ts +134 -0
- package/dist/postgres/settings.js +210 -0
- package/dist/postgres/sql/0001_processed_event.sql +4 -0
- package/dist/postgres/sql/0002_integration_event_outbox.sql +13 -0
- package/dist/postgres/sql/0003_document_number_counter.sql +6 -0
- package/dist/postgres/sql-failure.d.ts +19 -0
- package/dist/postgres/sql-failure.js +22 -0
- package/dist/postgres/store-failure.d.ts +17 -0
- package/dist/postgres/store-failure.js +16 -0
- package/dist/postgres/transaction-boundary.d.ts +13 -0
- package/dist/postgres/transaction-boundary.js +21 -0
- package/dist/publishing/event-publisher.sns.d.ts +66 -0
- package/dist/publishing/event-publisher.sns.js +75 -0
- package/dist/publishing/event-publisher.stdout.d.ts +15 -0
- package/dist/publishing/event-publisher.stdout.js +24 -0
- package/dist/publishing/index.d.ts +10 -0
- package/dist/publishing/index.js +10 -0
- package/dist/publishing/layer.d.ts +25 -0
- package/dist/publishing/layer.js +39 -0
- package/dist/publishing/settings.d.ts +60 -0
- package/dist/publishing/settings.js +85 -0
- package/dist/queue/consumer.d.ts +91 -0
- package/dist/queue/consumer.js +92 -0
- package/dist/queue/envelope.d.ts +39 -0
- package/dist/queue/envelope.js +39 -0
- package/dist/queue/failures.d.ts +33 -0
- package/dist/queue/failures.js +15 -0
- package/dist/queue/index.d.ts +14 -0
- package/dist/queue/index.js +14 -0
- package/dist/queue/layer.d.ts +33 -0
- package/dist/queue/layer.js +58 -0
- package/dist/queue/message-codec.d.ts +43 -0
- package/dist/queue/message-codec.js +33 -0
- package/dist/queue/queue-message-producer.d.ts +61 -0
- package/dist/queue/queue-message-producer.js +52 -0
- package/dist/queue/queue-message-source.d.ts +93 -0
- package/dist/queue/queue-message-source.js +86 -0
- package/dist/queue/readiness.d.ts +78 -0
- package/dist/queue/readiness.js +86 -0
- package/dist/queue/settings.d.ts +106 -0
- package/dist/queue/settings.js +160 -0
- package/dist/storage/blob-store.d.ts +84 -0
- package/dist/storage/blob-store.js +109 -0
- package/dist/storage/index.d.ts +9 -0
- package/dist/storage/index.js +9 -0
- package/dist/storage/layer.d.ts +17 -0
- package/dist/storage/layer.js +30 -0
- package/dist/storage/settings.d.ts +48 -0
- package/dist/storage/settings.js +65 -0
- package/dist/testing/blob-store.d.ts +27 -0
- package/dist/testing/blob-store.js +45 -0
- package/dist/testing/clock.d.ts +29 -0
- package/dist/testing/clock.js +41 -0
- package/dist/testing/dna-perturbator/attention.d.ts +55 -0
- package/dist/testing/dna-perturbator/attention.js +49 -0
- package/dist/testing/dna-perturbator/chains.d.ts +31 -0
- package/dist/testing/dna-perturbator/chains.js +53 -0
- package/dist/testing/dna-perturbator/report.d.ts +12 -0
- package/dist/testing/dna-perturbator/report.js +22 -0
- package/dist/testing/dna-perturbator/verdict.d.ts +26 -0
- package/dist/testing/dna-perturbator/verdict.js +28 -0
- package/dist/testing/dna-perturbator.d.ts +48 -0
- package/dist/testing/dna-perturbator.js +145 -0
- package/dist/testing/document-number-counter.d.ts +29 -0
- package/dist/testing/document-number-counter.js +39 -0
- package/dist/testing/event-publisher.d.ts +28 -0
- package/dist/testing/event-publisher.js +33 -0
- package/dist/testing/external-http-client.d.ts +29 -0
- package/dist/testing/external-http-client.js +49 -0
- package/dist/testing/index.d.ts +24 -0
- package/dist/testing/index.js +24 -0
- package/dist/testing/integration-event-outbox.d.ts +37 -0
- package/dist/testing/integration-event-outbox.js +67 -0
- package/dist/testing/mutation-settings.d.ts +71 -0
- package/dist/testing/mutation-settings.js +57 -0
- package/dist/testing/postgres-rows.d.ts +12 -0
- package/dist/testing/postgres-rows.js +19 -0
- package/dist/testing/processed-event-registry.d.ts +25 -0
- package/dist/testing/processed-event-registry.js +39 -0
- package/dist/testing/queue-message-producer.d.ts +27 -0
- package/dist/testing/queue-message-producer.js +41 -0
- package/dist/testing/queue-message-source.d.ts +54 -0
- package/dist/testing/queue-message-source.js +71 -0
- package/dist/testing/quiet-reporter.d.ts +86 -0
- package/dist/testing/quiet-reporter.js +81 -0
- package/dist/testing/readiness-probe.d.ts +27 -0
- package/dist/testing/readiness-probe.js +33 -0
- package/dist/testing/scenario.d.ts +94 -0
- package/dist/testing/scenario.js +81 -0
- package/dist/testing/transaction-boundary.d.ts +30 -0
- package/dist/testing/transaction-boundary.js +36 -0
- package/dist/testing/typed-failure.d.ts +8 -0
- package/dist/testing/typed-failure.js +15 -0
- package/dist/time/calendar-day.d.ts +83 -0
- package/dist/time/calendar-day.js +112 -0
- package/dist/time/failures.d.ts +51 -0
- package/dist/time/failures.js +56 -0
- package/dist/time/index.d.ts +11 -0
- package/dist/time/index.js +11 -0
- package/dist/time/instant.d.ts +64 -0
- package/dist/time/instant.js +67 -0
- package/dist/time/period.d.ts +45 -0
- package/dist/time/period.js +55 -0
- package/dist/vitest/container-reuse.d.ts +14 -0
- package/dist/vitest/container-reuse.js +15 -0
- package/dist/vitest/dotenv.d.ts +16 -0
- package/dist/vitest/dotenv.js +48 -0
- package/dist/vitest/effect-single-instance.d.ts +36 -0
- package/dist/vitest/effect-single-instance.js +125 -0
- package/dist/vitest/fast-check-single-instance.d.ts +11 -0
- package/dist/vitest/fast-check-single-instance.js +26 -0
- package/dist/vitest/index.d.ts +31 -0
- package/dist/vitest/index.js +30 -0
- package/dist/vitest/localstack-container.d.ts +32 -0
- package/dist/vitest/localstack-container.js +55 -0
- package/dist/vitest/port-contract-tester.d.ts +8 -0
- package/dist/vitest/port-contract-tester.js +1 -0
- package/dist/vitest/port-contracts/blob-store.contract.d.ts +22 -0
- package/dist/vitest/port-contracts/blob-store.contract.js +108 -0
- package/dist/vitest/port-contracts/document-number-counter.contract.d.ts +18 -0
- package/dist/vitest/port-contracts/document-number-counter.contract.js +90 -0
- package/dist/vitest/port-contracts/event-publisher.contract.d.ts +35 -0
- package/dist/vitest/port-contracts/event-publisher.contract.js +67 -0
- package/dist/vitest/port-contracts/external-http-client.contract.d.ts +35 -0
- package/dist/vitest/port-contracts/external-http-client.contract.js +52 -0
- package/dist/vitest/port-contracts/integration-event-outbox.contract.d.ts +23 -0
- package/dist/vitest/port-contracts/integration-event-outbox.contract.js +75 -0
- package/dist/vitest/port-contracts/processed-event-registry.contract.d.ts +18 -0
- package/dist/vitest/port-contracts/processed-event-registry.contract.js +84 -0
- package/dist/vitest/port-contracts/queue-message-producer.contract.d.ts +26 -0
- package/dist/vitest/port-contracts/queue-message-producer.contract.js +48 -0
- package/dist/vitest/port-contracts/queue-message-source.contract.d.ts +29 -0
- package/dist/vitest/port-contracts/queue-message-source.contract.js +71 -0
- package/dist/vitest/port-contracts/readiness-probe.contract.d.ts +18 -0
- package/dist/vitest/port-contracts/readiness-probe.contract.js +41 -0
- package/dist/vitest/port-contracts/transaction-boundary.contract.d.ts +21 -0
- package/dist/vitest/port-contracts/transaction-boundary.contract.js +64 -0
- package/dist/vitest/postgres-container.d.ts +57 -0
- package/dist/vitest/postgres-container.js +118 -0
- package/dist/vitest/reader-port-contract.d.ts +41 -0
- package/dist/vitest/reader-port-contract.js +43 -0
- package/dist/vitest/repo-source-files.d.ts +37 -0
- package/dist/vitest/repo-source-files.js +61 -0
- package/dist/vitest/reset-tables.d.ts +18 -0
- package/dist/vitest/reset-tables.js +30 -0
- package/dist/vitest/settings-contract.d.ts +38 -0
- package/dist/vitest/settings-contract.js +77 -0
- package/dist/vitest/stub-server.d.ts +43 -0
- package/dist/vitest/stub-server.js +60 -0
- package/dist/vitest/term-registry.json +730 -0
- package/dist/vitest/testclock-fork-isolated-files.d.ts +13 -0
- package/dist/vitest/testclock-fork-isolated-files.js +18 -0
- package/dist/vitest/testclock-fork-isolation.d.ts +77 -0
- package/dist/vitest/testclock-fork-isolation.js +188 -0
- package/dist/workflow/claim-once.d.ts +16 -0
- package/dist/workflow/claim-once.js +9 -0
- package/dist/workflow/failures.d.ts +47 -0
- package/dist/workflow/failures.js +22 -0
- package/dist/workflow/index.d.ts +12 -0
- package/dist/workflow/index.js +12 -0
- package/dist/workflow/on-claim.d.ts +17 -0
- package/dist/workflow/on-claim.js +8 -0
- package/dist/workflow/publish-pending-events.d.ts +31 -0
- package/dist/workflow/publish-pending-events.js +31 -0
- package/dist/workflow/transactionally.d.ts +8 -0
- package/dist/workflow/transactionally.js +8 -0
- package/dist/workflow/versioned-payload.d.ts +35 -0
- package/dist/workflow/versioned-payload.js +43 -0
- package/package.json +309 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The test files that need their own fresh module state, because they fork a
|
|
3
|
+
* fiber and move the clock.
|
|
4
|
+
*
|
|
5
|
+
* Files that fork a fiber and adjust TestClock (`Effect.fork` +
|
|
6
|
+
* `TestClock.adjust` + `Fiber.join`) and so need their own fresh module state
|
|
7
|
+
* (`isolate:true`) instead of the suite's shared worker. Guarded by
|
|
8
|
+
* `testclockForkIsolationGuard` (see `testclock-fork-isolation.ts`). Paths are
|
|
9
|
+
* relative to the package's own repository root. This file carries no `vitest`
|
|
10
|
+
* import on purpose: `vitest.config.ts` reads this list while building the
|
|
11
|
+
* test-runner config, before the `vitest` runtime exists.
|
|
12
|
+
*/
|
|
13
|
+
export declare const FILES_THAT_FORK_A_FIBER_AND_ADJUST_TEST_CLOCK: readonly string[];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The test files that need their own fresh module state, because they fork a
|
|
3
|
+
* fiber and move the clock.
|
|
4
|
+
*
|
|
5
|
+
* Files that fork a fiber and adjust TestClock (`Effect.fork` +
|
|
6
|
+
* `TestClock.adjust` + `Fiber.join`) and so need their own fresh module state
|
|
7
|
+
* (`isolate:true`) instead of the suite's shared worker. Guarded by
|
|
8
|
+
* `testclockForkIsolationGuard` (see `testclock-fork-isolation.ts`). Paths are
|
|
9
|
+
* relative to the package's own repository root. This file carries no `vitest`
|
|
10
|
+
* import on purpose: `vitest.config.ts` reads this list while building the
|
|
11
|
+
* test-runner config, before the `vitest` runtime exists.
|
|
12
|
+
*/
|
|
13
|
+
export const FILES_THAT_FORK_A_FIBER_AND_ADJUST_TEST_CLOCK = [
|
|
14
|
+
'src/postgres/readiness-probe.test.ts',
|
|
15
|
+
'src/integration/external-http-client.test.ts',
|
|
16
|
+
'src/queue/readiness-deadline.test.ts',
|
|
17
|
+
'src/workflow/publish-pending-events.test.ts',
|
|
18
|
+
];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Held inside this mechanism. The guard's own violations test can prove the
|
|
3
|
+
* pattern `testclockForkIsolationGuard` scans
|
|
4
|
+
* for directly, instead of only ever observing a clean tree.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export declare const forksAFiberAndAdjustsTestClock: (source: string) => boolean;
|
|
9
|
+
/**
|
|
10
|
+
* A module opens a real client when it imports one of the AWS SDK's
|
|
11
|
+
* clients. Held inside this mechanism so the guard's own violations test
|
|
12
|
+
* can feed a constructed module instead of only ever reading a clean tree.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export declare const opensARealClient: (source: string) => boolean;
|
|
17
|
+
/**
|
|
18
|
+
* One name a test takes from a module that opens a real client, and the
|
|
19
|
+
* module it takes it from.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export interface TestclockImportFinding {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly specifier: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Every name a source takes from a module that opens a real client, minus
|
|
29
|
+
* the ones the caller excused. `opensAClientAt` answers for one import
|
|
30
|
+
* specifier, so the guard's own violations test can answer from a
|
|
31
|
+
* constructed map instead of the file system.
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
export declare const namesTakenFromAModuleThatOpensAClient: (source: string, opensAClientAt: (specifier: string) => boolean, excusedNames: ReadonlySet<string>) => readonly TestclockImportFinding[];
|
|
36
|
+
/**
|
|
37
|
+
* One name a clock-driving test may keep importing from a module that
|
|
38
|
+
* opens a real client, and why that name opens nothing itself.
|
|
39
|
+
*
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export interface TestclockImportException {
|
|
43
|
+
readonly name: string;
|
|
44
|
+
readonly why: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Options for `testclockForkIsolationGuard`. `directories` default to
|
|
48
|
+
* `['src']`, the package's own source root; a consumer names its own
|
|
49
|
+
* source directories. `exceptions` name the imports a clock-driving test
|
|
50
|
+
* may keep taking from a module that opens a real client — a type, a
|
|
51
|
+
* constant, an entry the caller hands its own stand-in to — each with the
|
|
52
|
+
* reason.
|
|
53
|
+
*
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
export interface TestclockForkIsolationOptions {
|
|
57
|
+
readonly root?: string;
|
|
58
|
+
readonly directories?: readonly string[];
|
|
59
|
+
readonly minimumFiles: number;
|
|
60
|
+
readonly exceptions?: readonly TestclockImportException[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Registers a vitest suite that fails when a test moves the clock in a forked
|
|
64
|
+
* fiber outside its own list.
|
|
65
|
+
*
|
|
66
|
+
* Registers a vitest suite that fails if a `*.test.ts` file under the scanned
|
|
67
|
+
* directories forks a fiber and adjusts `TestClock` without being listed in
|
|
68
|
+
* `knownIsolatedFiles` — such a file needs `isolate:true` (see
|
|
69
|
+
* `vitest.config.ts`'s `testclock-fork-isolated` project), or it occasionally
|
|
70
|
+
* hits "testClock.adjust is not a function" under a shared worker. The same
|
|
71
|
+
* suite fails when one of those files imports a real client, straight from the
|
|
72
|
+
* AWS SDK or through a module that opens one: a real client can fail on its
|
|
73
|
+
* own, faster than the virtual clock moves, and then the test reports that
|
|
74
|
+
* failure instead of the deadline it was written for. Call it from a
|
|
75
|
+
* consumer's `*.test.ts` file with the consumer's own list.
|
|
76
|
+
*/
|
|
77
|
+
export declare const testclockForkIsolationGuard: (knownIsolatedFiles: readonly string[], options: TestclockForkIsolationOptions) => void;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { repoFilesUnder, repoRoot } from './repo-source-files.js';
|
|
5
|
+
const AWS_CLIENT_PACKAGE = '@aws-sdk/client-';
|
|
6
|
+
const IMPORT_CLAUSE = /import\s+([^'"]*?)\s*from\s*['"]([^'"]+)['"]/g;
|
|
7
|
+
/**
|
|
8
|
+
* Held inside this mechanism. The guard's own violations test can prove the
|
|
9
|
+
* pattern `testclockForkIsolationGuard` scans
|
|
10
|
+
* for directly, instead of only ever observing a clean tree.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export const forksAFiberAndAdjustsTestClock = (source) => source.includes('TestClock.adjust') &&
|
|
15
|
+
(source.includes('Effect.fork') || source.includes('Fiber.join'));
|
|
16
|
+
/**
|
|
17
|
+
* A module opens a real client when it imports one of the AWS SDK's
|
|
18
|
+
* clients. Held inside this mechanism so the guard's own violations test
|
|
19
|
+
* can feed a constructed module instead of only ever reading a clean tree.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export const opensARealClient = (source) => source.includes(AWS_CLIENT_PACKAGE);
|
|
24
|
+
const namesInClause = (clause) => {
|
|
25
|
+
const withoutTypeKeyword = clause.replace(/^type\s+/, '');
|
|
26
|
+
const opening = withoutTypeKeyword.indexOf('{');
|
|
27
|
+
const outside = opening === -1 ? withoutTypeKeyword : withoutTypeKeyword.slice(0, opening);
|
|
28
|
+
const inside = opening === -1
|
|
29
|
+
? ''
|
|
30
|
+
: withoutTypeKeyword.slice(opening + 1).replace(/\}[\s\S]*$/, '');
|
|
31
|
+
return [...outside.split(','), ...inside.split(',')]
|
|
32
|
+
.map((part) => part
|
|
33
|
+
.trim()
|
|
34
|
+
.replace(/^type\s+/, '')
|
|
35
|
+
.split(/\s+as\s+/)[0])
|
|
36
|
+
.map((name) => (name === undefined ? '' : name.trim()))
|
|
37
|
+
.filter((name) => name !== '' && name !== '*');
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Every name a source takes from a module that opens a real client, minus
|
|
41
|
+
* the ones the caller excused. `opensAClientAt` answers for one import
|
|
42
|
+
* specifier, so the guard's own violations test can answer from a
|
|
43
|
+
* constructed map instead of the file system.
|
|
44
|
+
*
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
export const namesTakenFromAModuleThatOpensAClient = (source, opensAClientAt, excusedNames) => [...source.matchAll(IMPORT_CLAUSE)].flatMap((match) => {
|
|
48
|
+
const clause = match[1] ?? '';
|
|
49
|
+
const specifier = match[2] ?? '';
|
|
50
|
+
if (!opensAClientAt(specifier))
|
|
51
|
+
return [];
|
|
52
|
+
return namesInClause(clause)
|
|
53
|
+
.filter((name) => !excusedNames.has(name))
|
|
54
|
+
.map((name) => ({ name, specifier }));
|
|
55
|
+
});
|
|
56
|
+
const readOrNothing = (path) => {
|
|
57
|
+
try {
|
|
58
|
+
return readFileSync(path, 'utf8');
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const codeOf = (error) => typeof error === 'object' &&
|
|
65
|
+
error !== null &&
|
|
66
|
+
typeof error.code === 'string'
|
|
67
|
+
? error.code
|
|
68
|
+
: 'an error with no code';
|
|
69
|
+
const readSource = (path) => {
|
|
70
|
+
try {
|
|
71
|
+
return { kind: 'ok', text: readFileSync(path, 'utf8') };
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return {
|
|
75
|
+
kind: 'unreadable',
|
|
76
|
+
reason: `the read failed with ${codeOf(error)}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const opensAClientAtFrom = (root, file) => (specifier) => {
|
|
81
|
+
if (specifier.startsWith(AWS_CLIENT_PACKAGE))
|
|
82
|
+
return true;
|
|
83
|
+
if (!specifier.startsWith('.'))
|
|
84
|
+
return false;
|
|
85
|
+
const base = join(root, dirname(file), specifier);
|
|
86
|
+
const source = readOrNothing(`${base}.ts`) ?? readOrNothing(join(base, 'index.ts'));
|
|
87
|
+
return source !== undefined && opensARealClient(source);
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Registers a vitest suite that fails when a test moves the clock in a forked
|
|
91
|
+
* fiber outside its own list.
|
|
92
|
+
*
|
|
93
|
+
* Registers a vitest suite that fails if a `*.test.ts` file under the scanned
|
|
94
|
+
* directories forks a fiber and adjusts `TestClock` without being listed in
|
|
95
|
+
* `knownIsolatedFiles` — such a file needs `isolate:true` (see
|
|
96
|
+
* `vitest.config.ts`'s `testclock-fork-isolated` project), or it occasionally
|
|
97
|
+
* hits "testClock.adjust is not a function" under a shared worker. The same
|
|
98
|
+
* suite fails when one of those files imports a real client, straight from the
|
|
99
|
+
* AWS SDK or through a module that opens one: a real client can fail on its
|
|
100
|
+
* own, faster than the virtual clock moves, and then the test reports that
|
|
101
|
+
* failure instead of the deadline it was written for. Call it from a
|
|
102
|
+
* consumer's `*.test.ts` file with the consumer's own list.
|
|
103
|
+
*/
|
|
104
|
+
export const testclockForkIsolationGuard = (knownIsolatedFiles, options) => {
|
|
105
|
+
const root = options.root ?? repoRoot();
|
|
106
|
+
const directories = options.directories ?? ['src'];
|
|
107
|
+
const known = new Set(knownIsolatedFiles);
|
|
108
|
+
const exceptions = options.exceptions ?? [];
|
|
109
|
+
const excusedNames = new Set(exceptions.map((exception) => exception.name));
|
|
110
|
+
const testFiles = repoFilesUnder(root, directories, '.ts').filter((file) => file.endsWith('.test.ts'));
|
|
111
|
+
const testFileSet = new Set(testFiles);
|
|
112
|
+
const reads = testFiles.map((file) => ({
|
|
113
|
+
file,
|
|
114
|
+
read: readSource(join(root, file)),
|
|
115
|
+
}));
|
|
116
|
+
const unreadableFiles = reads.flatMap((entry) => entry.read.kind === 'unreadable'
|
|
117
|
+
? [{ file: entry.file, reason: entry.read.reason }]
|
|
118
|
+
: []);
|
|
119
|
+
const sourcesMatchingThePattern = reads
|
|
120
|
+
.flatMap((entry) => entry.read.kind === 'ok'
|
|
121
|
+
? [{ file: entry.file, text: entry.read.text }]
|
|
122
|
+
: [])
|
|
123
|
+
.filter((entry) => forksAFiberAndAdjustsTestClock(entry.text));
|
|
124
|
+
const filesMatchingThePattern = sourcesMatchingThePattern.map((entry) => entry.file);
|
|
125
|
+
const matchingSet = new Set(filesMatchingThePattern);
|
|
126
|
+
const importsOfARealClient = sourcesMatchingThePattern.flatMap((entry) => namesTakenFromAModuleThatOpensAClient(entry.text, opensAClientAtFrom(root, entry.file), excusedNames).map((finding) => ({ file: entry.file, ...finding })));
|
|
127
|
+
const namesEveryExceptionCouldExcuse = new Set(sourcesMatchingThePattern.flatMap((entry) => namesTakenFromAModuleThatOpensAClient(entry.text, opensAClientAtFrom(root, entry.file), new Set()).map((finding) => finding.name)));
|
|
128
|
+
describe('testclock-fork-isolated list: a guard against a new flake', () => {
|
|
129
|
+
it('every test file with TestClock.adjust + fork/join is listed', () => {
|
|
130
|
+
const missingFromTheList = filesMatchingThePattern.filter((file) => !known.has(file));
|
|
131
|
+
expect(missingFromTheList, missingFromTheList
|
|
132
|
+
.map((file) => `${file} forks a fiber and adjusts TestClock, but is not in ` +
|
|
133
|
+
`the known-isolated list passed to testclockForkIsolationGuard. ` +
|
|
134
|
+
`Add a line there — otherwise this file, under the shared ` +
|
|
135
|
+
`worker, occasionally hits "testClock.adjust is not a ` +
|
|
136
|
+
`function".`)
|
|
137
|
+
.join('\n')).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
it('no listed file is stale', () => {
|
|
140
|
+
const staleEntries = knownIsolatedFiles.filter((file) => !testFileSet.has(file) || !matchingSet.has(file));
|
|
141
|
+
expect(staleEntries, staleEntries
|
|
142
|
+
.map((file) => testFileSet.has(file)
|
|
143
|
+
? `${file} is listed in testclockForkIsolationGuard's ` +
|
|
144
|
+
`known-isolated list, but no longer forks a fiber and ` +
|
|
145
|
+
'adjusts TestClock — STALE, prune the baseline.'
|
|
146
|
+
: `${file} is listed in testclockForkIsolationGuard's ` +
|
|
147
|
+
`known-isolated list, but no test file exists there ` +
|
|
148
|
+
'any more — STALE, prune the baseline.')
|
|
149
|
+
.join('\n')).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
it('no file that adjusts the clock imports a real client', () => {
|
|
152
|
+
expect(importsOfARealClient, importsOfARealClient
|
|
153
|
+
.map((finding) => `${finding.file} imports ${finding.name} from ` +
|
|
154
|
+
`${finding.specifier}, which opens a real client. A real ` +
|
|
155
|
+
`client can fail on its own — a missing region, a refused ` +
|
|
156
|
+
`connection — faster than the virtual clock moves, and then ` +
|
|
157
|
+
`this test reports that failure instead of the deadline it ` +
|
|
158
|
+
`was written for. Hand the code under test a stand-in, or ` +
|
|
159
|
+
`name ${finding.name} in the guard's exceptions with the ` +
|
|
160
|
+
`reason it opens nothing.`)
|
|
161
|
+
.join('\n')).toEqual([]);
|
|
162
|
+
});
|
|
163
|
+
it('every exception carries a reason and still matches an import', () => {
|
|
164
|
+
const badExceptions = exceptions.filter((exception) => exception.why.trim() === '' ||
|
|
165
|
+
!namesEveryExceptionCouldExcuse.has(exception.name));
|
|
166
|
+
expect(badExceptions, badExceptions
|
|
167
|
+
.map((exception) => exception.why.trim() === ''
|
|
168
|
+
? `${exception.name} is excused with no reason. Say why the ` +
|
|
169
|
+
'name opens nothing, or drop the exception.'
|
|
170
|
+
: `${exception.name} is excused, but no file that adjusts ` +
|
|
171
|
+
'the clock imports it from a module that opens a real ' +
|
|
172
|
+
'client any more — STALE, prune the exception.')
|
|
173
|
+
.join('\n')).toEqual([]);
|
|
174
|
+
});
|
|
175
|
+
it('every file the walk listed could be read', () => {
|
|
176
|
+
expect(unreadableFiles.map((entry) => entry.file), unreadableFiles
|
|
177
|
+
.map((entry) => `${entry.file} is on the walk's list, and this guard cannot ` +
|
|
178
|
+
`read it: ${entry.reason}. A file the guard passes over in ` +
|
|
179
|
+
'silence is a file nothing holds — delete the path, or repair ' +
|
|
180
|
+
'it.')
|
|
181
|
+
.join('\n')).toEqual([]);
|
|
182
|
+
});
|
|
183
|
+
it('read at least minimumFiles test files', () => {
|
|
184
|
+
console.log(`testclock fork isolation: read ${testFiles.length} test files`);
|
|
185
|
+
expect(testFiles.length).toBeGreaterThanOrEqual(options.minimumFiles);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ProcessedEventClaim, ProcessedEventRegistry, type ProcessedEventRegistryUnavailableError } from '@satorio/machinery/ports';
|
|
2
|
+
import { DateTime, Effect } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* The exactly-once claim: shared ground for every use case that takes an
|
|
5
|
+
* event or command once. The use case itself builds the idempotency key
|
|
6
|
+
* (an event id, a command id, "payment:<id>") — only it knows what counts
|
|
7
|
+
* as "the same" for its own case. This type carries the claim, nothing more.
|
|
8
|
+
*/
|
|
9
|
+
export type ClaimOnce = Effect.Effect<ProcessedEventClaim, ProcessedEventRegistryUnavailableError, ProcessedEventRegistry>;
|
|
10
|
+
/**
|
|
11
|
+
* Claims with a processedAt supplied by the caller, for a handler that has
|
|
12
|
+
* already read the clock and must store that exact moment.
|
|
13
|
+
*/
|
|
14
|
+
export declare const claimAsProcessedAt: (idempotencyKey: string, processedAt: DateTime.Utc) => ClaimOnce;
|
|
15
|
+
/** Claims with the current time, for a caller that only needs the claim. */
|
|
16
|
+
export declare const claimAsProcessedNow: (idempotencyKey: string) => ClaimOnce;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ProcessedEventRegistry, } from '@satorio/machinery/ports';
|
|
2
|
+
import { DateTime, Effect } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* Claims with a processedAt supplied by the caller, for a handler that has
|
|
5
|
+
* already read the clock and must store that exact moment.
|
|
6
|
+
*/
|
|
7
|
+
export const claimAsProcessedAt = (idempotencyKey, processedAt) => Effect.flatMap(ProcessedEventRegistry, (registry) => registry.claim({ eventId: idempotencyKey, processedAt }));
|
|
8
|
+
/** Claims with the current time, for a caller that only needs the claim. */
|
|
9
|
+
export const claimAsProcessedNow = (idempotencyKey) => Effect.flatMap(DateTime.now, (processedAt) => claimAsProcessedAt(idempotencyKey, processedAt));
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
declare const SchemaVersionFromTheFutureError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
2
|
+
readonly _tag: "SchemaVersionFromTheFutureError";
|
|
3
|
+
} & Readonly<A>;
|
|
4
|
+
/**
|
|
5
|
+
* The stored payload says it was written by a newer version of this code
|
|
6
|
+
* than the one reading it. There is no upcast step downwards, so the
|
|
7
|
+
* reader stops rather than guessing which fields it does not know about.
|
|
8
|
+
*/
|
|
9
|
+
export declare class SchemaVersionFromTheFutureError extends SchemaVersionFromTheFutureError_base<{
|
|
10
|
+
readonly eventType: string;
|
|
11
|
+
readonly storedVersion: number;
|
|
12
|
+
readonly currentVersion: number;
|
|
13
|
+
}> {
|
|
14
|
+
}
|
|
15
|
+
declare const MissingUpcastStepError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
16
|
+
readonly _tag: "MissingUpcastStepError";
|
|
17
|
+
} & Readonly<A>;
|
|
18
|
+
/**
|
|
19
|
+
* One version between the stored payload and the current one has no
|
|
20
|
+
* upcast step, so the lift cannot finish. The payload stays where it is
|
|
21
|
+
* rather than arriving half-lifted.
|
|
22
|
+
*/
|
|
23
|
+
export declare class MissingUpcastStepError extends MissingUpcastStepError_base<{
|
|
24
|
+
readonly eventType: string;
|
|
25
|
+
readonly fromVersion: number;
|
|
26
|
+
readonly currentVersion: number;
|
|
27
|
+
}> {
|
|
28
|
+
}
|
|
29
|
+
declare const UpcastStepFailedError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
30
|
+
readonly _tag: "UpcastStepFailedError";
|
|
31
|
+
} & Readonly<A>;
|
|
32
|
+
/**
|
|
33
|
+
* An upcast step threw while lifting the payload one version up. The
|
|
34
|
+
* throw becomes a typed failure here, so a caller reads it in the error
|
|
35
|
+
* channel rather than meeting an exception.
|
|
36
|
+
*/
|
|
37
|
+
export declare class UpcastStepFailedError extends UpcastStepFailedError_base<{
|
|
38
|
+
readonly eventType: string;
|
|
39
|
+
readonly fromVersion: number;
|
|
40
|
+
readonly cause: unknown;
|
|
41
|
+
}> {
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Every way reading a stored payload can refuse.
|
|
45
|
+
*/
|
|
46
|
+
export type VersionedPayloadFailure = SchemaVersionFromTheFutureError | MissingUpcastStepError | UpcastStepFailedError;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Data } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* The stored payload says it was written by a newer version of this code
|
|
4
|
+
* than the one reading it. There is no upcast step downwards, so the
|
|
5
|
+
* reader stops rather than guessing which fields it does not know about.
|
|
6
|
+
*/
|
|
7
|
+
export class SchemaVersionFromTheFutureError extends Data.TaggedError('SchemaVersionFromTheFutureError') {
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* One version between the stored payload and the current one has no
|
|
11
|
+
* upcast step, so the lift cannot finish. The payload stays where it is
|
|
12
|
+
* rather than arriving half-lifted.
|
|
13
|
+
*/
|
|
14
|
+
export class MissingUpcastStepError extends Data.TaggedError('MissingUpcastStepError') {
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* An upcast step threw while lifting the payload one version up. The
|
|
18
|
+
* throw becomes a typed failure here, so a caller reads it in the error
|
|
19
|
+
* channel rather than meeting an exception.
|
|
20
|
+
*/
|
|
21
|
+
export class UpcastStepFailedError extends Data.TaggedError('UpcastStepFailedError') {
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry to the workflow mechanism: the combinators a use case composes
|
|
3
|
+
* its own beat from. It gives you the combinators a use case composes its own
|
|
4
|
+
* beat from — a transaction boundary, the exactly-once claim, versioned
|
|
5
|
+
* payloads, and the outbox relay.
|
|
6
|
+
*/
|
|
7
|
+
export { type ClaimOnce, claimAsProcessedAt, claimAsProcessedNow, } from './claim-once.js';
|
|
8
|
+
export { MissingUpcastStepError, SchemaVersionFromTheFutureError, UpcastStepFailedError, type VersionedPayloadFailure, } from './failures.js';
|
|
9
|
+
export { type OnClaimMatchers, onClaim, } from './on-claim.js';
|
|
10
|
+
export { DEFAULT_PUBLISH_PENDING_EVENTS_SETTINGS, keepPublishingPendingEvents, type PublishPendingEventsFailure, type PublishPendingEventsSettings, publishOneBatch, } from './publish-pending-events.js';
|
|
11
|
+
export { transactionally } from './transactionally.js';
|
|
12
|
+
export { decodeStoredPayload, type UpcastStep, upcastToCurrentVersion, type VersionedEventDefinition, } from './versioned-payload.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry to the workflow mechanism: the combinators a use case composes
|
|
3
|
+
* its own beat from. It gives you the combinators a use case composes its own
|
|
4
|
+
* beat from — a transaction boundary, the exactly-once claim, versioned
|
|
5
|
+
* payloads, and the outbox relay.
|
|
6
|
+
*/
|
|
7
|
+
export { claimAsProcessedAt, claimAsProcessedNow, } from './claim-once.js';
|
|
8
|
+
export { MissingUpcastStepError, SchemaVersionFromTheFutureError, UpcastStepFailedError, } from './failures.js';
|
|
9
|
+
export { onClaim, } from './on-claim.js';
|
|
10
|
+
export { DEFAULT_PUBLISH_PENDING_EVENTS_SETTINGS, keepPublishingPendingEvents, publishOneBatch, } from './publish-pending-events.js';
|
|
11
|
+
export { transactionally } from './transactionally.js';
|
|
12
|
+
export { decodeStoredPayload, upcastToCurrentVersion, } from './versioned-payload.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ProcessedEventClaim } from '@satorio/machinery/ports';
|
|
2
|
+
import { type DateTime, Effect } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* The two branches `onClaim` takes: what to do when the event was already
|
|
5
|
+
* claimed, and what to do when this run claims it.
|
|
6
|
+
*/
|
|
7
|
+
export interface OnClaimMatchers<A1, E1, R1, A2, E2, R2> {
|
|
8
|
+
readonly alreadyProcessed: (processedAt: DateTime.Utc) => Effect.Effect<A1, E1, R1>;
|
|
9
|
+
readonly accepted: () => Effect.Effect<A2, E2, R2>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Branches on the state of the exactly-once claim: AlreadyProcessed goes one
|
|
13
|
+
* way, ClaimAccepted the other. The claim is shared by every use case that
|
|
14
|
+
* takes an event once, so this combinator lives in the machinery, not in a
|
|
15
|
+
* service.
|
|
16
|
+
*/
|
|
17
|
+
export declare const onClaim: <A1, E1, R1, A2, E2, R2, EClaim, RClaim>(claim: Effect.Effect<ProcessedEventClaim, EClaim, RClaim>, matchers: OnClaimMatchers<A1, E1, R1, A2, E2, R2>) => Effect.Effect<A1 | A2, EClaim | E1 | E2, RClaim | R1 | R2>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Effect, Match } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Branches on the state of the exactly-once claim: AlreadyProcessed goes one
|
|
4
|
+
* way, ClaimAccepted the other. The claim is shared by every use case that
|
|
5
|
+
* takes an event once, so this combinator lives in the machinery, not in a
|
|
6
|
+
* service.
|
|
7
|
+
*/
|
|
8
|
+
export const onClaim = (claim, matchers) => Effect.flatMap(claim, Match.type().pipe(Match.tag('AlreadyProcessed', ({ processedAt }) => matchers.alreadyProcessed(processedAt)), Match.tag('ClaimAccepted', () => matchers.accepted()), Match.exhaustive));
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { EventPublisher, type EventPublisherUnavailableError, IntegrationEventOutbox, type IntegrationEventOutboxUnavailableError } from '@satorio/machinery/ports';
|
|
2
|
+
import { Duration, Effect } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* What one beat of the relay can meet: the outbox did not answer, or the
|
|
5
|
+
* publisher did not.
|
|
6
|
+
*/
|
|
7
|
+
export type PublishPendingEventsFailure = IntegrationEventOutboxUnavailableError | EventPublisherUnavailableError;
|
|
8
|
+
/**
|
|
9
|
+
* How the relay paces itself: how many messages one beat takes, and how long
|
|
10
|
+
* it waits between beats.
|
|
11
|
+
*/
|
|
12
|
+
export interface PublishPendingEventsSettings {
|
|
13
|
+
readonly batchLimit: number;
|
|
14
|
+
readonly pollInterval: Duration.Duration;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The pace the relay keeps when a service names none of its own.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DEFAULT_PUBLISH_PENDING_EVENTS_SETTINGS: PublishPendingEventsSettings;
|
|
20
|
+
/**
|
|
21
|
+
* One outbox relay beat: take a batch of undelivered messages, hand it to
|
|
22
|
+
* the publisher, then mark it dispatched. The order matters — marking comes
|
|
23
|
+
* after a successful hand-off, so a failure leaves the message in the
|
|
24
|
+
* outbox instead of losing it.
|
|
25
|
+
*/
|
|
26
|
+
export declare const publishOneBatch: (batchLimit: number) => Effect.Effect<number, PublishPendingEventsFailure, IntegrationEventOutbox | EventPublisher>;
|
|
27
|
+
/**
|
|
28
|
+
* Runs the outbox relay for as long as the service lives. A beat that fails is
|
|
29
|
+
* logged, and its messages stay in the outbox for the next one.
|
|
30
|
+
*/
|
|
31
|
+
export declare const keepPublishingPendingEvents: (settings?: PublishPendingEventsSettings) => Effect.Effect<never, never, IntegrationEventOutbox | EventPublisher>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { EventPublisher, IntegrationEventOutbox, } from '@satorio/machinery/ports';
|
|
2
|
+
import { Duration, Effect, Schedule } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* The pace the relay keeps when a service names none of its own.
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_PUBLISH_PENDING_EVENTS_SETTINGS = {
|
|
7
|
+
batchLimit: 50,
|
|
8
|
+
pollInterval: Duration.seconds(5),
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* One outbox relay beat: take a batch of undelivered messages, hand it to
|
|
12
|
+
* the publisher, then mark it dispatched. The order matters — marking comes
|
|
13
|
+
* after a successful hand-off, so a failure leaves the message in the
|
|
14
|
+
* outbox instead of losing it.
|
|
15
|
+
*/
|
|
16
|
+
export const publishOneBatch = (batchLimit) => Effect.gen(function* () {
|
|
17
|
+
const outbox = yield* IntegrationEventOutbox;
|
|
18
|
+
const publisher = yield* EventPublisher;
|
|
19
|
+
const batch = yield* outbox.pending(batchLimit);
|
|
20
|
+
if (batch.length === 0)
|
|
21
|
+
return 0;
|
|
22
|
+
yield* publisher.publish(batch);
|
|
23
|
+
yield* outbox.markDispatched(batch.map((message) => message.messageId));
|
|
24
|
+
return batch.length;
|
|
25
|
+
});
|
|
26
|
+
const oneBatchSwallowingFailuresLoudly = (batchLimit) => publishOneBatch(batchLimit).pipe(Effect.asVoid, Effect.catchAll((failure) => Effect.logError('Event publishing: this beat failed, messages stay in the outbox until the next beat', { failure: failure._tag, operation: failure.operation })));
|
|
27
|
+
/**
|
|
28
|
+
* Runs the outbox relay for as long as the service lives. A beat that fails is
|
|
29
|
+
* logged, and its messages stay in the outbox for the next one.
|
|
30
|
+
*/
|
|
31
|
+
export const keepPublishingPendingEvents = (settings = DEFAULT_PUBLISH_PENDING_EVENTS_SETTINGS) => oneBatchSwallowingFailuresLoudly(settings.batchLimit).pipe(Effect.repeat(Schedule.spaced(settings.pollInterval)), Effect.andThen(Effect.never));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TransactionBoundary } from '@satorio/machinery/ports';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* Wraps work in a transaction boundary: pulls TransactionBoundary from
|
|
5
|
+
* context and runs the work through it. A reusable combinator for use cases
|
|
6
|
+
* that must write within one transaction.
|
|
7
|
+
*/
|
|
8
|
+
export declare const transactionally: <A, E, R>(work: Effect.Effect<A, E, R>) => Effect.Effect<A, import("@satorio/machinery/ports").TransactionBoundaryUnavailableError | E, TransactionBoundary | R>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TransactionBoundary } from '@satorio/machinery/ports';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* Wraps work in a transaction boundary: pulls TransactionBoundary from
|
|
5
|
+
* context and runs the work through it. A reusable combinator for use cases
|
|
6
|
+
* that must write within one transaction.
|
|
7
|
+
*/
|
|
8
|
+
export const transactionally = (work) => Effect.flatMap(TransactionBoundary, (tx) => tx.inTransaction(work));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Effect, type ParseResult, Schema } from 'effect';
|
|
2
|
+
import { type VersionedPayloadFailure } from './failures.js';
|
|
3
|
+
/**
|
|
4
|
+
* One lift, from the version it names to the next one up.
|
|
5
|
+
*/
|
|
6
|
+
export interface UpcastStep {
|
|
7
|
+
readonly fromVersion: number;
|
|
8
|
+
readonly lift: (payload: unknown) => unknown;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* An event stored yesterday is read by today's code. Between the two sit
|
|
12
|
+
* upcast steps: each lifts the payload one version up, and only after
|
|
13
|
+
* they run does the current schema's decoder take over. A definition
|
|
14
|
+
* carries the event's name, the version the code writes today, the
|
|
15
|
+
* schema that version decodes with, and the steps between.
|
|
16
|
+
*/
|
|
17
|
+
export interface VersionedEventDefinition<S extends Schema.Schema.AnyNoContext> {
|
|
18
|
+
readonly eventType: string;
|
|
19
|
+
readonly currentSchemaVersion: number;
|
|
20
|
+
readonly schema: S;
|
|
21
|
+
readonly upcastSteps: readonly UpcastStep[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Lifts a stored payload from the version it was written at to the
|
|
25
|
+
* version this code reads, one step per version, in order.
|
|
26
|
+
*/
|
|
27
|
+
export declare const upcastToCurrentVersion: <S extends Schema.Schema.AnyNoContext>(definition: VersionedEventDefinition<S>, storedVersion: number, payload: unknown) => Effect.Effect<unknown, VersionedPayloadFailure>;
|
|
28
|
+
/**
|
|
29
|
+
* Lifts a stored payload to the current version and decodes it with the
|
|
30
|
+
* current schema.
|
|
31
|
+
*
|
|
32
|
+
* Lifts a stored payload to the current version and then decodes it with the
|
|
33
|
+
* current schema, so a caller reads today's type or one named failure.
|
|
34
|
+
*/
|
|
35
|
+
export declare const decodeStoredPayload: <S extends Schema.Schema.AnyNoContext>(definition: VersionedEventDefinition<S>, storedVersion: number, payload: unknown) => Effect.Effect<Schema.Schema.Type<S>, VersionedPayloadFailure | ParseResult.ParseError>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Effect, Schema } from 'effect';
|
|
2
|
+
import { MissingUpcastStepError, SchemaVersionFromTheFutureError, UpcastStepFailedError, } from './failures.js';
|
|
3
|
+
/**
|
|
4
|
+
* Lifts a stored payload from the version it was written at to the
|
|
5
|
+
* version this code reads, one step per version, in order.
|
|
6
|
+
*/
|
|
7
|
+
export const upcastToCurrentVersion = (definition, storedVersion, payload) => Effect.gen(function* () {
|
|
8
|
+
if (storedVersion > definition.currentSchemaVersion) {
|
|
9
|
+
return yield* new SchemaVersionFromTheFutureError({
|
|
10
|
+
eventType: definition.eventType,
|
|
11
|
+
storedVersion,
|
|
12
|
+
currentVersion: definition.currentSchemaVersion,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
let lifted = payload;
|
|
16
|
+
for (let version = storedVersion; version < definition.currentSchemaVersion; version = version + 1) {
|
|
17
|
+
const step = definition.upcastSteps.find((candidate) => candidate.fromVersion === version);
|
|
18
|
+
if (step === undefined) {
|
|
19
|
+
return yield* new MissingUpcastStepError({
|
|
20
|
+
eventType: definition.eventType,
|
|
21
|
+
fromVersion: version,
|
|
22
|
+
currentVersion: definition.currentSchemaVersion,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
lifted = yield* Effect.try({
|
|
26
|
+
try: () => step.lift(lifted),
|
|
27
|
+
catch: (cause) => new UpcastStepFailedError({
|
|
28
|
+
eventType: definition.eventType,
|
|
29
|
+
fromVersion: step.fromVersion,
|
|
30
|
+
cause,
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return lifted;
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* Lifts a stored payload to the current version and decodes it with the
|
|
38
|
+
* current schema.
|
|
39
|
+
*
|
|
40
|
+
* Lifts a stored payload to the current version and then decodes it with the
|
|
41
|
+
* current schema, so a caller reads today's type or one named failure.
|
|
42
|
+
*/
|
|
43
|
+
export const decodeStoredPayload = (definition, storedVersion, payload) => upcastToCurrentVersion(definition, storedVersion, payload).pipe(Effect.flatMap((lifted) => Schema.decodeUnknown(definition.schema)(lifted)));
|