@reactionary/core 0.1.8 → 0.1.9
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/cache/memory-cache.js +17 -0
- package/cache/noop-cache.js +16 -0
- package/cache/redis-cache.js +14 -0
- package/decorators/reactionary.decorator.js +42 -13
- package/metrics/metrics.js +50 -0
- package/package.json +1 -1
- package/src/cache/memory-cache.d.ts +1 -0
- package/src/cache/noop-cache.d.ts +1 -0
- package/src/cache/redis-cache.d.ts +1 -0
- package/src/decorators/reactionary.decorator.d.ts +1 -1
- package/src/metrics/metrics.d.ts +14 -0
package/cache/memory-cache.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
+
import { getReactionaryCacheMeter } from "../metrics/metrics.js";
|
|
1
2
|
class MemoryCache {
|
|
2
3
|
constructor() {
|
|
3
4
|
this.entries = new Array();
|
|
5
|
+
this.meter = getReactionaryCacheMeter();
|
|
4
6
|
}
|
|
5
7
|
async get(key, schema) {
|
|
6
8
|
const c = this.entries.find((x) => x.key === key);
|
|
7
9
|
if (!c) {
|
|
10
|
+
this.meter.misses.add(1, {
|
|
11
|
+
"labels.cache_type": "memory"
|
|
12
|
+
});
|
|
8
13
|
return null;
|
|
9
14
|
}
|
|
10
15
|
const parsed = schema.parse(c.value);
|
|
11
16
|
parsed.meta.cache.hit = true;
|
|
17
|
+
this.meter.hits.add(1, {
|
|
18
|
+
"labels.cache_type": "memory"
|
|
19
|
+
});
|
|
12
20
|
return parsed;
|
|
13
21
|
}
|
|
14
22
|
async put(key, value, options) {
|
|
@@ -17,6 +25,9 @@ class MemoryCache {
|
|
|
17
25
|
value,
|
|
18
26
|
options
|
|
19
27
|
});
|
|
28
|
+
this.meter.items.record(this.entries.length, {
|
|
29
|
+
"labels.cache_type": "memory"
|
|
30
|
+
});
|
|
20
31
|
return;
|
|
21
32
|
}
|
|
22
33
|
async invalidate(dependencyIds) {
|
|
@@ -29,9 +40,15 @@ class MemoryCache {
|
|
|
29
40
|
}
|
|
30
41
|
index++;
|
|
31
42
|
}
|
|
43
|
+
this.meter.items.record(this.entries.length, {
|
|
44
|
+
"labels.cache_type": "memory"
|
|
45
|
+
});
|
|
32
46
|
}
|
|
33
47
|
async clear() {
|
|
34
48
|
this.entries = [];
|
|
49
|
+
this.meter.items.record(this.entries.length, {
|
|
50
|
+
"labels.cache_type": "memory"
|
|
51
|
+
});
|
|
35
52
|
}
|
|
36
53
|
}
|
|
37
54
|
export {
|
package/cache/noop-cache.js
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
|
+
import { getReactionaryCacheMeter } from "../metrics/metrics.js";
|
|
1
2
|
class NoOpCache {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.meter = getReactionaryCacheMeter();
|
|
5
|
+
}
|
|
2
6
|
async get(_key, _schema) {
|
|
7
|
+
this.meter.misses.add(1, {
|
|
8
|
+
"labels.cache_type": "noop"
|
|
9
|
+
});
|
|
3
10
|
return null;
|
|
4
11
|
}
|
|
5
12
|
async put(_key, _value, options) {
|
|
13
|
+
this.meter.items.record(0, {
|
|
14
|
+
"labels.cache_type": "noop"
|
|
15
|
+
});
|
|
6
16
|
return;
|
|
7
17
|
}
|
|
8
18
|
async invalidate(dependencyIds) {
|
|
19
|
+
this.meter.items.record(0, {
|
|
20
|
+
"labels.cache_type": "noop"
|
|
21
|
+
});
|
|
9
22
|
return;
|
|
10
23
|
}
|
|
11
24
|
async clear() {
|
|
25
|
+
this.meter.items.record(0, {
|
|
26
|
+
"labels.cache_type": "noop"
|
|
27
|
+
});
|
|
12
28
|
return;
|
|
13
29
|
}
|
|
14
30
|
}
|
package/cache/redis-cache.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Redis } from "@upstash/redis";
|
|
2
|
+
import { getReactionaryCacheMeter } from "../metrics/metrics.js";
|
|
2
3
|
class RedisCache {
|
|
3
4
|
constructor() {
|
|
5
|
+
this.meter = getReactionaryCacheMeter();
|
|
4
6
|
this.redis = Redis.fromEnv();
|
|
5
7
|
}
|
|
6
8
|
async get(key, schema) {
|
|
@@ -10,6 +12,9 @@ class RedisCache {
|
|
|
10
12
|
const unvalidated = await this.redis.get(key);
|
|
11
13
|
const parsed = schema.safeParse(unvalidated);
|
|
12
14
|
if (parsed.success) {
|
|
15
|
+
this.meter.hits.add(1, {
|
|
16
|
+
"labels.cache_type": "redis"
|
|
17
|
+
});
|
|
13
18
|
return parsed.data;
|
|
14
19
|
}
|
|
15
20
|
return null;
|
|
@@ -24,6 +29,9 @@ class RedisCache {
|
|
|
24
29
|
for (const depId of options.dependencyIds) {
|
|
25
30
|
multi.sadd(`dep:${depId}`, key);
|
|
26
31
|
}
|
|
32
|
+
this.meter.items.record(await this.redis.dbsize(), {
|
|
33
|
+
"labels.cache_type": "redis"
|
|
34
|
+
});
|
|
27
35
|
await multi.exec();
|
|
28
36
|
}
|
|
29
37
|
async invalidate(dependencyIds) {
|
|
@@ -35,8 +43,14 @@ class RedisCache {
|
|
|
35
43
|
}
|
|
36
44
|
await this.redis.del(depKey);
|
|
37
45
|
}
|
|
46
|
+
this.meter.items.record(await this.redis.dbsize(), {
|
|
47
|
+
"labels.cache_type": "redis"
|
|
48
|
+
});
|
|
38
49
|
}
|
|
39
50
|
async clear() {
|
|
51
|
+
this.meter.items.record(await this.redis.dbsize(), {
|
|
52
|
+
"labels.cache_type": "redis"
|
|
53
|
+
});
|
|
40
54
|
}
|
|
41
55
|
}
|
|
42
56
|
export {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { trace
|
|
1
|
+
import { trace } from "@opentelemetry/api";
|
|
2
|
+
import { getReactionaryMeter } from "../metrics/metrics.js";
|
|
2
3
|
const TRACER_NAME = "@reactionary";
|
|
3
4
|
const TRACER_VERSION = "0.0.1";
|
|
4
5
|
let globalTracer = null;
|
|
@@ -38,6 +39,13 @@ function Reactionary(options) {
|
|
|
38
39
|
const original = descriptor.value;
|
|
39
40
|
const scope = `${target.constructor.name}.${propertyKey.toString()}`;
|
|
40
41
|
const configuration = { ...new ReactionaryDecoratorOptions(), ...options };
|
|
42
|
+
const meter = getReactionaryMeter();
|
|
43
|
+
const attributes = {
|
|
44
|
+
"labels.scope": scope
|
|
45
|
+
};
|
|
46
|
+
const startTime = performance.now();
|
|
47
|
+
let status = "ok";
|
|
48
|
+
let cacheStatus = "miss";
|
|
41
49
|
if (!original) {
|
|
42
50
|
throw new Error(
|
|
43
51
|
"@Reactionary decorator may only be applied to methods on classes extending BaseProvider."
|
|
@@ -45,19 +53,40 @@ function Reactionary(options) {
|
|
|
45
53
|
}
|
|
46
54
|
descriptor.value = async function(...args) {
|
|
47
55
|
return traceSpan(scope, async () => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
meter.requestInProgress.add(1, attributes);
|
|
57
|
+
try {
|
|
58
|
+
const input = validateInput(args[0], configuration.inputSchema);
|
|
59
|
+
const cacheKey = this.generateCacheKeyForQuery(scope, input);
|
|
60
|
+
const fromCache = await this.cache.get(
|
|
61
|
+
cacheKey,
|
|
62
|
+
options.inputSchema
|
|
63
|
+
);
|
|
64
|
+
let result = fromCache;
|
|
65
|
+
if (!result) {
|
|
66
|
+
result = await original.apply(this, [input]);
|
|
67
|
+
const dependencyIds = this.generateDependencyIdsForModel(result);
|
|
68
|
+
this.cache.put(cacheKey, result, {
|
|
69
|
+
ttlSeconds: configuration.cacheTimeToLiveInSeconds,
|
|
70
|
+
dependencyIds
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
cacheStatus = "hit";
|
|
74
|
+
}
|
|
75
|
+
return validateOutput(result, configuration.outputSchema);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
status = "error";
|
|
78
|
+
throw err;
|
|
79
|
+
} finally {
|
|
80
|
+
const duration = performance.now() - startTime;
|
|
81
|
+
const finalAttributes = {
|
|
82
|
+
...attributes,
|
|
83
|
+
"labels.status": status,
|
|
84
|
+
"labels.cache_status": cacheStatus
|
|
85
|
+
};
|
|
86
|
+
meter.requestInProgress.add(-1, finalAttributes);
|
|
87
|
+
meter.requests.add(1, finalAttributes);
|
|
88
|
+
meter.requestDuration.record(duration, finalAttributes);
|
|
59
89
|
}
|
|
60
|
-
return validateOutput(result, configuration.outputSchema);
|
|
61
90
|
});
|
|
62
91
|
};
|
|
63
92
|
return descriptor;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { metrics } from "@opentelemetry/api";
|
|
2
|
+
const METRICS_NAME = "@reactionary";
|
|
3
|
+
const TRACER_VERSION = "0.0.1";
|
|
4
|
+
let globalCacheMetrics = null;
|
|
5
|
+
function getReactionaryCacheMeter() {
|
|
6
|
+
if (!globalCacheMetrics) {
|
|
7
|
+
const meter = metrics.getMeter(METRICS_NAME, TRACER_VERSION);
|
|
8
|
+
globalCacheMetrics = {
|
|
9
|
+
items: meter.createGauge("reactionary_cache_items", {
|
|
10
|
+
description: "Tracks the number of items in the cache"
|
|
11
|
+
}),
|
|
12
|
+
hits: meter.createCounter("reactionary_cache_hits", {
|
|
13
|
+
description: "Counts the number of cache hits"
|
|
14
|
+
}),
|
|
15
|
+
misses: meter.createCounter("reactionary_cache_misses", {
|
|
16
|
+
description: "Counts the number of cache misses"
|
|
17
|
+
})
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return globalCacheMetrics;
|
|
21
|
+
}
|
|
22
|
+
let globalMetrics = null;
|
|
23
|
+
function getReactionaryMeter() {
|
|
24
|
+
if (!globalMetrics) {
|
|
25
|
+
const meter = metrics.getMeter(METRICS_NAME, TRACER_VERSION);
|
|
26
|
+
globalMetrics = {
|
|
27
|
+
requests: meter.createCounter("reactionary_provider_requests", {
|
|
28
|
+
description: "Counts the number of requests made to provider queries and mutations"
|
|
29
|
+
}),
|
|
30
|
+
requestDuration: meter.createHistogram(
|
|
31
|
+
"reactionary_provider_request_duration",
|
|
32
|
+
{
|
|
33
|
+
description: "Records the duration of provider query and mutation requests",
|
|
34
|
+
unit: "ms"
|
|
35
|
+
}
|
|
36
|
+
),
|
|
37
|
+
requestInProgress: meter.createUpDownCounter(
|
|
38
|
+
"reactionary_provider_requests_in_progress",
|
|
39
|
+
{
|
|
40
|
+
description: "Tracks the number of in-progress requests to provider queries and mutations"
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return globalMetrics;
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
getReactionaryCacheMeter,
|
|
49
|
+
getReactionaryMeter
|
|
50
|
+
};
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ export declare class MemoryCache implements Cache {
|
|
|
11
11
|
value: unknown;
|
|
12
12
|
options: CacheEntryOptions;
|
|
13
13
|
}[];
|
|
14
|
+
protected meter: import("../metrics/metrics.js").ReactionaryCacheMetrics;
|
|
14
15
|
get<T extends BaseModel>(key: string, schema: z.ZodType<T>): Promise<T | null>;
|
|
15
16
|
put(key: string, value: unknown, options: CacheEntryOptions): Promise<void>;
|
|
16
17
|
invalidate(dependencyIds: Array<string>): Promise<void>;
|
|
@@ -5,6 +5,7 @@ import type z from 'zod';
|
|
|
5
5
|
* Useful for testing or when caching should be disabled.
|
|
6
6
|
*/
|
|
7
7
|
export declare class NoOpCache implements Cache {
|
|
8
|
+
protected meter: import("../metrics/metrics.js").ReactionaryCacheMetrics;
|
|
8
9
|
get<T>(_key: string, _schema: z.ZodType<T>): Promise<T | null>;
|
|
9
10
|
put(_key: string, _value: unknown, options: CacheEntryOptions): Promise<void>;
|
|
10
11
|
invalidate(dependencyIds: Array<string>): Promise<void>;
|
|
@@ -3,6 +3,7 @@ import type { Cache, CacheEntryOptions } from './cache.interface.js';
|
|
|
3
3
|
import type z from 'zod';
|
|
4
4
|
export declare class RedisCache implements Cache {
|
|
5
5
|
protected redis: Redis;
|
|
6
|
+
protected meter: import("../metrics/metrics.js").ReactionaryCacheMetrics;
|
|
6
7
|
constructor();
|
|
7
8
|
get<T>(key: string, schema: z.ZodType<T>): Promise<T | null>;
|
|
8
9
|
put(key: string, value: unknown, options: CacheEntryOptions): Promise<void>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { BaseProvider } from '../providers/index.js';
|
|
2
1
|
import type { Tracer } from '@opentelemetry/api';
|
|
3
2
|
import type { z } from 'zod';
|
|
3
|
+
import type { BaseProvider } from '../providers/index.js';
|
|
4
4
|
export declare function getTracer(): Tracer;
|
|
5
5
|
/**
|
|
6
6
|
* The options associated with annotating a provider function and marking
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Counter, Gauge } from '@opentelemetry/api';
|
|
2
|
+
import { type Histogram, type UpDownCounter } from '@opentelemetry/api';
|
|
3
|
+
export interface ReactionaryCacheMetrics {
|
|
4
|
+
items: Gauge;
|
|
5
|
+
hits: Counter;
|
|
6
|
+
misses: Counter;
|
|
7
|
+
}
|
|
8
|
+
export declare function getReactionaryCacheMeter(): ReactionaryCacheMetrics;
|
|
9
|
+
export interface ReactionaryMetrics {
|
|
10
|
+
requests: Counter;
|
|
11
|
+
requestDuration: Histogram;
|
|
12
|
+
requestInProgress: UpDownCounter;
|
|
13
|
+
}
|
|
14
|
+
export declare function getReactionaryMeter(): ReactionaryMetrics;
|