@vouchington/utils 0.7.0 → 0.7.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.
package/README.md CHANGED
@@ -21,6 +21,8 @@ import { createScopeCatalog } from '@vouchington/utils/scopes'
21
21
  - `cache-tags`: canonical printable-ASCII wire encoding, one-segment URL decoding, validation,
22
22
  and escape-boundary truncation. Applications own family names, route recognition, and purge policy.
23
23
  Single-tag validation rejects spaces and comma separators; raw commas are percent-encoded.
24
+ - `schema-growth`: `buildUnboundedUnpartitionedTables()` and `classifyTableGrowth()` map tables to
25
+ caller-supplied growth reasons. Applications own every table name and reason.
24
26
  - `sentry-envelope`: bounded UTF-8 envelope record scanning and safe item-type diagnostics.
25
27
  Callers choose the byte and type-list limits; DSN, tunnel, request, and logging policy stay local.
26
28
  - `scopes`: caller-owned scope catalog validation, prerequisite expansion, audience and surface
@@ -46,9 +46,7 @@ export function scrubSpanUrlAttributes(data, options = {}) {
46
46
  return transformRecord(data, (key, value) => {
47
47
  if (URL_COMPONENT_ATTRIBUTE_KEYS.includes(key))
48
48
  return DROP;
49
- return URL_ATTRIBUTE_KEYS.includes(key) && typeof value === 'string'
50
- ? scrubUrl(value, options)
51
- : value;
49
+ return URL_ATTRIBUTE_KEYS.includes(key) ? scrubUrlValue(value, options) : value;
52
50
  });
53
51
  }
54
52
  /** Scrub the `referer`/`referrer` header values of a headers record. */
@@ -1,5 +1,5 @@
1
1
  export { SENSITIVE_VALUE } from './observability-records.mts';
2
- export { createSpikeWindowTracker, type SpikeWindowTracker } from './spike-window.mts';
2
+ export { createSpikeWindowTracker, DEFAULT_MAX_SPIKE_FINGERPRINTS, type SpikeWindowTracker, } from './spike-window.mts';
3
3
  export { composeBeforeSend, scrubBreadcrumbs, scrubEvent, scrubHeaders, scrubRequest, scrubSpanAttributes, type ObservableEvent, type ScrubOptions, } from './observability-event.mts';
4
4
  export { scrubCredentialHeaders, scrubRequestCredentialFields, scrubSpanCredentialAttributes, } from './observability-credentials.mts';
5
5
  export { SCRUBBED_URL_ATTRIBUTE_KEYS, URL_ATTRIBUTE_KEYS, URL_COMPONENT_ATTRIBUTE_KEYS, scrubHeaderUrls, scrubRequestUrlFields, scrubSpanUrlAttributes, scrubUrlValue, stripUrlQueryAndFragment, type UrlScrubOptions, } from './observability-url.mts';
@@ -1,5 +1,5 @@
1
1
  export { SENSITIVE_VALUE } from './observability-records.mjs';
2
- export { createSpikeWindowTracker } from './spike-window.mjs';
2
+ export { createSpikeWindowTracker, DEFAULT_MAX_SPIKE_FINGERPRINTS, } from './spike-window.mjs';
3
3
  export { composeBeforeSend, scrubBreadcrumbs, scrubEvent, scrubHeaders, scrubRequest, scrubSpanAttributes, } from './observability-event.mjs';
4
4
  export { scrubCredentialHeaders, scrubRequestCredentialFields, scrubSpanCredentialAttributes, } from './observability-credentials.mjs';
5
5
  export { SCRUBBED_URL_ATTRIBUTE_KEYS, URL_ATTRIBUTE_KEYS, URL_COMPONENT_ATTRIBUTE_KEYS, scrubHeaderUrls, scrubRequestUrlFields, scrubSpanUrlAttributes, scrubUrlValue, stripUrlQueryAndFragment, } from './observability-url.mjs';
@@ -0,0 +1,20 @@
1
+ type GrowthPolicyGroup = {
2
+ /** Why these tables may grow without a time or size bound. */
3
+ reason: string;
4
+ tables: readonly string[];
5
+ /** Skip tables that are partitioned (they are bounded by partition retention instead). */
6
+ excludePartitioned?: boolean;
7
+ };
8
+ export interface BuildUnboundedTablesOptions {
9
+ partitionedTables: ReadonlySet<string>;
10
+ /** Applied in order; a later group or entry overrides an earlier reason for the same table. */
11
+ groups?: readonly GrowthPolicyGroup[];
12
+ /** Per-table `[table, reason]` entries, applied after `groups`. */
13
+ entries?: Iterable<readonly [table: string, reason: string]>;
14
+ }
15
+ export type TableGrowthClass = 'partitioned' | 'unbounded' | 'unclassified';
16
+ /** Maps every deliberately unbounded, unpartitioned table to its documented reason. */
17
+ export declare function buildUnboundedUnpartitionedTables(options: BuildUnboundedTablesOptions): Map<string, string>;
18
+ /** Partitioned wins over unbounded; anything else needs an explicit growth decision. */
19
+ export declare function classifyTableGrowth(table: string, partitionedTables: ReadonlySet<string>, unboundedTables: ReadonlyMap<string, string>): TableGrowthClass;
20
+ export {};
@@ -0,0 +1,20 @@
1
+ /** Maps every deliberately unbounded, unpartitioned table to its documented reason. */
2
+ export function buildUnboundedUnpartitionedTables(options) {
3
+ const result = new Map();
4
+ for (const group of options.groups ?? []) {
5
+ for (const table of group.tables) {
6
+ if (group.excludePartitioned && options.partitionedTables.has(table))
7
+ continue;
8
+ result.set(table, group.reason);
9
+ }
10
+ }
11
+ for (const [table, reason] of options.entries ?? [])
12
+ result.set(table, reason);
13
+ return result;
14
+ }
15
+ /** Partitioned wins over unbounded; anything else needs an explicit growth decision. */
16
+ export function classifyTableGrowth(table, partitionedTables, unboundedTables) {
17
+ if (partitionedTables.has(table))
18
+ return 'partitioned';
19
+ return unboundedTables.has(table) ? 'unbounded' : 'unclassified';
20
+ }
@@ -2,5 +2,10 @@ export interface SpikeWindowTracker {
2
2
  readonly size: number;
3
3
  recordAndCheck(fingerprint: string, timestamp: number): boolean;
4
4
  }
5
- /** Tracks per-fingerprint occurrence counts while evicting expired windows. */
6
- export declare function createSpikeWindowTracker(limit: number, windowMs: number): SpikeWindowTracker;
5
+ /** Default cap on distinct fingerprints tracked at once. */
6
+ export declare const DEFAULT_MAX_SPIKE_FINGERPRINTS = 1000;
7
+ /**
8
+ * Tracks per-fingerprint occurrence counts while evicting expired windows. At most
9
+ * `maxFingerprints` distinct fingerprints are kept; the least recently seen is evicted first.
10
+ */
11
+ export declare function createSpikeWindowTracker(limit: number, windowMs: number, maxFingerprints?: number): SpikeWindowTracker;
@@ -1,9 +1,16 @@
1
- /** Tracks per-fingerprint occurrence counts while evicting expired windows. */
2
- export function createSpikeWindowTracker(limit, windowMs) {
1
+ /** Default cap on distinct fingerprints tracked at once. */
2
+ export const DEFAULT_MAX_SPIKE_FINGERPRINTS = 1000;
3
+ /**
4
+ * Tracks per-fingerprint occurrence counts while evicting expired windows. At most
5
+ * `maxFingerprints` distinct fingerprints are kept; the least recently seen is evicted first.
6
+ */
7
+ export function createSpikeWindowTracker(limit, windowMs, maxFingerprints = DEFAULT_MAX_SPIKE_FINGERPRINTS) {
3
8
  if (!Number.isSafeInteger(limit) || limit < 0)
4
9
  throw new RangeError('limit must be a non-negative safe integer');
5
10
  if (!Number.isFinite(windowMs) || windowMs <= 0)
6
11
  throw new RangeError('windowMs must be positive and finite');
12
+ if (!Number.isSafeInteger(maxFingerprints) || maxFingerprints < 1)
13
+ throw new RangeError('maxFingerprints must be a positive safe integer');
7
14
  const windows = new Map();
8
15
  let lastSweepAt = -Infinity;
9
16
  let lastTimestamp = -Infinity;
@@ -34,7 +41,14 @@ export function createSpikeWindowTracker(limit, windowMs) {
34
41
  ? existing
35
42
  : { count: 0, windowStart: timestamp };
36
43
  window.count += 1;
44
+ windows.delete(fingerprint);
37
45
  windows.set(fingerprint, window);
46
+ if (windows.size > maxFingerprints) {
47
+ for (const oldest of windows.keys()) {
48
+ windows.delete(oldest);
49
+ break;
50
+ }
51
+ }
38
52
  return window.count <= limit;
39
53
  },
40
54
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vouchington/utils",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Dependency-free runtime utilities for Vouchington-style applications.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-platform/tree/main/packages/utils#readme",
6
6
  "bugs": {
@@ -30,6 +30,11 @@
30
30
  "import": "./dist/api-response-contract.mjs",
31
31
  "default": "./dist/api-response-contract.mjs"
32
32
  },
33
+ "./schema-growth": {
34
+ "types": "./dist/schema-growth.d.mts",
35
+ "import": "./dist/schema-growth.mjs",
36
+ "default": "./dist/schema-growth.mjs"
37
+ },
33
38
  "./cache-tags": {
34
39
  "types": "./dist/cache-tags.d.mts",
35
40
  "import": "./dist/cache-tags.mjs",