Use a single mutable location, event, slot, etc, for each unique resource that keeps track of the local editable client copy and the server copy of the data contained in it. This makes it much simpler to update these data structures as I can take advantage of the v-model bindings in Vue.js and work with the system instead of against it.
149 lines
3.7 KiB
TypeScript
149 lines
3.7 KiB
TypeScript
import { type Entity, type EntityLiving, type Id, type Living } from "~/shared/types/common";
|
|
import { DateTime, Zone } from "~/shared/utils/luxon";
|
|
import { ClientEntity } from "~/utils/client-entity";
|
|
|
|
export interface EntityClass<T extends ClientEntity> {
|
|
fromApi(api: EntityLiving, opts: { zone: Zone, locale: string }): T,
|
|
}
|
|
|
|
export class ClientMap<T extends ClientEntity> {
|
|
constructor(
|
|
public EntityClass: EntityClass<T>,
|
|
public map: Map<Id, T>,
|
|
public tombstones: Map<Id, number>,
|
|
) {
|
|
}
|
|
|
|
get(id: Id) {
|
|
return this.map.get(id);
|
|
}
|
|
|
|
keys() {
|
|
return this.map.keys();
|
|
}
|
|
|
|
values() {
|
|
return this.map.values();
|
|
}
|
|
|
|
get size() {
|
|
return this.map.size;
|
|
}
|
|
|
|
isModified() {
|
|
return [...this.map.values()].some(entity => entity.isModified());
|
|
}
|
|
|
|
add(entity: T) {
|
|
if (this.map.has(entity.id)) {
|
|
throw new Error("ClientMap.add: Entity already exists");
|
|
}
|
|
this.map.set(entity.id, entity);
|
|
}
|
|
|
|
discard() {
|
|
for (const id of this.keys()) {
|
|
this.discardId(id);
|
|
}
|
|
}
|
|
|
|
discardId(id: Id) {
|
|
const entity = this.map.get(id);
|
|
if (!entity) {
|
|
throw new Error("ClientMap.discard: entity does not exist.");
|
|
}
|
|
if (
|
|
this.tombstones.has(id)
|
|
|| entity.isNew()
|
|
) {
|
|
this.map.delete(id);
|
|
} else {
|
|
entity.discard();
|
|
}
|
|
}
|
|
|
|
static fromApi<T extends Entity, U extends ClientEntity>(
|
|
EntityClass: EntityClass<U>,
|
|
entities: T[],
|
|
opts: { zone: Zone, locale: string },
|
|
) {
|
|
const living = entities.filter(entity => !entity.deleted) as Living<T>[];
|
|
const tombstones = entities.filter(entity => entity.deleted === true);
|
|
return new this(
|
|
EntityClass,
|
|
idMap(living.map(apiEntity => EntityClass.fromApi(apiEntity, opts))),
|
|
new Map(tombstones.map(tombstone => [tombstone.id, Date.parse(tombstone.updatedAt)])),
|
|
);
|
|
}
|
|
|
|
apiUpdate(entities: Entity[], opts: { zone: Zone, locale: string }) {
|
|
const living = entities.filter(entity => !entity.deleted);
|
|
const tombstones = entities.filter(entity => entity.deleted === true);
|
|
for (const entity of living) {
|
|
const tombstoneMs = this.tombstones.get(entity.id);
|
|
const updatedMs = Date.parse(entity.updatedAt);
|
|
if (tombstoneMs !== undefined) {
|
|
if (tombstoneMs > updatedMs) {
|
|
continue;
|
|
}
|
|
this.tombstones.delete(entity.id);
|
|
}
|
|
|
|
const stored = this.map.get(entity.id);
|
|
if (stored) {
|
|
if (stored.updatedAt.toMillis() > updatedMs) {
|
|
continue;
|
|
}
|
|
stored.apiUpdate(entity, opts);
|
|
} else {
|
|
this.map.set(entity.id, this.EntityClass.fromApi(entity, opts))
|
|
}
|
|
}
|
|
for (const tombstone of tombstones) {
|
|
const updatedMs = Date.parse(tombstone.updatedAt);
|
|
const tombstoneMs = this.tombstones.get(tombstone.id);
|
|
const stored = this.map.get(tombstone.id);
|
|
if (
|
|
stored && stored.serverUpdatedAt.toMillis() > updatedMs
|
|
|| tombstoneMs && tombstoneMs > updatedMs
|
|
) {
|
|
continue;
|
|
}
|
|
this.tombstones.set(tombstone.id, updatedMs);
|
|
if (stored && stored.isModified() && !stored.deleted) {
|
|
stored.serverUpdatedAt = DateTime.fromISO(tombstone.updatedAt, opts);
|
|
stored.serverDeleted = true;
|
|
} else if (stored) {
|
|
this.map.delete(tombstone.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
toApi(diff: boolean): Entity[] {
|
|
if (!diff) {
|
|
return [
|
|
...[...this.map.values()].map(entity => entity.toApi()),
|
|
...[...this.tombstones].map(([id, updatedMs]) => ({
|
|
id,
|
|
updatedAt: new Date(updatedMs).toISOString(),
|
|
deleted: true as const,
|
|
})),
|
|
];
|
|
}
|
|
|
|
return [
|
|
...[...this.map.values()]
|
|
.filter(entity => entity.isModified() && !entity.deleted)
|
|
.map(entity => entity.toApi())
|
|
,
|
|
...[...this.map.values()]
|
|
.filter(entity => entity.deleted)
|
|
.map(entity => ({
|
|
id: entity.id,
|
|
updatedAt: toIso(entity.updatedAt),
|
|
deleted: true as const,
|
|
}))
|
|
,
|
|
];
|
|
}
|
|
}
|