vimp-engine 0.35.0 → 0.35.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vimp-engine",
3
- "version": "0.35.0",
3
+ "version": "0.35.1",
4
4
  "description": "VIMP — движок-приложение (мастер, P2P-транспорт, Worker-хост, мета, MVC-каркас клиента)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -210,6 +210,7 @@ export default class ClientReportsView {
210
210
  }
211
211
 
212
212
  details.appendChild(this._line(`User agent: ${report.userAgent ?? '—'}`));
213
+ details.appendChild(this._line(`Role: ${report.role ?? '—'}; page: ${report.page ?? '—'}`));
213
214
  details.appendChild(
214
215
  this._line(
215
216
  `First seen: ${report.firstSeen ?? '—'}; last seen: ${report.lastSeen ?? '—'}` +
@@ -6,8 +6,9 @@
6
6
  // Репортёр не должен стать новым источником падений и шума: каждая
7
7
  // публичная функция глушит свои исключения, повтор известной ошибки — только
8
8
  // счётчик, разных ошибок за сессию не больше maxKeysPerSession, отправка —
9
- // пачками с дебаунсом и дельтами счётчиков. console.* не перехватывается
10
- // осознанно: там шум и случайные данные в аргументах.
9
+ // пачками с дельтами счётчиков: первая через 2 с, следующие не чаще раза в
10
+ // 10 с. console.* не перехватывается осознанно: там шум и случайные данные в
11
+ // аргументах.
11
12
 
12
13
  // обрезки — те же числа, что у приёма на боксе (stage_2.md, контракт)
13
14
  const MAX_MESSAGE = 500;
@@ -125,10 +126,13 @@ function isExtensionUrl(value) {
125
126
  // очередь переполнена — fetch с keepalive. Ответ не читается
126
127
  function defaultSend(url, json) {
127
128
  if (typeof navigator !== 'undefined' && navigator.sendBeacon) {
128
- const blob = new Blob([json], { type: 'application/json' });
129
-
130
- if (navigator.sendBeacon(url, blob)) {
131
- return;
129
+ try {
130
+ if (navigator.sendBeacon(url, new Blob([json], { type: 'application/json' }))) {
131
+ return;
132
+ }
133
+ } catch {
134
+ // старые Chromium бросали SecurityError на Blob с application/json
135
+ // (crbug.com/490015) — тогда запасной fetch
132
136
  }
133
137
  }
134
138
 
@@ -151,7 +155,9 @@ function noop() {}
151
155
  * @param {Function} [opts.send] - (url, json) => void; по умолчанию beacon/fetch
152
156
  * @param {Function} [opts.now]
153
157
  * @param {number} [opts.maxKeysPerSession=50]
154
- * @param {number} [opts.flushDelayMs=2000]
158
+ * @param {number} [opts.flushDelayMs=2000] - задержка первой отправки
159
+ * @param {number} [opts.minIntervalMs=10000] - интервал между отправками по
160
+ * таймеру; прямой flush() его не ждёт
155
161
  * @returns {{ capture, warn, flush, setContext, install }}
156
162
  */
157
163
  export function createDiagnostics({
@@ -161,6 +167,7 @@ export function createDiagnostics({
161
167
  now = Date.now,
162
168
  maxKeysPerSession = 50,
163
169
  flushDelayMs = 2000,
170
+ minIntervalMs = 10000,
164
171
  } = {}) {
165
172
  if (!url) {
166
173
  return {
@@ -181,6 +188,7 @@ export function createDiagnostics({
181
188
 
182
189
  let capWarned = false;
183
190
  let timer = null;
191
+ let lastSentAt = null;
184
192
 
185
193
  // ошибка внутри самого репортёра не должна порождать новый отчёт
186
194
  let busy = false;
@@ -220,9 +228,16 @@ export function createDiagnostics({
220
228
  }
221
229
 
222
230
  function schedule() {
223
- if (timer === null) {
224
- timer = setTimeout(flush, flushDelayMs);
231
+ if (timer !== null) {
232
+ return;
225
233
  }
234
+
235
+ // первая отправка — через flushDelayMs, следующие — не чаще
236
+ // minIntervalMs: бокс режет приём лимитом запросов с адреса, и 429
237
+ // молча съедал бы приросты счётчиков непрерывно повторяющейся ошибки
238
+ const sinceLast = lastSentAt === null ? Infinity : now() - lastSentAt;
239
+
240
+ timer = setTimeout(flush, Math.max(flushDelayMs, minIntervalMs - sinceLast));
226
241
  }
227
242
 
228
243
  function guarded(fn) {
@@ -371,14 +386,25 @@ export function createDiagnostics({
371
386
  }
372
387
  }
373
388
 
389
+ if (items.length > 0) {
390
+ lastSentAt = now();
391
+ }
392
+
374
393
  for (let i = 0; i < items.length; i += MAX_BATCH_ITEMS) {
375
394
  sendBatch(items.slice(i, i + MAX_BATCH_ITEMS));
376
395
  }
377
396
  });
378
397
  }
379
398
 
399
+ // смена контекста (игра, роль): накопленное уходит СО СВОИМ контекстом —
400
+ // иначе отчёт игры A, отправленный уже под игрой B, получил бы на боксе
401
+ // отпечаток и source maps игры B
380
402
  function setContext(patch) {
381
403
  try {
404
+ if (Object.keys(patch).some(key => ctx[key] !== patch[key])) {
405
+ flush();
406
+ }
407
+
382
408
  Object.assign(ctx, patch);
383
409
  } catch {
384
410
  // контекст — вспомогательный, его сбой отчёты не останавливает
@@ -20,6 +20,24 @@ const SWAP_QUEUE_LIMIT = 2000;
20
20
  // предел ожидания ответа Worker'а на отладочный запрос (dump/запись)
21
21
  const DEBUG_TIMEOUT = 5000;
22
22
 
23
+ // ErrorEvent Worker'а в главном потоке: `error` там null (исключение между
24
+ // потоками не клонируется) — есть только message и filename/lineno/colno.
25
+ // Кадр собирается из них: без него у отчёта нет верхнего кадра, то есть ни
26
+ // различимого отпечатка, ни расшифровки по source map. Пустой message
27
+ // (Worker не загрузился) бокс отбросил бы — отсюда запасной текст
28
+ function workerErrorReport(event) {
29
+ if (typeof event?.error?.stack === 'string') {
30
+ return event.error;
31
+ }
32
+
33
+ const message = event?.message || 'Worker error';
34
+ const stack = event?.filename
35
+ ? `${message}\n at ${event.filename}:${event.lineno ?? 0}:${event.colno ?? 0}`
36
+ : null;
37
+
38
+ return { message, stack };
39
+ }
40
+
23
41
  export default class HostController {
24
42
  /**
25
43
  * @param {Object} room - настройки комнаты (имя/карта/лимит/таймеры).
@@ -85,7 +103,7 @@ export default class HostController {
85
103
  // браузера остаётся как есть
86
104
  _watchWorkerErrors(worker) {
87
105
  worker.onerror = event =>
88
- this._diagnostics?.capture(event.error ?? event.message, {
106
+ this._diagnostics?.capture(workerErrorReport(event), {
89
107
  source: 'host-worker',
90
108
  kind: 'worker',
91
109
  });
@@ -342,27 +360,20 @@ export default class HostController {
342
360
  if (msg.type === 'ready') {
343
361
  this._finishSwap();
344
362
  } else if (msg.type === 'error') {
345
- this._reportWorkerError(msg);
363
+ this._reportWorkerMessage(msg, 'error');
346
364
  this._abortSwap(msg.message);
347
365
  } else if (msg.type === 'diagnostic') {
348
- this._reportWorkerDiagnostic(msg);
366
+ this._reportWorkerMessage(msg, msg.kind);
349
367
  }
350
368
  }
351
369
 
352
- // сбой init Worker'а — в журнал клиентских ошибок (plan/client-reports)
353
- _reportWorkerError(msg) {
354
- this._diagnostics?.capture(new Error(msg.message), {
355
- source: 'host-worker',
356
- kind: 'error',
357
- });
358
- }
359
-
360
- // необработанный reject внутри Worker'а (host.worker.js) — только в
361
- // журнал: откат эстафеты завязан на 'error', не на него
362
- _reportWorkerDiagnostic(msg) {
370
+ // сбой Worker'а, присланный сообщением ('error' — провал init,
371
+ // 'diagnostic' — необработанный reject): стек — самого Worker'а, а не
372
+ // главного потока
373
+ _reportWorkerMessage(msg, kind) {
363
374
  this._diagnostics?.capture(
364
- { message: msg.message, stack: msg.stack },
365
- { source: 'host-worker', kind: msg.kind },
375
+ { message: msg.message, stack: msg.stack ?? null },
376
+ { source: 'host-worker', kind },
366
377
  );
367
378
  }
368
379
 
@@ -426,12 +437,12 @@ export default class HostController {
426
437
  break;
427
438
 
428
439
  case 'error':
429
- this._reportWorkerError(msg);
440
+ this._reportWorkerMessage(msg, 'error');
430
441
  this._onError?.(msg);
431
442
  break;
432
443
 
433
444
  case 'diagnostic':
434
- this._reportWorkerDiagnostic(msg);
445
+ this._reportWorkerMessage(msg, msg.kind);
435
446
  break;
436
447
 
437
448
  case 'map_changed':
@@ -175,7 +175,9 @@ export default {
175
175
  // известных отпечатков бюджет не тратят
176
176
  newFingerprintsPerMinute: 60,
177
177
  // ключ — адрес, для IPv6 — подсеть /64 (lib/clientIp.js → rateLimitKey)
178
- rateLimit: { limit: 10, windowMs: 60000 },
178
+ // клиент шлёт не чаще раза в 10 с — запас на несколько вкладок за одним
179
+ // адресом (NAT); главная защита — бюджет новых отпечатков
180
+ rateLimit: { limit: 30, windowMs: 60000 },
179
181
  bodyLimit: '16kb',
180
182
  maxItemsPerRequest: 10,
181
183
  },
@@ -178,6 +178,9 @@ self.onmessage = async event => {
178
178
  self.postMessage({
179
179
  type: 'error',
180
180
  message: e && e.message ? e.message : String(e),
181
+ // стек Worker'а — для журнала клиентских ошибок: в главном потоке его
182
+ // уже не восстановить
183
+ stack: e && typeof e.stack === 'string' ? e.stack : null,
181
184
  });
182
185
  }
183
186
  break;