-
Notifications
You must be signed in to change notification settings - Fork 6
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
feat: subscriptions (server-streams) + observable #22
Conversation
datatypes/index.ts
Outdated
@@ -0,0 +1 @@ | |||
export { Observable } from './observable'; |
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.
curious what your vision is here? i wouldn't really expect observable to need to be included in the river library
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 think having common state abstractions that are likely to be useful to service builders make sense to include in a sort of 'standard library'. this of course has fuzzy limits on what should be 'useful' enough to consider but I don't think it was entirely obvious to me how to do something like a subscribable state thing so having a toolbox to reach for is nice
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.
we had a smol chat irl. having state abstractions sounds great, but it's probably for the best if we keep those in another repository so that River can concern itself only / mostly with how we RPC.
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 convo with @lhchavez, we decided to just make this a test fixture (it helps simplify subscription testing but lets not kitchen sink a bunch of data types :))
datatypes/observable.ts
Outdated
* Represents an observable value that can be subscribed to for changes. | ||
* @template T - The type of the value being observed. | ||
*/ | ||
export class Observable<T> { |
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.
zawinski's law but for javascript libraries is that every eventually has its own observable.
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.
me when i observe all the things
@@ -126,6 +131,64 @@ describe.each(codecs)( | |||
close(); | |||
}); | |||
|
|||
test('subscription', async () => { |
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 dig the name: subscription
> server-stream
.
datatypes/index.ts
Outdated
@@ -0,0 +1 @@ | |||
export { Observable } from './observable'; |
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.
we had a smol chat irl. having state abstractions sounds great, but it's probably for the best if we keep those in another repository so that River can concern itself only / mostly with how we RPC.
router/client.ts
Outdated
: ProcType<Router, ProcName> extends 'subscription' // subscription | ||
? { | ||
subscribe: ( | ||
input: Static<ProcInput<Router, ProcName>>, // input |
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.
vvvv optional, but it's exceedingly obvious that this is an input without the comment.
input: Static<ProcInput<Router, ProcName>>, // input | |
input: Static<ProcInput<Router, ProcName>>, |
@@ -192,5 +216,34 @@ export const createClient = <Srv extends Server<Record<string, AnyService>>>( | |||
ControlFlags.StreamOpenBit | ControlFlags.StreamClosedBit; | |||
transport.send(m); | |||
return waitForMessage(transport, belongsToSameStream); | |||
} else if (procType === 'subscribe') { |
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.
💆♂️
router/server.ts
Outdated
@@ -140,11 +141,12 @@ export async function createServer<Services extends Record<string, AnyService>>( | |||
} else if (procedure.type === 'rpc') { | |||
openPromises.push( | |||
(async () => { | |||
for await (const inputMessage of incoming) { | |||
const inputMessage = await incoming.next(); | |||
if (inputMessage.value) { |
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.
what happens if the input message is empty? how can we communicate this to the peer?
also nit, can we use the guard clause pattern for less indentation?:
if (inputMessage.value) { | |
if (!inputMessage.value) { | |
return; | |
} |
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.
inputs are never empty (but they can be the empty object {}
)
.value
is only empty on the end of the stream in this case
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.
decided to guard on if (inputMessage.done)
instead 👍
supercedes #16
Subscriptions
We'd like the ability to express other procedure types aside from RPC and bidi streams. This PR introduces a new type of procedure type: subscriptions.
Types of procedures available:
To enable this, we needed to refactor how we type procedure calls. Currently, it looks like
client.<service name>.<procedure name>()
and then the client figures out whether it's actually an rpc/stream based on the number of arguments we call the proxy object withUnfortunately, this isn't scaleable / is kind of hacky as this only provides type-level safety. Because the client only has access to the structure of the server at compile-time (via the passed generic) to ensure safety, we have no knowledge of the server structure at run-time.
Turns out tRPC had it right and we should just use qualifiers to indicate the type of procedure so that the client can handle it properly:
.rpc(msg)
for rpc.stream()
for stream.subscribe(msg)
for subscriptionObservables
With subscriptions, we'd like a way to be able to watch something in a service's state for changes. We can do this pretty easily with EventEmitters but I think River is in a good place to start adding data structure primitives that allow for common usage patterns.
For example, here is a service that has a single
count
state entry that sends an update to all subscribed clients every time it's updated: