-
Notifications
You must be signed in to change notification settings - Fork 39
Proposal: Let's introduce asCID
static method
#111
Comments
I’m not sure what this accomplishes. Is this meant to convert between the new and old If so, this isn’t going to work as stated because what is causing the breaking change is the fact that the new implementation doesn’t have the full codec table, so you won’t have the You should be able to convert from the new However, I don’t think it’s worth writing this particular compatibility layer because there’s other changes across the new stuff as well. This is all covered by the Having code that sits in-between this migration is probably going to be more pain than it’s worth, there’s a lot of small differences covered by This isn’t just a breaking change across our code bases, it’s also a breaking change across all of our consumers. We have to consider what happens to third party consumers when we swap these type implementations out and ensure that we either maintain compatibility or cause an exception because there are many cases where you could not cause an exception but still cause incorrect behavior that consumers won’t even notice and instead be writing bad data. This is why we decided to make the new |
It may not cover this specific change because as you said it may not be possible to convert without codec table. However it can cover other changes that may occur. Here is the contrived example: V1class Bla {
static get typeSymbol() {
return Symbol.for('Bla')
}
static isBla(v) {
return v && v[Bla.typeSymbol]
}
static asBla(v) {
if (v instanceof v) {
return v
} else if (v && Buffer.isBuffer(v.buffer)) {
return new Bla(v.buffer)
} else {
return null
}
}
constructor(buffer) {
if (!Buffer.isBuffer(buffer)) {
throw new Error("Argument should be a buffer")
}
this.buffer = buffer
}
toHexString() {
return this.buffer.toString('hex')
}
} V2class Bla {
static get typeSymbol() {
return Symbol.for('Bla')
}
static isBla(v) {
return v && v[Bla.typeSymbol]
}
static asBla(v) {
if (v instanceof v) {
return v
} else if (v && Buffer.isBuffer(v.buffer)) {
return new Bla(v.buffer)
} else {
return null
}
}
constructor(buffer) {
if (!Buffer.isBuffer(buffer)) {
throw new Error("Argument should be a buffer")
}
this.buffer = buffer
}
toHexString() {
return this.buffer.toString('hex')
}
toBase64String() {
return return this.buffer.toString('base64')
}
} User assuming V2if (Bla.isBla(v)) {
v.toBase64String()
} If const bla = Bla.asBla(v)
if (bla) {
bla.toBase64String()
} Edit: Added constructors to the examples for the completeness |
@mikeal I think you were reading this in context of new CIDs work happening in IPLD, which is likely due to me mentioning comment about I think reading with that context is misleading. It is unrelated. There is problem with |
I like this pattern and I think it elegantly solves most of the challenges here and the I think this method might be able to solve the old-CID new-CID conversion problem too, both ways: for new to old it just has to do a lookup of We do have cases in IPLD that rely on an https://github.com/ipld/js-ipld-dag-cbor/blob/master/src/util.js#L46-L48 Similar in DAG-JSON: https://github.com/ipld/js-dag-json/blob/master/index.js#L8-L10 |
As I see it, the problem is you get a If this is not an option then a code change may be appropriate. As proposed it looks like Why not just add any additional checks there instead of having to add conditional logic to the calling code every time we touch a CID from another module? |
It does same exact check as
It really is poor mans pattern matching rather then constructor. Meaning it may return CID if value passed represents CID otherwise it returns Argument could be made that constructor could throw instead of returning |
Not to object properties, to local variables that and if not null only then assign. This really meant to do exact same check except null check instead of boolean. I do mention |
Throwing when it encounters invalid data is exactly what the constructor does right now.
If that's so, then just replace it. Don't give the user multiple ways of doing the same thing. However I'd rather not add another method that does 50% of isCID's job and 50% of the constructors job. If anything, I'd like to see v1 of this module released so we can properly communicate breaking changes through semver (e.g. not lose |
Ok but here is the prominent pattern I see today within PL code base and outside of it: if (CID.isCID(value)) {
useCID(value)
}
// ...etc Which following this proposal would turn into: const cid = CID.asCID(value)
if (cid) {
useCID(cid)
}
// ...etc Embracing constructor here would change that into (what I'd say an awkward) usage: try {
const cid = new CID(value)
useCID(cid)
} catch(_) {}
// ...etc Which also introduces a surface for bugs where exception thrown by
I agree I think long term goal is to replace |
Had a conversation with @mikeal today about this & I would like to provide a sketch as what I think we agreed was a worthy avenue to explore. class CID {
/**
* @param {any} value
* @returns {CID|null}
*/
static asCID(value) {
// If it is already a CID instance no allocation is necessary
// just return it back
if (value instanceof CID) {
return value
}
// If it is not instance of CID it might be instance from different version of CID
// library or it could be CID instance that was structure cloned. Instead of using
// symbol we recognize `value.asCID` circular that renders `value` invalid for
// JSON & DAG representation. In other words it's equivalent of current symbol
// check.
else if (value != null && value.asCID === value) {
const {version, codec, multihash, multibaseName} = value
return new CID(version, codec, multihash, multibaseName)
} else {
return null
}
}
constructor(version, codec, multihash, multibaseName) {
this.version = version
this.codec = codec
this.multihash = multihash
this.multibaseName = multibaseName
this.asCID = this
}
toJSON() {
const { version, codec, multihash, multibaseName } = this
return { version, codec, multihash, multibaseName }
}
} What this gives us is following: Works well with structure cloning algorithm, here is the sample illustrating it:const main = (multihash) => {
var cid = new CID(0, 'dag-pb', multihash, 'base32')
var {port1:sender, port2:receiver} = new MessageChannel()
receiver.onmessage = ({data}) => {
data instanceof CID // false
const cid2 = CID.asCID(data)
cid2 instanceof CID // true
}
sender.postMessage(cid)
}
|
Me and @mikeal also have talked about the idea of representing class CID {
/**
*
* @param {ArrayBuffer} buffer
* @param {number} byteOffset
* @param {number} byteLength
*/
constructor(buffer, byteOffset, byteLength) {
this.buffer = buffer
this.byteOffset = byteOffset
this.byteLength = byteLength
}
get version() {
const { version } = materialize(this)
return version
}
get codec() {
const { codec } = materialize(this)
return codec
}
get multihash() {
const { multihash } = materialize(this)
return multihash
}
}
const materialize = (cid) => {
const bytes = new Uint8Array(cid.buffer, cid.byteOffset, cid.byteLength)
const firstByte = bytes[0]
if (firstByte === 1) {
const bytes = new Uint8Array(cid.buffer, cid.byteOffset + 1, cid.byteLength - 1)
Object.defineProperties(cid, {
version: { value: 1 },
codec: { value: multicodec.getCodec(bytes) },
multihash: { value: multicodec.rmPrefix(bytes) }
})
} else {
Object.defineProperties(cid, {
version: { value: 0 },
codec: { value: 'dag-pb' },
multihash: { value: bytes }
})
}
return cid
} I do think that might be a good idea, however it is orthogonal to the overall |
Why is it nice to have to break |
Please correct me @mikeal if I got it wrong, but I think it was something to the effect of preventing error due to naive serialization. If I did get that wrong, and if want to get |
I see now that |
Me @achingbrain and @hugomrdias had a discussion about this on call other day, and I will try to summarize it here.
|
Now that you’re considering a breaking change (deprecating isCID) I should note that there’s a future upgrade to |
@mikeal I have couple of questions:
|
@Gozala these answers are a bit complicated, I’m going to join the Core Implementations meeting on Monday and can discuss it in more detail there. |
We had a higher bandwidth exchange today and have settled on the following course of action: Phase 1: Implement
Phase 2: Evaluate
|
This idea first was proposed in #109 and later came up in #110. I thought it would be best to factor it out into separate thread and discuss it here.
Problem
At the moment
CID.isCID(v)
is used all over the place as a glorifiedv instanceof CID
check. However it does not check ifv
is instanceofCID
instead it checks presence of special symbol. That addresses problem with having multiple different versions ofCID
implementations, however it does overlook the fact that two different version may in fact be incompatible e.g. new version may change behavior of specific method e.g.toString
I think that is already happening (per #109 (comment))Even if above is backwards compatible change, incompatible change may come sooner or later.
This is a symptom of more general issue with validation pattern.
If you're inclined I would invite to read excellent parse don't validate primer that explains hazzards of that problem (in the context of typed language, but it applies to this just as well)
Proposal
Changing behavior of
CID.isCID(v)
to do component validation (if I interperent @hugomrdias suggestion moxystudio/js-class-is#25 (comment)) is going to be too disruptive to be worth it. Furthermore it would not address the problem stated above. Instead I would like to propose an alternative in form ofasCID
static method to which existing code base could be gradually migrated and future code base should default.Implementation could be something along these lines:
With such method in place existing pattern of
Could gradually be transitioned to following pattern instead:
It is a bit more verbose, but it would avoid issues that may arise from version incompatibilities. It will also preserve CID-ness when things are structured cloned.
The text was updated successfully, but these errors were encountered: