@@ -232,22 +232,131 @@ export class GraphPath<TTrigger, RV extends Vertex = Vertex, TNullEdge extends n
232
232
return this . props . edgeIndexes . length ;
233
233
}
234
234
235
- subgraphJumps ( ) : number {
235
+ /**
236
+ * That method first look for the biggest common prefix to `this` and `that` (assuming that both path are build as choices
237
+ * of the same "query path"), and the count how many subgraph jumps each of the path has after said prefix.
238
+ *
239
+ * Note that this method always return someting but the biggest common prefix considered might well be empty.
240
+ *
241
+ * Please note that this method assumes that the 2 paths have the same root, and will fail if that's not the case.
242
+ */
243
+ countSubgraphJumpsAfterLastCommonVertex ( that : GraphPath < TTrigger , RV , TNullEdge > ) : {
244
+ thisJumps : number ,
245
+ thatJumps : number
246
+ } {
247
+ const { vertex, index } = this . findLastCommonVertex ( that ) ;
248
+ return {
249
+ thisJumps : this . subgraphJumpsAtIdx ( vertex , index ) ,
250
+ thatJumps : that . subgraphJumpsAtIdx ( vertex , index ) ,
251
+ } ;
252
+ }
253
+
254
+ private findLastCommonVertex ( that : GraphPath < TTrigger , RV , TNullEdge > ) : { vertex : Vertex , index : number } {
255
+ let vertex : Vertex = this . root ;
256
+ assert ( that . root === vertex , ( ) => `Expected both path to start on the same root, but 'this' has root ${ vertex } while 'that' has ${ that . root } ` ) ;
257
+
258
+ const minSize = Math . min ( this . size , that . size ) ;
259
+ let index = 0 ;
260
+ for ( ; index < minSize ; index ++ ) {
261
+ const thisEdge = this . edgeAt ( index , vertex ) ;
262
+ const thatEdge = that . edgeAt ( index , vertex ) ;
263
+ if ( thisEdge !== thatEdge ) {
264
+ break ;
265
+ }
266
+ if ( thisEdge ) {
267
+ vertex = thisEdge . tail ;
268
+ }
269
+ }
270
+ return { vertex, index} ;
271
+ }
272
+
273
+ private subgraphJumpsAtIdx ( vertex : Vertex , index : number ) : number {
236
274
let jumps = 0 ;
237
- let v : Vertex = this . root ;
238
- for ( let i = 0 ; i < this . size ; i ++ ) {
275
+ let v : Vertex = vertex ;
276
+ for ( let i = index ; i < this . size ; i ++ ) {
239
277
const edge = this . edgeAt ( i , v ) ;
240
278
if ( ! edge ) {
241
279
continue ;
242
280
}
243
- if ( edge . transition . kind === 'KeyResolution' || edge . transition . kind === 'RootTypeResolution' ) {
281
+ if ( edge . changesSubgraph ( ) ) {
244
282
++ jumps ;
245
283
}
246
284
v = edge . tail ;
247
285
}
248
286
return jumps ;
249
287
}
250
288
289
+ subgraphJumps ( ) : number {
290
+ return this . subgraphJumpsAtIdx ( this . root , 0 ) ;
291
+ }
292
+
293
+ isEquivalentSaveForTypeExplosionTo ( that : GraphPath < TTrigger , RV , TNullEdge > ) : boolean {
294
+ // We're looking a the specific case were both path are basically equivalent except
295
+ // for a single step of type-explosion, so if either the paths don't start and end in the
296
+ // same vertex, or if `other` is not exactly 1 more step than `this`, we're done.
297
+ if ( this . root !== that . root || this . tail !== that . tail || this . size !== that . size - 1 ) {
298
+ return false ;
299
+ }
300
+
301
+ // If that's true, then we get to our comparison.
302
+ let thisV : Vertex = this . root ;
303
+ let thatV : Vertex = that . root ;
304
+ for ( let i = 0 ; i < this . size ; i ++ ) {
305
+ let thisEdge = this . edgeAt ( i , thisV ) ;
306
+ let thatEdge = that . edgeAt ( i , thatV ) ;
307
+ if ( thisEdge !== thatEdge ) {
308
+ // First difference. If it's not a "type-explosion", that is `that` is a cast from an
309
+ // interface to one of the implementation, then we're not in the case we're looking for.
310
+ if ( ! thisEdge || ! thatEdge || ! isInterfaceType ( thatV . type ) || thatEdge . transition . kind !== 'DownCast' ) {
311
+ return false ;
312
+ }
313
+ thatEdge = that . edgeAt ( i + 1 , thatEdge . tail ) ;
314
+ if ( ! thatEdge ) {
315
+ return false ;
316
+ }
317
+ thisV = thisEdge . tail ;
318
+ thatV = thatEdge . tail ;
319
+
320
+ // At that point, we want both path to take the "same" key, but because one is starting
321
+ // from the interface while the other one from an implementation, they won't be technically
322
+ // the "same" edge object. So we check that both are key, to the same subgraph and type,
323
+ // and with the same condition.
324
+ if ( thisEdge . transition . kind !== 'KeyResolution'
325
+ || thatEdge . transition . kind !== 'KeyResolution'
326
+ || thisEdge . tail . source !== thatEdge . tail . source
327
+ || thisV !== thatV
328
+ || ! thisEdge . conditions ! . equals ( thatEdge . conditions ! )
329
+ ) {
330
+ return false ;
331
+ }
332
+
333
+ // So far, so good. `thisV` and `thatV` are positioned on the vertex after which the path
334
+ // must be equal again. So check that it's true, and if it is, we're good.
335
+ // Note that for `this`, the last edge we looked at was `i`, so the next is `i+1`. And
336
+ // for `that`, we've skipped over one more edge, so need to use `j+1`.
337
+ for ( let j = i + 1 ; j < this . size ; j ++ ) {
338
+ thisEdge = this . edgeAt ( j , thisV ) ;
339
+ thatEdge = that . edgeAt ( j + 1 , thatV ) ;
340
+ if ( thisEdge !== thatEdge ) {
341
+ return false ;
342
+ }
343
+ if ( thisEdge ) {
344
+ thisV = thisEdge . tail ;
345
+ thatV = thatEdge ! . tail ;
346
+ }
347
+ }
348
+ return true ;
349
+ }
350
+ if ( thisEdge ) {
351
+ thisV = thisEdge . tail ;
352
+ thatV = thatEdge ! . tail ;
353
+ }
354
+ }
355
+ // If we get here, both path are actually exactly the same. So technically there is not additional
356
+ // type explosion, but they are equivalent and we can return `true`.
357
+ return true ;
358
+ }
359
+
251
360
[ Symbol . iterator ] ( ) : PathIterator < TTrigger , TNullEdge > {
252
361
const path = this ;
253
362
return {
@@ -1814,6 +1923,35 @@ export function advanceSimultaneousPathsWithOperation<V extends Vertex>(
1814
1923
// is unsatisfiable. But as we've only taken type preserving transitions, we cannot get an empty results at this point if we haven't
1815
1924
// had one when testing direct transitions above (in which case we have exited the method early).
1816
1925
assert ( pathWithOperation . length > 0 , ( ) => `Unexpected empty options after non-collecting path ${ pathWithNonCollecting } for ${ operation } ` ) ;
1926
+
1927
+ // There is a special case we can deal with now. Namely, suppose we have a case where a query
1928
+ // is reaching an interface I in a subgraph S1, we query some field of that interface x, and
1929
+ // say that x is provided in subgraph S2 but by an @interfaceObject for I.
1930
+ // As we look for direct options for I.x in S1 initially, as we didn't found `x`, we will have tried
1931
+ // to type-explode I (in say implementation A and B). And in some case doing so is necessary, but
1932
+ // it may also lead for the type-exploding option to look like:
1933
+ // [
1934
+ // I(S1) -[... on A]-> A(S1) -[key]-> I(S2) -[x] -> Int(S2),
1935
+ // I(S1) -[... on B]-> B(S1) -[key]-> I(S2) -[x] -> Int(S2),
1936
+ // ]
1937
+ // But as we look at indirect option now (still from I in S1), we will note that we can also
1938
+ // do:
1939
+ // I(S1) -[key]-> I(S2) -[x] -> Int(S2),
1940
+ // And while both options are technically valid, the new one really subsume the first one: there
1941
+ // is no point in type-exploding to take a key to the same exact subgraph if using the key
1942
+ // on the interface directly works.
1943
+ //
1944
+ // So here, we look for that case and remove any type-exploding option that the new path
1945
+ // render unecessary.
1946
+ // Do note that we only make that check when the new option is a single-path option, because
1947
+ // this gets kind of complicated otherwise.
1948
+ if ( pathWithNonCollecting . tailIsInterfaceObject ( ) ) {
1949
+ for ( const indirectOption of pathWithOperation ) {
1950
+ if ( indirectOption . length === 1 ) {
1951
+ options = options . filter ( ( opt ) => ! opt . every ( ( p ) => indirectOption [ 0 ] . isEquivalentSaveForTypeExplosionTo ( p ) ) ) ;
1952
+ }
1953
+ }
1954
+ }
1817
1955
options = options . concat ( pathWithOperation ) ;
1818
1956
}
1819
1957
debug . groupEnd ( ) ;
@@ -1853,6 +1991,7 @@ export function advanceSimultaneousPathsWithOperation<V extends Vertex>(
1853
1991
return createLazyOptions ( allOptions , subgraphSimultaneousPaths , updatedContext ) ;
1854
1992
}
1855
1993
1994
+
1856
1995
export function createInitialOptions < V extends Vertex > (
1857
1996
initialPath : OpGraphPath < V > ,
1858
1997
initialContext : PathContext ,
0 commit comments