@reactionary/source 0.0.29 → 0.0.31

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 (44) hide show
  1. package/CLAUDE.md +11 -0
  2. package/core/src/cache/cache-evaluation.interface.ts +19 -0
  3. package/core/src/cache/cache.interface.ts +38 -0
  4. package/core/src/cache/noop-cache.ts +42 -0
  5. package/core/src/cache/redis-cache.ts +55 -22
  6. package/core/src/client/client.ts +27 -3
  7. package/core/src/index.ts +3 -1
  8. package/core/src/providers/analytics.provider.ts +24 -1
  9. package/core/src/providers/base.provider.ts +84 -6
  10. package/core/src/providers/cart.provider.ts +18 -1
  11. package/core/src/providers/identity.provider.ts +18 -1
  12. package/core/src/providers/inventory.provider.ts +15 -2
  13. package/core/src/providers/price.provider.ts +18 -1
  14. package/core/src/providers/product.provider.ts +24 -1
  15. package/core/src/providers/search.provider.ts +18 -1
  16. package/core/src/schemas/mutations/product.mutation.ts +0 -1
  17. package/examples/node/src/providers/custom-algolia-product.provider.ts +2 -2
  18. package/examples/trpc-node/.env.example +1 -1
  19. package/examples/trpc-node/src/main.ts +1 -5
  20. package/examples/trpc-node/src/router-instance.ts +4 -2
  21. package/otel/README.md +0 -1
  22. package/otel/package.json +0 -8
  23. package/otel/pnpm-lock.yaml +805 -0
  24. package/otel/src/index.ts +7 -10
  25. package/otel/src/sdk.ts +3 -65
  26. package/package.json +1 -1
  27. package/providers/algolia/src/core/initialize.ts +11 -9
  28. package/providers/algolia/src/providers/product.provider.ts +2 -2
  29. package/providers/algolia/src/providers/search.provider.ts +2 -2
  30. package/providers/commercetools/src/core/initialize.ts +27 -22
  31. package/providers/commercetools/src/providers/cart.provider.ts +3 -2
  32. package/providers/commercetools/src/providers/identity.provider.ts +3 -2
  33. package/providers/commercetools/src/providers/inventory.provider.ts +5 -14
  34. package/providers/commercetools/src/providers/price.provider.ts +3 -3
  35. package/providers/commercetools/src/providers/product.provider.ts +2 -2
  36. package/providers/commercetools/src/providers/search.provider.ts +2 -2
  37. package/providers/fake/src/core/initialize.ts +14 -12
  38. package/providers/fake/src/providers/identity.provider.ts +3 -2
  39. package/providers/fake/src/providers/product.provider.ts +2 -2
  40. package/providers/fake/src/providers/search.provider.ts +2 -2
  41. package/providers/posthog/src/core/initialize.ts +6 -4
  42. package/trpc/src/index.ts +4 -8
  43. package/core/src/cache/caching-strategy.ts +0 -25
  44. package/otel/src/config.ts +0 -139
