tiny-server-state 0.1.2 → 0.1.3

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 CHANGED
@@ -130,6 +130,22 @@ const onceCleanup = store.useEffect(() => {
130
130
  cleanup();
131
131
  ```
132
132
 
133
+ #### `cacheCompute<T>(factory: () => T, deps: string[], cacheId?: string): T`
134
+ Server-side alternative to React's `useMemo`. Memoizes a computed value based on dependencies. The value is recomputed only when dependencies change.
135
+
136
+ ```typescript
137
+ // Memoize an expensive computation based on dependencies
138
+ const result = store.cacheCompute(() => expensiveCalculation(a, b), ["a", "b"]);
139
+ ```
140
+
141
+ #### `cacheCallback<T extends (...args: any[]) => any>(fn: T, deps: string[], callbackId?: string): T`
142
+ Server-side alternative to React's `useCallback`. Memoizes a callback function based on dependencies. The function is recreated only when dependencies change.
143
+
144
+ ```typescript
145
+ // Memoize a callback function based on dependencies
146
+ const memoizedFn = store.cacheCallback(() => doSomething(a), ["a"]);
147
+ ```
148
+
133
149
  #### `cleanupAllEffects(): void`
134
150
  Cleanup all active effects (useful for testing or shutdown).
135
151
 
@@ -266,4 +282,4 @@ setUser(prev => ({
266
282
  - Data is stored in memory and will be lost when the server restarts
267
283
  - Don't store sensitive information without proper encryption
268
284
  - Be cautious with TTL values to prevent memory leaks
269
- - Consider implementing rate limiting for state updates if exposed to client requests
285
+ - Consider implementing rate limiting for state updates if exposed to client requests
package/dist/Store.d.ts CHANGED
@@ -42,5 +42,21 @@ export declare class ServerState {
42
42
  * Cleanup all effects (useful for testing or shutdown)
43
43
  */
44
44
  cleanupAllEffects(): void;
45
+ /**
46
+ * Memoize a value based on dependencies (server-side useMemo alternative).
47
+ * The cached value is recomputed only when dependencies change.
48
+ * @param factory Function to compute the value.
49
+ * @param deps Array of dependency keys.
50
+ * @param cacheId Optional unique cache identifier.
51
+ */
52
+ cacheCompute<T>(factory: () => T, deps: string[], cacheId?: string): T;
53
+ /**
54
+ * Memoize a callback function based on dependencies (server-side useCallback alternative).
55
+ * The cached function is recreated only when dependencies change.
56
+ * @param fn Function to memoize.
57
+ * @param deps Array of dependency keys.
58
+ * @param callbackId Optional unique cache identifier.
59
+ */
60
+ cacheCallback<T extends (...args: any[]) => any>(fn: T, deps: string[], callbackId?: string): T;
45
61
  }
46
62
  export declare const createServerState: (sweep?: number) => ServerState;
package/dist/index.cjs CHANGED
@@ -136,6 +136,38 @@ class ServerState {
136
136
  }
137
137
  this.effects.clear();
138
138
  }
139
+ /**
140
+ * Memoize a value based on dependencies (server-side useMemo alternative).
141
+ * The cached value is recomputed only when dependencies change.
142
+ * @param factory Function to compute the value.
143
+ * @param deps Array of dependency keys.
144
+ * @param cacheId Optional unique cache identifier.
145
+ */
146
+ cacheCompute(factory, deps, cacheId) {
147
+ const id = cacheId || `cacheCompute_${deps.join("_")}`;
148
+ let memoEntry = this.cache.get(id);
149
+ let prevDeps = this.cache.get(`${id}_deps`);
150
+ const depsChanged = !prevDeps ||
151
+ deps.length !== prevDeps.value.length ||
152
+ deps.some((d, i) => d !== prevDeps.value[i]);
153
+ if (!memoEntry || depsChanged) {
154
+ const value = factory();
155
+ memoEntry = { value };
156
+ this.cache.set(id, memoEntry);
157
+ this.cache.set(`${id}_deps`, { value: [...deps] });
158
+ }
159
+ return memoEntry.value;
160
+ }
161
+ /**
162
+ * Memoize a callback function based on dependencies (server-side useCallback alternative).
163
+ * The cached function is recreated only when dependencies change.
164
+ * @param fn Function to memoize.
165
+ * @param deps Array of dependency keys.
166
+ * @param callbackId Optional unique cache identifier.
167
+ */
168
+ cacheCallback(fn, deps, callbackId) {
169
+ return this.cacheCompute(() => fn, deps, callbackId);
170
+ }
139
171
  }
140
172
  // factory (so callers don’t import the class directly)
141
173
  const createServerState = (sweep) => new ServerState(sweep);
@@ -1 +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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"}
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 * Memoize a value based on dependencies (server-side useMemo alternative).\n * The cached value is recomputed only when dependencies change.\n * @param factory Function to compute the value.\n * @param deps Array of dependency keys.\n * @param cacheId Optional unique cache identifier.\n */\n cacheCompute<T>(factory: () => T, deps: string[], cacheId?: string): T {\n const id = cacheId || `cacheCompute_${deps.join(\"_\")}`;\n let memoEntry = this.cache.get(id) as Entry<T> | undefined;\n let prevDeps = this.cache.get(`${id}_deps`) as Entry<string[]> | undefined;\n\n const depsChanged =\n !prevDeps ||\n deps.length !== prevDeps.value.length ||\n deps.some((d, i) => d !== prevDeps!.value[i]);\n\n if (!memoEntry || depsChanged) {\n const value = factory();\n memoEntry = { value };\n this.cache.set(id, memoEntry);\n this.cache.set(`${id}_deps`, { value: [...deps] });\n }\n return memoEntry.value;\n }\n\n /**\n * Memoize a callback function based on dependencies (server-side useCallback alternative).\n * The cached function is recreated only when dependencies change.\n * @param fn Function to memoize.\n * @param deps Array of dependency keys.\n * @param callbackId Optional unique cache identifier.\n */\n cacheCallback<T extends (...args: any[]) => any>(\n fn: T,\n deps: string[],\n callbackId?: string\n ): T {\n return this.cacheCompute(() => fn, deps, callbackId) as T;\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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"}
package/dist/index.js CHANGED
@@ -134,6 +134,38 @@ class ServerState {
134
134
  }
135
135
  this.effects.clear();
136
136
  }
137
+ /**
138
+ * Memoize a value based on dependencies (server-side useMemo alternative).
139
+ * The cached value is recomputed only when dependencies change.
140
+ * @param factory Function to compute the value.
141
+ * @param deps Array of dependency keys.
142
+ * @param cacheId Optional unique cache identifier.
143
+ */
144
+ cacheCompute(factory, deps, cacheId) {
145
+ const id = cacheId || `cacheCompute_${deps.join("_")}`;
146
+ let memoEntry = this.cache.get(id);
147
+ let prevDeps = this.cache.get(`${id}_deps`);
148
+ const depsChanged = !prevDeps ||
149
+ deps.length !== prevDeps.value.length ||
150
+ deps.some((d, i) => d !== prevDeps.value[i]);
151
+ if (!memoEntry || depsChanged) {
152
+ const value = factory();
153
+ memoEntry = { value };
154
+ this.cache.set(id, memoEntry);
155
+ this.cache.set(`${id}_deps`, { value: [...deps] });
156
+ }
157
+ return memoEntry.value;
158
+ }
159
+ /**
160
+ * Memoize a callback function based on dependencies (server-side useCallback alternative).
161
+ * The cached function is recreated only when dependencies change.
162
+ * @param fn Function to memoize.
163
+ * @param deps Array of dependency keys.
164
+ * @param callbackId Optional unique cache identifier.
165
+ */
166
+ cacheCallback(fn, deps, callbackId) {
167
+ return this.cacheCompute(() => fn, deps, callbackId);
168
+ }
137
169
  }
138
170
  // factory (so callers don’t import the class directly)
139
171
  const createServerState = (sweep) => new ServerState(sweep);
package/dist/index.js.map CHANGED
@@ -1 +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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"}
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 * Memoize a value based on dependencies (server-side useMemo alternative).\n * The cached value is recomputed only when dependencies change.\n * @param factory Function to compute the value.\n * @param deps Array of dependency keys.\n * @param cacheId Optional unique cache identifier.\n */\n cacheCompute<T>(factory: () => T, deps: string[], cacheId?: string): T {\n const id = cacheId || `cacheCompute_${deps.join(\"_\")}`;\n let memoEntry = this.cache.get(id) as Entry<T> | undefined;\n let prevDeps = this.cache.get(`${id}_deps`) as Entry<string[]> | undefined;\n\n const depsChanged =\n !prevDeps ||\n deps.length !== prevDeps.value.length ||\n deps.some((d, i) => d !== prevDeps!.value[i]);\n\n if (!memoEntry || depsChanged) {\n const value = factory();\n memoEntry = { value };\n this.cache.set(id, memoEntry);\n this.cache.set(`${id}_deps`, { value: [...deps] });\n }\n return memoEntry.value;\n }\n\n /**\n * Memoize a callback function based on dependencies (server-side useCallback alternative).\n * The cached function is recreated only when dependencies change.\n * @param fn Function to memoize.\n * @param deps Array of dependency keys.\n * @param callbackId Optional unique cache identifier.\n */\n cacheCallback<T extends (...args: any[]) => any>(\n fn: T,\n deps: string[],\n callbackId?: string\n ): T {\n return this.cacheCompute(() => fn, deps, callbackId) as T;\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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-server-state",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Lightweight TTL-enabled in-memory state for Node servers",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",