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 authored and bjoerge committed Sep 27, 2024
1 parent 6d546e8 commit 0e128eb
Show file tree
Hide file tree
Showing 2 changed files with 246 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
/* 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'
import {finalize, map, publishReplay, refCount, startWith, switchMap, tap} from 'rxjs/operators'
import {combineLatest, defer, from, merge, type Observable, of} from 'rxjs'
import {
catchError,
finalize,
map,
publishReplay,
refCount,
startWith,
switchMap,
tap,
} from 'rxjs/operators'

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 +67,65 @@ 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
if (cachedDocumentPair) {
return cachedDocumentPair
}
return getPairFromLocalStorage(idPair)
// read the memoized value, if we don't have we will search it in the indexedDB
return cachedDocumentPair
}

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 from(getPairFromIndexedDB(idPair)).pipe(
catchError((error) => {
console.error('Error getting pair from IndexedDB:', error)
// Return an empty pair if there's an error
return of({draft: null, published: null})
}),
)
}).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(([draftSnapshot, publishedSnapshot]) => {
cachedDocumentPair = {draft: draftSnapshot, published: publishedSnapshot}
}),
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 +143,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,176 @@
/* 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()

const transaction = db.transaction(STORE_NAME, 'readonly')
const store = transaction.objectStore(STORE_NAME)

const getDocument = async (id: string): Promise<SanityDocument | null> => {
try {
const result = await new Promise<SanityDocument | null>((resolve, reject) => {
const request = store.get(id)
request.onsuccess = () => resolve(request.result)
request.onerror = () => reject(request.error)
})
return isSanityDocument(result) ? result : null
} catch (error) {
console.error(`Error retrieving document with ID ${id} from IndexedDB:`, error)
return null
}
}

const [draft, published] = await Promise.all([
getDocument(idPair.draftId),
getDocument(idPair.publishedId),
])

return {draft, published}
} catch (error) {
console.error(`Error opening IndexedDB:`, error)
return null
}
}

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

const saveDocument = async (doc: SanityDocument | null) => {
if (!doc) Promise.resolve()
await new Promise<void>((resolve, reject) => {
const request = store.put(doc)
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
}
await Promise.all([saveDocument(documentPair.draft), saveDocument(documentPair.published)])
} catch (error) {
console.error(`Error opening IndexedDB:`, error)
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 0e128eb

Please sign in to comment.