@holo-js/cache 0.1.9 → 0.2.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.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { CacheFacade, CacheRepository, CacheFlexibleTtlInput, CacheFallback, CacheValueResolver, CacheDriverContract, CacheOptionalPackageError, CacheDependencyIndex, CacheQueryBridge, CacheKeyInput, CacheDependencyDescriptor, CacheRuntimeBindings, CacheKey, CacheTtlInput, CacheLockContract, defineCacheKey, normalizeCacheTtl, serializeCacheValue, deserializeCacheValue } from './contracts.js';
1
+ import { CacheFlexibleTtlInput, CacheFacade, CacheRepository, CacheFallback, CacheValueResolver, CacheOptionalPackageError, CacheDriverContract, CacheDependencyIndex, CacheQueryBridge, CacheKeyInput, CacheDependencyDescriptor, CacheRuntimeBindings, CacheKey, CacheTtlInput, CacheLockContract, defineCacheKey, normalizeCacheTtl, serializeCacheValue, deserializeCacheValue } from './contracts.js';
2
2
  export { CacheConfigError, CacheDriverGetResult, CacheDriverPutInput, CacheDriverResolutionError, CacheError, CacheErrorCode, CacheFallbackResolver, CacheInvalidNumericMutationError, CacheInvalidTtlError, CacheLockAcquisitionError, CacheQueryIntegrationError, CacheRuntimeNotConfiguredError, CacheSerializationError, NormalizedCacheTtl, cacheContractsInternals, isCacheKey, resolveCacheKey } from './contracts.js';
3
3
  import { HoloDatabaseConfig, NormalizedHoloDatabaseConfig, HoloDatabaseConnectionConfig, HoloRedisConfig, NormalizedHoloRedisConfig, NormalizedHoloRedisConnectionConfig, NormalizedHoloCacheConfig, HoloCacheConfig } from '@holo-js/config';
4
4
  export { CacheDatabaseDriverConfig, CacheDriver, CacheDriverConfig, CacheFileDriverConfig, CacheMemoryDriverConfig, CacheRedisDriverConfig, HoloCacheConfig, NormalizedCacheDatabaseDriverConfig, NormalizedCacheDriverConfig, NormalizedCacheFileDriverConfig, NormalizedCacheMemoryDriverConfig, NormalizedCacheRedisDriverConfig, NormalizedHoloCacheConfig, defineCacheConfig } from '@holo-js/config';
@@ -13,11 +13,12 @@ type NormalizedFlexibleTtl = {
13
13
  readonly freshSeconds: number;
14
14
  readonly staleSeconds: number;
15
15
  };
16
+ declare function normalizeFlexibleTtl(ttl: CacheFlexibleTtlInput): NormalizedFlexibleTtl;
17
+ declare function isFlexibleEnvelope<TValue>(value: unknown): value is FlexibleEnvelope<TValue>;
18
+
16
19
  declare function resolveFallback<TValue>(fallback: CacheFallback<TValue>): Promise<TValue> | TValue;
17
20
  declare function resolveValue<TValue>(callback: CacheValueResolver<TValue>): Promise<Awaited<TValue>>;
18
21
  declare function resolveDriverKey(driverName?: string): string;
19
- declare function normalizeFlexibleTtl(ttl: CacheFlexibleTtlInput): NormalizedFlexibleTtl;
20
- declare function isFlexibleEnvelope<TValue>(value: unknown): value is FlexibleEnvelope<TValue>;
21
22
  declare function getOrCreateRepository(driverName?: string): CacheRepository;
22
23
  declare const cacheFacade: CacheFacade;
