66import { assert , unreachableCase , fail } from "@fluidframework/core-utils/internal" ;
77import type { IFluidHandle , Listenable } from "@fluidframework/core-interfaces/internal" ;
88import { createEmitter } from "@fluid-internal/client-utils" ;
9- import type { IIdCompressor } from "@fluidframework/id-compressor" ;
9+ import type { IIdCompressor , SessionId } from "@fluidframework/id-compressor" ;
1010import {
1111 UsageError ,
1212 type ITelemetryLoggerExt ,
@@ -20,7 +20,6 @@ import {
2020 type AnchorSetRootEvents ,
2121 type ChangeFamily ,
2222 CommitKind ,
23- type CommitMetadata ,
2423 type DeltaVisitor ,
2524 type DetachedFieldIndex ,
2625 type IEditableForest ,
@@ -48,6 +47,8 @@ import {
4847 type TreeNodeStoredSchema ,
4948 LeafNodeStoredSchema ,
5049 diffHistories ,
50+ type ChangeMetadata ,
51+ type ChangeEncodingContext ,
5152 type ReadOnlyDetachedFieldIndex ,
5253} from "../core/index.js" ;
5354import {
@@ -69,7 +70,13 @@ import {
6970 type SharedTreeBranchChange ,
7071 type Transactor ,
7172} from "../shared-tree-core/index.js" ;
72- import { Breakable , disposeSymbol , getOrCreate , type WithBreakable } from "../util/index.js" ;
73+ import {
74+ Breakable ,
75+ disposeSymbol ,
76+ getOrCreate ,
77+ type JsonCompatibleReadOnly ,
78+ type WithBreakable ,
79+ } from "../util/index.js" ;
7380
7481import { SharedTreeChangeFamily , hasSchemaChange } from "./sharedTreeChangeFamily.js" ;
7582import type { SharedTreeChange } from "./sharedTreeChangeTypes.js" ;
@@ -90,6 +97,7 @@ import {
9097 type CustomTreeNode ,
9198} from "../simple-tree/index.js" ;
9299import { getCheckout , SchematizingSimpleTreeView } from "./schematizingTreeView.js" ;
100+ import { isStableId } from "@fluidframework/id-compressor/internal" ;
93101
94102/**
95103 * Events for {@link ITreeCheckout}.
@@ -123,7 +131,7 @@ export interface CheckoutEvents {
123131 * @param getRevertible - a function provided that allows users to get a revertible for the change. If not provided,
124132 * this change is not revertible.
125133 */
126- changed ( data : CommitMetadata , getRevertible ?: RevertibleAlphaFactory ) : void ;
134+ changed ( data : ChangeMetadata , getRevertible ?: RevertibleAlphaFactory ) : void ;
127135
128136 /**
129137 * Fired when a new branch is created from this checkout.
@@ -537,7 +545,31 @@ export class TreeCheckout implements ITreeCheckoutFork {
537545 } ;
538546
539547 let withinEventContext = true ;
540- this . #events. emit ( "changed" , { isLocal : true , kind } , getRevertible ) ;
548+
549+ const metadata : ChangeMetadata = {
550+ kind,
551+ isLocal : true ,
552+ getChange : ( ) => {
553+ const context : ChangeEncodingContext = {
554+ idCompressor : this . idCompressor ,
555+ originatorId : this . idCompressor . localSessionId ,
556+ revision,
557+ } ;
558+ const encodedChange = this . changeFamily . codecs
559+ . resolve ( 4 )
560+ . json . encode ( change , context ) ;
561+
562+ assert ( commit . parent !== undefined , "Expected applied commit to be parented" ) ;
563+ return {
564+ version : 1 ,
565+ revision,
566+ originatorId : this . idCompressor . localSessionId ,
567+ change : encodedChange ,
568+ } satisfies SerializedChange ;
569+ } ,
570+ } ;
571+
572+ this . #events. emit ( "changed" , metadata , getRevertible ) ;
541573 withinEventContext = false ;
542574 }
543575 } else if ( this . isRemoteChangeEvent ( event ) ) {
@@ -564,6 +596,28 @@ export class TreeCheckout implements ITreeCheckoutFork {
564596 }
565597 } ;
566598
599+ /**
600+ * Applies the given serialized change (as was produced via a `"changed"` event of another checkout) to this checkout.
601+ */
602+ public applySerializedChange ( serializedChange : JsonCompatibleReadOnly ) : void {
603+ if ( ! isSerializedChange ( serializedChange ) ) {
604+ throw new UsageError ( `Cannot apply change. Invalid serialized change format.` ) ;
605+ }
606+ const { revision, originatorId, change } = serializedChange ;
607+ if ( originatorId !== this . idCompressor . localSessionId ) {
608+ throw new UsageError (
609+ `Cannot apply change. A serialized changed must be applied to the same SharedTree as it was created from.` ,
610+ ) ;
611+ }
612+ const context : ChangeEncodingContext = {
613+ idCompressor : this . idCompressor ,
614+ originatorId : this . idCompressor . localSessionId ,
615+ revision,
616+ } ;
617+ const decodedChange = this . changeFamily . codecs . resolve ( 4 ) . json . decode ( change , context ) ;
618+ this . applyChange ( decodedChange , revision ) ;
619+ }
620+
567621 // Revision is the revision of the commit, if any, which caused this change.
568622 private applyChange ( change : SharedTreeChange , revision ?: RevisionTag ) : void {
569623 // Conflicts due to schema will be empty and thus are not applied.
@@ -1210,3 +1264,24 @@ function verboseFromCursor(
12101264 fields : fields as CustomTreeNode < IFluidHandle > ,
12111265 } ;
12121266}
1267+
1268+ interface SerializedChange {
1269+ version : 1 ;
1270+ revision : RevisionTag ;
1271+ change : JsonCompatibleReadOnly ;
1272+ originatorId : SessionId ;
1273+ }
1274+
1275+ function isSerializedChange ( value : unknown ) : value is SerializedChange {
1276+ if ( typeof value !== "object" || value === null ) {
1277+ return false ;
1278+ }
1279+ const change = value as Partial < SerializedChange > ;
1280+ return (
1281+ change . version === 1 &&
1282+ ( change . revision === "root" || typeof change . revision === "number" ) &&
1283+ typeof change . originatorId === "string" &&
1284+ isStableId ( change . originatorId ) &&
1285+ change . change !== undefined
1286+ ) ;
1287+ }
0 commit comments