@naturalcycles/js-lib 15.86.1 → 15.87.0

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.
@@ -0,0 +1,76 @@
1
+ import type { AnyObject, Branded, StringMap, UnixTimestampMillis } from '../types.js';
2
+ /** Identity an event is attributed to: a generated device id, or an app-provided user id. */
3
+ export type AnalyticsDistinctId = Branded<string, 'AnalyticsDistinctId'>;
4
+ /** Emitted when the identity changes, naming the anonymous id the events so far belong to. */
5
+ export declare const ANALYTICS_IDENTIFY_EVENT_NAME = "identify";
6
+ /** Cap applied by `truncateAnalyticsProperty`. Event properties are passed through as given. */
7
+ export declare const MAX_ANALYTICS_PROPERTY_LENGTH = 255;
8
+ /**
9
+ * Caps a string property at the limit. Counts code points, because a plain
10
+ * slice(0, N) counts UTF-16 code units and can cut a surrogate pair in half.
11
+ */
12
+ export declare function truncateAnalyticsProperty(value: string): string;
13
+ /**
14
+ * POST body of the ingestion endpoint - a client-batched analytics event envelope.
15
+ *
16
+ * Sent as a JSON string with `text/plain` content-type,
17
+ * to keep the request CORS-simple (no preflight).
18
+ */
19
+ export interface AnalyticsBatchInput {
20
+ /**
21
+ * Client-side unix timestamp (ms) of when this request was sent.
22
+ * Kept as envelope timing metadata. It must not be used to adjust `event.ts`,
23
+ * because it changes between delivery attempts.
24
+ */
25
+ sentAt: UnixTimestampMillis;
26
+ /**
27
+ * Client that sends the events. Allow-listed by the destination.
28
+ */
29
+ clientId: number;
30
+ /**
31
+ * @deprecated Use `AnalyticsClientEvent.userId`, which is stamped when the event is tracked
32
+ * rather than when the batch is sent. Still sent for clients that predate the per-event id.
33
+ */
34
+ userId: AnalyticsDistinctId;
35
+ events: AnalyticsClientEvent[];
36
+ }
37
+ export interface AnalyticsClientEvent {
38
+ /**
39
+ * Unique event id, used for deduplication. Alphanumeric-only, so it can be
40
+ * forwarded as a dedupe key without conversion.
41
+ */
42
+ id: string;
43
+ /**
44
+ * Event name, e.g `WebClick`.
45
+ */
46
+ name: string;
47
+ /**
48
+ * Client-side unix timestamp (ms) of when the event was tracked (not when it was sent).
49
+ * Stable across delivery attempts, because a deduplication key may include it.
50
+ */
51
+ ts: UnixTimestampMillis;
52
+ /**
53
+ * Event properties.
54
+ */
55
+ props?: AnyObject;
56
+ /**
57
+ * Identity this event was tracked under
58
+ * TODO: make required
59
+ */
60
+ userId?: AnalyticsDistinctId;
61
+ }
62
+ /** Canonical utm parameters. */
63
+ export declare const CANONICAL_UTM_PARAMS: string[];
64
+ /** Keyed by `initial_<canonical utm param>`, to avoid permanently saving non-standard ones. */
65
+ export type FirstTouchUtms = StringMap<string | null>;
66
+ /**
67
+ * Properties that will be "set once" on a user profile. Meaning they are not changed by subsequent
68
+ * saves on the user profile. Keyed by `initial_referrer` and the first-touch utms.
69
+ */
70
+ export type SetOnceUserProperties = StringMap<string | null>;
71
+ /** Body of the first-touch endpoint, posted once per pageload. */
72
+ export interface FirstTouchInput {
73
+ clientId: number;
74
+ userId: AnalyticsDistinctId;
75
+ props: SetOnceUserProperties;
76
+ }
@@ -0,0 +1,26 @@
1
+ /** Emitted when the identity changes, naming the anonymous id the events so far belong to. */
2
+ export const ANALYTICS_IDENTIFY_EVENT_NAME = 'identify';
3
+ /** Cap applied by `truncateAnalyticsProperty`. Event properties are passed through as given. */
4
+ export const MAX_ANALYTICS_PROPERTY_LENGTH = 255;
5
+ /**
6
+ * Caps a string property at the limit. Counts code points, because a plain
7
+ * slice(0, N) counts UTF-16 code units and can cut a surrogate pair in half.
8
+ */
9
+ export function truncateAnalyticsProperty(value) {
10
+ if (value.length <= MAX_ANALYTICS_PROPERTY_LENGTH)
11
+ return value;
12
+ return Array.from(value).slice(0, MAX_ANALYTICS_PROPERTY_LENGTH).join('');
13
+ }
14
+ /** Canonical utm parameters. */
15
+ export const CANONICAL_UTM_PARAMS = [
16
+ 'utm_source',
17
+ 'utm_medium',
18
+ 'utm_campaign',
19
+ 'utm_content',
20
+ 'utm_term',
21
+ 'utm_id',
22
+ 'utm_source_platform',
23
+ 'utm_campaign_id',
24
+ 'utm_creative_format',
25
+ 'utm_marketing_tactic',
26
+ ];
@@ -0,0 +1 @@
1
+ export * from './analytics.model.js';
@@ -0,0 +1 @@
1
+ export * from './analytics.model.js';
@@ -21,7 +21,11 @@ export class LocalDate {
21
21
  this.day = day;
22
22
  }
