Skip to content
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

Add a robustier instanceof alternative to allow cross-package compatibility. #122

Merged
merged 26 commits into from
Jul 18, 2024

Conversation

Shaptic
Copy link
Contributor

@Shaptic Shaptic commented May 9, 2024

Note: Lots of people tagged for review mostly as an FYI as there may or may not be far-reaching implications to this change.

What

Introduce a better check for cross-compatible XdrCompositeType subtypes (Union, Struct, Enum, and Array) based on a Discord discussion from @earrietadev which will check the prototype chain for the constructors and properties we need:

export function isSerializableIsh(
    value: any, 
    subtype: InstanceType<typeof XdrCompositeType>
): value is XdrCompositeType;

(This isn't a proper TypeScript definition because js-xdr (a) doesn't use TS and (b) doesn't use interfaces, but imagine XdrCompositeType as being an interface and it makes more sense.)

We also make the error messages way better.

Details

Because the classes are generated, their prototype constructor name doesn't match the actual class name more often than not. However, they do still inject the "true" name into their prototype. The three types we're trying to match on all do this differently:

  • structs will use structName, see struct.js
  • unions will use unionName, see union.js
  • enums will use enumName, see enum.js

It's not perfect, but it's better than the instanceof check we have now.

So we will check these fields as well as the existence of a read() and write() method to make a best-effort guess that the given value matches the kind we expect.

Why

Major versions of a package should probably be cross-compatible in the same dependency tree. For example, js-xdr@3.1.1 should function interchangeably alongside js-xdr@3.0.0 if the additive features are not relevant.

That is not the case today due to instanceof checks that become package-specific, while isSerializableIsh should rectify this.

Known Limitations

There are only performance implications for people who are introducing this mixing behavior (besides the new overhead of a function call which I would consider trivial), so there are no performance regression risks.

@Shaptic Shaptic requested a review from fnando May 9, 2024 20:52
@BlaineHeffron
Copy link

The new check also fails to resolve stellar/stellar-cli#1285

src/xdr-type.js Outdated Show resolved Hide resolved
Copy link
Member

@willemneal willemneal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One small nit, but I don't think it should prevent merging. Always fun digging into the guts of JS.

@@ -26,8 +26,13 @@ export class Enum extends XdrPrimitiveType {
* @inheritDoc
*/
static write(value, writer) {
if (!(value instanceof this))
throw new XdrWriterError(`unknown ${value} is not a ${this.enumName}`);
if (!this.isValid(value)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming that this is the Class, but personally I find it less confusing if you use the Class directly to remove any ambiguities. Plus the caller could have used bind to change this method's this. Not that we need to worry, but it's possible.

Suggested change
if (!this.isValid(value)) {
if (!Enum.isValid(value)) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, we have to use this here for an oddly-specific reason: we need to make sure it deals with child classes and any modifications that are made after creation, e.g. like here. Just passing the class name doesn't cover this case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But shouldn't the classed be sealed? Why do we need to consider the case of subclasses?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can see in that linked line that a Union itself is never actually created, it's always a locally-generated subclass of ChildUnion that has a bunch of properties attached to it (most importantly unionName) that gets returned. So we'd need to be able to [the specific ChildUnion class].isValid if we wanted an accurate evaluation of "this"ness.

Copy link
Member

@willemneal willemneal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've always had issues with this in JS and in this case being this as the Class or (constructor?) is a bit odd to me. But if this is the standard JS way, pay my reservations to heed.

@piyalbasu
Copy link

This looks solid to me. I'll echo @willemneal 's thought about using the Class rather than this. Probably not super critical, but you may as well cover all your bases

@Shaptic Shaptic added the rpc-sdk-scrum Align with Platform's RPC/SDK planning board label Jul 1, 2024
@Shaptic
Copy link
Contributor Author

Shaptic commented Jul 1, 2024

I've tried testing this with downstream systems and landed upon the wonderfully helpful "[object Object]'s struct name is SorobanAuthorizationEntry not SorobanAuthorizationEntry: " so clearly the extra check is still failing in key moments. I'm still in the process of debugging it and will merge this once I can pull that off.

@Shaptic
Copy link
Contributor Author

Shaptic commented Jul 17, 2024

Yooooo I ran this through a project that uses vite all the way down through the SDK and it works!! 🥳 🥳

@Shaptic Shaptic merged commit 3e4fc1a into master Jul 18, 2024
4 checks passed
@Shaptic Shaptic deleted the robust-ctor-check branch July 18, 2024 23:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
rpc-sdk-scrum Align with Platform's RPC/SDK planning board
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

5 participants