Skip to content

Commit fea4638

Browse files
committed
public index plus race cond fix
1 parent 1e8752f commit fea4638

1 file changed

Lines changed: 57 additions & 28 deletions

File tree

src/profile/profileLogic.ts

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { ProfileLogic } from '../types'
1111
export 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

Comments
 (0)