@objectstack/service-cache 5.1.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -26,6 +26,9 @@ __export(index_exports, {
26
26
  });
27
27
  module.exports = __toCommonJS(index_exports);
28
28
 
29
+ // src/cache-service-plugin.ts
30
+ var import_observability2 = require("@objectstack/observability");
31
+
29
32
  // src/memory-cache-adapter.ts
30
33
  var import_observability = require("@objectstack/observability");
31
34
  var _MemoryCacheAdapter = class _MemoryCacheAdapter {
@@ -122,11 +125,23 @@ var CacheServicePlugin = class {
122
125
  'Redis cache adapter is not yet implemented. Use adapter: "memory" or provide a custom ICacheService via ctx.registerService("cache", impl).'
123
126
  );
124
127
  }
125
- const cache = new MemoryCacheAdapter(this.options.memory);
128
+ const metrics = resolveMetrics(ctx, this.options.metrics);
129
+ const cache = new MemoryCacheAdapter({ ...this.options.memory, metrics });
126
130
  ctx.registerService("cache", cache);
127
- ctx.logger.info("CacheServicePlugin: registered memory cache adapter");
131
+ ctx.logger.info(
132
+ `CacheServicePlugin: registered memory cache adapter (metrics=${metrics.constructor?.name ?? "unknown"})`
133
+ );
128
134
  }
129
135
  };
136
+ function resolveMetrics(ctx, override) {
137
+ if (override) return override;
138
+ try {
139
+ const m = ctx.getService(import_observability2.OBSERVABILITY_METRICS_SERVICE);
140
+ if (m) return m;
141
+ } catch {
142
+ }
143
+ return new import_observability2.NoopMetricsRegistry();
144
+ }
130
145
 
131
146
  // src/redis-cache-adapter.ts
