@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,112 @@
|
|
|
1
|
+
import { refusalText } from '@satorio/machinery/core';
|
|
2
|
+
import { DateTime, Either, Order, ParseResult, Schema } from 'effect';
|
|
3
|
+
import { NotACalendarDayError } from './failures.js';
|
|
4
|
+
const HUMAN_FORM = /^(\d{2})\.(\d{2})\.(\d{4})$/;
|
|
5
|
+
const ISO_FORM = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
6
|
+
const MILLISECONDS_IN_A_DAY = 86_400_000;
|
|
7
|
+
const FIRST_MONTH = 1;
|
|
8
|
+
const LAST_MONTH = 12;
|
|
9
|
+
const padded = (value, width) => String(value).padStart(width, '0');
|
|
10
|
+
const isoTextOf = (year, month, day) => `${padded(year, 4)}-${padded(month, 2)}-${padded(day, 2)}`;
|
|
11
|
+
const momentOf = (day) => DateTime.unsafeMake(`${day}T00:00:00.000Z`);
|
|
12
|
+
const dayOfMomentUtc = (moment) => {
|
|
13
|
+
const parts = DateTime.toPartsUtc(moment);
|
|
14
|
+
return dayOfDateParts(parts.year, parts.month, parts.day);
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The day named by parts a real moment already produced. This is the one
|
|
18
|
+
* place a calendar day is built, so a value of that type never comes
|
|
19
|
+
* from anywhere else.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export const dayOfDateParts = (year, month, day) => isoTextOf(year, month, day);
|
|
24
|
+
const refuse = (text, reason) => Either.left(new NotACalendarDayError({ text, reason }));
|
|
25
|
+
const dayFromNumbers = (text, year, month, day) => {
|
|
26
|
+
if (month < FIRST_MONTH || month > LAST_MONTH) {
|
|
27
|
+
return refuse(text, 'month out of range');
|
|
28
|
+
}
|
|
29
|
+
const built = DateTime.unsafeMake({ year, month, day });
|
|
30
|
+
const parts = DateTime.toPartsUtc(built);
|
|
31
|
+
const same = parts.year === year && parts.month === month && parts.day === day;
|
|
32
|
+
return same
|
|
33
|
+
? Either.right(dayOfMomentUtc(built))
|
|
34
|
+
: refuse(text, 'day out of range for that month');
|
|
35
|
+
};
|
|
36
|
+
const dayFromMatch = (text, matched, order) => {
|
|
37
|
+
if (matched === null)
|
|
38
|
+
return refuse(text, 'not the calendar-day form');
|
|
39
|
+
const [yearAt, monthAt, dayAt] = order;
|
|
40
|
+
return dayFromNumbers(text, Number(matched[yearAt]), Number(matched[monthAt]), Number(matched[dayAt]));
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Reads the form a person writes a day in: `19.03.2026`. A text of any
|
|
44
|
+
* other shape, a month outside the twelve, or a day the month does not
|
|
45
|
+
* have all come back as a refusal that names which rule was broken.
|
|
46
|
+
*/
|
|
47
|
+
export const calendarDayFromHumanForm = (text) => dayFromMatch(text, HUMAN_FORM.exec(text), [3, 2, 1]);
|
|
48
|
+
/**
|
|
49
|
+
* Reads the form a machine writes a day in: `2026-03-19`. It refuses
|
|
50
|
+
* exactly what the human reader refuses, for exactly the same reasons.
|
|
51
|
+
*/
|
|
52
|
+
export const calendarDayFromIsoForm = (text) => dayFromMatch(text, ISO_FORM.exec(text), [1, 2, 3]);
|
|
53
|
+
/**
|
|
54
|
+
* Writes a day the way a person reads it: `19.03.2026`.
|
|
55
|
+
*/
|
|
56
|
+
export const dayInHumanForm = (day) => `${day.slice(8, 10)}.${day.slice(5, 7)}.${day.slice(0, 4)}`;
|
|
57
|
+
/**
|
|
58
|
+
* Writes a day the way a machine reads it: `2026-03-19`. This is the
|
|
59
|
+
* one seam between a day and its text, so a change of how a day is held
|
|
60
|
+
* inside touches this function and nothing a consumer wrote.
|
|
61
|
+
*/
|
|
62
|
+
export const dayInIsoForm = (day) => day;
|
|
63
|
+
/**
|
|
64
|
+
* Puts two days in calendar order, earliest first.
|
|
65
|
+
*/
|
|
66
|
+
export const CalendarDayOrder = Order.string;
|
|
67
|
+
/**
|
|
68
|
+
* Counts whole days from the first day to the second. The count runs
|
|
69
|
+
* negative when the second day stands earlier.
|
|
70
|
+
*/
|
|
71
|
+
export const daysBetween = (earlier, later) => DateTime.distance(momentOf(earlier), momentOf(later)) / MILLISECONDS_IN_A_DAY;
|
|
72
|
+
/**
|
|
73
|
+
* Moves a day by whole days. A negative count moves it back.
|
|
74
|
+
*/
|
|
75
|
+
export const addDays = (day, days) => dayOfMomentUtc(DateTime.add(momentOf(day), { days }));
|
|
76
|
+
/**
|
|
77
|
+
* Moves a day by whole months, landing on the last day of a month too short to
|
|
78
|
+
* hold it.
|
|
79
|
+
*
|
|
80
|
+
* Moves a day by whole months, and lands on the last day of the month it
|
|
81
|
+
* arrives in when that month is too short to hold the same number.
|
|
82
|
+
*/
|
|
83
|
+
export const addMonths = (day, months) => dayOfMomentUtc(DateTime.add(momentOf(day), { months }));
|
|
84
|
+
/**
|
|
85
|
+
* The last day of the month the given day falls in. Ask the calendar
|
|
86
|
+
* rather than a table of month lengths: February in a leap year answers
|
|
87
|
+
* correctly here because the runtime's own calendar answers correctly.
|
|
88
|
+
*/
|
|
89
|
+
export const lastDayOfTheMonthOf = (day) => dayOfMomentUtc(DateTime.endOf(momentOf(day), 'month'));
|
|
90
|
+
/**
|
|
91
|
+
* The first day of the month the given day falls in. Every month starts
|
|
92
|
+
* on its first, so this one needs no calendar; it stays here beside its
|
|
93
|
+
* twin, and `monthOf` is the name a consumer reaches for.
|
|
94
|
+
*
|
|
95
|
+
* @internal
|
|
96
|
+
*/
|
|
97
|
+
export const firstDayOfTheMonthOf = (day) => dayOfMomentUtc(DateTime.startOf(momentOf(day), 'month'));
|
|
98
|
+
const isCalendarDay = (value) => typeof value === 'string' && Either.isRight(calendarDayFromIsoForm(value));
|
|
99
|
+
const CalendarDayValue = Schema.declare(isCalendarDay, {
|
|
100
|
+
identifier: 'CalendarDay',
|
|
101
|
+
});
|
|
102
|
+
/**
|
|
103
|
+
* Reads a day off the wire from the form a person writes, and writes it
|
|
104
|
+
* back in that same form. A text this schema refuses carries the whole
|
|
105
|
+
* three-part refusal into the parse error, so the reader of a failed
|
|
106
|
+
* command sees what to do next rather than a bare type name.
|
|
107
|
+
*/
|
|
108
|
+
export const CalendarDayFromHumanString = Schema.transformOrFail(Schema.String, CalendarDayValue, {
|
|
109
|
+
strict: true,
|
|
110
|
+
decode: (text, _options, ast) => Either.mapLeft(calendarDayFromHumanForm(text), (failure) => new ParseResult.Type(ast, text, refusalText(failure.refusal))),
|
|
111
|
+
encode: (day) => ParseResult.succeed(dayInHumanForm(day)),
|
|
112
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Refusal } from '@satorio/machinery/core';
|
|
2
|
+
/**
|
|
3
|
+
* Why a written day is not a calendar day. The reason is a code a caller
|
|
4
|
+
* matches on; the failure's own `refusal` is the sentence a person
|
|
5
|
+
* reads. Both come from the same failure, so the two never drift.
|
|
6
|
+
*/
|
|
7
|
+
export type CalendarDayRefusalReason = 'not the calendar-day form' | 'month out of range' | 'day out of range for that month';
|
|
8
|
+
declare const NotACalendarDayError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
9
|
+
readonly _tag: "NotACalendarDayError";
|
|
10
|
+
} & Readonly<A>;
|
|
11
|
+
/**
|
|
12
|
+
* The text a reader was handed does not name a day any calendar holds.
|
|
13
|
+
* `reason` names which rule it broke, and `refusal` says the same thing
|
|
14
|
+
* in the three parts a person needs.
|
|
15
|
+
*/
|
|
16
|
+
export declare class NotACalendarDayError extends NotACalendarDayError_base<{
|
|
17
|
+
readonly text: string;
|
|
18
|
+
readonly reason: CalendarDayRefusalReason;
|
|
19
|
+
}> {
|
|
20
|
+
get refusal(): Refusal;
|
|
21
|
+
}
|
|
22
|
+
declare const UnknownTimeZoneError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
23
|
+
readonly _tag: "UnknownTimeZoneError";
|
|
24
|
+
} & Readonly<A>;
|
|
25
|
+
/**
|
|
26
|
+
* The runtime has no zone under that name. Every zone this mechanism
|
|
27
|
+
* takes is an IANA name, and the runtime's own zone table answers.
|
|
28
|
+
*/
|
|
29
|
+
export declare class UnknownTimeZoneError extends UnknownTimeZoneError_base<{
|
|
30
|
+
readonly zoneId: string;
|
|
31
|
+
}> {
|
|
32
|
+
get refusal(): Refusal;
|
|
33
|
+
}
|
|
34
|
+
declare const PeriodEndsBeforeItStartsError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
35
|
+
readonly _tag: "PeriodEndsBeforeItStartsError";
|
|
36
|
+
} & Readonly<A>;
|
|
37
|
+
/**
|
|
38
|
+
* The two days handed in run backwards. Both fields carry the days as a
|
|
39
|
+
* person writes them, so the refusal quotes what the reader typed.
|
|
40
|
+
*/
|
|
41
|
+
export declare class PeriodEndsBeforeItStartsError extends PeriodEndsBeforeItStartsError_base<{
|
|
42
|
+
readonly from: string;
|
|
43
|
+
readonly to: string;
|
|
44
|
+
}> {
|
|
45
|
+
get refusal(): Refusal;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Every way this mechanism refuses a value.
|
|
49
|
+
*/
|
|
50
|
+
export type TimeFailure = NotACalendarDayError | UnknownTimeZoneError | PeriodEndsBeforeItStartsError;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Data } from 'effect';
|
|
2
|
+
const dayRefusals = {
|
|
3
|
+
'not the calendar-day form': (text) => ({
|
|
4
|
+
what: `${JSON.stringify(text)} is not written as a calendar day.`,
|
|
5
|
+
why: 'This mechanism reads a day written as dd.mm.yyyy or as ' +
|
|
6
|
+
'yyyy-mm-dd, and reads nothing else.',
|
|
7
|
+
whatToDo: 'Write the day as 19.03.2026 or as 2026-03-19, then hand it back.',
|
|
8
|
+
}),
|
|
9
|
+
'month out of range': (text) => ({
|
|
10
|
+
what: `${JSON.stringify(text)} names a month outside the twelve a year has.`,
|
|
11
|
+
why: 'No calendar holds that day, so this mechanism cannot place it.',
|
|
12
|
+
whatToDo: 'Correct the month to a number from 1 to 12, then hand the day back.',
|
|
13
|
+
}),
|
|
14
|
+
'day out of range for that month': (text) => ({
|
|
15
|
+
what: `${JSON.stringify(text)} names a day that month does not have.`,
|
|
16
|
+
why: 'The month it names is shorter than that, so the day never came.',
|
|
17
|
+
whatToDo: "Check that month's own length, correct the day, then hand it back.",
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The text a reader was handed does not name a day any calendar holds.
|
|
22
|
+
* `reason` names which rule it broke, and `refusal` says the same thing
|
|
23
|
+
* in the three parts a person needs.
|
|
24
|
+
*/
|
|
25
|
+
export class NotACalendarDayError extends Data.TaggedError('NotACalendarDayError') {
|
|
26
|
+
get refusal() {
|
|
27
|
+
return dayRefusals[this.reason](this.text);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The runtime has no zone under that name. Every zone this mechanism
|
|
32
|
+
* takes is an IANA name, and the runtime's own zone table answers.
|
|
33
|
+
*/
|
|
34
|
+
export class UnknownTimeZoneError extends Data.TaggedError('UnknownTimeZoneError') {
|
|
35
|
+
get refusal() {
|
|
36
|
+
return {
|
|
37
|
+
what: `${JSON.stringify(this.zoneId)} is not a time zone this runtime knows.`,
|
|
38
|
+
why: 'Without a zone this mechanism cannot say which calendar day an ' +
|
|
39
|
+
'instant falls on.',
|
|
40
|
+
whatToDo: 'Name an IANA zone such as Europe/Kyiv, then hand it back.',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The two days handed in run backwards. Both fields carry the days as a
|
|
46
|
+
* person writes them, so the refusal quotes what the reader typed.
|
|
47
|
+
*/
|
|
48
|
+
export class PeriodEndsBeforeItStartsError extends Data.TaggedError('PeriodEndsBeforeItStartsError') {
|
|
49
|
+
get refusal() {
|
|
50
|
+
return {
|
|
51
|
+
what: `This period runs from ${this.from} to ${this.to}.`,
|
|
52
|
+
why: 'Its end stands before its start, so no day belongs to it.',
|
|
53
|
+
whatToDo: 'Swap the two days, or correct the one that is wrong.',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry to the time mechanism: a day a person reads and writes, a
|
|
3
|
+
* period of days, and a moment on the clock. It gives you a calendar day a
|
|
4
|
+
* person can read and write, a period of such days with both bounds named, a
|
|
5
|
+
* moment on the world's clock, and the named zone that decides which day a
|
|
6
|
+
* moment belongs to.
|
|
7
|
+
*/
|
|
8
|
+
export { addDays, addMonths, type CalendarDay, CalendarDayFromHumanString, CalendarDayOrder, calendarDayFromHumanForm, calendarDayFromIsoForm, dayInHumanForm, dayInIsoForm, daysBetween, lastDayOfTheMonthOf, } from './calendar-day.js';
|
|
9
|
+
export { type CalendarDayRefusalReason, NotACalendarDayError, PeriodEndsBeforeItStartsError, type TimeFailure, UnknownTimeZoneError, } from './failures.js';
|
|
10
|
+
export { dateTimeOfInstant, dayOfInstant, type Instant, instantInHumanForm, instantOfDateTime, startOfDay, type TimeZone, todayIn, zoneIdOf, zoneNamed, } from './instant.js';
|
|
11
|
+
export { lengthInDays, monthOf, monthsOf, type Period, periodContains, periodOf, periodsOverlap, } from './period.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry to the time mechanism: a day a person reads and writes, a
|
|
3
|
+
* period of days, and a moment on the clock. It gives you a calendar day a
|
|
4
|
+
* person can read and write, a period of such days with both bounds named, a
|
|
5
|
+
* moment on the world's clock, and the named zone that decides which day a
|
|
6
|
+
* moment belongs to.
|
|
7
|
+
*/
|
|
8
|
+
export { addDays, addMonths, CalendarDayFromHumanString, CalendarDayOrder, calendarDayFromHumanForm, calendarDayFromIsoForm, dayInHumanForm, dayInIsoForm, daysBetween, lastDayOfTheMonthOf, } from './calendar-day.js';
|
|
9
|
+
export { NotACalendarDayError, PeriodEndsBeforeItStartsError, UnknownTimeZoneError, } from './failures.js';
|
|
10
|
+
export { dateTimeOfInstant, dayOfInstant, instantInHumanForm, instantOfDateTime, startOfDay, todayIn, zoneIdOf, zoneNamed, } from './instant.js';
|
|
11
|
+
export { lengthInDays, monthOf, monthsOf, periodContains, periodOf, periodsOverlap, } from './period.js';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { DateTime, Effect, Either } from 'effect';
|
|
2
|
+
import { type CalendarDay } from './calendar-day.js';
|
|
3
|
+
import { UnknownTimeZoneError } from './failures.js';
|
|
4
|
+
declare const timeZoneBrand: unique symbol;
|
|
5
|
+
/**
|
|
6
|
+
* One named time zone, as the runtime's own zone table knows it. The
|
|
7
|
+
* value carries a brand, so a name nobody checked never passes for a
|
|
8
|
+
* zone.
|
|
9
|
+
*/
|
|
10
|
+
export type TimeZone = string & {
|
|
11
|
+
readonly [timeZoneBrand]: 'TimeZone';
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* One moment on the world's clock, held as whole milliseconds since the
|
|
15
|
+
* start of 1970. A moment has no zone: a zone only decides which
|
|
16
|
+
* calendar day, and which wall time, a person in that place reads off it.
|
|
17
|
+
*/
|
|
18
|
+
export interface Instant {
|
|
19
|
+
readonly epochMilliseconds: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Takes an IANA zone name, and refuses a name the runtime does not know.
|
|
23
|
+
*/
|
|
24
|
+
export declare const zoneNamed: (zoneId: string) => Either.Either<TimeZone, UnknownTimeZoneError>;
|
|
25
|
+
/**
|
|
26
|
+
* Reads the IANA name back out of a zone, for a log line or a row.
|
|
27
|
+
*/
|
|
28
|
+
export declare const zoneIdOf: (zone: TimeZone) => string;
|
|
29
|
+
/**
|
|
30
|
+
* Takes a moment the machinery already holds as Effect's own UTC value.
|
|
31
|
+
* The queue envelope's `occurredAt` arrives that way, and this is where
|
|
32
|
+
* it becomes an instant.
|
|
33
|
+
*/
|
|
34
|
+
export declare const instantOfDateTime: (moment: DateTime.Utc) => Instant;
|
|
35
|
+
/**
|
|
36
|
+
* Hands a moment back as Effect's own UTC value, for a row, an envelope,
|
|
37
|
+
* or any code that already speaks that type.
|
|
38
|
+
*/
|
|
39
|
+
export declare const dateTimeOfInstant: (instant: Instant) => DateTime.Utc;
|
|
40
|
+
/**
|
|
41
|
+
* The calendar day a moment falls on for a person standing in that zone.
|
|
42
|
+
* The same moment is a different day in two different places, and this
|
|
43
|
+
* is the function that says which.
|
|
44
|
+
*/
|
|
45
|
+
export declare const dayOfInstant: (instant: Instant, zone: TimeZone) => CalendarDay;
|
|
46
|
+
/**
|
|
47
|
+
* The moment a calendar day begins in that zone. On the night a country
|
|
48
|
+
* moves its clocks, that moment is not the same distance from midnight
|
|
49
|
+
* in Greenwich as it was the day before.
|
|
50
|
+
*/
|
|
51
|
+
export declare const startOfDay: (day: CalendarDay, zone: TimeZone) => Instant;
|
|
52
|
+
/**
|
|
53
|
+
* Writes a moment the way a person in that zone reads it:
|
|
54
|
+
* `19.03.2026 14:05 +02:00`. The offset stands in the text on purpose,
|
|
55
|
+
* so the same line never means two things in two halves of a year.
|
|
56
|
+
*/
|
|
57
|
+
export declare const instantInHumanForm: (instant: Instant, zone: TimeZone) => string;
|
|
58
|
+
/**
|
|
59
|
+
* Today, as the calendar of that zone counts it, read from the clock the
|
|
60
|
+
* service already shares. A service that asked for today in Greenwich
|
|
61
|
+
* instead would close a customer's last day of the month two hours early.
|
|
62
|
+
*/
|
|
63
|
+
export declare const todayIn: (zone: TimeZone) => Effect.Effect<CalendarDay>;
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { DateTime, Effect, Either, Option } from 'effect';
|
|
2
|
+
import { dayInHumanForm, dayOfDateParts, } from './calendar-day.js';
|
|
3
|
+
import { UnknownTimeZoneError } from './failures.js';
|
|
4
|
+
const isKnownZone = (zoneId) => Option.isSome(DateTime.zoneMakeNamed(zoneId));
|
|
5
|
+
const zoneOf = (zone) => DateTime.zoneUnsafeMakeNamed(zone);
|
|
6
|
+
const zonedAt = (instant, zone) => DateTime.setZone(dateTimeOfInstant(instant), zoneOf(zone));
|
|
7
|
+
/**
|
|
8
|
+
* Takes an IANA zone name, and refuses a name the runtime does not know.
|
|
9
|
+
*/
|
|
10
|
+
export const zoneNamed = (zoneId) => isKnownZone(zoneId)
|
|
11
|
+
? Either.right(zoneId)
|
|
12
|
+
: Either.left(new UnknownTimeZoneError({ zoneId }));
|
|
13
|
+
/**
|
|
14
|
+
* Reads the IANA name back out of a zone, for a log line or a row.
|
|
15
|
+
*/
|
|
16
|
+
export const zoneIdOf = (zone) => zone;
|
|
17
|
+
/**
|
|
18
|
+
* Takes a moment the machinery already holds as Effect's own UTC value.
|
|
19
|
+
* The queue envelope's `occurredAt` arrives that way, and this is where
|
|
20
|
+
* it becomes an instant.
|
|
21
|
+
*/
|
|
22
|
+
export const instantOfDateTime = (moment) => ({
|
|
23
|
+
epochMilliseconds: DateTime.toEpochMillis(moment),
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* Hands a moment back as Effect's own UTC value, for a row, an envelope,
|
|
27
|
+
* or any code that already speaks that type.
|
|
28
|
+
*/
|
|
29
|
+
export const dateTimeOfInstant = (instant) => DateTime.unsafeMake(instant.epochMilliseconds);
|
|
30
|
+
/**
|
|
31
|
+
* The calendar day a moment falls on for a person standing in that zone.
|
|
32
|
+
* The same moment is a different day in two different places, and this
|
|
33
|
+
* is the function that says which.
|
|
34
|
+
*/
|
|
35
|
+
export const dayOfInstant = (instant, zone) => {
|
|
36
|
+
const parts = DateTime.toParts(zonedAt(instant, zone));
|
|
37
|
+
return dayOfDateParts(parts.year, parts.month, parts.day);
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The moment a calendar day begins in that zone. On the night a country
|
|
41
|
+
* moves its clocks, that moment is not the same distance from midnight
|
|
42
|
+
* in Greenwich as it was the day before.
|
|
43
|
+
*/
|
|
44
|
+
export const startOfDay = (day, zone) => instantOfDateTime(DateTime.toUtc(DateTime.unsafeMakeZoned(`${day}T00:00:00.000`, {
|
|
45
|
+
timeZone: zoneOf(zone),
|
|
46
|
+
adjustForTimeZone: true,
|
|
47
|
+
})));
|
|
48
|
+
/**
|
|
49
|
+
* Writes a moment the way a person in that zone reads it:
|
|
50
|
+
* `19.03.2026 14:05 +02:00`. The offset stands in the text on purpose,
|
|
51
|
+
* so the same line never means two things in two halves of a year.
|
|
52
|
+
*/
|
|
53
|
+
export const instantInHumanForm = (instant, zone) => {
|
|
54
|
+
const zoned = zonedAt(instant, zone);
|
|
55
|
+
const parts = DateTime.toParts(zoned);
|
|
56
|
+
const day = dayInHumanForm(dayOfInstant(instant, zone));
|
|
57
|
+
const hours = String(parts.hours).padStart(2, '0');
|
|
58
|
+
const minutes = String(parts.minutes).padStart(2, '0');
|
|
59
|
+
const clock = `${hours}:${minutes}`;
|
|
60
|
+
return `${day} ${clock} ${DateTime.zonedOffsetIso(zoned)}`;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Today, as the calendar of that zone counts it, read from the clock the
|
|
64
|
+
* service already shares. A service that asked for today in Greenwich
|
|
65
|
+
* instead would close a customer's last day of the month two hours early.
|
|
66
|
+
*/
|
|
67
|
+
export const todayIn = (zone) => DateTime.now.pipe(Effect.map((moment) => dayOfInstant(instantOfDateTime(moment), zone)));
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Either } from 'effect';
|
|
2
|
+
import { type CalendarDay } from './calendar-day.js';
|
|
3
|
+
import { PeriodEndsBeforeItStartsError } from './failures.js';
|
|
4
|
+
/**
|
|
5
|
+
* A run of calendar days. Both bounds belong to the period: the day in
|
|
6
|
+
* `from` and the day in `to` are inside it, and a period whose two days
|
|
7
|
+
* are the same is one day long. The rule is stated once, here, and every
|
|
8
|
+
* function below holds to it.
|
|
9
|
+
*/
|
|
10
|
+
export interface Period {
|
|
11
|
+
readonly from: CalendarDay;
|
|
12
|
+
readonly to: CalendarDay;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Takes two days as a period, earliest first. A pair that runs backwards
|
|
16
|
+
* comes back as a refusal naming both days as a person wrote them.
|
|
17
|
+
*/
|
|
18
|
+
export declare const periodOf: (from: CalendarDay, to: CalendarDay) => Either.Either<Period, PeriodEndsBeforeItStartsError>;
|
|
19
|
+
/**
|
|
20
|
+
* The whole calendar month the given day falls in, as a period. A close
|
|
21
|
+
* that runs over one month takes any day of it and gets the same two
|
|
22
|
+
* bounds, so nobody adds a month and subtracts a day to find the last of
|
|
23
|
+
* February.
|
|
24
|
+
*/
|
|
25
|
+
export declare const monthOf: (day: CalendarDay) => Period;
|
|
26
|
+
/**
|
|
27
|
+
* Answers whether the day belongs to the period. Either bound counts as
|
|
28
|
+
* inside.
|
|
29
|
+
*/
|
|
30
|
+
export declare const periodContains: (period: Period, day: CalendarDay) => boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Answers whether two periods share at least one day.
|
|
33
|
+
*/
|
|
34
|
+
export declare const periodsOverlap: (left: Period, right: Period) => boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Counts the days a period holds, both bounds included.
|
|
37
|
+
*/
|
|
38
|
+
export declare const lengthInDays: (period: Period) => number;
|
|
39
|
+
/**
|
|
40
|
+
* Cuts a period into one part per calendar month it runs over, in
|
|
41
|
+
* order. The first part starts on the period's own first day and the
|
|
42
|
+
* last part ends on its own last day, so the parts cover the period
|
|
43
|
+
* exactly once and never reach past it.
|
|
44
|
+
*/
|
|
45
|
+
export declare const monthsOf: (period: Period) => readonly Period[];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Either } from 'effect';
|
|
2
|
+
import { addDays, CalendarDayOrder, dayInHumanForm, daysBetween, firstDayOfTheMonthOf, lastDayOfTheMonthOf, } from './calendar-day.js';
|
|
3
|
+
import { PeriodEndsBeforeItStartsError } from './failures.js';
|
|
4
|
+
/**
|
|
5
|
+
* Takes two days as a period, earliest first. A pair that runs backwards
|
|
6
|
+
* comes back as a refusal naming both days as a person wrote them.
|
|
7
|
+
*/
|
|
8
|
+
export const periodOf = (from, to) => CalendarDayOrder(from, to) > 0
|
|
9
|
+
? Either.left(new PeriodEndsBeforeItStartsError({
|
|
10
|
+
from: dayInHumanForm(from),
|
|
11
|
+
to: dayInHumanForm(to),
|
|
12
|
+
}))
|
|
13
|
+
: Either.right({ from, to });
|
|
14
|
+
/**
|
|
15
|
+
* The whole calendar month the given day falls in, as a period. A close
|
|
16
|
+
* that runs over one month takes any day of it and gets the same two
|
|
17
|
+
* bounds, so nobody adds a month and subtracts a day to find the last of
|
|
18
|
+
* February.
|
|
19
|
+
*/
|
|
20
|
+
export const monthOf = (day) => ({
|
|
21
|
+
from: firstDayOfTheMonthOf(day),
|
|
22
|
+
to: lastDayOfTheMonthOf(day),
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Answers whether the day belongs to the period. Either bound counts as
|
|
26
|
+
* inside.
|
|
27
|
+
*/
|
|
28
|
+
export const periodContains = (period, day) => CalendarDayOrder(period.from, day) <= 0 &&
|
|
29
|
+
CalendarDayOrder(day, period.to) <= 0;
|
|
30
|
+
/**
|
|
31
|
+
* Answers whether two periods share at least one day.
|
|
32
|
+
*/
|
|
33
|
+
export const periodsOverlap = (left, right) => CalendarDayOrder(left.from, right.to) <= 0 &&
|
|
34
|
+
CalendarDayOrder(right.from, left.to) <= 0;
|
|
35
|
+
/**
|
|
36
|
+
* Counts the days a period holds, both bounds included.
|
|
37
|
+
*/
|
|
38
|
+
export const lengthInDays = (period) => daysBetween(period.from, period.to) + 1;
|
|
39
|
+
/**
|
|
40
|
+
* Cuts a period into one part per calendar month it runs over, in
|
|
41
|
+
* order. The first part starts on the period's own first day and the
|
|
42
|
+
* last part ends on its own last day, so the parts cover the period
|
|
43
|
+
* exactly once and never reach past it.
|
|
44
|
+
*/
|
|
45
|
+
export const monthsOf = (period) => {
|
|
46
|
+
const parts = [];
|
|
47
|
+
let start = period.from;
|
|
48
|
+
while (CalendarDayOrder(start, period.to) <= 0) {
|
|
49
|
+
const monthEnd = lastDayOfTheMonthOf(start);
|
|
50
|
+
const end = CalendarDayOrder(monthEnd, period.to) <= 0 ? monthEnd : period.to;
|
|
51
|
+
parts.push({ from: start, to: end });
|
|
52
|
+
start = addDays(end, 1);
|
|
53
|
+
}
|
|
54
|
+
return parts;
|
|
55
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a live stand may outlive the run that started it. A developer
|
|
3
|
+
* machine sets `TESTCONTAINERS_REUSE_ENABLE=true` once and every later
|
|
4
|
+
* run finds the database and the AWS stand already up, which takes the
|
|
5
|
+
* container start out of the full check. A pipeline leaves it unset, so
|
|
6
|
+
* every run gets a stand nobody has touched.
|
|
7
|
+
*
|
|
8
|
+
* What no test here proves: that a second run really reaches the first
|
|
9
|
+
* run's container. Only two runs on one machine prove that, and the page
|
|
10
|
+
* on the live stands says how to watch it happen.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export declare const standsMayBeReused: (environment?: Readonly<Record<string, string | undefined>>) => boolean;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const OFF = new Set(['', '0', 'false', 'no']);
|
|
2
|
+
/**
|
|
3
|
+
* Whether a live stand may outlive the run that started it. A developer
|
|
4
|
+
* machine sets `TESTCONTAINERS_REUSE_ENABLE=true` once and every later
|
|
5
|
+
* run finds the database and the AWS stand already up, which takes the
|
|
6
|
+
* container start out of the full check. A pipeline leaves it unset, so
|
|
7
|
+
* every run gets a stand nobody has touched.
|
|
8
|
+
*
|
|
9
|
+
* What no test here proves: that a second run really reaches the first
|
|
10
|
+
* run's container. Only two runs on one machine prove that, and the page
|
|
11
|
+
* on the live stands says how to watch it happen.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export const standsMayBeReused = (environment = process.env) => !OFF.has((environment.TESTCONTAINERS_REUSE_ENABLE ?? '').toLowerCase());
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses `.env` file contents into key-value pairs, one `KEY=value` per
|
|
3
|
+
* line. Skips blank lines and lines starting with `#`. Values keep any
|
|
4
|
+
* surrounding quotes as-is.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export declare const parseDotenv: (contents: string) => ReadonlyMap<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* Loads a `.env` file into `process.env`, without overwriting a value the
|
|
11
|
+
* real environment already set. Call it from `vitest.config.ts`, before
|
|
12
|
+
* vitest forks its worker pool, so every worker inherits the same values.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export declare const loadDotenvIntoProcessEnv: (path: string) => void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* Parses `.env` file contents into key-value pairs, one `KEY=value` per
|
|
4
|
+
* line. Skips blank lines and lines starting with `#`. Values keep any
|
|
5
|
+
* surrounding quotes as-is.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export const parseDotenv = (contents) => {
|
|
10
|
+
const variables = new Map();
|
|
11
|
+
for (const line of contents.split('\n')) {
|
|
12
|
+
const trimmed = line.trim();
|
|
13
|
+
if (trimmed === '' || trimmed.startsWith('#'))
|
|
14
|
+
continue;
|
|
15
|
+
const separatorIndex = trimmed.indexOf('=');
|
|
16
|
+
if (separatorIndex === -1)
|
|
17
|
+
continue;
|
|
18
|
+
const key = trimmed.slice(0, separatorIndex).trim();
|
|
19
|
+
const value = trimmed.slice(separatorIndex + 1).trim();
|
|
20
|
+
if (key === '')
|
|
21
|
+
continue;
|
|
22
|
+
variables.set(key, value);
|
|
23
|
+
}
|
|
24
|
+
return variables;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Loads a `.env` file into `process.env`, without overwriting a value the
|
|
28
|
+
* real environment already set. Call it from `vitest.config.ts`, before
|
|
29
|
+
* vitest forks its worker pool, so every worker inherits the same values.
|
|
30
|
+
*
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export const loadDotenvIntoProcessEnv = (path) => {
|
|
34
|
+
let contents;
|
|
35
|
+
try {
|
|
36
|
+
contents = readFileSync(path, 'utf8');
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
for (const [key, value] of parseDotenv(contents)) {
|
|
42
|
+
// @effect-diagnostics effect/processEnv:off
|
|
43
|
+
if (process.env[key] === undefined) {
|
|
44
|
+
process.env[key] = value;
|
|
45
|
+
}
|
|
46
|
+
// @effect-diagnostics effect/processEnv:error
|
|
47
|
+
}
|
|
48
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
*/
|
|
4
|
+
export interface EffectCopy {
|
|
5
|
+
readonly packageDirectory: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export declare const walkForEffectCopies: (nodeModulesDirectory: string, visited: Set<string>, found: EffectCopy[]) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Options for `oneEffectInstanceGuard`. `root` defaults to `repoRoot()`, the
|
|
13
|
+
* caller's own repository root — the same default every other guard in
|
|
14
|
+
* this mechanism uses.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export interface SingleEffectInstanceOptions {
|
|
19
|
+
readonly root?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Registers a vitest suite that fails when a tree holds more than one copy of
|
|
23
|
+
* Effect.
|
|
24
|
+
*
|
|
25
|
+
* Registers a vitest suite that fails if more than one physical copy of the
|
|
26
|
+
* `effect` package is reachable from a consumer's `node_modules` tree, or if
|
|
27
|
+
* `require.resolve('effect')` disagrees between the consumer root and this
|
|
28
|
+
* package's own installed directory. Call it from a consumer's `*.test.ts`
|
|
29
|
+
* file — effect is a peer dependency precisely so the consumer keeps a single
|
|
30
|
+
* instance, and this is the guard that makes that invariant loud instead of
|
|
31
|
+
* silent (see `dec-20260902-8898ccfd`). It walks `node_modules` rather than
|
|
32
|
+
* the repository, so it takes no floor option; instead it refuses on zero
|
|
33
|
+
* copies found and on a resolution that answers nothing, because a comparison
|
|
34
|
+
* over an empty walk passes every time.
|
|
35
|
+
*/
|
|
36
|
+
export declare const oneEffectInstanceGuard: (options?: SingleEffectInstanceOptions) => void;
|