Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,16 @@ describe('mutually-embedding block objects', () => {
// Generous bound - the conversion takes ~1ms when linear, while the
// unmemoized walk does not finish in any practical amount of time.
expect(durationMs).toBeLessThan(2_000)

// The emitted definition must be small AS A TREE, not merely cheap to
// build. The memoized walk builds shared `OfDefinition` objects (a
// DAG) in linear time, but before root block objects referenced by
// name at every position, each type's single expansion inlined the
// expansions of every type not on its first-visit ancestor path:
// tens of megabytes serialized for these types, out-of-memory in
// real studios. Every consumer walks the tree (schema compilation,
// React, serialization), so tree size is the contract, and
// conversion time alone cannot pin it.
expect(JSON.stringify(schema).length).toBeLessThan(100_000)
}, 10_000)
})
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@ function sanitySchemaTypeToSchema(
// per-branch ancestor sets below enumerate every simple path through
// mutually-embedding types, which grows combinatorially.
const memo = new Map<SchemaType, OfDefinition>()
// The canonical instances of the root-level block object types. Members
// reaching one of these at any `of` position emit a bare `{type: name}`
// reference instead of an inline expansion: the root `blockObjects`
// entry materializes the fields exactly once. Without this, each type's
// single memoized expansion inlines the expansions of every type not on
// its first-visit ancestor path, and the emitted definition, while
// cheap to build as a shared DAG, is factorially large as a tree, which
// is what every consumer (schema compilation, React, serialization)
// walks. Keyed by instance so a same-named but structurally different
// inline declaration keeps its own inline shape.
const rootBlockObjects = new Set<SchemaType>(blockObjectTypes)

return {
block: {
Expand Down Expand Up @@ -129,21 +140,31 @@ function sanitySchemaTypeToSchema(
name: annotation.name,
title: annotation.title,
fields: annotation.fields.map((field) =>
sanityFieldToSchemaField(field, new Set(), memo),
sanityFieldToSchemaField(field, new Set(), memo, rootBlockObjects),
),
})),
blockObjects: blockObjectTypes.map((blockObject) => ({
name: blockObject.name,
title: blockObject.title,
fields: blockObject.fields.map((field) =>
sanityFieldToSchemaField(field, new Set([blockObject.name]), memo),
sanityFieldToSchemaField(
field,
new Set([blockObject.name]),
memo,
rootBlockObjects,
),
),
})),
inlineObjects: inlineObjectTypes.map((inlineObject) => ({
name: inlineObject.name,
title: inlineObject.title,
fields: inlineObject.fields.map((field) =>
sanityFieldToSchemaField(field, new Set([inlineObject.name]), memo),
sanityFieldToSchemaField(
field,
new Set([inlineObject.name]),
memo,
rootBlockObjects,
),
),
})),
}
Expand All @@ -168,6 +189,7 @@ function sanityFieldToSchemaField(
},
ancestorNames: ReadonlySet<string>,
memo: Map<SchemaType, OfDefinition>,
rootBlockObjects: ReadonlySet<SchemaType>,
): FieldDefinition {
if (field.type.jsonType === 'array') {
const ofMembers = safeGetOf(field.type)
Expand All @@ -177,7 +199,12 @@ function sanityFieldToSchemaField(
...(field.type.title ? {title: field.type.title} : {}),
of: ofMembers
? ofMembers.map((member) =>
sanityOfMemberToOfDefinition(member, ancestorNames, memo),
sanityOfMemberToOfDefinition(
member,
ancestorNames,
memo,
rootBlockObjects,
),
)
: [],
}
Expand All @@ -194,6 +221,7 @@ function sanityOfMemberToOfDefinition(
memberType: SchemaType,
ancestorNames: ReadonlySet<string>,
memo: Map<SchemaType, OfDefinition>,
rootBlockObjects: ReadonlySet<SchemaType>,
): OfDefinition {
if (findBlockType(memberType)) {
return {type: 'block'}
Expand All @@ -208,9 +236,14 @@ function sanityOfMemberToOfDefinition(
'fields' in memberType &&
Array.isArray((memberType as ObjectSchemaType).fields)

if (!hasFields || ancestorNames.has(memberType.name)) {
if (
!hasFields ||
ancestorNames.has(memberType.name) ||
rootBlockObjects.has(memberType)
) {
// Bare reference. The editor's resolver looks up `memberType.name`
// in `blockObjects` / `inlineObjects`.
// in the root `blockObjects`. Root block objects take this branch at
// every nested position, not just on cycles; see `rootBlockObjects`.
return {
type: memberType.name,
...(memberType.title ? {title: memberType.title} : {}),
Expand All @@ -233,7 +266,7 @@ function sanityOfMemberToOfDefinition(
name: memberType.name,
...(memberType.title ? {title: memberType.title} : {}),
fields: (memberType as ObjectSchemaType).fields.map((field) =>
sanityFieldToSchemaField(field, nextAncestors, memo),
sanityFieldToSchemaField(field, nextAncestors, memo, rootBlockObjects),
),
}
memo.set(memberType, definition)
Expand Down
Loading