grafast
Version:
Cutting edge GraphQL planning and execution engine
389 lines • 15 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LoadStep = exports.LoadedRecordStep = void 0;
exports.loadOneCallback = loadOneCallback;
exports.loadManyCallback = loadManyCallback;
exports.loadMany = loadMany;
exports.loadOne = loadOne;
const index_js_1 = require("../index.js");
const multistep_js_1 = require("../multistep.js");
const step_js_1 = require("../step.js");
const utils_js_1 = require("../utils.js");
const access_js_1 = require("./access.js");
const nextTick = typeof process !== "undefined" && typeof process.nextTick === "function"
? (cb) => process.nextTick(cb)
: (cb) => setTimeout(cb, 0);
/**
* A TypeScript Identity Function to help you strongly type your
* LoadOneCallback.
*/
function loadOneCallback(callback) {
return callback;
}
/**
* A TypeScript Identity Function to help you strongly type your
* LoadManyCallback.
*/
function loadManyCallback(callback) {
return callback;
}
const idByLoad = new WeakMap();
let loadCounter = 0;
/**
* You shouldn't create instances of this yourself - use `loadOne` or `loadMany` instead.
*/
class LoadedRecordStep extends step_js_1.Step {
static { this.$$export = {
moduleName: "grafast",
exportName: "LoadedRecordStep",
}; }
constructor($data, isSingle, sourceDescription,
// Only safe to reference this during planning phase
ioEquivalence) {
super();
this.isSingle = isSingle;
this.sourceDescription = sourceDescription;
this.ioEquivalence = ioEquivalence;
this.isSyncAndSafe = true;
this.attributes = new Set();
this.params = Object.create(null);
this.addDependency($data);
}
toStringMeta() {
return this.sourceDescription ?? null;
}
get(attr) {
return this.cacheStep("get", attr, () => this._getInner(attr));
}
_getInner(attr) {
// Allow auto-collapsing of the waterfall by knowing keys are equivalent
if (this.operationPlan.phase === "plan" &&
this.ioEquivalence[attr]) {
return this.ioEquivalence[attr];
}
this.attributes.add(attr);
return (0, access_js_1.access)(this, attr);
}
setParam(paramKey, value) {
if (!this.isSingle) {
throw new Error("setParam should not be called on list items - call it on the collection (`loadMany()` step)");
}
this.params[paramKey] = value;
}
deduplicate(peers) {
return peers.filter((p) => p.isSingle === this.isSingle &&
p.sourceDescription === this.sourceDescription &&
(0, utils_js_1.recordsMatch)(p.ioEquivalence, this.ioEquivalence) &&
(0, utils_js_1.recordsMatch)(p.params, this.params));
}
deduplicatedWith(replacement) {
for (const attr of this.attributes) {
replacement.attributes.add(attr);
}
}
optimize() {
const $source = this.getDepDeep(0);
if ($source instanceof LoadStep) {
// Tell our parent we only need certain attributes
$source.addAttributes(this.attributes);
for (const [key, value] of Object.entries(this.params)) {
$source.setParam(key, value);
}
}
else {
// This should never happen
console.warn(`LoadedRecordStep could not find the parent LoadStep; instead found ${$source}`);
}
// Record has no run-time behaviour (it's just a plan-time helper), so we
// can replace ourself with our dependency:
return this.getDep(0);
}
// This'll never be called, due to `optimize` above.
execute({ count, values: [values0], }) {
return values0.isBatch
? values0.entries
: (0, utils_js_1.arrayOfLength)(count, values0.value);
}
}
exports.LoadedRecordStep = LoadedRecordStep;
class LoadStep extends step_js_1.Step {
/* implements ListCapableStep<TItem, LoadedRecordStep<TItem, TParams>> */
static { this.$$export = { moduleName: "grafast", exportName: "LoadStep" }; }
constructor(spec, unarySpec, ioEquivalence, load) {
super();
this.ioEquivalence = ioEquivalence;
this.load = load;
this.isSyncAndSafe = false;
this.loadOptions = null;
this.loadOptionsKey = "";
this.attributes = new Set();
this.params = Object.create(null);
this.unaryDepId = null;
const $spec = (0, multistep_js_1.multistep)(spec, "load");
this.addDependency($spec);
const $unarySpec = unarySpec == null ? null : (0, multistep_js_1.multistep)(unarySpec, "loadUnary");
if ($unarySpec) {
this.unaryDepId = this.addUnaryDependency($unarySpec);
}
}
toStringMeta() {
return this.load.displayName || this.load.name;
}
makeAccessMap() {
const map = Object.create(null);
const $spec = this.getDep(0);
if (this.ioEquivalence == null) {
return map;
}
else if (typeof this.ioEquivalence === "string") {
map[this.ioEquivalence] = $spec;
return map;
}
else if ((0, utils_js_1.isTuple)(this.ioEquivalence)) {
for (let i = 0, l = this.ioEquivalence.length; i < l; i++) {
const key = this.ioEquivalence[i];
map[key] = (0, step_js_1.isListLikeStep)($spec) ? $spec.at(i) : (0, access_js_1.access)($spec, [i]);
}
return map;
}
else if (typeof this.ioEquivalence === "object") {
for (const key of Object.keys(this.ioEquivalence)) {
const attr = this.ioEquivalence[key];
if (attr != null) {
map[attr] = (0, step_js_1.isObjectLikeStep)($spec)
? $spec.get(key)
: (0, access_js_1.access)($spec, [key]);
}
}
return map;
}
else {
throw new Error(`ioEquivalence passed to loadOne() or loadMany() call not understood`);
}
}
listItem($item) {
return new LoadedRecordStep($item, false, this.toStringMeta(), this.makeAccessMap());
}
single() {
return new LoadedRecordStep(this, true, this.toStringMeta(), this.makeAccessMap());
}
setParam(paramKey, value) {
this.params[paramKey] = value;
}
addAttributes(attributes) {
for (const attribute of attributes) {
this.attributes.add(attribute);
}
}
deduplicate(peers) {
return peers.filter((p) => p.load === this.load &&
ioEquivalenceMatches(p.ioEquivalence, this.ioEquivalence) &&
(0, utils_js_1.recordsMatch)(p.params, this.params));
}
deduplicatedWith(replacement) {
for (const attr of this.attributes) {
replacement.attributes.add(attr);
}
}
finalize() {
// Find all steps of this type that use the same callback and have
// equivalent params and then match their list of attributes together.
const stringifiedParams = (0, utils_js_1.canonicalJSONStringify)(this.params);
const kin = this.operationPlan
.getStepsByStepClass(LoadStep)
.filter((step) => {
if (step.id === this.id)
return false;
if (step.load !== this.load)
return false;
if ((0, utils_js_1.canonicalJSONStringify)(step.params) !== stringifiedParams)
return false;
return true;
});
for (const otherStep of kin) {
for (const attr of otherStep.attributes) {
this.attributes.add(attr);
}
}
// Build the loadOptions
this.loadOptions = {
attributes: [...this.attributes].sort(),
params: this.params,
};
// If the canonicalJSONStringify is the same, then we deem that the options are the same
this.loadOptionsKey = (0, utils_js_1.canonicalJSONStringify)(this.loadOptions);
let loadId = idByLoad.get(this.load);
if (!loadId) {
loadId = String(++loadCounter);
idByLoad.set(this.load, loadId);
}
this.metaKey = `LoadStep|${loadId}|${this.loadOptionsKey}`;
super.finalize();
}
execute({ count, values: [values0, values1], extra, }) {
const meta = extra.meta;
let cache = meta.cache;
if (!cache) {
cache = new Map();
meta.cache = cache;
}
const batch = new Map();
const unary = values1?.isBatch === false ? values1.value : undefined;
const results = [];
for (let i = 0; i < count; i++) {
const spec = values0.at(i);
if (cache.has(spec)) {
results.push(cache.get(spec));
}
else {
// We'll fill this in in a minute
const index = results.push(null) - 1;
const existingIdx = batch.get(spec);
if (existingIdx !== undefined) {
existingIdx.push(index);
}
else {
batch.set(spec, [index]);
}
}
}
const pendingCount = batch.size;
if (pendingCount > 0) {
const deferred = (0, index_js_1.defer)();
const batchSpecs = [...batch.keys()];
const loadBatch = { deferred, batchSpecs };
if (!meta.loadBatchesByLoad) {
meta.loadBatchesByLoad = new Map();
}
let loadBatches = meta.loadBatchesByLoad.get(this.load);
if (loadBatches) {
// Add to existing batch load
loadBatches.push(loadBatch);
}
else {
// Create new batch load
loadBatches = [loadBatch];
meta.loadBatchesByLoad.set(this.load, loadBatches);
// Guaranteed by the metaKey to be equivalent for all entries sharing the same `meta`. Note equivalent is not identical; key order may change.
const loadOptions = this.loadOptions;
nextTick(() => {
// Don't allow adding anything else to the batch
meta.loadBatchesByLoad.delete(this.load);
executeBatches(loadBatches, this.load, {
...loadOptions,
unary: unary,
});
});
}
return (async () => {
const loadResults = await deferred;
for (let pendingIndex = 0; pendingIndex < pendingCount; pendingIndex++) {
const spec = batchSpecs[pendingIndex];
const targetIndexes = batch.get(spec);
const loadResult = loadResults[pendingIndex];
cache.set(spec, loadResult);
for (const targetIndex of targetIndexes) {
results[targetIndex] = loadResult;
}
}
return results;
})();
}
return results;
}
}
exports.LoadStep = LoadStep;
async function executeBatches(loadBatches, load, loadOptions) {
try {
const numberOfBatches = loadBatches.length;
if (numberOfBatches === 1) {
const [loadBatch] = loadBatches;
loadBatch.deferred.resolve(load(loadBatch.batchSpecs, loadOptions));
return;
}
else {
// Do some tick-batching!
const indexStarts = [];
const allBatchSpecs = [];
for (let i = 0; i < numberOfBatches; i++) {
const loadBatch = loadBatches[i];
indexStarts[i] = allBatchSpecs.length;
for (const batchSpec of loadBatch.batchSpecs) {
allBatchSpecs.push(batchSpec);
}
}
const results = await load(allBatchSpecs, loadOptions);
for (let i = 0; i < numberOfBatches; i++) {
const loadBatch = loadBatches[i];
const start = indexStarts[i];
const stop = indexStarts[i + 1] ?? allBatchSpecs.length;
const entries = results.slice(start, stop);
loadBatch.deferred.resolve(entries);
}
}
}
catch (e) {
for (const loadBatch of loadBatches) {
loadBatch.deferred.reject(e);
}
}
}
function load(spec, unarySpec, ioEquivalence, loadCallback) {
return new LoadStep(spec, unarySpec, ioEquivalence, loadCallback);
}
function loadMany(spec, loadCallbackOrIoEquivalenceOrUnarySpec, loadCallbackOrIoEquivalence, loadCallbackOnly) {
if (loadCallbackOnly) {
return load(spec, loadCallbackOrIoEquivalenceOrUnarySpec, loadCallbackOrIoEquivalence, loadCallbackOnly);
}
// At most 3 arguments
else if ((0, multistep_js_1.isMultistep)(loadCallbackOrIoEquivalenceOrUnarySpec)) {
return load(spec, loadCallbackOrIoEquivalenceOrUnarySpec, null, loadCallbackOrIoEquivalence);
}
// Unary step is definitely null; 3 arguments
else if (loadCallbackOrIoEquivalence) {
return load(spec, null, loadCallbackOrIoEquivalenceOrUnarySpec, loadCallbackOrIoEquivalence);
}
// 2 arguments
else {
return load(spec, null, null, loadCallbackOrIoEquivalenceOrUnarySpec);
}
}
function loadOne(spec, loadCallbackOrIoEquivalenceOrUnarySpec, loadCallbackOrIoEquivalence, loadCallbackOnly) {
if (loadCallbackOnly) {
return load(spec, loadCallbackOrIoEquivalenceOrUnarySpec, loadCallbackOrIoEquivalence, loadCallbackOnly).single();
}
// At most 3 arguments
else if ((0, multistep_js_1.isMultistep)(loadCallbackOrIoEquivalenceOrUnarySpec)) {
return load(spec, loadCallbackOrIoEquivalenceOrUnarySpec, null, loadCallbackOrIoEquivalence).single();
}
// Unary step is definitely null; 3 arguments
else if (loadCallbackOrIoEquivalence) {
return load(spec, null, loadCallbackOrIoEquivalenceOrUnarySpec, loadCallbackOrIoEquivalence).single();
}
else {
return load(spec, null, null, loadCallbackOrIoEquivalenceOrUnarySpec).single();
}
}
function ioEquivalenceMatches(io1, io2) {
if (io1 === io2)
return true;
if (io1 == null)
return false;
if (io2 == null)
return false;
if (typeof io1 === "string")
return false;
if (typeof io2 === "string")
return false;
if (Array.isArray(io1)) {
if (!Array.isArray(io2))
return false;
return (0, utils_js_1.arraysMatch)(io1, io2);
}
else {
if (Array.isArray(io2))
return false;
return (0, utils_js_1.recordsMatch)(io1, io2);
}
}
//# sourceMappingURL=load.js.map