Remove type from Api serialisation of ClientMap
Move the logic that converts the EntityClass of a map to a string and then back into the class to the payload plugin in order to avoid a circular dependency where the ClientMap needs to know the entity classes and the entity classes needs to know the ClientMap. The only place that doesn't know the type of the entities stored in the client map is the payload reviver, so it makes sense to keep this logic contained to the payload plugin.
This commit is contained in:
parent
930d93a95f
commit
d48fb035b4
4 changed files with 70 additions and 91 deletions
|
@ -1,4 +1,10 @@
|
||||||
import { Info } from "~/shared/utils/luxon";
|
import { Info } from "~/shared/utils/luxon";
|
||||||
|
import { ClientEntityNew } from "~/utils/client-user";
|
||||||
|
|
||||||
|
const typeMap: Record<string, EntityClass<ClientEntityNew>> = {
|
||||||
|
"user": ClientUser,
|
||||||
|
};
|
||||||
|
const classMap = new Map(Object.entries(typeMap).map(([k, v]) => [v, k]));
|
||||||
|
|
||||||
export default definePayloadPlugin(() => {
|
export default definePayloadPlugin(() => {
|
||||||
definePayloadReducer(
|
definePayloadReducer(
|
||||||
|
@ -7,8 +13,10 @@ export default definePayloadPlugin(() => {
|
||||||
if (!(data instanceof ClientMap)) {
|
if (!(data instanceof ClientMap)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const type = classMap.get(data.EntityClass)!;
|
||||||
const accountStore = useAccountStore();
|
const accountStore = useAccountStore();
|
||||||
return {
|
return {
|
||||||
|
type,
|
||||||
timezone: accountStore.activeTimezone,
|
timezone: accountStore.activeTimezone,
|
||||||
locale: accountStore.activeLocale,
|
locale: accountStore.activeLocale,
|
||||||
api: data.toApi(false),
|
api: data.toApi(false),
|
||||||
|
@ -17,9 +25,10 @@ export default definePayloadPlugin(() => {
|
||||||
);
|
);
|
||||||
definePayloadReviver(
|
definePayloadReviver(
|
||||||
"ClientMap",
|
"ClientMap",
|
||||||
({ timezone, locale, api }) => {
|
({ type, timezone, locale, api }) => {
|
||||||
|
const EntityClass = typeMap[type];
|
||||||
const zone = Info.normalizeZone(timezone);
|
const zone = Info.normalizeZone(timezone);
|
||||||
return ClientMap.fromApi(api, { zone, locale })
|
return ClientMap.fromApi(EntityClass, api, { zone, locale })
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -33,10 +33,7 @@ export const useUsersStore = defineStore("users", () => {
|
||||||
const promise = (async () => {
|
const promise = (async () => {
|
||||||
try {
|
try {
|
||||||
const apiUsers = await requestFetch("/api/users", { signal: controller.signal });
|
const apiUsers = await requestFetch("/api/users", { signal: controller.signal });
|
||||||
state.users.value.apiUpdate({
|
state.users.value.apiUpdate(apiUsers, { zone, locale });
|
||||||
type: "user",
|
|
||||||
entities: apiUsers,
|
|
||||||
}, { zone, locale });
|
|
||||||
state.pendingSync.value = undefined;
|
state.pendingSync.value = undefined;
|
||||||
state.fetched.value = true;
|
state.fetched.value = true;
|
||||||
return state.users;
|
return state.users;
|
||||||
|
@ -70,10 +67,7 @@ export const useUsersStore = defineStore("users", () => {
|
||||||
console.log("appyling", event.data)
|
console.log("appyling", event.data)
|
||||||
const zone = Info.normalizeZone(accountStore.activeTimezone);
|
const zone = Info.normalizeZone(accountStore.activeTimezone);
|
||||||
const locale = accountStore.activeLocale;
|
const locale = accountStore.activeLocale;
|
||||||
state.users.value.apiUpdate({
|
state.users.value.apiUpdate([event.data.data], { zone, locale });
|
||||||
type: "user",
|
|
||||||
entities: [event.data.data],
|
|
||||||
}, { zone, locale });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -27,10 +27,8 @@ function fixtureClientMap() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fixtureApiMap(): ApiMap<ApiUser> {
|
function fixtureApiMap(): ApiUser[] {
|
||||||
return {
|
return [
|
||||||
type: "user",
|
|
||||||
entities: [
|
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
updatedAt: nowIso,
|
updatedAt: nowIso,
|
||||||
|
@ -48,13 +46,12 @@ function fixtureApiMap(): ApiMap<ApiUser> {
|
||||||
updatedAt: nowIso,
|
updatedAt: nowIso,
|
||||||
deleted: true,
|
deleted: true,
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("class ClientMap", () => {
|
describe("class ClientMap", () => {
|
||||||
test("load from api", () => {
|
test("load from api", () => {
|
||||||
const map = ClientMap.fromApi(fixtureApiMap(), { zone, locale })
|
const map = ClientMap.fromApi(ClientUser, fixtureApiMap(), { zone, locale })
|
||||||
expect(map).toStrictEqual(fixtureClientMap());
|
expect(map).toStrictEqual(fixtureClientMap());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -202,7 +199,7 @@ describe("class ClientMap", () => {
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown action pattern ${action}`)
|
throw new Error(`Unknown action pattern ${action}`)
|
||||||
}
|
}
|
||||||
map.apiUpdate({ type: "user", entities: [update] }, { zone, locale });
|
map.apiUpdate([update], { zone, locale });
|
||||||
// Check
|
// Check
|
||||||
const expectedUser = userFromPattern(expectedPattern, useSameTimestamp);
|
const expectedUser = userFromPattern(expectedPattern, useSameTimestamp);
|
||||||
const expectedTomb = tombFromPattern(expectedPattern, useSameTimestamp);
|
const expectedTomb = tombFromPattern(expectedPattern, useSameTimestamp);
|
||||||
|
@ -243,7 +240,7 @@ describe("class ClientMap", () => {
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown action pattern ${action}`)
|
throw new Error(`Unknown action pattern ${action}`)
|
||||||
}
|
}
|
||||||
map.apiUpdate({ type: "user", entities: [update] }, { zone, locale });
|
map.apiUpdate([update], { zone, locale });
|
||||||
// Check
|
// Check
|
||||||
const expectedUser = userFromPattern(startPattern, useSameTimestamp);
|
const expectedUser = userFromPattern(startPattern, useSameTimestamp);
|
||||||
const expectedTomb = tombFromPattern(startPattern, useSameTimestamp);
|
const expectedTomb = tombFromPattern(startPattern, useSameTimestamp);
|
||||||
|
|
|
@ -2,23 +2,11 @@ import { type Entity, type EntityLiving, type Id, type Living } from "~/shared/t
|
||||||
import { DateTime, Zone } from "~/shared/utils/luxon";
|
import { DateTime, Zone } from "~/shared/utils/luxon";
|
||||||
import { ClientEntityNew } from "~/utils/client-user";
|
import { ClientEntityNew } from "~/utils/client-user";
|
||||||
|
|
||||||
export interface ApiMap<T extends Entity> {
|
export interface EntityClass<T extends ClientEntityNew> {
|
||||||
type: string,
|
|
||||||
entities: T[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EntityClass<T extends ClientEntityNew> {
|
|
||||||
name: string,
|
|
||||||
type: string,
|
|
||||||
fromApi(api: EntityLiving, opts: { zone: Zone, locale: string }): T,
|
fromApi(api: EntityLiving, opts: { zone: Zone, locale: string }): T,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class ClientMap<T extends ClientEntityNew> {
|
export class ClientMap<T extends ClientEntityNew> {
|
||||||
static typeMap: Record<string, EntityClass<ClientEntityNew>> = {
|
|
||||||
user: ClientUser,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public EntityClass: EntityClass<T>,
|
public EntityClass: EntityClass<T>,
|
||||||
public map: Map<Id, T>,
|
public map: Map<Id, T>,
|
||||||
|
@ -60,27 +48,24 @@ export class ClientMap<T extends ClientEntityNew> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromApi<T extends Entity>(
|
static fromApi<T extends Entity, U extends ClientEntityNew>(
|
||||||
api: ApiMap<T>,
|
EntityClass: EntityClass<U>,
|
||||||
|
entities: T[],
|
||||||
opts: { zone: Zone, locale: string },
|
opts: { zone: Zone, locale: string },
|
||||||
) {
|
) {
|
||||||
const EntityClass = this.typeMap[api.type];
|
const living = entities.filter(entity => !entity.deleted) as Living<T>[];
|
||||||
const entities = api.entities.filter(entity => !entity.deleted) as Living<T>[];
|
const tombstones = entities.filter(entity => entity.deleted === true);
|
||||||
const tombstones = api.entities.filter(entity => entity.deleted === true);
|
|
||||||
return new this(
|
return new this(
|
||||||
EntityClass,
|
EntityClass,
|
||||||
idMap(entities.map(apiEntity => EntityClass.fromApi(apiEntity, opts))),
|
idMap(living.map(apiEntity => EntityClass.fromApi(apiEntity, opts))),
|
||||||
new Map(tombstones.map(tombstone => [tombstone.id, Date.parse(tombstone.updatedAt)])),
|
new Map(tombstones.map(tombstone => [tombstone.id, Date.parse(tombstone.updatedAt)])),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
apiUpdate(api: ApiMap<Entity>, opts: { zone: Zone, locale: string }) {
|
apiUpdate(entities: Entity[], opts: { zone: Zone, locale: string }) {
|
||||||
if (api.type !== this.EntityClass.type) {
|
const living = entities.filter(entity => !entity.deleted);
|
||||||
throw new Error(`ClientMap: Map of ${this.EntityClass.name} received update for ${api.type}.`);
|
const tombstones = entities.filter(entity => entity.deleted === true);
|
||||||
}
|
for (const entity of living) {
|
||||||
const entities = api.entities.filter(entity => !entity.deleted);
|
|
||||||
const tombstones = api.entities.filter(entity => entity.deleted === true);
|
|
||||||
for (const entity of entities) {
|
|
||||||
const tombstoneMs = this.tombstones.get(entity.id);
|
const tombstoneMs = this.tombstones.get(entity.id);
|
||||||
const updatedMs = Date.parse(entity.updatedAt);
|
const updatedMs = Date.parse(entity.updatedAt);
|
||||||
if (tombstoneMs !== undefined) {
|
if (tombstoneMs !== undefined) {
|
||||||
|
@ -120,24 +105,19 @@ export class ClientMap<T extends ClientEntityNew> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toApi(diff: boolean): ApiMap<Entity> {
|
toApi(diff: boolean): Entity[] {
|
||||||
if (!diff) {
|
if (!diff) {
|
||||||
return {
|
return [
|
||||||
type: this.EntityClass.type,
|
|
||||||
entities: [
|
|
||||||
...[...this.map.values()].map(entity => entity.toApi()),
|
...[...this.map.values()].map(entity => entity.toApi()),
|
||||||
...[...this.tombstones].map(([id, updatedMs]) => ({
|
...[...this.tombstones].map(([id, updatedMs]) => ({
|
||||||
id,
|
id,
|
||||||
updatedAt: new Date(updatedMs).toISOString(),
|
updatedAt: new Date(updatedMs).toISOString(),
|
||||||
deleted: true as const,
|
deleted: true as const,
|
||||||
}))
|
})),
|
||||||
],
|
];
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return [
|
||||||
type: this.EntityClass.type,
|
|
||||||
entities: [
|
|
||||||
...[...this.map.values()]
|
...[...this.map.values()]
|
||||||
.filter(entity => entity.isModified() && !entity.deleted)
|
.filter(entity => entity.isModified() && !entity.deleted)
|
||||||
.map(entity => entity.toApi())
|
.map(entity => entity.toApi())
|
||||||
|
@ -150,7 +130,6 @@ export class ClientMap<T extends ClientEntityNew> {
|
||||||
deleted: true as const,
|
deleted: true as const,
|
||||||
}))
|
}))
|
||||||
,
|
,
|
||||||
],
|
];
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue