@@ -11,6 +11,8 @@ import { ProfileLogic } from '../types'
1111export function createProfileLogic ( store , authn , utilityLogic ) : ProfileLogic {
1212 const ns = namespace
1313 const containerLogic = createContainerLogic ( store )
14+ const loadPreferencesInFlight = new Map < string , Promise < NamedNode > > ( )
15+ const cachedPreferencesFileByWebId = new Map < string , NamedNode > ( )
1416
1517 function isAbsoluteHttpUri ( uri : string | null | undefined ) : boolean {
1618 return ! ! uri && ( uri . startsWith ( 'https://' ) || uri . startsWith ( 'http://' ) )
@@ -40,19 +42,6 @@ export function createProfileLogic(store, authn, utilityLogic): ProfileLogic {
4042 return text . includes ( '404' ) || text . includes ( 'Not Found' )
4143 }
4244
43- function ownerOnlyContainerAcl ( webId : string ) : string {
44- return [
45- '@prefix acl: <http://www.w3.org/ns/auth/acl#>.' ,
46- '' ,
47- '<#owner>' ,
48- 'a acl:Authorization;' ,
49- `acl:agent <${ webId } >;` ,
50- 'acl:accessTo <./>;' ,
51- 'acl:default <./>;' ,
52- 'acl:mode acl:Read, acl:Write, acl:Control.'
53- ] . join ( '\n' )
54- }
55-
5645 function publicTypeIndexAcl ( webId : string , publicTypeIndex : NamedNode ) : string {
5746 const fileName = new URL ( publicTypeIndex . uri ) . pathname . split ( '/' ) . pop ( ) || 'publicTypeIndex.ttl'
5847 return [
@@ -84,6 +73,19 @@ export function createProfileLogic(store, authn, utilityLogic): ProfileLogic {
8473 ] . join ( '\n' )
8574 }
8675
76+ function ownerOnlyContainerAcl ( webId : string ) : string {
77+ return [
78+ '@prefix acl: <http://www.w3.org/ns/auth/acl#>.' ,
79+ '' ,
80+ '<#owner>' ,
81+ 'a acl:Authorization;' ,
82+ `acl:agent <${ webId } >;` ,
83+ 'acl:accessTo <./>;' ,
84+ 'acl:default <./>;' ,
85+ 'acl:mode acl:Read, acl:Write, acl:Control.'
86+ ] . join ( '\n' )
87+ }
88+
8789 async function ensureContainerExists ( containerUri : string ) : Promise < void > {
8890 const containerNode = sym ( containerUri )
8991 try {
@@ -143,16 +145,15 @@ export function createProfileLogic(store, authn, utilityLogic): ProfileLogic {
143145
144146 const aclDoc = sym ( aclDocUri )
145147 try {
146- await store . fetcher . load ( aclDoc )
147- return
148- } catch ( err ) {
149- if ( ! isNotFoundError ( err ) ) throw err
148+ await store . fetcher . webOperation ( 'PUT' , aclDoc . uri , {
149+ data : publicTypeIndexAcl ( user . uri , publicTypeIndex ) ,
150+ contentType : 'text/turtle' ,
151+ headers : { 'If-None-Match' : '*' }
152+ } )
153+ } catch ( err : any ) {
154+ const status = err ?. response ?. status ?? err ?. status
155+ if ( status !== 412 ) throw err
150156 }
151-
152- await store . fetcher . webOperation ( 'PUT' , aclDoc . uri , {
153- data : publicTypeIndexAcl ( user . uri , publicTypeIndex ) ,
154- contentType : 'text/turtle'
155- } )
156157 }
157158
158159 async function ensurePrivateTypeIndexOnCreate ( privateTypeIndex : NamedNode ) : Promise < void > {
@@ -213,13 +214,12 @@ export function createProfileLogic(store, authn, utilityLogic): ProfileLogic {
213214
214215 async function ensurePreferencesDocExists ( preferencesFile : NamedNode ) : Promise < boolean > {
215216 try {
216- await store . fetcher . load ( preferencesFile )
217- return false
218- } catch ( err ) {
219- if ( isNotFoundError ( err ) ) {
220- await utilityLogic . loadOrCreateIfNotExists ( preferencesFile )
217+ const created = await utilityLogic . loadOrCreateWithContentOnCreate ( preferencesFile , '' )
218+ if ( created ) {
221219 return true
222220 }
221+ return false
222+ } catch ( err ) {
223223 if ( err . response ?. status === 401 ) {
224224 throw new UnauthorizedError ( )
225225 }
@@ -254,6 +254,17 @@ export function createProfileLogic(store, authn, utilityLogic): ProfileLogic {
254254 * @returns undefined if preferenceFile cannot be an Error or NamedNode if it can find it or create it
255255 */
256256 async function loadPreferences ( user : NamedNode ) : Promise < NamedNode > {
257+ const cachedPreferencesFile = cachedPreferencesFileByWebId . get ( user . uri )
258+ if ( cachedPreferencesFile ) {
259+ return cachedPreferencesFile
260+ }
261+
262+ const inFlight = loadPreferencesInFlight . get ( user . uri )
263+ if ( inFlight ) {
264+ return inFlight
265+ }
266+
267+ const run = ( async ( ) : Promise < NamedNode > => {
257268 await loadProfile ( user )
258269
259270 const possiblePreferencesFile = suggestPreferencesFile ( user )
@@ -267,7 +278,6 @@ export function createProfileLogic(store, authn, utilityLogic): ProfileLogic {
267278 }
268279
269280 await ensureOwnerOnlyAclForSettings ( user , preferencesFile as NamedNode )
270-
271281 await ensurePreferencesDocExists ( preferencesFile as NamedNode )
272282 await initializePreferencesDefaults ( user , preferencesFile as NamedNode )
273283 } catch ( err ) {
@@ -287,6 +297,14 @@ export function createProfileLogic(store, authn, utilityLogic): ProfileLogic {
287297 await store . fetcher . load ( preferencesFile as NamedNode )
288298 } catch ( err ) { // Maybe a permission problem or origin problem
289299 const msg = `Unable to load preference of user ${ user } : ${ err } `
300+ if ( err . response ?. status === 404 ) {
301+ // Self-heal when a stale profile pointer references a missing preferences file.
302+ await ensureOwnerOnlyAclForSettings ( user , preferencesFile as NamedNode )
303+ await ensurePreferencesDocExists ( preferencesFile as NamedNode )
304+ await initializePreferencesDefaults ( user , preferencesFile as NamedNode )
305+ await store . fetcher . load ( preferencesFile as NamedNode )
306+ return preferencesFile as NamedNode
307+ }
290308 debug . warn ( msg )
291309 if ( err . response . status === 401 ) {
292310 throw new UnauthorizedError ( )
@@ -302,7 +320,18 @@ export function createProfileLogic(store, authn, utilityLogic): ProfileLogic {
302320 }*/
303321 throw new Error ( msg )
304322 }
323+ cachedPreferencesFileByWebId . set ( user . uri , preferencesFile as NamedNode )
305324 return preferencesFile as NamedNode
325+ } ) ( )
326+
327+ loadPreferencesInFlight . set ( user . uri , run )
328+ try {
329+ return await run
330+ } finally {
331+ if ( loadPreferencesInFlight . get ( user . uri ) === run ) {
332+ loadPreferencesInFlight . delete ( user . uri )
333+ }
334+ }
306335 }
307336
308337 async function loadProfile ( user : NamedNode ) :Promise < NamedNode > {
0 commit comments