mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
* docs(performance): plan Xtream benchmark * feat(xtream-mock-server): add deterministic 100k fixture * style(xtream-mock-server): apply repository formatting * fix(xtream-mock-server): harden performance fixture data * feat(xtream-mock-server): add performance control plane * docs(performance): correct Xtream capture plan * fix(xtream-mock-server): harden performance controls * fix(xtream-mock-server): harden control lifecycle * feat(performance): add Xtream preload markers * feat(performance): trace Xtream main phases * feat(performance): mark Xtream store publications * feat(performance): trace Xtream database phases * feat(performance): trace Xtream delete cancellation * feat(performance): capture Xtream phase attribution * feat(performance): mark Sources Xtream refresh * test(performance): define Xtream benchmark evidence contracts * test(performance): add Xtream benchmark runner * test(performance): surface failure evidence writes * test(performance): align database read clock * test(performance): preserve capture failure contracts
303 lines
9.4 KiB
TypeScript
303 lines
9.4 KiB
TypeScript
import type { ElectronApplication } from '@playwright/test';
|
|
import { isAbsolute, join } from 'node:path';
|
|
|
|
import type { XtreamCatalogDatabaseSnapshot } from './xtream-catalog-database-verification';
|
|
import {
|
|
buildXtreamCatalogDatabaseQueries,
|
|
type XtreamCatalogDatabaseQuery,
|
|
} from './xtream-catalog-database-queries';
|
|
|
|
export type XtreamCatalogDatabaseEvaluator = Pick<
|
|
ElectronApplication,
|
|
'evaluate'
|
|
>;
|
|
|
|
export interface XtreamCatalogDatabaseReadOptions {
|
|
readonly captureStoppedEpochMs: number;
|
|
readonly dataDirectory: string;
|
|
readonly playlistId: string;
|
|
readonly requireFromPath: string;
|
|
}
|
|
|
|
interface DatabaseReadInput {
|
|
readonly databasePath: string;
|
|
readonly queries: readonly XtreamCatalogDatabaseQuery[];
|
|
readonly requireFromPath: string;
|
|
}
|
|
|
|
interface DatabaseReadTransport {
|
|
readonly databaseReadStartedEpochMs: number;
|
|
readonly rows: readonly unknown[];
|
|
}
|
|
|
|
export async function readXtreamCatalogDatabaseSnapshot(
|
|
evaluator: XtreamCatalogDatabaseEvaluator,
|
|
options: XtreamCatalogDatabaseReadOptions
|
|
): Promise<XtreamCatalogDatabaseSnapshot> {
|
|
try {
|
|
assertOptions(options);
|
|
const queries = buildXtreamCatalogDatabaseQueries(options.playlistId);
|
|
const transport = (await evaluator.evaluate(
|
|
async (_electron, input: DatabaseReadInput) => {
|
|
interface SqliteStatement {
|
|
get(...parameters: readonly (number | string)[]): unknown;
|
|
}
|
|
interface SqliteDatabase {
|
|
close(): void;
|
|
pragma(source: string): unknown;
|
|
prepare(source: string): SqliteStatement;
|
|
}
|
|
interface SqliteConstructor {
|
|
new (
|
|
path: string,
|
|
options: {
|
|
readonly: true;
|
|
fileMustExist: true;
|
|
}
|
|
): SqliteDatabase;
|
|
}
|
|
interface NodeModuleBuiltin {
|
|
createRequire(path: string): (id: string) => unknown;
|
|
}
|
|
interface NodePerfHooksBuiltin {
|
|
readonly performance: {
|
|
now(): number;
|
|
readonly timeOrigin: number;
|
|
};
|
|
}
|
|
|
|
const perfHooks = process.getBuiltinModule(
|
|
'node:perf_hooks'
|
|
) as NodePerfHooksBuiltin;
|
|
const databaseReadStartedEpochMs =
|
|
perfHooks.performance.timeOrigin +
|
|
perfHooks.performance.now();
|
|
const nodeModule = process.getBuiltinModule(
|
|
'node:module'
|
|
) as NodeModuleBuiltin;
|
|
const requireFromBuild = nodeModule.createRequire(
|
|
input.requireFromPath
|
|
);
|
|
const Sqlite = requireFromBuild(
|
|
'better-sqlite3'
|
|
) as SqliteConstructor;
|
|
const database = new Sqlite(input.databasePath, {
|
|
readonly: true,
|
|
fileMustExist: true,
|
|
});
|
|
try {
|
|
database.pragma('query_only = ON');
|
|
return {
|
|
databaseReadStartedEpochMs,
|
|
rows: input.queries.map((query) =>
|
|
database.prepare(query.sql).get(...query.parameters)
|
|
),
|
|
};
|
|
} finally {
|
|
database.close();
|
|
}
|
|
},
|
|
{
|
|
databasePath: join(
|
|
options.dataDirectory,
|
|
'databases',
|
|
'iptvnator.db'
|
|
),
|
|
queries,
|
|
requireFromPath: options.requireFromPath,
|
|
}
|
|
)) as DatabaseReadTransport;
|
|
return snapshotTransport(
|
|
transport,
|
|
options.captureStoppedEpochMs,
|
|
queries.length
|
|
);
|
|
} catch {
|
|
throw new Error('invalid Xtream catalog database read');
|
|
}
|
|
}
|
|
|
|
function snapshotTransport(
|
|
value: unknown,
|
|
captureStoppedEpochMs: number,
|
|
expectedRowCount: number
|
|
): XtreamCatalogDatabaseSnapshot {
|
|
const transport = exactRecord(value, [
|
|
'databaseReadStartedEpochMs',
|
|
'rows',
|
|
]);
|
|
const databaseReadStartedEpochMs = transport[
|
|
'databaseReadStartedEpochMs'
|
|
] as unknown;
|
|
const rows = transport['rows'];
|
|
if (
|
|
!isPositiveFinite(databaseReadStartedEpochMs) ||
|
|
databaseReadStartedEpochMs < captureStoppedEpochMs ||
|
|
!Array.isArray(rows) ||
|
|
rows.length !== expectedRowCount
|
|
) {
|
|
invalid();
|
|
}
|
|
|
|
const playlist = numericRecord(rows[0], [
|
|
'playlistCount',
|
|
'matchingPlaylistCount',
|
|
'matchingPlaylistType',
|
|
]);
|
|
const categoryCount = singleCount(rows[1], 'categoryCount');
|
|
const contentCount = singleCount(rows[2], 'contentCount');
|
|
const categories = [rows[3], rows[4], rows[5]].map(categoryPartition);
|
|
const content = [rows[6], rows[7], rows[8]].map(contentPartition);
|
|
const importStatuses = statusRecord(rows[9]);
|
|
const matchingPlaylistType = playlist['matchingPlaylistType'];
|
|
if (
|
|
matchingPlaylistType !== null &&
|
|
typeof matchingPlaylistType !== 'string'
|
|
) {
|
|
invalid();
|
|
}
|
|
|
|
return Object.freeze({
|
|
captureStoppedEpochMs,
|
|
categories: Object.freeze({
|
|
live: categories[0],
|
|
movies: categories[1],
|
|
series: categories[2],
|
|
}),
|
|
categoryCount,
|
|
content: Object.freeze({
|
|
live: content[0],
|
|
movie: content[1],
|
|
series: content[2],
|
|
}),
|
|
contentCount,
|
|
databaseReadStartedEpochMs,
|
|
importStatuses,
|
|
matchingPlaylistCount: nonNegativeInteger(
|
|
playlist['matchingPlaylistCount']
|
|
),
|
|
matchingPlaylistType,
|
|
playlistCount: nonNegativeInteger(playlist['playlistCount']),
|
|
});
|
|
}
|
|
|
|
function categoryPartition(value: unknown) {
|
|
const row = numericRecord(value, [
|
|
'count',
|
|
'distinctXtreamIdCount',
|
|
'invalidNameCount',
|
|
'maxXtreamId',
|
|
'minXtreamId',
|
|
]);
|
|
return Object.freeze({
|
|
count: nonNegativeInteger(row['count']),
|
|
distinctXtreamIdCount: nonNegativeInteger(row['distinctXtreamIdCount']),
|
|
invalidNameCount: nonNegativeInteger(row['invalidNameCount']),
|
|
maxXtreamId: nullableInteger(row['maxXtreamId']),
|
|
minXtreamId: nullableInteger(row['minXtreamId']),
|
|
});
|
|
}
|
|
|
|
function contentPartition(value: unknown) {
|
|
const row = numericRecord(value, [
|
|
'count',
|
|
'distinctXtreamIdCount',
|
|
'invalidCategoryCardinalityCount',
|
|
'invalidTitleCount',
|
|
'maxXtreamId',
|
|
'minXtreamId',
|
|
]);
|
|
return Object.freeze({
|
|
count: nonNegativeInteger(row['count']),
|
|
distinctXtreamIdCount: nonNegativeInteger(row['distinctXtreamIdCount']),
|
|
invalidCategoryCardinalityCount: nonNegativeInteger(
|
|
row['invalidCategoryCardinalityCount']
|
|
),
|
|
invalidTitleCount: nonNegativeInteger(row['invalidTitleCount']),
|
|
maxXtreamId: nullableInteger(row['maxXtreamId']),
|
|
minXtreamId: nullableInteger(row['minXtreamId']),
|
|
});
|
|
}
|
|
|
|
function singleCount(value: unknown, key: string): number {
|
|
return nonNegativeInteger(numericRecord(value, [key])[key]);
|
|
}
|
|
|
|
function statusRecord(value: unknown) {
|
|
const row = exactRecord(value, ['live', 'movie', 'series']);
|
|
for (const key of ['live', 'movie', 'series']) {
|
|
if (row[key] !== null && typeof row[key] !== 'string') {
|
|
invalid();
|
|
}
|
|
}
|
|
return Object.freeze({
|
|
live: row['live'] as string | null,
|
|
movie: row['movie'] as string | null,
|
|
series: row['series'] as string | null,
|
|
});
|
|
}
|
|
|
|
function numericRecord(
|
|
value: unknown,
|
|
keys: readonly string[]
|
|
): Record<string, unknown> {
|
|
return exactRecord(value, keys);
|
|
}
|
|
|
|
function exactRecord(
|
|
value: unknown,
|
|
expectedKeys: readonly string[]
|
|
): Record<string, unknown> {
|
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
invalid();
|
|
}
|
|
const keys = Reflect.ownKeys(value);
|
|
if (
|
|
keys.length !== expectedKeys.length ||
|
|
keys.some(
|
|
(key) => typeof key !== 'string' || !expectedKeys.includes(key)
|
|
)
|
|
) {
|
|
invalid();
|
|
}
|
|
return Object.fromEntries(
|
|
expectedKeys.map((key) => [key, Reflect.get(value, key)])
|
|
);
|
|
}
|
|
|
|
function nonNegativeInteger(value: unknown): number {
|
|
if (
|
|
typeof value !== 'number' ||
|
|
!Number.isSafeInteger(value) ||
|
|
value < 0
|
|
) {
|
|
invalid();
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function nullableInteger(value: unknown): number | null {
|
|
if (value === null) return null;
|
|
return nonNegativeInteger(value);
|
|
}
|
|
|
|
function assertOptions(options: XtreamCatalogDatabaseReadOptions): void {
|
|
if (
|
|
!isPositiveFinite(options.captureStoppedEpochMs) ||
|
|
!isAbsolute(options.dataDirectory) ||
|
|
!isAbsolute(options.requireFromPath) ||
|
|
options.playlistId.length === 0 ||
|
|
options.playlistId.length > 256
|
|
) {
|
|
invalid();
|
|
}
|
|
}
|
|
|
|
function isPositiveFinite(value: unknown): value is number {
|
|
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
}
|
|
|
|
function invalid(): never {
|
|
throw new Error('invalid');
|
|
}
|