@teever/ez-hook-effect 0.4.4

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/README.md ADDED
@@ -0,0 +1,265 @@
1
+ # @teever/ez-hook-effect
2
+
3
+ A Discord webhook library built with [Effect](https://effect.website/) - type-safe, composable, and resilient.
4
+
5
+ ## Features
6
+
7
+ - **Type-safe** - Runtime validation with Effect schemas
8
+ - **Composable** - Functional programming patterns with Effect
9
+ - **Error Handling** - Comprehensive error tracking with structured validation issues
10
+ - **Retry Logic** - Built-in exponential backoff with jitter
11
+ - **Dependency Injection** - Layer-based architecture
12
+ - **Validated** - All Discord webhook constraints enforced
13
+ - **One Dependency** - Only requires Effect
14
+ - **Environment Support** - Configure via environment variables
15
+ - **CRUD Operations** - Send, get, modify, delete, and validate webhooks
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ bunx jsr add @teever/ez-hook-effect
21
+ # or
22
+ npx jsr add @teever/ez-hook-effect
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```typescript
28
+ import { Effect, Layer, pipe } from 'effect'
29
+ import { sendWebhook, makeConfigLayer, HttpClientLive, WebhookServiceLive, Webhook } from '@teever/ez-hook-effect'
30
+
31
+ // Configure your webhook
32
+ const WEBHOOK_URL = 'https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN'
33
+
34
+ // Create service layers
35
+ const AppLayer = WebhookServiceLive.pipe(
36
+ Layer.provide(makeConfigLayer(WEBHOOK_URL)),
37
+ Layer.provide(HttpClientLive)
38
+ )
39
+
40
+ // Send a simple message
41
+ const program = pipe(
42
+ Webhook.make,
43
+ Webhook.setContent('Hello from Effect!'),
44
+ Webhook.setUsername('My Bot'),
45
+ Webhook.build,
46
+ Effect.flatMap(sendWebhook)
47
+ )
48
+
49
+ // Run the program
50
+ Effect.runPromise(Effect.provide(program, AppLayer))
51
+ ```
52
+
53
+ ### Pipe-First API (Recommended)
54
+
55
+ Prefer composing with `pipe` for ergonomic, Effect-style data assembly. This library uses a single, pipe-first API (no OO builders).
56
+
57
+ ```ts
58
+ import { Effect, pipe } from 'effect'
59
+ import { Webhook, Embed, sendWebhook } from '@teever/ez-hook-effect'
60
+
61
+ const program = pipe(
62
+ Embed.make,
63
+ Embed.setTitle('System Status'),
64
+ Embed.setDescription('All systems operational'),
65
+ Embed.setColor(0x00ff00),
66
+ Embed.addField('CPU', '45%', true),
67
+ Embed.setTimestamp(),
68
+ Embed.build,
69
+ Effect.flatMap((embed) =>
70
+ pipe(
71
+ Webhook.make,
72
+ Webhook.setContent('📊 Status Update'),
73
+ Webhook.setUsername('Ez-Hook Bot'),
74
+ Webhook.addEmbed(embed),
75
+ Webhook.build
76
+ )
77
+ ),
78
+ Effect.flatMap(sendWebhook)
79
+ )
80
+ ```
81
+
82
+ ## Building Rich Embeds
83
+
84
+ ```typescript
85
+ const embedProgram = pipe(
86
+ Embed.make,
87
+ Embed.setTitle('Server Status'),
88
+ Embed.setDescription('All systems operational'),
89
+ Embed.setColor(0x00FF00),
90
+ Embed.addField('CPU', '45%', true),
91
+ Embed.addField('Memory', '2.1GB', true),
92
+ Embed.addField('Uptime', '15 days', true),
93
+ Embed.build,
94
+ Effect.flatMap((embed) =>
95
+ pipe(
96
+ Webhook.make,
97
+ Webhook.setContent('Daily Report'),
98
+ Webhook.addEmbed(embed),
99
+ Webhook.build
100
+ )
101
+ ),
102
+ Effect.flatMap(sendWebhook)
103
+ )
104
+ ```
105
+
106
+ ## Error Handling
107
+
108
+ Retries for rate limits (429) and transient errors (5xx) are handled automatically using configurable exponential backoff. You only need to handle errors that persist after retries are exhausted:
109
+
110
+ ```typescript
111
+ import { Effect, pipe } from 'effect'
112
+ import { Webhook, sendWebhook, ValidationError, HttpError, NetworkError, RateLimitError, WebhookError } from '@teever/ez-hook-effect'
113
+
114
+ const program = pipe(
115
+ Webhook.make,
116
+ Webhook.setContent('Hello!'),
117
+ Webhook.build,
118
+ Effect.flatMap(sendWebhook),
119
+ Effect.catchTag('ValidationError', (error) =>
120
+ Effect.logError(`Validation failed: ${error.message}`)
121
+ ),
122
+ Effect.catchTag('RateLimitError', (error) =>
123
+ Effect.logError(`Rate limited: retry after ${error.retryAfter}ms`)
124
+ ),
125
+ Effect.catchTag('HttpError', (error) =>
126
+ Effect.logError(`Request failed: ${error.message}`)
127
+ ),
128
+ Effect.catchTag('NetworkError', (error) =>
129
+ Effect.logError(`Network error: ${error.message}`)
130
+ ),
131
+ Effect.catchTag('WebhookError', (error) =>
132
+ Effect.logError(`Webhook error: ${error.message}`)
133
+ )
134
+ )
135
+ ```
136
+
137
+ ### Structured Validation Errors
138
+
139
+ Validation errors include detailed issue information:
140
+
141
+ ```typescript
142
+ Effect.catchTag('ValidationError', (error) =>
143
+ Effect.logError(`Validation failed:\n${error.format()}`)
144
+ )
145
+ ```
146
+
147
+ ## Configuration Options
148
+
149
+ ### Programmatic Configuration
150
+
151
+ ```typescript
152
+ const AppLayer = WebhookServiceLive.pipe(
153
+ Layer.provide(makeConfigLayer(WEBHOOK_URL, {
154
+ maxRetries: 5, // Maximum retry attempts
155
+ baseDelayMs: 1000, // Base delay for exponential backoff
156
+ maxDelayMs: 60000, // Maximum delay between retries
157
+ enableJitter: true // Add randomness to retry delays
158
+ })),
159
+ Layer.provide(HttpClientLive)
160
+ )
161
+ ```
162
+
163
+ ### Environment Variables
164
+
165
+ ```typescript
166
+ import { ConfigFromEnv, HttpClientLive, WebhookServiceLive } from '@teever/ez-hook-effect'
167
+
168
+ // Reads from DISCORD_WEBHOOK_URL, WEBHOOK_MAX_RETRIES, etc.
169
+ const AppLayer = WebhookServiceLive.pipe(
170
+ Layer.provide(ConfigFromEnv),
171
+ Layer.provide(HttpClientLive)
172
+ )
173
+
174
+ # Available environment variables:
175
+ # DISCORD_WEBHOOK_URL - Webhook URL (required)
176
+ # WEBHOOK_MAX_RETRIES - Maximum retry attempts (default: 3)
177
+ # WEBHOOK_BASE_DELAY_MS - Base delay in ms (default: 1000)
178
+ # WEBHOOK_MAX_DELAY_MS - Maximum delay in ms (default: 60000)
179
+ # WEBHOOK_ENABLE_JITTER - Enable jitter (default: true)
180
+ ```
181
+
182
+ ### Parse Webhook URL
183
+
184
+ Extract webhook ID and token from a URL:
185
+
186
+ ```typescript
187
+ import { parseWebhookUrl } from '@teever/ez-hook-effect'
188
+
189
+ const result = await Effect.runPromise(
190
+ parseWebhookUrl('https://discord.com/api/webhooks/123456789/token')
191
+ )
192
+ // result: { id: '123456789', token: 'token' }
193
+ ```
194
+
195
+ ## Webhook Operations
196
+
197
+ Beyond sending messages, the library supports full webhook CRUD:
198
+
199
+ ```typescript
200
+ import { Effect, Layer, pipe } from 'effect'
201
+ import { getWebhook, modifyWebhook, deleteWebhook, validateWebhook, WebhookServiceLive, makeConfigLayer, HttpClientLive } from '@teever/ez-hook-effect'
202
+
203
+ const AppLayer = WebhookServiceLive.pipe(
204
+ Layer.provide(makeConfigLayer(WEBHOOK_URL)),
205
+ Layer.provide(HttpClientLive)
206
+ )
207
+
208
+ const program = Effect.gen(function* () {
209
+ const service = yield* WebhookService
210
+
211
+ // Check if webhook is valid and accessible
212
+ const isValid = yield* service.validateWebhook()
213
+
214
+ // Get webhook information
215
+ const info = yield* service.getWebhook()
216
+
217
+ // Modify webhook settings
218
+ const modified = yield* service.modifyWebhook({
219
+ name: 'New Name',
220
+ })
221
+
222
+ // Delete the webhook
223
+ const deleted = yield* service.deleteWebhook()
224
+ })
225
+
226
+ Effect.runPromise(Effect.provide(program, AppLayer))
227
+ ```
228
+
229
+ ## Development
230
+
231
+ ```bash
232
+ bun run lint # Biome lint + format
233
+ bun run typecheck # TypeScript type checking
234
+ ```
235
+
236
+ ## Testing
237
+
238
+ The library includes comprehensive tests for all features:
239
+
240
+ ```bash
241
+ bun test # Run all tests
242
+ bun test:watch # Watch mode
243
+ bun test:coverage # Coverage report
244
+ ```
245
+
246
+ ## Building
247
+
248
+ ```bash
249
+ bun run build # Build the library
250
+ bun run build:standalone # Create standalone executable
251
+ ```
252
+
253
+ ## Examples
254
+
255
+ Check out the `examples/` directory:
256
+
257
+ - `01-basic-webhook.ts` - Send a simple message
258
+ - `02-rich-embeds.ts` - Build embeds with fields, colors, metadata
259
+ - `03-error-handling.ts` - Validation errors and recovery patterns
260
+ - `04-multiple-embeds.ts` - Add several embeds to one webhook
261
+ - `05-service-layer.ts` - Dependency injection with layers and CRUD operations
262
+
263
+ ## License
264
+
265
+ MIT
@@ -0,0 +1,156 @@
1
+ import { type ParseResult } from "effect";
2
+ declare const WebhookError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
3
+ readonly _tag: "WebhookError";
4
+ } & Readonly<A>;
5
+ /**
6
+ * Base error class for all webhook-related errors
7
+ */
8
+ export declare class WebhookError extends WebhookError_base<{
9
+ message: string;
10
+ cause?: unknown;
11
+ }> {
12
+ }
13
+ /**
14
+ * Structured validation issue with path and constraint details
15
+ */
16
+ export interface ValidationIssue {
17
+ /** JSONPath-style path to the invalid field, e.g. "$.embeds[0].fields[2].name" */
18
+ readonly path: string;
19
+ /** Human-readable error message */
20
+ readonly message: string;
21
+ /** Constraint name, e.g. "MaxLength", "MinItems", "Required" */
22
+ readonly constraint?: string;
23
+ /** Expected value or constraint description */
24
+ readonly expected?: string;
25
+ /** Actual value (truncated for large values) */
26
+ readonly actual?: unknown;
27
+ }
28
+ /**
29
+ * Convert Effect Schema ParseError to ValidationIssue array
30
+ */
31
+ export declare const parseErrorToIssues: (error: ParseResult.ParseError) => ValidationIssue[];
32
+ /**
33
+ * Create a single ValidationIssue from simple field validation
34
+ */
35
+ export declare const makeIssue: (field: string, message: string, options?: {
36
+ constraint?: string;
37
+ expected?: string;
38
+ actual?: unknown;
39
+ }) => ValidationIssue;
40
+ declare const ValidationError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
41
+ readonly _tag: "ValidationError";
42
+ } & Readonly<A>;
43
+ /**
44
+ * Validation errors for webhook data with structured issues
45
+ */
46
+ export declare class ValidationError extends ValidationError_base<{
47
+ /** Human-readable summary message */
48
+ message: string;
49
+ /** Structured validation issues */
50
+ issues: ReadonlyArray<ValidationIssue>;
51
+ /** Legacy: primary field that failed (for backward compatibility) */
52
+ field?: string;
53
+ /** Legacy: value that failed validation (for backward compatibility) */
54
+ value?: unknown;
55
+ }> {
56
+ /**
57
+ * Format issues as a human-readable string
58
+ */
59
+ format(options?: {
60
+ maxIssues?: number;
61
+ }): string;
62
+ /**
63
+ * Create ValidationError from Effect Schema ParseError
64
+ */
65
+ static fromParseError(parseError: ParseResult.ParseError, context?: {
66
+ field?: string;
67
+ value?: unknown;
68
+ }): ValidationError;
69
+ /**
70
+ * Create ValidationError from a single issue
71
+ */
72
+ static fromIssue(field: string, message: string, options?: {
73
+ constraint?: string;
74
+ expected?: string;
75
+ actual?: unknown;
76
+ }): ValidationError;
77
+ }
78
+ declare const NetworkError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
79
+ readonly _tag: "NetworkError";
80
+ } & Readonly<A>;
81
+ /**
82
+ * Network-related errors
83
+ */
84
+ export declare class NetworkError extends NetworkError_base<{
85
+ message: string;
86
+ statusCode?: number;
87
+ response?: unknown;
88
+ }> {
89
+ }
90
+ declare const RateLimitError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
91
+ readonly _tag: "RateLimitError";
92
+ } & Readonly<A>;
93
+ /**
94
+ * Rate limit errors
95
+ */
96
+ export declare class RateLimitError extends RateLimitError_base<{
97
+ message: string;
98
+ retryAfter: number;
99
+ limit?: number;
100
+ remaining?: number;
101
+ reset?: Date;
102
+ }> {
103
+ }
104
+ declare const ConfigError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
105
+ readonly _tag: "ConfigError";
106
+ } & Readonly<A>;
107
+ /**
108
+ * Configuration errors
109
+ */
110
+ export declare class ConfigError extends ConfigError_base<{
111
+ message: string;
112
+ parameter: string;
113
+ }> {
114
+ }
115
+ declare const FileError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
116
+ readonly _tag: "FileError";
117
+ } & Readonly<A>;
118
+ /**
119
+ * File-related errors
120
+ */
121
+ export declare class FileError extends FileError_base<{
122
+ message: string;
123
+ filename?: string;
124
+ size?: number;
125
+ maxSize?: number;
126
+ }> {
127
+ }
128
+ /**
129
+ * HTTP response error details
130
+ */
131
+ export interface HttpErrorResponse {
132
+ status: number;
133
+ statusText: string;
134
+ headers: Record<string, string>;
135
+ body?: unknown;
136
+ }
137
+ declare const HttpError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
138
+ readonly _tag: "HttpError";
139
+ } & Readonly<A>;
140
+ /**
141
+ * HTTP errors with detailed response information
142
+ */
143
+ export declare class HttpError extends HttpError_base<{
144
+ message: string;
145
+ request: {
146
+ method: string;
147
+ url: string;
148
+ };
149
+ response?: HttpErrorResponse;
150
+ }> {
151
+ }
152
+ /**
153
+ * All possible webhook errors
154
+ */
155
+ export type WebhookErrors = WebhookError | ValidationError | NetworkError | RateLimitError | ConfigError | FileError | HttpError;
156
+ export {};
@@ -0,0 +1 @@
1
+ export * from "./WebhookError";
@@ -0,0 +1,85 @@
1
+ /**
2
+ * ez-hook-effect - Discord webhook library built with Effect
3
+ *
4
+ * Type-safe, composable, and resilient Discord webhook client.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { Webhook, Embed, sendWebhook, WebhookServiceLive, makeConfigLayer, HttpClientLive } from "ez-hook-effect";
9
+ * import { Effect, Layer, pipe } from "effect";
10
+ *
11
+ * const webhook = pipe(
12
+ * Webhook.make,
13
+ * Webhook.setContent("Hello from Effect!"),
14
+ * Webhook.build
15
+ * );
16
+ *
17
+ * const layer = WebhookServiceLive.pipe(
18
+ * Layer.provide(makeConfigLayer("https://discord.com/api/webhooks/...")),
19
+ * Layer.provide(HttpClientLive)
20
+ * );
21
+ *
22
+ * Effect.gen(function* () {
23
+ * const msg = yield* webhook;
24
+ * yield* sendWebhook(msg);
25
+ * }).pipe(Effect.provide(layer), Effect.runPromise);
26
+ * ```
27
+ *
28
+ * @packageDocumentation
29
+ */
30
+ /** Error types for handling webhook failures */
31
+ export { ConfigError, FileError, HttpError, type HttpErrorResponse, makeIssue, NetworkError, parseErrorToIssues, RateLimitError, ValidationError, type ValidationIssue, WebhookError, type WebhookErrors, } from "./errors";
32
+ /**
33
+ * Configuration service and layers.
34
+ * Use makeConfigLayer for programmatic config or ConfigFromEnv for environment variables.
35
+ */
36
+ export { Config, ConfigFromEnv, makeConfig, makeConfigLayer, parseWebhookUrl, type WebhookConfig, } from "./layers/Config";
37
+ /**
38
+ * HTTP client service and layers.
39
+ * Provides fetch-based HTTP client with retry and rate limit support.
40
+ */
41
+ export { createRetrySchedule, defaultRetryConfig, FetchHttpClient, HttpClient, HttpClientLive, type HttpRequest, type HttpResponse, makeHttpClientLayer, parseRateLimitHeaders, type RateLimitInfo, type RetryConfig, } from "./layers/HttpClient";
42
+ /**
43
+ * Embed builder namespace.
44
+ * Pipe-first API for building Discord embed objects.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { Embed } from "ez-hook-effect";
49
+ * import { pipe } from "effect";
50
+ *
51
+ * const embed = pipe(
52
+ * Embed.make,
53
+ * Embed.setTitle("Hello"),
54
+ * Embed.setDescription("World"),
55
+ * Embed.setColor("#5865F2"),
56
+ * Embed.build
57
+ * );
58
+ * ```
59
+ */
60
+ export * as Embed from "./pipes/Embed";
61
+ /**
62
+ * Webhook builder namespace.
63
+ * Pipe-first API for building Discord webhook payloads.
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * import { Webhook } from "ez-hook-effect";
68
+ * import { pipe } from "effect";
69
+ *
70
+ * const webhook = pipe(
71
+ * Webhook.make,
72
+ * Webhook.setUsername("Bot"),
73
+ * Webhook.setContent("Hello!"),
74
+ * Webhook.build
75
+ * );
76
+ * ```
77
+ */
78
+ export * as Webhook from "./pipes/Webhook";
79
+ /** Schema types for Discord webhook payloads */
80
+ export * from "./schemas";
81
+ /**
82
+ * Webhook service and module-level accessors.
83
+ * Use these with Effect.flatMap or Effect.gen for sending webhooks.
84
+ */
85
+ export { deleteWebhook, getWebhook, makeWebhookService, modifyWebhook, sendWebhook, validateWebhook, WebhookService, WebhookServiceLive, } from "./services/WebhookService";