Skip to content

Commit

Permalink
a bit faster model creation
Browse files Browse the repository at this point in the history
  • Loading branch information
xaviergonz committed Mar 28, 2024
1 parent 0f7ba53 commit 84054e1
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 92 deletions.
13 changes: 7 additions & 6 deletions packages/lib/src/model/BaseModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { assertIsObject, failure } from "../utils"
import { getModelIdPropertyName } from "./getModelMetadata"
import { modelIdKey, modelTypeKey } from "./metadata"
import type { ModelConstructorOptions } from "./ModelConstructorOptions"
import { internalNewModel } from "./newModel"
import { internalFromSnapshotModel, internalNewModel } from "./newModel"
import { assertIsModelClass } from "./utils"

/**
Expand Down Expand Up @@ -147,13 +147,14 @@ export abstract class BaseModel<
// plain new
assertIsObject(initialData, "initialData")

internalNewModel(this, observable.object(initialData as any, undefined, { deep: false }), {
modelClass,
generateNewIds: true,
})
internalNewModel(
this,
observable.object(initialData as any, undefined, { deep: false }),
modelClass!
)
} else {
// from snapshot
internalNewModel(this, undefined, { modelClass, snapshotInitialData, generateNewIds })
internalFromSnapshotModel(this, snapshotInitialData!, modelClass!, !!generateNewIds)
}
}

Expand Down
245 changes: 159 additions & 86 deletions packages/lib/src/model/newModel.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { action, set } from "mobx"
import type { O } from "ts-toolbelt"
import { isModelAutoTypeCheckingEnabled } from "../globalConfig/globalConfig"
import type { ModelCreationData } from "../modelShared/BaseModelShared"
import type { ModelClass, ModelCreationData } from "../modelShared/BaseModelShared"
import { modelInfoByClass } from "../modelShared/modelInfo"
import { getInternalModelClassPropsInfo } from "../modelShared/modelPropsInfo"
import { applyModelInitializers } from "../modelShared/newModel"
Expand All @@ -11,7 +11,7 @@ import { createPatchForObjectValueChange, emitPatches } from "../patch/emitPatch
import { tweakModel } from "../tweaker/tweakModel"
import { tweakPlainObject } from "../tweaker/tweakPlainObject"
import { failure, inDevMode, makePropReadonly } from "../utils"
import { setIfDifferentWithReturn } from "../utils/setIfDifferent"
import { setIfDifferent, setIfDifferentWithReturn } from "../utils/setIfDifferent"
import type { AnyModel } from "./BaseModel"
import type { ModelConstructorOptions } from "./ModelConstructorOptions"
import { getModelIdPropertyName, getModelMetadata } from "./getModelMetadata"
Expand All @@ -25,63 +25,30 @@ export const internalNewModel = action(
"newModel",
<M extends AnyModel>(
origModelObj: M,
initialData: ModelCreationData<M> | undefined,
options: Pick<ModelConstructorOptions, "modelClass" | "snapshotInitialData" | "generateNewIds">
): M => {
const mode = initialData ? "new" : "fromSnapshot"
const { modelClass: _modelClass, snapshotInitialData, generateNewIds } = options
const modelClass = _modelClass!

initialData: ModelCreationData<M>,
modelClass: ModelClass<AnyModel>
): void => {
if (inDevMode) {
assertIsModelClass(modelClass, "modelClass")
}

const modelObj = origModelObj as O.Writable<M>
const { modelInfo, modelIdPropertyName, modelProps, modelIdPropData } =
getModelDetails(modelClass)

const modelInfo = modelInfoByClass.get(modelClass)
if (!modelInfo) {
throw failure(
`no model info for class ${modelClass.name} could be found - did you forget to add the @model decorator?`
)
}

const modelIdPropertyName = getModelIdPropertyName(modelClass)
const modelProps = getInternalModelClassPropsInfo(modelClass)
const modelIdPropData = modelIdPropertyName ? modelProps[modelIdPropertyName]! : undefined

let id: string | undefined
if (snapshotInitialData) {
let sn = snapshotInitialData.unprocessedSnapshot

if (modelIdPropData && modelIdPropertyName) {
if (generateNewIds) {
id = (modelIdPropData._defaultFn as () => string)()
} else {
id = sn[modelIdPropertyName]
}
}

if (modelClass.fromSnapshotProcessor) {
sn = modelClass.fromSnapshotProcessor(sn)
}

initialData = snapshotInitialData.snapshotToInitialData(sn)
} else {
// use symbol if provided
if (modelIdPropData && modelIdPropertyName) {
if (initialData![modelIdPropertyName]) {
id = initialData![modelIdPropertyName]
} else {
id = (modelIdPropData._defaultFn as () => string)()
}
// use symbol if provided
if (modelIdPropertyName && modelIdPropData) {
let id: string | undefined
if (initialData[modelIdPropertyName]) {
id = initialData[modelIdPropertyName]
} else {
id = (modelIdPropData._defaultFn as () => string)()
}
setIfDifferent(initialData, modelIdPropertyName, id)
}

const modelObj = origModelObj as O.Writable<M>
modelObj[modelTypeKey] = modelInfo.name

const patches: Patch[] = []
const inversePatches: Patch[] = []

// fill in defaults in initial data
const modelPropsKeys = Object.keys(modelProps)
for (let i = 0; i < modelPropsKeys.length; i++) {
Expand All @@ -94,12 +61,12 @@ export const internalNewModel = action(

const propData = modelProps[k]

const initialValue = initialData![k]
const initialValue = initialData[k]
let newValue = initialValue
let changed = false

// apply untransform (if any) if not in snapshot mode
if (mode === "new" && propData._transform) {
if (propData._transform) {
changed = true
newValue = propData._transform.untransform(newValue, modelObj, k)
}
Expand All @@ -118,60 +85,129 @@ export const internalNewModel = action(

if (changed) {
// setIfDifferent not required
set(initialData!, k, newValue)
set(initialData, k, newValue)
}
}

if (mode === "fromSnapshot" && newValue !== initialValue) {
const propPath = [k]
finalizeNewModel(modelObj, initialData, modelClass)

patches.push(createPatchForObjectValueChange(propPath, initialValue, newValue))
inversePatches.push(createPatchForObjectValueChange(propPath, newValue, initialValue))
}
// type check it if needed
if (isModelAutoTypeCheckingEnabled() && getModelMetadata(modelClass).dataType) {
const err = modelObj.typeCheck()
if (err) {
err.throw()
}
}
}
)

/**
* @internal
*/
export const internalFromSnapshotModel = action(
"fromSnapshotModel",
<M extends AnyModel>(
origModelObj: M,
snapshotInitialData: NonNullable<ModelConstructorOptions["snapshotInitialData"]>,
modelClass: ModelClass<AnyModel>,
generateNewIds: boolean
): void => {
if (inDevMode) {
assertIsModelClass(modelClass, "modelClass")
}

const { modelInfo, modelIdPropertyName, modelProps, modelIdPropData } =
getModelDetails(modelClass)

let id: string | undefined
let sn = snapshotInitialData.unprocessedSnapshot

if (modelIdPropData && modelIdPropertyName) {
if (generateNewIds) {
id = (modelIdPropData._defaultFn as () => string)()
} else {
id = sn[modelIdPropertyName]
}
}

if (modelClass.fromSnapshotProcessor) {
sn = modelClass.fromSnapshotProcessor(sn)
}

const initialData = snapshotInitialData.snapshotToInitialData(sn)

const modelObj = origModelObj as O.Writable<M>
modelObj[modelTypeKey] = modelInfo.name

const patches: Patch[] = []
const inversePatches: Patch[] = []

if (modelIdPropertyName) {
const initialValue = initialData![modelIdPropertyName]
const initialValue = initialData[modelIdPropertyName]
const valueChanged = setIfDifferentWithReturn(initialData, modelIdPropertyName, id)

if (valueChanged && mode === "fromSnapshot") {
if (valueChanged) {
const modelIdPath = [modelIdPropertyName]

patches.push(createPatchForObjectValueChange(modelIdPath, initialValue, id))
inversePatches.push(createPatchForObjectValueChange(modelIdPath, id, initialValue))
}
}

if (mode === "fromSnapshot") {
// also emit a patch for modelType, since it will get included in the snapshot
const initialModelType = snapshotInitialData?.unprocessedModelType
const newModelType = modelInfo.name
if (initialModelType !== newModelType) {
const modelTypePath = [modelTypeKey]

patches.push(createPatchForObjectValueChange(modelTypePath, initialModelType, newModelType))
inversePatches.push(
createPatchForObjectValueChange(modelTypePath, newModelType, initialModelType)
)
// fill in defaults in initial data
const modelPropsKeys = Object.keys(modelProps)
for (let i = 0; i < modelPropsKeys.length; i++) {
const k = modelPropsKeys[i]

// id is already initialized above
if (k === modelIdPropertyName) {
continue
}
}

tweakModel(modelObj, undefined)
const propData = modelProps[k]

// create observable data object with initial data
modelObj.$ = tweakPlainObject(
initialData!,
{ parent: modelObj, path: "$" },
modelObj[modelTypeKey],
false,
true
)
const initialValue = initialData[k]
let newValue = initialValue
let changed = false

if (inDevMode) {
makePropReadonly(modelObj, "$", true)
// apply default value (if needed)
if (newValue == null) {
const defaultValue = getModelPropDefaultValue(propData)
if (defaultValue !== noDefaultValue) {
changed = true
newValue = defaultValue
} else if (!(k in initialData!)) {
// for mobx4, we need to set up properties even if they are undefined
changed = true
}
}

if (changed) {
// setIfDifferent not required
set(initialData, k, newValue)

if (newValue !== initialValue) {
const propPath = [k]

patches.push(createPatchForObjectValueChange(propPath, initialValue, newValue))
inversePatches.push(createPatchForObjectValueChange(propPath, newValue, initialValue))
}
}
}

// also emit a patch for modelType, since it will get included in the snapshot
const initialModelType = snapshotInitialData?.unprocessedModelType
const newModelType = modelInfo.name
if (initialModelType !== newModelType) {
const modelTypePath = [modelTypeKey]

patches.push(createPatchForObjectValueChange(modelTypePath, initialModelType, newModelType))
inversePatches.push(
createPatchForObjectValueChange(modelTypePath, newModelType, initialModelType)
)
}

// run any extra initializers for the class as needed
applyModelInitializers(modelClass, modelObj)
finalizeNewModel(modelObj, initialData, modelClass)

emitPatches(modelObj, patches, inversePatches)

Expand All @@ -182,7 +218,44 @@ export const internalNewModel = action(
err.throw()
}
}

return modelObj as M
}
)

function getModelDetails(modelClass: ModelClass<AnyModel>) {
const modelInfo = modelInfoByClass.get(modelClass)
if (!modelInfo) {
throw failure(
`no model info for class ${modelClass.name} could be found - did you forget to add the @model decorator?`
)
}

const modelIdPropertyName = getModelIdPropertyName(modelClass)
const modelProps = getInternalModelClassPropsInfo(modelClass)
const modelIdPropData = modelIdPropertyName ? modelProps[modelIdPropertyName]! : undefined

return { modelInfo, modelIdPropertyName, modelProps, modelIdPropData }
}

function finalizeNewModel(
modelObj: O.Writable<AnyModel>,
initialData: any,
modelClass: ModelClass<AnyModel>
) {
tweakModel(modelObj, undefined)

// create observable data object with initial data
modelObj.$ = tweakPlainObject(
initialData,
{ parent: modelObj, path: "$" },
modelObj[modelTypeKey],
false,
true
)

if (inDevMode) {
makePropReadonly(modelObj, "$", true)
}

// run any extra initializers for the class as needed
applyModelInitializers(modelClass, modelObj)
}

0 comments on commit 84054e1

Please sign in to comment.