@classytic/arc 1.0.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/LICENSE +21 -0
- package/README.md +900 -0
- package/bin/arc.js +344 -0
- package/dist/adapters/index.d.ts +237 -0
- package/dist/adapters/index.js +668 -0
- package/dist/arcCorePlugin-DTPWXcZN.d.ts +273 -0
- package/dist/audit/index.d.ts +195 -0
- package/dist/audit/index.js +319 -0
- package/dist/auth/index.d.ts +47 -0
- package/dist/auth/index.js +174 -0
- package/dist/cli/commands/docs.d.ts +11 -0
- package/dist/cli/commands/docs.js +474 -0
- package/dist/cli/commands/introspect.d.ts +8 -0
- package/dist/cli/commands/introspect.js +338 -0
- package/dist/cli/index.d.ts +43 -0
- package/dist/cli/index.js +520 -0
- package/dist/createApp-pzUAkzbz.d.ts +77 -0
- package/dist/docs/index.d.ts +166 -0
- package/dist/docs/index.js +650 -0
- package/dist/errors-8WIxGS_6.d.ts +122 -0
- package/dist/events/index.d.ts +117 -0
- package/dist/events/index.js +89 -0
- package/dist/factory/index.d.ts +38 -0
- package/dist/factory/index.js +1664 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +199 -0
- package/dist/idempotency/index.d.ts +323 -0
- package/dist/idempotency/index.js +500 -0
- package/dist/index-DkAW8BXh.d.ts +1302 -0
- package/dist/index.d.ts +331 -0
- package/dist/index.js +4734 -0
- package/dist/migrations/index.d.ts +185 -0
- package/dist/migrations/index.js +274 -0
- package/dist/org/index.d.ts +129 -0
- package/dist/org/index.js +220 -0
- package/dist/permissions/index.d.ts +144 -0
- package/dist/permissions/index.js +100 -0
- package/dist/plugins/index.d.ts +46 -0
- package/dist/plugins/index.js +1069 -0
- package/dist/policies/index.d.ts +398 -0
- package/dist/policies/index.js +196 -0
- package/dist/presets/index.d.ts +336 -0
- package/dist/presets/index.js +382 -0
- package/dist/presets/multiTenant.d.ts +39 -0
- package/dist/presets/multiTenant.js +112 -0
- package/dist/registry/index.d.ts +16 -0
- package/dist/registry/index.js +253 -0
- package/dist/testing/index.d.ts +618 -0
- package/dist/testing/index.js +48032 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.js +8 -0
- package/dist/types-0IPhH_NR.d.ts +143 -0
- package/dist/types-B99TBmFV.d.ts +76 -0
- package/dist/utils/index.d.ts +655 -0
- package/dist/utils/index.js +905 -0
- package/package.json +227 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import fp from 'fastify-plugin';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
|
|
4
|
+
// src/idempotency/idempotencyPlugin.ts
|
|
5
|
+
|
|
6
|
+
// src/idempotency/stores/interface.ts
|
|
7
|
+
function createIdempotencyResult(statusCode, body, headers, ttlMs) {
|
|
8
|
+
const now = /* @__PURE__ */ new Date();
|
|
9
|
+
return {
|
|
10
|
+
statusCode,
|
|
11
|
+
headers,
|
|
12
|
+
body,
|
|
13
|
+
createdAt: now,
|
|
14
|
+
expiresAt: new Date(now.getTime() + ttlMs)
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/idempotency/stores/memory.ts
|
|
19
|
+
var MemoryIdempotencyStore = class {
|
|
20
|
+
name = "memory";
|
|
21
|
+
results = /* @__PURE__ */ new Map();
|
|
22
|
+
locks = /* @__PURE__ */ new Map();
|
|
23
|
+
ttlMs;
|
|
24
|
+
maxEntries;
|
|
25
|
+
cleanupInterval = null;
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this.ttlMs = options.ttlMs ?? 864e5;
|
|
28
|
+
this.maxEntries = options.maxEntries ?? 1e4;
|
|
29
|
+
const cleanupIntervalMs = options.cleanupIntervalMs ?? 6e4;
|
|
30
|
+
this.cleanupInterval = setInterval(() => {
|
|
31
|
+
this.cleanup();
|
|
32
|
+
}, cleanupIntervalMs);
|
|
33
|
+
if (this.cleanupInterval.unref) {
|
|
34
|
+
this.cleanupInterval.unref();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async get(key) {
|
|
38
|
+
const result = this.results.get(key);
|
|
39
|
+
if (!result) return void 0;
|
|
40
|
+
if (/* @__PURE__ */ new Date() > result.expiresAt) {
|
|
41
|
+
this.results.delete(key);
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
async set(key, result) {
|
|
47
|
+
if (this.results.size >= this.maxEntries) {
|
|
48
|
+
this.evictOldest();
|
|
49
|
+
}
|
|
50
|
+
this.results.set(key, { ...result, key });
|
|
51
|
+
}
|
|
52
|
+
async tryLock(key, requestId, ttlMs) {
|
|
53
|
+
const existing = this.locks.get(key);
|
|
54
|
+
if (existing) {
|
|
55
|
+
if (/* @__PURE__ */ new Date() > existing.expiresAt) {
|
|
56
|
+
this.locks.delete(key);
|
|
57
|
+
} else {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
this.locks.set(key, {
|
|
62
|
+
key,
|
|
63
|
+
requestId,
|
|
64
|
+
lockedAt: /* @__PURE__ */ new Date(),
|
|
65
|
+
expiresAt: new Date(Date.now() + ttlMs)
|
|
66
|
+
});
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
async unlock(key, requestId) {
|
|
70
|
+
const lock = this.locks.get(key);
|
|
71
|
+
if (lock && lock.requestId === requestId) {
|
|
72
|
+
this.locks.delete(key);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async isLocked(key) {
|
|
76
|
+
const lock = this.locks.get(key);
|
|
77
|
+
if (!lock) return false;
|
|
78
|
+
if (/* @__PURE__ */ new Date() > lock.expiresAt) {
|
|
79
|
+
this.locks.delete(key);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
async delete(key) {
|
|
85
|
+
this.results.delete(key);
|
|
86
|
+
this.locks.delete(key);
|
|
87
|
+
}
|
|
88
|
+
async close() {
|
|
89
|
+
if (this.cleanupInterval) {
|
|
90
|
+
clearInterval(this.cleanupInterval);
|
|
91
|
+
this.cleanupInterval = null;
|
|
92
|
+
}
|
|
93
|
+
this.results.clear();
|
|
94
|
+
this.locks.clear();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get current stats (for debugging/monitoring)
|
|
98
|
+
*/
|
|
99
|
+
getStats() {
|
|
100
|
+
return {
|
|
101
|
+
results: this.results.size,
|
|
102
|
+
locks: this.locks.size
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Remove expired entries
|
|
107
|
+
*/
|
|
108
|
+
cleanup() {
|
|
109
|
+
const now = /* @__PURE__ */ new Date();
|
|
110
|
+
for (const [key, result] of this.results) {
|
|
111
|
+
if (now > result.expiresAt) {
|
|
112
|
+
this.results.delete(key);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const [key, lock] of this.locks) {
|
|
116
|
+
if (now > lock.expiresAt) {
|
|
117
|
+
this.locks.delete(key);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Evict oldest entries when at capacity
|
|
123
|
+
*/
|
|
124
|
+
evictOldest() {
|
|
125
|
+
const entries = Array.from(this.results.entries()).sort((a, b) => a[1].createdAt.getTime() - b[1].createdAt.getTime());
|
|
126
|
+
const toRemove = Math.max(1, Math.floor(entries.length * 0.1));
|
|
127
|
+
for (let i = 0; i < toRemove; i++) {
|
|
128
|
+
const entry = entries[i];
|
|
129
|
+
if (entry) {
|
|
130
|
+
this.results.delete(entry[0]);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// src/idempotency/idempotencyPlugin.ts
|
|
137
|
+
var HEADER_IDEMPOTENCY_REPLAYED = "x-idempotency-replayed";
|
|
138
|
+
var HEADER_IDEMPOTENCY_KEY = "x-idempotency-key";
|
|
139
|
+
var idempotencyPlugin = async (fastify, opts = {}) => {
|
|
140
|
+
const {
|
|
141
|
+
enabled = false,
|
|
142
|
+
headerName = "idempotency-key",
|
|
143
|
+
ttlMs = 864e5,
|
|
144
|
+
// 24 hours
|
|
145
|
+
lockTimeoutMs = 3e4,
|
|
146
|
+
// 30 seconds
|
|
147
|
+
methods = ["POST", "PUT", "PATCH"],
|
|
148
|
+
include,
|
|
149
|
+
exclude,
|
|
150
|
+
store = new MemoryIdempotencyStore({ ttlMs }),
|
|
151
|
+
retryAfterSeconds = 1
|
|
152
|
+
} = opts;
|
|
153
|
+
if (!enabled) {
|
|
154
|
+
fastify.decorate("idempotency", {
|
|
155
|
+
invalidate: async () => {
|
|
156
|
+
},
|
|
157
|
+
has: async () => false
|
|
158
|
+
});
|
|
159
|
+
fastify.decorateRequest("idempotencyKey", void 0);
|
|
160
|
+
fastify.decorateRequest("idempotencyReplayed", false);
|
|
161
|
+
fastify.log?.debug?.("Idempotency plugin disabled");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const methodSet = new Set(methods.map((m) => m.toUpperCase()));
|
|
165
|
+
fastify.decorate("idempotency", {
|
|
166
|
+
invalidate: async (key) => {
|
|
167
|
+
await store.delete(key);
|
|
168
|
+
},
|
|
169
|
+
has: async (key) => {
|
|
170
|
+
const result = await store.get(key);
|
|
171
|
+
return !!result;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
fastify.decorateRequest("idempotencyKey", void 0);
|
|
175
|
+
fastify.decorateRequest("idempotencyReplayed", false);
|
|
176
|
+
function shouldApplyIdempotency(request) {
|
|
177
|
+
if (!methodSet.has(request.method)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
const url = request.url;
|
|
181
|
+
if (exclude?.some((pattern) => pattern.test(url))) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
if (include && !include.some((pattern) => pattern.test(url))) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
function normalizeBody(obj) {
|
|
190
|
+
if (obj === null || typeof obj !== "object") {
|
|
191
|
+
return obj;
|
|
192
|
+
}
|
|
193
|
+
if (Array.isArray(obj)) {
|
|
194
|
+
return obj.map(normalizeBody);
|
|
195
|
+
}
|
|
196
|
+
const sorted = {};
|
|
197
|
+
const keys = Object.keys(obj).sort();
|
|
198
|
+
for (const key of keys) {
|
|
199
|
+
sorted[key] = normalizeBody(obj[key]);
|
|
200
|
+
}
|
|
201
|
+
return sorted;
|
|
202
|
+
}
|
|
203
|
+
function getRequestFingerprint(request) {
|
|
204
|
+
let bodyHash = "nobody";
|
|
205
|
+
if (request.body && typeof request.body === "object") {
|
|
206
|
+
const normalized = normalizeBody(request.body);
|
|
207
|
+
const bodyString = JSON.stringify(normalized);
|
|
208
|
+
bodyHash = createHash("sha256").update(bodyString).digest("hex").substring(0, 16);
|
|
209
|
+
if (request.log && request.log.debug) {
|
|
210
|
+
request.log.debug({ bodyHash }, "Generated body hash");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const fingerprint = `${request.method}:${request.url}:${bodyHash}`;
|
|
214
|
+
return fingerprint;
|
|
215
|
+
}
|
|
216
|
+
fastify.addHook("preHandler", async (request, reply) => {
|
|
217
|
+
if (!shouldApplyIdempotency(request)) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const keyHeader = request.headers[headerName.toLowerCase()];
|
|
221
|
+
const idempotencyKey = typeof keyHeader === "string" ? keyHeader.trim() : void 0;
|
|
222
|
+
if (!idempotencyKey) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
request.idempotencyKey = idempotencyKey;
|
|
226
|
+
const fullKey = `${idempotencyKey}:${getRequestFingerprint(request)}`;
|
|
227
|
+
const cached = await store.get(fullKey);
|
|
228
|
+
if (cached) {
|
|
229
|
+
request.idempotencyReplayed = true;
|
|
230
|
+
reply.header(HEADER_IDEMPOTENCY_REPLAYED, "true");
|
|
231
|
+
reply.header(HEADER_IDEMPOTENCY_KEY, idempotencyKey);
|
|
232
|
+
for (const [key, value] of Object.entries(cached.headers)) {
|
|
233
|
+
if (!key.startsWith("x-idempotency")) {
|
|
234
|
+
reply.header(key, value);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
reply.code(cached.statusCode).send(cached.body);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const lockAcquired = await store.tryLock(fullKey, request.id, lockTimeoutMs);
|
|
241
|
+
if (!lockAcquired) {
|
|
242
|
+
reply.code(409).header("Retry-After", retryAfterSeconds.toString()).send({
|
|
243
|
+
error: "Request with this idempotency key is already in progress",
|
|
244
|
+
code: "IDEMPOTENCY_CONFLICT",
|
|
245
|
+
retryAfter: retryAfterSeconds
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
request._idempotencyFullKey = fullKey;
|
|
250
|
+
});
|
|
251
|
+
fastify.addHook("onSend", async (request, reply, payload) => {
|
|
252
|
+
if (request.idempotencyReplayed) {
|
|
253
|
+
return payload;
|
|
254
|
+
}
|
|
255
|
+
const fullKey = request._idempotencyFullKey;
|
|
256
|
+
if (!fullKey) {
|
|
257
|
+
return payload;
|
|
258
|
+
}
|
|
259
|
+
const statusCode = reply.statusCode;
|
|
260
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
261
|
+
await store.unlock(fullKey, request.id);
|
|
262
|
+
return payload;
|
|
263
|
+
}
|
|
264
|
+
const headersToCache = {};
|
|
265
|
+
const excludeHeaders = /* @__PURE__ */ new Set([
|
|
266
|
+
"content-length",
|
|
267
|
+
"transfer-encoding",
|
|
268
|
+
"connection",
|
|
269
|
+
"keep-alive",
|
|
270
|
+
"date",
|
|
271
|
+
"set-cookie"
|
|
272
|
+
]);
|
|
273
|
+
const rawHeaders = reply.getHeaders();
|
|
274
|
+
for (const [key, value] of Object.entries(rawHeaders)) {
|
|
275
|
+
if (!excludeHeaders.has(key.toLowerCase()) && typeof value === "string") {
|
|
276
|
+
headersToCache[key] = value;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
let body;
|
|
280
|
+
try {
|
|
281
|
+
body = typeof payload === "string" ? JSON.parse(payload) : payload;
|
|
282
|
+
} catch {
|
|
283
|
+
body = payload;
|
|
284
|
+
}
|
|
285
|
+
const result = createIdempotencyResult(statusCode, body, headersToCache, ttlMs);
|
|
286
|
+
await store.set(fullKey, result);
|
|
287
|
+
await store.unlock(fullKey, request.id);
|
|
288
|
+
reply.header(HEADER_IDEMPOTENCY_KEY, request.idempotencyKey);
|
|
289
|
+
return payload;
|
|
290
|
+
});
|
|
291
|
+
fastify.addHook("onError", async (request) => {
|
|
292
|
+
const fullKey = request._idempotencyFullKey;
|
|
293
|
+
if (fullKey) {
|
|
294
|
+
await store.unlock(fullKey, request.id);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
fastify.addHook("onClose", async () => {
|
|
298
|
+
await store.close?.();
|
|
299
|
+
});
|
|
300
|
+
fastify.log?.info?.({ headerName, ttlMs, methods }, "Idempotency plugin enabled");
|
|
301
|
+
};
|
|
302
|
+
var idempotencyPlugin_default = fp(idempotencyPlugin, {
|
|
303
|
+
name: "arc-idempotency",
|
|
304
|
+
fastify: "5.x"
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// src/idempotency/stores/redis.ts
|
|
308
|
+
var RedisIdempotencyStore = class {
|
|
309
|
+
name = "redis";
|
|
310
|
+
client;
|
|
311
|
+
prefix;
|
|
312
|
+
lockPrefix;
|
|
313
|
+
ttlMs;
|
|
314
|
+
constructor(options) {
|
|
315
|
+
this.client = options.client;
|
|
316
|
+
this.prefix = options.prefix ?? "idem:";
|
|
317
|
+
this.lockPrefix = options.lockPrefix ?? "idem:lock:";
|
|
318
|
+
this.ttlMs = options.ttlMs ?? 864e5;
|
|
319
|
+
}
|
|
320
|
+
resultKey(key) {
|
|
321
|
+
return `${this.prefix}${key}`;
|
|
322
|
+
}
|
|
323
|
+
lockKey(key) {
|
|
324
|
+
return `${this.lockPrefix}${key}`;
|
|
325
|
+
}
|
|
326
|
+
async get(key) {
|
|
327
|
+
const data = await this.client.get(this.resultKey(key));
|
|
328
|
+
if (!data) return void 0;
|
|
329
|
+
try {
|
|
330
|
+
const result = JSON.parse(data);
|
|
331
|
+
if (new Date(result.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
332
|
+
await this.delete(key);
|
|
333
|
+
return void 0;
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
...result,
|
|
337
|
+
createdAt: new Date(result.createdAt),
|
|
338
|
+
expiresAt: new Date(result.expiresAt)
|
|
339
|
+
};
|
|
340
|
+
} catch {
|
|
341
|
+
return void 0;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async set(key, result) {
|
|
345
|
+
const data = { key, ...result };
|
|
346
|
+
const ttlSeconds = Math.ceil(
|
|
347
|
+
(new Date(result.expiresAt).getTime() - Date.now()) / 1e3
|
|
348
|
+
);
|
|
349
|
+
if (ttlSeconds > 0) {
|
|
350
|
+
await this.client.set(this.resultKey(key), JSON.stringify(data), {
|
|
351
|
+
EX: ttlSeconds
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async tryLock(key, requestId, ttlMs) {
|
|
356
|
+
const ttlSeconds = Math.ceil(ttlMs / 1e3);
|
|
357
|
+
const result = await this.client.set(
|
|
358
|
+
this.lockKey(key),
|
|
359
|
+
requestId,
|
|
360
|
+
{ EX: ttlSeconds, NX: true }
|
|
361
|
+
);
|
|
362
|
+
return result === "OK";
|
|
363
|
+
}
|
|
364
|
+
async unlock(key, requestId) {
|
|
365
|
+
const currentHolder = await this.client.get(this.lockKey(key));
|
|
366
|
+
if (currentHolder === requestId) {
|
|
367
|
+
await this.client.del(this.lockKey(key));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async isLocked(key) {
|
|
371
|
+
const exists = await this.client.exists(this.lockKey(key));
|
|
372
|
+
return exists > 0;
|
|
373
|
+
}
|
|
374
|
+
async delete(key) {
|
|
375
|
+
await this.client.del([this.resultKey(key), this.lockKey(key)]);
|
|
376
|
+
}
|
|
377
|
+
async close() {
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// src/idempotency/stores/mongodb.ts
|
|
382
|
+
var MongoIdempotencyStore = class {
|
|
383
|
+
name = "mongodb";
|
|
384
|
+
connection;
|
|
385
|
+
collectionName;
|
|
386
|
+
ttlMs;
|
|
387
|
+
indexCreated = false;
|
|
388
|
+
constructor(options) {
|
|
389
|
+
this.connection = options.connection;
|
|
390
|
+
this.collectionName = options.collection ?? "arc_idempotency";
|
|
391
|
+
this.ttlMs = options.ttlMs ?? 864e5;
|
|
392
|
+
if (options.createIndex !== false) {
|
|
393
|
+
this.ensureIndex().catch((err) => {
|
|
394
|
+
console.warn("[MongoIdempotencyStore] Failed to create index:", err);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
get collection() {
|
|
399
|
+
return this.connection.db.collection(this.collectionName);
|
|
400
|
+
}
|
|
401
|
+
async ensureIndex() {
|
|
402
|
+
if (this.indexCreated) return;
|
|
403
|
+
try {
|
|
404
|
+
await this.collection.createIndex(
|
|
405
|
+
{ expiresAt: 1 },
|
|
406
|
+
{ expireAfterSeconds: 0 }
|
|
407
|
+
);
|
|
408
|
+
this.indexCreated = true;
|
|
409
|
+
} catch {
|
|
410
|
+
this.indexCreated = true;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async get(key) {
|
|
414
|
+
const doc = await this.collection.findOne({ _id: key });
|
|
415
|
+
if (!doc || !doc.result) return void 0;
|
|
416
|
+
if (new Date(doc.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
417
|
+
return void 0;
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
key,
|
|
421
|
+
statusCode: doc.result.statusCode,
|
|
422
|
+
headers: doc.result.headers,
|
|
423
|
+
body: doc.result.body,
|
|
424
|
+
createdAt: new Date(doc.createdAt),
|
|
425
|
+
expiresAt: new Date(doc.expiresAt)
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
async set(key, result) {
|
|
429
|
+
await this.collection.updateOne(
|
|
430
|
+
{ _id: key },
|
|
431
|
+
{
|
|
432
|
+
$set: {
|
|
433
|
+
result: {
|
|
434
|
+
statusCode: result.statusCode,
|
|
435
|
+
headers: result.headers,
|
|
436
|
+
body: result.body
|
|
437
|
+
},
|
|
438
|
+
createdAt: result.createdAt,
|
|
439
|
+
expiresAt: result.expiresAt
|
|
440
|
+
},
|
|
441
|
+
$unset: { lock: "" }
|
|
442
|
+
},
|
|
443
|
+
{ upsert: true }
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
async tryLock(key, requestId, ttlMs) {
|
|
447
|
+
const now = /* @__PURE__ */ new Date();
|
|
448
|
+
const expiresAt = new Date(now.getTime() + ttlMs);
|
|
449
|
+
try {
|
|
450
|
+
const existingDoc = await this.collection.findOne({ _id: key });
|
|
451
|
+
if (existingDoc) {
|
|
452
|
+
if (existingDoc.lock && new Date(existingDoc.lock.expiresAt) > now) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
const updateResult = await this.collection.updateOne(
|
|
456
|
+
{
|
|
457
|
+
_id: key,
|
|
458
|
+
$or: [
|
|
459
|
+
{ lock: { $exists: false } },
|
|
460
|
+
{ "lock.expiresAt": { $lt: now } }
|
|
461
|
+
]
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
$set: {
|
|
465
|
+
lock: { requestId, expiresAt }
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
return updateResult.acknowledged;
|
|
470
|
+
}
|
|
471
|
+
const insertResult = await this.collection.insertOne({
|
|
472
|
+
_id: key,
|
|
473
|
+
lock: { requestId, expiresAt },
|
|
474
|
+
createdAt: now,
|
|
475
|
+
expiresAt: new Date(now.getTime() + this.ttlMs)
|
|
476
|
+
});
|
|
477
|
+
return insertResult.acknowledged;
|
|
478
|
+
} catch {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async unlock(key, requestId) {
|
|
483
|
+
await this.collection.updateOne(
|
|
484
|
+
{ _id: key, "lock.requestId": requestId },
|
|
485
|
+
{ $unset: { lock: "" } }
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
async isLocked(key) {
|
|
489
|
+
const doc = await this.collection.findOne({ _id: key });
|
|
490
|
+
if (!doc || !doc.lock) return false;
|
|
491
|
+
return new Date(doc.lock.expiresAt) > /* @__PURE__ */ new Date();
|
|
492
|
+
}
|
|
493
|
+
async delete(key) {
|
|
494
|
+
await this.collection.deleteOne({ _id: key });
|
|
495
|
+
}
|
|
496
|
+
async close() {
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
export { MemoryIdempotencyStore, MongoIdempotencyStore, RedisIdempotencyStore, createIdempotencyResult, idempotencyPlugin_default as idempotencyPlugin, idempotencyPlugin as idempotencyPluginFn };
|