Fix reactive nature of generic resource store
Some checks failed
/ golint (push) Failing after 10s

These changes are meant to make it possible for new events that come in
to immediately render in the components that depend on them.
This commit is contained in:
Eli Ribble 2026-05-21 23:09:11 +00:00
parent 74ef9a8b3a
commit cecb9ef0f0
No known key found for this signature in database
5 changed files with 92 additions and 59 deletions

View file

@ -144,6 +144,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted } from "vue";
import { computedAsync } from "@vueuse/core"; import { computedAsync } from "@vueuse/core";
import MapMultipoint from "@/components/MapMultipoint.vue"; import MapMultipoint from "@/components/MapMultipoint.vue";
import TimeRelative from "@/components/TimeRelative.vue"; import TimeRelative from "@/components/TimeRelative.vue";
@ -151,6 +152,7 @@ import PublicReportCardCompliance from "@/components/PublicReportCardCompliance.
import PublicReportCardNuisance from "@/components/PublicReportCardNuisance.vue"; import PublicReportCardNuisance from "@/components/PublicReportCardNuisance.vue";
import PublicReportCardWater from "@/components/PublicReportCardWater.vue"; import PublicReportCardWater from "@/components/PublicReportCardWater.vue";
import { formatAddress } from "@/format"; import { formatAddress } from "@/format";
import { log } from "@/log";
import { useStoreResource } from "@/store/resource"; import { useStoreResource } from "@/store/resource";
import { import {
PublicReport, PublicReport,
@ -169,10 +171,15 @@ const emit = defineEmits<Emits>();
const props = defineProps<Props>(); const props = defineProps<Props>();
const storeResource = useStoreResource(); const storeResource = useStoreResource();
const report = computedAsync(() => { const report = computed(() => {
return storeResource.publicreport.byURI(props.reportURI); const result = storeResource.publicreport.byURI.get(props.reportURI);
log.info("geting computed resource", props.reportURI, result);
return result;
}); });
function openPhotoViewer(index: number) { function openPhotoViewer(index: number) {
emit("viewImage", index); emit("viewImage", index);
} }
onMounted(async () => {
await storeResource.publicreport.fetchByURI(props.reportURI);
});
</script> </script>

View file

@ -203,6 +203,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import ListCardCommunication from "@/components/ListCardCommunication.vue"; import ListCardCommunication from "@/components/ListCardCommunication.vue";
import { log } from "@/log";
import { Communication, LogEntry, PublicReport } from "@/type/api"; import { Communication, LogEntry, PublicReport } from "@/type/api";
interface Props { interface Props {
@ -263,11 +264,15 @@ const activeFilterCount = computed(() => {
// Filtered communications // Filtered communications
const filteredCommunications = computed((): Communication[] => { const filteredCommunications = computed((): Communication[] => {
log.info(
"getting communication column list filteredCommunications",
props.all,
);
if (props.all == null) { if (props.all == null) {
return []; return [];
} }
return props.all.filter((comm) => { const filtered = props.all.filter((comm) => {
// Status filter // Status filter
const selectedStatuses = Object.entries(statusFilters.value) const selectedStatuses = Object.entries(statusFilters.value)
.filter(([_, isSelected]) => isSelected) .filter(([_, isSelected]) => isSelected)
@ -299,5 +304,10 @@ const filteredCommunications = computed((): Communication[] => {
return true; return true;
}); });
const sorted = filtered.sort((a: Communication, b: Communication): number => {
return b.created.getTime() - a.created.getTime();
});
log.info("filtered and sorted to", sorted);
return sorted;
}); });
</script> </script>

View file

@ -1,8 +1,8 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { ref, shallowRef } from "vue"; import { ref, shallowRef, shallowReactive } from "vue";
import { log } from "@/log";
import { SSEManager, SSEMessageResource } from "@/SSEManager"; import { SSEManager, SSEMessageResource } from "@/SSEManager";
import { useSessionStore } from "@/store/session";
import { apiClient } from "@/client"; import { apiClient } from "@/client";
import { import {
@ -23,7 +23,7 @@ function createResourceStore<dto, full extends uriHaver>(
api_base: string, api_base: string,
from_json: jsonConverter<dto, full>, from_json: jsonConverter<dto, full>,
) { ) {
const _resourceByURI = shallowRef<Map<string, full>>(new Map()); const byURI = shallowReactive<Map<string, full>>(new Map());
const _resourceFetchAll = ref<Promise<full[]> | null>(null); const _resourceFetchAll = ref<Promise<full[]> | null>(null);
const _resourceFetchByURI = shallowRef<Map<string, Promise<full> | null>>( const _resourceFetchByURI = shallowRef<Map<string, Promise<full> | null>>(
new Map(), new Map(),
@ -33,32 +33,43 @@ function createResourceStore<dto, full extends uriHaver>(
SSEManager.subscribe((msg: SSEMessageResource) => { SSEManager.subscribe((msg: SSEMessageResource) => {
if (msg.resource.startsWith(resource_name)) { if (msg.resource.startsWith(resource_name)) {
if (msg.type == "created") { if (msg.type == "created") {
console.log("New resource", resource_name, msg.resource, msg.uri);
fetchByURI(msg.uri); fetchByURI(msg.uri);
} else if (msg.type == "updated") { } else if (msg.type == "updated") {
console.log("Updated resource", resource_name, msg.resource, msg.uri);
fetchByURI(msg.uri); fetchByURI(msg.uri);
} }
} }
}); });
async function byAll(): Promise<full[]> {
const cur = _resourceFetchAll.value;
if (cur) {
return cur;
}
return fetchAll();
}
async function byID(id: string): Promise<full> { async function byID(id: string): Promise<full> {
const uri = uriFromID(id); const uri = uriFromID(id);
return byURI(uri); return ensureURI(uri);
} }
async function byURI(uri: string): Promise<full> { async function ensureURI(uri: string): Promise<full> {
let cur = _resourceFetchByURI.value.get(uri); // Check if we already have the data
if (cur) { const existing = byURI.get(uri);
return cur; if (existing) {
return existing;
}
// Check if we're already fetching it
let fetchPromise = _resourceFetchByURI.value.get(uri);
if (fetchPromise) {
return fetchPromise;
}
// Start fetching
fetchPromise = fetchByURI(uri);
_resourceFetchByURI.value.set(uri, fetchPromise);
try {
const result = await fetchPromise;
return result;
} finally {
// Clean up the promise after it resolves
_resourceFetchByURI.value.delete(uri);
} }
cur = fetchByURI(uri);
_resourceFetchByURI.value.set(uri, cur);
return cur;
} }
async function fetchAll(): Promise<full[]> { async function fetchAll(): Promise<full[]> {
/* /*
@ -71,9 +82,13 @@ function createResourceStore<dto, full extends uriHaver>(
const url = `/api${api_base}`; const url = `/api${api_base}`;
const dtos = (await apiClient.JSONGet(url)) as dto[]; const dtos = (await apiClient.JSONGet(url)) as dto[];
const resources = dtos.map((m: dto) => from_json(m)); const resources = dtos.map((m: dto) => from_json(m));
//let new_all = new Map<string, full>();
resources.forEach((r: full) => { resources.forEach((r: full) => {
_resourceByURI.value.set(r.uri, r); byURI.set(r.uri, r);
//new_all.set(r.uri, r);
log.info("Set", resource_name, r);
}); });
//byURI.value = new_all;
return resources; return resources;
} }
async function fetchByID(id: string): Promise<full> { async function fetchByID(id: string): Promise<full> {
@ -89,19 +104,16 @@ function createResourceStore<dto, full extends uriHaver>(
} }
const body: dto = await response.json(); const body: dto = await response.json();
const report = from_json(body); const report = from_json(body);
_resourceByURI.value.set(report.uri, report); //let new_all = new Map<string, full>(byURI.value);
//new_all.set(report.uri, report);
byURI.set(report.uri, report);
//byURI.value = new_all;
return report; return report;
} catch (err) { } catch (err) {
console.error("Error loading users:", err); console.error("Error loading users:", err);
throw err; throw err;
} }
} }
function getAll(): full[] | null {
if (_resourceFetchAll) {
return Array.from(_resourceByURI.value.values());
}
return null;
}
function hasAll(): boolean { function hasAll(): boolean {
return !!_resourceFetchAll.value; return !!_resourceFetchAll.value;
} }
@ -118,13 +130,12 @@ function createResourceStore<dto, full extends uriHaver>(
return `${api_base}/${id}`; return `${api_base}/${id}`;
} }
return { return {
byAll,
byID, byID,
byURI, byURI,
ensureURI,
fetchAll, fetchAll,
fetchByID, fetchByID,
fetchByURI, fetchByURI,
getAll,
hasAll, hasAll,
loadingAll, loadingAll,
loadingURI, loadingURI,

View file

@ -23,10 +23,7 @@
</template> </template>
<template #center> <template #center>
<CommunicationColumnDetail <CommunicationColumnDetail
:loading=" :loading="storePublicReport.loading || loadingSelectedCommunication"
storePublicReport.loading ||
storeResource.communication.loadingURI(selectedCommunication?.uri)
"
:mapBounds="mapBounds || undefined" :mapBounds="mapBounds || undefined"
:mapMarkers="mapMarkers" :mapMarkers="mapMarkers"
:selectedCommunication="selectedCommunication" :selectedCommunication="selectedCommunication"
@ -36,10 +33,7 @@
</template> </template>
<template #right> <template #right>
<CommunicationColumnAction <CommunicationColumnAction
:isLoading=" :isLoading="storePublicReport.loading || loadingSelectedCommunication"
storePublicReport.loading ||
storeResource.communication.loadingURI(selectedCommunication?.uri)
"
@markInvalid="markInvalid" @markInvalid="markInvalid"
@markPendingResponse="markPendingResponse" @markPendingResponse="markPendingResponse"
@markPossibleIssue="markPossibleIssue" @markPossibleIssue="markPossibleIssue"
@ -78,6 +72,7 @@ import ImageViewerModal from "@/components/ImageViewerModal.vue";
import ThreeColumn from "@/components/layout/ThreeColumn.vue"; import ThreeColumn from "@/components/layout/ThreeColumn.vue";
import ToastNotification from "@/components/ToastNotification.vue"; import ToastNotification from "@/components/ToastNotification.vue";
import { useQueryParam } from "@/composable/use-query-param"; import { useQueryParam } from "@/composable/use-query-param";
import { log } from "@/log";
import { SSEManager } from "@/SSEManager"; import { SSEManager } from "@/SSEManager";
import { useStoreResource } from "@/store/resource"; import { useStoreResource } from "@/store/resource";
import { useSessionStore } from "@/store/session"; import { useSessionStore } from "@/store/session";
@ -111,10 +106,13 @@ const currentImages = computed(() => {
} }
return selectedReport.value?.images ?? []; return selectedReport.value?.images ?? [];
}); });
const loadingSelectedCommunication = computed<boolean>((): boolean => {
return !!selectedCommunication.value;
});
const mapBounds = computed<LngLatBounds | null>((): LngLatBounds | null => { const mapBounds = computed<LngLatBounds | null>((): LngLatBounds | null => {
let bounds = new Bounds(); let bounds = new Bounds();
const loc = selectedReport.value?.location; const loc = selectedReport.value?.location;
console.log("updating for loc", loc); log.info("updating for loc", loc);
if (loc && loc.latitude != 0 && loc.longitude != 0) { if (loc && loc.latitude != 0 && loc.longitude != 0) {
bounds.addLocation(loc); bounds.addLocation(loc);
} }
@ -174,15 +172,16 @@ const mapMarkers = computed<Marker[]>((): Marker[] => {
} }
return markers; return markers;
}); });
const selectedCommunication = computedAsync( const selectedCommunication = computed((): Communication | undefined => {
async (): Promise<Communication | undefined> => { log.info("get selectedCommunication", selectedId.value);
if (selectedId.value == undefined) { if (!selectedId.value) {
return undefined; return undefined;
} }
const all = await storeResource.communication.byAll(); const all = Array.from(storeResource.communication.byURI.values());
return all.find((c: Communication) => c.id == selectedId.value); const result = all.find((c: Communication) => c.id == selectedId.value);
}, log.info("selectedCommunication", selectedId.value, result);
); return result;
});
const selectedReport = computedAsync( const selectedReport = computedAsync(
async (): Promise<PublicReport | undefined> => { async (): Promise<PublicReport | undefined> => {
if ( if (
@ -195,14 +194,20 @@ const selectedReport = computedAsync(
return await storePublicReport.byURI(selectedCommunication.value.source); return await storePublicReport.byURI(selectedCommunication.value.source);
}, },
); );
const visibleCommunications = computedAsync( const visibleCommunications = computed((): Communication[] | undefined => {
async (): Promise<Communication[]> => { log.info("get visibleCommunications", storeResource.communication.byURI);
const all = await storeResource.communication.byAll(); if (!storeResource.communication.byURI) {
return all.filter((c: Communication) => { return undefined;
return c.status == "new" || c.status == "opened"; }
}); const all: Communication[] = Array.from(
}, storeResource.communication.byURI.values(),
); );
const result = all.filter((c: Communication) => {
return c.status == "new" || c.status == "opened";
});
log.info("visibleCommunications:", result);
return result;
});
const handleDeselect = (id: string) => { const handleDeselect = (id: string) => {
selectedId.value = undefined; selectedId.value = undefined;
}; };
@ -256,7 +261,7 @@ async function sendMessage(message: string) {
if (selectedCommunication.value == null) return; if (selectedCommunication.value == null) return;
if (selectedReport.value == null) return; if (selectedReport.value == null) return;
if (session.urls == null) return; if (session.urls == null) return;
console.log("Sending message reporter:", message); log.info("Sending message reporter:", message);
const payload = { const payload = {
message: message, message: message,

View file

@ -28,7 +28,7 @@ import { Contact } from "@/type/api";
const storeResource = useStoreResource(); const storeResource = useStoreResource();
const contacts = computedAsync(() => { const contacts = computedAsync(() => {
return storeResource.contact.byAll(); return Array.from(storeResource.contact.byURI.values());
}); });
const selectedContact = ref<Contact | undefined>(undefined); const selectedContact = ref<Contact | undefined>(undefined);
const handleSelectionChange = (selection: Contact | undefined) => { const handleSelectionChange = (selection: Contact | undefined) => {