-
Notifications
You must be signed in to change notification settings - Fork 1
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
Conversation
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 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
extraNodeModules: { | ||
stream: require.resolve('readable-stream'), | ||
} |
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.
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", |
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.
This also should not be necessary with latest rpc-reflector
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 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", |
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 can remove this too
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.
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...
scripts/build-backend.sh
Outdated
@@ -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/ |
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.
Does this actually work in the built app? I thought the nodejs-mobile project only bundles code in the nodejs-assets/nodejs-project folder
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.
yeah, this build correctly. It seems that the app can see up the nodejs-project/
directory. But probably @achou11 may see shortcomings of this?
src/frontend/App.tsx
Outdated
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; |
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.
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.
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.
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
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.
Yeah, this is basically refactoring what was already there, probably this will be worked on 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.
ah remember why i had it set up this way - mapeo mobile does it similarly (where api.startServer
calls nodejs.start()
):
either way, agree it probably could just be moved outside for the sake of this project
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.
addressed in #7
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.
Noticed some issues as I explored these changes locally:
-
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 -
Needed to polyfill
process
,util
, andevents
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 -
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 -
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 theEventEmitter
, so that's fine. On React Native, it extends a custom implementation of an EventEmitter, which behaves differently from Node's EventEmitter -
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
@tomasciccola I scheduled a meeting with you for tuesday to talk about these things:
|
As discussed in out meeting today, this is the list of todos for the mock API for observations To do (Backend)
To do (Front-end)
|
So, the type of /* @type {import('mapeo-core-next').Observation[]} */
const mockData = [...] |
So, with @ErikSin we went back and forth with setting 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 |
const log = debug('mapeo-mobile-node-next'); | ||
// @ts-expect-error | ||
const channel = new MessagePortLike(rn_bridge.channel); | ||
channel.start(); |
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.
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.
src/backend/message-port-like.js
Outdated
/** | ||
* @param {Channel} channel | ||
*/ | ||
constructor(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.
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.
src/frontend/lib/MessagePortLike.ts
Outdated
#state: ServerState = 'idle'; | ||
#queuedMessages: any[] = []; | ||
|
||
constructor(channel: 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.
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.
src/frontend/lib/MessagePortLike.ts
Outdated
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(); | ||
} |
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.
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?
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.
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
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.
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
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.
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
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.
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.
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.
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.
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.
after clearing up my misunderstandings with Gregor elsewhere, the actionable changes here would be:
-
use one of the aforementioned libraries to extend for this class, instead of React Native's EventEmitter
-
Remove the custom implementations of
on
,off
,removeListener
,removeAllListeners
. -
Remove
this.#eventSubscriptions
field -
Remove line 41 (
channel.addListener('message')
)
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.
Does extending from tiny-typed-emitter
would work? (I'm mentioning it 'cause the backend/message-port-like.js
extends from that...
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.
Don't think so because it relies on Node's EventEmitter, which isn't available in React Native's runtime
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.
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
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... |
that don't make sense to work on right now
I don't need to change de buildBackend script
Also, moved the starting of the port outside of the message-port-like classes on both sides, for the sake of symmetry
handle case in which the server sended the message before the listener is attached
This branch is a good starting point for a mapeo-mobile with rpc-reflector integrated.
Thinks to take into account:
src/shared/lib/message-port-like.js
src/shared
folder. I don't if this is a good approach or if there's a best practice.src/shared
. Its a plain object but probably I'll turn it into a class when adding the mocked api stuffscripts/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