@x12i/api-hub-cache 0.0.0-stage → 0.1.1

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
@@ -1,3 +1,11 @@
1
- # Temporary Holding Version
1
+ # @x12i/api-hub-cache
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Response cache with 6 hour and 24 hour TTLs for the x12i API hub
4
+
5
+ ESM package for Node.js 24 or newer. Import the public name:
6
+
7
+ ```js
8
+ import * as cache from '@x12i/api-hub-cache';
9
+ ```
10
+
11
+ The whole set is also available from [`@x12i/api-hub`](https://www.npmjs.com/package/@x12i/api-hub).
@@ -0,0 +1,23 @@
1
+ import type { Cache, CacheTtl } from '@x12i/api-hub-core';
2
+ export declare const TTL_SECONDS: {
3
+ readonly '6h': number;
4
+ readonly '24h': number;
5
+ };
6
+ export declare function ttlSeconds(ttl: CacheTtl): number;
7
+ /** Storage behind the cache, so Redis or another shared store can replace memory later. */
8
+ export interface CacheStore {
9
+ get(key: string): Promise<{
10
+ value: unknown;
11
+ expiresAt: number;
12
+ } | undefined>;
13
+ set(key: string, value: unknown, expiresAt: number): Promise<void>;
14
+ delete(key: string): Promise<void>;
15
+ }
16
+ export declare function memoryStore(maxEntries?: number): CacheStore;
17
+ /** Deterministic key for a capability call: same slug and same input (any key order) give the same key. */
18
+ export declare function cacheKey(namespace: string, input: unknown): string;
19
+ export interface CacheOptions {
20
+ store?: CacheStore;
21
+ now?: () => number;
22
+ }
23
+ export declare function createCache(options?: CacheOptions): Cache;
package/dist/index.js ADDED
@@ -0,0 +1,80 @@
1
+ import { createHash } from 'node:crypto';
2
+ export const TTL_SECONDS = { '6h': 6 * 3600, '24h': 24 * 3600 };
3
+ export function ttlSeconds(ttl) {
4
+ return typeof ttl === 'number' ? ttl : TTL_SECONDS[ttl];
5
+ }
6
+ export function memoryStore(maxEntries = 10_000) {
7
+ const map = new Map();
8
+ return {
9
+ async get(key) {
10
+ const hit = map.get(key);
11
+ if (!hit)
12
+ return undefined;
13
+ map.delete(key);
14
+ map.set(key, hit);
15
+ return hit;
16
+ },
17
+ async set(key, value, expiresAt) {
18
+ map.delete(key);
19
+ map.set(key, { value, expiresAt });
20
+ while (map.size > maxEntries)
21
+ map.delete(map.keys().next().value);
22
+ },
23
+ async delete(key) {
24
+ map.delete(key);
25
+ },
26
+ };
27
+ }
28
+ function stable(value) {
29
+ if (Array.isArray(value))
30
+ return value.map(stable);
31
+ if (value && typeof value === 'object') {
32
+ return Object.fromEntries(Object.keys(value)
33
+ .sort()
34
+ .map((k) => [k, stable(value[k])]));
35
+ }
36
+ return value;
37
+ }
38
+ /** Deterministic key for a capability call: same slug and same input (any key order) give the same key. */
39
+ export function cacheKey(namespace, input) {
40
+ const digest = createHash('sha256').update(JSON.stringify(stable(input))).digest('base64url');
41
+ return `${namespace}:${digest}`;
42
+ }
43
+ export function createCache(options = {}) {
44
+ const store = options.store ?? memoryStore();
45
+ const now = options.now ?? Date.now;
46
+ const inflight = new Map();
47
+ const cache = {
48
+ async get(key) {
49
+ const hit = await store.get(key);
50
+ if (!hit)
51
+ return undefined;
52
+ if (hit.expiresAt <= now()) {
53
+ await store.delete(key);
54
+ return undefined;
55
+ }
56
+ return hit.value;
57
+ },
58
+ async set(key, value, ttl) {
59
+ await store.set(key, value, now() + ttlSeconds(ttl) * 1000);
60
+ },
61
+ async wrap(key, ttl, fn) {
62
+ const cached = await cache.get(key);
63
+ if (cached !== undefined)
64
+ return cached;
65
+ const pending = inflight.get(key);
66
+ if (pending)
67
+ return pending;
68
+ const run = fn()
69
+ .then(async (value) => {
70
+ await cache.set(key, value, ttl);
71
+ return value;
72
+ })
73
+ .finally(() => inflight.delete(key));
74
+ inflight.set(key, run);
75
+ return run;
76
+ },
77
+ };
78
+ return cache;
79
+ }
80
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/cache/src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport type { Cache, CacheTtl } from '@x12i/api-hub-core';\n\nexport const TTL_SECONDS = { '6h': 6 * 3600, '24h': 24 * 3600 } as const;\n\nexport function ttlSeconds(ttl: CacheTtl): number {\n return typeof ttl === 'number' ? ttl : TTL_SECONDS[ttl];\n}\n\n/** Storage behind the cache, so Redis or another shared store can replace memory later. */\nexport interface CacheStore {\n get(key: string): Promise<{ value: unknown; expiresAt: number } | undefined>;\n set(key: string, value: unknown, expiresAt: number): Promise<void>;\n delete(key: string): Promise<void>;\n}\n\nexport function memoryStore(maxEntries = 10_000): CacheStore {\n const map = new Map<string, { value: unknown; expiresAt: number }>();\n return {\n async get(key) {\n const hit = map.get(key);\n if (!hit) return undefined;\n map.delete(key);\n map.set(key, hit);\n return hit;\n },\n async set(key, value, expiresAt) {\n map.delete(key);\n map.set(key, { value, expiresAt });\n while (map.size > maxEntries) map.delete(map.keys().next().value!);\n },\n async delete(key) {\n map.delete(key);\n },\n };\n}\n\nfunction stable(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(stable);\n if (value && typeof value === 'object') {\n return Object.fromEntries(\n Object.keys(value as Record<string, unknown>)\n .sort()\n .map((k) => [k, stable((value as Record<string, unknown>)[k])]),\n );\n }\n return value;\n}\n\n/** Deterministic key for a capability call: same slug and same input (any key order) give the same key. */\nexport function cacheKey(namespace: string, input: unknown): string {\n const digest = createHash('sha256').update(JSON.stringify(stable(input))).digest('base64url');\n return `${namespace}:${digest}`;\n}\n\nexport interface CacheOptions {\n store?: CacheStore;\n now?: () => number;\n}\n\nexport function createCache(options: CacheOptions = {}): Cache {\n const store = options.store ?? memoryStore();\n const now = options.now ?? Date.now;\n const inflight = new Map<string, Promise<unknown>>();\n\n const cache: Cache = {\n async get<T>(key: string) {\n const hit = await store.get(key);\n if (!hit) return undefined;\n if (hit.expiresAt <= now()) {\n await store.delete(key);\n return undefined;\n }\n return hit.value as T;\n },\n async set<T>(key: string, value: T, ttl: CacheTtl) {\n await store.set(key, value, now() + ttlSeconds(ttl) * 1000);\n },\n async wrap<T>(key: string, ttl: CacheTtl, fn: () => Promise<T>) {\n const cached = await cache.get<T>(key);\n if (cached !== undefined) return cached;\n const pending = inflight.get(key) as Promise<T> | undefined;\n if (pending) return pending;\n const run = fn()\n .then(async (value) => {\n await cache.set(key, value, ttl);\n return value;\n })\n .finally(() => inflight.delete(key));\n inflight.set(key, run);\n return run;\n },\n };\n return cache;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "@x12i/api-hub-cache",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Response cache with 6 hour and 24 hour TTLs for the x12i API hub",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=24"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/x12i/apis-hub.git",
24
+ "directory": "packages/cache"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/x12i/apis-hub/issues"
28
+ },
29
+ "homepage": "https://github.com/x12i/apis-hub",
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "dependencies": {
34
+ "@x12i/api-hub-core": "0.1.1"
35
+ }
36
+ }