platform/packages/core/src/collaboration.ts
2024-08-29 15:10:37 +07:00

188 lines
6.0 KiB
TypeScript

//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Doc, Ref } from './classes'
/**
* Identifier of the collaborative document holding collaborative content.
*
* Format:
* {documentId}:{versionId}:{lastVersionId}
* {documentId}:{versionId}
*
* Where:
* - documentId is an identifier of the document in storage
* - versionId is an identifier of the document version, HEAD for latest editable version
* - lastVersionId is an identifier of the latest available version
*
* The collaborative document may contain one or more such sections chained with # (hash):
* collaborativeDocId#collaborativeDocId#collaborativeDocId#...
*
* When collaborative document does not exist, it will be initialized from the first existing
* document in the list.
*
* @public
* */
export type CollaborativeDoc = string & { __collaborativeDoc: true }
/** @public */
export type CollaborativeDocVersion = string | typeof CollaborativeDocVersionHead
/** @public */
export const CollaborativeDocVersionHead = 'HEAD'
/** @public */
export function makeCollaborativeDoc (
objectId: Ref<Doc>,
objectAttr?: string | undefined,
versionId?: string | undefined
): CollaborativeDoc {
const storageDocumentId = objectAttr !== undefined && objectAttr !== '' ? `${objectId}%${objectAttr}` : `${objectId}`
return collaborativeDocFormat({
documentId: storageDocumentId,
versionId: CollaborativeDocVersionHead,
lastVersionId: versionId ?? '0'
})
}
/** @public */
export interface CollaborativeDocData {
// Id of the document in object storage
documentId: string
// Id of the document version
// HEAD version represents the editable last document version
// Otherwise, it is a readonly version
versionId: CollaborativeDocVersion
// For HEAD versionId it is the latest available document version
// Otherwise, it is the same value as versionId
lastVersionId: string
source?: CollaborativeDoc[]
}
/**
* Merge several collaborative docs into single collaborative doc train.
*
* @public
*/
export function collaborativeDocChain (...docs: CollaborativeDoc[]): CollaborativeDoc {
return docs.join('#') as CollaborativeDoc
}
/**
* Split collaborative doc train into separate collaborative docs.
*
* @public
*/
export function collaborativeDocUnchain (doc: CollaborativeDoc): CollaborativeDoc[] {
return doc.split('#') as CollaborativeDoc[]
}
/** @public */
export function collaborativeDocParse (doc: CollaborativeDoc): CollaborativeDocData {
const [first, ...other] = collaborativeDocUnchain(doc)
const [documentId, versionId, lastVersionId] = first.split(':')
return {
documentId,
versionId: versionId ?? CollaborativeDocVersionHead,
lastVersionId: lastVersionId ?? versionId ?? CollaborativeDocVersionHead,
source: other
}
}
const sanitize = (value: string): string => value.replace(/[:#]/g, '%')
/** @public */
export function collaborativeDocFormat ({
documentId,
versionId,
lastVersionId,
source
}: CollaborativeDocData): CollaborativeDoc {
const parts = [sanitize(documentId), sanitize(versionId), sanitize(lastVersionId)]
const collaborativeDoc = parts.join(':') as CollaborativeDoc
return collaborativeDocChain(collaborativeDoc, ...(source ?? []))
}
/**
* Updates versionId component in the collaborative document.
* Both versionId and lastVersionId will refer to the same collaborative document version.
*
* When versionId is not HEAD, the document will represent a readonly document version (snapshot).
*
* @public
*/
export function collaborativeDocWithVersion (collaborativeDoc: CollaborativeDoc, versionId: string): CollaborativeDoc {
const { documentId, source } = collaborativeDocParse(collaborativeDoc)
return collaborativeDocFormat({ documentId, versionId, lastVersionId: versionId, source })
}
/**
* Updates lastVersionId component in the collaborative document.
*
* When document versionId is HEAD, the function is no-op.
*
* @public
*/
export function collaborativeDocWithLastVersion (
collaborativeDoc: CollaborativeDoc,
lastVersionId: string
): CollaborativeDoc {
const { documentId, versionId, source } = collaborativeDocParse(collaborativeDoc)
return versionId === CollaborativeDocVersionHead
? collaborativeDocFormat({ documentId, versionId, lastVersionId, source })
: collaborativeDoc
}
/**
* Replaces source component in the collaborative document.
*
* @public
*/
export function collaborativeDocWithSource (
collaborativeDoc: CollaborativeDoc,
source: CollaborativeDoc
): CollaborativeDoc {
const { documentId, versionId, lastVersionId } = collaborativeDocParse(collaborativeDoc)
return collaborativeDocFormat({ documentId, versionId, lastVersionId, source: [source] })
}
/**
* Creates collaborative document that refers to the last version from the source collaborative document.
*
* @public
*/
export function collaborativeDocFromLastVersion (collaborativeDoc: CollaborativeDoc): CollaborativeDoc {
const { documentId, lastVersionId, source } = collaborativeDocParse(collaborativeDoc)
return collaborativeDocFormat({
documentId,
versionId: lastVersionId,
lastVersionId,
source
})
}
/**
* Creates collaborative document that refers to the last version from the source collaborative document.
*
* @public
*/
export function collaborativeDocFromCollaborativeDoc (
collaborativeDoc: CollaborativeDoc,
sourceCollaborativeDoc: CollaborativeDoc
): CollaborativeDoc {
return collaborativeDocWithSource(collaborativeDoc, collaborativeDocFromLastVersion(sourceCollaborativeDoc))
}