123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455 |
- import { reactive, ref, readonly } from "vue";
- import { defineStore } from "pinia";
- import { generateUuid } from "@common/utils/generateUuid";
- import { forEachIn } from "@common/utils/forEachIn";
- import { useWebsocketStore } from "./websocket";
- import Model from "@/Model";
- /**
- * Pinia store for managing models
- */
- export const useModelStore = defineStore("model", () => {
- const { runJob, subscribe, subscribeMany, unsubscribe, unsubscribeMany } =
- useWebsocketStore();
- const models = reactive({});
- const permissions = reactive({});
- const createdSubcription = ref(null);
- const subscriptions = reactive({
- created: {},
- updated: {},
- deleted: {}
- });
- /**
- * Returns generic model permissions for the current user for a specific model type
- */
- const getUserModelPermissions = async (modelName: string) => {
- if (permissions[modelName]) return permissions[modelName];
- const data = await runJob("data.users.getModelPermissions", {
- modelName
- });
- permissions[modelName] = data;
- return permissions[modelName];
- };
- /**
- * Checks if we have a specific generic permission for a specific model type
- */
- const hasPermission = async (modelName: string, permission: string) => {
- const data = await getUserModelPermissions(modelName);
- return !!data[permission];
- };
- /**
- * This functions gets called when the backend notifies us that a model was created
- * We then notify every subscription in the frontend about it
- */
- const onCreatedCallback = async (modelName: string, data) => {
- if (!subscriptions.created[modelName]) return;
- await forEachIn(
- Object.values(subscriptions.created[modelName]),
- async subscription => subscription(data) // TODO: Error handling
- );
- };
- /**
- * This functions gets called when the backend notifies us that a model was updated
- * We then notify every subscription in the frontend about it
- */
- const onUpdatedCallback = async (modelName: string, { doc }) => {
- const model = models[modelName] && models[modelName][doc._id];
- if (model) model.updateData(doc);
- if (!subscriptions.updated[modelName]) return;
- await forEachIn(
- Object.values(subscriptions.updated[modelName]),
- async subscription => subscription(data) // TODO: Error handling
- );
- };
- /**
- * This functions gets called when the backend notifies us that a model was deleted
- * We then notify every subscription in the frontend about it
- */
- const onDeletedCallback = async (modelName: string, data) => {
- const { oldDoc } = data;
- if (subscriptions.deleted[modelName])
- await forEachIn(
- Object.values(subscriptions.deleted[modelName]),
- async subscription => subscription(data) // TODO: Error handling
- );
- const model = models[modelName] && models[modelName][oldDoc._id];
- // TODO how does this work with addUse?
- if (model) await unregisterModels(modelName, oldDoc._id); // eslint-disable-line no-use-before-define
- };
- /**
- * Subscribes the provided callback to model creation events
- * The provided callback will be called when the backend notifies us that a model of the provided type is created
- */
- const onCreated = async (
- modelName: string,
- callback: (data?: any) => any
- ) => {
- if (!createdSubcription.value)
- createdSubcription.value = await subscribe(
- `model.${modelName}.created`,
- data => onCreatedCallback(modelName, data)
- );
- const uuid = generateUuid();
- subscriptions.created[modelName] ??= {};
- subscriptions.created[modelName][uuid] = callback;
- return uuid;
- };
- /**
- * Subscribes the provided callback to model update events
- * The provided callback will be called when the backend notifies us that a model of the provided type is updated
- */
- const onUpdated = async (
- modelName: string,
- callback: (data?: any) => any
- ) => {
- const uuid = generateUuid();
- subscriptions.updated[modelName] ??= {};
- subscriptions.updated[modelName][uuid] = callback;
- return uuid;
- };
- /**
- * Subscribes the provided callback to model deletion events
- * The provided callback will be called when the backend notifies us that a model of the provided type is deleted
- */
- const onDeleted = async (
- modelName: string,
- callback: (data?: any) => any
- ) => {
- const uuid = generateUuid();
- subscriptions.deleted[modelName] ??= {};
- subscriptions.deleted[modelName][uuid] = callback;
- return uuid;
- };
- /**
- * Allows removing a specific subscription, so the callback of that subscription is no longer called when the backend notifies us
- * For type created, we also unsubscribe to events from the backend
- * For type updated/deleted, we are subscribed to specific model id updated/deleted events, those are not unsubscribed when
- * there's no subscriptions in the frontend actually using them
- */
- const removeCallback = async (
- modelName: string,
- type: "created" | "updated" | "deleted",
- uuid: string
- ) => {
- if (
- !subscriptions[type][modelName] ||
- !subscriptions[type][modelName][uuid]
- )
- return;
- delete subscriptions[type][modelName][uuid];
- if (
- type === "created" &&
- Object.keys(subscriptions.created[modelName]).length === 0
- ) {
- await unsubscribe(
- `model.${modelName}.created`,
- createdSubcription.value
- );
- createdSubcription.value = null;
- }
- };
- /**
- * Returns the model for the provided name and id
- * First tries to get the model from the already loaded models
- * If it's not already loaded, it fetches it from the backend
- *
- * Does not register the model that was fetched
- *
- * // TODO return value?
- */
- const findById = async (modelName: string, modelId: string) => {
- const existingModel = models[modelName] && models[modelName][modelId];
- if (existingModel) return existingModel;
- return runJob(`data.${modelName}.findById`, { _id: modelId });
- };
- /**
- * Returns a list of models based on the provided model name and ids
- * First tries to get all models from the already loaded models
- * If after that we miss any models, we fetch those from the backend, and return everything we found
- *
- * Does not register the models that were fetched
- */
- const findManyById = async (modelName: string, modelIds: string[]) => {
- const existingModels = modelIds
- .map(modelId => models[modelName] && models[modelName][modelId])
- .filter(model => !!model);
- const existingModelIds = existingModels.map(model => model._id);
- const missingModelIds = modelIds.filter(
- _id => !existingModelIds.includes(_id)
- );
- let fetchedModels = [];
- if (missingModelIds.length > 0)
- fetchedModels = (await runJob(`data.${modelName}.findManyById`, {
- _ids: missingModelIds
- })) as unknown[];
- const allModels = existingModels.concat(fetchedModels);
- // Warning: returns models and direct results
- return Object.fromEntries(
- modelIds.map(modelId => [
- modelId,
- allModels.find(model => model._id === modelId)
- ])
- );
- };
- /**
- * Removes models locally if no one else is still using it
- * Also unsubscribes to any updated/deleted subscriptions
- */
- const unregisterModels = async (
- modelName: string,
- modelIdOrModelIds: string | string[]
- ) => {
- const modelIds = Array.isArray(modelIdOrModelIds)
- ? modelIdOrModelIds
- : [modelIdOrModelIds];
- const removeModels = [];
- await forEachIn(modelIds, async modelId => {
- if (!models[modelName] || !models[modelName][modelId]) return;
- const model = models[modelName][modelId];
- model.removeUse();
- if (model.getUses() > 1) return;
- // TODO only do this after a grace period
- removeModels.push(model);
- });
- if (removeModels.length === 0) return;
- await forEachIn(removeModels, async model =>
- model.unregisterRelations()
- );
- const subscriptions = Object.fromEntries(
- removeModels.flatMap(model => {
- const { updated, deleted } = model.getSubscriptions() ?? {};
- return [
- [
- updated,
- `model.${model.getName()}.updated.${model.getId()}`
- ],
- [
- deleted,
- `model.${model.getName()}.deleted.${model.getId()}`
- ]
- ];
- })
- );
- await unsubscribeMany(subscriptions);
- await forEachIn(removeModels, async removeModel => {
- const { _id: modelIdToRemove } = removeModel;
- if (!models[modelName] || !models[modelName][modelIdToRemove])
- return;
- delete models[modelName][modelIdToRemove];
- });
- console.log("After unregister", JSON.parse(JSON.stringify(models)));
- };
- /**
- * Registers models/documents
- * If any models/documents already exist, increments the use counter, and tries to load any potentially missing relations
- * For documents that don't already exist, registers them locally, adds subscriptions for updated/deleted events,
- * increments the use counter, and loads any potential relations
- */
- const registerModels = async (
- documentsOrModels: any[],
- relations?: Record<string, string | string[]>
- ): Promise<Model[]> => {
- console.info("Register models", documentsOrModels, relations);
- console.log(123123, documentsOrModels);
- const existingModels = documentsOrModels
- .map(({ _name, _id }) =>
- models[_name] ? models[_name][_id] ?? null : null
- )
- .filter(model => !!model);
- await forEachIn(existingModels, async model => {
- model.addUse();
- if (relations && relations[model._name])
- await model.loadRelations(relations[model._name]);
- });
- if (documentsOrModels.length === existingModels.length)
- return existingModels;
- const missingDocuments = documentsOrModels.filter(
- ({ _name, _id }) => !models[_name] || !models[_name][_id]
- );
- const channels = Object.fromEntries(
- missingDocuments.flatMap(({ _name, _id }) => [
- [
- `model.${_name}.updated.${_id}`,
- data => onUpdatedCallback(_name, data)
- ],
- [
- `model.${_name}.deleted.${_id}`,
- data => onDeletedCallback(_name, data)
- ]
- ])
- );
- const subscriptions = Object.entries(await subscribeMany(channels));
- const newModels = await forEachIn(missingDocuments, async document => {
- const { _name, _id } = document;
- const modelSubscriptions = subscriptions.filter(
- ([, { channel }]) =>
- channel.startsWith(`model.${_name}`) &&
- channel.endsWith(`.${_id}`)
- );
- const [updated] = modelSubscriptions.find(([, { channel }]) =>
- channel.includes(".updated.")
- );
- const [deleted] = modelSubscriptions.find(([, { channel }]) =>
- channel.includes(".deleted.")
- );
- if (!updated || !deleted) return null;
- const model = reactive(new Model(document));
- model.setSubscriptions(updated, deleted);
- model.addUse();
- // TODO what if relations are relevant for some registers, but not others? Unregister if no register relies on a relation
- if (relations && relations[_name])
- await model.loadRelations(relations[_name]);
- if (!models[_name]) {
- models[_name] = {};
- }
- models[_name][_id] = model;
- return model;
- });
- return existingModels.concat(newModels);
- };
- /**
- * Registers a model or document
- * Helper function to be able to register a single model/document, simply calls registerModels
- */
- const registerModel = async (
- documentOrModel: any,
- relations?: Record<string, string | string[]>
- ): Promise<Model> => {
- const [model] = await registerModels([documentOrModel], relations);
- return model;
- };
- /**
- * Loads one or more models for a provided model name and a provided model id or model ids, optionally including any relations
- * First fetches models from the already loaded models
- * Tries to fetch any missing models from the backend
- */
- const loadModels = async (
- modelName: string,
- modelIdsOrModelId: string | string[],
- relations?: Record<string, string | string[]>
- ): Promise<Map<string, Model | null>> => {
- const modelIds = Array.isArray(modelIdsOrModelId)
- ? modelIdsOrModelId
- : [modelIdsOrModelId];
- const existingModels = modelIds
- .map(_id => models[modelName] && models[modelName][_id])
- .filter(model => !!model);
- const existingModelIds = existingModels.map(model => model._id);
- const missingModelIds = modelIds.filter(
- _id => !existingModelIds.includes(_id)
- );
- console.info(
- "Load models",
- structuredClone(modelIds),
- structuredClone(existingModels),
- structuredClone(missingModelIds)
- );
- const fetchedModels = await findManyById(modelName, missingModelIds);
- console.log(999, modelName, missingModelIds, fetchedModels);
- const registeredModels = await registerModels(
- Object.values(fetchedModels)
- .filter(model => !!model)
- .concat(existingModels),
- relations
- );
- const modelsNotFound = modelIds
- .filter(
- modelId =>
- !registeredModels.find(model => model.getId() === modelId)
- )
- .map(modelId => [modelId, null]);
- console.log(123, registeredModels, modelsNotFound, fetchedModels);
- return Object.fromEntries(
- registeredModels
- .map(model => [model.getId(), model])
- .concat(modelsNotFound)
- );
- };
- return {
- models: readonly(models),
- permissions: readonly(permissions),
- subscriptions: readonly(subscriptions),
- onCreated,
- onUpdated,
- onDeleted,
- removeCallback,
- registerModel,
- registerModels,
- unregisterModels,
- loadModels,
- findById,
- findManyById,
- hasPermission
- };
- });
|