tiny-server-state 0.1.2

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/README.md ADDED
@@ -0,0 +1,269 @@
1
+ # 🧠 tiny-server-state
2
+
3
+ A **lightweight, reactive in-memory state engine** for Node.js servers. It offers simple key-value storage with optional TTL (Time-To-Live), real-time subscriptions, and a React-like API for easier state management.
4
+
5
+ > Ideal for handling temporary, shared, or cacheable state without a database.
6
+
7
+ ---
8
+
9
+ ## πŸš€ Features
10
+
11
+ - βœ… **Simple and minimal** state engine
12
+ - πŸ•“ **TTL-based expiration** (auto-deletes expired keys)
13
+ - 🧠 **React-style `useState`** for intuitive state access
14
+ - πŸ“‘ **Subscriptions** for real-time updates
15
+ - πŸ” **Automatic Garbage Collection (GC)**
16
+ - πŸ› οΈ **No external dependencies**
17
+ - πŸ”’ **TypeScript support** out of the box
18
+
19
+ ---
20
+
21
+ ## πŸ“¦ Installation
22
+
23
+ ```bash
24
+ npm install tiny-server-state
25
+ ```
26
+
27
+ ## πŸš€ Quick Start
28
+
29
+ ```typescript
30
+ import { createServerState } from "tiny-server-state";
31
+
32
+ // Create a new state store instance
33
+ const store = createServerState();
34
+
35
+ // Use React-like state management
36
+ const [count, setCount] = store.useState("count", 0);
37
+ console.log(count); // 0
38
+
39
+ // Update state with a new value
40
+ setCount(5);
41
+
42
+ // Or use a function to update based on previous value
43
+ setCount(prev => prev + 1);
44
+ ```
45
+
46
+ ## πŸ“š API Reference
47
+
48
+ ### Creating a Store
49
+
50
+ ```typescript
51
+ const store = createServerState(sweepInterval?: number);
52
+ ```
53
+
54
+ - `sweepInterval`: Optional interval (in ms) for garbage collection (default: 60000)
55
+
56
+ ### Core Methods
57
+
58
+ #### `get<T>(key: string): T | undefined`
59
+ Retrieve a value from the store.
60
+
61
+ ```typescript
62
+ const value = store.get("myKey");
63
+ ```
64
+
65
+ #### `set<T>(key: string, value: T, options?: StateOptions): void`
66
+ Set a value in the store.
67
+
68
+ ```typescript
69
+ // Simple set
70
+ store.set("myKey", "myValue");
71
+
72
+ // Set with TTL (expires after 5 seconds)
73
+ store.set("temporaryKey", "value", { ttl: 5000 });
74
+ ```
75
+
76
+ #### `useState<T>(key: string, initial: T | (() => T), options?: StateOptions)`
77
+ React-style state management.
78
+
79
+ ```typescript
80
+ // Basic usage
81
+ const [value, setValue] = store.useState("key", "initial");
82
+
83
+ // With initializer function
84
+ const [user, setUser] = store.useState("user", () => ({ name: "John" }));
85
+
86
+ // With TTL
87
+ const [token, setToken] = store.useState("token", "abc123", { ttl: 3600000 }); // 1 hour
88
+
89
+ // Update examples
90
+ setValue("new value"); // Direct update
91
+ setValue(prev => prev + " updated"); // Function update
92
+ ```
93
+
94
+ #### `subscribe<T>(key: string, callback: (value: T) => void)`
95
+ Subscribe to state changes.
96
+
97
+ ```typescript
98
+ // Subscribe to changes
99
+ const unsubscribe = store.subscribe("user", (newValue) => {
100
+ console.log("User updated:", newValue);
101
+ });
102
+
103
+ // Later: cleanup subscription
104
+ unsubscribe();
105
+ ```
106
+
107
+ #### `useEffect(effect: () => void | (() => void), deps?: string[], effectId?: string)`
108
+ React-style effect management for side effects when state changes.
109
+
110
+ ```typescript
111
+ // Run effect when dependencies change
112
+ const cleanup = store.useEffect(
113
+ () => {
114
+ console.log("User or settings changed");
115
+
116
+ // Optional cleanup function
117
+ return () => {
118
+ console.log("Effect cleanup");
119
+ };
120
+ },
121
+ ["user", "settings"] // Dependencies
122
+ );
123
+
124
+ // Run effect only once (no dependencies)
125
+ const onceCleanup = store.useEffect(() => {
126
+ console.log("This runs once");
127
+ });
128
+
129
+ // Later: cleanup effect
130
+ cleanup();
131
+ ```
132
+
133
+ #### `cleanupAllEffects(): void`
134
+ Cleanup all active effects (useful for testing or shutdown).
135
+
136
+ ```typescript
137
+ // Cleanup all effects at once
138
+ store.cleanupAllEffects();
139
+ ```
140
+
141
+ ## 🎯 Use Cases
142
+
143
+ ### 1. Session Management
144
+ ```typescript
145
+ // Store session data with automatic expiration
146
+ const [session, setSession] = store.useState("session", null, {
147
+ ttl: 30 * 60 * 1000 // 30 minutes
148
+ });
149
+
150
+ // Update session
151
+ setSession({ userId: "123", lastActive: Date.now() });
152
+ ```
153
+
154
+ ### 2. Caching
155
+ ```typescript
156
+ // Cache expensive operation results
157
+ async function getCachedData(id: string) {
158
+ const [data, setData] = store.useState(`cache:${id}`, null, {
159
+ ttl: 5 * 60 * 1000 // 5 minutes
160
+ });
161
+
162
+ if (!data) {
163
+ const newData = await fetchExpensiveData(id);
164
+ setData(newData);
165
+ return newData;
166
+ }
167
+
168
+ return data;
169
+ }
170
+ ```
171
+
172
+ ### 3. Real-time Updates and Side Effects
173
+ ```typescript
174
+ // Broadcast changes to all subscribers
175
+ const [status, setStatus] = store.useState("systemStatus", "operational");
176
+ const [metrics, setMetrics] = store.useState("metrics", { cpu: 0, memory: 0 });
177
+
178
+ // React to status and metrics changes
179
+ store.useEffect(() => {
180
+ const currentMetrics = store.get("metrics");
181
+ const currentStatus = store.get("systemStatus");
182
+
183
+ // Notify admins when system is stressed
184
+ if (currentMetrics.cpu > 90 || currentMetrics.memory > 90) {
185
+ notifyAdmins(`High resource usage detected! CPU: ${currentMetrics.cpu}%, Memory: ${currentMetrics.memory}%`);
186
+ setStatus("stressed");
187
+ }
188
+
189
+ // Log all status changes
190
+ console.log(`System status: ${currentStatus}`);
191
+
192
+ // Cleanup function (runs before next effect or on cleanup)
193
+ return () => {
194
+ console.log(`Previous status: ${currentStatus}`);
195
+ };
196
+ }, ["systemStatus", "metrics"]);
197
+
198
+ // Update status
199
+ setStatus("maintenance");
200
+
201
+ // Update metrics
202
+ setMetrics({ cpu: 95, memory: 85 }); // This will trigger the effect and change status to "stressed"
203
+ ```
204
+
205
+ ## πŸ”§ Configuration
206
+
207
+ ### StateOptions Interface
208
+
209
+ ```typescript
210
+ interface StateOptions {
211
+ ttl?: number; // Time-to-live in milliseconds
212
+ }
213
+ ```
214
+
215
+ ### Garbage Collection
216
+
217
+ The store automatically removes expired entries at regular intervals. You can configure the sweep interval when creating the store:
218
+
219
+ ```typescript
220
+ // Custom sweep interval (e.g., every 5 minutes)
221
+ const store = createServerState(5 * 60 * 1000);
222
+ ```
223
+
224
+ ## 🀝 Contributing
225
+
226
+ Contributions are welcome! Please feel free to submit a Pull Request.
227
+
228
+ ## πŸ“„ License
229
+
230
+ MIT License - feel free to use this in your own projects.
231
+
232
+ ---
233
+
234
+ ## πŸ” TypeScript Support
235
+
236
+ This package includes TypeScript definitions out of the box. You'll get full type safety and autocompletion in your TypeScript projects.
237
+
238
+ ```typescript
239
+ // Type-safe state management
240
+ const [user, setUser] = store.useState<{
241
+ id: string;
242
+ name: string;
243
+ age: number;
244
+ }>("user", {
245
+ id: "1",
246
+ name: "John",
247
+ age: 30
248
+ });
249
+
250
+ // TypeScript will ensure type safety
251
+ setUser(prev => ({
252
+ ...prev,
253
+ age: prev.age + 1
254
+ }));
255
+ ```
256
+
257
+ ## ⚑ Performance Considerations
258
+
259
+ - The store uses an in-memory Map, so be mindful of memory usage when storing large objects
260
+ - TTL and garbage collection help manage memory automatically
261
+ - Consider using shorter TTLs for frequently changing data
262
+ - For large-scale applications, consider using a proper database instead
263
+
264
+ ## πŸ” Security Notes
265
+
266
+ - Data is stored in memory and will be lost when the server restarts
267
+ - Don't store sensitive information without proper encryption
268
+ - Be cautious with TTL values to prevent memory leaks
269
+ - Consider implementing rate limiting for state updates if exposed to client requests
@@ -0,0 +1,46 @@
1
+ import { EventEmitter } from "node:events";
2
+ export interface StateOptions {
3
+ ttl?: number;
4
+ }
5
+ export declare class ServerState {
6
+ private sweepEvery;
7
+ private cache;
8
+ private events;
9
+ private effects;
10
+ constructor(sweepEvery?: number);
11
+ /** Internal GC */
12
+ private sweep;
13
+ /** Get value (or undefined if missing / expired) */
14
+ get<T>(key: string): T | undefined;
15
+ /** Set value + optional TTL */
16
+ set<T>(key: string, value: T, opts?: StateOptions): void;
17
+ /**
18
+ * React-like helper.
19
+ * ```ts
20
+ * const [count, setCount] = store.useState("count", 0, { ttl: 30_000 });
21
+ * ```
22
+ */
23
+ useState<T>(key: string, initial: T | (() => T), opts?: StateOptions): [T, (v: T | ((prev: T) => T), o?: StateOptions) => void];
24
+ /** Subscribe; returns an unsubscribe fn */
25
+ subscribe<T>(key: string, cb: (v: T) => void): () => EventEmitter<[never]>;
26
+ /**
27
+ * React-like useEffect helper.
28
+ * Runs effect when dependencies change, supports cleanup.
29
+ * ```ts
30
+ * // Run effect when 'user' or 'settings' change
31
+ * const cleanup = store.useEffect(
32
+ * () => {
33
+ * console.log('User or settings changed');
34
+ * return () => console.log('Cleanup');
35
+ * },
36
+ * ['user', 'settings']
37
+ * );
38
+ * ```
39
+ */
40
+ useEffect(effect: () => void | (() => void), deps?: string[], effectId?: string): () => void;
41
+ /**
42
+ * Cleanup all effects (useful for testing or shutdown)
43
+ */
44
+ cleanupAllEffects(): void;
45
+ }
46
+ export declare const createServerState: (sweep?: number) => ServerState;
package/dist/index.cjs ADDED
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ var node_events = require('node:events');
4
+
5
+ // [tiny-server-state] in-memory state engine
6
+ class ServerState {
7
+ constructor(sweepEvery = 60000 /* 1 min */) {
8
+ this.sweepEvery = sweepEvery;
9
+ this.cache = new Map();
10
+ this.events = new node_events.EventEmitter();
11
+ this.effects = new Map(); // Track effect cleanup functions
12
+ setInterval(() => this.sweep(), sweepEvery).unref();
13
+ }
14
+ // ───────────────────────── core helpers ─────────────────────────
15
+ /** Internal GC */
16
+ sweep() {
17
+ const now = Date.now();
18
+ for (const [k, v] of this.cache)
19
+ if (v.expiresAt && v.expiresAt <= now)
20
+ this.cache.delete(k);
21
+ }
22
+ /** Get value (or undefined if missing / expired) */
23
+ get(key) {
24
+ const hit = this.cache.get(key);
25
+ if (!hit)
26
+ return undefined;
27
+ if (hit.expiresAt && hit.expiresAt <= Date.now()) {
28
+ this.cache.delete(key);
29
+ return undefined;
30
+ }
31
+ return hit.value;
32
+ }
33
+ /** Set value + optional TTL */
34
+ set(key, value, opts = {}) {
35
+ const expiresAt = opts.ttl ? Date.now() + opts.ttl : undefined;
36
+ this.cache.set(key, { value, expiresAt });
37
+ this.events.emit(key, value); // notify subscribers
38
+ }
39
+ // ───────────────────── React-flavoured sugar ─────────────────────
40
+ /**
41
+ * React-like helper.
42
+ * ```ts
43
+ * const [count, setCount] = store.useState("count", 0, { ttl: 30_000 });
44
+ * ```
45
+ */
46
+ useState(key, initial, opts = {}) {
47
+ let current = this.get(key);
48
+ if (current === undefined) {
49
+ current = typeof initial === "function" ? initial() : initial;
50
+ this.set(key, current, opts);
51
+ }
52
+ const setter = (v, override = {}) => {
53
+ const prev = this.get(key);
54
+ const next = typeof v === "function" ? v(prev) : v;
55
+ this.set(key, next, { ...opts, ...override });
56
+ };
57
+ return [current, setter];
58
+ }
59
+ /** Subscribe; returns an unsubscribe fn */
60
+ subscribe(key, cb) {
61
+ this.events.on(key, cb);
62
+ return () => this.events.off(key, cb);
63
+ }
64
+ /**
65
+ * React-like useEffect helper.
66
+ * Runs effect when dependencies change, supports cleanup.
67
+ * ```ts
68
+ * // Run effect when 'user' or 'settings' change
69
+ * const cleanup = store.useEffect(
70
+ * () => {
71
+ * console.log('User or settings changed');
72
+ * return () => console.log('Cleanup');
73
+ * },
74
+ * ['user', 'settings']
75
+ * );
76
+ * ```
77
+ */
78
+ useEffect(effect, deps = [], effectId) {
79
+ const id = effectId || `effect_${Date.now()}_${Math.random()}`;
80
+ // Cleanup previous effect if it exists
81
+ const existingCleanup = this.effects.get(id);
82
+ if (existingCleanup) {
83
+ existingCleanup();
84
+ this.effects.delete(id);
85
+ }
86
+ let cleanup;
87
+ const unsubscribers = [];
88
+ // Run effect initially
89
+ const runEffect = () => {
90
+ // Cleanup previous run
91
+ if (cleanup)
92
+ cleanup();
93
+ // Run the effect
94
+ const result = effect();
95
+ cleanup = typeof result === 'function' ? result : undefined;
96
+ };
97
+ // If no dependencies, run once and return cleanup
98
+ if (deps.length === 0) {
99
+ runEffect();
100
+ const effectCleanup = () => {
101
+ if (cleanup)
102
+ cleanup();
103
+ this.effects.delete(id);
104
+ };
105
+ this.effects.set(id, effectCleanup);
106
+ return effectCleanup;
107
+ }
108
+ // Subscribe to all dependencies
109
+ deps.forEach(dep => {
110
+ const unsub = this.subscribe(dep, () => {
111
+ runEffect();
112
+ });
113
+ unsubscribers.push(unsub);
114
+ });
115
+ // Run effect initially
116
+ runEffect();
117
+ // Return cleanup function
118
+ const effectCleanup = () => {
119
+ // Cleanup effect
120
+ if (cleanup)
121
+ cleanup();
122
+ // Unsubscribe from all dependencies
123
+ unsubscribers.forEach(unsub => unsub());
124
+ // Remove from effects map
125
+ this.effects.delete(id);
126
+ };
127
+ this.effects.set(id, effectCleanup);
128
+ return effectCleanup;
129
+ }
130
+ /**
131
+ * Cleanup all effects (useful for testing or shutdown)
132
+ */
133
+ cleanupAllEffects() {
134
+ for (const cleanup of this.effects.values()) {
135
+ cleanup();
136
+ }
137
+ this.effects.clear();
138
+ }
139
+ }
140
+ // factory (so callers don’t import the class directly)
141
+ const createServerState = (sweep) => new ServerState(sweep);
142
+
143
+ exports.ServerState = ServerState;
144
+ exports.createServerState = createServerState;
145
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/Store.ts"],"sourcesContent":["// [tiny-server-state] in-memory state engine\nimport { EventEmitter } from \"node:events\";\n\nexport interface StateOptions { ttl?: number /* ms */; }\n\ninterface Entry<T = unknown> {\n value: T;\n /** epoch in ms when the value dies */\n expiresAt?: number;\n}\n\nexport class ServerState {\n private cache = new Map<string, Entry>();\n private events = new EventEmitter();\n private effects = new Map<string, () => void>(); // Track effect cleanup functions\n\n constructor(private sweepEvery = 60_000 /* 1 min */) {\n setInterval(() => this.sweep(), sweepEvery).unref();\n }\n\n // ───────────────────────── core helpers ─────────────────────────\n\n /** Internal GC */\n private sweep() {\n const now = Date.now();\n for (const [k, v] of this.cache) if (v.expiresAt && v.expiresAt <= now) this.cache.delete(k);\n }\n\n /** Get value (or undefined if missing / expired) */\n get<T>(key: string): T | undefined {\n const hit = this.cache.get(key);\n if (!hit) return undefined;\n if (hit.expiresAt && hit.expiresAt <= Date.now()) {\n this.cache.delete(key);\n return undefined;\n }\n return hit.value as T;\n }\n\n /** Set value + optional TTL */\n set<T>(key: string, value: T, opts: StateOptions = {}): void {\n const expiresAt = opts.ttl ? Date.now() + opts.ttl : undefined;\n this.cache.set(key, { value, expiresAt });\n this.events.emit(key, value); // notify subscribers\n }\n\n // ───────────────────── React-flavoured sugar ─────────────────────\n\n /**\n * React-like helper.\n * ```ts\n * const [count, setCount] = store.useState(\"count\", 0, { ttl: 30_000 });\n * ```\n */\n useState<T>(\n key: string,\n initial: T | (() => T),\n opts: StateOptions = {}\n ): [T, (v: T | ((prev: T) => T), o?: StateOptions) => void] {\n let current = this.get<T>(key);\n if (current === undefined) {\n current = typeof initial === \"function\" ? (initial as () => T)() : initial;\n this.set(key, current, opts);\n }\n const setter = (v: T | ((prev: T) => T), override: StateOptions = {}) => {\n const prev = this.get<T>(key) as T;\n const next = typeof v === \"function\" ? (v as (p: T) => T)(prev) : v;\n this.set(key, next, { ...opts, ...override });\n };\n return [current, setter];\n }\n\n /** Subscribe; returns an unsubscribe fn */\n subscribe<T>(key: string, cb: (v: T) => void) {\n this.events.on(key, cb);\n return () => this.events.off(key, cb);\n }\n\n /**\n * React-like useEffect helper.\n * Runs effect when dependencies change, supports cleanup.\n * ```ts\n * // Run effect when 'user' or 'settings' change\n * const cleanup = store.useEffect(\n * () => {\n * console.log('User or settings changed');\n * return () => console.log('Cleanup');\n * },\n * ['user', 'settings']\n * );\n * ```\n */\n useEffect(\n effect: () => void | (() => void),\n deps: string[] = [],\n effectId?: string\n ): () => void {\n const id = effectId || `effect_${Date.now()}_${Math.random()}`;\n \n // Cleanup previous effect if it exists\n const existingCleanup = this.effects.get(id);\n if (existingCleanup) {\n existingCleanup();\n this.effects.delete(id);\n }\n\n let cleanup: (() => void) | undefined;\n const unsubscribers: (() => void)[] = [];\n\n // Run effect initially\n const runEffect = () => {\n // Cleanup previous run\n if (cleanup) cleanup();\n \n // Run the effect\n const result = effect();\n cleanup = typeof result === 'function' ? result : undefined;\n };\n\n // If no dependencies, run once and return cleanup\n if (deps.length === 0) {\n runEffect();\n const effectCleanup = () => {\n if (cleanup) cleanup();\n this.effects.delete(id);\n };\n this.effects.set(id, effectCleanup);\n return effectCleanup;\n }\n\n // Subscribe to all dependencies\n deps.forEach(dep => {\n const unsub = this.subscribe(dep, () => {\n runEffect();\n });\n unsubscribers.push(unsub);\n });\n\n // Run effect initially\n runEffect();\n\n // Return cleanup function\n const effectCleanup = () => {\n // Cleanup effect\n if (cleanup) cleanup();\n \n // Unsubscribe from all dependencies\n unsubscribers.forEach(unsub => unsub());\n \n // Remove from effects map\n this.effects.delete(id);\n };\n\n this.effects.set(id, effectCleanup);\n return effectCleanup;\n }\n\n /**\n * Cleanup all effects (useful for testing or shutdown)\n */\n cleanupAllEffects(): void {\n for (const cleanup of this.effects.values()) {\n cleanup();\n }\n this.effects.clear();\n }\n}\n\n// factory (so callers don’t import the class directly)\nexport const createServerState = (sweep?: number) => new ServerState(sweep);\n"],"names":["EventEmitter"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export * from "./Store";
package/dist/index.js ADDED
@@ -0,0 +1,142 @@
1
+ import { EventEmitter } from 'node:events';
2
+
3
+ // [tiny-server-state] in-memory state engine
4
+ class ServerState {
5
+ constructor(sweepEvery = 60000 /* 1 min */) {
6
+ this.sweepEvery = sweepEvery;
7
+ this.cache = new Map();
8
+ this.events = new EventEmitter();
9
+ this.effects = new Map(); // Track effect cleanup functions
10
+ setInterval(() => this.sweep(), sweepEvery).unref();
11
+ }
12
+ // ───────────────────────── core helpers ─────────────────────────
13
+ /** Internal GC */
14
+ sweep() {
15
+ const now = Date.now();
16
+ for (const [k, v] of this.cache)
17
+ if (v.expiresAt && v.expiresAt <= now)
18
+ this.cache.delete(k);
19
+ }
20
+ /** Get value (or undefined if missing / expired) */
21
+ get(key) {
22
+ const hit = this.cache.get(key);
23
+ if (!hit)
24
+ return undefined;
25
+ if (hit.expiresAt && hit.expiresAt <= Date.now()) {
26
+ this.cache.delete(key);
27
+ return undefined;
28
+ }
29
+ return hit.value;
30
+ }
31
+ /** Set value + optional TTL */
32
+ set(key, value, opts = {}) {
33
+ const expiresAt = opts.ttl ? Date.now() + opts.ttl : undefined;
34
+ this.cache.set(key, { value, expiresAt });
35
+ this.events.emit(key, value); // notify subscribers
36
+ }
37
+ // ───────────────────── React-flavoured sugar ─────────────────────
38
+ /**
39
+ * React-like helper.
40
+ * ```ts
41
+ * const [count, setCount] = store.useState("count", 0, { ttl: 30_000 });
42
+ * ```
43
+ */
44
+ useState(key, initial, opts = {}) {
45
+ let current = this.get(key);
46
+ if (current === undefined) {
47
+ current = typeof initial === "function" ? initial() : initial;
48
+ this.set(key, current, opts);
49
+ }
50
+ const setter = (v, override = {}) => {
51
+ const prev = this.get(key);
52
+ const next = typeof v === "function" ? v(prev) : v;
53
+ this.set(key, next, { ...opts, ...override });
54
+ };
55
+ return [current, setter];
56
+ }
57
+ /** Subscribe; returns an unsubscribe fn */
58
+ subscribe(key, cb) {
59
+ this.events.on(key, cb);
60
+ return () => this.events.off(key, cb);
61
+ }
62
+ /**
63
+ * React-like useEffect helper.
64
+ * Runs effect when dependencies change, supports cleanup.
65
+ * ```ts
66
+ * // Run effect when 'user' or 'settings' change
67
+ * const cleanup = store.useEffect(
68
+ * () => {
69
+ * console.log('User or settings changed');
70
+ * return () => console.log('Cleanup');
71
+ * },
72
+ * ['user', 'settings']
73
+ * );
74
+ * ```
75
+ */
76
+ useEffect(effect, deps = [], effectId) {
77
+ const id = effectId || `effect_${Date.now()}_${Math.random()}`;
78
+ // Cleanup previous effect if it exists
79
+ const existingCleanup = this.effects.get(id);
80
+ if (existingCleanup) {
81
+ existingCleanup();
82
+ this.effects.delete(id);
83
+ }
84
+ let cleanup;
85
+ const unsubscribers = [];
86
+ // Run effect initially
87
+ const runEffect = () => {
88
+ // Cleanup previous run
89
+ if (cleanup)
90
+ cleanup();
91
+ // Run the effect
92
+ const result = effect();
93
+ cleanup = typeof result === 'function' ? result : undefined;
94
+ };
95
+ // If no dependencies, run once and return cleanup
96
+ if (deps.length === 0) {
97
+ runEffect();
98
+ const effectCleanup = () => {
99
+ if (cleanup)
100
+ cleanup();
101
+ this.effects.delete(id);
102
+ };
103
+ this.effects.set(id, effectCleanup);
104
+ return effectCleanup;
105
+ }
106
+ // Subscribe to all dependencies
107
+ deps.forEach(dep => {
108
+ const unsub = this.subscribe(dep, () => {
109
+ runEffect();
110
+ });
111
+ unsubscribers.push(unsub);
112
+ });
113
+ // Run effect initially
114
+ runEffect();
115
+ // Return cleanup function
116
+ const effectCleanup = () => {
117
+ // Cleanup effect
118
+ if (cleanup)
119
+ cleanup();
120
+ // Unsubscribe from all dependencies
121
+ unsubscribers.forEach(unsub => unsub());
122
+ // Remove from effects map
123
+ this.effects.delete(id);
124
+ };
125
+ this.effects.set(id, effectCleanup);
126
+ return effectCleanup;
127
+ }
128
+ /**
129
+ * Cleanup all effects (useful for testing or shutdown)
130
+ */
131
+ cleanupAllEffects() {
132
+ for (const cleanup of this.effects.values()) {
133
+ cleanup();
134
+ }
135
+ this.effects.clear();
136
+ }
137
+ }
138
+ // factory (so callers don’t import the class directly)
139
+ const createServerState = (sweep) => new ServerState(sweep);
140
+
141
+ export { ServerState, createServerState };
142
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/Store.ts"],"sourcesContent":["// [tiny-server-state] in-memory state engine\nimport { EventEmitter } from \"node:events\";\n\nexport interface StateOptions { ttl?: number /* ms */; }\n\ninterface Entry<T = unknown> {\n value: T;\n /** epoch in ms when the value dies */\n expiresAt?: number;\n}\n\nexport class ServerState {\n private cache = new Map<string, Entry>();\n private events = new EventEmitter();\n private effects = new Map<string, () => void>(); // Track effect cleanup functions\n\n constructor(private sweepEvery = 60_000 /* 1 min */) {\n setInterval(() => this.sweep(), sweepEvery).unref();\n }\n\n // ───────────────────────── core helpers ─────────────────────────\n\n /** Internal GC */\n private sweep() {\n const now = Date.now();\n for (const [k, v] of this.cache) if (v.expiresAt && v.expiresAt <= now) this.cache.delete(k);\n }\n\n /** Get value (or undefined if missing / expired) */\n get<T>(key: string): T | undefined {\n const hit = this.cache.get(key);\n if (!hit) return undefined;\n if (hit.expiresAt && hit.expiresAt <= Date.now()) {\n this.cache.delete(key);\n return undefined;\n }\n return hit.value as T;\n }\n\n /** Set value + optional TTL */\n set<T>(key: string, value: T, opts: StateOptions = {}): void {\n const expiresAt = opts.ttl ? Date.now() + opts.ttl : undefined;\n this.cache.set(key, { value, expiresAt });\n this.events.emit(key, value); // notify subscribers\n }\n\n // ───────────────────── React-flavoured sugar ─────────────────────\n\n /**\n * React-like helper.\n * ```ts\n * const [count, setCount] = store.useState(\"count\", 0, { ttl: 30_000 });\n * ```\n */\n useState<T>(\n key: string,\n initial: T | (() => T),\n opts: StateOptions = {}\n ): [T, (v: T | ((prev: T) => T), o?: StateOptions) => void] {\n let current = this.get<T>(key);\n if (current === undefined) {\n current = typeof initial === \"function\" ? (initial as () => T)() : initial;\n this.set(key, current, opts);\n }\n const setter = (v: T | ((prev: T) => T), override: StateOptions = {}) => {\n const prev = this.get<T>(key) as T;\n const next = typeof v === \"function\" ? (v as (p: T) => T)(prev) : v;\n this.set(key, next, { ...opts, ...override });\n };\n return [current, setter];\n }\n\n /** Subscribe; returns an unsubscribe fn */\n subscribe<T>(key: string, cb: (v: T) => void) {\n this.events.on(key, cb);\n return () => this.events.off(key, cb);\n }\n\n /**\n * React-like useEffect helper.\n * Runs effect when dependencies change, supports cleanup.\n * ```ts\n * // Run effect when 'user' or 'settings' change\n * const cleanup = store.useEffect(\n * () => {\n * console.log('User or settings changed');\n * return () => console.log('Cleanup');\n * },\n * ['user', 'settings']\n * );\n * ```\n */\n useEffect(\n effect: () => void | (() => void),\n deps: string[] = [],\n effectId?: string\n ): () => void {\n const id = effectId || `effect_${Date.now()}_${Math.random()}`;\n \n // Cleanup previous effect if it exists\n const existingCleanup = this.effects.get(id);\n if (existingCleanup) {\n existingCleanup();\n this.effects.delete(id);\n }\n\n let cleanup: (() => void) | undefined;\n const unsubscribers: (() => void)[] = [];\n\n // Run effect initially\n const runEffect = () => {\n // Cleanup previous run\n if (cleanup) cleanup();\n \n // Run the effect\n const result = effect();\n cleanup = typeof result === 'function' ? result : undefined;\n };\n\n // If no dependencies, run once and return cleanup\n if (deps.length === 0) {\n runEffect();\n const effectCleanup = () => {\n if (cleanup) cleanup();\n this.effects.delete(id);\n };\n this.effects.set(id, effectCleanup);\n return effectCleanup;\n }\n\n // Subscribe to all dependencies\n deps.forEach(dep => {\n const unsub = this.subscribe(dep, () => {\n runEffect();\n });\n unsubscribers.push(unsub);\n });\n\n // Run effect initially\n runEffect();\n\n // Return cleanup function\n const effectCleanup = () => {\n // Cleanup effect\n if (cleanup) cleanup();\n \n // Unsubscribe from all dependencies\n unsubscribers.forEach(unsub => unsub());\n \n // Remove from effects map\n this.effects.delete(id);\n };\n\n this.effects.set(id, effectCleanup);\n return effectCleanup;\n }\n\n /**\n * Cleanup all effects (useful for testing or shutdown)\n */\n cleanupAllEffects(): void {\n for (const cleanup of this.effects.values()) {\n cleanup();\n }\n this.effects.clear();\n }\n}\n\n// factory (so callers don’t import the class directly)\nexport const createServerState = (sweep?: number) => new ServerState(sweep);\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "tiny-server-state",
3
+ "version": "0.1.2",
4
+ "description": "Lightweight TTL-enabled in-memory state for Node servers",
5
+ "main": "dist/index.cjs",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ "import": "./dist/index.js",
10
+ "require": "./dist/index.cjs"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "keywords": [
16
+ "state",
17
+ "cache",
18
+ "ttl",
19
+ "node",
20
+ "server"
21
+ ],
22
+ "license": "MIT",
23
+ "scripts": {
24
+ "build": "rollup -c",
25
+ "clean": "rimraf dist",
26
+ "prepublishOnly": "npm run clean && npm run build",
27
+ "test": "node test/basic.test.js"
28
+ },
29
+ "devDependencies": {
30
+ "@rollup/plugin-typescript": "^11.1.3",
31
+ "@types/node": "^22.15.21",
32
+ "rimraf": "^5.0.5",
33
+ "rollup": "^4.41.0",
34
+ "typescript": "^5.4.5"
35
+ },
36
+ "dependencies": {
37
+ "tslib": "^2.8.1"
38
+ }
39
+ }