23
23
  get(unit) {
24
- return unit === 'year' ? this.year : unit === 'month' ? this.month : this.day;
24
+ if (unit === 'year')
25
+ return this.year;
26
+ if (unit === 'month')
27
+ return this.month;
28
+ return this.day;
25
29
  }
26
30
  set(unit, v, opt = {}) {
27
31
  const t = opt.mutate ? this : this.clone();
@@ -174,24 +174,49 @@ export function _errorSnippet(err, opt = {}) {
174
174
  .join(': ');
175
175
  }
176
176
  }
177
+ // These duck-typing checks read properties of an arbitrary object, which may invoke its
178
+ // property getters, which may throw (e.g a stateful getter, a Proxy trap).
179
+ // A type guard should never throw - an object whose properties cannot even be read safely
180
+ // is, by definition, not a well-behaved Error-like - hence try/catch returning false.
181
+ // try/catch is free in V8 unless it actually catches, so there's no performance penalty.
177
182
  export function _isBackendErrorResponseObject(o) {
178
- return _isErrorObject(o?.error);
183
+ try {
184
+ return _isErrorObject(o?.error);
185
+ }
186
+ catch {
187
+ return false;
188
+ }
179
189
  }
180
190
  export function _isHttpRequestErrorObject(o) {
181
- return !!o && o.name === 'HttpRequestError' && typeof o.data?.requestUrl === 'string';
191
+ try {
192
+ return !!o && o.name === 'HttpRequestError' && typeof o.data?.requestUrl === 'string';
193
+ }
194
+ catch {
195
+ return false;
196
+ }
182
197
  }
183
198
  /**
184
199
  * Note: any instance of AppError is also automatically an ErrorObject
185
200
  */
186
201
  export function _isErrorObject(o) {
187
- return (!!o &&
188
- typeof o === 'object' &&
189
- typeof o.name === 'string' &&
190
- typeof o.message === 'string' &&
191
- typeof o.data === 'object');
202
+ try {
203
+ return (!!o &&
204
+ typeof o === 'object' &&
205
+ typeof o.name === 'string' &&
206
+ typeof o.message === 'string' &&
207
+ typeof o.data === 'object');
208
+ }
209
+ catch {
210
+ return false;
211
+ }
192
212
  }
