@open-mercato/cache 0.3.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.
Files changed (44) hide show
  1. package/ENV.md +173 -0
  2. package/README.md +177 -0
  3. package/dist/errors.d.ts +7 -0
  4. package/dist/errors.d.ts.map +1 -0
  5. package/dist/errors.js +9 -0
  6. package/dist/index.d.ts +8 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +7 -0
  9. package/dist/service.d.ts +40 -0
  10. package/dist/service.d.ts.map +1 -0
  11. package/dist/service.js +321 -0
  12. package/dist/strategies/jsonfile.d.ts +10 -0
  13. package/dist/strategies/jsonfile.d.ts.map +1 -0
  14. package/dist/strategies/jsonfile.js +205 -0
  15. package/dist/strategies/memory.d.ts +9 -0
  16. package/dist/strategies/memory.d.ts.map +1 -0
  17. package/dist/strategies/memory.js +166 -0
  18. package/dist/strategies/redis.d.ts +5 -0
  19. package/dist/strategies/redis.d.ts.map +1 -0
  20. package/dist/strategies/redis.js +388 -0
  21. package/dist/strategies/sqlite.d.ts +13 -0
  22. package/dist/strategies/sqlite.d.ts.map +1 -0
  23. package/dist/strategies/sqlite.js +217 -0
  24. package/dist/tenantContext.d.ts +4 -0
  25. package/dist/tenantContext.d.ts.map +1 -0
  26. package/dist/tenantContext.js +9 -0
  27. package/dist/types.d.ts +86 -0
  28. package/dist/types.d.ts.map +1 -0
  29. package/dist/types.js +1 -0
  30. package/jest.config.js +19 -0
  31. package/package.json +39 -0
  32. package/src/__tests__/memory.strategy.test.ts +245 -0
  33. package/src/__tests__/service.test.ts +189 -0
  34. package/src/errors.ts +14 -0
  35. package/src/index.ts +7 -0
  36. package/src/service.ts +367 -0
  37. package/src/strategies/jsonfile.ts +249 -0
  38. package/src/strategies/memory.ts +185 -0
  39. package/src/strategies/redis.ts +443 -0
  40. package/src/strategies/sqlite.ts +285 -0
  41. package/src/tenantContext.ts +13 -0
  42. package/src/types.ts +100 -0
  43. package/tsconfig.build.json +5 -0
  44. package/tsconfig.json +12 -0
