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

RPC reflector #3

Merged
merged 33 commits into from
Aug 24, 2023
Merged

RPC reflector #3

merged 33 commits into from
Aug 24, 2023

Conversation

tomasciccola
Copy link
Contributor

This branch is a good starting point for a mapeo-mobile with rpc-reflector integrated.
Thinks to take into account:

  • Initially there were two different wrappers (frontend and backend) for the channel thats used for bridging. I basically merged them into one file trying to accommodate both cases. This now lives on src/shared/lib/message-port-like.js
  • For shared files between the frontend and backend I added a src/shared folder. I don't if this is a good approach or if there's a best practice.
  • The api (now called MapeoClient) lives on src/shared. Its a plain object but probably I'll turn it into a class when adding the mocked api stuff
  • I modified the scripts/build_backend.sh file so to copy the shared files in a way that preserves the relative file path and not mess imports. I don't know if this is a good approach, but it works

Copy link
Member

@gmaclennan gmaclennan left a comment

Choose a reason for hiding this comment

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

I few bits of feedback. I think it's ok to leave the useNodejsMobile() hook here for this PR, because it will work ok, but we should address that in a later PR.

Also to address in a later PR: #4

metro.config.js Outdated
Comment on lines 21 to 23
extraNodeModules: {
stream: require.resolve('readable-stream'),
}
Copy link
Member

Choose a reason for hiding this comment

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

This shouldn't be necessary with the latest rpc-reflector

