Files
yaak-mountain-loop/crates/yaak-models/guest-js/store.ts
T

255 lines
7.6 KiB
TypeScript

import { invoke } from "@tauri-apps/api/core";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { AnyModel, ModelPayload } from "../bindings/gen_models";
import { modelStoreDataAtom } from "./atoms";
import { ExtractModel, JotaiStore, ModelStoreData } from "./types";
import { newStoreData } from "./util";
let _store: JotaiStore | null = null;
const pendingModelWrites = new Set<Promise<unknown>>();
export function initModelStore(store: JotaiStore) {
_store = store;
getCurrentWebviewWindow()
.listen<ModelPayload>("model_write", ({ payload }) => {
if (shouldIgnoreModel(payload)) return;
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
if (payload.change.type === "upsert") {
return {
...prev,
[payload.model.model]: {
...prev[payload.model.model],
[payload.model.id]: payload.model,
},
};
} else {
const modelData = { ...prev[payload.model.model] };
delete modelData[payload.model.id];
return { ...prev, [payload.model.model]: modelData };
}
});
})
.catch(console.error);
}
function mustStore(): JotaiStore {
if (_store == null) {
throw new Error("Model store was not initialized");
}
return _store;
}
function trackModelWrite<T>(write: Promise<T>): Promise<T> {
const tracked = write.finally(() => {
pendingModelWrites.delete(tracked);
});
pendingModelWrites.add(tracked);
return tracked;
}
export async function flushAllModelWrites(): Promise<void> {
const results = await Promise.allSettled(pendingModelWrites);
const rejected = results.find((result) => result.status === "rejected");
if (rejected?.status === "rejected") {
throw rejected.reason;
}
}
let _activeWorkspaceId: string | null = null;
export async function changeModelStoreWorkspace(workspaceId: string | null) {
console.log("Syncing models with new workspace", workspaceId);
const workspaceModelsStr = await invoke<string>("models_workspace_models", {
workspaceId, // NOTE: if no workspace id provided, it will just fetch global models
});
const workspaceModels = JSON.parse(workspaceModelsStr) as AnyModel[];
const data = newStoreData();
for (const model of workspaceModels) {
data[model.model][model.id] = model;
}
mustStore().set(modelStoreDataAtom, data);
console.log("Synced model store with workspace", workspaceId, data);
_activeWorkspaceId = workspaceId;
}
export function listModels<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
modelType: M | ReadonlyArray<M>,
): T[] {
let data = mustStore().get(modelStoreDataAtom);
const types: ReadonlyArray<M> = Array.isArray(modelType) ? modelType : [modelType];
return types.flatMap((t) => Object.values(data[t]) as T[]);
}
export function getModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
modelType: M | ReadonlyArray<M>,
id: string,
): T | null {
let data = mustStore().get(modelStoreDataAtom);
const types: ReadonlyArray<M> = Array.isArray(modelType) ? modelType : [modelType];
for (const t of types) {
let v = data[t][id];
if (v?.model === t) return v as T;
}
return null;
}
export function getAnyModel(id: string): AnyModel | null {
let data = mustStore().get(modelStoreDataAtom);
for (const t of Object.keys(data)) {
// oxlint-disable-next-line no-explicit-any -- dynamic key access
let v = (data as any)[t]?.[id];
if (v?.model === t) return v;
}
return null;
}
export function patchModelById<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
model: M,
id: string,
patch: Partial<T> | ((prev: T) => T),
): Promise<string> {
let prev = getModel<M, T>(model, id);
if (prev == null) {
throw new Error(`Failed to get model to patch id=${id} model=${model}`);
}
const newModel = typeof patch === "function" ? patch(prev) : { ...prev, ...patch };
return updateModel(newModel);
}
export async function patchModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
base: Pick<T, "id" | "model">,
patch: Partial<T>,
): Promise<string> {
return patchModelById<M, T>(base.model, base.id, patch);
}
export async function updateModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
model: T,
): Promise<string> {
return trackModelWrite(invoke<string>("models_upsert", { model }));
}
export async function deleteModelById<
M extends AnyModel["model"],
T extends ExtractModel<AnyModel, M>,
>(modelType: M | M[], id: string) {
let model = getModel<M, T>(modelType, id);
await deleteModel(model);
}
export async function deleteModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
model: T | null,
) {
if (model == null) {
throw new Error("Failed to delete null model");
}
await trackModelWrite(invoke<string>("models_delete", { model }));
}
export async function duplicateModel<
M extends AnyModel["model"],
T extends ExtractModel<AnyModel, M>,
>(model: T | null): Promise<string> {
if (model == null) {
throw new Error("Failed to duplicate null model");
}
// Flush pending writes first, since the backend duplicates from the DB (the passed-in
// model may be a stale snapshot, eg. from the memoized sidebar tree). Conflict-free
// naming ("Foo Copy 2") is also handled by the backend.
await flushAllModelWrites();
return trackModelWrite(
invoke<string>("models_duplicate", { modelType: model.model, modelId: model.id }),
);
}
export async function createGlobalModel<T extends Exclude<AnyModel, { workspaceId: string }>>(
patch: Partial<T> & Pick<T, "model">,
): Promise<string> {
return trackModelWrite(invoke<string>("models_upsert", { model: patch }));
}
export async function createWorkspaceModel<T extends Extract<AnyModel, { workspaceId: string }>>(
patch: Partial<T> & Pick<T, "model" | "workspaceId">,
): Promise<string> {
return trackModelWrite(invoke<string>("models_upsert", { model: patch }));
}
export function replaceModelsInStore<
M extends AnyModel["model"],
T extends Extract<AnyModel, { model: M }>,
>(model: M, models: T[]) {
const newModels: Record<string, T> = {};
for (const model of models) {
newModels[model.id] = model;
}
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
return {
...prev,
[model]: newModels,
};
});
}
export function mergeModelsInStore<
M extends AnyModel["model"],
T extends Extract<AnyModel, { model: M }>,
>(model: M, models: T[], filter?: (model: T) => boolean) {
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
const existingModels = { ...prev[model] } as Record<string, T>;
// Merge in new models first
for (const m of models) {
existingModels[m.id] = m;
}
// Then filter out unwanted models
if (filter) {
for (const [id, m] of Object.entries(existingModels)) {
if (!filter(m)) {
delete existingModels[id];
}
}
}
return {
...prev,
[model]: existingModels,
};
});
}
function shouldIgnoreModel({ model, updateSource }: ModelPayload) {
// Never ignore updates from non-user sources
if (updateSource.type !== "window") {
return false;
}
// Never ignore same-window updates
if (updateSource.label === getCurrentWebviewWindow().label) {
return false;
}
// Only sync models that belong to this workspace, if a workspace ID is present
if ("workspaceId" in model && model.workspaceId !== _activeWorkspaceId) {
return true;
}
if (model.model === "key_value" && model.namespace === "no_sync") {
return true;
}
return false;
}