skill-base 2.0.15 → 2.0.17

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 (99) hide show
  1. package/README.md +22 -0
  2. package/bin/skill-base.js +7 -0
  3. package/package.json +1 -1
  4. package/src/cappy.js +86 -33
  5. package/src/index.js +45 -32
  6. package/src/models/skill.js +53 -27
  7. package/src/models/user.js +27 -2
  8. package/src/models/version.js +39 -21
  9. package/src/routes/collaborators.js +5 -0
  10. package/src/routes/publish.js +2 -0
  11. package/src/utils/lru-cache.js +160 -0
  12. package/src/utils/model-cache.js +158 -0
  13. package/static/assets/index-B3cIFt5P.css +1 -0
  14. package/static/assets/{index-BHB0vddE.js → index-Bw_H1j6L.js} +43 -43
  15. package/static/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
  16. package/static/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
  17. package/static/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
  18. package/static/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
  19. package/static/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
  20. package/static/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
  21. package/static/assets/inter-cyrillic-700-normal-CjBOestx.woff2 +0 -0
  22. package/static/assets/inter-cyrillic-700-normal-DrXBdSj3.woff +0 -0
  23. package/static/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
  24. package/static/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
  25. package/static/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
  26. package/static/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
  27. package/static/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
  28. package/static/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
  29. package/static/assets/inter-cyrillic-ext-700-normal-BjwYoWNd.woff2 +0 -0
  30. package/static/assets/inter-cyrillic-ext-700-normal-LO58E6JB.woff +0 -0
  31. package/static/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
  32. package/static/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
  33. package/static/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
  34. package/static/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
  35. package/static/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
  36. package/static/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
  37. package/static/assets/inter-greek-700-normal-BUv2fZ6O.woff +0 -0
  38. package/static/assets/inter-greek-700-normal-C3JjAnD8.woff2 +0 -0
  39. package/static/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
  40. package/static/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
  41. package/static/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
  42. package/static/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
  43. package/static/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
  44. package/static/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
  45. package/static/assets/inter-greek-ext-700-normal-BoQ6DsYi.woff +0 -0
  46. package/static/assets/inter-greek-ext-700-normal-qfdV9bQt.woff2 +0 -0
  47. package/static/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  48. package/static/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  49. package/static/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  50. package/static/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  51. package/static/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  52. package/static/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  53. package/static/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
  54. package/static/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
  55. package/static/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
  56. package/static/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
  57. package/static/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
  58. package/static/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
  59. package/static/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
  60. package/static/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
  61. package/static/assets/inter-latin-ext-700-normal-Ca8adRJv.woff2 +0 -0
  62. package/static/assets/inter-latin-ext-700-normal-TidjK2hL.woff +0 -0
  63. package/static/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
  64. package/static/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
  65. package/static/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
  66. package/static/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
  67. package/static/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
  68. package/static/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
  69. package/static/assets/inter-vietnamese-700-normal-BZaoP0fm.woff +0 -0
  70. package/static/assets/inter-vietnamese-700-normal-DlLaEgI2.woff2 +0 -0
  71. package/static/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  72. package/static/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  73. package/static/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
  74. package/static/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
  75. package/static/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
  76. package/static/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
  77. package/static/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  78. package/static/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  79. package/static/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
  80. package/static/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
  81. package/static/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
  82. package/static/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
  83. package/static/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  84. package/static/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  85. package/static/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  86. package/static/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  87. package/static/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
  88. package/static/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
  89. package/static/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  90. package/static/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  91. package/static/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
  92. package/static/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
  93. package/static/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
  94. package/static/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
  95. package/static/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  96. package/static/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
  97. package/static/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
  98. package/static/index.html +2 -5
  99. package/static/assets/index-EVWfLxoq.css +0 -1
@@ -1,9 +1,18 @@
1
1
  const db = require('../database');
2
+ const modelCache = require('../utils/model-cache');
3
+
4
+ function queryById(id) {
5
+ return db.prepare('SELECT id, username, name, role, status, created_at, updated_at FROM users WHERE id = ?').get(id);
6
+ }
2
7
 
