Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto pagination #596

Merged
merged 15 commits into from Aug 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
64 changes: 64 additions & 0 deletions docs/_pages/web_client.md
Expand Up @@ -13,6 +13,7 @@ headings:
- title: Changing the retry configuration
- title: Changing the request concurrency
- title: Rate limit handling
- title: Pagination
- title: Customizing the logger
- title: Custom agent for proxy support
- title: OAuth token exchange
Expand Down Expand Up @@ -244,6 +245,69 @@ web.on('rate_limited', retryAfter => console.log(`Delay future requests by at le

---

### Pagination

Some methods are meant to return large lists of things; whether it be users, channels, messages, or something else. In
order to efficiently get you the data you need, Slack will return parts of that entire list, or **pages**. Cursor-based
pagination describes using a couple options: `cursor` and `limit` to get exactly the page of data you desire. For
example, this is how your app would get the last 500 messages in a conversation.

```javascript
const { WebClient } = require('@slack/client');
const token = process.env.SLACK_TOKEN;
const web = new WebClient(token);
const conversationId = 'C123456'; // some conversation ID

web.conversations.history({ channel: conversationId, limit: 500 })
.then((res) => {
console.log(`Requested 500 messages, recieved ${res.messages.length} in the response`);
})
.catch(console.error);
```

In the code above, the `res.messages` array will contain, at maximum, 500 messages. But what about all the previous
messages? That's what the `cursor` argument is used for 😎.

Inside `res` is a property called `response_metadata`, which might (or might not) have a `next_cursor` property. When
that `next_cursor` property exists, and is not an empty string, you know there's still more data in the list. If you
want to read more messages in that channel's history, you would call the method again, but use that value as the
`cursor` argument. **NOTE**: It should be rare that your app needs to read the entire history of a channel, avoid that!
With other methods, such as `users.list`, it would be more common to request the entire list, so that's what we're
illustrating below.

```javascript
// A function that recursively iterates through each page while a next_cursor exists
function getAllUsers() {
let users = [];
function pageLoaded(res) {
users = users.concat(res.users);
if (res.response_metadata && res.response_metadata.next_cursor && res.response_metadata.cursor !== '') {
return web.users.list({ limit: 100, cursor: res.response_metadata.next_cursor }).then(pageLoaded);
}
return users;
}
return web.users.list({ limit: 100 }).then(pageLoaded);
}

getAllUsers()
.then(console.log) // prints out the list of users
.catch(console.error);
```

Cursor-based pagination, if available for a method, is always preferred. In fact, when you call a cursor-paginated
method without a `cursor` or `limit`, the `WebClient` will **automatically paginate** the requests for you until the
end of the list. Then, each page of results are concatenated, and that list takes the place of the last page in the last
response. In other words, if you don't specify any pagination options then you get the whole list in the result as well
as the non-list properties of the last API call. It's always preferred to perform your own pagination by specifying the
`limit` and/or `cursor` since you can optimize to your own application's needs.

A few methods that returns lists do not support cursor-based pagination, but do support
[other pagination types](https://api.slack.com/docs/pagination#classic_pagination). These methods will not be
automatically paginated for you, so you should give extra care and use appropriate options to only request a page at a
time. If you don't, you risk failing with `Error`s which have a `code` property set to `errorCode.HTTPError`.

---

### Customizing the logger

The `WebClient` also logs interesting events as it operates. By default, the log level is set to
Expand Down
8 changes: 3 additions & 5 deletions package.json
Expand Up @@ -43,7 +43,6 @@
"docs:jsdoc": "ts2jsdoc"
},
"dependencies": {
"@types/delay": "^2.0.1",
"@types/form-data": "^2.2.1",
"@types/got": "^7.1.7",
"@types/is-stream": "^1.1.0",
Expand All @@ -55,7 +54,6 @@
"@types/retry": "^0.10.2",
"@types/url-join": "^0.8.2",
"@types/ws": "^5.1.1",
"delay": "^2.0.0",
"eventemitter3": "^3.0.0",
"finity": "^0.5.4",
"form-data": "^2.3.1",
Expand All @@ -67,16 +65,16 @@
"object.values": "^1.0.4",
"p-cancelable": "^0.3.0",
"p-queue": "^2.3.0",
"p-retry": "^1.0.0",
"retry": "^0.10.1",
"p-retry": "^2.0.0",
"retry": "^0.12.0",
"url-join": "^4.0.0",
"ws": "^5.2.0"
},
"devDependencies": {
"@aoberoi/capture-console": "^1.0.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😎

"@types/chai": "^4.1.2",
"@types/mocha": "^2.2.48",
"busboy": "^0.2.14",
"capture-stdout": "^1.0.0",
"chai": "^4.1.2",
"codecov": "^3.0.0",
"husky": "^0.14.3",
Expand Down
4 changes: 0 additions & 4 deletions src/IncomingWebhook.ts
Expand Up @@ -29,7 +29,6 @@ export class IncomingWebhook {
/**
* Send a notification to a conversation
* @param message the message (a simple string, or an object describing the message)
* @param callback
*/
public send(message: string | IncomingWebhookSendArguments): Promise<IncomingWebhookResult>;
public send(message: string | IncomingWebhookSendArguments, callback: IncomingWebhookResultCallback): void;
Expand Down Expand Up @@ -75,7 +74,6 @@ export class IncomingWebhook {

/**
* Processes an HTTP response into an IncomingWebhookResult.
* @param response
*/
private buildResult(response: got.Response<string>): IncomingWebhookResult {
return {
Expand Down Expand Up @@ -147,7 +145,6 @@ function requestErrorWithOriginal(original: Error): IncomingWebhookRequestError
return (error as IncomingWebhookRequestError);
}


/**
* A factory to create IncomingWebhookReadError objects
* @param original The original error
Expand All @@ -161,7 +158,6 @@ function readErrorWithOriginal(original: Error): IncomingWebhookReadError {
return (error as IncomingWebhookReadError);
}


/**
* A factory to create IncomingWebhookHTTPError objects
* @param original The original error
Expand Down
1 change: 0 additions & 1 deletion src/KeepAlive.ts
Expand Up @@ -104,7 +104,6 @@ export class KeepAlive extends EventEmitter {

/**
* Start monitoring the RTMClient. This method should only be called after the client's websocket is already open.
* @param client an RTMClient to monitor
*/
public start(client: RTMClient): void {
this.logger.debug('start monitoring');
Expand Down
4 changes: 0 additions & 4 deletions src/RTMClient.ts
Expand Up @@ -354,7 +354,6 @@ export class RTMClient extends EventEmitter {
/**
* Begin an RTM session using the provided options. This method must be called before any messages can
* be sent or received.
* @param options arguments for the start method
*/
public start(options?: methods.RTMStartArguments | methods.RTMConnectArguments): void {
// TODO: should this return a Promise<WebAPICallResult>?
Expand Down Expand Up @@ -541,7 +540,6 @@ export class RTMClient extends EventEmitter {

/**
* Set up method for the client's websocket instance. This method will attach event listeners.
* @param url the websocket url
*/
private setupWebsocket(url: string): void {
// initialize the websocket
Expand Down Expand Up @@ -579,7 +577,6 @@ export class RTMClient extends EventEmitter {
/**
* `onmessage` handler for the client's websocket. This will parse the payload and dispatch the relevant events for
* each incoming message.
* @param websocketMessage an incoming message
*/
private onWebsocketMessage({ data }: { data: string }): void {
// v3 legacy
Expand Down Expand Up @@ -677,7 +674,6 @@ export interface RTMWebsocketError extends CodedError {

/**
* A factory to create RTMWebsocketError objects.
* @param original the original error
*/
function websocketErrorWithOriginal(original: Error): RTMWebsocketError {
const error = errorWithCode(
Expand Down