owltide/utils/client-schedule.ts
Hornwitser 985b8e0950 Refactor base types for entities and tombstones
Rename the base Entity type to ApiEntity, and the base EntityToombstone
to ApiTombstone to better reflect the reality that its only used in the
API interface and that the client and server types uses its own base if
any.

Remove EntityLiving and pull EntityTombstone out of of the base entity
type so that the types based on ApiEntity are always living entities and
if it's possible for it to contain tombstone this will be explicitly
told with the type including a union with ApiTombstone.

Refactor the types of the ClientEntity and ClientMap to better reflect
the types of the entities it stores and converts to/from.
2025-06-24 15:19:11 +02:00

990 lines
24 KiB
TypeScript

import { DateTime, FixedOffsetZone, Zone } from "~/shared/utils/luxon";
import type {
ApiEntity,
ApiSchedule,
ApiScheduleEvent,
ApiScheduleEventSlot,
ApiScheduleLocation,
ApiScheduleRole,
ApiScheduleShift,
ApiScheduleShiftSlot,
ApiTombstone
} from "~/shared/types/api";
import type { Id } from "~/shared/types/common";
import { setEquals } from "~/shared/utils/functions";
import { ClientEntity } from "~/utils/client-entity";
function filterEntity<T extends ApiEntity>(entities?: (T | ApiTombstone)[]) {
return (entities ?? []).filter((entity) => !entity.deleted) as T[];
}
function filterTombstone<T extends ApiEntity>(entities?: (T | ApiTombstone)[]) {
return (entities ?? []).filter((entity) => entity.deleted) as ApiTombstone[];
}
export function toIso(timestamp: DateTime) {
return timestamp.setZone(FixedOffsetZone.utcInstance).toISO();
}
function mapWith<K, V>(map: Map<K, V>, key: K, value: V) {
const copy = new Map(map);
copy.set(key, value);
return copy;
}
function mapWithout<K, V>(map: Map<K, V>, key: K) {
const copy = new Map(map);
copy.delete(key);
return copy;
}
export class ClientScheduleLocation extends ClientEntity<ApiScheduleLocation> {
serverName: string;
serverDescription: string;
constructor(
id: Id,
updatedAt: DateTime,
deleted: boolean,
public name: string,
public description: string,
) {
super(id, updatedAt, deleted);
this.serverName = name;
this.serverDescription = description;
}
override isModified() {
return (
super.isModified()
|| this.name !== this.serverName
|| this.description !== this.serverDescription
);
}
override discard() {
if (this.isNew()) {
throw new Error("ClientScheduleLocation.discard: Cannot discard new entity.")
}
this.updatedAt = this.serverUpdatedAt;
this.deleted = this.serverDeleted;
this.name = this.serverName;
this.description = this.description;
}
static create(
id: Id,
name: string,
description: string,
opts: { zone: Zone, locale: string },
) {
return new ClientScheduleLocation(
id,
DateTime.fromMillis(ClientEntity.newEntityMillis, opts),
false,
name,
description,
);
}
static fromApi(api: ApiScheduleLocation, opts: { zone: Zone, locale: string }) {
return new this(
api.id,
DateTime.fromISO(api.updatedAt, opts),
api.deleted ?? false,
api.name,
api.description ?? "",
);
}
override apiUpdate(api: ApiScheduleLocation, opts: { zone: Zone, locale: string }) {
const wasModified = this.isModified();
this.serverUpdatedAt = DateTime.fromISO(api.updatedAt, opts);
this.serverDeleted = false;
this.serverName = api.name;
this.serverDescription = api.description || "";
if (!wasModified || !this.isModified()) {
this.discard();
}
}
toApi(): ApiScheduleLocation | ApiTombstone {
if (this.deleted) {
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
deleted: true,
}
}
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
name: this.name,
description: this.description || undefined,
}
}
}
export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
schedule!: ClientSchedule;
serverName: string;
serverCrew: boolean;
serverHost: string;
serverCancelled: boolean;
serverDescription: string;
serverInterested: number;
serverSlotIds: Set<Id>;
constructor(
id: Id,
updatedAt: DateTime,
deleted: boolean,
public name: string,
public crew: boolean,
public host: string,
public cancelled: boolean,
public description: string,
public interested: number,
public slotIds: Set<Id>,
) {
super(id, updatedAt, deleted);
this.serverName = name;
this.serverCrew = crew;
this.serverHost = host;
this.serverCancelled = cancelled;
this.serverDescription = description;
this.serverInterested = interested;
this.serverSlotIds = new Set(slotIds);
}
get slots(): ReadonlyMap<Id, ClientScheduleEventSlot> {
return new Map([...this.slotIds].map(id => [id, this.schedule.eventSlots.get(id)!]));
}
override isModified() {
return (
super.isModified()
|| this.name !== this.serverName
|| this.crew !== this.serverCrew
|| this.host !== this.serverHost
|| this.cancelled !== this.serverCancelled
|| this.description !== this.serverDescription
|| this.interested !== this.serverInterested
|| !setEquals(this.slotIds, this.serverSlotIds)
|| [...this.slotIds].some(id => this.schedule.eventSlots.get(id)!.isModified())
);
}
override discard() {
if (this.isNew()) {
throw new Error("ClientScheduleEvent.discard: Cannot discard new entity.")
}
this.updatedAt = this.serverUpdatedAt;;
this.deleted = this.serverDeleted;;
this.name = this.serverName;
this.crew = this.serverCrew;
this.host = this.serverHost;
this.cancelled = this.serverCancelled;
this.description = this.serverDescription;
this.interested = this.serverInterested;
for (const id of this.serverSlotIds) {
this.schedule.eventSlots.get(id)!.discard();
}
for (const id of this.slotIds.difference(this.serverSlotIds)) {
const slot = this.schedule.eventSlots.get(id)!;
slot.setEventId(slot.serverEventId);
}
}
discardSlot(id: Id) {
if (!this.slotIds.has(id)) {
throw new Error("ClientScheduleEvent.discardSlot: slot does not exist");
}
const slot = this.schedule.eventSlots.get(id)!;
if (slot.isNewEntity) {
this.slotIds.delete(id);
this.schedule.eventSlots.delete(id);
} else {
slot.discard();
}
}
static create(
schedule: ClientSchedule,
id: Id,
name: string,
crew: boolean,
host: string,
cancelled: boolean,
description: string,
interested: number,
slotIds: Set<Id>,
opts: { zone: Zone, locale: string },
) {
const event = new ClientScheduleEvent(
id,
DateTime.fromMillis(ClientEntity.newEntityMillis, opts),
false,
name,
crew,
host,
cancelled,
description,
interested,
slotIds,
);
event.schedule = schedule;
return event;
}
static fromApi(
api: ApiScheduleEvent,
opts: { zone: Zone, locale: string },
) {
return new this(
api.id,
DateTime.fromISO(api.updatedAt, opts),
api.deleted ?? false,
api.name,
api.crew ?? false,
api.host ?? "",
api.cancelled ?? false,
api.description ?? "",
api.interested ?? 0,
new Set(api.slots.map(slot => slot.id)),
);
}
override apiUpdate(
api: ApiScheduleEvent,
opts: { zone: Zone, locale: string },
) {
const wasModified = this.isModified();
this.serverUpdatedAt = DateTime.fromISO(api.updatedAt, opts);
this.serverDeleted = false;
this.serverName = api.name;
this.serverCrew = api.crew ?? false;
this.serverHost = api.host ?? "";
this.serverCancelled = api.cancelled ?? false;
this.serverDescription = api.description ?? "";
this.serverInterested = api.interested ?? 0;
this.serverSlotIds = new Set(api.slots.map(slot => slot.id));
if (!wasModified || !this.isModified()) {
this.discard();
}
}
toApi(): ApiScheduleEvent | ApiTombstone {
if (this.deleted) {
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
deleted: true,
}
}
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
name: this.name,
crew: this.crew || undefined,
host: this.host || undefined,
cancelled: this.cancelled || undefined,
description: this.description || undefined,
interested: this.interested || undefined,
slots: [...this.slots.values()].filter(slot => !slot.deleted).map(slot => slot.toApi()),
}
}
}
export class ClientScheduleEventSlot {
schedule!: ClientSchedule;
serverDeleted: boolean;
serverEventId: Id;
serverStart: DateTime;
serverEnd: DateTime;
serverLocationIds: Set<Id>;
serverAssigned: Set<Id>;
serverInterested: number;
constructor(
public id: Id,
public isNewEntity: boolean,
public deleted: boolean,
public eventId: Id,
public start: DateTime,
public end: DateTime,
public locationIds: Set<Id>,
public assigned: Set<Id>,
public interested: number,
) {
this.serverDeleted = deleted;
this.serverEventId = eventId;
this.serverStart = start;
this.serverEnd = end;
this.serverLocationIds = new Set(locationIds);
this.serverAssigned = new Set(assigned);
this.serverInterested = interested;
}
isModified() {
return (
this.isNewEntity
|| this.deleted
|| this.serverDeleted
|| this.eventId !== this.serverEventId
|| this.start.toMillis() !== this.serverStart.toMillis()
|| this.end.toMillis() !== this.serverEnd.toMillis()
|| !setEquals(this.locationIds, this.serverLocationIds)
|| !setEquals(this.assigned, this.serverAssigned)
|| this.interested !== this.serverInterested
);
}
setEventId(newEventId: Id) {
if (this.eventId === newEventId)
return;
this.schedule.events.get(newEventId)!.slotIds.add(this.id);
this.schedule.events.get(this.eventId)!.slotIds.delete(this.id);
this.eventId = newEventId;
}
discard() {
if (this.isNewEntity) {
throw new Error("ClientScheduleEventSlot.discard: Cannot discard new slot");
}
this.deleted = this.serverDeleted;
this.setEventId(this.serverEventId);
this.start = this.serverStart;
this.end = this.serverEnd;
this.locationIds = new Set(this.serverLocationIds);
this.assigned = new Set(this.serverAssigned);
this.interested = this.serverInterested;
}
static create(
schedule: ClientSchedule,
id: Id,
eventId: Id,
start: DateTime,
end: DateTime,
locationIds: Set<Id>,
assigned: Set<Id>,
interested: number,
) {
const slot = new ClientScheduleEventSlot(
id,
true,
false,
eventId,
start,
end,
locationIds,
assigned,
interested,
);
slot.schedule = schedule;
return slot;
}
static fromApi(
api: ApiScheduleEventSlot,
eventId: Id,
opts: { zone: Zone, locale: string },
) {
return new this(
api.id,
false,
false,
eventId,
DateTime.fromISO(api.start, opts),
DateTime.fromISO(api.end, opts),
new Set(api.locationIds),
new Set(api.assigned),
api.interested ?? 0,
);
}
apiUpdate(
api: ApiScheduleEventSlot,
eventId: Id,
opts: { zone: Zone, locale: string }
) {
const wasModified = this.isModified();
this.isNewEntity = false;
this.serverDeleted = false;
this.serverEventId = eventId;
this.serverStart = DateTime.fromISO(api.start, opts);
this.serverEnd = DateTime.fromISO(api.end, opts);
this.serverLocationIds = new Set(api.locationIds);
this.serverAssigned = new Set(api.assigned);
this.serverInterested = api.interested ?? 0;
if (!wasModified || !this.isModified()) {
this.discard();
}
}
toApi(): ApiScheduleEventSlot {
if (this.deleted) {
throw new Error("ClientScheduleEventSlot.toApi: Unexpected deleted slot")
}
return {
id: this.id,
start: toIso(this.start),
end: toIso(this.end),
locationIds: [...this.locationIds],
assigned: this.assigned.size ? [...this.assigned] : undefined,
interested: this.interested || undefined,
}
}
}
export class ClientScheduleRole extends ClientEntity<ApiScheduleRole> {
serverName: string;
serverDescription: string;
constructor(
id: Id,
updatedAt: DateTime,
deleted: boolean,
public name: string,
public description: string,
) {
super(id, updatedAt, deleted);
this.serverName = name;
this.serverDescription = description;
}
override isModified() {
return (
super.isModified()
|| this.name !== this.serverName
|| this.description !== this.serverDescription
);
}
override discard() {
if (this.isNew()) {
throw new Error("ClientScheduleRole.discard: Cannot discard new entity.")
}
this.updatedAt = this.serverUpdatedAt;
this.deleted = this.serverDeleted;
this.name = this.serverName;
this.description = this.description;
}
static create(
id: Id,
name: string,
description: string,
opts: { zone: Zone, locale: string },
) {
return new ClientScheduleRole(
id,
DateTime.fromMillis(ClientEntity.newEntityMillis, opts),
false,
name,
description,
);
}
static fromApi(api: ApiScheduleRole, opts: { zone: Zone, locale: string }) {
return new this(
api.id,
DateTime.fromISO(api.updatedAt, opts),
api.deleted ?? false,
api.name,
api.description ?? "",
);
}
override apiUpdate(api: ApiScheduleRole, opts: { zone: Zone, locale: string }) {
const wasModified = this.isModified();
this.serverUpdatedAt = DateTime.fromISO(api.updatedAt, opts);
this.serverDeleted = false;
this.serverName = api.name;
this.serverDescription = api.description || "";
if (!wasModified || !this.isModified()) {
this.discard();
}
}
toApi(): ApiScheduleRole | ApiTombstone {
if (this.deleted) {
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
deleted: true,
}
}
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
name: this.name,
description: this.description || undefined,
}
}
}
export class ClientScheduleShift extends ClientEntity<ApiScheduleShift> {
schedule!: ClientSchedule;
serverRoleId: Id;
serverName: string;
serverDescription: string;
serverSlotIds: Set<Id>;
constructor(
id: Id,
updatedAt: DateTime,
deleted: boolean,
public roleId: Id,
public name: string,
public description: string,
public slotIds: Set<Id>,
) {
super(id, updatedAt, deleted);
this.serverRoleId = roleId;
this.serverName = name;
this.serverDescription = description;
this.serverSlotIds = new Set(slotIds);
}
get slots(): ReadonlyMap<Id, ClientScheduleShiftSlot> {
return new Map([...this.slotIds].map(id => [id, this.schedule.shiftSlots.get(id)!]));
}
override isModified() {
return (
super.isModified()
|| this.roleId !== this.serverRoleId
|| this.name !== this.serverName
|| this.description !== this.serverDescription
|| !setEquals(this.slotIds, this.serverSlotIds)
|| [...this.slotIds].some(id => this.schedule.shiftSlots.get(id)!.isModified())
);
}
override discard() {
if (this.isNew()) {
throw new Error("ClientScheduleShift.discard: Cannot discard new entity.")
}
this.updatedAt = this.serverUpdatedAt;;
this.deleted = this.serverDeleted;;
this.roleId = this.serverRoleId;
this.name = this.serverName;
this.description = this.serverDescription;
for (const id of this.serverSlotIds) {
this.schedule.shiftSlots.get(id)!.discard();
}
for (const id of this.slotIds.difference(this.serverSlotIds)) {
const slot = this.schedule.shiftSlots.get(id)!;
slot.setShiftId(slot.serverShiftId);
}
}
discardSlot(id: Id) {
if (!this.slotIds.has(id)) {
throw new Error("ClientScheduleShift.discardSlot: slot does not exist");
}
const slot = this.schedule.shiftSlots.get(id)!;
if (slot.isNewEntity) {
this.slotIds.delete(id);
this.schedule.shiftSlots.delete(id);
} else {
slot.discard();
}
}
static create(
schedule: ClientSchedule,
id: Id,
roleId: Id,
name: string,
description: string,
slotIds: Set<Id>,
opts: { zone: Zone, locale: string },
) {
const shift = new ClientScheduleShift(
id,
DateTime.fromMillis(ClientEntity.newEntityMillis, opts),
false,
roleId,
name,
description,
slotIds,
);
shift.schedule = schedule;
return shift;
}
static fromApi(
api: ApiScheduleShift,
opts: { zone: Zone, locale: string },
) {
return new this(
api.id,
DateTime.fromISO(api.updatedAt, opts),
api.deleted ?? false,
api.roleId,
api.name,
api.description ?? "",
new Set(api.slots.map(slot => slot.id)),
);
}
override apiUpdate(
api: ApiScheduleShift,
opts: { zone: Zone, locale: string },
) {
const wasModified = this.isModified();
this.serverUpdatedAt = DateTime.fromISO(api.updatedAt, opts);
this.serverDeleted = false;
this.roleId = api.roleId;
this.serverName = api.name;
this.serverDescription = api.description ?? "";
this.serverSlotIds = new Set(api.slots.map(slot => slot.id));
if (!wasModified || !this.isModified()) {
this.discard();
}
}
toApi(): ApiScheduleShift | ApiTombstone {
if (this.deleted) {
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
deleted: true,
}
}
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
roleId: this.roleId,
name: this.name,
description: this.description || undefined,
slots: [...this.slots.values()].filter(slot => !slot.deleted).map(slot => slot.toApi()),
}
}
}
export class ClientScheduleShiftSlot {
schedule!: ClientSchedule;
serverDeleted: boolean;
serverShiftId: Id;
serverStart: DateTime;
serverEnd: DateTime;
serverAssigned: Set<Id>;
constructor(
public id: Id,
public isNewEntity: boolean,
public deleted: boolean,
public shiftId: Id,
public start: DateTime,
public end: DateTime,
public assigned: Set<Id>,
) {
this.serverDeleted = deleted;
this.serverShiftId = shiftId;
this.serverStart = start;
this.serverEnd = end;
this.serverAssigned = new Set(assigned);
}
isModified() {
return (
this.isNewEntity
|| this.deleted
|| this.serverDeleted
|| this.shiftId !== this.serverShiftId
|| this.start.toMillis() !== this.serverStart.toMillis()
|| this.end.toMillis() !== this.serverEnd.toMillis()
|| !setEquals(this.assigned, this.serverAssigned)
);
}
setShiftId(newShiftId: Id) {
if (this.shiftId === newShiftId)
return;
this.schedule.shifts.get(newShiftId)!.slotIds.add(this.id);
this.schedule.shifts.get(this.shiftId)!.slotIds.delete(this.id);
this.shiftId = newShiftId;
}
discard() {
if (this.isNewEntity) {
throw new Error("ClientScheduleShiftSlot.discard: Cannot discard new slot");
}
this.deleted = this.serverDeleted;
this.setShiftId(this.serverShiftId);
this.start = this.serverStart;
this.end = this.serverEnd;
this.assigned = new Set(this.serverAssigned);
}
static create(
schedule: ClientSchedule,
id: Id,
shiftId: Id,
start: DateTime,
end: DateTime,
assigned: Set<Id>,
) {
const slot = new ClientScheduleShiftSlot(
id,
true,
false,
shiftId,
start,
end,
assigned,
);
slot.schedule = schedule;
return slot;
}
static fromApi(
api: ApiScheduleShiftSlot,
shiftId: Id,
opts: { zone: Zone, locale: string },
) {
return new this(
api.id,
false,
false,
shiftId,
DateTime.fromISO(api.start, opts),
DateTime.fromISO(api.end, opts),
new Set(api.assigned),
);
}
apiUpdate(
api: ApiScheduleShiftSlot,
shiftId: Id,
opts: { zone: Zone, locale: string },
) {
const wasModified = this.isModified();
this.isNewEntity = false;
this.serverDeleted = false;
this.serverShiftId = shiftId;
this.serverStart = DateTime.fromISO(api.start, opts);
this.serverEnd = DateTime.fromISO(api.end, opts);
this.serverAssigned = new Set(api.assigned);
if (!wasModified || !this.isModified()) {
this.discard();
}
}
toApi(): ApiScheduleShiftSlot {
if (this.deleted) {
throw new Error("ClientScheduleShiftSlot.toApi: Unexpected deleted slot")
}
return {
id: this.id,
start: toIso(this.start),
end: toIso(this.end),
assigned: this.assigned.size ? [...this.assigned] : undefined,
}
}
}
export class ClientSchedule extends ClientEntity<ApiSchedule> {
nextClientId = -1;
constructor(
id: 111,
updatedAt: DateTime,
deleted: boolean,
public locations: ClientMap<ClientScheduleLocation>,
public events: ClientMap<ClientScheduleEvent>,
public eventSlots: Map<Id, ClientScheduleEventSlot>,
public roles: ClientMap<ClientScheduleRole>,
public shifts: ClientMap<ClientScheduleShift>,
public shiftSlots: Map<Id, ClientScheduleShiftSlot>,
) {
super(id, updatedAt, deleted);
// XXX For now the prototype server is assigning client ids instead of remapping them.
this.nextClientId = Math.min(
0,
...locations.keys(),
...events.keys(),
...eventSlots.keys(),
...roles.keys(),
...shifts.keys(),
...shiftSlots.keys(),
) - 1;
}
discard() {
this.locations.discard();
this.events.discard();
this.roles.discard();
this.shifts.discard();
}
override isModified() {
return (
super.isModified()
|| [...this.locations.values()].some(location => location.isModified())
|| [...this.events.values()].some(event => event.isModified())
|| [...this.roles.values()].some(role => role.isModified())
|| [...this.shifts.values()].some(shift => shift.isModified())
);
}
private checkLocationRefsForDeletion(id: Id) {
for (const event of this.events.values()) {
if (event.deleted)
continue;
for (const slot of event.slots.values()) {
if (slot.locationIds.has(id)) {
throw new Error(`Cannot delete location, event "${event.name}" depends on it`);
}
}
}
}
private checkRoleRefsForDeletion(id: Id) {
for (const shift of this.shifts.values()) {
if (shift.deleted) {
continue;
}
if (shift.roleId === id) {
throw new Error(`Cannot delete role, shift "${shift.name}" depends on it`);
}
}
}
static fromApi(api: ApiSchedule, opts: { zone: Zone, locale: string }) {
const eventSlots = idMap((api.events ?? [])
.filter(event => !event.deleted)
.flatMap(event => event.slots.map(slot => ClientScheduleEventSlot.fromApi(slot, event.id, opts)))
)
const shiftSlots = idMap((api.shifts ?? [])
.filter(shift => !shift.deleted)
.flatMap(shift => shift.slots.map(slot => ClientScheduleShiftSlot.fromApi(slot, shift.id, opts)))
)
const schedule = new this(
api.id,
DateTime.fromISO(api.updatedAt, opts),
api.deleted ?? false,
ClientMap.fromApi(ClientScheduleLocation, api.locations ?? [], opts),
ClientMap.fromApi(ClientScheduleEvent, api.events ?? [], opts),
eventSlots,
ClientMap.fromApi(ClientScheduleRole, api.roles ?? [], opts),
ClientMap.fromApi(ClientScheduleShift, api.shifts ?? [], opts),
shiftSlots,
);
for (const event of schedule.events.values()) {
event.schedule = schedule;
}
for (const eventSlot of schedule.eventSlots.values()) {
eventSlot.schedule = schedule;
}
for (const shift of schedule.shifts.values()) {
shift.schedule = schedule;
}
for (const shiftSlot of schedule.shiftSlots.values()) {
shiftSlot.schedule = schedule;
}
return schedule;
}
toApi(diff = false): ApiSchedule | ApiTombstone {
if (this.deleted) {
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
deleted: true,
}
}
return {
id: this.id as 111,
updatedAt: toIso(this.updatedAt),
locations: this.locations.size ? this.locations.toApi(diff) : undefined,
events: this.events.size ? this.events.toApi(diff) : undefined,
roles: this.roles.size ? this.roles.toApi(diff) : undefined,
shifts: this.shifts.size ? this.shifts.toApi(diff) : undefined,
}
}
override apiUpdate(update: ApiSchedule, opts: { zone: Zone, locale: string }) {
if (update.deleted)
throw new Error("ClientSchedule.apiUpdate: Unexpected deletion");
if (update.id !== this.id)
throw new Error("ClientSchedule.apiUpdate: id mismatch");
this.updatedAt = DateTime.fromISO(update.updatedAt, opts);
if (update.locations)
this.locations.apiUpdate(update.locations, opts);
if (update.events) {
for (const apiEvent of update.events) {
if (!apiEvent.deleted) {
for (const apiSlot of apiEvent.slots) {
let slot = this.eventSlots.get(apiSlot.id);
if (slot) {
slot.apiUpdate(apiSlot, apiEvent.id, opts);
} else {
slot = ClientScheduleEventSlot.fromApi(apiSlot, apiEvent.id, opts);
slot.schedule = this;
this.eventSlots.set(slot.id, slot);
}
}
}
}
this.events.apiUpdate(update.events, opts);
const serverSlotIds = new Set([...this.events.values()].flatMap(event => [...event.serverSlotIds]));
for (const event of this.events.values()) {
for (const slotId of event.slotIds) {
if (!serverSlotIds.has(slotId)) {
const slot = this.eventSlots.get(slotId)!;
if (!slot.isNewEntity) {
if (slot.deleted) {
event.slotIds.delete(slotId);
this.eventSlots.delete(slotId);
} else {
slot.serverDeleted = true;
}
}
}
}
}
}
if (update.roles)
this.roles.apiUpdate(update.roles, opts);
if (update.shifts) {
for (const apiShift of update.shifts) {
if (!apiShift.deleted) {
for (const apiSlot of apiShift.slots) {
let slot = this.shiftSlots.get(apiSlot.id);
if (slot) {
slot.apiUpdate(apiSlot, apiShift.id, opts);
} else {
slot = ClientScheduleShiftSlot.fromApi(apiSlot, apiShift.id, opts);
slot.schedule = this;
this.shiftSlots.set(slot.id, slot);
}
}
}
}
this.shifts.apiUpdate(update.shifts, opts);
const serverSlotIds = new Set([...this.shifts.values()].flatMap(shift => [...shift.serverSlotIds]));
for (const shift of this.shifts.values()) {
for (const slotId of shift.slotIds) {
if (!serverSlotIds.has(slotId)) {
const slot = this.shiftSlots.get(slotId)!;
if (!slot.isNewEntity) {
if (slot.deleted) {
shift.slotIds.delete(slotId);
this.shiftSlots.delete(slotId);
} else {
slot.serverDeleted = true;
}
}
}
}
}
}
}
}