Skip to content

Commit

Permalink
Merge branch 'main' into bf-delete-draft-detour
Browse files Browse the repository at this point in the history
  • Loading branch information
bfauble authored Jan 15, 2025
2 parents c8aeebb + d90c3f9 commit 5896be4
Show file tree
Hide file tree
Showing 11 changed files with 1,174 additions and 18 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ RUN apk add --no-cache --update curl
WORKDIR /root

ADD \
--checksum=sha256:390fdc813e2e58ec5a0def8ce6422b83d75032899167052ab981d8e1b3b14ff2 \
--checksum=sha256:05c6b4a9bf4daa95c888711481e19cc8e2e11aed4de8db759f51f4a6fce64917 \
https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem \
aws-cert-bundle.pem

Expand Down
280 changes: 280 additions & 0 deletions assets/src/hooks/useDetours.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
import { Channel, Socket } from "phoenix"
import {
SimpleDetour,
SimpleDetourData,
simpleDetourFromData,
} from "../models/detoursList"
import { useEffect, useState } from "react"
import { reload } from "../models/browser"
import { userUuid } from "../util/userUuid"
import { ByRouteId, RouteId } from "../schedule"
import { equalByElements } from "../helpers/array"
import { array, create } from "superstruct"

interface DetoursMap {
[key: number]: SimpleDetour
}

const subscribe = (
socket: Socket,
topic: string,
initializeChannel: React.Dispatch<React.SetStateAction<DetoursMap>>,
handleDrafted: ((data: SimpleDetour) => void) | undefined,
handleActivated: ((data: SimpleDetour) => void) | undefined,
handleDeactivated: ((data: SimpleDetour) => void) | undefined
): Channel => {
const channel = socket.channel(topic)

handleDrafted &&
channel.on("drafted", ({ data: unknownData }: { data: unknown }) => {
const data = create(unknownData, SimpleDetourData)
handleDrafted(simpleDetourFromData(data))
})
handleActivated &&
channel.on("activated", ({ data: unknownData }: { data: unknown }) => {
const data = create(unknownData, SimpleDetourData)
handleActivated(simpleDetourFromData(data))
})
handleDeactivated &&
channel.on("deactivated", ({ data: unknownData }: { data: unknown }) => {
const data = create(unknownData, SimpleDetourData)
handleDeactivated(simpleDetourFromData(data))
})
channel.on("auth_expired", reload)

channel
.join()
.receive("ok", ({ data: unknownData }: { data: unknown }) => {
const data = create(unknownData, array(SimpleDetourData))

const detoursMap = Object.fromEntries(
data.map((v) => [v.id, simpleDetourFromData(v)])
)
initializeChannel(detoursMap)
})

.receive("error", ({ reason }) => {
if (reason === "not_authenticated") {
reload()
} else {
// eslint-disable-next-line no-console
console.error(`joining topic ${topic} failed`, reason)
}
})
.receive("timeout", reload)

return channel
}

// This is to refresh the Detours List page. We need all active detours
export const useActiveDetours = (socket: Socket | undefined) => {
const topic = "detours:active"
const [activeDetours, setActiveDetours] = useState<DetoursMap>({})

const handleActivated = (data: SimpleDetour) => {
setActiveDetours((activeDetours) => ({ ...activeDetours, [data.id]: data }))
}

const handleDeactivated = (data: SimpleDetour) => {
setActiveDetours((activeDetours) => {
delete activeDetours[data.id]
return activeDetours
})
}

useEffect(() => {
let channel: Channel | undefined
if (socket) {
channel = subscribe(
socket,
topic,
setActiveDetours,
undefined,
handleActivated,
handleDeactivated
)
}

return () => {
if (channel !== undefined) {
channel.leave()
channel = undefined
}
}
}, [socket])
return activeDetours
}

// This is to refresh the Detours List page, past detours section
export const usePastDetours = (socket: Socket | undefined) => {
const topic = "detours:past"
const [pastDetours, setPastDetours] = useState<DetoursMap>({})

const handleDeactivated = (data: SimpleDetour) => {
setPastDetours((pastDetours) => ({ ...pastDetours, [data.id]: data }))
}

useEffect(() => {
let channel: Channel | undefined
if (socket) {
channel = subscribe(
socket,
topic,
setPastDetours,
undefined,
undefined,
handleDeactivated
)
}

return () => {
if (channel !== undefined) {
channel.leave()
channel = undefined
}
}
}, [socket])
return pastDetours
}

// This is to refresh the Detours List page, just the current user drafts
export const useDraftDetours = (socket: Socket | undefined) => {
const topic = "detours:draft:" + userUuid()
const [draftDetours, setDraftDetours] = useState<DetoursMap>({})

const handleDrafted = (data: SimpleDetour) => {
setDraftDetours((draftDetours) => ({ ...draftDetours, [data.id]: data }))
}

const handleActivated = (data: SimpleDetour) => {
setDraftDetours((draftDetours) => {
delete draftDetours[data.id]
return draftDetours
})
}

useEffect(() => {
let channel: Channel | undefined
if (socket) {
channel = subscribe(
socket,
topic,
setDraftDetours,
handleDrafted,
handleActivated,
undefined
)
}

return () => {
if (channel !== undefined) {
channel.leave()
channel = undefined
}
}
}, [socket, topic])
return draftDetours
}

