rxdb
Version:
A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/
174 lines (171 loc) • 5.62 kB
JavaScript
import { newRxError, newRxFetchError } from "../../rx-error.js";
import { deepEqual, ensureNotFalsy } from "../utils/index.js";
import { fetchDocumentContents, getDocumentFiles, insertDocumentFiles, updateDocumentFiles } from "./document-handling.js";
import { DRIVE_MAX_BULK_SIZE, fillFileIfEtagMatches } from "./google-drive-helper.js";
export var WAL_FILE_NAME = 'rxdb-wal.json';
export async function fetchConflicts(googleDriveOptions, init, primaryPath, writeRows) {
if (writeRows.length > DRIVE_MAX_BULK_SIZE) {
throw newRxError('GDR18', {
args: {
DRIVE_MAX_BULK_SIZE
}
});
}
var ids = writeRows.map(row => row.newDocumentState[primaryPath]);
var filesMeta = await getDocumentFiles(googleDriveOptions, init, ids);
var fileIdByDocId = new Map();
var fileIds = filesMeta.files.map(f => {
var fileId = ensureNotFalsy(f.id);
var docId = f.name.split('.')[0];
fileIdByDocId.set(docId, fileId);
return fileId;
});
var contentsByFileId = await fetchDocumentContents(googleDriveOptions, fileIds);
var conflicts = [];
var nonConflicts = [];
writeRows.forEach(row => {
var docId = row.newDocumentState[primaryPath];
var fileContent;
var fileId = fileIdByDocId.get(docId);
if (fileId) {
fileContent = contentsByFileId.byId[fileId];
}
if (row.assumedMasterState) {
if (!deepEqual(row.assumedMasterState, fileContent)) {
conflicts.push(ensureNotFalsy(fileContent));
} else {
nonConflicts.push(row);
}
} else if (fileContent) {
conflicts.push(fileContent);
} else {
nonConflicts.push(row);
}
});
if (nonConflicts.length + conflicts.length !== writeRows.length) {
throw newRxError('SNH', {
pushRows: writeRows,
args: {
nonConflicts,
conflicts,
contentsByFileId: contentsByFileId.byId
}
});
}
return {
conflicts,
nonConflicts
};
}
export async function writeToWal(googleDriveOptions, init, writeRows) {
var walFileId = init.walFile.fileId;
var metaUrl = googleDriveOptions.apiEndpoint + ("/drive/v2/files/" + encodeURIComponent(walFileId) + "?") + new URLSearchParams({
fields: "id,fileSize,mimeType,title,etag",
supportsAllDrives: "true"
}).toString();
var metaRes = await fetch(metaUrl, {
method: "GET",
headers: {
Authorization: "Bearer " + googleDriveOptions.authToken
}
});
if (!metaRes.ok) {
throw await newRxFetchError(metaRes);
}
var meta = await metaRes.json();
var sizeStr = meta.fileSize ?? "0";
var sizeNum = Number(sizeStr);
if (writeRows && (!meta.fileSize || sizeNum > 0)) {
throw newRxError("GDR19", {
args: {
sizeNum,
walFileId,
size: meta.size,
meta,
writeRows: writeRows?.length
}
});
}
var etag = ensureNotFalsy(metaRes.headers.get("etag"), 'etag missing');
var writeResult = await fillFileIfEtagMatches(googleDriveOptions, walFileId, etag, writeRows);
if (writeResult.status !== 200) {
throw newRxError("GDR19", {
args: {
walFileId,
meta,
writeRows: writeRows?.length
}
});
}
}
export async function readWalContent(googleDriveOptions, init) {
var walFileId = init.walFile.fileId;
var contentUrl = googleDriveOptions.apiEndpoint + ("/drive/v2/files/" + encodeURIComponent(walFileId) + "?alt=media");
var res = await fetch(contentUrl, {
method: "GET",
headers: {
Authorization: "Bearer " + googleDriveOptions.authToken
}
});
if (!res.ok) {
throw await newRxFetchError(res);
}
var etag = ensureNotFalsy(res.headers.get("etag"), "etag missing on WAL read");
var text = await res.text();
// If empty or whitespace → no WAL entries
if (!text || !text.trim()) {
return {
etag,
rows: undefined
};
}
return {
etag,
rows: JSON.parse(text)
};
}
/**
* Here we read the WAL file content
* and sort the content into the actual
* document files.
* Notice that when the JavaScript process
* exists at any point here, we need to have
* a recoverable state on the next run. So this
* must be idempotent.
*/
export async function processWalFile(googleDriveOptions, init, primaryPath) {
var content = await readWalContent(googleDriveOptions, init);
if (!content.rows) {
return;
}
var docIds = content.rows.map(row => row.newDocumentState[primaryPath]);
var docFiles = await getDocumentFiles(googleDriveOptions, init, docIds);
var fileMetaByDocId = {};
docFiles.files.forEach(file => {
var docId = file.name.split('.')[0];
fileMetaByDocId[docId] = {
fileId: file.id,
etag: ensureNotFalsy(file.etag)
};
});
var toInsert = [];
var toUpdate = [];
content.rows.filter(row => {
var docId = row.newDocumentState[primaryPath];
var fileExists = fileMetaByDocId[docId];
if (!fileExists) {
toInsert.push(row.newDocumentState);
} else {
toUpdate.push(row.newDocumentState);
}
});
await Promise.all([insertDocumentFiles(googleDriveOptions, init, primaryPath, toInsert), updateDocumentFiles(googleDriveOptions, primaryPath, toUpdate, fileMetaByDocId)]);
// overwrite wal with emptyness
await writeToWal(googleDriveOptions, init, undefined);
}
export async function handleUpstreamBatch(googleDriveOptions, init, primaryPath, writeRows) {
var conflictResult = await fetchConflicts(googleDriveOptions, init, primaryPath, writeRows);
await writeToWal(googleDriveOptions, init, conflictResult.nonConflicts);
return conflictResult.conflicts;
}
//# sourceMappingURL=upstream.js.map