@fluojs/discord 1.0.3 → 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,13 +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
103
  - `DiscordService.sendMany(...)`는 `DiscordMessage[]`를 직접 순차 전송하는 batch API이며 `continueOnError`를 지원합니다. 이는 multi-recipient `@fluojs/notifications` dispatch shortcut이 아닙니다.
99
- - 서비스는 모듈 bootstrap 시 transport를 초기화하고, factory가 소유한 리소스만 애플리케이션 shutdown 시 닫습니다.
104
+ - 서비스는 모듈 bootstrap 시 transport를 초기화하고, shutdown 전에 시작된 factory 생성 transport아직 완료되지 않았더라도 이를 기다린 뒤 factory가 소유한 리소스를 애플리케이션 shutdown 시 닫습니다.
100
105
  - send는 bootstrap이 transport를 `ready`로 표시한 뒤에만 허용됩니다. bootstrap 전, startup 중, bootstrap 실패 후, shutdown 중, shutdown 후 시도는 전달 전에 거부됩니다.
101
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을 관찰할 수 있습니다.
102
108
  - 빈 `defaultThreadId`와 `notifications.channel` 값은 trim 후 무시됩니다. notifications channel은 기본적으로 `discord`입니다.
103
109
  - 이 패키지는 절대로 `process.env`를 직접 읽지 않습니다. 모든 설정은 명시적인 옵션 또는 DI를 통해 들어와야 합니다.
104
110
 
@@ -178,6 +184,7 @@ Discord 패키지는 의도적으로 다음을 **포함하지 않습니다**:
178
184
  - 자격 증명이나 webhook URL을 `process.env`에서 직접 읽는 동작
179
185
  - 공유 루트 패키지 경계에 Node 전용 Discord SDK를 내장하는 것
180
186
  - webhook helper와 export된 transport 계약 이상으로 하나의 provider 전략을 강제하는 것
187
+ - 애플리케이션 import용 내부 provider helper, normalized option token, 또는 NestJS-style custom provider replacement seam을 노출하는 것
181
188
  - 하나의 dispatch 호출 안에서 multi-thread fan-out을 자동 변환하는 것
182
189
 
183
190
  이 제한 사항은 런타임 선택, provider capability, rollout 전략이 애플리케이션 경계에서 명시적으로 결정되도록 하기 위한 package contract의 일부입니다.
@@ -191,12 +198,18 @@ Discord 패키지는 의도적으로 다음을 **포함하지 않습니다**:
191
198
  - `DiscordModuleOptions`
192
199
  - `DiscordAsyncModuleOptions`
193
200
  - `DiscordService`
201
+ - `DiscordService.send(message, options)`
202
+ - `DiscordService.sendMany(messages, options)`
203
+ - `DiscordService.sendNotification(notification, options)`
204
+ - `DiscordService.createPlatformStatusSnapshot()`
194
205
  - `DiscordChannel`
195
206
  - `DISCORD`
196
207
  - `DISCORD_CHANNEL`
197
208
 
198
209
  애플리케이션 구성은 `DiscordModule`로, notifications 연동은 `DISCORD_CHANNEL`과 export된 transport 계약으로 조합합니다.
199
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
+
200
213
  ### 계약과 헬퍼
201
214
 
202
215
  - `DiscordMessage`
@@ -227,6 +240,7 @@ Discord 패키지는 의도적으로 다음을 **포함하지 않습니다**:
227
240
 
228
241
  ### 상태 및 에러
229
242
 
243
+ - `DiscordService.createPlatformStatusSnapshot()`
230
244
  - `createDiscordPlatformStatusSnapshot(...)`
231
245
  - `DiscordLifecycleState`
232
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,13 +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
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.
99
- - The service initializes the configured transport during module bootstrap and closes factory-owned resources during application shutdown.
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.
100
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.
101
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.
102
108
  - Blank `defaultThreadId` and `notifications.channel` values are trimmed and ignored; the notifications channel defaults to `discord`.
103
109
  - The package never reads `process.env` directly. All configuration must enter through explicit options or DI.
104
110
 
@@ -178,6 +184,7 @@ The Discord package intentionally does **not**:
178
184
  - read credentials or webhook URLs from `process.env`
179
185
  - ship a Node-only Discord SDK inside the shared root package boundary
180
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
181
188
  - translate one notification into multi-thread fan-out inside a single dispatch call
182
189
 
183
190
  These limitations are part of the package contract so runtime choice, provider capability, and rollout strategy stay explicit at the application boundary.
@@ -191,12 +198,18 @@ These limitations are part of the package contract so runtime choice, provider c
191
198
  - `DiscordModuleOptions`
192
199
  - `DiscordAsyncModuleOptions`
193
200
  - `DiscordService`
201
+ - `DiscordService.send(message, options)`
202
+ - `DiscordService.sendMany(messages, options)`
203
+ - `DiscordService.sendNotification(notification, options)`
204
+ - `DiscordService.createPlatformStatusSnapshot()`
194
205
  - `DiscordChannel`
195
206
  - `DISCORD`
196
207
  - `DISCORD_CHANNEL`
197
208
 
198
209
  Compose applications through `DiscordModule` and integrate notifications through `DISCORD_CHANNEL` plus the exported transport contracts.
199
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
+
200
213
  ### Contracts and helpers
201
214
 
202
215
  - `DiscordMessage`
@@ -227,6 +240,7 @@ Compose applications through `DiscordModule` and integrate notifications through
227
240
 
228
241
  ### Status and errors
229
242
 
243
+ - `DiscordService.createPlatformStatusSnapshot()`
230
244
  - `createDiscordPlatformStatusSnapshot(...)`
231
245
  - `DiscordLifecycleState`
232
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.3",
12
+ "version": "1.0.4",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -37,9 +37,9 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "@fluojs/core": "^1.0.3",
40
- "@fluojs/notifications": "^1.0.1",
41
- "@fluojs/runtime": "^1.1.1",
42
- "@fluojs/di": "^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"