23
24
  declare const cacheFacadeInternals: {
@@ -30,6 +31,9 @@ declare const cacheFacadeInternals: {
30
31
  resolveValue: typeof resolveValue;
31
32
  };
32
33
 
34
+ type OptionalDriverModuleLoader<TModule> = () => Promise<TModule>;
35
+ declare function normalizeOptionalDriverModuleLoadError(error: unknown, expectedSpecifier: string, message: string): CacheOptionalPackageError | unknown;
36
+
33
37
  type DatabaseCacheDriverOptions = {
34
38
  readonly name: string;
35
39
  readonly connectionName: string;
@@ -41,19 +45,18 @@ type DatabaseCacheDriverOptions = {
41
45
  type DatabaseCacheDriverModule = {
42
46
  createDatabaseCacheDriver(options: DatabaseCacheDriverOptions): CacheDriverContract;
43
47
  };
44
- type DatabaseDriverModuleLoader = () => Promise<DatabaseCacheDriverModule>;
48
+ type DatabaseDriverModuleLoader = OptionalDriverModuleLoader<DatabaseCacheDriverModule>;
45
49
  declare function isNormalizedDatabaseConfig(config: HoloDatabaseConfig | NormalizedHoloDatabaseConfig): config is NormalizedHoloDatabaseConfig;
46
50
  declare function normalizeRuntimeDatabaseConfig(config: HoloDatabaseConfig | NormalizedHoloDatabaseConfig | undefined): NormalizedHoloDatabaseConfig | undefined;
47
51
  declare function resolveSharedDatabaseConnection(databaseConfig: NormalizedHoloDatabaseConfig | undefined, connectionName: string): HoloDatabaseConnectionConfig | string;
48
52
  declare function isModuleNotFoundError$1(error: unknown, expectedSpecifier?: string): boolean;
49
- declare function normalizeDatabaseModuleLoadError(error: unknown, expectedSpecifier?: string): CacheOptionalPackageError | unknown;
50
- declare function loadDatabaseDriverModule(): Promise<DatabaseCacheDriverModule>;
53
+ declare function normalizeDatabaseModuleLoadError(error: unknown, expectedSpecifier?: string): ReturnType<typeof normalizeOptionalDriverModuleLoadError>;
51
54
  declare function setDatabaseDriverModuleLoader(loader: DatabaseDriverModuleLoader): void;
52
55
  declare function resetDatabaseDriverModuleLoader(): void;
53
56
  declare const cacheDbInternals: {
54
57
  isModuleNotFoundError: typeof isModuleNotFoundError$1;
55
58
  isNormalizedDatabaseConfig: typeof isNormalizedDatabaseConfig;
56
- loadDatabaseDriverModule: typeof loadDatabaseDriverModule;
59
+ loadDatabaseDriverModule: OptionalDriverModuleLoader<DatabaseCacheDriverModule>;
57
60
  normalizeDatabaseModuleLoadError: typeof normalizeDatabaseModuleLoadError;
58
61
  normalizeRuntimeDatabaseConfig: typeof normalizeRuntimeDatabaseConfig;
59
62
  resolveSharedDatabaseConnection: typeof resolveSharedDatabaseConnection;
@@ -119,19 +122,18 @@ type RedisCacheDriverOptions = {
119
122
  type RedisCacheDriverModule = {
120
123
  createRedisCacheDriver(options: RedisCacheDriverOptions): CacheDriverContract;
121
124
  };
122
- type RedisDriverModuleLoader = () => Promise<RedisCacheDriverModule>;
125
+ type RedisDriverModuleLoader = OptionalDriverModuleLoader<RedisCacheDriverModule>;
123
126
  declare function isNormalizedRedisConfig(config: HoloRedisConfig | NormalizedHoloRedisConfig): config is NormalizedHoloRedisConfig;
124
127
  declare function normalizeRuntimeRedisConfig(config: HoloRedisConfig | NormalizedHoloRedisConfig | undefined): NormalizedHoloRedisConfig | undefined;
125
128
  declare function resolveSharedRedisConnection(redisConfig: NormalizedHoloRedisConfig | undefined, connectionName: string): NormalizedHoloRedisConnectionConfig;
126
129
  declare function isModuleNotFoundError(error: unknown, expectedSpecifier?: string): boolean;
127
- declare function normalizeRedisModuleLoadError(error: unknown, expectedSpecifier?: string): CacheOptionalPackageError | unknown;
128
- declare function loadRedisDriverModule(): Promise<RedisCacheDriverModule>;
130
+ declare function normalizeRedisModuleLoadError(error: unknown, expectedSpecifier?: string): ReturnType<typeof normalizeOptionalDriverModuleLoadError>;
129
131
  declare function setRedisDriverModuleLoader(loader: RedisDriverModuleLoader): void;
130
132
  declare function resetRedisDriverModuleLoader(): void;
131
133
  declare const cacheRedisInternals: {
132
134
  isModuleNotFoundError: typeof isModuleNotFoundError;
133
135
  isNormalizedRedisConfig: typeof isNormalizedRedisConfig;
134
- loadRedisDriverModule: typeof loadRedisDriverModule;
136
+ loadRedisDriverModule: OptionalDriverModuleLoader<RedisCacheDriverModule>;
135
137
  normalizeRedisModuleLoadError: typeof normalizeRedisModuleLoadError;
136
138
  normalizeRuntimeRedisConfig: typeof normalizeRuntimeRedisConfig;
137
139
  resolveSharedRedisConnection: typeof resolveSharedRedisConnection;
package/dist/index.mjs CHANGED
@@ -21,6 +21,78 @@ import {
21
21
  // src/index.ts
22
22
  import { defineCacheConfig } from "@holo-js/config";
23
23
 
24
+ // src/flexible.ts
25
+ function normalizeFlexibleTtl(ttl) {
26
+ const freshSeconds = "fresh" in ttl ? ttl.fresh : ttl[0];
27
+ const staleSeconds = "stale" in ttl ? ttl.stale : ttl[1];
28
+ if (!Number.isInteger(freshSeconds) || freshSeconds < 0) {
29
+ throw new CacheInvalidTtlError("[@holo-js/cache] Flexible fresh TTL must be an integer greater than or equal to 0.");
30
+ }
31
+ if (!Number.isInteger(staleSeconds) || staleSeconds < freshSeconds) {
32
+ throw new CacheInvalidTtlError("[@holo-js/cache] Flexible stale TTL must be an integer greater than or equal to the fresh TTL.");
33
+ }
34
+ return Object.freeze({
35
+ freshSeconds,
36
+ staleSeconds
37
+ });
38
+ }
39
+ function createFlexibleEnvelope(ttl, value, now = Date.now()) {
40
+ return Object.freeze({
41
+ __holo_cache_flexible: true,
42
+ value,
43
+ freshUntil: now + ttl.freshSeconds * 1e3,
44
+ staleUntil: now + ttl.staleSeconds * 1e3
45
+ });
46
+ }
47
+ function isFlexibleEnvelope(value) {
48
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
49
+ return false;
50
+ }
51
+ const envelope = value;
52
+ return envelope.__holo_cache_flexible === true && typeof envelope.freshUntil === "number" && Number.isFinite(envelope.freshUntil) && typeof envelope.staleUntil === "number" && Number.isFinite(envelope.staleUntil) && "value" in envelope;
53
+ }
54
+ function resolveFlexibleEnvelopeState(envelope, now = Date.now()) {
55
+ if (now <= envelope.freshUntil) {
56
+ return "fresh";
57
+ }
58
+ if (now <= envelope.staleUntil) {
59
+ return "stale";
60
+ }
61
+ return "expired";
62
+ }
63
+ async function resolveFlexibleCachedValue(options) {
64
+ const normalizedTtl = normalizeFlexibleTtl(options.ttl);
65
+ const cached = await options.read();
66
+ const now = options.now?.() ?? Date.now();
67
+ if (isFlexibleEnvelope(cached)) {
68
+ const state = resolveFlexibleEnvelopeState(cached, now);
69
+ if (state === "fresh") {
70
+ return cached.value;
71
+ }
72
+ if (state === "stale") {
73
+ const refreshLock2 = options.createLock(normalizedTtl);
74
+ void refreshLock2.get(async () => {
75
+ await options.refresh(normalizedTtl);
76
+ return true;
77
+ }).catch(() => void 0);
78
+ return cached.value;
79
+ }
80
+ }
81
+ const refreshLock = options.createLock(normalizedTtl);
82
+ const refreshed = await refreshLock.block(
83
+ options.blockSeconds?.(normalizedTtl) ?? 1,
84
+ async () => options.refresh(normalizedTtl)
85
+ );
86
+ if (refreshed !== false) {
87
+ return refreshed;
88
+ }
89
+ const retried = await options.read();
90
+ if (isFlexibleEnvelope(retried) && resolveFlexibleEnvelopeState(retried, now) !== "expired") {
91
+ return retried.value;
92
+ }
93
+ return options.refresh(normalizedTtl);
94
+ }
95
+
24
96
  // src/runtime-shared.ts
25
97
  import {
26
98
  holoCacheDefaults,
@@ -28,36 +100,19 @@ import {
28
100
  } from "@holo-js/config";
29
101
 
30
102
  // src/db.ts
31
- import { createRequire } from "module";
32
- import { join } from "path";
33
- import { pathToFileURL } from "url";
34
103
  import {
35
104
  normalizeDatabaseConfig
36
105
  } from "@holo-js/config";
106
+
107
+ // src/optional-driver.ts
108
+ import { createRequire } from "module";
109
+ import { join } from "path";
110
+ import { pathToFileURL } from "url";
111
+ var CACHE_DRIVER_DISPOSE_SYMBOL = /* @__PURE__ */ Symbol.for("holo.cache.driver.dispose");
37
112
  function escapeRegExp(value) {
38
113
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
39
114
  }
40
- function isNormalizedDatabaseConfig(config) {
41
- return typeof config === "object" && config !== null && typeof config.connections === "object" && config.connections !== null;
42
- }
43
- function normalizeRuntimeDatabaseConfig(config) {
44
- if (!config) return void 0;
45
- return normalizeDatabaseConfig(config);
46
- }
47
- function resolveSharedDatabaseConnection(databaseConfig, connectionName) {
48
- if (!databaseConfig) {
49
- throw new CacheDriverResolutionError(
50
- `[@holo-js/cache] Database cache driver "${connectionName}" requires a top-level database config from config/database.ts.`
51
- );
52
- }
53
- const connection = databaseConfig.connections[connectionName];
54
- if (connection) return connection;
55
- const availableConnections = Object.keys(databaseConfig.connections);
56
- throw new CacheDriverResolutionError(
57
- `[@holo-js/cache] Database cache connection "${connectionName}" was not found in config/database.ts. Available connections: ${availableConnections.join(", ") || "(none)"}.`
58
- );
59
- }
60
- function isModuleNotFoundError(error, expectedSpecifier = "@holo-js/cache-db") {
115
+ function isOptionalDriverModuleNotFoundError(error, expectedSpecifier) {
61
116
  if (!error || typeof error !== "object") {
62
117
  return false;
63
118
  }
@@ -73,63 +128,60 @@ function isModuleNotFoundError(error, expectedSpecifier = "@holo-js/cache-db") {
73
128
  return true;
74
129
  }
75
130
  if ("cause" in error) {
76
- return isModuleNotFoundError(error.cause, expectedSpecifier);
131
+ return isOptionalDriverModuleNotFoundError(error.cause, expectedSpecifier);
77
132
  }
78
133
  return false;
79
134
  }
80
- function normalizeDatabaseModuleLoadError(error, expectedSpecifier = "@holo-js/cache-db") {
81
- if (isModuleNotFoundError(error, expectedSpecifier)) {
82
- return new CacheOptionalPackageError(
83
- "[@holo-js/cache] Database cache support requires @holo-js/cache-db to be installed.",
84
- { cause: error }
85
- );
135
+ function normalizeOptionalDriverModuleLoadError(error, expectedSpecifier, message) {
136
+ if (isOptionalDriverModuleNotFoundError(error, expectedSpecifier)) {
137
+ return new CacheOptionalPackageError(message, { cause: error });
86
138
  }
87
139
  return error;
88
140
  }
89
- async function importDatabaseDriverModuleFromProject(specifier) {
141
+ async function importOptionalDriverModuleFromProject(specifier) {
90
142
  const projectRequire = createRequire(join(process.cwd(), "package.json"));
91
143
  return await import(pathToFileURL(projectRequire.resolve(specifier)).href);
92
144
  }
93
- async function loadDatabaseDriverModule() {
94
- const specifier = "@holo-js/cache-db";
95
- try {
96
- return await import(
97
- /* webpackIgnore: true */
98
- specifier
99
- );
100
- } catch (error) {
101
- if (!isModuleNotFoundError(error, specifier)) {
102
- throw normalizeDatabaseModuleLoadError(error, specifier);
103
- }
145
+ function createOptionalDriverModuleLoader(specifier, missingPackageMessage) {
146
+ return async () => {
104
147
  try {
105
- return await importDatabaseDriverModuleFromProject(specifier);
106
- } catch (fallbackError) {
107
- throw normalizeDatabaseModuleLoadError(fallbackError, specifier);
148
+ return await import(
149
+ /* webpackIgnore: true */
150
+ specifier
151
+ );
152
+ } catch (error) {
153
+ if (!isOptionalDriverModuleNotFoundError(error, specifier)) {
154
+ throw normalizeOptionalDriverModuleLoadError(error, specifier, missingPackageMessage);
155
+ }
156
+ try {
157
+ return await importOptionalDriverModuleFromProject(specifier);
158
+ } catch (fallbackError) {
159
+ throw normalizeOptionalDriverModuleLoadError(fallbackError, specifier, missingPackageMessage);
160
+ }
108
161
  }
109
- }
110
- }
111
- var databaseDriverModuleLoader = loadDatabaseDriverModule;
112
- function setDatabaseDriverModuleLoader(loader) {
113
- databaseDriverModuleLoader = loader;
114
- }
115
- function resetDatabaseDriverModuleLoader() {
116
- databaseDriverModuleLoader = loadDatabaseDriverModule;
162
+ };
117
163
  }
118
- var LazyDatabaseCacheDriver = class {
119
- constructor(options) {
120
- this.options = options;
164
+ var LazyOptionalCacheDriver = class {
165
+ constructor(lazyOptions) {
166
+ this.lazyOptions = lazyOptions;
121
167
  }
122
- driver = "database";
123
168
  driverInstance;
124
169
  pending;
170
+ disposalGeneration = 0;
125
171
  get name() {
126
- return this.options.name;
172
+ return this.lazyOptions.name;
173
+ }
174
+ get driver() {
175
+ return this.lazyOptions.driver;
127
176
  }
128
177
  async resolveDriver() {
129
178
  if (this.driverInstance) return this.driverInstance;
130
- this.pending ??= databaseDriverModuleLoader().then((module) => {
131
- const driver = module.createDatabaseCacheDriver(this.options);
132
- this.driverInstance = driver;
179
+ const pendingGeneration = this.disposalGeneration;
180
+ this.pending ??= this.lazyOptions.loadModule().then((module) => {
181
+ const driver = this.lazyOptions.createDriver(module, this.lazyOptions.options);
182
+ if (this.disposalGeneration === pendingGeneration) {
183
+ this.driverInstance = driver;
184
+ }
133
185
  return driver;
134
186
  }).finally(() => {
135
187
  this.pending = void 0;
@@ -139,6 +191,26 @@ var LazyDatabaseCacheDriver = class {
139
191
  async withDriver(callback) {
140
192
  return callback(await this.resolveDriver());
141
193
  }
194
+ [CACHE_DRIVER_DISPOSE_SYMBOL]() {
195
+ if (this.lazyOptions.disposeDriver !== true) {
196
+ return;
197
+ }
198
+ const pending = this.pending;
199
+ const driverInstance = this.driverInstance;
200
+ this.disposalGeneration += 1;
201
+ this.driverInstance = void 0;
202
+ this.pending = void 0;
203
+ if (driverInstance) {
204
+ const disposable = driverInstance;
205
+ disposable[CACHE_DRIVER_DISPOSE_SYMBOL]?.();
206
+ return;
207
+ }
208
+ pending?.then((driver) => {
209
+ const disposable = driver;
210
+ disposable[CACHE_DRIVER_DISPOSE_SYMBOL]?.();
211
+ }).catch(() => {
212
+ });
213
+ }
142
214
  createLockProxy(name, seconds) {
143
215
  let lockPromise;
144
216
  const resolveLock = async () => {
@@ -183,8 +255,58 @@ var LazyDatabaseCacheDriver = class {
183
255
  return this.createLockProxy(name, seconds);
184
256
  }
185
257
  };
258
+ function createLazyOptionalCacheDriver(options) {
259
+ return new LazyOptionalCacheDriver(options);
260
+ }
261
+
262
+ // src/db.ts
263
+ var DATABASE_CACHE_PACKAGE = "@holo-js/cache-db";
264
+ var DATABASE_CACHE_MISSING_MESSAGE = "[@holo-js/cache] Database cache support requires @holo-js/cache-db to be installed.";
265
+ function isNormalizedDatabaseConfig(config) {
266
+ return typeof config === "object" && config !== null && typeof config.connections === "object" && config.connections !== null;
267
+ }
268
+ function normalizeRuntimeDatabaseConfig(config) {
269
+ if (!config) return void 0;
270
+ return normalizeDatabaseConfig(config);
271
+ }
272
+ function resolveSharedDatabaseConnection(databaseConfig, connectionName) {
273
+ if (!databaseConfig) {
274
+ throw new CacheDriverResolutionError(
275
+ `[@holo-js/cache] Database cache driver "${connectionName}" requires a top-level database config from config/database.ts.`
276
+ );
277
+ }
278
+ const connection = databaseConfig.connections[connectionName];
279
+ if (connection) return connection;
280
+ const availableConnections = Object.keys(databaseConfig.connections);
281
+ throw new CacheDriverResolutionError(
282
+ `[@holo-js/cache] Database cache connection "${connectionName}" was not found in config/database.ts. Available connections: ${availableConnections.join(", ") || "(none)"}.`
283
+ );
284
+ }
285
+ function isModuleNotFoundError(error, expectedSpecifier = "@holo-js/cache-db") {
286
+ return isOptionalDriverModuleNotFoundError(error, expectedSpecifier);
287
+ }
288
+ function normalizeDatabaseModuleLoadError(error, expectedSpecifier = "@holo-js/cache-db") {
289
+ return normalizeOptionalDriverModuleLoadError(error, expectedSpecifier, DATABASE_CACHE_MISSING_MESSAGE);
290
+ }
291
+ var loadDatabaseDriverModule = createOptionalDriverModuleLoader(
292
+ DATABASE_CACHE_PACKAGE,
293
+ DATABASE_CACHE_MISSING_MESSAGE
294
+ );
295
+ var databaseDriverModuleLoader = loadDatabaseDriverModule;
296
+ function setDatabaseDriverModuleLoader(loader) {
297
+ databaseDriverModuleLoader = loader;
298
+ }
299
+ function resetDatabaseDriverModuleLoader() {
300
+ databaseDriverModuleLoader = loadDatabaseDriverModule;
301
+ }
186
302
  function createDatabaseCacheDriver(options) {
187
- return new LazyDatabaseCacheDriver(options);
303
+ return createLazyOptionalCacheDriver({
304
+ name: options.name,
305
+ driver: "database",
306
+ options,
307
+ loadModule: databaseDriverModuleLoader,
308
+ createDriver: (module, driverOptions) => module.createDatabaseCacheDriver(driverOptions)
309
+ });
188
310
  }
189
311
  var cacheDbInternals = {
190
312
  isModuleNotFoundError,
@@ -845,13 +967,11 @@ function createMemoryCacheDriver(options) {
845
967
  }
846
968
 
847
969
  // src/redis.ts
848
- import { createRequire as createRequire2 } from "module";
849
- import { join as join3 } from "path";
850
- import { pathToFileURL as pathToFileURL2 } from "url";
851
970
  import {
852
971
  normalizeRedisConfig
853
972
  } from "@holo-js/config";
854
- var CACHE_DRIVER_DISPOSE_SYMBOL = /* @__PURE__ */ Symbol.for("holo.cache.driver.dispose");
973
+ var REDIS_CACHE_PACKAGE = "@holo-js/cache-redis";
974
+ var REDIS_CACHE_MISSING_MESSAGE = "[@holo-js/cache] Redis cache support requires @holo-js/cache-redis to be installed.";
855
975
  function isNormalizedRedisConfig(config) {
856
976
  return typeof config.default === "string" && typeof config.connections === "object" && config.connections !== null && Object.values(config.connections).every((connection) => {
857
977
  return typeof connection === "object" && connection !== null && "name" in connection && "host" in connection && "port" in connection && typeof connection.name === "string" && typeof connection.host === "string" && typeof connection.port === "number";
@@ -861,9 +981,6 @@ function normalizeRuntimeRedisConfig(config) {
861
981
  if (!config) return void 0;
862
982
  return isNormalizedRedisConfig(config) ? config : normalizeRedisConfig(config);
863
983
  }
864
- function escapeRegExp2(value) {
865
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
866
- }
867
984
  function resolveSharedRedisConnection(redisConfig, connectionName) {
868
985
  if (!redisConfig) {
869
986
  throw new CacheDriverResolutionError(
@@ -878,56 +995,15 @@ function resolveSharedRedisConnection(redisConfig, connectionName) {
878
995
  );
879
996
  }
880
997
  function isModuleNotFoundError2(error, expectedSpecifier = "@holo-js/cache-redis") {
881
- if (!error || typeof error !== "object") {
882
- return false;
883
- }
884
- const message = "message" in error && typeof error.message === "string" ? error.message : "";
885
- const code = "code" in error ? error.code : void 0;
886
- const escapedSpecifier = escapeRegExp2(expectedSpecifier);
887
- if ((code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") && [
888
- new RegExp(`Cannot find package ['"]${escapedSpecifier}['"]`),
889
- new RegExp(`Cannot find module ['"]${escapedSpecifier}['"]`),
890
- new RegExp(`Could not resolve ['"]${escapedSpecifier}['"]`),
891
- new RegExp(`Failed to load url\\s+(?:['"\`]${escapedSpecifier}['"\`]|${escapedSpecifier}(?=[\\s(]|$))`)
892
- ].some((pattern) => pattern.test(message))) {
893
- return true;
894
- }
895
- if ("cause" in error) {
896
- return isModuleNotFoundError2(error.cause, expectedSpecifier);
897
- }
898
- return false;
998
+ return isOptionalDriverModuleNotFoundError(error, expectedSpecifier);
899
999
  }
900
1000
  function normalizeRedisModuleLoadError(error, expectedSpecifier = "@holo-js/cache-redis") {
901
- if (isModuleNotFoundError2(error, expectedSpecifier)) {
902
- return new CacheOptionalPackageError(
903
- "[@holo-js/cache] Redis cache support requires @holo-js/cache-redis to be installed.",
904
- { cause: error }
905
- );
906
- }
907
- return error;
908
- }
909
- async function importRedisDriverModuleFromProject(specifier) {
910
- const projectRequire = createRequire2(join3(process.cwd(), "package.json"));
911
- return await import(pathToFileURL2(projectRequire.resolve(specifier)).href);
912
- }
913
- async function loadRedisDriverModule() {
914
- const specifier = "@holo-js/cache-redis";
915
- try {
916
- return await import(
917
- /* webpackIgnore: true */
918
- specifier
919
- );
920
- } catch (error) {
921
- if (!isModuleNotFoundError2(error, specifier)) {
922
- throw normalizeRedisModuleLoadError(error, specifier);
923
- }
924
- try {
925
- return await importRedisDriverModuleFromProject(specifier);
926
- } catch (fallbackError) {
927
- throw normalizeRedisModuleLoadError(fallbackError, specifier);
928
- }
929
- }
1001
+ return normalizeOptionalDriverModuleLoadError(error, expectedSpecifier, REDIS_CACHE_MISSING_MESSAGE);
930
1002
  }
1003
+ var loadRedisDriverModule = createOptionalDriverModuleLoader(
1004
+ REDIS_CACHE_PACKAGE,
1005
+ REDIS_CACHE_MISSING_MESSAGE
1006
+ );
931
1007
  var redisDriverModuleLoader = loadRedisDriverModule;
932
1008
  function setRedisDriverModuleLoader(loader) {
933
1009
  redisDriverModuleLoader = loader;
@@ -935,97 +1011,15 @@ function setRedisDriverModuleLoader(loader) {
935
1011
  function resetRedisDriverModuleLoader() {
936
1012
  redisDriverModuleLoader = loadRedisDriverModule;
937
1013
  }
938
- var LazyRedisCacheDriver = class {
939
- constructor(options) {
940
- this.options = options;
941
- }
942
- driver = "redis";
943
- driverInstance;
944
- pending;
945
- disposalGeneration = 0;
946
- get name() {
947
- return this.options.name;
948
- }
949
- async resolveDriver() {
950
- if (this.driverInstance) return this.driverInstance;
951
- const pendingGeneration = this.disposalGeneration;
952
- this.pending ??= redisDriverModuleLoader().then((module) => {
953
- const driver = module.createRedisCacheDriver(this.options);
954
- if (this.disposalGeneration === pendingGeneration) {
955
- this.driverInstance = driver;
956
- }
957
- return driver;
958
- }).finally(() => {
959
- this.pending = void 0;
960
- });
961
- return this.pending;
962
- }
963
- async withDriver(callback) {
964
- return callback(await this.resolveDriver());
965
- }
966
- [CACHE_DRIVER_DISPOSE_SYMBOL]() {
967
- const pending = this.pending;
968
- const driverInstance = this.driverInstance;
969
- this.disposalGeneration += 1;
970
- this.driverInstance = void 0;
971
- this.pending = void 0;
972
- if (driverInstance) {
973
- const disposable = driverInstance;
974
- disposable[CACHE_DRIVER_DISPOSE_SYMBOL]?.();
975
- return;
976
- }
977
- pending?.then((driver) => {
978
- const disposable = driver;
979
- disposable[CACHE_DRIVER_DISPOSE_SYMBOL]?.();
980
- }).catch(() => {
981
- });
982
- }
983
- createLockProxy(name, seconds) {
984
- let lockPromise;
985
- const resolveLock = async () => {
986
- lockPromise ??= this.withDriver((driver) => driver.lock(name, seconds));
987
- return lockPromise;
988
- };
989
- return {
990
- name,
991
- async get(callback) {
992
- return (await resolveLock()).get(callback);
993
- },
994
- async release() {
995
- return (await resolveLock()).release();
996
- },
997
- async block(waitSeconds, callback) {
998
- return (await resolveLock()).block(waitSeconds, callback);
999
- }
1000
- };
1001
- }
1002
- async get(key) {
1003
- return this.withDriver((driver) => driver.get(key));
1004
- }
1005
- async put(input) {
1006
- return this.withDriver((driver) => driver.put(input));
1007
- }
1008
- async add(input) {
1009
- return this.withDriver((driver) => driver.add(input));
1010
- }
1011
- async forget(key) {
1012
- return this.withDriver((driver) => driver.forget(key));
1013
- }
1014
- async flush() {
1015
- await this.withDriver((driver) => driver.flush());
1016
- }
1017
- async increment(key, amount) {
1018
- return this.withDriver((driver) => driver.increment(key, amount));
1019
- }
1020
- async decrement(key, amount) {
1021
- return this.withDriver((driver) => driver.decrement(key, amount));
1022
- }
1023
- lock(name, seconds) {
1024
- return this.createLockProxy(name, seconds);
1025
- }
1026
- };
1027
1014
  function createRedisCacheDriver(options) {
1028
- return new LazyRedisCacheDriver(options);
1015
+ return createLazyOptionalCacheDriver({
1016
+ name: options.name,
1017
+ driver: "redis",
1018
+ options,
1019
+ loadModule: redisDriverModuleLoader,
1020
+ createDriver: (module, driverOptions) => module.createRedisCacheDriver(driverOptions),
1021
+ disposeDriver: true
1022
+ });
1029
1023
  }
1030
1024
  var cacheRedisInternals = {
1031
1025
  isModuleNotFoundError: isModuleNotFoundError2,
@@ -1226,27 +1220,6 @@ function resolveNormalizedKey(key, driverName) {
1226
1220
  const context = resolveDriverContext(driverName);
1227
1221
  return `${context.normalizedKeyPrefix}${resolveCacheKey(key)}`;
1228
1222
  }
1229
- function normalizeFlexibleTtl(ttl) {
1230
- const freshSeconds = "fresh" in ttl ? ttl.fresh : ttl[0];
1231
- const staleSeconds = "stale" in ttl ? ttl.stale : ttl[1];
1232
- if (!Number.isInteger(freshSeconds) || freshSeconds < 0) {
1233
- throw new CacheInvalidTtlError("[@holo-js/cache] Flexible fresh TTL must be an integer greater than or equal to 0.");
1234
- }
1235
- if (!Number.isInteger(staleSeconds) || staleSeconds < freshSeconds) {
1236
- throw new CacheInvalidTtlError("[@holo-js/cache] Flexible stale TTL must be an integer greater than or equal to the fresh TTL.");
1237
- }
1238
- return Object.freeze({
1239
- freshSeconds,
1240
- staleSeconds
1241
- });
1242
- }
1243
- function isFlexibleEnvelope(value) {
1244
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1245
- return false;
1246
- }
1247
- const envelope = value;
1248
- return envelope.__holo_cache_flexible === true && typeof envelope.freshUntil === "number" && Number.isFinite(envelope.freshUntil) && typeof envelope.staleUntil === "number" && Number.isFinite(envelope.staleUntil) && "value" in envelope;
1249
- }
1250
1223
  async function getCachedValue(key, driverName) {
1251
1224
  const context = resolveDriverContext(driverName);
1252
1225
  const entry = await context.driver.get(resolveNormalizedKey(key, driverName));
@@ -1313,18 +1286,9 @@ function createCacheQueryBridge(dependencyIndex = getOrCreateDependencyIndex())
1313
1286
  },
1314
1287
  async flexible(key, ttl, callback, options = {}) {
1315
1288
  const indexedKey = createIndexedKey(key, options.driver);
1316
- const normalizedTtl = normalizeFlexibleTtl(ttl);
1317
- const now = Date.now();
1318
- const cached = await getCachedValue(key, options.driver);
1319
- const refreshValue = async () => {
1289
+ const refreshValue = async (normalizedTtl) => {
1320
1290
  const value = await callback();
1321
- const refreshedAt = Date.now();
1322
- const envelope = {
1323
- __holo_cache_flexible: true,
1324
- value,
1325
- freshUntil: refreshedAt + normalizedTtl.freshSeconds * 1e3,
1326
- staleUntil: refreshedAt + normalizedTtl.staleSeconds * 1e3
1327
- };
1291
+ const envelope = createFlexibleEnvelope(normalizedTtl, value);
1328
1292
  await putCachedValue(
1329
1293
  key,
1330
1294
  envelope,
@@ -1334,31 +1298,12 @@ function createCacheQueryBridge(dependencyIndex = getOrCreateDependencyIndex())
1334
1298
  await syncDependencies(indexedKey, options.dependencies);
1335
1299
  return value;
1336
1300
  };
1337
- if (isFlexibleEnvelope(cached)) {
1338
- if (now <= cached.freshUntil) {
1339
- return cached.value;
1340
- }
1341
- if (now <= cached.staleUntil) {
1342
- const refreshLock2 = createFlexibleLock(key, normalizedTtl, options.driver);
1343
- void refreshLock2.get(async () => {
1344
- await refreshValue();
1345
- return true;
1346
- }).catch(() => void 0);
1347
- return cached.value;
1348
- }
1349
- }
1350
- const refreshLock = createFlexibleLock(key, normalizedTtl, options.driver);
1351
- const refreshed = await refreshLock.block(1, async () => refreshValue());
1352
- if (refreshed !== false) {
1353
- return refreshed;
1354
- }
1355
- const retried = await getCachedValue(key, options.driver);
1356
- if (isFlexibleEnvelope(retried)) {
1357
- if (Date.now() <= retried.staleUntil) {
1358
- return retried.value;
1359
- }
1360
- }
1361
- return refreshValue();
1301
+ return resolveFlexibleCachedValue({
1302
+ ttl,
1303
+ read: async () => getCachedValue(key, options.driver),
1304
+ refresh: (normalizedTtl) => refreshValue(normalizedTtl),
1305
+ createLock: (normalizedTtl) => createFlexibleLock(key, normalizedTtl, options.driver)
1306
+ });
1362
1307
  },
1363
1308
  async forget(key, options) {
1364
1309
  const indexedKey = createIndexedKey(key, options?.driver);
@@ -1445,27 +1390,6 @@ function resolveDriverKey(driverName) {
1445
1390
  const normalized = driverName?.trim();
1446
1391
  return normalized || "__default__";
1447
1392
  }
1448
- function normalizeFlexibleTtl2(ttl) {
1449
- const freshSeconds = "fresh" in ttl ? ttl.fresh : ttl[0];
1450
- const staleSeconds = "stale" in ttl ? ttl.stale : ttl[1];
1451
- if (!Number.isInteger(freshSeconds) || freshSeconds < 0) {
1452
- throw new CacheInvalidTtlError("[@holo-js/cache] Flexible fresh TTL must be an integer greater than or equal to 0.");
1453
- }
1454
- if (!Number.isInteger(staleSeconds) || staleSeconds < freshSeconds) {
1455
- throw new CacheInvalidTtlError("[@holo-js/cache] Flexible stale TTL must be an integer greater than or equal to the fresh TTL.");
1456
- }
1457
- return Object.freeze({
1458
- freshSeconds,
1459
- staleSeconds
1460
- });
1461
- }
1462
- function isFlexibleEnvelope2(value) {
1463
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1464
- return false;
1465
- }
1466
- const envelope = value;
1467
- return envelope.__holo_cache_flexible === true && typeof envelope.freshUntil === "number" && Number.isFinite(envelope.freshUntil) && typeof envelope.staleUntil === "number" && Number.isFinite(envelope.staleUntil) && "value" in envelope;
1468
- }
1469
1393
  function createCacheRepository(driverName) {
1470
1394
  function resolveDriverContext2() {
1471
1395
  const runtime = getCacheRuntime();
@@ -1511,13 +1435,7 @@ function createCacheRepository(driverName) {
1511
1435
  });
1512
1436
  }
1513
1437
  async function putFlexibleEnvelope(key, ttl, value) {
1514
- const now = Date.now();
1515
- const envelope = {
1516
- __holo_cache_flexible: true,
1517
- value,
1518
- freshUntil: now + ttl.freshSeconds * 1e3,
1519
- staleUntil: now + ttl.staleSeconds * 1e3
1520
- };
1438
+ const envelope = createFlexibleEnvelope(ttl, value);
1521
1439
  await putSerializedValue(key, serializeCacheValue(envelope), ttl.staleSeconds);
1522
1440
  return value;
1523
1441
  }
@@ -1617,40 +1535,19 @@ function createCacheRepository(driverName) {
1617
1535
  return value;
1618
1536
  },
1619
1537
  async flexible(key, ttl, callback) {
1620
- const normalizedTtl = normalizeFlexibleTtl2(ttl);
1621
- const now = Date.now();
1622
- const cached = await getCachedValue2(key);
1623
- if (cached.hit && isFlexibleEnvelope2(cached.value)) {
1624
- if (now <= cached.value.freshUntil) {
1625
- return cached.value.value;
1626
- }
1627
- if (now <= cached.value.staleUntil) {
1628
- const refreshLock2 = createRefreshLock(key, normalizedTtl.staleSeconds);
1629
- void refreshLock2.get(async () => {
1630
- await refreshFlexibleValue(key, normalizedTtl, callback);
1631
- return true;
1632
- }).catch(() => void 0);
1633
- return cached.value.value;
1634
- }
1635
- }
1636
- const refreshLock = createRefreshLock(key, normalizedTtl.staleSeconds);
1637
- const refreshed = await refreshLock.block(
1638
- Math.min(
1538
+ return resolveFlexibleCachedValue({
1539
+ ttl,
1540
+ read: async () => {
1541
+ const cached = await getCachedValue2(key);
1542
+ return cached.hit ? cached.value : void 0;
1543
+ },
1544
+ refresh: (normalizedTtl) => refreshFlexibleValue(key, normalizedTtl, callback),
1545
+ createLock: (normalizedTtl) => createRefreshLock(key, normalizedTtl.staleSeconds),
1546
+ blockSeconds: (normalizedTtl) => Math.min(
1639
1547
  MAX_REFRESH_BLOCK_SECONDS,
1640
1548
  Math.max(1, Math.ceil(normalizedTtl.staleSeconds / 300))
1641
- ),
1642
- async () => refreshFlexibleValue(key, normalizedTtl, callback)
1643
- );
1644
- if (refreshed !== false) {
1645
- return refreshed;
1646
- }
1647
- const retried = await getCachedValue2(key);
1648
- if (retried.hit && isFlexibleEnvelope2(retried.value)) {
1649
- if (Date.now() <= retried.value.staleUntil) {
1650
- return retried.value.value;
1651
- }
1652
- }
1653
- return refreshFlexibleValue(key, normalizedTtl, callback);
1549
+ )
1550
+ });
1654
1551
  },
1655
1552
  lock(name, seconds) {
1656
1553
  const { driver } = resolveDriverContext2();
@@ -1685,8 +1582,8 @@ var cacheFacadeInternals = {
1685
1582
  return `__flexible__:${resolveCacheKey(key)}`;
1686
1583
  },
1687
1584
  getOrCreateRepository,
1688
- isFlexibleEnvelope: isFlexibleEnvelope2,
1689
- normalizeFlexibleTtl: normalizeFlexibleTtl2,
1585
+ isFlexibleEnvelope,
1586
+ normalizeFlexibleTtl,
1690
1587
  resolveDriverKey,
1691
1588
  resolveFallback,
1692
1589
  resolveValue
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holo-js/cache",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "description": "Holo-JS Framework - cache contracts, config helpers, serialization, and runtime seams",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,7 +28,7 @@
28
28
  "test": "vitest --run"
29
29
  },
30
30
  "dependencies": {
31
- "@holo-js/config": "^0.1.9"
31
+ "@holo-js/config": "^0.2.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^22.10.2",