@@ -0,0 +1,249 @@
1
+ import type { CacheStrategy, CacheEntry, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ /**
6
+ * JSON file cache strategy with tag support
7
+ * Persistent across process restarts, stored in JSON files
8
+ * Simple and requires no external dependencies, but not suitable for high-performance scenarios
9
+ */
10
+ export function createJsonFileStrategy(filePath?: string, options?: { defaultTtl?: number }): CacheStrategy {
11
+ const defaultTtl = options?.defaultTtl
12
+ const cacheFile = filePath || process.env.CACHE_JSON_FILE_PATH || '.cache.json'
13
+ const dir = path.dirname(cacheFile)
14
+
15
+ type StorageData = {
16
+ entries: Record<string, CacheEntry>
17
+ tagIndex: Record<string, string[]> // tag -> array of keys
18
+ }
19
+
20
+ function ensureDir(): void {
21
+ if (!fs.existsSync(dir)) {
22
+ fs.mkdirSync(dir, { recursive: true })
23
+ }
24
+ }
25
+
26
+ function readData(): StorageData {
27
+ ensureDir()
28
+ if (!fs.existsSync(cacheFile)) {
29
+ return { entries: {}, tagIndex: {} }
30
+ }
31
+ try {
32
+ const content = fs.readFileSync(cacheFile, 'utf8')
33
+ return JSON.parse(content) as StorageData
34
+ } catch {
35
+ return { entries: {}, tagIndex: {} }
36
+ }
37
+ }
38
+
39
+ function writeData(data: StorageData): void {
40
+ ensureDir()
41
+ fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2), 'utf8')
42
+ }
43
+
44
+ function isExpired(entry: CacheEntry): boolean {
45
+ if (entry.expiresAt === null) return false
46
+ return Date.now() > entry.expiresAt
47
+ }
48
+
49
+ function matchPattern(key: string, pattern: string): boolean {
50
+ const regexPattern = pattern
51
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
52
+ .replace(/\*/g, '.*')
53
+ .replace(/\?/g, '.')
54
+ const regex = new RegExp(`^${regexPattern}$`)
55
+ return regex.test(key)
56
+ }
57
+
58
+ function addToTagIndex(data: StorageData, key: string, tags: string[]): void {
59
+ for (const tag of tags) {
60
+ if (!data.tagIndex[tag]) {
61
+ data.tagIndex[tag] = []
62
+ }
63
+ if (!data.tagIndex[tag].includes(key)) {
64
+ data.tagIndex[tag].push(key)
65
+ }
66
+ }
67
+ }
68
+
69
+ function removeFromTagIndex(data: StorageData, key: string, tags: string[]): void {
70
+ for (const tag of tags) {
71
+ if (data.tagIndex[tag]) {
72
+ data.tagIndex[tag] = data.tagIndex[tag].filter((k) => k !== key)
73
+ if (data.tagIndex[tag].length === 0) {
74
+ delete data.tagIndex[tag]
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ const get = async (key: string, options?: CacheGetOptions): Promise<CacheValue | null> => {
81
+ const data = readData()
82
+ const entry = data.entries[key]
83
+
84
+ if (!entry) return null
85
+
86
+ if (isExpired(entry)) {
87
+ if (options?.returnExpired) {
88
+ return entry.value
89
+ }
90
+ // Clean up expired entry
91
+ removeFromTagIndex(data, key, entry.tags)
92
+ delete data.entries[key]
93
+ writeData(data)
94
+ return null
95
+ }
96
+
97
+ return entry.value
98
+ }
99
+
100
+ const set = async (key: string, value: CacheValue, options?: CacheSetOptions): Promise<void> => {
101
+ const data = readData()
102
+
103
+ // Remove old entry from tag index if it exists
104
+ const oldEntry = data.entries[key]
105
+ if (oldEntry) {
106
+ removeFromTagIndex(data, key, oldEntry.tags)
107
+ }
108
+
109
+ const ttl = options?.ttl ?? defaultTtl
110
+ const tags = options?.tags || []
111
+ const expiresAt = ttl ? Date.now() + ttl : null
112
+
113
+ const entry: CacheEntry = {
114
+ key,
115
+ value,
116
+ tags,
117
+ expiresAt,
118
+ createdAt: Date.now(),
119
+ }
120
+
121
+ data.entries[key] = entry
122
+ addToTagIndex(data, key, tags)
123
+
124
+ writeData(data)
125
+ }
126
+
127
+ const has = async (key: string): Promise<boolean> => {
128
+ const data = readData()
129
+ const entry = data.entries[key]
130
+
131
+ if (!entry) return false
132
+
133
+ if (isExpired(entry)) {
134
+ removeFromTagIndex(data, key, entry.tags)
135
+ delete data.entries[key]
136
+ writeData(data)
137
+ return false
138
+ }
139
+
140
+ return true
141
+ }
142
+
143
+ const deleteKey = async (key: string): Promise<boolean> => {
144
+ const data = readData()
145
+ const entry = data.entries[key]
146
+
147
+ if (!entry) return false
148
+
149
+ removeFromTagIndex(data, key, entry.tags)
150
+ delete data.entries[key]
151
+ writeData(data)
152
+
153
+ return true
154
+ }
155
+
156
+ const deleteByTags = async (tags: string[]): Promise<number> => {
157
+ const data = readData()
158
+ const keysToDelete = new Set<string>()
159
+
160
+ // Collect all keys that have any of the specified tags
161
+ for (const tag of tags) {
162
+ const keys = data.tagIndex[tag] || []
163
+ for (const key of keys) {
164
+ keysToDelete.add(key)
165
+ }
166
+ }
167
+
168
+ // Delete all collected keys
169
+ for (const key of keysToDelete) {
170
+ const entry = data.entries[key]
171
+ if (entry) {
172
+ removeFromTagIndex(data, key, entry.tags)
173
+ delete data.entries[key]
174
+ }
175
+ }
176
+
177
+ writeData(data)
178
+ return keysToDelete.size
179
+ }
180
+
181
+ const clear = async (): Promise<number> => {
182
+ const data = readData()
183
+ const size = Object.keys(data.entries).length
184
+
185
+ writeData({ entries: {}, tagIndex: {} })
186
+
187
+ return size
188
+ }
189
+
190
+ const keys = async (pattern?: string): Promise<string[]> => {
191
+ const data = readData()
192
+ const allKeys = Object.keys(data.entries)
193
+
194
+ if (!pattern) return allKeys
195
+
196
+ return allKeys.filter((key) => matchPattern(key, pattern))
197
+ }
198
+
199
+ const stats = async (): Promise<{ size: number; expired: number }> => {
200
+ const data = readData()
201
+ const allEntries = Object.values(data.entries)
202
+
203
+ let expired = 0
204
+ for (const entry of allEntries) {
205
+ if (isExpired(entry)) {
206
+ expired++
207
+ }
208
+ }
209
+
210
+ return { size: allEntries.length, expired }
211
+ }
212
+
213
+ const cleanup = async (): Promise<number> => {
214
+ const data = readData()
215
+ let removed = 0
216
+
217
+ const keysToRemove: string[] = []
218
+ for (const [key, entry] of Object.entries(data.entries)) {
219
+ if (isExpired(entry)) {
220
+ keysToRemove.push(key)
221
+ }
222
+ }
223
+
224
+ for (const key of keysToRemove) {
225
+ const entry = data.entries[key]
226
+ removeFromTagIndex(data, key, entry.tags)
227
+ delete data.entries[key]
228
+ removed++
229
+ }
230
+
231
+ if (removed > 0) {
232
+ writeData(data)
233
+ }
234
+
235
+ return removed
236
+ }
237
+
238
+ return {
239
+ get,
240
+ set,
241
+ has,
242
+ delete: deleteKey,
243
+ deleteByTags,
244
+ clear,
245
+ keys,
246
+ stats,
247
+ cleanup,
248
+ }
249
+ }
@@ -0,0 +1,185 @@
1
+ import type { CacheStrategy, CacheEntry, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'
2
+
3
+ /**
4
+ * In-memory cache strategy with tag support
5
+ * Fast but data is lost when process restarts
6
+ */
7
+ export function createMemoryStrategy(options?: { defaultTtl?: number }): CacheStrategy {
8
+ const store = new Map<string, CacheEntry>()
9
+ const tagIndex = new Map<string, Set<string>>() // tag -> Set of keys
10
+ const defaultTtl = options?.defaultTtl
11
+
12
+ function isExpired(entry: CacheEntry): boolean {
13
+ if (entry.expiresAt === null) return false
14
+ return Date.now() > entry.expiresAt
15
+ }
16
+
17
+ function cleanupExpiredEntry(key: string, entry: CacheEntry): void {
18
+ store.delete(key)
19
+ // Remove from tag index
20
+ for (const tag of entry.tags) {
21
+ const keys = tagIndex.get(tag)
22
+ if (keys) {
23
+ keys.delete(key)
24
+ if (keys.size === 0) {
25
+ tagIndex.delete(tag)
26
+ }
27
+ }
28
+ }
29
+ }
30
+
31
+ function addToTagIndex(key: string, tags: string[]): void {
32
+ for (const tag of tags) {
33
+ if (!tagIndex.has(tag)) {
34
+ tagIndex.set(tag, new Set())
35
+ }
36
+ tagIndex.get(tag)!.add(key)
37
+ }
38
+ }
39
+
40
+ function removeFromTagIndex(key: string, tags: string[]): void {
41
+ for (const tag of tags) {
42
+ const keys = tagIndex.get(tag)
43
+ if (keys) {
44
+ keys.delete(key)
45
+ if (keys.size === 0) {
46
+ tagIndex.delete(tag)
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ function matchPattern(key: string, pattern: string): boolean {
53
+ const regexPattern = pattern
54
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars
55
+ .replace(/\*/g, '.*') // * matches any characters
56
+ .replace(/\?/g, '.') // ? matches single character
57
+ const regex = new RegExp(`^${regexPattern}$`)
58
+ return regex.test(key)
59
+ }
60
+
61
+ const get = async (key: string, options?: CacheGetOptions): Promise<CacheValue | null> => {
62
+ const entry = store.get(key)
63
+ if (!entry) return null
64
+
65
+ if (isExpired(entry)) {
66
+ if (options?.returnExpired) {
67
+ return entry.value
68
+ }
69
+ cleanupExpiredEntry(key, entry)
70
+ return null
71
+ }
72
+
73
+ return entry.value
74
+ }
75
+
76
+ const set = async (key: string, value: CacheValue, options?: CacheSetOptions): Promise<void> => {
77
+ // Remove old entry from tag index if it exists
78
+ const oldEntry = store.get(key)
79
+ if (oldEntry) {
80
+ removeFromTagIndex(key, oldEntry.tags)
81
+ }
82
+
83
+ const ttl = options?.ttl ?? defaultTtl
84
+ const tags = options?.tags || []
85
+ const expiresAt = ttl ? Date.now() + ttl : null
86
+
87
+ const entry: CacheEntry = {
88
+ key,
89
+ value,
90
+ tags,
91
+ expiresAt,
92
+ createdAt: Date.now(),
93
+ }
94
+
95
+ store.set(key, entry)
96
+ addToTagIndex(key, tags)
97
+ }
98
+
99
+ const has = async (key: string): Promise<boolean> => {
100
+ const entry = store.get(key)
101
+ if (!entry) return false
102
+ if (isExpired(entry)) {
103
+ cleanupExpiredEntry(key, entry)
104
+ return false
105
+ }
106
+ return true
107
+ }
108
+
109
+ const deleteKey = async (key: string): Promise<boolean> => {
110
+ const entry = store.get(key)
111
+ if (!entry) return false
112
+
113
+ removeFromTagIndex(key, entry.tags)
114
+ return store.delete(key)
115
+ }
116
+
117
+ const deleteByTags = async (tags: string[]): Promise<number> => {
118
+ const keysToDelete = new Set<string>()
119
+
120
+ // Collect all keys that have any of the specified tags
121
+ for (const tag of tags) {
122
+ const keys = tagIndex.get(tag)
123
+ if (keys) {
124
+ for (const key of keys) {
125
+ keysToDelete.add(key)
126
+ }
127
+ }
128
+ }
129
+
130
+ // Delete all collected keys
131
+ let deleted = 0
132
+ for (const key of keysToDelete) {
133
+ const success = await deleteKey(key)
134
+ if (success) deleted++
135
+ }
136
+
137
+ return deleted
138
+ }
139
+
140
+ const clear = async (): Promise<number> => {
141
+ const size = store.size
142
+ store.clear()
143
+ tagIndex.clear()
144
+ return size
145
+ }
146
+
147
+ const keys = async (pattern?: string): Promise<string[]> => {
148
+ const allKeys = Array.from(store.keys())
149
+ if (!pattern) return allKeys
150
+ return allKeys.filter((key) => matchPattern(key, pattern))
151
+ }
152
+
153
+ const stats = async (): Promise<{ size: number; expired: number }> => {
154
+ let expired = 0
155
+ for (const entry of store.values()) {
156
+ if (isExpired(entry)) {
157
+ expired++
158
+ }
159
+ }
160
+ return { size: store.size, expired }
161
+ }
162
+
163
+ const cleanup = async (): Promise<number> => {
164
+ let removed = 0
165
+ for (const [key, entry] of store.entries()) {
166
+ if (isExpired(entry)) {
167
+ cleanupExpiredEntry(key, entry)
168
+ removed++
169
+ }
170
+ }
171
+ return removed
172
+ }
173
+
174
+ return {
175
+ get,
176
+ set,
177
+ has,
178
+ delete: deleteKey,
179
+ deleteByTags,
180
+ clear,
181
+ keys,
182
+ stats,
183
+ cleanup,
184
+ }
185
+ }