169 lines
3.9 KiB
TypeScript
169 lines
3.9 KiB
TypeScript
|
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,
|
||
|
};
|
||
|
}
|
||
|
}
|