132
147
  var RedisCacheAdapter = class {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/memory-cache-adapter.ts","../src/cache-service-plugin.ts","../src/redis-cache-adapter.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport { CacheServicePlugin } from './cache-service-plugin.js';\nexport type { CacheServicePluginOptions } from './cache-service-plugin.js';\nexport { MemoryCacheAdapter } from './memory-cache-adapter.js';\nexport type { MemoryCacheAdapterOptions } from './memory-cache-adapter.js';\nexport { RedisCacheAdapter } from './redis-cache-adapter.js';\nexport type { RedisCacheAdapterOptions } from './redis-cache-adapter.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport {\n NoopMetricsRegistry,\n SEMCONV,\n type MetricsRegistry,\n} from '@objectstack/observability';\nimport type { ICacheService, CacheStats } from '@objectstack/spec/contracts';\n\n/**\n * In-memory cache entry with optional TTL expiry.\n */\ninterface CacheEntry<T = unknown> {\n value: T;\n expires?: number;\n}\n\n/**\n * Configuration options for MemoryCacheAdapter.\n */\nexport interface MemoryCacheAdapterOptions {\n /** Maximum number of entries before eviction (0 = unlimited) */\n maxSize?: number;\n /** Default TTL in seconds (0 = no expiry) */\n defaultTtl?: number;\n /** Optional MetricsRegistry for instrumentation. Defaults to NoopMetricsRegistry. */\n metrics?: MetricsRegistry;\n}\n\n/**\n * In-memory cache adapter implementing ICacheService.\n *\n * Uses a Map-backed store with TTL-based expiry and LRU-style eviction.\n * Suitable for single-process environments, development, and testing.\n */\nexport class MemoryCacheAdapter implements ICacheService {\n private readonly store = new Map<string, CacheEntry>();\n private hits = 0;\n private misses = 0;\n private readonly maxSize: number;\n private readonly defaultTtl: number;\n private readonly metrics: MetricsRegistry;\n private static readonly LABELS = { adapter: 'memory' } as const;\n\n constructor(options: MemoryCacheAdapterOptions = {}) {\n this.maxSize = options.maxSize ?? 0;\n this.defaultTtl = options.defaultTtl ?? 0;\n this.metrics = options.metrics ?? new NoopMetricsRegistry();\n }\n\n private recordLookup(result: 'hit' | 'miss'): void {\n try {\n this.metrics.counter(SEMCONV.cacheLookupsTotal, { ...MemoryCacheAdapter.LABELS, result });\n } catch { /* never throw from instrumentation */ }\n }\n\n private recordWrite(op: 'set' | 'delete' | 'clear'): void {\n try {\n this.metrics.counter(SEMCONV.cacheWritesTotal, { ...MemoryCacheAdapter.LABELS, op });\n } catch { /* never throw from instrumentation */ }\n }\n\n async get<T = unknown>(key: string): Promise<T | undefined> {\n const entry = this.store.get(key);\n if (!entry || (entry.expires && Date.now() > entry.expires)) {\n if (entry) this.store.delete(key);\n this.misses++;\n this.recordLookup('miss');\n return undefined;\n }\n this.hits++;\n this.recordLookup('hit');\n return entry.value as T;\n }\n\n async set<T = unknown>(key: string, value: T, ttl?: number): Promise<void> {\n const effectiveTtl = ttl ?? this.defaultTtl;\n if (this.maxSize > 0 && !this.store.has(key) && this.store.size >= this.maxSize) {\n // Evict oldest entry (first key in Map insertion order)\n const firstKey = this.store.keys().next().value;\n if (firstKey !== undefined) this.store.delete(firstKey);\n }\n this.store.set(key, {\n value,\n expires: effectiveTtl > 0 ? Date.now() + effectiveTtl * 1000 : undefined,\n });\n this.recordWrite('set');\n }\n\n async delete(key: string): Promise<boolean> {\n const result = this.store.delete(key);\n this.recordWrite('delete');\n return result;\n }\n\n async has(key: string): Promise<boolean> {\n const entry = this.store.get(key);\n if (!entry) {\n this.recordLookup('miss');\n return false;\n }\n if (entry.expires && Date.now() > entry.expires) {\n this.store.delete(key);\n this.recordLookup('miss');\n return false;\n }\n this.recordLookup('hit');\n return true;\n }\n\n async clear(): Promise<void> {\n this.store.clear();\n this.recordWrite('clear');\n }\n\n async stats(): Promise<CacheStats> {\n return {\n hits: this.hits,\n misses: this.misses,\n keyCount: this.store.size,\n };\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { MemoryCacheAdapter } from './memory-cache-adapter.js';\nimport type { MemoryCacheAdapterOptions } from './memory-cache-adapter.js';\n\n/**\n * Configuration options for the CacheServicePlugin.\n */\nexport interface CacheServicePluginOptions {\n /** Cache adapter type (default: 'memory') */\n adapter?: 'memory' | 'redis';\n /** Options for the memory cache adapter */\n memory?: MemoryCacheAdapterOptions;\n /** Redis connection URL (used when adapter is 'redis') */\n redisUrl?: string;\n}\n\n/**\n * CacheServicePlugin — Production ICacheService implementation.\n *\n * Registers a cache service with the kernel during the init phase.\n * Supports in-memory and Redis adapters.\n *\n * @example\n * ```ts\n * import { ObjectKernel } from '@objectstack/core';\n * import { CacheServicePlugin } from '@objectstack/service-cache';\n *\n * const kernel = new ObjectKernel();\n * kernel.use(new CacheServicePlugin({ adapter: 'memory', memory: { maxSize: 1000 } }));\n * await kernel.bootstrap();\n *\n * const cache = kernel.getService('cache');\n * await cache.set('key', 'value', 60);\n * ```\n */\nexport class CacheServicePlugin implements Plugin {\n name = 'com.objectstack.service.cache';\n version = '1.0.0';\n type = 'standard';\n\n private readonly options: CacheServicePluginOptions;\n\n constructor(options: CacheServicePluginOptions = {}) {\n this.options = { adapter: 'memory', ...options };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n const adapter = this.options.adapter;\n if (adapter === 'redis') {\n // Redis adapter is a skeleton — throw an informative error for now\n throw new Error(\n 'Redis cache adapter is not yet implemented. ' +\n 'Use adapter: \"memory\" or provide a custom ICacheService via ctx.registerService(\"cache\", impl).'\n );\n }\n\n const cache = new MemoryCacheAdapter(this.options.memory);\n ctx.registerService('cache', cache);\n ctx.logger.info('CacheServicePlugin: registered memory cache adapter');\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { ICacheService, CacheStats } from '@objectstack/spec/contracts';\n\n/**\n * Configuration for the Redis cache adapter.\n */\nexport interface RedisCacheAdapterOptions {\n /** Redis connection URL (e.g. 'redis://localhost:6379') */\n url: string;\n /** Key prefix for namespacing (default: 'os:') */\n keyPrefix?: string;\n /** Default TTL in seconds (0 = no expiry) */\n defaultTtl?: number;\n}\n\n/**\n * Redis cache adapter skeleton implementing ICacheService.\n *\n * This is a placeholder for future Redis integration.\n * Concrete implementation will use `ioredis` or `redis` client.\n *\n * @example\n * ```ts\n * const cache = new RedisCacheAdapter({ url: 'redis://localhost:6379' });\n * await cache.set('user:1', { name: 'Alice' }, 3600);\n * ```\n */\nexport class RedisCacheAdapter implements ICacheService {\n private readonly url: string;\n private readonly keyPrefix: string;\n private readonly defaultTtl: number;\n\n constructor(options: RedisCacheAdapterOptions) {\n this.url = options.url;\n this.keyPrefix = options.keyPrefix ?? 'os:';\n this.defaultTtl = options.defaultTtl ?? 0;\n }\n\n async get<T = unknown>(_key: string): Promise<T | undefined> {\n throw new Error(`RedisCacheAdapter not yet implemented (url: ${this.url}, prefix: ${this.keyPrefix}, ttl: ${this.defaultTtl})`);\n }\n\n async set<T = unknown>(_key: string, _value: T, _ttl?: number): Promise<void> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n\n async delete(_key: string): Promise<boolean> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n\n async has(_key: string): Promise<boolean> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n\n async clear(): Promise<void> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n\n async stats(): Promise<CacheStats> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/cache-service-plugin.ts","../src/memory-cache-adapter.ts","../src/redis-cache-adapter.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport { CacheServicePlugin } from './cache-service-plugin.js';\nexport type { CacheServicePluginOptions } from './cache-service-plugin.js';\nexport { MemoryCacheAdapter } from './memory-cache-adapter.js';\nexport type { MemoryCacheAdapterOptions } from './memory-cache-adapter.js';\nexport { RedisCacheAdapter } from './redis-cache-adapter.js';\nexport type { RedisCacheAdapterOptions } from './redis-cache-adapter.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport {\n OBSERVABILITY_METRICS_SERVICE,\n NoopMetricsRegistry,\n type MetricsRegistry,\n} from '@objectstack/observability';\nimport { MemoryCacheAdapter } from './memory-cache-adapter.js';\nimport type { MemoryCacheAdapterOptions } from './memory-cache-adapter.js';\n\n/**\n * Configuration options for the CacheServicePlugin.\n */\nexport interface CacheServicePluginOptions {\n /** Cache adapter type (default: 'memory') */\n adapter?: 'memory' | 'redis';\n /** Options for the memory cache adapter */\n memory?: MemoryCacheAdapterOptions;\n /** Redis connection URL (used when adapter is 'redis') */\n redisUrl?: string;\n /**\n * Optional explicit metrics backend. Wins over the service-registry\n * lookup. Mostly an escape hatch for tests — production code should\n * register `ObservabilityServicePlugin` (from `@objectstack/runtime`)\n * once and let every service pick the host's metrics backend up\n * automatically.\n */\n metrics?: MetricsRegistry;\n}\n\n/**\n * CacheServicePlugin — Production ICacheService implementation.\n *\n * Registers a cache service with the kernel during the init phase.\n * Supports in-memory and Redis adapters.\n *\n * ## Metrics\n *\n * The adapter emits `cache_lookups_total` and `cache_writes_total`\n * counters (see `SEMCONV` in `@objectstack/observability`). The\n * MetricsRegistry is resolved in this order:\n *\n * 1. `options.metrics` (explicit constructor wiring)\n * 2. `ctx.getService('observability:metrics')` (registered by\n * `ObservabilityServicePlugin`)\n * 3. `NoopMetricsRegistry` (silent; no instrumentation)\n *\n * @example\n * ```ts\n * import { ObjectKernel } from '@objectstack/core';\n * import { CacheServicePlugin } from '@objectstack/service-cache';\n *\n * const kernel = new ObjectKernel();\n * kernel.use(new CacheServicePlugin({ adapter: 'memory', memory: { maxSize: 1000 } }));\n * await kernel.bootstrap();\n *\n * const cache = kernel.getService('cache');\n * await cache.set('key', 'value', 60);\n * ```\n */\nexport class CacheServicePlugin implements Plugin {\n name = 'com.objectstack.service.cache';\n version = '1.0.0';\n type = 'standard';\n\n private readonly options: CacheServicePluginOptions;\n\n constructor(options: CacheServicePluginOptions = {}) {\n this.options = { adapter: 'memory', ...options };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n const adapter = this.options.adapter;\n if (adapter === 'redis') {\n // Redis adapter is a skeleton — throw an informative error for now\n throw new Error(\n 'Redis cache adapter is not yet implemented. ' +\n 'Use adapter: \"memory\" or provide a custom ICacheService via ctx.registerService(\"cache\", impl).'\n );\n }\n\n const metrics = resolveMetrics(ctx, this.options.metrics);\n const cache = new MemoryCacheAdapter({ ...this.options.memory, metrics });\n ctx.registerService('cache', cache);\n ctx.logger.info(\n `CacheServicePlugin: registered memory cache adapter (metrics=${metrics.constructor?.name ?? 'unknown'})`,\n );\n }\n}\n\n/**\n * Look up the host's MetricsRegistry from the service registry, with\n * the canonical fallback chain (explicit override → registered service\n * → noop). Local helper to avoid making `service-cache` depend on\n * `@objectstack/runtime`.\n */\nfunction resolveMetrics(\n ctx: PluginContext,\n override: MetricsRegistry | undefined,\n): MetricsRegistry {\n if (override) return override;\n try {\n const m = ctx.getService<MetricsRegistry | undefined>(OBSERVABILITY_METRICS_SERVICE);\n if (m) return m;\n } catch {\n // Service not registered — silent fall-through.\n }\n return new NoopMetricsRegistry();\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport {\n NoopMetricsRegistry,\n SEMCONV,\n type MetricsRegistry,\n} from '@objectstack/observability';\nimport type { ICacheService, CacheStats } from '@objectstack/spec/contracts';\n\n/**\n * In-memory cache entry with optional TTL expiry.\n */\ninterface CacheEntry<T = unknown> {\n value: T;\n expires?: number;\n}\n\n/**\n * Configuration options for MemoryCacheAdapter.\n */\nexport interface MemoryCacheAdapterOptions {\n /** Maximum number of entries before eviction (0 = unlimited) */\n maxSize?: number;\n /** Default TTL in seconds (0 = no expiry) */\n defaultTtl?: number;\n /** Optional MetricsRegistry for instrumentation. Defaults to NoopMetricsRegistry. */\n metrics?: MetricsRegistry;\n}\n\n/**\n * In-memory cache adapter implementing ICacheService.\n *\n * Uses a Map-backed store with TTL-based expiry and LRU-style eviction.\n * Suitable for single-process environments, development, and testing.\n */\nexport class MemoryCacheAdapter implements ICacheService {\n private readonly store = new Map<string, CacheEntry>();\n private hits = 0;\n private misses = 0;\n private readonly maxSize: number;\n private readonly defaultTtl: number;\n private readonly metrics: MetricsRegistry;\n private static readonly LABELS = { adapter: 'memory' } as const;\n\n constructor(options: MemoryCacheAdapterOptions = {}) {\n this.maxSize = options.maxSize ?? 0;\n this.defaultTtl = options.defaultTtl ?? 0;\n this.metrics = options.metrics ?? new NoopMetricsRegistry();\n }\n\n private recordLookup(result: 'hit' | 'miss'): void {\n try {\n this.metrics.counter(SEMCONV.cacheLookupsTotal, { ...MemoryCacheAdapter.LABELS, result });\n } catch { /* never throw from instrumentation */ }\n }\n\n private recordWrite(op: 'set' | 'delete' | 'clear'): void {\n try {\n this.metrics.counter(SEMCONV.cacheWritesTotal, { ...MemoryCacheAdapter.LABELS, op });\n } catch { /* never throw from instrumentation */ }\n }\n\n async get<T = unknown>(key: string): Promise<T | undefined> {\n const entry = this.store.get(key);\n if (!entry || (entry.expires && Date.now() > entry.expires)) {\n if (entry) this.store.delete(key);\n this.misses++;\n this.recordLookup('miss');\n return undefined;\n }\n this.hits++;\n this.recordLookup('hit');\n return entry.value as T;\n }\n\n async set<T = unknown>(key: string, value: T, ttl?: number): Promise<void> {\n const effectiveTtl = ttl ?? this.defaultTtl;\n if (this.maxSize > 0 && !this.store.has(key) && this.store.size >= this.maxSize) {\n // Evict oldest entry (first key in Map insertion order)\n const firstKey = this.store.keys().next().value;\n if (firstKey !== undefined) this.store.delete(firstKey);\n }\n this.store.set(key, {\n value,\n expires: effectiveTtl > 0 ? Date.now() + effectiveTtl * 1000 : undefined,\n });\n this.recordWrite('set');\n }\n\n async delete(key: string): Promise<boolean> {\n const result = this.store.delete(key);\n this.recordWrite('delete');\n return result;\n }\n\n async has(key: string): Promise<boolean> {\n const entry = this.store.get(key);\n if (!entry) {\n this.recordLookup('miss');\n return false;\n }\n if (entry.expires && Date.now() > entry.expires) {\n this.store.delete(key);\n this.recordLookup('miss');\n return false;\n }\n this.recordLookup('hit');\n return true;\n }\n\n async clear(): Promise<void> {\n this.store.clear();\n this.recordWrite('clear');\n }\n\n async stats(): Promise<CacheStats> {\n return {\n hits: this.hits,\n misses: this.misses,\n keyCount: this.store.size,\n };\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { ICacheService, CacheStats } from '@objectstack/spec/contracts';\n\n/**\n * Configuration for the Redis cache adapter.\n */\nexport interface RedisCacheAdapterOptions {\n /** Redis connection URL (e.g. 'redis://localhost:6379') */\n url: string;\n /** Key prefix for namespacing (default: 'os:') */\n keyPrefix?: string;\n /** Default TTL in seconds (0 = no expiry) */\n defaultTtl?: number;\n}\n\n/**\n * Redis cache adapter skeleton implementing ICacheService.\n *\n * This is a placeholder for future Redis integration.\n * Concrete implementation will use `ioredis` or `redis` client.\n *\n * @example\n * ```ts\n * const cache = new RedisCacheAdapter({ url: 'redis://localhost:6379' });\n * await cache.set('user:1', { name: 'Alice' }, 3600);\n * ```\n */\nexport class RedisCacheAdapter implements ICacheService {\n private readonly url: string;\n private readonly keyPrefix: string;\n private readonly defaultTtl: number;\n\n constructor(options: RedisCacheAdapterOptions) {\n this.url = options.url;\n this.keyPrefix = options.keyPrefix ?? 'os:';\n this.defaultTtl = options.defaultTtl ?? 0;\n }\n\n async get<T = unknown>(_key: string): Promise<T | undefined> {\n throw new Error(`RedisCacheAdapter not yet implemented (url: ${this.url}, prefix: ${this.keyPrefix}, ttl: ${this.defaultTtl})`);\n }\n\n async set<T = unknown>(_key: string, _value: T, _ttl?: number): Promise<void> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n\n async delete(_key: string): Promise<boolean> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n\n async has(_key: string): Promise<boolean> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n\n async clear(): Promise<void> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n\n async stats(): Promise<CacheStats> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n}\n"],"mappings":"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","names":["import_observability"]}
