@shivaedev/work-fleet 0.1.0 → 0.2.0

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 (323) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +2 -2
  3. package/dist/admit.d.ts +54 -0
  4. package/dist/admit.d.ts.map +1 -1
  5. package/dist/engine/abandonStalled.d.ts +9807 -0
  6. package/dist/engine/abandonStalled.d.ts.map +1 -0
  7. package/dist/engine/abandonStalled.js +55 -0
  8. package/dist/engine/abandonStalled.js.map +1 -0
  9. package/dist/engine/applyResolution.d.ts +108 -0
  10. package/dist/engine/applyResolution.d.ts.map +1 -1
  11. package/dist/engine/applyResolution.js +3 -0
  12. package/dist/engine/applyResolution.js.map +1 -1
  13. package/dist/engine/approveBrief.d.ts +164 -2
  14. package/dist/engine/approveBrief.d.ts.map +1 -1
  15. package/dist/engine/approveBrief.js +7 -5
  16. package/dist/engine/approveBrief.js.map +1 -1
  17. package/dist/engine/authorizeResolution.d.ts +942 -24
  18. package/dist/engine/authorizeResolution.d.ts.map +1 -1
  19. package/dist/engine/boardBrief.d.ts +162 -0
  20. package/dist/engine/boardBrief.d.ts.map +1 -1
  21. package/dist/engine/capacityRetry.d.ts +2883 -75
  22. package/dist/engine/capacityRetry.d.ts.map +1 -1
  23. package/dist/engine/closeDecisions.d.ts +108 -0
  24. package/dist/engine/closeDecisions.d.ts.map +1 -1
  25. package/dist/engine/completeBoardWork.d.ts +108 -0
  26. package/dist/engine/completeBoardWork.d.ts.map +1 -1
  27. package/dist/engine/completeReport.d.ts +108 -0
  28. package/dist/engine/completeReport.d.ts.map +1 -1
  29. package/dist/engine/continueCommand.d.ts +54 -0
  30. package/dist/engine/continueCommand.d.ts.map +1 -1
  31. package/dist/engine/continueWorker.d.ts +108 -0
  32. package/dist/engine/continueWorker.d.ts.map +1 -1
  33. package/dist/engine/deliverPendingAnswers.d.ts +1104 -24
  34. package/dist/engine/deliverPendingAnswers.d.ts.map +1 -1
  35. package/dist/engine/dispatchCommand.d.ts +54 -0
  36. package/dist/engine/dispatchCommand.d.ts.map +1 -1
  37. package/dist/engine/ensureDecisionOwner.d.ts +108 -0
  38. package/dist/engine/ensureDecisionOwner.d.ts.map +1 -1
  39. package/dist/engine/ensurePublication.d.ts +108 -0
  40. package/dist/engine/ensurePublication.d.ts.map +1 -1
  41. package/dist/engine/exhaustedCheckRepairs.d.ts.map +1 -1
  42. package/dist/engine/exhaustedCheckRepairs.js +3 -1
  43. package/dist/engine/exhaustedCheckRepairs.js.map +1 -1
  44. package/dist/engine/inspectRenewal.d.ts +108 -0
  45. package/dist/engine/inspectRenewal.d.ts.map +1 -1
  46. package/dist/engine/launchInput.d.ts +54 -0
  47. package/dist/engine/launchInput.d.ts.map +1 -1
  48. package/dist/engine/launchReceipt.d.ts +54 -0
  49. package/dist/engine/launchReceipt.d.ts.map +1 -1
  50. package/dist/engine/makeAdopt.d.ts +108 -0
  51. package/dist/engine/makeAdopt.d.ts.map +1 -1
  52. package/dist/engine/makeAdopt.js +10 -0
  53. package/dist/engine/makeAdopt.js.map +1 -1
  54. package/dist/engine/makeAdvance.d.ts +54 -0
  55. package/dist/engine/makeAdvance.d.ts.map +1 -1
  56. package/dist/engine/makeDeliver.d.ts +108 -0
  57. package/dist/engine/makeDeliver.d.ts.map +1 -1
  58. package/dist/engine/makeHuman.d.ts +108 -0
  59. package/dist/engine/makeHuman.d.ts.map +1 -1
  60. package/dist/engine/makeLaunch.d.ts +108 -0
  61. package/dist/engine/makeLaunch.d.ts.map +1 -1
  62. package/dist/engine/makeObserve.d.ts +108 -0
  63. package/dist/engine/makeObserve.d.ts.map +1 -1
  64. package/dist/engine/makeReview.d.ts +108 -0
  65. package/dist/engine/makeReview.d.ts.map +1 -1
  66. package/dist/engine/makeState.d.ts +888 -24
  67. package/dist/engine/makeState.d.ts.map +1 -1
  68. package/dist/engine/makeValidate.d.ts +108 -0
  69. package/dist/engine/makeValidate.d.ts.map +1 -1
  70. package/dist/engine/mergeReadiness.d.ts +1212 -24
  71. package/dist/engine/mergeReadiness.d.ts.map +1 -1
  72. package/dist/engine/observeDeliveryFailure.d.ts +996 -24
  73. package/dist/engine/observeDeliveryFailure.d.ts.map +1 -1
  74. package/dist/engine/observeRenewal.d.ts +189 -0
  75. package/dist/engine/observeRenewal.d.ts.map +1 -1
  76. package/dist/engine/observedAttempt.d.ts.map +1 -1
  77. package/dist/engine/observedAttempt.js +3 -0
  78. package/dist/engine/observedAttempt.js.map +1 -1
  79. package/dist/engine/obtainReview.d.ts +54 -0
  80. package/dist/engine/obtainReview.d.ts.map +1 -1
  81. package/dist/engine/operationRecord.d.ts +726 -24
  82. package/dist/engine/operationRecord.d.ts.map +1 -1
  83. package/dist/engine/persistTerminalReceipts.d.ts +108 -0
  84. package/dist/engine/persistTerminalReceipts.d.ts.map +1 -1
  85. package/dist/engine/prepareCommand.d.ts +54 -0
  86. package/dist/engine/prepareCommand.d.ts.map +1 -1
  87. package/dist/engine/prepareCommand.js +19 -0
  88. package/dist/engine/prepareCommand.js.map +1 -1
  89. package/dist/engine/queueAnswerCommand.d.ts +54 -0
  90. package/dist/engine/queueAnswerCommand.d.ts.map +1 -1
  91. package/dist/engine/readOrContinueReview.d.ts +54 -0
  92. package/dist/engine/readOrContinueReview.d.ts.map +1 -1
  93. package/dist/engine/readRetry.d.ts +1995 -51
  94. package/dist/engine/readRetry.d.ts.map +1 -1
  95. package/dist/engine/reassignCommand.d.ts +54 -0
  96. package/dist/engine/reassignCommand.d.ts.map +1 -1
  97. package/dist/engine/reconcileAttempt.d.ts +81 -0
  98. package/dist/engine/reconcileAttempt.d.ts.map +1 -1
  99. package/dist/engine/reconcileCommand.d.ts +54 -0
  100. package/dist/engine/reconcileCommand.d.ts.map +1 -1
  101. package/dist/engine/recoverStall.d.ts +1023 -24
  102. package/dist/engine/recoverStall.d.ts.map +1 -1
  103. package/dist/engine/recoverStall.js +5 -1
  104. package/dist/engine/recoverStall.js.map +1 -1
  105. package/dist/engine/recoverUnobservable.d.ts +1023 -24
  106. package/dist/engine/recoverUnobservable.d.ts.map +1 -1
  107. package/dist/engine/refreshBrief.d.ts +1997 -53
  108. package/dist/engine/refreshBrief.d.ts.map +1 -1
  109. package/dist/engine/refreshReviewResult.d.ts +108 -0
  110. package/dist/engine/refreshReviewResult.d.ts.map +1 -1
  111. package/dist/engine/releaseClosedPreparation.d.ts +996 -24
  112. package/dist/engine/releaseClosedPreparation.d.ts.map +1 -1
  113. package/dist/engine/releaseExecution.d.ts +996 -24
  114. package/dist/engine/releaseExecution.d.ts.map +1 -1
  115. package/dist/engine/releaseFinishedWork.d.ts +54 -0
  116. package/dist/engine/releaseFinishedWork.d.ts.map +1 -1
  117. package/dist/engine/repository.d.ts +672 -24
  118. package/dist/engine/repository.d.ts.map +1 -1
  119. package/dist/engine/repository.js +1 -0
  120. package/dist/engine/repository.js.map +1 -1
  121. package/dist/engine/requestConflictRepair.d.ts +996 -24
  122. package/dist/engine/requestConflictRepair.d.ts.map +1 -1
  123. package/dist/engine/requestRework.d.ts +108 -0
  124. package/dist/engine/requestRework.d.ts.map +1 -1
  125. package/dist/engine/resolveCommand.d.ts +54 -0
  126. package/dist/engine/resolveCommand.d.ts.map +1 -1
  127. package/dist/engine/resolveExecution.d.ts +996 -24
  128. package/dist/engine/resolveExecution.d.ts.map +1 -1
  129. package/dist/engine/resolveExecution.js +3 -1
  130. package/dist/engine/resolveExecution.js.map +1 -1
  131. package/dist/engine/resumeAttempt.d.ts +1023 -24
  132. package/dist/engine/resumeAttempt.d.ts.map +1 -1
  133. package/dist/engine/retireUnacknowledged.d.ts +108 -0
  134. package/dist/engine/retireUnacknowledged.d.ts.map +1 -1
  135. package/dist/engine/retryTerminal.d.ts +942 -24
  136. package/dist/engine/retryTerminal.d.ts.map +1 -1
  137. package/dist/engine/scheduler.d.ts +108 -0
  138. package/dist/engine/scheduler.d.ts.map +1 -1
  139. package/dist/engine/sessionCleanup.d.ts +108 -0
  140. package/dist/engine/sessionCleanup.d.ts.map +1 -1
  141. package/dist/engine/sessionCleanup.js +3 -1
  142. package/dist/engine/sessionCleanup.js.map +1 -1
  143. package/dist/engine/sessionStatus.d.ts.map +1 -1
  144. package/dist/engine/sessionStatus.js +3 -0
  145. package/dist/engine/sessionStatus.js.map +1 -1
  146. package/dist/engine/settleMergedDelivery.d.ts +996 -24
  147. package/dist/engine/settleMergedDelivery.d.ts.map +1 -1
  148. package/dist/engine/steerBrief.d.ts +10340 -0
  149. package/dist/engine/steerBrief.d.ts.map +1 -0
  150. package/dist/engine/steerBrief.js +42 -0
  151. package/dist/engine/steerBrief.js.map +1 -0
  152. package/dist/engine/submissionFailure.d.ts +888 -24
  153. package/dist/engine/submissionFailure.d.ts.map +1 -1
  154. package/dist/engine/submitLaunch.d.ts +942 -24
  155. package/dist/engine/submitLaunch.d.ts.map +1 -1
  156. package/dist/engine/views.js +1 -0
  157. package/dist/engine/views.js.map +1 -1
  158. package/dist/engine/workerReport.d.ts +162 -0
  159. package/dist/engine/workerReport.d.ts.map +1 -1
  160. package/dist/fleet.d.ts +540 -0
  161. package/dist/fleet.d.ts.map +1 -1
  162. package/dist/host/admission.d.ts +108 -0
  163. package/dist/host/admission.d.ts.map +1 -1
  164. package/dist/host/admission.js +2 -1
  165. package/dist/host/admission.js.map +1 -1
  166. package/dist/host/answerWork.d.ts +54 -0
  167. package/dist/host/answerWork.d.ts.map +1 -1
  168. package/dist/host/commands.d.ts +108 -0
  169. package/dist/host/commands.d.ts.map +1 -1
  170. package/dist/host/daemon.d.ts +600 -0
  171. package/dist/host/daemon.d.ts.map +1 -1
  172. package/dist/host/daemonClient.d.ts +691 -91
  173. package/dist/host/daemonClient.d.ts.map +1 -1
  174. package/dist/host/daemonContract.d.ts +984 -0
  175. package/dist/host/daemonContract.d.ts.map +1 -1
  176. package/dist/host/githubCheckState.d.ts +4 -0
  177. package/dist/host/githubCheckState.d.ts.map +1 -1
  178. package/dist/host/githubCheckState.js +17 -2
  179. package/dist/host/githubCheckState.js.map +1 -1
  180. package/dist/host/readWaitStates.d.ts.map +1 -1
  181. package/dist/host/readWaitStates.js +13 -4
  182. package/dist/host/readWaitStates.js.map +1 -1
  183. package/dist/host/reassignWork.d.ts +54 -0
  184. package/dist/host/reassignWork.d.ts.map +1 -1
  185. package/dist/host/recordPayloads.d.ts.map +1 -1
  186. package/dist/host/recordPayloads.js +8 -3
  187. package/dist/host/recordPayloads.js.map +1 -1
  188. package/dist/host/releaseWork.d.ts +108 -0
  189. package/dist/host/releaseWork.d.ts.map +1 -1
  190. package/dist/host/status.d.ts +30 -0
  191. package/dist/host/status.d.ts.map +1 -1
  192. package/dist/model.d.ts +144 -0
  193. package/dist/model.d.ts.map +1 -1
  194. package/dist/model.js +14 -1
  195. package/dist/model.js.map +1 -1
  196. package/dist/operations.d.ts +1224 -0
  197. package/dist/operations.d.ts.map +1 -1
  198. package/dist/packageInfo.d.ts +1 -1
  199. package/dist/packageInfo.js +1 -1
  200. package/dist/prompt/briefEdit.d.ts +2 -0
  201. package/dist/prompt/briefEdit.d.ts.map +1 -0
  202. package/dist/prompt/briefEdit.js +41 -0
  203. package/dist/prompt/briefEdit.js.map +1 -0
  204. package/dist/prompt/worker.d.ts.map +1 -1
  205. package/dist/prompt/worker.js +11 -2
  206. package/dist/prompt/worker.js.map +1 -1
  207. package/dist/qualifyDependencies.d.ts +54 -0
  208. package/dist/qualifyDependencies.d.ts.map +1 -1
  209. package/dist/scheduling/areas.d.ts +162 -0
  210. package/dist/scheduling/areas.d.ts.map +1 -1
  211. package/dist/storage/attemptV12.d.ts +108 -0
  212. package/dist/storage/attemptV12.d.ts.map +1 -0
  213. package/dist/storage/attemptV12.js +94 -0
  214. package/dist/storage/attemptV12.js.map +1 -0
  215. package/dist/storage/createCollections.d.ts +5 -0
  216. package/dist/storage/createCollections.d.ts.map +1 -0
  217. package/dist/storage/createCollections.js +21 -0
  218. package/dist/storage/createCollections.js.map +1 -0
  219. package/dist/storage/cycleV12.d.ts +287 -0
  220. package/dist/storage/cycleV12.d.ts.map +1 -0
  221. package/dist/storage/cycleV12.js +124 -0
  222. package/dist/storage/cycleV12.js.map +1 -0
  223. package/dist/storage/fleetMigrations.d.ts +0 -39
  224. package/dist/storage/fleetMigrations.d.ts.map +1 -1
  225. package/dist/storage/fleetMigrations.js +3 -9
  226. package/dist/storage/fleetMigrations.js.map +1 -1
  227. package/dist/storage/legacyColumns.d.ts +40 -0
  228. package/dist/storage/legacyColumns.d.ts.map +1 -0
  229. package/dist/storage/legacyColumns.js +12 -0
  230. package/dist/storage/legacyColumns.js.map +1 -0
  231. package/dist/storage/makeForeignReservations.d.ts +15 -0
  232. package/dist/storage/makeForeignReservations.d.ts.map +1 -0
  233. package/dist/storage/makeForeignReservations.js +34 -0
  234. package/dist/storage/makeForeignReservations.js.map +1 -0
  235. package/dist/storage/makePostgresFleetStore.d.ts +36 -16
  236. package/dist/storage/makePostgresFleetStore.d.ts.map +1 -1
  237. package/dist/storage/makePostgresFleetStore.js +24 -32
  238. package/dist/storage/makePostgresFleetStore.js.map +1 -1
  239. package/dist/storage/model.d.ts +53 -59
  240. package/dist/storage/model.d.ts.map +1 -1
  241. package/dist/storage/model.js +18 -60
  242. package/dist/storage/model.js.map +1 -1
  243. package/dist/storage/operationEntries.d.ts +6 -5
  244. package/dist/storage/operationEntries.d.ts.map +1 -1
  245. package/dist/storage/operationEntries.js +12 -15
  246. package/dist/storage/operationEntries.js.map +1 -1
  247. package/dist/storage/preparationV12.d.ts +44 -0
  248. package/dist/storage/preparationV12.d.ts.map +1 -0
  249. package/dist/storage/preparationV12.js +30 -0
  250. package/dist/storage/preparationV12.js.map +1 -0
  251. package/dist/storage/readRecords.d.ts +4 -0
  252. package/dist/storage/readRecords.d.ts.map +1 -0
  253. package/dist/storage/readRecords.js +10 -0
  254. package/dist/storage/readRecords.js.map +1 -0
  255. package/dist/storage/recordChildren.d.ts +11 -0
  256. package/dist/storage/recordChildren.d.ts.map +1 -0
  257. package/dist/storage/recordChildren.js +105 -0
  258. package/dist/storage/recordChildren.js.map +1 -0
  259. package/dist/storage/recordShape.d.ts +23 -0
  260. package/dist/storage/recordShape.d.ts.map +1 -0
  261. package/dist/storage/recordShape.js +112 -0
  262. package/dist/storage/recordShape.js.map +1 -0
  263. package/dist/storage/recordValue.d.ts +9 -6
  264. package/dist/storage/recordValue.d.ts.map +1 -1
  265. package/dist/storage/recordValue.js +42 -32
  266. package/dist/storage/recordValue.js.map +1 -1
  267. package/dist/storage/shapeV12.d.ts +794 -0
  268. package/dist/storage/shapeV12.d.ts.map +1 -0
  269. package/dist/storage/shapeV12.js +10 -0
  270. package/dist/storage/shapeV12.js.map +1 -0
  271. package/dist/storage/textRecordValue.d.ts +3 -0
  272. package/dist/storage/textRecordValue.d.ts.map +1 -0
  273. package/dist/storage/textRecordValue.js +13 -0
  274. package/dist/storage/textRecordValue.js.map +1 -0
  275. package/dist/storage/typedDecisionsV12.d.ts +103 -0
  276. package/dist/storage/typedDecisionsV12.d.ts.map +1 -0
  277. package/dist/storage/typedDecisionsV12.js +97 -0
  278. package/dist/storage/typedDecisionsV12.js.map +1 -0
  279. package/dist/storage/typedTables.d.ts +4 -0
  280. package/dist/storage/typedTables.d.ts.map +1 -0
  281. package/dist/storage/typedTables.js +39 -0
  282. package/dist/storage/typedTables.js.map +1 -0
  283. package/package.json +8 -8
  284. package/src/engine/abandonStalled.ts +63 -0
  285. package/src/engine/applyResolution.ts +3 -0
  286. package/src/engine/approveBrief.ts +34 -32
  287. package/src/engine/exhaustedCheckRepairs.ts +3 -1
  288. package/src/engine/makeAdopt.ts +12 -0
  289. package/src/engine/observedAttempt.ts +3 -0
  290. package/src/engine/prepareCommand.ts +21 -0
  291. package/src/engine/recoverStall.ts +6 -2
  292. package/src/engine/repository.ts +1 -0
  293. package/src/engine/resolveExecution.ts +3 -1
  294. package/src/engine/sessionCleanup.ts +3 -1
  295. package/src/engine/sessionStatus.ts +5 -0
  296. package/src/engine/steerBrief.ts +54 -0
  297. package/src/engine/views.ts +1 -0
  298. package/src/host/admission.ts +2 -1
  299. package/src/host/githubCheckState.ts +19 -4
  300. package/src/host/readWaitStates.ts +12 -11
  301. package/src/host/recordPayloads.ts +11 -3
  302. package/src/model.ts +20 -1
  303. package/src/packageInfo.ts +1 -1
  304. package/src/prompt/briefEdit.ts +44 -0
  305. package/src/prompt/worker.ts +11 -2
  306. package/src/storage/attemptV12.ts +95 -0
  307. package/src/storage/createCollections.ts +21 -0
  308. package/src/storage/cycleV12.ts +125 -0
  309. package/src/storage/fleetMigrations.ts +3 -10
  310. package/src/storage/legacyColumns.ts +12 -0
  311. package/src/storage/makeForeignReservations.ts +41 -0
  312. package/src/storage/makePostgresFleetStore.ts +30 -43
  313. package/src/storage/model.ts +19 -69
  314. package/src/storage/operationEntries.ts +12 -22
  315. package/src/storage/preparationV12.ts +31 -0
  316. package/src/storage/readRecords.ts +12 -0
  317. package/src/storage/recordChildren.ts +131 -0
  318. package/src/storage/recordShape.ts +137 -0
  319. package/src/storage/recordValue.ts +47 -34
  320. package/src/storage/shapeV12.ts +11 -0
  321. package/src/storage/textRecordValue.ts +16 -0
  322. package/src/storage/typedDecisionsV12.ts +100 -0
  323. package/src/storage/typedTables.ts +39 -0
