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.
- package/CHANGELOG.md +266 -0
- package/config/cache.config.ts +12 -2
- package/core/App.ts +390 -66
- package/core/ApplicationLifecycle.ts +68 -4
- package/core/Entity.ts +407 -256
- package/core/EntityHookManager.ts +88 -21
- package/core/EntityManager.ts +12 -3
- package/core/Logger.ts +4 -0
- package/core/RequestContext.ts +4 -1
- package/core/SchedulerManager.ts +92 -9
- package/core/cache/CacheFactory.ts +3 -1
- package/core/cache/CacheManager.ts +54 -17
- package/core/cache/RedisCache.ts +38 -3
- package/core/decorators/EntityHooks.ts +24 -12
- package/core/middleware/RateLimit.ts +105 -0
- package/core/middleware/index.ts +1 -0
- package/core/remote/CircuitBreaker.ts +115 -0
- package/core/remote/OutboxWorker.ts +183 -0
- package/core/remote/RemoteManager.ts +400 -0
- package/core/remote/RpcCaller.ts +310 -0
- package/core/remote/StreamConsumer.ts +535 -0
- package/core/remote/decorators.ts +121 -0
- package/core/remote/health.ts +139 -0
- package/core/remote/index.ts +37 -0
- package/core/remote/metrics.ts +99 -0
- package/core/remote/outboxSchema.ts +41 -0
- package/core/remote/types.ts +151 -0
- package/core/scheduler/DistributedLock.ts +324 -266
- package/gql/builders/ResolverBuilder.ts +4 -4
- package/gql/complexityLimit.ts +95 -0
- package/gql/index.ts +15 -3
- package/gql/visitors/ResolverGeneratorVisitor.ts +16 -2
- package/package.json +1 -1
- package/query/ComponentInclusionNode.ts +13 -6
- package/query/OrNode.ts +2 -4
- package/query/Query.ts +30 -3
- package/query/SqlIdentifier.ts +105 -0
- package/query/builders/FullTextSearchBuilder.ts +19 -6
- package/service/ServiceRegistry.ts +21 -8
- package/storage/LocalStorageProvider.ts +12 -3
- package/storage/S3StorageProvider.ts +6 -6
- package/tests/e2e/http.test.ts +6 -2
- package/tests/helpers/MockRedisClient.ts +113 -0
- package/tests/helpers/MockRedisStreamServer.ts +448 -0
- package/tests/integration/entity/Entity.saveTimeout.test.ts +110 -0
- package/tests/integration/remote/dlq.test.ts +175 -0
- package/tests/integration/remote/event-dispatch.test.ts +114 -0
- package/tests/integration/remote/outbox.test.ts +130 -0
- package/tests/integration/remote/rpc.test.ts +177 -0
- package/tests/unit/remote/CircuitBreaker.test.ts +159 -0
- package/tests/unit/remote/RemoteError.test.ts +55 -0
- package/tests/unit/remote/decorators.test.ts +195 -0
- package/tests/unit/remote/metrics.test.ts +115 -0
- package/tests/unit/remote/mockRedisStreamServer.test.ts +104 -0
- package/tests/unit/storage/S3StorageProvider.test.ts +6 -10
- package/upload/FileValidator.ts +9 -6
|
@@ -1,266 +1,324 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Distributed Lock using PostgreSQL Advisory Locks
|
|
3
|
-
*
|
|
4
|
-
* PostgreSQL advisory locks are
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
export interface
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*/
|
|
109
|
-
|
|
110
|
-
if (!this.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
180
|
+
originalError: process.env.NODE_ENV !== 'production' ? error : undefined
|
|
181
181
|
}
|
|
182
182
|
});
|
|
183
183
|
}
|