@kaikybrofc/omnizap-system 2.2.4 → 2.2.6

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 (55) hide show
  1. package/.env.example +5 -0
  2. package/.prettierrc +16 -0
  3. package/README.md +13 -13
  4. package/app/modules/stickerPackModule/autoPackCollectorService.js +63 -8
  5. package/app/modules/stickerPackModule/catalogHandlers/catalogAdminHttp.js +68 -0
  6. package/app/modules/stickerPackModule/catalogHandlers/catalogAuthHttp.js +34 -0
  7. package/app/modules/stickerPackModule/catalogHandlers/catalogPublicHttp.js +179 -0
  8. package/app/modules/stickerPackModule/catalogHandlers/catalogUploadHttp.js +92 -0
  9. package/app/modules/stickerPackModule/catalogRouter.js +79 -0
  10. package/app/modules/stickerPackModule/domainEventOutboxRepository.js +243 -0
  11. package/app/modules/stickerPackModule/domainEvents.js +61 -0
  12. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +21 -0
  13. package/app/modules/stickerPackModule/stickerAssetRepository.js +19 -0
  14. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +55 -15
  15. package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +238 -0
  16. package/app/modules/stickerPackModule/stickerDomainEventBus.js +71 -0
  17. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +198 -0
  18. package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
  19. package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +570 -536
  20. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +44 -0
  21. package/app/modules/stickerPackModule/stickerPackItemRepository.js +18 -0
  22. package/app/modules/stickerPackModule/stickerPackRepository.js +51 -0
  23. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +191 -0
  24. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +301 -0
  25. package/app/modules/stickerPackModule/stickerStorageService.js +111 -10
  26. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +21 -0
  27. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +59 -7
  28. package/app/observability/metrics.js +169 -0
  29. package/app/services/featureFlagService.js +137 -0
  30. package/database/index.js +5 -0
  31. package/database/migrations/20260228_0022_sticker_scale_indexes.sql +16 -0
  32. package/database/migrations/20260228_0023_sticker_pack_score_snapshot.sql +25 -0
  33. package/database/migrations/20260228_0024_domain_event_outbox.sql +42 -0
  34. package/database/migrations/20260228_0025_sticker_worker_task_idempotency_dlq.sql +23 -0
  35. package/database/migrations/20260228_0026_feature_flags.sql +21 -0
  36. package/ecosystem.prod.config.cjs +70 -9
  37. package/index.js +26 -0
  38. package/kaikybrofc-omnizap-system-2.2.6.tgz +0 -0
  39. package/observability/sticker-catalog-slo.md +83 -0
  40. package/observability/sticker-scale-hardening-rollout.md +128 -0
  41. package/package.json +7 -35
  42. package/public/assets/images/brand-icon-192.png +0 -0
  43. package/public/assets/images/brand-logo-128.webp +0 -0
  44. package/public/assets/images/hero-banner-1280.avif +0 -0
  45. package/public/assets/images/hero-banner-1280.jpg +0 -0
  46. package/public/assets/images/hero-banner-1280.webp +0 -0
  47. package/public/assets/images/hero-banner-720.avif +0 -0
  48. package/public/assets/images/hero-banner-720.webp +0 -0
  49. package/public/index.html +120 -18
  50. package/public/js/apps/homeApp.js +469 -353
  51. package/public/robots.txt +9 -0
  52. package/public/sitemap.xml +28 -0
  53. package/scripts/sticker-catalog-loadtest.mjs +208 -0
  54. package/scripts/sticker-worker-task.mjs +122 -0
  55. package/observability/mysql-exporter.cnf +0 -5
@@ -1,4 +1,6 @@
1
1
  import { executeQuery, TABLES } from '../../../database/index.js';
2
+ import { STICKER_DOMAIN_EVENTS } from './domainEvents.js';
3
+ import { publishStickerDomainEvent } from './stickerDomainEventBus.js';
2
4
 
