@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.
Files changed (341) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +71 -0
  3. package/dictionary/all.json +4656 -0
  4. package/dictionary/all.toon +639 -0
  5. package/dictionary/core.json +221 -0
  6. package/dictionary/core.toon +33 -0
  7. package/dictionary/graphql.json +212 -0
  8. package/dictionary/graphql.toon +32 -0
  9. package/dictionary/http.json +113 -0
  10. package/dictionary/http.toon +21 -0
  11. package/dictionary/identifiers.json +266 -0
  12. package/dictionary/identifiers.toon +38 -0
  13. package/dictionary/index.json +96 -0
  14. package/dictionary/index.toon +18 -0
  15. package/dictionary/integration.json +131 -0
  16. package/dictionary/integration.toon +23 -0
  17. package/dictionary/money.json +383 -0
  18. package/dictionary/money.toon +51 -0
  19. package/dictionary/ports.json +392 -0
  20. package/dictionary/ports.toon +52 -0
  21. package/dictionary/postgres.json +473 -0
  22. package/dictionary/postgres.toon +61 -0
  23. package/dictionary/publishing.json +131 -0
  24. package/dictionary/publishing.toon +23 -0
  25. package/dictionary/queue.json +365 -0
  26. package/dictionary/queue.toon +49 -0
  27. package/dictionary/reference/api.mdx +843 -0
  28. package/dictionary/reference/uk/api.mdx +844 -0
  29. package/dictionary/storage.json +194 -0
  30. package/dictionary/storage.toon +30 -0
  31. package/dictionary/testing.json +707 -0
  32. package/dictionary/testing.toon +87 -0
  33. package/dictionary/time.json +311 -0
  34. package/dictionary/time.toon +43 -0
  35. package/dictionary/vitest.json +356 -0
  36. package/dictionary/vitest.toon +48 -0
  37. package/dictionary/workflow.json +185 -0
  38. package/dictionary/workflow.toon +29 -0
  39. package/dist/bin/command.d.ts +45 -0
  40. package/dist/bin/command.js +113 -0
  41. package/dist/bin.d.ts +2 -0
  42. package/dist/bin.js +11 -0
  43. package/dist/core/bigint-json.d.ts +6 -0
  44. package/dist/core/bigint-json.js +6 -0
  45. package/dist/core/closed-set.d.ts +14 -0
  46. package/dist/core/closed-set.js +8 -0
  47. package/dist/core/domain-error.d.ts +8 -0
  48. package/dist/core/domain-error.js +11 -0
  49. package/dist/core/environment.d.ts +124 -0
  50. package/dist/core/environment.js +115 -0
  51. package/dist/core/index.d.ts +13 -0
  52. package/dist/core/index.js +13 -0
  53. package/dist/core/ulid.d.ts +27 -0
  54. package/dist/core/ulid.js +27 -0
  55. package/dist/graphql/builder.d.ts +36 -0
  56. package/dist/graphql/builder.js +35 -0
  57. package/dist/graphql/context.d.ts +36 -0
  58. package/dist/graphql/context.js +69 -0
  59. package/dist/graphql/failure-classification.d.ts +11 -0
  60. package/dist/graphql/failure-classification.js +1 -0
  61. package/dist/graphql/failures.d.ts +62 -0
  62. package/dist/graphql/failures.js +30 -0
  63. package/dist/graphql/graphiql.d.ts +45 -0
  64. package/dist/graphql/graphiql.js +56 -0
  65. package/dist/graphql/index.d.ts +15 -0
  66. package/dist/graphql/index.js +14 -0
  67. package/dist/graphql/layer.d.ts +15 -0
  68. package/dist/graphql/layer.js +23 -0
  69. package/dist/graphql/request-handler.d.ts +26 -0
  70. package/dist/graphql/request-handler.js +33 -0
  71. package/dist/graphql/server.d.ts +19 -0
  72. package/dist/graphql/server.js +16 -0
  73. package/dist/graphql/settings.d.ts +55 -0
  74. package/dist/graphql/settings.js +82 -0
  75. package/dist/http/health.d.ts +70 -0
  76. package/dist/http/health.js +60 -0
  77. package/dist/http/index.d.ts +5 -0
  78. package/dist/http/index.js +5 -0
  79. package/dist/identifiers/client-code.d.ts +37 -0
  80. package/dist/identifiers/client-code.js +41 -0
  81. package/dist/identifiers/creditor-reference.d.ts +38 -0
  82. package/dist/identifiers/creditor-reference.js +32 -0
  83. package/dist/identifiers/damm.d.ts +22 -0
  84. package/dist/identifiers/damm.js +50 -0
  85. package/dist/identifiers/document-number.d.ts +68 -0
  86. package/dist/identifiers/document-number.js +73 -0
  87. package/dist/identifiers/document-numbers.d.ts +27 -0
  88. package/dist/identifiers/document-numbers.js +16 -0
  89. package/dist/identifiers/failures.d.ts +46 -0
  90. package/dist/identifiers/failures.js +48 -0
  91. package/dist/identifiers/index.d.ts +14 -0
  92. package/dist/identifiers/index.js +13 -0
  93. package/dist/identifiers/next-document-number.d.ts +26 -0
  94. package/dist/identifiers/next-document-number.js +31 -0
  95. package/dist/identifiers/ulid.d.ts +28 -0
  96. package/dist/identifiers/ulid.js +71 -0
  97. package/dist/integration/external-http-client.d.ts +22 -0
  98. package/dist/integration/external-http-client.js +74 -0
  99. package/dist/integration/index.d.ts +12 -0
  100. package/dist/integration/index.js +12 -0
  101. package/dist/integration/layer.d.ts +19 -0
  102. package/dist/integration/layer.js +30 -0
  103. package/dist/integration/retry.d.ts +65 -0
  104. package/dist/integration/retry.js +54 -0
  105. package/dist/integration/settings.d.ts +73 -0
  106. package/dist/integration/settings.js +92 -0
  107. package/dist/money/amount.d.ts +60 -0
  108. package/dist/money/amount.js +67 -0
  109. package/dist/money/arithmetic.d.ts +60 -0
  110. package/dist/money/arithmetic.js +73 -0
  111. package/dist/money/currency.d.ts +25 -0
  112. package/dist/money/currency.js +24 -0
  113. package/dist/money/decimal.d.ts +29 -0
  114. package/dist/money/decimal.js +39 -0
  115. package/dist/money/exchange-rate.d.ts +32 -0
  116. package/dist/money/exchange-rate.js +28 -0
  117. package/dist/money/failures.d.ts +86 -0
  118. package/dist/money/failures.js +97 -0
  119. package/dist/money/index.d.ts +15 -0
  120. package/dist/money/index.js +15 -0
  121. package/dist/money/percent.d.ts +35 -0
  122. package/dist/money/percent.js +50 -0
  123. package/dist/money/schema.d.ts +25 -0
  124. package/dist/money/schema.js +41 -0
  125. package/dist/ports/blob-store.d.ts +42 -0
  126. package/dist/ports/blob-store.js +13 -0
  127. package/dist/ports/document-number-counter.d.ts +36 -0
  128. package/dist/ports/document-number-counter.js +12 -0
  129. package/dist/ports/event-publisher.d.ts +29 -0
  130. package/dist/ports/event-publisher.js +13 -0
  131. package/dist/ports/external-http-client.d.ts +60 -0
  132. package/dist/ports/external-http-client.js +25 -0
  133. package/dist/ports/index.d.ts +17 -0
  134. package/dist/ports/index.js +17 -0
  135. package/dist/ports/integration-event-outbox.d.ts +44 -0
  136. package/dist/ports/integration-event-outbox.js +13 -0
  137. package/dist/ports/port-unavailable.d.ts +21 -0
  138. package/dist/ports/port-unavailable.js +30 -0
  139. package/dist/ports/processed-event-registry.d.ts +57 -0
  140. package/dist/ports/processed-event-registry.js +24 -0
  141. package/dist/ports/queue-message-producer.d.ts +26 -0
  142. package/dist/ports/queue-message-producer.js +12 -0
  143. package/dist/ports/queue-message-source.d.ts +39 -0
  144. package/dist/ports/queue-message-source.js +13 -0
  145. package/dist/ports/readiness-probe.d.ts +27 -0
  146. package/dist/ports/readiness-probe.js +13 -0
  147. package/dist/ports/transaction-boundary.d.ts +28 -0
  148. package/dist/ports/transaction-boundary.js +13 -0
  149. package/dist/postgres/client.d.ts +51 -0
  150. package/dist/postgres/client.js +81 -0
  151. package/dist/postgres/document-number-counter.d.ts +14 -0
  152. package/dist/postgres/document-number-counter.js +27 -0
  153. package/dist/postgres/failures.d.ts +28 -0
  154. package/dist/postgres/failures.js +18 -0
  155. package/dist/postgres/index.d.ts +18 -0
  156. package/dist/postgres/index.js +18 -0
  157. package/dist/postgres/integration-event-outbox.d.ts +15 -0
  158. package/dist/postgres/integration-event-outbox.js +60 -0
  159. package/dist/postgres/jsonb-value.d.ts +10 -0
  160. package/dist/postgres/jsonb-value.js +9 -0
  161. package/dist/postgres/layer.d.ts +57 -0
  162. package/dist/postgres/layer.js +72 -0
  163. package/dist/postgres/migrations/advisory-lock.d.ts +47 -0
  164. package/dist/postgres/migrations/advisory-lock.js +59 -0
  165. package/dist/postgres/migrations/runner.d.ts +56 -0
  166. package/dist/postgres/migrations/runner.js +69 -0
  167. package/dist/postgres/migrations/sql-file-loader.d.ts +59 -0
  168. package/dist/postgres/migrations/sql-file-loader.js +99 -0
  169. package/dist/postgres/processed-event-registry.d.ts +13 -0
  170. package/dist/postgres/processed-event-registry.js +44 -0
  171. package/dist/postgres/readiness-probe.d.ts +27 -0
  172. package/dist/postgres/readiness-probe.js +33 -0
  173. package/dist/postgres/settings.d.ts +134 -0
  174. package/dist/postgres/settings.js +210 -0
  175. package/dist/postgres/sql/0001_processed_event.sql +4 -0
  176. package/dist/postgres/sql/0002_integration_event_outbox.sql +13 -0
  177. package/dist/postgres/sql/0003_document_number_counter.sql +6 -0
  178. package/dist/postgres/sql-failure.d.ts +19 -0
  179. package/dist/postgres/sql-failure.js +22 -0
  180. package/dist/postgres/store-failure.d.ts +17 -0
  181. package/dist/postgres/store-failure.js +16 -0
  182. package/dist/postgres/transaction-boundary.d.ts +13 -0
  183. package/dist/postgres/transaction-boundary.js +21 -0
  184. package/dist/publishing/event-publisher.sns.d.ts +66 -0
  185. package/dist/publishing/event-publisher.sns.js +75 -0
  186. package/dist/publishing/event-publisher.stdout.d.ts +15 -0
  187. package/dist/publishing/event-publisher.stdout.js +24 -0
  188. package/dist/publishing/index.d.ts +10 -0
  189. package/dist/publishing/index.js +10 -0
  190. package/dist/publishing/layer.d.ts +25 -0
  191. package/dist/publishing/layer.js +39 -0
  192. package/dist/publishing/settings.d.ts +60 -0
  193. package/dist/publishing/settings.js +85 -0
  194. package/dist/queue/consumer.d.ts +91 -0
  195. package/dist/queue/consumer.js +92 -0
  196. package/dist/queue/envelope.d.ts +39 -0
  197. package/dist/queue/envelope.js +39 -0
  198. package/dist/queue/failures.d.ts +33 -0
  199. package/dist/queue/failures.js +15 -0
  200. package/dist/queue/index.d.ts +14 -0
  201. package/dist/queue/index.js +14 -0
  202. package/dist/queue/layer.d.ts +33 -0
  203. package/dist/queue/layer.js +58 -0
  204. package/dist/queue/message-codec.d.ts +43 -0
  205. package/dist/queue/message-codec.js +33 -0
  206. package/dist/queue/queue-message-producer.d.ts +61 -0
  207. package/dist/queue/queue-message-producer.js +52 -0
  208. package/dist/queue/queue-message-source.d.ts +93 -0
  209. package/dist/queue/queue-message-source.js +86 -0
  210. package/dist/queue/readiness.d.ts +78 -0
  211. package/dist/queue/readiness.js +86 -0
  212. package/dist/queue/settings.d.ts +106 -0
  213. package/dist/queue/settings.js +160 -0
  214. package/dist/storage/blob-store.d.ts +84 -0
  215. package/dist/storage/blob-store.js +109 -0
  216. package/dist/storage/index.d.ts +9 -0
  217. package/dist/storage/index.js +9 -0
  218. package/dist/storage/layer.d.ts +17 -0
  219. package/dist/storage/layer.js +30 -0
  220. package/dist/storage/settings.d.ts +48 -0
  221. package/dist/storage/settings.js +65 -0
  222. package/dist/testing/blob-store.d.ts +27 -0
  223. package/dist/testing/blob-store.js +45 -0
  224. package/dist/testing/clock.d.ts +29 -0
  225. package/dist/testing/clock.js +41 -0
  226. package/dist/testing/dna-perturbator/attention.d.ts +55 -0
  227. package/dist/testing/dna-perturbator/attention.js +49 -0
  228. package/dist/testing/dna-perturbator/chains.d.ts +31 -0
  229. package/dist/testing/dna-perturbator/chains.js +53 -0
  230. package/dist/testing/dna-perturbator/report.d.ts +12 -0
  231. package/dist/testing/dna-perturbator/report.js +22 -0
  232. package/dist/testing/dna-perturbator/verdict.d.ts +26 -0
  233. package/dist/testing/dna-perturbator/verdict.js +28 -0
  234. package/dist/testing/dna-perturbator.d.ts +48 -0
  235. package/dist/testing/dna-perturbator.js +145 -0
  236. package/dist/testing/document-number-counter.d.ts +29 -0
  237. package/dist/testing/document-number-counter.js +39 -0
  238. package/dist/testing/event-publisher.d.ts +28 -0
  239. package/dist/testing/event-publisher.js +33 -0
  240. package/dist/testing/external-http-client.d.ts +29 -0
  241. package/dist/testing/external-http-client.js +49 -0
  242. package/dist/testing/index.d.ts +24 -0
  243. package/dist/testing/index.js +24 -0
  244. package/dist/testing/integration-event-outbox.d.ts +37 -0
  245. package/dist/testing/integration-event-outbox.js +67 -0
  246. package/dist/testing/mutation-settings.d.ts +71 -0
  247. package/dist/testing/mutation-settings.js +57 -0
  248. package/dist/testing/postgres-rows.d.ts +12 -0
  249. package/dist/testing/postgres-rows.js +19 -0
  250. package/dist/testing/processed-event-registry.d.ts +25 -0
  251. package/dist/testing/processed-event-registry.js +39 -0
  252. package/dist/testing/queue-message-producer.d.ts +27 -0
  253. package/dist/testing/queue-message-producer.js +41 -0
  254. package/dist/testing/queue-message-source.d.ts +54 -0
  255. package/dist/testing/queue-message-source.js +71 -0
  256. package/dist/testing/quiet-reporter.d.ts +86 -0
  257. package/dist/testing/quiet-reporter.js +81 -0
  258. package/dist/testing/readiness-probe.d.ts +27 -0
  259. package/dist/testing/readiness-probe.js +33 -0
  260. package/dist/testing/scenario.d.ts +94 -0
  261. package/dist/testing/scenario.js +81 -0
  262. package/dist/testing/transaction-boundary.d.ts +30 -0
  263. package/dist/testing/transaction-boundary.js +36 -0
  264. package/dist/testing/typed-failure.d.ts +8 -0
  265. package/dist/testing/typed-failure.js +15 -0
  266. package/dist/time/calendar-day.d.ts +83 -0
  267. package/dist/time/calendar-day.js +112 -0
  268. package/dist/time/failures.d.ts +51 -0
  269. package/dist/time/failures.js +56 -0
  270. package/dist/time/index.d.ts +11 -0
  271. package/dist/time/index.js +11 -0
  272. package/dist/time/instant.d.ts +64 -0
  273. package/dist/time/instant.js +67 -0
  274. package/dist/time/period.d.ts +45 -0
  275. package/dist/time/period.js +55 -0
  276. package/dist/vitest/container-reuse.d.ts +14 -0
  277. package/dist/vitest/container-reuse.js +15 -0
  278. package/dist/vitest/dotenv.d.ts +16 -0
  279. package/dist/vitest/dotenv.js +48 -0
  280. package/dist/vitest/effect-single-instance.d.ts +36 -0
  281. package/dist/vitest/effect-single-instance.js +125 -0
  282. package/dist/vitest/fast-check-single-instance.d.ts +11 -0
  283. package/dist/vitest/fast-check-single-instance.js +26 -0
  284. package/dist/vitest/index.d.ts +31 -0
  285. package/dist/vitest/index.js +30 -0
  286. package/dist/vitest/localstack-container.d.ts +32 -0
  287. package/dist/vitest/localstack-container.js +55 -0
  288. package/dist/vitest/port-contract-tester.d.ts +8 -0
  289. package/dist/vitest/port-contract-tester.js +1 -0
  290. package/dist/vitest/port-contracts/blob-store.contract.d.ts +22 -0
  291. package/dist/vitest/port-contracts/blob-store.contract.js +108 -0
  292. package/dist/vitest/port-contracts/document-number-counter.contract.d.ts +18 -0
  293. package/dist/vitest/port-contracts/document-number-counter.contract.js +90 -0
  294. package/dist/vitest/port-contracts/event-publisher.contract.d.ts +35 -0
  295. package/dist/vitest/port-contracts/event-publisher.contract.js +67 -0
  296. package/dist/vitest/port-contracts/external-http-client.contract.d.ts +35 -0
  297. package/dist/vitest/port-contracts/external-http-client.contract.js +52 -0
  298. package/dist/vitest/port-contracts/integration-event-outbox.contract.d.ts +23 -0
  299. package/dist/vitest/port-contracts/integration-event-outbox.contract.js +75 -0
  300. package/dist/vitest/port-contracts/processed-event-registry.contract.d.ts +18 -0
  301. package/dist/vitest/port-contracts/processed-event-registry.contract.js +84 -0
  302. package/dist/vitest/port-contracts/queue-message-producer.contract.d.ts +26 -0
  303. package/dist/vitest/port-contracts/queue-message-producer.contract.js +48 -0
  304. package/dist/vitest/port-contracts/queue-message-source.contract.d.ts +29 -0
  305. package/dist/vitest/port-contracts/queue-message-source.contract.js +71 -0
  306. package/dist/vitest/port-contracts/readiness-probe.contract.d.ts +18 -0
  307. package/dist/vitest/port-contracts/readiness-probe.contract.js +41 -0
  308. package/dist/vitest/port-contracts/transaction-boundary.contract.d.ts +21 -0
  309. package/dist/vitest/port-contracts/transaction-boundary.contract.js +64 -0
  310. package/dist/vitest/postgres-container.d.ts +57 -0
  311. package/dist/vitest/postgres-container.js +118 -0
  312. package/dist/vitest/reader-port-contract.d.ts +41 -0
  313. package/dist/vitest/reader-port-contract.js +43 -0
  314. package/dist/vitest/repo-source-files.d.ts +37 -0
  315. package/dist/vitest/repo-source-files.js +61 -0
  316. package/dist/vitest/reset-tables.d.ts +18 -0
  317. package/dist/vitest/reset-tables.js +30 -0
  318. package/dist/vitest/settings-contract.d.ts +38 -0
  319. package/dist/vitest/settings-contract.js +77 -0
  320. package/dist/vitest/stub-server.d.ts +43 -0
  321. package/dist/vitest/stub-server.js +60 -0
  322. package/dist/vitest/term-registry.json +730 -0
  323. package/dist/vitest/testclock-fork-isolated-files.d.ts +13 -0
  324. package/dist/vitest/testclock-fork-isolated-files.js +18 -0
  325. package/dist/vitest/testclock-fork-isolation.d.ts +77 -0
  326. package/dist/vitest/testclock-fork-isolation.js +188 -0
  327. package/dist/workflow/claim-once.d.ts +16 -0
  328. package/dist/workflow/claim-once.js +9 -0
  329. package/dist/workflow/failures.d.ts +47 -0
  330. package/dist/workflow/failures.js +22 -0
  331. package/dist/workflow/index.d.ts +12 -0
  332. package/dist/workflow/index.js +12 -0
  333. package/dist/workflow/on-claim.d.ts +17 -0
  334. package/dist/workflow/on-claim.js +8 -0
  335. package/dist/workflow/publish-pending-events.d.ts +31 -0
  336. package/dist/workflow/publish-pending-events.js +31 -0
  337. package/dist/workflow/transactionally.d.ts +8 -0
  338. package/dist/workflow/transactionally.js +8 -0
  339. package/dist/workflow/versioned-payload.d.ts +35 -0
  340. package/dist/workflow/versioned-payload.js +43 -0
  341. 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;