feat(upstream): Add upstream proxy/cache subsystem and integrate per-protocol upstreams
This commit is contained in:
@@ -7,6 +7,7 @@ import { BaseRegistry } from '../core/classes.baseregistry.js';
|
||||
import type { RegistryStorage } from '../core/classes.registrystorage.js';
|
||||
import type { AuthManager } from '../core/classes.authmanager.js';
|
||||
import type { IRequestContext, IResponse, IAuthToken } from '../core/interfaces.core.js';
|
||||
import type { IProtocolUpstreamConfig } from '../upstream/interfaces.upstream.js';
|
||||
import { isBinaryData, toBuffer } from '../core/helpers.buffer.js';
|
||||
import type {
|
||||
IComposerPackage,
|
||||
@@ -22,24 +23,41 @@ import {
|
||||
generatePackagesJson,
|
||||
sortVersions,
|
||||
} from './helpers.composer.js';
|
||||
import { ComposerUpstream } from './classes.composerupstream.js';
|
||||
|
||||
export class ComposerRegistry extends BaseRegistry {
|
||||
private storage: RegistryStorage;
|
||||
private authManager: AuthManager;
|
||||
private basePath: string = '/composer';
|
||||
private registryUrl: string;
|
||||
private upstream: ComposerUpstream | null = null;
|
||||
|
||||
constructor(
|
||||
storage: RegistryStorage,
|
||||
authManager: AuthManager,
|
||||
basePath: string = '/composer',
|
||||
registryUrl: string = 'http://localhost:5000/composer'
|
||||
registryUrl: string = 'http://localhost:5000/composer',
|
||||
upstreamConfig?: IProtocolUpstreamConfig
|
||||
) {
|
||||
super();
|
||||
this.storage = storage;
|
||||
this.authManager = authManager;
|
||||
this.basePath = basePath;
|
||||
this.registryUrl = registryUrl;
|
||||
|
||||
// Initialize upstream if configured
|
||||
if (upstreamConfig?.enabled) {
|
||||
this.upstream = new ComposerUpstream(upstreamConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources (timers, connections, etc.)
|
||||
*/
|
||||
public destroy(): void {
|
||||
if (this.upstream) {
|
||||
this.upstream.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
@@ -161,7 +179,26 @@ export class ComposerRegistry extends BaseRegistry {
|
||||
token: IAuthToken | null
|
||||
): Promise<IResponse> {
|
||||
// Read operations are public, no authentication required
|
||||
const metadata = await this.storage.getComposerPackageMetadata(vendorPackage);
|
||||
let metadata = await this.storage.getComposerPackageMetadata(vendorPackage);
|
||||
|
||||
// Try upstream if not found locally
|
||||
if (!metadata && this.upstream) {
|
||||
const [vendor, packageName] = vendorPackage.split('/');
|
||||
if (vendor && packageName) {
|
||||
const upstreamMetadata = includeDev
|
||||
? await this.upstream.fetchPackageDevMetadata(vendor, packageName)
|
||||
: await this.upstream.fetchPackageMetadata(vendor, packageName);
|
||||
|
||||
if (upstreamMetadata && upstreamMetadata.packages) {
|
||||
// Store upstream metadata locally
|
||||
metadata = {
|
||||
packages: upstreamMetadata.packages,
|
||||
lastModified: new Date().toUTCString(),
|
||||
};
|
||||
await this.storage.putComposerPackageMetadata(vendorPackage, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!metadata) {
|
||||
return {
|
||||
|
||||
200
ts/composer/classes.composerupstream.ts
Normal file
200
ts/composer/classes.composerupstream.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { BaseUpstream } from '../upstream/classes.baseupstream.js';
|
||||
import type {
|
||||
IProtocolUpstreamConfig,
|
||||
IUpstreamFetchContext,
|
||||
IUpstreamRegistryConfig,
|
||||
} from '../upstream/interfaces.upstream.js';
|
||||
|
||||
/**
|
||||
* Composer-specific upstream implementation.
|
||||
*
|
||||
* Handles:
|
||||
* - Package metadata fetching (packages.json, provider-includes)
|
||||
* - Package version metadata (p2/{vendor}/{package}.json)
|
||||
* - Dist file (zip) proxying
|
||||
* - Packagist v2 API support
|
||||
*/
|
||||
export class ComposerUpstream extends BaseUpstream {
|
||||
protected readonly protocolName = 'composer';
|
||||
|
||||
constructor(
|
||||
config: IProtocolUpstreamConfig,
|
||||
logger?: plugins.smartlog.Smartlog,
|
||||
) {
|
||||
super(config, logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the root packages.json from upstream.
|
||||
*/
|
||||
public async fetchPackagesJson(): Promise<any | null> {
|
||||
const context: IUpstreamFetchContext = {
|
||||
protocol: 'composer',
|
||||
resource: '*',
|
||||
resourceType: 'root',
|
||||
path: '/packages.json',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
},
|
||||
query: {},
|
||||
};
|
||||
|
||||
const result = await this.fetch(context);
|
||||
|
||||
if (!result || !result.success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Buffer.isBuffer(result.body)) {
|
||||
return JSON.parse(result.body.toString('utf8'));
|
||||
}
|
||||
|
||||
return result.body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch package metadata using v2 API (p2/{vendor}/{package}.json).
|
||||
*/
|
||||
public async fetchPackageMetadata(vendor: string, packageName: string): Promise<any | null> {
|
||||
const fullName = `${vendor}/${packageName}`;
|
||||
const path = `/p2/${vendor}/${packageName}.json`;
|
||||
|
||||
const context: IUpstreamFetchContext = {
|
||||
protocol: 'composer',
|
||||
resource: fullName,
|
||||
resourceType: 'metadata',
|
||||
path,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
},
|
||||
query: {},
|
||||
};
|
||||
|
||||
const result = await this.fetch(context);
|
||||
|
||||
if (!result || !result.success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Buffer.isBuffer(result.body)) {
|
||||
return JSON.parse(result.body.toString('utf8'));
|
||||
}
|
||||
|
||||
return result.body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch package metadata with dev versions (p2/{vendor}/{package}~dev.json).
|
||||
*/
|
||||
public async fetchPackageDevMetadata(vendor: string, packageName: string): Promise<any | null> {
|
||||
const fullName = `${vendor}/${packageName}`;
|
||||
const path = `/p2/${vendor}/${packageName}~dev.json`;
|
||||
|
||||
const context: IUpstreamFetchContext = {
|
||||
protocol: 'composer',
|
||||
resource: fullName,
|
||||
resourceType: 'metadata-dev',
|
||||
path,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
},
|
||||
query: {},
|
||||
};
|
||||
|
||||
const result = await this.fetch(context);
|
||||
|
||||
if (!result || !result.success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Buffer.isBuffer(result.body)) {
|
||||
return JSON.parse(result.body.toString('utf8'));
|
||||
}
|
||||
|
||||
return result.body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a provider-includes file.
|
||||
*/
|
||||
public async fetchProviderIncludes(path: string): Promise<any | null> {
|
||||
const context: IUpstreamFetchContext = {
|
||||
protocol: 'composer',
|
||||
resource: '*',
|
||||
resourceType: 'provider',
|
||||
path: path.startsWith('/') ? path : `/${path}`,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
},
|
||||
query: {},
|
||||
};
|
||||
|
||||
const result = await this.fetch(context);
|
||||
|
||||
if (!result || !result.success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Buffer.isBuffer(result.body)) {
|
||||
return JSON.parse(result.body.toString('utf8'));
|
||||
}
|
||||
|
||||
return result.body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a dist file (zip) from upstream.
|
||||
*/
|
||||
public async fetchDist(url: string): Promise<Buffer | null> {
|
||||
// Parse the URL to get the path
|
||||
let path: string;
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
path = parsed.pathname;
|
||||
} catch {
|
||||
path = url;
|
||||
}
|
||||
|
||||
const context: IUpstreamFetchContext = {
|
||||
protocol: 'composer',
|
||||
resource: '*',
|
||||
resourceType: 'dist',
|
||||
path,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'accept': 'application/zip, application/octet-stream',
|
||||
},
|
||||
query: {},
|
||||
};
|
||||
|
||||
const result = await this.fetch(context);
|
||||
|
||||
if (!result || !result.success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Buffer.isBuffer(result.body) ? result.body : Buffer.from(result.body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override URL building for Composer-specific handling.
|
||||
*/
|
||||
protected buildUpstreamUrl(
|
||||
upstream: IUpstreamRegistryConfig,
|
||||
context: IUpstreamFetchContext,
|
||||
): string {
|
||||
let baseUrl = upstream.url;
|
||||
|
||||
// Remove trailing slash
|
||||
if (baseUrl.endsWith('/')) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
return `${baseUrl}${context.path}`;
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,6 @@
|
||||
*/
|
||||
|
||||
export { ComposerRegistry } from './classes.composerregistry.js';
|
||||
export { ComposerUpstream } from './classes.composerupstream.js';
|
||||
export * from './interfaces.composer.js';
|
||||
export * from './helpers.composer.js';
|
||||
|
||||
Reference in New Issue
Block a user