3
8
  const UserModel = {
4
9
  // 根据 ID 查询用户
5
10
  findById(id) {
6
- return db.prepare('SELECT id, username, name, role, status, created_at, updated_at FROM users WHERE id = ?').get(id);
11
+ return modelCache.remember(
12
+ modelCache.keys.userBasic(id),
13
+ () => queryById(id),
14
+ modelCache.refs.user
15
+ );
7
16
  },
8
17
 
9
18
  // 根据用户名查询(含 password_hash,用于登录验证)
@@ -14,7 +23,8 @@ const UserModel = {
14
23
  // 创建用户
15
24
  create(username, passwordHash, role = 'developer') {
16
25
  const result = db.prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)').run(username, passwordHash, role);
17
- return this.findById(result.lastInsertRowid);
26
+ modelCache.invalidateUser(result.lastInsertRowid);
27
+ return queryById(result.lastInsertRowid);
18
28
  },
19
29
 
20
30
  // 列出用户(支持分页和搜索)
@@ -51,6 +61,9 @@ const UserModel = {
51
61
  const result = db.prepare(
52
62
  "UPDATE users SET username = ?, updated_at = datetime('now') WHERE id = ?"
53
63
  ).run(username, id);
64
+ if (result.changes > 0) {
65
+ modelCache.invalidateUser(id);
66
+ }
54
67
  return result.changes > 0;
55
68
  },
56
69
 
@@ -59,6 +72,9 @@ const UserModel = {
59
72
  const result = db.prepare(
60
73
  "UPDATE users SET password_hash = ?, updated_at = datetime('now') WHERE id = ?"
61
74
  ).run(passwordHash, id);
75
+ if (result.changes > 0) {
76
+ modelCache.invalidateUser(id);
77
+ }
62
78
  return result.changes > 0;
63
79
  },
64
80
 
@@ -81,6 +97,9 @@ const UserModel = {
81
97
  params.push(id);
82
98
 
83
99
  const result = db.prepare(`UPDATE users SET ${sets.join(', ')} WHERE id = ?`).run(...params);
100
+ if (result.changes > 0) {
101
+ modelCache.invalidateUser(id);
102
+ }
84
103
  return result.changes > 0;
85
104
  },
86
105
 
@@ -89,6 +108,9 @@ const UserModel = {
89
108
  const result = db.prepare(
90
109
  "UPDATE users SET password_hash = ?, updated_at = datetime('now') WHERE id = ?"
91
110
  ).run(passwordHash, id);
111
+ if (result.changes > 0) {
112
+ modelCache.invalidateUser(id);
113
+ }
92
114
  return result.changes > 0;
93
115
  },
94
116
 
@@ -123,6 +145,9 @@ const UserModel = {
123
145
  params.push(id);
124
146
 
125
147
  const result = db.prepare(`UPDATE users SET ${sets.join(', ')} WHERE id = ?`).run(...params);
148
+ if (result.changes > 0) {
149
+ modelCache.invalidateUser(id);
150
+ }
126
151
  return result.changes > 0;
127
152
  }
128
153
  };
@@ -1,4 +1,5 @@
1
1
  const db = require('../database');
2
+ const modelCache = require('../utils/model-cache');
2
3
 
