@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,72 @@
1
+ import { BunFileSystem } from '@effect/platform-bun';
2
+ import { SqlClient } from '@effect/sql';
3
+ import { PgClient } from '@effect/sql-pg';
4
+ import { DocumentNumberCounter, IntegrationEventOutbox, ProcessedEventRegistry, ReadinessProbe, TransactionBoundary, } from '@satorio/machinery/ports';
5
+ import { Effect, Layer } from 'effect';
6
+ import { poolOptionsFor, scopedPostgresTransport, sslOptionsFor, } from './client.js';
7
+ import { makePgDocumentNumberCounter } from './document-number-counter.js';
8
+ import { makePgIntegrationEventOutbox } from './integration-event-outbox.js';
9
+ import { MIGRATION_APPLICATION_NAME_SUFFIX, migrationPoolOptionsFor, } from './migrations/runner.js';
10
+ import { makePgProcessedEventRegistry } from './processed-event-registry.js';
11
+ import { readinessProbeAsking } from './readiness-probe.js';
12
+ import { postgresConnectionSettings } from './settings.js';
13
+ import { makePgTransactionBoundary } from './transaction-boundary.js';
14
+ const acquireDatabasePool = scopedPostgresTransport(poolOptionsFor, 'Could not open a connection pool to the database');
15
+ const acquireMigrationPool = scopedPostgresTransport(migrationPoolOptionsFor, 'Could not open a connection pool for the migration runner');
16
+ /**
17
+ * The `@effect/sql-pg` client every other layer here builds on, opened
18
+ * from the environment `settings.ts` names.
19
+ */
20
+ export const PostgresClientLive = Layer.unwrapEffect(Effect.gen(function* () {
21
+ const settings = yield* postgresConnectionSettings;
22
+ const ssl = yield* sslOptionsFor(settings);
23
+ return PgClient.layerFromPool({
24
+ acquire: acquireDatabasePool(settings, ssl),
25
+ applicationName: settings.applicationName,
26
+ });
27
+ }));
28
+ /**
29
+ * The client the migration runner uses: one connection, under its own name.
30
+ *
31
+ * The same client for the migration runner: one connection, and its own name
32
+ * in `pg_stat_activity`, so a long migration is easy to spot next to the
33
+ * service's own pool.
34
+ */
35
+ export const MigrationPgClientLive = Layer.unwrapEffect(Effect.gen(function* () {
36
+ const settings = yield* postgresConnectionSettings;
37
+ const ssl = yield* sslOptionsFor(settings);
38
+ return PgClient.layerFromPool({
39
+ acquire: acquireMigrationPool(settings, ssl),
40
+ applicationName: `${settings.applicationName}${MIGRATION_APPLICATION_NAME_SUFFIX}`,
41
+ });
42
+ }));
43
+ /**
44
+ * The transaction boundary on a real Postgres transaction.
45
+ */
46
+ export const PgTransactionBoundaryLive = Layer.effect(TransactionBoundary, makePgTransactionBoundary);
47
+ /**
48
+ * The exactly-once claim on a real `processed_event` table.
49
+ */
50
+ export const PgProcessedEventRegistryLive = Layer.effect(ProcessedEventRegistry, makePgProcessedEventRegistry);
51
+ /**
52
+ * The document number counter on a real `document_number_counter` table.
53
+ */
54
+ export const PgDocumentNumberCounterLive = Layer.effect(DocumentNumberCounter, makePgDocumentNumberCounter);
55
+ /**
56
+ * The outbox on a real `integration_event_outbox` table.
57
+ */
58
+ export const PgIntegrationEventOutboxLive = Layer.effect(IntegrationEventOutbox, makePgIntegrationEventOutbox);
59
+ /**
60
+ * The readiness probe on one `SELECT 1` against the service's own pool.
61
+ */
62
+ export const PgReadinessProbeLive = Layer.effect(ReadinessProbe, Effect.map(SqlClient.SqlClient, (sql) => readinessProbeAsking(sql `SELECT 1`)));
63
+ /**
64
+ * Every port this mechanism implements, on one client, for a service
65
+ * that supplies its own `FileSystem`.
66
+ */
67
+ export const PostgresPortsLive = Layer.mergeAll(PgProcessedEventRegistryLive, PgIntegrationEventOutboxLive, PgDocumentNumberCounterLive, PgTransactionBoundaryLive, PgReadinessProbeLive).pipe(Layer.provideMerge(PostgresClientLive));
68
+ /**
69
+ * The same bundle on Bun's own file system, which is the whole Postgres
70
+ * side of the machinery in one layer.
71
+ */
72
+ export const PostgresPortsOnBunLive = PostgresPortsLive.pipe(Layer.provide(BunFileSystem.layer));
@@ -0,0 +1,47 @@
1
+ import { SqlClient, type SqlError } from '@effect/sql';
2
+ import { Effect, Schedule } from 'effect';
3
+ import { MigrationLockBusyError } from '../failures.js';
4
+ /**
5
+ * The lowest int4 Postgres accepts, which is the floor of both halves of
6
+ * an advisory-lock key.
7
+ *
8
+ * @internal
9
+ */
10
+ export declare const POSTGRES_INTEGER_MIN = -2147483648;
11
+ /**
12
+ * The highest int4 Postgres accepts, which is the ceiling of both halves
13
+ * of an advisory-lock key.
14
+ *
15
+ * @internal
16
+ */
17
+ export declare const POSTGRES_INTEGER_MAX = 2147483647;
18
+ /**
19
+ * A lock somebody else may already hold: its name, the attempt to take
20
+ * it, and the release that follows.
21
+ *
22
+ * @internal
23
+ */
24
+ export interface AdvisoryLock<E, R> {
25
+ readonly name: string;
26
+ readonly tryAcquire: Effect.Effect<boolean, E, R>;
27
+ readonly release: Effect.Effect<void, E, R>;
28
+ }
29
+ /**
30
+ * Turns a lock name into the pair of int4 keys Postgres advisory locks
31
+ * need. Two different hashes of the same name form the two halves, so a
32
+ * collision needs both halves to match.
33
+ *
34
+ * @internal
35
+ */
36
+ export declare const advisoryLockKeyFor: (name: string) => readonly [number, number];
37
+ /**
38
+ * Runs an effect while holding the lock, waiting on the given schedule
39
+ * while somebody else holds it, and letting go however the effect ends.
40
+ */
41
+ export declare const holdingAdvisoryLock: <ELock, RLock, ScheduleOut, RSchedule>(lock: AdvisoryLock<ELock, RLock>, retryWhileBusy: Schedule.Schedule<ScheduleOut, unknown, RSchedule>) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | ELock | MigrationLockBusyError, R | RLock | RSchedule>;
42
+ /**
43
+ * A named advisory lock on the connection's own database. The name is
44
+ * hashed into the pair of int4 keys Postgres wants, so two names collide
45
+ * only when both halves do.
46
+ */
47
+ export declare const postgresAdvisoryLock: (name: string) => AdvisoryLock<SqlError.SqlError, SqlClient.SqlClient>;
@@ -0,0 +1,59 @@
1
+ import { SqlClient } from '@effect/sql';
2
+ import { Effect, Schedule } from 'effect';
3
+ import { MigrationLockBusyError } from '../failures.js';
4
+ /**
5
+ * The lowest int4 Postgres accepts, which is the floor of both halves of
6
+ * an advisory-lock key.
7
+ *
8
+ * @internal
9
+ */
10
+ export const POSTGRES_INTEGER_MIN = -2_147_483_648;
11
+ /**
12
+ * The highest int4 Postgres accepts, which is the ceiling of both halves
13
+ * of an advisory-lock key.
14
+ *
15
+ * @internal
16
+ */
17
+ export const POSTGRES_INTEGER_MAX = 2_147_483_647;
18
+ const FNV_OFFSET_BASIS = 0x811c9dc5;
19
+ const FNV_PRIME = 0x01000193;
20
+ const LOWER_HALF_SALT = 'postgres-advisory-lock';
21
+ const fnv1a32 = (text) => {
22
+ let hash = FNV_OFFSET_BASIS;
23
+ for (let index = 0; index < text.length; index += 1) {
24
+ hash = Math.imul(hash ^ text.charCodeAt(index), FNV_PRIME);
25
+ }
26
+ return hash | 0;
27
+ };
28
+ /**
29
+ * Turns a lock name into the pair of int4 keys Postgres advisory locks
30
+ * need. Two different hashes of the same name form the two halves, so a
31
+ * collision needs both halves to match.
32
+ *
33
+ * @internal
34
+ */
35
+ export const advisoryLockKeyFor = (name) => [
36
+ fnv1a32(name),
37
+ fnv1a32(`${LOWER_HALF_SALT}:${name}`),
38
+ ];
39
+ const errorIsLockBusy = (error) => error instanceof MigrationLockBusyError;
40
+ /**
41
+ * Runs an effect while holding the lock, waiting on the given schedule
42
+ * while somebody else holds it, and letting go however the effect ends.
43
+ */
44
+ export const holdingAdvisoryLock = (lock, retryWhileBusy) => (effect) => Effect.acquireUseRelease(Effect.retry(Effect.flatMap(lock.tryAcquire, (acquired) => acquired
45
+ ? Effect.void
46
+ : Effect.fail(new MigrationLockBusyError({ lockName: lock.name }))), Schedule.whileInput(retryWhileBusy, errorIsLockBusy)), () => effect, () => Effect.orDie(lock.release));
47
+ /**
48
+ * A named advisory lock on the connection's own database. The name is
49
+ * hashed into the pair of int4 keys Postgres wants, so two names collide
50
+ * only when both halves do.
51
+ */
52
+ export const postgresAdvisoryLock = (name) => {
53
+ const [upperHalf, lowerHalf] = advisoryLockKeyFor(name);
54
+ return {
55
+ name,
56
+ tryAcquire: Effect.flatMap(SqlClient.SqlClient, (sql) => sql `SELECT pg_try_advisory_lock(${upperHalf}, ${lowerHalf}) AS acquired`.pipe(Effect.map((rows) => rows[0]?.acquired === true))),
57
+ release: Effect.flatMap(SqlClient.SqlClient, (sql) => Effect.asVoid(sql `SELECT pg_advisory_unlock(${upperHalf}, ${lowerHalf})`)),
58
+ };
59
+ };
@@ -0,0 +1,56 @@
1
+ import type { ConnectionOptions } from 'node:tls';
2
+ import { Migrator, SqlClient } from '@effect/sql';
3
+ import type { SqlError } from '@effect/sql/SqlError';
4
+ import { Effect, Schedule } from 'effect';
5
+ import type { PoolConfig } from 'pg';
6
+ import { type PostgresConnectionSettings } from '../settings.js';
7
+ /**
8
+ * Directory holding the machinery's shared migrations: the processed-event
9
+ * registry and the outbox. The machinery reserves numbers 1-99; a service's
10
+ * own migrations start at 100.
11
+ */
12
+ export declare const MACHINERY_MIGRATIONS_DIRECTORY: string;
13
+ /**
14
+ * What the migration runner adds to the application name, so a long
15
+ * migration is easy to tell from the service's own pool in
16
+ * `pg_stat_activity`.
17
+ *
18
+ * @internal
19
+ */
20
+ export declare const MIGRATION_APPLICATION_NAME_SUFFIX = "-migrate";
21
+ /**
22
+ * The name every migration lock is built from, so one reader finds them all.
23
+ */
24
+ export declare const MIGRATION_LOCK_NAME = "migrations";
25
+ /**
26
+ * The lock one schema's migrations take, so two services migrating two
27
+ * schemas never wait on each other.
28
+ */
29
+ export declare const migrationLockNameFor: (schema: string) => string;
30
+ /**
31
+ * Retry schedule for the migration advisory lock. A second process does
32
+ * not fail or run in parallel: it waits up to five minutes for the first
33
+ * process to finish.
34
+ *
35
+ * @internal
36
+ */
37
+ export declare const MIGRATION_LOCK_RETRY: Schedule.Schedule<[number, number], unknown, never>;
38
+ /**
39
+ * Builds pool options for the migration runner: one process, one
40
+ * connection. Migrations run one after another, so a pool of two would
41
+ * only confuse the picture in `pg_stat_activity`.
42
+ *
43
+ * @internal
44
+ */
45
+ export declare const migrationPoolOptionsFor: (settings: PostgresConnectionSettings, ssl: boolean | ConnectionOptions) => PoolConfig;
46
+ /**
47
+ * Applies pending migrations from the given directories, the machinery's
48
+ * own directory first, then the caller's. Creates the schema before
49
+ * running any migration, since the first migration already writes to it.
50
+ */
51
+ export declare const applyMigrations: (directories?: readonly string[]) => Effect.Effect<readonly (readonly [id: number, name: string])[], import("effect/ConfigError").ConfigError | SqlError | import("..").MigrationLockBusyError | Migrator.MigrationError, import("@effect/platform/FileSystem").FileSystem | SqlClient.SqlClient>;
52
+ /**
53
+ * Says in one line what the run applied, or that the database was
54
+ * already current.
55
+ */
56
+ export declare const reportApplied: (applied: ReadonlyArray<readonly [number, string]>) => Effect.Effect<void>;
@@ -0,0 +1,69 @@
1
+ import { Migrator, SqlClient } from '@effect/sql';
2
+ import { Duration, Effect, Schedule } from 'effect';
3
+ import { poolOptionsFor } from '../client.js';
4
+ import { postgresConnectionSettings, } from '../settings.js';
5
+ import { holdingAdvisoryLock, postgresAdvisoryLock } from './advisory-lock.js';
6
+ import { sqlMigrationsIn } from './sql-file-loader.js';
7
+ /**
8
+ * Directory holding the machinery's shared migrations: the processed-event
9
+ * registry and the outbox. The machinery reserves numbers 1-99; a service's
10
+ * own migrations start at 100.
11
+ */
12
+ export const MACHINERY_MIGRATIONS_DIRECTORY = `${import.meta.dirname}/../sql`;
13
+ /**
14
+ * What the migration runner adds to the application name, so a long
15
+ * migration is easy to tell from the service's own pool in
16
+ * `pg_stat_activity`.
17
+ *
18
+ * @internal
19
+ */
20
+ export const MIGRATION_APPLICATION_NAME_SUFFIX = '-migrate';
21
+ /**
22
+ * The name every migration lock is built from, so one reader finds them all.
23
+ */
24
+ export const MIGRATION_LOCK_NAME = 'migrations';
25
+ /**
26
+ * The lock one schema's migrations take, so two services migrating two
27
+ * schemas never wait on each other.
28
+ */
29
+ export const migrationLockNameFor = (schema) => `${MIGRATION_LOCK_NAME}:${schema}`;
30
+ /**
31
+ * Retry schedule for the migration advisory lock. A second process does
32
+ * not fail or run in parallel: it waits up to five minutes for the first
33
+ * process to finish.
34
+ *
35
+ * @internal
36
+ */
37
+ export const MIGRATION_LOCK_RETRY = Schedule.spaced(Duration.seconds(2)).pipe(Schedule.intersect(Schedule.recurs(150)));
38
+ /**
39
+ * Builds pool options for the migration runner: one process, one
40
+ * connection. Migrations run one after another, so a pool of two would
41
+ * only confuse the picture in `pg_stat_activity`.
42
+ *
43
+ * @internal
44
+ */
45
+ export const migrationPoolOptionsFor = (settings, ssl) => ({
46
+ ...poolOptionsFor(settings, ssl),
47
+ application_name: `${settings.applicationName}${MIGRATION_APPLICATION_NAME_SUFFIX}`,
48
+ max: 1,
49
+ min: 1,
50
+ });
51
+ const ensureSchemaExists = (schema) => Effect.flatMap(SqlClient.SqlClient, (sql) => Effect.asVoid(sql `CREATE SCHEMA IF NOT EXISTS ${sql(schema)}`));
52
+ /**
53
+ * Applies pending migrations from the given directories, the machinery's
54
+ * own directory first, then the caller's. Creates the schema before
55
+ * running any migration, since the first migration already writes to it.
56
+ */
57
+ export const applyMigrations = (directories = [MACHINERY_MIGRATIONS_DIRECTORY]) => Effect.flatMap(postgresConnectionSettings, (settings) => Effect.gen(function* () {
58
+ yield* ensureSchemaExists(settings.schema);
59
+ return yield* Migrator.make({})({
60
+ loader: sqlMigrationsIn(directories),
61
+ });
62
+ }).pipe(holdingAdvisoryLock(postgresAdvisoryLock(migrationLockNameFor(settings.schema)), MIGRATION_LOCK_RETRY)));
63
+ /**
64
+ * Says in one line what the run applied, or that the database was
65
+ * already current.
66
+ */
67
+ export const reportApplied = (applied) => applied.length === 0
68
+ ? Effect.logInfo('Migrations: nothing new, the database is already current')
69
+ : Effect.logInfo(`Migrations applied: ${applied.map(([id, name]) => `${id}_${name}`).join(', ')}`);
@@ -0,0 +1,59 @@
1
+ import { FileSystem } from '@effect/platform';
2
+ import { Migrator, SqlClient, type SqlError } from '@effect/sql';
3
+ import { Effect } from 'effect';
4
+ /**
5
+ * What a migration file is called: a number, an underscore, a name, and
6
+ * `.sql`.
7
+ *
8
+ * @internal
9
+ */
10
+ export declare const SQL_MIGRATION_FILE_NAME: RegExp;
11
+ /**
12
+ * One migration's body, ready to run against the client.
13
+ *
14
+ * @internal
15
+ */
16
+ export type SqlMigrationExecution = Effect.Effect<ReadonlyArray<object>, SqlError.SqlError, SqlClient.SqlClient>;
17
+ /**
18
+ * One migration in the shape `@effect/sql`'s migrator loads.
19
+ *
20
+ * @internal
21
+ */
22
+ export type ResolvedSqlMigration = readonly [
23
+ id: number,
24
+ name: string,
25
+ load: Effect.Effect<SqlMigrationExecution>
26
+ ];
27
+ /**
28
+ * One migration found on disk, with the body already read.
29
+ *
30
+ * @internal
31
+ */
32
+ export interface DiscoveredSqlMigration {
33
+ readonly id: number;
34
+ readonly name: string;
35
+ readonly fileName: string;
36
+ readonly body: string;
37
+ }
38
+ /**
39
+ * Discovers migrations across one or more directories: the machinery's own
40
+ * shared tables and a service's own. The migration number must be unique
41
+ * across all directories, or the apply order would differ between
42
+ * environments — the machinery holds numbers 1-99, a service starts at 100.
43
+ *
44
+ * @internal
45
+ */
46
+ export declare const discoverSqlMigrationsIn: (directories: readonly string[]) => Effect.Effect<ReadonlyArray<DiscoveredSqlMigration>, Migrator.MigrationError, FileSystem.FileSystem>;
47
+ /**
48
+ * Runs one migration body as a single statement batch.
49
+ *
50
+ * @internal
51
+ */
52
+ export declare const executeSqlBody: (body: string) => SqlMigrationExecution;
53
+ /**
54
+ * The loader `@effect/sql`'s migrator takes: every migration of every
55
+ * directory, oldest number first.
56
+ *
57
+ * @internal
58
+ */
59
+ export declare const sqlMigrationsIn: (directories: readonly string[]) => Effect.Effect<ReadonlyArray<ResolvedSqlMigration>, Migrator.MigrationError, FileSystem.FileSystem>;
@@ -0,0 +1,99 @@
1
+ import { FileSystem } from '@effect/platform';
2
+ import { Migrator, SqlClient } from '@effect/sql';
3
+ import { Effect } from 'effect';
4
+ /**
5
+ * What a migration file is called: a number, an underscore, a name, and
6
+ * `.sql`.
7
+ *
8
+ * @internal
9
+ */
10
+ export const SQL_MIGRATION_FILE_NAME = /^(\d+)_([^.]+)\.sql$/;
11
+ const directoryIsUnreadable = (directory, cause) => new Migrator.MigrationError({
12
+ cause,
13
+ reason: 'failed',
14
+ message: `Migration directory ${directory} is unreadable`,
15
+ });
16
+ const fileIsUnreadable = (path, cause) => new Migrator.MigrationError({
17
+ cause,
18
+ reason: 'failed',
19
+ message: `Migration file ${path} is unreadable`,
20
+ });
21
+ const fileNameBreaksTheNamingRule = (fileName) => new Migrator.MigrationError({
22
+ reason: 'failed',
23
+ message: `File ${fileName} sits in the migrations directory but isn't a ` +
24
+ `migration: the name must look like <number>_<name>.sql`,
25
+ });
26
+ const migrationNumberIsNotUnique = (clashes) => new Migrator.MigrationError({
27
+ reason: 'duplicates',
28
+ message: `Migration number isn't unique, no migration was applied: ` +
29
+ clashes
30
+ .map(([id, fileNames]) => `${id} - ${fileNames.join(', ')}`)
31
+ .join('; '),
32
+ });
33
+ const nameFileOrRefuse = (directory, fileName) => {
34
+ const parts = SQL_MIGRATION_FILE_NAME.exec(fileName);
35
+ const number = parts?.[1];
36
+ const name = parts?.[2];
37
+ return number === undefined || name === undefined
38
+ ? Effect.fail(fileNameBreaksTheNamingRule(`${directory}/${fileName}`))
39
+ : Effect.succeed({
40
+ id: Number(number),
41
+ name,
42
+ fileName: `${directory}/${fileName}`,
43
+ });
44
+ };
45
+ const refuseRepeatedMigrationNumbers = (migrations) => {
46
+ const fileNamesByNumber = new Map();
47
+ for (const migration of migrations) {
48
+ fileNamesByNumber.set(migration.id, [
49
+ ...(fileNamesByNumber.get(migration.id) ?? []),
50
+ migration.fileName,
51
+ ]);
52
+ }
53
+ const clashes = [...fileNamesByNumber.entries()]
54
+ .filter(([, fileNames]) => fileNames.length > 1)
55
+ .map(([id, fileNames]) => [id, [...fileNames].sort()]);
56
+ return clashes.length === 0
57
+ ? Effect.void
58
+ : Effect.fail(migrationNumberIsNotUnique(clashes));
59
+ };
60
+ const byAscendingMigrationNumber = (left, right) => left.id - right.id;
61
+ const namedFilesIn = (directory) => Effect.gen(function* () {
62
+ const fileSystem = yield* FileSystem.FileSystem;
63
+ const entries = yield* fileSystem
64
+ .readDirectory(directory)
65
+ .pipe(Effect.mapError((cause) => directoryIsUnreadable(directory, cause)));
66
+ return yield* Effect.forEach(entries, (fileName) => nameFileOrRefuse(directory, fileName));
67
+ });
68
+ /**
69
+ * Discovers migrations across one or more directories: the machinery's own
70
+ * shared tables and a service's own. The migration number must be unique
71
+ * across all directories, or the apply order would differ between
72
+ * environments — the machinery holds numbers 1-99, a service starts at 100.
73
+ *
74
+ * @internal
75
+ */
76
+ export const discoverSqlMigrationsIn = (directories) => Effect.gen(function* () {
77
+ const fileSystem = yield* FileSystem.FileSystem;
78
+ const perDirectory = yield* Effect.forEach(directories, namedFilesIn);
79
+ const named = perDirectory.flat();
80
+ yield* refuseRepeatedMigrationNumbers(named);
81
+ return yield* Effect.forEach([...named].sort(byAscendingMigrationNumber), (migration) => fileSystem.readFileString(migration.fileName).pipe(Effect.mapError((cause) => fileIsUnreadable(migration.fileName, cause)), Effect.map((body) => ({ ...migration, body }))));
82
+ });
83
+ /**
84
+ * Runs one migration body as a single statement batch.
85
+ *
86
+ * @internal
87
+ */
88
+ export const executeSqlBody = (body) => Effect.flatMap(SqlClient.SqlClient, (sql) => sql.unsafe(body));
89
+ /**
90
+ * The loader `@effect/sql`'s migrator takes: every migration of every
91
+ * directory, oldest number first.
92
+ *
93
+ * @internal
94
+ */
95
+ export const sqlMigrationsIn = (directories) => Effect.map(discoverSqlMigrationsIn(directories), (migrations) => migrations.map((migration) => [
96
+ migration.id,
97
+ migration.name,
98
+ Effect.succeed(executeSqlBody(migration.body)),
99
+ ]));
@@ -0,0 +1,13 @@
1
+ import { PgClient } from '@effect/sql-pg';
2
+ import { type ClaimProcessedEventInput, ProcessedEventRegistryUnavailableError } from '@satorio/machinery/ports';
3
+ import { Effect } from 'effect';
4
+ /**
5
+ * Builds the registry on the service's own pool: one statement that both
6
+ * takes the claim and reads who won it, retried only over the race
7
+ * window.
8
+ *
9
+ * @internal
10
+ */
11
+ export declare const makePgProcessedEventRegistry: Effect.Effect<{
12
+ claim: (input: ClaimProcessedEventInput) => Effect.Effect<import("@satorio/machinery/ports").ProcessedEventClaim, ProcessedEventRegistryUnavailableError>;
13
+ }, never, PgClient.PgClient>;
@@ -0,0 +1,44 @@
1
+ import { PgClient } from '@effect/sql-pg';
2
+ import { alreadyProcessed, claimAccepted, ProcessedEventRegistryUnavailableError, portUnavailable, } from '@satorio/machinery/ports';
3
+ import { DateTime, Effect, Schedule } from 'effect';
4
+ import { ClaimRaceWindowMissedError } from './failures.js';
5
+ const CLAIM_RACE_WINDOW_RETRY_LIMIT = 5;
6
+ const RETRY_ONLY_ON_RACE_WINDOW_MISS = Schedule.recurs(CLAIM_RACE_WINDOW_RETRY_LIMIT).pipe(Schedule.whileInput((failure) => failure._tag === 'ClaimRaceWindowMissedError'));
7
+ /**
8
+ * Builds the registry on the service's own pool: one statement that both
9
+ * takes the claim and reads who won it, retried only over the race
10
+ * window.
11
+ *
12
+ * @internal
13
+ */
14
+ export const makePgProcessedEventRegistry = Effect.gen(function* () {
15
+ const sql = yield* PgClient.PgClient;
16
+ const unavailable = portUnavailable(ProcessedEventRegistryUnavailableError);
17
+ const oneClaimAttempt = (input) => Effect.gen(function* () {
18
+ const outcome = yield* sql `
19
+ WITH accepted AS (
20
+ INSERT INTO processed_event (event_id, processed_at)
21
+ VALUES (${input.eventId}::text, ${DateTime.toDate(input.processedAt)}::timestamptz)
22
+ ON CONFLICT (event_id) DO NOTHING
23
+ RETURNING processed_at AS "processedAt", true AS "claimed"
24
+ )
25
+ SELECT "processedAt", "claimed" FROM accepted
26
+ UNION ALL
27
+ SELECT processed_at AS "processedAt", false AS "claimed"
28
+ FROM processed_event
29
+ WHERE event_id = ${input.eventId}::text
30
+ AND NOT EXISTS (SELECT 1 FROM accepted)
31
+ `;
32
+ const row = outcome[0];
33
+ return row === undefined
34
+ ? yield* new ClaimRaceWindowMissedError({ eventId: input.eventId })
35
+ : row;
36
+ });
37
+ const claim = (input) => oneClaimAttempt(input).pipe(Effect.retry(RETRY_ONLY_ON_RACE_WINDOW_MISS), Effect.map((row) => row.claimed
38
+ ? claimAccepted
39
+ : alreadyProcessed(DateTime.unsafeMake(row.processedAt))), Effect.catchTags({
40
+ SqlError: unavailable('claim'),
41
+ ClaimRaceWindowMissedError: (failure) => unavailable('claim')(`Race window did not close within ${CLAIM_RACE_WINDOW_RETRY_LIMIT} attempts for eventId ${failure.eventId}`),
42
+ }));
43
+ return { claim };
44
+ });
@@ -0,0 +1,27 @@
1
+ import type { SqlError } from '@effect/sql/SqlError';
2
+ import { DatabaseDidNotAnswerError } from '@satorio/machinery/ports';
3
+ import { Effect } from 'effect';
4
+ /**
5
+ * Reason reported when the readiness query answers with a failure. A
6
+ * probe reads `reason` to tell a silent database from a slow one.
7
+ */
8
+ export declare const READINESS_REASON_WHEN_THE_DATABASE_STAYS_SILENT = "database did not answer the readiness query";
9
+ /**
10
+ * Reason reported when the readiness query times out. A database you can't
11
+ * reach doesn't always fail fast: the client may keep trying to connect
12
+ * while the caller waits. Without a deadline the probe would hang instead
13
+ * of reporting "down", and `/readyz` would hang with it.
14
+ */
15
+ export declare const READINESS_REASON_WHEN_THE_QUERY_TIMES_OUT = "readiness query did not answer within the deadline";
16
+ /**
17
+ * How long the probe waits for its query before it reports "down".
18
+ */
19
+ export declare const READINESS_QUERY_DEADLINE_MILLIS = 2000;
20
+ /**
21
+ * Builds a probe around one query the database must answer, with a
22
+ * deadline over it so an unreachable database reports "down" instead of
23
+ * hanging.
24
+ */
25
+ export declare const readinessProbeAsking: (aQueryTheDatabaseMustAnswer: Effect.Effect<unknown, SqlError>) => {
26
+ verifyDatabaseAnswers: Effect.Effect<void, DatabaseDidNotAnswerError, never>;
27
+ };
@@ -0,0 +1,33 @@
1
+ import { DatabaseDidNotAnswerError } from '@satorio/machinery/ports';
2
+ import { Duration, Effect } from 'effect';
3
+ /**
4
+ * Reason reported when the readiness query answers with a failure. A
5
+ * probe reads `reason` to tell a silent database from a slow one.
6
+ */
7
+ export const READINESS_REASON_WHEN_THE_DATABASE_STAYS_SILENT = 'database did not answer the readiness query';
8
+ /**
9
+ * Reason reported when the readiness query times out. A database you can't
10
+ * reach doesn't always fail fast: the client may keep trying to connect
11
+ * while the caller waits. Without a deadline the probe would hang instead
12
+ * of reporting "down", and `/readyz` would hang with it.
13
+ */
14
+ export const READINESS_REASON_WHEN_THE_QUERY_TIMES_OUT = 'readiness query did not answer within the deadline';
15
+ /**
16
+ * How long the probe waits for its query before it reports "down".
17
+ */
18
+ export const READINESS_QUERY_DEADLINE_MILLIS = 2_000;
19
+ /**
20
+ * Builds a probe around one query the database must answer, with a
21
+ * deadline over it so an unreachable database reports "down" instead of
22
+ * hanging.
23
+ */
24
+ export const readinessProbeAsking = (aQueryTheDatabaseMustAnswer) => ({
25
+ verifyDatabaseAnswers: aQueryTheDatabaseMustAnswer.pipe(Effect.asVoid, Effect.catchTag('SqlError', (silence) => Effect.logError('readiness query failed', silence).pipe(Effect.andThen(new DatabaseDidNotAnswerError({
26
+ reason: READINESS_REASON_WHEN_THE_DATABASE_STAYS_SILENT,
27
+ })))), Effect.timeoutFail({
28
+ duration: Duration.millis(READINESS_QUERY_DEADLINE_MILLIS),
29
+ onTimeout: () => new DatabaseDidNotAnswerError({
30
+ reason: READINESS_REASON_WHEN_THE_QUERY_TIMES_OUT,
31
+ }),
32
+ })),
33
+ });