@fluojs/discord 1.0.5 → 2.0.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/README.ko.md CHANGED
@@ -18,6 +18,7 @@ fluo를 위한 webhook-first, transport-agnostic Discord 전달 코어 패키지
18
18
  - [`@fluojs/notifications`와의 통합](#fluojs-notifications와의-통합)
19
19
  - [payload override를 사용하는 template rendering](#payload-override를-사용하는-template-rendering)
20
20
  - [명시적 fetch 주입을 사용하는 webhook-first 전달](#명시적-fetch-주입을-사용하는-webhook-first-전달)
21
+ - [구성 가능한 webhook 재시도 정책](#구성-가능한-webhook-재시도-정책)
21
22
  - [의도적인 제한 사항](#의도적인-제한-사항)
22
23
  - [공개 API 개요](#공개-api-개요)
23
24
  - [관련 패키지](#관련-패키지)
@@ -29,7 +30,7 @@ fluo를 위한 webhook-first, transport-agnostic Discord 전달 코어 패키지
29
30
  npm install @fluojs/discord @fluojs/notifications
30
31
  ```
31
32
 
32
- 이 패키지는 published package metadata에 반영된 저장소 전반의 Node.js 20+ 설치 baseline을 따르지만, 런타임 전달 계약 자체는 명시적인 fetch-compatible 경계를 통해 계속 transport-agnostic하게 유지됩니다.
33
+ 이 패키지는 패키지 자체의 지원 계약에 따라 Node.js `>=24.0.0 <27`을 요구합니다. Node 20 Node 22 host를 Node.js `>=24.0.0 <27`로 올리세요. Node 24 미만과 Node 27 이상은 지원하지 않으므로 process를 지원되는 line으로 옮겨야 합니다. 런타임 전달 계약 자체는 명시적인 fetch-compatible 경계를 통해 계속 transport-agnostic하게 유지됩니다.
33
34
 
34
35
  ## 사용 시점
35
36
 
@@ -110,8 +111,9 @@ Behavioral contract 메모:
110
111
  - `DiscordModule.forRoot(...)`와 `DiscordModule.forRootAsync(...)`는 `DiscordService`, `DiscordChannel`, `DISCORD`, `DISCORD_CHANNEL`을 기본 global로 export합니다. fluo 옵션인 `global?: boolean`을 사용하고, migrated code가 Discord provider를 importing module 안에만 유지해야 할 때만 `global: false`를 설정하세요. NestJS `isGlobal`은 지원하지 않습니다.
111
112
  - `DiscordService.send(...)`는 전달 전에 `defaultThreadId`를 해석합니다.
112
113
  - `DiscordService.sendMany(...)`는 `DiscordMessage[]`를 직접 순차 전송하는 batch API이며 `continueOnError`를 지원합니다. 이는 multi-recipient `@fluojs/notifications` dispatch shortcut이 아닙니다.
113
- - 서비스는 모듈 bootstrap 시 transport를 초기화하고, bootstrap verification 실패와 애플리케이션 shutdown 전반에서 factory-owned 리소스를 정확히 한 번 닫습니다. shutdown 전에 시작된 factory 생성 transport가 아직 완료되지 않았더라도 이를 기다리며, reject된 factory creation은 shutdown cleanup failure로 재분류되지 않고 initialization failure로 유지됩니다.
114
+ - 서비스는 모듈 bootstrap 시 transport를 초기화하고, bootstrap verification 실패와 애플리케이션 shutdown 전반에서 factory-owned 리소스를 정확히 한 번 닫습니다. shutdown 전에 시작된 factory 생성 transport가 아직 완료되지 않았더라도 이를 기다리며, 진행 중인 bootstrap verification이 settle뒤에만 factory-owned transport를 닫습니다. Verification 실패 후 owned cleanup 실패하면 status diagnostics는 이를 `shutdown-cleanup`으로 재분류하지 않고 최초 `initialization` failure phase를 유지하며, reject된 factory creation도 initialization failure로 유지됩니다.
114
115
  - send는 bootstrap이 transport를 `ready`로 표시한 뒤에만 허용됩니다. bootstrap 전, startup 중, bootstrap 실패 후, shutdown 중, shutdown 후 시도는 전달 전에 거부됩니다.
116
+ - `ready` 상태에서 허용된 직접 send 또는 notification dispatch는 factory-owned transport resource를 닫기 전에 renderer와 transport delivery가 끝날 때까지 drain합니다. 전달 실패는 caller의 결과로 남고 shutdown 실패로 전파하지 않습니다.
115
117
  - 서비스가 shutdown 중이거나 이미 stopped 상태라면 cached transport를 재사용하지 않고 send를 거부합니다.
116
118
  - `DiscordService.sendNotification(...)`은 구성된 renderer를 호출하기 전에 lifecycle readiness를 확인하고, 호출자의 `AbortSignal`을 `DiscordTemplateRenderInput.signal`과 transport delivery 양쪽에 전달합니다.
117
119
  - `DiscordService.createPlatformStatusSnapshot()`은 `createDiscordPlatformStatusSnapshot(...)`과 같은 status 계약을 노출합니다. 여기에는 lifecycle/readiness, health, transport kind와 ownership, 기본 thread 구성, bootstrap verification 상태, bootstrap initialization 실패와 shutdown cleanup 실패를 구분하는 diagnostics, notifications channel dependency details가 포함되어, 호출자가 내부 옵션에 접근하지 않고도 Discord wiring을 관찰할 수 있습니다.
@@ -244,9 +246,26 @@ await discord.send({
244
246
 
245
247
  bot 기반 REST 전달처럼 더 풍부한 API 연동이 필요하다면 export된 `DiscordTransport` 계약을 구현해 `DiscordModule.forRoot(...)` 또는 `forRootAsync(...)`에 주입하면 됩니다.
246
248
 
249
+ ### 구성 가능한 webhook 재시도 정책
250
+
251
+ 내장 webhook transport는 일시적인 `408`, `429`, `5xx` 응답과 transport-level exception을 재시도합니다. `retry`를 생략하면 총 세 번 시도하고 250ms를 base로 지수 backoff하는 기존 기본값을 유지합니다. workload의 latency 또는 허용 가능한 재시도 수준이 다르다면 transport 생성 시 하나의 정책을 구성하세요.
252
+
253
+ ```typescript
254
+ const transport = createDiscordWebhookTransport({
255
+ fetch: runtime.fetch,
256
+ retry: {
257
+ attempts: 5,
258
+ baseDelayMs: 500,
259
+ },
260
+ webhookUrl: config.discordWebhookUrl,
261
+ });
262
+ ```
263
+
264
+ `attempts`에는 최초 요청이 포함되며 `1`부터 `10`까지의 정수여야 합니다. `baseDelayMs`는 `0`부터 `60000`까지의 정수여야 하고 이후 대기 시간은 이 값을 기준으로 두 배씩 증가합니다. 이 정책은 이 프로세스 안에서 한 번 보내는 bounded webhook 재시도에만 적용됩니다. 프로세스 재시작 뒤에도 notification을 보존해야 하거나, durable scheduling이 필요하거나, request latency와 독립적으로 delivery를 관리해야 한다면 webhook attempts를 늘리는 대신 queue-backed delivery를 사용하는 `@fluojs/notifications`를 사용하세요.
265
+
247
266
  Behavioral contract 메모:
248
267
 
249
- - 내장 webhook transport는 `408`, `429`, `5xx` 같은 일시적 응답뿐 아니라 transport-level exception도 bounded exponential backoff로 재시도한 뒤 호출자에게 에러를 노출합니다. 영구적인 upstream 응답은 재시도하지 않습니다.
268
+ - 내장 webhook transport는 `408`, `429`, `5xx` 같은 일시적 응답뿐 아니라 transport-level exception도 구성한 bounded exponential backoff로 재시도한 뒤 호출자에게 에러를 노출합니다. 영구적인 upstream 응답은 재시도하지 않습니다.
250
269
  - Retry backoff는 `DiscordSendOptions.signal`을 관찰합니다. 이미 abort된 signal은 다음 backoff timer를 기다리지 않고 즉시 reject됩니다.
251
270
  - 성공한 webhook 응답은 `DiscordSendResult.response`로 노출됩니다. rate-limit 재시도가 끝내 실패한 경우를 포함해, 호출자에게 보이는 `DiscordTransportError` 메시지는 기본적으로 raw upstream response body를 포함하지 않습니다.
252
271
  - 잘못되었거나 절대 URL이 아닌 `webhookUrl` 값은 전달 실패로 재시도하지 않고 즉시 `DiscordConfigurationError`로 거부됩니다.
@@ -288,6 +307,7 @@ Discord 패키지는 의도적으로 다음을 **포함하지 않습니다**:
288
307
 
289
308
  - `DiscordMessage`
290
309
  - `NormalizedDiscordMessage`
310
+ - `DiscordWebhookRetryOptions`
291
311
  - `DiscordWebhookTransportOptions`
292
312
  - `DiscordFetchLike`
293
313
  - `DiscordFetchResponse`
package/README.md CHANGED
@@ -18,6 +18,7 @@ Migration boundary: the module API is intentionally Nest-like but not a NestJS d
18
18
  - [Integration with `@fluojs/notifications`](#integration-with-fluojs-notifications)
19
19
  - [Template rendering with payload overrides](#template-rendering-with-payload-overrides)
20
20
  - [Webhook-first delivery with explicit fetch injection](#webhook-first-delivery-with-explicit-fetch-injection)
21
+ - [Configurable webhook retry policy](#configurable-webhook-retry-policy)
21
22
  - [Intentional limitations](#intentional-limitations)
22
23
  - [Public API Overview](#public-api-overview)
23
24
  - [Related Packages](#related-packages)
@@ -29,7 +30,7 @@ Migration boundary: the module API is intentionally Nest-like but not a NestJS d
29
30
  npm install @fluojs/discord @fluojs/notifications
30
31
  ```
31
32
 
32
- This package follows the repo-wide Node.js 20+ install baseline reflected in published package metadata, while keeping its delivery contract transport-agnostic at runtime through explicit fetch-compatible boundaries.
33
+ This package requires Node.js `>=24.0.0 <27` as its package-owned support contract. Upgrade Node 20 and Node 22 hosts to Node.js `>=24.0.0 <27`; Node versions below 24 and Node 27+ are unsupported, so move the process to a supported line. Its delivery contract remains transport-agnostic at runtime through explicit fetch-compatible boundaries.
33
34
 
34
35
  ## When to Use
35
36
 
@@ -110,8 +111,9 @@ Behavioral contract notes:
110
111
  - `DiscordModule.forRoot(...)` and `DiscordModule.forRootAsync(...)` export `DiscordService`, `DiscordChannel`, `DISCORD`, and `DISCORD_CHANNEL` globally by default. Use the fluo `global?: boolean` option and set `global: false` only when migrated code must keep Discord providers local to importing modules; NestJS `isGlobal` is not supported.
111
112
  - `DiscordService.send(...)` resolves `defaultThreadId` before delivery.
112
113
  - `DiscordService.sendMany(...)` is a direct `DiscordMessage[]` batch API that sends messages sequentially and supports `continueOnError`; it is not a multi-recipient `@fluojs/notifications` dispatch shortcut.
113
- - The service initializes the configured transport during module bootstrap and closes factory-owned resources exactly once across bootstrap verification failure and application shutdown, including any in-flight factory-created transport before shutdown began. A rejected factory creation remains an initialization failure instead of being reclassified as a shutdown cleanup failure.
114
+ - The service initializes the configured transport during module bootstrap and closes factory-owned resources exactly once across bootstrap verification failure and application shutdown, including any in-flight factory-created transport before shutdown began. Shutdown waits for in-flight bootstrap verification to settle before closing a factory-owned transport. If verification fails and the subsequent owned cleanup also fails, status diagnostics preserve the first `initialization` failure phase instead of reclassifying it as `shutdown-cleanup`; a rejected factory creation likewise remains an initialization failure.
114
115
  - Sends are accepted only after bootstrap marks the transport `ready`; attempts before bootstrap, during startup, after failed bootstrap, while shutting down, or after shutdown are rejected before delivery.
116
+ - A direct send or notification dispatch accepted while `ready` drains its renderer and transport delivery before factory-owned transport resources close. Delivery rejection remains the caller's result and does not make shutdown fail.
115
117
  - Sends attempted while the service is shutting down or already stopped are rejected before reusing the cached transport.
116
118
  - `DiscordService.sendNotification(...)` checks lifecycle readiness before invoking a configured renderer and passes the caller's `AbortSignal` to both `DiscordTemplateRenderInput.signal` and transport delivery.
117
119
  - `DiscordService.createPlatformStatusSnapshot()` exposes the same status contract as `createDiscordPlatformStatusSnapshot(...)`: lifecycle/readiness, health, transport kind and ownership, default thread configuration, bootstrap verification state, distinct bootstrap initialization versus shutdown cleanup failure diagnostics, and notifications channel dependency details, so callers can observe Discord wiring without reaching into internal options.
@@ -244,9 +246,26 @@ await discord.send({
244
246
 
245
247
  For richer API integrations such as bot-backed REST delivery, implement the exported `DiscordTransport` contract and inject it through `DiscordModule.forRoot(...)` or `forRootAsync(...)`.
246
248
 
249
+ ### Configurable webhook retry policy
250
+
251
+ The built-in webhook transport retries transient `408`, `429`, and `5xx` responses and transport-level exceptions. Omit `retry` to preserve the default of three total attempts with a 250ms exponential-backoff base delay. Configure one workload-specific policy at transport construction when its latency or tolerance differs:
252
+
253
+ ```typescript
254
+ const transport = createDiscordWebhookTransport({
255
+ fetch: runtime.fetch,
256
+ retry: {
257
+ attempts: 5,
258
+ baseDelayMs: 500,
259
+ },
260
+ webhookUrl: config.discordWebhookUrl,
261
+ });
262
+ ```
263
+
264
+ `attempts` includes the initial request and must be an integer from `1` through `10`. `baseDelayMs` must be an integer from `0` through `60000`; later waits double from that base. The retry policy applies only to this bounded in-process webhook send. When a notification must survive process restarts, require durable scheduling, or need delivery managed independently of request latency, use `@fluojs/notifications` with queue-backed delivery instead of increasing webhook attempts.
265
+
247
266
  Behavioral contract notes:
248
267
 
249
- - The built-in webhook transport retries transient `408`, `429`, and `5xx` responses, and also retries transport-level exceptions, using bounded exponential backoff before surfacing an error. Permanent upstream responses are not retried.
268
+ - The built-in webhook transport retries transient `408`, `429`, and `5xx` responses, and also retries transport-level exceptions, using the configured bounded exponential backoff before surfacing an error. Permanent upstream responses are not retried.
250
269
  - Retry backoff observes `DiscordSendOptions.signal`; an already-aborted signal rejects immediately instead of waiting for the next backoff timer.
251
270
  - Successful webhook responses are exposed through `DiscordSendResult.response`; caller-visible `DiscordTransportError` messages still omit raw upstream response bodies by default, including after rate-limit retries fail.
252
271
  - Malformed or non-absolute `webhookUrl` values are rejected immediately as `DiscordConfigurationError` instead of being retried as delivery failures.
@@ -288,6 +307,7 @@ The package intentionally keeps `createDiscordProviders(...)`, `DISCORD_OPTIONS`
288
307
 
289
308
  - `DiscordMessage`
290
309
  - `NormalizedDiscordMessage`
310
+ - `DiscordWebhookRetryOptions`
291
311
  - `DiscordWebhookTransportOptions`
292
312
  - `DiscordFetchLike`
293
313
  - `DiscordFetchResponse`
package/dist/index.d.ts CHANGED
@@ -5,6 +5,6 @@ export { DiscordService } from './service.js';
5
5
  export { createDiscordPlatformStatusSnapshot } from './status.js';
6
6
  export type { DiscordLifecycleState, DiscordPlatformStatusSnapshot, DiscordStatusAdapterInput } from './status.js';
7
7
  export { DISCORD, DISCORD_CHANNEL } from './tokens.js';
8
- export type { Discord, DiscordAllowedMentions, DiscordAsyncModuleOptions, DiscordAttachment, DiscordComponent, DiscordEmbed, DiscordFetchLike, DiscordFetchResponse, DiscordMessage, DiscordModuleOptions, DiscordNotificationDispatchRequest, DiscordNotificationPayload, DiscordPoll, DiscordSendBatchResult, DiscordSendFailure, DiscordSendManyOptions, DiscordSendOptions, DiscordSendResult, DiscordTemplateRenderInput, DiscordTemplateRenderer, DiscordTemplateRenderResult, DiscordTransport, DiscordTransportContext, DiscordTransportFactory, DiscordTransportReceipt, DiscordWebhookTransportOptions, NormalizedDiscordMessage, } from './types.js';
8
+ export type { Discord, DiscordAllowedMentions, DiscordAsyncModuleOptions, DiscordAttachment, DiscordComponent, DiscordEmbed, DiscordFetchLike, DiscordFetchResponse, DiscordMessage, DiscordModuleOptions, DiscordNotificationDispatchRequest, DiscordNotificationPayload, DiscordPoll, DiscordSendBatchResult, DiscordSendFailure, DiscordSendManyOptions, DiscordSendOptions, DiscordSendResult, DiscordTemplateRenderInput, DiscordTemplateRenderer, DiscordTemplateRenderResult, DiscordTransport, DiscordTransportContext, DiscordTransportFactory, DiscordTransportReceipt, DiscordWebhookRetryOptions, DiscordWebhookTransportOptions, NormalizedDiscordMessage, } from './types.js';
9
9
  export { createDiscordWebhookTransport } from './webhook.js';
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,EACzB,6BAA6B,EAC7B,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,mCAAmC,EAAE,MAAM,aAAa,CAAC;AAClE,YAAY,EAAE,qBAAqB,EAAE,6BAA6B,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AACnH,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACvD,YAAY,EACV,OAAO,EACP,sBAAsB,EACtB,yBAAyB,EACzB,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,gBAAgB,EAChB,oBAAoB,EACpB,cAAc,EACd,oBAAoB,EACpB,kCAAkC,EAClC,0BAA0B,EAC1B,WAAW,EACX,sBAAsB,EACtB,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,0BAA0B,EAC1B,uBAAuB,EACvB,2BAA2B,EAC3B,gBAAgB,EAChB,uBAAuB,EACvB,uBAAuB,EACvB,uBAAuB,EACvB,8BAA8B,EAC9B,wBAAwB,GACzB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,6BAA6B,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,EACzB,6BAA6B,EAC7B,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,mCAAmC,EAAE,MAAM,aAAa,CAAC;AAClE,YAAY,EAAE,qBAAqB,EAAE,6BAA6B,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AACnH,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACvD,YAAY,EACV,OAAO,EACP,sBAAsB,EACtB,yBAAyB,EACzB,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,gBAAgB,EAChB,oBAAoB,EACpB,cAAc,EACd,oBAAoB,EACpB,kCAAkC,EAClC,0BAA0B,EAC1B,WAAW,EACX,sBAAsB,EACtB,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,0BAA0B,EAC1B,uBAAuB,EACvB,2BAA2B,EAC3B,gBAAgB,EAChB,uBAAuB,EACvB,uBAAuB,EACvB,uBAAuB,EACvB,0BAA0B,EAC1B,8BAA8B,EAC9B,wBAAwB,GACzB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,6BAA6B,EAAE,MAAM,cAAc,CAAC"}
package/dist/service.d.ts CHANGED
@@ -10,12 +10,14 @@ import type { Discord, DiscordMessage, DiscordNotificationDispatchRequest, Disco
10
10
  */
11
11
  export declare class DiscordService implements Discord, OnModuleInit, OnApplicationShutdown {
12
12
  private readonly options;
13
+ private readonly acceptedDeliveryPromises;
13
14
  private lifecycleFailurePhase;
14
15
  private lifecycleState;
15
16
  private ownedTransportCleanupPromise;
16
17
  private resolvedTransport;
17
18
  private shutdownPromise;
18
19
  private transportPromise;
20
+ private transportVerificationPromise;
19
21
  constructor(options: NormalizedDiscordModuleOptions);
20
22
  onApplicationShutdown(): Promise<void>;
21
23
  private closeOwnedTransport;
@@ -45,6 +47,7 @@ export declare class DiscordService implements Discord, OnModuleInit, OnApplicat
45
47
  * ```
46
48
  */
47
49
  send(message: DiscordMessage, options?: DiscordSendOptions): Promise<DiscordSendResult>;
50
+ private sendAccepted;
48
51
  /**
49
52
  * Sends multiple Discord messages in input order with optional tolerant failure handling.
50
53
  *
@@ -77,8 +80,11 @@ export declare class DiscordService implements Discord, OnModuleInit, OnApplicat
77
80
  * ```
78
81
  */
79
82
  sendNotification(notification: DiscordNotificationDispatchRequest, options?: DiscordSendOptions): Promise<DiscordSendResult>;
83
+ private sendNotificationAccepted;
80
84
  private ensureTransport;
81
85
  private clearResolvedTransport;
86
+ private drainAcceptedDeliveries;
87
+ private trackAcceptedDelivery;
82
88
  private handleTransportInitializationFailure;
83
89
  private assertReadyForSend;
84
90
  private normalizeMessage;
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAK3E,OAAO,KAAK,EACV,OAAO,EACP,cAAc,EACd,kCAAkC,EAClC,sBAAsB,EAEtB,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EAIjB,8BAA8B,EAC/B,MAAM,YAAY,CAAC;AA+DpB;;;;;;;GAOG;AACH,qBACa,cAAe,YAAW,OAAO,EAAE,YAAY,EAAE,qBAAqB;IAQrE,OAAO,CAAC,QAAQ,CAAC,OAAO;IAPpC,OAAO,CAAC,qBAAqB,CAAkD;IAC/E,OAAO,CAAC,cAAc,CAA2C;IACjE,OAAO,CAAC,4BAA4B,CAA4B;IAChE,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,eAAe,CAA4B;IACnD,OAAO,CAAC,gBAAgB,CAAwC;gBAEnC,OAAO,EAAE,8BAA8B;IAE9D,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;YAiB9B,mBAAmB;IAajC,OAAO,CAAC,4BAA4B;YAQtB,gCAAgC;YAYhC,0BAA0B;IAelC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IA8BnC;;;;OAIG;IACH,4BAA4B;IAY5B;;;;;;;;;;;;;;OAcG;IACG,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA0BjG;;;;;;;;;;;;OAYG;IACG,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,EAAE,OAAO,GAAE,sBAA2B,GAAG,OAAO,CAAC,sBAAsB,CAAC;IA6B1H;;;;;;;;;;;;;;;;OAgBG;IACG,gBAAgB,CACpB,YAAY,EAAE,kCAAkC,EAChD,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC;YAmCf,eAAe;IAuB7B,OAAO,CAAC,sBAAsB;YAKhB,oCAAoC;IAwBlD,OAAO,CAAC,kBAAkB;IAU1B,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,2BAA2B;YAkBrB,kBAAkB;CAiBjC"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAK3E,OAAO,KAAK,EACV,OAAO,EACP,cAAc,EACd,kCAAkC,EAClC,sBAAsB,EAEtB,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EAIjB,8BAA8B,EAC/B,MAAM,YAAY,CAAC;AA+DpB;;;;;;;GAOG;AACH,qBACa,cAAe,YAAW,OAAO,EAAE,YAAY,EAAE,qBAAqB;IAUrE,OAAO,CAAC,QAAQ,CAAC,OAAO;IATpC,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAA+B;IACxE,OAAO,CAAC,qBAAqB,CAAkD;IAC/E,OAAO,CAAC,cAAc,CAA2C;IACjE,OAAO,CAAC,4BAA4B,CAA4B;IAChE,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,eAAe,CAA4B;IACnD,OAAO,CAAC,gBAAgB,CAAwC;IAChE,OAAO,CAAC,4BAA4B,CAA4B;gBAEnC,OAAO,EAAE,8BAA8B;IAE9D,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;YAgB9B,mBAAmB;IAcjC,OAAO,CAAC,4BAA4B;YAQtB,gCAAgC;YAoBhC,0BAA0B;IAelC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IA+BnC;;;;OAIG;IACH,4BAA4B;IAY5B;;;;;;;;;;;;;;OAcG;IACG,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,iBAAiB,CAAC;YAUnF,YAAY;IAuB1B;;;;;;;;;;;;OAYG;IACG,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,EAAE,OAAO,GAAE,sBAA2B,GAAG,OAAO,CAAC,sBAAsB,CAAC;IA6B1H;;;;;;;;;;;;;;;;OAgBG;IACG,gBAAgB,CACpB,YAAY,EAAE,kCAAkC,EAChD,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC;YAYf,wBAAwB;YAkCxB,eAAe;IAuB7B,OAAO,CAAC,sBAAsB;YAMhB,uBAAuB;IAIrC,OAAO,CAAC,qBAAqB;YA2Bf,oCAAoC;IAuBlD,OAAO,CAAC,kBAAkB;IAU1B,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,2BAA2B;YAkBrB,kBAAkB;CAiBjC"}
package/dist/service.js CHANGED
@@ -53,12 +53,14 @@ class DiscordService {
53
53
  static {
54
54
  [_DiscordService, _initClass] = _applyDecs(this, [Inject(DISCORD_OPTIONS)], []).c;
55
55
  }
56
+ acceptedDeliveryPromises = new Set();
56
57
  lifecycleFailurePhase;
57
58
  lifecycleState = 'created';
58
59
  ownedTransportCleanupPromise;
59
60
  resolvedTransport;
60
61
  shutdownPromise;
61
62
  transportPromise;
63
+ transportVerificationPromise;
62
64
  constructor(options) {
63
65
  this.options = options;
64
66
  }
@@ -70,18 +72,18 @@ class DiscordService {
70
72
  return this.shutdownPromise;
71
73
  }
72
74
  this.lifecycleState = 'stopping';
73
- this.lifecycleFailurePhase = undefined;
74
75
  this.shutdownPromise = this.closeOwnedTransport();
75
76
  return this.shutdownPromise;
76
77
  }
77
78
  async closeOwnedTransport() {
78
79
  try {
80
+ await this.drainAcceptedDeliveries();
79
81
  await this.closeOwnedTransportResources();
80
82
  this.lifecycleState = 'stopped';
81
83
  this.lifecycleFailurePhase = undefined;
82
84
  } catch (error) {
83
85
  this.lifecycleState = 'failed';
84
- this.lifecycleFailurePhase = 'shutdown-cleanup';
86
+ this.lifecycleFailurePhase ??= 'shutdown-cleanup';
85
87
  throw new Error('Discord transport failed to close cleanly.', {
86
88
  cause: error
87
89
  });
@@ -96,6 +98,10 @@ class DiscordService {
96
98
  async closeOwnedTransportResourcesOnce() {
97
99
  try {
98
100
  const transport = await this.resolveTransportForCleanup();
101
+ const verificationPromise = this.transportVerificationPromise;
102
+ if (verificationPromise) {
103
+ await verificationPromise.then(() => undefined, () => undefined);
104
+ }
99
105
  if (transport && this.options.transport.ownsResources && transport.close) {
100
106
  await transport.close();
101
107
  }
@@ -124,7 +130,8 @@ class DiscordService {
124
130
  return;
125
131
  }
126
132
  if (this.options.verifyOnModuleInit && transport.verify) {
127
- await transport.verify();
133
+ this.transportVerificationPromise = Promise.resolve(transport.verify());
134
+ await this.transportVerificationPromise;
128
135
  }
129
136
  if (this.lifecycleState !== 'starting') {
130
137
  return;
@@ -173,8 +180,10 @@ class DiscordService {
173
180
  throw createAbortError();
174
181
  }
175
182
  this.assertReadyForSend();
176
- const transport = await this.ensureTransport();
177
- this.assertReadyForSend();
183
+ return this.trackAcceptedDelivery(() => this.sendAccepted(message, options, this.ensureTransport()));
184
+ }
185
+ async sendAccepted(message, options, transportPromise) {
186
+ const transport = await transportPromise;
178
187
  const normalized = this.normalizeMessage(message);
179
188
  assertMessageContent(normalized);
180
189
  const result = await transport.send(normalized, options);
@@ -251,9 +260,12 @@ class DiscordService {
251
260
  throw createAbortError();
252
261
  }
253
262
  this.assertReadyForSend();
263
+ return this.trackAcceptedDelivery(() => this.sendNotificationAccepted(notification, options, this.ensureTransport()));
264
+ }
265
+ async sendNotificationAccepted(notification, options, transportPromise) {
254
266
  const payload = notification.payload;
255
267
  const rendered = await this.renderNotification(notification, options.signal);
256
- return this.send({
268
+ return this.sendAccepted({
257
269
  allowedMentions: payload.allowedMentions,
258
270
  attachments: payload.attachments ?? [],
259
271
  avatarUrl: payload.avatarUrl,
@@ -276,7 +288,7 @@ class DiscordService {
276
288
  threadName: payload.threadName,
277
289
  tts: payload.tts,
278
290
  username: payload.username
279
- }, options);
291
+ }, options, transportPromise);
280
292
  }
281
293
  async ensureTransport() {
282
294
  if (this.lifecycleState === 'stopping' || this.lifecycleState === 'stopped') {
@@ -299,12 +311,35 @@ class DiscordService {
299
311
  clearResolvedTransport() {
300
312
  this.resolvedTransport = undefined;
301
313
  this.transportPromise = undefined;
314
+ this.transportVerificationPromise = undefined;
315
+ }
316
+ async drainAcceptedDeliveries() {
317
+ await Promise.allSettled(this.acceptedDeliveryPromises);
318
+ }
319
+ trackAcceptedDelivery(startDelivery) {
320
+ let rejectDelivery;
321
+ let resolveDelivery;
322
+ const delivery = new Promise((resolve, reject) => {
323
+ rejectDelivery = reject;
324
+ resolveDelivery = resolve;
325
+ });
326
+ this.acceptedDeliveryPromises.add(delivery);
327
+ void delivery.then(() => {
328
+ this.acceptedDeliveryPromises.delete(delivery);
329
+ }, () => {
330
+ this.acceptedDeliveryPromises.delete(delivery);
331
+ });
332
+ if (!resolveDelivery || !rejectDelivery) {
333
+ throw new Error('Discord delivery tracker failed to initialize.');
334
+ }
335
+ void startDelivery().then(resolveDelivery, rejectDelivery);
336
+ return delivery;
302
337
  }
303
338
  async handleTransportInitializationFailure(error) {
304
339
  const interruptedByShutdown = isShutdownLifecycleState(this.lifecycleState);
340
+ this.lifecycleFailurePhase ??= 'initialization';
305
341
  if (!interruptedByShutdown) {
306
342
  this.lifecycleState = 'failed';
307
- this.lifecycleFailurePhase = 'initialization';
308
343
  }
309
344
  let cause = error;
310
345
  try {
@@ -314,7 +349,6 @@ class DiscordService {
314
349
  }
315
350
  if (!interruptedByShutdown && !isShutdownLifecycleState(this.lifecycleState)) {
316
351
  this.lifecycleState = 'failed';
317
- this.lifecycleFailurePhase = 'initialization';
318
352
  }
319
353
  throw new Error('Discord transport failed to initialize.', {
320
354
  cause
package/dist/types.d.ts CHANGED
@@ -121,9 +121,17 @@ export type DiscordFetchLike = (input: string, init?: {
121
121
  method?: string;
122
122
  signal?: AbortSignal;
123
123
  }) => MaybePromise<DiscordFetchResponse>;
124
+ /** Bounded retry policy accepted by {@link createDiscordWebhookTransport}. */
125
+ export interface DiscordWebhookRetryOptions {
126
+ /** Total delivery attempts, including the initial request. Defaults to `3` and accepts values from `1` through `10`. */
127
+ attempts?: number;
128
+ /** Initial exponential-backoff delay in milliseconds. Defaults to `250` and accepts values from `0` through `60000`. */
129
+ baseDelayMs?: number;
130
+ }
124
131
  /** Options accepted by {@link createDiscordWebhookTransport}. */
125
132
  export interface DiscordWebhookTransportOptions {
126
133
  fetch?: DiscordFetchLike;
134
+ retry?: DiscordWebhookRetryOptions;
127
135
  wait?: boolean;
128
136
  webhookUrl: string;
129
137
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../src/webhook.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAGV,gBAAgB,EAEhB,8BAA8B,EAE/B,MAAM,YAAY,CAAC;AAqIpB;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,8BAA8B,GAAG,gBAAgB,CAyEvG"}
1
+ {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../src/webhook.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAGV,gBAAgB,EAEhB,8BAA8B,EAE/B,MAAM,YAAY,CAAC;AA8JpB;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,8BAA8B,GAAG,gBAAgB,CAwEvG"}
package/dist/webhook.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { DiscordConfigurationError, DiscordTransportError } from './errors.js';
2
2
  const DEFAULT_RETRY_ATTEMPTS = 3;
3
3
  const DEFAULT_RETRY_DELAY_MS = 250;
4
+ const MAX_RETRY_ATTEMPTS = 10;
5
+ const MAX_RETRY_DELAY_MS = 60_000;
4
6
  function normalizeOptionalString(value) {
5
7
  const trimmed = value?.trim();
6
8
  return trimmed && trimmed.length > 0 ? trimmed : undefined;
@@ -89,13 +91,27 @@ function isAbortError(error) {
89
91
  function isTransientStatus(status) {
90
92
  return status === 408 || status === 429 || status >= 500 && status <= 599;
91
93
  }
92
- async function waitForRetry(delayMs, signal) {
93
- if (delayMs <= 0) {
94
- return;
94
+ function resolveRetryPolicy(retry) {
95
+ const attempts = retry?.attempts ?? DEFAULT_RETRY_ATTEMPTS;
96
+ if (!Number.isInteger(attempts) || attempts < 1 || attempts > MAX_RETRY_ATTEMPTS) {
97
+ throw new DiscordConfigurationError(`Discord webhook transport \`retry.attempts\` must be an integer between 1 and ${String(MAX_RETRY_ATTEMPTS)}.`);
95
98
  }
99
+ const baseDelayMs = retry?.baseDelayMs ?? DEFAULT_RETRY_DELAY_MS;
100
+ if (!Number.isInteger(baseDelayMs) || baseDelayMs < 0 || baseDelayMs > MAX_RETRY_DELAY_MS) {
101
+ throw new DiscordConfigurationError(`Discord webhook transport \`retry.baseDelayMs\` must be an integer between 0 and ${String(MAX_RETRY_DELAY_MS)}.`);
102
+ }
103
+ return {
104
+ attempts,
105
+ baseDelayMs
106
+ };
107
+ }
108
+ async function waitForRetry(delayMs, signal) {
96
109
  if (signal?.aborted) {
97
110
  throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');
98
111
  }
112
+ if (delayMs <= 0) {
113
+ return;
114
+ }
99
115
  await new Promise((resolve, reject) => {
100
116
  const timer = setTimeout(() => {
101
117
  signal?.removeEventListener('abort', onAbort);
@@ -147,12 +163,14 @@ export function createDiscordWebhookTransport(options) {
147
163
  }
148
164
  const parsedWebhookUrl = parseWebhookUrl(webhookUrl);
149
165
  const fetchLike = resolveFetch(options.fetch);
166
+ const retry = resolveRetryPolicy(options.retry);
150
167
  const wait = options.wait ?? true;
151
168
  return {
152
169
  async send(message, context) {
153
- for (let attempt = 1; attempt <= DEFAULT_RETRY_ATTEMPTS; attempt += 1) {
170
+ for (let attempt = 1; attempt <= retry.attempts; attempt += 1) {
171
+ let response;
154
172
  try {
155
- const response = await fetchLike(resolveWebhookUrl(parsedWebhookUrl, message, wait), {
173
+ response = await fetchLike(resolveWebhookUrl(parsedWebhookUrl, message, wait), {
156
174
  body: JSON.stringify(createWebhookPayload(message)),
157
175
  headers: {
158
176
  'content-type': 'application/json; charset=utf-8'
@@ -160,44 +178,41 @@ export function createDiscordWebhookTransport(options) {
160
178
  method: 'POST',
161
179
  signal: context.signal
162
180
  });
163
- const body = await readResponseBody(response);
164
- if (!response.ok) {
165
- if (attempt < DEFAULT_RETRY_ATTEMPTS && isTransientStatus(response.status)) {
166
- await waitForRetry(DEFAULT_RETRY_DELAY_MS * 2 ** (attempt - 1), context.signal);
167
- continue;
168
- }
169
- throw new DiscordTransportError(createStatusFailureMessage(response, attempt));
170
- }
171
- const parsed = parseJsonRecord(body);
172
- const warnings = [];
173
- if (body && !parsed) {
174
- warnings.push('Discord webhook returned a non-JSON success body.');
175
- }
176
- return {
177
- channelId: getStringField(parsed, 'channel_id'),
178
- guildId: getStringField(parsed, 'guild_id'),
179
- messageId: getStringField(parsed, 'id'),
180
- ok: true,
181
- response: body || undefined,
182
- statusCode: response.status,
183
- threadId: getStringField(parsed, 'thread_id') ?? message.threadId,
184
- warnings
185
- };
186
181
  } catch (error) {
187
182
  if (isAbortError(error) || context.signal?.aborted) {
188
183
  throw error;
189
184
  }
190
- if (error instanceof DiscordTransportError) {
191
- throw error;
192
- }
193
- if (attempt < DEFAULT_RETRY_ATTEMPTS) {
194
- await waitForRetry(DEFAULT_RETRY_DELAY_MS * 2 ** (attempt - 1), context.signal);
185
+ if (attempt < retry.attempts) {
186
+ await waitForRetry(retry.baseDelayMs * 2 ** (attempt - 1), context.signal);
195
187
  continue;
196
188
  }
197
189
  throw new DiscordTransportError(createTransportFailureMessage(attempt));
198
190
  }
191
+ const body = await readResponseBody(response);
192
+ if (!response.ok) {
193
+ if (attempt < retry.attempts && isTransientStatus(response.status)) {
194
+ await waitForRetry(retry.baseDelayMs * 2 ** (attempt - 1), context.signal);
195
+ continue;
196
+ }
197
+ throw new DiscordTransportError(createStatusFailureMessage(response, attempt));
198
+ }
199
+ const parsed = parseJsonRecord(body);
200
+ const warnings = [];
201
+ if (body && !parsed) {
202
+ warnings.push('Discord webhook returned a non-JSON success body.');
203
+ }
204
+ return {
205
+ channelId: getStringField(parsed, 'channel_id'),
206
+ guildId: getStringField(parsed, 'guild_id'),
207
+ messageId: getStringField(parsed, 'id'),
208
+ ok: true,
209
+ response: body || undefined,
210
+ statusCode: response.status,
211
+ threadId: getStringField(parsed, 'thread_id') ?? message.threadId,
212
+ warnings
213
+ };
199
214
  }
200
- throw new DiscordTransportError(createTransportFailureMessage(DEFAULT_RETRY_ATTEMPTS));
215
+ throw new DiscordTransportError(createTransportFailureMessage(retry.attempts));
201
216
  }
202
217
  };
203
218
  }
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "portable",
10
10
  "fetch"
11
11
  ],
12
- "version": "1.0.5",
12
+ "version": "2.0.0",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -18,7 +18,7 @@
18
18
  "directory": "packages/discord"
19
19
  },
20
20
  "engines": {
21
- "node": ">=20.0.0"
21
+ "node": ">=24.0.0 <27"
22
22
  },
23
23
  "publishConfig": {
24
24
  "access": "public"
@@ -36,13 +36,13 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
- "@fluojs/core": "^1.1.0",
40
- "@fluojs/di": "^2.0.0",
41
- "@fluojs/notifications": "^1.0.3",
42
- "@fluojs/runtime": "^2.0.1"
39
+ "@fluojs/core": "^2.0.0",
40
+ "@fluojs/di": "^3.0.0",
41
+ "@fluojs/notifications": "^2.0.0",
42
+ "@fluojs/runtime": "^3.0.0"
43
43
  },
44
44
  "devDependencies": {
45
- "vitest": "^3.2.4"
45
+ "vitest": "^4.1.11"
46
46
  },
47
47
  "scripts": {
48
48
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",