@@ -0,0 +1,41 @@
1
+ import { Effect } from "effect";
2
+ import { SqlClient } from "effect/sql";
3
+ import { makeRepository } from "@shivaedev/effect-sql/repository.ts";
4
+ import { fleetRecords } from "#fleetRecords.ts";
5
+ import { makeFleetTransaction } from "#storage/makeFleetTransaction.ts";
6
+ import { ForeignReservationRow } from "#storage/model.ts";
7
+
8
+ export const makeForeignReservations = Effect.fn("FleetStore.makeForeignReservations")(function* (namespace: string) {
9
+ const sql = yield* SqlClient.SqlClient;
10
+ const commit = yield* makeFleetTransaction;
11
+ const reservations = yield* makeRepository(ForeignReservationRow, {
12
+ idColumn: "owner",
13
+ spanPrefix: "FleetForeign",
14
+ tableName: `${namespace}_foreign_reservations`,
15
+ });
16
+ const lock = sql`select pg_advisory_xact_lock(hashtextextended(${`work_fleet_admission:${namespace}`}, 0))`;
17
+ return {
18
+ foreignReservations: () => reservations.findMany({ orderBy: { direction: "asc", field: "owner" } }),
19
+ releaseForeign: (owner: string) =>
20
+ commit(
21
+ [fleetRecords.list],
22
+ Effect.gen(function* () {
23
+ yield* lock;
24
+ yield* reservations.delete(owner);
25
+ }),
26
+ ),
27
+ reserveForeign: (owner: string, occupancy: { readonly executing: boolean; readonly backlog: boolean } = { backlog: true, executing: true }) =>
28
+ commit(
29
+ [fleetRecords.list],
30
+ Effect.gen(function* () {
31
+ yield* lock;
32
+ const existing = yield* reservations.findMany({ where: { owner } });
33
+ if (existing.length === 0) {
34
+ yield* reservations.insertVoid({ owner, ...occupancy });
35
+ } else {
36
+ yield* reservations.updateVoid({ owner, ...occupancy });
37
+ }
38
+ }),
39
+ ),
40
+ };
41
+ });
@@ -1,16 +1,20 @@
1
1
  import { Effect, Array as EffectArray, Schema } from "effect";
