@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,106 @@
|
|
|
1
|
+
import { type EnvironmentEntry, type Refusal } from '@satorio/machinery/core';
|
|
2
|
+
import { Config, Option } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* The most messages SQS hands back from one receive call. The service
|
|
5
|
+
* asks for no more than this, because SQS returns no more than this.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export declare const SQS_HIGHEST_MESSAGES_PER_RECEIVE = 10;
|
|
10
|
+
/**
|
|
11
|
+
* The longest long poll SQS allows. Anything above it is a value SQS
|
|
12
|
+
* itself refuses.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export declare const SQS_HIGHEST_WAIT_TIME_SECONDS = 20;
|
|
17
|
+
/**
|
|
18
|
+
* The longest SQS keeps a received message hidden from other consumers:
|
|
19
|
+
* twelve hours.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export declare const SQS_HIGHEST_VISIBILITY_TIMEOUT_SECONDS = 43200;
|
|
24
|
+
/**
|
|
25
|
+
* How long a received message stays hidden while a handler works on it,
|
|
26
|
+
* when nobody names a number. A minute is long enough for an ordinary
|
|
27
|
+
* handler and short enough that a crashed one redelivers soon.
|
|
28
|
+
*/
|
|
29
|
+
export declare const SQS_DEFAULT_VISIBILITY_TIMEOUT_SECONDS = 60;
|
|
30
|
+
/**
|
|
31
|
+
* Builds the environment variable name from the prefix the application
|
|
32
|
+
* gives at mount time. One process serves any number of queues this way,
|
|
33
|
+
* with readable names: `INBOUND_QUEUE_URL`, `RETRY_QUEUE_URL`, and so on.
|
|
34
|
+
*/
|
|
35
|
+
export declare const queueUrlVariable: (variablePrefix: string) => string;
|
|
36
|
+
/**
|
|
37
|
+
* What a consumer needs before it can receive a single message.
|
|
38
|
+
*
|
|
39
|
+
* What a consumer needs before it can receive: the queue to poll, how it
|
|
40
|
+
* polls, and the two variables the AWS SDK reads for itself when nobody names
|
|
41
|
+
* them.
|
|
42
|
+
*/
|
|
43
|
+
export interface QueueConsumerSettings {
|
|
44
|
+
readonly queueUrl: string;
|
|
45
|
+
readonly maxMessagesPerReceive: number;
|
|
46
|
+
readonly waitTimeSeconds: number;
|
|
47
|
+
readonly visibilityTimeoutSeconds: number;
|
|
48
|
+
readonly region: Option.Option<string>;
|
|
49
|
+
readonly endpoint: Option.Option<string>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* What a producer needs before it can send: the queue, and the same two
|
|
53
|
+
* variables the SDK decides for itself.
|
|
54
|
+
*/
|
|
55
|
+
export interface QueueProducerSettings {
|
|
56
|
+
readonly queueUrl: string;
|
|
57
|
+
readonly region: Option.Option<string>;
|
|
58
|
+
readonly endpoint: Option.Option<string>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The three parts a reader meets when the queue URL is not set. The
|
|
62
|
+
* mount desk logs this text and every operation of the loud stub fails
|
|
63
|
+
* with it, so an operator reads one sentence rather than two.
|
|
64
|
+
*
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
export declare const queueUrlRefusal: (variablePrefix: string) => Refusal;
|
|
68
|
+
/**
|
|
69
|
+
* Reads the queue URL without refusing, so the mount desk can put up the
|
|
70
|
+
* loud stub instead of failing service startup.
|
|
71
|
+
*
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
74
|
+
export declare const configuredQueueUrl: (variablePrefix: string) => Config.Config<Option.Option<string>>;
|
|
75
|
+
/**
|
|
76
|
+
* Every variable this adapter reads, and the one state each of them
|
|
77
|
+
* stands in. `<PREFIX>` is the prefix the application passes at mount
|
|
78
|
+
* time; the three poll settings share that prefix and keep their own
|
|
79
|
+
* suffixes.
|
|
80
|
+
*/
|
|
81
|
+
export declare const ENVIRONMENT: readonly EnvironmentEntry[];
|
|
82
|
+
/**
|
|
83
|
+
* Reads every variable a consumer needs under one prefix. A missing or
|
|
84
|
+
* blank queue URL refuses in three parts; the three poll settings fall
|
|
85
|
+
* back to their defaults; region and endpoint stay `Option.none`, and
|
|
86
|
+
* the SDK decides.
|
|
87
|
+
*/
|
|
88
|
+
export declare const queueConsumerSettings: (variablePrefix: string) => Config.Config<QueueConsumerSettings>;
|
|
89
|
+
/**
|
|
90
|
+
* Reads every variable a producer needs under one prefix. A producer
|
|
91
|
+
* only sends, so it reads the queue URL and nothing about polling.
|
|
92
|
+
*/
|
|
93
|
+
export declare const queueProducerSettings: (variablePrefix: string) => Config.Config<QueueProducerSettings>;
|
|
94
|
+
/**
|
|
95
|
+
* The client configuration shape both transports and the readiness probe
|
|
96
|
+
* share: region and endpoint appear in it only when they are set.
|
|
97
|
+
*
|
|
98
|
+
* @internal
|
|
99
|
+
*/
|
|
100
|
+
export declare const sqsClientConfigurationOf: (settings: {
|
|
101
|
+
readonly region: Option.Option<string>;
|
|
102
|
+
readonly endpoint: Option.Option<string>;
|
|
103
|
+
}) => {
|
|
104
|
+
readonly region?: string;
|
|
105
|
+
readonly endpoint?: string;
|
|
106
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { fallsBackTo, handedOn, optionalText, refuses, requiredText, wholeNumberFallingBackTo, } from '@satorio/machinery/core';
|
|
2
|
+
import { Config, Option } from 'effect';
|
|
3
|
+
const QUEUE_URL_SUFFIX = '_QUEUE_URL';
|
|
4
|
+
const MAX_MESSAGES_SUFFIX = '_MAX_MESSAGES_PER_RECEIVE';
|
|
5
|
+
const WAIT_TIME_SUFFIX = '_WAIT_TIME_SECONDS';
|
|
6
|
+
const VISIBILITY_TIMEOUT_SUFFIX = '_VISIBILITY_TIMEOUT_SECONDS';
|
|
7
|
+
const REGION = 'AWS_REGION';
|
|
8
|
+
const ENDPOINT = 'AWS_ENDPOINT_URL';
|
|
9
|
+
const LOWEST = 1;
|
|
10
|
+
/**
|
|
11
|
+
* The most messages SQS hands back from one receive call. The service
|
|
12
|
+
* asks for no more than this, because SQS returns no more than this.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export const SQS_HIGHEST_MESSAGES_PER_RECEIVE = 10;
|
|
17
|
+
/**
|
|
18
|
+
* The longest long poll SQS allows. Anything above it is a value SQS
|
|
19
|
+
* itself refuses.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export const SQS_HIGHEST_WAIT_TIME_SECONDS = 20;
|
|
24
|
+
/**
|
|
25
|
+
* The longest SQS keeps a received message hidden from other consumers:
|
|
26
|
+
* twelve hours.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export const SQS_HIGHEST_VISIBILITY_TIMEOUT_SECONDS = 43_200;
|
|
31
|
+
/**
|
|
32
|
+
* How long a received message stays hidden while a handler works on it,
|
|
33
|
+
* when nobody names a number. A minute is long enough for an ordinary
|
|
34
|
+
* handler and short enough that a crashed one redelivers soon.
|
|
35
|
+
*/
|
|
36
|
+
export const SQS_DEFAULT_VISIBILITY_TIMEOUT_SECONDS = 60;
|
|
37
|
+
/**
|
|
38
|
+
* Builds the environment variable name from the prefix the application
|
|
39
|
+
* gives at mount time. One process serves any number of queues this way,
|
|
40
|
+
* with readable names: `INBOUND_QUEUE_URL`, `RETRY_QUEUE_URL`, and so on.
|
|
41
|
+
*/
|
|
42
|
+
export const queueUrlVariable = (variablePrefix) => `${variablePrefix}${QUEUE_URL_SUFFIX}`;
|
|
43
|
+
/**
|
|
44
|
+
* The three parts a reader meets when the queue URL is not set. The
|
|
45
|
+
* mount desk logs this text and every operation of the loud stub fails
|
|
46
|
+
* with it, so an operator reads one sentence rather than two.
|
|
47
|
+
*
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
export const queueUrlRefusal = (variablePrefix) => ({
|
|
51
|
+
what: `${queueUrlVariable(variablePrefix)} is not set, so this queue has no address.`,
|
|
52
|
+
why: 'One process serves several queues, each under its own prefix, so the machinery cannot guess which one you meant.',
|
|
53
|
+
whatToDo: `Set ${queueUrlVariable(variablePrefix)} to the full queue URL from the AWS console or your .env, then start the service again.`,
|
|
54
|
+
});
|
|
55
|
+
const rangeRefusal = (name, what, highest) => ({
|
|
56
|
+
what: `${name} is set to something this consumer cannot poll with.`,
|
|
57
|
+
why: `${what} It has to be a whole number from ${LOWEST} to ${highest}, which is the range SQS itself accepts.`,
|
|
58
|
+
whatToDo: `Set ${name} to a whole number from ${LOWEST} to ${highest}, or leave it unset and take the default.`,
|
|
59
|
+
});
|
|
60
|
+
/**
|
|
61
|
+
* Reads the queue URL without refusing, so the mount desk can put up the
|
|
62
|
+
* loud stub instead of failing service startup.
|
|
63
|
+
*
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
export const configuredQueueUrl = (variablePrefix) => optionalText(queueUrlVariable(variablePrefix));
|
|
67
|
+
/**
|
|
68
|
+
* Every variable this adapter reads, and the one state each of them
|
|
69
|
+
* stands in. `<PREFIX>` is the prefix the application passes at mount
|
|
70
|
+
* time; the three poll settings share that prefix and keep their own
|
|
71
|
+
* suffixes.
|
|
72
|
+
*/
|
|
73
|
+
export const ENVIRONMENT = [
|
|
74
|
+
{
|
|
75
|
+
name: QUEUE_URL_SUFFIX,
|
|
76
|
+
prefixed: true,
|
|
77
|
+
what: 'The queue a consumer receives from and a producer sends to.',
|
|
78
|
+
state: refuses(queueUrlRefusal('<PREFIX>')),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: MAX_MESSAGES_SUFFIX,
|
|
82
|
+
prefixed: true,
|
|
83
|
+
what: 'How many messages one receive call asks for.',
|
|
84
|
+
state: fallsBackTo(String(SQS_HIGHEST_MESSAGES_PER_RECEIVE)),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: WAIT_TIME_SUFFIX,
|
|
88
|
+
prefixed: true,
|
|
89
|
+
what: 'How many seconds a receive call waits for a message before it comes back empty.',
|
|
90
|
+
state: fallsBackTo(String(SQS_HIGHEST_WAIT_TIME_SECONDS)),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: VISIBILITY_TIMEOUT_SUFFIX,
|
|
94
|
+
prefixed: true,
|
|
95
|
+
what: 'How many seconds a received message stays hidden from other consumers.',
|
|
96
|
+
state: fallsBackTo(String(SQS_DEFAULT_VISIBILITY_TIMEOUT_SECONDS)),
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: REGION,
|
|
100
|
+
what: 'The region the SQS client talks to.',
|
|
101
|
+
state: handedOn('the AWS SDK finds its own region from the machine it runs on.'),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: ENDPOINT,
|
|
105
|
+
what: 'A local stand to talk to instead of real SQS, such as localstack.',
|
|
106
|
+
state: handedOn('the client goes to the real SQS for the region it found.'),
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
const queueUrl = (variablePrefix) => requiredText(queueUrlVariable(variablePrefix), queueUrlRefusal(variablePrefix));
|
|
110
|
+
const maxMessagesPerReceive = (variablePrefix) => {
|
|
111
|
+
const name = `${variablePrefix}${MAX_MESSAGES_SUFFIX}`;
|
|
112
|
+
return wholeNumberFallingBackTo(name, LOWEST, SQS_HIGHEST_MESSAGES_PER_RECEIVE, SQS_HIGHEST_MESSAGES_PER_RECEIVE, rangeRefusal(name, 'It says how many messages one receive call asks for.', SQS_HIGHEST_MESSAGES_PER_RECEIVE));
|
|
113
|
+
};
|
|
114
|
+
const waitTimeSeconds = (variablePrefix) => {
|
|
115
|
+
const name = `${variablePrefix}${WAIT_TIME_SUFFIX}`;
|
|
116
|
+
return wholeNumberFallingBackTo(name, LOWEST, SQS_HIGHEST_WAIT_TIME_SECONDS, SQS_HIGHEST_WAIT_TIME_SECONDS, rangeRefusal(name, 'Zero turns long polling off and makes the consumer a hot loop.', SQS_HIGHEST_WAIT_TIME_SECONDS));
|
|
117
|
+
};
|
|
118
|
+
const visibilityTimeoutSeconds = (variablePrefix) => {
|
|
119
|
+
const name = `${variablePrefix}${VISIBILITY_TIMEOUT_SUFFIX}`;
|
|
120
|
+
return wholeNumberFallingBackTo(name, LOWEST, SQS_HIGHEST_VISIBILITY_TIMEOUT_SECONDS, SQS_DEFAULT_VISIBILITY_TIMEOUT_SECONDS, rangeRefusal(name, 'It says how long a handler has before the message comes back to another consumer.', SQS_HIGHEST_VISIBILITY_TIMEOUT_SECONDS));
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Reads every variable a consumer needs under one prefix. A missing or
|
|
124
|
+
* blank queue URL refuses in three parts; the three poll settings fall
|
|
125
|
+
* back to their defaults; region and endpoint stay `Option.none`, and
|
|
126
|
+
* the SDK decides.
|
|
127
|
+
*/
|
|
128
|
+
export const queueConsumerSettings = (variablePrefix) => Config.all({
|
|
129
|
+
queueUrl: queueUrl(variablePrefix),
|
|
130
|
+
maxMessagesPerReceive: maxMessagesPerReceive(variablePrefix),
|
|
131
|
+
waitTimeSeconds: waitTimeSeconds(variablePrefix),
|
|
132
|
+
visibilityTimeoutSeconds: visibilityTimeoutSeconds(variablePrefix),
|
|
133
|
+
region: optionalText(REGION),
|
|
134
|
+
endpoint: optionalText(ENDPOINT),
|
|
135
|
+
});
|
|
136
|
+
/**
|
|
137
|
+
* Reads every variable a producer needs under one prefix. A producer
|
|
138
|
+
* only sends, so it reads the queue URL and nothing about polling.
|
|
139
|
+
*/
|
|
140
|
+
export const queueProducerSettings = (variablePrefix) => Config.all({
|
|
141
|
+
queueUrl: queueUrl(variablePrefix),
|
|
142
|
+
region: optionalText(REGION),
|
|
143
|
+
endpoint: optionalText(ENDPOINT),
|
|
144
|
+
});
|
|
145
|
+
/**
|
|
146
|
+
* The client configuration shape both transports and the readiness probe
|
|
147
|
+
* share: region and endpoint appear in it only when they are set.
|
|
148
|
+
*
|
|
149
|
+
* @internal
|
|
150
|
+
*/
|
|
151
|
+
export const sqsClientConfigurationOf = (settings) => ({
|
|
152
|
+
...Option.match(settings.region, {
|
|
153
|
+
onNone: () => ({}),
|
|
154
|
+
onSome: (region) => ({ region }),
|
|
155
|
+
}),
|
|
156
|
+
...Option.match(settings.endpoint, {
|
|
157
|
+
onNone: () => ({}),
|
|
158
|
+
onSome: (endpoint) => ({ endpoint }),
|
|
159
|
+
}),
|
|
160
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { type Refusal } from '@satorio/machinery/core';
|
|
2
|
+
import { type BlobStoreShape } from '@satorio/machinery/ports';
|
|
3
|
+
import { Effect, type Scope } from 'effect';
|
|
4
|
+
import type { BlobStoreSettings } from './settings.js';
|
|
5
|
+
/**
|
|
6
|
+
* The narrowest slice of the SDK the store uses: put, get, check
|
|
7
|
+
* existence, delete. A test can substitute its own object of this shape
|
|
8
|
+
* without a network or a mock of the whole client.
|
|
9
|
+
*/
|
|
10
|
+
export interface S3PutRequest {
|
|
11
|
+
readonly Bucket: string;
|
|
12
|
+
readonly Key: string;
|
|
13
|
+
readonly Body: Uint8Array;
|
|
14
|
+
readonly ContentType: string;
|
|
15
|
+
readonly Metadata: Readonly<Record<string, string>>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* One object named: the bucket it sits in and the key it sits under.
|
|
19
|
+
*/
|
|
20
|
+
export interface S3KeyRequest {
|
|
21
|
+
readonly Bucket: string;
|
|
22
|
+
readonly Key: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* One object as the store hands it back: its bytes, and the content type and
|
|
26
|
+
* metadata it carries when it carries any.
|
|
27
|
+
*/
|
|
28
|
+
export interface S3FetchedObject {
|
|
29
|
+
readonly body: Uint8Array;
|
|
30
|
+
readonly contentType: string | undefined;
|
|
31
|
+
readonly metadata: Readonly<Record<string, string>> | undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The narrow slice of the object store a blob store calls: put, get, check,
|
|
35
|
+
* delete, and nothing else.
|
|
36
|
+
*/
|
|
37
|
+
export interface S3ObjectTransport {
|
|
38
|
+
readonly putObject: (request: S3PutRequest, signal?: AbortSignal) => Promise<unknown>;
|
|
39
|
+
readonly getObject: (request: S3KeyRequest, signal?: AbortSignal) => Promise<S3FetchedObject>;
|
|
40
|
+
readonly headObject: (request: S3KeyRequest, signal?: AbortSignal) => Promise<unknown>;
|
|
41
|
+
readonly deleteObject: (request: S3KeyRequest, signal?: AbortSignal) => Promise<unknown>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A transport that also closes, so the client behind it is released when the
|
|
45
|
+
* scope around it ends.
|
|
46
|
+
*/
|
|
47
|
+
export interface ClosableS3Transport extends S3ObjectTransport {
|
|
48
|
+
readonly close: () => void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* S3 signals "no such key" as an ERROR, not an empty response: `GetObject`
|
|
52
|
+
* throws `NoSuchKey`, `HeadObject` throws `NotFound`. The port is more
|
|
53
|
+
* honest than the SDK: absence is `Option.none` and `false`, not a failure.
|
|
54
|
+
*/
|
|
55
|
+
export declare const signalsAbsentKey: (error: unknown) => boolean;
|
|
56
|
+
/**
|
|
57
|
+
* What an object is read as when the store hands back no content type of its
|
|
58
|
+
* own: plain bytes.
|
|
59
|
+
*/
|
|
60
|
+
export declare const DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
|
61
|
+
/**
|
|
62
|
+
* Builds the blob store over one transport and one bucket, turning the store's
|
|
63
|
+
* own way of signalling a missing key into an absent value.
|
|
64
|
+
*/
|
|
65
|
+
export declare const makeS3BlobStore: (transport: S3ObjectTransport, settings: BlobStoreSettings) => BlobStoreShape;
|
|
66
|
+
/**
|
|
67
|
+
* Opens a transport that closes with the scope around it, so no client
|
|
68
|
+
* outlives the work that opened it.
|
|
69
|
+
*/
|
|
70
|
+
export declare const scopedS3Transport: (openTransport: () => ClosableS3Transport) => Effect.Effect<S3ObjectTransport, never, Scope.Scope>;
|
|
71
|
+
/**
|
|
72
|
+
* Opens the real object-store client against the settings handed in, and is
|
|
73
|
+
* the one place that client is constructed.
|
|
74
|
+
*/
|
|
75
|
+
export declare const openS3Transport: (settings: BlobStoreSettings) => ClosableS3Transport;
|
|
76
|
+
/**
|
|
77
|
+
* Without a bucket set, the store does not fail service startup: it mounts
|
|
78
|
+
* a stub that fails every operation loudly. The rest of the service keeps
|
|
79
|
+
* running, and the use case reports the missing configuration honestly
|
|
80
|
+
* instead of silently losing what it stored. The stub carries the same
|
|
81
|
+
* `Refusal` the mount desk logs, so the operator reads one text on both
|
|
82
|
+
* paths.
|
|
83
|
+
*/
|
|
84
|
+
export declare const blobStoreUnavailableWithoutConfiguration: (refusal: Refusal) => BlobStoreShape;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3';
|
|
2
|
+
import { refusalText } from '@satorio/machinery/core';
|
|
3
|
+
import { BlobStoreUnavailableError, } from '@satorio/machinery/ports';
|
|
4
|
+
import { Effect, Option } from 'effect';
|
|
5
|
+
/**
|
|
6
|
+
* S3 signals "no such key" as an ERROR, not an empty response: `GetObject`
|
|
7
|
+
* throws `NoSuchKey`, `HeadObject` throws `NotFound`. The port is more
|
|
8
|
+
* honest than the SDK: absence is `Option.none` and `false`, not a failure.
|
|
9
|
+
*/
|
|
10
|
+
export const signalsAbsentKey = (error) => error instanceof Error &&
|
|
11
|
+
(error.name === 'NoSuchKey' || error.name === 'NotFound');
|
|
12
|
+
/**
|
|
13
|
+
* What an object is read as when the store hands back no content type of its
|
|
14
|
+
* own: plain bytes.
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_CONTENT_TYPE = 'application/octet-stream';
|
|
17
|
+
const storedBlobOf = (fetched) => ({
|
|
18
|
+
body: fetched.body,
|
|
19
|
+
contentType: fetched.contentType ?? DEFAULT_CONTENT_TYPE,
|
|
20
|
+
metadata: fetched.metadata ?? {},
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Builds the blob store over one transport and one bucket, turning the store's
|
|
24
|
+
* own way of signalling a missing key into an absent value.
|
|
25
|
+
*/
|
|
26
|
+
export const makeS3BlobStore = (transport, settings) => ({
|
|
27
|
+
put: (key, blob) => Effect.tryPromise({
|
|
28
|
+
try: (signal) => transport.putObject({
|
|
29
|
+
Bucket: settings.bucket,
|
|
30
|
+
Key: key,
|
|
31
|
+
Body: blob.body,
|
|
32
|
+
ContentType: blob.contentType,
|
|
33
|
+
Metadata: blob.metadata,
|
|
34
|
+
}, signal),
|
|
35
|
+
catch: (cause) => new BlobStoreUnavailableError({ operation: 'put', cause }),
|
|
36
|
+
}).pipe(Effect.asVoid),
|
|
37
|
+
get: (key) => Effect.tryPromise({
|
|
38
|
+
try: (signal) => transport.getObject({ Bucket: settings.bucket, Key: key }, signal),
|
|
39
|
+
catch: (cause) => new BlobStoreUnavailableError({ operation: 'get', cause }),
|
|
40
|
+
}).pipe(Effect.map((fetched) => Option.some(storedBlobOf(fetched))), Effect.catchIf((error) => signalsAbsentKey(error.cause), () => Effect.succeed(Option.none()))),
|
|
41
|
+
exists: (key) => Effect.tryPromise({
|
|
42
|
+
try: (signal) => transport.headObject({ Bucket: settings.bucket, Key: key }, signal),
|
|
43
|
+
catch: (cause) => new BlobStoreUnavailableError({ operation: 'exists', cause }),
|
|
44
|
+
}).pipe(Effect.as(true), Effect.catchIf((error) => signalsAbsentKey(error.cause), () => Effect.succeed(false))),
|
|
45
|
+
delete: (key) => Effect.tryPromise({
|
|
46
|
+
try: (signal) => transport.deleteObject({ Bucket: settings.bucket, Key: key }, signal),
|
|
47
|
+
catch: (cause) => new BlobStoreUnavailableError({ operation: 'delete', cause }),
|
|
48
|
+
}).pipe(Effect.asVoid),
|
|
49
|
+
});
|
|
50
|
+
/**
|
|
51
|
+
* Opens a transport that closes with the scope around it, so no client
|
|
52
|
+
* outlives the work that opened it.
|
|
53
|
+
*/
|
|
54
|
+
export const scopedS3Transport = (openTransport) => Effect.acquireRelease(Effect.sync(openTransport), (transport) => Effect.sync(() => transport.close()));
|
|
55
|
+
const clientConfigurationOf = (settings) => ({
|
|
56
|
+
...Option.match(settings.region, {
|
|
57
|
+
onNone: () => ({}),
|
|
58
|
+
onSome: (region) => ({ region }),
|
|
59
|
+
}),
|
|
60
|
+
...Option.match(settings.endpoint, {
|
|
61
|
+
onNone: () => ({}),
|
|
62
|
+
onSome: (endpoint) => ({ endpoint, forcePathStyle: true }),
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
const abortOptions = (signal) => signal === undefined ? {} : { abortSignal: signal };
|
|
66
|
+
/**
|
|
67
|
+
* Opens the real object-store client against the settings handed in, and is
|
|
68
|
+
* the one place that client is constructed.
|
|
69
|
+
*/
|
|
70
|
+
export const openS3Transport = (settings) => {
|
|
71
|
+
const client = new S3Client(clientConfigurationOf(settings));
|
|
72
|
+
return {
|
|
73
|
+
putObject: (request, signal) => client.send(new PutObjectCommand(request), abortOptions(signal)),
|
|
74
|
+
getObject: async (request, signal) => {
|
|
75
|
+
const result = await client.send(new GetObjectCommand(request), abortOptions(signal));
|
|
76
|
+
if (result.Body === undefined) {
|
|
77
|
+
throw new Error('S3 returned an object without a body');
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
body: await result.Body.transformToByteArray(),
|
|
81
|
+
contentType: result.ContentType,
|
|
82
|
+
metadata: result.Metadata,
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
headObject: (request, signal) => client.send(new HeadObjectCommand(request), abortOptions(signal)),
|
|
86
|
+
deleteObject: (request, signal) => client.send(new DeleteObjectCommand(request), abortOptions(signal)),
|
|
87
|
+
close: () => client.destroy(),
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Without a bucket set, the store does not fail service startup: it mounts
|
|
92
|
+
* a stub that fails every operation loudly. The rest of the service keeps
|
|
93
|
+
* running, and the use case reports the missing configuration honestly
|
|
94
|
+
* instead of silently losing what it stored. The stub carries the same
|
|
95
|
+
* `Refusal` the mount desk logs, so the operator reads one text on both
|
|
96
|
+
* paths.
|
|
97
|
+
*/
|
|
98
|
+
export const blobStoreUnavailableWithoutConfiguration = (refusal) => {
|
|
99
|
+
const failure = (operation) => new BlobStoreUnavailableError({
|
|
100
|
+
operation,
|
|
101
|
+
cause: refusalText(refusal),
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
put: () => failure('put'),
|
|
105
|
+
get: () => failure('get'),
|
|
106
|
+
exists: () => failure('exists'),
|
|
107
|
+
delete: () => failure('delete'),
|
|
108
|
+
};
|
|
109
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry to the storage mechanism: whole objects stored under a key and
|
|
3
|
+
* read back. It gives you the `BlobStore` port bound to S3, with the bucket
|
|
4
|
+
* read from one prefixed variable and a loud stub in its place when nobody set
|
|
5
|
+
* it.
|
|
6
|
+
*/
|
|
7
|
+
export { blobStoreUnavailableWithoutConfiguration, type ClosableS3Transport, DEFAULT_CONTENT_TYPE, makeS3BlobStore, openS3Transport, type S3FetchedObject, type S3KeyRequest, type S3ObjectTransport, type S3PutRequest, scopedS3Transport, signalsAbsentKey, } from './blob-store.js';
|
|
8
|
+
export { blobStoreFromEnvironmentLive, s3BlobStoreLive } from './layer.js';
|
|
9
|
+
export { type BlobStoreSettings, blobStoreSettings, bucketVariable, ENVIRONMENT, } from './settings.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry to the storage mechanism: whole objects stored under a key and
|
|
3
|
+
* read back. It gives you the `BlobStore` port bound to S3, with the bucket
|
|
4
|
+
* read from one prefixed variable and a loud stub in its place when nobody set
|
|
5
|
+
* it.
|
|
6
|
+
*/
|
|
7
|
+
export { blobStoreUnavailableWithoutConfiguration, DEFAULT_CONTENT_TYPE, makeS3BlobStore, openS3Transport, scopedS3Transport, signalsAbsentKey, } from './blob-store.js';
|
|
8
|
+
export { blobStoreFromEnvironmentLive, s3BlobStoreLive } from './layer.js';
|
|
9
|
+
export { blobStoreSettings, bucketVariable, ENVIRONMENT, } from './settings.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BlobStore } from '@satorio/machinery/ports';
|
|
2
|
+
import { type ConfigError, Layer } from 'effect';
|
|
3
|
+
import { type ClosableS3Transport } from './blob-store.js';
|
|
4
|
+
import { type BlobStoreSettings } from './settings.js';
|
|
5
|
+
/**
|
|
6
|
+
* A blob store built from environment variables with the given prefix.
|
|
7
|
+
* With no bucket set it mounts the loud stub and logs the one refusal
|
|
8
|
+
* every later operation repeats, so startup carries on and nothing goes
|
|
9
|
+
* missing in silence. The transport is a parameter, so a test
|
|
10
|
+
* substitutes its own without touching the wiring.
|
|
11
|
+
*/
|
|
12
|
+
export declare const blobStoreFromEnvironmentLive: (variablePrefix: string, openTransport: (settings: BlobStoreSettings) => ClosableS3Transport) => Layer.Layer<BlobStore, ConfigError.ConfigError>;
|
|
13
|
+
/**
|
|
14
|
+
* The blob store on the real AWS SDK transport, reading the variables
|
|
15
|
+
* this prefix names.
|
|
16
|
+
*/
|
|
17
|
+
export declare const s3BlobStoreLive: (variablePrefix: string) => Layer.Layer<BlobStore, ConfigError.ConfigError>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { refusalText } from '@satorio/machinery/core';
|
|
2
|
+
import { BlobStore } from '@satorio/machinery/ports';
|
|
3
|
+
import { Effect, Layer, Option } from 'effect';
|
|
4
|
+
import { blobStoreUnavailableWithoutConfiguration, makeS3BlobStore, openS3Transport, scopedS3Transport, } from './blob-store.js';
|
|
5
|
+
import { blobStoreSettings, bucketRefusal, configuredBucket, } from './settings.js';
|
|
6
|
+
/**
|
|
7
|
+
* A blob store built from environment variables with the given prefix.
|
|
8
|
+
* With no bucket set it mounts the loud stub and logs the one refusal
|
|
9
|
+
* every later operation repeats, so startup carries on and nothing goes
|
|
10
|
+
* missing in silence. The transport is a parameter, so a test
|
|
11
|
+
* substitutes its own without touching the wiring.
|
|
12
|
+
*/
|
|
13
|
+
export const blobStoreFromEnvironmentLive = (variablePrefix, openTransport) => Layer.unwrapEffect(Effect.gen(function* () {
|
|
14
|
+
const configured = yield* configuredBucket(variablePrefix);
|
|
15
|
+
if (Option.isNone(configured)) {
|
|
16
|
+
const refusal = bucketRefusal(variablePrefix);
|
|
17
|
+
yield* Effect.logError(refusalText(refusal));
|
|
18
|
+
return Layer.succeed(BlobStore, blobStoreUnavailableWithoutConfiguration(refusal));
|
|
19
|
+
}
|
|
20
|
+
return Layer.scoped(BlobStore, Effect.gen(function* () {
|
|
21
|
+
const settings = yield* blobStoreSettings(variablePrefix);
|
|
22
|
+
const transport = yield* scopedS3Transport(() => openTransport(settings));
|
|
23
|
+
return makeS3BlobStore(transport, settings);
|
|
24
|
+
}));
|
|
25
|
+
}));
|
|
26
|
+
/**
|
|
27
|
+
* The blob store on the real AWS SDK transport, reading the variables
|
|
28
|
+
* this prefix names.
|
|
29
|
+
*/
|
|
30
|
+
export const s3BlobStoreLive = (variablePrefix) => blobStoreFromEnvironmentLive(variablePrefix, openS3Transport);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type EnvironmentEntry, type Refusal } from '@satorio/machinery/core';
|
|
2
|
+
import { Config, type Option } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* Builds the environment variable name from the prefix the application
|
|
5
|
+
* gives at mount time. One machinery instance serves any number of buckets
|
|
6
|
+
* in one process this way, with readable names: `MEDIA_BUCKET`,
|
|
7
|
+
* `REPORTS_BUCKET`, and so on.
|
|
8
|
+
*/
|
|
9
|
+
export declare const bucketVariable: (variablePrefix: string) => string;
|
|
10
|
+
/**
|
|
11
|
+
* What the store needs before it can talk to the object store.
|
|
12
|
+
*
|
|
13
|
+
* What the store needs before it can talk to S3: one bucket, and the two
|
|
14
|
+
* variables the AWS SDK reads for itself when nobody names them.
|
|
15
|
+
*/
|
|
16
|
+
export interface BlobStoreSettings {
|
|
17
|
+
readonly bucket: string;
|
|
18
|
+
readonly region: Option.Option<string>;
|
|
19
|
+
readonly endpoint: Option.Option<string>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The three parts a reader meets when the bucket is not set: what
|
|
23
|
+
* happened, why the store cannot go on, and what to do next. The mount
|
|
24
|
+
* desk logs this text and every operation of the loud stub fails with
|
|
25
|
+
* it, so an operator reads one sentence rather than two.
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export declare const bucketRefusal: (variablePrefix: string) => Refusal;
|
|
30
|
+
/**
|
|
31
|
+
* Reads the bucket without refusing, so the mount desk can put up the
|
|
32
|
+
* loud stub instead of failing service startup.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
export declare const configuredBucket: (variablePrefix: string) => Config.Config<Option.Option<string>>;
|
|
37
|
+
/**
|
|
38
|
+
* Every variable this adapter reads, and the one state each of them
|
|
39
|
+
* stands in. `<PREFIX>` is the prefix the application passes at mount
|
|
40
|
+
* time.
|
|
41
|
+
*/
|
|
42
|
+
export declare const ENVIRONMENT: readonly EnvironmentEntry[];
|
|
43
|
+
/**
|
|
44
|
+
* Reads every variable of `ENVIRONMENT` under one prefix. A missing or
|
|
45
|
+
* blank bucket refuses in three parts; region and endpoint stay
|
|
46
|
+
* `Option.none`, and the SDK decides.
|
|
47
|
+
*/
|
|
48
|
+
export declare const blobStoreSettings: (variablePrefix: string) => Config.Config<BlobStoreSettings>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { handedOn, optionalText, refuses, requiredText, } from '@satorio/machinery/core';
|
|
2
|
+
import { Config } from 'effect';
|
|
3
|
+
const BUCKET_SUFFIX = '_BUCKET';
|
|
4
|
+
const REGION = 'AWS_REGION';
|
|
5
|
+
const ENDPOINT = 'AWS_ENDPOINT_URL';
|
|
6
|
+
/**
|
|
7
|
+
* Builds the environment variable name from the prefix the application
|
|
8
|
+
* gives at mount time. One machinery instance serves any number of buckets
|
|
9
|
+
* in one process this way, with readable names: `MEDIA_BUCKET`,
|
|
10
|
+
* `REPORTS_BUCKET`, and so on.
|
|
11
|
+
*/
|
|
12
|
+
export const bucketVariable = (variablePrefix) => `${variablePrefix}${BUCKET_SUFFIX}`;
|
|
13
|
+
/**
|
|
14
|
+
* The three parts a reader meets when the bucket is not set: what
|
|
15
|
+
* happened, why the store cannot go on, and what to do next. The mount
|
|
16
|
+
* desk logs this text and every operation of the loud stub fails with
|
|
17
|
+
* it, so an operator reads one sentence rather than two.
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
export const bucketRefusal = (variablePrefix) => ({
|
|
22
|
+
what: `${bucketVariable(variablePrefix)} is not set, so this object store has no bucket.`,
|
|
23
|
+
why: 'One process serves several buckets, each under its own prefix, so the store cannot guess which one you meant.',
|
|
24
|
+
whatToDo: `Set ${bucketVariable(variablePrefix)} to the bucket name from the AWS console or your .env, then start the service again.`,
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Reads the bucket without refusing, so the mount desk can put up the
|
|
28
|
+
* loud stub instead of failing service startup.
|
|
29
|
+
*
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export const configuredBucket = (variablePrefix) => optionalText(bucketVariable(variablePrefix));
|
|
33
|
+
/**
|
|
34
|
+
* Every variable this adapter reads, and the one state each of them
|
|
35
|
+
* stands in. `<PREFIX>` is the prefix the application passes at mount
|
|
36
|
+
* time.
|
|
37
|
+
*/
|
|
38
|
+
export const ENVIRONMENT = [
|
|
39
|
+
{
|
|
40
|
+
name: BUCKET_SUFFIX,
|
|
41
|
+
prefixed: true,
|
|
42
|
+
what: 'The bucket this store puts objects into and reads them back from.',
|
|
43
|
+
state: refuses(bucketRefusal('<PREFIX>')),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: REGION,
|
|
47
|
+
what: 'The region the S3 client talks to.',
|
|
48
|
+
state: handedOn('the AWS SDK finds its own region from the machine it runs on.'),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: ENDPOINT,
|
|
52
|
+
what: 'A local stand to talk to instead of real S3, such as localstack.',
|
|
53
|
+
state: handedOn('the client goes to the real S3 for the region it found.'),
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
/**
|
|
57
|
+
* Reads every variable of `ENVIRONMENT` under one prefix. A missing or
|
|
58
|
+
* blank bucket refuses in three parts; region and endpoint stay
|
|
59
|
+
* `Option.none`, and the SDK decides.
|
|
60
|
+
*/
|
|
61
|
+
export const blobStoreSettings = (variablePrefix) => Config.all({
|
|
62
|
+
bucket: requiredText(bucketVariable(variablePrefix), bucketRefusal(variablePrefix)),
|
|
63
|
+
region: optionalText(REGION),
|
|
64
|
+
endpoint: optionalText(ENDPOINT),
|
|
65
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { BlobStore, type BlobStoreShape, type StoredBlob } from '@satorio/machinery/ports';
|
|
2
|
+
import { Effect, Layer, Ref } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* What an in-memory blob store holds: every blob it was given, by key. A
|
|
5
|
+
* scenario keeps the `Ref` and reads this map to see what a use case
|
|
6
|
+
* stored.
|
|
7
|
+
*/
|
|
8
|
+
export type BlobsByKey = ReadonlyMap<string, StoredBlob>;
|
|
9
|
+
/**
|
|
10
|
+
* An in-memory blob store over a `Ref` you keep, so a test can seed it
|
|
11
|
+
* before the run and read it after.
|
|
12
|
+
*/
|
|
13
|
+
export declare const makeInMemoryBlobStoreWith: (blobs: Ref.Ref<BlobsByKey>) => BlobStoreShape;
|
|
14
|
+
/**
|
|
15
|
+
* An in-memory blob store over a `Ref` of its own, empty at the start.
|
|
16
|
+
*/
|
|
17
|
+
export declare const makeInMemoryBlobStore: Effect.Effect<BlobStoreShape, never, never>;
|
|
18
|
+
/**
|
|
19
|
+
* The in-memory blob store, mounted.
|
|
20
|
+
*/
|
|
21
|
+
export declare const InMemoryBlobStoreLive: Layer.Layer<BlobStore, never, never>;
|
|
22
|
+
/**
|
|
23
|
+
* A blob store that answers nothing: every operation fails with
|
|
24
|
+
* `BlobStoreUnavailableError`. Mount it to walk the branch a working
|
|
25
|
+
* stand-in never reaches.
|
|
26
|
+
*/
|
|
27
|
+
export declare const BlobStoreUnavailableLive: Layer.Layer<BlobStore>;
|