@thumbmarkjs/thumbmarkjs 1.8.1 → 1.9.1
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 +9 -4
- package/dist/thumbmark.cjs.js +1 -1
- package/dist/thumbmark.cjs.js.map +1 -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/dist/types/components/audio/index.d.ts +2 -0
- package/dist/types/components/canvas/index.d.ts +3 -0
- package/dist/types/components/fonts/index.d.ts +4 -0
- package/dist/types/components/hardware/index.d.ts +2 -0
- package/dist/types/components/intl/index.d.ts +2 -0
- package/dist/types/components/locales/index.d.ts +2 -0
- package/dist/types/components/math/index.d.ts +2 -0
- package/dist/types/components/mathml/index.d.ts +2 -0
- package/dist/types/components/mediaDevices/index.d.ts +2 -0
- package/dist/types/components/permissions/index.d.ts +3 -0
- package/dist/types/components/plugins/index.d.ts +2 -0
- package/dist/types/components/screen/index.d.ts +2 -0
- package/dist/types/components/speech/index.d.ts +2 -0
- package/dist/types/components/system/browser.d.ts +9 -0
- package/dist/types/components/system/index.d.ts +2 -0
- package/dist/types/components/webgl/index.d.ts +13 -0
- package/dist/types/components/webrtc/index.d.ts +3 -0
- package/dist/types/factory.d.ts +66 -0
- package/dist/types/functions/api.d.ts +73 -0
- package/dist/types/functions/filterComponents.d.ts +15 -0
- package/dist/types/functions/index.d.ts +63 -0
- package/dist/types/functions/legacy_functions.d.ts +27 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/options.d.ts +76 -0
- package/dist/types/thumbmark.d.ts +28 -0
- package/dist/types/utils/cache.d.ts +23 -0
- package/dist/types/utils/commonPixels.d.ts +1 -0
- package/dist/types/utils/ephemeralIFrame.d.ts +4 -0
- package/dist/types/utils/getMostFrequent.d.ts +5 -0
- package/dist/types/utils/hash.d.ts +5 -0
- package/dist/types/utils/imageDataToDataURL.d.ts +1 -0
- package/dist/types/utils/log.d.ts +9 -0
- package/dist/types/utils/raceAll.d.ts +10 -0
- package/dist/types/utils/sort.d.ts +8 -0
- package/dist/types/utils/stableStringify.d.ts +22 -0
- package/dist/types/utils/version.d.ts +4 -0
- package/dist/types/utils/visitorId.d.ts +14 -0
- package/package.json +3 -2
- package/src/components/audio/index.ts +0 -1
- package/src/components/system/browser.ts +24 -4
- package/src/components/webgl/index.test.ts +223 -0
- package/src/components/webgl/index.ts +188 -146
- package/src/components/webrtc/index.ts +15 -38
- package/src/functions/filterComponents.test.ts +35 -0
- package/src/functions/filterComponents.ts +2 -3
- package/src/functions/index.ts +70 -10
- package/src/utils/commonPixels.ts +25 -0
- package/src/utils/raceAll.ts +22 -12
- package/src/utils/stableStringify.ts +6 -4
|
@@ -161,6 +161,17 @@ describe('getExcludeList', () => {
|
|
|
161
161
|
// Should not throw
|
|
162
162
|
expect(() => getExcludeList({ ...defaultOptions }, obj)).not.toThrow();
|
|
163
163
|
});
|
|
164
|
+
|
|
165
|
+
test('non-array string exclude does not throw and is ignored', () => {
|
|
166
|
+
const result = getExcludeList({ ...defaultOptions, exclude: 'one' as any, stabilize: [] });
|
|
167
|
+
expect(Array.isArray(result)).toBe(true);
|
|
168
|
+
expect(result).not.toContain('one');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('non-array object exclude does not throw and is ignored', () => {
|
|
172
|
+
const result = getExcludeList({ ...defaultOptions, exclude: {} as any, stabilize: [] });
|
|
173
|
+
expect(Array.isArray(result)).toBe(true);
|
|
174
|
+
});
|
|
164
175
|
});
|
|
165
176
|
|
|
166
177
|
// ── filterThumbmarkData ─────────────────────────────────────────
|
|
@@ -235,4 +246,28 @@ describe('filterThumbmarkData', () => {
|
|
|
235
246
|
expect((result.audio as componentInterface).other).toBe('jkl');
|
|
236
247
|
expect((result.audio as componentInterface).sampleHash).toBeUndefined();
|
|
237
248
|
});
|
|
249
|
+
|
|
250
|
+
test('non-array string include does not throw and does not rescue excluded keys', () => {
|
|
251
|
+
const result = filterThumbmarkData(testData, {
|
|
252
|
+
...defaultOptions,
|
|
253
|
+
exclude: ['one'],
|
|
254
|
+
include: 'one' as any,
|
|
255
|
+
stabilize: [],
|
|
256
|
+
});
|
|
257
|
+
// Bad include is treated as empty, so 'one' stays excluded
|
|
258
|
+
expect(result.one).toBeUndefined();
|
|
259
|
+
expect(result.two).toBe(2);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('null include does not throw and behaves as empty include', () => {
|
|
263
|
+
const result = filterThumbmarkData(testData, {
|
|
264
|
+
...defaultOptions,
|
|
265
|
+
exclude: ['two'],
|
|
266
|
+
include: null as any,
|
|
267
|
+
stabilize: [],
|
|
268
|
+
});
|
|
269
|
+
// null include treated as empty, so 'two' stays excluded
|
|
270
|
+
expect(result.two).toBeUndefined();
|
|
271
|
+
expect(result.one).toBe('1');
|
|
272
|
+
});
|
|
238
273
|
});
|
|
@@ -27,7 +27,7 @@ export function getExcludeList(options?: optionsInterface, obj?: componentInterf
|
|
|
27
27
|
|
|
28
28
|
const name = browser.name.toLowerCase();
|
|
29
29
|
const majorVer = parseInt(browser.version.split('.')[0] || '0', 10);
|
|
30
|
-
const excludeList =
|
|
30
|
+
const excludeList = Array.isArray(options?.exclude) ? [...options.exclude] : [];
|
|
31
31
|
const stabilizationOptions = [...new Set([...(options?.stabilize || []), 'always'])];
|
|
32
32
|
|
|
33
33
|
for (const option of stabilizationOptions) {
|
|
@@ -52,7 +52,7 @@ export function filterThumbmarkData(
|
|
|
52
52
|
options?: optionsInterface,
|
|
53
53
|
): componentInterface {
|
|
54
54
|
const excludeList = getExcludeList(options, obj);
|
|
55
|
-
const includeList = options?.include
|
|
55
|
+
const includeList = Array.isArray(options?.include) ? options.include : [];
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Inner recursive function to perform the actual filtering.
|
|
@@ -84,6 +84,5 @@ export function filterThumbmarkData(
|
|
|
84
84
|
return result;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
// Start the filtering process
|
|
88
87
|
return performFilter(obj);
|
|
89
88
|
}
|
package/src/functions/index.ts
CHANGED
|
@@ -91,16 +91,18 @@ export async function getThumbmark(
|
|
|
91
91
|
...customComponents,
|
|
92
92
|
...instanceCustomComponents,
|
|
93
93
|
} as Record<string, componentFunctionInterface>;
|
|
94
|
-
const { elapsed, resolvedComponents: clientComponentsResult, errors: componentErrors } = await resolveClientComponents(allComponents, _options);
|
|
94
|
+
const { elapsed, resolvedComponents: clientComponentsResult, errors: componentErrors, pipelineTimings: mainPipelineTimings } = await resolveClientComponents(allComponents, _options);
|
|
95
95
|
allErrors.push(...componentErrors);
|
|
96
96
|
|
|
97
97
|
// Resolve experimental components only when logging
|
|
98
98
|
let experimentalComponents = {};
|
|
99
99
|
let experimentalElapsed = {};
|
|
100
|
+
let expPipelineTimings: Record<string, number> = {};
|
|
100
101
|
if (shouldLog || _options.experimental) {
|
|
101
|
-
const { elapsed: expElapsed, resolvedComponents, errors: expErrors } = await resolveClientComponents(tm_experimental_component_promises, _options);
|
|
102
|
+
const { elapsed: expElapsed, resolvedComponents, errors: expErrors, pipelineTimings: expTimings } = await resolveClientComponents(tm_experimental_component_promises, _options);
|
|
102
103
|
experimentalComponents = resolvedComponents;
|
|
103
104
|
experimentalElapsed = expElapsed;
|
|
105
|
+
expPipelineTimings = expTimings;
|
|
104
106
|
allErrors.push(...expErrors);
|
|
105
107
|
}
|
|
106
108
|
|
|
@@ -132,15 +134,29 @@ export async function getThumbmark(
|
|
|
132
134
|
allErrors.push({ type: 'api_timeout', message: 'API request timed out' });
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
|
|
136
|
-
const allElapsed = { ...elapsed, ...experimentalElapsed };
|
|
137
|
-
const maybeElapsed = _options.performance ? { elapsed: allElapsed } : {};
|
|
137
|
+
const filterStart = performance.now();
|
|
138
138
|
const apiComponents = filterThumbmarkData(apiResult?.components || {}, _options);
|
|
139
|
+
const filterMs = performance.now() - filterStart;
|
|
140
|
+
|
|
139
141
|
const components = { ...clientComponentsResult, ...apiComponents };
|
|
140
142
|
const info: infoInterface = apiResult?.info || { uniqueness: { score: 'api only' } };
|
|
141
143
|
|
|
142
144
|
// Use API thumbmark if available to ensure API/client sync, otherwise calculate locally
|
|
143
|
-
|
|
145
|
+
let thumbmark: string;
|
|
146
|
+
let stringifyMs = 0;
|
|
147
|
+
let hashMs = 0;
|
|
148
|
+
if (apiResult?.thumbmark) {
|
|
149
|
+
thumbmark = apiResult.thumbmark;
|
|
150
|
+
} else {
|
|
151
|
+
const stringifyStart = performance.now();
|
|
152
|
+
const stringified = stableStringify(components);
|
|
153
|
+
stringifyMs = performance.now() - stringifyStart;
|
|
154
|
+
|
|
155
|
+
const hashStart = performance.now();
|
|
156
|
+
thumbmark = hash(stringified);
|
|
157
|
+
hashMs = performance.now() - hashStart;
|
|
158
|
+
}
|
|
159
|
+
|
|
144
160
|
const version = getVersion();
|
|
145
161
|
|
|
146
162
|
// Only log to server when not in debug mode
|
|
@@ -148,6 +164,26 @@ export async function getThumbmark(
|
|
|
148
164
|
logThumbmarkData(thumbmark, components, _options, experimentalComponents, allErrors).catch(() => { /* do nothing */ });
|
|
149
165
|
}
|
|
150
166
|
|
|
167
|
+
// Accumulate _pipeline timings from both the main and (if run) experimental component phases.
|
|
168
|
+
// Filter time includes: main component filter + optional experimental filter + apiComponents filter.
|
|
169
|
+
const expFilterMs = expPipelineTimings['_pipeline.filter'] ?? 0;
|
|
170
|
+
const _pipelineTimings: Record<string, number> = {
|
|
171
|
+
'_pipeline.dispatch': mainPipelineTimings['_pipeline.dispatch'],
|
|
172
|
+
'_pipeline.resolve': mainPipelineTimings['_pipeline.resolve'],
|
|
173
|
+
'_pipeline.filter': mainPipelineTimings['_pipeline.filter'] + expFilterMs + filterMs,
|
|
174
|
+
'_pipeline.stringify': stringifyMs,
|
|
175
|
+
'_pipeline.hash': hashMs,
|
|
176
|
+
'_pipeline.assembly': 0, // placeholder, updated below after result construction
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Only add 'elapsed' if performance is true
|
|
180
|
+
// allElapsed holds a live reference to _pipelineTimings entries via spread — we update assembly after.
|
|
181
|
+
// mainPipelineTimings contains both _pipeline.* keys (overridden by _pipelineTimings below) and
|
|
182
|
+
// _dispatch.<name> keys (per-component sync prelude timings) that flow through unchanged.
|
|
183
|
+
const allElapsed: Record<string, number> = { ...elapsed, ...experimentalElapsed, ...mainPipelineTimings, ..._pipelineTimings };
|
|
184
|
+
const maybeElapsed = _options.performance ? { elapsed: allElapsed } : {};
|
|
185
|
+
|
|
186
|
+
const assemblyStart = performance.now();
|
|
151
187
|
const result: ThumbmarkResponse = {
|
|
152
188
|
...(apiResult?.visitorId && { visitorId: apiResult.visitorId }),
|
|
153
189
|
thumbmark,
|
|
@@ -160,6 +196,8 @@ export async function getThumbmark(
|
|
|
160
196
|
...(apiResult?.requestId && { requestId: apiResult.requestId }),
|
|
161
197
|
...(apiResult?.metadata && { metadata: apiResult.metadata }),
|
|
162
198
|
};
|
|
199
|
+
// Update assembly timing in allElapsed directly (allElapsed is the same object referenced by result.elapsed).
|
|
200
|
+
allElapsed['_pipeline.assembly'] = performance.now() - assemblyStart;
|
|
163
201
|
|
|
164
202
|
return result;
|
|
165
203
|
} catch (e) {
|
|
@@ -180,12 +218,12 @@ export async function getThumbmark(
|
|
|
180
218
|
*
|
|
181
219
|
* @param comps - Map of component functions
|
|
182
220
|
* @param options - Options for filtering and timing
|
|
183
|
-
* @returns Object with elapsed times
|
|
221
|
+
* @returns Object with elapsed times, filtered resolved components, errors, and pipeline phase timings
|
|
184
222
|
*/
|
|
185
223
|
export async function resolveClientComponents(
|
|
186
224
|
comps: { [key: string]: (options?: optionsInterface) => Promise<componentInterface | null> },
|
|
187
225
|
options?: optionsInterface
|
|
188
|
-
): Promise<{ elapsed: Record<string, number>, resolvedComponents: componentInterface, errors: ThumbmarkError[] }> {
|
|
226
|
+
): Promise<{ elapsed: Record<string, number>, resolvedComponents: componentInterface, errors: ThumbmarkError[], pipelineTimings: Record<string, number> }> {
|
|
189
227
|
const opts = { ...defaultOptions, ...options };
|
|
190
228
|
const topLevelExcludes = getExcludeList(opts).filter(e => !e.includes('.'));
|
|
191
229
|
const filtered = Object.entries(comps)
|
|
@@ -197,8 +235,20 @@ export async function resolveClientComponents(
|
|
|
197
235
|
: opts?.include?.length === 0 || opts?.include?.includes(key)
|
|
198
236
|
);
|
|
199
237
|
const keys = filtered.map(([key]) => key);
|
|
200
|
-
|
|
238
|
+
|
|
239
|
+
const perComponentDispatch: Record<string, number> = {};
|
|
240
|
+
const dispatchStart = performance.now();
|
|
241
|
+
const promises = filtered.map(([key, fn]) => {
|
|
242
|
+
const t0 = performance.now();
|
|
243
|
+
const p = fn(options);
|
|
244
|
+
perComponentDispatch[`_dispatch.${key}`] = performance.now() - t0;
|
|
245
|
+
return p;
|
|
246
|
+
});
|
|
247
|
+
const dispatchMs = performance.now() - dispatchStart;
|
|
248
|
+
|
|
249
|
+
const resolveStart = performance.now();
|
|
201
250
|
const resolvedValues = await raceAllPerformance(promises, opts?.timeout || 5000, timeoutInstance);
|
|
251
|
+
const resolveMs = performance.now() - resolveStart;
|
|
202
252
|
|
|
203
253
|
const elapsed: Record<string, number> = {};
|
|
204
254
|
const resolvedComponentsRaw: Record<string, componentInterface> = {};
|
|
@@ -219,8 +269,18 @@ export async function resolveClientComponents(
|
|
|
219
269
|
}
|
|
220
270
|
});
|
|
221
271
|
|
|
272
|
+
const filterStart = performance.now();
|
|
222
273
|
const resolvedComponents = filterThumbmarkData(resolvedComponentsRaw, opts);
|
|
223
|
-
|
|
274
|
+
const filterMs = performance.now() - filterStart;
|
|
275
|
+
|
|
276
|
+
const pipelineTimings: Record<string, number> = {
|
|
277
|
+
'_pipeline.dispatch': dispatchMs,
|
|
278
|
+
'_pipeline.resolve': resolveMs,
|
|
279
|
+
'_pipeline.filter': filterMs,
|
|
280
|
+
...perComponentDispatch,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
return { elapsed, resolvedComponents, errors, pipelineTimings };
|
|
224
284
|
}
|
|
225
285
|
|
|
226
286
|
export { globalIncludeComponent as includeComponent };
|
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
export function getCommonPixels(images: ImageData[], width: number, height: number ): ImageData {
|
|
2
|
+
// Short-circuit: single image — every byte is trivially its own mode.
|
|
3
|
+
if (images.length === 1) {
|
|
4
|
+
return images[0];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Fast path: exactly 3 images — inline 3-way comparison that mirrors
|
|
8
|
+
// getMostFrequent's tie-breaking exactly for all 5 value-combination cases:
|
|
9
|
+
// all-equal → that value
|
|
10
|
+
// a===b → a (freq 2)
|
|
11
|
+
// a===c → a (freq 2)
|
|
12
|
+
// b===c → b (freq 2)
|
|
13
|
+
// all-diff → a (mostFrequent stays arr[0], no key beats freq 1)
|
|
14
|
+
if (images.length === 3) {
|
|
15
|
+
const a = images[0].data;
|
|
16
|
+
const b = images[1].data;
|
|
17
|
+
const c = images[2].data;
|
|
18
|
+
const out = new Uint8ClampedArray(a.length);
|
|
19
|
+
for (let i = 0; i < a.length; i++) {
|
|
20
|
+
const x = a[i], y = b[i], z = c[i];
|
|
21
|
+
out[i] = (x === y) ? x : (x === z) ? x : (y === z) ? y : x;
|
|
22
|
+
}
|
|
23
|
+
return new ImageData(out, width, height);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Generic fallback for any other length (no current caller uses this path).
|
|
2
27
|
let finalData: number[] = [];
|
|
3
28
|
for (let i = 0; i < images[0].data.length; i++) {
|
|
4
29
|
let indice: number[] = [];
|
package/src/utils/raceAll.ts
CHANGED
|
@@ -21,21 +21,31 @@ export function raceAllPerformance<T>(
|
|
|
21
21
|
return Promise.all(
|
|
22
22
|
promises.map((p) => {
|
|
23
23
|
const startTime = performance.now();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
25
|
+
|
|
26
|
+
const resultPromise = p.then((value) => {
|
|
27
|
+
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
28
|
+
return { value, elapsed: performance.now() - startTime };
|
|
29
|
+
}).catch((err: unknown) => {
|
|
30
|
+
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
31
|
+
return {
|
|
29
32
|
value: timeoutVal,
|
|
30
33
|
elapsed: performance.now() - startTime,
|
|
31
34
|
error: err instanceof Error ? err.message : String(err),
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const timeoutPromise = new Promise<RaceResult<T>>((resolve) => {
|
|
39
|
+
timeoutId = setTimeout(() => {
|
|
40
|
+
resolve({
|
|
41
|
+
value: timeoutVal,
|
|
42
|
+
elapsed: performance.now() - startTime,
|
|
43
|
+
error: 'timeout',
|
|
44
|
+
});
|
|
45
|
+
}, timeoutTime);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return Promise.race([resultPromise, timeoutPromise]);
|
|
39
49
|
})
|
|
40
50
|
);
|
|
41
51
|
}
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
*/
|
|
23
23
|
export function stableStringify(data: any): string {
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// Use a Set for O(1) cycle detection instead of O(N) array indexOf.
|
|
26
|
+
// The serialisation logic (key sorting, value recursion, output format) is unchanged.
|
|
27
|
+
const seen = new Set<any>();
|
|
26
28
|
|
|
27
29
|
return (function stringify(node: any): string | undefined {
|
|
28
30
|
if (node && node.toJSON && typeof node.toJSON === 'function') {
|
|
@@ -47,11 +49,11 @@ export function stableStringify(data: any): string {
|
|
|
47
49
|
|
|
48
50
|
if (node === null) return 'null';
|
|
49
51
|
|
|
50
|
-
if (seen.
|
|
52
|
+
if (seen.has(node)) {
|
|
51
53
|
throw new TypeError('Converting circular structure to JSON');
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
seen.add(node);
|
|
55
57
|
const keys = Object.keys(node).sort();
|
|
56
58
|
out = '';
|
|
57
59
|
|
|
@@ -64,7 +66,7 @@ export function stableStringify(data: any): string {
|
|
|
64
66
|
out += JSON.stringify(key) + ':' + value;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
seen.
|
|
69
|
+
seen.delete(node);
|
|
68
70
|
return '{' + out + '}';
|
|
69
71
|
})(data) || '';
|
|
70
72
|
}
|