Skip to content

Commit

Permalink
Update TypeScript to 4.9, improve job typing (#198)
Browse files Browse the repository at this point in the history
* Remove dead expansion test harness

* Remove dead constraint prop tests

* Drop extraneous flow from expand

* Remove unused object.map

* update to TS 4.9

* improve job typing
  • Loading branch information
kdblocher committed Mar 12, 2023
1 parent 9dc5cc6 commit e6a63e5
Show file tree
Hide file tree
Showing 10 changed files with 102 additions and 132 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"rxjs": "^7.4.0",
"styled-components": "^5.3.6",
"ts-toolbelt": "^9.6.0",
"typescript": "^4.5.0",
"typescript": "^4.9.0",
"use-async-selector": "^0.2.7",
"uuid-tool": "^2.0.3"
},
Expand Down
4 changes: 2 additions & 2 deletions src/components/Jobs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ const JobView = ({ job }: JobViewProps) => {
O.toNullable)
const dispatch = useAppDispatch()
const onRemoveClick = useCallback(() => job && dispatch(removeJob(job.id)), [dispatch, job])
const onStartClick = useCallback(() => job && dispatch(startJob({ jobId: job.id, type: job.type.type })), [dispatch, job])
const onStartClick = useCallback(() => job && dispatch(startJob({ jobId: job.id, type: job.type })), [dispatch, job])
return (<>{job &&
<JobListItem>
<h5>{job.type.type}</h5>
<h5>{job.type}</h5>
{!progress && <p>
Estimated Units: {job.unitsInitial} <br />
<Button onClick={onStartClick}>Start</Button>
Expand Down
5 changes: 0 additions & 5 deletions src/lib/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ import ts from 'ts-toolbelt';

type Key = ts.Any.Key

export const map = <T, U>(obj: T, fn: (k: keyof T, v: T[keyof T], i: number) => any) =>
Object.fromEntries<U>(
(Object.entries(obj) as ([keyof T, T[keyof T]])[]).map(
([k, v], i) => [k, fn(k, v, i)]))

export const hasProperty = <X extends {}, Y extends PropertyKey>(obj: X, prop: Y): obj is X & Record<Y, unknown> =>
obj.hasOwnProperty(prop)

Expand Down
27 changes: 0 additions & 27 deletions src/model/constraints.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,19 +82,6 @@ describe('constraint implications (compact)', () => {
})))
})

describe('constraint implications', () => {
pipe(tests.constraintPropositionTests,
RR.mapWithIndex((name, { value, expected }) => {
test(name, () => fc.assert(
fc.property(fc.context(), handA, (ctx, hand) => {
ctx.log(encodeHand(hand))
boolean.BooleanAlgebra.implies(
satisfies(value)(hand),
satisfies(expected)(hand))
})))
}))
})

describe('constraint equivalencies', () => {
pipe(tests.syntaxPropCompactTests,
RR.mapWithIndex((name, [value, expected]) =>
Expand All @@ -114,20 +101,6 @@ describe('constraint equivalencies', () => {
})))
})

describe('expansion path validation', () => {
pipe(tests.expansionPathValidTests,
RR.mapWithIndex((name, { value, expected }) => {
describe(name, () => {
test("expands", () => {
expect(pipe(value,
expandSingleSyntacticBidPath,
x => TH.getChain(semigroup.first<unknown>()).chain(x, validateForest),
TH.isRight)).toEqual(expected)
})
})
}))
})

