maraisr / meros

🪢 A fast utility that makes reading multipart responses simple
MIT License
184 stars 9 forks source link

[feature request] Nested multiparts #15

Closed artola closed 2 years ago

artola commented 2 years ago

As properly stated in the README (caveats) the are some known limitations, from my point of view, Nested multiparts is the more important, as we use to compose components, soon we might get caught in this trap.

@maraisr Do you have something in mind on this topic?

maraisr commented 2 years ago

Hi @artola 👋🏻 and yeah excellent question.

I guess the use-case has never been there, so not quite sure how to effectively design an api around that. But perhaps you can give me a tangible example of how the data is produced, and intended consumption?

Seeing as meros is intended for the client side (whether that is server or browser), this has never had much of a use-case? Most if not all of the time, the response is tied to a single request, where messages arrive (in any order) over time for that request, and probably json.

To now nest that such that half way down you can instill a secondary boundary and flush messages against that boundary, I personally never seen how it has any benefit than to just encode the asynchronicity in a property of the message itself.

I dare say the benefit could be where you're using 1 transport to encode multiple unique payloads. Which can most certainly be encoded in layer 6, layer 7. For example graphql defer/stream does that through a label property in the json message.

You mentioned "compose components"; so guessing youre talking some frontend javascript component?

artola commented 2 years ago

Yeah, front-end Relay. For example, I have a Dashboard for crypto currencies, something like "show me a ticker with some currencies, plus top gainers and losers". The ticker renders first, while the rest can be deferred. As the development evolves, more fragments/components are spread, as they should be kinda blackbox I should not either care about if someone has already a defer in place.

query Dashboard {
  ...DashboardTickerFragment
  ...DashboardHighlightsFragment
}

fragment DashboardTickerFragment on Query {
  ticker: assets(first: 3, order: { tradableMarketCapRank: ASC }) {
    nodes {
      symbol
      ...DashboardTickerItemFragment
    }
  }
}

fragment DashboardTickerItemFragment on Asset {
  symbol
  color
  price {
    currency
    lastPrice
  }
}

fragment DashboardHighlightsFragment on Query {
  gainers: assets(
    first: 3
    where: { change24Hour: { gt: 0 } }
    order: { change24Hour: DESC }
  ) {
    ...DashboardCardFragment @defer
  }
  losers: assets(
    first: 3
    where: { change24Hour: { lt: 0 } }
    order: { change24Hour: ASC }
  ) {
    ...DashboardCardFragment @defer
  }
}

fragment DashboardCardFragment on AssetsConnection {
  nodes {
    id
    symbol
    name
    price {
      currency
      lastPrice
    }
  }
}

Here I have NO nested defer. The server's response:

---
Content-Type: application/json; charset=utf-8

{"data":{"ticker":{"nodes":[{"symbol":"BTC","color":"#F7931A","price":{"currency":"USD","lastPrice":36855.68}},{"symbol":"ADA","color":"#0033AD","price":{"currency":"USD","lastPrice":1.04}},{"symbol":"MATIC","color":"#8247E5","price":{"currency":"USD","lastPrice":1.54}}]},"gainers":{},"losers":{}},"hasNext":true}
---
Content-Type: application/json; charset=utf-8

{"path":["gainers"],"data":{"nodes":[{"id":"QXNzZXQKaTM5","symbol":"REQ","name":"Request","price":{"currency":"USD","lastPrice":0.22}},{"id":"QXNzZXQKaTMz","symbol":"VGX","name":"Voyager Token","price":{"currency":"USD","lastPrice":1.79}},{"id":"QXNzZXQKaTQ0","symbol":"ALCX","name":"Alchemix","price":{"currency":"USD","lastPrice":158.61}}]},"hasNext":true}
---
Content-Type: application/json; charset=utf-8

{"path":["losers"],"data":{"nodes":[{"id":"QXNzZXQKaTM3","symbol":"IMX","name":"Immutable X","price":{"currency":"USD","lastPrice":3.2}},{"id":"QXNzZXQKaTU0","symbol":"FORTH","name":"Ampleforth Governance Token","price":{"currency":"USD","lastPrice":7.45}},{"id":"QXNzZXQKaTUx","symbol":"MPL","name":"Maple","price":{"currency":"USD","lastPrice":15.04}}]},"hasNext":false}
-----

This case works OK, other combinations are possible as long as there are no nested defer.

