@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.
- package/CHANGELOG.md +23 -0
- package/README.md +2 -2
- package/dist/admit.d.ts +54 -0
- package/dist/admit.d.ts.map +1 -1
- package/dist/engine/abandonStalled.d.ts +9807 -0
- package/dist/engine/abandonStalled.d.ts.map +1 -0
- package/dist/engine/abandonStalled.js +55 -0
- package/dist/engine/abandonStalled.js.map +1 -0
- package/dist/engine/applyResolution.d.ts +108 -0
- package/dist/engine/applyResolution.d.ts.map +1 -1
- package/dist/engine/applyResolution.js +3 -0
- package/dist/engine/applyResolution.js.map +1 -1
- package/dist/engine/approveBrief.d.ts +164 -2
- package/dist/engine/approveBrief.d.ts.map +1 -1
- package/dist/engine/approveBrief.js +7 -5
- package/dist/engine/approveBrief.js.map +1 -1
- package/dist/engine/authorizeResolution.d.ts +942 -24
- package/dist/engine/authorizeResolution.d.ts.map +1 -1
- package/dist/engine/boardBrief.d.ts +162 -0
- package/dist/engine/boardBrief.d.ts.map +1 -1
- package/dist/engine/capacityRetry.d.ts +2883 -75
- package/dist/engine/capacityRetry.d.ts.map +1 -1
- package/dist/engine/closeDecisions.d.ts +108 -0
- package/dist/engine/closeDecisions.d.ts.map +1 -1
- package/dist/engine/completeBoardWork.d.ts +108 -0
- package/dist/engine/completeBoardWork.d.ts.map +1 -1
- package/dist/engine/completeReport.d.ts +108 -0
- package/dist/engine/completeReport.d.ts.map +1 -1
- package/dist/engine/continueCommand.d.ts +54 -0
- package/dist/engine/continueCommand.d.ts.map +1 -1
- package/dist/engine/continueWorker.d.ts +108 -0
- package/dist/engine/continueWorker.d.ts.map +1 -1
- package/dist/engine/deliverPendingAnswers.d.ts +1104 -24
- package/dist/engine/deliverPendingAnswers.d.ts.map +1 -1
- package/dist/engine/dispatchCommand.d.ts +54 -0
- package/dist/engine/dispatchCommand.d.ts.map +1 -1
- package/dist/engine/ensureDecisionOwner.d.ts +108 -0
- package/dist/engine/ensureDecisionOwner.d.ts.map +1 -1
- package/dist/engine/ensurePublication.d.ts +108 -0
- package/dist/engine/ensurePublication.d.ts.map +1 -1
- package/dist/engine/exhaustedCheckRepairs.d.ts.map +1 -1
- package/dist/engine/exhaustedCheckRepairs.js +3 -1
- package/dist/engine/exhaustedCheckRepairs.js.map +1 -1
- package/dist/engine/inspectRenewal.d.ts +108 -0
- package/dist/engine/inspectRenewal.d.ts.map +1 -1
- package/dist/engine/launchInput.d.ts +54 -0
- package/dist/engine/launchInput.d.ts.map +1 -1
- package/dist/engine/launchReceipt.d.ts +54 -0
- package/dist/engine/launchReceipt.d.ts.map +1 -1
- package/dist/engine/makeAdopt.d.ts +108 -0
- package/dist/engine/makeAdopt.d.ts.map +1 -1
- package/dist/engine/makeAdopt.js +10 -0
- package/dist/engine/makeAdopt.js.map +1 -1
- package/dist/engine/makeAdvance.d.ts +54 -0
- package/dist/engine/makeAdvance.d.ts.map +1 -1
- package/dist/engine/makeDeliver.d.ts +108 -0
- package/dist/engine/makeDeliver.d.ts.map +1 -1
- package/dist/engine/makeHuman.d.ts +108 -0
- package/dist/engine/makeHuman.d.ts.map +1 -1
- package/dist/engine/makeLaunch.d.ts +108 -0
- package/dist/engine/makeLaunch.d.ts.map +1 -1
- package/dist/engine/makeObserve.d.ts +108 -0
- package/dist/engine/makeObserve.d.ts.map +1 -1
- package/dist/engine/makeReview.d.ts +108 -0
- package/dist/engine/makeReview.d.ts.map +1 -1
- package/dist/engine/makeState.d.ts +888 -24
- package/dist/engine/makeState.d.ts.map +1 -1
- package/dist/engine/makeValidate.d.ts +108 -0
- package/dist/engine/makeValidate.d.ts.map +1 -1
- package/dist/engine/mergeReadiness.d.ts +1212 -24
- package/dist/engine/mergeReadiness.d.ts.map +1 -1
- package/dist/engine/observeDeliveryFailure.d.ts +996 -24
- package/dist/engine/observeDeliveryFailure.d.ts.map +1 -1
- package/dist/engine/observeRenewal.d.ts +189 -0
- package/dist/engine/observeRenewal.d.ts.map +1 -1
- package/dist/engine/observedAttempt.d.ts.map +1 -1
- package/dist/engine/observedAttempt.js +3 -0
- package/dist/engine/observedAttempt.js.map +1 -1
- package/dist/engine/obtainReview.d.ts +54 -0
- package/dist/engine/obtainReview.d.ts.map +1 -1
- package/dist/engine/operationRecord.d.ts +726 -24
- package/dist/engine/operationRecord.d.ts.map +1 -1
- package/dist/engine/persistTerminalReceipts.d.ts +108 -0
- package/dist/engine/persistTerminalReceipts.d.ts.map +1 -1
- package/dist/engine/prepareCommand.d.ts +54 -0
- package/dist/engine/prepareCommand.d.ts.map +1 -1
- package/dist/engine/prepareCommand.js +19 -0
- package/dist/engine/prepareCommand.js.map +1 -1
- package/dist/engine/queueAnswerCommand.d.ts +54 -0
- package/dist/engine/queueAnswerCommand.d.ts.map +1 -1
- package/dist/engine/readOrContinueReview.d.ts +54 -0
- package/dist/engine/readOrContinueReview.d.ts.map +1 -1
- package/dist/engine/readRetry.d.ts +1995 -51
- package/dist/engine/readRetry.d.ts.map +1 -1
- package/dist/engine/reassignCommand.d.ts +54 -0
- package/dist/engine/reassignCommand.d.ts.map +1 -1
- package/dist/engine/reconcileAttempt.d.ts +81 -0
- package/dist/engine/reconcileAttempt.d.ts.map +1 -1
- package/dist/engine/reconcileCommand.d.ts +54 -0
- package/dist/engine/reconcileCommand.d.ts.map +1 -1
- package/dist/engine/recoverStall.d.ts +1023 -24
- package/dist/engine/recoverStall.d.ts.map +1 -1
- package/dist/engine/recoverStall.js +5 -1
- package/dist/engine/recoverStall.js.map +1 -1
- package/dist/engine/recoverUnobservable.d.ts +1023 -24
- package/dist/engine/recoverUnobservable.d.ts.map +1 -1
- package/dist/engine/refreshBrief.d.ts +1997 -53
- package/dist/engine/refreshBrief.d.ts.map +1 -1
- package/dist/engine/refreshReviewResult.d.ts +108 -0
- package/dist/engine/refreshReviewResult.d.ts.map +1 -1
- package/dist/engine/releaseClosedPreparation.d.ts +996 -24
- package/dist/engine/releaseClosedPreparation.d.ts.map +1 -1
- package/dist/engine/releaseExecution.d.ts +996 -24
- package/dist/engine/releaseExecution.d.ts.map +1 -1
- package/dist/engine/releaseFinishedWork.d.ts +54 -0
- package/dist/engine/releaseFinishedWork.d.ts.map +1 -1
- package/dist/engine/repository.d.ts +672 -24
- package/dist/engine/repository.d.ts.map +1 -1
- package/dist/engine/repository.js +1 -0
- package/dist/engine/repository.js.map +1 -1
- package/dist/engine/requestConflictRepair.d.ts +996 -24
- package/dist/engine/requestConflictRepair.d.ts.map +1 -1
- package/dist/engine/requestRework.d.ts +108 -0
- package/dist/engine/requestRework.d.ts.map +1 -1
- package/dist/engine/resolveCommand.d.ts +54 -0
- package/dist/engine/resolveCommand.d.ts.map +1 -1
- package/dist/engine/resolveExecution.d.ts +996 -24
- package/dist/engine/resolveExecution.d.ts.map +1 -1
- package/dist/engine/resolveExecution.js +3 -1
- package/dist/engine/resolveExecution.js.map +1 -1
- package/dist/engine/resumeAttempt.d.ts +1023 -24
- package/dist/engine/resumeAttempt.d.ts.map +1 -1
- package/dist/engine/retireUnacknowledged.d.ts +108 -0
- package/dist/engine/retireUnacknowledged.d.ts.map +1 -1
- package/dist/engine/retryTerminal.d.ts +942 -24
- package/dist/engine/retryTerminal.d.ts.map +1 -1
- package/dist/engine/scheduler.d.ts +108 -0
- package/dist/engine/scheduler.d.ts.map +1 -1
- package/dist/engine/sessionCleanup.d.ts +108 -0
- package/dist/engine/sessionCleanup.d.ts.map +1 -1
- package/dist/engine/sessionCleanup.js +3 -1
- package/dist/engine/sessionCleanup.js.map +1 -1
- package/dist/engine/sessionStatus.d.ts.map +1 -1
- package/dist/engine/sessionStatus.js +3 -0
- package/dist/engine/sessionStatus.js.map +1 -1
- package/dist/engine/settleMergedDelivery.d.ts +996 -24
- package/dist/engine/settleMergedDelivery.d.ts.map +1 -1
- package/dist/engine/steerBrief.d.ts +10340 -0
- package/dist/engine/steerBrief.d.ts.map +1 -0
- package/dist/engine/steerBrief.js +42 -0
- package/dist/engine/steerBrief.js.map +1 -0
- package/dist/engine/submissionFailure.d.ts +888 -24
- package/dist/engine/submissionFailure.d.ts.map +1 -1
- package/dist/engine/submitLaunch.d.ts +942 -24
- package/dist/engine/submitLaunch.d.ts.map +1 -1
- package/dist/engine/views.js +1 -0
- package/dist/engine/views.js.map +1 -1
- package/dist/engine/workerReport.d.ts +162 -0
- package/dist/engine/workerReport.d.ts.map +1 -1
- package/dist/fleet.d.ts +540 -0
- package/dist/fleet.d.ts.map +1 -1
- package/dist/host/admission.d.ts +108 -0
- package/dist/host/admission.d.ts.map +1 -1
- package/dist/host/admission.js +2 -1
- package/dist/host/admission.js.map +1 -1
- package/dist/host/answerWork.d.ts +54 -0
- package/dist/host/answerWork.d.ts.map +1 -1
- package/dist/host/commands.d.ts +108 -0
- package/dist/host/commands.d.ts.map +1 -1
- package/dist/host/daemon.d.ts +600 -0
- package/dist/host/daemon.d.ts.map +1 -1
- package/dist/host/daemonClient.d.ts +691 -91
- package/dist/host/daemonClient.d.ts.map +1 -1
- package/dist/host/daemonContract.d.ts +984 -0
- package/dist/host/daemonContract.d.ts.map +1 -1
- package/dist/host/githubCheckState.d.ts +4 -0
- package/dist/host/githubCheckState.d.ts.map +1 -1
- package/dist/host/githubCheckState.js +17 -2
- package/dist/host/githubCheckState.js.map +1 -1
- package/dist/host/readWaitStates.d.ts.map +1 -1
- package/dist/host/readWaitStates.js +13 -4
- package/dist/host/readWaitStates.js.map +1 -1
- package/dist/host/reassignWork.d.ts +54 -0
- package/dist/host/reassignWork.d.ts.map +1 -1
- package/dist/host/recordPayloads.d.ts.map +1 -1
- package/dist/host/recordPayloads.js +8 -3
- package/dist/host/recordPayloads.js.map +1 -1
- package/dist/host/releaseWork.d.ts +108 -0
- package/dist/host/releaseWork.d.ts.map +1 -1
- package/dist/host/status.d.ts +30 -0
- package/dist/host/status.d.ts.map +1 -1
- package/dist/model.d.ts +144 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +14 -1
- package/dist/model.js.map +1 -1
- package/dist/operations.d.ts +1224 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/packageInfo.d.ts +1 -1
- package/dist/packageInfo.js +1 -1
- package/dist/prompt/briefEdit.d.ts +2 -0
- package/dist/prompt/briefEdit.d.ts.map +1 -0
- package/dist/prompt/briefEdit.js +41 -0
- package/dist/prompt/briefEdit.js.map +1 -0
- package/dist/prompt/worker.d.ts.map +1 -1
- package/dist/prompt/worker.js +11 -2
- package/dist/prompt/worker.js.map +1 -1
- package/dist/qualifyDependencies.d.ts +54 -0
- package/dist/qualifyDependencies.d.ts.map +1 -1
- package/dist/scheduling/areas.d.ts +162 -0
- package/dist/scheduling/areas.d.ts.map +1 -1
- package/dist/storage/attemptV12.d.ts +108 -0
- package/dist/storage/attemptV12.d.ts.map +1 -0
- package/dist/storage/attemptV12.js +94 -0
- package/dist/storage/attemptV12.js.map +1 -0
- package/dist/storage/createCollections.d.ts +5 -0
- package/dist/storage/createCollections.d.ts.map +1 -0
- package/dist/storage/createCollections.js +21 -0
- package/dist/storage/createCollections.js.map +1 -0
- package/dist/storage/cycleV12.d.ts +287 -0
- package/dist/storage/cycleV12.d.ts.map +1 -0
- package/dist/storage/cycleV12.js +124 -0
- package/dist/storage/cycleV12.js.map +1 -0
- package/dist/storage/fleetMigrations.d.ts +0 -39
- package/dist/storage/fleetMigrations.d.ts.map +1 -1
- package/dist/storage/fleetMigrations.js +3 -9
- package/dist/storage/fleetMigrations.js.map +1 -1
- package/dist/storage/legacyColumns.d.ts +40 -0
- package/dist/storage/legacyColumns.d.ts.map +1 -0
- package/dist/storage/legacyColumns.js +12 -0
- package/dist/storage/legacyColumns.js.map +1 -0
- package/dist/storage/makeForeignReservations.d.ts +15 -0
- package/dist/storage/makeForeignReservations.d.ts.map +1 -0
- package/dist/storage/makeForeignReservations.js +34 -0
- package/dist/storage/makeForeignReservations.js.map +1 -0
- package/dist/storage/makePostgresFleetStore.d.ts +36 -16
- package/dist/storage/makePostgresFleetStore.d.ts.map +1 -1
- package/dist/storage/makePostgresFleetStore.js +24 -32
- package/dist/storage/makePostgresFleetStore.js.map +1 -1
- package/dist/storage/model.d.ts +53 -59
- package/dist/storage/model.d.ts.map +1 -1
- package/dist/storage/model.js +18 -60
- package/dist/storage/model.js.map +1 -1
- package/dist/storage/operationEntries.d.ts +6 -5
- package/dist/storage/operationEntries.d.ts.map +1 -1
- package/dist/storage/operationEntries.js +12 -15
- package/dist/storage/operationEntries.js.map +1 -1
- package/dist/storage/preparationV12.d.ts +44 -0
- package/dist/storage/preparationV12.d.ts.map +1 -0
- package/dist/storage/preparationV12.js +30 -0
- package/dist/storage/preparationV12.js.map +1 -0
- package/dist/storage/readRecords.d.ts +4 -0
- package/dist/storage/readRecords.d.ts.map +1 -0
- package/dist/storage/readRecords.js +10 -0
- package/dist/storage/readRecords.js.map +1 -0
- package/dist/storage/recordChildren.d.ts +11 -0
- package/dist/storage/recordChildren.d.ts.map +1 -0
- package/dist/storage/recordChildren.js +105 -0
- package/dist/storage/recordChildren.js.map +1 -0
- package/dist/storage/recordShape.d.ts +23 -0
- package/dist/storage/recordShape.d.ts.map +1 -0
- package/dist/storage/recordShape.js +112 -0
- package/dist/storage/recordShape.js.map +1 -0
- package/dist/storage/recordValue.d.ts +9 -6
- package/dist/storage/recordValue.d.ts.map +1 -1
- package/dist/storage/recordValue.js +42 -32
- package/dist/storage/recordValue.js.map +1 -1
- package/dist/storage/shapeV12.d.ts +794 -0
- package/dist/storage/shapeV12.d.ts.map +1 -0
- package/dist/storage/shapeV12.js +10 -0
- package/dist/storage/shapeV12.js.map +1 -0
- package/dist/storage/textRecordValue.d.ts +3 -0
- package/dist/storage/textRecordValue.d.ts.map +1 -0
- package/dist/storage/textRecordValue.js +13 -0
- package/dist/storage/textRecordValue.js.map +1 -0
- package/dist/storage/typedDecisionsV12.d.ts +103 -0
- package/dist/storage/typedDecisionsV12.d.ts.map +1 -0
- package/dist/storage/typedDecisionsV12.js +97 -0
- package/dist/storage/typedDecisionsV12.js.map +1 -0
- package/dist/storage/typedTables.d.ts +4 -0
- package/dist/storage/typedTables.d.ts.map +1 -0
- package/dist/storage/typedTables.js +39 -0
- package/dist/storage/typedTables.js.map +1 -0
- package/package.json +8 -8
- package/src/engine/abandonStalled.ts +63 -0
- package/src/engine/applyResolution.ts +3 -0
- package/src/engine/approveBrief.ts +34 -32
- package/src/engine/exhaustedCheckRepairs.ts +3 -1
- package/src/engine/makeAdopt.ts +12 -0
- package/src/engine/observedAttempt.ts +3 -0
- package/src/engine/prepareCommand.ts +21 -0
- package/src/engine/recoverStall.ts +6 -2
- package/src/engine/repository.ts +1 -0
- package/src/engine/resolveExecution.ts +3 -1
- package/src/engine/sessionCleanup.ts +3 -1
- package/src/engine/sessionStatus.ts +5 -0
- package/src/engine/steerBrief.ts +54 -0
- package/src/engine/views.ts +1 -0
- package/src/host/admission.ts +2 -1
- package/src/host/githubCheckState.ts +19 -4
- package/src/host/readWaitStates.ts +12 -11
- package/src/host/recordPayloads.ts +11 -3
- package/src/model.ts +20 -1
- package/src/packageInfo.ts +1 -1
- package/src/prompt/briefEdit.ts +44 -0
- package/src/prompt/worker.ts +11 -2
- package/src/storage/attemptV12.ts +95 -0
- package/src/storage/createCollections.ts +21 -0
- package/src/storage/cycleV12.ts +125 -0
- package/src/storage/fleetMigrations.ts +3 -10
- package/src/storage/legacyColumns.ts +12 -0
- package/src/storage/makeForeignReservations.ts +41 -0
- package/src/storage/makePostgresFleetStore.ts +30 -43
- package/src/storage/model.ts +19 -69
- package/src/storage/operationEntries.ts +12 -22
- package/src/storage/preparationV12.ts +31 -0
- package/src/storage/readRecords.ts +12 -0
- package/src/storage/recordChildren.ts +131 -0
- package/src/storage/recordShape.ts +137 -0
- package/src/storage/recordValue.ts +47 -34
- package/src/storage/shapeV12.ts +11 -0
- package/src/storage/textRecordValue.ts +16 -0
- package/src/storage/typedDecisionsV12.ts +100 -0
- 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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
57
|
-
return
|
|
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({ ...
|
|
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.
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
});
|
package/src/storage/model.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { Effect, Schema
|
|
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
|
-
|
|
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.
|
|
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.
|
|
70
|
-
decision: Schema.NullOr(Schema.
|
|
71
|
-
history: Schema.NullOr(Schema.
|
|
72
|
-
launchRetry: Schema.NullOr(Schema.
|
|
73
|
-
outcome: Schema.NullOr(Schema.
|
|
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
|
-
|
|
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:
|
|
97
|
-
|
|
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
|
|
113
|
-
|
|
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
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { SqlClient } from "effect/sql";
|
|
3
|
+
import { childTable } from "#storage/recordChildren.ts";
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
}
|