@@ -12,7 +12,7 @@ import {
1212 commitIdOf ,
1313 commitToJsonADObject ,
1414} from './commit.js' ;
15- import { validateDatatype } from './datatypes.js' ;
15+ import { validateDatatype , datatypeTag } from './datatypes.js' ;
1616import { isUnauthorized } from './error.js' ;
1717import { commits } from './ontologies/commits.js' ;
1818import { core } from './ontologies/core.js' ;
@@ -537,6 +537,45 @@ export class Resource<C extends OptionalClass = any> {
537537 this . _cache = nextCache ;
538538 }
539539
540+ /**
541+ * Phase 1 (loro-source-of-truth): populate the sibling `datatypes` Loro map
542+ * so the server recovers reference / array `Value` variants exactly instead
543+ * of guessing. The map is sparse — only load-bearing datatypes get a tag;
544+ * see {@link datatypeTag}. Idempotent: re-signing rewrites nothing.
545+ *
546+ * Cache-only — never triggers a fetch. A property whose definition is not
547+ * already cached is left untagged; the server then falls back to its
548+ * materialization heuristic, exactly as before this map existed. Properties
549+ * edited via `set()` with validation are always cached by the time we sign.
550+ */
551+ private writeDatatypeTags ( ) : void {
552+ const doc = this . _loroDoc ;
553+
554+ if ( ! doc ) {
555+ return ;
556+ }
557+
558+ const props = doc . getMap ( 'properties' ) . toJSON ( ) as Record < string , unknown > ;
559+ const datatypesMap = doc . getMap ( 'datatypes' ) ;
560+
561+ for ( const [ prop , loroValue ] of Object . entries ( props ) ) {
562+ const datatype = this . store ?. resources
563+ . get ( prop )
564+ ?. get ( core . properties . datatype )
565+ ?. toString ( ) ;
566+
567+ if ( datatype === undefined ) {
568+ continue ;
569+ }
570+
571+ const tag = datatypeTag ( datatype , loroValue ) ;
572+
573+ if ( tag !== undefined && datatypesMap . get ( prop ) !== tag ) {
574+ datatypesMap . set ( prop , tag ) ;
575+ }
576+ }
577+ }
578+
540579 private resetLoroState ( ) : void {
541580 this . _loroDoc = undefined ;
542581 this . _loroMap = undefined ;
@@ -1396,11 +1435,7 @@ export class Resource<C extends OptionalClass = any> {
13961435 list = map . setContainer ( propUrl , new LoroList ( ) ) ;
13971436 }
13981437
1399- if (
1400- item !== null &&
1401- typeof item === 'object' &&
1402- ! Array . isArray ( item )
1403- ) {
1438+ if ( item !== null && typeof item === 'object' && ! Array . isArray ( item ) ) {
14041439 const itemMap = list . pushContainer ( new LoroMap ( ) ) ;
14051440 this . writeJsonToLoroMap ( itemMap , item as JSONObject ) ;
14061441 } else {
@@ -1438,10 +1473,7 @@ export class Resource<C extends OptionalClass = any> {
14381473 }
14391474 }
14401475
1441- private writeJsonToLoroList (
1442- list : LoroList ,
1443- arr : JSONValue [ ] ,
1444- ) : void {
1476+ private writeJsonToLoroList ( list : LoroList , arr : JSONValue [ ] ) : void {
14451477 const { LoroList, LoroMap } = LoroLoader . Loro ;
14461478
14471479 for ( const item of arr ) {
@@ -1501,6 +1533,12 @@ export class Resource<C extends OptionalClass = any> {
15011533 this . rebuildCacheFromLoro ( ) ;
15021534 this . _cacheDirty = false ;
15031535
1536+ // Phase 1 (loro-source-of-truth): stamp the sibling `datatypes` map so
1537+ // the server materializes references/arrays exactly. Runs here — after
1538+ // every property is in the doc, before the snapshot export below — so it
1539+ // covers props set via `set()` and via cache hydration alike.
1540+ this . writeDatatypeTags ( ) ;
1541+
15041542 // Chain: use last locally-signed commit, or the server-known lastCommit.
15051543 if ( this . _lastLocalSignature ) {
15061544 // Construct the full commit URL that the server will use. This ensures
0 commit comments