Skip to content

Commit

Permalink
feat(core): use indexedDB POC for editState
Browse files Browse the repository at this point in the history
  • Loading branch information
pedrobonamin committed Sep 26, 2024
1 parent c98ed5e commit 7f95804
Show file tree
Hide file tree
Showing 2 changed files with 247 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import {type SanityClient} from '@sanity/client'
import {type SanityDocument, type Schema} from '@sanity/types'
import {combineLatest, defer, merge, type Observable, of} from 'rxjs'
Expand All @@ -7,8 +8,8 @@ import {type IdPair, type PendingMutationsEvent} from '../types'
import {memoize} from '../utils/createMemoizer'
import {memoizeKeyGen} from './memoizeKeyGen'
import {snapshotPair} from './snapshotPair'
import {getPairFromIndexedDB, savePairToIndexedDB} from './utils/indexedDbPOC'
import {isLiveEditEnabled} from './utils/isLiveEditEnabled'
import {getPairFromLocalStorage, savePairToLocalStorage} from './utils/localStoragePOC'

interface TransactionSyncLockState {
enabled: boolean
Expand Down Expand Up @@ -57,42 +58,62 @@ export const editState = memoize(
} | null = null

function getCachedPair() {
// try first read it from memory
// if we haven't got it in memory, see if it's in localstorage
// read the memoized value, if we don't have we will search it in the indexedDB
if (cachedDocumentPair) {
return cachedDocumentPair
}
return getPairFromLocalStorage(idPair)
return null
}

return snapshotPair(ctx.client, idPair, typeName, ctx.serverActionsEnabled).pipe(
switchMap((versions) =>
combineLatest([
versions.draft.snapshots$,
versions.published.snapshots$,
versions.transactionsPendingEvents$.pipe(
// eslint-disable-next-line max-nested-callbacks
map((ev: PendingMutationsEvent) => (ev.phase === 'begin' ? LOCKED : NOT_LOCKED)),
startWith(NOT_LOCKED),
return defer(() => {
const cachedPair = getCachedPair()
if (cachedPair) {
console.log('using cachedPair, no need to check the indexedDB')
return of(cachedPair)
}
return getPairFromIndexedDB(idPair)
}).pipe(
switchMap((initialPair) => {
console.log('cached pair', initialPair)
return snapshotPair(ctx.client, idPair, typeName, ctx.serverActionsEnabled).pipe(
switchMap((versions) =>
combineLatest([
versions.draft.snapshots$,
versions.published.snapshots$,
versions.transactionsPendingEvents$.pipe(
// eslint-disable-next-line max-nested-callbacks
map((ev: PendingMutationsEvent) => (ev.phase === 'begin' ? LOCKED : NOT_LOCKED)),
startWith(NOT_LOCKED),
),
]),
),
]),
),
tap(([draft, published]) => {
cachedDocumentPair = {draft, published}
tap(([draft, published]) => {
cachedDocumentPair = {draft, published}
}),
map(([draftSnapshot, publishedSnapshot, transactionSyncLock]) => ({
id: idPair.publishedId,
type: typeName,
draft: draftSnapshot,
published: publishedSnapshot,
liveEdit,
ready: true,
transactionSyncLock,
})),
startWith({
id: idPair.publishedId,
type: typeName,
draft: initialPair.draft,
published: initialPair.published,
liveEdit,
ready: false,
transactionSyncLock: null,
}),
)
}),
map(([draftSnapshot, publishedSnapshot, transactionSyncLock]) => ({
id: idPair.publishedId,
type: typeName,
draft: draftSnapshot,
published: publishedSnapshot,
liveEdit,
ready: true,
transactionSyncLock,
})),
// todo: turn this into a proper operator function - It's like startWith only that it takes a function that will be invoked upon subscription
(input$) => {
return defer(() => {
const cachedPair = getCachedPair()
console.log('creating initial value for editState observable, cachedPair:', cachedPair)
return merge(
cachedPair
? of({
Expand All @@ -110,7 +131,11 @@ export const editState = memoize(
})
},
finalize(() => {
savePairToLocalStorage(cachedDocumentPair)
console.log(
'Closing subscription for: ',
cachedDocumentPair?.published?._id || cachedDocumentPair?.draft?._id,
)
savePairToIndexedDB(cachedDocumentPair)
}),
publishReplay(1),
refCount(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/* eslint-disable no-console */
import {isSanityDocument, type SanityDocument} from '@sanity/types'

import {type IdPair} from '../../types'

const DB_NAME = 'sanityDocumentsDB'
const DB_VERSION = 1
const STORE_NAME = 'documents'

let idb: IDBDatabase | null = null

function openDatabase() {
if (idb) {
return Promise.resolve(idb)
}
return new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION)

request.onupgradeneeded = () => {
const db = request.result
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, {keyPath: '_id'})
}
}

request.onsuccess = () => {
idb = request.result
resolve(request.result)
}

request.onerror = () => {
reject(request.error)
}
})
}

async function getDocumentFromIndexedDB(id: string): Promise<SanityDocument | null> {
try {
const db = await openDatabase()
return new Promise<SanityDocument | null>((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, 'readonly')
const store = transaction.objectStore(STORE_NAME)
const request = store.get(id)

request.onsuccess = () => {
const result = request.result
resolve(isSanityDocument(result) ? result : null)
}

transaction.onerror = () => {
console.error(`Error retrieving document with ID ${id} from IndexedDB:`, request.error)
reject(transaction.error)
}
})
} catch (error) {
console.error(`Error opening IndexedDB:`, error)
return null
}
}

async function saveDocumentToIndexedDB(document: SanityDocument): Promise<void> {
const db = await openDatabase()
return new Promise<void>((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, 'readwrite')
const store = transaction.objectStore(STORE_NAME)
const request = store.put(document)

request.onsuccess = () => {
resolve()
}

transaction.onerror = () => {
console.error(`Error saving document with ID ${document._id} to IndexedDB:`, request.error)
reject(transaction.error)
}
})
}

interface DocumentPair {
draft: SanityDocument | null
published: SanityDocument | null
}

/**
* returns the pair in one transaction
*/
async function getDocumentPairIndexedDB(idPair: IdPair): Promise<DocumentPair | null> {
try {
const db = await openDatabase()
return new Promise<DocumentPair>((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, 'readonly')
const store = transaction.objectStore(STORE_NAME)

let draft: SanityDocument | null = null
let published: SanityDocument | null = null

transaction.oncomplete = () => {
resolve({draft, published})
}

// Handle transaction errors
transaction.onerror = () => {
console.error('Transaction error:', transaction.error)
reject(transaction.error)
}

// Initiate the get request for the draft document
const draftRequest = store.get(idPair.draftId)
draftRequest.onsuccess = () => {
const result = draftRequest.result
draft = isSanityDocument(result) ? result : null
}
// Initiate the get request for the published document
const publishedRequest = store.get(idPair.publishedId)
publishedRequest.onsuccess = () => {
const result = publishedRequest.result
published = isSanityDocument(result) ? result : null
}
})
} catch (error) {
console.error(`Error opening IndexedDB:`, error)
return null
}
}

async function saveDocumentPairIndexedDB(documentPair: DocumentPair): Promise<void> {
try {
const db = await openDatabase()
return new Promise<void>((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, 'readwrite')
const store = transaction.objectStore(STORE_NAME)

transaction.oncomplete = () => {
resolve()
}

transaction.onerror = () => {
console.error('Transaction error:', transaction.error)
reject(transaction.error)
}

// Save the draft document if it exists
if (documentPair.draft) {
store.put(documentPair.draft)
}

// Save the published document if it exists
if (documentPair.published) {
store.put(documentPair.published)
}
})
} catch (error) {
console.error(`Error opening IndexedDB:`, error)
// Optionally, rethrow the error or handle it
throw error
}
}

export const supportsIndexedDB = (() => {
try {
return 'indexedDB' in window && window.indexedDB !== null
} catch (e) {
return false
}
})()

export async function getPairFromIndexedDB(idPair: IdPair): Promise<DocumentPair> {
console.log('Getting idbPair', idPair)
if (!supportsIndexedDB) {
console.info("IndexedDB isn't supported, returning null")
return {
draft: null,
published: null,
}
}
console.time('getPairFromIndexedDB')
const pair = await getDocumentPairIndexedDB(idPair)
console.timeEnd('getPairFromIndexedDB')
if (!pair) {
return {
draft: null,
published: null,
}
}
return pair
}

export async function savePairToIndexedDB(documentPair: DocumentPair | null) {
console.log('Saving pair to indexedDB', documentPair?.published?._id || documentPair?.draft?._id)
if (!supportsIndexedDB || !documentPair) return
console.time('savePairToIndexedDB')
await saveDocumentPairIndexedDB(documentPair)
console.timeEnd('savePairToIndexedDB')
}

0 comments on commit 7f95804

Please sign in to comment.