kiliman / remix-typedjson

This package is a replacement for superjson to use in your Remix app. It handles a subset of types that `superjson` supports, but is faster and smaller.
MIT License
435 stars 19 forks source link

`typedDefer`? #20

Closed frontsideair closed 10 months ago

frontsideair commented 1 year ago

Now that Remix is React-Router'ed, I think a wrapper around defer is needed. A useTypedAsyncValue counterpart may be needed as well.

kiliman commented 1 year ago

Yes, I haven't had a chance to add support for defer, but it is on my TODO list.

TheRealFlyingCoder commented 1 year ago

I'm just here to say that i'm waiting eagerly for the typedDefer update haha

dteknolove commented 1 year ago

Thanks for create this awesome library. I hope this wil be well maintained.

kiliman commented 1 year ago

Working on this now. Almost there.

sixers commented 1 year ago

Hey, any update on this?

kiliman commented 1 year ago

The more I work on this, the more I realize it may not be possible without help from Remix.

I can get the "fast data" transferred just fine since I know the values and the "types" at call time. However, I don't have the actual data for "deferred data," just a Promise, so I cannot serialize and send the metadata.

The issue is that Remix doesn't provide any hooks into the deferred response pipeline, so I can't use the typedjson serializer on the Promise result.

I could create a patch, but that's not a long-term solution. Ideally, Remix would provide hooks for an encode/decode function (default being JSON.stringify/parse).

I'll keep you all posted.

function enqueueTrackedPromise(
  controller: any,
  encoder: TextEncoder,
  settledKey: string,
  promise: TrackedPromise
) {
  if ("_error" in promise) {
    controller.enqueue(
      encoder.encode(
        "error:" +
          JSON.stringify({
            [settledKey]: serializeError(promise._error),
          }) +
          "\n\n"
      )
    );
  } else {
    controller.enqueue(
      encoder.encode(
        "data:" +
          JSON.stringify({ [settledKey]: promise._data ?? null }) +
          "\n\n"
      )
    );
  }
}

https://github.com/remix-run/remix/blob/72c22b3deb9e84e97359b481f7f2af6cdc355877/packages/remix-server-runtime/responses.ts#L198-L223

TheRealFlyingCoder commented 1 year ago

That's rough... It might pay to push for an update to the system anyway, they've mentioned it's not where they want it right now?

It wouldn't be a breaking change for them to add the hook, so I don't think they'd reject if you had the explanation and a PR?

kiliman commented 1 year ago

Yeah, I'm going to play with adding that feature. At least show there's a need and that it wouldn't be intrusive.

kiliman commented 1 year ago

I added this to entry.server just to see what would happen and sure enough all calls to JSON.stringify/parse got rerouted.

// monkey patch JSON.stringify and JSON.parse
let oldStringify = JSON.stringify;
let oldParse = JSON.parse;

JSON.stringify = function (
  value: any,
  replacer?: (
    this: any,
    key: string,
    value: any
  ) => any | (number | string)[] | null,
  space?: string | number
) {
  console.log("stringify", value);
  return oldStringify(value, replacer, space);
};
JSON.parse = function (
  text: string,
  reviver?: (this: any, key: string, value: any) => any
) {
  console.log("parse", text);
  return oldParse(text, reviver);
};

Anyway, I may be able to handle this entirely in userland.

TheRealFlyingCoder commented 1 year ago

That is filthy but I love it haha

kiliman commented 1 year ago

Making progress. Deferred data is now encoded via typedjson. Unfortunately, it does require a patch. Hopefully, we'll get something like this added to the core.

In entry.server, you add the following line. This replaces the standard JSON calls.

setDataTransformer({
  encode: typedjson.stringifyRemix,
  decode: typedjson.deserializeRemix,
});
image
TheRealFlyingCoder commented 1 year ago

I'll give it a go today in my project and let you know if I find any issues 👍

kiliman commented 1 year ago

Sorry if I wasn't clear, but this is still work in progress. I hope to have a release by the end of the week. Thanks!

TheRealFlyingCoder commented 1 year ago

Sorry if I wasn't clear, but this is still work in progress. I hope to have a release by the end of the week. Thanks!

I was thinking that when I sent the message and saw there was no release after haha All good!

KingSora commented 1 year ago

@kiliman Couldn't you do something like this:

// serialize responses from promises on the server as soon as they resolve
const typedDefer: typeof defer = (data, ...args) => {
  Object.entries(defer).forEach(([key, value]) => {
    if (value instanceof Promise) {
      Object.assign(data, {
        [key]: value.then((resolvedData) => serialize(resolvedData)),
      });
    }
    else {
      Object.assign(data, {
        [key]: serialize(value),
      });
    }
  });

  return defer(data, ...args);
};

// deserialize responses from promises on the client as soon as they resolve
const TypedAwait = (props) => {
  return <Await {...props}>
    {(resolvedData) => props.children(deserialize(resolvedData))}
  </Await>
}

const loader = () => {
  //...
  const myPromise = fetch(/* ... */);
  return typedDefer({ myPromise });
}

export default function Component() {
  const { myPromise } = useTypedDefer<typeof loader>();

  return <Suspense fallback={<p>Loading..</p>}>
      <TypedAwait
        resolve={myPromise }
        errorElement={<p>Error!</p>}>
          {(response) => {
            return <p>hi</p>;
          }}
      </TypedAwait>
    </Suspense>
}

This works at least on my end.. useTypedDefer is basically a wrapper around useLoaderData which knows whether a field is a promise or not.

dev-xo commented 1 year ago

Any updates on this? Would be amazing to have some news @kiliman! Thanks for the work you have set into this!

dev-xo commented 10 months ago

Thank you so much @kiliman! Been waiting for this one! 🎉🙏

kiliman commented 10 months ago

BTW: I just want to thank @KingSora for posting his snippet. It was the basis for the actual working version. 🙌

kiliman commented 10 months ago

If you're using typeddefer in your project, let me know how it's working out for you. I hope I covered all the edge-cases.