193
213
  export function _isErrorLike(o) {
194
- return !!o && typeof o === 'object' && typeof o.name === 'string' && typeof o.message === 'string';
214
+ try {
215
+ return (!!o && typeof o === 'object' && typeof o.name === 'string' && typeof o.message === 'string');
216
+ }
217
+ catch {
218
+ return false;
219
+ }
195
220
  }
196
221
  /**
197
222
  * Convenience function to safely add properties to Error's `data` object
@@ -40,7 +40,11 @@ export function _isBetween(x, min, max, incl) {
40
40
  return true;
41
41
  }
42
42
  export function _clamp(x, minIncl, maxIncl) {
43
- return x <= minIncl ? minIncl : x >= maxIncl ? maxIncl : x;
43
+ if (x <= minIncl)
44
+ return minIncl;
45
+ if (x >= maxIncl)
46
+ return maxIncl;
47
+ return x;
44
48
  }
45
49
  /**
46
50
  * Same as .toFixed(), but conveniently casts the output to Number.
package/dist/qr/qr.js CHANGED
@@ -560,8 +560,11 @@ function getLengthInBits(mode, type) {
560
560
  const bits = LENGTH_BITS[mode];
561
561
  if (!bits)
562
562
  throw new Error(`qr: bad mode: ${mode}`);
563
- const range = type < 10 ? 0 : type < 27 ? 1 : 2;
564
- return bits[range];
563
+ if (type < 10)
564
+ return bits[0];
565
+ if (type < 27)
566
+ return bits[1];
567
+ return bits[2];
565
568
  }
566
569
  // --- Reed-Solomon polynomials over GF(256) ---------------------------------
567
570
  /** Drop leading zeros, then right-pad with `shift` zeros. */
package/dist/semver.js CHANGED
@@ -147,5 +147,9 @@ export function _quickSemverCompare(a, b) {
147
147
  const s2 = _range(3)
148
148
  .map(i => (t2[i] || '').padStart(5))
149
149
  .join('');
150
- return s1 < s2 ? -1 : s1 > s2 ? 1 : 0;
150
+ if (s1 < s2)
151
+ return -1;
152
+ if (s1 > s2)
153
+ return 1;
154
+ return 0;
151
155
  }
@@ -63,14 +63,13 @@ export function _leven(first, second, limit) {
63
63
  for (index = 0; index < firstLength; index++) {
64
64
  temporary2 = bCharacterCode === characterCodeCache[index] ? temporary : temporary + 1;
65
65
  temporary = array[index];
66
- result = array[index] =
67
- temporary > result
68
- ? temporary2 > result
69
- ? result + 1
70
- : temporary2
71
- : temporary2 > temporary
72
- ? temporary + 1
73
- : temporary2;
66
+ if (temporary > result) {
67
+ result = temporary2 > result ? result + 1 : temporary2;
68
+ }
69
+ else {
70
+ result = temporary2 > temporary ? temporary + 1 : temporary2;
71
+ }
72
+ array[index] = result;
74
73
  }
75
74
  }
76
75
  return result;
package/dist/ua/ua.d.ts CHANGED
@@ -152,7 +152,7 @@ export declare class UAParser {
152
152
  getOS(): UAOS;
153
153
  getResult(): UAResult;
154
154
  getUA(): string;
155
- setUA(ua: string | undefined): this;
155
+ setUA(ua?: string | undefined): this;
156
156
  /**
157
157
  * True if the OS is iOS.
158
158
  * Note: desktop-mode iPads send a Mac-like UA and are not detected as iOS
package/dist/ua/ua.js CHANGED
@@ -146,8 +146,7 @@ export class UAParser {
146
146
  getUA() {
147
147
  return this.ua;
148
148
  }
149
- setUA(ua) {
150
- ua ||= '';
149
+ setUA(ua = '') {
151
150
  this.ua = ua.length > UA_MAX_LENGTH ? ua.replace(/^\s\s*/, '').slice(0, UA_MAX_LENGTH) : ua;
152
151
  this.cachedBrowser = this.cachedOs = this.cachedDevice = undefined;
