Files
Jyotisha/frontend/src/lib/current-subject.ts
T
jesse-ux ab6c55f822 feat: keep up to five chart subjects on typed columns
Chart, ephemeris, reports, daily language, consult, and synastry read birth data only through resolveSubjectBirth. The current person is a request parameter outside Home(). /people replaces the settings chart pane, and deleting someone removes that person's chats and reports.
2026-09-25 04:19:05 +08:00

111 lines
3.4 KiB
TypeScript

"use client";
import { useSyncExternalStore } from "react";
import { fetchAccount, fetchCloudChartLibrary } from "@/lib/home-cloud-sync";
import { readProfile, upsertSelfChart } from "@/lib/home-profile";
import type { ChartLibraryRecord } from "@/lib/home-types";
const SELF_SUBJECT_ID = "self";
let accountId = "";
let subjectId = SELF_SUBJECT_ID;
let catalogReady = false;
const listeners = new Set<() => void>();
export function currentSubjectStorageKey(id: string): string {
return `jyotisha_current_subject:${id}`;
}
export function readCurrentSubjectId(): string {
return subjectId;
}
export function currentSubjectCatalogReady(): boolean {
return catalogReady;
}
export function subscribeCurrentSubject(listener: () => void): () => void {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
function emit(): void {
for (const listener of listeners) listener();
}
export function setCurrentSubject(next: string): void {
const id = next.trim() || SELF_SUBJECT_ID;
if (id === subjectId) return;
subjectId = id;
if (accountId && typeof localStorage !== "undefined") {
localStorage.setItem(currentSubjectStorageKey(accountId), id);
}
emit();
}
/**
* `knownOtherIds === null` means the archive has not loaded yet, so a stored
* id is kept. Once ids are known, an id that is not in the archive falls
* back to self.
*/
export function bindCurrentSubjectAccount(nextAccountId: string, knownOtherIds: readonly string[] | null): void {
accountId = nextAccountId;
if (!nextAccountId || typeof localStorage === "undefined") {
catalogReady = knownOtherIds !== null;
if (subjectId !== SELF_SUBJECT_ID) {
subjectId = SELF_SUBJECT_ID;
emit();
}
return;
}
localStorage.removeItem(`jyotisha_active_chart:${nextAccountId}`);
const stored = localStorage.getItem(currentSubjectStorageKey(nextAccountId)) || SELF_SUBJECT_ID;
const valid = stored === SELF_SUBJECT_ID || knownOtherIds === null || knownOtherIds.includes(stored);
const next = valid ? stored : SELF_SUBJECT_ID;
if (!valid) localStorage.setItem(currentSubjectStorageKey(nextAccountId), SELF_SUBJECT_ID);
catalogReady = knownOtherIds !== null;
if (next !== subjectId || knownOtherIds !== null) {
subjectId = next;
emit();
}
}
export function useCurrentSubject(): string {
return useSyncExternalStore(subscribeCurrentSubject, readCurrentSubjectId, () => SELF_SUBJECT_ID);
}
let catalogRequest: Promise<ChartLibraryRecord[]> | null = null;
let catalogAccountId = "";
export function resetSubjectCatalogForTests(): void {
catalogRequest = null;
catalogAccountId = "";
accountId = "";
subjectId = SELF_SUBJECT_ID;
catalogReady = false;
}
export function loadSubjectCatalog(force = false): Promise<ChartLibraryRecord[]> {
if (!force && catalogRequest && catalogAccountId) return catalogRequest;
catalogRequest = Promise.all([
fetchAccount(),
fetchCloudChartLibrary(),
]).then(([account, others]) => {
const self = readProfile(account.profile);
const library = upsertSelfChart(others.filter((record) => record.role === "other"), self);
catalogAccountId = account.user.id;
bindCurrentSubjectAccount(
account.user.id,
library.filter((record) => record.role === "other").map((record) => record.id),
);
return library;
}).catch((error: unknown) => {
catalogRequest = null;
throw error;
});
return catalogRequest;
}