@thumbmarkjs/thumbmarkjs 1.3.3 → 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/README.md +1 -1
- package/dist/thumbmark.cjs.js +1 -1
- package/dist/thumbmark.cjs.js.map +1 -1
- package/dist/thumbmark.esm.d.ts +30 -3
- 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 +74 -46
- package/src/components/permissions/index.ts +11 -6
- package/src/components/speech/index.ts +104 -0
- package/src/components/webrtc/index.ts +21 -11
- package/src/factory.ts +3 -1
- package/src/functions/api.ts +10 -7
- package/src/functions/index.ts +80 -72
- package/src/index.ts +8 -3
- package/src/options.ts +3 -10
- package/src/utils/log.ts +3 -3
- package/src/utils/stableStringify.test.ts +335 -0
- package/src/utils/stableStringify.ts +70 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { componentInterface } from '../../factory';
|
|
2
2
|
import { hash } from '../../utils/hash';
|
|
3
|
+
import { ephemeralIFrame } from '../../utils/ephemeralIFrame';
|
|
4
|
+
import { stableStringify } from '../../utils/stableStringify';
|
|
3
5
|
|
|
4
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'];
|
|
5
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'];
|
|
@@ -7,33 +9,58 @@ const GREEK_SYMBOLS = ['\u03B2', '\u03C8', '\u03BB', '\u03B5', '\u03B6', '\u03B1
|
|
|
7
9
|
export default async function getMathML(): Promise<componentInterface | null> {
|
|
8
10
|
return new Promise((resolve) => {
|
|
9
11
|
try {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
ephemeralIFrame(async ({ iframe }) => {
|
|
13
|
+
try {
|
|
14
|
+
if (!isMathMLSupported(iframe)) {
|
|
15
|
+
resolve({
|
|
16
|
+
supported: false,
|
|
17
|
+
error: 'MathML not supported'
|
|
18
|
+
});
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const measurements: any = {};
|
|
27
|
-
structures.forEach((struct, i) => {
|
|
28
|
-
measurements[`struct_${i}`] = measureMathMLStructure(struct);
|
|
29
|
-
});
|
|
22
|
+
const structures = [
|
|
23
|
+
createMathML('integral', '<msubsup><mo>\u222B</mo><mi>a</mi><mi>b</mi></msubsup><mfrac><mrow><mi>f</mi><mo>(</mo><mi>x</mi><mo>)</mo></mrow><mrow><mi>g</mi><mo>(</mo><mi>x</mi><mo>)</mo></mrow></mfrac><mi>dx</mi>'),
|
|
24
|
+
createMathML('fraction', '<mfrac><mrow><mi>\u03C0</mi><mo>\u00D7</mo><msup><mi>r</mi><mn>2</mn></msup></mrow><mrow><mn>2</mn><mi>\u03C3</mi></mrow></mfrac>'),
|
|
25
|
+
createMathML('matrix', '<mo>[</mo><mtable><mtr><mtd><mi>\u03B1</mi></mtd><mtd><mi>\u03B2</mi></mtd></mtr><mtr><mtd><mi>\u03B3</mi></mtd><mtd><mi>\u03B4</mi></mtd></mtr></mtable><mo>]</mo>'),
|
|
26
|
+
createComplexNestedStructure(),
|
|
27
|
+
...createSymbolStructures()
|
|
28
|
+
];
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
const dimensionsArray: any[] = [];
|
|
31
|
+
let fontStyleHash: string = '';
|
|
32
|
+
|
|
33
|
+
structures.forEach((struct, i) => {
|
|
34
|
+
const measurement = measureMathMLStructure(struct, iframe);
|
|
35
|
+
// Extract dimensions for this structure
|
|
36
|
+
dimensionsArray.push({
|
|
37
|
+
width: measurement.dimensions.width,
|
|
38
|
+
height: measurement.dimensions.height
|
|
39
|
+
});
|
|
40
|
+
// Capture font style hash from the first structure (it's the same for all)
|
|
41
|
+
if (i === 0 && measurement.fontInfo) {
|
|
42
|
+
fontStyleHash = hash(stableStringify(measurement.fontInfo));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
36
45
|
|
|
46
|
+
const details = {
|
|
47
|
+
fontStyleHash,
|
|
48
|
+
dimensions: dimensionsArray
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
resolve({
|
|
52
|
+
//supported: true,
|
|
53
|
+
details,
|
|
54
|
+
hash: hash(stableStringify(details))
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
} catch (error) {
|
|
58
|
+
resolve({
|
|
59
|
+
supported: false,
|
|
60
|
+
error: `MathML error: ${(error as Error).message}`
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
37
64
|
} catch (error) {
|
|
38
65
|
resolve({
|
|
39
66
|
supported: false,
|
|
@@ -43,17 +70,17 @@ export default async function getMathML(): Promise<componentInterface | null> {
|
|
|
43
70
|
});
|
|
44
71
|
}
|
|
45
72
|
|
|
46
|
-
function isMathMLSupported(): boolean {
|
|
73
|
+
function isMathMLSupported(iframe: Document): boolean {
|
|
47
74
|
try {
|
|
48
|
-
const testElement =
|
|
75
|
+
const testElement = iframe.createElement('math');
|
|
49
76
|
testElement.innerHTML = '<mrow><mi>x</mi></mrow>';
|
|
50
77
|
testElement.style.position = 'absolute';
|
|
51
78
|
testElement.style.visibility = 'hidden';
|
|
52
|
-
|
|
53
|
-
|
|
79
|
+
|
|
80
|
+
iframe.body.appendChild(testElement);
|
|
54
81
|
const rect = testElement.getBoundingClientRect();
|
|
55
|
-
|
|
56
|
-
|
|
82
|
+
iframe.body.removeChild(testElement);
|
|
83
|
+
|
|
57
84
|
return rect.width > 0 && rect.height > 0;
|
|
58
85
|
} catch {
|
|
59
86
|
return false;
|
|
@@ -66,7 +93,7 @@ function createMathML(name: string, content: string): string {
|
|
|
66
93
|
|
|
67
94
|
function createComplexNestedStructure(): string {
|
|
68
95
|
let nestedContent = '<mo>\u220F</mo>'; // Product symbol (∏)
|
|
69
|
-
|
|
96
|
+
|
|
70
97
|
// Add all symbol combinations inside the main structure
|
|
71
98
|
BLACKBOARD_BOLD.forEach((bbSymbol, bbIndex) => {
|
|
72
99
|
const startIdx = bbIndex * 2;
|
|
@@ -76,7 +103,7 @@ function createComplexNestedStructure(): string {
|
|
|
76
103
|
nestedContent += `<mmultiscripts><mi>${bbSymbol}</mi><none/><mi>${greekSet[1]}</mi><mprescripts></mprescripts><mi>${greekSet[0]}</mi><none/></mmultiscripts>`;
|
|
77
104
|
}
|
|
78
105
|
});
|
|
79
|
-
|
|
106
|
+
|
|
80
107
|
return createMathML('complex_nested',
|
|
81
108
|
`<munderover><mmultiscripts>${nestedContent}</mmultiscripts></munderover>`
|
|
82
109
|
);
|
|
@@ -84,7 +111,7 @@ function createComplexNestedStructure(): string {
|
|
|
84
111
|
|
|
85
112
|
function createSymbolStructures(): string[] {
|
|
86
113
|
const structures: string[] = [];
|
|
87
|
-
|
|
114
|
+
|
|
88
115
|
// Use blackboard bold as base symbols with Greek symbols as subscripts/superscripts
|
|
89
116
|
BLACKBOARD_BOLD.forEach((bbSymbol, bbIndex) => {
|
|
90
117
|
// Get 2 Greek symbols for this blackboard bold symbol (lower left, top right)
|
|
@@ -97,29 +124,30 @@ function createSymbolStructures(): string[] {
|
|
|
97
124
|
));
|
|
98
125
|
}
|
|
99
126
|
});
|
|
100
|
-
|
|
127
|
+
|
|
101
128
|
return structures;
|
|
102
129
|
}
|
|
103
130
|
|
|
104
|
-
function measureMathMLStructure(mathml: string): any {
|
|
131
|
+
function measureMathMLStructure(mathml: string, iframe: Document): any {
|
|
105
132
|
try {
|
|
106
|
-
const mathElement =
|
|
133
|
+
const mathElement = iframe.createElement('math');
|
|
107
134
|
mathElement.innerHTML = mathml.replace(/<\/?math>/g, '');
|
|
108
135
|
mathElement.style.whiteSpace = 'nowrap';
|
|
109
136
|
mathElement.style.position = 'absolute';
|
|
110
137
|
mathElement.style.visibility = 'hidden';
|
|
111
138
|
mathElement.style.top = '-9999px';
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
139
|
+
|
|
140
|
+
iframe.body.appendChild(mathElement);
|
|
141
|
+
|
|
115
142
|
const rect = mathElement.getBoundingClientRect();
|
|
116
|
-
const
|
|
117
|
-
|
|
143
|
+
const iframeWindow = iframe.defaultView || window;
|
|
144
|
+
const computedStyle = iframeWindow.getComputedStyle(mathElement);
|
|
145
|
+
|
|
118
146
|
const measurements = {
|
|
119
147
|
dimensions: {
|
|
120
148
|
width: rect.width,
|
|
121
149
|
height: rect.height,
|
|
122
|
-
|
|
150
|
+
|
|
123
151
|
},
|
|
124
152
|
fontInfo: {
|
|
125
153
|
fontFamily: computedStyle.fontFamily,
|
|
@@ -129,7 +157,7 @@ function measureMathMLStructure(mathml: string): any {
|
|
|
129
157
|
lineHeight: computedStyle.lineHeight,
|
|
130
158
|
// Enhanced font properties for better system detection
|
|
131
159
|
fontVariant: computedStyle.fontVariant || 'normal',
|
|
132
|
-
fontStretch: computedStyle.fontStretch || 'normal',
|
|
160
|
+
fontStretch: computedStyle.fontStretch || 'normal',
|
|
133
161
|
fontSizeAdjust: computedStyle.fontSizeAdjust || 'none',
|
|
134
162
|
textRendering: computedStyle.textRendering || 'auto',
|
|
135
163
|
fontFeatureSettings: computedStyle.fontFeatureSettings || 'normal',
|
|
@@ -137,10 +165,10 @@ function measureMathMLStructure(mathml: string): any {
|
|
|
137
165
|
fontKerning: computedStyle.fontKerning || 'auto'
|
|
138
166
|
}
|
|
139
167
|
};
|
|
140
|
-
|
|
141
|
-
|
|
168
|
+
|
|
169
|
+
iframe.body.removeChild(mathElement);
|
|
142
170
|
return measurements;
|
|
143
|
-
|
|
171
|
+
|
|
144
172
|
} catch (error) {
|
|
145
173
|
return {
|
|
146
174
|
error: (error as Error).message
|
|
@@ -25,13 +25,18 @@ const defaultPermissionKeys: PermissionName[] = [
|
|
|
25
25
|
] as PermissionName[];
|
|
26
26
|
|
|
27
27
|
export default async function getPermissions(options?: optionsInterface): Promise<componentInterface> {
|
|
28
|
-
|
|
28
|
+
const permission_keys = options?.permissions_to_check || defaultPermissionKeys;
|
|
29
29
|
const retries = 3;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
|
|
31
|
+
// Run permission checks multiple times
|
|
32
|
+
const results = await Promise.all(
|
|
33
|
+
Array.from({ length: retries }, () => getBrowserPermissionsOnce(permission_keys))
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Get most frequent values across all retries
|
|
37
|
+
const permissionStatus = mostFrequentValuesInArrayOfDictionaries(results, permission_keys);
|
|
38
|
+
|
|
39
|
+
return permissionStatus;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
async function getBrowserPermissionsOnce(permission_keys: PermissionName[]): Promise<componentInterface> {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { componentInterface } from '../../factory';
|
|
2
|
+
import { hash } from '../../utils/hash';
|
|
3
|
+
import { stableStringify } from '../../utils/stableStringify';
|
|
4
|
+
|
|
5
|
+
const VOICE_LOAD_TIMEOUT = 800; // milliseconds to wait for voices to load
|
|
6
|
+
|
|
7
|
+
export default async function getSpeech(): Promise<componentInterface | null> {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
try {
|
|
10
|
+
// Check if Speech Synthesis API is available
|
|
11
|
+
if (typeof window === 'undefined' || !window.speechSynthesis || typeof window.speechSynthesis.getVoices !== 'function') {
|
|
12
|
+
resolve({
|
|
13
|
+
supported: false,
|
|
14
|
+
error: 'Speech Synthesis API not supported'
|
|
15
|
+
});
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let voicesResolved = false;
|
|
20
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
21
|
+
|
|
22
|
+
const processVoices = (voices: SpeechSynthesisVoice[]) => {
|
|
23
|
+
if (voicesResolved) return;
|
|
24
|
+
voicesResolved = true;
|
|
25
|
+
|
|
26
|
+
// Clear timeout if it exists
|
|
27
|
+
if (timeoutHandle) {
|
|
28
|
+
clearTimeout(timeoutHandle);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Collect voice signatures
|
|
33
|
+
const voiceSignatures = voices.map((voice) => {
|
|
34
|
+
// Escape commas and backslashes in voice properties
|
|
35
|
+
const escapeValue = (value: string): string => {
|
|
36
|
+
return value.replace(/\\/g, '\\\\').replace(/,/g, '\\,');
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Format: voiceURI,name,lang,localService,default
|
|
40
|
+
const signature = [
|
|
41
|
+
escapeValue(voice.voiceURI || ''),
|
|
42
|
+
escapeValue(voice.name || ''),
|
|
43
|
+
escapeValue(voice.lang || ''),
|
|
44
|
+
voice.localService ? '1' : '0',
|
|
45
|
+
voice.default ? '1' : '0'
|
|
46
|
+
].join(',');
|
|
47
|
+
|
|
48
|
+
return signature;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Sort alphabetically for consistent ordering
|
|
52
|
+
voiceSignatures.sort();
|
|
53
|
+
|
|
54
|
+
// Create details object with count and hash
|
|
55
|
+
const details = {
|
|
56
|
+
voiceCount: voices.length,
|
|
57
|
+
voicesHash: hash(stableStringify(voiceSignatures))
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
resolve({
|
|
61
|
+
details,
|
|
62
|
+
hash: hash(stableStringify(details))
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
} catch (error) {
|
|
66
|
+
resolve({
|
|
67
|
+
supported: true,
|
|
68
|
+
error: `Voice processing failed: ${(error as Error).message}`
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Try to get voices immediately
|
|
74
|
+
const voices = window.speechSynthesis.getVoices();
|
|
75
|
+
|
|
76
|
+
// If voices are available immediately, process them
|
|
77
|
+
if (voices.length > 0) {
|
|
78
|
+
processVoices(voices);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Set up timeout in case voices never load
|
|
83
|
+
timeoutHandle = setTimeout(() => {
|
|
84
|
+
const voices = window.speechSynthesis.getVoices();
|
|
85
|
+
processVoices(voices);
|
|
86
|
+
}, VOICE_LOAD_TIMEOUT);
|
|
87
|
+
|
|
88
|
+
// Listen for voiceschanged event (for browsers that load voices asynchronously)
|
|
89
|
+
const onVoicesChanged = () => {
|
|
90
|
+
window.speechSynthesis.removeEventListener('voiceschanged', onVoicesChanged);
|
|
91
|
+
const voices = window.speechSynthesis.getVoices();
|
|
92
|
+
processVoices(voices);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
window.speechSynthesis.addEventListener('voiceschanged', onVoicesChanged);
|
|
96
|
+
|
|
97
|
+
} catch (error) {
|
|
98
|
+
resolve({
|
|
99
|
+
supported: false,
|
|
100
|
+
error: `Speech Synthesis error: ${(error as Error).message}`
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -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
|
|
|
@@ -64,9 +65,19 @@ export default async function getWebRTC(): Promise<componentInterface | null> {
|
|
|
64
65
|
}).filter(Boolean);
|
|
65
66
|
};
|
|
66
67
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
const audioCodecs = constructDescriptions('audio', getDescriptors('audio'));
|
|
69
|
+
const videoCodecs = constructDescriptions('video', getDescriptors('video'));
|
|
70
|
+
|
|
71
|
+
const compressedData = {
|
|
72
|
+
audio: {
|
|
73
|
+
count: audioCodecs.length,
|
|
74
|
+
hash: hash(stableStringify(audioCodecs))
|
|
75
|
+
},
|
|
76
|
+
video: {
|
|
77
|
+
count: videoCodecs.length,
|
|
78
|
+
hash: hash(stableStringify(videoCodecs))
|
|
79
|
+
},
|
|
80
|
+
extensionsHash: hash(stableStringify(extensions))
|
|
70
81
|
};
|
|
71
82
|
|
|
72
83
|
// Set up for ICE candidate collection with timeout
|
|
@@ -74,10 +85,9 @@ export default async function getWebRTC(): Promise<componentInterface | null> {
|
|
|
74
85
|
const timeout = setTimeout(() => {
|
|
75
86
|
connection.removeEventListener('icecandidate', onIceCandidate);
|
|
76
87
|
connection.close();
|
|
77
|
-
resolveResult({
|
|
88
|
+
resolveResult({
|
|
78
89
|
supported: true,
|
|
79
|
-
|
|
80
|
-
extensions: extensions as string[],
|
|
90
|
+
...compressedData,
|
|
81
91
|
timeout: true
|
|
82
92
|
});
|
|
83
93
|
}, 3000);
|
|
@@ -90,10 +100,9 @@ export default async function getWebRTC(): Promise<componentInterface | null> {
|
|
|
90
100
|
connection.removeEventListener('icecandidate', onIceCandidate);
|
|
91
101
|
connection.close();
|
|
92
102
|
|
|
93
|
-
resolveResult({
|
|
103
|
+
resolveResult({
|
|
94
104
|
supported: true,
|
|
95
|
-
|
|
96
|
-
extensions: extensions as string[],
|
|
105
|
+
...compressedData,
|
|
97
106
|
candidateType: candidateObj.type || ''
|
|
98
107
|
});
|
|
99
108
|
};
|
|
@@ -102,7 +111,8 @@ export default async function getWebRTC(): Promise<componentInterface | null> {
|
|
|
102
111
|
});
|
|
103
112
|
|
|
104
113
|
resolve({
|
|
105
|
-
|
|
114
|
+
details: result,
|
|
115
|
+
hash: hash(stableStringify(result)),
|
|
106
116
|
});
|
|
107
117
|
|
|
108
118
|
} catch (error) {
|
package/src/factory.ts
CHANGED
|
@@ -23,6 +23,7 @@ import getWebGL from "./components/webgl";
|
|
|
23
23
|
// Import experimental component functions
|
|
24
24
|
import getWebRTC from "./components/webrtc";
|
|
25
25
|
import getMathML from "./components/mathml";
|
|
26
|
+
import getSpeech from "./components/speech";
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* @description key->function map of built-in components. Do not call the function here.
|
|
@@ -46,7 +47,8 @@ export const tm_component_promises = {
|
|
|
46
47
|
*/
|
|
47
48
|
export const tm_experimental_component_promises = {
|
|
48
49
|
'webrtc': getWebRTC,
|
|
49
|
-
'mathml': getMathML
|
|
50
|
+
'mathml': getMathML,
|
|
51
|
+
'speech': getSpeech
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
// the component interface is the form of the JSON object the function's promise must return
|
package/src/functions/api.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { optionsInterface,
|
|
1
|
+
import { optionsInterface, DEFAULT_API_ENDPOINT } from '../options';
|
|
2
2
|
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 =====================
|
|
@@ -63,18 +65,19 @@ export const getApiPromise = (
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
// 3. Otherwise, initiate a new API call with timeout.
|
|
66
|
-
const
|
|
68
|
+
const apiEndpoint = options.api_endpoint || DEFAULT_API_ENDPOINT;
|
|
69
|
+
const endpoint = `${apiEndpoint}/thumbmark`;
|
|
67
70
|
const visitorId = getVisitorId();
|
|
68
|
-
const requestBody: any = {
|
|
69
|
-
components,
|
|
70
|
-
options,
|
|
71
|
-
clientHash: hash(
|
|
71
|
+
const requestBody: any = {
|
|
72
|
+
components,
|
|
73
|
+
options,
|
|
74
|
+
clientHash: hash(stableStringify(components)),
|
|
72
75
|
version: getVersion()
|
|
73
76
|
};
|
|
74
77
|
if (visitorId) {
|
|
75
78
|
requestBody.visitorId = visitorId;
|
|
76
79
|
}
|
|
77
|
-
|
|
80
|
+
|
|
78
81
|
const fetchPromise = fetch(endpoint, {
|
|
79
82
|
method: 'POST',
|
|
80
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,66 +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
|
-
if (shouldLog || _options.experimental) {
|
|
59
|
-
const { resolvedComponents } = await resolveClientComponents(tm_experimental_component_promises, _options);
|
|
60
|
-
experimentalComponents = resolvedComponents;
|
|
61
|
-
}
|
|
48
|
+
const _options = { ...defaultOptions, ...options };
|
|
62
49
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
}
|
|
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;
|
|
83
68
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
94
84
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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;
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
// ===================== Component Resolution & Performance =====================
|