gqty
Version:
The No-GraphQL Client for TypeScript
322 lines (319 loc) • 11.2 kB
JavaScript
import { debounceMicrotaskPromise } from 'debounce-microtasks';
import { MultiDict } from 'multidict';
import { createSchemaAccessor } from '../Accessor/index.mjs';
import { createDeferredIterator } from '../Utils/deferred.mjs';
import { pick } from '../Utils/pick.mjs';
import { addSelections, getSelectionsSet, delSelectionSet } from './batching.mjs';
import { createContext } from './context.mjs';
import { fetchSelections, subscribeSelections } from './resolveSelections.mjs';
import { isWsClient, createSubscriber } from './subscriber.mjs';
import { updateCaches } from './updateCaches.mjs';
const pendingQueries = /* @__PURE__ */ new WeakMap();
const getIntersection = (subject, object) => {
if (typeof subject.intersection === "function") {
return subject.intersection(object);
}
const intersection = /* @__PURE__ */ new Set();
for (const item of object) {
if (subject.has(item)) {
intersection.add(item);
}
}
return intersection;
};
const createResolvers = ({
aliasLength,
batchWindow,
cache: resolverCache,
debugger: debug,
depthLimit,
fetchOptions,
fetchOptions: {
cachePolicy: defaultCachePolicy = "default",
retryPolicy: defaultRetryPoliy
},
scalars,
schema,
parentContext
}) => {
const correlatedCaches = new MultiDict();
const subscriber = isWsClient(fetchOptions.subscriber) ? createSubscriber(fetchOptions.subscriber) : fetchOptions.subscriber;
const createResolver = ({
cachePolicy = defaultCachePolicy,
extensions,
onSelect,
onSubscribe,
operationName,
retryPolicy = defaultRetryPoliy
} = {}) => {
const prevSelections = /* @__PURE__ */ new Set();
const replaceSet = (target, source) => {
target.clear();
for (const value of source) {
target.add(value);
}
};
const selections = /* @__PURE__ */ new Set();
const context = createContext({
aliasLength,
cache: resolverCache,
cachePolicy,
depthLimit,
scalars,
schema
});
context.subscribeSelect((selection, selectionCache) => {
const targetSelections = selectionCache === void 0 ? (
// For empty arrays and null objects, trigger sub-selections made
// in previous selections.
getIntersection(selection.getLeafNodes(), prevSelections)
) : [selection];
for (const selection2 of targetSelections) {
if (!selections.has(selection2)) {
if (false === (onSelect == null ? void 0 : onSelect(selection2, selectionCache))) {
continue;
}
selections.add(selection2);
parentContext == null ? void 0 : parentContext.select(selection2, selectionCache);
}
}
});
const { accessor } = createSchemaAccessor(context);
let activePromise;
const resolve = async () => {
if (selections.size === 0) {
if (process.env.NODE_ENV !== "production") {
console.warn(
"[GQty] No selections found. If you are reading from the global accessors, try using the first argument instead."
);
}
return;
}
if (!context.shouldFetch) return;
if (cachePolicy === "only-if-cached") {
throw new TypeError("Failed to fetch");
}
const selectionsCacheKey = `${operationName != null ? operationName : cachePolicy === "no-store" ? "no-store" : "default"}`;
const pendingSelections = addSelections(
resolverCache,
selectionsCacheKey,
selections
);
correlatedCaches.set(pendingSelections, context.cache);
if (!pendingQueries.has(pendingSelections)) {
pendingQueries.set(
pendingSelections,
// Batching happens at the end of microtask queue
debounceMicrotaskPromise(
async () => {
var _a, _b;
pendingQueries.delete(pendingSelections);
if (batchWindow) {
await new Promise(
(resolve2) => setTimeout(resolve2, batchWindow)
);
}
const uniqueSelections = /* @__PURE__ */ new Set();
(_a = getSelectionsSet(resolverCache, selectionsCacheKey)) == null ? void 0 : _a.forEach(
(selections2) => {
selections2.forEach((selection) => {
uniqueSelections.add(selection);
});
}
);
delSelectionSet(resolverCache, selectionsCacheKey);
const results = await fetchSelections(uniqueSelections, {
cache: context.cache,
debugger: debug,
extensions,
fetchOptions: { ...fetchOptions, cachePolicy, retryPolicy },
operationName
});
const targetCaches = (_b = correlatedCaches.get(pendingSelections)) != null ? _b : /* @__PURE__ */ new Set();
if (cachePolicy !== "no-store") {
targetCaches.add(resolverCache);
}
updateCaches(results, targetCaches, {
skipNotify: promiseDropped() || !context.notifyCacheUpdate
});
correlatedCaches.delete(resolverCache);
return results;
},
// When naughty users are adding selections every next microtask, we
// forcibly start the fetch after a number of delays. This number is
// picked arbitrarily, it should be a number that is large enough to
// prevent excessive fetches but small enough to not block the
// actual fetch indefinitely.
{
debounceLimit: 20,
limitAction: "invoke"
}
)
);
const currentPromise = pendingQueries.get(pendingSelections)();
activePromise = currentPromise;
const promiseDropped = () => activePromise !== void 0 && currentPromise !== activePromise;
currentPromise.then(
() => {
if (promiseDropped()) return;
if (selections.size === 0) return;
replaceSet(prevSelections, selections);
selections.clear();
},
() => {
}
).finally(() => {
if (promiseDropped()) return;
context.reset();
activePromise = void 0;
});
}
return pendingQueries.get(pendingSelections)();
};
const subscribe = ({
onComplete,
onError,
onNext
} = {}) => {
if (selections.size === 0) {
if (process.env.NODE_ENV !== "production") {
console.warn(
"[GQty] No selections found! If you are reading from the global accessors, try using the first argument instead."
);
}
return () => {
};
}
const unsubscibers = /* @__PURE__ */ new Set();
const unsubscribe = () => {
for (const unsubscribe2 of unsubscibers) {
unsubscribe2();
}
};
if (onNext) {
const unsubscribe2 = context.cache.subscribe(
[...selections].map((s) => s.cacheKeys.join(".")),
(data) => onNext({ data })
);
unsubscibers.add(unsubscribe2);
}
const subscriptionSelections = /* @__PURE__ */ new Set();
const promises = [];
if (context.shouldFetch) {
for (const selection of selections) {
if (selection.root.key === "subscription") {
selections.delete(selection);
subscriptionSelections.add(selection);
}
}
if (selections.size > 0) {
promises.push(resolve().catch(onError));
}
}
for (const selection of subscriptionSelections) {
selections.add(selection);
}
if (subscriptionSelections.size) {
let lastSelectionsUpdated = false;
const promise = new Promise((resolve2, reject) => {
const unsubscribe2 = subscribeSelections(
subscriptionSelections,
({ data, error, extensions: extensions2 }) => {
if (error) {
onError == null ? void 0 : onError(error);
reject(error);
} else if (data !== void 0) {
updateCaches(
[{ data, error, extensions: extensions2 }],
cachePolicy !== "no-store" && context.cache !== resolverCache ? [context.cache, resolverCache] : [context.cache]
);
if (!lastSelectionsUpdated) {
lastSelectionsUpdated = true;
if (selections.size > 0) {
replaceSet(prevSelections, selections);
}
}
} else ;
},
{
cache: context.cache,
debugger: debug,
extensions,
fetchOptions: {
...fetchOptions,
cachePolicy,
retryPolicy,
subscriber
},
operationName,
onSubscribe: () => queueMicrotask(() => onSubscribe == null ? void 0 : onSubscribe(unsubscribe2)),
onComplete: () => resolve2()
}
);
unsubscibers.add(unsubscribe2);
});
promises.push(promise);
}
Promise.allSettled(promises).finally(onComplete);
return unsubscribe;
};
return {
accessor,
context,
resolve,
restorePreviousSelections: () => replaceSet(selections, prevSelections),
selections,
subscribe
};
};
return {
createResolver,
resolve: async (fn, options) => {
var _a, _b;
const { accessor, resolve, selections } = createResolver(options);
const dataFn = () => fn(accessor);
dataFn();
const fetchPromise = resolve().then(dataFn, options == null ? void 0 : options.onError);
if ((_a = options == null ? void 0 : options.awaitsFetch) != null ? _a : true) {
await fetchPromise;
}
(_b = options == null ? void 0 : options.onFetch) == null ? void 0 : _b.call(options, fetchPromise);
const result = dataFn();
if (result === void 0) {
return pick(accessor, selections);
}
return result;
},
subscribe: (fn, { onSubscribe, ...options } = {}) => {
const { accessor, selections, subscribe } = createResolver({
...options,
onSubscribe: (unsubscribe2) => {
onSubscribe == null ? void 0 : onSubscribe(() => {
unsubscribe2();
observable.complete();
});
}
});
fn(accessor);
const unsubscribe = subscribe({
onError: (error) => {
if (observable.throw === void 0) {
throw error;
}
observable.throw(error);
},
onNext(value) {
var _a;
const message = (_a = fn(accessor)) != null ? _a : value;
observable.send(message);
}
});
const observable = createDeferredIterator();
if (selections.size === 0) {
observable.complete();
}
return { ...observable, unsubscribe };
}
};
};
export { createResolvers };