@govish/shared-services 1.5.1 → 1.6.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/CHANGELOG.md +7 -0
- package/README.md +2 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/utils/rollingCache.d.ts +34 -0
- package/dist/utils/rollingCache.js +203 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Version 1.6.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Rolling cache utilities** (`createRollingCache`): list/item pattern with `buildKey`, `get`, `set`, `getOrSet`, `invalidate`, `invalidateByPrefix`, `recordAccess` (Redis `ZINCRBY` for hot-key telemetry), and default TTL helpers from `CACHE_DAYS_LIST` / `CACHE_DAYS_ITEM`. Opt-in via `ROLLING_CACHE_ENABLED` / `CACHE_ENABLED`. Uses the host `redisClient` for `setEx`, `del`, `scan`, and sorted sets.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
3
10
|
## Version 1.5.1
|
|
4
11
|
|
|
5
12
|
### Fixed
|
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@ const authenticateDeviceOrOfficer = createAuthenticateDeviceOrOfficer(dependenci
|
|
|
37
37
|
|
|
38
38
|
## Utilities Included
|
|
39
39
|
|
|
40
|
+
- **createRollingCache**: List/item rolling cache (`buildKey`, `get`, `set`, `getOrSet`, `invalidate`, `invalidateByPrefix`, `recordAccess`) with day-based TTL from `CACHE_DAYS_LIST` / `CACHE_DAYS_ITEM`; enable with `ROLLING_CACHE_ENABLED=true`
|
|
40
41
|
- **createSafeRedisGet**: Factory function to create a safe Redis GET operation with automatic reconnection
|
|
41
42
|
- **createSafeRedisSet**: Factory function to create a safe Redis SET operation with automatic reconnection
|
|
42
43
|
- **createSafeRedisUtils**: Factory function that creates both safeRedisGet and safeRedisSet functions
|
|
@@ -65,7 +66,7 @@ import {
|
|
|
65
66
|
ApiKeyService,
|
|
66
67
|
createAuthenticateDeviceOrOfficer,
|
|
67
68
|
SharedServicesDependencies
|
|
68
|
-
} from '@
|
|
69
|
+
} from '@govish/shared-services';
|
|
69
70
|
import { redisClient } from './app';
|
|
70
71
|
import { logger } from './utils/logger';
|
|
71
72
|
// ... other dependencies
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export { AuditEventType } from './services/auditService';
|
|
|
13
13
|
export type { AuditEventPayload } from './services/auditService';
|
|
14
14
|
export { createAuthenticateDeviceOrOfficer } from './middleware/authenticateDevice';
|
|
15
15
|
export { createSafeRedisGet, createSafeRedisSet, createSafeRedisUtils } from './utils/redis';
|
|
16
|
+
export { createRollingCache, ROLLING_CACHE_SEP, } from './utils/rollingCache';
|
|
17
|
+
export type { RollingCacheInstance, RollingCacheOptions, RollingCacheTtl, } from './utils/rollingCache';
|
|
16
18
|
export { getEnvironmentMode, isDevelopmentMode, isProductionMode, devLog, devError, devWarn, devDebug } from './utils/logMode';
|
|
17
19
|
export type { EnvironmentMode } from './utils/logMode';
|
|
18
20
|
export type { SharedServicesDependencies } from './types/dependencies';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.devDebug = exports.devWarn = exports.devError = exports.devLog = exports.isProductionMode = exports.isDevelopmentMode = exports.getEnvironmentMode = exports.createSafeRedisUtils = exports.createSafeRedisSet = exports.createSafeRedisGet = exports.createAuthenticateDeviceOrOfficer = exports.AuditEventType = exports.AuditService = exports.ApiKeyService = exports.RankLeaveDaysCacheService = exports.StationDutyBlockCacheService = exports.StationCacheService = exports.DeviceService = exports.DeviceCacheService = exports.PenalCodeCacheService = exports.OfficerService = exports.OfficerCacheService = void 0;
|
|
3
|
+
exports.devDebug = exports.devWarn = exports.devError = exports.devLog = exports.isProductionMode = exports.isDevelopmentMode = exports.getEnvironmentMode = exports.ROLLING_CACHE_SEP = exports.createRollingCache = exports.createSafeRedisUtils = exports.createSafeRedisSet = exports.createSafeRedisGet = exports.createAuthenticateDeviceOrOfficer = exports.AuditEventType = exports.AuditService = exports.ApiKeyService = exports.RankLeaveDaysCacheService = exports.StationDutyBlockCacheService = exports.StationCacheService = exports.DeviceService = exports.DeviceCacheService = exports.PenalCodeCacheService = exports.OfficerService = exports.OfficerCacheService = void 0;
|
|
4
4
|
// Export services
|
|
5
5
|
var officerCacheService_1 = require("./services/officerCacheService");
|
|
6
6
|
Object.defineProperty(exports, "OfficerCacheService", { enumerable: true, get: function () { return officerCacheService_1.OfficerCacheService; } });
|
|
@@ -32,6 +32,9 @@ var redis_1 = require("./utils/redis");
|
|
|
32
32
|
Object.defineProperty(exports, "createSafeRedisGet", { enumerable: true, get: function () { return redis_1.createSafeRedisGet; } });
|
|
33
33
|
Object.defineProperty(exports, "createSafeRedisSet", { enumerable: true, get: function () { return redis_1.createSafeRedisSet; } });
|
|
34
34
|
Object.defineProperty(exports, "createSafeRedisUtils", { enumerable: true, get: function () { return redis_1.createSafeRedisUtils; } });
|
|
35
|
+
var rollingCache_1 = require("./utils/rollingCache");
|
|
36
|
+
Object.defineProperty(exports, "createRollingCache", { enumerable: true, get: function () { return rollingCache_1.createRollingCache; } });
|
|
37
|
+
Object.defineProperty(exports, "ROLLING_CACHE_SEP", { enumerable: true, get: function () { return rollingCache_1.ROLLING_CACHE_SEP; } });
|
|
35
38
|
var logMode_1 = require("./utils/logMode");
|
|
36
39
|
Object.defineProperty(exports, "getEnvironmentMode", { enumerable: true, get: function () { return logMode_1.getEnvironmentMode; } });
|
|
37
40
|
Object.defineProperty(exports, "isDevelopmentMode", { enumerable: true, get: function () { return logMode_1.isDevelopmentMode; } });
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type RollingCacheTtl = number | {
|
|
2
|
+
days: number;
|
|
3
|
+
};
|
|
4
|
+
export type RollingCacheOptions = {
|
|
5
|
+
redisClient: any;
|
|
6
|
+
/** If omitted, uses createSafeRedisGet(redisClient). */
|
|
7
|
+
safeRedisGet?: (key: string) => Promise<string | null>;
|
|
8
|
+
/** Env var name for enable flag; default ROLLING_CACHE_ENABLED. Also reads CACHE_ENABLED if unset. */
|
|
9
|
+
enableEnvKey?: string;
|
|
10
|
+
logWarn?: (message: string, meta?: Record<string, unknown>) => void;
|
|
11
|
+
};
|
|
12
|
+
export type RollingCacheInstance = {
|
|
13
|
+
buildKey: (entity: string, ...parts: (string | number)[]) => string;
|
|
14
|
+
isCacheEnabled: () => boolean;
|
|
15
|
+
get: <T = unknown>(key: string) => Promise<T | null>;
|
|
16
|
+
set: (key: string, value: unknown, ttl?: RollingCacheTtl) => Promise<boolean>;
|
|
17
|
+
getOrSet: <T>(key: string, ttl: RollingCacheTtl | undefined, loader: () => Promise<T>) => Promise<T>;
|
|
18
|
+
invalidate: (key: string) => Promise<void>;
|
|
19
|
+
invalidateByPrefix: (prefix: string) => Promise<number>;
|
|
20
|
+
recordAccess: (zsetKey: string, member: string, incrementBy?: number) => Promise<void>;
|
|
21
|
+
getDefaultListTtl: () => {
|
|
22
|
+
days: number;
|
|
23
|
+
};
|
|
24
|
+
getDefaultItemTtl: () => {
|
|
25
|
+
days: number;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Factory for list/item rolling cache (Redis string keys + JSON values).
|
|
30
|
+
* Pass the same redis client used by the host app. Optional safeRedisGet for custom wiring.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createRollingCache(options: RollingCacheOptions): RollingCacheInstance;
|
|
33
|
+
/** Key segment separator used by buildKey (exported for invalidation helpers). */
|
|
34
|
+
export declare const ROLLING_CACHE_SEP = "|";
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.ROLLING_CACHE_SEP = void 0;
|
|
13
|
+
exports.createRollingCache = createRollingCache;
|
|
14
|
+
const redis_1 = require("./redis");
|
|
15
|
+
const SEP = '|';
|
|
16
|
+
const DEFAULT_LIST_DAYS = 7;
|
|
17
|
+
const DEFAULT_ITEM_DAYS = 30;
|
|
18
|
+
const DEFAULT_ENABLE_ENV = 'ROLLING_CACHE_ENABLED';
|
|
19
|
+
function ensureConnected(redisClient) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
var _a;
|
|
22
|
+
if (redisClient === null || redisClient === void 0 ? void 0 : redisClient.isOpen) {
|
|
23
|
+
resolve(true);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
(_a = redisClient === null || redisClient === void 0 ? void 0 : redisClient.connect) === null || _a === void 0 ? void 0 : _a.call(redisClient).then(() => resolve(true)).catch(() => resolve(false));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function normalizePart(part) {
|
|
30
|
+
if (part === null || part === undefined)
|
|
31
|
+
return '';
|
|
32
|
+
const s = String(part).trim();
|
|
33
|
+
return s.replace(/\|/g, '_').replace(/:/g, '_');
|
|
34
|
+
}
|
|
35
|
+
function ttlSeconds(ttl) {
|
|
36
|
+
if (ttl == null)
|
|
37
|
+
return undefined;
|
|
38
|
+
if (typeof ttl === 'number')
|
|
39
|
+
return ttl > 0 ? ttl : undefined;
|
|
40
|
+
if (ttl.days > 0)
|
|
41
|
+
return ttl.days * 86400;
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
function getCacheDaysList() {
|
|
45
|
+
const v = process.env.CACHE_DAYS_LIST;
|
|
46
|
+
if (v != null && v !== '') {
|
|
47
|
+
const n = parseInt(v, 10);
|
|
48
|
+
if (!Number.isNaN(n) && n > 0)
|
|
49
|
+
return n;
|
|
50
|
+
}
|
|
51
|
+
return DEFAULT_LIST_DAYS;
|
|
52
|
+
}
|
|
53
|
+
function getCacheDaysItem() {
|
|
54
|
+
const v = process.env.CACHE_DAYS_ITEM;
|
|
55
|
+
if (v != null && v !== '') {
|
|
56
|
+
const n = parseInt(v, 10);
|
|
57
|
+
if (!Number.isNaN(n) && n > 0)
|
|
58
|
+
return n;
|
|
59
|
+
}
|
|
60
|
+
return DEFAULT_ITEM_DAYS;
|
|
61
|
+
}
|
|
62
|
+
const SCAN_COUNT = 100;
|
|
63
|
+
/**
|
|
64
|
+
* Factory for list/item rolling cache (Redis string keys + JSON values).
|
|
65
|
+
* Pass the same redis client used by the host app. Optional safeRedisGet for custom wiring.
|
|
66
|
+
*/
|
|
67
|
+
function createRollingCache(options) {
|
|
68
|
+
var _a;
|
|
69
|
+
const { redisClient, enableEnvKey = DEFAULT_ENABLE_ENV, logWarn } = options;
|
|
70
|
+
const safeRedisGet = (_a = options.safeRedisGet) !== null && _a !== void 0 ? _a : (0, redis_1.createSafeRedisGet)(redisClient);
|
|
71
|
+
const warn = (message, meta) => {
|
|
72
|
+
if (logWarn)
|
|
73
|
+
logWarn(message, meta);
|
|
74
|
+
else
|
|
75
|
+
console.warn(`[RollingCache] ${message}`, meta !== null && meta !== void 0 ? meta : '');
|
|
76
|
+
};
|
|
77
|
+
const isCacheEnabled = () => {
|
|
78
|
+
var _a, _b;
|
|
79
|
+
const v = (_b = (_a = process.env[enableEnvKey]) !== null && _a !== void 0 ? _a : process.env.CACHE_ENABLED) !== null && _b !== void 0 ? _b : '';
|
|
80
|
+
const s = String(v).trim().toLowerCase();
|
|
81
|
+
return s === '1' || s === 'true' || s === 'yes';
|
|
82
|
+
};
|
|
83
|
+
const buildKey = (entity, ...parts) => {
|
|
84
|
+
const normalized = parts.map(normalizePart);
|
|
85
|
+
return [entity, ...normalized].join(SEP);
|
|
86
|
+
};
|
|
87
|
+
const get = (key) => __awaiter(this, void 0, void 0, function* () {
|
|
88
|
+
if (!isCacheEnabled())
|
|
89
|
+
return null;
|
|
90
|
+
try {
|
|
91
|
+
const raw = yield safeRedisGet(key);
|
|
92
|
+
if (raw == null)
|
|
93
|
+
return null;
|
|
94
|
+
return JSON.parse(raw);
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
warn('get parse error', { key, error: e instanceof Error ? e.message : String(e) });
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
const set = (key, value, ttl) => __awaiter(this, void 0, void 0, function* () {
|
|
102
|
+
if (!isCacheEnabled())
|
|
103
|
+
return true;
|
|
104
|
+
try {
|
|
105
|
+
const str = JSON.stringify(value);
|
|
106
|
+
const sec = ttlSeconds(ttl);
|
|
107
|
+
const ok = yield ensureConnected(redisClient);
|
|
108
|
+
if (!ok)
|
|
109
|
+
return false;
|
|
110
|
+
if (sec != null) {
|
|
111
|
+
yield redisClient.setEx(key, sec, str);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
yield redisClient.set(key, str);
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
warn('set error', { key, error: e instanceof Error ? e.message : String(e) });
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
const getOrSet = (key, ttl, loader) => __awaiter(this, void 0, void 0, function* () {
|
|
124
|
+
const hit = yield get(key);
|
|
125
|
+
if (hit !== null && hit !== undefined) {
|
|
126
|
+
return hit;
|
|
127
|
+
}
|
|
128
|
+
const v = yield loader();
|
|
129
|
+
yield set(key, v, ttl);
|
|
130
|
+
return v;
|
|
131
|
+
});
|
|
132
|
+
const invalidate = (key) => __awaiter(this, void 0, void 0, function* () {
|
|
133
|
+
if (!isCacheEnabled())
|
|
134
|
+
return;
|
|
135
|
+
try {
|
|
136
|
+
const ok = yield ensureConnected(redisClient);
|
|
137
|
+
if (!ok)
|
|
138
|
+
return;
|
|
139
|
+
yield redisClient.del(key);
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
warn('invalidate error', { key, error: e instanceof Error ? e.message : String(e) });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
const invalidateByPrefix = (prefix) => __awaiter(this, void 0, void 0, function* () {
|
|
146
|
+
if (!isCacheEnabled())
|
|
147
|
+
return 0;
|
|
148
|
+
let deleted = 0;
|
|
149
|
+
try {
|
|
150
|
+
const ok = yield ensureConnected(redisClient);
|
|
151
|
+
if (!ok)
|
|
152
|
+
return 0;
|
|
153
|
+
let cursor = 0;
|
|
154
|
+
for (;;) {
|
|
155
|
+
const scanResult = yield redisClient.scan(cursor, {
|
|
156
|
+
MATCH: `${prefix}*`,
|
|
157
|
+
COUNT: SCAN_COUNT,
|
|
158
|
+
});
|
|
159
|
+
const nextCursor = scanResult.cursor;
|
|
160
|
+
const keys = scanResult.keys;
|
|
161
|
+
if (keys.length > 0) {
|
|
162
|
+
yield redisClient.del(keys);
|
|
163
|
+
deleted += keys.length;
|
|
164
|
+
}
|
|
165
|
+
const c = typeof nextCursor === 'bigint' ? Number(nextCursor) : Number(nextCursor);
|
|
166
|
+
if (c === 0)
|
|
167
|
+
break;
|
|
168
|
+
cursor = nextCursor;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
warn('invalidateByPrefix error', { prefix, error: e instanceof Error ? e.message : String(e) });
|
|
173
|
+
}
|
|
174
|
+
return deleted;
|
|
175
|
+
});
|
|
176
|
+
const recordAccess = (zsetKey_1, member_1, ...args_1) => __awaiter(this, [zsetKey_1, member_1, ...args_1], void 0, function* (zsetKey, member, incrementBy = 1) {
|
|
177
|
+
if (!isCacheEnabled())
|
|
178
|
+
return;
|
|
179
|
+
try {
|
|
180
|
+
const ok = yield ensureConnected(redisClient);
|
|
181
|
+
if (!ok)
|
|
182
|
+
return;
|
|
183
|
+
yield redisClient.zIncrBy(zsetKey, incrementBy, member);
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
warn('recordAccess error', { zsetKey, error: e instanceof Error ? e.message : String(e) });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
buildKey,
|
|
191
|
+
isCacheEnabled,
|
|
192
|
+
get,
|
|
193
|
+
set,
|
|
194
|
+
getOrSet,
|
|
195
|
+
invalidate,
|
|
196
|
+
invalidateByPrefix,
|
|
197
|
+
recordAccess,
|
|
198
|
+
getDefaultListTtl: () => ({ days: getCacheDaysList() }),
|
|
199
|
+
getDefaultItemTtl: () => ({ days: getCacheDaysItem() }),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/** Key segment separator used by buildKey (exported for invalidation helpers). */
|
|
203
|
+
exports.ROLLING_CACHE_SEP = SEP;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@govish/shared-services",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Govish shared services package - officer, device, station, penal code, station-duty block, and rank leave days cache services; API key service; audit logging; authentication middleware with support for API key + officer combined authentication",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|