Skip to content

Commit c214929

Browse files
chriszaratechriszarateyouknowriad
authored
Collaborative editing: Make syncing a side-concern instead of a replacement for local state (#72114)
Co-authored-by: chriszarate <czarate@git.wordpress.org> Co-authored-by: youknowriad <youknowriad@git.wordpress.org>
1 parent a585b8b commit c214929

3 files changed

Lines changed: 100 additions & 115 deletions

File tree

packages/core-data/src/actions.js

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -417,32 +417,28 @@ export const editEntityRecord =
417417
edit.edits
418418
);
419419
}
420-
} else {
421-
if ( ! options.undoIgnore ) {
422-
select.getUndoManager().addRecord(
423-
[
424-
{
425-
id: { kind, name, recordId },
426-
changes: Object.keys( edits ).reduce(
427-
( acc, key ) => {
428-
acc[ key ] = {
429-
from: editedRecord[ key ],
430-
to: edits[ key ],
431-
};
432-
return acc;
433-
},
434-
{}
435-
),
436-
},
437-
],
438-
options.isCached
439-
);
440-
}
441-
dispatch( {
442-
type: 'EDIT_ENTITY_RECORD',
443-
...edit,
444-
} );
445420
}
421+
if ( ! options.undoIgnore ) {
422+
select.getUndoManager().addRecord(
423+
[
424+
{
425+
id: { kind, name, recordId },
426+
changes: Object.keys( edits ).reduce( ( acc, key ) => {
427+
acc[ key ] = {
428+
from: editedRecord[ key ],
429+
to: edits[ key ],
430+
};
431+
return acc;
432+
}, {} ),
433+
},
434+
],
435+
options.isCached
436+
);
437+
}
438+
dispatch( {
439+
type: 'EDIT_ENTITY_RECORD',
440+
...edit,
441+
} );
446442
};
447443

448444
/**

packages/core-data/src/entities.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ async function loadPostTypeEntities() {
313313
name
314314
);
315315
const namespace = postType?.rest_namespace ?? 'wp/v2';
316+
const syncedProperties = new Set( [ 'blocks' ] );
316317
return {
317318
kind: 'postType',
318319
baseURL: `/${ namespace }/${ postType.rest_base }`,
@@ -343,6 +344,10 @@ async function loadPostTypeEntities() {
343344
const document = doc.getMap( 'document' );
344345

345346
Object.entries( changes ).forEach( ( [ key, value ] ) => {
347+
if ( ! syncedProperties.has( key ) ) {
348+
return;
349+
}
350+
346351
if ( typeof value !== 'function' ) {
347352
if ( key === 'blocks' ) {
348353
if ( ! serialisableBlocksCache.has( value ) ) {

packages/core-data/src/resolvers.js

Lines changed: 74 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,68 @@ export const getEntityRecord =
9393
);
9494

9595
try {
96-
// Entity supports configs,
97-
// use the sync algorithm instead of the old fetch behavior.
96+
if ( query !== undefined && query._fields ) {
97+
// If requesting specific fields, items and query association to said
98+
// records are stored by ID reference. Thus, fields must always include
99+
// the ID.
100+
query = {
101+
...query,
102+
_fields: [
103+
...new Set( [
104+
...( getNormalizedCommaSeparable( query._fields ) ||
105+
[] ),
106+
entityConfig.key || DEFAULT_ENTITY_KEY,
107+
] ),
108+
].join(),
109+
};
110+
}
111+
112+
if ( query !== undefined && query._fields ) {
113+
// The resolution cache won't consider query as reusable based on the
114+
// fields, so it's tested here, prior to initiating the REST request,
115+
// and without causing `getEntityRecord` resolution to occur.
116+
const hasRecord = select.hasEntityRecord(
117+
kind,
118+
name,
119+
key,
120+
query
121+
);
122+
if ( hasRecord ) {
123+
return;
124+
}
125+
}
126+
127+
const path = addQueryArgs(
128+
entityConfig.baseURL + ( key ? '/' + key : '' ),
129+
{
130+
...entityConfig.baseURLParams,
131+
...query,
132+
}
133+
);
134+
const response = await apiFetch( { path, parse: false } );
135+
const record = await response.json();
136+
const permissions = getUserPermissionsFromAllowHeader(
137+
response.headers?.get( 'allow' )
138+
);
139+
140+
const canUserResolutionsArgs = [];
141+
const receiveUserPermissionArgs = {};
142+
for ( const action of ALLOWED_RESOURCE_ACTIONS ) {
143+
receiveUserPermissionArgs[
144+
getUserPermissionCacheKey( action, {
145+
kind,
146+
name,
147+
id: key,
148+
} )
149+
] = permissions[ action ];
150+
151+
canUserResolutionsArgs.push( [
152+
action,
153+
{ kind, name, id: key },
154+
] );
155+
}
156+
157+
// Entity supports syncing.
98158
if (
99159
window.__experimentalEnableSync &&
100160
entityConfig.syncConfig &&
@@ -103,112 +163,36 @@ export const getEntityRecord =
103163
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
104164
const objectId = entityConfig.getSyncObjectId( key );
105165

106-
// Loads the persisted document.
107-
await getSyncProvider().bootstrap(
108-
entityConfig.syncObjectType,
109-
objectId,
110-
( record ) => {
111-
dispatch.receiveEntityRecords(
112-
kind,
113-
name,
114-
record,
115-
query
116-
);
117-
}
166+
getSyncProvider().register(
167+
entityConfig.syncObjectType + '--edit',
168+
entityConfig.syncConfig
118169
);
119170

120-
// Bootstraps the edited document as well (and load from peers).
171+
// Bootstraps the edited document (and load from peers).
121172
await getSyncProvider().bootstrap(
122173
entityConfig.syncObjectType + '--edit',
123174
objectId,
124-
( record ) => {
175+
( edits ) => {
125176
dispatch( {
126177
type: 'EDIT_ENTITY_RECORD',
127178
kind,
128179
name,
129180
recordId: key,
130-
edits: record,
181+
edits,
131182
meta: {
132183
undo: undefined,
133184
},
134185
} );
135186
}
136187
);
137188
}
138-
} else {
139-
if ( query !== undefined && query._fields ) {
140-
// If requesting specific fields, items and query association to said
141-
// records are stored by ID reference. Thus, fields must always include
142-
// the ID.
143-
query = {
144-
...query,
145-
_fields: [
146-
...new Set( [
147-
...( getNormalizedCommaSeparable(
148-
query._fields
149-
) || [] ),
150-
entityConfig.key || DEFAULT_ENTITY_KEY,
151-
] ),
152-
].join(),
153-
};
154-
}
155-
156-
if ( query !== undefined && query._fields ) {
157-
// The resolution cache won't consider query as reusable based on the
158-
// fields, so it's tested here, prior to initiating the REST request,
159-
// and without causing `getEntityRecord` resolution to occur.
160-
const hasRecord = select.hasEntityRecord(
161-
kind,
162-
name,
163-
key,
164-
query
165-
);
166-
if ( hasRecord ) {
167-
return;
168-
}
169-
}
170-
171-
const path = addQueryArgs(
172-
entityConfig.baseURL + ( key ? '/' + key : '' ),
173-
{
174-
...entityConfig.baseURLParams,
175-
...query,
176-
}
177-
);
178-
const response = await apiFetch( { path, parse: false } );
179-
const record = await response.json();
180-
const permissions = getUserPermissionsFromAllowHeader(
181-
response.headers?.get( 'allow' )
182-
);
183-
184-
const canUserResolutionsArgs = [];
185-
const receiveUserPermissionArgs = {};
186-
for ( const action of ALLOWED_RESOURCE_ACTIONS ) {
187-
receiveUserPermissionArgs[
188-
getUserPermissionCacheKey( action, {
189-
kind,
190-
name,
191-
id: key,
192-
} )
193-
] = permissions[ action ];
194-
195-
canUserResolutionsArgs.push( [
196-
action,
197-
{ kind, name, id: key },
198-
] );
199-
}
200-
201-
registry.batch( () => {
202-
dispatch.receiveEntityRecords( kind, name, record, query );
203-
dispatch.receiveUserPermissions(
204-
receiveUserPermissionArgs
205-
);
206-
dispatch.finishResolutions(
207-
'canUser',
208-
canUserResolutionsArgs
209-
);
210-
} );
211189
}
190+
191+
registry.batch( () => {
192+
dispatch.receiveEntityRecords( kind, name, record, query );
193+
dispatch.receiveUserPermissions( receiveUserPermissionArgs );
194+
dispatch.finishResolutions( 'canUser', canUserResolutionsArgs );
195+
} );
212196
} finally {
213197
dispatch.__unstableReleaseStoreLock( lock );
214198
}

0 commit comments

Comments
 (0)