package/dist/index.d.cts CHANGED
@@ -48,6 +48,14 @@ interface CacheServicePluginOptions {
48
48
  memory?: MemoryCacheAdapterOptions;
49
49
  /** Redis connection URL (used when adapter is 'redis') */
50
50
  redisUrl?: string;
51
+ /**
52
+ * Optional explicit metrics backend. Wins over the service-registry
53
+ * lookup. Mostly an escape hatch for tests — production code should
54
+ * register `ObservabilityServicePlugin` (from `@objectstack/runtime`)
55
+ * once and let every service pick the host's metrics backend up
56
+ * automatically.
57
+ */
58
+ metrics?: MetricsRegistry;
51
59
  }
52
60
  /**
53
61
  * CacheServicePlugin — Production ICacheService implementation.
@@ -55,6 +63,17 @@ interface CacheServicePluginOptions {
55
63
  * Registers a cache service with the kernel during the init phase.
56
64
  * Supports in-memory and Redis adapters.
57
65
  *
66
+ * ## Metrics
67
+ *
68
+ * The adapter emits `cache_lookups_total` and `cache_writes_total`
69
+ * counters (see `SEMCONV` in `@objectstack/observability`). The
70
+ * MetricsRegistry is resolved in this order:
71
+ *
72
+ * 1. `options.metrics` (explicit constructor wiring)
73
+ * 2. `ctx.getService('observability:metrics')` (registered by
74
+ * `ObservabilityServicePlugin`)
75
+ * 3. `NoopMetricsRegistry` (silent; no instrumentation)
76
+ *
58
77
  * @example
59
78
  * ```ts
60
79
  * import { ObjectKernel } from '@objectstack/core';
package/dist/index.d.ts CHANGED
@@ -48,6 +48,14 @@ interface CacheServicePluginOptions {
48
48
  memory?: MemoryCacheAdapterOptions;
49
49
  /** Redis connection URL (used when adapter is 'redis') */
