Skip to content

Commit

Permalink
JS state manager: clean up the hack for .state accessor
Browse files Browse the repository at this point in the history
observable.state now formally issues a state update in order to gain access to
the current state.
It works, but it's counterintuitive when debugging and might be inefficient
  • Loading branch information
exyi committed May 12, 2024
1 parent b8bef11 commit ef7cc4c
Showing 1 changed file with 23 additions and 25 deletions.
48 changes: 23 additions & 25 deletions src/Framework/Framework/Resources/Scripts/state-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class StateManager<TViewModel extends { $type?: TypeDefinition }> {
public stateUpdateEvent: DotvvmEvent<DeepReadonly<TViewModel>>
) {
this._state = coerce(initialState, initialState.$type || { type: "dynamic" })
this.stateObservable = createWrappedObservable(initialState, (initialState as any)["$type"], u => this.update(u as any))
this.stateObservable = createWrappedObservable(initialState, (initialState as any)["$type"], () => this._state, u => this.update(u as any))
this.dispatchUpdate()
}

Expand Down Expand Up @@ -135,7 +135,7 @@ class FakeObservableObject<T extends object> implements UpdatableObjectExtension
return Object.freeze({ ...vm, [propName]: newValue }) as any
})
}
constructor(initialValue: T, updater: UpdateDispatcher<T>, typeId: TypeDefinition, typeInfo: ObjectTypeMetadata | DynamicTypeMetadata | undefined, additionalProperties: string[]) {
constructor(initialValue: T, getter: () => DeepReadonly<T> | undefined, updater: UpdateDispatcher<T>, typeId: TypeDefinition, typeInfo: ObjectTypeMetadata | DynamicTypeMetadata | undefined, additionalProperties: string[]) {
this[currentStateSymbol] = initialValue
this[updateSymbol] = updater
this[errorsSymbol] = []
Expand All @@ -154,10 +154,11 @@ class FakeObservableObject<T extends object> implements UpdatableObjectExtension
const newObs = createWrappedObservable(
currentState[p],
props[p]?.type,
() => (getter() as any)?.[p],
u => this[updatePropertySymbol](p, u)
)

const isDynamic = typeId === undefined || typeId.hasOwnProperty("type") && (typeId as any)["type"] === "dynamic";
const isDynamic = typeId === undefined || (typeId as any)?.type === "dynamic";
if (typeInfo && p in props) {
const clientExtenders = props[p].clientExtenders;
if (clientExtenders) {
Expand Down Expand Up @@ -224,7 +225,7 @@ export function unmapKnockoutObservables(viewModel: any, allowStateUnwrap: boole
return result ?? value
}

function createObservableObject<T extends object>(initialObject: T, typeHint: TypeDefinition | undefined, update: ((updater: StateUpdate<any>) => void)) {
function createObservableObject<T extends object>(initialObject: T, typeHint: TypeDefinition | undefined, getter: () => DeepReadonly<T> | undefined, update: ((updater: StateUpdate<any>) => void)) {
const typeId = (initialObject as any)["$type"] || typeHint
let typeInfo;
if (typeId && !(typeId.hasOwnProperty("type") && typeId["type"] === "dynamic")) {
Expand All @@ -234,7 +235,7 @@ function createObservableObject<T extends object>(initialObject: T, typeHint: Ty
const pSet = new Set(keys((typeInfo?.type === "object") ? typeInfo.properties : {}));
const additionalProperties = keys(initialObject).filter(p => !pSet.has(p))

return new FakeObservableObject(initialObject, update, typeId, typeInfo, additionalProperties) as FakeObservableObject<T> & DeepKnockoutObservableObject<T>
return new FakeObservableObject(initialObject, getter, update, typeId, typeInfo, additionalProperties) as FakeObservableObject<T> & DeepKnockoutObservableObject<T>
}

/** Informs that we cloned an ko.observable, so updating it won't work */
Expand Down Expand Up @@ -266,7 +267,7 @@ function logObservableCloneWarning(value: any) {
}
}

function createWrappedObservable<T>(initialValue: DeepReadonly<T>, typeHint: TypeDefinition | undefined, updater: UpdateDispatcher<T>): DeepKnockoutObservable<T> {
function createWrappedObservable<T>(initialValue: DeepReadonly<T>, typeHint: TypeDefinition | undefined, getter: () => DeepReadonly<T> | undefined, updater: UpdateDispatcher<T>): DeepKnockoutObservable<T> {

let isUpdating = false

Expand Down Expand Up @@ -316,7 +317,7 @@ function createWrappedObservable<T>(initialValue: DeepReadonly<T>, typeHint: Typ
obs[currentStateSymbol] = newVal

const result = notifyCore(newVal, currentValue, observableWasSetFromOutside);
if (result && "newContents" in result) {
if (result) {
try {
isUpdating = true
obs(result.newContents)
Expand All @@ -327,7 +328,7 @@ function createWrappedObservable<T>(initialValue: DeepReadonly<T>, typeHint: Typ
}
}

function notifyCore(newVal: any, currentValue: any, observableWasSetFromOutside: boolean) {
function notifyCore(newVal: any, currentValue: any, observableWasSetFromOutside: boolean): { newContents: any } | undefined {
let newContents
const oldContents = obs.peek()
if (isPrimitive(newVal) || newVal instanceof Date) {
Expand All @@ -348,20 +349,24 @@ function createWrappedObservable<T>(initialValue: DeepReadonly<T>, typeHint: Typ
newContents = oldContents instanceof Array ? oldContents.slice(0, newVal.length) : []
// then append (potential) new values into the array
for (let index = 0; index < newVal.length; index++) {
if (newContents[index] && newContents[index][notifySymbol as any]) {
if (newContents[index]?.[notifySymbol as any]) {
continue
}
const indexForClosure = index
newContents[index] = createWrappedObservable(newVal[index], Array.isArray(typeHint) ? typeHint[0] : void 0, update => updater((viewModelArray: any) => {
if (viewModelArray == null || viewModelArray.length <= indexForClosure) {
const itemUpdater = (update: any) => updater((viewModelArray: any) => {
if (viewModelArray == null || viewModelArray.length <= index) {
// the item or the array does not exist anymore
return viewModelArray
}
const newElement = update(viewModelArray[indexForClosure])
const newArray = createArray(viewModelArray)
newArray[indexForClosure] = newElement
const newArray = [...viewModelArray]
newArray[index] = update(viewModelArray[index])
return Object.freeze(newArray) as any
}))
})
newContents[index] = createWrappedObservable(
newVal[index],
Array.isArray(typeHint) ? typeHint[0] : void 0,
() => (getter() as any[])?.[index],
itemUpdater
)
}
}
else {
Expand All @@ -386,7 +391,7 @@ function createWrappedObservable<T>(initialValue: DeepReadonly<T>, typeHint: Typ
}
else {
// create new object and replace
newContents = createObservableObject(newVal, typeHint, updater)
newContents = createObservableObject(newVal, typeHint, getter, updater)
}

// return a result indicating that the observable needs to be set
Expand All @@ -397,14 +402,7 @@ function createWrappedObservable<T>(initialValue: DeepReadonly<T>, typeHint: Typ
notify(initialValue)

Object.defineProperty(obs, "state", {
get: () => {
let resultState
updater(state => {
resultState = state
return state
})
return resultState
}
get: getter
});
defineConstantProperty(obs, "patchState", (patch: any) => {
updater(state => patchViewModel(state, patch))
Expand Down

0 comments on commit ef7cc4c

Please sign in to comment.