@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
@@ -0,0 +1,9 @@
1
+ User-agent: *
2
+ Allow: /
3
+
4
+ # Pastas e arquivos internos que nao precisam aparecer em buscas
5
+ Disallow: /data/
6
+ Disallow: /app/
7
+ Disallow: /.env
8
+
9
+ Sitemap: https://omnizap.shop/sitemap.xml
@@ -0,0 +1,28 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
+ <url>
4
+ <loc>https://omnizap.shop/</loc>
5
+ <changefreq>daily</changefreq>
6
+ <priority>1.0</priority>
7
+ </url>
8
+ <url>
9
+ <loc>https://omnizap.shop/stickers/</loc>
10
+ <changefreq>hourly</changefreq>
11
+ <priority>0.9</priority>
12
+ </url>
13
+ <url>
14
+ <loc>https://omnizap.shop/api-docs/</loc>
15
+ <changefreq>weekly</changefreq>
16
+ <priority>0.8</priority>
17
+ </url>
18
+ <url>
19
+ <loc>https://omnizap.shop/termos-de-uso/</loc>
20
+ <changefreq>monthly</changefreq>
21
+ <priority>0.5</priority>
22
+ </url>
23
+ <url>
24
+ <loc>https://omnizap.shop/licenca/</loc>
25
+ <changefreq>monthly</changefreq>
26
+ <priority>0.5</priority>
27
+ </url>
28
+ </urlset>
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+ import http from 'node:http';
3
+ import https from 'node:https';
4
+ import { performance } from 'node:perf_hooks';
5
+ import fs from 'node:fs/promises';
6
+ import { URL } from 'node:url';
7
+
8
+ const DEFAULT_BASE_URL = process.env.STICKER_LOADTEST_BASE_URL || 'http://127.0.0.1:9102';
9
+ const DEFAULT_PATHS = [
10
+ '/api/sticker-packs?limit=24&offset=0&sort=popular',
11
+ '/api/sticker-packs/stats',
12
+ '/api/sticker-packs/creators?limit=25',
13
+ ];
14
+ const DEFAULT_DURATION_SECONDS = 30;
15
+ const DEFAULT_CONCURRENCY = 20;
16
+ const DEFAULT_TIMEOUT_MS = 12_000;
17
+ const DEFAULT_HTTP_SLO_MS = Number(process.env.HTTP_SLO_TARGET_MS || 750);
18
+
19
+ const parseCliArgs = (argv) => {
20
+ const args = new Map();
21
+ for (let i = 0; i < argv.length; i += 1) {
22
+ const token = argv[i];
23
+ if (!token.startsWith('--')) continue;
24
+ const next = argv[i + 1];
25
+ if (!next || next.startsWith('--')) {
26
+ args.set(token, true);
27
+ continue;
28
+ }
29
+ args.set(token, next);
30
+ i += 1;
31
+ }
32
+ return args;
33
+ };
34
+
35
+ const args = parseCliArgs(process.argv.slice(2));
36
+ const baseUrl = String(args.get('--base-url') || DEFAULT_BASE_URL).trim().replace(/\/+$/, '');
37
+ const durationSeconds = Math.max(5, Number(args.get('--duration-seconds') || DEFAULT_DURATION_SECONDS));
38
+ const concurrency = Math.max(1, Number(args.get('--concurrency') || DEFAULT_CONCURRENCY));
39
+ const timeoutMs = Math.max(1000, Number(args.get('--timeout-ms') || DEFAULT_TIMEOUT_MS));
40
+ const outFile = String(args.get('--out') || '').trim();
41
+ const sloMs = Math.max(50, Number(args.get('--slo-ms') || DEFAULT_HTTP_SLO_MS));
42
+ const paths = String(args.get('--paths') || '')
43
+ .split(',')
44
+ .map((entry) => entry.trim())
45
+ .filter(Boolean);
46
+ const requestPaths = paths.length ? paths : DEFAULT_PATHS;
47
+
48
+ const url = new URL(baseUrl);
49
+ const isHttps = url.protocol === 'https:';
50
+ const requestClient = isHttps ? https : http;
51
+
52
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
53
+
54
+ const quantile = (sortedValues, q) => {
55
+ if (!sortedValues.length) return 0;
56
+ const index = Math.max(0, Math.min(sortedValues.length - 1, Math.floor((sortedValues.length - 1) * q)));
57
+ return sortedValues[index];
58
+ };
59
+
60
+ const runRequest = (pathName) =>
61
+ new Promise((resolve) => {
62
+ const started = performance.now();
63
+ const req = requestClient.request(
64
+ {
65
+ protocol: url.protocol,
66
+ hostname: url.hostname,
67
+ port: url.port || (isHttps ? 443 : 80),
68
+ path: pathName,
69
+ method: 'GET',
70
+ timeout: timeoutMs,
71
+ headers: {
72
+ Connection: 'keep-alive',
73
+ Accept: 'application/json',
74
+ 'X-Viewer-Key': 'loadtest',
75
+ 'X-Session-Key': 'loadtest',
76
+ 'User-Agent': 'omnizap-sticker-loadtest/1.0',
77
+ },
78
+ },
79
+ (res) => {
80
+ res.resume();
81
+ res.on('end', () => {
82
+ const durationMs = performance.now() - started;
83
+ resolve({
84
+ ok: res.statusCode >= 200 && res.statusCode < 500,
85
+ statusCode: Number(res.statusCode || 0),
86
+ durationMs,
87
+ pathName,
88
+ });
89
+ });
90
+ },
91
+ );
92
+
93
+ req.on('timeout', () => {
94
+ req.destroy(new Error('timeout'));
95
+ });
96
+ req.on('error', (error) => {
97
+ const durationMs = performance.now() - started;
98
+ resolve({
99
+ ok: false,
100
+ statusCode: 0,
101
+ durationMs,
102
+ pathName,
103
+ error: error?.message || 'request_error',
104
+ });
105
+ });
106
+ req.end();
107
+ });
108
+
109
+ const runWorker = async ({ deadlineMs, workerIndex, stats }) => {
110
+ let requestIndex = workerIndex % requestPaths.length;
111
+ while (Date.now() < deadlineMs) {
112
+ const pathName = requestPaths[requestIndex % requestPaths.length];
113
+ requestIndex += 1;
114
+
115
+ const result = await runRequest(pathName);
116
+ stats.total += 1;
117
+ stats.latencies.push(result.durationMs);
118
+ stats.byStatus.set(result.statusCode, Number(stats.byStatus.get(result.statusCode) || 0) + 1);
119
+ if (result.ok) {
120
+ stats.success += 1;
121
+ } else {
122
+ stats.errors += 1;
123
+ stats.lastErrors.push({
124
+ path: pathName,
125
+ status_code: result.statusCode,
126
+ error: result.error || null,
127
+ });
128
+ if (stats.lastErrors.length > 10) stats.lastErrors.shift();
129
+ }
130
+ }
131
+ };
132
+
133
+ const main = async () => {
134
+ const startedAt = Date.now();
135
+ const deadlineMs = startedAt + durationSeconds * 1000;
136
+ const stats = {
137
+ total: 0,
138
+ success: 0,
139
+ errors: 0,
140
+ latencies: [],
141
+ byStatus: new Map(),
142
+ lastErrors: [],
143
+ };
144
+
145
+ console.log(`[loadtest] base_url=${baseUrl}`);
146
+ console.log(`[loadtest] duration_seconds=${durationSeconds} concurrency=${concurrency} paths=${requestPaths.join(',')}`);
147
+ await sleep(250);
148
+
149
+ await Promise.all(Array.from({ length: concurrency }).map((_, index) => runWorker({
150
+ deadlineMs,
151
+ workerIndex: index,
152
+ stats,
153
+ })));
154
+
155
+ const elapsedSeconds = Math.max(0.001, (Date.now() - startedAt) / 1000);
156
+ const sortedLatencies = [...stats.latencies].sort((a, b) => a - b);
157
+ const p50 = quantile(sortedLatencies, 0.5);
158
+ const p90 = quantile(sortedLatencies, 0.9);
159
+ const p95 = quantile(sortedLatencies, 0.95);
160
+ const p99 = quantile(sortedLatencies, 0.99);
161
+ const throughputRps = stats.total / elapsedSeconds;
162
+ const errorRate = stats.total > 0 ? stats.errors / stats.total : 1;
163
+
164
+ const summary = {
165
+ started_at: new Date(startedAt).toISOString(),
166
+ ended_at: new Date().toISOString(),
167
+ base_url: baseUrl,
168
+ duration_seconds: Number(elapsedSeconds.toFixed(3)),
169
+ concurrency,
170
+ paths: requestPaths,
171
+ requests_total: stats.total,
172
+ requests_success: stats.success,
173
+ requests_error: stats.errors,
174
+ error_rate: Number(errorRate.toFixed(6)),
175
+ throughput_rps: Number(throughputRps.toFixed(3)),
176
+ latency_ms: {
177
+ p50: Number(p50.toFixed(2)),
178
+ p90: Number(p90.toFixed(2)),
179
+ p95: Number(p95.toFixed(2)),
180
+ p99: Number(p99.toFixed(2)),
181
+ max: Number((sortedLatencies[sortedLatencies.length - 1] || 0).toFixed(2)),
182
+ },
183
+ by_status: Object.fromEntries(stats.byStatus.entries()),
184
+ slo: {
185
+ target_ms: sloMs,
186
+ p95_within_target: p95 <= sloMs,
187
+ },
188
+ sample_errors: stats.lastErrors,
189
+ };
190
+
191
+ console.log(JSON.stringify(summary, null, 2));
192
+ if (outFile) {
193
+ await fs.writeFile(outFile, `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
194
+ console.log(`[loadtest] report_written=${outFile}`);
195
+ }
196
+
197
+ if (summary.requests_total <= 0) {
198
+ process.exitCode = 2;
199
+ return;
200
+ }
201
+ if (summary.slo.p95_within_target && summary.error_rate <= 0.02) return;
202
+ process.exitCode = 1;
203
+ };
204
+
205
+ main().catch((error) => {
206
+ console.error('[loadtest] fatal_error', error?.message || error);
207
+ process.exitCode = 2;
208
+ });
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+
4
+ import logger from '../app/utils/logger/loggerModule.js';
5
+ import initializeDatabase from '../database/init.js';
6
+ import { closePool } from '../database/index.js';
7
+ import {
8
+ isSupportedStickerWorkerTaskType,
9
+ startDedicatedStickerWorker,
10
+ } from '../app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js';
11
+
12
+ const parseCliArgs = (argv = []) => {
13
+ const args = new Map();
14
+ for (let index = 0; index < argv.length; index += 1) {
15
+ const token = argv[index];
16
+ if (!token.startsWith('--')) continue;
17
+ const next = argv[index + 1];
18
+ if (!next || next.startsWith('--')) {
19
+ args.set(token, true);
20
+ continue;
21
+ }
22
+ args.set(token, next);
23
+ index += 1;
24
+ }
25
+ return args;
26
+ };
27
+
28
+ const args = parseCliArgs(process.argv.slice(2));
29
+ const workerTaskType = String(args.get('--task-type') || process.env.STICKER_WORKER_TASK_TYPE || '')
30
+ .trim()
31
+ .toLowerCase();
32
+
33
+ if (!isSupportedStickerWorkerTaskType(workerTaskType)) {
34
+ logger.error('Tipo de task inválido para worker dedicado.', {
35
+ action: 'sticker_worker_task_invalid_type',
36
+ task_type: workerTaskType || null,
37
+ });
38
+ process.exit(1);
39
+ }
40
+
41
+ let shuttingDown = false;
42
+ let workerHandle = null;
43
+
44
+ const shutdown = async (signal, error = null) => {
45
+ if (shuttingDown) return;
46
+ shuttingDown = true;
47
+
48
+ logger.warn('Encerrando worker dedicado de sticker.', {
49
+ action: 'sticker_worker_task_shutdown',
50
+ signal,
51
+ task_type: workerTaskType,
52
+ error: error?.message || null,
53
+ });
54
+
55
+ try {
56
+ workerHandle?.stop?.();
57
+ } catch (stopError) {
58
+ logger.warn('Falha ao encerrar loop do worker dedicado.', {
59
+ action: 'sticker_worker_task_stop_failed',
60
+ task_type: workerTaskType,
61
+ error: stopError?.message,
62
+ });
63
+ }
64
+
65
+ try {
66
+ await closePool();
67
+ } catch (poolError) {
68
+ logger.warn('Falha ao encerrar pool MySQL no worker dedicado.', {
69
+ action: 'sticker_worker_task_pool_close_failed',
70
+ task_type: workerTaskType,
71
+ error: poolError?.message,
72
+ });
73
+ }
74
+
75
+ process.exit(error ? 1 : 0);
76
+ };
77
+
78
+ process.on('SIGINT', () => {
79
+ void shutdown('SIGINT');
80
+ });
81
+
82
+ process.on('SIGTERM', () => {
83
+ void shutdown('SIGTERM');
84
+ });
85
+
86
+ process.on('uncaughtException', (error) => {
87
+ logger.error('Exceção não capturada no worker dedicado de sticker.', {
88
+ action: 'sticker_worker_task_uncaught_exception',
89
+ task_type: workerTaskType,
90
+ error: error?.message,
91
+ stack: error?.stack,
92
+ });
93
+ void shutdown('uncaughtException', error);
94
+ });
95
+
96
+ process.on('unhandledRejection', (reason) => {
97
+ const message = reason instanceof Error ? reason.message : String(reason || '');
98
+ logger.error('Promise rejeitada sem tratamento no worker dedicado de sticker.', {
99
+ action: 'sticker_worker_task_unhandled_rejection',
100
+ task_type: workerTaskType,
101
+ error: message,
102
+ });
103
+ void shutdown('unhandledRejection', reason instanceof Error ? reason : new Error(message));
104
+ });
105
+
106
+ const start = async () => {
107
+ await initializeDatabase();
108
+ workerHandle = startDedicatedStickerWorker({
109
+ taskType: workerTaskType,
110
+ label: `process:${process.pid}`,
111
+ });
112
+ };
113
+
114
+ start().catch((error) => {
115
+ logger.error('Falha ao inicializar worker dedicado de sticker.', {
116
+ action: 'sticker_worker_task_start_failed',
117
+ task_type: workerTaskType,
118
+ error: error?.message,
119
+ stack: error?.stack,
120
+ });
121
+ void shutdown('startup_error', error);
122
+ });
@@ -1,5 +0,0 @@
1
- [client]
2
- host=host.docker.internal
3
- port=3306
4
- user=exporter
5
- password=exporter