-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat!: implement receipts #266
Conversation
d590015
to
0883fb4
Compare
e303c6e
to
ff7220c
Compare
const receipts = [] | ||
for await (const invocation of workflow) { | ||
const { cid } = await invocation.delegate() | ||
const receipt = await Receipt.issue({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels weird that an error in the channel + codec working together would lead to a Receipt
vs just not catching the error here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would be happy for it to just throw/reject when encountering unexpected errors here vs 'wrapping' something it doesn't understand (error
is unknown
).
In doing so, I think this takes the thrown object, throws away all properties other than 'message' and 'cause'. It would be frustrating if those properties were important to debug some mistake.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(i.e. if we wrap at all, I think we should only wrap errors from successful decodes, not errors encountered while decoding, i.e. errors where the error might have been in the decoder vs what was on the other side of the channel)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree this is not ideal, yet I feel very strongly that two channels for error handling is going to be a super error prone.
try {
const receipt = await agent.invoke(capbility)
if (receipt.out.error) {
// handle error
}
} catch (error) {
// handle error
}
Perhaps we could come up with a better solution so that they are separate steps, but lets do it in a followup
'application/car': CAR.request, | ||
}, | ||
decoders: { | ||
'application/car+receipt': CAR.response, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These media types are not defined by IANA, yet application/vnd.ipld.car
is https://www.iana.org/assignments/media-types/application/vnd.ipld.car
I don't think it's a good idea to use unregistered media types that don't have a corresponding RFC. It could collide with something made by someone else.
I think we can prototype what might be in an RFC by using the existing media types or something else starting with application/vnd
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The media types here are things I'd expec t be defined independent of any implementation in something like a 'ucanto over http' spec/profile
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm application/car
had been used all over the place, but looks like IPFS gateway now sets application/vnd.ipld.car;version=1
and so should we.
Complication however here is that our codecs also used to set this as content type before this change so not recognizing it will break existing clients. I created a followup issue for this #269
*/ | ||
export const iterate = function* (value) { | ||
if ( | ||
value && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use the type checker to enforce this?
That way it doesn't silently iterate nothing if some accidentally passes undefined instead of the iterable they intended.
If unknown
is desirable as type of value,
we could add an else
block here that throws TypeError
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm intention here was to pass anything and if it is an IPLDView
it would iterate over it's blocks and if it isn't do nothing. It is to traverse arbitrary structures that may contain IPLDView
s inside. We could instead have isIPLDView(object)
predicate so it would be:
for (const [key, value] of Object.entries(something)) {
if (isIPLDView(value)) {
for (const block of value.iterateIPLDBlocks()) {
}
}
}
But I find it more convenient to do this instead
for (const [key, value] of Object.entries(something)) {
for (const block of DAG.iterate(value)) {
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a comment to make it bit more clear.
@@ -22,16 +23,20 @@ export interface EncodeOptions { | |||
readonly hasher?: UCAN.MultihashHasher | |||
} | |||
|
|||
export interface RequestEncodeOptions extends EncodeOptions { | |||
accept?: string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is an example value?
I find myself wondering how the string values here would be different from the string values I'd pass in an http accept header.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(or e.g. if comma separated values are allowed too). comment or jsdoc example would illustrate
capability, | ||
/** @type {Error} */ (error) | ||
/** @type {Error} */ (cause) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels brittle to type cast this here. There's no guarantee that cause
matches Error
afaict.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HandlerExecutionError
just wraps an error and it types it's argument as Error
, now it is true that handler could throw non error, but if someone wrote the handler that throws null
should suffer the consequences I say.
More seriously though I don't think threading through unknown
everywhere is a better approach. Perhaps better solution would be to change HandlerExecutionError
so it accepts unknown
as cause to avoid cast here.
}) | ||
|
||
const client = Client.connect({ | ||
id: w3, | ||
encoder: CAR, | ||
decoder: CBOR, | ||
codec: CAR.outbound, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be great if client could interrogate options.channel
to pick a good default for this if possible. (e.g. if options.channel
is an HTTP Channel, it could issue an HTTP request to the URL interrogating 'which codecs do you support' e.g. options.channel.supportedCodecs(clientSupports)
to get back a supported codec intersection)
(we could ship CAR codec with client so that always works, and if someone passes more supported codecs, it works as long as the supported codecs intersection has at least 1)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not saying we should do now, but I hope one day we can make creating ucanto clients not require extra a priori knowledge about how which codecs are required.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By reading further I think I realized that the outbound
object is already set up to do something like that!
cool. I'd be down for CAR.outbound
to just be a default value here (seems like it wouldn't add very much to the bundle)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think clients should have defaults, but since ucanto client and transports are separate packages without depndencey on each other I don't want to introduce one. That said I do want to collide them into one @ucanto/agent
package in the future which is where we could do such a thing.
On the negotiation thing, I also want outbound
to send one request to find out about the transports supported and protocol schema. But something I hope to do in the future.
export { CAR as codec } | ||
|
||
const HEADERS = Object.freeze({ | ||
'content-type': 'application/car', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems like this media type should be defined in @ipld/js-car
or otherwise stored in a constant somewhere
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think adding it ot @ipld/js-car
is a great idea. I'll create an issue.
* @returns {API.ReceiptResult<API.InboundAcceptCodec, API.HTTPError>} transport | ||
*/ | ||
accept({ headers }) { | ||
const contentType = headers['content-type'] || headers['Content-Type'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
http headers are defined as case insensitive. We should probably only check for content-type
. cOnTent-TypE
is also as valid as headers['Content-Type']
and it seems reasonable to expect normalization at a higher layer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are correct but (my estimate) 90% of cases most systems will normalize them either as one or the other so it seemed like a good idea to just check the other on in case. I don't mind removing second one but it was added because we encountered it somewhere and was easier to just check both than trying to thread the changes into third party libraries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put it differently this appears as new code, but it is just moving existing transport code that already was doing it. I really don't want introduce another potential break here. If we decided to not check that it's fine, but lets do it in the future when we already have migrated old clients.
Co-authored-by: Benjamin Goering <171782+gobengo@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only use jsdoc tags supported by TS https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html when we adopt other tools that recognize such tags we can add tags recognized by them.
}) | ||
} | ||
|
||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
0.2 end up inlining this block into receipt and I have branch that remove this, so I won't bother commenting
}) | ||
} | ||
|
||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As of comments plz note that interfaces classes implement have comments I just don't put duplicate them on implementation. Functions also return interfaces so they should lead to the well commented types.
capability, | ||
/** @type {Error} */ (error) | ||
/** @type {Error} */ (cause) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HandlerExecutionError
just wraps an error and it types it's argument as Error
, now it is true that handler could throw non error, but if someone wrote the handler that throws null
should suffer the consequences I say.
More seriously though I don't think threading through unknown
everywhere is a better approach. Perhaps better solution would be to change HandlerExecutionError
so it accepts unknown
as cause to avoid cast here.
}) | ||
|
||
const client = Client.connect({ | ||
id: w3, | ||
encoder: CAR, | ||
decoder: CBOR, | ||
codec: CAR.outbound, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think clients should have defaults, but since ucanto client and transports are separate packages without depndencey on each other I don't want to introduce one. That said I do want to collide them into one @ucanto/agent
package in the future which is where we could do such a thing.
On the negotiation thing, I also want outbound
to send one request to find out about the transports supported and protocol schema. But something I hope to do in the future.
export { CAR as codec } | ||
|
||
const HEADERS = Object.freeze({ | ||
'content-type': 'application/car', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think adding it ot @ipld/js-car
is a great idea. I'll create an issue.
* @returns {API.ReceiptResult<API.InboundAcceptCodec, API.HTTPError>} transport | ||
*/ | ||
accept({ headers }) { | ||
const contentType = headers['content-type'] || headers['Content-Type'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are correct but (my estimate) 90% of cases most systems will normalize them either as one or the other so it seemed like a good idea to just check the other on in case. I don't mind removing second one but it was added because we encountered it somewhere and was easier to just check both than trying to thread the changes into third party libraries.
* @returns {API.ReceiptResult<API.InboundAcceptCodec, API.HTTPError>} transport | ||
*/ | ||
accept({ headers }) { | ||
const contentType = headers['content-type'] || headers['Content-Type'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put it differently this appears as new code, but it is just moving existing transport code that already was doing it. I really don't want introduce another potential break here. If we decided to not check that it's fine, but lets do it in the future when we already have migrated old clients.
Overview
Receipt
as opposed toResult
type which is a breaking change.Transport.Codec
as opposed to{ encoder, decoder }
pair which performs content type negotiation.codec
field corresponding toTransport.OutboundCodec
codec
field corresponding toTransport.InboundCodec
Legacy
codec to deal with all clients and new clients should not use CBOR.@ucanto/core
as they are needed for receipts. Transport still reexports them so it's not breaking change.@ucanto/core
now re-exports things that were used from multiformats to cut down deps and avoid things getting out of sync.{}
in the receipt.@ucanto/core
now has exportsDAG
module with bunch of utilities to for building and viewing DAGs encoded in CARs.Fixes #265