Skip to content

Commit c0c64e6

Browse files
Compress manifest sources in the database (#1405)
1 parent 05e99b3 commit c0c64e6

File tree

6 files changed

+33207
-44
lines changed

6 files changed

+33207
-44
lines changed

packages/catalog-server/src/lib/firestore/firestore-repository.ts

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import {
8+
brotliCompress as brotliCompressCallbackStyle,
9+
brotliDecompress as brotliDecompressCallbackStyle,
10+
} from 'node:zlib';
11+
import {promisify} from 'node:util';
712
import {
813
FieldValue,
914
Query,
@@ -31,6 +36,7 @@ import {
3136
ReadablePackageVersion,
3237
ReadablePackageInfo,
3338
UnreadablePackageStatus,
39+
UnreadablePackageVersion,
3440
} from '@webcomponents/catalog-api/lib/schema.js';
3541
import {
3642
Package,
@@ -41,14 +47,22 @@ import {
4147
packageInfoConverter,
4248
packageNameToId,
4349
} from './package-info-converter.js';
44-
import {packageVersionConverter} from './package-version-converter.js';
50+
import {
51+
CompressedPackageVersion,
52+
isReadableCompressedPackageVersion,
53+
packageVersionConverter,
54+
ReadableCompressedPackageVersion,
55+
} from './package-version-converter.js';
4556
import {customElementConverter} from './custom-element-converter.js';
4657
import {validationProblemConverter} from './validation-problem-converter.js';
4758

4859
const projectId = process.env['GCP_PROJECT_ID'] || 'wc-catalog';
4960
firebase.initializeApp({projectId});
5061
export const db = new Firestore({projectId});
5162

63+
const brotliCompress = promisify(brotliCompressCallbackStyle);
64+
const brotliDecompress = promisify(brotliDecompressCallbackStyle);
65+
5266
export class FirestoreRepository implements Repository {
5367
/**
5468
* A namespace suffix to apply to the 'packages' collection to support
@@ -234,8 +248,13 @@ export class FirestoreRepository implements Repository {
234248
const distTags = packageMetadata['dist-tags'];
235249
const versionDistTags = getDistTagsForVersion(distTags, version);
236250

251+
const compressedManifest =
252+
customElementsManifestSource &&
253+
(await brotliCompress(customElementsManifestSource)).toString('base64');
254+
237255
// Store package data and mark version as ready
238256
t.set(versionRef, {
257+
__typename: 'ReadableCompressedPackageVersion',
239258
name: packageName,
240259
version,
241260
status: VersionStatus.READY,
@@ -246,14 +265,23 @@ export class FirestoreRepository implements Repository {
246265
author,
247266
time: new Date(packageTime),
248267
homepage: packageVersionMetadata.homepage ?? null,
249-
customElementsManifest: customElementsManifestSource ?? null,
268+
customElementsManifestCompressed: compressedManifest,
250269
});
251270
});
252271
const packageVersion = await db.runTransaction(async (t) => {
253272
// There doesn't seem to be a way to get a WriteResult and therefore
254273
// a writeTime inside a transaction, so we read from the database to
255274
// get the server timestamp.
256-
return (await t.get(versionRef)).data() as ReadablePackageVersion;
275+
const packageVersionCompressed = (await t.get(versionRef)).data()!;
276+
if (isReadableCompressedPackageVersion(packageVersionCompressed)) {
277+
return decompressPackageVersion(
278+
packageVersionCompressed,
279+
versionRef.id
280+
);
281+
}
282+
throw new Error(
283+
`Internal error: expected package version ${versionRef.id} to be readable`
284+
);
257285
});
258286
return packageVersion;
259287
}
@@ -282,14 +310,14 @@ export class FirestoreRepository implements Repository {
282310
t.update(versionRef, {
283311
status,
284312
lastUpdate: FieldValue.serverTimestamp(),
285-
} as UpdateData<PackageVersion> as PackageVersion);
313+
} as UpdateData<UnreadablePackageVersion> as UnreadablePackageVersion);
286314
});
287315
const packageVersion = await db.runTransaction(async (t) => {
288316
// There doesn't seem to be a way to get a WriteResult and therefore
289317
// a writeTime inside a transaction, so we read from the database to
290318
// get the server timestamp.
291319
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
292-
return (await t.get(versionRef)).data()!;
320+
return (await t.get(versionRef)).data() as UnreadablePackageVersion;
293321
});
294322
return packageVersion;
295323
}
@@ -439,7 +467,14 @@ export class FirestoreRepository implements Repository {
439467
// If version is valid semver we can build a document reference.
440468
const versionRef = this.getPackageVersionRef(packageName, versionNumber);
441469
const versionDoc = await versionRef.get();
442-
return versionDoc.data();
470+
const packageVersion = versionDoc.data();
471+
if (
472+
packageVersion &&
473+
isReadableCompressedPackageVersion(packageVersion)
474+
) {
475+
return decompressPackageVersion(packageVersion, versionRef.id);
476+
}
477+
return packageVersion;
443478
} else {
444479
// If version is not a valid semver it may be a dist-tag
445480

@@ -450,7 +485,9 @@ export class FirestoreRepository implements Repository {
450485
}
451486

452487
// Now query for a version that's assigned this dist-tag
453-
let query: CollectionReference<PackageVersion> | Query<PackageVersion> =
488+
let query:
489+
| CollectionReference<CompressedPackageVersion>
490+
| Query<CompressedPackageVersion> =
454491
this.getPackageVersionCollectionRef(packageName);
455492
if (versionOrTag === 'latest') {
456493
query = query.where('isLatest', '==', true);
@@ -459,7 +496,16 @@ export class FirestoreRepository implements Repository {
459496
}
460497
const result = await query.limit(1).get();
461498
if (result.size !== 0) {
462-
return result.docs[0]!.data();
499+
const doc = result.docs[0]!;
500+
const packageVersion = doc.data();
501+
// Decompress the custom elements manifest.
502+
// Note: We'd like to do this in the packageVersionConverter Firestore
503+
// converter so that we don't have to remember to compress/decompress
504+
// at every read and write operation, but we can't because we also want
505+
// this to be an async operation and not block the main thread.
506+
if (isReadableCompressedPackageVersion(packageVersion)) {
507+
return decompressPackageVersion(packageVersion, doc.id);
508+
}
463509
}
464510
return undefined;
465511
}
@@ -549,6 +595,28 @@ export class FirestoreRepository implements Repository {
549595
}
550596
}
551597

598+
export const decompressPackageVersion = async (
599+
packageVersion: ReadableCompressedPackageVersion,
600+
id: string
601+
): Promise<ReadablePackageVersion> => {
602+
const manifestCompressed = packageVersion.customElementsManifestCompressed;
603+
try {
604+
const customElementsManifest =
605+
manifestCompressed &&
606+
(
607+
await brotliDecompress(Buffer.from(manifestCompressed, 'base64'))
608+
).toString();
609+
return {
610+
...packageVersion,
611+
__typename: undefined,
612+
customElementsManifest,
613+
};
614+
} catch (e) {
615+
console.error(`Filed to decompress manifest for package version ${id}`);
616+
throw e;
617+
}
618+
};
619+
552620
// /**
553621
// * Generates a type representing a Firestore document from a GraphQL schema
554622
// * type.

packages/catalog-server/src/lib/firestore/package-version-converter.ts

Lines changed: 63 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,43 +14,70 @@ import {
1414
import {
1515
isReadablePackageVersion,
1616
PackageVersion,
17+
ReadablePackageVersion,
18+
UnreadablePackageVersion,
1719
} from '@webcomponents/catalog-api/lib/schema.js';
1820

19-
export const packageVersionConverter: FirestoreDataConverter<PackageVersion> = {
20-
fromFirestore(snapshot: QueryDocumentSnapshot<DocumentData>): PackageVersion {
21-
return {
22-
status: snapshot.get('status'),
23-
lastUpdate: (snapshot.get('lastUpdate') as Timestamp).toDate(),
24-
version: snapshot.id,
25-
distTags: snapshot.get('distTags'),
26-
description: snapshot.get('description'),
27-
type: snapshot.get('type'),
28-
author: snapshot.get('author'),
29-
time: snapshot.get('time'),
30-
homepage: snapshot.get('homepage'),
31-
customElements: snapshot.get('customElements'),
32-
customElementsManifest: snapshot.get('customElementsManifest'),
33-
};
34-
},
35-
toFirestore(packageVersion: PackageVersion) {
36-
if (isReadablePackageVersion(packageVersion)) {
37-
return {
38-
author: packageVersion.author,
39-
customElementsManifest: packageVersion.customElementsManifest,
40-
description: packageVersion.description,
41-
distTags: packageVersion.distTags,
42-
isLatest: packageVersion.distTags.includes('latest'),
43-
homepage: packageVersion.homepage,
44-
lastUpdate: packageVersion.lastUpdate,
45-
status: packageVersion.status,
46-
time: Timestamp.fromDate(packageVersion.time),
47-
type: packageVersion.type,
48-
};
49-
} else {
21+
export type ReadableCompressedPackageVersion = Omit<
22+
ReadablePackageVersion,
23+
'customElementsManifest' | '__typename'
24+
> & {
25+
customElementsManifestCompressed?: string;
26+
// Prevent this from being assignable to a ReadablePackageVersion to force
27+
// consumer to convert correctly.
28+
__typename: 'ReadableCompressedPackageVersion';
29+
};
30+
31+
export type CompressedPackageVersion =
32+
| ReadableCompressedPackageVersion
33+
| UnreadablePackageVersion;
34+
35+
export const packageVersionConverter: FirestoreDataConverter<CompressedPackageVersion> =
36+
{
37+
fromFirestore(
38+
snapshot: QueryDocumentSnapshot<DocumentData>
39+
): CompressedPackageVersion {
5040
return {
51-
status: packageVersion.status,
52-
lastUpdate: packageVersion.lastUpdate,
41+
status: snapshot.get('status'),
42+
lastUpdate: (snapshot.get('lastUpdate') as Timestamp).toDate(),
43+
version: snapshot.id,
44+
distTags: snapshot.get('distTags'),
45+
description: snapshot.get('description'),
46+
type: snapshot.get('type'),
47+
author: snapshot.get('author'),
48+
time: snapshot.get('time'),
49+
homepage: snapshot.get('homepage'),
50+
customElements: snapshot.get('customElements'),
51+
customElementsManifestCompressed: snapshot.get(
52+
'customElementsManifest'
53+
),
5354
};
54-
}
55-
},
56-
};
55+
},
56+
toFirestore(packageVersion: CompressedPackageVersion) {
57+
if (isReadableCompressedPackageVersion(packageVersion)) {
58+
return {
59+
author: packageVersion.author,
60+
customElementsManifest:
61+
packageVersion.customElementsManifestCompressed,
62+
description: packageVersion.description,
63+
distTags: packageVersion.distTags,
64+
isLatest: packageVersion.distTags.includes('latest'),
65+
homepage: packageVersion.homepage,
66+
lastUpdate: packageVersion.lastUpdate,
67+
status: packageVersion.status,
68+
time: Timestamp.fromDate(packageVersion.time),
69+
type: packageVersion.type,
70+
};
71+
} else {
72+
return {
73+
status: packageVersion.status,
74+
lastUpdate: packageVersion.lastUpdate,
75+
};
76+
}
77+
},
78+
};
79+
80+
export const isReadableCompressedPackageVersion = (
81+
p: CompressedPackageVersion
82+
): p is ReadableCompressedPackageVersion =>
83+
isReadablePackageVersion(p as PackageVersion);

packages/catalog-server/src/test/lib/catalog_test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,4 +358,42 @@ test('Imports a non-existent package', async () => {
358358
assert.equal(importResult.packageInfo?.status, PackageStatus.NOT_FOUND);
359359
});
360360

361+
test('Imports a large manifest', async () => {
362+
const packageName = 'large-manifest';
363+
const packagePath = fileURLToPath(
364+
new URL('../test-packages/large-manifest/', import.meta.url)
365+
);
366+
367+
const files = new LocalFsPackageFiles({
368+
path: packagePath,
369+
packageName,
370+
publishedVersions: ['1.0.0'],
371+
distTags: {
372+
latest: '1.0.0',
373+
},
374+
});
375+
const repository = new FirestoreRepository(TEST_SEQUENCE_THREE);
376+
const catalog = new Catalog({files, repository});
377+
const importResult = await catalog.importPackage(packageName);
378+
379+
assert.equal(importResult.problems ?? [], []);
380+
assert.equal(importResult.packageInfo?.status, PackageStatus.READY);
381+
382+
assert.ok(importResult.packageVersion);
383+
assert.equal(importResult.packageVersion.status, VersionStatus.READY);
384+
const manifest = (importResult.packageVersion as ReadablePackageVersion)
385+
.customElementsManifest;
386+
assert.ok(manifest);
387+
const parsedManifest = JSON.parse(manifest);
388+
assert.equal(parsedManifest.schemaVersion, '1.0.0');
389+
390+
// Check that getting the version through getPackageVersion() decompresses
391+
const packageVersion2 = await catalog.getPackageVersion(packageName, '1.0.0');
392+
const manifest2 = (packageVersion2 as ReadablePackageVersion)
393+
.customElementsManifest;
394+
assert.ok(manifest2);
395+
const parsedManifest2 = JSON.parse(manifest2);
396+
assert.equal(parsedManifest2.schemaVersion, '1.0.0');
397+
});
398+
361399
test.run();

0 commit comments

Comments
 (0)