cloudflare-next-intl 0.6.0 → 0.6.2

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.
@@ -8,11 +8,10 @@ import { type ReportErrorConfig } from './report_error';
8
8
  * own `console`). Only takes effect when `config.errorHandling.overrideConsoleError`
9
9
  * is `true`.
10
10
  *
11
- * Caps at `MAX_REPORTS_PER_INSTALL` (20) reports per install a component
12
- * stuck in a render-error loop calls `console.error` on every render, and
13
- * without a cap this would report (and, server-side, background via
14
- * `waitUntil`) unboundedly. Once the cap is hit, `console.error` still runs
15
- * normally, it just stops being reported.
11
+ * A component stuck in a render-error loop calls `console.error` on every
12
+ * render `reportError`'s own dedup/cap (on by default, see
13
+ * `errorHandling.dedup`/`maxReports`) is what stops that from reporting
14
+ * unboundedly; this function does not duplicate that cap itself.
16
15
  *
17
16
  * `config.errorHandling.ignoreConsoleErrors` (default
18
17
  * `defaultIgnoredConsoleErrors` — this package's own Firebase Auth error
@@ -1,7 +1,6 @@
1
1
  import reportError from './report_error';
2
2
  import stringifyUnknown from './stringify_unknown';
3
3
  import { defaultIgnoredConsoleErrors } from './default_ignored_console_errors';
4
- const MAX_REPORTS_PER_INSTALL = 20;
5
4
  /**
6
5
  * Replaces the global `console.error` so every `console.error(...)` call is
7
6
  * also routed through `config.errorHandling.onError`/`reportError` — the
@@ -11,11 +10,10 @@ const MAX_REPORTS_PER_INSTALL = 20;
11
10
  * own `console`). Only takes effect when `config.errorHandling.overrideConsoleError`
12
11
  * is `true`.
13
12
  *
14
- * Caps at `MAX_REPORTS_PER_INSTALL` (20) reports per install a component
15
- * stuck in a render-error loop calls `console.error` on every render, and
16
- * without a cap this would report (and, server-side, background via
17
- * `waitUntil`) unboundedly. Once the cap is hit, `console.error` still runs
18
- * normally, it just stops being reported.
13
+ * A component stuck in a render-error loop calls `console.error` on every
14
+ * render `reportError`'s own dedup/cap (on by default, see
15
+ * `errorHandling.dedup`/`maxReports`) is what stops that from reporting
16
+ * unboundedly; this function does not duplicate that cap itself.
19
17
  *
20
18
  * `config.errorHandling.ignoreConsoleErrors` (default
21
19
  * `defaultIgnoredConsoleErrors` — this package's own Firebase Auth error
@@ -36,18 +34,14 @@ export default function installConsoleErrorOverride(config, isClient) {
36
34
  if (console.error.__isErrorHandlingOverride)
37
35
  return;
38
36
  const originalConsoleError = console.error.bind(console);
39
- let reportCount = 0;
40
37
  const override = (message, ...optionalParams) => {
41
38
  originalConsoleError(message, ...optionalParams);
42
- if (reportCount >= MAX_REPORTS_PER_INSTALL)
43
- return;
44
39
  const stringified = stringifyUnknown(message, isClient);
45
40
  const ignoreList = config.errorHandling?.ignoreConsoleErrors ?? defaultIgnoredConsoleErrors;
46
41
  if (ignoreList.some((ignored) => stringified.includes(ignored)))
47
42
  return;
48
43
  if (config.errorHandling?.ignoreConsoleError?.(stringified))
49
44
  return;
50
- reportCount++;
51
45
  void reportError(config, { error: message, classOrMethodName: 'Global Console Error Handler', params: optionalParams, isClient });
52
46
  };
53
47
  override.__isErrorHandlingOverride = true;
@@ -6,10 +6,11 @@ export interface ReportErrorConfig {
6
6
  /**
7
7
  * Reports `params` via `config.errorHandling.onError` (default
8
8
  * `console.error(params.formattedMessage)`), unless
9
- * `config.errorHandling.enable === false` or `params.consent` is set and
10
- * not `true` (reporting to a third party without cookie consent can itself
11
- * be GDPR-relevant). Never throws a broken reporter must not mask the
12
- * original error.
9
+ * `config.errorHandling.enable === false`, `params.consent` is set and not
10
+ * `true` (reporting to a third party without cookie consent can itself be
11
+ * GDPR-relevant), or dedup throttles it (on by default see
12
+ * `errorHandling.dedup`/`throttleMs`/`resetDedup`). Never throws — a broken
13
+ * reporter must not mask the original error.
13
14
  *
14
15
  * Always overwrites `params.formattedMessage` with a fresh
15
16
  * `formatErrorMessage(params)` before reporting — a human-readable one-line
@@ -17,11 +18,6 @@ export interface ReportErrorConfig {
17
18
  * instead of the raw `error`/`params` object, for a default reporter (or a
18
19
  * simple `onError`) to print directly.
19
20
  *
20
- * No built-in dedup/throttling: this package has no per-request context to
21
- * safely scope such state to (module-scope state would leak across
22
- * concurrent requests in a long-lived server process). Do dedup/throttling
23
- * in your own `onError` if you need it, scoped to your own request context.
24
- *
25
21
  * When `config.generate?.getCloudflareContext` is set, `waitUntil` is called
26
22
  * SYNCHRONOUSLY, in the same tick, with the `callOnError(...)` promise —
27
23
  * Cloudflare Workers only extends the request's lifetime for work already
@@ -32,6 +28,12 @@ export interface ReportErrorConfig {
32
28
  * `onError` directly when `getCloudflareContext`/`ctx.waitUntil` is unset or
33
29
  * unavailable (e.g. outside a Cloudflare Worker).
34
30
  *
31
+ * Passing `params.error` as `null`/`undefined` with `errorHandling.resetDedup: true`
32
+ * and nothing else is a valid "reset-only" call: the dedup state clears and
33
+ * `reportError` returns immediately, without calling `onError` — useful to
34
+ * clear dedup state once at the very start of a request/cron tick, before
35
+ * any handler that might call `reportError` for a real error runs.
36
+ *
35
37
  * @param config Pass the relevant slices of your `RoutingConfig` directly —
36
38
  * `{ errorHandling: config.errorHandling, generate: config.generate }`.
37
39
  */
@@ -1,4 +1,16 @@
1
1
  import formatErrorMessage from './format_error_message';
2
+ import stringifyUnknown from './stringify_unknown';
3
+ const DEFAULT_THROTTLE_MS = 5000;
4
+ // Module-scope dedup state — safe by default only because a fresh JS realm
5
+ // (isolate/Worker instance) starts with it cleared. In a long-lived server
6
+ // process reused across many requests, pass `resetDedup: true` on the first
7
+ // `reportError` call of each request/cron tick, or one request's errors can
8
+ // suppress another's.
9
+ let lastDedupKey = null;
10
+ let lastReportedAt = 0;
11
+ function buildDedupKey(params) {
12
+ return params.dedupKey ?? `${params.classOrMethodName} ${stringifyUnknown(params.error, params.isClient)} ${params.params ? stringifyUnknown(params.params, params.isClient) : ''}`;
13
+ }
2
14
  async function callOnError(config, params) {
3
15
  const paramsWithFormattedMessage = { ...params, formattedMessage: formatErrorMessage(params) };
4
16
  try {
@@ -16,10 +28,11 @@ async function callOnError(config, params) {
16
28
  /**
17
29
  * Reports `params` via `config.errorHandling.onError` (default
18
30
  * `console.error(params.formattedMessage)`), unless
19
- * `config.errorHandling.enable === false` or `params.consent` is set and
20
- * not `true` (reporting to a third party without cookie consent can itself
21
- * be GDPR-relevant). Never throws a broken reporter must not mask the
22
- * original error.
31
+ * `config.errorHandling.enable === false`, `params.consent` is set and not
32
+ * `true` (reporting to a third party without cookie consent can itself be
33
+ * GDPR-relevant), or dedup throttles it (on by default see
34
+ * `errorHandling.dedup`/`throttleMs`/`resetDedup`). Never throws — a broken
35
+ * reporter must not mask the original error.
23
36
  *
24
37
  * Always overwrites `params.formattedMessage` with a fresh
25
38
  * `formatErrorMessage(params)` before reporting — a human-readable one-line
@@ -27,11 +40,6 @@ async function callOnError(config, params) {
27
40
  * instead of the raw `error`/`params` object, for a default reporter (or a
28
41
  * simple `onError`) to print directly.
29
42
  *
30
- * No built-in dedup/throttling: this package has no per-request context to
31
- * safely scope such state to (module-scope state would leak across
32
- * concurrent requests in a long-lived server process). Do dedup/throttling
33
- * in your own `onError` if you need it, scoped to your own request context.
34
- *
35
43
  * When `config.generate?.getCloudflareContext` is set, `waitUntil` is called
36
44
  * SYNCHRONOUSLY, in the same tick, with the `callOnError(...)` promise —
37
45
  * Cloudflare Workers only extends the request's lifetime for work already
@@ -42,15 +50,36 @@ async function callOnError(config, params) {
42
50
  * `onError` directly when `getCloudflareContext`/`ctx.waitUntil` is unset or
43
51
  * unavailable (e.g. outside a Cloudflare Worker).
44
52
  *
53
+ * Passing `params.error` as `null`/`undefined` with `errorHandling.resetDedup: true`
54
+ * and nothing else is a valid "reset-only" call: the dedup state clears and
55
+ * `reportError` returns immediately, without calling `onError` — useful to
56
+ * clear dedup state once at the very start of a request/cron tick, before
57
+ * any handler that might call `reportError` for a real error runs.
58
+ *
45
59
  * @param config Pass the relevant slices of your `RoutingConfig` directly —
46
60
  * `{ errorHandling: config.errorHandling, generate: config.generate }`.
47
61
  */
48
62
  export default async function reportError(config, params) {
49
63
  const errorHandling = config?.errorHandling;
64
+ if (errorHandling?.resetDedup) {
65
+ lastDedupKey = null;
66
+ lastReportedAt = 0;
67
+ if (params.error === null || params.error === undefined)
68
+ return;
69
+ }
50
70
  if (errorHandling?.enable === false)
51
71
  return;
52
72
  if (params.consent !== undefined && params.consent !== true)
53
73
  return;
74
+ if (errorHandling?.dedup !== false) {
75
+ const throttleMs = errorHandling?.throttleMs ?? DEFAULT_THROTTLE_MS;
76
+ const dedupKey = buildDedupKey(params);
77
+ const now = Date.now();
78
+ if (dedupKey === lastDedupKey && now - lastReportedAt < throttleMs)
79
+ return;
80
+ lastDedupKey = dedupKey;
81
+ lastReportedAt = now;
82
+ }
54
83
  const waitUntil = config?.generate?.getCloudflareContext?.({ async: false })?.ctx?.waitUntil;
55
84
  if (waitUntil) {
56
85
  waitUntil(callOnError(errorHandling, params));
@@ -149,6 +149,14 @@ export interface ErrorHandlingParams {
149
149
  * yourself — it's always overwritten.
150
150
  */
151
151
  formattedMessage?: string;
152
+ /**
153
+ * Key used by `config.errorHandling.dedupGate` to dedup this report
154
+ * against the immediately preceding one. Defaults to
155
+ * `` `${classOrMethodName} ${stringifyUnknown(error)} ${stringifyUnknown(params ?? '')}` ``
156
+ * when omitted (built by `reportError` itself) — set this explicitly
157
+ * only if you want a coarser/different dedup key.
158
+ */
159
+ dedupKey?: string;
152
160
  }
153
161
  export interface ErrorHandlingRoutingConfig {
154
162
  /**
@@ -193,6 +201,27 @@ export interface ErrorHandlingRoutingConfig {
193
201
  * substring match.
194
202
  */
195
203
  ignoreConsoleError?: (message: string) => boolean;
204
+ /**
205
+ * Dedup: `reportError` skips reporting an error whose key (`dedupKey`,
206
+ * or a built-in key derived from `classOrMethodName`/`error`/`params`
207
+ * when omitted) matches the immediately preceding reported error's key,
208
+ * within `throttleMs`. On by default (matches this package's own
209
+ * internal call sites and `installConsoleErrorOverride`'s console-loop
210
+ * guard). Set `false` to report every distinct call with no dedup at
211
+ * all.
212
+ *
213
+ * The dedup state lives inside `reportError`'s own module — this is
214
+ * shared mutable state, safe by default only because a fresh JS realm
215
+ * (isolate/Worker instance) starts with it cleared; in a long-lived
216
+ * server process reused across many requests, pass `resetDedup: true`
217
+ * on the first `reportError` call of each request/cron tick to clear it
218
+ * (otherwise one request's errors can suppress another's).
219
+ */
220
+ dedup?: boolean;
221
+ /** Throttle window in ms: the same dedup key reported again within this window is skipped. Defaults to `5000`. Only consulted when `dedup` isn't `false`. */
222
+ throttleMs?: number;
223
+ /** Clears the dedup last-key/timestamp state before processing this call. Pass `true` on the first `reportError` call of each request/cron tick in a long-lived server process. */
224
+ resetDedup?: boolean;
196
225
  }
197
226
  export interface CookieConsentRoutingConfig {
198
227
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Optimized Next Intl Package Special for App Router and Cloudflare",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",