const subscribeByRoute = (
socket: Socket,
topic: string,
routeId: string,
setDetours: React.Dispatch<React.SetStateAction<ByRouteId<DetoursMap>>>
): Channel => {
const channel = socket.channel(topic + routeId)

channel.on("activated", ({ data: unknownData }: { data: unknown }) => {
const data = create(unknownData, SimpleDetourData)
setDetours((activeDetours) => ({
...activeDetours,
[routeId]: {
...activeDetours[routeId],
[data.id]: simpleDetourFromData(data),
},
}))
})
channel.on("deactivated", ({ data: unknownData }: { data: unknown }) => {
const data = create(unknownData, SimpleDetourData)
setDetours((activeDetours) => {
delete activeDetours[routeId][data.id]
return activeDetours
})
})
channel.on("auth_expired", reload)

channel
.join()
.receive("ok", ({ data: unknownData }: { data: unknown }) => {
const data = create(unknownData, array(SimpleDetourData))
const detoursMap = Object.fromEntries(
data.map((v) => [v.id, simpleDetourFromData(v)])
)
setDetours((detoursByRouteId) => ({
...detoursByRouteId,
[routeId]: detoursMap,
}))
})

.receive("error", ({ reason }) => {
if (reason === "not_authenticated") {
reload()
} else {
// eslint-disable-next-line no-console
console.error(`joining topic ${topic} failed`, reason)
}
})
.receive("timeout", reload)

return channel
}

// This is to refresh the Route Ladders
export const useActiveDetoursByRoute = (
socket: Socket | undefined,
routeIds: RouteId[]
): ByRouteId<DetoursMap> => {
const baseTopic = "detours:active:"
const [activeDetoursByRoute, setActiveDetoursByRoute] = useState<
ByRouteId<DetoursMap>
>({})
// eslint-disable-next-line react/hook-use-state
const [, setChannelsByRouteId] = useState<ByRouteId<Channel>>({})

const [currentRouteIds, setCurrentRouteIds] = useState<RouteId[]>(routeIds)

if (!equalByElements(currentRouteIds, routeIds)) {
setCurrentRouteIds(routeIds)
}

useEffect(() => {
if (socket) {
setChannelsByRouteId((oldChannelsByRoutId) => {
const channelsByRouteId: ByRouteId<Channel> = {}

Object.entries(oldChannelsByRoutId).forEach(([routeId, channel]) => {
if (!currentRouteIds.includes(routeId)) {
channel.leave()
}
})

currentRouteIds.forEach((routeId) => {
if (routeId in oldChannelsByRoutId) {
channelsByRouteId[routeId] = oldChannelsByRoutId[routeId]
} else {
channelsByRouteId[routeId] = subscribeByRoute(
socket,
baseTopic,
routeId,
setActiveDetoursByRoute
)
}
})

return channelsByRouteId
})
}
}, [socket, currentRouteIds])

return activeDetoursByRoute
}
33 changes: 21 additions & 12 deletions assets/tests/factories/detourListFactory.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import { Factory } from "fishery"
import {
GroupedSimpleDetours,
SimpleDetour,
SimpleDetourData,
simpleDetourFromData,
} from "../../src/models/detoursList"

export const detourListFactory = Factory.define<GroupedSimpleDetours>(() => {
return {
active: [
simpleDetourFactory.build(),
simpleDetourFactory.build({ direction: "Outbound" }),
simpleDetourFromData(simpleDetourDataFactory.build()),
simpleDetourFromData(
simpleDetourDataFactory.build({ direction: "Outbound" })
),
],
draft: undefined,
past: [simpleDetourFactory.build({ name: "Headsign Z" })],
past: [
simpleDetourFromData(
simpleDetourDataFactory.build({ name: "Headsign Z" })
),
],
}
})

const simpleDetourFactory = Factory.define<SimpleDetour>(({ sequence }) => ({
id: sequence,
route: `${sequence}`,
direction: "Inbound",
name: `Headsign ${sequence}`,
intersection: `Street A${sequence} & Avenue B${sequence}`,
updatedAt: 1724866392,
}))
export const simpleDetourDataFactory = Factory.define<SimpleDetourData>(
({ sequence }) => ({
id: sequence,
route: `${sequence}`,
direction: "Inbound",
name: `Headsign ${sequence}`,
intersection: `Street A${sequence} & Avenue B${sequence}`,
updated_at: 1724866392,
})
)
Loading

0 comments on commit 5896be4

Please sign in to comment.