owltide/utils/client-user.ts

169 lines
3.9 KiB
TypeScript
Raw Normal View History

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<ApiUser>, 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<ApiUser>, 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,
};
}
}