package/CLAUDE.md ADDED
@@ -0,0 +1,11 @@
1
+ # Verification commands
2
+ - `npx nx affected:build` builds the projects affected by current changes.
3
+ - `npx nx affected:lint` lints and verifies the projects affected by current changes.
4
+
5
+ # Code style
6
+ - Use ES modules (import/export) syntax, not CommonJS (require).
7
+ - Keep things strongly typed. Do not use `as any` or similar unchecked type casts.
8
+
9
+ # Workflow
10
+ - Be sure to ensure that changes lint and build.
11
+ - Avoid introducing changes that would be breaking for any client consumer of the library without asking for clearance.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Cache evaluation result that determines how and if a query should be cached
3
+ */
4
+ export interface CacheEvaluation {
5
+ /**
6
+ * The cache key to use for storing/retrieving the value
7
+ */
8
+ key: string;
9
+
10
+ /**
11
+ * How long to cache the value in seconds
12
+ */
13
+ cacheDurationInSeconds: number;
14
+
15
+ /**
16
+ * Whether this query result can be cached
17
+ */
18
+ canCache: boolean;
19
+ }
@@ -0,0 +1,38 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Generic cache interface that can be implemented by different cache backends
5
+ * (Redis, memory, file-based, etc.)
6
+ */
7
+ export interface Cache {
8
+ /**
9
+ * Retrieves a value from cache and validates it against the provided schema
10
+ */
11
+ get<T>(key: string, schema: z.ZodType<T>): Promise<T | null>;
12
+
13
+ /**
14
+ * Stores a value in cache with optional expiration time
15
+ */
16
+ put(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
17
+
18
+ /**
19
+ * Removes one or more keys from cache
20
+ * Supports wildcard patterns (implementation dependent)
21
+ */
22
+ del(keys: string | string[]): Promise<void>;
23
+
24
+ /**
25
+ * Finds all keys matching a pattern (implementation dependent)
26
+ */
27
+ keys(pattern: string): Promise<string[]>;
28
+
29
+ /**
30
+ * Clears all cache entries or entries matching a pattern
31
+ */
32
+ clear(pattern?: string): Promise<void>;
33
+
34
+ /**
35
+ * Gets basic cache statistics (implementation dependent)
36
+ */
37
+ getStats(): Promise<{ hits: number; misses: number; size: number }>;
38
+ }
@@ -0,0 +1,42 @@
1
+ import { Cache } from './cache.interface';
2
+ import z from 'zod';
3
+
4
+ /**
5
+ * No-op cache implementation that never stores or returns data.
6
+ * Useful for testing or when caching should be disabled.
7
+ */
8
+ export class NoOpCache implements Cache {
9
+ public async get<T>(_key: string, _schema: z.ZodType<T>): Promise<T | null> {
10
+ // Always return null - never a cache hit
11
+ return null;
12
+ }
13
+
14
+ public async put(_key: string, _value: unknown, _ttlSeconds?: number): Promise<void> {
15
+ // No-op - silently ignore cache write requests
16
+ return;
17
+ }
18
+
19
+ public async del(_keys: string | string[]): Promise<void> {
20
+ // No-op - silently ignore delete requests
21
+ return;
22
+ }
23
+
24
+ public async keys(_pattern: string): Promise<string[]> {
25
+ // Always return empty array
26
+ return [];
27
+ }
28
+
29
+ public async clear(_pattern?: string): Promise<void> {
30
+ // No-op - silently ignore clear requests
31
+ return;
32
+ }
33
+
34
+ public async getStats(): Promise<{ hits: number; misses: number; size: number }> {
35
+ // Always return zeros
36
+ return {
37
+ hits: 0,
38
+ misses: 0,
39
+ size: 0
40
+ };
41
+ }
42
+ }
@@ -1,41 +1,74 @@
1
1
  import { Redis } from '@upstash/redis';
2
- import { CachingStrategy } from './caching-strategy';
3
- import { BaseQuery } from '../schemas/queries/base.query';
4
- import { Session } from '../schemas/session.schema';
5
- import { BaseModel } from '../schemas/models/base.model';
2
+ import { Cache } from './cache.interface';
6
3
  import z from 'zod';
7
4
 
8
- export class RedisCache {
9
- protected strategy: CachingStrategy;
5
+ export class RedisCache implements Cache {
10
6
  protected redis: Redis;
11
7
 
12
- constructor(strategy: CachingStrategy) {
13
- this.strategy = strategy;
8
+ constructor() {
14
9
  this.redis = Redis.fromEnv();
15
10
  }
16
11
 
17
- public async get<T extends BaseModel>(query: BaseQuery, session: Session, schema: z.ZodType<T>): Promise<T | null> {
18
- let result = null;
12
+ public async get<T>(key: string, schema: z.ZodType<T>): Promise<T | null> {
13
+ if (!key) {
14
+ return null;
15
+ }
19
16
 
20
- const cacheInformation = this.strategy.get(query, session);
17
+ const unvalidated = await this.redis.get(key);
18
+ const parsed = schema.safeParse(unvalidated);
19
+
20
+ if (parsed.success) {
21
+ return parsed.data;
22
+ }
21
23
 
22
- if (cacheInformation.canCache && cacheInformation.key) {
23
- const unvalidated = await this.redis.get(cacheInformation.key);
24
- const parsed = schema.safeParse(unvalidated);
24
+ return null;
25
+ }
25
26
 
26
- if (parsed.success) {
27
- result = parsed.data;
28
- }
27
+ public async put(key: string, value: unknown, ttlSeconds?: number): Promise<void> {
28
+ if (!key) {
29
+ return;
29
30
  }
31
+
32
+ const options = ttlSeconds ? { ex: ttlSeconds } : undefined;
33
+ await this.redis.set(key, value, options);
34
+ }
35
+
36
+ public async del(keys: string | string[]): Promise<void> {
37
+ const keyArray = Array.isArray(keys) ? keys : [keys];
30
38
 
31
- return result;
39
+ for (const key of keyArray) {
40
+ if (key.includes('*')) {
41
+ // Handle wildcard patterns
42
+ const matchingKeys = await this.redis.keys(key);
43
+ if (matchingKeys.length > 0) {
44
+ await this.redis.del(...matchingKeys);
45
+ }
46
+ } else {
47
+ // Delete specific key
48
+ await this.redis.del(key);
49
+ }
50
+ }
32
51
  }
33
52
 
34
- public put(query: BaseQuery, session: Session, value: unknown): void {
35
- const cacheInformation = this.strategy.get(query, session);
53
+ public async keys(pattern: string): Promise<string[]> {
54
+ return await this.redis.keys(pattern);
55
+ }
36
56
 
37
- if (cacheInformation.canCache && cacheInformation.key) {
38
- this.redis.set(cacheInformation.key, value, { ex: cacheInformation.cacheDurationInSeconds });
57
+ public async clear(pattern?: string): Promise<void> {
58
+ const searchPattern = pattern || '*';
59
+ const keys = await this.redis.keys(searchPattern);
60
+ if (keys.length > 0) {
61
+ await this.redis.del(...keys);
39
62
  }
40
63
  }
64
+
65
+ public async getStats(): Promise<{ hits: number; misses: number; size: number }> {
66
+ // Basic cache statistics (could be enhanced with actual Redis metrics)
67
+ const keys = await this.redis.keys('*');
68
+ return {
69
+ hits: 0, // Would need to track this separately
70
+ misses: 0, // Would need to track this separately
71
+ size: keys.length
72
+ };
73
+ }
41
74
  }
@@ -5,6 +5,8 @@ import { IdentityProvider } from '../providers/identity.provider';
5
5
  import { CartProvider } from "../providers/cart.provider";
6
6
  import { PriceProvider } from "../providers/price.provider";
7
7
  import { InventoryProvider } from "../providers/inventory.provider";
8
+ import { Cache } from "../cache/cache.interface";
9
+ import { RedisCache } from "../cache/redis-cache";
8
10
 
9
11
  export interface Client {
10
12
  product: ProductProvider,
@@ -17,12 +19,23 @@ export interface Client {
17
19
  inventory: InventoryProvider
18
20
  }
19
21
 
20
- export function buildClient<T extends Partial<Client>>(providers: Array<T>): Required<T> {
22
+ export interface BuildClientOptions {
23
+ cache?: Cache;
24
+ }
25
+
26
+ export function buildClient<T extends Partial<Client>>(
27
+ providerFactories: Array<(cache: Cache) => T>,
28
+ options: BuildClientOptions = {}
29
+ ): Required<T> {
21
30
  let client = { } as Required<T>;
22
31
 
32
+ // Create shared cache instance
33
+ const sharedCache = options.cache || new RedisCache();
34
+
23
35
  const mergedAnalytics = [];
24
36
 
25
- for (const provider of providers) {
37
+ for (const factory of providerFactories) {
38
+ const provider = factory(sharedCache);
26
39
  client = {
27
40
  ...client,
28
41
  ...provider
@@ -34,6 +47,17 @@ export function buildClient<T extends Partial<Client>>(providers: Array<T>): Req
34
47
  }
35
48
 
36
49
  client.analytics = mergedAnalytics;
50
+
51
+ // Add cache to complete the client
52
+ const completeClient = {
53
+ ...client,
54
+ cache: sharedCache
55
+ } as Required<T>;
56
+
57
+ return completeClient;
58
+ }
37
59
 
38
- return client satisfies T;
60
+ // Convenience function to create a shared cache instance
61
+ export function createCache(): Cache {
62
+ return new RedisCache();
39
63
  }
package/core/src/index.ts CHANGED
@@ -1,5 +1,7 @@
1
- export * from './cache/caching-strategy';
1
+ export * from './cache/cache.interface';
2
+ export * from './cache/cache-evaluation.interface';
2
3
  export * from './cache/redis-cache';
4
+ export * from './cache/noop-cache';
3
5
 
4
6
  export * from './client/client';
5
7
 
@@ -2,9 +2,32 @@ import { AnalyticsEvent } from '../schemas/models/analytics.model';
2
2
  import { AnalyticsMutation } from '../schemas/mutations/analytics.mutation';
3
3
  import { AnalyticsQuery } from '../schemas/queries/analytics.query';
4
4
  import { BaseProvider } from './base.provider';
5
+ import { CacheEvaluation } from '../cache/cache-evaluation.interface';
6
+ import { Session } from '../schemas/session.schema';
7
+ import * as crypto from 'crypto';
5
8
 
6
9
  export abstract class AnalyticsProvider<
7
10
  T extends AnalyticsEvent = AnalyticsEvent,
8
11
  Q extends AnalyticsQuery = AnalyticsQuery,
9
12
  M extends AnalyticsMutation = AnalyticsMutation
10
- > extends BaseProvider<T, Q, M> {}
13
+ > extends BaseProvider<T, Q, M> {
14
+
15
+ protected override getCacheEvaluation(query: Q, _session: Session): CacheEvaluation {
16
+ const providerName = this.constructor.name.toLowerCase();
17
+
18
+ // Hash analytics query parameters
19
+ const relevantFields = {
20
+ type: typeof query === 'object' && query !== null && 'type' in query ? (query as any).type : undefined,
21
+ dateRange: typeof query === 'object' && query !== null && 'dateRange' in query ? (query as any).dateRange : undefined,
22
+ filters: typeof query === 'object' && query !== null && 'filters' in query ? (query as any).filters : undefined
23
+ };
24
+ const analyticsHash = crypto.createHash('md5').update(JSON.stringify(relevantFields)).digest('hex').substring(0, 12);
25
+ const key = `${providerName}:analytics:${analyticsHash}`;
26
+
27
+ return {
28
+ key,
29
+ cacheDurationInSeconds: 0,
30
+ canCache: false
31
+ };
32
+ }
33
+ }
@@ -4,6 +4,9 @@ import { BaseQuery } from '../schemas/queries/base.query';
4
4
  import { BaseMutation } from '../schemas/mutations/base.mutation';
5
5
  import { BaseModel } from '../schemas/models/base.model';
6
6
  import { createProviderInstrumentation } from '@reactionary/otel';
7
+ import { Cache } from '../cache/cache.interface';
8
+ import { CacheEvaluation } from '../cache/cache-evaluation.interface';
9
+ import * as crypto from 'crypto';
7
10
 
8
11
  /**
9
12
  * Base capability provider, responsible for mutations (changes) and queries (fetches)
@@ -14,10 +17,17 @@ export abstract class BaseProvider<
14
17
  Q extends BaseQuery = BaseQuery,
15
18
  M extends BaseMutation = BaseMutation
16
19
  > {
17
- private instrumentation: ReturnType<typeof createProviderInstrumentation>;
20
+ protected instrumentation: ReturnType<typeof createProviderInstrumentation>;
21
+ protected cache: Cache;
18
22
 
19
- constructor(public readonly schema: z.ZodType<T>, public readonly querySchema: z.ZodType<Q, Q>, public readonly mutationSchema: z.ZodType<M, M>) {
23
+ constructor(
24
+ public readonly schema: z.ZodType<T>,
25
+ public readonly querySchema: z.ZodType<Q, Q>,
26
+ public readonly mutationSchema: z.ZodType<M, M>,
27
+ cache: Cache
28
+ ) {
20
29
  this.instrumentation = createProviderInstrumentation(this.constructor.name);
30
+ this.cache = cache;
21
31
  }
22
32
 
23
33
  /**
@@ -46,13 +56,60 @@ export abstract class BaseProvider<
46
56
  'query',
47
57
  async (span) => {
48
58
  span.setAttribute('provider.query.count', queries.length);
49
- const results = await this.fetch(queries, session);
59
+
60
+ let cacheHits = 0;
61
+ let cacheMisses = 0;
62
+ const results: T[] = [];
50
63
 
51
- for (const result of results) {
52
- this.assert(result);
64
+ // Process each query individually for cache efficiency
65
+ for (const query of queries) {
66
+ let result: T | null = null;
67
+
68
+ // Get cache evaluation from provider
69
+ const cacheInfo = this.getCacheEvaluation(query, session);
70
+
71
+ // Try cache first if caching is enabled
72
+ if (cacheInfo.canCache && cacheInfo.key) {
73
+ try {
74
+ result = await this.cache.get(cacheInfo.key, this.schema);
75
+ if (result) {
76
+ cacheHits++;
77
+ span.setAttribute('provider.cache.hit', true);
78
+ }
79
+ } catch (error) {
80
+ // Cache error shouldn't break the query - log and continue
81
+ console.warn(`Cache get error for ${this.constructor.name}:`, error);
82
+ }
83
+ }
84
+
85
+ // If not in cache, fetch from source
86
+ if (!result) {
87
+ const singleResult = await this.fetch([query], session);
88
+ result = singleResult[0];
89
+ cacheMisses++;
90
+
91
+ // Store in cache if caching is enabled
92
+ if (result && cacheInfo.canCache && cacheInfo.key) {
93
+ try {
94
+ await this.cache.put(cacheInfo.key, result, cacheInfo.cacheDurationInSeconds);
95
+ } catch (error) {
96
+ // Cache put error shouldn't break the query - log and continue
97
+ console.warn(`Cache put error for ${this.constructor.name}:`, error);
98
+ }
99
+ }
100
+ }
101
+
102
+ if (result) {
103
+ this.assert(result);
104
+ results.push(result);
105
+ }
53
106
  }
54
107
 
55
108
  span.setAttribute('provider.result.count', results.length);
109
+ span.setAttribute('provider.cache.hits', cacheHits);
110
+ span.setAttribute('provider.cache.misses', cacheMisses);
111
+ span.setAttribute('provider.cache.hit_ratio', cacheHits / (cacheHits + cacheMisses));
112
+
56
113
  return results;
57
114
  },
58
115
  { queryCount: queries.length }
@@ -68,8 +125,9 @@ export abstract class BaseProvider<
68
125
  'mutate',
69
126
  async (span) => {
70
127
  span.setAttribute('provider.mutation.count', mutations.length);
128
+
129
+ // Perform the mutation
71
130
  const result = await this.process(mutations, session);
72
-
73
131
  this.assert(result);
74
132
 
75
133
  return result;
@@ -92,4 +150,24 @@ export abstract class BaseProvider<
92
150
  mutations: M[],
93
151
  session: Session
94
152
  ): Promise<T>;
153
+
154
+ /**
155
+ * Provider-specific cache evaluation logic.
156
+ * Returns information about how this query should be cached.
157
+ * Override this method to enable caching with custom keys and TTL.
158
+ * Default implementation returns no caching.
159
+ */
160
+ protected getCacheEvaluation(query: Q, session: Session): CacheEvaluation {
161
+ // Default implementation: generate a cache key but don't cache
162
+ const providerName = this.constructor.name.toLowerCase();
163
+ const userId = session.identity?.id || 'anonymous';
164
+ const queryHash = crypto.createHash('md5').update(JSON.stringify(query)).digest('hex').substring(0, 12);
165
+ const key = `${providerName}:${userId}:${queryHash}`;
166
+
167
+ return {
168
+ key,
169
+ cacheDurationInSeconds: 0,
170
+ canCache: false
171
+ };
172
+ }
95
173
  }
@@ -2,9 +2,26 @@ import { CartQuery } from "../schemas/queries/cart.query";
2
2
  import { CartMutation } from "../schemas/mutations/cart.mutation";
3
3
  import { BaseProvider } from "./base.provider";
4
4
  import { Cart } from "../schemas/models/cart.model";
5
+ import { CacheEvaluation } from '../cache/cache-evaluation.interface';
6
+ import { Session } from '../schemas/session.schema';
7
+ import * as crypto from 'crypto';
5
8
 
6
9
  export abstract class CartProvider<
7
10
  T extends Cart = Cart,
8
11
  Q extends CartQuery = CartQuery,
9
12
  M extends CartMutation = CartMutation
10
- > extends BaseProvider<T, Q, M> {}
13
+ > extends BaseProvider<T, Q, M> {
14
+
15
+ protected override getCacheEvaluation(query: Q, session: Session): CacheEvaluation {
16
+ const providerName = this.constructor.name.toLowerCase();
17
+ const userId = session.identity?.id || 'anonymous';
18
+ const queryHash = crypto.createHash('md5').update(JSON.stringify(query)).digest('hex').substring(0, 12);
19
+ const key = `${providerName}:cart:${userId}:${queryHash}`;
20
+
21
+ return {
22
+ key,
23
+ cacheDurationInSeconds: 0,
24
+ canCache: false
25
+ };
26
+ }
27
+ }
@@ -2,9 +2,26 @@ import { Identity } from "../schemas/models/identity.model";
2
2
  import { IdentityQuery } from "../schemas/queries/identity.query";
3
3
  import { IdentityMutation } from "../schemas/mutations/identity.mutation";
4
4
  import { BaseProvider } from "./base.provider";
5
+ import { CacheEvaluation } from '../cache/cache-evaluation.interface';
6
+ import { Session } from '../schemas/session.schema';
5
7
 
6
8
  export abstract class IdentityProvider<
7
9
  T extends Identity = Identity,
8
10
  Q extends IdentityQuery = IdentityQuery,
9
11
  M extends IdentityMutation = IdentityMutation
10
- > extends BaseProvider<T, Q, M> {}
12
+ > extends BaseProvider<T, Q, M> {
13
+
14
+ protected override getCacheEvaluation(_query: Q, session: Session): CacheEvaluation {
15
+ const providerName = this.constructor.name.toLowerCase();
16
+ const userId = session.identity?.id;
17
+ const key = userId
18
+ ? `${providerName}:identity:${userId}`
19
+ : `${providerName}:identity:anonymous`;
20
+
21
+ return {
22
+ key,
23
+ cacheDurationInSeconds: 0,
24
+ canCache: false
25
+ };
26
+ }
27
+ }
@@ -1,11 +1,24 @@
1
- import z from 'zod';
2
1
  import { Inventory } from '../schemas/models/inventory.model';
3
2
  import { InventoryQuery } from '../schemas/queries/inventory.query';
4
3
  import { InventoryMutation } from '../schemas/mutations/inventory.mutation';
5
4
  import { BaseProvider } from './base.provider';
5
+ import { CacheEvaluation } from '../cache/cache-evaluation.interface';
6
+ import { Session } from '../schemas/session.schema';
6
7
 
7
8
  export abstract class InventoryProvider<
8
9
  T extends Inventory = Inventory,
9
10
  Q extends InventoryQuery = InventoryQuery,
10
11
  M extends InventoryMutation = InventoryMutation
11
- > extends BaseProvider<T, Q, M> {}
12
+ > extends BaseProvider<T, Q, M> {
13
+
14
+ protected override getCacheEvaluation(query: Q, _session: Session): CacheEvaluation {
15
+ const providerName = this.constructor.name.toLowerCase();
16
+ const key = `${providerName}:inventory:${query.sku}`;
17
+
18
+ return {
19
+ key,
20
+ cacheDurationInSeconds: 0,
21
+ canCache: false
22
+ };
23
+ }
24
+ }
@@ -2,9 +2,26 @@ import { Price } from '../schemas/models/price.model';
2
2
  import { PriceMutation } from '../schemas/mutations/price.mutation';
3
3
  import { PriceQuery } from '../schemas/queries/price.query';
4
4
  import { BaseProvider } from './base.provider';
5
+ import { CacheEvaluation } from '../cache/cache-evaluation.interface';
6
+ import { Session } from '../schemas/session.schema';
7
+ import * as crypto from 'crypto';
5
8
 
6
9
  export abstract class PriceProvider<
7
10
  T extends Price = Price,
8
11
  Q extends PriceQuery = PriceQuery,
9
12
  M extends PriceMutation = PriceMutation
10
- > extends BaseProvider<T, Q, M> {}
13
+ > extends BaseProvider<T, Q, M> {
14
+
15
+ protected override getCacheEvaluation(query: Q, _session: Session): CacheEvaluation {
16
+ const providerName = this.constructor.name.toLowerCase();
17
+ // Hash complex SKU objects (price queries with currency, customer group, etc.)
18
+ const skuHash = crypto.createHash('md5').update(JSON.stringify(query.sku)).digest('hex').substring(0, 12);
19
+ const key = `${providerName}:price:${skuHash}`;
20
+
21
+ return {
22
+ key,
23
+ cacheDurationInSeconds: 0,
24
+ canCache: false
25
+ };
26
+ }
27
+ }
@@ -1,11 +1,34 @@
1
1
  import { Product } from '../schemas/models/product.model';
2
2
  import { ProductMutation } from '../schemas/mutations/product.mutation';
3
- import { ProductQuery } from '../schemas/queries/product.query';
3
+ import { ProductQuery, ProductQueryById, ProductQueryBySlug } from '../schemas/queries/product.query';
4
4
  import { BaseProvider } from './base.provider';
5
+ import { Session } from '../schemas/session.schema';
6
+ import { CacheEvaluation } from '../cache/cache-evaluation.interface';
7
+ import * as crypto from 'crypto';
5
8
 
6
9
  export abstract class ProductProvider<
7
10
  T extends Product = Product,
8
11
  Q extends ProductQuery = ProductQuery,
9
12
  M extends ProductMutation = ProductMutation
10
13
  > extends BaseProvider<T, Q, M> {
14
+
15
+ protected override getCacheEvaluation(query: Q, _session: Session): CacheEvaluation {
16
+ const providerName = this.constructor.name.toLowerCase();
17
+
18
+ let key: string;
19
+ if (query.query === 'slug') {
20
+ key = `${providerName}:product:slug:${(query as ProductQueryBySlug).slug}`;
21
+ } else if (query.query === 'id') {
22
+ key = `${providerName}:product:id:${(query as ProductQueryById).id}`;
23
+ } else {
24
+ const queryHash = crypto.createHash('md5').update(JSON.stringify(query)).digest('hex').substring(0, 12);
25
+ key = `${providerName}:product:${queryHash}`;
26
+ }
27
+
28
+ return {
29
+ key,
30
+ cacheDurationInSeconds: 300, // Products are moderately stable - 5 minutes
31
+ canCache: true
32
+ };
33
+ }
11
34
  }
@@ -2,11 +2,28 @@ import { SearchResult } from '../schemas/models/search.model';
2
2
  import { SearchQuery } from '../schemas/queries/search.query';
3
3
  import { SearchMutation } from '../schemas/mutations/search.mutation';
4
4
  import { BaseProvider } from './base.provider';
5
+ import { CacheEvaluation } from '../cache/cache-evaluation.interface';
6
+ import { Session } from '../schemas/session.schema';
7
+ import * as crypto from 'crypto';
5
8
 
6
9
  export abstract class SearchProvider<
7
10
  T extends SearchResult = SearchResult,
8
11
  Q extends SearchQuery = SearchQuery,
9
12
  M extends SearchMutation = SearchMutation
10
- > extends BaseProvider<T, Q, M> {}
13
+ > extends BaseProvider<T, Q, M> {
14
+
15
+ protected override getCacheEvaluation(query: Q, _session: Session): CacheEvaluation {
16
+ const providerName = this.constructor.name.toLowerCase();
17
+ // Hash search parameters (term, filters, pagination, etc.)
18
+ const searchHash = crypto.createHash('md5').update(JSON.stringify(query.search)).digest('hex').substring(0, 12);
19
+ const key = `${providerName}:search:${searchHash}`;
20
+
21
+ return {
22
+ key,
23
+ cacheDurationInSeconds: 0,
24
+ canCache: false
25
+ };
26
+ }
27
+ }
11
28
 
12
29
 
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { BaseMutationSchema } from './base.mutation';
3
2
 
4
3
  export const ProductMutationSchema = z.union([]);
5
4
 
@@ -3,8 +3,8 @@ import { CustomProduct, CustomProductSchema } from '../schemas/custom-product.sc
3
3
  import { ProductMutationSchema, ProductQuerySchema } from '@reactionary/core';
4
4
 
5
5
  export class CustomAlgoliaProductProvider extends AlgoliaProductProvider<CustomProduct> {
6
- constructor(config: AlgoliaConfiguration) {
7
- super(config, CustomProductSchema, ProductQuerySchema, ProductMutationSchema);
6
+ constructor(config: AlgoliaConfiguration, cache: any) {
7
+ super(config, CustomProductSchema, ProductQuerySchema, ProductMutationSchema, cache);
8
8
  }
9
9
 
10
10
  public parse(data: any): CustomProduct {
@@ -5,7 +5,6 @@
5
5
  # Service identification
6
6
  OTEL_SERVICE_NAME=reactionary-trpc-example
7
7
  OTEL_SERVICE_VERSION=1.0.0
8
- DEPLOYMENT_ENVIRONMENT=development
9
8
 
10
9
  # Traces exporter: console | otlp | otlp/http | none
11
10
  OTEL_TRACES_EXPORTER=console
@@ -16,6 +15,7 @@ OTEL_METRICS_EXPORTER=console
16
15
  # For OTLP exporters (e.g., Honeycomb, Jaeger, Grafana)
17
16
  # OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io
18
17
  # OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=your-api-key
18
+ # OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
19
19
 
20
20
  # Or use specific endpoints for traces/metrics
21
21
  # OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://api.honeycomb.io/v1/traces
@@ -54,10 +54,6 @@ app.use(
54
54
 
55
55
  const server = app.listen(3000, () => {
56
56
  console.log('Server started on http://localhost:3000');
57
- // OTEL auto-initializes based on standard env vars
58
- if (process.env['OTEL_LOG_LEVEL'] === 'debug') {
59
- console.log('OTEL traces exporter:', process.env['OTEL_TRACES_EXPORTER'] || 'console');
60
- console.log('OTEL metrics exporter:', process.env['OTEL_METRICS_EXPORTER'] || 'console');
61
- }
57
+ // OpenTelemetry automatically initializes based on OTEL_* environment variables
62
58
  });
63
59