UNPKG

@instantdb/core

Version:
244 lines • 9.02 kB
// The FrameworkClient class is a mini version of a query store that allows making queries on both the frontend and backend // you can register queries, await their results and serialize them over a server/client boundary. // The class is generic so that it can be a good starting off point to make other ssr adapters. import { coerceQuery, InstantAPIError, InstantError, weakHash, } from "./index.js"; import * as s from './store.js'; import instaql from './instaql.js'; import { createLinkIndex } from "./utils/linkIndex.js"; export const isServer = typeof window === 'undefined' || 'Deno' in globalThis; export class FrameworkClient { params; db; // stores all of the query promises so that ssr can read them // and send the relevant results alongside the html that resulted in the query resolving resultMap = new Map(); queryResolvedCallbacks = []; constructor(params) { this.params = params; this.db = params.db; this.resultMap = new Map(); } subscribe = (callback) => { this.queryResolvedCallbacks.push(callback); }; // Runs on the client when ssr gets html script tags addQueryResult = (queryKey, value) => { this.resultMap.set(queryKey, { type: value.type, status: 'success', data: value, promise: null, error: null, }); // send the result to the client if (!isServer) { // make sure the attrs are there to create stores if (!this.db._reactor.attrs) { this.db._reactor._setAttrs(value.attrs); } this.db._reactor._addQueryData(value.query, value, !!this.db._reactor.config.schema); } }; removeCachedQueryResult = (queryHash) => { this.resultMap.delete(queryHash); }; // Run a query on the client and return a promise with the result queryClient = (query_, opts) => { const { hash, query } = this.hashQuery(query_, opts); let resolve; let reject; const promise = new Promise((resolvePromise, rejectPromise) => { resolve = resolvePromise; reject = rejectPromise; }); let entry = { status: 'pending', type: 'session', data: undefined, error: undefined, promise: promise, }; let unsub = null; let unsubImmediately = false; unsub = this.db.subscribeQuery(query, (res) => { if (res.error) { entry.status = 'error'; entry.error = res.error; entry.promise = null; reject(res.error); } else { entry.status = 'success'; entry.data = res; entry.promise = null; resolve(res); } if (unsub !== null) { unsub(); } else { unsubImmediately; } }); // We may have gotten the result inside of subscribeQuery before // we defined the `unsub` function if (unsubImmediately) { unsub(); } this.resultMap.set(hash, entry); return promise; }; // creates an entry in the results map // and returns the same thing added to the map query = (_query, opts) => { const { hash, query } = this.hashQuery(_query, opts); if (this.db._reactor.status === 'authenticated') { const promise = this.db.queryOnce(_query, opts); let entry = { status: 'pending', type: 'session', data: undefined, error: undefined, promise: promise, }; promise.then((result) => { entry.status = 'success'; entry.data = result; entry.promise = null; }); promise.catch((error) => { entry.status = 'error'; entry.error = error; entry.promise = null; }); this.resultMap.set(hash, entry); return entry; } const promise = this.getTriplesAndAttrsForQuery(query); let entry = { status: 'pending', type: 'http', data: undefined, error: undefined, promise: promise, }; promise.then((result) => { entry.status = 'success'; entry.data = result; entry.promise = null; }); promise.catch((error) => { entry.status = 'error'; entry.error = error; entry.promise = null; }); promise.then((result) => { this.queryResolvedCallbacks.forEach((callback) => { callback({ queryHash: hash, query: query, attrs: result.attrs, triples: result.triples, pageInfo: result.pageInfo, }); }); }); this.resultMap.set(hash, entry); return entry; }; getExistingResultForQuery = (_query, opts) => { const { hash } = this.hashQuery(_query, opts); return this.resultMap.get(hash); }; // creates a query result from a set of triples, query, and attrs // can be run server side or client side completeIsomorphic = (query, triples, attrs, pageInfo) => { const attrMap = {}; attrs.forEach((attr) => { attrMap[attr.id] = attr; }); const enableCardinalityInference = Boolean(this.db?._reactor?.config?.schema) && ('cardinalityInference' in this.db?._reactor?.config ? Boolean(this.db?._reactor.config?.cardinalityInference) : true); const attrsStore = new s.AttrsStoreClass(attrs.reduce((acc, attr) => { acc[attr.id] = attr; return acc; }, {}), createLinkIndex(this.db?._reactor.config.schema)); const store = s.createStore(attrsStore, triples, enableCardinalityInference, this.params.db._reactor.config.useDateObjects || false); const resp = instaql({ store: store, attrsStore: attrsStore, pageInfo: pageInfo, aggregate: undefined, }, query); return resp; }; hashQuery = (_query, opts) => { if (_query && opts && 'ruleParams' in opts) { _query = { $$ruleParams: opts['ruleParams'], ..._query }; } const query = _query ? coerceQuery(_query) : null; return { hash: weakHash(query), query: query }; }; // Run by the server to get triples and attrs getTriplesAndAttrsForQuery = async (query) => { try { const response = await fetch(`${this.db._reactor.config.apiURI}/runtime/framework/query`, { method: 'POST', headers: { 'app-id': this.params.db._reactor.config.appId, 'Content-Type': 'application/json', Authorization: this.params.token ? `Bearer ${this.params.token}` : undefined, }, body: JSON.stringify({ query: query, }), }); if (!response.ok) { try { const data = await response.json(); if ('message' in data) { throw new InstantAPIError({ body: data, status: response.status }); } else { throw new Error('Error getting triples from server'); } } catch (e) { if (e instanceof InstantError) { throw e; } throw new Error('Error getting triples from server'); } } const data = await response.json(); const attrs = data?.attrs; if (!attrs) { throw new Error('No attrs'); } // TODO: make safer const triples = data.result?.[0].data?.['datalog-result']?.['join-rows'][0]; const pageInfo = data.result?.[0]?.data?.['page-info']; return { attrs, triples, type: 'http', queryHash: this.hashQuery(query).hash, query, pageInfo, }; } catch (err) { if (err instanceof InstantError) { throw err; } const errWithMessage = new Error('Error getting triples from framework client'); errWithMessage.cause = err; throw errWithMessage; } }; } //# sourceMappingURL=framework.js.map