@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,71 @@
|
|
|
1
|
+
import { QueueMessageSource, QueueMessageSourceUnavailableError, } from '@satorio/machinery/ports';
|
|
2
|
+
import { Effect, Layer, Ref } from 'effect';
|
|
3
|
+
const NOT_MOUNTED = 'queue unavailable (test)';
|
|
4
|
+
/**
|
|
5
|
+
* One raw message, built the way a queue hands it over: an id, the
|
|
6
|
+
* receipt an acknowledgement quotes, and the body.
|
|
7
|
+
*/
|
|
8
|
+
export const messageOf = (messageId, body) => ({
|
|
9
|
+
messageId,
|
|
10
|
+
receiptHandle: `receipt-${messageId}`,
|
|
11
|
+
body,
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* An in-memory message source over a `Ref` you keep, so a test seeds the
|
|
15
|
+
* queue before the run and reads what is left after it. `receive` hands
|
|
16
|
+
* back everything waiting; `acknowledge` takes one message off for good,
|
|
17
|
+
* as a real queue does when the receipt is deleted.
|
|
18
|
+
*/
|
|
19
|
+
export const makeInMemoryQueueMessageSourceWith = (pending) => ({
|
|
20
|
+
receive: Ref.get(pending),
|
|
21
|
+
acknowledge: (receiptHandle) => Ref.update(pending, (current) => current.filter((message) => message.receiptHandle !== receiptHandle)),
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* An in-memory message source over a `Ref` of its own, empty at the
|
|
25
|
+
* start.
|
|
26
|
+
*/
|
|
27
|
+
export const makeInMemoryQueueMessageSource = Effect.gen(function* () {
|
|
28
|
+
const pending = yield* Ref.make([]);
|
|
29
|
+
return makeInMemoryQueueMessageSourceWith(pending);
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* The in-memory message source, mounted.
|
|
33
|
+
*/
|
|
34
|
+
export const InMemoryQueueMessageSourceLive = Layer.effect(QueueMessageSource, makeInMemoryQueueMessageSource);
|
|
35
|
+
/**
|
|
36
|
+
* A message source that answers nothing: every operation fails with
|
|
37
|
+
* `QueueMessageSourceUnavailableError`. Mount it to walk the branch a
|
|
38
|
+
* working stand-in never reaches.
|
|
39
|
+
*/
|
|
40
|
+
export const QueueMessageSourceUnavailableLive = Layer.succeed(QueueMessageSource, {
|
|
41
|
+
receive: Effect.fail(new QueueMessageSourceUnavailableError({
|
|
42
|
+
operation: 'receive',
|
|
43
|
+
cause: NOT_MOUNTED,
|
|
44
|
+
})),
|
|
45
|
+
acknowledge: () => Effect.fail(new QueueMessageSourceUnavailableError({
|
|
46
|
+
operation: 'acknowledge',
|
|
47
|
+
cause: NOT_MOUNTED,
|
|
48
|
+
})),
|
|
49
|
+
});
|
|
50
|
+
/**
|
|
51
|
+
* Builds the scripted source and the reader for what it acknowledged.
|
|
52
|
+
*/
|
|
53
|
+
export const makeScriptedMessageSource = (script) => Effect.gen(function* () {
|
|
54
|
+
const remaining = yield* Ref.make(script);
|
|
55
|
+
const acknowledged = yield* Ref.make([]);
|
|
56
|
+
const source = {
|
|
57
|
+
receive: Ref.modify(remaining, (current) => {
|
|
58
|
+
const [head, ...tail] = current;
|
|
59
|
+
return head === undefined
|
|
60
|
+
? [Effect.never, current]
|
|
61
|
+
: [Effect.succeed(head), tail];
|
|
62
|
+
}).pipe(Effect.flatten),
|
|
63
|
+
acknowledge: (receiptHandle) => Ref.update(acknowledged, (current) => [...current, receiptHandle]),
|
|
64
|
+
};
|
|
65
|
+
return { source, acknowledged: Ref.get(acknowledged) };
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* The scripted source, mounted: each `receive` answers with the next
|
|
69
|
+
* batch of the script.
|
|
70
|
+
*/
|
|
71
|
+
export const scriptedQueueMessageSourceLive = (script) => Layer.effect(QueueMessageSource, Effect.map(makeScriptedMessageSource(script), (queue) => queue.source));
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one test case a summary reads: its full name, and the result the
|
|
3
|
+
* runner recorded for it. Declared here rather than imported from the
|
|
4
|
+
* runner, because this file is read while a test-runner configuration is
|
|
5
|
+
* built and the runner's own runtime does not exist yet.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export interface ReportedTestCase {
|
|
10
|
+
readonly fullName: string;
|
|
11
|
+
readonly result: () => {
|
|
12
|
+
readonly state: string;
|
|
13
|
+
readonly errors?: readonly {
|
|
14
|
+
readonly message?: string;
|
|
15
|
+
}[];
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The one test module a summary reads: the file it came from and the
|
|
20
|
+
* test cases inside it.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export interface ReportedTestModule {
|
|
25
|
+
readonly moduleId: string;
|
|
26
|
+
readonly children: {
|
|
27
|
+
readonly allTests: () => Iterable<ReportedTestCase>;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* How the quiet reporter names itself and where it writes. `label` opens
|
|
32
|
+
* the summary line, so a run of several packages says which one spoke.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
export interface QuietReporterOptions {
|
|
37
|
+
readonly label?: string;
|
|
38
|
+
readonly write?: (line: string) => void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A test-runner reporter that prints one summary line for a whole run:
|
|
42
|
+
* how many files, how many tests, how long it took. A run where nothing
|
|
43
|
+
* failed says nothing else. A run with a failure prints each failed test
|
|
44
|
+
* by its full name with the message the runner recorded, so the reason
|
|
45
|
+
* is in the same output as the count.
|
|
46
|
+
*
|
|
47
|
+
* It forwards whatever a suite printed on purpose. A guard that prints
|
|
48
|
+
* the number of files it read is a floor somebody watches move, and a
|
|
49
|
+
* reporter that swallows it hides the one line that matters. What it
|
|
50
|
+
* drops is the per-test tick list, which is the part that grows with the
|
|
51
|
+
* suite and says nothing a green summary does not.
|
|
52
|
+
*/
|
|
53
|
+
export declare class QuietReporter {
|
|
54
|
+
private readonly label;
|
|
55
|
+
private readonly write;
|
|
56
|
+
private startedAt;
|
|
57
|
+
private root;
|
|
58
|
+
constructor(options?: QuietReporterOptions);
|
|
59
|
+
onInit(context: {
|
|
60
|
+
readonly config?: {
|
|
61
|
+
readonly root?: string;
|
|
62
|
+
};
|
|
63
|
+
}): void;
|
|
64
|
+
onTestRunStart(): void;
|
|
65
|
+
onUserConsoleLog(log: {
|
|
66
|
+
readonly content: string;
|
|
67
|
+
}): void;
|
|
68
|
+
onTestRunEnd(testModules: readonly ReportedTestModule[], unhandledErrors?: readonly {
|
|
69
|
+
readonly message?: string;
|
|
70
|
+
}[]): void;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether the runner should print everything. A run asks for it with
|
|
74
|
+
* `VITEST_VERBOSE=1`; unset, empty, `0`, `false` and `no` all mean no.
|
|
75
|
+
*/
|
|
76
|
+
export declare const verboseOutputWanted: (environment?: Readonly<Record<string, string | undefined>>) => boolean;
|
|
77
|
+
/**
|
|
78
|
+
* The `reporters` a test-runner configuration takes. By default it is
|
|
79
|
+
* the quiet reporter, so a run says how many files and tests passed and
|
|
80
|
+
* nothing else; `VITEST_VERBOSE=1` hands the run back to the runner's
|
|
81
|
+
* own reporter, whole. The array is mutable on purpose: the runner's
|
|
82
|
+
* own configuration type takes no readonly array.
|
|
83
|
+
*/
|
|
84
|
+
export declare const quietReporters: (options?: QuietReporterOptions & {
|
|
85
|
+
readonly verbose?: boolean;
|
|
86
|
+
}) => ("default" | QuietReporter)[];
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const DEFAULT_LABEL = 'tests';
|
|
2
|
+
const secondsOf = (millis) => `${(millis / 1000).toFixed(1)}s`;
|
|
3
|
+
const relativeTo = (root, moduleId) => moduleId.startsWith(root) ? moduleId.slice(root.length + 1) : moduleId;
|
|
4
|
+
/**
|
|
5
|
+
* A test-runner reporter that prints one summary line for a whole run:
|
|
6
|
+
* how many files, how many tests, how long it took. A run where nothing
|
|
7
|
+
* failed says nothing else. A run with a failure prints each failed test
|
|
8
|
+
* by its full name with the message the runner recorded, so the reason
|
|
9
|
+
* is in the same output as the count.
|
|
10
|
+
*
|
|
11
|
+
* It forwards whatever a suite printed on purpose. A guard that prints
|
|
12
|
+
* the number of files it read is a floor somebody watches move, and a
|
|
13
|
+
* reporter that swallows it hides the one line that matters. What it
|
|
14
|
+
* drops is the per-test tick list, which is the part that grows with the
|
|
15
|
+
* suite and says nothing a green summary does not.
|
|
16
|
+
*/
|
|
17
|
+
// @effect-diagnostics effect/globalDate:off
|
|
18
|
+
export class QuietReporter {
|
|
19
|
+
label;
|
|
20
|
+
write;
|
|
21
|
+
startedAt = Date.now();
|
|
22
|
+
root = '';
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.label = options.label ?? DEFAULT_LABEL;
|
|
25
|
+
this.write = options.write ?? ((line) => process.stdout.write(`${line}\n`));
|
|
26
|
+
}
|
|
27
|
+
onInit(context) {
|
|
28
|
+
this.root = context.config?.root ?? '';
|
|
29
|
+
}
|
|
30
|
+
onTestRunStart() {
|
|
31
|
+
this.startedAt = Date.now();
|
|
32
|
+
}
|
|
33
|
+
onUserConsoleLog(log) {
|
|
34
|
+
this.write(log.content.replace(/\n$/, ''));
|
|
35
|
+
}
|
|
36
|
+
onTestRunEnd(testModules, unhandledErrors = []) {
|
|
37
|
+
const failed = [];
|
|
38
|
+
let tests = 0;
|
|
39
|
+
let failures = 0;
|
|
40
|
+
for (const testModule of testModules) {
|
|
41
|
+
for (const testCase of testModule.children.allTests()) {
|
|
42
|
+
tests++;
|
|
43
|
+
const result = testCase.result();
|
|
44
|
+
if (result.state !== 'failed')
|
|
45
|
+
continue;
|
|
46
|
+
failures++;
|
|
47
|
+
failed.push(`${relativeTo(this.root, testModule.moduleId)} > ${testCase.fullName}`);
|
|
48
|
+
for (const error of result.errors ?? []) {
|
|
49
|
+
failed.push(` ${error.message ?? 'no message'}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const outcome = failures === 0 && unhandledErrors.length === 0
|
|
54
|
+
? 'green'
|
|
55
|
+
: `${failures} failed`;
|
|
56
|
+
this.write(`${this.label}: ${testModules.length} files, ${tests} tests, ` +
|
|
57
|
+
`${secondsOf(Date.now() - this.startedAt)} — ${outcome}`);
|
|
58
|
+
for (const line of failed)
|
|
59
|
+
this.write(line);
|
|
60
|
+
for (const error of unhandledErrors) {
|
|
61
|
+
this.write(` unhandled: ${error.message ?? 'no message'}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// @effect-diagnostics effect/globalDate:error
|
|
66
|
+
const OFF = new Set(['', '0', 'false', 'no']);
|
|
67
|
+
/**
|
|
68
|
+
* Whether the runner should print everything. A run asks for it with
|
|
69
|
+
* `VITEST_VERBOSE=1`; unset, empty, `0`, `false` and `no` all mean no.
|
|
70
|
+
*/
|
|
71
|
+
export const verboseOutputWanted = (environment = process.env) => !OFF.has((environment.VITEST_VERBOSE ?? '').toLowerCase());
|
|
72
|
+
/**
|
|
73
|
+
* The `reporters` a test-runner configuration takes. By default it is
|
|
74
|
+
* the quiet reporter, so a run says how many files and tests passed and
|
|
75
|
+
* nothing else; `VITEST_VERBOSE=1` hands the run back to the runner's
|
|
76
|
+
* own reporter, whole. The array is mutable on purpose: the runner's
|
|
77
|
+
* own configuration type takes no readonly array.
|
|
78
|
+
*/
|
|
79
|
+
export const quietReporters = (options = {}) => (options.verbose ?? verboseOutputWanted())
|
|
80
|
+
? ['default']
|
|
81
|
+
: [new QuietReporter(options)];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ReadinessProbe, type ReadinessProbeShape } from '@satorio/machinery/ports';
|
|
2
|
+
import { Layer } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* The reason an unavailable probe reports, so a test asserts on a
|
|
5
|
+
* sentence rather than on the shape of a failure alone.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export declare const READINESS_REASON_IN_A_TEST = "database did not answer (test)";
|
|
10
|
+
/**
|
|
11
|
+
* A probe that always says the database answered. It is the only thing
|
|
12
|
+
* an in-memory probe can honestly say: there is no database behind it to
|
|
13
|
+
* fall silent.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export declare const makeInMemoryReadinessProbe: ReadinessProbeShape;
|
|
18
|
+
/**
|
|
19
|
+
* The in-memory probe, mounted.
|
|
20
|
+
*/
|
|
21
|
+
export declare const InMemoryReadinessProbeLive: Layer.Layer<ReadinessProbe>;
|
|
22
|
+
/**
|
|
23
|
+
* A probe whose database never answers. Mount it to walk the branch a
|
|
24
|
+
* working stand-in never reaches — the one a readiness endpoint turns
|
|
25
|
+
* into "not ready".
|
|
26
|
+
*/
|
|
27
|
+
export declare const ReadinessProbeUnavailableLive: Layer.Layer<ReadinessProbe>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DatabaseDidNotAnswerError, ReadinessProbe, } from '@satorio/machinery/ports';
|
|
2
|
+
import { Effect, Layer } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* The reason an unavailable probe reports, so a test asserts on a
|
|
5
|
+
* sentence rather than on the shape of a failure alone.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export const READINESS_REASON_IN_A_TEST = 'database did not answer (test)';
|
|
10
|
+
/**
|
|
11
|
+
* A probe that always says the database answered. It is the only thing
|
|
12
|
+
* an in-memory probe can honestly say: there is no database behind it to
|
|
13
|
+
* fall silent.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export const makeInMemoryReadinessProbe = {
|
|
18
|
+
verifyDatabaseAnswers: Effect.void,
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The in-memory probe, mounted.
|
|
22
|
+
*/
|
|
23
|
+
export const InMemoryReadinessProbeLive = Layer.succeed(ReadinessProbe, makeInMemoryReadinessProbe);
|
|
24
|
+
/**
|
|
25
|
+
* A probe whose database never answers. Mount it to walk the branch a
|
|
26
|
+
* working stand-in never reaches — the one a readiness endpoint turns
|
|
27
|
+
* into "not ready".
|
|
28
|
+
*/
|
|
29
|
+
export const ReadinessProbeUnavailableLive = Layer.succeed(ReadinessProbe, {
|
|
30
|
+
verifyDatabaseAnswers: new DatabaseDidNotAnswerError({
|
|
31
|
+
reason: READINESS_REASON_IN_A_TEST,
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Cause, Effect } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* One step in a scenario. `at` is the step's technical moment — it becomes
|
|
4
|
+
* a stored row's `processed_at`, `recorded_at`, or `occurred_at`. The
|
|
5
|
+
* business date, such as a movement's day or a loan's day, lives in
|
|
6
|
+
* `command` instead.
|
|
7
|
+
*/
|
|
8
|
+
export interface ScenarioStep<Command> {
|
|
9
|
+
readonly at: string;
|
|
10
|
+
readonly what: string;
|
|
11
|
+
readonly command: Command;
|
|
12
|
+
readonly firstPass: string;
|
|
13
|
+
readonly onReplay: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A story told as data: the steps a world walks through, and the numbers
|
|
17
|
+
* that world ends on. `context` carries what every step of that scenario
|
|
18
|
+
* shares and no step states — the currency of an account, the account
|
|
19
|
+
* itself, the tenant. A snapshot then reads those from the scenario
|
|
20
|
+
* rather than deriving them from whatever the first row happened to
|
|
21
|
+
* hold. A scenario that shares nothing leaves it out.
|
|
22
|
+
*/
|
|
23
|
+
export interface Scenario<Command, Snapshot, Context = never> {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly story: string;
|
|
26
|
+
readonly steps: readonly ScenarioStep<Command>[];
|
|
27
|
+
readonly expectedSnapshot: Snapshot;
|
|
28
|
+
readonly context?: Context;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Reads the cross-cutting data of a scenario that declares some. A
|
|
32
|
+
* scenario whose type promises it and whose data leaves it out dies
|
|
33
|
+
* here, naming itself, rather than handing `undefined` on to a snapshot
|
|
34
|
+
* that then reports a wrong number.
|
|
35
|
+
*/
|
|
36
|
+
export declare const contextOf: <Command, Snapshot, Context>(scenario: Scenario<Command, Snapshot, Context>) => Context;
|
|
37
|
+
/**
|
|
38
|
+
* What a service gives the scenario player: how to run a step, and how to
|
|
39
|
+
* snapshot what followed.
|
|
40
|
+
*
|
|
41
|
+
* What a service gives the player: how to run a command and name its result,
|
|
42
|
+
* and how to take a snapshot of the numbers after the run. The failure channel
|
|
43
|
+
* is deliberately wide — a label only NAMES a failure, it never parses it.
|
|
44
|
+
*/
|
|
45
|
+
export interface ScenarioService<Command, Snapshot, R, Context = never> {
|
|
46
|
+
readonly perform: (command: Command) => Effect.Effect<string, unknown, R>;
|
|
47
|
+
readonly snapshot: (scenario: Scenario<Command, Snapshot, Context>) => Effect.Effect<Snapshot, never, R>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* What one run of a scenario leaves behind: the label of every step, and the
|
|
51
|
+
* snapshot the run ended on.
|
|
52
|
+
*/
|
|
53
|
+
export interface ScenarioPlayback<Snapshot> {
|
|
54
|
+
readonly labels: readonly string[];
|
|
55
|
+
readonly snapshot: Snapshot;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The labels a scenario expects the first time it runs, in order.
|
|
59
|
+
*/
|
|
60
|
+
export declare const firstPassLabelsOf: <C, S, X>(scenario: Scenario<C, S, X>) => readonly string[];
|
|
61
|
+
/**
|
|
62
|
+
* The labels a scenario expects when the same steps run a second time, in
|
|
63
|
+
* order.
|
|
64
|
+
*/
|
|
65
|
+
export declare const replayLabelsOf: <C, S, X>(scenario: Scenario<C, S, X>) => readonly string[];
|
|
66
|
+
/**
|
|
67
|
+
* Labels a failure, so a scenario reads the same on every run.
|
|
68
|
+
*
|
|
69
|
+
* Labels a failure: a domain failure by its named reason, a storage conflict
|
|
70
|
+
* by its tag, a defect on its own so it stands out from both.
|
|
71
|
+
*/
|
|
72
|
+
export declare const labelOfFailure: (cause: Cause.Cause<unknown>) => string;
|
|
73
|
+
/**
|
|
74
|
+
* Runs one step and labels it: what it returned, or the name of the failure it
|
|
75
|
+
* met.
|
|
76
|
+
*/
|
|
77
|
+
export declare const labelOf: <R>(program: Effect.Effect<string, unknown, R>) => Effect.Effect<string, never, R>;
|
|
78
|
+
/**
|
|
79
|
+
* Fails loudly when a scenario's steps are not in strictly increasing
|
|
80
|
+
* time order — that is a broken scenario, not a run outcome.
|
|
81
|
+
*/
|
|
82
|
+
export declare const ensureStepsAreInOrder: <C, S, X>(scenario: Scenario<C, S, X>) => Effect.Effect<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Plays a scenario: each step runs at its own moment, and the
|
|
85
|
+
* snapshot is taken at the last step's moment. Replaying the same
|
|
86
|
+
* scenario against the same world should produce `onReplay` labels
|
|
87
|
+
* and leave the snapshot unchanged — that is how claim-once holds.
|
|
88
|
+
*/
|
|
89
|
+
export declare const playScenario: <C, S, R, X>(scenario: Scenario<C, S, X>, service: ScenarioService<C, S, R, X>) => Effect.Effect<ScenarioPlayback<S>, never, R>;
|
|
90
|
+
/**
|
|
91
|
+
* Returns the moments of the chosen steps as ISO strings, in the
|
|
92
|
+
* format a database round-trip returns them.
|
|
93
|
+
*/
|
|
94
|
+
export declare const momentsOfSteps: <C, S, X>(scenario: Scenario<C, S, X>, chosen: (step: ScenarioStep<C>) => boolean) => readonly string[];
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Cause, DateTime, Effect, Exit } from 'effect';
|
|
2
|
+
import { atTheMoment } from './clock.js';
|
|
3
|
+
/**
|
|
4
|
+
* Reads the cross-cutting data of a scenario that declares some. A
|
|
5
|
+
* scenario whose type promises it and whose data leaves it out dies
|
|
6
|
+
* here, naming itself, rather than handing `undefined` on to a snapshot
|
|
7
|
+
* that then reports a wrong number.
|
|
8
|
+
*/
|
|
9
|
+
export const contextOf = (scenario) => {
|
|
10
|
+
if (scenario.context === undefined) {
|
|
11
|
+
throw new Error(`Scenario "${scenario.name}" declares cross-cutting data and ` +
|
|
12
|
+
`carries none: give it a context, or drop the type it promises.`);
|
|
13
|
+
}
|
|
14
|
+
return scenario.context;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The labels a scenario expects the first time it runs, in order.
|
|
18
|
+
*/
|
|
19
|
+
export const firstPassLabelsOf = (scenario) => scenario.steps.map((step) => step.firstPass);
|
|
20
|
+
/**
|
|
21
|
+
* The labels a scenario expects when the same steps run a second time, in
|
|
22
|
+
* order.
|
|
23
|
+
*/
|
|
24
|
+
export const replayLabelsOf = (scenario) => scenario.steps.map((step) => step.onReplay);
|
|
25
|
+
/**
|
|
26
|
+
* Labels a failure, so a scenario reads the same on every run.
|
|
27
|
+
*
|
|
28
|
+
* Labels a failure: a domain failure by its named reason, a storage conflict
|
|
29
|
+
* by its tag, a defect on its own so it stands out from both.
|
|
30
|
+
*/
|
|
31
|
+
export const labelOfFailure = (cause) => {
|
|
32
|
+
const failure = Cause.failureOption(cause);
|
|
33
|
+
if (failure._tag === 'None')
|
|
34
|
+
return `DEFECT: ${Cause.pretty(cause)}`;
|
|
35
|
+
const tagged = failure.value;
|
|
36
|
+
return tagged.reason ?? tagged._tag ?? String(failure.value);
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Runs one step and labels it: what it returned, or the name of the failure it
|
|
40
|
+
* met.
|
|
41
|
+
*/
|
|
42
|
+
export const labelOf = (program) => Effect.exit(program).pipe(Effect.map((exit) => Exit.isSuccess(exit) ? exit.value : labelOfFailure(exit.cause)));
|
|
43
|
+
const millisOf = (isoTimestamp) => DateTime.toEpochMillis(DateTime.unsafeMake(isoTimestamp));
|
|
44
|
+
/**
|
|
45
|
+
* Fails loudly when a scenario's steps are not in strictly increasing
|
|
46
|
+
* time order — that is a broken scenario, not a run outcome.
|
|
47
|
+
*/
|
|
48
|
+
export const ensureStepsAreInOrder = (scenario) => Effect.forEach(scenario.steps, (step, index) => {
|
|
49
|
+
const previous = scenario.steps[index - 1];
|
|
50
|
+
return previous !== undefined &&
|
|
51
|
+
millisOf(step.at) <= millisOf(previous.at)
|
|
52
|
+
? Effect.die(new Error(`Scenario "${scenario.name}": step ${index + 1} ` +
|
|
53
|
+
`(${step.at}) is not later than step ${index} ` +
|
|
54
|
+
`(${previous.at})`))
|
|
55
|
+
: Effect.void;
|
|
56
|
+
}, { discard: true });
|
|
57
|
+
const lastMomentOf = (scenario) => {
|
|
58
|
+
const last = scenario.steps.at(-1);
|
|
59
|
+
return last === undefined
|
|
60
|
+
? DateTime.formatIso(DateTime.unsafeMake(0))
|
|
61
|
+
: last.at;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Plays a scenario: each step runs at its own moment, and the
|
|
65
|
+
* snapshot is taken at the last step's moment. Replaying the same
|
|
66
|
+
* scenario against the same world should produce `onReplay` labels
|
|
67
|
+
* and leave the snapshot unchanged — that is how claim-once holds.
|
|
68
|
+
*/
|
|
69
|
+
export const playScenario = (scenario, service) => Effect.gen(function* () {
|
|
70
|
+
yield* ensureStepsAreInOrder(scenario);
|
|
71
|
+
const labels = yield* Effect.forEach(scenario.steps, (step) => atTheMoment(step.at)(labelOf(service.perform(step.command))));
|
|
72
|
+
const snapshot = yield* atTheMoment(lastMomentOf(scenario))(service.snapshot(scenario));
|
|
73
|
+
return { labels, snapshot };
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* Returns the moments of the chosen steps as ISO strings, in the
|
|
77
|
+
* format a database round-trip returns them.
|
|
78
|
+
*/
|
|
79
|
+
export const momentsOfSteps = (scenario, chosen) => scenario.steps
|
|
80
|
+
.filter(chosen)
|
|
81
|
+
.map((step) => DateTime.formatIso(DateTime.unsafeMake(step.at)));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { TransactionBoundary, type TransactionBoundaryShape } from '@satorio/machinery/ports';
|
|
2
|
+
import { Effect, Layer, Ref } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* One participant in an in-memory transaction: it can capture its own
|
|
5
|
+
* state now and hand back the effect that puts it back.
|
|
6
|
+
*/
|
|
7
|
+
export interface TransactionalState {
|
|
8
|
+
readonly capture: Effect.Effect<Effect.Effect<void>>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Makes a `Ref` a participant, so an in-memory boundary can put it back
|
|
12
|
+
* where it was when the work fails.
|
|
13
|
+
*/
|
|
14
|
+
export declare const stateOfRef: <A>(ref: Ref.Ref<A>) => TransactionalState;
|
|
15
|
+
/**
|
|
16
|
+
* Builds an in-memory transaction boundary: it captures each
|
|
17
|
+
* participant's state on entry and restores it if the work fails.
|
|
18
|
+
*/
|
|
19
|
+
export declare const makeInMemoryTransactionBoundary: (participants: readonly TransactionalState[]) => TransactionBoundaryShape;
|
|
20
|
+
/**
|
|
21
|
+
* The in-memory transaction boundary, mounted over the participants you
|
|
22
|
+
* name.
|
|
23
|
+
*/
|
|
24
|
+
export declare const inMemoryTransactionBoundaryLive: (participants: readonly TransactionalState[]) => Layer.Layer<TransactionBoundary>;
|
|
25
|
+
/**
|
|
26
|
+
* A transaction boundary that opens nothing: the work never runs and the
|
|
27
|
+
* caller reads the refusal. Mount it to walk the branch a working
|
|
28
|
+
* stand-in never reaches.
|
|
29
|
+
*/
|
|
30
|
+
export declare const TransactionBoundaryUnavailableLive: Layer.Layer<TransactionBoundary>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { TransactionBoundary, TransactionBoundaryUnavailableError, } from '@satorio/machinery/ports';
|
|
2
|
+
import { Effect, Layer, Ref } from 'effect';
|
|
3
|
+
const NOT_MOUNTED = 'transaction boundary unavailable (test)';
|
|
4
|
+
/**
|
|
5
|
+
* Makes a `Ref` a participant, so an in-memory boundary can put it back
|
|
6
|
+
* where it was when the work fails.
|
|
7
|
+
*/
|
|
8
|
+
export const stateOfRef = (ref) => ({
|
|
9
|
+
capture: Ref.get(ref).pipe(Effect.map((valueAtEntry) => Ref.set(ref, valueAtEntry))),
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Builds an in-memory transaction boundary: it captures each
|
|
13
|
+
* participant's state on entry and restores it if the work fails.
|
|
14
|
+
*/
|
|
15
|
+
export const makeInMemoryTransactionBoundary = (participants) => ({
|
|
16
|
+
inTransaction: (work) => Effect.gen(function* () {
|
|
17
|
+
const restores = yield* Effect.forEach(participants, (state) => state.capture);
|
|
18
|
+
return yield* work.pipe(Effect.tapError(() => Effect.forEach(restores, (restore) => restore, { discard: true })));
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* The in-memory transaction boundary, mounted over the participants you
|
|
23
|
+
* name.
|
|
24
|
+
*/
|
|
25
|
+
export const inMemoryTransactionBoundaryLive = (participants) => Layer.succeed(TransactionBoundary, makeInMemoryTransactionBoundary(participants));
|
|
26
|
+
/**
|
|
27
|
+
* A transaction boundary that opens nothing: the work never runs and the
|
|
28
|
+
* caller reads the refusal. Mount it to walk the branch a working
|
|
29
|
+
* stand-in never reaches.
|
|
30
|
+
*/
|
|
31
|
+
export const TransactionBoundaryUnavailableLive = Layer.succeed(TransactionBoundary, {
|
|
32
|
+
inTransaction: () => new TransactionBoundaryUnavailableError({
|
|
33
|
+
operation: 'inTransaction',
|
|
34
|
+
cause: NOT_MOUNTED,
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Extracts a use case's typed failure (Fail) through `Effect.exit`
|
|
4
|
+
* and `Cause`, rather than by matching an error message string.
|
|
5
|
+
* Fails loudly if the effect succeeded, or if it died or was
|
|
6
|
+
* interrupted instead of producing a typed Fail.
|
|
7
|
+
*/
|
|
8
|
+
export declare const typedFailureOf: <A, E>(effect: Effect.Effect<A, E>) => Effect.Effect<E>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Cause, Effect, Exit, Option } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Extracts a use case's typed failure (Fail) through `Effect.exit`
|
|
4
|
+
* and `Cause`, rather than by matching an error message string.
|
|
5
|
+
* Fails loudly if the effect succeeded, or if it died or was
|
|
6
|
+
* interrupted instead of producing a typed Fail.
|
|
7
|
+
*/
|
|
8
|
+
export const typedFailureOf = (effect) => Effect.exit(effect).pipe(Effect.flatMap((exit) => Exit.isSuccess(exit)
|
|
9
|
+
? Effect.die(new Error(`expected a typed failure — the use case succeeded ` +
|
|
10
|
+
`instead: ${JSON.stringify(exit.value)}`))
|
|
11
|
+
: Cause.failureOption(exit.cause).pipe(Option.match({
|
|
12
|
+
onNone: () => Effect.die(new Error(`expected a typed failure (Fail) — the Cause ` +
|
|
13
|
+
`is not a Fail: ${Cause.pretty(exit.cause)}`)),
|
|
14
|
+
onSome: Effect.succeed,
|
|
15
|
+
}))));
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Either, Order, Schema } from 'effect';
|
|
2
|
+
import { NotACalendarDayError } from './failures.js';
|
|
3
|
+
declare const calendarDayBrand: unique symbol;
|
|
4
|
+
/**
|
|
5
|
+
* One day of the calendar, with no time and no zone in it. The value
|
|
6
|
+
* carries a brand, so a plain string never passes for a day until it has
|
|
7
|
+
* been read by one of the two readers below.
|
|
8
|
+
*/
|
|
9
|
+
export type CalendarDay = string & {
|
|
10
|
+
readonly [calendarDayBrand]: 'CalendarDay';
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The day named by parts a real moment already produced. This is the one
|
|
14
|
+
* place a calendar day is built, so a value of that type never comes
|
|
15
|
+
* from anywhere else.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export declare const dayOfDateParts: (year: number, month: number, day: number) => CalendarDay;
|
|
20
|
+
/**
|
|
21
|
+
* Reads the form a person writes a day in: `19.03.2026`. A text of any
|
|
22
|
+
* other shape, a month outside the twelve, or a day the month does not
|
|
23
|
+
* have all come back as a refusal that names which rule was broken.
|
|
24
|
+
*/
|
|
25
|
+
export declare const calendarDayFromHumanForm: (text: string) => Either.Either<CalendarDay, NotACalendarDayError>;
|
|
26
|
+
/**
|
|
27
|
+
* Reads the form a machine writes a day in: `2026-03-19`. It refuses
|
|
28
|
+
* exactly what the human reader refuses, for exactly the same reasons.
|
|
29
|
+
*/
|
|
30
|
+
export declare const calendarDayFromIsoForm: (text: string) => Either.Either<CalendarDay, NotACalendarDayError>;
|
|
31
|
+
/**
|
|
32
|
+
* Writes a day the way a person reads it: `19.03.2026`.
|
|
33
|
+
*/
|
|
34
|
+
export declare const dayInHumanForm: (day: CalendarDay) => string;
|
|
35
|
+
/**
|
|
36
|
+
* Writes a day the way a machine reads it: `2026-03-19`. This is the
|
|
37
|
+
* one seam between a day and its text, so a change of how a day is held
|
|
38
|
+
* inside touches this function and nothing a consumer wrote.
|
|
39
|
+
*/
|
|
40
|
+
export declare const dayInIsoForm: (day: CalendarDay) => string;
|
|
41
|
+
/**
|
|
42
|
+
* Puts two days in calendar order, earliest first.
|
|
43
|
+
*/
|
|
44
|
+
export declare const CalendarDayOrder: Order.Order<CalendarDay>;
|
|
45
|
+
/**
|
|
46
|
+
* Counts whole days from the first day to the second. The count runs
|
|
47
|
+
* negative when the second day stands earlier.
|
|
48
|
+
*/
|
|
49
|
+
export declare const daysBetween: (earlier: CalendarDay, later: CalendarDay) => number;
|
|
50
|
+
/**
|
|
51
|
+
* Moves a day by whole days. A negative count moves it back.
|
|
52
|
+
*/
|
|
53
|
+
export declare const addDays: (day: CalendarDay, days: number) => CalendarDay;
|
|
54
|
+
/**
|
|
55
|
+
* Moves a day by whole months, landing on the last day of a month too short to
|
|
56
|
+
* hold it.
|
|
57
|
+
*
|
|
58
|
+
* Moves a day by whole months, and lands on the last day of the month it
|
|
59
|
+
* arrives in when that month is too short to hold the same number.
|
|
60
|
+
*/
|
|
61
|
+
export declare const addMonths: (day: CalendarDay, months: number) => CalendarDay;
|
|
62
|
+
/**
|
|
63
|
+
* The last day of the month the given day falls in. Ask the calendar
|
|
64
|
+
* rather than a table of month lengths: February in a leap year answers
|
|
65
|
+
* correctly here because the runtime's own calendar answers correctly.
|
|
66
|
+
*/
|
|
67
|
+
export declare const lastDayOfTheMonthOf: (day: CalendarDay) => CalendarDay;
|
|
68
|
+
/**
|
|
69
|
+
* The first day of the month the given day falls in. Every month starts
|
|
70
|
+
* on its first, so this one needs no calendar; it stays here beside its
|
|
71
|
+
* twin, and `monthOf` is the name a consumer reaches for.
|
|
72
|
+
*
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
export declare const firstDayOfTheMonthOf: (day: CalendarDay) => CalendarDay;
|
|
76
|
+
/**
|
|
77
|
+
* Reads a day off the wire from the form a person writes, and writes it
|
|
78
|
+
* back in that same form. A text this schema refuses carries the whole
|
|
79
|
+
* three-part refusal into the parse error, so the reader of a failed
|
|
80
|
+
* command sees what to do next rather than a bare type name.
|
|
81
|
+
*/
|
|
82
|
+
export declare const CalendarDayFromHumanString: Schema.Schema<CalendarDay, string>;
|
|
83
|
+
export {};
|