pagefind
Version:
Implement search on any static website.
303 lines (280 loc) • 9.96 kB
JavaScript
import { PagefindService } from "./service.js";
import { decode } from "./encoding.js";
/**
* @typedef {import('pagefindInternal').InternalResponseCallback} InternalResponseCallback
* @typedef {import('pagefindInternal').InternalResponsePayload} InternalResponsePayload
*/
/**
* @type {PagefindService?}
*/
let persistentService;
const launch = () => {
if (!persistentService) {
persistentService = new PagefindService();
}
return persistentService;
}
/**
* @template T
* @param {function(any): void} resolve
* @param {function(any): void} reject
* @param {InternalResponseCallback} response_callback
* @param {function(InternalResponsePayload): T} resultFn
*/
const handleApiResponse = (resolve, reject, { exception, err, result }, resultFn) => {
if (exception) {
reject(exception);
} else {
resolve({
errors: err ? [err.message] : [],
...(result ? resultFn(result) : {})
});
}
}
/**
* @typedef {import('pagefindService').NewIndexResponse} NewIndexResponse
*
* @param {import('pagefindService').PagefindServiceConfig=} config
* @type {import('pagefindService').createIndex}
* */
export const createIndex = (config) => new Promise((resolve, reject) => {
// TODO: Validate `config` here, instead of waiting for the backend to throw an error.
// Ideally we create a global Pagefind config JSON schema that (a subset of) can be used here.
const action = 'NewIndex';
launch().sendMessage(
{
type: action,
config: {
root_selector: config?.rootSelector,
exclude_selectors: config?.excludeSelectors,
force_language: config?.forceLanguage,
verbose: config?.verbose,
logfile: config?.logfile,
keep_index_url: config?.keepIndexUrl,
write_playground: config?.writePlayground,
}
}, (response) => {
/** @type {function(InternalResponsePayload): Omit<NewIndexResponse, 'errors'>?} */
const successCallback = (success) => {
if (success.type !== action) {
reject(`Message returned from backend should have been ${action}, but was ${success.type}`);
return null;
}
return {
index: indexFns(success.index_id),
}
};
handleApiResponse(resolve, reject, response, successCallback);
}
);
});
/**
* @type {import('pagefindService').close}
*/
export const close = () => new Promise((resolve, reject) => {
persistentService?.close(null);
persistentService = null;
resolve(null);
});
/**
* @param {number} indexId
* @returns {import ('pagefindService').PagefindIndex}
*/
const indexFns = (indexId) => {
return {
addHTMLFile: (file) => addHTMLFile(indexId, file),
addCustomRecord: (record) => addCustomRecord(indexId, record),
addDirectory: (dir) => addDirectory(indexId, dir),
writeFiles: (options) => writeFiles(indexId, options),
getFiles: () => getFiles(indexId),
deleteIndex: () => deleteIndex(indexId)
}
}
/**
* @typedef {import('pagefindService').NewFileResponse} NewFileResponse
*
* @param {number} indexId
* @param {import('pagefindService').HTMLFile} file
* @returns {Promise<NewFileResponse>}
*/
const addHTMLFile = (indexId, file) => new Promise((resolve, reject) => {
const action = 'AddFile';
const responseAction = 'IndexedFile';
launch().sendMessage(
{
type: action,
index_id: indexId,
file_path: file.sourcePath,
url: file.url,
file_contents: file.content
}, (response) => {
/** @type {function(InternalResponsePayload): Omit<NewFileResponse, 'errors'>?} */
const successCallback = (success) => {
if (success.type !== responseAction) {
reject(`Message returned from backend should have been ${action}, but was ${success.type}`);
return null;
}
return {
file: {
uniqueWords: success.page_word_count,
url: success.page_url,
meta: success.page_meta,
}
}
};
handleApiResponse(resolve, reject, response, successCallback);
}
);
});
/**
* @param {number} indexId
* @param {import('pagefindService').CustomRecord} record
* @returns {Promise<NewFileResponse>}
*/
const addCustomRecord = (indexId, record) => new Promise((resolve, reject) => {
const action = 'AddRecord';
const responseAction = 'IndexedFile';
launch().sendMessage(
{
type: action,
index_id: indexId,
url: record.url,
content: record.content,
language: record.language,
meta: record.meta,
filters: record.filters,
sort: record.sort,
}, (response) => {
/** @type {function(InternalResponsePayload): Omit<NewFileResponse, 'errors'>?} */
const successCallback = (success) => {
if (success.type !== responseAction) {
reject(`Message returned from backend should have been ${action}, but was ${success.type}`);
return null;
}
return {
file: {
uniqueWords: success.page_word_count,
url: success.page_url,
meta: success.page_meta,
}
}
};
handleApiResponse(resolve, reject, response, successCallback);
}
);
});
/**
* @typedef {import('pagefindService').IndexingResponse} IndexingResponse
*
* @param {number} indexId
* @param {import('pagefindService').SiteDirectory} dir
* @returns {Promise<IndexingResponse>}
*/
const addDirectory = (indexId, dir) => new Promise((resolve, reject) => {
const action = 'AddDir';
const responseAction = 'IndexedDir';
launch().sendMessage(
{
type: action,
index_id: indexId,
path: dir.path,
glob: dir.glob
}, (response) => {
/** @type {function(InternalResponsePayload): Omit<IndexingResponse, 'errors'>?} */
const successCallback = (success) => {
if (success.type !== responseAction) {
reject(`Message returned from backend should have been ${action}, but was ${success.type}`);
return null;
}
return {
page_count: success.page_count
}
};
handleApiResponse(resolve, reject, response, successCallback);
}
);
});
/**
* @typedef {import ('pagefindService').WriteFilesResponse} WriteFilesResponse
*
* @param {number} indexId
* @param {import('pagefindService').WriteOptions=} options
* @returns {Promise<WriteFilesResponse>}
*/
const writeFiles = (indexId, options) => new Promise((resolve, reject) => {
const action = 'WriteFiles';
launch().sendMessage(
{
type: action,
index_id: indexId,
output_path: options?.outputPath
}, (response) => {
/** @type {function(InternalResponsePayload): Omit<WriteFilesResponse, 'errors'>?} */
const successCallback = (success) => {
if (success.type !== action) {
reject(`Message returned from backend should have been ${action}, but was ${success.type}`);
return null;
}
return {
outputPath: success.output_path
}
};
handleApiResponse(resolve, reject, response, successCallback);
}
);
});
/**
* @typedef {import ('pagefindService').GetFilesResponse} GetFilesResponse
*
* @param {number} indexId
* @returns {Promise<GetFilesResponse>}
*/
const getFiles = (indexId) => new Promise((resolve, reject) => {
const action = 'GetFiles';
launch().sendMessage(
{
type: action,
index_id: indexId,
}, (response) => {
/** @type {function(InternalResponsePayload): Omit<GetFilesResponse, 'errors'>?} */
const successCallback = (success) => {
if (success.type !== action) {
reject(`Message returned from backend should have been ${action}, but was ${success.type}`);
return null;
}
return {
files: success.files.map(file => {
return {
path: file.path,
content: decode(file.content)
}
})
}
};
handleApiResponse(resolve, reject, response, successCallback);
}
);
});
/**
* @param {number} indexId
* @returns {Promise<null>}
*/
const deleteIndex = (indexId) => new Promise((resolve, reject) => {
const action = 'DeleteIndex';
launch().sendMessage(
{
type: action,
index_id: indexId,
}, (response) => {
/** @type {function(InternalResponsePayload): Omit<GetFilesResponse, 'errors'>?} */
const successCallback = (success) => {
if (success.type !== action) {
reject(`Message returned from backend should have been ${action}, but was ${success.type}`);
return null;
}
return null;
};
handleApiResponse(resolve, reject, response, successCallback);
}
);
});