@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.
- package/.env.example +5 -0
- package/.prettierrc +16 -0
- package/README.md +13 -13
- package/app/modules/stickerPackModule/autoPackCollectorService.js +63 -8
- package/app/modules/stickerPackModule/catalogHandlers/catalogAdminHttp.js +68 -0
- package/app/modules/stickerPackModule/catalogHandlers/catalogAuthHttp.js +34 -0
- package/app/modules/stickerPackModule/catalogHandlers/catalogPublicHttp.js +179 -0
- package/app/modules/stickerPackModule/catalogHandlers/catalogUploadHttp.js +92 -0
- package/app/modules/stickerPackModule/catalogRouter.js +79 -0
- package/app/modules/stickerPackModule/domainEventOutboxRepository.js +243 -0
- package/app/modules/stickerPackModule/domainEvents.js +61 -0
- package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +21 -0
- package/app/modules/stickerPackModule/stickerAssetRepository.js +19 -0
- package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +55 -15
- package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +238 -0
- package/app/modules/stickerPackModule/stickerDomainEventBus.js +71 -0
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +198 -0
- package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
- package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +570 -536
- package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +44 -0
- package/app/modules/stickerPackModule/stickerPackItemRepository.js +18 -0
- package/app/modules/stickerPackModule/stickerPackRepository.js +51 -0
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +191 -0
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +301 -0
- package/app/modules/stickerPackModule/stickerStorageService.js +111 -10
- package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +21 -0
- package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +59 -7
- package/app/observability/metrics.js +169 -0
- package/app/services/featureFlagService.js +137 -0
- package/database/index.js +5 -0
- package/database/migrations/20260228_0022_sticker_scale_indexes.sql +16 -0
- package/database/migrations/20260228_0023_sticker_pack_score_snapshot.sql +25 -0
- package/database/migrations/20260228_0024_domain_event_outbox.sql +42 -0
- package/database/migrations/20260228_0025_sticker_worker_task_idempotency_dlq.sql +23 -0
- package/database/migrations/20260228_0026_feature_flags.sql +21 -0
- package/ecosystem.prod.config.cjs +70 -9
- package/index.js +26 -0
- package/kaikybrofc-omnizap-system-2.2.6.tgz +0 -0
- package/observability/sticker-catalog-slo.md +83 -0
- package/observability/sticker-scale-hardening-rollout.md +128 -0
- package/package.json +7 -35
- package/public/assets/images/brand-icon-192.png +0 -0
- package/public/assets/images/brand-logo-128.webp +0 -0
- package/public/assets/images/hero-banner-1280.avif +0 -0
- package/public/assets/images/hero-banner-1280.jpg +0 -0
- package/public/assets/images/hero-banner-1280.webp +0 -0
- package/public/assets/images/hero-banner-720.avif +0 -0
- package/public/assets/images/hero-banner-720.webp +0 -0
- package/public/index.html +120 -18
- package/public/js/apps/homeApp.js +469 -353
- package/public/robots.txt +9 -0
- package/public/sitemap.xml +28 -0
- package/scripts/sticker-catalog-loadtest.mjs +208 -0
- package/scripts/sticker-worker-task.mjs +122 -0
- 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
|
+
};
|