UNPKG

grafast

Version:

Cutting edge GraphQL planning and execution engine

320 lines (318 loc) 13.2 kB
"use strict"; // import debugFactory from "debug"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ObjectStep = void 0; exports.object = object; const dev_js_1 = require("../dev.js"); const step_js_1 = require("../step.js"); const tamedevilUtils_1 = require("../tamedevilUtils"); const utils_js_1 = require("../utils.js"); const constant_js_1 = require("./constant.js"); const DEFAULT_CACHE_SIZE = 100; const EMPTY_OBJECT = Object.freeze(Object.create(null)); /** * A plan that represents an object using the keys given and the values being * the results of the associated plans. */ class ObjectStep extends step_js_1.UnbatchedStep { static { this.$$export = { moduleName: "grafast", exportName: "ObjectStep", }; } constructor(obj, cacheConfig) { super(); this.cacheConfig = cacheConfig; this.isSyncAndSafe = true; this.allowMultipleOptimizations = true; this.keys = []; // Optimize needs the same 'meta' for all ObjectSteps this.optimizeMetaKey = "ObjectStep"; this.cacheSize = cacheConfig?.cacheSize ?? (cacheConfig?.identifier ? DEFAULT_CACHE_SIZE : 0); const keys = Object.keys(obj); this._setKeys(keys); for (let i = 0, l = this.keys.length; i < l; i++) { this.addDependency({ step: obj[keys[i]], skipDeduplication: true }); } } _setKeys(keys) { this.keys = keys; this.peerKey = (0, utils_js_1.digestKeys)(keys); this.metaKey = this.cacheSize <= 0 ? undefined : this.cacheConfig?.identifier ? `object|${this.peerKey}|${this.cacheConfig.identifier}` : this.id; } /** * This key doesn't get typed, but it can be added later which can be quite * handy. */ set(key, plan) { this._setKeys([...this.keys, key]); this.addDependency({ step: plan, skipDeduplication: true }); } getStepForKey(key, allowMissing = false) { const idx = this.keys.indexOf(key); if (idx < 0) { if (!allowMissing) { throw new Error(`${this}: failed to retrieve plan for key '${String(key)}' - we have no such key`); } return null; } return this.getDepOptions(idx).step; } toStringMeta() { return "{" + this.keys.join(",") + "}"; } /* tupleToObject( meta: ObjectPlanMeta<TPlans>, ...tuple: Array<DataFromObjectSteps<TPlans>[keyof TPlans]> ): DataFromObjectSteps<TPlans> { // Note: `outerloop` is a JavaScript "label". They are not very common. // First look for an existing match: outerloop: for (let i = 0, l = meta.results.length; i < l; i++) { const [values, obj] = meta.results[i]; // Slow loop over each value in the tuples; this is not expected to be a // particularly big loop, typically only 2-5 keys. for (let j = 0, m = this.keys.length; j < m; j++) { if (values[j] !== tuple[j]) { // This isn't a match; try the next record in the outer loop continue outerloop; } } return obj; } // That failed; create a new object. debugObjectPlanVerbose( "%s: Could not find cache for keys %c values %c, constructing new object", this, this.keys, tuple, ); const newObj = this.keys.reduce((memo, key, i) => { memo[key] = tuple[i]; return memo; }, {} as Partial<DataFromObjectSteps<TPlans>>) as DataFromObjectSteps<TPlans>; // Cache newObj so the same tuple values result in the exact same object. meta.results.push([tuple, newObj]); return newObj; } */ tupleToObjectJIT(callback) { const keysAreSafe = this.keys.every(tamedevilUtils_1.isSafeObjectPropertyName); // Optimize common cases if (keysAreSafe) { if (this.cacheSize === 0 || this.keys.length === 0) { switch (this.keys.length) { case 0: { return callback(() => EMPTY_OBJECT); } case 1: { const [k0] = this.keys; return callback((_, val0) => ({ [k0]: val0 })); } case 2: { const [k0, k1] = this.keys; return callback((_, val0, val1) => ({ [k0]: val0, [k1]: val1 })); } case 3: { const [k0, k1, k2] = this.keys; return callback((_, val0, val1, val2) => ({ [k0]: val0, [k1]: val1, [k2]: val2 })); } case 4: { const [k0, k1, k2, k3] = this.keys; return callback((_, val0, val1, val2, val3) => ({ [k0]: val0, [k1]: val1, [k2]: val2, [k3]: val3 })); } case 5: { const [k0, k1, k2, k3, k4] = this.keys; return callback((_, val0, val1, val2, val3, val4) => ({ [k0]: val0, [k1]: val1, [k2]: val2, [k3]: val3, [k4]: val4, })); } case 6: { const [k0, k1, k2, k3, k4, k5] = this.keys; return callback((_, val0, val1, val2, val3, val4, val5) => ({ [k0]: val0, [k1]: val1, [k2]: val2, [k3]: val3, [k4]: val4, [k5]: val5, })); } } } else { const maxIdx = this.cacheSize - 1; switch (this.keys.length) { case 1: { const [k0] = this.keys; return callback((extra, val0) => { const meta = extra.meta; if (meta.nextIndex == null) { meta.nextIndex = 0; meta.results = []; } else { const cacheLen = meta.results.length; for (let cacheIdx = 0; cacheIdx < cacheLen; cacheIdx++) { const [cache0, obj] = meta.results[cacheIdx]; if (cache0 === val0) { return obj; } } } const obj = { [k0]: val0 }; meta.results[meta.nextIndex] = [val0, obj]; // Only cache `this.cacheSize` results, use a round-robin meta.nextIndex = meta.nextIndex >= maxIdx ? 0 : meta.nextIndex + 1; return obj; }); } case 2: { const [k0, k1] = this.keys; return callback((extra, val0, val1) => { const meta = extra.meta; if (meta.nextIndex == null) { meta.nextIndex = 0; meta.results = []; } else { const cacheLen = meta.results.length; for (let cacheIdx = 0; cacheIdx < cacheLen; cacheIdx++) { const [cache0, cache1, obj] = meta.results[cacheIdx]; if (cache0 === val0 && cache1 === val1) { return obj; } } } const obj = { [k0]: val0, [k1]: val1 }; meta.results[meta.nextIndex] = [val0, val1, obj]; // Only cache `this.cacheSize` results, use a round-robin meta.nextIndex = meta.nextIndex >= maxIdx ? 0 : meta.nextIndex + 1; return obj; }); } } } } const keys = this.keys; const keyCount = keys.length; if (this.cacheSize > 0) { // NOTE: `peerKey` ensures that the keys match, so we only need to check values return callback((extra, ...vals) => { const meta = extra.meta; if (meta.nextIndex != null) { nextMetaResult: for (let metaResultIndex = 0, metaResultLength = meta.results.length; metaResultIndex < metaResultLength; metaResultIndex++) { const [cacheValues, obj] = meta.results[metaResultIndex]; for (let keyIndex = 0; keyIndex < keyCount; keyIndex++) { if (cacheValues[keyIndex] !== vals[keyIndex]) { continue nextMetaResult; } } return obj; } } else { meta.nextIndex = 0; meta.results = []; } const obj = Object.create(null); for (let keyIndex = 0; keyIndex < keyCount; keyIndex++) { obj[keys[keyIndex]] = vals[keyIndex]; } meta.results[meta.nextIndex] = [vals, obj]; // Only cache `this.cacheSize` results, use a round-robin meta.nextIndex = meta.nextIndex >= this.cacheSize - 1 ? 0 : meta.nextIndex + 1; return obj; }); } else { return callback((_, ...values) => { const obj = Object.create(null); for (let keyIndex = 0; keyIndex < keyCount; keyIndex++) { obj[keys[keyIndex]] = values[keyIndex]; } return obj; }); } } finalize() { this.tupleToObjectJIT((fn) => { this.unbatchedExecute = fn; }); return super.finalize(); } execute({ indexMap, values, extra, }) { return indexMap((i) => this.unbatchedExecute(extra, ...values.map((v) => v.at(i)))); } unbatchedExecute(_extra, ..._values) { throw new Error(`${this} didn't finalize? No unbatchedExecute method.`); } deduplicate(peers) { // Managed through peerKey return peers; } optimize(opts) { if (this.dependencies.every((dep) => dep instanceof constant_js_1.ConstantStep)) { // Replace self with constant // We'll cache so that the constants can be more easily deduplicated const meta = opts.meta; const keysJoined = this.keys.join(","); if (!meta[keysJoined]) { meta[keysJoined] = []; } const existing = meta[keysJoined].find((existingObj) => this.keys.every((key, i) => existingObj[key] === this.dependencies[i].data)); const isSensitive = this.dependencies.some((d) => d.isSensitive); if (existing !== undefined) { return (0, constant_js_1.constant)(existing, isSensitive); } else { const obj = Object.create(null); for (let i = 0, l = this.keys.length; i < l; i++) { const key = this.keys[i]; const value = this.dependencies[i].data; obj[key] = value; } meta[keysJoined].push(obj); return (0, constant_js_1.constant)(obj, isSensitive); } } return this; } /** * Get the original plan with the given key back again. */ get(key) { const index = this.keys.indexOf(key); if (index < 0) { if (dev_js_1.isDev) { // TODO: move this to diagnostics console.warn(`${this} doesn't have key '${String(key)}'; supported keys: '${this.keys.join("', '")}'`); } return (0, constant_js_1.constant)(undefined); } return this.getDepOptions(index).step; } } exports.ObjectStep = ObjectStep; /** * A plan that represents an object using the keys given and the values being * the results of the associated plans. */ function object(obj, cacheConfig) { return new ObjectStep(obj, cacheConfig); } //# sourceMappingURL=object.js.map