Приложение для заметок должно работать офлайн: пользователь в самолёте редактирует заметки, а при восстановлении интернета — синхронизирует с сервером. localStorage не подходит — только строки и 5 MB. Нужна IndexedDB: браузерная база данных с транзакциями, индексами и поддержкой любых объектов.
| | localStorage | IndexedDB |
|---|---|---|
| Объём | ~5 MB | Сотни MB и более |
| Тип данных | Только строки | Любые объекты, Blob, File |
| Запросы | Только по ключу | По ключу и по индексам |
| Транзакции | Нет | Есть (атомарность) |
| Асинхронность | Синхронный (блокирует UI) | Асинхронный |
const request = indexedDB.open('NotesDB', 1)
// Вызывается при первом открытии или увеличении версии
request.onupgradeneeded = (event) => {
const db = event.target.result
const notesStore = db.createObjectStore('notes', {
keyPath: 'id',
autoIncrement: true,
})
notesStore.createIndex('by-folder', 'folderId', { unique: false })
notesStore.createIndex('by-tag', 'tags', { unique: false, multiEntry: true })
}
request.onsuccess = (event) => {
const db = event.target.result
console.log('БД открыта:', db.name, 'v' + db.version)
}function addNote(db, note) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readwrite')
const req = tx.objectStore('notes').add(note)
req.onsuccess = () => resolve(req.result) // вернёт новый id
req.onerror = () => reject(req.error)
})
}
function getNote(db, id) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readonly')
const req = tx.objectStore('notes').get(id)
req.onsuccess = () => resolve(req.result)
req.onerror = () => reject(req.error)
})
}
function updateNote(db, note) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readwrite')
const req = tx.objectStore('notes').put(note) // put — добавит или обновит
req.onsuccess = () => resolve(req.result)
req.onerror = () => reject(req.error)
})
}
function deleteNote(db, id) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readwrite')
const req = tx.objectStore('notes').delete(id)
req.onsuccess = () => resolve()
req.onerror = () => reject(req.error)
})
}function getNotesByFolder(db, folderId) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readonly')
const idx = tx.objectStore('notes').index('by-folder')
const req = idx.getAll(folderId)
req.onsuccess = () => resolve(req.result)
req.onerror = () => reject(req.error)
})
}import { openDB } from 'idb'
const db = await openDB('NotesDB', 1, {
upgrade(db) {
const store = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true })
store.createIndex('by-folder', 'folderId')
}
})
await db.add('notes', { title: 'Встреча', folderId: 1, tags: ['работа'] })
const note = await db.get('notes', 1)
const all = await db.getAll('notes')Ошибка 1: readwrite-транзакция для чтения
// Сломано: readwrite блокирует другие транзакции без необходимости
const tx = db.transaction('notes', 'readwrite') // избыточно
// Исправлено:
const tx = db.transaction('notes', 'readonly') // для чтения достаточноОшибка 2: изменение схемы без увеличения версии
// onupgradeneeded не вызовется — версия та же!
const request = indexedDB.open('NotesDB', 1) // была 1, стала 1 — не обновляется
// Исправлено:
const request = indexedDB.open('NotesDB', 2) // увеличить версиюОшибка 3: await между операциями одной транзакции
// Сломано: await завершает транзакцию раньше времени
const tx = db.transaction('notes', 'readwrite')
await someAsyncOperation() // транзакция уже закрыта!
tx.objectStore('notes').add(note) // ошибка
// Исправлено: все операции транзакции — без await между ними
const tx = db.transaction('notes', 'readwrite')
const store = tx.objectStore('notes')
store.add(note1)
store.add(note2)Симуляция IndexedDB через Map: открытие, CRUD и поиск по индексу
// IndexedDB недоступен в sandbox — показываем тот же паттерн через Map
// В браузере замените MemoryDB на реальный indexedDB.open(...)
class MemoryDB {
constructor() {
this._stores = new Map()
}
createStore(name) {
this._stores.set(name, { data: new Map(), nextId: 1, indexes: {} })
return this
}
createIndex(storeName, indexName, field) {
this._stores.get(storeName).indexes[indexName] = field
return this
}
_store(name) {
if (!this._stores.has(name)) throw new Error(`Store "${name}" не существует`)
return this._stores.get(name)
}
add(storeName, item) {
const store = this._store(storeName)
const id = store.nextId++
store.data.set(id, { ...item, id })
return Promise.resolve(id)
}
get(storeName, id) {
return Promise.resolve(this._store(storeName).data.get(id))
}
getAll(storeName) {
return Promise.resolve([...this._store(storeName).data.values()])
}
getByIndex(storeName, indexName, value) {
const store = this._store(storeName)
const field = store.indexes[indexName]
const results = [...store.data.values()].filter(r =>
Array.isArray(r[field]) ? r[field].includes(value) : r[field] === value
)
return Promise.resolve(results)
}
put(storeName, item) {
this._store(storeName).data.set(item.id, item)
return Promise.resolve(item.id)
}
delete(storeName, id) {
this._store(storeName).data.delete(id)
return Promise.resolve()
}
}
// Инициализация (аналог onupgradeneeded)
const db = new MemoryDB()
.createStore('notes')
.createIndex('notes', 'by-folder', 'folderId')
.createIndex('notes', 'by-tag', 'tags')
async function main() {
// add — вставить записи
await db.add('notes', { title: 'Q1 план', folderId: 1, tags: ['работа', 'планирование'] })
await db.add('notes', { title: 'Ретроспектива', folderId: 1, tags: ['работа', 'встреча'] })
await db.add('notes', { title: 'Рецепт пасты', folderId: 2, tags: ['еда'] })
await db.add('notes', { title: 'Список покупок', folderId: 2, tags: ['еда', 'планирование'] })
await db.add('notes', { title: 'Идеи для проекта', folderId: 1, tags: ['работа', 'идеи'] })
// getAll
const all = await db.getAll('notes')
console.log(`Всего заметок: ${all.length}`) // 5
// get по id
const note1 = await db.get('notes', 1)
console.log('Заметка #1:', note1.title) // 'Q1 план'
// Поиск по индексу by-folder (аналог index.getAll в IndexedDB)
const workNotes = await db.getByIndex('notes', 'by-folder', 1)
console.log(`\nРабочие заметки (${workNotes.length}):`)
workNotes.forEach(n => console.log(` - ${n.title}`))
// - Q1 план
// - Ретроспектива
// - Идеи для проекта
// Поиск по тегу (multiEntry в IndexedDB)
const planningNotes = await db.getByIndex('notes', 'by-tag', 'планирование')
console.log(`\nЗаметки с тегом "планирование" (${planningNotes.length}):`)
planningNotes.forEach(n => console.log(` - ${n.title}`))
// - Q1 план
// - Список покупок
// put — обновить
const note = await db.get('notes', 1)
await db.put('notes', { ...note, title: 'Q1 план (обновлён)' })
const updated = await db.get('notes', 1)
console.log('\nПосле обновления:', updated.title) // 'Q1 план (обновлён)'
// delete
await db.delete('notes', 3)
const remaining = await db.getAll('notes')
console.log(`После удаления: ${remaining.length} заметок`) // 4
}
main()Приложение для заметок должно работать офлайн: пользователь в самолёте редактирует заметки, а при восстановлении интернета — синхронизирует с сервером. localStorage не подходит — только строки и 5 MB. Нужна IndexedDB: браузерная база данных с транзакциями, индексами и поддержкой любых объектов.
| | localStorage | IndexedDB |
|---|---|---|
| Объём | ~5 MB | Сотни MB и более |
| Тип данных | Только строки | Любые объекты, Blob, File |
| Запросы | Только по ключу | По ключу и по индексам |
| Транзакции | Нет | Есть (атомарность) |
| Асинхронность | Синхронный (блокирует UI) | Асинхронный |
const request = indexedDB.open('NotesDB', 1)
// Вызывается при первом открытии или увеличении версии
request.onupgradeneeded = (event) => {
const db = event.target.result
const notesStore = db.createObjectStore('notes', {
keyPath: 'id',
autoIncrement: true,
})
notesStore.createIndex('by-folder', 'folderId', { unique: false })
notesStore.createIndex('by-tag', 'tags', { unique: false, multiEntry: true })
}
request.onsuccess = (event) => {
const db = event.target.result
console.log('БД открыта:', db.name, 'v' + db.version)
}function addNote(db, note) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readwrite')
const req = tx.objectStore('notes').add(note)
req.onsuccess = () => resolve(req.result) // вернёт новый id
req.onerror = () => reject(req.error)
})
}
function getNote(db, id) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readonly')
const req = tx.objectStore('notes').get(id)
req.onsuccess = () => resolve(req.result)
req.onerror = () => reject(req.error)
})
}
function updateNote(db, note) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readwrite')
const req = tx.objectStore('notes').put(note) // put — добавит или обновит
req.onsuccess = () => resolve(req.result)
req.onerror = () => reject(req.error)
})
}
function deleteNote(db, id) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readwrite')
const req = tx.objectStore('notes').delete(id)
req.onsuccess = () => resolve()
req.onerror = () => reject(req.error)
})
}function getNotesByFolder(db, folderId) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readonly')
const idx = tx.objectStore('notes').index('by-folder')
const req = idx.getAll(folderId)
req.onsuccess = () => resolve(req.result)
req.onerror = () => reject(req.error)
})
}import { openDB } from 'idb'
const db = await openDB('NotesDB', 1, {
upgrade(db) {
const store = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true })
store.createIndex('by-folder', 'folderId')
}
})
await db.add('notes', { title: 'Встреча', folderId: 1, tags: ['работа'] })
const note = await db.get('notes', 1)
const all = await db.getAll('notes')Ошибка 1: readwrite-транзакция для чтения
// Сломано: readwrite блокирует другие транзакции без необходимости
const tx = db.transaction('notes', 'readwrite') // избыточно
// Исправлено:
const tx = db.transaction('notes', 'readonly') // для чтения достаточноОшибка 2: изменение схемы без увеличения версии
// onupgradeneeded не вызовется — версия та же!
const request = indexedDB.open('NotesDB', 1) // была 1, стала 1 — не обновляется
// Исправлено:
const request = indexedDB.open('NotesDB', 2) // увеличить версиюОшибка 3: await между операциями одной транзакции
// Сломано: await завершает транзакцию раньше времени
const tx = db.transaction('notes', 'readwrite')
await someAsyncOperation() // транзакция уже закрыта!
tx.objectStore('notes').add(note) // ошибка
// Исправлено: все операции транзакции — без await между ними
const tx = db.transaction('notes', 'readwrite')
const store = tx.objectStore('notes')
store.add(note1)
store.add(note2)Симуляция IndexedDB через Map: открытие, CRUD и поиск по индексу
// IndexedDB недоступен в sandbox — показываем тот же паттерн через Map
// В браузере замените MemoryDB на реальный indexedDB.open(...)
class MemoryDB {
constructor() {
this._stores = new Map()
}
createStore(name) {
this._stores.set(name, { data: new Map(), nextId: 1, indexes: {} })
return this
}
createIndex(storeName, indexName, field) {
this._stores.get(storeName).indexes[indexName] = field
return this
}
_store(name) {
if (!this._stores.has(name)) throw new Error(`Store "${name}" не существует`)
return this._stores.get(name)
}
add(storeName, item) {
const store = this._store(storeName)
const id = store.nextId++
store.data.set(id, { ...item, id })
return Promise.resolve(id)
}
get(storeName, id) {
return Promise.resolve(this._store(storeName).data.get(id))
}
getAll(storeName) {
return Promise.resolve([...this._store(storeName).data.values()])
}
getByIndex(storeName, indexName, value) {
const store = this._store(storeName)
const field = store.indexes[indexName]
const results = [...store.data.values()].filter(r =>
Array.isArray(r[field]) ? r[field].includes(value) : r[field] === value
)
return Promise.resolve(results)
}
put(storeName, item) {
this._store(storeName).data.set(item.id, item)
return Promise.resolve(item.id)
}
delete(storeName, id) {
this._store(storeName).data.delete(id)
return Promise.resolve()
}
}
// Инициализация (аналог onupgradeneeded)
const db = new MemoryDB()
.createStore('notes')
.createIndex('notes', 'by-folder', 'folderId')
.createIndex('notes', 'by-tag', 'tags')
async function main() {
// add — вставить записи
await db.add('notes', { title: 'Q1 план', folderId: 1, tags: ['работа', 'планирование'] })
await db.add('notes', { title: 'Ретроспектива', folderId: 1, tags: ['работа', 'встреча'] })
await db.add('notes', { title: 'Рецепт пасты', folderId: 2, tags: ['еда'] })
await db.add('notes', { title: 'Список покупок', folderId: 2, tags: ['еда', 'планирование'] })
await db.add('notes', { title: 'Идеи для проекта', folderId: 1, tags: ['работа', 'идеи'] })
// getAll
const all = await db.getAll('notes')
console.log(`Всего заметок: ${all.length}`) // 5
// get по id
const note1 = await db.get('notes', 1)
console.log('Заметка #1:', note1.title) // 'Q1 план'
// Поиск по индексу by-folder (аналог index.getAll в IndexedDB)
const workNotes = await db.getByIndex('notes', 'by-folder', 1)
console.log(`\nРабочие заметки (${workNotes.length}):`)
workNotes.forEach(n => console.log(` - ${n.title}`))
// - Q1 план
// - Ретроспектива
// - Идеи для проекта
// Поиск по тегу (multiEntry в IndexedDB)
const planningNotes = await db.getByIndex('notes', 'by-tag', 'планирование')
console.log(`\nЗаметки с тегом "планирование" (${planningNotes.length}):`)
planningNotes.forEach(n => console.log(` - ${n.title}`))
// - Q1 план
// - Список покупок
// put — обновить
const note = await db.get('notes', 1)
await db.put('notes', { ...note, title: 'Q1 план (обновлён)' })
const updated = await db.get('notes', 1)
console.log('\nПосле обновления:', updated.title) // 'Q1 план (обновлён)'
// delete
await db.delete('notes', 3)
const remaining = await db.getAll('notes')
console.log(`После удаления: ${remaining.length} заметок`) // 4
}
main()Реализуй класс MemoryStore — аналог IndexedDB objectStore на основе Map. Методы: add(item) возвращает id; get(id) — запись или undefined; getAll() — массив всех записей; put(item) — обновляет по item.id; delete(id) — возвращает true если удалена.
get: return this._data.get(id); getAll: return [...this._data.values()]; put: this._data.set(item.id, item); delete: return this._data.delete(id)