@splitsoftware/splitio 10.16.0 → 10.16.1-rc.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/CHANGES.txt +5 -0
- package/README.md +2 -1
- package/es/engine/engine/murmur3/murmur3_128.js +1 -1
- package/es/engine/engine/murmur3/murmur3_128_x86.js +2 -3
- package/es/engine/matchers/index.js +20 -37
- package/es/engine/matchers/types.js +19 -72
- package/es/engine/transforms/matcherGroup.js +2 -2
- package/es/engine/transforms/matchers.js +2 -2
- package/es/engine/value/sanitize.js +12 -14
- package/es/impressions/hasher/hashImpression128.js +1 -1
- package/es/impressions/observer/observer.js +0 -1
- package/es/producer/updater/SplitChangesFromObject.js +3 -2
- package/es/sync/PushManager/index.js +3 -11
- package/es/utils/logger/LoggerFactory.js +23 -16
- package/es/utils/settings/index.js +1 -1
- package/es/utils/settings/storage/browser.js +19 -4
- package/lib/engine/engine/murmur3/murmur3_128.js +1 -1
- package/lib/engine/engine/murmur3/murmur3_128_x86.js +2 -3
- package/lib/engine/matchers/index.js +21 -39
- package/lib/engine/matchers/types.js +23 -77
- package/lib/engine/transforms/matcherGroup.js +3 -3
- package/lib/engine/transforms/matchers.js +22 -22
- package/lib/engine/value/sanitize.js +11 -13
- package/lib/impressions/hasher/hashImpression128.js +2 -2
- package/lib/impressions/observer/observer.js +0 -1
- package/lib/producer/updater/SplitChangesFromObject.js +4 -2
- package/lib/sync/PushManager/index.js +3 -11
- package/lib/utils/logger/LoggerFactory.js +23 -17
- package/lib/utils/settings/index.js +1 -1
- package/lib/utils/settings/storage/browser.js +18 -3
- package/package.json +13 -11
- package/src/engine/engine/murmur3/murmur3_128.js +1 -1
- package/src/engine/engine/murmur3/murmur3_128_x86.js +2 -3
- package/src/engine/matchers/index.js +22 -36
- package/src/engine/matchers/types.js +20 -55
- package/src/engine/transforms/matcherGroup.js +2 -5
- package/src/engine/transforms/matchers.js +2 -6
- package/src/engine/value/sanitize.js +12 -17
- package/src/impressions/hasher/hashImpression128.js +1 -1
- package/src/impressions/observer/observer.js +0 -2
- package/src/producer/updater/SplitChangesFromObject.js +2 -2
- package/src/sync/PushManager/index.js +3 -10
- package/src/sync/constants.js +1 -1
- package/src/utils/logger/LoggerFactory.js +25 -16
- package/src/utils/settings/index.js +1 -1
- package/src/utils/settings/storage/browser.js +18 -3
|
@@ -15,28 +15,28 @@ limitations under the License.
|
|
|
15
15
|
**/
|
|
16
16
|
|
|
17
17
|
// @WARNING Symbol is not correctly working in PhantomJS
|
|
18
|
-
export const
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
export const matcherTypes = {
|
|
19
|
+
UNDEFINED: 0,
|
|
20
|
+
ALL_KEYS: 1,
|
|
21
|
+
IN_SEGMENT: 2,
|
|
21
22
|
WHITELIST: 3,
|
|
22
23
|
EQUAL_TO: 4,
|
|
23
24
|
GREATER_THAN_OR_EQUAL_TO: 5,
|
|
24
25
|
LESS_THAN_OR_EQUAL_TO: 6,
|
|
25
26
|
BETWEEN: 7,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
MATCHES_STRING: 18
|
|
27
|
+
EQUAL_TO_SET: 8,
|
|
28
|
+
CONTAINS_ANY_OF_SET: 9,
|
|
29
|
+
CONTAINS_ALL_OF_SET: 10,
|
|
30
|
+
PART_OF_SET: 11,
|
|
31
|
+
ENDS_WITH: 12,
|
|
32
|
+
STARTS_WITH: 13,
|
|
33
|
+
CONTAINS_STRING: 14,
|
|
34
|
+
IN_SPLIT_TREATMENT: 15,
|
|
35
|
+
EQUAL_TO_BOOLEAN: 16,
|
|
36
|
+
MATCHES_STRING: 17
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
export const
|
|
39
|
+
export const matcherDataTypes = {
|
|
40
40
|
BOOLEAN: 'BOOLEAN',
|
|
41
41
|
STRING: 'STRING',
|
|
42
42
|
NUMBER: 'NUMBER',
|
|
@@ -45,43 +45,8 @@ export const dataTypes = {
|
|
|
45
45
|
NOT_SPECIFIED: 'NOT_SPECIFIED'
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
-
export
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return types.SEGMENT;
|
|
54
|
-
case 'WHITELIST':
|
|
55
|
-
return types.WHITELIST;
|
|
56
|
-
case 'EQUAL_TO':
|
|
57
|
-
return types.EQUAL_TO;
|
|
58
|
-
case 'GREATER_THAN_OR_EQUAL_TO':
|
|
59
|
-
return types.GREATER_THAN_OR_EQUAL_TO;
|
|
60
|
-
case 'LESS_THAN_OR_EQUAL_TO':
|
|
61
|
-
return types.LESS_THAN_OR_EQUAL_TO;
|
|
62
|
-
case 'BETWEEN':
|
|
63
|
-
return types.BETWEEN;
|
|
64
|
-
case 'EQUAL_TO_SET':
|
|
65
|
-
return types.EQUAL_TO_SET;
|
|
66
|
-
case 'CONTAINS_ANY_OF_SET':
|
|
67
|
-
return types.CONTAINS_ANY_OF_SET;
|
|
68
|
-
case 'CONTAINS_ALL_OF_SET':
|
|
69
|
-
return types.CONTAINS_ALL_OF_SET;
|
|
70
|
-
case 'PART_OF_SET':
|
|
71
|
-
return types.PART_OF_SET;
|
|
72
|
-
case 'ENDS_WITH':
|
|
73
|
-
return types.ENDS_WITH;
|
|
74
|
-
case 'STARTS_WITH':
|
|
75
|
-
return types.STARTS_WITH;
|
|
76
|
-
case 'CONTAINS_STRING':
|
|
77
|
-
return types.CONTAINS_STRING;
|
|
78
|
-
case 'IN_SPLIT_TREATMENT':
|
|
79
|
-
return types.IN_SPLIT_TREATMENT;
|
|
80
|
-
case 'EQUAL_TO_BOOLEAN':
|
|
81
|
-
return types.EQUAL_TO_BOOLEAN;
|
|
82
|
-
case 'MATCHES_STRING':
|
|
83
|
-
return types.MATCHES_STRING;
|
|
84
|
-
default:
|
|
85
|
-
return types.UNDEFINED;
|
|
86
|
-
}
|
|
87
|
-
};
|
|
48
|
+
export function matcherTypesMapper(matcherType) {
|
|
49
|
+
const type = matcherTypes[matcherType];
|
|
50
|
+
if (type) return type;
|
|
51
|
+
else return matcherTypes.UNDEFINED;
|
|
52
|
+
}
|
|
@@ -13,10 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
**/
|
|
16
|
-
import {
|
|
17
|
-
types as matcherTypes,
|
|
18
|
-
mapper as matcherTypesMapper
|
|
19
|
-
} from '../matchers/types';
|
|
16
|
+
import { matcherTypes, matcherTypesMapper } from '../matchers/types';
|
|
20
17
|
import segmentTransform from './segment';
|
|
21
18
|
import whitelistTransform from './whitelist';
|
|
22
19
|
|
|
@@ -33,7 +30,7 @@ function transform(matcherGroup) {
|
|
|
33
30
|
let type = matcherTypesMapper(matcherType);
|
|
34
31
|
let value = undefined;
|
|
35
32
|
|
|
36
|
-
if (type === matcherTypes.
|
|
33
|
+
if (type === matcherTypes.IN_SEGMENT) {
|
|
37
34
|
value = segmentTransform(segmentObject);
|
|
38
35
|
} else if (type === matcherTypes.WHITELIST) {
|
|
39
36
|
value = whitelistTransform(whitelistObject);
|
|
@@ -15,11 +15,7 @@ limitations under the License.
|
|
|
15
15
|
**/
|
|
16
16
|
|
|
17
17
|
import { findIndex } from '../../utils/lang';
|
|
18
|
-
import {
|
|
19
|
-
types as matcherTypes,
|
|
20
|
-
mapper as matcherTypesMapper,
|
|
21
|
-
dataTypes as matcherDataTypes
|
|
22
|
-
} from '../matchers/types';
|
|
18
|
+
import { matcherTypes, matcherTypesMapper, matcherDataTypes } from '../matchers/types';
|
|
23
19
|
import segmentTransform from './segment';
|
|
24
20
|
import whitelistTransform from './whitelist';
|
|
25
21
|
import setTransform from './set';
|
|
@@ -49,7 +45,7 @@ function transform(matchers) {
|
|
|
49
45
|
let dataType = matcherDataTypes.STRING;
|
|
50
46
|
let value = undefined;
|
|
51
47
|
|
|
52
|
-
if (type === matcherTypes.
|
|
48
|
+
if (type === matcherTypes.IN_SEGMENT) {
|
|
53
49
|
value = segmentTransform(segmentObject);
|
|
54
50
|
} else if (type === matcherTypes.WHITELIST) {
|
|
55
51
|
value = whitelistTransform(whitelistObject);
|
|
@@ -18,12 +18,7 @@ import logFactory from '../../utils/logger';
|
|
|
18
18
|
const log = logFactory('splitio-engine:sanitize');
|
|
19
19
|
import { isObject, uniq, toString, toNumber } from '../../utils/lang';
|
|
20
20
|
import { zeroSinceHH, zeroSinceSS } from '../convertions';
|
|
21
|
-
import {
|
|
22
|
-
types as matcherTypes,
|
|
23
|
-
dataTypes as matcherDataTypes
|
|
24
|
-
} from '../matchers/types';
|
|
25
|
-
const MATCHERS = matcherTypes;
|
|
26
|
-
const DATA_TYPES = matcherDataTypes;
|
|
21
|
+
import { matcherTypes, matcherDataTypes } from '../matchers/types';
|
|
27
22
|
|
|
28
23
|
function sanitizeNumber(val) {
|
|
29
24
|
const num = toNumber(val);
|
|
@@ -72,13 +67,13 @@ function dependencyProcessor(sanitizedValue, attributes) {
|
|
|
72
67
|
*/
|
|
73
68
|
function getProcessingFunction(matcherTypeID, dataType) {
|
|
74
69
|
switch (matcherTypeID) {
|
|
75
|
-
case
|
|
70
|
+
case matcherTypes.EQUAL_TO:
|
|
76
71
|
return dataType === 'DATETIME' ? zeroSinceHH : undefined;
|
|
77
|
-
case
|
|
78
|
-
case
|
|
79
|
-
case
|
|
72
|
+
case matcherTypes.GREATER_THAN_OR_EQUAL_TO:
|
|
73
|
+
case matcherTypes.LESS_THAN_OR_EQUAL_TO:
|
|
74
|
+
case matcherTypes.BETWEEN:
|
|
80
75
|
return dataType === 'DATETIME' ? zeroSinceSS : undefined;
|
|
81
|
-
case
|
|
76
|
+
case matcherTypes.IN_SPLIT_TREATMENT:
|
|
82
77
|
return dependencyProcessor;
|
|
83
78
|
default:
|
|
84
79
|
return undefined;
|
|
@@ -90,20 +85,20 @@ function sanitizeValue(matcherTypeID, value, dataType, attributes) {
|
|
|
90
85
|
let sanitizedValue;
|
|
91
86
|
|
|
92
87
|
switch (dataType) {
|
|
93
|
-
case
|
|
94
|
-
case
|
|
88
|
+
case matcherDataTypes.NUMBER:
|
|
89
|
+
case matcherDataTypes.DATETIME:
|
|
95
90
|
sanitizedValue = sanitizeNumber(value);
|
|
96
91
|
break;
|
|
97
|
-
case
|
|
92
|
+
case matcherDataTypes.STRING:
|
|
98
93
|
sanitizedValue = sanitizeString(value);
|
|
99
94
|
break;
|
|
100
|
-
case
|
|
95
|
+
case matcherDataTypes.SET:
|
|
101
96
|
sanitizedValue = sanitizeArray(value);
|
|
102
97
|
break;
|
|
103
|
-
case
|
|
98
|
+
case matcherDataTypes.BOOLEAN:
|
|
104
99
|
sanitizedValue = sanitizeBoolean(value);
|
|
105
100
|
break;
|
|
106
|
-
case
|
|
101
|
+
case matcherDataTypes.NOT_SPECIFIED:
|
|
107
102
|
sanitizedValue = value;
|
|
108
103
|
break;
|
|
109
104
|
default:
|
|
@@ -13,6 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
**/
|
|
16
|
+
import { STORAGE_LOCALSTORAGE } from '../../utils/constants';
|
|
16
17
|
import { forOwn } from '../../utils/lang';
|
|
17
18
|
import logFactory from '../../utils/logger';
|
|
18
19
|
const log = logFactory('splitio-producer:offline');
|
|
@@ -59,7 +60,6 @@ function FromObjectUpdaterFactory(Fetcher, context) {
|
|
|
59
60
|
|
|
60
61
|
return Promise.all([
|
|
61
62
|
storage.splits.flush(), // required to sync removed splits from mock
|
|
62
|
-
storage.splits.setChangeNumber(Date.now()),
|
|
63
63
|
storage.splits.addSplits(splits)
|
|
64
64
|
]).then(() => {
|
|
65
65
|
readiness.splits.emit(readiness.splits.SDK_SPLITS_ARRIVED);
|
|
@@ -67,7 +67,7 @@ function FromObjectUpdaterFactory(Fetcher, context) {
|
|
|
67
67
|
if (startingUp) {
|
|
68
68
|
startingUp = false;
|
|
69
69
|
// Emits SDK_READY_FROM_CACHE
|
|
70
|
-
if (storage.
|
|
70
|
+
if (settings.storage.__originalType === STORAGE_LOCALSTORAGE) readiness.splits.emit(readiness.splits.SDK_SPLITS_CACHE_LOADED);
|
|
71
71
|
// Only emits SDK_SEGMENTS_ARRIVED the first time for SDK_READY
|
|
72
72
|
readiness.segments.emit(readiness.segments.SDK_SEGMENTS_ARRIVED);
|
|
73
73
|
}
|
|
@@ -247,19 +247,12 @@ export default function PushManagerFactory(context, clientContexts /* undefined
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
forOwn(clients, ({ hash64, worker }) => {
|
|
250
|
-
|
|
250
|
+
const add = added.has(hash64.dec) ? true : removed.has(hash64.dec) ? false : undefined;
|
|
251
|
+
if (add !== undefined) {
|
|
251
252
|
worker.put(parsedData.changeNumber, {
|
|
252
253
|
name: parsedData.segmentName,
|
|
253
|
-
add
|
|
254
|
+
add
|
|
254
255
|
});
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
if (removed.has(hash64.dec)) {
|
|
258
|
-
worker.put(parsedData.changeNumber, {
|
|
259
|
-
name: parsedData.segmentName,
|
|
260
|
-
add: false
|
|
261
|
-
});
|
|
262
|
-
return;
|
|
263
256
|
}
|
|
264
257
|
});
|
|
265
258
|
return;
|
package/src/sync/constants.js
CHANGED
|
@@ -5,17 +5,28 @@ import objectAssign from 'object-assign';
|
|
|
5
5
|
|
|
6
6
|
export const LogLevels = {
|
|
7
7
|
'DEBUG': 'DEBUG',
|
|
8
|
-
'INFO':
|
|
9
|
-
'WARN':
|
|
8
|
+
'INFO': 'INFO',
|
|
9
|
+
'WARN': 'WARN',
|
|
10
10
|
'ERROR': 'ERROR',
|
|
11
11
|
'NONE': 'NONE'
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
const LogLevelIndexes = {
|
|
15
|
+
DEBUG: 1,
|
|
16
|
+
INFO: 2,
|
|
17
|
+
WARN: 3,
|
|
18
|
+
ERROR: 4,
|
|
19
|
+
NONE: 5
|
|
20
|
+
};
|
|
21
|
+
|
|
14
22
|
// DEBUG is the default. The log level is not specific to an SDK instance.
|
|
15
|
-
let GlobalLogLevel =
|
|
23
|
+
let GlobalLogLevel = LogLevelIndexes.DEBUG;
|
|
16
24
|
|
|
25
|
+
/**
|
|
26
|
+
* @param {'DEBUG'|'INFO'|'WARN'|'ERROR'|'NONE'} level
|
|
27
|
+
*/
|
|
17
28
|
export const setLogLevel = (level) => {
|
|
18
|
-
GlobalLogLevel = level;
|
|
29
|
+
GlobalLogLevel = LogLevelIndexes[level];
|
|
19
30
|
};
|
|
20
31
|
|
|
21
32
|
const defaultOptions = {
|
|
@@ -30,22 +41,22 @@ export class Logger {
|
|
|
30
41
|
}
|
|
31
42
|
|
|
32
43
|
debug(msg) {
|
|
33
|
-
if(this._shouldLog(
|
|
44
|
+
if (this._shouldLog(LogLevelIndexes.DEBUG))
|
|
34
45
|
this._log(LogLevels.DEBUG, msg);
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
info(msg) {
|
|
38
|
-
if(this._shouldLog(
|
|
49
|
+
if (this._shouldLog(LogLevelIndexes.INFO))
|
|
39
50
|
this._log(LogLevels.INFO, msg);
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
warn(msg) {
|
|
43
|
-
if(this._shouldLog(
|
|
54
|
+
if (this._shouldLog(LogLevelIndexes.WARN))
|
|
44
55
|
this._log(LogLevels.WARN, msg);
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
error(msg) {
|
|
48
|
-
if(this.options.displayAllErrors || this._shouldLog(
|
|
59
|
+
if (this.options.displayAllErrors || this._shouldLog(LogLevelIndexes.ERROR))
|
|
49
60
|
this._log(LogLevels.ERROR, msg);
|
|
50
61
|
}
|
|
51
62
|
|
|
@@ -59,8 +70,8 @@ export class Logger {
|
|
|
59
70
|
const textPre = ' => ';
|
|
60
71
|
let result = '';
|
|
61
72
|
|
|
62
|
-
if(this.options.showLevel) {
|
|
63
|
-
result += '[' + level +']' + (level === LogLevels.INFO || level === LogLevels.WARN ? ' ' : '') + ' ';
|
|
73
|
+
if (this.options.showLevel) {
|
|
74
|
+
result += '[' + level + ']' + (level === LogLevels.INFO || level === LogLevels.WARN ? ' ' : '') + ' ';
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
if (this.category) {
|
|
@@ -70,12 +81,10 @@ export class Logger {
|
|
|
70
81
|
return result += text;
|
|
71
82
|
}
|
|
72
83
|
|
|
84
|
+
/**
|
|
85
|
+
* @param {number} level
|
|
86
|
+
*/
|
|
73
87
|
_shouldLog(level) {
|
|
74
|
-
|
|
75
|
-
const levels = Object.keys(LogLevels).map(f => LogLevels[f]);
|
|
76
|
-
const index = levels.indexOf(level); // What's the index of what it's trying to check if it should log
|
|
77
|
-
const levelIdx = levels.indexOf(logLevel); // What's the current log level index.
|
|
78
|
-
|
|
79
|
-
return index >= levelIdx;
|
|
88
|
+
return level >= GlobalLogLevel;
|
|
80
89
|
}
|
|
81
90
|
}
|
|
@@ -27,7 +27,7 @@ import { API } from '../../utils/logger';
|
|
|
27
27
|
import { STANDALONE_MODE, STORAGE_MEMORY, CONSUMER_MODE, OPTIMIZED } from '../../utils/constants';
|
|
28
28
|
import validImpressionsMode from './impressionsMode';
|
|
29
29
|
|
|
30
|
-
const version = '10.16.0';
|
|
30
|
+
const version = '10.16.1-rc.0';
|
|
31
31
|
const eventsEndpointMatcher = /^\/(testImpressions|metrics|events)/;
|
|
32
32
|
const authEndpointMatcher = /^\/v2\/auth/;
|
|
33
33
|
const streamingEndpointMatcher = /^\/(sse|event-stream)/;
|
|
@@ -18,18 +18,21 @@ import logFactory from '../../../utils/logger';
|
|
|
18
18
|
const log = logFactory('splitio-settings');
|
|
19
19
|
import isLocalStorageAvailable from '../../../utils/localstorage/isAvailable';
|
|
20
20
|
import {
|
|
21
|
+
LOCALHOST_MODE,
|
|
21
22
|
STORAGE_MEMORY,
|
|
22
23
|
STORAGE_LOCALSTORAGE
|
|
23
24
|
} from '../../../utils/constants';
|
|
24
25
|
|
|
25
26
|
const ParseStorageSettings = settings => {
|
|
26
27
|
let {
|
|
28
|
+
mode,
|
|
27
29
|
storage: {
|
|
28
30
|
type = STORAGE_MEMORY,
|
|
29
31
|
options = {},
|
|
30
32
|
prefix
|
|
31
33
|
},
|
|
32
34
|
} = settings;
|
|
35
|
+
let __originalType;
|
|
33
36
|
|
|
34
37
|
if (prefix) {
|
|
35
38
|
prefix += '.SPLITIO';
|
|
@@ -37,18 +40,30 @@ const ParseStorageSettings = settings => {
|
|
|
37
40
|
prefix = 'SPLITIO';
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
const fallbackToMemory = () => {
|
|
44
|
+
__originalType = type;
|
|
45
|
+
type = STORAGE_MEMORY;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// In localhost mode, fallback to Memory storage and track original
|
|
49
|
+
// type to emit SDK_READY_FROM_CACHE if corresponds
|
|
50
|
+
if (mode === LOCALHOST_MODE && type === STORAGE_LOCALSTORAGE) {
|
|
51
|
+
fallbackToMemory();
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
// If an invalid storage type is provided OR we want to use LOCALSTORAGE and
|
|
41
55
|
// it's not available, fallback into MEMORY
|
|
42
56
|
if (type !== STORAGE_MEMORY && type !== STORAGE_LOCALSTORAGE ||
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
type === STORAGE_LOCALSTORAGE && !isLocalStorageAvailable()) {
|
|
58
|
+
fallbackToMemory();
|
|
45
59
|
log.warn('Invalid or unavailable storage. Fallbacking into MEMORY storage');
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
return {
|
|
49
63
|
type,
|
|
50
64
|
options,
|
|
51
|
-
prefix
|
|
65
|
+
prefix,
|
|
66
|
+
__originalType
|
|
52
67
|
};
|
|
53
68
|
};
|
|
54
69
|
|