Skip to content

Commit 20a7d76

Browse files
committed
Add catalog-server endpoint to update packages
1 parent fe375cc commit 20a7d76

File tree

6 files changed

+156
-4
lines changed

6 files changed

+156
-4
lines changed

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

+19-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ const toTemporalInstant = (date: Date) => {
4242
*/
4343
const defaultPackageRefreshInterval = Temporal.Duration.from({minutes: 5});
4444

45+
/**
46+
* The default amount of time between automated bulk updates of packages.
47+
*/
48+
const defaultPackageUpdateInterval = Temporal.Duration.from({hours: 6});
49+
4550
export interface CatalogInit {
4651
repository: Repository;
4752
files: PackageFiles;
@@ -77,7 +82,7 @@ export class Catalog {
7782
packageVersion?: PackageVersion;
7883
problems?: ValidationProblem[];
7984
}> {
80-
console.log('Catalog.importPackage');
85+
console.log('Catalog.importPackage', packageName);
8186

8287
const currentPackageInfo = await this.#repository.getPackageInfo(
8388
packageName
@@ -347,4 +352,17 @@ export class Catalog {
347352
// to the repository
348353
return this.#repository.queryElements({query, limit});
349354
}
355+
356+
async getPackagesToUpdate(notUpdatedSince?: Temporal.Instant) {
357+
if (notUpdatedSince === undefined) {
358+
const now = Temporal.Now.instant();
359+
notUpdatedSince = now.subtract(defaultPackageUpdateInterval);
360+
}
361+
362+
const packages = await this.#repository.getPackagesToUpdate(
363+
notUpdatedSince,
364+
100
365+
);
366+
return packages;
367+
}
350368
}

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

+28-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
CollectionReference,
1616
CollectionGroup,
1717
UpdateData,
18+
Timestamp,
1819
} from '@google-cloud/firestore';
1920
import {Firestore} from '@google-cloud/firestore';
2021
import firebase from 'firebase-admin';
@@ -55,6 +56,7 @@ import {
5556
} from './package-version-converter.js';
5657
import {customElementConverter} from './custom-element-converter.js';
5758
import {validationProblemConverter} from './validation-problem-converter.js';
59+
import type {Temporal} from '@js-temporal/polyfill';
5860

5961
const projectId = process.env['GCP_PROJECT_ID'] || 'wc-catalog';
6062
firebase.initializeApp({projectId});
@@ -577,13 +579,37 @@ export class FirestoreRepository implements Repository {
577579
return result;
578580
}
579581

