import type { ApiUser, ApiUserType } from "~/shared/types/api"; import type { Entity, EntityLiving, Id, Living } from "~/shared/types/common"; import { DateTime, Zone } from "~/shared/utils/luxon"; export abstract class ClientEntityNew { /** Millisecond offset used to indicate this is a new entitity. */ static newEntityMillis = -1; /** Timestamp of the entity received from server. If this is a new entity this will have a millisecond offset equal to {@link ClientEntityNew.newEntityMillis}. */ serverUpdatedAt: DateTime; /** True if the server has deleted this entity, but the client is holding on to it in order to resolve an edit conflcit */ serverDeleted: boolean; constructor( /** Server supplied id of this entity. Each kind of entity has its own namespace of ids. */ public readonly id: Id, /** Server's timestamp of this entity at the time it was modified. If the entity is unmodified this will track {@link serverUpdatedAt}. If this is a new entity it'll have a millesecond offset equal to {@link ClientEntityNew.newEntityMillis}. */ public updatedAt: DateTime, /** Flag indicating the client intends to delete this entity. */ public deleted: boolean, ) { this.serverUpdatedAt = updatedAt; this.serverDeleted = deleted; } /** True if this entity does not yet exist on the server. */ isNew() { return this.serverUpdatedAt.toMillis() === ClientEntityNew.newEntityMillis; } /** True if both the server and the client have modified this entity independently of each other. */ isConflict() { return this.serverUpdatedAt.toMillis() !== this.updatedAt.toMillis(); } /** True if this entity has been modified on the client. */ isModified() { return ( this.isNew() || this.deleted || this.serverDeleted ); } /** Discard any client side modifications to this entity. Not allowed if {@link serverDeleted} is true or this is a new entity. */ abstract discard(): void /** Apply an update delivered from the API to this entity. */ abstract apiUpdate(api: EntityLiving, opts: { zone: Zone, locale: string }): void /** Serialise this entity to the API format. Not allowed if {@link deleted} is true. */ abstract toApi(): Entity } export class ClientUser extends ClientEntityNew { static type = "user"; serverName: string | undefined; serverType: ApiUserType constructor( id: Id, updatedAt: DateTime, deleted: boolean, public name: string | undefined, public type: ApiUserType, ) { super(id, updatedAt, deleted); this.serverName = name; this.serverType = type; } override isModified() { return ( super.isModified() || this.name !== this.serverName || this.type !== this.serverType ); } override discard() { if (this.isNew()) { throw new Error("ClientUser.discard: Cannot discard new entity.") } this.updatedAt = this.serverUpdatedAt; this.name = this.serverName; this.type = this.serverType; } static create( id: Id, name: string | undefined, type: ApiUserType, opts: { zone: Zone, locale: string }, ) { return new this( id, DateTime.fromMillis(ClientEntityNew.newEntityMillis, opts), false, name, type, ) } static fromApi(api: Living, opts: { zone: Zone, locale: string }) { try { return new this( api.id, DateTime.fromISO(api.updatedAt, opts), false, api.name, api.type, ); } catch (err) { console.error(api); throw err; } } override apiUpdate(api: Living, opts: { zone: Zone, locale: string }) { const wasModified = this.isModified(); this.serverUpdatedAt = DateTime.fromISO(api.updatedAt, opts); this.serverDeleted = false; this.serverName = api.name; this.serverType = api.type; if (!wasModified || !this.isModified()) { this.discard(); } } toApi(): ApiUser { return { id: this.id, updatedAt: toIso(this.updatedAt), name: this.name, type: this.type, }; } }