Repository: ably/ably-go
Pull request #206 (2020-10-28 09:54:45)

Update README for 1.2 preview, with associated fixes
Also, I was anxious about fixing README without actually checking that the fixes are correct, so I added script to generate a Go test from it.

[Rendered README](https://github.com/ably/ably-go/blob/feature/1.2-preview-readme/README.md)


Patch:
diff --git a/Makefile b/Makefile
index 55ffafd..0c5c148 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
 all: vet build test
 
 vet:
-	go vet ./ably ./scripts
+	go vet ./ably ./scripts/...
 
 build:
 	go generate ./...
diff --git a/README.md b/README.md
index be42391..bc5b9eb 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-## [Ably Go](https://www.ably.io)
+## [Ably Go](https://www.ably.io) [![PkgGoDev](https://pkg.go.dev/badge/github.com/ably/ably-go@v1.2.0-apipreview.1/ably)](https://pkg.go.dev/github.com/ably/ably-go@v1.2.0-apipreview.1/ably)
 
 A Go client library for [www.ably.io](https://ably.io), the realtime messaging service.
 
@@ -8,49 +8,18 @@ A Go client library for [www.ably.io](https://ably.io), the realtime messaging s
 ~ $ go get -u github.com/ably/ably-go/ably
 ```
 
-## Feature support
-
-This library implements the Ably REST and Realtime client APIs.
-
-### REST API
-
-In respect of the Ably REST API, this library targets the Ably 1.1 client library specification,
-with some omissions as follows (see [the client library specification](https://www.ably.io/documentation/client-lib-development-guide/features) for specification references):
-
-| Feature |
-| --- |
-| [Push notifications admin API](https://www.ably.io/documentation/general/push/admin) |
-| [JWT authentication](https://www.ably.io/documentation/core-features/authentication#ably-jwt-process) |
-
-It is intended that this library is upgraded incrementally, with 1.1 feature support expanded in successive minor
-releases. If there are features that are currently missing that are a high priority for your use-case then please
-[contact Ably customer support](https://support.ably.io). Pull Requests are also welcomed.
-
-### Realtime API
-
-In respect of the Realtime API, this is an early experimental implementation that targets the (now superseded) 0.8
-library specification. This means that there are significant shortfalls in functionality; the principal issues are:
-
-- there is no channel `suspended` state; this means that the client will not automatically reattach to channels if a
-connection becomes `suspended` and then resumes, and presence members associated with the client will not be
-automatically re-entered;
-
-- transient realtime publishing is not supported, so a call to `publish()` on a realtime channel will trigger attachment
- of the channel;
-
-- inband reauthentication is not supported; expiring tokens will trigger a disconnection and resume of a realtime
-connection.
-
-As with the REST API, it is intended that this library is upgraded incrementally and brought into line with the 1.1
-specification. If there are features that are currently missing that are a high priority for your use-case then please
-[contact Ably customer support](https://support.ably.io). Pull Requests are also welcomed.
-
 ## Using the Realtime API
 
 ### Creating a client
 
+<!-- GO IMPORT "context" -->
+
+<!-- GO EXAMPLE
+ctx := context.Background()
+-->
+
 ```go
-client, err := ably.NewRealtime(ably.NewClientOptions("xxx:xxx"))
+client, err := ably.NewRealtime(ably.WithKey("xxx:xxx"))
 if err != nil {
 	panic(err)
 }
@@ -58,73 +27,83 @@ if err != nil {
 channel := client.Channels.Get("test")
 ```
 
-### Subscribing to a channel for all events
+### Subscribing to a channel for all messages
 
 ```go
-sub, err := channel.Subscribe()
+unsubscribe, err := channel.SubscribeAll(ctx, func(msg *ably.Message) {
+	fmt.Printf("Received message: name=%s data=%v\n", msg.Name, msg.Data)
+})
 if err != nil {
 	panic(err)
 }
-
-for msg := range sub.MessageChannel() {
-	fmt.Println("Received message:", msg)
-}
 ```
 
-### Subscribing to a channel for `EventName1` and `EventName2` events
+<!-- GO EXAMPLE
+unsubscribe()
+-->
+
+### Subscribing to a channel for `EventName1` and `EventName2` message names
 
 ```go
-sub, err := channel.Subscribe("EventName1", "EventName2")
+unsubscribe1, err := channel.Subscribe(ctx, "EventName1", func(msg *ably.Message) {
+	fmt.Printf("Received message: name=%s data=%v\n", msg.Name, msg.Data)
+})
 if err != nil {
 	panic(err)
 }
 
-for msg := range sub.MessageChannel() {
-	fmt.Println("Received message:", msg)
+unsubscribe2, err := channel.Subscribe(ctx, "EventName2", func(msg *ably.Message) {
+	fmt.Printf("Received message: name=%s data=%v\n", msg.Name, msg.Data)
+})
+if err != nil {
+	panic(err)
 }
 ```
 
+<!-- GO EXAMPLE
+unsubscribe1()
+unsubscribe2()
+-->
+
 ### Publishing to a channel
 
 ```go
-// send request to a server
-res, err := channel.Publish("EventName1", "EventData1")
+err = channel.Publish(ctx, "EventName1", "EventData1")
 if err != nil {
 	panic(err)
 }
-
-// await confirmation
-if err = res.Wait(); err != nil {
-	panic(err)
-}
 ```
 
 ### Announcing presence on a channel
 
 ```go
-// send request to a server
-res, err := channel.Presence.Enter("presence data")
+err = channel.Presence.Enter(ctx, "presence data")
 if err != nil {
 	panic(err)
 }
+```
+
+### Announcing presence on a channel on behalf of other client
 
-// await confirmation
-if err = res.Wait(); err != nil {
+```go
+err = channel.Presence.EnterClient(ctx, "clientID", "presence data")
+if err != nil {
 	panic(err)
 }
 ```
 
-### Announcing presence on a channel on behalf of other client
+### Updating and leaving presence
 
 ```go
-// send request to a server
-res, err := channel.Presence.EnterClient("clientID", "presence data")
+// Update also has an UpdateClient variant.
+err = channel.Presence.Update(ctx, "new presence data")
 if err != nil {
 	panic(err)
 }
 
-// await confirmation
-if err = res.Wait(); err != nil {
+// Leave also has an LeaveClient variant.
+err = channel.Presence.Leave(ctx, "last presence data")
+if err != nil {
 	panic(err)
 }
 ```
@@ -132,7 +111,7 @@ if err = res.Wait(); err != nil {
 ### Getting all clients present on a channel
 
 ```go
-clients, err := channel.Presence.Get(true)
+clients, err := channel.Presence.Get(ctx)
 if err != nil {
 	panic(err)
 }
@@ -145,37 +124,45 @@ for _, client := range clients {
 ### Subscribing to all presence messages
 
 ```go
-sub, err := channel.Presence.Subscribe()
+unsubscribe, err = channel.Presence.SubscribeAll(ctx, func(msg *ably.PresenceMessage) {
+	fmt.Printf("Presence event: action=%v data=%v", msg.Action, msg.Data)
+})
 if err != nil {
 	panic(err)
 }
-
-for msg := range sub.PresenceChannel() {
-	fmt.Println("Presence event:", msg)
-}
 ```
 
+<!-- GO EXAMPLE
+unsubscribe()
+-->
+
 ### Subscribing to 'Enter' presence messages only
 
 ```go
-sub, err := channel.Presence.Subscribe(proto.PresenceEnter)
+unsubscribe, err = channel.Presence.Subscribe(ctx, ably.PresenceActionEnter, func(msg *ably.PresenceMessage) {
+	fmt.Printf("Presence event: action=%v data=%v", msg.Action, msg.Data)
+})
 if err != nil {
 	panic(err)
 }
-
-for msg := range sub.PresenceChannel() {
-	fmt.Println("Presence event:", msg)
-}
 ```
 
+<!-- GO EXAMPLE
+unsubscribe()
+-->
+
 ## Using the REST API
 
 ### Introduction
 
 All examples assume a client and/or channel has been created as follows:
 
+<!-- GO EXAMPLE
+{
+-->
+
 ```go
-client, err := ably.NewREST(ably.NewClientOptions("xxx:xxx"))
+client, err := ably.NewREST(ably.WithKey("xxx:xxx"))
 if err != nil {
 	panic(err)
 }
@@ -186,7 +173,16 @@ channel := client.Channels.Get("test")
 ### Publishing a message to a channel
 
 ```go
-err = channel.Publish("HelloEvent", "Hello!")
+err = channel.Publish(ctx, "HelloEvent", "Hello!")
+if err != nil {
+	panic(err)
+}
+
+// You can also publish a batch of messages in a single request.
+err = channel.PublishBatch(ctx, []*ably.Message{
+	{Name: "HelloEvent", Data: "Hello!"},
+	{Name: "ByeEvent", Data: "Bye!"},
+})
 if err != nil {
 	panic(err)
 }
@@ -196,7 +192,7 @@ if err != nil {
 
 ```go
 page, err := channel.History(nil)
-for ; err == nil; page, err = page.Next() {
+for ; err == nil && page != nil; page, err = page.Next() {
 	for _, message := range page.Messages() {
 		fmt.Println(message)
 	}
@@ -209,8 +205,8 @@ if err != nil {
 ### Presence on a channel
 
 ```go
-page, err := channel.Presence.Get(nil)
-for ; err == nil; page, err = page.Next() {
+page, err = channel.Presence.Get(nil)
+for ; err == nil && page != nil; page, err = page.Next() {
 	for _, presence := range page.PresenceMessages() {
 		fmt.Println(presence)
 	}
@@ -223,8 +219,8 @@ if err != nil {
 ### Querying the Presence History
 
 ```go
-page, err := channel.Presence.History(nil)
-for ; err == nil; page, err = page.Next() {
+page, err = channel.Presence.History(nil)
+for ; err == nil && page != nil; page, err = page.Next() {
 	for _, presence := range page.PresenceMessages() {
 		fmt.Println(presence)
 	}
@@ -234,18 +230,11 @@ if err != nil {
 }
 ```
 
-### Generate Token and Token Request
-
-```go
-client.Auth.RequestToken()
-client.Auth.CreateTokenRequest()
-```
-
 ### Fetching your application's stats
 
 ```go
-page, err := client.Stats(&ably.PaginateParams{})
-for ; err == nil; page, err = page.Next() {
+page, err = client.Stats(&ably.PaginateParams{})
+for ; err == nil && page != nil; page, err = page.Next() {
 	for _, stat := range page.Stats() {
 		fmt.Println(stat)
 	}
@@ -255,20 +244,45 @@ if err != nil {
 }
 ```
 
-## Known limitations (work in progress)
+<!-- GO EXAMPLE
+}
+-->
+
+## Feature support
+
+This library targets the Ably 1.2 [client library specification](https://www.ably.io/documentation/client-lib-development-guide/features).
+
+### Known limitations
+
+As of release 1.2.0, the following are not implemented and will be covered in future 1.2.x releases. If there are features that are currently missing that are a high priority for your use-case then please [contact Ably customer support](https://support.ably.io). Pull Requests are also welcomed.
+
+### REST API
+
+- [Push notifications admin API](https://www.ably.io/documentation/general/push/admin) is not implemented.
+
+- [JWT authentication](https://www.ably.io/documentation/core-features/authentication#ably-jwt-process) is not implemented.
+
+### Realtime API
+
+- There is no channel `suspended` state; this means that the client will not automatically reattach to channels if a
+connection becomes `suspended` and then resumes, and presence members associated with the client will not be
+automatically re-entered.
+
+- Transient realtime publishing is not supported, so a call to `publish()` on a realtime channel will trigger attachment
+ of the channel.
+
+- Inband reauthentication is not supported; expiring tokens will trigger a disconnection and resume of a realtime
+connection.
 
-As the library is actively developed couple of features are not there yet:
+- Realtime connection failure handling is partially implemented.
 
-- Realtime connection recovery is not implemented
-- Realtime connection failure handling is not implemented
-- ChannelsOptions and CipherParams are not supported when creating a Channel
-- Realtime Ping function is not implemented
+- Realtime Ping function is not implemented.
 
 ## Release process
 
-This library uses [semantic versioning](http://semver.org/). For each release, the following needs to be done:
+Starting with release 1.2, this library uses [semantic versioning](http://semver.org/). For each release, the following needs to be done:
 
-* Create a branch for the release, named like `release/1.1.6`
+* Create a branch for the release, named like `release/1.2.0`
 * Replace all references of the current version number with the new version number and commit the changes
 * Run [`github_changelog_generator`](https://github.com/github-changelog-generator/github-changelog-generator) to automate the update of the [CHANGELOG](./CHANGELOG.md). This may require some manual intervention, both in terms of how the command is run and how the change log file is modified. Your mileage may vary:
     * The command you will need to run will look something like this: `github_changelog_generator -u ably -p ably-go --since-tag v1.1.4 --output delta.md`
@@ -279,7 +293,7 @@ This library uses [semantic versioning](http://semver.org/). For each release, t
 * Commit [CHANGELOG](./CHANGELOG.md)
 * Make a PR against `main`
 * Once the PR is approved, merge it into `main`
-* Add a tag to the new `main` head commit and push to origin such as `git tag v1.1.6 && git push origin v1.1.6`
+* Add a tag to the new `main` head commit and push to origin such as `git tag v1.2.0 && git push origin v1.2.0`
 
 ## Support and feedback
 
diff --git a/ably/ably.go b/ably/ably.go
index 94dd404..0a05f2a 100644
--- a/ably/ably.go
+++ b/ably/ably.go
@@ -1,7 +1,5 @@
 package ably
 
-//go:generate go run ../scripts/errors.go -json ../common/protocol/errors.json -o errors.go
-
 import (
 	"crypto/rand"
 	"encoding/hex"
diff --git a/ably/error.go b/ably/error.go
index 22e401c..9616d3e 100644
--- a/ably/error.go
+++ b/ably/error.go
@@ -86,8 +86,6 @@ func (e ErrorInfo) Message() string {
 
 func newError(defaultCode ErrorCode, err error) *ErrorInfo {
 	switch err := err.(type) {
-	case nil:
-		return nil
 	case *ErrorInfo:
 		return err
 	case net.Error:
diff --git a/ably/generate.go b/ably/generate.go
index b239791..bd987ae 100644
--- a/ably/generate.go
+++ b/ably/generate.go
@@ -1,3 +1,4 @@
 package ably
 
-//go:generate go run ../scripts/errors.go -json ../common/protocol/errors.json -o errors.go
+//go:generate go run ../scripts/errors/main.go -json ../common/protocol/errors.json -o errors.go
+//go:generate go run ../scripts/test_readme_examples/main.go
diff --git a/ably/paginated_result.go b/ably/paginated_result.go
index ee5f0c3..a236176 100644
--- a/ably/paginated_result.go
+++ b/ably/paginated_result.go
@@ -178,10 +178,12 @@ func copyHeader(dest, src http.Header) {
 // Next returns the path to the next page as found in the response headers.
 // The response headers from the REST API contains a relative link to the next result.
 // (Link: <./path>; rel="next").
+//
+// If there is no next link, both return values are nil.
 func (p *PaginatedResult) Next() (*PaginatedResult, error) {
 	nextPath, ok := p.paginationHeaders()["next"]
 	if !ok {
-		return nil, newErrorf(ErrProtocolError, "no next page after %q", p.path)
+		return nil, nil
 	}
 	nextPage := p.buildPath(p.path, nextPath)
 	req := p.req
diff --git a/ably/realtime_presence.go b/ably/realtime_presence.go
index 6acaf8f..3d179e2 100644
--- a/ably/realtime_presence.go
+++ b/ably/realtime_presence.go
@@ -198,17 +198,59 @@ func (pres *RealtimePresence) processIncomingMessage(msg *proto.ProtocolMessage,
 	}
 }
 
-// Get returns a list of current members on the channel.
+// Get returns a list of current members on the channel, attaching the channel
+// first is needed.
 //
-// If wait is true it blocks until undergoing sync operation completes.
-// If wait is false or sync already completed, the function returns immediately.
-func (pres *RealtimePresence) Get(wait bool) ([]*proto.PresenceMessage, error) {
-	if _, err := pres.channel.attach(false); err != nil {
+// If the context is canceled before the operation finishes, the call
+// returns with an error, but the operation carries on in the background and
+// the channel may eventually be attached anyway.
+func (pres *RealtimePresence) Get(ctx context.Context) ([]*proto.PresenceMessage, error) {
+	return pres.GetWithOptions(ctx)
+}
+
+// A PresenceGetOption is an optional parameter for
+// RealtimePresence.GetWithOptions.
+type PresenceGetOption func(*presenceGetOptions)
+
+// PresenceGetWithWaitForSync, if true, makes GetWithOptions wait until the
+// presence information is fully synchronized with the server before returning.
+// It defaults to true.
+func PresenceGetWithWaitForSync(wait bool) PresenceGetOption {
+	return func(o *presenceGetOptions) {
+		o.waitForSync = true
+	}
+}
+
+type presenceGetOptions struct {
+	waitForSync bool
+}
+
+func (o *presenceGetOptions) applyWithDefaults(options ...PresenceGetOption) {
+	o.waitForSync = true
+	for _, opt := range options {
+		opt(o)
+	}
+}
+
+// GetWithOptions is Get with optional parameters.
+func (pres *RealtimePresence) GetWithOptions(ctx context.Context, options ...PresenceGetOption) ([]*proto.PresenceMessage, error) {
+	var opts presenceGetOptions
+	opts.applyWithDefaults(options...)
+
+	res, err := pres.channel.attach(true)
+	if err != nil {
 		return nil, err
 	}
-	if wait {
+	// TODO: Don't ignore context.
+	err = res.Wait()
+	if err != nil {
+		return nil, err
+	}
+
+	if opts.waitForSync {
 		pres.syncWait()
 	}
+
 	pres.mtx.Lock()
 	defer pres.mtx.Unlock()
 	members := make([]*proto.PresenceMessage, 0, len(pres.members))
@@ -295,39 +337,55 @@ func (pres *RealtimePresence) SubscribeAll(ctx context.Context, handle func(*Pre
 
 // Enter announces presence of the current client with an enter message
 // for the associated channel.
-func (pres *RealtimePresence) Enter(data string) (Result, error) {
+//
+// If the context is canceled before the operation finishes, the call
+// returns with an error, but the operation carries on in the background and
+// presence state may eventually be updated anyway.
+func (pres *RealtimePresence) Enter(ctx context.Context, data interface{}) error {
 	clientID := pres.auth().ClientID()
 	if clientID == "" {
-		return nil, newError(91000, nil)
+		return newError(91000, nil)
 	}
-	return pres.EnterClient(clientID, data)
+	return pres.EnterClient(ctx, clientID, data)
 }
 
 // Update announces an updated presence message for the current client.
 //
 // If the current client is not present on the channel, Update will
 // behave as Enter method.
-func (pres *RealtimePresence) Update(data string) (Result, error) {
+//
+// If the context is canceled before the operation finishes, the call
+// returns with an error, but the operation carries on in the background and
+// presence state may eventually be updated anyway.
+func (pres *RealtimePresence) Update(ctx context.Context, data interface{}) error {
 	clientID := pres.auth().ClientID()
 	if clientID == "" {
-		return nil, newError(91000, nil)
+		return newError(91000, nil)
 	}
-	return pres.UpdateClient(clientID, data)
+	return pres.UpdateClient(ctx, clientID, data)
 }
 
 // Leave announces current client leave the channel altogether with a leave
 // message if data is non-empty.
-func (pres *RealtimePresence) Leave(data string) (Result, error) {
+//
+// If the context is canceled before the operation finishes, the call
+// returns with an error, but the operation carries on in the background and
+// presence state may eventually be updated anyway.
+func (pres *RealtimePresence) Leave(ctx context.Context, data interface{}) error {
 	clientID := pres.auth().ClientID()
 	if clientID == "" {
-		return nil, newError(91000, nil)
+		return newError(91000, nil)
 	}
-	return pres.LeaveClient(clientID, data)
+	return pres.LeaveClient(ctx, clientID, data)
 }
 
 // EnterClient announces presence of the given clientID altogether with an enter
 // message for the associated channel.
-func (pres *RealtimePresence) EnterClient(clientID string, data interface{}) (Result, error) {
+//
+// If the context is canceled before the operation finishes, the call
+// returns with an error, but the operation carries on in the background and
+// presence state may eventually be updated anyway.
+func (pres *RealtimePresence) EnterClient(ctx context.Context, clientID string, data interface{}) error {
 	pres.mtx.Lock()
 	pres.data = data
 	pres.state = proto.PresenceEnter
@@ -337,7 +395,12 @@ func (pres *RealtimePresence) EnterClient(clientID string, data interface{}) (Re
 	}
 	msg.Data = data
 	msg.ClientID = clientID
-	return pres.send(&msg)
+	res, err := pres.send(&msg)
+	if err != nil {
+		return err
+	}
+	// TODO: Don't ignore context.
+	return res.Wait()
 }
 
 func nonnil(a, b interface{}) interface{} {
@@ -351,12 +414,16 @@ func nonnil(a, b interface{}) interface{} {
 //
 // If the given clientID is not present on the channel, Update will
 // behave as Enter method.
-func (pres *RealtimePresence) UpdateClient(clientID string, data interface{}) (Result, error) {
+//
+// If the context is canceled before the operation finishes, the call
+// returns with an error, but the operation carries on in the background and
+// presence data may eventually be updated anyway.
+func (pres *RealtimePresence) UpdateClient(ctx context.Context, clientID string, data interface{}) error {
 	pres.mtx.Lock()
 	if pres.state != proto.PresenceEnter {
 		oldData := pres.data
 		pres.mtx.Unlock()
-		return pres.EnterClient(clientID, nonnil(data, oldData))
+		return pres.EnterClient(ctx, clientID, nonnil(data, oldData))
 	}
 	pres.data = data
 	pres.mtx.Unlock()
@@ -365,16 +432,25 @@ func (pres *RealtimePresence) UpdateClient(clientID string, data interface{}) (R
 	}
 	msg.ClientID = clientID
 	msg.Data = data
-	return pres.send(&msg)
+	res, err := pres.send(&msg)
+	if err != nil {
+		return err
+	}
+	// TODO: Don't ignore context.
+	return res.Wait()
 }
 
 // LeaveClient announces the given clientID leave the associated channel altogether
 // with a leave message if data is non-empty.
-func (pres *RealtimePresence) LeaveClient(clientID string, data interface{}) (Result, error) {
+//
+// If the context is canceled before the operation finishes, the call
+// returns with an error, but the operation carries on in the background and
+// presence data may eventually be updated anyway.
+func (pres *RealtimePresence) LeaveClient(ctx context.Context, clientID string, data interface{}) error {
 	pres.mtx.Lock()
 	if pres.state != proto.PresenceEnter {
 		pres.mtx.Unlock()
-		return nil, newError(91001, nil)
+		return newError(91001, nil)
 	}
 	if pres.data == nil {
 		pres.data = data
@@ -386,7 +462,12 @@ func (pres *RealtimePresence) LeaveClient(clientID string, data interface{}) (Re
 	}
 	msg.ClientID = clientID
 	msg.Data = data
-	return pres.send(&msg)
+	res, err := pres.send(&msg)
+	if err != nil {
+		return err
+	}
+	// TODO: Don't ignore context.
+	return res.Wait()
 }
 
 func (pres *RealtimePresence) auth() *Auth {
diff --git a/ably/state.go b/ably/state.go
index 43abec2..5086092 100644
--- a/ably/state.go
+++ b/ably/state.go
@@ -62,6 +62,9 @@ func connStateError(state ConnectionState, err error) *ErrorInfo {
 		}
 		err = &e
 	}
+	if err == nil {
+		return nil
+	}
 	return newError(0, err)
 }
 
@@ -81,6 +84,9 @@ func channelStateError(state ChannelState, err error) *ErrorInfo {
 		}
 		err = &e
 	}
+	if err == nil {
+		return nil
+	}
 	return newError(0, err)
 }
 


Test patch:
diff --git a/ably/readme_examples_test.go b/ably/readme_examples_test.go
new file mode 100644
index 0000000..f867f79
--- /dev/null
+++ b/ably/readme_examples_test.go
@@ -0,0 +1,185 @@
+// Generated by test_readme_examples. DO NOT EDIT
+package ably_test
+
+import "testing"
+import "github.com/ably/ably-go/ably"
+import "github.com/ably/ably-go/ably/ablytest"
+
+/* README.md:15 */ import "context"
+
+func TestReadmeExamples(t *testing.T) {
+	t.Parallel()
+
+	fmt := struct {
+		Println func(a ...interface{}) (n int, err error)
+		Printf  func(s string, a ...interface{}) (n int, err error)
+	}{
+		Println: func(a ...interface{}) (n int, err error) { return 0, nil },
+		Printf:  func(s string, a ...interface{}) (n int, err error) { return 0, nil },
+	}
+
+	app := ablytest.MustSandbox(nil)
+	defer safeclose(t, app)
+	/* README.md:18 */ ctx := context.Background()
+	/* README.md:22 */ client, err := ably.NewRealtime(app.Options(ably.WithClientID("clientID"))...)
+	/* README.md:23 */ if err != nil {
+		/* README.md:24 */ panic(err)
+		/* README.md:25 */
+	}
+	/* README.md:27 */ channel := client.Channels.Get("test")
+	/* README.md:33 */ unsubscribe, err := channel.SubscribeAll(ctx, func(msg *ably.Message) {
+		/* README.md:34 */ fmt.Printf("Received message: name=%s data=%v\n", msg.Name, msg.Data)
+		/* README.md:35 */
+	})
+	/* README.md:36 */ if err != nil {
+		/* README.md:37 */ panic(err)
+		/* README.md:38 */
+	}
+	/* README.md:42 */ unsubscribe()
+	/* README.md:48 */ unsubscribe1, err := channel.Subscribe(ctx, "EventName1", func(msg *ably.Message) {
+		/* README.md:49 */ fmt.Printf("Received message: name=%s data=%v\n", msg.Name, msg.Data)
+		/* README.md:50 */
+	})
+	/* README.md:51 */ if err != nil {
+		/* README.md:52 */ panic(err)
+		/* README.md:53 */
+	}
+	/* README.md:55 */ unsubscribe2, err := channel.Subscribe(ctx, "EventName2", func(msg *ably.Message) {
+		/* README.md:56 */ fmt.Printf("Received message: name=%s data=%v\n", msg.Name, msg.Data)
+		/* README.md:57 */
+	})
+	/* README.md:58 */ if err != nil {
+		/* README.md:59 */ panic(err)
+		/* README.md:60 */
+	}
+	/* README.md:64 */ unsubscribe1()
+	/* README.md:65 */ unsubscribe2()
+	/* README.md:71 */ err = channel.Publish(ctx, "EventName1", "EventData1")
+	/* README.md:72 */ if err != nil {
+		/* README.md:73 */ panic(err)
+		/* README.md:74 */
+	}
+	/* README.md:80 */ err = channel.Presence.Enter(ctx, "presence data")
+	/* README.md:81 */ if err != nil {
+		/* README.md:82 */ panic(err)
+		/* README.md:83 */
+	}
+	/* README.md:89 */ err = channel.Presence.EnterClient(ctx, "clientID", "presence data")
+	/* README.md:90 */ if err != nil {
+		/* README.md:91 */ panic(err)
+		/* README.md:92 */
+	}
+	/* README.md:98 */ // Update also has an UpdateClient variant.
+	/* README.md:99 */
+	err = channel.Presence.Update(ctx, "new presence data")
+	/* README.md:100 */ if err != nil {
+		/* README.md:101 */ panic(err)
+		/* README.md:102 */
+	}
+	/* README.md:104 */ // Leave also has an LeaveClient variant.
+	/* README.md:105 */
+	err = channel.Presence.Leave(ctx, "last presence data")
+	/* README.md:106 */ if err != nil {
+		/* README.md:107 */ panic(err)
+		/* README.md:108 */
+	}
+	/* README.md:114 */ clients, err := channel.Presence.Get(ctx)
+	/* README.md:115 */ if err != nil {
+		/* README.md:116 */ panic(err)
+		/* README.md:117 */
+	}
+	/* README.md:119 */ for _, client := range clients {
+		/* README.md:120 */ fmt.Println("Present client:", client)
+		/* README.md:121 */
+	}
+	/* README.md:127 */ unsubscribe, err = channel.Presence.SubscribeAll(ctx, func(msg *ably.PresenceMessage) {
+		/* README.md:128 */ fmt.Printf("Presence event: action=%v data=%v", msg.Action, msg.Data)
+		/* README.md:129 */
+	})
+	/* README.md:130 */ if err != nil {
+		/* README.md:131 */ panic(err)
+		/* README.md:132 */
+	}
+	/* README.md:136 */ unsubscribe()
+	/* README.md:142 */ unsubscribe, err = channel.Presence.Subscribe(ctx, ably.PresenceActionEnter, func(msg *ably.PresenceMessage) {
+		/* README.md:143 */ fmt.Printf("Presence event: action=%v data=%v", msg.Action, msg.Data)
+		/* README.md:144 */
+	})
+	/* README.md:145 */ if err != nil {
+		/* README.md:146 */ panic(err)
+		/* README.md:147 */
+	}
+	/* README.md:151 */ unsubscribe()
+	/* README.md:161 */ {
+		/* README.md:165 */ client, err := ably.NewREST(app.Options(ably.WithClientID("clientID"))...)
+		/* README.md:166 */ if err != nil {
+			/* README.md:167 */ panic(err)
+			/* README.md:168 */
+		}
+		/* README.md:170 */ channel := client.Channels.Get("test")
+		/* README.md:176 */ err = channel.Publish(ctx, "HelloEvent", "Hello!")
+		/* README.md:177 */ if err != nil {
+			/* README.md:178 */ panic(err)
+			/* README.md:179 */
+		}
+		/* README.md:181 */ // You can also publish a batch of messages in a single request.
+		/* README.md:182 */
+		err = channel.PublishBatch(ctx, []*ably.Message{
+			/* README.md:183 */ {Name: "HelloEvent", Data: "Hello!"},
+			/* README.md:184 */ {Name: "ByeEvent", Data: "Bye!"},
+			/* README.md:185 */})
+		/* README.md:186 */ if err != nil {
+			/* README.md:187 */ panic(err)
+			/* README.md:188 */
+		}
+		/* README.md:194 */ page, err := channel.History(nil)
+		/* README.md:195 */ for ; err == nil && page != nil; page, err = page.Next() {
+			/* README.md:196 */ for _, message := range page.Messages() {
+				/* README.md:197 */ fmt.Println(message)
+				/* README.md:198 */
+			}
+			/* README.md:199 */
+		}
+		/* README.md:200 */ if err != nil {
+			/* README.md:201 */ panic(err)
+			/* README.md:202 */
+		}
+		/* README.md:208 */ page, err = channel.Presence.Get(nil)
+		/* README.md:209 */ for ; err == nil && page != nil; page, err = page.Next() {
+			/* README.md:210 */ for _, presence := range page.PresenceMessages() {
+				/* README.md:211 */ fmt.Println(presence)
+				/* README.md:212 */
+			}
+			/* README.md:213 */
+		}
+		/* README.md:214 */ if err != nil {
+			/* README.md:215 */ panic(err)
+			/* README.md:216 */
+		}
+		/* README.md:222 */ page, err = channel.Presence.History(nil)
+		/* README.md:223 */ for ; err == nil && page != nil; page, err = page.Next() {
+			/* README.md:224 */ for _, presence := range page.PresenceMessages() {
+				/* README.md:225 */ fmt.Println(presence)
+				/* README.md:226 */
+			}
+			/* README.md:227 */
+		}
+		/* README.md:228 */ if err != nil {
+			/* README.md:229 */ panic(err)
+			/* README.md:230 */
+		}
+		/* README.md:236 */ page, err = client.Stats(&ably.PaginateParams{})
+		/* README.md:237 */ for ; err == nil && page != nil; page, err = page.Next() {
+			/* README.md:238 */ for _, stat := range page.Stats() {
+				/* README.md:239 */ fmt.Println(stat)
+				/* README.md:240 */
+			}
+			/* README.md:241 */
+		}
+		/* README.md:242 */ if err != nil {
+			/* README.md:243 */ panic(err)
+			/* README.md:244 */
+		}
+		/* README.md:248 */
+	}
+}
diff --git a/ably/realtime_client_test.go b/ably/realtime_client_test.go
index 1b7da75..8d32ab8 100644
--- a/ably/realtime_client_test.go
+++ b/ably/realtime_client_test.go
@@ -113,7 +113,7 @@ func TestRealtime_multiple(t *testing.T) {
 				channel := c.Channels.Get(fmt.Sprintf("client/%d/channel/%d", i, j))
 				rg.GoAdd(func() error { return channel.Attach(context.Background()) })
 				rg.GoAdd(func() error { return channel.Attach(context.Background()) })
-				rg.Add(channel.Presence.Enter(""))
+				rg.GoAdd(func() error { return channel.Presence.Enter(context.Background(), "") })
 			}
 			if err := rg.Wait(); err != nil {
 				all.Add(nil, err)
@@ -130,7 +130,7 @@ func TestRealtime_multiple(t *testing.T) {
 			}
 			for j := 0; j < 10; j++ {
 				channel := c.Channels.Get(fmt.Sprintf("client/%d/channel/%d", i, j))
-				rg.Add(channel.Presence.Leave(""))
+				rg.GoAdd(func() error { return channel.Presence.Leave(context.Background(), "") })
 				rg.GoAdd(func() error { return channel.Detach(context.Background()) })
 				rg.GoAdd(func() error { return channel.Detach(context.Background()) })
 			}
diff --git a/ably/realtime_presence_test.go b/ably/realtime_presence_test.go
index cc4563c..64ddedd 100644
--- a/ably/realtime_presence_test.go
+++ b/ably/realtime_presence_test.go
@@ -45,7 +45,7 @@ func TestRealtimePresence_Sync(t *testing.T) {
 	app, client := ablytest.NewRealtime(nil...)
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 
-	members, err := client.Channels.Get("persisted:presence_fixtures").Presence.Get(true)
+	members, err := client.Channels.Get("persisted:presence_fixtures").Presence.Get(context.Background())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -83,7 +83,8 @@ func TestRealtimePresence_Sync250(t *testing.T) {
 	var rg ablytest.ResultGroup
 	var clients = generateClients(250)
 	for _, client := range clients {
-		rg.Add(channel1.Presence.EnterClient(client, ""))
+		client := client
+		rg.GoAdd(func() error { return channel1.Presence.EnterClient(context.Background(), client, "") })
 	}
 	if err := rg.Wait(); err != nil {
 		t.Fatalf("rg.Wait()=%v", err)
@@ -103,7 +104,7 @@ func TestRealtimePresence_Sync250(t *testing.T) {
 	if err = contains(members2, clients...); err != nil {
 		t.Fatalf("members2: %v", err)
 	}
-	members3, err := client3.Channels.Get("sync250").Presence.Get(true)
+	members3, err := client3.Channels.Get("sync250").Presence.Get(context.Background())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -131,7 +132,7 @@ func TestRealtimePresence_EnsureChannelIsAttached(t *testing.T) {
 	if err := ablytest.ConnWaiter(client, client.Connect, ably.ConnectionEventConnected).Wait(); err != nil {
 		t.Fatal(err)
 	}
-	members, err := channel.Presence.Get(true)
+	members, err := channel.Presence.Get(context.Background())
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/ably/rest_presence_test.go b/ably/rest_presence_test.go
index ae7f8c6..bf410a8 100644
--- a/ably/rest_presence_test.go
+++ b/ably/rest_presence_test.go
@@ -61,9 +61,12 @@ func TestChannel_Presence(t *testing.T) {
 				ts.Errorf("expected %d items got %d", limit, n)
 			}
 
-			_, err = page2.Next()
-			if err == nil {
-				ts.Fatal("expected an error")
+			noPage, err := page2.Next()
+			if err != nil {
+				ts.Fatal(err)
+			}
+			if noPage != nil {
+				ts.Fatal("no more pages expected")
 			}
 		})
 	})
diff --git a/scripts/errors.go b/scripts/errors/main.go
similarity index 100%
rename from scripts/errors.go
rename to scripts/errors/main.go
diff --git a/scripts/test_readme_examples/main.go b/scripts/test_readme_examples/main.go
new file mode 100644
index 0000000..a2ef66f
--- /dev/null
+++ b/scripts/test_readme_examples/main.go
@@ -0,0 +1,215 @@
+// Command test_readme_examples, when run at the /ably directory, generates
+// a readme_examples_test.go file with the examples from README.md.
+//
+// Packages to import can be added at the top with comments like:
+//
+// 	<!-- GO IMPORT "path/to/import" -->
+//
+// Also, Go code that isn't part of the visible example but needs to be included
+// in the resulting test can be added like:
+//
+// 	<!-- GO EXAMPLE
+// 	... code goes here ...
+// 	-->
+//
+// README code may use fmt.Println and fmt.Printf, which are mocked as no-ops
+// in the actual test.
+//
+// The test runs against a sandbox app. When copying README code, this:
+//
+// 	ably.WithKey("xxx:xxx")
+//
+// is replaced with:
+//
+// 	app.Options(ably.WithClientID("clientID"))...
+//
+// So that the REST and Realtime instances can run against the app, without
+// polluting the README with test plumbing.
+//
+// The generated Go code includes references to the README.md line from which
+// it's derived, for debugging.
+package main
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"go/format"
+	"io"
+	"os"
+	"strings"
+)
+
+func main() {
+	src, err := os.Open("../README.md")
+	try(err)
+	defer src.Close()
+
+	var out bytes.Buffer
+
+	(&writer{}).write(&out, src)
+	b, err := format.Source(out.Bytes())
+	try(err)
+
+	dst, err := os.Create("readme_examples_test.go")
+	try(err)
+	defer dst.Close()
+
+	try(dst.Write(b))
+}
+
+type writer struct {
+	out io.Writer
+	in  *bufio.Reader
+
+	lineNum     int
+	pendingLine string
+	eof         bool
+}
+
+type state func() state
+
+func (w *writer) write(out io.Writer, src io.Reader) {
+	w.out = out
+	w.in = bufio.NewReader(src)
+
+	var state state = w.writeHeader
+	for state != nil {
+		state = state()
+	}
+}
+
+const (
+	goImportPrefix = "<!-- GO IMPORT "
+	goCommentBegin = "<!-- GO EXAMPLE"
+	goBlockBegin   = "```go"
+)
+
+func (w *writer) writeHeader() state {
+	w.writeln(`// Generated by test_readme_examples. DO NOT EDIT`)
+	w.writeln(`package ably_test`)
+	w.writeln(`import "testing"`)
+	w.writeln(`import "github.com/ably/ably-go/ably"`)
+	w.writeln(`import "github.com/ably/ably-go/ably/ablytest"`)
+
+loop:
+	for {
+		lineNum, l, ok := w.nextLine()
+		if !ok {
+			return nil
+		}
+
+		switch {
+		case l == goCommentBegin:
+			w.pendingLine = l
+			break loop
+		case l == goBlockBegin:
+			w.pendingLine = l
+			break loop
+		case !strings.HasPrefix(l, goImportPrefix):
+			continue
+		}
+
+		l = l[len(goImportPrefix):] // remove prefix
+		l = l[:len(l)-len(" -->")]  // remove suffix
+		w.writeln(`/*`, readmeRef(lineNum), `*/`, `import`, l)
+	}
+
+	w.writeln(`func TestReadmeExamples(t *testing.T) {`)
+	w.writeln(`t.Parallel()`)
+
+	// Fake fmt so that tests don't actually print things.
+	w.writeln(`
+		fmt := struct {
+			Println func(a ...interface{}) (n int, err error)
+			Printf func(s string, a ...interface{}) (n int, err error)
+		}{
+			Println: func(a ...interface{}) (n int, err error) { return 0, nil },
+			Printf: func(s string, a ...interface{}) (n int, err error) { return 0, nil },
+		}
+	`)
+
+	w.writeln(`app := ablytest.MustSandbox(nil)`)
+	w.writeln(`defer safeclose(t, app)`)
+
+	return w.writeMain
+}
+
+func (w *writer) writeMain() state {
+	for {
+		_, l, ok := w.nextLine()
+		if !ok {
+			w.writeln(`}`)
+			return nil
+		}
+
+		switch l {
+		case goCommentBegin:
+			return w.writeGo(`-->`)
+		case goBlockBegin:
+			return w.writeGo("```")
+		}
+	}
+}
+
+func (w *writer) writeGo(endBlock string) state {
+	for {
+		lineNum, l, ok := w.nextLine()
+		if !ok {
+			break
+		}
+
+		if l == endBlock {
+			break
+		}
+
+		l = strings.ReplaceAll(l, `ably.WithKey("xxx:xxx")`, `app.Options(ably.WithClientID("clientID"))...`)
+
+		w.writeln(`/*`, readmeRef(lineNum), `*/`, l)
+	}
+
+	return w.writeMain
+}
+
+func (w *writer) nextLine() (int, string, bool) {
+	if l := w.pendingLine; l != "" {
+		w.pendingLine = ""
+		return w.lineNum, l, true
+	}
+	for {
+		if w.eof {
+			return 0, "", false
+		}
+
+		l, err := w.in.ReadString('\n')
+		w.lineNum++
+		if errors.Is(err, io.EOF) {
+			w.eof = true
+		} else {
+			try(err)
+			l = l[:len(l)-1] // remove '\n'
+		}
+
+		if l == "" { // ignore empty lines
+			continue
+		}
+
+		return w.lineNum, l, true
+	}
+}
+
+func (w *writer) writeln(args ...interface{}) {
+	try(fmt.Fprintln(w.out, args...))
+}
+
+func try(args ...interface{}) {
+	err := args[len(args)-1]
+	if err != nil {
+		panic(err)
+	}
+}
+
+func readmeRef(lineNum int) string {
+	return fmt.Sprintf("README.md:%d", lineNum)
+}