wise-json-db
Version:
Blazing fast, crash-proof embedded JSON database for Node.js with batch operations, TTL, indexes, and segmented checkpointing.
261 lines (231 loc) • 11.8 kB
JavaScript
// wise-json/collection/ops.js
const { isAlive } = require('./ttl.js');
const logger = require('../logger'); // Убедитесь, что logger импортирован
async function insert(doc) {
if (!this.isPlainObject(doc)) {
throw new Error('insert: аргумент должен быть объектом.');
}
return this._enqueue(async () => {
const _id = doc._id || this._idGenerator();
const now = new Date().toISOString();
const finalDoc = {
...doc,
_id,
createdAt: doc.createdAt || now,
updatedAt: now,
};
const result = await this._enqueueDataModification(
{ op: 'INSERT', doc: finalDoc },
'INSERT',
(_prev, insertedDoc) => insertedDoc
);
this._stats.inserts++;
return result;
});
}
async function insertMany(docs) {
if (!Array.isArray(docs)) {
throw new Error('insertMany: аргумент должен быть массивом.');
}
if (docs.length === 0) {
return [];
}
// Максимальное количество документов в одной WAL-записи BATCH_INSERT.
// Можно сделать настраиваемым через this.options, если необходимо.
const MAX_DOCS_PER_BATCH_WAL_ENTRY = this.options?.maxDocsPerBatchWalEntry || 1000;
// Если в this.options нет maxDocsPerBatchWalEntry, используем 1000 по умолчанию.
// Вся операция insertMany (включая все чанки) должна быть атомарной
// с точки зрения блокировки коллекции, поэтому оборачиваем все в один _enqueue.
return this._enqueue(async () => {
await this._acquireLock(); // Захватываем блокировку в начале
const allInsertedDocs = [];
let totalProcessed = 0;
try {
for (let i = 0; i < docs.length; i += MAX_DOCS_PER_BATCH_WAL_ENTRY) {
const chunk = docs.slice(i, i + MAX_DOCS_PER_BATCH_WAL_ENTRY);
// logger.debug(`[Ops] insertMany: обрабатываем чанк ${i / MAX_DOCS_PER_BATCH_WAL_ENTRY + 1} из ${Math.ceil(docs.length / MAX_DOCS_PER_BATCH_WAL_ENTRY)}, размер: ${chunk.length}`);
const now = new Date().toISOString();
const preparedChunk = chunk.map(doc => ({
...doc,
_id: doc._id || this._idGenerator(),
createdAt: doc.createdAt || now, // Используем один 'now' для всего чанка
updatedAt: now,
}));
// Каждая порция (chunk) записывается как отдельная BATCH_INSERT операция в WAL
// _enqueueDataModification выполняет запись в WAL и применение в памяти.
// Важно: _enqueueDataModification сам по себе не должен вызывать _acquireLock/_releaseLock,
// так как мы уже под общей блокировкой.
const insertedChunk = await this._enqueueDataModification( // Предполагается, что этот метод не вызывает _acquireLock
{ op: 'BATCH_INSERT', docs: preparedChunk },
'BATCH_INSERT',
(_prev, inserted) => inserted
);
if (Array.isArray(insertedChunk)) { // Убедимся, что результат - массив
allInsertedDocs.push(...insertedChunk);
this._stats.inserts += insertedChunk.length;
totalProcessed += insertedChunk.length;
} else {
// Это не должно произойти, если _enqueueDataModification для BATCH_INSERT возвращает массив
logger.warn(`[Ops] insertMany: _enqueueDataModification для чанка не вернул массив. Чанк пропущен или обработан некорректно.`);
}
}
// logger.debug(`[Ops] insertMany: успешно обработано ${totalProcessed} документов из ${docs.length}.`);
return allInsertedDocs;
} catch (error) {
// Если произошла ошибка при обработке любого из чанков (например, нарушение уникальности
// которое было проверено внутри _enqueueDataModification, или ошибка записи WAL для чанка),
// то вся операция insertMany откатывается (т.к. мы под одним _enqueue).
// В текущей реализации _enqueueDataModification сам бросит ошибку, и она будет поймана
// обработчиком ошибок в _processQueue, который вызовет task.reject(err).
// Поэтому здесь мы просто пробрасываем ошибку дальше.
logger.error(`[Ops] insertMany: ошибка во время обработки чанков: ${error.message}. Обработано до ошибки: ${totalProcessed} документов.`);
throw error;
} finally {
await this._releaseLockIfHeld(); // Освобождаем блокировку в конце
}
});
}
async function update(id, updates) {
if (typeof id !== 'string' || id.length === 0) {
throw new Error('update: id должен быть непустой строкой.');
}
if (!this.isPlainObject(updates)) {
throw new Error('update: обновления должны быть объектом.');
}
return this._enqueue(async () => {
if (!this.documents.has(id)) {
return null;
}
const now = new Date().toISOString();
const result = await this._enqueueDataModification(
{ op: 'UPDATE', id, data: { ...updates, updatedAt: now } },
'UPDATE',
(_prev, updatedDoc) => updatedDoc,
{ idToUpdate: id }
);
if (result) {
this._stats.updates++;
}
return result;
});
}
async function updateMany(queryFn, updates) {
if (typeof queryFn !== 'function') {
throw new Error('updateMany: queryFn должен быть функцией.');
}
if (!this.isPlainObject(updates)) {
throw new Error('updateMany: обновления должны быть объектом.');
}
// Собираем ID ДО постановки в очередь, чтобы не итерировать по изменяемой коллекции.
const idsToUpdate = [];
// Эта часть выполняется вне _enqueue, читая текущее состояние this.documents.
// Это нормально, так как фактические изменения будут в _enqueue.
for (const [id, doc] of this.documents.entries()) {
if (isAlive(doc) && queryFn(doc)) {
idsToUpdate.push(id);
}
}
if (idsToUpdate.length === 0) {
return 0;
}
// Все обновления для updateMany выполняются в рамках одного _enqueue вызова
// для обеспечения атомарности на уровне всей операции updateMany, если это возможно.
// Однако, this.update внутри цикла сам вызывает _enqueue.
// Чтобы сделать updateMany по-настоящему атомарным (все или ничего для всех найденных документов),
// потребовалась бы другая архитектура для _enqueueDataModification, принимающая массив обновлений.
// Текущая реализация делает каждую отдельную операцию update атомарной, но не весь updateMany.
// Оставляем текущую реализацию, где каждое обновление - отдельная операция в очереди.
// Это проще, но менее атомарно для всего набора.
let successfullyUpdatedCount = 0;
for (const id of idsToUpdate) { // Этот цикл выполнится вне _enqueue
try {
// Каждый this.update будет поставлен в очередь и выполнен последовательно.
const updatedDoc = await this.update(id, updates);
if (updatedDoc) {
successfullyUpdatedCount++;
}
} catch (error) {
// Если один из update падает (например, нарушение уникальности),
// то updateMany прерывается здесь, и предыдущие успешные обновления остаются.
logger.error(`[Ops] Ошибка при обновлении документа ID '${id}' в updateMany. Прерывание. Ошибка: ${error.message}`);
throw error;
}
}
return successfullyUpdatedCount;
}
async function remove(id) {
if (typeof id !== 'string' || id.length === 0) {
throw new Error('remove: id должен быть непустой строкой.');
}
if (!this.documents.has(id)) {
return false;
}
return this._enqueue(async () => {
if (!this.documents.has(id)) {
return false;
}
const success = await this._enqueueDataModification(
{ op: 'REMOVE', id },
'REMOVE',
(_prev, _next) => true,
{ idToRemove: id }
);
if (success) {
this._stats.removes++;
}
return success;
});
}
async function removeMany(predicate) {
if (typeof predicate !== 'function') {
throw new Error('removeMany: predicate должен быть функцией.');
}
const idsToRemove = [];
for (const [id, doc] of this.documents.entries()) {
if (isAlive(doc) && predicate(doc)) {
idsToRemove.push(id);
}
}
if (idsToRemove.length === 0) {
return 0;
}
let removedCount = 0;
for (const id of idsToRemove) { // Аналогично updateMany, цикл вне _enqueue
try {
const success = await this.remove(id); // Каждый remove ставится в очередь
if (success) {
removedCount++;
}
} catch (error) {
logger.error(`[Ops] Ошибка при удалении документа ID '${id}' в removeMany. Прерывание. Ошибка: ${error.message}`);
throw error;
}
}
return removedCount;
}
async function clear() {
return this._enqueue(async () => {
const success = await this._enqueueDataModification(
{ op: 'CLEAR' },
'CLEAR',
() => true
);
if (success) {
this._stats.clears++;
this._stats.inserts = 0;
this._stats.updates = 0;
this._stats.removes = 0;
this._stats.walEntriesSinceCheckpoint = 0;
}
return success;
});
}
module.exports = {
insert,
insertMany,
update,
updateMany,
remove,
removeMany,
clear,
};