nmtjs 0.16.1 → 0.17.0-beta.2

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.
Files changed (224) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +41 -1
  3. package/dist/index.d.ts +22 -161
  4. package/dist/index.js +24 -53
  5. package/dist/index.js.map +1 -1
  6. package/package.json +11 -63
  7. package/src/index.ts +71 -91
  8. package/bin/cli.js +0 -2
  9. package/dist/cli.d.ts +0 -1
  10. package/dist/cli.js +0 -137
  11. package/dist/cli.js.map +0 -1
  12. package/dist/codegen.d.ts +0 -5
  13. package/dist/codegen.js +0 -18
  14. package/dist/codegen.js.map +0 -1
  15. package/dist/config.d.ts +0 -47
  16. package/dist/config.js +0 -16
  17. package/dist/config.js.map +0 -1
  18. package/dist/entrypoints/cli.d.ts +0 -1
  19. package/dist/entrypoints/cli.js +0 -12
  20. package/dist/entrypoints/cli.js.map +0 -1
  21. package/dist/entrypoints/main.d.ts +0 -5
  22. package/dist/entrypoints/main.js +0 -248
  23. package/dist/entrypoints/main.js.map +0 -1
  24. package/dist/entrypoints/thread.d.ts +0 -16
  25. package/dist/entrypoints/thread.js +0 -182
  26. package/dist/entrypoints/thread.js.map +0 -1
  27. package/dist/entrypoints/worker.d.ts +0 -9
  28. package/dist/entrypoints/worker.js +0 -143
  29. package/dist/entrypoints/worker.js.map +0 -1
  30. package/dist/resolver.d.ts +0 -2
  31. package/dist/resolver.js +0 -7
  32. package/dist/resolver.js.map +0 -1
  33. package/dist/runtime/application/index.d.ts +0 -1
  34. package/dist/runtime/application/index.js +0 -2
  35. package/dist/runtime/application/index.js.map +0 -1
  36. package/dist/runtime/constants.d.ts +0 -8
  37. package/dist/runtime/constants.js +0 -5
  38. package/dist/runtime/constants.js.map +0 -1
  39. package/dist/runtime/enums.d.ts +0 -10
  40. package/dist/runtime/enums.js +0 -13
  41. package/dist/runtime/enums.js.map +0 -1
  42. package/dist/runtime/hooks.d.ts +0 -1
  43. package/dist/runtime/hooks.js +0 -2
  44. package/dist/runtime/hooks.js.map +0 -1
  45. package/dist/runtime/index.d.ts +0 -28
  46. package/dist/runtime/index.js +0 -29
  47. package/dist/runtime/index.js.map +0 -1
  48. package/dist/runtime/injectables.d.ts +0 -69
  49. package/dist/runtime/injectables.js +0 -40
  50. package/dist/runtime/injectables.js.map +0 -1
  51. package/dist/runtime/jobs/job.d.ts +0 -146
  52. package/dist/runtime/jobs/job.js +0 -89
  53. package/dist/runtime/jobs/job.js.map +0 -1
  54. package/dist/runtime/jobs/manager.d.ts +0 -131
  55. package/dist/runtime/jobs/manager.js +0 -309
  56. package/dist/runtime/jobs/manager.js.map +0 -1
  57. package/dist/runtime/jobs/router.d.ts +0 -296
  58. package/dist/runtime/jobs/router.js +0 -458
  59. package/dist/runtime/jobs/router.js.map +0 -1
  60. package/dist/runtime/jobs/runner.d.ts +0 -103
  61. package/dist/runtime/jobs/runner.js +0 -425
  62. package/dist/runtime/jobs/runner.js.map +0 -1
  63. package/dist/runtime/jobs/step.d.ts +0 -26
  64. package/dist/runtime/jobs/step.js +0 -18
  65. package/dist/runtime/jobs/step.js.map +0 -1
  66. package/dist/runtime/jobs/types.d.ts +0 -86
  67. package/dist/runtime/jobs/types.js +0 -5
  68. package/dist/runtime/jobs/types.js.map +0 -1
  69. package/dist/runtime/metrics/metric.d.ts +0 -8
  70. package/dist/runtime/metrics/metric.js +0 -11
  71. package/dist/runtime/metrics/metric.js.map +0 -1
  72. package/dist/runtime/metrics/registry.d.ts +0 -3
  73. package/dist/runtime/metrics/registry.js +0 -5
  74. package/dist/runtime/metrics/registry.js.map +0 -1
  75. package/dist/runtime/metrics/server.d.ts +0 -7
  76. package/dist/runtime/metrics/server.js +0 -53
  77. package/dist/runtime/metrics/server.js.map +0 -1
  78. package/dist/runtime/plugin.d.ts +0 -2
  79. package/dist/runtime/plugin.js +0 -2
  80. package/dist/runtime/plugin.js.map +0 -1
  81. package/dist/runtime/runtime.d.ts +0 -27
  82. package/dist/runtime/runtime.js +0 -77
  83. package/dist/runtime/runtime.js.map +0 -1
  84. package/dist/runtime/scheduler/index.d.ts +0 -22
  85. package/dist/runtime/scheduler/index.js +0 -20
  86. package/dist/runtime/scheduler/index.js.map +0 -1
  87. package/dist/runtime/server/applications.d.ts +0 -91
  88. package/dist/runtime/server/applications.js +0 -171
  89. package/dist/runtime/server/applications.js.map +0 -1
  90. package/dist/runtime/server/config.d.ts +0 -148
  91. package/dist/runtime/server/config.js +0 -33
  92. package/dist/runtime/server/config.js.map +0 -1
  93. package/dist/runtime/server/environment.d.ts +0 -43
  94. package/dist/runtime/server/environment.js +0 -33
  95. package/dist/runtime/server/environment.js.map +0 -1
  96. package/dist/runtime/server/error-policy.d.ts +0 -62
  97. package/dist/runtime/server/error-policy.js +0 -73
  98. package/dist/runtime/server/error-policy.js.map +0 -1
  99. package/dist/runtime/server/hmr-coordinator.d.ts +0 -37
  100. package/dist/runtime/server/hmr-coordinator.js +0 -94
  101. package/dist/runtime/server/hmr-coordinator.js.map +0 -1
  102. package/dist/runtime/server/jobs.d.ts +0 -54
  103. package/dist/runtime/server/jobs.js +0 -176
  104. package/dist/runtime/server/jobs.js.map +0 -1
  105. package/dist/runtime/server/lifecycle.d.ts +0 -85
  106. package/dist/runtime/server/lifecycle.js +0 -177
  107. package/dist/runtime/server/lifecycle.js.map +0 -1
  108. package/dist/runtime/server/managed-worker.d.ts +0 -158
  109. package/dist/runtime/server/managed-worker.js +0 -420
  110. package/dist/runtime/server/managed-worker.js.map +0 -1
  111. package/dist/runtime/server/pool-manager.d.ts +0 -85
  112. package/dist/runtime/server/pool-manager.js +0 -129
  113. package/dist/runtime/server/pool-manager.js.map +0 -1
  114. package/dist/runtime/server/proxy.d.ts +0 -21
  115. package/dist/runtime/server/proxy.js +0 -76
  116. package/dist/runtime/server/proxy.js.map +0 -1
  117. package/dist/runtime/server/server.d.ts +0 -60
  118. package/dist/runtime/server/server.js +0 -163
  119. package/dist/runtime/server/server.js.map +0 -1
  120. package/dist/runtime/server/types.d.ts +0 -31
  121. package/dist/runtime/server/types.js +0 -2
  122. package/dist/runtime/server/types.js.map +0 -1
  123. package/dist/runtime/server/worker-pool.d.ts +0 -121
  124. package/dist/runtime/server/worker-pool.js +0 -171
  125. package/dist/runtime/server/worker-pool.js.map +0 -1
  126. package/dist/runtime/store/index.d.ts +0 -3
  127. package/dist/runtime/store/index.js +0 -23
  128. package/dist/runtime/store/index.js.map +0 -1
  129. package/dist/runtime/subscription/manager.d.ts +0 -49
  130. package/dist/runtime/subscription/manager.js +0 -170
  131. package/dist/runtime/subscription/manager.js.map +0 -1
  132. package/dist/runtime/subscription/redis.d.ts +0 -18
  133. package/dist/runtime/subscription/redis.js +0 -119
  134. package/dist/runtime/subscription/redis.js.map +0 -1
  135. package/dist/runtime/types.d.ts +0 -95
  136. package/dist/runtime/types.js +0 -2
  137. package/dist/runtime/types.js.map +0 -1
  138. package/dist/runtime/workers/application.d.ts +0 -29
  139. package/dist/runtime/workers/application.js +0 -77
  140. package/dist/runtime/workers/application.js.map +0 -1
  141. package/dist/runtime/workers/base.d.ts +0 -14
  142. package/dist/runtime/workers/base.js +0 -41
  143. package/dist/runtime/workers/base.js.map +0 -1
  144. package/dist/runtime/workers/cli.d.ts +0 -1
  145. package/dist/runtime/workers/cli.js +0 -2
  146. package/dist/runtime/workers/cli.js.map +0 -1
  147. package/dist/runtime/workers/job.d.ts +0 -20
  148. package/dist/runtime/workers/job.js +0 -170
  149. package/dist/runtime/workers/job.js.map +0 -1
  150. package/dist/vite/builder.d.ts +0 -5
  151. package/dist/vite/builder.js +0 -79
  152. package/dist/vite/builder.js.map +0 -1
  153. package/dist/vite/config.d.ts +0 -28
  154. package/dist/vite/config.js +0 -22
  155. package/dist/vite/config.js.map +0 -1
  156. package/dist/vite/plugins.d.ts +0 -2
  157. package/dist/vite/plugins.js +0 -43
  158. package/dist/vite/plugins.js.map +0 -1
  159. package/dist/vite/runners/worker.d.ts +0 -3
  160. package/dist/vite/runners/worker.js +0 -34
  161. package/dist/vite/runners/worker.js.map +0 -1
  162. package/dist/vite/server.d.ts +0 -3
  163. package/dist/vite/server.js +0 -36
  164. package/dist/vite/server.js.map +0 -1
  165. package/dist/vite/servers/main.d.ts +0 -8
  166. package/dist/vite/servers/main.js +0 -24
  167. package/dist/vite/servers/main.js.map +0 -1
  168. package/dist/vite/servers/worker.d.ts +0 -13
  169. package/dist/vite/servers/worker.js +0 -124
  170. package/dist/vite/servers/worker.js.map +0 -1
  171. package/src/cli.ts +0 -162
  172. package/src/codegen.ts +0 -30
  173. package/src/config.ts +0 -69
  174. package/src/entrypoints/cli.ts +0 -13
  175. package/src/entrypoints/main.ts +0 -317
  176. package/src/entrypoints/thread.ts +0 -223
  177. package/src/entrypoints/worker.ts +0 -174
  178. package/src/resolver.ts +0 -7
  179. package/src/runtime/application/index.ts +0 -1
  180. package/src/runtime/constants.ts +0 -13
  181. package/src/runtime/enums.ts +0 -12
  182. package/src/runtime/hooks.ts +0 -1
  183. package/src/runtime/index.ts +0 -29
  184. package/src/runtime/injectables.ts +0 -101
  185. package/src/runtime/jobs/job.ts +0 -467
  186. package/src/runtime/jobs/manager.ts +0 -454
  187. package/src/runtime/jobs/router.ts +0 -967
  188. package/src/runtime/jobs/runner.ts +0 -583
  189. package/src/runtime/jobs/step.ts +0 -66
  190. package/src/runtime/jobs/types.ts +0 -116
  191. package/src/runtime/metrics/metric.ts +0 -18
  192. package/src/runtime/metrics/registry.ts +0 -11
  193. package/src/runtime/metrics/server.ts +0 -69
  194. package/src/runtime/plugin.ts +0 -2
  195. package/src/runtime/runtime.ts +0 -103
  196. package/src/runtime/scheduler/index.ts +0 -39
  197. package/src/runtime/server/applications.ts +0 -264
  198. package/src/runtime/server/config.ts +0 -199
  199. package/src/runtime/server/environment.ts +0 -71
  200. package/src/runtime/server/error-policy.ts +0 -132
  201. package/src/runtime/server/hmr-coordinator.ts +0 -116
  202. package/src/runtime/server/jobs.ts +0 -266
  203. package/src/runtime/server/lifecycle.ts +0 -234
  204. package/src/runtime/server/managed-worker.ts +0 -546
  205. package/src/runtime/server/pool-manager.ts +0 -187
  206. package/src/runtime/server/proxy.ts +0 -119
  207. package/src/runtime/server/server.ts +0 -218
  208. package/src/runtime/server/types.ts +0 -32
  209. package/src/runtime/server/worker-pool.ts +0 -275
  210. package/src/runtime/store/index.ts +0 -30
  211. package/src/runtime/subscription/manager.ts +0 -307
  212. package/src/runtime/subscription/redis.ts +0 -159
  213. package/src/runtime/types.ts +0 -84
  214. package/src/runtime/workers/application.ts +0 -133
  215. package/src/runtime/workers/base.ts +0 -65
  216. package/src/runtime/workers/cli.ts +0 -0
  217. package/src/runtime/workers/job.ts +0 -142
  218. package/src/vite/builder.ts +0 -94
  219. package/src/vite/config.ts +0 -45
  220. package/src/vite/plugins.ts +0 -53
  221. package/src/vite/runners/worker.ts +0 -55
  222. package/src/vite/server.ts +0 -55
  223. package/src/vite/servers/main.ts +0 -41
  224. package/src/vite/servers/worker.ts +0 -177
