@fluojs/discord 1.0.2 → 1.0.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.ko.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  fluo를 위한 webhook-first, transport-agnostic Discord 전달 코어 패키지입니다. Nest-like 모듈 API, standalone 사용을 위한 주입 가능한 `DiscordService`, 그리고 Node 전용 Discord SDK를 가정하지 않는 `@fluojs/notifications` 연동용 1st-party `DiscordChannel`을 제공합니다.
6
6
 
7
+ 마이그레이션 경계: 이 모듈 API는 의도적으로 Nest-like이지만 NestJS dynamic-module clone은 아닙니다. `DiscordModule`은 `global: options.global ?? true`로 기본 global이며, `forRootAsync(...)`는 `inject`와 `useFactory`만 지원하고, 내부 provider helper/token은 private으로 유지되어 애플리케이션은 module facade와 export된 service/channel token으로 Discord를 조합해야 합니다.
8
+
7
9
  ## 목차
8
10
 
9
11
  - [설치](#설치)
@@ -92,12 +94,17 @@ DiscordModule.forRootAsync({
92
94
  });
93
95
  ```
94
96
 
97
+ `forRootAsync(...)`는 fluo async 형태만 받습니다. 필요한 의존성은 애플리케이션 module graph에 먼저 등록하고, token을 `inject`에 나열한 뒤, `useFactory`에서 최종 `DiscordModuleOptions`를 반환하세요. NestJS `imports`, `useClass`, `useExisting` 변형은 소비하지 않으므로 그런 패턴은 Discord에 option을 넘기기 전에 application-owned provider로 옮겨야 합니다.
98
+
95
99
  Behavioral contract 메모:
96
100
 
101
+ - `DiscordModule.forRoot(...)`와 `DiscordModule.forRootAsync(...)`는 `DiscordService`, `DiscordChannel`, `DISCORD`, `DISCORD_CHANNEL`을 기본 global로 export합니다. fluo 옵션인 `global?: boolean`을 사용하고, migrated code가 Discord provider를 importing module 안에만 유지해야 할 때만 `global: false`를 설정하세요. NestJS `isGlobal`은 지원하지 않습니다.
97
102
  - `DiscordService.send(...)`는 전달 전에 `defaultThreadId`를 해석합니다.
98
- - 서비스는 모듈 bootstrap transport를 초기화하고, factory가 소유한 리소스만 애플리케이션 shutdown 닫습니다.
103
+ - `DiscordService.sendMany(...)`는 `DiscordMessage[]`를 직접 순차 전송하는 batch API이며 `continueOnError`를 지원합니다. 이는 multi-recipient `@fluojs/notifications` dispatch shortcut이 아닙니다.
104
+ - 서비스는 모듈 bootstrap 시 transport를 초기화하고, shutdown 전에 시작된 factory 생성 transport가 아직 완료되지 않았더라도 이를 기다린 뒤 factory가 소유한 리소스를 애플리케이션 shutdown 시 닫습니다.
99
105
  - send는 bootstrap이 transport를 `ready`로 표시한 뒤에만 허용됩니다. bootstrap 전, startup 중, bootstrap 실패 후, shutdown 중, shutdown 후 시도는 전달 전에 거부됩니다.
100
106
  - 서비스가 shutdown 중이거나 이미 stopped 상태라면 cached transport를 재사용하지 않고 send를 거부합니다.
107
+ - `DiscordService.createPlatformStatusSnapshot()`은 `createDiscordPlatformStatusSnapshot(...)`과 같은 status 계약을 노출합니다. 여기에는 lifecycle/readiness, health, transport kind와 ownership, 기본 thread 구성, bootstrap verification 상태, notifications channel dependency details가 포함되어, 호출자가 내부 옵션에 접근하지 않고도 Discord wiring을 관찰할 수 있습니다.
101
108
  - 빈 `defaultThreadId`와 `notifications.channel` 값은 trim 후 무시됩니다. notifications channel은 기본적으로 `discord`입니다.
102
109
  - 이 패키지는 절대로 `process.env`를 직접 읽지 않습니다. 모든 설정은 명시적인 옵션 또는 DI를 통해 들어와야 합니다.
103
110
 
@@ -144,7 +151,7 @@ Behavioral contract 메모:
144
151
  - 하나의 notification dispatch는 정확히 하나의 Discord thread 경로로 매핑됩니다. `payload.threadId` 또는 `recipients`의 단일 항목을 사용해야 합니다.
145
152
  - `payload.threadId`가 없으면 `DiscordService.sendNotification(...)`는 첫 번째 `recipients` 항목을 사용하고, 그것도 없으면 `defaultThreadId`로 폴백합니다.
146
153
  - notification metadata는 payload metadata, dispatch metadata, template/subject marker를 합쳐 구성됩니다. `template`은 renderer가 구성된 경우에만 렌더링됩니다.
147
- - 여러 Discord thread로 fan-out이 필요하다면 하나의 multi-recipient dispatch 대신 `sendMany(...)`를 사용해야 합니다.
154
+ - 여러 Discord thread로 fan-out이 필요한 notification workflow라면 thread별 concrete Discord message를 만들어 `DiscordService.sendMany(...)`로 보내거나 별도 notification dispatch를 실행해야 합니다. 하나의 notification dispatch는 multi-recipient fan-out을 암묵적으로 확장하지 않습니다.
148
155
 
149
156
  ### 명시적 fetch 주입을 사용하는 webhook-first 전달
150
157
 
@@ -177,6 +184,7 @@ Discord 패키지는 의도적으로 다음을 **포함하지 않습니다**:
177
184
  - 자격 증명이나 webhook URL을 `process.env`에서 직접 읽는 동작
178
185
  - 공유 루트 패키지 경계에 Node 전용 Discord SDK를 내장하는 것
179
186
  - webhook helper와 export된 transport 계약 이상으로 하나의 provider 전략을 강제하는 것
187
+ - 애플리케이션 import용 내부 provider helper, normalized option token, 또는 NestJS-style custom provider replacement seam을 노출하는 것
180
188
  - 하나의 dispatch 호출 안에서 multi-thread fan-out을 자동 변환하는 것
181
189
 
182
190
  이 제한 사항은 런타임 선택, provider capability, rollout 전략이 애플리케이션 경계에서 명시적으로 결정되도록 하기 위한 package contract의 일부입니다.
@@ -190,12 +198,18 @@ Discord 패키지는 의도적으로 다음을 **포함하지 않습니다**:
190
198
  - `DiscordModuleOptions`
191
199
  - `DiscordAsyncModuleOptions`
192
200
  - `DiscordService`
201
+ - `DiscordService.send(message, options)`
202
+ - `DiscordService.sendMany(messages, options)`
203
+ - `DiscordService.sendNotification(notification, options)`
204
+ - `DiscordService.createPlatformStatusSnapshot()`
193
205
  - `DiscordChannel`
194
206
  - `DISCORD`
195
207
  - `DISCORD_CHANNEL`
196
208
 
197
209
  애플리케이션 구성은 `DiscordModule`로, notifications 연동은 `DISCORD_CHANNEL`과 export된 transport 계약으로 조합합니다.
198
210
 
211
+ 이 패키지는 `createDiscordProviders(...)`, `DISCORD_OPTIONS`, `NormalizedDiscordModuleOptions`를 public root barrel에 의도적으로 노출하지 않습니다. 기존 migration이 NestJS 내부 provider token이나 custom provider seam을 바꾸고 있었다면 private helper를 import하지 말고 `DiscordModule.forRoot(...)` / `forRootAsync(...)`를 감싸는 app-owned module을 구성하세요.
212
+
199
213
  ### 계약과 헬퍼
200
214
 
201
215
  - `DiscordMessage`
@@ -226,6 +240,7 @@ Discord 패키지는 의도적으로 다음을 **포함하지 않습니다**:
226
240
 
227
241
  ### 상태 및 에러
228
242
 
243
+ - `DiscordService.createPlatformStatusSnapshot()`
229
244
  - `createDiscordPlatformStatusSnapshot(...)`
230
245
  - `DiscordLifecycleState`
231
246
  - `DiscordPlatformStatusSnapshot`
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  Webhook-first, transport-agnostic Discord delivery core for fluo. It provides a Nest-like module API, an injectable `DiscordService` for standalone usage, and a first-party `DiscordChannel` for `@fluojs/notifications` integration without assuming a Node-only Discord SDK.
6
6
 
7
+ Migration boundary: the module API is intentionally Nest-like but not a NestJS dynamic-module clone. `DiscordModule` is global by default through `global: options.global ?? true`, `forRootAsync(...)` supports only `inject` plus `useFactory`, and internal provider helpers/tokens stay private so applications compose Discord through the module facade and exported service/channel tokens.
8
+
7
9
  ## Table of Contents
8
10
 
9
11
  - [Installation](#installation)
@@ -92,12 +94,17 @@ DiscordModule.forRootAsync({
92
94
  });
93
95
  ```
94
96
 
97
+ `forRootAsync(...)` accepts the fluo async shape only: register dependencies elsewhere in the application graph, list their tokens in `inject`, and return final `DiscordModuleOptions` from `useFactory`. It does not consume NestJS `imports`, `useClass`, or `useExisting` variants, so migrate those patterns to application-owned providers before passing resolved options to Discord.
98
+
95
99
  Behavioral contract notes:
96
100
 
101
+ - `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.
97
102
  - `DiscordService.send(...)` resolves `defaultThreadId` before delivery.
98
- - The service initializes the configured transport during module bootstrap and closes factory-owned resources during application shutdown.
103
+ - `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.
104
+ - The service initializes the configured transport during module bootstrap and closes factory-owned resources during application shutdown, including any in-flight factory-created transport before shutdown began.
99
105
  - 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.
100
106
  - Sends attempted while the service is shutting down or already stopped are rejected before reusing the cached transport.
107
+ - `DiscordService.createPlatformStatusSnapshot()` exposes the same status contract as `createDiscordPlatformStatusSnapshot(...)`: lifecycle/readiness, health, transport kind and ownership, default thread configuration, bootstrap verification state, and notifications channel dependency details, so callers can observe Discord wiring without reaching into internal options.
101
108
  - Blank `defaultThreadId` and `notifications.channel` values are trimmed and ignored; the notifications channel defaults to `discord`.
102
109
  - The package never reads `process.env` directly. All configuration must enter through explicit options or DI.
103
110
 
@@ -144,7 +151,7 @@ Behavioral contract notes:
144
151
  - One notification dispatch maps to exactly one Discord thread route. Use `payload.threadId` or a single entry in `recipients`.
145
152
  - If `payload.threadId` is omitted, `DiscordService.sendNotification(...)` uses the first `recipients` entry or falls back to `defaultThreadId`.
146
153
  - Notification metadata is merged from payload metadata, dispatch metadata, and template/subject markers. `template` is rendered only when a renderer is configured.
147
- - If a notification needs fan-out across multiple Discord threads, call `sendMany(...)` instead of one multi-recipient dispatch.
154
+ - If a notification workflow needs fan-out across multiple Discord threads, create one concrete Discord message per thread with `DiscordService.sendMany(...)` or issue separate notification dispatches; a single notification dispatch never expands multi-recipient fan-out implicitly.
148
155
 
149
156
  ### Webhook-first delivery with explicit fetch injection
150
157
 
@@ -177,6 +184,7 @@ The Discord package intentionally does **not**:
177
184
  - read credentials or webhook URLs from `process.env`
178
185
  - ship a Node-only Discord SDK inside the shared root package boundary
179
186
  - force one provider strategy beyond the webhook-first helper and exported transport contract
187
+ - expose internal provider helpers, normalized option tokens, or NestJS-style custom provider replacement seams for application imports
180
188
  - translate one notification into multi-thread fan-out inside a single dispatch call
181
189
 
182
190
  These limitations are part of the package contract so runtime choice, provider capability, and rollout strategy stay explicit at the application boundary.
@@ -190,12 +198,18 @@ These limitations are part of the package contract so runtime choice, provider c
190
198
  - `DiscordModuleOptions`
191
199
  - `DiscordAsyncModuleOptions`
192
200
  - `DiscordService`
201
+ - `DiscordService.send(message, options)`
202
+ - `DiscordService.sendMany(messages, options)`
203
+ - `DiscordService.sendNotification(notification, options)`
204
+ - `DiscordService.createPlatformStatusSnapshot()`
193
205
  - `DiscordChannel`
194
206
  - `DISCORD`
195
207
  - `DISCORD_CHANNEL`
196
208
 
197
209
  Compose applications through `DiscordModule` and integrate notifications through `DISCORD_CHANNEL` plus the exported transport contracts.
198
210
 
211
+ The package intentionally keeps `createDiscordProviders(...)`, `DISCORD_OPTIONS`, and `NormalizedDiscordModuleOptions` out of the public root barrel. If a migration previously customized NestJS internals or provider tokens, wrap `DiscordModule.forRoot(...)` / `forRootAsync(...)` in an app-owned module instead of importing private helpers.
212
+
199
213
  ### Contracts and helpers
200
214
 
201
215
  - `DiscordMessage`
@@ -226,6 +240,7 @@ Compose applications through `DiscordModule` and integrate notifications through
226
240
 
227
241
  ### Status and errors
228
242
 
243
+ - `DiscordService.createPlatformStatusSnapshot()`
229
244
  - `createDiscordPlatformStatusSnapshot(...)`
230
245
  - `DiscordLifecycleState`
231
246
  - `DiscordPlatformStatusSnapshot`
package/dist/service.d.ts CHANGED
@@ -12,9 +12,11 @@ export declare class DiscordService implements Discord, OnModuleInit, OnApplicat
12
12
  private readonly options;
13
13
  private lifecycleState;
14
14
  private resolvedTransport;
15
+ private shutdownPromise;
15
16
  private transportPromise;
16
17
  constructor(options: NormalizedDiscordModuleOptions);
17
18
  onApplicationShutdown(): Promise<void>;
19
+ private closeOwnedTransport;
18
20
  onModuleInit(): Promise<void>;
19
21
  /**
20
22
  * Creates a platform status snapshot for the active Discord transport wiring.
@@ -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;AAyCpB;;;;;;;GAOG;AACH,qBACa,cAAe,YAAW,OAAO,EAAE,YAAY,EAAE,qBAAqB;IAKrE,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,OAAO,CAAC,cAAc,CAA2C;IACjE,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,gBAAgB,CAAwC;gBAEnC,OAAO,EAAE,8BAA8B;IAE9D,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAetC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBnC;;;;OAIG;IACH,4BAA4B;IAW5B;;;;;;;;;;;;;;OAcG;IACG,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAyBjG;;;;;;;;;;;;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;YAiCf,eAAe;IAe7B,OAAO,CAAC,kBAAkB;IAU1B,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,2BAA2B;YAkBrB,kBAAkB;CAejC"}
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+CpB;;;;;;;GAOG;AACH,qBACa,cAAe,YAAW,OAAO,EAAE,YAAY,EAAE,qBAAqB;IAMrE,OAAO,CAAC,QAAQ,CAAC,OAAO;IALpC,OAAO,CAAC,cAAc,CAA2C;IACjE,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;YAgB9B,mBAAmB;IAe3B,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAiCnC;;;;OAIG;IACH,4BAA4B;IAW5B;;;;;;;;;;;;;;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;YAiCf,eAAe;IAuB7B,OAAO,CAAC,kBAAkB;IAU1B,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,2BAA2B;YAkBrB,kBAAkB;CAejC"}
package/dist/service.js CHANGED
@@ -22,6 +22,9 @@ function createLifecycleReadinessError(lifecycleState) {
22
22
  }
23
23
  return new DiscordTransportError('Discord transport is not ready for delivery.');
24
24
  }
25
+ function isShutdownLifecycleState(state) {
26
+ return state === 'stopping' || state === 'stopped';
27
+ }
25
28
  function normalizeOptionalString(value) {
26
29
  const trimmed = value?.trim();
27
30
  return trimmed && trimmed.length > 0 ? trimmed : undefined;
@@ -46,15 +49,27 @@ class DiscordService {
46
49
  }
47
50
  lifecycleState = 'created';
48
51
  resolvedTransport;
52
+ shutdownPromise;
49
53
  transportPromise;
50
54
  constructor(options) {
51
55
  this.options = options;
52
56
  }
53
57
  async onApplicationShutdown() {
58
+ if (this.lifecycleState === 'stopped') {
59
+ return;
60
+ }
61
+ if (this.shutdownPromise) {
62
+ return this.shutdownPromise;
63
+ }
54
64
  this.lifecycleState = 'stopping';
65
+ this.shutdownPromise = this.closeOwnedTransport();
66
+ return this.shutdownPromise;
67
+ }
68
+ async closeOwnedTransport() {
55
69
  try {
56
- if (this.resolvedTransport && this.options.transport.ownsResources && this.resolvedTransport.close) {
57
- await this.resolvedTransport.close();
70
+ const transport = this.resolvedTransport ?? (this.transportPromise ? await this.transportPromise : undefined);
71
+ if (transport && this.options.transport.ownsResources && transport.close) {
72
+ await transport.close();
58
73
  }
59
74
  this.lifecycleState = 'stopped';
60
75
  } catch (error) {
@@ -65,14 +80,26 @@ class DiscordService {
65
80
  }
66
81
  }
67
82
  async onModuleInit() {
83
+ if (isShutdownLifecycleState(this.lifecycleState)) {
84
+ return;
85
+ }
68
86
  this.lifecycleState = 'starting';
69
87
  try {
70
88
  const transport = await this.ensureTransport();
89
+ if (this.lifecycleState !== 'starting') {
90
+ return;
91
+ }
71
92
  if (this.options.verifyOnModuleInit && transport.verify) {
72
93
  await transport.verify();
73
94
  }
95
+ if (this.lifecycleState !== 'starting') {
96
+ return;
97
+ }
74
98
  this.lifecycleState = 'ready';
75
99
  } catch (error) {
100
+ if (isShutdownLifecycleState(this.lifecycleState)) {
101
+ throw error;
102
+ }
76
103
  this.lifecycleState = 'failed';
77
104
  throw new Error('Discord transport failed to initialize.', {
78
105
  cause: error
@@ -117,6 +144,7 @@ class DiscordService {
117
144
  }
118
145
  this.assertReadyForSend();
119
146
  const transport = await this.ensureTransport();
147
+ this.assertReadyForSend();
120
148
  const normalized = this.normalizeMessage(message);
121
149
  assertMessageContent(normalized);
122
150
  const result = await transport.send(normalized, options);
@@ -220,6 +248,12 @@ class DiscordService {
220
248
  }, options);
221
249
  }
222
250
  async ensureTransport() {
251
+ if (this.lifecycleState === 'stopping' || this.lifecycleState === 'stopped') {
252
+ throw createStoppedTransportError();
253
+ }
254
+ if (this.lifecycleState === 'failed') {
255
+ throw createLifecycleReadinessError(this.lifecycleState);
256
+ }
223
257
  if (this.resolvedTransport) {
224
258
  return this.resolvedTransport;
225
259
  }
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "portable",
10
10
  "fetch"
11
11
  ],
12
- "version": "1.0.2",
12
+ "version": "1.0.4",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -36,10 +36,10 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
- "@fluojs/core": "^1.0.2",
40
- "@fluojs/di": "^1.0.2",
41
- "@fluojs/notifications": "^1.0.0",
42
- "@fluojs/runtime": "^1.1.0"
39
+ "@fluojs/core": "^1.0.3",
40
+ "@fluojs/di": "^1.1.0",
41
+ "@fluojs/notifications": "^1.0.2",
42
+ "@fluojs/runtime": "^1.1.8"
43
43
  },
44
44
  "devDependencies": {
45
45
  "vitest": "^3.2.4"