@thumbmarkjs/thumbmarkjs 1.3.4 → 1.4.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.
- package/dist/thumbmark.cjs.js +1 -1
- package/dist/thumbmark.cjs.js.map +1 -1
- package/dist/thumbmark.esm.d.ts +24 -1
- package/dist/thumbmark.esm.js +1 -1
- package/dist/thumbmark.esm.js.map +1 -1
- package/dist/thumbmark.umd.js +1 -1
- package/dist/thumbmark.umd.js.map +1 -1
- package/package.json +2 -2
- package/src/components/mathml/index.ts +7 -6
- package/src/components/speech/index.ts +3 -2
- package/src/components/webrtc/index.ts +6 -5
- package/src/functions/api.ts +7 -5
- package/src/functions/index.ts +80 -75
- package/src/index.ts +8 -3
- package/src/utils/stableStringify.test.ts +335 -0
- package/src/utils/stableStringify.ts +70 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { componentInterface } from '../../factory';
|
|
2
2
|
import { hash } from '../../utils/hash';
|
|
3
3
|
import { ephemeralIFrame } from '../../utils/ephemeralIFrame';
|
|
4
|
+
import { stableStringify } from '../../utils/stableStringify';
|
|
4
5
|
|
|
5
6
|
const BLACKBOARD_BOLD = ['\uD835\uDD04', '\uD835\uDD05', '\u212D', '\uD835\uDD07', '\uD835\uDD08', '\uD835\uDD09', '\uD835\uDD38', '\uD835\uDD39', '\u2102', '\uD835\uDD3B', '\uD835\uDD3C', '\uD835\uDD3D'];
|
|
6
7
|
const GREEK_SYMBOLS = ['\u03B2', '\u03C8', '\u03BB', '\u03B5', '\u03B6', '\u03B1', '\u03BE', '\u03BC', '\u03C1', '\u03C6', '\u03BA', '\u03C4', '\u03B7', '\u03C3', '\u03B9', '\u03C9', '\u03B3', '\u03BD', '\u03C7', '\u03B4', '\u03B8', '\u03C0', '\u03C5', '\u03BF'];
|
|
@@ -38,7 +39,7 @@ export default async function getMathML(): Promise<componentInterface | null> {
|
|
|
38
39
|
});
|
|
39
40
|
// Capture font style hash from the first structure (it's the same for all)
|
|
40
41
|
if (i === 0 && measurement.fontInfo) {
|
|
41
|
-
fontStyleHash = hash(
|
|
42
|
+
fontStyleHash = hash(stableStringify(measurement.fontInfo));
|
|
42
43
|
}
|
|
43
44
|
});
|
|
44
45
|
|
|
@@ -50,7 +51,7 @@ export default async function getMathML(): Promise<componentInterface | null> {
|
|
|
50
51
|
resolve({
|
|
51
52
|
//supported: true,
|
|
52
53
|
details,
|
|
53
|
-
hash: hash(
|
|
54
|
+
hash: hash(stableStringify(details))
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
} catch (error) {
|
|
@@ -92,7 +93,7 @@ function createMathML(name: string, content: string): string {
|
|
|
92
93
|
|
|
93
94
|
function createComplexNestedStructure(): string {
|
|
94
95
|
let nestedContent = '<mo>\u220F</mo>'; // Product symbol (∏)
|
|
95
|
-
|
|
96
|
+
|
|
96
97
|
// Add all symbol combinations inside the main structure
|
|
97
98
|
BLACKBOARD_BOLD.forEach((bbSymbol, bbIndex) => {
|
|
98
99
|
const startIdx = bbIndex * 2;
|
|
@@ -102,7 +103,7 @@ function createComplexNestedStructure(): string {
|
|
|
102
103
|
nestedContent += `<mmultiscripts><mi>${bbSymbol}</mi><none/><mi>${greekSet[1]}</mi><mprescripts></mprescripts><mi>${greekSet[0]}</mi><none/></mmultiscripts>`;
|
|
103
104
|
}
|
|
104
105
|
});
|
|
105
|
-
|
|
106
|
+
|
|
106
107
|
return createMathML('complex_nested',
|
|
107
108
|
`<munderover><mmultiscripts>${nestedContent}</mmultiscripts></munderover>`
|
|
108
109
|
);
|
|
@@ -110,7 +111,7 @@ function createComplexNestedStructure(): string {
|
|
|
110
111
|
|
|
111
112
|
function createSymbolStructures(): string[] {
|
|
112
113
|
const structures: string[] = [];
|
|
113
|
-
|
|
114
|
+
|
|
114
115
|
// Use blackboard bold as base symbols with Greek symbols as subscripts/superscripts
|
|
115
116
|
BLACKBOARD_BOLD.forEach((bbSymbol, bbIndex) => {
|
|
116
117
|
// Get 2 Greek symbols for this blackboard bold symbol (lower left, top right)
|
|
@@ -123,7 +124,7 @@ function createSymbolStructures(): string[] {
|
|
|
123
124
|
));
|
|
124
125
|
}
|
|
125
126
|
});
|
|
126
|
-
|
|
127
|
+
|
|
127
128
|
return structures;
|
|
128
129
|
}
|
|
129
130
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { componentInterface } from '../../factory';
|
|
2
2
|
import { hash } from '../../utils/hash';
|
|
3
|
+
import { stableStringify } from '../../utils/stableStringify';
|
|
3
4
|
|
|
4
5
|
const VOICE_LOAD_TIMEOUT = 800; // milliseconds to wait for voices to load
|
|
5
6
|
|
|
@@ -53,12 +54,12 @@ export default async function getSpeech(): Promise<componentInterface | null> {
|
|
|
53
54
|
// Create details object with count and hash
|
|
54
55
|
const details = {
|
|
55
56
|
voiceCount: voices.length,
|
|
56
|
-
voicesHash: hash(
|
|
57
|
+
voicesHash: hash(stableStringify(voiceSignatures))
|
|
57
58
|
};
|
|
58
59
|
|
|
59
60
|
resolve({
|
|
60
61
|
details,
|
|
61
|
-
hash: hash(
|
|
62
|
+
hash: hash(stableStringify(details))
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
} catch (error) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { componentInterface } from '../../factory';
|
|
2
2
|
import { hash } from '../../utils/hash';
|
|
3
|
+
import { stableStringify } from '../../utils/stableStringify';
|
|
3
4
|
|
|
4
5
|
export default async function getWebRTC(): Promise<componentInterface | null> {
|
|
5
6
|
return new Promise((resolve) => {
|
|
@@ -29,7 +30,7 @@ export default async function getWebRTC(): Promise<componentInterface | null> {
|
|
|
29
30
|
await connection.setLocalDescription(offer);
|
|
30
31
|
|
|
31
32
|
const sdp = offer.sdp || '';
|
|
32
|
-
|
|
33
|
+
|
|
33
34
|
// Extract RTP extensions
|
|
34
35
|
const extensions = [...new Set((sdp.match(/extmap:\d+ [^\n\r]+/g) || []).map((x: string) => x.replace(/extmap:\d+ /, '')))].sort();
|
|
35
36
|
|
|
@@ -70,13 +71,13 @@ export default async function getWebRTC(): Promise<componentInterface | null> {
|
|
|
70
71
|
const compressedData = {
|
|
71
72
|
audio: {
|
|
72
73
|
count: audioCodecs.length,
|
|
73
|
-
hash: hash(
|
|
74
|
+
hash: hash(stableStringify(audioCodecs))
|
|
74
75
|
},
|
|
75
76
|
video: {
|
|
76
77
|
count: videoCodecs.length,
|
|
77
|
-
hash: hash(
|
|
78
|
+
hash: hash(stableStringify(videoCodecs))
|
|
78
79
|
},
|
|
79
|
-
extensionsHash: hash(
|
|
80
|
+
extensionsHash: hash(stableStringify(extensions))
|
|
80
81
|
};
|
|
81
82
|
|
|
82
83
|
// Set up for ICE candidate collection with timeout
|
|
@@ -111,7 +112,7 @@ export default async function getWebRTC(): Promise<componentInterface | null> {
|
|
|
111
112
|
|
|
112
113
|
resolve({
|
|
113
114
|
details: result,
|
|
114
|
-
hash: hash(
|
|
115
|
+
hash: hash(stableStringify(result)),
|
|
115
116
|
});
|
|
116
117
|
|
|
117
118
|
} catch (error) {
|
package/src/functions/api.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { componentInterface } from '../factory';
|
|
|
3
3
|
import { getVisitorId, setVisitorId } from '../utils/visitorId';
|
|
4
4
|
import { getVersion } from "../utils/version";
|
|
5
5
|
import { hash } from '../utils/hash';
|
|
6
|
+
import { stableStringify } from '../utils/stableStringify';
|
|
6
7
|
|
|
7
8
|
// ===================== Types & Interfaces =====================
|
|
8
9
|
|
|
@@ -37,6 +38,7 @@ interface apiResponse {
|
|
|
37
38
|
version?: string;
|
|
38
39
|
components?: componentInterface;
|
|
39
40
|
visitorId?: string;
|
|
41
|
+
thumbmark?: string;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
// ===================== API Call Logic =====================
|
|
@@ -66,16 +68,16 @@ export const getApiPromise = (
|
|
|
66
68
|
const apiEndpoint = options.api_endpoint || DEFAULT_API_ENDPOINT;
|
|
67
69
|
const endpoint = `${apiEndpoint}/thumbmark`;
|
|
68
70
|
const visitorId = getVisitorId();
|
|
69
|
-
const requestBody: any = {
|
|
70
|
-
components,
|
|
71
|
-
options,
|
|
72
|
-
clientHash: hash(
|
|
71
|
+
const requestBody: any = {
|
|
72
|
+
components,
|
|
73
|
+
options,
|
|
74
|
+
clientHash: hash(stableStringify(components)),
|
|
73
75
|
version: getVersion()
|
|
74
76
|
};
|
|
75
77
|
if (visitorId) {
|
|
76
78
|
requestBody.visitorId = visitorId;
|
|
77
79
|
}
|
|
78
|
-
|
|
80
|
+
|
|
79
81
|
const fetchPromise = fetch(endpoint, {
|
|
80
82
|
method: 'POST',
|
|
81
83
|
headers: {
|
package/src/functions/index.ts
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { defaultOptions, optionsInterface } from "../options";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
import {
|
|
11
|
+
timeoutInstance,
|
|
12
|
+
componentInterface,
|
|
13
|
+
tm_component_promises,
|
|
14
|
+
customComponents,
|
|
15
|
+
tm_experimental_component_promises,
|
|
16
|
+
includeComponent as globalIncludeComponent
|
|
17
17
|
} from "../factory";
|
|
18
18
|
import { hash } from "../utils/hash";
|
|
19
19
|
import { raceAllPerformance } from "../utils/raceAll";
|
|
@@ -21,20 +21,21 @@ import { getVersion } from "../utils/version";
|
|
|
21
21
|
import { filterThumbmarkData } from './filterComponents'
|
|
22
22
|
import { logThumbmarkData } from '../utils/log';
|
|
23
23
|
import { getApiPromise, infoInterface } from "./api";
|
|
24
|
+
import { stableStringify } from "../utils/stableStringify";
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Final thumbmark response structure
|
|
28
29
|
*/
|
|
29
30
|
interface thumbmarkResponse {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
components: componentInterface,
|
|
32
|
+
info: { [key: string]: any },
|
|
33
|
+
version: string,
|
|
34
|
+
thumbmark: string,
|
|
35
|
+
visitorId?: string,
|
|
36
|
+
elapsed?: any;
|
|
37
|
+
error?: string;
|
|
38
|
+
experimental?: componentInterface;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/**
|
|
@@ -44,69 +45,73 @@ interface thumbmarkResponse {
|
|
|
44
45
|
* @returns thumbmarkResponse (elapsed is present only if options.performance is true)
|
|
45
46
|
*/
|
|
46
47
|
export async function getThumbmark(options?: optionsInterface): Promise<thumbmarkResponse> {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// Early logging decision
|
|
50
|
-
const shouldLog = (_options.logging && !sessionStorage.getItem("_tmjs_l") && Math.random() < 0.0001);
|
|
51
|
-
|
|
52
|
-
// Merge built-in and user-registered components
|
|
53
|
-
const allComponents = { ...tm_component_promises, ...customComponents };
|
|
54
|
-
const { elapsed, resolvedComponents: clientComponentsResult } = await resolveClientComponents(allComponents, _options);
|
|
55
|
-
|
|
56
|
-
// Resolve experimental components only when logging
|
|
57
|
-
let experimentalComponents = {};
|
|
58
|
-
let experimentalElapsed = {};
|
|
59
|
-
if (shouldLog || _options.experimental) {
|
|
60
|
-
const { elapsed: expElapsed, resolvedComponents } = await resolveClientComponents(tm_experimental_component_promises, _options);
|
|
61
|
-
experimentalComponents = resolvedComponents;
|
|
62
|
-
experimentalElapsed = expElapsed;
|
|
63
|
-
}
|
|
48
|
+
const _options = { ...defaultOptions, ...options };
|
|
64
49
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
}
|
|
50
|
+
// Early logging decision
|
|
51
|
+
const shouldLog = (_options.logging && !sessionStorage.getItem("_tmjs_l") && Math.random() < 0.0001);
|
|
52
|
+
|
|
53
|
+
// Merge built-in and user-registered components
|
|
54
|
+
const allComponents = { ...tm_component_promises, ...customComponents };
|
|
55
|
+
const { elapsed, resolvedComponents: clientComponentsResult } = await resolveClientComponents(allComponents, _options);
|
|
56
|
+
|
|
57
|
+
// Resolve experimental components only when logging
|
|
58
|
+
let experimentalComponents = {};
|
|
59
|
+
let experimentalElapsed = {};
|
|
60
|
+
if (shouldLog || _options.experimental) {
|
|
61
|
+
const { elapsed: expElapsed, resolvedComponents } = await resolveClientComponents(tm_experimental_component_promises, _options);
|
|
62
|
+
experimentalComponents = resolvedComponents;
|
|
63
|
+
experimentalElapsed = expElapsed;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const apiPromise = _options.api_key ? getApiPromise(_options, clientComponentsResult) : null;
|
|
67
|
+
let apiResult = null;
|
|
85
68
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
69
|
+
if (apiPromise) {
|
|
70
|
+
try {
|
|
71
|
+
apiResult = await apiPromise;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
// Handle API key/quota errors
|
|
74
|
+
if (error instanceof Error && error.message === 'INVALID_API_KEY') {
|
|
75
|
+
return {
|
|
76
|
+
error: 'Invalid API key or quota exceeded',
|
|
77
|
+
components: {},
|
|
78
|
+
info: {},
|
|
79
|
+
version: getVersion(),
|
|
80
|
+
thumbmark: ''
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
throw error; // Re-throw other errors
|
|
97
84
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Only add 'elapsed' if performance is true
|
|
88
|
+
const allElapsed = { ...elapsed, ...experimentalElapsed };
|
|
89
|
+
const maybeElapsed = _options.performance ? { elapsed: allElapsed } : {};
|
|
90
|
+
const apiComponents = filterThumbmarkData(apiResult?.components || {}, _options);
|
|
91
|
+
const components = { ...clientComponentsResult, ...apiComponents };
|
|
92
|
+
const info: infoInterface = apiResult?.info || { uniqueness: { score: 'api only' } };
|
|
93
|
+
|
|
94
|
+
// Use API thumbmark if available to ensure API/client sync, otherwise calculate locally
|
|
95
|
+
console.log(apiResult);
|
|
96
|
+
const thumbmark = apiResult?.thumbmark ?? hash(stableStringify(components));
|
|
97
|
+
const version = getVersion();
|
|
98
|
+
|
|
99
|
+
// Only log to server when not in debug mode
|
|
100
|
+
if (shouldLog) {
|
|
101
|
+
logThumbmarkData(thumbmark, components, _options, experimentalComponents).catch(() => { /* do nothing */ });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const result: thumbmarkResponse = {
|
|
105
|
+
...(apiResult?.visitorId && { visitorId: apiResult.visitorId }),
|
|
106
|
+
thumbmark,
|
|
107
|
+
components: components,
|
|
108
|
+
info,
|
|
109
|
+
version,
|
|
110
|
+
...maybeElapsed,
|
|
111
|
+
...(Object.keys(experimentalComponents).length > 0 && _options.experimental && { experimental: experimentalComponents }),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return result;
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
// ===================== Component Resolution & Performance =====================
|
package/src/index.ts
CHANGED
|
@@ -2,19 +2,24 @@ import {
|
|
|
2
2
|
getFingerprint,
|
|
3
3
|
getFingerprintData,
|
|
4
4
|
getFingerprintPerformance
|
|
5
|
-
|
|
5
|
+
} from './functions/legacy_functions'
|
|
6
6
|
import { getThumbmark } from './functions'
|
|
7
7
|
import { getVersion } from './utils/version';
|
|
8
8
|
import { setOption, optionsInterface, stabilizationExclusionRules } from './options'
|
|
9
9
|
import { includeComponent } from './factory'
|
|
10
10
|
import { Thumbmark } from './thumbmark'
|
|
11
11
|
import { filterThumbmarkData } from './functions/filterComponents'
|
|
12
|
+
import { stableStringify } from './utils/stableStringify'
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
Thumbmark, getThumbmark, getVersion,
|
|
12
16
|
|
|
13
|
-
export { Thumbmark, getThumbmark, getVersion,
|
|
14
|
-
|
|
15
17
|
// Filtering functions for server-side use
|
|
16
18
|
filterThumbmarkData, optionsInterface, stabilizationExclusionRules,
|
|
17
19
|
|
|
20
|
+
// Stable JSON stringify for consistent hashing
|
|
21
|
+
stableStringify,
|
|
22
|
+
|
|
18
23
|
// deprecated functions. Don't use anymore.
|
|
19
24
|
setOption, getFingerprint, getFingerprintData, getFingerprintPerformance, includeComponent
|
|
20
25
|
}
|