-
Notifications
You must be signed in to change notification settings - Fork 31
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
v1.2 API design #197
Comments
@tcard this all sounds good to me 👍 On the question around contexts, I discussed with @paddybyers and we concluded that the context should just be used as a timeout to unblock the goroutine but have no further side-effects. In particularly, if |
I've noticed that the spec has been liberally adding optional parameters in minor revisions (see ably/specification#41). If that's the case, then we need a consistent strategy to be able to incorporate new optional parameters as non-breaking changes. Unfortunately, I see no ideal option here:
3 seems to me the most balanced option. The "bag" should follow the functional options pattern described above. Getting some assurance from the spec that some methods won't ever add new optional parameters in minor versions. Then we can stop being so defensive for those methods. For the rest, we can go with 1-3. |
I think 3 is the way to go. So to clarify, I'm assuming you mean implement each method just with the required parameters, and complement those with a method suffixed with For example, assuming the following in the spec:
That would be implemented as: func (Foo) Bar(baz string)
type BarOptions func(Foo)
func WithQux(qux string) BarOptions
func (Foo) BarWithOptions(baz string, opts ...BarOptions) which would be used like: foo.Bar(baz)
foo.BarWithOptions(baz, WithQux(qux)) I think this is a good approach as it means we don't have to add support for options to every method, but can support options for those methods in the future by introducing the |
@tcard some things I've noticed from looking through the current API as it stands on the
|
@tcard it would be good to discuss and agree the approach for using functional options across the library, and in particular for instantiating a Realtime or REST client since that is going to be used by all users of this library. It seems we are in disagreement about the use of a struct to contain the functional options versus having them be functions on the package (e.g. see #141 (comment), #145 (comment) and #202 (comment)), which for
ably.NewRealtime(ably.NewClientOptions("<key>"))
ably.NewRealtime(ably.ClientOptions{}.Token("<token>"))
ably.NewRealtime(ably.WithKey("<key>"))
ably.NewRealtime(ably.Key("<key>"))
ably.NewRealtime(ably.WithToken("<token>"))
ably.NewRealtime(ably.Token("<token>")) To reply to your concern raised in #141 (comment):
I personally think the improved user-facing API I propose outweighs the problems you mention. To give an example, the popular grpc package has 40 dial options, all defined as functions on the package that return @gernest @paddybyers do you have any opinions on this? |
I am concerned about the potential for name conflicts if we have multiple APIs with variadic arguments of this type. That said, I think the scope of the API isn't going to change that much, and the number of types, and the opportunity for conflict is limited, so I would say we should go with whatever we think is the most idiomatic. |
There's some rationale at #146. But then, it was just an ad-hoc design at the time. We should revisit it now. By strictly following what's been discussed here, there should be a
Done at c57b530 and 6febe35 (part of ably/specification#83).
Yes, for sure, it's just pending. ably/specification#83 implements the subset we talked about so that users can start consuming the new API with some confidence: connection and channel events, message and presence publish and subscribe.
Just for clarification: it isn't a struct, it's a slice type of which each method appends an option and returns the resulting slice.
We can (and really should) add a prefix for options that aren't ClientOptions. I don't really feel strongly about keeping the slice design, and given you do, let's go that way. |
I've incorporated all this into the "Options" and "Optional arguments" in the issue text. @lmars @gernest @paddybyers Please take a look. |
Assuming you mean: channels.Get("name", ably.WithChannelCipher("..."))
channels.Get("name", ably.WithChannelCipherKey("...")) Sounds good to me.
Nice 👍
Ok, is there something tracking that? Just want to make sure we don't miss anything.
Ok great, so is the next step a PR with the options changed? Happy to help with that if needed, just let me know. |
About history API, by @lmars from #206 (comment):
|
➤ Lewis Marshall commented: I think this discussion has been had and agreements made, let’s open new tickets if anything else comes up. |
This issue tracks and discuss the changes to be made to the public API in order to make it 1.2 spec-compliant.
Subscriptions: from channels to callbacks
For some features, we make a long-lived subscription to a series of events that the caller receives. The spec says those events are delivered by calling a callback. In Go, instead, we were sending them to channels, from which user code receive events at their own. For 1.2 we're taking callbacks. See the relevant discussion.
So we go from something like:
To:
The list of such features is:
Options
We've decided to implement the classes from the spec that define options as functional options instead.
Each option is defined as a package-level function, with a
With
prefix. Additionally, exceptClientOptions
, each function has first the name of what they're options for as prefix (e. g.RestChannel.publish(params?)
→PublishBatchWithParams
). (Discussion below.)Optional arguments
Optional arguments are translated as functional options, as described above. (Discussion below.)
If an optional argument is itself an options aggregate per the spec, the options "collapse" into a single level.
io
actions: fromResult
to blocking callsThe spec marks with a
io
annotation values that result from making an IO operation (to the network, typically). In some environments (JavaScript, Cocoa) those are implemented by passing them to a callback or an equivalent Promise-style mechanism. In others, it's just a normal function call that blocks until the operation is complete and returns the value. If the user wants to do more than one thing at once, they use threads or some equivalent.In Go, it's common-place to do the latter: calls that hit the network block for the whole duration, and if you want asynchrony, you spawn goroutines. But we're using a two-stage Promise-like thing in which a method first initiates the operation on the background and returns
(ably.Result, error)
immediately; then, you can callably.Result.Wait()
to wait for the operation to complete, which may produce.This is weird for Go (see ably/docs#155) and we're switching to fully blocking calls for the duration of the operation.
So this:
Becomes:
Context
In Go, it has become common-place for IO operations to take a
context.Context
. For 1.2, we will implement theio
annotation by adding a firstcontext.Context
argument in such methods.For HTTP requests, the context is passed in the underlying
*http.Request
.For operations that result in WebSocket sends and receives, the situation is a bit more complex. A WebSocket is ultimately a
net.Conn
, which is just aio.Reader
andio.Writer
; those don't take contexts. So the context would only be used to unblock the call at the point it's waiting for a WebSocket receive, e. g. an ATTACHED message.This raises the question: do we then act as if the operation has failed entirely? E. g. do we go back to the INITIALIZED/DETACHED state from ATTACHING?
I'd say we can go with 2 and just implement a transparent way of dealing with the inconsistency, e. g. automatically detach once the ATTACHED arrives. This should be in the spec too.
Misc: names, argument order, etc.
The rest would just be straightforward: make sure we expose only the API from the spec, in the form and shape in which the spec defines it. This includes renaming, removing things or moving them to separate packages, reordering arguments, etc.
┆Issue is synchronized with this Jira Story by Unito
The text was updated successfully, but these errors were encountered: