bunsane 0.2.9 → 0.3.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/config/cache.config.ts +12 -2
  3. package/core/App.ts +390 -66
  4. package/core/ApplicationLifecycle.ts +68 -4
  5. package/core/Entity.ts +407 -256
  6. package/core/EntityHookManager.ts +88 -21
  7. package/core/EntityManager.ts +12 -3
  8. package/core/Logger.ts +4 -0
  9. package/core/RequestContext.ts +4 -1
  10. package/core/SchedulerManager.ts +92 -9
  11. package/core/cache/CacheFactory.ts +3 -1
  12. package/core/cache/CacheManager.ts +54 -17
  13. package/core/cache/RedisCache.ts +38 -3
  14. package/core/decorators/EntityHooks.ts +24 -12
  15. package/core/middleware/RateLimit.ts +105 -0
  16. package/core/middleware/index.ts +1 -0
  17. package/core/remote/CircuitBreaker.ts +115 -0
  18. package/core/remote/OutboxWorker.ts +183 -0
  19. package/core/remote/RemoteManager.ts +400 -0
  20. package/core/remote/RpcCaller.ts +310 -0
  21. package/core/remote/StreamConsumer.ts +535 -0
  22. package/core/remote/decorators.ts +121 -0
  23. package/core/remote/health.ts +139 -0
  24. package/core/remote/index.ts +37 -0
  25. package/core/remote/metrics.ts +99 -0
  26. package/core/remote/outboxSchema.ts +41 -0
  27. package/core/remote/types.ts +151 -0
  28. package/core/scheduler/DistributedLock.ts +324 -266
  29. package/gql/builders/ResolverBuilder.ts +4 -4
  30. package/gql/complexityLimit.ts +95 -0
  31. package/gql/index.ts +15 -3
  32. package/gql/visitors/ResolverGeneratorVisitor.ts +16 -2
  33. package/package.json +1 -1
  34. package/query/ComponentInclusionNode.ts +13 -6
  35. package/query/OrNode.ts +2 -4
  36. package/query/Query.ts +30 -3
  37. package/query/SqlIdentifier.ts +105 -0
  38. package/query/builders/FullTextSearchBuilder.ts +19 -6
  39. package/service/ServiceRegistry.ts +21 -8
  40. package/storage/LocalStorageProvider.ts +12 -3
  41. package/storage/S3StorageProvider.ts +6 -6
  42. package/tests/e2e/http.test.ts +6 -2
  43. package/tests/helpers/MockRedisClient.ts +113 -0
  44. package/tests/helpers/MockRedisStreamServer.ts +448 -0
  45. package/tests/integration/entity/Entity.saveTimeout.test.ts +110 -0
  46. package/tests/integration/remote/dlq.test.ts +175 -0
  47. package/tests/integration/remote/event-dispatch.test.ts +114 -0
  48. package/tests/integration/remote/outbox.test.ts +130 -0
  49. package/tests/integration/remote/rpc.test.ts +177 -0
  50. package/tests/unit/remote/CircuitBreaker.test.ts +159 -0
  51. package/tests/unit/remote/RemoteError.test.ts +55 -0
  52. package/tests/unit/remote/decorators.test.ts +195 -0
  53. package/tests/unit/remote/metrics.test.ts +115 -0
  54. package/tests/unit/remote/mockRedisStreamServer.test.ts +104 -0
  55. package/tests/unit/storage/S3StorageProvider.test.ts +6 -10
  56. package/upload/FileValidator.ts +9 -6
@@ -1,266 +1,324 @@
1
- /**
2
- * Distributed Lock using PostgreSQL Advisory Locks
3
- *
4
- * PostgreSQL advisory locks are application-level locks that can be used
5
- * to coordinate between multiple application instances. They are:
6
- * - Session-based: automatically released when connection closes
7
- * - Non-blocking with pg_try_advisory_lock
8
- * - Perfect for distributed task scheduling
9
- *
10
- * @see https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS
11
- */
12
-
13
- import db from "../../database";
14
- import { logger } from "../Logger";
15
-
16
- const loggerInstance = logger.child({ scope: "DistributedLock" });
17
-
18
- /**
19
- * Result of a lock acquisition attempt
20
- */
21
- export interface LockResult {
22
- acquired: boolean;
23
- lockKey: bigint;
24
- taskId: string;
25
- }
26
-
27
- /**
28
- * Configuration for the distributed lock system
29
- */
30
- export interface DistributedLockConfig {
31
- /** Whether distributed locking is enabled */
32
- enabled: boolean;
33
- /** Prefix for lock keys to avoid collisions with other applications */
34
- lockKeyPrefix: number;
35
- /** Whether to log lock acquisition/release events */
36
- enableLogging: boolean;
37
- /** Timeout for lock acquisition attempts in milliseconds (0 = no retry) */
38
- lockTimeout: number;
39
- /** Retry interval when lockTimeout > 0 */
40
- retryInterval: number;
41
- }
42
-
43
- /**
44
- * Default configuration
45
- */
46
- export const DEFAULT_LOCK_CONFIG: DistributedLockConfig = {
47
- enabled: true,
48
- lockKeyPrefix: 0x42554E53, // "BUNS" in hex as a namespace prefix
49
- enableLogging: false,
50
- lockTimeout: 0, // No retry by default - skip if can't acquire
51
- retryInterval: 100,
52
- };
53
-
54
- /**
55
- * Distributed Lock Manager using PostgreSQL Advisory Locks
56
- *
57
- * Provides distributed coordination for scheduled tasks across multiple
58
- * application instances. Uses PostgreSQL's advisory lock system which
59
- * guarantees that only one instance can hold a lock at a time.
60
- *
61
- * Advisory locks are automatically released when:
62
- * - Explicitly unlocked via pg_advisory_unlock
63
- * - The database session ends
64
- * - The connection is closed
65
- */
66
- export class DistributedLock {
67
- private config: DistributedLockConfig;
68
- private heldLocks: Set<string> = new Set();
69
-
70
- constructor(config: Partial<DistributedLockConfig> = {}) {
71
- this.config = { ...DEFAULT_LOCK_CONFIG, ...config };
72
- }
73
-
74
- /**
75
- * Generate a consistent 64-bit lock key from a task ID
76
- * Uses a simple hash function to convert string task IDs to bigints
77
- *
78
- * The lock key is composed of:
79
- * - Upper 32 bits: lockKeyPrefix (namespace)
80
- * - Lower 32 bits: hash of taskId
81
- */
82
- private generateLockKey(taskId: string): bigint {
83
- // Simple hash function for the task ID
84
- let hash = 0;
85
- for (let i = 0; i < taskId.length; i++) {
86
- const char = taskId.charCodeAt(i);
87
- hash = ((hash << 5) - hash) + char;
88
- hash = hash & hash; // Convert to 32-bit integer
89
- }
90
- // Make it positive
91
- hash = Math.abs(hash);
92
-
93
- // Combine prefix (upper 32 bits) with hash (lower 32 bits)
94
- const prefix = BigInt(this.config.lockKeyPrefix);
95
- const hashBigInt = BigInt(hash >>> 0); // Ensure unsigned
96
- return (prefix << 32n) | hashBigInt;
97
- }
98
-
99
- /**
100
- * Try to acquire a distributed lock for a task
101
- *
102
- * Uses pg_try_advisory_lock which is non-blocking:
103
- * - Returns true immediately if lock is available
104
- * - Returns false immediately if lock is held by another session
105
- *
106
- * @param taskId The unique identifier for the task
107
- * @returns LockResult indicating whether the lock was acquired
108
- */
109
- async tryAcquire(taskId: string): Promise<LockResult> {
110
- if (!this.config.enabled) {
111
- return { acquired: true, lockKey: 0n, taskId };
112
- }
113
-
114
- const lockKey = this.generateLockKey(taskId);
115
- const startTime = Date.now();
116
-
117
- try {
118
- // Try to acquire the lock
119
- let acquired = await this.attemptLock(lockKey);
120
-
121
- // If lockTimeout > 0, retry until timeout
122
- if (!acquired && this.config.lockTimeout > 0) {
123
- while (!acquired && (Date.now() - startTime) < this.config.lockTimeout) {
124
- await this.sleep(this.config.retryInterval);
125
- acquired = await this.attemptLock(lockKey);
126
- }
127
- }
128
-
129
- if (acquired) {
130
- this.heldLocks.add(taskId);
131
- if (this.config.enableLogging) {
132
- loggerInstance.debug(`Acquired lock for task ${taskId} (key: ${lockKey})`);
133
- }
134
- } else {
135
- if (this.config.enableLogging) {
136
- loggerInstance.debug(`Failed to acquire lock for task ${taskId} (key: ${lockKey}) - another instance is executing`);
137
- }
138
- }
139
-
140
- return { acquired, lockKey, taskId };
141
- } catch (error) {
142
- loggerInstance.error(`Error acquiring lock for task ${taskId}: ${error instanceof Error ? error.message : String(error)}`);
143
- // On error, return false to be safe (don't execute without lock)
144
- return { acquired: false, lockKey, taskId };
145
- }
146
- }
147
-
148
- /**
149
- * Attempt to acquire the PostgreSQL advisory lock
150
- */
151
- private async attemptLock(lockKey: bigint): Promise<boolean> {
152
- const result = await db`
153
- SELECT pg_try_advisory_lock(${lockKey}::bigint) as pg_try_advisory_lock
154
- `;
155
- return result[0]?.pg_try_advisory_lock ?? false;
156
- }
157
-
158
- /**
159
- * Release a distributed lock for a task
160
- *
161
- * Uses pg_advisory_unlock to explicitly release the lock.
162
- * The lock is also automatically released if the connection closes.
163
- *
164
- * @param taskId The unique identifier for the task
165
- * @returns true if the lock was released, false if it wasn't held
166
- */
167
- async release(taskId: string): Promise<boolean> {
168
- if (!this.config.enabled) {
169
- return true;
170
- }
171
-
172
- const lockKey = this.generateLockKey(taskId);
173
-
174
- try {
175
- const result = await db`
176
- SELECT pg_advisory_unlock(${lockKey}::bigint) as pg_advisory_unlock
177
- `;
178
-
179
- const released = result[0]?.pg_advisory_unlock ?? false;
180
-
181
- if (released) {
182
- this.heldLocks.delete(taskId);
183
- if (this.config.enableLogging) {
184
- loggerInstance.debug(`Released lock for task ${taskId} (key: ${lockKey})`);
185
- }
186
- } else {
187
- if (this.config.enableLogging) {
188
- loggerInstance.warn(`Lock for task ${taskId} was not held or already released`);
189
- }
190
- }
191
-
192
- return released;
193
- } catch (error) {
194
- loggerInstance.error(`Error releasing lock for task ${taskId}: ${error instanceof Error ? error.message : String(error)}`);
195
- this.heldLocks.delete(taskId); // Remove from tracking even on error
196
- return false;
197
- }
198
- }
199
-
200
- /**
201
- * Release all locks held by this instance
202
- * Useful during shutdown
203
- */
204
- async releaseAll(): Promise<void> {
205
- const tasks = Array.from(this.heldLocks);
206
- for (const taskId of tasks) {
207
- await this.release(taskId);
208
- }
209
- }
210
-
211
- /**
212
- * Check if a lock is currently held (locally tracked)
213
- */
214
- isHeld(taskId: string): boolean {
215
- return this.heldLocks.has(taskId);
216
- }
217
-
218
- /**
219
- * Get the count of locks held by this instance
220
- */
221
- getHeldLockCount(): number {
222
- return this.heldLocks.size;
223
- }
224
-
225
- /**
226
- * Update the configuration
227
- */
228
- updateConfig(config: Partial<DistributedLockConfig>): void {
229
- this.config = { ...this.config, ...config };
230
- }
231
-
232
- /**
233
- * Get current configuration
234
- */
235
- getConfig(): DistributedLockConfig {
236
- return { ...this.config };
237
- }
238
-
239
- private sleep(ms: number): Promise<void> {
240
- return new Promise(resolve => setTimeout(resolve, ms));
241
- }
242
- }
243
-
244
- /**
245
- * Singleton instance for global access
246
- */
247
- let distributedLockInstance: DistributedLock | null = null;
248
-
249
- export function getDistributedLock(config?: Partial<DistributedLockConfig>): DistributedLock {
250
- if (!distributedLockInstance) {
251
- distributedLockInstance = new DistributedLock(config);
252
- } else if (config) {
253
- distributedLockInstance.updateConfig(config);
254
- }
255
- return distributedLockInstance;
256
- }
257
-
258
- /**
259
- * Reset the singleton instance (useful for testing)
260
- */
261
- export function resetDistributedLock(): void {
262
- if (distributedLockInstance) {
263
- distributedLockInstance.releaseAll().catch(() => {});
264
- distributedLockInstance = null;
265
- }
266
- }
1
+ /**
2
+ * Distributed Lock using PostgreSQL Advisory Locks
3
+ *
4
+ * PostgreSQL advisory locks are session-level (bound to the connection that
5
+ * acquired them). Bun's SQL pool hands out a different connection per query,
6
+ * so naively calling `pg_try_advisory_lock` on the pooled client leaves the
7
+ * lock stranded on whichever connection was used — `pg_advisory_unlock` on a
8
+ * different connection silently returns `false` and the lock is held until
9
+ * that connection eventually closes.
10
+ *
11
+ * Fix: reserve a dedicated connection via `sql.reserve()` once per instance
12
+ * and route every lock/unlock query through it. All locks owned by this
13
+ * instance live in one PostgreSQL session, so unlock always hits the session
14
+ * that acquired the lock. If the process crashes, PostgreSQL terminates the
15
+ * session and every held lock is released automatically — no cleanup needed.
16
+ *
17
+ * The reservation is lazy (acquired on first use) and released when either
18
+ * `releaseAll()` is called or no locks remain outstanding, so idle instances
19
+ * do not permanently consume a pool slot.
20
+ *
21
+ * @see https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS
22
+ */
23
+
24
+ import type { ReservedSQL } from "bun";
25
+ import db from "../../database";
26
+ import { logger } from "../Logger";
27
+
28
+ const loggerInstance = logger.child({ scope: "DistributedLock" });
29
+
30
+ export interface LockResult {
31
+ acquired: boolean;
32
+ lockKey: bigint;
33
+ taskId: string;
34
+ }
35
+
36
+ export interface DistributedLockConfig {
37
+ enabled: boolean;
38
+ lockKeyPrefix: number;
39
+ enableLogging: boolean;
40
+ /** Timeout for lock acquisition attempts in ms (0 = no retry) */
41
+ lockTimeout: number;
42
+ /** Retry interval when lockTimeout > 0 */
43
+ retryInterval: number;
44
+ }
45
+
46
+ export const DEFAULT_LOCK_CONFIG: DistributedLockConfig = {
47
+ enabled: true,
48
+ lockKeyPrefix: 0x42554E53, // "BUNS" in hex as a namespace prefix
49
+ enableLogging: false,
50
+ lockTimeout: 0,
51
+ retryInterval: 100,
52
+ };
53
+
54
+ export class DistributedLock {
55
+ private config: DistributedLockConfig;
56
+ private heldLocks: Set<string> = new Set();
57
+ private reservedConn: ReservedSQL | null = null;
58
+ private reservePromise: Promise<ReservedSQL> | null = null;
59
+
60
+ constructor(config: Partial<DistributedLockConfig> = {}) {
61
+ this.config = { ...DEFAULT_LOCK_CONFIG, ...config };
62
+ }
63
+
64
+ private generateLockKey(taskId: string): bigint {
65
+ let hash = 0;
66
+ for (let i = 0; i < taskId.length; i++) {
67
+ const char = taskId.charCodeAt(i);
68
+ hash = ((hash << 5) - hash) + char;
69
+ hash = hash & hash;
70
+ }
71
+ hash = Math.abs(hash);
72
+
73
+ const prefix = BigInt(this.config.lockKeyPrefix);
74
+ const hashBigInt = BigInt(hash >>> 0);
75
+ return (prefix << 32n) | hashBigInt;
76
+ }
77
+
78
+ /**
79
+ * Lazily reserve one dedicated connection that owns every advisory lock
80
+ * this instance takes. Concurrent callers share the same reservation via
81
+ * `reservePromise`.
82
+ */
83
+ private async ensureReserved(): Promise<ReservedSQL> {
84
+ if (this.reservedConn) return this.reservedConn;
85
+ if (!this.reservePromise) {
86
+ // On reject (pool exhausted, shutdown mid-reserve), null the
87
+ // promise so subsequent callers retry a fresh reserve instead of
88
+ // receiving the same rejected promise forever (H-DB-2).
89
+ this.reservePromise = db.reserve().then(
90
+ (conn) => {
91
+ this.reservedConn = conn;
92
+ this.reservePromise = null;
93
+ return conn;
94
+ },
95
+ (err) => {
96
+ this.reservePromise = null;
97
+ throw err;
98
+ }
99
+ );
100
+ }
101
+ return this.reservePromise;
102
+ }
103
+
104
+ /**
105
+ * Release the pinned connection back to the pool. Only safe when no
106
+ * advisory locks are currently held on this instance — otherwise the
107
+ * session would be closed and locks forfeited.
108
+ */
109
+ private releaseReservation(): void {
110
+ if (!this.reservedConn) return;
111
+ try {
112
+ this.reservedConn.release();
113
+ } catch (error) {
114
+ loggerInstance.warn(
115
+ `Failed to release reserved connection: ${error instanceof Error ? error.message : String(error)}`
116
+ );
117
+ }
118
+ this.reservedConn = null;
119
+ }
120
+
121
+ /**
122
+ * Try to acquire a distributed lock for a task. Non-blocking when
123
+ * `lockTimeout` is 0 (default); retries every `retryInterval` ms up to
124
+ * `lockTimeout` otherwise.
125
+ */
126
+ async tryAcquire(taskId: string): Promise<LockResult> {
127
+ if (!this.config.enabled) {
128
+ return { acquired: true, lockKey: 0n, taskId };
129
+ }
130
+
131
+ const lockKey = this.generateLockKey(taskId);
132
+
133
+ if (this.heldLocks.has(taskId)) {
134
+ // Defense in depth: if this instance already holds the lock for
135
+ // taskId, a second concurrent acquirer would mean overlapping
136
+ // execution (retry firing while previous run is still in the
137
+ // finally → release step, for example). Return acquired:false so
138
+ // the second caller skips, even if caller-side guards missed it.
139
+ // (H-SCHED-4).
140
+ if (this.config.enableLogging) {
141
+ loggerInstance.debug(
142
+ `Lock for ${taskId} already held locally reporting overlap (acquired:false)`
143
+ );
144
+ }
145
+ return { acquired: false, lockKey, taskId };
146
+ }
147
+
148
+ const startTime = Date.now();
149
+
150
+ try {
151
+ const conn = await this.ensureReserved();
152
+
153
+ let acquired = await this.attemptLock(conn, lockKey);
154
+
155
+ if (!acquired && this.config.lockTimeout > 0) {
156
+ while (
157
+ !acquired &&
158
+ Date.now() - startTime < this.config.lockTimeout
159
+ ) {
160
+ await this.sleep(this.config.retryInterval);
161
+ acquired = await this.attemptLock(conn, lockKey);
162
+ }
163
+ }
164
+
165
+ if (acquired) {
166
+ this.heldLocks.add(taskId);
167
+ if (this.config.enableLogging) {
168
+ loggerInstance.debug(
169
+ `Acquired lock for task ${taskId} (key: ${lockKey})`
170
+ );
171
+ }
172
+ return { acquired: true, lockKey, taskId };
173
+ }
174
+
175
+ // No locks taken on this attempt — if nothing else is held,
176
+ // return the reserved connection to the pool.
177
+ if (this.heldLocks.size === 0) {
178
+ this.releaseReservation();
179
+ }
180
+
181
+ if (this.config.enableLogging) {
182
+ loggerInstance.debug(
183
+ `Failed to acquire lock for task ${taskId} (key: ${lockKey}) — another instance is executing`
184
+ );
185
+ }
186
+ return { acquired: false, lockKey, taskId };
187
+ } catch (error) {
188
+ loggerInstance.error(
189
+ `Error acquiring lock for task ${taskId}: ${error instanceof Error ? error.message : String(error)}`
190
+ );
191
+ if (this.heldLocks.size === 0) {
192
+ this.releaseReservation();
193
+ }
194
+ return { acquired: false, lockKey, taskId };
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Release a single distributed lock. When the last lock is released the
200
+ * reserved connection is returned to the pool.
201
+ */
202
+ async release(taskId: string): Promise<boolean> {
203
+ if (!this.config.enabled) {
204
+ return true;
205
+ }
206
+
207
+ if (!this.heldLocks.has(taskId)) {
208
+ if (this.config.enableLogging) {
209
+ loggerInstance.warn(
210
+ `Lock for task ${taskId} was not held or already released`
211
+ );
212
+ }
213
+ return false;
214
+ }
215
+
216
+ const lockKey = this.generateLockKey(taskId);
217
+
218
+ if (!this.reservedConn) {
219
+ loggerInstance.warn(
220
+ `No reserved connection available for ${taskId}; dropping from heldLocks`
221
+ );
222
+ this.heldLocks.delete(taskId);
223
+ return false;
224
+ }
225
+
226
+ try {
227
+ const result = await this.reservedConn`
228
+ SELECT pg_advisory_unlock(${lockKey}::bigint) as pg_advisory_unlock
229
+ `;
230
+ const released = result[0]?.pg_advisory_unlock ?? false;
231
+
232
+ this.heldLocks.delete(taskId);
233
+
234
+ if (released && this.config.enableLogging) {
235
+ loggerInstance.debug(
236
+ `Released lock for task ${taskId} (key: ${lockKey})`
237
+ );
238
+ } else if (!released) {
239
+ loggerInstance.warn(
240
+ `pg_advisory_unlock returned false for task ${taskId} (key: ${lockKey})`
241
+ );
242
+ }
243
+
244
+ if (this.heldLocks.size === 0) {
245
+ this.releaseReservation();
246
+ }
247
+ return released;
248
+ } catch (error) {
249
+ loggerInstance.error(
250
+ `Error releasing lock for task ${taskId}: ${error instanceof Error ? error.message : String(error)}`
251
+ );
252
+ this.heldLocks.delete(taskId);
253
+ if (this.heldLocks.size === 0) {
254
+ this.releaseReservation();
255
+ }
256
+ return false;
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Release all held locks. Safe to call during shutdown.
262
+ */
263
+ async releaseAll(): Promise<void> {
264
+ const tasks = Array.from(this.heldLocks);
265
+ for (const taskId of tasks) {
266
+ await this.release(taskId);
267
+ }
268
+ // release() returns the reservation once heldLocks empties, but if
269
+ // nothing was held we still need to clean up any pending reservation.
270
+ if (this.heldLocks.size === 0) {
271
+ this.releaseReservation();
272
+ }
273
+ }
274
+
275
+ isHeld(taskId: string): boolean {
276
+ return this.heldLocks.has(taskId);
277
+ }
278
+
279
+ getHeldLockCount(): number {
280
+ return this.heldLocks.size;
281
+ }
282
+
283
+ updateConfig(config: Partial<DistributedLockConfig>): void {
284
+ this.config = { ...this.config, ...config };
285
+ }
286
+
287
+ getConfig(): DistributedLockConfig {
288
+ return { ...this.config };
289
+ }
290
+
291
+ private async attemptLock(
292
+ conn: ReservedSQL,
293
+ lockKey: bigint
294
+ ): Promise<boolean> {
295
+ const result = await conn`
296
+ SELECT pg_try_advisory_lock(${lockKey}::bigint) as pg_try_advisory_lock
297
+ `;
298
+ return result[0]?.pg_try_advisory_lock ?? false;
299
+ }
300
+
301
+ private sleep(ms: number): Promise<void> {
302
+ return new Promise((resolve) => setTimeout(resolve, ms));
303
+ }
304
+ }
305
+
306
+ let distributedLockInstance: DistributedLock | null = null;
307
+
308
+ export function getDistributedLock(
309
+ config?: Partial<DistributedLockConfig>
310
+ ): DistributedLock {
311
+ if (!distributedLockInstance) {
312
+ distributedLockInstance = new DistributedLock(config);
313
+ } else if (config) {
314
+ distributedLockInstance.updateConfig(config);
315
+ }
316
+ return distributedLockInstance;
317
+ }
318
+
319
+ export function resetDistributedLock(): void {
320
+ if (distributedLockInstance) {
321
+ distributedLockInstance.releaseAll().catch(() => {});
322
+ distributedLockInstance = null;
323
+ }
324
+ }
@@ -87,7 +87,7 @@ export class ResolverBuilder {
87
87
  throw new GraphQLError(`Internal error`, {
88
88
  extensions: {
89
89
  code: "INTERNAL_ERROR",
90
- originalError: process.env.NODE_ENV === 'development' ? error : undefined
90
+ originalError: process.env.NODE_ENV !== 'production' ? error : undefined
91
91
  }
92
92
  });
93
93
  }
@@ -111,7 +111,7 @@ export class ResolverBuilder {
111
111
  throw new GraphQLError(`Internal error`, {
112
112
  extensions: {
113
113
  code: "INTERNAL_ERROR",
114
- originalError: process.env.NODE_ENV === 'development' ? error : undefined
114
+ originalError: process.env.NODE_ENV !== 'production' ? error : undefined
115
115
  }
116
116
  });
117
117
  }
@@ -151,7 +151,7 @@ export class ResolverBuilder {
151
151
  throw new GraphQLError(`Internal error in subscription`, {
152
152
  extensions: {
153
153
  code: "INTERNAL_ERROR",
154
- originalError: process.env.NODE_ENV === 'development' ? error : undefined
154
+ originalError: process.env.NODE_ENV !== 'production' ? error : undefined
155
155
  }
156
156
  });
157
157
  }
@@ -177,7 +177,7 @@ export class ResolverBuilder {
177
177
  throw new GraphQLError(`Internal error in subscription`, {
178
178
  extensions: {
179
179
  code: "INTERNAL_ERROR",
180
- originalError: process.env.NODE_ENV === 'development' ? error : undefined
180
+ originalError: process.env.NODE_ENV !== 'production' ? error : undefined
181
181
  }
182
182
  });
183
183
  }