50
50
  redisUrl?: string;
51
+ /**
52
+ * Optional explicit metrics backend. Wins over the service-registry
53
+ * lookup. Mostly an escape hatch for tests — production code should
54
+ * register `ObservabilityServicePlugin` (from `@objectstack/runtime`)
55
+ * once and let every service pick the host's metrics backend up
56
+ * automatically.
57
+ */
58
+ metrics?: MetricsRegistry;
51
59
  }
52
60
  /**
53
61
  * CacheServicePlugin — Production ICacheService implementation.
@@ -55,6 +63,17 @@ interface CacheServicePluginOptions {
55
63
  * Registers a cache service with the kernel during the init phase.
56
64
  * Supports in-memory and Redis adapters.
57
65
  *
66
+ * ## Metrics
67
+ *
68
+ * The adapter emits `cache_lookups_total` and `cache_writes_total`
69
+ * counters (see `SEMCONV` in `@objectstack/observability`). The
70
+ * MetricsRegistry is resolved in this order:
71
+ *
72
+ * 1. `options.metrics` (explicit constructor wiring)
73
+ * 2. `ctx.getService('observability:metrics')` (registered by
74
+ * `ObservabilityServicePlugin`)
75
+ * 3. `NoopMetricsRegistry` (silent; no instrumentation)
76
+ *
58
77
  * @example
59
78
  * ```ts
60
79
  * import { ObjectKernel } from '@objectstack/core';
package/dist/index.js CHANGED
@@ -1,3 +1,9 @@
1
+ // src/cache-service-plugin.ts
2
+ import {
3
+ OBSERVABILITY_METRICS_SERVICE,
4
+ NoopMetricsRegistry as NoopMetricsRegistry2
5
+ } from "@objectstack/observability";
6
+
1
7
  // src/memory-cache-adapter.ts
2
8
  import {
3
9
  NoopMetricsRegistry,
@@ -97,11 +103,23 @@ var CacheServicePlugin = class {
97
103
  'Redis cache adapter is not yet implemented. Use adapter: "memory" or provide a custom ICacheService via ctx.registerService("cache", impl).'
98
104
  );
99
105
  }
100
- const cache = new MemoryCacheAdapter(this.options.memory);
106
+ const metrics = resolveMetrics(ctx, this.options.metrics);
107
+ const cache = new MemoryCacheAdapter({ ...this.options.memory, metrics });
101
108
  ctx.registerService("cache", cache);
102
- ctx.logger.info("CacheServicePlugin: registered memory cache adapter");
109
+ ctx.logger.info(
110
+ `CacheServicePlugin: registered memory cache adapter (metrics=${metrics.constructor?.name ?? "unknown"})`
111
+ );
103
112
  }
104
113
  };
114
+ function resolveMetrics(ctx, override) {
115
+ if (override) return override;
116
+ try {
117
+ const m = ctx.getService(OBSERVABILITY_METRICS_SERVICE);
118
+ if (m) return m;
119
+ } catch {
120
+ }
121
+ return new NoopMetricsRegistry2();
122
+ }
105
123
 
106
124
  // src/redis-cache-adapter.ts
107
125
  var RedisCacheAdapter = class {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/memory-cache-adapter.ts","../src/cache-service-plugin.ts","../src/redis-cache-adapter.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport {\n NoopMetricsRegistry,\n SEMCONV,\n type MetricsRegistry,\n} from '@objectstack/observability';\nimport type { ICacheService, CacheStats } from '@objectstack/spec/contracts';\n\n/**\n * In-memory cache entry with optional TTL expiry.\n */\ninterface CacheEntry<T = unknown> {\n value: T;\n expires?: number;\n}\n\n/**\n * Configuration options for MemoryCacheAdapter.\n */\nexport interface MemoryCacheAdapterOptions {\n /** Maximum number of entries before eviction (0 = unlimited) */\n maxSize?: number;\n /** Default TTL in seconds (0 = no expiry) */\n defaultTtl?: number;\n /** Optional MetricsRegistry for instrumentation. Defaults to NoopMetricsRegistry. */\n metrics?: MetricsRegistry;\n}\n\n/**\n * In-memory cache adapter implementing ICacheService.\n *\n * Uses a Map-backed store with TTL-based expiry and LRU-style eviction.\n * Suitable for single-process environments, development, and testing.\n */\nexport class MemoryCacheAdapter implements ICacheService {\n private readonly store = new Map<string, CacheEntry>();\n private hits = 0;\n private misses = 0;\n private readonly maxSize: number;\n private readonly defaultTtl: number;\n private readonly metrics: MetricsRegistry;\n private static readonly LABELS = { adapter: 'memory' } as const;\n\n constructor(options: MemoryCacheAdapterOptions = {}) {\n this.maxSize = options.maxSize ?? 0;\n this.defaultTtl = options.defaultTtl ?? 0;\n this.metrics = options.metrics ?? new NoopMetricsRegistry();\n }\n\n private recordLookup(result: 'hit' | 'miss'): void {\n try {\n this.metrics.counter(SEMCONV.cacheLookupsTotal, { ...MemoryCacheAdapter.LABELS, result });\n } catch { /* never throw from instrumentation */ }\n }\n\n private recordWrite(op: 'set' | 'delete' | 'clear'): void {\n try {\n this.metrics.counter(SEMCONV.cacheWritesTotal, { ...MemoryCacheAdapter.LABELS, op });\n } catch { /* never throw from instrumentation */ }\n }\n\n async get<T = unknown>(key: string): Promise<T | undefined> {\n const entry = this.store.get(key);\n if (!entry || (entry.expires && Date.now() > entry.expires)) {\n if (entry) this.store.delete(key);\n this.misses++;\n this.recordLookup('miss');\n return undefined;\n }\n this.hits++;\n this.recordLookup('hit');\n return entry.value as T;\n }\n\n async set<T = unknown>(key: string, value: T, ttl?: number): Promise<void> {\n const effectiveTtl = ttl ?? this.defaultTtl;\n if (this.maxSize > 0 && !this.store.has(key) && this.store.size >= this.maxSize) {\n // Evict oldest entry (first key in Map insertion order)\n const firstKey = this.store.keys().next().value;\n if (firstKey !== undefined) this.store.delete(firstKey);\n }\n this.store.set(key, {\n value,\n expires: effectiveTtl > 0 ? Date.now() + effectiveTtl * 1000 : undefined,\n });\n this.recordWrite('set');\n }\n\n async delete(key: string): Promise<boolean> {\n const result = this.store.delete(key);\n this.recordWrite('delete');\n return result;\n }\n\n async has(key: string): Promise<boolean> {\n const entry = this.store.get(key);\n if (!entry) {\n this.recordLookup('miss');\n return false;\n }\n if (entry.expires && Date.now() > entry.expires) {\n this.store.delete(key);\n this.recordLookup('miss');\n return false;\n }\n this.recordLookup('hit');\n return true;\n }\n\n async clear(): Promise<void> {\n this.store.clear();\n this.recordWrite('clear');\n }\n\n async stats(): Promise<CacheStats> {\n return {\n hits: this.hits,\n misses: this.misses,\n keyCount: this.store.size,\n };\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { MemoryCacheAdapter } from './memory-cache-adapter.js';\nimport type { MemoryCacheAdapterOptions } from './memory-cache-adapter.js';\n\n/**\n * Configuration options for the CacheServicePlugin.\n */\nexport interface CacheServicePluginOptions {\n /** Cache adapter type (default: 'memory') */\n adapter?: 'memory' | 'redis';\n /** Options for the memory cache adapter */\n memory?: MemoryCacheAdapterOptions;\n /** Redis connection URL (used when adapter is 'redis') */\n redisUrl?: string;\n}\n\n/**\n * CacheServicePlugin — Production ICacheService implementation.\n *\n * Registers a cache service with the kernel during the init phase.\n * Supports in-memory and Redis adapters.\n *\n * @example\n * ```ts\n * import { ObjectKernel } from '@objectstack/core';\n * import { CacheServicePlugin } from '@objectstack/service-cache';\n *\n * const kernel = new ObjectKernel();\n * kernel.use(new CacheServicePlugin({ adapter: 'memory', memory: { maxSize: 1000 } }));\n * await kernel.bootstrap();\n *\n * const cache = kernel.getService('cache');\n * await cache.set('key', 'value', 60);\n * ```\n */\nexport class CacheServicePlugin implements Plugin {\n name = 'com.objectstack.service.cache';\n version = '1.0.0';\n type = 'standard';\n\n private readonly options: CacheServicePluginOptions;\n\n constructor(options: CacheServicePluginOptions = {}) {\n this.options = { adapter: 'memory', ...options };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n const adapter = this.options.adapter;\n if (adapter === 'redis') {\n // Redis adapter is a skeleton — throw an informative error for now\n throw new Error(\n 'Redis cache adapter is not yet implemented. ' +\n 'Use adapter: \"memory\" or provide a custom ICacheService via ctx.registerService(\"cache\", impl).'\n );\n }\n\n const cache = new MemoryCacheAdapter(this.options.memory);\n ctx.registerService('cache', cache);\n ctx.logger.info('CacheServicePlugin: registered memory cache adapter');\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { ICacheService, CacheStats } from '@objectstack/spec/contracts';\n\n/**\n * Configuration for the Redis cache adapter.\n */\nexport interface RedisCacheAdapterOptions {\n /** Redis connection URL (e.g. 'redis://localhost:6379') */\n url: string;\n /** Key prefix for namespacing (default: 'os:') */\n keyPrefix?: string;\n /** Default TTL in seconds (0 = no expiry) */\n defaultTtl?: number;\n}\n\n/**\n * Redis cache adapter skeleton implementing ICacheService.\n *\n * This is a placeholder for future Redis integration.\n * Concrete implementation will use `ioredis` or `redis` client.\n *\n * @example\n * ```ts\n * const cache = new RedisCacheAdapter({ url: 'redis://localhost:6379' });\n * await cache.set('user:1', { name: 'Alice' }, 3600);\n * ```\n */\nexport class RedisCacheAdapter implements ICacheService {\n private readonly url: string;\n private readonly keyPrefix: string;\n private readonly defaultTtl: number;\n\n constructor(options: RedisCacheAdapterOptions) {\n this.url = options.url;\n this.keyPrefix = options.keyPrefix ?? 'os:';\n this.defaultTtl = options.defaultTtl ?? 0;\n }\n\n async get<T = unknown>(_key: string): Promise<T | undefined> {\n throw new Error(`RedisCacheAdapter not yet implemented (url: ${this.url}, prefix: ${this.keyPrefix}, ttl: ${this.defaultTtl})`);\n }\n\n async set<T = unknown>(_key: string, _value: T, _ttl?: number): Promise<void> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n\n async delete(_key: string): Promise<boolean> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n\n async has(_key: string): Promise<boolean> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n\n async clear(): Promise<void> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n\n async stats(): Promise<CacheStats> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/cache-service-plugin.ts","../src/memory-cache-adapter.ts","../src/redis-cache-adapter.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport {\n OBSERVABILITY_METRICS_SERVICE,\n NoopMetricsRegistry,\n type MetricsRegistry,\n} from '@objectstack/observability';\nimport { MemoryCacheAdapter } from './memory-cache-adapter.js';\nimport type { MemoryCacheAdapterOptions } from './memory-cache-adapter.js';\n\n/**\n * Configuration options for the CacheServicePlugin.\n */\nexport interface CacheServicePluginOptions {\n /** Cache adapter type (default: 'memory') */\n adapter?: 'memory' | 'redis';\n /** Options for the memory cache adapter */\n memory?: MemoryCacheAdapterOptions;\n /** Redis connection URL (used when adapter is 'redis') */\n redisUrl?: string;\n /**\n * Optional explicit metrics backend. Wins over the service-registry\n * lookup. Mostly an escape hatch for tests — production code should\n * register `ObservabilityServicePlugin` (from `@objectstack/runtime`)\n * once and let every service pick the host's metrics backend up\n * automatically.\n */\n metrics?: MetricsRegistry;\n}\n\n/**\n * CacheServicePlugin — Production ICacheService implementation.\n *\n * Registers a cache service with the kernel during the init phase.\n * Supports in-memory and Redis adapters.\n *\n * ## Metrics\n *\n * The adapter emits `cache_lookups_total` and `cache_writes_total`\n * counters (see `SEMCONV` in `@objectstack/observability`). The\n * MetricsRegistry is resolved in this order:\n *\n * 1. `options.metrics` (explicit constructor wiring)\n * 2. `ctx.getService('observability:metrics')` (registered by\n * `ObservabilityServicePlugin`)\n * 3. `NoopMetricsRegistry` (silent; no instrumentation)\n *\n * @example\n * ```ts\n * import { ObjectKernel } from '@objectstack/core';\n * import { CacheServicePlugin } from '@objectstack/service-cache';\n *\n * const kernel = new ObjectKernel();\n * kernel.use(new CacheServicePlugin({ adapter: 'memory', memory: { maxSize: 1000 } }));\n * await kernel.bootstrap();\n *\n * const cache = kernel.getService('cache');\n * await cache.set('key', 'value', 60);\n * ```\n */\nexport class CacheServicePlugin implements Plugin {\n name = 'com.objectstack.service.cache';\n version = '1.0.0';\n type = 'standard';\n\n private readonly options: CacheServicePluginOptions;\n\n constructor(options: CacheServicePluginOptions = {}) {\n this.options = { adapter: 'memory', ...options };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n const adapter = this.options.adapter;\n if (adapter === 'redis') {\n // Redis adapter is a skeleton — throw an informative error for now\n throw new Error(\n 'Redis cache adapter is not yet implemented. ' +\n 'Use adapter: \"memory\" or provide a custom ICacheService via ctx.registerService(\"cache\", impl).'\n );\n }\n\n const metrics = resolveMetrics(ctx, this.options.metrics);\n const cache = new MemoryCacheAdapter({ ...this.options.memory, metrics });\n ctx.registerService('cache', cache);\n ctx.logger.info(\n `CacheServicePlugin: registered memory cache adapter (metrics=${metrics.constructor?.name ?? 'unknown'})`,\n );\n }\n}\n\n/**\n * Look up the host's MetricsRegistry from the service registry, with\n * the canonical fallback chain (explicit override → registered service\n * → noop). Local helper to avoid making `service-cache` depend on\n * `@objectstack/runtime`.\n */\nfunction resolveMetrics(\n ctx: PluginContext,\n override: MetricsRegistry | undefined,\n): MetricsRegistry {\n if (override) return override;\n try {\n const m = ctx.getService<MetricsRegistry | undefined>(OBSERVABILITY_METRICS_SERVICE);\n if (m) return m;\n } catch {\n // Service not registered — silent fall-through.\n }\n return new NoopMetricsRegistry();\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport {\n NoopMetricsRegistry,\n SEMCONV,\n type MetricsRegistry,\n} from '@objectstack/observability';\nimport type { ICacheService, CacheStats } from '@objectstack/spec/contracts';\n\n/**\n * In-memory cache entry with optional TTL expiry.\n */\ninterface CacheEntry<T = unknown> {\n value: T;\n expires?: number;\n}\n\n/**\n * Configuration options for MemoryCacheAdapter.\n */\nexport interface MemoryCacheAdapterOptions {\n /** Maximum number of entries before eviction (0 = unlimited) */\n maxSize?: number;\n /** Default TTL in seconds (0 = no expiry) */\n defaultTtl?: number;\n /** Optional MetricsRegistry for instrumentation. Defaults to NoopMetricsRegistry. */\n metrics?: MetricsRegistry;\n}\n\n/**\n * In-memory cache adapter implementing ICacheService.\n *\n * Uses a Map-backed store with TTL-based expiry and LRU-style eviction.\n * Suitable for single-process environments, development, and testing.\n */\nexport class MemoryCacheAdapter implements ICacheService {\n private readonly store = new Map<string, CacheEntry>();\n private hits = 0;\n private misses = 0;\n private readonly maxSize: number;\n private readonly defaultTtl: number;\n private readonly metrics: MetricsRegistry;\n private static readonly LABELS = { adapter: 'memory' } as const;\n\n constructor(options: MemoryCacheAdapterOptions = {}) {\n this.maxSize = options.maxSize ?? 0;\n this.defaultTtl = options.defaultTtl ?? 0;\n this.metrics = options.metrics ?? new NoopMetricsRegistry();\n }\n\n private recordLookup(result: 'hit' | 'miss'): void {\n try {\n this.metrics.counter(SEMCONV.cacheLookupsTotal, { ...MemoryCacheAdapter.LABELS, result });\n } catch { /* never throw from instrumentation */ }\n }\n\n private recordWrite(op: 'set' | 'delete' | 'clear'): void {\n try {\n this.metrics.counter(SEMCONV.cacheWritesTotal, { ...MemoryCacheAdapter.LABELS, op });\n } catch { /* never throw from instrumentation */ }\n }\n\n async get<T = unknown>(key: string): Promise<T | undefined> {\n const entry = this.store.get(key);\n if (!entry || (entry.expires && Date.now() > entry.expires)) {\n if (entry) this.store.delete(key);\n this.misses++;\n this.recordLookup('miss');\n return undefined;\n }\n this.hits++;\n this.recordLookup('hit');\n return entry.value as T;\n }\n\n async set<T = unknown>(key: string, value: T, ttl?: number): Promise<void> {\n const effectiveTtl = ttl ?? this.defaultTtl;\n if (this.maxSize > 0 && !this.store.has(key) && this.store.size >= this.maxSize) {\n // Evict oldest entry (first key in Map insertion order)\n const firstKey = this.store.keys().next().value;\n if (firstKey !== undefined) this.store.delete(firstKey);\n }\n this.store.set(key, {\n value,\n expires: effectiveTtl > 0 ? Date.now() + effectiveTtl * 1000 : undefined,\n });\n this.recordWrite('set');\n }\n\n async delete(key: string): Promise<boolean> {\n const result = this.store.delete(key);\n this.recordWrite('delete');\n return result;\n }\n\n async has(key: string): Promise<boolean> {\n const entry = this.store.get(key);\n if (!entry) {\n this.recordLookup('miss');\n return false;\n }\n if (entry.expires && Date.now() > entry.expires) {\n this.store.delete(key);\n this.recordLookup('miss');\n return false;\n }\n this.recordLookup('hit');\n return true;\n }\n\n async clear(): Promise<void> {\n this.store.clear();\n this.recordWrite('clear');\n }\n\n async stats(): Promise<CacheStats> {\n return {\n hits: this.hits,\n misses: this.misses,\n keyCount: this.store.size,\n };\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { ICacheService, CacheStats } from '@objectstack/spec/contracts';\n\n/**\n * Configuration for the Redis cache adapter.\n */\nexport interface RedisCacheAdapterOptions {\n /** Redis connection URL (e.g. 'redis://localhost:6379') */\n url: string;\n /** Key prefix for namespacing (default: 'os:') */\n keyPrefix?: string;\n /** Default TTL in seconds (0 = no expiry) */\n defaultTtl?: number;\n}\n\n/**\n * Redis cache adapter skeleton implementing ICacheService.\n *\n * This is a placeholder for future Redis integration.\n * Concrete implementation will use `ioredis` or `redis` client.\n *\n * @example\n * ```ts\n * const cache = new RedisCacheAdapter({ url: 'redis://localhost:6379' });\n * await cache.set('user:1', { name: 'Alice' }, 3600);\n * ```\n */\nexport class RedisCacheAdapter implements ICacheService {\n private readonly url: string;\n private readonly keyPrefix: string;\n private readonly defaultTtl: number;\n\n constructor(options: RedisCacheAdapterOptions) {\n this.url = options.url;\n this.keyPrefix = options.keyPrefix ?? 'os:';\n this.defaultTtl = options.defaultTtl ?? 0;\n }\n\n async get<T = unknown>(_key: string): Promise<T | undefined> {\n throw new Error(`RedisCacheAdapter not yet implemented (url: ${this.url}, prefix: ${this.keyPrefix}, ttl: ${this.defaultTtl})`);\n }\n\n async set<T = unknown>(_key: string, _value: T, _ttl?: number): Promise<void> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n\n async delete(_key: string): Promise<boolean> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n\n async has(_key: string): Promise<boolean> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n\n async clear(): Promise<void> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n\n async stats(): Promise<CacheStats> {\n throw new Error('RedisCacheAdapter not yet implemented');\n }\n}\n"],"mappings":"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","names":["NoopMetricsRegistry","NoopMetricsRegistry"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/service-cache",
3
- "version": "5.1.0",
3
+ "version": "6.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Cache Service for ObjectStack — implements ICacheService with in-memory and Redis adapters",
6
6
  "type": "module",
@@ -14,9 +14,9 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "@objectstack/core": "5.1.0",
18
- "@objectstack/observability": "5.1.0",
19
- "@objectstack/spec": "5.1.0"
17
+ "@objectstack/core": "6.0.0",
18
+ "@objectstack/observability": "6.0.0",
19
+ "@objectstack/spec": "6.0.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^25.9.1",