@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,125 @@
1
+ import { existsSync, readdirSync, realpathSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { describe, expect, it } from 'vitest';
6
+ import { repoRoot } from './repo-source-files.js';
7
+ const isSkippedEntry = (name) => name.startsWith('.');
8
+ /**
9
+ * @internal
10
+ */
11
+ export const walkForEffectCopies = (nodeModulesDirectory, visited, found) => {
12
+ let entries;
13
+ try {
14
+ entries = readdirSync(nodeModulesDirectory);
15
+ }
16
+ catch {
17
+ return;
18
+ }
19
+ for (const name of entries) {
20
+ if (isSkippedEntry(name))
21
+ continue;
22
+ const entryPath = join(nodeModulesDirectory, name);
23
+ let realEntryPath;
24
+ try {
25
+ realEntryPath = realpathSync(entryPath);
26
+ }
27
+ catch {
28
+ continue;
29
+ }
30
+ if (visited.has(realEntryPath))
31
+ continue;
32
+ visited.add(realEntryPath);
33
+ if (name.startsWith('@')) {
34
+ walkForEffectCopies(entryPath, visited, found);
35
+ continue;
36
+ }
37
+ if (name === 'effect' && existsSync(join(entryPath, 'package.json'))) {
38
+ found.push({ packageDirectory: realEntryPath });
39
+ }
40
+ const nested = join(entryPath, 'node_modules');
41
+ if (existsSync(nested))
42
+ walkForEffectCopies(nested, visited, found);
43
+ }
44
+ };
45
+ const resolveEffectEntryFrom = (directory) => {
46
+ const anchor = join(directory, 'package.json');
47
+ try {
48
+ return realpathSync(createRequire(anchor).resolve('effect'));
49
+ }
50
+ catch {
51
+ return undefined;
52
+ }
53
+ };
54
+ const WHY_ONE_COPY_MATTERS = "why one copy matters: effect's Context.Tag identity and TestClock " +
55
+ 'depend on true module-singleton state. Two physical copies break both ' +
56
+ 'silently — a service registered against one copy is invisible to code ' +
57
+ 'running against the other, and a TestClock the test believes it is ' +
58
+ 'driving never touches the runtime under test.';
59
+ const WHAT_TO_DO = 'what to do: dedupe node_modules (reinstall, or run your package ' +
60
+ "manager's dedupe command) so only one effect copy remains, or align " +
61
+ "the offending path's effect version with this package's peer range " +
62
+ '(^3.22.1) so the package manager collapses it back to one copy.';
63
+ const tooManyCopiesMessage = (directories) => [
64
+ `what happened: found ${directories.length} physical copies of the ` +
65
+ `effect package under node_modules:\n${directories.map((d) => ` - ${d}`).join('\n')}`,
66
+ WHY_ONE_COPY_MATTERS,
67
+ WHAT_TO_DO,
68
+ ].join('\n');
69
+ const noCopyAtAllMessage = (root) => [
70
+ 'what happened: the walk over ' +
71
+ `${root}/node_modules found no copy of the effect package at all.`,
72
+ 'why that is a finding, not a pass: this suite proves there is exactly ' +
73
+ 'one copy. Zero copies means it walked the wrong tree — an unbuilt ' +
74
+ 'install, a moved root, a workspace whose packages live elsewhere — ' +
75
+ 'and a comparison over nothing passes every time.',
76
+ 'what to do: install dependencies at that root, or pass the root that ' +
77
+ "really holds node_modules through the guard's `root` option.",
78
+ ].join('\n');
79
+ const resolveMismatchMessage = (fromConsumerRoot, fromPackageDirectory) => [
80
+ "what happened: require.resolve('effect') resolves to a different " +
81
+ `file depending on where it is called from — from the consumer ` +
82
+ `root: ${fromConsumerRoot ?? '(could not resolve)'}; from the ` +
83
+ `package's own directory: ${fromPackageDirectory ?? '(could not resolve)'}.`,
84
+ WHY_ONE_COPY_MATTERS,
85
+ WHAT_TO_DO,
86
+ ].join('\n');
87
+ /**
88
+ * Registers a vitest suite that fails when a tree holds more than one copy of
89
+ * Effect.
90
+ *
91
+ * Registers a vitest suite that fails if more than one physical copy of the
92
+ * `effect` package is reachable from a consumer's `node_modules` tree, or if
93
+ * `require.resolve('effect')` disagrees between the consumer root and this
94
+ * package's own installed directory. Call it from a consumer's `*.test.ts`
95
+ * file — effect is a peer dependency precisely so the consumer keeps a single
96
+ * instance, and this is the guard that makes that invariant loud instead of
97
+ * silent (see `dec-20260902-8898ccfd`). It walks `node_modules` rather than
98
+ * the repository, so it takes no floor option; instead it refuses on zero
99
+ * copies found and on a resolution that answers nothing, because a comparison
100
+ * over an empty walk passes every time.
101
+ */
102
+ export const oneEffectInstanceGuard = (options = {}) => {
103
+ const root = options.root ?? repoRoot();
104
+ const packageOwnDirectory = fileURLToPath(new URL('.', import.meta.url));
105
+ describe('machinery: effect lives in a single instance', () => {
106
+ it('node_modules holds exactly one physical copy of effect', () => {
107
+ const found = [];
108
+ walkForEffectCopies(join(root, 'node_modules'), new Set(), found);
109
+ const distinctDirectories = [
110
+ ...new Set(found.map((copy) => copy.packageDirectory)),
111
+ ].sort();
112
+ const violation = distinctDirectories.length > 1 ? distinctDirectories : [];
113
+ console.log(`effect single instance: found ${distinctDirectories.length} copy ` +
114
+ `or copies under ${root}/node_modules`);
115
+ expect(distinctDirectories.length, noCopyAtAllMessage(root)).toBeGreaterThanOrEqual(1);
116
+ expect(violation, tooManyCopiesMessage(distinctDirectories)).toEqual([]);
117
+ });
118
+ it("require.resolve('effect') agrees from the consumer root and the package's own directory", () => {
119
+ const fromConsumerRoot = resolveEffectEntryFrom(root);
120
+ const fromPackageDirectory = resolveEffectEntryFrom(packageOwnDirectory);
121
+ expect(fromConsumerRoot, resolveMismatchMessage(fromConsumerRoot, fromPackageDirectory)).toBeDefined();
122
+ expect(fromConsumerRoot, resolveMismatchMessage(fromConsumerRoot, fromPackageDirectory)).toBe(fromPackageDirectory);
123
+ });
124
+ });
125
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Registers a vitest suite that fails when the property library resolves to
3
+ * two module instances.
4
+ *
5
+ * Registers a vitest suite that fails if the direct `fast-check` import and
6
+ * `effect/FastCheck` resolve to two separate module instances — a split that
7
+ * silently breaks arbitraries built from one being usable in a property built
8
+ * from the other. Call it from a consumer's `*.test.ts` file that depends on
9
+ * both `effect` and `fast-check`.
10
+ */
11
+ export declare const oneFastCheckInstanceGuard: () => void;
@@ -0,0 +1,26 @@
1
+ import * as viaEffect from 'effect/FastCheck';
2
+ import * as direct from 'fast-check';
3
+ import { describe, expect, it } from 'vitest';
4
+ /**
5
+ * Registers a vitest suite that fails when the property library resolves to
6
+ * two module instances.
7
+ *
8
+ * Registers a vitest suite that fails if the direct `fast-check` import and
9
+ * `effect/FastCheck` resolve to two separate module instances — a split that
10
+ * silently breaks arbitraries built from one being usable in a property built
11
+ * from the other. Call it from a consumer's `*.test.ts` file that depends on
12
+ * both `effect` and `fast-check`.
13
+ */
14
+ export const oneFastCheckInstanceGuard = () => {
15
+ describe('machinery: fast-check lives in a single instance', () => {
16
+ it('the direct import and effect/FastCheck are the same module', () => {
17
+ expect(direct.integer).toBe(viaEffect.integer);
18
+ expect(direct.bigInt).toBe(viaEffect.bigInt);
19
+ expect(direct.property).toBe(viaEffect.property);
20
+ });
21
+ it('an arbitrary from the direct import works in an effect property', () => {
22
+ const outcome = viaEffect.check(viaEffect.property(direct.bigInt({ min: -8n, max: 8n }), (value) => typeof value === 'bigint'), { numRuns: 25 });
23
+ expect(outcome.failed).toBe(false);
24
+ });
25
+ });
26
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Public entry to the vitest mechanism: live stands, port contracts, and
3
+ * the guards that read the test runtime. It gives you fixtures tied to the
4
+ * test runner — suites on live stands (Postgres and the localstack AWS
5
+ * stand, both in containers), port contracts, the settings contract, a stub
6
+ * HTTP server, and the three guards that hold the runtime this package
7
+ * loads. The guards that hold a tree to its written canon ship separately,
8
+ * as `@satorio/canon`.
9
+ */
10
+ export { oneEffectInstanceGuard } from './effect-single-instance.js';
11
+ export { oneFastCheckInstanceGuard } from './fast-check-single-instance.js';
12
+ export { type AwsStand, LOCALSTACK_REGION, onLocalstack, } from './localstack-container.js';
13
+ export type { PortContractTester } from './port-contract-tester.js';
14
+ export { blobStoreContract, storedBlob, } from './port-contracts/blob-store.contract.js';
15
+ export { documentNumberCounterContract } from './port-contracts/document-number-counter.contract.js';
16
+ export { eventPublisherContract, outboxEvent, } from './port-contracts/event-publisher.contract.js';
17
+ export { externalHttpClientContract } from './port-contracts/external-http-client.contract.js';
18
+ export { integrationEventOutboxContract, outboxMessage, } from './port-contracts/integration-event-outbox.contract.js';
19
+ export { processedEventRegistryContract } from './port-contracts/processed-event-registry.contract.js';
20
+ export { queueMessageProducerContract } from './port-contracts/queue-message-producer.contract.js';
21
+ export { queueMessageSourceContract } from './port-contracts/queue-message-source.contract.js';
22
+ export { readinessProbeContract } from './port-contracts/readiness-probe.contract.js';
23
+ export { transactionBoundaryContract } from './port-contracts/transaction-boundary.contract.js';
24
+ export { onPostgres, onPostgresContainer, POSTGRES_IMAGE, PostgresPortsUnderTestLive, postgresInContainerLive, postgresUnderTestLive, } from './postgres-container.js';
25
+ export { type ReaderPortUnderTest, readerPortContract, } from './reader-port-contract.js';
26
+ export { repoCommittableFiles, repoFilesUnder, repoRoot, } from './repo-source-files.js';
27
+ export { truncateTables } from './reset-tables.js';
28
+ export { settingsContract } from './settings-contract.js';
29
+ export { type RecordedStubRequest, type StubResponse, type StubRoute, scopedStubServer, } from './stub-server.js';
30
+ export { FILES_THAT_FORK_A_FIBER_AND_ADJUST_TEST_CLOCK } from './testclock-fork-isolated-files.js';
31
+ export { testclockForkIsolationGuard } from './testclock-fork-isolation.js';
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Public entry to the vitest mechanism: live stands, port contracts, and
3
+ * the guards that read the test runtime. It gives you fixtures tied to the
4
+ * test runner — suites on live stands (Postgres and the localstack AWS
5
+ * stand, both in containers), port contracts, the settings contract, a stub
6
+ * HTTP server, and the three guards that hold the runtime this package
7
+ * loads. The guards that hold a tree to its written canon ship separately,
8
+ * as `@satorio/canon`.
9
+ */
10
+ export { oneEffectInstanceGuard } from './effect-single-instance.js';
11
+ export { oneFastCheckInstanceGuard } from './fast-check-single-instance.js';
12
+ export { LOCALSTACK_REGION, onLocalstack, } from './localstack-container.js';
13
+ export { blobStoreContract, storedBlob, } from './port-contracts/blob-store.contract.js';
14
+ export { documentNumberCounterContract } from './port-contracts/document-number-counter.contract.js';
15
+ export { eventPublisherContract, outboxEvent, } from './port-contracts/event-publisher.contract.js';
16
+ export { externalHttpClientContract } from './port-contracts/external-http-client.contract.js';
17
+ export { integrationEventOutboxContract, outboxMessage, } from './port-contracts/integration-event-outbox.contract.js';
18
+ export { processedEventRegistryContract } from './port-contracts/processed-event-registry.contract.js';
19
+ export { queueMessageProducerContract } from './port-contracts/queue-message-producer.contract.js';
20
+ export { queueMessageSourceContract } from './port-contracts/queue-message-source.contract.js';
21
+ export { readinessProbeContract } from './port-contracts/readiness-probe.contract.js';
22
+ export { transactionBoundaryContract } from './port-contracts/transaction-boundary.contract.js';
23
+ export { onPostgres, onPostgresContainer, POSTGRES_IMAGE, PostgresPortsUnderTestLive, postgresInContainerLive, postgresUnderTestLive, } from './postgres-container.js';
24
+ export { readerPortContract, } from './reader-port-contract.js';
25
+ export { repoCommittableFiles, repoFilesUnder, repoRoot, } from './repo-source-files.js';
26
+ export { truncateTables } from './reset-tables.js';
27
+ export { settingsContract } from './settings-contract.js';
28
+ export { scopedStubServer, } from './stub-server.js';
29
+ export { FILES_THAT_FORK_A_FIBER_AND_ADJUST_TEST_CLOCK } from './testclock-fork-isolated-files.js';
30
+ export { testclockForkIsolationGuard } from './testclock-fork-isolation.js';
@@ -0,0 +1,32 @@
1
+ import { type Vitest } from '@effect/vitest';
2
+ /**
3
+ * The one region every client and resource in a suite agrees on. The
4
+ * value itself is arbitrary; what matters is that nothing in the suite
5
+ * reaches for a second one.
6
+ */
7
+ export declare const LOCALSTACK_REGION = "eu-central-1";
8
+ type SuiteChecks = (it: Vitest.MethodsNonLive<never, true>) => void;
9
+ /**
10
+ * The address and the region of the live AWS stand a suite talks to.
11
+ */
12
+ export interface AwsStand {
13
+ readonly endpoint: string;
14
+ readonly region: string;
15
+ }
16
+ /**
17
+ * `prepare` sets up the suite's own AWS resources (bucket, topic, queue) and
18
+ * returns the environment variables the machinery needs to find them. If
19
+ * setup fails, the suite fails too.
20
+ *
21
+ * @internal
22
+ */
23
+ export interface LocalstackSuiteOptions {
24
+ readonly prepare: (stand: AwsStand) => Promise<ReadonlyMap<string, string>>;
25
+ }
26
+ /**
27
+ * Runs a suite against a real AWS stand (localstack) for `subject`. Pass
28
+ * `checks` to declare the tests, and `options.prepare` to seed AWS resources
29
+ * before they run.
30
+ */
31
+ export declare const onLocalstack: (options: LocalstackSuiteOptions, subject: string, checks: SuiteChecks) => void;
32
+ export {};
@@ -0,0 +1,55 @@
1
+ import { layer } from '@effect/vitest';
2
+ import { LocalstackContainer } from '@testcontainers/localstack';
3
+ import { Config, ConfigProvider, Duration, Effect, Layer, Option, } from 'effect';
4
+ import { standsMayBeReused } from './container-reuse.js';
5
+ const LOCALSTACK_IMAGE = 'localstack/localstack:4.9';
6
+ /**
7
+ * The one region every client and resource in a suite agrees on. The
8
+ * value itself is arbitrary; what matters is that nothing in the suite
9
+ * reaches for a second one.
10
+ */
11
+ export const LOCALSTACK_REGION = 'eu-central-1';
12
+ const SUITE_STARTUP_BUDGET = Duration.minutes(5);
13
+ // @effect-diagnostics effect/processEnvInEffect:off
14
+ const ensureSdkCredentials = Effect.sync(() => {
15
+ delete process.env.AWS_PROFILE;
16
+ process.env.AWS_ACCESS_KEY_ID ??= 'test';
17
+ process.env.AWS_SECRET_ACCESS_KEY ??= 'test';
18
+ });
19
+ // @effect-diagnostics effect/processEnvInEffect:error
20
+ const REUSING_STANDS = standsMayBeReused();
21
+ const startedContainer = Effect.acquireRelease(Effect.promise(() => {
22
+ const container = new LocalstackContainer(LOCALSTACK_IMAGE);
23
+ return (REUSING_STANDS ? container.withReuse() : container).start();
24
+ }), (container) => REUSING_STANDS ? Effect.void : Effect.promise(() => container.stop()));
25
+ const containerStand = Effect.map(startedContainer, (container) => ({
26
+ endpoint: container.getConnectionUri(),
27
+ region: LOCALSTACK_REGION,
28
+ }));
29
+ const externallyConfiguredStand = Config.all({
30
+ endpoint: Config.string('AWS_ENDPOINT_URL'),
31
+ region: Config.string('AWS_REGION').pipe(Config.withDefault(LOCALSTACK_REGION)),
32
+ });
33
+ const standUnderTest = Effect.flatMap(Config.option(externallyConfiguredStand), Option.match({
34
+ onSome: (Effect.succeed),
35
+ onNone: () => containerStand,
36
+ }));
37
+ const configuredStand = (options) => Layer.unwrapScoped(Effect.gen(function* () {
38
+ yield* ensureSdkCredentials;
39
+ const stand = yield* standUnderTest;
40
+ const prepared = yield* Effect.promise(() => options.prepare(stand));
41
+ return Layer.setConfigProvider(ConfigProvider.fromMap(new Map([
42
+ ['AWS_ENDPOINT_URL', stand.endpoint],
43
+ ['AWS_REGION', stand.region],
44
+ ...prepared,
45
+ ])));
46
+ })).pipe(Layer.orDie);
47
+ /**
48
+ * Runs a suite against a real AWS stand (localstack) for `subject`. Pass
49
+ * `checks` to declare the tests, and `options.prepare` to seed AWS resources
50
+ * before they run.
51
+ */
52
+ export const onLocalstack = (options, subject, checks) => layer(configuredStand(options), {
53
+ excludeTestServices: true,
54
+ timeout: SUITE_STARTUP_BUDGET,
55
+ })(subject, checks);
@@ -0,0 +1,8 @@
1
+ import type { Vitest } from '@effect/vitest';
2
+ /**
3
+ * The subset of vitest test methods a port contract needs from its tester.
4
+ * Pass the tester that matches the runtime under test — plain `it` for
5
+ * in-memory implementations, or an `it.layer` suite's `it` for a live
6
+ * Postgres.
7
+ */
8
+ export type PortContractTester = Pick<Vitest.MethodsNonLive<never, true>, 'effect'>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { BlobStore, type StoredBlob } from '@satorio/machinery/ports';
2
+ import { type Layer } from 'effect';
3
+ import type { PortContractTester } from '../port-contract-tester.js';
4
+ /**
5
+ * One blob a contract stores and reads back, so every implementation is asked
6
+ * the same question.
7
+ */
8
+ export declare const storedBlob: (text: string) => StoredBlob;
9
+ /**
10
+ * One contract, every blob store. The same body runs against an
11
+ * in-memory store and a live S3 store, so a difference between them shows
12
+ * up as a red test, not a surprise in production. Hand it the port's
13
+ * unavailable stand-in as a fourth argument and it walks the refusal too:
14
+ * every operation fails, and the failure reaches the caller.
15
+ *
16
+ * What this contract cannot prove: that a real S3 answers an absent key
17
+ * the way the in-memory store does. Both sides of that promise are held
18
+ * here, and only the live run in
19
+ * `src/storage/ports.localstack.test.ts` proves the S3 half of
20
+ * it — the fake decides its own answer, the bucket does not.
21
+ */
22
+ export declare const blobStoreContract: (it: PortContractTester, implementationName: string, layer: Layer.Layer<BlobStore>, unavailable?: Layer.Layer<BlobStore>) => void;
@@ -0,0 +1,108 @@
1
+ import { describe, expect } from '@effect/vitest';
2
+ import { BlobStore } from '@satorio/machinery/ports';
3
+ import { Effect, Option } from 'effect';
4
+ const encoder = new TextEncoder();
5
+ /**
6
+ * One blob a contract stores and reads back, so every implementation is asked
7
+ * the same question.
8
+ */
9
+ export const storedBlob = (text) => ({
10
+ body: encoder.encode(text),
11
+ contentType: 'text/plain',
12
+ metadata: { origin: 'contract' },
13
+ });
14
+ /**
15
+ * One contract, every blob store. The same body runs against an
16
+ * in-memory store and a live S3 store, so a difference between them shows
17
+ * up as a red test, not a surprise in production. Hand it the port's
18
+ * unavailable stand-in as a fourth argument and it walks the refusal too:
19
+ * every operation fails, and the failure reaches the caller.
20
+ *
21
+ * What this contract cannot prove: that a real S3 answers an absent key
22
+ * the way the in-memory store does. Both sides of that promise are held
23
+ * here, and only the live run in
24
+ * `src/storage/ports.localstack.test.ts` proves the S3 half of
25
+ * it — the fake decides its own answer, the bucket does not.
26
+ */
27
+ export const blobStoreContract = (it, implementationName, layer, unavailable) => {
28
+ const run = (effect) => Effect.provide(effect, layer);
29
+ describe(`BlobStore contract (${implementationName})`, () => {
30
+ it.effect('what is put is read back the same: bytes, content type, metadata', () => run(Effect.gen(function* () {
31
+ const store = yield* BlobStore;
32
+ const blob = {
33
+ body: encoder.encode('hello, store'),
34
+ contentType: 'text/plain',
35
+ metadata: { origin: 'contract', kind: 'greeting' },
36
+ };
37
+ yield* store.put('blob-1', blob);
38
+ const fetched = yield* store.get('blob-1');
39
+ expect(Option.isSome(fetched)).toBe(true);
40
+ if (Option.isSome(fetched)) {
41
+ expect(fetched.value.body).toEqual(blob.body);
42
+ expect(fetched.value.contentType).toBe('text/plain');
43
+ expect(fetched.value.metadata).toEqual(blob.metadata);
44
+ }
45
+ })));
46
+ it.effect('an unknown key is Option.none, not a failure', () => run(Effect.gen(function* () {
47
+ const store = yield* BlobStore;
48
+ const fetched = yield* store.get('blob-never-put');
49
+ expect(Option.isNone(fetched)).toBe(true);
50
+ })));
51
+ it.effect('exists reports false before put and true after', () => run(Effect.gen(function* () {
52
+ const store = yield* BlobStore;
53
+ const before = yield* store.exists('blob-2');
54
+ yield* store.put('blob-2', storedBlob('appeared'));
55
+ const after = yield* store.exists('blob-2');
56
+ expect(before).toBe(false);
57
+ expect(after).toBe(true);
58
+ })));
59
+ it.effect('putting the same key again replaces the object', () => run(Effect.gen(function* () {
60
+ const store = yield* BlobStore;
61
+ yield* store.put('blob-3', storedBlob('first version'));
62
+ yield* store.put('blob-3', storedBlob('second version'));
63
+ const fetched = yield* store.get('blob-3');
64
+ expect(Option.isSome(fetched)).toBe(true);
65
+ if (Option.isSome(fetched)) {
66
+ expect(new TextDecoder().decode(fetched.value.body)).toBe('second version');
67
+ }
68
+ })));
69
+ it.effect('after delete, the key is gone', () => run(Effect.gen(function* () {
70
+ const store = yield* BlobStore;
71
+ yield* store.put('blob-4', storedBlob('temporary'));
72
+ yield* store.delete('blob-4');
73
+ const fetched = yield* store.get('blob-4');
74
+ const present = yield* store.exists('blob-4');
75
+ expect(Option.isNone(fetched)).toBe(true);
76
+ expect(present).toBe(false);
77
+ })));
78
+ it.effect('deleting an absent key does not fail', () => run(Effect.gen(function* () {
79
+ const store = yield* BlobStore;
80
+ yield* store.delete('blob-never-put');
81
+ })));
82
+ });
83
+ if (unavailable === undefined)
84
+ return;
85
+ describe(`BlobStore contract (${implementationName}, not mounted)`, () => {
86
+ it.effect('every operation fails, and the caller reads the refusal', () => Effect.gen(function* () {
87
+ const store = yield* BlobStore;
88
+ const refusals = yield* Effect.all([
89
+ Effect.flip(store.put('blob-5', storedBlob('never stored'))),
90
+ Effect.flip(store.get('blob-5')),
91
+ Effect.flip(store.exists('blob-5')),
92
+ Effect.flip(store.delete('blob-5')),
93
+ ]);
94
+ expect(refusals.map((refusal) => refusal._tag)).toEqual([
95
+ 'BlobStoreUnavailableError',
96
+ 'BlobStoreUnavailableError',
97
+ 'BlobStoreUnavailableError',
98
+ 'BlobStoreUnavailableError',
99
+ ]);
100
+ expect(refusals.map((refusal) => refusal.operation)).toEqual([
101
+ 'put',
102
+ 'get',
103
+ 'exists',
104
+ 'delete',
105
+ ]);
106
+ }).pipe(Effect.provide(unavailable)));
107
+ });
108
+ };
@@ -0,0 +1,18 @@
1
+ import { DocumentNumberCounter } from '@satorio/machinery/ports';
2
+ import { type Layer } from 'effect';
3
+ import type { PortContractTester } from '../port-contract-tester.js';
4
+ /**
5
+ * One contract, every counter implementation. The same body runs against
6
+ * an in-memory counter and a live Postgres counter, so a difference
7
+ * between them shows up as a red test rather than as two documents
8
+ * carrying one number. Hand it the port's unavailable stand-in as a
9
+ * fourth argument and it walks the refusal too.
10
+ *
11
+ * What this contract cannot prove: that two callers asking for the same
12
+ * client and month at the same instant get two different counts. The
13
+ * in-memory counter runs one fiber at a time over a `Ref`, so it cannot
14
+ * lose that race; only the live run in
15
+ * `src/postgres/ports.postgres.test.ts` puts a real row lock behind the
16
+ * count.
17
+ */
18
+ export declare const documentNumberCounterContract: (it: PortContractTester, implementationName: string, layer: Layer.Layer<DocumentNumberCounter>, unavailable?: Layer.Layer<DocumentNumberCounter>) => void;
@@ -0,0 +1,90 @@
1
+ import { describe, expect } from '@effect/vitest';
2
+ import { DocumentNumberCounter } from '@satorio/machinery/ports';
3
+ import { Effect } from 'effect';
4
+ const A_CLIENT = '41738297';
5
+ const ANOTHER_CLIENT = '55511122';
6
+ const SEPTEMBER = '2609';
7
+ const OCTOBER = '2610';
8
+ /**
9
+ * One contract, every counter implementation. The same body runs against
10
+ * an in-memory counter and a live Postgres counter, so a difference
11
+ * between them shows up as a red test rather than as two documents
12
+ * carrying one number. Hand it the port's unavailable stand-in as a
13
+ * fourth argument and it walks the refusal too.
14
+ *
15
+ * What this contract cannot prove: that two callers asking for the same
16
+ * client and month at the same instant get two different counts. The
17
+ * in-memory counter runs one fiber at a time over a `Ref`, so it cannot
18
+ * lose that race; only the live run in
19
+ * `src/postgres/ports.postgres.test.ts` puts a real row lock behind the
20
+ * count.
21
+ */
22
+ export const documentNumberCounterContract = (it, implementationName, layer, unavailable) => {
23
+ const run = (effect) => Effect.provide(effect, layer);
24
+ describe(`DocumentNumberCounter contract (${implementationName})`, () => {
25
+ it.effect('the first document of a month is the first one', () => run(Effect.gen(function* () {
26
+ const counter = yield* DocumentNumberCounter;
27
+ const count = yield* counter.nextCount({
28
+ clientCode: A_CLIENT,
29
+ reportingMonth: SEPTEMBER,
30
+ });
31
+ expect(count).toBe(1);
32
+ })));
33
+ it.effect('the next document of that month steps past it', () => run(Effect.gen(function* () {
34
+ const counter = yield* DocumentNumberCounter;
35
+ const input = {
36
+ clientCode: ANOTHER_CLIENT,
37
+ reportingMonth: SEPTEMBER,
38
+ };
39
+ const first = yield* counter.nextCount(input);
40
+ const second = yield* counter.nextCount(input);
41
+ const third = yield* counter.nextCount(input);
42
+ expect([first, second, third]).toEqual([1, 2, 3]);
43
+ })));
44
+ it.effect('a new month starts its own count', () => run(Effect.gen(function* () {
45
+ const counter = yield* DocumentNumberCounter;
46
+ yield* counter.nextCount({
47
+ clientCode: A_CLIENT,
48
+ reportingMonth: OCTOBER,
49
+ });
50
+ yield* counter.nextCount({
51
+ clientCode: A_CLIENT,
52
+ reportingMonth: OCTOBER,
53
+ });
54
+ const november = yield* counter.nextCount({
55
+ clientCode: A_CLIENT,
56
+ reportingMonth: '2611',
57
+ });
58
+ expect(november).toBe(1);
59
+ })));
60
+ it.effect('one client never moves another client count', () => run(Effect.gen(function* () {
61
+ const counter = yield* DocumentNumberCounter;
62
+ yield* counter.nextCount({
63
+ clientCode: A_CLIENT,
64
+ reportingMonth: '2612',
65
+ });
66
+ yield* counter.nextCount({
67
+ clientCode: A_CLIENT,
68
+ reportingMonth: '2612',
69
+ });
70
+ const other = yield* counter.nextCount({
71
+ clientCode: ANOTHER_CLIENT,
72
+ reportingMonth: '2612',
73
+ });
74
+ expect(other).toBe(1);
75
+ })));
76
+ });
77
+ if (unavailable === undefined)
78
+ return;
79
+ describe(`DocumentNumberCounter contract (${implementationName}, not mounted)`, () => {
80
+ it.effect('the count fails, and the caller reads the refusal', () => Effect.gen(function* () {
81
+ const counter = yield* DocumentNumberCounter;
82
+ const refusal = yield* Effect.flip(counter.nextCount({
83
+ clientCode: A_CLIENT,
84
+ reportingMonth: SEPTEMBER,
85
+ }));
86
+ expect(refusal._tag).toBe('DocumentNumberCounterUnavailableError');
87
+ expect(refusal.operation).toBe('nextCount');
88
+ }).pipe(Effect.provide(unavailable)));
89
+ });
90
+ };
@@ -0,0 +1,35 @@
1
+ import { EventPublisher, type OutboxMessage } from '@satorio/machinery/ports';
2
+ import { Effect, type Layer } from 'effect';
3
+ import type { PortContractTester } from '../port-contract-tester.js';
4
+ /**
5
+ * One message, built the way an outbox hands it over.
6
+ */
7
+ export declare const outboxEvent: (messageId: string, payload: {
8
+ readonly [key: string]: unknown;
9
+ }) => OutboxMessage;
10
+ /**
11
+ * One publisher, described the way a contract can hold it: the layer
12
+ * that mounts the port, and the one call that reads back the message ids
13
+ * that reached whatever stands behind it.
14
+ *
15
+ * @internal
16
+ */
17
+ export interface PublisherUnderTest {
18
+ readonly layer: Layer.Layer<EventPublisher>;
19
+ readonly delivered: Effect.Effect<readonly string[]>;
20
+ }
21
+ /**
22
+ * One contract, every event publisher. The same body runs against an
23
+ * in-memory publisher and a live SNS topic, so a difference between them
24
+ * shows up as a red test rather than as a surprise in production. Hand
25
+ * it the port's unavailable stand-in as a fourth argument and it walks
26
+ * the refusal too.
27
+ *
28
+ * What this contract cannot prove: that a subscriber reads the envelope
29
+ * the publisher wrote. The shape on the wire is each implementation's
30
+ * own — `stdoutLineOf` carries a channel, `snsMessageBodyOf` does not —
31
+ * so the live test in `src/publishing/ports.localstack.test.ts` owns
32
+ * that branch, reading the envelope back off a queue subscribed to the
33
+ * topic.
34
+ */
35
+ export declare const eventPublisherContract: (it: PortContractTester, implementationName: string, publisher: PublisherUnderTest, unavailable?: Layer.Layer<EventPublisher>) => void;