safe-memory-layer 1.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/src/Stats.ts ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Statistics tracking for safe-memory-layer.
3
+ *
4
+ * Tracks store metrics including entry counts, expiration counts,
5
+ * deletion counts, cleanup cycles, uptime, and memory estimates.
6
+ *
7
+ * @module Stats
8
+ */
9
+
10
+ import type { StoreStats, CleanupStats } from "./types.js";
11
+
12
+ /**
13
+ * Tracks statistics for a MemoryStore instance.
14
+ *
15
+ * All counters are monotonically increasing (except entries).
16
+ */
17
+ export class Stats {
18
+ /** Total number of entries that have expired. */
19
+ #expired = 0;
20
+
21
+ /** Total number of entries that have been deleted. */
22
+ #deleted = 0;
23
+
24
+ /** Total number of cleanup cycles completed. */
25
+ #cleaned = 0;
26
+
27
+ /** Timestamp when the store was created. */
28
+ readonly #createdAt: number;
29
+
30
+ /** Current number of entries in the store. */
31
+ #entries = 0;
32
+
33
+ /**
34
+ * Creates a new Stats instance.
35
+ *
36
+ * @param createdAt - Timestamp when the store was created.
37
+ */
38
+ constructor(createdAt: number) {
39
+ this.#createdAt = createdAt;
40
+ }
41
+
42
+ /**
43
+ * Increments the expired counter.
44
+ */
45
+ incrementExpired(): void {
46
+ this.#expired++;
47
+ }
48
+
49
+ /**
50
+ * Increments the deleted counter.
51
+ */
52
+ incrementDeleted(): void {
53
+ this.#deleted++;
54
+ }
55
+
56
+ /**
57
+ * Increments the cleaned counter.
58
+ */
59
+ incrementCleaned(): void {
60
+ this.#cleaned++;
61
+ }
62
+
63
+ /**
64
+ * Sets the current number of entries.
65
+ *
66
+ * @param count - The current entry count.
67
+ */
68
+ setEntries(count: number): void {
69
+ this.#entries = count;
70
+ }
71
+
72
+ /**
73
+ * Returns the current statistics snapshot.
74
+ *
75
+ * @param currentEntries - Current number of entries (overrides internal count).
76
+ * @returns A snapshot of the current statistics.
77
+ */
78
+ snapshot(currentEntries?: number): StoreStats {
79
+ const uptime = Date.now() - this.#createdAt;
80
+ const entries = currentEntries ?? this.#entries;
81
+
82
+ return {
83
+ entries,
84
+ expired: this.#expired,
85
+ deleted: this.#deleted,
86
+ cleaned: this.#cleaned,
87
+ uptime,
88
+ memoryEstimate: this.estimateMemory(entries),
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Creates a cleanup stats object.
94
+ *
95
+ * @param removed - Number of entries removed.
96
+ * @param totalBefore - Total entries before cleanup.
97
+ * @param totalAfter - Total entries after cleanup.
98
+ * @param duration - Duration of cleanup in ms.
99
+ * @returns A CleanupStats object.
100
+ */
101
+ createCleanupStats(
102
+ removed: number,
103
+ totalBefore: number,
104
+ totalAfter: number,
105
+ duration: number,
106
+ ): CleanupStats {
107
+ return {
108
+ removed,
109
+ totalBefore,
110
+ totalAfter,
111
+ duration,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Estimates memory usage based on entry count.
117
+ * This is a rough approximation for monitoring purposes.
118
+ *
119
+ * @param entryCount - Number of entries.
120
+ * @returns Estimated memory usage in bytes.
121
+ */
122
+ estimateMemory(entryCount: number): number {
123
+ // Rough estimate: each entry is approximately 200 bytes
124
+ // This includes the key, value reference, and metadata
125
+ // Actual memory usage varies based on key/value sizes
126
+ return entryCount * 200;
127
+ }
128
+
129
+ /**
130
+ * Resets all statistics.
131
+ */
132
+ reset(): void {
133
+ this.#expired = 0;
134
+ this.#deleted = 0;
135
+ this.#cleaned = 0;
136
+ this.#entries = 0;
137
+ }
138
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * WeakMemoryStore implementation for safe-memory-layer.
3
+ *
4
+ * Provides an object-only storage layer using WeakMap internally.
5
+ * Objects disappear automatically after garbage collection.
6
+ * No iteration, no manual cleanup required.
7
+ *
8
+ * @module WeakMemoryStore
9
+ */
10
+
11
+ import type { WeakMemoryStoreOptions } from "./types.js";
12
+
13
+ /**
14
+ * A memory store that uses WeakMap for object-only storage.
15
+ *
16
+ * Keys must be objects. Values are held weakly and will be
17
+ * automatically garbage collected when no longer referenced elsewhere.
18
+ *
19
+ * **Limitations:**
20
+ * - Keys must be objects (not primitives)
21
+ * - No iteration support (WeakMap is not iterable)
22
+ * - No size property
23
+ * - No TTL support
24
+ * - No automatic cleanup needed
25
+ *
26
+ * @typeParam V - The type of values stored in the map.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const weakStore = new WeakMemoryStore<User>();
31
+ *
32
+ * const user = { id: 1, name: "Alice" };
33
+ * weakStore.set(user, userData);
34
+ *
35
+ * // When 'user' is no longer referenced, the entry is automatically removed
36
+ * ```
37
+ */
38
+ export class WeakMemoryStore<V> {
39
+ /** Internal WeakMap for storage. */
40
+ #map = new WeakMap<object, V>();
41
+
42
+ /** Store configuration options. */
43
+ readonly #options: WeakMemoryStoreOptions<V>;
44
+
45
+ /** Statistics tracker. */
46
+ #deleted = 0;
47
+
48
+ /** Timestamp when the store was created. */
49
+ readonly #createdAt: number;
50
+
51
+ /** Whether the store has been disposed. */
52
+ #disposed = false;
53
+
54
+ /**
55
+ * Creates a new WeakMemoryStore instance.
56
+ *
57
+ * @param options - Configuration options for the store.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const store = new WeakMemoryStore({
62
+ * onDelete: (value) => console.log("Deleted:", value)
63
+ * });
64
+ * ```
65
+ */
66
+ constructor(options: WeakMemoryStoreOptions<V> = {}) {
67
+ this.#options = options;
68
+ this.#createdAt = Date.now();
69
+ }
70
+
71
+ /**
72
+ * Stores a value with the given object key.
73
+ *
74
+ * @param key - The object key to store the value under. Must be an object.
75
+ * @param value - The value to store.
76
+ * @returns True if the value was stored.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * const obj = { id: 1 };
81
+ * weakStore.set(obj, { name: "Alice" });
82
+ * ```
83
+ */
84
+ set(key: object, value: V): boolean {
85
+ this.#checkDisposed();
86
+
87
+ // Type guard: ensure key is an object
88
+ if (typeof key !== "object" || key === null) {
89
+ throw new TypeError("WeakMemoryStore keys must be objects");
90
+ }
91
+
92
+ this.#map.set(key, value);
93
+ return true;
94
+ }
95
+
96
+ /**
97
+ * Retrieves a value by object key.
98
+ *
99
+ * @param key - The object key to look up.
100
+ * @returns The value if found, undefined otherwise.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * const user = weakStore.get(obj);
105
+ * ```
106
+ */
107
+ get(key: object): V | undefined {
108
+ this.#checkDisposed();
109
+
110
+ if (typeof key !== "object" || key === null) {
111
+ return undefined;
112
+ }
113
+
114
+ return this.#map.get(key);
115
+ }
116
+
117
+ /**
118
+ * Checks if a key exists in the store.
119
+ *
120
+ * @param key - The object key to check.
121
+ * @returns True if the key exists.
122
+ */
123
+ has(key: object): boolean {
124
+ this.#checkDisposed();
125
+
126
+ if (typeof key !== "object" || key === null) {
127
+ return false;
128
+ }
129
+
130
+ return this.#map.has(key);
131
+ }
132
+
133
+ /**
134
+ * Deletes a value by object key.
135
+ *
136
+ * @param key - The object key to delete.
137
+ * @returns True if the key was present, false otherwise.
138
+ */
139
+ delete(key: object): boolean {
140
+ this.#checkDisposed();
141
+
142
+ if (typeof key !== "object" || key === null) {
143
+ return false;
144
+ }
145
+
146
+ const value = this.#map.get(key);
147
+ if (value === undefined) return false;
148
+
149
+ this.#map.delete(key);
150
+ this.#deleted++;
151
+
152
+ // Emit delete event
153
+ if (this.#options.onDelete !== undefined) {
154
+ try {
155
+ this.#options.onDelete(value);
156
+ } catch {
157
+ // Isolate errors from callbacks
158
+ }
159
+ }
160
+
161
+ return true;
162
+ }
163
+
164
+ /**
165
+ * Removes all entries from the store.
166
+ * Note: This does not prevent garbage collection of the keys.
167
+ */
168
+ clear(): void {
169
+ this.#checkDisposed();
170
+
171
+ // We cannot iterate over WeakMap to emit events
172
+ // Just clear the map
173
+ this.#map = new WeakMap<object, V>();
174
+ this.#deleted++;
175
+ }
176
+
177
+ /**
178
+ * Returns the number of deleted entries.
179
+ * Note: Cannot return current size (WeakMap limitation).
180
+ */
181
+ get deleted(): number {
182
+ return this.#deleted;
183
+ }
184
+
185
+ /**
186
+ * Returns statistics about the store.
187
+ * Note: entries count is not available for WeakMap.
188
+ *
189
+ * @returns A snapshot of the current statistics.
190
+ */
191
+ stats(): {
192
+ deleted: number;
193
+ uptime: number;
194
+ } {
195
+ this.#checkDisposed();
196
+
197
+ return {
198
+ deleted: this.#deleted,
199
+ uptime: Date.now() - this.#createdAt,
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Disposes the store, releasing the WeakMap reference.
205
+ * After disposal, the store cannot be used.
206
+ */
207
+ dispose(): void {
208
+ if (this.#disposed) return;
209
+
210
+ this.#disposed = true;
211
+ this.#map = new WeakMap<object, V>();
212
+ }
213
+
214
+ /**
215
+ * Checks if the store has been disposed.
216
+ */
217
+ get disposed(): boolean {
218
+ return this.#disposed;
219
+ }
220
+
221
+ /**
222
+ * Checks if the store has been disposed and throws if so.
223
+ */
224
+ #checkDisposed(): void {
225
+ if (this.#disposed) {
226
+ throw new Error("WeakMemoryStore has been disposed");
227
+ }
228
+ }
229
+ }
package/src/index.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Safe Memory Layer - Secure, lightweight, dependency-free in-memory storage
3
+ *
4
+ * @module safe-memory-layer
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { MemoryStore, WeakMemoryStore } from "safe-memory-layer";
9
+ *
10
+ * // Basic usage
11
+ * const store = new MemoryStore<string, User>();
12
+ * store.set("user:123", user, { ttl: 60000 });
13
+ * const user = store.get("user:123");
14
+ * ```
15
+ */
16
+
17
+ // Main store implementation
18
+ export { MemoryStore } from "./MemoryStore.js";
19
+
20
+ // Weak reference store
21
+ export { WeakMemoryStore } from "./WeakMemoryStore.js";
22
+
23
+ // Supporting modules (for advanced usage)
24
+ export { Scheduler } from "./Scheduler.js";
25
+ export { Stats } from "./Stats.js";
26
+ export { EventEmitter } from "./Events.js";
27
+
28
+ // Utility functions
29
+ export {
30
+ createEntry,
31
+ isExpired,
32
+ touchEntry,
33
+ getRemainingTTL,
34
+ } from "./Entry.js";
35
+
36
+ export { createTimer, createInterval } from "./utils/timer.js";
37
+ export {
38
+ detectFeatures,
39
+ getFeatures,
40
+ createSafeFinalizationRegistry,
41
+ } from "./utils/featureDetection.js";
42
+
43
+ // Type exports
44
+ export type {
45
+ MaxEntriesStrategy,
46
+ SetOptions,
47
+ MemoryStoreOptions,
48
+ CleanupStats,
49
+ StoreStats,
50
+ InternalEntry,
51
+ InternalStoreOptions,
52
+ WeakMemoryStoreOptions,
53
+ FeatureSupport,
54
+ } from "./types.js";
55
+
56
+ export type { CleanupFn } from "./Scheduler.js";
57
+ export type { TimerHandle } from "./utils/timer.js";
package/src/types.ts ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Core type definitions for safe-memory-layer.
3
+ *
4
+ * @module types
5
+ */
6
+
7
+ /** Strategy to use when the store reaches its maximum entry limit. */
8
+ export type MaxEntriesStrategy = "reject" | "FIFO" | "LRU";
9
+
10
+ /** Options for setting an individual entry. */
11
+ export interface SetOptions {
12
+ /** Time-to-live in milliseconds. After this duration, the entry expires. */
13
+ ttl?: number;
14
+ }
15
+
16
+ /** Configuration options for creating a MemoryStore. */
17
+ export interface MemoryStoreOptions<K, V> {
18
+ /** Default TTL in milliseconds for all entries (default: no expiration). */
19
+ defaultTTL?: number;
20
+ /** Interval in milliseconds between automatic cleanup runs (default: 10000). */
21
+ cleanupInterval?: number;
22
+ /** Whether to automatically run cleanup on an interval (default: true). */
23
+ autoCleanup?: boolean;
24
+ /** Whether to automatically dispose the store after being empty for a configurable time (default: false). */
25
+ autoDispose?: boolean;
26
+ /** Time in milliseconds to wait after empty before auto-disposing (default: 60000). */
27
+ autoDisposeDelay?: number;
28
+ /** Maximum number of entries allowed in the store (default: no limit). */
29
+ maxEntries?: number;
30
+ /** Strategy when max entries is reached (default: "reject"). */
31
+ maxEntriesStrategy?: MaxEntriesStrategy;
32
+ /** Callback invoked when an entry expires. */
33
+ onExpire?: (key: K, value: V) => void;
34
+ /** Callback invoked when an entry is deleted. */
35
+ onDelete?: (key: K, value: V) => void;
36
+ /** Callback invoked after a cleanup cycle completes. */
37
+ onCleanup?: (stats: CleanupStats) => void;
38
+ }
39
+
40
+ /** Statistics about a cleanup cycle. */
41
+ export interface CleanupStats {
42
+ /** Number of entries removed during cleanup. */
43
+ removed: number;
44
+ /** Total number of entries before cleanup. */
45
+ totalBefore: number;
46
+ /** Total number of entries after cleanup. */
47
+ totalAfter: number;
48
+ /** Duration of the cleanup cycle in milliseconds. */
49
+ duration: number;
50
+ }
51
+
52
+ /** Statistics about the store. */
53
+ export interface StoreStats {
54
+ /** Current number of entries in the store. */
55
+ entries: number;
56
+ /** Total number of entries that have expired. */
57
+ expired: number;
58
+ /** Total number of entries that have been deleted. */
59
+ deleted: number;
60
+ /** Total number of cleanup cycles completed. */
61
+ cleaned: number;
62
+ /** Uptime of the store in milliseconds. */
63
+ uptime: number;
64
+ /** Estimated memory usage in bytes (approximate). */
65
+ memoryEstimate: number;
66
+ }
67
+
68
+ /** Internal entry stored in the map. */
69
+ export interface InternalEntry<K, V> {
70
+ /** The stored value. */
71
+ value: V;
72
+ /** The key (stored for cleanup callbacks). */
73
+ key: K;
74
+ /** Expiration timestamp (ms since epoch), or undefined if no expiration. */
75
+ expiresAt: number | undefined;
76
+ /** Timestamp of last access (for LRU). */
77
+ lastAccessed: number;
78
+ /** Timestamp of creation. */
79
+ createdAt: number;
80
+ }
81
+
82
+ /** Internal options for the MemoryStore constructor. */
83
+ export interface InternalStoreOptions<K, V> {
84
+ defaultTTL: number | undefined;
85
+ cleanupInterval: number;
86
+ autoCleanup: boolean;
87
+ autoDispose: boolean;
88
+ autoDisposeDelay: number;
89
+ maxEntries: number | undefined;
90
+ maxEntriesStrategy: MaxEntriesStrategy;
91
+ onExpire: ((key: K, value: V) => void) | undefined;
92
+ onDelete: ((key: K, value: V) => void) | undefined;
93
+ onCleanup: ((stats: CleanupStats) => void) | undefined;
94
+ }
95
+
96
+ /** Options for WeakMemoryStore. */
97
+ export interface WeakMemoryStoreOptions<V> {
98
+ /** Callback invoked when an entry is deleted. */
99
+ onDelete?: (value: V) => void;
100
+ }
101
+
102
+ /** Feature detection result for FinalizationRegistry. */
103
+ export interface FeatureSupport {
104
+ /** Whether FinalizationRegistry is available. */
105
+ finalizationRegistry: boolean;
106
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Feature detection utilities for safe-memory-layer.
3
+ *
4
+ * Detects available platform features and provides graceful fallbacks.
5
+ *
6
+ * @module utils/featureDetection
7
+ */
8
+
9
+ import type { FeatureSupport } from "../types.js";
10
+
11
+ /**
12
+ * Detects which platform features are available.
13
+ * This is called once at module load time and cached.
14
+ */
15
+ export function detectFeatures(): FeatureSupport {
16
+ return {
17
+ finalizationRegistry:
18
+ typeof globalThis.FinalizationRegistry !== "undefined",
19
+ };
20
+ }
21
+
22
+ /** Cached feature support flags. */
23
+ let cachedFeatures: FeatureSupport | undefined;
24
+
25
+ /**
26
+ * Returns the cached feature support flags, detecting them on first call.
27
+ */
28
+ export function getFeatures(): FeatureSupport {
29
+ if (cachedFeatures === undefined) {
30
+ cachedFeatures = detectFeatures();
31
+ }
32
+ return cachedFeatures;
33
+ }
34
+
35
+ /**
36
+ * Creates a FinalizationRegistry if available, otherwise returns null.
37
+ * Never throws if FinalizationRegistry is unavailable.
38
+ */
39
+ export function createSafeFinalizationRegistry<T>(
40
+ cleanup: (heldValue: T) => void,
41
+ ): FinalizationRegistry<T> | null {
42
+ const features = getFeatures();
43
+ if (features.finalizationRegistry) {
44
+ try {
45
+ return new FinalizationRegistry(cleanup);
46
+ } catch {
47
+ // Graceful fallback if construction fails
48
+ return null;
49
+ }
50
+ }
51
+ return null;
52
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Safe timer utilities for safe-memory-layer.
3
+ *
4
+ * Provides platform-agnostic timer creation and cleanup.
5
+ * Uses only Web-compatible APIs (setTimeout/clearTimeout).
6
+ *
7
+ * @module utils/timer
8
+ */
9
+
10
+ /** Represents a timer handle that can be cancelled. */
11
+ export interface TimerHandle {
12
+ /** Cancels the timer. Safe to call multiple times. */
13
+ cancel: () => void;
14
+ /** Whether the timer has been cancelled. */
15
+ readonly cancelled: boolean;
16
+ }
17
+
18
+ /**
19
+ * Creates a safe timer that can be cancelled.
20
+ * Uses setTimeout internally for maximum compatibility.
21
+ *
22
+ * @param fn - The function to execute after the delay.
23
+ * @param delay - The delay in milliseconds.
24
+ * @returns A TimerHandle that can be used to cancel the timer.
25
+ */
26
+ export function createTimer(fn: () => void, delay: number): TimerHandle {
27
+ let cancelled = false;
28
+ let timerId: ReturnType<typeof setTimeout> | undefined;
29
+
30
+ const wrappedFn = (): void => {
31
+ if (cancelled) return;
32
+ timerId = undefined;
33
+ try {
34
+ fn();
35
+ } catch {
36
+ // Isolate errors from timer callbacks to prevent crashes
37
+ }
38
+ };
39
+
40
+ timerId = setTimeout(wrappedFn, delay);
41
+
42
+ return {
43
+ cancel: (): void => {
44
+ cancelled = true;
45
+ if (timerId !== undefined) {
46
+ clearTimeout(timerId);
47
+ timerId = undefined;
48
+ }
49
+ },
50
+ get cancelled(): boolean {
51
+ return cancelled;
52
+ },
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Creates an interval timer that runs a function repeatedly.
58
+ *
59
+ * @param fn - The function to execute on each interval.
60
+ * @param interval - The interval in milliseconds.
61
+ * @returns A TimerHandle that can be used to cancel the interval.
62
+ */
63
+ export function createInterval(
64
+ fn: () => void,
65
+ interval: number,
66
+ ): TimerHandle {
67
+ let cancelled = false;
68
+ let timerId: ReturnType<typeof setInterval> | undefined;
69
+
70
+ const wrappedFn = (): void => {
71
+ if (cancelled) return;
72
+ try {
73
+ fn();
74
+ } catch {
75
+ // Isolate errors from interval callbacks
76
+ }
77
+ };
78
+
79
+ timerId = setInterval(wrappedFn, interval);
80
+
81
+ return {
82
+ cancel: (): void => {
83
+ cancelled = true;
84
+ if (timerId !== undefined) {
85
+ clearInterval(timerId);
86
+ timerId = undefined;
87
+ }
88
+ },
89
+ get cancelled(): boolean {
90
+ return cancelled;
91
+ },
92
+ };
93
+ }