Skip to content

Commit 39e6a10

Browse files
refactor: optimize WebID profile fetching and fix storage discovery
1 parent 35f42fc commit 39e6a10

4 files changed

Lines changed: 134 additions & 175 deletions

File tree

app/lib/helpers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export * from "./fileFilters";
55
export * from "./urlUtils";
66
export * from "./breadcrumbUtils";
77
export * from "./fileTypeUtils";
8+
export * from "./profileUtils";
89

app/lib/helpers/profileUtils.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { getDefaultSession } from "@inrupt/solid-client-authn-browser";
2+
import { Parser, Store, NamedNode } from "n3";
3+
4+
// Cache for parsed profile documents
5+
const profileCache = new Map<string, { store: Store; baseUrl: string; mainSubject: NamedNode }>();
6+
7+
/**
8+
* Fetches and parses the WebID profile document, with caching to avoid duplicate fetches
9+
* @param webId - The WebID to fetch
10+
* @returns The parsed RDF store, base URL, and main subject
11+
*/
12+
export async function fetchAndParseProfile(
13+
webId: string
14+
): Promise<{ store: Store; baseUrl: string; mainSubject: NamedNode }> {
15+
// Check cache first
16+
if (profileCache.has(webId)) {
17+
return profileCache.get(webId)!;
18+
}
19+
20+
const session = getDefaultSession();
21+
const fetchFn = session.fetch || fetch;
22+
23+
// Try different Accept headers to get the profile
24+
const acceptHeaders = [
25+
'text/turtle, application/turtle, text/n3, application/n3',
26+
'text/turtle',
27+
'application/ld+json',
28+
];
29+
30+
let content: string | null = null;
31+
let contentType: string = '';
32+
33+
for (const acceptHeader of acceptHeaders) {
34+
try {
35+
const response = await fetchFn(webId, {
36+
method: 'GET',
37+
headers: {
38+
'Accept': acceptHeader,
39+
},
40+
});
41+
42+
if (response.ok) {
43+
contentType = response.headers.get('content-type') || '';
44+
content = await response.text();
45+
break;
46+
}
47+
} catch (err) {
48+
continue;
49+
}
50+
}
51+
52+
if (!content) {
53+
throw new Error("Failed to fetch profile document with any Accept header");
54+
}
55+
56+
// Parse the RDF content
57+
const store = new Store();
58+
const baseUrl = webId.split('#')[0];
59+
60+
if (contentType.includes('text/turtle') || contentType.includes('application/turtle') ||
61+
contentType.includes('text/n3') || contentType.includes('application/n3')) {
62+
const parser = new Parser({ baseIRI: baseUrl });
63+
const quads = parser.parse(content);
64+
store.addQuads(quads);
65+
} else if (contentType.includes('application/ld+json')) {
66+
// Try parsing as Turtle anyway (most servers return Turtle even if JSON-LD is requested)
67+
try {
68+
const parser = new Parser({ baseIRI: baseUrl });
69+
const quads = parser.parse(content);
70+
store.addQuads(quads);
71+
} catch (e) {
72+
// Silent error handling
73+
}
74+
}
75+
76+
// Find the main subject - try different variants
77+
const FOAF_NAME = "http://xmlns.com/foaf/0.1/name";
78+
const subjectVariants = [
79+
new NamedNode(webId),
80+
new NamedNode(baseUrl + '#me'),
81+
new NamedNode('#me'),
82+
new NamedNode(baseUrl + '#card'),
83+
];
84+
85+
let mainSubject: NamedNode | null = null;
86+
87+
for (const subject of subjectVariants) {
88+
const nameQuads = store.getQuads(subject, new NamedNode(FOAF_NAME), null, null);
89+
if (nameQuads.length > 0) {
90+
mainSubject = subject;
91+
break;
92+
}
93+
}
94+
95+
// If still not found, try to find Person type
96+
if (!mainSubject) {
97+
const personType = new NamedNode('http://xmlns.com/foaf/0.1/Person');
98+
const personQuads = store.getQuads(null, new NamedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), personType, null);
99+
if (personQuads.length > 0 && personQuads[0].subject.termType === 'NamedNode') {
100+
mainSubject = personQuads[0].subject as NamedNode;
101+
}
102+
}
103+
104+
// Fallback to WebID itself
105+
if (!mainSubject) {
106+
mainSubject = new NamedNode(webId);
107+
}
108+
109+
// Cache the result
110+
const result = { store, baseUrl, mainSubject };
111+
profileCache.set(webId, result);
112+
113+
return result;
114+
}
115+
116+
/**
117+
* Clears the profile cache (useful for testing or when profile might have changed)
118+
*/
119+
export function clearProfileCache(webId?: string): void {
120+
if (webId) {
121+
profileCache.delete(webId);
122+
} else {
123+
profileCache.clear();
124+
}
125+
}
126+

app/lib/hooks/useSolidStorages.ts

Lines changed: 3 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useEffect, useState } from "react";
44
import { getDefaultSession } from "@inrupt/solid-client-authn-browser";
55
import { Parser, Store, NamedNode, Literal } from "n3";
6+
import { fetchAndParseProfile } from "../helpers/profileUtils";
67

78
// Storage predicates and types
89
const PIM_STORAGE = "http://www.w3.org/ns/pim/space#storage";
@@ -257,96 +258,8 @@ export function useSolidStorages(): UseSolidStoragesResult {
257258

258259
const webId = session.info.webId;
259260

260-
// Try different Accept headers to get the profile
261-
const acceptHeaders = [
262-
'text/turtle, application/turtle, text/n3, application/n3',
263-
'text/turtle',
264-
'application/ld+json',
265-
];
266-
267-
let content: string | null = null;
268-
let contentType: string = '';
269-
270-
for (const acceptHeader of acceptHeaders) {
271-
try {
272-
// Always use the authenticated session's fetch function
273-
const fetchFn = session.fetch || fetch;
274-
const response = await fetchFn(webId, {
275-
method: 'GET',
276-
headers: {
277-
'Accept': acceptHeader,
278-
},
279-
});
280-
281-
if (response.ok) {
282-
contentType = response.headers.get('content-type') || '';
283-
content = await response.text();
284-
break;
285-
}
286-
} catch (err) {
287-
continue;
288-
}
289-
}
290-
291-
if (!content) {
292-
throw new Error("Failed to fetch profile document with any Accept header");
293-
}
294-
295-
// Parse the RDF content
296-
const store = new Store();
297-
298-
// Extract base URL for resolving relative URIs like <#me>
299-
const baseUrl = webId.split('#')[0];
300-
301-
if (contentType.includes('text/turtle') || contentType.includes('application/turtle') ||
302-
contentType.includes('text/n3') || contentType.includes('application/n3')) {
303-
const parser = new Parser({ baseIRI: baseUrl });
304-
const quads = parser.parse(content);
305-
store.addQuads(quads);
306-
} else if (contentType.includes('application/ld+json')) {
307-
// For JSON-LD, we'd need a different parser, but for now let's try to extract from Turtle
308-
// Most Solid servers return Turtle even if JSON-LD is requested
309-
try {
310-
const parser = new Parser({ baseIRI: baseUrl });
311-
const quads = parser.parse(content);
312-
store.addQuads(quads);
313-
} catch (e) {
314-
// TODO: Add JSON-LD parsing if needed
315-
}
316-
}
317-
318-
// Find the main subject - try different variants
319-
const subjectVariants = [
320-
new NamedNode(webId),
321-
new NamedNode(baseUrl + '#me'),
322-
new NamedNode('#me'),
323-
new NamedNode(baseUrl + '#card'),
324-
];
325-
326-
// Find the main subject by looking for common profile properties
327-
let mainSubject: NamedNode | null = null;
328-
329-
for (const subject of subjectVariants) {
330-
const nameQuads = store.getQuads(subject, new NamedNode(FOAF_NAME), null, null);
331-
if (nameQuads.length > 0) {
332-
mainSubject = subject;
333-
break;
334-
}
335-
}
336-
337-
// If still not found, try to find Person type
338-
if (!mainSubject) {
339-
const personType = new NamedNode('http://xmlns.com/foaf/0.1/Person');
340-
const personQuads = store.getQuads(null, new NamedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), personType, null);
341-
if (personQuads.length > 0 && personQuads[0].subject.termType === 'NamedNode') {
342-
mainSubject = personQuads[0].subject as NamedNode;
343-
}
344-
}
345-
346-
// Fallback to WebID itself
347-
if (!mainSubject) {
348-
mainSubject = new NamedNode(webId);
349-
}
261+
// Use shared profile fetching utility (with caching)
262+
const { store, baseUrl, mainSubject } = await fetchAndParseProfile(webId);
350263

351264
// Get profile name
352265
const getName = (subject: NamedNode): string | null => {

app/lib/hooks/useUserProfile.ts

Lines changed: 4 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import { useEffect, useState } from "react";
44
import { getDefaultSession } from "@inrupt/solid-client-authn-browser";
5-
import { Parser, Store, NamedNode, Literal } from "n3";
5+
import { NamedNode, Literal } from "n3";
6+
import { fetchAndParseProfile } from "../helpers/profileUtils";
67

78
// vCard predicates
89
const VCARD_FN = "http://www.w3.org/2006/vcard/ns#fn";
@@ -68,90 +69,8 @@ export function useUserProfile(): UseUserProfileResult {
6869

6970
const webId = session.info.webId;
7071

71-
// Fetch the profile document
72-
const acceptHeaders = [
73-
'text/turtle, application/turtle, text/n3, application/n3',
74-
'text/turtle',
75-
'application/ld+json',
76-
];
77-
78-
let content: string | null = null;
79-
let contentType: string = '';
80-
81-
for (const acceptHeader of acceptHeaders) {
82-
try {
83-
const fetchFn = session.fetch || fetch;
84-
const response = await fetchFn(webId, {
85-
method: 'GET',
86-
headers: {
87-
'Accept': acceptHeader,
88-
},
89-
});
90-
91-
if (response.ok) {
92-
contentType = response.headers.get('content-type') || '';
93-
content = await response.text();
94-
break;
95-
}
96-
} catch (err) {
97-
continue;
98-
}
99-
}
100-
101-
if (!content) {
102-
throw new Error("Failed to fetch profile document");
103-
}
104-
105-
// Parse the RDF content
106-
const store = new Store();
107-
if (contentType.includes('text/turtle') || contentType.includes('application/turtle') ||
108-
contentType.includes('text/n3') || contentType.includes('application/n3')) {
109-
const parser = new Parser({ baseIRI: webId });
110-
const quads = parser.parse(content);
111-
store.addQuads(quads);
112-
} else {
113-
// Try parsing as Turtle anyway
114-
try {
115-
const parser = new Parser({ baseIRI: webId });
116-
const quads = parser.parse(content);
117-
store.addQuads(quads);
118-
} catch (e) {
119-
// Silent error handling
120-
}
121-
}
122-
123-
// Find the main subject
124-
const baseUrl = webId.split('#')[0];
125-
const subjectVariants = [
126-
new NamedNode(webId),
127-
new NamedNode(baseUrl + '#me'),
128-
new NamedNode('#me'),
129-
new NamedNode(baseUrl + '#card'),
130-
];
131-
132-
let mainSubject: NamedNode | null = null;
133-
134-
for (const subject of subjectVariants) {
135-
const nameQuads = store.getQuads(subject, new NamedNode(FOAF_NAME), null, null);
136-
if (nameQuads.length > 0) {
137-
mainSubject = subject;
138-
break;
139-
}
140-
}
141-
142-
// If still not found, try to find Person type
143-
if (!mainSubject) {
144-
const personType = new NamedNode('http://xmlns.com/foaf/0.1/Person');
145-
const personQuads = store.getQuads(null, new NamedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), personType, null);
146-
if (personQuads.length > 0 && personQuads[0].subject.termType === 'NamedNode') {
147-
mainSubject = personQuads[0].subject as NamedNode;
148-
}
149-
}
150-
151-
// Fallback to WebID itself
152-
if (!mainSubject) {
153-
mainSubject = new NamedNode(webId);
154-
}
72+
// Use shared profile fetching utility (with caching)
73+
const { store, baseUrl, mainSubject } = await fetchAndParseProfile(webId);
15574

15675
// Extract profile information
15776
let name: string | null = null;

0 commit comments

Comments
 (0)