153
152
  return this;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
3
  "type": "module",
4
- "version": "15.86.1",
4
+ "version": "15.87.0",
5
5
  "dependencies": {
6
6
  "tslib": "^2"
7
7
  },
@@ -14,14 +14,16 @@
14
14
  }
15
15
  },
16
16
  "devDependencies": {
17
+ "@naturalcycles/dev-lib": "0.0.0",
17
18
  "@types/node": "^26",
18
19
  "@types/semver": "^7",
19
20
  "typescript": "^7",
20
- "dayjs": "^1",
21
- "@naturalcycles/dev-lib": "0.0.0"
21
+ "dayjs": "^1"
22
22
  },
23
23
  "exports": {
24
24
  ".": "./dist/index.js",
25
+ "./analytics": "./dist/analytics/index.js",
26
+ "./analytics/*.js": "./dist/analytics/*.js",
25
27
  "./array": "./dist/array/index.js",
26
28
  "./array/*.js": "./dist/array/*.js",
27
29
  "./browser": "./dist/browser/index.js",
@@ -0,0 +1,100 @@
1
+ import type { AnyObject, Branded, StringMap, UnixTimestampMillis } from '../types.js'
2
+
3
+ /** Identity an event is attributed to: a generated device id, or an app-provided user id. */
4
+ export type AnalyticsDistinctId = Branded<string, 'AnalyticsDistinctId'>
5
+
6
+ /** Emitted when the identity changes, naming the anonymous id the events so far belong to. */
7
+ export const ANALYTICS_IDENTIFY_EVENT_NAME = 'identify'
8
+
9
+ /** Cap applied by `truncateAnalyticsProperty`. Event properties are passed through as given. */
10
+ export const MAX_ANALYTICS_PROPERTY_LENGTH = 255
11
+
12
+ /**
13
+ * Caps a string property at the limit. Counts code points, because a plain
14
+ * slice(0, N) counts UTF-16 code units and can cut a surrogate pair in half.
15
+ */
16
+ export function truncateAnalyticsProperty(value: string): string {
17
+ if (value.length <= MAX_ANALYTICS_PROPERTY_LENGTH) return value
18
+ return Array.from(value).slice(0, MAX_ANALYTICS_PROPERTY_LENGTH).join('')
19
+ }
20
+
21
+ /**
22
+ * POST body of the ingestion endpoint - a client-batched analytics event envelope.
23
+ *
24
+ * Sent as a JSON string with `text/plain` content-type,
25
+ * to keep the request CORS-simple (no preflight).
26
+ */
27
+ export interface AnalyticsBatchInput {
28
+ /**
29
+ * Client-side unix timestamp (ms) of when this request was sent.
30
+ * Kept as envelope timing metadata. It must not be used to adjust `event.ts`,
31
+ * because it changes between delivery attempts.
32
+ */
33
+ sentAt: UnixTimestampMillis
34
+ /**
35
+ * Client that sends the events. Allow-listed by the destination.
36
+ */
37
+ clientId: number
38
+ /**
39
+ * @deprecated Use `AnalyticsClientEvent.userId`, which is stamped when the event is tracked
40
+ * rather than when the batch is sent. Still sent for clients that predate the per-event id.
41
+ */
42
+ userId: AnalyticsDistinctId
43
+ events: AnalyticsClientEvent[]
44
+ }
45
+
46
+ export interface AnalyticsClientEvent {
47
+ /**
48
+ * Unique event id, used for deduplication. Alphanumeric-only, so it can be
49
+ * forwarded as a dedupe key without conversion.
50
+ */
51
+ id: string
52
+ /**
53
+ * Event name, e.g `WebClick`.
54
+ */
55
+ name: string
56
+ /**
57
+ * Client-side unix timestamp (ms) of when the event was tracked (not when it was sent).
58
+ * Stable across delivery attempts, because a deduplication key may include it.
59
+ */
60
+ ts: UnixTimestampMillis
61
+ /**
62
+ * Event properties.
63
+ */
64
+ props?: AnyObject
65
+ /**
66
+ * Identity this event was tracked under
67
+ * TODO: make required
68
+ */
69
+ userId?: AnalyticsDistinctId
70
+ }
71
+
72
+ /** Canonical utm parameters. */
73
+ export const CANONICAL_UTM_PARAMS = [
74
+ 'utm_source',
75
+ 'utm_medium',
76
+ 'utm_campaign',
77
+ 'utm_content',
78
+ 'utm_term',
79
+ 'utm_id',
80
+ 'utm_source_platform',
81
+ 'utm_campaign_id',
82
+ 'utm_creative_format',
83
+ 'utm_marketing_tactic',
84
+ ]
85
+
86
+ /** Keyed by `initial_<canonical utm param>`, to avoid permanently saving non-standard ones. */
87
+ export type FirstTouchUtms = StringMap<string | null>
88
+
89
+ /**
90
+ * Properties that will be "set once" on a user profile. Meaning they are not changed by subsequent
91
+ * saves on the user profile. Keyed by `initial_referrer` and the first-touch utms.
92
+ */
93
+ export type SetOnceUserProperties = StringMap<string | null>
94
+
95
+ /** Body of the first-touch endpoint, posted once per pageload. */
96
+ export interface FirstTouchInput {
97
+ clientId: number
98
+ userId: AnalyticsDistinctId
99
+ props: SetOnceUserProperties
100
+ }
@@ -0,0 +1 @@
1
+ export * from './analytics.model.js'
@@ -40,7 +40,9 @@ export class LocalDate {
40
40
  ) {}
41
41
 
42
42
  get(unit: LocalDateUnitStrict): number {
43
- return unit === 'year' ? this.year : unit === 'month' ? this.month : this.day
43
+ if (unit === 'year') return this.year
44
+ if (unit === 'month') return this.month
45
+ return this.day
44
46
  }
45
47
 
46
48
  set(unit: LocalDateUnitStrict, v: number, opt: MutateOptions = {}): LocalDate {
@@ -226,12 +226,26 @@ export function _errorSnippet(err: any, opt: ErrorSnippetOptions = {}): string {
226
226
  }
227
227
  }
228
228
 
229
+ // These duck-typing checks read properties of an arbitrary object, which may invoke its
230
+ // property getters, which may throw (e.g a stateful getter, a Proxy trap).
231
+ // A type guard should never throw - an object whose properties cannot even be read safely
232
+ // is, by definition, not a well-behaved Error-like - hence try/catch returning false.
233
+ // try/catch is free in V8 unless it actually catches, so there's no performance penalty.
234
+
229
235
  export function _isBackendErrorResponseObject(o: any): o is BackendErrorResponseObject {
230
- return _isErrorObject(o?.error)
236
+ try {
237
+ return _isErrorObject(o?.error)
238
+ } catch {
239
+ return false
240
+ }
231
241
  }
232
242
 
233
243
  export function _isHttpRequestErrorObject(o: any): o is ErrorObject<HttpRequestErrorData> {
234
- return !!o && o.name === 'HttpRequestError' && typeof o.data?.requestUrl === 'string'
244
+ try {
245
+ return !!o && o.name === 'HttpRequestError' && typeof o.data?.requestUrl === 'string'
246
+ } catch {
247
+ return false
248
+ }
235
249
  }
236
250
 
237
251
  /**
@@ -240,17 +254,27 @@ export function _isHttpRequestErrorObject(o: any): o is ErrorObject<HttpRequestE
240
254
  export function _isErrorObject<DATA_TYPE extends ErrorData = ErrorData>(
241
255
  o: any,
242
256
  ): o is ErrorObject<DATA_TYPE> {
243
- return (
244
- !!o &&
245
- typeof o === 'object' &&
246
- typeof o.name === 'string' &&
247
- typeof o.message === 'string' &&
248
- typeof o.data === 'object'
249
- )
257
+ try {
258
+ return (
259
+ !!o &&
260
+ typeof o === 'object' &&
261
+ typeof o.name === 'string' &&
262
+ typeof o.message === 'string' &&
263
+ typeof o.data === 'object'
264
+ )
265
+ } catch {
266
+ return false
267
+ }
250
268
  }
251
269
 
252
270
  export function _isErrorLike(o: any): o is ErrorLike {
253
- return !!o && typeof o === 'object' && typeof o.name === 'string' && typeof o.message === 'string'
271
+ try {
272
+ return (
273
+ !!o && typeof o === 'object' && typeof o.name === 'string' && typeof o.message === 'string'
274
+ )
275
+ } catch {
276
+ return false
277
+ }
254
278
  }
255
279
 
256
280
  /**
@@ -50,7 +50,9 @@ export function _isBetween<T extends number | string>(
50
50
  }
51
51
 
52
52
  export function _clamp(x: number, minIncl: number, maxIncl: number): number {
53
- return x <= minIncl ? minIncl : x >= maxIncl ? maxIncl : x
53
+ if (x <= minIncl) return minIncl
54
+ if (x >= maxIncl) return maxIncl
55
+ return x
54
56
  }
55
57
 
56
58
  /**
package/src/qr/qr.ts CHANGED
@@ -684,8 +684,9 @@ function getLengthInBits(mode: number, type: number): number {
684
684
  if (type < 1 || type > 40) throw new Error(`qr: bad type number: ${type}`)
685
685
  const bits = LENGTH_BITS[mode]
686
686
  if (!bits) throw new Error(`qr: bad mode: ${mode}`)
687
- const range = type < 10 ? 0 : type < 27 ? 1 : 2
688
- return bits[range]
687
+ if (type < 10) return bits[0]
688
+ if (type < 27) return bits[1]
689
+ return bits[2]
689
690
  }
690
691
 
691
692
  // --- Reed-Solomon polynomials over GF(256) ---------------------------------
package/src/semver.ts CHANGED
@@ -171,5 +171,7 @@ export function _quickSemverCompare(a: string, b: string): -1 | 0 | 1 {
171
171
  const s2 = _range(3)
172
172
  .map(i => (t2[i] || '').padStart(5))
173
173
  .join('')
174
- return s1 < s2 ? -1 : s1 > s2 ? 1 : 0
174
+ if (s1 < s2) return -1
175
+ if (s1 > s2) return 1
176
+ return 0
175
177
  }
@@ -76,14 +76,12 @@ export function _leven(first: string, second: string, limit?: number): number {
76
76
  temporary2 = bCharacterCode === characterCodeCache[index] ? temporary : temporary + 1
77
77
  temporary = array[index]!
78
78
 
79
- result = array[index] =
80
- temporary > result
81
- ? temporary2 > result
82
- ? result + 1
83
- : temporary2
84
- : temporary2 > temporary
85
- ? temporary + 1
86
- : temporary2
79
+ if (temporary > result) {
80
+ result = temporary2 > result ? result + 1 : temporary2
81
+ } else {
82
+ result = temporary2 > temporary ? temporary + 1 : temporary2
83
+ }
84
+ array[index] = result
87
85
  }
88
86
  }
89
87
 
package/src/ua/ua.ts CHANGED
@@ -266,8 +266,7 @@ export class UAParser {
266
266
  return this.ua
267
267
  }
268
268
 
269
- setUA(ua: string | undefined): this {
270
- ua ||= ''
269
+ setUA(ua: string | undefined = ''): this {
271
270
  this.ua = ua.length > UA_MAX_LENGTH ? ua.replace(/^\s\s*/, '').slice(0, UA_MAX_LENGTH) : ua
272
271
  this.cachedBrowser = this.cachedOs = this.cachedDevice = undefined
273
272
  return this