@thumbmarkjs/thumbmarkjs 1.5.1 → 1.6.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 +4 -4
- package/dist/thumbmark.cjs.js +1 -1
- package/dist/thumbmark.cjs.js.map +1 -1
- package/dist/thumbmark.esm.d.ts +19 -2
- 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/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/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 +7 -0
- package/dist/types/components/system/index.d.ts +2 -0
- package/dist/types/components/webgl/index.d.ts +2 -0
- package/dist/types/components/webrtc/index.d.ts +2 -0
- package/dist/types/factory.d.ts +62 -0
- package/dist/types/functions/api.d.ts +50 -0
- package/dist/types/functions/filterComponents.d.ts +10 -0
- package/dist/types/functions/index.d.ts +45 -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 +59 -0
- package/dist/types/thumbmark.d.ts +26 -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 +8 -0
- package/dist/types/utils/raceAll.d.ts +9 -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 +1 -1
- package/src/functions/api.test.ts +71 -0
- package/src/functions/api.ts +49 -5
- package/src/functions/index.ts +2 -2
- package/src/options.test.ts +10 -0
- package/src/options.ts +30 -4
- package/src/utils/cache.test.ts +94 -0
- package/src/utils/cache.ts +59 -0
- package/src/utils/visitorId.test.ts +12 -3
- package/src/utils/visitorId.ts +31 -8
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function imageDataToDataURL(imageData: ImageData): string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { componentInterface } from '../factory';
|
|
2
|
+
import { optionsInterface } from '../options';
|
|
3
|
+
/**
|
|
4
|
+
* Logs thumbmark data to remote logging endpoint (only once per session)
|
|
5
|
+
* You can disable this by setting options.logging to false.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export declare function logThumbmarkData(thisHash: string, thumbmarkData: componentInterface, options: optionsInterface, experimentalData?: componentInterface): Promise<void>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type DelayedPromise<T> = Promise<T>;
|
|
2
|
+
export declare function delay<T>(t: number, val: T): DelayedPromise<T>;
|
|
3
|
+
export interface RaceResult<T> {
|
|
4
|
+
value: T;
|
|
5
|
+
elapsed?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function raceAllPerformance<T>(promises: Promise<T>[], timeoutTime: number, timeoutVal: T): Promise<RaceResult<T>[]>;
|
|
8
|
+
export declare function raceAll<T>(promises: Promise<T>[], timeoutTime: number, timeoutVal: T): Promise<(T | undefined)[]>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { componentInterface } from '../factory';
|
|
2
|
+
/**
|
|
3
|
+
* Recursively sorts the keys of a component object alphabetically.
|
|
4
|
+
* This ensures a consistent order for hashing.
|
|
5
|
+
* @param obj The component object to sort.
|
|
6
|
+
* @returns A new object with sorted keys.
|
|
7
|
+
*/
|
|
8
|
+
export declare function sortComponentKeys(obj: componentInterface): componentInterface;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable JSON stringify implementation
|
|
3
|
+
* Based on fast-json-stable-stringify by Evgeny Poberezkin
|
|
4
|
+
* https://github.com/epoberezkin/fast-json-stable-stringify
|
|
5
|
+
*
|
|
6
|
+
* This implementation ensures consistent JSON serialization by sorting object keys,
|
|
7
|
+
* which is critical for generating stable hashes from fingerprint data.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Converts data to a stable JSON string with sorted keys
|
|
11
|
+
*
|
|
12
|
+
* @param data - The data to stringify
|
|
13
|
+
* @returns Stable JSON string representation
|
|
14
|
+
* @throws TypeError if circular reference is detected
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const obj = { b: 2, a: 1 };
|
|
19
|
+
* stableStringify(obj); // '{"a":1,"b":2}'
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function stableStringify(data: any): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { OptionsAfterDefaults } from "../options";
|
|
2
|
+
/**
|
|
3
|
+
* Get the storage property name for visitor id
|
|
4
|
+
* @param _options
|
|
5
|
+
*/
|
|
6
|
+
export declare function getVisitorIdPropertyName(_options: Pick<OptionsAfterDefaults, 'storage_property_name' | 'property_name_factory'>): string;
|
|
7
|
+
/**
|
|
8
|
+
* Gets visitor ID from localStorage, returns null if unavailable
|
|
9
|
+
*/
|
|
10
|
+
export declare function getVisitorId(_options: OptionsAfterDefaults): string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Sets visitor ID in localStorage
|
|
13
|
+
*/
|
|
14
|
+
export declare function setVisitorId(visitorId: string, _options: OptionsAfterDefaults): void;
|
package/package.json
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {apiResponse, getCachedApiResponse, setCachedApiResponse} from "./api";
|
|
2
|
+
import { defaultOptions } from "../options";
|
|
3
|
+
import {getCache, setCache} from "../utils/cache";
|
|
4
|
+
|
|
5
|
+
const options = {
|
|
6
|
+
...defaultOptions,
|
|
7
|
+
cache_lifetime_in_ms: 100,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const apiResponse = {
|
|
11
|
+
identifier: 'test'
|
|
12
|
+
} as apiResponse;
|
|
13
|
+
|
|
14
|
+
describe('setCachedApiResponse', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
localStorage.clear();
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('it should write the apiResponse to the cache if options allow that', () => {
|
|
20
|
+
setCachedApiResponse(options, apiResponse);
|
|
21
|
+
expect(getCache(options).apiResponse).toEqual(apiResponse);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('it should not write if cache if off', () => {
|
|
25
|
+
setCachedApiResponse({
|
|
26
|
+
...options,
|
|
27
|
+
cache_api_call: false,
|
|
28
|
+
}, apiResponse);
|
|
29
|
+
|
|
30
|
+
expect(getCache(options).apiResponse).not.toBeDefined();
|
|
31
|
+
expect(getCache(options).apiResponseExpiry).not.toBeDefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('it should not write if lifetime is 0', () => {
|
|
35
|
+
setCachedApiResponse({
|
|
36
|
+
...options,
|
|
37
|
+
cache_lifetime_in_ms: 0
|
|
38
|
+
}, apiResponse);
|
|
39
|
+
|
|
40
|
+
expect(getCache(options).apiResponse).not.toBeDefined();
|
|
41
|
+
expect(getCache(options).apiResponseExpiry).not.toBeDefined();
|
|
42
|
+
});
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('getCachedApiResponse', () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
localStorage.clear();
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('it should get from the cache if a value exists there', () => {
|
|
51
|
+
setCache(options, {
|
|
52
|
+
apiResponseExpiry: Date.now() + 2000000,
|
|
53
|
+
apiResponse,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const cached = getCachedApiResponse(options);
|
|
57
|
+
expect(cached).toBeDefined();
|
|
58
|
+
expect(cached).toEqual(apiResponse);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('it should not return an expiried cache', () => {
|
|
62
|
+
setCache(options, {
|
|
63
|
+
apiResponseExpiry: Date.now() - 2000,
|
|
64
|
+
apiResponse,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const cached = getCachedApiResponse(options);
|
|
68
|
+
expect(cached).not.toBeDefined();
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
})
|
package/src/functions/api.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { optionsInterface, DEFAULT_API_ENDPOINT } from '../options';
|
|
1
|
+
import { optionsInterface, DEFAULT_API_ENDPOINT, OptionsAfterDefaults} 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
6
|
import { stableStringify } from '../utils/stableStringify';
|
|
7
|
+
import { getCache, getApiResponseExpiry, setCache } from "../utils/cache";
|
|
7
8
|
|
|
8
9
|
// ===================== Types & Interfaces =====================
|
|
9
10
|
|
|
@@ -33,7 +34,7 @@ export interface infoInterface {
|
|
|
33
34
|
/**
|
|
34
35
|
* API response structure
|
|
35
36
|
*/
|
|
36
|
-
interface apiResponse {
|
|
37
|
+
export interface apiResponse {
|
|
37
38
|
info?: infoInterface;
|
|
38
39
|
version?: string;
|
|
39
40
|
components?: componentInterface;
|
|
@@ -51,12 +52,21 @@ let apiPromiseResult: apiResponse | null = null;
|
|
|
51
52
|
* Returns a promise for the API response or null on error.
|
|
52
53
|
*/
|
|
53
54
|
export const getApiPromise = (
|
|
54
|
-
options:
|
|
55
|
+
options: OptionsAfterDefaults,
|
|
55
56
|
components: componentInterface
|
|
56
57
|
): Promise<apiResponse | null> => {
|
|
57
58
|
// 1. If a result is already cached and caching is enabled, return it.
|
|
58
|
-
if (options.cache_api_call
|
|
59
|
-
|
|
59
|
+
if (options.cache_api_call) {
|
|
60
|
+
// Check the in-memory cache
|
|
61
|
+
if(apiPromiseResult) {
|
|
62
|
+
return Promise.resolve(apiPromiseResult);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check the localStorage cache
|
|
66
|
+
const cached = getCachedApiResponse(options);
|
|
67
|
+
if(cached) {
|
|
68
|
+
return Promise.resolve(cached);
|
|
69
|
+
}
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
// 2. If a request is already in flight, return that promise to prevent duplicate calls.
|
|
@@ -103,6 +113,7 @@ export const getApiPromise = (
|
|
|
103
113
|
setVisitorId(data.visitorId, options);
|
|
104
114
|
}
|
|
105
115
|
apiPromiseResult = data; // Cache the successful result
|
|
116
|
+
setCachedApiResponse(options, data); // Cache to localStorage according to options
|
|
106
117
|
currentApiPromise = null; // Clear the in-flight promise
|
|
107
118
|
return data;
|
|
108
119
|
})
|
|
@@ -131,3 +142,36 @@ export const getApiPromise = (
|
|
|
131
142
|
currentApiPromise = Promise.race([fetchPromise, timeoutPromise]);
|
|
132
143
|
return currentApiPromise;
|
|
133
144
|
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* If a valid cached api response exists, returns it
|
|
148
|
+
* @param options
|
|
149
|
+
*/
|
|
150
|
+
export function getCachedApiResponse(
|
|
151
|
+
options: Pick<OptionsAfterDefaults, 'property_name_factory'>,
|
|
152
|
+
): apiResponse | undefined {
|
|
153
|
+
const cache = getCache(options);
|
|
154
|
+
if (cache && cache.apiResponse && cache.apiResponseExpiry && Date.now() <= cache.apiResponseExpiry) {
|
|
155
|
+
return cache.apiResponse;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Writes the api response to the cache according to the options
|
|
163
|
+
* @param options
|
|
164
|
+
* @param response
|
|
165
|
+
*/
|
|
166
|
+
export function setCachedApiResponse(
|
|
167
|
+
options: Pick<OptionsAfterDefaults, 'cache_api_call' | 'cache_lifetime_in_ms' | 'property_name_factory'>, response: apiResponse
|
|
168
|
+
): void {
|
|
169
|
+
if(!options.cache_api_call || !options.cache_lifetime_in_ms) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
setCache(options, {
|
|
174
|
+
apiResponseExpiry: getApiResponseExpiry(options),
|
|
175
|
+
apiResponse: response,
|
|
176
|
+
});
|
|
177
|
+
}
|
package/src/functions/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {defaultOptions, OptionsAfterDefaults, optionsInterface} from "../options";
|
|
10
10
|
import {
|
|
11
11
|
timeoutInstance,
|
|
12
12
|
componentInterface,
|
|
@@ -45,7 +45,7 @@ interface thumbmarkResponse {
|
|
|
45
45
|
* @returns thumbmarkResponse (elapsed is present only if options.performance is true)
|
|
46
46
|
*/
|
|
47
47
|
export async function getThumbmark(options?: optionsInterface): Promise<thumbmarkResponse> {
|
|
48
|
-
const _options = { ...defaultOptions, ...options };
|
|
48
|
+
const _options = { ...defaultOptions, ...options } as OptionsAfterDefaults;
|
|
49
49
|
|
|
50
50
|
// Early logging decision
|
|
51
51
|
const shouldLog = (_options.logging && !sessionStorage.getItem("_tmjs_l") && Math.random() < 0.0001);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DEFAULT_STORAGE_PREFIX, defaultOptions } from "./options";
|
|
2
|
+
|
|
3
|
+
describe('property_name_factory', () => {
|
|
4
|
+
test('it should default to the default value', () => {
|
|
5
|
+
const name = 'mykey';
|
|
6
|
+
expect(
|
|
7
|
+
defaultOptions.property_name_factory(name)
|
|
8
|
+
).toEqual(`${DEFAULT_STORAGE_PREFIX}_${name}`);
|
|
9
|
+
})
|
|
10
|
+
})
|
package/src/options.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface OptionsAfterDefaults {
|
|
2
|
+
/**
|
|
3
|
+
* A function to customise localStorage names used by thumbmark
|
|
4
|
+
* @param name Original name of the storage property eg. visitor_id
|
|
5
|
+
* @returns The name under which the storage property should be saved eg. myprefix_visitor_id
|
|
6
|
+
*/
|
|
7
|
+
property_name_factory: (name: string) => string,
|
|
8
|
+
/**
|
|
9
|
+
* @deprecated use property_name_factory
|
|
10
|
+
*/
|
|
2
11
|
storage_property_name?: string,
|
|
3
12
|
exclude?: string[],
|
|
4
13
|
include?: string[],
|
|
@@ -7,32 +16,49 @@ export interface optionsInterface {
|
|
|
7
16
|
logging?: boolean,
|
|
8
17
|
api_key?: string,
|
|
9
18
|
api_endpoint?: string,
|
|
19
|
+
/**
|
|
20
|
+
* @deprecated This will be removed in Thumbmarkjs 2.0, use cache_lifetime_in_ms instead
|
|
21
|
+
*/
|
|
10
22
|
cache_api_call?: boolean,
|
|
23
|
+
/**
|
|
24
|
+
* How long the cache will be valid for, maximum is 72h (259_200_000)
|
|
25
|
+
*/
|
|
26
|
+
cache_lifetime_in_ms: number,
|
|
11
27
|
performance?: boolean,
|
|
12
28
|
stabilize?: string[],
|
|
13
29
|
experimental?: boolean,
|
|
14
30
|
}
|
|
15
31
|
|
|
32
|
+
export type optionsInterface = Partial<OptionsAfterDefaults>;
|
|
33
|
+
|
|
34
|
+
// Default to zero to avoid breaking existing integrations
|
|
35
|
+
export const DEFAULT_CACHE_LIFETIME = 0;
|
|
36
|
+
export const MAXIMUM_CACHE_LIFETIME = 259_200_000;
|
|
37
|
+
export const DEFAULT_STORAGE_PREFIX = 'thumbmark';
|
|
16
38
|
export const DEFAULT_API_ENDPOINT = 'https://api.thumbmarkjs.com';
|
|
17
39
|
|
|
18
|
-
export const defaultOptions:
|
|
40
|
+
export const defaultOptions: OptionsAfterDefaults = {
|
|
19
41
|
exclude: [],
|
|
20
42
|
include: [],
|
|
21
43
|
stabilize: ['private', 'iframe'],
|
|
22
44
|
logging: true,
|
|
23
45
|
timeout: 5000,
|
|
24
46
|
cache_api_call: true,
|
|
47
|
+
cache_lifetime_in_ms: DEFAULT_CACHE_LIFETIME,
|
|
25
48
|
performance: false,
|
|
26
49
|
experimental: false,
|
|
50
|
+
property_name_factory: (name: string) => {
|
|
51
|
+
return `${DEFAULT_STORAGE_PREFIX}_${name}`;
|
|
52
|
+
},
|
|
27
53
|
};
|
|
28
54
|
|
|
29
|
-
export let options = { ...defaultOptions };
|
|
55
|
+
export let options: OptionsAfterDefaults = { ...defaultOptions };
|
|
30
56
|
/**
|
|
31
57
|
*
|
|
32
58
|
* @param key @deprecated this function will be removed
|
|
33
59
|
* @param value
|
|
34
60
|
*/
|
|
35
|
-
export function setOption<K extends keyof optionsInterface>(key: K, value:
|
|
61
|
+
export function setOption<K extends keyof optionsInterface>(key: K, value: OptionsAfterDefaults[K]) {
|
|
36
62
|
options[key] = value;
|
|
37
63
|
}
|
|
38
64
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Cache, getCache, setCache, getApiResponseExpiry, CACHE_KEY } from "./cache";
|
|
2
|
+
import { DEFAULT_STORAGE_PREFIX, defaultOptions, MAXIMUM_CACHE_LIFETIME } from "../options";
|
|
3
|
+
|
|
4
|
+
const _options = {
|
|
5
|
+
property_name_factory: (name: string) => {
|
|
6
|
+
return `${name}-mypostfix`;
|
|
7
|
+
},
|
|
8
|
+
cache_lifetime_in_ms: 0,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const values: Cache = {
|
|
12
|
+
apiResponseExpiry: 100,
|
|
13
|
+
apiResponse: {
|
|
14
|
+
version: '1.2.3',
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const cacheProperty = defaultOptions.property_name_factory(CACHE_KEY);
|
|
19
|
+
const customCacheProperty = _options.property_name_factory(CACHE_KEY);
|
|
20
|
+
|
|
21
|
+
describe('getCache', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
localStorage.clear();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('it should return all values from cache', () => {
|
|
27
|
+
localStorage.setItem(cacheProperty, JSON.stringify(values));
|
|
28
|
+
const fromStorage = getCache(defaultOptions);
|
|
29
|
+
|
|
30
|
+
expect(fromStorage).toEqual(values);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('it should return an empty object in case of nothing cached', () => {
|
|
34
|
+
expect(getCache(defaultOptions)).toEqual({});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('it should return an empty object in case non-json content', () => {
|
|
38
|
+
localStorage.setItem(cacheProperty, 'abc123');
|
|
39
|
+
expect(getCache(defaultOptions)).toEqual({});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('it should return from the correct property', () => {
|
|
43
|
+
localStorage.setItem(cacheProperty, JSON.stringify(values));
|
|
44
|
+
|
|
45
|
+
expect(getCache(_options)).toEqual({});
|
|
46
|
+
expect(getCache(defaultOptions)).toEqual(values);
|
|
47
|
+
});
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('setCache', () => {
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
localStorage.clear();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('it should write given values', () => {
|
|
56
|
+
setCache(defaultOptions, values);
|
|
57
|
+
expect(JSON.parse(localStorage.getItem(cacheProperty)!))
|
|
58
|
+
.toEqual(values);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('it should not touch values that are not provided', () => {
|
|
62
|
+
setCache(_options, values);
|
|
63
|
+
setCache(_options, {
|
|
64
|
+
apiResponseExpiry: 200
|
|
65
|
+
});
|
|
66
|
+
const fromStorage = JSON.parse(localStorage.getItem(customCacheProperty)!) as Cache;
|
|
67
|
+
expect(fromStorage.apiResponseExpiry).toEqual(200);
|
|
68
|
+
expect(fromStorage.apiResponse!.version).toEqual(values.apiResponse!.version);
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('getApiResponseExpiry', () => {
|
|
73
|
+
const systemTime = 1000;
|
|
74
|
+
beforeAll(() => {
|
|
75
|
+
jest.useFakeTimers().setSystemTime(systemTime);
|
|
76
|
+
localStorage.clear();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('it should return a value based on options', () => {
|
|
80
|
+
expect(getApiResponseExpiry({
|
|
81
|
+
cache_lifetime_in_ms: 100,
|
|
82
|
+
})).toBe(systemTime + 100);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('it should not go over maximum cache lifetime', () => {
|
|
86
|
+
expect(getApiResponseExpiry({
|
|
87
|
+
cache_lifetime_in_ms: MAXIMUM_CACHE_LIFETIME + 200,
|
|
88
|
+
})).toBe(systemTime + MAXIMUM_CACHE_LIFETIME);
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
afterAll(() => {
|
|
92
|
+
jest.useRealTimers()
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { apiResponse } from "../functions/api";
|
|
2
|
+
import { MAXIMUM_CACHE_LIFETIME, OptionsAfterDefaults } from "../options";
|
|
3
|
+
|
|
4
|
+
export const CACHE_KEY = 'cache';
|
|
5
|
+
|
|
6
|
+
export interface Cache {
|
|
7
|
+
apiResponse?: apiResponse
|
|
8
|
+
apiResponseExpiry?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get all values from cache
|
|
13
|
+
* @param _options
|
|
14
|
+
*/
|
|
15
|
+
export function getCache(_options: Pick<OptionsAfterDefaults, 'property_name_factory'>): Cache {
|
|
16
|
+
try {
|
|
17
|
+
const rawCache = localStorage.getItem(_options.property_name_factory(CACHE_KEY));
|
|
18
|
+
const jsonCache = JSON.parse(rawCache!);
|
|
19
|
+
if(!jsonCache) {
|
|
20
|
+
return {};
|
|
21
|
+
} else {
|
|
22
|
+
return jsonCache as Cache;
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Ignore storage errors
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Write given values to cache
|
|
33
|
+
* @param _options
|
|
34
|
+
* @param values
|
|
35
|
+
*/
|
|
36
|
+
export function setCache(_options: OptionsAfterDefaults, values: Partial<Cache>): void {
|
|
37
|
+
const newValues: Cache = {
|
|
38
|
+
...getCache(_options),
|
|
39
|
+
...values
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
localStorage.setItem(_options.property_name_factory(CACHE_KEY), JSON.stringify(newValues));
|
|
44
|
+
} catch {
|
|
45
|
+
// Ignore storage errors
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns the expiry time for cache
|
|
51
|
+
* @param _options
|
|
52
|
+
*/
|
|
53
|
+
export function getApiResponseExpiry(_options: Pick<OptionsAfterDefaults, 'cache_lifetime_in_ms'>): number {
|
|
54
|
+
if(_options.cache_lifetime_in_ms > MAXIMUM_CACHE_LIFETIME) {
|
|
55
|
+
return Date.now() + MAXIMUM_CACHE_LIFETIME;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Date.now() + _options.cache_lifetime_in_ms;
|
|
59
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getVisitorId, setVisitorId } from './visitorId';
|
|
2
|
-
import {
|
|
2
|
+
import { defaultOptions, options, OptionsAfterDefaults } from '../options';
|
|
3
3
|
|
|
4
4
|
describe('visitorId storage tests', () => {
|
|
5
5
|
beforeEach(() => {
|
|
@@ -21,7 +21,7 @@ describe('visitorId storage tests', () => {
|
|
|
21
21
|
|
|
22
22
|
test('should use custom storage property name', () => {
|
|
23
23
|
const visitorId = 'custom-visitor-456';
|
|
24
|
-
const customOptions:
|
|
24
|
+
const customOptions: OptionsAfterDefaults = {
|
|
25
25
|
...defaultOptions,
|
|
26
26
|
storage_property_name: 'my_custom_visitor_key'
|
|
27
27
|
};
|
|
@@ -37,7 +37,7 @@ describe('visitorId storage tests', () => {
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
test('should return null when storage property does not exist', () => {
|
|
40
|
-
const options:
|
|
40
|
+
const options: OptionsAfterDefaults = {
|
|
41
41
|
...defaultOptions,
|
|
42
42
|
storage_property_name: 'nonexistent_key'
|
|
43
43
|
};
|
|
@@ -56,6 +56,15 @@ describe('visitorId storage tests', () => {
|
|
|
56
56
|
setVisitorId(newVisitorId, options);
|
|
57
57
|
expect(getVisitorId(options)).toBe(newVisitorId);
|
|
58
58
|
});
|
|
59
|
+
|
|
60
|
+
test('should migrate from old value in case new prefix is set', () => {
|
|
61
|
+
const visitorId = 'test-visitor-123';
|
|
62
|
+
setVisitorId(visitorId, options);
|
|
63
|
+
expect(getVisitorId({
|
|
64
|
+
property_name_factory: (name) => `custom_prefix_${name}`,
|
|
65
|
+
} as OptionsAfterDefaults)).toBe(visitorId);
|
|
66
|
+
expect(localStorage.getItem(`custom_prefix_visitor_id`)).toBe(visitorId);
|
|
67
|
+
})
|
|
59
68
|
});
|
|
60
69
|
|
|
61
70
|
describe('error handling', () => {
|
package/src/utils/visitorId.ts
CHANGED
|
@@ -1,18 +1,42 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DEFAULT_STORAGE_PREFIX, OptionsAfterDefaults } from "../options";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Visitor ID storage utilities - localStorage only, server generates IDs
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const DEFAULT_STORAGE_PROPERTY_NAME = '
|
|
7
|
+
const DEFAULT_STORAGE_PROPERTY_NAME = 'visitor_id';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Get the storage property name for visitor id
|
|
11
|
+
* @param _options
|
|
12
|
+
*/
|
|
13
|
+
export function getVisitorIdPropertyName(
|
|
14
|
+
_options: Pick<OptionsAfterDefaults, 'storage_property_name' | 'property_name_factory'>
|
|
15
|
+
): string {
|
|
16
|
+
if(_options.storage_property_name) {
|
|
17
|
+
return _options.storage_property_name;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return _options.property_name_factory(DEFAULT_STORAGE_PROPERTY_NAME);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DEFAULT_VISITOR_ID_NAME = `${DEFAULT_STORAGE_PREFIX}_${DEFAULT_STORAGE_PROPERTY_NAME}`;
|
|
9
24
|
/**
|
|
10
25
|
* Gets visitor ID from localStorage, returns null if unavailable
|
|
11
26
|
*/
|
|
12
|
-
export function getVisitorId(_options:
|
|
13
|
-
const storagePropertyName = _options.storage_property_name || DEFAULT_STORAGE_PROPERTY_NAME;
|
|
27
|
+
export function getVisitorId(_options: OptionsAfterDefaults): string | null {
|
|
14
28
|
try {
|
|
15
|
-
|
|
29
|
+
const propertyName = getVisitorIdPropertyName(_options);
|
|
30
|
+
let visitorId = localStorage.getItem(propertyName);
|
|
31
|
+
if(!visitorId && propertyName !== DEFAULT_VISITOR_ID_NAME) {
|
|
32
|
+
// Migration case in case going from thumbmark prefix to a custom one
|
|
33
|
+
visitorId = localStorage.getItem(DEFAULT_VISITOR_ID_NAME);
|
|
34
|
+
if(visitorId) {
|
|
35
|
+
setVisitorId(visitorId, _options);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return visitorId;
|
|
16
40
|
} catch {
|
|
17
41
|
return null;
|
|
18
42
|
}
|
|
@@ -21,10 +45,9 @@ export function getVisitorId(_options: optionsInterface): string | null {
|
|
|
21
45
|
/**
|
|
22
46
|
* Sets visitor ID in localStorage
|
|
23
47
|
*/
|
|
24
|
-
export function setVisitorId(visitorId: string, _options:
|
|
25
|
-
const storagePropertyName = _options.storage_property_name || DEFAULT_STORAGE_PROPERTY_NAME;
|
|
48
|
+
export function setVisitorId(visitorId: string, _options: OptionsAfterDefaults): void {
|
|
26
49
|
try {
|
|
27
|
-
localStorage.setItem(
|
|
50
|
+
localStorage.setItem(getVisitorIdPropertyName(_options), visitorId);
|
|
28
51
|
} catch {
|
|
29
52
|
// Ignore storage errors
|
|
30
53
|
}
|