Refactor API types and sync logic
Rename and refactor the types passed over the API to be based on an entity that's either living or a tombstone. A living entity has a deleted property that's either undefined or false, while a tombstone has a deleted property set to true. All entities have a numeric id and an updatedAt timestamp. To sync entities, an array of replacements are passed around. Living entities are replaced with tombstones when they're deleted. And tombstones are replaced with living entities when restored.
This commit is contained in:
parent
251e83f640
commit
fe06d0d6bd
36 changed files with 1242 additions and 834 deletions
|
@ -1,30 +1,41 @@
|
|||
import { Account } from "~/shared/types/account";
|
||||
import { Role, Schedule, Shift, ShiftSlot, TimeSlot } from "~/shared/types/schedule";
|
||||
import type { ApiAccount, ApiSchedule, ApiScheduleEventSlot, ApiScheduleShiftSlot } from "~/shared/types/api";
|
||||
import { toId } from "~/shared/utils/functions";
|
||||
|
||||
const locations = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Stage",
|
||||
description: "Inside the main building."
|
||||
description: "Inside the main building.",
|
||||
updatedAt: "d0 18:21",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Clubhouse",
|
||||
description: "That big red building in the middle of the park."
|
||||
description: "That big red building in the middle of the park.",
|
||||
updatedAt: "d0 18:25",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Summerhouse",
|
||||
description: "Next to the campfire by the lake"
|
||||
description: "Next to the campfire by the lake",
|
||||
updatedAt: "d0 18:22",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Campfire",
|
||||
description: "Next to the big tree by the lake."
|
||||
description: "Next to the big tree by the lake.",
|
||||
updatedAt: "d0 18:41",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Outside",
|
||||
description: "Takes place somewhere outside."
|
||||
description: "Takes place somewhere outside.",
|
||||
updatedAt: "d0 18:37",
|
||||
}
|
||||
]
|
||||
|
||||
let slotId = 1;
|
||||
let eventId = 1;
|
||||
const events = [
|
||||
{
|
||||
name: "Arcade",
|
||||
|
@ -133,17 +144,24 @@ const events = [
|
|||
{ name: "Artist Alley", slots: ["d4 12:00 4h clubhouse"]},
|
||||
{ name: "Teardown Artist Alley", crew: true, slots: ["d4 16:00 1h clubhouse"]},
|
||||
{ name: "Feedback Panel", slots: ["d5 12:00 1h clubhouse"]},
|
||||
];
|
||||
].map(({ slots, ...rest }) => ({
|
||||
...rest,
|
||||
id: eventId++,
|
||||
slots: slots.map(slot => `${slot} ${slotId++}`),
|
||||
}));
|
||||
|
||||
const roles: Role[] = [
|
||||
{ id: "medic", name: "Medic" },
|
||||
{ id: "security", name: "Security" },
|
||||
|
||||
const idMedic = 1;
|
||||
const idSecurity = 2;
|
||||
const roles = [
|
||||
{ id: idMedic, name: "Medic", updatedAt: "d1 12:34" },
|
||||
{ id: idSecurity, name: "Security", updatedAt: "d1 12:39" },
|
||||
]
|
||||
|
||||
const rota = [
|
||||
const shifts = [
|
||||
{
|
||||
name: "Medic Early",
|
||||
role: "medic",
|
||||
roleId: idMedic,
|
||||
slots: [
|
||||
"d1 12:00 4h",
|
||||
"d2 12:00 4h",
|
||||
|
@ -154,7 +172,7 @@ const rota = [
|
|||
},
|
||||
{
|
||||
name: "Medic Late",
|
||||
role: "medic",
|
||||
roleId: idMedic,
|
||||
slots: [
|
||||
"d1 16:00 7h",
|
||||
"d2 16:00 6h",
|
||||
|
@ -164,7 +182,7 @@ const rota = [
|
|||
},
|
||||
{
|
||||
name: "Security Early",
|
||||
role: "security",
|
||||
roleId: idSecurity,
|
||||
slots: [
|
||||
"d1 12:00 6h",
|
||||
"d2 12:00 6h",
|
||||
|
@ -175,7 +193,7 @@ const rota = [
|
|||
},
|
||||
{
|
||||
name: "Security Late",
|
||||
role: "security",
|
||||
roleId: idSecurity,
|
||||
slots: [
|
||||
"d1 18:00 5h",
|
||||
"d2 18:00 4h",
|
||||
|
@ -183,13 +201,17 @@ const rota = [
|
|||
"d4 18:00 5h",
|
||||
],
|
||||
},
|
||||
]
|
||||
].map(({ slots, ...rest }) => ({
|
||||
...rest,
|
||||
id: eventId++,
|
||||
slots: slots.map(slot => `${slot} ${slotId++}`),
|
||||
}));
|
||||
|
||||
function toIso(date: Date) {
|
||||
return date.toISOString().replace(":00.000Z", "Z");
|
||||
}
|
||||
|
||||
function toDates(origin: Date, day: string, start: string, duration: string) {
|
||||
function toDate(origin: Date, day: string, start: string) {
|
||||
const [startHours, startMinutes] = start.split(":").map(time => parseInt(time, 10));
|
||||
const dayNumber = parseInt(day.slice(1));
|
||||
|
||||
|
@ -198,6 +220,11 @@ function toDates(origin: Date, day: string, start: string, duration: string) {
|
|||
startDate.setUTCHours(startDate.getUTCHours() + startHours);
|
||||
startDate.setUTCMinutes(startDate.getUTCMinutes() + startMinutes);
|
||||
|
||||
return startDate;
|
||||
}
|
||||
|
||||
function toDates(origin: Date, day: string, start: string, duration: string) {
|
||||
const startDate = toDate(origin, day, start);
|
||||
const [_, durationHours, durationMinutes] = /(?:(\d+)h)?(?:(\d+)m)?/.exec(duration)!;
|
||||
const durationTotal = parseInt(durationHours ?? "0") * 60 + parseInt(durationMinutes ?? "0")
|
||||
const endDate = new Date(startDate.getTime() + durationTotal * 60e3);
|
||||
|
@ -205,33 +232,35 @@ function toDates(origin: Date, day: string, start: string, duration: string) {
|
|||
return [startDate, endDate];
|
||||
}
|
||||
|
||||
function toSlot(origin: Date, id: string, shorthand: string, index: number, counts: Map<string, number>, idToAssigned: Map<string, number[]>): TimeSlot {
|
||||
const [day, start, duration, location] = shorthand.split(" ");
|
||||
function toSlot(origin: Date, shorthand: string, counts: Map<number, number>, idToAssigned: Map<number, number[]>): ApiScheduleEventSlot {
|
||||
const [day, start, duration, location, idStr] = shorthand.split(" ");
|
||||
const [startDate, endDate] = toDates(origin, day, start, duration);
|
||||
const id = parseInt(idStr, 10);
|
||||
|
||||
return {
|
||||
id: `${id}-${index}`,
|
||||
id,
|
||||
start: toIso(startDate),
|
||||
end: toIso(endDate),
|
||||
locations: [location],
|
||||
assigned: idToAssigned.get(`${id}-${index}`),
|
||||
interested: counts.get(`${id}-${index}`),
|
||||
locationIds: [locations.find(l => toId(l.name) === location)!.id],
|
||||
assigned: idToAssigned.get(id),
|
||||
interested: counts.get(id),
|
||||
};
|
||||
}
|
||||
|
||||
function toShift(origin: Date, id: string, shorthand: string, index: number, idToAssigned: Map<string, number[]>): ShiftSlot {
|
||||
const [day, start, duration] = shorthand.split(" ");
|
||||
function toShift(origin: Date, shorthand: string, idToAssigned: Map<number, number[]>): ApiScheduleShiftSlot {
|
||||
const [day, start, duration, idStr] = shorthand.split(" ");
|
||||
const [startDate, endDate] = toDates(origin, day, start, duration);
|
||||
const id = parseInt(idStr, 10);
|
||||
|
||||
return {
|
||||
id: `${id}-${index}`,
|
||||
id,
|
||||
start: toIso(startDate),
|
||||
end: toIso(endDate),
|
||||
assigned: idToAssigned.get(`${id}-${index}`),
|
||||
assigned: idToAssigned.get(id),
|
||||
};
|
||||
}
|
||||
|
||||
export function generateDemoSchedule(): Schedule {
|
||||
export function generateDemoSchedule(): ApiSchedule {
|
||||
const origin = new Date();
|
||||
const utcOffset = 1;
|
||||
origin.setUTCDate(origin.getUTCDate() - origin.getUTCDay() + 1); // Go to Monday
|
||||
|
@ -240,101 +269,98 @@ export function generateDemoSchedule(): Schedule {
|
|||
origin.setUTCSeconds(0);
|
||||
origin.setUTCMilliseconds(0);
|
||||
|
||||
const counts = new Map<string, number>()
|
||||
const eventCounts = new Map<number, number>()
|
||||
const slotCounts = new Map<number, number>()
|
||||
const accounts = generateDemoAccounts();
|
||||
for (const account of accounts) {
|
||||
for (const id of account.interestedIds ?? []) {
|
||||
counts.set(id, (counts.get(id) ?? 0) + 1);
|
||||
for (const id of account.interestedEventIds ?? []) {
|
||||
eventCounts.set(id, (eventCounts.get(id) ?? 0) + 1);
|
||||
}
|
||||
for (const id of account.interestedEventSlotIds ?? []) {
|
||||
slotCounts.set(id, (slotCounts.get(id) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
seed = 2;
|
||||
const idToAssigned = new Map<string, number[]>();
|
||||
for (const account of accounts.filter(a => a.type === "crew" || a.type === "admin")) {
|
||||
const assignedIds: string[] = [];
|
||||
const slotsToAdd = Math.floor(random() * 20);
|
||||
while (assignedIds.length < slotsToAdd) {
|
||||
const event = events[Math.floor(random() * events.length)];
|
||||
const eventId = toId(event.name);
|
||||
if (assignedIds.some(id => id.replace(/-\d+$/, "") === eventId)) {
|
||||
continue;
|
||||
}
|
||||
if (event.slots.length === 1 || random() < 0.8) {
|
||||
for (const index of event.slots.map((_, index) => index)) {
|
||||
assignedIds.push(toId(`${toId(event.name)}-${index}`));
|
||||
function assignSlots(events: { id: number, slots: string[] }[], count: number) {
|
||||
const idToAssigned = new Map<number, number[]>();
|
||||
for (const account of accounts.filter(a => a.type === "crew" || a.type === "admin")) {
|
||||
const assignedIds = new Set<number>;
|
||||
const usedEvents = new Set<number>;
|
||||
const slotsToAdd = Math.floor(random() * count);
|
||||
while (assignedIds.size < slotsToAdd) {
|
||||
const event = events[Math.floor(random() * events.length)];
|
||||
if (usedEvents.has(event.id)) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
for (const index of event.slots.map((_, index) => index)) {
|
||||
if (random() < 0.5) {
|
||||
assignedIds.push(toId(`${toId(event.name)}-${index}`));
|
||||
if (event.slots.length === 1 || random() < 0.8) {
|
||||
for (const slot of event.slots) {
|
||||
const id = parseInt(slot.split(" ").slice(-1)[0]);
|
||||
assignedIds.add(id);
|
||||
usedEvents.add(event.id);
|
||||
}
|
||||
} else {
|
||||
for (const slot of event.slots) {
|
||||
if (random() < 0.5) {
|
||||
const id = parseInt(slot.split(" ").slice(-1)[0]);
|
||||
assignedIds.add(id);
|
||||
usedEvents.add(event.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const id of assignedIds) {
|
||||
const assigned = idToAssigned.get(id);
|
||||
if (assigned) {
|
||||
assigned.push(account.id);
|
||||
} else {
|
||||
idToAssigned.set(id, [account.id]);
|
||||
for (const id of assignedIds) {
|
||||
const assigned = idToAssigned.get(id);
|
||||
if (assigned) {
|
||||
assigned.push(account.id);
|
||||
} else {
|
||||
idToAssigned.set(id, [account.id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return idToAssigned;
|
||||
}
|
||||
seed = 2;
|
||||
const eventSlotIdToAssigned = assignSlots(events, 20);
|
||||
|
||||
seed = 5;
|
||||
for (const account of accounts.filter(a => a.type === "crew" || a.type === "admin")) {
|
||||
const assignedIds: string[] = [];
|
||||
const slotsToAdd = Math.floor(random() * 3);
|
||||
while (assignedIds.length < slotsToAdd) {
|
||||
const shift = rota[Math.floor(random() * rota.length)];
|
||||
const shiftId = toId(shift.name);
|
||||
if (assignedIds.some(id => id.replace(/-\d+$/, "") === shiftId)) {
|
||||
continue;
|
||||
}
|
||||
if (shift.slots.length === 1 || random() < 0.8) {
|
||||
for (const index of shift.slots.map((_, index) => index)) {
|
||||
assignedIds.push(toId(`${toId(shift.name)}-${index}`));
|
||||
}
|
||||
} else {
|
||||
for (const index of shift.slots.map((_, index) => index)) {
|
||||
if (random() < 0.5) {
|
||||
assignedIds.push(toId(`${toId(shift.name)}-${index}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const id of assignedIds) {
|
||||
const assigned = idToAssigned.get(id);
|
||||
if (assigned) {
|
||||
assigned.push(account.id);
|
||||
} else {
|
||||
idToAssigned.set(id, [account.id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shiftSlotIdToAssigned = assignSlots(shifts, 3);
|
||||
|
||||
return {
|
||||
id: 111,
|
||||
updatedAt: toIso(toDate(origin, "d2", "10:01")),
|
||||
events: events.map(
|
||||
({ name, crew, description, slots }) => ({
|
||||
id: toId(name),
|
||||
({ id, name, crew, description, slots }) => ({
|
||||
id,
|
||||
name,
|
||||
crew,
|
||||
description,
|
||||
interested: counts.get(toId(name)),
|
||||
slots: slots.map((shorthand, index) => toSlot(origin, toId(name), shorthand, index, counts, idToAssigned))
|
||||
interested: eventCounts.get(id),
|
||||
slots: slots.map(shorthand => toSlot(origin, shorthand, slotCounts, eventSlotIdToAssigned)),
|
||||
updatedAt: toIso(toDate(origin, "d0", "15:11")),
|
||||
})
|
||||
),
|
||||
locations: locations.map(
|
||||
({ name, description }) => ({ id: toId(name), name, description })
|
||||
),
|
||||
roles,
|
||||
rota: rota.map(
|
||||
({ name, role, slots }) => ({
|
||||
id: toId(name),
|
||||
({ id, name, description, updatedAt }) => ({
|
||||
id,
|
||||
name,
|
||||
role,
|
||||
slots: slots.map((shorthand, index) => toShift(origin, toId(name), shorthand, index, idToAssigned))
|
||||
description,
|
||||
updatedAt: toIso(toDate(origin, ...(updatedAt.split(" ")) as [string, string])),
|
||||
})
|
||||
),
|
||||
roles: roles.map(
|
||||
({ id, name, updatedAt }) => ({
|
||||
id,
|
||||
name,
|
||||
updatedAt: toIso(toDate(origin, ...(updatedAt.split(" ")) as [string, string])),
|
||||
})
|
||||
),
|
||||
shifts: shifts.map(
|
||||
({ id, name, roleId, slots }) => ({
|
||||
id,
|
||||
name,
|
||||
roleId,
|
||||
slots: slots.map(shorthand => toShift(origin, shorthand, shiftSlotIdToAssigned)),
|
||||
updatedAt: toIso(toDate(origin, "d0", "13:23")),
|
||||
})
|
||||
)
|
||||
};
|
||||
|
@ -364,9 +390,9 @@ function random() {
|
|||
return (seed = (a * seed + c) % m | 0) / 2 ** 31;
|
||||
}
|
||||
|
||||
export function generateDemoAccounts(): Account[] {
|
||||
export function generateDemoAccounts(): ApiAccount[] {
|
||||
seed = 1;
|
||||
const accounts: Account[] = [];
|
||||
const accounts: ApiAccount[] = [];
|
||||
|
||||
for (const name of names) {
|
||||
accounts.push({
|
||||
|
@ -378,38 +404,45 @@ export function generateDemoAccounts(): Account[] {
|
|||
|
||||
seed = 1;
|
||||
// These have a much higher probability of being in someone's interested list.
|
||||
const desiredEvent = ["opening", "closing", "fursuit-games"];
|
||||
const desiredEvent = ["opening", "closing", "fursuit-games"].map(
|
||||
id => events.find(e => toId(e.name) === id)!.id
|
||||
);
|
||||
const nonCrewEvents = events.filter(event => !event.crew);
|
||||
|
||||
for (const account of accounts) {
|
||||
const interestedIds: string[] = [];
|
||||
const interestedEventIds = new Set<number>;
|
||||
const interestedSlotIds = new Set<number>;
|
||||
const usedEvents = new Set<number>;
|
||||
for (const id of desiredEvent) {
|
||||
if (random() < 0.5) {
|
||||
interestedIds.push(id);
|
||||
interestedEventIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
const eventsToAdd = Math.floor(random() * 10);
|
||||
while (interestedIds.length < eventsToAdd) {
|
||||
while (interestedEventIds.size + interestedSlotIds.size < eventsToAdd) {
|
||||
const event = nonCrewEvents[Math.floor(random() * nonCrewEvents.length)];
|
||||
const eventId = toId(event.name);
|
||||
if (interestedIds.some(id => id.replace(/-\d+$/, "") === eventId)) {
|
||||
if (usedEvents.has(event.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.slots.length === 1 || random() < 0.8) {
|
||||
interestedIds.push(toId(event.name))
|
||||
interestedEventIds.add(event.id)
|
||||
} else {
|
||||
for (const index of event.slots.map((_, index) => index)) {
|
||||
for (const slot of event.slots) {
|
||||
if (random() < 0.5) {
|
||||
interestedIds.push(toId(`${toId(event.name)}-${index}`));
|
||||
const id = parseInt(slot.split(" ")[4], 10);
|
||||
interestedSlotIds.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (interestedIds.length) {
|
||||
account.interestedIds = interestedIds;
|
||||
if (interestedEventIds.size) {
|
||||
account.interestedEventIds = [...interestedEventIds];
|
||||
}
|
||||
if (interestedSlotIds.size) {
|
||||
account.interestedEventSlotIds = [...interestedSlotIds];
|
||||
}
|
||||
}
|
||||
return accounts;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue