@@ -12,37 +12,26 @@ import ManagerInformation from "../../_managerinf/ManagerInformation";
1212import { EcosystemModloaderPackages , EcosystemSupportedGames } from "../../model/schema/ThunderstoreSchema" ;
1313import { updateModLoaderExports } from "../installing/profile_installers/ModLoaderVariantRecord" ;
1414import LoggerProvider , { LogSeverity } from "../../providers/ror2/logging/LoggerProvider" ;
15+ import { getAxiosWithTimeouts } from "../../utils/HttpUtils" ;
16+ import { retry } from "../../utils/Common" ;
1517
16- export type VersionedThunderstoreEcosystem = ThunderstoreEcosystem & { version : string } ;
18+ export type VersionedThunderstoreEcosystem = ThunderstoreEcosystem & {
19+ version : string ;
20+ lastModified ?: string ;
21+ } ;
1722
18- async function getMergedEcosystemPath ( ) : Promise < string > {
19- return path . join ( PathResolver . ROOT , "latest-ecosystem-schema.json" ) ;
20- }
21-
22- export async function updateLatestEcosystemSchema ( ) : Promise < void > {
23- const latestSchema = await fetchLatestSchema ( ) ;
24- await writeLatestEcosystemSchema ( latestSchema ) ;
25- await internalUpdateEcosystemReactives ( latestSchema ) ;
26- }
23+ type LatestSchemaFetchResult =
24+ | { kind : "not-modified" }
25+ | { kind : "fetched" , schema : ThunderstoreEcosystem , lastModified ?: string }
26+ | { kind : "failed" } ;
2727
28- async function writeLatestEcosystemSchema ( schema : ThunderstoreEcosystem ) : Promise < void > {
29- const asMergedSchema : VersionedThunderstoreEcosystem = {
30- ...schema ,
31- version : ManagerInformation . VERSION . toString ( ) ,
32- } ;
33- const writable = JSON . stringify ( asMergedSchema ) ;
34- return FsProvider . instance . writeFile ( await getMergedEcosystemPath ( ) , writable ) ;
35- }
28+ const ECOSYSTEM_DATA_URL = "https://thunderstore.io/api/experimental/schema/dev/latest/" ;
3629
37- async function getLastSavedEcosystemSchema ( ) : Promise < VersionedThunderstoreEcosystem > {
38- const contentBuffer = await FsProvider . instance . readFile ( await getMergedEcosystemPath ( ) ) ;
39- const content = contentBuffer . toString ( "utf8" ) ;
40- const parsedContent = JSON . parse ( content ) ;
41- await validateSchema ( parsedContent ) ;
42- return parsedContent ;
30+ async function getMergedEcosystemPath ( ) : Promise < string > {
31+ return path . join ( PathResolver . ROOT , "latest-ecosystem-schema.json" ) ;
4332}
4433
45- async function validateSchema ( schema : any ) : Promise < void > {
34+ function validateSchema ( schema : unknown ) : ThunderstoreEcosystem {
4635 const ajv = new Ajv ( ) ;
4736 addFormats ( ajv ) ;
4837
@@ -52,15 +41,15 @@ async function validateSchema(schema: any): Promise<void> {
5241 if ( ! isOk ) {
5342 throw new R2Error ( "Schema validation error" , ajv . errorsText ( validate . errors ) ) ;
5443 }
44+
45+ return schema as ThunderstoreEcosystem ;
5546}
5647
57- async function loadBundledSchema ( ) : Promise < ThunderstoreEcosystem > {
58- await validateSchema ( bundledEcosystem ) ;
59- return bundledEcosystem as ThunderstoreEcosystem ;
48+ function loadBundledSchema ( ) : ThunderstoreEcosystem {
49+ return validateSchema ( bundledEcosystem ) ;
6050}
6151
62- async function fetchLatestSchema ( ) : Promise < ThunderstoreEcosystem > {
63- // TODO - Implement fetching of latest resource
52+ function createEmptySchema ( ) : ThunderstoreEcosystem {
6453 return {
6554 schemaVersion : "" ,
6655 communities : { } ,
@@ -70,16 +59,130 @@ async function fetchLatestSchema(): Promise<ThunderstoreEcosystem> {
7059 } ;
7160}
7261
73- async function resolveCachedEcosystemSchema ( ) : Promise < VersionedThunderstoreEcosystem > {
62+ function mergeSchemas (
63+ bundledSchema : ThunderstoreEcosystem ,
64+ latestSchema : ThunderstoreEcosystem
65+ ) : ThunderstoreEcosystem {
66+ const modloaderMap = new Map (
67+ [ ...bundledSchema . modloaderPackages , ...latestSchema . modloaderPackages ]
68+ . map ( pkg => [ pkg . packageId , pkg ] )
69+ ) ;
70+
71+ return {
72+ schemaVersion : latestSchema . schemaVersion ,
73+ communities : {
74+ ...bundledSchema . communities ,
75+ ...latestSchema . communities ,
76+ } ,
77+ games : {
78+ ...bundledSchema . games ,
79+ ...latestSchema . games ,
80+ } ,
81+ modloaderPackages : [ ...modloaderMap . values ( ) ] ,
82+ packageInstallers : {
83+ ...bundledSchema . packageInstallers ,
84+ ...latestSchema . packageInstallers ,
85+ } ,
86+ } ;
87+ }
88+
89+ async function fetchLatestSchema (
90+ currentSchema : VersionedThunderstoreEcosystem | null
91+ ) : Promise < LatestSchemaFetchResult > {
92+ const timeout = 5000 ;
93+ const requestConfig = {
94+ validateStatus : ( status : number ) => {
95+ if ( status === 304 ) {
96+ return true ;
97+ }
98+ return status >= 200 && status < 300 ;
99+ } ,
100+ ...( currentSchema ?. lastModified ? { headers : { "If-Modified-Since" : currentSchema . lastModified } } : { } ) ,
101+ } ;
102+
103+ try {
104+ const axios = getAxiosWithTimeouts ( timeout , timeout * 2 ) ;
105+ const response = await retry (
106+ ( ) => axios . get ( ECOSYSTEM_DATA_URL , requestConfig ) ,
107+ { attempts : 3 , interval : 1000 , throwLastErrorAsIs : true }
108+ ) ;
109+ const lastModified = typeof response . headers [ "last-modified" ] === "string"
110+ ? response . headers [ "last-modified" ]
111+ : undefined ;
112+
113+ if ( response . status === 304 ) {
114+ return { kind : "not-modified" } ;
115+ }
116+
117+ return {
118+ kind : "fetched" ,
119+ schema : validateSchema ( response . data ) ,
120+ ...( lastModified ? { lastModified} : { } ) ,
121+ } ;
122+ } catch ( e ) {
123+ console . error ( e ) ;
124+ return { kind : "failed" } ;
125+ }
126+ }
127+
128+ export async function updateLatestEcosystemSchema ( ) : Promise < void > {
129+ const bundledSchema = loadBundledSchema ( ) ;
130+ const currentSchema = await loadSavedEcosystemSchema ( ) ;
131+ const result = await fetchLatestSchema ( currentSchema ) ;
132+
133+ if ( result . kind === "not-modified" ) {
134+ return ;
135+ }
136+
137+ if ( result . kind === "failed" ) {
138+ if ( currentSchema != null ) {
139+ return ;
140+ }
141+
142+ await writeLatestEcosystemSchema ( bundledSchema ) ;
143+ await internalUpdateEcosystemReactives ( bundledSchema ) ;
144+ return ;
145+ }
146+
147+ const mergedSchema = mergeSchemas ( bundledSchema , result . schema ) ;
148+ await writeLatestEcosystemSchema ( mergedSchema , result . lastModified ) ;
149+ await internalUpdateEcosystemReactives ( mergedSchema ) ;
150+ }
151+
152+ async function writeLatestEcosystemSchema (
153+ schema : ThunderstoreEcosystem ,
154+ lastModified ?: string
155+ ) : Promise < void > {
156+ const asMergedSchema : VersionedThunderstoreEcosystem = {
157+ ...schema ,
158+ version : ManagerInformation . VERSION . toString ( ) ,
159+ ...( lastModified != null ? { lastModified} : { } ) ,
160+ } ;
161+ const writable = JSON . stringify ( asMergedSchema ) ;
162+ return FsProvider . instance . writeFile ( await getMergedEcosystemPath ( ) , writable ) ;
163+ }
164+
165+ async function readSavedEcosystemSchema ( ) : Promise < VersionedThunderstoreEcosystem > {
166+ const contentBuffer = await FsProvider . instance . readFile ( await getMergedEcosystemPath ( ) ) ;
167+ const content = contentBuffer . toString ( "utf8" ) ;
168+ const parsedContent = JSON . parse ( content ) ;
169+ const { version, lastModified, ...schemaContent } = parsedContent as VersionedThunderstoreEcosystem ;
170+ void version ;
171+ void lastModified ;
172+ validateSchema ( schemaContent ) ;
173+ return parsedContent as VersionedThunderstoreEcosystem ;
174+ }
175+
176+ async function loadSavedEcosystemSchema ( ) : Promise < VersionedThunderstoreEcosystem | null > {
74177 const mergeFilePath = await getMergedEcosystemPath ( ) ;
75- const bundledSchema = async ( ) => ( { ...( await loadBundledSchema ( ) ) , version : ManagerInformation . VERSION . toString ( ) } ) ;
76178 if ( ! ( await FsProvider . instance . exists ( mergeFilePath ) ) ) {
77- return bundledSchema ( ) ;
179+ return null ;
78180 }
181+
79182 try {
80- let content = await getLastSavedEcosystemSchema ( ) ;
183+ const content = await readSavedEcosystemSchema ( ) ;
81184 if ( ! new VersionNumber ( content . version ) . isEqualTo ( ManagerInformation . VERSION ) ) {
82- return bundledSchema ( ) ;
185+ return null ;
83186 }
84187 return content ;
85188 } catch ( e ) {
@@ -88,8 +191,16 @@ async function resolveCachedEcosystemSchema(): Promise<VersionedThunderstoreEcos
88191 LogSeverity . ERROR ,
89192 `Failed to load cached ecosystem schema, falling back to bundled schema\n${ err . message } `
90193 ) ;
91- return bundledSchema ( ) ;
194+ return null ;
195+ }
196+ }
197+
198+ async function resolveCachedEcosystemSchema ( ) : Promise < ThunderstoreEcosystem > {
199+ const content = await loadSavedEcosystemSchema ( ) ;
200+ if ( content != null ) {
201+ return content ;
92202 }
203+ return loadBundledSchema ( ) ;
93204}
94205
95206async function internalUpdateEcosystemReactives ( schema : ThunderstoreEcosystem ) : Promise < void > {
0 commit comments