@c9up/echo 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/CacheManager.d.ts +63 -28
  2. package/dist/CacheManager.d.ts.map +1 -1
  3. package/dist/CacheManager.js +348 -70
  4. package/dist/CacheManager.js.map +1 -1
  5. package/dist/EchoProvider.d.ts +15 -9
  6. package/dist/EchoProvider.d.ts.map +1 -1
  7. package/dist/EchoProvider.js +47 -13
  8. package/dist/EchoProvider.js.map +1 -1
  9. package/dist/StoreManager.d.ts +61 -0
  10. package/dist/StoreManager.d.ts.map +1 -0
  11. package/dist/StoreManager.js +70 -0
  12. package/dist/StoreManager.js.map +1 -0
  13. package/dist/drivers/MemoryDriver.d.ts +13 -6
  14. package/dist/drivers/MemoryDriver.d.ts.map +1 -1
  15. package/dist/drivers/MemoryDriver.js +67 -61
  16. package/dist/drivers/MemoryDriver.js.map +1 -1
  17. package/dist/drivers/RedisDriver.d.ts +17 -17
  18. package/dist/drivers/RedisDriver.d.ts.map +1 -1
  19. package/dist/drivers/RedisDriver.js +103 -55
  20. package/dist/drivers/RedisDriver.js.map +1 -1
  21. package/dist/drivers/TieredDriver.d.ts +41 -0
  22. package/dist/drivers/TieredDriver.d.ts.map +1 -0
  23. package/dist/drivers/TieredDriver.js +132 -0
  24. package/dist/drivers/TieredDriver.js.map +1 -0
  25. package/dist/duration.d.ts +32 -0
  26. package/dist/duration.d.ts.map +1 -0
  27. package/dist/duration.js +75 -0
  28. package/dist/duration.js.map +1 -0
  29. package/dist/errors.d.ts +21 -0
  30. package/dist/errors.d.ts.map +1 -0
  31. package/dist/errors.js +31 -0
  32. package/dist/errors.js.map +1 -0
  33. package/dist/index.d.ts +15 -3
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +10 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/testing/main.d.ts +41 -0
  38. package/dist/testing/main.d.ts.map +1 -0
  39. package/dist/testing/main.js +41 -0
  40. package/dist/testing/main.js.map +1 -0
  41. package/dist/types.d.ts +146 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +6 -0
  44. package/dist/types.js.map +1 -0
  45. package/package.json +6 -1
  46. package/src/CacheManager.ts +497 -99
  47. package/src/EchoProvider.ts +58 -15
  48. package/src/StoreManager.ts +104 -0
  49. package/src/drivers/MemoryDriver.ts +97 -66
  50. package/src/drivers/RedisDriver.ts +135 -58
  51. package/src/drivers/TieredDriver.ts +186 -0
  52. package/src/duration.ts +86 -0
  53. package/src/errors.ts +33 -0
  54. package/src/index.ts +43 -3
  55. package/src/testing/main.ts +69 -0
  56. package/src/types.ts +156 -0
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `@c9up/echo/testing` — helpers for testing code that depends on the cache.
3
+ *
4
+ * import { createTestCache } from "@c9up/echo/testing"
5
+ *
6
+ * const { cache, events, dispose } = createTestCache()
7
+ * await cache.set({ key: "k", value: 1 })
8
+ * expect(events).toContainEqual({ event: "cache:written", payload: { key: "k", value: 1, store: "test" } })
9
+ * dispose()
10
+ */
11
+
12
+ import { CacheManager } from "../CacheManager.js";
13
+ import { MemoryDriver } from "../drivers/MemoryDriver.js";
14
+ import type { Duration } from "../duration.js";
15
+ import type { CacheEmitter } from "../types.js";
16
+
17
+ /** A single recorded cache event. */
18
+ export interface RecordedEvent {
19
+ event: string;
20
+ payload: unknown;
21
+ }
22
+
23
+ export interface CreateTestCacheOptions {
24
+ prefix?: string;
25
+ /** Default TTL in seconds. */
26
+ ttl?: number;
27
+ grace?: Duration;
28
+ name?: string;
29
+ }
30
+
31
+ export interface TestCache {
32
+ /** A ready-to-use in-memory cache. */
33
+ cache: CacheManager;
34
+ /** The underlying driver (for direct assertions / setup). */
35
+ driver: MemoryDriver;
36
+ /** Every event emitted by the cache, in order. */
37
+ events: RecordedEvent[];
38
+ /** Stop the driver's sweep timer. Call in `afterEach`. */
39
+ dispose(): void;
40
+ }
41
+
42
+ /**
43
+ * Build an isolated in-memory {@link CacheManager} with a recording emitter, for
44
+ * unit tests. Each call is fully isolated (fresh driver + event log).
45
+ */
46
+ export function createTestCache(options?: CreateTestCacheOptions): TestCache {
47
+ const driver = new MemoryDriver();
48
+ const events: RecordedEvent[] = [];
49
+ const emitter: CacheEmitter = {
50
+ emit(event, payload) {
51
+ events.push({ event, payload });
52
+ },
53
+ };
54
+ const cache = new CacheManager(driver, {
55
+ prefix: options?.prefix,
56
+ ttl: options?.ttl,
57
+ grace: options?.grace,
58
+ name: options?.name ?? "test",
59
+ emitter,
60
+ });
61
+ return {
62
+ cache,
63
+ driver,
64
+ events,
65
+ dispose() {
66
+ driver.destroy();
67
+ },
68
+ };
69
+ }
package/src/types.ts ADDED
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Shared cache contracts — driver interface, stored-entry shape, method option
3
+ * objects (bentocache/@adonisjs/cache parity) and the event emitter contract.
4
+ */
5
+
6
+ import type { Duration } from "./duration.js";
7
+ import type { FactoryError } from "./errors.js";
8
+
9
+ /**
10
+ * A driver-level entry. `stale` is `true` when the value is past its logical
11
+ * TTL but still retained under the grace period (stale-while-revalidate).
12
+ */
13
+ export interface CacheEntry<T = unknown> {
14
+ value: T;
15
+ stale: boolean;
16
+ /**
17
+ * Logical expiry as an epoch-ms timestamp; `0` means "never expires".
18
+ * `undefined` means the driver does not track expiry. Lets a composing
19
+ * driver (e.g. {@link TieredDriver}) preserve the remaining TTL when it
20
+ * promotes an entry between tiers instead of resetting it to immortal.
21
+ */
22
+ expiresAt?: number;
23
+ }
24
+
25
+ /** Options for the grace-aware {@link CacheDriver.setEntry}. */
26
+ export interface DriverSetOptions {
27
+ /** Logical TTL in seconds; `0`/omitted means never expires. */
28
+ ttlSeconds?: number;
29
+ /** Extra retention beyond the logical TTL, in seconds (grace period). */
30
+ graceSeconds?: number;
31
+ /** Tags for grouped invalidation (only honoured by taggable drivers). */
32
+ tags?: string[];
33
+ /**
34
+ * Absolute logical expiry as an epoch-ms timestamp. When set it OVERRIDES
35
+ * `ttlSeconds` for the logical-freshness boundary (a past value makes the
36
+ * entry immediately stale), while `graceSeconds` still governs physical
37
+ * retention from now. Used by {@link CacheManager.expire} to mark a key stale
38
+ * at once without deleting it.
39
+ */
40
+ expiresAt?: number;
41
+ }
42
+
43
+ /**
44
+ * Cache driver contract.
45
+ *
46
+ * The five core methods (`get`/`set`/`delete`/`flush`/`has`) are the minimal
47
+ * surface — any KV store satisfies them. `getEntry`/`setEntry` are the optional
48
+ * grace-aware extensions; {@link CacheManager} falls back to `get`/`set` when a
49
+ * driver doesn't implement them, so grace simply degrades to "no stale window"
50
+ * on minimal drivers (agnostic-friendly).
51
+ */
52
+ export interface CacheDriver {
53
+ get<T = unknown>(key: string): Promise<T | null>;
54
+ set(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
55
+ delete(key: string): Promise<boolean>;
56
+ flush(): Promise<void>;
57
+ has(key: string): Promise<boolean>;
58
+ /** Grace-aware read: returns the entry even if stale, or `null` if physically gone. */
59
+ getEntry?<T = unknown>(key: string): Promise<CacheEntry<T> | null>;
60
+ /** Grace-aware write. */
61
+ setEntry?(
62
+ key: string,
63
+ value: unknown,
64
+ options: DriverSetOptions,
65
+ ): Promise<void>;
66
+ }
67
+
68
+ /** A driver that supports tag-based grouped invalidation. */
69
+ export interface TaggableDriver extends CacheDriver {
70
+ setWithTags(
71
+ key: string,
72
+ value: unknown,
73
+ tags: string[],
74
+ ttlSeconds?: number,
75
+ ): Promise<void>;
76
+ /** Invalidate every entry carrying any of the given tags (bento `deleteByTag`). */
77
+ deleteByTag(tags: string[]): Promise<void>;
78
+ /** @deprecated alias of {@link deleteByTag} (echo pre-0.2 name). */
79
+ flushTags(tags: string[]): Promise<void>;
80
+ }
81
+
82
+ /** A `getOrSet` factory. */
83
+ export type Factory<T> = () => T | Promise<T>;
84
+
85
+ /** A lazily-resolved default value for `get`. */
86
+ export type DefaultValue<T> = T | (() => T);
87
+
88
+ export interface GetOptions<T = unknown> {
89
+ key: string;
90
+ defaultValue?: DefaultValue<T>;
91
+ grace?: Duration;
92
+ }
93
+
94
+ export interface SetOptions {
95
+ key: string;
96
+ value: unknown;
97
+ ttl?: Duration;
98
+ grace?: Duration;
99
+ tags?: string[];
100
+ }
101
+
102
+ export interface GetOrSetOptions<T> {
103
+ key: string;
104
+ factory: Factory<T>;
105
+ ttl?: Duration;
106
+ grace?: Duration;
107
+ /** Soft timeout: on a stale hit, return the stale value if the factory is slower, then refresh in the background. */
108
+ timeout?: Duration;
109
+ /** Hard timeout: reject with {@link TimeoutError} if the factory exceeds it (even without a stale fallback). */
110
+ hardTimeout?: Duration;
111
+ /** Max time to wait on another caller's in-flight factory before falling back to stale (single-flight lock). */
112
+ lockTimeout?: Duration;
113
+ tags?: string[];
114
+ /** Observe factory failures (foreground and background). */
115
+ onFactoryError?: (error: FactoryError) => void;
116
+ }
117
+
118
+ export type GetOrSetForeverOptions<T> = Omit<GetOrSetOptions<T>, "ttl">;
119
+
120
+ export interface DeleteOptions {
121
+ key: string;
122
+ }
123
+ export interface DeleteManyOptions {
124
+ keys: string[];
125
+ }
126
+ export interface DeleteByTagOptions {
127
+ tags: string[];
128
+ }
129
+ export interface HasOptions {
130
+ key: string;
131
+ }
132
+ export interface ExpireOptions {
133
+ key: string;
134
+ }
135
+
136
+ /** Payloads for each emitted cache event (bento event-name parity). */
137
+ export interface CacheEventMap {
138
+ "cache:hit": {
139
+ key: string;
140
+ value: unknown;
141
+ store: string;
142
+ graced: boolean;
143
+ };
144
+ "cache:miss": { key: string; store: string };
145
+ "cache:written": { key: string; value: unknown; store: string };
146
+ "cache:deleted": { key: string; store: string };
147
+ "cache:cleared": { store: string };
148
+ }
149
+
150
+ /**
151
+ * Duck-typed event emitter — any object exposing `emit(event, payload)` works
152
+ * (ream's emitter, Node's EventEmitter, mitt, …). echo never imports one.
153
+ */
154
+ export interface CacheEmitter {
155
+ emit(event: string, payload: unknown): void;
156
+ }