@@ -10,9 +10,14 @@
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
},
"dependencies": {
"assert": "^2.0.0",
Copy link
Member

Choose a reason for hiding this comment

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

This also should not be necessary with latest rpc-reflector

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems that removing this breaks the app, don't know the real reason though...

"nodejs-mobile-react-native": "^0.8.2",
"react": "18.1.0",
"react-native": "0.70.6"
"react-native": "0.70.6",
"readable-stream": "^4.4.1",
Copy link
Member

Choose a reason for hiding this comment

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

You can remove this too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same with this, I had to leave the readable-stream dep for the app to work. But the metro translation to stream wasn't necessary. react-native mysteries I guess...

@@ -27,6 +27,8 @@ else
echo "Set Build Native Modules on"
fi
cp -r ./src/backend ./nodejs-assets
mkdir -p ./nodejs-assets/shared/lib
cp -r ./src/shared/* ./nodejs-assets/shared/
Copy link
Member

Choose a reason for hiding this comment

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

Does this actually work in the built app? I thought the nodejs-mobile project only bundles code in the nodejs-assets/nodejs-project folder

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, this build correctly. It seems that the app can see up the nodejs-project/ directory. But probably @achou11 may see shortcomings of this?

Comment on lines 46 to 55
const [clientApi, setClientApi] = React.useState<typeof MapeoClient>();
React.useEffect(() => {
nodejs.start('loader.js');

const subscription = nodejs.channel.addListener('message', msg => {
console.log('RECEIVED MESSAGE', msg);
});

const channel = new MessagePortLike(nodejs.channel);
setClientApi(createClient<typeof MapeoClient>(channel));
return () => {
// @ts-expect-error
subscription.remove();
channel.close();
};
}, []);

return {
send: (msg: string) => nodejs.channel.send(msg),
};
return clientApi;
Copy link
Member

Choose a reason for hiding this comment

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

nodejs.start() shouldn't be in here - that should just happen once when the JS file first loads, not tied to the react lifecycle.

I'm not sure if the channel and clientApi need to be in here either, nor tied to the useEffect. This isn't a side-effect, and there is no async here, so this code is rendering the app once with no clientApi, then calling setClientApi() and rendering again. If we want to do a useClientApi() it should probably just be a wrapper for useMemo(), since we only want it once, or we could just put the client api in the global scope.

Copy link
Member

Choose a reason for hiding this comment

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

ah yeah this stuff I worked on - not sure why I did this but fully agree that it should be set up outside of react

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, this is basically refactoring what was already there, probably this will be worked on here

Copy link
Member

Choose a reason for hiding this comment

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

ah remember why i had it set up this way - mapeo mobile does it similarly (where api.startServer calls nodejs.start()):

https://github.com/digidem/mapeo-mobile/blob/ca167c257b90ba74b0e63e623f405aed98d86145/src/frontend/AppLoading.tsx#L77-L81

either way, agree it probably could just be moved outside for the sake of this project

Copy link
Member

Choose a reason for hiding this comment

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

addressed in #7

Copy link
Member

@achou11 achou11 left a comment

Choose a reason for hiding this comment

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

Noticed some issues as I explored these changes locally:

  1. rpc-reflector was not being specified as a dep for the backend project. It was only being specified for the top-level. Guessing that this causes some of the issues related to certain deps seeming to be needed but not really

  2. Needed to polyfill process, util, and events in order for rpc-reflector to not complain on the React Native side. Using something like https://github.com/ionic-team/rollup-plugin-node-polyfills to achieve and updating the metro config worked for now

  3. The MessagePortLike class needs to add an off method, which is what rpc-reflector uses to determine if it's a viable message port interface: https://github.com/digidem/rpc-reflector/blob/9bcc507edd6e498145593bf491a927e3ab5ba652/lib/is-message-port-like.js#L19-L21

  4. The MessagePortLike doesn't fully account for the differences between the nodejs mobile channel on React Native vs NodeJS Mobile. On NodeJS Mobile, it extends the EventEmitter, so that's fine. On React Native, it extends a custom implementation of an EventEmitter, which behaves differently from Node's EventEmitter

  5. Not as important, but currently this PR introduces deps changes via npm 6 (previously was 8). Ideally we'd keep it at 8 but Tomas mentioned potential issues getting 8 to work in his environment, so may need some looking into

@ErikSin
Copy link
Contributor

ErikSin commented Jul 21, 2023

@tomasciccola I scheduled a meeting with you for tuesday to talk about these things:

  1. I am unable to build this app on my phone/emulator
  2. Typescript typing is not working for me.
  3. Review general architecture
  4. Can we talk about strategies of creating fake data (ts object, json objects....etc)

@ErikSin
Copy link
Contributor

ErikSin commented Jul 25, 2023

As discussed in out meeting today, this is the list of todos for the mock API for observations

To do (Backend)

  • Generate observations (as JSON) using 'Mapeo-Mock-Data'.
  • Convert JSON into objects, and creating them as files in `src/utils'
  • On creation of Mapeo-Core, write them objects to database

To do (Front-end)

  • Ask Gregor/Andrew about typings
  • Move createClient()outside of react lifecycle/hook
  • Create useObservation and useObservations hook
  • Bring Screens from mapeo-mobile which utilize observations

@tomasciccola
Copy link
Contributor Author

tomasciccola commented Jul 26, 2023

So, the type of mapeoClient.observation is now just a plain object, this was so that the generated docs from mapeo-mock-data would be valid observations. In the, not so distant, future we are going to get the type from mapeo-core-next so I didn't took the time to get more strict typing.
Additionally I created src/utils/mockData.js as javascript (not typescript) despite what initially discussed with Erik, this is 'cause the backend, which is plain js, is loading this file and transpiling didn't make much sense.
I know @ErikSin we talked about using ts for this file, but in the future the JSDoc on the mockData file won't be much more than

/* @type {import('mapeo-core-next').Observation[]} */
const mockData = [...]

@tomasciccola
Copy link
Contributor Author

So, with @ErikSin we went back and forth with setting clientApi as a function initialized on App.tsx or as a hook. For now its a hook, but I like the approach that @gmaclennan proposed, copying and pasting from the chat:

export function WithClientApi({ children, client }) {
  return (
    <ClientApiContext.Provider value={client}>
      {children}
    </ClientApiContext.Provider>
  )
}

// this will probably be smth like initClient() that sets up the MessagePort like and coordinates server state
const client = createClient(port) 

<WithClientApi client={client}>
  <App />
</WithClientApi>

// That way if you want to unit-test components then you can do
const client = {
  observation: {
    getMany(): [...mockedData]
  }
}

<WithClientApi client={client}>
  <ComponentToUnitTest />
</WithClientApi>

So basically use a react context. We can set up that in this PR, or merge it in the state that's in, and do it later

@gmaclennan gmaclennan changed the title Rcp reflector RPC reflector Aug 15, 2023
const log = debug('mapeo-mobile-node-next');
// @ts-expect-error
const channel = new MessagePortLike(rn_bridge.channel);
channel.start();
Copy link
Member

Choose a reason for hiding this comment

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

Can you move this (.start()) to after the RPC server is created? Strictly this will be ok as-is because everything happens in the same tick, but if someone changes that, e.g. with an await before createServer(), then there would be a chance that messages would be lost.

/**
* @param {Channel} channel
*/
constructor(channel) {
Copy link
Member

Choose a reason for hiding this comment

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

Since this is strictly tied to the static import nodejs-mobile-react-native.Channel, I think this should just be an import in this file, and not a constructor parameter, so this file encapsulates the Channel dependency.

#state: ServerState = 'idle';
#queuedMessages: any[] = [];

constructor(channel: Channel) {
Copy link
Member

Choose a reason for hiding this comment

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

Since this is strictly tied to the static import nodejs-mobile-react-native.Channel, I think this should just be an import in this file, and not a constructor parameter, so this file encapsulates the Channel dependency.

Comment on lines 63 to 106
on(event: string, listener: (...args: any[]) => any) {
const sub = this.addListener(event, listener);

const registry = this.#eventsSubscriptions.get(event);

if (!registry) {
this.#eventsSubscriptions.set(event, new WeakMap([[listener, sub]]));
return this;
}

if (!registry.has(listener)) {
registry.set(listener, sub);
}

return this;
}

off(event: string, listener: (...args: any[]) => void) {
return this.removeListener(event, listener);
}

removeListener(event: string, listener: (...args: any[]) => void) {
const registry = this.#eventsSubscriptions.get(event);

if (!registry) {
return;
}

const subscription = registry.get(listener);

if (subscription) {
subscription.remove();
registry.delete(listener);

// TODO: Call this.#subscriptions.delete(event) if no more listeners?
}

return this;
}

removeAllListeners() {
this.#eventsSubscriptions.clear();
this.removeAllListeners();
}
Copy link
Member

Choose a reason for hiding this comment

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

This seems to be re-implementing an event emitter, which is a hard thing to do and is full of edge cases. Why not use an existing library and extend from that rather than the react-native eventemitter?

Copy link
Member

Choose a reason for hiding this comment

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

think the context for this (as well as the other review comments) mostly lives with stuff that I updated via #8 (see PR description)

but yeah if there's something we can use to simplify this, probably a better approach

Copy link
Member

@achou11 achou11 Aug 15, 2023

Choose a reason for hiding this comment

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

think the main trickiness was that since this is tied to nodejs mobile's Channel implementation (which extends React Native's EventEmitter), it doesn't have the expected interface as the traditional Node EventEmitter, which RPC reflector relies on.

React Native's EventEmitter uses a subscription based API i.e. addListener returns a Subscription and there are no methods like on and off, hence why this class is reimplementing some of those while also tying it with the Nodejs mobile Channel

Copy link
Member

@achou11 achou11 Aug 15, 2023

Choose a reason for hiding this comment

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

link to React Native EventEmiiter implementation that the Channel extends (for the version of React Native we're using):

https://github.com/facebook/react-native/blob/v0.70.6/Libraries/vendor/emitter/EventEmitter.js

note that it's missing methods that rpc-reflector checks to determine message port compat, hence this wrapper class to begin with and the reimplementation of some event emitter like methods:

https://github.com/digidem/rpc-reflector/blob/main/lib/is-message-port-like.js#L19-L21

Copy link
Member

Choose a reason for hiding this comment

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

Yes, but why extend MessagePortLike from the react-native EventEmitter? Why not extend EventEmitter from events or EventEmitter3 - those have the methods that rpc-reflector checks. I would have thought that would work fine? There is no relationship between the type of EventEmitter that Channel extends and what MessagePortLike needs to be, except how you internally handle attaching and removing listeners from the channel.

Copy link
Member

Choose a reason for hiding this comment

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

yeah, because what you're doing here is essentially reimplementing an EventEmitter, and also creating a compatibility layer with the react native EventEmitter, which is a maintenance overhead, and probably needs a lot more work to catch all the edge cases that the npm libs already check for.

Copy link
Member

@achou11 achou11 Aug 15, 2023

Choose a reason for hiding this comment

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

after clearing up my misunderstandings with Gregor elsewhere, the actionable changes here would be:

  1. use one of the aforementioned libraries to extend for this class, instead of React Native's EventEmitter

  2. Remove the custom implementations of on, off, removeListener, removeAllListeners.

  3. Remove this.#eventSubscriptions field

  4. Remove line 41 (channel.addListener('message'))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does extending from tiny-typed-emitter would work? (I'm mentioning it 'cause the backend/message-port-like.js extends from that...

Copy link
Member

Choose a reason for hiding this comment

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

Don't think so because it relies on Node's EventEmitter, which isn't available in React Native's runtime

Copy link
Member

Choose a reason for hiding this comment

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

tiny-typed-emitter is just require('events').EventEmitter. It works in node, but you need to have a polyfill for react native. You can just use events which is handy because it has the same name - you just need to install it from npm in the frontend - or you can use EventEmitter3 with tiny-typed-emitter if you configure Metro to package events -> EventEmitter3

@achou11
Copy link
Member

achou11 commented Aug 15, 2023

Having trouble getting this to work when running locally. Just to check, is it working for other people?

@tomasciccola
Copy link
Contributor Author

Having trouble getting this to work when running locally. Just to check, is it working for other people?

Yep, up until now, when I implemented the changes proposed by @gmaclennan, now the app crashes upon opening it, and logcat doesn't give any useful output. Gonna rollback the chances and introduce them one by one to see where it fails...

@ErikSin ErikSin merged commit 6d9e242 into main Aug 24, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants