@@ -125,6 +125,13 @@ const assertIntervals = (
125125 assert . deepEqual ( actualPos , expected , "intervals are not as expected" ) ;
126126} ;
127127
128+ // Helper function just for validating that we've captured the pending state we expect
129+ const pendingBlobsFromPendingState = ( pendingState : string ) : Record < string , any > => {
130+ const pendingStateParsed = JSON . parse ( pendingState ) ;
131+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
132+ return pendingStateParsed . pendingRuntimeState . pendingAttachmentBlobs ;
133+ } ;
134+
128135// Introduced in 0.37
129136// REVIEW: enable compat testing
130137describeCompat ( "stashed ops" , "NoCompat" , ( getTestObjectProvider , apis ) => {
@@ -157,6 +164,9 @@ describeCompat("stashed ops", "NoCompat", (getTestObjectProvider, apis) => {
157164 } ,
158165 } ,
159166 enableRuntimeIdCompressor : "on" ,
167+ // Enable createBlobPayloadPending, otherwise there will be no pending blobs to test.
168+ explicitSchemaControl : true ,
169+ createBlobPayloadPending : true ,
160170 } ,
161171 loaderProps : {
162172 configProvider : configProvider ( {
@@ -1628,27 +1638,36 @@ describeCompat("stashed ops", "NoCompat", (getTestObjectProvider, apis) => {
16281638 assert . strictEqual ( bufferToString ( handleGet2 , "utf8" ) , "blob contents" ) ;
16291639 } ) ;
16301640
1631- // ADO#44999: The following scenarios are possible with payload pending, but will function differently.
1632- // The in-flight blob upload will need to be completable after loading with the pending state, but
1633- // we will expect the customer to have already stored the blob handle prior to calling getPendingState.
1634-
1635- it . skip ( "close while uploading blob" , async function ( ) {
1641+ it ( "close while uploading blob" , async function ( ) {
16361642 const dataStore = ( await container1 . getEntryPoint ( ) ) as ITestFluidObject ;
16371643 const map = await dataStore . getSharedObject < ISharedMap > ( mapId ) ;
16381644 await provider . ensureSynchronized ( ) ;
16391645
1640- const blobP = dataStore . runtime . uploadBlob ( stringToBuffer ( "blob contents" , "utf8" ) ) ;
1641- // TODO: This portion was using closeAndGetPendingLocalState - using getPendingLocalState instead to allow compilation
1642- // const pendingOpsP = container1.closeAndGetPendingLocalState?.();
1643- const pendingOpsP = container1 . getPendingLocalState ( ) ;
1644- const handle = await blobP ;
1646+ const handle = await dataStore . runtime . uploadBlob ( stringToBuffer ( "blob contents" , "utf8" ) ) ;
16451647 map . set ( "blob handle" , handle ) ;
1646- const pendingOps = await pendingOpsP ;
1648+ const pendingState = await container1 . getPendingLocalState ( ) ;
1649+ container1 . close ( ) ;
1650+
1651+ // In practice, the upload that was triggered by setting the handle in the map probably didn't complete before
1652+ // the getPendingLocalState() call was able to record the pending state, but maybe it could happen with the local
1653+ // service if microtasks shift around in the future. Verify we are testing the scenario we think we are testing.
1654+ const pendingBlobsRecord = pendingBlobsFromPendingState ( pendingState ) ;
1655+ const pendingBlob = Object . values ( pendingBlobsRecord ) [ 0 ] ;
1656+ assert ( pendingBlob !== undefined , "Expect a pending blob in pending state" ) ;
1657+ assert . strictEqual (
1658+ pendingBlob . state ,
1659+ "localOnly" ,
1660+ "Expect the upload hasn't completed yet" ,
1661+ ) ;
16471662
1648- const container2 = await loader . resolve ( { url } , pendingOps ) ;
1663+ const container2 = await loader . resolve ( { url } , pendingState ) ;
16491664 const dataStore2 = ( await container2 . getEntryPoint ( ) ) as ITestFluidObject ;
16501665 const map2 = await dataStore2 . getSharedObject < ISharedMap > ( mapId ) ;
16511666
1667+ // TODO: We've not yet decided where to expose sharePendingBlobs(), so for now casting and reaching.
1668+ // Replace with calling the proper API when available.
1669+ // Share the pending blob so container1 will be able to find it.
1670+ await ( dataStore2 . context . containerRuntime as any ) . blobManager . sharePendingBlobs ( ) ;
16521671 await provider . ensureSynchronized ( ) ;
16531672 assert . strictEqual (
16541673 bufferToString ( await map1 . get ( "blob handle" ) . get ( ) , "utf8" ) ,
@@ -1660,25 +1679,44 @@ describeCompat("stashed ops", "NoCompat", (getTestObjectProvider, apis) => {
16601679 ) ;
16611680 } ) ;
16621681
1663- it . skip ( "close while uploading multiple blob" , async function ( ) {
1682+ it ( "close while uploading multiple blob" , async function ( ) {
16641683 const dataStore = ( await container1 . getEntryPoint ( ) ) as ITestFluidObject ;
16651684 const map = await dataStore . getSharedObject < ISharedMap > ( mapId ) ;
16661685 await provider . ensureSynchronized ( ) ;
16671686
1668- const blobP1 = dataStore . runtime . uploadBlob ( stringToBuffer ( "blob contents 1" , "utf8" ) ) ;
1669- const blobP2 = dataStore . runtime . uploadBlob ( stringToBuffer ( "blob contents 2" , "utf8" ) ) ;
1670- const blobP3 = dataStore . runtime . uploadBlob ( stringToBuffer ( "blob contents 3" , "utf8" ) ) ;
1671- // TODO: This portion was using closeAndGetPendingLocalState - using getPendingLocalState instead to allow compilation
1672- // const pendingOpsP = container1.closeAndGetPendingLocalState?.();
1673- const pendingOpsP = container1 . getPendingLocalState ( ) ;
1674- map . set ( "blob handle 1" , await blobP1 ) ;
1675- map . set ( "blob handle 2" , await blobP2 ) ;
1676- map . set ( "blob handle 3" , await blobP3 ) ;
1677- const pendingOps = await pendingOpsP ;
1687+ const handle1 = await dataStore . runtime . uploadBlob (
1688+ stringToBuffer ( "blob contents 1" , "utf8" ) ,
1689+ ) ;
1690+ const handle2 = await dataStore . runtime . uploadBlob (
1691+ stringToBuffer ( "blob contents 2" , "utf8" ) ,
1692+ ) ;
1693+ const handle3 = await dataStore . runtime . uploadBlob (
1694+ stringToBuffer ( "blob contents 3" , "utf8" ) ,
1695+ ) ;
1696+ map . set ( "blob handle 1" , handle1 ) ;
1697+ map . set ( "blob handle 2" , handle2 ) ;
1698+ map . set ( "blob handle 3" , handle3 ) ;
1699+ const pendingState = await container1 . getPendingLocalState ( ) ;
1700+ container1 . close ( ) ;
1701+
1702+ // In practice, the uploads triggered by setting the handles in the map probably didn't complete before
1703+ // the getPendingLocalState() call was able to record the pending state, but maybe it could happen with the local
1704+ // service if microtasks shift around in the future. Verify we are testing the scenario we think we are testing.
1705+ const pendingBlobsRecord = pendingBlobsFromPendingState ( pendingState ) ;
1706+ const pendingBlobs = Object . values ( pendingBlobsRecord ) ;
1707+ assert . strictEqual ( pendingBlobs ?. length , 3 , "Expect 3 pending blobs in pending state" ) ;
1708+ assert (
1709+ pendingBlobs . every ( ( pendingBlob ) => pendingBlob . state === "localOnly" ) ,
1710+ "Expect none of the uploads have completed yet" ,
1711+ ) ;
16781712
1679- const container2 = await loader . resolve ( { url } , pendingOps ) ;
1713+ const container2 = await loader . resolve ( { url } , pendingState ) ;
16801714 const dataStore2 = ( await container2 . getEntryPoint ( ) ) as ITestFluidObject ;
16811715 const map2 = await dataStore2 . getSharedObject < ISharedMap > ( mapId ) ;
1716+ // TODO: We've not yet decided where to expose sharePendingBlobs(), so for now casting and reaching.
1717+ // Replace with calling the proper API when available.
1718+ // Share the pending blob so container1 will be able to find it.
1719+ await ( dataStore2 . context . containerRuntime as any ) . blobManager . sharePendingBlobs ( ) ;
16821720 await provider . ensureSynchronized ( ) ;
16831721 for ( let i = 1 ; i <= 3 ; i ++ ) {
16841722 assert . strictEqual (
@@ -1692,26 +1730,24 @@ describeCompat("stashed ops", "NoCompat", (getTestObjectProvider, apis) => {
16921730 }
16931731 } ) ;
16941732
1695- it . skip ( "load offline from stashed ops with pending blob" , async function ( ) {
1733+ it ( "load offline from stashed ops with pending blob" , async function ( ) {
16961734 const container = await loadContainerOffline ( testContainerConfig , provider , { url } ) ;
16971735 const dataStore = ( await container . container . getEntryPoint ( ) ) as ITestFluidObject ;
16981736 const map = await dataStore . getSharedObject < ISharedMap > ( mapId ) ;
16991737
17001738 // Call uploadBlob() while offline to get local ID handle, and generate an op referencing it
1701- const handleP = dataStore . runtime . uploadBlob ( stringToBuffer ( "blob contents 1" , "utf8" ) ) ;
1702- // TODO: This portion was using closeAndGetPendingLocalState - using getPendingLocalState instead to allow compilation
1703- // const stashedChangesP = container.container.closeAndGetPendingLocalState?.();
1704- const stashedChangesP = container . container . getPendingLocalState ( ) ;
1705- const handle = await handleP ;
1739+ const handle = await dataStore . runtime . uploadBlob (
1740+ stringToBuffer ( "blob contents 1" , "utf8" ) ,
1741+ ) ;
17061742 map . set ( "blob handle 1" , handle ) ;
1707-
1708- const stashedChanges = await stashedChangesP ;
1743+ const pendingState = await container . container . getPendingLocalState ( ) ;
1744+ container . container . close ( ) ;
17091745
17101746 const container3 = await loadContainerOffline (
17111747 testContainerConfig ,
17121748 provider ,
17131749 { url } ,
1714- stashedChanges ,
1750+ pendingState ,
17151751 ) ;
17161752 const dataStore3 = ( await container3 . container . getEntryPoint ( ) ) as ITestFluidObject ;
17171753 const map3 = await dataStore3 . getSharedObject < ISharedMap > ( mapId ) ;
@@ -1723,6 +1759,10 @@ describeCompat("stashed ops", "NoCompat", (getTestObjectProvider, apis) => {
17231759 ) ;
17241760 container3 . connect ( ) ;
17251761 await waitForContainerConnection ( container3 . container ) ;
1762+ // TODO: We've not yet decided where to expose sharePendingBlobs(), so for now casting and reaching.
1763+ // Replace with calling the proper API when available.
1764+ // Share the pending blob so container1 will be able to find it.
1765+ await ( dataStore3 . context . containerRuntime as any ) . blobManager . sharePendingBlobs ( ) ;
17261766 await provider . ensureSynchronized ( ) ;
17271767
17281768 assert . strictEqual (
@@ -1735,25 +1775,27 @@ describeCompat("stashed ops", "NoCompat", (getTestObjectProvider, apis) => {
17351775 ) ;
17361776 } ) ;
17371777
1738- it . skip ( "stashed changes with blobs" , async function ( ) {
1778+ it ( "stashed changes with blobs" , async function ( ) {
17391779 const container = await loadContainerOffline ( testContainerConfig , provider , { url } ) ;
17401780 const dataStore = ( await container . container . getEntryPoint ( ) ) as ITestFluidObject ;
17411781 const map = await dataStore . getSharedObject < ISharedMap > ( mapId ) ;
17421782
17431783 // Call uploadBlob() while offline to get local ID handle, and generate an op referencing it
1744- const handleP = dataStore . runtime . uploadBlob ( stringToBuffer ( "blob contents 1" , "utf8" ) ) ;
1745- // TODO: This portion was using closeAndGetPendingLocalState - using getPendingLocalState instead to allow compilation
1746- // const stashedChangesP = container.container.closeAndGetPendingLocalState?.();
1747- const stashedChangesP = container . container . getPendingLocalState ( ) ;
1748- const handle = await handleP ;
1784+ const handle = await dataStore . runtime . uploadBlob (
1785+ stringToBuffer ( "blob contents 1" , "utf8" ) ,
1786+ ) ;
17491787 map . set ( "blob handle 1" , handle ) ;
1788+ const pendingState = await container . container . getPendingLocalState ( ) ;
1789+ container . container . close ( ) ;
17501790
1751- const stashedChanges = await stashedChangesP ;
1752-
1753- const container3 = await loader . resolve ( { url } , stashedChanges ) ;
1791+ const container3 = await loader . resolve ( { url } , pendingState ) ;
17541792 const dataStore3 = ( await container3 . getEntryPoint ( ) ) as ITestFluidObject ;
17551793 const map3 = await dataStore3 . getSharedObject < ISharedMap > ( mapId ) ;
17561794
1795+ // TODO: We've not yet decided where to expose sharePendingBlobs(), so for now casting and reaching.
1796+ // Replace with calling the proper API when available.
1797+ // Share the pending blob so container1 will be able to find it.
1798+ await ( dataStore3 . context . containerRuntime as any ) . blobManager . sharePendingBlobs ( ) ;
17571799 await provider . ensureSynchronized ( ) ;
17581800
17591801 // Blob is uploaded and accessible by all clients
0 commit comments