From e659f8d82c28adfcbff71e6e84fc29200c668773 Mon Sep 17 00:00:00 2001 From: pedrobonamin Date: Thu, 26 Sep 2024 08:01:41 +0200 Subject: [PATCH] chore(core): add document store in local storage POC --- .../document/document-pair/editState.ts | 88 ++++++++++--------- .../document-pair/utils/localStoragePOC.ts | 76 ++++++++++++++++ .../document/document-pair/validation.ts | 4 +- .../store/_legacy/document/document-store.ts | 19 ++-- 4 files changed, 129 insertions(+), 58 deletions(-) create mode 100644 packages/sanity/src/core/store/_legacy/document/document-pair/utils/localStoragePOC.ts diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/editState.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/editState.ts index c77e1e8d740..711d486bc1c 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/editState.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/editState.ts @@ -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 @@ -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 => { 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}` + }, ) diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/utils/localStoragePOC.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/utils/localStoragePOC.ts new file mode 100644 index 00000000000..aebda4781b5 --- /dev/null +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/utils/localStoragePOC.ts @@ -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) + } +} diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/validation.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/validation.ts index b1cf371ab4b..cd62be3a907 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/validation.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/validation.ts @@ -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 => { - 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) => { diff --git a/packages/sanity/src/core/store/_legacy/document/document-store.ts b/packages/sanity/src/core/store/_legacy/document/document-store.ts index 6e4551b7683..4e0a3a1233b 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-store.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-store.ts @@ -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' @@ -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 @@ -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) { @@ -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)) }, }, }