3
5
  const normalizeEngagementRow = (row) => ({
4
6
  pack_id: row?.pack_id || null,
@@ -70,6 +72,20 @@ export async function incrementStickerPackOpen(packId, connection = null) {
70
72
  connection,
71
73
  );
72
74
 
75
+ await publishStickerDomainEvent(
76
+ {
77
+ eventType: STICKER_DOMAIN_EVENTS.ENGAGEMENT_RECORDED,
78
+ aggregateType: 'sticker_pack',
79
+ aggregateId: packId,
80
+ payload: {
81
+ action: 'open',
82
+ pack_id: packId,
83
+ },
84
+ priority: 55,
85
+ },
86
+ { connection },
87
+ );
88
+
73
89
  return getStickerPackEngagementByPackId(packId, connection);
74
90
  }
75
91
 
@@ -87,6 +103,20 @@ export async function incrementStickerPackLike(packId, connection = null) {
87
103
  connection,
88
104
  );
89
105
 
106
+ await publishStickerDomainEvent(
107
+ {
108
+ eventType: STICKER_DOMAIN_EVENTS.ENGAGEMENT_RECORDED,
109
+ aggregateType: 'sticker_pack',
110
+ aggregateId: packId,
111
+ payload: {
112
+ action: 'like',
113
+ pack_id: packId,
114
+ },
115
+ priority: 55,
116
+ },
117
+ { connection },
118
+ );
119
+
90
120
  return getStickerPackEngagementByPackId(packId, connection);
91
121
  }
92
122
 
@@ -104,5 +134,19 @@ export async function incrementStickerPackDislike(packId, connection = null) {
104
134
  connection,
105
135
  );
106
136
 
137
+ await publishStickerDomainEvent(
138
+ {
139
+ eventType: STICKER_DOMAIN_EVENTS.ENGAGEMENT_RECORDED,
140
+ aggregateType: 'sticker_pack',
141
+ aggregateId: packId,
142
+ payload: {
143
+ action: 'dislike',
144
+ pack_id: packId,
145
+ },
146
+ priority: 55,
147
+ },
148
+ { connection },
149
+ );
150
+
107
151
  return getStickerPackEngagementByPackId(packId, connection);
108
152
  }
@@ -207,6 +207,24 @@ export async function countStickerPackItemRefsByStickerId(stickerId, connection
207
207
  return Number(rows?.[0]?.total || 0);
208
208
  }
209
209
 