@@ -1,275 +0,0 @@
1
- import EventEmitter from 'node:events'
2
-
3
- import type { Logger } from '@nmtjs/core'
4
-
5
- import type { WorkerType } from '../enums.ts'
6
- import type { JobTaskResult, WorkerJobTask } from '../types.ts'
7
- import type { ErrorPolicy } from './error-policy.ts'
8
- import type {
9
- ManagedWorker,
10
- ManagedWorkerConfig,
11
- WorkerState,
12
- } from './managed-worker.ts'
13
- import type { WorkerRegistration } from './types.ts'
14
-
15
- /**
16
- * Health states for a worker pool.
17
- */
18
- export type PoolHealth = 'healthy' | 'degraded' | 'unhealthy'
19
-
20
- /**
21
- * Report on pool health status.
22
- */
23
- export interface PoolHealthReport {
24
- poolName: string
25
- overall: PoolHealth
26
- healthy: number
27
- total: number
28
- workers: Array<{
29
- id: string
30
- state: WorkerState
31
- consecutiveFailures: number
32
- totalFailures: number
33
- }>
34
- }
35
-
36
- /**
37
- * Configuration for creating a worker pool.
38
- */
39
- export interface WorkerPoolConfig {
40
- name: string
41
- workerType: WorkerType
42
- path: string
43
- workerData?: any
44
- onWorker?: (registration: WorkerRegistration) => void
45
- }
46
-
47
- /**
48
- * Events emitted by WorkerPool.
49
- */
50
- export interface WorkerPoolEvents {
51
- 'health-change': [health: PoolHealth]
52
- }
53
-
54
- /**
55
- * Factory function for creating ManagedWorker instances.
56
- * This allows for dependency injection and testing.
57
- */
58
- export type ManagedWorkerFactory = (
59
- config: ManagedWorkerConfig,
60
- policy: ErrorPolicy,
61
- logger: Logger,
62
- ) => ManagedWorker
63
-
64
- /**
65
- * Factory function for creating WorkerPool instances.
66
- * This allows for dependency injection and testing.
67
- */
68
- export type WorkerPoolFactory = (
69
- config: WorkerPoolConfig,
70
- policy: ErrorPolicy,
71
- workerFactory: ManagedWorkerFactory,
72
- logger: Logger,
73
- ) => WorkerPool
74
-
75
- /**
76
- * WorkerPool manages a collection of workers for a single application or job pool.
77
- *
78
- * Features:
79
- * - Health tracking with three states: healthy, degraded, unhealthy
80
- * - Integration with ErrorPolicy for degraded mode decisions
81
- * - Worker lifecycle management (start, stop)
82
- * - Health reports for debugging and monitoring
83
- *
84
- * Each application gets its own WorkerPool for isolated failure domains.
85
- */
86
- export class WorkerPool extends EventEmitter<WorkerPoolEvents> {
87
- private workers: ManagedWorker[] = []
88
- private health: PoolHealth = 'healthy'
89
- private logger: Logger
90
-
91
- constructor(
92
- readonly config: WorkerPoolConfig,
93
- private readonly policy: ErrorPolicy,
94
- private readonly workerFactory: ManagedWorkerFactory,
95
- logger: Logger,
96
- ) {
97
- super()
98
- this.logger = logger.child({ component: 'WorkerPool', pool: config.name })
99
- }
100
-
101
- /** Current pool health */
102
- get currentHealth(): PoolHealth {
103
- return this.health
104
- }
105
-
106
- /** Total number of workers in the pool */
107
- get workerCount(): number {
108
- return this.workers.length
109
- }
110
-
111
- /** Number of healthy (ready) workers */
112
- get healthyCount(): number {
113
- return this.workers.filter((w) => w.isHealthy).length
114
- }
115
-
116
- /** Get all workers in this pool */
117
- getWorkers(): readonly ManagedWorker[] {
118
- return this.workers
119
- }
120
-
121
- /**
122
- * Add a worker to the pool.
123
- *
124
- * @param workerData - Additional data to pass to the worker
125
- * @param index - Worker index (used for naming)
126
- * @returns The created ManagedWorker
127
- */
128
- add(workerData: any, index: number): ManagedWorker {
129
- const { config } = this
130
-
131
- const workerConfig: ManagedWorkerConfig = {
132
- id: `${config.name}-${index + 1}`,
133
- name: config.name,
134
- index,
135
- workerType: config.workerType,
136
- path: config.path,
137
- workerData: { ...config.workerData, ...workerData },
138
- onWorker: config.onWorker,
139
- }
140
-
141
- const worker = this.workerFactory(workerConfig, this.policy, this.logger)
142
-
143
- worker.on('state-change', () => this.updateHealth())
144
-
145
- this.workers.push(worker)
146
- return worker
147
- }
148
-
149
- /**
150
- * Start all workers in the pool.
151
- */
152
- async startAll(): Promise<void> {
153
- this.logger.debug({ count: this.workers.length }, 'Starting all workers')
154
- await Promise.all(this.workers.map((w) => w.start()))
155
- this.logger.debug({ count: this.workers.length }, 'All workers started')
156
- }
157
-
158
- /**
159
- * Restart workers that are in error state.
160
- * Called when HMR update comes in and there are failed workers.
161
- * @returns Number of workers that were restarted
162
- */
163
- async restartFailedWorkers(): Promise<number> {
164
- const failedWorkers = this.workers.filter((w) => w.currentState === 'error')
165
- if (failedWorkers.length === 0) return 0
166
-
167
- this.logger.debug(
168
- { count: failedWorkers.length },
169
- 'Restarting failed workers after HMR update',
170
- )
171
-
172
- // Reset failure counts before restarting
173
- for (const worker of failedWorkers) {
174
- worker.resetFailureCount()
175
- }
176
-
177
- await Promise.all(failedWorkers.map((w) => w.start()))
178
- return failedWorkers.length
179
- }
180
-
181
- /**
182
- * Stop all workers in the pool and clean up.
183
- */
184
- async stopAll(): Promise<void> {
185
- this.logger.debug({ count: this.workers.length }, 'Stopping all workers')
186
-
187
- // Remove all event listeners from workers before stopping
188
- for (const worker of this.workers) {
189
- worker.removeAllListeners()
190
- }
191
-
192
- await Promise.all(this.workers.map((w) => w.stop()))
193
-
194
- // Clear workers array
195
- this.workers = []
196
-
197
- // Remove all listeners from the pool itself
198
- this.removeAllListeners()
199
-
200
- this.logger.debug('All workers stopped and cleaned up')
201
- }
202
-
203
- /**
204
- * Get a health report for this pool.
205
- */
206
- getHealthReport(): PoolHealthReport {
207
- return {
208
- poolName: this.config.name,
209
- overall: this.health,
210
- healthy: this.healthyCount,
211
- total: this.workerCount,
212
- workers: this.workers.map((w) => ({
213
- id: w.config.id,
214
- state: w.currentState,
215
- consecutiveFailures: w.context.consecutiveFailures,
216
- totalFailures: w.context.totalFailures,
217
- })),
218
- }
219
- }
220
-
221
- /**
222
- * Update pool health based on worker states.
223
- */
224
- private updateHealth(): void {
225
- const readyCount = this.healthyCount
226
- const total = this.workerCount
227
-
228
- let newHealth: PoolHealth
229
- if (readyCount === total) {
230
- newHealth = 'healthy'
231
- } else if (readyCount > 0 && this.policy.allowDegradedMode) {
232
- newHealth = 'degraded'
233
- } else {
234
- newHealth = 'unhealthy'
235
- }
236
-
237
- if (newHealth !== this.health) {
238
- const oldHealth = this.health
239
- this.health = newHealth
240
- this.logger.trace(
241
- { from: oldHealth, to: newHealth, ready: readyCount, total },
242
- 'Pool health changed',
243
- )
244
- this.emit('health-change', newHealth)
245
- }
246
- }
247
- }
248
-
249
- /**
250
- * JobRunnersPool extends WorkerPool with round-robin task execution.
251
- * Used for job worker pools where tasks need to be distributed across workers.
252
- */
253
- export class JobRunnersPool extends WorkerPool {
254
- private runIndex = 0
255
-
256
- /**
257
- * Run a task on the next available worker (round-robin).
258
- */
259
- async run(task: WorkerJobTask): Promise<JobTaskResult> {
260
- const workers = this.getWorkers()
261
- const healthyWorkers = workers.filter((w) => w.isHealthy)
262
-
263
- if (healthyWorkers.length === 0) {
264
- throw new Error('No healthy job runner threads available')
265
- }
266
-
267
- if (this.runIndex >= healthyWorkers.length) {
268
- this.runIndex = 0
269
- }
270
-
271
- const worker = healthyWorkers[this.runIndex]!
272
- this.runIndex++
273
- return await worker.run(task)
274
- }
275
- }
@@ -1,30 +0,0 @@
1
- import type { ServerStoreConfig } from '../server/config.ts'
2
- import type { StoreTypes } from '../types.ts'
3
- import { StoreType } from '../enums.ts'
4
-
5
- export async function createStoreClient<T extends ServerStoreConfig>(
6
- config: T,
7
- ): Promise<StoreTypes[T['type']]> {
8
- if (config.type === StoreType.Redis) {
9
- const { Redis } = await import('ioredis').catch(() => {
10
- throw new Error(
11
- 'ioredis package is not installed. Please install it to use Redis store.',
12
- )
13
- })
14
- return new Redis({
15
- ...config.options,
16
- lazyConnect: true,
17
- }) as StoreTypes[T['type']]
18
- } else if (config.type === StoreType.Valkey) {
19
- const { Redis } = await import('iovalkey').catch(() => {
20
- throw new Error(
21
- 'iovalkey package is not installed. Please install it to use Valkey store.',
22
- )
23
- })
24
- return new Redis({
25
- ...config.options,
26
- lazyConnect: true,
27
- }) as StoreTypes[T['type']]
28
- }
29
- throw new Error('Unsupported store')
30
- }
@@ -1,307 +0,0 @@
1
- import assert from 'node:assert'
2
- import { createHash } from 'node:crypto'
3
- import { PassThrough, Readable } from 'node:stream'
4
-
5
- import type {
6
- SubcriptionOptions,
7
- TAnyEventContract,
8
- TAnySubscriptionContract,
9
- } from '@nmtjs/contract'
10
- import type { Logger } from '@nmtjs/core'
11
- import type { t } from '@nmtjs/type'
12
- import { isAbortError } from '@nmtjs/common'
13
-
14
- export type SubscriptionAdapterEvent = { channel: string; payload: any }
15
-
16
- export interface SubscriptionAdapterType {
17
- publish(channel: string, payload: any): Promise<boolean>
18
- subscribe(
19
- channel: string,
20
- signal?: AbortSignal,
21
- ): AsyncGenerator<SubscriptionAdapterEvent>
22
- initialize(): Promise<void>
23
- dispose(): Promise<void>
24
- }
25
-
26
- export type SubscriptionChannel = {
27
- stream: Readable
28
- subscription: TAnySubscriptionContract
29
- event: TAnyEventContract
30
- }
31
-
32
- export type SubscribeFn = <
33
- Contract extends TAnySubscriptionContract,
34
- Events extends {
35
- [K in keyof Contract['events']]?: true
36
- },
37
- >(
38
- subscription: Contract,
39
- events: Events,
40
- options: Contract['options'],
41
- signal?: AbortSignal,
42
- ) => Promise<
43
- Omit<Readable, typeof Symbol.asyncIterator> & {
44
- [Symbol.asyncIterator]: () => AsyncIterator<
45
- {} extends Events
46
- ? {
47
- [K in keyof Contract['events']]: {
48
- event: K
49
- data: t.infer.decode.output<Contract['events'][K]['payload']>
50
- }
51
- }[keyof Contract['events']]
52
- : {
53
- [K in keyof Events]: K extends keyof Contract['events']
54
- ? {
55
- event: K
56
- data: t.infer.decode.output<Contract['events'][K]['payload']>
57
- }
58
- : never
59
- }[keyof Events]
60
- >
61
- }
62
- >
63
-
64
- export type PublishFn = <
65
- S extends TAnySubscriptionContract,
66
- E extends S['events'][keyof S['events']],
67
- >(
68
- event: E,
69
- options: S['options'],
70
- data: t.infer.decode.input<E['payload']>,
71
- ) => Promise<boolean>
72
-
73
- export type SubscriptionManagerOptions = {
74
- logger: Logger
75
- adapter: SubscriptionAdapterType
76
- }
77
-
78
- export class SubscriptionManager {
79
- readonly subscriptions = new Map<string, SubscriptionChannel>()
80
- protected readonly logger: Logger
81
-
82
- constructor(protected readonly options: SubscriptionManagerOptions) {
83
- this.logger = options.logger.child({ component: SubscriptionManager.name })
84
- }
85
-
86
- subscribe: SubscribeFn = async (subscription, events, options, signal) => {
87
- const eventKeys =
88
- Object.keys(events).length === 0
89
- ? Object.keys(subscription.events)
90
- : Object.keys(events)
91
-
92
- this.logger.debug(
93
- { subscription: subscription.name, eventCount: eventKeys.length },
94
- 'Opening subscription',
95
- )
96
-
97
- const streams = Array(eventKeys.length)
98
-
99
- for (const index in eventKeys) {
100
- const event = subscription.events[eventKeys[index]]
101
- const channel = getChannelName(event, options)
102
- if (this.subscriptions.has(channel)) {
103
- streams[index] = this.subscriptions.get(channel)!.stream
104
- this.logger.trace(
105
- {
106
- channel,
107
- event: event.name,
108
- activeChannels: this.subscriptions.size,
109
- },
110
- 'Reusing pubsub channel stream',
111
- )
112
- } else {
113
- const iterable = this.options.adapter.subscribe(channel, signal)
114
- const stream = this.createEventStream(iterable)
115
- stream.on('close', () => {
116
- this.subscriptions.delete(channel)
117
- this.logger.debug(
118
- {
119
- channel,
120
- event: event.name,
121
- activeChannels: this.subscriptions.size,
122
- },
123
- 'Pubsub channel stream closed',
124
- )
125
- })
126
- stream.on('error', (error) => {
127
- this.logger.warn(
128
- { channel, event: event.name, error },
129
- 'Pubsub channel stream failed',
130
- )
131
- })
132
- streams[index] = stream
133
- this.subscriptions.set(channel, { subscription, event, stream })
134
- this.logger.debug(
135
- {
136
- channel,
137
- event: event.name,
138
- activeChannels: this.subscriptions.size,
139
- },
140
- 'Creating channel stream',
141
- )
142
- }
143
- }
144
-
145
- const mergedStream = mergeEventStreams(streams, signal)
146
-
147
- mergedStream.once('close', () => {
148
- this.logger.debug(
149
- { subscription: subscription.name, eventCount: eventKeys.length },
150
- 'Pubsub subscription stream closed',
151
- )
152
- })
153
- mergedStream.once('error', (error) => {
154
- this.logger.warn(
155
- {
156
- subscription: subscription.name,
157
- eventCount: eventKeys.length,
158
- error,
159
- },
160
- 'Pubsub subscription stream failed',
161
- )
162
- })
163
-
164
- return mergedStream
165
- }
166
-
167
- publish: PublishFn = async (event, options, data) => {
168
- const channel = getChannelName(event, options)
169
-
170
- this.logger.trace({ channel, event: event.name }, 'Publishing pubsub event')
171
-
172
- try {
173
- const payload = event.payload.encode(data)
174
- const published = await this.options.adapter.publish(channel, payload)
175
-
176
- if (published) {
177
- this.logger.trace(
178
- { channel, event: event.name },
179
- 'Published pubsub event',
180
- )
181
- } else {
182
- this.logger.warn(
183
- { channel, event: event.name },
184
- 'Pubsub adapter reported publish failure',
185
- )
186
- }
187
-
188
- return published
189
- } catch (error: any) {
190
- this.logger.error(
191
- { channel, event: event.name, error },
192
- 'Failed to publish pubsub event',
193
- )
194
- throw error
195
- }
196
- }
197
-
198
- async dispose(): Promise<void> {
199
- for (const { stream } of this.subscriptions.values()) {
200
- stream.destroy()
201
- }
202
- this.subscriptions.clear()
203
- }
204
-
205
- private createEventStream(
206
- iterable: AsyncGenerator<SubscriptionAdapterEvent>,
207
- ): Readable {
208
- const { subscriptions } = this
209
- const logger = this.logger
210
-
211
- return new Readable({
212
- objectMode: true,
213
- read() {
214
- iterable.next().then(
215
- ({ value, done }) => {
216
- if (done) {
217
- logger.trace('Pubsub adapter stream ended')
218
- this.push(null)
219
- } else {
220
- const subscription = subscriptions.get(value.channel)
221
- if (subscription) {
222
- const { event } = subscription
223
- try {
224
- const data = event.payload.decode(value.payload)
225
- logger.trace(
226
- { channel: value.channel, event: event.name },
227
- 'Received event',
228
- )
229
- this.push({ event: event.name, data })
230
- } catch (error: any) {
231
- logger.warn(
232
- { channel: value.channel, event: event.name, error },
233
- 'Failed to decode pubsub event payload',
234
- )
235
- this.destroy(error)
236
- }
237
- } else {
238
- logger.trace(
239
- { channel: value.channel },
240
- 'Dropped pubsub event for inactive channel',
241
- )
242
- }
243
- }
244
- },
245
- (error) => {
246
- if (isAbortError(error)) {
247
- logger.trace('Pubsub adapter stream aborted')
248
- this.push(null)
249
- } else {
250
- logger.warn({ error }, 'Pubsub adapter stream failed')
251
- this.destroy(error)
252
- }
253
- },
254
- )
255
- },
256
- })
257
- }
258
- }
259
-
260
- function concat(...args: any) {
261
- return args.filter(Boolean).join('/')
262
- }
263
-
264
- function getChannelName<T extends TAnyEventContract>(
265
- contract: T,
266
- options: T['options'],
267
- ) {
268
- const key = options ? serializerOptions(options) : ''
269
- assert(contract.name, 'Event contract must have a name')
270
- return concat(contract.name, key)
271
- }
272
-
273
- function serializerOptions(options: Exclude<SubcriptionOptions, null>): string {
274
- const hash = createHash('sha1')
275
- const serialized = Object.entries(options)
276
- .sort((a, b) => a[0].localeCompare(b[0]))
277
- .map(([key, value]) => `${key}=${value}`)
278
- .join(';')
279
- hash.update(serialized)
280
- return hash.digest('base64url')
281
- }
282
-
283
- function mergeEventStreams(
284
- streams: Readable[],
285
- signal?: AbortSignal,
286
- ): Readable {
287
- const destination = new PassThrough({
288
- signal,
289
- objectMode: true,
290
- readableObjectMode: true,
291
- writableObjectMode: true,
292
- })
293
-
294
- let ended = 0
295
-
296
- for (const source of streams) {
297
- source.pipe(destination, { end: false })
298
- source.once('end', () => {
299
- ended++
300
- if (ended === streams.length) {
301
- destination.end()
302
- }
303
- })
304
- }
305
-
306
- return destination
307
- }