Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Update @mapeo/schema, fix breaking changes related to Icon #548

Merged
merged 14 commits into from
Apr 17, 2024
Merged
Binary file added mapeo-schema-3.0.0-next.14.tgz
Binary file not shown.
7 changes: 4 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@
"@fastify/type-provider-typebox": "^3.3.0",
"@hyperswarm/secret-stream": "^6.1.2",
"@mapeo/crypto": "1.0.0-alpha.10",
"@mapeo/schema": "3.0.0-next.14",
"@mapeo/schema": "file:mapeo-schema-3.0.0-next.14.tgz",
"@mapeo/sqlite-indexer": "1.0.0-alpha.8",
"@sinclair/typebox": "^0.29.6",
"b4a": "^1.6.3",
Expand Down
28 changes: 14 additions & 14 deletions src/fastify-plugins/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ export default fp(iconServerPlugin, {
const ICON_DOC_ID_STRING = T.String({ pattern: HEX_REGEX_32_BYTES })
const PROJECT_PUBLIC_ID_STRING = T.String({ pattern: Z_BASE_32_REGEX_32_BYTES })

const VALID_SIZES =
docSchemas.icon.properties.variants.items.properties.size.enum
const VALID_MIME_TYPES =
docSchemas.icon.properties.variants.items.properties.mimeType.enum
const VALID_PIXEL_DENSITIES =
docSchemas.icon.properties.variants.items.properties.pixelDensity.enum
const VALID_SIZES = docSchemas.icon.definitions.size.enum
const VALID_MIME_TYPES = docSchemas.icon.properties.variants.items.oneOf.map(
(iconType) => iconType.properties.mimeType.const
)
const VALID_PIXEL_DENSITIES = docSchemas.icon.properties.variants.items.oneOf
.filter((iconType) => iconType.properties.mimeType.const === 'image/png')
/** @ts-ignore using the array.filter as type guard? */
.map((pngIcon) => pngIcon.properties.pixelDensity.enum)
tomasciccola marked this conversation as resolved.
Show resolved Hide resolved

const PARAMS_JSON_SCHEMA = T.Object({
iconDocId: ICON_DOC_ID_STRING,
Expand Down Expand Up @@ -101,7 +103,10 @@ const DENSITY_MATCH_REGEX = /@(\d+)x$/i
/**
* @param {string} input
*
* @return {Pick<import('@mapeo/schema').Icon['variants'][number], 'size' | 'pixelDensity'>}
* @return {{
* pixelDensity: import('../icon-api.js').BitmapOpts['pixelDensity'],
* size: import('../icon-api.js').IconVariant['size']
* }}
*/
function extractSizeAndPixelDensity(input) {
const result = DENSITY_MATCH_REGEX.exec(input)
Expand Down Expand Up @@ -139,15 +144,10 @@ function assertValidSize(value) {

/**
* @param {number} value
* @returns {asserts value is import('@mapeo/schema').Icon['variants'][number]['pixelDensity']}
* @returns {asserts value is import('../icon-api.js').BitmapOpts['pixelDensity']}
*/
function assertValidPixelDensity(value) {
if (
!VALID_PIXEL_DENSITIES.includes(
// @ts-expect-error
value
)
) {
if (!VALID_PIXEL_DENSITIES.includes(value)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would fix this error by casting value to any.

Suggested change
if (!VALID_PIXEL_DENSITIES.includes(value)) {
if (!VALID_PIXEL_DENSITIES.includes(/** @type {any} */ (value))) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mhmh, I also needed to change the signature of the fn to /** @param {any} value */

throw new Error(`${value} is not a valid icon pixel density`)
}
}
51 changes: 23 additions & 28 deletions src/icon-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const kGetIconBlob = Symbol('getIcon')
/**
* @typedef {Object} BitmapOpts
* @property {Extract<IconVariant['mimeType'], 'image/png'>} mimeType
* @property {IconVariant['pixelDensity']} pixelDensity
* @property {Extract<IconVariant, {mimeType: 'image/png'}>['pixelDensity']} pixelDensity
* @property {IconVariant['size']} size
*
* @typedef {Object} SvgOpts
Expand Down Expand Up @@ -58,17 +58,7 @@ export class IconApi {
const savedVariants = await Promise.all(
icon.variants.map(async ({ blob, ...variant }) => {
const blobVersionId = await this.#dataStore.writeRaw(blob)

return {
...variant,
blobVersionId,
pixelDensity:
// Pixel density does not apply to svg variants
// TODO: Ideally @mapeo/schema wouldn't require pixelDensity when the mime type is svg
variant.mimeType === 'image/svg+xml'
? /** @type {const} */ (1)
: variant.pixelDensity,
}
return { ...variant, blobVersionId }
})
)

Expand Down Expand Up @@ -146,7 +136,7 @@ const SIZE_AS_NUMERIC = {
* 2. Matching size. If no exact match:
* 1. If smaller ones exist, prefer closest smaller size.
* 2. Otherwise prefer closest larger size.
* 3. Matching pixel density. If no exact match:
* 3. Matching pixel density (when asking for PNGs). If no exact match:
* 1. If smaller ones exist, prefer closest smaller density.
* 2. Otherwise prefer closest larger density.
*
Expand All @@ -155,10 +145,11 @@ const SIZE_AS_NUMERIC = {
*/
export function getBestVariant(variants, opts) {
EvanHahn marked this conversation as resolved.
Show resolved Hide resolved
const { size: wantedSize, mimeType: wantedMimeType } = opts
// Pixel density doesn't matter for svg so default to 1
const wantedPixelDensity =
opts.mimeType === 'image/svg+xml' ? 1 : opts.pixelDensity

/** @type {BitmapOpts['pixelDensity']} */
let wantedPixelDensity
if (opts.mimeType === 'image/png') {
wantedPixelDensity = opts.pixelDensity
}
if (variants.length === 0) {
throw new Error('No variants exist')
}
Expand All @@ -170,7 +161,6 @@ export function getBestVariant(variants, opts) {
`No variants with desired mime type ${wantedMimeType} exist`
)
}

const wantedSizeNum = SIZE_AS_NUMERIC[wantedSize]

// Sort the relevant variants based on the desired size and pixel density, using the rules of the preference.
Expand All @@ -182,18 +172,23 @@ export function getBestVariant(variants, opts) {
const aSizeDiff = aSizeNum - wantedSizeNum
const bSizeDiff = bSizeNum - wantedSizeNum

// Both variants match desired size, use pixel density to determine preferred match
// Both variants match desired size, use pixel density (when png) to determine preferred match
if (aSizeDiff === 0 && bSizeDiff === 0) {
// Pixel density doesn't matter for svg but prefer lower for consistent results
if (opts.mimeType === 'image/svg+xml') {
return a.pixelDensity <= b.pixelDensity ? -1 : 1
// What to do if asking for an svg and both (a and b) are svgs and have the same size, what criteria do we use?
// For now, we don't change sort order
EvanHahn marked this conversation as resolved.
Show resolved Hide resolved
if (wantedMimeType === 'image/svg+xml') {
return 0
} else if (
wantedMimeType === 'image/png' &&
a.mimeType === 'image/png' &&
EvanHahn marked this conversation as resolved.
Show resolved Hide resolved
b.mimeType === 'image/png'
) {
return determineSortValue(
wantedPixelDensity,
a.pixelDensity,
b.pixelDensity
)
}

return determineSortValue(
wantedPixelDensity,
a.pixelDensity,
b.pixelDensity
)
}

return determineSortValue(wantedSizeNum, aSizeNum, bSizeNum)
Expand Down
57 changes: 28 additions & 29 deletions tests/icon-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,12 @@ test('getBestVariant() - no variants exist', (t) => {
})

test('getBestVariant() - specify mimeType', (t) => {
/** @type {Pick<import('@mapeo/schema').Icon['variants'][number], 'pixelDensity' | 'size'>} */
const common = { pixelDensity: 1, size: 'small' }
/** @type {{size:import('../src/icon-api.js').IconVariant['size']}} */
const common = { size: 'small' }

const pngVariant = createIconVariant({
...common,
pixelDensity: 1,
mimeType: 'image/png',
})

Expand Down Expand Up @@ -562,30 +563,34 @@ test('getBestVariant() - params prioritization', (t) => {
})

// TODO: The IconApi doesn't allow creating svg variants with a custom pixel density, so maybe can remove this test?
test('getBestVariant() - svg requests are not affected by pixel density', (t) => {
/** @type {Pick<import('@mapeo/schema').Icon['variants'][number], 'size' | 'mimeType'>} */
const common = { size: 'small', mimeType: 'image/svg+xml' }

const variant1 = createIconVariant({ ...common, pixelDensity: 1 })
const variant2 = createIconVariant({ ...common, pixelDensity: 2 })
const variant3 = createIconVariant({ ...common, pixelDensity: 3 })
test(
'getBestVariant() - svg requests are not affected by pixel density',
{ skip: true },
(t) => {
/** @type {import('../src/icon-api.js').SvgOpts} */
const common = { size: 'small', mimeType: 'image/svg+xml' }

const result = getBestVariant([variant1, variant2, variant3], {
size: 'small',
mimeType: 'image/svg+xml',
})
const variant1 = createIconVariant({ ...common })
const variant2 = createIconVariant({ ...common, size: 'large' })
const variant3 = createIconVariant({ ...common, size: 'medium' })

t.alike(
result,
getBestVariant([variant1, variant2, variant3].reverse(), {
mimeType: 'image/svg+xml',
const result = getBestVariant([variant1, variant2, variant3], {
size: 'small',
}),
'same result regardless of variants order'
)
mimeType: 'image/svg+xml',
})

t.alike(result, variant1)
})
t.alike(
result,
getBestVariant([variant1, variant2, variant3].reverse(), {
mimeType: 'image/svg+xml',
size: 'small',
}),
'same result regardless of variants order'
)

t.alike(result, variant1)
}
)

// TODO: Currently fails. Not sure if we'd run into this situation often in reality
test(
Expand All @@ -594,12 +599,10 @@ test(
(t) => {
const variantA = createIconVariant({
size: 'small',
pixelDensity: 1,
mimeType: 'image/svg+xml',
})
const variantB = createIconVariant({
size: 'small',
pixelDensity: 1,
mimeType: 'image/svg+xml',
})

Expand Down Expand Up @@ -729,11 +732,7 @@ function createRandomVersionId(index = 0) {
}

/**
* @param {object} opts
* @param {import('@mapeo/schema').Icon['variants'][number]['size']} opts.size
* @param {import('@mapeo/schema').Icon['variants'][number]['mimeType']} opts.mimeType
* @param {import('@mapeo/schema').Icon['variants'][number]['pixelDensity']} opts.pixelDensity
*
* @param {import('../src/icon-api.js').BitmapOpts | import('../src/icon-api.js').SvgOpts} opts
* @returns {import('@mapeo/schema').Icon['variants'][number]}
*/
function createIconVariant(opts) {
Expand Down
Loading