describe('partnership overlaps', () => {
pipe(tests.partnershipOverlappingTestsFalse,
RR.mapWithIndex((name, [north, south]) =>
Expand Down
11 changes: 0 additions & 11 deletions src/model/constraints.testdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,6 @@ export const decodeTests: RR.ReadonlyRecord<string, DecodeTest> = {
},
}

interface ConstraintPropositionTest {
value: Constraint
expected: Constraint
}

// The first constraint implies the second. Both constraints are syntax. Two rules; A implies B.
export const constraintPropCompactTests: RR.ReadonlyRecord<string, [string, string]> = {
// Point count tests
Expand Down Expand Up @@ -209,12 +204,6 @@ export const syntaxPropCompactTests: RR.ReadonlyRecord<string, [string, string]>
distUnBalanced: ["unBAL", "1-C or 1-D or 1-H or 1-S or 7222*"],
}

interface ExpansionPathValidTest {
value: Path<SyntacticBid>
expected: boolean
}


// This group of tests is first entity is north hand, while the second is the south hand. The implication is that all these must be FALSE
export const partnershipOverlappingTestsFalse: RR.ReadonlyRecord<string, [string, string]> = {
'Both hands cannot hold SK': ["SK", "SK"],
Expand Down
79 changes: 43 additions & 36 deletions src/model/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ interface ProgressDataWithValue<T> extends ProgressData {
type Progress<T> = O.Option<ProgressDataWithValue<T>>;

export const getGenericProgress = (job: Job) =>
job.type.progress as O.Option<ProgressData>;
job.details.progress as O.Option<ProgressData>;

export const initProgress = <T>(value: T): Progress<T> =>
O.some({
Expand Down Expand Up @@ -147,11 +147,38 @@ export const zeroAnalysis = (
});

export interface JobTypeGenerateDeals {
type: "GenerateDeals";
parameter: number;
context: { generationId: GenerationId };
progress: Progress<number>;
parameter: number
context: { generationId: GenerationId }
progress: Progress<number>
}
export interface JobTypeSatisfies {
parameter: Paths<ConstrainedBid>
context: { generationId: GenerationId }
progress: Progress<Satisfies>
}
export interface JobTypeSolve {
parameter: ReadonlyArray<SerializedDeal>
context: { generationId: GenerationId; bidPath: SerializedBidPath }
progress: Progress<Solution>
}
export type JobDetailsMap = {
"GenerateDeals": JobTypeGenerateDeals
"Satisfies": JobTypeSatisfies
"Solve": JobTypeSolve
}
export type Job<K extends keyof JobDetailsMap = keyof JobDetailsMap> = { [P in K]: {
id: JobId
analysisId: AnalysisId
dependsOn: ReadonlyArray<JobId>
unitsInitial: number
startDate: O.Option<DateNumber>
completedDate: O.Option<DateNumber>
running: boolean
error: O.Option<string>
type: P
details: JobDetailsMap[P]
}}[K];

const zeroGenerateDealsProgress = () => initProgress(0);
export const updateGenerateDealsProgress = (deals: ReadonlyArray<any>) =>
updateProgress(number.MonoidSum)(deals.length)(deals.length);
Expand All @@ -169,12 +196,7 @@ export const getBidPathHash = (cb: Path<ConstrainedBid>) =>
objectHash(cb)
) as Right<ConstrainedBidPathHash>
).right;
export interface JobTypeSatisfies {
type: "Satisfies";
parameter: Paths<ConstrainedBid>;
context: JobTypeGenerateDeals["context"];
progress: Progress<Satisfies>;
}

const zeroSatisfiesProgress = () => initProgress<Satisfies>({});
export const updateSatisfiesProgress = (result: SatisfiesResult) =>
updateProgress(RR.getUnionSemigroup(number.MonoidSum))(result.testedCount)({
Expand All @@ -184,22 +206,15 @@ export const updateSatisfiesProgress = (result: SatisfiesResult) =>
export type Solution = RR.ReadonlyRecord<
SerializedDeal["id"],
DoubleDummyResult
>;
export interface JobTypeSolve {
type: "Solve";
parameter: ReadonlyArray<SerializedDeal>;
context: { generationId: GenerationId; bidPath: SerializedBidPath };
progress: Progress<Solution>;
}
>

const zeroSolveProgress = () => initProgress<Solution>({});
export const updateSolveProgress = (solutions: Solution) =>
updateProgress(RR.getUnionSemigroup(semigroup.first<DoubleDummyResult>()))(
pipe(solutions, RR.keys, (keys) => keys.length)
)(solutions);

export type JobType = JobTypeGenerateDeals | JobTypeSatisfies | JobTypeSolve;

export const initJobProgress = (type: JobType["type"]) => {
export const initJobProgress = <K extends keyof JobDetailsMap>(type: K): Job<K>["details"]["progress"] => {
switch (type) {
case "GenerateDeals":
return zeroGenerateDealsProgress();
Expand All @@ -221,31 +236,23 @@ export const JobIdB = t.brand(
export type JobId = t.TypeOf<typeof JobIdB>;
const newJobId = () =>
(JobIdB.decode(UuidTool.newUuid()) as Right<JobId>).right;
export interface Job {
id: JobId;
analysisId: AnalysisId;
dependsOn: ReadonlyArray<JobId>;
type: JobType;
unitsInitial: number;
startDate: O.Option<DateNumber>;
completedDate: O.Option<DateNumber>;
running: boolean;
error: O.Option<string>;
}
export const zeroJob = (

export const zeroJob = <K extends keyof JobDetailsMap>(
analysisId: AnalysisId,
estimatedUnitsInitial: number,
type: JobType
): Job => ({
type: K,
details: JobDetailsMap[K]
): Job<K> => ({
id: newJobId(),
analysisId,
dependsOn: [],
type,
unitsInitial: estimatedUnitsInitial,
startDate: O.none,
completedDate: O.none,
running: false,
error: O.none,
type,
details,
});

export const unitsRemaining = (job: Job) =>
Expand Down
2 changes: 1 addition & 1 deletion src/model/system/expander.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ const expand = (syntax: Syntax) : S.State<ExpandContext, E.Either<ExpandErrorRea
expandOnce,
S.chain(flow(
E.mapLeft(E.left),
E.chain(flow(E.mapLeft(c => E.right(c)))),
E.chain(E.mapLeft(c => E.right(c))),
E.fold(ofS, expand))))

export type SyntacticBid = {
Expand Down
50 changes: 27 additions & 23 deletions src/reducers/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { concatWith, EMPTY, Observable, of } from 'rxjs';
import { AnyAction, createSlice, PayloadAction } from '@reduxjs/toolkit';

import { RootState } from '../app/store';
import { AnalysisId, GenerationId, initJobProgress, Job, JobId, JobType, JobTypeGenerateDeals, JobTypeSatisfies, JobTypeSolve, now, Solution, updateGenerateDealsProgress, updateSatisfiesProgress, updateSolveProgress, zeroJob } from '../model/job';
import { AnalysisId, GenerationId, initJobProgress, Job, JobDetailsMap, JobId, JobTypeGenerateDeals, JobTypeSatisfies, JobTypeSolve, now, Solution, updateGenerateDealsProgress, updateSatisfiesProgress, updateSolveProgress, zeroJob } from '../model/job';
import { SerializedDeal } from '../model/serialization';
import { Paths } from '../model/system';
import { ConstrainedBid } from '../model/system/core';
Expand All @@ -26,31 +26,35 @@ const initialState: State = {
completed: []
}

type InferJobType<T extends JobType["type"]> = Job & { type: JobType & { type: T } }
interface ScheduleJobPayload<T extends JobType["type"]> {
interface ScheduleJobPayload<T extends keyof JobDetailsMap, P extends JobDetailsMap[T]> {
analysisId: AnalysisId
type: T
parameter: InferJobType<T>["type"]["parameter"]
context: InferJobType<T>["type"]["context"]
parameter: P["parameter"]
context: P["context"]
estimatedUnitsInitial: number
}

const slice = createSlice({
name,
initialState,
reducers: {
scheduleJob: <T extends JobType["type"]>(state: WritableDraft<State>, action: PayloadAction<ScheduleJobPayload<T>>) => {
state.jobs.push(castDraft(zeroJob(action.payload.analysisId, action.payload.estimatedUnitsInitial, {
type: action.payload.type,
parameter: action.payload.parameter,
context: action.payload.context,
progress: O.none
} as JobType)))
scheduleJob: <K extends keyof JobDetailsMap, D extends JobDetailsMap[K]>(state: WritableDraft<State>, action: PayloadAction<ScheduleJobPayload<K, D>>) => {
const job = zeroJob<K>(
action.payload.analysisId,
action.payload.estimatedUnitsInitial,
action.payload.type,
{
parameter: action.payload.parameter,
context: action.payload.context,
progress: O.none
} as D
)
state.jobs.push(castDraft(job) as WritableDraft<Job>)
},
startJob: (state, action: PayloadAction<{ jobId: JobId, type: JobType["type"] }>) => {
startJob: (state, action: PayloadAction<{ jobId: JobId, type: Job["type"] }>) => {
const job = state.jobs.find(j => j.id === action.payload.jobId)
if (job) {
job.type.progress = pipe(action.payload.type, initJobProgress, castDraft)
job.details.progress = pipe(action.payload.type, initJobProgress, castDraft)
job.startDate = O.some(now())
job.running = true
}
Expand Down Expand Up @@ -80,19 +84,19 @@ const slice = createSlice({
O.map(idx => state.completed.splice(idx, 1)))
},
reportDeals: (state, action: PayloadAction<{ jobId: JobId, value: ReadonlyArray<SerializedDeal> }>) => {
const jobType = state.jobs.find(j => j.id === action.payload.jobId)?.type as JobTypeGenerateDeals
const jobType = state.jobs.find(j => j.id === action.payload.jobId)?.details as JobTypeGenerateDeals
if (jobType) {
jobType.progress = pipe(jobType.progress, updateGenerateDealsProgress(action.payload.value))
}
},
reportSatisfies: (state, action: PayloadAction<{ jobId: JobId, value: SatisfiesResult }>) => {
const jobType = state.jobs.find(j => j.id === action.payload.jobId)?.type as JobTypeSatisfies
const jobType = state.jobs.find(j => j.id === action.payload.jobId)?.details as JobTypeSatisfies
if (jobType) {
jobType.progress = pipe(jobType.progress, updateSatisfiesProgress(action.payload.value))
}
},
reportSolutions: (state, action: PayloadAction<{ jobId: JobId, value: Solution }>) => {
const jobType = state.jobs.find(j => j.id === action.payload.jobId)?.type as JobTypeSolve
const jobType = state.jobs.find(j => j.id === action.payload.jobId)?.details as JobTypeSolve
if (jobType) {
jobType.progress = pipe(jobType.progress, updateSolveProgress(action.payload.value))
}
Expand Down Expand Up @@ -132,21 +136,21 @@ const generateSolutions = (jobId: JobId, generationId: GenerationId, deals: Read
of(completeJob(jobId, O.some(err)))),
concatWith([completeJob(jobId, O.none)]))

const withJobType = <T extends JobType["type"]>(jobType: T) => (action$: Observable<AnyAction>, state$: StateObservable<RootState>) =>
const withJobType = <T extends keyof JobDetailsMap>(type: T) => (action$: Observable<AnyAction>, state$: StateObservable<RootState>) =>
action$.pipe(
Ob.filter(startJob.match),
Ob.map(a => a.payload),
Ob.filter((p): p is { jobId: JobId, type: T } => p.type === jobType),
Ob.filter(p => p.type === type),
Ob.map(p =>
pipe(state$.value.generator.jobs,
RA.findFirst(j => j.id === p.jobId),
O.map(j => j as InferJobType<T>))))
O.map(x => x as Job<T>))))

export const epics : ReadonlyArray<Epic<AnyAction, AnyAction, RootState>> = [
flow(withJobType("GenerateDeals"),
ObO.fold(() => EMPTY, job => generateDeals(job.id, job.type.context.generationId, job.type.parameter))),
ObO.fold(() => EMPTY, job => generateDeals(job.id, job.details.context.generationId, job.details.parameter))),
flow(withJobType("Satisfies"),
ObO.fold(() => EMPTY, job => generateSatisfies(job.id, job.type.context.generationId, job.type.parameter))),
ObO.fold(() => EMPTY, job => generateSatisfies(job.id, job.details.context.generationId, job.details.parameter))),
flow(withJobType("Solve"),
ObO.fold(() => EMPTY, job => generateSolutions(job.id, job.type.context.generationId, job.type.parameter)))
ObO.fold(() => EMPTY, job => generateSolutions(job.id, job.details.context.generationId, job.details.parameter)))
]
Loading

0 comments on commit e6a63e5

Please sign in to comment.