210
+ /**
211
+ * Lista IDs de packs que referenciam um sticker.
212
+ *
213
+ * @param {string} stickerId ID do sticker/asset.
214
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null]
215
+ * @returns {Promise<string[]>}
216
+ */
217
+ export async function listPackIdsByStickerId(stickerId, connection = null) {
218
+ const rows = await executeQuery(
219
+ `SELECT DISTINCT pack_id
220
+ FROM ${TABLES.STICKER_PACK_ITEM}
221
+ WHERE sticker_id = ?`,
222
+ [stickerId],
223
+ connection,
224
+ );
225
+ return (Array.isArray(rows) ? rows : []).map((row) => String(row?.pack_id || '')).filter(Boolean);
226
+ }
227
+
210
228
  /**
211
229
  * Obtém a maior posição atualmente usada no pack.
212
230
  *
@@ -1,4 +1,6 @@
1
1
  import { executeQuery, TABLES } from '../../../database/index.js';
2
+ import { STICKER_DOMAIN_EVENTS } from './domainEvents.js';
3
+ import { publishStickerDomainEvent } from './stickerDomainEventBus.js';
2
4
 
3
5
  const CATALOG_COMPLETE_PACK_TARGET = Math.max(1, Number(process.env.STICKER_PACK_MAX_ITEMS) || 30);
4
6
 
@@ -337,6 +339,24 @@ export async function createStickerPack(pack, connection = null) {
337
339
  connection,
338
340
  );
339
341
 
342
+ await publishStickerDomainEvent(
343
+ {
344
+ eventType: STICKER_DOMAIN_EVENTS.PACK_UPDATED,
345
+ aggregateType: 'sticker_pack',
346
+ aggregateId: pack.id,
347
+ payload: {
348
+ action: 'created',
349
+ pack_id: pack.id,
350
+ pack_key: pack.pack_key,
351
+ owner_jid: pack.owner_jid,
352
+ visibility: pack.visibility,
353
+ },
354
+ priority: 75,
355
+ idempotencyKey: `pack_created:${pack.id}`,
356
+ },
357
+ { connection },
358
+ );
359
+
340
360
  return findStickerPackById(pack.id, { includeDeleted: true, connection });
341
361
  }
342
362
 
@@ -389,6 +409,22 @@ export async function updateStickerPackFields(packId, fields, connection = null)
389
409
  connection,
390
410
  );
391
411
 
412
+ await publishStickerDomainEvent(
413
+ {
414
+ eventType: STICKER_DOMAIN_EVENTS.PACK_UPDATED,
415
+ aggregateType: 'sticker_pack',
416
+ aggregateId: packId,
417
+ payload: {
418
+ action: 'updated',
419
+ pack_id: packId,
420
+ fields: Object.keys(fields || {}).slice(0, 30),
421
+ },
422
+ priority: 70,
423
+ idempotencyKey: `pack_updated:${packId}:${Object.keys(fields || {}).sort().join(',')}`,
424
+ },
425
+ { connection },
426
+ );
427
+
392
428
  return findStickerPackById(packId, { includeDeleted: true, connection });
393
429
  }
394
430
 
@@ -438,5 +474,20 @@ export async function bumpStickerPackVersion(packId, connection = null) {
438
474
  connection,
439
475
  );
440
476
 
477
+ await publishStickerDomainEvent(
478
+ {
479
+ eventType: STICKER_DOMAIN_EVENTS.PACK_UPDATED,
480
+ aggregateType: 'sticker_pack',
481
+ aggregateId: packId,
482
+ payload: {
483
+ action: 'version_bumped',
484
+ pack_id: packId,
485
+ },
486
+ priority: 65,
487
+ idempotencyKey: `pack_version_bumped:${packId}`,
488
+ },
489
+ { connection },
490
+ );
491
+
441
492
  return findStickerPackById(packId, { includeDeleted: true, connection });
442
493
  }
@@ -0,0 +1,191 @@
1
+ import { executeQuery, TABLES } from '../../../database/index.js';
2
+
3
+ const parseJson = (value, fallback = null) => {
4
+ if (value === null || value === undefined) return fallback;
5
+ if (typeof value === 'object') return value;
6
+ if (Buffer.isBuffer(value)) {
7
+ try {
8
+ return JSON.parse(value.toString('utf8'));
9
+ } catch {
10
+ return fallback;
11
+ }
12
+ }
13
+ if (typeof value === 'string') {
14
+ try {
15
+ return JSON.parse(value);
16
+ } catch {
17
+ return fallback;
18
+ }
19
+ }
20
+ return fallback;
21
+ };
22
+
23
+ const clampScore = (value) => {
24
+ const numeric = Number(value);
25
+ if (!Number.isFinite(numeric)) return 0;
26
+ return Number(numeric.toFixed(6));
27
+ };
28
+
29
+ const normalizeNsfwLevel = (value) => {
30
+ const normalized = String(value || '').trim().toLowerCase();
31
+ if (['safe', 'suggestive', 'explicit'].includes(normalized)) return normalized;
32
+ return 'safe';
33
+ };
34
+
35
+ const normalizeRow = (row) => {
36
+ if (!row) return null;
37
+ const scoresFromJson = parseJson(row.scores_json, {}) || {};
38
+ return {
39
+ pack_id: row.pack_id,
40
+ ranking_score: clampScore(row.ranking_score),
41
+ pack_score: clampScore(row.pack_score),
42
+ trend_score: clampScore(row.trend_score),
43
+ quality_score: clampScore(row.quality_score),
44
+ engagement_score: clampScore(row.engagement_score),
45
+ diversity_score: clampScore(row.diversity_score),
46
+ cohesion_score: clampScore(row.cohesion_score),
47
+ sensitive_content: row.sensitive_content === 1 || row.sensitive_content === true,
48
+ nsfw_level: normalizeNsfwLevel(row.nsfw_level),
49
+ sticker_count: Number(row.sticker_count || 0),
50
+ tags: Array.isArray(parseJson(row.tags, [])) ? parseJson(row.tags, []) : [],
51
+ source_version: row.source_version || 'v1',
52
+ refreshed_at: row.refreshed_at || null,
53
+ updated_at: row.updated_at || null,
54
+ signals: {
55
+ ...scoresFromJson,
56
+ quality_score: clampScore(row.quality_score),
57
+ engagement_score: clampScore(row.engagement_score),
58
+ diversity_score: clampScore(row.diversity_score),
59
+ cohesion_score: clampScore(row.cohesion_score),
60
+ trend_score: clampScore(row.trend_score),
61
+ pack_score: clampScore(row.pack_score),
62
+ ranking_score: clampScore(row.ranking_score),
63
+ nsfw_level: normalizeNsfwLevel(row.nsfw_level),
64
+ sensitive_content: row.sensitive_content === 1 || row.sensitive_content === true,
65
+ },
66
+ };
67
+ };
68
+
69
+ const normalizeSnapshotInput = (entry) => {
70
+ if (!entry?.pack_id) return null;
71
+ const signals = entry?.signals && typeof entry.signals === 'object' ? entry.signals : {};
72
+ return {
73
+ pack_id: String(entry.pack_id),
74
+ ranking_score: clampScore(signals.ranking_score),
75
+ pack_score: clampScore(signals.pack_score),
76
+ trend_score: clampScore(signals.trend_score),
77
+ quality_score: clampScore(signals.quality_score),
78
+ engagement_score: clampScore(signals.engagement_score),
79
+ diversity_score: clampScore(signals.diversity_score),
80
+ cohesion_score: clampScore(signals.cohesion_score),
81
+ sensitive_content: signals.sensitive_content === true || signals.sensitive_content === 1 ? 1 : 0,
82
+ nsfw_level: normalizeNsfwLevel(signals.nsfw_level),
83
+ sticker_count: Math.max(0, Number(entry.sticker_count || 0)),
84
+ tags: Array.isArray(entry.tags) ? entry.tags.slice(0, 30) : [],
85
+ source_version: String(entry.source_version || 'v1').trim().slice(0, 32) || 'v1',
86
+ scores_json: signals,
87
+ };
88
+ };
89
+
90
+ export async function upsertStickerPackScoreSnapshots(entries = [], connection = null) {
91
+ const normalized = (Array.isArray(entries) ? entries : [])
92
+ .map((entry) => normalizeSnapshotInput(entry))
93
+ .filter(Boolean);
94
+ if (!normalized.length) return 0;
95
+
96
+ let written = 0;
97
+ for (let offset = 0; offset < normalized.length; offset += 100) {
98
+ const chunk = normalized.slice(offset, offset + 100);
99
+ const placeholders = chunk.map(() => '(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP())').join(', ');
100
+ const params = chunk.flatMap((entry) => [
101
+ entry.pack_id,
102
+ entry.ranking_score,
103
+ entry.pack_score,
104
+ entry.trend_score,
105
+ entry.quality_score,
106
+ entry.engagement_score,
107
+ entry.diversity_score,
108
+ entry.cohesion_score,
109
+ entry.sensitive_content,
110
+ entry.nsfw_level,
111
+ entry.sticker_count,
112
+ JSON.stringify(entry.tags || []),
113
+ JSON.stringify(entry.scores_json || {}),
114
+ entry.source_version,
115
+ ]);
116
+ const result = await executeQuery(
117
+ `INSERT INTO ${TABLES.STICKER_PACK_SCORE_SNAPSHOT}
118
+ (
119
+ pack_id,
120
+ ranking_score,
121
+ pack_score,
122
+ trend_score,
123
+ quality_score,
124
+ engagement_score,
125
+ diversity_score,
126
+ cohesion_score,
127
+ sensitive_content,
128
+ nsfw_level,
129
+ sticker_count,
130
+ tags,
131
+ scores_json,
132
+ source_version,
133
+ refreshed_at
134
+ )
135
+ VALUES ${placeholders}
136
+ ON DUPLICATE KEY UPDATE
137
+ ranking_score = VALUES(ranking_score),
138
+ pack_score = VALUES(pack_score),
139
+ trend_score = VALUES(trend_score),
140
+ quality_score = VALUES(quality_score),
141
+ engagement_score = VALUES(engagement_score),
142
+ diversity_score = VALUES(diversity_score),
143
+ cohesion_score = VALUES(cohesion_score),
144
+ sensitive_content = VALUES(sensitive_content),
145
+ nsfw_level = VALUES(nsfw_level),
146
+ sticker_count = VALUES(sticker_count),
147
+ tags = VALUES(tags),
148
+ scores_json = VALUES(scores_json),
149
+ source_version = VALUES(source_version),
150
+ refreshed_at = UTC_TIMESTAMP(),
151
+ updated_at = CURRENT_TIMESTAMP`,
152
+ params,
153
+ connection,
154
+ );
155
+ written += Number(result?.affectedRows || 0);
156
+ }
157
+
158
+ return written;
159
+ }
160
+
161
+ export async function listStickerPackScoreSnapshotsByPackIds(packIds = [], connection = null) {
162
+ const ids = Array.from(new Set((Array.isArray(packIds) ? packIds : []).filter(Boolean)));
163
+ if (!ids.length) return new Map();
164
+ const placeholders = ids.map(() => '?').join(', ');
165
+ const rows = await executeQuery(
166
+ `SELECT *
167
+ FROM ${TABLES.STICKER_PACK_SCORE_SNAPSHOT}
168
+ WHERE pack_id IN (${placeholders})`,
169
+ ids,
170
+ connection,
171
+ );
172
+ const byPackId = new Map();
173
+ (Array.isArray(rows) ? rows : []).forEach((row) => {
174
+ const normalized = normalizeRow(row);
175
+ if (!normalized?.pack_id) return;
176
+ byPackId.set(normalized.pack_id, normalized);
177
+ });
178
+ return byPackId;
179
+ }
180
+
181
+ export async function removeSnapshotsForDeletedPacks(connection = null) {
182
+ const result = await executeQuery(
183
+ `DELETE s
184
+ FROM ${TABLES.STICKER_PACK_SCORE_SNAPSHOT} s
185
+ LEFT JOIN ${TABLES.STICKER_PACK} p ON p.id = s.pack_id
186
+ WHERE p.id IS NULL OR p.deleted_at IS NOT NULL`,
187
+ [],
188
+ connection,
189
+ );
190
+ return Number(result?.affectedRows || 0);
191
+ }
@@ -0,0 +1,301 @@
1
+ import { executeQuery, TABLES } from '../../../database/index.js';
2
+ import logger from '../../utils/logger/loggerModule.js';
3
+ import {
4
+ getEmptyStickerPackEngagement,
5
+ getStickerPackEngagementByPackId,
6
+ } from './stickerPackEngagementRepository.js';
7
+ import { listStickerPackItems } from './stickerPackItemRepository.js';
8
+ import { listStickerClassificationsByAssetIds } from './stickerAssetClassificationRepository.js';
9
+ import { getPackClassificationSummaryByAssetIds } from './stickerClassificationService.js';
10
+ import { listStickerPackInteractionStatsByPackIds } from './stickerPackInteractionEventRepository.js';
11
+ import { getMarketplaceDriftSnapshot } from './stickerMarketplaceDriftService.js';
12
+ import { computePackSignals } from './stickerPackMarketplaceService.js';
13
+ import {
14
+ removeSnapshotsForDeletedPacks,
15
+ upsertStickerPackScoreSnapshots,
16
+ } from './stickerPackScoreSnapshotRepository.js';
17
+ import { setQueueDepth } from '../../observability/metrics.js';
18
+
19
+ const parseEnvBool = (value, fallback) => {
20
+ if (value === undefined || value === null || value === '') return fallback;
21
+ const normalized = String(value).trim().toLowerCase();
22
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
23
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
24
+ return fallback;
25
+ };
26
+
27
+ const SNAPSHOT_ENABLED = parseEnvBool(process.env.STICKER_SCORE_SNAPSHOT_ENABLED, true);
28
+ const SNAPSHOT_STARTUP_DELAY_MS = Math.max(
29
+ 1_000,
30
+ Number(process.env.STICKER_SCORE_SNAPSHOT_STARTUP_DELAY_MS) || 20_000,
31
+ );
32
+ const SNAPSHOT_REFRESH_INTERVAL_MS = Math.max(
33
+ 30_000,
34
+ Number(process.env.STICKER_SCORE_SNAPSHOT_REFRESH_INTERVAL_MS) || 5 * 60_000,
35
+ );
36
+ const SNAPSHOT_BATCH_SIZE = Math.max(
37
+ 10,
38
+ Math.min(500, Number(process.env.STICKER_SCORE_SNAPSHOT_BATCH_SIZE) || 120),
39
+ );
40
+ const SNAPSHOT_TARGETED_BATCH_SIZE = Math.max(
41
+ 5,
42
+ Math.min(200, Number(process.env.STICKER_SCORE_SNAPSHOT_TARGETED_BATCH_SIZE) || 60),
43
+ );
44
+ const SNAPSHOT_SOURCE_VERSION = String(process.env.STICKER_SCORE_SNAPSHOT_SOURCE_VERSION || 'v1').trim() || 'v1';
45
+ const SNAPSHOT_MAX_PENDING_PACKS = Math.max(
46
+ 20,
47
+ Math.min(20_000, Number(process.env.STICKER_SCORE_SNAPSHOT_MAX_PENDING_PACKS) || 2_000),
48
+ );
49
+ const SNAPSHOT_FULL_REBUILD_EVERY_CYCLES = Math.max(
50
+ 1,
51
+ Math.min(500, Number(process.env.STICKER_SCORE_SNAPSHOT_FULL_REBUILD_EVERY_CYCLES) || 12),
52
+ );
53
+
54
+ let startupHandle = null;
55
+ let cycleHandle = null;
56
+ let running = false;
57
+ let cycleCounter = 0;
58
+ const pendingPackIds = new Set();
59
+
60
+ const listPublishedCatalogPacks = async ({ limit = SNAPSHOT_BATCH_SIZE, offset = 0 } = {}) => {
61
+ const safeLimit = Math.max(1, Math.min(500, Number(limit) || SNAPSHOT_BATCH_SIZE));
62
+ const safeOffset = Math.max(0, Number(offset) || 0);
63
+ const rows = await executeQuery(
64
+ `SELECT id, owner_jid, name, publisher, description, pack_key, cover_sticker_id, visibility, status, pack_status, is_auto_pack, updated_at, created_at
65
+ FROM ${TABLES.STICKER_PACK}
66
+ WHERE deleted_at IS NULL
67
+ AND status = 'published'
68
+ AND COALESCE(pack_status, 'ready') = 'ready'
69
+ AND visibility IN ('public', 'unlisted')
70
+ ORDER BY updated_at DESC, id DESC
71
+ LIMIT ${safeLimit} OFFSET ${safeOffset}`,
72
+ [],
73
+ );
74
+ return Array.isArray(rows) ? rows : [];
75
+ };
76
+
77
+ const listPacksByIdsForSnapshot = async (packIds = []) => {
78
+ const ids = Array.from(new Set((Array.isArray(packIds) ? packIds : []).filter(Boolean)));
79
+ if (!ids.length) return [];
80
+ const placeholders = ids.map(() => '?').join(', ');
81
+ const rows = await executeQuery(
82
+ `SELECT id, owner_jid, name, publisher, description, pack_key, cover_sticker_id, visibility, status, pack_status, is_auto_pack, updated_at, created_at
83
+ FROM ${TABLES.STICKER_PACK}
84
+ WHERE id IN (${placeholders})
85
+ AND deleted_at IS NULL
86
+ AND status = 'published'
87
+ AND COALESCE(pack_status, 'ready') = 'ready'
88
+ AND visibility IN ('public', 'unlisted')`,
89
+ ids,
90
+ );
91
+ return Array.isArray(rows) ? rows : [];
92
+ };
93
+
94
+ const buildSnapshotForPack = async ({ pack, driftWeights }) => {
95
+ const items = await listStickerPackItems(pack.id);
96
+ const stickerIds = items.map((item) => item.sticker_id).filter(Boolean);
97
+ const [packClassification, itemClassifications, engagement, interactionStatsByPackId] = await Promise.all([
98
+ getPackClassificationSummaryByAssetIds(stickerIds),
99
+ stickerIds.length ? listStickerClassificationsByAssetIds(stickerIds) : Promise.resolve([]),
100
+ getStickerPackEngagementByPackId(pack.id),
101
+ listStickerPackInteractionStatsByPackIds([pack.id]),
102
+ ]);
103
+
104
+ const byAssetId = new Map((Array.isArray(itemClassifications) ? itemClassifications : []).map((entry) => [entry.asset_id, entry]));
105
+ const orderedClassifications = stickerIds.map((id) => byAssetId.get(id)).filter(Boolean);
106
+ const signals = computePackSignals({
107
+ pack: { ...pack, items },
108
+ engagement: engagement || getEmptyStickerPackEngagement(),
109
+ packClassification,
110
+ itemClassifications: orderedClassifications,
111
+ interactionStats: interactionStatsByPackId.get(pack.id) || null,
112
+ scoringWeights: driftWeights || null,
113
+ });
114
+
115
+ return {
116
+ pack_id: pack.id,
117
+ signals,
118
+ tags: Array.isArray(packClassification?.tags) ? packClassification.tags : [],
119
+ sticker_count: items.length,
120
+ source_version: SNAPSHOT_SOURCE_VERSION,
121
+ };
122
+ };
123
+
124
+ const rebuildSnapshotsForPacks = async (packs = []) => {
125
+ if (!packs.length) return { scanned: 0, written: 0 };
126
+ const driftSnapshot = await getMarketplaceDriftSnapshot();
127
+ const driftWeights = driftSnapshot?.weights || null;
128
+ const snapshots = [];
129
+ let scanned = 0;
130
+
131
+ for (const pack of packs) {
132
+ scanned += 1;
133
+ try {
134
+ const snapshot = await buildSnapshotForPack({ pack, driftWeights });
135
+ snapshots.push(snapshot);
136
+ } catch (error) {
137
+ logger.warn('Falha ao montar snapshot de score do pack.', {
138
+ action: 'sticker_pack_score_snapshot_build_failed',
139
+ pack_id: pack?.id || null,
140
+ error: error?.message,
141
+ });
142
+ }
143
+ }
144
+
145
+ const written = await upsertStickerPackScoreSnapshots(snapshots);
146
+ return { scanned, written };
147
+ };
148
+
149
+ const consumePendingPackIds = (limit = SNAPSHOT_TARGETED_BATCH_SIZE) => {
150
+ const consumed = [];
151
+ for (const packId of pendingPackIds) {
152
+ consumed.push(packId);
153
+ pendingPackIds.delete(packId);
154
+ if (consumed.length >= limit) break;
155
+ }
156
+ return consumed;
157
+ };
158
+
159
+ export const enqueuePackScoreSnapshotRefresh = (packIds = []) => {
160
+ const ids = Array.isArray(packIds) ? packIds : [packIds];
161
+ ids.forEach((packId) => {
162
+ const normalized = String(packId || '').trim();
163
+ if (!normalized) return;
164
+ if (pendingPackIds.size >= SNAPSHOT_MAX_PENDING_PACKS) return;
165
+ pendingPackIds.add(normalized);
166
+ });
167
+ setQueueDepth('sticker_pack_score_snapshot_pending', pendingPackIds.size);
168
+ };
169
+
170
+ export const runStickerPackScoreSnapshotCycle = async () => {
171
+ if (!SNAPSHOT_ENABLED) {
172
+ return {
173
+ executed: false,
174
+ reason: 'disabled',
175
+ pending_pack_ids: pendingPackIds.size,
176
+ };
177
+ }
178
+ if (running) {
179
+ return {
180
+ executed: false,
181
+ reason: 'already_running',
182
+ pending_pack_ids: pendingPackIds.size,
183
+ };
184
+ }
185
+
186
+ running = true;
187
+ const startedAt = Date.now();
188
+ let scanned = 0;
189
+ let written = 0;
190
+ let fullRebuildExecuted = false;
191
+
192
+ try {
193
+ const targetedPackIds = consumePendingPackIds(SNAPSHOT_TARGETED_BATCH_SIZE);
194
+ if (targetedPackIds.length) {
195
+ const targetedPacks = await listPacksByIdsForSnapshot(targetedPackIds);
196
+ const targetedResult = await rebuildSnapshotsForPacks(targetedPacks);
197
+ scanned += targetedResult.scanned;
198
+ written += targetedResult.written;
199
+ }
200
+
201
+ cycleCounter += 1;
202
+ fullRebuildExecuted =
203
+ cycleCounter % SNAPSHOT_FULL_REBUILD_EVERY_CYCLES === 0 || targetedPackIds.length === 0;
204
+
205
+ if (fullRebuildExecuted) {
206
+ let offset = 0;
207
+ while (true) {
208
+ const packs = await listPublishedCatalogPacks({
209
+ limit: SNAPSHOT_BATCH_SIZE,
210
+ offset,
211
+ });
212
+ if (!packs.length) break;
213
+ const result = await rebuildSnapshotsForPacks(packs);
214
+ scanned += result.scanned;
215
+ written += result.written;
216
+ offset += packs.length;
217
+ if (packs.length < SNAPSHOT_BATCH_SIZE) break;
218
+ }
219
+ }
220
+
221
+ const removed = await removeSnapshotsForDeletedPacks().catch(() => 0);
222
+ setQueueDepth('sticker_pack_score_snapshot_pending', pendingPackIds.size);
223
+
224
+ logger.info('Ciclo de snapshot de score de packs finalizado.', {
225
+ action: 'sticker_pack_score_snapshot_cycle',
226
+ scanned,
227
+ written,
228
+ removed,
229
+ pending_pack_ids: pendingPackIds.size,
230
+ full_rebuild_executed: fullRebuildExecuted,
231
+ full_rebuild_every_cycles: SNAPSHOT_FULL_REBUILD_EVERY_CYCLES,
232
+ duration_ms: Date.now() - startedAt,
233
+ batch_size: SNAPSHOT_BATCH_SIZE,
234
+ targeted_batch_size: SNAPSHOT_TARGETED_BATCH_SIZE,
235
+ });
236
+
237
+ return {
238
+ executed: true,
239
+ reason: 'ok',
240
+ scanned,
241
+ written,
242
+ removed,
243
+ pending_pack_ids: pendingPackIds.size,
244
+ full_rebuild_executed: fullRebuildExecuted,
245
+ duration_ms: Date.now() - startedAt,
246
+ };
247
+ } catch (error) {
248
+ logger.error('Falha no ciclo de snapshot de score de packs.', {
249
+ action: 'sticker_pack_score_snapshot_cycle_failed',
250
+ error: error?.message,
251
+ });
252
+ return {
253
+ executed: true,
254
+ reason: 'failed',
255
+ scanned,
256
+ written,
257
+ pending_pack_ids: pendingPackIds.size,
258
+ full_rebuild_executed: fullRebuildExecuted,
259
+ duration_ms: Date.now() - startedAt,
260
+ };
261
+ } finally {
262
+ running = false;
263
+ }
264
+ };
265
+
266
+ const scheduleNextCycle = () => {
267
+ if (!SNAPSHOT_ENABLED) return;
268
+ if (cycleHandle) {
269
+ clearTimeout(cycleHandle);
270
+ cycleHandle = null;
271
+ }
272
+ cycleHandle = setTimeout(() => {
273
+ cycleHandle = null;
274
+ void runStickerPackScoreSnapshotCycle().finally(() => {
275
+ scheduleNextCycle();
276
+ });
277
+ }, SNAPSHOT_REFRESH_INTERVAL_MS);
278
+ if (typeof cycleHandle?.unref === 'function') cycleHandle.unref();
279
+ };
280
+
281
+ export const startStickerPackScoreSnapshotRuntime = () => {
282
+ if (!SNAPSHOT_ENABLED) return;
283
+ if (startupHandle || cycleHandle) return;
284
+ startupHandle = setTimeout(() => {
285
+ startupHandle = null;
286
+ void runStickerPackScoreSnapshotCycle();
287
+ scheduleNextCycle();
288
+ }, SNAPSHOT_STARTUP_DELAY_MS);
289
+ if (typeof startupHandle?.unref === 'function') startupHandle.unref();
290
+ };
291
+
292
+ export const stopStickerPackScoreSnapshotRuntime = () => {
293
+ if (startupHandle) {
294
+ clearTimeout(startupHandle);
295
+ startupHandle = null;
296
+ }
297
+ if (cycleHandle) {
298
+ clearTimeout(cycleHandle);
299
+ cycleHandle = null;
300
+ }
301
+ };