@reactionary/core 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.
package/index.js CHANGED
@@ -1,47 +1,95 @@
1
- // core/src/cache/caching-strategy.ts
2
- var BaseCachingStrategy = class {
3
- get(query, session) {
4
- const q = query;
5
- return {
6
- key: q.sku,
7
- cacheDurationInSeconds: 300,
8
- canCache: true
9
- };
10
- }
11
- };
12
-
13
1
  // core/src/cache/redis-cache.ts
14
2
  import { Redis } from "@upstash/redis";
15
3
  var RedisCache = class {
16
- constructor(strategy) {
17
- this.strategy = strategy;
4
+ constructor() {
18
5
  this.redis = Redis.fromEnv();
19
6
  }
20
- async get(query, session, schema) {
21
- let result = null;
22
- const cacheInformation = this.strategy.get(query, session);
23
- if (cacheInformation.canCache && cacheInformation.key) {
24
- const unvalidated = await this.redis.get(cacheInformation.key);
25
- const parsed = schema.safeParse(unvalidated);
26
- if (parsed.success) {
27
- result = parsed.data;
7
+ async get(key, schema) {
8
+ if (!key) {
9
+ return null;
10
+ }
11
+ const unvalidated = await this.redis.get(key);
12
+ const parsed = schema.safeParse(unvalidated);
13
+ if (parsed.success) {
14
+ return parsed.data;
15
+ }
16
+ return null;
17
+ }
18
+ async put(key, value, ttlSeconds) {
19
+ if (!key) {
20
+ return;
21
+ }
22
+ const options = ttlSeconds ? { ex: ttlSeconds } : void 0;
23
+ await this.redis.set(key, value, options);
24
+ }
25
+ async del(keys) {
26
+ const keyArray = Array.isArray(keys) ? keys : [keys];
27
+ for (const key of keyArray) {
28
+ if (key.includes("*")) {
29
+ const matchingKeys = await this.redis.keys(key);
30
+ if (matchingKeys.length > 0) {
31
+ await this.redis.del(...matchingKeys);
32
+ }
33
+ } else {
34
+ await this.redis.del(key);
28
35
  }
29
36
  }
30
- return result;
31
37
  }
32
- put(query, session, value) {
33
- const cacheInformation = this.strategy.get(query, session);
34
- if (cacheInformation.canCache && cacheInformation.key) {
35
- this.redis.set(cacheInformation.key, value, { ex: cacheInformation.cacheDurationInSeconds });
38
+ async keys(pattern) {
39
+ return await this.redis.keys(pattern);
40
+ }
41
+ async clear(pattern) {
42
+ const searchPattern = pattern || "*";
43
+ const keys = await this.redis.keys(searchPattern);
44
+ if (keys.length > 0) {
45
+ await this.redis.del(...keys);
36
46
  }
37
47
  }
48
+ async getStats() {
49
+ const keys = await this.redis.keys("*");
50
+ return {
51
+ hits: 0,
52
+ // Would need to track this separately
53
+ misses: 0,
54
+ // Would need to track this separately
55
+ size: keys.length
56
+ };
57
+ }
58
+ };
59
+
60
+ // core/src/cache/noop-cache.ts
61
+ var NoOpCache = class {
62
+ async get(_key, _schema) {
63
+ return null;
64
+ }
65
+ async put(_key, _value, _ttlSeconds) {
66
+ return;
67
+ }
68
+ async del(_keys) {
69
+ return;
70
+ }
71
+ async keys(_pattern) {
72
+ return [];
73
+ }
74
+ async clear(_pattern) {
75
+ return;
76
+ }
77
+ async getStats() {
78
+ return {
79
+ hits: 0,
80
+ misses: 0,
81
+ size: 0
82
+ };
83
+ }
38
84
  };
39
85
 
40
86
  // core/src/client/client.ts
41
- function buildClient(providers) {
87
+ function buildClient(providerFactories, options = {}) {
42
88
  let client = {};
89
+ const sharedCache = options.cache || new RedisCache();
43
90
  const mergedAnalytics = [];
44
- for (const provider of providers) {
91
+ for (const factory of providerFactories) {
92
+ const provider = factory(sharedCache);
45
93
  client = {
46
94
  ...client,
47
95
  ...provider
@@ -51,184 +99,48 @@ function buildClient(providers) {
51
99
  }
52
100
  }
53
101
  client.analytics = mergedAnalytics;
54
- return client;
102
+ const completeClient = {
103
+ ...client,
104
+ cache: sharedCache
105
+ };
106
+ return completeClient;
107
+ }
108
+ function createCache() {
109
+ return new RedisCache();
55
110
  }
56
111
 
57
- // otel/src/sdk.ts
58
- import { NodeSDK } from "@opentelemetry/sdk-node";
59
- import { resourceFromAttributes, defaultResource } from "@opentelemetry/resources";
60
- import { SEMRESATTRS_SERVICE_NAME, SEMRESATTRS_SERVICE_VERSION, SEMRESATTRS_DEPLOYMENT_ENVIRONMENT } from "@opentelemetry/semantic-conventions";
112
+ // otel/src/trpc-middleware.ts
113
+ import { TRPCError } from "@trpc/server";
61
114
  import {
62
- BatchSpanProcessor
63
- } from "@opentelemetry/sdk-trace-base";
64
- import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
65
- import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express";
115
+ SpanKind as SpanKind2,
116
+ SpanStatusCode as SpanStatusCode3
117
+ } from "@opentelemetry/api";
66
118
 
67
- // otel/src/config.ts
68
- import { ConsoleSpanExporter } from "@opentelemetry/sdk-trace-base";
69
- import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
70
- import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
71
- import { PeriodicExportingMetricReader, ConsoleMetricExporter } from "@opentelemetry/sdk-metrics";
72
- function isBrowser() {
73
- return typeof window !== "undefined" && typeof process === "undefined";
74
- }
75
- function getConfigFromEnv() {
76
- if (isBrowser()) {
77
- return {
78
- serviceName: "browser-service",
79
- serviceVersion: void 0,
80
- environment: "browser",
81
- otlpEndpoint: void 0,
82
- otlpHeaders: void 0,
83
- traceEnabled: false,
84
- metricsEnabled: false,
85
- metricExportIntervalMillis: 6e4
86
- };
87
- }
88
- const tracesExporter = process.env["OTEL_TRACES_EXPORTER"] || "console";
89
- const metricsExporter = process.env["OTEL_METRICS_EXPORTER"] || "console";
90
- return {
91
- serviceName: process.env["OTEL_SERVICE_NAME"] || "unknown_service",
92
- serviceVersion: process.env["OTEL_SERVICE_VERSION"],
93
- environment: process.env["DEPLOYMENT_ENVIRONMENT"] || process.env["NODE_ENV"] || "development",
94
- otlpEndpoint: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"],
95
- otlpHeaders: process.env["OTEL_EXPORTER_OTLP_HEADERS"] ? parseHeaders(process.env["OTEL_EXPORTER_OTLP_HEADERS"]) : void 0,
96
- traceEnabled: tracesExporter !== "none",
97
- metricsEnabled: metricsExporter !== "none",
98
- metricExportIntervalMillis: process.env["OTEL_METRIC_EXPORT_INTERVAL"] ? parseInt(process.env["OTEL_METRIC_EXPORT_INTERVAL"], 10) : 6e4
99
- // Default 60 seconds per OTEL spec
100
- };
101
- }
102
- function parseHeaders(headerString) {
103
- const headers = {};
104
- headerString.split(",").forEach((header) => {
105
- const [key, value] = header.split("=");
106
- if (key && value) {
107
- headers[key.trim()] = value.trim();
108
- }
109
- });
110
- return headers;
111
- }
112
- function createTraceExporter(config) {
113
- if (!config.traceEnabled) {
114
- return void 0;
115
- }
116
- const tracesExporter = process.env["OTEL_TRACES_EXPORTER"] || "console";
117
- switch (tracesExporter) {
118
- case "otlp":
119
- case "otlp/http": {
120
- if (!config.otlpEndpoint) {
121
- console.warn("OTEL_EXPORTER_OTLP_ENDPOINT not set, falling back to console");
122
- return new ConsoleSpanExporter();
123
- }
124
- const tracesEndpoint = process.env["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"] || `${config.otlpEndpoint}/v1/traces`;
125
- return new OTLPTraceExporter({
126
- url: tracesEndpoint,
127
- headers: config.otlpHeaders
128
- });
129
- }
130
- case "console":
131
- return new ConsoleSpanExporter();
132
- case "none":
133
- default:
134
- return void 0;
135
- }
136
- }
137
- function createMetricReader(config) {
138
- if (!config.metricsEnabled) {
139
- return void 0;
140
- }
141
- const metricsExporter = process.env["OTEL_METRICS_EXPORTER"] || "console";
142
- let exporter;
143
- switch (metricsExporter) {
144
- case "otlp":
145
- case "otlp/http": {
146
- if (!config.otlpEndpoint) {
147
- console.warn("OTEL_EXPORTER_OTLP_ENDPOINT not set, falling back to console");
148
- exporter = new ConsoleMetricExporter();
149
- } else {
150
- const metricsEndpoint = process.env["OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"] || `${config.otlpEndpoint}/v1/metrics`;
151
- exporter = new OTLPMetricExporter({
152
- url: metricsEndpoint,
153
- headers: config.otlpHeaders
154
- });
155
- }
156
- break;
157
- }
158
- case "console":
159
- exporter = new ConsoleMetricExporter();
160
- break;
161
- case "none":
162
- default:
163
- return void 0;
164
- }
165
- return new PeriodicExportingMetricReader({
166
- exporter,
167
- exportIntervalMillis: config.metricExportIntervalMillis
168
- });
169
- }
119
+ // otel/src/tracer.ts
120
+ import {
121
+ trace,
122
+ SpanStatusCode,
123
+ context as otelContext
124
+ } from "@opentelemetry/api";
170
125
 
171
126
  // otel/src/sdk.ts
127
+ import { NodeSDK } from "@opentelemetry/sdk-node";
172
128
  var sdk = null;
173
129
  var isInitialized = false;
174
130
  var initializationPromise = null;
175
- function isBrowser2() {
131
+ function isBrowser() {
176
132
  return typeof window !== "undefined" && typeof process === "undefined";
177
133
  }
178
134
  function ensureInitialized() {
179
135
  if (isInitialized || initializationPromise) {
180
136
  return;
181
137
  }
182
- if (isBrowser2()) {
138
+ if (isBrowser()) {
183
139
  isInitialized = true;
184
140
  return;
185
141
  }
186
142
  initializationPromise = Promise.resolve().then(() => {
187
- const config = getConfigFromEnv();
188
- if (process.env["OTEL_LOG_LEVEL"] === "debug") {
189
- console.log("OpenTelemetry auto-initializing with config:", {
190
- serviceName: config.serviceName,
191
- environment: config.environment,
192
- tracesExporter: process.env["OTEL_TRACES_EXPORTER"] || "console",
193
- metricsExporter: process.env["OTEL_METRICS_EXPORTER"] || "console"
194
- });
195
- }
196
- const attributes = {
197
- [SEMRESATTRS_SERVICE_NAME]: config.serviceName
198
- };
199
- if (config.serviceVersion) {
200
- attributes[SEMRESATTRS_SERVICE_VERSION] = config.serviceVersion;
201
- }
202
- if (config.environment) {
203
- attributes[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT] = config.environment;
204
- }
205
- const customResource = resourceFromAttributes(attributes);
206
- const resource = defaultResource().merge(customResource);
207
- const traceExporter = createTraceExporter(config);
208
- const metricReader = createMetricReader(config);
209
- const instrumentations = [
210
- new HttpInstrumentation({
211
- requestHook: (span, request) => {
212
- const req = request;
213
- if (req.headers) {
214
- span.setAttribute("http.request.body.size", req.headers["content-length"] || 0);
215
- }
216
- },
217
- responseHook: (span, response) => {
218
- const res = response;
219
- if (res.headers) {
220
- span.setAttribute("http.response.body.size", res.headers["content-length"] || 0);
221
- }
222
- }
223
- }),
224
- new ExpressInstrumentation()
225
- ];
226
- sdk = new NodeSDK({
227
- resource,
228
- spanProcessors: traceExporter ? [new BatchSpanProcessor(traceExporter)] : [],
229
- metricReader,
230
- instrumentations
231
- });
143
+ sdk = new NodeSDK();
232
144
  sdk.start();
233
145
  isInitialized = true;
234
146
  process.on("SIGTERM", async () => {
@@ -256,11 +168,6 @@ function isOtelInitialized() {
256
168
  }
257
169
 
258
170
  // otel/src/tracer.ts
259
- import {
260
- trace,
261
- SpanStatusCode,
262
- context as otelContext
263
- } from "@opentelemetry/api";
264
171
  import { SpanKind, SpanStatusCode as SpanStatusCode2 } from "@opentelemetry/api";
265
172
  var TRACER_NAME = "@reactionary/otel";
266
173
  var TRACER_VERSION = "0.0.1";
@@ -356,13 +263,6 @@ function getMetrics() {
356
263
  return metricsInstance;
357
264
  }
358
265
 
359
- // otel/src/trpc-middleware.ts
360
- import { TRPCError } from "@trpc/server";
361
- import {
362
- SpanKind as SpanKind2,
363
- SpanStatusCode as SpanStatusCode3
364
- } from "@opentelemetry/api";
365
-
366
266
  // otel/src/provider-instrumentation.ts
367
267
  import { SpanKind as SpanKind3 } from "@opentelemetry/api";
368
268
  async function withProviderSpan(options, fn) {
@@ -440,16 +340,15 @@ function createProviderInstrumentation(providerName) {
440
340
  };
441
341
  }
442
342
 
443
- // otel/src/index.ts
444
- import { trace as trace2, context, SpanKind as SpanKind4, SpanStatusCode as SpanStatusCode4 } from "@opentelemetry/api";
445
-
446
343
  // core/src/providers/base.provider.ts
344
+ import * as crypto from "crypto";
447
345
  var BaseProvider = class {
448
- constructor(schema, querySchema, mutationSchema) {
346
+ constructor(schema, querySchema, mutationSchema, cache) {
449
347
  this.schema = schema;
450
348
  this.querySchema = querySchema;
451
349
  this.mutationSchema = mutationSchema;
452
350
  this.instrumentation = createProviderInstrumentation(this.constructor.name);
351
+ this.cache = cache;
453
352
  }
454
353
  /**
455
354
  * Validates that the final domain model constructed by the provider
@@ -475,11 +374,44 @@ var BaseProvider = class {
475
374
  "query",
476
375
  async (span) => {
477
376
  span.setAttribute("provider.query.count", queries.length);
478
- const results = await this.fetch(queries, session);
479
- for (const result of results) {
480
- this.assert(result);
377
+ let cacheHits = 0;
378
+ let cacheMisses = 0;
379
+ const results = [];
380
+ for (const query of queries) {
381
+ let result = null;
382
+ const cacheInfo = this.getCacheEvaluation(query, session);
383
+ if (cacheInfo.canCache && cacheInfo.key) {
384
+ try {
385
+ result = await this.cache.get(cacheInfo.key, this.schema);
386
+ if (result) {
387
+ cacheHits++;
388
+ span.setAttribute("provider.cache.hit", true);
389
+ }
390
+ } catch (error) {
391
+ console.warn(`Cache get error for ${this.constructor.name}:`, error);
392
+ }
393
+ }
394
+ if (!result) {
395
+ const singleResult = await this.fetch([query], session);
396
+ result = singleResult[0];
397
+ cacheMisses++;
398
+ if (result && cacheInfo.canCache && cacheInfo.key) {
399
+ try {
400
+ await this.cache.put(cacheInfo.key, result, cacheInfo.cacheDurationInSeconds);
401
+ } catch (error) {
402
+ console.warn(`Cache put error for ${this.constructor.name}:`, error);
403
+ }
404
+ }
405
+ }
406
+ if (result) {
407
+ this.assert(result);
408
+ results.push(result);
409
+ }
481
410
  }
482
411
  span.setAttribute("provider.result.count", results.length);
412
+ span.setAttribute("provider.cache.hits", cacheHits);
413
+ span.setAttribute("provider.cache.misses", cacheMisses);
414
+ span.setAttribute("provider.cache.hit_ratio", cacheHits / (cacheHits + cacheMisses));
483
415
  return results;
484
416
  },
485
417
  { queryCount: queries.length }
@@ -501,34 +433,139 @@ var BaseProvider = class {
501
433
  { mutationCount: mutations.length }
502
434
  );
503
435
  }
436
+ /**
437
+ * Provider-specific cache evaluation logic.
438
+ * Returns information about how this query should be cached.
439
+ * Override this method to enable caching with custom keys and TTL.
440
+ * Default implementation returns no caching.
441
+ */
442
+ getCacheEvaluation(query, session) {
443
+ const providerName = this.constructor.name.toLowerCase();
444
+ const userId = session.identity?.id || "anonymous";
445
+ const queryHash = crypto.createHash("md5").update(JSON.stringify(query)).digest("hex").substring(0, 12);
446
+ const key = `${providerName}:${userId}:${queryHash}`;
447
+ return {
448
+ key,
449
+ cacheDurationInSeconds: 0,
450
+ canCache: false
451
+ };
452
+ }
504
453
  };
505
454
 
506
455
  // core/src/providers/analytics.provider.ts
456
+ import * as crypto2 from "crypto";
507
457
  var AnalyticsProvider = class extends BaseProvider {
458
+ getCacheEvaluation(query, _session) {
459
+ const providerName = this.constructor.name.toLowerCase();
460
+ const relevantFields = {
461
+ type: typeof query === "object" && query !== null && "type" in query ? query.type : void 0,
462
+ dateRange: typeof query === "object" && query !== null && "dateRange" in query ? query.dateRange : void 0,
463
+ filters: typeof query === "object" && query !== null && "filters" in query ? query.filters : void 0
464
+ };
465
+ const analyticsHash = crypto2.createHash("md5").update(JSON.stringify(relevantFields)).digest("hex").substring(0, 12);
466
+ const key = `${providerName}:analytics:${analyticsHash}`;
467
+ return {
468
+ key,
469
+ cacheDurationInSeconds: 0,
470
+ canCache: false
471
+ };
472
+ }
508
473
  };
509
474
 
510
475
  // core/src/providers/cart.provider.ts
476
+ import * as crypto3 from "crypto";
511
477
  var CartProvider = class extends BaseProvider {
478
+ getCacheEvaluation(query, session) {
479
+ const providerName = this.constructor.name.toLowerCase();
480
+ const userId = session.identity?.id || "anonymous";
481
+ const queryHash = crypto3.createHash("md5").update(JSON.stringify(query)).digest("hex").substring(0, 12);
482
+ const key = `${providerName}:cart:${userId}:${queryHash}`;
483
+ return {
484
+ key,
485
+ cacheDurationInSeconds: 0,
486
+ canCache: false
487
+ };
488
+ }
512
489
  };
513
490
 
514
491
  // core/src/providers/identity.provider.ts
515
492
  var IdentityProvider = class extends BaseProvider {
493
+ getCacheEvaluation(_query, session) {
494
+ const providerName = this.constructor.name.toLowerCase();
495
+ const userId = session.identity?.id;
496
+ const key = userId ? `${providerName}:identity:${userId}` : `${providerName}:identity:anonymous`;
497
+ return {
498
+ key,
499
+ cacheDurationInSeconds: 0,
500
+ canCache: false
501
+ };
502
+ }
516
503
  };
517
504
 
518
505
  // core/src/providers/inventory.provider.ts
519
506
  var InventoryProvider = class extends BaseProvider {
507
+ getCacheEvaluation(query, _session) {
508
+ const providerName = this.constructor.name.toLowerCase();
509
+ const key = `${providerName}:inventory:${query.sku}`;
510
+ return {
511
+ key,
512
+ cacheDurationInSeconds: 0,
513
+ canCache: false
514
+ };
515
+ }
520
516
  };
521
517
 
522
518
  // core/src/providers/price.provider.ts
519
+ import * as crypto4 from "crypto";
523
520
  var PriceProvider = class extends BaseProvider {
521
+ getCacheEvaluation(query, _session) {
522
+ const providerName = this.constructor.name.toLowerCase();
523
+ const skuHash = crypto4.createHash("md5").update(JSON.stringify(query.sku)).digest("hex").substring(0, 12);
524
+ const key = `${providerName}:price:${skuHash}`;
525
+ return {
526
+ key,
527
+ cacheDurationInSeconds: 0,
528
+ canCache: false
529
+ };
530
+ }
524
531
  };
525
532
 
526
533
  // core/src/providers/product.provider.ts
534
+ import * as crypto5 from "crypto";
527
535
  var ProductProvider = class extends BaseProvider {
536
+ getCacheEvaluation(query, _session) {
537
+ const providerName = this.constructor.name.toLowerCase();
538
+ let key;
539
+ if (query.query === "slug") {
540
+ key = `${providerName}:product:slug:${query.slug}`;
541
+ } else if (query.query === "id") {
542
+ key = `${providerName}:product:id:${query.id}`;
543
+ } else {
544
+ const queryHash = crypto5.createHash("md5").update(JSON.stringify(query)).digest("hex").substring(0, 12);
545
+ key = `${providerName}:product:${queryHash}`;
546
+ }
547
+ return {
548
+ key,
549
+ cacheDurationInSeconds: 300,
550
+ // Products are moderately stable - 5 minutes
551
+ canCache: true
552
+ };
553
+ }
528
554
  };
529
555
 
530
556
  // core/src/providers/search.provider.ts
557
+ import * as crypto6 from "crypto";
531
558
  var SearchProvider = class extends BaseProvider {
559
+ getCacheEvaluation(query, _session) {
560
+ const providerName = this.constructor.name.toLowerCase();
561
+ const searchHash = crypto6.createHash("md5").update(JSON.stringify(query.search)).digest("hex").substring(0, 12);
562
+ const key = `${providerName}:search:${searchHash}`;
563
+ return {
564
+ key,
565
+ cacheDurationInSeconds: 0,
566
+ canCache: false
567
+ };
568
+ }
532
569
  };
533
570
 
534
571
  // core/src/schemas/capabilities.schema.ts
@@ -986,7 +1023,6 @@ var SearchQueryByTermSchema = BaseQuerySchema.extend({
986
1023
  var SearchQuerySchema = z25.union([SearchQueryByTermSchema]);
987
1024
  export {
988
1025
  AnalyticsProvider,
989
- BaseCachingStrategy,
990
1026
  BaseModelSchema,
991
1027
  BaseMutationSchema,
992
1028
  BaseProvider,
@@ -1021,6 +1057,7 @@ export {
1021
1057
  InventorySchema,
1022
1058
  MetaSchema,
1023
1059
  MonetaryAmountSchema,
1060
+ NoOpCache,
1024
1061
  PriceIdentifierSchema,
1025
1062
  PriceMutationSchema,
1026
1063
  PriceProvider,
@@ -1048,5 +1085,6 @@ export {
1048
1085
  SearchResultProductSchema,
1049
1086
  SearchResultSchema,
1050
1087
  SessionSchema,
1051
- buildClient
1088
+ buildClient,
1089
+ createCache
1052
1090
  };
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@reactionary/core",
3
- "version": "0.0.29",
3
+ "version": "0.0.31",
4
4
  "dependencies": {
5
5
  "zod": "4.0.0-beta.20250430T185432",
6
6
  "@upstash/redis": "^1.34.9",
7
- "@reactionary/otel": "0.0.29"
7
+ "@reactionary/otel": "0.0.31"
8
8
  }
9
9
  }
@@ -0,0 +1,17 @@
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
+ * How long to cache the value in seconds
11
+ */
12
+ cacheDurationInSeconds: number;
13
+ /**
14
+ * Whether this query result can be cached
15
+ */
16
+ canCache: boolean;
17
+ }
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Generic cache interface that can be implemented by different cache backends
4
+ * (Redis, memory, file-based, etc.)
5
+ */
6
+ export interface Cache {
7
+ /**
8
+ * Retrieves a value from cache and validates it against the provided schema
9
+ */
10
+ get<T>(key: string, schema: z.ZodType<T>): Promise<T | null>;
11
+ /**
12
+ * Stores a value in cache with optional expiration time
13
+ */
14
+ put(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
15
+ /**
16
+ * Removes one or more keys from cache
17
+ * Supports wildcard patterns (implementation dependent)
18
+ */
19
+ del(keys: string | string[]): Promise<void>;
20
+ /**
21
+ * Finds all keys matching a pattern (implementation dependent)
22
+ */
23
+ keys(pattern: string): Promise<string[]>;
24
+ /**
25
+ * Clears all cache entries or entries matching a pattern
26
+ */
27
+ clear(pattern?: string): Promise<void>;
28
+ /**
29
+ * Gets basic cache statistics (implementation dependent)
30
+ */
31
+ getStats(): Promise<{
32
+ hits: number;
33
+ misses: number;
34
+ size: number;
35
+ }>;
36
+ }
@@ -0,0 +1,18 @@
1
+ import { Cache } from './cache.interface';
2
+ import z from 'zod';
3
+ /**
4
+ * No-op cache implementation that never stores or returns data.
5
+ * Useful for testing or when caching should be disabled.
6
+ */
7
+ export declare class NoOpCache implements Cache {
8
+ get<T>(_key: string, _schema: z.ZodType<T>): Promise<T | null>;
9
+ put(_key: string, _value: unknown, _ttlSeconds?: number): Promise<void>;
10
+ del(_keys: string | string[]): Promise<void>;
11
+ keys(_pattern: string): Promise<string[]>;
12
+ clear(_pattern?: string): Promise<void>;
13
+ getStats(): Promise<{
14
+ hits: number;
15
+ misses: number;
16
+ size: number;
17
+ }>;
18
+ }
@@ -1,13 +1,17 @@
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
- export declare class RedisCache {
8
- protected strategy: CachingStrategy;
4
+ export declare class RedisCache implements Cache {
9
5
  protected redis: Redis;
10
- constructor(strategy: CachingStrategy);
11
- get<T extends BaseModel>(query: BaseQuery, session: Session, schema: z.ZodType<T>): Promise<T | null>;
12
- put(query: BaseQuery, session: Session, value: unknown): void;
6
+ constructor();
7
+ get<T>(key: string, schema: z.ZodType<T>): Promise<T | null>;
8
+ put(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
9
+ del(keys: string | string[]): Promise<void>;
10
+ keys(pattern: string): Promise<string[]>;
11
+ clear(pattern?: string): Promise<void>;
12
+ getStats(): Promise<{
13
+ hits: number;
14
+ misses: number;
15
+ size: number;
16
+ }>;
13
17
  }
@@ -5,6 +5,7 @@ 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";
8
9
  export interface Client {
9
10
  product: ProductProvider;
10
11
  search: SearchProvider;
@@ -15,4 +16,8 @@ export interface Client {
15
16
  price: PriceProvider;
16
17
  inventory: InventoryProvider;
17
18
  }
18
- export declare function buildClient<T extends Partial<Client>>(providers: Array<T>): Required<T>;
19
+ export interface BuildClientOptions {
20
+ cache?: Cache;
21
+ }
22
+ export declare function buildClient<T extends Partial<Client>>(providerFactories: Array<(cache: Cache) => T>, options?: BuildClientOptions): Required<T>;
23
+ export declare function createCache(): Cache;
package/src/index.d.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
  export * from './client/client';
4
6
  export * from './providers/analytics.provider';
5
7
  export * from './providers/base.provider';
@@ -2,5 +2,8 @@ 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';
5
7
  export declare abstract class AnalyticsProvider<T extends AnalyticsEvent = AnalyticsEvent, Q extends AnalyticsQuery = AnalyticsQuery, M extends AnalyticsMutation = AnalyticsMutation> extends BaseProvider<T, Q, M> {
8
+ protected getCacheEvaluation(query: Q, _session: Session): CacheEvaluation;
6
9
  }
@@ -3,6 +3,9 @@ import { Session } from '../schemas/session.schema';
3
3
  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
+ import { createProviderInstrumentation } from '@reactionary/otel';
7
+ import { Cache } from '../cache/cache.interface';
8
+ import { CacheEvaluation } from '../cache/cache-evaluation.interface';
6
9
  /**
7
10
  * Base capability provider, responsible for mutations (changes) and queries (fetches)
8
11
  * for a given business object domain.
@@ -11,8 +14,9 @@ export declare abstract class BaseProvider<T extends BaseModel = BaseModel, Q ex
11
14
  readonly schema: z.ZodType<T>;
12
15
  readonly querySchema: z.ZodType<Q, Q>;
13
16
  readonly mutationSchema: z.ZodType<M, M>;
14
- private instrumentation;
15
- constructor(schema: z.ZodType<T>, querySchema: z.ZodType<Q, Q>, mutationSchema: z.ZodType<M, M>);
17
+ protected instrumentation: ReturnType<typeof createProviderInstrumentation>;
18
+ protected cache: Cache;
19
+ constructor(schema: z.ZodType<T>, querySchema: z.ZodType<Q, Q>, mutationSchema: z.ZodType<M, M>, cache: Cache);
16
20
  /**
17
21
  * Validates that the final domain model constructed by the provider
18
22
  * fulfills the schema as defined. This will throw an exception.
@@ -44,4 +48,11 @@ export declare abstract class BaseProvider<T extends BaseModel = BaseModel, Q ex
44
48
  * capabilities.
45
49
  */
46
50
  protected abstract process(mutations: M[], session: Session): Promise<T>;
51
+ /**
52
+ * Provider-specific cache evaluation logic.
53
+ * Returns information about how this query should be cached.
54
+ * Override this method to enable caching with custom keys and TTL.
55
+ * Default implementation returns no caching.
56
+ */
57
+ protected getCacheEvaluation(query: Q, session: Session): CacheEvaluation;
47
58
  }
@@ -2,5 +2,8 @@ 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';
5
7
  export declare abstract class CartProvider<T extends Cart = Cart, Q extends CartQuery = CartQuery, M extends CartMutation = CartMutation> extends BaseProvider<T, Q, M> {
8
+ protected getCacheEvaluation(query: Q, session: Session): CacheEvaluation;
6
9
  }
@@ -2,5 +2,8 @@ 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
  export declare abstract class IdentityProvider<T extends Identity = Identity, Q extends IdentityQuery = IdentityQuery, M extends IdentityMutation = IdentityMutation> extends BaseProvider<T, Q, M> {
8
+ protected getCacheEvaluation(_query: Q, session: Session): CacheEvaluation;
6
9
  }
@@ -2,5 +2,8 @@ import { Inventory } from '../schemas/models/inventory.model';
2
2
  import { InventoryQuery } from '../schemas/queries/inventory.query';
3
3
  import { InventoryMutation } from '../schemas/mutations/inventory.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
  export declare abstract class InventoryProvider<T extends Inventory = Inventory, Q extends InventoryQuery = InventoryQuery, M extends InventoryMutation = InventoryMutation> extends BaseProvider<T, Q, M> {
8
+ protected getCacheEvaluation(query: Q, _session: Session): CacheEvaluation;
6
9
  }
@@ -2,5 +2,8 @@ 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';
5
7
  export declare abstract class PriceProvider<T extends Price = Price, Q extends PriceQuery = PriceQuery, M extends PriceMutation = PriceMutation> extends BaseProvider<T, Q, M> {
8
+ protected getCacheEvaluation(query: Q, _session: Session): CacheEvaluation;
6
9
  }
@@ -2,5 +2,8 @@ import { Product } from '../schemas/models/product.model';
2
2
  import { ProductMutation } from '../schemas/mutations/product.mutation';
3
3
  import { ProductQuery } 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';
5
7
  export declare abstract class ProductProvider<T extends Product = Product, Q extends ProductQuery = ProductQuery, M extends ProductMutation = ProductMutation> extends BaseProvider<T, Q, M> {
8
+ protected getCacheEvaluation(query: Q, _session: Session): CacheEvaluation;
6
9
  }
@@ -2,5 +2,8 @@ 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';
5
7
  export declare abstract class SearchProvider<T extends SearchResult = SearchResult, Q extends SearchQuery = SearchQuery, M extends SearchMutation = SearchMutation> extends BaseProvider<T, Q, M> {
8
+ protected getCacheEvaluation(query: Q, _session: Session): CacheEvaluation;
6
9
  }
@@ -1,13 +0,0 @@
1
- import { BaseQuery } from "../schemas/queries/base.query";
2
- import { Session } from "../schemas/session.schema";
3
- export interface CachingStrategyEvaluation {
4
- key: string;
5
- cacheDurationInSeconds: number;
6
- canCache: boolean;
7
- }
8
- export interface CachingStrategy {
9
- get(query: BaseQuery, session: Session): CachingStrategyEvaluation;
10
- }
11
- export declare class BaseCachingStrategy implements CachingStrategy {
12
- get(query: BaseQuery, session: Session): CachingStrategyEvaluation;
13
- }