effect-slack 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/dist/Errors.d.ts +103 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Errors.js +84 -0
- package/dist/Errors.js.map +1 -0
- package/dist/Retry.d.ts +63 -0
- package/dist/Retry.d.ts.map +1 -0
- package/dist/Retry.js +77 -0
- package/dist/Retry.js.map +1 -0
- package/dist/SlackClient.d.ts +19 -0
- package/dist/SlackClient.d.ts.map +1 -0
- package/dist/SlackClient.js +21 -0
- package/dist/SlackClient.js.map +1 -0
- package/dist/SlackConfig.d.ts +26 -0
- package/dist/SlackConfig.d.ts.map +1 -0
- package/dist/SlackConfig.js +19 -0
- package/dist/SlackConfig.js.map +1 -0
- package/dist/SlackService.d.ts +32 -0
- package/dist/SlackService.d.ts.map +1 -0
- package/dist/SlackService.js +135 -0
- package/dist/SlackService.js.map +1 -0
- package/dist/generated/AdminService.d.ts +174 -0
- package/dist/generated/AdminService.d.ts.map +1 -0
- package/dist/generated/AdminService.js +1029 -0
- package/dist/generated/AdminService.js.map +1 -0
- package/dist/generated/ApiService.d.ts +18 -0
- package/dist/generated/ApiService.d.ts.map +1 -0
- package/dist/generated/ApiService.js +27 -0
- package/dist/generated/ApiService.js.map +1 -0
- package/dist/generated/AppsService.d.ts +33 -0
- package/dist/generated/AppsService.d.ts.map +1 -0
- package/dist/generated/AppsService.js +105 -0
- package/dist/generated/AppsService.js.map +1 -0
- package/dist/generated/AssistantService.d.ts +22 -0
- package/dist/generated/AssistantService.d.ts.map +1 -0
- package/dist/generated/AssistantService.js +49 -0
- package/dist/generated/AssistantService.js.map +1 -0
- package/dist/generated/AuthService.d.ts +22 -0
- package/dist/generated/AuthService.d.ts.map +1 -0
- package/dist/generated/AuthService.js +46 -0
- package/dist/generated/AuthService.js.map +1 -0
- package/dist/generated/BookmarksService.d.ts +21 -0
- package/dist/generated/BookmarksService.d.ts.map +1 -0
- package/dist/generated/BookmarksService.js +57 -0
- package/dist/generated/BookmarksService.js.map +1 -0
- package/dist/generated/BotsService.d.ts +18 -0
- package/dist/generated/BotsService.d.ts.map +1 -0
- package/dist/generated/BotsService.js +27 -0
- package/dist/generated/BotsService.js.map +1 -0
- package/dist/generated/CallsService.d.ts +25 -0
- package/dist/generated/CallsService.d.ts.map +1 -0
- package/dist/generated/CallsService.js +76 -0
- package/dist/generated/CallsService.js.map +1 -0
- package/dist/generated/CanvasesService.d.ts +27 -0
- package/dist/generated/CanvasesService.d.ts.map +1 -0
- package/dist/generated/CanvasesService.js +81 -0
- package/dist/generated/CanvasesService.js.map +1 -0
- package/dist/generated/ChatService.d.ts +32 -0
- package/dist/generated/ChatService.d.ts.map +1 -0
- package/dist/generated/ChatService.js +149 -0
- package/dist/generated/ChatService.js.map +1 -0
- package/dist/generated/ConversationsService.d.ts +51 -0
- package/dist/generated/ConversationsService.d.ts.map +1 -0
- package/dist/generated/ConversationsService.js +303 -0
- package/dist/generated/ConversationsService.js.map +1 -0
- package/dist/generated/DialogService.d.ts +18 -0
- package/dist/generated/DialogService.d.ts.map +1 -0
- package/dist/generated/DialogService.js +27 -0
- package/dist/generated/DialogService.js.map +1 -0
- package/dist/generated/DndService.d.ts +22 -0
- package/dist/generated/DndService.d.ts.map +1 -0
- package/dist/generated/DndService.js +67 -0
- package/dist/generated/DndService.js.map +1 -0
- package/dist/generated/EmojiService.d.ts +18 -0
- package/dist/generated/EmojiService.d.ts.map +1 -0
- package/dist/generated/EmojiService.js +27 -0
- package/dist/generated/EmojiService.js.map +1 -0
- package/dist/generated/EntityService.d.ts +18 -0
- package/dist/generated/EntityService.d.ts.map +1 -0
- package/dist/generated/EntityService.js +27 -0
- package/dist/generated/EntityService.js.map +1 -0
- package/dist/generated/FilesService.d.ts +37 -0
- package/dist/generated/FilesService.d.ts.map +1 -0
- package/dist/generated/FilesService.js +182 -0
- package/dist/generated/FilesService.js.map +1 -0
- package/dist/generated/FunctionsService.d.ts +19 -0
- package/dist/generated/FunctionsService.d.ts.map +1 -0
- package/dist/generated/FunctionsService.js +37 -0
- package/dist/generated/FunctionsService.js.map +1 -0
- package/dist/generated/MigrationService.d.ts +18 -0
- package/dist/generated/MigrationService.d.ts.map +1 -0
- package/dist/generated/MigrationService.js +27 -0
- package/dist/generated/MigrationService.js.map +1 -0
- package/dist/generated/OauthService.d.ts +22 -0
- package/dist/generated/OauthService.d.ts.map +1 -0
- package/dist/generated/OauthService.js +50 -0
- package/dist/generated/OauthService.js.map +1 -0
- package/dist/generated/OpenidService.d.ts +21 -0
- package/dist/generated/OpenidService.d.ts.map +1 -0
- package/dist/generated/OpenidService.js +39 -0
- package/dist/generated/OpenidService.js.map +1 -0
- package/dist/generated/PinsService.d.ts +20 -0
- package/dist/generated/PinsService.d.ts.map +1 -0
- package/dist/generated/PinsService.js +47 -0
- package/dist/generated/PinsService.js.map +1 -0
- package/dist/generated/ReactionsService.d.ts +21 -0
- package/dist/generated/ReactionsService.d.ts.map +1 -0
- package/dist/generated/ReactionsService.js +57 -0
- package/dist/generated/ReactionsService.js.map +1 -0
- package/dist/generated/RemindersService.d.ts +22 -0
- package/dist/generated/RemindersService.d.ts.map +1 -0
- package/dist/generated/RemindersService.js +67 -0
- package/dist/generated/RemindersService.js.map +1 -0
- package/dist/generated/RtmService.d.ts +19 -0
- package/dist/generated/RtmService.d.ts.map +1 -0
- package/dist/generated/RtmService.js +38 -0
- package/dist/generated/RtmService.js.map +1 -0
- package/dist/generated/SearchService.d.ts +20 -0
- package/dist/generated/SearchService.d.ts.map +1 -0
- package/dist/generated/SearchService.js +47 -0
- package/dist/generated/SearchService.js.map +1 -0
- package/dist/generated/SlackListsService.d.ts +35 -0
- package/dist/generated/SlackListsService.d.ts.map +1 -0
- package/dist/generated/SlackListsService.js +143 -0
- package/dist/generated/SlackListsService.js.map +1 -0
- package/dist/generated/StarsService.d.ts +20 -0
- package/dist/generated/StarsService.d.ts.map +1 -0
- package/dist/generated/StarsService.js +50 -0
- package/dist/generated/StarsService.js.map +1 -0
- package/dist/generated/TeamService.d.ts +34 -0
- package/dist/generated/TeamService.d.ts.map +1 -0
- package/dist/generated/TeamService.js +115 -0
- package/dist/generated/TeamService.js.map +1 -0
- package/dist/generated/ToolingService.d.ts +20 -0
- package/dist/generated/ToolingService.d.ts.map +1 -0
- package/dist/generated/ToolingService.js +29 -0
- package/dist/generated/ToolingService.js.map +1 -0
- package/dist/generated/UsergroupsService.d.ts +26 -0
- package/dist/generated/UsergroupsService.d.ts.map +1 -0
- package/dist/generated/UsergroupsService.js +89 -0
- package/dist/generated/UsergroupsService.js.map +1 -0
- package/dist/generated/UsersService.d.ts +33 -0
- package/dist/generated/UsersService.d.ts.map +1 -0
- package/dist/generated/UsersService.js +141 -0
- package/dist/generated/UsersService.js.map +1 -0
- package/dist/generated/ViewsService.d.ts +21 -0
- package/dist/generated/ViewsService.d.ts.map +1 -0
- package/dist/generated/ViewsService.js +57 -0
- package/dist/generated/ViewsService.js.map +1 -0
- package/dist/generated/WorkflowsService.d.ts +26 -0
- package/dist/generated/WorkflowsService.d.ts.map +1 -0
- package/dist/generated/WorkflowsService.js +92 -0
- package/dist/generated/WorkflowsService.js.map +1 -0
- package/dist/generated/index.d.ts +38 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/dist/generated/index.js +38 -0
- package/dist/generated/index.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/errors.d.ts +13 -0
- package/dist/internal/errors.d.ts.map +1 -0
- package/dist/internal/errors.js +68 -0
- package/dist/internal/errors.js.map +1 -0
- package/package.json +80 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mateo Kruk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# effect-slack
|
|
2
|
+
|
|
3
|
+
An Effect-native Slack SDK ✨
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **100% Type-safe** — Full TypeScript types for all 272 methods, arguments, and responses
|
|
8
|
+
- **Typed errors** — Discriminated unions with `catchTag`/`catchTags` for precise error handling
|
|
9
|
+
- **Observability built-in** — OpenTelemetry spans with rich attributes (method, channel, user, timestamps)
|
|
10
|
+
- **Smart retries** — Rate limit aware with exponential backoff and jitter
|
|
11
|
+
- **Testable by design** — Dependency injection via Effect layers, easily mockable
|
|
12
|
+
- **Always up-to-date** — Auto-generated from official `@slack/web-api` types
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bun add effect-slack effect
|
|
18
|
+
# or
|
|
19
|
+
npm install effect-slack effect
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { Effect } from "effect"
|
|
26
|
+
import { SlackService } from "effect-slack"
|
|
27
|
+
|
|
28
|
+
// Using environment variables (SLACK_TOKEN)
|
|
29
|
+
const program = Effect.gen(function* () {
|
|
30
|
+
const slack = yield* SlackService
|
|
31
|
+
|
|
32
|
+
const result = yield* slack.postMessage({
|
|
33
|
+
channel: "C1234567890",
|
|
34
|
+
text: "Hello from Effect!"
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
console.log("Message sent:", result.ts)
|
|
38
|
+
}).pipe(Effect.provide(SlackService.Live))
|
|
39
|
+
|
|
40
|
+
Effect.runPromise(program)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Custom Configuration
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { Effect, Layer, Redacted } from "effect"
|
|
47
|
+
import { SlackService, SlackConfig, SlackClient } from "effect-slack"
|
|
48
|
+
|
|
49
|
+
const customConfig = SlackConfig.make({
|
|
50
|
+
token: Redacted.make(process.env.MY_SLACK_TOKEN!),
|
|
51
|
+
options: {
|
|
52
|
+
retryConfig: { retries: 5 }
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const CustomSlackLayer = SlackService.Default.pipe(
|
|
57
|
+
Layer.provide(SlackClient.Default),
|
|
58
|
+
Layer.provide(customConfig)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const program = Effect.gen(function* () {
|
|
62
|
+
const slack = yield* SlackService
|
|
63
|
+
// ... use slack methods
|
|
64
|
+
}).pipe(Effect.provide(CustomSlackLayer))
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Error Handling
|
|
68
|
+
|
|
69
|
+
All Slack API errors are mapped to typed Effect errors that can be handled with `catchTag` or `catchTags`:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { Effect } from "effect"
|
|
73
|
+
import { SlackService } from "effect-slack"
|
|
74
|
+
|
|
75
|
+
const program = Effect.gen(function* () {
|
|
76
|
+
const slack = yield* SlackService
|
|
77
|
+
return yield* slack.postMessage({ channel: "C123", text: "Hi" })
|
|
78
|
+
}).pipe(
|
|
79
|
+
Effect.catchTags({
|
|
80
|
+
SlackRateLimitedError: (e) =>
|
|
81
|
+
Effect.log(`Rate limited, retry in ${e.retryAfter}s`),
|
|
82
|
+
SlackPlatformError: (e) =>
|
|
83
|
+
e.isAuthError
|
|
84
|
+
? Effect.logError(`Auth failed: ${e.error}`)
|
|
85
|
+
: Effect.logError(`API error: ${e.error}`),
|
|
86
|
+
SlackHttpError: (e) =>
|
|
87
|
+
Effect.logError(`HTTP ${e.statusCode}: ${e.statusMessage}`)
|
|
88
|
+
}),
|
|
89
|
+
Effect.provide(SlackService.Live)
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Error Types
|
|
94
|
+
|
|
95
|
+
| Error Type | Description |
|
|
96
|
+
| -------------------------------------- | -------------------------------------------------------------- |
|
|
97
|
+
| `SlackRequestError` | Network failures, DNS errors |
|
|
98
|
+
| `SlackHttpError` | Non-200 HTTP responses with `statusCode` and `body` |
|
|
99
|
+
| `SlackPlatformError` | Slack API errors with `error` code (e.g., `channel_not_found`) |
|
|
100
|
+
| `SlackRateLimitedError` | Rate limit exceeded, includes `retryAfter` (seconds) |
|
|
101
|
+
| `SlackFileUploadInvalidArgumentsError` | Invalid file upload arguments |
|
|
102
|
+
| `SlackFileUploadReadError` | Failed to read file data |
|
|
103
|
+
| `SlackUnknownError` | Unexpected errors |
|
|
104
|
+
|
|
105
|
+
`SlackPlatformError` includes an `isAuthError` getter that returns `true` for auth-related errors (`invalid_auth`, `not_authed`, `token_revoked`, `token_expired`, `account_inactive`).
|
|
106
|
+
|
|
107
|
+
## Retry Support
|
|
108
|
+
|
|
109
|
+
The library provides two approaches to retry handling:
|
|
110
|
+
|
|
111
|
+
### SDK-Level Retries (Default)
|
|
112
|
+
|
|
113
|
+
The underlying `@slack/web-api` SDK handles retries automatically. You can configure it via `SlackConfig`:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { Redacted } from "effect"
|
|
117
|
+
import { SlackConfig } from "effect-slack"
|
|
118
|
+
|
|
119
|
+
const config = SlackConfig.make({
|
|
120
|
+
token: Redacted.make("xoxb-..."),
|
|
121
|
+
options: {
|
|
122
|
+
retryConfig: { retries: 5 }
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Disabling SDK Retries
|
|
128
|
+
|
|
129
|
+
To use Effect-level retries exclusively, disable SDK retries:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { Redacted } from "effect"
|
|
133
|
+
import { SlackConfig } from "effect-slack"
|
|
134
|
+
|
|
135
|
+
const config = SlackConfig.make({
|
|
136
|
+
token: Redacted.make("xoxb-..."),
|
|
137
|
+
options: {
|
|
138
|
+
retryConfig: { retries: 0 }, // Disable SDK retries
|
|
139
|
+
rejectRateLimitedCalls: true // Don't auto-handle rate limits
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Effect-Level Retries
|
|
145
|
+
|
|
146
|
+
For more control, use the Effect-native retry utilities:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { Effect, pipe } from "effect"
|
|
150
|
+
import {
|
|
151
|
+
SlackService,
|
|
152
|
+
withDefaultRetry,
|
|
153
|
+
withRateLimitRetry,
|
|
154
|
+
rapidRetryPolicy,
|
|
155
|
+
isRetryableError
|
|
156
|
+
} from "effect-slack"
|
|
157
|
+
|
|
158
|
+
// Apply default retry policy (10 retries with exponential backoff)
|
|
159
|
+
const program = pipe(
|
|
160
|
+
Effect.flatMap(SlackService, (slack) =>
|
|
161
|
+
slack.postMessage({ channel: "#general", text: "Hello!" })
|
|
162
|
+
),
|
|
163
|
+
withDefaultRetry
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
// Or use a custom schedule
|
|
167
|
+
const programWithCustomRetry = pipe(
|
|
168
|
+
Effect.flatMap(SlackService, (slack) =>
|
|
169
|
+
slack.postMessage({ channel: "#general", text: "Hello!" })
|
|
170
|
+
),
|
|
171
|
+
Effect.retry(rapidRetryPolicy)
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Pre-built Schedules
|
|
176
|
+
|
|
177
|
+
| Schedule | Description |
|
|
178
|
+
| -------------------------------- | --------------------------------------------- |
|
|
179
|
+
| `tenRetriesInAboutThirtyMinutes` | 10 retries with exponential backoff + jitter |
|
|
180
|
+
| `fiveRetriesInFiveMinutes` | 5 retries with exponential backoff + jitter |
|
|
181
|
+
| `rapidRetryPolicy` | 3 retries with 100ms delay (for testing) |
|
|
182
|
+
| `rateLimitAwareSchedule(opts)` | Configurable retries with exponential backoff |
|
|
183
|
+
|
|
184
|
+
### Retryable Errors
|
|
185
|
+
|
|
186
|
+
The `isRetryableError` function determines which errors are safe to retry:
|
|
187
|
+
|
|
188
|
+
| Error Type | Retryable | Reason |
|
|
189
|
+
| ----------------------- | --------- | --------------------------- |
|
|
190
|
+
| `SlackRateLimitedError` | Yes | Transient, has `retryAfter` |
|
|
191
|
+
| `SlackRequestError` | Yes | Network failures |
|
|
192
|
+
| `SlackHttpError` (5xx) | Yes | Server errors |
|
|
193
|
+
| `SlackHttpError` (4xx) | No | Client errors |
|
|
194
|
+
| `SlackPlatformError` | Partial | Only `service_unavailable` |
|
|
195
|
+
| Auth errors | No | Won't resolve with retry |
|
|
196
|
+
|
|
197
|
+
## Observability
|
|
198
|
+
|
|
199
|
+
All SlackService methods are instrumented with OpenTelemetry-compatible spans.
|
|
200
|
+
|
|
201
|
+
### Span Attributes
|
|
202
|
+
|
|
203
|
+
| Attribute | Description |
|
|
204
|
+
| ------------------- | -------------------------------------------------- |
|
|
205
|
+
| `slack.method` | Slack API method (e.g., `chat.postMessage`) |
|
|
206
|
+
| `slack.channel` | Channel ID (where applicable) |
|
|
207
|
+
| `slack.user` | User ID (for user operations) |
|
|
208
|
+
| `slack.ts` | Message timestamp (for updates/deletes) |
|
|
209
|
+
| `slack.reaction` | Reaction name (for reaction operations) |
|
|
210
|
+
| `error.type` | Error tag on failures (e.g., `SlackPlatformError`) |
|
|
211
|
+
| `slack.error` | Platform error code (e.g., `channel_not_found`) |
|
|
212
|
+
| `http.status_code` | HTTP status code on HTTP errors |
|
|
213
|
+
| `slack.retry_after` | Retry-After seconds on rate limit errors |
|
|
214
|
+
|
|
215
|
+
### Exporting Traces
|
|
216
|
+
|
|
217
|
+
Use `@effect/opentelemetry` to export traces to your observability backend:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { Effect } from "effect"
|
|
221
|
+
import { NodeSdk } from "@effect/opentelemetry"
|
|
222
|
+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"
|
|
223
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
|
|
224
|
+
import { SlackService } from "effect-slack"
|
|
225
|
+
|
|
226
|
+
const TracingLive = NodeSdk.layer(() => ({
|
|
227
|
+
resource: { serviceName: "my-slack-app" },
|
|
228
|
+
spanProcessor: new BatchSpanProcessor(new OTLPTraceExporter())
|
|
229
|
+
}))
|
|
230
|
+
|
|
231
|
+
const program = Effect.gen(function* () {
|
|
232
|
+
const slack = yield* SlackService
|
|
233
|
+
yield* slack.postMessage({ channel: "#general", text: "Hello!" })
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
Effect.runPromise(
|
|
237
|
+
program.pipe(
|
|
238
|
+
Effect.provide(SlackService.Live),
|
|
239
|
+
Effect.provide(TracingLive)
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Available Methods
|
|
245
|
+
|
|
246
|
+
The library provides **272 methods** across **33 services**, auto-generated from the official `@slack/web-api` types. Services include:
|
|
247
|
+
|
|
248
|
+
- **ChatService** - Messages, threads, scheduled messages
|
|
249
|
+
- **ConversationsService** - Channels, DMs, group conversations
|
|
250
|
+
- **UsersService** - User profiles, presence, identity
|
|
251
|
+
- **ReactionsService** - Emoji reactions
|
|
252
|
+
- **FilesService** - File uploads and management
|
|
253
|
+
- **AdminService** - Workspace administration (100+ methods)
|
|
254
|
+
- **AppsService**, **AuthService**, **BookmarksService**, **CallsService**, **ViewsService**, and more...
|
|
255
|
+
|
|
256
|
+
Each service is available as an Effect service with full TypeScript types. See [`src/generated/`](./src/generated/) for the complete API.
|
|
257
|
+
|
|
258
|
+
## Testing
|
|
259
|
+
|
|
260
|
+
The library is designed to be easily testable by providing mock implementations:
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
import { Effect, Layer } from "effect"
|
|
264
|
+
import { SlackService, SlackClient } from "effect-slack"
|
|
265
|
+
import type { WebClient } from "@slack/web-api"
|
|
266
|
+
|
|
267
|
+
// Create a mock WebClient
|
|
268
|
+
const mockClient = {
|
|
269
|
+
chat: {
|
|
270
|
+
postMessage: async () => ({ ok: true, ts: "1234.5678" })
|
|
271
|
+
}
|
|
272
|
+
} as unknown as WebClient
|
|
273
|
+
|
|
274
|
+
// Create test layer
|
|
275
|
+
const TestLayer = SlackService.Default.pipe(
|
|
276
|
+
Layer.provide(SlackClient.make(mockClient))
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
// Use in tests
|
|
280
|
+
const testProgram = Effect.gen(function* () {
|
|
281
|
+
const slack = yield* SlackService
|
|
282
|
+
const result = yield* slack.postMessage({
|
|
283
|
+
channel: "C123",
|
|
284
|
+
text: "Test message"
|
|
285
|
+
})
|
|
286
|
+
return result
|
|
287
|
+
}).pipe(Effect.provide(TestLayer))
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Architecture
|
|
291
|
+
|
|
292
|
+
Services are auto-generated from the `@slack/web-api` TypeScript definitions:
|
|
293
|
+
|
|
294
|
+
1. **Parse** — Extract method signatures from `@slack/web-api/dist/methods.d.ts`
|
|
295
|
+
2. **Generate** — Create Effect-wrapped services with typed arguments and responses
|
|
296
|
+
3. **Instrument** — Add OpenTelemetry spans and error mapping to each method
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
@slack/web-api types → Parser → Code Generator → Effect Services
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Generated services follow a consistent pattern:
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
// Each method is wrapped with Effect.tryPromise, error mapping, and tracing
|
|
306
|
+
const postMessage = (args: ChatPostMessageArguments): Effect.Effect<ChatPostMessageResponse, SlackError> =>
|
|
307
|
+
Effect.tryPromise({
|
|
308
|
+
try: () => client.chat.postMessage(args),
|
|
309
|
+
catch: mapSlackError
|
|
310
|
+
}).pipe(
|
|
311
|
+
Effect.tapError(annotateSpanWithError),
|
|
312
|
+
Effect.withSpan("ChatService.postMessage", {
|
|
313
|
+
attributes: { "slack.method": "chat.postMessage" }
|
|
314
|
+
})
|
|
315
|
+
)
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Run `bun run generate` to regenerate services when updating `@slack/web-api`.
|
|
319
|
+
|
|
320
|
+
## License
|
|
321
|
+
|
|
322
|
+
MIT
|
package/dist/Errors.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { ErrorCode } from "@slack/web-api";
|
|
3
|
+
/**
|
|
4
|
+
* Re-export ErrorCode for consumers to use in type guards
|
|
5
|
+
*/
|
|
6
|
+
export { ErrorCode };
|
|
7
|
+
declare const SlackRequestError_base: Schema.TaggedErrorClass<SlackRequestError, "SlackRequestError", {
|
|
8
|
+
readonly _tag: Schema.tag<"SlackRequestError">;
|
|
9
|
+
} & {
|
|
10
|
+
message: typeof Schema.String;
|
|
11
|
+
cause: Schema.optional<typeof Schema.Defect>;
|
|
12
|
+
}>;
|
|
13
|
+
/**
|
|
14
|
+
* Request error - network failures, DNS errors, etc.
|
|
15
|
+
* Maps to: slack_webapi_request_error
|
|
16
|
+
*/
|
|
17
|
+
export declare class SlackRequestError extends SlackRequestError_base {
|
|
18
|
+
}
|
|
19
|
+
declare const SlackHttpError_base: Schema.TaggedErrorClass<SlackHttpError, "SlackHttpError", {
|
|
20
|
+
readonly _tag: Schema.tag<"SlackHttpError">;
|
|
21
|
+
} & {
|
|
22
|
+
statusCode: typeof Schema.Number;
|
|
23
|
+
statusMessage: typeof Schema.String;
|
|
24
|
+
message: typeof Schema.String;
|
|
25
|
+
body: Schema.optional<typeof Schema.Unknown>;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* HTTP error - non-200 responses from Slack API
|
|
29
|
+
* Maps to: slack_webapi_http_error
|
|
30
|
+
*/
|
|
31
|
+
export declare class SlackHttpError extends SlackHttpError_base {
|
|
32
|
+
}
|
|
33
|
+
declare const SlackPlatformError_base: Schema.TaggedErrorClass<SlackPlatformError, "SlackPlatformError", {
|
|
34
|
+
readonly _tag: Schema.tag<"SlackPlatformError">;
|
|
35
|
+
} & {
|
|
36
|
+
error: typeof Schema.String;
|
|
37
|
+
message: typeof Schema.String;
|
|
38
|
+
data: Schema.optional<typeof Schema.Unknown>;
|
|
39
|
+
}>;
|
|
40
|
+
/**
|
|
41
|
+
* Platform error - Slack API returned an error response
|
|
42
|
+
* Maps to: slack_webapi_platform_error
|
|
43
|
+
*
|
|
44
|
+
* The `error` field contains the Slack error code (e.g., "channel_not_found", "invalid_auth")
|
|
45
|
+
*/
|
|
46
|
+
export declare class SlackPlatformError extends SlackPlatformError_base {
|
|
47
|
+
/**
|
|
48
|
+
* Check if this is an authentication error
|
|
49
|
+
*/
|
|
50
|
+
get isAuthError(): boolean;
|
|
51
|
+
}
|
|
52
|
+
declare const SlackRateLimitedError_base: Schema.TaggedErrorClass<SlackRateLimitedError, "SlackRateLimitedError", {
|
|
53
|
+
readonly _tag: Schema.tag<"SlackRateLimitedError">;
|
|
54
|
+
} & {
|
|
55
|
+
retryAfter: typeof Schema.Number;
|
|
56
|
+
message: typeof Schema.String;
|
|
57
|
+
}>;
|
|
58
|
+
/**
|
|
59
|
+
* Rate limited error - too many requests
|
|
60
|
+
* Maps to: slack_webapi_rate_limited_error
|
|
61
|
+
*/
|
|
62
|
+
export declare class SlackRateLimitedError extends SlackRateLimitedError_base {
|
|
63
|
+
}
|
|
64
|
+
declare const SlackFileUploadInvalidArgumentsError_base: Schema.TaggedErrorClass<SlackFileUploadInvalidArgumentsError, "SlackFileUploadInvalidArgumentsError", {
|
|
65
|
+
readonly _tag: Schema.tag<"SlackFileUploadInvalidArgumentsError">;
|
|
66
|
+
} & {
|
|
67
|
+
message: typeof Schema.String;
|
|
68
|
+
data: Schema.optional<typeof Schema.Unknown>;
|
|
69
|
+
}>;
|
|
70
|
+
/**
|
|
71
|
+
* File upload error - invalid arguments for file upload
|
|
72
|
+
* Maps to: slack_webapi_file_upload_invalid_args_error
|
|
73
|
+
*/
|
|
74
|
+
export declare class SlackFileUploadInvalidArgumentsError extends SlackFileUploadInvalidArgumentsError_base {
|
|
75
|
+
}
|
|
76
|
+
declare const SlackFileUploadReadError_base: Schema.TaggedErrorClass<SlackFileUploadReadError, "SlackFileUploadReadError", {
|
|
77
|
+
readonly _tag: Schema.tag<"SlackFileUploadReadError">;
|
|
78
|
+
} & {
|
|
79
|
+
message: typeof Schema.String;
|
|
80
|
+
cause: Schema.optional<typeof Schema.Defect>;
|
|
81
|
+
}>;
|
|
82
|
+
/**
|
|
83
|
+
* File upload read error - failed to read file data
|
|
84
|
+
* Maps to: slack_webapi_file_upload_read_file_data_error
|
|
85
|
+
*/
|
|
86
|
+
export declare class SlackFileUploadReadError extends SlackFileUploadReadError_base {
|
|
87
|
+
}
|
|
88
|
+
declare const SlackUnknownError_base: Schema.TaggedErrorClass<SlackUnknownError, "SlackUnknownError", {
|
|
89
|
+
readonly _tag: Schema.tag<"SlackUnknownError">;
|
|
90
|
+
} & {
|
|
91
|
+
message: typeof Schema.String;
|
|
92
|
+
cause: typeof Schema.Defect;
|
|
93
|
+
}>;
|
|
94
|
+
/**
|
|
95
|
+
* Unknown error - catch-all for unexpected errors
|
|
96
|
+
*/
|
|
97
|
+
export declare class SlackUnknownError extends SlackUnknownError_base {
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Union type of all Slack errors for exhaustive handling
|
|
101
|
+
*/
|
|
102
|
+
export type SlackError = SlackRequestError | SlackHttpError | SlackPlatformError | SlackRateLimitedError | SlackFileUploadInvalidArgumentsError | SlackFileUploadReadError | SlackUnknownError;
|
|
103
|
+
//# sourceMappingURL=Errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Errors.d.ts","sourceRoot":"","sources":["../src/Errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE1C;;GAEG;AACH,OAAO,EAAE,SAAS,EAAE,CAAA;;;;;;;AAEpB;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,sBAMtC;CAAG;;;;;;;;;AAEJ;;;GAGG;AACH,qBAAa,cAAe,SAAQ,mBAKlC;CAAG;;;;;;;;AAEL;;;;;GAKG;AACH,qBAAa,kBAAmB,SAAQ,uBAOvC;IACC;;OAEG;IACH,IAAI,WAAW,IAAI,OAAO,CAQzB;CACF;;;;;;;AAED;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,0BAM1C;CAAG;;;;;;;AAEJ;;;GAGG;AACH,qBAAa,oCAAqC,SAAQ,yCAMzD;CAAG;;;;;;;AAEJ;;;GAGG;AACH,qBAAa,wBAAyB,SAAQ,6BAM7C;CAAG;;;;;;;AAEJ;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,sBAMtC;CAAG;AAEJ;;GAEG;AACH,MAAM,MAAM,UAAU,GAClB,iBAAiB,GACjB,cAAc,GACd,kBAAkB,GAClB,qBAAqB,GACrB,oCAAoC,GACpC,wBAAwB,GACxB,iBAAiB,CAAA"}
|
package/dist/Errors.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { ErrorCode } from "@slack/web-api";
|
|
3
|
+
/**
|
|
4
|
+
* Re-export ErrorCode for consumers to use in type guards
|
|
5
|
+
*/
|
|
6
|
+
export { ErrorCode };
|
|
7
|
+
/**
|
|
8
|
+
* Request error - network failures, DNS errors, etc.
|
|
9
|
+
* Maps to: slack_webapi_request_error
|
|
10
|
+
*/
|
|
11
|
+
export class SlackRequestError extends Schema.TaggedError()("SlackRequestError", {
|
|
12
|
+
message: Schema.String,
|
|
13
|
+
cause: Schema.optional(Schema.Defect)
|
|
14
|
+
}) {
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* HTTP error - non-200 responses from Slack API
|
|
18
|
+
* Maps to: slack_webapi_http_error
|
|
19
|
+
*/
|
|
20
|
+
export class SlackHttpError extends Schema.TaggedError()("SlackHttpError", {
|
|
21
|
+
statusCode: Schema.Number,
|
|
22
|
+
statusMessage: Schema.String,
|
|
23
|
+
message: Schema.String,
|
|
24
|
+
body: Schema.optional(Schema.Unknown)
|
|
25
|
+
}) {
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Platform error - Slack API returned an error response
|
|
29
|
+
* Maps to: slack_webapi_platform_error
|
|
30
|
+
*
|
|
31
|
+
* The `error` field contains the Slack error code (e.g., "channel_not_found", "invalid_auth")
|
|
32
|
+
*/
|
|
33
|
+
export class SlackPlatformError extends Schema.TaggedError()("SlackPlatformError", {
|
|
34
|
+
error: Schema.String,
|
|
35
|
+
message: Schema.String,
|
|
36
|
+
data: Schema.optional(Schema.Unknown)
|
|
37
|
+
}) {
|
|
38
|
+
/**
|
|
39
|
+
* Check if this is an authentication error
|
|
40
|
+
*/
|
|
41
|
+
get isAuthError() {
|
|
42
|
+
return (this.error === "invalid_auth" ||
|
|
43
|
+
this.error === "not_authed" ||
|
|
44
|
+
this.error === "token_revoked" ||
|
|
45
|
+
this.error === "token_expired" ||
|
|
46
|
+
this.error === "account_inactive");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Rate limited error - too many requests
|
|
51
|
+
* Maps to: slack_webapi_rate_limited_error
|
|
52
|
+
*/
|
|
53
|
+
export class SlackRateLimitedError extends Schema.TaggedError()("SlackRateLimitedError", {
|
|
54
|
+
retryAfter: Schema.Number,
|
|
55
|
+
message: Schema.String
|
|
56
|
+
}) {
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* File upload error - invalid arguments for file upload
|
|
60
|
+
* Maps to: slack_webapi_file_upload_invalid_args_error
|
|
61
|
+
*/
|
|
62
|
+
export class SlackFileUploadInvalidArgumentsError extends Schema.TaggedError()("SlackFileUploadInvalidArgumentsError", {
|
|
63
|
+
message: Schema.String,
|
|
64
|
+
data: Schema.optional(Schema.Unknown)
|
|
65
|
+
}) {
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* File upload read error - failed to read file data
|
|
69
|
+
* Maps to: slack_webapi_file_upload_read_file_data_error
|
|
70
|
+
*/
|
|
71
|
+
export class SlackFileUploadReadError extends Schema.TaggedError()("SlackFileUploadReadError", {
|
|
72
|
+
message: Schema.String,
|
|
73
|
+
cause: Schema.optional(Schema.Defect)
|
|
74
|
+
}) {
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Unknown error - catch-all for unexpected errors
|
|
78
|
+
*/
|
|
79
|
+
export class SlackUnknownError extends Schema.TaggedError()("SlackUnknownError", {
|
|
80
|
+
message: Schema.String,
|
|
81
|
+
cause: Schema.Defect
|
|
82
|
+
}) {
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=Errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Errors.js","sourceRoot":"","sources":["../src/Errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE1C;;GAEG;AACH,OAAO,EAAE,SAAS,EAAE,CAAA;AAEpB;;;GAGG;AACH,MAAM,OAAO,iBAAkB,SAAQ,MAAM,CAAC,WAAW,EAAqB,CAC5E,mBAAmB,EACnB;IACE,OAAO,EAAE,MAAM,CAAC,MAAM;IACtB,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;CACtC,CACF;CAAG;AAEJ;;;GAGG;AACH,MAAM,OAAO,cAAe,SAAQ,MAAM,CAAC,WAAW,EAAkB,CAAC,gBAAgB,EAAE;IACzF,UAAU,EAAE,MAAM,CAAC,MAAM;IACzB,aAAa,EAAE,MAAM,CAAC,MAAM;IAC5B,OAAO,EAAE,MAAM,CAAC,MAAM;IACtB,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC;CACtC,CAAC;CAAG;AAEL;;;;;GAKG;AACH,MAAM,OAAO,kBAAmB,SAAQ,MAAM,CAAC,WAAW,EAAsB,CAC9E,oBAAoB,EACpB;IACE,KAAK,EAAE,MAAM,CAAC,MAAM;IACpB,OAAO,EAAE,MAAM,CAAC,MAAM;IACtB,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC;CACtC,CACF;IACC;;OAEG;IACH,IAAI,WAAW;QACb,OAAO,CACL,IAAI,CAAC,KAAK,KAAK,cAAc;YAC7B,IAAI,CAAC,KAAK,KAAK,YAAY;YAC3B,IAAI,CAAC,KAAK,KAAK,eAAe;YAC9B,IAAI,CAAC,KAAK,KAAK,eAAe;YAC9B,IAAI,CAAC,KAAK,KAAK,kBAAkB,CAClC,CAAA;IACH,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,qBAAsB,SAAQ,MAAM,CAAC,WAAW,EAAyB,CACpF,uBAAuB,EACvB;IACE,UAAU,EAAE,MAAM,CAAC,MAAM;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM;CACvB,CACF;CAAG;AAEJ;;;GAGG;AACH,MAAM,OAAO,oCAAqC,SAAQ,MAAM,CAAC,WAAW,EAAwC,CAClH,sCAAsC,EACtC;IACE,OAAO,EAAE,MAAM,CAAC,MAAM;IACtB,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC;CACtC,CACF;CAAG;AAEJ;;;GAGG;AACH,MAAM,OAAO,wBAAyB,SAAQ,MAAM,CAAC,WAAW,EAA4B,CAC1F,0BAA0B,EAC1B;IACE,OAAO,EAAE,MAAM,CAAC,MAAM;IACtB,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;CACtC,CACF;CAAG;AAEJ;;GAEG;AACH,MAAM,OAAO,iBAAkB,SAAQ,MAAM,CAAC,WAAW,EAAqB,CAC5E,mBAAmB,EACnB;IACE,OAAO,EAAE,MAAM,CAAC,MAAM;IACtB,KAAK,EAAE,MAAM,CAAC,MAAM;CACrB,CACF;CAAG"}
|
package/dist/Retry.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Duration, Effect, Schedule } from "effect";
|
|
2
|
+
import type { SlackError } from "./Errors.js";
|
|
3
|
+
/**
|
|
4
|
+
* Options for creating retry schedules
|
|
5
|
+
*/
|
|
6
|
+
export interface RetryOptions {
|
|
7
|
+
readonly maxRetries?: number;
|
|
8
|
+
readonly baseDelay?: Duration.DurationInput;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Check if an error is transient and safe to retry
|
|
12
|
+
*/
|
|
13
|
+
export declare const isRetryableError: (error: SlackError) => boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Effect equivalent of SDK's tenRetriesInAboutThirtyMinutes
|
|
16
|
+
* 10 retries with exponential backoff + jitter
|
|
17
|
+
*/
|
|
18
|
+
export declare const tenRetriesInAboutThirtyMinutes: Schedule.Schedule<number, SlackError>;
|
|
19
|
+
/**
|
|
20
|
+
* Effect equivalent of SDK's fiveRetriesInFiveMinutes
|
|
21
|
+
* 5 retries with exponential backoff + jitter
|
|
22
|
+
*/
|
|
23
|
+
export declare const fiveRetriesInFiveMinutes: Schedule.Schedule<number, SlackError>;
|
|
24
|
+
/**
|
|
25
|
+
* Rapid retry policy for testing
|
|
26
|
+
* 3 retries with 100ms fixed delay
|
|
27
|
+
*/
|
|
28
|
+
export declare const rapidRetryPolicy: Schedule.Schedule<number, SlackError>;
|
|
29
|
+
/**
|
|
30
|
+
* Schedule that respects Slack's Retry-After header for rate limits
|
|
31
|
+
* Uses exponential backoff for non-rate-limit errors
|
|
32
|
+
*/
|
|
33
|
+
export declare const rateLimitAwareSchedule: (options?: RetryOptions) => Schedule.Schedule<number, SlackError>;
|
|
34
|
+
/**
|
|
35
|
+
* Retry an effect with the default policy (tenRetriesInAboutThirtyMinutes)
|
|
36
|
+
*/
|
|
37
|
+
export declare const withDefaultRetry: <A, E extends SlackError, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
38
|
+
/**
|
|
39
|
+
* Retry an effect with rate-limit awareness
|
|
40
|
+
* Automatically uses retryAfter from SlackRateLimitedError
|
|
41
|
+
*/
|
|
42
|
+
export declare const withRateLimitRetry: <A, E extends SlackError, R>(effect: Effect.Effect<A, E, R>, options?: RetryOptions) => Effect.Effect<A, E, R>;
|
|
43
|
+
/**
|
|
44
|
+
* Retry only rate limit errors with exact retryAfter timing
|
|
45
|
+
* Does NOT retry other error types
|
|
46
|
+
*/
|
|
47
|
+
export declare const withRateLimitOnlyRetry: <A, E extends SlackError, R>(effect: Effect.Effect<A, E, R>, options?: {
|
|
48
|
+
maxRetries?: number;
|
|
49
|
+
}) => Effect.Effect<A, E, R>;
|
|
50
|
+
/**
|
|
51
|
+
* Retry with fallback - returns a fallback value if all retries fail
|
|
52
|
+
*/
|
|
53
|
+
export declare const withRetryOrElse: <A, B, E extends SlackError, R, R2>(effect: Effect.Effect<A, E, R>, fallback: (error: E, fiberId: unknown) => Effect.Effect<B, E, R2>, schedule?: Schedule.Schedule<unknown, E>) => Effect.Effect<A | B, E, R | R2>;
|
|
54
|
+
/**
|
|
55
|
+
* All pre-built schedules as a namespace
|
|
56
|
+
*/
|
|
57
|
+
export declare const Schedules: {
|
|
58
|
+
readonly tenRetriesInAboutThirtyMinutes: Schedule.Schedule<number, SlackError, never>;
|
|
59
|
+
readonly fiveRetriesInFiveMinutes: Schedule.Schedule<number, SlackError, never>;
|
|
60
|
+
readonly rapidRetryPolicy: Schedule.Schedule<number, SlackError, never>;
|
|
61
|
+
readonly rateLimitAwareSchedule: (options?: RetryOptions) => Schedule.Schedule<number, SlackError>;
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=Retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Retry.d.ts","sourceRoot":"","sources":["../src/Retry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAQ,QAAQ,EAAE,MAAM,QAAQ,CAAA;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,aAAa,CAAA;CAC5C;AAED;;GAEG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,UAAU,KAAG,OAgBpD,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,8BAA8B,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAMhF,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,wBAAwB,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAM1E,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAKlE,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GACjC,UAAU,YAAY,KACrB,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAWtC,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,EAAE,CAAC,SAAS,UAAU,EAAE,CAAC,EACzD,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAC7B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAC8D,CAAA;AAEtF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EAAE,CAAC,SAAS,UAAU,EAAE,CAAC,EAC3D,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC9B,UAAU,YAAY,KACrB,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAC+D,CAAA;AAEvF;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GAAI,CAAC,EAAE,CAAC,SAAS,UAAU,EAAE,CAAC,EAC/D,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC9B,UAAU;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,KAChC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAIpB,CAAA;AAEJ;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,UAAU,EAAE,CAAC,EAAE,EAAE,EAC/D,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC9B,UAAU,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EACjE,WAAW,QAAQ,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,KACvC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,CAK9B,CAAA;AAEH;;GAEG;AACH,eAAO,MAAM,SAAS;;;;gDA9DV,YAAY,KACrB,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;CAkE9B,CAAA"}
|
package/dist/Retry.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Duration, Effect, pipe, Schedule } from "effect";
|
|
2
|
+
/**
|
|
3
|
+
* Check if an error is transient and safe to retry
|
|
4
|
+
*/
|
|
5
|
+
export const isRetryableError = (error) => {
|
|
6
|
+
switch (error._tag) {
|
|
7
|
+
case "SlackRateLimitedError":
|
|
8
|
+
return true;
|
|
9
|
+
case "SlackRequestError":
|
|
10
|
+
return true;
|
|
11
|
+
case "SlackHttpError":
|
|
12
|
+
return error.statusCode >= 500;
|
|
13
|
+
case "SlackPlatformError":
|
|
14
|
+
if (error.isAuthError)
|
|
15
|
+
return false;
|
|
16
|
+
return error.error === "service_unavailable" || error.error === "internal_error";
|
|
17
|
+
case "SlackFileUploadInvalidArgumentsError":
|
|
18
|
+
case "SlackFileUploadReadError":
|
|
19
|
+
case "SlackUnknownError":
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Effect equivalent of SDK's tenRetriesInAboutThirtyMinutes
|
|
25
|
+
* 10 retries with exponential backoff + jitter
|
|
26
|
+
*/
|
|
27
|
+
export const tenRetriesInAboutThirtyMinutes = pipe(Schedule.exponential("1 second", 2), Schedule.jittered, Schedule.intersect(Schedule.recurs(10)), Schedule.whileInput(isRetryableError), Schedule.map(([, count]) => count));
|
|
28
|
+
/**
|
|
29
|
+
* Effect equivalent of SDK's fiveRetriesInFiveMinutes
|
|
30
|
+
* 5 retries with exponential backoff + jitter
|
|
31
|
+
*/
|
|
32
|
+
export const fiveRetriesInFiveMinutes = pipe(Schedule.exponential("1 second", 2), Schedule.jittered, Schedule.intersect(Schedule.recurs(5)), Schedule.whileInput(isRetryableError), Schedule.map(([, count]) => count));
|
|
33
|
+
/**
|
|
34
|
+
* Rapid retry policy for testing
|
|
35
|
+
* 3 retries with 100ms fixed delay
|
|
36
|
+
*/
|
|
37
|
+
export const rapidRetryPolicy = pipe(Schedule.fixed("100 millis"), Schedule.intersect(Schedule.recurs(3)), Schedule.whileInput(isRetryableError), Schedule.map(([, count]) => count));
|
|
38
|
+
/**
|
|
39
|
+
* Schedule that respects Slack's Retry-After header for rate limits
|
|
40
|
+
* Uses exponential backoff for non-rate-limit errors
|
|
41
|
+
*/
|
|
42
|
+
export const rateLimitAwareSchedule = (options) => {
|
|
43
|
+
const maxRetries = options?.maxRetries ?? 10;
|
|
44
|
+
const baseDelay = options?.baseDelay ?? "1 second";
|
|
45
|
+
return pipe(Schedule.exponential(baseDelay, 2), Schedule.jittered, Schedule.intersect(Schedule.recurs(maxRetries)), Schedule.whileInput(isRetryableError), Schedule.map(([, count]) => count));
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Retry an effect with the default policy (tenRetriesInAboutThirtyMinutes)
|
|
49
|
+
*/
|
|
50
|
+
export const withDefaultRetry = (effect) => Effect.retry(effect, tenRetriesInAboutThirtyMinutes);
|
|
51
|
+
/**
|
|
52
|
+
* Retry an effect with rate-limit awareness
|
|
53
|
+
* Automatically uses retryAfter from SlackRateLimitedError
|
|
54
|
+
*/
|
|
55
|
+
export const withRateLimitRetry = (effect, options) => Effect.retry(effect, rateLimitAwareSchedule(options));
|
|
56
|
+
/**
|
|
57
|
+
* Retry only rate limit errors with exact retryAfter timing
|
|
58
|
+
* Does NOT retry other error types
|
|
59
|
+
*/
|
|
60
|
+
export const withRateLimitOnlyRetry = (effect, options) => Effect.retry(effect, {
|
|
61
|
+
while: (error) => error._tag === "SlackRateLimitedError",
|
|
62
|
+
times: options?.maxRetries ?? 5
|
|
63
|
+
});
|
|
64
|
+
/**
|
|
65
|
+
* Retry with fallback - returns a fallback value if all retries fail
|
|
66
|
+
*/
|
|
67
|
+
export const withRetryOrElse = (effect, fallback, schedule) => Effect.retryOrElse(effect, schedule ?? tenRetriesInAboutThirtyMinutes, fallback);
|
|
68
|
+
/**
|
|
69
|
+
* All pre-built schedules as a namespace
|
|
70
|
+
*/
|
|
71
|
+
export const Schedules = {
|
|
72
|
+
tenRetriesInAboutThirtyMinutes,
|
|
73
|
+
fiveRetriesInFiveMinutes,
|
|
74
|
+
rapidRetryPolicy,
|
|
75
|
+
rateLimitAwareSchedule
|
|
76
|
+
};
|
|
77
|
+
//# sourceMappingURL=Retry.js.map
|