@periodic/vanadium 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.
Files changed (136) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/LICENSE +21 -0
  3. package/README.md +846 -0
  4. package/dist/cjs/adapters/memory/index.js +134 -0
  5. package/dist/cjs/adapters/memory/index.js.map +1 -0
  6. package/dist/cjs/adapters/mongodb/index.js +189 -0
  7. package/dist/cjs/adapters/mongodb/index.js.map +1 -0
  8. package/dist/cjs/adapters/mongoose/index.js +199 -0
  9. package/dist/cjs/adapters/mongoose/index.js.map +1 -0
  10. package/dist/cjs/adapters/postgres/index.js +202 -0
  11. package/dist/cjs/adapters/postgres/index.js.map +1 -0
  12. package/dist/cjs/adapters/prisma/index.js +176 -0
  13. package/dist/cjs/adapters/prisma/index.js.map +1 -0
  14. package/dist/cjs/adapters/redis/index.js +178 -0
  15. package/dist/cjs/adapters/redis/index.js.map +1 -0
  16. package/dist/cjs/cleanup/engine.js +100 -0
  17. package/dist/cjs/cleanup/engine.js.map +1 -0
  18. package/dist/cjs/core/concurrencyGuard.js +50 -0
  19. package/dist/cjs/core/concurrencyGuard.js.map +1 -0
  20. package/dist/cjs/core/metrics.js +39 -0
  21. package/dist/cjs/core/metrics.js.map +1 -0
  22. package/dist/cjs/core/stateMachine.js +46 -0
  23. package/dist/cjs/core/stateMachine.js.map +1 -0
  24. package/dist/cjs/errors/index.js +127 -0
  25. package/dist/cjs/errors/index.js.map +1 -0
  26. package/dist/cjs/http/express.js +84 -0
  27. package/dist/cjs/http/express.js.map +1 -0
  28. package/dist/cjs/http/fastify.js +70 -0
  29. package/dist/cjs/http/fastify.js.map +1 -0
  30. package/dist/cjs/idempotency/engine.js +266 -0
  31. package/dist/cjs/idempotency/engine.js.map +1 -0
  32. package/dist/cjs/index.js +19 -0
  33. package/dist/cjs/index.js.map +1 -0
  34. package/dist/cjs/lock/engine.js +187 -0
  35. package/dist/cjs/lock/engine.js.map +1 -0
  36. package/dist/cjs/observability/metrics.js +92 -0
  37. package/dist/cjs/observability/metrics.js.map +1 -0
  38. package/dist/cjs/resilience/circuitBreaker.js +129 -0
  39. package/dist/cjs/resilience/circuitBreaker.js.map +1 -0
  40. package/dist/cjs/types/index.js +13 -0
  41. package/dist/cjs/types/index.js.map +1 -0
  42. package/dist/cjs/utils/crypto.js +64 -0
  43. package/dist/cjs/utils/crypto.js.map +1 -0
  44. package/dist/cjs/utils/keys.js +40 -0
  45. package/dist/cjs/utils/keys.js.map +1 -0
  46. package/dist/cjs/utils/sleep.js +25 -0
  47. package/dist/cjs/utils/sleep.js.map +1 -0
  48. package/dist/esm/adapters/memory/index.js +129 -0
  49. package/dist/esm/adapters/memory/index.js.map +1 -0
  50. package/dist/esm/adapters/mongodb/index.js +184 -0
  51. package/dist/esm/adapters/mongodb/index.js.map +1 -0
  52. package/dist/esm/adapters/mongoose/index.js +193 -0
  53. package/dist/esm/adapters/mongoose/index.js.map +1 -0
  54. package/dist/esm/adapters/postgres/index.js +197 -0
  55. package/dist/esm/adapters/postgres/index.js.map +1 -0
  56. package/dist/esm/adapters/prisma/index.js +171 -0
  57. package/dist/esm/adapters/prisma/index.js.map +1 -0
  58. package/dist/esm/adapters/redis/index.js +173 -0
  59. package/dist/esm/adapters/redis/index.js.map +1 -0
  60. package/dist/esm/cleanup/engine.js +95 -0
  61. package/dist/esm/cleanup/engine.js.map +1 -0
  62. package/dist/esm/core/concurrencyGuard.js +46 -0
  63. package/dist/esm/core/concurrencyGuard.js.map +1 -0
  64. package/dist/esm/core/metrics.js +35 -0
  65. package/dist/esm/core/metrics.js.map +1 -0
  66. package/dist/esm/core/stateMachine.js +40 -0
  67. package/dist/esm/core/stateMachine.js.map +1 -0
  68. package/dist/esm/errors/index.js +114 -0
  69. package/dist/esm/errors/index.js.map +1 -0
  70. package/dist/esm/http/express.js +81 -0
  71. package/dist/esm/http/express.js.map +1 -0
  72. package/dist/esm/http/fastify.js +67 -0
  73. package/dist/esm/http/fastify.js.map +1 -0
  74. package/dist/esm/idempotency/engine.js +261 -0
  75. package/dist/esm/idempotency/engine.js.map +1 -0
  76. package/dist/esm/index.js +9 -0
  77. package/dist/esm/index.js.map +1 -0
  78. package/dist/esm/lock/engine.js +182 -0
  79. package/dist/esm/lock/engine.js.map +1 -0
  80. package/dist/esm/observability/metrics.js +89 -0
  81. package/dist/esm/observability/metrics.js.map +1 -0
  82. package/dist/esm/resilience/circuitBreaker.js +124 -0
  83. package/dist/esm/resilience/circuitBreaker.js.map +1 -0
  84. package/dist/esm/types/index.js +10 -0
  85. package/dist/esm/types/index.js.map +1 -0
  86. package/dist/esm/utils/crypto.js +58 -0
  87. package/dist/esm/utils/crypto.js.map +1 -0
  88. package/dist/esm/utils/keys.js +35 -0
  89. package/dist/esm/utils/keys.js.map +1 -0
  90. package/dist/esm/utils/sleep.js +20 -0
  91. package/dist/esm/utils/sleep.js.map +1 -0
  92. package/dist/types/adapters/memory/index.d.ts +49 -0
  93. package/dist/types/adapters/memory/index.d.ts.map +1 -0
  94. package/dist/types/adapters/mongodb/index.d.ts +97 -0
  95. package/dist/types/adapters/mongodb/index.d.ts.map +1 -0
  96. package/dist/types/adapters/mongoose/index.d.ts +107 -0
  97. package/dist/types/adapters/mongoose/index.d.ts.map +1 -0
  98. package/dist/types/adapters/postgres/index.d.ts +85 -0
  99. package/dist/types/adapters/postgres/index.d.ts.map +1 -0
  100. package/dist/types/adapters/prisma/index.d.ts +73 -0
  101. package/dist/types/adapters/prisma/index.d.ts.map +1 -0
  102. package/dist/types/adapters/redis/index.d.ts +77 -0
  103. package/dist/types/adapters/redis/index.d.ts.map +1 -0
  104. package/dist/types/cleanup/engine.d.ts +41 -0
  105. package/dist/types/cleanup/engine.d.ts.map +1 -0
  106. package/dist/types/core/concurrencyGuard.d.ts +28 -0
  107. package/dist/types/core/concurrencyGuard.d.ts.map +1 -0
  108. package/dist/types/core/metrics.d.ts +13 -0
  109. package/dist/types/core/metrics.d.ts.map +1 -0
  110. package/dist/types/core/stateMachine.d.ts +20 -0
  111. package/dist/types/core/stateMachine.d.ts.map +1 -0
  112. package/dist/types/errors/index.d.ts +32 -0
  113. package/dist/types/errors/index.d.ts.map +1 -0
  114. package/dist/types/http/express.d.ts +50 -0
  115. package/dist/types/http/express.d.ts.map +1 -0
  116. package/dist/types/http/fastify.d.ts +48 -0
  117. package/dist/types/http/fastify.d.ts.map +1 -0
  118. package/dist/types/idempotency/engine.d.ts +24 -0
  119. package/dist/types/idempotency/engine.d.ts.map +1 -0
  120. package/dist/types/index.d.ts +8 -0
  121. package/dist/types/index.d.ts.map +1 -0
  122. package/dist/types/lock/engine.d.ts +28 -0
  123. package/dist/types/lock/engine.d.ts.map +1 -0
  124. package/dist/types/observability/metrics.d.ts +45 -0
  125. package/dist/types/observability/metrics.d.ts.map +1 -0
  126. package/dist/types/resilience/circuitBreaker.d.ts +48 -0
  127. package/dist/types/resilience/circuitBreaker.d.ts.map +1 -0
  128. package/dist/types/types/index.d.ts +170 -0
  129. package/dist/types/types/index.d.ts.map +1 -0
  130. package/dist/types/utils/crypto.d.ts +20 -0
  131. package/dist/types/utils/crypto.d.ts.map +1 -0
  132. package/dist/types/utils/keys.d.ts +15 -0
  133. package/dist/types/utils/keys.d.ts.map +1 -0
  134. package/dist/types/utils/sleep.d.ts +13 -0
  135. package/dist/types/utils/sleep.d.ts.map +1 -0
  136. package/package.json +140 -0
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CleanupEngine = void 0;
4
+ exports.createCleanupEngine = createCleanupEngine;
5
+ const DEFAULT_INTERVAL_MS = 60000; // 1 minute
6
+ const DEFAULT_STALE_THRESHOLD_MS = 10 * 60000; // 10 minutes
7
+ // ─── Cleanup Engine ───────────────────────────────────────────────────────────
8
+ /**
9
+ * Optional background cleanup engine.
10
+ *
11
+ * Scans for orphaned IN_PROGRESS records and removes or marks them FAILED.
12
+ * Does NOT break distributed semantics — TTL handles the authoritative expiry.
13
+ * This is purely a maintenance utility.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const cleanup = createCleanupEngine({ adapter, intervalMs: 30_000 });
18
+ * cleanup.start();
19
+ * process.on('SIGTERM', () => cleanup.stop());
20
+ * ```
21
+ */
22
+ class CleanupEngine {
23
+ constructor(options) {
24
+ this.intervalHandle = null;
25
+ this.running = false;
26
+ this.adapter = options.adapter;
27
+ this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
28
+ this.staleThresholdMs = options.staleThresholdMs ?? DEFAULT_STALE_THRESHOLD_MS;
29
+ this.deleteOnClean = options.deleteOnClean ?? true;
30
+ this.clock = options.clock ?? Date.now;
31
+ }
32
+ /**
33
+ * Start the cleanup timer.
34
+ */
35
+ start() {
36
+ if (this.running)
37
+ return;
38
+ this.running = true;
39
+ this.intervalHandle = setInterval(() => {
40
+ void this._runCleanup();
41
+ }, this.intervalMs);
42
+ // Allow process to exit without waiting for cleanup timer
43
+ if (this.intervalHandle.unref) {
44
+ this.intervalHandle.unref();
45
+ }
46
+ }
47
+ /**
48
+ * Stop the cleanup timer gracefully.
49
+ */
50
+ stop() {
51
+ if (this.intervalHandle !== null) {
52
+ clearInterval(this.intervalHandle);
53
+ this.intervalHandle = null;
54
+ }
55
+ this.running = false;
56
+ }
57
+ /**
58
+ * Run a single cleanup pass manually (useful for testing).
59
+ */
60
+ async runOnce(keysToCheck) {
61
+ return this._cleanKeys(keysToCheck);
62
+ }
63
+ // ── Private ───────────────────────────────────────────────────────────────
64
+ async _runCleanup() {
65
+ // NOTE: The cleanup engine operates on keys that callers explicitly provide.
66
+ // A full scan requires adapter-level support (e.g., SCAN in Redis, SELECT in Postgres).
67
+ // This base implementation is a no-op periodic tick — adapters may extend it.
68
+ // Use runOnce(keys) to clean specific known stale keys.
69
+ }
70
+ async _cleanKeys(keys) {
71
+ const now = this.clock();
72
+ let cleaned = 0;
73
+ for (const key of keys) {
74
+ try {
75
+ const record = await this.adapter.get(key);
76
+ if (!record)
77
+ continue;
78
+ if (record.status !== 'IN_PROGRESS')
79
+ continue;
80
+ const staleAt = record.updatedAt + this.staleThresholdMs;
81
+ if (now < staleAt)
82
+ continue;
83
+ if (this.deleteOnClean) {
84
+ await this.adapter.delete(key);
85
+ }
86
+ cleaned++;
87
+ }
88
+ catch {
89
+ // Best effort — never let cleanup crash the app
90
+ }
91
+ }
92
+ return cleaned;
93
+ }
94
+ }
95
+ exports.CleanupEngine = CleanupEngine;
96
+ // ─── Factory ──────────────────────────────────────────────────────────────────
97
+ function createCleanupEngine(options) {
98
+ return new CleanupEngine(options);
99
+ }
100
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../src/cleanup/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConcurrencyGuard = void 0;
4
+ /**
5
+ * Local concurrency guard — prevents redundant concurrent calls to the
6
+ * distributed adapter for the same key within a single process.
7
+ *
8
+ * This is a performance optimization ONLY. It does NOT replace the
9
+ * distributed guarantee provided by the storage adapter.
10
+ */
11
+ class ConcurrencyGuard {
12
+ constructor() {
13
+ this.inFlight = new Map();
14
+ }
15
+ /**
16
+ * If a promise for this key is already in-flight, return it.
17
+ * Otherwise, register the new promise and clean up when it settles.
18
+ */
19
+ wrap(key, fn) {
20
+ const existing = this.inFlight.get(key);
21
+ if (existing !== undefined) {
22
+ return existing;
23
+ }
24
+ const promise = fn().finally(() => {
25
+ this.inFlight.delete(key);
26
+ });
27
+ this.inFlight.set(key, promise);
28
+ return promise;
29
+ }
30
+ /**
31
+ * Check if a key is currently in-flight locally.
32
+ */
33
+ isInFlight(key) {
34
+ return this.inFlight.has(key);
35
+ }
36
+ /**
37
+ * Return the number of keys currently tracked.
38
+ */
39
+ size() {
40
+ return this.inFlight.size;
41
+ }
42
+ /**
43
+ * Clear all tracked keys (for testing / cleanup).
44
+ */
45
+ clear() {
46
+ this.inFlight.clear();
47
+ }
48
+ }
49
+ exports.ConcurrencyGuard = ConcurrencyGuard;
50
+ //# sourceMappingURL=concurrencyGuard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"concurrencyGuard.js","sourceRoot":"","sources":["../../../src/core/concurrencyGuard.ts"],"names":[],"mappings":";;;AAAA;;;;;;GAMG;AACH,MAAa,gBAAgB;IAA7B;QACmB,aAAQ,GAAG,IAAI,GAAG,EAA4B,CAAC;IAwClE,CAAC;IAtCC;;;OAGG;IACH,IAAI,CAAI,GAAW,EAAE,EAAoB;QACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACxC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,OAAO,QAAsB,CAAC;QAChC,CAAC;QAED,MAAM,OAAO,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YAChC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC5B,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAChC,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;OAEG;IACH,UAAU,CAAC,GAAW;QACpB,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAChC,CAAC;IAED;;OAEG;IACH,IAAI;QACF,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC5B,CAAC;IAED;;OAEG;IACH,KAAK;QACH,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;CACF;AAzCD,4CAyCC"}
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MetricsStore = void 0;
4
+ // ─── Metrics Store ────────────────────────────────────────────────────────────
5
+ /**
6
+ * Mutable metrics container. One instance per engine.
7
+ * Never uses global state.
8
+ */
9
+ class MetricsStore {
10
+ constructor() {
11
+ this.data = {
12
+ totalExecutions: 0,
13
+ totalDuplicates: 0,
14
+ totalLocksAcquired: 0,
15
+ totalLockFailures: 0,
16
+ totalPayloadMismatches: 0,
17
+ inProgressCount: 0,
18
+ totalTakeovers: 0,
19
+ totalStorageErrors: 0,
20
+ totalFailuresCached: 0,
21
+ };
22
+ }
23
+ increment(key, amount = 1) {
24
+ this.data[key] += amount;
25
+ }
26
+ decrement(key, amount = 1) {
27
+ this.data[key] = Math.max(0, this.data[key] - amount);
28
+ }
29
+ get() {
30
+ return { ...this.data };
31
+ }
32
+ reset() {
33
+ for (const key of Object.keys(this.data)) {
34
+ this.data[key] = 0;
35
+ }
36
+ }
37
+ }
38
+ exports.MetricsStore = MetricsStore;
39
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../../src/core/metrics.ts"],"names":[],"mappings":";;;AAEA,iFAAiF;AAEjF;;;GAGG;AACH,MAAa,YAAY;IAAzB;QACU,SAAI,GAAoB;YAC9B,eAAe,EAAE,CAAC;YAClB,eAAe,EAAE,CAAC;YAClB,kBAAkB,EAAE,CAAC;YACrB,iBAAiB,EAAE,CAAC;YACpB,sBAAsB,EAAE,CAAC;YACzB,eAAe,EAAE,CAAC;YAClB,cAAc,EAAE,CAAC;YACjB,kBAAkB,EAAE,CAAC;YACrB,mBAAmB,EAAE,CAAC;SACvB,CAAC;IAmBJ,CAAC;IAjBC,SAAS,CAAC,GAA0B,EAAE,MAAM,GAAG,CAAC;QAC9C,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC;IAC3B,CAAC;IAED,SAAS,CAAC,GAA0B,EAAE,MAAM,GAAG,CAAC;QAC9C,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC;IACxD,CAAC;IAED,GAAG;QACD,OAAO,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC1B,CAAC;IAED,KAAK;QACH,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAA8B,EAAE,CAAC;YACtE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;CACF;AA9BD,oCA8BC"}
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertValidTransition = assertValidTransition;
4
+ exports.isValidTransition = isValidTransition;
5
+ exports.isExpired = isExpired;
6
+ exports.canTakeover = canTakeover;
7
+ const index_js_1 = require("../errors/index.js");
8
+ // ─── Valid Transitions ────────────────────────────────────────────────────────
9
+ const VALID_TRANSITIONS = new Set([
10
+ 'IN_PROGRESS -> COMPLETED',
11
+ 'IN_PROGRESS -> FAILED',
12
+ 'IN_PROGRESS -> IN_PROGRESS', // expired takeover only
13
+ ]);
14
+ // ─── State Machine ────────────────────────────────────────────────────────────
15
+ /**
16
+ * Assert that a transition from one execution status to another is valid.
17
+ * Throws a VanadiumError(STATE_TRANSITION_ERROR) on illegal transitions.
18
+ */
19
+ function assertValidTransition(from, to, key, adapterName, clock = Date.now) {
20
+ const transition = `${from} -> ${to}`;
21
+ if (!VALID_TRANSITIONS.has(transition)) {
22
+ throw (0, index_js_1.createStateTransitionError)(key, adapterName, from, to, clock);
23
+ }
24
+ }
25
+ /**
26
+ * Check if a transition is valid without throwing.
27
+ */
28
+ function isValidTransition(from, to) {
29
+ return VALID_TRANSITIONS.has(`${from} -> ${to}`);
30
+ }
31
+ /**
32
+ * Determine whether a stored record has expired based on current time.
33
+ */
34
+ function isExpired(expiresAt, clock = Date.now) {
35
+ if (expiresAt === undefined)
36
+ return false;
37
+ return clock() >= expiresAt;
38
+ }
39
+ /**
40
+ * Determine whether an IN_PROGRESS record can be taken over.
41
+ * Requires: status is IN_PROGRESS AND expiresAt has passed.
42
+ */
43
+ function canTakeover(status, expiresAt, clock = Date.now) {
44
+ return status === 'IN_PROGRESS' && isExpired(expiresAt, clock);
45
+ }
46
+ //# sourceMappingURL=stateMachine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stateMachine.js","sourceRoot":"","sources":["../../../src/core/stateMachine.ts"],"names":[],"mappings":";;AAiBA,sDAWC;AAKD,8CAEC;AAKD,8BAGC;AAMD,kCAMC;AAtDD,iDAAgE;AAEhE,iFAAiF;AAEjF,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAS;IACxC,0BAA0B;IAC1B,uBAAuB;IACvB,4BAA4B,EAAE,wBAAwB;CACvD,CAAC,CAAC;AAEH,iFAAiF;AAEjF;;;GAGG;AACH,SAAgB,qBAAqB,CACnC,IAAqB,EACrB,EAAmB,EACnB,GAAW,EACX,WAAmB,EACnB,QAAsB,IAAI,CAAC,GAAG;IAE9B,MAAM,UAAU,GAAG,GAAG,IAAI,OAAO,EAAE,EAAE,CAAC;IACtC,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;QACvC,MAAM,IAAA,qCAA0B,EAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;IACtE,CAAC;AACH,CAAC;AAED;;GAEG;AACH,SAAgB,iBAAiB,CAAC,IAAqB,EAAE,EAAmB;IAC1E,OAAO,iBAAiB,CAAC,GAAG,CAAC,GAAG,IAAI,OAAO,EAAE,EAAE,CAAC,CAAC;AACnD,CAAC;AAED;;GAEG;AACH,SAAgB,SAAS,CAAC,SAA6B,EAAE,QAAsB,IAAI,CAAC,GAAG;IACrF,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAC1C,OAAO,KAAK,EAAE,IAAI,SAAS,CAAC;AAC9B,CAAC;AAED;;;GAGG;AACH,SAAgB,WAAW,CACzB,MAAuB,EACvB,SAA6B,EAC7B,QAAsB,IAAI,CAAC,GAAG;IAE9B,OAAO,MAAM,KAAK,aAAa,IAAI,SAAS,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;AACjE,CAAC"}
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VanadiumError = void 0;
4
+ exports.createDuplicateExecutionError = createDuplicateExecutionError;
5
+ exports.createInProgressError = createInProgressError;
6
+ exports.createLockAcquisitionFailedError = createLockAcquisitionFailedError;
7
+ exports.createLockTimeoutError = createLockTimeoutError;
8
+ exports.createPayloadMismatchError = createPayloadMismatchError;
9
+ exports.createConfigurationError = createConfigurationError;
10
+ exports.createStorageError = createStorageError;
11
+ exports.createStateTransitionError = createStateTransitionError;
12
+ exports.isVanadiumError = isVanadiumError;
13
+ class VanadiumError extends Error {
14
+ constructor(details) {
15
+ super(details.message);
16
+ this.name = 'VanadiumError';
17
+ this.type = details.type;
18
+ this.key = details.key;
19
+ this.adapterName = details.adapterName;
20
+ this.timestamp = details.timestamp;
21
+ this.originalError = details.originalError;
22
+ this.attempts = details.attempts;
23
+ this.payloadHash = details.payloadHash;
24
+ // Maintain proper prototype chain
25
+ Object.setPrototypeOf(this, VanadiumError.prototype);
26
+ if (Error.captureStackTrace) {
27
+ Error.captureStackTrace(this, VanadiumError);
28
+ }
29
+ }
30
+ toJSON() {
31
+ return {
32
+ name: this.name,
33
+ type: this.type,
34
+ message: this.message,
35
+ key: this.key,
36
+ adapterName: this.adapterName,
37
+ timestamp: this.timestamp,
38
+ attempts: this.attempts,
39
+ payloadHash: this.payloadHash,
40
+ };
41
+ }
42
+ }
43
+ exports.VanadiumError = VanadiumError;
44
+ // ─── Error Factories ──────────────────────────────────────────────────────────
45
+ function createDuplicateExecutionError(key, adapterName, clock = Date.now) {
46
+ return new VanadiumError({
47
+ type: 'DUPLICATE_EXECUTION',
48
+ key,
49
+ adapterName,
50
+ timestamp: clock(),
51
+ message: `Duplicate execution detected for key "${key}". Returning cached result.`,
52
+ });
53
+ }
54
+ function createInProgressError(key, adapterName, attempts, clock = Date.now) {
55
+ return new VanadiumError({
56
+ type: 'IN_PROGRESS',
57
+ key,
58
+ adapterName,
59
+ timestamp: clock(),
60
+ attempts,
61
+ message: `Execution for key "${key}" is currently in progress (attempt ${attempts}). Another node owns execution.`,
62
+ });
63
+ }
64
+ function createLockAcquisitionFailedError(key, adapterName, clock = Date.now) {
65
+ return new VanadiumError({
66
+ type: 'LOCK_ACQUISITION_FAILED',
67
+ key,
68
+ adapterName,
69
+ timestamp: clock(),
70
+ message: `Failed to acquire lock for key "${key}".`,
71
+ });
72
+ }
73
+ function createLockTimeoutError(key, adapterName, maxWaitMs, clock = Date.now) {
74
+ return new VanadiumError({
75
+ type: 'LOCK_TIMEOUT',
76
+ key,
77
+ adapterName,
78
+ timestamp: clock(),
79
+ message: `Timed out waiting to acquire lock for key "${key}" after ${maxWaitMs}ms.`,
80
+ });
81
+ }
82
+ function createPayloadMismatchError(key, adapterName, storedHash, incomingHash, clock = Date.now) {
83
+ return new VanadiumError({
84
+ type: 'PAYLOAD_MISMATCH',
85
+ key,
86
+ adapterName,
87
+ timestamp: clock(),
88
+ payloadHash: incomingHash,
89
+ message: `Payload mismatch for key "${key}". ` +
90
+ `Stored hash: "${storedHash ?? 'none'}", Incoming hash: "${incomingHash}". ` +
91
+ `The same idempotency key cannot be used with different payloads.`,
92
+ });
93
+ }
94
+ function createConfigurationError(key, adapterName, message, clock = Date.now) {
95
+ return new VanadiumError({
96
+ type: 'CONFIGURATION_ERROR',
97
+ key,
98
+ adapterName,
99
+ timestamp: clock(),
100
+ message,
101
+ });
102
+ }
103
+ function createStorageError(key, adapterName, originalError, clock = Date.now) {
104
+ const msg = originalError instanceof Error ? originalError.message : String(originalError);
105
+ return new VanadiumError({
106
+ type: 'STORAGE_ERROR',
107
+ key,
108
+ adapterName,
109
+ timestamp: clock(),
110
+ originalError,
111
+ message: `Storage error for key "${key}" in adapter "${adapterName}": ${msg}`,
112
+ });
113
+ }
114
+ function createStateTransitionError(key, adapterName, from, to, clock = Date.now) {
115
+ return new VanadiumError({
116
+ type: 'STATE_TRANSITION_ERROR',
117
+ key,
118
+ adapterName,
119
+ timestamp: clock(),
120
+ message: `Invalid state transition for key "${key}": ${from} -> ${to}`,
121
+ });
122
+ }
123
+ // ─── Type Guard ───────────────────────────────────────────────────────────────
124
+ function isVanadiumError(err) {
125
+ return err instanceof VanadiumError;
126
+ }
127
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/errors/index.ts"],"names":[],"mappings":";;;AA2DA,sEAYC;AAED,sDAcC;AAED,4EAYC;AAED,wDAaC;AAED,gEAkBC;AAED,4DAaC;AAED,gDAeC;AAED,gEAcC;AAID,0CAEC;AA/KD,MAAa,aAAc,SAAQ,KAAK;IAStC,YAAY,OAA6B;QACvC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;QACvB,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;QACvC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;QACnC,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;QAC3C,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QACjC,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;QAEvC,kCAAkC;QAClC,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;QAErD,IAAI,KAAK,CAAC,iBAAiB,EAAE,CAAC;YAC5B,KAAK,CAAC,iBAAiB,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,MAAM;QACJ,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,WAAW,EAAE,IAAI,CAAC,WAAW;SAC9B,CAAC;IACJ,CAAC;CACF;AAxCD,sCAwCC;AAED,iFAAiF;AAEjF,SAAgB,6BAA6B,CAC3C,GAAW,EACX,WAAmB,EACnB,QAAsB,IAAI,CAAC,GAAG;IAE9B,OAAO,IAAI,aAAa,CAAC;QACvB,IAAI,EAAE,qBAAqB;QAC3B,GAAG;QACH,WAAW;QACX,SAAS,EAAE,KAAK,EAAE;QAClB,OAAO,EAAE,yCAAyC,GAAG,6BAA6B;KACnF,CAAC,CAAC;AACL,CAAC;AAED,SAAgB,qBAAqB,CACnC,GAAW,EACX,WAAmB,EACnB,QAAgB,EAChB,QAAsB,IAAI,CAAC,GAAG;IAE9B,OAAO,IAAI,aAAa,CAAC;QACvB,IAAI,EAAE,aAAa;QACnB,GAAG;QACH,WAAW;QACX,SAAS,EAAE,KAAK,EAAE;QAClB,QAAQ;QACR,OAAO,EAAE,sBAAsB,GAAG,uCAAuC,QAAQ,iCAAiC;KACnH,CAAC,CAAC;AACL,CAAC;AAED,SAAgB,gCAAgC,CAC9C,GAAW,EACX,WAAmB,EACnB,QAAsB,IAAI,CAAC,GAAG;IAE9B,OAAO,IAAI,aAAa,CAAC;QACvB,IAAI,EAAE,yBAAyB;QAC/B,GAAG;QACH,WAAW;QACX,SAAS,EAAE,KAAK,EAAE;QAClB,OAAO,EAAE,mCAAmC,GAAG,IAAI;KACpD,CAAC,CAAC;AACL,CAAC;AAED,SAAgB,sBAAsB,CACpC,GAAW,EACX,WAAmB,EACnB,SAAiB,EACjB,QAAsB,IAAI,CAAC,GAAG;IAE9B,OAAO,IAAI,aAAa,CAAC;QACvB,IAAI,EAAE,cAAc;QACpB,GAAG;QACH,WAAW;QACX,SAAS,EAAE,KAAK,EAAE;QAClB,OAAO,EAAE,8CAA8C,GAAG,WAAW,SAAS,KAAK;KACpF,CAAC,CAAC;AACL,CAAC;AAED,SAAgB,0BAA0B,CACxC,GAAW,EACX,WAAmB,EACnB,UAA8B,EAC9B,YAAoB,EACpB,QAAsB,IAAI,CAAC,GAAG;IAE9B,OAAO,IAAI,aAAa,CAAC;QACvB,IAAI,EAAE,kBAAkB;QACxB,GAAG;QACH,WAAW;QACX,SAAS,EAAE,KAAK,EAAE;QAClB,WAAW,EAAE,YAAY;QACzB,OAAO,EACL,6BAA6B,GAAG,KAAK;YACrC,iBAAiB,UAAU,IAAI,MAAM,sBAAsB,YAAY,KAAK;YAC5E,kEAAkE;KACrE,CAAC,CAAC;AACL,CAAC;AAED,SAAgB,wBAAwB,CACtC,GAAW,EACX,WAAmB,EACnB,OAAe,EACf,QAAsB,IAAI,CAAC,GAAG;IAE9B,OAAO,IAAI,aAAa,CAAC;QACvB,IAAI,EAAE,qBAAqB;QAC3B,GAAG;QACH,WAAW;QACX,SAAS,EAAE,KAAK,EAAE;QAClB,OAAO;KACR,CAAC,CAAC;AACL,CAAC;AAED,SAAgB,kBAAkB,CAChC,GAAW,EACX,WAAmB,EACnB,aAAsB,EACtB,QAAsB,IAAI,CAAC,GAAG;IAE9B,MAAM,GAAG,GAAG,aAAa,YAAY,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IAC3F,OAAO,IAAI,aAAa,CAAC;QACvB,IAAI,EAAE,eAAe;QACrB,GAAG;QACH,WAAW;QACX,SAAS,EAAE,KAAK,EAAE;QAClB,aAAa;QACb,OAAO,EAAE,0BAA0B,GAAG,iBAAiB,WAAW,MAAM,GAAG,EAAE;KAC9E,CAAC,CAAC;AACL,CAAC;AAED,SAAgB,0BAA0B,CACxC,GAAW,EACX,WAAmB,EACnB,IAAY,EACZ,EAAU,EACV,QAAsB,IAAI,CAAC,GAAG;IAE9B,OAAO,IAAI,aAAa,CAAC;QACvB,IAAI,EAAE,wBAAwB;QAC9B,GAAG;QACH,WAAW;QACX,SAAS,EAAE,KAAK,EAAE;QAClB,OAAO,EAAE,qCAAqC,GAAG,MAAM,IAAI,OAAO,EAAE,EAAE;KACvE,CAAC,CAAC;AACL,CAAC;AAED,iFAAiF;AAEjF,SAAgB,eAAe,CAAC,GAAY;IAC1C,OAAO,GAAG,YAAY,aAAa,CAAC;AACtC,CAAC"}
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ /**
3
+ * Express middleware for @periodic/vanadium
4
+ *
5
+ * Automatically wraps HTTP handlers with idempotency semantics.
6
+ * Reads the Idempotency-Key header and caches successful responses.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import express from 'express';
11
+ * import { createIdempotency, createMemoryAdapter } from '@periodic/vanadium';
12
+ * import { vanadiumMiddleware } from '@periodic/vanadium/http/express';
13
+ *
14
+ * const app = express();
15
+ * const idempotency = createIdempotency({ adapter: createMemoryAdapter(), ttlMs: 86_400_000 });
16
+ *
17
+ * app.post('/payments', vanadiumMiddleware(idempotency), async (req, res) => {
18
+ * const result = await chargeCard(req.body);
19
+ * res.json(result);
20
+ * });
21
+ * ```
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.vanadiumMiddleware = vanadiumMiddleware;
25
+ const index_js_1 = require("../errors/index.js");
26
+ // ─── Middleware Factory ───────────────────────────────────────────────────────
27
+ /**
28
+ * Create Express middleware that enforces idempotency on HTTP handlers.
29
+ */
30
+ function vanadiumMiddleware(idempotency, options = {}) {
31
+ const headerName = options.headerName ?? 'idempotency-key';
32
+ const methods = (options.methods ?? ['POST', 'PUT', 'PATCH']).map((m) => m.toUpperCase());
33
+ return function vanadiumHandler(req, res, next) {
34
+ if (!methods.includes(req.method.toUpperCase())) {
35
+ next();
36
+ return;
37
+ }
38
+ const rawHeader = req.headers[headerName.toLowerCase()];
39
+ const idempotencyKey = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
40
+ if (!idempotencyKey) {
41
+ next();
42
+ return;
43
+ }
44
+ const fullKey = `http:${req.method}:${req.path}:${idempotencyKey}`;
45
+ void idempotency
46
+ .execute(fullKey, () => {
47
+ return new Promise((resolve, _reject) => {
48
+ // Intercept res.json to capture the response
49
+ const originalJson = res.json.bind(res);
50
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
+ res.json = function (body) {
52
+ const statusCode = res.statusCode ?? 200;
53
+ originalJson(body);
54
+ resolve({ statusCode, body });
55
+ };
56
+ next();
57
+ });
58
+ })
59
+ .then((cached) => {
60
+ // If we got here without executing the handler (duplicate), replay cached response
61
+ if (cached) {
62
+ res.status(cached.statusCode).json(cached.body);
63
+ }
64
+ })
65
+ .catch((err) => {
66
+ if ((0, index_js_1.isVanadiumError)(err)) {
67
+ if (err.type === 'DUPLICATE_EXECUTION') {
68
+ // Already handled — no-op
69
+ return;
70
+ }
71
+ if (err.type === 'IN_PROGRESS') {
72
+ res.status(409).json({
73
+ error: 'Request is currently being processed',
74
+ type: 'IN_PROGRESS',
75
+ key: err.key,
76
+ });
77
+ return;
78
+ }
79
+ }
80
+ next(err);
81
+ });
82
+ };
83
+ }
84
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.js","sourceRoot":"","sources":["../../../src/http/express.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;AAiDH,gDA+DC;AA7GD,iDAAqD;AAyCrD,iFAAiF;AAEjF;;GAEG;AACH,SAAgB,kBAAkB,CAChC,WAA8B,EAC9B,UAAqC,EAAE;IAEvC,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,iBAAiB,CAAC;IAC3D,MAAM,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IAE1F,OAAO,SAAS,eAAe,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;QAC7E,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YAChD,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QAED,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC;QACxD,MAAM,cAAc,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAE3E,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QAED,MAAM,OAAO,GAAG,QAAQ,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,IAAI,IAAI,cAAc,EAAE,CAAC;QAEnE,KAAK,WAAW;aACb,OAAO,CAAiB,OAAO,EAAE,GAAG,EAAE;YACrC,OAAO,IAAI,OAAO,CAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE;gBACtD,6CAA6C;gBAC7C,MAAM,YAAY,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBAExC,8DAA8D;gBAC9D,GAAG,CAAC,IAAI,GAAG,UAAU,IAAS;oBAC5B,MAAM,UAAU,GAAG,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC;oBACzC,YAAY,CAAC,IAAI,CAAC,CAAC;oBACnB,OAAO,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;gBAChC,CAAC,CAAC;gBAEF,IAAI,EAAE,CAAC;YACT,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;aACD,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;YACf,mFAAmF;YACnF,IAAI,MAAM,EAAE,CAAC;gBACX,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAClD,CAAC;QACH,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACb,IAAI,IAAA,0BAAe,EAAC,GAAG,CAAC,EAAE,CAAC;gBACzB,IAAI,GAAG,CAAC,IAAI,KAAK,qBAAqB,EAAE,CAAC;oBACvC,0BAA0B;oBAC1B,OAAO;gBACT,CAAC;gBACD,IAAI,GAAG,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;oBAC/B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;wBACnB,KAAK,EAAE,sCAAsC;wBAC7C,IAAI,EAAE,aAAa;wBACnB,GAAG,EAAE,GAAG,CAAC,GAAG;qBACb,CAAC,CAAC;oBACH,OAAO;gBACT,CAAC;YACH,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,CAAC;QACZ,CAAC,CAAC,CAAC;IACP,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ /**
3
+ * Fastify plugin for @periodic/vanadium
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import Fastify from 'fastify';
8
+ * import { createIdempotency, createMemoryAdapter } from '@periodic/vanadium';
9
+ * import { vanadiumFastifyPlugin } from '@periodic/vanadium/http/fastify';
10
+ *
11
+ * const app = Fastify();
12
+ * const idempotency = createIdempotency({ adapter: createMemoryAdapter(), ttlMs: 86_400_000 });
13
+ *
14
+ * await app.register(vanadiumFastifyPlugin, { idempotency });
15
+ *
16
+ * app.post('/payments', async (request, reply) => {
17
+ * const result = await chargeCard(request.body);
18
+ * return result;
19
+ * });
20
+ * ```
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.vanadiumFastifyPlugin = vanadiumFastifyPlugin;
24
+ const index_js_1 = require("../errors/index.js");
25
+ // ─── Plugin ───────────────────────────────────────────────────────────────────
26
+ /**
27
+ * Register Vanadium idempotency as a Fastify plugin.
28
+ */
29
+ async function vanadiumFastifyPlugin(fastify, options) {
30
+ const { idempotency } = options;
31
+ const headerName = options.headerName ?? 'idempotency-key';
32
+ const methods = (options.methods ?? ['POST', 'PUT', 'PATCH']).map((m) => m.toUpperCase());
33
+ fastify.addHook('preHandler', async (request, reply) => {
34
+ if (!methods.includes(request.method.toUpperCase()))
35
+ return;
36
+ const rawHeader = request.headers[headerName.toLowerCase()];
37
+ const idempotencyKey = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
38
+ if (!idempotencyKey)
39
+ return;
40
+ const fullKey = `http:${request.method}:${request.url}:${idempotencyKey}`;
41
+ try {
42
+ // For Fastify, we check if a COMPLETED result exists and replay it
43
+ // The actual execution is handled downstream
44
+ const existing = await idempotency
45
+ .execute(fullKey, async () => {
46
+ // placeholder — real response will be captured in onSend
47
+ return null;
48
+ })
49
+ .catch((err) => {
50
+ if ((0, index_js_1.isVanadiumError)(err) && err.type === 'IN_PROGRESS') {
51
+ reply.code(409).send({
52
+ error: 'Request is currently being processed',
53
+ type: 'IN_PROGRESS',
54
+ key: err.key,
55
+ });
56
+ }
57
+ throw err;
58
+ });
59
+ if (existing !== null && reply.sent === false) {
60
+ reply.code(200).send(existing);
61
+ }
62
+ }
63
+ catch (err) {
64
+ if ((0, index_js_1.isVanadiumError)(err) && err.type === 'DUPLICATE_EXECUTION') {
65
+ return; // Already sent cached response
66
+ }
67
+ }
68
+ });
69
+ }
70
+ //# sourceMappingURL=fastify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fastify.js","sourceRoot":"","sources":["../../../src/http/fastify.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;GAmBG;;AA0CH,sDA6CC;AApFD,iDAAqD;AAkCrD,iFAAiF;AAEjF;;GAEG;AACI,KAAK,UAAU,qBAAqB,CACzC,OAAwB,EACxB,OAA+B;IAE/B,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC;IAChC,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,iBAAiB,CAAC;IAC3D,MAAM,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IAE1F,OAAO,CAAC,OAAO,CAAC,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACrD,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;YAAE,OAAO;QAE5D,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC;QAC5D,MAAM,cAAc,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC3E,IAAI,CAAC,cAAc;YAAE,OAAO;QAE5B,MAAM,OAAO,GAAG,QAAQ,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,IAAI,cAAc,EAAE,CAAC;QAE1E,IAAI,CAAC;YACH,mEAAmE;YACnE,6CAA6C;YAC7C,MAAM,QAAQ,GAAG,MAAM,WAAW;iBAC/B,OAAO,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;gBAC3B,yDAAyD;gBACzD,OAAO,IAAI,CAAC;YACd,CAAC,CAAC;iBACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;gBACb,IAAI,IAAA,0BAAe,EAAC,GAAG,CAAC,IAAI,GAAG,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;oBACvD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;wBACnB,KAAK,EAAE,sCAAsC;wBAC7C,IAAI,EAAE,aAAa;wBACnB,GAAG,EAAE,GAAG,CAAC,GAAG;qBACb,CAAC,CAAC;gBACL,CAAC;gBACD,MAAM,GAAG,CAAC;YACZ,CAAC,CAAC,CAAC;YAEL,IAAI,QAAQ,KAAK,IAAI,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;gBAC9C,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,IAAA,0BAAe,EAAC,GAAG,CAAC,IAAI,GAAG,CAAC,IAAI,KAAK,qBAAqB,EAAE,CAAC;gBAC/D,OAAO,CAAC,+BAA+B;YACzC,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}