grafast 0.1.1-beta.16 → 0.1.1-beta.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -190,5 +190,19 @@ export declare class OperationPlan {
190
190
  getStepsByStepClass<TClass extends ExecutableStep>(klass: {
191
191
  new (...args: any[]): TClass;
192
192
  }): TClass[];
193
+ private _cacheStepStoreByLayerPlanAndActionKey;
194
+ /**
195
+ * Cache a generated step by a given identifier (cacheKey) such that we don't
196
+ * need to regenerate it on future calls, significantly reducing the load on
197
+ * deduplication later.
198
+ *
199
+ * @experimental
200
+ */
201
+ cacheStep<T extends ExecutableStep>(ownerStep: ExecutableStep, actionKey: string, cacheKey: symbol | string | number, cb: () => T): T;
202
+ /**
203
+ * Clears the cache, typically due to side effects having taken place. Called
204
+ * from setting hasSideEffects on an ExecutableStep, among other places.
205
+ */
206
+ resetCache(): void;
193
207
  }
194
208
  //# sourceMappingURL=OperationPlan.d.ts.map
@@ -137,6 +137,7 @@ class OperationPlan {
137
137
  return step[interfaces_js_1.$$proxy] ?? step;
138
138
  };
139
139
  this.polymorphicLayerPlanByPathByLayerPlan = new Map();
140
+ this._cacheStepStoreByLayerPlanAndActionKey = Object.create(null);
140
141
  this.scalarPlanInfo = { schema: this.schema };
141
142
  const queryType = schema.getQueryType();
142
143
  assert.ok(queryType, "Schema must have a query type");