2
2
  import { SqlClient } from "effect/sql";
3
- import { makeRepository } from "@shivaedev/effect-sql/repository.ts";
3
+ import { makeRepository, OptimisticConcurrencyError } from "@shivaedev/effect-sql/repository.ts";
4
4
  import { fleetRecords } from "#fleetRecords.ts";
5
5
  import { checkRecordAdmission } from "#storage/admission.ts";
6
6
  import { admissionReads } from "#storage/admissionReads.ts";
7
7
  import { StorageConflict } from "#storage/errors.ts";
8
8
  import type { FleetStoreOptions } from "#storage/FleetStoreOptions.ts";
9
9
  import { makeFleetTransaction } from "#storage/makeFleetTransaction.ts";
10
+ import { makeForeignReservations } from "#storage/makeForeignReservations.ts";
10
11
  import { migrateFleet } from "#storage/migrateFleet.ts";
11
- import { type AdmissionLimits, ForeignReservationRow, recordColumns, recordInput, recordModel, type Versioned } from "#storage/model.ts";
12
+ import { type AdmissionLimits, recordModel, type Versioned } from "#storage/model.ts";
12
13
  import { nextAdmissionOrder } from "#storage/nextAdmissionOrder.ts";
13
14
  import { operationEntries } from "#storage/operationEntries.ts";
