Repository: ably/ably-go
Pull request #312 (2021-05-13 17:47:10)

Allow setting connection and request timeouts separately
See the discussion [there](https://github.com/ably/ably-boomer/pull/44).

This is a minimal set of changes required to set a different connection and request timeout for Realtime. This also adds `WithHTTPRequestTimeout` to set a timeout for REST requests without using `WithHTTPClient`.


Patch:
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index a10a805..d0e93ed 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -33,9 +33,6 @@ jobs:
       - name: Vet
         run: go vet ./ably/... ./scripts/...
 
-      - name: Ensure generated code is up-to-date
-        run: go generate ./... && [ -z "$(git status --porcelain)" ]
-
       - name: Test with JSON Protocol
         env:
           ABLY_PROTOCOL: application/json
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index dd3a0a0..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,15 +0,0 @@
-# Binaries for programs and plugins
-*.exe
-*.exe~
-*.dll
-*.so
-*.dylib
-
-
-# Output of the go coverage tool, specifically when used with LiteIDE
-*.out
-
-# Dependency directories (remove the comment below to include it)
- vendor/
-.idea/
-*.env
\ No newline at end of file
diff --git a/README.md b/README.md
index 16d784f..6992a4b 100644
--- a/README.md
+++ b/README.md
@@ -231,8 +231,8 @@ if err != nil {
 	panic(err)
 }
 
-// You can also publish multiple messages in a single request.
-err = channel.PublishMultiple(ctx, []*ably.Message{
+// 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!"},
 })
@@ -243,97 +243,56 @@ if err != nil {
 
 ### Querying the History
 
-<!-- GO EXAMPLE
-{
--->
-
 ```go
-pages, err := channel.History().Pages(ctx)
-if err != nil {
-	panic(err)
-}
-for pages.Next(ctx) {
-	for _, message := range pages.Items() {
+page, err := channel.History(ctx, nil)
+for ; err == nil && page != nil; page, err = page.Next(ctx) {
+	for _, message := range page.Messages() {
 		fmt.Println(message)
 	}
 }
-if err := pages.Err(); err != nil {
+if err != nil {
 	panic(err)
 }
-
 ```
 
-<!-- GO EXAMPLE
-}
--->
-
 ### Presence on a channel
 
-<!-- GO EXAMPLE
-{
--->
-
 ```go
-pages, err := channel.Presence.Get().Pages(ctx)
-if err != nil {
-	panic(err)
-}
-for pages.Next(ctx) {
-	for _, presence := range pages.Items() {
+page, err = channel.Presence.Get(ctx, nil)
+for ; err == nil && page != nil; page, err = page.Next(ctx) {
+	for _, presence := range page.PresenceMessages() {
 		fmt.Println(presence)
 	}
 }
-if err := pages.Err(); err != nil {
+if err != nil {
 	panic(err)
 }
 ```
 
-<!-- GO EXAMPLE
-}
--->
-
 ### Querying the Presence History
 
-<!-- GO EXAMPLE
-{
--->
-
 ```go
-pages, err := channel.Presence.History().Pages(ctx)
-if err != nil {
-	panic(err)
-}
-for pages.Next(ctx) {
-	for _, presence := range pages.Items() {
+page, err = channel.Presence.History(ctx, nil)
+for ; err == nil && page != nil; page, err = page.Next(ctx) {
+	for _, presence := range page.PresenceMessages() {
 		fmt.Println(presence)
 	}
 }
-if err := pages.Err(); err != nil {
+if err != nil {
 	panic(err)
 }
 ```
 
-<!-- GO EXAMPLE
-}
--->
-
 ### Fetching your application's stats
 
-<!-- GO EXAMPLE
-{
--->
-
 ```go
-pages, err := client.Stats().Pages(ctx)
-if err != nil {
-	panic(err)
-}
-for pages.Next(ctx) {
-	for _, stat := range pages.Items() {
+page, err = client.Stats(ctx, &ably.PaginateParams{})
+for ; err == nil && page != nil; page, err = page.Next(ctx) {
+	for _, stat := range page.Stats() {
 		fmt.Println(stat)
 	}
 }
-if err := pages.Err(); err != nil {
+if err != nil {
 	panic(err)
 }
 ```
@@ -342,10 +301,6 @@ if err := pages.Err(); err != nil {
 }
 -->
 
-<!-- GO EXAMPLE
-}
--->
-
 ## Feature support
 
 This library targets the Ably 1.2 [client library specification](https://www.ably.io/documentation/client-lib-development-guide/features).
diff --git a/ably/auth.go b/ably/auth.go
index 5689b32..cda459b 100644
--- a/ably/auth.go
+++ b/ably/auth.go
@@ -127,8 +127,10 @@ func (a *Auth) clientIDForCheck() string {
 func (a *Auth) updateClientID(clientID string) {
 	a.mtx.Lock()
 	defer a.mtx.Unlock()
-	//Spec RSA7b3, RSA7b4, RSA12a,RSA12b, RSA7b2,
-	a.clientID = clientID
+	if a.clientID == "" {
+		//Spec RSA7b3, RSA7b4, RSA12a,RSA12b, RSA7b2,
+		a.clientID = clientID
+	}
 }
 
 // CreateTokenRequest
diff --git a/ably/doc.go b/ably/doc.go
index 1c0c474..e053372 100644
--- a/ably/doc.go
+++ b/ably/doc.go
@@ -33,28 +33,4 @@
 //
 // For messages and presence messages, "on" is called "subscribe" and "off" is
 // called "unsubscribe".
-//
-// Paginated results
-//
-// Most requests to the Ably REST API return a single page of results, with
-// hyperlinks to the first and next pages in the whole collection of results.
-// To facilitate navigating through these pages, the library provides access to
-// such paginated results though a common pattern.
-//
-// A method that prepares a paginated request returns a Request object with two
-// methods: Pages and Items. Pages returns a PaginatedResult, an iterator that,
-// on each iteration, yields a whole page of results. Items is simply a
-// convenience wrapper that yields single results insteads.
-//
-// In both cases, calling the method validates the request and may return an
-// error.
-//
-// Then, for accessing the results, the Next method from the resulting
-// iterator object must be called repeatedly; each time it returns true, the
-// result that has been retrieved can be inspected with the Items or Item method
-// from the iterator object. Finally, once it returns false, the Err method must
-// be called to check if the iterator stopped due to some error, or else, it
-// just finished going through all pages.
-//
-// See the PaginatedResults example.
 package ably
diff --git a/ably/http_paginated_response.go b/ably/http_paginated_response.go
new file mode 100644
index 0000000..dadfafe
--- /dev/null
+++ b/ably/http_paginated_response.go
@@ -0,0 +1,59 @@
+package ably
+
+import (
+	"context"
+	"net/http"
+	"reflect"
+
+	"github.com/ably/ably-go/ably/proto"
+)
+
+// HTTPPaginatedResponse represent a response from an http request.
+type HTTPPaginatedResponse struct {
+	*PaginatedResult
+	StatusCode   int         //spec HP4
+	Success      bool        //spec HP5
+	ErrorCode    ErrorCode   //spec HP6
+	ErrorMessage string      //spec HP7
+	Headers      http.Header //spec HP8
+}
+
+func decodeHTTPPaginatedResult(opts *proto.ChannelOptions, typ reflect.Type, resp *http.Response) (interface{}, error) {
+	var o interface{}
+	err := decodeResp(resp, &o)
+	if err != nil {
+		return nil, err
+	}
+	return o, nil
+}
+
+func newHTTPPaginatedResult(ctx context.Context, path string, params *PaginateParams,
+	query queryFunc, log *LoggerOptions) (*HTTPPaginatedResponse, error) {
+	p, err := newPaginatedResult(ctx, nil, paginatedRequest{typ: arrayTyp, path: path, params: params, query: query, logger: log, respCheck: func(_ *http.Response) error {
+		return nil
+	}, decoder: decodeHTTPPaginatedResult})
+	if err != nil {
+		return nil, err
+	}
+	//spec RSC19d
+	return newHTTPPaginatedResultFromPaginatedResult(p), nil
+}
+
+func newHTTPPaginatedResultFromPaginatedResult(p *PaginatedResult) *HTTPPaginatedResponse {
+	h := &HTTPPaginatedResponse{PaginatedResult: p}
+	h.StatusCode = p.statusCode
+	h.Success = p.success
+	h.ErrorCode = ErrorCode(p.errorCode)
+	h.ErrorMessage = p.errorMessage
+	return h
+}
+
+// Next overrides PaginatedResult.Next
+// spec HP2
+func (h *HTTPPaginatedResponse) Next(ctx context.Context) (*HTTPPaginatedResponse, error) {
+	p, err := h.PaginatedResult.Next(ctx)
+	if err != nil {
+		return nil, err
+	}
+	return newHTTPPaginatedResultFromPaginatedResult(p), nil
+}
diff --git a/ably/options.go b/ably/options.go
index 34a47b5..d6211f6 100644
--- a/ably/options.go
+++ b/ably/options.go
@@ -340,7 +340,7 @@ func (opts *clientOptions) httpclient() *http.Client {
 		return opts.HTTPClient
 	}
 	return &http.Client{
-		Timeout: defaultOptions.HTTPRequestTimeout,
+		Timeout: opts.HTTPRequestTimeout,
 	}
 }
 
@@ -609,6 +609,12 @@ func WithRESTHost(host string) ClientOption {
 	}
 }
 
+func WithHTTPRequestTimeout(timeout time.Duration) ClientOption {
+	return func(os *clientOptions) {
+		os.HTTPRequestTimeout = timeout
+	}
+}
+
 func WithRealtimeHost(host string) ClientOption {
 	return func(os *clientOptions) {
 		os.RealtimeHost = host
@@ -663,6 +669,12 @@ func WithHTTPOpenTimeout(d time.Duration) ClientOption {
 	}
 }
 
+func WithRealtimeRequestTimeout(d time.Duration) ClientOption {
+	return func(os *clientOptions) {
+		os.RealtimeRequestTimeout = d
+	}
+}
+
 func WithSuspendedRetryTimeout(d time.Duration) ClientOption {
 	return func(os *clientOptions) {
 		os.SuspendedRetryTimeout = d
diff --git a/ably/paginated_result.go b/ably/paginated_result.go
index 4bfa13b..8d6e406 100644
--- a/ably/paginated_result.go
+++ b/ably/paginated_result.go
@@ -2,199 +2,246 @@ package ably
 
 import (
 	"context"
+	"errors"
+	"fmt"
+	"io"
 	"net/http"
 	"net/url"
 	"path"
 	"reflect"
 	"regexp"
+	"strconv"
+	"strings"
 
 	"github.com/ably/ably-go/ably/proto"
 )
 
-type Direction string
-
-const (
-	Backwards Direction = "backwards"
-	Forwards  Direction = "forwards"
-)
-
-type paginatedRequest struct {
-	path   string
-	params url.Values
+// relLinkRegexp is the regexp that matches our pagination format
+var relLinkRegexp = regexp.MustCompile(`<(?P<url>[^>]+)>; rel="(?P<rel>[^"]+)"`)
 
-	query queryFunc
+type errInvalidType struct {
+	typ reflect.Type
 }
 
-func (r *REST) newPaginatedRequest(path string, params url.Values) paginatedRequest {
-	return paginatedRequest{
-		path:   path,
-		params: params,
-
-		query: query(r.get),
-	}
+func (err errInvalidType) Error() string {
+	return "requested value of incompatible type: " + err.typ.String()
 }
 
-// PaginatedResult is a generic iterator for PaginatedResult pagination.
-// Items decoding is delegated to type-specific wrappers.
-//
-// See "Paginated results" section in the package-level documentation.
+// queryFunc queries the given URL and gives non-nil HTTP response if no error
+// occurred.
+type queryFunc func(ctx context.Context, url string) (*http.Response, error)
+
+// PaginatedResult represents a single page coming back from the REST API.
+// Any call to create a new page will generate a new instance.
 type PaginatedResult struct {
-	basePath  string
-	nextLink  string
-	firstLink string
-	res       *http.Response
-	err       error
-
-	query queryFunc
-	first bool
-}
-
-// load loads the first page of results. Must be called from the type-specific
-// wrapper Pages method that creates the PaginatedResult object.
-func (p *PaginatedResult) load(ctx context.Context, r paginatedRequest) error {
-	p.basePath = path.Dir(r.path)
-	p.firstLink = (&url.URL{
-		Path:     r.path,
-		RawQuery: r.params.Encode(),
-	}).String()
-	p.query = r.query
-	return p.First(ctx)
-}
-
-// loadItems loads the first page of results and returns a next function. Must
-// be called from the type-specific wrapper Items method that creates the
-// PaginatedItems object.
-//
-// The returned next function must be called from the wrapper's Next method, and
-// returns the index of the object that should be returned by the Item method,
-// previously loading the next page if necessary.
-//
-// pageDecoder will be called each time a new page is retrieved under the hood.
-// It should return a destination object on which the page of results will be
-// decoded, and a pageLength function that, when called after the page has been
-// decoded, must return the length of the page.
-func (p *PaginatedResult) loadItems(
-	ctx context.Context,
-	r paginatedRequest,
-	pageDecoder func() (page interface{}, pageLength func() int),
-) (
-	next func(context.Context) (int, bool),
-	err error,
-) {
-	err = p.load(ctx, r)
-	if err != nil {
-		return nil, err
-	}
+	path     string
+	headers  map[string]string
+	links    []string
+	items    []interface{}
+	typItems interface{}
+	opts     *proto.ChannelOptions
+	req      paginatedRequest
 
-	var page interface{}
-	var pageLen int
-	nextItem := 0
+	statusCode   int
+	success      bool
+	errorCode    int
+	errorMessage string
+	respHeaders  http.Header
+}
+
+type paginatedRequest struct {
+	typ       reflect.Type
+	path      string
+	params    *PaginateParams
+	query     queryFunc
+	logger    *LoggerOptions
+	respCheck func(*http.Response) error
+	decoder   func(*proto.ChannelOptions, reflect.Type, *http.Response) (interface{}, error)
+}
 
-	return func(ctx context.Context) (int, bool) {
-		if nextItem == 0 {
-			var getLen func() int
-			page, getLen = pageDecoder()
-			hasNext := p.next(ctx, &page)
-			if !hasNext {
-				return 0, false
+func decodePaginatedResult(opts *proto.ChannelOptions, typ reflect.Type, resp *http.Response) (interface{}, error) {
+	switch typ {
+	case msgType:
+		var o []map[string]interface{}
+		err := decodeResp(resp, &o)
+		if err != nil {
+			return nil, err
+		}
+		rs := make([]*proto.Message, len(o))
+		for k, v := range o {
+			m := &proto.Message{
+				ChannelOptions: opts,
 			}
-			pageLen = getLen()
-			if pageLen == 0 { // compatible with hasNext if first page is empty
-				return 0, false
+			err = m.FromMap(v)
+			if err != nil {
+				return nil, err
 			}
+			rs[k] = m
 		}
-
-		idx := nextItem
-		nextItem = (nextItem + 1) % pageLen
-		return idx, true
-	}, nil
+		return rs, nil
+	case presMsgType:
+		var o []map[string]interface{}
+		err := decodeResp(resp, &o)
+		if err != nil {
+			return nil, err
+		}
+		rs := make([]*proto.PresenceMessage, len(o))
+		for k, v := range o {
+			m := &proto.PresenceMessage{
+				Message: proto.Message{
+					ChannelOptions: opts,
+				},
+			}
+			err = m.FromMap(v)
+			if err != nil {
+				return nil, err
+			}
+			rs[k] = m
+		}
+		return rs, nil
+	default:
+		v := reflect.New(typ)
+		if err := decodeResp(resp, v.Interface()); err != nil {
+			return nil, err
+		}
+		return v.Elem().Interface(), nil
+	}
 }
 
-func (p *PaginatedResult) goTo(ctx context.Context, link string) error {
-	var err error
-	p.res, err = p.query(ctx, link)
+func newPaginatedResult(ctx context.Context, opts *proto.ChannelOptions, req paginatedRequest) (*PaginatedResult, error) {
+	if req.decoder == nil {
+		req.decoder = decodePaginatedResult
+	}
+	p := &PaginatedResult{
+		opts: opts,
+		req:  req,
+	}
+	builtPath, err := p.buildPaginatedPath(req.path, req.params)
 	if err != nil {
-		return err
+		return nil, err
 	}
-	p.nextLink = ""
-	for _, rawLink := range p.res.Header["Link"] {
-		m := relLinkRegexp.FindStringSubmatch(rawLink)
-		if len(m) == 0 {
-			continue
-		}
-		relPath, rel := m[1], m[2]
-		path := path.Join(p.basePath, relPath)
-		switch rel {
-		case "first":
-			p.firstLink = path
-		case "next":
-			p.nextLink = path
+	resp, err := p.req.query(ctx, builtPath)
+	if err != nil {
+		return nil, err
+	}
+	if err = req.respCheck(resp); err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	if p.respHeaders == nil {
+		p.respHeaders = make(http.Header)
+	}
+	p.statusCode = resp.StatusCode
+	p.success = 200 <= p.statusCode && p.statusCode < 300
+	copyHeader(p.respHeaders, resp.Header)
+	if h := p.respHeaders.Get(proto.AblyErrorCodeHeader); h != "" {
+		i, err := strconv.Atoi(h)
+		if err != nil {
+			return nil, err
 		}
+		p.errorCode = i
+	} else if !p.success {
+		return nil, malformedPaginatedResponseError(resp)
 	}
-	return nil
+	if h := p.respHeaders.Get(proto.AblyErrormessageHeader); h != "" {
+		p.errorMessage = h
+	} else if !p.success {
+		return nil, malformedPaginatedResponseError(resp)
+	}
+	p.path = builtPath
+	p.links = resp.Header["Link"]
+	v, err := p.req.decoder(opts, p.req.typ, resp)
+	if err != nil {
+		return nil, err
+	}
+	p.typItems = v
+	return p, nil
 }
 
-// next loads the next page of items, if there is one. It returns whether a page
-// was successfully loaded or not; after it returns false, Err should be
-// called to check for any errors.
-//
-// Items can then be inspected with the type-specific Items method.
-//
-// For items iterators, use the next function returned by loadItems instead.
-func (p *PaginatedResult) next(ctx context.Context, into interface{}) bool {
-	if !p.first {
-		if p.nextLink == "" {
-			return false
-		}
-		p.err = p.goTo(ctx, p.nextLink)
-		if p.err != nil {
-			return false
-		}
+func malformedPaginatedResponseError(resp *http.Response) error {
+	body := make([]byte, 200)
+	n, err := io.ReadFull(resp.Body, body)
+	body = body[:n]
+	msg := fmt.Sprintf("invalid PaginatedResult HTTP response; status: %d; body (first %d bytes): %q", resp.StatusCode, len(body), body)
+	if err != nil && !errors.Is(err, io.EOF) {
+		return fmt.Errorf("%s; body read error: %w", msg, err)
 	}
-	p.first = false
-
-	p.err = decodeResp(p.res, into)
-	return p.err == nil
+	return errors.New(msg)
 }
 
-// First loads the first page of items. Next should be called before inspecting
-// the Items.
-func (p *PaginatedResult) First(ctx context.Context) error {
-	p.first = true
-	return p.goTo(ctx, p.firstLink)
+func copyHeader(dest, src http.Header) {
+	for k, v := range src {
+		d := make([]string, len(v))
+		copy(d, v)
+		dest[k] = v
+	}
 }
 
-// Err returns the error that caused Next to fail, if there was one.
-func (p *PaginatedResult) Err() error {
-	return p.err
+// 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(ctx context.Context) (*PaginatedResult, error) {
+	nextPath, ok := p.paginationHeaders()["next"]
+	if !ok {
+		return nil, nil
+	}
+	nextPage := p.buildPath(p.path, nextPath)
+	req := p.req
+	req.path = nextPage
+	req.params = nil
+	return newPaginatedResult(ctx, p.opts, req)
 }
 
-// relLinkRegexp is the regexp that matches our pagination format
-var relLinkRegexp = regexp.MustCompile(`<(?P<url>[^>]+)>; rel="(?P<rel>[^"]+)"`)
-
-type errInvalidType struct {
-	typ reflect.Type
+// Items gives a slice of results of the current page.
+func (p *PaginatedResult) Items() []interface{} {
+	if p.items == nil {
+		v := reflect.ValueOf(p.typItems)
+		if v.Kind() == reflect.Slice {
+			p.items = make([]interface{}, v.Len())
+			for i := range p.items {
+				p.items[i] = v.Index(i).Interface()
+			}
+		} else {
+			p.items = []interface{}{p.typItems}
+		}
+	}
+	return p.items
 }
 
-func (err errInvalidType) Error() string {
-	return "requested value of incompatible type: " + err.typ.String()
+// Messages gives a slice of messages for the current page. The method panics if
+// the underlying paginated result is not a message.
+func (p *PaginatedResult) Messages() []*Message {
+	items, ok := p.typItems.([]*proto.Message)
+	if !ok {
+		panic(errInvalidType{typ: p.req.typ})
+	}
+	return items
 }
 
-// queryFunc queries the given URL and gives non-nil HTTP response if no error
-// occurred.
-type queryFunc func(ctx context.Context, url string) (*http.Response, error)
+// PresenceMessages gives a slice of presence messages for the current path.
+// The method panics if the underlying paginated result is not a presence message.
+func (p *PaginatedResult) PresenceMessages() []*PresenceMessage {
+	items, ok := p.typItems.([]*proto.PresenceMessage)
+	if !ok {
+		panic(errInvalidType{typ: p.req.typ})
+	}
+	return items
+}
 
-func copyHeader(dest, src http.Header) {
-	for k, v := range src {
-		d := make([]string, len(v))
-		copy(d, v)
-		dest[k] = v
+// Stats gives a slice of statistics for the current page. The method panics if
+// the underlying paginated result is not statistics.
+func (p *PaginatedResult) Stats() []*Stats {
+	items, ok := p.typItems.([]*proto.Stats)
+	if !ok {
+		panic(errInvalidType{typ: p.req.typ})
 	}
+	return items
 }
 
 type Stats = proto.Stats
 type StatsMessageTypes = proto.MessageTypes
-type StatsMessageCount = proto.MessageCount
 type StatsMessageTraffic = proto.MessageTraffic
 type StatsConnectionTypes = proto.ConnectionTypes
 type StatsResourceCount = proto.ResourceCount
@@ -202,3 +249,48 @@ type StatsRequestCount = proto.RequestCount
 type StatsPushStats = proto.PushStats
 type StatsXchgMessages = proto.XchgMessages
 type PushStats = proto.PushStats
+
+func (c *PaginatedResult) buildPaginatedPath(path string, params *PaginateParams) (string, error) {
+	if params == nil {
+		return path, nil
+	}
+	values := &url.Values{}
+	err := params.EncodeValues(values)
+	if err != nil {
+		return "", newError(50000, err)
+	}
+	queryString := values.Encode()
+	if len(queryString) > 0 {
+		return path + "?" + queryString, nil
+	}
+	return path, nil
+}
+
+// buildPath finds the absolute path based on the path parameter and the new relative path.
+func (p *PaginatedResult) buildPath(origPath string, newRelativePath string) string {
+	if i := strings.IndexRune(origPath, '?'); i != -1 {
+		origPath = origPath[:i]
+	}
+	return path.Join(path.Dir(origPath), newRelativePath)
+}
+
+func (p *PaginatedResult) paginationHeaders() map[string]string {
+	if p.headers == nil {
+		p.headers = make(map[string]string)
+		for _, link := range p.links {
+			if result := relLinkRegexp.FindStringSubmatch(link); result != nil {
+				p.addMatch(result)
+			}
+		}
+	}
+	return p.headers
+}
+
+func (p *PaginatedResult) addMatch(matches []string) {
+	matchingNames := relLinkRegexp.SubexpNames()
+	matchMap := map[string]string{}
+	for i, value := range matches {
+		matchMap[matchingNames[i]] = value
+	}
+	p.headers[matchMap["rel"]] = matchMap["url"]
+}
diff --git a/ably/proto/crypto.go b/ably/proto/crypto.go
index 525ec45..13aebcc 100644
--- a/ably/proto/crypto.go
+++ b/ably/proto/crypto.go
@@ -81,9 +81,6 @@ type ChannelOptions struct {
 // GetCipher returns a ChannelCipher based on the algorithms set in the
 // ChannelOptions.CipherParams.
 func (c *ChannelOptions) GetCipher() (ChannelCipher, error) {
-	if c == nil {
-		return nil, errors.New("no cipher configured")
-	}
 	if c.cipher != nil {
 		return c.cipher, nil
 	}
diff --git a/ably/proto/http.go b/ably/proto/http.go
index 21a0b55..38dd222 100644
--- a/ably/proto/http.go
+++ b/ably/proto/http.go
@@ -5,7 +5,7 @@ const (
 	AblyVersionHeader      = "X-Ably-Version"
 	AblyLibHeader          = "X-Ably-Lib"
 	AblyErrorCodeHeader    = "X-Ably-Errorcode"
-	AblyErrorMessageHeader = "X-Ably-Errormessage"
+	AblyErrormessageHeader = "X-Ably-Errormessage"
 	LibraryVersion         = "1.2.0-apipreview.4"
 	LibraryName            = "go"
 	LibraryString          = LibraryName + "-" + LibraryVersion
diff --git a/ably/proto/message.go b/ably/proto/message.go
index 2667918..89c72b8 100644
--- a/ably/proto/message.go
+++ b/ably/proto/message.go
@@ -1,13 +1,15 @@
 package proto
 
 import (
-	"bytes"
 	"crypto/aes"
 	"encoding/base64"
 	"encoding/json"
+	"errors"
 	"fmt"
+	"reflect"
 	"strings"
-	"unicode/utf8"
+
+	"github.com/ugorji/go/codec"
 )
 
 // encodings
@@ -19,117 +21,309 @@ const (
 )
 
 type Message struct {
-	ID           string                 `json:"id,omitempty" codec:"id,omitempty"`
-	ClientID     string                 `json:"clientId,omitempty" codec:"clientId,omitempty"`
-	ConnectionID string                 `json:"connectionId,omitempty" codec:"connectionID,omitempty"`
-	Name         string                 `json:"name,omitempty" codec:"name,omitempty"`
-	Data         interface{}            `json:"data,omitempty" codec:"data,omitempty"`
-	Encoding     string                 `json:"encoding,omitempty" codec:"encoding,omitempty"`
-	Timestamp    int64                  `json:"timestamp,omitempty" codec:"timestamp,omitempty"`
-	Extras       map[string]interface{} `json:"extras,omitempty" codec:"extras,omitempty"`
+	ID              string                 `json:"id,omitempty" codec:"id,omitempty"`
+	ClientID        string                 `json:"clientId,omitempty" codec:"clientId,omitempty"`
+	ConnectionID    string                 `json:"connectionId,omitempty" codec:"connectionID,omitempty"`
+	Name            string                 `json:"name,omitempty" codec:"name,omitempty"`
+	Data            interface{}            `json:"data,omitempty" codec:"data,omitempty"`
+	Encoding        string                 `json:"encoding,omitempty" codec:"encoding,omitempty"`
+	Timestamp       int64                  `json:"timestamp" codec:"timestamp"`
+	Extras          map[string]interface{} `json:"extras" codec:"extras"`
+	*ChannelOptions `json:"-" codec:"-"`
 }
 
-func (m Message) String() string {
-	return fmt.Sprintf("<Message %q data=%v>", m.Name, m.Data)
+func (m *Message) maybeJSONEncode() error {
+	if m.Data == nil {
+		return nil
+	}
+	switch m.Data.(type) {
+	case json.Marshaler:
+		bs, err := m.Data.(json.Marshaler).MarshalJSON()
+		if err != nil {
+			return err
+		}
+		m.Data = string(bs)
+		m.Encoding = mergeEncoding(m.Encoding, JSON)
+		return nil
+	case map[string]interface{}:
+		bs, err := json.Marshal(m.Data)
+		if err != nil {
+			return err
+		}
+		m.Data = string(bs)
+		m.Encoding = mergeEncoding(m.Encoding, JSON)
+		return nil
+	}
+	dataType := reflect.TypeOf(m.Data)
+
+	// marshal any sort of slice except for []byte (i.e. []uint8)
+	if dataType.Kind() == reflect.Slice && dataType.Elem().Kind() != reflect.Uint8 {
+		bs, err := json.Marshal(m.Data)
+		if err != nil {
+			return err
+		}
+		m.Data = string(bs)
+		m.Encoding = mergeEncoding(m.Encoding, JSON)
+	}
+	return nil
 }
 
-func unencodableDataErr(data interface{}) error {
-	return fmt.Errorf("message data type %T must be string, []byte, or a value that can be encoded as a JSON object or array", data)
+func (m Message) HasCipher() bool {
+	if m.ChannelOptions != nil {
+		c, _ := m.ChannelOptions.GetCipher()
+		return c != nil
+	}
+	return false
 }
 
-func (m Message) WithEncodedData(cipher ChannelCipher) (Message, error) {
-	// TODO: Unexport once proto gets merged into package ably.
+func (m Message) encode() (Message, error) {
 	if m.Data == nil {
 		return m, nil
 	}
-
-	switch d := m.Data.(type) {
+	err := m.maybeJSONEncode()
+	if err != nil {
+		return m, err
+	}
+	switch m.Data.(type) {
 	case string:
-		if utf8.ValidString(d) {
+		if m.HasCipher() {
 			m.Encoding = mergeEncoding(m.Encoding, UTF8)
-		} else {
-			// If string isn't UTF-8, convert to []byte to encode it as base64
-			// below.
-			m.Data = []byte(d)
+		}
+	case []byte:
+		// ok
+	default:
+		return Message{}, errors.New("unsupported payload type")
+	}
+	if m.ChannelOptions != nil {
+		if cipher, err := m.GetCipher(); err == nil {
+			// since we know that m.Data is either []byte or string at this point, coerceBytes is always
+			// safe here
+			bs, err := coerceBytes(m.Data)
+			if err != nil {
+				return Message{}, err
+			}
+			e, err := cipher.Encrypt(bs)
+			if err != nil {
+				return Message{}, err
+			}
+			m.Data = e
+			m.Encoding = mergeEncoding(m.Encoding, cipher.GetAlgorithm())
 		}
 	}
-	switch d := m.Data.(type) {
+
+	return m, nil
+}
+
+func (m Message) encodeJSON() (Message, error) {
+	e, err := m.encode()
+	if err != nil {
+		return m, err
+	}
+	if d, ok := e.Data.([]byte); ok {
+		b := base64.StdEncoding.EncodeToString(d)
+		e.Data = b
+		e.Encoding = mergeEncoding(e.Encoding, Base64)
+	}
+	return e, nil
+}
+
+func coerceString(i interface{}) (string, error) {
+	switch v := i.(type) {
+	case []byte:
+		return string(v), nil
 	case string:
+		return v, nil
+	default:
+		return "", fmt.Errorf("UTF8 encoding can only handle types string or []byte, but got %T", i)
+	}
+}
+
+func coerceBytes(i interface{}) ([]byte, error) {
+	switch v := i.(type) {
 	case []byte:
-		m.Data = base64.StdEncoding.EncodeToString(d)
-		m.Encoding = mergeEncoding(m.Encoding, Base64)
+		return v, nil
+	case string:
+		return []byte(v), nil
 	default:
-		// RSL4c3, RSL4d3: JSON is only for objects and arrays. So marshal data
-		// into JSON, then check if if's one of those.
-		b, err := json.Marshal(d)
+		return nil, fmt.Errorf("coerceBytes should never get anything but strings or []byte, got %T", i)
+	}
+}
+
+// ToMap returns a map of all message field names to their respective value if
+// the field are set.
+//
+// Spec RSL1j
+func (m Message) ToMap() map[string]interface{} {
+	ctx := make(map[string]interface{})
+	if m.ID != "" {
+		ctx["id"] = m.ID
+	}
+	if m.ClientID != "" {
+		ctx["clientId"] = m.ClientID
+	}
+	if m.ConnectionID != "" {
+		ctx["connectionId"] = m.ConnectionID
+	}
+	if m.Name != "" {
+		ctx["name"] = m.Name
+	}
+	encoding := m.Encoding
+	if m.Data != nil {
+		ctx["data"] = m.Data
+	}
+	if encoding != "" {
+		ctx["encoding"] = encoding
+	}
+	if m.Timestamp != 0 {
+		ctx["timestamp"] = m.Timestamp
+	}
+	if m.Extras != nil {
+		ctx["extras"] = m.Extras
+	}
+	return ctx
+}
+
+func (m Message) MarshalJSON() ([]byte, error) {
+	e, err := m.encodeJSON()
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(e.ToMap())
+}
+
+func (m *Message) UnmarshalJSON(data []byte) error {
+	var ctx map[string]interface{}
+	if err := json.Unmarshal(data, &ctx); err != nil {
+		return err
+	}
+	return m.FromMap(ctx)
+}
+
+func (m Message) CodecEncodeSelf(encoder *codec.Encoder) {
+	e, err := m.encode()
+	if err != nil {
+		panic(err)
+	}
+	encoder.MustEncode(e.ToMap())
+}
+
+// CodecDecodeSelf implements codec.Selfer interface for msgpack decoding.
+func (m *Message) CodecDecodeSelf(decoder *codec.Decoder) {
+	ctx := make(map[string]interface{})
+	decoder.MustDecode(&ctx)
+	m.FromMap(ctx)
+}
+
+func (m *Message) FromMap(ctx map[string]interface{}) error {
+	if v, ok := ctx["id"]; ok {
+		x, err := coerceString(v)
 		if err != nil {
-			return Message{}, fmt.Errorf("%s; encoding as JSON: %w", unencodableDataErr(d), err)
+			return err
 		}
-		token, _ := json.NewDecoder(bytes.NewReader(b)).Token()
-		if token != json.Delim('[') && token != json.Delim('{') {
-			return Message{}, fmt.Errorf("%s; encoded as JSON %T", unencodableDataErr(d), token)
+		m.ID = string(x)
+	}
+	if v, ok := ctx["clientId"]; ok {
+		x, err := coerceString(v)
+		if err != nil {
+			return err
 		}
-		m.Data = string(b)
-		m.Encoding = mergeEncoding(m.Encoding, JSON)
+		m.ClientID = string(x)
 	}
-
-	if cipher == nil {
-		return m, nil
+	if v, ok := ctx["connectionId"]; ok {
+		x, err := coerceString(v)
+		if err != nil {
+			return err
+		}
+		m.ConnectionID = string(x)
 	}
+	if v, ok := ctx["name"]; ok {
+		x, err := coerceString(v)
+		if err != nil {
+			return err
+		}
+		m.Name = string(x)
+	}
+	if v, ok := ctx["encoding"]; ok {
+		x, err := coerceString(v)
+		if err != nil {
+			return err
+		}
+		m.Encoding = string(x)
+	}
+	if v, ok := ctx["data"]; ok {
+		m.Data = v
+		dec, err := m.decode()
+		if err != nil {
+			return err
+		}
+		*m = dec
+	}
+	if v, ok := ctx["timestamp"]; ok {
+		switch e := v.(type) {
+		case float64:
+			m.Timestamp = int64(e)
+		case uint64:
+			m.Timestamp = int64(e)
+		case int64:
+			m.Timestamp = e
+		}
+	}
+	if v, ok := ctx["extras"]; ok {
+		m.Extras = v.(map[string]interface{})
+	}
+	return nil
+}
+
+// MemberKey returns string that allows to uniquely identify connected clients.
+func (m *Message) MemberKey() string {
+	return m.ConnectionID + ":" + m.ClientID
+}
 
-	// since we know that m.Data is either []byte or string at this point, coerceBytes is always
-	// safe here
-	bs, err := coerceBytes(m.Data)
+func (m Message) Decrypt() (interface{}, error) {
+	cipher, err := m.GetCipher()
 	if err != nil {
-		panic(err)
+		return nil, err
 	}
-	e, err := cipher.Encrypt(bs)
+
+	d, err := coerceBytes(m.Data)
 	if err != nil {
-		return Message{}, fmt.Errorf("encrypting message data: %w", err)
+		return nil, err
 	}
-	if _, ok := m.Data.(string); ok {
-		// Not sure why this is useful, and can't find the requirement in the
-		// spec, but test fixtures expect this.
-		m.Encoding = mergeEncoding(m.Encoding, UTF8)
+	v, err := cipher.Decrypt(d)
+	if err != nil {
+		return nil, err
 	}
-	m.Data = base64.StdEncoding.EncodeToString(e)
-	m.Encoding = mergeEncoding(m.Encoding, cipher.GetAlgorithm())
-	m.Encoding = mergeEncoding(m.Encoding, Base64)
-	return m, nil
+	return v, nil
 }
 
-func (m Message) WithDecodedData(cipher ChannelCipher) (Message, error) {
-	// TODO: Unexport once proto gets merged into package ably.
-
+func (m Message) decode() (Message, error) {
 	// strings.Split on empty string returns []string{""}
 	if m.Data == nil || m.Encoding == "" {
 		return m, nil
 	}
 	encodings := strings.Split(m.Encoding, "/")
-	for len(encodings) > 0 {
-		encoding := encodings[len(encodings)-1]
-		encodings = encodings[:len(encodings)-1]
-		switch encoding {
+	for i := len(encodings) - 1; i >= 0; i-- {
+		switch encodings[i] {
 		case Base64:
 			d, err := coerceString(m.Data)
 			if err != nil {
-				return m, err
+				return Message{}, err
 			}
 			data, err := base64.StdEncoding.DecodeString(d)
 			if err != nil {
-				return m, err
+				return Message{}, err
 			}
 			m.Data = data
 		case UTF8:
 			d, err := coerceString(m.Data)
 			if err != nil {
-				return m, err
+				return Message{}, err
 			}
 			m.Data = d
 		case JSON:
 			d, err := coerceBytes(m.Data)
 			if err != nil {
-				return m, err
+				return Message{}, err
 			}
 			var result interface{}
 			if err := json.Unmarshal(d, &result); err != nil {
@@ -137,55 +331,22 @@ func (m Message) WithDecodedData(cipher ChannelCipher) (Message, error) {
 			}
 			m.Data = result
 		default:
-			if strings.HasPrefix(encoding, Cipher) {
-				if cipher == nil {
-					return m, fmt.Errorf("message data is encrypted as %s, but cipher wasn't provided", encoding)
-				}
-				d, err := coerceBytes(m.Data)
+			switch {
+			case strings.HasPrefix(encodings[i], Cipher):
+				d, err := m.Decrypt()
 				if err != nil {
 					return m, err
 				}
-				d, err = cipher.Decrypt(d)
-				if err != nil {
-					return m, fmt.Errorf("decrypting message data: %w", err)
-				}
 				m.Data = d
-			} else {
-				return m, fmt.Errorf("unknown encoding %s", encoding)
+			default:
+				return m, fmt.Errorf("unknown encoding %s", encodings[i])
 			}
+
 		}
-		m.Encoding = strings.Join(encodings, "/")
 	}
 	return m, nil
 }
 
-func coerceString(i interface{}) (string, error) {
-	switch v := i.(type) {
-	case []byte:
-		return string(v), nil
-	case string:
-		return v, nil
-	default:
-		return "", fmt.Errorf("UTF8 encoding can only handle types string or []byte, but got %T", i)
-	}
-}
-
-func coerceBytes(i interface{}) ([]byte, error) {
-	switch v := i.(type) {
-	case []byte:
-		return v, nil
-	case string:
-		return []byte(v), nil
-	default:
-		return nil, fmt.Errorf("coerceBytes should never get anything but strings or []byte, got %T", i)
-	}
-}
-
-// MemberKey returns string that allows to uniquely identify connected clients.
-func (m *Message) MemberKey() string {
-	return m.ConnectionID + ":" + m.ClientID
-}
-
 func addPadding(src []byte) []byte {
 	padlen := byte(aes.BlockSize - (len(src) % aes.BlockSize))
 	data := make([]byte, len(src)+int(padlen))
diff --git a/ably/proto/presence_message.go b/ably/proto/presence_message.go
index 3b40d0f..78b423e 100644
--- a/ably/proto/presence_message.go
+++ b/ably/proto/presence_message.go
@@ -1,7 +1,9 @@
 package proto
 
 import (
-	"fmt"
+	"encoding/json"
+
+	"github.com/ugorji/go/codec"
 )
 
 type PresenceAction int64
@@ -19,12 +21,57 @@ type PresenceMessage struct {
 	Action PresenceAction `json:"action" codec:"action"`
 }
 
-func (m PresenceMessage) String() string {
-	return fmt.Sprintf("<PresenceMessage %v clientID=%v data=%v>", [...]string{
-		"absent",
-		"present",
-		"enter",
-		"leave",
-		"update",
-	}[m.Action], m.ClientID, m.Data)
+func (m PresenceMessage) MarshalJSON() ([]byte, error) {
+	e, err := m.encodeJSON()
+	if err != nil {
+		return nil, err
+	}
+	ctx := e.ToMap()
+	ctx["action"] = m.Action
+	return json.Marshal(ctx)
+}
+
+func (m *PresenceMessage) UnmarshalJSON(data []byte) error {
+	var ctx map[string]interface{}
+	if err := json.Unmarshal(data, &ctx); err != nil {
+		return err
+	}
+	return m.FromMap(ctx)
+}
+
+// CodecEncodeSelf encodes PresenceMessage into a msgpack format.
+func (m PresenceMessage) CodecEncodeSelf(encoder *codec.Encoder) {
+	e, err := m.encode()
+	if err != nil {
+		panic(err)
+	}
+	ctx := e.ToMap()
+	ctx["action"] = m.Action
+	encoder.MustEncode(ctx)
+}
+
+// CodecDecodeSelf implements codec.Selfer interface for msgpack decoding.
+func (m *PresenceMessage) CodecDecodeSelf(decoder *codec.Decoder) {
+	ctx := make(map[string]interface{})
+	decoder.MustDecode(&ctx)
+	if err := m.FromMap(ctx); err != nil {
+		panic(err)
+	}
+}
+
+func (m *PresenceMessage) FromMap(ctx map[string]interface{}) error {
+	msg := &m.Message
+	if err := msg.FromMap(ctx); err != nil {
+		return err
+	}
+	if v, ok := ctx["action"]; ok {
+		m.Action = PresenceAction(coerceInt64(v))
+	}
+	return nil
+}
+
+func (m PresenceMessage) ToMap() map[string]interface{} {
+	ctx := m.Message.ToMap()
+	ctx["action"] = m.Action
+	return ctx
 }
diff --git a/ably/proto/protocol_message.go b/ably/proto/protocol_message.go
index 4d88698..a9db03b 100644
--- a/ably/proto/protocol_message.go
+++ b/ably/proto/protocol_message.go
@@ -1,27 +1,20 @@
 package proto
 
 import (
+	"encoding/json"
 	"fmt"
 	"time"
 )
 
-// TR3
 const (
-	FlagHasPresence       Flag = 1 << 0
-	FlagHasBacklog        Flag = 1 << 1
-	FlagResumed           Flag = 1 << 2
-	FlagTransient         Flag = 1 << 4
-	FlagAttachResume      Flag = 1 << 5
-	FlagPresence          Flag = 1 << 16
-	FlagPublish           Flag = 1 << 17
-	FlagSubscribe         Flag = 1 << 18
-	FlagPresenceSubscribe Flag = 1 << 19
+	FlagPresence Flag = iota + 1
+	FlagBacklog
 )
 
 type Flag int64
 
-func (flags Flag) Has(flag Flag) bool {
-	return flags&flag == flag
+func (f Flag) Has(flag Flag) bool {
+	return f&flag == flag
 }
 
 type ConnectionDetails struct {
@@ -100,6 +93,81 @@ type ProtocolMessage struct {
 	Flags             Flag               `json:"flags,omitempty" codec:"flags,omitempty"`
 }
 
+func (p *ProtocolMessage) UnmarshalJSON(b []byte) error {
+	ctx := make(map[string]interface{})
+	err := json.Unmarshal(b, &ctx)
+	if err != nil {
+		return err
+	}
+	p.FromMap(ctx)
+	return nil
+}
+
+func (p *ProtocolMessage) FromMap(ctx map[string]interface{}) {
+	if v, ok := ctx["messages"]; ok {
+		i := v.([]interface{})
+		for _, v := range i {
+			var msg Message
+			msg.FromMap(v.(map[string]interface{}))
+			p.Messages = append(p.Messages, &msg)
+		}
+	}
+	if v, ok := ctx["presence"]; ok {
+		i := v.([]interface{})
+		for _, v := range i {
+			var msg PresenceMessage
+			msg.FromMap(v.(map[string]interface{}))
+			p.Presence = append(p.Presence, &msg)
+		}
+	}
+	if v, ok := ctx["id"]; ok {
+		p.ID = v.(string)
+	}
+	if v, ok := ctx["applicationId"]; ok {
+		p.ApplicationID = v.(string)
+	}
+	if v, ok := ctx["connectionId"]; ok {
+		p.ConnectionID = v.(string)
+	}
+	if v, ok := ctx["connectionKey"]; ok {
+		p.ConnectionKey = v.(string)
+	}
+	if v, ok := ctx["channel"]; ok {
+		p.Channel = v.(string)
+	}
+	if v, ok := ctx["channelSerial"]; ok {
+		p.ChannelSerial = v.(string)
+	}
+	if v, ok := ctx["connectionDetails"]; ok {
+		c := &ConnectionDetails{}
+		c.FromMap(v.(map[string]interface{}))
+		p.ConnectionDetails = c
+	}
+	if v, ok := ctx["error"]; ok {
+		c := &ErrorInfo{}
+		c.FromMap(v.(map[string]interface{}))
+		p.Error = c
+	}
+	if v, ok := ctx["msgSerial"]; ok {
+		p.MsgSerial = coerceInt64(v)
+	}
+	if v, ok := ctx["connectionSerial"]; ok {
+		p.ConnectionSerial = coerceInt64(v)
+	}
+	if v, ok := ctx["timestamp"]; ok {
+		p.Timestamp = coerceInt64(v)
+	}
+	if v, ok := ctx["count"]; ok {
+		p.Count = int(coerceInt64(v))
+	}
+	if v, ok := ctx["action"]; ok {
+		p.Action = Action(coerceInt8(v))
+	}
+	if v, ok := ctx["flags"]; ok {
+		p.Flags = Flag(coerceInt64(v))
+	}
+}
+
 func (msg *ProtocolMessage) String() string {
 	switch msg.Action {
 	case ActionHeartbeat:
diff --git a/ably/proto/stat.go b/ably/proto/stat.go
index 622d686..621262b 100644
--- a/ably/proto/stat.go
+++ b/ably/proto/stat.go
@@ -1,9 +1,6 @@
 package proto
 
-import (
-	"fmt"
-	"time"
-)
+import "time"
 
 const (
 	StatGranularityMinute = "minute"
@@ -49,7 +46,7 @@ type MessageCount struct {
 
 type MessageTypes struct {
 	All      MessageCount `json:"all" codec:"all"`
-	Messages MessageCount `json:"messages" codec:"messages"`
+	Plain    MessageCount `json:"plain" codec:"plain"`
 	Presence MessageCount `json:"presence" codec:"presence"`
 }
 
@@ -139,7 +136,3 @@ type Stats struct {
 	XchgConsumer  XchgMessages    `json:"xchgConsumer" codec:"xchgConsumer"`
 	PeakRates     Rates           `json:"peakRates" codec:"peakRates"`
 }
-
-func (s Stats) String() string {
-	return fmt.Sprintf("<Stats %v; unit=%v; count=%v>", s.IntervalID, s.Unit, s.Count)
-}
diff --git a/ably/realtime_channel.go b/ably/realtime_channel.go
index 3c03936..9ebecb5 100644
--- a/ably/realtime_channel.go
+++ b/ably/realtime_channel.go
@@ -174,7 +174,7 @@ func (c *RealtimeChannel) onConnStateChange(change ConnectionStateChange) {
 	case ConnectionStateConnected:
 		c.queue.Flush()
 	case ConnectionStateFailed:
-		c.setState(ChannelStateFailed, change.Reason, false)
+		c.setState(ChannelStateFailed, change.Reason)
 		c.queue.Fail(change.Reason)
 	}
 }
@@ -221,7 +221,7 @@ func (c *RealtimeChannel) lockAttach(err error) (result, error) {
 		return nil, newError(80000, errAttach)
 	}
 
-	c.lockSetState(ChannelStateAttaching, err, false)
+	c.lockSetState(ChannelStateAttaching, err)
 
 	res := c.internalEmitter.listenResult(ChannelStateAttached, ChannelStateFailed)
 
@@ -267,9 +267,9 @@ func (c *RealtimeChannel) detachUnsafe() (result, error) {
 		return nil, channelStateError(ChannelStateFailed, errDetach)
 	}
 	if !c.client.Connection.lockIsActive() {
-		return nil, c.lockSetState(ChannelStateFailed, errDetach, false)
+		return nil, c.lockSetState(ChannelStateFailed, errDetach)
 	}
-	c.lockSetState(ChannelStateDetaching, nil, false)
+	c.lockSetState(ChannelStateDetaching, nil)
 	res := c.internalEmitter.listenResult(ChannelStateDetached, ChannelStateFailed)
 	msg := &proto.ProtocolMessage{
 		Action:  proto.ActionDetach,
@@ -402,16 +402,16 @@ func (em ChannelEventEmitter) OffAll() {
 // returns with an error, but the operation carries on in the background and
 // the channel may eventually be attached and the message published anyway.
 func (c *RealtimeChannel) Publish(ctx context.Context, name string, data interface{}) error {
-	return c.PublishMultiple(ctx, []*Message{{Name: name, Data: data}})
+	return c.PublishBatch(ctx, []*Message{{Name: name, Data: data}})
 }
 
-// PublishMultiple publishes all given messages on the channel at once.
+// PublishBatch publishes all given messages on the channel at once.
 //
 // This implicitly attaches the channel if it's not already attached. If the
 // context is canceled before the attach operation finishes, the call
 // returns with an error, but the operation carries on in the background and
 // the channel may eventually be attached and the message published anyway.
-func (c *RealtimeChannel) PublishMultiple(ctx context.Context, messages []*Message) error {
+func (c *RealtimeChannel) PublishBatch(ctx context.Context, messages []*Message) error {
 	id := c.client.Auth.clientIDForCheck()
 	for _, v := range messages {
 		if v.ClientID != "" && id != wildcardClientID && v.ClientID != id {
@@ -431,9 +431,11 @@ func (c *RealtimeChannel) PublishMultiple(ctx context.Context, messages []*Messa
 	return res.Wait(ctx)
 }
 
-// History is equivalent to RESTChannel.History.
-func (c *RealtimeChannel) History(o ...HistoryOption) HistoryRequest {
-	return c.client.rest.Channels.Get(c.Name).History(o...)
+// History gives the channel's message history according to the given parameters.
+// The returned result can be inspected for the messages via the Messages()
+// method.
+func (c *RealtimeChannel) History(ctx context.Context, params *PaginateParams) (*PaginatedResult, error) {
+	return c.client.rest.Channels.Get(c.Name).History(ctx, params)
 }
 
 func (c *RealtimeChannel) send(msg *proto.ProtocolMessage) (result, error) {
@@ -510,11 +512,9 @@ func (c *RealtimeChannel) ErrorReason() *ErrorInfo {
 
 func (c *RealtimeChannel) notify(msg *proto.ProtocolMessage) {
 	switch msg.Action {
-
 	case proto.ActionAttached:
 		c.Presence.onAttach(msg)
-		// RTL12
-		c.setState(ChannelStateAttached, newErrorFromProto(msg.Error), msg.Flags.Has(proto.FlagResumed))
+		c.setState(ChannelStateAttached, nil)
 		c.queue.Flush()
 	case proto.ActionDetached:
 		c.mtx.Lock()
@@ -522,7 +522,7 @@ func (c *RealtimeChannel) notify(msg *proto.ProtocolMessage) {
 		err := error(newErrorFromProto(msg.Error))
 		switch c.state {
 		case ChannelStateDetaching:
-			c.lockSetState(ChannelStateDetached, err, false)
+			c.lockSetState(ChannelStateDetached, err)
 			c.mtx.Unlock()
 			return
 		case ChannelStateAttached: // TODO: Also SUSPENDED; RTL13a
@@ -560,13 +560,11 @@ func (c *RealtimeChannel) notify(msg *proto.ProtocolMessage) {
 	case proto.ActionPresence:
 		c.Presence.processIncomingMessage(msg, "")
 	case proto.ActionError:
-		c.setState(ChannelStateFailed, newErrorFromProto(msg.Error), false)
+		c.setState(ChannelStateFailed, newErrorFromProto(msg.Error))
 		c.queue.Fail(newErrorFromProto(msg.Error))
 	case proto.ActionMessage:
-		if c.State() == ChannelStateAttached {
-			for _, msg := range msg.Messages {
-				c.messageEmitter.Emit(subscriptionName(msg.Name), (*subscriptionMessage)(msg))
-			}
+		for _, msg := range msg.Messages {
+			c.messageEmitter.Emit(subscriptionName(msg.Name), (*subscriptionMessage)(msg))
 		}
 	default:
 	}
@@ -574,7 +572,7 @@ func (c *RealtimeChannel) notify(msg *proto.ProtocolMessage) {
 
 func (c *RealtimeChannel) lockStartRetryAttachLoop(err error) {
 	// TODO: Move to SUSPENDED; move it to DETACHED for now.
-	c.lockSetState(ChannelStateDetached, err, false)
+	c.lockSetState(ChannelStateDetached, err)
 
 	stateChange := make(channelStateChanges, 1)
 	off := c.internalEmitter.OnceAll(stateChange.Receive)
@@ -609,7 +607,7 @@ func (c *RealtimeChannel) retryAttach(stateChange channelStateChanges) (done boo
 		return true
 	}
 	// TODO: Move to SUSPENDED; move it to DETACHED for now.
-	c.setState(ChannelStateDetached, err, false)
+	c.setState(ChannelStateDetached, err)
 	return false
 }
 
@@ -625,13 +623,13 @@ func (c *RealtimeChannel) logger() *LoggerOptions {
 	return c.client.logger()
 }
 
-func (c *RealtimeChannel) setState(state ChannelState, err error, resumed bool) error {
+func (c *RealtimeChannel) setState(state ChannelState, err error) error {
 	c.mtx.Lock()
 	defer c.mtx.Unlock()
-	return c.lockSetState(state, err, resumed)
+	return c.lockSetState(state, err)
 }
 
-func (c *RealtimeChannel) lockSetState(state ChannelState, err error, resumed bool) error {
+func (c *RealtimeChannel) lockSetState(state ChannelState, err error) error {
 	previous := c.state
 	changed := c.state != state
 	c.state = state
@@ -640,9 +638,7 @@ func (c *RealtimeChannel) lockSetState(state ChannelState, err error, resumed bo
 		Current:  c.state,
 		Previous: previous,
 		Reason:   c.errorReason,
-		Resumed:  resumed,
 	}
-	// RTL2g
 	if !changed {
 		change.Event = ChannelEventUpdate
 	} else {
diff --git a/ably/realtime_client.go b/ably/realtime_client.go
index cfa98b2..98ca2ce 100644
--- a/ably/realtime_client.go
+++ b/ably/realtime_client.go
@@ -51,9 +51,11 @@ func (c *Realtime) Close() {
 	c.Connection.Close()
 }
 
-// Stats is the same as REST.Stats.
-func (c *Realtime) Stats(o ...StatsOption) StatsRequest {
-	return c.rest.Stats(o...)
+// Stats gives the clients metrics according to the given parameters. The
+// returned result can be inspected for the statistics via the Stats()
+// method.
+func (c *Realtime) Stats(ctx context.Context, params *PaginateParams) (*PaginatedResult, error) {
+	return c.rest.Stats(ctx, params)
 }
 
 // Time
@@ -92,7 +94,7 @@ func (c *Realtime) onReconnected(isNewID bool) {
 
 func (c *Realtime) onReconnectionFailed(err *proto.ErrorInfo) {
 	for _, ch := range c.Channels.All() {
-		ch.setState(ChannelStateFailed, newErrorFromProto(err), false)
+		ch.setState(ChannelStateFailed, newErrorFromProto(err))
 	}
 }
 
diff --git a/ably/realtime_conn.go b/ably/realtime_conn.go
index 63ae45e..9c1aef6 100644
--- a/ably/realtime_conn.go
+++ b/ably/realtime_conn.go
@@ -115,7 +115,7 @@ func (c *Connection) dial(proto string, u *url.URL) (conn proto.Conn, err error)
 	query := u.Query()
 	query.Add("heartbeats", "true")
 	u.RawQuery = query.Encode()
-	timeout := c.opts.realtimeRequestTimeout()
+	timeout := c.opts.httpOpenTimeout()
 	if c.opts.Dial != nil {
 		conn, err = c.opts.Dial(proto, u, timeout)
 	} else {
@@ -470,9 +470,9 @@ func (c *Connection) Key() string {
 //
 // Ping returns non-nil error without any attempt of communication with Ably
 // if the connection state is ConnectionStateClosed or ConnectionStateFailed.
-//func (c *Connection) Ping() (ping, pong time.Duration, err error) {
-//	return 0, 0, errors.New("TODO")
-//}
+func (c *Connection) Ping() (ping, pong time.Duration, err error) {
+	return 0, 0, errors.New("TODO")
+}
 
 // ErrorReason gives last known error that caused connection transit to
 // ConnectionStateFailed state.
@@ -821,8 +821,7 @@ func (c *Connection) eventloop() {
 			} else {
 				// preserve old behavior.
 				c.mtx.Lock()
-				// RTN24
-				c.lockSetState(ConnectionStateConnected, newErrorFromProto(msg.Error), 0)
+				c.lockSetState(ConnectionStateConnected, nil, 0)
 				c.mtx.Unlock()
 			}
 			c.queue.Flush()
diff --git a/ably/realtime_presence.go b/ably/realtime_presence.go
index ed8ee43..b98e927 100644
--- a/ably/realtime_presence.go
+++ b/ably/realtime_presence.go
@@ -98,7 +98,7 @@ func (pres *RealtimePresence) onAttach(msg *proto.ProtocolMessage) {
 	pres.mtx.Lock()
 	defer pres.mtx.Unlock()
 	switch {
-	case msg.Flags.Has(proto.FlagHasPresence) || serial != "":
+	case msg.Flags.Has(proto.FlagPresence) || serial != "":
 		pres.syncStart(serial)
 	case pres.syncState == syncInitial:
 		pres.syncState = syncComplete
diff --git a/ably/rest_channel.go b/ably/rest_channel.go
index 852771a..41000ff 100644
--- a/ably/rest_channel.go
+++ b/ably/rest_channel.go
@@ -2,15 +2,11 @@ package ably
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"net/url"
-	"strconv"
 	"strings"
-	"time"
 
 	"github.com/ably/ably-go/ably/internal/ablyutil"
-	"github.com/ugorji/go/codec"
 
 	"github.com/ably/ably-go/ably/proto"
 )
@@ -55,7 +51,7 @@ func newRESTChannel(name string, client *REST) *RESTChannel {
 
 // Publish publishes a message on the channel.
 func (c *RESTChannel) Publish(ctx context.Context, name string, data interface{}) error {
-	return c.PublishMultiple(ctx, []*Message{
+	return c.PublishBatch(ctx, []*Message{
 		{Name: name, Data: data},
 	})
 }
@@ -63,38 +59,40 @@ func (c *RESTChannel) Publish(ctx context.Context, name string, data interface{}
 // Message is what Ably channels send and receive.
 type Message = proto.Message
 
-// PublishMultiple publishes multiple messages in a batch.
-func (c *RESTChannel) PublishMultiple(ctx context.Context, messages []*Message) error {
-	return c.PublishMultipleWithOptions(ctx, messages)
+// PublishBatch publishes multiple messages in a batch.
+func (c *RESTChannel) PublishBatch(ctx context.Context, messages []*Message) error {
+	return c.PublishBatchWithOptions(ctx, messages)
 }
 
-// PublishMultipleOption is an optional parameter for
-// RESTChannel.PublishMultipleWithOptions.
-type PublishMultipleOption func(*publishMultipleOptions)
+// PublishBatchOptions is an optional parameter for
+// RESTChannel.PublishBatchWithOptions.
+type PublishBatchOption func(*publishBatchOptions)
 
-type publishMultipleOptions struct {
+type publishBatchOptions struct {
 	params map[string]string
 }
 
 // Params adds query parameters to the resulting HTTP request to the REST API.
-func PublishMultipleWithParams(params map[string]string) PublishMultipleOption {
-	return func(options *publishMultipleOptions) {
+func PublishBatchWithParams(params map[string]string) PublishBatchOption {
+	return func(options *publishBatchOptions) {
 		options.params = params
 	}
 }
 
-// PublishMultipleWithOptions is PublishMultiple with optional parameters.
-func (c *RESTChannel) PublishMultipleWithOptions(ctx context.Context, messages []*Message, options ...PublishMultipleOption) error {
-	var publishOpts publishMultipleOptions
+// PublishBatchWithOptions is PublishBatch with optional parameters.
+func (c *RESTChannel) PublishBatchWithOptions(ctx context.Context, messages []*Message, options ...PublishBatchOption) error {
+	// TODO: Use context
+	var publishOpts publishBatchOptions
 	for _, o := range options {
 		o(&publishOpts)
 	}
-	for i, m := range messages {
-		cipher, _ := c.options.GetCipher()
-		var err error
-		*m, err = (*m).WithEncodedData(cipher)
-		if err != nil {
-			return fmt.Errorf("encoding data for message #%d: %w", i, err)
+	msgPtrs := make([]*proto.Message, 0, len(messages))
+	for _, m := range messages {
+		msgPtrs = append(msgPtrs, (*proto.Message)(m))
+	}
+	if c.options != nil {
+		for _, v := range messages {
+			v.ChannelOptions = c.options
 		}
 	}
 	useIdempotent := c.client.opts.idempotentRESTPublishing()
@@ -143,179 +141,16 @@ func (c *RESTChannel) PublishMultipleWithOptions(ctx context.Context, messages [
 	return res.Body.Close()
 }
 
-// History gives the channel's message history.
-func (c *RESTChannel) History(o ...HistoryOption) HistoryRequest {
-	params := (&historyOptions{}).apply(o...)
-	return HistoryRequest{
-		r:       c.client.newPaginatedRequest("/channels/"+c.Name+"/history", params),
-		channel: c,
-	}
-}
-
-// A HistoryOption configures a call to RESTChannel.History or RealtimeChannel.History.
-type HistoryOption func(*historyOptions)
-
-func HistoryWithStart(t time.Time) HistoryOption {
-	return func(o *historyOptions) {
-		o.params.Set("start", strconv.FormatInt(unixMilli(t), 10))
-	}
-}
-
-func HistoryWithEnd(t time.Time) HistoryOption {
-	return func(o *historyOptions) {
-		o.params.Set("end", strconv.FormatInt(unixMilli(t), 10))
-	}
-}
-
-func HistoryWithLimit(limit int) HistoryOption {
-	return func(o *historyOptions) {
-		o.params.Set("limit", strconv.Itoa(limit))
-	}
-}
-
-func HistoryWithDirection(d Direction) HistoryOption {
-	return func(o *historyOptions) {
-		o.params.Set("direction", string(d))
-	}
-}
-
-type historyOptions struct {
-	params url.Values
-}
-
-func (o *historyOptions) apply(opts ...HistoryOption) url.Values {
-	o.params = make(url.Values)
-	for _, opt := range opts {
-		opt(o)
-	}
-	return o.params
-}
-
-// HistoryRequest represents a request prepared by the RESTChannel.History or
-// RealtimeChannel.History method, ready to be performed by its Pages or Items methods.
-type HistoryRequest struct {
-	r       paginatedRequest
-	channel *RESTChannel
-}
-
-// Pages returns an iterator for whole pages of History.
-//
-// See "Paginated results" section in the package-level documentation.
-func (r HistoryRequest) Pages(ctx context.Context) (*MessagesPaginatedResult, error) {
-	var res MessagesPaginatedResult
-	return &res, res.load(ctx, r.r)
-}
-
-// A MessagesPaginatedResult is an iterator for the result of a History request.
-//
-// See "Paginated results" section in the package-level documentation.
-type MessagesPaginatedResult struct {
-	PaginatedResult
-	items []*Message
-}
-
-// Next retrieves the next page of results.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *MessagesPaginatedResult) Next(ctx context.Context) bool {
-	p.items = nil // avoid mutating already returned items
-	return p.next(ctx, &p.items)
-}
-
-// Items returns the current page of results.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *MessagesPaginatedResult) Items() []*Message {
-	return p.items
-}
-
-// Items returns a convenience iterator for single History, over an underlying
-// paginated iterator.
-//
-// See "Paginated results" section in the package-level documentation.
-func (r HistoryRequest) Items(ctx context.Context) (*MessagesPaginatedItems, error) {
-	var res MessagesPaginatedItems
-	var err error
-	res.next, err = res.loadItems(ctx, r.r, func() (interface{}, func() int) {
-		res.items = nil // avoid mutating already returned Items
-		return r.channel.fullMessagesDecoder(&res.items), func() int {
-			return len(res.items)
-		}
-	})
-	return &res, err
-}
-
-// fullMessagesDecoder wraps a destination slice of messages in a decoder value
-// that decodes both the message itself from the transport-level encoding and
-// the data field within from its message-specific encoding.
-func (c *RESTChannel) fullMessagesDecoder(dst *[]*Message) interface{} {
-	return &fullMessagesDecoder{dst: dst, c: c}
-}
-
-type fullMessagesDecoder struct {
-	dst *[]*Message
-	c   *RESTChannel
-}
-
-func (t *fullMessagesDecoder) UnmarshalJSON(b []byte) error {
-	err := json.Unmarshal(b, &t.dst)
+// History gives the channel's message history according to the given parameters.
+// The returned result can be inspected for the messages via the Messages()
+// method.
+func (c *RESTChannel) History(ctx context.Context, params *PaginateParams) (*PaginatedResult, error) {
+	path := c.baseURL + "/history"
+	rst, err := newPaginatedResult(ctx, c.options, paginatedRequest{typ: msgType, path: path, params: params, query: query(c.client.get), logger: c.logger(), respCheck: checkValidHTTPResponse})
 	if err != nil {
-		return err
+		return nil, err
 	}
-	t.decodeMessagesData()
-	return nil
-}
-
-func (t *fullMessagesDecoder) CodecEncodeSelf(*codec.Encoder) {
-	panic("messagesDecoderForChannel cannot be used as encoder")
-}
-
-func (t *fullMessagesDecoder) CodecDecodeSelf(decoder *codec.Decoder) {
-	decoder.MustDecode(&t.dst)
-	t.decodeMessagesData()
-}
-
-var _ interface {
-	json.Unmarshaler
-	codec.Selfer
-} = (*fullMessagesDecoder)(nil)
-
-func (t *fullMessagesDecoder) decodeMessagesData() {
-	cipher, _ := t.c.options.GetCipher()
-	for _, m := range *t.dst {
-		var err error
-		*m, err = m.WithDecodedData(cipher)
-		if err != nil {
-			// RSL6b
-			t.c.logger().sugar().Errorf("Couldn't fully decode message data from channel %q: %w", t.c.Name, err)
-		}
-	}
-}
-
-type MessagesPaginatedItems struct {
-	PaginatedResult
-	items []*Message
-	item  *Message
-	next  func(context.Context) (int, bool)
-}
-
-// Next retrieves the next result.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *MessagesPaginatedItems) Next(ctx context.Context) bool {
-	i, ok := p.next(ctx)
-	if !ok {
-		return false
-	}
-	p.item = p.items[i]
-	return true
-}
-
-// Item returns the current result.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *MessagesPaginatedItems) Item() *Message {
-	return p.item
+	return rst, nil
 }
 
 func (c *RESTChannel) logger() *LoggerOptions {
diff --git a/ably/rest_client.go b/ably/rest_client.go
index 3b6dc3e..4615ce1 100644
--- a/ably/rest_client.go
+++ b/ably/rest_client.go
@@ -14,9 +14,7 @@ import (
 	"net/http"
 	"net/http/httptrace"
 	"net/http/httputil"
-	"net/url"
 	"reflect"
-	"strconv"
 	"strings"
 	"sync"
 	"time"
@@ -27,6 +25,7 @@ import (
 
 var (
 	msgType     = reflect.TypeOf((*[]*proto.Message)(nil)).Elem()
+	statType    = reflect.TypeOf((*[]*proto.Stats)(nil)).Elem()
 	presMsgType = reflect.TypeOf((*[]*proto.PresenceMessage)(nil)).Elem()
 	arrayTyp    = reflect.TypeOf((*[]interface{})(nil)).Elem()
 )
@@ -134,9 +133,6 @@ func NewREST(options ...ClientOption) (*REST, error) {
 		cache:  make(map[string]*RESTChannel),
 		client: c,
 	}
-	c.successFallbackHost = &fallbackCache{
-		duration: c.opts.fallbackRetryTimeout(),
-	}
 	return c, nil
 }
 
@@ -158,141 +154,11 @@ func (c *REST) Time(ctx context.Context) (time.Time, error) {
 	return time.Unix(times[0]/1000, times[0]%1000), nil
 }
 
-// Stats retrieves statistics about the Ably app's activity.
-func (c *REST) Stats(o ...StatsOption) StatsRequest {
-	params := (&statsOptions{}).apply(o...)
-	return StatsRequest{r: c.newPaginatedRequest("/stats", params)}
-}
-
-// A StatsOption configures a call to REST.Stats or Realtime.Stats.
-type StatsOption func(*statsOptions)
-
-func StatsWithStart(t time.Time) StatsOption {
-	return func(o *statsOptions) {
-		o.params.Set("start", strconv.FormatInt(unixMilli(t), 10))
-	}
-}
-
-func StatsWithEnd(t time.Time) StatsOption {
-	return func(o *statsOptions) {
-		o.params.Set("end", strconv.FormatInt(unixMilli(t), 10))
-	}
-}
-
-func StatsWithLimit(limit int) StatsOption {
-	return func(o *statsOptions) {
-		o.params.Set("limit", strconv.Itoa(limit))
-	}
-}
-
-func StatsWithDirection(d Direction) StatsOption {
-	return func(o *statsOptions) {
-		o.params.Set("direction", string(d))
-	}
-}
-
-type PeriodUnit string
-
-const (
-	PeriodMinute PeriodUnit = "minute"
-	PeriodHour   PeriodUnit = "hour"
-	PeriodDay    PeriodUnit = "day"
-	PeriodMonth  PeriodUnit = "month"
-)
-
-func StatsWithUnit(d PeriodUnit) StatsOption {
-	return func(o *statsOptions) {
-		o.params.Set("unit", string(d))
-	}
-}
-
-type statsOptions struct {
-	params url.Values
-}
-
-func (o *statsOptions) apply(opts ...StatsOption) url.Values {
-	o.params = make(url.Values)
-	for _, opt := range opts {
-		opt(o)
-	}
-	return o.params
-}
-
-// StatsRequest represents a request prepared by the REST.Stats or
-// Realtime.Stats method, ready to be performed by its Pages or Items methods.
-type StatsRequest struct {
-	r paginatedRequest
-}
-
-// Pages returns an iterator for whole pages of Stats.
-//
-// See "Paginated results" section in the package-level documentation.
-func (r StatsRequest) Pages(ctx context.Context) (*StatsPaginatedResult, error) {
-	var res StatsPaginatedResult
-	return &res, res.load(ctx, r.r)
-}
-
-// A StatsPaginatedResult is an iterator for the result of a Stats request.
-//
-// See "Paginated results" section in the package-level documentation.
-type StatsPaginatedResult struct {
-	PaginatedResult
-	items []*Stats
-}
-
-// Next retrieves the next page of results.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *StatsPaginatedResult) Next(ctx context.Context) bool {
-	p.items = nil // avoid mutating already returned items
-	return p.next(ctx, &p.items)
-}
-
-// Items returns the current page of results.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *StatsPaginatedResult) Items() []*Stats {
-	return p.items
-}
-
-// Items returns a convenience iterator for single Stats, over an underlying
-// paginated iterator.
-//
-// See "Paginated results" section in the package-level documentation.
-func (r StatsRequest) Items(ctx context.Context) (*StatsPaginatedItems, error) {
-	var res StatsPaginatedItems
-	var err error
-	res.next, err = res.loadItems(ctx, r.r, func() (interface{}, func() int) {
-		res.items = nil // avoid mutating already returned items
-		return &res.items, func() int { return len(res.items) }
-	})
-	return &res, err
-}
-
-type StatsPaginatedItems struct {
-	PaginatedResult
-	items []*Stats
-	item  *Stats
-	next  func(context.Context) (int, bool)
-}
-
-// Next retrieves the next result.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *StatsPaginatedItems) Next(ctx context.Context) bool {
-	i, ok := p.next(ctx)
-	if !ok {
-		return false
-	}
-	p.item = p.items[i]
-	return true
-}
-
-// Item returns the current result.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *StatsPaginatedItems) Item() *Stats {
-	return p.item
+// Stats gives the channel's metrics according to the given parameters.
+// The returned result can be inspected for the statistics via the Stats()
+// method.
+func (c *REST) Stats(ctx context.Context, params *PaginateParams) (*PaginatedResult, error) {
+	return newPaginatedResult(ctx, nil, paginatedRequest{typ: statType, path: "/stats", params: params, query: query(c.get), logger: c.logger(), respCheck: checkValidHTTPResponse})
 }
 
 // request this contains fields necessary to compose http request that will be
@@ -311,174 +177,30 @@ type request struct {
 	header  http.Header
 }
 
-// Request prepares an arbitrary request to the REST API.
-func (c *REST) Request(method string, path string, o ...RequestOption) RESTRequest {
+// Request sends http request to ably.
+// spec RSC19
+func (c *REST) Request(ctx context.Context, method string, path string, params *PaginateParams, body interface{}, headers http.Header) (*HTTPPaginatedResponse, error) {
 	method = strings.ToUpper(method)
-	var opts requestOptions
-	opts.apply(o...)
-	return RESTRequest{r: paginatedRequest{
-		path:   path,
-		params: opts.params,
-		query: func(ctx context.Context, path string) (*http.Response, error) {
-			switch method {
-			case "GET", "POST", "PUT", "PATCH", "DELETE": // spec RSC19a
-			default:
-				return nil, fmt.Errorf("invalid HTTP method: %q", method)
-			}
-
+	switch method {
+	case "GET", "POST", "PUT", "PATCH", "DELETE": // spec RSC19a
+		return newHTTPPaginatedResult(ctx, path, params, func(ctx context.Context, p string) (*http.Response, error) {
 			req := &request{
 				Method: method,
-				Path:   path,
-				In:     opts.body,
-				header: opts.headers,
+				Path:   p,
+				In:     body,
+				header: headers,
 			}
 			return c.doWithHandle(ctx, req, func(resp *http.Response, out interface{}) (*http.Response, error) {
 				return resp, nil
 			})
-		},
-	}}
-}
-
-type requestOptions struct {
-	params  url.Values
-	headers http.Header
-	body    interface{}
-}
-
-// A RequestOption configures a call to REST.Request.
-type RequestOption func(*requestOptions)
-
-func RequestWithParams(params url.Values) RequestOption {
-	return func(o *requestOptions) {
-		o.params = params
-	}
-}
-
-func RequestWithHeaders(headers http.Header) RequestOption {
-	return func(o *requestOptions) {
-		o.headers = headers
-	}
-}
-
-func RequestWithBody(body interface{}) RequestOption {
-	return func(o *requestOptions) {
-		o.body = body
-	}
-}
-
-func (o *requestOptions) apply(opts ...RequestOption) {
-	o.params = make(url.Values)
-	for _, opt := range opts {
-		opt(o)
-	}
-}
-
-// RESTRequest represents a request prepared by the REST.Request method, ready
-// to be performed by its Pages or Items methods.
-type RESTRequest struct {
-	r paginatedRequest
-}
-
-// Pages returns an iterator for whole pages of results.
-//
-// See "Paginated results" section in the package-level documentation.
-func (r RESTRequest) Pages(ctx context.Context) (*HTTPPaginatedResponse, error) {
-	var res HTTPPaginatedResponse
-	return &res, res.load(ctx, r.r)
-}
-
-// A HTTPPaginatedResponse is an iterator for the response of a REST request.
-//
-// See "Paginated results" section in the package-level documentation.
-type HTTPPaginatedResponse struct {
-	PaginatedResult
-	items jsonRawArray
-}
-
-func (r *HTTPPaginatedResponse) StatusCode() int {
-	return r.res.StatusCode
-}
-
-func (r *HTTPPaginatedResponse) Success() bool {
-	return 200 <= r.res.StatusCode && r.res.StatusCode < 300
-}
-
-func (r *HTTPPaginatedResponse) ErrorCode() ErrorCode {
-	codeStr := r.res.Header.Get(proto.AblyErrorCodeHeader)
-	if codeStr == "" {
-		return ErrNotSet
-	}
-	code, err := strconv.Atoi(codeStr)
-	if err != nil {
-		return ErrNotSet
+		}, c.logger())
+	default:
+		return nil, newErrorFromProto(&proto.ErrorInfo{
+			Message:    fmt.Sprintf("%s method is not supported", method),
+			Code:       int(ErrMethodNotAllowed),
+			StatusCode: http.StatusMethodNotAllowed,
+		})
 	}
-	return ErrorCode(code)
-}
-
-func (r *HTTPPaginatedResponse) ErrorMessage() string {
-	return r.res.Header.Get(proto.AblyErrorMessageHeader)
-}
-
-func (r *HTTPPaginatedResponse) Headers() http.Header {
-	return r.res.Header
-}
-
-// Next retrieves the next page of results.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *HTTPPaginatedResponse) Next(ctx context.Context) bool {
-	p.items = nil
-	return p.next(ctx, &p.items)
-}
-
-// Items unmarshals the current page of results as JSON into the provided
-// variable.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *HTTPPaginatedResponse) Items(dst interface{}) error {
-	return json.Unmarshal(p.items, dst)
-}
-
-// Items returns a convenience iterator for single items, over an underlying
-// paginated iterator.
-//
-// For each item,
-//
-// See "Paginated results" section in the package-level documentation.
-func (r RESTRequest) Items(ctx context.Context) (*RESTPaginatedItems, error) {
-	var res RESTPaginatedItems
-	var err error
-	res.next, err = res.loadItems(ctx, r.r, func() (interface{}, func() int) {
-		res.items = nil
-		return &res.items, func() int { return len(res.items) }
-	})
-	return &res, err
-}
-
-type RESTPaginatedItems struct {
-	PaginatedResult
-	items []json.RawMessage
-	item  json.RawMessage
-	next  func(context.Context) (int, bool)
-}
-
-// Next retrieves the next result.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *RESTPaginatedItems) Next(ctx context.Context) bool {
-	i, ok := p.next(ctx)
-	if !ok {
-		return false
-	}
-	p.item = p.items[i]
-	return true
-}
-
-// Item unmarshal the current result as JSON into the provided variable.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *RESTPaginatedItems) Item(dst interface{}) error {
-	return json.Unmarshal(p.item, dst)
 }
 
 func (c *REST) get(ctx context.Context, path string, out interface{}) (*http.Response, error) {
@@ -569,6 +291,12 @@ func (f *fallbackCache) put(host string) {
 
 func (c *REST) doWithHandle(ctx context.Context, r *request, handle func(*http.Response, interface{}) (*http.Response, error)) (*http.Response, error) {
 	log := c.opts.Logger.sugar()
+	if c.successFallbackHost == nil {
+		c.successFallbackHost = &fallbackCache{
+			duration: c.opts.fallbackRetryTimeout(),
+		}
+		log.Verbosef("RestClient: setup fallback duration to %v", c.successFallbackHost.duration)
+	}
 	req, err := c.newHTTPRequest(ctx, r)
 	if err != nil {
 		return nil, err
@@ -818,29 +546,5 @@ func decodeResp(resp *http.Response, out interface{}) error {
 	if err != nil {
 		return err
 	}
-	b, _ := ioutil.ReadAll(resp.Body)
-
-	return decode(typ, bytes.NewReader(b), out)
-}
-
-// jsonRawArray is a json.RawMessage that, if it's not an array already, wrap
-// itself in a JSON array when marshaled into.
-type jsonRawArray json.RawMessage
-
-func (m *jsonRawArray) UnmarshalJSON(data []byte) error {
-	err := (*json.RawMessage)(m).UnmarshalJSON(data)
-	if err != nil {
-		return err
-	}
-	token, _ := json.NewDecoder(bytes.NewReader(*m)).Token()
-	if token != json.Delim('[') {
-		*m = append(
-			jsonRawArray("["),
-			append(
-				*m,
-				']',
-			)...,
-		)
-	}
-	return nil
+	return decode(typ, resp.Body, out)
 }
diff --git a/ably/rest_presence.go b/ably/rest_presence.go
index 11b804a..0adad4b 100644
--- a/ably/rest_presence.go
+++ b/ably/rest_presence.go
@@ -1,236 +1,28 @@
 package ably
 
-import (
-	"context"
-	"encoding/json"
-	"net/url"
-	"strconv"
-	"time"
-
-	"github.com/ugorji/go/codec"
-)
+import "context"
 
 type RESTPresence struct {
 	client  *REST
 	channel *RESTChannel
 }
 
-func (c *RESTPresence) Get(o ...GetPresenceOption) PresenceRequest {
-	params := (&getPresenceOptions{}).apply(o...)
-	return PresenceRequest{
-		r:       c.client.newPaginatedRequest("/channels/"+c.channel.Name+"/presence", params),
-		channel: c.channel,
-	}
-}
-
-// A GetPresenceOption configures a call to RESTPresence.Get or RealtimePresence.Get.
-type GetPresenceOption func(*getPresenceOptions)
-
-func GetPresenceWithLimit(limit int) GetPresenceOption {
-	return func(o *getPresenceOptions) {
-		o.params.Set("limit", strconv.Itoa(limit))
-	}
-}
-
-func GetPresenceWithClientID(clientID string) GetPresenceOption {
-	return func(o *getPresenceOptions) {
-		o.params.Set("clientId", clientID)
-	}
-}
-
-func GetPresenceWithConnectionID(connectionID string) GetPresenceOption {
-	return func(o *getPresenceOptions) {
-		o.params.Set("connectionId", connectionID)
-	}
+// Get gives the channel's presence messages according to the given parameters.
+// The returned result can be inspected for the presence messages via
+// the PresenceMessages() method.
+func (p *RESTPresence) Get(ctx context.Context, params *PaginateParams) (*PaginatedResult, error) {
+	path := p.channel.baseURL + "/presence"
+	return newPaginatedResult(ctx, nil, paginatedRequest{typ: presMsgType, path: path, params: params, query: query(p.client.get), logger: p.logger(), respCheck: checkValidHTTPResponse})
 }
 
-type getPresenceOptions struct {
-	params url.Values
-}
-
-func (o *getPresenceOptions) apply(opts ...GetPresenceOption) url.Values {
-	o.params = make(url.Values)
-	for _, opt := range opts {
-		opt(o)
-	}
-	return o.params
+// History gives the channel's presence messages history according to the given
+// parameters. The returned result can be inspected for the presence messages
+// via the PresenceMessages() method.
+func (p *RESTPresence) History(ctx context.Context, params *PaginateParams) (*PaginatedResult, error) {
+	path := p.channel.baseURL + "/presence/history"
+	return newPaginatedResult(ctx, nil, paginatedRequest{typ: presMsgType, path: path, params: params, query: query(p.client.get), logger: p.logger(), respCheck: checkValidHTTPResponse})
 }
 
 func (p *RESTPresence) logger() *LoggerOptions {
 	return p.client.logger()
 }
-
-// History gives the channel's presence history.
-func (c *RESTPresence) History(o ...PresenceHistoryOption) PresenceRequest {
-	params := (&presenceHistoryOptions{}).apply(o...)
-	return PresenceRequest{
-		r:       c.client.newPaginatedRequest("/channels/"+c.channel.Name+"/presence/history", params),
-		channel: c.channel,
-	}
-}
-
-// A HistoryOption configures a call to RESTChannel.History or RealtimeChannel.History.
-type PresenceHistoryOption func(*presenceHistoryOptions)
-
-func PresenceHistoryWithStart(t time.Time) PresenceHistoryOption {
-	return func(o *presenceHistoryOptions) {
-		o.params.Set("start", strconv.FormatInt(unixMilli(t), 10))
-	}
-}
-
-func PresenceHistoryWithEnd(t time.Time) PresenceHistoryOption {
-	return func(o *presenceHistoryOptions) {
-		o.params.Set("end", strconv.FormatInt(unixMilli(t), 10))
-	}
-}
-
-func PresenceHistoryWithLimit(limit int) PresenceHistoryOption {
-	return func(o *presenceHistoryOptions) {
-		o.params.Set("limit", strconv.Itoa(limit))
-	}
-}
-
-func PresenceHistoryWithDirection(d Direction) PresenceHistoryOption {
-	return func(o *presenceHistoryOptions) {
-		o.params.Set("direction", string(d))
-	}
-}
-
-type presenceHistoryOptions struct {
-	params url.Values
-}
-
-func (o *presenceHistoryOptions) apply(opts ...PresenceHistoryOption) url.Values {
-	o.params = make(url.Values)
-	for _, opt := range opts {
-		opt(o)
-	}
-	return o.params
-}
-
-// PresenceRequest represents a request prepared by the RESTPresence.History or
-// RealtimePresence.History method, ready to be performed by its Pages or Items methods.
-type PresenceRequest struct {
-	r       paginatedRequest
-	channel *RESTChannel
-}
-
-// Pages returns an iterator for whole pages of presence messages.
-//
-// See "Paginated results" section in the package-level documentation.
-func (r PresenceRequest) Pages(ctx context.Context) (*PresencePaginatedResult, error) {
-	res := PresencePaginatedResult{decoder: r.channel.fullPresenceDecoder}
-	return &res, res.load(ctx, r.r)
-}
-
-// A PresencePaginatedResult is an iterator for the result of a PresenceHistory request.
-//
-// See "Paginated results" section in the package-level documentation.
-type PresencePaginatedResult struct {
-	PaginatedResult
-	items   []*PresenceMessage
-	decoder func(*[]*PresenceMessage) interface{}
-}
-
-// Next retrieves the next page of results.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *PresencePaginatedResult) Next(ctx context.Context) bool {
-	p.items = nil // avoid mutating already returned items
-	return p.next(ctx, p.decoder(&p.items))
-}
-
-// Items returns the current page of results.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *PresencePaginatedResult) Items() []*PresenceMessage {
-	return p.items
-}
-
-// Items returns a convenience iterator for single PresenceHistory, over an underlying
-// paginated iterator.
-//
-// See "Paginated results" section in the package-level documentation.
-func (r PresenceRequest) Items(ctx context.Context) (*PresencePaginatedItems, error) {
-	var res PresencePaginatedItems
-	var err error
-	res.next, err = res.loadItems(ctx, r.r, func() (interface{}, func() int) {
-		return r.channel.fullPresenceDecoder(&res.items), func() int {
-			return len(res.items)
-		}
-	})
-	return &res, err
-}
-
-// fullPresenceDecoder wraps a destination slice of messages in a decoder value
-// that decodes both the message itself from the transport-level encoding and
-// the data field within from its message-specific encoding.
-func (c *RESTChannel) fullPresenceDecoder(dst *[]*PresenceMessage) interface{} {
-	return &fullPresenceDecoder{dst: dst, c: c}
-}
-
-type fullPresenceDecoder struct {
-	dst *[]*PresenceMessage
-	c   *RESTChannel
-}
-
-func (t *fullPresenceDecoder) UnmarshalJSON(b []byte) error {
-	err := json.Unmarshal(b, &t.dst)
-	if err != nil {
-		return err
-	}
-	t.decodeMessagesData()
-	return nil
-}
-
-func (t *fullPresenceDecoder) CodecEncodeSelf(*codec.Encoder) {
-	panic("presenceDecoderForChannel cannot be used as encoder")
-}
-
-func (t *fullPresenceDecoder) CodecDecodeSelf(decoder *codec.Decoder) {
-	decoder.MustDecode(&t.dst)
-	t.decodeMessagesData()
-}
-
-var _ interface {
-	json.Unmarshaler
-	codec.Selfer
-} = (*fullPresenceDecoder)(nil)
-
-func (t *fullPresenceDecoder) decodeMessagesData() {
-	cipher, _ := t.c.options.GetCipher()
-	for _, m := range *t.dst {
-		var err error
-		m.Message, err = m.Message.WithDecodedData(cipher)
-		if err != nil {
-			// RSL6b
-			t.c.logger().sugar().Errorf("Couldn't fully decode presence message data from channel %q: %w", t.c.Name, err)
-		}
-	}
-}
-
-type PresencePaginatedItems struct {
-	PaginatedResult
-	items []*PresenceMessage
-	item  *PresenceMessage
-	next  func(context.Context) (int, bool)
-}
-
-// Next retrieves the next result.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *PresencePaginatedItems) Next(ctx context.Context) bool {
-	i, ok := p.next(ctx)
-	if !ok {
-		return false
-	}
-	p.item = p.items[i]
-	return true
-}
-
-// Item returns the current result.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *PresencePaginatedItems) Item() *PresenceMessage {
-	return p.item
-}
diff --git a/examples/README.md b/examples/README.md
deleted file mode 100644
index 63bbab9..0000000
--- a/examples/README.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# How to run
-
-1. Realtime
-
-- Go to realtime dir `cd realtime`
-- Rename **.env.example** to **.env**, update `ABLY_KEY` with your own key 
-- Realtime channel pub-sub
-
-    `go run presence.go constants.go`
-
-
-- Realtime channel presence
-
-    `go run pub-sub.go constants.go`
-    
-2. REST
-
-- Go to rest dir `cd rest`     
-- Rename **.env.example** to **.env**, update `ABLY_KEY` with your own key 
-- Rest channel publish
-
-    `go run publish.go constants.go utils.go`
-
-- Rest channel presence
-
-    `go run presence.go constants.go utils.go`
-
-- Rest channel message history
-
-    `go run history.go constants.go utils.go`
-
-- Rest application stats
-
-    `go run stats.go utils.go constants.go`
diff --git a/examples/go.mod b/examples/go.mod
deleted file mode 100644
index 0399dcf..0000000
--- a/examples/go.mod
+++ /dev/null
@@ -1,8 +0,0 @@
-module examples
-
-go 1.14
-
-require (
-	github.com/ably/ably-go v1.2.0-apipreview.4
-	github.com/joho/godotenv v1.3.0
-)
diff --git a/examples/go.sum b/examples/go.sum
deleted file mode 100644
index 5d33101..0000000
--- a/examples/go.sum
+++ /dev/null
@@ -1,17 +0,0 @@
-github.com/ably/ably-go v1.2.0-apipreview.4 h1:e02ojSY5OpEruYgiv9mjpwfM4aLhLSdfDb/FpiHTBV8=
-github.com/ably/ably-go v1.2.0-apipreview.4/go.mod h1:bdOnFcqJrbZE7WSp1jqbdsIMBo5aMGXh98GWiiwmeZ0=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
-github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
-github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
-github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
-github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
-golang.org/x/net v0.0.0-20190110200230-915654e7eabc h1:Yx9JGxI1SBhVLFjpAkWMaO1TF+xyqtHLjZpvQboJGiM=
-golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/examples/realtime/.env.example b/examples/realtime/.env.example
deleted file mode 100644
index 2a6697a..0000000
--- a/examples/realtime/.env.example
+++ /dev/null
@@ -1,4 +0,0 @@
-// Rename this file to '.env', and replace 'YOUR_API_KEY' with your Ably API key.
-// If you have an Ably account, you can find
-// your API key at https://www.ably.io/accounts/any/apps/any/app_keys
-ABLY_KEY=YOUR_API_KEY
\ No newline at end of file
diff --git a/examples/realtime/constants.go b/examples/realtime/constants.go
deleted file mode 100644
index 084108c..0000000
--- a/examples/realtime/constants.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package main
-
-var EventName = "message"
-var UserName = "testUser"
-var AblyKey = "ABLY_KEY"
-var ChannelName = "chat"
diff --git a/examples/realtime/presence.go b/examples/realtime/presence.go
deleted file mode 100644
index 09217ae..0000000
--- a/examples/realtime/presence.go
+++ /dev/null
@@ -1,148 +0,0 @@
-package main
-
-// go run presence.go constants.go
-
-import (
-	"context"
-	"fmt"
-	"os"
-	"time"
-
-	"github.com/ably/ably-go/ably"
-	"github.com/ably/ably-go/ably/proto"
-	"github.com/joho/godotenv"
-)
-
-func main() {
-	godotenv.Load()
-
-	// Connect to Ably using the API key and ClientID specified
-	client, err := ably.NewRealtime(
-		ably.WithKey(os.Getenv(AblyKey)),
-		ably.WithClientID(UserName))
-	if err != nil {
-		panic(err)
-	}
-
-	checkPresenceEnter(client)
-	checkPresenceLeave(client)
-	checkPresenceEnterAndLeave(client)
-}
-
-func checkPresenceEnter(client *ably.Realtime) {
-	channel := client.Channels.Get(ChannelName)
-	unsubscribe := subscribePresenceEnter(channel)
-	enterPresence(channel)
-	time.Sleep(time.Second)
-	unsubscribe()
-}
-
-func checkPresenceLeave(client *ably.Realtime) {
-	channel := client.Channels.Get(ChannelName)
-	unsubscribe := subscribePresenceLeave(channel)
-	leavePresence(channel)
-	time.Sleep(time.Second)
-	unsubscribe()
-}
-
-func checkPresenceEnterAndLeave(client *ably.Realtime) {
-	channel := client.Channels.Get(ChannelName)
-	unsubscribe := subscribeAllPresence(channel)
-	enterPresence(channel)
-	printAllClientsOnChannel(channel)
-	leavePresence(channel)
-	time.Sleep(time.Second)
-	unsubscribe()
-}
-
-func enterPresence(channel *ably.RealtimeChannel) {
-	pErr := channel.Presence.Enter(context.Background(), UserName+" entered the channel")
-	if pErr != nil {
-		err := fmt.Errorf("error with enter presence on the channel %w", pErr)
-		fmt.Println(err)
-	}
-}
-
-func enterOnBehalfOf(clientId string, channel *ably.RealtimeChannel) {
-	pErr := channel.Presence.EnterClient(context.Background(), clientId, UserName+" entered the channel on behalf of "+clientId)
-	if pErr != nil {
-		err := fmt.Errorf("error with enter presence on behalf of other client on the channel %w", pErr)
-		fmt.Println(err)
-	}
-}
-
-func updatePresence(channel *ably.RealtimeChannel) {
-	pErr := channel.Presence.Update(context.Background(), UserName+" entered the channel")
-	if pErr != nil {
-		err := fmt.Errorf("error with update presence on the channel %w", pErr)
-		fmt.Println(err)
-	}
-}
-
-func leavePresence(channel *ably.RealtimeChannel) {
-	pErr := channel.Presence.Leave(context.Background(), UserName+" entered the channel")
-	if pErr != nil {
-		err := fmt.Errorf("error with leave presence on the channel %w", pErr)
-		fmt.Println(err)
-	}
-}
-
-func subscribeAllPresence(channel *ably.RealtimeChannel) func() {
-	// Subscribe to presence events (people entering and leaving) on the channel
-	unsubscribeAll, pErr := channel.Presence.SubscribeAll(context.Background(), func(msg *ably.PresenceMessage) {
-		if msg.Action == proto.PresenceEnter {
-			fmt.Printf("%v has entered the chat\n", msg.ClientID)
-		} else if msg.Action == proto.PresenceLeave {
-			fmt.Printf("%v has left the chat\n", msg.ClientID)
-		}
-	})
-	if pErr != nil {
-		err := fmt.Errorf("error subscribing to presence in channel: %w", pErr)
-		fmt.Println(err)
-
-	}
-	return unsubscribeAll
-}
-
-func subscribePresenceEnter(channel *ably.RealtimeChannel) func() {
-	// Subscribe to presence events entering the channel
-	unsubscribe, pErr := channel.Presence.Subscribe(context.Background(), ably.PresenceActionEnter, func(msg *ably.PresenceMessage) {
-		if msg.Action == proto.PresenceEnter {
-			fmt.Printf("%v has entered the chat\n", msg.ClientID)
-		} else {
-			panic("Not supposed to get presence related to actions other than presence enter")
-		}
-	})
-
-	if pErr != nil {
-		err := fmt.Errorf("error subscribing to enter presence in channel: %w", pErr)
-		fmt.Println(err)
-	}
-	return unsubscribe
-}
-
-func subscribePresenceLeave(channel *ably.RealtimeChannel) func() {
-	// Subscribe to presence events leaving the channel
-	unsubscribe, pErr := channel.Presence.Subscribe(context.Background(), ably.PresenceActionLeave, func(msg *ably.PresenceMessage) {
-		if msg.Action == proto.PresenceLeave {
-			fmt.Printf("%v has left the chat\n", msg.ClientID)
-		} else {
-			panic("Not supposed to get presence related actions other than presence leave")
-		}
-	})
-	if pErr != nil {
-		err := fmt.Errorf("error subscribing to leave presence in channel: %w", pErr)
-		fmt.Println(err)
-	}
-	return unsubscribe
-}
-
-func printAllClientsOnChannel(channel *ably.RealtimeChannel) {
-	clients, err := channel.Presence.Get(context.Background())
-	if err != nil {
-		panic(err)
-	}
-	for _, client := range clients {
-		fmt.Println("Present client:", client)
-	}
-}
diff --git a/examples/realtime/pub-sub.go b/examples/realtime/pub-sub.go
deleted file mode 100644
index d157369..0000000
--- a/examples/realtime/pub-sub.go
+++ /dev/null
@@ -1,88 +0,0 @@
-package main
-
-//  go run pub-sub.go constants.go
-
-import (
-	"context"
-	"fmt"
-	"os"
-	"time"
-
-	"github.com/ably/ably-go/ably"
-	"github.com/joho/godotenv"
-)
-
-func main() {
-	godotenv.Load()
-
-	// Connect to Ably using the API key and ClientID specified
-	client, err := ably.NewRealtime(
-		ably.WithKey(os.Getenv(AblyKey)),
-		ably.WithClientID(UserName))
-	if err != nil {
-		panic(err)
-	}
-
-	checkSubscribeAll(client)
-	checkSubscribeToEvent(client)
-}
-
-func checkSubscribeAll(client *ably.Realtime) {
-
-	channel := client.Channels.Get(ChannelName)
-
-	unsubscribeAll := subscribeAll(channel)
-
-	publish(channel, "Hey there !!")
-
-	time.Sleep(time.Second)
-
-	unsubscribeAll()
-}
-
-func checkSubscribeToEvent(client *ably.Realtime) {
-	// Connect to the Ably Channel with name 'chat'
-	channel := client.Channels.Get(ChannelName)
-
-	unsubscribe := subscribeToEvent(channel)
-
-	// publish message with blocking call
-	publish(channel, "Hey there !!")
-
-	time.Sleep(time.Second)
-
-	unsubscribe()
-}
-
-func subscribeToEvent(channel *ably.RealtimeChannel) func() {
-	// Subscribe to messages sent on the channel with given eventName
-	unsubscribe, err := channel.Subscribe(context.Background(), EventName, func(msg *ably.Message) {
-		fmt.Printf("Received message from %v: '%v'\n", msg.ClientID, msg.Data)
-	})
-	if err != nil {
-		err := fmt.Errorf("error subscribing to channel: %w", err)
-		fmt.Println(err)
-	}
-	return unsubscribe
-}
-
-func subscribeAll(channel *ably.RealtimeChannel) func() {
-	// Subscribe to all messages sent on the channel
-	unsubscribeAll, err := channel.SubscribeAll(context.Background(), func(msg *ably.Message) {
-		fmt.Printf("Received message from %v: '%v'\n", msg.ClientID, msg.Data)
-	})
-	if err != nil {
-		err := fmt.Errorf("error subscribing to channel: %w", err)
-		fmt.Println(err)
-	}
-	return unsubscribeAll
-}
-
-func publish(channel *ably.RealtimeChannel, message string) {
-	// Publish the message to Ably Channel
-	err := channel.Publish(context.Background(), EventName, message)
-	if err != nil {
-		err := fmt.Errorf("error publishing to channel: %w", err)
-		fmt.Println(err)
-	}
-}
diff --git a/examples/rest/.env.example b/examples/rest/.env.example
deleted file mode 100644
index 2a6697a..0000000
--- a/examples/rest/.env.example
+++ /dev/null
@@ -1,4 +0,0 @@
-// Rename this file to '.env', and replace 'YOUR_API_KEY' with your Ably API key.
-// If you have an Ably account, you can find
-// your API key at https://www.ably.io/accounts/any/apps/any/app_keys
-ABLY_KEY=YOUR_API_KEY
\ No newline at end of file
diff --git a/examples/rest/constants.go b/examples/rest/constants.go
deleted file mode 100644
index 24683e8..0000000
--- a/examples/rest/constants.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package main
-
-var EventName = "message"
-var UserName = "testUser"
-var AblyKey = "ABLY_KEY"
-var ChannelName = "chat"
-var ChannelName2 = "chat2"
diff --git a/examples/rest/history.go b/examples/rest/history.go
deleted file mode 100644
index 7bcd622..0000000
--- a/examples/rest/history.go
+++ /dev/null
@@ -1,80 +0,0 @@
-package main
-
-// go run history.go constants.go utils.go
-
-import (
-	"context"
-	"fmt"
-	"os"
-	"time"
-
-	"github.com/ably/ably-go/ably"
-	"github.com/joho/godotenv"
-)
-
-func main() {
-	godotenv.Load()
-
-	// Connect to Ably using the API key and ClientID
-	client, err := ably.NewREST(
-		ably.WithKey(os.Getenv(AblyKey)),
-		ably.WithClientID(UserName))
-
-	if err != nil {
-		panic(err)
-	}
-
-	checkRestChannelMessageHistory(client)
-	checkRestChannelPresenceHistory(client)
-}
-
-func checkRestChannelMessageHistory(client *ably.REST) {
-	channel := client.Channels.Get(ChannelName)
-	realtimeClient := initRealtimeClient()
-	realtimePublish(realtimeClient, "Hey there!")
-	realtimePublish(realtimeClient, "How are you")
-
-	time.Sleep(time.Second)
-	printChannelMessageHistory(channel)
-	time.Sleep(time.Second)
-	realtimeClient.Close()
-}
-
-func checkRestChannelPresenceHistory(client *ably.REST) {
-	channel := client.Channels.Get(ChannelName)
-	realtimeClient := initRealtimeClient()
-	realtimeEnterPresence(realtimeClient)
-
-	time.Sleep(time.Second)
-	printChannelPresenceHistory(channel)
-	time.Sleep(time.Second)
-	realtimeClient.Close()
-}
-
-func printChannelMessageHistory(channel *ably.RESTChannel) {
-	page, err := channel.History(context.Background(), nil)
-	for ; err == nil && page != nil; page, err = page.Next(context.Background()) {
-		for _, message := range page.Messages() {
-			fmt.Println("--- Channel history ---")
-			fmt.Println(jsonify(message))
-			fmt.Println("--------")
-		}
-	}
-	if err != nil {
-		panic(err)
-	}
-}
-
-func printChannelPresenceHistory(channel *ably.RESTChannel) {
-	page, err := channel.Presence.History(context.Background(), nil)
-	for ; err == nil && page != nil; page, err = page.Next(context.Background()) {
-		for _, presence := range page.PresenceMessages() {
-			fmt.Println("--- Channel presence history ---")
-			fmt.Println(jsonify(presence))
-			fmt.Println("----------")
-		}
-	}
-	if err != nil {
-		panic(err)
-	}
-}
diff --git a/examples/rest/presence.go b/examples/rest/presence.go
deleted file mode 100644
index 379ec66..0000000
--- a/examples/rest/presence.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package main
-
-// go run presence.go constants.go utils.go
-
-import (
-	"context"
-	"fmt"
-	"os"
-	"time"
-
-	"github.com/ably/ably-go/ably"
-	"github.com/joho/godotenv"
-)
-
-func main() {
-	godotenv.Load()
-
-	// Connect to Ably using the API key and ClientID
-	client, err := ably.NewREST(
-		ably.WithKey(os.Getenv(AblyKey)),
-		ably.WithClientID(UserName))
-
-	if err != nil {
-		panic(err)
-	}
-
-	checkPresence(client)
-}
-
-func checkPresence(client *ably.REST) {
-	channel := client.Channels.Get(ChannelName)
-	realtimeClient := initRealtimeClient()
-	realtimeEnterPresence(realtimeClient)
-
-	printPresenceMessages(channel)
-
-	time.Sleep(time.Second)
-	realtimeLeavePresence(realtimeClient)
-	realtimeClient.Close()
-}
-
-func printPresenceMessages(channel *ably.RESTChannel) {
-	page, err := channel.Presence.Get(context.Background(), nil)
-	for ; err == nil && page != nil; page, err = page.Next(context.Background()) {
-		for _, presence := range page.PresenceMessages() {
-			fmt.Println(jsonify(presence))
-		}
-	}
-	if err != nil {
-		err := fmt.Errorf("error getting presence on the channel: %w", err)
-		panic(err)
-	}
-}
diff --git a/examples/rest/publish.go b/examples/rest/publish.go
deleted file mode 100644
index 502db18..0000000
--- a/examples/rest/publish.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package main
-
-// go run publish.go constants.go utils.go
-
-import (
-	"context"
-	"fmt"
-	"os"
-	"time"
-
-	"github.com/ably/ably-go/ably"
-	"github.com/joho/godotenv"
-)
-
-func main() {
-	godotenv.Load()
-
-	// Connect to Ably using the API key and ClientID
-	client, err := ably.NewREST(
-		ably.WithKey(os.Getenv(AblyKey)),
-		ably.WithClientID(UserName))
-	if err != nil {
-		panic(err)
-	}
-
-	checkRestPublish(client)
-	checkRestBulkPublish(client)
-}
-
-func checkRestPublish(client *ably.REST) {
-	channel := client.Channels.Get(ChannelName)
-	realtimeClient := initRealtimeClient()
-	unsubscribe := realtimeSubscribeToEvent(realtimeClient)
-
-	restPublish(channel, "Hey there")
-
-	time.Sleep(time.Second)
-	unsubscribe()
-	realtimeClient.Close()
-}
-
-func checkRestBulkPublish(client *ably.REST) {
-	channel := client.Channels.Get(ChannelName)
-	realtimeClient := initRealtimeClient()
-	unsubscribe := realtimeSubscribeToEvent(realtimeClient)
-
-	restPublishBatch(channel, "Hey there", "How are you?")
-
-	time.Sleep(time.Second)
-	unsubscribe()
-	realtimeClient.Close()
-}
-
-func restPublish(channel *ably.RESTChannel, message string) {
-
-	err := channel.Publish(context.Background(), EventName, message)
-	if err != nil {
-		err := fmt.Errorf("error publishing to channel: %w", err)
-		panic(err)
-	}
-}
-
-func restPublishBatch(channel *ably.RESTChannel, message1 string, message2 string) {
-
-	err := channel.PublishBatch(context.Background(), []*ably.Message{
-		{Name: EventName, Data: message1},
-		{Name: EventName, Data: message2},
-	})
-	if err != nil {
-		err := fmt.Errorf("error batch publishing to channel: %w", err)
-		panic(err)
-	}
-}
diff --git a/examples/rest/stats.go b/examples/rest/stats.go
deleted file mode 100644
index 0905445..0000000
--- a/examples/rest/stats.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package main
-
-//go run stats.go utils.go constants.go
-
-import (
-	"context"
-	"fmt"
-	"os"
-
-	"github.com/ably/ably-go/ably"
-	"github.com/joho/godotenv"
-)
-
-func main() {
-	godotenv.Load()
-
-	// Connect to Ably using the API key and ClientID
-	client, err := ably.NewREST(
-		ably.WithKey(os.Getenv(AblyKey)),
-		ably.WithClientID(UserName))
-	if err != nil {
-		panic(err)
-	}
-
-	printApplicationStats(client)
-}
-
-func printApplicationStats(client *ably.REST) {
-	page, err := client.Stats(context.Background(), &ably.PaginateParams{})
-	for ; err == nil && page != nil; page, err = page.Next(context.Background()) {
-		for _, stat := range page.Stats() {
-			fmt.Println(jsonify(stat))
-		}
-	}
-	if err != nil {
-		err := fmt.Errorf("error getting application stats %w", err)
-		panic(err)
-	}
-}
diff --git a/examples/rest/utils.go b/examples/rest/utils.go
deleted file mode 100644
index 6b63a29..0000000
--- a/examples/rest/utils.go
+++ /dev/null
@@ -1,68 +0,0 @@
-package main
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-	"os"
-
-	"github.com/ably/ably-go/ably"
-)
-
-func initRealtimeClient() *ably.Realtime {
-	client, err := ably.NewRealtime(
-		ably.WithKey(os.Getenv(AblyKey)),
-		// ably.WithEchoMessages(true), // Uncomment to stop messages you send from being sent back
-		ably.WithClientID(UserName))
-	if err != nil {
-		panic(err)
-	}
-	return client
-}
-
-func realtimeSubscribeToEvent(client *ably.Realtime) func() {
-	channel := client.Channels.Get(ChannelName)
-
-	// Subscribe to messages sent on the channel
-	unsubscribe, err := channel.Subscribe(context.Background(), EventName, func(msg *ably.Message) {
-		fmt.Printf("Received message from %v: '%v'\n", msg.ClientID, msg.Data)
-	})
-	if err != nil {
-		err := fmt.Errorf("error subscribing to channel: %w", err)
-		fmt.Println(err)
-	}
-	return unsubscribe
-}
-
-func realtimeEnterPresence(client *ably.Realtime) {
-	channel := client.Channels.Get(ChannelName)
-	pErr := channel.Presence.Enter(context.Background(), UserName+" entered the channel")
-	if pErr != nil {
-		err := fmt.Errorf("error with enter presence on the channel %w", pErr)
-		fmt.Println(err)
-	}
-}
-
-func realtimeLeavePresence(client *ably.Realtime) {
-	channel := client.Channels.Get(ChannelName)
-	pErr := channel.Presence.Leave(context.Background(), UserName+" entered the channel")
-	if pErr != nil {
-		err := fmt.Errorf("error with leave presence on the channel %w", pErr)
-		fmt.Println(err)
-	}
-}
-
-func realtimePublish(client *ably.Realtime, message string) {
-	channel := client.Channels.Get(ChannelName)
-	// Publish the message typed in to the Ably Channel
-	err := channel.Publish(context.Background(), EventName, message)
-	if err != nil {
-		err := fmt.Errorf("error publishing to channel: %w", err)
-		fmt.Println(err)
-	}
-}
-
-func jsonify(i interface{}) string {
-	s, _ := json.MarshalIndent(i, "", "\t")
-	return string(s)
-}


Test patch:
diff --git a/ably/ablytest/cryptodata.go b/ably/ablytest/cryptodata.go
index b019b93..4146439 100644
--- a/ably/ablytest/cryptodata.go
+++ b/ably/ablytest/cryptodata.go
@@ -15,8 +15,8 @@ type CryptoData struct {
 	Key       string `json:"key"`
 	IV        string `json:"iv"`
 	Items     []struct {
-		Encoded   json.RawMessage `json:"encoded"`
-		Encrypted json.RawMessage `json:"encrypted"`
+		Encoded   map[string]interface{} `json:"encoded"`
+		Encrypted map[string]interface{} `json:"encrypted"`
 	} `json:"items"`
 }
 
diff --git a/ably/ablytest/pagination.go b/ably/ablytest/pagination.go
deleted file mode 100644
index 8b23a88..0000000
--- a/ably/ablytest/pagination.go
+++ /dev/null
@@ -1,203 +0,0 @@
-package ablytest
-
-import (
-	"context"
-	"fmt"
-	"reflect"
-)
-
-// AllPages appends all items from all pages resulting from a paginated
-// request into the slice pointed to by dst, which must be a pointer to a slice
-// of the same type as the paginated response's items.
-func AllPages(dst, paginatedRequest interface{}) error {
-	_, getItems := generalizePagination(reflect.ValueOf(paginatedRequest))
-	items, err := getItems()
-	if err != nil {
-		return err
-	}
-	all := reflect.ValueOf(dst).Elem()
-	for items.next() {
-		all.Set(reflect.Append(all, reflect.ValueOf(items.item())))
-	}
-	return items.err()
-}
-
-type paginationOptions struct {
-	equal      func(x, y interface{}) bool
-	sortResult func([]interface{})
-}
-
-type PaginationOption func(*paginationOptions)
-
-func PaginationWithEqual(equal func(x, y interface{}) bool) PaginationOption {
-	return func(o *paginationOptions) {
-		o.equal = equal
-	}
-}
-
-func PaginationWithSortResult(sort func([]interface{})) PaginationOption {
-	return func(o *paginationOptions) {
-		o.sortResult = sort
-	}
-}
-
-func TestPagination(expected, request interface{}, perPage int, options ...PaginationOption) error {
-	opts := paginationOptions{
-		equal:      reflect.DeepEqual,
-		sortResult: func(items []interface{}) {},
-	}
-	for _, o := range options {
-		o(&opts)
-	}
-
-	var items []interface{}
-	rexpected := reflect.ValueOf(expected)
-	for i := 0; i < reflect.ValueOf(expected).Len(); i++ {
-		items = append(items, rexpected.Index(i).Interface())
-	}
-	return testPagination(reflect.ValueOf(request), items, perPage, opts)
-}
-
-func testPagination(request reflect.Value, expectedItems []interface{}, perPage int, opts paginationOptions) error {
-	getPages, getItems := generalizePagination(request)
-
-	for i := 0; i < 2; i++ {
-		pages, err := getPages()
-		if err != nil {
-			return fmt.Errorf("calling Pages: %w", err)
-		}
-		var gotItems []interface{}
-		pageNum := 1
-		expectedFullPages := len(expectedItems) / perPage
-		for pages.next() {
-			if (pageNum <= expectedFullPages && len(pages.items()) != perPage) ||
-				(pageNum > expectedFullPages && len(pages.items()) >= perPage) {
-				return fmt.Errorf("page #%d got %d items, expected at most %d", pageNum, len(pages.items()), perPage)
-			}
-			gotItems = append(gotItems, pages.items()...)
-			pageNum++
-		}
-		if err := pages.err(); err != nil {
-			return fmt.Errorf("iterating pages: %w", err)
-		}
-
-		opts.sortResult(gotItems)
-
-		if !ItemsEqual(expectedItems, gotItems, opts.equal) {
-			return fmt.Errorf("expected items: %+v, got: %+v", expectedItems, gotItems)
-		}
-
-		if err := pages.first(); err != nil {
-			return fmt.Errorf("going back to first page: %w", err)
-		}
-	}
-
-	for i := 0; i < 2; i++ {
-		items, err := getItems()
-		if err != nil {
-			return fmt.Errorf("calling Items: %w", err)
-		}
-		var gotItems []interface{}
-		for items.next() {
-			gotItems = append(gotItems, items.item())
-		}
-		if err := items.err(); err != nil {
-			return fmt.Errorf("iterating items: %w", err)
-		}
-
-		opts.sortResult(gotItems)
-
-		if !ItemsEqual(expectedItems, gotItems, opts.equal) {
-			return fmt.Errorf("expected items: %+v, got: %+v", expectedItems, gotItems)
-		}
-
-		if err := items.first(); err != nil {
-			return fmt.Errorf("going back to first page: %w", err)
-		}
-	}
-
-	return nil
-}
-
-type paginated struct {
-	next  func() bool
-	first func() error
-	err   func() error
-}
-
-type paginatedResult struct {
-	paginated
-	items func() []interface{}
-}
-
-type paginatedItems struct {
-	paginated
-	item func() interface{}
-}
-
-func generalizePagination(request reflect.Value) (func() (paginatedResult, error), func() (paginatedItems, error)) {
-	ctx := reflect.ValueOf(context.Background())
-
-	generalizeCommon := func(r reflect.Value) paginated {
-		return paginated{
-			next: func() bool {
-				return r.MethodByName("Next").Call([]reflect.Value{ctx})[0].Bool()
-			},
-			first: func() error {
-				err, _ := r.MethodByName("First").Call([]reflect.Value{ctx})[0].Interface().(error)
-				return err
-			},
-			err: func() error {
-				err, _ := r.MethodByName("Err").Call(nil)[0].Interface().(error)
-				return err
-			},
-		}
-	}
-
-	pages := func() (paginatedResult, error) {
-		ret := request.MethodByName("Pages").Call([]reflect.Value{ctx})
-		if err, ok := ret[1].Interface().(error); ok && err != nil {
-			return paginatedResult{}, err
-		}
-		r := ret[0]
-		return paginatedResult{
-			paginated: generalizeCommon(r),
-			items: func() []interface{} {
-				ritems := r.MethodByName("Items").Call(nil)[0]
-				var items []interface{}
-				for i := 0; i < ritems.Len(); i++ {
-					items = append(items, ritems.Index(i).Interface())
-				}
-				return items
-			},
-		}, nil
-	}
-
-	items := func() (paginatedItems, error) {
-		ret := request.MethodByName("Items").Call([]reflect.Value{ctx})
-		if err, ok := ret[1].Interface().(error); ok && err != nil {
-			return paginatedItems{}, err
-		}
-		r := ret[0]
-		return paginatedItems{
-			paginated: generalizeCommon(r),
-			item: func() interface{} {
-				return r.MethodByName("Item").Call(nil)[0].Interface()
-			},
-		}, nil
-	}
-
-	return pages, items
-}
-
-func ItemsEqual(expected, got []interface{}, equal func(x, y interface{}) bool) bool {
-	if len(expected) != len(got) {
-		return false
-	}
-	for i := range expected {
-		if !equal(expected[i], got[i]) {
-			return false
-		}
-	}
-	return true
-}
diff --git a/ably/ablytest/sandbox.go b/ably/ablytest/sandbox.go
index 36782f2..a3bdf82 100644
--- a/ably/ablytest/sandbox.go
+++ b/ably/ablytest/sandbox.go
@@ -77,22 +77,18 @@ func DefaultConfig() *Config {
 		},
 		Channels: []Channel{
 			{
-				Name:     "persisted:presence_fixtures",
-				Presence: PresenceFixtures(),
+				Name: "persisted:presence_fixtures",
+				Presence: []Presence{
+					{ClientID: "client_bool", Data: "true"},
+					{ClientID: "client_int", Data: "true"},
+					{ClientID: "client_string", Data: "true"},
+					{ClientID: "client_json", Data: `{"test": "This is a JSONObject clientData payload"}`},
+				},
 			},
 		},
 	}
 }
 
-var PresenceFixtures = func() []Presence {
-	return []Presence{
-		{ClientID: "client_bool", Data: "true"},
-		{ClientID: "client_int", Data: "true"},
-		{ClientID: "client_string", Data: "true"},
-		{ClientID: "client_json", Data: `{"test": "This is a JSONObject clientData payload"}`},
-	}
-}
-
 type Sandbox struct {
 	Config      *Config
 	Environment string
diff --git a/ably/ablytest/timeout.go b/ably/ablytest/timeout.go
index c8991c6..4bfa459 100644
--- a/ably/ablytest/timeout.go
+++ b/ably/ablytest/timeout.go
@@ -91,11 +91,11 @@ func (wt WithTimeout) IsTrue(pred func() bool) bool {
 	timeout := time.NewTimer(wt.before)
 	defer timeout.Stop()
 	for {
-		if pred() {
-			return true
-		}
 		select {
 		case <-ticker.C:
+			if pred() {
+				return true
+			}
 		case <-timeout.C:
 			return pred()
 		}
diff --git a/ably/auth_test.go b/ably/auth_test.go
index a426f15..d00a4b3 100644
--- a/ably/auth_test.go
+++ b/ably/auth_test.go
@@ -55,7 +55,7 @@ func TestAuth_BasicAuth(t *testing.T) {
 	if _, err := client.Time(context.Background()); err != nil {
 		t.Fatalf("client.Time()=%v", err)
 	}
-	if _, err := client.Stats().Pages(context.Background()); err != nil {
+	if _, err := client.Stats(context.Background(), single()); err != nil {
 		t.Fatalf("client.Stats()=%v", err)
 	}
 	if n := rec.Len(); n != 2 {
@@ -107,7 +107,7 @@ func TestAuth_TokenAuth(t *testing.T) {
 	if _, err := client.Time(context.Background()); err != nil {
 		t.Fatalf("client.Time()=%v", err)
 	}
-	if _, err := client.Stats().Pages(context.Background()); err != nil {
+	if _, err := client.Stats(context.Background(), single()); err != nil {
 		t.Fatalf("client.Stats()=%v", err)
 	}
 	// At this points there should be two requests recorded:
@@ -262,7 +262,7 @@ func TestAuth_TokenAuth_Renew(t *testing.T) {
 		t.Fatalf("want ttl=1s; got %v", ttl)
 	}
 	time.Sleep(2 * time.Second) // wait till expires
-	_, err = client.Stats().Pages(context.Background())
+	_, err = client.Stats(context.Background(), single())
 	if err != nil {
 		t.Fatalf("Stats()=%v", err)
 	}
@@ -296,7 +296,7 @@ func TestAuth_TokenAuth_Renew(t *testing.T) {
 	if err != nil {
 		t.Fatalf("NewREST()=%v", err)
 	}
-	if _, err := client.Stats().Pages(context.Background()); err == nil {
+	if _, err := client.Stats(context.Background(), single()); err == nil {
 		t.Fatal("want err!=nil")
 	}
 	// Ensure no requests were made to Ably servers.
@@ -460,7 +460,7 @@ func TestAuth_RequestToken(t *testing.T) {
 			t.Errorf("NewRealtime()=%v", err)
 			continue
 		}
-		if _, err = c.Stats().Pages(context.Background()); err != nil {
+		if _, err = c.Stats(context.Background(), single()); err != nil {
 			t.Errorf("c.Stats()=%v (method=%s)", err, method)
 		}
 	}
@@ -568,7 +568,7 @@ func TestAuth_RequestToken_PublishClientID(t *testing.T) {
 			Name:     "test",
 			Data:     "payload",
 		}}
-		err = channel.PublishMultiple(context.Background(), msg)
+		err = channel.PublishBatch(context.Background(), msg)
 		if cas.rejected {
 			if err == nil {
 				t.Errorf("%d: expected message to be rejected %#v", i, cas)
@@ -576,7 +576,7 @@ func TestAuth_RequestToken_PublishClientID(t *testing.T) {
 			continue
 		}
 		if err != nil {
-			t.Errorf("%d: PublishMultiple()=%v", i, err)
+			t.Errorf("%d: PublishBatch()=%v", i, err)
 			continue
 		}
 		select {
diff --git a/ably/doc_test.go b/ably/doc_test.go
deleted file mode 100644
index 7a7670b..0000000
--- a/ably/doc_test.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package ably_test
-
-import "fmt"
-
-func Example_paginatedResults() {
-	fmt.Println("TODO once the Requests method is adapted")
-	// Output: TODO once the Requests method is adapted
-}
diff --git a/ably/export_test.go b/ably/export_test.go
index 77855d6..a6fb481 100644
--- a/ably/export_test.go
+++ b/ably/export_test.go
@@ -7,6 +7,10 @@ import (
 	"time"
 )
 
+func (p *PaginatedResult) BuildPath(base, rel string) string {
+	return p.buildPath(base, rel)
+}
+
 func (opts *clientOptions) RestURL() string {
 	return opts.restURL()
 }
@@ -87,12 +91,6 @@ func (c *Connection) MsgSerial() int64 {
 	return c.msgSerial
 }
 
-func WithRealtimeRequestTimeout(d time.Duration) ClientOption {
-	return func(os *clientOptions) {
-		os.RealtimeRequestTimeout = d
-	}
-}
-
 func WithTrace(trace *httptrace.ClientTrace) ClientOption {
 	return func(os *clientOptions) {
 		os.Trace = trace
diff --git a/ably/http_paginated_response_test.go b/ably/http_paginated_response_test.go
index 5edf6d8..02e08e2 100644
--- a/ably/http_paginated_response_test.go
+++ b/ably/http_paginated_response_test.go
@@ -3,7 +3,6 @@ package ably_test
 import (
 	"context"
 	"net/http"
-	"net/url"
 	"reflect"
 	"sort"
 	"testing"
@@ -27,43 +26,37 @@ func TestHTTPPaginatedResponse(t *testing.T) {
 		t.Fatal(err)
 	}
 	t.Run("request_time", func(ts *testing.T) {
-		res, err := client.Request("get", "/time").Pages(context.Background())
+		res, err := client.Request(context.Background(), "get", "/time", nil, nil, nil)
 		if err != nil {
 			ts.Fatal(err)
 		}
-		if res.StatusCode() != http.StatusOK {
-			ts.Errorf("expected %d got %d", http.StatusOK, res.StatusCode())
+		if res.StatusCode != http.StatusOK {
+			ts.Errorf("expected %d got %d", http.StatusOK, res.StatusCode)
 		}
-		if !res.Success() {
+		if !res.Success {
 			ts.Error("expected success to be true")
 		}
-		res.Next(context.Background())
-		var items []interface{}
-		err = res.Items(&items)
-		if err != nil {
-			ts.Error(err)
-		}
-		n := len(items)
+		n := len(res.Items())
 		if n != 1 {
 			ts.Errorf("expected 1 item got %d", n)
 		}
 	})
 
 	t.Run("request_404", func(ts *testing.T) {
-		res, err := client.Request("get", "/keys/ablyjs.test/requestToken").Pages(context.Background())
+		res, err := client.Request(context.Background(), "get", "/keys/ablyjs.test/requestToken", nil, nil, nil)
 		if err != nil {
 			ts.Fatal(err)
 		}
-		if res.StatusCode() != http.StatusNotFound {
-			ts.Errorf("expected %d got %d", http.StatusNotFound, res.StatusCode())
+		if res.StatusCode != http.StatusNotFound {
+			ts.Errorf("expected %d got %d", http.StatusNotFound, res.StatusCode)
 		}
-		if res.ErrorCode() != ably.ErrNotFound {
-			ts.Errorf("expected %d got %d", ably.ErrNotFound, res.ErrorCode())
+		if res.ErrorCode != ably.ErrNotFound {
+			ts.Errorf("expected %d got %d", ably.ErrNotFound, res.ErrorCode)
 		}
-		if res.Success() {
+		if res.Success {
 			ts.Error("expected success to be false")
 		}
-		if res.ErrorMessage() == "" {
+		if res.ErrorMessage == "" {
 			ts.Error("expected error message")
 		}
 	})
@@ -78,23 +71,17 @@ func TestHTTPPaginatedResponse(t *testing.T) {
 
 		ts.Run("post", func(ts *testing.T) {
 			for _, message := range msgs {
-				res, err := client.Request("POST", channelPath, ably.RequestWithBody(message)).Pages(context.Background())
+				res, err := client.Request(context.Background(), "POST", channelPath, nil, message, nil)
 				if err != nil {
 					ts.Fatal(err)
 				}
-				if res.StatusCode() != http.StatusCreated {
-					ts.Errorf("expected %d got %d", http.StatusCreated, res.StatusCode())
+				if res.StatusCode != http.StatusCreated {
+					ts.Errorf("expected %d got %d", http.StatusCreated, res.StatusCode)
 				}
-				if !res.Success() {
+				if !res.Success {
 					ts.Error("expected success to be true")
 				}
-				res.Next(context.Background())
-				var items []interface{}
-				err = res.Items(&items)
-				if err != nil {
-					ts.Error(err)
-				}
-				n := len(items)
+				n := len(res.Items())
 				if n != 1 {
 					ts.Errorf("expected 1 item got %d", n)
 				}
@@ -102,27 +89,21 @@ func TestHTTPPaginatedResponse(t *testing.T) {
 		})
 
 		ts.Run("get", func(ts *testing.T) {
-			res, err := client.Request("get", channelPath, ably.RequestWithParams(url.Values{
-				"limit":     {"1"},
-				"direction": {"forwards"},
-			})).Pages(context.Background())
+			res, err := client.Request(context.Background(), "get", channelPath, &ably.PaginateParams{
+				Limit:     1,
+				Direction: "forwards",
+			}, nil, nil)
 			if err != nil {
 				ts.Fatal(err)
 			}
-			if res.StatusCode() != http.StatusOK {
-				ts.Errorf("expected %d got %d", http.StatusOK, res.StatusCode())
+			if res.StatusCode != http.StatusOK {
+				ts.Errorf("expected %d got %d", http.StatusOK, res.StatusCode)
 			}
-			res.Next(context.Background())
-			var items []interface{}
-			err = res.Items(&items)
-			if err != nil {
-				ts.Error(err)
-			}
-			n := len(items)
+			n := len(res.Items())
 			if n != 1 {
 				ts.Fatalf("expected 1 item got %d", n)
 			}
-			m := items[0].(map[string]interface{})
+			m := res.Items()[0].(map[string]interface{})
 			name := m["name"].(string)
 			data := m["data"].(string)
 			if name != msgs[0].Name {
@@ -132,18 +113,15 @@ func TestHTTPPaginatedResponse(t *testing.T) {
 				ts.Errorf("expected %v got %s", msgs[0].Data, data)
 			}
 
-			if !res.Next(context.Background()) {
-				ts.Fatal(res.Err())
-			}
-			err = res.Items(&items)
+			res, err = res.Next(context.Background())
 			if err != nil {
-				ts.Error(err)
+				ts.Fatal(err)
 			}
-			n = len(items)
+			n = len(res.Items())
 			if n != 1 {
 				ts.Fatalf("expected 1 item got %d", n)
 			}
-			m = items[0].(map[string]interface{})
+			m = res.Items()[0].(map[string]interface{})
 			name = m["name"].(string)
 			data = m["data"].(string)
 			if name != msgs[1].Name {
@@ -155,11 +133,11 @@ func TestHTTPPaginatedResponse(t *testing.T) {
 
 		})
 
-		var m []*ably.Message
-		err := ablytest.AllPages(&m, client.Channels.Get(channelName).History())
+		res, err := client.Channels.Get(channelName).History(context.Background(), nil)
 		if err != nil {
 			ts.Fatal(err)
 		}
+		m := res.Messages()
 		sort.Slice(m, func(i, j int) bool {
 			return m[i].Name < m[j].Name
 		})
diff --git a/ably/paginated_result_test.go b/ably/paginated_result_test.go
new file mode 100644
index 0000000..5f74002
--- /dev/null
+++ b/ably/paginated_result_test.go
@@ -0,0 +1,60 @@
+package ably_test
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"github.com/ably/ably-go/ably"
+)
+
+func TestPaginatedResult(t *testing.T) {
+	t.Parallel()
+	result := &ably.PaginatedResult{}
+	newPath := result.BuildPath("/path/to/resource?hello", "./newresource?world")
+	expected := "/path/to/newresource?world"
+	if newPath != expected {
+		t.Errorf("expected %s got %s", expected, newPath)
+	}
+}
+
+func TestMalformedPaginatedResult(t *testing.T) {
+	bodyBytes, _ := json.Marshal([]string{"\x00 not really a PaginatedResult"})
+	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(400)
+		w.Write(bodyBytes)
+	}))
+	defer srv.Close()
+
+	srvAddr := srv.Listener.Addr().(*net.TCPAddr)
+	client, err := ably.NewREST(
+		ably.WithToken("xxxxxxx.yyyyyyy:zzzzzzz"),
+		ably.WithTLS(false),
+		ably.WithRESTHost(srvAddr.IP.String()),
+		ably.WithPort(srvAddr.Port),
+	)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	resp, err := client.Request(context.Background(), "POST", "/foo", nil, nil, nil)
+	if resp != nil {
+		t.Errorf("expected no HTTPPaginatedResult; got %+v", resp)
+	}
+	if err == nil {
+		t.Fatal("expected an error")
+	}
+	errMsg := err.Error()
+	if !strings.Contains(errMsg, "status: 400") {
+		t.Errorf("expected error to contain status code; got: %v", err)
+	}
+	if !strings.Contains(errMsg, fmt.Sprintf("%q", bodyBytes)) {
+		t.Errorf("expected error to contain body; got: %v", err)
+	}
+}
diff --git a/ably/proto/message_test.go b/ably/proto/message_test.go
index 27386a6..39e70e6 100644
--- a/ably/proto/message_test.go
+++ b/ably/proto/message_test.go
@@ -13,7 +13,7 @@ import (
 type custom struct{}
 
 func (custom) MarshalJSON() ([]byte, error) {
-	return json.Marshal([]string{"custom"})
+	return json.Marshal("custom")
 }
 
 func TestMessage(t *testing.T) {
@@ -63,8 +63,8 @@ func TestMessage(t *testing.T) {
 		{
 			desc:        "with a json encoding RSL4d3 json.Marshaler data",
 			data:        custom{},
-			encodedJSON: `{"data":"[\"custom\"]","encoding":"json"}`,
-			decoded:     []interface{}{"custom"},
+			encodedJSON: `{"data":"\"custom\"","encoding":"json"}`,
+			decoded:     "custom",
 		},
 		{
 			desc:        "with a base64 encoding RSL4d3 binary data",
@@ -88,34 +88,29 @@ func TestMessage(t *testing.T) {
 	for _, v := range sample {
 		// pin
 		v := v
-		t.Run(v.desc, func(t *testing.T) {
-			cipher, _ := v.opts.GetCipher()
-			msg, err := proto.Message{
-				Data: v.data,
-			}.WithEncodedData(cipher)
-			if err != nil {
-				t.Fatal(err)
+		t.Run(v.desc, func(ts *testing.T) {
+			msg := &proto.Message{
+				Data:           v.data,
+				ChannelOptions: v.opts,
 			}
 			b, err := json.Marshal(msg)
 			if err != nil {
-				t.Fatal(err)
+				ts.Fatal(err)
 			}
 			got := string(b)
 			if got != v.encodedJSON {
-				t.Errorf("expected %s got %s", v.encodedJSON, got)
+				ts.Errorf("expected %s got %s", v.encodedJSON, got)
 			}
 
-			var encoded proto.Message
-			err = json.Unmarshal(b, &encoded)
-			if err != nil {
-				t.Fatal(err)
+			decoded := &proto.Message{
+				ChannelOptions: v.opts,
 			}
-			decoded, err := encoded.WithDecodedData(cipher)
+			err = decoded.UnmarshalJSON(b)
 			if err != nil {
-				t.Fatal(err)
+				ts.Fatal(err)
 			}
 			if !reflect.DeepEqual(decoded.Data, v.decoded) {
-				t.Errorf("expected %#v got %#v", v.decoded, decoded.Data)
+				ts.Errorf("expected %#v got %v", v.decoded, decoded.Data)
 			}
 		})
 	}
@@ -133,10 +128,10 @@ func TestMessage_CryptoDataFixtures_RSL6a1_RSL5b_RSL5c(t *testing.T) {
 	for _, fixture := range fixtures {
 		// pin
 		fixture := fixture
-		t.Run(fixture.desc, func(t *testing.T) {
+		t.Run(fixture.desc, func(ts *testing.T) {
 			test, key, iv, err := ablytest.LoadCryptoData(fixture.file)
 			if err != nil {
-				t.Fatal(err)
+				ts.Fatal(err)
 			}
 			opts := &proto.ChannelOptions{
 				Cipher: proto.CipherParams{
@@ -145,35 +140,26 @@ func TestMessage_CryptoDataFixtures_RSL6a1_RSL5b_RSL5c(t *testing.T) {
 					IV:        iv,
 				},
 			}
-			t.Run("fixture encode", func(t *testing.T) {
+			ts.Run("fixture encode", func(ts *testing.T) {
 				for _, item := range test.Items {
-					cipher, _ := opts.GetCipher()
-
-					var encoded proto.Message
-					err := json.Unmarshal(item.Encoded, &encoded)
+					// All test-cases from the common fixtures files are encoded
+					// for binary transports. Decode the input message first,
+					// to ensure we're not encrypting base64d payloads.
+					encoded := &proto.Message{ChannelOptions: opts}
+					err := encoded.FromMap(item.Encoded)
 					if err != nil {
-						t.Fatal(err)
+						ts.Fatal(err)
 					}
-					encoded, err = encoded.WithDecodedData(cipher)
+					encrypted := &proto.Message{ChannelOptions: opts}
+					err = encrypted.FromMap(item.Encrypted)
 					if err != nil {
-						t.Fatal(err)
+						ts.Fatal(err)
 					}
-
-					var encrypted proto.Message
-					err = json.Unmarshal(item.Encoded, &encrypted)
-					if err != nil {
-						t.Fatal(err)
-					}
-					encrypted, err = encrypted.WithDecodedData(cipher)
-					if err != nil {
-						t.Fatal(err)
-					}
-
 					if encoded.Name != encrypted.Name {
-						t.Errorf("expected %s got %s", encoded.Name, encrypted.Name)
+						ts.Errorf("expected %s got %s", encoded.Name, encrypted.Name)
 					}
 					if !reflect.DeepEqual(encoded.Data, encrypted.Data) {
-						t.Errorf("expected %s got %s :encoding %s",
+						ts.Errorf("expected %s got %s :encoding %s",
 							encoded.Data, encrypted.Data, encoded.Encoding)
 					}
 				}
diff --git a/ably/readme_examples_test.go b/ably/readme_examples_test.go
index b2643a3..5caad05 100644
--- a/ably/readme_examples_test.go
+++ b/ably/readme_examples_test.go
@@ -160,9 +160,9 @@ func TestReadmeExamples(t *testing.T) {
 			/* README.md:231 */ panic(err)
 			/* README.md:232 */
 		}
-		/* README.md:234 */ // You can also publish multiple messages in a single request.
+		/* README.md:234 */ // You can also publish a batch of messages in a single request.
 		/* README.md:235 */
-		err = channel.PublishMultiple(ctx, []*ably.Message{
+		err = channel.PublishBatch(ctx, []*ably.Message{
 			/* README.md:236 */ {Name: "HelloEvent", Data: "Hello!"},
 			/* README.md:237 */ {Name: "ByeEvent", Data: "Bye!"},
 			/* README.md:238 */})
@@ -170,82 +170,54 @@ func TestReadmeExamples(t *testing.T) {
 			/* README.md:240 */ panic(err)
 			/* README.md:241 */
 		}
-		/* README.md:247 */ {
-			/* README.md:251 */ pages, err := channel.History().Pages(ctx)
-			/* README.md:252 */ if err != nil {
-				/* README.md:253 */ panic(err)
-				/* README.md:254 */
+		/* README.md:247 */ page, err := channel.History(ctx, nil)
+		/* README.md:248 */ for ; err == nil && page != nil; page, err = page.Next(ctx) {
+			/* README.md:249 */ for _, message := range page.Messages() {
+				/* README.md:250 */ fmt.Println(message)
+				/* README.md:251 */
 			}
-			/* README.md:255 */ for pages.Next(ctx) {
-				/* README.md:256 */ for _, message := range pages.Items() {
-					/* README.md:257 */ fmt.Println(message)
-					/* README.md:258 */
-				}
-				/* README.md:259 */
-			}
-			/* README.md:260 */ if err := pages.Err(); err != nil {
-				/* README.md:261 */ panic(err)
-				/* README.md:262 */
-			}
-			/* README.md:267 */
+			/* README.md:252 */
 		}
-		/* README.md:273 */ {
-			/* README.md:277 */ pages, err := channel.Presence.Get().Pages(ctx)
-			/* README.md:278 */ if err != nil {
-				/* README.md:279 */ panic(err)
-				/* README.md:280 */
-			}
-			/* README.md:281 */ for pages.Next(ctx) {
-				/* README.md:282 */ for _, presence := range pages.Items() {
-					/* README.md:283 */ fmt.Println(presence)
-					/* README.md:284 */
-				}
-				/* README.md:285 */
-			}
-			/* README.md:286 */ if err := pages.Err(); err != nil {
-				/* README.md:287 */ panic(err)
-				/* README.md:288 */
-			}
-			/* README.md:292 */
+		/* README.md:253 */ if err != nil {
+			/* README.md:254 */ panic(err)
+			/* README.md:255 */
 		}
-		/* README.md:298 */ {
-			/* README.md:302 */ pages, err := channel.Presence.History().Pages(ctx)
-			/* README.md:303 */ if err != nil {
-				/* README.md:304 */ panic(err)
-				/* README.md:305 */
-			}
-			/* README.md:306 */ for pages.Next(ctx) {
-				/* README.md:307 */ for _, presence := range pages.Items() {
-					/* README.md:308 */ fmt.Println(presence)
-					/* README.md:309 */
-				}
-				/* README.md:310 */
+		/* README.md:261 */ page, err = channel.Presence.Get(ctx, nil)
+		/* README.md:262 */ for ; err == nil && page != nil; page, err = page.Next(ctx) {
+			/* README.md:263 */ for _, presence := range page.PresenceMessages() {
+				/* README.md:264 */ fmt.Println(presence)
+				/* README.md:265 */
 			}
-			/* README.md:311 */ if err := pages.Err(); err != nil {
-				/* README.md:312 */ panic(err)
-				/* README.md:313 */
-			}
-			/* README.md:317 */
+			/* README.md:266 */
 		}
-		/* README.md:323 */ {
-			/* README.md:327 */ pages, err := client.Stats().Pages(ctx)
-			/* README.md:328 */ if err != nil {
-				/* README.md:329 */ panic(err)
-				/* README.md:330 */
-			}
-			/* README.md:331 */ for pages.Next(ctx) {
-				/* README.md:332 */ for _, stat := range pages.Items() {
-					/* README.md:333 */ fmt.Println(stat)
-					/* README.md:334 */
-				}
-				/* README.md:335 */
+		/* README.md:267 */ if err != nil {
+			/* README.md:268 */ panic(err)
+			/* README.md:269 */
+		}
+		/* README.md:275 */ page, err = channel.Presence.History(ctx, nil)
+		/* README.md:276 */ for ; err == nil && page != nil; page, err = page.Next(ctx) {
+			/* README.md:277 */ for _, presence := range page.PresenceMessages() {
+				/* README.md:278 */ fmt.Println(presence)
+				/* README.md:279 */
 			}
-			/* README.md:336 */ if err := pages.Err(); err != nil {
-				/* README.md:337 */ panic(err)
-				/* README.md:338 */
+			/* README.md:280 */
+		}
+		/* README.md:281 */ if err != nil {
+			/* README.md:282 */ panic(err)
+			/* README.md:283 */
+		}
+		/* README.md:289 */ page, err = client.Stats(ctx, &ably.PaginateParams{})
+		/* README.md:290 */ for ; err == nil && page != nil; page, err = page.Next(ctx) {
+			/* README.md:291 */ for _, stat := range page.Stats() {
+				/* README.md:292 */ fmt.Println(stat)
+				/* README.md:293 */
 			}
-			/* README.md:342 */
+			/* README.md:294 */
+		}
+		/* README.md:295 */ if err != nil {
+			/* README.md:296 */ panic(err)
+			/* README.md:297 */
 		}
-		/* README.md:346 */
+		/* README.md:301 */
 	}
 }
diff --git a/ably/realtime_channel_spec_test.go b/ably/realtime_channel_spec_test.go
index 0355a36..97763b4 100644
--- a/ably/realtime_channel_spec_test.go
+++ b/ably/realtime_channel_spec_test.go
@@ -469,144 +469,6 @@ func TestRealtimeChannel_RTL6c5_NoImplicitAttach(t *testing.T) {
 	}
 }
 
-func TestRealtimeChannel_RTL2f_RTL12_HandleResume(t *testing.T) {
-	t.Parallel()
-	const channelRetryTimeout = 123 * time.Millisecond
-
-	setup := func(t *testing.T) (
-		in, out chan *proto.ProtocolMessage,
-		c *ably.Realtime,
-		channel *ably.RealtimeChannel,
-		stateChanges ably.ChannelStateChanges,
-		afterCalls chan ablytest.AfterCall,
-	) {
-		in = make(chan *proto.ProtocolMessage, 1)
-		out = make(chan *proto.ProtocolMessage, 16)
-		afterCalls = make(chan ablytest.AfterCall, 1)
-		now, after := ablytest.TimeFuncs(afterCalls)
-
-		c, _ = ably.NewRealtime(
-			ably.WithToken("fake:token"),
-			ably.WithAutoConnect(false),
-			ably.WithNow(now),
-			ably.WithAfter(after),
-			ably.WithChannelRetryTimeout(channelRetryTimeout),
-			ably.WithDial(ablytest.MessagePipe(in, out)),
-		)
-
-		in <- &proto.ProtocolMessage{
-			Action:            proto.ActionConnected,
-			ConnectionID:      "connection-id",
-			ConnectionDetails: &proto.ConnectionDetails{},
-		}
-
-		err := ablytest.Wait(ablytest.ConnWaiter(c, c.Connect, ably.ConnectionEventConnected), nil)
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		channel = c.Channels.Get("test")
-
-		ctx, cancel := context.WithCancel(context.Background())
-		cancel()
-		channel.Attach(ctx)
-
-		ablytest.Instantly.Recv(t, nil, out, t.Fatalf) // Consume ATTACHING
-
-		stateChanges = make(ably.ChannelStateChanges, 10)
-		channel.OnAll(stateChanges.Receive)
-
-		return
-	}
-
-	flags := make(map[proto.Flag]string)
-	flags[proto.FlagHasPresence] = "flag has_presence is provided"
-	flags[proto.FlagHasBacklog] = "flag has_backlog is provided"
-	flags[proto.FlagResumed] = "flag resumed is provided"
-
-	for flag, flagDescription := range flags {
-		t.Run(fmt.Sprintf("RTL2f: when %v, set channelChangeState resume to %v", flagDescription, flag == proto.FlagResumed), func(t *testing.T) {
-			t.Parallel()
-			isResume := flag == proto.FlagResumed
-			in, _, _, channel, stateChanges, afterCalls := setup(t)
-			// Get the channel to ATTACHED.
-
-			in <- &proto.ProtocolMessage{
-				Action:  proto.ActionAttached,
-				Channel: channel.Name,
-				Flags:   flag,
-			}
-
-			var change ably.ChannelStateChange
-
-			ablytest.Instantly.Recv(t, &change, stateChanges, t.Fatalf)
-			if expected, got := ably.ChannelStateAttached, change.Current; expected != got {
-				t.Fatalf("expected %v; got %v (event: %+v)", expected, got, change)
-			}
-
-			if change.Resumed != isResume {
-				t.Fatalf("expected resumed to be %v (event: %+v)", isResume, change)
-			}
-
-			// Expect the retry loop to be finished.
-			ablytest.Instantly.NoRecv(t, nil, afterCalls, t.Fatalf)
-			ablytest.Instantly.NoRecv(t, nil, stateChanges, t.Fatalf)
-		})
-	}
-	t.Run("RTL12: when RE-ATTACH with error, set ChannelEventUpdated", func(t *testing.T) {
-		t.Parallel()
-		in, _, _, channel, stateChanges, afterCalls := setup(t)
-
-		// Get the channel to ATTACHED.
-		in <- &proto.ProtocolMessage{
-			Action:  proto.ActionAttached,
-			Channel: channel.Name,
-			Flags:   proto.FlagResumed,
-		}
-
-		var change ably.ChannelStateChange
-
-		ablytest.Instantly.Recv(t, &change, stateChanges, t.Fatalf)
-		if expected, got := ably.ChannelStateAttached, change.Current; expected != got {
-			t.Fatalf("expected %v; got %v (event: %+v)", expected, got, change)
-		}
-
-		// Re-attach the channel
-		errInfo := proto.ErrorInfo{
-			StatusCode: 500,
-			Code:       50500,
-			Message:    "fake error",
-		}
-
-		in <- &proto.ProtocolMessage{
-			Action:  proto.ActionAttached,
-			Channel: channel.Name,
-			Flags:   0,
-			Error:   &errInfo,
-		}
-
-		ablytest.Instantly.Recv(t, &change, stateChanges, t.Fatalf)
-		if expected, got := ably.ChannelEventUpdate, change.Event; expected != got {
-			t.Fatalf("expected %v; got %v (event: %+v)", expected, got, change)
-		}
-		if expected, got := ably.ChannelStateAttached, change.Current; expected != got {
-			t.Fatalf("expected %v; got %v (event: %+v)", expected, got, change)
-		}
-		if expected, got := ably.ChannelStateAttached, change.Previous; expected != got {
-			t.Fatalf("expected %v; got %v (event: %+v)", expected, got, change)
-		}
-		if change.Resumed {
-			t.Fatalf("expected resume to be false")
-		}
-		if got := fmt.Sprint(change.Reason); !strings.Contains(got, errInfo.Message) {
-			t.Fatalf("expected %+v; got %v (error: %+v)", errInfo, got, change.Reason)
-		}
-		// Expect the retry loop to be finished.
-		ablytest.Instantly.NoRecv(t, nil, afterCalls, t.Fatalf)
-		ablytest.Instantly.NoRecv(t, nil, stateChanges, t.Fatalf)
-	})
-}
-
 func TestRealtimeChannel_RTL13_HandleDetached(t *testing.T) {
 	t.Parallel()
 
@@ -647,13 +509,14 @@ func TestRealtimeChannel_RTL13_HandleDetached(t *testing.T) {
 		channel = c.Channels.Get("test")
 
 		ctx, cancel := context.WithCancel(context.Background())
-		cancel()
-		channel.Attach(ctx)
+		go channel.Attach(ctx)
+		defer cancel()
 
-		ablytest.Instantly.Recv(t, nil, out, t.Fatalf) // Consume outgoing ATTACHING msg
+		ablytest.Instantly.Recv(t, nil, out, t.Fatalf) // Consume ATTACH
 
 		stateChanges = make(ably.ChannelStateChanges, 10)
 		channel.OnAll(stateChanges.Receive)
+
 		return
 	}
 
@@ -704,7 +567,7 @@ func TestRealtimeChannel_RTL13_HandleDetached(t *testing.T) {
 			t.Fatalf("expected %v; got %v (message: %+v)", expected, got, msg)
 		}
 
-		// TODO: Test attach failure too, which requires RTL4e.
+		// TODO: Test attach failure too, which requires RTL4ef.
 
 		in <- &proto.ProtocolMessage{
 			Action:  proto.ActionAttached,
@@ -848,231 +711,3 @@ func TestRealtimeChannel_RTL13_HandleDetached(t *testing.T) {
 		ablytest.Instantly.NoRecv(t, nil, out, t.Fatalf)
 	})
 }
-
-func TestRealtimeChannel_RTL17_IgnoreMessagesWhenNotAttached(t *testing.T) {
-	t.Parallel()
-
-	const channelRetryTimeout = 123 * time.Millisecond
-
-	setup := func(t *testing.T) (
-		in, out chan *proto.ProtocolMessage,
-		msg chan *proto.Message,
-		c *ably.Realtime,
-		channel *ably.RealtimeChannel,
-		stateChanges ably.ChannelStateChanges,
-	) {
-		in = make(chan *proto.ProtocolMessage, 1)
-		out = make(chan *proto.ProtocolMessage, 16)
-		msg = make(chan *proto.Message, 1)
-		afterCalls := make(chan ablytest.AfterCall, 1)
-		now, after := ablytest.TimeFuncs(afterCalls)
-
-		c, _ = ably.NewRealtime(
-			ably.WithToken("fake:token"),
-			ably.WithAutoConnect(false),
-			ably.WithNow(now),
-			ably.WithAfter(after),
-			ably.WithChannelRetryTimeout(channelRetryTimeout),
-			ably.WithDial(ablytest.MessagePipe(in, out)),
-		)
-
-		in <- &proto.ProtocolMessage{
-			Action:            proto.ActionConnected,
-			ConnectionID:      "connection-id",
-			ConnectionDetails: &proto.ConnectionDetails{},
-		}
-
-		err := ablytest.Wait(ablytest.ConnWaiter(c, c.Connect, ably.ConnectionEventConnected), nil)
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		channel = c.Channels.Get("test")
-
-		stateChanges = make(ably.ChannelStateChanges, 10)
-		channel.OnAll(stateChanges.Receive)
-
-		ctx, cancel := context.WithCancel(context.Background())
-		cancel()
-
-		channel.SubscribeAll(ctx, func(message *ably.Message) {
-			msg <- message
-		})
-
-		channel.Attach(ctx)
-		return
-	}
-
-	t.Run("Shouldn't receive message when not attached", func(t *testing.T) {
-		t.Parallel()
-
-		in, out, msg, _, channel, stateChanges := setup(t)
-
-		receiveMessage := func() {
-			message := &ably.Message{
-				ID:           "Id",
-				ClientID:     "clientId",
-				ConnectionID: "connectionId",
-				Name:         "Sample Name",
-				Data:         "Sample Data",
-				Encoding:     "encoding",
-				Timestamp:    0,
-				Extras:       nil,
-			}
-			in <- &proto.ProtocolMessage{
-				Action:        proto.ActionMessage,
-				Channel:       channel.Name,
-				ID:            "uniqueId",
-				MsgSerial:     3,
-				ChannelSerial: "channelSerial",
-				Messages:      append(make([]*ably.Message, 0), message),
-			}
-		}
-
-		var change ably.ChannelStateChange
-
-		ablytest.Instantly.Recv(t, &change, stateChanges, t.Fatalf) // Consume ATTACHING
-		if expected, got := ably.ChannelStateAttaching, change.Current; expected != got {
-			t.Fatalf("expected %v; got %v (event: %+v)", expected, got, change)
-		}
-
-		// Shouldn't receive message when state is ATTACHING
-		receiveMessage()
-		ablytest.Instantly.NoRecv(t, nil, msg, t.Fatalf)
-
-		// Get the channel to ATTACHED.
-		in <- &proto.ProtocolMessage{
-			Action:  proto.ActionAttached,
-			Channel: channel.Name,
-		}
-
-		ablytest.Instantly.Recv(t, &change, stateChanges, t.Fatalf) // Consume ATTACHED
-		if expected, got := ably.ChannelStateAttached, change.Current; expected != got {
-			t.Fatalf("expected %v; got %v (event: %+v)", expected, got, change)
-		}
-
-		channel.SubscribeAll(context.Background(), func(message *ably.Message) {
-			msg <- message
-		})
-
-		// receive message when state is ATTACHED
-		receiveMessage()
-		ablytest.Instantly.Recv(t, nil, msg, t.Fatalf)
-
-		ctx, cancel := context.WithCancel(context.Background())
-		cancel()
-		channel.Detach(ctx)
-		// Get the channel to DETACHED.
-
-		ablytest.Instantly.Recv(t, nil, out, t.Fatalf) // Consume DETACHING
-
-		ablytest.Instantly.Recv(t, &change, stateChanges, t.Fatalf) // DETACHING channel state
-		if expected, got := ably.ChannelStateDetaching, change.Current; expected != got {
-			t.Fatalf("expected %v; got %v (event: %+v)", expected, got, change)
-		}
-
-		in <- &proto.ProtocolMessage{
-			Action:  proto.ActionDetached,
-			Channel: channel.Name,
-		}
-
-		ablytest.Instantly.Recv(t, &change, stateChanges, t.Fatalf) // Consume DETACHED
-		if expected, got := ably.ChannelStateDetached, change.Current; expected != got {
-			t.Fatalf("expected %v; got %v (event: %+v)", expected, got, change)
-		}
-
-		// Shouldn't receive message when state is DETACHED
-		receiveMessage()
-		ablytest.Instantly.NoRecv(t, nil, msg, t.Fatalf)
-
-	})
-}
-
-func TestRealtimeChannel_RTL14_HandleChannelError(t *testing.T) {
-	t.Parallel()
-
-	const channelRetryTimeout = 123 * time.Millisecond
-
-	setup := func(t *testing.T) (
-		in, out chan *proto.ProtocolMessage,
-		c *ably.Realtime,
-		channel *ably.RealtimeChannel,
-		stateChanges ably.ChannelStateChanges,
-		afterCalls chan ablytest.AfterCall,
-	) {
-		in = make(chan *proto.ProtocolMessage, 1)
-		out = make(chan *proto.ProtocolMessage, 16)
-		afterCalls = make(chan ablytest.AfterCall, 1)
-		now, after := ablytest.TimeFuncs(afterCalls)
-
-		c, _ = ably.NewRealtime(
-			ably.WithToken("fake:token"),
-			ably.WithAutoConnect(false),
-			ably.WithNow(now),
-			ably.WithAfter(after),
-			ably.WithChannelRetryTimeout(channelRetryTimeout),
-			ably.WithDial(ablytest.MessagePipe(in, out)),
-		)
-
-		in <- &proto.ProtocolMessage{
-			Action:            proto.ActionConnected,
-			ConnectionID:      "connection-id",
-			ConnectionDetails: &proto.ConnectionDetails{},
-		}
-
-		err := ablytest.Wait(ablytest.ConnWaiter(c, c.Connect, ably.ConnectionEventConnected), nil)
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		channel = c.Channels.Get("test")
-
-		ctx, cancel := context.WithCancel(context.Background())
-		cancel()
-		channel.Attach(ctx)
-
-		ablytest.Instantly.Recv(t, nil, out, t.Fatalf) // Consume ATTACH
-
-		stateChanges = make(ably.ChannelStateChanges, 10)
-		channel.OnAll(stateChanges.Receive)
-
-		return
-	}
-
-	t.Run("RTL14: when Error, should transition to failed state", func(t *testing.T) {
-		t.Parallel()
-		in, out, _, channel, stateChanges, afterCalls := setup(t)
-
-		errInfo := proto.ErrorInfo{
-			StatusCode: 500,
-			Code:       50500,
-			Message:    "fake error",
-		}
-
-		in <- &proto.ProtocolMessage{
-			Action:  proto.ActionError,
-			Channel: channel.Name,
-			Error:   &errInfo,
-		}
-
-		// Expect a state change with the error.
-
-		var change ably.ChannelStateChange
-		ablytest.Instantly.Recv(t, &change, stateChanges, t.Fatalf)
-		if expected, got := ably.ChannelStateFailed, change.Current; expected != got {
-			t.Fatalf("expected %v; got %v (event: %+v)", expected, got, change)
-		}
-
-		if got := fmt.Sprint(change.Reason); !strings.Contains(got, errInfo.Message) {
-			t.Fatalf("expected %+v; got %v (error: %+v)", errInfo, got, change.Reason)
-		}
-
-		if got := fmt.Sprint(channel.ErrorReason()); !strings.Contains(got, errInfo.Message) {
-			t.Fatalf("expected %+v; got %v (error: %+v)", errInfo, got, change.Reason)
-		}
-
-		ablytest.Instantly.NoRecv(t, nil, afterCalls, t.Fatalf)
-		ablytest.Instantly.NoRecv(t, nil, stateChanges, t.Fatalf)
-		ablytest.Instantly.NoRecv(t, nil, out, t.Fatalf)
-	})
-}
diff --git a/ably/realtime_channel_test.go b/ably/realtime_channel_test.go
index 387b9c8..879eb1d 100644
--- a/ably/realtime_channel_test.go
+++ b/ably/realtime_channel_test.go
@@ -2,6 +2,7 @@ package ably_test
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net/url"
 	"reflect"
@@ -105,6 +106,52 @@ func TestRealtimeChannel_Subscribe(t *testing.T) {
 	}
 }
 
+func TestRealtimeChannel_Detach(t *testing.T) {
+	t.Parallel()
+	app, client := ablytest.NewRealtime()
+	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
+	if err := ablytest.Wait(ablytest.ConnWaiter(client, client.Connect, ably.ConnectionEventConnected), nil); err != nil {
+		t.Fatal(err)
+	}
+	channel := client.Channels.Get("test")
+	sub, unsub, err := ablytest.ReceiveMessages(channel, "")
+	if err != nil {
+		t.Fatalf("ablytest.ReceiveMessages(channel)=%v", err)
+	}
+	defer unsub()
+	if err := channel.Publish(context.Background(), "hello", "world"); err != nil {
+		t.Fatalf("channel.Publish()=%v", err)
+	}
+	done := make(chan error)
+	go func() {
+		msg, ok := <-sub
+		if !ok {
+			done <- errors.New("did not receive published message")
+		}
+		if msg.Name != "hello" || !reflect.DeepEqual(msg.Data, "world") {
+			done <- fmt.Errorf(`want name="hello", data="world"; got %s, %v`, msg.Name, msg.Data)
+		}
+		done <- nil
+	}()
+	if state := channel.State(); state != ably.ChannelStateAttached {
+		t.Fatalf("want state=%v; got %v", ably.ChannelStateAttached, state)
+	}
+	if err := channel.Detach(context.Background()); err != nil {
+		t.Fatalf("channel.Detach(context.Background())=%v", err)
+	}
+	if err := ablytest.FullRealtimeCloser(client).Close(); err != nil {
+		t.Fatalf("ablytest.FullRealtimeCloser(client).Close()=%v", err)
+	}
+	select {
+	case err := <-done:
+		if err != nil {
+			t.Fatalf("waiting on subscribed channel close failed: err=%s", err)
+		}
+	case <-time.After(ablytest.Timeout):
+		t.Fatalf("waiting on subscribed channel close timed out after %v", ablytest.Timeout)
+	}
+}
+
 func TestRealtimeChannel_AttachWhileDisconnected(t *testing.T) {
 	t.Parallel()
 
diff --git a/ably/realtime_conn_spec_test.go b/ably/realtime_conn_spec_test.go
index c1c4cee..c993d60 100644
--- a/ably/realtime_conn_spec_test.go
+++ b/ably/realtime_conn_spec_test.go
@@ -2358,97 +2358,3 @@ func TestRealtimeConn_RTN19a(t *testing.T) {
 		t.Fatalf("expected %v got %v", expect, got)
 	}
 }
-
-func TestRealtimeConn_RTN24_RTN21_RTC8a_RTN4h_Override_ConnectionDetails_On_Connected(t *testing.T) {
-	t.Parallel()
-
-	in := make(chan *proto.ProtocolMessage, 1)
-	out := make(chan *proto.ProtocolMessage, 16)
-
-	c, _ := ably.NewRealtime(
-		ably.WithAutoConnect(false),
-		ably.WithToken("fake:token"),
-		ably.WithDial(ablytest.MessagePipe(in, out)),
-	)
-
-	connDetails := proto.ConnectionDetails{
-		ClientID:           "id1",
-		ConnectionKey:      "foo",
-		MaxFrameSize:       12,
-		MaxInboundRate:     14,
-		MaxMessageSize:     67,
-		ConnectionStateTTL: proto.DurationFromMsecs(time.Minute * 2),
-		MaxIdleInterval:    proto.DurationFromMsecs(time.Second),
-	}
-
-	in <- &proto.ProtocolMessage{
-		Action:            proto.ActionConnected,
-		ConnectionID:      "connection-id-1",
-		ConnectionDetails: &connDetails,
-	}
-
-	err := ablytest.Wait(ablytest.ConnWaiter(c, c.Connect, ably.ConnectionEventConnected), nil)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	newConnDetails := proto.ConnectionDetails{
-		ClientID:           "id2",
-		ConnectionKey:      "bar",
-		MaxFrameSize:       13,
-		MaxInboundRate:     15,
-		MaxMessageSize:     70,
-		ConnectionStateTTL: proto.DurationFromMsecs(time.Minute * 2),
-		MaxIdleInterval:    proto.DurationFromMsecs(time.Second),
-	}
-
-	errInfo := proto.ErrorInfo{
-		StatusCode: 500,
-		Code:       50500,
-		Message:    "fake error",
-	}
-
-	changes := make(ably.ConnStateChanges, 3)
-	off := c.Connection.OnAll(changes.Receive)
-	defer off()
-
-	//  Send new connection details
-	in <- &proto.ProtocolMessage{
-		Action:            proto.ActionConnected,
-		ConnectionID:      "connection-id-2",
-		ConnectionDetails: &newConnDetails,
-		Error:             &errInfo,
-	}
-
-	var newConnectionState ably.ConnectionStateChange
-	ablytest.Instantly.Recv(t, &newConnectionState, changes, t.Fatalf)
-
-	// RTN4h - can emit UPDATE event
-	if expected, got := ably.ConnectionEventUpdate, newConnectionState.Event; expected != got {
-		t.Fatalf("expected %v; got %v (event: %+v)", expected, got, newConnectionState)
-	}
-	if expected, got := ably.ConnectionStateConnected, newConnectionState.Current; expected != got {
-		t.Fatalf("expected %v; got %v (event: %+v)", expected, got, newConnectionState)
-	}
-	if expected, got := ably.ConnectionStateConnected, newConnectionState.Previous; expected != got {
-		t.Fatalf("expected %v; got %v (event: %+v)", expected, got, newConnectionState)
-	}
-	if got := fmt.Sprint(newConnectionState.Reason); !strings.Contains(got, errInfo.Message) {
-		t.Fatalf("expected %+v; got %v (error: %+v)", errInfo, got, newConnectionState.Reason)
-	}
-	if got := c.Connection.ErrorReason().Message(); !strings.Contains(got, errInfo.Message) {
-		t.Fatalf("expected %+v; got %v (error: %+v)", errInfo, got, c.Connection.ErrorReason().Message())
-	}
-	// RTN21 - new connection details over write old values
-	if c.Connection.Key() != newConnDetails.ConnectionKey {
-		t.Fatalf("expected %v; got %v", newConnDetails.ConnectionKey, c.Connection.Key())
-	}
-
-	if c.Auth.ClientID() != newConnDetails.ClientID {
-		t.Fatalf("expected %v; got %v", newConnDetails.ClientID, c.Auth.ClientID())
-	}
-
-	if c.Connection.ID() != "connection-id-2" {
-		t.Fatalf("expected %v; got %v", "connection-id-2", c.Connection.ID())
-	}
-}
diff --git a/ably/rest_channel_spec_test.go b/ably/rest_channel_spec_test.go
deleted file mode 100644
index 1128920..0000000
--- a/ably/rest_channel_spec_test.go
+++ /dev/null
@@ -1,211 +0,0 @@
-package ably_test
-
-import (
-	"context"
-	"fmt"
-	"reflect"
-	"testing"
-
-	"github.com/ably/ably-go/ably"
-	"github.com/ably/ably-go/ably/ablytest"
-)
-
-func TestRSL1f1(t *testing.T) {
-	t.Parallel()
-	app, err := ablytest.NewSandbox(nil)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer app.Close()
-	opts := app.Options()
-	// RSL1f
-	opts = append(opts, ably.WithUseTokenAuth(false))
-	client, err := ably.NewREST(opts...)
-	if err != nil {
-		t.Fatal(err)
-	}
-	channel := client.Channels.Get("RSL1f")
-	id := "any_client_id"
-	var msgs []*ably.Message
-	size := 10
-	for i := 0; i < size; i++ {
-		msgs = append(msgs, &ably.Message{
-			ClientID: id,
-			Data:     fmt.Sprint(i),
-		})
-	}
-	err = channel.PublishMultiple(context.Background(), msgs)
-	if err != nil {
-		t.Fatal(err)
-	}
-	var m []*ably.Message
-	err = ablytest.AllPages(&m, channel.History())
-	if err != nil {
-		t.Fatal(err)
-	}
-	n := len(m)
-	if n != size {
-		t.Errorf("expected %d messages got %d", size, n)
-	}
-	for _, v := range m {
-		if v.ClientID != id {
-			t.Errorf("expected clientId %s got %s data:%v", id, v.ClientID, v.Data)
-		}
-	}
-}
-
-func TestRSL1g(t *testing.T) {
-	t.Parallel()
-	app, err := ablytest.NewSandbox(nil)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer app.Close()
-	opts := append(app.Options(),
-		ably.WithUseTokenAuth(true),
-	)
-	clientID := "some_client_id"
-	opts = append(opts, ably.WithClientID(clientID))
-	client, err := ably.NewREST(opts...)
-	if err != nil {
-		t.Fatal(err)
-	}
-	t.Run("RSL1g1b", func(ts *testing.T) {
-		channel := client.Channels.Get("RSL1g1b")
-		err := channel.PublishMultiple(context.Background(), []*ably.Message{
-			{Name: "some 1"},
-			{Name: "some 2"},
-			{Name: "some 3"},
-		})
-		if err != nil {
-			ts.Fatal(err)
-		}
-		var history []*ably.Message
-		err = ablytest.AllPages(&history, channel.History())
-		if err != nil {
-			ts.Fatal(err)
-		}
-		for _, m := range history {
-			if m.ClientID != clientID {
-				ts.Errorf("expected %s got %s", clientID, m.ClientID)
-			}
-		}
-	})
-	t.Run("RSL1g2", func(ts *testing.T) {
-		channel := client.Channels.Get("RSL1g2")
-		err := channel.PublishMultiple(context.Background(), []*ably.Message{
-			{Name: "1", ClientID: clientID},
-			{Name: "2", ClientID: clientID},
-			{Name: "3", ClientID: clientID},
-		})
-		if err != nil {
-			ts.Fatal(err)
-		}
-		var history []*ably.Message
-		err = ablytest.AllPages(&history, channel.History())
-		if err != nil {
-			ts.Fatal(err)
-		}
-		for _, m := range history {
-			if m.ClientID != clientID {
-				ts.Errorf("expected %s got %s", clientID, m.ClientID)
-			}
-		}
-	})
-	t.Run("RSL1g3", func(ts *testing.T) {
-		channel := client.Channels.Get("RSL1g3")
-		err := channel.PublishMultiple(context.Background(), []*ably.Message{
-			{Name: "1", ClientID: clientID},
-			{Name: "2", ClientID: "other client"},
-			{Name: "3", ClientID: clientID},
-		})
-		if err == nil {
-			ts.Fatal("expected an error")
-		}
-	})
-}
-
-func TestHistory_RSL2_RSL2b3(t *testing.T) {
-	t.Parallel()
-
-	for _, limit := range []int{2, 3, 20} {
-		t.Run(fmt.Sprintf("limit=%d", limit), func(t *testing.T) {
-			t.Parallel()
-			app, rest := ablytest.NewREST()
-			defer app.Close()
-			channel := rest.Channels.Get("test")
-
-			fixtures := historyFixtures()
-			channel.PublishMultiple(context.Background(), fixtures)
-
-			err := ablytest.TestPagination(
-				reverseMessages(fixtures),
-				channel.History(ably.HistoryWithLimit(limit)),
-				limit,
-				ablytest.PaginationWithEqual(messagesEqual),
-			)
-			if err != nil {
-				t.Fatal(err)
-			}
-		})
-	}
-}
-
-func TestHistory_Direction_RSL2b2(t *testing.T) {
-	t.Parallel()
-
-	for _, c := range []struct {
-		direction ably.Direction
-		expected  []*ably.Message
-	}{
-		{
-			direction: ably.Backwards,
-			expected:  reverseMessages(historyFixtures()),
-		},
-		{
-			direction: ably.Forwards,
-			expected:  historyFixtures(),
-		},
-	} {
-		c := c
-		t.Run(fmt.Sprintf("direction=%v", c.direction), func(t *testing.T) {
-			app, rest := ablytest.NewREST()
-			defer app.Close()
-			channel := rest.Channels.Get("test")
-
-			fixtures := historyFixtures()
-			channel.PublishMultiple(context.Background(), fixtures)
-
-			expected := c.expected
-
-			err := ablytest.TestPagination(expected, channel.History(
-				ably.HistoryWithLimit(len(expected)),
-				ably.HistoryWithDirection(c.direction),
-			), 100, ablytest.PaginationWithEqual(messagesEqual))
-			if err != nil {
-				t.Fatal(err)
-			}
-		})
-	}
-}
-
-func historyFixtures() []*ably.Message {
-	var fixtures []*ably.Message
-	for i := 0; i < 10; i++ {
-		fixtures = append(fixtures, &ably.Message{Name: fmt.Sprintf("msg%d", i)})
-	}
-	return fixtures
-}
-
-func reverseMessages(msgs []*ably.Message) []*ably.Message {
-	var reversed []*ably.Message
-	for i := len(msgs) - 1; i >= 0; i-- {
-		reversed = append(reversed, msgs[i])
-	}
-	return reversed
-}
-
-func messagesEqual(x, y interface{}) bool {
-	mx, my := x.(*ably.Message), y.(*ably.Message)
-	return mx.Name == my.Name && reflect.DeepEqual(mx.Data, my.Data)
-}
diff --git a/ably/rest_channel_test.go b/ably/rest_channel_test.go
index d4776f5..e77a673 100644
--- a/ably/rest_channel_test.go
+++ b/ably/rest_channel_test.go
@@ -44,8 +44,7 @@ func TestRESTChannel(t *testing.T) {
 		m := make(map[string]dataSample)
 		if ablytest.NoBinaryProtocol {
 			m["string"] = dataSample{
-				encoding: proto.UTF8,
-				data:     "string",
+				data: "string",
 			}
 			m["binary"] = dataSample{
 				encoding: proto.Base64,
@@ -59,12 +58,10 @@ func TestRESTChannel(t *testing.T) {
 			}
 		} else {
 			m["string"] = dataSample{
-				encoding: proto.UTF8,
-				data:     "string",
+				data: "string",
 			}
 			m["binary"] = dataSample{
-				encoding: proto.UTF8,
-				data:     "string",
+				data: "string",
 			}
 			m["json"] = dataSample{
 				encoding: proto.JSON,
@@ -80,45 +77,82 @@ func TestRESTChannel(t *testing.T) {
 			}
 		}
 		ts.Run("is available in the history", func(ts *testing.T) {
-			var messages []*ably.Message
-			err := ablytest.AllPages(&messages, channel.History())
+			page, err := channel.History(context.Background(), nil)
 			if err != nil {
 				ts.Fatal(err)
 			}
+			messages := page.Messages()
 			if len(messages) == 0 {
 				ts.Fatal("expected messages")
 			}
+			for _, v := range messages {
+				e := m[v.Name]
+				if v.Encoding != e.encoding {
+					ts.Errorf("expected %s got %s ", e.encoding, v.Encoding)
+				}
+			}
 		})
 	})
 
-	t.Run("PublishMultiple", func(ts *testing.T) {
+	t.Run("History", func(ts *testing.T) {
+		historyRESTChannel := client.Channels.Get("channelhistory")
+		for i := 0; i < 2; i++ {
+			historyRESTChannel.Publish(context.Background(), "breakingnews", "Another Shark attack!!")
+		}
+
+		page1, err := historyRESTChannel.History(context.Background(), &ably.PaginateParams{Limit: 1})
+		if err != nil {
+			ts.Fatal(err)
+		}
+		if len(page1.Messages()) != 1 {
+			ts.Errorf("expected 1 message got %d", len(page1.Messages()))
+		}
+		if len(page1.Items()) != 1 {
+			ts.Errorf("expected 1 item got %d", len(page1.Items()))
+		}
+
+		page2, err := page1.Next(context.Background())
+		if err != nil {
+			ts.Fatal(err)
+		}
+		if len(page2.Messages()) != 1 {
+			ts.Errorf("expected 1 message got %d", len(page2.Messages()))
+		}
+		if len(page2.Items()) != 1 {
+			ts.Errorf("expected 1 item got %d", len(page2.Items()))
+		}
+	})
+
+	t.Run("PublishBatch", func(ts *testing.T) {
 		encodingRESTChannel := client.Channels.Get("this?is#an?encoding#channel")
 		messages := []*ably.Message{
 			{Name: "send", Data: "test data 1"},
 			{Name: "send", Data: "test data 2"},
 		}
-		err := encodingRESTChannel.PublishMultiple(context.Background(), messages)
+		err := encodingRESTChannel.PublishBatch(context.Background(), messages)
 		if err != nil {
 			ts.Fatal(err)
 		}
-		var history []*ably.Message
-		err = ablytest.AllPages(&history, encodingRESTChannel.History(ably.HistoryWithLimit(2)))
+		page, err := encodingRESTChannel.History(context.Background(), &ably.PaginateParams{Limit: 2})
 		if err != nil {
 			ts.Fatal(err)
 		}
-		if len(history) != 2 {
-			ts.Errorf("expected 2 messages got %d", len(history))
+		if len(page.Messages()) != 2 {
+			ts.Errorf("expected 2 messages got %d", len(page.Messages()))
+		}
+		if len(page.Items()) != 2 {
+			ts.Errorf("expected 2 items got %d", len(page.Items()))
 		}
 	})
 
-	t.Run("encryption", func(t *testing.T) {
+	t.Run("encryption", func(ts *testing.T) {
 		key, err := base64.StdEncoding.DecodeString("WUP6u0K7MXI5Zeo0VppPwg==")
 		if err != nil {
-			t.Fatal(err)
+			ts.Fatal(err)
 		}
 		iv, err := base64.StdEncoding.DecodeString("HO4cYSP8LybPYBPZPHQOtg==")
 		if err != nil {
-			t.Fatal(err)
+			ts.Fatal(err)
 		}
 		opts := []ably.ChannelOption{ably.ChannelWithCipher(proto.CipherParams{
 			Key:       key,
@@ -137,14 +171,17 @@ func TestRESTChannel(t *testing.T) {
 		for _, v := range sample {
 			err := channel.Publish(context.Background(), v.event, v.message)
 			if err != nil {
-				t.Error(err)
+				ts.Error(err)
 			}
 		}
 
-		var msg []*ably.Message
-		err = ablytest.AllPages(&msg, channel.History())
+		rst, err := channel.History(context.Background(), nil)
 		if err != nil {
-			t.Fatal(err)
+			ts.Fatal(err)
+		}
+		msg := rst.Messages()
+		if len(msg) != len(sample) {
+			t.Errorf("expected %d messages got %d", len(sample), len(msg))
 		}
 		sort.Slice(msg, func(i, j int) bool {
 			return msg[i].Name < msg[j].Name
@@ -186,12 +223,11 @@ func TestIdempotentPublishing(t *testing.T) {
 				ts.Fatal(err)
 			}
 		}
-		var history []*ably.Message
-		err := ablytest.AllPages(&history, channel.History())
+		res, err := channel.History(context.Background(), nil)
 		if err != nil {
 			ts.Fatal(err)
 		}
-		n := len(history)
+		n := len(res.Items())
 		if n != 3 {
 			ts.Errorf("expected %d got %d", 3, n)
 		}
@@ -200,7 +236,7 @@ func TestIdempotentPublishing(t *testing.T) {
 	t.Run("when ID is included (#RSL1k2, #RSL1k5)", func(ts *testing.T) {
 		channel := client.Channels.Get("idempotent_test_2")
 		for range make([]struct{}, 3) {
-			err := channel.PublishMultiple(context.Background(), []*ably.Message{
+			err := channel.PublishBatch(context.Background(), []*ably.Message{
 				{
 					ID:   randomStr,
 					Data: randomStr,
@@ -210,17 +246,16 @@ func TestIdempotentPublishing(t *testing.T) {
 				ts.Fatal(err)
 			}
 		}
-		var history []*ably.Message
-		err := ablytest.AllPages(&history, channel.History())
+		res, err := channel.History(context.Background(), nil)
 		if err != nil {
 			ts.Fatal(err)
 		}
-		n := len(history)
+		n := len(res.Items())
 		if n != 1 {
 			// three REST publishes result in only one message being published
 			ts.Errorf("expected %d got %d", 1, n)
 		}
-		msg := history[0]
+		msg := res.Messages()[0]
 		if msg.ID != randomStr {
 			ts.Errorf("expected id to be %s got %s", randomStr, msg.ID)
 		}
@@ -228,7 +263,7 @@ func TestIdempotentPublishing(t *testing.T) {
 
 	t.Run("multiple messages in one publish operation (#RSL1k3)", func(ts *testing.T) {
 		channel := client.Channels.Get("idempotent_test_3")
-		err := channel.PublishMultiple(context.Background(), []*ably.Message{
+		err := channel.PublishBatch(context.Background(), []*ably.Message{
 			{
 				ID:   randomStr,
 				Data: randomStr,
@@ -259,19 +294,19 @@ func TestIdempotentPublishing(t *testing.T) {
 				ID: fmt.Sprintf("%s:%d", randomStr, i),
 			})
 		}
-		err := channel.PublishMultiple(context.Background(), m)
+		err := channel.PublishBatch(context.Background(), m)
 		if err != nil {
 			ts.Fatal(err)
 		}
-		var messages []*ably.Message
-		err = ablytest.AllPages(&messages, channel.History())
+		res, err := channel.History(context.Background(), nil)
 		if err != nil {
 			ts.Fatal(err)
 		}
-		n := len(messages)
+		n := len(res.Items())
 		if n != 3 {
 			ts.Errorf("expected %d got %d", 3, n)
 		}
+		messages := res.Messages()
 
 		// we need to sort so we we can easily test the serial in order.
 		sort.Slice(messages, func(i, j int) bool {
@@ -305,11 +340,11 @@ func TestIdempotentPublishing(t *testing.T) {
 		if err != nil {
 			ts.Fatal(err)
 		}
-		var m []*ably.Message
-		err = ablytest.AllPages(&m, channel.History())
+		res, err := channel.History(context.Background(), nil)
 		if err != nil {
 			ts.Fatal(err)
 		}
+		m := res.Messages()
 		if len(m) != 1 {
 			ts.Fatalf("expected %d got %d", 1, len(m))
 		}
@@ -335,7 +370,7 @@ func TestIdempotentPublishing(t *testing.T) {
 	t.Run("publishing a batch of messages", func(ts *testing.T) {
 		channel := client.Channels.Get("idempotent_test_6")
 		name := "event"
-		err := channel.PublishMultiple(context.Background(), []*ably.Message{
+		err := channel.PublishBatch(context.Background(), []*ably.Message{
 			{Name: name},
 			{Name: name},
 			{Name: name},
@@ -343,11 +378,11 @@ func TestIdempotentPublishing(t *testing.T) {
 		if err != nil {
 			ts.Fatal(err)
 		}
-		var m []*ably.Message
-		err = ablytest.AllPages(&m, channel.History())
+		res, err := channel.History(context.Background(), nil)
 		if err != nil {
 			ts.Fatal(err)
 		}
+		m := res.Messages()
 		if len(m) != 3 {
 			ts.Fatalf("expected %d got %d", 1, len(m))
 		}
@@ -442,11 +477,11 @@ func TestIdempotent_retry(t *testing.T) {
 			if retryCount != 2 {
 				ts.Errorf("expected %d retry attempts got %d", 2, retryCount)
 			}
-			var m []*ably.Message
-			err = ablytest.AllPages(&m, channel.History())
+			res, err := channel.History(context.Background(), nil)
 			if err != nil {
 				ts.Fatal(err)
 			}
+			m := res.Messages()
 			if len(m) != 1 {
 				ts.Errorf("expected %d messages got %d", 1, len(m))
 			}
@@ -460,16 +495,137 @@ func TestIdempotent_retry(t *testing.T) {
 				{Data: randomStr},
 			}
 			for range msgs {
-				channel.PublishMultiple(context.Background(), msgs)
+				channel.PublishBatch(context.Background(), msgs)
 			}
-			var m []*ably.Message
-			err := ablytest.AllPages(&m, channel.History())
+			res, err := channel.History(context.Background(), nil)
 			if err != nil {
 				ts.Fatal(err)
 			}
+			m := res.Messages()
 			if len(m) != len(msgs) {
 				ts.Errorf("expected %d messages got %d", len(msgs), len(m))
 			}
 		})
 	})
 }
+
+func TestRSL1f1(t *testing.T) {
+	t.Parallel()
+	app, err := ablytest.NewSandbox(nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer app.Close()
+	opts := app.Options()
+	// RSL1f
+	opts = append(opts, ably.WithUseTokenAuth(false))
+	client, err := ably.NewREST(opts...)
+	if err != nil {
+		t.Fatal(err)
+	}
+	channel := client.Channels.Get("RSL1f")
+	id := "any_client_id"
+	var msgs []*ably.Message
+	size := 10
+	for i := 0; i < size; i++ {
+		msgs = append(msgs, &ably.Message{
+			ClientID: id,
+			Data:     fmt.Sprint(i),
+		})
+	}
+	err = channel.PublishBatch(context.Background(), msgs)
+	if err != nil {
+		t.Fatal(err)
+	}
+	res, err := channel.History(context.Background(), nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	m := res.Messages()
+	n := len(m)
+	if n != size {
+		t.Errorf("expected %d messages got %d", size, n)
+	}
+	for _, v := range m {
+		if v.ClientID != id {
+			t.Errorf("expected clientId %s got %s data:%v", id, v.ClientID, v.Data)
+		}
+	}
+}
+
+func TestRSL1g(t *testing.T) {
+	t.Parallel()
+	app, err := ablytest.NewSandbox(nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer app.Close()
+	opts := append(app.Options(),
+		ably.WithUseTokenAuth(true),
+	)
+	clientID := "some_client_id"
+	opts = append(opts, ably.WithClientID(clientID))
+	client, err := ably.NewREST(opts...)
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Run("RSL1g1b", func(ts *testing.T) {
+		channel := client.Channels.Get("RSL1g1b")
+		err := channel.PublishBatch(context.Background(), []*ably.Message{
+			{Name: "some 1"},
+			{Name: "some 2"},
+			{Name: "some 3"},
+		})
+		if err != nil {
+			ts.Fatal(err)
+		}
+		pages, err := channel.History(context.Background(), nil)
+		if err != nil {
+			ts.Fatal(err)
+		}
+		msg := pages.Messages()
+		if len(msg) != 3 {
+			ts.Fatalf("expected 3 messages got %d", len(msg))
+		}
+		for _, m := range msg {
+			if m.ClientID != clientID {
+				ts.Errorf("expected %s got %s", clientID, m.ClientID)
+			}
+		}
+	})
+	t.Run("RSL1g2", func(ts *testing.T) {
+		channel := client.Channels.Get("RSL1g2")
+		err := channel.PublishBatch(context.Background(), []*ably.Message{
+			{Name: "1", ClientID: clientID},
+			{Name: "2", ClientID: clientID},
+			{Name: "3", ClientID: clientID},
+		})
+		if err != nil {
+			ts.Fatal(err)
+		}
+		pages, err := channel.History(context.Background(), nil)
+		if err != nil {
+			ts.Fatal(err)
+		}
+		msg := pages.Messages()
+		if len(msg) != 3 {
+			ts.Fatalf("expected 3 messages got %d", len(msg))
+		}
+		for _, m := range msg {
+			if m.ClientID != clientID {
+				ts.Errorf("expected %s got %s", clientID, m.ClientID)
+			}
+		}
+	})
+	t.Run("RSL1g3", func(ts *testing.T) {
+		channel := client.Channels.Get("RSL1g3")
+		err := channel.PublishBatch(context.Background(), []*ably.Message{
+			{Name: "1", ClientID: clientID},
+			{Name: "2", ClientID: "other client"},
+			{Name: "3", ClientID: clientID},
+		})
+		if err == nil {
+			ts.Fatal("expected an error")
+		}
+	})
+}
diff --git a/ably/rest_client_test.go b/ably/rest_client_test.go
index 52efec3..719a13c 100644
--- a/ably/rest_client_test.go
+++ b/ably/rest_client_test.go
@@ -1,21 +1,18 @@
 package ably_test
 
 import (
-	"bytes"
 	"context"
 	"crypto/tls"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"io/ioutil"
-	"net"
 	"net/http"
 	"net/http/httptest"
+	"net/http/httptrace"
 	"net/url"
-	"reflect"
 	"regexp"
 	"strings"
-	"sync/atomic"
 	"testing"
 	"time"
 
@@ -203,20 +200,19 @@ func TestRestClient(t *testing.T) {
 					errCh <- errors.New("timeout waiting for client.Stats to return nonempty value")
 					return
 				case <-tick:
-					page, err := client.Stats(
-						ably.StatsWithLimit(1),
-						ably.StatsWithStart(longAgo),
-						ably.StatsWithUnit(ably.PeriodMinute),
-					).Pages(context.Background())
+					page, err := client.Stats(context.Background(), &ably.PaginateParams{
+						Limit: 1,
+						ScopeParams: ably.ScopeParams{
+							Start: longAgo,
+							Unit:  proto.StatGranularityMinute,
+						},
+					})
 					if err != nil {
 						errCh <- err
 						return
 					}
-					if !page.Next(context.Background()) {
-						errCh <- page.Err()
-						return
-					}
-					if stats := page.Items(); len(stats) != 0 {
+
+					if stats := page.Stats(); len(stats) != 0 {
 						statsCh <- stats
 						return
 					}
@@ -260,7 +256,7 @@ func TestRSC7(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	_, _ = c.Request("POST", "/foo").Pages(context.Background())
+	_, _ = c.Request(context.Background(), "POST", "/foo", nil, nil, nil)
 
 	var req *http.Request
 	ablytest.Instantly.Recv(t, &req, requests, t.Fatalf)
@@ -546,28 +542,19 @@ func TestRESTChannels_RSN1(t *testing.T) {
 }
 
 func TestFixConnLeak_ISSUE89(t *testing.T) {
+	var trackRecord []httptrace.GotConnInfo
+	trace := &httptrace.ClientTrace{
+		GotConn: func(c httptrace.GotConnInfo) {
+			trackRecord = append(trackRecord, c)
+		},
+	}
 	app, err := ablytest.NewSandbox(nil)
 	if err != nil {
 		t.Fatal(err)
 	}
 	defer app.Close()
-
-	var conns []*connCloseTracker
-
-	httpClient := ablytest.NewHTTPClientNoKeepAlive()
-	transport := httpClient.Transport.(*http.Transport)
-	dial := transport.Dial
-	transport.Dial = func(network, address string) (net.Conn, error) {
-		c, err := dial(network, address)
-		if err != nil {
-			return nil, err
-		}
-		tracked := &connCloseTracker{Conn: c}
-		conns = append(conns, tracked)
-		return tracked, nil
-	}
-
-	opts := app.Options(ably.WithHTTPClient(httpClient))
+	opts := app.Options(ably.WithHTTPClient(ablytest.NewHTTPClientNoKeepAlive()))
+	opts = append(opts, ably.WithTrace(trace))
 	client, err := ably.NewREST(opts...)
 	if err != nil {
 		t.Fatal(err)
@@ -580,220 +567,16 @@ func TestFixConnLeak_ISSUE89(t *testing.T) {
 		}
 	}
 
-	for _, c := range conns {
-		if !ablytest.Before(1 * time.Second).IsTrue(func() bool {
-			return atomic.LoadUintptr(&c.closed) != 0
-		}) {
-			t.Errorf("conn to %v wasn't closed", c.RemoteAddr())
-		}
-	}
-}
-
-type connCloseTracker struct {
-	net.Conn
-	closed uintptr
-}
-
-func (c *connCloseTracker) Close() error {
-	atomic.StoreUintptr(&c.closed, 1)
-	return c.Conn.Close()
-}
-
-func TestStatsPagination_RSC6a_RSCb3(t *testing.T) {
-	t.Parallel()
-
-	for _, limit := range []int{2, 3, 20} {
-		t.Run(fmt.Sprintf("limit=%d", limit), func(t *testing.T) {
-			t.Parallel()
-			app, rest := ablytest.NewREST()
-			defer app.Close()
-
-			fixtures := statsFixtures()
-			postStats(app, fixtures)
-
-			err := ablytest.TestPagination(
-				reverseStats(fixtures),
-				rest.Stats(ably.StatsWithLimit(limit)),
-				limit,
-			)
-			if err != nil {
-				t.Fatal(err)
-			}
-		})
-	}
-}
-
-func TestStats_StartEnd_RSC6b1(t *testing.T) {
-	t.Parallel()
-
-	ctx := context.Background()
-
-	app, rest := ablytest.NewREST()
-	defer app.Close()
-
-	fixtures := statsFixtures()
-	postStats(app, fixtures)
-
-	expected := reverseStats(fixtures[1:3])
-
-	pages, err := rest.Stats(
-		ably.StatsWithStart(time.Date(2020, time.January, 28, 14, 1, 0, 0, time.UTC)),
-		ably.StatsWithEnd(time.Date(2020, time.January, 28, 14, 2, 30, 0, time.UTC)),
-	).Pages(ctx)
-	if err != nil {
-		t.Fatal(err)
-	}
-	var got []*ably.Stats
-	for pages.Next(ctx) {
-		got = append(got, pages.Items()...)
-	}
-	if err := pages.Err(); err != nil {
-		t.Fatal(err)
-	}
-
-	if !reflect.DeepEqual(expected, got) {
-		t.Fatalf("expected: %+v; got: %+v", expected, got)
-	}
-}
-
-func TestStats_Direction_RSC6b2(t *testing.T) {
-	t.Parallel()
-
-	for _, c := range []struct {
-		direction ably.Direction
-		expected  []*ably.Stats
-	}{
-		{
-			direction: ably.Backwards,
-			expected:  reverseStats(statsFixtures()),
-		},
-		{
-			direction: ably.Forwards,
-			expected:  statsFixtures(),
-		},
-	} {
-		c := c
-		t.Run(fmt.Sprintf("direction=%v", c.direction), func(t *testing.T) {
-			t.Parallel()
-			ctx := context.Background()
-
-			app, rest := ablytest.NewREST()
-			defer app.Close()
-
-			fixtures := statsFixtures()
-			postStats(app, fixtures)
-
-			expected := c.expected
-
-			pages, err := rest.Stats(
-				ably.StatsWithLimit(len(expected)),
-				ably.StatsWithDirection(c.direction),
-			).Pages(ctx)
-			if err != nil {
-				t.Fatal(err)
+	for _, v := range trackRecord {
+		v.Conn.SetReadDeadline(time.Now())
+		if _, err := v.Conn.Read(make([]byte, 1)); err != nil {
+			if !connIsClosed(err) {
+				t.Errorf("expected conn %s to be closed", v.Conn.LocalAddr())
 			}
-			var got []*ably.Stats
-			for pages.Next(ctx) {
-				got = append(got, pages.Items()...)
-			}
-			if err := pages.Err(); err != nil {
-				t.Fatal(err)
-			}
-
-			if !reflect.DeepEqual(expected, got) {
-				t.Fatalf("expected: %+v; got: %+v", expected, got)
-			}
-		})
-	}
-}
-
-func TestStats_Unit_RSC6b4(t *testing.T) {
-	t.Parallel()
-
-	ctx := context.Background()
-
-	app, rest := ablytest.NewREST()
-	defer app.Close()
-
-	fixtures := statsFixtures()
-	postStats(app, fixtures)
-
-	pages, err := rest.Stats(
-		ably.StatsWithUnit(ably.PeriodMonth),
-	).Pages(ctx)
-	if err != nil {
-		t.Fatal(err)
-	}
-	var got []*ably.Stats
-	for pages.Next(ctx) {
-		got = append(got, pages.Items()...)
-	}
-	if err := pages.Err(); err != nil {
-		t.Fatal(err)
-	}
-
-	if expected, got := 1, len(got); expected != got {
-		t.Fatalf("expected: %v; got: %v", expected, got)
-	}
-
-	stats := got[0]
-	if expected, got := "month", stats.Unit; expected != got {
-		t.Fatalf("expected: %v; got: %v", expected, got)
-	}
-}
-
-func statsFixtures() []*ably.Stats {
-	var fixtures []*ably.Stats
-	baseDate := time.Date(2020, time.January, 28, 14, 0, 0, 0, time.UTC)
-	msgCounts := ably.StatsMessageCount{
-		Count: 50,
-		Data:  5000,
-	}
-	msgTypes := ably.StatsMessageTypes{
-		All:      msgCounts,
-		Messages: msgCounts,
-	}
-	for i := time.Duration(0); i < 10; i++ {
-		fixtures = append(fixtures, &ably.Stats{
-			IntervalID: baseDate.Add(i * time.Minute).Format("2006-01-02:15:04"),
-			Unit:       "minute",
-			All:        msgTypes,
-			Inbound: ably.StatsMessageTraffic{
-				All:      msgTypes,
-				RealTime: msgTypes,
-			},
-		})
-	}
-	return fixtures
-}
-
-func postStats(app *ablytest.Sandbox, stats []*ably.Stats) error {
-	ctx, cancel := context.WithTimeout(context.Background(), ablytest.Timeout)
-	defer cancel()
-
-	statsJSON, err := json.Marshal(stats)
-	if err != nil {
-		return fmt.Errorf("marshaling stats: %w", err)
-	}
-
-	req, err := http.NewRequest("POST", "https://sandbox-rest.ably.io/stats", bytes.NewReader(statsJSON))
-	if err != nil {
-		return fmt.Errorf("creating request: %w", err)
-	}
-	req = req.WithContext(ctx)
-	req.SetBasicAuth(app.KeyParts())
-	resp, err := http.DefaultClient.Do(req)
-	if err != nil {
-		return fmt.Errorf("performing request: %w", err)
+		}
 	}
-	resp.Body.Close()
-	return nil
 }
 
-func reverseStats(stats []*ably.Stats) []*ably.Stats {
-	var reversed []*ably.Stats
-	for i := len(stats) - 1; i >= 0; i-- {
-		reversed = append(reversed, stats[i])
-	}
-	return reversed
+func connIsClosed(err error) bool {
+	return strings.Contains(err.Error(), "use of closed network connection")
 }
diff --git a/ably/rest_presence_spec_test.go b/ably/rest_presence_spec_test.go
deleted file mode 100644
index 0b76166..0000000
--- a/ably/rest_presence_spec_test.go
+++ /dev/null
@@ -1,291 +0,0 @@
-package ably_test
-
-import (
-	"context"
-	"fmt"
-	"io"
-	"reflect"
-	"sort"
-	"testing"
-
-	"github.com/ably/ably-go/ably"
-	"github.com/ably/ably-go/ably/ablytest"
-	"github.com/ably/ably-go/ably/proto"
-)
-
-func TestPresenceHistory_RSP4_RSP4b3(t *testing.T) {
-	t.Parallel()
-
-	for _, limit := range []int{2, 3, 20} {
-		t.Run(fmt.Sprintf("limit=%d", limit), func(t *testing.T) {
-			t.Parallel()
-
-			app, rest := ablytest.NewREST()
-			defer app.Close()
-			channel := rest.Channels.Get("test")
-
-			fixtures := presenceHistoryFixtures()
-			realtime := postPresenceHistoryFixtures(context.Background(), app, "test", fixtures)
-			defer safeclose(t, realtime, app)
-
-			var err error
-			if !ablytest.Soon.IsTrue(func() bool {
-				err = ablytest.TestPagination(
-					reversePresence(fixtures),
-					channel.Presence.History(ably.PresenceHistoryWithLimit(limit)),
-					limit,
-					ablytest.PaginationWithEqual(presenceEqual),
-				)
-				return err == nil
-			}) {
-				t.Fatal(err)
-			}
-		})
-	}
-}
-
-func TestPresenceHistory_Direction_RSP4b2(t *testing.T) {
-	t.Parallel()
-
-	for _, c := range []struct {
-		direction ably.Direction
-		expected  []*ably.PresenceMessage
-	}{
-		{
-			direction: ably.Backwards,
-			expected:  reversePresence(presenceHistoryFixtures()),
-		},
-		{
-			direction: ably.Forwards,
-			expected:  presenceHistoryFixtures(),
-		},
-	} {
-		c := c
-		t.Run(fmt.Sprintf("direction=%v", c.direction), func(t *testing.T) {
-			t.Parallel()
-
-			app, rest := ablytest.NewREST()
-			channel := rest.Channels.Get("test")
-
-			fixtures := presenceHistoryFixtures()
-			realtime := postPresenceHistoryFixtures(context.Background(), app, "test", fixtures)
-			defer safeclose(t, realtime, app)
-
-			expected := c.expected
-
-			var err error
-			if !ablytest.Soon.IsTrue(func() bool {
-				err = ablytest.TestPagination(expected, channel.Presence.History(
-					ably.PresenceHistoryWithLimit(len(expected)),
-					ably.PresenceHistoryWithDirection(c.direction),
-				), len(expected), ablytest.PaginationWithEqual(presenceEqual))
-				return err == nil
-			}) {
-				t.Fatal(err)
-			}
-		})
-	}
-}
-
-func TestPresenceGet_RSP3_RSP3a1(t *testing.T) {
-	t.Parallel()
-
-	for _, limit := range []int{2, 3, 20} {
-		t.Run(fmt.Sprintf("limit=%d", limit), func(t *testing.T) {
-			t.Parallel()
-
-			app, rest := ablytest.NewREST()
-			defer app.Close()
-			channel := rest.Channels.Get("persisted:presence_fixtures")
-
-			expected := persistedPresenceFixtures()
-
-			var err error
-			if !ablytest.Soon.IsTrue(func() bool {
-				err = ablytest.TestPagination(
-					expected,
-					channel.Presence.Get(ably.GetPresenceWithLimit(limit)),
-					limit,
-					ablytest.PaginationWithEqual(presenceEqual),
-					ablytest.PaginationWithSortResult(sortPresenceByClientID),
-				)
-				return err == nil
-			}) {
-				t.Fatal(err)
-			}
-		})
-	}
-}
-
-func TestPresenceGet_ClientID_RSP3a2(t *testing.T) {
-	t.Parallel()
-
-	for _, clientID := range []string{
-		"client_bool",
-		"client_string",
-	} {
-		clientID := clientID
-		t.Run(fmt.Sprintf("clientID=%v", clientID), func(t *testing.T) {
-			t.Parallel()
-
-			app, rest := ablytest.NewREST()
-			defer app.Close()
-			channel := rest.Channels.Get("persisted:presence_fixtures")
-
-			expected := persistedPresenceFixtures(func(p ablytest.Presence) bool {
-				return p.ClientID == clientID
-			})
-
-			var err error
-			if !ablytest.Soon.IsTrue(func() bool {
-				err = ablytest.TestPagination(
-					expected,
-					channel.Presence.Get(ably.GetPresenceWithClientID(clientID)),
-					1,
-					ablytest.PaginationWithEqual(presenceEqual),
-					ablytest.PaginationWithSortResult(sortPresenceByClientID),
-				)
-				return err == nil
-			}) {
-				t.Fatal(err)
-			}
-		})
-	}
-}
-
-func TestPresenceGet_ConnectionID_RSP3a3(t *testing.T) {
-	t.Parallel()
-
-	app, rest := ablytest.NewREST()
-	defer app.Close()
-
-	expectedByConnID := map[string]ably.Message{}
-
-	for i := 0; i < 3; i++ {
-		realtime := app.NewRealtime()
-		defer safeclose(t, ablytest.FullRealtimeCloser(realtime))
-		m := ably.Message{
-			Data:     fmt.Sprintf("msg%d", i),
-			ClientID: fmt.Sprintf("client%d", i),
-		}
-		realtime.Channels.Get("test").Presence.EnterClient(context.Background(), m.ClientID, m.Data)
-		expectedByConnID[realtime.Connection.ID()] = m
-	}
-
-	channel := rest.Channels.Get("test")
-
-	var rg ablytest.ResultGroup
-
-	for connID, expected := range expectedByConnID {
-		connID, expected := connID, expected
-		rg.GoAdd(func(ctx context.Context) error {
-			var err error
-			if !ablytest.Soon.IsTrue(func() bool {
-				err = ablytest.TestPagination(
-					[]*ably.PresenceMessage{{
-						Action:  proto.PresencePresent,
-						Message: expected,
-					}},
-					channel.Presence.Get(ably.GetPresenceWithConnectionID(connID)),
-					1,
-					ablytest.PaginationWithEqual(presenceEqual),
-					ablytest.PaginationWithSortResult(sortPresenceByClientID),
-				)
-				return err == nil
-			}) {
-				return fmt.Errorf("connID %s: %w", connID, err)
-			}
-			return nil
-		})
-	}
-
-	if err := rg.Wait(); err != nil {
-		t.Fatal(err)
-	}
-}
-
-func presenceHistoryFixtures() []*ably.PresenceMessage {
-	actions := []proto.PresenceAction{
-		proto.PresenceEnter,
-		proto.PresenceUpdate,
-		proto.PresenceLeave,
-	}
-	var fixtures []*ably.PresenceMessage
-	for i := 0; i < 10; i++ {
-		for j, action := range actions[:i%3+1] {
-			fixtures = append(fixtures, &ably.PresenceMessage{
-				Action: action,
-				Message: ably.Message{
-					Data:     fmt.Sprintf("msg%d.%d", i, j),
-					ClientID: fmt.Sprintf("client%d", i),
-				},
-			})
-		}
-	}
-	return fixtures
-}
-
-func postPresenceHistoryFixtures(ctx context.Context, app *ablytest.Sandbox, channel string, fixtures []*ably.PresenceMessage) io.Closer {
-	realtime := app.NewRealtime()
-	p := realtime.Channels.Get(channel).Presence
-
-	for _, m := range fixtures {
-		switch m.Action {
-		case proto.PresenceEnter:
-			p.EnterClient(ctx, m.ClientID, m.Data)
-		case proto.PresenceUpdate:
-			p.UpdateClient(ctx, m.ClientID, m.Data)
-		case proto.PresenceLeave:
-			p.LeaveClient(ctx, m.ClientID, m.Data)
-		}
-	}
-
-	return ablytest.FullRealtimeCloser(realtime)
-}
-
-func reversePresence(msgs []*ably.PresenceMessage) []*ably.PresenceMessage {
-	var reversed []*ably.PresenceMessage
-	for i := len(msgs) - 1; i >= 0; i-- {
-		reversed = append(reversed, msgs[i])
-	}
-	return reversed
-}
-
-func presenceEqual(x, y interface{}) bool {
-	mx, my := x.(*ably.PresenceMessage), y.(*ably.PresenceMessage)
-	return mx.Action == my.Action &&
-		mx.ClientID == my.ClientID &&
-		mx.Name == my.Name &&
-		reflect.DeepEqual(mx.Data, my.Data)
-}
-
-func persistedPresenceFixtures(filter ...func(ablytest.Presence) bool) []interface{} {
-	var expected []interface{}
-fixtures:
-	for _, p := range ablytest.PresenceFixtures() {
-		for _, f := range filter {
-			if !f(p) {
-				continue fixtures
-			}
-		}
-		expected = append(expected, &ably.PresenceMessage{
-			Action: proto.PresencePresent,
-			Message: ably.Message{
-				ClientID: p.ClientID,
-				Data:     p.Data,
-			},
-		})
-	}
-
-	// presence.get result order is undefined, so we need to sort both
-	// expected and actual items client-side to get consistent results.
-	sortPresenceByClientID(expected)
-
-	return expected
-}
-
-func sortPresenceByClientID(items []interface{}) {
-	sort.Slice(items, func(i, j int) bool {
-		return items[i].(*ably.PresenceMessage).ClientID < items[j].(*ably.PresenceMessage).ClientID
-	})
-}
diff --git a/ably/rest_presence_test.go b/ably/rest_presence_test.go
new file mode 100644
index 0000000..a368db8
--- /dev/null
+++ b/ably/rest_presence_test.go
@@ -0,0 +1,105 @@
+package ably_test
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/ably/ably-go/ably"
+	"github.com/ably/ably-go/ably/ablytest"
+)
+
+func TestChannel_Presence(t *testing.T) {
+	t.Parallel()
+	app, err := ablytest.NewSandbox(nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer app.Close()
+	client, err := ably.NewREST(app.Options()...)
+	if err != nil {
+		t.Fatal(err)
+	}
+	channel := client.Channels.Get("persisted:presence_fixtures")
+	presence := channel.Presence
+
+	t.Run("Get", func(ts *testing.T) {
+		page, err := presence.Get(context.Background(), nil)
+		if err != nil {
+			ts.Fatal(err)
+		}
+		n := len(page.PresenceMessages())
+		expect := len(app.Config.Channels[0].Presence)
+		if n != expect {
+			ts.Errorf("expected %d got %d", expect, n)
+		}
+
+		ts.Run("With limit option", func(ts *testing.T) {
+			limit := 2
+			page1, err := presence.Get(context.Background(), &ably.PaginateParams{Limit: limit})
+			if err != nil {
+				ts.Fatal(err)
+			}
+			n := len(page1.PresenceMessages())
+			if n != limit {
+				ts.Errorf("expected %d messages got %d", limit, n)
+			}
+			n = len(page1.Items())
+			if n != limit {
+				ts.Errorf("expected %d items got %d", limit, n)
+			}
+
+			page2, err := page1.Next(context.Background())
+			if err != nil {
+				ts.Fatal(err)
+			}
+			n = len(page2.PresenceMessages())
+			if n != limit {
+				ts.Errorf("expected %d messages got %d", limit, n)
+			}
+			n = len(page2.Items())
+			if n != limit {
+				ts.Errorf("expected %d items got %d", limit, n)
+			}
+
+			noPage, err := page2.Next(context.Background())
+			if err != nil {
+				ts.Fatal(err)
+			}
+			if noPage != nil {
+				ts.Fatal("no more pages expected")
+			}
+		})
+	})
+
+	t.Run("History", func(ts *testing.T) {
+		page, err := presence.History(context.Background(), nil)
+		if err != nil {
+			ts.Fatal(err)
+		}
+		n := len(page.PresenceMessages())
+		expect := len(app.Config.Channels[0].Presence)
+		if n != expect {
+			ts.Errorf("expected %d got %d", expect, n)
+		}
+
+		ts.Run("with start and end time", func(ts *testing.T) {
+			params := &ably.PaginateParams{
+				ScopeParams: ably.ScopeParams{
+					Start: time.Now().Add(-24 * time.Hour),
+					End:   time.Now(),
+				},
+			}
+			page, err := presence.History(context.Background(), params)
+			if err != nil {
+				ts.Fatal(err)
+			}
+			n := len(page.PresenceMessages())
+			expect := len(app.Config.Channels[0].Presence)
+			if n != expect {
+				ts.Errorf("expected %d got %d", expect, n)
+			}
+		})
+	})
+
+}