580-
getPackageRef(packageName: string) {
582+
async getPackagesToUpdate(
583+
notUpdatedSince: Temporal.Instant,
584+
limit = 100
585+
): Promise<Array<PackageInfo>> {
586+
const date = new Date(notUpdatedSince.epochMilliseconds);
587+
const notUpdatedSinceTimestamp = Timestamp.fromDate(date);
588+
589+
// Only query 'READY', 'ERROR', and 'NOT_FOUND' packages.
590+
// INITIALIZING and UPDATING packages are being updated, possibly by the
591+
// batch update task calling this method.
592+
// ERROR and NOT_FOUND are "recoverable" errors, so we should try to import
593+
// them again.
594+
const result = await this.getPackageCollectionRef()
595+
.where('status', 'in', ['READY', 'ERROR', 'NOT_FOUND'])
596+
.where('lastUpdate', '<', notUpdatedSinceTimestamp)
597+
.limit(limit)
598+
.get();
599+
const packages = result.docs.map((d) => d.data());
600+
return packages;
601+
}
602+
603+
getPackageCollectionRef() {
581604
return db
582605
.collection('packages' + (this.namespace ? `-${this.namespace}` : ''))
583-
.doc(packageNameToId(packageName))
584606
.withConverter(packageInfoConverter);
585607
}
586608

609+
getPackageRef(packageName: string) {
610+
return this.getPackageCollectionRef().doc(packageNameToId(packageName));
611+
}
612+
587613
getPackageVersionCollectionRef(packageName: string) {
588614
return this.getPackageRef(packageName)
589615
.collection('versions')

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

+9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import type {Temporal} from '@js-temporal/polyfill';
78
import type {
89
CustomElement,
910
PackageInfo,
@@ -150,4 +151,12 @@ export interface Repository {
150151
packageName: string,
151152
version: string
152153
): Promise<PackageVersion | undefined>;
154+
155+
/**
156+
* Returns packages that have not been updated since the date given.
157+
*/
158+
getPackagesToUpdate(
159+
notUpdatedSince: Temporal.Instant,
160+
limit: number
161+
): Promise<Array<PackageInfo>>;
153162
}

packages/catalog-server/src/lib/server/routes/bootstrap-packages.ts

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ export const makeBootstrapPackagesRoute =
2222
const bootstrapListFile = await readFile(bootstrapListFilePath, 'utf-8');
2323
const bootstrapList = JSON.parse(bootstrapListFile);
2424
const packageNames = bootstrapList['packages'] as Array<string>;
25+
26+
// TODO (justinfagnani): rather than import the packages directly, add them
27+
// to the DB in a non-imported state, then kick off the standard update
28+
// workflow, which will import them all.
2529
const results = await Promise.all(
2630
packageNames.map(
2731
async (
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {Temporal} from '@js-temporal/polyfill';
2+
import {PackageInfo} from '@webcomponents/catalog-api/lib/schema.js';
3+
import type Koa from 'koa';
4+
import type {Catalog} from '../../catalog.js';
5+
6+
// Google Cloud Run default request timeout is 5 minutes, so to do longer
7+
// imports we need to configure the timeout.
8+
const maxImportDuration = Temporal.Duration.from({minutes: 5});
9+
10+
export const makeUpdatePackagesRoute =
11+
(catalog: Catalog) => async (context: Koa.Context) => {
12+
const startInstant = Temporal.Now.instant();
13+
// If the `force` query parameter is present we force updating of all
14+
// packages by setting the `notUpdatedSince` parameter to `startInstant` so
15+
// that we get all packages last updated before now. We calculate the
16+
// `notUpdatedSince` time once before updates so that we don't retrieve
17+
// packages that we update in this operation.
18+
// `force`` is useful for development and testing as we may be trying to
19+
// update packages that were just imported.
20+
// TODO (justinfagnani): check a DEV mode also so this isn't available
21+
// in production?
22+
const force = 'force' in context.query;
23+
const notUpdatedSince = force ? startInstant : undefined;
24+
25+
// If `force` is true, override the default packageUpdateInterval
26+
// TODO: how do we make an actually 0 duration?
27+
const packageUpdateInterval = force
28+
? Temporal.Duration.from({microseconds: 1})
29+
: undefined;
30+
31+
console.log('Starting package update at', startInstant, `force: ${force}`);
32+
33+
let packagesToUpdate!: Array<PackageInfo>;
34+
let packagesUpdated = 0;
35+
let iteration = 0;
36+
37+
// Loop through batches of packages to update.
38+
// We batch here so that we can pause and check that we're still within the
39+
// maxImportDuration, and use small enough batches so that we can ensure at
40+
// least one batch in that time.
41+
do {
42+
// getPackagesToUpdate() queries the first N (default 100) packages that
43+
// have not been updated since the update interval (default 6 hours).
44+
// When a package is imported it's lastUpdate date will be updated and the
45+
// next call to getPackagesToUpdate() will return the next 100 packages.
46+
// This way we don't need a DB cursor to make progress through the
47+
// package list.
48+
packagesToUpdate = await catalog.getPackagesToUpdate(notUpdatedSince);
49+
50+
if (packagesToUpdate.length === 0) {
51+
// No more packages to update
52+
if (iteration === 0) {
53+
console.log('No packages to update');
54+
}
55+
break;
56+
}
57+
58+
await Promise.allSettled(
59+
packagesToUpdate.map(async (pkg) => {
60+
try {
61+
return await catalog.importPackage(pkg.name, packageUpdateInterval);
62+
} catch (e) {
63+
console.error(e);
64+
throw e;
65+
}
66+
})
67+
);
68+
packagesUpdated += packagesToUpdate.length;
69+
70+
const now = Temporal.Now.instant();
71+
const timeSinceStart = now.since(startInstant);
72+
// If the time since the update started is not less than that max import
73+
// duration, stop.
74+
// TODO (justinfagnani): we need a way to test this
75+
if (Temporal.Duration.compare(timeSinceStart, maxImportDuration) !== -1) {
76+
break;
77+
}
78+
} while (true);
79+
console.log(`Updated ${packagesUpdated} packages`);
80+
81+
if (packagesToUpdate.length > 0) {
82+
// TODO (justinfagnani): kick off new update request
83+
console.log(`Not all packages were updated (${packagesToUpdate.length})`);
84+
}
85+
86+
context.status = 200;
87+
context.type = 'html';
88+
context.body = `
89+
<h1>Update Results</h1>
90+
<p>Updated ${packagesUpdated} package</p>
91+
`;
92+
};

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @license
3-
* Copyright 2021 Google LLC
3+
* Copyright 2022 Google LLC
44
* SPDX-License-Identifier: BSD-3-Clause
55
*/
66

@@ -17,6 +17,7 @@ import {NpmAndUnpkgFiles} from '@webcomponents/custom-elements-manifest-tools/li
1717

1818
import {makeGraphQLRoute} from './routes/graphql.js';
1919
import {makeBootstrapPackagesRoute} from './routes/bootstrap-packages.js';
20+
import {makeUpdatePackagesRoute} from './routes/update-packages.js';
2021

2122
export const makeServer = async () => {
2223
const files = new NpmAndUnpkgFiles();
@@ -32,6 +33,8 @@ export const makeServer = async () => {
3233

3334
router.get('/bootstrap-packages', makeBootstrapPackagesRoute(catalog));
3435

36+
router.get('/update-packages', makeUpdatePackagesRoute(catalog));
37+
3538
router.get('/', async (ctx) => {
3639
ctx.status = 200;
3740
ctx.type = 'html';

0 commit comments

Comments
 (0)