@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,58 @@
|
|
|
1
|
+
import { refusalText } from '@satorio/machinery/core';
|
|
2
|
+
import { QueueMessageProducer, QueueMessageSource, } from '@satorio/machinery/ports';
|
|
3
|
+
import { Effect, Layer, Option } from 'effect';
|
|
4
|
+
import { makeSqsQueueMessageProducer, openSqsSendTransport, queueMessageProducerUnavailableWithoutConfiguration, scopedSqsSendTransport, } from './queue-message-producer.js';
|
|
5
|
+
import { makeSqsQueueMessageSource, openSqsTransport, queueMessageSourceUnavailableWithoutConfiguration, scopedSqsTransport, } from './queue-message-source.js';
|
|
6
|
+
import { configuredQueueUrl, queueConsumerSettings, queueProducerSettings, queueUrlRefusal, } from './settings.js';
|
|
7
|
+
/**
|
|
8
|
+
* A message source built from environment variables with the given
|
|
9
|
+
* prefix. With no queue URL set it mounts the loud stand and logs the
|
|
10
|
+
* one refusal every later receive repeats, so startup carries on and
|
|
11
|
+
* nothing goes missing in silence. The transport is a parameter, so a
|
|
12
|
+
* test substitutes its own without touching the wiring.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export const queueMessageSourceFromEnvironmentLive = (variablePrefix, openTransport) => Layer.unwrapEffect(Effect.gen(function* () {
|
|
17
|
+
const configured = yield* configuredQueueUrl(variablePrefix);
|
|
18
|
+
if (Option.isNone(configured)) {
|
|
19
|
+
const refusal = queueUrlRefusal(variablePrefix);
|
|
20
|
+
yield* Effect.logError(refusalText(refusal));
|
|
21
|
+
return Layer.succeed(QueueMessageSource, queueMessageSourceUnavailableWithoutConfiguration(refusal));
|
|
22
|
+
}
|
|
23
|
+
return Layer.scoped(QueueMessageSource, Effect.gen(function* () {
|
|
24
|
+
const settings = yield* queueConsumerSettings(variablePrefix);
|
|
25
|
+
const transport = yield* scopedSqsTransport(() => openTransport(settings));
|
|
26
|
+
return makeSqsQueueMessageSource(transport, settings);
|
|
27
|
+
}));
|
|
28
|
+
}));
|
|
29
|
+
/**
|
|
30
|
+
* The message source on the real AWS SDK transport, reading the
|
|
31
|
+
* variables this prefix names.
|
|
32
|
+
*/
|
|
33
|
+
export const sqsQueueMessageSourceLive = (variablePrefix) => queueMessageSourceFromEnvironmentLive(variablePrefix, openSqsTransport);
|
|
34
|
+
/**
|
|
35
|
+
* A message producer built from environment variables with the given
|
|
36
|
+
* prefix, mounting the loud stand when no queue URL is set. The
|
|
37
|
+
* transport is a parameter for the same reason the source's is.
|
|
38
|
+
*
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
export const queueMessageProducerFromEnvironmentLive = (variablePrefix, openTransport) => Layer.unwrapEffect(Effect.gen(function* () {
|
|
42
|
+
const configured = yield* configuredQueueUrl(variablePrefix);
|
|
43
|
+
if (Option.isNone(configured)) {
|
|
44
|
+
const refusal = queueUrlRefusal(variablePrefix);
|
|
45
|
+
yield* Effect.logError(refusalText(refusal));
|
|
46
|
+
return Layer.succeed(QueueMessageProducer, queueMessageProducerUnavailableWithoutConfiguration(refusal));
|
|
47
|
+
}
|
|
48
|
+
return Layer.scoped(QueueMessageProducer, Effect.gen(function* () {
|
|
49
|
+
const settings = yield* queueProducerSettings(variablePrefix);
|
|
50
|
+
const transport = yield* scopedSqsSendTransport(() => openTransport(settings));
|
|
51
|
+
return makeSqsQueueMessageProducer(transport, settings);
|
|
52
|
+
}));
|
|
53
|
+
}));
|
|
54
|
+
/**
|
|
55
|
+
* The message producer on the real AWS SDK transport, reading the
|
|
56
|
+
* variables this prefix names.
|
|
57
|
+
*/
|
|
58
|
+
export const sqsQueueMessageProducerLive = (variablePrefix) => queueMessageProducerFromEnvironmentLive(variablePrefix, openSqsSendTransport);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type VersionedEventDefinition } from '@satorio/machinery/workflow';
|
|
2
|
+
import { Effect, Schema } from 'effect';
|
|
3
|
+
import { StoredEventEnvelope } from './envelope.js';
|
|
4
|
+
import { QueueMessageNotDecodableError } from './failures.js';
|
|
5
|
+
/**
|
|
6
|
+
* The envelope as it travels: the JSON text on the wire, read and written
|
|
7
|
+
* through one schema.
|
|
8
|
+
*/
|
|
9
|
+
export declare const QueueMessageBody: Schema.transform<Schema.SchemaClass<unknown, string, never>, Schema.Struct<{
|
|
10
|
+
eventId: typeof Schema.NonEmptyTrimmedString;
|
|
11
|
+
eventType: typeof Schema.NonEmptyTrimmedString;
|
|
12
|
+
schemaVersion: Schema.brand<Schema.filter<typeof Schema.Int>, "SchemaVersion">;
|
|
13
|
+
occurredAt: typeof Schema.DateTimeUtc;
|
|
14
|
+
payload: Schema.Record$<typeof Schema.String, typeof Schema.Unknown>;
|
|
15
|
+
}>>;
|
|
16
|
+
/**
|
|
17
|
+
* Reads a message body into an envelope, or refuses at the envelope stage and
|
|
18
|
+
* says what it could not read.
|
|
19
|
+
*/
|
|
20
|
+
export declare const decodeQueueMessageBody: (body: string) => Effect.Effect<StoredEventEnvelope, QueueMessageNotDecodableError>;
|
|
21
|
+
/**
|
|
22
|
+
* What turns a stored payload of some version into the event a handler works
|
|
23
|
+
* with.
|
|
24
|
+
*/
|
|
25
|
+
export type PayloadDecoder<A> = (storedVersion: number, payload: unknown) => Effect.Effect<A, QueueMessageNotDecodableError>;
|
|
26
|
+
/**
|
|
27
|
+
* Builds the payload decoder for one event definition, climbing a stored
|
|
28
|
+
* payload up to the version this code knows.
|
|
29
|
+
*/
|
|
30
|
+
export declare const payloadDecoderFor: <S extends Schema.Schema.AnyNoContext>(definition: VersionedEventDefinition<S>) => PayloadDecoder<Schema.Schema.Type<S>>;
|
|
31
|
+
/**
|
|
32
|
+
* Builds the same envelope contract, but for sending: the producer sets
|
|
33
|
+
* exactly the fields the consumer expects to read.
|
|
34
|
+
*/
|
|
35
|
+
export declare const queueMessageBodyOf: (envelope: {
|
|
36
|
+
readonly eventId: string;
|
|
37
|
+
readonly eventType: string;
|
|
38
|
+
readonly schemaVersion: number;
|
|
39
|
+
readonly occurredAtIso: string;
|
|
40
|
+
readonly payload: {
|
|
41
|
+
readonly [key: string]: unknown;
|
|
42
|
+
};
|
|
43
|
+
}) => string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { decodeStoredPayload, } from '@satorio/machinery/workflow';
|
|
2
|
+
import { Effect, Schema } from 'effect';
|
|
3
|
+
import { StoredEventEnvelope } from './envelope.js';
|
|
4
|
+
import { QueueMessageNotDecodableError } from './failures.js';
|
|
5
|
+
/**
|
|
6
|
+
* The envelope as it travels: the JSON text on the wire, read and written
|
|
7
|
+
* through one schema.
|
|
8
|
+
*/
|
|
9
|
+
export const QueueMessageBody = Schema.parseJson(StoredEventEnvelope).annotations({
|
|
10
|
+
identifier: 'QueueMessageBody',
|
|
11
|
+
});
|
|
12
|
+
const decodeBody = Schema.decode(QueueMessageBody);
|
|
13
|
+
/**
|
|
14
|
+
* Reads a message body into an envelope, or refuses at the envelope stage and
|
|
15
|
+
* says what it could not read.
|
|
16
|
+
*/
|
|
17
|
+
export const decodeQueueMessageBody = (body) => decodeBody(body).pipe(Effect.mapError((cause) => new QueueMessageNotDecodableError({ stage: 'envelope', cause })));
|
|
18
|
+
/**
|
|
19
|
+
* Builds the payload decoder for one event definition, climbing a stored
|
|
20
|
+
* payload up to the version this code knows.
|
|
21
|
+
*/
|
|
22
|
+
export const payloadDecoderFor = (definition) => (storedVersion, payload) => decodeStoredPayload(definition, storedVersion, payload).pipe(Effect.mapError((cause) => new QueueMessageNotDecodableError({ stage: 'payload', cause })));
|
|
23
|
+
/**
|
|
24
|
+
* Builds the same envelope contract, but for sending: the producer sets
|
|
25
|
+
* exactly the fields the consumer expects to read.
|
|
26
|
+
*/
|
|
27
|
+
export const queueMessageBodyOf = (envelope) => JSON.stringify({
|
|
28
|
+
eventId: envelope.eventId,
|
|
29
|
+
eventType: envelope.eventType,
|
|
30
|
+
schemaVersion: envelope.schemaVersion,
|
|
31
|
+
occurredAt: envelope.occurredAtIso,
|
|
32
|
+
payload: envelope.payload,
|
|
33
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type Refusal } from '@satorio/machinery/core';
|
|
2
|
+
import { type QueueMessageProducerShape } from '@satorio/machinery/ports';
|
|
3
|
+
import { Effect, type Scope } from 'effect';
|
|
4
|
+
import { type QueueProducerSettings } from './settings.js';
|
|
5
|
+
/**
|
|
6
|
+
* What one send call asks for.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export interface SqsSendRequest {
|
|
11
|
+
readonly QueueUrl: string;
|
|
12
|
+
readonly MessageBody: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The narrowest slice of the SDK the producer uses: put one body on the
|
|
16
|
+
* queue.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export interface SqsSendTransport {
|
|
21
|
+
readonly sendMessage: (request: SqsSendRequest, signal?: AbortSignal) => Promise<unknown>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The transport plus the one call that closes the client behind it.
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export interface ClosableSqsSendTransport extends SqsSendTransport {
|
|
29
|
+
readonly close: () => void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The producer over one SQS transport: every send goes to the queue this
|
|
33
|
+
* prefix named.
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export declare const makeSqsQueueMessageProducer: (transport: SqsSendTransport, settings: QueueProducerSettings) => QueueMessageProducerShape;
|
|
38
|
+
/**
|
|
39
|
+
* Holds one sending transport open for the length of a scope and closes
|
|
40
|
+
* it when the scope ends.
|
|
41
|
+
*
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
export declare const scopedSqsSendTransport: (openTransport: () => ClosableSqsSendTransport) => Effect.Effect<SqsSendTransport, never, Scope.Scope>;
|
|
45
|
+
/**
|
|
46
|
+
* Opens the real SQS client. This is the only place the sending half of
|
|
47
|
+
* the SDK is constructed.
|
|
48
|
+
*
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
export declare const openSqsSendTransport: (settings: QueueProducerSettings) => ClosableSqsSendTransport;
|
|
52
|
+
/**
|
|
53
|
+
* Without a configured queue, the producer does not fail service
|
|
54
|
+
* startup: it mounts a stand that fails `send` loudly with the same
|
|
55
|
+
* three-part refusal the mount desk logged. The rest of the service
|
|
56
|
+
* keeps running, and the procedure that wanted to send says honestly
|
|
57
|
+
* that the queue is not configured instead of losing what it sent.
|
|
58
|
+
*
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
export declare const queueMessageProducerUnavailableWithoutConfiguration: (refusal: Refusal) => QueueMessageProducerShape;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { SendMessageCommand, SQSClient } from '@aws-sdk/client-sqs';
|
|
2
|
+
import { refusalText } from '@satorio/machinery/core';
|
|
3
|
+
import { QueueMessageProducerUnavailableError, } from '@satorio/machinery/ports';
|
|
4
|
+
import { Effect } from 'effect';
|
|
5
|
+
import { sqsClientConfigurationOf, } from './settings.js';
|
|
6
|
+
/**
|
|
7
|
+
* The producer over one SQS transport: every send goes to the queue this
|
|
8
|
+
* prefix named.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export const makeSqsQueueMessageProducer = (transport, settings) => ({
|
|
13
|
+
send: (body) => Effect.tryPromise({
|
|
14
|
+
try: (signal) => transport.sendMessage({ QueueUrl: settings.queueUrl, MessageBody: body }, signal),
|
|
15
|
+
catch: (cause) => new QueueMessageProducerUnavailableError({ operation: 'send', cause }),
|
|
16
|
+
}).pipe(Effect.asVoid),
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* Holds one sending transport open for the length of a scope and closes
|
|
20
|
+
* it when the scope ends.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export const scopedSqsSendTransport = (openTransport) => Effect.acquireRelease(Effect.sync(openTransport), (transport) => Effect.sync(() => transport.close()));
|
|
25
|
+
/**
|
|
26
|
+
* Opens the real SQS client. This is the only place the sending half of
|
|
27
|
+
* the SDK is constructed.
|
|
28
|
+
*
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
export const openSqsSendTransport = (settings) => {
|
|
32
|
+
const client = new SQSClient(sqsClientConfigurationOf(settings));
|
|
33
|
+
return {
|
|
34
|
+
sendMessage: (request, signal) => client.send(new SendMessageCommand(request), signal === undefined ? {} : { abortSignal: signal }),
|
|
35
|
+
close: () => client.destroy(),
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Without a configured queue, the producer does not fail service
|
|
40
|
+
* startup: it mounts a stand that fails `send` loudly with the same
|
|
41
|
+
* three-part refusal the mount desk logged. The rest of the service
|
|
42
|
+
* keeps running, and the procedure that wanted to send says honestly
|
|
43
|
+
* that the queue is not configured instead of losing what it sent.
|
|
44
|
+
*
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
export const queueMessageProducerUnavailableWithoutConfiguration = (refusal) => ({
|
|
48
|
+
send: () => Effect.fail(new QueueMessageProducerUnavailableError({
|
|
49
|
+
operation: 'send',
|
|
50
|
+
cause: refusalText(refusal),
|
|
51
|
+
})),
|
|
52
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type Refusal } from '@satorio/machinery/core';
|
|
2
|
+
import { type QueueMessageSourceShape } from '@satorio/machinery/ports';
|
|
3
|
+
import { Effect, type Scope } from 'effect';
|
|
4
|
+
import { type QueueConsumerSettings } from './settings.js';
|
|
5
|
+
/**
|
|
6
|
+
* One message as SQS hands it over, before anything checks that the
|
|
7
|
+
* fields a consumer needs are there.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export interface SqsReceivedMessage {
|
|
12
|
+
readonly MessageId?: string | undefined;
|
|
13
|
+
readonly ReceiptHandle?: string | undefined;
|
|
14
|
+
readonly Body?: string | undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* What one receive call answers with.
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
export interface SqsReceiveResult {
|
|
22
|
+
readonly Messages?: readonly SqsReceivedMessage[] | undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* What one receive call asks for.
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export interface SqsReceiveRequest {
|
|
30
|
+
readonly QueueUrl: string;
|
|
31
|
+
readonly MaxNumberOfMessages: number;
|
|
32
|
+
readonly WaitTimeSeconds: number;
|
|
33
|
+
readonly VisibilityTimeout: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* What one acknowledgement asks for.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export interface SqsDeleteRequest {
|
|
41
|
+
readonly QueueUrl: string;
|
|
42
|
+
readonly ReceiptHandle: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The transport is the narrowest slice of the SDK the source uses:
|
|
46
|
+
* receive a batch and delete what was taken. A test can supply its own
|
|
47
|
+
* object of this shape, needing neither the network nor a mock of the
|
|
48
|
+
* whole client.
|
|
49
|
+
*
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
export interface SqsMessageTransport {
|
|
53
|
+
readonly receiveMessages: (request: SqsReceiveRequest, signal?: AbortSignal) => Promise<SqsReceiveResult>;
|
|
54
|
+
readonly deleteMessage: (request: SqsDeleteRequest, signal?: AbortSignal) => Promise<unknown>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The transport plus the one call that closes the client behind it.
|
|
58
|
+
*
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
export interface ClosableSqsTransport extends SqsMessageTransport {
|
|
62
|
+
readonly close: () => void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The message source over one SQS transport: receive a batch with the
|
|
66
|
+
* poll settings this prefix named, and acknowledge by deleting.
|
|
67
|
+
*
|
|
68
|
+
* @internal
|
|
69
|
+
*/
|
|
70
|
+
export declare const makeSqsQueueMessageSource: (transport: SqsMessageTransport, settings: QueueConsumerSettings) => QueueMessageSourceShape;
|
|
71
|
+
/**
|
|
72
|
+
* Holds one transport open for the length of a scope and closes it when
|
|
73
|
+
* the scope ends.
|
|
74
|
+
*
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
export declare const scopedSqsTransport: (openTransport: () => ClosableSqsTransport) => Effect.Effect<SqsMessageTransport, never, Scope.Scope>;
|
|
78
|
+
/**
|
|
79
|
+
* Opens the real SQS client. This is the only place the receiving half
|
|
80
|
+
* of the SDK is constructed.
|
|
81
|
+
*
|
|
82
|
+
* @internal
|
|
83
|
+
*/
|
|
84
|
+
export declare const openSqsTransport: (settings: QueueConsumerSettings) => ClosableSqsTransport;
|
|
85
|
+
/**
|
|
86
|
+
* Without a configured queue, the source does not fail service startup:
|
|
87
|
+
* it mounts a stand that fails `receive` and `acknowledge` loudly with
|
|
88
|
+
* the same three-part refusal the mount desk logged. A consumer polling
|
|
89
|
+
* an unconfigured queue says so on every beat instead of sitting quiet.
|
|
90
|
+
*
|
|
91
|
+
* @internal
|
|
92
|
+
*/
|
|
93
|
+
export declare const queueMessageSourceUnavailableWithoutConfiguration: (refusal: Refusal) => QueueMessageSourceShape;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { DeleteMessageCommand, ReceiveMessageCommand, SQSClient, } from '@aws-sdk/client-sqs';
|
|
2
|
+
import { refusalText } from '@satorio/machinery/core';
|
|
3
|
+
import { QueueMessageSourceUnavailableError, } from '@satorio/machinery/ports';
|
|
4
|
+
import { Effect } from 'effect';
|
|
5
|
+
import { sqsClientConfigurationOf, } from './settings.js';
|
|
6
|
+
const MISSING_FIELD_NAMES = ['MessageId', 'ReceiptHandle', 'Body'];
|
|
7
|
+
const missingFieldNamesOf = (message) => MISSING_FIELD_NAMES.filter((name) => message[name] === undefined);
|
|
8
|
+
const rawMessageOf = (message) => {
|
|
9
|
+
const { MessageId, ReceiptHandle, Body } = message;
|
|
10
|
+
if (MessageId === undefined ||
|
|
11
|
+
ReceiptHandle === undefined ||
|
|
12
|
+
Body === undefined) {
|
|
13
|
+
return Effect.fail(new QueueMessageSourceUnavailableError({
|
|
14
|
+
operation: 'receive',
|
|
15
|
+
cause: `SQS returned a message missing required fields: ${missingFieldNamesOf(message).join(', ')}`,
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
return Effect.succeed({
|
|
19
|
+
messageId: MessageId,
|
|
20
|
+
receiptHandle: ReceiptHandle,
|
|
21
|
+
body: Body,
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The message source over one SQS transport: receive a batch with the
|
|
26
|
+
* poll settings this prefix named, and acknowledge by deleting.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export const makeSqsQueueMessageSource = (transport, settings) => ({
|
|
31
|
+
receive: Effect.tryPromise({
|
|
32
|
+
try: (signal) => transport.receiveMessages({
|
|
33
|
+
QueueUrl: settings.queueUrl,
|
|
34
|
+
MaxNumberOfMessages: settings.maxMessagesPerReceive,
|
|
35
|
+
WaitTimeSeconds: settings.waitTimeSeconds,
|
|
36
|
+
VisibilityTimeout: settings.visibilityTimeoutSeconds,
|
|
37
|
+
}, signal),
|
|
38
|
+
catch: (cause) => new QueueMessageSourceUnavailableError({ operation: 'receive', cause }),
|
|
39
|
+
}).pipe(Effect.flatMap((result) => Effect.forEach(result.Messages ?? [], rawMessageOf))),
|
|
40
|
+
acknowledge: (receiptHandle) => Effect.tryPromise({
|
|
41
|
+
try: (signal) => transport.deleteMessage({ QueueUrl: settings.queueUrl, ReceiptHandle: receiptHandle }, signal),
|
|
42
|
+
catch: (cause) => new QueueMessageSourceUnavailableError({
|
|
43
|
+
operation: 'acknowledge',
|
|
44
|
+
cause,
|
|
45
|
+
}),
|
|
46
|
+
}).pipe(Effect.asVoid),
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* Holds one transport open for the length of a scope and closes it when
|
|
50
|
+
* the scope ends.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export const scopedSqsTransport = (openTransport) => Effect.acquireRelease(Effect.sync(openTransport), (transport) => Effect.sync(() => transport.close()));
|
|
55
|
+
/**
|
|
56
|
+
* Opens the real SQS client. This is the only place the receiving half
|
|
57
|
+
* of the SDK is constructed.
|
|
58
|
+
*
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
export const openSqsTransport = (settings) => {
|
|
62
|
+
const client = new SQSClient(sqsClientConfigurationOf(settings));
|
|
63
|
+
return {
|
|
64
|
+
receiveMessages: (request, signal) => client.send(new ReceiveMessageCommand(request), signal === undefined ? {} : { abortSignal: signal }),
|
|
65
|
+
deleteMessage: (request, signal) => client.send(new DeleteMessageCommand(request), signal === undefined ? {} : { abortSignal: signal }),
|
|
66
|
+
close: () => client.destroy(),
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Without a configured queue, the source does not fail service startup:
|
|
71
|
+
* it mounts a stand that fails `receive` and `acknowledge` loudly with
|
|
72
|
+
* the same three-part refusal the mount desk logged. A consumer polling
|
|
73
|
+
* an unconfigured queue says so on every beat instead of sitting quiet.
|
|
74
|
+
*
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
export const queueMessageSourceUnavailableWithoutConfiguration = (refusal) => ({
|
|
78
|
+
receive: Effect.fail(new QueueMessageSourceUnavailableError({
|
|
79
|
+
operation: 'receive',
|
|
80
|
+
cause: refusalText(refusal),
|
|
81
|
+
})),
|
|
82
|
+
acknowledge: () => Effect.fail(new QueueMessageSourceUnavailableError({
|
|
83
|
+
operation: 'acknowledge',
|
|
84
|
+
cause: refusalText(refusal),
|
|
85
|
+
})),
|
|
86
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type GetQueueAttributesCommandInput } from '@aws-sdk/client-sqs';
|
|
2
|
+
import { QueueMessageSourceUnavailableError } from '@satorio/machinery/ports';
|
|
3
|
+
import { type ConfigError, Effect, Option, type Scope } from 'effect';
|
|
4
|
+
import { type QueueConsumerSettings } from './settings.js';
|
|
5
|
+
/**
|
|
6
|
+
* How long to wait for an SQS reply before treating the queue as
|
|
7
|
+
* unavailable. Without a deadline, a hung network keeps the readiness
|
|
8
|
+
* probe open indefinitely — the same class of gap as the Postgres probe.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare const QUEUE_READINESS_DEADLINE_MILLIS = 2000;
|
|
13
|
+
/**
|
|
14
|
+
* What the probe asks SQS: one attribute of one queue.
|
|
15
|
+
* `GetQueueAttributes` reads nothing off the queue, unlike receive,
|
|
16
|
+
* which polls and hides messages while it does.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export declare const probeRequestOf: (queueUrl: string) => GetQueueAttributesCommandInput;
|
|
21
|
+
/**
|
|
22
|
+
* The one call the probe makes: read an attribute of the queue. It takes
|
|
23
|
+
* no message off the queue, unlike receive.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export interface SqsProbeTransport {
|
|
28
|
+
readonly describeQueue: (queueUrl: string, signal?: AbortSignal) => Promise<unknown>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The probe transport plus the one call that closes the client behind
|
|
32
|
+
* it.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
export interface ClosableSqsProbeTransport extends SqsProbeTransport {
|
|
37
|
+
readonly close: () => void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Opens the client the probe asks with. `GetQueueAttributes` is the
|
|
41
|
+
* cheapest call SQS offers: it reads one attribute and takes nothing off
|
|
42
|
+
* the queue.
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
export declare const openSqsProbeTransport: (settings: QueueConsumerSettings) => ClosableSqsProbeTransport;
|
|
47
|
+
/**
|
|
48
|
+
* Holds the probe's client open for the length of a scope and closes it
|
|
49
|
+
* when the scope ends — including the scope an interruption ends, which
|
|
50
|
+
* a `finally` block around a promise does not reach.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export declare const scopedSqsProbeTransport: (openTransport: () => ClosableSqsProbeTransport) => Effect.Effect<SqsProbeTransport, never, Scope.Scope>;
|
|
55
|
+
/**
|
|
56
|
+
* Pings queue readiness. The client lives for exactly one call — the
|
|
57
|
+
* probe runs rarely, so a long-lived client is not worth it. The SDK
|
|
58
|
+
* call carries Effect's own `abortSignal`, so an interruption cancels
|
|
59
|
+
* the promise through that same signal: the deadline stops the network
|
|
60
|
+
* call itself, not just the wait for it. The transport is a parameter,
|
|
61
|
+
* so a test answers without a network.
|
|
62
|
+
*
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
65
|
+
export declare const pingQueueMessageSourceWith: (settings: QueueConsumerSettings, openTransport: (settings: QueueConsumerSettings) => ClosableSqsProbeTransport) => Effect.Effect<void, QueueMessageSourceUnavailableError>;
|
|
66
|
+
/**
|
|
67
|
+
* Pings queue readiness against the real SQS client.
|
|
68
|
+
*
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
export declare const pingQueueMessageSource: (settings: QueueConsumerSettings) => Effect.Effect<void, QueueMessageSourceUnavailableError>;
|
|
72
|
+
/**
|
|
73
|
+
* The readiness check for a queue, absent when no queue is mounted at all.
|
|
74
|
+
*
|
|
75
|
+
* A queue without a URL is never mounted at all, so its readiness probe is
|
|
76
|
+
* `None`: readyz does not ask about something the application does not have.
|
|
77
|
+
*/
|
|
78
|
+
export declare const queueReadinessCheck: (variablePrefix: string) => Effect.Effect<Option.Option<Effect.Effect<void, QueueMessageSourceUnavailableError>>, ConfigError.ConfigError>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { GetQueueAttributesCommand, SQSClient, } from '@aws-sdk/client-sqs';
|
|
2
|
+
import { QueueMessageSourceUnavailableError } from '@satorio/machinery/ports';
|
|
3
|
+
import { Duration, Effect, Option } from 'effect';
|
|
4
|
+
import { configuredQueueUrl, queueConsumerSettings, sqsClientConfigurationOf, } from './settings.js';
|
|
5
|
+
/**
|
|
6
|
+
* How long to wait for an SQS reply before treating the queue as
|
|
7
|
+
* unavailable. Without a deadline, a hung network keeps the readiness
|
|
8
|
+
* probe open indefinitely — the same class of gap as the Postgres probe.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export const QUEUE_READINESS_DEADLINE_MILLIS = 2_000;
|
|
13
|
+
const QUEUE_ARN_ATTRIBUTE = 'QueueArn';
|
|
14
|
+
/**
|
|
15
|
+
* What the probe asks SQS: one attribute of one queue.
|
|
16
|
+
* `GetQueueAttributes` reads nothing off the queue, unlike receive,
|
|
17
|
+
* which polls and hides messages while it does.
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
export const probeRequestOf = (queueUrl) => ({
|
|
22
|
+
QueueUrl: queueUrl,
|
|
23
|
+
AttributeNames: [QUEUE_ARN_ATTRIBUTE],
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* Opens the client the probe asks with. `GetQueueAttributes` is the
|
|
27
|
+
* cheapest call SQS offers: it reads one attribute and takes nothing off
|
|
28
|
+
* the queue.
|
|
29
|
+
*
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export const openSqsProbeTransport = (settings) => {
|
|
33
|
+
const client = new SQSClient(sqsClientConfigurationOf(settings));
|
|
34
|
+
return {
|
|
35
|
+
describeQueue: (queueUrl, signal) => client.send(new GetQueueAttributesCommand(probeRequestOf(queueUrl)), signal === undefined ? {} : { abortSignal: signal }),
|
|
36
|
+
close: () => client.destroy(),
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Holds the probe's client open for the length of a scope and closes it
|
|
41
|
+
* when the scope ends — including the scope an interruption ends, which
|
|
42
|
+
* a `finally` block around a promise does not reach.
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
export const scopedSqsProbeTransport = (openTransport) => Effect.acquireRelease(Effect.sync(openTransport), (transport) => Effect.sync(() => transport.close()));
|
|
47
|
+
const askWith = (transport, settings) => Effect.tryPromise({
|
|
48
|
+
try: (signal) => transport.describeQueue(settings.queueUrl, signal),
|
|
49
|
+
catch: (cause) => new QueueMessageSourceUnavailableError({ operation: 'ping', cause }),
|
|
50
|
+
}).pipe(Effect.asVoid);
|
|
51
|
+
/**
|
|
52
|
+
* Pings queue readiness. The client lives for exactly one call — the
|
|
53
|
+
* probe runs rarely, so a long-lived client is not worth it. The SDK
|
|
54
|
+
* call carries Effect's own `abortSignal`, so an interruption cancels
|
|
55
|
+
* the promise through that same signal: the deadline stops the network
|
|
56
|
+
* call itself, not just the wait for it. The transport is a parameter,
|
|
57
|
+
* so a test answers without a network.
|
|
58
|
+
*
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
export const pingQueueMessageSourceWith = (settings, openTransport) => Effect.scoped(Effect.flatMap(scopedSqsProbeTransport(() => openTransport(settings)), (transport) => askWith(transport, settings))).pipe(Effect.timeoutFail({
|
|
62
|
+
duration: Duration.millis(QUEUE_READINESS_DEADLINE_MILLIS),
|
|
63
|
+
onTimeout: () => new QueueMessageSourceUnavailableError({
|
|
64
|
+
operation: 'ping',
|
|
65
|
+
cause: 'timed out waiting for a response',
|
|
66
|
+
}),
|
|
67
|
+
}));
|
|
68
|
+
/**
|
|
69
|
+
* Pings queue readiness against the real SQS client.
|
|
70
|
+
*
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
export const pingQueueMessageSource = (settings) => pingQueueMessageSourceWith(settings, openSqsProbeTransport);
|
|
74
|
+
/**
|
|
75
|
+
* The readiness check for a queue, absent when no queue is mounted at all.
|
|
76
|
+
*
|
|
77
|
+
* A queue without a URL is never mounted at all, so its readiness probe is
|
|
78
|
+
* `None`: readyz does not ask about something the application does not have.
|
|
79
|
+
*/
|
|
80
|
+
export const queueReadinessCheck = (variablePrefix) => Effect.gen(function* () {
|
|
81
|
+
const configured = yield* configuredQueueUrl(variablePrefix);
|
|
82
|
+
if (Option.isNone(configured))
|
|
83
|
+
return Option.none();
|
|
84
|
+
const settings = yield* queueConsumerSettings(variablePrefix);
|
|
85
|
+
return Option.some(pingQueueMessageSource(settings));
|
|
86
|
+
});
|