Skip to content

Commit

Permalink
chore(core): add document store in local storage POC
Browse files Browse the repository at this point in the history
  • Loading branch information
pedrobonamin authored and bjoerge committed Sep 27, 2024
1 parent 75e33c1 commit e659f8d
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {type SanityClient} from '@sanity/client'
import {type SanityDocument, type Schema} from '@sanity/types'
import {combineLatest, type Observable} from 'rxjs'
import {map, publishReplay, refCount, startWith, switchMap, take} from 'rxjs/operators'
import {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 {isLiveEditEnabled} from './utils/isLiveEditEnabled'
import {savePairToLocalStorage} from './utils/localStoragePOC'

interface TransactionSyncLockState {
enabled: boolean
Expand Down Expand Up @@ -38,53 +38,55 @@ export const editState = memoize(
},
idPair: IdPair,
typeName: string,
visited$: Observable<(SanityDocument | undefined)[]>,
localStoragePair: {draft: SanityDocument | null; published: SanityDocument | null} | undefined,
): Observable<EditStateFor> => {
const liveEdit = isLiveEditEnabled(ctx.schema, typeName)
return visited$.pipe(
take(1),
map((visited) => {
return {
draft: visited.find((doc) => doc?._id === idPair.draftId) || null,
published: visited.find((doc) => doc?._id === idPair.publishedId) || null,
}
}),
switchMap((visitedPair) => {
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),
),
]),
let documentPair: {
draft: SanityDocument | null
published: SanityDocument | null
} | null = 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),
),
map(([draftSnapshot, publishedSnapshot, transactionSyncLock]) => ({
id: idPair.publishedId,
type: typeName,
draft: draftSnapshot,
published: publishedSnapshot,
liveEdit,
ready: true,
transactionSyncLock,
})),
startWith({
id: idPair.publishedId,
type: typeName,
draft: visitedPair.draft,
published: visitedPair.published,
liveEdit,
ready: false,
transactionSyncLock: null,
}),
)
]),
),
tap(([draft, published]) => {
documentPair = {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: localStoragePair?.draft || null,
published: localStoragePair?.published || null,
liveEdit,
ready: false,
transactionSyncLock: null,
}),
finalize(() => {
savePairToLocalStorage(documentPair)
}),
publishReplay(1),
refCount(),
)
},
(ctx, idPair, typeName) => memoizeKeyGen(ctx.client, idPair, typeName),
(ctx, idPair, typeName, lsPair) => {
const config = ctx.client.config()
return `${config.dataset ?? ''}-${config.projectId ?? ''}-${idPair.publishedId}-${typeName}-${lsPair?.draft?._rev}-${lsPair?.published?._rev}`
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {isSanityDocument, type SanityDocument} from '@sanity/types'

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

const createDocumentLocalStorageKey = (documentId: string) => `sanity:editState:${documentId}`

const getDocumentFromLocalStorage = (id: string): SanityDocument | null => {
if (!supportsLocalStorage) return null

const key = createDocumentLocalStorageKey(id)

try {
const document = localStorage.getItem(key)

if (!document) return null
const parsed = JSON.parse(document)
return isSanityDocument(parsed) ? parsed : null
} catch (error) {
console.error(`Error parsing document with ID ${id} from localStorage:`, error)
return null
}
}

const saveDocumentToLocalStorage = (document: SanityDocument) => {
if (!supportsLocalStorage) return

const key = createDocumentLocalStorageKey(document._id)

try {
localStorage.setItem(key, JSON.stringify(document))
} catch (error) {
console.error(`Error saving document with ID ${document._id} to localStorage:`, error)
}
}

/**
* Function to get the draft and published document from local storage
* it's not production ready, it's POC only, local storage supports up to 5mb of data which won't be enough for the datasets.
* @internal
* @hidden
*/
export const getPairFromLocalStorage = (idPair: IdPair) => {
if (!supportsLocalStorage) {
return {
draft: null,
published: null,
}
}

return {
draft: getDocumentFromLocalStorage(idPair.draftId),
published: getDocumentFromLocalStorage(idPair.publishedId),
}
}

/**
* Function to save the draft and published documents to local storage.
* Note: This is a proof of concept and not production-ready.
* Local storage supports up to 5MB of data, which will not be sufficient for large datasets.
* @internal
* @hidden
*/
export const savePairToLocalStorage = (
documentPair: {
draft: SanityDocument | null
published: SanityDocument | null
} | null,
) => {
if (documentPair?.draft) {
saveDocumentToLocalStorage(documentPair.draft)
}
if (documentPair?.published) {
saveDocumentToLocalStorage(documentPair.published)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ export const validation = memoize(
},
{draftId, publishedId}: IdPair,
typeName: string,
visited$: Observable<(SanityDocument | undefined)[]>,
localStoragePair: {draft: SanityDocument | null; published: SanityDocument | null},
): Observable<ValidationStatus> => {
const document$ = editState(ctx, {draftId, publishedId}, typeName, visited$).pipe(
const document$ = editState(ctx, {draftId, publishedId}, typeName, localStoragePair).pipe(
map(({draft, published}) => draft || published),
throttleTime(DOC_UPDATE_DELAY, asyncScheduler, {trailing: true}),
distinctUntilChanged((prev, next) => {
Expand Down
19 changes: 6 additions & 13 deletions packages/sanity/src/core/store/_legacy/document/document-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import {
type OperationSuccess,
} from './document-pair/operationEvents'
import {type OperationsAPI} from './document-pair/operations'
import {getPairFromLocalStorage} from './document-pair/utils/localStoragePOC'
import {validation} from './document-pair/validation'
import {getVisitedDocuments} from './getVisitedDocuments'
import {getInitialValueStream, type InitialValueMsg, type InitialValueOptions} from './initialValue'
import {listenQuery, type ListenQueryOptions} from './listenQuery'
import {resolveTypeForDocument} from './resolveTypeForDocument'
Expand Down Expand Up @@ -101,10 +101,6 @@ export function createDocumentStore({
const observeDocumentPairAvailability =
documentPreviewStore.unstable_observeDocumentPairAvailability

const visitedDocuments = getVisitedDocuments({
observeDocuments: documentPreviewStore.unstable_observeDocuments,
})

// Note that we're both passing a shared `client` here which is used by the
// internal operations, and a `getClient` method that we expose to user-land
// for things like validations
Expand Down Expand Up @@ -161,13 +157,9 @@ export function createDocumentStore({
return editOperations(ctx, getIdPairFromPublished(publishedId), type)
},
editState(publishedId, type) {
const edit = editState(
ctx,
getIdPairFromPublished(publishedId),
type,
visitedDocuments.visited$,
)
visitedDocuments.add(publishedId)
const idPair = getIdPairFromPublished(publishedId)

const edit = editState(ctx, idPair, type, getPairFromLocalStorage(idPair))
return edit
},
operationEvents(publishedId, type) {
Expand All @@ -190,7 +182,8 @@ export function createDocumentStore({
)
},
validation(publishedId, type) {
return validation(ctx, getIdPairFromPublished(publishedId), type, visitedDocuments.visited$)
const idPair = getIdPairFromPublished(publishedId)
return validation(ctx, idPair, type, getPairFromLocalStorage(idPair))
},
},
}
Expand Down

0 comments on commit e659f8d

Please sign in to comment.