15
+ import { readRecords } from "#storage/readRecords.ts";
16
+ import { inputColumns, makeRecordChildren } from "#storage/recordChildren.ts";
17
+ import { recordShape } from "#storage/recordShape.ts";
14
18
  import { withRecordTimes } from "#storage/recordTimes.ts";
15
19
  import { recordValue, recordValues } from "#storage/recordValue.ts";
16
20
 
@@ -25,36 +29,40 @@ export const makePostgresFleetStore = Effect.fn("FleetStore.makePostgresFleetSto
25
29
  const namespace = options.namespace ?? "work_fleet";
26
30
  const commit = yield* makeFleetTransaction;
27
31
  const records = `${namespace}_records`;
28
- const foreign = `${namespace}_foreign_reservations`;
29
- const columns = recordColumns(schema);
32
+ const shape = recordShape(schema);
30
33
  const model = recordModel(schema);
34
+ const saveChildren = yield* makeRecordChildren(namespace, shape);
31
35
  const rows = yield* makeRepository(model, { idColumn: "work_id", spanPrefix: "FleetRow", tableName: records, versionColumn: "version" });
32
- const reservations = yield* makeRepository(ForeignReservationRow, { idColumn: "owner", spanPrefix: "FleetForeign", tableName: foreign });
36
+ const foreign = yield* makeForeignReservations(namespace);
33
37
  const encodeRecord = Schema.encodeEffect(schema);
34
38
  const decodeObject = Schema.decodeUnknownEffect(Schema.Record(Schema.String, Schema.Unknown));
35
39
  const decodeInsert = Schema.decodeUnknownEffect(model.insert);
36
40
  const decodeRecord = Schema.decodeUnknownEffect(schema);
37
41
  const decodeRecords = Schema.decodeUnknownEffect(Schema.Array(schema));
38
42
  function decode(row: { readonly "work_id": string; readonly version: number }) {
39
- return recordValue(row, columns).pipe(
43
+ return recordValue(row, shape, namespace).pipe(
40
44
  Effect.flatMap(decodeRecord),
45
+ Effect.provideService(SqlClient.SqlClient, sql),
41
46
  Effect.map((value) => ({ value, version: row.version + 1 })),
42
47
  );
43
48
  }
44
- const loadEntry = Effect.fn("FleetStore.loadEntry")(function* (workId: string) {
49
+ const readEntry = Effect.fn("FleetStore.readEntry")(function* (workId: string) {
45
50
  const found = yield* rows.findById(workId).pipe(Effect.catchTag("NoSuchElementError", () => Effect.succeed(undefined)));
46
51
  return found === undefined ? undefined : { order: found.admission_order, stored: yield* decode(found) };
47
52
  });
53
+ function loadEntry(workId: string) {
54
+ return readRecords(namespace, readEntry(workId)).pipe(Effect.provideService(SqlClient.SqlClient, sql));
55
+ }
48
56
  function load(workId: string) {
49
57
  return loadEntry(workId).pipe(Effect.map((entry) => entry?.stored));
50
58
  }
51
- const list = Effect.fn("FleetStore.list")(function* () {
59
+ const readList = Effect.fn("FleetStore.readList")(function* () {
52
60
  const found = yield* rows.findMany({ orderBy: { direction: "asc", field: "admission_order" } });
53
- const values = yield* decodeRecords(yield* recordValues(found, columns));
61
+ const values = yield* decodeRecords(yield* recordValues(found, shape, namespace).pipe(Effect.provideService(SqlClient.SqlClient, sql)));
54
62
  return EffectArray.zipWith(values, found, (value, row) => ({ value, version: row.version + 1 }));
55
63
  });
56
- function foreignReservations() {
57
- return reservations.findMany({ orderBy: { direction: "asc", field: "owner" } });
64
+ function list() {
65
+ return readRecords(namespace, readList()).pipe(Effect.provideService(SqlClient.SqlClient, sql));
58
66
  }
59
67
  const lock = sql`select pg_advisory_xact_lock(hashtextextended(${`work_fleet_admission:${namespace}`}, 0))`.pipe(Effect.asVoid);
60
68
  const persist = Effect.fn("FleetStore.persist")(function* (
@@ -65,13 +73,14 @@ export const makePostgresFleetStore = Effect.fn("FleetStore.makePostgresFleetSto
65
73
  entry: { readonly order: bigint; readonly stored: Versioned<TSchema["Type"]> } | undefined,
66
74
  ) {
67
75
  const requeue = entry !== undefined && options.requeue?.(record, entry.stored.value) === true;
68
- const input = yield* decodeInsert({ ...recordInput(encoded, columns), "work_id": workId });
76
+ const input = yield* decodeInsert({ ...inputColumns(encoded, shape), "work_id": workId });
69
77
  if (entry === undefined) {
70
78
  yield* rows.insertVoid(input);
71
79
  } else {
72
80
  const order = requeue ? yield* nextAdmissionOrder(sql, records) : entry.order;
73
81
  yield* rows.updateVoid({ ...input, "admission_order": order, version: expectedVersion === null ? 0 : expectedVersion - 1 });
74
82
  }
83
+ yield* saveChildren(workId, encoded, entry !== undefined);
75
84
  return { value: record, version: (expectedVersion ?? 0) + 1 };
76
85
  });
77
86
  const write = Effect.fn("FleetStore.write")(function* (
@@ -96,52 +105,30 @@ export const makePostgresFleetStore = Effect.fn("FleetStore.makePostgresFleetSto
96
105
  }
97
106
  const reads = admissionReads(options, record, previous?.value, limits);
98
107
  const entries = reads.records ? yield* list() : [];
99
- const external = reads.foreign ? yield* foreignReservations() : [];
108
+ const external = reads.foreign ? yield* foreign.foreignReservations() : [];
100
109
  yield* checkRecordAdmission(options, record, previous?.value, entries, external, limits);
101
110
  const timed = yield* withRecordTimes(schema, encoded, previous?.value);
102
111
  return yield* persist(workId, expectedVersion, timed.value, timed.encoded, entry);
103
112
  }),
104
113
  ).pipe(
105
- Effect.catchTag("OptimisticConcurrencyError", () =>
106
- Effect.flatMap(load(workId), (actual) =>
107
- Effect.fail(new StorageConflict({ actualVersion: actual?.version ?? null, expectedVersion, workId })),
108
- ),
114
+ Effect.catchIf(
115
+ (error) => error instanceof OptimisticConcurrencyError,
116
+ () =>
117
+ Effect.flatMap(load(workId), (actual) =>
118
+ Effect.fail(new StorageConflict({ actualVersion: actual?.version ?? null, expectedVersion, workId })),
119
+ ),
109
120
  ),
110
121
  );
111
122
  });
112
123
  return {
113
124
  compareAndSet: (workId: string, expectedVersion: number, record: TSchema["Type"], limits?: AdmissionLimits) =>
114
125
  write(workId, expectedVersion, record, limits),
115
- foreignReservations,
126
+ ...foreign,
116
127
  hasLaunchRetry: () => rows.findMany({ select: ["launchRetry"] }).pipe(Effect.map((entries) => entries.some((row) => row.launchRetry !== null))),
117
128
  initialize: () => commit([fleetRecords.list], migrateFleet(namespace)),
118
129
  insert: (record: TSchema["Type"], limits?: AdmissionLimits) => write(record.workId, null, record, limits),
119
130
  list,
120
131
  load,
121
- operationEntries: (operationId: string) =>
122
- rows
123
- .findMany({ select: ["work_id", "version", "attempts", "history"] })
124
- .pipe(Effect.flatMap((entries) => operationEntries(entries, operationId))),
125
- releaseForeign: (owner: string) =>
126
- commit(
127
- [fleetRecords.list],
128
- Effect.gen(function* () {
129
- yield* lock;
130
- yield* reservations.delete(owner);
131
- }),
132
- ),
133
- reserveForeign: (owner: string, occupancy: { readonly executing: boolean; readonly backlog: boolean } = { backlog: true, executing: true }) =>
134
- commit(
135
- [fleetRecords.list],
136
- Effect.gen(function* () {
137
- yield* lock;
138
- const existing = yield* reservations.findMany({ where: { owner } });
139
- if (existing.length === 0) {
140
- yield* reservations.insertVoid({ owner, ...occupancy });
141
- } else {
142
- yield* reservations.updateVoid({ owner, ...occupancy });
143
- }
144
- }),
145
- ),
132
+ operationEntries: (operationId: string) => operationEntries(namespace, operationId).pipe(Effect.provideService(SqlClient.SqlClient, sql)),
146
133
  };
147
134
  });
@@ -1,6 +1,8 @@
1
- import { Effect, Schema, SchemaAST } from "effect";
1
+ import { Effect, Schema } from "effect";
2
2
  import { Model } from "effect/schema";
3
3
  import { FleetRecord } from "#model.ts";
4
+ import { inputColumns } from "#storage/recordChildren.ts";
5
+ import { type ColumnKind, columnSchema, recordShape, shapeColumns } from "#storage/recordShape.ts";
4
6
  import { recordValue } from "#storage/recordValue.ts";
5
7
  export interface Versioned<A> {
6
8
  readonly value: A;
@@ -14,63 +16,20 @@ export interface AdmissionLimits {
14
16
  readonly scheduling?: { readonly now: number; readonly areaMergeWaitMs: number };
15
17
  }
16
18
 
17
- function scalarKind(ast: SchemaAST.AST): "string" | "number" | "boolean" | "json" {
18
- if (SchemaAST.isString(ast)) {
19
- return "string";
20
- }
21
- if (SchemaAST.isNumber(ast)) {
22
- return "number";
23
- }
24
- if (SchemaAST.isBoolean(ast)) {
25
- return "boolean";
26
- }
27
- if (SchemaAST.isLiteral(ast)) {
28
- if (typeof ast.literal === "string") {
29
- return "string";
30
- }
31
- if (typeof ast.literal === "number") {
32
- return "number";
33
- }
34
- if (typeof ast.literal === "boolean") {
35
- return "boolean";
36
- }
37
- }
38
- if (SchemaAST.isUnion(ast)) {
39
- const kinds = new Set(ast.types.filter((type) => !SchemaAST.isUndefined(type)).map(scalarKind));
40
- if (kinds.size === 1) {
41
- return [...kinds][0] ?? "json";
42
- }
43
- }
44
- return "json";
45
- }
46
-
47
19
  export function recordColumns(schema: Schema.Top = FleetRecord) {
48
- const ast = SchemaAST.toEncoded(schema.ast);
49
- const base = SchemaAST.toEncoded(FleetRecord.ast);
50
- const properties = [...(SchemaAST.isObjects(base) ? base.propertySignatures : []), ...(SchemaAST.isObjects(ast) ? ast.propertySignatures : [])];
51
- return Object.fromEntries(properties.filter((field) => field.name !== "workId").map((field) => [String(field.name), scalarKind(field.type)]));
52
- }
53
-
54
- function columnSchema(kind: string) {
55
- if (kind === "number") {
56
- return Schema.Number;
57
- }
58
- if (kind === "boolean") {
59
- return Schema.Boolean;
60
- }
61
- return Schema.String;
20
+ return shapeColumns(recordShape(schema));
62
21
  }
63
22
 
64
23
  const rowFields = {
65
24
  "admission_order": Model.Field({ select: Schema.BigInt, update: Schema.BigInt }),
66
- attempts: Schema.NullOr(Schema.String),
25
+ attempts: Schema.NullOr(Schema.Boolean),
67
26
  attentionOwner: Schema.NullOr(Schema.String),
68
27
  blocker: Schema.NullOr(Schema.String),
69
- checks: Schema.NullOr(Schema.String),
70
- decision: Schema.NullOr(Schema.String),
71
- history: Schema.NullOr(Schema.String),
72
- launchRetry: Schema.NullOr(Schema.String),
73
- outcome: Schema.NullOr(Schema.String),
28
+ checks: Schema.NullOr(Schema.Boolean),
29
+ decision: Schema.NullOr(Schema.Boolean),
30
+ history: Schema.NullOr(Schema.Boolean),
31
+ launchRetry: Schema.NullOr(Schema.Boolean),
32
+ outcome: Schema.NullOr(Schema.Boolean),
74
33
  stage: Schema.NullOr(Schema.String),
75
34
  version: Model.Field({ select: Schema.Number, update: Schema.Number }),
76
35
  "work_id": Schema.String,
@@ -78,7 +37,11 @@ const rowFields = {
78
37
  class FleetRow extends Model.Class<FleetRow>("FleetRow")(rowFields) {}
79
38
 
80
39
  export function recordModel(schema: Schema.Top = FleetRecord): typeof FleetRow {
81
- const fields = Object.fromEntries(Object.entries(recordColumns(schema)).map(([name, kind]) => [name, Schema.NullOr(columnSchema(kind))]));
40
+ return modelFromColumns(recordColumns(schema));
41
+ }
42
+
43
+ export function modelFromColumns(columns: Readonly<Record<string, ColumnKind>>): typeof FleetRow {
44
+ const fields = Object.fromEntries(Object.entries(columns).map(([name, kind]) => [name, columnSchema(kind)]));
82
45
  // Explicit summary fields retain typed projections while the remaining fields come from the record schema.
83
46
  class RecordRow extends Model.Class<RecordRow>("FleetRow")({ ...rowFields, ...fields }) {}
84
47
  return RecordRow;
@@ -93,23 +56,10 @@ export type ForeignReservation = typeof ForeignReservation.Type;
93
56
 
94
57
  export class ForeignReservationRow extends Model.Class<ForeignReservationRow>("ForeignReservationRow")(ForeignReservation.fields) {}
95
58
 
96
- export function recordInput(value: string | object, columns: ReturnType<typeof recordColumns>) {
97
- const encoded: Record<string, unknown> = typeof value === "string" ? JSON.parse(value) : { ...value };
98
- return Object.fromEntries(
99
- Object.entries(columns).map(([name, kind]) => {
100
- if (encoded[name] === undefined) {
101
- return [name, null];
102
- }
103
- return [name, kind === "json" ? JSON.stringify(encoded[name]) : encoded[name]];
104
- }),
105
- );
106
- }
107
-
108
- export function recordJson(row: object, columns: ReturnType<typeof recordColumns>) {
109
- return recordValue(row, columns).pipe(Effect.map(JSON.stringify));
59
+ export function recordInput(value: object, schema: Schema.Top = FleetRecord) {
60
+ return inputColumns(value, recordShape(schema));
110
61
  }
111
62
 
112
- export interface StoredRow {
113
- readonly value: string;
114
- readonly version: number;
63
+ export function recordJson(row: object, namespace: string) {
64
+ return recordValue(row, recordShape(), namespace).pipe(Effect.map(JSON.stringify));
115
65
  }
@@ -1,24 +1,14 @@
1
- import { Effect, Schema } from "effect";
2
- import { Attempt, FleetRecord } from "#model.ts";
3
- import { recordValues } from "#storage/recordValue.ts";
1
+ import { Effect } from "effect";
2
+ import { SqlClient } from "effect/sql";
3
+ import { childTable } from "#storage/recordChildren.ts";
4
4
 
5
- const OperationAttempts = Schema.Array(Schema.Struct({ operationId: Attempt.fields.operationId }));
6
- const decode = Schema.decodeUnknownEffect(
7
- Schema.Array(
8
- Schema.Struct({
9
- attempts: OperationAttempts,
10
- history: Schema.optional(Schema.Array(Schema.Struct({ attempts: OperationAttempts }))),
11
- version: Schema.Number,
12
- workId: FleetRecord.fields.workId,
13
- }),
14
- ),
15
- );
16
-
17
- export const operationEntries = Effect.fn("FleetRow.operationEntries")(function* (rows: readonly object[], operationId: string) {
18
- const records = yield* decode(yield* recordValues(rows, { attempts: "json", history: "json", version: "number" }));
19
- return records.flatMap((record) =>
20
- [...(record.history ?? []).flatMap((cycle) => cycle.attempts), ...record.attempts]
21
- .filter((attempt) => attempt.operationId === operationId)
22
- .map(() => ({ version: record.version + 1, workId: record.workId })),
23
- );
5
+ export const operationEntries = Effect.fn("FleetRow.operationEntries")(function* (namespace: string, operationId: string) {
6
+ const sql = yield* SqlClient.SqlClient;
7
+ return yield* sql<{ readonly workId: string; readonly version: number }>`
8
+ select records.work_id as "workId", records.version + 1 as version
9
+ from ${sql(`${namespace}_records`)} records join (
10
+ select work_id from ${sql(childTable(namespace, ["attempts"]))} where ${sql("operationId")} = ${operationId}
11
+ union all
12
+ select work_id from ${sql(childTable(namespace, ["history", "attempts"]))} where ${sql("operationId")} = ${operationId}
13
+ ) attempts on attempts.work_id = records.work_id`;
24
14
  });
@@ -0,0 +1,31 @@
1
+ import type { Shape } from "#storage/recordShape.ts";
2
+
3
+ export const preparationV12 = {
4
+ fields: {
5
+ baselinePaths: { array: "string" },
6
+ baseRevision: "string",
7
+ configuration: {
8
+ array: {
9
+ fields: {
10
+ path: "string",
11
+ relevantKeys: { array: "string" },
12
+ },
13
+ },
14
+ },
15
+ contextComplete: "boolean",
16
+ dependencyPaths: { array: "string" },
17
+ observedAt: "number",
18
+ ownedPaths: { array: "string" },
19
+ qualityPaths: { array: "string" },
20
+ setupPaths: { array: "string" },
21
+ sourcePaths: { array: "string" },
22
+ validation: {
23
+ array: {
24
+ fields: {
25
+ command: "string",
26
+ revision: "string",
27
+ },
28
+ },
29
+ },
30
+ },
31
+ } as const satisfies Shape;
@@ -0,0 +1,12 @@
1
+ import { Effect } from "effect";
2
+ import { SqlClient } from "effect/sql";
3
+
4
+ export const readRecords = Effect.fn("FleetRow.readRecords")(function* <A, E, R>(namespace: string, body: Effect.Effect<A, E, R>) {
5
+ const sql = yield* SqlClient.SqlClient;
6
+ return yield* sql.withTransaction(
7
+ Effect.gen(function* () {
8
+ yield* sql`select pg_advisory_xact_lock_shared(hashtextextended(${`work_fleet_admission:${namespace}`}, 0))`;
9
+ return yield* body;
10
+ }),
11
+ );
12
+ });
@@ -0,0 +1,131 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Effect, Schema } from "effect";
3
+ import { Model } from "effect/schema";
4
+ import { SqlClient } from "effect/sql";
5
+ import type { SqlError } from "effect/sql/SqlError";
6
+ import { makeRepository } from "@shivaedev/effect-sql/repository.ts";
7
+ import { type Collection, collections, columnSchema, type Shape, shapeColumns } from "#storage/recordShape.ts";
8
+
9
+ export function childTable(namespace: string, path: readonly string[]) {
10
+ return `${namespace}_c_${createHash("sha256").update(path.join(".")).digest("hex").slice(0, 12)}`;
11
+ }
12
+
13
+ export function positions(collection: Collection) {
14
+ return Array.from({ length: collection.parents.length + 1 }, (_unused, index) => `position${index}`);
15
+ }
16
+
17
+ function childModel(collection: Collection) {
18
+ class Child extends Model.Class<Child>("FleetChild")({
19
+ ...Object.fromEntries(Object.entries(shapeColumns(collection.shape)).map(([name, kind]) => [name, columnSchema(kind)])),
20
+ ...Object.fromEntries(positions(collection).map((name) => [name, Schema.Number])),
21
+ "work_id": Schema.String,
22
+ }) {}
23
+ return Child;
24
+ }
25
+
26
+ const decodeObject = Schema.decodeUnknownSync(Schema.Record(Schema.String, Schema.Unknown));
27
+
28
+ export function objectValue(value: unknown): Record<string, unknown> {
29
+ return decodeObject(value);
30
+ }
31
+
32
+ function presence(value: unknown) {
33
+ return value === undefined ? null : true;
34
+ }
35
+
36
+ export function inputColumns(value: unknown, shape: Shape, path: readonly string[] = []): Record<string, unknown> {
37
+ const name = path.join("__");
38
+ if (typeof shape === "string") {
39
+ return { [name]: value ?? null };
40
+ }
41
+ if ("array" in shape) {
42
+ const stored = typeof shape.array === "string" ? (value ?? null) : presence(value);
43
+ return { [name]: stored };
44
+ }
45
+ const fields = value === undefined ? {} : objectValue(value);
46
+ return Object.assign(
47
+ path.length === 0 ? {} : { [name]: presence(value) },
48
+ ...Object.entries(shape.fields).map(([key, field]) => inputColumns(fields[key], field, [...path, key])),
49
+ );
50
+ }
51
+
52
+ export const makeRecordChildren = Effect.fn("FleetRow.makeRecordChildren")(function* (namespace: string, shape: Shape) {
53
+ const sql = yield* SqlClient.SqlClient;
54
+ const tables = collections(shape);
55
+ const repositories = new Map(
56
+ yield* Effect.forEach(tables, (collection) =>
57
+ Effect.gen(function* () {
58
+ const model = childModel(collection);
59
+ const repository = yield* makeRepository(model, {
60
+ idColumn: "work_id",
61
+ spanPrefix: "FleetChild",
62
+ tableName: childTable(namespace, collection.path),
63
+ });
64
+ return [collection.path.join("."), { model, repository }] as const;
65
+ }),
66
+ ),
67
+ );
68
+ const save = Effect.fn("FleetRow.save")(function* (workId: string, value: unknown, replace = true) {
69
+ if (replace) {
70
+ for (const collection of [...tables].reverse()) {
71
+ yield* sql`delete from ${sql(childTable(namespace, collection.path))} where work_id = ${workId}`;
72
+ }
73
+ }
74
+ function write(
75
+ current: unknown,
76
+ node: Shape,
77
+ path: readonly string[],
78
+ indices: readonly number[],
79
+ ): Effect.Effect<void, Schema.SchemaError | SqlError> {
80
+ return Effect.gen(function* () {
81
+ if (current === undefined || typeof node === "string") {
82
+ return;
83
+ }
84
+ if ("fields" in node) {
85
+ const fields = objectValue(current);
86
+ yield* Effect.forEach(Object.entries(node.fields), ([name, field]) => write(fields[name], field, [...path, name], indices), {
87
+ discard: true,
88
+ });
89
+ return;
90
+ }
91
+ if (typeof node.array === "string") {
92
+ return;
93
+ }
94
+ yield* writeArray(current, node.array, path, indices);
95
+ });
96
+ }
97
+ function writeArray(
98
+ current: unknown,
99
+ item: Shape,
100
+ path: readonly string[],
101
+ indices: readonly number[],
102
+ ): Effect.Effect<void, Schema.SchemaError | SqlError> {
103
+ return Effect.gen(function* () {
104
+ const child = repositories.get(path.join("."));
105
+ if (child === undefined) {
106
+ return yield* Effect.die("Missing Fleet collection");
107
+ }
108
+ const { model, repository } = child;
109
+ const entries = yield* Schema.decodeUnknownEffect(Schema.Array(Schema.Unknown))(current);
110
+ for (const [index, entry] of entries.entries()) {
111
+ const keys = [...indices, index];
112
+ const input = yield* Schema.decodeUnknownEffect(model.insert)({
113
+ ...inputColumns(entry, item),
114
+ ...Object.fromEntries(keys.map((position, depth) => [`position${depth}`, position])),
115
+ "work_id": workId,
116
+ });
117
+ yield* repository.insertVoid(input);
118
+ yield* write(entry, item, path, keys);
119
+ }
120
+ });
121
+ }
122
+
123
+ yield* write(value, shape, [], []);
124
+ });
125
+ return save;
126
+ });
127
+
128
+ export const writeChildren = Effect.fn("FleetRow.writeChildren")(function* (namespace: string, workId: string, value: unknown, shape: Shape) {
129
+ const save = yield* makeRecordChildren(namespace, shape);
130
+ yield* save(workId, value);
131
+ });
@@ -0,0 +1,137 @@
1
+ import * as PgTypes from "@effect/sql-pg/PgTypes";
2
+ import { Effect, Result, Schema, SchemaAST, SchemaGetter } from "effect";
3
+ import { FleetRecord } from "#model.ts";
4
+
5
+ export type ScalarKind = "string" | "number" | "boolean";
6
+ export type Shape = ScalarKind | { readonly array: Shape } | { readonly fields: Readonly<Record<string, Shape>> };
7
+ export type ColumnKind = ScalarKind | "string[]" | "number[]" | "boolean[]";
8
+
9
+ function flatten(ast: SchemaAST.AST): readonly SchemaAST.AST[] {
10
+ return SchemaAST.isUnion(ast) ? ast.types.flatMap(flatten) : [ast];
11
+ }
12
+
13
+ function fromAsts(asts: readonly SchemaAST.AST[]): Shape {
14
+ const types = asts.flatMap(flatten).filter((ast) => !SchemaAST.isUndefined(ast));
15
+ if (types.every(SchemaAST.isObjects)) {
16
+ const names = [...new Set(types.flatMap((ast) => ast.propertySignatures.map((field) => String(field.name))))];
17
+ return {
18
+ fields: Object.fromEntries(
19
+ names.map((name) => [
20
+ name,
21
+ fromAsts(types.flatMap((ast) => ast.propertySignatures.filter((field) => field.name === name).map((field) => field.type))),
22
+ ]),
23
+ ),
24
+ };
25
+ }
26
+ if (types.every(SchemaAST.isArrays)) {
27
+ return { array: fromAsts(types.flatMap((ast) => ast.rest)) };
28
+ }
29
+ const kinds = types.map((ast) => {
30
+ if (SchemaAST.isString(ast)) {
31
+ return "string";
32
+ }
33
+ if (SchemaAST.isNumber(ast)) {
34
+ return "number";
35
+ }
36
+ if (SchemaAST.isBoolean(ast)) {
37
+ return "boolean";
38
+ }
39
+ if (SchemaAST.isLiteral(ast)) {
40
+ return typeof ast.literal;
41
+ }
42
+ throw new Error(`Unsupported Fleet storage schema: ${ast._tag}`);
43
+ });
44
+ const kind = kinds[0];
45
+ if ((kind === "string" || kind === "number" || kind === "boolean") && kinds.every((value) => value === kind)) {
46
+ return kind;
47
+ }
48
+ throw new Error("Fleet storage requires a single typed column kind");
49
+ }
50
+
51
+ export function schemaShape(schema: Schema.Top): Shape {
52
+ const shape = fromAsts([SchemaAST.toEncoded(schema.ast)]);
53
+ if (typeof shape === "object" && "fields" in shape) {
54
+ const { workId: _workId, ...fields } = shape.fields;
55
+ return { fields };
56
+ }
57
+ throw new Error("Fleet records must be objects");
58
+ }
59
+
60
+ export function recordShape(schema: Schema.Top = FleetRecord): Shape {
61
+ const base = schemaShape(FleetRecord);
62
+ const extra = schemaShape(schema);
63
+ if (typeof base === "object" && "fields" in base && typeof extra === "object" && "fields" in extra) {
64
+ return { fields: { ...base.fields, ...extra.fields } };
65
+ }
66
+ throw new Error("Fleet records must be objects");
67
+ }
68
+
69
+ export function shapeColumns(shape: Shape, prefix: readonly string[] = []): Record<string, ColumnKind> {
70
+ const name = prefix.join("__");
71
+ if (typeof shape === "string") {
72
+ return { [name]: shape };
73
+ }
74
+ if ("array" in shape) {
75
+ if (typeof shape.array !== "string") {
76
+ return { [name]: "boolean" };
77
+ }
78
+ const kinds = { boolean: "boolean[]", number: "number[]", string: "string[]" } as const;
79
+ return { [name]: kinds[shape.array] };
80
+ }
81
+ return Object.assign(
82
+ prefix.length === 0 ? {} : { [name]: "boolean" },
83
+ ...Object.entries(shape.fields).map(([key, field]) => shapeColumns(field, [...prefix, key])),
84
+ );
85
+ }
86
+
87
+ export interface Collection {
88
+ readonly parents: readonly string[];
89
+ readonly path: readonly string[];
90
+ readonly shape: Shape;
91
+ }
92
+
93
+ export function collections(shape: Shape, path: readonly string[] = [], parents: readonly string[] = []): Collection[] {
94
+ if (typeof shape === "string") {
95
+ return [];
96
+ }
97
+ if ("array" in shape) {
98
+ if (typeof shape.array === "string") {
99
+ return [];
100
+ }
101
+ return [{ parents, path, shape: shape.array }, ...collections(shape.array, path, [...parents, path.join(".")])];
102
+ }
103
+ return Object.entries(shape.fields).flatMap(([key, field]) => collections(field, [...path, key], parents));
104
+ }
105
+
106
+ export const sqlTypes: Readonly<Record<ColumnKind, string>> = {
107
+ boolean: "boolean",
108
+ "boolean[]": "boolean[]",
109
+ number: "double precision",
110
+ "number[]": "double precision[]",
111
+ string: "text",
112
+ "string[]": "text[]",
113
+ };
114
+
115
+ function scalarCodec(kind: ColumnKind) {
116
+ if (kind.startsWith("number")) {
117
+ return { oid: PgTypes.OID.float8, schema: Schema.Number };
118
+ }
119
+ if (kind.startsWith("boolean")) {
120
+ return { oid: PgTypes.OID.bool, schema: Schema.Boolean };
121
+ }
122
+ return { oid: PgTypes.OID.text, schema: Schema.String };
123
+ }
124
+
125
+ export function columnSchema(kind: ColumnKind): Schema.Top & { readonly DecodingServices: never; readonly EncodingServices: never } {
126
+ const { schema: Scalar, oid } = scalarCodec(kind);
127
+ if (!kind.endsWith("[]")) {
128
+ return Schema.NullOr(Scalar);
129
+ }
130
+ const ArrayValue = Schema.NullOr(Schema.Array(Scalar));
131
+ return Schema.Unknown.pipe(
132
+ Schema.decodeTo(ArrayValue, {
133
+ decode: SchemaGetter.transformEffect((value) => Schema.decodeUnknownEffect(ArrayValue)(value).pipe(Effect.mapError((error) => error.issue))),
134
+ encode: SchemaGetter.transform((values) => (values === null ? null : Result.getOrThrow(PgTypes.array(values, oid)))),
135
+ }),
136
+ );
137
+ }