Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
pedrobonamin committed Oct 13, 2024
1 parent da3a1e6 commit f638790
Show file tree
Hide file tree
Showing 18 changed files with 614 additions and 299 deletions.
25 changes: 3 additions & 22 deletions packages/sanity/src/core/field/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,40 +25,21 @@ import {
} from '@sanity/types'
import {type ComponentType} from 'react'

import {type DocumentGroupEvent} from '../store/events/types'
import {type FieldValueError} from './validation'

/**
* History timeline / chunking
*
*
* @hidden
* @beta
*/
export type ChunkType =
| 'initial'
| 'create'
| 'editDraft'
| 'delete'
| 'publish'
| 'unpublish'
| 'discardDraft'
| 'editLive'

/**
* @hidden
* @beta */
export type Chunk = {
index: number

id: string
type: ChunkType
start: number
end: number
startTimestamp: string
endTimestamp: string
authors: Set<string>
draftState: 'present' | 'missing' | 'unknown'
publishedState: 'present' | 'missing' | 'unknown'

event: DocumentGroupEvent
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,9 @@ export default function HistoryTimelineStory() {
selected={realRevChunk === chunk}
>
<Stack space={2}>
<Text>{chunk.type}</Text>
<Text>{chunk.event.type}</Text>
<Text muted size={1}>
{format(new Date(chunk.endTimestamp), 'MMM d, YYY @ HH:mm')}
{format(new Date(chunk.event.type), 'MMM d, YYY @ HH:mm')}
</Text>
</Stack>
</Card>
Expand All @@ -169,7 +169,7 @@ export default function HistoryTimelineStory() {
selected={sinceTime === chunk}
>
<Stack space={2}>
<Text>{chunk.type}</Text>
<Text>{chunk.event.type}</Text>
<Text muted size={1}>
{format(new Date(chunk.endTimestamp), 'MMM d, YYY @ HH:mm')}
</Text>
Expand Down
23 changes: 17 additions & 6 deletions packages/sanity/src/core/store/_legacy/history/history/Timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,11 @@ export class Timeline {
const nextTransactionToChunk = this._chunks.length > 0 ? this._chunks.last.end : firstIdx
for (let idx = nextTransactionToChunk; idx <= lastIdx; idx++) {
const transaction = this._transactions.get(idx)
this._chunks.mergeAtEnd(chunkFromTransaction(transaction), mergeChunk)
const allTransactions = this._transactions.getAllItems
this._chunks.mergeAtEnd(
chunkFromTransaction(this.publishedId, transaction, allTransactions),
mergeChunk,
)
}

// Add transactions at the beginning:
Expand All @@ -204,7 +208,12 @@ export class Timeline {

for (let idx = firstTransactionChunked - 1; idx >= firstIdx; idx--) {
const transaction = this._transactions.get(idx)
this._chunks.mergeAtBeginning(chunkFromTransaction(transaction), mergeChunk)
const allTransactions = this._transactions.getAllItems

this._chunks.mergeAtBeginning(
chunkFromTransaction(this.publishedId, transaction, allTransactions),
mergeChunk,
)
}
}

Expand All @@ -216,12 +225,14 @@ export class Timeline {

private _createInitialChunk() {
if (this.reachedEarliestEntry) {
if (this._chunks.first?.type === 'initial') return
if (this._chunks.first?.event.type === 'document.createVersion') return

const firstTx = this._transactions.first
if (!firstTx) return
const initialChunk = chunkFromTransaction(firstTx)
initialChunk.type = 'initial'
const allTransactions = this._transactions.getAllItems

const initialChunk = chunkFromTransaction(this.publishedId, firstTx, allTransactions)
initialChunk.event.type = 'document.createVersion'
initialChunk.id = '@initial'
initialChunk.end = initialChunk.start
this._chunks.addToBeginning(initialChunk)
Expand Down Expand Up @@ -275,7 +286,7 @@ export class Timeline {
chunkIdx--
) {
const currentChunk = this._chunks.get(chunkIdx)
if (currentChunk.type === 'publish' || currentChunk.type === 'initial') {
if (currentChunk.event.type === 'document.publishVersion' || currentChunk.id === '@initial') {
return currentChunk
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ export class TwoEndedArray<T extends {index: number}> {
}
}

get getAllItems(): T[] {
// Reverse the negative side and concatenate with the positive side
return [...this._negative.slice().reverse(), ...this._postive.slice()]
}

get lastIdx(): number {
// Note: This also works correctly when _positive is empty (it returns -1)
return this._postive.length - 1
Expand Down
158 changes: 31 additions & 127 deletions packages/sanity/src/core/store/_legacy/history/history/chunker.ts
Original file line number Diff line number Diff line change
@@ -1,160 +1,64 @@
/* eslint-disable no-nested-ternary */
import {type MendozaEffectPair, type MendozaPatch} from '@sanity/types'

import {type Chunk, type ChunkType} from '../../../../field'
import {type Chunk} from '../../../../field'
import {getEventFromTransaction} from '../../../events/getDocumentEvents'
import {type EditDocumentVersionEvent} from '../../../events/types'
import {type Transaction} from './types'

function canMergeEdit(type: ChunkType) {
return type === 'create' || type === 'editDraft'
}

const CHUNK_WINDOW = 5 * 60 * 1000 // 5 minutes

function isWithinMergeWindow(a: string, b: string) {
return Date.parse(b) - Date.parse(a) < CHUNK_WINDOW
}

export function mergeChunk(left: Chunk, right: Chunk): Chunk | [Chunk, Chunk] {
if (left.end !== right.start) throw new Error('chunks are not next to each other')

// TODO: How to detect first squash/create

const draftState = combineState(left.draftState, right.draftState)
const publishedState = combineState(left.publishedState, right.publishedState)

if (left.type === 'delete' && right.type === 'editDraft') {
return [left, {...right, type: 'create', draftState, publishedState}]
}

// Convert deletes into either discardDraft or unpublish depending on what's been deleted.
if (right.type === 'delete') {
if (draftState === 'missing' && publishedState === 'present') {
return [left, {...right, type: 'discardDraft', draftState, publishedState}]
}

if (draftState === 'present' && publishedState === 'missing') {
return [left, {...right, type: 'unpublish', draftState, publishedState}]
}
const addMergedEvents = (
leftEvent: EditDocumentVersionEvent,
rightEvent: EditDocumentVersionEvent,
): EditDocumentVersionEvent => {
const mergedEvents = leftEvent.mergedEvents || []
delete leftEvent.mergedEvents
return {
...rightEvent,
mergedEvents: [...mergedEvents, leftEvent],
}
}

export function mergeChunk(left: Chunk, right: Chunk): Chunk | [Chunk, Chunk] {
if (left.end !== right.start) throw new Error('chunks are not next to each other')
if (
canMergeEdit(left.type) &&
right.type === 'editDraft' &&
isWithinMergeWindow(left.endTimestamp, right.startTimestamp)
left.event.type === 'document.editVersion' &&
right.event.type === 'document.editVersion' &&
isWithinMergeWindow(left.endTimestamp, right.startTimestamp) &&
// TODO: confirm we don't want to merge if the author is different
left.event.author === right.event.author
) {
const authors = new Set<string>()
for (const author of left.authors) authors.add(author)
for (const author of right.authors) authors.add(author)

return {
index: 0,
id: right.id,
type: left.type,
start: left.start,
end: right.end,
event: addMergedEvents(left.event, right.event),
startTimestamp: left.startTimestamp,
endTimestamp: right.endTimestamp,
authors,
draftState,
publishedState,
}
}

return [left, {...right, draftState, publishedState}]
}

type ChunkState = 'unedited' | 'deleted' | 'upsert'
function getChunkState(effect?: MendozaEffectPair): ChunkState {
const modified = Boolean(effect)
const deleted = effect && isDeletePatch(effect?.apply)

if (deleted) {
return 'deleted'
}

if (modified) {
return 'upsert'
}

return 'unedited'
return [left, right]
}

/*
* getChunkType tries to determine what effect the given transaction had on the document
* More information about the logic can be found here https://github.com/sanity-io/sanity/pull/2633#issuecomment-886461812
*
* | | draft unedited | draft deleted | draft upsert |
* |--------------------|----------------|---------------|--------------|
* | published unedited | X | delete | editDraft |
* | published deleted | delete | delete | delete |
* | published upsert | liveEdit | publish | liveEdit |
*/
function getChunkType(transaction: Transaction): ChunkType {
const draftState = getChunkState(transaction.draftEffect)
const publishedState = getChunkState(transaction.publishedEffect)

if (publishedState === 'unedited') {
if (draftState === 'deleted') {
return 'delete'
}

if (draftState === 'upsert') {
return 'editDraft'
}
}

if (publishedState === 'deleted') {
return 'delete'
}

if (publishedState === 'upsert') {
if (draftState === 'unedited') {
return 'editLive'
}

if (draftState === 'deleted') {
return 'publish'
}

if (draftState === 'upsert') {
return 'editLive'
}
}

return 'editLive'
}

export function chunkFromTransaction(transaction: Transaction): Chunk {
const modifiedDraft = Boolean(transaction.draftEffect)
const modifiedPublished = Boolean(transaction.publishedEffect)

const draftDeleted = transaction.draftEffect && isDeletePatch(transaction.draftEffect.apply)
const publishedDeleted =
transaction.publishedEffect && isDeletePatch(transaction.publishedEffect.apply)

const type = getChunkType(transaction)

export function chunkFromTransaction(
publishedId: string,
transaction: Transaction,
transactions: Transaction[],
): Chunk {
// TODO; get the previous transactions, they need to account for the index of this transaction, we need all the previous ones.
const previousTransactions = transactions.filter((tx) => tx.index < transaction.index).reverse()
console.log(transaction.id, {transaction, previousTransactions})
return {
index: 0,
id: transaction.id,
type,
start: transaction.index,
end: transaction.index + 1,
startTimestamp: transaction.timestamp,
endTimestamp: transaction.timestamp,
authors: new Set([transaction.author]),
draftState: modifiedDraft ? (draftDeleted ? 'missing' : 'present') : 'unknown',
publishedState: modifiedPublished ? (publishedDeleted ? 'missing' : 'present') : 'unknown',
event: getEventFromTransaction(publishedId, transaction, previousTransactions),
}
}

function combineState(
left: 'present' | 'missing' | 'unknown',
right: 'present' | 'missing' | 'unknown',
) {
return right === 'unknown' ? left : right
}

export function isDeletePatch(patch: MendozaPatch): boolean {
return patch[0] === 0 && patch[1] === null
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,13 @@ export function useTimelineStore({
.pipe(
map((innerController) => {
const chunks = innerController.timeline.mapChunks((c) => c)
const lastNonDeletedChunk = chunks.filter(
(chunk) => !['delete', 'initial'].includes(chunk.type),
const lastNonDeletedChunk = chunks.find(
(chunk) =>
![
'document.deleteGroup',
'document.deleteVersion',
'document.createVersion',
].includes(chunk.event.type),
)
const hasMoreChunks = !innerController.timeline.reachedEarliestEntry

Expand All @@ -220,7 +225,7 @@ export function useTimelineStore({
isLoading: false,
isPristine: timelineReady ? chunks.length === 0 && hasMoreChunks === false : null,
hasMoreChunks: !innerController.timeline.reachedEarliestEntry,
lastNonDeletedRevId: lastNonDeletedChunk?.[0]?.id,
lastNonDeletedRevId: lastNonDeletedChunk?.id || null,
onOlderRevision: innerController.onOlderRevision(),
realRevChunk: innerController.realRevChunk,
revTime: innerController.revTime,
Expand All @@ -229,7 +234,7 @@ export function useTimelineStore({
sinceTime: innerController.sinceTime,
timelineDisplayed: innerController.displayed(),
timelineReady,
}
} satisfies TimelineState
}),
// Only emit (and in turn, re-render) when values have changed
distinctUntilChanged(deepEquals),
Expand Down
Loading

0 comments on commit f638790

Please sign in to comment.