/*! * Copyright (c) Microsoft Corporation and contributors. All rights reserved. * Licensed under the MIT License. */ import { assert, isObject } from "@fluidframework/core-utils/internal"; import { UnassignedSequenceNumber } from "./constants.js"; import { type ISegmentInternal, type ISegmentPrivate, MergeBlock, type ObliterateInfo, } from "./mergeTreeNodes.js"; import type { InsertOperationStamp, OperationStamp, RemoveOperationStamp } from "./stamps.js"; export interface StringToType { "string": string; "number": number; "object": object; "array": unknown[]; "boolean": boolean; } export function propExists

( thing: unknown, prop: P, ): thing is Record { return isObject(thing) && prop in thing; } export function hasProp

( thing: unknown, prop: P, type: T, ): thing is Record { return ( propExists(thing, prop) && (type === "array" ? Array.isArray(thing[prop]) : typeof thing[prop] === type) ); } export function propInstanceOf

( thing: unknown, prop: P, type: new (...args: any[]) => T, ): thing is Record { return propExists(thing, prop) && thing[prop] instanceof type; } /** * Contains insertion information associated to an {@link ISegment}. */ export interface IHasInsertionInfo { insert: InsertOperationStamp; } export interface ISegmentInsideObliterateInfo { /** * Populated iff this segment was inserted into a range affected by concurrent obliterates at the time of its insertion. * Contains information about the 'most recent' (i.e. 'winning' in the sense below) obliterate. * * BEWARE: We have opted for a certain form of last-write wins (LWW) semantics for obliterates: * the client which last obliterated a range is considered to have "won ownership" of that range and may insert into it * without that insertion being obliterated by other clients' concurrent obliterates. * * Therefore, this field can be populated even if the segment has not been obliterated (i.e. is still visible). * This happens precisely when the segment was inserted by the same client that 'won' the obliterate (in a scenario where * a client first issues a sided obliterate impacting a range, then inserts into that range before the server has acked the obliterate). * * See the test case "obliterate with mismatched final states" for an example of such a scenario. * * TODO:AB#29553: This property is not persisted in the V1 summary, but it should be. */ obliteratePrecedingInsertion?: ObliterateInfo; /** * Populated iff this segment was inserted into a range concurrently removed by a local obliterate operation. * This field is unset once the newest such overlapping obliterate is acked, and allows recomputing {@link obliteratePrecedingInsertion} * if that local obliterate is resubmitted. * * TODO:AB#29553: This property is not persisted in the V1 summary, but it should be. */ insertionRefSeqStamp?: OperationStamp; } /** * Converts a segment-like object to an insertion info object if possible. * * @param segmentLike - The segment-like object to convert. * @returns The insertion info object if the conversion is possible, otherwise undefined. */ export const toInsertionInfo = (segmentLike: unknown): IHasInsertionInfo | undefined => { return segmentLike !== undefined && hasProp(segmentLike, "insert", "object") && hasProp(segmentLike.insert, "clientId", "number") && hasProp(segmentLike.insert, "seq", "number") ? (segmentLike as IHasInsertionInfo) : undefined; }; /** * A type-guard which determines if the segment has insertion info, and * returns true if it does, along with applying strong typing. * * @param segmentLike - The segment-like object to check. * @returns True if the segment has insertion info, otherwise false. */ export const isInserted = (segmentLike: unknown): segmentLike is IHasInsertionInfo => toInsertionInfo(segmentLike) !== undefined; export const isInsideObliterate = ( segmentLike: unknown, ): segmentLike is ISegmentInsideObliterateInfo => segmentLike !== undefined && hasProp(segmentLike, "obliteratePrecedingInsertion", "object"); /** * Asserts that the segment has insertion info. Usage of this function should not produce a user facing error. * * @param segmentLike - The segment-like object to check. * @throws Will throw an error if the segment does not have insertion info. */ export const assertInserted: | undefined>( segmentLike: ISegmentInternal | Partial | T, ) => asserts segmentLike is IHasInsertionInfo | Exclude> = ( segmentLike, ) => assert( segmentLike === undefined || isInserted(segmentLike), 0xaa0 /* must be insertionInfo */, ); /** * Common properties for a node in a merge tree. */ export interface IMergeNodeInfo { /** * The parent merge block if the node is parented */ parent: MergeBlock; /** * The index of this node in its parent's list of children. */ index: number; /** * A string that can be used for comparing the location of this node to other `MergeNode`s in the same tree. * `a.ordinal < b.ordinal` if and only if `a` comes before `b` in a pre-order traversal of the tree. */ ordinal: string; } /** * Converts a segment-like object to a merge node info object if possible. * * @param segmentLike - The segment-like object to convert. * @returns The merge node info object if the conversion is possible, otherwise undefined. */ export const toMergeNodeInfo = (nodeLike: unknown): IMergeNodeInfo | undefined => propInstanceOf(nodeLike, "parent", MergeBlock) && hasProp(nodeLike, "ordinal", "string") && hasProp(nodeLike, "index", "number") ? nodeLike : undefined; /** * A type-guard which determines if the segment has merge node info, and * returns true if it does, along with applying strong typing. * * @param nodeLike - The segment-like object to check. * @returns True if the segment has merge node info, otherwise false. */ export const isMergeNodeInfo = (nodeLike: unknown): nodeLike is IMergeNodeInfo => toMergeNodeInfo(nodeLike) !== undefined; /** * Asserts that the segment has merge node info. Usage of this function should not produce a user facing error. * * @param segmentLike - The segment-like object to check. * @throws Will throw an error if the segment does not have merge node info. */ export const assertMergeNode: | undefined>( nodeLike: ISegmentInternal | ISegmentPrivate | Partial | T, ) => asserts nodeLike is IMergeNodeInfo | Exclude> = ( segmentLike, ) => assert( segmentLike === undefined || isMergeNodeInfo(segmentLike), 0xaa1 /* must be MergeNodeInfo */, ); /** * Removes the merge node info. This is used to remove nodes from the merge-tree. * @param segmentLike - The segment-like object to check. * @returns This function will change the type of the provided node like to never via an assertion. This * ensures no further usage of the removed merge node info is allowed. if continued use is required other * type coercion methods should be used to correctly re-type the variable. */ export const removeMergeNodeInfo: (nodeLike: IMergeNodeInfo) => asserts nodeLike is never = ( nodeLike, ) => Object.assign>(nodeLike, { parent: undefined, index: undefined, ordinal: undefined, }); /** * Contains removal information associated with an {@link ISegment}. * * Segments can be removed concurrently by multiple clients. */ export interface IHasRemovalInfo { /** * Operation stamps which have removed this segment. This list is sorted by stamp order, where removes[0] is the earliest removal. */ removes: RemoveOperationStamp[]; } /** * Converts a segment-like object to a removal info object if possible. * * @param segmentLike - The segment-like object to convert. * @returns The removal info object if the conversion is possible, otherwise undefined. */ export const toRemovalInfo = (segmentLike: unknown): IHasRemovalInfo | undefined => { return hasProp(segmentLike, "removes", "array") && segmentLike.removes.length > 0 && hasProp(segmentLike.removes[0], "clientId", "number") && hasProp(segmentLike.removes[0], "seq", "number") ? (segmentLike as IHasRemovalInfo) : undefined; }; /** * A type-guard which determines if the segment has removal info, and * returns true if it does, along with applying strong typing. * * @param segmentLike - The segment-like object to check. * @returns True if the segment has removal info, otherwise false. */ // export const isRemoved = (segmentLike: unknown): segmentLike is IHasRemovalInfo => // toRemovalInfo(segmentLike) !== undefined; export const isRemoved = (segmentLike: unknown): segmentLike is IHasRemovalInfo => toRemovalInfo(segmentLike) !== undefined; /** * Asserts that the segment has removal info. Usage of this function should not produce a user facing error. * * @param segmentLike - The segment-like object to check. * @throws Will throw an error if the segment does not have removal info. */ export const assertRemoved: | undefined>( segmentLike: ISegmentInternal | Partial | T, ) => asserts segmentLike is IHasRemovalInfo | Exclude> = ( segmentLike, ) => assert(segmentLike === undefined || isRemoved(segmentLike), 0xaa2 /* must be removalInfo */); /** * Removes the removal info. This is used in rollback. * @param segmentLike - The segment-like object to check. * @returns This function will change the type of the provided node like to never via an assertion. This * ensures no further usage of the removed removal info is allowed. if continued use is required other * type coercion methods should be use to correctly re-type the variable. */ export const removeRemovalInfo: ( nodeLike: IHasRemovalInfo, ) => asserts nodeLike is Record = (nodeLike) => Object.assign>(nodeLike, { removes: undefined, }); /** * Returns whether this segment was marked removed as soon as its insertion was acked. * * This can happen when an an insert occurs concurrent to an obliterate over the range the segment was inserted into, * and the obliterate was sequenced first. * * When this happens, the segment is only ever visible to the client that inserted the segment * (and only until that client has seen the obliterate which removed their segment). */ export function wasRemovedOnInsert(segment: IHasInsertionInfo & ISegmentPrivate): boolean { const removeInfo = toRemovalInfo(segment); const removedSeq = removeInfo?.removes[0].seq; if (removedSeq === undefined || removedSeq === UnassignedSequenceNumber) { return false; } const insertSeq = segment.insert.seq; return insertSeq === UnassignedSequenceNumber || insertSeq > removedSeq; } /** * A union type representing any segment info. */ export type SegmentInfo = | IMergeNodeInfo | IHasInsertionInfo | IHasRemovalInfo | ISegmentInsideObliterateInfo; /** * A type representing a segment with additional info. */ export type SegmentWithInfo< T extends SegmentInfo, S extends ISegmentPrivate = ISegmentPrivate, > = S & T; /** * Overwrites the segment info on a segment-like object. * * @param segmentLike - The segment-like object to set the info on. * @param info - The segment info to overwrite. * @returns The segment-like object with the info set. */ export const overwriteInfo = < T extends SegmentInfo, S extends ISegmentPrivate = ISegmentPrivate, >( segmentLike: S, info: T, ): SegmentWithInfo => Object.assign(segmentLike, info);