3
4
  const VersionModel = {
4
5
  // 创建新版本
@@ -7,6 +8,7 @@ const VersionModel = {
7
8
  INSERT INTO skill_versions (skill_id, version, changelog, zip_path, uploader_id, description)
8
9
  VALUES (?, ?, ?, ?, ?, ?)
9
10
  `).run(skillId, version, changelog || '', zipPath, uploaderId, description || '');
11
+ modelCache.invalidateSkill(skillId);
10
12
  return this.findById(result.lastInsertRowid);
11
13
  },
12
14
 
@@ -22,44 +24,60 @@ const VersionModel = {
22
24
 
23
25
  // 根据 skill_id 和 version 查询
24
26
  findByVersion(skillId, version) {
25
- return db.prepare(`
26
- SELECT sv.*, u.username as uploader_username, u.name as uploader_name
27
- FROM skill_versions sv
28
- LEFT JOIN users u ON sv.uploader_id = u.id
29
- WHERE sv.skill_id = ? AND sv.version = ?
30
- `).get(skillId, version);
27
+ return modelCache.remember(
28
+ modelCache.keys.skillVersion(skillId, version),
29
+ () => db.prepare(`
30
+ SELECT sv.*, u.username as uploader_username, u.name as uploader_name
31
+ FROM skill_versions sv
32
+ LEFT JOIN users u ON sv.uploader_id = u.id
33
+ WHERE sv.skill_id = ? AND sv.version = ?
34
+ `).get(skillId, version),
35
+ modelCache.refs.version
36
+ );
31
37
  },
32
38
 
33
39
  // 列出某 Skill 的所有版本(按创建时间倒序)
34
40
  listBySkillId(skillId) {
35
- return db.prepare(`
36
- SELECT sv.*, u.username as uploader_username, u.name as uploader_name
37
- FROM skill_versions sv
38
- LEFT JOIN users u ON sv.uploader_id = u.id
39
- WHERE sv.skill_id = ?
40
- ORDER BY sv.created_at DESC
41
- `).all(skillId);
41
+ return modelCache.remember(
42
+ modelCache.keys.skillVersions(skillId),
43
+ () => db.prepare(`
44
+ SELECT sv.*, u.username as uploader_username, u.name as uploader_name
45
+ FROM skill_versions sv
46
+ LEFT JOIN users u ON sv.uploader_id = u.id
47
+ WHERE sv.skill_id = ?
48
+ ORDER BY sv.created_at DESC, sv.id DESC
49
+ `).all(skillId),
50
+ (versions) => modelCache.refs.versionList(skillId, versions)
51
+ );
42
52
  },
43
53
 
44
54
  // 获取某 Skill 的最新版本
45
55
  getLatest(skillId) {
46
- return db.prepare(`
47
- SELECT sv.*, u.username as uploader_username, u.name as uploader_name
48
- FROM skill_versions sv
49
- LEFT JOIN users u ON sv.uploader_id = u.id
50
- WHERE sv.skill_id = ?
51
- ORDER BY sv.created_at DESC
52
- LIMIT 1
53
- `).get(skillId);
56
+ return modelCache.remember(
57
+ modelCache.keys.skillLatest(skillId),
58
+ () => db.prepare(`
59
+ SELECT sv.*, u.username as uploader_username, u.name as uploader_name
60
+ FROM skill_versions sv
61
+ LEFT JOIN users u ON sv.uploader_id = u.id
62
+ WHERE sv.skill_id = ?
63
+ ORDER BY sv.created_at DESC, sv.id DESC
64
+ LIMIT 1
65
+ `).get(skillId),
66
+ modelCache.refs.version
67
+ );
54
68
  },
55
69
 
56
70
  // 更新版本描述和更新日志
57
71
  update(id, description, changelog) {
72
+ const existing = this.findById(id);
58
73
  db.prepare(`
59
74
  UPDATE skill_versions
60
75
  SET description = ?, changelog = ?
61
76
  WHERE id = ?
62
77
  `).run(description, changelog, id);
78
+ if (existing) {
79
+ modelCache.invalidateSkill(existing.skill_id);
80
+ }
63
81
  return this.findById(id);
64
82
  }
65
83
  };
@@ -1,6 +1,7 @@
1
1
  const db = require('../database');
2
2
  const { canManageSkill } = require('../utils/permission');
3
3
  const UserModel = require('../models/user');
4
+ const { invalidateSkill } = require('../utils/model-cache');
4
5
 
5
6
  async function collaboratorsRoutes(fastify, options) {
6
7
 
@@ -84,6 +85,7 @@ async function collaboratorsRoutes(fastify, options) {
84
85
  const result = db.prepare(
85
86
  'INSERT INTO skill_collaborators (skill_id, user_id, role, created_by) VALUES (?, ?, ?, ?)'
86
87
  ).run(skill_id, targetUser.id, 'collaborator', request.user.id);
88
+ invalidateSkill(skill_id);
87
89
 
88
90
  return reply.code(201).send({
89
91
  ok: true,
@@ -129,6 +131,7 @@ async function collaboratorsRoutes(fastify, options) {
129
131
 
130
132
  db.prepare('DELETE FROM skill_collaborators WHERE skill_id = ? AND user_id = ?')
131
133
  .run(skill_id, parseInt(user_id));
134
+ invalidateSkill(skill_id);
132
135
 
133
136
  return reply.send({ ok: true, message: 'Collaborator removed' });
134
137
  });
@@ -192,6 +195,7 @@ async function collaboratorsRoutes(fastify, options) {
192
195
  });
193
196
 
194
197
  transferTx();
198
+ invalidateSkill(skill_id);
195
199
 
196
200
  return reply.send({
197
201
  ok: true,
@@ -239,6 +243,7 @@ async function collaboratorsRoutes(fastify, options) {
239
243
  });
240
244
 
241
245
  deleteSkillTx();
246
+ invalidateSkill(skill_id);
242
247
 
243
248
  // 删除文件系统中的文件
244
249
  const fs = require('fs');
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const db = require('../database');
3
3
  const SkillModel = require('../models/skill');
4
4
  const VersionModel = require('../models/version');
5
+ const { invalidateSkill } = require('../utils/model-cache');
5
6
  const { ensureSkillDir, generateVersionNumber, getZipPath, getZipRelativePath } = require('../utils/zip');
6
7
  const { canPublishSkill } = require('../utils/permission');
7
8
 
@@ -96,6 +97,7 @@ async function publishRoutes(fastify, options) {
96
97
 
97
98
  // 更新 skill 的最新版本
98
99
  SkillModel.updateLatestVersion(skill_id, version);
100
+ invalidateSkill(skill_id);
99
101
 
100
102
  return {
101
103
  ok: true,
@@ -0,0 +1,160 @@
1
+ class LruCache {
2
+ constructor({ maxBytes = 50 * 1024 * 1024, defaultTtlMs = 0 } = {}) {
3
+ this.maxBytes = Number.isFinite(maxBytes) ? Math.max(0, Math.floor(maxBytes)) : 0;
4
+ this.defaultTtlMs = Number.isFinite(defaultTtlMs) ? Math.max(0, defaultTtlMs) : 0;
5
+ this.map = new Map();
6
+ this.totalBytes = 0;
7
+ this.hits = 0;
8
+ this.misses = 0;
9
+ this.evictions = 0;
10
+ }
11
+
12
+ static estimateSize(value) {
13
+ try {
14
+ const serialized = JSON.stringify(value);
15
+ return Buffer.byteLength(serialized === undefined ? 'null' : serialized, 'utf8');
16
+ } catch (error) {
17
+ return 0;
18
+ }
19
+ }
20
+
21
+ has(key) {
22
+ const entry = this.map.get(key);
23
+ if (!entry) {
24
+ return false;
25
+ }
26
+
27
+ if (this.#isExpired(entry)) {
28
+ this.#deleteEntry(key, entry);
29
+ return false;
30
+ }
31
+
32
+ return true;
33
+ }
34
+
35
+ get(key) {
36
+ const entry = this.map.get(key);
37
+ if (!entry) {
38
+ this.misses += 1;
39
+ return undefined;
40
+ }
41
+
42
+ if (this.#isExpired(entry)) {
43
+ this.#deleteEntry(key, entry);
44
+ this.misses += 1;
45
+ return undefined;
46
+ }
47
+
48
+ this.map.delete(key);
49
+ this.map.set(key, entry);
50
+ this.hits += 1;
51
+ return entry.value;
52
+ }
53
+
54
+ set(key, value, options = {}) {
55
+ if (this.maxBytes <= 0) {
56
+ return false;
57
+ }
58
+
59
+ const size = Number.isFinite(options.size) ? Math.max(0, Math.floor(options.size)) : LruCache.estimateSize(value);
60
+ if (size > this.maxBytes) {
61
+ this.delete(key);
62
+ return false;
63
+ }
64
+
65
+ const ttlMs = Number.isFinite(options.ttlMs) ? Math.max(0, options.ttlMs) : this.defaultTtlMs;
66
+ const expiresAt = ttlMs > 0 ? Date.now() + ttlMs : null;
67
+ const entry = {
68
+ value,
69
+ size,
70
+ refs: new Set(options.refs || []),
71
+ expiresAt
72
+ };
73
+
74
+ const existing = this.map.get(key);
75
+ if (existing) {
76
+ this.#deleteEntry(key, existing);
77
+ }
78
+
79
+ this.map.set(key, entry);
80
+ this.totalBytes += size;
81
+ this.#evictIfNeeded();
82
+ return true;
83
+ }
84
+
85
+ delete(key) {
86
+ const entry = this.map.get(key);
87
+ if (!entry) {
88
+ return false;
89
+ }
90
+
91
+ this.#deleteEntry(key, entry);
92
+ return true;
93
+ }
94
+
95
+ clear() {
96
+ this.map.clear();
97
+ this.totalBytes = 0;
98
+ }
99
+
100
+ clearByPrefix(prefix) {
101
+ const keys = [];
102
+ for (const key of this.map.keys()) {
103
+ if (key.startsWith(prefix)) {
104
+ keys.push(key);
105
+ }
106
+ }
107
+
108
+ for (const key of keys) {
109
+ this.delete(key);
110
+ }
111
+
112
+ return keys.length;
113
+ }
114
+
115
+ clearByRef(ref) {
116
+ const keys = [];
117
+ for (const [key, entry] of this.map.entries()) {
118
+ if (entry.refs.has(ref)) {
119
+ keys.push(key);
120
+ }
121
+ }
122
+
123
+ for (const key of keys) {
124
+ this.delete(key);
125
+ }
126
+
127
+ return keys.length;
128
+ }
129
+
130
+ stats() {
131
+ return {
132
+ entries: this.map.size,
133
+ totalBytes: this.totalBytes,
134
+ maxBytes: this.maxBytes,
135
+ hits: this.hits,
136
+ misses: this.misses,
137
+ evictions: this.evictions
138
+ };
139
+ }
140
+
141
+ #isExpired(entry) {
142
+ return entry.expiresAt !== null && Date.now() > entry.expiresAt;
143
+ }
144
+
145
+ #deleteEntry(key, entry) {
146
+ this.map.delete(key);
147
+ this.totalBytes = Math.max(0, this.totalBytes - entry.size);
148
+ }
149
+
150
+ #evictIfNeeded() {
151
+ while (this.totalBytes > this.maxBytes && this.map.size > 0) {
152
+ const oldestKey = this.map.keys().next().value;
153
+ const oldestEntry = this.map.get(oldestKey);
154
+ this.#deleteEntry(oldestKey, oldestEntry);
155
+ this.evictions += 1;
156
+ }
157
+ }
158
+ }
159
+
160
+ module.exports = LruCache;
@@ -0,0 +1,158 @@
1
+ const LruCache = require('./lru-cache');
2
+
3
+ const DEFAULT_CACHE_MAX_MB = 50;
4
+
5
+ function parseCacheMaxBytes() {
6
+ const raw = process.env.CACHE_MAX_MB;
7
+ if (raw === undefined || raw === null || raw === '') {
8
+ return DEFAULT_CACHE_MAX_MB * 1024 * 1024;
9
+ }
10
+
11
+ const parsed = Number(raw);
12
+ if (!Number.isFinite(parsed) || parsed < 0) {
13
+ return DEFAULT_CACHE_MAX_MB * 1024 * 1024;
14
+ }
15
+
16
+ return Math.floor(parsed * 1024 * 1024);
17
+ }
18
+
19
+ const cache = new LruCache({ maxBytes: parseCacheMaxBytes() });
20
+
21
+ function uniqueRefs(refs) {
22
+ return Array.from(new Set((refs || []).filter(Boolean)));
23
+ }
24
+
25
+ function remember(key, loader, refsBuilder, options = {}) {
26
+ const cachedValue = cache.get(key);
27
+ if (cachedValue !== undefined) {
28
+ return cachedValue;
29
+ }
30
+
31
+ const value = loader();
32
+ if (value === undefined) {
33
+ return value;
34
+ }
35
+
36
+ const refs = typeof refsBuilder === 'function' ? refsBuilder(value) : refsBuilder;
37
+ cache.set(key, value, {
38
+ refs: uniqueRefs(refs),
39
+ ttlMs: options.ttlMs
40
+ });
41
+ return value;
42
+ }
43
+
44
+ function skillKey(skillId) {
45
+ return `skill:${skillId}`;
46
+ }
47
+
48
+ function skillSearchKey(query) {
49
+ return `skill-search:${query || ''}`;
50
+ }
51
+
52
+ function skillExistsKey(skillId) {
53
+ return `skill-exists:${skillId}`;
54
+ }
55
+
56
+ function skillVersionsKey(skillId) {
57
+ return `skill-versions:${skillId}`;
58
+ }
59
+
60
+ function skillVersionKey(skillId, version) {
61
+ return `skill-version:${skillId}:${version}`;
62
+ }
63
+
64
+ function skillLatestKey(skillId) {
65
+ return `skill-latest:${skillId}`;
66
+ }
67
+
68
+ function userBasicKey(userId) {
69
+ return `user-basic:${userId}`;
70
+ }
71
+
72
+ function skillRefs(skill) {
73
+ if (!skill) {
74
+ return [];
75
+ }
76
+ return [`skill:${skill.id}`, `user:${skill.owner_id}`];
77
+ }
78
+
79
+ function skillSearchRefs(skills) {
80
+ const refs = ['collection:skill-search'];
81
+ for (const skill of skills || []) {
82
+ refs.push(...skillRefs(skill));
83
+ }
84
+ return refs;
85
+ }
86
+
87
+ function versionRefs(version) {
88
+ if (!version) {
89
+ return [];
90
+ }
91
+ return [
92
+ `skill:${version.skill_id}`,
93
+ `version:${version.skill_id}:${version.version}`,
94
+ `user:${version.uploader_id}`
95
+ ];
96
+ }
97
+
98
+ function versionListRefs(skillId, versions) {
99
+ const refs = [`skill:${skillId}`];
100
+ for (const version of versions || []) {
101
+ refs.push(...versionRefs(version));
102
+ }
103
+ return refs;
104
+ }
105
+
106
+ function userRefs(user) {
107
+ if (!user) {
108
+ return [];
109
+ }
110
+ return [`user:${user.id}`];
111
+ }
112
+
113
+ function invalidateSkill(skillId) {
114
+ cache.delete(skillKey(skillId));
115
+ cache.delete(skillExistsKey(skillId));
116
+ cache.delete(skillVersionsKey(skillId));
117
+ cache.delete(skillLatestKey(skillId));
118
+ cache.clearByPrefix(`skill-version:${skillId}:`);
119
+ cache.clearByRef(`skill:${skillId}`);
120
+ cache.clearByRef('collection:skill-search');
121
+ }
122
+
123
+ function invalidateUser(userId) {
124
+ cache.delete(userBasicKey(userId));
125
+ cache.clearByRef(`user:${userId}`);
126
+ }
127
+
128
+ function invalidateAllSkillSearches() {
129
+ cache.clearByRef('collection:skill-search');
130
+ }
131
+
132
+ function getStats() {
133
+ return cache.stats();
134
+ }
135
+
136
+ module.exports = {
137
+ remember,
138
+ getStats,
139
+ invalidateSkill,
140
+ invalidateUser,
141
+ invalidateAllSkillSearches,
142
+ keys: {
143
+ skill: skillKey,
144
+ skillSearch: skillSearchKey,
145
+ skillExists: skillExistsKey,
146
+ skillVersions: skillVersionsKey,
147
+ skillVersion: skillVersionKey,
148
+ skillLatest: skillLatestKey,
149
+ userBasic: userBasicKey
150
+ },
151
+ refs: {
152
+ skill: skillRefs,
153
+ skillSearch: skillSearchRefs,
154
+ version: versionRefs,
155
+ versionList: versionListRefs,
156
+ user: userRefs
157
+ }
158
+ };