import {ApolloClient, NormalizedCacheObject} from "@apollo/client"; import {Assert} from "js-vextensions"; import {observable} from "mobx"; import {AccessorCallPlan} from "./Accessors/@AccessorCallPlan.js"; import {DataCommitScheduler} from "./Components/DataCommitScheduler.js"; import {nodesByPath, SubscriptionStatus, TreeNode} from "./Tree/TreeNode.js"; import {TreeRequestWatcher} from "./Tree/TreeRequestWatcher.js"; import {makeObservable_safe, RunInAction} from "./Utils/General/MobX.js"; import {GQLIntrospector} from "./DBShape/GQLIntrospector.js"; export class GraphlinkInitOptions { rootStore: StoreShape; apollo: ApolloClient; onServer: boolean; //initSubs = true; /** * After X milliseconds of being unobserved, a TreeNode will unsubscribe its GraphQL subscription, by sending "stop" over the websocket. * Special values: 5000 (default), -1 (never auto-unsubscribe) * */ unsubscribeTreeNodesAfter?: number; // server-specific pgPool?: any; //Pool; } export type GraphlinkOptionsInit = ConstructorParameters[0]; export class GraphlinkOptions { constructor(data?: Partial) { Object.assign(this, data); } // these fields let mobx-graphlink be compatible with both the old and new graphlink-rust API (so some of these may be removed, once all projects are using the new API) useIntrospection = false; alwaysRequestExtrasField = true; useCollectionEntryCaching = true; unsubscribeTreeNodesAfter = 5000; /** After each data-update, how long to wait for another data-update; if another occurs during this period, the timer is reset, and another wait occurs. (until max-wait is reached) */ dataUpdateBuffering_minWait = 10; dataUpdateBuffering_maxWait = 100; dataUpdateBuffering_commitSetMaxFuncCount = Number.MAX_SAFE_INTEGER; dataUpdateBuffering_commitSetMaxTime = 1000; dataUpdateBuffering_breakDuration = 100; } export class Graphlink { static instances = [] as Graphlink[]; /** You must call graphlink.Initialize(...) after constructing the Graphlink instance. */ constructor(/*initOptions?: GraphlinkInitOptions, options?: GraphlinkOptions*/) { makeObservable_safe(this, { initialized: observable, userInfo: observable, }); /*if (initOptions) { this.Initialize(initOptions, options); } else { Assert(options == null); }*/ } initialized = false; // [@O] async Initialize(initOptions: GraphlinkInitOptions, options?: GraphlinkOptionsInit) { const {rootStore, apollo, onServer, pgPool} = initOptions; Graphlink.instances.push(this); this.rootStore = rootStore; //if (initSubs) { //this.InitSubs(); this.onServer = onServer; this.subs.apollo = apollo; this.subs.pgPool = pgPool; this.options = new GraphlinkOptions(options); if (this.options.useIntrospection) { await this.introspector.RetrieveTypeShapes(apollo); } this.commitScheduler = new DataCommitScheduler(this); this.tree = new TreeNode(this, []); RunInAction("Graphlink.Initialize", ()=>this.initialized = true); } rootStore: StoreShape; storeOverridesStack = [] as StoreShape[]; /** Set this to false if you need to make sure all relevant database-requests within an accessor tree are being activated. */ storeAccessorCachingTempDisabled = false; //accessorContext: AccessorContext = new AccessorContext(this); // call-stack stuff //lastRunAccessor_meta: AccessorMetadata|undefined; //currentDeepestCallPlanActive: AccessorCallPlan; // only use this for determining the "current deepest call-plan"; cannot construct a true/traditional "call-stack" since mobx-based "call-stacks" can trigger either top-down or bottom-up callPlan_callStack = [] as AccessorCallPlan[]; GetDeepestCallPlanCurrentlyRunning() { return this.callPlan_callStack[this.callPlan_callStack.length - 1]; } /*InitSubs() { // todo this.subs.apollo = null; }*/ onServer: boolean; subs = {} as { apollo: ApolloClient; //pgPool?: Pool|null; // only used if on db-server pgPool?: any|null; // only used if on db-server }; options: GraphlinkOptions; readonly userInfo: UserInfo|null = null; // [@O] /** Can be called prior to Graphlink.Initialize(). */ async SetUserInfo(userInfo: UserInfo|null, clearCaches = true, resubscribeAfter = true) { RunInAction("SetUserInfo", ()=>(this as any).userInfo = userInfo); if (clearCaches && this.initialized) { console.log("Clearing memory-cache of mobx-graphlink and apollo, due to user-info change."); const nodesThatHadActiveOrInitializingSub = await this.ClearCaches(); if (resubscribeAfter) { RunInAction("SetUserInfo.resubscribeAfter", ()=>{ for (const node of nodesThatHadActiveOrInitializingSub) { node.SubscribeIfNotAlready(); } }); } return nodesThatHadActiveOrInitializingSub; } } async ClearCaches() { /*for (const node of this.tree.AllDescendantNodes) { node.data }*/ // first, unsubscribe everything; this lets the server release the old live-queries const nodesThatHadActiveOrInitializingSub = this.tree.UnsubscribeAll(false); nodesByPath.clear(); // also clear this (debugging collection to track if multiple nodes are created for same path); tree is resetting, so reset this list too // then, delete/detach all the collection tree-nodes; this is equivalent to clearing the mobx-graphlink cache (well, cache should be cleared by `UnsubscribeAll(false)` above, but this makes certain) // commented; this causes issues in mobx-graphlink, where the old subtrees are still being observed (by the accessors), yet are disconnected from the new set created by new requests // todo: add asserts to avoid mistakes like this in the future (eg. by confirming that whenever processing is done for a TreeNode, it is still connected to the graphlink root) /*for (const [key, collectionNode] of this.tree.collectionNodes) { this.tree.collectionNodes.delete(key); }*/ // clear the apollo-cache as well (since mobx-graphlink uses subscriptions exclusively, this probably isn't necessary, but we'll clear it anyway to be sure) await this.subs.apollo.cache.reset(); await this.subs.apollo.clearStore(); return nodesThatHadActiveOrInitializingSub; } // todo (probably) /*async LogIn() { // todo return null; } async LogIn_WithCredential() { // todo return null; } async LogOut() { // todo }*/ introspector = new GQLIntrospector(); commitScheduler: DataCommitScheduler; /** * This is set to true whenever a call-chain is running which was triggered by data being committed to the Graphlink tree. (ie. on data being received from server) * Example usage: For easier debugging of what userland code was running a particular accessor. (add conditional breakpoint, breaking only when `graph.inDataCommitChain == false`) * Related/alternative: `graph.callPlan_callStack.length > 1` * */ inDataCommitChain = false; tree: TreeNode; // this is just a tree-node holder, so its data-shape is-any/doesn't-matter treeRequestWatchers = new Set(); //pathSubscriptions: Map; UnsubscribeAll() { this.tree.UnsubscribeAll(); } ValidateDBData?: (dbData: DBShape)=>void; allTreeNodes = new Set>(); NodesWhere(filterFunc: (node: TreeNode)=>boolean): TreeNode[] { return [...this.allTreeNodes].filter(filterFunc); } // these are just stats that the consumer may be interested in GetStats(): GraphlinkStats { return new GraphlinkStats({ attachedTreeNodes: this.allTreeNodes.size, nodesWithRequestedSubscriptions: this.NodesWhere(a=>a.self_subscriptionStatus != SubscriptionStatus.Initial).length, //nodesWithFulfilledSubscriptions: this.NodesWhere(a=>ObjectCE(a.PreferredDataContainer.status).IsOneOf(DataStatus.Received_Live, DataStatus.Received_CachedByMGL)).length, nodesWithFulfilledSubscriptions: this.NodesWhere(a=>a.self_subscriptionStatus == SubscriptionStatus.ReadyAndLive).length, }); } } export class GraphlinkStats { constructor(data?: Partial) { Object.assign(this, data); } attachedTreeNodes: number; nodesWithRequestedSubscriptions: number; nodesWithFulfilledSubscriptions: number; } export class UserInfo { id: string; //displayName: string; } // graph-refs system (this is how the standalone functions are able to know which graph they're operating on) // ========== export let defaultGraphRefs: GraphRefs; export function SetDefaultGraphRefs(opt: GraphRefs) { defaultGraphRefs = opt; } export interface GraphRefs { graph: Graphlink; }