Now imagine that I change the query to "defer" the fragment DashboardHighlightsFragment too, in such case HC resolves without problem, but I have no access to the data while Relay prints an error. This is just an example, same situation happens if we have nested defer in any other fragment spread.

      query DashboardContainerQuery {
        ...DashboardTickerFragment
        ...DashboardHighlightsFragment @defer(label: "highlights")
      }
"Invariant Violation: OperationExecutor: invalid incremental payload, expected `path` and `label` to either both be null/undefined, or `path` to be an `Array<string | number>` and `label` to be a `string`.
    at invariant (webpack-internal:///./node_modules/invariant/browser.js:38:15)
    at eval (webpack-internal:///./node_modules/relay-runtime/lib/store/OperationExecutor.js:1464:25)
    at Array.forEach (<anonymous>)
    at partitionGraphQLResponses (webpack-internal:///./node_modules/relay-runtime/lib/store/OperationExecutor.js:1458:13)
    at Executor._handleNext (webpack-internal:///./node_modules/relay-runtime/lib/store/OperationExecutor.js:470:33)
    at eval (webpack-internal:///./node_modules/relay-runtime/lib/store/OperationExecutor.js:343:16)
    at withDuration (webpack-internal:///./node_modules/relay-runtime/lib/util/withDuration.js:27:16)
    at eval (webpack-internal:///./node_modules/relay-runtime/lib/store/OperationExecutor.js:342:28)
    at Executor._schedule (webpack-internal:///./node_modules/relay-runtime/lib/store/OperationExecutor.js:300:7)
    at Executor._next (webpack-internal:///./node_modules/relay-runtime/lib/store/OperationExecutor.js:341:10)
    at Object.next (webpack-internal:///./node_modules/relay-runtime/lib/store/OperationExecutor.js:157:17)
    at Object.next (webpack-internal:///./node_modules/relay-runtime/lib/network/RelayObservable.js:535:20)
    at Object.eval [as next] (webpack-internal:///./node_modules/relay-runtime/lib/network/RelayObservable.js:190:40)
    at Object.next (webpack-internal:///./node_modules/relay-runtime/lib/network/RelayObservable.js:535:20)
    at _callee$ (webpack-internal:///./client/index.js:381:30)
    at tryCatch (webpack-internal:///./node_modules/next/dist/compiled/regenerator-runtime/runtime.js:45:40)
    at Generator.invoke [as _invoke] (webpack-internal:///./node_modules/next/dist/compiled/regenerator-runtime/runtime.js:274:22)
    at Generator.prototype.<computed> [as next] (webpack-internal:///./node_modules/next/dist/compiled/regenerator-runtime/runtime.js:97:21)
    at asyncGeneratorStep (webpack-internal:///./client/index.js:51:28)
---
Content-Type: application/json; charset=utf-8

{"data":{"ticker":{"nodes":[{"symbol":"BTC","color":"#F7931A","price":{"currency":"USD","lastPrice":37078.15}},{"symbol":"ADA","color":"#0033AD","price":{"currency":"USD","lastPrice":1.0419}},{"symbol":"MATIC","color":"#8247E5","price":{"currency":"USD","lastPrice":1.5489}}]}},"hasNext":true}
---
Content-Type: application/json; charset=utf-8

{"data":{"gainers":{},"losers":{}},"hasNext":true}
---
Content-Type: application/json; charset=utf-8

{"path":["gainers"],"data":{"nodes":[{"id":"QXNzZXQKaTM5","symbol":"REQ","name":"Request","price":{"currency":"USD","lastPrice":0.22}},{"id":"QXNzZXQKaTMz","symbol":"VGX","name":"Voyager Token","price":{"currency":"USD","lastPrice":1.77}},{"id":"QXNzZXQKaTQ0","symbol":"ALCX","name":"Alchemix","price":{"currency":"USD","lastPrice":157.19}}]},"hasNext":true}
---
Content-Type: application/json; charset=utf-8

{"path":["losers"],"data":{"nodes":[{"id":"QXNzZXQKaTM3","symbol":"IMX","name":"Immutable X","price":{"currency":"USD","lastPrice":3.21}},{"id":"QXNzZXQKaTU0","symbol":"FORTH","name":"Ampleforth Governance Token","price":{"currency":"USD","lastPrice":7.46}},{"id":"QXNzZXQKaTUx","symbol":"MPL","name":"Maple","price":{"currency":"USD","lastPrice":15.05}}]},"hasNext":false}
-----
artola commented 2 years ago

@maraisr It seems just a small thing with HC, but not related with multiparts. I will recheck in future and back if needed. THANKS A LOT.

For reference, escape hatch:

                if (part.body.label && !part.body.path) {
                  part.body.path = [];
                }