@@ -187,11 +188,13 @@ class OperationPlan {
187
188
  this.lap("hoistSteps", "planOperation");
188
189
  if (index_js_1.isDev) {
189
190
  this.phase = "validate";
191
+ this.resetCache();
190
192
  // Helpfully check steps don't do forbidden things.
191
193
  this.validateSteps();
192
194
  this.lap("validateSteps");
193
195
  }
194
196
  this.phase = "optimize";
197
+ this.resetCache();
195
198
  // Get rid of temporary steps before `optimize` triggers side-effects.
196
199
  // (Critical due to steps that may have been discarded due to field errors
197
200
  // or similar.)
@@ -208,6 +211,7 @@ class OperationPlan {
208
211
  this.checkTimeout();
209
212
  this.lap("inlineSteps");
210
213
  this.phase = "finalize";
214
+ this.resetCache();
211
215
  this.stepTracker.finalizeSteps();
212
216
  // Get rid of steps that are no longer needed after optimising outputPlans
213
217
  // (we shouldn't see any new steps or dependencies after here)
@@ -244,6 +248,7 @@ class OperationPlan {
244
248
  });
245
249
  this.lap("finalizeOutputPlans");
246
250
  this.phase = "ready";
251
+ this.resetCache();
247
252
  // this.walkFinalizedPlans();
248
253
  // this.preparePrefetches();
249
254
  const allMetaKeys = new Set();
@@ -1409,15 +1414,16 @@ class OperationPlan {
1409
1414
  if (step.hasSideEffects) {
1410
1415
  return EMPTY_ARRAY;
1411
1416
  }
1412
- const { dependencies: deps, dependencyForbiddenFlags: flags, dependencyOnReject: onReject, layerPlan: layerPlan, constructor: stepConstructor, } = (0, utils_js_1.sudo)(step);
1417
+ const { dependencies: deps, dependencyForbiddenFlags: flags, dependencyOnReject: onReject, layerPlan: layerPlan, constructor: stepConstructor, peerKey, } = (0, utils_js_1.sudo)(step);
1413
1418
  const dependencyCount = deps.length;
1414
1419
  if (dependencyCount === 0) {
1415
1420
  let allPeers = null;
1416
- for (const possiblyPeer of this.stepTracker.stepsWithNoDependencies) {
1421
+ const stepsWithNoDependencies = this.stepTracker.stepsWithNoDependenciesByConstructor.get(step.constructor) ?? new Set();
1422
+ for (const possiblyPeer of stepsWithNoDependencies) {
1417
1423
  if (possiblyPeer !== step &&
1418
1424
  !possiblyPeer.hasSideEffects &&
1419
1425
  possiblyPeer.layerPlan === layerPlan &&
1420
- possiblyPeer.constructor === stepConstructor) {
1426
+ possiblyPeer.peerKey === peerKey) {
1421
1427
  if (allPeers === null) {
1422
1428
  allPeers = [possiblyPeer];
1423
1429
  }
@@ -1441,14 +1447,17 @@ class OperationPlan {
1441
1447
  const minDepth = Math.max(deferBoundaryDepth, dep.layerPlan.depth);
1442
1448
  let allPeers = null;
1443
1449
  for (const { dependencyIndex: peerDependencyIndex, step: rawPossiblyPeer, } of dep.dependents) {
1450
+ if (peerDependencyIndex !== 0 ||
1451
+ rawPossiblyPeer === step ||
1452
+ rawPossiblyPeer.hasSideEffects ||
1453
+ rawPossiblyPeer.constructor !== stepConstructor ||
1454
+ rawPossiblyPeer.peerKey !== peerKey) {
1455
+ continue;
1456
+ }
1444
1457
  const possiblyPeer = (0, utils_js_1.sudo)(rawPossiblyPeer);
1445
1458
  const { layerPlan: peerLayerPlan, dependencyForbiddenFlags: peerFlags, dependencyOnReject: peerOnReject, } = possiblyPeer;
1446
- if (possiblyPeer !== step &&
1447
- peerDependencyIndex === 0 &&
1448
- !possiblyPeer.hasSideEffects &&
1449
- possiblyPeer.constructor === stepConstructor &&
1450
- peerLayerPlan.depth >= minDepth &&
1451
- (0, utils_js_1.sudo)(possiblyPeer).dependencies.length === dependencyCount &&
1459
+ if (peerLayerPlan.depth >= minDepth &&
1460
+ possiblyPeer.dependencies.length === dependencyCount &&
1452
1461
  peerLayerPlan === ancestry[peerLayerPlan.depth] &&
1453
1462
  peerFlags[0] === flags[0] &&
1454
1463
  peerOnReject[0] === onReject[0]) {
@@ -1475,7 +1484,8 @@ class OperationPlan {
1475
1484
  */
1476
1485
  let minDepth = deferBoundaryDepth;
1477
1486
  const possiblePeers = [];
1478
- for (let dependencyIndex = 0; dependencyIndex < dependencyCount; dependencyIndex++) {
1487
+ // Loop backwards since last dependency is most likely to be most unique
1488
+ for (let dependencyIndex = dependencyCount - 1; dependencyIndex >= 0; dependencyIndex--) {
1479
1489
  const dep = deps[dependencyIndex];
1480
1490
  const dl = dep.dependents.length;
1481
1491
  if (dl === 1) {
@@ -1488,13 +1498,16 @@ class OperationPlan {
1488
1498
  // dependents (since it was added last it's more likely to be
1489
1499
  // unique).
1490
1500
  for (const { dependencyIndex: peerDependencyIndex, step: rawPossiblyPeer, } of dep.dependents) {
1501
+ if (peerDependencyIndex !== dependencyIndex ||
1502
+ rawPossiblyPeer === step ||
1503
+ rawPossiblyPeer.hasSideEffects ||
1504
+ rawPossiblyPeer.constructor !== stepConstructor ||
1505
+ rawPossiblyPeer.peerKey !== peerKey) {
1506
+ continue;
1507
+ }
1491
1508
  const possiblyPeer = (0, utils_js_1.sudo)(rawPossiblyPeer);
1492
1509
  const { layerPlan: peerLayerPlan, dependencyForbiddenFlags: peerFlags, dependencyOnReject: peerOnReject, dependencies: peerDependencies, } = possiblyPeer;
1493
- if (possiblyPeer !== step &&
1494
- peerDependencyIndex === dependencyIndex &&
1495
- !possiblyPeer.hasSideEffects &&
1496
- possiblyPeer.constructor === stepConstructor &&
1497
- peerDependencies.length === dependencyCount &&
1510
+ if (peerDependencies.length === dependencyCount &&
1498
1511
  peerLayerPlan === ancestry[peerLayerPlan.depth] &&
1499
1512
  peerFlags[0] === flags[0] &&
1500
1513
  peerOnReject[0] === onReject[0]) {
@@ -2611,6 +2624,38 @@ class OperationPlan {
2611
2624
  }
2612
2625
  return matches;
2613
2626
  }
2627
+ /**
2628
+ * Cache a generated step by a given identifier (cacheKey) such that we don't
2629
+ * need to regenerate it on future calls, significantly reducing the load on
2630
+ * deduplication later.
2631
+ *
2632
+ * @experimental
2633
+ */
2634
+ cacheStep(ownerStep, actionKey, cacheKey, cb) {
2635
+ const layerPlan = (0, withGlobalLayerPlan_js_1.currentLayerPlan)();
2636
+ const cache = (this._cacheStepStoreByLayerPlanAndActionKey[`${actionKey}|${layerPlan.id}|${ownerStep.id}`] ??= Object.create(null));
2637
+ const cacheIt = () => {
2638
+ const stepToCache = cb();
2639
+ if (!(stepToCache instanceof index_js_1.ExecutableStep)) {
2640
+ throw new Error(`The callback passed to cacheStep must always return an ExecutableStep; but this call from ${ownerStep} returned instead ${(0, inspect_js_1.inspect)(stepToCache)}`);
2641
+ }
2642
+ cache[cacheKey] = stepToCache.id;
2643
+ return stepToCache;
2644
+ };
2645
+ if (!(cacheKey in cache)) {
2646
+ return cacheIt();
2647
+ }
2648
+ const cachedStepId = cache[cacheKey];
2649
+ const cachedStep = this.stepTracker.stepById[cachedStepId];
2650
+ return cachedStep ?? cacheIt();
2651
+ }
2652
+ /**
2653
+ * Clears the cache, typically due to side effects having taken place. Called
2654
+ * from setting hasSideEffects on an ExecutableStep, among other places.
2655
+ */
2656
+ resetCache() {
2657
+ this._cacheStepStoreByLayerPlanAndActionKey = Object.create(null);
2658
+ }
2614
2659
  }
2615
2660
  exports.OperationPlan = OperationPlan;
2616
2661
  function makeDefaultPlan(fieldName) {
@@ -30,7 +30,7 @@ class StepTracker {
30
30
  /** @internal */
31
31
  this.aliasesById = [];
32
32
  /** @internal */
33
- this.stepsWithNoDependencies = new Set();
33
+ this.stepsWithNoDependenciesByConstructor = new Map();
34
34
  /** @internal */
35
35
  this.outputPlansByRootStep = new Map();
36
36
  /** @internal */
@@ -73,7 +73,13 @@ class StepTracker {
73
73
  addStep($step) {
74
74
  const stepId = this.stepCount++;
75
75
  this.activeSteps.add($step);
76
- this.stepsWithNoDependencies.add($step);
76
+ const ctor = $step.constructor;
77
+ let stepsWithNoDependencies = this.stepsWithNoDependenciesByConstructor.get(ctor);
78
+ if (!stepsWithNoDependencies) {
79
+ stepsWithNoDependencies ??= new Set();
80
+ this.stepsWithNoDependenciesByConstructor.set(ctor, stepsWithNoDependencies);
81
+ }
82
+ stepsWithNoDependencies.add($step);
77
83
  this.stepById[stepId] = $step;
78
84
  this.aliasesById[stepId] = undefined;
79
85
  this.addStepToItsLayerPlan($step);
@@ -253,7 +259,9 @@ class StepTracker {
253
259
  $dependent._isUnary = false;
254
260
  }
255
261
  const forbiddenFlags = interfaces_js_1.ALL_FLAGS & ~(acceptFlags & interfaces_js_1.TRAPPABLE_FLAGS);
256
- this.stepsWithNoDependencies.delete($dependent);
262
+ this.stepsWithNoDependenciesByConstructor
263
+ .get($dependent.constructor)
264
+ ?.delete($dependent);
257
265
  const dependencyIndex = dependentDependencies.push($dependency) - 1;
258
266
  dependentDependencyForbiddenFlags[dependencyIndex] = forbiddenFlags;
259
267
  dependentDependencyOnReject[dependencyIndex] = onReject;
@@ -538,7 +546,9 @@ class StepTracker {
538
546
  ]}`);
539
547
  }
540
548
  }
541
- this.stepsWithNoDependencies.delete($original);
549
+ this.stepsWithNoDependenciesByConstructor
550
+ .get($original.constructor)
551
+ ?.delete($original);
542
552
  this.outputPlansByRootStep.delete($original);
543
553
  this.layerPlansByRootStep.delete($original);
544
554
  this.layerPlansByParentStep.delete($original);
package/dist/envelop.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useMoreDetailedErrors = exports.useGrafast = void 0;
4
+ const graphile_config_1 = require("graphile-config");
4
5
  const execute_js_1 = require("./execute.js");
5
6
  const stripAnsi_js_1 = require("./stripAnsi.js");
6
7
  const subscribe_js_1 = require("./subscribe.js");
@@ -33,9 +34,9 @@ const useGrafast = (options = {}) => {
33
34
  async onExecute(opts) {
34
35
  const explainHeaders = opts.args.contextValue?.req?.headers["x-graphql-explain"];
35
36
  const explain = processExplain(explainAllowed, explainHeaders);
36
- opts.setExecuteFn((args) => (0, execute_js_1.execute)(args, {
37
+ opts.setExecuteFn((args) => (0, execute_js_1.execute)(args, (0, graphile_config_1.resolvePreset)({
37
38
  grafast: { explain },
38
- }));
39
+ })));
39
40
  },
40
41
  async onSubscribe(opts) {
41
42
  const ctx = opts.args.contextValue;
@@ -43,9 +44,9 @@ const useGrafast = (options = {}) => {
43
44
  ctx?.request?.headers ||
44
45
  ctx?.connectionParams)?.["x-graphql-explain"];
45
46
  const explain = processExplain(explainAllowed, explainHeaders);
46
- opts.setSubscribeFn(async (args) => (0, subscribe_js_1.subscribe)(args, {
47
+ opts.setSubscribeFn(async (args) => (0, subscribe_js_1.subscribe)(args, (0, graphile_config_1.resolvePreset)({
47
48
  grafast: { explain },
48
- }));
49
+ })));
49
50
  },
50
51
  };
51
52
  };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import "./thereCanBeOnlyOne.js";
2
2
  import type LRU from "@graphile/lru";
3
- import type { CallbackOrDescriptor, MiddlewareNext } from "graphile-config";
3
+ import type { MiddlewareHandlers } from "graphile-config";
4
4
  import type { DocumentNode, GraphQLError, OperationDefinitionNode } from "graphql";
5
5
  import type { DataFromObjectSteps } from "./steps/object.js";
6
6
  type PromiseOrValue<T> = T | Promise<T>;
@@ -160,9 +160,7 @@ declare global {
160
160
  }
161
161
  interface Plugin {
162
162
  grafast?: {
163
- middleware?: {
164
- [key in keyof GrafastMiddleware]?: CallbackOrDescriptor<GrafastMiddleware[key] extends (...args: infer UArgs) => infer UResult ? (next: MiddlewareNext<UResult>, ...args: UArgs) => UResult : never>;
165
- };
163
+ middleware?: MiddlewareHandlers<GrafastMiddleware>;
166
164
  };
167
165
  }
168
166
  }
package/dist/step.d.ts CHANGED
@@ -108,6 +108,18 @@ export declare class ExecutableStep<TData = any> extends BaseStep {
108
108
  * Like `metaKey` but for the optimize phase
109
109
  */
110
110
  optimizeMetaKey: number | string | symbol | undefined;
111
+ /**
112
+ * If the peerKey of two steps do not match, then they are definitely not
113
+ * peers. Use this to reduce the load on deduplicate by more quickly
114
+ * eradicating definitely-not-peers.
115
+ *
116
+ * Note: we may well change this to be a function in future, so it's advised
117
+ * that you don't use this unless you're working inside the graphile/crystal
118
+ * core codebase.
119
+ *
120
+ * @experimental
121
+ */
122
+ peerKey: string | null;
111
123
  /**
112
124
  * Set this true for plans that implement mutations; this will prevent them
113
125
  * from being tree-shaken.
@@ -125,6 +137,14 @@ export declare class ExecutableStep<TData = any> extends BaseStep {
125
137
  * @experimental
126
138
  */
127
139
  protected getDepDeep(depId: number): ExecutableStep;
140
+ /**
141
+ * Cache a generated step by a given identifier (cacheKey) such that we don't
142
+ * need to regenerate it on future calls, significantly reducing the load on
143
+ * deduplication later.
144
+ *
145
+ * @experimental
146
+ */
147
+ protected cacheStep<T extends ExecutableStep>(actionKey: string, cacheKey: symbol | string | number, cb: () => T): T;
128
148
  toString(): string;
129
149
  protected canAddDependency(step: ExecutableStep): boolean;
130
150
  protected addDependency(stepOrOptions: ExecutableStep | AddDependencyOptions): number;
package/dist/step.js CHANGED
@@ -107,6 +107,18 @@ class ExecutableStep extends BaseStep {
107
107
  * (default = ALL_FLAGS & ~FLAG_NULL)
108
108
  */
109
109
  this.defaultForbiddenFlags = interfaces_js_1.DEFAULT_FORBIDDEN_FLAGS;
110
+ /**
111
+ * If the peerKey of two steps do not match, then they are definitely not
112
+ * peers. Use this to reduce the load on deduplicate by more quickly
113
+ * eradicating definitely-not-peers.
114
+ *
115
+ * Note: we may well change this to be a function in future, so it's advised
116
+ * that you don't use this unless you're working inside the graphile/crystal
117
+ * core codebase.
118
+ *
119
+ * @experimental
120
+ */
121
+ this.peerKey = null;
110
122
  this.implicitSideEffectStep = null;
111
123
  this.hasSideEffects ??= false;
112
124
  let hasSideEffects = false;
@@ -144,6 +156,7 @@ class ExecutableStep extends BaseStep {
144
156
  hasSideEffects = value;
145
157
  if (value === true) {
146
158
  this.layerPlan.latestSideEffectStep = this;
159
+ this.operationPlan.resetCache();
147
160
  }
148
161
  else if (value !== true && hasSideEffects === true) {
149
162
  throw new Error(`Cannot mark ${this} as having no side effects after having set it to have side effects.`);
@@ -205,6 +218,16 @@ class ExecutableStep extends BaseStep {
205
218
  }
206
219
  return $dep;
207
220
  }
221
+ /**
222
+ * Cache a generated step by a given identifier (cacheKey) such that we don't
223
+ * need to regenerate it on future calls, significantly reducing the load on
224
+ * deduplication later.
225
+ *
226
+ * @experimental
227
+ */
228
+ cacheStep(actionKey, cacheKey, cb) {
229
+ return this.operationPlan.cacheStep(this, actionKey, cacheKey, cb);
230
+ }
208
231
  toString() {
209
232
  let meta;
210
233
  try {
@@ -37,10 +37,10 @@ class __ValueStep extends step_js_1.ExecutableStep {
37
37
  throw new Error(`GrafastInternalError<7696a514-f452-4d47-92d3-85aeb5b23f48>: ${this} is a __ValueStep and thus must never execute`);
38
38
  }
39
39
  get(attrName) {
40
- return (0, access_js_1.access)(this, [attrName]);
40
+ return this.cacheStep("get", attrName, () => (0, access_js_1.access)(this, [attrName]));
41
41
  }
42
42
  at(index) {
43
- return (0, access_js_1.access)(this, [index]);
43
+ return this.cacheStep("at", index, () => (0, access_js_1.access)(this, [index]));
44
44
  }
45
45
  }
46
46
  exports.__ValueStep = __ValueStep;
@@ -18,6 +18,7 @@ export declare class AccessStep<TData> extends UnbatchedExecutableStep<TData> {
18
18
  isSyncAndSafe: boolean;
19
19
  allowMultipleOptimizations: boolean;
20
20
  readonly path: (string | number | symbol)[];
21
+ private readonly hasSymbols;
21
22
  constructor(parentPlan: ExecutableStep<unknown>, path: (string | number | symbol)[], fallback?: any);
22
23
  toStringMeta(): string;
23
24
  /**
@@ -37,5 +38,5 @@ export declare class AccessStep<TData> extends UnbatchedExecutableStep<TData> {
37
38
  * Access the property at path `path` in the value returned from `parentPlan`,
38
39
  * falling back to `fallback` if it were null-ish.
39
40
  */
40
- export declare function access<TData>(parentPlan: ExecutableStep<unknown>, path?: (string | number | symbol)[] | string | number | symbol, fallback?: any): AccessStep<TData>;
41
+ export declare function access<TData>(parentPlan: ExecutableStep<unknown>, rawPath?: (string | number | symbol)[] | string | number | symbol, fallback?: any): AccessStep<TData>;
41
42
  //# sourceMappingURL=access.d.ts.map
@@ -3,10 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.access = exports.AccessStep = exports.expressionSymbol = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const chalk_1 = tslib_1.__importDefault(require("chalk"));
6
- const debug_1 = tslib_1.__importDefault(require("debug"));
7
6
  const tamedevil_1 = tslib_1.__importDefault(require("tamedevil"));
8
7
  const inspect_js_1 = require("../inspect.js");
9
8
  const step_js_1 = require("../step.js");
9
+ const utils_js_1 = require("../utils.js");
10
10
  /** @internal */
11
11
  exports.expressionSymbol = Symbol("expression");
12
12
  const makeDestructureCache = Object.create(null);
@@ -100,8 +100,6 @@ return (_meta, value) => value?.${tamedevil_1.default.join(access, "?.")}${fallb
100
100
  });
101
101
  }
102
102
  }
103
- const debugAccessPlan = (0, debug_1.default)("grafast:AccessStep");
104
- const debugAccessPlanVerbose = debugAccessPlan.extend("verbose");
105
103
  /**
106
104
  * Accesses a (potentially nested) property from the result of a plan.
107
105
  *
@@ -121,6 +119,11 @@ class AccessStep extends step_js_1.UnbatchedExecutableStep {
121
119
  this.isSyncAndSafe = true;
122
120
  this.allowMultipleOptimizations = true;
123
121
  this.path = path;
122
+ this.hasSymbols = this.path.some((k) => typeof k === "symbol");
123
+ this.peerKey =
124
+ (this.fallback === "undefined" ? "U" : "D") +
125
+ (this.hasSymbols ? "§" : ".") +
126
+ (0, utils_js_1.digestKeys)(this.path);
124
127
  this.addDependency(parentPlan);
125
128
  }
126
129
  toStringMeta() {
@@ -135,7 +138,7 @@ class AccessStep extends step_js_1.UnbatchedExecutableStep {
135
138
  if (typeof attrName !== "string") {
136
139
  throw new Error(`AccessStep::get can only be called with string values`);
137
140
  }
138
- return new AccessStep(this.getDep(0), [...this.path, attrName]);
141
+ return access(this.getDep(0), [...this.path, attrName]);
139
142
  }
140
143
  /**
141
144
  * Get the entry at the given index in an array.
@@ -144,7 +147,7 @@ class AccessStep extends step_js_1.UnbatchedExecutableStep {
144
147
  if (typeof index !== "number") {
145
148
  throw new Error(`AccessStep::get can only be called with string values`);
146
149
  }
147
- return new AccessStep(this.getDep(0), [...this.path, index]);
150
+ return access(this.getDep(0), [...this.path, index]);
148
151
  }
149
152
  // An access of an access can become a single access
150
153
  optimize() {
@@ -165,10 +168,23 @@ class AccessStep extends step_js_1.UnbatchedExecutableStep {
165
168
  throw new Error(`${this}: should have had unbatchedExecute method replaced`);
166
169
  }
167
170
  deduplicate(peers) {
168
- const myPath = JSON.stringify(this.path);
169
- const peersWithSamePath = peers.filter((p) => p.fallback === this.fallback && JSON.stringify(p.path) === myPath);
170
- debugAccessPlanVerbose("%c deduplicate: peers with same path %o = %c", this, this.path, peersWithSamePath);
171
- return peersWithSamePath;
171
+ if (peers.length === 0) {
172
+ return peers;
173
+ }
174
+ else if (!this.hasSymbols && this.fallback === undefined) {
175
+ // Rely entirely on peerKey
176
+ return peers;
177
+ }
178
+ else if (!this.hasSymbols) {
179
+ // Rely on peerKey for path, but check fallback
180
+ const { fallback } = this;
181
+ return peers.filter((p) => p.fallback === fallback);
182
+ }
183
+ else {
184
+ // Check both fallback and path
185
+ const { fallback, path } = this;
186
+ return peers.filter((p) => p.fallback === fallback && (0, utils_js_1.arraysMatch)(p.path, path));
187
+ }
172
188
  }
173
189
  }
174
190
  exports.AccessStep = AccessStep;
@@ -176,8 +192,18 @@ exports.AccessStep = AccessStep;
176
192
  * Access the property at path `path` in the value returned from `parentPlan`,
177
193
  * falling back to `fallback` if it were null-ish.
178
194
  */
179
- function access(parentPlan, path, fallback) {
180
- return new AccessStep(parentPlan, Array.isArray(path) ? path : path != null ? [path] : [], fallback);
195
+ function access(parentPlan, rawPath, fallback) {
196
+ const path = Array.isArray(rawPath)
197
+ ? rawPath
198
+ : rawPath != null
199
+ ? [rawPath]
200
+ : [];
201
+ if (typeof fallback === "undefined" &&
202
+ !path.some((k) => typeof k === "symbol")) {
203
+ const pathKey = (0, utils_js_1.digestKeys)(path);
204
+ return parentPlan.operationPlan.cacheStep(parentPlan, "GrafastInternal:access()", pathKey, () => new AccessStep(parentPlan, path));
205
+ }
206
+ return new AccessStep(parentPlan, path, fallback);
181
207
  }
182
208
  exports.access = access;
183
209
  //# sourceMappingURL=access.js.map
@@ -160,7 +160,7 @@ exports.ApplyTransformsStep = ApplyTransformsStep;
160
160
  */
161
161
  function applyTransforms($step) {
162
162
  if ((0, step_js_1.isListCapableStep)($step)) {
163
- return new ApplyTransformsStep($step);
163
+ return $step.operationPlan.cacheStep($step, "GrafastInternal:applyTransforms()", "", () => new ApplyTransformsStep($step));
164
164
  }
165
165
  else {
166
166
  // No eval necessary
@@ -17,6 +17,13 @@ class ConstantStep extends step_js_1.UnbatchedExecutableStep {
17
17
  this.data = data;
18
18
  this.isSensitive = isSensitive;
19
19
  this.isSyncAndSafe = true;
20
+ const t = typeof data;
21
+ if (data == null ||
22
+ t === "boolean" ||
23
+ t === "number" ||
24
+ (t === "string" && t.length < 200)) {
25
+ this.peerKey = t + "|" + String(data);
26
+ }
20
27
  }
21
28
  toStringMeta() {
22
29
  // ENHANCE: use nicer simplification
@@ -38,7 +38,7 @@ exports.FirstStep = FirstStep;
38
38
  * plan.
39
39
  */
40
40
  function first(plan) {
41
- return new FirstStep(plan);
41
+ return plan.operationPlan.cacheStep(plan, "GrafastInternal:first()", "", () => new FirstStep(plan));
42
42
  }
43
43
  exports.first = first;
44
44
  //# sourceMappingURL=first.js.map
@@ -35,7 +35,7 @@ exports.LastStep = LastStep;
35
35
  * plan.
36
36
  */
37
37
  function last(plan) {
38
- return new LastStep(plan);
38
+ return plan.operationPlan.cacheStep(plan, "GrafastInternal:last()", "", () => new LastStep(plan));
39
39
  }
40
40
  exports.last = last;
41
41
  //# sourceMappingURL=last.js.map
@@ -40,6 +40,7 @@ export declare class LoadedRecordStep<TItem, TParams extends Record<string, any>
40
40
  constructor($data: ExecutableStep<TItem>, isSingle: boolean, sourceDescription: string, ioEquivalence: Record<string, ExecutableStep>);
41
41
  toStringMeta(): string;
42
42
  get(attr: keyof TItem & (string | number)): ExecutableStep<any>;
43
+ private _getInner;
43
44
  setParam<TParamKey extends keyof TParams>(paramKey: TParamKey, value: TParams[TParamKey]): void;
44
45
  optimize(): ExecutableStep<any>;
45
46
  execute({ count, values: [values0], }: ExecutionDetails<[TItem]>): GrafastResultsList<TItem>;
@@ -48,6 +48,9 @@ class LoadedRecordStep extends step_js_1.ExecutableStep {
48
48
  return this.sourceDescription ?? null;
49
49
  }
50
50
  get(attr) {
51
+ return this.cacheStep("get", attr, () => this._getInner(attr));
52
+ }
53
+ _getInner(attr) {
51
54
  // Allow auto-collapsing of the waterfall by knowing keys are equivalent
52
55
  if (this.operationPlan.phase === "plan" &&
53
56
  this.ioEquivalence[attr]) {
@@ -32,16 +32,18 @@ export declare class ObjectStep<TPlans extends {
32
32
  } = {
33
33
  [key: string]: ExecutableStep;
34
34
  }> extends UnbatchedExecutableStep<DataFromObjectSteps<TPlans>> implements SetterCapableStep<TPlans> {
35
+ private cacheConfig?;
35
36
  static $$export: {
36
37
  moduleName: string;
37
38
  exportName: string;
38
39
  };
39
40
  isSyncAndSafe: boolean;
40
41
  allowMultipleOptimizations: boolean;
41
- private keys;
42
+ private readonly keys;
42
43
  optimizeMetaKey: string;
43
44
  private cacheSize;
44
- constructor(obj: TPlans, cacheConfig?: ObjectStepCacheConfig);
45
+ constructor(obj: TPlans, cacheConfig?: ObjectStepCacheConfig | undefined);
46
+ private _setKeys;
45
47
  /**
46
48
  * This key doesn't get typed, but it can be added later which can be quite
47
49
  * handy.
@@ -5,6 +5,7 @@ exports.object = exports.ObjectStep = void 0;
5
5
  const tslib_1 = require("tslib");
6
6
  const tamedevil_1 = tslib_1.__importStar(require("tamedevil"));
7
7
  const step_js_1 = require("../step.js");
8
+ const utils_js_1 = require("../utils.js");
8
9
  const constant_js_1 = require("./constant.js");
9
10
  const DEFAULT_CACHE_SIZE = 100;
10
11
  const EMPTY_OBJECT = Object.freeze(Object.create(null));
@@ -19,30 +20,37 @@ class ObjectStep extends step_js_1.UnbatchedExecutableStep {
19
20
  }; }
20
21
  constructor(obj, cacheConfig) {
21
22
  super();
23
+ this.cacheConfig = cacheConfig;
22
24
  this.isSyncAndSafe = true;
23
25
  this.allowMultipleOptimizations = true;
26
+ this.keys = [];
24
27
  // Optimize needs the same 'meta' for all ObjectSteps
25
28
  this.optimizeMetaKey = "ObjectStep";
26
29
  this.cacheSize =
27
30
  cacheConfig?.cacheSize ??
28
31
  (cacheConfig?.identifier ? DEFAULT_CACHE_SIZE : 0);
32
+ const keys = Object.keys(obj);
33
+ this._setKeys(keys);
34
+ for (let i = 0, l = this.keys.length; i < l; i++) {
35
+ this.addDependency({ step: obj[keys[i]], skipDeduplication: true });
36
+ }
37
+ }
38
+ _setKeys(keys) {
39
+ this.keys = keys;
40
+ this.peerKey = (0, utils_js_1.digestKeys)(keys);
29
41
  this.metaKey =
30
42
  this.cacheSize <= 0
31
43
  ? undefined
32
- : cacheConfig?.identifier
33
- ? `object|${JSON.stringify(Object.keys(obj))}|${cacheConfig.identifier}`
44
+ : this.cacheConfig?.identifier
45
+ ? `object|${this.peerKey}|${this.cacheConfig.identifier}`
34
46
  : this.id;
35
- this.keys = Object.keys(obj);
36
- for (let i = 0, l = this.keys.length; i < l; i++) {
37
- this.addDependency({ step: obj[this.keys[i]], skipDeduplication: true });
38
- }
39
47
  }
40
48
  /**
41
49
  * This key doesn't get typed, but it can be added later which can be quite
42
50
  * handy.
43
51
  */
44
52
  set(key, plan) {
45
- this.keys.push(key);
53
+ this._setKeys([...this.keys, key]);
46
54
  this.addDependency({ step: plan, skipDeduplication: true });
47
55
  }
48
56
  getStepForKey(key, allowMissing = false) {
@@ -154,8 +162,8 @@ ${inner}
154
162
  throw new Error(`${this} didn't finalize? No unbatchedExecute method.`);
155
163
  }
156
164
  deduplicate(peers) {
157
- const myKeys = JSON.stringify(this.keys);
158
- return peers.filter((p) => JSON.stringify(p.keys) === myKeys);
165
+ // Managed through peerKey
166
+ return peers;
159
167
  }
160
168
  optimize(opts) {
161
169
  if (this.dependencies.every((dep) => dep instanceof constant_js_1.ConstantStep)) {
@@ -9,7 +9,7 @@ export type ActualKeyByDesiredKey = {
9
9
  * `actualKey` from the input and storing it as the `desiredKey` in the output.
10
10
  */
11
11
  export declare class RemapKeysStep extends UnbatchedExecutableStep {
12
- private actualKeyByDesiredKey;
12
+ private readonly actualKeyByDesiredKey;
13
13
  static $$export: {
14
14
  moduleName: string;
15
15
  exportName: string;
@@ -6,6 +6,7 @@ const tslib_1 = require("tslib");
6
6
  const chalk_1 = tslib_1.__importDefault(require("chalk"));
7
7
  const tamedevil_1 = tslib_1.__importStar(require("tamedevil"));
8
8
  const step_js_1 = require("../step.js");
9
+ const utils_js_1 = require("../utils.js");
9
10
  function makeMapper(actualKeyByDesiredKey, callback) {
10
11
  const entries = Object.entries(actualKeyByDesiredKey);
11
12
  if (entries.every(([key, val]) => (0, tamedevil_1.isSafeObjectPropertyName)(key) && (0, tamedevil_1.isSafeObjectPropertyName)(val))) {
@@ -40,6 +41,10 @@ class RemapKeysStep extends step_js_1.UnbatchedExecutableStep {
40
41
  this.isSyncAndSafe = true;
41
42
  this.allowMultipleOptimizations = true;
42
43
  this.addDependency($plan);
44
+ this.peerKey = (0, utils_js_1.digestKeys)([
45
+ ...Object.keys(this.actualKeyByDesiredKey),
46
+ ...Object.values(this.actualKeyByDesiredKey),
47
+ ]);
43
48
  }
44
49
  toStringMeta() {
45
50
  return (chalk_1.default.bold.yellow(String(this.dependencies[0].id)) +
@@ -70,8 +75,8 @@ class RemapKeysStep extends step_js_1.UnbatchedExecutableStep {
70
75
  return this.mapper(value);
71
76
  }
72
77
  deduplicate(peers) {
73
- const myMap = JSON.stringify(this.actualKeyByDesiredKey);
74
- return peers.filter((p) => JSON.stringify(p.actualKeyByDesiredKey) === myMap);
78
+ // Handled by peerKey
79
+ return peers;
75
80
  }
76
81
  }
77
82
  exports.RemapKeysStep = RemapKeysStep;
@@ -49,7 +49,7 @@ exports.ReverseStep = ReverseStep;
49
49
  * Reverses a list.
50
50
  */
51
51
  function reverse(plan) {
52
- return new ReverseStep(plan);
52
+ return plan.operationPlan.cacheStep(plan, "GrafastInternal:reverse()", "", () => new ReverseStep(plan));
53
53
  }
54
54
  exports.reverse = reverse;
55
55
  //# sourceMappingURL=reverse.js.map
package/dist/utils.d.ts CHANGED
@@ -151,5 +151,11 @@ export declare function hasItemPlan(step: ExecutableStep & {
151
151
  };
152
152
  export declare function exportNameHint(obj: any, nameHint: string): void;
153
153
  export declare function isTuple<T extends readonly [...(readonly any[])]>(t: any | T): t is T;
154
+ /**
155
+ * Turns an array of keys into a digest, avoiding conflicts.
156
+ * Symbols are treated as equivalent. (Theoretically faster
157
+ * than JSON.stringify().)
158
+ */
159
+ export declare function digestKeys(keys: ReadonlyArray<string | number | symbol>): string;
154
160
  export {};
155
161
  //# sourceMappingURL=utils.d.ts.map
package/dist/utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isTuple = exports.exportNameHint = exports.hasItemPlan = exports.assertNotPromise = exports.assertNotAsync = exports.canonicalJSONStringify = exports.stepsAreInSamePhase = exports.stepAMayDependOnStepB = exports.stepADependsOnStepB = exports.writeableArray = exports.sudo = exports.isTypePlanned = exports.findVariableNamesUsed = exports.arrayOfLengthCb = exports.arrayOfLength = exports.stack = exports.sharedNull = exports.getEnumValueConfig = exports.inputObjectFieldSpec = exports.newInputObjectTypeBuilder = exports.newGrafastFieldConfigBuilder = exports.objectFieldSpec = exports.newObjectTypeBuilder = exports.objectSpec = exports.arraysMatch = exports.isDeferred = exports.isPromiseLike = exports.isPromise = exports.defaultValueToValueNode = exports.assertNullPrototype = exports.ROOT_VALUE_OBJECT = void 0;
3
+ exports.digestKeys = exports.isTuple = exports.exportNameHint = exports.hasItemPlan = exports.assertNotPromise = exports.assertNotAsync = exports.canonicalJSONStringify = exports.stepsAreInSamePhase = exports.stepAMayDependOnStepB = exports.stepADependsOnStepB = exports.writeableArray = exports.sudo = exports.isTypePlanned = exports.findVariableNamesUsed = exports.arrayOfLengthCb = exports.arrayOfLength = exports.stack = exports.sharedNull = exports.getEnumValueConfig = exports.inputObjectFieldSpec = exports.newInputObjectTypeBuilder = exports.newGrafastFieldConfigBuilder = exports.objectFieldSpec = exports.newObjectTypeBuilder = exports.objectSpec = exports.arraysMatch = exports.isDeferred = exports.isPromiseLike = exports.isPromise = exports.defaultValueToValueNode = exports.assertNullPrototype = exports.ROOT_VALUE_OBJECT = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const graphql = tslib_1.__importStar(require("graphql"));
6
6
  const assert = tslib_1.__importStar(require("./assert.js"));
@@ -756,4 +756,27 @@ function isTuple(t) {
756
756
  return Array.isArray(t);
757
757
  }
758
758
  exports.isTuple = isTuple;
759
+ /**
760
+ * Turns an array of keys into a digest, avoiding conflicts.
761
+ * Symbols are treated as equivalent. (Theoretically faster
762
+ * than JSON.stringify().)
763
+ */
764
+ function digestKeys(keys) {
765
+ let str = "";
766
+ for (let i = 0, l = keys.length; i < l; i++) {
767
+ const item = keys[i];
768
+ if (typeof item === "string") {
769
+ // str += `|§${item.replace(/§/g, "§§")}§`;
770
+ str += `§${item.length}:${item}`;
771
+ }
772
+ else if (typeof item === "number") {
773
+ str += `N${item}`;
774
+ }
775
+ else {
776
+ str += "!";
777
+ }
778
+ }
779
+ return str;
780
+ }
781
+ exports.digestKeys = digestKeys;
759
782
  //# sourceMappingURL=utils.js.map
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const version = "0.1.1-beta.16";
1
+ export declare const version = "0.1.1-beta.18";
2
2
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -2,5 +2,5 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.version = void 0;
4
4
  // This file is autogenerated by /scripts/postversion.mjs
5
- exports.version = "0.1.1-beta.16";
5
+ exports.version = "0.1.1-beta.18";
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grafast",
3
- "version": "0.1.1-beta.16",
3
+ "version": "0.1.1-beta.18",
4
4
  "description": "Cutting edge GraphQL planning and execution engine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -59,7 +59,7 @@
59
59
  "chalk": "^4.1.2",
60
60
  "debug": "^4.3.4",
61
61
  "eventemitter3": "^5.0.1",
62
- "graphile-config": "^0.0.1-beta.11",
62
+ "graphile-config": "^0.0.1-beta.13",
63
63
  "graphql": "^16.1.0-experimental-stream-defer.6",
64
64
  "iterall": "^1.3.0",
65
65
  "tamedevil": "^0.0.0-beta.7",