From f17774b859ecd7a90d5a01473428cbe127295286 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Tue, 7 Mar 2023 23:20:25 +0330 Subject: [PATCH 01/85] docs(fsm): update desc --- core/fsm/README.md | 2 +- core/fsm/package.json | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/fsm/README.md b/core/fsm/README.md index 2bfb14714..ed034c9eb 100644 --- a/core/fsm/README.md +++ b/core/fsm/README.md @@ -1,3 +1,3 @@ # Alwatr Finite State Machines - `@alwatr/fsm` -Managing invocations finite-state machines as actors written in tiny TypeScript module. +Managing invocations finite-state machines for lit-element written in tiny TypeScript module. diff --git a/core/fsm/package.json b/core/fsm/package.json index 855a36f36..caecfaebc 100644 --- a/core/fsm/package.json +++ b/core/fsm/package.json @@ -1,11 +1,14 @@ { "name": "@alwatr/fsm", "version": "0.30.0", - "description": "Managing invocations finite-state machines as actors written in tiny TypeScript module.", + "description": "Managing invocations finite-state machines for lit-element written in tiny TypeScript module.", "keywords": [ "state", "finite", "machine", + "lit", + "lit-element", + "lit-html", "typescript", "esm", "alwatr" From 7d7d6cb6e473177aa2062d5a57490a57e6b027ff Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Wed, 8 Mar 2023 18:18:25 +0330 Subject: [PATCH 02/85] feat(element): add reactive controller --- ui/element/src/index.ts | 3 ++ .../finite-state-machine.ts | 52 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 ui/element/src/reactive-controllers/finite-state-machine.ts diff --git a/ui/element/src/index.ts b/ui/element/src/index.ts index 90232ff8e..f54fe8e01 100644 --- a/ui/element/src/index.ts +++ b/ui/element/src/index.ts @@ -10,9 +10,12 @@ export * from './mixins/signal.js'; export * from './mixins/toggle.js'; export * from './mixins/unresolved.js'; export * from './mixins/state-machine.js'; +export * from './mixins/schedule-update-to-frame.js'; export * from './directives/map.js'; +export * from './reactive-controllers/finite-state-machine.js'; + export * from './lit.js'; globalAlwatr.registeredList.push({ diff --git a/ui/element/src/reactive-controllers/finite-state-machine.ts b/ui/element/src/reactive-controllers/finite-state-machine.ts new file mode 100644 index 000000000..ed6d44eda --- /dev/null +++ b/ui/element/src/reactive-controllers/finite-state-machine.ts @@ -0,0 +1,52 @@ +import {FiniteStateMachine, type StateContext, type MachineConfig} from '@alwatr/fsm'; + +import {nothing, ReactiveController} from '../lit.js'; + +import type {LoggerMixinInterface} from '../mixins/logging.js'; +import type {StringifyableRecord} from '@alwatr/type'; + +export declare class AlwatrElementHostController< + TState extends string, + TEventId extends string +> extends LoggerMixinInterface { + stateUpdate(state: StateContext): void; +} + +export class FiniteStateMachineController< + TState extends string, + TEventId extends string, + TContext extends StringifyableRecord + > + extends FiniteStateMachine + implements ReactiveController { + constructor( + private _host: AlwatrElementHostController, + config: Readonly>, + ) { + super(config); + this._logger.logMethodArgs('constructor', config); + this._host.addController(this); + } + + protected override setState(to: TState, by: TEventId | 'INIT' ): void { + super.setState(to, by); + this._host.stateUpdate.call(this._host, this.state); + } + + render(states: {[P in TState]?: (() => unknown) | TState}): unknown { + this._logger.logMethodArgs('render', this.state.to); + let renderFn = states[this.state.to]; + if (typeof renderFn === 'string') { + renderFn = states[renderFn]; + } + if (typeof renderFn === 'function') { + return renderFn?.call(this._host); + } + // else + return nothing; + } + + hostUpdate(): void { + this._host.setAttribute('state', this.state.to); + } +} From 7f246959e5a80b21c1c4b21e895e75f8fbe56798 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 9 Mar 2023 00:50:44 +0330 Subject: [PATCH 03/85] feat(fsm): rewrite state machine --- core/fsm/src/core.ts | 131 +++++++++++++++++++++---------------------- core/fsm/src/type.ts | 79 ++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 66 deletions(-) create mode 100644 core/fsm/src/type.ts diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index 62546737e..61dbe6481 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -2,80 +2,58 @@ import {createLogger, globalAlwatr} from '@alwatr/logger'; import {contextConsumer} from '@alwatr/signal'; import {dispatch} from '@alwatr/signal/core.js'; -import type {Stringifyable, StringifyableRecord} from '@alwatr/type'; +import type {FsmConfig, StateContext} from './type.js'; +import type {MaybeArray, MaybePromise, StringifyableRecord} from '@alwatr/type'; + +export type {FsmConfig, StateContext}; globalAlwatr.registeredList.push({ name: '@alwatr/fsm', version: _ALWATR_VERSION_, }); -export interface MachineConfig - extends StringifyableRecord { - /** - * Machine ID (It is used in the state change signal identifier, so it must be unique). - */ - id: string; - - /** - * Initial state. - */ - initial: TState; - - /** - * Initial context. - */ - context: TContext; - - /** - * States list - */ - states: { - [S in TState | '$all']: { - /** - * An object mapping eventId (keys) to state. - */ - on: { - [E in TEventId]?: TState | '$self'; - }; - }; - }; -} - -export interface StateContext { - [T: string]: string; - to: TState; - from: TState | 'init'; - by: TEventId | 'INIT'; -} - export class FiniteStateMachine< TState extends string = string, TEventId extends string = string, TContext extends StringifyableRecord = StringifyableRecord > { state: StateContext = { - to: this.config.initial, - from: 'init', + target: this.config.initial, + from: this.config.initial, by: 'INIT', }; + context = this.config.context; + signal = contextConsumer.bind>('finite-state-machine-' + this.config.id); protected _logger = createLogger(`alwatr/fsm:${this.config.id}`); - protected setState(to: TState, by: TEventId | 'INIT'): void { - this.state = { - to, - from: this.signal.getValue()?.to ?? 'init', + protected async setState(target: TState, by: TEventId): Promise { + const state = (this.state = { + target: target, + from: this.signal.getValue()?.target ?? target, by, - }; - dispatch>(this.signal.id, this.state, {debounce: 'NextCycle'}); + }); + + dispatch>(this.signal.id, state, {debounce: 'NextCycle'}); + + if (state.from !== state.target) { + await this.execActions(this.config.stateRecord.$all.exit); + await this.execActions(this.config.stateRecord[state.from]?.exit); + await this.execActions(this.config.stateRecord.$all.entry); + await this.execActions(this.config.stateRecord[state.target]?.entry); + } + await this.execActions( + this.config.stateRecord[state.from]?.on[state.by]?.actions ?? + this.config.stateRecord.$all.on[state.by]?.actions, + ); } - constructor(public readonly config: Readonly>) { + constructor(public readonly config: Readonly>) { this._logger.logMethodArgs('constructor', config); dispatch>(this.signal.id, this.state, {debounce: 'NextCycle'}); - if (!config.states[config.initial]) { + if (!config.stateRecord[config.initial]) { this._logger.error('constructor', 'invalid_initial_state', config); } } @@ -83,17 +61,10 @@ export class FiniteStateMachine< /** * Machine transition. */ - transition(event: TEventId, context?: Partial): TState | null { - const fromState = this.state.to; - - let toState: TState | '$self' | undefined = - this.config.states[fromState]?.on?.[event] ?? this.config.states.$all?.on?.[event]; - - if (toState === '$self') { - toState = fromState; - } - - this._logger.logMethodFull('transition', {fromState, event, context}, toState); + async transition(event: TEventId, context?: Partial): Promise { + const fromState = this.state.target; + const transitionConfig = this.config.stateRecord[fromState]?.on[event] ?? this.config.stateRecord.$all?.on[event]; + this._logger.logMethodArgs('transition', {fromState, event, context, target: transitionConfig?.target}); if (context !== undefined) { this.context = { @@ -102,7 +73,7 @@ export class FiniteStateMachine< }; } - if (toState == null) { + if (transitionConfig == null) { this._logger.incident( 'transition', 'invalid_target_state', @@ -110,13 +81,41 @@ export class FiniteStateMachine< { fromState, event, - events: {...this.config.states.$all?.on, ...this.config.states[fromState]?.on}, + events: {...this.config.stateRecord.$all?.on, ...this.config.stateRecord[fromState]?.on}, }, ); - return null; + return; } - this.setState(toState, event); - return toState; + if (await this.callFunction(transitionConfig.condition) === false) { + return; + } + + transitionConfig.target ??= fromState; + await this.setState(transitionConfig.target, event); + } + + protected async execActions(actions?: MaybeArray<() => MaybePromise>): Promise { + if (actions == null) return; + + try { + if (!Array.isArray(actions)) { + await this.callFunction(actions); + return; + } + + // else + for (const action of actions) { + await this.callFunction(action); + } + } + catch (error) { + this._logger.accident('execActions', 'action_error', 'Error in executing actions', error); + } + } + + protected callFunction(fn?: () => T): T | void { + if (typeof fn !== 'function') return; + return fn(); } } diff --git a/core/fsm/src/type.ts b/core/fsm/src/type.ts new file mode 100644 index 000000000..352aa5e26 --- /dev/null +++ b/core/fsm/src/type.ts @@ -0,0 +1,79 @@ +import type {MaybeArray, MaybePromise, StringifyableRecord} from '@alwatr/type'; + + +export interface FsmConfig { + /** + * Machine ID (It is used in the state change signal identifier, so it must be unique). + */ + id: string; + + /** + * Initial state. + */ + initial: TState; + + /** + * Initial context. + */ + context: TContext; + + /** + * Define state list + */ + stateRecord: { + [S in TState | '$all']: { + /** + * On state exit actions + */ + exit?: MaybeArray<() => MaybePromise>; + + /** + * On state entry actions + */ + entry?: MaybeArray<() => MaybePromise>; + + /** + * An object mapping eventId to state. + * + * Example: + * + * ```ts + * stateRecord: { + * on: { + * TIMER: { + * target: 'green', + * condition: () => car.gas > 0, + * actions: () => car.go(), + * } + * } + * } + * ``` + */ + on: { + [E in TEventId]?: TransitionConfig; + }; + }; + }; +} + +export interface StateContext { + [T: string]: string | undefined; + /** + * Current state + */ + target: TState; + /** + * Last state + */ + from: TState; + /** + * Transition event + */ + by: TEventId | 'INIT'; +} + +export interface TransitionConfig { + target?: TState; + condition?: () => MaybePromise; + actions?: MaybeArray<() => MaybePromise>; +} From 592fc8dd586255e719a31785d3989a348f63cce8 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 9 Mar 2023 00:51:12 +0330 Subject: [PATCH 04/85] feat(element/fsm): rewrite state machine for lit --- .../finite-state-machine.ts | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/ui/element/src/reactive-controllers/finite-state-machine.ts b/ui/element/src/reactive-controllers/finite-state-machine.ts index ed6d44eda..1fc9389f2 100644 --- a/ui/element/src/reactive-controllers/finite-state-machine.ts +++ b/ui/element/src/reactive-controllers/finite-state-machine.ts @@ -1,41 +1,26 @@ -import {FiniteStateMachine, type StateContext, type MachineConfig} from '@alwatr/fsm'; +import {FiniteStateMachine, type FsmConfig} from '@alwatr/fsm'; -import {nothing, ReactiveController} from '../lit.js'; +import {nothing, type ReactiveController} from '../lit.js'; import type {LoggerMixinInterface} from '../mixins/logging.js'; import type {StringifyableRecord} from '@alwatr/type'; -export declare class AlwatrElementHostController< - TState extends string, - TEventId extends string -> extends LoggerMixinInterface { - stateUpdate(state: StateContext): void; -} - export class FiniteStateMachineController< TState extends string, TEventId extends string, TContext extends StringifyableRecord - > - extends FiniteStateMachine - implements ReactiveController { + > extends FiniteStateMachine implements ReactiveController { constructor( - private _host: AlwatrElementHostController, - config: Readonly>, + private _host: LoggerMixinInterface, + config: Readonly>, ) { super(config); - this._logger.logMethodArgs('constructor', config); this._host.addController(this); } - protected override setState(to: TState, by: TEventId | 'INIT' ): void { - super.setState(to, by); - this._host.stateUpdate.call(this._host, this.state); - } - render(states: {[P in TState]?: (() => unknown) | TState}): unknown { - this._logger.logMethodArgs('render', this.state.to); - let renderFn = states[this.state.to]; + this._logger.logMethodArgs('render', this.state.target); + let renderFn = states[this.state.target]; if (typeof renderFn === 'string') { renderFn = states[renderFn]; } @@ -47,6 +32,11 @@ export class FiniteStateMachineController< } hostUpdate(): void { - this._host.setAttribute('state', this.state.to); + this._host.setAttribute('state', this.state.target); + } + + protected override callFunction(fn?: () => T): T | void { + if (typeof fn !== 'function') return; + return fn.call(this._host); } } From 136b563220310d8ec637b4e776c48c1d3bb62ecc Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 9 Mar 2023 00:52:07 +0330 Subject: [PATCH 05/85] feat(demo/fsm): new demo for state machine --- demo/finite-state-machine/light-machine.ts | 52 +++++++++++++++++----- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/demo/finite-state-machine/light-machine.ts b/demo/finite-state-machine/light-machine.ts index 66b1f6af3..1acec657c 100644 --- a/demo/finite-state-machine/light-machine.ts +++ b/demo/finite-state-machine/light-machine.ts @@ -7,42 +7,70 @@ const lightMachine = new FiniteStateMachine({ a: 0, b: 0, }, - states: { + stateRecord: { $all: { + entry: (): void => console.log('$all entry called'), + exit: (): void => console.log('$all exit called'), on: { - POWER_LOST: 'flashingRed', + POWER_LOST: { + target: 'flashingRed', + actions: (): void => console.log('$all.POWER_LOST actions called'), + }, }, }, green: { + entry: (): void => console.log('green entry called'), + exit: (): void => console.log('green exit called'), on: { - TIMER: 'yellow', + TIMER: { + target: 'yellow', + actions: (): void => console.log('green.TIMER actions called'), + }, }, }, yellow: { + entry: (): void => console.log('yellow entry called'), + exit: (): void => console.log('yellow exit called'), on: { - TIMER: 'red', + TIMER: { + target: 'red', + actions: (): void => console.log('yellow.TIMER actions called'), + }, }, }, red: { + entry: (): void => console.log('red entry called'), + exit: (): void => console.log('red exit called'), on: { - TIMER: 'green', + TIMER: { + target: 'green', + actions: (): void => console.log('red.TIMER actions called'), + }, }, }, flashingRed: { + entry: (): void => console.log('flashingRed entry called'), + exit: (): void => console.log('flashingRed exit called'), on: { - POWER_BACK: 'green', + POWER_BACK: { + target: 'green', + actions: (): void => console.log('flashingRed.POWER_BACK actions called'), + }, }, }, }, }); + lightMachine.signal.subscribe((state) => { console.log('****\nstate: %s, context: %s\n****', state, lightMachine.context); }, {receivePrevious: 'No'}); -lightMachine.transition('TIMER', {a: 1}); -lightMachine.transition('TIMER', {b: 2}); -lightMachine.transition('TIMER'); -lightMachine.transition('POWER_LOST', {a: 4}); -lightMachine.transition('TIMER', {a: 5, b: 5}); -lightMachine.transition('POWER_BACK', {a: 6}); +console.log('start'); + +await lightMachine.transition('TIMER', {a: 1}); +await lightMachine.transition('TIMER', {b: 2}); +await lightMachine.transition('TIMER'); +await lightMachine.transition('POWER_LOST', {a: 4}); +await lightMachine.transition('TIMER', {a: 5, b: 5}); +await lightMachine.transition('POWER_BACK', {a: 6}); From fe4427ef62c2fd1680bcb3212feed681add4c2d4 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 9 Mar 2023 00:52:39 +0330 Subject: [PATCH 06/85] fix(element/fsm): compatible old mixins --- ui/element/src/mixins/state-machine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/element/src/mixins/state-machine.ts b/ui/element/src/mixins/state-machine.ts index c94b8cfb4..82b172d94 100644 --- a/ui/element/src/mixins/state-machine.ts +++ b/ui/element/src/mixins/state-machine.ts @@ -45,7 +45,7 @@ export function StateMachineMixin, T * Subscribe to this.stateMachine.signal event. */ protected stateUpdated(state: TMachine['state']): void { - this.setAttribute('state', state.to); + this.setAttribute('state', state.target); this.requestUpdate(); } From 8694cd00ed3f67fc5b5d53ad002308173d9164fd Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 9 Mar 2023 00:54:07 +0330 Subject: [PATCH 07/85] refactor(com-pwa): compatible with new fsm --- .../src/manager/controller/new-order.ts | 10 +- .../src/manager/controller/order-detail.ts | 4 +- .../src/manager/controller/order-list.ts | 224 ++++++++---------- .../src/manager/controller/order-tracking.ts | 4 +- uniquely/com-pwa/src/ui/alwatr-pwa.ts | 16 +- uniquely/com-pwa/src/ui/page/new-order.ts | 2 +- uniquely/com-pwa/src/ui/page/order-detail.ts | 2 +- uniquely/com-pwa/src/ui/page/order-list.ts | 120 +++++++--- .../com-pwa/src/ui/page/order-tracking.ts | 2 +- 9 files changed, 205 insertions(+), 179 deletions(-) diff --git a/uniquely/com-pwa/src/manager/controller/new-order.ts b/uniquely/com-pwa/src/manager/controller/new-order.ts index 3b72b0c89..751e39adb 100644 --- a/uniquely/com-pwa/src/manager/controller/new-order.ts +++ b/uniquely/com-pwa/src/manager/controller/new-order.ts @@ -31,7 +31,7 @@ export const pageNewOrderStateMachine = new FiniteStateMachine({ priceStorage: | null>null, finalPriceStorage: | null>null, }, - states: { + stateRecord: { $all: { on: { CONNECTED: '$self', @@ -214,19 +214,19 @@ pageNewOrderStateMachine.signal.subscribe(async (state) => { pageNewOrderStateMachine.signal.subscribe(async (state) => { localStorage.setItem('draft-order-x2', JSON.stringify(pageNewOrderStateMachine.context.order)); - if (state.to != 'shippingForm' && state.to != state.from) { + if (state.target != 'shippingForm' && state.target != state.from) { scrollToTopCommand.request({}); } if ( - state.to === 'edit' && + state.target === 'edit' && state.from != 'selectProduct' && !pageNewOrderStateMachine.context.order?.itemList?.length ) { pageNewOrderStateMachine.transition('SELECT_PRODUCT'); } - else if (state.to === 'edit' || state.to === 'review') { + else if (state.target === 'edit' || state.target === 'review') { const order = pageNewOrderStateMachine.context.order; let totalPrice = 0; let finalTotalPrice = 0; @@ -238,7 +238,7 @@ pageNewOrderStateMachine.signal.subscribe(async (state) => { order.finalTotalPrice = Math.round(finalTotalPrice); } - if (state.to === 'review') { + if (state.target === 'review') { try { validator(orderInfoSchema, pageNewOrderStateMachine.context.order, true); } diff --git a/uniquely/com-pwa/src/manager/controller/order-detail.ts b/uniquely/com-pwa/src/manager/controller/order-detail.ts index 2fbe53e81..b6ca1edc7 100644 --- a/uniquely/com-pwa/src/manager/controller/order-detail.ts +++ b/uniquely/com-pwa/src/manager/controller/order-detail.ts @@ -17,7 +17,7 @@ export const pageOrderDetailStateMachine = new FiniteStateMachine({ orderStorage: | null>null, productStorage: | null> null, }, - states: { + stateRecord: { $all: { on: { SHOW_DETAIL: '$self', @@ -105,7 +105,7 @@ pageOrderDetailStateMachine.signal.subscribe(async (state) => { } } - if (state.to === 'loading') { + if (state.target === 'loading') { if ( pageOrderDetailStateMachine.context.orderStorage != null && pageOrderDetailStateMachine.context.productStorage != null diff --git a/uniquely/com-pwa/src/manager/controller/order-list.ts b/uniquely/com-pwa/src/manager/controller/order-list.ts index a6d080903..71e043447 100644 --- a/uniquely/com-pwa/src/manager/controller/order-list.ts +++ b/uniquely/com-pwa/src/manager/controller/order-list.ts @@ -1,138 +1,106 @@ -import {FiniteStateMachine} from '@alwatr/fsm'; -import {redirect} from '@alwatr/router'; -import {eventListener} from '@alwatr/signal'; +// import {FiniteStateMachine} from '@alwatr/fsm'; +// import {redirect} from '@alwatr/router'; +// import {eventListener} from '@alwatr/signal'; -import {fetchOrderStorage} from '../context-provider/order-storage.js'; -import {orderStorageContextConsumer, topAppBarContextProvider} from '../context.js'; +// import {fetchOrderStorage} from '../context-provider/order-storage.js'; +// import {orderStorageContextConsumer, topAppBarContextProvider} from '../context.js'; -import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; -import type {Order} from '@alwatr/type/customer-order-management.js'; +// import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; +// import type {Order} from '@alwatr/type/customer-order-management.js'; -export const pageOrderListStateMachine = new FiniteStateMachine({ - id: 'page-order-list', - initial: 'unresolved', - context: { - orderStorage: | null>null, - }, - states: { - $all: { - on: { - CONNECTED: '$self', - CONTEXT_LOADED: '$self', - }, - }, - unresolved: { - on: { - IMPORT: 'resolving', - }, - }, - resolving: { - on: { - CONNECTED: 'loading', - }, - }, - loading: { - on: { - CONTEXT_LOADED: 'list', - }, - }, - list: { - on: { - REQUEST_UPDATE: 'reloading', - }, - }, - reloading: { - on: { - CONTEXT_LOADED: 'list', - }, - }, - }, -} as const); +// export const pageOrderListStateMachine = new FiniteStateMachine({ +// id: 'page-order-list', +// initial: 'pending', +// context: { +// orderStorage: | null>null, +// }, +// states: { +// $all: { +// on: { +// }, +// }, +// pending: { +// on: { +// CONTEXT_LOADED: 'list', +// }, +// }, +// list: { +// on: { +// REQUEST_UPDATE: 'reloading', +// }, +// }, +// reloading: { +// on: { +// CONTEXT_LOADED: 'list', +// }, +// }, +// }, +// } as const); -export const buttons = { - backToHome: { - icon: 'arrow-back-outline', - flipRtl: true, - clickSignalId: 'back_to_home_click_event', - }, - reload: { - icon: 'reload-outline', - // flipRtl: true, - clickSignalId: pageOrderListStateMachine.config.id + '_reload_click_event', - }, - newOrder: { - icon: 'add-outline', - clickSignalId: pageOrderListStateMachine.config.id + '_new_order_click_event', - }, - orderDetail: { - clickSignalId: pageOrderListStateMachine.config.id + '_order_detail_click_event', - }, -} as const; +// export const buttons = { +// backToHome: { +// icon: 'arrow-back-outline', +// flipRtl: true, +// clickSignalId: 'back_to_home_click_event', +// }, +// reload: { +// icon: 'reload-outline', +// // flipRtl: true, +// clickSignalId: pageOrderListStateMachine.config.id + '_reload_click_event', +// }, +// newOrder: { +// icon: 'add-outline', +// clickSignalId: pageOrderListStateMachine.config.id + '_new_order_click_event', +// }, +// orderDetail: { +// clickSignalId: pageOrderListStateMachine.config.id + '_order_detail_click_event', +// }, +// } as const; -pageOrderListStateMachine.signal.subscribe(async (state) => { - // logger.logMethodArgs('pageOrderListFsm.changed', state); - switch (state.by) { - case 'IMPORT': { - // just in unresolved - topAppBarContextProvider.setValue({ - headlineKey: 'loading', - }); - if (orderStorageContextConsumer.getValue() == null) { - fetchOrderStorage(); - } - break; - } +// pageOrderListStateMachine.signal.subscribe(async (state) => { +// // logger.logMethodArgs('pageOrderListFsm.changed', state); +// switch (state.by) { +// case 'REQUEST_UPDATE': { +// // eslint-disable-next-line @typescript-eslint/no-non-null-assertion +// topAppBarContextProvider.setValue({ +// headlineKey: 'loading', +// startIcon: buttons.backToHome, +// endIconList: [buttons.newOrder, buttons.reload], +// }); +// await fetchOrderStorage(); +// topAppBarContextProvider.setValue({ +// headlineKey: 'page_order_list_headline', +// startIcon: buttons.backToHome, +// endIconList: [buttons.newOrder, buttons.reload], +// }); +// pageOrderListStateMachine.transition('CONTEXT_LOADED'); +// break; +// } +// } - case 'CONNECTED': { - topAppBarContextProvider.setValue({ - headlineKey: 'page_order_list_headline', - startIcon: buttons.backToHome, - endIconList: [buttons.newOrder, buttons.reload], - }); - break; - } +// if (state.to === 'loading') { +// if (pageOrderListStateMachine.context.orderStorage != null) { +// pageOrderListStateMachine.transition('CONTEXT_LOADED'); +// } +// } +// }); - case 'REQUEST_UPDATE': { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - topAppBarContextProvider.setValue({ - headlineKey: 'loading', - startIcon: buttons.backToHome, - endIconList: [buttons.newOrder, buttons.reload], - }); - await fetchOrderStorage(); - topAppBarContextProvider.setValue({ - headlineKey: 'page_order_list_headline', - startIcon: buttons.backToHome, - endIconList: [buttons.newOrder, buttons.reload], - }); - pageOrderListStateMachine.transition('CONTEXT_LOADED'); - break; - } - } +// orderStorageContextConsumer.subscribe((orderStorage) => { +// pageOrderListStateMachine.transition('CONTEXT_LOADED', {orderStorage}); +// }); - if (state.to === 'loading') { - if (pageOrderListStateMachine.context.orderStorage != null) { - pageOrderListStateMachine.transition('CONTEXT_LOADED'); - } - } -}); +// eventListener.subscribe(buttons.reload.clickSignalId, () => { +// pageOrderListStateMachine.transition('REQUEST_UPDATE'); +// }); -orderStorageContextConsumer.subscribe((orderStorage) => { - pageOrderListStateMachine.transition('CONTEXT_LOADED', {orderStorage}); -}); +// eventListener.subscribe(buttons.newOrder.clickSignalId, () => { +// redirect({ +// sectionList: ['new-order'], +// }); +// }); -eventListener.subscribe(buttons.reload.clickSignalId, () => { - pageOrderListStateMachine.transition('REQUEST_UPDATE'); -}); - -eventListener.subscribe(buttons.newOrder.clickSignalId, () => { - redirect({ - sectionList: ['new-order'], - }); -}); - -eventListener.subscribe>(buttons.orderDetail.clickSignalId, (event) => { - redirect({ - sectionList: ['order-detail', event.detail.id], - }); -}); +// eventListener.subscribe>(buttons.orderDetail.clickSignalId, (event) => { +// redirect({ +// sectionList: ['order-detail', event.detail.id], +// }); +// }); diff --git a/uniquely/com-pwa/src/manager/controller/order-tracking.ts b/uniquely/com-pwa/src/manager/controller/order-tracking.ts index 18a72dded..10da45bf7 100644 --- a/uniquely/com-pwa/src/manager/controller/order-tracking.ts +++ b/uniquely/com-pwa/src/manager/controller/order-tracking.ts @@ -14,7 +14,7 @@ export const pageOrderTrackingFsm = new FiniteStateMachine({ orderId: null, orderStorage: | null>null, }, - states: { + stateRecord: { $all: { on: { SHOW_TRACKING: '$self', @@ -84,7 +84,7 @@ pageOrderTrackingFsm.signal.subscribe(async (state) => { } } - if (state.to === 'loading') { + if (state.target === 'loading') { if (pageOrderTrackingFsm.context.orderStorage != null) { pageOrderTrackingFsm.transition('CONTEXT_LOADED'); } diff --git a/uniquely/com-pwa/src/ui/alwatr-pwa.ts b/uniquely/com-pwa/src/ui/alwatr-pwa.ts index 7f6ae5a48..94bc7004a 100644 --- a/uniquely/com-pwa/src/ui/alwatr-pwa.ts +++ b/uniquely/com-pwa/src/ui/alwatr-pwa.ts @@ -7,9 +7,9 @@ import '@alwatr/ui-kit/style/theme/palette-270.css'; import './page/home.js'; // for perf import './stuff/app-footer.js'; +import {topAppBarContextProvider} from '../manager/context.js'; import {pageNewOrderStateMachine} from '../manager/controller/new-order.js'; import {pageOrderDetailStateMachine} from '../manager/controller/order-detail.js'; -import {pageOrderListStateMachine} from '../manager/controller/order-list.js'; import {pageOrderTrackingFsm} from '../manager/controller/order-tracking.js'; import type {RoutesConfig} from '@alwatr/router'; @@ -37,14 +37,14 @@ class AlwatrPwa extends AlwatrPwaElement { return html`...`; }, 'order-list': () => { - if (pageOrderListStateMachine.state.to === 'unresolved') { - pageOrderListStateMachine.transition('IMPORT'); - import('./page/order-list.js'); - } + topAppBarContextProvider.setValue({ + headlineKey: 'loading', + }); + import('./page/order-list.js'); return html`...`; }, 'order-detail': (routeContext) => { - if (pageOrderDetailStateMachine.state.to === 'unresolved') { + if (pageOrderDetailStateMachine.state.target === 'unresolved') { pageOrderDetailStateMachine.transition('IMPORT'); import('./page/order-detail.js'); } @@ -52,7 +52,7 @@ class AlwatrPwa extends AlwatrPwaElement { return html`...`; }, 'order-tracking': (routeContext) => { - if (pageOrderTrackingFsm.state.to === 'unresolved') { + if (pageOrderTrackingFsm.state.target === 'unresolved') { pageOrderTrackingFsm.transition('IMPORT'); import('./page/order-tracking.js'); } @@ -60,7 +60,7 @@ class AlwatrPwa extends AlwatrPwaElement { return html`...`; }, 'new-order': () => { - if (pageNewOrderStateMachine.state.to === 'unresolved') { + if (pageNewOrderStateMachine.state.target === 'unresolved') { pageNewOrderStateMachine.transition('IMPORT'); import('./page/new-order.js'); } diff --git a/uniquely/com-pwa/src/ui/page/new-order.ts b/uniquely/com-pwa/src/ui/page/new-order.ts index 390158d37..cfb37fe68 100644 --- a/uniquely/com-pwa/src/ui/page/new-order.ts +++ b/uniquely/com-pwa/src/ui/page/new-order.ts @@ -21,7 +21,7 @@ export class AlwatrPageNewOrder extends StateMachineMixin( ) { protected override render(): unknown { this._logger.logMethod('render'); - return this[`render_state_${this.stateMachine.state.to}`]?.(); + return this[`render_state_${this.stateMachine.state.target}`]?.(); } protected render_state_loading(): unknown { diff --git a/uniquely/com-pwa/src/ui/page/order-detail.ts b/uniquely/com-pwa/src/ui/page/order-detail.ts index a7975d8c9..109587db8 100644 --- a/uniquely/com-pwa/src/ui/page/order-detail.ts +++ b/uniquely/com-pwa/src/ui/page/order-detail.ts @@ -23,7 +23,7 @@ export class AlwatrPageOrderDetail extends StateMachineMixin( ) { protected override render(): unknown { this._logger.logMethod('render'); - return this[`render_state_${this.stateMachine.state.to}`]?.(); + return this[`render_state_${this.stateMachine.state.target}`]?.(); } protected render_state_loading(): unknown { diff --git a/uniquely/com-pwa/src/ui/page/order-list.ts b/uniquely/com-pwa/src/ui/page/order-list.ts index 6e7f13fc0..793c9543a 100644 --- a/uniquely/com-pwa/src/ui/page/order-list.ts +++ b/uniquely/com-pwa/src/ui/page/order-list.ts @@ -6,29 +6,50 @@ import { SignalMixin, AlwatrBaseElement, UnresolvedMixin, - StateMachineMixin, + FiniteStateMachineController, } from '@alwatr/element'; import {message} from '@alwatr/i18n'; +import {Order} from '@alwatr/type/src/customer-order-management.js'; import '@alwatr/ui-kit/button/button.js'; import {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; -import {pageOrderListStateMachine, buttons} from '../../manager/controller/order-list.js'; +import {fetchOrderStorage} from '../../manager/context-provider/order-storage.js'; +import {orderStorageContextConsumer, topAppBarContextProvider} from '../../manager/context.js'; import '../stuff/order-list.js'; +import type {AlwatrDocumentStorage} from '@alwatr/type'; + declare global { interface HTMLElementTagNameMap { 'alwatr-page-order-list': AlwatrPageOrderList; } } +export const buttons = { + backToHome: { + icon: 'arrow-back-outline', + flipRtl: true, + clickSignalId: 'back_to_home_click_event', + }, + reload: { + icon: 'reload-outline', + // flipRtl: true, + clickSignalId: 'config.id' + '_reload_click_event', + }, + newOrder: { + icon: 'add-outline', + clickSignalId: 'config.id' + '_new_order_click_event', + }, + orderDetail: { + clickSignalId: 'config.id' + '_order_detail_click_event', + }, +} as const; + /** * List of all orders. */ @customElement('alwatr-page-order-list') -export class AlwatrPageOrderList extends StateMachineMixin( - pageOrderListStateMachine, - UnresolvedMixin(LocalizeMixin(SignalMixin(AlwatrBaseElement))), -) { +export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMixin(AlwatrBaseElement))) { static override styles = css` :host { display: block; @@ -46,37 +67,74 @@ export class AlwatrPageOrderList extends StateMachineMixin( } `; - override render(): unknown { - this._logger.logMethod('render'); - return this[`render_state_${this.stateMachine.state.to}`]?.(); - } + private _stateMachine = new FiniteStateMachineController(this, { + id: 'fsm-order-list-' + this.ali, + initial: 'pending', + context: { + orderStorage: | null>null, + }, + stateRecord: { + $all: { + on: { + }, + }, + pending: { + on: { + CONTEXT_LOADED: 'list', + }, + }, + list: { + on: { + REQUEST_UPDATE: 'reloading', + }, + }, + reloading: { + on: { + CONTEXT_LOADED: 'list', + }, + }, + }, + } as const); - render_state_loading(): unknown { - this._logger.logMethod('render_state_loading'); - return this.render_part_message('loading', 'cloud-download-outline'); + stateUpdate(): void { + this.requestUpdate(); } - render_state_reloading(): unknown { - this._logger.logMethod('render_state_reloading'); - return this.render_state_list(); - } + override connectedCallback(): void { + super.connectedCallback(); - render_state_list(): unknown { - this._logger.logMethod('render_state_list'); - return html``; + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_list_headline', + startIcon: buttons.backToHome, + endIconList: [buttons.newOrder, buttons.reload], + }); + + if (orderStorageContextConsumer.getValue() == null) { + fetchOrderStorage(); + } } - protected render_part_message(key: string, icon: string): unknown { - this._logger.logMethod('render_part_message'); - const content: IconBoxContent = { - headline: message(key), - icon: icon, - tinted: 1, - }; + override render(): unknown { + this._logger.logMethod('render'); + this._stateMachine.state.target; + return this._stateMachine.render({ + 'pending': () => { + const content: IconBoxContent = { + tinted: 1, + icon: 'cloud-download-outline', + headline: message('loading'), + }; + return html``; + }, + + 'reloading': 'list', - return html``; + 'list': () => { + return html``; + }, + }); } } diff --git a/uniquely/com-pwa/src/ui/page/order-tracking.ts b/uniquely/com-pwa/src/ui/page/order-tracking.ts index c153ecd44..1b774cb50 100644 --- a/uniquely/com-pwa/src/ui/page/order-tracking.ts +++ b/uniquely/com-pwa/src/ui/page/order-tracking.ts @@ -53,7 +53,7 @@ export class AlwatrPageOrderTracking extends StateMachineMixin( protected override render(): unknown { this._logger.logMethod('render'); - return this[`render_state_${this.stateMachine.state.to}`]?.(); + return this[`render_state_${this.stateMachine.state.target}`]?.(); } protected render_state_loading(): unknown { From 9c4ebc35d4ec9f8482fd4860bd1179bd04b13a86 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 9 Mar 2023 14:33:33 +0330 Subject: [PATCH 08/85] refactor(com-pwa): fetchOrderStorage simple error toast --- .../com-pwa/src/manager/context-provider/order-storage.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/uniquely/com-pwa/src/manager/context-provider/order-storage.ts b/uniquely/com-pwa/src/manager/context-provider/order-storage.ts index 0d3adb25d..7be28dce3 100644 --- a/uniquely/com-pwa/src/manager/context-provider/order-storage.ts +++ b/uniquely/com-pwa/src/manager/context-provider/order-storage.ts @@ -28,16 +28,10 @@ export const fetchOrderStorage = async (): Promise => { ); } catch (err) { - // TODO: refactor logger.error('fetchOrderStorage', 'fetch_failed', err); await l18eReadyPromise; - const response = await snackbarSignalTrigger.requestWithResponse({ + await snackbarSignalTrigger.requestWithResponse({ messageKey: 'fetch_failed', - actionLabelKey: 'retry', - duration: orderStorageContextConsumer.getValue() == null ? -1 : 5_000, }); - if (response.actionButton) { - await fetchOrderStorage(); - } } }; From 1005db318ebef14ae6caf28a18fb0d928f6c4788 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 9 Mar 2023 14:34:53 +0330 Subject: [PATCH 09/85] feat(com-pwa/order-list): refactor with new fsm --- .../src/manager/controller/order-list.ts | 106 ------------------ uniquely/com-pwa/src/ui/page/order-list.ts | 89 ++++++++++++--- 2 files changed, 75 insertions(+), 120 deletions(-) delete mode 100644 uniquely/com-pwa/src/manager/controller/order-list.ts diff --git a/uniquely/com-pwa/src/manager/controller/order-list.ts b/uniquely/com-pwa/src/manager/controller/order-list.ts deleted file mode 100644 index 71e043447..000000000 --- a/uniquely/com-pwa/src/manager/controller/order-list.ts +++ /dev/null @@ -1,106 +0,0 @@ -// import {FiniteStateMachine} from '@alwatr/fsm'; -// import {redirect} from '@alwatr/router'; -// import {eventListener} from '@alwatr/signal'; - -// import {fetchOrderStorage} from '../context-provider/order-storage.js'; -// import {orderStorageContextConsumer, topAppBarContextProvider} from '../context.js'; - -// import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; -// import type {Order} from '@alwatr/type/customer-order-management.js'; - -// export const pageOrderListStateMachine = new FiniteStateMachine({ -// id: 'page-order-list', -// initial: 'pending', -// context: { -// orderStorage: | null>null, -// }, -// states: { -// $all: { -// on: { -// }, -// }, -// pending: { -// on: { -// CONTEXT_LOADED: 'list', -// }, -// }, -// list: { -// on: { -// REQUEST_UPDATE: 'reloading', -// }, -// }, -// reloading: { -// on: { -// CONTEXT_LOADED: 'list', -// }, -// }, -// }, -// } as const); - -// export const buttons = { -// backToHome: { -// icon: 'arrow-back-outline', -// flipRtl: true, -// clickSignalId: 'back_to_home_click_event', -// }, -// reload: { -// icon: 'reload-outline', -// // flipRtl: true, -// clickSignalId: pageOrderListStateMachine.config.id + '_reload_click_event', -// }, -// newOrder: { -// icon: 'add-outline', -// clickSignalId: pageOrderListStateMachine.config.id + '_new_order_click_event', -// }, -// orderDetail: { -// clickSignalId: pageOrderListStateMachine.config.id + '_order_detail_click_event', -// }, -// } as const; - -// pageOrderListStateMachine.signal.subscribe(async (state) => { -// // logger.logMethodArgs('pageOrderListFsm.changed', state); -// switch (state.by) { -// case 'REQUEST_UPDATE': { -// // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -// topAppBarContextProvider.setValue({ -// headlineKey: 'loading', -// startIcon: buttons.backToHome, -// endIconList: [buttons.newOrder, buttons.reload], -// }); -// await fetchOrderStorage(); -// topAppBarContextProvider.setValue({ -// headlineKey: 'page_order_list_headline', -// startIcon: buttons.backToHome, -// endIconList: [buttons.newOrder, buttons.reload], -// }); -// pageOrderListStateMachine.transition('CONTEXT_LOADED'); -// break; -// } -// } - -// if (state.to === 'loading') { -// if (pageOrderListStateMachine.context.orderStorage != null) { -// pageOrderListStateMachine.transition('CONTEXT_LOADED'); -// } -// } -// }); - -// orderStorageContextConsumer.subscribe((orderStorage) => { -// pageOrderListStateMachine.transition('CONTEXT_LOADED', {orderStorage}); -// }); - -// eventListener.subscribe(buttons.reload.clickSignalId, () => { -// pageOrderListStateMachine.transition('REQUEST_UPDATE'); -// }); - -// eventListener.subscribe(buttons.newOrder.clickSignalId, () => { -// redirect({ -// sectionList: ['new-order'], -// }); -// }); - -// eventListener.subscribe>(buttons.orderDetail.clickSignalId, (event) => { -// redirect({ -// sectionList: ['order-detail', event.detail.id], -// }); -// }); diff --git a/uniquely/com-pwa/src/ui/page/order-list.ts b/uniquely/com-pwa/src/ui/page/order-list.ts index 793c9543a..050aa0b4c 100644 --- a/uniquely/com-pwa/src/ui/page/order-list.ts +++ b/uniquely/com-pwa/src/ui/page/order-list.ts @@ -7,8 +7,11 @@ import { AlwatrBaseElement, UnresolvedMixin, FiniteStateMachineController, + state, } from '@alwatr/element'; import {message} from '@alwatr/i18n'; +import {redirect} from '@alwatr/router'; +import {eventListener} from '@alwatr/signal'; import {Order} from '@alwatr/type/src/customer-order-management.js'; import '@alwatr/ui-kit/button/button.js'; import {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; @@ -17,7 +20,7 @@ import {fetchOrderStorage} from '../../manager/context-provider/order-storage.js import {orderStorageContextConsumer, topAppBarContextProvider} from '../../manager/context.js'; import '../stuff/order-list.js'; -import type {AlwatrDocumentStorage} from '@alwatr/type'; +import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; declare global { interface HTMLElementTagNameMap { @@ -34,14 +37,14 @@ export const buttons = { reload: { icon: 'reload-outline', // flipRtl: true, - clickSignalId: 'config.id' + '_reload_click_event', + clickSignalId: 'order_list_reload_click_event', }, newOrder: { icon: 'add-outline', - clickSignalId: 'config.id' + '_new_order_click_event', + clickSignalId: 'order_list_new_order_click_event', }, orderDetail: { - clickSignalId: 'config.id' + '_order_detail_click_event', + clickSignalId: 'order_list_order_detail_click_event', }, } as const; @@ -75,30 +78,50 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix }, stateRecord: { $all: { + entry: () => { + this.gotState = this._stateMachine.state.target; + }, on: { }, }, pending: { + entry: () => { + if (orderStorageContextConsumer.getValue() == null) { + fetchOrderStorage(); + } + if (this._stateMachine.context.orderStorage != null) { + this._stateMachine.transition('LOADED_SUCCESS'); + } + }, on: { - CONTEXT_LOADED: 'list', + LOADED_SUCCESS: { + target: 'list', + }, }, }, list: { on: { - REQUEST_UPDATE: 'reloading', + REQUEST_UPDATE: { + target: 'reloading', + actions: this._requestUpdateAction, + }, }, }, reloading: { on: { - CONTEXT_LOADED: 'list', + LOADED_SUCCESS: { + target: 'list', + }, + // LOAD_FAILED: { + // target: 'list', + // }, }, }, }, } as const); - stateUpdate(): void { - this.requestUpdate(); - } + @state() + gotState = this._stateMachine.state.target; override connectedCallback(): void { super.connectedCallback(); @@ -109,14 +132,37 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix endIconList: [buttons.newOrder, buttons.reload], }); - if (orderStorageContextConsumer.getValue() == null) { - fetchOrderStorage(); - } + this._signalListenerList.push( + orderStorageContextConsumer.subscribe((orderStorage) => { + this._stateMachine.transition('LOADED_SUCCESS', {orderStorage}); + }), + ); + + this._signalListenerList.push( + eventListener.subscribe(buttons.reload.clickSignalId, () => { + this._stateMachine.transition('REQUEST_UPDATE'); + }), + ); + + this._signalListenerList.push( + eventListener.subscribe(buttons.newOrder.clickSignalId, () => { + redirect({ + sectionList: ['new-order'], + }); + }), + ); + + this._signalListenerList.push( + eventListener.subscribe>(buttons.orderDetail.clickSignalId, (event) => { + redirect({ + sectionList: ['order-detail', event.detail.id], + }); + }), + ); } override render(): unknown { this._logger.logMethod('render'); - this._stateMachine.state.target; return this._stateMachine.render({ 'pending': () => { const content: IconBoxContent = { @@ -137,4 +183,19 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix }, }); } + + private async _requestUpdateAction(): Promise { + topAppBarContextProvider.setValue({ + headlineKey: 'loading', + startIcon: buttons.backToHome, + endIconList: [buttons.newOrder, buttons.reload], + }); + await fetchOrderStorage(); + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_list_headline', + startIcon: buttons.backToHome, + endIconList: [buttons.newOrder, buttons.reload], + }); + this._stateMachine.transition('LOADED_SUCCESS'); + } } From f11d9070852763deabc61ea83f19a28ba68a726e Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 9 Mar 2023 15:39:10 +0330 Subject: [PATCH 10/85] chore(com-pwa): order-list signalRecord model --- uniquely/com-pwa/src/ui/page/order-list.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/uniquely/com-pwa/src/ui/page/order-list.ts b/uniquely/com-pwa/src/ui/page/order-list.ts index 050aa0b4c..b011a61c3 100644 --- a/uniquely/com-pwa/src/ui/page/order-list.ts +++ b/uniquely/com-pwa/src/ui/page/order-list.ts @@ -86,6 +86,7 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix }, pending: { entry: () => { + this._logger.logMethod('state.pending.entry'); if (orderStorageContextConsumer.getValue() == null) { fetchOrderStorage(); } @@ -118,6 +119,11 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix }, }, }, + // signalRecord: { + // 'order_list_reload': { + // translate: 'REQUEST_UPDATE' + // } + // }, } as const); @state() From a427ab8698aecf1a8c3f40365becc8ee22439a6c Mon Sep 17 00:00:00 2001 From: "S. Amir Mohammad Najafi" Date: Thu, 9 Mar 2023 17:37:20 +0330 Subject: [PATCH 11/85] refactor(com-pwa): new state machine for order detail --- .../src/manager/controller/order-detail.ts | 256 +++++++++--------- uniquely/com-pwa/src/ui/alwatr-pwa.ts | 14 +- uniquely/com-pwa/src/ui/page/order-detail.ts | 222 ++++++++++++--- 3 files changed, 321 insertions(+), 171 deletions(-) diff --git a/uniquely/com-pwa/src/manager/controller/order-detail.ts b/uniquely/com-pwa/src/manager/controller/order-detail.ts index b6ca1edc7..077561821 100644 --- a/uniquely/com-pwa/src/manager/controller/order-detail.ts +++ b/uniquely/com-pwa/src/manager/controller/order-detail.ts @@ -1,140 +1,140 @@ -import {FiniteStateMachine} from '@alwatr/fsm'; -import {redirect} from '@alwatr/router'; -import {eventListener} from '@alwatr/signal'; +// import {FiniteStateMachine} from '@alwatr/fsm'; +// import {redirect} from '@alwatr/router'; +// import {eventListener} from '@alwatr/signal'; -import {fetchOrderStorage} from '../context-provider/order-storage.js'; -import {fetchProductStorage} from '../context-provider/product-storage.js'; -import {orderStorageContextConsumer, productStorageContextConsumer, topAppBarContextProvider} from '../context.js'; +// import {fetchOrderStorage} from '../context-provider/order-storage.js'; +// import {fetchProductStorage} from '../context-provider/product-storage.js'; +// import {orderStorageContextConsumer, productStorageContextConsumer, topAppBarContextProvider} from '../context.js'; -import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; -import type {Order, Product} from '@alwatr/type/customer-order-management.js'; +// import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; +// import type {Order, Product} from '@alwatr/type/customer-order-management.js'; -export const pageOrderDetailStateMachine = new FiniteStateMachine({ - id: 'page_order_detail', - initial: 'unresolved', - context: { - orderId: null, - orderStorage: | null>null, - productStorage: | null> null, - }, - stateRecord: { - $all: { - on: { - SHOW_DETAIL: '$self', - CONNECTED: '$self', - CONTEXT_LOADED: '$self', - }, - }, - unresolved: { - on: { - IMPORT: 'resolving', - }, - }, - resolving: { - on: { - CONNECTED: 'loading', - }, - }, - loading: { - on: { - CONTEXT_LOADED: 'detail', - }, - }, - detail: { - on: { - REQUEST_UPDATE: 'reloading', - INVALID_ORDER: 'notFound', - }, - }, - reloading: { - on: { - CONTEXT_LOADED: 'detail', - }, - }, - notFound: { - on: { - CONTEXT_LOADED: 'detail', - }, - }, - }, -}); +// export const pageOrderDetailStateMachine = new FiniteStateMachine({ +// id: 'page_order_detail', +// initial: 'unresolved', +// context: { +// orderId: null, +// orderStorage: | null>null, +// productStorage: | null> null, +// }, +// stateRecord: { +// $all: { +// on: { +// SHOW_DETAIL: '$self', +// CONNECTED: '$self', +// CONTEXT_LOADED: '$self', +// }, +// }, +// unresolved: { +// on: { +// IMPORT: 'resolving', +// }, +// }, +// resolving: { +// on: { +// CONNECTED: 'loading', +// }, +// }, +// loading: { +// on: { +// CONTEXT_LOADED: 'detail', +// }, +// }, +// detail: { +// on: { +// REQUEST_UPDATE: 'reloading', +// INVALID_ORDER: 'notFound', +// }, +// }, +// reloading: { +// on: { +// CONTEXT_LOADED: 'detail', +// }, +// }, +// notFound: { +// on: { +// CONTEXT_LOADED: 'detail', +// }, +// }, +// }, +// }); -const buttons = { - backToOrderList: { - icon: 'arrow-back-outline', - flipRtl: true, - clickSignalId: pageOrderDetailStateMachine.config.id + '_back_to_order_list_click_event', - }, - reload: { - icon: 'reload-outline', - flipRtl: true, - clickSignalId: pageOrderDetailStateMachine.config.id + '_reload_click_event', - }, -} as const; +// const buttons = { +// backToOrderList: { +// icon: 'arrow-back-outline', +// flipRtl: true, +// clickSignalId: pageOrderDetailStateMachine.config.id + '_back_to_order_list_click_event', +// }, +// reload: { +// icon: 'reload-outline', +// flipRtl: true, +// clickSignalId: pageOrderDetailStateMachine.config.id + '_reload_click_event', +// }, +// } as const; -pageOrderDetailStateMachine.signal.subscribe(async (state) => { - // logger.logMethodArgs('pageOrderDetailFsm.changed', state); - switch (state.by) { - case 'IMPORT': { - // just in unresolved - topAppBarContextProvider.setValue({ - headlineKey: 'loading', - }); - if (productStorageContextConsumer.getValue() == null) { - fetchProductStorage(); - } - if (orderStorageContextConsumer.getValue() == null) { - fetchOrderStorage(); - } - break; - } +// pageOrderDetailStateMachine.signal.subscribe(async (state) => { +// logger.logMethodArgs('pageOrderDetailFsm.changed', state); +// switch (state.by) { +// case 'IMPORT': { +// // just in unresolved +// topAppBarContextProvider.setValue({ +// headlineKey: 'loading', +// }); +// if (productStorageContextConsumer.getValue() == null) { +// fetchProductStorage(); +// } +// if (orderStorageContextConsumer.getValue() == null) { +// fetchOrderStorage(); +// } +// break; +// } - case 'CONNECTED': { - topAppBarContextProvider.setValue({ - headlineKey: 'page_order_list_headline', - startIcon: buttons.backToOrderList, - endIconList: [buttons.reload], - }); - break; - } +// case 'CONNECTED': { +// topAppBarContextProvider.setValue({ +// headlineKey: 'page_order_list_headline', +// startIcon: buttons.backToOrderList, +// endIconList: [buttons.reload], +// }); +// break; +// } - case 'REQUEST_UPDATE': { - await fetchOrderStorage(); // if not changed signal not fired! - pageOrderDetailStateMachine.transition('CONTEXT_LOADED'); - break; - } - } +// case 'REQUEST_UPDATE': { +// await fetchOrderStorage(); // if not changed signal not fired! +// pageOrderDetailStateMachine.transition('CONTEXT_LOADED'); +// break; +// } +// } - if (state.target === 'loading') { - if ( - pageOrderDetailStateMachine.context.orderStorage != null && - pageOrderDetailStateMachine.context.productStorage != null - ) { - pageOrderDetailStateMachine.transition('CONTEXT_LOADED'); - } - } -}); +// if (state.target === 'loading') { +// if ( +// pageOrderDetailStateMachine.context.orderStorage != null && +// pageOrderDetailStateMachine.context.productStorage != null +// ) { +// pageOrderDetailStateMachine.transition('CONTEXT_LOADED'); +// } +// } +// }); -productStorageContextConsumer.subscribe((productStorage) => { - pageOrderDetailStateMachine.context.productStorage = productStorage; - if (pageOrderDetailStateMachine.context.orderStorage != null) { - pageOrderDetailStateMachine.transition('CONTEXT_LOADED'); - } -}); +// productStorageContextConsumer.subscribe((productStorage) => { +// pageOrderDetailStateMachine.context.productStorage = productStorage; +// if (pageOrderDetailStateMachine.context.orderStorage != null) { +// pageOrderDetailStateMachine.transition('CONTEXT_LOADED'); +// } +// }); -orderStorageContextConsumer.subscribe((orderStorage) => { - pageOrderDetailStateMachine.context.orderStorage = orderStorage; - if (pageOrderDetailStateMachine.context.productStorage != null) { - pageOrderDetailStateMachine.transition('CONTEXT_LOADED'); - } -}); +// orderStorageContextConsumer.subscribe((orderStorage) => { +// pageOrderDetailStateMachine.context.orderStorage = orderStorage; +// if (pageOrderDetailStateMachine.context.productStorage != null) { +// pageOrderDetailStateMachine.transition('CONTEXT_LOADED'); +// } +// }); -eventListener.subscribe(buttons.backToOrderList.clickSignalId, () => { - redirect({ - sectionList: ['order-list'], - }); -}); +// eventListener.subscribe(buttons.backToOrderList.clickSignalId, () => { +// redirect({ +// sectionList: ['order-list'], +// }); +// }); -eventListener.subscribe(buttons.reload.clickSignalId, () => { - pageOrderDetailStateMachine.transition('REQUEST_UPDATE'); -}); +// eventListener.subscribe(buttons.reload.clickSignalId, () => { +// pageOrderDetailStateMachine.transition('REQUEST_UPDATE'); +// }); diff --git a/uniquely/com-pwa/src/ui/alwatr-pwa.ts b/uniquely/com-pwa/src/ui/alwatr-pwa.ts index 94bc7004a..ce97e565d 100644 --- a/uniquely/com-pwa/src/ui/alwatr-pwa.ts +++ b/uniquely/com-pwa/src/ui/alwatr-pwa.ts @@ -9,7 +9,6 @@ import './page/home.js'; // for perf import './stuff/app-footer.js'; import {topAppBarContextProvider} from '../manager/context.js'; import {pageNewOrderStateMachine} from '../manager/controller/new-order.js'; -import {pageOrderDetailStateMachine} from '../manager/controller/order-detail.js'; import {pageOrderTrackingFsm} from '../manager/controller/order-tracking.js'; import type {RoutesConfig} from '@alwatr/router'; @@ -44,12 +43,13 @@ class AlwatrPwa extends AlwatrPwaElement { return html`...`; }, 'order-detail': (routeContext) => { - if (pageOrderDetailStateMachine.state.target === 'unresolved') { - pageOrderDetailStateMachine.transition('IMPORT'); - import('./page/order-detail.js'); - } - pageOrderDetailStateMachine.transition('SHOW_DETAIL', {orderId: +routeContext.sectionList[1]}); - return html`...`; + topAppBarContextProvider.setValue({ + headlineKey: 'loading', + }); + import('./page/order-detail.js'); + return html`...`; }, 'order-tracking': (routeContext) => { if (pageOrderTrackingFsm.state.target === 'unresolved') { diff --git a/uniquely/com-pwa/src/ui/page/order-detail.ts b/uniquely/com-pwa/src/ui/page/order-detail.ts index 109587db8..103c338c1 100644 --- a/uniquely/com-pwa/src/ui/page/order-detail.ts +++ b/uniquely/com-pwa/src/ui/page/order-detail.ts @@ -1,10 +1,22 @@ import { customElement, - StateMachineMixin, + FiniteStateMachineController, + html, + property, + state, UnresolvedMixin, } from '@alwatr/element'; +import {message} from '@alwatr/i18n'; +import {topAppBarContextProvider} from '@alwatr/pwa-helper/src/context.js'; +import {redirect} from '@alwatr/router'; +import {eventListener} from '@alwatr/signal'; +import {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; +import {Order, Product} from '@alwatr/type/src/customer-order-management.js'; +import {IconBoxContent} from '@alwatr/ui-kit/src/card/icon-box.js'; -import {pageOrderDetailStateMachine} from '../../manager/controller/order-detail.js'; +import {fetchOrderStorage} from '../../manager/context-provider/order-storage.js'; +import {fetchProductStorage} from '../../manager/context-provider/product-storage.js'; +import {orderStorageContextConsumer, productStorageContextConsumer} from '../../manager/context.js'; import {AlwatrOrderDetailBase} from '../stuff/order-detail-base.js'; declare global { @@ -13,49 +25,187 @@ declare global { } } +const buttons = { + backToOrderList: { + icon: 'arrow-back-outline', + flipRtl: true, + clickSignalId: 'order_detail_back_to_order_list_event', + }, + reload: { + icon: 'reload-outline', + flipRtl: true, + clickSignalId: 'order_detail_reload_event', + }, +} as const; + /** - * Alwatr Customer Order Management Order Form Page + * Alwatr Customer Order Management Order Detail Page. */ @customElement('alwatr-page-order-detail') -export class AlwatrPageOrderDetail extends StateMachineMixin( - pageOrderDetailStateMachine, - UnresolvedMixin(AlwatrOrderDetailBase), -) { - protected override render(): unknown { - this._logger.logMethod('render'); - return this[`render_state_${this.stateMachine.state.target}`]?.(); - } +export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase) { + private _stateMachine = new FiniteStateMachineController(this, { + id: 'fsm-order-detail-' + this.ali, + initial: 'pending', + context: { + orderId: null, + orderStorage: | null> null, + productStorage: | null> null, + }, + stateRecord: { + '$all': { + entry: (): void => { + this.gotState = this._stateMachine.state.target; + }, + on: { + }, + }, + 'pending': { + entry: (): void => { + if (productStorageContextConsumer.getValue() == null) { + fetchProductStorage(); + } + if (orderStorageContextConsumer.getValue() == null) { + fetchOrderStorage(); + } + }, + on: { + LOADED_SUCCESS: { + target: 'detail', + condition: () => { + if (this._stateMachine.context.orderStorage == null || + this._stateMachine.context.productStorage == null + ) return false; + return true; + }, + actions: () => { + if (this._stateMachine.context.orderId == null || + this._stateMachine.context.orderStorage?.data[this._stateMachine.context.orderId] == null + ) this._stateMachine.transition('NOT_FOUND'); + }, + }, + }, + }, + 'detail': { + on: { + REQUEST_UPDATE: { + target: 'reloading', + actions: this._requestUpdateAction, + }, + NOT_FOUND: { + target: 'notFound', + }, + }, + }, + 'reloading': { + on: { + LOADED_SUCCESS: { + target: 'detail', + }, + // LOAD_FAILED: { + // target: 'detail', + // }, + }, + }, + 'notFound': { + on: {}, + }, + }} as const); + + @state() + gotState = this._stateMachine.state.target; - protected render_state_loading(): unknown { - this._logger.logMethod('render_state_loading'); - return this.render_part_message('loading', 'ellipsis-horizontal'); + @property({type: Number}) + get orderId(): number { + return this.orderId; } + set orderId(orderId: number) { + this.orderId = orderId; + this._stateMachine.transition('LOADED_SUCCESS', {orderId}); + } + + override connectedCallback(): void { + super.connectedCallback(); + + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_list_headline', + startIcon: buttons.backToOrderList, + endIconList: [buttons.reload], + }); + + this._signalListenerList.push( + productStorageContextConsumer.subscribe((productStorage) => { + this._stateMachine.transition('LOADED_SUCCESS', {productStorage}); + }), + ); - protected render_state_notFound(): unknown { - this._logger.logMethod('render_state_notFound'); - return this.render_part_message('page_order_detail_not_found', 'close'); + this._signalListenerList.push( + orderStorageContextConsumer.subscribe((orderStorage) => { + this._stateMachine.transition('LOADED_SUCCESS', {orderStorage}); + }), + ); + + this._signalListenerList.push( + eventListener.subscribe(buttons.backToOrderList.clickSignalId, () => { + redirect({sectionList: ['order-list']}); + }), + ); + + this._signalListenerList.push( + eventListener.subscribe(buttons.reload.clickSignalId, () => { + this._stateMachine.transition('REQUEST_UPDATE'); + }), + ); } - protected render_state_reloading(): unknown { - this._logger.logMethod('render_state_reloading'); - return this.render_state_detail(); + protected override render(): unknown { + this._logger.logMethod('render'); + + return this._stateMachine.render({ + 'pending': () => { + const content: IconBoxContent = { + headline: message('loading'), + icon: 'cloud-download-outline', + tinted: 1, + }; + return html``; + }, + + 'reloading': 'detail', + + 'detail': () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const order = this._stateMachine.context.orderStorage!.data[this._stateMachine.context.orderId!]; + return [ + this.render_part_status(order), + this.render_part_item_list(order.itemList, this._stateMachine.context.productStorage), + this.render_part_shipping_info(order.shippingInfo), + this.render_part_summary(order), + ]; + }, + + 'notFound': () => { + const content: IconBoxContent = { + headline: message('page_order_detail_not_found'), + icon: 'close', + tinted: 1, + }; + return html``; + }, + }); } - protected render_state_detail(): unknown { - this._logger.logMethod('render_state_detail'); - - // validate order id - const order = this.stateMachine.context.orderStorage?.data[this.stateMachine.context.orderId ?? ''] ?? null; - if (order === null) { - this.stateMachine.transition('INVALID_ORDER'); - return; - } - - return [ - this.render_part_status(order), - this.render_part_item_list(order.itemList, this.stateMachine.context.productStorage), - this.render_part_shipping_info(order.shippingInfo), - this.render_part_summary(order), - ]; + private async _requestUpdateAction(): Promise { + topAppBarContextProvider.setValue({ + headlineKey: 'loading', + startIcon: buttons.backToOrderList, + endIconList: [buttons.reload], + }); + await fetchOrderStorage(); + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_list_headline', + startIcon: buttons.backToOrderList, + endIconList: [buttons.reload], + }); + this._stateMachine.transition('LOADED_SUCCESS'); } } From 99f2d01dde7a8cb915cf910464e7527fdd2d8381 Mon Sep 17 00:00:00 2001 From: "S. Amir Mohammad Najafi" Date: Thu, 9 Mar 2023 17:42:20 +0330 Subject: [PATCH 12/85] fix(com-pwa/order-detail): import issue --- uniquely/com-pwa/src/ui/page/order-detail.ts | 24 ++++++++------------ uniquely/com-pwa/src/ui/page/order-list.ts | 2 +- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/uniquely/com-pwa/src/ui/page/order-detail.ts b/uniquely/com-pwa/src/ui/page/order-detail.ts index 103c338c1..1f8c3a6fd 100644 --- a/uniquely/com-pwa/src/ui/page/order-detail.ts +++ b/uniquely/com-pwa/src/ui/page/order-detail.ts @@ -7,18 +7,19 @@ import { UnresolvedMixin, } from '@alwatr/element'; import {message} from '@alwatr/i18n'; -import {topAppBarContextProvider} from '@alwatr/pwa-helper/src/context.js'; +import {topAppBarContextProvider} from '@alwatr/pwa-helper/context.js'; import {redirect} from '@alwatr/router'; import {eventListener} from '@alwatr/signal'; -import {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; -import {Order, Product} from '@alwatr/type/src/customer-order-management.js'; -import {IconBoxContent} from '@alwatr/ui-kit/src/card/icon-box.js'; import {fetchOrderStorage} from '../../manager/context-provider/order-storage.js'; import {fetchProductStorage} from '../../manager/context-provider/product-storage.js'; import {orderStorageContextConsumer, productStorageContextConsumer} from '../../manager/context.js'; import {AlwatrOrderDetailBase} from '../stuff/order-detail-base.js'; +import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; +import type {Order, Product} from '@alwatr/type/customer-order-management.js'; +import type {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; + declare global { interface HTMLElementTagNameMap { 'alwatr-page-order-detail': AlwatrPageOrderDetail; @@ -53,20 +54,15 @@ export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase }, stateRecord: { '$all': { - entry: (): void => { + entry: () => { this.gotState = this._stateMachine.state.target; }, - on: { - }, + on: {}, }, 'pending': { - entry: (): void => { - if (productStorageContextConsumer.getValue() == null) { - fetchProductStorage(); - } - if (orderStorageContextConsumer.getValue() == null) { - fetchOrderStorage(); - } + entry: () => { + if (productStorageContextConsumer.getValue() == null) fetchProductStorage(); + if (orderStorageContextConsumer.getValue() == null) fetchOrderStorage(); }, on: { LOADED_SUCCESS: { diff --git a/uniquely/com-pwa/src/ui/page/order-list.ts b/uniquely/com-pwa/src/ui/page/order-list.ts index b011a61c3..590ec051f 100644 --- a/uniquely/com-pwa/src/ui/page/order-list.ts +++ b/uniquely/com-pwa/src/ui/page/order-list.ts @@ -12,7 +12,7 @@ import { import {message} from '@alwatr/i18n'; import {redirect} from '@alwatr/router'; import {eventListener} from '@alwatr/signal'; -import {Order} from '@alwatr/type/src/customer-order-management.js'; +import {Order} from '@alwatr/type/customer-order-management.js'; import '@alwatr/ui-kit/button/button.js'; import {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; From 28b07acc0cc14562747da6a6cd6c97ed20ef404a Mon Sep 17 00:00:00 2001 From: "S. Amir Mohammad Najafi" Date: Thu, 9 Mar 2023 18:11:15 +0330 Subject: [PATCH 13/85] fix(com-pwa/order-detail): fetch product storage on request update --- uniquely/com-pwa/src/ui/page/order-detail.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/uniquely/com-pwa/src/ui/page/order-detail.ts b/uniquely/com-pwa/src/ui/page/order-detail.ts index 1f8c3a6fd..a51147ad0 100644 --- a/uniquely/com-pwa/src/ui/page/order-detail.ts +++ b/uniquely/com-pwa/src/ui/page/order-detail.ts @@ -197,6 +197,7 @@ export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase endIconList: [buttons.reload], }); await fetchOrderStorage(); + await fetchProductStorage(); topAppBarContextProvider.setValue({ headlineKey: 'page_order_list_headline', startIcon: buttons.backToOrderList, From eef7ef306b2e8d22ba6062f4a9ee5b2392bb0e9e Mon Sep 17 00:00:00 2001 From: "S. Amir Mohammad Najafi" Date: Thu, 9 Mar 2023 18:24:54 +0330 Subject: [PATCH 14/85] refactor(com-pwa): new state machine for order tracking --- .../src/manager/controller/order-tracking.ts | 184 ++++++++-------- uniquely/com-pwa/src/ui/alwatr-pwa.ts | 14 +- uniquely/com-pwa/src/ui/page/order-detail.ts | 1 - .../com-pwa/src/ui/page/order-tracking.ts | 203 +++++++++++++++--- 4 files changed, 267 insertions(+), 135 deletions(-) diff --git a/uniquely/com-pwa/src/manager/controller/order-tracking.ts b/uniquely/com-pwa/src/manager/controller/order-tracking.ts index 10da45bf7..dfbf7ef97 100644 --- a/uniquely/com-pwa/src/manager/controller/order-tracking.ts +++ b/uniquely/com-pwa/src/manager/controller/order-tracking.ts @@ -1,101 +1,101 @@ -import {FiniteStateMachine} from '@alwatr/fsm'; -import {eventListener} from '@alwatr/signal'; +// import {FiniteStateMachine} from '@alwatr/fsm'; +// import {eventListener} from '@alwatr/signal'; -import {fetchOrderStorage} from '../context-provider/order-storage.js'; -import {orderStorageContextConsumer, topAppBarContextProvider} from '../context.js'; +// import {fetchOrderStorage} from '../context-provider/order-storage.js'; +// import {orderStorageContextConsumer, topAppBarContextProvider} from '../context.js'; -import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; -import type {Order} from '@alwatr/type/customer-order-management.js'; +// import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; +// import type {Order} from '@alwatr/type/customer-order-management.js'; -export const pageOrderTrackingFsm = new FiniteStateMachine({ - id: 'page-order-tracking', - initial: 'unresolved', - context: { - orderId: null, - orderStorage: | null>null, - }, - stateRecord: { - $all: { - on: { - SHOW_TRACKING: '$self', - CONNECTED: '$self', - CONTEXT_LOADED: '$self', - }, - }, - unresolved: { - on: { - IMPORT: 'resolving', - }, - }, - resolving: { - on: { - CONNECTED: 'loading', - }, - }, - loading: { - on: { - CONTEXT_LOADED: 'tracking', - }, - }, - tracking: { - on: { - REQUEST_UPDATE: 'reloading', - INVALID_ORDER: 'notFound', - }, - }, - reloading: { - on: { - CONTEXT_LOADED: 'tracking', - }, - }, - notFound: { - on: { - CONTEXT_LOADED: 'tracking', - }, - }, - }, -}); +// export const pageOrderTrackingFsm = new FiniteStateMachine({ +// id: 'page-order-tracking', +// initial: 'unresolved', +// context: { +// orderId: null, +// orderStorage: | null>null, +// }, +// stateRecord: { +// $all: { +// on: { +// SHOW_TRACKING: '$self', +// CONNECTED: '$self', +// CONTEXT_LOADED: '$self', +// }, +// }, +// unresolved: { +// on: { +// IMPORT: 'resolving', +// }, +// }, +// resolving: { +// on: { +// CONNECTED: 'loading', +// }, +// }, +// loading: { +// on: { +// CONTEXT_LOADED: 'tracking', +// }, +// }, +// tracking: { +// on: { +// REQUEST_UPDATE: 'reloading', +// INVALID_ORDER: 'notFound', +// }, +// }, +// reloading: { +// on: { +// CONTEXT_LOADED: 'tracking', +// }, +// }, +// notFound: { +// on: { +// CONTEXT_LOADED: 'tracking', +// }, +// }, +// }, +// }); -pageOrderTrackingFsm.signal.subscribe(async (state) => { - // logger.logMethodArgs('pageOrderTrackingFsm.changed', state); - switch (state.by) { - case 'IMPORT': { - // just in unresolved - topAppBarContextProvider.setValue({ - headlineKey: 'loading', - }); - if (orderStorageContextConsumer.getValue() == null) { - fetchOrderStorage(); - } - break; - } +// pageOrderTrackingFsm.signal.subscribe(async (state) => { +// // logger.logMethodArgs('pageOrderTrackingFsm.changed', state); +// switch (state.by) { +// case 'IMPORT': { +// // just in unresolved +// topAppBarContextProvider.setValue({ +// headlineKey: 'loading', +// }); +// if (orderStorageContextConsumer.getValue() == null) { +// fetchOrderStorage(); +// } +// break; +// } - case 'CONNECTED': { - topAppBarContextProvider.setValue({ - headlineKey: 'page_order_tracking_headline', - }); - break; - } +// case 'CONNECTED': { +// topAppBarContextProvider.setValue({ +// headlineKey: 'page_order_tracking_headline', +// }); +// break; +// } - case 'REQUEST_UPDATE': { - await fetchOrderStorage(); // if not changed signal not fired! - pageOrderTrackingFsm.transition('CONTEXT_LOADED'); - break; - } - } +// case 'REQUEST_UPDATE': { +// await fetchOrderStorage(); // if not changed signal not fired! +// pageOrderTrackingFsm.transition('CONTEXT_LOADED'); +// break; +// } +// } - if (state.target === 'loading') { - if (pageOrderTrackingFsm.context.orderStorage != null) { - pageOrderTrackingFsm.transition('CONTEXT_LOADED'); - } - } -}); +// if (state.target === 'loading') { +// if (pageOrderTrackingFsm.context.orderStorage != null) { +// pageOrderTrackingFsm.transition('CONTEXT_LOADED'); +// } +// } +// }); -orderStorageContextConsumer.subscribe((orderStorage) => { - pageOrderTrackingFsm.context.orderStorage = orderStorage; - pageOrderTrackingFsm.transition('CONTEXT_LOADED'); -}); +// orderStorageContextConsumer.subscribe((orderStorage) => { +// pageOrderTrackingFsm.context.orderStorage = orderStorage; +// pageOrderTrackingFsm.transition('CONTEXT_LOADED'); +// }); -eventListener.subscribe('page_order_tracking_reload_click_event', () => { - pageOrderTrackingFsm.transition('REQUEST_UPDATE'); -}); +// eventListener.subscribe('page_order_tracking_reload_click_event', () => { +// pageOrderTrackingFsm.transition('REQUEST_UPDATE'); +// }); diff --git a/uniquely/com-pwa/src/ui/alwatr-pwa.ts b/uniquely/com-pwa/src/ui/alwatr-pwa.ts index ce97e565d..3b8b7b9c2 100644 --- a/uniquely/com-pwa/src/ui/alwatr-pwa.ts +++ b/uniquely/com-pwa/src/ui/alwatr-pwa.ts @@ -9,7 +9,6 @@ import './page/home.js'; // for perf import './stuff/app-footer.js'; import {topAppBarContextProvider} from '../manager/context.js'; import {pageNewOrderStateMachine} from '../manager/controller/new-order.js'; -import {pageOrderTrackingFsm} from '../manager/controller/order-tracking.js'; import type {RoutesConfig} from '@alwatr/router'; @@ -52,12 +51,13 @@ class AlwatrPwa extends AlwatrPwaElement { unresolved>...`; }, 'order-tracking': (routeContext) => { - if (pageOrderTrackingFsm.state.target === 'unresolved') { - pageOrderTrackingFsm.transition('IMPORT'); - import('./page/order-tracking.js'); - } - pageOrderTrackingFsm.transition('SHOW_TRACKING', {orderId: +routeContext.sectionList[1]}); - return html`...`; + topAppBarContextProvider.setValue({ + headlineKey: 'loading', + }); + import('./page/order-tracking.js'); + return html`...`; }, 'new-order': () => { if (pageNewOrderStateMachine.state.target === 'unresolved') { diff --git a/uniquely/com-pwa/src/ui/page/order-detail.ts b/uniquely/com-pwa/src/ui/page/order-detail.ts index a51147ad0..bb44d0cf4 100644 --- a/uniquely/com-pwa/src/ui/page/order-detail.ts +++ b/uniquely/com-pwa/src/ui/page/order-detail.ts @@ -115,7 +115,6 @@ export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase return this.orderId; } set orderId(orderId: number) { - this.orderId = orderId; this._stateMachine.transition('LOADED_SUCCESS', {orderId}); } diff --git a/uniquely/com-pwa/src/ui/page/order-tracking.ts b/uniquely/com-pwa/src/ui/page/order-tracking.ts index 1b774cb50..0303c8c8d 100644 --- a/uniquely/com-pwa/src/ui/page/order-tracking.ts +++ b/uniquely/com-pwa/src/ui/page/order-tracking.ts @@ -2,37 +2,57 @@ import { customElement, css, html, - StateMachineMixin, UnresolvedMixin, - LocalizeMixin, - SignalMixin, AlwatrBaseElement, + FiniteStateMachineController, + state, + property, + SignalMixin, + LocalizeMixin, } from '@alwatr/element'; import {message} from '@alwatr/i18n'; import '@alwatr/icon'; +import {topAppBarContextProvider} from '@alwatr/pwa-helper/src/context.js'; +import {redirect} from '@alwatr/router'; +import {eventListener} from '@alwatr/signal'; import '@alwatr/ui-kit/button/button.js'; -import '@alwatr/ui-kit/card/icon-box.js'; +import {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; import '@alwatr/ui-kit/card/surface.js'; import '@alwatr/ui-kit/chat/chat.js'; import '@alwatr/ui-kit/radio-group/radio-group.js'; -import {pageOrderTrackingFsm} from '../../manager/controller/order-tracking.js'; +import {fetchOrderStorage} from '../../manager/context-provider/order-storage.js'; +import {orderStorageContextConsumer} from '../../manager/context.js'; import '../stuff/order-status-box.js'; +import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; +import type {Order} from '@alwatr/type/src/customer-order-management.js'; + declare global { interface HTMLElementTagNameMap { 'alwatr-page-order-tracking': AlwatrPageOrderTracking; } } +const buttons = { + backToOrderList: { + icon: 'arrow-back-outline', + flipRtl: true, + clickSignalId: 'order_tracking_back_to_order_list_event', + }, + reload: { + icon: 'reload-outline', + flipRtl: true, + clickSignalId: 'order_tracking_reload_event', + }, +} as const; + + /** * Alwatr Customer Order Management Order Form Page */ @customElement('alwatr-page-order-tracking') -export class AlwatrPageOrderTracking extends StateMachineMixin( - pageOrderTrackingFsm, - UnresolvedMixin(LocalizeMixin(SignalMixin(AlwatrBaseElement))), -) { +export class AlwatrPageOrderTracking extends UnresolvedMixin(LocalizeMixin(SignalMixin(AlwatrBaseElement))) { static override styles = css` :host { display: flex; @@ -51,39 +71,152 @@ export class AlwatrPageOrderTracking extends StateMachineMixin( } `; - protected override render(): unknown { - this._logger.logMethod('render'); - return this[`render_state_${this.stateMachine.state.target}`]?.(); - } + private _stateMachine = new FiniteStateMachineController(this, { + id: 'fsm-order-tracking-' + this.ali, + initial: 'pending', + context: { + orderId: null, + orderStorage: | null> null, + }, + stateRecord: { + '$all': { + entry: () => { + this.gotState = this._stateMachine.state.target; + }, + on: {}, + }, + 'pending': { + entry: () => { + if (orderStorageContextConsumer.getValue() == null) fetchOrderStorage(); + }, + on: { + LOADED_SUCCESS: { + target: 'tracking', + condition: () => { + this._logger.logMethod('state-pending-condition'); + if (this._stateMachine.context.orderStorage == null) return false; + return true; + }, + actions: () => { + if (this._stateMachine.context.orderId == null || + this._stateMachine.context.orderStorage?.data[this._stateMachine.context.orderId] == null + ) this._stateMachine.transition('NOT_FOUND'); + }, + }, + }, + }, + 'tracking': { + on: { + REQUEST_UPDATE: { + target: 'reloading', + actions: this._requestUpdateAction, + }, + NOT_FOUND: { + target: 'notFound', + }, + }, + }, + 'reloading': { + on: { + LOADED_SUCCESS: { + target: 'tracking', + }, + // LOAD_FAILED: { + // target: 'tracking', + // }, + }, + }, + 'notFound': { + on: {}, + }, + }} as const); - protected render_state_loading(): unknown { - this._logger.logMethod('render_state_loading'); - return message('loading'); - } + @state() + gotState = this._stateMachine.state.target; - protected render_state_notFound(): unknown { - this._logger.logMethod('render_state_notFound'); - return message('page_order_tracking_not_found'); + @property({type: Number}) + get orderId(): number { + return this.orderId; + } + set orderId(orderId: number) { + this._stateMachine.transition('LOADED_SUCCESS', {orderId}); } - protected render_state_reloading(): unknown { - this._logger.logMethod('render_state_reloading'); - return this.render_state_tracking(); + override connectedCallback(): void { + super.connectedCallback(); + + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_tracking_headline', + startIcon: buttons.backToOrderList, + endIconList: [buttons.reload], + }); + + this._signalListenerList.push( + orderStorageContextConsumer.subscribe((orderStorage) => { + this._stateMachine.transition('LOADED_SUCCESS', {orderStorage}); + }), + ); + + this._signalListenerList.push( + eventListener.subscribe(buttons.backToOrderList.clickSignalId, () => { + redirect({sectionList: ['order-list']}); + }), + ); + + this._signalListenerList.push( + eventListener.subscribe(buttons.reload.clickSignalId, () => { + this._stateMachine.transition('REQUEST_UPDATE'); + }), + ); } - protected render_state_tracking(): unknown { - this._logger.logMethod('render_state_tracking'); - // validate order id - const order = this.stateMachine.context.orderStorage?.data[this.stateMachine.context.orderId ?? ''] ?? null; - if (order === null) { - this.stateMachine.transition('INVALID_ORDER'); - return; - } + protected override render(): unknown { + this._logger.logMethod('render'); + return this._stateMachine.render({ + pending: () => { + const content: IconBoxContent = { + headline: message('loading'), + icon: 'cloud-download-outline', + tinted: 1, + }; + return html``; + }, + + reloading: 'tracking', + + tracking: () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const order = this._stateMachine.context.orderStorage!.data[this._stateMachine.context.orderId!]; + return [ + html``, + html``, + ]; + }, + + notFound: () => { + const content: IconBoxContent = { + headline: message('page_order_detail_not_found'), + icon: 'close', + tinted: 1, + }; + return html``; + }, + }); + } - return [ - html``, - html``, - ]; + private async _requestUpdateAction(): Promise { + topAppBarContextProvider.setValue({ + headlineKey: 'loading', + startIcon: buttons.backToOrderList, + endIconList: [buttons.reload], + }); + await fetchOrderStorage(); + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_tracking_headline', + startIcon: buttons.backToOrderList, + endIconList: [buttons.reload], + }); + this._stateMachine.transition('LOADED_SUCCESS'); } } From 1a352915fba978da141513517655d1e07350c3ec Mon Sep 17 00:00:00 2001 From: Mohammad Honarvar Date: Thu, 9 Mar 2023 19:31:11 +0330 Subject: [PATCH 15/85] feat(fsm): add `signalRecord` to config --- core/fsm/src/core.ts | 2 +- core/fsm/src/type.ts | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index 61dbe6481..925294783 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -31,7 +31,7 @@ export class FiniteStateMachine< protected async setState(target: TState, by: TEventId): Promise { const state = (this.state = { - target: target, + target, from: this.signal.getValue()?.target ?? target, by, }); diff --git a/core/fsm/src/type.ts b/core/fsm/src/type.ts index 352aa5e26..39f370d46 100644 --- a/core/fsm/src/type.ts +++ b/core/fsm/src/type.ts @@ -1,6 +1,5 @@ import type {MaybeArray, MaybePromise, StringifyableRecord} from '@alwatr/type'; - export interface FsmConfig { /** * Machine ID (It is used in the state change signal identifier, so it must be unique). @@ -54,6 +53,18 @@ export interface FsmConfig MaybePromise>; + transition?: keyof FsmConfig['stateRecord'][ + keyof FsmConfig['stateRecord'] + ]['on']; + } + } } export interface StateContext { From 833e9cc58a5515edcc1c1c72b2761c124557201d Mon Sep 17 00:00:00 2001 From: Mohammad Honarvar Date: Thu, 9 Mar 2023 19:38:46 +0330 Subject: [PATCH 16/85] feat(element): register and remove necessary listeners --- .../finite-state-machine.ts | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/ui/element/src/reactive-controllers/finite-state-machine.ts b/ui/element/src/reactive-controllers/finite-state-machine.ts index 1fc9389f2..0fa42d08d 100644 --- a/ui/element/src/reactive-controllers/finite-state-machine.ts +++ b/ui/element/src/reactive-controllers/finite-state-machine.ts @@ -1,15 +1,20 @@ import {FiniteStateMachine, type FsmConfig} from '@alwatr/fsm'; +import {eventListener, ListenerSpec} from '@alwatr/signal'; import {nothing, type ReactiveController} from '../lit.js'; import type {LoggerMixinInterface} from '../mixins/logging.js'; -import type {StringifyableRecord} from '@alwatr/type'; +import type {ListenerFunction} from '@alwatr/signal/src/type.js'; +import type {Stringifyable, StringifyableRecord} from '@alwatr/type'; export class FiniteStateMachineController< TState extends string, TEventId extends string, TContext extends StringifyableRecord > extends FiniteStateMachine implements ReactiveController { + // FIXME: Choose a proper name + private _listenerList: ListenerSpec[] = []; + constructor( private _host: LoggerMixinInterface, config: Readonly>, @@ -39,4 +44,36 @@ export class FiniteStateMachineController< if (typeof fn !== 'function') return; return fn.call(this._host); } + + hostConnected(): void { + if (this.config.signalRecord == null) return; + + for (const signalId of Object.keys(this.config.signalRecord)) { + let listenerCallback: ListenerFunction | null = null; + + if ('transition' in this.config.signalRecord![signalId]) { + listenerCallback = (): void => { + this.transition(this.config.signalRecord![signalId].transition as TEventId); + }; + } + + if ('actions' in this.config.signalRecord![signalId]) { + // TODO: Check array type of `actions` + + listenerCallback = this.config.signalRecord?.[signalId].actions as ListenerFunction; + } + + if (listenerCallback) { + this._listenerList.push( + eventListener.subscribe(signalId, listenerCallback), + ); + } + } + } + + hostDisconnected(): void { + for (const listener of this._listenerList) { + eventListener.unsubscribe(listener); + } + } } From f7f43016480e7045ec316bef611f0bc01944b4d4 Mon Sep 17 00:00:00 2001 From: Mohammad Honarvar Date: Thu, 9 Mar 2023 19:40:12 +0330 Subject: [PATCH 17/85] fix(com-pwa/ui): fix errors base on last changes in `FSM` --- uniquely/com-pwa/src/ui/page/order-list.ts | 47 +++++++++------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/uniquely/com-pwa/src/ui/page/order-list.ts b/uniquely/com-pwa/src/ui/page/order-list.ts index 590ec051f..608df105c 100644 --- a/uniquely/com-pwa/src/ui/page/order-list.ts +++ b/uniquely/com-pwa/src/ui/page/order-list.ts @@ -11,7 +11,6 @@ import { } from '@alwatr/element'; import {message} from '@alwatr/i18n'; import {redirect} from '@alwatr/router'; -import {eventListener} from '@alwatr/signal'; import {Order} from '@alwatr/type/customer-order-management.js'; import '@alwatr/ui-kit/button/button.js'; import {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; @@ -119,11 +118,25 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix }, }, }, - // signalRecord: { - // 'order_list_reload': { - // translate: 'REQUEST_UPDATE' - // } - // }, + signalRecord: { + [buttons.reload.clickSignalId]: { + transition: 'REQUEST_UPDATE', + }, + [buttons.newOrder.clickSignalId]: { + actions: () => { + redirect({ + sectionList: ['new-order'], + }); + }, + }, + [buttons.orderDetail.clickSignalId]: { + actions: (event: ClickSignalType) => { + redirect({ + sectionList: ['order-detail', event.detail.id], + }); + }, + }, + }, } as const); @state() @@ -143,28 +156,6 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix this._stateMachine.transition('LOADED_SUCCESS', {orderStorage}); }), ); - - this._signalListenerList.push( - eventListener.subscribe(buttons.reload.clickSignalId, () => { - this._stateMachine.transition('REQUEST_UPDATE'); - }), - ); - - this._signalListenerList.push( - eventListener.subscribe(buttons.newOrder.clickSignalId, () => { - redirect({ - sectionList: ['new-order'], - }); - }), - ); - - this._signalListenerList.push( - eventListener.subscribe>(buttons.orderDetail.clickSignalId, (event) => { - redirect({ - sectionList: ['order-detail', event.detail.id], - }); - }), - ); } override render(): unknown { From cc1b29e8dd8475f253f149ea4d5b1f5db6e01d54 Mon Sep 17 00:00:00 2001 From: "S. Amir Mohammad Najafi" Date: Thu, 9 Mar 2023 20:00:02 +0330 Subject: [PATCH 18/85] refactor(com-pwa): new state machine for new order --- .../finite-state-machine.ts | 2 +- .../src/manager/controller/new-order.ts | 554 ++++++++--------- uniquely/com-pwa/src/ui/alwatr-pwa.ts | 19 +- uniquely/com-pwa/src/ui/page/new-order.ts | 582 ++++++++++++++---- .../com-pwa/src/ui/page/order-tracking.ts | 5 +- 5 files changed, 733 insertions(+), 429 deletions(-) diff --git a/ui/element/src/reactive-controllers/finite-state-machine.ts b/ui/element/src/reactive-controllers/finite-state-machine.ts index 0fa42d08d..6f5e0cd85 100644 --- a/ui/element/src/reactive-controllers/finite-state-machine.ts +++ b/ui/element/src/reactive-controllers/finite-state-machine.ts @@ -4,7 +4,7 @@ import {eventListener, ListenerSpec} from '@alwatr/signal'; import {nothing, type ReactiveController} from '../lit.js'; import type {LoggerMixinInterface} from '../mixins/logging.js'; -import type {ListenerFunction} from '@alwatr/signal/src/type.js'; +import type {ListenerFunction} from '@alwatr/signal/type.js'; import type {Stringifyable, StringifyableRecord} from '@alwatr/type'; export class FiniteStateMachineController< diff --git a/uniquely/com-pwa/src/manager/controller/new-order.ts b/uniquely/com-pwa/src/manager/controller/new-order.ts index 751e39adb..a18c3a740 100644 --- a/uniquely/com-pwa/src/manager/controller/new-order.ts +++ b/uniquely/com-pwa/src/manager/controller/new-order.ts @@ -1,330 +1,278 @@ -import {FiniteStateMachine} from '@alwatr/fsm'; -import {redirect} from '@alwatr/router'; -import {eventListener} from '@alwatr/signal'; -import {orderInfoSchema, tileQtyStep} from '@alwatr/type/customer-order-management.js'; -import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; -import {getLocalStorageItem} from '@alwatr/util'; -import {validator} from '@alwatr/validator'; +// import {FiniteStateMachine} from '@alwatr/fsm'; +// import {message} from '@alwatr/i18n'; +// import {redirect} from '@alwatr/router'; +// import {eventListener} from '@alwatr/signal'; +// import {orderInfoSchema, tileQtyStep} from '@alwatr/type/customer-order-management.js'; +// import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; +// import {getLocalStorageItem} from '@alwatr/util'; +// import {validator} from '@alwatr/validator'; -import {fetchPriceStorage} from '../context-provider/price-storage.js'; -import {fetchProductStorage} from '../context-provider/product-storage.js'; -import { - finalPriceStorageContextConsumer, - priceStorageContextConsumer, - productStorageContextConsumer, - scrollToTopCommand, - submitOrderCommandTrigger, - topAppBarContextProvider, -} from '../context.js'; -import {logger} from '../logger.js'; +// import {fetchPriceStorage} from '../context-provider/price-storage.js'; +// import {fetchProductStorage} from '../context-provider/product-storage.js'; +// import { +// finalPriceStorageContextConsumer, +// priceStorageContextConsumer, +// productStorageContextConsumer, +// scrollToTopCommand, +// submitOrderCommandTrigger, +// topAppBarContextProvider, +// } from '../context.js'; +// import {logger} from '../logger.js'; -import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; -import type {Product, ProductPrice, OrderDraft, OrderItem} from '@alwatr/type/customer-order-management.js'; +// import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; +// import type {Product, ProductPrice, OrderDraft, OrderItem} from '@alwatr/type/customer-order-management.js'; -export const pageNewOrderStateMachine = new FiniteStateMachine({ - id: 'page-order-detail', - initial: 'unresolved', - context: { - registeredOrderId: '', - order: getLocalStorageItem('draft-order-x2', {id: 'new', status: 'draft'}), - productStorage: | null>null, - priceStorage: | null>null, - finalPriceStorage: | null>null, - }, - stateRecord: { - $all: { - on: { - CONNECTED: '$self', - PARTIAL_LOAD: '$self', - }, - }, - unresolved: { - on: { - IMPORT: 'resolving', - CONTEXT_LOADED: 'edit', - }, - }, - resolving: { - on: { - CONNECTED: 'loading', - CONTEXT_LOADED: 'edit', - }, - }, - loading: { - on: { - CONTEXT_LOADED: 'edit', - }, - }, - edit: { - on: { - SELECT_PRODUCT: 'selectProduct', - EDIT_SHIPPING: 'shippingForm', - SUBMIT: 'review', - QTY_UPDATE: '$self', - }, - }, - selectProduct: { - on: { - SUBMIT: 'edit', - }, - }, - shippingForm: { - on: { - SUBMIT: 'edit', - }, - }, - review: { - on: { - BACK: 'edit', - VALIDATION_FAILED: 'edit', - FINAL_SUBMIT: 'submitting', - }, - }, - submitting: { - on: { - SUBMIT_SUCCESS: 'submitSuccess', - SUBMIT_FAILED: 'submitFailed', - }, - }, - submitSuccess: { - on: { - NEW_ORDER: 'edit', - }, - }, - submitFailed: { - on: { - FINAL_SUBMIT: 'submitting', - }, - }, - }, -}); +// export const pageNewOrderStateMachine = new FiniteStateMachine({ +// id: 'page-order-detail', +// initial: 'unresolved', +// context: { +// registeredOrderId: '', +// order: getLocalStorageItem('draft-order-x2', {id: 'new', status: 'draft'}), +// productStorage: | null>null, +// priceStorage: | null>null, +// finalPriceStorage: | null>null, +// }, +// stateRecord: { +// $all: { +// on: { +// CONNECTED: '$self', +// PARTIAL_LOAD: '$self', +// }, +// }, +// unresolved: { +// on: { +// IMPORT: 'resolving', +// CONTEXT_LOADED: 'edit', +// }, +// }, +// resolving: { +// on: { +// CONNECTED: 'loading', +// CONTEXT_LOADED: 'edit', +// }, +// }, +// loading: { +// on: { +// CONTEXT_LOADED: 'edit', +// }, +// }, +// edit: { +// on: { +// SELECT_PRODUCT: 'selectProduct', +// EDIT_SHIPPING: 'shippingForm', +// SUBMIT: 'review', +// QTY_UPDATE: '$self', +// }, +// }, +// selectProduct: { +// on: { +// SUBMIT: 'edit', +// }, +// }, +// shippingForm: { +// on: { +// SUBMIT: 'edit', +// }, +// }, +// review: { +// on: { +// BACK: 'edit', +// VALIDATION_FAILED: 'edit', +// FINAL_SUBMIT: 'submitting', +// }, +// }, +// submitting: { +// on: { +// SUBMIT_SUCCESS: 'submitSuccess', +// SUBMIT_FAILED: 'submitFailed', +// }, +// }, +// submitSuccess: { +// on: { +// NEW_ORDER: 'edit', +// }, +// }, +// submitFailed: { +// on: { +// FINAL_SUBMIT: 'submitting', +// }, +// }, +// }, +// }); -export const buttons = { - back: { - icon: 'arrow-back-outline', - flipRtl: true, - clickSignalId: pageNewOrderStateMachine.config.id + '_back_click_event', - }, - backToHome: { - icon: 'arrow-back-outline', - flipRtl: true, - clickSignalId: 'back_to_home_click_event', - }, - editItems: { - icon: 'create-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_edit_items_click_event', - }, - submit: { - icon: 'checkmark-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_submit_click_event', - }, - edit: { - icon: 'create-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_edit_click_event', - }, - submitFinal: { - icon: 'checkmark-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_submit_final_click_event', - }, - submitShippingForm: { - icon: 'checkmark-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_submit_shipping_form_click_event', - }, - editShippingForm: { - icon: 'checkmark-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_edit_shipping_form_click_event', - }, - newOrder: { - icon: 'add-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_new_order_click_event', - }, - detail: { - icon: 'information-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_detail_click_event', - }, - tracking: { - icon: 'chatbox-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_tracking_click_event', - }, - retry: { - icon: 'reload-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_retry_click_event', - }, -} as const; +// pageNewOrderStateMachine.signal.subscribe(async (state) => { +// switch (state.by) { +// case 'IMPORT': { +// // just in unresolved +// // topAppBarContextProvider.setValue({ +// // headlineKey: 'loading', +// // }); +// if (productStorageContextConsumer.getValue() == null) { +// fetchProductStorage(); +// } +// if ( +// priceStorageContextConsumer.getValue() == null || +// finalPriceStorageContextConsumer.getValue() == null +// ) { +// fetchPriceStorage(); +// } +// break; +// } -pageNewOrderStateMachine.signal.subscribe(async (state) => { - switch (state.by) { - case 'IMPORT': { - // just in unresolved - topAppBarContextProvider.setValue({ - headlineKey: 'loading', - }); - if (productStorageContextConsumer.getValue() == null) { - fetchProductStorage(); - } - if ( - priceStorageContextConsumer.getValue() == null || - finalPriceStorageContextConsumer.getValue() == null - ) { - fetchPriceStorage(); - } - break; - } +// case 'PARTIAL_LOAD': { +// if (Object.values(pageNewOrderStateMachine.context).indexOf(null) === -1) { +// pageNewOrderStateMachine.transition('CONTEXT_LOADED'); +// } +// break; +// } - case 'PARTIAL_LOAD': { - if (Object.values(pageNewOrderStateMachine.context).indexOf(null) === -1) { - pageNewOrderStateMachine.transition('CONTEXT_LOADED'); - } - break; - } +// case 'CONNECTED': { +// topAppBarContextProvider.setValue({ +// headlineKey: 'page_new_order_headline', +// startIcon: buttons.backToHome, +// }); +// break; +// } - case 'CONNECTED': { - topAppBarContextProvider.setValue({ - headlineKey: 'page_new_order_headline', - startIcon: buttons.backToHome, - }); - break; - } +// case 'NEW_ORDER': { +// pageNewOrderStateMachine.context.registeredOrderId = ''; +// break; +// } - case 'NEW_ORDER': { - pageNewOrderStateMachine.context.registeredOrderId = ''; - break; - } +// case 'FINAL_SUBMIT': { +// const order = await submitOrderCommandTrigger.requestWithResponse(pageNewOrderStateMachine.context.order); +// if (order == null) { +// pageNewOrderStateMachine.transition('SUBMIT_FAILED'); +// return; +// } +// // else +// pageNewOrderStateMachine.context.registeredOrderId = order.id; +// pageNewOrderStateMachine.transition('SUBMIT_SUCCESS'); +// break; +// } - case 'FINAL_SUBMIT': { - const order = await submitOrderCommandTrigger.requestWithResponse(pageNewOrderStateMachine.context.order); - if (order == null) { - pageNewOrderStateMachine.transition('SUBMIT_FAILED'); - return; - } - // else - pageNewOrderStateMachine.context.registeredOrderId = order.id; - pageNewOrderStateMachine.transition('SUBMIT_SUCCESS'); - break; - } +// case 'SUBMIT_SUCCESS': { +// localStorage.removeItem('draft-order-x2'); +// pageNewOrderStateMachine.context.order = getLocalStorageItem('draft-order-x2', {id: 'new', status: 'draft'}); +// break; +// } +// } +// }); - case 'SUBMIT_SUCCESS': { - localStorage.removeItem('draft-order-x2'); - pageNewOrderStateMachine.context.order = getLocalStorageItem('draft-order-x2', {id: 'new', status: 'draft'}); - break; - } - } -}); +// pageNewOrderStateMachine.signal.subscribe(async (state) => { +// localStorage.setItem('draft-order-x2', JSON.stringify(pageNewOrderStateMachine.context.order)); -pageNewOrderStateMachine.signal.subscribe(async (state) => { - localStorage.setItem('draft-order-x2', JSON.stringify(pageNewOrderStateMachine.context.order)); +// if (state.target != 'shippingForm' && state.target != state.from) { +// scrollToTopCommand.request({}); +// } - if (state.target != 'shippingForm' && state.target != state.from) { - scrollToTopCommand.request({}); - } +// if ( +// state.target === 'edit' && +// state.from != 'selectProduct' && +// !pageNewOrderStateMachine.context.order?.itemList?.length +// ) { +// pageNewOrderStateMachine.transition('SELECT_PRODUCT'); +// } - if ( - state.target === 'edit' && - state.from != 'selectProduct' && - !pageNewOrderStateMachine.context.order?.itemList?.length - ) { - pageNewOrderStateMachine.transition('SELECT_PRODUCT'); - } +// else if (state.target === 'edit' || state.target === 'review') { +// const order = pageNewOrderStateMachine.context.order; +// let totalPrice = 0; +// let finalTotalPrice = 0; +// for (const item of order.itemList ?? []) { +// totalPrice += item.price * item.qty * tileQtyStep; +// finalTotalPrice += item.finalPrice * item.qty * tileQtyStep; +// } +// order.totalPrice = Math.round(totalPrice); +// order.finalTotalPrice = Math.round(finalTotalPrice); +// } - else if (state.target === 'edit' || state.target === 'review') { - const order = pageNewOrderStateMachine.context.order; - let totalPrice = 0; - let finalTotalPrice = 0; - for (const item of order.itemList ?? []) { - totalPrice += item.price * item.qty * tileQtyStep; - finalTotalPrice += item.finalPrice * item.qty * tileQtyStep; - } - order.totalPrice = Math.round(totalPrice); - order.finalTotalPrice = Math.round(finalTotalPrice); - } +// if (state.target === 'review') { +// try { +// validator(orderInfoSchema, pageNewOrderStateMachine.context.order, true); +// } +// catch (err) { +// pageNewOrderStateMachine.transition('VALIDATION_FAILED'); +// const _err = err as (Error & {cause?: Record}); +// logger.incident('SUBMIT', _err.name, 'validation failed', _err); +// if (_err.cause?.itemPath?.indexOf('shippingInfo') !== -1) { +// snackbarSignalTrigger.request({ +// message: message('page_new_order_shipping_info_not_valid_message'), +// }); +// } +// else { +// snackbarSignalTrigger.request({ +// message: message('page_new_order_order_not_valid_message'), +// }); +// } +// } +// } +// }); - if (state.target === 'review') { - try { - validator(orderInfoSchema, pageNewOrderStateMachine.context.order, true); - } - catch (err) { - pageNewOrderStateMachine.transition('VALIDATION_FAILED'); - const _err = err as (Error & {cause?: Record}); - logger.incident('SUBMIT', _err.name, 'validation failed', _err); - if (_err.cause?.itemPath?.indexOf('shippingInfo') !== -1) { - snackbarSignalTrigger.request({ - messageKey: 'page_new_order_shipping_info_not_valid_message', - }); - } - else { - snackbarSignalTrigger.request({ - messageKey: 'page_new_order_order_not_valid_message', - }); - } - } - } -}); +// productStorageContextConsumer.subscribe((productStorage) => { +// pageNewOrderStateMachine.transition('PARTIAL_LOAD', {productStorage}); +// }); +// priceStorageContextConsumer.subscribe((priceStorage) => { +// pageNewOrderStateMachine.transition('PARTIAL_LOAD', {priceStorage}); +// }); +// finalPriceStorageContextConsumer.subscribe((finalPriceStorage) => { +// pageNewOrderStateMachine.transition('PARTIAL_LOAD', {finalPriceStorage}); +// }); -productStorageContextConsumer.subscribe((productStorage) => { - pageNewOrderStateMachine.transition('PARTIAL_LOAD', {productStorage}); -}); -priceStorageContextConsumer.subscribe((priceStorage) => { - pageNewOrderStateMachine.transition('PARTIAL_LOAD', {priceStorage}); -}); -finalPriceStorageContextConsumer.subscribe((finalPriceStorage) => { - pageNewOrderStateMachine.transition('PARTIAL_LOAD', {finalPriceStorage}); -}); +// eventListener.subscribe(buttons.submit.clickSignalId, () => { +// pageNewOrderStateMachine.transition('SUBMIT'); +// }); -eventListener.subscribe(buttons.submit.clickSignalId, () => { - pageNewOrderStateMachine.transition('SUBMIT'); -}); +// eventListener.subscribe(buttons.edit.clickSignalId, () => { +// pageNewOrderStateMachine.transition('BACK'); +// }); -eventListener.subscribe(buttons.edit.clickSignalId, () => { - pageNewOrderStateMachine.transition('BACK'); -}); +// eventListener.subscribe(buttons.submitFinal.clickSignalId, () => { +// pageNewOrderStateMachine.transition('FINAL_SUBMIT'); +// }); -eventListener.subscribe(buttons.submitFinal.clickSignalId, () => { - pageNewOrderStateMachine.transition('FINAL_SUBMIT'); -}); +// eventListener.subscribe(buttons.editItems.clickSignalId, () => { +// pageNewOrderStateMachine.transition('SELECT_PRODUCT'); +// }); -eventListener.subscribe(buttons.editItems.clickSignalId, () => { - pageNewOrderStateMachine.transition('SELECT_PRODUCT'); -}); +// eventListener.subscribe(buttons.editShippingForm.clickSignalId, () => { +// pageNewOrderStateMachine.transition('EDIT_SHIPPING'); +// }); -eventListener.subscribe(buttons.editShippingForm.clickSignalId, () => { - pageNewOrderStateMachine.transition('EDIT_SHIPPING'); -}); +// eventListener.subscribe(buttons.submitShippingForm.clickSignalId, () => { +// pageNewOrderStateMachine.transition('SUBMIT'); +// }); -eventListener.subscribe(buttons.submitShippingForm.clickSignalId, () => { - pageNewOrderStateMachine.transition('SUBMIT'); -}); +// eventListener.subscribe(buttons.tracking.clickSignalId, () => { +// const orderId = pageNewOrderStateMachine.context.registeredOrderId; +// pageNewOrderStateMachine.transition('NEW_ORDER'); +// redirect({sectionList: ['order-tracking', orderId]}); +// }); -eventListener.subscribe(buttons.tracking.clickSignalId, () => { - const orderId = pageNewOrderStateMachine.context.registeredOrderId; - pageNewOrderStateMachine.transition('NEW_ORDER'); - redirect({sectionList: ['order-tracking', orderId]}); -}); +// eventListener.subscribe(buttons.detail.clickSignalId, () => { +// const orderId = pageNewOrderStateMachine.context.registeredOrderId; +// pageNewOrderStateMachine.transition('NEW_ORDER'); +// redirect({sectionList: ['order-detail', orderId]}); +// }); -eventListener.subscribe(buttons.detail.clickSignalId, () => { - const orderId = pageNewOrderStateMachine.context.registeredOrderId; - pageNewOrderStateMachine.transition('NEW_ORDER'); - redirect({sectionList: ['order-detail', orderId]}); -}); +// eventListener.subscribe(buttons.newOrder.clickSignalId, () => { +// pageNewOrderStateMachine.transition('NEW_ORDER'); +// redirect('/new-order/'); +// }); -eventListener.subscribe(buttons.newOrder.clickSignalId, () => { - pageNewOrderStateMachine.transition('NEW_ORDER'); - redirect('/new-order/'); -}); +// eventListener.subscribe(buttons.retry.clickSignalId, async () => { +// pageNewOrderStateMachine.transition('FINAL_SUBMIT'); +// }); -eventListener.subscribe(buttons.retry.clickSignalId, async () => { - pageNewOrderStateMachine.transition('FINAL_SUBMIT'); -}); - -export const qtyUpdate = (orderItem: OrderItem, add: number): void => { - const qty = orderItem.qty + add; - if (qty <= 0) return; - orderItem.qty = qty; - pageNewOrderStateMachine.transition('QTY_UPDATE'); -}; -eventListener.subscribe>('order_item_qty_add', (event) => { - qtyUpdate(event.detail, 1); -}); -eventListener.subscribe>('order_item_qty_remove', (event) => { - qtyUpdate(event.detail, -1); -}); +// export const qtyUpdate = (orderItem: OrderItem, add: number): void => { +// const qty = orderItem.qty + add; +// if (qty <= 0) return; +// orderItem.qty = qty; +// pageNewOrderStateMachine.transition('QTY_UPDATE'); +// }; +// eventListener.subscribe>('order_item_qty_add', (event) => { +// qtyUpdate(event.detail, 1); +// }); +// eventListener.subscribe>('order_item_qty_remove', (event) => { +// qtyUpdate(event.detail, -1); +// }); diff --git a/uniquely/com-pwa/src/ui/alwatr-pwa.ts b/uniquely/com-pwa/src/ui/alwatr-pwa.ts index 3b8b7b9c2..3c98eca58 100644 --- a/uniquely/com-pwa/src/ui/alwatr-pwa.ts +++ b/uniquely/com-pwa/src/ui/alwatr-pwa.ts @@ -8,7 +8,6 @@ import '@alwatr/ui-kit/style/theme/palette-270.css'; import './page/home.js'; // for perf import './stuff/app-footer.js'; import {topAppBarContextProvider} from '../manager/context.js'; -import {pageNewOrderStateMachine} from '../manager/controller/new-order.js'; import type {RoutesConfig} from '@alwatr/router'; @@ -35,35 +34,27 @@ class AlwatrPwa extends AlwatrPwaElement { return html`...`; }, 'order-list': () => { - topAppBarContextProvider.setValue({ - headlineKey: 'loading', - }); + topAppBarContextProvider.setValue({headlineKey: 'loading'}); import('./page/order-list.js'); return html`...`; }, 'order-detail': (routeContext) => { - topAppBarContextProvider.setValue({ - headlineKey: 'loading', - }); + topAppBarContextProvider.setValue({headlineKey: 'loading'}); import('./page/order-detail.js'); return html`...`; }, 'order-tracking': (routeContext) => { - topAppBarContextProvider.setValue({ - headlineKey: 'loading', - }); + topAppBarContextProvider.setValue({headlineKey: 'loading'}); import('./page/order-tracking.js'); return html`...`; }, 'new-order': () => { - if (pageNewOrderStateMachine.state.target === 'unresolved') { - pageNewOrderStateMachine.transition('IMPORT'); - import('./page/new-order.js'); - } + topAppBarContextProvider.setValue({headlineKey: 'loading'}); + import('./page/new-order.js'); return html`...`; }, }, diff --git a/uniquely/com-pwa/src/ui/page/new-order.ts b/uniquely/com-pwa/src/ui/page/new-order.ts index cfb37fe68..b8d7f4800 100644 --- a/uniquely/com-pwa/src/ui/page/new-order.ts +++ b/uniquely/com-pwa/src/ui/page/new-order.ts @@ -1,7 +1,32 @@ -import {customElement, html, StateMachineMixin, UnresolvedMixin} from '@alwatr/element'; +import {customElement, FiniteStateMachineController, html, state, UnresolvedMixin} from '@alwatr/element'; import {message} from '@alwatr/i18n'; +import {redirect} from '@alwatr/router'; +import {eventListener} from '@alwatr/signal'; +import {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; +import { + Order, + OrderDraft, + orderInfoSchema, + OrderItem, + Product, + ProductPrice, + tileQtyStep, +} from '@alwatr/type/customer-order-management.js'; +import {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; +import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; +import {getLocalStorageItem} from '@alwatr/util'; +import {validator} from '@alwatr/validator'; -import {buttons, pageNewOrderStateMachine} from '../../manager/controller/new-order.js'; +import {fetchPriceStorage} from '../../manager/context-provider/price-storage.js'; +import {fetchProductStorage} from '../../manager/context-provider/product-storage.js'; +import { + finalPriceStorageContextConsumer, + priceStorageContextConsumer, + productStorageContextConsumer, + scrollToTopCommand, + submitOrderCommandTrigger, + topAppBarContextProvider, +} from '../../manager/context.js'; import {AlwatrOrderDetailBase} from '../stuff/order-detail-base.js'; import '../stuff/select-product.js'; @@ -11,116 +36,432 @@ declare global { } } +const newOrderLocalStorageKey = 'draft-order-x2'; + +const buttons = { + back: { + icon: 'arrow-back-outline', + flipRtl: true, + clickSignalId: 'page_new_order_back_click_event', + }, + backToHome: { + icon: 'arrow-back-outline', + flipRtl: true, + clickSignalId: 'back_to_home_click_event', + }, + editItems: { + icon: 'create-outline', + clickSignalId: 'page_new_order_edit_items_click_event', + }, + submit: { + icon: 'checkmark-outline', + clickSignalId: 'page_new_order_submit_click_event', + }, + edit: { + icon: 'create-outline', + clickSignalId: 'page_new_order_edit_click_event', + }, + submitFinal: { + icon: 'checkmark-outline', + clickSignalId: 'page_new_order_submit_final_click_event', + }, + submitShippingForm: { + icon: 'checkmark-outline', + clickSignalId: 'page_new_order_submit_shipping_form_click_event', + }, + editShippingForm: { + icon: 'checkmark-outline', + clickSignalId: 'page_new_order_edit_shipping_form_click_event', + }, + newOrder: { + icon: 'add-outline', + clickSignalId: 'page_new_order_new_order_click_event', + }, + detail: { + icon: 'information-outline', + clickSignalId: 'page_new_order_detail_click_event', + }, + tracking: { + icon: 'chatbox-outline', + clickSignalId: 'page_new_order_tracking_click_event', + }, + retry: { + icon: 'reload-outline', + clickSignalId: 'page_new_order_retry_click_event', + }, +} as const; + /** * Alwatr Customer Order Management Order Form Page */ @customElement('alwatr-page-new-order') -export class AlwatrPageNewOrder extends StateMachineMixin( - pageNewOrderStateMachine, - UnresolvedMixin(AlwatrOrderDetailBase), -) { - protected override render(): unknown { - this._logger.logMethod('render'); - return this[`render_state_${this.stateMachine.state.target}`]?.(); - } +export class AlwatrPageNewOrder extends UnresolvedMixin(AlwatrOrderDetailBase) { + private _stateMachine = new FiniteStateMachineController(this, { + id: 'fsm-new-order-' + this.ali, + initial: 'pending', + context: { + registeredOrderId: null, + order: getLocalStorageItem(newOrderLocalStorageKey, {id: 'new', status: 'draft'}), + productStorage: | null>null, + priceStorage: | null>null, + finalPriceStorage: | null>null, + }, + stateRecord: { + $all: { + entry: () => { + this.gotState = this._stateMachine.state.target; + localStorage.setItem(newOrderLocalStorageKey, JSON.stringify(this._stateMachine.context.order)); - protected render_state_loading(): unknown { - this._logger.logMethod('render_state_loading'); - return this.render_part_message('loading', 'cloud-download-outline'); - } + if ( + this._stateMachine.state.target != 'shippingForm' && + this._stateMachine.state.target != this._stateMachine.state.from + ) { + scrollToTopCommand.request({}); + } - protected render_state_edit(): unknown { - this._logger.logMethod('render_state_detail'); - const order = this.stateMachine.context.order; - return [ - // this.render_part_status(order), - this.render_part_item_list(order.itemList ?? [], this.stateMachine.context.productStorage, true), - this.render_part_btn_product(), - this.render_part_shipping_info(order.shippingInfo), - this.render_part_btn_shipping_edit(), - this.render_part_summary(order), - this.render_part_btn_submit(), - ]; - } + if ( + this._stateMachine.state.target === 'edit' && + this._stateMachine.state.from != 'selectProduct' && + !this._stateMachine.context.order?.itemList?.length + ) { + this._stateMachine.transition('SELECT_PRODUCT'); + } + else if (this._stateMachine.state.target === 'edit' || this._stateMachine.state.target === 'review') { + const order = this._stateMachine.context.order; + let totalPrice = 0; + let finalTotalPrice = 0; + for (const item of order.itemList ?? []) { + totalPrice += item.price * item.qty * tileQtyStep; + finalTotalPrice += item.finalPrice * item.qty * tileQtyStep; + } + order.totalPrice = Math.round(totalPrice); + order.finalTotalPrice = Math.round(finalTotalPrice); + } + }, + on: {}, + }, + pending: { + entry: () => { + if (productStorageContextConsumer.getValue() == null) { + fetchProductStorage(); + } + if (priceStorageContextConsumer.getValue() == null || finalPriceStorageContextConsumer.getValue() == null) { + fetchPriceStorage(); + } + }, + on: { + LOADED_SUCCESS: { + target: 'edit', + condition: () => { + if ( + this._stateMachine.context.finalPriceStorage == null || + this._stateMachine.context.priceStorage == null || + this._stateMachine.context.productStorage == null + ) { + return false; + } + return true; + }, + }, + }, + }, + edit: { + on: { + SELECT_PRODUCT: { + target: 'selectProduct', + }, + EDIT_SHIPPING: { + target: 'shippingForm', + actions: () => { + this._stateMachine.context.order.shippingInfo ??= {}; + }, + }, + SUBMIT: { + target: 'review', + condition: () => { + if ( + !this._stateMachine.context.order.itemList?.length && + this._stateMachine.context.order.shippingInfo == null + ) { + return false; + } + // else + return this.validateOrder(); + }, + }, + QTY_UPDATE: {}, + }, + }, + selectProduct: { + on: { + SUBMIT: { + target: 'edit', + }, + }, + }, + shippingForm: { + on: { + SUBMIT: { + target: 'edit', + }, + }, + }, + review: { + on: { + BACK: {}, + FINAL_SUBMIT: { + target: 'submitting', + actions: async () => { + const order = await submitOrderCommandTrigger.requestWithResponse(this._stateMachine.context.order); + if (order == null) { + this._stateMachine.transition('SUBMIT_FAILED'); + return; + } + // else + this._stateMachine.transition('SUBMIT_SUCCESS', {registeredOrderId: order.id}); + }, + }, + }, + }, + submitting: { + on: { + SUBMIT_SUCCESS: { + target: 'submitSuccess', + actions: () => { + localStorage.removeItem(newOrderLocalStorageKey); + // TODO: this._stateMachine.context.order = getLocalStorageItem(newOrderLocalStorageKey, {id: 'new', status: 'draft'}); + }, + }, + SUBMIT_FAILED: { + target: 'submitFailed', + }, + }, + }, + submitSuccess: { + on: { + NEW_ORDER: { + target: 'edit', + actions: () => { + // TODO: registeredOrderId = '' + }, + }, + }, + }, + submitFailed: { + on: { + FINAL_SUBMIT: { + target: 'submitting', + }, + }, + }, + }, + } as const); - protected render_state_review(): unknown { - this._logger.logMethod('render_state_review'); - const order = this.stateMachine.context.order; - if (!(order.itemList?.length && order.shippingInfo)) { - this.stateMachine.transition('BACK'); - return; - } - // else - return [ - this.render_part_status(order), - this.render_part_item_list(order.itemList, this.stateMachine.context.productStorage), - this.render_part_shipping_info(order.shippingInfo), - this.render_part_summary(order), - this.render_part_btn_final_submit(), - ]; - } + @state() + gotState = this._stateMachine.state.target; - protected render_state_selectProduct(): unknown { - this._logger.logMethod('render_state_selectProduct'); - return html``; - } + override connectedCallback(): void { + super.connectedCallback(); - protected render_state_shippingForm(): unknown { - this._logger.logMethod('render_state_shippingForm'); - const order = this.stateMachine.context.order; - order.shippingInfo ??= {}; - return [ - this.render_part_item_list(order.itemList ?? [], this.stateMachine.context.productStorage, false), - this.render_part_shipping_form(order.shippingInfo), - this.render_part_btn_shipping_submit(), - ]; - } + topAppBarContextProvider.setValue({ + headlineKey: 'page_new_order_headline', + startIcon: buttons.backToHome, + }); + + this._signalListenerList.push( + productStorageContextConsumer.subscribe((productStorage) => { + this._stateMachine.transition('LOADED_SUCCESS', {productStorage}); + }), + ); - protected render_state_submitting(): unknown { - this._logger.logMethod('render_state_submitting'); - return this.render_part_message('page_new_order_submitting_message', 'cloud-upload-outline'); + this._signalListenerList.push( + priceStorageContextConsumer.subscribe((priceStorage) => { + this._stateMachine.transition('LOADED_SUCCESS', {priceStorage}); + }), + ); + + this._signalListenerList.push( + finalPriceStorageContextConsumer.subscribe((finalPriceStorage) => { + this._stateMachine.transition('LOADED_SUCCESS', {finalPriceStorage}); + }), + ); + this._signalListenerList.push( + eventListener.subscribe(buttons.submit.clickSignalId, () => { + this._stateMachine.transition('SUBMIT'); + }), + ); + this._signalListenerList.push( + eventListener.subscribe(buttons.edit.clickSignalId, () => { + this._stateMachine.transition('BACK'); + }), + ); + this._signalListenerList.push( + eventListener.subscribe(buttons.submitFinal.clickSignalId, () => { + this._stateMachine.transition('FINAL_SUBMIT'); + }), + ); + this._signalListenerList.push( + eventListener.subscribe(buttons.editItems.clickSignalId, () => { + this._stateMachine.transition('SELECT_PRODUCT'); + }), + ); + this._signalListenerList.push( + eventListener.subscribe(buttons.editShippingForm.clickSignalId, () => { + this._stateMachine.transition('EDIT_SHIPPING'); + }), + ); + this._signalListenerList.push( + eventListener.subscribe(buttons.submitShippingForm.clickSignalId, () => { + this._stateMachine.transition('SUBMIT'); + }), + ); + this._signalListenerList.push( + eventListener.subscribe(buttons.tracking.clickSignalId, () => { + const orderId = this._stateMachine.context.registeredOrderId as string; + this._stateMachine.transition('NEW_ORDER'); + redirect({sectionList: ['order-tracking', orderId]}); + }), + ); + this._signalListenerList.push( + eventListener.subscribe(buttons.detail.clickSignalId, () => { + const orderId = this._stateMachine.context.registeredOrderId as string; + this._stateMachine.transition('NEW_ORDER'); + redirect({sectionList: ['order-detail', orderId]}); + }), + ); + this._signalListenerList.push( + eventListener.subscribe(buttons.newOrder.clickSignalId, () => { + this._stateMachine.transition('NEW_ORDER'); + redirect('/new-order/'); + }), + ); + + this._signalListenerList.push( + eventListener.subscribe(buttons.retry.clickSignalId, async () => { + this._stateMachine.transition('FINAL_SUBMIT'); + }), + ); + + this._signalListenerList.push( + eventListener.subscribe>('order_item_qty_add', (event) => { + this.qtyUpdate(event.detail, 1); + }), + ); + + this._signalListenerList.push( + eventListener.subscribe>('order_item_qty_remove', (event) => { + this.qtyUpdate(event.detail, -1); + }), + ); } - protected render_state_submitSuccess(): unknown { - this._logger.logMethod('render_state_submitSuccess'); - return [ - this.render_part_message('page_new_order_submit_success_message', 'cloud-done-outline'), - this.render_part_btn_submit_success(), - ]; + protected override render(): unknown { + this._logger.logMethod('render'); + return this._stateMachine.render({ + edit: () => { + const order = this._stateMachine.context.order; + return [ + // this.render_part_status(order), + this.render_part_item_list(order.itemList ?? [], this._stateMachine.context.productStorage, true), + this.render_part_btn_product(), + this.render_part_shipping_info(order.shippingInfo), + this.render_part_btn_shipping_edit(), + this.render_part_summary(order), + this.render_part_btn_submit(), + ]; + }, + + selectProduct: () => { + return [html``]; + }, + + shippingForm: () => { + const order = this._stateMachine.context.order; + return [ + this.render_part_item_list(order.itemList ?? [], this._stateMachine.context.productStorage, false), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.render_part_shipping_form(order.shippingInfo!), + this.render_part_btn_shipping_submit(), + ]; + }, + + review: () => { + const order = this._stateMachine.context.order as Order; + return [ + this.render_part_status(order), + this.render_part_item_list(order.itemList, this._stateMachine.context.productStorage), + this.render_part_shipping_info(order.shippingInfo), + this.render_part_summary(order), + this.render_part_btn_final_submit(), + ]; + }, + + submitting: () => { + const content: IconBoxContent = { + headline: message('page_new_order_submitting_message'), + icon: 'cloud-upload-outline', + tinted: 1, + }; + return html``; + }, + + submitSuccess: () => { + const content: IconBoxContent = { + headline: message('page_new_order_submit_success_message'), + icon: 'cloud-done-outline', + tinted: 1, + }; + return [html``, this.render_part_btn_submit_success()]; + }, + + submitFailed: () => { + const content: IconBoxContent = { + headline: message('page_new_order_submit_failed_message'), + icon: 'cloud-offline-outline', + tinted: 1, + }; + return [html``, this.render_part_btn_submit_failed()]; + }, + }); } - protected render_state_submitFailed(): unknown { - this._logger.logMethod('render_state_submitFailed'); - return [ - this.render_part_message('page_new_order_submit_failed_message', 'cloud-offline-outline'), - this.render_part_btn_submit_failed(), - ]; + protected render_part_btn_select_product(): unknown { + return html` +
+ ${message('select_product_submit_button')} +
+ `; } protected render_part_btn_product(): unknown { - return html`
- ${message('page_new_order_edit_items')} -
`; + return html` +
+ + ${message('page_new_order_edit_items')} + +
+ `; } protected render_part_btn_shipping_edit(): unknown { return html`
- ${message('page_new_order_shipping_edit')} + ${message('page_new_order_shipping_edit')}
`; } protected render_part_btn_shipping_submit(): unknown { return html`
- ${message('page_new_order_shipping_submit')} + ${message('page_new_order_shipping_submit')}
`; } @@ -130,8 +471,9 @@ export class AlwatrPageNewOrder extends StateMachineMixin( ${message('page_new_order_submit')} + ?disabled=${!this._stateMachine.context.order.itemList?.length} + >${message('page_new_order_submit')} `; } @@ -139,14 +481,12 @@ export class AlwatrPageNewOrder extends StateMachineMixin( protected render_part_btn_submit_success(): unknown { return html`
- ${message('page_new_order_detail_button')} - ${message('page_new_order_headline')} + ${message('page_new_order_detail_button')} + ${message('page_new_order_headline')}
`; } @@ -154,10 +494,9 @@ export class AlwatrPageNewOrder extends StateMachineMixin( protected render_part_btn_submit_failed(): unknown { return html`
- ${message('page_new_order_retry_button')} + ${message('page_new_order_retry_button')}
`; } @@ -165,15 +504,42 @@ export class AlwatrPageNewOrder extends StateMachineMixin( protected render_part_btn_final_submit(): unknown { return html`
- ${message('page_new_order_edit')} - ${message('page_new_order_submit_final')} + ${message('page_new_order_edit')} + ${message('page_new_order_submit_final')}
`; } + + protected validateOrder(): boolean { + try { + validator(orderInfoSchema, this._stateMachine.context.order, true); + return true; + } + catch (err) { + const _err = err as Error & {cause?: Record}; + this._logger.incident('validateOrder', _err.name, 'validation failed', _err); + if (_err.cause?.itemPath?.indexOf('shippingInfo') !== -1) { + snackbarSignalTrigger.request({ + message: message('page_new_order_shipping_info_not_valid_message'), + }); + } + else { + snackbarSignalTrigger.request({ + message: message('page_new_order_order_not_valid_message'), + }); + } + return false; + } + } + + protected qtyUpdate(orderItem: OrderItem, add: number): void { + const qty = orderItem.qty + add; + if (qty <= 0) return; + orderItem.qty = qty; + this._stateMachine.transition('QTY_UPDATE'); + } } diff --git a/uniquely/com-pwa/src/ui/page/order-tracking.ts b/uniquely/com-pwa/src/ui/page/order-tracking.ts index 0303c8c8d..cac33c176 100644 --- a/uniquely/com-pwa/src/ui/page/order-tracking.ts +++ b/uniquely/com-pwa/src/ui/page/order-tracking.ts @@ -12,7 +12,7 @@ import { } from '@alwatr/element'; import {message} from '@alwatr/i18n'; import '@alwatr/icon'; -import {topAppBarContextProvider} from '@alwatr/pwa-helper/src/context.js'; +import {topAppBarContextProvider} from '@alwatr/pwa-helper/context.js'; import {redirect} from '@alwatr/router'; import {eventListener} from '@alwatr/signal'; import '@alwatr/ui-kit/button/button.js'; @@ -26,7 +26,7 @@ import {orderStorageContextConsumer} from '../../manager/context.js'; import '../stuff/order-status-box.js'; import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; -import type {Order} from '@alwatr/type/src/customer-order-management.js'; +import type {Order} from '@alwatr/type/customer-order-management.js'; declare global { interface HTMLElementTagNameMap { @@ -93,7 +93,6 @@ export class AlwatrPageOrderTracking extends UnresolvedMixin(LocalizeMixin(Signa LOADED_SUCCESS: { target: 'tracking', condition: () => { - this._logger.logMethod('state-pending-condition'); if (this._stateMachine.context.orderStorage == null) return false; return true; }, From 3b1df45c64620533ced198206a74e910b12c98e1 Mon Sep 17 00:00:00 2001 From: "S. Amir Mohammad Najafi" Date: Thu, 9 Mar 2023 20:00:22 +0330 Subject: [PATCH 19/85] refactor(com-pwa): new state machine for select product (not-completed) --- .../com-pwa/src/ui/stuff/order-detail-base.ts | 1 - .../com-pwa/src/ui/stuff/select-product.ts | 35 +++++++------------ 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/uniquely/com-pwa/src/ui/stuff/order-detail-base.ts b/uniquely/com-pwa/src/ui/stuff/order-detail-base.ts index 84d5eff15..e4af7cbd5 100644 --- a/uniquely/com-pwa/src/ui/stuff/order-detail-base.ts +++ b/uniquely/com-pwa/src/ui/stuff/order-detail-base.ts @@ -18,7 +18,6 @@ import '@alwatr/ui-kit/card/surface.js'; import './order-shipping-form.js'; import './order-status-box.js'; import {config} from '../../config.js'; -import {qtyUpdate} from '../../manager/controller/new-order.js'; import type {AlwatrDocumentStorage} from '@alwatr/type'; import type {Order, OrderShippingInfo, OrderDraft, OrderItem, Product} from '@alwatr/type/customer-order-management.js'; diff --git a/uniquely/com-pwa/src/ui/stuff/select-product.ts b/uniquely/com-pwa/src/ui/stuff/select-product.ts index 319fdb428..f4162728f 100644 --- a/uniquely/com-pwa/src/ui/stuff/select-product.ts +++ b/uniquely/com-pwa/src/ui/stuff/select-product.ts @@ -7,16 +7,17 @@ import { html, mapObject, UnresolvedMixin, + property, } from '@alwatr/element'; -import {message} from '@alwatr/i18n'; import '@alwatr/ui-kit/button/button.js'; import '@alwatr/ui-kit/card/product-card.js'; import {config} from '../../config.js'; -import {pageNewOrderStateMachine, buttons} from '../../manager/controller/new-order.js'; +import type {OrderDraft} from '@alwatr/type/customer-order-management.js'; import type {AlwatrProductCard, ProductCartContent} from '@alwatr/ui-kit/card/product-card.js'; + declare global { interface HTMLElementTagNameMap { 'alwatr-select-product': AlwatrSelectProduct; @@ -50,39 +51,27 @@ export class AlwatrSelectProduct extends LocalizeMixin(SignalMixin(UnresolvedMix } `; + @property() + protected order?: OrderDraft; + selectedRecord: Record = {}; override render(): unknown { this._logger.logMethod('render'); + this._updateSelectedRecord(); - return [ - this.render_part_product_list(), - this.render_part_submit(), - ]; + return this.render_part_product_list(); } private _updateSelectedRecord(): void { this._logger.logMethod('_updateSelectedRecord'); - const order = pageNewOrderStateMachine.context.order; this.selectedRecord = {}; - if (!order?.itemList?.length) return; - for (const item of order.itemList) { + if (!this.order?.itemList?.length) return; + for (const item of this.order.itemList) { this.selectedRecord[item.productId] = true; } } - protected render_part_submit(): unknown { - return html` -
- ${message('select_product_submit_button')} - `; - } - protected render_part_product_list(): unknown { this._logger.logMethod('render_part_product_list'); @@ -91,9 +80,9 @@ export class AlwatrSelectProduct extends LocalizeMixin(SignalMixin(UnresolvedMix if (productStorage == null || priceStorage == null || finalPriceStorage == null) { return this._logger.accident( 'render_part_product_list', - 'contenx_not_valid', + 'context_not_valid', 'Some context not valid', - pageNewOrderStateMachine.context, + this.order, ); } From f39d617f106e03748d8ed9f539a77f4e810765b5 Mon Sep 17 00:00:00 2001 From: Mohammad Honarvar Date: Thu, 9 Mar 2023 20:09:29 +0330 Subject: [PATCH 20/85] fix(element): check type of `actions` --- ui/element/src/reactive-controllers/finite-state-machine.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/element/src/reactive-controllers/finite-state-machine.ts b/ui/element/src/reactive-controllers/finite-state-machine.ts index 6f5e0cd85..e85de0dd9 100644 --- a/ui/element/src/reactive-controllers/finite-state-machine.ts +++ b/ui/element/src/reactive-controllers/finite-state-machine.ts @@ -60,7 +60,9 @@ export class FiniteStateMachineController< if ('actions' in this.config.signalRecord![signalId]) { // TODO: Check array type of `actions` - listenerCallback = this.config.signalRecord?.[signalId].actions as ListenerFunction; + if (!Array.isArray(this.config.signalRecord?.[signalId].actions)) { + listenerCallback = this.config.signalRecord?.[signalId].actions as ListenerFunction; + } } if (listenerCallback) { From e6f224dab6a291316f22b3c2b1d8702dfcf535d4 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 9 Mar 2023 22:26:36 +0330 Subject: [PATCH 21/85] lint: make eslint happy --- core/fsm/src/type.ts | 2 +- .../reactive-controllers/finite-state-machine.ts | 15 ++++++++------- uniquely/com-pwa/src/ui/page/new-order.ts | 3 ++- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/core/fsm/src/type.ts b/core/fsm/src/type.ts index 39f370d46..723d7f8bf 100644 --- a/core/fsm/src/type.ts +++ b/core/fsm/src/type.ts @@ -59,7 +59,7 @@ export interface FsmConfig MaybePromise>; + actions?: MaybeArray<(signalDetail: unknown) => MaybePromise>; transition?: keyof FsmConfig['stateRecord'][ keyof FsmConfig['stateRecord'] ]['on']; diff --git a/ui/element/src/reactive-controllers/finite-state-machine.ts b/ui/element/src/reactive-controllers/finite-state-machine.ts index e85de0dd9..87838c9ff 100644 --- a/ui/element/src/reactive-controllers/finite-state-machine.ts +++ b/ui/element/src/reactive-controllers/finite-state-machine.ts @@ -46,22 +46,23 @@ export class FiniteStateMachineController< } hostConnected(): void { - if (this.config.signalRecord == null) return; + const signalRecord = this.config.signalRecord; + if (signalRecord == null) return; - for (const signalId of Object.keys(this.config.signalRecord)) { + for (const signalId of Object.keys(signalRecord)) { let listenerCallback: ListenerFunction | null = null; - if ('transition' in this.config.signalRecord![signalId]) { + if ('transition' in signalRecord[signalId]) { listenerCallback = (): void => { - this.transition(this.config.signalRecord![signalId].transition as TEventId); + this.transition(signalRecord[signalId].transition as TEventId); }; } - if ('actions' in this.config.signalRecord![signalId]) { + if ('actions' in signalRecord[signalId]) { // TODO: Check array type of `actions` - if (!Array.isArray(this.config.signalRecord?.[signalId].actions)) { - listenerCallback = this.config.signalRecord?.[signalId].actions as ListenerFunction; + if (!Array.isArray(signalRecord?.[signalId].actions)) { + listenerCallback = signalRecord?.[signalId].actions as ListenerFunction; } } diff --git a/uniquely/com-pwa/src/ui/page/new-order.ts b/uniquely/com-pwa/src/ui/page/new-order.ts index b8d7f4800..c807f13bd 100644 --- a/uniquely/com-pwa/src/ui/page/new-order.ts +++ b/uniquely/com-pwa/src/ui/page/new-order.ts @@ -229,7 +229,8 @@ export class AlwatrPageNewOrder extends UnresolvedMixin(AlwatrOrderDetailBase) { target: 'submitSuccess', actions: () => { localStorage.removeItem(newOrderLocalStorageKey); - // TODO: this._stateMachine.context.order = getLocalStorageItem(newOrderLocalStorageKey, {id: 'new', status: 'draft'}); + // TODO: this._stateMachine.context.order = + // getLocalStorageItem(newOrderLocalStorageKey, {id: 'new', status: 'draft'}); }, }, SUBMIT_FAILED: { From 7835b9d3d842b2833bcb218a2b22d2e1c8d81b35 Mon Sep 17 00:00:00 2001 From: "S. Amir Mohammad Najafi" Date: Thu, 9 Mar 2023 22:36:44 +0330 Subject: [PATCH 22/85] refactor(com-pwa/new-order): use signalRecord in state machine --- uniquely/com-pwa/src/ui/page/new-order.ts | 122 +++++++++------------ uniquely/com-pwa/src/ui/page/order-list.ts | 4 +- 2 files changed, 55 insertions(+), 71 deletions(-) diff --git a/uniquely/com-pwa/src/ui/page/new-order.ts b/uniquely/com-pwa/src/ui/page/new-order.ts index c807f13bd..34e2449aa 100644 --- a/uniquely/com-pwa/src/ui/page/new-order.ts +++ b/uniquely/com-pwa/src/ui/page/new-order.ts @@ -1,7 +1,6 @@ import {customElement, FiniteStateMachineController, html, state, UnresolvedMixin} from '@alwatr/element'; import {message} from '@alwatr/i18n'; import {redirect} from '@alwatr/router'; -import {eventListener} from '@alwatr/signal'; import {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; import { Order, @@ -256,6 +255,59 @@ export class AlwatrPageNewOrder extends UnresolvedMixin(AlwatrOrderDetailBase) { }, }, }, + signalRecord: { + [buttons.submit.clickSignalId]: { + transition: 'SUBMIT', + }, + [buttons.submitShippingForm.clickSignalId]: { + transition: 'SUBMIT', + }, + [buttons.edit.clickSignalId]: { + transition: 'BACK', + }, + [buttons.submitFinal.clickSignalId]: { + transition: 'FINAL_SUBMIT', + }, + [buttons.editItems.clickSignalId]: { + transition: 'FINAL_SUBMIT', + }, + [buttons.retry.clickSignalId]: { + transition: 'FINAL_SUBMIT', + }, + [buttons.editShippingForm.clickSignalId]: { + transition: 'EDIT_SHIPPING', + }, + [buttons.tracking.clickSignalId]: { + actions: () => { + const orderId = this._stateMachine.context.registeredOrderId as string; + this._stateMachine.transition('NEW_ORDER'); + redirect({sectionList: ['order-tracking', orderId]}); + }, + }, + [buttons.detail.clickSignalId]: { + actions: () => { + const orderId = this._stateMachine.context.registeredOrderId as string; + this._stateMachine.transition('NEW_ORDER'); + redirect({sectionList: ['order-detail', orderId]}); + }, + }, + [buttons.newOrder.clickSignalId]: { + actions: () => { + this._stateMachine.transition('NEW_ORDER'); + redirect('/new-order/'); + }, + }, + 'order_item_qty_add': { + actions: (event) => { + this.qtyUpdate((event as ClickSignalType).detail, 1); // TODO: set type with action + }, + }, + 'order_item_qty_remove': { + actions: (event) => { + this.qtyUpdate((event as ClickSignalType).detail, -1); + }, + }, + }, } as const); @state() @@ -286,74 +338,6 @@ export class AlwatrPageNewOrder extends UnresolvedMixin(AlwatrOrderDetailBase) { this._stateMachine.transition('LOADED_SUCCESS', {finalPriceStorage}); }), ); - this._signalListenerList.push( - eventListener.subscribe(buttons.submit.clickSignalId, () => { - this._stateMachine.transition('SUBMIT'); - }), - ); - this._signalListenerList.push( - eventListener.subscribe(buttons.edit.clickSignalId, () => { - this._stateMachine.transition('BACK'); - }), - ); - this._signalListenerList.push( - eventListener.subscribe(buttons.submitFinal.clickSignalId, () => { - this._stateMachine.transition('FINAL_SUBMIT'); - }), - ); - this._signalListenerList.push( - eventListener.subscribe(buttons.editItems.clickSignalId, () => { - this._stateMachine.transition('SELECT_PRODUCT'); - }), - ); - this._signalListenerList.push( - eventListener.subscribe(buttons.editShippingForm.clickSignalId, () => { - this._stateMachine.transition('EDIT_SHIPPING'); - }), - ); - this._signalListenerList.push( - eventListener.subscribe(buttons.submitShippingForm.clickSignalId, () => { - this._stateMachine.transition('SUBMIT'); - }), - ); - this._signalListenerList.push( - eventListener.subscribe(buttons.tracking.clickSignalId, () => { - const orderId = this._stateMachine.context.registeredOrderId as string; - this._stateMachine.transition('NEW_ORDER'); - redirect({sectionList: ['order-tracking', orderId]}); - }), - ); - this._signalListenerList.push( - eventListener.subscribe(buttons.detail.clickSignalId, () => { - const orderId = this._stateMachine.context.registeredOrderId as string; - this._stateMachine.transition('NEW_ORDER'); - redirect({sectionList: ['order-detail', orderId]}); - }), - ); - this._signalListenerList.push( - eventListener.subscribe(buttons.newOrder.clickSignalId, () => { - this._stateMachine.transition('NEW_ORDER'); - redirect('/new-order/'); - }), - ); - - this._signalListenerList.push( - eventListener.subscribe(buttons.retry.clickSignalId, async () => { - this._stateMachine.transition('FINAL_SUBMIT'); - }), - ); - - this._signalListenerList.push( - eventListener.subscribe>('order_item_qty_add', (event) => { - this.qtyUpdate(event.detail, 1); - }), - ); - - this._signalListenerList.push( - eventListener.subscribe>('order_item_qty_remove', (event) => { - this.qtyUpdate(event.detail, -1); - }), - ); } protected override render(): unknown { diff --git a/uniquely/com-pwa/src/ui/page/order-list.ts b/uniquely/com-pwa/src/ui/page/order-list.ts index 608df105c..634b5503b 100644 --- a/uniquely/com-pwa/src/ui/page/order-list.ts +++ b/uniquely/com-pwa/src/ui/page/order-list.ts @@ -130,9 +130,9 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix }, }, [buttons.orderDetail.clickSignalId]: { - actions: (event: ClickSignalType) => { + actions: (event) => { redirect({ - sectionList: ['order-detail', event.detail.id], + sectionList: ['order-detail', (event as ClickSignalType).detail.id], }); }, }, From 0133d93edca78d3604d416b1d484cc2afec92be5 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 9 Mar 2023 23:50:48 +0330 Subject: [PATCH 23/85] refactor(fsm): StateRecord, signalRecord types --- core/fsm/src/type.ts | 87 ++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/core/fsm/src/type.ts b/core/fsm/src/type.ts index 723d7f8bf..d4b2b5297 100644 --- a/core/fsm/src/type.ts +++ b/core/fsm/src/type.ts @@ -1,6 +1,6 @@ -import type {MaybeArray, MaybePromise, StringifyableRecord} from '@alwatr/type'; +import type {SingleOrArray, MaybePromise, StringifyableRecord} from '@alwatr/type'; -export interface FsmConfig { +export type FsmConfig = { /** * Machine ID (It is used in the state change signal identifier, so it must be unique). */ @@ -19,56 +19,55 @@ export interface FsmConfig MaybePromise>; - - /** - * On state entry actions - */ - entry?: MaybeArray<() => MaybePromise>; - - /** - * An object mapping eventId to state. - * - * Example: - * - * ```ts - * stateRecord: { - * on: { - * TIMER: { - * target: 'green', - * condition: () => car.gas > 0, - * actions: () => car.go(), - * } - * } - * } - * ``` - */ - on: { - [E in TEventId]?: TransitionConfig; - }; - }; - }; + stateRecord: StateRecord; /** * A list of signals ... */ signalRecord?: { [signalId: string]: { - actions?: MaybeArray<(signalDetail: unknown) => MaybePromise>; - transition?: keyof FsmConfig['stateRecord'][ - keyof FsmConfig['stateRecord'] - ]['on']; + actions?: SingleOrArray<(signalDetail: unknown) => MaybePromise>; + transition?: keyof StateRecord[TState]['on']; } - } + }, +} + +export type StateRecord = { + [S in TState | '$all']: { + /** + * On state exit actions + */ + exit?: SingleOrArray<() => MaybePromise>; + + /** + * On state entry actions + */ + entry?: SingleOrArray<() => MaybePromise>; + + /** + * An object mapping eventId to state. + * + * Example: + * + * ```ts + * stateRecord: { + * on: { + * TIMER: { + * target: 'green', + * condition: () => car.gas > 0, + * actions: () => car.go(), + * } + * } + * } + * ``` + */ + on: { + [E in TEventId]?: TransitionConfig | undefined; + }; + }; } -export interface StateContext { - [T: string]: string | undefined; +export type StateContext = { /** * Current state */ @@ -86,5 +85,5 @@ export interface StateContext { export interface TransitionConfig { target?: TState; condition?: () => MaybePromise; - actions?: MaybeArray<() => MaybePromise>; + actions?: SingleOrArray<() => MaybePromise>; } From 777ae459f2b77f79696daf3a0ca355d6d78e57d3 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 9 Mar 2023 23:51:12 +0330 Subject: [PATCH 24/85] fix(fsm): run init entry actions --- core/fsm/src/core.ts | 54 +++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index 925294783..25619aab8 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -3,7 +3,7 @@ import {contextConsumer} from '@alwatr/signal'; import {dispatch} from '@alwatr/signal/core.js'; import type {FsmConfig, StateContext} from './type.js'; -import type {MaybeArray, MaybePromise, StringifyableRecord} from '@alwatr/type'; +import type {SingleOrArray, MaybePromise, StringifyableRecord} from '@alwatr/type'; export type {FsmConfig, StateContext}; @@ -17,11 +17,7 @@ export class FiniteStateMachine< TEventId extends string = string, TContext extends StringifyableRecord = StringifyableRecord > { - state: StateContext = { - target: this.config.initial, - from: this.config.initial, - by: 'INIT', - }; + state: StateContext = this.setState(this.config.initial, 'INIT'); context = this.config.context; @@ -29,30 +25,23 @@ export class FiniteStateMachine< protected _logger = createLogger(`alwatr/fsm:${this.config.id}`); - protected async setState(target: TState, by: TEventId): Promise { - const state = (this.state = { + protected setState(target: TState, by: TEventId | 'INIT'): StateContext { + const state: StateContext = this.state = { target, from: this.signal.getValue()?.target ?? target, by, - }); + }; dispatch>(this.signal.id, state, {debounce: 'NextCycle'}); - if (state.from !== state.target) { - await this.execActions(this.config.stateRecord.$all.exit); - await this.execActions(this.config.stateRecord[state.from]?.exit); - await this.execActions(this.config.stateRecord.$all.entry); - await this.execActions(this.config.stateRecord[state.target]?.entry); - } - await this.execActions( - this.config.stateRecord[state.from]?.on[state.by]?.actions ?? - this.config.stateRecord.$all.on[state.by]?.actions, - ); + this.execAllActions().catch((err) => this._logger.error('myMethod', 'error_code', err)); + + return state; } constructor(public readonly config: Readonly>) { this._logger.logMethodArgs('constructor', config); - dispatch>(this.signal.id, this.state, {debounce: 'NextCycle'}); + if (!config.stateRecord[config.initial]) { this._logger.error('constructor', 'invalid_initial_state', config); } @@ -95,7 +84,30 @@ export class FiniteStateMachine< await this.setState(transitionConfig.target, event); } - protected async execActions(actions?: MaybeArray<() => MaybePromise>): Promise { + protected async execAllActions(): Promise { + const state = this.state; + const stateRecord = this.config.stateRecord; + + if (state.by === 'INIT') { + await this.execActions(stateRecord.$all.entry); + await this.execActions(stateRecord[state.target]?.entry); + return; + } + // else + if (state.from !== state.target) { + await this.execActions(stateRecord.$all.exit); + await this.execActions(stateRecord[state.from]?.exit); + await this.execActions(stateRecord.$all.entry); + await this.execActions(stateRecord[state.target]?.entry); + } + await this.execActions( + stateRecord[state.from]?.on[state.by]?.actions ?? + stateRecord.$all.on[state.by]?.actions, + ); + } + + + protected async execActions(actions?: SingleOrArray<() => MaybePromise>): Promise { if (actions == null) return; try { From 1f4e63e5f8aad1c59a0d75134018e487c58e3118 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Fri, 10 Mar 2023 02:50:33 +0330 Subject: [PATCH 25/85] refactor(fsm): signalList array --- core/fsm/src/core.ts | 64 +++++++++++++++++++++++++++++++++++++------- core/fsm/src/type.ts | 33 ++++++++++++++++------- 2 files changed, 78 insertions(+), 19 deletions(-) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index 25619aab8..e3593e921 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -1,8 +1,9 @@ import {createLogger, globalAlwatr} from '@alwatr/logger'; -import {contextConsumer} from '@alwatr/signal'; +import {contextConsumer, eventListener} from '@alwatr/signal'; import {dispatch} from '@alwatr/signal/core.js'; import type {FsmConfig, StateContext} from './type.js'; +import type {ListenerSpec} from '@alwatr/signal/src/type.js'; import type {SingleOrArray, MaybePromise, StringifyableRecord} from '@alwatr/type'; export type {FsmConfig, StateContext}; @@ -26,11 +27,11 @@ export class FiniteStateMachine< protected _logger = createLogger(`alwatr/fsm:${this.config.id}`); protected setState(target: TState, by: TEventId | 'INIT'): StateContext { - const state: StateContext = this.state = { + const state: StateContext = (this.state = { target, from: this.signal.getValue()?.target ?? target, by, - }; + }); dispatch>(this.signal.id, state, {debounce: 'NextCycle'}); @@ -76,12 +77,12 @@ export class FiniteStateMachine< return; } - if (await this.callFunction(transitionConfig.condition) === false) { + if ((await this.callFunction(transitionConfig.condition)) === false) { return; } transitionConfig.target ??= fromState; - await this.setState(transitionConfig.target, event); + this.setState(transitionConfig.target, event); } protected async execAllActions(): Promise { @@ -100,13 +101,9 @@ export class FiniteStateMachine< await this.execActions(stateRecord.$all.entry); await this.execActions(stateRecord[state.target]?.entry); } - await this.execActions( - stateRecord[state.from]?.on[state.by]?.actions ?? - stateRecord.$all.on[state.by]?.actions, - ); + await this.execActions(stateRecord[state.from]?.on[state.by]?.actions ?? stateRecord.$all.on[state.by]?.actions); } - protected async execActions(actions?: SingleOrArray<() => MaybePromise>): Promise { if (actions == null) return; @@ -130,4 +127,51 @@ export class FiniteStateMachine< if (typeof fn !== 'function') return; return fn(); } + + private _listenerList: Array = []; + + protected subscribeSignals(): void { + this.unsubscribeSignals(); + const signalList = this.config.signalList; + if (signalList == null) return; + + for (const signalConfig of signalList) { + const actions = + signalConfig.actions ?? + ((signalDetail: unknown): void => { + let context = undefined; + if (signalConfig.contextName) { + context = >{ + [signalConfig.contextName]: signalDetail, + }; + } + this.transition(signalConfig.transition as TEventId, context); + }); + + if (Array.isArray(actions)) { + for (const action of actions) { + this._listenerList.push( + eventListener.subscribe(signalConfig.signalId, action, { + receivePrevious: signalConfig.receivePrevious ?? 'No', + }), + ); + } + } + else { + this._listenerList.push( + eventListener.subscribe(signalConfig.signalId, actions, { + receivePrevious: signalConfig.receivePrevious ?? 'No', + }), + ); + } + } + } + + protected unsubscribeSignals(): void { + if (this._listenerList.length === 0) return; + for (const listener of this._listenerList) { + eventListener.unsubscribe(listener); + } + this._listenerList.length = 0; + } } diff --git a/core/fsm/src/type.ts b/core/fsm/src/type.ts index d4b2b5297..b0e71fe84 100644 --- a/core/fsm/src/type.ts +++ b/core/fsm/src/type.ts @@ -1,3 +1,5 @@ +import {DebounceType} from '@alwatr/signal'; + import type {SingleOrArray, MaybePromise, StringifyableRecord} from '@alwatr/type'; export type FsmConfig = { @@ -24,13 +26,26 @@ export type FsmConfig MaybePromise>; - transition?: keyof StateRecord[TState]['on']; - } - }, -} + signalList?: Array< + { + signalId: string; + receivePrevious?: DebounceType; + } & ( + | { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + actions: SingleOrArray<(signalDetail: any) => MaybePromise>; + transition?: never; + } + | { + transition: keyof StateRecord[TState]['on']; + contextName?: keyof TContext; + actions?: never; + } + ) + >; + + autoSignalUnsubscribe?: boolean; +}; export type StateRecord = { [S in TState | '$all']: { @@ -65,7 +80,7 @@ export type StateRecord = { [E in TEventId]?: TransitionConfig | undefined; }; }; -} +}; export type StateContext = { /** @@ -80,7 +95,7 @@ export type StateContext = { * Transition event */ by: TEventId | 'INIT'; -} +}; export interface TransitionConfig { target?: TState; From d53202c7daf3f55682e47e78f9a8a1b8bc70441d Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Fri, 10 Mar 2023 02:50:47 +0330 Subject: [PATCH 26/85] fix(fsm/demo): signalList --- demo/finite-state-machine/light-machine.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/demo/finite-state-machine/light-machine.ts b/demo/finite-state-machine/light-machine.ts index 1acec657c..c1acc7ed9 100644 --- a/demo/finite-state-machine/light-machine.ts +++ b/demo/finite-state-machine/light-machine.ts @@ -59,6 +59,12 @@ const lightMachine = new FiniteStateMachine({ }, }, }, + signalList: [ + { + signalId: 'ali', + actions: (a): void => console.log(a), + }, + ], }); From 23efdf25d2bbb768b70fd65f45f9fed3affcbd15 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Fri, 10 Mar 2023 02:51:09 +0330 Subject: [PATCH 27/85] fix(element): remove old fsm mixin --- ui/element/src/mixins/state-machine.ts | 59 -------------------------- 1 file changed, 59 deletions(-) delete mode 100644 ui/element/src/mixins/state-machine.ts diff --git a/ui/element/src/mixins/state-machine.ts b/ui/element/src/mixins/state-machine.ts deleted file mode 100644 index 82b172d94..000000000 --- a/ui/element/src/mixins/state-machine.ts +++ /dev/null @@ -1,59 +0,0 @@ -import {untilNextFrame} from '@alwatr/util'; - -import {nothing} from '../lit.js'; - -import type {SignalMixinInterface} from './signal.js'; -import type {FiniteStateMachine} from '@alwatr/fsm'; -import type {Constructor} from '@alwatr/type'; - -export declare class StateMachineMixinInterface extends SignalMixinInterface { - protected stateMachine: TMachine; - protected stateUpdated(state: TMachine['state']): void; - protected render_state_unresolved(): unknown; - protected render_state_resolving(): unknown; -} - -export function StateMachineMixin, TMachine extends FiniteStateMachine>( - stateMachine: TMachine, - superClass: T, -): Constructor> & T { - class StateMachineMixinClass extends superClass { - protected stateMachine = stateMachine; - - protected render_state_unresolved(): unknown { - return nothing; - } - protected render_state_resolving(): unknown { - return nothing; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(...args: any[]) { - super(...args); - this.stateUpdated = this.stateUpdated.bind(this); - } - - override connectedCallback(): void { - super.connectedCallback(); - this.stateMachine.transition('CONNECTED'); - this._signalListenerList.push( - this.stateMachine.signal.subscribe(this.stateUpdated, {receivePrevious: 'NextCycle'}), - ); - } - - /** - * Subscribe to this.stateMachine.signal event. - */ - protected stateUpdated(state: TMachine['state']): void { - this.setAttribute('state', state.target); - this.requestUpdate(); - } - - protected override async scheduleUpdate(): Promise { - await untilNextFrame(); - super.scheduleUpdate(); - } - } - - return StateMachineMixinClass as unknown as Constructor> & T; -} From e51aaa241ea21a91df4e7399a4c7801be41ded49 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Fri, 10 Mar 2023 02:52:23 +0330 Subject: [PATCH 28/85] feat(element): refactor fsm controller with new fsm api --- ui/element/src/index.ts | 1 - .../finite-state-machine.ts | 35 ++++--------------- 2 files changed, 7 insertions(+), 29 deletions(-) diff --git a/ui/element/src/index.ts b/ui/element/src/index.ts index f54fe8e01..87b3a35e9 100644 --- a/ui/element/src/index.ts +++ b/ui/element/src/index.ts @@ -9,7 +9,6 @@ export * from './mixins/logging.js'; export * from './mixins/signal.js'; export * from './mixins/toggle.js'; export * from './mixins/unresolved.js'; -export * from './mixins/state-machine.js'; export * from './mixins/schedule-update-to-frame.js'; export * from './directives/map.js'; diff --git a/ui/element/src/reactive-controllers/finite-state-machine.ts b/ui/element/src/reactive-controllers/finite-state-machine.ts index 87838c9ff..64bb8d062 100644 --- a/ui/element/src/reactive-controllers/finite-state-machine.ts +++ b/ui/element/src/reactive-controllers/finite-state-machine.ts @@ -13,7 +13,6 @@ export class FiniteStateMachineController< TContext extends StringifyableRecord > extends FiniteStateMachine implements ReactiveController { // FIXME: Choose a proper name - private _listenerList: ListenerSpec[] = []; constructor( private _host: LoggerMixinInterface, @@ -21,6 +20,9 @@ export class FiniteStateMachineController< ) { super(config); this._host.addController(this); + if (!this.config.autoSignalUnsubscribe) { + this.subscribeSignals(); + } } render(states: {[P in TState]?: (() => unknown) | TState}): unknown { @@ -46,37 +48,14 @@ export class FiniteStateMachineController< } hostConnected(): void { - const signalRecord = this.config.signalRecord; - if (signalRecord == null) return; - - for (const signalId of Object.keys(signalRecord)) { - let listenerCallback: ListenerFunction | null = null; - - if ('transition' in signalRecord[signalId]) { - listenerCallback = (): void => { - this.transition(signalRecord[signalId].transition as TEventId); - }; - } - - if ('actions' in signalRecord[signalId]) { - // TODO: Check array type of `actions` - - if (!Array.isArray(signalRecord?.[signalId].actions)) { - listenerCallback = signalRecord?.[signalId].actions as ListenerFunction; - } - } - - if (listenerCallback) { - this._listenerList.push( - eventListener.subscribe(signalId, listenerCallback), - ); - } + if (this.config.autoSignalUnsubscribe) { + this.subscribeSignals(); } } hostDisconnected(): void { - for (const listener of this._listenerList) { - eventListener.unsubscribe(listener); + if (this.config.autoSignalUnsubscribe) { + this.unsubscribeSignals(); } } } From 3bf753bdd4e56098633d67a63cf2c741a8ff0ccf Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Fri, 10 Mar 2023 02:52:48 +0330 Subject: [PATCH 29/85] feat(com-pwa/order-list): refactor woth new fsm --- uniquely/com-pwa/src/ui/page/order-list.ts | 34 ++++++++++++---------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/uniquely/com-pwa/src/ui/page/order-list.ts b/uniquely/com-pwa/src/ui/page/order-list.ts index 634b5503b..dc87c8b1d 100644 --- a/uniquely/com-pwa/src/ui/page/order-list.ts +++ b/uniquely/com-pwa/src/ui/page/order-list.ts @@ -77,14 +77,14 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix }, stateRecord: { $all: { - entry: () => { + entry: (): void => { this.gotState = this._stateMachine.state.target; }, on: { }, }, pending: { - entry: () => { + entry: (): void => { this._logger.logMethod('state.pending.entry'); if (orderStorageContextConsumer.getValue() == null) { fetchOrderStorage(); @@ -118,26 +118,34 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix }, }, }, - signalRecord: { - [buttons.reload.clickSignalId]: { + signalList: [ + { + signalId: buttons.reload.clickSignalId, transition: 'REQUEST_UPDATE', }, - [buttons.newOrder.clickSignalId]: { - actions: () => { + { + signalId: buttons.newOrder.clickSignalId, + actions: (): void => { redirect({ sectionList: ['new-order'], }); }, }, - [buttons.orderDetail.clickSignalId]: { - actions: (event) => { + { + signalId: buttons.orderDetail.clickSignalId, + actions: (event: ClickSignalType): void => { redirect({ sectionList: ['order-detail', (event as ClickSignalType).detail.id], }); }, }, - }, - } as const); + { + signalId: orderStorageContextConsumer.id, + transition: 'LOADED_SUCCESS', + contextName: 'orderStorage', + }, + ], + }); @state() gotState = this._stateMachine.state.target; @@ -150,12 +158,6 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix startIcon: buttons.backToHome, endIconList: [buttons.newOrder, buttons.reload], }); - - this._signalListenerList.push( - orderStorageContextConsumer.subscribe((orderStorage) => { - this._stateMachine.transition('LOADED_SUCCESS', {orderStorage}); - }), - ); } override render(): unknown { From 224799c5c664bcc11dac8061048c85708b3ba5ef Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Sat, 11 Mar 2023 01:20:19 +0330 Subject: [PATCH 30/85] fix(element): build issue --- ui/element/src/reactive-controllers/finite-state-machine.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ui/element/src/reactive-controllers/finite-state-machine.ts b/ui/element/src/reactive-controllers/finite-state-machine.ts index 64bb8d062..638e14f79 100644 --- a/ui/element/src/reactive-controllers/finite-state-machine.ts +++ b/ui/element/src/reactive-controllers/finite-state-machine.ts @@ -1,19 +1,15 @@ import {FiniteStateMachine, type FsmConfig} from '@alwatr/fsm'; -import {eventListener, ListenerSpec} from '@alwatr/signal'; import {nothing, type ReactiveController} from '../lit.js'; import type {LoggerMixinInterface} from '../mixins/logging.js'; -import type {ListenerFunction} from '@alwatr/signal/type.js'; -import type {Stringifyable, StringifyableRecord} from '@alwatr/type'; +import type {StringifyableRecord} from '@alwatr/type'; export class FiniteStateMachineController< TState extends string, TEventId extends string, TContext extends StringifyableRecord > extends FiniteStateMachine implements ReactiveController { - // FIXME: Choose a proper name - constructor( private _host: LoggerMixinInterface, config: Readonly>, From e7435c870a054b0ec3e4004f13c6db7610610be0 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Sat, 11 Mar 2023 01:47:03 +0330 Subject: [PATCH 31/85] fix(fsm): last reported bugs in set state --- core/fsm/src/core.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index e3593e921..774ad4c1b 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -18,24 +18,24 @@ export class FiniteStateMachine< TEventId extends string = string, TContext extends StringifyableRecord = StringifyableRecord > { - state: StateContext = this.setState(this.config.initial, 'INIT'); - context = this.config.context; - signal = contextConsumer.bind>('finite-state-machine-' + this.config.id); + signal = contextConsumer.bind>('finite_state_machine_' + this.config.id); protected _logger = createLogger(`alwatr/fsm:${this.config.id}`); + state: StateContext = this.setState(this.config.initial, 'INIT'); + protected setState(target: TState, by: TEventId | 'INIT'): StateContext { const state: StateContext = (this.state = { target, - from: this.signal.getValue()?.target ?? target, + from: this.signal?.getValue()?.target ?? target, by, }); dispatch>(this.signal.id, state, {debounce: 'NextCycle'}); - this.execAllActions().catch((err) => this._logger.error('myMethod', 'error_code', err)); + setTimeout(() => this.execAllActions(), 0); return state; } From cc524b8c6e25abd53ff436e342317d83c5dd7838 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Sat, 11 Mar 2023 01:49:46 +0330 Subject: [PATCH 32/85] fix(com-pwa/order-list): state machine id --- uniquely/com-pwa/src/ui/page/order-list.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uniquely/com-pwa/src/ui/page/order-list.ts b/uniquely/com-pwa/src/ui/page/order-list.ts index dc87c8b1d..07947ae0a 100644 --- a/uniquely/com-pwa/src/ui/page/order-list.ts +++ b/uniquely/com-pwa/src/ui/page/order-list.ts @@ -70,7 +70,7 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix `; private _stateMachine = new FiniteStateMachineController(this, { - id: 'fsm-order-list-' + this.ali, + id: 'order_list_' + this.ali, initial: 'pending', context: { orderStorage: | null>null, From f7db30bf5a90ff3d163f036b313a412a5149ff2b Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Sat, 11 Mar 2023 16:14:50 +0330 Subject: [PATCH 33/85] fix(fsm): autoSignalUnsubscribe type --- core/fsm/src/type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/fsm/src/type.ts b/core/fsm/src/type.ts index b0e71fe84..fc1c105bb 100644 --- a/core/fsm/src/type.ts +++ b/core/fsm/src/type.ts @@ -44,7 +44,7 @@ export type FsmConfig; - autoSignalUnsubscribe?: boolean; + autoSignalUnsubscribe?: true; }; export type StateRecord = { From c7c542331ba2e1c86b372948a393d237f93293c8 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Sat, 11 Mar 2023 16:21:09 +0330 Subject: [PATCH 34/85] feat(com/order-list): new state config --- uniquely/com-pwa/src/ui/page/order-list.ts | 99 ++++++++++++---------- 1 file changed, 55 insertions(+), 44 deletions(-) diff --git a/uniquely/com-pwa/src/ui/page/order-list.ts b/uniquely/com-pwa/src/ui/page/order-list.ts index 07947ae0a..dd38c8140 100644 --- a/uniquely/com-pwa/src/ui/page/order-list.ts +++ b/uniquely/com-pwa/src/ui/page/order-list.ts @@ -11,12 +11,12 @@ import { } from '@alwatr/element'; import {message} from '@alwatr/i18n'; import {redirect} from '@alwatr/router'; +import {requestableContextConsumer} from '@alwatr/signal'; import {Order} from '@alwatr/type/customer-order-management.js'; import '@alwatr/ui-kit/button/button.js'; import {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; -import {fetchOrderStorage} from '../../manager/context-provider/order-storage.js'; -import {orderStorageContextConsumer, topAppBarContextProvider} from '../../manager/context.js'; +import {topAppBarContextProvider} from '../../manager/context.js'; import '../stuff/order-list.js'; import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; @@ -27,7 +27,11 @@ declare global { } } -export const buttons = { +const orderStorageContextConsumer = requestableContextConsumer.bind>( + 'order-storage-context', +); + +const buttons = { backToHome: { icon: 'arrow-back-outline', flipRtl: true, @@ -64,7 +68,7 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix transform: opacity var(--sys-motion-duration-small); } - :host([state=reloading]) alwatr-order-list { + :host([state='reloading']) alwatr-order-list { opacity: var(--sys-surface-disabled-opacity); } `; @@ -80,48 +84,61 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix entry: (): void => { this.gotState = this._stateMachine.state.target; }, - on: { - }, + on: {}, }, pending: { entry: (): void => { - this._logger.logMethod('state.pending.entry'); - if (orderStorageContextConsumer.getValue() == null) { - fetchOrderStorage(); - } - if (this._stateMachine.context.orderStorage != null) { - this._stateMachine.transition('LOADED_SUCCESS'); + const orderContext = orderStorageContextConsumer.getValue(); + if (orderContext.state === 'initial') { + orderStorageContextConsumer.request(null); } }, on: { - LOADED_SUCCESS: { + context_request_initial: {}, + context_request_pending: {}, + context_request_error: { + target: 'contextError', + }, + context_request_complete: { target: 'list', }, + context_request_reloading: { + target: 'reloading', + }, + }, + }, + contextError: { + on: { + request_context: { + target: 'pending', + actions: (): void => orderStorageContextConsumer.request(null), + }, }, }, list: { on: { - REQUEST_UPDATE: { + request_context: { target: 'reloading', - actions: this._requestUpdateAction, + actions: (): void => orderStorageContextConsumer.request(null), }, }, }, reloading: { on: { - LOADED_SUCCESS: { + context_request_error: { + target: 'list', + actions: (): void => alert('context_request_error'), + }, + context_request_complete: { target: 'list', }, - // LOAD_FAILED: { - // target: 'list', - // }, }, }, }, signalList: [ { signalId: buttons.reload.clickSignalId, - transition: 'REQUEST_UPDATE', + transition: 'request_context', }, { signalId: buttons.newOrder.clickSignalId, @@ -135,15 +152,10 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix signalId: buttons.orderDetail.clickSignalId, actions: (event: ClickSignalType): void => { redirect({ - sectionList: ['order-detail', (event as ClickSignalType).detail.id], + sectionList: ['order-detail', event.detail.id], }); }, }, - { - signalId: orderStorageContextConsumer.id, - transition: 'LOADED_SUCCESS', - contextName: 'orderStorage', - }, ], }); @@ -153,6 +165,15 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix override connectedCallback(): void { super.connectedCallback(); + this._signalListenerList.push( + orderStorageContextConsumer.subscribe( + (context) => { + this._stateMachine.transition(`context_request_${context.state}`, {orderStorage: context.content}); + }, + {receivePrevious: 'NextCycle'}, + ), + ); + topAppBarContextProvider.setValue({ headlineKey: 'page_order_list_headline', startIcon: buttons.backToHome, @@ -163,7 +184,7 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix override render(): unknown { this._logger.logMethod('render'); return this._stateMachine.render({ - 'pending': () => { + pending: () => { const content: IconBoxContent = { tinted: 1, icon: 'cloud-download-outline', @@ -172,9 +193,14 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix return html``; }, - 'reloading': 'list', + contextError: () => { + // TODO: update me with alwatr-icon-box and retry button + return 'error!'; + }, + + reloading: 'list', - 'list': () => { + list: () => { return html` { - topAppBarContextProvider.setValue({ - headlineKey: 'loading', - startIcon: buttons.backToHome, - endIconList: [buttons.newOrder, buttons.reload], - }); - await fetchOrderStorage(); - topAppBarContextProvider.setValue({ - headlineKey: 'page_order_list_headline', - startIcon: buttons.backToHome, - endIconList: [buttons.newOrder, buttons.reload], - }); - this._stateMachine.transition('LOADED_SUCCESS'); - } } From 93f8ea31b8fa4f8845871a795eb2de107797f669 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Sat, 11 Mar 2023 16:27:10 +0330 Subject: [PATCH 35/85] fix(element/fsmc): all render state must defined --- .../src/reactive-controllers/finite-state-machine.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ui/element/src/reactive-controllers/finite-state-machine.ts b/ui/element/src/reactive-controllers/finite-state-machine.ts index 638e14f79..38c09ffbf 100644 --- a/ui/element/src/reactive-controllers/finite-state-machine.ts +++ b/ui/element/src/reactive-controllers/finite-state-machine.ts @@ -1,6 +1,6 @@ import {FiniteStateMachine, type FsmConfig} from '@alwatr/fsm'; -import {nothing, type ReactiveController} from '../lit.js'; +import {type ReactiveController} from '../lit.js'; import type {LoggerMixinInterface} from '../mixins/logging.js'; import type {StringifyableRecord} from '@alwatr/type'; @@ -21,17 +21,19 @@ export class FiniteStateMachineController< } } - render(states: {[P in TState]?: (() => unknown) | TState}): unknown { + render(states: {[P in TState]: (() => unknown) | TState}): unknown { this._logger.logMethodArgs('render', this.state.target); let renderFn = states[this.state.target]; + if (typeof renderFn === 'string') { renderFn = states[renderFn]; } + if (typeof renderFn === 'function') { return renderFn?.call(this._host); } - // else - return nothing; + + return; } hostUpdate(): void { From 045afa8de341bd6b1389da1e03bd439c517f8383 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Sat, 11 Mar 2023 19:15:57 +0330 Subject: [PATCH 36/85] feat(com/provider/storage): new provider base on new context state --- .../manager/context-provider/order-storage.ts | 112 ++++++++++++++---- 1 file changed, 86 insertions(+), 26 deletions(-) diff --git a/uniquely/com-pwa/src/manager/context-provider/order-storage.ts b/uniquely/com-pwa/src/manager/context-provider/order-storage.ts index 7be28dce3..a12213e44 100644 --- a/uniquely/com-pwa/src/manager/context-provider/order-storage.ts +++ b/uniquely/com-pwa/src/manager/context-provider/order-storage.ts @@ -1,37 +1,97 @@ -import {fetchContext} from '@alwatr/fetch'; -import {l18eReadyPromise} from '@alwatr/i18n'; -import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; +import {serviceRequest, type FetchOptions} from '@alwatr/fetch'; +import {contextConsumer, DispatchOptions, requestableContextProvider} from '@alwatr/signal'; import {config} from '../../config.js'; -import { - orderStorageContextConsumer, - userContextConsumer, -} from '../context.js'; import {logger} from '../logger.js'; -export const fetchOrderStorage = async (): Promise => { - logger.logMethod('fetchOrderStorage'); +import type {AlwatrDocumentStorage, User} from '@alwatr/type'; +import type {Order} from '@alwatr/type/customer-order-management.js'; + +const orderStorageContextProvider = requestableContextProvider.bind>( + 'order-storage-context', +); + +const userContextConsumer = contextConsumer.bind('user-context'); + +orderStorageContextProvider.setProvider(async () => { + logger.logMethod('requestOrderStorageContext'); + let context = orderStorageContextProvider.getValue(); + + if (context.state === 'pending' || context.state === 'reloading') return; const userContext = userContextConsumer.getValue() ?? (await userContextConsumer.untilChange()); + const dispatchOptions: Partial = {debounce: 'NextCycle'}; + const fetchOption: FetchOptions = { + ...config.fetchContextOptions, + url: config.api + '/order-list/', + queryParameters: { + userId: userContext.id, + }, + }; + + if (context.state === 'initial') { + context = {state: 'pending'}; + orderStorageContextProvider.setValue(context, dispatchOptions); + try { + fetchOption.cacheStrategy = 'cache_only'; + const response = await serviceRequest(fetchOption) as AlwatrDocumentStorage; + context = { + state: 'reloading', + content: response, + }; + orderStorageContextProvider.setValue(context, dispatchOptions); + } + catch (err) { + if ((err as Error).message === 'fetch_cache_not_found') { + logger.logOther('requestOrderStorageContext:', 'fetch_cache_not_found'); + } + else { + logger.error('requestOrderStorageContext', 'fetch_failed', err); + context = {state: 'error'}; + orderStorageContextProvider.setValue(context, dispatchOptions); + return; + } + } + } + + if (navigator.onLine === false) { + context = { + state: 'error', + content: context.content, // maybe offline exist + }; + orderStorageContextProvider.setValue(context, dispatchOptions); + return; + } try { - await fetchContext( - orderStorageContextConsumer.id, - { - ...config.fetchContextOptions, - url: config.api + '/order-list/', - queryParameters: { - userId: userContext.id, - }, - }, - {debounce: 'NextCycle'}, - ); + fetchOption.cacheStrategy = 'update_cache'; + const response = await serviceRequest(fetchOption) as AlwatrDocumentStorage; + if ( + context.content != null && + response.meta?.lastUpdated != undefined && + response.meta.lastUpdated === context.content.meta?.lastUpdated + ) { + // no changed + context = { + state: 'complete', + content: context.content, + }; + } + else { + context = { + state: 'complete', + content: response, + }; + } + orderStorageContextProvider.setValue(context, dispatchOptions); } catch (err) { - logger.error('fetchOrderStorage', 'fetch_failed', err); - await l18eReadyPromise; - await snackbarSignalTrigger.requestWithResponse({ - messageKey: 'fetch_failed', - }); + logger.error('fetchContext', 'fetch_failed', err); + context = { + state: 'error', + content: context.content, // maybe offline exist + }; + orderStorageContextProvider.setValue(context, dispatchOptions); + return; } -}; +}); From 758a4a7ddc9c558485e68fc5d9f6d8ea845334b2 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Sat, 11 Mar 2023 19:18:06 +0330 Subject: [PATCH 37/85] refactor(com): user provider --- uniquely/com-pwa/src/manager/context-provider/user.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/uniquely/com-pwa/src/manager/context-provider/user.ts b/uniquely/com-pwa/src/manager/context-provider/user.ts index 744baff28..c929157d3 100644 --- a/uniquely/com-pwa/src/manager/context-provider/user.ts +++ b/uniquely/com-pwa/src/manager/context-provider/user.ts @@ -1,9 +1,11 @@ +import {contextProvider} from '@alwatr/signal'; +import {User} from '@alwatr/type'; import {getLocalStorageItem} from '@alwatr/util'; -import {userContextProvider} from '../context.js'; +const userContextProvider = contextProvider.bind('user-context'); // demo -userContextProvider.setValue(getLocalStorageItem('user-context', { +userContextProvider.setValue(getLocalStorageItem(userContextProvider.id, { id: 'demo-123', fullName: 'Demo User', })); From adabe196882540cbe2c5901a6ac7cc9e33af5ea7 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Sat, 11 Mar 2023 19:18:22 +0330 Subject: [PATCH 38/85] fix(com-pwa): cleanup --- uniquely/com-pwa/src/manager/context.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/uniquely/com-pwa/src/manager/context.ts b/uniquely/com-pwa/src/manager/context.ts index e92cc84a5..1bca2c146 100644 --- a/uniquely/com-pwa/src/manager/context.ts +++ b/uniquely/com-pwa/src/manager/context.ts @@ -6,7 +6,7 @@ import { import {PageHomeContent} from '../type.js'; -import type {AlwatrDocumentStorage, User} from '@alwatr/type'; +import type {AlwatrDocumentStorage} from '@alwatr/type'; import type {Product, Order, ProductPrice} from '@alwatr/type/customer-order-management.js'; export * from '@alwatr/pwa-helper/context.js'; @@ -20,12 +20,6 @@ export const priceStorageContextConsumer = export const finalPriceStorageContextConsumer = contextConsumer.bind>('final-price-storage-tile-context'); -export const orderStorageContextConsumer = - contextConsumer.bind>('order-storage-context'); - -export const userContextProvider = contextProvider.bind('user-context'); -export const userContextConsumer = contextConsumer.bind(userContextProvider.id); - export const homePageContentContextProvider = contextProvider.bind('home-page-content-context'); export const homePageContentContextConsumer = From 8f286d295abd01e70539fd5dcba46ff1691c72d8 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Sat, 11 Mar 2023 19:19:19 +0330 Subject: [PATCH 39/85] feat(com/order-list): contextError state render --- uniquely/com-pwa/src/content/l18e-fa.json | 3 +++ uniquely/com-pwa/src/ui/page/order-list.ts | 21 ++++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/uniquely/com-pwa/src/content/l18e-fa.json b/uniquely/com-pwa/src/content/l18e-fa.json index e73c81cc7..c1b9534d0 100644 --- a/uniquely/com-pwa/src/content/l18e-fa.json +++ b/uniquely/com-pwa/src/content/l18e-fa.json @@ -14,6 +14,9 @@ "page_404_under_develope": "دردست ساخت", "page_404_under_develope_description": "برنامه نویسان سخت مشغول کارند...\nاندکی صبر سحر نزدیک است...\nدرضمن اگر رو من بزنی یک سفارش الکی ثبت می‌شود\nپس لطفا نزن!", + "fetch_failed_headline": "خطا!", + "fetch_failed_description": "خطا در اتصال به اینترنت! لطفا مجدد تلاش کنید.", + "page_order_detail_not_found": "سفارش پیدا نشد!", "page_order_tracking_not_found": "سفارش پیدا نشد!", diff --git a/uniquely/com-pwa/src/ui/page/order-list.ts b/uniquely/com-pwa/src/ui/page/order-list.ts index dd38c8140..908c8786d 100644 --- a/uniquely/com-pwa/src/ui/page/order-list.ts +++ b/uniquely/com-pwa/src/ui/page/order-list.ts @@ -15,6 +15,7 @@ import {requestableContextConsumer} from '@alwatr/signal'; import {Order} from '@alwatr/type/customer-order-management.js'; import '@alwatr/ui-kit/button/button.js'; import {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; +import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; import {topAppBarContextProvider} from '../../manager/context.js'; import '../stuff/order-list.js'; @@ -127,7 +128,9 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix on: { context_request_error: { target: 'list', - actions: (): void => alert('context_request_error'), + actions: (): void => snackbarSignalTrigger.request({ + messageKey: 'fetch_failed_description', + }), }, context_request_complete: { target: 'list', @@ -194,8 +197,20 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix }, contextError: () => { - // TODO: update me with alwatr-icon-box and retry button - return 'error!'; + const content: IconBoxContent = { + icon: 'cloud-offline-outline', + tinted: 1, + headline: message('fetch_failed_headline'), + description: message('fetch_failed_description'), + }; + return html` + + ${message('retry')} Date: Sun, 12 Mar 2023 01:28:32 +0330 Subject: [PATCH 40/85] feat(com/order-list): new topAppBar change model --- uniquely/com-pwa/src/ui/page/order-list.ts | 45 +++++++++++++--------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/uniquely/com-pwa/src/ui/page/order-list.ts b/uniquely/com-pwa/src/ui/page/order-list.ts index 908c8786d..26ac9dc6f 100644 --- a/uniquely/com-pwa/src/ui/page/order-list.ts +++ b/uniquely/com-pwa/src/ui/page/order-list.ts @@ -8,6 +8,7 @@ import { UnresolvedMixin, FiniteStateMachineController, state, + ScheduleUpdateToFrameMixin, } from '@alwatr/element'; import {message} from '@alwatr/i18n'; import {redirect} from '@alwatr/router'; @@ -28,9 +29,8 @@ declare global { } } -const orderStorageContextConsumer = requestableContextConsumer.bind>( - 'order-storage-context', -); +const orderStorageContextConsumer = + requestableContextConsumer.bind>('order-storage-context'); const buttons = { backToHome: { @@ -56,7 +56,9 @@ const buttons = { * List of all orders. */ @customElement('alwatr-page-order-list') -export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMixin(AlwatrBaseElement))) { +export class AlwatrPageOrderList extends ScheduleUpdateToFrameMixin( + UnresolvedMixin(LocalizeMixin(SignalMixin(AlwatrBaseElement))), +) { static override styles = css` :host { display: block; @@ -128,9 +130,10 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix on: { context_request_error: { target: 'list', - actions: (): void => snackbarSignalTrigger.request({ - messageKey: 'fetch_failed_description', - }), + actions: (): void => + snackbarSignalTrigger.request({ + messageKey: 'fetch_failed_description', + }), }, context_request_complete: { target: 'list', @@ -176,18 +179,16 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix {receivePrevious: 'NextCycle'}, ), ); - - topAppBarContextProvider.setValue({ - headlineKey: 'page_order_list_headline', - startIcon: buttons.backToHome, - endIconList: [buttons.newOrder, buttons.reload], - }); } override render(): unknown { this._logger.logMethod('render'); return this._stateMachine.render({ pending: () => { + topAppBarContextProvider.setValue({ + headlineKey: 'loading', + startIcon: buttons.backToHome, + }); const content: IconBoxContent = { tinted: 1, icon: 'cloud-download-outline', @@ -197,6 +198,11 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix }, contextError: () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_list_headline', + startIcon: buttons.backToHome, + endIconList: [buttons.reload], + }); const content: IconBoxContent = { icon: 'cloud-offline-outline', tinted: 1, @@ -205,17 +211,20 @@ export class AlwatrPageOrderList extends UnresolvedMixin(LocalizeMixin(SignalMix }; return html` - ${message('retry')} + ${message('retry')} + `; }, reloading: 'list', list: () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_list_headline', + startIcon: buttons.backToHome, + endIconList: [buttons.newOrder, {...buttons.reload, disabled: this.gotState === 'reloading'}], + }); return html` Date: Mon, 13 Mar 2023 14:18:47 +0330 Subject: [PATCH 41/85] feat(com-pwa): upgrade to new fsm api --- uniquely/com-pwa/src/manager/context.ts | 5 +- uniquely/com-pwa/src/ui/page/order-detail.ts | 188 ++++++++++++------- uniquely/com-pwa/src/ui/page/order-list.ts | 4 +- 3 files changed, 119 insertions(+), 78 deletions(-) diff --git a/uniquely/com-pwa/src/manager/context.ts b/uniquely/com-pwa/src/manager/context.ts index 1bca2c146..481f31a6d 100644 --- a/uniquely/com-pwa/src/manager/context.ts +++ b/uniquely/com-pwa/src/manager/context.ts @@ -7,13 +7,10 @@ import { import {PageHomeContent} from '../type.js'; import type {AlwatrDocumentStorage} from '@alwatr/type'; -import type {Product, Order, ProductPrice} from '@alwatr/type/customer-order-management.js'; +import type {Order, ProductPrice} from '@alwatr/type/customer-order-management.js'; export * from '@alwatr/pwa-helper/context.js'; -export const productStorageContextConsumer = - contextConsumer.bind>('product-storage-tile-context'); - export const priceStorageContextConsumer = contextConsumer.bind>('price-storage-tile-context'); diff --git a/uniquely/com-pwa/src/ui/page/order-detail.ts b/uniquely/com-pwa/src/ui/page/order-detail.ts index bb44d0cf4..47f88c0c7 100644 --- a/uniquely/com-pwa/src/ui/page/order-detail.ts +++ b/uniquely/com-pwa/src/ui/page/order-detail.ts @@ -9,14 +9,12 @@ import { import {message} from '@alwatr/i18n'; import {topAppBarContextProvider} from '@alwatr/pwa-helper/context.js'; import {redirect} from '@alwatr/router'; -import {eventListener} from '@alwatr/signal'; +import {requestableContextConsumer} from '@alwatr/signal'; +import {snackbarSignalTrigger} from '@alwatr/ui-kit/src/snackbar/show-snackbar.js'; -import {fetchOrderStorage} from '../../manager/context-provider/order-storage.js'; -import {fetchProductStorage} from '../../manager/context-provider/product-storage.js'; -import {orderStorageContextConsumer, productStorageContextConsumer} from '../../manager/context.js'; import {AlwatrOrderDetailBase} from '../stuff/order-detail-base.js'; -import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; +import type {AlwatrDocumentStorage} from '@alwatr/type'; import type {Order, Product} from '@alwatr/type/customer-order-management.js'; import type {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; @@ -26,6 +24,12 @@ declare global { } } +const orderStorageContextConsumer = + requestableContextConsumer.bind>('order-storage-context'); + +const productStorageContextConsumer = + requestableContextConsumer.bind>('product-storage-tile-context'); + const buttons = { backToOrderList: { icon: 'arrow-back-outline', @@ -45,7 +49,7 @@ const buttons = { @customElement('alwatr-page-order-detail') export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase) { private _stateMachine = new FiniteStateMachineController(this, { - id: 'fsm-order-detail-' + this.ali, + id: 'order_detail_' + this.ali, initial: 'pending', context: { orderId: null, @@ -54,58 +58,104 @@ export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase }, stateRecord: { '$all': { - entry: () => { + entry: (): void => { this.gotState = this._stateMachine.state.target; }, on: {}, }, 'pending': { - entry: () => { - if (productStorageContextConsumer.getValue() == null) fetchProductStorage(); - if (orderStorageContextConsumer.getValue() == null) fetchOrderStorage(); + entry: (): void => { + const orderContext = orderStorageContextConsumer.getValue(); + const productContext = productStorageContextConsumer.getValue(); + if (orderContext.state === 'initial') { + orderStorageContextConsumer.request(null); + } + if (productContext.state === 'initial') { + productStorageContextConsumer.request(null); + } }, on: { - LOADED_SUCCESS: { + context_request_initial: {}, + context_request_pending: {}, + context_request_error: { + target: 'contextError', + }, + context_request_complete: { target: 'detail', - condition: () => { - if (this._stateMachine.context.orderStorage == null || - this._stateMachine.context.productStorage == null - ) return false; - return true; + condition: (): boolean => { + if (this.orderId == null) { + this._stateMachine.transition('not_found'); + return false; + } + if (orderStorageContextConsumer.getValue().state === 'complete' && + productStorageContextConsumer.getValue().state === 'complete' + ) return true; + return false; }, - actions: () => { - if (this._stateMachine.context.orderId == null || - this._stateMachine.context.orderStorage?.data[this._stateMachine.context.orderId] == null - ) this._stateMachine.transition('NOT_FOUND'); + }, + context_request_reloading: { + target: 'reloading', + }, + }, + }, + 'contextError': { + on: { + request_context: { + target: 'pending', + actions: (): void => { + orderStorageContextConsumer.request(null); + productStorageContextConsumer.request(null); }, }, }, }, 'detail': { on: { - REQUEST_UPDATE: { + request_context: { target: 'reloading', - actions: this._requestUpdateAction, + actions: (): void => { + orderStorageContextConsumer.request(null); + productStorageContextConsumer.request(null); + }, }, - NOT_FOUND: { + not_found: { target: 'notFound', }, }, }, + 'notFound': { + on: {}, + }, 'reloading': { on: { - LOADED_SUCCESS: { + context_request_error: { target: 'detail', + actions: (): void => + snackbarSignalTrigger.request({messageKey: 'fetch_failed_description'}), + }, + context_request_complete: { + target: 'detail', + condition: (): boolean => { + if (orderStorageContextConsumer.getValue().state === 'complete' && + productStorageContextConsumer.getValue().state === 'complete' + ) return true; + return false; + }, }, - // LOAD_FAILED: { - // target: 'detail', - // }, }, }, - 'notFound': { - on: {}, + }, + signalList: [ + { + signalId: buttons.backToOrderList.clickSignalId, + actions: (): void => {redirect({sectionList: ['order-list']});}, }, - }} as const); + { + signalId: buttons.reload.clickSignalId, + transition: + 'request_context', + }, + ]}); @state() gotState = this._stateMachine.state.target; @@ -115,40 +165,22 @@ export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase return this.orderId; } set orderId(orderId: number) { - this._stateMachine.transition('LOADED_SUCCESS', {orderId}); + this._stateMachine.transition('context_request_complete', {orderId}); } override connectedCallback(): void { super.connectedCallback(); - topAppBarContextProvider.setValue({ - headlineKey: 'page_order_list_headline', - startIcon: buttons.backToOrderList, - endIconList: [buttons.reload], - }); - - this._signalListenerList.push( - productStorageContextConsumer.subscribe((productStorage) => { - this._stateMachine.transition('LOADED_SUCCESS', {productStorage}); - }), - ); - - this._signalListenerList.push( - orderStorageContextConsumer.subscribe((orderStorage) => { - this._stateMachine.transition('LOADED_SUCCESS', {orderStorage}); - }), - ); - this._signalListenerList.push( - eventListener.subscribe(buttons.backToOrderList.clickSignalId, () => { - redirect({sectionList: ['order-list']}); - }), + orderStorageContextConsumer.subscribe((context) => { + this._stateMachine.transition(`context_request_${context.state}`, {orderStorage: context.content}); + }, {receivePrevious: 'NextCycle'}), ); this._signalListenerList.push( - eventListener.subscribe(buttons.reload.clickSignalId, () => { - this._stateMachine.transition('REQUEST_UPDATE'); - }), + productStorageContextConsumer.subscribe((context) => { + this._stateMachine.transition(`context_request_${context.state}`, {productStorage: context.content}); + }, {receivePrevious: 'NextCycle'}), ); } @@ -157,6 +189,11 @@ export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase return this._stateMachine.render({ 'pending': () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_detail_headline', + startIcon: buttons.backToOrderList, + endIconList: [buttons.reload], + }); const content: IconBoxContent = { headline: message('loading'), icon: 'cloud-download-outline', @@ -165,9 +202,34 @@ export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase return html``; }, + 'contextError': () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_detail_headline', + startIcon: buttons.backToOrderList, + endIconList: [buttons.reload], + }); + const content: IconBoxContent = { + icon: 'cloud-offline-outline', + tinted: 1, + headline: message('fetch_failed_headline'), + description: message('fetch_failed_description'), + }; + return html` + + + ${message('retry')} + + `; + }, + 'reloading': 'detail', 'detail': () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_detail_headline', + startIcon: buttons.backToOrderList, + endIconList: [{...buttons.reload, disabled: this.gotState === 'reloading'}], + }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const order = this._stateMachine.context.orderStorage!.data[this._stateMachine.context.orderId!]; return [ @@ -188,20 +250,4 @@ export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase }, }); } - - private async _requestUpdateAction(): Promise { - topAppBarContextProvider.setValue({ - headlineKey: 'loading', - startIcon: buttons.backToOrderList, - endIconList: [buttons.reload], - }); - await fetchOrderStorage(); - await fetchProductStorage(); - topAppBarContextProvider.setValue({ - headlineKey: 'page_order_list_headline', - startIcon: buttons.backToOrderList, - endIconList: [buttons.reload], - }); - this._stateMachine.transition('LOADED_SUCCESS'); - } } diff --git a/uniquely/com-pwa/src/ui/page/order-list.ts b/uniquely/com-pwa/src/ui/page/order-list.ts index 26ac9dc6f..d5ac7e3b4 100644 --- a/uniquely/com-pwa/src/ui/page/order-list.ts +++ b/uniquely/com-pwa/src/ui/page/order-list.ts @@ -157,9 +157,7 @@ export class AlwatrPageOrderList extends ScheduleUpdateToFrameMixin( { signalId: buttons.orderDetail.clickSignalId, actions: (event: ClickSignalType): void => { - redirect({ - sectionList: ['order-detail', event.detail.id], - }); + redirect({sectionList: ['order-detail', event.detail.id]}); }, }, ], From 8abb5de0931778e382ba5bb568aca5ba88fc393c Mon Sep 17 00:00:00 2001 From: "S. Amir Mohammad Najafi" Date: Mon, 13 Mar 2023 14:21:57 +0330 Subject: [PATCH 42/85] feat(com-pwa/order-tracking): upgrade to new fsm api --- .../com-pwa/src/ui/page/order-tracking.ts | 235 ++++++++++-------- 1 file changed, 133 insertions(+), 102 deletions(-) diff --git a/uniquely/com-pwa/src/ui/page/order-tracking.ts b/uniquely/com-pwa/src/ui/page/order-tracking.ts index cac33c176..c6ee5abb0 100644 --- a/uniquely/com-pwa/src/ui/page/order-tracking.ts +++ b/uniquely/com-pwa/src/ui/page/order-tracking.ts @@ -1,39 +1,36 @@ import { customElement, - css, - html, - UnresolvedMixin, - AlwatrBaseElement, FiniteStateMachineController, - state, + html, property, - SignalMixin, - LocalizeMixin, + state, + UnresolvedMixin, } from '@alwatr/element'; import {message} from '@alwatr/i18n'; -import '@alwatr/icon'; import {topAppBarContextProvider} from '@alwatr/pwa-helper/context.js'; import {redirect} from '@alwatr/router'; -import {eventListener} from '@alwatr/signal'; -import '@alwatr/ui-kit/button/button.js'; -import {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; -import '@alwatr/ui-kit/card/surface.js'; +import {requestableContextConsumer} from '@alwatr/signal'; import '@alwatr/ui-kit/chat/chat.js'; -import '@alwatr/ui-kit/radio-group/radio-group.js'; +import {snackbarSignalTrigger} from '@alwatr/ui-kit/src/snackbar/show-snackbar.js'; -import {fetchOrderStorage} from '../../manager/context-provider/order-storage.js'; -import {orderStorageContextConsumer} from '../../manager/context.js'; -import '../stuff/order-status-box.js'; +import {AlwatrOrderDetailBase} from '../stuff/order-detail-base.js'; -import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; -import type {Order} from '@alwatr/type/customer-order-management.js'; +import type {AlwatrDocumentStorage} from '@alwatr/type'; +import type {Order, Product} from '@alwatr/type/customer-order-management.js'; +import type {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; declare global { interface HTMLElementTagNameMap { - 'alwatr-page-order-tracking': AlwatrPageOrderTracking; + 'alwatr-page-order-tracking': AlwatrPageOrderDetail; } } +const orderStorageContextConsumer = + requestableContextConsumer.bind>('order-storage-context'); + +const productStorageContextConsumer = + requestableContextConsumer.bind>('product-storage-tile-context'); + const buttons = { backToOrderList: { icon: 'arrow-back-outline', @@ -47,88 +44,119 @@ const buttons = { }, } as const; - /** - * Alwatr Customer Order Management Order Form Page + * Alwatr Customer Order Management Order Tracking Page. */ @customElement('alwatr-page-order-tracking') -export class AlwatrPageOrderTracking extends UnresolvedMixin(LocalizeMixin(SignalMixin(AlwatrBaseElement))) { - static override styles = css` - :host { - display: flex; - flex-direction: column; - padding: calc(2 * var(--sys-spacing-track)); - box-sizing: border-box; - min-height: 100%; - } - - alwatr-order-status-box { - margin-bottom: var(--sys-spacing-track); - } - - alwatr-chat { - flex-grow: 1; - } - `; - +export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase) { private _stateMachine = new FiniteStateMachineController(this, { - id: 'fsm-order-tracking-' + this.ali, + id: 'order_tracking_' + this.ali, initial: 'pending', context: { orderId: null, orderStorage: | null> null, + productStorage: | null> null, }, stateRecord: { '$all': { - entry: () => { + entry: (): void => { this.gotState = this._stateMachine.state.target; }, on: {}, }, 'pending': { - entry: () => { - if (orderStorageContextConsumer.getValue() == null) fetchOrderStorage(); + entry: (): void => { + const orderContext = orderStorageContextConsumer.getValue(); + const productContext = productStorageContextConsumer.getValue(); + if (orderContext.state === 'initial') { + orderStorageContextConsumer.request(null); + } + if (productContext.state === 'initial') { + productStorageContextConsumer.request(null); + } }, on: { - LOADED_SUCCESS: { + context_request_initial: {}, + context_request_pending: {}, + context_request_error: { + target: 'contextError', + }, + context_request_complete: { target: 'tracking', - condition: () => { - if (this._stateMachine.context.orderStorage == null) return false; - return true; + condition: (): boolean => { + if (this.orderId == null) { + this._stateMachine.transition('not_found'); + return false; + } + if (orderStorageContextConsumer.getValue().state === 'complete' && + productStorageContextConsumer.getValue().state === 'complete' + ) return true; + return false; }, - actions: () => { - if (this._stateMachine.context.orderId == null || - this._stateMachine.context.orderStorage?.data[this._stateMachine.context.orderId] == null - ) this._stateMachine.transition('NOT_FOUND'); + }, + context_request_reloading: { + target: 'reloading', + }, + }, + }, + 'contextError': { + on: { + request_context: { + target: 'pending', + actions: (): void => { + orderStorageContextConsumer.request(null); + productStorageContextConsumer.request(null); }, }, }, }, 'tracking': { on: { - REQUEST_UPDATE: { + request_context: { target: 'reloading', - actions: this._requestUpdateAction, + actions: (): void => { + orderStorageContextConsumer.request(null); + productStorageContextConsumer.request(null); + }, }, - NOT_FOUND: { + not_found: { target: 'notFound', }, }, }, + 'notFound': { + on: {}, + }, 'reloading': { on: { - LOADED_SUCCESS: { + context_request_error: { + target: 'tracking', + actions: (): void => + snackbarSignalTrigger.request({messageKey: 'fetch_failed_description'}), + }, + context_request_complete: { target: 'tracking', + condition: (): boolean => { + if (orderStorageContextConsumer.getValue().state === 'complete' && + productStorageContextConsumer.getValue().state === 'complete' + ) return true; + return false; + }, }, - // LOAD_FAILED: { - // target: 'tracking', - // }, }, }, - 'notFound': { - on: {}, + }, + signalList: [ + { + signalId: buttons.backToOrderList.clickSignalId, + actions: (): void => {redirect({sectionList: ['order-list']});}, + }, + { + signalId: buttons.reload.clickSignalId, + transition: + 'request_context', }, - }} as const); + ]}); @state() gotState = this._stateMachine.state.target; @@ -138,42 +166,35 @@ export class AlwatrPageOrderTracking extends UnresolvedMixin(LocalizeMixin(Signa return this.orderId; } set orderId(orderId: number) { - this._stateMachine.transition('LOADED_SUCCESS', {orderId}); + this._stateMachine.transition('context_request_complete', {orderId}); } override connectedCallback(): void { super.connectedCallback(); - topAppBarContextProvider.setValue({ - headlineKey: 'page_order_tracking_headline', - startIcon: buttons.backToOrderList, - endIconList: [buttons.reload], - }); - - this._signalListenerList.push( - orderStorageContextConsumer.subscribe((orderStorage) => { - this._stateMachine.transition('LOADED_SUCCESS', {orderStorage}); - }), - ); - this._signalListenerList.push( - eventListener.subscribe(buttons.backToOrderList.clickSignalId, () => { - redirect({sectionList: ['order-list']}); - }), + orderStorageContextConsumer.subscribe((context) => { + this._stateMachine.transition(`context_request_${context.state}`, {orderStorage: context.content}); + }, {receivePrevious: 'NextCycle'}), ); this._signalListenerList.push( - eventListener.subscribe(buttons.reload.clickSignalId, () => { - this._stateMachine.transition('REQUEST_UPDATE'); - }), + productStorageContextConsumer.subscribe((context) => { + this._stateMachine.transition(`context_request_${context.state}`, {productStorage: context.content}); + }, {receivePrevious: 'NextCycle'}), ); } - protected override render(): unknown { this._logger.logMethod('render'); + return this._stateMachine.render({ - pending: () => { + 'pending': () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_tracking_headline', + startIcon: buttons.backToOrderList, + endIconList: [buttons.reload], + }); const content: IconBoxContent = { headline: message('loading'), icon: 'cloud-download-outline', @@ -182,9 +203,34 @@ export class AlwatrPageOrderTracking extends UnresolvedMixin(LocalizeMixin(Signa return html``; }, - reloading: 'tracking', + 'contextError': () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_tracking_headline', + startIcon: buttons.backToOrderList, + endIconList: [buttons.reload], + }); + const content: IconBoxContent = { + icon: 'cloud-offline-outline', + tinted: 1, + headline: message('fetch_failed_headline'), + description: message('fetch_failed_description'), + }; + return html` + + + ${message('retry')} + + `; + }, - tracking: () => { + 'reloading': 'tracking', + + 'tracking': () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_tracking_headline', + startIcon: buttons.backToOrderList, + endIconList: [{...buttons.reload, disabled: this.gotState === 'reloading'}], + }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const order = this._stateMachine.context.orderStorage!.data[this._stateMachine.context.orderId!]; return [ @@ -193,9 +239,9 @@ export class AlwatrPageOrderTracking extends UnresolvedMixin(LocalizeMixin(Signa ]; }, - notFound: () => { + 'notFound': () => { const content: IconBoxContent = { - headline: message('page_order_detail_not_found'), + headline: message('page_order_tracking_not_found'), icon: 'close', tinted: 1, }; @@ -203,19 +249,4 @@ export class AlwatrPageOrderTracking extends UnresolvedMixin(LocalizeMixin(Signa }, }); } - - private async _requestUpdateAction(): Promise { - topAppBarContextProvider.setValue({ - headlineKey: 'loading', - startIcon: buttons.backToOrderList, - endIconList: [buttons.reload], - }); - await fetchOrderStorage(); - topAppBarContextProvider.setValue({ - headlineKey: 'page_order_tracking_headline', - startIcon: buttons.backToOrderList, - endIconList: [buttons.reload], - }); - this._stateMachine.transition('LOADED_SUCCESS'); - } } From e2a5f51c5a190316172ccaa9312fa77247ef3518 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Mon, 13 Mar 2023 23:10:29 +0330 Subject: [PATCH 43/85] feat(com-pwa/new-order): upgrade to new fsm api --- uniquely/com-pwa/src/manager/context.ts | 9 +- .../manager/submit-order-command-handler.ts | 14 +- uniquely/com-pwa/src/ui/page/new-order.ts | 534 ++++++++++-------- uniquely/com-pwa/src/ui/page/order-detail.ts | 2 +- uniquely/com-pwa/src/ui/page/order-list.ts | 6 +- .../com-pwa/src/ui/page/order-tracking.ts | 2 +- 6 files changed, 323 insertions(+), 244 deletions(-) diff --git a/uniquely/com-pwa/src/manager/context.ts b/uniquely/com-pwa/src/manager/context.ts index 481f31a6d..575583f92 100644 --- a/uniquely/com-pwa/src/manager/context.ts +++ b/uniquely/com-pwa/src/manager/context.ts @@ -6,17 +6,10 @@ import { import {PageHomeContent} from '../type.js'; -import type {AlwatrDocumentStorage} from '@alwatr/type'; -import type {Order, ProductPrice} from '@alwatr/type/customer-order-management.js'; +import type {Order} from '@alwatr/type/customer-order-management.js'; export * from '@alwatr/pwa-helper/context.js'; -export const priceStorageContextConsumer = - contextConsumer.bind>('price-storage-tile-context'); - -export const finalPriceStorageContextConsumer = - contextConsumer.bind>('final-price-storage-tile-context'); - export const homePageContentContextProvider = contextProvider.bind('home-page-content-context'); export const homePageContentContextConsumer = diff --git a/uniquely/com-pwa/src/manager/submit-order-command-handler.ts b/uniquely/com-pwa/src/manager/submit-order-command-handler.ts index 5947f35ff..de0cb8351 100644 --- a/uniquely/com-pwa/src/manager/submit-order-command-handler.ts +++ b/uniquely/com-pwa/src/manager/submit-order-command-handler.ts @@ -1,12 +1,20 @@ import {serviceRequest} from '@alwatr/fetch'; -import {commandHandler, contextProvider} from '@alwatr/signal'; +import {commandHandler, contextConsumer, contextProvider, requestableContextConsumer} from '@alwatr/signal'; -import {userContextConsumer, submitOrderCommandTrigger, orderStorageContextConsumer} from './context.js'; + +import {submitOrderCommandTrigger} from './context.js'; import {logger} from './logger.js'; import {config} from '../config.js'; +import type {AlwatrDocumentStorage, User} from '@alwatr/type'; import type {Order} from '@alwatr/type/customer-order-management.js'; + +const orderStorageContextConsumer = + requestableContextConsumer.bind>('order-storage-context'); + +const userContextConsumer = contextConsumer.bind('user-context'); + commandHandler.define(submitOrderCommandTrigger.id, async (order) => { const userContext = userContextConsumer.getValue() ?? await userContextConsumer.untilChange(); @@ -24,7 +32,7 @@ commandHandler.define(submitOrderCommandTrigger.id, async ( const newOrder = response.data; - const orderStorage = orderStorageContextConsumer.getValue(); + const orderStorage = orderStorageContextConsumer.getValue().content; if (orderStorage != null) { orderStorage.data[newOrder.id] = newOrder; orderStorage.meta.lastUpdated = Date.now(); diff --git a/uniquely/com-pwa/src/ui/page/new-order.ts b/uniquely/com-pwa/src/ui/page/new-order.ts index 34e2449aa..797eeaaae 100644 --- a/uniquely/com-pwa/src/ui/page/new-order.ts +++ b/uniquely/com-pwa/src/ui/page/new-order.ts @@ -1,40 +1,43 @@ import {customElement, FiniteStateMachineController, html, state, UnresolvedMixin} from '@alwatr/element'; import {message} from '@alwatr/i18n'; import {redirect} from '@alwatr/router'; +import {requestableContextConsumer} from '@alwatr/signal'; import {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; -import { - Order, - OrderDraft, - orderInfoSchema, - OrderItem, - Product, - ProductPrice, - tileQtyStep, -} from '@alwatr/type/customer-order-management.js'; +import {tileQtyStep, orderInfoSchema} from '@alwatr/type/customer-order-management.js'; import {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; import {getLocalStorageItem} from '@alwatr/util'; import {validator} from '@alwatr/validator'; -import {fetchPriceStorage} from '../../manager/context-provider/price-storage.js'; -import {fetchProductStorage} from '../../manager/context-provider/product-storage.js'; -import { - finalPriceStorageContextConsumer, - priceStorageContextConsumer, - productStorageContextConsumer, - scrollToTopCommand, - submitOrderCommandTrigger, - topAppBarContextProvider, -} from '../../manager/context.js'; +import {scrollToTopCommand, submitOrderCommandTrigger, topAppBarContextProvider} from '../../manager/context.js'; import {AlwatrOrderDetailBase} from '../stuff/order-detail-base.js'; import '../stuff/select-product.js'; +import type { + Order, + OrderDraft, + OrderShippingInfo, + OrderItem, + Product, + ProductPrice, +} from '@alwatr/type/customer-order-management.js'; + declare global { interface HTMLElementTagNameMap { 'alwatr-page-new-order': AlwatrPageNewOrder; } } +const productStorageContextConsumer = + requestableContextConsumer.bind>('product-storage-tile-context'); + +const priceStorageContextConsumer = + requestableContextConsumer.bind>('price-storage-tile-context'); + +const finalPriceStorageContextConsumer = requestableContextConsumer.bind>( + 'final-price-storage-tile-context', +); + const newOrderLocalStorageKey = 'draft-order-x2'; const buttons = { @@ -84,6 +87,11 @@ const buttons = { icon: 'chatbox-outline', clickSignalId: 'page_new_order_tracking_click_event', }, + reload: { + icon: 'reload-outline', + // flipRtl: true, + clickSignalId: 'order_list_reload_click_event', + }, retry: { icon: 'reload-outline', clickSignalId: 'page_new_order_retry_click_event', @@ -96,7 +104,7 @@ const buttons = { @customElement('alwatr-page-new-order') export class AlwatrPageNewOrder extends UnresolvedMixin(AlwatrOrderDetailBase) { private _stateMachine = new FiniteStateMachineController(this, { - id: 'fsm-new-order-' + this.ali, + id: 'new_order_' + this.ali, initial: 'pending', context: { registeredOrderId: null, @@ -107,77 +115,76 @@ export class AlwatrPageNewOrder extends UnresolvedMixin(AlwatrOrderDetailBase) { }, stateRecord: { $all: { - entry: () => { + entry: (): void => { this.gotState = this._stateMachine.state.target; localStorage.setItem(newOrderLocalStorageKey, JSON.stringify(this._stateMachine.context.order)); - - if ( - this._stateMachine.state.target != 'shippingForm' && - this._stateMachine.state.target != this._stateMachine.state.from - ) { - scrollToTopCommand.request({}); - } - - if ( - this._stateMachine.state.target === 'edit' && - this._stateMachine.state.from != 'selectProduct' && - !this._stateMachine.context.order?.itemList?.length - ) { - this._stateMachine.transition('SELECT_PRODUCT'); - } - else if (this._stateMachine.state.target === 'edit' || this._stateMachine.state.target === 'review') { - const order = this._stateMachine.context.order; - let totalPrice = 0; - let finalTotalPrice = 0; - for (const item of order.itemList ?? []) { - totalPrice += item.price * item.qty * tileQtyStep; - finalTotalPrice += item.finalPrice * item.qty * tileQtyStep; - } - order.totalPrice = Math.round(totalPrice); - order.finalTotalPrice = Math.round(finalTotalPrice); - } }, on: {}, }, pending: { - entry: () => { - if (productStorageContextConsumer.getValue() == null) { - fetchProductStorage(); - } - if (priceStorageContextConsumer.getValue() == null || finalPriceStorageContextConsumer.getValue() == null) { - fetchPriceStorage(); - } + entry: (): void => { + const productStorage = productStorageContextConsumer.getValue(); + const priceStorage = priceStorageContextConsumer.getValue(); + const finalPriceStorage = finalPriceStorageContextConsumer.getValue(); + if (productStorage.state == 'initial') productStorageContextConsumer.request(null); + if (priceStorage.state == 'initial') priceStorageContextConsumer.request(null); + if (finalPriceStorage.state == 'initial') finalPriceStorageContextConsumer.request(null); }, on: { - LOADED_SUCCESS: { + context_request_initial: {}, + context_request_pending: {}, + context_request_error: { + target: 'contextError', + }, + context_request_complete: { target: 'edit', - condition: () => { + condition: (): boolean => { if ( - this._stateMachine.context.finalPriceStorage == null || - this._stateMachine.context.priceStorage == null || - this._stateMachine.context.productStorage == null + productStorageContextConsumer.getValue().state === 'complete' && + priceStorageContextConsumer.getValue().state === 'complete' && + finalPriceStorageContextConsumer.getValue().state === 'complete' ) { - return false; + return true; } - return true; + return false; + }, + }, + context_request_reloading: { + target: 'reloading', + }, + }, + }, + contextError: { + on: { + request_context: { + target: 'pending', + actions: (): void => { + productStorageContextConsumer.request(null); + priceStorageContextConsumer.request(null); + finalPriceStorageContextConsumer.request(null); }, }, }, }, edit: { + entry: (): void => { + if (this._stateMachine.state.from != 'selectProduct' && !this._stateMachine.context.order?.itemList?.length) { + this._stateMachine.transition('select_product'); + } + }, on: { - SELECT_PRODUCT: { + select_product: { target: 'selectProduct', }, - EDIT_SHIPPING: { + edit_shipping: { target: 'shippingForm', - actions: () => { + actions: (): void => { this._stateMachine.context.order.shippingInfo ??= {}; }, }, - SUBMIT: { + submit: { target: 'review', - condition: () => { + condition: (): boolean => { if ( !this._stateMachine.context.order.itemList?.length && this._stateMachine.context.order.shippingInfo == null @@ -188,127 +195,155 @@ export class AlwatrPageNewOrder extends UnresolvedMixin(AlwatrOrderDetailBase) { return this.validateOrder(); }, }, - QTY_UPDATE: {}, + qty_update: {}, }, }, selectProduct: { + entry: (): void => { + if (this._stateMachine.state.target != this._stateMachine.state.from) { + scrollToTopCommand.request({}); + } + }, on: { - SUBMIT: { + submit: { + target: 'edit', + }, + }, + }, + reloading: { + on: { + context_request_error: { + target: 'edit', + actions: (): void => snackbarSignalTrigger.request({messageKey: 'fetch_failed_description'}), + }, + context_request_complete: { target: 'edit', }, }, }, shippingForm: { on: { - SUBMIT: { + submit: { target: 'edit', }, }, }, review: { on: { - BACK: {}, - FINAL_SUBMIT: { + back: {}, + final_submit: { target: 'submitting', - actions: async () => { + actions: async (): Promise => { const order = await submitOrderCommandTrigger.requestWithResponse(this._stateMachine.context.order); if (order == null) { - this._stateMachine.transition('SUBMIT_FAILED'); + this._stateMachine.transition('submit_failed'); return; } // else - this._stateMachine.transition('SUBMIT_SUCCESS', {registeredOrderId: order.id}); + this._stateMachine.transition('submit_success', {registeredOrderId: order.id}); }, }, }, }, submitting: { on: { - SUBMIT_SUCCESS: { + submit_success: { target: 'submitSuccess', - actions: () => { + actions: (): void => { localStorage.removeItem(newOrderLocalStorageKey); - // TODO: this._stateMachine.context.order = - // getLocalStorageItem(newOrderLocalStorageKey, {id: 'new', status: 'draft'}); + this._stateMachine.context.order = + getLocalStorageItem(newOrderLocalStorageKey, {id: 'new', status: 'draft'}); }, }, - SUBMIT_FAILED: { + submit_failed: { target: 'submitFailed', }, }, }, submitSuccess: { on: { - NEW_ORDER: { + new_order: { target: 'edit', - actions: () => { - // TODO: registeredOrderId = '' + actions: (): void => { + this._stateMachine.context.registeredOrderId = null; }, }, }, }, submitFailed: { on: { - FINAL_SUBMIT: { + final_submit: { target: 'submitting', }, }, }, }, - signalRecord: { - [buttons.submit.clickSignalId]: { - transition: 'SUBMIT', + signalList: [ + { + signalId: buttons.submit.clickSignalId, + transition: 'submit', }, - [buttons.submitShippingForm.clickSignalId]: { - transition: 'SUBMIT', + { + signalId: buttons.submitShippingForm.clickSignalId, + transition: 'submit', }, - [buttons.edit.clickSignalId]: { - transition: 'BACK', + { + signalId: buttons.edit.clickSignalId, + transition: 'back', }, - [buttons.submitFinal.clickSignalId]: { - transition: 'FINAL_SUBMIT', + { + signalId: buttons.submitFinal.clickSignalId, + transition: 'final_submit', }, - [buttons.editItems.clickSignalId]: { - transition: 'FINAL_SUBMIT', + { + signalId: buttons.editItems.clickSignalId, + transition: 'final_submit', }, - [buttons.retry.clickSignalId]: { - transition: 'FINAL_SUBMIT', + { + signalId: buttons.retry.clickSignalId, + transition: 'final_submit', }, - [buttons.editShippingForm.clickSignalId]: { - transition: 'EDIT_SHIPPING', + { + signalId: buttons.editShippingForm.clickSignalId, + transition: 'edit_shipping', }, - [buttons.tracking.clickSignalId]: { - actions: () => { + { + signalId: buttons.tracking.clickSignalId, + actions: (): void => { const orderId = this._stateMachine.context.registeredOrderId as string; - this._stateMachine.transition('NEW_ORDER'); + this._stateMachine.transition('new_order'); redirect({sectionList: ['order-tracking', orderId]}); }, }, - [buttons.detail.clickSignalId]: { - actions: () => { + { + signalId: buttons.detail.clickSignalId, + actions: (): void => { const orderId = this._stateMachine.context.registeredOrderId as string; - this._stateMachine.transition('NEW_ORDER'); + this._stateMachine.transition('new_order'); redirect({sectionList: ['order-detail', orderId]}); }, }, - [buttons.newOrder.clickSignalId]: { - actions: () => { - this._stateMachine.transition('NEW_ORDER'); + { + signalId: buttons.newOrder.clickSignalId, + actions: (): void => { + this._stateMachine.transition('new_order'); redirect('/new-order/'); }, }, - 'order_item_qty_add': { - actions: (event) => { - this.qtyUpdate((event as ClickSignalType).detail, 1); // TODO: set type with action + { + signalId: 'order_item_qty_add', + actions: (event: ClickSignalType): void => { + this.qtyUpdate(event.detail, 1); // TODO: set type with action }, }, - 'order_item_qty_remove': { - actions: (event) => { - this.qtyUpdate((event as ClickSignalType).detail, -1); + { + signalId: 'order_item_qty_remove', + actions: (event: ClickSignalType): void => { + this.qtyUpdate(event.detail, -1); }, }, - }, - } as const); + ], + }); @state() gotState = this._stateMachine.state.target; @@ -316,57 +351,147 @@ export class AlwatrPageNewOrder extends UnresolvedMixin(AlwatrOrderDetailBase) { override connectedCallback(): void { super.connectedCallback(); - topAppBarContextProvider.setValue({ - headlineKey: 'page_new_order_headline', - startIcon: buttons.backToHome, - }); - this._signalListenerList.push( - productStorageContextConsumer.subscribe((productStorage) => { - this._stateMachine.transition('LOADED_SUCCESS', {productStorage}); - }), + productStorageContextConsumer.subscribe( + (context) => { + this._stateMachine.transition(`context_request_${context.state}`, {productStorage: context.content}); + }, + {receivePrevious: 'NextCycle'}, + ), ); this._signalListenerList.push( - priceStorageContextConsumer.subscribe((priceStorage) => { - this._stateMachine.transition('LOADED_SUCCESS', {priceStorage}); - }), + priceStorageContextConsumer.subscribe( + (context) => { + this._stateMachine.transition(`context_request_${context.state}`, {priceStorage: context.content}); + }, + {receivePrevious: 'NextCycle'}, + ), ); this._signalListenerList.push( - finalPriceStorageContextConsumer.subscribe((finalPriceStorage) => { - this._stateMachine.transition('LOADED_SUCCESS', {finalPriceStorage}); - }), + finalPriceStorageContextConsumer.subscribe( + (context) => { + this._stateMachine.transition(`context_request_${context.state}`, {finalPriceStorage: context.content}); + }, + {receivePrevious: 'NextCycle'}, + ), ); } protected override render(): unknown { this._logger.logMethod('render'); return this._stateMachine.render({ + pending: () => { + topAppBarContextProvider.setValue({ + headlineKey: 'loading', + startIcon: buttons.backToHome, + }); + const content: IconBoxContent = { + tinted: 1, + icon: 'cloud-download-outline', + headline: message('loading'), + }; + return html``; + }, + edit: () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_new_order_headline', + startIcon: buttons.backToHome, + }); const order = this._stateMachine.context.order; return [ - // this.render_part_status(order), this.render_part_item_list(order.itemList ?? [], this._stateMachine.context.productStorage, true), - this.render_part_btn_product(), + html` +
+ + ${message('page_new_order_edit_items')} + +
+ `, this.render_part_shipping_info(order.shippingInfo), - this.render_part_btn_shipping_edit(), + html` +
+ + ${message('page_new_order_shipping_edit')} + +
+ `, this.render_part_summary(order), - this.render_part_btn_submit(), + html` +
+ ${message('page_new_order_submit')} + +
+ `, ]; }, + contextError: () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_list_headline', + startIcon: buttons.backToHome, + endIconList: [buttons.reload], + }); + const content: IconBoxContent = { + icon: 'cloud-offline-outline', + tinted: 1, + headline: message('fetch_failed_headline'), + description: message('fetch_failed_description'), + }; + return html` + + + ${message('retry')} + + `; + }, + + reloading: 'selectProduct', selectProduct: () => { - return [html``]; + topAppBarContextProvider.setValue({ + headlineKey: 'page_new_order_headline', + startIcon: buttons.backToHome, + }); + return [ + html``, + html` +
+ ${message('select_product_submit_button')} + +
+ `, + ]; }, shippingForm: () => { const order = this._stateMachine.context.order; return [ this.render_part_item_list(order.itemList ?? [], this._stateMachine.context.productStorage, false), - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.render_part_shipping_form(order.shippingInfo!), - this.render_part_btn_shipping_submit(), + this.render_part_shipping_form(order.shippingInfo as Partial), + html` +
+ + ${message('page_new_order_shipping_submit')} + +
+ `, ]; }, @@ -377,7 +502,16 @@ export class AlwatrPageNewOrder extends UnresolvedMixin(AlwatrOrderDetailBase) { this.render_part_item_list(order.itemList, this._stateMachine.context.productStorage), this.render_part_shipping_info(order.shippingInfo), this.render_part_summary(order), - this.render_part_btn_final_submit(), + html` +
+ + ${message('page_new_order_edit')} + + + ${message('page_new_order_submit_final')} + +
+ `, ]; }, @@ -396,7 +530,19 @@ export class AlwatrPageNewOrder extends UnresolvedMixin(AlwatrOrderDetailBase) { icon: 'cloud-done-outline', tinted: 1, }; - return [html``, this.render_part_btn_submit_success()]; + return [ + html``, + html` +
+ + ${message('page_new_order_detail_button')} + + + ${message('page_new_order_headline')} + +
+ `, + ]; }, submitFailed: () => { @@ -405,98 +551,30 @@ export class AlwatrPageNewOrder extends UnresolvedMixin(AlwatrOrderDetailBase) { icon: 'cloud-offline-outline', tinted: 1, }; - return [html``, this.render_part_btn_submit_failed()]; + return [ + html``, + html` +
+ + ${message('page_new_order_retry_button')} + +
+ `, + ]; }, }); } - protected render_part_btn_select_product(): unknown { - return html` -
- ${message('select_product_submit_button')} -
- `; - } - - protected render_part_btn_product(): unknown { - return html` -
- - ${message('page_new_order_edit_items')} - -
- `; - } - - protected render_part_btn_shipping_edit(): unknown { - return html`
- ${message('page_new_order_shipping_edit')} -
`; - } - - protected render_part_btn_shipping_submit(): unknown { - return html`
- ${message('page_new_order_shipping_submit')} -
`; - } - - protected render_part_btn_submit(): unknown { - return html` -
- ${message('page_new_order_submit')} -
- `; - } - - protected render_part_btn_submit_success(): unknown { - return html` -
- ${message('page_new_order_detail_button')} - ${message('page_new_order_headline')} -
- `; - } - - protected render_part_btn_submit_failed(): unknown { - return html` -
- ${message('page_new_order_retry_button')} -
- `; - } - - protected render_part_btn_final_submit(): unknown { - return html` -
- ${message('page_new_order_edit')} - ${message('page_new_order_submit_final')} -
- `; + protected calculateOrderPrice(): void { + const order = this._stateMachine.context.order; + let totalPrice = 0; + let finalTotalPrice = 0; + for (const item of order.itemList ?? []) { + totalPrice += item.price * item.qty * tileQtyStep; + finalTotalPrice += item.finalPrice * item.qty * tileQtyStep; + } + order.totalPrice = Math.round(totalPrice); + order.finalTotalPrice = Math.round(finalTotalPrice); } protected validateOrder(): boolean { @@ -525,6 +603,6 @@ export class AlwatrPageNewOrder extends UnresolvedMixin(AlwatrOrderDetailBase) { const qty = orderItem.qty + add; if (qty <= 0) return; orderItem.qty = qty; - this._stateMachine.transition('QTY_UPDATE'); + this._stateMachine.transition('qty_update'); } } diff --git a/uniquely/com-pwa/src/ui/page/order-detail.ts b/uniquely/com-pwa/src/ui/page/order-detail.ts index 47f88c0c7..d929ed2f1 100644 --- a/uniquely/com-pwa/src/ui/page/order-detail.ts +++ b/uniquely/com-pwa/src/ui/page/order-detail.ts @@ -10,7 +10,7 @@ import {message} from '@alwatr/i18n'; import {topAppBarContextProvider} from '@alwatr/pwa-helper/context.js'; import {redirect} from '@alwatr/router'; import {requestableContextConsumer} from '@alwatr/signal'; -import {snackbarSignalTrigger} from '@alwatr/ui-kit/src/snackbar/show-snackbar.js'; +import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; import {AlwatrOrderDetailBase} from '../stuff/order-detail-base.js'; diff --git a/uniquely/com-pwa/src/ui/page/order-list.ts b/uniquely/com-pwa/src/ui/page/order-list.ts index d5ac7e3b4..b6396c1e6 100644 --- a/uniquely/com-pwa/src/ui/page/order-list.ts +++ b/uniquely/com-pwa/src/ui/page/order-list.ts @@ -29,9 +29,6 @@ declare global { } } -const orderStorageContextConsumer = - requestableContextConsumer.bind>('order-storage-context'); - const buttons = { backToHome: { icon: 'arrow-back-outline', @@ -52,6 +49,9 @@ const buttons = { }, } as const; +const orderStorageContextConsumer = + requestableContextConsumer.bind>('order-storage-context'); + /** * List of all orders. */ diff --git a/uniquely/com-pwa/src/ui/page/order-tracking.ts b/uniquely/com-pwa/src/ui/page/order-tracking.ts index c6ee5abb0..bae4217d8 100644 --- a/uniquely/com-pwa/src/ui/page/order-tracking.ts +++ b/uniquely/com-pwa/src/ui/page/order-tracking.ts @@ -11,7 +11,7 @@ import {topAppBarContextProvider} from '@alwatr/pwa-helper/context.js'; import {redirect} from '@alwatr/router'; import {requestableContextConsumer} from '@alwatr/signal'; import '@alwatr/ui-kit/chat/chat.js'; -import {snackbarSignalTrigger} from '@alwatr/ui-kit/src/snackbar/show-snackbar.js'; +import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; import {AlwatrOrderDetailBase} from '../stuff/order-detail-base.js'; From f6770a07fdf6855ccd63a85822d44d5ef9c72dee Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Mon, 13 Mar 2023 23:11:05 +0330 Subject: [PATCH 44/85] fix(fms): import path --- core/fsm/src/core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index 774ad4c1b..39deb1dba 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -3,7 +3,7 @@ import {contextConsumer, eventListener} from '@alwatr/signal'; import {dispatch} from '@alwatr/signal/core.js'; import type {FsmConfig, StateContext} from './type.js'; -import type {ListenerSpec} from '@alwatr/signal/src/type.js'; +import type {ListenerSpec} from '@alwatr/signal/type.js'; import type {SingleOrArray, MaybePromise, StringifyableRecord} from '@alwatr/type'; export type {FsmConfig, StateContext}; From 5d8173a467ef96b3a23654623afd80904770ddd4 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Tue, 14 Mar 2023 19:24:20 +0330 Subject: [PATCH 45/85] feat(fms): complete new FSM api base on signal! --- core/signal/src/fsm.ts | 333 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 core/signal/src/fsm.ts diff --git a/core/signal/src/fsm.ts b/core/signal/src/fsm.ts new file mode 100644 index 000000000..4acfe99cc --- /dev/null +++ b/core/signal/src/fsm.ts @@ -0,0 +1,333 @@ +import {dispatch, getDetail, logger} from './core.js'; + +import type {OmitFirstParam, SingleOrArray, StringifyableRecord} from '@alwatr/type'; + +export interface FsmConfig< + TState extends string = string, + TEventId extends string = string, + TActionName extends string = string, + TContext extends StringifyableRecord = StringifyableRecord +> extends StringifyableRecord { + name: string; + + /** + * Initial context. + */ + context: TContext; + + /** + * Initial state. + */ + initial: TState; + + /** + * Define state list + */ + stateRecord: StateRecord; +} + +export type StateRecord = { + [S in TState | '$all']: { + /** + * On state exit actions + */ + exit?: SingleOrArray; + + /** + * On state entry actions + */ + entry?: SingleOrArray; + + /** + * An object mapping eventId to state. + * + * Example: + * + * ```ts + * stateRecord: { + * on: { + * TIMER: { + * target: 'green', + * condition: () => car.gas > 0, + * actions: () => car.go(), + * } + * } + * } + * ``` + */ + on: { + [E in TEventId]?: TransitionConfig | undefined; + }; + }; +}; + +export interface StateContext extends StringifyableRecord { + /** + * Current state + */ + target: TState; + /** + * Last state + */ + from: TState; + /** + * Transition event + */ + by: TEventId | 'INIT'; +} + +export interface TransitionConfig + extends StringifyableRecord { + target?: TState; + condition?: TActionName; + actions?: SingleOrArray; +} + +export interface SignalDetail< + TState extends string = string, + TEventId extends string = string, + TContext extends StringifyableRecord = StringifyableRecord +> extends StringifyableRecord { + name: string; + state: StateContext; + context: TContext; +} + +// type helper + +export type TState = Exclude; +export type TEventId = keyof TMachine['stateRecord'][TState]['on']; +export type TActionName = TMachine['stateRecord'][TState]['entry']; +export type TContext = TMachine['context']; + +export type StateMachineHelper = Readonly<{ + TState: Exclude; + TEventId: keyof TMachine['stateRecord'][StateMachineHelper['TState']]['on']; + TActionName: TMachine['stateRecord'][StateMachineHelper['TState']]['entry']; + TContext: TMachine['context']; +}>; + +// ---- + +const fsmStorage: Record = {}; + +export function contractStateMachine< + TState extends string = string, + TEventId extends string = string, + TActionName extends string = string, + TContext extends StringifyableRecord = StringifyableRecord +>(config: FsmConfig): FsmConfig { + return config; +} + +export const getState = ( + machineId: string, +): StateContext => { + logger.logMethodArgs('getState', machineId); + const detail = getDetail>(machineId); + if (detail == null) throw new Error('fsm_undefined', {cause: {machineId}}); + return detail.state; +}; + +export const setState = ( + machineId: string, + target: TState, + by: TEventId, +): void => { + logger.logMethodArgs('setState', machineId); + const detail = getDetail>(machineId); + if (detail == null) throw new Error('fsm_undefined', {cause: {machineId}}); + + detail.state = { + target, + from: detail.state.target, + by, + }; + + dispatch(machineId, detail, {debounce: 'NextCycle'}); +}; + +export const getContext = (machineId: string): TContext => { + logger.logMethodArgs('getContext', machineId); + const detail = getDetail>(machineId); + if (detail == null) throw new Error('fsm_undefined', {cause: {machineId}}); + return detail.context; +}; + +export const setContext = ( + machineId: string, + context: Partial, + notify?: boolean, +): void => { + logger.logMethodArgs('setContext', {machineId, context}); + const detail = getDetail>(machineId); + if (detail == null) throw new Error('fsm_undefined', {cause: {machineId}}); + + detail.context = { + ...detail.context, + ...context, + }; + + if (notify) { + dispatch(machineId, detail, {debounce: 'NextCycle'}); + } +}; + +export const transition = async < + TEventId extends string = string, + TContext extends StringifyableRecord = StringifyableRecord +>( + machineId: string, + event: TEventId, + context?: Partial, +): Promise => { + const detail = getDetail>(machineId); + if (detail == null) throw new Error('fsm_undefined', {cause: {machineId}}); + const config = fsmStorage[detail.name]; + if (config == null) throw new Error('fsm_undefined', {cause: {machineName: detail.name}}); + + const fromState = detail.state.target; + const transitionConfig = config.stateRecord[fromState]?.on[event] ?? config.stateRecord.$all?.on[event]; + + logger.logMethodArgs('transition', {machineId, fromState, event, context, target: transitionConfig?.target}); + + if (context !== undefined) { + detail.context = { + ...detail.context, + ...context, + }; + } + + if (transitionConfig == null) { + logger.incident( + 'transition', + 'invalid_target_state', + 'Defined target state for this event not found in state config', + { + fromState, + event, + events: {...config.stateRecord.$all?.on, ...config.stateRecord[fromState]?.on}, + }, + ); + return; + } + + // if ((await this.callFunction(transitionConfig.condition)) === false) { + // return; + // TODO: condition + // } + + transitionConfig.target ??= fromState; + setState(machineId, transitionConfig.target, event); +}; + +export const defineMachine = (machineId: string, config: TMachine): void => { + const detail = getDetail(machineId); + if (detail != null) throw new Error('fsm_exist', {cause: {machineId, config}}); + + fsmStorage[config.name] = config; + dispatch( + machineId, + { + name: config.name, + state: { + target: config.initial, + from: config.initial, + by: 'INIT', + }, + context: config.context, + }, + {debounce: 'NextCycle'}, + ); +}; + +export const stateMachineLookup = < + TMachine extends StateMachineHelper, + TContext extends TMachine['TContext'] = TMachine['TContext'] +>( + machineId: string, + ) => + ({ + defineMachine: defineMachine.bind(null, machineId) as OmitFirstParam, + getState: getState.bind(null, machineId) as OmitFirstParam< + typeof getState + >, + getContext: getContext.bind(null, machineId) as OmitFirstParam>, + setContext: setContext.bind(null, machineId) as OmitFirstParam>, + transition: transition.bind(null, machineId) as OmitFirstParam< + typeof transition + >, + } as const); + +// demo provider + +export const lightMachineConfig = contractStateMachine({ + name: 'light_machine', + context: { + a: 0, + b: 0, + }, + initial: 'green', + stateRecord: { + $all: { + entry: 'action_all_entry', + exit: 'action_all_exit', + on: { + POWER_LOST: { + target: 'flashingRed', + actions: 'action_all_power_lost', + }, + }, + }, + green: { + entry: 'action_green_entry', + exit: 'action_green_exit', + on: { + TIMER: { + target: 'yellow', + actions: 'action_green_timer', + condition: 'condition_green_timer', + }, + }, + }, + yellow: { + on: { + TIMER: { + target: 'red', + }, + }, + }, + red: { + on: { + TIMER: { + target: 'green', + }, + }, + }, + flashingRed: { + on: { + POWER_BACK: { + target: 'green', + }, + }, + }, + }, +}); + +export type LightMachine = StateMachineHelper; +const lightMachine = stateMachineLookup('light_machine_56'); + +lightMachine.defineMachine(lightMachineConfig); + +lightMachine.handleAction({ + 'asdasd': () => { + + }, +}); + +lightMachine.handleSignal([ + { + signalId: 'asdasd', + ... + } +]); From c5f5f3ccd7e53ea4633214b95f68def52470bddf Mon Sep 17 00:00:00 2001 From: "S. Amir Mohammad Najafi" Date: Tue, 14 Mar 2023 13:36:24 +0330 Subject: [PATCH 46/85] feat(com-pwa/select-product): upgrade to new fsm api --- .../com-pwa/src/ui/stuff/order-detail-base.ts | 2 +- .../com-pwa/src/ui/stuff/select-product.ts | 50 ++++++++++++------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/uniquely/com-pwa/src/ui/stuff/order-detail-base.ts b/uniquely/com-pwa/src/ui/stuff/order-detail-base.ts index e4af7cbd5..4adc148bd 100644 --- a/uniquely/com-pwa/src/ui/stuff/order-detail-base.ts +++ b/uniquely/com-pwa/src/ui/stuff/order-detail-base.ts @@ -250,7 +250,7 @@ export class AlwatrOrderDetailBase extends LocalizeMixin(SignalMixin(AlwatrBaseE const target = event.target as AlwatrTextField; if (target == null) return; const qty = target.value && +target.value ? +target.value : 100; - qtyUpdate(orderItem, qty - orderItem.qty); + // qtyUpdate(orderItem, qty - orderItem.qty); // FIXME: target.value = qty + ''; this.requestUpdate(); }}> diff --git a/uniquely/com-pwa/src/ui/stuff/select-product.ts b/uniquely/com-pwa/src/ui/stuff/select-product.ts index f4162728f..b11fc4ea5 100644 --- a/uniquely/com-pwa/src/ui/stuff/select-product.ts +++ b/uniquely/com-pwa/src/ui/stuff/select-product.ts @@ -14,10 +14,10 @@ import '@alwatr/ui-kit/card/product-card.js'; import {config} from '../../config.js'; -import type {OrderDraft} from '@alwatr/type/customer-order-management.js'; +import type {AlwatrDocumentStorage} from '@alwatr/type'; +import type {OrderDraft, Product, ProductPrice} from '@alwatr/type/customer-order-management.js'; import type {AlwatrProductCard, ProductCartContent} from '@alwatr/ui-kit/card/product-card.js'; - declare global { interface HTMLElementTagNameMap { 'alwatr-select-product': AlwatrSelectProduct; @@ -54,6 +54,15 @@ export class AlwatrSelectProduct extends LocalizeMixin(SignalMixin(UnresolvedMix @property() protected order?: OrderDraft; + @property() + protected productStorage?: AlwatrDocumentStorage; + + @property() + protected finalPriceStorage?: AlwatrDocumentStorage; + + @property() + protected priceStorage?: AlwatrDocumentStorage; + selectedRecord: Record = {}; override render(): unknown { @@ -75,9 +84,7 @@ export class AlwatrSelectProduct extends LocalizeMixin(SignalMixin(UnresolvedMix protected render_part_product_list(): unknown { this._logger.logMethod('render_part_product_list'); - const {productStorage, priceStorage, finalPriceStorage} = pageNewOrderStateMachine.context; - - if (productStorage == null || priceStorage == null || finalPriceStorage == null) { + if (this.productStorage == null || this.priceStorage == null || this.finalPriceStorage == null) { return this._logger.accident( 'render_part_product_list', 'context_not_valid', @@ -86,13 +93,13 @@ export class AlwatrSelectProduct extends LocalizeMixin(SignalMixin(UnresolvedMix ); } - return mapObject(this, productStorage?.data, (product) => { + return mapObject(this, this.productStorage?.data, (product) => { const content: ProductCartContent = { id: product.id, title: product.title.fa, imagePath: config.cdn + product.image.id, - price: priceStorage?.data[product.id]?.price ?? 0, - finalPrice: finalPriceStorage?.data[product.id]?.price ?? 0, + price: this.priceStorage?.data[product.id]?.price ?? 0, + finalPrice: this.finalPriceStorage?.data[product.id]?.price ?? 0, }; // this._logger.logProperty('selected', !!this.selectedRecord[product.id]); return html`event.target; const productId = target?.content?.id; - const {order, priceStorage, finalPriceStorage} = pageNewOrderStateMachine.context; this._logger.logMethodArgs('_selectedChanged', {productId}); - if (target == null || productId == null || priceStorage == null || finalPriceStorage == null) { + if ( + target == null || + productId == null || + this.priceStorage == null || + this.finalPriceStorage == null || + this.order == null + ) { return this._logger.accident( 'render_part_product_list', 'context_not_valid', 'Some context not valid', - {productId, priceStorage, finalPriceStorage}, + {productId, priceStorage: this.priceStorage, finalPriceStorage: this.finalPriceStorage}, ); } - order.itemList ??= []; + this.order.itemList ??= []; if (target.selected === true) { - order.itemList.push({ + this.order.itemList.push({ productId, qty: 0, - price: priceStorage.data[productId].price, - finalPrice: finalPriceStorage.data[productId].price, + price: this.priceStorage.data[productId].price, + finalPrice: this.finalPriceStorage.data[productId].price, }); } else { - const itemIndex = order.itemList.findIndex((item) => item.productId === productId); + const itemIndex = this.order.itemList.findIndex((item) => item.productId === productId); if (itemIndex !== -1) { - order.itemList.splice(itemIndex, 1); + this.order.itemList.splice(itemIndex, 1); } } const submitButton = this.renderRoot.querySelector('alwatr-button'); - if (submitButton && order.itemList.length < 2) { - submitButton.toggleAttribute('disabled', !order.itemList.length); + if (submitButton && this.order.itemList.length < 2) { + submitButton.toggleAttribute('disabled', !this.order.itemList.length); } } } From 39e1b9a128cc63de61887c9af522af45afc072cc Mon Sep 17 00:00:00 2001 From: "S. Amir Mohammad Najafi" Date: Tue, 14 Mar 2023 14:49:45 +0330 Subject: [PATCH 47/85] refactor(com-pwa/select-product): render --- .../com-pwa/src/ui/stuff/select-product.ts | 69 ++++++++----------- 1 file changed, 27 insertions(+), 42 deletions(-) diff --git a/uniquely/com-pwa/src/ui/stuff/select-product.ts b/uniquely/com-pwa/src/ui/stuff/select-product.ts index b11fc4ea5..2070fea20 100644 --- a/uniquely/com-pwa/src/ui/stuff/select-product.ts +++ b/uniquely/com-pwa/src/ui/stuff/select-product.ts @@ -8,6 +8,7 @@ import { mapObject, UnresolvedMixin, property, + nothing, } from '@alwatr/element'; import '@alwatr/ui-kit/button/button.js'; import '@alwatr/ui-kit/card/product-card.js'; @@ -25,7 +26,7 @@ declare global { } /** - * Alwatr Select Product. + * Alwatr Select Product Element. */ @customElement('alwatr-select-product') export class AlwatrSelectProduct extends LocalizeMixin(SignalMixin(UnresolvedMixin(AlwatrBaseElement))) { @@ -65,11 +66,9 @@ export class AlwatrSelectProduct extends LocalizeMixin(SignalMixin(UnresolvedMix selectedRecord: Record = {}; - override render(): unknown { - this._logger.logMethod('render'); - + override connectedCallback(): void { + super.connectedCallback(); this._updateSelectedRecord(); - return this.render_part_product_list(); } private _updateSelectedRecord(): void { @@ -81,55 +80,41 @@ export class AlwatrSelectProduct extends LocalizeMixin(SignalMixin(UnresolvedMix } } - protected render_part_product_list(): unknown { - this._logger.logMethod('render_part_product_list'); + override render(): unknown { + this._logger.logMethod('render'); if (this.productStorage == null || this.priceStorage == null || this.finalPriceStorage == null) { - return this._logger.accident( - 'render_part_product_list', - 'context_not_valid', - 'Some context not valid', - this.order, - ); + this._logger.accident('render_part_product_list', 'context_not_valid', 'Some context not valid', this.order); + return nothing; } - return mapObject(this, this.productStorage?.data, (product) => { - const content: ProductCartContent = { - id: product.id, - title: product.title.fa, - imagePath: config.cdn + product.image.id, - price: this.priceStorage?.data[product.id]?.price ?? 0, - finalPrice: this.finalPriceStorage?.data[product.id]?.price ?? 0, - }; - // this._logger.logProperty('selected', !!this.selectedRecord[product.id]); - return html``; - }); + return mapObject(this, this.productStorage?.data, this.render_part_product_card); + } + + protected render_part_product_card(product: Product): unknown { + if (this.productStorage == null || this.priceStorage == null || this.finalPriceStorage == null) return; + const content: ProductCartContent = { + id: product.id, + title: product.title.fa, + imagePath: config.cdn + product.image.id, + price: this.priceStorage?.data[product.id]?.price ?? 0, + finalPrice: this.finalPriceStorage?.data[product.id]?.price ?? 0, + }; + return html``; } private _selectedChanged(event: CustomEvent): void { + if (this.order == null || this.priceStorage == null || this.finalPriceStorage == null) return; const target = event.target; const productId = target?.content?.id; this._logger.logMethodArgs('_selectedChanged', {productId}); - if ( - target == null || - productId == null || - this.priceStorage == null || - this.finalPriceStorage == null || - this.order == null - ) { - return this._logger.accident( - 'render_part_product_list', - 'context_not_valid', - 'Some context not valid', - {productId, priceStorage: this.priceStorage, finalPriceStorage: this.finalPriceStorage}, - ); - } + if (target == null || productId == null) return; this.order.itemList ??= []; if (target.selected === true) { From 43ce284d3e3d2e16e255d756044588c817f08032 Mon Sep 17 00:00:00 2001 From: "S. Amir Mohammad Najafi" Date: Tue, 14 Mar 2023 16:06:24 +0330 Subject: [PATCH 48/85] refactor(com-pwa): productStorage --- .../manager/context-provider/order-storage.ts | 5 +- .../context-provider/product-storage.ts | 114 +++++++++++++----- uniquely/com-pwa/src/manager/index.ts | 1 + 3 files changed, 89 insertions(+), 31 deletions(-) diff --git a/uniquely/com-pwa/src/manager/context-provider/order-storage.ts b/uniquely/com-pwa/src/manager/context-provider/order-storage.ts index a12213e44..979698a87 100644 --- a/uniquely/com-pwa/src/manager/context-provider/order-storage.ts +++ b/uniquely/com-pwa/src/manager/context-provider/order-storage.ts @@ -7,9 +7,8 @@ import {logger} from '../logger.js'; import type {AlwatrDocumentStorage, User} from '@alwatr/type'; import type {Order} from '@alwatr/type/customer-order-management.js'; -const orderStorageContextProvider = requestableContextProvider.bind>( - 'order-storage-context', -); +const orderStorageContextProvider = + requestableContextProvider.bind>('order-storage-context'); const userContextConsumer = contextConsumer.bind('user-context'); diff --git a/uniquely/com-pwa/src/manager/context-provider/product-storage.ts b/uniquely/com-pwa/src/manager/context-provider/product-storage.ts index f471751c4..699fa527e 100644 --- a/uniquely/com-pwa/src/manager/context-provider/product-storage.ts +++ b/uniquely/com-pwa/src/manager/context-provider/product-storage.ts @@ -1,37 +1,95 @@ -import {fetchContext} from '@alwatr/fetch'; -import {l18eReadyPromise} from '@alwatr/i18n'; -import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; +import {serviceRequest, type FetchOptions} from '@alwatr/fetch'; +import {DispatchOptions, requestableContextProvider} from '@alwatr/signal'; +import {Product} from '@alwatr/type/src/customer-order-management.js'; import {config} from '../../config.js'; import {logger} from '../logger.js'; -export const fetchProductStorage = async (productStorageName = 'tile'): Promise => { - logger.logMethod('fetchProductStorage'); +import type {AlwatrDocumentStorage} from '@alwatr/type'; + +const productStorageContextProvider = requestableContextProvider.bind< + AlwatrDocumentStorage, + {productStorageName: string} +>('product-storage-context'); + +productStorageContextProvider.setProvider(async (args) => { + logger.logMethod('requestProductStorageContext'); + let context = productStorageContextProvider.getValue(); + + if (context.state === 'pending' || context.state === 'reloading') return; + + const dispatchOptions: Partial = {debounce: 'NextCycle'}; + const fetchOption: FetchOptions = { + ...config.fetchContextOptions, + url: config.api + '/product-list/', + queryParameters: { + storage: args.productStorageName, + }, + }; + + if (context.state === 'initial') { + context = {state: 'pending'}; + productStorageContextProvider.setValue(context, dispatchOptions); + try { + fetchOption.cacheStrategy = 'cache_only'; + const response = (await serviceRequest(fetchOption)) as AlwatrDocumentStorage; + context = { + state: 'reloading', + content: response, + }; + productStorageContextProvider.setValue(context, dispatchOptions); + } + catch (err) { + if ((err as Error).message === 'fetch_cache_not_found') { + logger.logOther('requestProductStorageContext:', 'fetch_cache_not_found'); + } + else { + logger.error('requestProductStorageContext', 'fetch_failed', err); + context = {state: 'error'}; + productStorageContextProvider.setValue(context, dispatchOptions); + return; + } + } + } + + if (navigator.onLine === false) { + context = { + state: 'error', + content: context.content, // maybe offline exist + }; + productStorageContextProvider.setValue(context, dispatchOptions); + return; + } try { - await fetchContext( - `product-storage-${productStorageName}-context`, - { - ...config.fetchContextOptions, - url: config.api + '/product-list/', - queryParameters: { - storage: productStorageName, - }, - }, - {debounce: 'NextCycle'}, - ); + fetchOption.cacheStrategy = 'update_cache'; + const response = (await serviceRequest(fetchOption)) as AlwatrDocumentStorage; + if ( + context.content != null && + response.meta?.lastUpdated != undefined && + response.meta.lastUpdated === context.content.meta?.lastUpdated + ) { + // no changed + context = { + state: 'complete', + content: context.content, + }; + } + else { + context = { + state: 'complete', + content: response, + }; + } + productStorageContextProvider.setValue(context, dispatchOptions); } catch (err) { - // TODO: refactor - logger.error('provideProductStorageContext', 'fetch_failed', err); - await l18eReadyPromise; - const response = await snackbarSignalTrigger.requestWithResponse({ - messageKey: 'fetch_failed', - actionLabelKey: 'retry', - duration: -1, - }); - if (response.actionButton) { - await fetchProductStorage(productStorageName); - } + logger.error('fetchContext', 'fetch_failed', err); + context = { + state: 'error', + content: context.content, // maybe offline exist + }; + productStorageContextProvider.setValue(context, dispatchOptions); + return; } -}; +}); diff --git a/uniquely/com-pwa/src/manager/index.ts b/uniquely/com-pwa/src/manager/index.ts index 11e6b976b..38c54539c 100644 --- a/uniquely/com-pwa/src/manager/index.ts +++ b/uniquely/com-pwa/src/manager/index.ts @@ -1,5 +1,6 @@ import './context-provider/home-page-content.js'; import './context-provider/l18e.js'; import './context-provider/order-storage.js'; +import './context-provider/product-storage.js'; import './context-provider/user.js'; import './submit-order-command-handler.js'; From ea197001e4c653fe4f22d2dcbcaeaf0eabda5110 Mon Sep 17 00:00:00 2001 From: "S. Amir Mohammad Najafi" Date: Tue, 14 Mar 2023 16:07:35 +0330 Subject: [PATCH 49/85] fix(com-pwa): orderId property --- uniquely/com-pwa/src/ui/page/order-detail.ts | 133 +++++++++++-------- 1 file changed, 74 insertions(+), 59 deletions(-) diff --git a/uniquely/com-pwa/src/ui/page/order-detail.ts b/uniquely/com-pwa/src/ui/page/order-detail.ts index d929ed2f1..882a79ea8 100644 --- a/uniquely/com-pwa/src/ui/page/order-detail.ts +++ b/uniquely/com-pwa/src/ui/page/order-detail.ts @@ -1,11 +1,4 @@ -import { - customElement, - FiniteStateMachineController, - html, - property, - state, - UnresolvedMixin, -} from '@alwatr/element'; +import {customElement, FiniteStateMachineController, html, property, state, UnresolvedMixin} from '@alwatr/element'; import {message} from '@alwatr/i18n'; import {topAppBarContextProvider} from '@alwatr/pwa-helper/context.js'; import {redirect} from '@alwatr/router'; @@ -27,8 +20,10 @@ declare global { const orderStorageContextConsumer = requestableContextConsumer.bind>('order-storage-context'); -const productStorageContextConsumer = - requestableContextConsumer.bind>('product-storage-tile-context'); +const productStorageContextConsumer = requestableContextConsumer.bind< + AlwatrDocumentStorage, + {productStorageName: string} +>('product-storage-context'); const buttons = { backToOrderList: { @@ -57,22 +52,18 @@ export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase productStorage: | null> null, }, stateRecord: { - '$all': { + $all: { entry: (): void => { this.gotState = this._stateMachine.state.target; }, on: {}, }, - 'pending': { + pending: { entry: (): void => { const orderContext = orderStorageContextConsumer.getValue(); const productContext = productStorageContextConsumer.getValue(); - if (orderContext.state === 'initial') { - orderStorageContextConsumer.request(null); - } - if (productContext.state === 'initial') { - productStorageContextConsumer.request(null); - } + if (orderContext.state === 'initial') orderStorageContextConsumer.request(null); + if (productContext.state === 'initial') productStorageContextConsumer.request({productStorageName: 'tile'}); }, on: { context_request_initial: {}, @@ -83,14 +74,21 @@ export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase context_request_complete: { target: 'detail', condition: (): boolean => { - if (this.orderId == null) { + const orderStorage = orderStorageContextConsumer.getValue(); + const productStorage = productStorageContextConsumer.getValue(); + if (orderStorage.state !== 'complete' || productStorage.state !== 'complete') { + return false; + } + + if (this.orderId == null || orderStorage.content.data[this.orderId] == null) { this._stateMachine.transition('not_found'); return false; } - if (orderStorageContextConsumer.getValue().state === 'complete' && - productStorageContextConsumer.getValue().state === 'complete' - ) return true; - return false; + else { + this._stateMachine.context.orderId = this.orderId; + } + + return true; }, }, context_request_reloading: { @@ -98,24 +96,24 @@ export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase }, }, }, - 'contextError': { + contextError: { on: { request_context: { target: 'pending', actions: (): void => { orderStorageContextConsumer.request(null); - productStorageContextConsumer.request(null); + productStorageContextConsumer.request({productStorageName: 'tile'}); }, }, }, }, - 'detail': { + detail: { on: { request_context: { target: 'reloading', actions: (): void => { orderStorageContextConsumer.request(null); - productStorageContextConsumer.request(null); + productStorageContextConsumer.request({productStorageName: 'tile'}); }, }, not_found: { @@ -123,64 +121,81 @@ export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase }, }, }, - 'notFound': { + notFound: { on: {}, }, - 'reloading': { + reloading: { on: { - context_request_error: { - target: 'detail', - actions: (): void => - snackbarSignalTrigger.request({messageKey: 'fetch_failed_description'}), - }, context_request_complete: { target: 'detail', condition: (): boolean => { - if (orderStorageContextConsumer.getValue().state === 'complete' && - productStorageContextConsumer.getValue().state === 'complete' - ) return true; - return false; + const orderStorage = orderStorageContextConsumer.getValue(); + const productStorage = productStorageContextConsumer.getValue(); + if (orderStorage.state !== 'complete' || productStorage.state !== 'complete') { + return false; + } + + if (this.orderId == null || orderStorage.content.data[this.orderId] == null) { + this._stateMachine.transition('not_found'); + return false; + } + else { + this._stateMachine.context.orderId = this.orderId; + } + + return true; }, }, + context_request_reloading: {}, + context_request_error: { + target: 'detail', + actions: (): void => snackbarSignalTrigger.request({messageKey: 'fetch_failed_description'}), + }, + not_found: { + target: 'notFound', + }, }, }, }, signalList: [ { signalId: buttons.backToOrderList.clickSignalId, - actions: (): void => {redirect({sectionList: ['order-list']});}, + actions: (): void => { + redirect({sectionList: ['order-list']}); + }, }, { signalId: buttons.reload.clickSignalId, - transition: - 'request_context', + transition: 'request_context', }, - ]}); + ], + }); @state() gotState = this._stateMachine.state.target; @property({type: Number}) - get orderId(): number { - return this.orderId; - } - set orderId(orderId: number) { - this._stateMachine.transition('context_request_complete', {orderId}); - } + orderId?: number; override connectedCallback(): void { super.connectedCallback(); this._signalListenerList.push( - orderStorageContextConsumer.subscribe((context) => { - this._stateMachine.transition(`context_request_${context.state}`, {orderStorage: context.content}); - }, {receivePrevious: 'NextCycle'}), + orderStorageContextConsumer.subscribe( + (context) => { + this._stateMachine.transition(`context_request_${context.state}`, {orderStorage: context.content}); + }, + {receivePrevious: 'NextCycle'}, + ), ); this._signalListenerList.push( - productStorageContextConsumer.subscribe((context) => { - this._stateMachine.transition(`context_request_${context.state}`, {productStorage: context.content}); - }, {receivePrevious: 'NextCycle'}), + productStorageContextConsumer.subscribe( + (context) => { + this._stateMachine.transition(`context_request_${context.state}`, {productStorage: context.content}); + }, + {receivePrevious: 'NextCycle'}, + ), ); } @@ -188,7 +203,7 @@ export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase this._logger.logMethod('render'); return this._stateMachine.render({ - 'pending': () => { + pending: () => { topAppBarContextProvider.setValue({ headlineKey: 'page_order_detail_headline', startIcon: buttons.backToOrderList, @@ -202,7 +217,7 @@ export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase return html``; }, - 'contextError': () => { + contextError: () => { topAppBarContextProvider.setValue({ headlineKey: 'page_order_detail_headline', startIcon: buttons.backToOrderList, @@ -222,9 +237,9 @@ export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase `; }, - 'reloading': 'detail', + reloading: 'detail', - 'detail': () => { + detail: () => { topAppBarContextProvider.setValue({ headlineKey: 'page_order_detail_headline', startIcon: buttons.backToOrderList, @@ -240,7 +255,7 @@ export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase ]; }, - 'notFound': () => { + notFound: () => { const content: IconBoxContent = { headline: message('page_order_detail_not_found'), icon: 'close', From 4738f74ba7e8719c986281a28fcab80a948c54b1 Mon Sep 17 00:00:00 2001 From: "S. Amir Mohammad Najafi" Date: Tue, 14 Mar 2023 17:26:41 +0330 Subject: [PATCH 50/85] refactor(com-pwa): priceProductStorage --- .../manager/context-provider/price-storage.ts | 216 ++++++++++++++---- ...ct-storage.ts => product-price-storage.ts} | 0 uniquely/com-pwa/src/manager/index.ts | 1 + 3 files changed, 175 insertions(+), 42 deletions(-) rename uniquely/com-pwa/src/manager/context-provider/{product-storage.ts => product-price-storage.ts} (100%) diff --git a/uniquely/com-pwa/src/manager/context-provider/price-storage.ts b/uniquely/com-pwa/src/manager/context-provider/price-storage.ts index 7cdddc4f3..3406f0f54 100644 --- a/uniquely/com-pwa/src/manager/context-provider/price-storage.ts +++ b/uniquely/com-pwa/src/manager/context-provider/price-storage.ts @@ -1,50 +1,182 @@ -import {fetchContext} from '@alwatr/fetch'; -import {l18eReadyPromise} from '@alwatr/i18n'; -import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; +import {serviceRequest, type FetchOptions} from '@alwatr/fetch'; +import {DispatchOptions, requestableContextProvider} from '@alwatr/signal'; +import {ProductPrice} from '@alwatr/type/src/customer-order-management.js'; import {config} from '../../config.js'; import {logger} from '../logger.js'; -export const fetchPriceStorage = async (productStorageName = 'tile'): Promise => { - logger.logMethod('fetchPriceStorage'); +import type {AlwatrDocumentStorage} from '@alwatr/type'; + +const productPriceStorageContextProvider = requestableContextProvider.bind< + AlwatrDocumentStorage, + {productPriceStorageName: string} +>('product-price-context'); + +const finalProductPriceStorageContextProvider = requestableContextProvider.bind< + AlwatrDocumentStorage, + {productPriceStorageName: string} +>('final-product-price-context'); + +productPriceStorageContextProvider.setProvider(async (args) => { + logger.logMethod('requestProductPriceStorageContext'); + let context = productPriceStorageContextProvider.getValue(); + + if (context.state === 'pending' || context.state === 'reloading') return; + + const dispatchOptions: Partial = {debounce: 'NextCycle'}; + const fetchOption: FetchOptions = { + ...config.fetchContextOptions, + url: config.api + '/price-list/', + queryParameters: { + name: config.priceListName.replace('${productStorage}', args.productPriceStorageName), + }, + }; + + if (context.state === 'initial') { + context = {state: 'pending'}; + productPriceStorageContextProvider.setValue(context, dispatchOptions); + try { + fetchOption.cacheStrategy = 'cache_only'; + const response = (await serviceRequest(fetchOption)) as AlwatrDocumentStorage; + context = { + state: 'reloading', + content: response, + }; + productPriceStorageContextProvider.setValue(context, dispatchOptions); + } + catch (err) { + if ((err as Error).message === 'fetch_cache_not_found') { + logger.logOther('requestProductPriceStorageContext:', 'fetch_cache_not_found'); + } + else { + logger.error('requestProductPriceStorageContext', 'fetch_failed', err); + context = {state: 'error'}; + productPriceStorageContextProvider.setValue(context, dispatchOptions); + return; + } + } + } + + if (navigator.onLine === false) { + context = { + state: 'error', + content: context.content, // maybe offline exist + }; + productPriceStorageContextProvider.setValue(context, dispatchOptions); + return; + } + + try { + fetchOption.cacheStrategy = 'update_cache'; + const response = (await serviceRequest(fetchOption)) as AlwatrDocumentStorage; + if ( + context.content != null && + response.meta?.lastUpdated != undefined && + response.meta.lastUpdated === context.content.meta?.lastUpdated + ) { + // no changed + context = { + state: 'complete', + content: context.content, + }; + } + else { + context = { + state: 'complete', + content: response, + }; + } + productPriceStorageContextProvider.setValue(context, dispatchOptions); + } + catch (err) { + logger.error('fetchContext', 'fetch_failed', err); + context = { + state: 'error', + content: context.content, // maybe offline exist + }; + productPriceStorageContextProvider.setValue(context, dispatchOptions); + return; + } +}); + +finalProductPriceStorageContextProvider.setProvider(async (args) => { + logger.logMethod('requestFinalProductPriceStorageContext'); + let context = finalProductPriceStorageContextProvider.getValue(); + + if (context.state === 'pending' || context.state === 'reloading') return; + + const dispatchOptions: Partial = {debounce: 'NextCycle'}; + const fetchOption: FetchOptions = { + ...config.fetchContextOptions, + url: config.api + '/price-list/', + queryParameters: { + name: config.finalPriceListName.replace('${productStorage}', args.productPriceStorageName), + }, + }; + + if (context.state === 'initial') { + context = {state: 'pending'}; + finalProductPriceStorageContextProvider.setValue(context, dispatchOptions); + try { + fetchOption.cacheStrategy = 'cache_only'; + const response = (await serviceRequest(fetchOption)) as AlwatrDocumentStorage; + context = { + state: 'reloading', + content: response, + }; + finalProductPriceStorageContextProvider.setValue(context, dispatchOptions); + } + catch (err) { + if ((err as Error).message === 'fetch_cache_not_found') { + logger.logOther('requestFinalProductPriceStorageContext:', 'fetch_cache_not_found'); + } + else { + logger.error('requestFinalProductPriceStorageContext', 'fetch_failed', err); + context = {state: 'error'}; + finalProductPriceStorageContextProvider.setValue(context, dispatchOptions); + return; + } + } + } + + if (navigator.onLine === false) { + context = { + state: 'error', + content: context.content, // maybe offline exist + }; + finalProductPriceStorageContextProvider.setValue(context, dispatchOptions); + return; + } try { - void await Promise.all([ - fetchContext( - `price-storage-${productStorageName}-context`, - { - ...config.fetchContextOptions, - url: config.api + '/price-list/', - queryParameters: { - name: config.priceListName.replace('${productStorage}', productStorageName), - }, - }, - {debounce: 'NextCycle'}, - ), - fetchContext( - `final-price-storage-${productStorageName}-context`, - { - ...config.fetchContextOptions, - url: config.api + '/price-list/', - queryParameters: { - name: config.finalPriceListName.replace('${productStorage}', productStorageName), - }, - }, - {debounce: 'NextCycle'}, - ), - ]); + fetchOption.cacheStrategy = 'update_cache'; + const response = (await serviceRequest(fetchOption)) as AlwatrDocumentStorage; + if ( + context.content != null && + response.meta?.lastUpdated != undefined && + response.meta.lastUpdated === context.content.meta?.lastUpdated + ) { + // no changed + context = { + state: 'complete', + content: context.content, + }; + } + else { + context = { + state: 'complete', + content: response, + }; + } + finalProductPriceStorageContextProvider.setValue(context, dispatchOptions); } catch (err) { - // TODO: refactor - logger.error('provideProductStorageContext', 'fetch_failed', err); - await l18eReadyPromise; - const response = await snackbarSignalTrigger.requestWithResponse({ - messageKey: 'fetch_failed', - actionLabelKey: 'retry', - duration: -1, - }); - if (response.actionButton) { - await fetchPriceStorage(productStorageName); - } - } -}; + logger.error('fetchContext', 'fetch_failed', err); + context = { + state: 'error', + content: context.content, // maybe offline exist + }; + finalProductPriceStorageContextProvider.setValue(context, dispatchOptions); + return; + } +}); diff --git a/uniquely/com-pwa/src/manager/context-provider/product-storage.ts b/uniquely/com-pwa/src/manager/context-provider/product-price-storage.ts similarity index 100% rename from uniquely/com-pwa/src/manager/context-provider/product-storage.ts rename to uniquely/com-pwa/src/manager/context-provider/product-price-storage.ts diff --git a/uniquely/com-pwa/src/manager/index.ts b/uniquely/com-pwa/src/manager/index.ts index 38c54539c..8d49127f5 100644 --- a/uniquely/com-pwa/src/manager/index.ts +++ b/uniquely/com-pwa/src/manager/index.ts @@ -1,6 +1,7 @@ import './context-provider/home-page-content.js'; import './context-provider/l18e.js'; import './context-provider/order-storage.js'; +import './context-provider/product-price-storage.js'; import './context-provider/product-storage.js'; import './context-provider/user.js'; import './submit-order-command-handler.js'; From 1ff13428e706b2424f9fb2b54702983bbf611a55 Mon Sep 17 00:00:00 2001 From: "S. Amir Mohammad Najafi" Date: Tue, 14 Mar 2023 17:26:57 +0330 Subject: [PATCH 51/85] refactor(com-pwa/new-order): priceProductStorage (not completed) --- uniquely/com-pwa/src/ui/page/new-order.ts | 56 +++++++++++++++-------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/uniquely/com-pwa/src/ui/page/new-order.ts b/uniquely/com-pwa/src/ui/page/new-order.ts index 797eeaaae..99d652271 100644 --- a/uniquely/com-pwa/src/ui/page/new-order.ts +++ b/uniquely/com-pwa/src/ui/page/new-order.ts @@ -28,15 +28,20 @@ declare global { } } -const productStorageContextConsumer = - requestableContextConsumer.bind>('product-storage-tile-context'); +const productStorageContextConsumer = requestableContextConsumer.bind< + AlwatrDocumentStorage, + {productStorageName: string} +>('product-storage-context'); -const priceStorageContextConsumer = - requestableContextConsumer.bind>('price-storage-tile-context'); +const productPriceStorageContextProvider = requestableContextConsumer.bind< + AlwatrDocumentStorage, + {productPriceStorageName: string} +>('product-price-context'); -const finalPriceStorageContextConsumer = requestableContextConsumer.bind>( - 'final-price-storage-tile-context', -); +const finalProductPriceStorageContextProvider = requestableContextConsumer.bind< + AlwatrDocumentStorage, + {productPriceStorageName: string} +>('final-product-price-context'); const newOrderLocalStorageKey = 'draft-order-x2'; @@ -124,11 +129,15 @@ export class AlwatrPageNewOrder extends UnresolvedMixin(AlwatrOrderDetailBase) { pending: { entry: (): void => { const productStorage = productStorageContextConsumer.getValue(); - const priceStorage = priceStorageContextConsumer.getValue(); - const finalPriceStorage = finalPriceStorageContextConsumer.getValue(); - if (productStorage.state == 'initial') productStorageContextConsumer.request(null); - if (priceStorage.state == 'initial') priceStorageContextConsumer.request(null); - if (finalPriceStorage.state == 'initial') finalPriceStorageContextConsumer.request(null); + const priceStorage = productPriceStorageContextProvider.getValue(); + const finalPriceStorage = finalProductPriceStorageContextProvider.getValue(); + if (productStorage.state == 'initial') productStorageContextConsumer.request({productStorageName: 'tile'}); + if (priceStorage.state == 'initial') { + productPriceStorageContextProvider.request({productPriceStorageName: 'tile'}); + } + if (finalPriceStorage.state == 'initial') { + finalProductPriceStorageContextProvider.request({productPriceStorageName: 'tile'}); + } }, on: { context_request_initial: {}, @@ -141,8 +150,8 @@ export class AlwatrPageNewOrder extends UnresolvedMixin(AlwatrOrderDetailBase) { condition: (): boolean => { if ( productStorageContextConsumer.getValue().state === 'complete' && - priceStorageContextConsumer.getValue().state === 'complete' && - finalPriceStorageContextConsumer.getValue().state === 'complete' + productPriceStorageContextProvider.getValue().state === 'complete' && + finalProductPriceStorageContextProvider.getValue().state === 'complete' ) { return true; } @@ -159,9 +168,9 @@ export class AlwatrPageNewOrder extends UnresolvedMixin(AlwatrOrderDetailBase) { request_context: { target: 'pending', actions: (): void => { - productStorageContextConsumer.request(null); - priceStorageContextConsumer.request(null); - finalPriceStorageContextConsumer.request(null); + productStorageContextConsumer.request({productStorageName: 'tile'}); + productPriceStorageContextProvider.request({productPriceStorageName: 'tile'}); + finalProductPriceStorageContextProvider.request({productPriceStorageName: 'tile'}); }, }, }, @@ -361,7 +370,7 @@ export class AlwatrPageNewOrder extends UnresolvedMixin(AlwatrOrderDetailBase) { ); this._signalListenerList.push( - priceStorageContextConsumer.subscribe( + productPriceStorageContextProvider.subscribe( (context) => { this._stateMachine.transition(`context_request_${context.state}`, {priceStorage: context.content}); }, @@ -370,7 +379,16 @@ export class AlwatrPageNewOrder extends UnresolvedMixin(AlwatrOrderDetailBase) { ); this._signalListenerList.push( - finalPriceStorageContextConsumer.subscribe( + finalProductPriceStorageContextProvider.subscribe( + (context) => { + this._stateMachine.transition(`context_request_${context.state}`, {finalPriceStorage: context.content}); + }, + {receivePrevious: 'NextCycle'}, + ), + ); + + this._signalListenerList.push( + finalProductPriceStorageContextProvider.subscribe( (context) => { this._stateMachine.transition(`context_request_${context.state}`, {finalPriceStorage: context.content}); }, From aa432644d76a0f81ea6e5c3b93da63f998ab159c Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 05:48:55 +0330 Subject: [PATCH 52/85] fix(fsm): cleanup old --- core/signal/src/fsm.ts | 333 ------------------ ui/element/src/index.ts | 2 - .../finite-state-machine.ts | 59 ---- 3 files changed, 394 deletions(-) delete mode 100644 core/signal/src/fsm.ts delete mode 100644 ui/element/src/reactive-controllers/finite-state-machine.ts diff --git a/core/signal/src/fsm.ts b/core/signal/src/fsm.ts deleted file mode 100644 index 4acfe99cc..000000000 --- a/core/signal/src/fsm.ts +++ /dev/null @@ -1,333 +0,0 @@ -import {dispatch, getDetail, logger} from './core.js'; - -import type {OmitFirstParam, SingleOrArray, StringifyableRecord} from '@alwatr/type'; - -export interface FsmConfig< - TState extends string = string, - TEventId extends string = string, - TActionName extends string = string, - TContext extends StringifyableRecord = StringifyableRecord -> extends StringifyableRecord { - name: string; - - /** - * Initial context. - */ - context: TContext; - - /** - * Initial state. - */ - initial: TState; - - /** - * Define state list - */ - stateRecord: StateRecord; -} - -export type StateRecord = { - [S in TState | '$all']: { - /** - * On state exit actions - */ - exit?: SingleOrArray; - - /** - * On state entry actions - */ - entry?: SingleOrArray; - - /** - * An object mapping eventId to state. - * - * Example: - * - * ```ts - * stateRecord: { - * on: { - * TIMER: { - * target: 'green', - * condition: () => car.gas > 0, - * actions: () => car.go(), - * } - * } - * } - * ``` - */ - on: { - [E in TEventId]?: TransitionConfig | undefined; - }; - }; -}; - -export interface StateContext extends StringifyableRecord { - /** - * Current state - */ - target: TState; - /** - * Last state - */ - from: TState; - /** - * Transition event - */ - by: TEventId | 'INIT'; -} - -export interface TransitionConfig - extends StringifyableRecord { - target?: TState; - condition?: TActionName; - actions?: SingleOrArray; -} - -export interface SignalDetail< - TState extends string = string, - TEventId extends string = string, - TContext extends StringifyableRecord = StringifyableRecord -> extends StringifyableRecord { - name: string; - state: StateContext; - context: TContext; -} - -// type helper - -export type TState = Exclude; -export type TEventId = keyof TMachine['stateRecord'][TState]['on']; -export type TActionName = TMachine['stateRecord'][TState]['entry']; -export type TContext = TMachine['context']; - -export type StateMachineHelper = Readonly<{ - TState: Exclude; - TEventId: keyof TMachine['stateRecord'][StateMachineHelper['TState']]['on']; - TActionName: TMachine['stateRecord'][StateMachineHelper['TState']]['entry']; - TContext: TMachine['context']; -}>; - -// ---- - -const fsmStorage: Record = {}; - -export function contractStateMachine< - TState extends string = string, - TEventId extends string = string, - TActionName extends string = string, - TContext extends StringifyableRecord = StringifyableRecord ->(config: FsmConfig): FsmConfig { - return config; -} - -export const getState = ( - machineId: string, -): StateContext => { - logger.logMethodArgs('getState', machineId); - const detail = getDetail>(machineId); - if (detail == null) throw new Error('fsm_undefined', {cause: {machineId}}); - return detail.state; -}; - -export const setState = ( - machineId: string, - target: TState, - by: TEventId, -): void => { - logger.logMethodArgs('setState', machineId); - const detail = getDetail>(machineId); - if (detail == null) throw new Error('fsm_undefined', {cause: {machineId}}); - - detail.state = { - target, - from: detail.state.target, - by, - }; - - dispatch(machineId, detail, {debounce: 'NextCycle'}); -}; - -export const getContext = (machineId: string): TContext => { - logger.logMethodArgs('getContext', machineId); - const detail = getDetail>(machineId); - if (detail == null) throw new Error('fsm_undefined', {cause: {machineId}}); - return detail.context; -}; - -export const setContext = ( - machineId: string, - context: Partial, - notify?: boolean, -): void => { - logger.logMethodArgs('setContext', {machineId, context}); - const detail = getDetail>(machineId); - if (detail == null) throw new Error('fsm_undefined', {cause: {machineId}}); - - detail.context = { - ...detail.context, - ...context, - }; - - if (notify) { - dispatch(machineId, detail, {debounce: 'NextCycle'}); - } -}; - -export const transition = async < - TEventId extends string = string, - TContext extends StringifyableRecord = StringifyableRecord ->( - machineId: string, - event: TEventId, - context?: Partial, -): Promise => { - const detail = getDetail>(machineId); - if (detail == null) throw new Error('fsm_undefined', {cause: {machineId}}); - const config = fsmStorage[detail.name]; - if (config == null) throw new Error('fsm_undefined', {cause: {machineName: detail.name}}); - - const fromState = detail.state.target; - const transitionConfig = config.stateRecord[fromState]?.on[event] ?? config.stateRecord.$all?.on[event]; - - logger.logMethodArgs('transition', {machineId, fromState, event, context, target: transitionConfig?.target}); - - if (context !== undefined) { - detail.context = { - ...detail.context, - ...context, - }; - } - - if (transitionConfig == null) { - logger.incident( - 'transition', - 'invalid_target_state', - 'Defined target state for this event not found in state config', - { - fromState, - event, - events: {...config.stateRecord.$all?.on, ...config.stateRecord[fromState]?.on}, - }, - ); - return; - } - - // if ((await this.callFunction(transitionConfig.condition)) === false) { - // return; - // TODO: condition - // } - - transitionConfig.target ??= fromState; - setState(machineId, transitionConfig.target, event); -}; - -export const defineMachine = (machineId: string, config: TMachine): void => { - const detail = getDetail(machineId); - if (detail != null) throw new Error('fsm_exist', {cause: {machineId, config}}); - - fsmStorage[config.name] = config; - dispatch( - machineId, - { - name: config.name, - state: { - target: config.initial, - from: config.initial, - by: 'INIT', - }, - context: config.context, - }, - {debounce: 'NextCycle'}, - ); -}; - -export const stateMachineLookup = < - TMachine extends StateMachineHelper, - TContext extends TMachine['TContext'] = TMachine['TContext'] ->( - machineId: string, - ) => - ({ - defineMachine: defineMachine.bind(null, machineId) as OmitFirstParam, - getState: getState.bind(null, machineId) as OmitFirstParam< - typeof getState - >, - getContext: getContext.bind(null, machineId) as OmitFirstParam>, - setContext: setContext.bind(null, machineId) as OmitFirstParam>, - transition: transition.bind(null, machineId) as OmitFirstParam< - typeof transition - >, - } as const); - -// demo provider - -export const lightMachineConfig = contractStateMachine({ - name: 'light_machine', - context: { - a: 0, - b: 0, - }, - initial: 'green', - stateRecord: { - $all: { - entry: 'action_all_entry', - exit: 'action_all_exit', - on: { - POWER_LOST: { - target: 'flashingRed', - actions: 'action_all_power_lost', - }, - }, - }, - green: { - entry: 'action_green_entry', - exit: 'action_green_exit', - on: { - TIMER: { - target: 'yellow', - actions: 'action_green_timer', - condition: 'condition_green_timer', - }, - }, - }, - yellow: { - on: { - TIMER: { - target: 'red', - }, - }, - }, - red: { - on: { - TIMER: { - target: 'green', - }, - }, - }, - flashingRed: { - on: { - POWER_BACK: { - target: 'green', - }, - }, - }, - }, -}); - -export type LightMachine = StateMachineHelper; -const lightMachine = stateMachineLookup('light_machine_56'); - -lightMachine.defineMachine(lightMachineConfig); - -lightMachine.handleAction({ - 'asdasd': () => { - - }, -}); - -lightMachine.handleSignal([ - { - signalId: 'asdasd', - ... - } -]); diff --git a/ui/element/src/index.ts b/ui/element/src/index.ts index 87b3a35e9..4d86b7efd 100644 --- a/ui/element/src/index.ts +++ b/ui/element/src/index.ts @@ -13,8 +13,6 @@ export * from './mixins/schedule-update-to-frame.js'; export * from './directives/map.js'; -export * from './reactive-controllers/finite-state-machine.js'; - export * from './lit.js'; globalAlwatr.registeredList.push({ diff --git a/ui/element/src/reactive-controllers/finite-state-machine.ts b/ui/element/src/reactive-controllers/finite-state-machine.ts deleted file mode 100644 index 38c09ffbf..000000000 --- a/ui/element/src/reactive-controllers/finite-state-machine.ts +++ /dev/null @@ -1,59 +0,0 @@ -import {FiniteStateMachine, type FsmConfig} from '@alwatr/fsm'; - -import {type ReactiveController} from '../lit.js'; - -import type {LoggerMixinInterface} from '../mixins/logging.js'; -import type {StringifyableRecord} from '@alwatr/type'; - -export class FiniteStateMachineController< - TState extends string, - TEventId extends string, - TContext extends StringifyableRecord - > extends FiniteStateMachine implements ReactiveController { - constructor( - private _host: LoggerMixinInterface, - config: Readonly>, - ) { - super(config); - this._host.addController(this); - if (!this.config.autoSignalUnsubscribe) { - this.subscribeSignals(); - } - } - - render(states: {[P in TState]: (() => unknown) | TState}): unknown { - this._logger.logMethodArgs('render', this.state.target); - let renderFn = states[this.state.target]; - - if (typeof renderFn === 'string') { - renderFn = states[renderFn]; - } - - if (typeof renderFn === 'function') { - return renderFn?.call(this._host); - } - - return; - } - - hostUpdate(): void { - this._host.setAttribute('state', this.state.target); - } - - protected override callFunction(fn?: () => T): T | void { - if (typeof fn !== 'function') return; - return fn.call(this._host); - } - - hostConnected(): void { - if (this.config.autoSignalUnsubscribe) { - this.subscribeSignals(); - } - } - - hostDisconnected(): void { - if (this.config.autoSignalUnsubscribe) { - this.unsubscribeSignals(); - } - } -} From 3ef68b034562a96a927d969d35a54966997aff6e Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 05:48:59 +0330 Subject: [PATCH 53/85] feat(es-bench): new bench model --- demo/es-bench/many-bind.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 demo/es-bench/many-bind.ts diff --git a/demo/es-bench/many-bind.ts b/demo/es-bench/many-bind.ts new file mode 100644 index 000000000..f6d62342d --- /dev/null +++ b/demo/es-bench/many-bind.ts @@ -0,0 +1,28 @@ +/* eslint-disable camelcase */ + +import {bench} from './bench.js'; + +function test1(id: string): void { + console.log(id); +} +function test2(id: string): void { + console.log(id); +} +function test3(id: string): void { + console.log(id); +} + +const bind = (id: string) => ({ + id, + test1: test1.bind(null, id), + test2: test2.bind(null, id), + test3: test3.bind(null, id), +} as const); + +function test_bind(): void { + bind('123'); +} + +bench('test_bind', test_bind); + +globalThis.document?.body.append(' Done. Check the console.'); From 2866e3bd5ff56fd2b5bddcaed3673a5868bae4bb Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 05:58:09 +0330 Subject: [PATCH 54/85] feat(fsm): new types Co-authored-by: Mohammad Honarvar --- core/fsm/package.json | 4 +- core/fsm/src/type.ts | 143 ++++++++++++++++++++++++++++-------------- 2 files changed, 98 insertions(+), 49 deletions(-) diff --git a/core/fsm/package.json b/core/fsm/package.json index caecfaebc..9cfe0f620 100644 --- a/core/fsm/package.json +++ b/core/fsm/package.json @@ -13,9 +13,9 @@ "esm", "alwatr" ], - "main": "core.js", + "main": "index.js", "type": "module", - "types": "core.d.ts", + "types": "index.d.ts", "author": "S. Ali Mihandoost ", "license": "MIT", "files": [ diff --git a/core/fsm/src/type.ts b/core/fsm/src/type.ts index fc1c105bb..815b473b4 100644 --- a/core/fsm/src/type.ts +++ b/core/fsm/src/type.ts @@ -1,63 +1,53 @@ -import {DebounceType} from '@alwatr/signal'; - -import type {SingleOrArray, MaybePromise, StringifyableRecord} from '@alwatr/type'; - -export type FsmConfig = { - /** - * Machine ID (It is used in the state change signal identifier, so it must be unique). - */ - id: string; +import type {finiteStateMachineConsumer} from './core.js'; +import type {DebounceType} from '@alwatr/signal'; +import type {ArrayItems, SingleOrArray, StringifyableRecord} from '@alwatr/type'; +export interface FsmConstructor { /** - * Initial state. + * Constructor id. */ - initial: TState; + readonly id: string; + readonly config: FsmConstructorConfig; + actionRecord: ActionRecord; +} +export interface FsmConstructorConfig< + TState extends string = string, + TEventId extends string = string, + TActionName extends string = string, + TContext extends StringifyableRecord = StringifyableRecord +> extends StringifyableRecord { /** * Initial context. */ - context: TContext; + readonly context: TContext; /** - * Define state list + * Initial state. */ - stateRecord: StateRecord; + readonly initial: TState; /** - * A list of signals ... + * Define state list */ - signalList?: Array< - { - signalId: string; - receivePrevious?: DebounceType; - } & ( - | { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - actions: SingleOrArray<(signalDetail: any) => MaybePromise>; - transition?: never; - } - | { - transition: keyof StateRecord[TState]['on']; - contextName?: keyof TContext; - actions?: never; - } - ) - >; - - autoSignalUnsubscribe?: true; -}; + readonly stateRecord: StateRecord; +} -export type StateRecord = { - [S in TState | '$all']: { +export type StateRecord< + TState extends string = string, + TEventId extends string = string, + TActionName extends string = string +> = { + readonly [S in TState | '$all']: { /** * On state exit actions */ - exit?: SingleOrArray<() => MaybePromise>; + readonly exit?: SingleOrArray; /** * On state entry actions */ - entry?: SingleOrArray<() => MaybePromise>; + readonly entry?: SingleOrArray; /** * An object mapping eventId to state. @@ -76,13 +66,14 @@ export type StateRecord = { * } * ``` */ - on: { - [E in TEventId]?: TransitionConfig | undefined; + readonly on: { + readonly [E in TEventId]?: TransitionConfig | undefined; }; }; }; -export type StateContext = { +export interface FsmState + extends StringifyableRecord { /** * Current state */ @@ -95,10 +86,68 @@ export type StateContext = { * Transition event */ by: TEventId | 'INIT'; -}; +} + +export interface TransitionConfig + extends StringifyableRecord { + readonly target?: TState; + readonly condition?: TActionName; + readonly actions?: SingleOrArray; +} -export interface TransitionConfig { - target?: TState; - condition?: () => MaybePromise; - actions?: SingleOrArray<() => MaybePromise>; +export interface FsmInstance< + TState extends string = string, + TEventId extends string = string, + TContext extends StringifyableRecord = StringifyableRecord +> extends StringifyableRecord { + readonly constructorId: string; + state: FsmState; + context: TContext; + signalList: Array; } + +export type ActionRecord = { + readonly [P in T['TActionName']]?: ( + finiteStateMachine: FsmConsumerInterface, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signalDetail?: any + ) => void | boolean; +}; + +export type SignalConfig< + TEventId extends string = string, + TActionName extends string = string, + TContext extends StringifyableRecord = StringifyableRecord +> = { + signalId: string; + receivePrevious?: DebounceType; +} & ( + | { + transition: TEventId; + contextName?: keyof TContext; + actions?: never; + } + | { + actions: SingleOrArray; + transition?: never; + } +); + +// type helper + +export type TState = Exclude; +export type TEventId = keyof T['stateRecord'][TState]['on']; +export type TActionName = T['stateRecord'][TState]['entry']; +export type TContext = T['context']; + +export type FsmTypeHelper = Readonly<{ + TState: Exclude; + TEventId: keyof T['stateRecord'][FsmTypeHelper['TState']]['on']; + TActionName: NonNullable['TState']]['entry']>>; + TContext: T['context']; +}>; + +export type FsmConsumerInterface< + T extends FsmTypeHelper = FsmTypeHelper, + TContext extends T['TContext'] = T['TContext'] +> = ReturnType>; From ee7ebf24c21c049c935951f9aa531b10df56536c Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 05:59:02 +0330 Subject: [PATCH 55/85] fix(fsm)!: remove old apis Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 177 ------------------------------------------- 1 file changed, 177 deletions(-) delete mode 100644 core/fsm/src/core.ts diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts deleted file mode 100644 index 39deb1dba..000000000 --- a/core/fsm/src/core.ts +++ /dev/null @@ -1,177 +0,0 @@ -import {createLogger, globalAlwatr} from '@alwatr/logger'; -import {contextConsumer, eventListener} from '@alwatr/signal'; -import {dispatch} from '@alwatr/signal/core.js'; - -import type {FsmConfig, StateContext} from './type.js'; -import type {ListenerSpec} from '@alwatr/signal/type.js'; -import type {SingleOrArray, MaybePromise, StringifyableRecord} from '@alwatr/type'; - -export type {FsmConfig, StateContext}; - -globalAlwatr.registeredList.push({ - name: '@alwatr/fsm', - version: _ALWATR_VERSION_, -}); - -export class FiniteStateMachine< - TState extends string = string, - TEventId extends string = string, - TContext extends StringifyableRecord = StringifyableRecord -> { - context = this.config.context; - - signal = contextConsumer.bind>('finite_state_machine_' + this.config.id); - - protected _logger = createLogger(`alwatr/fsm:${this.config.id}`); - - state: StateContext = this.setState(this.config.initial, 'INIT'); - - protected setState(target: TState, by: TEventId | 'INIT'): StateContext { - const state: StateContext = (this.state = { - target, - from: this.signal?.getValue()?.target ?? target, - by, - }); - - dispatch>(this.signal.id, state, {debounce: 'NextCycle'}); - - setTimeout(() => this.execAllActions(), 0); - - return state; - } - - constructor(public readonly config: Readonly>) { - this._logger.logMethodArgs('constructor', config); - - if (!config.stateRecord[config.initial]) { - this._logger.error('constructor', 'invalid_initial_state', config); - } - } - - /** - * Machine transition. - */ - async transition(event: TEventId, context?: Partial): Promise { - const fromState = this.state.target; - const transitionConfig = this.config.stateRecord[fromState]?.on[event] ?? this.config.stateRecord.$all?.on[event]; - this._logger.logMethodArgs('transition', {fromState, event, context, target: transitionConfig?.target}); - - if (context !== undefined) { - this.context = { - ...this.context, - ...context, - }; - } - - if (transitionConfig == null) { - this._logger.incident( - 'transition', - 'invalid_target_state', - 'Defined target state for this event not found in state config', - { - fromState, - event, - events: {...this.config.stateRecord.$all?.on, ...this.config.stateRecord[fromState]?.on}, - }, - ); - return; - } - - if ((await this.callFunction(transitionConfig.condition)) === false) { - return; - } - - transitionConfig.target ??= fromState; - this.setState(transitionConfig.target, event); - } - - protected async execAllActions(): Promise { - const state = this.state; - const stateRecord = this.config.stateRecord; - - if (state.by === 'INIT') { - await this.execActions(stateRecord.$all.entry); - await this.execActions(stateRecord[state.target]?.entry); - return; - } - // else - if (state.from !== state.target) { - await this.execActions(stateRecord.$all.exit); - await this.execActions(stateRecord[state.from]?.exit); - await this.execActions(stateRecord.$all.entry); - await this.execActions(stateRecord[state.target]?.entry); - } - await this.execActions(stateRecord[state.from]?.on[state.by]?.actions ?? stateRecord.$all.on[state.by]?.actions); - } - - protected async execActions(actions?: SingleOrArray<() => MaybePromise>): Promise { - if (actions == null) return; - - try { - if (!Array.isArray(actions)) { - await this.callFunction(actions); - return; - } - - // else - for (const action of actions) { - await this.callFunction(action); - } - } - catch (error) { - this._logger.accident('execActions', 'action_error', 'Error in executing actions', error); - } - } - - protected callFunction(fn?: () => T): T | void { - if (typeof fn !== 'function') return; - return fn(); - } - - private _listenerList: Array = []; - - protected subscribeSignals(): void { - this.unsubscribeSignals(); - const signalList = this.config.signalList; - if (signalList == null) return; - - for (const signalConfig of signalList) { - const actions = - signalConfig.actions ?? - ((signalDetail: unknown): void => { - let context = undefined; - if (signalConfig.contextName) { - context = >{ - [signalConfig.contextName]: signalDetail, - }; - } - this.transition(signalConfig.transition as TEventId, context); - }); - - if (Array.isArray(actions)) { - for (const action of actions) { - this._listenerList.push( - eventListener.subscribe(signalConfig.signalId, action, { - receivePrevious: signalConfig.receivePrevious ?? 'No', - }), - ); - } - } - else { - this._listenerList.push( - eventListener.subscribe(signalConfig.signalId, actions, { - receivePrevious: signalConfig.receivePrevious ?? 'No', - }), - ); - } - } - } - - protected unsubscribeSignals(): void { - if (this._listenerList.length === 0) return; - for (const listener of this._listenerList) { - eventListener.unsubscribe(listener); - } - this._listenerList.length = 0; - } -} From 322f83701f86fb0825bca8d9ce1a66ee8586a50e Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 05:59:50 +0330 Subject: [PATCH 56/85] feat(fsm)!: new core Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 core/fsm/src/core.ts diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts new file mode 100644 index 000000000..db2c49399 --- /dev/null +++ b/core/fsm/src/core.ts @@ -0,0 +1,27 @@ +import {createLogger, globalAlwatr} from '@alwatr/logger'; +import {ListenerSpec, contextProvider, contextConsumer} from '@alwatr/signal'; + +import type { + ActionRecord, + FsmConstructor, + FsmConstructorConfig, + FsmConsumerInterface, + FsmInstance, + FsmState, + FsmTypeHelper, + SignalConfig, +} from './type.js'; +import type {OmitFirstParam, SingleOrArray, StringifyableRecord} from '@alwatr/type'; + +globalAlwatr.registeredList.push({ + name: '@alwatr/fsm', + version: _ALWATR_VERSION_, +}); + +const logger = createLogger(`alwatr/fsm`); + +/** + * Finite state machine constructor storage. + */ +const fsmConstructorStorage: Record = {}; + From 3503be80c67d9c46970b9df5bb0293fae6e76288 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 06:00:10 +0330 Subject: [PATCH 57/85] feat(fsm)!: defineConstructor Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index db2c49399..dd086365a 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -25,3 +25,20 @@ const logger = createLogger(`alwatr/fsm`); */ const fsmConstructorStorage: Record = {}; +export function defineConstructor< + TState extends string = string, + TEventId extends string = string, + TActionName extends string = string, + TContext extends StringifyableRecord = StringifyableRecord +>( + id: string, + config: FsmConstructorConfig, +): FsmConstructorConfig { + if (fsmConstructorStorage[id] != null) throw new Error('fsm_exist', {cause: {id}}); + fsmConstructorStorage[id] = { + id, + config, + actionRecord: {}, + }; + return config; +} From f8ced3f55ddc56f9ac1b1a9024a3e53e8cd95dc0 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 06:02:13 +0330 Subject: [PATCH 58/85] feat(fsm)!: _getFsmInstance Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index dd086365a..cb7ff0a5d 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -42,3 +42,16 @@ export function defineConstructor< }; return config; } + +export const _getFsmInstance = < + TState extends string = string, + TEventId extends string = string, + TContext extends StringifyableRecord = StringifyableRecord +>( + instanceId: string, + ): FsmInstance => { + logger.logMethodArgs('_getFsmInstance', instanceId); + const machineInstance = contextConsumer.getValue>(instanceId); + if (machineInstance == null) throw new Error('fsm_undefined', {cause: {instanceId}}); + return machineInstance; +}; From 2a87b7cddffe5b4d531033e8fd160e018b457789 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 06:02:24 +0330 Subject: [PATCH 59/85] feat(fsm)!: _getFsmConstructor Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index cb7ff0a5d..3e85875b2 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -55,3 +55,10 @@ export const _getFsmInstance = < if (machineInstance == null) throw new Error('fsm_undefined', {cause: {instanceId}}); return machineInstance; }; + +export const _getFsmConstructor = (constructorId: string): FsmConstructor => { + logger.logMethodArgs('_getFsmConstructor', constructorId); + const machineConstructor = fsmConstructorStorage[constructorId]; + if (machineConstructor == null) throw new Error('fsm_undefined', {cause: {constructorId: constructorId}}); + return machineConstructor; +}; From 0dfa8b14e010a218c6ac113ca30663203b53c326 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 06:02:35 +0330 Subject: [PATCH 60/85] feat(fsm)!: getState Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index 3e85875b2..36cdb5563 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -62,3 +62,12 @@ export const _getFsmConstructor = (constructorId: string): FsmConstructor => { if (machineConstructor == null) throw new Error('fsm_undefined', {cause: {constructorId: constructorId}}); return machineConstructor; }; + +export const getState = ( + instanceId: string, +): FsmState => { + logger.logMethodArgs('getState', instanceId); + const detail = contextConsumer.getValue>(instanceId); + if (detail == null) throw new Error('fsm_undefined', {cause: {instanceId}}); + return detail.state; +}; From 5d126fcb124372b6a141b67f44785c54258f8bdb Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 06:02:48 +0330 Subject: [PATCH 61/85] feat(fsm)!: getContext Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index 36cdb5563..9e13174d4 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -71,3 +71,10 @@ export const getState = ( + instanceId: string, +): TContext => { + logger.logMethodArgs('getContext', instanceId); + return _getFsmInstance(instanceId).context as TContext; +}; From 28430336bfdbe3d3fec577d0cff284317ba180b2 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 06:03:05 +0330 Subject: [PATCH 62/85] feat(fsm)!: setContext Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index 9e13174d4..00fc5a4df 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -78,3 +78,20 @@ export const getContext = ( + instanceId: string, + context: Partial, + notify?: boolean, +): void => { + logger.logMethodArgs('setContext', {instanceId, context}); + const detail = _getFsmInstance(instanceId); + detail.context = { + ...detail.context, + ...context, + }; + + if (notify) { + contextProvider.setValue(instanceId, detail, {debounce: 'NextCycle'}); + } +}; From 53963124c7c7c7f59bae9026b9413933fffb7841 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 06:03:18 +0330 Subject: [PATCH 63/85] feat(fsm)!: transition Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 57 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index 00fc5a4df..c45de0d8e 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -95,3 +95,60 @@ export const setContext = ( + instanceId: string, + event: TEventId, + context?: Partial, + ): void => { + const fsmInstance = _getFsmInstance(instanceId); + const fsmConstructor = _getFsmConstructor(fsmInstance.constructorId); + const fromState = fsmInstance.state.target; + const stateRecord = fsmConstructor.config.stateRecord; + const transitionConfig = stateRecord[fromState]?.on[event] ?? stateRecord.$all.on[event]; + + logger.logMethodArgs('transition', {instanceId, fromState, event, context, target: transitionConfig?.target}); + + if (context !== undefined) { + fsmInstance.context = { + ...fsmInstance.context, + ...context, + }; + } + + if (transitionConfig == null) { + logger.incident( + 'transition', + 'invalid_target_state', + 'Defined target state for this event not found in state config', + { + fromState, + event, + events: { + ...stateRecord.$all?.on, + ...stateRecord[fromState]?.on, + }, + }, + ); + return; + } + + const consumerInterface = finiteStateMachineConsumer(instanceId); + + if (transitionConfig.condition) { + if (_execAction(fsmConstructor, transitionConfig.condition, consumerInterface) === false) return; + } + + fsmInstance.state = { + target: transitionConfig.target ?? fromState, + from: fromState, + by: event, + }; + + contextProvider.setValue(instanceId, fsmInstance, {debounce: 'NextCycle'}); + + _execAllActions(fsmConstructor, fsmInstance.state, consumerInterface); +}; From 5c7e61563467698118fac9eda20633bbaf0ca9c5 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 06:03:30 +0330 Subject: [PATCH 64/85] feat(fsm)!: defineActions Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index c45de0d8e..a0623962a 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -152,3 +152,12 @@ export const transition = < _execAllActions(fsmConstructor, fsmInstance.state, consumerInterface); }; + +export function defineActions(constructorId: string, actionRecord: ActionRecord): void { + const constructor = _getFsmConstructor(constructorId); + constructor.actionRecord = { + ...constructor.actionRecord, + ...actionRecord, + }; +} + From 13cbf8ddf34c019d43ebf68a1e859a2778d99669 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 06:03:49 +0330 Subject: [PATCH 65/85] feat(fsm)!: _execAllActions Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index a0623962a..a6c91f952 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -161,3 +161,35 @@ export function defineActions(constructorId: string, ac }; } +export const _execAllActions = ( + constructor: FsmConstructor, + state: FsmState, + consumerInterface: FsmConsumerInterface, +): void => { + logger.logMethodArgs('_execAllActions', consumerInterface.id); + + const stateRecord = constructor.config.stateRecord; + + if (state.by === 'INIT') { + _execAction(constructor, stateRecord.$all.entry, consumerInterface); + _execAction(constructor, stateRecord[state.target]?.entry, consumerInterface); + return; + } + + // else + if (state.from !== state.target) { + _execAction(constructor, stateRecord.$all.exit, consumerInterface); + _execAction(constructor, stateRecord[state.from]?.exit, consumerInterface); + _execAction(constructor, stateRecord.$all.entry, consumerInterface); + _execAction(constructor, stateRecord[state.target]?.entry, consumerInterface); + } + + _execAction( + constructor, + stateRecord[state.from]?.on[state.by] != null + ? stateRecord[state.from].on[state.by]?.actions + : stateRecord.$all.on[state.by]?.actions, + consumerInterface, + ); +}; + From 4c7a85bcd25a15406fd35490ba7071a06b4794f0 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 06:04:02 +0330 Subject: [PATCH 66/85] feat(fsm)!: _execAction Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index a6c91f952..f48645f74 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -193,3 +193,38 @@ export const _execAllActions = ( ); }; +export const _execAction = ( + constructor: FsmConstructor, + actionNames: SingleOrArray | undefined, + finiteStateMachine: FsmConsumerInterface, + signalDetail?: unknown, +): boolean | void => { + if (actionNames == null) return; + + logger.logMethodArgs('execAction', actionNames); + + if (Array.isArray(actionNames)) { + return actionNames + .map((actionName) => _execAction(constructor, actionName, finiteStateMachine, signalDetail)) + .every((r) => r === true); + } + + try { + const actionFn = constructor.actionRecord[actionNames]; + if (actionFn == null) { + return logger.error('execAction', 'action_not_found', { + actionNames, + constructorId: constructor.id, + instanceId: finiteStateMachine.id, + }); + } + return actionFn(finiteStateMachine, signalDetail); + } + catch (error) { + return logger.error('execAction', 'action_error', error, { + actionNames, + constructorId: constructor.id, + instanceId: finiteStateMachine.id, + }); + } +}; From 383f3420841193687d26b7e6df7e38ac072974a7 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 06:04:13 +0330 Subject: [PATCH 67/85] feat(fsm)!: initFsmInstance Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index f48645f74..f5fb5b2c7 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -228,3 +228,22 @@ export const _execAction = ( }); } }; + +export const initFsmInstance = (constructorId: string, instanceId: string): void => { + logger.logMethodArgs('initializeMachine', {constructorId, instanceId}); + const {initial, context} = _getFsmConstructor(constructorId).config; + contextProvider.setValue( + instanceId, + { + constructorId, + state: { + target: initial, + from: initial, + by: 'INIT', + }, + context, + signalList: [], + }, + {debounce: 'NextCycle'}, + ); +}; From 4bf9d26ff22938909264998d3f163ff1a6da8d72 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 06:05:17 +0330 Subject: [PATCH 68/85] feat(fsm)!: subscribeSignals Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index f5fb5b2c7..02bed99da 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -247,3 +247,44 @@ export const initFsmInstance = (constructorId: string, instanceId: string): void {debounce: 'NextCycle'}, ); }; + +export const subscribeSignals = (instanceId: string): Array => { + const fsmInstance = _getFsmInstance(instanceId); + const fsmConstructor = _getFsmConstructor(fsmInstance.constructorId); + const consumerInterface = finiteStateMachineConsumer(instanceId); + const listenerList: Array = []; + + for (const signalConfig of fsmInstance.signalList) { + if (signalConfig.actions == null) { + listenerList.push( + contextConsumer.subscribe( + signalConfig.signalId, + (signalDetail: Partial): void => { + transition( + instanceId, + signalConfig.transition, + signalConfig.contextName + ? { + [signalConfig.contextName]: signalDetail, + } + : undefined, + ); + }, + {receivePrevious: signalConfig.receivePrevious ?? 'No'}, + ), + ); + } + // else + listenerList.push( + contextConsumer.subscribe( + signalConfig.signalId, + (signalDetail) => { + _execAction(fsmConstructor, signalConfig.actions, consumerInterface, signalDetail); + }, + {receivePrevious: signalConfig.receivePrevious ?? 'No'}, + ), + ); + } + + return listenerList; +}; From b2f027728013c663286af0e16abef75bf4dc9961 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 06:05:36 +0330 Subject: [PATCH 69/85] doc(fsm): unsubscribeSignals Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index 02bed99da..358b83cd4 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -288,3 +288,11 @@ export const subscribeSignals = (instanceId: string): Array => { return listenerList; }; + +// protected unsubscribeSignals(): void { +// if (this._listenerList.length === 0) return; +// for (const listener of this._listenerList) { +// eventListener.unsubscribe(listener); +// } +// this._listenerList.length = 0; +// } From 423afdbfe87a1711faa288a54fdb72b3b481403f Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 06:06:10 +0330 Subject: [PATCH 70/85] feat(fsm)!: defineSignals Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index 358b83cd4..8216e5867 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -296,3 +296,11 @@ export const subscribeSignals = (instanceId: string): Array => { // } // this._listenerList.length = 0; // } + +export function defineSignals( + instanceId: string, + signalList: SingleOrArray>, +): void { + const instance = _getFsmInstance(instanceId); + instance.signalList = instance.signalList.concat(signalList as Array); +} From 89165c8b38b973710b09cc4bea92879f456c6a5c Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 06:06:22 +0330 Subject: [PATCH 71/85] feat(fsm)!: render Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index 8216e5867..308420fd9 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -304,3 +304,22 @@ export function defineSignals( const instance = _getFsmInstance(instanceId); instance.signalList = instance.signalList.concat(signalList as Array); } + +export const render = ( + instanceId: string, + states: {[P in TState]: (() => unknown) | TState}, +): unknown => { + const state = _getFsmInstance(instanceId).state; + logger.logMethodArgs('render', state.target); + let renderFn = states[state.target as TState]; + + if (typeof renderFn === 'string') { + renderFn = states[renderFn as TState]; + } + + if (typeof renderFn === 'function') { + return renderFn(); + } + + return; +}; From b83a39ce02fe4ad7768de5e50e31d0a45386735a Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 06:06:49 +0330 Subject: [PATCH 72/85] feat(fsm)!: finiteStateMachineConsumer Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index 308420fd9..c9ab13cc9 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -323,3 +323,32 @@ export const render = ( return; }; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const finiteStateMachineConsumer = ( + instanceId: string, + makeFromConstructor?: string, +) => { + logger.logMethodArgs('stateMachineLookup', instanceId); + + const machineInstance = contextConsumer.getValue(instanceId); + if (machineInstance == null) { + // instance not initialized. + if (makeFromConstructor == null) { + throw new Error('fsm_undefined', {cause: {instanceId}}); + } + initFsmInstance(instanceId, makeFromConstructor); + } + + return { + id: instanceId, + constructorId: machineInstance?.constructorId ?? makeFromConstructor, + render: render.bind(null, instanceId) as OmitFirstParam>, + getState: getState.bind(null, instanceId) as OmitFirstParam>, + getContext: getContext.bind(null, instanceId) as OmitFirstParam>, + setContext: setContext.bind(null, instanceId) as OmitFirstParam>, + transition: transition.bind(null, instanceId) as OmitFirstParam>, + defineSignals: defineSignals.bind(null, instanceId) as OmitFirstParam>, + subscribeSignals: subscribeSignals.bind(null, instanceId) as OmitFirstParam, + } as const; +}; From d7dcbb348d4f8515423b86a2affc6485d66bd554 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 06:18:41 +0330 Subject: [PATCH 73/85] refactor(fsm)!: final review Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index c9ab13cc9..cd511178c 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -25,7 +25,7 @@ const logger = createLogger(`alwatr/fsm`); */ const fsmConstructorStorage: Record = {}; -export function defineConstructor< +export const defineConstructor = < TState extends string = string, TEventId extends string = string, TActionName extends string = string, @@ -33,7 +33,8 @@ export function defineConstructor< >( id: string, config: FsmConstructorConfig, -): FsmConstructorConfig { + ): FsmConstructorConfig => { + logger.logMethodArgs('defineConstructor', {id, config}); if (fsmConstructorStorage[id] != null) throw new Error('fsm_exist', {cause: {id}}); fsmConstructorStorage[id] = { id, @@ -41,7 +42,7 @@ export function defineConstructor< actionRecord: {}, }; return config; -} +}; export const _getFsmInstance = < TState extends string = string, @@ -153,13 +154,14 @@ export const transition = < _execAllActions(fsmConstructor, fsmInstance.state, consumerInterface); }; -export function defineActions(constructorId: string, actionRecord: ActionRecord): void { +export const defineActions = (constructorId: string, actionRecord: ActionRecord): void => { + logger.logMethodArgs('defineActions', {constructorId, actionRecord}); const constructor = _getFsmConstructor(constructorId); constructor.actionRecord = { ...constructor.actionRecord, ...actionRecord, }; -} +}; export const _execAllActions = ( constructor: FsmConstructor, @@ -200,7 +202,6 @@ export const _execAction = ( signalDetail?: unknown, ): boolean | void => { if (actionNames == null) return; - logger.logMethodArgs('execAction', actionNames); if (Array.isArray(actionNames)) { @@ -249,6 +250,7 @@ export const initFsmInstance = (constructorId: string, instanceId: string): void }; export const subscribeSignals = (instanceId: string): Array => { + logger.logMethodArgs('subscribeSignals', instanceId); const fsmInstance = _getFsmInstance(instanceId); const fsmConstructor = _getFsmConstructor(fsmInstance.constructorId); const consumerInterface = finiteStateMachineConsumer(instanceId); @@ -297,20 +299,21 @@ export const subscribeSignals = (instanceId: string): Array => { // this._listenerList.length = 0; // } -export function defineSignals( - instanceId: string, - signalList: SingleOrArray>, -): void { +export const defineSignals = ( + instanceId: string, + signalList: SingleOrArray>, +): void => { + logger.logMethodArgs('defineSignals', {instanceId, signalList}); const instance = _getFsmInstance(instanceId); instance.signalList = instance.signalList.concat(signalList as Array); -} +}; export const render = ( instanceId: string, states: {[P in TState]: (() => unknown) | TState}, ): unknown => { const state = _getFsmInstance(instanceId).state; - logger.logMethodArgs('render', state.target); + logger.logMethodArgs('render', {instanceId, state: state.target}); let renderFn = states[state.target as TState]; if (typeof renderFn === 'string') { From 4cdfcc6cfade646ffa5243eda4b74be1e4e4e5c0 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 06:19:10 +0330 Subject: [PATCH 74/85] feat(fsm)!: export fsm main api Co-authored-by: Mohammad Honarvar --- core/fsm/src/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 core/fsm/src/index.ts diff --git a/core/fsm/src/index.ts b/core/fsm/src/index.ts new file mode 100644 index 000000000..0e24603a5 --- /dev/null +++ b/core/fsm/src/index.ts @@ -0,0 +1,9 @@ +import {defineActions, defineConstructor} from './core.js'; + +export {finiteStateMachineConsumer} from './core.js'; +export const finiteStateMachineProvider = { + defineConstructor, + defineActions, +} as const; + +export type {FsmTypeHelper, FsmConstructorConfig} from './type.js'; From 3b60138ecebcbcb4d732e4d1a3e79f5b8661ae47 Mon Sep 17 00:00:00 2001 From: Mohammad Honarvar Date: Thu, 16 Mar 2023 19:19:00 +0330 Subject: [PATCH 75/85] fix(fsm): fix order of `initFsmInstance` args --- core/fsm/src/core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index cd511178c..611ba8214 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -230,7 +230,7 @@ export const _execAction = ( } }; -export const initFsmInstance = (constructorId: string, instanceId: string): void => { +export const initFsmInstance = (instanceId: string, constructorId: string): void => { logger.logMethodArgs('initializeMachine', {constructorId, instanceId}); const {initial, context} = _getFsmConstructor(constructorId).config; contextProvider.setValue( From 551e5fe75fa106bc3252bbbbf108a68bf0dc19e7 Mon Sep 17 00:00:00 2001 From: Mohammad Honarvar Date: Thu, 16 Mar 2023 19:20:39 +0330 Subject: [PATCH 76/85] fix(demo): fix demo based on latest `fsm`s changes --- demo/finite-state-machine/index.html | 13 +++ demo/finite-state-machine/light-machine.ts | 95 ++++++++++++++-------- demo/index.html | 1 + 3 files changed, 74 insertions(+), 35 deletions(-) create mode 100644 demo/finite-state-machine/index.html diff --git a/demo/finite-state-machine/index.html b/demo/finite-state-machine/index.html new file mode 100644 index 000000000..497593dbf --- /dev/null +++ b/demo/finite-state-machine/index.html @@ -0,0 +1,13 @@ + + + + + + + FSM + + + + + + diff --git a/demo/finite-state-machine/light-machine.ts b/demo/finite-state-machine/light-machine.ts index c1acc7ed9..ff3f0a22b 100644 --- a/demo/finite-state-machine/light-machine.ts +++ b/demo/finite-state-machine/light-machine.ts @@ -1,7 +1,6 @@ -import {FiniteStateMachine} from '@alwatr/fsm'; +import {finiteStateMachineConsumer, finiteStateMachineProvider} from '@alwatr/fsm'; -const lightMachine = new FiniteStateMachine({ - id: 'light-machine', +const config = { initial: 'green', context: { a: 0, @@ -9,74 +8,100 @@ const lightMachine = new FiniteStateMachine({ }, stateRecord: { $all: { - entry: (): void => console.log('$all entry called'), - exit: (): void => console.log('$all exit called'), + entry: 'action_all_entry', + exit: 'action_all_exit', on: { POWER_LOST: { target: 'flashingRed', - actions: (): void => console.log('$all.POWER_LOST actions called'), + actions: 'action_all_POWER_LOST', }, }, }, green: { - entry: (): void => console.log('green entry called'), - exit: (): void => console.log('green exit called'), + entry: 'action_green_entry', + exit: 'action_green_exit', on: { TIMER: { target: 'yellow', - actions: (): void => console.log('green.TIMER actions called'), + actions: 'action_GREEN_TIMER', }, }, }, yellow: { - entry: (): void => console.log('yellow entry called'), - exit: (): void => console.log('yellow exit called'), + entry: 'action_yellow_entry', + exit: 'action_yellow_exit', on: { TIMER: { target: 'red', - actions: (): void => console.log('yellow.TIMER actions called'), + actions: 'action_yellow_TIMER', }, }, }, red: { - entry: (): void => console.log('red entry called'), - exit: (): void => console.log('red exit called'), + entry: 'action_red_entry', + exit: 'action_red_exit', on: { TIMER: { target: 'green', - actions: (): void => console.log('red.TIMER actions called'), + actions: 'action_red_TIMER', }, }, }, flashingRed: { - entry: (): void => console.log('flashingRed entry called'), - exit: (): void => console.log('flashingRed exit called'), + entry: 'action_flashingRed_entry', + exit: 'action_flashingRed_exit', on: { POWER_BACK: { target: 'green', - actions: (): void => console.log('flashingRed.POWER_BACK actions called'), + actions: 'action_flashingRed_POWER_BACK', }, }, }, }, - signalList: [ - { - signalId: 'ali', - actions: (a): void => console.log(a), - }, - ], -}); +}; + +finiteStateMachineProvider.defineConstructor('light-machine', config); + +finiteStateMachineProvider.defineActions('light-machine', { + // entries + 'action_all_entry': (): void => console.log('$all entry called'), + 'action_green_entry': (): void => console.log('green entry called'), + 'action_yellow_entry': (): void => console.log('yellow entry called'), + 'action_red_entry': (): void => console.log('red entry called'), + 'action_flashingRed_entry': (): void => console.log('flashingRed entry called'), + + // on actions + 'action_all_POWER_LOST': (): void => console.log('$all.POWER_LOST actions called'), + 'action_GREEN_TIMER': (): void => console.log('green.TIMER actions called'), + 'action_yellow_TIMER': (): void => console.log('yellow.TIMER actions called'), + 'action_red_TIMER': (): void => console.log('red.TIMER actions called'), + 'action_flashingRed_POWER_BACK': (): void => console.log('flashingRed.POWER_BACK actions called'), + // exits + 'action_all_exit': (): void => console.log('$all exit called'), + 'action_green_exit': (): void => console.log('green exit called'), + 'action_yellow_exit': (): void => console.log('yellow exit called'), + 'action_red_exit': (): void => console.log('red exit called'), + 'action_flashingRed_exit': (): void => console.log('flashingRed exit called'), -lightMachine.signal.subscribe((state) => { - console.log('****\nstate: %s, context: %s\n****', state, lightMachine.context); -}, {receivePrevious: 'No'}); + // signals + 'action_ali_signal': (a): void => console.log('ali signal ', a), + +}); + +const lightMachineConsumer = finiteStateMachineConsumer('light-machine-50', 'light-machine'); +lightMachineConsumer.defineSignals([ + { + signalId: 'ali', + actions: 'test', + }, +]); -console.log('start'); +console.log('start ', lightMachineConsumer); -await lightMachine.transition('TIMER', {a: 1}); -await lightMachine.transition('TIMER', {b: 2}); -await lightMachine.transition('TIMER'); -await lightMachine.transition('POWER_LOST', {a: 4}); -await lightMachine.transition('TIMER', {a: 5, b: 5}); -await lightMachine.transition('POWER_BACK', {a: 6}); +lightMachineConsumer.transition('TIMER', {a: 1}); +lightMachineConsumer.transition('TIMER', {b: 2}); +lightMachineConsumer.transition('TIMER'); +lightMachineConsumer.transition('POWER_LOST', {a: 4}); +lightMachineConsumer.transition('TIMER', {a: 5, b: 5}); +lightMachineConsumer.transition('POWER_BACK', {a: 6}); diff --git a/demo/index.html b/demo/index.html index 754be5368..493df6600 100644 --- a/demo/index.html +++ b/demo/index.html @@ -21,6 +21,7 @@
  • Math
  • Icon
  • ES Bench
  • +
  • FSM
  • From dc7f8882ea0ed442790cc7cef639ff8ae01a8f74 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 22:38:08 +0330 Subject: [PATCH 77/85] refactor(fsm): review --- core/fsm/src/core.ts | 24 +++++++++++------------- core/fsm/src/type.ts | 6 ------ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index 611ba8214..118d3588b 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -52,25 +52,23 @@ export const _getFsmInstance = < instanceId: string, ): FsmInstance => { logger.logMethodArgs('_getFsmInstance', instanceId); - const machineInstance = contextConsumer.getValue>(instanceId); - if (machineInstance == null) throw new Error('fsm_undefined', {cause: {instanceId}}); - return machineInstance; + const fsmInstance = contextConsumer.getValue>(instanceId); + if (fsmInstance == null) throw new Error('fsm_undefined', {cause: {instanceId}}); + return fsmInstance; }; export const _getFsmConstructor = (constructorId: string): FsmConstructor => { logger.logMethodArgs('_getFsmConstructor', constructorId); - const machineConstructor = fsmConstructorStorage[constructorId]; - if (machineConstructor == null) throw new Error('fsm_undefined', {cause: {constructorId: constructorId}}); - return machineConstructor; + const fsmConstructor = fsmConstructorStorage[constructorId]; + if (fsmConstructor == null) throw new Error('fsm_undefined', {cause: {constructorId: constructorId}}); + return fsmConstructor; }; export const getState = ( instanceId: string, ): FsmState => { logger.logMethodArgs('getState', instanceId); - const detail = contextConsumer.getValue>(instanceId); - if (detail == null) throw new Error('fsm_undefined', {cause: {instanceId}}); - return detail.state; + return _getFsmInstance(instanceId).state; }; export const getContext = ( @@ -86,14 +84,14 @@ export const setContext = { logger.logMethodArgs('setContext', {instanceId, context}); - const detail = _getFsmInstance(instanceId); - detail.context = { - ...detail.context, + const fsmInstance = _getFsmInstance(instanceId); + fsmInstance.context = { + ...fsmInstance.context, ...context, }; if (notify) { - contextProvider.setValue(instanceId, detail, {debounce: 'NextCycle'}); + contextProvider.setValue(instanceId, fsmInstance, {debounce: 'NextCycle'}); } }; diff --git a/core/fsm/src/type.ts b/core/fsm/src/type.ts index 815b473b4..0371470bc 100644 --- a/core/fsm/src/type.ts +++ b/core/fsm/src/type.ts @@ -134,12 +134,6 @@ export type SignalConfig< ); // type helper - -export type TState = Exclude; -export type TEventId = keyof T['stateRecord'][TState]['on']; -export type TActionName = T['stateRecord'][TState]['entry']; -export type TContext = T['context']; - export type FsmTypeHelper = Readonly<{ TState: Exclude; TEventId: keyof T['stateRecord'][FsmTypeHelper['TState']]['on']; From 32fa2155d73be3c1328b4926273176ee47505c39 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Thu, 16 Mar 2023 22:39:55 +0330 Subject: [PATCH 78/85] fix(demo/fsm): new demo for new fsm --- demo/finite-state-machine/light-machine.ts | 55 +++++++++++++--------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/demo/finite-state-machine/light-machine.ts b/demo/finite-state-machine/light-machine.ts index ff3f0a22b..cd828139c 100644 --- a/demo/finite-state-machine/light-machine.ts +++ b/demo/finite-state-machine/light-machine.ts @@ -1,6 +1,7 @@ -import {finiteStateMachineConsumer, finiteStateMachineProvider} from '@alwatr/fsm'; +import {finiteStateMachineConsumer, finiteStateMachineProvider, type FsmTypeHelper} from '@alwatr/fsm'; -const config = { +// Provider +const lightMachineConstructor = finiteStateMachineProvider.defineConstructor('light_machine', { initial: 'green', context: { a: 0, @@ -23,7 +24,7 @@ const config = { on: { TIMER: { target: 'yellow', - actions: 'action_GREEN_TIMER', + actions: 'action_green_TIMER', }, }, }, @@ -58,46 +59,56 @@ const config = { }, }, }, -}; +}); -finiteStateMachineProvider.defineConstructor('light-machine', config); +type LightMachine = FsmTypeHelper; -finiteStateMachineProvider.defineActions('light-machine', { - // entries - 'action_all_entry': (): void => console.log('$all entry called'), +// entries actions +finiteStateMachineProvider.defineActions('light_machine', { + 'action_all_entry': (m): void => console.log('$all entry called', m.getState()), 'action_green_entry': (): void => console.log('green entry called'), 'action_yellow_entry': (): void => console.log('yellow entry called'), 'action_red_entry': (): void => console.log('red entry called'), 'action_flashingRed_entry': (): void => console.log('flashingRed entry called'), +}); - // on actions - 'action_all_POWER_LOST': (): void => console.log('$all.POWER_LOST actions called'), - 'action_GREEN_TIMER': (): void => console.log('green.TIMER actions called'), - 'action_yellow_TIMER': (): void => console.log('yellow.TIMER actions called'), - 'action_red_TIMER': (): void => console.log('red.TIMER actions called'), - 'action_flashingRed_POWER_BACK': (): void => console.log('flashingRed.POWER_BACK actions called'), - - // exits +// exits actions +finiteStateMachineProvider.defineActions('light_machine', { 'action_all_exit': (): void => console.log('$all exit called'), 'action_green_exit': (): void => console.log('green exit called'), 'action_yellow_exit': (): void => console.log('yellow exit called'), 'action_red_exit': (): void => console.log('red exit called'), 'action_flashingRed_exit': (): void => console.log('flashingRed exit called'), +}); - // signals - 'action_ali_signal': (a): void => console.log('ali signal ', a), - +// transition events actions +finiteStateMachineProvider.defineActions('light_machine', { + 'action_all_POWER_LOST': (): void => console.log('$all.POWER_LOST actions called'), + 'action_green_TIMER': (): void => console.log('green.TIMER actions called'), + 'action_yellow_TIMER': (): void => console.log('yellow.TIMER actions called'), + 'action_red_TIMER': (): void => console.log('red.TIMER actions called'), + 'action_flashingRed_POWER_BACK': (): void => console.log('flashingRed.POWER_BACK actions called'), }); -const lightMachineConsumer = finiteStateMachineConsumer('light-machine-50', 'light-machine'); +// signals +// 'action_ali_signal': (a): void => console.log('ali signal ', a), + +// Consumer +const lightMachineConsumer = finiteStateMachineConsumer('light_machine-50', 'light_machine'); + lightMachineConsumer.defineSignals([ { signalId: 'ali', - actions: 'test', + transition: 'POWER_BACK', + // contextName: 'a', }, ]); -console.log('start ', lightMachineConsumer); +lightMachineConsumer.subscribeSignals(); + +console.log('start', lightMachineConsumer.getState()); + +// lightMachineConsumer lightMachineConsumer.transition('TIMER', {a: 1}); lightMachineConsumer.transition('TIMER', {b: 2}); From f60a9fce1ca9491e3916fd40fb7a1443927f9d65 Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Fri, 17 Mar 2023 00:35:09 +0330 Subject: [PATCH 79/85] feat(bench): test object vs map --- demo/es-bench/bench.ts | 5 +-- demo/es-bench/object-vs-map.ts | 60 ++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 demo/es-bench/object-vs-map.ts diff --git a/demo/es-bench/bench.ts b/demo/es-bench/bench.ts index 70d1564a0..88647b4a8 100644 --- a/demo/es-bench/bench.ts +++ b/demo/es-bench/bench.ts @@ -1,12 +1,13 @@ +const count = 1_000_000; export const bench = (name: string, func: () => void): void => { globalThis.gc?.(); const startMemory = process.memoryUsage.rss(); const startTime = performance.now(); - for (let i = 1_000_000; i; i--) { + for (let i = count; i; i--) { func(); } const duration = performance.now() - startTime; - const runPerSec = Math.round( 1_000_000 / duration * 1000); + const runPerSec = Math.round( count / duration * 1000); const memoryUsage = Math.round((process.memoryUsage.rss() - startMemory) / 10) / 100; console.log(`run ${name} ${runPerSec.toLocaleString()}/s with ${memoryUsage.toLocaleString()}kb`); diff --git a/demo/es-bench/object-vs-map.ts b/demo/es-bench/object-vs-map.ts new file mode 100644 index 000000000..e31459f2a --- /dev/null +++ b/demo/es-bench/object-vs-map.ts @@ -0,0 +1,60 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable camelcase */ + +import {bench} from './bench.js'; + +const size = 1_000; + +let userListRecord: Record> = {}; +let userListMap = new Map>(); + +function test_make_map() { + userListMap = new Map>(); + for (let i = size; i; i--) { + const userId = 'user_' + i; + + const user = new Map(); + user.set('user', userId); + user.set('fname', 'ali'); + user.set('lname', 'md'); + user.set('email', 'i@ali.md'); + user.set('token', '1234abcd'); + + userListMap.set(userId, user); + } +} + +function test_make_obj() { + userListRecord = {}; + for (let i = size; i; i--) { + const userId = 'user_' + i; + userListRecord[userId] = { + user: userId, + fname: 'ali', + lname: 'md', + email: 'i@ali.md', + token: '1234abcd', + }; + } +} + +function test_access_map() { + if (userListMap.get('user_' + size / 2)!.get('user') !== 'user_' + size / 2) throw new Error('not_match'); +} + +function test_access_obj() { + if (userListRecord['user_' + size / 2]['user'] !== 'user_' + size / 2) throw new Error('not_match'); +} + +bench('test_make_map_1st', test_make_map); +bench('test_make_obj_1st', test_make_obj); +bench('test_access_map_1st', test_access_map); +bench('test_access_obj_1st', test_access_obj); + +bench('test_make_map_2nd', test_make_map); +bench('test_make_obj_2nd', test_make_obj); +bench('test_access_map_2nd', test_access_map); +bench('test_access_obj_2nd', test_access_obj); + +globalThis.document?.body.append(' Done. Check the console.'); From 24978567ff1108f0c6a695997bf3cc162956ae1e Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Fri, 17 Mar 2023 02:20:53 +0330 Subject: [PATCH 80/85] feat(fsm)!: new defineConstructorSignals, defineInstanceSignals Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 107 +++++++++++++++++++++--------------------- core/fsm/src/index.ts | 3 +- core/fsm/src/type.ts | 27 ++++------- 3 files changed, 65 insertions(+), 72 deletions(-) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index 118d3588b..c495f2645 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -40,11 +40,12 @@ export const defineConstructor = < id, config, actionRecord: {}, + signalList: [], }; return config; }; -export const _getFsmInstance = < +export const getFsmInstance = < TState extends string = string, TEventId extends string = string, TContext extends StringifyableRecord = StringifyableRecord @@ -57,7 +58,7 @@ export const _getFsmInstance = < return fsmInstance; }; -export const _getFsmConstructor = (constructorId: string): FsmConstructor => { +export const getFsmConstructor = (constructorId: string): FsmConstructor => { logger.logMethodArgs('_getFsmConstructor', constructorId); const fsmConstructor = fsmConstructorStorage[constructorId]; if (fsmConstructor == null) throw new Error('fsm_undefined', {cause: {constructorId: constructorId}}); @@ -68,14 +69,14 @@ export const getState = => { logger.logMethodArgs('getState', instanceId); - return _getFsmInstance(instanceId).state; + return getFsmInstance(instanceId).state; }; export const getContext = ( instanceId: string, ): TContext => { logger.logMethodArgs('getContext', instanceId); - return _getFsmInstance(instanceId).context as TContext; + return getFsmInstance(instanceId).context; }; export const setContext = ( @@ -84,14 +85,14 @@ export const setContext = { logger.logMethodArgs('setContext', {instanceId, context}); - const fsmInstance = _getFsmInstance(instanceId); + const fsmInstance = getFsmInstance(instanceId); fsmInstance.context = { ...fsmInstance.context, ...context, }; if (notify) { - contextProvider.setValue(instanceId, fsmInstance, {debounce: 'NextCycle'}); + contextProvider.setValue(instanceId, fsmInstance, {debounce: 'Timeout'}); } }; @@ -103,8 +104,8 @@ export const transition = < event: TEventId, context?: Partial, ): void => { - const fsmInstance = _getFsmInstance(instanceId); - const fsmConstructor = _getFsmConstructor(fsmInstance.constructorId); + const fsmInstance = getFsmInstance(instanceId); + const fsmConstructor = getFsmConstructor(fsmInstance.constructorId); const fromState = fsmInstance.state.target; const stateRecord = fsmConstructor.config.stateRecord; const transitionConfig = stateRecord[fromState]?.on[event] ?? stateRecord.$all.on[event]; @@ -147,16 +148,16 @@ export const transition = < by: event, }; - contextProvider.setValue(instanceId, fsmInstance, {debounce: 'NextCycle'}); + contextProvider.setValue(instanceId, fsmInstance, {debounce: 'Timeout'}); _execAllActions(fsmConstructor, fsmInstance.state, consumerInterface); }; export const defineActions = (constructorId: string, actionRecord: ActionRecord): void => { logger.logMethodArgs('defineActions', {constructorId, actionRecord}); - const constructor = _getFsmConstructor(constructorId); - constructor.actionRecord = { - ...constructor.actionRecord, + const fmsConstructor = getFsmConstructor(constructorId); + fmsConstructor.actionRecord = { + ...fmsConstructor.actionRecord, ...actionRecord, }; }; @@ -197,14 +198,13 @@ export const _execAction = ( constructor: FsmConstructor, actionNames: SingleOrArray | undefined, finiteStateMachine: FsmConsumerInterface, - signalDetail?: unknown, ): boolean | void => { if (actionNames == null) return; logger.logMethodArgs('execAction', actionNames); if (Array.isArray(actionNames)) { return actionNames - .map((actionName) => _execAction(constructor, actionName, finiteStateMachine, signalDetail)) + .map((actionName) => _execAction(constructor, actionName, finiteStateMachine)) .every((r) => r === true); } @@ -217,7 +217,7 @@ export const _execAction = ( instanceId: finiteStateMachine.id, }); } - return actionFn(finiteStateMachine, signalDetail); + return actionFn(finiteStateMachine); } catch (error) { return logger.error('execAction', 'action_error', error, { @@ -230,7 +230,7 @@ export const _execAction = ( export const initFsmInstance = (instanceId: string, constructorId: string): void => { logger.logMethodArgs('initializeMachine', {constructorId, instanceId}); - const {initial, context} = _getFsmConstructor(constructorId).config; + const {initial, context} = getFsmConstructor(constructorId).config; contextProvider.setValue( instanceId, { @@ -247,39 +247,32 @@ export const initFsmInstance = (instanceId: string, constructorId: string): void ); }; -export const subscribeSignals = (instanceId: string): Array => { - logger.logMethodArgs('subscribeSignals', instanceId); - const fsmInstance = _getFsmInstance(instanceId); - const fsmConstructor = _getFsmConstructor(fsmInstance.constructorId); - const consumerInterface = finiteStateMachineConsumer(instanceId); +export const subscribeSignals = ( + instanceId: string, + signalList: Array, + subscribeConstructorSignals = true, +): Array => { + logger.logMethodArgs('subscribeSignals', {instanceId, signalList}); const listenerList: Array = []; - for (const signalConfig of fsmInstance.signalList) { - if (signalConfig.actions == null) { - listenerList.push( - contextConsumer.subscribe( - signalConfig.signalId, - (signalDetail: Partial): void => { - transition( - instanceId, - signalConfig.transition, - signalConfig.contextName - ? { - [signalConfig.contextName]: signalDetail, - } - : undefined, - ); - }, - {receivePrevious: signalConfig.receivePrevious ?? 'No'}, - ), - ); - } - // else + if (subscribeConstructorSignals) { + signalList = getFsmConstructor(getFsmInstance(instanceId).constructorId).signalList.concat(signalList); + } + + for (const signalConfig of signalList) { listenerList.push( contextConsumer.subscribe( signalConfig.signalId, - (signalDetail) => { - _execAction(fsmConstructor, signalConfig.actions, consumerInterface, signalDetail); + (signalDetail: StringifyableRecord): void => { + transition( + instanceId, + signalConfig.transition, + signalConfig.contextName + ? { + [signalConfig.contextName]: signalDetail, + } + : undefined, + ); }, {receivePrevious: signalConfig.receivePrevious ?? 'No'}, ), @@ -297,20 +290,29 @@ export const subscribeSignals = (instanceId: string): Array => { // this._listenerList.length = 0; // } -export const defineSignals = ( - instanceId: string, - signalList: SingleOrArray>, +export const defineConstructorSignals = ( + constructorId: string, + signalList: Array>, ): void => { - logger.logMethodArgs('defineSignals', {instanceId, signalList}); - const instance = _getFsmInstance(instanceId); - instance.signalList = instance.signalList.concat(signalList as Array); + logger.logMethodArgs('defineSignals', {constructorId, signalList: signalList}); + const fsmConstructor = getFsmConstructor(constructorId); + fsmConstructor.signalList = fsmConstructor.signalList.concat(signalList); +}; + +export const defineInstanceSignals = ( + instanceId: string, + signalList: Array>, + subscribeConstructorSignals = true, +): Array => { + logger.logMethodArgs('defineSignals', {instanceId, signals: signalList}); + return subscribeSignals(instanceId, signalList, subscribeConstructorSignals); }; export const render = ( instanceId: string, states: {[P in TState]: (() => unknown) | TState}, ): unknown => { - const state = _getFsmInstance(instanceId).state; + const state = getFsmInstance(instanceId).state; logger.logMethodArgs('render', {instanceId, state: state.target}); let renderFn = states[state.target as TState]; @@ -349,7 +351,6 @@ export const finiteStateMachineConsumer = >, setContext: setContext.bind(null, instanceId) as OmitFirstParam>, transition: transition.bind(null, instanceId) as OmitFirstParam>, - defineSignals: defineSignals.bind(null, instanceId) as OmitFirstParam>, - subscribeSignals: subscribeSignals.bind(null, instanceId) as OmitFirstParam, + defineSignals: defineInstanceSignals.bind(null, instanceId) as OmitFirstParam>, } as const; }; diff --git a/core/fsm/src/index.ts b/core/fsm/src/index.ts index 0e24603a5..4f6f7edb0 100644 --- a/core/fsm/src/index.ts +++ b/core/fsm/src/index.ts @@ -1,9 +1,10 @@ -import {defineActions, defineConstructor} from './core.js'; +import {defineActions, defineConstructor, defineConstructorSignals} from './core.js'; export {finiteStateMachineConsumer} from './core.js'; export const finiteStateMachineProvider = { defineConstructor, defineActions, + defineSignals: defineConstructorSignals, } as const; export type {FsmTypeHelper, FsmConstructorConfig} from './type.js'; diff --git a/core/fsm/src/type.ts b/core/fsm/src/type.ts index 0371470bc..10abef0c7 100644 --- a/core/fsm/src/type.ts +++ b/core/fsm/src/type.ts @@ -9,6 +9,7 @@ export interface FsmConstructor { readonly id: string; readonly config: FsmConstructorConfig; actionRecord: ActionRecord; + signalList: Array; } export interface FsmConstructorConfig< @@ -103,35 +104,25 @@ export interface FsmInstance< readonly constructorId: string; state: FsmState; context: TContext; - signalList: Array; } export type ActionRecord = { - readonly [P in T['TActionName']]?: ( - finiteStateMachine: FsmConsumerInterface, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - signalDetail?: any - ) => void | boolean; + readonly [P in T['TActionName']]?: (finiteStateMachine: FsmConsumerInterface) => void | boolean; }; export type SignalConfig< TEventId extends string = string, - TActionName extends string = string, TContext extends StringifyableRecord = StringifyableRecord > = { signalId: string; + /** + * @default `No` + */ receivePrevious?: DebounceType; -} & ( - | { - transition: TEventId; - contextName?: keyof TContext; - actions?: never; - } - | { - actions: SingleOrArray; - transition?: never; - } -); + transition: TEventId; + contextName?: keyof TContext; + actions?: never; +}; // type helper export type FsmTypeHelper = Readonly<{ From 917069457630eecfa24c6fe83b7d34fb281a9d2d Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Fri, 17 Mar 2023 02:22:57 +0330 Subject: [PATCH 81/85] feat(demo/fsm): update with new api Co-authored-by: Mohammad Honarvar --- demo/finite-state-machine/light-machine.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/demo/finite-state-machine/light-machine.ts b/demo/finite-state-machine/light-machine.ts index cd828139c..d110b3081 100644 --- a/demo/finite-state-machine/light-machine.ts +++ b/demo/finite-state-machine/light-machine.ts @@ -90,6 +90,15 @@ finiteStateMachineProvider.defineActions('light_machine', { 'action_flashingRed_POWER_BACK': (): void => console.log('flashingRed.POWER_BACK actions called'), }); +finiteStateMachineProvider.defineSignals('light_machine', [ + { + signalId: 'new_content_received', + transition: 'POWER_BACK', + contextName: 'a', + receivePrevious: 'NextCycle', + }, +]); + // signals // 'action_ali_signal': (a): void => console.log('ali signal ', a), @@ -98,14 +107,19 @@ const lightMachineConsumer = finiteStateMachineConsumer('light_mac lightMachineConsumer.defineSignals([ { - signalId: 'ali', + signalId: 'power_button_click_event', transition: 'POWER_BACK', - // contextName: 'a', + receivePrevious: 'No', + }, + { + signalId: 'jafang', + callback: (signalDetail) => { + console.log(signalDetail); + }, + receivePrevious: 'NextCycle', }, ]); -lightMachineConsumer.subscribeSignals(); - console.log('start', lightMachineConsumer.getState()); // lightMachineConsumer From 47c22e92a8a8085148b44b316d649b695ff8071a Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Fri, 17 Mar 2023 02:47:59 +0330 Subject: [PATCH 82/85] feat(fsm): custom signal callback Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 37 ++++++++++++---------- core/fsm/src/type.ts | 27 ++++++++++++++-- demo/finite-state-machine/light-machine.ts | 2 +- 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index c495f2645..93aa72337 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -3,13 +3,14 @@ import {ListenerSpec, contextProvider, contextConsumer} from '@alwatr/signal'; import type { ActionRecord, + ConstructorSignalConfig, FsmConstructor, FsmConstructorConfig, FsmConsumerInterface, FsmInstance, FsmState, FsmTypeHelper, - SignalConfig, + InstanceSignalConfig, } from './type.js'; import type {OmitFirstParam, SingleOrArray, StringifyableRecord} from '@alwatr/type'; @@ -249,31 +250,33 @@ export const initFsmInstance = (instanceId: string, constructorId: string): void export const subscribeSignals = ( instanceId: string, - signalList: Array, + signalList: Array, subscribeConstructorSignals = true, ): Array => { logger.logMethodArgs('subscribeSignals', {instanceId, signalList}); const listenerList: Array = []; if (subscribeConstructorSignals) { - signalList = getFsmConstructor(getFsmInstance(instanceId).constructorId).signalList.concat(signalList); + signalList = signalList.concat(getFsmConstructor(getFsmInstance(instanceId).constructorId).signalList); } for (const signalConfig of signalList) { listenerList.push( contextConsumer.subscribe( signalConfig.signalId, - (signalDetail: StringifyableRecord): void => { - transition( - instanceId, - signalConfig.transition, - signalConfig.contextName - ? { - [signalConfig.contextName]: signalDetail, - } - : undefined, - ); - }, + signalConfig.callback + ? signalConfig.callback + : (signalDetail: StringifyableRecord): void => { + transition( + instanceId, + signalConfig.transition, + signalConfig.contextName + ? { + [signalConfig.contextName]: signalDetail, + } + : undefined, + ); + }, {receivePrevious: signalConfig.receivePrevious ?? 'No'}, ), ); @@ -292,7 +295,7 @@ export const subscribeSignals = ( export const defineConstructorSignals = ( constructorId: string, - signalList: Array>, + signalList: Array>, ): void => { logger.logMethodArgs('defineSignals', {constructorId, signalList: signalList}); const fsmConstructor = getFsmConstructor(constructorId); @@ -301,11 +304,11 @@ export const defineConstructorSignals = ( export const defineInstanceSignals = ( instanceId: string, - signalList: Array>, + signalList: Array>, subscribeConstructorSignals = true, ): Array => { logger.logMethodArgs('defineSignals', {instanceId, signals: signalList}); - return subscribeSignals(instanceId, signalList, subscribeConstructorSignals); + return subscribeSignals(instanceId, signalList as Array, subscribeConstructorSignals); }; export const render = ( diff --git a/core/fsm/src/type.ts b/core/fsm/src/type.ts index 10abef0c7..2607ac846 100644 --- a/core/fsm/src/type.ts +++ b/core/fsm/src/type.ts @@ -9,7 +9,7 @@ export interface FsmConstructor { readonly id: string; readonly config: FsmConstructorConfig; actionRecord: ActionRecord; - signalList: Array; + signalList: Array; } export interface FsmConstructorConfig< @@ -110,7 +110,7 @@ export type ActionRecord = { readonly [P in T['TActionName']]?: (finiteStateMachine: FsmConsumerInterface) => void | boolean; }; -export type SignalConfig< +export type ConstructorSignalConfig< TEventId extends string = string, TContext extends StringifyableRecord = StringifyableRecord > = { @@ -121,9 +121,30 @@ export type SignalConfig< receivePrevious?: DebounceType; transition: TEventId; contextName?: keyof TContext; - actions?: never; }; +export type InstanceSignalConfig< + TEventId extends string = string, + TContext extends StringifyableRecord = StringifyableRecord +> = { + signalId: string; + /** + * @default `No` + */ + receivePrevious?: DebounceType; +} & ( + | { + transition: TEventId; + contextName?: keyof TContext; + callback?: never; + } + | { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callback: (detail: any) => void; + transition?: never; + } +); + // type helper export type FsmTypeHelper = Readonly<{ TState: Exclude; diff --git a/demo/finite-state-machine/light-machine.ts b/demo/finite-state-machine/light-machine.ts index d110b3081..621eab5aa 100644 --- a/demo/finite-state-machine/light-machine.ts +++ b/demo/finite-state-machine/light-machine.ts @@ -113,7 +113,7 @@ lightMachineConsumer.defineSignals([ }, { signalId: 'jafang', - callback: (signalDetail) => { + callback: (signalDetail: Record): void => { console.log(signalDetail); }, receivePrevious: 'NextCycle', From 2af4f44f0e8a2dee39cde10dcaa3281075632e6a Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Fri, 17 Mar 2023 03:37:29 +0330 Subject: [PATCH 83/85] feat(fsm): subscribe Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 13 ++++++++++++- core/fsm/src/index.ts | 3 ++- core/fsm/src/type.ts | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index 93aa72337..22ae7a45d 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -1,5 +1,6 @@ import {createLogger, globalAlwatr} from '@alwatr/logger'; import {ListenerSpec, contextProvider, contextConsumer} from '@alwatr/signal'; +import {SubscribeOptions} from '@alwatr/signal/type.js'; import type { ActionRecord, @@ -263,7 +264,7 @@ export const subscribeSignals = ( for (const signalConfig of signalList) { listenerList.push( contextConsumer.subscribe( - signalConfig.signalId, + signalConfig.signalId ?? instanceId, signalConfig.callback ? signalConfig.callback : (signalDetail: StringifyableRecord): void => { @@ -330,6 +331,15 @@ export const render = ( return; }; +export const subscribe = ( + instanceId: string, + callback: () => void, + options?: Partial, +): ListenerSpec => { + logger.logMethodArgs('subscribe', instanceId); + return contextConsumer.subscribe(instanceId, callback, options); +}; + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export const finiteStateMachineConsumer = ( instanceId: string, @@ -350,6 +360,7 @@ export const finiteStateMachineConsumer = machineInstance?.constructorId ?? makeFromConstructor, render: render.bind(null, instanceId) as OmitFirstParam>, + subscribe: subscribe.bind(null, instanceId) as OmitFirstParam, getState: getState.bind(null, instanceId) as OmitFirstParam>, getContext: getContext.bind(null, instanceId) as OmitFirstParam>, setContext: setContext.bind(null, instanceId) as OmitFirstParam>, diff --git a/core/fsm/src/index.ts b/core/fsm/src/index.ts index 4f6f7edb0..e03c9fcb1 100644 --- a/core/fsm/src/index.ts +++ b/core/fsm/src/index.ts @@ -1,10 +1,11 @@ -import {defineActions, defineConstructor, defineConstructorSignals} from './core.js'; +import {defineActions, defineConstructor, defineConstructorSignals, subscribe} from './core.js'; export {finiteStateMachineConsumer} from './core.js'; export const finiteStateMachineProvider = { defineConstructor, defineActions, defineSignals: defineConstructorSignals, + subscribe, } as const; export type {FsmTypeHelper, FsmConstructorConfig} from './type.js'; diff --git a/core/fsm/src/type.ts b/core/fsm/src/type.ts index 2607ac846..d0c0f602f 100644 --- a/core/fsm/src/type.ts +++ b/core/fsm/src/type.ts @@ -127,7 +127,7 @@ export type InstanceSignalConfig< TEventId extends string = string, TContext extends StringifyableRecord = StringifyableRecord > = { - signalId: string; + signalId?: string; /** * @default `No` */ From 60804694ccab53b5c22ea636992f54ef1dde6a4b Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Fri, 17 Mar 2023 03:38:32 +0330 Subject: [PATCH 84/85] feat(demo/fsm): update Co-authored-by: Mohammad Honarvar --- demo/finite-state-machine/light-machine.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/demo/finite-state-machine/light-machine.ts b/demo/finite-state-machine/light-machine.ts index 621eab5aa..8ab860dba 100644 --- a/demo/finite-state-machine/light-machine.ts +++ b/demo/finite-state-machine/light-machine.ts @@ -1,4 +1,5 @@ import {finiteStateMachineConsumer, finiteStateMachineProvider, type FsmTypeHelper} from '@alwatr/fsm'; +import {delay} from '@alwatr/util'; // Provider const lightMachineConstructor = finiteStateMachineProvider.defineConstructor('light_machine', { @@ -118,15 +119,25 @@ lightMachineConsumer.defineSignals([ }, receivePrevious: 'NextCycle', }, + { + callback: (): void => { + console.log('subscribe_callback', lightMachineConsumer.getState()); + }, + receivePrevious: 'NextCycle', + }, ]); console.log('start', lightMachineConsumer.getState()); -// lightMachineConsumer - +await delay(1000); lightMachineConsumer.transition('TIMER', {a: 1}); +await delay(1000); lightMachineConsumer.transition('TIMER', {b: 2}); +await delay(1000); lightMachineConsumer.transition('TIMER'); +await delay(1000); lightMachineConsumer.transition('POWER_LOST', {a: 4}); +await delay(1000); lightMachineConsumer.transition('TIMER', {a: 5, b: 5}); +await delay(1000); lightMachineConsumer.transition('POWER_BACK', {a: 6}); From 772818baa7953b6fbb4d4128fcee76733f42cc2d Mon Sep 17 00:00:00 2001 From: Ali Mihandoost Date: Fri, 17 Mar 2023 14:48:24 +0330 Subject: [PATCH 85/85] feat(fsm): callback in provider signals Co-authored-by: Mohammad Honarvar --- core/fsm/src/core.ts | 25 ++++++++++++++----------- core/fsm/src/type.ts | 26 +++++--------------------- 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index 22ae7a45d..5c0e0517e 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -4,14 +4,13 @@ import {SubscribeOptions} from '@alwatr/signal/type.js'; import type { ActionRecord, - ConstructorSignalConfig, FsmConstructor, FsmConstructorConfig, FsmConsumerInterface, FsmInstance, FsmState, FsmTypeHelper, - InstanceSignalConfig, + SignalConfig, } from './type.js'; import type {OmitFirstParam, SingleOrArray, StringifyableRecord} from '@alwatr/type'; @@ -251,7 +250,7 @@ export const initFsmInstance = (instanceId: string, constructorId: string): void export const subscribeSignals = ( instanceId: string, - signalList: Array, + signalList: Array, subscribeConstructorSignals = true, ): Array => { logger.logMethodArgs('subscribeSignals', {instanceId, signalList}); @@ -265,9 +264,12 @@ export const subscribeSignals = ( listenerList.push( contextConsumer.subscribe( signalConfig.signalId ?? instanceId, - signalConfig.callback - ? signalConfig.callback - : (signalDetail: StringifyableRecord): void => { + (signalDetail: StringifyableRecord): void => { + if (signalConfig.callback) { + signalConfig.callback(signalDetail, finiteStateMachineConsumer(instanceId)); + } + else { + // prettier-ignore transition( instanceId, signalConfig.transition, @@ -277,7 +279,8 @@ export const subscribeSignals = ( } : undefined, ); - }, + } + }, {receivePrevious: signalConfig.receivePrevious ?? 'No'}, ), ); @@ -296,20 +299,20 @@ export const subscribeSignals = ( export const defineConstructorSignals = ( constructorId: string, - signalList: Array>, + signalList: Array>, ): void => { logger.logMethodArgs('defineSignals', {constructorId, signalList: signalList}); const fsmConstructor = getFsmConstructor(constructorId); - fsmConstructor.signalList = fsmConstructor.signalList.concat(signalList); + fsmConstructor.signalList = fsmConstructor.signalList.concat(signalList as Array); }; export const defineInstanceSignals = ( instanceId: string, - signalList: Array>, + signalList: Array>, subscribeConstructorSignals = true, ): Array => { logger.logMethodArgs('defineSignals', {instanceId, signals: signalList}); - return subscribeSignals(instanceId, signalList as Array, subscribeConstructorSignals); + return subscribeSignals(instanceId, signalList as Array, subscribeConstructorSignals); }; export const render = ( diff --git a/core/fsm/src/type.ts b/core/fsm/src/type.ts index d0c0f602f..d6b7b17e4 100644 --- a/core/fsm/src/type.ts +++ b/core/fsm/src/type.ts @@ -9,7 +9,7 @@ export interface FsmConstructor { readonly id: string; readonly config: FsmConstructorConfig; actionRecord: ActionRecord; - signalList: Array; + signalList: Array; } export interface FsmConstructorConfig< @@ -110,23 +110,7 @@ export type ActionRecord = { readonly [P in T['TActionName']]?: (finiteStateMachine: FsmConsumerInterface) => void | boolean; }; -export type ConstructorSignalConfig< - TEventId extends string = string, - TContext extends StringifyableRecord = StringifyableRecord -> = { - signalId: string; - /** - * @default `No` - */ - receivePrevious?: DebounceType; - transition: TEventId; - contextName?: keyof TContext; -}; - -export type InstanceSignalConfig< - TEventId extends string = string, - TContext extends StringifyableRecord = StringifyableRecord -> = { +export type SignalConfig = { signalId?: string; /** * @default `No` @@ -134,13 +118,13 @@ export type InstanceSignalConfig< receivePrevious?: DebounceType; } & ( | { - transition: TEventId; - contextName?: keyof TContext; + transition: T['TEventId']; + contextName?: keyof T['TContext']; callback?: never; } | { // eslint-disable-next-line @typescript-eslint/no-explicit-any - callback: (detail: any) => void; + callback: (detail: any, fsmInstance: FsmConsumerInterface) => void; transition?: never; } );