Skip to content

Commit

Permalink
Export the JS StateManager instance
Browse files Browse the repository at this point in the history
Most notably, this makes it possible to manually dispatch
a ko.observable update using

    dotvvm.rootStateManager.doUpdateNow()
  • Loading branch information
exyi committed Dec 15, 2024
1 parent e9a59f4 commit 85d1aa4
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 7 deletions.
3 changes: 2 additions & 1 deletion src/Framework/Framework/Resources/Scripts/dotvvm-root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ const dotvvmExports = {
getStateManager().patchState(a)
},
setState(a: any) { getStateManager().setState(a) },
updateState(updateFunction: StateUpdate<any>) { getStateManager().update(updateFunction) },
updateState(updateFunction: StateUpdate<any>) { getStateManager().updateState(updateFunction) },
get rootStateManager() { return getStateManager() },
viewModelObservables: {
get root() { return getViewModelObservable(); }
},
Expand Down
2 changes: 1 addition & 1 deletion src/Framework/Framework/Resources/Scripts/postback/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export async function retryOnInvalidCsrfToken<TResult>(postbackFunction: () => P
if (err.reason.type === "serverError") {
if (err.reason.responseObject?.action === "invalidCsrfToken") {
logInfoVerbose("postback", "Resending postback due to invalid CSRF token.");
getStateManager().update(u => ({ ...u, $csrfToken: undefined }))
getStateManager().updateState(u => ({ ...u, $csrfToken: undefined }))

if (iteration < 3) {
return await retryOnInvalidCsrfToken(postbackFunction, iteration + 1);
Expand Down
36 changes: 31 additions & 5 deletions src/Framework/Framework/Resources/Scripts/state-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,21 @@ export type UpdatableObjectExtensions<T> = {
[updateSymbol]?: UpdateDispatcher<T>
}

export class StateManager<TViewModel extends { $type?: TypeDefinition }> {
/** Manages the consistency of DotVVM immutable ViewModel state object with the knockout observables.
* Knockout observables are by-default updated asynchronously after a state change, but the synchronization can be forced by calling `doUpdateNow`.
* The newState event is also called asynchronously right before the knockout observables are updated.
* Changes from observables to state are immediate.
*/
export class StateManager<TViewModel extends { $type?: TypeDefinition }> implements DotvvmStateContainer<TViewModel> {
/** The knockout observable containing the root objects, equivalent to `dotvvm.viewModels.root.viewModel` */
public readonly stateObservable: DeepKnockoutObservable<TViewModel>;
private _state: DeepReadonly<TViewModel>
/** Returns the current */
public get state() {
return this._state
}
private _isDirty: boolean = false;
/** Indicates whether there is a pending update of the knockout observables. */
public get isDirty() {
return this._isDirty
}
Expand All @@ -49,17 +57,20 @@ 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"], () => this._state, u => this.update(u as any))
this.stateObservable = createWrappedObservable(initialState, (initialState as any)["$type"], () => this._state, u => this.updateState(u as any))
this.dispatchUpdate()
}

public dispatchUpdate() {
private dispatchUpdate() {
if (!this._isDirty) {
this._isDirty = true;
this._currentFrameNumber = window.requestAnimationFrame(this.rerender.bind(this))
}
}

/** Performs a synchronous update of knockout observables with the data currently stored in `state`.
* Consequently, if ko.options.deferUpdates is false (default), the UI will be updated immediately.
* If ko.options.deferUpdates is true, the UI can be manually updated by also calling the `ko.tasks.runEarly()` function. */
public doUpdateNow() {
if (this._currentFrameNumber !== null)
window.cancelAnimationFrame(this._currentFrameNumber);
Expand All @@ -86,6 +97,13 @@ export class StateManager<TViewModel extends { $type?: TypeDefinition }> {
//logInfoVerbose("New state dispatched, t = ", performance.now() - time, "; t_cpu = ", performance.now() - realStart);
}

/** Sets a new view model state, after checking its type compatibility and possibly performing implicit conversions.
* Only the changed objects are re-checked and updated in the knockout observables.
* It is therefore recommended to clone only the changed parts of the view model using the `{... x, ChangedProp: 1 }` syntax.
* In the rarely occuring complex cases where this is difficult, you can use `structuredClone` to obtain a writable clone of some part of the viewmodel.
*
* @throws CoerceError if the new state has incompatible type.
* @returns The type-coerced version of the new state. */
public setState(newState: DeepReadonly<TViewModel>): DeepReadonly<TViewModel> {
if (compileConstants.debug && newState == null) throw new Error("State can't be null or undefined.")
if (newState === this._state) return newState
Expand All @@ -98,13 +116,21 @@ export class StateManager<TViewModel extends { $type?: TypeDefinition }> {
return this._state = coercionResult
}

public patchState(patch: Partial<TViewModel>): DeepReadonly<TViewModel> {
/** Applies a patch to the current view model state.
* @throws CoerceError if the new state has incompatible type.
* @returns The type-coerced version of the new state. */
public patchState(patch: DeepReadonly<DeepPartial<TViewModel>>): DeepReadonly<TViewModel> {
return this.setState(patchViewModel(this._state, patch))
}

public update(updater: StateUpdate<TViewModel>) {
/** Updates the view model state using the provided `State => State` function.
* @throws CoerceError if the new state has incompatible type.
* @returns The type-coerced version of the new state. */
public updateState(updater: StateUpdate<TViewModel>) {
return this.setState(updater(this._state))
}
/** @deprecated Use updateState method instead */
public update: UpdateDispatcher<TViewModel> = this.updateState;
}

class FakeObservableObject<T extends object> implements UpdatableObjectExtensions<T> {
Expand Down

0 comments on commit 85d1aa4

Please sign in to comment.